From cef3a47cca8137858b264ec73138724ea8b95561 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 14:43:53 +0200 Subject: [PATCH 0001/1208] Add optional eval runtime bridge --- .plans/elephc-eval-complete-plan.md | 987 ++++++++++++++++++ Cargo.lock | 5 + Cargo.toml | 10 +- crates/elephc-eval/Cargo.toml | 12 + crates/elephc-eval/src/abi.rs | 49 + crates/elephc-eval/src/context.rs | 50 + crates/elephc-eval/src/errors.rs | 47 + crates/elephc-eval/src/eval_ir.rs | 119 +++ crates/elephc-eval/src/interpreter.rs | 773 ++++++++++++++ crates/elephc-eval/src/lib.rs | 511 +++++++++ crates/elephc-eval/src/lower.rs | 15 + crates/elephc-eval/src/parser.rs | 819 +++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 195 ++++ crates/elephc-eval/src/scope.rs | 308 ++++++ crates/elephc-eval/src/value.rs | 38 + docs/internals/the-runtime.md | 8 + docs/php/system-and-io.md | 5 + examples/eval/.gitignore | 3 + examples/eval/main.php | 12 + src/codegen/frame.rs | 79 +- src/codegen/lower_inst/builtins.rs | 2 + src/codegen/lower_inst/builtins/eval.rs | 424 ++++++++ src/codegen_support/runtime/emitters.rs | 4 + src/codegen_support/runtime/eval_bridge.rs | 448 ++++++++ src/codegen_support/runtime/mod.rs | 7 +- src/codegen_support/runtime_features.rs | 22 + src/ir/function.rs | 1 + src/ir_lower/context.rs | 47 + src/ir_lower/expr/mod.rs | 22 + src/ir_lower/program.rs | 17 + src/linker.rs | 22 + src/types/checker/builtins/catalog.rs | 10 +- src/types/checker/builtins/mod.rs | 7 + src/types/checker/driver/init.rs | 1 + src/types/checker/driver/top_level.rs | 3 + src/types/checker/inference/expr/effects.rs | 23 + src/types/checker/inference/expr/mod.rs | 19 +- src/types/checker/mod.rs | 5 + src/types/checker/type_compat/declarations.rs | 3 + src/types/signatures.rs | 2 + tests/codegen/eval.rs | 319 ++++++ tests/codegen/mod.rs | 1 + tests/codegen/support/runner.rs | 26 +- tests/error_tests/misc/functions.rs | 9 + tests/error_tests/misc/system_builtins.rs | 6 + 45 files changed, 5464 insertions(+), 31 deletions(-) create mode 100644 .plans/elephc-eval-complete-plan.md create mode 100644 crates/elephc-eval/Cargo.toml create mode 100644 crates/elephc-eval/src/abi.rs create mode 100644 crates/elephc-eval/src/context.rs create mode 100644 crates/elephc-eval/src/errors.rs create mode 100644 crates/elephc-eval/src/eval_ir.rs create mode 100644 crates/elephc-eval/src/interpreter.rs create mode 100644 crates/elephc-eval/src/lib.rs create mode 100644 crates/elephc-eval/src/lower.rs create mode 100644 crates/elephc-eval/src/parser.rs create mode 100644 crates/elephc-eval/src/runtime_hooks.rs create mode 100644 crates/elephc-eval/src/scope.rs create mode 100644 crates/elephc-eval/src/value.rs create mode 100644 examples/eval/.gitignore create mode 100644 examples/eval/main.php create mode 100644 src/codegen/lower_inst/builtins/eval.rs create mode 100644 src/codegen_support/runtime/eval_bridge.rs create mode 100644 tests/codegen/eval.rs diff --git a/.plans/elephc-eval-complete-plan.md b/.plans/elephc-eval-complete-plan.md new file mode 100644 index 0000000000..54798dc9b1 --- /dev/null +++ b/.plans/elephc-eval-complete-plan.md @@ -0,0 +1,987 @@ +# Piano dettagliato: supporto completo a eval tramite libelephc-eval + +Nota percorso: la richiesta indicava `~/Downlaods`, che sembra un refuso. Questo file e' stato scritto in `~/Downloads`. + +## Obiettivo + +Implementare `eval($code)` completo per il sottoinsieme PHP supportato da elephc, senza imporre parser runtime, interprete o scope dinamico ai programmi che non usano `eval`. + +La soluzione proposta e': + +```text +programma senza eval + -> runtime elephc normale + -> nessuna lib eval + -> nessun interprete + -> stesse performance di oggi + +programma con eval + -> runtime elephc normale + -> + libelephc-eval linkata condizionalmente + -> solo gli scope che arrivano a eval pagano il costo dinamico +``` + +La strategia runtime per le funzioni che contengono `eval` e': + +```text +codice nativo prima di eval + -> resta nativo e ottimizzabile finche' non attraversa la barriera eval + +al punto eval + -> valuta l'argomento in source order + -> materializza lo scope se non esiste + -> flush dei locals vivi nello scope dinamico + -> chiama libelephc-eval + +libelephc-eval + -> parse della stringa PHP + -> lowering a EvalIR/EIR dinamico + -> interpretazione usando lo scope ricevuto + +dopo eval + -> invalida i locals che eval puo' aver letto/scritto/unset + -> ricarica lazy dai dynamic cells al prossimo uso +``` + +## Decisioni architetturali + +1. `libelephc-eval` e' una bridge staticlib, come `elephc_tls`, `elephc_pdo`, `elephc_crypto` e `elephc_phar`. +2. Il linker la include solo quando il programma contiene una chiamata PHP a `eval`. +3. Il backend attivo resta AST -> EIR -> `src/codegen_ir/`; non si estende il backend AST legacy. +4. Il codice statico resta nativo. Non si interpreta tutta la funzione salvo dove necessario. +5. `eval` e' una barriera di effetti totale per optimizer, type checker e lowering. +6. L'interprete eval non introduce un value system separato: lavora sugli stessi valori/celle runtime di elephc. +7. L'EIR statico non deve diventare completamente dinamico. Lato eval conviene introdurre un `EvalIR` o un sottoinsieme EIR dinamico con istruzioni esplicite di scope. +8. Nessun JIT nella prima versione: niente assembler/linker a runtime, niente `dlopen`, niente compilazione ARM64/x86 al volo. + +## Semantica PHP da preservare + +Da verificare con `php -r` e codificare in test: + +1. La stringa passata a `eval` non deve contenere tag ` true se c'e' una call PHP a eval dopo name resolution/case-insensitive builtin lookup +``` + +Required libraries: + +```rust +if features.eval { + libs.push("elephc_eval".to_string()); +} +``` + +Importante: `eval` deve essere rilevato anche se scritto con casing diverso, se PHP lo consente, e anche in presenza di namespace fallback. + +## ABI nativa verso libelephc-eval + +L'ABI deve essere piccola, stabile e target-aware. Non passare enum Rust non `repr(C)` attraverso il confine staticlib. + +Simboli minimi: + +```c +uint32_t __elephc_eval_abi_version(void); + +int32_t __elephc_eval_execute( + ElephcEvalContext *ctx, + ElephcEvalScope *scope, + const uint8_t *code_ptr, + uint64_t code_len, + ElephcEvalResult *out +); +``` + +Possibili codici ritorno: + +```text +0 = ok, out contiene il valore di ritorno di eval o null +1 = parse error +2 = runtime fatal +3 = uncaught throwable +4 = unsupported construct in eval subset +5 = ABI/runtime version mismatch +``` + +`ElephcEvalResult`: + +```c +typedef struct { + uint32_t kind; // normal, return, fatal, throw + void *value_cell; // Mixed/runtime cell, owned secondo contratto runtime + void *error; // optional runtime error/throwable object +} ElephcEvalResult; +``` + +Regole: + +1. Nessun panic Rust deve attraversare l'ABI. +2. Nessuna eccezione C++/unwind attraverso l'ABI. +3. Tutti i valori passati sono opaque handle o puntatori a celle runtime elephc. +4. Il codice generato deve controllare il codice ritorno e abbassarlo al comportamento PHP atteso. +5. L'ABI deve essere identica su `macos-aarch64`, `linux-aarch64`, `linux-x86_64`. + +## EvalContext + +`EvalContext` rappresenta lo stato globale richiesto da eval: + +```text +EvalContext + - ABI version / runtime version + - allocator / GC hooks + - global function table dinamica + - global class table dinamica + - global constants table dinamica + - builtin registry + - current file path + - current line + - current namespace metadata + - current class/function/method metadata + - diagnostics sink +``` + +Il codice nativo deve creare o ottenere un `EvalContext` per processo/program activation. Non va ricreato a ogni chiamata se contiene tabelle globali dinamiche. + +## Dynamic Scope + +Lo scope deve rappresentare celle PHP per nome: + +```text +ElephcEvalScope + - parent/global link opzionale + - map: string name -> RuntimeCell* + - flags per entry: + present + unset + dirty + by_ref + persistent/owned/borrowed + - generation counter +``` + +Ogni variabile locale materializzata diventa una cella: + +```text +$x static local + -> slot nativo corrente + -> cella scope "x" +``` + +Regola chiave: + +```text +il codice nativo puo' continuare a usare slot/registri statici, +ma al punto eval deve sincronizzare i locals osservabili nello scope dinamico. +``` + +## Flush / invalidation / reload + +### Prima di eval + +Il lowering della chiamata `eval($code)` deve: + +1. Valutare `$code` in ordine sorgente. +2. Convertire/coercire `$code` a stringa secondo PHP. +3. Creare lo scope materializzato della activation se non esiste. +4. Calcolare il set di locals vivi e osservabili. +5. Fare flush dei locals nello scope: + +```text +local slot x -> RuntimeCell scope["x"] +local slot y -> RuntimeCell scope["y"] +``` + +6. Passare `EvalContext`, `Scope`, `code_ptr`, `code_len` a `__elephc_eval_execute`. + +### Durante eval + +L'interprete legge e scrive solo lo scope: + +```text +LoadVar("x") -> scope_get("x") +StoreVar("x", v) -> scope_set("x", v) +UnsetVar("x") -> scope_unset("x") +``` + +### Dopo eval + +Il codice nativo non deve fidarsi dei valori statici precedenti. + +Strategia consigliata: + +1. Marcare invalidi tutti i locals flushati. +2. Se libelephc-eval restituisce una dirty set affidabile, invalidare almeno quella. +3. Se non c'e' dirty set o se eval usa variabili variabili, references, `unset`, `global`, invalidare tutto lo scope materializzato. +4. Al prossimo uso statico di `$x`, fare reload lazy: + +```text +if local x invalid: + if scope has "x": + slot x = scope_get("x") + else: + slot x = null/unset semantics secondo contesto +``` + +Questo mantiene il codice prima e dopo eval nativo, ma tratta eval come barriera forte. + +### Variabili create da eval + +Esempio: + +```php +eval('$newVar = 42;'); +echo $newVar; +``` + +Il compiler statico vede `$newVar` dopo eval, ma non ha una definizione precedente. + +Regola proposta: + +1. Dentro una funzione che contiene eval, ogni read di variabile dopo una barriera eval deve poter fare fallback allo scope dinamico. +2. Se la variabile e' nota staticamente e non invalidata, usare lo slot nativo. +3. Se e' unknown/created-by-eval, leggere da `scope_get_or_null`. +4. Il tipo statico dopo eval degrada a `Mixed` per le variabili che possono essere toccate da eval. + +### unset + +Esempio: + +```php +$x = 10; +eval('unset($x);'); +echo $x; +``` + +Lo scope deve poter distinguere: + +```text +present null +missing/unset +``` + +Non basta salvare `Null`, perche' PHP distingue variabile definita a `null` da variabile non definita per `isset`, warning e certi accessi. + +## Cambi type checker + +### Builtin/catalog + +Agganciare `eval` nei punti canonici: + +1. `src/types/checker/builtins/catalog.rs` +2. `src/types/signatures.rs` +3. categoria builtin appropriata sotto `src/types/checker/builtins/` +4. `first_class_callable_builtin_sig()` solo se si decide che `eval` e' callable; PHP lo tratta come language construct, quindi probabilmente non deve diventare first-class callable. +5. `function_exists("eval")` va verificato contro PHP e allineato alla policy scelta. Se PHP restituisce false per language constructs, non inserirlo nel catalogo dei callable normali senza un'eccezione. + +Firma semantica: + +```text +eval(string $code): mixed +``` + +Ma internamente va modellata come language construct: + +```text +reads/writes locals +may define functions/classes/constants +may output +may throw/fatal +may return a value +is never pure +``` + +### Dynamic barrier + +Aggiungere una nozione nel checker: + +```text +FunctionDynamicState + contains_eval: bool + eval_barrier_seen: bool per blocco/control-flow +``` + +Dopo una barriera eval: + +1. I tipi dei locals osservabili diventano `Mixed` o `MaybeUnset`. +2. Le chiamate a funzioni/classi non note potrebbero essere permesse come lookup dinamico se il control-flow ha attraversato un eval che puo' averle dichiarate. +3. Warning/diagnostiche devono evitare falsi positivi su variabili create da eval. + +## Cambi optimizer/effects + +In `src/optimize/effects/`: + +```text +eval effects: + - reads all visible locals + - writes all visible locals + - may unset locals + - may define global functions/classes/constants + - may read/write globals + - may allocate heap + - may output + - may throw/fatal + - may call arbitrary functions +``` + +Regole: + +1. Non eliminare mai `eval`, anche se il risultato non e' usato. +2. Non propagare costanti attraverso `eval` per variabili visibili. +3. Non riordinare side effects attraverso `eval`. +4. Non foldare `eval` come stringa statica nella prima implementazione completa, per evitare due semantiche divergenti. Eventuale AOT static-eval puo' essere una ottimizzazione successiva. +5. Il DCE deve trattare eval come una call con effetti massimi. + +## Cambi IR / lowering + +### Nel programma statico + +Il codice statico non deve usare istruzioni dinamiche ovunque. Aggiungere solo cio' che serve per la barriera: + +```text +EnsureMaterializedScope +FlushLocalToScope(name, local) +CallEvalRuntime(scope, code) +InvalidateScopeLocals(scope) +ReloadLocalFromScope(name, local) +``` + +Queste possono essere: + +1. istruzioni EIR nuove, se servono al backend; +2. oppure lowering diretto in `src/ir_lower/expr/` verso call runtime + metadata; +3. oppure metadata sulla call builtin `eval` che `src/codegen_ir/` abbassa target-aware. + +Preferenza: + +```text +EIR statico esplicito abbastanza da validare ownership e frame layout, +ma senza trasformare tutte le variabili normali in lookup dinamici. +``` + +### Nel runtime eval + +Usare `EvalIR`, non necessariamente l'EIR completo: + +```text +EvalIR + ConstNull + ConstBool + ConstInt + ConstFloat + ConstString + LoadVar(name) + StoreVar(name, value) + UnsetVar(name) + LoadGlobal(name) + BinaryOp(op, left, right) + UnaryOp(op, value) + Echo(value) + Return(value) + CallDynamic(name, args) + If(...) + While(...) + Foreach(...) + ArrayGet/ArraySet + PropertyGet/PropertySet + NewObject + DefineFunction + DefineClass +``` + +Motivo: l'EIR statico e' ottimizzato per frame/locals noti; eval ha bisogno di operazioni by-name e lookup dinamico. + +## Runtime value bridge + +Non creare: + +```rust +enum Value { + Null, + Bool(bool), + Int(i64), + ... +} +``` + +come rappresentazione autonoma se poi il runtime nativo usa un'altra ABI. + +Fare invece: + +```text +EvalValue = handle/cell runtime elephc +``` + +Il bridge deve offrire operazioni: + +```text +value_null() +value_bool(bool) +value_int(i64) +value_float(f64) +value_string(ptr,len) +value_add(a,b) +value_concat(a,b) +value_truthy(v) +value_to_string(v) +value_release(v) +value_retain(v) +array_get/array_set with COW +object_get/object_set +``` + +Queste operazioni devono rispettare: + +1. boxed `Mixed` contract; +2. refcount; +3. ownership temporanei; +4. borrowed vs owned; +5. copy-on-write array/string; +6. cleanup su normal return, fatal, throw. + +## Dynamic function/class tables + +Eval completo richiede tabelle dinamiche globali. + +Esempio: + +```php +eval('function dyn() { return 42; }'); +echo dyn(); +``` + +Il codice statico dopo eval potrebbe chiamare una funzione non nota al compile-time. + +Serve: + +```text +DynamicFunctionTable + name -> EvalFunction + +DynamicClassTable + name -> EvalClass +``` + +Per chiamate statiche a simboli non risolti dopo eval: + +1. Il checker deve permettere lookup dinamico se il path di controllo puo' aver attraversato eval. +2. Il lowering deve generare `__rt_dynamic_call("dyn", args)` invece di direct symbol call. +3. `__rt_dynamic_call` cerca prima funzioni native note, poi funzioni eval-defined. +4. Le funzioni definite da eval possono essere interpretate da libelephc-eval. + +Per chiamate note staticamente: + +```text +foo(); // foo definita nel programma AOT +``` + +continuare a generare direct call quando possibile. + +## Name resolution e namespace + +Eval deve ricevere metadata del punto chiamante: + +```text +current namespace +current file +current line +current class +current function +current method +``` + +Pero' funzioni e classi dichiarate dentro eval vanno verificate contro PHP. Il test locale mostra che una funzione dichiarata da eval dentro `namespace A` risulta globale: + +```text +function_exists("A\\f_eval_test") -> false +function_exists("f_eval_test") -> true +``` + +Da trasformare in test prima di fissare l'implementazione. + +## Codegen target-aware + +Tutto il lowering verso la call ABI deve stare nel percorso EIR attivo: + +```text +src/ir_lower/expr/ +src/codegen_ir/lower_inst/ +src/codegen/abi/ +``` + +Regole: + +1. Non hardcodare registri ARM64 o x86_64 fuori dagli helper ABI. +2. La call a `__elephc_eval_execute` deve passare per gli helper target-aware. +3. Frame slots per scope handle, code string e result devono essere allocati prima del frame sizing. +4. Ogni `emitter.instruction(...)` aggiunta deve avere commento allineato secondo policy. +5. Coprire `macos-aarch64`, `linux-aarch64`, `linux-x86_64`. + +## Pipeline di implementazione + +### Fase 0: spike semantico + +Obiettivo: fissare comportamento PHP prima del codice. + +Deliverable: + +1. File di note o test fixture con output PHP per: + - variable read/write; + - variable creation; + - unset; + - return from eval; + - parse error; + - output; + - function declaration; + - class declaration; + - namespace interaction; + - `$this` in method scope; + - by-ref variables; + - global/static variables. +2. Decidere se il supporto iniziale e' "eval completo per tutto il sottoinsieme elephc" o "eval completo PHP" piu' ampio del compilatore statico. + +### Fase 1: link condizionale e stub ABI + +Obiettivo: linkare `libelephc-eval` solo quando serve. + +Modifiche: + +1. Aggiungere crate `crates/elephc-eval`. +2. Aggiungere `elephc_eval` a `BRIDGES` in `src/linker.rs`. +3. Aggiungere `RuntimeFeatures.eval`. +4. Aggiungere detection `program_requires_eval`. +5. Aggiungere builtin/language construct `eval` al checker. +6. Generare una call a `__elephc_eval_execute` che per ora ritorna un errore controllato. + +Test: + +1. Programma senza eval non linka `elephc_eval`. +2. Programma con eval linka `elephc_eval`. +3. Test linker per `ELEPHC_EVAL_LIB_DIR`. +4. Errore chiaro se la libreria non e' trovata. + +### Fase 2: MaterializedScope minimo + +Obiettivo: avere uno scope dinamico passabile alla lib, ancora senza parser completo. + +Modifiche: + +1. Rappresentare `ElephcEvalScope`. +2. Aggiungere helper runtime per scope: + - create/destroy activation scope; + - set cell by name; + - get cell by name; + - unset by name; + - mark dirty; + - generation counter. +3. Nel lowering, creare scope handle per funzioni con eval. +4. Al punto eval, flush dei locals vivi. +5. Dopo eval, invalidazione totale dei locals flushati. +6. Reload lazy al prossimo uso. + +Test: + +1. Stub eval modifica `$x` via scope e il codice nativo dopo eval vede il nuovo valore. +2. Stub eval crea `$y` e `echo $y` dopo eval funziona. +3. Stub eval unsetta `$x` e il codice nativo vede lo stato unset. + +Nota: gli stub devono essere test-only o nascosti, non comportamento PHP pubblico. + +### Fase 3: parser runtime e fragment parsing + +Obiettivo: parse della stringa eval. + +Opzioni: + +1. Estrarre lexer/parser in crate riusabile, ad esempio `crates/elephc-frontend`. +2. Oppure duplicare temporaneamente solo il parser necessario in `crates/elephc-eval`, ma e' sconsigliato. + +Regole: + +1. `eval` parse-a statement fragment senza tag `x = $this->x + 1;'); + } +} +$a = new A(); +$a->bump(); +echo $a->x; +``` + +### Fase 8: references, globals, static locals + +Obiettivo: chiudere le parti piu' sensibili dello scope PHP. + +Implementare: + +1. references/by-ref nello scope; +2. `global $x` dentro eval; +3. `static $x` se supportato dal compilatore; +4. variabili variabili `${$name}` se supportate; +5. superglobals; +6. closure capture interaction se supportata. + +Test: + +1. modifica by-ref dentro eval; +2. `global` dentro eval modifica global esterno; +3. `unset` su reference; +4. nested eval con scope condiviso. + +### Fase 9: errori, throwable, cleanup + +Obiettivo: ownership e control flow robusti. + +Implementare: + +1. parse error; +2. fatal runtime; +3. exception/throw dentro eval se supportato; +4. cleanup temporanei su return/fatal/throw; +5. GC/refcount stress tests. + +Test: + +1. parse error catchable come PHP supportato; +2. throw dentro eval attraversa chiamante; +3. temporanei refcounted non leakano; +4. array COW dopo eccezione. + +### Fase 10: ottimizzazione performance + +Solo dopo correttezza. + +Possibili ottimizzazioni: + +1. Scope materializzato lazy: creare solo alla prima eval call eseguita. +2. Flush selettivo con liveness precisa. +3. Dirty set dalla lib per reload selettivo. +4. Versioned cells: reload solo se generation cambia. +5. Cache parse/lower per stringhe eval ripetute identiche. +6. Interning nomi variabili nello scope. +7. Fast path per eval senza writes esterni, se l'analisi runtime lo prova. + +Non fare queste ottimizzazioni prima dei test semantici. + +## Strategia test + +### Test unitari compiler + +1. `RuntimeFeatures.eval` detection. +2. Link required libs. +3. Builtin/catalog/signature. +4. Effects: eval non pure, non DCE. +5. Type invalidation after eval. + +### Test codegen end-to-end + +In `tests/codegen/eval.rs` o modulo dedicato: + +1. read local; +2. write local; +3. create local; +4. unset local; +5. return value; +6. output; +7. parse error; +8. dynamic function declaration; +9. dynamic function call after eval; +10. class/method `$this`; +11. arrays and COW; +12. nested eval; +13. eval inside loop; +14. eval in branch; +15. eval in function and top-level. + +### Test error + +In `tests/error_tests/`: + +1. wrong argument count; +2. non-string coercion behavior; +3. unsupported construct inside eval, se il sottoinsieme non lo supporta ancora; +4. duplicate function/class declaration; +5. parse error formatting. + +### Test cross-target + +Per ogni fase che tocca ABI/codegen/runtime: + +```bash +cargo test --test codegen_tests eval +./scripts/test-linux-x86_64.sh eval +./scripts/test-linux-arm64.sh eval +``` + +Prima del merge completo: + +```bash +cargo build +cargo test eval +cargo test +cargo test -- --include-ignored +git diff --check +``` + +## Rischi principali + +1. Scope sync incompleta: bug sottili in variabili create/unset dopo eval. +2. Value model duplicato: se EvalIR usa valori propri, COW/refcount diventano fragili. +3. Dynamic declarations: `eval('function f(){}'); f();` richiede fallback dinamico nel codice statico. +4. Type checker troppo statico: potrebbe rifiutare codice PHP valido dopo eval. +5. Optimizer troppo aggressivo: const propagation/DCE possono miscompilare attraversando eval. +6. ABI instabile tra compiler/runtime eval. +7. Parser runtime troppo accoppiato al compiler CLI. +8. File size e responsabilita': evitare un unico file gigante `interpreter.rs` che contiene parser, scope, lowering e runtime calls. + +## Criteri di completamento + +La feature e' considerata completa solo quando: + +1. Programmi senza eval non linkano `elephc_eval`. +2. Programmi senza eval non cambiano assembly/performance in modo osservabile. +3. Programmi con eval linkano `elephc_eval` automaticamente. +4. Le funzioni con eval usano flush/invalidate/reload corretto. +5. Eval modifica, crea e unsetta variabili dello scope chiamante. +6. `return` dentro eval ritorna il valore di `eval`. +7. Funzioni/classi dichiarate dentro eval sono richiamabili secondo semantica PHP. +8. Ownership/GC/COW sono coperti da test. +9. Tutti i target supportati passano i test eval. +10. Docs PHP e internals descrivono chiaramente che eval abilita un runtime dinamico opzionale. + +## Prima implementazione consigliata + +Sequenza pragmatica: + +1. Link condizionale `elephc_eval` + stub ABI. +2. `contains_eval` + lowering call runtime nel backend EIR. +3. `MaterializedScope` + flush/invalidate/reload con stub test-only. +4. Parser runtime per fragment eval. +5. EvalIR base: scalari, variabili, assign, add/concat, echo, return. +6. Arrays/control flow. +7. Dynamic calls. +8. Declarations. +9. Objects/classes. +10. References/global/static. +11. Performance pass. + +Questa sequenza tiene separati i rischi: prima si dimostra che lo scope condiviso funziona, poi si rende reale l'interprete eval. diff --git a/Cargo.lock b/Cargo.lock index 24407fff5d..a53bf4415b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,6 +583,7 @@ dependencies = [ "bitflags 2.13.0", "bzip2-rs", "elephc-crypto", + "elephc-eval", "elephc-image", "elephc-pdo", "elephc-phar", @@ -615,6 +616,10 @@ dependencies = [ "whirlpool", ] +[[package]] +name = "elephc-eval" +version = "0.1.0" + [[package]] name = "elephc-image" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2b3d71d6e7..7fdc1250a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,11 +17,12 @@ categories = ["compilers", "command-line-utilities"] # (hash/HMAC family), libelephc_phar.a (runtime phar:// archive reads), # libelephc_tz.a (DateTimeZone introspection: getLocation/getTransitions/ # listAbbreviations), libelephc_image.a (GD/Exif/Imagick/Gmagick/Cairo), and -# libelephc_web.a (--web prefork HTTP server) into target//, where -# linker.rs locates them for programs that use those features. +# libelephc_web.a (--web prefork HTTP server), and libelephc_eval.a (optional +# eval runtime) into target//, where linker.rs locates them for +# programs that use those features. [workspace] -members = [".", "crates/elephc-tls", "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", "crates/elephc-tz", "crates/elephc-image", "crates/elephc-web"] -default-members = [".", "crates/elephc-tls", "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", "crates/elephc-tz", "crates/elephc-image", "crates/elephc-web"] +members = [".", "crates/elephc-tls", "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", "crates/elephc-tz", "crates/elephc-image", "crates/elephc-web", "crates/elephc-eval"] +default-members = [".", "crates/elephc-tls", "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", "crates/elephc-tz", "crates/elephc-image", "crates/elephc-web", "crates/elephc-eval"] resolver = "2" [[bin]] @@ -60,6 +61,7 @@ elephc-phar = { path = "crates/elephc-phar" } elephc-tz = { path = "crates/elephc-tz" } elephc-image = { path = "crates/elephc-image" } elephc-web = { path = "crates/elephc-web" } +elephc-eval = { path = "crates/elephc-eval" } # flate2 lets the phar:// codegen tests build gzip (raw-DEFLATE) fixture entries # with the same encoder the compiler decodes, guaranteeing a version-stable # round-trip without shelling out to php. diff --git a/crates/elephc-eval/Cargo.toml b/crates/elephc-eval/Cargo.toml new file mode 100644 index 0000000000..f74ea88cd4 --- /dev/null +++ b/crates/elephc-eval/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "elephc-eval" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "Optional eval bridge staticlib for elephc compiled PHP programs" +publish = false + +[lib] +crate-type = ["staticlib", "rlib"] + +[dependencies] diff --git a/crates/elephc-eval/src/abi.rs b/crates/elephc-eval/src/abi.rs new file mode 100644 index 0000000000..e76b6dca6a --- /dev/null +++ b/crates/elephc-eval/src/abi.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Defines the C-compatible eval bridge ABI structs and version constant. +//! Keeps opaque runtime handles separate from Rust implementation details. +//! +//! Called from: +//! - `crate::__elephc_eval_abi_version()` +//! - `crate::__elephc_eval_execute()` +//! +//! Key details: +//! - C-visible result structs are `#[repr(C)]`; context/scope cross the ABI as +//! opaque pointers whose internal Rust layout is not exposed. +//! - Runtime values cross this boundary as opaque cell pointers, not Rust enums. + +use std::ffi::c_void; + +pub use crate::context::ElephcEvalContext; +pub use crate::scope::ElephcEvalScope; + +/// ABI version shared by generated call sites and the eval bridge. +pub const ABI_VERSION: u32 = 1; + +/// Scope-entry ABI flag indicating that a variable has a visible value. +pub const SCOPE_FLAG_PRESENT: u32 = 1 << 0; +/// Scope-entry ABI flag indicating that a variable has been unset. +pub const SCOPE_FLAG_UNSET: u32 = 1 << 1; +/// Scope-entry ABI flag indicating that native code must resynchronize this entry. +pub const SCOPE_FLAG_DIRTY: u32 = 1 << 2; +/// Scope-entry ABI flag indicating that the scope entry is by-reference. +pub const SCOPE_FLAG_BY_REF: u32 = 1 << 3; +/// Scope-entry ABI flag indicating that the scope owns the runtime cell handle. +pub const SCOPE_FLAG_OWNED: u32 = 1 << 4; + +/// Result storage written by `__elephc_eval_execute`. +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct ElephcEvalResult { + pub kind: u32, + pub value_cell: *mut c_void, + pub error: *mut c_void, +} + +impl ElephcEvalResult { + /// Resets result storage to the normal-null placeholder used by the stub. + pub fn clear(&mut self) { + self.kind = 0; + self.value_cell = std::ptr::null_mut(); + self.error = std::ptr::null_mut(); + } +} diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs new file mode 100644 index 0000000000..d4ac46ae08 --- /dev/null +++ b/crates/elephc-eval/src/context.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Declares the opaque process-level eval context handle. +//! The full implementation will hold dynamic function, class, constant, and +//! builtin registries plus runtime hooks. +//! +//! Called from: +//! - `crate::abi` +//! - `crate::__elephc_eval_execute()` +//! +//! Key details: +//! - The handle is intentionally opaque to generated code. +//! - No Rust-owned layout is promised across the C ABI. + +use crate::abi::ABI_VERSION; + +/// Process-level eval context passed opaquely across the C ABI. +/// +/// Generated code never inspects this layout directly; it only passes pointers +/// back to the eval bridge. Keeping a concrete Rust type here lets the bridge +/// grow dynamic registries without exposing them to generated assembly. +pub struct ElephcEvalContext { + abi_version: u32, +} + +impl ElephcEvalContext { + /// Creates a context using the current eval bridge ABI version. + pub const fn new() -> Self { + Self { + abi_version: ABI_VERSION, + } + } + + /// Creates a context with an explicit ABI version for compatibility tests. + #[cfg(test)] + pub const fn for_abi_version(abi_version: u32) -> Self { + Self { abi_version } + } + + /// Returns the ABI version this context was created for. + pub const fn abi_version(&self) -> u32 { + self.abi_version + } +} + +impl Default for ElephcEvalContext { + /// Creates the default process-level eval context. + fn default() -> Self { + Self::new() + } +} diff --git a/crates/elephc-eval/src/errors.rs b/crates/elephc-eval/src/errors.rs new file mode 100644 index 0000000000..ffd0c9c438 --- /dev/null +++ b/crates/elephc-eval/src/errors.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Defines stable integer status codes returned by the eval bridge ABI. +//! Keeps Rust error shapes internal while exposing C-compatible outcomes. +//! +//! Called from: +//! - `crate::__elephc_eval_execute()` +//! +//! Key details: +//! - Numeric values are part of the ABI contract and must remain stable. + +/// Stable eval bridge status codes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalStatus { + Ok, + ParseError, + RuntimeFatal, + UncaughtThrowable, + UnsupportedConstruct, + AbiMismatch, +} + +impl EvalStatus { + /// Returns the C ABI integer code for this status. + pub const fn code(self) -> i32 { + match self { + Self::Ok => 0, + Self::ParseError => 1, + Self::RuntimeFatal => 2, + Self::UncaughtThrowable => 3, + Self::UnsupportedConstruct => 4, + Self::AbiMismatch => 5, + } + } +} + +/// Parse failures detected before lowering a runtime eval fragment. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EvalParseError { + PhpOpenTag, + InvalidUtf8, + UnexpectedToken, + UnexpectedEof, + InvalidNumber, + UnterminatedString, + ExpectedVariable, + ExpectedSemicolon, +} diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs new file mode 100644 index 0000000000..97738e26be --- /dev/null +++ b/crates/elephc-eval/src/eval_ir.rs @@ -0,0 +1,119 @@ +//! Purpose: +//! Defines the dynamic EvalIR used by runtime `eval()` fragments. +//! EvalIR models by-name variable operations and expression shape without +//! introducing an independent runtime value representation. +//! +//! Called from: +//! - `crate::parser::parse_fragment()` +//! - Future `crate::interpreter` execution. +//! +//! Key details: +//! - Runtime execution must turn constants into elephc runtime cells through +//! value-bridge hooks; EvalIR constants are syntax data, not owned PHP values. + +/// Parsed eval fragment lowered into dynamic by-name statements. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalProgram { + source_len: usize, + statements: Vec, +} + +impl EvalProgram { + /// Creates an EvalIR program for a source fragment and statement list. + pub fn new(source_len: usize, statements: Vec) -> Self { + Self { + source_len, + statements, + } + } + + /// Returns the byte length of the parsed eval fragment. + pub const fn source_len(&self) -> usize { + self.source_len + } + + /// Returns the ordered EvalIR statements in source order. + pub fn statements(&self) -> &[EvalStmt] { + &self.statements + } + + /// Consumes the program and returns its statement list. + pub fn into_statements(self) -> Vec { + self.statements + } +} + +/// Dynamic eval statements that operate on a materialized activation scope. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalStmt { + ArraySetVar { + name: String, + index: EvalExpr, + value: EvalExpr, + }, + Break, + Continue, + Echo(EvalExpr), + If { + condition: EvalExpr, + then_branch: Vec, + else_branch: Vec, + }, + Return(Option), + StoreVar { + name: String, + value: EvalExpr, + }, + UnsetVar { + name: String, + }, + While { + condition: EvalExpr, + body: Vec, + }, + Expr(EvalExpr), +} + +/// Dynamic eval expressions evaluated by the interpreter against runtime cells. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalExpr { + Array(Vec), + ArrayGet { + array: Box, + index: Box, + }, + Const(EvalConst), + LoadVar(String), + Print(Box), + Binary { + op: EvalBinOp, + left: Box, + right: Box, + }, +} + +/// One element in a PHP array literal parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalArrayElement { + Value(EvalExpr), + KeyValue { key: EvalExpr, value: EvalExpr }, +} + +/// Literal syntax supported by the initial EvalIR parser. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalConst { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), +} + +/// Binary operations supported by the initial EvalIR parser. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalBinOp { + Add, + Sub, + Mul, + Concat, +} diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs new file mode 100644 index 0000000000..12d363fabf --- /dev/null +++ b/crates/elephc-eval/src/interpreter.rs @@ -0,0 +1,773 @@ +//! Purpose: +//! Interprets EvalIR against a materialized caller scope. +//! The interpreter is generic over runtime value operations so it can execute +//! by manipulating opaque elephc runtime-cell handles. +//! +//! Called from: +//! - Future `crate::__elephc_eval_execute()` implementation. +//! - `cargo test -p elephc-eval` for scope/value-flow validation. +//! +//! Key details: +//! - This module does not own PHP values. Constants and operations are delegated +//! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. + +use crate::errors::EvalStatus; +use crate::eval_ir::{EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalProgram, EvalStmt}; +use crate::scope::{ElephcEvalScope, ScopeCellOwnership}; +use crate::value::RuntimeCellHandle; + +/// Internal statement-control result used to propagate eval returns and loops. +enum EvalControl { + None, + Return(RuntimeCellHandle), + Break, + Continue, +} + +/// Runtime value hooks required by the EvalIR interpreter. +pub trait RuntimeValueOps { + /// Creates a runtime indexed-array cell with room for at least `capacity` elements. + fn array_new(&mut self, capacity: usize) -> Result; + + /// Creates a runtime associative-array cell with room for at least `capacity` elements. + fn assoc_new(&mut self, capacity: usize) -> Result; + + /// Reads one element from a runtime array-like Mixed cell using an index expression. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result; + + /// Writes one element to a runtime array-like Mixed cell and returns the target cell. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result; + + /// Returns whether a runtime cell can be indexed like an array by eval writes. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result; + + /// Releases one owned runtime cell that is no longer held by the eval scope. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; + + /// Creates a runtime null cell. + fn null(&mut self) -> Result; + + /// Creates a runtime bool cell. + fn bool_value(&mut self, value: bool) -> Result; + + /// Creates a runtime int cell. + fn int(&mut self, value: i64) -> Result; + + /// Creates a runtime float cell. + fn float(&mut self, value: f64) -> Result; + + /// Creates a runtime string cell. + fn string(&mut self, value: &str) -> Result; + + /// Adds two runtime cells using PHP addition semantics. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Subtracts two runtime cells using PHP numeric semantics. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Multiplies two runtime cells using PHP numeric semantics. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Concatenates two runtime cells using PHP string conversion semantics. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Emits one runtime cell to stdout using PHP echo semantics. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; + + /// Converts one runtime cell to PHP boolean truthiness. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result; +} + +/// Executes an EvalIR program and returns the eval result cell. +pub fn execute_program( + program: &EvalProgram, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_statements(program.statements(), scope, values)? { + EvalControl::None => values.null(), + EvalControl::Return(result) => Ok(result), + EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Executes statements in source order and propagates the first eval `return`. +fn execute_statements( + statements: &[EvalStmt], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + for stmt in statements { + match execute_stmt(stmt, scope, values)? { + EvalControl::None => {} + control => return Ok(control), + } + } + Ok(EvalControl::None) +} + +/// Executes one statement and returns `Some` only for eval `return`. +fn execute_stmt( + stmt: &EvalStmt, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match stmt { + EvalStmt::ArraySetVar { name, index, value } => { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some(existing) = + scope.entry(name).filter(|entry| entry.flags().is_visible()) + { + if values.is_array_like(existing.cell())? { + ownership = existing.flags().ownership; + existing.cell() + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_expr(index, scope, values)?; + let value = eval_expr(value, scope, values)?; + let array = values.array_set(array, index, value)?; + if let Some(replaced) = scope.set(name.clone(), array, ownership) { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::Break => Ok(EvalControl::Break), + EvalStmt::Continue => Ok(EvalControl::Continue), + EvalStmt::Echo(expr) => { + let value = eval_expr(expr, scope, values)?; + values.echo(value)?; + Ok(EvalControl::None) + } + EvalStmt::If { + condition, + then_branch, + else_branch, + } => { + let condition = eval_expr(condition, scope, values)?; + if values.truthy(condition)? { + execute_statements(then_branch, scope, values) + } else { + execute_statements(else_branch, scope, values) + } + } + EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr(expr, scope, values)?)), + EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), + EvalStmt::StoreVar { name, value } => { + let value = eval_expr(value, scope, values)?; + if let Some(replaced) = scope.set(name.clone(), value, ScopeCellOwnership::Owned) { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::UnsetVar { name } => { + if let Some(replaced) = scope.unset(name.clone()) { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::While { condition, body } => { + while { + let condition = eval_expr(condition, scope, values)?; + values.truthy(condition)? + } { + match execute_statements(body, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) + } + EvalStmt::Expr(expr) => { + let _ = eval_expr(expr, scope, values)?; + Ok(EvalControl::None) + } + } +} + +/// Evaluates one expression to an opaque runtime-cell handle. +fn eval_expr( + expr: &EvalExpr, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match expr { + EvalExpr::Array(elements) => { + if elements + .iter() + .any(|element| matches!(element, EvalArrayElement::KeyValue { .. })) + { + eval_assoc_array(elements, scope, values) + } else { + eval_indexed_array(elements, scope, values) + } + } + EvalExpr::ArrayGet { array, index } => { + let array = eval_expr(array, scope, values)?; + let index = eval_expr(index, scope, values)?; + values.array_get(array, index) + } + EvalExpr::Const(value) => eval_const(value, values), + EvalExpr::LoadVar(name) => scope.visible_cell(name).map_or_else(|| values.null(), Ok), + EvalExpr::Print(inner) => { + let value = eval_expr(inner, scope, values)?; + values.echo(value)?; + values.int(1) + } + EvalExpr::Binary { op, left, right } => { + let left = eval_expr(left, scope, values)?; + let right = eval_expr(right, scope, values)?; + match op { + EvalBinOp::Add => values.add(left, right), + EvalBinOp::Sub => values.sub(left, right), + EvalBinOp::Mul => values.mul(left, right), + EvalBinOp::Concat => values.concat(left, right), + } + } + } +} + +/// Evaluates an indexed array literal into a boxed runtime Mixed array. +fn eval_indexed_array( + elements: &[EvalArrayElement], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array = values.array_new(elements.len())?; + for (index, element) in elements.iter().enumerate() { + let EvalArrayElement::Value(element) = element else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let index = values.int(index as i64)?; + let value = eval_expr(element, scope, values)?; + let _ = values.array_set(array, index, value)?; + } + Ok(array) +} + +/// Evaluates an associative array literal into a boxed runtime Mixed hash. +fn eval_assoc_array( + elements: &[EvalArrayElement], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array = values.assoc_new(elements.len())?; + let mut next_index = 0; + for element in elements { + let (key, value) = match element { + EvalArrayElement::Value(value) => { + let key = values.int(next_index)?; + next_index += 1; + (key, value) + } + EvalArrayElement::KeyValue { key, value } => { + let key = eval_expr(key, scope, values)?; + (key, value) + } + }; + let value = eval_expr(value, scope, values)?; + let _ = values.array_set(array, key, value)?; + } + Ok(array) +} + +/// Converts one EvalIR constant into a runtime-cell handle. +fn eval_const( + value: &EvalConst, + values: &mut impl RuntimeValueOps, +) -> Result { + match value { + EvalConst::Null => values.null(), + EvalConst::Bool(value) => values.bool_value(*value), + EvalConst::Int(value) => values.int(*value), + EvalConst::Float(value) => values.float(*value), + EvalConst::String(value) => values.string(value), + } +} + +/// Returns the current interpreter availability status for the ABI stub. +pub fn current_stub_status() -> EvalStatus { + EvalStatus::UnsupportedConstruct +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::parser::parse_fragment; + use crate::value::RuntimeCell; + + use super::*; + + /// Test-only array key representation for fake indexed and associative arrays. + #[derive(Clone, Debug, PartialEq, Eq, Hash)] + enum FakeKey { + Int(i64), + String(String), + } + + /// Test-only runtime value representation used behind opaque cell handles. + #[derive(Clone, Debug, PartialEq)] + enum FakeValue { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), + Array(Vec), + Assoc(HashMap), + } + + /// Test runtime hooks that allocate stable fake handles and record echo output. + #[derive(Default)] + struct FakeOps { + next_id: usize, + values: HashMap, + output: String, + releases: Vec, + } + + impl FakeOps { + /// Allocates one fake runtime cell and returns its opaque handle. + fn alloc(&mut self, value: FakeValue) -> RuntimeCellHandle { + self.next_id += 1; + let id = self.next_id; + self.values.insert(id, value); + RuntimeCellHandle::from_raw(id as *mut RuntimeCell) + } + + /// Reads a fake runtime cell by opaque handle. + fn get(&self, handle: RuntimeCellHandle) -> FakeValue { + let id = handle.as_ptr() as usize; + self.values.get(&id).cloned().expect("fake cell missing") + } + + /// Converts a fake runtime cell into a fake PHP array key. + fn key(&self, handle: RuntimeCellHandle) -> Result { + match self.get(handle) { + FakeValue::Int(value) => Ok(FakeKey::Int(value)), + FakeValue::String(value) => Ok(FakeKey::String(value)), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + } + + impl RuntimeValueOps for FakeOps { + /// Creates a fake indexed array cell. + fn array_new(&mut self, capacity: usize) -> Result { + Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) + } + + /// Creates a fake associative array cell. + fn assoc_new(&mut self, capacity: usize) -> Result { + Ok(self.alloc(FakeValue::Assoc(HashMap::with_capacity(capacity)))) + } + + /// Reads one fake indexed array element. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + match self.get(array) { + FakeValue::Array(elements) => { + let FakeKey::Int(index) = key else { + return self.null(); + }; + if index < 0 { + return self.null(); + } + elements + .get(index as usize) + .copied() + .map_or_else(|| self.null(), Ok) + } + FakeValue::Assoc(entries) => { + entries.get(&key).copied().map_or_else(|| self.null(), Ok) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Writes one fake indexed or associative array element. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + let id = array.as_ptr() as usize; + match self.values.get_mut(&id) { + Some(FakeValue::Array(elements)) => { + let FakeKey::Int(index) = key else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if index < 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let index = index as usize; + while elements.len() <= index { + elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); + } + elements[index] = value; + } + Some(FakeValue::Assoc(entries)) => { + entries.insert(key, value); + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + Ok(array) + } + + /// Returns whether a fake runtime cell is an indexed or associative array. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + Ok(matches!( + self.get(value), + FakeValue::Array(_) | FakeValue::Assoc(_) + )) + } + + /// Records fake releases without freeing handles needed for assertions. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.releases.push(value); + Ok(()) + } + + /// Creates a fake null cell. + fn null(&mut self) -> Result { + Ok(self.alloc(FakeValue::Null)) + } + + /// Creates a fake bool cell. + fn bool_value(&mut self, value: bool) -> Result { + Ok(self.alloc(FakeValue::Bool(value))) + } + + /// Creates a fake int cell. + fn int(&mut self, value: i64) -> Result { + Ok(self.alloc(FakeValue::Int(value))) + } + + /// Creates a fake float cell. + fn float(&mut self, value: f64) -> Result { + Ok(self.alloc(FakeValue::Float(value))) + } + + /// Creates a fake string cell. + fn string(&mut self, value: &str) -> Result { + Ok(self.alloc(FakeValue::String(value.to_string()))) + } + + /// Adds fake numeric cells for interpreter tests. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Subtracts fake numeric cells for interpreter tests. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Multiplies fake numeric cells for interpreter tests. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Concatenates fake cells with simple string conversion for interpreter tests. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.stringify(left); + let right = self.stringify(right); + self.string(&(left + &right)) + } + + /// Appends fake echo output for interpreter tests. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + let value = self.stringify(value); + self.output.push_str(&value); + Ok(()) + } + + /// Returns PHP-like truthiness for fake runtime cells. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Null => false, + FakeValue::Bool(value) => value, + FakeValue::Int(value) => value != 0, + FakeValue::Float(value) => value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + }) + } + } + + impl FakeOps { + /// Converts a fake runtime cell to a PHP-like string for test echo/concat. + fn stringify(&self, handle: RuntimeCellHandle) -> String { + match self.get(handle) { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value, + FakeValue::Array(_) => "Array".to_string(), + FakeValue::Assoc(_) => "Array".to_string(), + } + } + } + + /// Verifies assignment writes a named scope entry and return reads it back. + #[test] + fn execute_program_stores_and_returns_scope_value() { + let program = parse_fragment(b"$x = 3; return $x + 4;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::Int(3)); + assert_eq!(values.get(result), FakeValue::Int(7)); + } + + /// Verifies echo and unset operate through runtime hooks and scope metadata. + #[test] + fn execute_program_echoes_and_unsets_scope_value() { + let program = + parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let name = values.string(" Ada").expect("create fake string"); + scope.set("name", name, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hi Ada"); + assert_eq!(values.get(result), FakeValue::Null); + assert!(scope.entry("name").expect("unset marker").flags().unset); + } + + /// Verifies print writes output and returns integer 1. + #[test] + fn execute_program_print_returns_one() { + let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "p"); + assert_eq!(values.get(result), FakeValue::Int(1)); + } + + /// Verifies if/else executes only the PHP-truthy branch. + #[test] + fn execute_program_if_else_uses_php_truthiness() { + let program = parse_fragment(br#"if ($flag) { $x = "then"; } else { $x = "else"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(0).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::String("else".to_string())); + } + + /// Verifies while repeats while the condition remains truthy and propagates writes. + #[test] + fn execute_program_while_uses_php_truthiness() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(2).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let flag = scope + .visible_cell("flag") + .expect("scope should contain flag"); + + assert_eq!(values.output, "2"); + assert_eq!(values.get(flag), FakeValue::Bool(false)); + } + + /// Verifies indexed array literals and reads execute through runtime hooks. + #[test] + fn execute_program_reads_indexed_array_literal() { + let program = parse_fragment(br#"return ["a", "b"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); + } + + /// Verifies associative array literals and string-key reads execute through runtime hooks. + #[test] + fn execute_program_reads_assoc_array_literal() { + let program = + parse_fragment(br#"return ["name" => "Ada"]["name"];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); + } + + /// Verifies indexed array writes mutate an existing scope array. + #[test] + fn execute_program_writes_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[1] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); + } + + /// Verifies mutating a borrowed scope array does not make the eval scope own it. + #[test] + fn execute_program_preserves_borrowed_array_ownership() { + let program = parse_fragment(br#"$items[0] = "b";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let array = values.array_new(1).expect("create fake array"); + scope.set("items", array, ScopeCellOwnership::Borrowed); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let entry = scope.entry("items").expect("scope should contain items"); + + assert_eq!(entry.cell(), array); + assert_eq!(entry.flags().ownership, ScopeCellOwnership::Borrowed); + assert!(values.releases.is_empty()); + } + + /// Verifies replacing an eval-owned scope value releases the old cell. + #[test] + fn execute_program_releases_replaced_scope_value() { + let program = parse_fragment(br#"$x = "old"; $x = "new";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); + } + + /// Verifies unsetting an eval-owned scope value releases the old cell. + #[test] + fn execute_program_releases_unset_scope_value() { + let program = parse_fragment(br#"$x = "old"; unset($x);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); + } + + /// Verifies break exits a runtime eval loop before later statements run. + #[test] + fn execute_program_break_exits_loop() { + let program = parse_fragment(br#"while ($flag) { echo "a"; break; echo "b"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a"); + } + + /// Verifies continue restarts a runtime eval loop and observes later scope updates. + #[test] + fn execute_program_continue_restarts_loop() { + let program = parse_fragment( + br#"while ($flag) { $flag = false; continue; echo "unreachable"; } echo "done";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "done"); + } +} diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs new file mode 100644 index 0000000000..074ceb4afc --- /dev/null +++ b/crates/elephc-eval/src/lib.rs @@ -0,0 +1,511 @@ +//! Purpose: +//! Optional C ABI bridge for elephc's runtime `eval()` support. +//! Exposes the stable entry points linked only into programs that use eval. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_*` symbols. +//! - `cargo test -p elephc-eval` for ABI-shape validation. +//! +//! Key details: +//! - No Rust panic or Rust-specific enum crosses the ABI boundary. +//! - Non-test builds execute the base EvalIR subset through generated runtime +//! value wrappers; crate unit tests keep a controlled stub because they do not +//! link the generated runtime assembly object. + +pub mod abi; +pub mod context; +pub mod errors; +pub mod eval_ir; +pub mod interpreter; +pub mod lower; +pub mod parser; +pub mod runtime_hooks; +pub mod scope; +pub mod value; + +use abi::{ + ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_BY_REF, + SCOPE_FLAG_DIRTY, SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, +}; +use errors::EvalStatus; +#[cfg(not(test))] +use runtime_hooks::ElephcRuntimeOps; +use scope::{ScopeCellOwnership, ScopeEntry}; +use std::slice; +use value::{RuntimeCell, RuntimeCellHandle}; + +#[cfg(not(test))] +unsafe extern "C" { + fn __elephc_eval_value_release(value: *mut RuntimeCell); +} + +/// Returns the ABI version expected by generated elephc eval call sites. +#[no_mangle] +pub extern "C" fn __elephc_eval_abi_version() -> u32 { + ABI_VERSION +} + +/// Allocates a process-level eval context handle for generated code. +#[no_mangle] +pub extern "C" fn __elephc_eval_context_new() -> *mut ElephcEvalContext { + Box::into_raw(Box::new(ElephcEvalContext::new())) +} + +/// Frees a process-level eval context handle allocated by the eval bridge. +/// +/// # Safety +/// `ctx` must be null or a pointer returned by `__elephc_eval_context_new` +/// that has not already been freed. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_free(ctx: *mut ElephcEvalContext) { + if !ctx.is_null() { + drop(Box::from_raw(ctx)); + } +} + +/// Allocates a materialized activation scope handle for generated code. +#[no_mangle] +pub extern "C" fn __elephc_eval_scope_new() -> *mut ElephcEvalScope { + Box::into_raw(Box::new(ElephcEvalScope::new())) +} + +/// Frees a materialized activation scope handle allocated by the eval bridge. +/// +/// # Safety +/// `scope` must be null or a pointer returned by `__elephc_eval_scope_new` +/// that has not already been freed. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_free(scope: *mut ElephcEvalScope) { + if !scope.is_null() { + let mut scope = Box::from_raw(scope); + release_owned_scope_cells(&mut scope); + drop(scope); + } +} + +/// Stores a named runtime cell in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`; names must be UTF-8 variable names. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_set( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + cell: *mut RuntimeCell, + flags: u32, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let ownership = if flags & SCOPE_FLAG_OWNED != 0 { + ScopeCellOwnership::Owned + } else { + ScopeCellOwnership::Borrowed + }; + if let Some(replaced) = scope.set(name, RuntimeCellHandle::from_raw(cell), ownership) { + release_scope_cell(replaced); + } + EvalStatus::Ok.code() +} + +/// Looks up a named runtime cell in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. Output pointers may be null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_get( + scope: *const ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + out_cell: *mut *mut RuntimeCell, + out_flags: *mut u32, +) -> i32 { + let Some(scope) = scope.as_ref() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let entry = scope.entry(&name); + if !out_cell.is_null() { + *out_cell = entry + .filter(|entry| entry.flags().is_visible()) + .map(|entry| entry.cell().as_ptr()) + .unwrap_or(std::ptr::null_mut()); + } + if !out_flags.is_null() { + *out_flags = entry.map(scope_entry_abi_flags).unwrap_or(0); + } + EvalStatus::Ok.code() +} + +/// Marks a named runtime cell as unset in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`; names must be UTF-8 variable names. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_unset( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + if let Some(replaced) = scope.unset(name) { + release_scope_cell(replaced); + } + EvalStatus::Ok.code() +} + +/// Clears dirty flags for every entry in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle allocated by the eval bridge. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_clear_dirty(scope: *mut ElephcEvalScope) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + scope.mark_all_clean(); + EvalStatus::Ok.code() +} + +/// Executes an eval fragment against a materialized caller scope. +/// +/// The FFI shape is final for the initial bridge: context/scope are opaque +/// runtime handles, `code_ptr`/`code_len` identify the PHP fragment bytes, and +/// `out` receives the eval return cell when provided. Non-test builds execute +/// the current EvalIR subset; test builds return `UnsupportedConstruct` because +/// they do not link elephc's generated runtime value wrappers. +/// +/// # Safety +/// Callers must pass valid pointers for any non-null handle and ensure +/// `code_ptr` is readable for `code_len` bytes when `code_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_execute( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, + code_ptr: *const u8, + code_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { execute_eval_inner(ctx, scope, code_ptr, code_len, out) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the eval ABI body after the exported wrapper has installed a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_execute`; callers must provide valid handles and code +/// storage for every non-null pointer argument. +unsafe fn execute_eval_inner( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, + code_ptr: *const u8, + code_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + if !ctx.is_null() && (*ctx).abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if code_len > 0 && code_ptr.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(code_len) = usize::try_from(code_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let code = if code_len == 0 { + &[] + } else { + slice::from_raw_parts(code_ptr, code_len) + }; + let Ok(program) = parser::parse_fragment(code) else { + return EvalStatus::ParseError.code(); + }; + if !out.is_null() { + (*out).clear(); + } + execute_parsed_eval(scope, &program, out) +} + +/// Executes a parsed eval program in production builds using elephc runtime hooks. +/// +/// # Safety +/// `scope` and `out` must be null or valid pointers supplied by generated code. +#[cfg(not(test))] +unsafe fn execute_parsed_eval( + scope: *mut ElephcEvalScope, + program: &eval_ir::EvalProgram, + out: *mut ElephcEvalResult, +) -> i32 { + let mut fallback_scope; + let scope = if let Some(scope) = scope.as_mut() { + scope + } else { + fallback_scope = ElephcEvalScope::new(); + &mut fallback_scope + }; + let mut values = ElephcRuntimeOps::new(); + match interpreter::execute_program(program, scope, &mut values) { + Ok(result) => { + if !out.is_null() { + (*out).kind = 0; + (*out).value_cell = result.as_ptr(); + (*out).error = std::ptr::null_mut(); + } + EvalStatus::Ok.code() + } + Err(status) => status.code(), + } +} + +/// Keeps crate unit tests independent from generated runtime assembly wrappers. +/// +/// # Safety +/// `out` must be null or valid result storage supplied by the test caller. +#[cfg(test)] +unsafe fn execute_parsed_eval( + _scope: *mut ElephcEvalScope, + _program: &eval_ir::EvalProgram, + _out: *mut ElephcEvalResult, +) -> i32 { + EvalStatus::UnsupportedConstruct.code() +} + +/// Converts an ABI name byte slice into an owned Rust string. +fn abi_name_to_string(name_ptr: *const u8, name_len: u64) -> Result { + if name_len > 0 && name_ptr.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + let name_len = usize::try_from(name_len).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = if name_len == 0 { + &[] + } else { + unsafe { slice::from_raw_parts(name_ptr, name_len) } + }; + std::str::from_utf8(bytes) + .map(|name| name.to_string()) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts internal scope-entry flags into stable ABI bit flags. +fn scope_entry_abi_flags(entry: ScopeEntry) -> u32 { + let flags = entry.flags(); + let mut abi_flags = 0; + if flags.present { + abi_flags |= SCOPE_FLAG_PRESENT; + } + if flags.unset { + abi_flags |= SCOPE_FLAG_UNSET; + } + if flags.dirty { + abi_flags |= SCOPE_FLAG_DIRTY; + } + if flags.by_ref { + abi_flags |= SCOPE_FLAG_BY_REF; + } + if flags.ownership == ScopeCellOwnership::Owned { + abi_flags |= SCOPE_FLAG_OWNED; + } + abi_flags +} + +/// Releases every owned cell currently held by a scope. +fn release_owned_scope_cells(scope: &mut ElephcEvalScope) { + for cell in scope.drain_owned_cells() { + release_scope_cell(cell); + } +} + +/// Releases one scope-owned runtime cell through the generated runtime wrapper. +fn release_scope_cell(cell: RuntimeCellHandle) { + #[cfg(not(test))] + unsafe { + __elephc_eval_value_release(cell.as_ptr()); + } + #[cfg(test)] + { + let _ = cell; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies the exported version entry point reports the crate ABI constant. + #[test] + fn abi_version_matches_constant() { + assert_eq!(__elephc_eval_abi_version(), ABI_VERSION); + } + + /// Verifies the initial execute stub clears result storage and returns the + /// documented unsupported status instead of panicking or succeeding. + #[test] + fn execute_stub_returns_unsupported_and_clears_result() { + let mut result = ElephcEvalResult { + kind: 99, + value_cell: 1usize as *mut std::ffi::c_void, + error: 2usize as *mut std::ffi::c_void, + }; + let status = unsafe { + __elephc_eval_execute( + std::ptr::null_mut(), + std::ptr::null_mut(), + b"$x = 1;".as_ptr(), + 7, + &mut result, + ) + }; + assert_eq!(status, EvalStatus::UnsupportedConstruct.code()); + assert_eq!(result.kind, 0); + assert!(result.value_cell.is_null()); + assert!(result.error.is_null()); + } + + /// Verifies context allocation returns a current-version opaque handle. + #[test] + fn context_new_returns_current_version_handle() { + let ctx = __elephc_eval_context_new(); + assert!(!ctx.is_null()); + let version = unsafe { (*ctx).abi_version() }; + unsafe { + __elephc_eval_context_free(ctx); + } + assert_eq!(version, ABI_VERSION); + } + + /// Verifies scope allocation returns an empty opaque activation scope handle. + #[test] + fn scope_new_returns_empty_handle() { + let scope = __elephc_eval_scope_new(); + assert!(!scope.is_null()); + let generation = unsafe { (*scope).generation() }; + unsafe { + __elephc_eval_scope_free(scope); + } + assert_eq!(generation, 0); + } + + /// Verifies execute rejects contexts whose ABI version no longer matches. + #[test] + fn execute_rejects_mismatched_context_version() { + let mut ctx = ElephcEvalContext::for_abi_version(ABI_VERSION + 1); + let status = unsafe { + __elephc_eval_execute( + &mut ctx, + std::ptr::null_mut(), + std::ptr::null(), + 0, + std::ptr::null_mut(), + ) + }; + + assert_eq!(status, EvalStatus::AbiMismatch.code()); + } + + /// Verifies execute maps invalid eval fragments to the stable parse status. + #[test] + fn execute_rejects_php_opening_tags_as_parse_errors() { + let code = b" bool { + true +} diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs new file mode 100644 index 0000000000..4db42781ba --- /dev/null +++ b/crates/elephc-eval/src/parser.rs @@ -0,0 +1,819 @@ +//! Purpose: +//! Parses runtime PHP eval fragments into the initial EvalIR statement form. +//! The parser handles a small statement subset now and keeps unsupported syntax +//! as parse failure until the full eval parser is implemented. +//! +//! Called from: +//! - `crate::__elephc_eval_execute()` +//! - Future `crate::interpreter` entry points. +//! +//! Key details: +//! - PHP eval fragments are statement fragments and must not include opening +//! ` Result { + if contains_php_open_tag(code) { + return Err(EvalParseError::PhpOpenTag); + } + let source = std::str::from_utf8(code).map_err(|_| EvalParseError::InvalidUtf8)?; + let tokens = Lexer::new(source).tokenize()?; + Parser::new(tokens, code.len()).parse_program() +} + +/// Returns true when a fragment contains a PHP opening tag sequence. +fn contains_php_open_tag(code: &[u8]) -> bool { + code.windows(2).any(|window| window == b" { + source: &'a str, + pos: usize, +} + +impl<'a> Lexer<'a> { + /// Creates a lexer over a UTF-8 eval fragment. + fn new(source: &'a str) -> Self { + Self { source, pos: 0 } + } + + /// Tokenizes the complete source and appends an EOF sentinel. + fn tokenize(mut self) -> Result, EvalParseError> { + let mut tokens = Vec::new(); + loop { + let token = self.next_token()?; + let done = token == TokenKind::Eof; + tokens.push(token); + if done { + break; + } + } + Ok(tokens) + } + + /// Reads the next token from the source. + fn next_token(&mut self) -> Result { + self.skip_ws(); + let Some(ch) = self.peek_char() else { + return Ok(TokenKind::Eof); + }; + match ch { + '$' => self.lex_variable(), + '\'' | '"' => self.lex_string(ch), + '0'..='9' => self.lex_number(), + '+' => { + self.bump_char(); + Ok(TokenKind::Plus) + } + '-' => { + self.bump_char(); + Ok(TokenKind::Minus) + } + '*' => { + self.bump_char(); + Ok(TokenKind::Star) + } + '.' => { + self.bump_char(); + Ok(TokenKind::Dot) + } + '=' => { + self.bump_char(); + if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::FatArrow) + } else { + Ok(TokenKind::Equal) + } + } + ';' => { + self.bump_char(); + Ok(TokenKind::Semicolon) + } + '(' => { + self.bump_char(); + Ok(TokenKind::LParen) + } + ')' => { + self.bump_char(); + Ok(TokenKind::RParen) + } + '[' => { + self.bump_char(); + Ok(TokenKind::LBracket) + } + ']' => { + self.bump_char(); + Ok(TokenKind::RBracket) + } + '{' => { + self.bump_char(); + Ok(TokenKind::LBrace) + } + '}' => { + self.bump_char(); + Ok(TokenKind::RBrace) + } + ',' => { + self.bump_char(); + Ok(TokenKind::Comma) + } + _ if is_ident_start(ch) => Ok(TokenKind::Ident(self.lex_ident().to_ascii_lowercase())), + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Reads a `$name` token. + fn lex_variable(&mut self) -> Result { + self.bump_char(); + let name = self.lex_ident(); + if name.is_empty() { + return Err(EvalParseError::ExpectedVariable); + } + Ok(TokenKind::DollarIdent(name)) + } + + /// Reads a PHP identifier body at the current byte offset. + fn lex_ident(&mut self) -> String { + let mut ident = String::new(); + while let Some(ch) = self.peek_char() { + if !is_ident_continue(ch) { + break; + } + ident.push(ch); + self.bump_char(); + } + ident + } + + /// Reads an integer or float literal. + fn lex_number(&mut self) -> Result { + let start = self.pos; + while matches!(self.peek_char(), Some('0'..='9')) { + self.bump_char(); + } + let mut is_float = false; + if self.peek_char() == Some('.') && matches!(self.peek_next_char(), Some('0'..='9')) { + is_float = true; + self.bump_char(); + while matches!(self.peek_char(), Some('0'..='9')) { + self.bump_char(); + } + } + let raw = &self.source[start..self.pos]; + if is_float { + raw.parse::() + .map(TokenKind::Float) + .map_err(|_| EvalParseError::InvalidNumber) + } else { + raw.parse::() + .map(TokenKind::Int) + .map_err(|_| EvalParseError::InvalidNumber) + } + } + + /// Reads a single- or double-quoted string literal. + fn lex_string(&mut self, quote: char) -> Result { + self.bump_char(); + let mut out = String::new(); + while let Some(ch) = self.peek_char() { + self.bump_char(); + if ch == quote { + return Ok(TokenKind::String(out)); + } + if ch == '\\' { + let Some(escaped) = self.peek_char() else { + return Err(EvalParseError::UnterminatedString); + }; + self.bump_char(); + out.push(match escaped { + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + '\\' => '\\', + '\'' => '\'', + '"' => '"', + other => other, + }); + } else { + out.push(ch); + } + } + Err(EvalParseError::UnterminatedString) + } + + /// Advances past ASCII and Unicode whitespace. + fn skip_ws(&mut self) { + while self.peek_char().is_some_and(char::is_whitespace) { + self.bump_char(); + } + } + + /// Returns the current char without advancing. + fn peek_char(&self) -> Option { + self.source[self.pos..].chars().next() + } + + /// Returns the char after the current char without advancing. + fn peek_next_char(&self) -> Option { + let mut chars = self.source[self.pos..].chars(); + chars.next()?; + chars.next() + } + + /// Advances by one UTF-8 char. + fn bump_char(&mut self) { + if let Some(ch) = self.peek_char() { + self.pos += ch.len_utf8(); + } + } +} + +/// Parses tokenized eval fragments into EvalIR. +struct Parser { + tokens: Vec, + pos: usize, + source_len: usize, +} + +impl Parser { + /// Creates a parser over tokens produced from a source fragment. + fn new(tokens: Vec, source_len: usize) -> Self { + Self { + tokens, + pos: 0, + source_len, + } + } + + /// Parses a complete eval fragment until EOF. + fn parse_program(mut self) -> Result { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::Eof) { + statements.extend(self.parse_stmt()?); + } + Ok(EvalProgram::new(self.source_len, statements)) + } + + /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. + fn parse_stmt(&mut self) -> Result, EvalParseError> { + match self.current() { + TokenKind::Ident(name) if name == "break" => { + self.advance(); + self.expect_semicolon()?; + Ok(vec![EvalStmt::Break]) + } + TokenKind::Ident(name) if name == "continue" => { + self.advance(); + self.expect_semicolon()?; + Ok(vec![EvalStmt::Continue]) + } + TokenKind::Ident(name) if name == "echo" => { + self.advance(); + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Echo(expr)]) + } + TokenKind::Ident(name) if name == "if" => self.parse_if_stmt(), + TokenKind::Ident(name) if name == "return" => { + self.advance(); + if self.consume_semicolon() { + return Ok(vec![EvalStmt::Return(None)]); + } + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Return(Some(expr))]) + } + TokenKind::Ident(name) if name == "unset" => self.parse_unset_stmt(), + TokenKind::Ident(name) if name == "while" => self.parse_while_stmt(), + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_stmt(name.clone()) + } + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::Equal) => { + let name = name.clone(); + self.advance(); + self.advance(); + let value = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::StoreVar { name, value }]) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => { + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Expr(expr)]) + } + } + } + + /// Parses `$name[index] = expr;` for indexed-array eval writes. + fn parse_array_set_stmt(&mut self, name: String) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `if (expr) { ... } [else { ... }]`. + fn parse_if_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let then_branch = self.parse_block()?; + let else_branch = if matches!(self.current(), TokenKind::Ident(name) if name == "else") { + self.advance(); + self.parse_block()? + } else { + Vec::new() + }; + Ok(vec![EvalStmt::If { + condition, + then_branch, + else_branch, + }]) + } + + /// Parses `unset($name[, ...]);`. + fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let mut statements = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + statements.push(EvalStmt::UnsetVar { name: name.clone() }); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(statements) + } + + /// Parses `while (expr) { ... }`. + fn parse_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let body = self.parse_block()?; + Ok(vec![EvalStmt::While { condition, body }]) + } + + /// Parses a brace-delimited statement block. + fn parse_block(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LBrace)?; + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + statements.extend(self.parse_stmt()?); + } + self.expect(TokenKind::RBrace)?; + Ok(statements) + } + + /// Parses an expression using PHP-like precedence for `+` over `.`. + fn parse_expr(&mut self) -> Result { + self.parse_concat() + } + + /// Parses left-associative string concatenation. + fn parse_concat(&mut self) -> Result { + let mut expr = self.parse_add()?; + while self.consume(TokenKind::Dot) { + let right = self.parse_add()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric addition and subtraction. + fn parse_add(&mut self) -> Result { + let mut expr = self.parse_mul()?; + loop { + let op = if self.consume(TokenKind::Plus) { + EvalBinOp::Add + } else if self.consume(TokenKind::Minus) { + EvalBinOp::Sub + } else { + break; + }; + let right = self.parse_mul()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric multiplication. + fn parse_mul(&mut self) -> Result { + let mut expr = self.parse_postfix()?; + while self.consume(TokenKind::Star) { + let right = self.parse_postfix()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses postfix array reads after a primary expression. + fn parse_postfix(&mut self) -> Result { + let mut expr = self.parse_primary()?; + while self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + } + Ok(expr) + } + + /// Parses primary expressions supported by the initial eval subset. + fn parse_primary(&mut self) -> Result { + match self.current() { + TokenKind::Int(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Int(value))) + } + TokenKind::Float(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Float(value))) + } + TokenKind::String(value) => { + let value = value.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(value))) + } + TokenKind::DollarIdent(name) => { + let name = name.clone(); + self.advance(); + Ok(EvalExpr::LoadVar(name)) + } + TokenKind::Ident(name) if name == "null" => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Null)) + } + TokenKind::Ident(name) if name == "true" => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(true))) + } + TokenKind::Ident(name) if name == "false" => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(false))) + } + TokenKind::Ident(name) if name == "print" => { + self.advance(); + let expr = self.parse_expr()?; + Ok(EvalExpr::Print(Box::new(expr))) + } + TokenKind::LBracket => self.parse_array_literal(), + TokenKind::LParen => { + self.advance(); + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + Ok(expr) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Parses an array literal with source-order optional key/value element expressions. + fn parse_array_literal(&mut self) -> Result { + self.expect(TokenKind::LBracket)?; + let mut elements = Vec::new(); + if self.consume(TokenKind::RBracket) { + return Ok(EvalExpr::Array(elements)); + } + loop { + let first = self.parse_expr()?; + if self.consume(TokenKind::FatArrow) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyValue { key: first, value }); + } else { + elements.push(EvalArrayElement::Value(first)); + } + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RBracket) { + return Ok(EvalExpr::Array(elements)); + } + } + self.expect(TokenKind::RBracket)?; + Ok(EvalExpr::Array(elements)) + } + + /// Consumes `expected` or returns a parse error. + fn expect(&mut self, expected: TokenKind) -> Result<(), EvalParseError> { + if self.consume(expected) { + Ok(()) + } else { + Err(EvalParseError::UnexpectedToken) + } + } + + /// Consumes a semicolon or returns the semicolon-specific parse error. + fn expect_semicolon(&mut self) -> Result<(), EvalParseError> { + if self.consume_semicolon() { + Ok(()) + } else { + Err(EvalParseError::ExpectedSemicolon) + } + } + + /// Consumes a semicolon if present. + fn consume_semicolon(&mut self) -> bool { + self.consume(TokenKind::Semicolon) + } + + /// Consumes `expected` if the current token matches it. + fn consume(&mut self, expected: TokenKind) -> bool { + if *self.current() == expected { + self.advance(); + true + } else { + false + } + } + + /// Returns the current token. + fn current(&self) -> &TokenKind { + self.tokens.get(self.pos).unwrap_or(&TokenKind::Eof) + } + + /// Returns the next token without advancing. + fn peek(&self) -> &TokenKind { + self.tokens.get(self.pos + 1).unwrap_or(&TokenKind::Eof) + } + + /// Advances to the next token. + fn advance(&mut self) { + if self.pos < self.tokens.len() { + self.pos += 1; + } + } +} + +/// Returns true for the first character of a PHP variable/function identifier. +fn is_ident_start(ch: char) -> bool { + ch == '_' || ch.is_ascii_alphabetic() +} + +/// Returns true for subsequent characters in a PHP variable/function identifier. +fn is_ident_continue(ch: char) -> bool { + is_ident_start(ch) || ch.is_ascii_digit() +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies assignment fragments lower to by-name StoreVar statements. + #[test] + fn parse_fragment_accepts_assignment_source() { + let program = parse_fragment(b"$x = 1;").expect("fragment should parse"); + assert_eq!(program.source_len(), 7); + assert_eq!( + program.statements(), + &[EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::Int(1)), + }] + ); + } + + /// Verifies echo fragments preserve expression source order. + #[test] + fn parse_fragment_accepts_echo_source() { + let program = parse_fragment(br#"echo "hi" . $name;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Echo(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Const(EvalConst::String("hi".to_string()))), + right: Box::new(EvalExpr::LoadVar("name".to_string())), + })] + ); + } + + /// Verifies if/else fragments lower to branch statements with nested blocks. + #[test] + fn parse_fragment_accepts_if_else_source() { + let program = parse_fragment(br#"if ($flag) { $x = "yes"; } else { $x = "no"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("flag".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("yes".to_string())), + }], + else_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("no".to_string())), + }], + }] + ); + } + + /// Verifies print fragments lower to expression-form print with the printed value. + #[test] + fn parse_fragment_accepts_print_source() { + let program = parse_fragment(br#"print "hi";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Expr(EvalExpr::Print(Box::new(EvalExpr::Const( + EvalConst::String("hi".to_string()) + ))))] + ); + } + + /// Verifies indexed array literals and reads parse as runtime array expressions. + #[test] + fn parse_fragment_accepts_indexed_array_read_source() { + let program = parse_fragment(br#"return [1, 2][0];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(2))), + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }))] + ); + } + + /// Verifies associative array literals preserve explicit key/value expressions. + #[test] + fn parse_fragment_accepts_assoc_array_literal_source() { + let program = + parse_fragment(br#"return ["name" => "Ada"];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + } + ])))] + ); + } + + /// Verifies indexed array writes parse as variable-target array set statements. + #[test] + fn parse_fragment_accepts_indexed_array_write_source() { + let program = parse_fragment(br#"$items[1] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArraySetVar { + name: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(1)), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); + } + + /// Verifies while fragments lower to loop statements with a nested block. + #[test] + fn parse_fragment_accepts_while_source() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::While { + condition: EvalExpr::LoadVar("flag".to_string()), + body: vec![ + EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), + EvalStmt::StoreVar { + name: "flag".to_string(), + value: EvalExpr::Const(EvalConst::Bool(false)), + }, + ], + }] + ); + } + + /// Verifies loop control statements parse inside while blocks. + #[test] + fn parse_fragment_accepts_break_and_continue_source() { + let program = parse_fragment(br#"while ($flag) { continue; break; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::While { + condition: EvalExpr::LoadVar("flag".to_string()), + body: vec![EvalStmt::Continue, EvalStmt::Break], + }] + ); + } + + /// Verifies return fragments parse optional return expressions. + #[test] + fn parse_fragment_accepts_return_source() { + let program = parse_fragment(b"return ($x - 1) * 4;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }))] + ); + } + + /// Verifies unset fragments expand to one by-name unset statement per variable. + #[test] + fn parse_fragment_accepts_unset_source() { + let program = parse_fragment(b"unset($x, $y);").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetVar { + name: "x".to_string() + }, + EvalStmt::UnsetVar { + name: "y".to_string() + }, + ] + ); + } + + /// Verifies eval fragments reject PHP opening tags. + #[test] + fn parse_fragment_rejects_opening_tag() { + assert_eq!( + parse_fragment(b" *mut RuntimeCell; + fn __elephc_eval_value_assoc_new(capacity: u64) -> *mut RuntimeCell; + fn __elephc_eval_value_array_get( + array: *mut RuntimeCell, + index: *mut RuntimeCell, + ) -> *mut RuntimeCell; + fn __elephc_eval_value_array_set( + array: *mut RuntimeCell, + index: *mut RuntimeCell, + value: *mut RuntimeCell, + ) -> *mut RuntimeCell; + fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; + fn __elephc_eval_value_null() -> *mut RuntimeCell; + fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; + fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; + fn __elephc_eval_value_float(value: f64) -> *mut RuntimeCell; + fn __elephc_eval_value_string(ptr: *const u8, len: u64) -> *mut RuntimeCell; + fn __elephc_eval_value_add(left: *mut RuntimeCell, right: *mut RuntimeCell) + -> *mut RuntimeCell; + fn __elephc_eval_value_sub(left: *mut RuntimeCell, right: *mut RuntimeCell) + -> *mut RuntimeCell; + fn __elephc_eval_value_mul(left: *mut RuntimeCell, right: *mut RuntimeCell) + -> *mut RuntimeCell; + fn __elephc_eval_value_concat( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + fn __elephc_eval_value_echo(value: *mut RuntimeCell); + fn __elephc_eval_value_truthy(value: *mut RuntimeCell) -> u64; + fn __elephc_eval_value_release(value: *mut RuntimeCell); +} + +/// Runtime hook adapter that produces and consumes boxed elephc Mixed cells. +#[cfg(not(test))] +pub struct ElephcRuntimeOps; + +#[cfg(not(test))] +impl ElephcRuntimeOps { + /// Creates a new stateless runtime hook adapter. + pub const fn new() -> Self { + Self + } + + /// Converts a runtime wrapper result into an interpreter handle. + fn handle(ptr: *mut RuntimeCell) -> Result { + if ptr.is_null() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(RuntimeCellHandle::from_raw(ptr)) + } + } +} + +#[cfg(not(test))] +impl RuntimeValueOps for ElephcRuntimeOps { + /// Creates a boxed Mixed indexed array through the generated runtime wrapper. + fn array_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_new(capacity as u64) }) + } + + /// Creates a boxed Mixed associative array through the generated runtime wrapper. + fn assoc_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_assoc_new(capacity as u64) }) + } + + /// Reads one element from a boxed Mixed array through the generated runtime wrapper. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_get(array.as_ptr(), index.as_ptr()) }) + } + + /// Writes one element to a boxed Mixed array through the generated runtime wrapper. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_array_set(array.as_ptr(), index.as_ptr(), value.as_ptr()) + }) + } + + /// Returns whether a boxed Mixed cell has an array-like runtime tag. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_is_array_like(value.as_ptr()) != 0 }) + } + + /// Releases one boxed Mixed cell through the generated runtime wrapper. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release(value.as_ptr()); + } + Ok(()) + } + + /// Creates a boxed null Mixed cell through the generated runtime wrapper. + fn null(&mut self) -> Result { + Self::handle(unsafe { __elephc_eval_value_null() }) + } + + /// Creates a boxed bool Mixed cell through the generated runtime wrapper. + fn bool_value(&mut self, value: bool) -> Result { + Self::handle(unsafe { __elephc_eval_value_bool(u64::from(value)) }) + } + + /// Creates a boxed int Mixed cell through the generated runtime wrapper. + fn int(&mut self, value: i64) -> Result { + Self::handle(unsafe { __elephc_eval_value_int(value) }) + } + + /// Creates a boxed float Mixed cell through the generated runtime wrapper. + fn float(&mut self, value: f64) -> Result { + Self::handle(unsafe { __elephc_eval_value_float(value) }) + } + + /// Creates a boxed string Mixed cell through the generated runtime wrapper. + fn string(&mut self, value: &str) -> Result { + Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) + } + + /// Adds two boxed Mixed cells using elephc runtime numeric semantics. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_add(left.as_ptr(), right.as_ptr()) }) + } + + /// Subtracts two boxed Mixed cells using elephc runtime numeric semantics. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_sub(left.as_ptr(), right.as_ptr()) }) + } + + /// Multiplies two boxed Mixed cells using elephc runtime numeric semantics. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_mul(left.as_ptr(), right.as_ptr()) }) + } + + /// Concatenates two boxed Mixed cells using elephc runtime string semantics. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_concat(left.as_ptr(), right.as_ptr()) }) + } + + /// Emits one boxed Mixed cell to stdout through the generated runtime wrapper. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_echo(value.as_ptr()); + } + Ok(()) + } + + /// Converts one boxed Mixed cell to PHP truthiness through the generated runtime wrapper. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_truthy(value.as_ptr()) != 0 }) + } +} diff --git a/crates/elephc-eval/src/scope.rs b/crates/elephc-eval/src/scope.rs new file mode 100644 index 0000000000..4e867ba42d --- /dev/null +++ b/crates/elephc-eval/src/scope.rs @@ -0,0 +1,308 @@ +//! Purpose: +//! Owns the materialized activation scope used by runtime eval. +//! The scope maps PHP variable names to opaque elephc runtime cells and tracks +//! unset/dirty/generation metadata for native reloads after an eval barrier. +//! +//! Called from: +//! - `crate::abi` +//! - `crate::__elephc_eval_execute()` +//! +//! Key details: +//! - Scope entries store runtime-cell handles only; the eval bridge does not +//! introduce a second PHP value representation. + +use std::collections::HashMap; + +use crate::value::RuntimeCellHandle; + +/// Records whether a scope entry owns or borrows its runtime cell. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScopeCellOwnership { + Borrowed, + Owned, +} + +/// Tracks the observable PHP state associated with one named scope cell. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ScopeEntryFlags { + pub present: bool, + pub unset: bool, + pub dirty: bool, + pub by_ref: bool, + pub ownership: ScopeCellOwnership, +} + +impl ScopeEntryFlags { + /// Builds flags for a present runtime cell written by native code or eval. + pub const fn present(ownership: ScopeCellOwnership) -> Self { + Self { + present: true, + unset: false, + dirty: true, + by_ref: false, + ownership, + } + } + + /// Builds flags for a PHP variable that has been unset from the dynamic scope. + pub const fn unset() -> Self { + Self { + present: false, + unset: true, + dirty: true, + by_ref: false, + ownership: ScopeCellOwnership::Borrowed, + } + } + + /// Returns true when this entry names a PHP variable visible to reads. + pub const fn is_visible(self) -> bool { + self.present && !self.unset + } +} + +/// Stores one named variable's runtime cell plus sync metadata. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ScopeEntry { + cell: RuntimeCellHandle, + flags: ScopeEntryFlags, + generation: u64, +} + +impl ScopeEntry { + /// Creates an entry for a present runtime cell at the given scope generation. + pub const fn present( + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, + generation: u64, + ) -> Self { + Self { + cell, + flags: ScopeEntryFlags::present(ownership), + generation, + } + } + + /// Creates an unset marker at the given scope generation. + pub const fn unset(generation: u64) -> Self { + Self { + cell: RuntimeCellHandle::from_raw(std::ptr::null_mut()), + flags: ScopeEntryFlags::unset(), + generation, + } + } + + /// Returns the runtime cell handle stored for this variable. + pub const fn cell(self) -> RuntimeCellHandle { + self.cell + } + + /// Returns the PHP visibility and ownership flags for this entry. + pub const fn flags(self) -> ScopeEntryFlags { + self.flags + } + + /// Returns the scope generation when this entry was last updated. + pub const fn generation(self) -> u64 { + self.generation + } + + /// Clears the dirty flag after native code has synchronized the entry. + pub fn mark_clean(&mut self) { + self.flags.dirty = false; + } +} + +/// Materialized activation scope passed opaquely across the eval ABI. +pub struct ElephcEvalScope { + entries: HashMap, + generation: u64, +} + +impl ElephcEvalScope { + /// Creates an empty materialized activation scope. + pub fn new() -> Self { + Self { + entries: HashMap::new(), + generation: 0, + } + } + + /// Returns the current scope generation counter. + pub const fn generation(&self) -> u64 { + self.generation + } + + /// Stores or replaces a named variable with a runtime cell handle. + pub fn set( + &mut self, + name: impl Into, + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, + ) -> Option { + self.bump_generation(); + let previous = self.entries.insert( + name.into(), + ScopeEntry::present(cell, ownership, self.generation), + ); + owned_cell_except(previous, cell) + } + + /// Marks a named variable as unset while preserving the fact that eval touched it. + pub fn unset(&mut self, name: impl Into) -> Option { + self.bump_generation(); + let previous = self + .entries + .insert(name.into(), ScopeEntry::unset(self.generation)); + owned_cell(previous) + } + + /// Returns the entry for a named variable, including unset markers. + pub fn entry(&self, name: &str) -> Option { + self.entries.get(name).copied() + } + + /// Returns the visible runtime cell for a named variable, excluding unset markers. + pub fn visible_cell(&self, name: &str) -> Option { + self.entry(name) + .filter(|entry| entry.flags().is_visible()) + .map(ScopeEntry::cell) + } + + /// Returns true when the scope contains a visible value for the named variable. + pub fn contains_visible(&self, name: &str) -> bool { + self.visible_cell(name).is_some() + } + + /// Returns the names of entries dirtied since the last synchronization. + pub fn dirty_names(&self) -> Vec<&str> { + self.entries + .iter() + .filter_map(|(name, entry)| entry.flags().dirty.then_some(name.as_str())) + .collect() + } + + /// Clears dirty flags after native code reloads or invalidates affected locals. + pub fn mark_all_clean(&mut self) { + for entry in self.entries.values_mut() { + entry.mark_clean(); + } + } + + /// Removes every entry and returns runtime cells owned by the scope. + pub fn drain_owned_cells(&mut self) -> Vec { + self.entries + .drain() + .filter_map(|(_, entry)| owned_cell(Some(entry))) + .collect() + } + + /// Advances the generation counter for a scope mutation. + fn bump_generation(&mut self) { + self.generation = self.generation.saturating_add(1); + } +} + +/// Returns the owned cell for a visible owned entry, if one exists. +fn owned_cell(entry: Option) -> Option { + let entry = entry?; + let flags = entry.flags(); + (flags.is_visible() && flags.ownership == ScopeCellOwnership::Owned).then_some(entry.cell()) +} + +/// Returns an owned cell unless it is the same handle as the newly stored cell. +fn owned_cell_except( + entry: Option, + replacement: RuntimeCellHandle, +) -> Option { + owned_cell(entry).filter(|cell| *cell != replacement) +} + +impl Default for ElephcEvalScope { + /// Creates the default empty materialized activation scope. + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies setting a variable records a visible dirty runtime-cell handle. + #[test] + fn set_records_visible_dirty_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let old = scope.set("x", cell, ScopeCellOwnership::Borrowed); + + let entry = scope.entry("x").expect("entry must exist"); + assert_eq!(old, None); + assert_eq!(entry.cell(), cell); + assert!(entry.flags().is_visible()); + assert!(entry.flags().dirty); + assert_eq!(entry.generation(), 1); + assert_eq!(scope.visible_cell("x"), Some(cell)); + } + + /// Verifies unsetting a variable creates a dirty marker that is not visible. + #[test] + fn unset_records_missing_dirty_marker() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + scope.set("x", cell, ScopeCellOwnership::Borrowed); + scope.mark_all_clean(); + let old = scope.unset("x"); + + let entry = scope.entry("x").expect("unset marker must exist"); + assert_eq!(old, None); + assert!(entry.flags().unset); + assert!(!entry.flags().is_visible()); + assert!(entry.flags().dirty); + assert_eq!(scope.visible_cell("x"), None); + assert_eq!(scope.dirty_names(), vec!["x"]); + } + + /// Verifies replacing an owned entry returns the old cell for release. + #[test] + fn set_returns_replaced_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let old_cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let new_cell = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + + scope.set("x", old_cell, ScopeCellOwnership::Owned); + let replaced = scope.set("x", new_cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, Some(old_cell)); + assert_eq!(scope.visible_cell("x"), Some(new_cell)); + } + + /// Verifies replacing an owned entry with the same cell does not release it. + #[test] + fn set_does_not_return_same_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + + scope.set("x", cell, ScopeCellOwnership::Owned); + let replaced = scope.set("x", cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, None); + } + + /// Verifies draining a scope returns only visible owned cells. + #[test] + fn drain_owned_cells_returns_visible_owned_entries() { + let mut scope = ElephcEvalScope::new(); + let owned = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let borrowed = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + scope.set("owned", owned, ScopeCellOwnership::Owned); + scope.set("borrowed", borrowed, ScopeCellOwnership::Borrowed); + scope.unset("borrowed"); + + let drained = scope.drain_owned_cells(); + + assert_eq!(drained, vec![owned]); + assert!(scope.entry("owned").is_none()); + assert!(scope.entry("borrowed").is_none()); + } +} diff --git a/crates/elephc-eval/src/value.rs b/crates/elephc-eval/src/value.rs new file mode 100644 index 0000000000..52c39f6d8c --- /dev/null +++ b/crates/elephc-eval/src/value.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Names the opaque runtime cell/value handles used by eval internals. +//! Prevents the eval bridge from introducing a second PHP value system. +//! +//! Called from: +//! - Future `crate::scope` and `crate::interpreter` implementations. +//! +//! Key details: +//! - Handles point at elephc runtime cells whose tag/payload/refcount contract +//! is owned by the main runtime. + +use std::ffi::c_void; + +/// Opaque pointer to an elephc runtime cell. +pub type RuntimeCell = c_void; + +/// Wraps an opaque runtime cell pointer without taking ownership by itself. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RuntimeCellHandle { + ptr: *mut RuntimeCell, +} + +impl RuntimeCellHandle { + /// Creates a runtime-cell handle from a raw pointer supplied by elephc. + pub const fn from_raw(ptr: *mut RuntimeCell) -> Self { + Self { ptr } + } + + /// Returns the raw runtime-cell pointer for ABI calls back into elephc. + pub const fn as_ptr(self) -> *mut RuntimeCell { + self.ptr + } + + /// Returns true when this handle does not reference a runtime cell. + pub const fn is_null(self) -> bool { + self.ptr.is_null() + } +} diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 51e3143d00..7fc43a1608 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -11,6 +11,14 @@ The runtime is a collection of **hand-written assembly routines** that handle op These routines end up in every compiled binary. In the CLI flow they are usually pre-assembled into the cached runtime object rather than textually appended to each user `.s` file, but they are still part of the final executable rather than an external shared dependency. +## Optional eval bridge + +Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. + +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. + +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, basic control flow, indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. + ## Why a runtime? Some operations can't be done with a few inline instructions: diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b8b4a87d7b..898cac5be8 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -20,6 +20,7 @@ sidebar: | `putenv()` | `putenv($assignment): bool` | Set environment variable ("KEY=VALUE") | | `define()` | `define($name, $value): bool` | Define a compile-time global constant with a string-literal name | | `defined()` | `defined($name): bool` | Check whether a string-literal constant name is defined | +| `eval()` | `eval($code): mixed` | Parse and execute a PHP fragment in the caller's local scope | | `php_uname()` | `php_uname($mode = "a"): string` | Get system information from the target runtime | | `phpversion()` | `phpversion(): string` | Get the elephc package version from `Cargo.toml` | | `exec()` | `exec($command): string` | Execute command, return output | @@ -29,6 +30,10 @@ sidebar: `define()` returns `true` the first time a constant is defined at runtime. Duplicate attempts keep the first value, return `false`, and emit a suppressible runtime warning. `defined()` currently requires a string literal in AOT mode. +`eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. + +The evaluated string must be a PHP fragment without an opening ` "Ada"]; +$result = eval('$x = $x + 2; $created = "dynamic"; return $x + 4;'); +eval('$profile["name"] = "Grace";'); +$meta = eval('return ["source" => "eval"];'); + +echo "x=" . $x . "\n"; +echo "created=" . $created . "\n"; +echo "name=" . $profile["name"] . "\n"; +echo "source=" . $meta["source"] . "\n"; +echo "result=" . $result . "\n"; diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index d7928e7a1d..68138bd5a6 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -166,6 +166,7 @@ pub(super) fn emit_main_prologue(ctx: &mut FunctionContext<'_>) { store_argv_local_if_present(ctx); zero_initialize_main_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_scope_locals(ctx); } /// Emits a callable function prologue using an already-resolved entry label. @@ -205,6 +206,7 @@ pub(super) fn emit_function_prologue_with_label( } zero_initialize_function_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_scope_locals(ctx); Ok(()) } @@ -339,6 +341,7 @@ pub(super) fn emit_web_handler_prologue(ctx: &mut FunctionContext<'_>) { emit_callee_saved_saves(ctx); zero_initialize_main_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_scope_locals(ctx); } /// Emits the `--web` top-level handler epilogue and returns to the bridge. @@ -419,6 +422,7 @@ fn emit_main_local_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { _ => {} } } + emit_eval_scope_epilogue_cleanup(ctx); } /// Returns main local slots that receive owned refcounted values through `StoreLocal`. @@ -441,7 +445,9 @@ fn main_cleanup_locals(ctx: &FunctionContext<'_>) -> Vec<(String, LocalSlotId, P .as_deref() .is_none_or(|name| !param_names.contains(name)) }) - .filter(|local| local_slot_has_store(ctx.function, local.id)) + .filter(|local| { + local_slot_has_store(ctx.function, local.id) || function_has_eval_scope(ctx.function) + }) .filter_map(|local| { let ty = local.php_type.codegen_repr(); if !(matches!(ty, PhpType::Str | PhpType::Callable) || ty.is_refcounted()) { @@ -525,6 +531,66 @@ fn ref_cell_owner_locals(ctx: &FunctionContext<'_>) -> Vec<(String, LocalSlotId, locals } +/// Zero-initializes persistent eval scope handles before the first eval call can allocate one. +fn zero_initialize_eval_scope_locals(ctx: &mut FunctionContext<'_>) { + for (_, offset) in eval_scope_locals(ctx) { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } +} + +/// Releases persistent eval scopes allocated for this frame. +fn emit_eval_scope_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { + for (name, offset) in eval_scope_locals(ctx) { + ctx.emitter.comment(&format!("epilogue cleanup {}", name)); + emit_eval_scope_cleanup(ctx, offset); + } +} + +/// Frees one persistent eval scope handle when it was allocated. +fn emit_eval_scope_cleanup(ctx: &mut FunctionContext<'_>, offset: usize) { + let result_reg = abi::int_result_reg(ctx.emitter); + let done = ctx.next_label("eval_scope_cleanup_done"); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done); + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if arg_reg != result_reg { + ctx.emitter + .instruction(&format!("mov {}, {}", arg_reg, result_reg)); // pass the persistent eval scope handle to the free helper + } + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_free"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + ctx.emitter.label(&done); +} + +/// Returns hidden eval scope slots and their frame offsets. +fn eval_scope_locals(ctx: &FunctionContext<'_>) -> Vec<(String, usize)> { + let mut locals = ctx + .function + .locals + .iter() + .filter(|local| local.kind == LocalKind::EvalScope) + .filter_map(|local| { + let offset = ctx.local_offset(local.id).ok()?; + let name = local + .name + .clone() + .unwrap_or_else(|| format!("slot{}", local.id.as_raw())); + Some((name, offset)) + }) + .collect::>(); + locals.sort_by_key(|(_, offset)| *offset); + locals +} + +/// Returns true when the function owns a persistent eval scope local. +fn function_has_eval_scope(function: &Function) -> bool { + function + .locals + .iter() + .any(|local| local.kind == LocalKind::EvalScope) +} + /// Returns true when a local slot is written by an explicit EIR `StoreLocal`. fn local_slot_has_store(function: &Function, slot: LocalSlotId) -> bool { function.instructions.iter().any(|inst| { @@ -645,7 +711,8 @@ fn emit_function_local_epilogue_cleanup( ) { let cleanup_locals = function_cleanup_locals(ctx, skip_return_slot); let ref_cell_owners = ref_cell_owner_locals(ctx); - if cleanup_locals.is_empty() && ref_cell_owners.is_empty() { + let eval_scopes = eval_scope_locals(ctx); + if cleanup_locals.is_empty() && ref_cell_owners.is_empty() && eval_scopes.is_empty() { return; } let return_ty = ctx.function.return_php_type.codegen_repr(); @@ -663,6 +730,10 @@ fn emit_function_local_epilogue_cleanup( _ => {} } } + for (name, offset) in eval_scopes { + ctx.emitter.comment(&format!("epilogue cleanup {}", name)); + emit_eval_scope_cleanup(ctx, offset); + } if preserves_return { pop_return_value(ctx, &return_ty); } @@ -696,7 +767,9 @@ fn function_cleanup_locals( .is_none_or(|name| !param_names.contains(name)) }) .filter(|local| Some(local.id) != skip_return_slot) - .filter(|local| local_slot_has_store(ctx.function, local.id)) + .filter(|local| { + local_slot_has_store(ctx.function, local.id) || function_has_eval_scope(ctx.function) + }) .filter_map(|local| { let ty = local.php_type.codegen_repr(); if !cleanup_tracked_codegen_type(&ty) { diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 08c35e0f99..0d3a3fa9e4 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -27,6 +27,7 @@ mod buffers; pub(crate) mod class_relations; pub(crate) mod ctype; pub(crate) mod debug; +mod eval; pub(crate) mod io; mod isset; pub(crate) mod is_numeric; @@ -57,6 +58,7 @@ pub(super) fn lower_builtin_call(ctx: &mut FunctionContext<'_>, inst: &Instructi } match key.as_str() { "closure_bind" => lower_closure_bind(ctx, inst), + "eval" => eval::lower_eval(ctx, inst), "buffer_len" => buffers::lower_buffer_len(ctx, inst), "buffer_free" => buffers::lower_buffer_free(ctx, inst), diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs new file mode 100644 index 0000000000..4430687378 --- /dev/null +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -0,0 +1,424 @@ +//! Purpose: +//! Lowers PHP `eval()` calls to the optional libelephc-eval bridge ABI. +//! Materializes a persistent per-function eval scope handle, flushes visible +//! locals into that scope, calls the bridge, and reloads synchronized locals +//! from boxed Mixed cells after the call returns. +//! +//! Called from: +//! - `crate::codegen::lower_inst::builtins::lower_builtin_call()`. +//! +//! Key details: +//! - Argument evaluation has already happened in PHP source order during EIR +//! lowering; this module only materializes the bridge ABI call. +//! - The bridge is target-mangled like other C staticlib symbols. + +use crate::codegen::platform::Arch; +use crate::codegen::{CodegenIrError, Result}; +use crate::codegen::{abi, emit_box_current_value_as_mixed}; +use crate::ir::{Instruction, LocalKind, LocalSlotId}; +use crate::types::PhpType; + +use super::super::super::context::FunctionContext; +use super::super::{expect_operand, store_if_result}; + +const EVAL_STATUS_PARSE_ERROR: i64 = 1; +const EVAL_STATUS_UNSUPPORTED: i64 = 4; +const EVAL_PARSE_ERROR_MESSAGE: &str = "Parse error: eval() fragment is invalid\n"; +const EVAL_UNSUPPORTED_MESSAGE: &str = + "Fatal error: eval() fragment uses an unsupported construct\n"; +const EVAL_RUNTIME_FATAL_MESSAGE: &str = "Fatal error: eval() runtime failed\n"; +const EVAL_STACK_BYTES: usize = 64; +const EVAL_RESULT_VALUE_CELL_OFFSET: usize = 8; +const EVAL_SCOPE_HANDLE_OFFSET: usize = 32; +const EVAL_TEMP_CELL_OFFSET: usize = 40; +const EVAL_CODE_PTR_OFFSET: usize = 48; +const EVAL_CODE_LEN_OFFSET: usize = 56; +const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; +const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; + +/// Local slot metadata needed for conservative eval scope synchronization. +#[derive(Clone)] +struct EvalSyncLocal { + name: String, + slot: LocalSlotId, + ty: PhpType, +} + +/// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. +pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::ensure_arg_count(inst, "eval", 1)?; + let code = expect_operand(inst, 0)?; + let ty = ctx.load_value_to_result(code)?.codegen_repr(); + if ty != PhpType::Str { + return Err(CodegenIrError::unsupported(format!( + "eval() argument lowering for PHP type {:?}", + ty + ))); + } + + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + save_eval_code_string(ctx); + ensure_eval_scope(ctx)?; + let sync_locals = eval_sync_locals(ctx); + flush_eval_scope_locals(ctx, &sync_locals)?; + abi::emit_load_int_immediate(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 0), 0); + load_eval_scope_to_arg(ctx, 1); + move_saved_eval_code_to_eval_args(ctx); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_execute"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + reload_eval_scope_locals(ctx, &sync_locals)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Saves the loaded eval source string while scope setup calls use argument registers. +fn save_eval_code_string(ctx: &mut FunctionContext<'_>) { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, ptr_reg, EVAL_CODE_PTR_OFFSET); + abi::emit_store_to_sp(ctx.emitter, len_reg, EVAL_CODE_LEN_OFFSET); +} + +/// Reloads the saved eval source string into the bridge code pointer/length arguments. +fn move_saved_eval_code_to_eval_args(ctx: &mut FunctionContext<'_>) { + let code_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + let code_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_load_temporary_stack_slot(ctx.emitter, code_ptr_arg, EVAL_CODE_PTR_OFFSET); + abi::emit_load_temporary_stack_slot(ctx.emitter, code_len_arg, EVAL_CODE_LEN_OFFSET); +} + +/// Ensures a persistent eval scope exists and stores its handle in the scratch frame. +fn ensure_eval_scope(ctx: &mut FunctionContext<'_>) -> Result<()> { + let slot = eval_scope_slot(ctx)?; + let offset = ctx.local_offset(slot)?; + let ready = ctx.next_label("eval_scope_ready"); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &ready); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_new"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::store_at_offset(ctx.emitter, result_reg, offset); + ctx.emitter.label(&ready); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_SCOPE_HANDLE_OFFSET); + Ok(()) +} + +/// Returns the hidden frame slot that owns this function's persistent eval scope. +fn eval_scope_slot(ctx: &FunctionContext<'_>) -> Result { + ctx.function + .locals + .iter() + .find(|local| local.kind == LocalKind::EvalScope) + .map(|local| local.id) + .ok_or_else(|| CodegenIrError::invalid_module("eval call missing eval scope local")) +} + +/// Loads the current eval scope handle into the selected integer argument register. +fn load_eval_scope_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + abi::emit_load_temporary_stack_slot(ctx.emitter, arg_reg, EVAL_SCOPE_HANDLE_OFFSET); +} + +/// Collects PHP-visible locals that the current conservative scope sync can round-trip. +fn eval_sync_locals(ctx: &FunctionContext<'_>) -> Vec { + ctx.function + .locals + .iter() + .filter(|local| local.kind == LocalKind::PhpLocal) + .filter_map(|local| { + let name = local.name.clone()?; + let ty = local.php_type.codegen_repr(); + eval_sync_type_supported(&ty).then_some(EvalSyncLocal { + name, + slot: local.id, + ty, + }) + }) + .collect() +} + +/// Returns true when a local type can be boxed to Mixed and restored from Mixed after eval. +fn eval_sync_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Union(_) + ) +} + +/// Flushes visible native locals into the materialized eval scope before executing eval. +fn flush_eval_scope_locals(ctx: &mut FunctionContext<'_>, locals: &[EvalSyncLocal]) -> Result<()> { + for local in locals { + let ty = ctx.load_local_to_result(local.slot)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + emit_eval_scope_set(ctx, local, scope_set_flags_for_type(&ty)); + } + Ok(()) +} + +/// Returns ABI flags for a scope value produced from the given native type. +fn scope_set_flags_for_type(ty: &PhpType) -> i64 { + if matches!(ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + 0 + } else { + EVAL_SCOPE_FLAG_OWNED + } +} + +/// Calls `__elephc_eval_scope_set` for one boxed local value. +fn emit_eval_scope_set(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Reloads synchronized locals from the eval scope after the eval interpreter returns. +fn reload_eval_scope_locals(ctx: &mut FunctionContext<'_>, locals: &[EvalSyncLocal]) -> Result<()> { + for local in locals { + emit_eval_scope_get(ctx, local); + let missing = ctx.next_label("eval_scope_reload_missing"); + let done = ctx.next_label("eval_scope_reload_done"); + emit_branch_if_scope_entry_missing(ctx, &missing); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 0); + store_mixed_scope_cell_to_local(ctx, local)?; + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&missing); + store_missing_scope_entry_to_local(ctx, local)?; + ctx.emitter.label(&done); + } + Ok(()) +} + +/// Calls `__elephc_eval_scope_get` and stores out cell/flags at the start of eval scratch. +fn emit_eval_scope_get(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal) { + let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, 0); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, 8); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Branches to `label` when the latest scope-get flags do not mark a visible value. +fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str) { + let flags_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, flags_reg, 8); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible + ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible + ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local + } + } +} + +/// Converts a scope Mixed cell back to the local's native storage type. +fn store_mixed_scope_cell_to_local( + ctx: &mut FunctionContext<'_>, + local: &EvalSyncLocal, +) -> Result<()> { + match local.ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => { + emit_retain_scope_cell_if_owned(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + let offset = ctx.local_offset(local.slot)?; + abi::store_at_offset(ctx.emitter, result_reg, offset); + } + PhpType::Int => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + let offset = ctx.local_offset(local.slot)?; + abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + } + PhpType::Bool => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); + let offset = ctx.local_offset(local.slot)?; + abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + } + PhpType::Float => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + let offset = ctx.local_offset(local.slot)?; + abi::store_at_offset(ctx.emitter, abi::float_result_reg(ctx.emitter), offset); + } + PhpType::Str => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + let offset = ctx.local_offset(local.slot)?; + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::store_at_offset(ctx.emitter, ptr_reg, offset); + abi::store_at_offset(ctx.emitter, len_reg, offset - 8); + } + other => { + return Err(CodegenIrError::unsupported(format!( + "eval scope reload for PHP type {:?}", + other + ))) + } + } + Ok(()) +} + +/// Retains a scope-owned Mixed cell before storing it into a native local owner. +fn emit_retain_scope_cell_if_owned(ctx: &mut FunctionContext<'_>) { + let flags_reg = abi::secondary_scratch_reg(ctx.emitter); + let skip = ctx.next_label("eval_scope_reload_borrowed"); + abi::emit_load_temporary_stack_slot(ctx.emitter, flags_reg, 8); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner + ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner + ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining + } + } + abi::emit_call_label(ctx.emitter, "__rt_incref"); + ctx.emitter.label(&skip); +} + +/// Stores the local fallback used when eval unsets or removes a synchronized local. +fn store_missing_scope_entry_to_local( + ctx: &mut FunctionContext<'_>, + local: &EvalSyncLocal, +) -> Result<()> { + let offset = ctx.local_offset(local.slot)?; + match local.ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => { + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + } + PhpType::Int | PhpType::Bool => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + } + PhpType::Float => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_int_result_to_float_result(ctx.emitter); + abi::store_at_offset(ctx.emitter, abi::float_result_reg(ctx.emitter), offset); + } + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + abi::store_at_offset(ctx.emitter, ptr_reg, offset); + abi::store_at_offset(ctx.emitter, len_reg, offset - 8); + } + other => { + return Err(CodegenIrError::unsupported(format!( + "eval scope missing reload for PHP type {:?}", + other + ))) + } + } + Ok(()) +} + +/// Emits a fatal diagnostic when the eval bridge reports any non-zero status. +fn emit_eval_status_check(ctx: &mut FunctionContext<'_>) { + let ok_label = ctx.next_label("eval_status_ok"); + let parse_error_label = ctx.next_label("eval_status_parse_error"); + let unsupported_label = ctx.next_label("eval_status_unsupported"); + abi::emit_branch_if_int_result_zero(ctx.emitter, &ok_label); + emit_branch_if_eval_status(ctx, EVAL_STATUS_PARSE_ERROR, &parse_error_label); + emit_branch_if_eval_status(ctx, EVAL_STATUS_UNSUPPORTED, &unsupported_label); + emit_eval_fatal_message(ctx, EVAL_RUNTIME_FATAL_MESSAGE); + ctx.emitter.label(&parse_error_label); + emit_eval_fatal_message(ctx, EVAL_PARSE_ERROR_MESSAGE); + ctx.emitter.label(&unsupported_label); + emit_eval_fatal_message(ctx, EVAL_UNSUPPORTED_MESSAGE); + ctx.emitter.label(&ok_label); +} + +/// Branches to a label when the eval bridge returned a specific status code. +fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: &str) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler + } + } +} + +/// Emits an eval diagnostic message and exits the process. +fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { + let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr + ctx.emitter.adrp("x1", &message_label); + ctx.emitter.add_lo12("x1", "x1", &message_label); + ctx.emitter + .instruction(&format!("mov x2, #{}", message_len)); // pass the eval runtime diagnostic byte length + ctx.emitter.syscall(4); + abi::emit_exit(ctx.emitter, 1); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr + abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); + ctx.emitter + .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting + abi::emit_exit(ctx.emitter, 1); + } + } +} diff --git a/src/codegen_support/runtime/emitters.rs b/src/codegen_support/runtime/emitters.rs index 518ac9ae42..f74d121b23 100644 --- a/src/codegen_support/runtime/emitters.rs +++ b/src/codegen_support/runtime/emitters.rs @@ -12,6 +12,7 @@ use super::arrays; use super::buffers; use super::callables; use super::diagnostics; +use super::eval_bridge; use super::exceptions; use super::fibers; use super::generators; @@ -320,6 +321,9 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_mixed_write_stdout(emitter); arrays::emit_object_free_deep(emitter); arrays::emit_refcount(emitter); + if features.eval { + eval_bridge::emit_eval_bridge_runtime(emitter); + } // SPL runtime-managed containers spl::emit_doubly_linked_list_runtime(emitter); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs new file mode 100644 index 0000000000..7128d11bc6 --- /dev/null +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -0,0 +1,448 @@ +//! Purpose: +//! Emits C-ABI wrappers used by the optional `elephc-eval` bridge crate. +//! Adapts Rust staticlib calls to elephc's internal runtime value helper ABI. +//! +//! Called from: +//! - `crate::codegen_support::runtime::emitters::emit_runtime()` when `RuntimeFeatures.eval` is enabled. +//! +//! Key details: +//! - Exported wrapper labels use platform C-symbol mangling because they are +//! referenced from Rust object files, while internal `__rt_*` calls keep the +//! existing assembly ABI. + +use crate::codegen_support::emit::Emitter; +use crate::codegen_support::platform::Arch; + +const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; + +/// Emits every eval value wrapper required by `libelephc-eval`. +pub(crate) fn emit_eval_bridge_runtime(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: eval bridge value wrappers ---"); + match emitter.target.arch { + Arch::AArch64 => emit_aarch64_wrappers(emitter), + Arch::X86_64 => emit_x86_64_wrappers(emitter), + } +} + +/// Emits ARM64 C-ABI wrappers around the internal mixed value helpers. +fn emit_aarch64_wrappers(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("b __rt_mixed_from_value"); // box the null payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_bool"); + emitter.instruction("cmp x0, #0"); // normalize any non-zero C bool payload to PHP true + emitter.instruction("cset x1, ne"); // bool payload is 1 for true and 0 for false + emitter.instruction("mov x0, #3"); // runtime tag 3 = bool + emitter.instruction("mov x2, xzr"); // bool payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box the bool payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_array_new"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for array allocation and boxing + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("mov x9, #4"); // minimum indexed-array capacity for eval literals + emitter.instruction("cmp x0, x9"); // compare requested capacity with the minimum capacity + emitter.instruction("csel x0, x0, x9, hs"); // use max(requested, 4) as the runtime allocation capacity + emitter.instruction("mov x1, #8"); // Mixed indexed arrays store boxed-cell pointers + emitter.instruction("bl __rt_array_new"); // allocate indexed-array storage for boxed Mixed slots + emitter.instruction("ldr x10, [x0, #-8]"); // load the packed indexed-array heap kind word + emitter.instruction("mov x12, #0x80ff"); // preserve indexed-array kind and persistent COW metadata + emitter.instruction("and x10, x10, x12"); // clear the default scalar value_type bits + emitter.instruction("mov x11, #7"); // runtime value_type 7 = boxed Mixed + emitter.instruction("lsl x11, x11, #8"); // move the value_type tag into the packed kind word + emitter.instruction("orr x10, x10, x11"); // stamp the array as carrying boxed Mixed slots + emitter.instruction("str x10, [x0, #-8]"); // persist the updated indexed-array metadata + emitter.instruction("str x0, [sp, #0]"); // save the owned array pointer while allocating the Mixed box + emitter.instruction("mov x0, #24"); // Mixed cells store tag plus two payload words + emitter.instruction("bl __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array + emitter.instruction("mov x9, #5"); // low byte 5 = mixed cell heap kind + emitter.instruction("str x9, [x0, #-8]"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov x10, #4"); // runtime tag 4 = indexed array + emitter.instruction("str x10, [x0]"); // store the indexed-array tag in the Mixed cell + emitter.instruction("ldr x11, [sp, #0]"); // reload the owned indexed-array pointer + emitter.instruction("str x11, [x0, #8]"); // store the array pointer as the Mixed low payload word + emitter.instruction("str xzr, [x0, #16]"); // indexed arrays do not use the high payload word + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the array-new wrapper frame + emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_assoc_new"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for hash allocation and boxing + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("mov x9, #16"); // minimum hash capacity for eval associative literals + emitter.instruction("cmp x0, x9"); // compare requested capacity with the minimum hash capacity + emitter.instruction("csel x0, x0, x9, hs"); // use max(requested, 16) as the hash allocation capacity + emitter.instruction("mov x1, #7"); // runtime value_type 7 = boxed Mixed hash values + emitter.instruction("bl __rt_hash_new"); // allocate associative-array storage for boxed Mixed entries + emitter.instruction("str x0, [sp, #0]"); // save the owned hash pointer while allocating the Mixed box + emitter.instruction("mov x0, #24"); // Mixed cells store tag plus two payload words + emitter.instruction("bl __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new hash + emitter.instruction("mov x9, #5"); // low byte 5 = mixed cell heap kind + emitter.instruction("str x9, [x0, #-8]"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov x10, #5"); // runtime tag 5 = associative array + emitter.instruction("str x10, [x0]"); // store the associative-array tag in the Mixed cell + emitter.instruction("ldr x11, [sp, #0]"); // reload the owned hash pointer + emitter.instruction("str x11, [x0, #8]"); // store the hash pointer as the Mixed low payload word + emitter.instruction("str xzr, [x0, #16]"); // associative arrays do not use the high payload word + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the assoc-new wrapper frame + emitter.instruction("ret"); // return the boxed associative-array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_array_get"); + emitter.instruction("sub sp, sp, #32"); // allocate a wrapper frame for key coercion + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed array receiver while coercing the key + emitter.instruction("mov x0, x1"); // pass the boxed key to the eval key normalizer + emitter.instruction("bl __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed array receiver + emitter.instruction("bl __rt_mixed_array_get"); // read the boxed Mixed element or Mixed(null) + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the array-get wrapper frame + emitter.instruction("ret"); // return the boxed element to Rust + + label_c_global(emitter, "__elephc_eval_value_array_set"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for key coercion and value retention + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed array receiver + emitter.instruction("str x2, [sp, #8]"); // save the boxed value being written + emitter.instruction("mov x0, x1"); // pass the boxed key to the eval key normalizer + emitter.instruction("bl __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("str x1, [sp, #16]"); // save the normalized key low word + emitter.instruction("str x2, [sp, #24]"); // save the normalized key high word + emitter.instruction("ldr x0, [sp, #8]"); // reload the value so the array consumes a retained owner + emitter.instruction("bl __rt_incref"); // retain the boxed value for Mixed array storage + emitter.instruction("ldr x0, [sp, #0]"); // pass the boxed array receiver to the Mixed array setter + emitter.instruction("ldr x1, [sp, #16]"); // pass the normalized key low word to the setter + emitter.instruction("ldr x2, [sp, #24]"); // pass the normalized key high word to the setter + emitter.instruction("ldr x3, [sp, #8]"); // pass the retained boxed value to be consumed by the setter + emitter.instruction("bl __rt_mixed_array_set"); // mutate the boxed Mixed array through the shared runtime helper + emitter.instruction("ldr x0, [sp, #0]"); // return the target boxed array receiver to Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the array-set wrapper frame + emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + + emitter.label("__elephc_eval_key_normalize"); + emitter.instruction("sub sp, sp, #32"); // allocate a helper frame while classifying the boxed eval key + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #16"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the original boxed key for fallback integer casts + emitter.instruction("bl __rt_mixed_unbox"); // expose key tag plus payload words + emitter.instruction("cmp x0, #1"); // is the eval key a string? + emitter.instruction("b.eq __elephc_eval_key_normalize_string"); // normalize PHP string array keys through hash rules + emitter.instruction("cmp x0, #0"); // is the eval key already an integer? + emitter.instruction("b.eq __elephc_eval_key_normalize_int"); // integer keys only need the sentinel high word + emitter.instruction("ldr x0, [sp, #0]"); // reload the original boxed key for PHP integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce non-string keys to the current integer-key fallback + emitter.instruction("mov x1, x0"); // publish the coerced integer key low word + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer array key + emitter.instruction("b __elephc_eval_key_normalize_done"); // return the fallback integer key tuple + emitter.label("__elephc_eval_key_normalize_string"); + emitter.instruction("bl __rt_hash_normalize_key"); // normalize numeric strings while preserving true string keys + emitter.instruction("b __elephc_eval_key_normalize_done"); // return the normalized string/int key tuple + emitter.label("__elephc_eval_key_normalize_int"); + emitter.instruction("mov x2, #-1"); // key_hi = -1 marks an integer array key + emitter.label("__elephc_eval_key_normalize_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the key-normalizer helper frame + emitter.instruction("ret"); // return key_lo/key_hi in x1/x2 + + label_c_global(emitter, "__elephc_eval_value_is_array_like"); + emitter.instruction("cbz x0, __elephc_eval_value_is_array_like_false"); // null handles cannot be indexed as arrays + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __elephc_eval_value_is_array_like_true"); // indexed arrays are valid eval array-write receivers + emitter.instruction("cmp x9, #5"); // tag 5 = associative array + emitter.instruction("b.eq __elephc_eval_value_is_array_like_true"); // associative arrays are valid eval array-write receivers + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.eq __elephc_eval_value_is_array_like_true"); // ArrayAccess-capable objects are delegated to runtime set helpers + emitter.label("__elephc_eval_value_is_array_like_false"); + emitter.instruction("mov x0, #0"); // report false for scalar and null values + emitter.instruction("ret"); // return the boolean result to Rust + emitter.label("__elephc_eval_value_is_array_like_true"); + emitter.instruction("mov x0, #1"); // report true for array-like values + emitter.instruction("ret"); // return the boolean result to Rust + + label_c_global(emitter, "__elephc_eval_value_int"); + emitter.instruction("mov x1, x0"); // move the C integer argument into the mixed payload slot + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box the integer payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_float"); + emitter.instruction("fmov x1, d0"); // move the C double bits into the mixed payload slot + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box the double payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_string"); + emitter.instruction("mov x2, x1"); // move the C string length into mixed value_hi + emitter.instruction("mov x1, x0"); // move the C string pointer into mixed value_lo + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("b __rt_mixed_from_value"); // persist and box the string payload for eval + + label_c_global(emitter, "__elephc_eval_value_add"); + emitter.instruction("b __rt_mixed_numeric_add"); // add two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_sub"); + emitter.instruction("b __rt_mixed_numeric_sub"); // subtract two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_mul"); + emitter.instruction("b __rt_mixed_numeric_mul"); // multiply two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_concat"); + emitter.instruction("sub sp, sp, #64"); // allocate wrapper frame for the right operand and string pairs + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #48"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_string"); // cast the left boxed operand to a PHP string pair + emitter.instruction("stp x1, x2, [sp, #8]"); // save the left string pointer and length + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for string casting + emitter.instruction("bl __rt_mixed_cast_string"); // cast the right boxed operand to a PHP string pair + emitter.instruction("mov x3, x1"); // move the right string pointer into concat's right pointer register + emitter.instruction("mov x4, x2"); // move the right string length into concat's right length register + emitter.instruction("ldp x1, x2, [sp, #8]"); // reload the left string pair for concat + emitter.instruction("bl __rt_concat"); // concatenate the two PHP string pairs + emitter.instruction("mov x0, #1"); // runtime tag 1 = string for boxing the concat result + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the concatenated string + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the concat wrapper frame + emitter.instruction("ret"); // return the boxed concat result to Rust + + label_c_global(emitter, "__elephc_eval_value_echo"); + emitter.instruction("b __rt_mixed_write_stdout"); // echo one boxed mixed value and return to Rust + + label_c_global(emitter, "__elephc_eval_value_truthy"); + emitter.instruction("b __rt_mixed_cast_bool"); // cast one boxed mixed value to PHP truthiness for eval + + label_c_global(emitter, "__elephc_eval_value_release"); + emitter.instruction("b __rt_decref_mixed"); // release one eval-owned boxed Mixed cell +} + +/// Emits Linux x86_64 C-ABI wrappers around the internal mixed value helpers. +fn emit_x86_64_wrappers(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_null"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("jmp __rt_mixed_from_value"); // box the null payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_bool"); + emitter.instruction("xor r10d, r10d"); // prepare the normalized PHP bool payload + emitter.instruction("test rdi, rdi"); // treat any non-zero C bool argument as true + emitter.instruction("setne r10b"); // bool payload is 1 for true and 0 for false + emitter.instruction("mov rdi, r10"); // move the normalized bool into mixed value_lo + emitter.instruction("mov eax, 3"); // runtime tag 3 = bool + emitter.instruction("xor esi, esi"); // bool payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box the bool payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_array_new"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve local slots for the array pointer + emitter.instruction("cmp rdi, 4"); // compare requested capacity with the minimum capacity + emitter.instruction("mov r10, 4"); // minimum indexed-array capacity for eval literals + emitter.instruction("cmovb rdi, r10"); // use max(requested, 4) as the runtime allocation capacity + emitter.instruction("mov rsi, 8"); // Mixed indexed arrays store boxed-cell pointers + emitter.instruction("call __rt_array_new"); // allocate indexed-array storage for boxed Mixed slots + emitter.instruction("mov r10, QWORD PTR [rax - 8]"); // load the packed indexed-array heap kind word + emitter.instruction("mov r11, 0xffffffff000080ff"); // preserve heap magic, indexed-array kind, and COW metadata + emitter.instruction("and r10, r11"); // clear the default scalar value_type bits + emitter.instruction("mov r11, 7"); // runtime value_type 7 = boxed Mixed + emitter.instruction("shl r11, 8"); // move the value_type tag into the packed kind word + emitter.instruction("or r10, r11"); // stamp the array as carrying boxed Mixed slots + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // persist the updated indexed-array metadata + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the owned array pointer while allocating the Mixed box + emitter.instruction("mov rax, 24"); // Mixed cells store tag plus two payload words + emitter.instruction("call __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array + emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5)); // materialize the mixed-cell heap kind with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov QWORD PTR [rax], 4"); // runtime tag 4 = indexed array + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the owned indexed-array pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the array pointer as the Mixed low payload word + emitter.instruction("mov QWORD PTR [rax + 16], 0"); // indexed arrays do not use the high payload word + emitter.instruction("add rsp, 16"); // release the array-new wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_assoc_new"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve local slots for the hash pointer + emitter.instruction("cmp rdi, 16"); // compare requested capacity with the minimum hash capacity + emitter.instruction("mov r10, 16"); // minimum hash capacity for eval associative literals + emitter.instruction("cmovb rdi, r10"); // use max(requested, 16) as the hash allocation capacity + emitter.instruction("mov rsi, 7"); // runtime value_type 7 = boxed Mixed hash values + emitter.instruction("call __rt_hash_new"); // allocate associative-array storage for boxed Mixed entries + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the owned hash pointer while allocating the Mixed box + emitter.instruction("mov rax, 24"); // Mixed cells store tag plus two payload words + emitter.instruction("call __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new hash + emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5)); // materialize the mixed-cell heap kind with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov QWORD PTR [rax], 5"); // runtime tag 5 = associative array + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the owned hash pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the hash pointer as the Mixed low payload word + emitter.instruction("mov QWORD PTR [rax + 16], 0"); // associative arrays do not use the high payload word + emitter.instruction("add rsp, 16"); // release the assoc-new wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed associative-array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_array_get"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve local slots for the boxed array receiver + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed array receiver while coercing the key + emitter.instruction("mov rdi, rsi"); // pass the boxed key to the eval key normalizer + emitter.instruction("call __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("mov rsi, rax"); // pass normalized key_lo to the reader + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the boxed array receiver + emitter.instruction("call __rt_mixed_array_get"); // read the boxed Mixed element or Mixed(null) + emitter.instruction("add rsp, 16"); // release the array-get wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed element to Rust + + label_c_global(emitter, "__elephc_eval_value_array_set"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve local slots for receiver, value, and key + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed array receiver + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the boxed value being written + emitter.instruction("mov rdi, rsi"); // pass the boxed key to the eval key normalizer + emitter.instruction("call __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the normalized key low word + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // save the normalized key high word + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the value so the array consumes a retained owner + emitter.instruction("call __rt_incref"); // retain the boxed value for Mixed array storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the boxed array receiver to the Mixed array setter + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the normalized key low word to the setter + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // pass the normalized key high word to the setter + emitter.instruction("mov rcx, QWORD PTR [rbp - 16]"); // pass the retained boxed value to be consumed by the setter + emitter.instruction("call __rt_mixed_array_set"); // mutate the boxed Mixed array through the shared runtime helper + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the target boxed array receiver to Rust + emitter.instruction("add rsp, 32"); // release the array-set wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + + emitter.label("__elephc_eval_key_normalize"); + emitter.instruction("push rbp"); // preserve the caller frame pointer while classifying the eval key + emitter.instruction("mov rbp, rsp"); // establish a stable key-normalizer frame + emitter.instruction("sub rsp, 16"); // reserve an aligned slot for the original boxed key + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the original boxed key for fallback integer casts + emitter.instruction("mov rax, rdi"); // pass the boxed key to mixed_unbox's internal input register + emitter.instruction("call __rt_mixed_unbox"); // expose key tag plus payload words + emitter.instruction("cmp rax, 1"); // is the eval key a string? + emitter.instruction("je __elephc_eval_key_normalize_string"); // normalize PHP string array keys through hash rules + emitter.instruction("test rax, rax"); // is the eval key already an integer? + emitter.instruction("jz __elephc_eval_key_normalize_int"); // integer keys only need the sentinel high word + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the original boxed key for PHP integer coercion + emitter.instruction("mov rax, rdi"); // satisfy mixed_cast_int's mixed_unbox input convention + emitter.instruction("call __rt_mixed_cast_int"); // coerce non-string keys to the current integer-key fallback + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer array key + emitter.instruction("jmp __elephc_eval_key_normalize_done"); // return the fallback integer key tuple + emitter.label("__elephc_eval_key_normalize_string"); + emitter.instruction("mov rax, rdi"); // pass the string key pointer to hash normalization + emitter.instruction("call __rt_hash_normalize_key"); // normalize numeric strings while preserving true string keys + emitter.instruction("jmp __elephc_eval_key_normalize_done"); // return the normalized string/int key tuple + emitter.label("__elephc_eval_key_normalize_int"); + emitter.instruction("mov rax, rdi"); // publish the unboxed integer key low word + emitter.instruction("mov rdx, -1"); // key_hi = -1 marks an integer array key + emitter.label("__elephc_eval_key_normalize_done"); + emitter.instruction("add rsp, 16"); // release the key-normalizer spill slot + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return key_lo/key_hi in rax/rdx + + label_c_global(emitter, "__elephc_eval_value_is_array_like"); + emitter.instruction("test rdi, rdi"); // null handles cannot be indexed as arrays + emitter.instruction("jz __elephc_eval_value_is_array_like_false"); // report false for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __elephc_eval_value_is_array_like_true"); // indexed arrays are valid eval array-write receivers + emitter.instruction("cmp r10, 5"); // tag 5 = associative array + emitter.instruction("je __elephc_eval_value_is_array_like_true"); // associative arrays are valid eval array-write receivers + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("je __elephc_eval_value_is_array_like_true"); // ArrayAccess-capable objects are delegated to runtime set helpers + emitter.label("__elephc_eval_value_is_array_like_false"); + emitter.instruction("mov rax, 0"); // report false for scalar and null values + emitter.instruction("ret"); // return the boolean result to Rust + emitter.label("__elephc_eval_value_is_array_like_true"); + emitter.instruction("mov rax, 1"); // report true for array-like values + emitter.instruction("ret"); // return the boolean result to Rust + + label_c_global(emitter, "__elephc_eval_value_int"); + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box the C integer payload in rdi and return + + label_c_global(emitter, "__elephc_eval_value_float"); + emitter.instruction("movq rdi, xmm0"); // move the C double bits into mixed value_lo + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box the double payload and return to Rust + + label_c_global(emitter, "__elephc_eval_value_string"); + emitter.instruction("mov eax, 1"); // runtime tag 1 = string, with C ptr/len already in rdi/rsi + emitter.instruction("jmp __rt_mixed_from_value"); // persist and box the string payload for eval + + label_c_global(emitter, "__elephc_eval_value_add"); + emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register + emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register + emitter.instruction("jmp __rt_mixed_numeric_add"); // add two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_sub"); + emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register + emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register + emitter.instruction("jmp __rt_mixed_numeric_sub"); // subtract two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_mul"); + emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register + emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register + emitter.instruction("jmp __rt_mixed_numeric_mul"); // multiply two boxed mixed values and return the boxed result + + label_c_global(emitter, "__elephc_eval_value_concat"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for right operand and left string pair + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_string input + emitter.instruction("call __rt_mixed_cast_string"); // cast the left boxed operand to a PHP string pair + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the left string pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the left string length + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for string casting + emitter.instruction("call __rt_mixed_cast_string"); // cast the right boxed operand to a PHP string pair + emitter.instruction("mov rdi, rax"); // move the right string pointer into concat's right pointer register + emitter.instruction("mov rsi, rdx"); // move the right string length into concat's right length register + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the left string pointer for concat + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the left string length for concat + emitter.instruction("call __rt_concat"); // concatenate the two PHP string pairs + emitter.instruction("mov rdi, rax"); // move the concat string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the concat string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string for boxing the concat result + emitter.instruction("call __rt_mixed_from_value"); // persist and box the concatenated string + emitter.instruction("add rsp, 32"); // release the concat wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed concat result to Rust + + label_c_global(emitter, "__elephc_eval_value_echo"); + emitter.instruction("mov rax, rdi"); // move the C boxed value argument into mixed echo input + emitter.instruction("jmp __rt_mixed_write_stdout"); // echo one boxed mixed value and return to Rust + + label_c_global(emitter, "__elephc_eval_value_truthy"); + emitter.instruction("mov rax, rdi"); // move the C boxed value argument into mixed truthiness input + emitter.instruction("jmp __rt_mixed_cast_bool"); // cast one boxed mixed value to PHP truthiness for eval + + label_c_global(emitter, "__elephc_eval_value_release"); + emitter.instruction("mov rax, rdi"); // move the C boxed Mixed argument into the internal release register + emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell +} + +/// Emits a global label with platform C-symbol mangling. +fn label_c_global(emitter: &mut Emitter, name: &str) { + let symbol = emitter.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen_support/runtime/mod.rs b/src/codegen_support/runtime/mod.rs index b21b4dca2c..514e0353d2 100644 --- a/src/codegen_support/runtime/mod.rs +++ b/src/codegen_support/runtime/mod.rs @@ -15,6 +15,7 @@ mod callables; mod data; mod diagnostics; mod emitters; +mod eval_bridge; mod exceptions; mod fibers; /// Runtime helpers for generator state management (yield, resume, stack frames). @@ -22,9 +23,9 @@ pub(crate) mod generators; mod io; mod objects; mod pointers; -mod strings; /// Standard PHP library constants, functions, and classes. pub(crate) mod spl; +mod strings; mod system; /// zval pack/unpack bridge helpers (elephc values ↔ PHP zval structs). mod zval; @@ -37,8 +38,8 @@ pub(crate) use emitters::emit_runtime; /// Emit full runtime helpers (orchestrates all runtime sections). pub(crate) use fibers::{ FIBER_CALLABLE_OFFSET, FIBER_PENDING_THROW_OFFSET, FIBER_STACK_BASE_OFFSET, - FIBER_STACK_SIZE_OFFSET, FIBER_START_ARG_COUNT_OFFSET, FIBER_START_ARGS_MAX, - FIBER_START_ARGS_OFFSET, FIBER_STATE_NOT_STARTED, FIBER_STATE_RUNNING, + FIBER_STACK_SIZE_OFFSET, FIBER_START_ARGS_MAX, FIBER_START_ARGS_OFFSET, + FIBER_START_ARG_COUNT_OFFSET, FIBER_STATE_NOT_STARTED, FIBER_STATE_RUNNING, FIBER_STATE_SUSPENDED, FIBER_STATE_TERMINATED, FIBER_TRANSFER_VALUE_OFFSET, FIBER_USER_ARG_MAX_OFFSET, }; diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index 63ab6ad9c5..d9472f03b2 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -14,6 +14,8 @@ //! - The dynamic builtin dispatcher (descriptor invoker) emits per-builtin //! wrappers — including md5/sha1/hash — that reference the `elephc_crypto` //! staticlib, so its detection forces that crate to link. +//! - `eval()` enables the optional libelephc-eval bridge only when lowered EIR +//! actually references the eval bridge call path. use std::collections::HashMap; @@ -33,6 +35,8 @@ pub struct RuntimeFeatures { /// True when codegen can emit the runtime callable dispatcher (descriptor /// invoker) that builds per-builtin wrappers referencing `elephc_crypto`. pub descriptor_invoker: bool, + /// True when codegen can call the optional eval bridge staticlib. + pub eval: bool, /// True when compiling a `--web` program. Selects the output-capture variant /// of `__rt_stdout_write`, which checks the `_elephc_web_capture` flag and may /// tail-call `elephc_web_write` (a symbol only linked into `--web` binaries). @@ -47,6 +51,7 @@ impl RuntimeFeatures { regex: false, phar_archive: false, descriptor_invoker: false, + eval: false, web: false, } } @@ -58,6 +63,7 @@ impl RuntimeFeatures { regex: true, phar_archive: true, descriptor_invoker: true, + eval: true, web: true, } } @@ -94,6 +100,9 @@ pub fn required_libraries_for_runtime_features(features: RuntimeFeatures) -> Vec // reference `elephc_crypto_hash`; force the crate to link on all targets. libs.push("elephc_crypto".to_string()); } + if features.eval { + libs.push("elephc_eval".to_string()); + } libs } @@ -899,6 +908,18 @@ mod tests { assert!(required_libraries_for_runtime_features(RuntimeFeatures::none()).is_empty()); } + /// Verifies eval runtime features request the eval bridge staticlib for final linking. + #[test] + fn test_eval_runtime_features_require_elephc_eval_library() { + assert_eq!( + required_libraries_for_runtime_features(RuntimeFeatures { + eval: true, + ..RuntimeFeatures::none() + }), + vec!["elephc_eval".to_string()] + ); + } + /// Verifies literal callback dispatch to preg builtins enables regex helpers. #[test] fn test_runtime_features_include_regex_for_call_user_func_literal() { @@ -1021,6 +1042,7 @@ mod tests { regex: false, phar_archive: false, descriptor_invoker: true, + eval: false, web: false, }) .iter() diff --git a/src/ir/function.rs b/src/ir/function.rs index 6b3929263e..d5570be37e 100644 --- a/src/ir/function.rs +++ b/src/ir/function.rs @@ -179,6 +179,7 @@ pub enum LocalKind { NamedArgTemp, IteratorState, GeneratorState, + EvalScope, } /// Function-level shape flags used by lowering and later codegen. diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 6bce5860a6..c708d10c2f 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -88,6 +88,8 @@ pub(crate) struct ClosureCapture { pub value: ValueId, } +const EVAL_SCOPE_LOCAL_NAME: &str = "__eir_eval_scope"; + /// Mutable state for one function body while it is lowered. pub(crate) struct LoweringContext<'m, 'f> { pub builder: Builder<'f>, @@ -453,6 +455,43 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.declare_local_with_kind(name, php_type, LocalKind::OwnedTemp) } + /// Ensures this function has a persistent eval scope handle slot. + pub(crate) fn declare_eval_scope_local(&mut self) -> LocalSlotId { + self.declare_local_with_kind(EVAL_SCOPE_LOCAL_NAME, PhpType::Int, LocalKind::EvalScope) + } + + /// Applies the static part of the eval barrier to visible PHP local storage. + pub(crate) fn apply_eval_barrier(&mut self) { + self.declare_eval_scope_local(); + let local_names = self + .local_slots + .iter() + .filter_map(|(name, slot)| { + let kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); + (kind == LocalKind::PhpLocal).then_some((name.clone(), *slot)) + }) + .collect::>(); + for (name, slot) in local_names { + if eval_barrier_can_widen(&self.builder.local_php_type(slot)) { + self.set_local_type(&name, PhpType::Mixed); + } + } + for (name, ty) in self.local_types.clone() { + let kind = self + .local_kinds + .get(&name) + .copied() + .unwrap_or(LocalKind::PhpLocal); + if kind == LocalKind::PhpLocal && eval_barrier_can_widen(&ty) { + self.local_types.insert(name, PhpType::Mixed); + } + } + } + /// Declares a hidden owner slot for a promoted local ref-cell pointer. fn declare_ref_cell_owner(&mut self, variable: &str, php_type: PhpType) -> LocalSlotId { let name = format!("__eir_ref_owner{}_{}", self.hidden_temp_counter, variable); @@ -1431,6 +1470,14 @@ fn builtin_call_result_owns_storage_as_temporary(name: &str) -> bool { ) } +/// Returns true when eval can replace a local value with an arbitrary boxed cell. +fn eval_barrier_can_widen(php_type: &PhpType) -> bool { + !matches!( + php_type.codegen_repr(), + PhpType::Never | PhpType::Pointer(_) | PhpType::Buffer(_) | PhpType::Packed(_) + ) +} + /// Converts an owner function name into a valid fragment for synthetic closure names. fn closure_name_fragment(value: &str) -> String { value diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 71a2f70ede..26226a66f4 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1852,6 +1852,9 @@ fn emit_builtin_call_value( Some(span), ); release_owned_call_arg_temporaries(ctx, &operands, Some(call.value), span); + if php_symbol_key(name.trim_start_matches('\\')) == "eval" { + ctx.apply_eval_barrier(); + } call } @@ -4044,6 +4047,7 @@ fn lower_builtin_call_args( } match php_symbol_key(name.trim_start_matches('\\')).as_str() { "date" => lower_date_args(ctx, sig, args), + "eval" => lower_eval_args(ctx, sig, args), "json_decode" => lower_json_decode_args(ctx, sig, args), "preg_replace_callback" if !crate::types::call_args::has_named_args(args) @@ -4067,6 +4071,24 @@ fn lower_builtin_call_args( } } +/// Lowers eval's code operand and coerces it through PHP string-conversion rules. +fn lower_eval_args( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + let operands = lower_args_with_signature(ctx, sig, args); + let Some(code) = operands.first().copied() else { + return operands; + }; + let code_value = LoweredValue { + value: code, + ir_type: ctx.builder.value_type(code), + }; + let span = args.first().map(|arg| arg.span); + vec![coerce_to_string_at_span(ctx, code_value, span).value] +} + /// Lowers `usort`/`uasort` arguments, typing an unannotated comparator closure /// against the array's object element type. /// diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 14e4ce6b01..e9e087763d 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -97,6 +97,7 @@ fn include_lowered_runtime_features(module: &mut Module) { module.required_runtime_features.regex |= features.regex; module.required_runtime_features.phar_archive |= features.phar_archive; module.required_runtime_features.descriptor_invoker |= features.descriptor_invoker; + module.required_runtime_features.eval |= features.eval; } /// Derives optional runtime features from the actual EIR instruction stream. @@ -115,6 +116,9 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { if builtin_call_requires_descriptor_invoker(module, function, inst) { features.descriptor_invoker = true; } + if builtin_call_requires_eval(module, inst) { + features.eval = true; + } } Op::ExprCall | Op::CallableDescriptorInvoke => { features.descriptor_invoker = true; @@ -190,6 +194,14 @@ fn builtin_call_requires_descriptor_invoker( .is_some_and(|value| value.php_type.codegen_repr() == PhpType::Str) } +/// Returns true when a lowered builtin call references the optional eval bridge. +fn builtin_call_requires_eval(module: &Module, inst: &crate::ir::Instruction) -> bool { + let Some(name) = builtin_call_name(module, inst) else { + return false; + }; + is_eval_builtin_name(name) +} + /// Returns the canonical builtin name attached to a lowered builtin instruction. fn builtin_call_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { let Some(Immediate::Data(data)) = inst.immediate else { @@ -214,6 +226,11 @@ fn string_callback_operand_index(name: &str) -> Option { } } +/// Returns true when a builtin name denotes PHP's eval language construct. +fn is_eval_builtin_name(name: &str) -> bool { + php_symbol_key(name.trim_start_matches('\\')) == "eval" +} + /// Returns true when a builtin name is lowered through the regex runtime helpers. fn is_regex_builtin_name(name: &str) -> bool { matches!( diff --git a/src/linker.rs b/src/linker.rs index 939538a379..8d3d4cc19f 100644 --- a/src/linker.rs +++ b/src/linker.rs @@ -134,6 +134,15 @@ const BRIDGES: &[BridgeStaticlib] = &[ // Rust runtime/unwinder symbols, like the other bridges. needs_libdl: true, }, + BridgeStaticlib { + lib_name: "elephc_eval", + env_var: "ELEPHC_EVAL_LIB_DIR", + crate_name: "elephc-eval", + flag_name: "eval", + whole_archive: false, + macos_frameworks: &[], + needs_libdl: true, + }, ]; /// Resolves a `--with-` crate flag to its bridge `lib_name`, or `None` @@ -779,6 +788,19 @@ mod tests { assert!(!entry.whole_archive, "tz bridge must not force-load (no link-time side effects)"); } + /// Verifies the optional eval bridge is registered for programs that use `eval()`. + #[test] + fn bridges_includes_elephc_eval() { + let entry = BRIDGES + .iter() + .find(|b| b.lib_name == "elephc_eval") + .expect("elephc_eval must be a registered bridge"); + assert_eq!(entry.crate_name, "elephc-eval"); + assert_eq!(entry.env_var, "ELEPHC_EVAL_LIB_DIR"); + assert_eq!(entry.archive_filename(), "libelephc_eval.a"); + assert!(!entry.whole_archive, "eval bridge must not force-load"); + } + /// Verifies every bridge exposes a non-empty `--with-` name and that /// `bridge_lib_for_flag` maps each one back to its `lib_name`, so the CLI's /// `--with-` validation stays in lockstep with the `BRIDGES` table. diff --git a/src/types/checker/builtins/catalog.rs b/src/types/checker/builtins/catalog.rs index 37f08b92d1..d29b71debe 100644 --- a/src/types/checker/builtins/catalog.rs +++ b/src/types/checker/builtins/catalog.rs @@ -10,6 +10,8 @@ //! - `SUPPORTED_BUILTIN_FUNCTIONS` is the source of truth for PHP-visible builtin names. //! - `INTERNAL_BUILTIN_FUNCTIONS` is now an empty placeholder; internal builtins are //! registered via `internal: true` in `src/builtins/` and recognized through the registry. +//! - `LANGUAGE_CONSTRUCT_FUNCTIONS` participates in call resolution but stays +//! hidden from `function_exists()` and first-class callable surfaces. const SUPPORTED_BUILTIN_FUNCTIONS: &[&str] = &[ "buffer_free", @@ -28,10 +30,14 @@ const SUPPORTED_BUILTIN_FUNCTIONS: &[&str] = &[ // `is_supported_builtin_function_exact` compiles unchanged. const INTERNAL_BUILTIN_FUNCTIONS: &[&str] = &[]; -/// Checks if the exact (lowercase) name is in the PHP-visible or internal builtin lists. +const LANGUAGE_CONSTRUCT_FUNCTIONS: &[&str] = &["eval"]; + +/// Checks if the exact (lowercase) name is in any callable-resolution builtin list. /// Does not perform case folding; use `is_supported_builtin_function` for case-insensitive lookup. fn is_supported_builtin_function_exact(name: &str) -> bool { - SUPPORTED_BUILTIN_FUNCTIONS.contains(&name) || INTERNAL_BUILTIN_FUNCTIONS.contains(&name) + SUPPORTED_BUILTIN_FUNCTIONS.contains(&name) + || INTERNAL_BUILTIN_FUNCTIONS.contains(&name) + || LANGUAGE_CONSTRUCT_FUNCTIONS.contains(&name) } /// Returns the union of PHP-visible supported builtin function names from the diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index 2a25e0a38d..cd89cd1fd0 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -82,6 +82,13 @@ impl Checker { args }; + if name == "eval" { + if let Some(arg) = args.first() { + self.infer_type(arg, env)?; + } + return Ok(Some(PhpType::Mixed)); + } + // Registry-first: if the builtin is registered, use its spec to check arity // and derive the return type (or call the spec's check hook for refined types). // Falls through to the legacy per-area dispatch when the name is not registered. diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index 857768be73..d78d61d9f4 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -111,6 +111,7 @@ impl Checker { active_globals: HashSet::new(), active_statics: HashSet::new(), foreach_key_locals: HashSet::new(), + eval_barrier_active: false, break_continue_depth: 0, finally_break_continue_bases: Vec::new(), warnings: Vec::new(), diff --git a/src/types/checker/driver/top_level.rs b/src/types/checker/driver/top_level.rs index ef24e0d09b..45b15f45a1 100644 --- a/src/types/checker/driver/top_level.rs +++ b/src/types/checker/driver/top_level.rs @@ -27,6 +27,8 @@ impl Checker { &mut self, program: &Program, ) -> (TypeEnv, Vec>) { + let saved_eval_barrier_active = self.eval_barrier_active; + self.eval_barrier_active = false; let mut global_env = self.seed_global_env(); let mut all_errors = Vec::with_capacity(program.len()); for stmt in program { @@ -38,6 +40,7 @@ impl Checker { .unwrap_or_default(); all_errors.push(stmt_errors); } + self.eval_barrier_active = saved_eval_barrier_active; (global_env, all_errors) } diff --git a/src/types/checker/inference/expr/effects.rs b/src/types/checker/inference/expr/effects.rs index 0d89efbbc0..7c94e33050 100644 --- a/src/types/checker/inference/expr/effects.rs +++ b/src/types/checker/inference/expr/effects.rs @@ -45,6 +45,10 @@ impl Checker { env: &mut TypeEnv, ) -> Result { match &expr.kind { + ExprKind::Variable(name) if self.eval_barrier_active && !env.contains_key(name) => { + env.insert(name.clone(), PhpType::Mixed); + Ok(PhpType::Mixed) + } ExprKind::Assignment { target, value, @@ -256,6 +260,9 @@ impl Checker { promote_indexed_local_for_element_unset(arg, env); } } + if builtin_name.eq_ignore_ascii_case("eval") { + self.mark_eval_barrier(env); + } Ok(ty) } ExprKind::NewObject { args, .. } | ExprKind::StaticMethodCall { args, .. } => { @@ -356,6 +363,22 @@ impl Checker { .get(var_name) .is_some_and(callable_target_is_preg_replace_callback) } + + /// Marks the active statement stream as having crossed eval and widens local facts. + fn mark_eval_barrier(&mut self, env: &mut TypeEnv) { + self.eval_barrier_active = true; + let local_names = env.keys().cloned().collect::>(); + for ty in env.values_mut() { + *ty = PhpType::Mixed; + } + for name in local_names { + self.closure_return_types.remove(&name); + self.callable_sigs.remove(&name); + self.callable_captures.remove(&name); + self.callable_array_targets.remove(&name); + self.first_class_callable_targets.remove(&name); + } + } } /// Returns true when a first-class callable target is PHP `preg_replace_callback`. diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index a8c6edbc44..442db30950 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -10,6 +10,7 @@ use crate::errors::CompileError; use crate::parser::ast::{Expr, ExprKind}; +use crate::span::Span; use crate::types::{ merge_array_key_types, normalized_array_key_type, packed_type_size, PhpType, TypeEnv, }; @@ -43,9 +44,7 @@ impl Checker { ExprKind::StringLiteral(_) => Ok(PhpType::Str), ExprKind::IntLiteral(_) => Ok(PhpType::Int), ExprKind::FloatLiteral(_) => Ok(PhpType::Float), - ExprKind::Variable(name) => env.get(name).cloned().ok_or_else(|| { - CompileError::new(expr.span, &format!("Undefined variable: ${}", name)) - }), + ExprKind::Variable(name) => self.variable_type_or_eval_dynamic(name, expr.span, env), ExprKind::Negate(inner) => { let ty = self.infer_type(inner, env)?; match ty { @@ -100,6 +99,7 @@ impl Checker { expr.span, &format!("Cannot increment/decrement ${} of type {:?}", name, other), )), + None if self.eval_barrier_active => Ok(PhpType::Int), None => Err(CompileError::new( expr.span, &format!("Undefined variable: ${}", name), @@ -671,6 +671,19 @@ impl Checker { } } + /// Returns a variable type, allowing dynamic eval-created locals after an eval barrier. + fn variable_type_or_eval_dynamic( + &self, + name: &str, + span: Span, + env: &TypeEnv, + ) -> Result { + env.get(name) + .cloned() + .or_else(|| self.eval_barrier_active.then_some(PhpType::Mixed)) + .ok_or_else(|| CompileError::new(span, &format!("Undefined variable: ${}", name))) + } + /// Returns the element type of an array literal that contains at least one /// spread of an associative array. /// diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index 1685e7a0d4..a8c98ef019 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -150,6 +150,11 @@ pub(crate) struct Checker { /// promoting to `AssocArray` like a statically-known string key would. Mirrors /// the lowering's `foreach_int_key_locals` lifetime (per function, not popped). pub foreach_key_locals: HashSet, + /// Whether the active local statement stream has crossed an `eval()` call. + /// + /// Once set, unknown local reads are treated as dynamic `Mixed` values because + /// eval fragments can create caller-scope variables at runtime. + pub eval_barrier_active: bool, /// Active break/continue target depth in the current function or closure body. pub break_continue_depth: usize, /// Stacks of break/continue depths at each enclosing `finally` block boundary, diff --git a/src/types/checker/type_compat/declarations.rs b/src/types/checker/type_compat/declarations.rs index ec7b0415ef..cf1845fcfb 100644 --- a/src/types/checker/type_compat/declarations.rs +++ b/src/types/checker/type_compat/declarations.rs @@ -307,6 +307,7 @@ impl Checker { let saved_globals = self.active_globals.clone(); let saved_statics = self.active_statics.clone(); let saved_foreach_keys = self.foreach_key_locals.clone(); + let saved_eval_barrier_active = self.eval_barrier_active; let saved_break_continue_depth = self.break_continue_depth; let saved_finally_break_continue_bases = self.finally_break_continue_bases.clone(); @@ -314,6 +315,7 @@ impl Checker { self.active_globals.clear(); self.active_statics.clear(); self.foreach_key_locals.clear(); + self.eval_barrier_active = false; self.break_continue_depth = 0; self.finally_break_continue_bases.clear(); @@ -323,6 +325,7 @@ impl Checker { self.active_globals = saved_globals; self.active_statics = saved_statics; self.foreach_key_locals = saved_foreach_keys; + self.eval_barrier_active = saved_eval_barrier_active; self.break_continue_depth = saved_break_continue_depth; self.finally_break_continue_bases = saved_finally_break_continue_bases; diff --git a/src/types/signatures.rs b/src/types/signatures.rs index f9f93cfa62..4dd4b01d72 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -99,6 +99,8 @@ pub(crate) fn builtin_call_sig(name: &str) -> Option { /// longer needed. pub(crate) fn legacy_builtin_call_sig(name: &str) -> Option { match name { + "eval" => Some(fixed(&["code"])), + "time" | "phpversion" | "json_last_error" | "json_last_error_msg" | "pi" | "ptr_null" | "getcwd" | "sys_get_temp_dir" | "tmpfile" | "hash_algos" | "date_default_timezone_get" => Some(fixed(&[])), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs new file mode 100644 index 0000000000..45250b7ea4 --- /dev/null +++ b/tests/codegen/eval.rs @@ -0,0 +1,319 @@ +//! Purpose: +//! Integration tests for the initial `eval()` bridge wiring. +//! Covers language-construct visibility, conditional bridge linking, and the +//! base runtime interpreter path for scalar, branch, and indexed-array eval fragments. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval` through Rust's test harness. +//! +//! Key details: +//! - These tests intentionally cover the first scalar/control-flow/indexed-array subset, +//! not full PHP eval scope synchronization or dynamic declaration semantics. + +use crate::support::*; + +/// Verifies `eval` is resolved as a language construct, not a PHP-visible callable function. +#[test] +fn test_eval_is_not_function_exists_or_callable() { + let out = compile_and_run( + r#" "Ada"]; +eval('echo $items["name"];'); +"#, + ); + assert_eq!(out, "Ada"); +} + +/// Verifies eval can write string-keyed native associative arrays through Mixed helpers. +#[test] +fn test_eval_writes_native_assoc_array_string_key() { + let out = compile_and_run( + r#" "Ada"]; +eval('$items["name"] = "Grace";'); +echo $items["name"]; +"#, + ); + assert_eq!(out, "Grace"); +} + +/// Verifies eval can create and read associative array literals with string keys. +#[test] +fn test_eval_assoc_array_literal_and_string_key_read() { + let out = compile_and_run(r#" "Ada"]["name"];');"#); + assert_eq!(out, "Ada"); +} + +/// Verifies eval-created associative arrays remain visible to native code. +#[test] +fn test_eval_created_assoc_array_is_visible_after_eval() { + let out = compile_and_run( + r#" "Ada"];'); +echo $items["name"]; +"#, + ); + assert_eq!(out, "Ada"); +} + +/// Verifies `return` inside eval becomes the expression result of `eval(...)`. +#[test] +fn test_eval_return_value_is_available_to_native_code() { + let out = compile_and_run("/debug` alongside the test - // binaries; surface that directory on the linker search path automatically - // whenever a compiled program links any bridge, so PDO/crypto/phar/tz/image - // tests get the same robust, absolute `-L` as TLS instead of depending on a - // cwd-relative lookup. The Docker scripts override CARGO_TARGET_DIR to point at - // a shared volume, so honour that envvar before falling back to the in-tree - // target/. - let needs_bridge_staticlib = actual_link_libs.iter().any(|l| { - *l == "elephc_tls" - || *l == "elephc_pdo" - || *l == "elephc_crypto" - || *l == "elephc_phar" - || *l == "elephc_tz" - || *l == "elephc_image" - }); + // Bridge staticlibs live in `/debug` alongside the test binaries; + // surface that directory automatically whenever a compiled program links a + // known bridge, so tests get robust absolute `-L` paths instead of depending + // on cwd-relative lookup. Docker scripts override CARGO_TARGET_DIR to point + // at a shared volume, so honour that envvar before falling back in-tree. + let needs_bridge_staticlib = !requested_bridge_staticlibs(&actual_link_libs).is_empty(); let bridge_staticlib_dir = match std::env::var("CARGO_TARGET_DIR") { Ok(dir) if !dir.is_empty() => format!("{}/debug", dir), _ => format!("{}/target/debug", env!("CARGO_MANIFEST_DIR")), diff --git a/tests/error_tests/misc/functions.rs b/tests/error_tests/misc/functions.rs index 42e3495327..d2a22a5e58 100644 --- a/tests/error_tests/misc/functions.rs +++ b/tests/error_tests/misc/functions.rs @@ -48,6 +48,15 @@ fn test_error_first_class_callable_rejects_unsupported_builtin() { ); } +/// Verifies `eval` remains a language construct and is rejected as a first-class callable. +#[test] +fn test_error_eval_first_class_callable_is_rejected() { + expect_error( + " Date: Mon, 15 Jun 2026 14:55:06 +0200 Subject: [PATCH 0002/1208] Support nested eval fragments --- crates/elephc-eval/src/eval_ir.rs | 4 ++ crates/elephc-eval/src/interpreter.rs | 44 ++++++++++++++++++ crates/elephc-eval/src/parser.rs | 38 ++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 22 +++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- scripts/test-linux-arm64.sh | 7 +-- scripts/test-linux-x86_64.sh | 7 +-- src/codegen_support/runtime/eval_bridge.rs | 53 +++++++++++++++++++++- tests/codegen/eval.rs | 20 ++++++++ 10 files changed, 189 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 97738e26be..28338a9cb4 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -82,6 +82,10 @@ pub enum EvalExpr { array: Box, index: Box, }, + Call { + name: String, + args: Vec, + }, Const(EvalConst), LoadVar(String), Print(Box), diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 12d363fabf..d7fc6ab305 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -13,6 +13,7 @@ use crate::errors::EvalStatus; use crate::eval_ir::{EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalProgram, EvalStmt}; +use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership}; use crate::value::RuntimeCellHandle; @@ -99,6 +100,9 @@ pub trait RuntimeValueOps { /// Emits one runtime cell to stdout using PHP echo semantics. fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; + /// Casts one runtime cell to a PHP string and copies its bytes for parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus>; + /// Converts one runtime cell to PHP boolean truthiness. fn truthy(&mut self, value: RuntimeCellHandle) -> Result; } @@ -236,6 +240,7 @@ fn eval_expr( let index = eval_expr(index, scope, values)?; values.array_get(array, index) } + EvalExpr::Call { name, args } => eval_call(name, args, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::LoadVar(name) => scope.visible_cell(name).map_or_else(|| values.null(), Ok), EvalExpr::Print(inner) => { @@ -256,6 +261,25 @@ fn eval_expr( } } +/// Evaluates supported function-like calls from a runtime eval fragment. +fn eval_call( + name: &str, + args: &[EvalExpr], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if name != "eval" { + return Err(EvalStatus::UnsupportedConstruct); + } + let [code] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let code = eval_expr(code, scope, values)?; + let code = values.string_bytes(code)?; + let program = parse_fragment(&code).map_err(|_| EvalStatus::ParseError)?; + execute_program(&program, scope, values) +} + /// Evaluates an indexed array literal into a boxed runtime Mixed array. fn eval_indexed_array( elements: &[EvalArrayElement], @@ -543,6 +567,11 @@ mod tests { Ok(()) } + /// Casts one fake runtime cell to bytes for nested eval parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + Ok(self.stringify(value).into_bytes()) + } + /// Returns PHP-like truthiness for fake runtime cells. fn truthy(&mut self, value: RuntimeCellHandle) -> Result { Ok(match self.get(value) { @@ -677,6 +706,21 @@ mod tests { assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); } + /// Verifies nested eval calls parse and execute against the same dynamic scope. + #[test] + fn execute_program_nested_eval_uses_same_scope() { + let program = + parse_fragment(br#"eval("$x = $x + 4;"); return $x;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); + } + /// Verifies indexed array writes mutate an existing scope array. #[test] fn execute_program_writes_indexed_scope_array() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 4db42781ba..857c227f9c 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -520,6 +520,9 @@ impl Parser { let expr = self.parse_expr()?; Ok(EvalExpr::Print(Box::new(expr))) } + TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { + self.parse_call_expr(name.clone()) + } TokenKind::LBracket => self.parse_array_literal(), TokenKind::LParen => { self.advance(); @@ -532,6 +535,27 @@ impl Parser { } } + /// Parses a function-like call expression and its source-order arguments. + fn parse_call_expr(&mut self, name: String) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let mut args = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(EvalExpr::Call { name, args }); + } + loop { + args.push(self.parse_expr()?); + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RParen) { + return Ok(EvalExpr::Call { name, args }); + } + } + self.expect(TokenKind::RParen)?; + Ok(EvalExpr::Call { name, args }) + } + /// Parses an array literal with source-order optional key/value element expressions. fn parse_array_literal(&mut self) -> Result { self.expect(TokenKind::LBracket)?; @@ -684,6 +708,20 @@ mod tests { ); } + /// Verifies call expressions preserve their callee name and source-order arguments. + #[test] + fn parse_fragment_accepts_call_expression_source() { + let program = + parse_fragment(br#"return eval("return 1;");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "eval".to_string(), + args: vec![EvalExpr::Const(EvalConst::String("return 1;".to_string()))], + }))] + ); + } + /// Verifies indexed array literals and reads parse as runtime array expressions. #[test] fn parse_fragment_accepts_indexed_array_read_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 2535d7a357..252bb4cc0b 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -48,6 +48,11 @@ unsafe extern "C" { right: *mut RuntimeCell, ) -> *mut RuntimeCell; fn __elephc_eval_value_echo(value: *mut RuntimeCell); + fn __elephc_eval_value_string_bytes( + value: *mut RuntimeCell, + out_ptr: *mut *const u8, + out_len: *mut u64, + ) -> u64; fn __elephc_eval_value_truthy(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_release(value: *mut RuntimeCell); } @@ -188,6 +193,23 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(()) } + /// Casts one boxed Mixed cell to a PHP string and copies the bytes into Rust memory. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + let mut ptr = std::ptr::null(); + let mut len = 0; + let ok = unsafe { __elephc_eval_value_string_bytes(value.as_ptr(), &mut ptr, &mut len) }; + if ok == 0 || (len > 0 && ptr.is_null()) { + return Err(EvalStatus::RuntimeFatal); + } + let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = if len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(ptr, len) } + }; + Ok(bytes.to_vec()) + } + /// Converts one boxed Mixed cell to PHP truthiness through the generated runtime wrapper. fn truthy(&mut self, value: RuntimeCellHandle) -> Result { Ok(unsafe { __elephc_eval_value_truthy(value.as_ptr()) != 0 }) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 7fc43a1608..f8f59e46e9 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, basic control flow, indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, basic control flow, indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 898cac5be8..405b84d0fb 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ` Date: Mon, 15 Jun 2026 14:59:52 +0200 Subject: [PATCH 0003/1208] Support for loops in eval fragments --- crates/elephc-eval/src/eval_ir.rs | 6 ++ crates/elephc-eval/src/interpreter.rs | 73 ++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 98 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 24 +++++++ 6 files changed, 203 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 28338a9cb4..f1ef070529 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -54,6 +54,12 @@ pub enum EvalStmt { Break, Continue, Echo(EvalExpr), + For { + init: Vec, + condition: Option, + update: Vec, + body: Vec, + }, If { condition: EvalExpr, then_branch: Vec, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d7fc6ab305..f9c5601d56 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -171,6 +171,12 @@ fn execute_stmt( values.echo(value)?; Ok(EvalControl::None) } + EvalStmt::For { + init, + condition, + update, + body, + } => execute_for_stmt(init, condition.as_ref(), update, body, scope, values), EvalStmt::If { condition, then_branch, @@ -218,6 +224,41 @@ fn execute_stmt( } } +/// Executes a PHP `for` loop while preserving update-on-continue semantics. +fn execute_for_stmt( + init: &[EvalStmt], + condition: Option<&EvalExpr>, + update: &[EvalStmt], + body: &[EvalStmt], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_statements(init, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => return Ok(EvalControl::None), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + loop { + if let Some(condition) = condition { + let condition = eval_expr(condition, scope, values)?; + if !values.truthy(condition)? { + break; + } + } + match execute_statements(body, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + match execute_statements(update, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + /// Evaluates one expression to an opaque runtime-cell handle. fn eval_expr( expr: &EvalExpr, @@ -681,6 +722,38 @@ mod tests { assert_eq!(values.get(flag), FakeValue::Bool(false)); } + /// Verifies for loops run init, condition, update, and body in PHP order. + #[test] + fn execute_program_for_loop_updates_after_body() { + let program = parse_fragment(br#"for ($i = 3; $i; $i = $i - 1) { echo $i; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "321"); + assert_eq!(values.get(i), FakeValue::Int(0)); + } + + /// Verifies `continue` in a for loop still runs the update clause. + #[test] + fn execute_program_for_continue_runs_update_clause() { + let program = parse_fragment( + br#"for ($i = 3; $i; $i = $i - 1) { if ($i - 1) { continue; } echo "done"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "done"); + assert_eq!(values.get(i), FakeValue::Int(0)); + } + /// Verifies indexed array literals and reads execute through runtime hooks. #[test] fn execute_program_reads_indexed_array_literal() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 857c227f9c..50986e3fb2 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -304,6 +304,7 @@ impl Parser { self.expect_semicolon()?; Ok(vec![EvalStmt::Echo(expr)]) } + TokenKind::Ident(name) if name == "for" => self.parse_for_stmt(), TokenKind::Ident(name) if name == "if" => self.parse_if_stmt(), TokenKind::Ident(name) if name == "return" => { self.advance(); @@ -348,6 +349,77 @@ impl Parser { Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) } + /// Parses `for (init; condition; update) { ... }`. + fn parse_for_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let init = self.parse_for_init_clause()?; + self.expect_semicolon()?; + let condition = if matches!(self.current(), TokenKind::Semicolon) { + None + } else { + Some(self.parse_expr()?) + }; + self.expect_semicolon()?; + let update = self.parse_for_update_clause()?; + let body = self.parse_block()?; + Ok(vec![EvalStmt::For { + init, + condition, + update, + body, + }]) + } + + /// Parses the optional first clause of a `for` loop. + fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Semicolon) { + return Ok(Vec::new()); + } + self.parse_for_clause_stmt() + } + + /// Parses the optional update clause of a `for` loop. + fn parse_for_update_clause(&mut self) -> Result, EvalParseError> { + if self.consume(TokenKind::RParen) { + return Ok(Vec::new()); + } + let statements = self.parse_for_clause_stmt()?; + self.expect(TokenKind::RParen)?; + Ok(statements) + } + + /// Parses one statement-like `for` clause without consuming a delimiter. + fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { + match self.current() { + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_clause(name.clone()) + } + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::Equal) => { + let name = name.clone(); + self.advance(); + self.advance(); + let value = self.parse_expr()?; + Ok(vec![EvalStmt::StoreVar { name, value }]) + } + _ => { + let expr = self.parse_expr()?; + Ok(vec![EvalStmt::Expr(expr)]) + } + } + } + + /// Parses `$name[index] = expr` in a `for` clause. + fn parse_array_set_clause(&mut self, name: String) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + /// Parses `if (expr) { ... } [else { ... }]`. fn parse_if_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -696,6 +768,32 @@ mod tests { ); } + /// Verifies for loops lower clauses and body statements separately. + #[test] + fn parse_fragment_accepts_for_source() { + let program = parse_fragment(br#"for ($i = 2; $i; $i = $i - 1) { echo $i; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::For { + init: vec![EvalStmt::StoreVar { + name: "i".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }], + condition: Some(EvalExpr::LoadVar("i".to_string())), + update: vec![EvalStmt::StoreVar { + name: "i".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }], + body: vec![EvalStmt::Echo(EvalExpr::LoadVar("i".to_string()))], + }] + ); + } + /// Verifies print fragments lower to expression-form print with the printed value. #[test] fn parse_fragment_accepts_print_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index f8f59e46e9..d5c8a5fbe3 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, basic control flow, indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, basic control flow (`if`/`while`/`for`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 405b84d0fb..a6b245023b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ` Date: Mon, 15 Jun 2026 15:19:53 +0200 Subject: [PATCH 0004/1208] Support comparisons in eval fragments --- crates/elephc-eval/src/eval_ir.rs | 6 + crates/elephc-eval/src/interpreter.rs | 106 +++++ crates/elephc-eval/src/parser.rs | 120 +++++- crates/elephc-eval/src/runtime_hooks.rs | 33 ++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + src/codegen_support/runtime/eval_bridge.rs | 431 +++++++++++++++++++++ tests/codegen/eval.rs | 34 ++ 9 files changed, 730 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index f1ef070529..fd52ddc0f9 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -126,4 +126,10 @@ pub enum EvalBinOp { Sub, Mul, Concat, + LooseEq, + LooseNotEq, + Lt, + LtEq, + Gt, + GtEq, } diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f9c5601d56..0cc8beba07 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -97,6 +97,14 @@ pub trait RuntimeValueOps { right: RuntimeCellHandle, ) -> Result; + /// Compares two runtime cells and returns a boxed PHP boolean cell. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + /// Emits one runtime cell to stdout using PHP echo semantics. fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; @@ -297,6 +305,12 @@ fn eval_expr( EvalBinOp::Sub => values.sub(left, right), EvalBinOp::Mul => values.mul(left, right), EvalBinOp::Concat => values.concat(left, right), + EvalBinOp::LooseEq + | EvalBinOp::LooseNotEq + | EvalBinOp::Lt + | EvalBinOp::LtEq + | EvalBinOp::Gt + | EvalBinOp::GtEq => values.compare(*op, left, right), } } } @@ -601,6 +615,27 @@ mod tests { self.string(&(left + &right)) } + /// Compares fake scalar cells and returns a fake PHP boolean. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let result = match op { + EvalBinOp::LooseEq => self.loose_eq(left, right), + EvalBinOp::LooseNotEq => !self.loose_eq(left, right), + EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, + EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, + EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, + EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, + EvalBinOp::Add | EvalBinOp::Sub | EvalBinOp::Mul | EvalBinOp::Concat => { + return Err(EvalStatus::UnsupportedConstruct); + } + }; + self.bool_value(result) + } + /// Appends fake echo output for interpreter tests. fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { let value = self.stringify(value); @@ -628,6 +663,62 @@ mod tests { } impl FakeOps { + /// Compares fake scalar values with the same loose rules covered by eval tests. + fn loose_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Bool(left), right) => left == self.fake_truthy(&right), + (left, FakeValue::Bool(right)) => self.fake_truthy(&left) == right, + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Null, FakeValue::String(value)) + | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), + (FakeValue::String(left), FakeValue::String(right)) => { + match (left.parse::(), right.parse::()) { + (Ok(left), Ok(right)) => left == right, + _ => left == right, + } + } + (FakeValue::String(left), right) => left + .parse::() + .is_ok_and(|left| left == self.fake_numeric(&right)), + (left, FakeValue::String(right)) => right + .parse::() + .is_ok_and(|right| self.fake_numeric(&left) == right), + (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), + } + } + + /// Converts one fake scalar cell to a numeric value for comparison tests. + fn numeric(&self, handle: RuntimeCellHandle) -> Result { + Ok(self.fake_numeric(&self.get(handle))) + } + + /// Converts a fake value to the numeric scalar used by comparison tests. + fn fake_numeric(&self, value: &FakeValue) -> f64 { + match value { + FakeValue::Null => 0.0, + FakeValue::Bool(false) => 0.0, + FakeValue::Bool(true) => 1.0, + FakeValue::Int(value) => *value as f64, + FakeValue::Float(value) => *value, + FakeValue::String(value) => value.parse::().unwrap_or(0.0), + FakeValue::Array(value) => value.len() as f64, + FakeValue::Assoc(value) => value.len() as f64, + } + } + + /// Returns fake PHP truthiness for already-loaded test values. + fn fake_truthy(&self, value: &FakeValue) -> bool { + match value { + FakeValue::Null => false, + FakeValue::Bool(value) => *value, + FakeValue::Int(value) => *value != 0, + FakeValue::Float(value) => *value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + } + } + /// Converts a fake runtime cell to a PHP-like string for test echo/concat. fn stringify(&self, handle: RuntimeCellHandle) -> String { match self.get(handle) { @@ -754,6 +845,21 @@ mod tests { assert_eq!(values.get(i), FakeValue::Int(0)); } + /// Verifies comparison operators return boolean cells usable by echo and branches. + #[test] + fn execute_program_comparisons_return_bool_cells() { + let program = parse_fragment( + br#"echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; if ("10" == 10) { echo "n"; } if ("a" != "b") { echo "s"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1111ns"); + } + /// Verifies indexed array literals and reads execute through runtime hooks. #[test] fn execute_program_reads_indexed_array_literal() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 50986e3fb2..640112ae60 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -43,6 +43,12 @@ enum TokenKind { Star, Dot, Equal, + EqualEqual, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, FatArrow, Semicolon, LParen, @@ -109,13 +115,43 @@ impl<'a> Lexer<'a> { } '=' => { self.bump_char(); - if self.peek_char() == Some('>') { + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::EqualEqual) + } else if self.peek_char() == Some('>') { self.bump_char(); Ok(TokenKind::FatArrow) } else { Ok(TokenKind::Equal) } } + '!' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::NotEqual) + } else { + Err(EvalParseError::UnexpectedToken) + } + } + '<' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::LessEqual) + } else { + Ok(TokenKind::Less) + } + } + '>' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::GreaterEqual) + } else { + Ok(TokenKind::Greater) + } + } ';' => { self.bump_char(); Ok(TokenKind::Semicolon) @@ -484,9 +520,55 @@ impl Parser { Ok(statements) } - /// Parses an expression using PHP-like precedence for `+` over `.`. + /// Parses an expression using PHP-like comparison, concatenation, and arithmetic precedence. fn parse_expr(&mut self) -> Result { - self.parse_concat() + self.parse_equality() + } + + /// Parses left-associative loose equality and inequality comparisons. + fn parse_equality(&mut self) -> Result { + let mut expr = self.parse_ordering()?; + loop { + let op = if self.consume(TokenKind::EqualEqual) { + EvalBinOp::LooseEq + } else if self.consume(TokenKind::NotEqual) { + EvalBinOp::LooseNotEq + } else { + break; + }; + let right = self.parse_ordering()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative ordered comparisons. + fn parse_ordering(&mut self) -> Result { + let mut expr = self.parse_concat()?; + loop { + let op = if self.consume(TokenKind::Less) { + EvalBinOp::Lt + } else if self.consume(TokenKind::LessEqual) { + EvalBinOp::LtEq + } else if self.consume(TokenKind::Greater) { + EvalBinOp::Gt + } else if self.consume(TokenKind::GreaterEqual) { + EvalBinOp::GtEq + } else { + break; + }; + let right = self.parse_concat()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) } /// Parses left-associative string concatenation. @@ -794,6 +876,38 @@ mod tests { ); } + /// Verifies comparison operators parse with lower precedence than arithmetic. + #[test] + fn parse_fragment_accepts_comparison_source() { + let program = parse_fragment(br#"return $i + 1 < 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Lt, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); + } + + /// Verifies loose equality operators parse as binary EvalIR expressions. + #[test] + fn parse_fragment_accepts_loose_equality_source() { + let program = parse_fragment(br#"return "a" != "b";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LooseNotEq, + left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String("b".to_string()))), + }))] + ); + } + /// Verifies print fragments lower to expression-form print with the printed value. #[test] fn parse_fragment_accepts_print_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 252bb4cc0b..ed2da7e130 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -14,6 +14,8 @@ #[cfg(not(test))] use crate::errors::EvalStatus; #[cfg(not(test))] +use crate::eval_ir::EvalBinOp; +#[cfg(not(test))] use crate::interpreter::RuntimeValueOps; #[cfg(not(test))] use crate::value::{RuntimeCell, RuntimeCellHandle}; @@ -47,6 +49,11 @@ unsafe extern "C" { left: *mut RuntimeCell, right: *mut RuntimeCell, ) -> *mut RuntimeCell; + fn __elephc_eval_value_compare( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + op: u64, + ) -> *mut RuntimeCell; fn __elephc_eval_value_echo(value: *mut RuntimeCell); fn __elephc_eval_value_string_bytes( value: *mut RuntimeCell, @@ -185,6 +192,18 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_concat(left.as_ptr(), right.as_ptr()) }) } + /// Compares two boxed Mixed cells through the generated runtime wrapper. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_compare(left.as_ptr(), right.as_ptr(), compare_op_tag(op)) + }) + } + /// Emits one boxed Mixed cell to stdout through the generated runtime wrapper. fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { unsafe { @@ -215,3 +234,17 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_truthy(value.as_ptr()) != 0 }) } } + +/// Maps an EvalIR comparison operator to the bridge ABI opcode. +#[cfg(not(test))] +fn compare_op_tag(op: EvalBinOp) -> u64 { + match op { + EvalBinOp::LooseEq => 0, + EvalBinOp::LooseNotEq => 1, + EvalBinOp::Lt => 2, + EvalBinOp::LtEq => 3, + EvalBinOp::Gt => 4, + EvalBinOp::GtEq => 5, + EvalBinOp::Add | EvalBinOp::Sub | EvalBinOp::Mul | EvalBinOp::Concat => 0, + } +} diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d5c8a5fbe3..9470effa10 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, basic control flow (`if`/`while`/`for`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, scalar equality/comparisons, basic control flow (`if`/`while`/`for`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a6b245023b..b755565fae 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 4c1c80b021..fdbfd83a53 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -3,6 +3,7 @@ $profile = ["name" => "Ada"]; $result = eval('$x = $x + 2; $created = "dynamic"; return $x + 4;'); eval('$profile["name"] = "Grace";'); +eval('if ($x >= 3) { echo "x>=3\n"; }'); $meta = eval('return ["source" => "eval"];'); echo "x=" . $x . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 2a6aeea3db..111f3f0315 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -215,6 +215,201 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #64"); // release the concat wrapper frame emitter.instruction("ret"); // return the boxed concat result to Rust + label_c_global(emitter, "__elephc_eval_value_compare"); + emitter.instruction("sub sp, sp, #64"); // allocate a wrapper frame for comparison operands and opcode + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across comparison helpers + emitter.instruction("add x29, sp, #48"); // establish a stable comparison wrapper frame + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand for later casts + emitter.instruction("str x2, [sp, #8]"); // save the eval comparison opcode + emitter.instruction("str x0, [sp, #16]"); // save the left boxed operand for equality helper calls + emitter.instruction("cmp x2, #0"); // is this loose equality? + emitter.instruction("b.eq __elephc_eval_value_compare_eq"); // route == through the mixed loose-equality helper + emitter.instruction("cmp x2, #1"); // is this loose inequality? + emitter.instruction("b.eq __elephc_eval_value_compare_ne"); // route != through the mixed loose-equality helper + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a numeric comparison double + emitter.instruction("str d0, [sp, #24]"); // save the normalized left numeric operand + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a numeric comparison double + emitter.instruction("ldr d1, [sp, #24]"); // reload the left numeric operand for the float comparison + emitter.instruction("ldr x9, [sp, #8]"); // reload the eval comparison opcode for dispatch + emitter.instruction("cmp x9, #2"); // is this a less-than comparison? + emitter.instruction("b.eq __elephc_eval_value_compare_lt"); // materialize left < right from float comparison flags + emitter.instruction("cmp x9, #3"); // is this a less-than-or-equal comparison? + emitter.instruction("b.eq __elephc_eval_value_compare_lte"); // materialize left <= right from float comparison flags + emitter.instruction("cmp x9, #4"); // is this a greater-than comparison? + emitter.instruction("b.eq __elephc_eval_value_compare_gt"); // materialize left > right from float comparison flags + emitter.instruction("cmp x9, #5"); // is this a greater-than-or-equal comparison? + emitter.instruction("b.eq __elephc_eval_value_compare_gte"); // materialize left >= right from float comparison flags + emitter.instruction("mov x1, #0"); // unknown comparison opcodes fail closed as false + emitter.instruction("b __elephc_eval_value_compare_box"); // box the fallback false result + emitter.label("__elephc_eval_value_compare_eq"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the left operand for loose equality + emitter.instruction("ldr x1, [sp, #0]"); // reload the right operand for loose equality + emitter.instruction("bl __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality + emitter.instruction("mov x1, x0"); // move equality into the bool payload register + emitter.instruction("b __elephc_eval_value_compare_box"); // box the equality result + emitter.label("__elephc_eval_value_compare_ne"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the left operand for loose inequality + emitter.instruction("ldr x1, [sp, #0]"); // reload the right operand for loose inequality + emitter.instruction("bl __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality before inversion + emitter.instruction("eor x1, x0, #1"); // invert equality for the != operator + emitter.instruction("b __elephc_eval_value_compare_box"); // box the inequality result + emitter.label("__elephc_eval_value_compare_lt"); + emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for < + emitter.instruction("cset x1, mi"); // ordered less-than becomes boolean true + emitter.instruction("b __elephc_eval_value_compare_box"); // box the less-than result + emitter.label("__elephc_eval_value_compare_lte"); + emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for <= + emitter.instruction("cset x1, ls"); // ordered less-than-or-equal becomes boolean true + emitter.instruction("b __elephc_eval_value_compare_box"); // box the less-than-or-equal result + emitter.label("__elephc_eval_value_compare_gt"); + emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for > + emitter.instruction("cset x1, gt"); // ordered greater-than becomes boolean true + emitter.instruction("b __elephc_eval_value_compare_box"); // box the greater-than result + emitter.label("__elephc_eval_value_compare_gte"); + emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for >= + emitter.instruction("cset x1, ge"); // ordered greater-than-or-equal becomes boolean true + emitter.label("__elephc_eval_value_compare_box"); + emitter.instruction("mov x0, #3"); // runtime tag 3 = bool + emitter.instruction("mov x2, xzr"); // bool payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the comparison result as a Mixed bool + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the comparison wrapper frame + emitter.instruction("ret"); // return the boxed comparison result to Rust + + emitter.label("__elephc_eval_mixed_loose_eq"); + emitter.instruction("sub sp, sp, #96"); // allocate helper slots for unboxed tags, payloads, and casts + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across mixed helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable loose-equality helper frame + emitter.instruction("stp x0, x1, [sp, #0]"); // save incoming boxed operands for later casts + emitter.instruction("bl __rt_mixed_unbox"); // unbox the left eval operand into tag and payload words + emitter.instruction("str x0, [sp, #16]"); // save the left runtime tag + emitter.instruction("stp x1, x2, [sp, #24]"); // save the left payload words + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand for unboxing + emitter.instruction("bl __rt_mixed_unbox"); // unbox the right eval operand into tag and payload words + emitter.instruction("str x0, [sp, #40]"); // save the right runtime tag + emitter.instruction("stp x1, x2, [sp, #48]"); // save the right payload words + emitter.instruction("ldr x9, [sp, #16]"); // reload the left runtime tag for equality dispatch + emitter.instruction("cmp x9, #3"); // does the left operand have PHP bool semantics? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_bool"); // bool comparisons use truthiness on both operands + emitter.instruction("cmp x0, #3"); // does the right operand have PHP bool semantics? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_bool"); // bool comparisons use truthiness on both operands + emitter.instruction("cmp x9, x0"); // do the operands have the same runtime tag? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_same_tag"); // same-tag scalars use focused payload comparisons + emitter.instruction("cmp x9, #8"); // is the left operand null? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_left_null"); // null compares equal only to empty strings before numeric fallback + emitter.instruction("cmp x0, #8"); // is the right operand null? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_right_null"); // null compares equal only to empty strings before numeric fallback + emitter.instruction("cmp x9, #1"); // is a non-matching left operand a string? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_left_string"); // compare numeric strings against numeric scalars + emitter.instruction("cmp x0, #1"); // is a non-matching right operand a string? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_right_string"); // compare numeric strings against numeric scalars + emitter.instruction("b __elephc_eval_mixed_loose_eq_numeric"); // remaining scalar mismatches compare numerically + emitter.label("__elephc_eval_mixed_loose_eq_same_tag"); + emitter.instruction("cmp x9, #8"); // are both operands null? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_true"); // null loosely equals null + emitter.instruction("cmp x9, #1"); // are both operands strings? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_strings"); // strings use PHP loose string equality + emitter.instruction("cmp x9, #2"); // are both operands floats? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_floats"); // floats compare with native floating equality + emitter.instruction("ldr x10, [sp, #24]"); // reload the left low payload word + emitter.instruction("ldr x11, [sp, #48]"); // reload the right low payload word + emitter.instruction("cmp x10, x11"); // compare low payload words for int and pointer-like scalars + emitter.instruction("b.ne __elephc_eval_mixed_loose_eq_false"); // mismatched low payloads are not equal + emitter.instruction("ldr x10, [sp, #32]"); // reload the left high payload word + emitter.instruction("ldr x11, [sp, #56]"); // reload the right high payload word + emitter.instruction("cmp x10, x11"); // compare high payload words for pointer-like scalars + emitter.instruction("cset x0, eq"); // materialize same-tag payload equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the payload equality result + emitter.label("__elephc_eval_mixed_loose_eq_strings"); + emitter.instruction("ldp x1, x2, [sp, #24]"); // reload the left string pointer and length + emitter.instruction("ldp x3, x4, [sp, #48]"); // reload the right string pointer and length + emitter.instruction("bl __rt_str_loose_eq"); // compare strings with PHP loose numeric-string rules + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the string loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_floats"); + emitter.instruction("ldr d1, [sp, #24]"); // reload the left float payload + emitter.instruction("ldr d0, [sp, #48]"); // reload the right float payload + emitter.instruction("fcmp d1, d0"); // compare same-tag float payloads + emitter.instruction("cset x0, eq"); // floats loosely equal only when ordered equal + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the float equality result + emitter.label("__elephc_eval_mixed_loose_eq_left_null"); + emitter.instruction("cmp x0, #1"); // is null being compared with a string? + emitter.instruction("b.ne __elephc_eval_mixed_loose_eq_numeric"); // non-string null comparisons fall back to numeric zero + emitter.instruction("ldr x10, [sp, #56]"); // load the right string length + emitter.instruction("cmp x10, #0"); // null loosely equals only the empty string + emitter.instruction("cset x0, eq"); // materialize the null/string equality result + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the null/string equality result + emitter.label("__elephc_eval_mixed_loose_eq_right_null"); + emitter.instruction("cmp x9, #1"); // is null being compared with a string? + emitter.instruction("b.ne __elephc_eval_mixed_loose_eq_numeric"); // non-string null comparisons fall back to numeric zero + emitter.instruction("ldr x10, [sp, #32]"); // load the left string length + emitter.instruction("cmp x10, #0"); // null loosely equals only the empty string + emitter.instruction("cset x0, eq"); // materialize the string/null equality result + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the string/null equality result + emitter.label("__elephc_eval_mixed_loose_eq_left_string"); + emitter.instruction("cmp x0, #0"); // can the right operand be compared numerically as an int? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_left_string_numeric"); // parse the left string for numeric equality + emitter.instruction("cmp x0, #2"); // can the right operand be compared numerically as a float? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_left_string_numeric"); // parse the left string for numeric equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_false"); // non-numeric string mismatches are not loosely equal here + emitter.label("__elephc_eval_mixed_loose_eq_left_string_numeric"); + emitter.instruction("ldp x1, x2, [sp, #24]"); // reload the left string pointer and length for numeric parsing + emitter.instruction("bl __rt_str_to_number"); // parse the left string under PHP numeric-string rules + emitter.instruction("cbz x0, __elephc_eval_mixed_loose_eq_false"); // non-numeric strings do not equal numeric scalars + emitter.instruction("str d0, [sp, #64]"); // save the parsed left numeric-string value + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right operand to a comparison double + emitter.instruction("ldr d1, [sp, #64]"); // reload the parsed left numeric-string value + emitter.instruction("fcmp d1, d0"); // compare parsed string and numeric scalar values + emitter.instruction("cset x0, eq"); // materialize string/numeric loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the string/numeric equality result + emitter.label("__elephc_eval_mixed_loose_eq_right_string"); + emitter.instruction("cmp x9, #0"); // can the left operand be compared numerically as an int? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_right_string_numeric"); // parse the right string for numeric equality + emitter.instruction("cmp x9, #2"); // can the left operand be compared numerically as a float? + emitter.instruction("b.eq __elephc_eval_mixed_loose_eq_right_string_numeric"); // parse the right string for numeric equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_false"); // non-numeric string mismatches are not loosely equal here + emitter.label("__elephc_eval_mixed_loose_eq_right_string_numeric"); + emitter.instruction("ldp x1, x2, [sp, #48]"); // reload the right string pointer and length for numeric parsing + emitter.instruction("bl __rt_str_to_number"); // parse the right string under PHP numeric-string rules + emitter.instruction("cbz x0, __elephc_eval_mixed_loose_eq_false"); // non-numeric strings do not equal numeric scalars + emitter.instruction("str d0, [sp, #64]"); // save the parsed right numeric-string value + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left operand to a comparison double + emitter.instruction("ldr d1, [sp, #64]"); // reload the parsed right numeric-string value + emitter.instruction("fcmp d0, d1"); // compare numeric scalar and parsed string values + emitter.instruction("cset x0, eq"); // materialize numeric/string loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the numeric/string equality result + emitter.label("__elephc_eval_mixed_loose_eq_bool"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand for truthiness + emitter.instruction("bl __rt_mixed_cast_bool"); // cast the left operand to PHP truthiness + emitter.instruction("str x0, [sp, #64]"); // save the left truthiness result + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand for truthiness + emitter.instruction("bl __rt_mixed_cast_bool"); // cast the right operand to PHP truthiness + emitter.instruction("ldr x9, [sp, #64]"); // reload the left truthiness result + emitter.instruction("cmp x9, x0"); // compare boolean truthiness for loose equality + emitter.instruction("cset x0, eq"); // materialize bool loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the bool loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_numeric"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the left boxed operand for numeric equality + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left operand to a comparison double + emitter.instruction("str d0, [sp, #64]"); // save the left numeric equality operand + emitter.instruction("ldr x0, [sp, #8]"); // reload the right boxed operand for numeric equality + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right operand to a comparison double + emitter.instruction("ldr d1, [sp, #64]"); // reload the left numeric equality operand + emitter.instruction("fcmp d1, d0"); // compare numeric operands for loose equality + emitter.instruction("cset x0, eq"); // materialize numeric loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the numeric loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_true"); + emitter.instruction("mov x0, #1"); // materialize true for loose equality + emitter.instruction("b __elephc_eval_mixed_loose_eq_done"); // return the true result + emitter.label("__elephc_eval_mixed_loose_eq_false"); + emitter.instruction("mov x0, #0"); // materialize false for loose equality + emitter.label("__elephc_eval_mixed_loose_eq_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the loose-equality helper frame + emitter.instruction("ret"); // return the loose-equality boolean in x0 + label_c_global(emitter, "__elephc_eval_value_echo"); emitter.instruction("b __rt_mixed_write_stdout"); // echo one boxed mixed value and return to Rust @@ -454,6 +649,242 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed concat result to Rust + label_c_global(emitter, "__elephc_eval_value_compare"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across comparison helpers + emitter.instruction("mov rbp, rsp"); // establish a stable comparison wrapper frame + emitter.instruction("sub rsp, 32"); // reserve slots for operands, opcode, and numeric casts + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left boxed operand + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right boxed operand + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the eval comparison opcode + emitter.instruction("cmp rdx, 0"); // is this loose equality? + emitter.instruction("je __elephc_eval_value_compare_eq"); // route == through the mixed loose-equality helper + emitter.instruction("cmp rdx, 1"); // is this loose inequality? + emitter.instruction("je __elephc_eval_value_compare_ne"); // route != through the mixed loose-equality helper + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a numeric comparison double + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save the normalized left numeric operand + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed operand for numeric casting + emitter.instruction("mov rax, rdi"); // move the right boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a numeric comparison double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 32]"); // reload the left numeric operand for the float comparison + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the eval comparison opcode for dispatch + emitter.instruction("cmp r10, 2"); // is this a less-than comparison? + emitter.instruction("je __elephc_eval_value_compare_lt"); // materialize left < right from float comparison flags + emitter.instruction("cmp r10, 3"); // is this a less-than-or-equal comparison? + emitter.instruction("je __elephc_eval_value_compare_lte"); // materialize left <= right from float comparison flags + emitter.instruction("cmp r10, 4"); // is this a greater-than comparison? + emitter.instruction("je __elephc_eval_value_compare_gt"); // materialize left > right from float comparison flags + emitter.instruction("cmp r10, 5"); // is this a greater-than-or-equal comparison? + emitter.instruction("je __elephc_eval_value_compare_gte"); // materialize left >= right from float comparison flags + emitter.instruction("xor eax, eax"); // unknown comparison opcodes fail closed as false + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the fallback false result + emitter.label("__elephc_eval_value_compare_eq"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left operand for loose equality + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right operand for loose equality + emitter.instruction("call __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the equality result + emitter.label("__elephc_eval_value_compare_ne"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left operand for loose inequality + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right operand for loose inequality + emitter.instruction("call __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality before inversion + emitter.instruction("xor rax, 1"); // invert equality for the != operator + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the inequality result + emitter.label("__elephc_eval_value_compare_lt"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for < + emitter.instruction("setb al"); // set true when left is below right + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN less-than results + emitter.instruction("movzx rax, al"); // widen the less-than boolean result + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the less-than result + emitter.label("__elephc_eval_value_compare_lte"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for <= + emitter.instruction("setbe al"); // set true when left is below or equal to right + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN less-than-or-equal results + emitter.instruction("movzx rax, al"); // widen the less-than-or-equal boolean result + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the less-than-or-equal result + emitter.label("__elephc_eval_value_compare_gt"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for > + emitter.instruction("seta al"); // set true when left is above right + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN greater-than results + emitter.instruction("movzx rax, al"); // widen the greater-than boolean result + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the greater-than result + emitter.label("__elephc_eval_value_compare_gte"); + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for >= + emitter.instruction("setae al"); // set true when left is above or equal to right + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN greater-than-or-equal results + emitter.instruction("movzx rax, al"); // widen the greater-than-or-equal boolean result + emitter.label("__elephc_eval_value_compare_box"); + emitter.instruction("mov rdi, rax"); // move the comparison boolean into the Mixed payload register + emitter.instruction("mov eax, 3"); // runtime tag 3 = bool + emitter.instruction("xor esi, esi"); // bool payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the comparison result as a Mixed bool + emitter.instruction("add rsp, 32"); // release the comparison wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed comparison result to Rust + + emitter.label("__elephc_eval_mixed_loose_eq"); + emitter.instruction("push rbp"); // preserve the caller frame pointer across mixed helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable loose-equality helper frame + emitter.instruction("sub rsp, 96"); // allocate helper slots for unboxed tags, payloads, and casts + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the left boxed operand for later casts + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the right boxed operand for later casts + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // unbox the left eval operand into tag and payload words + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the left runtime tag + emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the left low payload word + emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // save the left high payload word + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the right boxed operand for unboxing + emitter.instruction("call __rt_mixed_unbox"); // unbox the right eval operand into tag and payload words + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the right runtime tag + emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // save the right low payload word + emitter.instruction("mov QWORD PTR [rbp - 64], rdx"); // save the right high payload word + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the left runtime tag for equality dispatch + emitter.instruction("cmp r10, 3"); // does the left operand have PHP bool semantics? + emitter.instruction("je __elephc_eval_mixed_loose_eq_bool"); // bool comparisons use truthiness on both operands + emitter.instruction("cmp rax, 3"); // does the right operand have PHP bool semantics? + emitter.instruction("je __elephc_eval_mixed_loose_eq_bool"); // bool comparisons use truthiness on both operands + emitter.instruction("cmp r10, rax"); // do the operands have the same runtime tag? + emitter.instruction("je __elephc_eval_mixed_loose_eq_same_tag"); // same-tag scalars use focused payload comparisons + emitter.instruction("cmp r10, 8"); // is the left operand null? + emitter.instruction("je __elephc_eval_mixed_loose_eq_left_null"); // null compares equal only to empty strings before numeric fallback + emitter.instruction("cmp rax, 8"); // is the right operand null? + emitter.instruction("je __elephc_eval_mixed_loose_eq_right_null"); // null compares equal only to empty strings before numeric fallback + emitter.instruction("cmp r10, 1"); // is a non-matching left operand a string? + emitter.instruction("je __elephc_eval_mixed_loose_eq_left_string"); // compare numeric strings against numeric scalars + emitter.instruction("cmp rax, 1"); // is a non-matching right operand a string? + emitter.instruction("je __elephc_eval_mixed_loose_eq_right_string"); // compare numeric strings against numeric scalars + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_numeric"); // remaining scalar mismatches compare numerically + emitter.label("__elephc_eval_mixed_loose_eq_same_tag"); + emitter.instruction("cmp r10, 8"); // are both operands null? + emitter.instruction("je __elephc_eval_mixed_loose_eq_true"); // null loosely equals null + emitter.instruction("cmp r10, 1"); // are both operands strings? + emitter.instruction("je __elephc_eval_mixed_loose_eq_strings"); // strings use PHP loose string equality + emitter.instruction("cmp r10, 2"); // are both operands floats? + emitter.instruction("je __elephc_eval_mixed_loose_eq_floats"); // floats compare with native floating equality + emitter.instruction("mov r11, QWORD PTR [rbp - 32]"); // reload the left low payload word + emitter.instruction("cmp r11, QWORD PTR [rbp - 56]"); // compare low payload words for int and pointer-like scalars + emitter.instruction("jne __elephc_eval_mixed_loose_eq_false"); // mismatched low payloads are not equal + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // reload the left high payload word + emitter.instruction("cmp r11, QWORD PTR [rbp - 64]"); // compare high payload words for pointer-like scalars + emitter.instruction("sete al"); // materialize same-tag payload equality + emitter.instruction("movzx rax, al"); // widen the payload equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the payload equality result + emitter.label("__elephc_eval_mixed_loose_eq_strings"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the left string pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // reload the left string length + emitter.instruction("mov rdx, QWORD PTR [rbp - 56]"); // reload the right string pointer + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // reload the right string length + emitter.instruction("call __rt_str_loose_eq"); // compare strings with PHP loose numeric-string rules + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the string loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_floats"); + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 32]"); // reload the left float payload + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 56]"); // reload the right float payload + emitter.instruction("ucomisd xmm1, xmm0"); // compare same-tag float payloads + emitter.instruction("sete al"); // set true for ordered float equality + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN equality + emitter.instruction("movzx rax, al"); // widen the float equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the float equality result + emitter.label("__elephc_eval_mixed_loose_eq_left_null"); + emitter.instruction("cmp rax, 1"); // is null being compared with a string? + emitter.instruction("jne __elephc_eval_mixed_loose_eq_numeric"); // non-string null comparisons fall back to numeric zero + emitter.instruction("cmp QWORD PTR [rbp - 64], 0"); // null loosely equals only the empty string + emitter.instruction("sete al"); // materialize the null/string equality result + emitter.instruction("movzx rax, al"); // widen the null/string equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the null/string equality result + emitter.label("__elephc_eval_mixed_loose_eq_right_null"); + emitter.instruction("cmp r10, 1"); // is null being compared with a string? + emitter.instruction("jne __elephc_eval_mixed_loose_eq_numeric"); // non-string null comparisons fall back to numeric zero + emitter.instruction("cmp QWORD PTR [rbp - 40], 0"); // null loosely equals only the empty string + emitter.instruction("sete al"); // materialize the string/null equality result + emitter.instruction("movzx rax, al"); // widen the string/null equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the string/null equality result + emitter.label("__elephc_eval_mixed_loose_eq_left_string"); + emitter.instruction("cmp rax, 0"); // can the right operand be compared numerically as an int? + emitter.instruction("je __elephc_eval_mixed_loose_eq_left_string_numeric"); // parse the left string for numeric equality + emitter.instruction("cmp rax, 2"); // can the right operand be compared numerically as a float? + emitter.instruction("je __elephc_eval_mixed_loose_eq_left_string_numeric"); // parse the left string for numeric equality + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_false"); // non-numeric string mismatches are not loosely equal here + emitter.label("__elephc_eval_mixed_loose_eq_left_string_numeric"); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the left string pointer for numeric parsing + emitter.instruction("mov rdx, QWORD PTR [rbp - 40]"); // reload the left string length for numeric parsing + emitter.instruction("call __rt_str_to_number"); // parse the left string under PHP numeric-string rules + emitter.instruction("test rax, rax"); // did the left string parse as numeric? + emitter.instruction("je __elephc_eval_mixed_loose_eq_false"); // non-numeric strings do not equal numeric scalars + emitter.instruction("movsd QWORD PTR [rbp - 72], xmm0"); // save the parsed left numeric-string value + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed operand for numeric casting + emitter.instruction("mov rax, rdi"); // move the right boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the right operand to a comparison double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 72]"); // reload the parsed left numeric-string value + emitter.instruction("ucomisd xmm1, xmm0"); // compare parsed string and numeric scalar values + emitter.instruction("sete al"); // set true for ordered string/numeric equality + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN equality + emitter.instruction("movzx rax, al"); // widen the string/numeric equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the string/numeric equality result + emitter.label("__elephc_eval_mixed_loose_eq_right_string"); + emitter.instruction("cmp r10, 0"); // can the left operand be compared numerically as an int? + emitter.instruction("je __elephc_eval_mixed_loose_eq_right_string_numeric"); // parse the right string for numeric equality + emitter.instruction("cmp r10, 2"); // can the left operand be compared numerically as a float? + emitter.instruction("je __elephc_eval_mixed_loose_eq_right_string_numeric"); // parse the right string for numeric equality + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_false"); // non-numeric string mismatches are not loosely equal here + emitter.label("__elephc_eval_mixed_loose_eq_right_string_numeric"); + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the right string pointer for numeric parsing + emitter.instruction("mov rdx, QWORD PTR [rbp - 64]"); // reload the right string length for numeric parsing + emitter.instruction("call __rt_str_to_number"); // parse the right string under PHP numeric-string rules + emitter.instruction("test rax, rax"); // did the right string parse as numeric? + emitter.instruction("je __elephc_eval_mixed_loose_eq_false"); // non-numeric strings do not equal numeric scalars + emitter.instruction("movsd QWORD PTR [rbp - 72], xmm0"); // save the parsed right numeric-string value + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left boxed operand for numeric casting + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left operand to a comparison double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 72]"); // reload the parsed right numeric-string value + emitter.instruction("ucomisd xmm0, xmm1"); // compare numeric scalar and parsed string values + emitter.instruction("sete al"); // set true for ordered numeric/string equality + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN equality + emitter.instruction("movzx rax, al"); // widen the numeric/string equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the numeric/string equality result + emitter.label("__elephc_eval_mixed_loose_eq_bool"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left boxed operand for truthiness + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_bool input + emitter.instruction("call __rt_mixed_cast_bool"); // cast the left operand to PHP truthiness + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the left truthiness result + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed operand for truthiness + emitter.instruction("mov rax, rdi"); // move the right boxed operand into mixed_cast_bool input + emitter.instruction("call __rt_mixed_cast_bool"); // cast the right operand to PHP truthiness + emitter.instruction("cmp QWORD PTR [rbp - 72], rax"); // compare boolean truthiness for loose equality + emitter.instruction("sete al"); // materialize bool loose equality + emitter.instruction("movzx rax, al"); // widen the bool equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the bool loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_numeric"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left boxed operand for numeric equality + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left operand to a comparison double + emitter.instruction("movsd QWORD PTR [rbp - 72], xmm0"); // save the left numeric equality operand + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // reload the right boxed operand for numeric equality + emitter.instruction("mov rax, rdi"); // move the right boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the right operand to a comparison double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 72]"); // reload the left numeric equality operand + emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric operands for loose equality + emitter.instruction("sete al"); // set true for ordered numeric equality + emitter.instruction("setnp r10b"); // require an ordered comparison + emitter.instruction("and al, r10b"); // clear unordered NaN equality + emitter.instruction("movzx rax, al"); // widen the numeric equality result + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the numeric loose-equality result + emitter.label("__elephc_eval_mixed_loose_eq_true"); + emitter.instruction("mov rax, 1"); // materialize true for loose equality + emitter.instruction("jmp __elephc_eval_mixed_loose_eq_done"); // return the true result + emitter.label("__elephc_eval_mixed_loose_eq_false"); + emitter.instruction("xor eax, eax"); // materialize false for loose equality + emitter.label("__elephc_eval_mixed_loose_eq_done"); + emitter.instruction("add rsp, 96"); // release the loose-equality helper slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the loose-equality boolean in rax + label_c_global(emitter, "__elephc_eval_value_echo"); emitter.instruction("mov rax, rdi"); // move the C boxed value argument into mixed echo input emitter.instruction("jmp __rt_mixed_write_stdout"); // echo one boxed mixed value and return to Rust diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3264496c8a..aa22babbfb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -93,6 +93,28 @@ fn test_eval_scalar_concat_executes_through_bridge() { assert_eq!(out, "ab"); } +/// Verifies eval comparison operators return boxed booleans through the bridge. +#[test] +fn test_eval_scalar_comparisons_execute_through_bridge() { + let out = compile_and_run( + r#" 3; echo 4 >= 4; echo 5 != 6; echo 7 == 7;'); +"#, + ); + assert_eq!(out, "111111"); +} + +/// Verifies loose scalar equality in eval handles strings and null/empty-string rules. +#[test] +fn test_eval_scalar_loose_equality_executes_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 15:29:29 +0200 Subject: [PATCH 0005/1208] Support foreach in eval fragments --- crates/elephc-eval/src/eval_ir.rs | 5 ++ crates/elephc-eval/src/interpreter.rs | 77 ++++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 39 +++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 7 ++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + src/codegen_support/runtime/eval_bridge.rs | 34 ++++++++++ tests/codegen/eval.rs | 36 ++++++++++ 9 files changed, 201 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index fd52ddc0f9..9e913fa8a6 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -60,6 +60,11 @@ pub enum EvalStmt { update: Vec, body: Vec, }, + Foreach { + array: EvalExpr, + value_name: String, + body: Vec, + }, If { condition: EvalExpr, then_branch: Vec, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0cc8beba07..cd407399f1 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -48,6 +48,9 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result; + /// Returns the visible element count for an array-like runtime cell. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result; + /// Returns whether a runtime cell can be indexed like an array by eval writes. fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result; @@ -185,6 +188,11 @@ fn execute_stmt( update, body, } => execute_for_stmt(init, condition.as_ref(), update, body, scope, values), + EvalStmt::Foreach { + array, + value_name, + body, + } => execute_foreach_stmt(array, value_name, body, scope, values), EvalStmt::If { condition, then_branch, @@ -267,6 +275,33 @@ fn execute_for_stmt( Ok(EvalControl::None) } +/// Executes a value-only PHP `foreach` loop over indexed eval array values. +fn execute_foreach_stmt( + array: &EvalExpr, + value_name: &str, + body: &[EvalStmt], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array = eval_expr(array, scope, values)?; + let len = values.array_len(array)?; + for index in 0..len { + let index = values.int(index as i64)?; + let value = values.array_get(array, index)?; + if let Some(replaced) = + scope.set(value_name.to_string(), value, ScopeCellOwnership::Owned) + { + values.release(replaced)?; + } + match execute_statements(body, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + /// Evaluates one expression to an opaque runtime-cell handle. fn eval_expr( expr: &EvalExpr, @@ -529,6 +564,15 @@ mod tests { Ok(array) } + /// Returns the visible element count for fake array values. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + match self.get(array) { + FakeValue::Array(elements) => Ok(elements.len()), + FakeValue::Assoc(entries) => Ok(entries.len()), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns whether a fake runtime cell is an indexed or associative array. fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { Ok(matches!( @@ -860,6 +904,39 @@ mod tests { assert_eq!(values.output, "1111ns"); } + /// Verifies foreach assigns each indexed element to the value variable. + #[test] + fn execute_program_foreach_iterates_indexed_values() { + let program = + parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "ab"); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); + } + + /// Verifies break and continue control foreach execution inside eval. + #[test] + fn execute_program_foreach_honors_break_and_continue() { + let program = parse_fragment( + br#"foreach ([1, 2, 3] as $item) { if ($item == 1) { continue; } echo $item; break; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2"); + } + /// Verifies indexed array literals and reads execute through runtime hooks. #[test] fn execute_program_reads_indexed_array_literal() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 640112ae60..312bcddcca 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -341,6 +341,7 @@ impl Parser { Ok(vec![EvalStmt::Echo(expr)]) } TokenKind::Ident(name) if name == "for" => self.parse_for_stmt(), + TokenKind::Ident(name) if name == "foreach" => self.parse_foreach_stmt(), TokenKind::Ident(name) if name == "if" => self.parse_if_stmt(), TokenKind::Ident(name) if name == "return" => { self.advance(); @@ -407,6 +408,29 @@ impl Parser { }]) } + /// Parses `foreach (expr as $value) { ... }`. + fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let array = self.parse_expr()?; + if !matches!(self.current(), TokenKind::Ident(name) if name == "as") { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + let TokenKind::DollarIdent(value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let value_name = value_name.clone(); + self.advance(); + self.expect(TokenKind::RParen)?; + let body = self.parse_block()?; + Ok(vec![EvalStmt::Foreach { + array, + value_name, + body, + }]) + } + /// Parses the optional first clause of a `for` loop. fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { if matches!(self.current(), TokenKind::Semicolon) { @@ -876,6 +900,21 @@ mod tests { ); } + /// Verifies value-only foreach loops lower to an array expression, value target, and body. + #[test] + fn parse_fragment_accepts_foreach_source() { + let program = + parse_fragment(br#"foreach ($items as $item) { echo $item; }"#).expect("parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Foreach { + array: EvalExpr::LoadVar("items".to_string()), + value_name: "item".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::LoadVar("item".to_string()))], + }] + ); + } + /// Verifies comparison operators parse with lower precedence than arithmetic. #[test] fn parse_fragment_accepts_comparison_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index ed2da7e130..d6fd888560 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -33,6 +33,7 @@ unsafe extern "C" { index: *mut RuntimeCell, value: *mut RuntimeCell, ) -> *mut RuntimeCell; + fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_null() -> *mut RuntimeCell; fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; @@ -118,6 +119,12 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; + usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) + } + /// Returns whether a boxed Mixed cell has an array-like runtime tag. fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { Ok(unsafe { __elephc_eval_value_is_array_like(value.as_ptr()) != 0 }) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9470effa10..c7a9c60b24 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, scalar equality/comparisons, basic control flow (`if`/`while`/`for`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, scalar equality/comparisons, basic control flow (`if`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b755565fae..e16301ca95 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index fdbfd83a53..565f0ba3f0 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -4,6 +4,7 @@ $result = eval('$x = $x + 2; $created = "dynamic"; return $x + 4;'); eval('$profile["name"] = "Grace";'); eval('if ($x >= 3) { echo "x>=3\n"; }'); +eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); $meta = eval('return ["source" => "eval"];'); echo "x=" . $x . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 111f3f0315..fb28155c06 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -128,6 +128,22 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #48"); // release the array-set wrapper frame emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_value_array_len"); + emitter.instruction("cbz x0, __elephc_eval_value_array_len_zero"); // null handles have no iterable eval elements + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __elephc_eval_value_array_len_load"); // indexed arrays expose their header element count + emitter.instruction("cmp x9, #5"); // tag 5 = associative array + emitter.instruction("b.eq __elephc_eval_value_array_len_load"); // associative arrays expose their header entry count + emitter.label("__elephc_eval_value_array_len_zero"); + emitter.instruction("mov x0, #0"); // scalar values have zero foreach-visible elements in this subset + emitter.instruction("ret"); // return the empty length to Rust + emitter.label("__elephc_eval_value_array_len_load"); + emitter.instruction("ldr x9, [x0, #8]"); // load the array/hash payload pointer from the Mixed cell + emitter.instruction("cbz x9, __elephc_eval_value_array_len_zero"); // null payloads are treated as empty containers + emitter.instruction("ldr x0, [x9]"); // load the runtime container element count + emitter.instruction("ret"); // return the element count to Rust + emitter.label("__elephc_eval_key_normalize"); emitter.instruction("sub sp, sp, #32"); // allocate a helper frame while classifying the boxed eval key emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across runtime calls @@ -550,6 +566,24 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_value_array_len"); + emitter.instruction("test rdi, rdi"); // null handles have no iterable eval elements + emitter.instruction("jz __elephc_eval_value_array_len_zero"); // report empty length for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __elephc_eval_value_array_len_load"); // indexed arrays expose their header element count + emitter.instruction("cmp r10, 5"); // tag 5 = associative array + emitter.instruction("je __elephc_eval_value_array_len_load"); // associative arrays expose their header entry count + emitter.label("__elephc_eval_value_array_len_zero"); + emitter.instruction("xor eax, eax"); // scalar values have zero foreach-visible elements in this subset + emitter.instruction("ret"); // return the empty length to Rust + emitter.label("__elephc_eval_value_array_len_load"); + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the array/hash payload pointer from the Mixed cell + emitter.instruction("test r10, r10"); // is the container payload null? + emitter.instruction("jz __elephc_eval_value_array_len_zero"); // null payloads are treated as empty containers + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the runtime container element count + emitter.instruction("ret"); // return the element count to Rust + emitter.label("__elephc_eval_key_normalize"); emitter.instruction("push rbp"); // preserve the caller frame pointer while classifying the eval key emitter.instruction("mov rbp, rsp"); // establish a stable key-normalizer frame diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index aa22babbfb..87d51f0d52 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -190,6 +190,42 @@ echo ":" . $i; assert_eq!(out, "012:3"); } +/// Verifies value-only foreach loops inside eval iterate indexed array values. +#[test] +fn test_eval_foreach_iterates_indexed_values() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 15:45:15 +0200 Subject: [PATCH 0006/1208] Dispatch simple eval builtins --- crates/elephc-eval/src/interpreter.rs | 59 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 25 +++++++++++- 5 files changed, 85 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index cd407399f1..2bd652a5ac 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -358,9 +358,20 @@ fn eval_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - if name != "eval" { - return Err(EvalStatus::UnsupportedConstruct); + match name { + "count" => eval_builtin_count(args, scope, values), + "eval" => eval_nested_eval(args, scope, values), + "strlen" => eval_builtin_strlen(args, scope, values), + _ => Err(EvalStatus::UnsupportedConstruct), } +} + +/// Evaluates nested `eval(...)` calls against the current materialized scope. +fn eval_nested_eval( + args: &[EvalExpr], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { let [code] = args else { return Err(EvalStatus::RuntimeFatal); }; @@ -370,6 +381,36 @@ fn eval_call( execute_program(&program, scope, values) } +/// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. +fn eval_builtin_strlen( + args: &[EvalExpr], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, scope, values)?; + let bytes = values.string_bytes(value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) +} + +/// Evaluates the builtin `count(...)` for one runtime array-like argument. +fn eval_builtin_count( + args: &[EvalExpr], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, scope, values)?; + let len = values.array_len(value)?; + let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) +} + /// Evaluates an indexed array literal into a boxed runtime Mixed array. fn eval_indexed_array( elements: &[EvalArrayElement], @@ -977,6 +1018,20 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. + #[test] + fn execute_program_dispatches_simple_builtins() { + let program = + parse_fragment(br#"return strlen("abc") + count([1, 2, 3]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(6)); + } + /// Verifies indexed array writes mutate an existing scope array. #[test] fn execute_program_writes_indexed_scope_array() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c7a9c60b24..0628a21bb2 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, scalar equality/comparisons, basic control flow (`if`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic function/class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, scalar equality/comparisons, basic control flow (`if`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic user functions, function/class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index e16301ca95..f86b77f98d 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 565f0ba3f0..a7af22d9b5 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -6,9 +6,11 @@ eval('if ($x >= 3) { echo "x>=3\n"; }'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); $meta = eval('return ["source" => "eval"];'); +$meta_count = eval('return count($meta);'); echo "x=" . $x . "\n"; echo "created=" . $created . "\n"; echo "name=" . $profile["name"] . "\n"; echo "source=" . $meta["source"] . "\n"; +echo "meta-count=" . $meta_count . "\n"; echo "result=" . $result . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 87d51f0d52..b9815b50ee 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1,7 +1,7 @@ //! Purpose: //! Integration tests for the initial `eval()` bridge wiring. //! Covers language-construct visibility, conditional bridge linking, and the -//! base runtime interpreter path for scalar, branch, and indexed-array eval fragments. +//! base runtime interpreter path for scalar, branch, indexed-array, and simple builtin eval fragments. //! //! Called from: //! - `cargo test --test codegen_tests eval` through Rust's test harness. @@ -321,6 +321,29 @@ fn test_eval_nested_eval_return_value_is_expression_result() { assert_eq!(out, "9"); } +/// Verifies eval can dispatch simple builtin calls through its dynamic call path. +#[test] +fn test_eval_dispatches_simple_builtin_calls() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 15:47:55 +0200 Subject: [PATCH 0007/1208] Support elseif in eval fragments --- crates/elephc-eval/src/interpreter.rs | 20 +++++++ crates/elephc-eval/src/parser.rs | 78 +++++++++++++++++++++++---- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + tests/codegen/eval.rs | 24 +++++++++ 6 files changed, 116 insertions(+), 11 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2bd652a5ac..3cc9bf98c7 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -879,6 +879,26 @@ mod tests { assert_eq!(values.get(x), FakeValue::String("else".to_string())); } + /// Verifies elseif chains execute the first truthy branch and skip later branches. + #[test] + fn execute_program_elseif_uses_first_truthy_branch() { + let program = parse_fragment( + br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; } else { $x = "c"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let a = values.bool_value(false).expect("create fake bool"); + let b = values.bool_value(true).expect("create fake bool"); + scope.set("a", a, ScopeCellOwnership::Owned); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::String("b".to_string())); + } + /// Verifies while repeats while the condition remains truthy and propagates writes. #[test] fn execute_program_while_uses_php_truthiness() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 312bcddcca..cfbbabb8bc 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -480,24 +480,42 @@ impl Parser { Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) } - /// Parses `if (expr) { ... } [else { ... }]`. + /// Parses a complete `if` statement after consuming the `if` keyword. fn parse_if_stmt(&mut self) -> Result, EvalParseError> { self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } + + /// Parses the condition, then block, and optional else branch for an `if` chain. + fn parse_if_after_keyword(&mut self) -> Result { self.expect(TokenKind::LParen)?; let condition = self.parse_expr()?; self.expect(TokenKind::RParen)?; let then_branch = self.parse_block()?; - let else_branch = if matches!(self.current(), TokenKind::Ident(name) if name == "else") { - self.advance(); - self.parse_block()? - } else { - Vec::new() - }; - Ok(vec![EvalStmt::If { + let else_branch = self.parse_optional_else_branch()?; + Ok(EvalStmt::If { condition, then_branch, else_branch, - }]) + }) + } + + /// Parses `elseif`, `else if`, or `else` branches after an `if` body. + fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Ident(name) if name == "elseif") { + self.advance(); + return Ok(vec![self.parse_if_after_keyword()?]); + } + if !matches!(self.current(), TokenKind::Ident(name) if name == "else") { + return Ok(Vec::new()); + } + self.advance(); + if matches!(self.current(), TokenKind::Ident(name) if name == "if") { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } else { + self.parse_block() + } } /// Parses `unset($name[, ...]);`. @@ -874,6 +892,48 @@ mod tests { ); } + /// Verifies elseif fragments lower to nested if statements in the else branch. + #[test] + fn parse_fragment_accepts_elseif_source() { + let program = + parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("a".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("a".to_string())), + }], + else_branch: vec![EvalStmt::If { + condition: EvalExpr::LoadVar("b".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("b".to_string())), + }], + else_branch: Vec::new(), + }], + }] + ); + } + + /// Verifies PHP's `else if` spelling follows the same nested branch shape. + #[test] + fn parse_fragment_accepts_else_if_source() { + let program = + parse_fragment(br#"if ($a) { $x = "a"; } else if ($b) { $x = "b"; }"#) + .expect("fragment should parse"); + + assert!(matches!( + program.statements(), + [EvalStmt::If { + else_branch, + .. + }] if matches!(else_branch.as_slice(), [EvalStmt::If { .. }]) + )); + } + /// Verifies for loops lower clauses and body statements separately. #[test] fn parse_fragment_accepts_for_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 0628a21bb2..d9fe74a1e1 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, scalar equality/comparisons, basic control flow (`if`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic user functions, function/class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic user functions, function/class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index f86b77f98d..c6491d49ba 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index a7af22d9b5..b9b8751e31 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -4,6 +4,7 @@ $result = eval('$x = $x + 2; $created = "dynamic"; return $x + 4;'); eval('$profile["name"] = "Grace";'); eval('if ($x >= 3) { echo "x>=3\n"; }'); +eval('if ($x < 0) { echo "negative\n"; } elseif ($x == 3) { echo "x==3\n"; }'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); $meta = eval('return ["source" => "eval"];'); $meta_count = eval('return count($meta);'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b9815b50ee..67d8218a48 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -128,6 +128,30 @@ echo $result; assert_eq!(out, "else"); } +/// Verifies eval elseif chains execute the first truthy branch. +#[test] +fn test_eval_elseif_updates_scope() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 15:58:49 +0200 Subject: [PATCH 0008/1208] Persist eval context handles --- docs/internals/the-runtime.md | 2 +- src/codegen/frame.rs | 66 ++++++++++++++++++++++++- src/codegen/lower_inst/builtins/eval.rs | 37 +++++++++++++- src/ir/function.rs | 1 + src/ir_lower/context.rs | 7 +++ tests/codegen/eval.rs | 8 +++ 6 files changed, 118 insertions(+), 3 deletions(-) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d9fe74a1e1..ec04e5c580 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,7 +15,7 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The context is the future home for dynamic function/class tables, while the scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic user functions, function/class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index 68138bd5a6..30e8f39bfc 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -166,6 +166,7 @@ pub(super) fn emit_main_prologue(ctx: &mut FunctionContext<'_>) { store_argv_local_if_present(ctx); zero_initialize_main_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_context_locals(ctx); zero_initialize_eval_scope_locals(ctx); } @@ -206,6 +207,7 @@ pub(super) fn emit_function_prologue_with_label( } zero_initialize_function_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_context_locals(ctx); zero_initialize_eval_scope_locals(ctx); Ok(()) } @@ -341,6 +343,7 @@ pub(super) fn emit_web_handler_prologue(ctx: &mut FunctionContext<'_>) { emit_callee_saved_saves(ctx); zero_initialize_main_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); + zero_initialize_eval_context_locals(ctx); zero_initialize_eval_scope_locals(ctx); } @@ -423,6 +426,7 @@ fn emit_main_local_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { } } emit_eval_scope_epilogue_cleanup(ctx); + emit_eval_context_epilogue_cleanup(ctx); } /// Returns main local slots that receive owned refcounted values through `StoreLocal`. @@ -538,6 +542,13 @@ fn zero_initialize_eval_scope_locals(ctx: &mut FunctionContext<'_>) { } } +/// Zero-initializes persistent eval context handles before the first eval call can allocate one. +fn zero_initialize_eval_context_locals(ctx: &mut FunctionContext<'_>) { + for (_, offset) in eval_context_locals(ctx) { + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + } +} + /// Releases persistent eval scopes allocated for this frame. fn emit_eval_scope_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { for (name, offset) in eval_scope_locals(ctx) { @@ -546,6 +557,14 @@ fn emit_eval_scope_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { } } +/// Releases persistent eval contexts allocated for this frame. +fn emit_eval_context_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { + for (name, offset) in eval_context_locals(ctx) { + ctx.emitter.comment(&format!("epilogue cleanup {}", name)); + emit_eval_context_cleanup(ctx, offset); + } +} + /// Frees one persistent eval scope handle when it was allocated. fn emit_eval_scope_cleanup(ctx: &mut FunctionContext<'_>, offset: usize) { let result_reg = abi::int_result_reg(ctx.emitter); @@ -563,6 +582,22 @@ fn emit_eval_scope_cleanup(ctx: &mut FunctionContext<'_>, offset: usize) { ctx.emitter.label(&done); } +/// Frees one persistent eval context handle when it was allocated. +fn emit_eval_context_cleanup(ctx: &mut FunctionContext<'_>, offset: usize) { + let result_reg = abi::int_result_reg(ctx.emitter); + let done = ctx.next_label("eval_context_cleanup_done"); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done); + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if arg_reg != result_reg { + ctx.emitter.instruction(&format!("mov {}, {}", arg_reg, result_reg)); // pass the persistent eval context handle to the free helper + } + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_context_free"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_store_zero_to_local_slot(ctx.emitter, offset); + ctx.emitter.label(&done); +} + /// Returns hidden eval scope slots and their frame offsets. fn eval_scope_locals(ctx: &FunctionContext<'_>) -> Vec<(String, usize)> { let mut locals = ctx @@ -583,6 +618,26 @@ fn eval_scope_locals(ctx: &FunctionContext<'_>) -> Vec<(String, usize)> { locals } +/// Returns hidden eval context slots and their frame offsets. +fn eval_context_locals(ctx: &FunctionContext<'_>) -> Vec<(String, usize)> { + let mut locals = ctx + .function + .locals + .iter() + .filter(|local| local.kind == LocalKind::EvalContext) + .filter_map(|local| { + let offset = ctx.local_offset(local.id).ok()?; + let name = local + .name + .clone() + .unwrap_or_else(|| format!("slot{}", local.id.as_raw())); + Some((name, offset)) + }) + .collect::>(); + locals.sort_by_key(|(_, offset)| *offset); + locals +} + /// Returns true when the function owns a persistent eval scope local. fn function_has_eval_scope(function: &Function) -> bool { function @@ -712,7 +767,12 @@ fn emit_function_local_epilogue_cleanup( let cleanup_locals = function_cleanup_locals(ctx, skip_return_slot); let ref_cell_owners = ref_cell_owner_locals(ctx); let eval_scopes = eval_scope_locals(ctx); - if cleanup_locals.is_empty() && ref_cell_owners.is_empty() && eval_scopes.is_empty() { + let eval_contexts = eval_context_locals(ctx); + if cleanup_locals.is_empty() + && ref_cell_owners.is_empty() + && eval_scopes.is_empty() + && eval_contexts.is_empty() + { return; } let return_ty = ctx.function.return_php_type.codegen_repr(); @@ -734,6 +794,10 @@ fn emit_function_local_epilogue_cleanup( ctx.emitter.comment(&format!("epilogue cleanup {}", name)); emit_eval_scope_cleanup(ctx, offset); } + for (name, offset) in eval_contexts { + ctx.emitter.comment(&format!("epilogue cleanup {}", name)); + emit_eval_context_cleanup(ctx, offset); + } if preserves_return { pop_return_value(ctx, &return_ty); } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 4430687378..9dc2e84fb2 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -29,6 +29,7 @@ const EVAL_UNSUPPORTED_MESSAGE: &str = const EVAL_RUNTIME_FATAL_MESSAGE: &str = "Fatal error: eval() runtime failed\n"; const EVAL_STACK_BYTES: usize = 64; const EVAL_RESULT_VALUE_CELL_OFFSET: usize = 8; +const EVAL_CONTEXT_HANDLE_OFFSET: usize = 24; const EVAL_SCOPE_HANDLE_OFFSET: usize = 32; const EVAL_TEMP_CELL_OFFSET: usize = 40; const EVAL_CODE_PTR_OFFSET: usize = 48; @@ -58,10 +59,11 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); save_eval_code_string(ctx); + ensure_eval_context(ctx)?; ensure_eval_scope(ctx)?; let sync_locals = eval_sync_locals(ctx); flush_eval_scope_locals(ctx, &sync_locals)?; - abi::emit_load_int_immediate(ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 0), 0); + load_eval_context_to_arg(ctx, 0); load_eval_scope_to_arg(ctx, 1); move_saved_eval_code_to_eval_args(ctx); let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); @@ -85,6 +87,39 @@ fn save_eval_code_string(ctx: &mut FunctionContext<'_>) { abi::emit_store_to_sp(ctx.emitter, len_reg, EVAL_CODE_LEN_OFFSET); } +/// Ensures a persistent eval context exists and stores its handle in the scratch frame. +fn ensure_eval_context(ctx: &mut FunctionContext<'_>) -> Result<()> { + let slot = eval_context_slot(ctx)?; + let offset = ctx.local_offset(slot)?; + let ready = ctx.next_label("eval_context_ready"); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &ready); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_context_new"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::store_at_offset(ctx.emitter, result_reg, offset); + ctx.emitter.label(&ready); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_CONTEXT_HANDLE_OFFSET); + Ok(()) +} + +/// Returns the hidden frame slot that owns this function's persistent eval context. +fn eval_context_slot(ctx: &FunctionContext<'_>) -> Result { + ctx.function + .locals + .iter() + .find(|local| local.kind == LocalKind::EvalContext) + .map(|local| local.id) + .ok_or_else(|| CodegenIrError::invalid_module("eval call missing eval context local")) +} + +/// Loads the current eval context handle into the selected integer argument register. +fn load_eval_context_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + abi::emit_load_temporary_stack_slot(ctx.emitter, arg_reg, EVAL_CONTEXT_HANDLE_OFFSET); +} + /// Reloads the saved eval source string into the bridge code pointer/length arguments. fn move_saved_eval_code_to_eval_args(ctx: &mut FunctionContext<'_>) { let code_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); diff --git a/src/ir/function.rs b/src/ir/function.rs index d5570be37e..2c3cee5e13 100644 --- a/src/ir/function.rs +++ b/src/ir/function.rs @@ -179,6 +179,7 @@ pub enum LocalKind { NamedArgTemp, IteratorState, GeneratorState, + EvalContext, EvalScope, } diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index c708d10c2f..6248650e04 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -88,6 +88,7 @@ pub(crate) struct ClosureCapture { pub value: ValueId, } +const EVAL_CONTEXT_LOCAL_NAME: &str = "__eir_eval_context"; const EVAL_SCOPE_LOCAL_NAME: &str = "__eir_eval_scope"; /// Mutable state for one function body while it is lowered. @@ -455,6 +456,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.declare_local_with_kind(name, php_type, LocalKind::OwnedTemp) } + /// Ensures this function has a persistent eval context handle slot. + pub(crate) fn declare_eval_context_local(&mut self) -> LocalSlotId { + self.declare_local_with_kind(EVAL_CONTEXT_LOCAL_NAME, PhpType::Int, LocalKind::EvalContext) + } + /// Ensures this function has a persistent eval scope handle slot. pub(crate) fn declare_eval_scope_local(&mut self) -> LocalSlotId { self.declare_local_with_kind(EVAL_SCOPE_LOCAL_NAME, PhpType::Int, LocalKind::EvalScope) @@ -462,6 +468,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Applies the static part of the eval barrier to visible PHP local storage. pub(crate) fn apply_eval_barrier(&mut self) { + self.declare_eval_context_local(); self.declare_eval_scope_local(); let local_names = self .local_slots diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 67d8218a48..0b7601950b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -34,6 +34,14 @@ fn test_eval_codegen_requires_eval_bridge() { user_asm.contains("__elephc_eval_execute"), "user assembly should call the eval bridge:\n{user_asm}" ); + assert!( + user_asm.contains("__elephc_eval_context_new"), + "user assembly should create a persistent eval context:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_context_free"), + "user assembly should free the persistent eval context:\n{user_asm}" + ); assert!( required_libraries.iter().any(|lib| lib == "elephc_eval"), "required libraries should include elephc_eval: {required_libraries:?}" From 6a401a72007bf1de2e9a120932feb48261fb87ad Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 16:09:29 +0200 Subject: [PATCH 0009/1208] Support eval-declared functions --- crates/elephc-eval/src/context.rs | 33 ++++- crates/elephc-eval/src/eval_ir.rs | 29 +++++ crates/elephc-eval/src/interpreter.rs | 177 ++++++++++++++++++++------ crates/elephc-eval/src/lib.rs | 13 +- crates/elephc-eval/src/parser.rs | 57 +++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 3 + tests/codegen/eval.rs | 23 ++++ 9 files changed, 291 insertions(+), 48 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index d4ac46ae08..e254acc08d 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -11,7 +11,10 @@ //! - The handle is intentionally opaque to generated code. //! - No Rust-owned layout is promised across the C ABI. +use std::collections::HashMap; + use crate::abi::ABI_VERSION; +use crate::eval_ir::EvalFunction; /// Process-level eval context passed opaquely across the C ABI. /// @@ -20,26 +23,50 @@ use crate::abi::ABI_VERSION; /// grow dynamic registries without exposing them to generated assembly. pub struct ElephcEvalContext { abi_version: u32, + functions: HashMap, } impl ElephcEvalContext { /// Creates a context using the current eval bridge ABI version. - pub const fn new() -> Self { + pub fn new() -> Self { Self { abi_version: ABI_VERSION, + functions: HashMap::new(), } } /// Creates a context with an explicit ABI version for compatibility tests. #[cfg(test)] - pub const fn for_abi_version(abi_version: u32) -> Self { - Self { abi_version } + pub fn for_abi_version(abi_version: u32) -> Self { + Self { + abi_version, + functions: HashMap::new(), + } } /// Returns the ABI version this context was created for. pub const fn abi_version(&self) -> u32 { self.abi_version } + + /// Defines a dynamic user function, failing if the name already exists. + pub fn define_function( + &mut self, + name: impl Into, + function: EvalFunction, + ) -> Result<(), EvalFunction> { + let name = name.into(); + if self.functions.contains_key(&name) { + return Err(function); + } + self.functions.insert(name, function); + Ok(()) + } + + /// Returns a dynamic user function by its lowercase PHP function name. + pub fn function(&self, name: &str) -> Option<&EvalFunction> { + self.functions.get(name) + } } impl Default for ElephcEvalContext { diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 9e913fa8a6..c6b9d25af4 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -65,6 +65,11 @@ pub enum EvalStmt { value_name: String, body: Vec, }, + FunctionDecl { + name: String, + params: Vec, + body: Vec, + }, If { condition: EvalExpr, then_branch: Vec, @@ -85,6 +90,30 @@ pub enum EvalStmt { Expr(EvalExpr), } +/// Runtime user function declared by an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalFunction { + params: Vec, + body: Vec, +} + +impl EvalFunction { + /// Creates a dynamic eval function with source-order parameters and body. + pub fn new(params: Vec, body: Vec) -> Self { + Self { params, body } + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } + + /// Returns the dynamic EvalIR statements that form the function body. + pub fn body(&self) -> &[EvalStmt] { + &self.body + } +} + /// Dynamic eval expressions evaluated by the interpreter against runtime cells. #[derive(Debug, Clone, PartialEq)] pub enum EvalExpr { diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 3cc9bf98c7..ba190a57b6 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -12,7 +12,10 @@ //! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. use crate::errors::EvalStatus; -use crate::eval_ir::{EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalProgram, EvalStmt}; +use crate::context::ElephcEvalContext; +use crate::eval_ir::{ + EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalFunction, EvalProgram, EvalStmt, +}; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership}; use crate::value::RuntimeCellHandle; @@ -124,7 +127,18 @@ pub fn execute_program( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - match execute_statements(program.statements(), scope, values)? { + let mut context = ElephcEvalContext::new(); + execute_program_with_context(&mut context, program, scope, values) +} + +/// Executes an EvalIR program with a persistent eval context for dynamic declarations. +pub fn execute_program_with_context( + context: &mut ElephcEvalContext, + program: &EvalProgram, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_statements(program.statements(), context, scope, values)? { EvalControl::None => values.null(), EvalControl::Return(result) => Ok(result), EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), @@ -134,11 +148,12 @@ pub fn execute_program( /// Executes statements in source order and propagates the first eval `return`. fn execute_statements( statements: &[EvalStmt], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { for stmt in statements { - match execute_stmt(stmt, scope, values)? { + match execute_stmt(stmt, context, scope, values)? { EvalControl::None => {} control => return Ok(control), } @@ -149,6 +164,7 @@ fn execute_statements( /// Executes one statement and returns `Some` only for eval `return`. fn execute_stmt( stmt: &EvalStmt, + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { @@ -167,8 +183,8 @@ fn execute_stmt( } else { values.array_new(1)? }; - let index = eval_expr(index, scope, values)?; - let value = eval_expr(value, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; let array = values.array_set(array, index, value)?; if let Some(replaced) = scope.set(name.clone(), array, ownership) { values.release(replaced)?; @@ -178,7 +194,7 @@ fn execute_stmt( EvalStmt::Break => Ok(EvalControl::Break), EvalStmt::Continue => Ok(EvalControl::Continue), EvalStmt::Echo(expr) => { - let value = eval_expr(expr, scope, values)?; + let value = eval_expr(expr, context, scope, values)?; values.echo(value)?; Ok(EvalControl::None) } @@ -187,28 +203,36 @@ fn execute_stmt( condition, update, body, - } => execute_for_stmt(init, condition.as_ref(), update, body, scope, values), + } => execute_for_stmt(init, condition.as_ref(), update, body, context, scope, values), EvalStmt::Foreach { array, value_name, body, - } => execute_foreach_stmt(array, value_name, body, scope, values), + } => execute_foreach_stmt(array, value_name, body, context, scope, values), + EvalStmt::FunctionDecl { name, params, body } => { + context + .define_function(name.clone(), EvalFunction::new(params.clone(), body.clone())) + .map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(EvalControl::None) + } EvalStmt::If { condition, then_branch, else_branch, } => { - let condition = eval_expr(condition, scope, values)?; + let condition = eval_expr(condition, context, scope, values)?; if values.truthy(condition)? { - execute_statements(then_branch, scope, values) + execute_statements(then_branch, context, scope, values) } else { - execute_statements(else_branch, scope, values) + execute_statements(else_branch, context, scope, values) } } - EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr(expr, scope, values)?)), + EvalStmt::Return(Some(expr)) => { + Ok(EvalControl::Return(eval_expr(expr, context, scope, values)?)) + } EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), EvalStmt::StoreVar { name, value } => { - let value = eval_expr(value, scope, values)?; + let value = eval_expr(value, context, scope, values)?; if let Some(replaced) = scope.set(name.clone(), value, ScopeCellOwnership::Owned) { values.release(replaced)?; } @@ -222,10 +246,10 @@ fn execute_stmt( } EvalStmt::While { condition, body } => { while { - let condition = eval_expr(condition, scope, values)?; + let condition = eval_expr(condition, context, scope, values)?; values.truthy(condition)? } { - match execute_statements(body, scope, values)? { + match execute_statements(body, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Return(result) => return Ok(EvalControl::Return(result)), @@ -234,7 +258,7 @@ fn execute_stmt( Ok(EvalControl::None) } EvalStmt::Expr(expr) => { - let _ = eval_expr(expr, scope, values)?; + let _ = eval_expr(expr, context, scope, values)?; Ok(EvalControl::None) } } @@ -246,27 +270,28 @@ fn execute_for_stmt( condition: Option<&EvalExpr>, update: &[EvalStmt], body: &[EvalStmt], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - match execute_statements(init, scope, values)? { + match execute_statements(init, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => return Ok(EvalControl::None), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } loop { if let Some(condition) = condition { - let condition = eval_expr(condition, scope, values)?; + let condition = eval_expr(condition, context, scope, values)?; if !values.truthy(condition)? { break; } } - match execute_statements(body, scope, values)? { + match execute_statements(body, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } - match execute_statements(update, scope, values)? { + match execute_statements(update, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Return(result) => return Ok(EvalControl::Return(result)), @@ -280,10 +305,11 @@ fn execute_foreach_stmt( array: &EvalExpr, value_name: &str, body: &[EvalStmt], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let array = eval_expr(array, scope, values)?; + let array = eval_expr(array, context, scope, values)?; let len = values.array_len(array)?; for index in 0..len { let index = values.int(index as i64)?; @@ -293,7 +319,7 @@ fn execute_foreach_stmt( { values.release(replaced)?; } - match execute_statements(body, scope, values)? { + match execute_statements(body, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Return(result) => return Ok(EvalControl::Return(result)), @@ -305,6 +331,7 @@ fn execute_foreach_stmt( /// Evaluates one expression to an opaque runtime-cell handle. fn eval_expr( expr: &EvalExpr, + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { @@ -314,27 +341,27 @@ fn eval_expr( .iter() .any(|element| matches!(element, EvalArrayElement::KeyValue { .. })) { - eval_assoc_array(elements, scope, values) + eval_assoc_array(elements, context, scope, values) } else { - eval_indexed_array(elements, scope, values) + eval_indexed_array(elements, context, scope, values) } } EvalExpr::ArrayGet { array, index } => { - let array = eval_expr(array, scope, values)?; - let index = eval_expr(index, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; values.array_get(array, index) } - EvalExpr::Call { name, args } => eval_call(name, args, scope, values), + EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::LoadVar(name) => scope.visible_cell(name).map_or_else(|| values.null(), Ok), EvalExpr::Print(inner) => { - let value = eval_expr(inner, scope, values)?; + let value = eval_expr(inner, context, scope, values)?; values.echo(value)?; values.int(1) } EvalExpr::Binary { op, left, right } => { - let left = eval_expr(left, scope, values)?; - let right = eval_expr(right, scope, values)?; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; match op { EvalBinOp::Add => values.add(left, right), EvalBinOp::Sub => values.sub(left, right), @@ -355,42 +382,50 @@ fn eval_expr( fn eval_call( name: &str, args: &[EvalExpr], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { match name { - "count" => eval_builtin_count(args, scope, values), - "eval" => eval_nested_eval(args, scope, values), - "strlen" => eval_builtin_strlen(args, scope, values), - _ => Err(EvalStatus::UnsupportedConstruct), + "count" => eval_builtin_count(args, context, scope, values), + "eval" => eval_nested_eval(args, context, scope, values), + "strlen" => eval_builtin_strlen(args, context, scope, values), + _ => context + .function(name) + .cloned() + .map_or(Err(EvalStatus::UnsupportedConstruct), |function| { + eval_dynamic_function(&function, args, context, scope, values) + }), } } /// Evaluates nested `eval(...)` calls against the current materialized scope. fn eval_nested_eval( args: &[EvalExpr], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { let [code] = args else { return Err(EvalStatus::RuntimeFatal); }; - let code = eval_expr(code, scope, values)?; + let code = eval_expr(code, context, scope, values)?; let code = values.string_bytes(code)?; let program = parse_fragment(&code).map_err(|_| EvalStatus::ParseError)?; - execute_program(&program, scope, values) + execute_program_with_context(context, &program, scope, values) } /// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. fn eval_builtin_strlen( args: &[EvalExpr], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { let [value] = args else { return Err(EvalStatus::RuntimeFatal); }; - let value = eval_expr(value, scope, values)?; + let value = eval_expr(value, context, scope, values)?; let bytes = values.string_bytes(value)?; let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len) @@ -399,21 +434,49 @@ fn eval_builtin_strlen( /// Evaluates the builtin `count(...)` for one runtime array-like argument. fn eval_builtin_count( args: &[EvalExpr], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { let [value] = args else { return Err(EvalStatus::RuntimeFatal); }; - let value = eval_expr(value, scope, values)?; + let value = eval_expr(value, context, scope, values)?; let len = values.array_len(value)?; let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len) } +/// Evaluates an eval-declared user function with positional argument binding. +fn eval_dynamic_function( + function: &EvalFunction, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() != function.params().len() { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, caller_scope, values)?); + } + let mut function_scope = ElephcEvalScope::new(); + for (name, value) in function.params().iter().zip(evaluated_args) { + function_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); + } + match execute_statements(function.body(), context, &mut function_scope, values)? { + EvalControl::None => values.null(), + EvalControl::Return(result) => Ok(result), + EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Evaluates an indexed array literal into a boxed runtime Mixed array. fn eval_indexed_array( elements: &[EvalArrayElement], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { @@ -423,7 +486,7 @@ fn eval_indexed_array( return Err(EvalStatus::UnsupportedConstruct); }; let index = values.int(index as i64)?; - let value = eval_expr(element, scope, values)?; + let value = eval_expr(element, context, scope, values)?; let _ = values.array_set(array, index, value)?; } Ok(array) @@ -432,6 +495,7 @@ fn eval_indexed_array( /// Evaluates an associative array literal into a boxed runtime Mixed hash. fn eval_assoc_array( elements: &[EvalArrayElement], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { @@ -445,11 +509,11 @@ fn eval_assoc_array( (key, value) } EvalArrayElement::KeyValue { key, value } => { - let key = eval_expr(key, scope, values)?; + let key = eval_expr(key, context, scope, values)?; (key, value) } }; - let value = eval_expr(value, scope, values)?; + let value = eval_expr(value, context, scope, values)?; let _ = values.array_set(array, key, value)?; } Ok(array) @@ -1038,6 +1102,37 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies eval-declared functions can be called by the same fragment. + #[test] + fn execute_program_calls_declared_function() { + let program = parse_fragment(br#"function dyn($x) { return $x + 1; } return dyn(4);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); + } + + /// Verifies eval-declared functions persist in a shared eval context. + #[test] + fn execute_program_context_keeps_declared_function() { + let define = + parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("parse eval fragment"); + let call = parse_fragment(br#"return dyn(4);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute eval ir"); + let result = execute_program_with_context(&mut context, &call, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); + } + /// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. #[test] fn execute_program_dispatches_simple_builtins() { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 074ceb4afc..ab992aed10 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -236,7 +236,7 @@ unsafe fn execute_eval_inner( if !out.is_null() { (*out).clear(); } - execute_parsed_eval(scope, &program, out) + execute_parsed_eval(ctx, scope, &program, out) } /// Executes a parsed eval program in production builds using elephc runtime hooks. @@ -245,10 +245,18 @@ unsafe fn execute_eval_inner( /// `scope` and `out` must be null or valid pointers supplied by generated code. #[cfg(not(test))] unsafe fn execute_parsed_eval( + ctx: *mut ElephcEvalContext, scope: *mut ElephcEvalScope, program: &eval_ir::EvalProgram, out: *mut ElephcEvalResult, ) -> i32 { + let mut fallback_context; + let context = if let Some(ctx) = ctx.as_mut() { + ctx + } else { + fallback_context = ElephcEvalContext::new(); + &mut fallback_context + }; let mut fallback_scope; let scope = if let Some(scope) = scope.as_mut() { scope @@ -257,7 +265,7 @@ unsafe fn execute_parsed_eval( &mut fallback_scope }; let mut values = ElephcRuntimeOps::new(); - match interpreter::execute_program(program, scope, &mut values) { + match interpreter::execute_program_with_context(context, program, scope, &mut values) { Ok(result) => { if !out.is_null() { (*out).kind = 0; @@ -276,6 +284,7 @@ unsafe fn execute_parsed_eval( /// `out` must be null or valid result storage supplied by the test caller. #[cfg(test)] unsafe fn execute_parsed_eval( + _ctx: *mut ElephcEvalContext, _scope: *mut ElephcEvalScope, _program: &eval_ir::EvalProgram, _out: *mut ElephcEvalResult, diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index cfbbabb8bc..96000a06aa 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -342,6 +342,7 @@ impl Parser { } TokenKind::Ident(name) if name == "for" => self.parse_for_stmt(), TokenKind::Ident(name) if name == "foreach" => self.parse_foreach_stmt(), + TokenKind::Ident(name) if name == "function" => self.parse_function_decl_stmt(), TokenKind::Ident(name) if name == "if" => self.parse_if_stmt(), TokenKind::Ident(name) if name == "return" => { self.advance(); @@ -431,6 +432,43 @@ impl Parser { }]) } + /// Parses `function name($param, ...) { ... }` declarations. + fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let params = self.parse_function_params()?; + let body = self.parse_block()?; + Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) + } + + /// Parses a dynamic function declaration parameter list after `(`. + fn parse_function_params(&mut self) -> Result, EvalParseError> { + let mut params = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(params); + } + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + params.push(name.clone()); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok(params) + } + /// Parses the optional first clause of a `for` loop. fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { if matches!(self.current(), TokenKind::Semicolon) { @@ -975,6 +1013,25 @@ mod tests { ); } + /// Verifies dynamic function declarations preserve name, parameters, and body. + #[test] + fn parse_fragment_accepts_function_declaration_source() { + let program = parse_fragment(br#"function dyn($x) { return $x + 1; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::FunctionDecl { + name: "dyn".to_string(), + params: vec!["x".to_string()], + body: vec![EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }))], + }] + ); + } + /// Verifies comparison operators parse with lower precedence than arithmetic. #[test] fn parse_fragment_accepts_comparison_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index ec04e5c580..860c267324 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The context is the future home for dynamic function/class tables, while the scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Dynamic user functions, function/class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch to eval-declared functions, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c6491d49ba..3997ee6054 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b9b8751e31..b04bc26153 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -8,10 +8,13 @@ eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); $meta = eval('return ["source" => "eval"];'); $meta_count = eval('return count($meta);'); +eval('function plus_one($value) { return $value + 1; }'); +$dynamic_call = eval('return plus_one(4);'); echo "x=" . $x . "\n"; echo "created=" . $created . "\n"; echo "name=" . $profile["name"] . "\n"; echo "source=" . $meta["source"] . "\n"; echo "meta-count=" . $meta_count . "\n"; +echo "dynamic-call=" . $dynamic_call . "\n"; echo "result=" . $result . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0b7601950b..1f119f85e7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -376,6 +376,29 @@ eval('echo count($items);'); assert_eq!(out, "2"); } +/// Verifies eval-declared functions can be called inside the same fragment. +#[test] +fn test_eval_declared_function_can_be_called_in_fragment() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 16:10:35 +0200 Subject: [PATCH 0010/1208] Cover duplicate eval function declarations --- crates/elephc-eval/src/interpreter.rs | 16 ++++++++++++++++ docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ba190a57b6..dffc850b1c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1133,6 +1133,22 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies duplicate eval-declared function names fail in a shared context. + #[test] + fn execute_program_rejects_duplicate_declared_function() { + let define = parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute first declaration"); + let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect_err("duplicate function declaration should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } + /// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. #[test] fn execute_program_dispatches_simple_builtins() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 3997ee6054..8f3045457b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1f119f85e7..d7eb54f4ab 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -399,6 +399,21 @@ eval('echo dyn_eval_inc(4);'); assert_eq!(out, "5"); } +/// Verifies duplicate eval-declared functions fail through the runtime bridge. +#[test] +fn test_eval_duplicate_declared_function_fails() { + let err = compile_and_run_expect_failure( + r#" Date: Mon, 15 Jun 2026 16:28:27 +0200 Subject: [PATCH 0011/1208] Call eval-declared functions from native code --- crates/elephc-eval/src/interpreter.rs | 15 +++++ crates/elephc-eval/src/lib.rs | 61 +++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen/context.rs | 1 + src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/builtins.rs | 8 +++ src/codegen/lower_inst/builtins/eval.rs | 34 ++++++++++- src/ir/instr.rs | 7 ++- src/ir_lower/context.rs | 9 +++ src/ir_lower/expr/mod.rs | 15 +++++ src/ir_lower/program.rs | 3 + src/types/checker/functions/resolution/mod.rs | 7 +++ tests/codegen/eval.rs | 12 ++++ 15 files changed, 175 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index dffc850b1c..3174d07bdf 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -145,6 +145,21 @@ pub fn execute_program_with_context( } } +/// Executes a zero-argument function declared in the shared eval context. +pub fn execute_context_function_zero_args( + context: &mut ElephcEvalContext, + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + context + .function(name) + .cloned() + .map_or(Err(EvalStatus::UnsupportedConstruct), |function| { + let mut caller_scope = ElephcEvalScope::new(); + eval_dynamic_function(&function, &[], context, &mut caller_scope, values) + }) +} + /// Executes statements in source order and propagates the first eval `return`. fn execute_statements( statements: &[EvalStmt], diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index ab992aed10..ecd5eeb2e3 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -204,6 +204,67 @@ pub unsafe extern "C" fn __elephc_eval_execute( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Calls a zero-argument function previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function_zero_args( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_zero_args_inner(ctx, name_ptr, name_len, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the dynamic function-call ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_call_function_zero_args`; callers must provide a valid +/// context and readable function-name bytes. +#[cfg(not(test))] +unsafe fn call_eval_function_zero_args_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + if !out.is_null() { + (*out).clear(); + } + let mut values = ElephcRuntimeOps::new(); + match interpreter::execute_context_function_zero_args( + context, + &name.to_ascii_lowercase(), + &mut values, + ) { + Ok(result) => { + if !out.is_null() { + (*out).kind = 0; + (*out).value_cell = result.as_ptr(); + (*out).error = std::ptr::null_mut(); + } + EvalStatus::Ok.code() + } + Err(status) => status.code(), + } +} + /// Runs the eval ABI body after the exported wrapper has installed a panic boundary. /// /// # Safety diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 860c267324..b11b5d96ff 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The context is the future home for dynamic function/class tables, while the scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried by native zero-argument call sites after an eval barrier. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch to eval-declared functions, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, zero-argument eval-declared functions callable from native code after the barrier, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch with arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 8f3045457b..1361a12af0 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b04bc26153..d4f16b5c96 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -10,6 +10,7 @@ $meta_count = eval('return count($meta);'); eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); +eval('function native_answer() { return 42; }'); echo "x=" . $x . "\n"; echo "created=" . $created . "\n"; @@ -17,4 +18,5 @@ echo "source=" . $meta["source"] . "\n"; echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; +echo "native-dynamic-call=" . native_answer() . "\n"; echo "result=" . $result . "\n"; diff --git a/src/codegen/context.rs b/src/codegen/context.rs index a53b46dcac..31c544f7cf 100644 --- a/src/codegen/context.rs +++ b/src/codegen/context.rs @@ -549,6 +549,7 @@ impl<'a> FunctionContext<'a> { | Op::Call | Op::FunctionVariantCall | Op::BuiltinCall + | Op::EvalFunctionCall | Op::RuntimeCall | Op::ExternCall | Op::MethodCall diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index ba362e3f93..9dcb4e5a3d 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -197,6 +197,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::EnumBackingMixedToInt => enums::lower_enum_backing_mixed_to_int(ctx, &inst), Op::ExternCall => externs::lower_extern_call(ctx, &inst), Op::BuiltinCall => builtins::lower_builtin_call(ctx, &inst), + Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), Op::ClosureCapture => lower_closure_capture(ctx, &inst), Op::ClosureNew => lower_closure_new(ctx, &inst), Op::FirstClassCallableNew => lower_first_class_callable_new(ctx, &inst), diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 0d3a3fa9e4..0b00120c52 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -80,6 +80,14 @@ pub(super) fn lower_hash_isset(ctx: &mut FunctionContext<'_>, inst: &Instruction isset::lower_hash_isset(ctx, inst) } +/// Lowers a native call to a zero-argument function declared through `eval()`. +pub(super) fn lower_eval_function_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_function_call(ctx, inst) +} + /// Lowers `define("NAME", value)` with the duplicate-name runtime guard. pub(crate) fn lower_define(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "define", 2)?; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 9dc2e84fb2..7abdb2eb94 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -19,7 +19,7 @@ use crate::ir::{Instruction, LocalKind, LocalSlotId}; use crate::types::PhpType; use super::super::super::context::FunctionContext; -use super::super::{expect_operand, store_if_result}; +use super::super::{expect_data, expect_operand, store_if_result}; const EVAL_STATUS_PARSE_ERROR: i64 = 1; const EVAL_STATUS_UNSUPPORTED: i64 = 4; @@ -80,6 +80,38 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R store_if_result(ctx, inst) } +/// Lowers a native call to a zero-argument function declared by a prior `eval()` call. +pub(super) fn lower_eval_function_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "eval-declared function call", 0)?; + let function_name = ctx.function_name_data(expect_data(inst)?)?.to_string(); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_call_function_zero_args"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Saves the loaded eval source string while scope setup calls use argument registers. fn save_eval_code_string(ctx: &mut FunctionContext<'_>) { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 389ff8e84a..ae68727c9e 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -355,6 +355,7 @@ pub enum Op { Call, FunctionVariantCall, BuiltinCall, + EvalFunctionCall, RuntimeCall, ExternCall, ClosureNew, @@ -491,8 +492,8 @@ impl Op { EnumBackingStringToInt | EnumBackingMixedToInt => { E::READS_HEAP | E::ALLOC_HEAP | E::MAY_THROW } - Call | FunctionVariantCall | BuiltinCall | RuntimeCall | ClosureCall | ExprCall - | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { + Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | RuntimeCall + | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { E::all().difference(E::REFCOUNT_OP) } ExternCall | ExternGlobalLoad | ExternGlobalStore => { @@ -515,6 +516,7 @@ impl Op { Op::Call | Op::FunctionVariantCall | Op::BuiltinCall + | Op::EvalFunctionCall | Op::RuntimeCall | Op::ExternCall | Op::MethodCall @@ -689,6 +691,7 @@ impl Op { Call => "call", FunctionVariantCall => "function_variant_call", BuiltinCall => "builtin_call", + EvalFunctionCall => "eval_function_call", RuntimeCall => "runtime_call", ExternCall => "extern_call", ClosureNew => "closure_new", diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 6248650e04..b0fa370b92 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -140,6 +140,7 @@ pub(crate) struct LoweringContext<'m, 'f> { pending_static_callable_result: Option, closure_counter: usize, hidden_temp_counter: usize, + eval_barrier_active: bool, } impl<'m, 'f> LoweringContext<'m, 'f> { @@ -204,6 +205,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { pending_static_callable_result: None, closure_counter: 0, hidden_temp_counter: 0, + eval_barrier_active: false, } } @@ -468,6 +470,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Applies the static part of the eval barrier to visible PHP local storage. pub(crate) fn apply_eval_barrier(&mut self) { + self.eval_barrier_active = true; self.declare_eval_context_local(); self.declare_eval_scope_local(); let local_names = self @@ -499,6 +502,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } } + /// Returns true after this function has lowered an `eval()` call. + pub(crate) const fn has_eval_barrier(&self) -> bool { + self.eval_barrier_active + } + /// Declares a hidden owner slot for a promoted local ref-cell pointer. fn declare_ref_cell_owner(&mut self, variable: &str, php_type: PhpType) -> LocalSlotId { let name = format!("__eir_ref_owner{}_{}", self.hidden_temp_counter, variable); @@ -1142,6 +1150,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::GeneratorYieldFrom | Op::Call | Op::FunctionVariantCall + | Op::EvalFunctionCall | Op::RuntimeCall | Op::ExternCall | Op::MethodCall diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 26226a66f4..7e393c52bc 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1831,6 +1831,21 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E release_owned_call_arg_temporaries(ctx, &operands, Some(call.value), expr.span); return call; } + if ctx.has_eval_barrier() + && args.is_empty() + && canonical_builtin_function_name(canonical).is_none() + { + let dynamic_name = php_symbol_key(canonical.trim_start_matches('\\')); + let data = ctx.intern_function_name(&dynamic_name); + return ctx.emit_value( + Op::EvalFunctionCall, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalFunctionCall.default_effects(), + Some(expr.span), + ); + } emit_builtin_call_value(ctx, canonical, operands, php_type, expr.span) } diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index e9e087763d..57bea89bcf 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -120,6 +120,9 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { features.eval = true; } } + Op::EvalFunctionCall => { + features.eval = true; + } Op::ExprCall | Op::CallableDescriptorInvoke => { features.descriptor_invoker = true; } diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index 1f366a2859..c248f1054f 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -158,6 +158,13 @@ impl Checker { return result; } + if self.eval_barrier_active { + for arg in args { + self.infer_type(arg, caller_env)?; + } + return Ok(PhpType::Mixed); + } + let decl = self .fn_decls .get(name) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d7eb54f4ab..f176ced1d1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -399,6 +399,18 @@ eval('echo dyn_eval_inc(4);'); assert_eq!(out, "5"); } +/// Verifies native code can call a zero-argument function declared by eval. +#[test] +fn test_eval_declared_function_can_be_called_from_native_code() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 16:38:00 +0200 Subject: [PATCH 0012/1208] Pass args to eval-declared native calls --- crates/elephc-eval/src/interpreter.rs | 23 +++++++++++- crates/elephc-eval/src/lib.rs | 49 ++++++++++++++++++++++--- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 4 +- src/codegen/lower_inst/builtins/eval.rs | 49 +++++++++++++++++++++---- src/ir_lower/expr/mod.rs | 10 ++++- tests/codegen/eval.rs | 12 ++++++ 8 files changed, 131 insertions(+), 22 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 3174d07bdf..2ff76912ff 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -150,13 +150,22 @@ pub fn execute_context_function_zero_args( context: &mut ElephcEvalContext, name: &str, values: &mut impl RuntimeValueOps, +) -> Result { + execute_context_function(context, name, Vec::new(), values) +} + +/// Executes a function declared in the shared eval context with prepared argument cells. +pub fn execute_context_function( + context: &mut ElephcEvalContext, + name: &str, + args: Vec, + values: &mut impl RuntimeValueOps, ) -> Result { context .function(name) .cloned() .map_or(Err(EvalStatus::UnsupportedConstruct), |function| { - let mut caller_scope = ElephcEvalScope::new(); - eval_dynamic_function(&function, &[], context, &mut caller_scope, values) + eval_dynamic_function_with_values(&function, args, context, values) }) } @@ -477,6 +486,16 @@ fn eval_dynamic_function( for arg in args { evaluated_args.push(eval_expr(arg, context, caller_scope, values)?); } + eval_dynamic_function_with_values(function, evaluated_args, context, values) +} + +/// Evaluates an eval-declared function after its positional arguments are prepared. +fn eval_dynamic_function_with_values( + function: &EvalFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { let mut function_scope = ElephcEvalScope::new(); for (name, value) in function.params().iter().zip(evaluated_args) { function_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index ecd5eeb2e3..75a94a06b0 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -218,7 +218,29 @@ pub unsafe extern "C" fn __elephc_eval_call_function_zero_args( out: *mut ElephcEvalResult, ) -> i32 { std::panic::catch_unwind(|| unsafe { - call_eval_function_zero_args_inner(ctx, name_ptr, name_len, out) + call_eval_function_inner(ctx, name_ptr, name_len, std::ptr::null(), 0, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a function previously declared through `eval()` with positional cells. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `args` must be readable for +/// `arg_count` runtime-cell pointers when `arg_count > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_inner(ctx, name_ptr, name_len, args, arg_count, out) }) .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } @@ -226,13 +248,15 @@ pub unsafe extern "C" fn __elephc_eval_call_function_zero_args( /// Runs the dynamic function-call ABI body after installing a panic boundary. /// /// # Safety -/// Mirrors `__elephc_eval_call_function_zero_args`; callers must provide a valid -/// context and readable function-name bytes. +/// Mirrors `__elephc_eval_call_function`; callers must provide a valid context, +/// readable function-name bytes, and readable argument pointer storage. #[cfg(not(test))] -unsafe fn call_eval_function_zero_args_inner( +unsafe fn call_eval_function_inner( ctx: *mut ElephcEvalContext, name_ptr: *const u8, name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, out: *mut ElephcEvalResult, ) -> i32 { let Some(context) = ctx.as_mut() else { @@ -244,13 +268,28 @@ unsafe fn call_eval_function_zero_args_inner( let Ok(name) = abi_name_to_string(name_ptr, name_len) else { return EvalStatus::RuntimeFatal.code(); }; + let Ok(arg_count) = usize::try_from(arg_count) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_count > 0 && args.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(args, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; if !out.is_null() { (*out).clear(); } let mut values = ElephcRuntimeOps::new(); - match interpreter::execute_context_function_zero_args( + match interpreter::execute_context_function( context, &name.to_ascii_lowercase(), + args, &mut values, ) { Ok(result) => { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index b11b5d96ff..af646608dc 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried by native zero-argument call sites after an eval barrier. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried by native call sites with simple positional arguments after an eval barrier. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, zero-argument eval-declared functions callable from native code after the barrier, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch with arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, eval-declared functions callable from native code with simple positional arguments after the barrier, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 1361a12af0..35dfb10fe8 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index d4f16b5c96..ecd55b2cc9 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -10,7 +10,7 @@ $meta_count = eval('return count($meta);'); eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); -eval('function native_answer() { return 42; }'); +eval('function native_add($left, $right) { return $left + $right; }'); echo "x=" . $x . "\n"; echo "created=" . $created . "\n"; @@ -18,5 +18,5 @@ echo "source=" . $meta["source"] . "\n"; echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; -echo "native-dynamic-call=" . native_answer() . "\n"; +echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "result=" . $result . "\n"; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 7abdb2eb94..f87f3ae6c4 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -85,10 +85,12 @@ pub(super) fn lower_eval_function_call( ctx: &mut FunctionContext<'_>, inst: &Instruction, ) -> Result<()> { - super::ensure_arg_count(inst, "eval-declared function call", 0)?; let function_name = ctx.function_name_data(expect_data(inst)?)?.to_string(); - abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_function_call_stack_bytes(inst.operands.len()); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); ensure_eval_context(ctx)?; + store_eval_function_call_args(ctx, inst, args_offset)?; load_eval_context_to_arg(ctx, 0); let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); @@ -98,20 +100,51 @@ pub(super) fn lower_eval_function_call( abi::int_arg_reg_name(ctx.emitter.target, 2), name_len as i64, ); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + if inst.operands.is_empty() { + abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + } else { + abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + inst.operands.len() as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); - let symbol = ctx - .emitter - .target - .extern_symbol("__elephc_eval_call_function_zero_args"); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_call_function"); abi::emit_call_label(ctx.emitter, &symbol); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); - abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); store_if_result(ctx, inst) } +/// Returns the aligned scratch size for an eval-declared function call. +fn eval_function_call_stack_bytes(arg_count: usize) -> usize { + let bytes = EVAL_STACK_BYTES + arg_count * 8; + (bytes + 15) & !15 +} + +/// Stores positional operands as boxed Mixed cells for the eval function-call ABI. +fn store_eval_function_call_args( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + args_offset: usize, +) -> Result<()> { + for (index, operand) in inst.operands.iter().enumerate() { + let ty = ctx.load_value_to_result(*operand)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset + index * 8); + } + Ok(()) +} + /// Saves the loaded eval source string while scope setup calls use argument registers. fn save_eval_code_string(ctx: &mut FunctionContext<'_>) { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 7e393c52bc..6ebb698e4b 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1832,14 +1832,14 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E return call; } if ctx.has_eval_barrier() - && args.is_empty() + && plain_positional_call_args(args) && canonical_builtin_function_name(canonical).is_none() { let dynamic_name = php_symbol_key(canonical.trim_start_matches('\\')); let data = ctx.intern_function_name(&dynamic_name); return ctx.emit_value( Op::EvalFunctionCall, - Vec::new(), + operands, Some(Immediate::Data(data)), PhpType::Mixed, Op::EvalFunctionCall.default_effects(), @@ -1873,6 +1873,12 @@ fn emit_builtin_call_value( call } +/// Returns true when a dynamic eval fallback can preserve simple positional call semantics. +fn plain_positional_call_args(args: &[Expr]) -> bool { + !crate::types::call_args::has_named_args(args) + && !args.iter().any(is_spread_arg) +} + /// Lowers `isset()` as a lazy language construct instead of an eager builtin call. fn lower_lazy_isset( ctx: &mut LoweringContext<'_, '_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f176ced1d1..dc8150f790 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -411,6 +411,18 @@ echo dyn_eval_native(); assert_eq!(out, "42"); } +/// Verifies native calls can pass positional arguments to eval-declared functions. +#[test] +fn test_eval_declared_function_native_call_accepts_positional_args() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 16:50:02 +0200 Subject: [PATCH 0013/1208] Dispatch eval functions through call_user_func --- docs/internals/the-runtime.md | 4 ++-- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 ++ src/ir_lower/expr/mod.rs | 37 +++++++++++++++++++++++++++++++++-- tests/codegen/eval.rs | 12 ++++++++++++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index af646608dc..5582cd64da 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried by native call sites with simple positional arguments after an eval barrier. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried by native call sites and string-literal `call_user_func()` callbacks with simple positional arguments after an eval barrier. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, eval-declared functions callable from native code with simple positional arguments after the barrier, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 35dfb10fe8..f89f1e0c2b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index ecd55b2cc9..6eea6e1fbf 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -11,6 +11,7 @@ eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); eval('function native_add($left, $right) { return $left + $right; }'); +eval('function native_double($value) { return $value * 2; }'); echo "x=" . $x . "\n"; echo "created=" . $created . "\n"; @@ -19,4 +20,5 @@ echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; echo "native-dynamic-call=" . native_add(40, 2) . "\n"; +echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; echo "result=" . $result . "\n"; diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 6ebb698e4b..f343a94a8d 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -2360,8 +2360,10 @@ fn lower_static_call_user_func( expr, ); } - let callback = static_call_user_func_callback(ctx, callback_expr)?; - lower_static_callable_call(ctx, callback, callback_args, expr) + if let Some(callback) = static_call_user_func_callback(ctx, callback_expr) { + return lower_static_callable_call(ctx, callback, callback_args, expr); + } + lower_eval_call_user_func_fallback(ctx, callback_expr, callback_args, expr) } "call_user_func_array" => { let [callback_arg, arg_array] = args else { @@ -2390,6 +2392,37 @@ fn lower_static_call_user_func( } } +/// Lowers unresolved string callbacks after an eval barrier through the eval function table. +fn lower_eval_call_user_func_fallback( + ctx: &mut LoweringContext<'_, '_>, + callback_expr: &Expr, + callback_args: &[Expr], + expr: &Expr, +) -> Option { + if !ctx.has_eval_barrier() || !plain_positional_call_args(callback_args) { + return None; + } + let ExprKind::StringLiteral(callback_name) = &callback_expr.kind else { + return None; + }; + if callback_name.contains("::") + || resolve_static_string_callable(ctx, callback_name).is_some() + { + return None; + } + let dynamic_name = php_symbol_key(callback_name.trim_start_matches('\\')); + let data = ctx.intern_function_name(&dynamic_name); + let operands = lower_args(ctx, callback_args); + Some(ctx.emit_value( + Op::EvalFunctionCall, + operands, + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalFunctionCall.default_effects(), + Some(expr.span), + )) +} + /// Lowers `call_user_func*` for receiver-bound first-class callables through `expr_call`. fn lower_instance_callable_call_user_func( ctx: &mut LoweringContext<'_, '_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index dc8150f790..1a55c47f51 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -423,6 +423,18 @@ echo dyn_eval_native_add(4, 5); assert_eq!(out, "9"); } +/// Verifies `call_user_func()` can dispatch to an eval-declared function after the barrier. +#[test] +fn test_eval_declared_function_can_be_called_with_call_user_func() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 17:02:47 +0200 Subject: [PATCH 0014/1208] Probe eval-declared functions from native code --- crates/elephc-eval/src/context.rs | 5 ++ crates/elephc-eval/src/lib.rs | 62 +++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/builtins.rs | 8 ++++ src/codegen/lower_inst/builtins/eval.rs | 23 +++++++++ src/ir/instr.rs | 3 ++ src/ir/validator.rs | 4 +- src/ir_lower/expr/mod.rs | 41 ++++++++++++++++ src/ir_lower/program.rs | 2 +- tests/codegen/eval.rs | 14 ++++++ 13 files changed, 165 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index e254acc08d..7ad03dc6fe 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -67,6 +67,11 @@ impl ElephcEvalContext { pub fn function(&self, name: &str) -> Option<&EvalFunction> { self.functions.get(name) } + + /// Returns true when the context has a dynamic function with this lowercase PHP name. + pub fn has_function(&self, name: &str) -> bool { + self.functions.contains_key(name) + } } impl Default for ElephcEvalContext { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 75a94a06b0..5ccd8db521 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -204,6 +204,23 @@ pub unsafe extern "C" fn __elephc_eval_execute( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Checks whether a function was previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_function_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_function_exists_inner(ctx, name_ptr, name_len) + }) + .unwrap_or(0) +} + /// Calls a zero-argument function previously declared through `eval()`. /// /// # Safety @@ -245,6 +262,28 @@ pub unsafe extern "C" fn __elephc_eval_call_function( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Runs the eval function-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_function_exists`; invalid handles or unreadable name +/// storage fail closed as `false`. +unsafe fn eval_function_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_function(&name.to_ascii_lowercase())) +} + /// Runs the dynamic function-call ABI body after installing a panic boundary. /// /// # Safety @@ -495,6 +534,29 @@ mod tests { assert_eq!(version, ABI_VERSION); } + /// Verifies the function-exists ABI probes eval-declared functions by folded name. + #[test] + fn function_exists_reports_declared_eval_function() { + let mut ctx = ElephcEvalContext::new(); + ctx.define_function( + "dyn_probe", + crate::eval_ir::EvalFunction::new(Vec::new(), Vec::new()), + ) + .expect("first dynamic function declaration should succeed"); + let existing = b"DYN_PROBE"; + let missing = b"missing"; + + let existing_result = unsafe { + __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) + }; + let missing_result = unsafe { + __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) + }; + + assert_eq!(existing_result, 1); + assert_eq!(missing_result, 0); + } + /// Verifies scope allocation returns an empty opaque activation scope handle. #[test] fn scope_new_returns_empty_handle() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 5582cd64da..bf71bb313e 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried by native call sites and string-literal `call_user_func()` callbacks with simple positional arguments after an eval barrier. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index f89f1e0c2b..d0e9b48525 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 6eea6e1fbf..98e4f71275 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -21,4 +21,5 @@ echo "dynamic-call=" . $dynamic_call . "\n"; echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; +echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; echo "result=" . $result . "\n"; diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 9dcb4e5a3d..260d25a937 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -198,6 +198,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::ExternCall => externs::lower_extern_call(ctx, &inst), Op::BuiltinCall => builtins::lower_builtin_call(ctx, &inst), Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), + Op::EvalFunctionExists => builtins::lower_eval_function_exists(ctx, &inst), Op::ClosureCapture => lower_closure_capture(ctx, &inst), Op::ClosureNew => lower_closure_new(ctx, &inst), Op::FirstClassCallableNew => lower_first_class_callable_new(ctx, &inst), diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 0b00120c52..3884fa5894 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -88,6 +88,14 @@ pub(super) fn lower_eval_function_call( eval::lower_eval_function_call(ctx, inst) } +/// Lowers post-eval dynamic function existence probes to the optional eval bridge. +pub(super) fn lower_eval_function_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_function_exists(ctx, inst) +} + /// Lowers `define("NAME", value)` with the duplicate-name runtime guard. pub(crate) fn lower_define(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "define", 2)?; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index f87f3ae6c4..14a7582192 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -122,6 +122,29 @@ pub(super) fn lower_eval_function_call( store_if_result(ctx, inst) } +/// Lowers a post-eval dynamic function existence probe to the eval bridge ABI. +pub(super) fn lower_eval_function_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let function_name = ctx.function_name_data(expect_data(inst)?)?.to_string(); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_function_exists"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Returns the aligned scratch size for an eval-declared function call. fn eval_function_call_stack_bytes(arg_count: usize) -> usize { let bytes = EVAL_STACK_BYTES + arg_count * 8; diff --git a/src/ir/instr.rs b/src/ir/instr.rs index ae68727c9e..4ade27dae0 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -356,6 +356,7 @@ pub enum Op { FunctionVariantCall, BuiltinCall, EvalFunctionCall, + EvalFunctionExists, RuntimeCall, ExternCall, ClosureNew, @@ -492,6 +493,7 @@ impl Op { EnumBackingStringToInt | EnumBackingMixedToInt => { E::READS_HEAP | E::ALLOC_HEAP | E::MAY_THROW } + EvalFunctionExists => E::READS_GLOBAL, Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | RuntimeCall | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { E::all().difference(E::REFCOUNT_OP) @@ -692,6 +694,7 @@ impl Op { FunctionVariantCall => "function_variant_call", BuiltinCall => "builtin_call", EvalFunctionCall => "eval_function_call", + EvalFunctionExists => "eval_function_exists", RuntimeCall => "runtime_call", ExternCall => "extern_call", ClosureNew => "closure_new", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 508a134912..a38edda8c3 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -274,6 +274,7 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstBool => require_immediate(inst_id, inst, "bool", |imm| matches!(imm, Imm::Bool(_))), ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell + | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionExists | EnumBackingStringToInt | EnumBackingMixedToInt => { require_immediate(inst_id, inst, "data id", |imm| matches!(imm, Imm::Data(_))) } @@ -354,7 +355,8 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio | ErrorSuppressBegin | ErrorSuppressEnd | TryPushHandler | TryPopHandler | CatchCurrent | CatchBind | FinallyEnter | FinallyExit | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | ConcatReset - | GcCollect | Nop => { + | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | EvalFunctionExists + | ConcatReset | GcCollect | Nop => { check_count(inst_id, inst, 0, "0") } ClosureNew => Ok(()), diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index f343a94a8d..a3d4cd6dcf 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1785,6 +1785,9 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E if let Some(value) = lower_static_is_callable(ctx, canonical, args, expr) { return value; } + if let Some(value) = lower_eval_function_probe(ctx, canonical, args, expr) { + return value; + } let sig = call_signature(ctx, canonical, args); let is_extern = ctx.extern_functions.contains_key(canonical); let is_user_function = ctx.functions.contains_key(canonical); @@ -1879,6 +1882,44 @@ fn plain_positional_call_args(args: &[Expr]) -> bool { && !args.iter().any(is_spread_arg) } +/// Lowers post-eval function-name probes through the eval context's dynamic table. +fn lower_eval_function_probe( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let probe_name = php_symbol_key(name.trim_start_matches('\\')); + if probe_name != "function_exists" && probe_name != "is_callable" { + return None; + } + if !ctx.has_eval_barrier() + || args.len() != 1 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { + return None; + } + let ExprKind::StringLiteral(function_name) = &args[0].kind else { + return None; + }; + if function_name.contains("::") + || resolve_static_string_callable(ctx, function_name).is_some() + { + return None; + } + let dynamic_name = php_symbol_key(function_name.trim_start_matches('\\')); + let data = ctx.intern_function_name(&dynamic_name); + Some(ctx.emit_value( + Op::EvalFunctionExists, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Bool, + Op::EvalFunctionExists.default_effects(), + Some(expr.span), + )) +} + /// Lowers `isset()` as a lazy language construct instead of an eager builtin call. fn lower_lazy_isset( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 57bea89bcf..5a626790f9 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -120,7 +120,7 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { features.eval = true; } } - Op::EvalFunctionCall => { + Op::EvalFunctionCall | Op::EvalFunctionExists => { features.eval = true; } Op::ExprCall | Op::CallableDescriptorInvoke => { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1a55c47f51..7db5368561 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -435,6 +435,20 @@ echo call_user_func('dyn_eval_cuf', 4); assert_eq!(out, "5"); } +/// Verifies native callable probes can see functions declared by eval after the barrier. +#[test] +fn test_eval_declared_function_is_visible_to_callable_probes() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 17:08:31 +0200 Subject: [PATCH 0015/1208] Support eval function probes in fragments --- crates/elephc-eval/src/interpreter.rs | 51 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 16 +++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2ff76912ff..203879b33e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -413,6 +413,9 @@ fn eval_call( match name { "count" => eval_builtin_count(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), + "function_exists" | "is_callable" => { + eval_builtin_function_probe(args, context, scope, values) + } "strlen" => eval_builtin_strlen(args, context, scope, values), _ => context .function(name) @@ -423,6 +426,34 @@ fn eval_call( } } +/// Evaluates string-name function probes against eval and supported builtin tables. +fn eval_builtin_function_probe( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\').to_ascii_lowercase(); + values.bool_value(eval_function_probe_exists(context, &name)) +} + +/// Returns true when a PHP function name is visible to eval builtin probes. +fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { + !name.contains("::") + && (context.has_function(name) || eval_php_visible_builtin_exists(name)) +} + +/// Returns true for PHP-visible builtin names implemented by the eval interpreter. +fn eval_php_visible_builtin_exists(name: &str) -> bool { + matches!(name, "count" | "function_exists" | "is_callable" | "strlen") +} + /// Evaluates nested `eval(...)` calls against the current materialized scope. fn eval_nested_eval( args: &[EvalExpr], @@ -1197,6 +1228,26 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(6)); } + /// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. + #[test] + fn execute_program_function_probes_use_eval_context() { + let program = parse_fragment( + br#"function dyn_probe() { return 1; } +echo function_exists("DYN_PROBE") . "x"; +echo is_callable("dyn_probe") . "x"; +echo function_exists("strlen") . "x"; +echo function_exists("eval") . "x"; +echo function_exists("missing_probe") . "x";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1x1x1xxx"); + } + /// Verifies indexed array writes mutate an existing scope array. #[test] fn execute_program_writes_indexed_scope_array() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index bf71bb313e..8af1517110 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()` and `count()`, eval-declared functions callable from eval fragments that share the same generated context, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index d0e9b48525..ad3e4db325 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7db5368561..1dd72dddd0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -449,6 +449,22 @@ echo function_exists('missing_eval_probe') ? '1' : '0'; assert_eq!(out, "110"); } +/// Verifies callable probes inside eval inspect dynamic functions and supported builtins. +#[test] +fn test_eval_fragment_function_probes_use_dynamic_context() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 17:26:48 +0200 Subject: [PATCH 0016/1208] Call native functions from eval fragments --- crates/elephc-eval/src/context.rs | 71 +++++++++++++- crates/elephc-eval/src/interpreter.rs | 89 +++++++++++++++-- crates/elephc-eval/src/lib.rs | 92 ++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 4 + src/codegen/lower_inst/builtins/eval.rs | 122 +++++++++++++++++++++++- tests/codegen/eval.rs | 12 +++ 8 files changed, 376 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 7ad03dc6fe..a12400dd75 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -12,9 +12,52 @@ //! - No Rust-owned layout is promised across the C ABI. use std::collections::HashMap; +use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::EvalFunction; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +/// Native descriptor-invoker ABI registered by generated code for AOT functions. +pub type NativeFunctionInvoker = + unsafe extern "C" fn(*mut c_void, *mut RuntimeCell) -> *mut RuntimeCell; + +/// Native AOT function callback metadata visible to runtime eval fragments. +#[derive(Clone, Copy)] +pub struct NativeFunction { + descriptor: *mut c_void, + invoker: NativeFunctionInvoker, + param_count: usize, +} + +impl NativeFunction { + /// Creates callback metadata for a descriptor-compatible AOT function. + pub const fn new( + descriptor: *mut c_void, + invoker: NativeFunctionInvoker, + param_count: usize, + ) -> Self { + Self { + descriptor, + invoker, + param_count, + } + } + + /// Returns the visible positional parameter count accepted by this callback. + pub const fn param_count(self) -> usize { + self.param_count + } + + /// Invokes the descriptor-compatible callback with a boxed Mixed arg array. + /// + /// # Safety + /// `arg_array` must be a boxed Mixed indexed array whose elements are boxed + /// Mixed cells following the descriptor-invoker ABI. + pub unsafe fn call(self, arg_array: RuntimeCellHandle) -> RuntimeCellHandle { + RuntimeCellHandle::from_raw((self.invoker)(self.descriptor, arg_array.as_ptr())) + } +} /// Process-level eval context passed opaquely across the C ABI. /// @@ -24,6 +67,7 @@ use crate::eval_ir::EvalFunction; pub struct ElephcEvalContext { abi_version: u32, functions: HashMap, + native_functions: HashMap, } impl ElephcEvalContext { @@ -32,6 +76,7 @@ impl ElephcEvalContext { Self { abi_version: ABI_VERSION, functions: HashMap::new(), + native_functions: HashMap::new(), } } @@ -41,6 +86,7 @@ impl ElephcEvalContext { Self { abi_version, functions: HashMap::new(), + native_functions: HashMap::new(), } } @@ -56,21 +102,40 @@ impl ElephcEvalContext { function: EvalFunction, ) -> Result<(), EvalFunction> { let name = name.into(); - if self.functions.contains_key(&name) { + if self.functions.contains_key(&name) || self.native_functions.contains_key(&name) { return Err(function); } self.functions.insert(name, function); Ok(()) } + /// Defines a generated native function callback, failing if the name already exists. + pub fn define_native_function( + &mut self, + name: impl Into, + function: NativeFunction, + ) -> Result<(), NativeFunction> { + let name = name.into(); + if self.functions.contains_key(&name) || self.native_functions.contains_key(&name) { + return Err(function); + } + self.native_functions.insert(name, function); + Ok(()) + } + /// Returns a dynamic user function by its lowercase PHP function name. pub fn function(&self, name: &str) -> Option<&EvalFunction> { self.functions.get(name) } - /// Returns true when the context has a dynamic function with this lowercase PHP name. + /// Returns a native AOT function callback by its lowercase PHP function name. + pub fn native_function(&self, name: &str) -> Option { + self.native_functions.get(name).copied() + } + + /// Returns true when the context has a dynamic or native function with this lowercase PHP name. pub fn has_function(&self, name: &str) -> bool { - self.functions.contains_key(name) + self.functions.contains_key(name) || self.native_functions.contains_key(name) } } diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 203879b33e..b5f1ba190c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -12,7 +12,7 @@ //! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. use crate::errors::EvalStatus; -use crate::context::ElephcEvalContext; +use crate::context::{ElephcEvalContext, NativeFunction}; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalFunction, EvalProgram, EvalStmt, }; @@ -417,12 +417,15 @@ fn eval_call( eval_builtin_function_probe(args, context, scope, values) } "strlen" => eval_builtin_strlen(args, context, scope, values), - _ => context - .function(name) - .cloned() - .map_or(Err(EvalStatus::UnsupportedConstruct), |function| { - eval_dynamic_function(&function, args, context, scope, values) - }), + _ => { + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + Err(EvalStatus::UnsupportedConstruct) + } } } @@ -538,6 +541,31 @@ fn eval_dynamic_function_with_values( } } +/// Evaluates a registered AOT function through its descriptor-compatible invoker. +fn eval_native_function( + function: NativeFunction, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() != function.param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let arg_array = values.array_new(args.len())?; + for (index, arg) in args.iter().enumerate() { + let index = values.int(index as i64)?; + let value = eval_expr(arg, context, caller_scope, values)?; + let _ = values.array_set(arg_array, index, value)?; + } + let result = unsafe { function.call(arg_array) }; + values.release(arg_array)?; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(result) +} + /// Evaluates an indexed array literal into a boxed runtime Mixed array. fn eval_indexed_array( elements: &[EvalArrayElement], @@ -606,6 +634,7 @@ pub fn current_stub_status() -> EvalStatus { #[cfg(test)] mod tests { use std::collections::HashMap; + use std::ffi::c_void; use crate::parser::parse_fragment; use crate::value::RuntimeCell; @@ -948,6 +977,14 @@ mod tests { } } + /// Test native invoker that returns the descriptor pointer as a runtime cell. + unsafe extern "C" fn fake_native_return_descriptor( + descriptor: *mut c_void, + _args: *mut RuntimeCell, + ) -> *mut RuntimeCell { + descriptor.cast() + } + /// Verifies assignment writes a named scope entry and return reads it back. #[test] fn execute_program_stores_and_returns_scope_value() { @@ -1236,16 +1273,50 @@ mod tests { echo function_exists("DYN_PROBE") . "x"; echo is_callable("dyn_probe") . "x"; echo function_exists("strlen") . "x"; +echo function_exists("native_probe") . "x"; echo function_exists("eval") . "x"; echo function_exists("missing_probe") . "x";"#, ) .expect("parse eval fragment"); + let native = NativeFunction::new( + 1usize as *mut c_void, + fake_native_return_descriptor, + 0, + ); + let mut context = ElephcEvalContext::new(); + assert!(context.define_native_function("native_probe", native).is_ok()); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "1x1x1x1xxx"); + } + + /// Verifies eval fragments can dispatch registered native AOT functions. + #[test] + fn execute_program_calls_registered_native_function() { + let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new( + expected.as_ptr().cast(), + fake_native_return_descriptor, + 0, + ); + assert!( + context + .define_native_function("native_answer", native) + .is_ok() + ); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); - assert_eq!(values.output, "1x1x1xxx"); + assert_eq!(result, expected); } /// Verifies indexed array writes mutate an existing scope array. diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 5ccd8db521..505aa9f718 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -27,10 +27,12 @@ use abi::{ ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_BY_REF, SCOPE_FLAG_DIRTY, SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, }; +use context::{NativeFunction, NativeFunctionInvoker}; use errors::EvalStatus; #[cfg(not(test))] use runtime_hooks::ElephcRuntimeOps; use scope::{ScopeCellOwnership, ScopeEntry}; +use std::ffi::c_void; use std::slice; use value::{RuntimeCell, RuntimeCellHandle}; @@ -221,6 +223,27 @@ pub unsafe extern "C" fn __elephc_eval_function_exists( .unwrap_or(0) } +/// Registers a generated native PHP function callback in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `descriptor` and `invoker` must follow +/// the descriptor-invoker ABI emitted by generated code. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + descriptor: *mut c_void, + invoker: Option, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_inner(ctx, name_ptr, name_len, descriptor, invoker, param_count) + }) + .unwrap_or(0) +} + /// Calls a zero-argument function previously declared through `eval()`. /// /// # Safety @@ -284,6 +307,42 @@ unsafe fn eval_function_exists_inner( i32::from(context.has_function(&name.to_ascii_lowercase())) } +/// Runs the native registration ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function`; invalid handles, names, or +/// callback pointers fail closed as `false`. +unsafe fn register_native_function_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + descriptor: *mut c_void, + invoker: Option, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || descriptor.is_null() { + return 0; + } + let Some(invoker) = invoker else { + return 0; + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + let function = NativeFunction::new(descriptor, invoker, param_count); + i32::from( + context + .define_native_function(name.to_ascii_lowercase(), function) + .is_ok(), + ) +} + /// Runs the dynamic function-call ABI body after installing a panic boundary. /// /// # Safety @@ -492,6 +551,14 @@ fn release_scope_cell(cell: RuntimeCellHandle) { mod tests { use super::*; + /// Test native invoker placeholder used only to validate ABI registration. + unsafe extern "C" fn fake_native_invoker( + _descriptor: *mut c_void, + _args: *mut RuntimeCell, + ) -> *mut RuntimeCell { + std::ptr::null_mut() + } + /// Verifies the exported version entry point reports the crate ABI constant. #[test] fn abi_version_matches_constant() { @@ -557,6 +624,31 @@ mod tests { assert_eq!(missing_result, 0); } + /// Verifies native AOT registration makes functions visible through the ABI probe. + #[test] + fn register_native_function_reports_function_exists() { + let mut ctx = ElephcEvalContext::new(); + let name = b"NATIVE_PROBE"; + let descriptor = 1usize as *mut c_void; + + let registered = unsafe { + __elephc_eval_register_native_function( + &mut ctx, + name.as_ptr(), + name.len() as u64, + descriptor, + Some(fake_native_invoker), + 0, + ) + }; + let exists = unsafe { + __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) + }; + + assert_eq!(registered, 1); + assert_eq!(exists, 1); + } + /// Verifies scope allocation returns an empty opaque activation scope handle. #[test] fn scope_new_returns_empty_handle() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 8af1517110..4989214824 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ad3e4db325..b111f1885a 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 98e4f71275..c6219143ca 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -1,4 +1,6 @@ "Ada"]; $result = eval('$x = $x + 2; $created = "dynamic"; return $x + 4;'); @@ -10,6 +12,7 @@ $meta_count = eval('return count($meta);'); eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); +$eval_native_call = eval('return compiled_add(2, 8);'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -19,6 +22,7 @@ echo "source=" . $meta["source"] . "\n"; echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; +echo "eval-native-call=" . $eval_native_call . "\n"; echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 14a7582192..acbc2e4c3b 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -13,13 +13,18 @@ //! - The bridge is target-mangled like other C staticlib symbols. use crate::codegen::platform::Arch; -use crate::codegen::{CodegenIrError, Result}; -use crate::codegen::{abi, emit_box_current_value_as_mixed}; -use crate::ir::{Instruction, LocalKind, LocalSlotId}; -use crate::types::PhpType; +use crate::codegen::{ + abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, +}; +use crate::ir::{Function, Instruction, LocalKind, LocalSlotId}; +use crate::names::function_symbol; +use crate::types::{FunctionSig, PhpType}; use super::super::super::context::FunctionContext; -use super::super::{expect_data, expect_operand, store_if_result}; +use super::super::{ + emit_runtime_callable_invoker_inline, expect_data, expect_operand, function_signature_from_eir, + store_if_result, +}; const EVAL_STATUS_PARSE_ERROR: i64 = 1; const EVAL_STATUS_UNSUPPORTED: i64 = 4; @@ -45,6 +50,12 @@ struct EvalSyncLocal { ty: PhpType, } +/// A module-local function that can be registered with the eval context. +struct EvalNativeFunctionRegistration { + name: String, + signature: FunctionSig, +} + /// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { super::ensure_arg_count(inst, "eval", 1)?; @@ -186,6 +197,7 @@ fn ensure_eval_context(ctx: &mut FunctionContext<'_>) -> Result<()> { let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_context_new"); abi::emit_call_label(ctx.emitter, &symbol); abi::store_at_offset(ctx.emitter, result_reg, offset); + register_eval_native_functions(ctx, offset)?; ctx.emitter.label(&ready); abi::load_at_offset(ctx.emitter, result_reg, offset); abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_CONTEXT_HANDLE_OFFSET); @@ -202,6 +214,106 @@ fn eval_context_slot(ctx: &FunctionContext<'_>) -> Result { .ok_or_else(|| CodegenIrError::invalid_module("eval call missing eval context local")) } +/// Registers eligible AOT global functions with a newly allocated eval context. +fn register_eval_native_functions( + ctx: &mut FunctionContext<'_>, + context_offset: usize, +) -> Result<()> { + let registrations = eval_native_function_registrations(ctx); + for registration in registrations { + register_eval_native_function(ctx, context_offset, ®istration)?; + } + Ok(()) +} + +/// Collects global PHP functions that can use the descriptor-invoker bridge. +fn eval_native_function_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + ctx.module + .functions + .iter() + .filter(|function| function_can_register_with_eval(function)) + .map(|function| EvalNativeFunctionRegistration { + name: function.name.clone(), + signature: function_signature_from_eir(function), + }) + .collect() +} + +/// Returns true when a module function is a PHP-visible AOT function supported by this bridge. +fn function_can_register_with_eval(function: &Function) -> bool { + !function.flags.is_main + && !function.name.starts_with('_') + && function.params.iter().all(|param| !param.by_ref && !param.variadic) +} + +/// Emits one native-function registration call into the just-created eval context. +fn register_eval_native_function( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeFunctionRegistration, +) -> Result<()> { + let invoker_label = emit_runtime_callable_invoker_inline(ctx, ®istration.signature, &[]); + let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( + ctx.data, + &function_symbol(®istration.name), + Some(®istration.name), + callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, + Some(®istration.signature), + &[], + &[], + callable_descriptor::CallableDescriptorInvocation::named( + callable_descriptor::CallableDescriptorShape::Function, + registration.name.clone(), + ), + Some(&invoker_label), + ); + load_eval_context_local_to_arg(ctx, context_offset, 0); + let (name_label, name_len) = ctx.data.add_string(registration.name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &descriptor_label, + ); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &invoker_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + registration.signature.params.len() as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function"); + abi::emit_call_label(ctx.emitter, &symbol); + Ok(()) +} + +/// Loads the persistent eval context local into the selected integer argument register. +fn load_eval_context_local_to_arg( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + arg_index: usize, +) { + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + abi::load_at_offset(ctx.emitter, arg_reg, context_offset); +} + /// Loads the current eval context handle into the selected integer argument register. fn load_eval_context_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1dd72dddd0..3d6a1a73ca 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -435,6 +435,18 @@ echo call_user_func('dyn_eval_cuf', 4); assert_eq!(out, "5"); } +/// Verifies eval fragments can call AOT user functions registered in the eval context. +#[test] +fn test_eval_fragment_can_call_native_user_function() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 17:47:05 +0200 Subject: [PATCH 0017/1208] Support eval access to native properties --- crates/elephc-eval/src/eval_ir.rs | 9 + crates/elephc-eval/src/interpreter.rs | 89 ++++ crates/elephc-eval/src/parser.rs | 105 +++- crates/elephc-eval/src/runtime_hooks.rs | 48 ++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 11 + src/codegen/eval_property_helpers.rs | 650 ++++++++++++++++++++++++ src/codegen/lower_inst/builtins/eval.rs | 14 + src/codegen/mod.rs | 4 +- tests/codegen/eval.rs | 21 + 10 files changed, 943 insertions(+), 10 deletions(-) create mode 100644 src/codegen/eval_property_helpers.rs diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index c6b9d25af4..5d35eb07a8 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -76,6 +76,11 @@ pub enum EvalStmt { else_branch: Vec, }, Return(Option), + PropertySet { + object: EvalExpr, + property: String, + value: EvalExpr, + }, StoreVar { name: String, value: EvalExpr, @@ -128,6 +133,10 @@ pub enum EvalExpr { }, Const(EvalConst), LoadVar(String), + PropertyGet { + object: Box, + property: String, + }, Print(Box), Binary { op: EvalBinOp, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b5f1ba190c..25f59b8d66 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -51,6 +51,21 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result; + /// Reads a named property from a runtime object held in a boxed Mixed cell. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result; + + /// Writes a named property on a runtime object held in a boxed Mixed cell. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus>; + /// Returns the visible element count for an array-like runtime cell. fn array_len(&mut self, array: RuntimeCellHandle) -> Result; @@ -255,6 +270,16 @@ fn execute_stmt( Ok(EvalControl::Return(eval_expr(expr, context, scope, values)?)) } EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), + EvalStmt::PropertySet { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + values.property_set(object, property, value)?; + Ok(EvalControl::None) + } EvalStmt::StoreVar { name, value } => { let value = eval_expr(value, context, scope, values)?; if let Some(replaced) = scope.set(name.clone(), value, ScopeCellOwnership::Owned) { @@ -378,6 +403,10 @@ fn eval_expr( EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::LoadVar(name) => scope.visible_cell(name).map_or_else(|| values.null(), Ok), + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + values.property_get(object, property) + } EvalExpr::Print(inner) => { let value = eval_expr(inner, context, scope, values)?; values.echo(value)?; @@ -658,6 +687,7 @@ mod tests { String(String), Array(Vec), Assoc(HashMap), + Object(HashMap), } /// Test runtime hooks that allocate stable fake handles and record echo output. @@ -763,6 +793,35 @@ mod tests { Ok(array) } + /// Reads one fake object property by name. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + properties.get(property).copied().map_or_else(|| self.null(), Ok) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Writes one fake object property by name. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + properties.insert(property.to_string(), value); + Ok(()) + } + /// Returns the visible element count for fake array values. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { match self.get(array) { @@ -901,6 +960,7 @@ mod tests { FakeValue::String(value) => !value.is_empty() && value != "0", FakeValue::Array(value) => !value.is_empty(), FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) => true, }) } } @@ -946,6 +1006,7 @@ mod tests { FakeValue::String(value) => value.parse::().unwrap_or(0.0), FakeValue::Array(value) => value.len() as f64, FakeValue::Assoc(value) => value.len() as f64, + FakeValue::Object(_) => 1.0, } } @@ -959,6 +1020,7 @@ mod tests { FakeValue::String(value) => !value.is_empty() && value != "0", FakeValue::Array(value) => !value.is_empty(), FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) => true, } } @@ -973,6 +1035,7 @@ mod tests { FakeValue::String(value) => value, FakeValue::Array(_) => "Array".to_string(), FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) => "Object".to_string(), } } } @@ -1029,6 +1092,32 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(1)); } + /// Verifies eval property reads and writes dispatch through runtime hooks. + #[test] + fn execute_program_reads_and_writes_object_property() { + let program = + parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + let mut properties = HashMap::new(); + properties.insert("x".to_string(), x); + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!( + values + .property_get(object, "x") + .map(|value| values.get(value)) + .expect("property should be readable"), + FakeValue::Int(2) + ); + } + /// Verifies if/else executes only the PHP-truthy branch. #[test] fn execute_program_if_else_uses_php_truthiness() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 96000a06aa..f83aef5a0b 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -40,6 +40,7 @@ enum TokenKind { String(String), Plus, Minus, + Arrow, Star, Dot, Equal, @@ -103,7 +104,12 @@ impl<'a> Lexer<'a> { } '-' => { self.bump_char(); - Ok(TokenKind::Minus) + if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::Arrow) + } else { + Ok(TokenKind::Minus) + } } '*' => { self.bump_char(); @@ -355,6 +361,9 @@ impl Parser { } TokenKind::Ident(name) if name == "unset" => self.parse_unset_stmt(), TokenKind::Ident(name) if name == "while" => self.parse_while_stmt(), + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(true) + } TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { self.parse_array_set_stmt(name.clone()) } @@ -493,6 +502,9 @@ impl Parser { TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { self.parse_array_set_clause(name.clone()) } + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(false) + } TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::Equal) => { let name = name.clone(); self.advance(); @@ -518,6 +530,32 @@ impl Parser { Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) } + /// Parses `$object->property` as either an expression statement or property write. + fn parse_property_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let target = self.parse_expr()?; + if !self.consume(TokenKind::Equal) { + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::Expr(target)]); + } + let EvalExpr::PropertyGet { object, property } = target else { + return Err(EvalParseError::UnexpectedToken); + }; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::PropertySet { + object: *object, + property, + value, + }]) + } + /// Parses a complete `if` statement after consuming the `if` keyword. fn parse_if_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -703,13 +741,29 @@ impl Parser { /// Parses postfix array reads after a primary expression. fn parse_postfix(&mut self) -> Result { let mut expr = self.parse_primary()?; - while self.consume(TokenKind::LBracket) { - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - expr = EvalExpr::ArrayGet { - array: Box::new(expr), - index: Box::new(index), - }; + loop { + if self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + continue; + } + if self.consume(TokenKind::Arrow) { + let TokenKind::Ident(property) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let property = property.clone(); + self.advance(); + expr = EvalExpr::PropertyGet { + object: Box::new(expr), + property, + }; + continue; + } + break; } Ok(expr) } @@ -1136,6 +1190,41 @@ mod tests { ); } + /// Verifies object property reads parse as postfix EvalIR expressions. + #[test] + fn parse_fragment_accepts_property_read_source() { + let program = parse_fragment(br#"return $this->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + ); + } + + /// Verifies object property writes parse as dedicated EvalIR statements. + #[test] + fn parse_fragment_accepts_property_write_source() { + let program = + parse_fragment(br#"$this->x = $this->x + 1;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }] + ); + } + /// Verifies while fragments lower to loop statements with a nested block. #[test] fn parse_fragment_accepts_while_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index d6fd888560..b3b56733e9 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -33,6 +33,17 @@ unsafe extern "C" { index: *mut RuntimeCell, value: *mut RuntimeCell, ) -> *mut RuntimeCell; + fn __elephc_eval_value_property_get( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + ) -> *mut RuntimeCell; + fn __elephc_eval_value_property_set( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + value: *mut RuntimeCell, + ) -> u64; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_null() -> *mut RuntimeCell; @@ -119,6 +130,43 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Reads a boxed Mixed object property through the generated user helper. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_property_get( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + ) + }) + } + + /// Writes a boxed Mixed object property through the generated user helper. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let ok = unsafe { + __elephc_eval_value_property_set( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + value.as_ptr(), + ) + }; + if ok == 0 { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } + } + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b111f1885a..54528f4c77 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening ``, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index c6219143ca..d6a9c51d90 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -1,6 +1,14 @@ value = $this->value + 1;'); + } +} + $x = 1; $profile = ["name" => "Ada"]; $result = eval('$x = $x + 2; $created = "dynamic"; return $x + 4;'); @@ -23,6 +31,9 @@ function compiled_add($left, $right) { return $left + $right; } echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; +$counter = new EvalCounter(); +$counter->bump(); +echo "eval-this-property=" . $counter->value . "\n"; echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs new file mode 100644 index 0000000000..9ee62d7987 --- /dev/null +++ b/src/codegen/eval_property_helpers.rs @@ -0,0 +1,650 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-eval access properties on +//! native objects using the current module's class metadata. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - The cacheable runtime object cannot know user class ids or property +//! offsets, so these C-ABI symbols are emitted into the user assembly. +//! - The first slice supports public declared properties with scalar/Mixed +//! storage, which covers `$this->prop` inside eval-called native methods. + +use std::collections::BTreeMap; + +use crate::codegen::abi; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen::runtime_value_tag; +use crate::ir::{Function, LocalKind, Module}; +use crate::parser::ast::Visibility; +use crate::types::{ClassInfo, PhpType}; + +/// Property slot metadata needed by eval property bridge dispatch. +#[derive(Clone)] +struct EvalPropertySlot { + class_id: u64, + class_name: String, + property: String, + offset: usize, + ty: PhpType, +} + +/// Emits eval property helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_property_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_property_slots(module); + emit_property_get_helper(module, emitter, data, &slots); + emit_property_set_helper(module, emitter, data, &slots); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function + .locals + .iter() + .any(|local| matches!(local.kind, LocalKind::EvalContext | LocalKind::EvalScope)) +} + +/// Collects public declared properties with storage layouts the bridge can access. +fn collect_eval_property_slots(module: &Module) -> Vec { + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_property_slots(class_name, class_info, &mut slots); + } + slots +} + +/// Adds bridge-supported public properties for one class. +fn collect_class_property_slots( + class_name: &str, + class_info: &ClassInfo, + slots: &mut Vec, +) { + for (property, ty) in &class_info.properties { + if !property_is_public(class_info, property) || !property_type_supported(ty) { + continue; + } + let Some(offset) = class_info.property_offsets.get(property).copied() else { + continue; + }; + slots.push(EvalPropertySlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + property: property.clone(), + offset, + ty: ty.codegen_repr(), + }); + } +} + +/// Returns true when the property is visible to PHP eval from method scope. +fn property_is_public(class_info: &ClassInfo, property: &str) -> bool { + class_info + .property_visibilities + .get(property) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + +/// Returns true for property storage shapes the bridge can box and update. +fn property_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Union(_) + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } + ) +} + +/// Emits `__elephc_eval_value_property_get(Mixed*, name, len) -> Mixed*`. +fn emit_property_get_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user property get ---"); + label_c_global(module, emitter, "__elephc_eval_value_property_get"); + match module.target.arch { + Arch::AArch64 => emit_property_get_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_property_get_x86_64(module, emitter, data, slots), + } +} + +/// Emits `__elephc_eval_value_property_set(Mixed*, name, len, Mixed*) -> bool`. +fn emit_property_set_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user property set ---"); + label_c_global(module, emitter, "__elephc_eval_value_property_set"); + match module.target.arch { + Arch::AArch64 => emit_property_set_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_property_set_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 property-get helper body. +fn emit_property_get_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let null_label = "__elephc_eval_value_property_get_null"; + let done_label = "__elephc_eval_value_property_get_done"; + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for saved name/object and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction(&format!("cbz x0, {}", null_label)); // null Mixed receiver reads as PHP null + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", null_label)); // non-object receivers read as PHP null + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property loads + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emit_aarch64_property_dispatch(module, emitter, data, slots, "get"); + emitter.instruction(&format!("b {}", null_label)); // no declared public property matched the request + emit_aarch64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(null_label); + let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed property value to Rust +} + +/// Emits the x86_64 property-get helper body. +fn emit_property_get_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let null_label = "__elephc_eval_value_property_get_null_x"; + let done_label = "__elephc_eval_value_property_get_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for name, length, and object + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction(&format!("test rdi, rdi")); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", null_label)); // null Mixed receiver reads as PHP null + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", null_label)); // non-object receivers read as PHP null + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property loads + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emit_x86_64_property_dispatch(module, emitter, data, slots, "get"); + emitter.instruction(&format!("jmp {}", null_label)); // no declared public property matched the request + emit_x86_64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(null_label); + let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed property value to Rust +} + +/// Emits the ARM64 property-set helper body. +fn emit_property_set_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let fail_label = "__elephc_eval_value_property_set_fail"; + let done_label = "__elephc_eval_value_property_set_done"; + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for inputs, object, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed value being assigned + emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot accept a property write + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers reject the property write + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property stores + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emit_aarch64_property_dispatch(module, emitter, data, slots, "set"); + emitter.instruction(&format!("b {}", fail_label)); // no declared public property matched the request + emit_aarch64_set_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("mov x0, #0"); // report a failed eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the write-status flag to Rust +} + +/// Emits the x86_64 property-set helper body. +fn emit_property_set_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let fail_label = "__elephc_eval_value_property_set_fail_x"; + let done_label = "__elephc_eval_value_property_set_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for name, length, object, and value + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed value being assigned + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot accept a property write + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers reject the property write + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property stores + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emit_x86_64_property_dispatch(module, emitter, data, slots, "set"); + emitter.instruction(&format!("jmp {}", fail_label)); // no declared public property matched the request + emit_x86_64_set_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report a failed eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the write-status flag to Rust +} + +/// Emits ARM64 class-id and property-name dispatch for helper slot bodies. +fn emit_aarch64_property_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], + mode: &str, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let class_label = format!("__elephc_eval_property_{}_class_{}", mode, class_id); + let next_label = format!("__elephc_eval_property_{}_next_{}", mode, class_id); + abi::emit_load_int_immediate(emitter, "x10", class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next class when ids differ + for slot in class_slots { + emit_aarch64_property_name_compare(module, emitter, data, slot, mode); + } + emitter.label(&class_label); + emitter.instruction(&format!("b {}", next_label)); // fall through to the next class after a name miss + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-id and property-name dispatch for helper slot bodies. +fn emit_x86_64_property_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], + mode: &str, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let class_label = format!("__elephc_eval_property_{}_class_{}_x", mode, class_id); + let next_label = format!("__elephc_eval_property_{}_next_{}_x", mode, class_id); + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("jne {}", next_label)); // try the next class when ids differ + for slot in class_slots { + emit_x86_64_property_name_compare(module, emitter, data, slot, mode); + } + emitter.label(&class_label); + emitter.instruction(&format!("jmp {}", next_label)); // fall through to the next class after a name miss + emitter.label(&next_label); + } +} + +/// Emits one ARM64 property-name comparison and branch to the matching body. +fn emit_aarch64_property_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + mode: &str, +) { + let (label, len) = data.add_string(slot.property.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare requested property name with this declared property + emitter.instruction(&format!( + "cbnz x0, {}", + slot_body_label(module, slot, mode) + )); // dispatch to the property body when the names match +} + +/// Emits one x86_64 property-name comparison and branch to the matching body. +fn emit_x86_64_property_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + mode: &str, +) { + let (label, len) = data.add_string(slot.property.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested property-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare requested property name with this declared property + emitter.instruction("test rax, rax"); // check whether the property names matched + emitter.instruction(&format!("jne {}", slot_body_label(module, slot, mode))); // dispatch to the property body when the names match +} + +/// Emits ARM64 property-get bodies for every bridge-supported property slot. +fn emit_aarch64_get_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "get")); + emit_aarch64_box_property_slot(emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the declared property value + } +} + +/// Emits x86_64 property-get bodies for every bridge-supported property slot. +fn emit_x86_64_get_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "get")); + emit_x86_64_box_property_slot(emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the declared property value + } +} + +/// Emits ARM64 property-set bodies for every bridge-supported property slot. +fn emit_aarch64_set_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "set")); + emit_aarch64_store_property_slot(emitter, slot); + emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // return after storing the declared property value + } +} + +/// Emits x86_64 property-set bodies for every bridge-supported property slot. +fn emit_x86_64_set_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "set")); + emit_x86_64_store_property_slot(emitter, slot); + emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // return after storing the declared property value + } +} + +/// Boxes a property value loaded from an ARM64 object slot into a Mixed cell. +fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer + match slot.ty.codegen_repr() { + PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset)); // load the property payload low word + emitter.instruction("mov x2, xzr"); // heap/scalar property payloads do not use a high word here + abi::emit_load_int_immediate(emitter, "x0", runtime_value_tag(&slot.ty) as i64); + emitter.instruction("bl __rt_mixed_from_value"); // box the property payload as a Mixed cell + } + PhpType::Float => { + emitter.instruction(&format!("ldr d0, [x9, #{}]", slot.offset)); // load the floating property payload + emitter.instruction("fmov x1, d0"); // move float bits into the Mixed low payload word + emitter.instruction("mov x2, xzr"); // float payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = float + emitter.instruction("bl __rt_mixed_from_value"); // box the floating property payload as Mixed + } + PhpType::Str => { + emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset)); // load the string property pointer + emitter.instruction(&format!("ldr x2, [x9, #{}]", slot.offset + 8)); // load the string property length + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the string property payload + } + PhpType::Mixed | PhpType::Union(_) => { + let null_label = format!("{}_mixed_null", label_fragment(&slot_body_label_raw(slot, "get"))); + let done_label = format!("{}_mixed_done", label_fragment(&slot_body_label_raw(slot, "get"))); + emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the stored Mixed property cell + emitter.instruction(&format!("cbz x0, {}", null_label)); // null property storage reads as PHP null + emitter.instruction("bl __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("b {}", done_label)); // skip null materialization after a retained hit + emitter.label(&null_label); + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(&done_label); + } + _ => { + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } + } +} + +/// Boxes a property value loaded from an x86_64 object slot into a Mixed cell. +fn emit_x86_64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer + match slot.ty.codegen_repr() { + PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + emitter.instruction(&format!("mov rdi, QWORD PTR [r11 + {}]", slot.offset)); // load the property payload low word + emitter.instruction("xor esi, esi"); // heap/scalar property payloads do not use a high word here + abi::emit_load_int_immediate(emitter, "rax", runtime_value_tag(&slot.ty) as i64); + emitter.instruction("call __rt_mixed_from_value"); // box the property payload as a Mixed cell + } + PhpType::Float => { + emitter.instruction(&format!("movsd xmm0, QWORD PTR [r11 + {}]", slot.offset)); // load the floating property payload + emitter.instruction("movq rdi, xmm0"); // move float bits into the Mixed low payload word + emitter.instruction("xor esi, esi"); // float payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = float + emitter.instruction("call __rt_mixed_from_value"); // box the floating property payload as Mixed + } + PhpType::Str => { + emitter.instruction(&format!("mov rdi, QWORD PTR [r11 + {}]", slot.offset)); // load the string property pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [r11 + {}]", slot.offset + 8)); // load the string property length + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the string property payload + } + PhpType::Mixed | PhpType::Union(_) => { + let null_label = format!("{}_mixed_null_x", label_fragment(&slot_body_label_raw(slot, "get"))); + let done_label = format!("{}_mixed_done_x", label_fragment(&slot_body_label_raw(slot, "get"))); + emitter.instruction(&format!("mov rax, QWORD PTR [r11 + {}]", slot.offset)); // load the stored Mixed property cell + emitter.instruction("test rax, rax"); // check whether the property storage is initialized + emitter.instruction(&format!("jz {}", null_label)); // null property storage reads as PHP null + emitter.instruction("call __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("jmp {}", done_label)); // skip null materialization after a retained hit + emitter.label(&null_label); + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(&done_label); + } + _ => { + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } + } +} + +/// Stores a boxed Mixed eval value into an ARM64 object property slot. +fn emit_aarch64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { + match slot.ty.codegen_repr() { + PhpType::Int => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "x0"), + PhpType::Bool => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "x0"), + PhpType::Float => { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval value to a PHP float + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str d0, [x9, #{}]", slot.offset)); // store the coerced float into the property slot + } + PhpType::Str => { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for string coercion + emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval value to a PHP string pair + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str x1, [x9, #{}]", slot.offset)); // store the coerced string pointer into the property slot + emitter.instruction(&format!("str x2, [x9, #{}]", slot.offset + 8)); // store the coerced string length into the property slot + } + PhpType::Mixed | PhpType::Union(_) => { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value being assigned + emitter.instruction("bl __rt_incref"); // retain the Mixed cell for property ownership + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the retained Mixed cell into the property slot + emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the unused property high word + } + _ => {} + } +} + +/// Stores a boxed Mixed eval value into an x86_64 object property slot. +fn emit_x86_64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { + match slot.ty.codegen_repr() { + PhpType::Int => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "rax"), + PhpType::Bool => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "rax"), + PhpType::Float => { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for float coercion + emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval value to a PHP float + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("movsd QWORD PTR [r11 + {}], xmm0", slot.offset)); // store the coerced float into the property slot + } + PhpType::Str => { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for string coercion + emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval value to a PHP string pair + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); // store the coerced string pointer into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rdx", slot.offset + 8)); // store the coerced string length into the property slot + } + PhpType::Mixed | PhpType::Union(_) => { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value being assigned + emitter.instruction("call __rt_incref"); // retain the Mixed cell for property ownership + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); // store the retained Mixed cell into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); // clear the unused property high word + } + _ => {} + } +} + +/// Emits an ARM64 scalar property store after Mixed coercion. +fn emit_aarch64_store_cast_scalar( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + helper: &str, + result_reg: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for scalar coercion + emitter.instruction(&format!("bl {}", helper)); // coerce the eval value to the declared property type + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str {}, [x9, #{}]", result_reg, slot.offset)); // store the coerced scalar into the property slot +} + +/// Emits an x86_64 scalar property store after Mixed coercion. +fn emit_x86_64_store_cast_scalar( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + helper: &str, + result_reg: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for scalar coercion + emitter.instruction(&format!("call {}", helper)); // coerce the eval value to the declared property type + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], {}", slot.offset, result_reg)); // store the coerced scalar into the property slot +} + +/// Groups property slots by class id while preserving sorted class order. +fn grouped_slots(slots: &[EvalPropertySlot]) -> BTreeMap> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped.entry(slot.class_id).or_insert_with(Vec::new).push(slot); + } + grouped +} + +/// Returns a platform-safe body label for a get/set property slot. +fn slot_body_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!("{}{}", slot_body_label_raw(slot, mode), suffix) +} + +/// Returns the architecture-independent body label stem for a property slot. +fn slot_body_label_raw(slot: &EvalPropertySlot, mode: &str) -> String { + format!( + "__elephc_eval_property_{}_{}_{}", + mode, + label_fragment(&slot.class_name), + label_fragment(&slot.property) + ) +} + +/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index acbc2e4c3b..f067607e4b 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -387,6 +387,7 @@ fn eval_sync_type_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Object(_) | PhpType::Mixed | PhpType::Union(_) ) @@ -531,6 +532,15 @@ fn store_mixed_scope_cell_to_local( abi::store_at_offset(ctx.emitter, ptr_reg, offset); abi::store_at_offset(ctx.emitter, len_reg, offset - 8); } + PhpType::Object(_) => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + let offset = ctx.local_offset(local.slot)?; + let object_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x1", + Arch::X86_64 => "rdi", + }; + abi::store_at_offset(ctx.emitter, object_reg, offset); + } other => { return Err(CodegenIrError::unsupported(format!( "eval scope reload for PHP type {:?}", @@ -590,6 +600,10 @@ fn store_missing_scope_entry_to_local( abi::store_at_offset(ctx.emitter, ptr_reg, offset); abi::store_at_offset(ctx.emitter, len_reg, offset - 8); } + PhpType::Object(_) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + } other => { return Err(CodegenIrError::unsupported(format!( "eval scope missing reload for PHP type {:?}", diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index c91bbeb39a..df56fa06a8 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -11,6 +11,7 @@ mod block_emit; pub(crate) mod context; +mod eval_property_helpers; mod fibers; mod frame; mod function_variants; @@ -187,10 +188,11 @@ pub fn generate_user_asm_from_ir_with_options( fn finalize_user_asm( module: &Module, mut emitter: Emitter, - data: DataSection, + mut data: DataSection, emit: Emit, exported_functions: &HashMap, ) -> String { + eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); let data_output = data.emit(); let empty_globals = HashSet::::new(); let empty_static_vars = HashMap::<(String, String), PhpType>::new(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3d6a1a73ca..cb5ecb918a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -447,6 +447,27 @@ eval('echo native_eval_add(4, 6); echo ":"; echo function_exists("native_eval_ad assert_eq!(out, "10:1"); } +/// Verifies eval fragments called from methods can mutate public properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_public_property() { + let out = compile_and_run( + r#"x = $this->x + 1;'); + } +} + +$box = new EvalPropBox(); +$box->bump(); +echo $box->x; +"#, + ); + assert_eq!(out, "2"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From a3e9db042748c39d6d588d4e3c3b6cee714b8c07 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 18:01:56 +0200 Subject: [PATCH 0018/1208] Support eval calls to native zero-arg methods --- crates/elephc-eval/src/eval_ir.rs | 5 + crates/elephc-eval/src/interpreter.rs | 53 ++++ crates/elephc-eval/src/parser.rs | 49 ++- crates/elephc-eval/src/runtime_hooks.rs | 35 +++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 9 + src/codegen/eval_method_helpers.rs | 392 ++++++++++++++++++++++++ src/codegen/eval_property_helpers.rs | 6 +- src/codegen/mod.rs | 2 + tests/codegen/eval.rs | 24 ++ 10 files changed, 562 insertions(+), 15 deletions(-) create mode 100644 src/codegen/eval_method_helpers.rs diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 5d35eb07a8..a09139e65a 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -133,6 +133,11 @@ pub enum EvalExpr { }, Const(EvalConst), LoadVar(String), + MethodCall { + object: Box, + method: String, + args: Vec, + }, PropertyGet { object: Box, property: String, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 25f59b8d66..d28f34d3ec 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -66,6 +66,14 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result<(), EvalStatus>; + /// Calls a named method on a runtime object held in a boxed Mixed cell. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result; + /// Returns the visible element count for an array-like runtime cell. fn array_len(&mut self, array: RuntimeCellHandle) -> Result; @@ -403,6 +411,18 @@ fn eval_expr( EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::LoadVar(name) => scope.visible_cell(name).map_or_else(|| values.null(), Ok), + EvalExpr::MethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + values.method_call(object, method, evaluated_args) + } EvalExpr::PropertyGet { object, property } => { let object = eval_expr(object, context, scope, values)?; values.property_get(object, property) @@ -822,6 +842,25 @@ mod tests { Ok(()) } + /// Calls one fake object method by name. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + if !args.is_empty() { + return Err(EvalStatus::UnsupportedConstruct); + } + match (self.get(object), method) { + (FakeValue::Object(_), "answer") => self.int(42), + (FakeValue::Object(properties), "read_x") => { + properties.get("x").copied().map_or_else(|| self.null(), Ok) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns the visible element count for fake array values. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { match self.get(array) { @@ -1118,6 +1157,20 @@ mod tests { ); } + /// Verifies eval method calls dispatch through the runtime method hook. + #[test] + fn execute_program_calls_object_method() { + let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(HashMap::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + } + /// Verifies if/else executes only the PHP-truthy branch. #[test] fn execute_program_if_else_uses_php_truthiness() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index f83aef5a0b..b0b2ca2c6c 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -738,7 +738,7 @@ impl Parser { Ok(expr) } - /// Parses postfix array reads after a primary expression. + /// Parses postfix array reads, property reads, and method calls after a primary expression. fn parse_postfix(&mut self) -> Result { let mut expr = self.parse_primary()?; loop { @@ -752,15 +752,24 @@ impl Parser { continue; } if self.consume(TokenKind::Arrow) { - let TokenKind::Ident(property) = self.current() else { + let TokenKind::Ident(member) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; - let property = property.clone(); + let member = member.clone(); self.advance(); - expr = EvalExpr::PropertyGet { - object: Box::new(expr), - property, - }; + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + expr = EvalExpr::MethodCall { + object: Box::new(expr), + method: member, + args, + }; + } else { + expr = EvalExpr::PropertyGet { + object: Box::new(expr), + property: member, + }; + } continue; } break; @@ -826,10 +835,16 @@ impl Parser { /// Parses a function-like call expression and its source-order arguments. fn parse_call_expr(&mut self, name: String) -> Result { self.advance(); + let args = self.parse_call_args()?; + Ok(EvalExpr::Call { name, args }) + } + + /// Parses a parenthesized source-order argument list. + fn parse_call_args(&mut self) -> Result, EvalParseError> { self.expect(TokenKind::LParen)?; let mut args = Vec::new(); if self.consume(TokenKind::RParen) { - return Ok(EvalExpr::Call { name, args }); + return Ok(args); } loop { args.push(self.parse_expr()?); @@ -837,11 +852,11 @@ impl Parser { break; } if self.consume(TokenKind::RParen) { - return Ok(EvalExpr::Call { name, args }); + return Ok(args); } } self.expect(TokenKind::RParen)?; - Ok(EvalExpr::Call { name, args }) + Ok(args) } /// Parses an array literal with source-order optional key/value element expressions. @@ -1203,6 +1218,20 @@ mod tests { ); } + /// Verifies object method calls parse as postfix EvalIR call expressions. + #[test] + fn parse_fragment_accepts_method_call_source() { + let program = parse_fragment(br#"return $this->answer();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "answer".to_string(), + args: Vec::new(), + }))] + ); + } + /// Verifies object property writes parse as dedicated EvalIR statements. #[test] fn parse_fragment_accepts_property_write_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index b3b56733e9..712cfb07f6 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -44,6 +44,12 @@ unsafe extern "C" { name_len: u64, value: *mut RuntimeCell, ) -> u64; + fn __elephc_eval_value_method_call( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + args: *mut RuntimeCell, + ) -> *mut RuntimeCell; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_null() -> *mut RuntimeCell; @@ -167,6 +173,35 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } + /// Calls a boxed Mixed object method through the generated user helper. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + let arg_array = unsafe { __elephc_eval_value_array_new(args.len() as u64) }; + let arg_array = Self::handle(arg_array)?; + for (index, value) in args.into_iter().enumerate() { + let index = Self::handle(unsafe { __elephc_eval_value_int(index as i64) })?; + Self::handle(unsafe { + __elephc_eval_value_array_set(arg_array.as_ptr(), index.as_ptr(), value.as_ptr()) + })?; + } + let result = Self::handle(unsafe { + __elephc_eval_value_method_call( + object.as_ptr(), + method.as_ptr(), + method.len() as u64, + arg_array.as_ptr(), + ) + }); + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + result + } + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 54528f4c77..416fa1e702 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-argument method calls through `$this->method()`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index d6a9c51d90..f8df0c2b15 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -7,6 +7,14 @@ class EvalCounter { public function bump(): void { eval('$this->value = $this->value + 1;'); } + + public function read(): int { + return $this->value; + } + + public function echoReadThroughEval(): void { + echo "eval-this-method=" . eval('return $this->read();') . "\n"; + } } $x = 1; @@ -34,6 +42,7 @@ public function bump(): void { $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; +$counter->echoReadThroughEval(); echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs new file mode 100644 index 0000000000..e9e70cbbe1 --- /dev/null +++ b/src/codegen/eval_method_helpers.rs @@ -0,0 +1,392 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-eval call public native +//! instance methods on runtime objects known to the current module. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - The cacheable runtime object cannot know user class ids, method symbols, +//! or return types, so this bridge is emitted into the user assembly. +//! - This first method-call slice supports public zero-argument AOT methods and +//! reports unsupported calls as runtime failure to the eval bridge. + +use std::collections::BTreeMap; + +use crate::codegen::abi; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen::emit_box_current_value_as_mixed; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::method_symbol; +use crate::parser::ast::Visibility; +use crate::types::{ClassInfo, PhpType}; + +/// Method metadata needed by eval method-call bridge dispatch. +#[derive(Clone)] +struct EvalMethodSlot { + class_id: u64, + class_name: String, + method: String, + impl_class: String, + return_ty: PhpType, +} + +/// Emits eval method-call helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_method_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_method_slots(module); + emit_method_call_helper(module, emitter, data, &slots); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function + .locals + .iter() + .any(|local| matches!(local.kind, LocalKind::EvalContext | LocalKind::EvalScope)) +} + +/// Collects public zero-argument instance methods backed by emitted EIR symbols. +fn collect_eval_method_slots(module: &Module) -> Vec { + let emitted_methods = super::eir_class_method_keys(module); + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_method_slots(class_name, class_info, &emitted_methods, &mut slots); + } + slots +} + +/// Adds bridge-supported public methods for one class. +fn collect_class_method_slots( + class_name: &str, + class_info: &ClassInfo, + emitted_methods: &std::collections::HashSet<(String, String, bool)>, + slots: &mut Vec, +) { + let mut methods = class_info.methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method, sig) in methods { + if !method_is_public(class_info, method) + || !method_signature_supported(sig) + || !method_return_supported(&sig.return_type) + { + continue; + } + let impl_class = class_info + .method_impl_classes + .get(method) + .map(String::as_str) + .unwrap_or(class_name); + if !emitted_methods.contains(&(impl_class.to_string(), method.clone(), false)) { + continue; + } + slots.push(EvalMethodSlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + method: method.clone(), + impl_class: impl_class.to_string(), + return_ty: sig.return_type.codegen_repr(), + }); + } +} + +/// Returns true when a method is publicly visible to runtime eval. +fn method_is_public(class_info: &ClassInfo, method: &str) -> bool { + class_info + .method_visibilities + .get(method) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + +/// Returns true for method signatures supported by this first eval bridge slice. +fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { + sig.params.is_empty() && sig.variadic.is_none() +} + +/// Returns true for return storage shapes the bridge can box for eval. +fn method_return_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Void + | PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Union(_) + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } + ) +} + +/// Emits `__elephc_eval_value_method_call(Mixed*, name, len, MixedArray*) -> Mixed*`. +fn emit_method_call_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user method call ---"); + label_c_global(module, emitter, "__elephc_eval_value_method_call"); + match module.target.arch { + Arch::AArch64 => emit_method_call_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_method_call_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 method-call helper body. +fn emit_method_call_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], +) { + let fail_label = "__elephc_eval_value_method_call_fail"; + let done_label = "__elephc_eval_value_method_call_done"; + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for inputs, object, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested method-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested method-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed eval argument array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot dispatch a method + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers cannot dispatch instance methods + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for method calls + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction(&format!("cbnz x0, {}", fail_label)); // this bridge slice only supports zero-argument methods + emit_aarch64_method_dispatch(module, emitter, data, slots); + emitter.instruction(&format!("b {}", fail_label)); // no public zero-argument method matched the request + emit_aarch64_method_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed method result to Rust +} + +/// Emits the x86_64 method-call helper body. +fn emit_method_call_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], +) { + let fail_label = "__elephc_eval_value_method_call_fail_x"; + let done_label = "__elephc_eval_value_method_call_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for name, length, object, and args + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested method-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested method-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed eval argument array + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot dispatch a method + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers cannot dispatch instance methods + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for method calls + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("test rax, rax"); // check whether eval supplied any method arguments + emitter.instruction(&format!("jnz {}", fail_label)); // this bridge slice only supports zero-argument methods + emit_x86_64_method_dispatch(module, emitter, data, slots); + emitter.instruction(&format!("jmp {}", fail_label)); // no public zero-argument method matched the request + emit_x86_64_method_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed method result to Rust +} + +/// Emits ARM64 class-id and method-name dispatch for helper method bodies. +fn emit_aarch64_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], +) { + for (class_id, class_slots) in grouped_slots(slots) { + let next_label = format!("__elephc_eval_method_next_{}", class_id); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer before this class test + emitter.instruction("ldr x9, [x9]"); // load the receiver class id for method dispatch + abi::emit_load_int_immediate(emitter, "x10", class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next class when ids differ + for slot in class_slots { + emit_aarch64_method_name_compare(module, emitter, data, slot); + } + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-id and method-name dispatch for helper method bodies. +fn emit_x86_64_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalMethodSlot], +) { + for (class_id, class_slots) in grouped_slots(slots) { + let next_label = format!("__elephc_eval_method_next_{}_x", class_id); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer before this class test + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the receiver class id for method dispatch + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("jne {}", next_label)); // try the next class when ids differ + for slot in class_slots { + emit_x86_64_method_name_compare(module, emitter, data, slot); + } + emitter.label(&next_label); + } +} + +/// Emits one ARM64 method-name comparison and branch to the matching body. +fn emit_aarch64_method_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalMethodSlot, +) { + let (label, len) = data.add_string(slot.method.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested method-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested method-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare requested method name with this public method + emitter.instruction(&format!("cbnz x0, {}", method_body_label(module, slot))); // dispatch to the method body when the names match +} + +/// Emits one x86_64 method-name comparison and branch to the matching body. +fn emit_x86_64_method_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalMethodSlot, +) { + let (label, len) = data.add_string(slot.method.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested method-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare requested method name with this public method + emitter.instruction("test rax, rax"); // check whether the method names matched + emitter.instruction(&format!("jne {}", method_body_label(module, slot))); // dispatch to the method body when the names match +} + +/// Emits ARM64 method-call bodies for every bridge-supported method. +fn emit_aarch64_method_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalMethodSlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&method_body_label(module, slot)); + emitter.instruction("ldr x0, [sp, #16]"); // pass the unboxed receiver as the method's `$this` argument + abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); + emit_box_method_result(module, emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result + } +} + +/// Emits x86_64 method-call bodies for every bridge-supported method. +fn emit_x86_64_method_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalMethodSlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&method_body_label(module, slot)); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the unboxed receiver as the method's `$this` argument + abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); + emit_box_method_result(module, emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result + } +} + +/// Boxes the current native method result as the Mixed cell expected by eval. +fn emit_box_method_result(module: &Module, emitter: &mut Emitter, slot: &EvalMethodSlot) { + if slot.return_ty.codegen_repr() == PhpType::Void { + let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } else { + emit_box_current_value_as_mixed(emitter, &slot.return_ty); + } +} + +/// Groups method slots by class id while preserving sorted class order. +fn grouped_slots(slots: &[EvalMethodSlot]) -> BTreeMap> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped.entry(slot.class_id).or_insert_with(Vec::new).push(slot); + } + grouped +} + +/// Returns a platform-safe body label for a method slot. +fn method_body_label(module: &Module, slot: &EvalMethodSlot) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!( + "__elephc_eval_method_{}_{}{}", + label_fragment(&slot.class_name), + label_fragment(&slot.method), + suffix + ) +} + +/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index 9ee62d7987..f12c372f0b 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -356,10 +356,8 @@ fn emit_aarch64_property_name_compare( abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); emitter.instruction("bl __rt_str_eq"); // compare requested property name with this declared property - emitter.instruction(&format!( - "cbnz x0, {}", - slot_body_label(module, slot, mode) - )); // dispatch to the property body when the names match + let target_label = slot_body_label(module, slot, mode); + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the property body when the names match } /// Emits one x86_64 property-name comparison and branch to the matching body. diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index df56fa06a8..20ab3ef79b 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -11,6 +11,7 @@ mod block_emit; pub(crate) mod context; +mod eval_method_helpers; mod eval_property_helpers; mod fibers; mod frame; @@ -193,6 +194,7 @@ fn finalize_user_asm( exported_functions: &HashMap, ) -> String { eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); + eval_method_helpers::emit_eval_method_helpers(module, &mut emitter, &mut data); let data_output = data.emit(); let empty_globals = HashSet::::new(); let empty_static_vars = HashMap::<(String, String), PhpType>::new(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cb5ecb918a..9053f6ba6f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -468,6 +468,30 @@ echo $box->x; assert_eq!(out, "2"); } +/// Verifies eval fragments can call public zero-argument AOT methods through `$this`. +#[test] +fn test_eval_fragment_can_call_this_public_zero_arg_method() { + let out = compile_and_run( + r#"x + 1; + } + + public function run(): void { + echo eval('return $this->answer();'); + } +} + +$box = new EvalMethodBox(); +$box->run(); +"#, + ); + assert_eq!(out, "42"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From 480827ec82be3140fbfab2cf218aa30055e42216 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 18:12:30 +0200 Subject: [PATCH 0019/1208] Support eval method calls with one scalar arg --- crates/elephc-eval/src/interpreter.rs | 38 +++++- crates/elephc-eval/src/parser.rs | 19 +++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 9 ++ src/codegen/eval_method_helpers.rs | 188 +++++++++++++++++++++++--- tests/codegen/eval.rs | 24 ++++ 6 files changed, 255 insertions(+), 25 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d28f34d3ec..ea9e3301e8 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -849,14 +849,27 @@ mod tests { method: &str, args: Vec, ) -> Result { - if !args.is_empty() { - return Err(EvalStatus::UnsupportedConstruct); - } match (self.get(object), method) { - (FakeValue::Object(_), "answer") => self.int(42), + (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), (FakeValue::Object(properties), "read_x") => { + if !args.is_empty() { + return Err(EvalStatus::UnsupportedConstruct); + } properties.get("x").copied().map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "add_x") => { + let [arg] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = properties.get("x").copied().ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(arg) = self.get(*arg) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + arg) + } _ => Err(EvalStatus::UnsupportedConstruct), } } @@ -1171,6 +1184,23 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(42)); } + /// Verifies eval method calls forward evaluated arguments to the runtime hook. + #[test] + fn execute_program_calls_object_method_with_argument() { + let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let mut properties = HashMap::new(); + properties.insert("x".to_string(), x); + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + /// Verifies if/else executes only the PHP-truthy branch. #[test] fn execute_program_if_else_uses_php_truthiness() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index b0b2ca2c6c..eb6bfe6216 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1232,6 +1232,25 @@ mod tests { ); } + /// Verifies object method calls preserve source-order argument expressions. + #[test] + fn parse_fragment_accepts_method_call_args_source() { + let program = + parse_fragment(br#"return $this->add($x + 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "add".to_string(), + args: vec![EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }], + }))] + ); + } + /// Verifies object property writes parse as dedicated EvalIR statements. #[test] fn parse_fragment_accepts_property_write_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 416fa1e702..bbe79ef662 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-argument method calls through `$this->method()`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero- or one-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index f8df0c2b15..f2b0bb4d3c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -12,9 +12,17 @@ public function read(): int { return $this->value; } + public function add(int $amount): int { + return $this->value + $amount; + } + public function echoReadThroughEval(): void { echo "eval-this-method=" . eval('return $this->read();') . "\n"; } + + public function echoAddThroughEval(): void { + echo "eval-this-method-arg=" . eval('return $this->add(5);') . "\n"; + } } $x = 1; @@ -43,6 +51,7 @@ public function echoReadThroughEval(): void { $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; $counter->echoReadThroughEval(); +$counter->echoAddThroughEval(); echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index e9e70cbbe1..350f51e21a 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -8,8 +8,8 @@ //! Key details: //! - The cacheable runtime object cannot know user class ids, method symbols, //! or return types, so this bridge is emitted into the user assembly. -//! - This first method-call slice supports public zero-argument AOT methods and -//! reports unsupported calls as runtime failure to the eval bridge. +//! - This method-call slice supports public AOT methods with zero or one +//! non-by-ref scalar argument and reports unsupported calls as runtime failure. use std::collections::BTreeMap; @@ -30,6 +30,7 @@ struct EvalMethodSlot { class_name: String, method: String, impl_class: String, + params: Vec, return_ty: PhpType, } @@ -72,7 +73,7 @@ fn function_uses_eval(function: &Function) -> bool { .any(|local| matches!(local.kind, LocalKind::EvalContext | LocalKind::EvalScope)) } -/// Collects public zero-argument instance methods backed by emitted EIR symbols. +/// Collects public bridge-supported instance methods backed by emitted EIR symbols. fn collect_eval_method_slots(module: &Module) -> Vec { let emitted_methods = super::eir_class_method_keys(module); let mut slots = Vec::new(); @@ -113,6 +114,11 @@ fn collect_class_method_slots( class_name: class_name.to_string(), method: method.clone(), impl_class: impl_class.to_string(), + params: sig + .params + .iter() + .map(|(_, ty)| ty.codegen_repr()) + .collect(), return_ty: sig.return_type.codegen_repr(), }); } @@ -128,7 +134,21 @@ fn method_is_public(class_info: &ClassInfo, method: &str) -> bool { /// Returns true for method signatures supported by this first eval bridge slice. fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { - sig.params.is_empty() && sig.variadic.is_none() + sig.params.len() <= 1 + && sig.variadic.is_none() + && sig.ref_params.iter().all(|is_ref| !*is_ref) + && sig + .params + .iter() + .all(|(_, ty)| method_param_supported(ty)) +} + +/// Returns true for one eval-supplied method argument type supported by this bridge. +fn method_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str + ) } /// Returns true for return storage shapes the bridge can box for eval. @@ -184,13 +204,9 @@ fn emit_method_call_aarch64( emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers cannot dispatch instance methods emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for method calls - emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); - abi::emit_call_label(emitter, &array_len_symbol); - emitter.instruction(&format!("cbnz x0, {}", fail_label)); // this bridge slice only supports zero-argument methods emit_aarch64_method_dispatch(module, emitter, data, slots); - emitter.instruction(&format!("b {}", fail_label)); // no public zero-argument method matched the request - emit_aarch64_method_bodies(module, emitter, slots, done_label); + emitter.instruction(&format!("b {}", fail_label)); // no supported public method matched the request + emit_aarch64_method_bodies(module, emitter, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -210,7 +226,7 @@ fn emit_method_call_x86_64( let done_label = "__elephc_eval_value_method_call_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // reserve aligned slots for name, length, object, and args + emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, length, object, args, and first argument emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested method-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested method-name length emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed eval argument array @@ -221,14 +237,9 @@ fn emit_method_call_x86_64( emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers cannot dispatch instance methods emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for method calls - emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation - let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); - abi::emit_call_label(emitter, &array_len_symbol); - emitter.instruction("test rax, rax"); // check whether eval supplied any method arguments - emitter.instruction(&format!("jnz {}", fail_label)); // this bridge slice only supports zero-argument methods emit_x86_64_method_dispatch(module, emitter, data, slots); - emitter.instruction(&format!("jmp {}", fail_label)); // no public zero-argument method matched the request - emit_x86_64_method_bodies(module, emitter, slots, done_label); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported public method matched the request + emit_x86_64_method_bodies(module, emitter, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -318,10 +329,12 @@ fn emit_aarch64_method_bodies( emitter: &mut Emitter, slots: &[EvalMethodSlot], done_label: &str, + fail_label: &str, ) { for slot in slots { emitter.label(&method_body_label(module, slot)); - emitter.instruction("ldr x0, [sp, #16]"); // pass the unboxed receiver as the method's `$this` argument + emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); + emit_aarch64_prepare_method_args(module, emitter, slot); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); emit_box_method_result(module, emitter, slot); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result @@ -334,16 +347,151 @@ fn emit_x86_64_method_bodies( emitter: &mut Emitter, slots: &[EvalMethodSlot], done_label: &str, + fail_label: &str, ) { for slot in slots { emitter.label(&method_body_label(module, slot)); - emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the unboxed receiver as the method's `$this` argument + emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); + emit_x86_64_prepare_method_args(module, emitter, slot); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); emit_box_method_result(module, emitter, slot); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result } } +/// Emits ARM64 arity validation for one method body. +fn emit_aarch64_validate_method_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalMethodSlot, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); + emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the method signature + emitter.instruction(&format!("b.ne {}", fail_label)); // reject method dispatch when arity differs +} + +/// Emits x86_64 arity validation for one method body. +fn emit_x86_64_validate_method_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalMethodSlot, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); + emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the method signature + emitter.instruction(&format!("jne {}", fail_label)); // reject method dispatch when arity differs +} + +/// Prepares ARM64 method ABI registers for the supported argument shapes. +fn emit_aarch64_prepare_method_args( + module: &Module, + emitter: &mut Emitter, + slot: &EvalMethodSlot, +) { + if let Some(param_ty) = slot.params.first() { + emit_aarch64_load_first_eval_arg(module, emitter); + emit_aarch64_cast_first_eval_arg(emitter, param_ty); + } + emitter.instruction("ldr x0, [sp, #16]"); // pass the unboxed receiver as the method's `$this` argument +} + +/// Prepares x86_64 method ABI registers for the supported argument shapes. +fn emit_x86_64_prepare_method_args( + module: &Module, + emitter: &mut Emitter, + slot: &EvalMethodSlot, +) { + if let Some(param_ty) = slot.params.first() { + emit_x86_64_load_first_eval_arg(module, emitter); + emit_x86_64_cast_first_eval_arg(emitter, param_ty); + } + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the unboxed receiver as the method's `$this` argument +} + +/// Loads the first eval argument into an ARM64 spill slot as a boxed Mixed cell. +fn emit_aarch64_load_first_eval_arg(module: &Module, emitter: &mut Emitter) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + emitter.instruction("mov x0, xzr"); // materialize eval argument index 0 + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("str x0, [sp, #32]"); // save the boxed index while loading from the argument array + emitter.instruction("ldr x1, [sp, #32]"); // pass the boxed index to the eval array reader + emitter.instruction("ldr x0, [sp, #24]"); // pass the eval argument array to the reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("str x0, [sp, #32]"); // save the boxed first argument for coercion +} + +/// Loads the first eval argument into an x86_64 spill slot as a boxed Mixed cell. +fn emit_x86_64_load_first_eval_arg(module: &Module, emitter: &mut Emitter) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + emitter.instruction("xor edi, edi"); // materialize eval argument index 0 + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed index while loading from the argument array + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the boxed index to the eval array reader + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the eval argument array to the reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed first argument for coercion +} + +/// Casts the first boxed eval argument into ARM64 method argument registers. +fn emit_aarch64_cast_first_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { + match param_ty.codegen_repr() { + PhpType::Int => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval argument for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the eval argument to a PHP int + emitter.instruction("mov x1, x0"); // pass the coerced integer as the first visible method argument + } + PhpType::Bool => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("bl __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool + emitter.instruction("mov x1, x0"); // pass the coerced boolean as the first visible method argument + } + PhpType::Float => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval argument for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in d0 + } + PhpType::Str => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval argument for string coercion + emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 + } + _ => {} + } +} + +/// Casts the first boxed eval argument into x86_64 method argument registers. +fn emit_x86_64_cast_first_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { + match param_ty.codegen_repr() { + PhpType::Int => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce the eval argument to a PHP int + emitter.instruction("mov rsi, rax"); // pass the coerced integer as the first visible method argument + } + PhpType::Bool => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("call __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool + emitter.instruction("mov rsi, rax"); // pass the coerced boolean as the first visible method argument + } + PhpType::Float => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for float coercion + emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in xmm0 + } + PhpType::Str => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion + emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair + emitter.instruction("mov rsi, rax"); // pass the coerced string pointer as the first visible method argument + } + _ => {} + } +} + /// Boxes the current native method result as the Mixed cell expected by eval. fn emit_box_method_result(module: &Module, emitter: &mut Emitter, slot: &EvalMethodSlot) { if slot.return_ty.codegen_repr() == PhpType::Void { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9053f6ba6f..d97a6c19a1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -492,6 +492,30 @@ $box->run(); assert_eq!(out, "42"); } +/// Verifies eval fragments pass one scalar argument to public AOT methods through `$this`. +#[test] +fn test_eval_fragment_can_call_this_public_one_arg_method() { + let out = compile_and_run( + r#"x + $amount; + } + + public function run(): void { + echo eval('return $this->add(9);'); + } +} + +$box = new EvalMethodArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "50"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From c540a29fe59bb2dcc87c711729b76230c0905de9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 18:22:20 +0200 Subject: [PATCH 0020/1208] Support eval method calls with two scalar args --- crates/elephc-eval/src/interpreter.rs | 34 +++++++++ crates/elephc-eval/src/parser.rs | 18 +++++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 9 +++ src/codegen/eval_method_helpers.rs | 103 ++++++++++++++++---------- tests/codegen/eval.rs | 24 ++++++ 6 files changed, 148 insertions(+), 42 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ea9e3301e8..0229750854 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -870,6 +870,22 @@ mod tests { }; self.int(x + arg) } + (FakeValue::Object(properties), "add2_x") => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = properties.get("x").copied().ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + left + right) + } _ => Err(EvalStatus::UnsupportedConstruct), } } @@ -1201,6 +1217,24 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(12)); } + /// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. + #[test] + fn execute_program_calls_object_method_with_two_arguments() { + let program = + parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let mut properties = HashMap::new(); + properties.insert("x".to_string(), x); + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); + } + /// Verifies if/else executes only the PHP-truthy branch. #[test] fn execute_program_if_else_uses_php_truthiness() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index eb6bfe6216..37bd014642 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1251,6 +1251,24 @@ mod tests { ); } + /// Verifies object method calls parse multiple argument expressions in source order. + #[test] + fn parse_fragment_accepts_method_call_multiple_args_source() { + let program = + parse_fragment(br#"return $this->label($x, "ok");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "label".to_string(), + args: vec![ + EvalExpr::LoadVar("x".to_string()), + EvalExpr::Const(EvalConst::String("ok".to_string())), + ], + }))] + ); + } + /// Verifies object property writes parse as dedicated EvalIR statements. #[test] fn parse_fragment_accepts_property_write_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index bbe79ef662..66d5c51d1c 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero- or one-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index f2b0bb4d3c..9d5ef9729f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -16,6 +16,10 @@ public function add(int $amount): int { return $this->value + $amount; } + public function label(int $amount, string $suffix): string { + return ($this->value + $amount) . $suffix; + } + public function echoReadThroughEval(): void { echo "eval-this-method=" . eval('return $this->read();') . "\n"; } @@ -23,6 +27,10 @@ public function echoReadThroughEval(): void { public function echoAddThroughEval(): void { echo "eval-this-method-arg=" . eval('return $this->add(5);') . "\n"; } + + public function echoLabelThroughEval(): void { + echo "eval-this-method-two-args=" . eval('return $this->label(5, "!");') . "\n"; + } } $x = 1; @@ -52,6 +60,7 @@ public function echoAddThroughEval(): void { echo "eval-this-property=" . $counter->value . "\n"; $counter->echoReadThroughEval(); $counter->echoAddThroughEval(); +$counter->echoLabelThroughEval(); echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 350f51e21a..cd2c0447a0 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -8,16 +8,16 @@ //! Key details: //! - The cacheable runtime object cannot know user class ids, method symbols, //! or return types, so this bridge is emitted into the user assembly. -//! - This method-call slice supports public AOT methods with zero or one -//! non-by-ref scalar argument and reports unsupported calls as runtime failure. +//! - This method-call slice supports public AOT methods with zero, one, or two +//! non-by-ref scalar arguments and reports unsupported calls as runtime failure. use std::collections::BTreeMap; use crate::codegen::abi; use crate::codegen::data_section::DataSection; +use crate::codegen::emit_box_current_value_as_mixed; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; -use crate::codegen::emit_box_current_value_as_mixed; use crate::ir::{Function, LocalKind, Module}; use crate::names::method_symbol; use crate::parser::ast::Visibility; @@ -34,6 +34,8 @@ struct EvalMethodSlot { return_ty: PhpType, } +const MAX_EVAL_METHOD_ARGS: usize = 2; + /// Emits eval method-call helpers when any lowered function owns an eval context. pub(super) fn emit_eval_method_helpers( module: &Module, @@ -134,7 +136,7 @@ fn method_is_public(class_info: &ClassInfo, method: &str) -> bool { /// Returns true for method signatures supported by this first eval bridge slice. fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { - sig.params.len() <= 1 + sig.params.len() <= MAX_EVAL_METHOD_ARGS && sig.variadic.is_none() && sig.ref_params.iter().all(|is_ref| !*is_ref) && sig @@ -143,7 +145,7 @@ fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { .all(|(_, ty)| method_param_supported(ty)) } -/// Returns true for one eval-supplied method argument type supported by this bridge. +/// Returns true for an eval-supplied method argument type supported by this bridge. fn method_param_supported(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), @@ -334,8 +336,9 @@ fn emit_aarch64_method_bodies( for slot in slots { emitter.label(&method_body_label(module, slot)); emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); - emit_aarch64_prepare_method_args(module, emitter, slot); + let overflow_bytes = emit_aarch64_prepare_method_args(module, emitter, slot); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); + abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, slot); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result } @@ -352,8 +355,9 @@ fn emit_x86_64_method_bodies( for slot in slots { emitter.label(&method_body_label(module, slot)); emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); - emit_x86_64_prepare_method_args(module, emitter, slot); + let overflow_bytes = emit_x86_64_prepare_method_args(module, emitter, slot); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); + abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, slot); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result } @@ -394,12 +398,16 @@ fn emit_aarch64_prepare_method_args( module: &Module, emitter: &mut Emitter, slot: &EvalMethodSlot, -) { - if let Some(param_ty) = slot.params.first() { - emit_aarch64_load_first_eval_arg(module, emitter); - emit_aarch64_cast_first_eval_arg(emitter, param_ty); +) -> usize { + let receiver_ty = PhpType::Object(slot.class_name.clone()); + emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first method argument + abi::emit_push_result_value(emitter, &receiver_ty); + for (index, param_ty) in slot.params.iter().enumerate() { + emit_aarch64_load_eval_arg(module, emitter, index); + emit_aarch64_cast_eval_arg(emitter, param_ty); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } - emitter.instruction("ldr x0, [sp, #16]"); // pass the unboxed receiver as the method's `$this` argument + materialize_method_args(module, emitter, &receiver_ty, &slot.params) } /// Prepares x86_64 method ABI registers for the supported argument shapes. @@ -407,77 +415,91 @@ fn emit_x86_64_prepare_method_args( module: &Module, emitter: &mut Emitter, slot: &EvalMethodSlot, -) { - if let Some(param_ty) = slot.params.first() { - emit_x86_64_load_first_eval_arg(module, emitter); - emit_x86_64_cast_first_eval_arg(emitter, param_ty); +) -> usize { + let receiver_ty = PhpType::Object(slot.class_name.clone()); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first method argument + abi::emit_push_result_value(emitter, &receiver_ty); + for (index, param_ty) in slot.params.iter().enumerate() { + emit_x86_64_load_eval_arg(module, emitter, index); + emit_x86_64_cast_eval_arg(emitter, param_ty); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } - emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the unboxed receiver as the method's `$this` argument + materialize_method_args(module, emitter, &receiver_ty, &slot.params) } -/// Loads the first eval argument into an ARM64 spill slot as a boxed Mixed cell. -fn emit_aarch64_load_first_eval_arg(module: &Module, emitter: &mut Emitter) { +/// Materializes the pushed receiver and eval arguments into the target method ABI. +fn materialize_method_args( + module: &Module, + emitter: &mut Emitter, + receiver_ty: &PhpType, + params: &[PhpType], +) -> usize { + let mut arg_types = Vec::with_capacity(params.len() + 1); + arg_types.push(receiver_ty.clone()); + arg_types.extend(params.iter().map(|param| param.codegen_repr())); + let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); + abi::materialize_outgoing_args(emitter, &assignments) +} + +/// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. +fn emit_aarch64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); - emitter.instruction("mov x0, xzr"); // materialize eval argument index 0 + abi::emit_load_int_immediate(emitter, "x0", index as i64); abi::emit_call_label(emitter, &value_int_symbol); - emitter.instruction("str x0, [sp, #32]"); // save the boxed index while loading from the argument array - emitter.instruction("ldr x1, [sp, #32]"); // pass the boxed index to the eval array reader - emitter.instruction("ldr x0, [sp, #24]"); // pass the eval argument array to the reader + emitter.instruction("str x0, [x29, #-16]"); // save the boxed index while loading from the argument array + emitter.instruction("ldr x1, [x29, #-16]"); // pass the boxed index to the eval array reader + emitter.instruction("ldr x0, [x29, #-24]"); // pass the eval argument array to the reader abi::emit_call_label(emitter, &array_get_symbol); - emitter.instruction("str x0, [sp, #32]"); // save the boxed first argument for coercion + emitter.instruction("str x0, [x29, #-16]"); // save the boxed eval argument for coercion } -/// Loads the first eval argument into an x86_64 spill slot as a boxed Mixed cell. -fn emit_x86_64_load_first_eval_arg(module: &Module, emitter: &mut Emitter) { +/// Loads one eval argument into an x86_64 spill slot as a boxed Mixed cell. +fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); - emitter.instruction("xor edi, edi"); // materialize eval argument index 0 + abi::emit_load_int_immediate(emitter, "rdi", index as i64); abi::emit_call_label(emitter, &value_int_symbol); emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed index while loading from the argument array emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the boxed index to the eval array reader emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the eval argument array to the reader abi::emit_call_label(emitter, &array_get_symbol); - emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed first argument for coercion + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed eval argument for coercion } -/// Casts the first boxed eval argument into ARM64 method argument registers. -fn emit_aarch64_cast_first_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { +/// Casts one boxed eval argument into ARM64 result registers for temporary staging. +fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { match param_ty.codegen_repr() { PhpType::Int => { - emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval argument for integer coercion + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for integer coercion emitter.instruction("bl __rt_mixed_cast_int"); // coerce the eval argument to a PHP int - emitter.instruction("mov x1, x0"); // pass the coerced integer as the first visible method argument } PhpType::Bool => { - emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for boolean coercion emitter.instruction("bl __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool - emitter.instruction("mov x1, x0"); // pass the coerced boolean as the first visible method argument } PhpType::Float => { - emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval argument for float coercion + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for float coercion emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in d0 } PhpType::Str => { - emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval argument for string coercion + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 } _ => {} } } -/// Casts the first boxed eval argument into x86_64 method argument registers. -fn emit_x86_64_cast_first_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { +/// Casts one boxed eval argument into x86_64 result registers for temporary staging. +fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { match param_ty.codegen_repr() { PhpType::Int => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion emitter.instruction("call __rt_mixed_cast_int"); // coerce the eval argument to a PHP int - emitter.instruction("mov rsi, rax"); // pass the coerced integer as the first visible method argument } PhpType::Bool => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for boolean coercion emitter.instruction("call __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool - emitter.instruction("mov rsi, rax"); // pass the coerced boolean as the first visible method argument } PhpType::Float => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for float coercion @@ -486,7 +508,6 @@ fn emit_x86_64_cast_first_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { PhpType::Str => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair - emitter.instruction("mov rsi, rax"); // pass the coerced string pointer as the first visible method argument } _ => {} } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d97a6c19a1..5e2c96a56f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -516,6 +516,30 @@ $box->run(); assert_eq!(out, "50"); } +/// Verifies eval fragments pass two scalar arguments to public AOT methods through `$this`. +#[test] +fn test_eval_fragment_can_call_this_public_two_arg_method() { + let out = compile_and_run( + r#"x + $amount) . $suffix; + } + + public function run(): void { + echo eval('return $this->label(9, "!");'); + } +} + +$box = new EvalMethodTwoArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "50!"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From 6552d1414d66ac4a3227a95c13d5f85563f75f90 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 18:28:02 +0200 Subject: [PATCH 0021/1208] Preserve eval property identifier case --- crates/elephc-eval/src/parser.rs | 70 +++++++++++++++++++++----------- tests/codegen/eval.rs | 20 +++++++++ 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 37bd014642..293764f389 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -190,7 +190,7 @@ impl<'a> Lexer<'a> { self.bump_char(); Ok(TokenKind::Comma) } - _ if is_ident_start(ch) => Ok(TokenKind::Ident(self.lex_ident().to_ascii_lowercase())), + _ if is_ident_start(ch) => Ok(TokenKind::Ident(self.lex_ident())), _ => Err(EvalParseError::UnexpectedToken), } } @@ -330,27 +330,29 @@ impl Parser { /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. fn parse_stmt(&mut self) -> Result, EvalParseError> { match self.current() { - TokenKind::Ident(name) if name == "break" => { + TokenKind::Ident(name) if ident_eq(name, "break") => { self.advance(); self.expect_semicolon()?; Ok(vec![EvalStmt::Break]) } - TokenKind::Ident(name) if name == "continue" => { + TokenKind::Ident(name) if ident_eq(name, "continue") => { self.advance(); self.expect_semicolon()?; Ok(vec![EvalStmt::Continue]) } - TokenKind::Ident(name) if name == "echo" => { + TokenKind::Ident(name) if ident_eq(name, "echo") => { self.advance(); let expr = self.parse_expr()?; self.expect_semicolon()?; Ok(vec![EvalStmt::Echo(expr)]) } - TokenKind::Ident(name) if name == "for" => self.parse_for_stmt(), - TokenKind::Ident(name) if name == "foreach" => self.parse_foreach_stmt(), - TokenKind::Ident(name) if name == "function" => self.parse_function_decl_stmt(), - TokenKind::Ident(name) if name == "if" => self.parse_if_stmt(), - TokenKind::Ident(name) if name == "return" => { + TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), + TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), + TokenKind::Ident(name) if ident_eq(name, "function") => { + self.parse_function_decl_stmt() + } + TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), + TokenKind::Ident(name) if ident_eq(name, "return") => { self.advance(); if self.consume_semicolon() { return Ok(vec![EvalStmt::Return(None)]); @@ -359,8 +361,8 @@ impl Parser { self.expect_semicolon()?; Ok(vec![EvalStmt::Return(Some(expr))]) } - TokenKind::Ident(name) if name == "unset" => self.parse_unset_stmt(), - TokenKind::Ident(name) if name == "while" => self.parse_while_stmt(), + TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), + TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(true) } @@ -423,7 +425,7 @@ impl Parser { self.advance(); self.expect(TokenKind::LParen)?; let array = self.parse_expr()?; - if !matches!(self.current(), TokenKind::Ident(name) if name == "as") { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "as")) { return Err(EvalParseError::UnexpectedToken); } self.advance(); @@ -447,7 +449,7 @@ impl Parser { let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; - let name = name.clone(); + let name = name.to_ascii_lowercase(); self.advance(); self.expect(TokenKind::LParen)?; let params = self.parse_function_params()?; @@ -578,15 +580,15 @@ impl Parser { /// Parses `elseif`, `else if`, or `else` branches after an `if` body. fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::Ident(name) if name == "elseif") { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { self.advance(); return Ok(vec![self.parse_if_after_keyword()?]); } - if !matches!(self.current(), TokenKind::Ident(name) if name == "else") { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "else")) { return Ok(Vec::new()); } self.advance(); - if matches!(self.current(), TokenKind::Ident(name) if name == "if") { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "if")) { self.advance(); Ok(vec![self.parse_if_after_keyword()?]) } else { @@ -761,7 +763,7 @@ impl Parser { let args = self.parse_call_args()?; expr = EvalExpr::MethodCall { object: Box::new(expr), - method: member, + method: member.to_ascii_lowercase(), args, }; } else { @@ -800,19 +802,19 @@ impl Parser { self.advance(); Ok(EvalExpr::LoadVar(name)) } - TokenKind::Ident(name) if name == "null" => { + TokenKind::Ident(name) if ident_eq(name, "null") => { self.advance(); Ok(EvalExpr::Const(EvalConst::Null)) } - TokenKind::Ident(name) if name == "true" => { + TokenKind::Ident(name) if ident_eq(name, "true") => { self.advance(); Ok(EvalExpr::Const(EvalConst::Bool(true))) } - TokenKind::Ident(name) if name == "false" => { + TokenKind::Ident(name) if ident_eq(name, "false") => { self.advance(); Ok(EvalExpr::Const(EvalConst::Bool(false))) } - TokenKind::Ident(name) if name == "print" => { + TokenKind::Ident(name) if ident_eq(name, "print") => { self.advance(); let expr = self.parse_expr()?; Ok(EvalExpr::Print(Box::new(expr))) @@ -836,7 +838,10 @@ impl Parser { fn parse_call_expr(&mut self, name: String) -> Result { self.advance(); let args = self.parse_call_args()?; - Ok(EvalExpr::Call { name, args }) + Ok(EvalExpr::Call { + name: name.to_ascii_lowercase(), + args, + }) } /// Parses a parenthesized source-order argument list. @@ -946,6 +951,11 @@ fn is_ident_continue(ch: char) -> bool { is_ident_start(ch) || ch.is_ascii_digit() } +/// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. +fn ident_eq(actual: &str, expected: &str) -> bool { + actual.eq_ignore_ascii_case(expected) +} + #[cfg(test)] mod tests { use super::*; @@ -1218,10 +1228,24 @@ mod tests { ); } + /// Verifies property names preserve source case while keywords remain case-insensitive. + #[test] + fn parse_fragment_preserves_property_case_source() { + let program = + parse_fragment(br#"RETURN $this->camelName;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "camelName".to_string(), + }))] + ); + } + /// Verifies object method calls parse as postfix EvalIR call expressions. #[test] fn parse_fragment_accepts_method_call_source() { - let program = parse_fragment(br#"return $this->answer();"#).expect("fragment should parse"); + let program = parse_fragment(br#"return $this->Answer();"#).expect("fragment should parse"); assert_eq!( program.statements(), &[EvalStmt::Return(Some(EvalExpr::MethodCall { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5e2c96a56f..c6e162fc51 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -468,6 +468,26 @@ echo $box->x; assert_eq!(out, "2"); } +/// Verifies eval keeps PHP property names case-sensitive while parsing keywords case-insensitively. +#[test] +fn test_eval_fragment_preserves_this_property_case() { + let out = compile_and_run( + r#"camelName;'); + } +} + +$box = new EvalCasePropBox(); +$box->read(); +"#, + ); + assert_eq!(out, "42"); +} + /// Verifies eval fragments can call public zero-argument AOT methods through `$this`. #[test] fn test_eval_fragment_can_call_this_public_zero_arg_method() { From 0b829b99826075c4eb4b4646bc8edd04b00504e4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 18:35:57 +0200 Subject: [PATCH 0022/1208] Support eval short-circuit logical operators --- crates/elephc-eval/src/eval_ir.rs | 2 + crates/elephc-eval/src/interpreter.rs | 54 ++++++++++++++++++- crates/elephc-eval/src/parser.rs | 71 ++++++++++++++++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 7 ++- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 13 +++++ 7 files changed, 146 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index a09139e65a..07fe886ba9 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -174,6 +174,8 @@ pub enum EvalBinOp { Sub, Mul, Concat, + LogicalAnd, + LogicalOr, LooseEq, LooseNotEq, Lt, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0229750854..1faeb005f5 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -433,6 +433,24 @@ fn eval_expr( values.int(1) } EvalExpr::Binary { op, left, right } => { + if *op == EvalBinOp::LogicalAnd { + let left = eval_expr(left, context, scope, values)?; + if !values.truthy(left)? { + return values.bool_value(false); + } + let right = eval_expr(right, context, scope, values)?; + let truthy = values.truthy(right)?; + return values.bool_value(truthy); + } + if *op == EvalBinOp::LogicalOr { + let left = eval_expr(left, context, scope, values)?; + if values.truthy(left)? { + return values.bool_value(true); + } + let right = eval_expr(right, context, scope, values)?; + let truthy = values.truthy(right)?; + return values.bool_value(truthy); + } let left = eval_expr(left, context, scope, values)?; let right = eval_expr(right, context, scope, values)?; match op { @@ -446,6 +464,9 @@ fn eval_expr( | EvalBinOp::LtEq | EvalBinOp::Gt | EvalBinOp::GtEq => values.compare(*op, left, right), + EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => { + Err(EvalStatus::UnsupportedConstruct) + } } } } @@ -999,7 +1020,12 @@ mod tests { EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, - EvalBinOp::Add | EvalBinOp::Sub | EvalBinOp::Mul | EvalBinOp::Concat => { + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Concat + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr => { return Err(EvalStatus::UnsupportedConstruct); } }; @@ -1337,6 +1363,32 @@ mod tests { assert_eq!(values.output, "1111ns"); } + /// Verifies logical AND skips an unsupported right-hand expression after a false left side. + #[test] + fn execute_program_short_circuits_logical_and() { + let program = + parse_fragment(br#"return false && missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(false)); + } + + /// Verifies logical OR skips an unsupported right-hand expression after a true left side. + #[test] + fn execute_program_short_circuits_logical_or() { + let program = + parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies foreach assigns each indexed element to the value variable. #[test] fn execute_program_foreach_iterates_indexed_values() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 293764f389..eedc2df4c4 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -46,6 +46,8 @@ enum TokenKind { Equal, EqualEqual, NotEqual, + AndAnd, + OrOr, Less, LessEqual, Greater, @@ -140,6 +142,24 @@ impl<'a> Lexer<'a> { Err(EvalParseError::UnexpectedToken) } } + '&' => { + self.bump_char(); + if self.peek_char() == Some('&') { + self.bump_char(); + Ok(TokenKind::AndAnd) + } else { + Err(EvalParseError::UnexpectedToken) + } + } + '|' => { + self.bump_char(); + if self.peek_char() == Some('|') { + self.bump_char(); + Ok(TokenKind::OrOr) + } else { + Err(EvalParseError::UnexpectedToken) + } + } '<' => { self.bump_char(); if self.peek_char() == Some('=') { @@ -640,9 +660,37 @@ impl Parser { Ok(statements) } - /// Parses an expression using PHP-like comparison, concatenation, and arithmetic precedence. + /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. fn parse_expr(&mut self) -> Result { - self.parse_equality() + self.parse_logical_or() + } + + /// Parses left-associative logical OR with lower precedence than logical AND. + fn parse_logical_or(&mut self) -> Result { + let mut expr = self.parse_logical_and()?; + while self.consume(TokenKind::OrOr) { + let right = self.parse_logical_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative logical AND with lower precedence than equality. + fn parse_logical_and(&mut self) -> Result { + let mut expr = self.parse_equality()?; + while self.consume(TokenKind::AndAnd) { + let right = self.parse_equality()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) } /// Parses left-associative loose equality and inequality comparisons. @@ -1143,6 +1191,25 @@ mod tests { ); } + /// Verifies logical operators parse with `&&` binding tighter than `||`. + #[test] + fn parse_fragment_accepts_short_circuit_logical_source() { + let program = + parse_fragment(br#"return $a && $b || false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::LoadVar("a".to_string())), + right: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); + } + /// Verifies print fragments lower to expression-form print with the printed value. #[test] fn parse_fragment_accepts_print_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 712cfb07f6..37dbeedb03 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -335,6 +335,11 @@ fn compare_op_tag(op: EvalBinOp) -> u64 { EvalBinOp::LtEq => 3, EvalBinOp::Gt => 4, EvalBinOp::GtEq => 5, - EvalBinOp::Add | EvalBinOp::Sub | EvalBinOp::Mul | EvalBinOp::Concat => 0, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Concat + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr => 0, } } diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 66d5c51d1c..214cabf21b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 9d5ef9729f..649f30d357 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -45,6 +45,7 @@ public function echoLabelThroughEval(): void { eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); $eval_native_call = eval('return compiled_add(2, 8);'); +$logic = eval('return true || missing_eval_rhs();'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -55,6 +56,7 @@ public function echoLabelThroughEval(): void { echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; +echo "logic=" . $logic . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c6e162fc51..69857dcf3c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -123,6 +123,19 @@ eval('echo "a" == "a"; echo "a" != "b"; echo "" == null; echo "10" == 10; echo " assert_eq!(out, "111111"); } +/// Verifies eval logical operators short-circuit before evaluating unsupported RHS calls. +#[test] +fn test_eval_logical_operators_short_circuit() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 18:42:04 +0200 Subject: [PATCH 0023/1208] Support eval logical negation --- crates/elephc-eval/src/eval_ir.rs | 10 +++++++ crates/elephc-eval/src/interpreter.rs | 23 +++++++++++++++ crates/elephc-eval/src/parser.rs | 41 ++++++++++++++++++++++++--- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 13 +++++++++ 6 files changed, 86 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 07fe886ba9..686fb07533 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -143,6 +143,10 @@ pub enum EvalExpr { property: String, }, Print(Box), + Unary { + op: EvalUnaryOp, + expr: Box, + }, Binary { op: EvalBinOp, left: Box, @@ -183,3 +187,9 @@ pub enum EvalBinOp { Gt, GtEq, } + +/// Unary operations supported by the initial EvalIR parser. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalUnaryOp { + LogicalNot, +} diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 1faeb005f5..2f3ef9102d 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -15,6 +15,7 @@ use crate::errors::EvalStatus; use crate::context::{ElephcEvalContext, NativeFunction}; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalFunction, EvalProgram, EvalStmt, + EvalUnaryOp, }; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership}; @@ -432,6 +433,15 @@ fn eval_expr( values.echo(value)?; values.int(1) } + EvalExpr::Unary { op, expr } => { + let value = eval_expr(expr, context, scope, values)?; + match op { + EvalUnaryOp::LogicalNot => { + let truthy = values.truthy(value)?; + values.bool_value(!truthy) + } + } + } EvalExpr::Binary { op, left, right } => { if *op == EvalBinOp::LogicalAnd { let left = eval_expr(left, context, scope, values)?; @@ -1389,6 +1399,19 @@ mod tests { assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies logical negation returns boolean cells using PHP truthiness. + #[test] + fn execute_program_evaluates_logical_not() { + let program = + parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1"); + } + /// Verifies foreach assigns each indexed element to the value variable. #[test] fn execute_program_foreach_iterates_indexed_values() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index eedc2df4c4..9c8756283d 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -13,7 +13,9 @@ //! - Fragment spans must be based on call-site metadata when implemented. use crate::errors::EvalParseError; -use crate::eval_ir::{EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalProgram, EvalStmt}; +use crate::eval_ir::{ + EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalProgram, EvalStmt, EvalUnaryOp, +}; /// Parses an eval fragment into by-name EvalIR statements. pub fn parse_fragment(code: &[u8]) -> Result { @@ -45,6 +47,7 @@ enum TokenKind { Dot, Equal, EqualEqual, + Bang, NotEqual, AndAnd, OrOr, @@ -139,7 +142,7 @@ impl<'a> Lexer<'a> { self.bump_char(); Ok(TokenKind::NotEqual) } else { - Err(EvalParseError::UnexpectedToken) + Ok(TokenKind::Bang) } } '&' => { @@ -776,9 +779,9 @@ impl Parser { /// Parses left-associative numeric multiplication. fn parse_mul(&mut self) -> Result { - let mut expr = self.parse_postfix()?; + let mut expr = self.parse_unary()?; while self.consume(TokenKind::Star) { - let right = self.parse_postfix()?; + let right = self.parse_unary()?; expr = EvalExpr::Binary { op: EvalBinOp::Mul, left: Box::new(expr), @@ -788,6 +791,18 @@ impl Parser { Ok(expr) } + /// Parses right-associative unary prefix expressions. + fn parse_unary(&mut self) -> Result { + if self.consume(TokenKind::Bang) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(expr), + }); + } + self.parse_postfix() + } + /// Parses postfix array reads, property reads, and method calls after a primary expression. fn parse_postfix(&mut self) -> Result { let mut expr = self.parse_primary()?; @@ -1210,6 +1225,24 @@ mod tests { ); } + /// Verifies logical negation parses as a unary expression before comparisons. + #[test] + fn parse_fragment_accepts_logical_not_source() { + let program = + parse_fragment(br#"return !$flag == true;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LooseEq, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(EvalExpr::LoadVar("flag".to_string())), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + }))] + ); + } + /// Verifies print fragments lower to expression-form print with the printed value. #[test] fn parse_fragment_accepts_print_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 214cabf21b..a7c0b86118 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 649f30d357..76ded3448b 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -46,6 +46,7 @@ public function echoLabelThroughEval(): void { $dynamic_call = eval('return plus_one(4);'); $eval_native_call = eval('return compiled_add(2, 8);'); $logic = eval('return true || missing_eval_rhs();'); +$not_false = eval('return !false;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -57,6 +58,7 @@ public function echoLabelThroughEval(): void { echo "dynamic-call=" . $dynamic_call . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; echo "logic=" . $logic . "\n"; +echo "not-false=" . $not_false . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 69857dcf3c..22fb044d4f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -136,6 +136,19 @@ echo eval('return true || missing_eval_rhs();'); assert_eq!(out, "ab:1"); } +/// Verifies eval logical negation returns PHP boolean cells through the bridge. +#[test] +fn test_eval_logical_not_executes_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 18:47:38 +0200 Subject: [PATCH 0024/1208] Support eval unary numeric operators --- crates/elephc-eval/src/eval_ir.rs | 2 ++ crates/elephc-eval/src/interpreter.rs | 23 ++++++++++++++++++ crates/elephc-eval/src/parser.rs | 35 +++++++++++++++++++++++++++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 7 ++++++ 6 files changed, 70 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 686fb07533..08385c7153 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -191,5 +191,7 @@ pub enum EvalBinOp { /// Unary operations supported by the initial EvalIR parser. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EvalUnaryOp { + Plus, + Negate, LogicalNot, } diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2f3ef9102d..7f5f1e8eee 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -436,6 +436,14 @@ fn eval_expr( EvalExpr::Unary { op, expr } => { let value = eval_expr(expr, context, scope, values)?; match op { + EvalUnaryOp::Plus => { + let zero = values.int(0)?; + values.add(zero, value) + } + EvalUnaryOp::Negate => { + let zero = values.int(0)?; + values.sub(zero, value) + } EvalUnaryOp::LogicalNot => { let truthy = values.truthy(value)?; values.bool_value(!truthy) @@ -1412,6 +1420,21 @@ mod tests { assert_eq!(values.output, "1"); } + /// Verifies unary numeric operators delegate to PHP numeric runtime operations. + #[test] + fn execute_program_evaluates_unary_numeric_ops() { + let program = parse_fragment(br#"return -$x + +2;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(5).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(-3)); + } + /// Verifies foreach assigns each indexed element to the value variable. #[test] fn execute_program_foreach_iterates_indexed_values() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 9c8756283d..24641d7f64 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -793,6 +793,20 @@ impl Parser { /// Parses right-associative unary prefix expressions. fn parse_unary(&mut self) -> Result { + if self.consume(TokenKind::Plus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Minus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(expr), + }); + } if self.consume(TokenKind::Bang) { let expr = self.parse_unary()?; return Ok(EvalExpr::Unary { @@ -1243,6 +1257,27 @@ mod tests { ); } + /// Verifies unary numeric operators bind tighter than multiplication. + #[test] + fn parse_fragment_accepts_unary_numeric_source() { + let program = + parse_fragment(br#"return -$x * +2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(EvalExpr::LoadVar("x".to_string())), + }), + right: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + }))] + ); + } + /// Verifies print fragments lower to expression-form print with the printed value. #[test] fn parse_fragment_accepts_print_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a7c0b86118..bc95879f82 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 76ded3448b..092ce464fc 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -47,6 +47,7 @@ public function echoLabelThroughEval(): void { $eval_native_call = eval('return compiled_add(2, 8);'); $logic = eval('return true || missing_eval_rhs();'); $not_false = eval('return !false;'); +$negative = eval('return -5 + +2;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -59,6 +60,7 @@ public function echoLabelThroughEval(): void { echo "eval-native-call=" . $eval_native_call . "\n"; echo "logic=" . $logic . "\n"; echo "not-false=" . $not_false . "\n"; +echo "negative=" . $negative . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 22fb044d4f..a4e40e0d84 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -149,6 +149,13 @@ echo eval('return !"x";'); assert_eq!(out, "1:"); } +/// Verifies eval unary numeric operators execute through runtime numeric helpers. +#[test] +fn test_eval_unary_numeric_operators_execute_through_bridge() { + let out = compile_and_run(" Date: Mon, 15 Jun 2026 18:56:55 +0200 Subject: [PATCH 0025/1208] Support call_user_func inside eval fragments --- crates/elephc-eval/src/interpreter.rs | 178 +++++++++++++++++++++++++- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 37 ++++++ 4 files changed, 214 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7f5f1e8eee..f30fd886ad 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -499,6 +499,7 @@ fn eval_call( values: &mut impl RuntimeValueOps, ) -> Result { match name { + "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "count" => eval_builtin_count(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), "function_exists" | "is_callable" => { @@ -542,7 +543,106 @@ fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { /// Returns true for PHP-visible builtin names implemented by the eval interpreter. fn eval_php_visible_builtin_exists(name: &str) -> bool { - matches!(name, "count" | "function_exists" | "is_callable" | "strlen") + matches!( + name, + "call_user_func" | "count" | "function_exists" | "is_callable" | "strlen" + ) +} + +/// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. +fn eval_builtin_call_user_func( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_call_user_func_with_values(evaluated_args, context, values) +} + +/// Dispatches `call_user_func` after its callback and arguments are already evaluated. +fn eval_call_user_func_with_values( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, callback_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = values.string_bytes(*callback)?; + let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; + let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); + if callback.contains("::") { + return Err(EvalStatus::UnsupportedConstruct); + } + eval_callable_with_values(&callback, callback_args.to_vec(), context, values) +} + +/// Invokes a PHP-visible callable name with source-order positional values. +fn eval_callable_with_values( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { + return Ok(result); + } + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function_with_values(function, evaluated_args, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. +fn eval_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "call_user_func" => { + return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) + .map(Some); + } + "count" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let len = values.array_len(*value)?; + let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len)? + } + "function_exists" | "is_callable" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = values.string_bytes(*name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\').to_ascii_lowercase(); + values.bool_value(eval_function_probe_exists(context, &name))? + } + "strlen" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let bytes = values.string_bytes(*value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len)? + } + _ => return Ok(None), + }; + Ok(Some(result)) } /// Evaluates nested `eval(...)` calls against the current materialized scope. @@ -640,10 +740,25 @@ fn eval_native_function( if args.len() != function.param_count() { return Err(EvalStatus::RuntimeFatal); } - let arg_array = values.array_new(args.len())?; - for (index, arg) in args.iter().enumerate() { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, caller_scope, values)?); + } + eval_native_function_with_values(function, evaluated_args, values) +} + +/// Invokes a registered AOT function after its positional arguments are prepared. +fn eval_native_function_with_values( + function: NativeFunction, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.len() != function.param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let arg_array = values.array_new(evaluated_args.len())?; + for (index, value) in evaluated_args.into_iter().enumerate() { let index = values.int(index as i64)?; - let value = eval_expr(arg, context, caller_scope, values)?; let _ = values.array_set(arg_array, index, value)?; } let result = unsafe { function.call(arg_array) }; @@ -1539,6 +1654,61 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies `call_user_func` inside eval can dispatch an eval-declared function. + #[test] + fn execute_program_call_user_func_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x) { return $x + 1; } +return call_user_func("dyn", 4);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); + } + + /// Verifies `call_user_func` inside eval can dispatch a supported builtin. + #[test] + fn execute_program_call_user_func_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + } + + /// Verifies `call_user_func` inside eval can dispatch a registered native function. + #[test] + fn execute_program_call_user_func_dispatches_registered_native_function() { + let program = parse_fragment(br#"return call_user_func("native_answer");"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new( + expected.as_ptr().cast(), + fake_native_return_descriptor, + 0, + ); + assert!( + context + .define_native_function("native_answer", native) + .is_ok() + ); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + /// Verifies duplicate eval-declared function names fail in a shared context. #[test] fn execute_program_rejects_duplicate_declared_function() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index bc95879f82..b65a85ad4c 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 092ce464fc..3f8c60b671 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -44,6 +44,7 @@ public function echoLabelThroughEval(): void { $meta_count = eval('return count($meta);'); eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); +$dynamic_cuf = eval('return call_user_func("plus_one", 6);'); $eval_native_call = eval('return compiled_add(2, 8);'); $logic = eval('return true || missing_eval_rhs();'); $not_false = eval('return !false;'); @@ -57,6 +58,7 @@ public function echoLabelThroughEval(): void { echo "source=" . $meta["source"] . "\n"; echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; +echo "dynamic-cuf=" . $dynamic_cuf . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; echo "logic=" . $logic . "\n"; echo "not-false=" . $not_false . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a4e40e0d84..efdb4a3915 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -468,6 +468,43 @@ echo call_user_func('dyn_eval_cuf', 4); assert_eq!(out, "5"); } +/// Verifies `call_user_func()` inside eval can dispatch to an eval-declared function. +#[test] +fn test_eval_fragment_call_user_func_dispatches_eval_declared_function() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 19:03:46 +0200 Subject: [PATCH 0026/1208] Support call_user_func_array inside eval fragments --- crates/elephc-eval/src/interpreter.rs | 110 +++++++++++++++++++++++++- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 37 +++++++++ 4 files changed, 149 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f30fd886ad..2874abe749 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -500,6 +500,9 @@ fn eval_call( ) -> Result { match name { "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), + "call_user_func_array" => { + eval_builtin_call_user_func_array(args, context, scope, values) + } "count" => eval_builtin_count(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), "function_exists" | "is_callable" => { @@ -545,7 +548,12 @@ fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, - "call_user_func" | "count" | "function_exists" | "is_callable" | "strlen" + "call_user_func" + | "call_user_func_array" + | "count" + | "function_exists" + | "is_callable" + | "strlen" ) } @@ -566,6 +574,43 @@ fn eval_builtin_call_user_func( eval_call_user_func_with_values(evaluated_args, context, values) } +/// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. +fn eval_builtin_call_user_func_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [callback, arg_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let arg_array = eval_expr(arg_array, context, scope, values)?; + eval_call_user_func_array_with_values(callback, arg_array, context, values) +} + +/// Dispatches `call_user_func_array` after callback and array arguments are evaluated. +fn eval_call_user_func_array_with_values( + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = values.string_bytes(callback)?; + let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; + let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); + if callback.contains("::") { + return Err(EvalStatus::UnsupportedConstruct); + } + let len = values.array_len(arg_array)?; + let mut evaluated_args = Vec::with_capacity(len); + for index in 0..len { + let index = values.int(index as i64)?; + evaluated_args.push(values.array_get(arg_array, index)?); + } + eval_callable_with_values(&callback, evaluated_args, context, values) +} + /// Dispatches `call_user_func` after its callback and arguments are already evaluated. fn eval_call_user_func_with_values( evaluated_args: Vec, @@ -615,6 +660,13 @@ fn eval_builtin_with_values( return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) .map(Some); } + "call_user_func_array" => { + let [callback, arg_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) + .map(Some); + } "count" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1709,6 +1761,62 @@ return call_user_func("dyn", 4);"#, assert_eq!(result, expected); } + /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. + #[test] + fn execute_program_call_user_func_array_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", [4, 5]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(9)); + } + + /// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. + #[test] + fn execute_program_call_user_func_array_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + } + + /// Verifies `call_user_func_array` inside eval can dispatch a registered native function. + #[test] + fn execute_program_call_user_func_array_dispatches_registered_native_function() { + let program = + parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new( + expected.as_ptr().cast(), + fake_native_return_descriptor, + 2, + ); + assert!( + context + .define_native_function("native_answer", native) + .is_ok() + ); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + /// Verifies duplicate eval-declared function names fail in a shared context. #[test] fn execute_program_rejects_duplicate_declared_function() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b65a85ad4c..7b292397ec 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 3f8c60b671..adf0e69207 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -45,6 +45,7 @@ public function echoLabelThroughEval(): void { eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); $dynamic_cuf = eval('return call_user_func("plus_one", 6);'); +$dynamic_cufa = eval('return call_user_func_array("plus_one", [8]);'); $eval_native_call = eval('return compiled_add(2, 8);'); $logic = eval('return true || missing_eval_rhs();'); $not_false = eval('return !false;'); @@ -59,6 +60,7 @@ public function echoLabelThroughEval(): void { echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; echo "dynamic-cuf=" . $dynamic_cuf . "\n"; +echo "dynamic-cufa=" . $dynamic_cufa . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; echo "logic=" . $logic . "\n"; echo "not-false=" . $not_false . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index efdb4a3915..0d2b430270 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -505,6 +505,43 @@ eval('echo call_user_func("native_eval_cuf_add", 4, 6);'); assert_eq!(out, "10"); } +/// Verifies `call_user_func_array()` inside eval dispatches to eval-declared functions. +#[test] +fn test_eval_fragment_call_user_func_array_dispatches_eval_declared_function() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 19:15:49 +0200 Subject: [PATCH 0027/1208] Document eval namespace function semantics --- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7b292397ec..a1dd0fc3d6 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0d2b430270..b3b0d4ad6b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -444,6 +444,23 @@ echo dyn_eval_native(); assert_eq!(out, "42"); } +/// Verifies functions declared by eval from a namespace are registered globally. +#[test] +fn test_eval_declared_function_in_namespace_is_global() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 19:26:17 +0200 Subject: [PATCH 0028/1208] Support isset inside eval fragments --- crates/elephc-eval/src/interpreter.rs | 71 ++++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 20 ++++++ crates/elephc-eval/src/runtime_hooks.rs | 6 ++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + src/codegen_support/runtime/eval_bridge.rs | 22 +++++++ tests/codegen/eval.rs | 20 ++++++ 7 files changed, 141 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2874abe749..085d22c29f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -81,6 +81,9 @@ pub trait RuntimeValueOps { /// Returns whether a runtime cell can be indexed like an array by eval writes. fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result; + /// Returns whether a runtime cell holds PHP null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result; + /// Releases one owned runtime cell that is no longer held by the eval scope. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; @@ -508,6 +511,7 @@ fn eval_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) } + "isset" => eval_builtin_isset(args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), _ => { if let Some(function) = context.function(name).cloned() { @@ -538,6 +542,41 @@ fn eval_builtin_function_probe( values.bool_value(eval_function_probe_exists(context, &name)) } +/// Evaluates PHP's `isset(...)` language construct over eval-visible values. +fn eval_builtin_isset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return values.bool_value(false); + } + for arg in args { + if !eval_isset_arg(arg, context, scope, values)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates one `isset` operand without allocating a null cell for missing variables. +fn eval_isset_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = scope.visible_cell(name) else { + return Ok(false); + }; + return Ok(!values.is_null(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.is_null(value)?) +} + /// Returns true when a PHP function name is visible to eval builtin probes. fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { !name.contains("::") @@ -1113,6 +1152,11 @@ mod tests { )) } + /// Returns whether a fake runtime cell is null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + Ok(matches!(self.get(value), FakeValue::Null)) + } + /// Records fake releases without freeing handles needed for assertions. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { self.releases.push(value); @@ -1847,6 +1891,33 @@ return call_user_func_array("dyn", [4, 5]);"#, assert_eq!(values.get(result), FakeValue::Int(6)); } + /// Verifies `isset` distinguishes missing, null, and other falsey values. + #[test] + fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { + let program = parse_fragment( + br#"if (isset($missing)) { echo "1"; } else { echo "0"; } +if (isset($nullish)) { echo "1"; } else { echo "0"; } +if (isset($zero)) { echo "1"; } else { echo "0"; } +if (isset($empty)) { echo "1"; } else { echo "0"; } +if (isset($zero, $empty)) { echo "1"; } else { echo "0"; } +if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty = values.string("").expect("create fake string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty", empty, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "001110"); + assert_eq!(values.get(result), FakeValue::Null); + } + /// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. #[test] fn execute_program_function_probes_use_eval_context() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 24641d7f64..2879fd9158 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1304,6 +1304,26 @@ mod tests { ); } + /// Verifies `isset` parses as a case-insensitive function-like expression. + #[test] + fn parse_fragment_accepts_isset_source() { + let program = parse_fragment(br#"return ISSET($x, $items["k"]);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "isset".to_string(), + args: vec![ + EvalExpr::LoadVar("x".to_string()), + EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("items".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), + }, + ], + }))] + ); + } + /// Verifies indexed array literals and reads parse as runtime array expressions. #[test] fn parse_fragment_accepts_indexed_array_read_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 37dbeedb03..eec96c9879 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -52,6 +52,7 @@ unsafe extern "C" { ) -> *mut RuntimeCell; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; + fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_null() -> *mut RuntimeCell; fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; @@ -213,6 +214,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_is_array_like(value.as_ptr()) != 0 }) } + /// Returns whether a boxed Mixed cell unwraps to PHP null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_is_null(value.as_ptr()) != 0 }) + } + /// Releases one boxed Mixed cell through the generated runtime wrapper. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { unsafe { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a1dd0fc3d6..599ce7d00f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index adf0e69207..b6f8218afe 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -40,6 +40,7 @@ public function echoLabelThroughEval(): void { eval('if ($x >= 3) { echo "x>=3\n"; }'); eval('if ($x < 0) { echo "negative\n"; } elseif ($x == 3) { echo "x==3\n"; }'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); +eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); $meta = eval('return ["source" => "eval"];'); $meta_count = eval('return count($meta);'); eval('function plus_one($value) { return $value + 1; }'); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index fb28155c06..6da3c6583a 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -185,6 +185,17 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov x0, #1"); // report true for array-like values emitter.instruction("ret"); // return the boolean result to Rust + label_c_global(emitter, "__elephc_eval_value_is_null"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the Mixed cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells to a concrete runtime tag + emitter.instruction("cmp x0, #8"); // runtime tag 8 means PHP null + emitter.instruction("cset x0, eq"); // return true when the unboxed tag is null + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the null-check wrapper frame + emitter.instruction("ret"); // return the boolean result to Rust + label_c_global(emitter, "__elephc_eval_value_int"); emitter.instruction("mov x1, x0"); // move the C integer argument into the mixed payload slot emitter.instruction("mov x0, #0"); // runtime tag 0 = integer @@ -629,6 +640,17 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rax, 1"); // report true for array-like values emitter.instruction("ret"); // return the boolean result to Rust + label_c_global(emitter, "__elephc_eval_value_is_null"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // pass the boxed Mixed argument to mixed_unbox + emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells to a concrete runtime tag + emitter.instruction("cmp rax, 8"); // runtime tag 8 means PHP null + emitter.instruction("sete al"); // set the low byte when the tag is null + emitter.instruction("movzx eax, al"); // widen the C boolean result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boolean result to Rust + label_c_global(emitter, "__elephc_eval_value_int"); emitter.instruction("mov eax, 0"); // runtime tag 0 = integer emitter.instruction("xor esi, esi"); // integer payloads do not use a high word diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b3b0d4ad6b..cecea070f1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -397,6 +397,26 @@ eval('echo STRLEN("abcd") . ":" . count([1, 2, 3]);'); assert_eq!(out, "4:3"); } +/// Verifies eval `isset()` distinguishes missing, null, and falsey non-null values. +#[test] +fn test_eval_isset_distinguishes_missing_null_and_falsey_values() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 19:34:07 +0200 Subject: [PATCH 0029/1208] Support empty inside eval fragments --- crates/elephc-eval/src/interpreter.rs | 63 +++++++++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 17 ++++++++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + tests/codegen/eval.rs | 22 ++++++++++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 085d22c29f..2718050a1e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -507,6 +507,7 @@ fn eval_call( eval_builtin_call_user_func_array(args, context, scope, values) } "count" => eval_builtin_count(args, context, scope, values), + "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) @@ -560,6 +561,37 @@ fn eval_builtin_isset( values.bool_value(true) } +/// Evaluates PHP's `empty(...)` language construct over eval-visible values. +fn eval_builtin_empty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = eval_empty_arg(arg, context, scope, values)?; + values.bool_value(empty) +} + +/// Evaluates one `empty` operand without warning or failing on missing variables. +fn eval_empty_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = scope.visible_cell(name) else { + return Ok(true); + }; + return Ok(!values.truthy(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.truthy(value)?) +} + /// Evaluates one `isset` operand without allocating a null cell for missing variables. fn eval_isset_arg( arg: &EvalExpr, @@ -1918,6 +1950,37 @@ if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; }"#, assert_eq!(values.get(result), FakeValue::Null); } + /// Verifies `empty` treats missing, null, and falsey values as empty. + #[test] + fn execute_program_empty_uses_php_truthiness_without_missing_warnings() { + let program = parse_fragment( + br#"if (empty($missing)) { echo "1"; } else { echo "0"; } +if (empty($nullish)) { echo "1"; } else { echo "0"; } +if (empty($zero)) { echo "1"; } else { echo "0"; } +if (empty($empty_string)) { echo "1"; } else { echo "0"; } +if (empty($zero_string)) { echo "1"; } else { echo "0"; } +if (empty($value)) { echo "1"; } else { echo "0"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty_string = values.string("").expect("create fake empty string"); + let zero_string = values.string("0").expect("create fake zero string"); + let value = values.string("x").expect("create fake non-empty string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty_string", empty_string, ScopeCellOwnership::Owned); + scope.set("zero_string", zero_string, ScopeCellOwnership::Owned); + scope.set("value", value, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111110"); + assert_eq!(values.get(result), FakeValue::Null); + } + /// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. #[test] fn execute_program_function_probes_use_eval_context() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 2879fd9158..e40db24239 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1324,6 +1324,23 @@ mod tests { ); } + /// Verifies `empty` parses as a case-insensitive function-like expression. + #[test] + fn parse_fragment_accepts_empty_source() { + let program = parse_fragment(br#"return EMPTY($items["k"]);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "empty".to_string(), + args: vec![EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("items".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), + }], + }))] + ); + } + /// Verifies indexed array literals and reads parse as runtime array expressions. #[test] fn parse_fragment_accepts_indexed_array_read_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 599ce7d00f..7195010bf8 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b6f8218afe..87ea95b399 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -41,6 +41,7 @@ public function echoLabelThroughEval(): void { eval('if ($x < 0) { echo "negative\n"; } elseif ($x == 3) { echo "x==3\n"; }'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); +eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); $meta = eval('return ["source" => "eval"];'); $meta_count = eval('return count($meta);'); eval('function plus_one($value) { return $value + 1; }'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cecea070f1..feb5095f83 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -417,6 +417,28 @@ echo function_exists("isset") . "x";'); assert_eq!(out, "001110x"); } +/// Verifies eval `empty()` uses PHP truthiness without warning on missing variables. +#[test] +fn test_eval_empty_uses_php_truthiness_without_missing_warnings() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 19:45:10 +0200 Subject: [PATCH 0030/1208] Support eval magic line and function constants --- crates/elephc-eval/src/context.rs | 18 +++ crates/elephc-eval/src/eval_ir.rs | 26 +++- crates/elephc-eval/src/interpreter.rs | 194 ++++++++++++++++---------- crates/elephc-eval/src/lib.rs | 17 +-- crates/elephc-eval/src/parser.rs | 90 +++++++++--- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 7 + tests/codegen/eval.rs | 14 ++ 8 files changed, 257 insertions(+), 111 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index a12400dd75..6016524019 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -68,6 +68,7 @@ pub struct ElephcEvalContext { abi_version: u32, functions: HashMap, native_functions: HashMap, + function_stack: Vec, } impl ElephcEvalContext { @@ -77,6 +78,7 @@ impl ElephcEvalContext { abi_version: ABI_VERSION, functions: HashMap::new(), native_functions: HashMap::new(), + function_stack: Vec::new(), } } @@ -87,6 +89,7 @@ impl ElephcEvalContext { abi_version, functions: HashMap::new(), native_functions: HashMap::new(), + function_stack: Vec::new(), } } @@ -137,6 +140,21 @@ impl ElephcEvalContext { pub fn has_function(&self, name: &str) -> bool { self.functions.contains_key(name) || self.native_functions.contains_key(name) } + + /// Pushes an eval-executed function name for magic-constant resolution. + pub fn push_function(&mut self, name: impl Into) { + self.function_stack.push(name.into()); + } + + /// Pops the current eval-executed function name after its body completes. + pub fn pop_function(&mut self) { + self.function_stack.pop(); + } + + /// Returns the current eval-executed function name, if execution is inside one. + pub fn current_function(&self) -> Option<&str> { + self.function_stack.last().map(String::as_str) + } } impl Default for ElephcEvalContext { diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 08385c7153..bcbfacbd51 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -98,14 +98,24 @@ pub enum EvalStmt { /// Runtime user function declared by an eval fragment. #[derive(Debug, Clone, PartialEq)] pub struct EvalFunction { + name: String, params: Vec, body: Vec, } impl EvalFunction { /// Creates a dynamic eval function with source-order parameters and body. - pub fn new(params: Vec, body: Vec) -> Self { - Self { params, body } + pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { + Self { + name: name.into(), + params, + body, + } + } + + /// Returns the original source spelling of this eval-declared function name. + pub fn name(&self) -> &str { + &self.name } /// Returns source-order parameter names without leading `$`. @@ -138,6 +148,7 @@ pub enum EvalExpr { method: String, args: Vec, }, + Magic(EvalMagicConst), PropertyGet { object: Box, property: String, @@ -171,6 +182,17 @@ pub enum EvalConst { String(String), } +/// PHP magic constants supported by runtime eval fragments. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalMagicConst { + Line(i64), + Function, + Class, + Method, + Namespace, + Trait, +} + /// Binary operations supported by the initial EvalIR parser. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EvalBinOp { diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2718050a1e..67edae83f0 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -11,11 +11,11 @@ //! - This module does not own PHP values. Constants and operations are delegated //! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. -use crate::errors::EvalStatus; use crate::context::{ElephcEvalContext, NativeFunction}; +use crate::errors::EvalStatus; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalFunction, EvalProgram, EvalStmt, - EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, EvalProgram, + EvalStmt, EvalUnaryOp, }; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership}; @@ -254,15 +254,27 @@ fn execute_stmt( condition, update, body, - } => execute_for_stmt(init, condition.as_ref(), update, body, context, scope, values), + } => execute_for_stmt( + init, + condition.as_ref(), + update, + body, + context, + scope, + values, + ), EvalStmt::Foreach { array, value_name, body, } => execute_foreach_stmt(array, value_name, body, context, scope, values), EvalStmt::FunctionDecl { name, params, body } => { + let key = name.to_ascii_lowercase(); context - .define_function(name.clone(), EvalFunction::new(params.clone(), body.clone())) + .define_function( + key, + EvalFunction::new(name.clone(), params.clone(), body.clone()), + ) .map_err(|_| EvalStatus::RuntimeFatal)?; Ok(EvalControl::None) } @@ -278,9 +290,9 @@ fn execute_stmt( execute_statements(else_branch, context, scope, values) } } - EvalStmt::Return(Some(expr)) => { - Ok(EvalControl::Return(eval_expr(expr, context, scope, values)?)) - } + EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr( + expr, context, scope, values, + )?)), EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), EvalStmt::PropertySet { object, @@ -375,8 +387,7 @@ fn execute_foreach_stmt( for index in 0..len { let index = values.int(index as i64)?; let value = values.array_get(array, index)?; - if let Some(replaced) = - scope.set(value_name.to_string(), value, ScopeCellOwnership::Owned) + if let Some(replaced) = scope.set(value_name.to_string(), value, ScopeCellOwnership::Owned) { values.release(replaced)?; } @@ -415,6 +426,7 @@ fn eval_expr( EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::LoadVar(name) => scope.visible_cell(name).map_or_else(|| values.null(), Ok), + EvalExpr::Magic(magic) => eval_magic_const(magic, context, values), EvalExpr::MethodCall { object, method, @@ -503,9 +515,7 @@ fn eval_call( ) -> Result { match name { "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), - "call_user_func_array" => { - eval_builtin_call_user_func_array(args, context, scope, values) - } + "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "count" => eval_builtin_count(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), @@ -611,8 +621,7 @@ fn eval_isset_arg( /// Returns true when a PHP function name is visible to eval builtin probes. fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { - !name.contains("::") - && (context.has_function(name) || eval_php_visible_builtin_exists(name)) + !name.contains("::") && (context.has_function(name) || eval_php_visible_builtin_exists(name)) } /// Returns true for PHP-visible builtin names implemented by the eval interpreter. @@ -845,7 +854,10 @@ fn eval_dynamic_function_with_values( for (name, value) in function.params().iter().zip(evaluated_args) { function_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); } - match execute_statements(function.body(), context, &mut function_scope, values)? { + context.push_function(function.name()); + let result = execute_statements(function.body(), context, &mut function_scope, values); + context.pop_function(); + match result? { EvalControl::None => values.null(), EvalControl::Return(result) => Ok(result), EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), @@ -952,6 +964,22 @@ fn eval_const( } } +/// Resolves one eval magic constant against fragment and dynamic-call metadata. +fn eval_magic_const( + magic: &EvalMagicConst, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match magic { + EvalMagicConst::Line(line) => values.int(*line), + EvalMagicConst::Function => values.string(context.current_function().unwrap_or("")), + EvalMagicConst::Class + | EvalMagicConst::Method + | EvalMagicConst::Namespace + | EvalMagicConst::Trait => values.string(""), + } +} + /// Returns the current interpreter availability status for the ABI stub. pub fn current_stub_status() -> EvalStatus { EvalStatus::UnsupportedConstruct @@ -1097,9 +1125,10 @@ mod tests { property: &str, ) -> Result { match self.get(object) { - FakeValue::Object(properties) => { - properties.get(property).copied().map_or_else(|| self.null(), Ok) - } + FakeValue::Object(properties) => properties + .get(property) + .copied() + .map_or_else(|| self.null(), Ok), _ => Err(EvalStatus::UnsupportedConstruct), } } @@ -1138,7 +1167,10 @@ mod tests { let [arg] = args.as_slice() else { return Err(EvalStatus::UnsupportedConstruct); }; - let x = properties.get("x").copied().ok_or(EvalStatus::RuntimeFatal)?; + let x = properties + .get("x") + .copied() + .ok_or(EvalStatus::RuntimeFatal)?; let FakeValue::Int(x) = self.get(x) else { return Err(EvalStatus::UnsupportedConstruct); }; @@ -1151,7 +1183,10 @@ mod tests { let [left, right] = args.as_slice() else { return Err(EvalStatus::UnsupportedConstruct); }; - let x = properties.get("x").copied().ok_or(EvalStatus::RuntimeFatal)?; + let x = properties + .get("x") + .copied() + .ok_or(EvalStatus::RuntimeFatal)?; let FakeValue::Int(x) = self.get(x) else { return Err(EvalStatus::UnsupportedConstruct); }; @@ -1450,9 +1485,8 @@ mod tests { /// Verifies eval property reads and writes dispatch through runtime hooks. #[test] fn execute_program_reads_and_writes_object_property() { - let program = - parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) - .expect("parse eval fragment"); + let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) + .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let x = values.int(1).expect("create fake int"); @@ -1640,8 +1674,7 @@ mod tests { /// Verifies logical OR skips an unsupported right-hand expression after a true left side. #[test] fn execute_program_short_circuits_logical_or() { - let program = - parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); + let program = parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -1653,8 +1686,7 @@ mod tests { /// Verifies logical negation returns boolean cells using PHP truthiness. #[test] fn execute_program_evaluates_logical_not() { - let program = - parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); + let program = parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -1672,8 +1704,7 @@ mod tests { let x = values.int(5).expect("create fake int"); scope.set("x", x, ScopeCellOwnership::Owned); - let result = - execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); assert_eq!(values.get(result), FakeValue::Int(-3)); } @@ -1681,9 +1712,8 @@ mod tests { /// Verifies foreach assigns each indexed element to the value variable. #[test] fn execute_program_foreach_iterates_indexed_values() { - let program = - parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) - .expect("parse eval fragment"); + let program = parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) + .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -1751,6 +1781,18 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies `__LINE__` inside eval uses the source line within the fragment. + #[test] + fn execute_program_magic_line_uses_fragment_line() { + let program = parse_fragment(b"\nreturn __LINE__;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + } + /// Verifies eval-declared functions can be called by the same fragment. #[test] fn execute_program_calls_declared_function() { @@ -1764,6 +1806,24 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies `__FUNCTION__` inside an eval-declared function keeps the declaration spelling. + #[test] + fn execute_program_magic_function_uses_eval_declared_name() { + let program = parse_fragment( + br#"function DynMagicCase() { return __FUNCTION__; } return dynmagiccase();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("DynMagicCase".to_string()) + ); + } + /// Verifies eval-declared functions persist in a shared eval context. #[test] fn execute_program_context_keeps_declared_function() { @@ -1820,16 +1880,11 @@ return call_user_func("dyn", 4);"#, let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let expected = values.int(42).expect("allocate fake result"); - let native = NativeFunction::new( - expected.as_ptr().cast(), - fake_native_return_descriptor, - 0, - ); - assert!( - context - .define_native_function("native_answer", native) - .is_ok() - ); + let native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) .expect("execute eval ir"); @@ -1869,23 +1924,17 @@ return call_user_func_array("dyn", [4, 5]);"#, /// Verifies `call_user_func_array` inside eval can dispatch a registered native function. #[test] fn execute_program_call_user_func_array_dispatches_registered_native_function() { - let program = - parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) - .expect("parse eval fragment"); + let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) + .expect("parse eval fragment"); let mut context = ElephcEvalContext::new(); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let expected = values.int(42).expect("allocate fake result"); - let native = NativeFunction::new( - expected.as_ptr().cast(), - fake_native_return_descriptor, - 2, - ); - assert!( - context - .define_native_function("native_answer", native) - .is_ok() - ); + let native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) .expect("execute eval ir"); @@ -1896,7 +1945,8 @@ return call_user_func_array("dyn", [4, 5]);"#, /// Verifies duplicate eval-declared function names fail in a shared context. #[test] fn execute_program_rejects_duplicate_declared_function() { - let define = parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); + let define = + parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); let mut context = ElephcEvalContext::new(); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -1912,9 +1962,8 @@ return call_user_func_array("dyn", [4, 5]);"#, /// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. #[test] fn execute_program_dispatches_simple_builtins() { - let program = - parse_fragment(br#"return strlen("abc") + count([1, 2, 3]);"#) - .expect("parse eval fragment"); + let program = parse_fragment(br#"return strlen("abc") + count([1, 2, 3]);"#) + .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -1994,13 +2043,11 @@ echo function_exists("eval") . "x"; echo function_exists("missing_probe") . "x";"#, ) .expect("parse eval fragment"); - let native = NativeFunction::new( - 1usize as *mut c_void, - fake_native_return_descriptor, - 0, - ); + let native = NativeFunction::new(1usize as *mut c_void, fake_native_return_descriptor, 0); let mut context = ElephcEvalContext::new(); - assert!(context.define_native_function("native_probe", native).is_ok()); + assert!(context + .define_native_function("native_probe", native) + .is_ok()); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -2018,16 +2065,11 @@ echo function_exists("missing_probe") . "x";"#, let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let expected = values.int(42).expect("allocate fake result"); - let native = NativeFunction::new( - expected.as_ptr().cast(), - fake_native_return_descriptor, - 0, - ); - assert!( - context - .define_native_function("native_answer", native) - .is_ok() - ); + let native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) .expect("execute eval ir"); diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 505aa9f718..81c52e8d12 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -217,10 +217,8 @@ pub unsafe extern "C" fn __elephc_eval_function_exists( name_ptr: *const u8, name_len: u64, ) -> i32 { - std::panic::catch_unwind(|| unsafe { - eval_function_exists_inner(ctx, name_ptr, name_len) - }) - .unwrap_or(0) + std::panic::catch_unwind(|| unsafe { eval_function_exists_inner(ctx, name_ptr, name_len) }) + .unwrap_or(0) } /// Registers a generated native PHP function callback in an eval context. @@ -607,7 +605,7 @@ mod tests { let mut ctx = ElephcEvalContext::new(); ctx.define_function( "dyn_probe", - crate::eval_ir::EvalFunction::new(Vec::new(), Vec::new()), + crate::eval_ir::EvalFunction::new("dyn_probe", Vec::new(), Vec::new()), ) .expect("first dynamic function declaration should succeed"); let existing = b"DYN_PROBE"; @@ -616,9 +614,8 @@ mod tests { let existing_result = unsafe { __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; - let missing_result = unsafe { - __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) - }; + let missing_result = + unsafe { __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; assert_eq!(existing_result, 1); assert_eq!(missing_result, 0); @@ -641,9 +638,7 @@ mod tests { 0, ) }; - let exists = unsafe { - __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) - }; + let exists = unsafe { __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) }; assert_eq!(registered, 1); assert_eq!(exists, 1); diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index e40db24239..be74ec9a39 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -14,7 +14,8 @@ use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalProgram, EvalStmt, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalMagicConst, EvalProgram, EvalStmt, + EvalUnaryOp, }; /// Parses an eval fragment into by-name EvalIR statements. @@ -37,6 +38,7 @@ fn contains_php_open_tag(code: &[u8]) -> bool { enum TokenKind { DollarIdent(String), Ident(String), + Magic(EvalMagicConst), Int(i64), Float(f64), String(String), @@ -71,12 +73,17 @@ enum TokenKind { struct Lexer<'a> { source: &'a str, pos: usize, + line: i64, } impl<'a> Lexer<'a> { /// Creates a lexer over a UTF-8 eval fragment. fn new(source: &'a str) -> Self { - Self { source, pos: 0 } + Self { + source, + pos: 0, + line: 1, + } } /// Tokenizes the complete source and appends an EOF sentinel. @@ -99,6 +106,7 @@ impl<'a> Lexer<'a> { let Some(ch) = self.peek_char() else { return Ok(TokenKind::Eof); }; + let line = self.line; match ch { '$' => self.lex_variable(), '\'' | '"' => self.lex_string(ch), @@ -213,7 +221,10 @@ impl<'a> Lexer<'a> { self.bump_char(); Ok(TokenKind::Comma) } - _ if is_ident_start(ch) => Ok(TokenKind::Ident(self.lex_ident())), + _ if is_ident_start(ch) => { + let ident = self.lex_ident(); + Ok(magic_const_token(&ident, line).unwrap_or(TokenKind::Ident(ident))) + } _ => Err(EvalParseError::UnexpectedToken), } } @@ -320,6 +331,9 @@ impl<'a> Lexer<'a> { fn bump_char(&mut self) { if let Some(ch) = self.peek_char() { self.pos += ch.len_utf8(); + if ch == '\n' { + self.line += 1; + } } } } @@ -371,9 +385,7 @@ impl Parser { } TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), - TokenKind::Ident(name) if ident_eq(name, "function") => { - self.parse_function_decl_stmt() - } + TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), TokenKind::Ident(name) if ident_eq(name, "return") => { self.advance(); @@ -472,7 +484,7 @@ impl Parser { let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; - let name = name.to_ascii_lowercase(); + let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; let params = self.parse_function_params()?; @@ -879,6 +891,11 @@ impl Parser { self.advance(); Ok(EvalExpr::LoadVar(name)) } + TokenKind::Magic(magic) => { + let magic = magic.clone(); + self.advance(); + Ok(EvalExpr::Magic(magic)) + } TokenKind::Ident(name) if ident_eq(name, "null") => { self.advance(); Ok(EvalExpr::Const(EvalConst::Null)) @@ -1028,6 +1045,26 @@ fn is_ident_continue(ch: char) -> bool { is_ident_start(ch) || ch.is_ascii_digit() } +/// Converts a PHP magic-constant identifier into a parser token when recognized. +fn magic_const_token(name: &str, line: i64) -> Option { + let magic = if ident_eq(name, "__LINE__") { + EvalMagicConst::Line(line) + } else if ident_eq(name, "__FUNCTION__") { + EvalMagicConst::Function + } else if ident_eq(name, "__CLASS__") { + EvalMagicConst::Class + } else if ident_eq(name, "__METHOD__") { + EvalMagicConst::Method + } else if ident_eq(name, "__NAMESPACE__") { + EvalMagicConst::Namespace + } else if ident_eq(name, "__TRAIT__") { + EvalMagicConst::Trait + } else { + return None; + }; + Some(TokenKind::Magic(magic)) +} + /// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. fn ident_eq(actual: &str, expected: &str) -> bool { actual.eq_ignore_ascii_case(expected) @@ -1089,9 +1126,8 @@ mod tests { /// Verifies elseif fragments lower to nested if statements in the else branch. #[test] fn parse_fragment_accepts_elseif_source() { - let program = - parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; }"#) - .expect("fragment should parse"); + let program = parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; }"#) + .expect("fragment should parse"); assert_eq!( program.statements(), &[EvalStmt::If { @@ -1115,9 +1151,8 @@ mod tests { /// Verifies PHP's `else if` spelling follows the same nested branch shape. #[test] fn parse_fragment_accepts_else_if_source() { - let program = - parse_fragment(br#"if ($a) { $x = "a"; } else if ($b) { $x = "b"; }"#) - .expect("fragment should parse"); + let program = parse_fragment(br#"if ($a) { $x = "a"; } else if ($b) { $x = "b"; }"#) + .expect("fragment should parse"); assert!(matches!( program.statements(), @@ -1188,6 +1223,21 @@ mod tests { ); } + /// Verifies eval magic constants lower to explicit EvalIR nodes with fragment line metadata. + #[test] + fn parse_fragment_accepts_magic_constants() { + let program = + parse_fragment(b"\nreturn __line__ . __FUNCTION__;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Magic(EvalMagicConst::Line(2))), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Function)), + }))] + ); + } + /// Verifies comparison operators parse with lower precedence than arithmetic. #[test] fn parse_fragment_accepts_comparison_source() { @@ -1242,8 +1292,7 @@ mod tests { /// Verifies logical negation parses as a unary expression before comparisons. #[test] fn parse_fragment_accepts_logical_not_source() { - let program = - parse_fragment(br#"return !$flag == true;"#).expect("fragment should parse"); + let program = parse_fragment(br#"return !$flag == true;"#).expect("fragment should parse"); assert_eq!( program.statements(), &[EvalStmt::Return(Some(EvalExpr::Binary { @@ -1260,8 +1309,7 @@ mod tests { /// Verifies unary numeric operators bind tighter than multiplication. #[test] fn parse_fragment_accepts_unary_numeric_source() { - let program = - parse_fragment(br#"return -$x * +2;"#).expect("fragment should parse"); + let program = parse_fragment(br#"return -$x * +2;"#).expect("fragment should parse"); assert_eq!( program.statements(), &[EvalStmt::Return(Some(EvalExpr::Binary { @@ -1307,8 +1355,8 @@ mod tests { /// Verifies `isset` parses as a case-insensitive function-like expression. #[test] fn parse_fragment_accepts_isset_source() { - let program = parse_fragment(br#"return ISSET($x, $items["k"]);"#) - .expect("fragment should parse"); + let program = + parse_fragment(br#"return ISSET($x, $items["k"]);"#).expect("fragment should parse"); assert_eq!( program.statements(), &[EvalStmt::Return(Some(EvalExpr::Call { @@ -1327,8 +1375,8 @@ mod tests { /// Verifies `empty` parses as a case-insensitive function-like expression. #[test] fn parse_fragment_accepts_empty_source() { - let program = parse_fragment(br#"return EMPTY($items["k"]);"#) - .expect("fragment should parse"); + let program = + parse_fragment(br#"return EMPTY($items["k"]);"#).expect("fragment should parse"); assert_eq!( program.statements(), &[EvalStmt::Return(Some(EvalExpr::Call { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7195010bf8..cf2a62d8b2 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval `__FILE__`, `__DIR__`, and caller class/namespace metadata are still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 87ea95b399..b4f604c786 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -52,6 +52,11 @@ public function echoLabelThroughEval(): void { $logic = eval('return true || missing_eval_rhs();'); $not_false = eval('return !false;'); $negative = eval('return -5 + +2;'); +$magic_line = eval(" +return __LINE__; +"); +eval('function EvalMagicName() { return __FUNCTION__; }'); +$magic_function = eval('return evalmagicname();'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -67,6 +72,8 @@ public function echoLabelThroughEval(): void { echo "logic=" . $logic . "\n"; echo "not-false=" . $not_false . "\n"; echo "negative=" . $negative . "\n"; +echo "magic-line=" . $magic_line . "\n"; +echo "magic-function=" . $magic_function . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index feb5095f83..0989c874b2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -462,6 +462,20 @@ echo eval('function dyn_eval_add($x) { return $x + 1; } return dyn_eval_add(4);' assert_eq!(out, "5"); } +/// Verifies eval magic constants use fragment line and eval-declared function metadata. +#[test] +fn test_eval_magic_line_and_function_execute_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 20:01:57 +0200 Subject: [PATCH 0031/1208] Support eval file and dir magic constants --- crates/elephc-eval/src/abi.rs | 2 +- crates/elephc-eval/src/context.rs | 29 +++++++++ crates/elephc-eval/src/eval_ir.rs | 2 + crates/elephc-eval/src/interpreter.rs | 21 ++++++ crates/elephc-eval/src/lib.rs | 75 +++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 23 ++++++- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 4 ++ src/codegen/lower_inst/builtins/eval.rs | 86 +++++++++++++++++++++---- src/ir/module.rs | 2 + src/ir_lower/mod.rs | 13 +++- src/ir_lower/program.rs | 12 ++++ src/pipeline.rs | 14 +++- tests/codegen/eval.rs | 13 ++++ tests/codegen/support/compiler.rs | 11 +++- 15 files changed, 286 insertions(+), 23 deletions(-) diff --git a/crates/elephc-eval/src/abi.rs b/crates/elephc-eval/src/abi.rs index e76b6dca6a..f53a4eee90 100644 --- a/crates/elephc-eval/src/abi.rs +++ b/crates/elephc-eval/src/abi.rs @@ -17,7 +17,7 @@ pub use crate::context::ElephcEvalContext; pub use crate::scope::ElephcEvalScope; /// ABI version shared by generated call sites and the eval bridge. -pub const ABI_VERSION: u32 = 1; +pub const ABI_VERSION: u32 = 2; /// Scope-entry ABI flag indicating that a variable has a visible value. pub const SCOPE_FLAG_PRESENT: u32 = 1 << 0; diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 6016524019..1fcc168a41 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -69,6 +69,9 @@ pub struct ElephcEvalContext { functions: HashMap, native_functions: HashMap, function_stack: Vec, + call_file: String, + call_dir: String, + call_line: i64, } impl ElephcEvalContext { @@ -79,6 +82,9 @@ impl ElephcEvalContext { functions: HashMap::new(), native_functions: HashMap::new(), function_stack: Vec::new(), + call_file: String::new(), + call_dir: String::new(), + call_line: 0, } } @@ -90,6 +96,9 @@ impl ElephcEvalContext { functions: HashMap::new(), native_functions: HashMap::new(), function_stack: Vec::new(), + call_file: String::new(), + call_dir: String::new(), + call_line: 0, } } @@ -155,6 +164,26 @@ impl ElephcEvalContext { pub fn current_function(&self) -> Option<&str> { self.function_stack.last().map(String::as_str) } + + /// Updates the source file, directory, and line for the current eval call site. + pub fn set_call_site(&mut self, file: impl Into, dir: impl Into, line: i64) { + self.call_file = file.into(); + self.call_dir = dir.into(); + self.call_line = line; + } + + /// Returns the source directory associated with the current eval call site. + pub fn call_dir(&self) -> &str { + &self.call_dir + } + + /// Returns PHP's `__FILE__` string for code currently running inside eval. + pub fn eval_file_magic(&self) -> String { + if self.call_file.is_empty() { + return String::new(); + } + format!("{}({}) : eval()'d code", self.call_file, self.call_line) + } } impl Default for ElephcEvalContext { diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index bcbfacbd51..3ba805d06e 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -185,6 +185,8 @@ pub enum EvalConst { /// PHP magic constants supported by runtime eval fragments. #[derive(Debug, Clone, PartialEq)] pub enum EvalMagicConst { + File, + Dir, Line(i64), Function, Class, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 67edae83f0..a087d0d762 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -971,6 +971,8 @@ fn eval_magic_const( values: &mut impl RuntimeValueOps, ) -> Result { match magic { + EvalMagicConst::File => values.string(&context.eval_file_magic()), + EvalMagicConst::Dir => values.string(context.call_dir()), EvalMagicConst::Line(line) => values.int(*line), EvalMagicConst::Function => values.string(context.current_function().unwrap_or("")), EvalMagicConst::Class @@ -1793,6 +1795,25 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(2)); } + /// Verifies file-dependent eval magic constants use call-site metadata from the context. + #[test] + fn execute_program_magic_file_and_dir_use_context_call_site() { + let program = + parse_fragment(br#"return __FILE__ . "|" . __DIR__;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/main.php", "/tmp", 17); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("/tmp/main.php(17) : eval()'d code|/tmp".to_string()) + ); + } + /// Verifies eval-declared functions can be called by the same fragment. #[test] fn execute_program_calls_declared_function() { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 81c52e8d12..bdaa5ddb48 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -65,6 +65,26 @@ pub unsafe extern "C" fn __elephc_eval_context_free(ctx: *mut ElephcEvalContext) } } +/// Records source metadata for the next eval fragment executed in this context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `file_ptr` and `dir_ptr` must be +/// readable for their matching lengths when the length is greater than zero. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_set_call_site( + ctx: *mut ElephcEvalContext, + file_ptr: *const u8, + file_len: u64, + dir_ptr: *const u8, + dir_len: u64, + line: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_context_set_call_site_inner(ctx, file_ptr, file_len, dir_ptr, dir_len, line) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Allocates a materialized activation scope handle for generated code. #[no_mangle] pub extern "C" fn __elephc_eval_scope_new() -> *mut ElephcEvalScope { @@ -341,6 +361,38 @@ unsafe fn register_native_function_inner( ) } +/// Runs the call-site metadata setter ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_set_call_site`; callers must pass a valid +/// context and readable UTF-8 file/directory byte slices. +unsafe fn eval_context_set_call_site_inner( + ctx: *mut ElephcEvalContext, + file_ptr: *const u8, + file_len: u64, + dir_ptr: *const u8, + dir_len: u64, + line: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(file) = abi_name_to_string(file_ptr, file_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(dir) = abi_name_to_string(dir_ptr, dir_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(line) = i64::try_from(line) else { + return EvalStatus::RuntimeFatal.code(); + }; + context.set_call_site(file, dir, line); + EvalStatus::Ok.code() +} + /// Runs the dynamic function-call ABI body after installing a panic boundary. /// /// # Safety @@ -599,6 +651,29 @@ mod tests { assert_eq!(version, ABI_VERSION); } + /// Verifies call-site metadata can be set through the stable context ABI. + #[test] + fn context_set_call_site_records_file_dir_and_line() { + let mut ctx = ElephcEvalContext::new(); + let file = b"/tmp/source.php"; + let dir = b"/tmp"; + + let status = unsafe { + __elephc_eval_context_set_call_site( + &mut ctx, + file.as_ptr(), + file.len() as u64, + dir.as_ptr(), + dir.len() as u64, + 9, + ) + }; + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!(ctx.call_dir(), "/tmp"); + assert_eq!(ctx.eval_file_magic(), "/tmp/source.php(9) : eval()'d code"); + } + /// Verifies the function-exists ABI probes eval-declared functions by folded name. #[test] fn function_exists_reports_declared_eval_function() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index be74ec9a39..a2209d3ad7 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -10,7 +10,8 @@ //! Key details: //! - PHP eval fragments are statement fragments and must not include opening //! ` bool { /// Converts a PHP magic-constant identifier into a parser token when recognized. fn magic_const_token(name: &str, line: i64) -> Option { - let magic = if ident_eq(name, "__LINE__") { + let magic = if ident_eq(name, "__FILE__") { + EvalMagicConst::File + } else if ident_eq(name, "__DIR__") { + EvalMagicConst::Dir + } else if ident_eq(name, "__LINE__") { EvalMagicConst::Line(line) } else if ident_eq(name, "__FUNCTION__") { EvalMagicConst::Function @@ -1238,6 +1243,20 @@ mod tests { ); } + /// Verifies file-dependent eval magic constants lower to EvalIR nodes. + #[test] + fn parse_fragment_accepts_file_magic_constants() { + let program = parse_fragment(b"return __FILE__ . __dir__;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Magic(EvalMagicConst::File)), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Dir)), + }))] + ); + } + /// Verifies comparison operators parse with lower precedence than arithmetic. #[test] fn parse_fragment_accepts_comparison_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index cf2a62d8b2..9fb4dfa6e9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval `__FILE__`, `__DIR__`, and caller class/namespace metadata are still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b4f604c786..79c4289ce9 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -57,6 +57,8 @@ public function echoLabelThroughEval(): void { "); eval('function EvalMagicName() { return __FUNCTION__; }'); $magic_function = eval('return evalmagicname();'); +$magic_file_has_path = eval('return strlen(__FILE__) > strlen(__DIR__);'); +$magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -74,6 +76,8 @@ public function echoLabelThroughEval(): void { echo "negative=" . $negative . "\n"; echo "magic-line=" . $magic_line . "\n"; echo "magic-function=" . $magic_function . "\n"; +echo "magic-file=" . $magic_file_has_path . "\n"; +echo "magic-dir=" . $magic_dir_has_path . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index f067607e4b..d42caf7c83 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -12,6 +12,8 @@ //! lowering; this module only materializes the bridge ABI call. //! - The bridge is target-mangled like other C staticlib symbols. +use std::path::Path; + use crate::codegen::platform::Arch; use crate::codegen::{ abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, @@ -71,6 +73,7 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); save_eval_code_string(ctx); ensure_eval_context(ctx)?; + set_eval_call_site(ctx, inst); ensure_eval_scope(ctx)?; let sync_locals = eval_sync_locals(ctx); flush_eval_scope_locals(ctx, &sync_locals)?; @@ -91,6 +94,49 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R store_if_result(ctx, inst) } +/// Updates eval context source metadata for file, directory, and call-site line magic constants. +fn set_eval_call_site(ctx: &mut FunctionContext<'_>, inst: &Instruction) { + let Some(source_path) = ctx.module.source_path.as_deref() else { + return; + }; + load_eval_context_to_arg(ctx, 0); + let (file_label, file_len) = ctx.data.add_string(source_path.as_bytes()); + let file_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, file_arg, &file_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + file_len as i64, + ); + let dir = Path::new(source_path) + .parent() + .map(|path| path.display().to_string()) + .unwrap_or_default(); + let (dir_label, dir_len) = ctx.data.add_string(dir.as_bytes()); + let dir_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_symbol_address(ctx.emitter, dir_arg, &dir_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + dir_len as i64, + ); + let line = inst + .span + .and_then(|span| i64::try_from(span.line).ok()) + .unwrap_or(0); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + line, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_set_call_site"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + /// Lowers a native call to a zero-argument function declared by a prior `eval()` call. pub(super) fn lower_eval_function_call( ctx: &mut FunctionContext<'_>, @@ -124,7 +170,10 @@ pub(super) fn lower_eval_function_call( ); let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_call_function"); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_call_function"); abi::emit_call_label(ctx.emitter, &symbol); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); @@ -150,7 +199,10 @@ pub(super) fn lower_eval_function_exists( abi::int_arg_reg_name(ctx.emitter.target, 2), name_len as i64, ); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_function_exists"); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_function_exists"); abi::emit_call_label(ctx.emitter, &symbol); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); store_if_result(ctx, inst) @@ -194,7 +246,10 @@ fn ensure_eval_context(ctx: &mut FunctionContext<'_>) -> Result<()> { let result_reg = abi::int_result_reg(ctx.emitter); abi::load_at_offset(ctx.emitter, result_reg, offset); abi::emit_branch_if_int_result_nonzero(ctx.emitter, &ready); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_context_new"); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_new"); abi::emit_call_label(ctx.emitter, &symbol); abi::store_at_offset(ctx.emitter, result_reg, offset); register_eval_native_functions(ctx, offset)?; @@ -245,7 +300,10 @@ fn eval_native_function_registrations( fn function_can_register_with_eval(function: &Function) -> bool { !function.flags.is_main && !function.name.starts_with('_') - && function.params.iter().all(|param| !param.by_ref && !param.variadic) + && function + .params + .iter() + .all(|param| !param.by_ref && !param.variadic) } /// Emits one native-function registration call into the just-created eval context. @@ -488,12 +546,12 @@ fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local } } } @@ -560,12 +618,12 @@ fn emit_retain_scope_cell_if_owned(ctx: &mut FunctionContext<'_>) { Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining } } abi::emit_call_label(ctx.emitter, "__rt_incref"); @@ -637,12 +695,12 @@ fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: Arch::AArch64 => { ctx.emitter .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler } Arch::X86_64 => { ctx.emitter .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler } } } @@ -652,7 +710,7 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr + ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); ctx.emitter @@ -661,12 +719,12 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { abi::emit_exit(ctx.emitter, 1); } Arch::X86_64 => { - ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr + ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); ctx.emitter .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length - ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting abi::emit_exit(ctx.emitter, 1); } } diff --git a/src/ir/module.rs b/src/ir/module.rs index ddee0b271b..62f9f34db3 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -39,6 +39,7 @@ impl DataId { #[derive(Debug, Clone)] pub struct Module { pub target: Target, + pub source_path: Option, pub functions: Vec, pub class_methods: Vec, pub closures: Vec, @@ -72,6 +73,7 @@ impl Module { pub fn new(target: Target) -> Self { Self { target, + source_path: None, functions: Vec::new(), class_methods: Vec::new(), closures: Vec::new(), diff --git a/src/ir_lower/mod.rs b/src/ir_lower/mod.rs index 7d43f8adc9..c3e83c72a0 100644 --- a/src/ir_lower/mod.rs +++ b/src/ir_lower/mod.rs @@ -24,6 +24,7 @@ mod stmt; mod tests; use std::fmt; +use std::path::Path; use crate::codegen::platform::Target; use crate::ir::{Module, ValidationError}; @@ -36,7 +37,17 @@ pub fn lower_program( check_result: &CheckResult, target: Target, ) -> Result { - program::lower(program, check_result, target) + program::lower(program, check_result, target, None) +} + +/// Lowers `program` into an EIR module and records the main PHP source path. +pub fn lower_program_with_source_path( + program: &Program, + check_result: &CheckResult, + target: Target, + source_path: &Path, +) -> Result { + program::lower(program, check_result, target, Some(source_path)) } /// Error produced while building or validating EIR. diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 5a626790f9..263af120fe 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -10,6 +10,7 @@ //! - The module is validated before it is returned to CLI/test callers. use std::collections::{HashMap, HashSet}; +use std::path::Path; use crate::codegen::platform::Target; use crate::codegen::RuntimeFeatures; @@ -27,8 +28,10 @@ pub(crate) fn lower( program: &Program, check_result: &CheckResult, target: Target, + source_path: Option<&Path>, ) -> Result { let mut module = Module::new(target); + module.source_path = source_path.map(canonical_source_path); let constants = crate::codegen::collect_constants(program, target.platform); let fiber_return_sigs = crate::ir_lower::fibers::collect_fiber_return_sigs(program); populate_metadata(&mut module, program, check_result); @@ -49,6 +52,15 @@ pub(crate) fn lower( Ok(module) } +/// Converts a PHP source path into the canonical display string stored in EIR metadata. +fn canonical_source_path(source_path: &Path) -> String { + source_path + .canonicalize() + .unwrap_or_else(|_| source_path.to_path_buf()) + .display() + .to_string() +} + /// Copies declaration metadata into the EIR module placeholder tables. fn populate_metadata(module: &mut Module, program: &Program, check_result: &CheckResult) { module.class_table.names = sorted_keys(&check_result.classes); diff --git a/src/pipeline.rs b/src/pipeline.rs index 0d9f77f251..75f8d7f5dd 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -258,7 +258,12 @@ pub(crate) fn compile(config: CliConfig) { if emit_ir { let phase_started = Instant::now(); - let mut module = match ir_lower::lower_program(&ast, &check_result, target) { + let mut module = match ir_lower::lower_program_with_source_path( + &ast, + &check_result, + target, + Path::new(filename), + ) { Ok(module) => module, Err(err) => { eprintln!("EIR lowering error: {}", err); @@ -282,7 +287,12 @@ pub(crate) fn compile(config: CliConfig) { } let phase_started = Instant::now(); - let mut ir_module = match ir_lower::lower_program(&ast, &check_result, target) { + let mut ir_module = match ir_lower::lower_program_with_source_path( + &ast, + &check_result, + target, + Path::new(filename), + ) { Ok(module) => module, Err(err) => { eprintln!("EIR lowering error: {}", err); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0989c874b2..b44c7ca70f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -476,6 +476,19 @@ eval('function DynEvalMagic() { return __FUNCTION__; } echo dynevalmagic();'); assert_eq!(out, "2:DynEvalMagic"); } +/// Verifies eval file-dependent magic constants receive generated call-site metadata. +#[test] +fn test_eval_magic_file_and_dir_execute_through_bridge() { + let out = compile_and_run( + r#" 0) { echo "D"; } else { echo "d"; } +echo ":"; +if (strlen(__FILE__) > strlen(__DIR__)) { echo "F"; } else { echo "f"; }'); +"#, + ); + assert_eq!(out, "D:F"); +} + /// Verifies eval-declared functions persist across eval calls in the same generated context. #[test] fn test_eval_declared_function_persists_across_eval_calls() { diff --git a/tests/codegen/support/compiler.rs b/tests/codegen/support/compiler.rs index bfe5740281..00e17cfb03 100644 --- a/tests/codegen/support/compiler.rs +++ b/tests/codegen/support/compiler.rs @@ -111,7 +111,8 @@ pub(crate) fn compile_source_to_asm_with_defines_repr( .required_libraries .iter() .any(|lib| lib == "elephc_tls"); - let ir_module = lower_and_validate_ir_for_codegen_fixture(&optimized, &check_result); + let ir_module = + lower_and_validate_ir_for_codegen_fixture(&optimized, &check_result, &synthetic_main); let exported_functions = HashMap::new(); // Honor ELEPHC_REGALLOC so the whole codegen suite can be run under both // the linear-scan allocator (default) and the stack fallback. @@ -144,8 +145,14 @@ pub(crate) fn compile_source_to_asm_with_defines_repr( pub(crate) fn lower_and_validate_ir_for_codegen_fixture( program: &elephc::parser::ast::Program, check_result: &elephc::types::CheckResult, + source_path: &Path, ) -> elephc::ir::Module { - let mut module = elephc::ir_lower::lower_program(program, check_result, target()) + let mut module = elephc::ir_lower::lower_program_with_source_path( + program, + check_result, + target(), + source_path, + ) .expect("AST-to-EIR lowering failed for codegen fixture"); if ir_opt_enabled_for_codegen_fixture() { elephc::ir_passes::optimize_module(&mut module); From 262c3da08e9ff6525554116d481198180bc60c28 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 20:05:38 +0200 Subject: [PATCH 0032/1208] Support eval method magic constant --- crates/elephc-eval/src/interpreter.rs | 16 ++++++++-------- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 3 +++ tests/codegen/eval.rs | 6 +++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index a087d0d762..201bd3ca75 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -975,10 +975,10 @@ fn eval_magic_const( EvalMagicConst::Dir => values.string(context.call_dir()), EvalMagicConst::Line(line) => values.int(*line), EvalMagicConst::Function => values.string(context.current_function().unwrap_or("")), - EvalMagicConst::Class - | EvalMagicConst::Method - | EvalMagicConst::Namespace - | EvalMagicConst::Trait => values.string(""), + EvalMagicConst::Method => values.string(context.current_function().unwrap_or("")), + EvalMagicConst::Class | EvalMagicConst::Namespace | EvalMagicConst::Trait => { + values.string("") + } } } @@ -1827,11 +1827,11 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(5)); } - /// Verifies `__FUNCTION__` inside an eval-declared function keeps the declaration spelling. + /// Verifies function-scope magic constants keep the eval declaration spelling. #[test] - fn execute_program_magic_function_uses_eval_declared_name() { + fn execute_program_magic_function_and_method_use_eval_declared_name() { let program = parse_fragment( - br#"function DynMagicCase() { return __FUNCTION__; } return dynmagiccase();"#, + br#"function DynMagicCase() { return __FUNCTION__ . ":" . __METHOD__; } return dynmagiccase();"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -1841,7 +1841,7 @@ mod tests { assert_eq!( values.get(result), - FakeValue::String("DynMagicCase".to_string()) + FakeValue::String("DynMagicCase:DynMagicCase".to_string()) ); } diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 9fb4dfa6e9..4926ed87db 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 79c4289ce9..a280a7b60a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -57,6 +57,8 @@ public function echoLabelThroughEval(): void { "); eval('function EvalMagicName() { return __FUNCTION__; }'); $magic_function = eval('return evalmagicname();'); +eval('function EvalMagicMethodName() { return __METHOD__; }'); +$magic_method = eval('return evalmagicmethodname();'); $magic_file_has_path = eval('return strlen(__FILE__) > strlen(__DIR__);'); $magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); eval('function native_add($left, $right) { return $left + $right; }'); @@ -76,6 +78,7 @@ public function echoLabelThroughEval(): void { echo "negative=" . $negative . "\n"; echo "magic-line=" . $magic_line . "\n"; echo "magic-function=" . $magic_function . "\n"; +echo "magic-method=" . $magic_method . "\n"; echo "magic-file=" . $magic_file_has_path . "\n"; echo "magic-dir=" . $magic_dir_has_path . "\n"; $counter = new EvalCounter(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b44c7ca70f..eda5e670f1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -464,16 +464,16 @@ echo eval('function dyn_eval_add($x) { return $x + 1; } return dyn_eval_add(4);' /// Verifies eval magic constants use fragment line and eval-declared function metadata. #[test] -fn test_eval_magic_line_and_function_execute_through_bridge() { +fn test_eval_magic_line_function_and_method_execute_through_bridge() { let out = compile_and_run( r#" Date: Mon, 15 Jun 2026 20:10:29 +0200 Subject: [PATCH 0033/1208] Support do while inside eval fragments --- crates/elephc-eval/src/eval_ir.rs | 4 +++ crates/elephc-eval/src/interpreter.rs | 42 +++++++++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 36 +++++++++++++++++++++++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + tests/codegen/eval.rs | 13 +++++++++ 6 files changed, 97 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 3ba805d06e..90aa5782ed 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -53,6 +53,10 @@ pub enum EvalStmt { }, Break, Continue, + DoWhile { + body: Vec, + condition: EvalExpr, + }, Echo(EvalExpr), For { init: Vec, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 201bd3ca75..8a0d123019 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -244,6 +244,9 @@ fn execute_stmt( } EvalStmt::Break => Ok(EvalControl::Break), EvalStmt::Continue => Ok(EvalControl::Continue), + EvalStmt::DoWhile { body, condition } => { + execute_do_while_stmt(body, condition, context, scope, values) + } EvalStmt::Echo(expr) => { let value = eval_expr(expr, context, scope, values)?; values.echo(value)?; @@ -337,6 +340,28 @@ fn execute_stmt( } } +/// Executes a PHP `do/while` loop, evaluating the condition after every body run. +fn execute_do_while_stmt( + body: &[EvalStmt], + condition: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + loop { + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + let condition = eval_expr(condition, context, scope, values)?; + if !values.truthy(condition)? { + break; + } + } + Ok(EvalControl::None) +} + /// Executes a PHP `for` loop while preserving update-on-continue semantics. fn execute_for_stmt( init: &[EvalStmt], @@ -1613,6 +1638,23 @@ mod tests { assert_eq!(values.get(flag), FakeValue::Bool(false)); } + /// Verifies do/while runs the body before testing the condition. + #[test] + fn execute_program_do_while_runs_body_before_condition() { + let program = parse_fragment(br#"do { echo $i; $i = $i + 1; } while (false);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let i = values.int(0).expect("create fake int"); + scope.set("i", i, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "0"); + assert_eq!(values.get(i), FakeValue::Int(1)); + } + /// Verifies for loops run init, condition, update, and body in PHP order. #[test] fn execute_program_for_loop_updates_after_body() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index a2209d3ad7..b077a4e343 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -378,6 +378,7 @@ impl Parser { self.expect_semicolon()?; Ok(vec![EvalStmt::Continue]) } + TokenKind::Ident(name) if ident_eq(name, "do") => self.parse_do_while_stmt(), TokenKind::Ident(name) if ident_eq(name, "echo") => { self.advance(); let expr = self.parse_expr()?; @@ -422,6 +423,21 @@ impl Parser { } } + /// Parses `do { ... } while (expr);`. + fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_block()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::DoWhile { body, condition }]) + } + /// Parses `$name[index] = expr;` for indexed-array eval writes. fn parse_array_set_stmt(&mut self, name: String) -> Result, EvalParseError> { self.advance(); @@ -1574,6 +1590,26 @@ mod tests { ); } + /// Verifies do/while fragments lower to body-first loop statements. + #[test] + fn parse_fragment_accepts_do_while_source() { + let program = parse_fragment(br#"do { echo $flag; $flag = false; } while ($flag);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DoWhile { + body: vec![ + EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), + EvalStmt::StoreVar { + name: "flag".to_string(), + value: EvalExpr::Const(EvalConst::Bool(false)), + }, + ], + condition: EvalExpr::LoadVar("flag".to_string()), + }] + ); + } + /// Verifies loop control statements parse inside while blocks. #[test] fn parse_fragment_accepts_break_and_continue_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4926ed87db..1903d0e30e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index a280a7b60a..1a3f1a4f01 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -39,6 +39,7 @@ public function echoLabelThroughEval(): void { eval('$profile["name"] = "Grace";'); eval('if ($x >= 3) { echo "x>=3\n"; }'); eval('if ($x < 0) { echo "negative\n"; } elseif ($x == 3) { echo "x==3\n"; }'); +eval('do { echo "do-once\n"; } while (false);'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eda5e670f1..4050ee789b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -206,6 +206,19 @@ echo $i; assert_eq!(out, "3210"); } +/// Verifies eval do/while loops execute the body before checking the condition. +#[test] +fn test_eval_do_while_runs_body_before_condition() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 20:13:58 +0200 Subject: [PATCH 0034/1208] Support braceless eval control flow bodies --- crates/elephc-eval/src/parser.rs | 40 +++++++++++++++++++++++++++----- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + tests/codegen/eval.rs | 12 ++++++++++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index b077a4e343..6c53a7edaa 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -426,7 +426,7 @@ impl Parser { /// Parses `do { ... } while (expr);`. fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { self.advance(); - let body = self.parse_block()?; + let body = self.parse_statement_body()?; if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { return Err(EvalParseError::UnexpectedToken); } @@ -463,7 +463,7 @@ impl Parser { }; self.expect_semicolon()?; let update = self.parse_for_update_clause()?; - let body = self.parse_block()?; + let body = self.parse_statement_body()?; Ok(vec![EvalStmt::For { init, condition, @@ -487,7 +487,7 @@ impl Parser { let value_name = value_name.clone(); self.advance(); self.expect(TokenKind::RParen)?; - let body = self.parse_block()?; + let body = self.parse_statement_body()?; Ok(vec![EvalStmt::Foreach { array, value_name, @@ -621,7 +621,7 @@ impl Parser { self.expect(TokenKind::LParen)?; let condition = self.parse_expr()?; self.expect(TokenKind::RParen)?; - let then_branch = self.parse_block()?; + let then_branch = self.parse_statement_body()?; let else_branch = self.parse_optional_else_branch()?; Ok(EvalStmt::If { condition, @@ -644,7 +644,7 @@ impl Parser { self.advance(); Ok(vec![self.parse_if_after_keyword()?]) } else { - self.parse_block() + self.parse_statement_body() } } @@ -674,10 +674,19 @@ impl Parser { self.expect(TokenKind::LParen)?; let condition = self.parse_expr()?; self.expect(TokenKind::RParen)?; - let body = self.parse_block()?; + let body = self.parse_statement_body()?; Ok(vec![EvalStmt::While { condition, body }]) } + /// Parses either a brace-delimited block or one braceless statement body. + fn parse_statement_body(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::LBrace) { + self.parse_block() + } else { + self.parse_stmt() + } + } + /// Parses a brace-delimited statement block. fn parse_block(&mut self) -> Result, EvalParseError> { self.expect(TokenKind::LBrace)?; @@ -1144,6 +1153,25 @@ mod tests { ); } + /// Verifies braceless if/else bodies parse as single-statement branch bodies. + #[test] + fn parse_fragment_accepts_braceless_if_else_source() { + let program = parse_fragment(br#"if ($flag) echo "yes"; else echo "no";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("flag".to_string()), + then_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))], + else_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "no".to_string() + )))], + }] + ); + } + /// Verifies elseif fragments lower to nested if statements in the else branch. #[test] fn parse_fragment_accepts_elseif_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 1903d0e30e..f119e73b58 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, and value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 1a3f1a4f01..6f75a9194c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -40,6 +40,7 @@ public function echoLabelThroughEval(): void { eval('if ($x >= 3) { echo "x>=3\n"; }'); eval('if ($x < 0) { echo "negative\n"; } elseif ($x == 3) { echo "x==3\n"; }'); eval('do { echo "do-once\n"; } while (false);'); +eval('if (true) echo "single-if\n";'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4050ee789b..f99f7b92f9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -193,6 +193,18 @@ echo $result; assert_eq!(out, "b"); } +/// Verifies eval accepts braceless single-statement control-flow bodies. +#[test] +fn test_eval_braceless_control_flow_bodies() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 20:20:20 +0200 Subject: [PATCH 0035/1208] Support switch inside eval fragments --- crates/elephc-eval/src/eval_ir.rs | 11 ++++ crates/elephc-eval/src/interpreter.rs | 59 ++++++++++++++++- crates/elephc-eval/src/parser.rs | 91 ++++++++++++++++++++++++++- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + tests/codegen/eval.rs | 11 ++++ 6 files changed, 172 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 90aa5782ed..b4dd27d3b9 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -89,6 +89,10 @@ pub enum EvalStmt { name: String, value: EvalExpr, }, + Switch { + expr: EvalExpr, + cases: Vec, + }, UnsetVar { name: String, }, @@ -176,6 +180,13 @@ pub enum EvalArrayElement { KeyValue { key: EvalExpr, value: EvalExpr }, } +/// One ordered case arm in a PHP switch parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalSwitchCase { + pub condition: Option, + pub body: Vec, +} + /// Literal syntax supported by the initial EvalIR parser. #[derive(Debug, Clone, PartialEq)] pub enum EvalConst { diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 8a0d123019..280291ddbc 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -15,7 +15,7 @@ use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::EvalStatus; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, EvalProgram, - EvalStmt, EvalUnaryOp, + EvalStmt, EvalSwitchCase, EvalUnaryOp, }; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership}; @@ -314,6 +314,9 @@ fn execute_stmt( } Ok(EvalControl::None) } + EvalStmt::Switch { expr, cases } => { + execute_switch_stmt(expr, cases, context, scope, values) + } EvalStmt::UnsetVar { name } => { if let Some(replaced) = scope.unset(name.clone()) { values.release(replaced)?; @@ -340,6 +343,44 @@ fn execute_stmt( } } +/// Executes a PHP switch with loose case matching, default fallback, and fallthrough. +fn execute_switch_stmt( + expr: &EvalExpr, + cases: &[EvalSwitchCase], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let subject = eval_expr(expr, context, scope, values)?; + let mut default_index = None; + let mut matched_index = None; + for (index, case) in cases.iter().enumerate() { + let Some(condition) = &case.condition else { + if default_index.is_none() { + default_index = Some(index); + } + continue; + }; + let condition = eval_expr(condition, context, scope, values)?; + let matches = values.compare(EvalBinOp::LooseEq, subject, condition)?; + if values.truthy(matches)? { + matched_index = Some(index); + break; + } + } + let Some(start_index) = matched_index.or(default_index) else { + return Ok(EvalControl::None); + }; + for case in &cases[start_index..] { + match execute_statements(&case.body, context, scope, values)? { + EvalControl::None => {} + EvalControl::Break | EvalControl::Continue => break, + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + /// Executes a PHP `do/while` loop, evaluating the condition after every body run. fn execute_do_while_stmt( body: &[EvalStmt], @@ -1655,6 +1696,22 @@ mod tests { assert_eq!(values.get(i), FakeValue::Int(1)); } + /// Verifies switch uses loose matching and falls through after the matching case. + #[test] + fn execute_program_switch_matches_and_falls_through() { + let program = + parse_fragment(br#"switch ($x) { case 1: echo "one"; break; case 2: echo "two"; default: echo "default"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(2).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "twodefault"); + } + /// Verifies for loops run init, condition, update, and body in PHP order. #[test] fn execute_program_for_loop_updates_after_body() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 6c53a7edaa..ed803dba8f 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -16,7 +16,7 @@ use crate::errors::EvalParseError; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalMagicConst, EvalProgram, EvalStmt, - EvalUnaryOp, + EvalSwitchCase, EvalUnaryOp, }; /// Parses an eval fragment into by-name EvalIR statements. @@ -67,6 +67,7 @@ enum TokenKind { LBrace, RBrace, Comma, + Colon, Eof, } @@ -222,6 +223,10 @@ impl<'a> Lexer<'a> { self.bump_char(); Ok(TokenKind::Comma) } + ':' => { + self.bump_char(); + Ok(TokenKind::Colon) + } _ if is_ident_start(ch) => { let ident = self.lex_ident(); Ok(magic_const_token(&ident, line).unwrap_or(TokenKind::Ident(ident))) @@ -398,6 +403,7 @@ impl Parser { self.expect_semicolon()?; Ok(vec![EvalStmt::Return(Some(expr))]) } + TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { @@ -648,6 +654,54 @@ impl Parser { } } + /// Parses `switch (expr) { case expr: ... default: ... }`. + fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + let mut cases = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + cases.push(self.parse_switch_case()?); + } + self.expect(TokenKind::RBrace)?; + Ok(vec![EvalStmt::Switch { expr, cases }]) + } + + /// Parses one `case` or `default` arm inside a switch body. + fn parse_switch_case(&mut self) -> Result { + let condition = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) + { + self.advance(); + let condition = self.parse_expr()?; + Some(condition) + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + None + } else { + return Err(EvalParseError::UnexpectedToken); + }; + self.expect(TokenKind::Colon)?; + let body = self.parse_switch_case_body()?; + Ok(EvalSwitchCase { condition, body }) + } + + /// Parses case body statements until the next case boundary or switch close. + fn parse_switch_case_body(&mut self) -> Result, EvalParseError> { + let mut body = Vec::new(); + while !is_switch_case_boundary(self.current()) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + body.extend(self.parse_stmt()?); + } + Ok(body) + } + /// Parses `unset($name[, ...]);`. fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -1095,6 +1149,12 @@ fn magic_const_token(name: &str, line: i64) -> Option { Some(TokenKind::Magic(magic)) } +/// Returns true when the current token closes or starts a switch case arm. +fn is_switch_case_boundary(token: &TokenKind) -> bool { + matches!(token, TokenKind::RBrace) + || matches!(token, TokenKind::Ident(name) if ident_eq(name, "case") || ident_eq(name, "default")) +} + /// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. fn ident_eq(actual: &str, expected: &str) -> bool { actual.eq_ignore_ascii_case(expected) @@ -1238,6 +1298,35 @@ mod tests { ); } + /// Verifies switch fragments preserve ordered case and default bodies. + #[test] + fn parse_fragment_accepts_switch_source() { + let program = + parse_fragment(br#"switch ($x) { case 1: echo "one"; break; default: echo "other"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Switch { + expr: EvalExpr::LoadVar("x".to_string()), + cases: vec![ + EvalSwitchCase { + condition: Some(EvalExpr::Const(EvalConst::Int(1))), + body: vec![ + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("one".to_string()))), + EvalStmt::Break, + ], + }, + EvalSwitchCase { + condition: None, + body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "other".to_string() + )))], + }, + ], + }] + ); + } + /// Verifies value-only foreach loops lower to an array expression, value target, and body. #[test] fn parse_fragment_accepts_foreach_source() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index f119e73b58..a506edb31c 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, and value-only `foreach`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only `foreach`, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 6f75a9194c..29ff9addfe 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -42,6 +42,7 @@ public function echoLabelThroughEval(): void { eval('do { echo "do-once\n"; } while (false);'); eval('if (true) echo "single-if\n";'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); +eval('switch (2) { case 1: echo "switch-one\n"; break; case 2: echo "switch-two\n"; break; }'); eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); $meta = eval('return ["source" => "eval"];'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f99f7b92f9..6b662506be 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -231,6 +231,17 @@ echo ":" . $i; assert_eq!(out, "0:1"); } +/// Verifies eval switch supports matching, default fallback, and fallthrough. +#[test] +fn test_eval_switch_matches_default_and_fallthrough() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 20:29:56 +0200 Subject: [PATCH 0036/1208] Support strict equality inside eval fragments --- crates/elephc-eval/src/eval_ir.rs | 2 + crates/elephc-eval/src/interpreter.rs | 31 +++++++++++++++ crates/elephc-eval/src/parser.rs | 45 ++++++++++++++++++++-- crates/elephc-eval/src/runtime_hooks.rs | 2 + docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + src/codegen_support/runtime/eval_bridge.rs | 31 +++++++++++++++ tests/codegen/eval.rs | 11 ++++++ 8 files changed, 121 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index b4dd27d3b9..814e50bce4 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -221,6 +221,8 @@ pub enum EvalBinOp { LogicalOr, LooseEq, LooseNotEq, + StrictEq, + StrictNotEq, Lt, LtEq, Gt, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 280291ddbc..98cd40f061 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -559,6 +559,8 @@ fn eval_expr( EvalBinOp::Concat => values.concat(left, right), EvalBinOp::LooseEq | EvalBinOp::LooseNotEq + | EvalBinOp::StrictEq + | EvalBinOp::StrictNotEq | EvalBinOp::Lt | EvalBinOp::LtEq | EvalBinOp::Gt @@ -1380,6 +1382,8 @@ mod tests { let result = match op { EvalBinOp::LooseEq => self.loose_eq(left, right), EvalBinOp::LooseNotEq => !self.loose_eq(left, right), + EvalBinOp::StrictEq => self.strict_eq(left, right), + EvalBinOp::StrictNotEq => !self.strict_eq(left, right), EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, @@ -1448,6 +1452,18 @@ mod tests { } } + /// Compares fake scalar values by PHP strict tag and payload equality. + fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, + (FakeValue::Int(left), FakeValue::Int(right)) => left == right, + (FakeValue::Float(left), FakeValue::Float(right)) => left == right, + (FakeValue::String(left), FakeValue::String(right)) => left == right, + _ => false, + } + } + /// Converts one fake scalar cell to a numeric value for comparison tests. fn numeric(&self, handle: RuntimeCellHandle) -> Result { Ok(self.fake_numeric(&self.get(handle))) @@ -1759,6 +1775,21 @@ mod tests { assert_eq!(values.output, "1111ns"); } + /// Verifies strict equality keeps PHP type identity distinct from loose equality. + #[test] + fn execute_program_strict_equality_uses_type_identity() { + let program = parse_fragment( + br#"echo "10" == 10; echo "10" === 10; echo "10" === "10"; echo "10" !== 10;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111"); + } + /// Verifies logical AND skips an unsupported right-hand expression after a false left side. #[test] fn execute_program_short_circuits_logical_and() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index ed803dba8f..4ab47af020 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -50,8 +50,10 @@ enum TokenKind { Dot, Equal, EqualEqual, + EqualEqualEqual, Bang, NotEqual, + NotEqualEqual, AndAnd, OrOr, Less, @@ -138,7 +140,12 @@ impl<'a> Lexer<'a> { self.bump_char(); if self.peek_char() == Some('=') { self.bump_char(); - Ok(TokenKind::EqualEqual) + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::EqualEqualEqual) + } else { + Ok(TokenKind::EqualEqual) + } } else if self.peek_char() == Some('>') { self.bump_char(); Ok(TokenKind::FatArrow) @@ -150,7 +157,12 @@ impl<'a> Lexer<'a> { self.bump_char(); if self.peek_char() == Some('=') { self.bump_char(); - Ok(TokenKind::NotEqual) + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::NotEqualEqual) + } else { + Ok(TokenKind::NotEqual) + } } else { Ok(TokenKind::Bang) } @@ -788,7 +800,7 @@ impl Parser { Ok(expr) } - /// Parses left-associative loose equality and inequality comparisons. + /// Parses left-associative equality and inequality comparisons. fn parse_equality(&mut self) -> Result { let mut expr = self.parse_ordering()?; loop { @@ -796,6 +808,10 @@ impl Parser { EvalBinOp::LooseEq } else if self.consume(TokenKind::NotEqual) { EvalBinOp::LooseNotEq + } else if self.consume(TokenKind::EqualEqualEqual) { + EvalBinOp::StrictEq + } else if self.consume(TokenKind::NotEqualEqual) { + EvalBinOp::StrictNotEq } else { break; }; @@ -1422,6 +1438,29 @@ mod tests { ); } + /// Verifies strict equality operators parse as distinct EvalIR comparisons. + #[test] + fn parse_fragment_accepts_strict_equality_source() { + let program = parse_fragment(br#"return "10" === "10" && "10" !== 10;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::StrictEq, + left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + }), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::StrictNotEq, + left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::Int(10))), + }), + }))] + ); + } + /// Verifies logical operators parse with `&&` binding tighter than `||`. #[test] fn parse_fragment_accepts_short_circuit_logical_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index eec96c9879..661bfdf9c3 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -341,6 +341,8 @@ fn compare_op_tag(op: EvalBinOp) -> u64 { EvalBinOp::LtEq => 3, EvalBinOp::Gt => 4, EvalBinOp::GtEq => 5, + EvalBinOp::StrictEq => 6, + EvalBinOp::StrictNotEq => 7, EvalBinOp::Add | EvalBinOp::Sub | EvalBinOp::Mul diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a506edb31c..b25c066260 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only `foreach`, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only `foreach`, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 29ff9addfe..ff5068c622 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -39,6 +39,7 @@ public function echoLabelThroughEval(): void { eval('$profile["name"] = "Grace";'); eval('if ($x >= 3) { echo "x>=3\n"; }'); eval('if ($x < 0) { echo "negative\n"; } elseif ($x == 3) { echo "x==3\n"; }'); +eval('if ("10" !== 10) { echo "strict-ok\n"; }'); eval('do { echo "do-once\n"; } while (false);'); eval('if (true) echo "single-if\n";'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 6da3c6583a..af19dae18e 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -253,6 +253,10 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("b.eq __elephc_eval_value_compare_eq"); // route == through the mixed loose-equality helper emitter.instruction("cmp x2, #1"); // is this loose inequality? emitter.instruction("b.eq __elephc_eval_value_compare_ne"); // route != through the mixed loose-equality helper + emitter.instruction("cmp x2, #6"); // is this strict equality? + emitter.instruction("b.eq __elephc_eval_value_compare_strict_eq"); // route === through the mixed strict-equality helper + emitter.instruction("cmp x2, #7"); // is this strict inequality? + emitter.instruction("b.eq __elephc_eval_value_compare_strict_ne"); // route !== through the mixed strict-equality helper emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a numeric comparison double emitter.instruction("str d0, [sp, #24]"); // save the normalized left numeric operand emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting @@ -281,6 +285,18 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("bl __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality before inversion emitter.instruction("eor x1, x0, #1"); // invert equality for the != operator emitter.instruction("b __elephc_eval_value_compare_box"); // box the inequality result + emitter.label("__elephc_eval_value_compare_strict_eq"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the left operand for strict equality + emitter.instruction("ldr x1, [sp, #0]"); // reload the right operand for strict equality + emitter.instruction("bl __rt_mixed_strict_eq"); // compute PHP strict equality + emitter.instruction("mov x1, x0"); // move strict equality into the bool payload register + emitter.instruction("b __elephc_eval_value_compare_box"); // box the strict-equality result + emitter.label("__elephc_eval_value_compare_strict_ne"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the left operand for strict inequality + emitter.instruction("ldr x1, [sp, #0]"); // reload the right operand for strict inequality + emitter.instruction("bl __rt_mixed_strict_eq"); // compute PHP strict equality before inversion + emitter.instruction("eor x1, x0, #1"); // invert equality for the !== operator + emitter.instruction("b __elephc_eval_value_compare_box"); // box the strict-inequality result emitter.label("__elephc_eval_value_compare_lt"); emitter.instruction("fcmp d1, d0"); // compare numeric eval operands for < emitter.instruction("cset x1, mi"); // ordered less-than becomes boolean true @@ -716,6 +732,10 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("je __elephc_eval_value_compare_eq"); // route == through the mixed loose-equality helper emitter.instruction("cmp rdx, 1"); // is this loose inequality? emitter.instruction("je __elephc_eval_value_compare_ne"); // route != through the mixed loose-equality helper + emitter.instruction("cmp rdx, 6"); // is this strict equality? + emitter.instruction("je __elephc_eval_value_compare_strict_eq"); // route === through the mixed strict-equality helper + emitter.instruction("cmp rdx, 7"); // is this strict inequality? + emitter.instruction("je __elephc_eval_value_compare_strict_ne"); // route !== through the mixed strict-equality helper emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a numeric comparison double emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save the normalized left numeric operand @@ -745,6 +765,17 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("call __elephc_eval_mixed_loose_eq"); // compute scalar PHP loose equality before inversion emitter.instruction("xor rax, 1"); // invert equality for the != operator emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the inequality result + emitter.label("__elephc_eval_value_compare_strict_eq"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left operand for strict equality + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right operand for strict equality + emitter.instruction("call __rt_mixed_strict_eq"); // compute PHP strict equality + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the strict-equality result + emitter.label("__elephc_eval_value_compare_strict_ne"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the left operand for strict inequality + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the right operand for strict inequality + emitter.instruction("call __rt_mixed_strict_eq"); // compute PHP strict equality before inversion + emitter.instruction("xor rax, 1"); // invert equality for the !== operator + emitter.instruction("jmp __elephc_eval_value_compare_box"); // box the strict-inequality result emitter.label("__elephc_eval_value_compare_lt"); emitter.instruction("ucomisd xmm1, xmm0"); // compare numeric eval operands for < emitter.instruction("setb al"); // set true when left is below right diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6b662506be..11eee1acaf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -123,6 +123,17 @@ eval('echo "a" == "a"; echo "a" != "b"; echo "" == null; echo "10" == 10; echo " assert_eq!(out, "111111"); } +/// Verifies strict scalar equality in eval preserves PHP type identity. +#[test] +fn test_eval_scalar_strict_equality_executes_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 20:40:42 +0200 Subject: [PATCH 0037/1208] Support key value eval foreach --- crates/elephc-eval/src/eval_ir.rs | 1 + crates/elephc-eval/src/interpreter.rs | 127 +++++++++++++++++++-- crates/elephc-eval/src/parser.rs | 37 +++++- crates/elephc-eval/src/runtime_hooks.rs | 13 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + src/codegen_support/runtime/eval_bridge.rs | 121 ++++++++++++++++++++ tests/codegen/eval.rs | 35 ++++++ 9 files changed, 327 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 814e50bce4..77fb45d52b 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -66,6 +66,7 @@ pub enum EvalStmt { }, Foreach { array: EvalExpr, + key_name: Option, value_name: String, body: Vec, }, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 98cd40f061..1e068b2982 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -44,6 +44,13 @@ pub trait RuntimeValueOps { index: RuntimeCellHandle, ) -> Result; + /// Returns the foreach-visible key at a zero-based iteration position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result; + /// Writes one element to a runtime array-like Mixed cell and returns the target cell. fn array_set( &mut self, @@ -268,9 +275,18 @@ fn execute_stmt( ), EvalStmt::Foreach { array, + key_name, value_name, body, - } => execute_foreach_stmt(array, value_name, body, context, scope, values), + } => execute_foreach_stmt( + array, + key_name.as_deref(), + value_name, + body, + context, + scope, + values, + ), EvalStmt::FunctionDecl { name, params, body } => { let key = name.to_ascii_lowercase(); context @@ -439,9 +455,10 @@ fn execute_for_stmt( Ok(EvalControl::None) } -/// Executes a value-only PHP `foreach` loop over indexed eval array values. +/// Executes a PHP `foreach` loop over eval array values. fn execute_foreach_stmt( array: &EvalExpr, + key_name: Option<&str>, value_name: &str, body: &[EvalStmt], context: &mut ElephcEvalContext, @@ -451,8 +468,16 @@ fn execute_foreach_stmt( let array = eval_expr(array, context, scope, values)?; let len = values.array_len(array)?; for index in 0..len { - let index = values.int(index as i64)?; - let value = values.array_get(array, index)?; + let key = values.array_iter_key(array, index)?; + let value = values.array_get(array, key)?; + if let Some(key_name) = key_name { + if let Some(replaced) = scope.set(key_name.to_string(), key, ScopeCellOwnership::Owned) + { + values.release(replaced)?; + } + } else { + values.release(key)?; + } if let Some(replaced) = scope.set(value_name.to_string(), value, ScopeCellOwnership::Owned) { values.release(replaced)?; @@ -1081,7 +1106,7 @@ mod tests { Float(f64), String(String), Array(Vec), - Assoc(HashMap), + Assoc(Vec<(FakeKey, RuntimeCellHandle)>), Object(HashMap), } @@ -1117,6 +1142,14 @@ mod tests { _ => Err(EvalStatus::UnsupportedConstruct), } } + + /// Allocates a fake runtime cell for an existing PHP array key. + fn alloc_key(&mut self, key: &FakeKey) -> Result { + match key { + FakeKey::Int(value) => self.int(*value), + FakeKey::String(value) => self.string(value), + } + } } impl RuntimeValueOps for FakeOps { @@ -1126,8 +1159,8 @@ mod tests { } /// Creates a fake associative array cell. - fn assoc_new(&mut self, capacity: usize) -> Result { - Ok(self.alloc(FakeValue::Assoc(HashMap::with_capacity(capacity)))) + fn assoc_new(&mut self, _capacity: usize) -> Result { + Ok(self.alloc(FakeValue::Assoc(Vec::new()))) } /// Reads one fake indexed array element. @@ -1150,9 +1183,31 @@ mod tests { .copied() .map_or_else(|| self.null(), Ok) } + FakeValue::Assoc(entries) => entries + .iter() + .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Returns one fake foreach key by insertion-order position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(array) { + FakeValue::Array(elements) if position < elements.len() => { + self.int(position as i64) + } FakeValue::Assoc(entries) => { - entries.get(&key).copied().map_or_else(|| self.null(), Ok) + let Some((key, _)) = entries.get(position) else { + return self.null(); + }; + self.alloc_key(key) } + FakeValue::Array(_) => self.null(), _ => Err(EvalStatus::UnsupportedConstruct), } } @@ -1181,7 +1236,13 @@ mod tests { elements[index] = value; } Some(FakeValue::Assoc(entries)) => { - entries.insert(key, value); + if let Some((_, existing_value)) = + entries.iter_mut().find(|(entry_key, _)| entry_key == &key) + { + *existing_value = value; + } else { + entries.push((key, value)); + } } _ => return Err(EvalStatus::UnsupportedConstruct), } @@ -1858,6 +1919,54 @@ mod tests { assert_eq!(values.get(item), FakeValue::String("b".to_string())); } + /// Verifies foreach key-value targets receive indexed integer keys and values. + #[test] + fn execute_program_foreach_assigns_indexed_keys() { + let program = + parse_fragment(br#"foreach (["a", "b"] as $key => $item) { echo $key . $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let key = scope.visible_cell("key").expect("scope should contain key"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "0a1b"); + assert_eq!(values.get(key), FakeValue::Int(1)); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); + } + + /// Verifies foreach over associative arrays preserves insertion-order keys and values. + #[test] + fn execute_program_foreach_iterates_assoc_keys_and_values() { + let program = parse_fragment( + br#"foreach (["a" => 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a:1;b:2;"); + } + + /// Verifies value-only foreach over associative arrays still yields values in insertion order. + #[test] + fn execute_program_foreach_iterates_assoc_values_only() { + let program = parse_fragment(br#"foreach (["a" => 1, "b" => 2] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12"); + } + /// Verifies break and continue control foreach execution inside eval. #[test] fn execute_program_foreach_honors_break_and_continue() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 4ab47af020..a30957f62f 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -490,7 +490,7 @@ impl Parser { }]) } - /// Parses `foreach (expr as $value) { ... }`. + /// Parses `foreach (expr as $value) { ... }` or `foreach (expr as $key => $value) { ... }`. fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { self.advance(); self.expect(TokenKind::LParen)?; @@ -504,10 +504,23 @@ impl Parser { }; let value_name = value_name.clone(); self.advance(); + let (key_name, value_name) = if matches!(self.current(), TokenKind::FatArrow) { + self.advance(); + let TokenKind::DollarIdent(next_value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let key_name = value_name; + let value_name = next_value_name.clone(); + self.advance(); + (Some(key_name), value_name) + } else { + (None, value_name) + }; self.expect(TokenKind::RParen)?; let body = self.parse_statement_body()?; Ok(vec![EvalStmt::Foreach { array, + key_name, value_name, body, }]) @@ -1352,12 +1365,34 @@ mod tests { program.statements(), &[EvalStmt::Foreach { array: EvalExpr::LoadVar("items".to_string()), + key_name: None, value_name: "item".to_string(), body: vec![EvalStmt::Echo(EvalExpr::LoadVar("item".to_string()))], }] ); } + /// Verifies key-value foreach loops preserve both loop target names in EvalIR. + #[test] + fn parse_fragment_accepts_foreach_key_value_source() { + let program = + parse_fragment(br#"foreach ($items as $key => $item) { echo $key . $item; }"#) + .expect("parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Foreach { + array: EvalExpr::LoadVar("items".to_string()), + key_name: Some("key".to_string()), + value_name: "item".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("key".to_string())), + right: Box::new(EvalExpr::LoadVar("item".to_string())), + })], + }] + ); + } + /// Verifies dynamic function declarations preserve name, parameters, and body. #[test] fn parse_fragment_accepts_function_declaration_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 661bfdf9c3..22ed1a3dff 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -28,6 +28,10 @@ unsafe extern "C" { array: *mut RuntimeCell, index: *mut RuntimeCell, ) -> *mut RuntimeCell; + fn __elephc_eval_value_array_iter_key( + array: *mut RuntimeCell, + position: u64, + ) -> *mut RuntimeCell; fn __elephc_eval_value_array_set( array: *mut RuntimeCell, index: *mut RuntimeCell, @@ -125,6 +129,15 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_array_get(array.as_ptr(), index.as_ptr()) }) } + /// Returns one foreach-visible key from a boxed Mixed array by iteration position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_iter_key(array.as_ptr(), position as u64) }) + } + /// Writes one element to a boxed Mixed array through the generated runtime wrapper. fn array_set( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 4989214824..6d714e062e 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/value-only `foreach`), indexed arrays, associative array literals, and string-key reads/writes on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b25c066260..7af22e1b28 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only `foreach`, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index ff5068c622..a7cc603951 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -43,6 +43,7 @@ public function echoLabelThroughEval(): void { eval('do { echo "do-once\n"; } while (false);'); eval('if (true) echo "single-if\n";'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); +eval('foreach (["a" => 1, "b" => 2] as $key => $value) { echo "pair=" . $key . ":" . $value . "\n"; }'); eval('switch (2) { case 1: echo "switch-one\n"; break; case 2: echo "switch-two\n"; break; }'); eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index af19dae18e..42ae11e995 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -106,6 +106,65 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #32"); // release the array-get wrapper frame emitter.instruction("ret"); // return the boxed element to Rust + label_c_global(emitter, "__elephc_eval_value_array_iter_key"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for insertion-order key iteration + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable iterator-key frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed array receiver while walking the container + emitter.instruction("str x1, [sp, #8]"); // save the requested zero-based foreach position + emitter.instruction("cbz x0, __elephc_eval_value_array_iter_key_null"); // null handles produce a null key + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __elephc_eval_value_array_iter_key_indexed"); // indexed arrays expose integer positions as foreach keys + emitter.instruction("cmp x9, #5"); // tag 5 = associative array + emitter.instruction("b.eq __elephc_eval_value_array_iter_key_assoc"); // associative arrays expose insertion-order hash keys + emitter.instruction("b __elephc_eval_value_array_iter_key_null"); // scalar values have no foreach-visible key + emitter.label("__elephc_eval_value_array_iter_key_indexed"); + emitter.instruction("ldr x1, [sp, #8]"); // use the requested foreach position as the integer key payload + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer key + emitter.instruction("mov x2, xzr"); // integer keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box the indexed foreach key as an owned Mixed cell + emitter.instruction("b __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_assoc"); + emitter.instruction("ldr x9, [x0, #8]"); // load the hash payload pointer from the Mixed cell + emitter.instruction("cbz x9, __elephc_eval_value_array_iter_key_null"); // null hash payloads produce a null key + emitter.instruction("str x9, [sp, #16]"); // save the hash pointer for repeated iterator helper calls + emitter.instruction("str xzr, [sp, #24]"); // start the insertion-order position counter at zero + emitter.instruction("mov x1, xzr"); // cursor 0 starts at the hash head entry + emitter.label("__elephc_eval_value_array_iter_key_assoc_loop"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the hash pointer before advancing the hash iterator + emitter.instruction("bl __rt_hash_iter_next"); // fetch the next insertion-order hash key + emitter.instruction("cmn x0, #1"); // did the iterator report the done sentinel? + emitter.instruction("b.eq __elephc_eval_value_array_iter_key_null"); // out-of-range positions produce a null key + emitter.instruction("ldr x10, [sp, #24]"); // load the current insertion-order position + emitter.instruction("ldr x11, [sp, #8]"); // load the requested foreach position + emitter.instruction("cmp x10, x11"); // is this the requested hash entry? + emitter.instruction("b.eq __elephc_eval_value_array_iter_key_assoc_box"); // box the current hash key when the position matches + emitter.instruction("add x10, x10, #1"); // advance the insertion-order position counter + emitter.instruction("str x10, [sp, #24]"); // persist the updated position counter for the next probe + emitter.instruction("mov x1, x0"); // use the returned cursor for the next hash iterator call + emitter.instruction("b __elephc_eval_value_array_iter_key_assoc_loop"); // continue walking until the requested position is reached + emitter.label("__elephc_eval_value_array_iter_key_assoc_box"); + emitter.instruction("cmn x2, #1"); // integer hash keys carry key_hi = -1 + emitter.instruction("b.ne __elephc_eval_value_array_iter_key_assoc_string"); // string hash keys need string-tag boxing + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer key + emitter.instruction("mov x2, xzr"); // integer keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box the associative integer key as Mixed + emitter.instruction("b __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_assoc_string"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string key + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the associative string key as Mixed + emitter.instruction("b __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null keys do not use a low payload word + emitter.instruction("mov x2, xzr"); // null keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for invalid foreach-key requests + emitter.label("__elephc_eval_value_array_iter_key_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the iterator-key wrapper frame + emitter.instruction("ret"); // return the boxed foreach key to Rust + label_c_global(emitter, "__elephc_eval_value_array_set"); emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for key coercion and value retention emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls @@ -571,6 +630,68 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed element to Rust + label_c_global(emitter, "__elephc_eval_value_array_iter_key"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable iterator-key wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve slots for receiver, target position, hash pointer, and counter + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed array receiver while walking the container + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested zero-based foreach position + emitter.instruction("test rdi, rdi"); // null handles produce a null key + emitter.instruction("jz __elephc_eval_value_array_iter_key_null"); // branch to boxed null for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __elephc_eval_value_array_iter_key_indexed"); // indexed arrays expose integer positions as foreach keys + emitter.instruction("cmp r10, 5"); // tag 5 = associative array + emitter.instruction("je __elephc_eval_value_array_iter_key_assoc"); // associative arrays expose insertion-order hash keys + emitter.instruction("jmp __elephc_eval_value_array_iter_key_null"); // scalar values have no foreach-visible key + emitter.label("__elephc_eval_value_array_iter_key_indexed"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // use the requested foreach position as the integer key payload + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer key + emitter.instruction("xor esi, esi"); // integer keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box the indexed foreach key as an owned Mixed cell + emitter.instruction("jmp __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_assoc"); + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the hash payload pointer from the Mixed cell + emitter.instruction("test r10, r10"); // null hash payloads produce a null key + emitter.instruction("jz __elephc_eval_value_array_iter_key_null"); // branch to boxed null for missing hash payloads + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the hash pointer for repeated iterator helper calls + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the insertion-order position counter at zero + emitter.instruction("xor esi, esi"); // cursor 0 starts at the hash head entry + emitter.label("__elephc_eval_value_array_iter_key_assoc_loop"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the hash pointer before advancing the hash iterator + emitter.instruction("call __rt_hash_iter_next"); // fetch the next insertion-order hash key + emitter.instruction("cmp rax, -1"); // did the iterator report the done sentinel? + emitter.instruction("je __elephc_eval_value_array_iter_key_null"); // out-of-range positions produce a null key + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // load the current insertion-order position + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // load the requested foreach position + emitter.instruction("cmp r10, r11"); // is this the requested hash entry? + emitter.instruction("je __elephc_eval_value_array_iter_key_assoc_box"); // box the current hash key when the position matches + emitter.instruction("add r10, 1"); // advance the insertion-order position counter + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the updated position counter for the next probe + emitter.instruction("mov rsi, rax"); // use the returned cursor for the next hash iterator call + emitter.instruction("jmp __elephc_eval_value_array_iter_key_assoc_loop"); // continue walking until the requested position is reached + emitter.label("__elephc_eval_value_array_iter_key_assoc_box"); + emitter.instruction("cmp rdx, -1"); // integer hash keys carry key_hi = -1 + emitter.instruction("jne __elephc_eval_value_array_iter_key_assoc_string"); // string hash keys need string-tag boxing + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer key + emitter.instruction("xor esi, esi"); // integer keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box the associative integer key as Mixed + emitter.instruction("jmp __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_assoc_string"); + emitter.instruction("mov rsi, rdx"); // move the string key length into the boxing high word + emitter.instruction("mov eax, 1"); // runtime tag 1 = string key + emitter.instruction("call __rt_mixed_from_value"); // persist and box the associative string key as Mixed + emitter.instruction("jmp __elephc_eval_value_array_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_array_iter_key_null"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null keys do not use a low payload word + emitter.instruction("xor esi, esi"); // null keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for invalid foreach-key requests + emitter.label("__elephc_eval_value_array_iter_key_done"); + emitter.instruction("add rsp, 32"); // release the iterator-key wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed foreach key to Rust + label_c_global(emitter, "__elephc_eval_value_array_set"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 11eee1acaf..4b808fc521 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -314,6 +314,18 @@ echo ":" . $item; assert_eq!(out, "123:3"); } +/// Verifies key-value foreach loops inside eval expose indexed array positions. +#[test] +fn test_eval_foreach_iterates_indexed_keys_and_values() { + let out = compile_and_run( + r#" $item) { echo $key . ":" . $item . ";"; }'); +echo "|" . $key . ":" . $item; +"#, + ); + assert_eq!(out, "0:10;1:20;|1:20"); +} + /// Verifies eval foreach can iterate an indexed array from the caller scope. #[test] fn test_eval_foreach_reads_scope_array() { @@ -338,6 +350,29 @@ echo ":" . $item; assert_eq!(out, "2:2"); } +/// Verifies value-only foreach loops inside eval iterate associative array values. +#[test] +fn test_eval_foreach_iterates_assoc_values() { + let out = compile_and_run( + r#" 1, "b" => 2] as $item) { echo $item; }'); +"#, + ); + assert_eq!(out, "12"); +} + +/// Verifies key-value foreach loops inside eval expose associative keys in insertion order. +#[test] +fn test_eval_foreach_iterates_assoc_keys_and_values() { + let out = compile_and_run( + r#" 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }'); +echo "|" . $key . ":" . $item; +"#, + ); + assert_eq!(out, "a:1;b:2;|b:2"); +} + /// Verifies eval indexed-array literals and reads execute through Mixed array helpers. #[test] fn test_eval_indexed_array_literal_and_read() { From 0e96208c424239a3da9fe11a1141911b47135090 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 20:44:32 +0200 Subject: [PATCH 0038/1208] Support ternary inside eval fragments --- crates/elephc-eval/src/eval_ir.rs | 5 +++ crates/elephc-eval/src/interpreter.rs | 43 ++++++++++++++++++ crates/elephc-eval/src/parser.rs | 64 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 17 +++++++ 7 files changed, 132 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 77fb45d52b..f512f8eaa8 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -163,6 +163,11 @@ pub enum EvalExpr { property: String, }, Print(Box), + Ternary { + condition: Box, + then_branch: Option>, + else_branch: Box, + }, Unary { op: EvalUnaryOp, expr: Box, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 1e068b2982..c2e0fd20ab 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -539,6 +539,22 @@ fn eval_expr( values.echo(value)?; values.int(1) } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + let condition = eval_expr(condition, context, scope, values)?; + if values.truthy(condition)? { + if let Some(then_branch) = then_branch { + eval_expr(then_branch, context, scope, values) + } else { + Ok(condition) + } + } else { + eval_expr(else_branch, context, scope, values) + } + } EvalExpr::Unary { op, expr } => { let value = eval_expr(expr, context, scope, values)?; match op { @@ -1876,6 +1892,33 @@ mod tests { assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies ternary expressions evaluate only the selected branch. + #[test] + fn execute_program_ternary_short_circuits_unselected_branch() { + let program = + parse_fragment(br#"echo true ? "yes" : missing(); echo false ? missing() : "no";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "yesno"); + } + + /// Verifies the short ternary form returns the condition value when it is truthy. + #[test] + fn execute_program_short_ternary_reuses_truthy_condition() { + let program = parse_fragment(br#"echo "x" ?: "fallback"; echo false ?: "fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xfallback"); + } + /// Verifies logical negation returns boolean cells using PHP truthiness. #[test] fn execute_program_evaluates_logical_not() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index a30957f62f..b6735df0ef 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -61,6 +61,7 @@ enum TokenKind { Greater, GreaterEqual, FatArrow, + Question, Semicolon, LParen, RParen, @@ -203,6 +204,10 @@ impl<'a> Lexer<'a> { Ok(TokenKind::Greater) } } + '?' => { + self.bump_char(); + Ok(TokenKind::Question) + } ';' => { self.bump_char(); Ok(TokenKind::Semicolon) @@ -782,7 +787,28 @@ impl Parser { /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. fn parse_expr(&mut self) -> Result { - self.parse_logical_or() + self.parse_ternary() + } + + /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. + fn parse_ternary(&mut self) -> Result { + let condition = self.parse_logical_or()?; + if !self.consume(TokenKind::Question) { + return Ok(condition); + } + let then_branch = if self.consume(TokenKind::Colon) { + None + } else { + let expr = self.parse_expr()?; + self.expect(TokenKind::Colon)?; + Some(Box::new(expr)) + }; + let else_branch = self.parse_expr()?; + Ok(EvalExpr::Ternary { + condition: Box::new(condition), + then_branch, + else_branch: Box::new(else_branch), + }) } /// Parses left-associative logical OR with lower precedence than logical AND. @@ -1515,6 +1541,42 @@ mod tests { ); } + /// Verifies ternary expressions parse below logical OR and preserve both branches. + #[test] + fn parse_fragment_accepts_ternary_source() { + let program = + parse_fragment(br#"return $a || $b ? "yes" : "no";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::LoadVar("a".to_string())), + right: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))), + else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), + }))] + ); + } + + /// Verifies PHP's short ternary form omits the explicit then branch in EvalIR. + #[test] + fn parse_fragment_accepts_short_ternary_source() { + let program = + parse_fragment(br#"return $name ?: "fallback";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::LoadVar("name".to_string())), + then_branch: None, + else_branch: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), + }))] + ); + } + /// Verifies logical negation parses as a unary expression before comparisons. #[test] fn parse_fragment_accepts_logical_not_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 6d714e062e..d354faf949 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7af22e1b28..c0a4fa203f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index a7cc603951..c6bde9d60b 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -40,6 +40,7 @@ public function echoLabelThroughEval(): void { eval('if ($x >= 3) { echo "x>=3\n"; }'); eval('if ($x < 0) { echo "negative\n"; } elseif ($x == 3) { echo "x==3\n"; }'); eval('if ("10" !== 10) { echo "strict-ok\n"; }'); +$ternary = eval('return $x >= 3 ? "ternary-yes" : "ternary-no";'); eval('do { echo "do-once\n"; } while (false);'); eval('if (true) echo "single-if\n";'); eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); @@ -80,6 +81,7 @@ public function echoLabelThroughEval(): void { echo "eval-native-call=" . $eval_native_call . "\n"; echo "logic=" . $logic . "\n"; echo "not-false=" . $not_false . "\n"; +echo "ternary=" . $ternary . "\n"; echo "negative=" . $negative . "\n"; echo "magic-line=" . $magic_line . "\n"; echo "magic-function=" . $magic_function . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4b808fc521..c239640008 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -160,6 +160,23 @@ echo eval('return !"x";'); assert_eq!(out, "1:"); } +/// Verifies eval ternary operators short-circuit and return the selected branch. +#[test] +fn test_eval_ternary_executes_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 20:47:40 +0200 Subject: [PATCH 0039/1208] Support null coalescing inside eval fragments --- crates/elephc-eval/src/eval_ir.rs | 4 ++ crates/elephc-eval/src/interpreter.rs | 36 ++++++++++++++++ crates/elephc-eval/src/parser.rs | 60 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++++ 7 files changed, 117 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index f512f8eaa8..afcac824cf 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -158,6 +158,10 @@ pub enum EvalExpr { args: Vec, }, Magic(EvalMagicConst), + NullCoalesce { + value: Box, + default: Box, + }, PropertyGet { object: Box, property: String, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c2e0fd20ab..d9a79034fa 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -530,6 +530,14 @@ fn eval_expr( } values.method_call(object, method, evaluated_args) } + EvalExpr::NullCoalesce { value, default } => { + let value = eval_expr(value, context, scope, values)?; + if values.is_null(value)? { + eval_expr(default, context, scope, values) + } else { + Ok(value) + } + } EvalExpr::PropertyGet { object, property } => { let object = eval_expr(object, context, scope, values)?; values.property_get(object, property) @@ -1919,6 +1927,34 @@ mod tests { assert_eq!(values.output, "xfallback"); } + /// Verifies null coalescing uses the default for missing or null values. + #[test] + fn execute_program_null_coalesce_uses_default_for_missing_or_null() { + let program = + parse_fragment(br#"echo $missing ?? "fallback"; echo $x ?? "null-fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.null().expect("create fake null"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "fallbacknull-fallback"); + } + + /// Verifies null coalescing skips the default expression for non-null values. + #[test] + fn execute_program_null_coalesce_short_circuits_non_null_value() { + let program = parse_fragment(br#"echo "set" ?? missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "set"); + } + /// Verifies logical negation returns boolean cells using PHP truthiness. #[test] fn execute_program_evaluates_logical_not() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index b6735df0ef..1e013f780e 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -62,6 +62,7 @@ enum TokenKind { GreaterEqual, FatArrow, Question, + QuestionQuestion, Semicolon, LParen, RParen, @@ -206,7 +207,12 @@ impl<'a> Lexer<'a> { } '?' => { self.bump_char(); - Ok(TokenKind::Question) + if self.peek_char() == Some('?') { + self.bump_char(); + Ok(TokenKind::QuestionQuestion) + } else { + Ok(TokenKind::Question) + } } ';' => { self.bump_char(); @@ -792,7 +798,7 @@ impl Parser { /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. fn parse_ternary(&mut self) -> Result { - let condition = self.parse_logical_or()?; + let condition = self.parse_null_coalesce()?; if !self.consume(TokenKind::Question) { return Ok(condition); } @@ -811,6 +817,19 @@ impl Parser { }) } + /// Parses right-associative null coalescing below logical OR and above ternary. + fn parse_null_coalesce(&mut self) -> Result { + let value = self.parse_logical_or()?; + if !self.consume(TokenKind::QuestionQuestion) { + return Ok(value); + } + let default = self.parse_null_coalesce()?; + Ok(EvalExpr::NullCoalesce { + value: Box::new(value), + default: Box::new(default), + }) + } + /// Parses left-associative logical OR with lower precedence than logical AND. fn parse_logical_or(&mut self) -> Result { let mut expr = self.parse_logical_and()?; @@ -1577,6 +1596,43 @@ mod tests { ); } + /// Verifies null coalescing parses as a right-associative expression. + #[test] + fn parse_fragment_accepts_null_coalesce_source() { + let program = + parse_fragment(br#"return $a ?? $b ?? "fallback";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("a".to_string())), + default: Box::new(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("b".to_string())), + default: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), + }), + }))] + ); + } + + /// Verifies null coalescing binds tighter than PHP ternary expressions. + #[test] + fn parse_fragment_null_coalesce_binds_tighter_than_ternary() { + let program = + parse_fragment(br#"return $a ?? $b ? "yes" : "no";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("a".to_string())), + default: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))), + else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), + }))] + ); + } + /// Verifies logical negation parses as a unary expression before comparisons. #[test] fn parse_fragment_accepts_logical_not_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d354faf949..5eea60ed7d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c0a4fa203f..87f8d3537f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index c6bde9d60b..d2fd6e414b 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -57,6 +57,7 @@ public function echoLabelThroughEval(): void { $eval_native_call = eval('return compiled_add(2, 8);'); $logic = eval('return true || missing_eval_rhs();'); $not_false = eval('return !false;'); +$coalesced = eval('return $missing ?? "coalesced";'); $negative = eval('return -5 + +2;'); $magic_line = eval(" return __LINE__; @@ -81,6 +82,7 @@ public function echoLabelThroughEval(): void { echo "eval-native-call=" . $eval_native_call . "\n"; echo "logic=" . $logic . "\n"; echo "not-false=" . $not_false . "\n"; +echo "coalesce=" . $coalesced . "\n"; echo "ternary=" . $ternary . "\n"; echo "negative=" . $negative . "\n"; echo "magic-line=" . $magic_line . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c239640008..278659be2d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -177,6 +177,21 @@ echo eval('return false ?: "fallback";'); assert_eq!(out, "yes:no:x:fallback"); } +/// Verifies eval null coalescing returns defaults only for missing or null values. +#[test] +fn test_eval_null_coalesce_executes_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 20:50:20 +0200 Subject: [PATCH 0040/1208] Support comments inside eval fragments --- crates/elephc-eval/src/errors.rs | 1 + crates/elephc-eval/src/parser.rs | 63 ++++++++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 11 ++++++ 5 files changed, 73 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/errors.rs b/crates/elephc-eval/src/errors.rs index ffd0c9c438..847bcf7af1 100644 --- a/crates/elephc-eval/src/errors.rs +++ b/crates/elephc-eval/src/errors.rs @@ -42,6 +42,7 @@ pub enum EvalParseError { UnexpectedEof, InvalidNumber, UnterminatedString, + UnterminatedComment, ExpectedVariable, ExpectedSemicolon, } diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 1e013f780e..b91ba29d82 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -108,7 +108,7 @@ impl<'a> Lexer<'a> { /// Reads the next token from the source. fn next_token(&mut self) -> Result { - self.skip_ws(); + self.skip_trivia()?; let Some(ch) = self.peek_char() else { return Ok(TokenKind::Eof); }; @@ -337,13 +337,46 @@ impl<'a> Lexer<'a> { Err(EvalParseError::UnterminatedString) } - /// Advances past ASCII and Unicode whitespace. - fn skip_ws(&mut self) { - while self.peek_char().is_some_and(char::is_whitespace) { + /// Advances past ASCII/Unicode whitespace and PHP comments. + fn skip_trivia(&mut self) -> Result<(), EvalParseError> { + loop { + while self.peek_char().is_some_and(char::is_whitespace) { + self.bump_char(); + } + match (self.peek_char(), self.peek_next_char()) { + (Some('/'), Some('/')) => self.skip_line_comment(), + (Some('#'), _) => self.skip_line_comment(), + (Some('/'), Some('*')) => self.skip_block_comment()?, + _ => return Ok(()), + } + } + } + + /// Advances past a `//` or `#` comment, including its trailing newline when present. + fn skip_line_comment(&mut self) { + while let Some(ch) = self.peek_char() { self.bump_char(); + if ch == '\n' { + break; + } } } + /// Advances past a `/* ... */` comment while preserving fragment line metadata. + fn skip_block_comment(&mut self) -> Result<(), EvalParseError> { + self.bump_char(); + self.bump_char(); + while let Some(ch) = self.peek_char() { + if ch == '*' && self.peek_next_char() == Some('/') { + self.bump_char(); + self.bump_char(); + return Ok(()); + } + self.bump_char(); + } + Err(EvalParseError::UnterminatedComment) + } + /// Returns the current char without advancing. fn peek_char(&self) -> Option { self.source[self.pos..].chars().next() @@ -1486,6 +1519,28 @@ mod tests { ); } + /// Verifies PHP comments are skipped while preserving fragment line numbers. + #[test] + fn parse_fragment_skips_comments_and_preserves_line_metadata() { + let program = parse_fragment(b"// leading\n# hash\n/* block\ncomment */ return __LINE__;") + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Magic( + EvalMagicConst::Line(4) + )))] + ); + } + + /// Verifies unterminated block comments fail before partial EvalIR is returned. + #[test] + fn parse_fragment_rejects_unterminated_block_comment() { + assert_eq!( + parse_fragment(b"/* open").unwrap_err(), + EvalParseError::UnterminatedComment + ); + } + /// Verifies comparison operators parse with lower precedence than arithmetic. #[test] fn parse_fragment_accepts_comparison_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 5eea60ed7d..02163c4a4d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 87f8d3537f..ffadbd1e09 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 278659be2d..35bfb82785 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -70,6 +70,17 @@ fn test_eval_print_return_value_is_one() { assert_eq!(out, "x1"); } +/// Verifies eval fragments accept PHP comments and keep line metadata aligned. +#[test] +fn test_eval_comments_execute_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 20:56:13 +0200 Subject: [PATCH 0041/1208] Support logical keywords inside eval fragments --- crates/elephc-eval/src/eval_ir.rs | 1 + crates/elephc-eval/src/interpreter.rs | 39 +++++++++++- crates/elephc-eval/src/parser.rs | 85 ++++++++++++++++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 3 +- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 17 +++++ 8 files changed, 146 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index afcac824cf..e639894c9d 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -229,6 +229,7 @@ pub enum EvalBinOp { Concat, LogicalAnd, LogicalOr, + LogicalXor, LooseEq, LooseNotEq, StrictEq, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d9a79034fa..98da179cba 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -606,6 +606,11 @@ fn eval_expr( EvalBinOp::Sub => values.sub(left, right), EvalBinOp::Mul => values.mul(left, right), EvalBinOp::Concat => values.concat(left, right), + EvalBinOp::LogicalXor => { + let left_truthy = values.truthy(left)?; + let right_truthy = values.truthy(right)?; + values.bool_value(left_truthy ^ right_truthy) + } EvalBinOp::LooseEq | EvalBinOp::LooseNotEq | EvalBinOp::StrictEq @@ -1478,7 +1483,8 @@ mod tests { | EvalBinOp::Mul | EvalBinOp::Concat | EvalBinOp::LogicalAnd - | EvalBinOp::LogicalOr => { + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor => { return Err(EvalStatus::UnsupportedConstruct); } }; @@ -1900,6 +1906,37 @@ mod tests { assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. + #[test] + fn execute_program_evaluates_keyword_logical_operators() { + let program = parse_fragment( + br#"echo (false || true and false) ? "T" : "F"; return true or missing();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "F"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies PHP keyword `xor` evaluates both operands and returns a boolean cell. + #[test] + fn execute_program_evaluates_keyword_xor() { + let program = parse_fragment( + br#"echo (true xor false) ? "T" : "F"; echo (true xor true) ? "T" : "F";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TF"); + } + /// Verifies ternary expressions evaluate only the selected branch. #[test] fn execute_program_ternary_short_circuits_unselected_branch() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index b91ba29d82..ece42917f8 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -826,7 +826,52 @@ impl Parser { /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. fn parse_expr(&mut self) -> Result { - self.parse_ternary() + self.parse_keyword_or() + } + + /// Parses PHP keyword `or`, whose precedence is lower than `xor`, `and`, and ternary. + fn parse_keyword_or(&mut self) -> Result { + let mut expr = self.parse_keyword_xor()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "or")) { + self.advance(); + let right = self.parse_keyword_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `xor`, whose operands are evaluated before boolean XOR. + fn parse_keyword_xor(&mut self) -> Result { + let mut expr = self.parse_keyword_and()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "xor")) { + self.advance(); + let right = self.parse_keyword_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `and`, whose precedence is lower than ternary and `&&`. + fn parse_keyword_and(&mut self) -> Result { + let mut expr = self.parse_ternary()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "and")) { + self.advance(); + let right = self.parse_ternary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) } /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. @@ -1615,6 +1660,44 @@ mod tests { ); } + /// Verifies PHP logical keywords parse case-insensitively with their own precedence. + #[test] + fn parse_fragment_accepts_keyword_logical_source() { + let program = + parse_fragment(br#"return false || true AnD false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); + } + + /// Verifies PHP `xor` binds between `or` and `and` in eval expressions. + #[test] + fn parse_fragment_accepts_keyword_xor_source() { + let program = + parse_fragment(br#"return true XoR false or false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); + } + /// Verifies ternary expressions parse below logical OR and preserve both branches. #[test] fn parse_fragment_accepts_ternary_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 22ed1a3dff..f8eec88838 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -361,6 +361,7 @@ fn compare_op_tag(op: EvalBinOp) -> u64 { | EvalBinOp::Mul | EvalBinOp::Concat | EvalBinOp::LogicalAnd - | EvalBinOp::LogicalOr => 0, + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor => 0, } } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 02163c4a4d..1e8dfefb12 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ffadbd1e09..793509d85e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index d2fd6e414b..152ce64d2d 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -56,6 +56,7 @@ public function echoLabelThroughEval(): void { $dynamic_cufa = eval('return call_user_func_array("plus_one", [8]);'); $eval_native_call = eval('return compiled_add(2, 8);'); $logic = eval('return true || missing_eval_rhs();'); +$keyword_logic = eval('return true xor false;'); $not_false = eval('return !false;'); $coalesced = eval('return $missing ?? "coalesced";'); $negative = eval('return -5 + +2;'); @@ -81,6 +82,7 @@ public function echoLabelThroughEval(): void { echo "dynamic-cufa=" . $dynamic_cufa . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; echo "logic=" . $logic . "\n"; +echo "keyword-logic=" . $keyword_logic . "\n"; echo "not-false=" . $not_false . "\n"; echo "coalesce=" . $coalesced . "\n"; echo "ternary=" . $ternary . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 35bfb82785..cbb96c643e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -158,6 +158,23 @@ echo eval('return true || missing_eval_rhs();'); assert_eq!(out, "ab:1"); } +/// Verifies eval supports PHP logical keyword operators with PHP precedence. +#[test] +fn test_eval_logical_keyword_operators_execute_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 21:01:39 +0200 Subject: [PATCH 0042/1208] Support eval compound variable assignments --- crates/elephc-eval/src/interpreter.rs | 16 ++++ crates/elephc-eval/src/parser.rs | 130 +++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 13 +++ 6 files changed, 149 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 98da179cba..c3058c1b40 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1627,6 +1627,22 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(7)); } + /// Verifies simple variable compound assignments read, compute, and write the scope value. + #[test] + fn execute_program_evaluates_compound_assignments() { + let program = + parse_fragment(br#"$x = 2; $x += 3; $x *= 4; $x -= 5; $s = "v"; $s .= $x; echo $s;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "v15"); + assert_eq!(values.get(x), FakeValue::Int(15)); + } + /// Verifies echo and unset operate through runtime hooks and scope metadata. #[test] fn execute_program_echoes_and_unsets_scope_value() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index ece42917f8..12dd7bb2eb 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -44,10 +44,14 @@ enum TokenKind { Float(f64), String(String), Plus, + PlusEqual, Minus, + MinusEqual, Arrow, Star, + StarEqual, Dot, + DotEqual, Equal, EqualEqual, EqualEqualEqual, @@ -119,24 +123,42 @@ impl<'a> Lexer<'a> { '0'..='9' => self.lex_number(), '+' => { self.bump_char(); - Ok(TokenKind::Plus) + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PlusEqual) + } else { + Ok(TokenKind::Plus) + } } '-' => { self.bump_char(); if self.peek_char() == Some('>') { self.bump_char(); Ok(TokenKind::Arrow) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::MinusEqual) } else { Ok(TokenKind::Minus) } } '*' => { self.bump_char(); - Ok(TokenKind::Star) + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::StarEqual) + } else { + Ok(TokenKind::Star) + } } '.' => { self.bump_char(); - Ok(TokenKind::Dot) + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::DotEqual) + } else { + Ok(TokenKind::Dot) + } } '=' => { self.bump_char(); @@ -468,13 +490,9 @@ impl Parser { TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { self.parse_array_set_stmt(name.clone()) } - TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::Equal) => { + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { let name = name.clone(); - self.advance(); - self.advance(); - let value = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::StoreVar { name, value }]) + self.parse_var_store_stmt(name, true) } TokenKind::Eof => Err(EvalParseError::UnexpectedEof), _ => { @@ -634,12 +652,9 @@ impl Parser { TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(false) } - TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::Equal) => { + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { let name = name.clone(); - self.advance(); - self.advance(); - let value = self.parse_expr()?; - Ok(vec![EvalStmt::StoreVar { name, value }]) + self.parse_var_store_stmt(name, false) } _ => { let expr = self.parse_expr()?; @@ -659,6 +674,25 @@ impl Parser { Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) } + /// Parses `$name = expr` and simple variable compound assignments. + fn parse_var_store_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = assignment_value(&name, op, value); + Ok(vec![EvalStmt::StoreVar { name, value }]) + } + /// Parses `$object->property` as either an expression statement or property write. fn parse_property_stmt( &mut self, @@ -1307,6 +1341,30 @@ fn is_switch_case_boundary(token: &TokenKind) -> bool { || matches!(token, TokenKind::Ident(name) if ident_eq(name, "case") || ident_eq(name, "default")) } +/// Maps simple variable assignment tokens to an optional compound EvalIR operator. +fn assignment_op(token: &TokenKind) -> Option> { + match token { + TokenKind::Equal => Some(None), + TokenKind::PlusEqual => Some(Some(EvalBinOp::Add)), + TokenKind::MinusEqual => Some(Some(EvalBinOp::Sub)), + TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), + TokenKind::DotEqual => Some(Some(EvalBinOp::Concat)), + _ => None, + } +} + +/// Builds the assigned value expression for plain and compound variable assignment. +fn assignment_value(name: &str, op: Option, value: EvalExpr) -> EvalExpr { + match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::LoadVar(name.to_string())), + right: Box::new(value), + }, + None => value, + } +} + /// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. fn ident_eq(actual: &str, expected: &str) -> bool { actual.eq_ignore_ascii_case(expected) @@ -1330,6 +1388,50 @@ mod tests { ); } + /// Verifies simple variable compound assignments lower to StoreVar with binary expressions. + #[test] + fn parse_fragment_accepts_compound_assignment_source() { + let program = parse_fragment(br#"$x += 2; $x -= 1; $x *= 3; $s .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::StoreVar { + name: "s".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("s".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::String("ok".to_string()))), + }, + }, + ] + ); + } + /// Verifies echo fragments preserve expression source order. #[test] fn parse_fragment_accepts_echo_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 1e8dfefb12..9e24885270 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output, return, unset, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output, return, unset, simple variable compound assignments, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 793509d85e..7ba20a5d46 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 152ce64d2d..38f513403c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -59,6 +59,7 @@ public function echoLabelThroughEval(): void { $keyword_logic = eval('return true xor false;'); $not_false = eval('return !false;'); $coalesced = eval('return $missing ?? "coalesced";'); +$compound = eval('$n = 2; $n += 3; $label = "n="; $label .= $n; return $label;'); $negative = eval('return -5 + +2;'); $magic_line = eval(" return __LINE__; @@ -86,6 +87,7 @@ public function echoLabelThroughEval(): void { echo "not-false=" . $not_false . "\n"; echo "coalesce=" . $coalesced . "\n"; echo "ternary=" . $ternary . "\n"; +echo "compound=" . $compound . "\n"; echo "negative=" . $negative . "\n"; echo "magic-line=" . $magic_line . "\n"; echo "magic-function=" . $magic_function . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cbb96c643e..b23ea09dc9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -227,6 +227,19 @@ fn test_eval_unary_numeric_operators_execute_through_bridge() { assert_eq!(out, "-3"); } +/// Verifies eval simple variable compound assignments execute through existing value hooks. +#[test] +fn test_eval_compound_assignment_executes_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 21:05:47 +0200 Subject: [PATCH 0043/1208] Support eval variable inc dec statements --- crates/elephc-eval/src/interpreter.rs | 15 +++++ crates/elephc-eval/src/parser.rs | 86 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++ 6 files changed, 119 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c3058c1b40..902bf60f9d 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1643,6 +1643,21 @@ mod tests { assert_eq!(values.get(x), FakeValue::Int(15)); } + /// Verifies simple variable increment and decrement statements update the scope value. + #[test] + fn execute_program_evaluates_inc_dec_statements() { + let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "1"); + assert_eq!(values.get(i), FakeValue::Int(1)); + } + /// Verifies echo and unset operate through runtime hooks and scope metadata. #[test] fn execute_program_echoes_and_unsets_scope_value() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 12dd7bb2eb..574d634a7c 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -44,8 +44,10 @@ enum TokenKind { Float(f64), String(String), Plus, + PlusPlus, PlusEqual, Minus, + MinusMinus, MinusEqual, Arrow, Star, @@ -123,7 +125,10 @@ impl<'a> Lexer<'a> { '0'..='9' => self.lex_number(), '+' => { self.bump_char(); - if self.peek_char() == Some('=') { + if self.peek_char() == Some('+') { + self.bump_char(); + Ok(TokenKind::PlusPlus) + } else if self.peek_char() == Some('=') { self.bump_char(); Ok(TokenKind::PlusEqual) } else { @@ -135,6 +140,9 @@ impl<'a> Lexer<'a> { if self.peek_char() == Some('>') { self.bump_char(); Ok(TokenKind::Arrow) + } else if self.peek_char() == Some('-') { + self.bump_char(); + Ok(TokenKind::MinusMinus) } else if self.peek_char() == Some('=') { self.bump_char(); Ok(TokenKind::MinusEqual) @@ -484,12 +492,18 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), + TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(true) } TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { self.parse_array_set_stmt(name.clone()) } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), true) + } TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { let name = name.clone(); self.parse_var_store_stmt(name, true) @@ -646,12 +660,18 @@ impl Parser { /// Parses one statement-like `for` clause without consuming a delimiter. fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { match self.current() { + TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(false), TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { self.parse_array_set_clause(name.clone()) } TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(false) } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), false) + } TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { let name = name.clone(); self.parse_var_store_stmt(name, false) @@ -693,6 +713,39 @@ impl Parser { Ok(vec![EvalStmt::StoreVar { name, value }]) } + /// Parses prefix `++$name` and `--$name` as simple statement effects. + fn parse_prefix_inc_dec_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![inc_dec_store(name, increment)]) + } + + /// Parses postfix `$name++` and `$name--` as simple statement effects. + fn parse_postfix_inc_dec_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![inc_dec_store(name, increment)]) + } + /// Parses `$object->property` as either an expression statement or property write. fn parse_property_stmt( &mut self, @@ -1365,6 +1418,22 @@ fn assignment_value(name: &str, op: Option, value: EvalExpr) -> EvalE } } +/// Builds the StoreVar statement for a simple variable increment or decrement. +fn inc_dec_store(name: String, increment: bool) -> EvalStmt { + EvalStmt::StoreVar { + value: EvalExpr::Binary { + op: if increment { + EvalBinOp::Add + } else { + EvalBinOp::Sub + }, + left: Box::new(EvalExpr::LoadVar(name.clone())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + name, + } +} + /// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. fn ident_eq(actual: &str, expected: &str) -> bool { actual.eq_ignore_ascii_case(expected) @@ -1432,6 +1501,21 @@ mod tests { ); } + /// Verifies simple variable increment and decrement statements lower to StoreVar. + #[test] + fn parse_fragment_accepts_inc_dec_statement_source() { + let program = parse_fragment(br#"$i++; ++$j; $k--; --$m;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + inc_dec_store("i".to_string(), true), + inc_dec_store("j".to_string(), true), + inc_dec_store("k".to_string(), false), + inc_dec_store("m".to_string(), false), + ] + ); + } + /// Verifies echo fragments preserve expression source order. #[test] fn parse_fragment_accepts_echo_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9e24885270..a91dc3dc81 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output, return, unset, simple variable compound assignments, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7ba20a5d46..25e747c2ce 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 38f513403c..edeb557793 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -60,6 +60,7 @@ public function echoLabelThroughEval(): void { $not_false = eval('return !false;'); $coalesced = eval('return $missing ?? "coalesced";'); $compound = eval('$n = 2; $n += 3; $label = "n="; $label .= $n; return $label;'); +$incdec = eval('$i = 0; $i++; ++$i; return $i;'); $negative = eval('return -5 + +2;'); $magic_line = eval(" return __LINE__; @@ -88,6 +89,7 @@ public function echoLabelThroughEval(): void { echo "coalesce=" . $coalesced . "\n"; echo "ternary=" . $ternary . "\n"; echo "compound=" . $compound . "\n"; +echo "incdec=" . $incdec . "\n"; echo "negative=" . $negative . "\n"; echo "magic-line=" . $magic_line . "\n"; echo "magic-function=" . $magic_function . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b23ea09dc9..06766ed836 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -240,6 +240,21 @@ eval('for ($i = 0; $i < 3; $i += 1) { echo $i; }'); assert_eq!(out, "v15:012"); } +/// Verifies eval simple variable increment and decrement statements execute in loops. +#[test] +fn test_eval_inc_dec_statements_execute_through_bridge() { + let out = compile_and_run( + r#" 0; --$k) { echo $k; }'); +"#, + ); + assert_eq!(out, "1:012:321"); +} + /// Verifies eval if/else branches use PHP truthiness and update the caller scope. #[test] fn test_eval_if_else_updates_scope() { From 1df9d51a478d074cec92ab3fa0f31d52d884fd64 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 21:09:14 +0200 Subject: [PATCH 0044/1208] Support eval echo comma lists --- crates/elephc-eval/src/interpreter.rs | 14 ++++++++++++++ crates/elephc-eval/src/parser.rs | 21 +++++++++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 1 + tests/codegen/eval.rs | 7 +++++++ 6 files changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 902bf60f9d..31e8c28ecd 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1675,6 +1675,20 @@ mod tests { assert!(scope.entry("name").expect("unset marker").flags().unset); } + /// Verifies comma-separated echo expressions are executed in source order. + #[test] + fn execute_program_echoes_comma_list() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let b = values.string("b").expect("create fake string"); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "abc"); + } + /// Verifies print writes output and returns integer 1. #[test] fn execute_program_print_returns_one() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 574d634a7c..c7e0c51124 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -472,9 +472,12 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "do") => self.parse_do_while_stmt(), TokenKind::Ident(name) if ident_eq(name, "echo") => { self.advance(); - let expr = self.parse_expr()?; + let mut statements = vec![EvalStmt::Echo(self.parse_expr()?)]; + while self.consume(TokenKind::Comma) { + statements.push(EvalStmt::Echo(self.parse_expr()?)); + } self.expect_semicolon()?; - Ok(vec![EvalStmt::Echo(expr)]) + Ok(statements) } TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), @@ -1530,6 +1533,20 @@ mod tests { ); } + /// Verifies PHP echo comma lists lower to one EvalIR echo statement per expression. + #[test] + fn parse_fragment_accepts_echo_comma_list_source() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("a".to_string()))), + EvalStmt::Echo(EvalExpr::LoadVar("b".to_string())), + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("c".to_string()))), + ] + ); + } + /// Verifies if/else fragments lower to branch statements with nested blocks. #[test] fn parse_fragment_accepts_if_else_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a91dc3dc81..7987259f0c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 25e747c2ce..a811634451 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index edeb557793..4e973eab06 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -46,6 +46,7 @@ public function echoLabelThroughEval(): void { eval('foreach ([1, 2] as $n) { echo "n=" . $n . "\n"; }'); eval('foreach (["a" => 1, "b" => 2] as $key => $value) { echo "pair=" . $key . ":" . $value . "\n"; }'); eval('switch (2) { case 1: echo "switch-one\n"; break; case 2: echo "switch-two\n"; break; }'); +eval('echo "echo-list=", "ok\n";'); eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); $meta = eval('return ["source" => "eval"];'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 06766ed836..47b71e9b8f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -56,6 +56,13 @@ fn test_eval_scalar_echo_executes_through_bridge() { assert_eq!(out, "x"); } +/// Verifies comma-separated echo expressions inside eval emit in source order. +#[test] +fn test_eval_echo_comma_list_executes_through_bridge() { + let out = compile_and_run(" Date: Mon, 15 Jun 2026 21:20:45 +0200 Subject: [PATCH 0045/1208] Support eval division and modulo --- crates/elephc-eval/src/eval_ir.rs | 2 + crates/elephc-eval/src/interpreter.rs | 67 ++++++++++++ crates/elephc-eval/src/parser.rs | 78 +++++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 24 +++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 4 + src/codegen_support/runtime/eval_bridge.rs | 119 +++++++++++++++++++++ tests/codegen/eval.rs | 15 +++ 9 files changed, 306 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index e639894c9d..cd59174d8a 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -226,6 +226,8 @@ pub enum EvalBinOp { Add, Sub, Mul, + Div, + Mod, Concat, LogicalAnd, LogicalOr, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 31e8c28ecd..c17d02132c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -130,6 +130,20 @@ pub trait RuntimeValueOps { right: RuntimeCellHandle, ) -> Result; + /// Divides two runtime cells using PHP numeric semantics. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Computes modulo for two runtime cells using PHP integer modulo semantics. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + /// Concatenates two runtime cells using PHP string conversion semantics. fn concat( &mut self, @@ -605,6 +619,8 @@ fn eval_expr( EvalBinOp::Add => values.add(left, right), EvalBinOp::Sub => values.sub(left, right), EvalBinOp::Mul => values.mul(left, right), + EvalBinOp::Div => values.div(left, right), + EvalBinOp::Mod => values.modulo(left, right), EvalBinOp::Concat => values.concat(left, right), EvalBinOp::LogicalXor => { let left_truthy = values.truthy(left)?; @@ -1451,6 +1467,34 @@ mod tests { } } + /// Divides fake numeric cells for interpreter tests. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_numeric(&self.get(right)); + if right == 0.0 { + return Err(EvalStatus::RuntimeFatal); + } + let left = self.fake_numeric(&self.get(left)); + self.float(left / right) + } + + /// Computes fake integer modulo for interpreter tests. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_int(&self.get(right)); + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let left = self.fake_int(&self.get(left)); + self.int(left % right) + } + /// Concatenates fake cells with simple string conversion for interpreter tests. fn concat( &mut self, @@ -1481,6 +1525,8 @@ mod tests { EvalBinOp::Add | EvalBinOp::Sub | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod | EvalBinOp::Concat | EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr @@ -1575,6 +1621,11 @@ mod tests { } } + /// Converts a fake value to the integer scalar used by modulo tests. + fn fake_int(&self, value: &FakeValue) -> i64 { + self.fake_numeric(value) as i64 + } + /// Returns fake PHP truthiness for already-loaded test values. fn fake_truthy(&self, value: &FakeValue) -> bool { match value { @@ -1643,6 +1694,22 @@ mod tests { assert_eq!(values.get(x), FakeValue::Int(15)); } + /// Verifies division and modulo evaluate through fake runtime numeric hooks. + #[test] + fn execute_program_evaluates_division_and_modulo() { + let program = parse_fragment(br#"$x = 20; $x /= 2; $x %= 6; echo $x; return 9 / 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "4"); + assert_eq!(values.get(x), FakeValue::Int(4)); + assert_eq!(values.get(result), FakeValue::Float(4.5)); + } + /// Verifies simple variable increment and decrement statements update the scope value. #[test] fn execute_program_evaluates_inc_dec_statements() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index c7e0c51124..53d7eff16e 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -52,6 +52,10 @@ enum TokenKind { Arrow, Star, StarEqual, + Slash, + SlashEqual, + Percent, + PercentEqual, Dot, DotEqual, Equal, @@ -159,6 +163,24 @@ impl<'a> Lexer<'a> { Ok(TokenKind::Star) } } + '/' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::SlashEqual) + } else { + Ok(TokenKind::Slash) + } + } + '%' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PercentEqual) + } else { + Ok(TokenKind::Percent) + } + } '.' => { self.bump_char(); if self.peek_char() == Some('=') { @@ -1111,13 +1133,22 @@ impl Parser { Ok(expr) } - /// Parses left-associative numeric multiplication. + /// Parses left-associative numeric multiplication, division, and modulo. fn parse_mul(&mut self) -> Result { let mut expr = self.parse_unary()?; - while self.consume(TokenKind::Star) { + loop { + let op = if self.consume(TokenKind::Star) { + EvalBinOp::Mul + } else if self.consume(TokenKind::Slash) { + EvalBinOp::Div + } else if self.consume(TokenKind::Percent) { + EvalBinOp::Mod + } else { + break; + }; let right = self.parse_unary()?; expr = EvalExpr::Binary { - op: EvalBinOp::Mul, + op, left: Box::new(expr), right: Box::new(right), }; @@ -1404,6 +1435,8 @@ fn assignment_op(token: &TokenKind) -> Option> { TokenKind::PlusEqual => Some(Some(EvalBinOp::Add)), TokenKind::MinusEqual => Some(Some(EvalBinOp::Sub)), TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), + TokenKind::SlashEqual => Some(Some(EvalBinOp::Div)), + TokenKind::PercentEqual => Some(Some(EvalBinOp::Mod)), TokenKind::DotEqual => Some(Some(EvalBinOp::Concat)), _ => None, } @@ -1460,11 +1493,30 @@ mod tests { ); } + /// Verifies multiplicative operators preserve PHP precedence and associativity. + #[test] + fn parse_fragment_accepts_division_and_modulo_source() { + let program = parse_fragment(b"return 10 / 4 % 3;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mod, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Div, + left: Box::new(EvalExpr::Const(EvalConst::Int(10))), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); + } + /// Verifies simple variable compound assignments lower to StoreVar with binary expressions. #[test] fn parse_fragment_accepts_compound_assignment_source() { - let program = parse_fragment(br#"$x += 2; $x -= 1; $x *= 3; $s .= "ok";"#) - .expect("fragment should parse"); + let program = + parse_fragment(br#"$x += 2; $x -= 1; $x *= 3; $x /= 2; $x %= 5; $s .= "ok";"#) + .expect("fragment should parse"); assert_eq!( program.statements(), &[ @@ -1492,6 +1544,22 @@ mod tests { right: Box::new(EvalExpr::Const(EvalConst::Int(3))), }, }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Div, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Mod, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(5))), + }, + }, EvalStmt::StoreVar { name: "s".to_string(), value: EvalExpr::Binary { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index f8eec88838..196aaa5a56 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -68,6 +68,10 @@ unsafe extern "C" { -> *mut RuntimeCell; fn __elephc_eval_value_mul(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_div(left: *mut RuntimeCell, right: *mut RuntimeCell) + -> *mut RuntimeCell; + fn __elephc_eval_value_mod(left: *mut RuntimeCell, right: *mut RuntimeCell) + -> *mut RuntimeCell; fn __elephc_eval_value_concat( left: *mut RuntimeCell, right: *mut RuntimeCell, @@ -292,6 +296,24 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_mul(left.as_ptr(), right.as_ptr()) }) } + /// Divides two boxed Mixed cells using elephc runtime numeric semantics. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_div(left.as_ptr(), right.as_ptr()) }) + } + + /// Computes modulo for two boxed Mixed cells using elephc runtime integer semantics. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_mod(left.as_ptr(), right.as_ptr()) }) + } + /// Concatenates two boxed Mixed cells using elephc runtime string semantics. fn concat( &mut self, @@ -359,6 +381,8 @@ fn compare_op_tag(op: EvalBinOp) -> u64 { EvalBinOp::Add | EvalBinOp::Sub | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod | EvalBinOp::Concat | EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 7987259f0c..3430cad146 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar numeric arithmetic including division and modulo, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a811634451..c86b61d4ef 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `/`, `%`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 4e973eab06..b962c245df 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -63,6 +63,8 @@ public function echoLabelThroughEval(): void { $compound = eval('$n = 2; $n += 3; $label = "n="; $label .= $n; return $label;'); $incdec = eval('$i = 0; $i++; ++$i; return $i;'); $negative = eval('return -5 + +2;'); +$quotient = eval('return 9 / 2;'); +$modulo = eval('$n = 20; $n /= 2; return $n % 6;'); $magic_line = eval(" return __LINE__; "); @@ -92,6 +94,8 @@ public function echoLabelThroughEval(): void { echo "compound=" . $compound . "\n"; echo "incdec=" . $incdec . "\n"; echo "negative=" . $negative . "\n"; +echo "quotient=" . $quotient . "\n"; +echo "modulo=" . $modulo . "\n"; echo "magic-line=" . $magic_line . "\n"; echo "magic-function=" . $magic_function . "\n"; echo "magic-method=" . $magic_method . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 42ae11e995..b4fac80933 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -282,6 +282,63 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_mul"); emitter.instruction("b __rt_mixed_numeric_mul"); // multiply two boxed mixed values and return the boxed result + label_c_global(emitter, "__elephc_eval_value_div"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the left double across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("fcmp d0, #0.0"); // detect division by zero before the hardware divide + emitter.instruction("b.eq __elephc_eval_value_div_null"); // return null until eval has throwable propagation + emitter.instruction("fmov d1, d0"); // keep the right divisor in d1 + emitter.instruction("ldr d0, [sp, #8]"); // reload the left dividend into d0 + emitter.instruction("fdiv d0, d0, d1"); // compute PHP division as a double result + emitter.instruction("fmov x1, d0"); // move the double bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the division result into a Mixed cell + emitter.instruction("b __elephc_eval_value_div_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_div_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null fallback for division by zero + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for unsupported division-by-zero propagation + emitter.label("__elephc_eval_value_div_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the division wrapper frame + emitter.instruction("ret"); // return the boxed division result to Rust + + label_c_global(emitter, "__elephc_eval_value_mod"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left integer + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_int"); // cast the left boxed operand to a PHP integer + emitter.instruction("str x0, [sp, #8]"); // save the left integer across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for integer casting + emitter.instruction("bl __rt_mixed_cast_int"); // cast the right boxed operand to a PHP integer + emitter.instruction("cbz x0, __elephc_eval_value_mod_null"); // return null until eval has throwable propagation + emitter.instruction("mov x2, x0"); // keep the integer divisor in x2 + emitter.instruction("ldr x1, [sp, #8]"); // reload the integer dividend into x1 + emitter.instruction("sdiv x3, x1, x2"); // compute the signed integer quotient + emitter.instruction("msub x1, x3, x2, x1"); // compute dividend - quotient * divisor + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the modulo result into a Mixed cell + emitter.instruction("b __elephc_eval_value_mod_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_mod_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null fallback for modulo by zero + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for unsupported modulo-by-zero propagation + emitter.label("__elephc_eval_value_mod_done"); + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the modulo wrapper frame + emitter.instruction("ret"); // return the boxed modulo result to Rust + label_c_global(emitter, "__elephc_eval_value_concat"); emitter.instruction("sub sp, sp, #64"); // allocate wrapper frame for the right operand and string pairs emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across helper calls @@ -818,6 +875,68 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register emitter.instruction("jmp __rt_mixed_numeric_mul"); // multiply two boxed mixed values and return the boxed result + label_c_global(emitter, "__elephc_eval_value_div"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the left double across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("pxor xmm1, xmm1"); // materialize a zero double for divisor checking + emitter.instruction("ucomisd xmm0, xmm1"); // detect division by zero before the hardware divide + emitter.instruction("je __elephc_eval_value_div_null_x86"); // return null until eval has throwable propagation + emitter.instruction("movapd xmm1, xmm0"); // keep the right divisor in xmm1 + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // reload the left dividend into xmm0 + emitter.instruction("divsd xmm0, xmm1"); // compute PHP division as a double result + emitter.instruction("movq rdi, xmm0"); // move the double bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the division result into a Mixed cell + emitter.instruction("jmp __elephc_eval_value_div_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_div_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null fallback for division by zero + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for unsupported division-by-zero propagation + emitter.label("__elephc_eval_value_div_done_x86"); + emitter.instruction("add rsp, 32"); // release the division wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed division result to Rust + + label_c_global(emitter, "__elephc_eval_value_mod"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left integer + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_int input + emitter.instruction("call __rt_mixed_cast_int"); // cast the left boxed operand to a PHP integer + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the left integer across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for integer casting + emitter.instruction("call __rt_mixed_cast_int"); // cast the right boxed operand to a PHP integer + emitter.instruction("test rax, rax"); // detect modulo by zero before the hardware divide + emitter.instruction("jz __elephc_eval_value_mod_null_x86"); // return null until eval has throwable propagation + emitter.instruction("mov rdi, rax"); // keep the integer divisor in rdi + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the integer dividend into rax + emitter.instruction("cqo"); // sign-extend the dividend for signed division + emitter.instruction("idiv rdi"); // compute quotient in rax and remainder in rdx + emitter.instruction("mov rdi, rdx"); // move the integer remainder into mixed value_lo + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the modulo result into a Mixed cell + emitter.instruction("jmp __elephc_eval_value_mod_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_mod_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null fallback for modulo by zero + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for unsupported modulo-by-zero propagation + emitter.label("__elephc_eval_value_mod_done_x86"); + emitter.instruction("add rsp, 32"); // release the modulo wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed modulo result to Rust + label_c_global(emitter, "__elephc_eval_value_concat"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 47b71e9b8f..5f5cc334f8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -112,6 +112,21 @@ fn test_eval_scalar_add_executes_through_bridge() { assert_eq!(out, "9"); } +/// Verifies eval division and modulo execute through target-specific bridge wrappers. +#[test] +fn test_eval_division_modulo_execute_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 21:32:10 +0200 Subject: [PATCH 0046/1208] Support eval bitwise and shift operators --- crates/elephc-eval/src/eval_ir.rs | 6 + crates/elephc-eval/src/interpreter.rs | 80 +++++++ crates/elephc-eval/src/parser.rs | 230 ++++++++++++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 57 +++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 145 +++++++++++++ tests/codegen/eval.rs | 13 ++ 9 files changed, 528 insertions(+), 9 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index cd59174d8a..32ac6360e2 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -228,6 +228,11 @@ pub enum EvalBinOp { Mul, Div, Mod, + BitAnd, + BitOr, + BitXor, + ShiftLeft, + ShiftRight, Concat, LogicalAnd, LogicalOr, @@ -248,4 +253,5 @@ pub enum EvalUnaryOp { Plus, Negate, LogicalNot, + BitNot, } diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c17d02132c..f91ec8365f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -144,6 +144,17 @@ pub trait RuntimeValueOps { right: RuntimeCellHandle, ) -> Result; + /// Applies an integer bitwise or shift operation to two runtime cells. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Applies integer bitwise NOT to one runtime cell. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result; + /// Concatenates two runtime cells using PHP string conversion semantics. fn concat( &mut self, @@ -592,6 +603,7 @@ fn eval_expr( let truthy = values.truthy(value)?; values.bool_value(!truthy) } + EvalUnaryOp::BitNot => values.bit_not(value), } } EvalExpr::Binary { op, left, right } => { @@ -621,6 +633,11 @@ fn eval_expr( EvalBinOp::Mul => values.mul(left, right), EvalBinOp::Div => values.div(left, right), EvalBinOp::Mod => values.modulo(left, right), + EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight => values.bitwise(*op, left, right), EvalBinOp::Concat => values.concat(left, right), EvalBinOp::LogicalXor => { let left_truthy = values.truthy(left)?; @@ -1495,6 +1512,42 @@ mod tests { self.int(left % right) } + /// Applies fake integer bitwise and shift operations for interpreter tests. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_int(&self.get(left)); + let right = self.fake_int(&self.get(right)); + let value = match op { + EvalBinOp::BitAnd => left & right, + EvalBinOp::BitOr => left | right, + EvalBinOp::BitXor => left ^ right, + EvalBinOp::ShiftLeft => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); + } + left.wrapping_shl(right as u32) + } + EvalBinOp::ShiftRight => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); + } + left.wrapping_shr(right as u32) + } + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.int(value) + } + + /// Applies fake integer bitwise NOT for interpreter tests. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.fake_int(&self.get(value)); + self.int(!value) + } + /// Concatenates fake cells with simple string conversion for interpreter tests. fn concat( &mut self, @@ -1527,6 +1580,11 @@ mod tests { | EvalBinOp::Mul | EvalBinOp::Div | EvalBinOp::Mod + | EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight | EvalBinOp::Concat | EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr @@ -1710,6 +1768,28 @@ mod tests { assert_eq!(values.get(result), FakeValue::Float(4.5)); } + /// Verifies bitwise and shift operators evaluate through fake runtime hooks. + #[test] + fn execute_program_evaluates_bitwise_and_shift_ops() { + let program = parse_fragment( + br#"$x = 6; $x &= 3; echo $x; echo ":"; +$x = 4; $x |= 1; echo $x; echo ":"; +$x = 7; $x ^= 3; echo $x; echo ":"; +$x = 1; $x <<= 5; echo $x; echo ":"; +$x = 64; $x >>= 3; echo $x; echo ":"; +echo ~0; echo ":"; echo -16 >> 2; +return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:5:4:32:8:-1:-4"); + assert_eq!(values.get(result), FakeValue::Int(21)); + } + /// Verifies simple variable increment and decrement statements update the scope value. #[test] fn execute_program_evaluates_inc_dec_statements() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 53d7eff16e..4f7754c5d7 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -56,6 +56,13 @@ enum TokenKind { SlashEqual, Percent, PercentEqual, + Ampersand, + AmpEqual, + Pipe, + PipeEqual, + Caret, + CaretEqual, + Tilde, Dot, DotEqual, Equal, @@ -68,8 +75,12 @@ enum TokenKind { OrOr, Less, LessEqual, + LessLess, + LessLessEqual, Greater, GreaterEqual, + GreaterGreater, + GreaterGreaterEqual, FatArrow, Question, QuestionQuestion, @@ -226,8 +237,11 @@ impl<'a> Lexer<'a> { if self.peek_char() == Some('&') { self.bump_char(); Ok(TokenKind::AndAnd) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::AmpEqual) } else { - Err(EvalParseError::UnexpectedToken) + Ok(TokenKind::Ampersand) } } '|' => { @@ -235,13 +249,37 @@ impl<'a> Lexer<'a> { if self.peek_char() == Some('|') { self.bump_char(); Ok(TokenKind::OrOr) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PipeEqual) } else { - Err(EvalParseError::UnexpectedToken) + Ok(TokenKind::Pipe) } } - '<' => { + '^' => { self.bump_char(); if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::CaretEqual) + } else { + Ok(TokenKind::Caret) + } + } + '~' => { + self.bump_char(); + Ok(TokenKind::Tilde) + } + '<' => { + self.bump_char(); + if self.peek_char() == Some('<') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::LessLessEqual) + } else { + Ok(TokenKind::LessLess) + } + } else if self.peek_char() == Some('=') { self.bump_char(); Ok(TokenKind::LessEqual) } else { @@ -250,7 +288,15 @@ impl<'a> Lexer<'a> { } '>' => { self.bump_char(); - if self.peek_char() == Some('=') { + if self.peek_char() == Some('>') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::GreaterGreaterEqual) + } else { + Ok(TokenKind::GreaterGreater) + } + } else if self.peek_char() == Some('=') { self.bump_char(); Ok(TokenKind::GreaterEqual) } else { @@ -1036,9 +1082,9 @@ impl Parser { /// Parses left-associative logical AND with lower precedence than equality. fn parse_logical_and(&mut self) -> Result { - let mut expr = self.parse_equality()?; + let mut expr = self.parse_bit_or()?; while self.consume(TokenKind::AndAnd) { - let right = self.parse_equality()?; + let right = self.parse_bit_or()?; expr = EvalExpr::Binary { op: EvalBinOp::LogicalAnd, left: Box::new(expr), @@ -1048,6 +1094,48 @@ impl Parser { Ok(expr) } + /// Parses left-associative bitwise OR with lower precedence than bitwise XOR. + fn parse_bit_or(&mut self) -> Result { + let mut expr = self.parse_bit_xor()?; + while self.consume(TokenKind::Pipe) { + let right = self.parse_bit_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise XOR with lower precedence than bitwise AND. + fn parse_bit_xor(&mut self) -> Result { + let mut expr = self.parse_bit_and()?; + while self.consume(TokenKind::Caret) { + let right = self.parse_bit_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise AND with lower precedence than equality. + fn parse_bit_and(&mut self) -> Result { + let mut expr = self.parse_equality()?; + while self.consume(TokenKind::Ampersand) { + let right = self.parse_equality()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + /// Parses left-associative equality and inequality comparisons. fn parse_equality(&mut self) -> Result { let mut expr = self.parse_ordering()?; @@ -1075,7 +1163,7 @@ impl Parser { /// Parses left-associative ordered comparisons. fn parse_ordering(&mut self) -> Result { - let mut expr = self.parse_concat()?; + let mut expr = self.parse_shift()?; loop { let op = if self.consume(TokenKind::Less) { EvalBinOp::Lt @@ -1088,6 +1176,27 @@ impl Parser { } else { break; }; + let right = self.parse_shift()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative integer shift operators. + fn parse_shift(&mut self) -> Result { + let mut expr = self.parse_concat()?; + loop { + let op = if self.consume(TokenKind::LessLess) { + EvalBinOp::ShiftLeft + } else if self.consume(TokenKind::GreaterGreater) { + EvalBinOp::ShiftRight + } else { + break; + }; let right = self.parse_concat()?; expr = EvalExpr::Binary { op, @@ -1179,6 +1288,13 @@ impl Parser { expr: Box::new(expr), }); } + if self.consume(TokenKind::Tilde) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(expr), + }); + } self.parse_postfix() } @@ -1437,6 +1553,11 @@ fn assignment_op(token: &TokenKind) -> Option> { TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), TokenKind::SlashEqual => Some(Some(EvalBinOp::Div)), TokenKind::PercentEqual => Some(Some(EvalBinOp::Mod)), + TokenKind::AmpEqual => Some(Some(EvalBinOp::BitAnd)), + TokenKind::PipeEqual => Some(Some(EvalBinOp::BitOr)), + TokenKind::CaretEqual => Some(Some(EvalBinOp::BitXor)), + TokenKind::LessLessEqual => Some(Some(EvalBinOp::ShiftLeft)), + TokenKind::GreaterGreaterEqual => Some(Some(EvalBinOp::ShiftRight)), TokenKind::DotEqual => Some(Some(EvalBinOp::Concat)), _ => None, } @@ -1511,6 +1632,49 @@ mod tests { ); } + /// Verifies bitwise operators preserve PHP precedence. + #[test] + fn parse_fragment_accepts_bitwise_source() { + let program = parse_fragment(b"return ~0 | 2 ^ 3 & 4;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(EvalExpr::Const(EvalConst::Int(3))), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }), + }), + }))] + ); + } + + /// Verifies shift operators bind lower than additive expressions. + #[test] + fn parse_fragment_accepts_shift_source() { + let program = parse_fragment(b"return 1 + 2 << 3;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::ShiftLeft, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::Const(EvalConst::Int(1))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); + } + /// Verifies simple variable compound assignments lower to StoreVar with binary expressions. #[test] fn parse_fragment_accepts_compound_assignment_source() { @@ -1572,6 +1736,58 @@ mod tests { ); } + /// Verifies bitwise compound assignments lower to StoreVar with binary expressions. + #[test] + fn parse_fragment_accepts_bitwise_compound_assignment_source() { + let program = parse_fragment(br#"$x &= 3; $x |= 1; $x ^= 2; $x <<= 4; $x >>= 1;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::ShiftLeft, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::ShiftRight, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + ] + ); + } + /// Verifies simple variable increment and decrement statements lower to StoreVar. #[test] fn parse_fragment_accepts_inc_dec_statement_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 196aaa5a56..c09348e5ff 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -72,6 +72,12 @@ unsafe extern "C" { -> *mut RuntimeCell; fn __elephc_eval_value_mod(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_bitwise( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + op: u64, + ) -> *mut RuntimeCell; + fn __elephc_eval_value_bit_not(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_concat( left: *mut RuntimeCell, right: *mut RuntimeCell, @@ -314,6 +320,23 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_mod(left.as_ptr(), right.as_ptr()) }) } + /// Applies an integer bitwise or shift operation through the generated runtime wrapper. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_bitwise(left.as_ptr(), right.as_ptr(), bitwise_op_tag(op)) + }) + } + + /// Applies integer bitwise NOT through the generated runtime wrapper. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_bit_not(value.as_ptr()) }) + } + /// Concatenates two boxed Mixed cells using elephc runtime string semantics. fn concat( &mut self, @@ -383,9 +406,43 @@ fn compare_op_tag(op: EvalBinOp) -> u64 { | EvalBinOp::Mul | EvalBinOp::Div | EvalBinOp::Mod + | EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight | EvalBinOp::Concat | EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr | EvalBinOp::LogicalXor => 0, } } + +/// Maps bitwise EvalIR operators onto the generated runtime wrapper opcode table. +#[cfg(not(test))] +fn bitwise_op_tag(op: EvalBinOp) -> u64 { + match op { + EvalBinOp::BitAnd => 0, + EvalBinOp::BitOr => 1, + EvalBinOp::BitXor => 2, + EvalBinOp::ShiftLeft => 3, + EvalBinOp::ShiftRight => 4, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Concat + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor + | EvalBinOp::LooseEq + | EvalBinOp::LooseNotEq + | EvalBinOp::StrictEq + | EvalBinOp::StrictNotEq + | EvalBinOp::Lt + | EvalBinOp::LtEq + | EvalBinOp::Gt + | EvalBinOp::GtEq => 0, + } +} diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3430cad146..34c5b25a54 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar numeric arithmetic including division and modulo, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar numeric arithmetic including division and modulo, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c86b61d4ef..af32c46c52 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), binary `+`, `-`, `*`, `/`, `%`, `.`, logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b962c245df..77e246a306 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -65,6 +65,7 @@ public function echoLabelThroughEval(): void { $negative = eval('return -5 + +2;'); $quotient = eval('return 9 / 2;'); $modulo = eval('$n = 20; $n /= 2; return $n % 6;'); +$bitwise = eval('return (5 & 3) | (1 << 2);'); $magic_line = eval(" return __LINE__; "); @@ -96,6 +97,7 @@ public function echoLabelThroughEval(): void { echo "negative=" . $negative . "\n"; echo "quotient=" . $quotient . "\n"; echo "modulo=" . $modulo . "\n"; +echo "bitwise=" . $bitwise . "\n"; echo "magic-line=" . $magic_line . "\n"; echo "magic-function=" . $magic_function . "\n"; echo "magic-method=" . $magic_method . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index b4fac80933..cd5aa6810d 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -339,6 +339,76 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #32"); // release the modulo wrapper frame emitter.instruction("ret"); // return the boxed modulo result to Rust + label_c_global(emitter, "__elephc_eval_value_bit_not"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame for the cast helper call + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across the cast + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_int"); // cast the boxed operand to a PHP integer + emitter.instruction("mvn x1, x0"); // compute bitwise complement of the integer payload + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the bitwise NOT result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the bitwise NOT wrapper frame + emitter.instruction("ret"); // return the boxed bitwise NOT result to Rust + + label_c_global(emitter, "__elephc_eval_value_bitwise"); + emitter.instruction("sub sp, sp, #48"); // allocate wrapper slots for right operand, opcode, and left integer + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("str x2, [sp, #8]"); // save the eval bitwise opcode across helper calls + emitter.instruction("bl __rt_mixed_cast_int"); // cast the left boxed operand to a PHP integer + emitter.instruction("str x0, [sp, #16]"); // save the left integer across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for integer casting + emitter.instruction("bl __rt_mixed_cast_int"); // cast the right boxed operand to a PHP integer + emitter.instruction("ldr x1, [sp, #16]"); // reload the left integer into the payload register + emitter.instruction("ldr x2, [sp, #8]"); // reload the eval bitwise opcode for dispatch + emitter.instruction("cmp x2, #0"); // is this integer bitwise AND? + emitter.instruction("b.eq __elephc_eval_value_bitwise_and"); // route opcode 0 to integer AND + emitter.instruction("cmp x2, #1"); // is this integer bitwise OR? + emitter.instruction("b.eq __elephc_eval_value_bitwise_or"); // route opcode 1 to integer OR + emitter.instruction("cmp x2, #2"); // is this integer bitwise XOR? + emitter.instruction("b.eq __elephc_eval_value_bitwise_xor"); // route opcode 2 to integer XOR + emitter.instruction("cmp x2, #3"); // is this integer left shift? + emitter.instruction("b.eq __elephc_eval_value_bitwise_shl"); // route opcode 3 to integer left shift + emitter.instruction("cmp x2, #4"); // is this integer right shift? + emitter.instruction("b.eq __elephc_eval_value_bitwise_shr"); // route opcode 4 to integer right shift + emitter.instruction("b __elephc_eval_value_bitwise_null"); // fail closed for unknown bitwise opcodes + emitter.label("__elephc_eval_value_bitwise_and"); + emitter.instruction("and x1, x1, x0"); // compute integer bitwise AND + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_or"); + emitter.instruction("orr x1, x1, x0"); // compute integer bitwise OR + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_xor"); + emitter.instruction("eor x1, x1, x0"); // compute integer bitwise XOR + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_shl"); + emitter.instruction("cmp x0, #0"); // negative shift counts are runtime errors in PHP + emitter.instruction("b.lt __elephc_eval_value_bitwise_null"); // return null until eval has throwable propagation + emitter.instruction("lsl x1, x1, x0"); // shift the integer payload left + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer shift result + emitter.label("__elephc_eval_value_bitwise_shr"); + emitter.instruction("cmp x0, #0"); // negative shift counts are runtime errors in PHP + emitter.instruction("b.lt __elephc_eval_value_bitwise_null"); // return null until eval has throwable propagation + emitter.instruction("asr x1, x1, x0"); // shift the integer payload right arithmetically + emitter.instruction("b __elephc_eval_value_bitwise_box"); // box the integer shift result + emitter.label("__elephc_eval_value_bitwise_box"); + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the bitwise result into a Mixed cell + emitter.instruction("b __elephc_eval_value_bitwise_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_bitwise_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null fallback for unsupported bitwise errors + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for unsupported bitwise error propagation + emitter.label("__elephc_eval_value_bitwise_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the bitwise wrapper frame + emitter.instruction("ret"); // return the boxed bitwise result to Rust + label_c_global(emitter, "__elephc_eval_value_concat"); emitter.instruction("sub sp, sp, #64"); // allocate wrapper frame for the right operand and string pairs emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across helper calls @@ -937,6 +1007,81 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed modulo result to Rust + label_c_global(emitter, "__elephc_eval_value_bit_not"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // keep stack alignment for the cast and boxing calls + emitter.instruction("mov rax, rdi"); // move the boxed operand into mixed_cast_int input + emitter.instruction("call __rt_mixed_cast_int"); // cast the boxed operand to a PHP integer + emitter.instruction("not rax"); // compute bitwise complement of the integer payload + emitter.instruction("mov rdi, rax"); // move the complement into mixed value_lo + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the bitwise NOT result into a Mixed cell + emitter.instruction("add rsp, 16"); // release the bitwise NOT wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed bitwise NOT result to Rust + + label_c_global(emitter, "__elephc_eval_value_bitwise"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve slots for right operand, opcode, and left integer + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the eval bitwise opcode across helper calls + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_int input + emitter.instruction("call __rt_mixed_cast_int"); // cast the left boxed operand to a PHP integer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the left integer across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for integer casting + emitter.instruction("call __rt_mixed_cast_int"); // cast the right boxed operand to a PHP integer + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the left integer into the payload register + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the eval bitwise opcode for dispatch + emitter.instruction("cmp r10, 0"); // is this integer bitwise AND? + emitter.instruction("je __elephc_eval_value_bitwise_and_x86"); // route opcode 0 to integer AND + emitter.instruction("cmp r10, 1"); // is this integer bitwise OR? + emitter.instruction("je __elephc_eval_value_bitwise_or_x86"); // route opcode 1 to integer OR + emitter.instruction("cmp r10, 2"); // is this integer bitwise XOR? + emitter.instruction("je __elephc_eval_value_bitwise_xor_x86"); // route opcode 2 to integer XOR + emitter.instruction("cmp r10, 3"); // is this integer left shift? + emitter.instruction("je __elephc_eval_value_bitwise_shl_x86"); // route opcode 3 to integer left shift + emitter.instruction("cmp r10, 4"); // is this integer right shift? + emitter.instruction("je __elephc_eval_value_bitwise_shr_x86"); // route opcode 4 to integer right shift + emitter.instruction("jmp __elephc_eval_value_bitwise_null_x86"); // fail closed for unknown bitwise opcodes + emitter.label("__elephc_eval_value_bitwise_and_x86"); + emitter.instruction("and rdi, rax"); // compute integer bitwise AND + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_or_x86"); + emitter.instruction("or rdi, rax"); // compute integer bitwise OR + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_xor_x86"); + emitter.instruction("xor rdi, rax"); // compute integer bitwise XOR + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer bitwise result + emitter.label("__elephc_eval_value_bitwise_shl_x86"); + emitter.instruction("test rax, rax"); // negative shift counts are runtime errors in PHP + emitter.instruction("js __elephc_eval_value_bitwise_null_x86"); // return null until eval has throwable propagation + emitter.instruction("mov rcx, rax"); // move the shift count into the x86 shift-count register + emitter.instruction("shl rdi, cl"); // shift the integer payload left + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer shift result + emitter.label("__elephc_eval_value_bitwise_shr_x86"); + emitter.instruction("test rax, rax"); // negative shift counts are runtime errors in PHP + emitter.instruction("js __elephc_eval_value_bitwise_null_x86"); // return null until eval has throwable propagation + emitter.instruction("mov rcx, rax"); // move the shift count into the x86 shift-count register + emitter.instruction("sar rdi, cl"); // shift the integer payload right arithmetically + emitter.instruction("jmp __elephc_eval_value_bitwise_box_x86"); // box the integer shift result + emitter.label("__elephc_eval_value_bitwise_box_x86"); + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the bitwise result into a Mixed cell + emitter.instruction("jmp __elephc_eval_value_bitwise_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_bitwise_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null fallback for unsupported bitwise errors + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for unsupported bitwise error propagation + emitter.label("__elephc_eval_value_bitwise_done_x86"); + emitter.instruction("add rsp, 32"); // release the bitwise wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed bitwise result to Rust + label_c_global(emitter, "__elephc_eval_value_concat"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5f5cc334f8..57bdfc2f7f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -127,6 +127,19 @@ eval('$x = 20; $x /= 2; $x %= 6; echo $x;'); assert_eq!(out, "4.5:2:4"); } +/// Verifies eval integer bitwise and shift operators execute through bridge wrappers. +#[test] +fn test_eval_bitwise_shift_execute_through_bridge() { + let out = compile_and_run( + r#"> 2);'); +echo ":"; +eval('$x = 6; $x &= 3; echo $x; echo ","; $x = 4; $x |= 1; echo $x; echo ","; $x = 7; $x ^= 3; echo $x; echo ","; $x = 1; $x <<= 5; echo $x; echo ","; $x = 64; $x >>= 3; echo $x;'); +"#, + ); + assert_eq!(out, "1:7:6:-1:16:-4:2,5,4,32,8"); +} + /// Verifies the eval bridge routes concatenation through runtime string helpers. #[test] fn test_eval_scalar_concat_executes_through_bridge() { From f84c66c24d88ddf2091593394bb578cb1d49c0ce Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 21:40:44 +0200 Subject: [PATCH 0047/1208] Support eval spaceship operator --- crates/elephc-eval/src/eval_ir.rs | 1 + crates/elephc-eval/src/interpreter.rs | 41 ++++++++++++++++ crates/elephc-eval/src/parser.rs | 28 ++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 17 ++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 55 ++++++++++++++++++++++ tests/codegen/eval.rs | 11 +++++ 9 files changed, 155 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 32ac6360e2..63ac2bbff7 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -245,6 +245,7 @@ pub enum EvalBinOp { LtEq, Gt, GtEq, + Spaceship, } /// Unary operations supported by the initial EvalIR parser. diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f91ec8365f..5efa4ce163 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -170,6 +170,13 @@ pub trait RuntimeValueOps { right: RuntimeCellHandle, ) -> Result; + /// Compares two runtime cells and returns a boxed PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + /// Emits one runtime cell to stdout using PHP echo semantics. fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; @@ -652,6 +659,7 @@ fn eval_expr( | EvalBinOp::LtEq | EvalBinOp::Gt | EvalBinOp::GtEq => values.compare(*op, left, right), + EvalBinOp::Spaceship => values.spaceship(left, right), EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => { Err(EvalStatus::UnsupportedConstruct) } @@ -1586,6 +1594,7 @@ mod tests { | EvalBinOp::ShiftLeft | EvalBinOp::ShiftRight | EvalBinOp::Concat + | EvalBinOp::Spaceship | EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr | EvalBinOp::LogicalXor => { @@ -1595,6 +1604,24 @@ mod tests { self.bool_value(result) } + /// Compares fake numeric cells and returns a PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.numeric(left)?; + let right = self.numeric(right)?; + let value = if left < right { + -1 + } else if left > right { + 1 + } else { + 0 + }; + self.int(value) + } + /// Appends fake echo output for interpreter tests. fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { let value = self.stringify(value); @@ -2058,6 +2085,20 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.output, "1111ns"); } + /// Verifies spaceship comparisons return PHP -1/0/1 integer cells. + #[test] + fn execute_program_spaceship_returns_int_cells() { + let program = + parse_fragment(br#"echo 1 <=> 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "-1:0:1"); + } + /// Verifies strict equality keeps PHP type identity distinct from loose equality. #[test] fn execute_program_strict_equality_uses_type_identity() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 4f7754c5d7..2bcce29732 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -75,6 +75,7 @@ enum TokenKind { OrOr, Less, LessEqual, + Spaceship, LessLess, LessLessEqual, Greater, @@ -281,7 +282,12 @@ impl<'a> Lexer<'a> { } } else if self.peek_char() == Some('=') { self.bump_char(); - Ok(TokenKind::LessEqual) + if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::Spaceship) + } else { + Ok(TokenKind::LessEqual) + } } else { Ok(TokenKind::Less) } @@ -1173,6 +1179,8 @@ impl Parser { EvalBinOp::Gt } else if self.consume(TokenKind::GreaterEqual) { EvalBinOp::GtEq + } else if self.consume(TokenKind::Spaceship) { + EvalBinOp::Spaceship } else { break; }; @@ -2091,6 +2099,24 @@ mod tests { ); } + /// Verifies the spaceship operator parses at ordered-comparison precedence. + #[test] + fn parse_fragment_accepts_spaceship_source() { + let program = parse_fragment(br#"return $i + 1 <=> 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Spaceship, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); + } + /// Verifies loose equality operators parse as binary EvalIR expressions. #[test] fn parse_fragment_accepts_loose_equality_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index c09348e5ff..8b0efa5f96 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -87,6 +87,10 @@ unsafe extern "C" { right: *mut RuntimeCell, op: u64, ) -> *mut RuntimeCell; + fn __elephc_eval_value_spaceship( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; fn __elephc_eval_value_echo(value: *mut RuntimeCell); fn __elephc_eval_value_string_bytes( value: *mut RuntimeCell, @@ -358,6 +362,15 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Computes a PHP numeric spaceship result through the generated runtime wrapper. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_spaceship(left.as_ptr(), right.as_ptr()) }) + } + /// Emits one boxed Mixed cell to stdout through the generated runtime wrapper. fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { unsafe { @@ -412,6 +425,7 @@ fn compare_op_tag(op: EvalBinOp) -> u64 { | EvalBinOp::ShiftLeft | EvalBinOp::ShiftRight | EvalBinOp::Concat + | EvalBinOp::Spaceship | EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr | EvalBinOp::LogicalXor => 0, @@ -443,6 +457,7 @@ fn bitwise_op_tag(op: EvalBinOp) -> u64 { | EvalBinOp::Lt | EvalBinOp::LtEq | EvalBinOp::Gt - | EvalBinOp::GtEq => 0, + | EvalBinOp::GtEq + | EvalBinOp::Spaceship => 0, } } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 34c5b25a54..157405bd9a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar numeric arithmetic including division and modulo, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar numeric arithmetic including division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index af32c46c52..ae32ce8595 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 77e246a306..9ec2a12c7b 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -66,6 +66,7 @@ public function echoLabelThroughEval(): void { $quotient = eval('return 9 / 2;'); $modulo = eval('$n = 20; $n /= 2; return $n % 6;'); $bitwise = eval('return (5 & 3) | (1 << 2);'); +$spaceship = eval('return 3 <=> 2;'); $magic_line = eval(" return __LINE__; "); @@ -98,6 +99,7 @@ public function echoLabelThroughEval(): void { echo "quotient=" . $quotient . "\n"; echo "modulo=" . $modulo . "\n"; echo "bitwise=" . $bitwise . "\n"; +echo "spaceship=" . $spaceship . "\n"; echo "magic-line=" . $magic_line . "\n"; echo "magic-function=" . $magic_function . "\n"; echo "magic-method=" . $magic_method . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index cd5aa6810d..49399cb813 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -639,6 +639,31 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #96"); // release the loose-equality helper frame emitter.instruction("ret"); // return the loose-equality boolean in x0 + label_c_global(emitter, "__elephc_eval_value_spaceship"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the left numeric spaceship operand + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("ldr d1, [sp, #8]"); // reload the left numeric spaceship operand + emitter.instruction("fcmp d1, d0"); // compare left and right numeric operands for spaceship + emitter.instruction("b.vs __elephc_eval_value_spaceship_gt"); // PHP treats unordered NaN spaceship comparisons as greater + emitter.instruction("cset x1, gt"); // set result to 1 when left is greater than right + emitter.instruction("csinv x1, x1, xzr, ge"); // keep 1/0 for greater/equal, or produce -1 for less + emitter.instruction("b __elephc_eval_value_spaceship_box"); // box the ordered spaceship result + emitter.label("__elephc_eval_value_spaceship_gt"); + emitter.instruction("mov x1, #1"); // greater or unordered comparisons produce result 1 + emitter.label("__elephc_eval_value_spaceship_box"); + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the spaceship result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the spaceship wrapper frame + emitter.instruction("ret"); // return the boxed spaceship result to Rust + label_c_global(emitter, "__elephc_eval_value_echo"); emitter.instruction("b __rt_mixed_write_stdout"); // echo one boxed mixed value and return to Rust @@ -1357,6 +1382,36 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the loose-equality boolean in rax + label_c_global(emitter, "__elephc_eval_value_spaceship"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the left numeric spaceship operand + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 16]"); // reload the left numeric spaceship operand + emitter.instruction("ucomisd xmm1, xmm0"); // compare left and right numeric operands for spaceship + emitter.instruction("jp __elephc_eval_value_spaceship_gt_x86"); // PHP treats unordered NaN spaceship comparisons as greater + emitter.instruction("ja __elephc_eval_value_spaceship_gt_x86"); // route left > right to result 1 + emitter.instruction("jb __elephc_eval_value_spaceship_lt_x86"); // route left < right to result -1 + emitter.instruction("xor edi, edi"); // equal operands produce spaceship result 0 + emitter.instruction("jmp __elephc_eval_value_spaceship_box_x86"); // box the equal spaceship result + emitter.label("__elephc_eval_value_spaceship_gt_x86"); + emitter.instruction("mov rdi, 1"); // greater or unordered comparisons produce result 1 + emitter.instruction("jmp __elephc_eval_value_spaceship_box_x86"); // box the greater spaceship result + emitter.label("__elephc_eval_value_spaceship_lt_x86"); + emitter.instruction("mov rdi, -1"); // lesser comparisons produce result -1 + emitter.label("__elephc_eval_value_spaceship_box_x86"); + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the spaceship result into a Mixed cell + emitter.instruction("add rsp, 32"); // release the spaceship wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed spaceship result to Rust + label_c_global(emitter, "__elephc_eval_value_echo"); emitter.instruction("mov rax, rdi"); // move the C boxed value argument into mixed echo input emitter.instruction("jmp __rt_mixed_write_stdout"); // echo one boxed mixed value and return to Rust diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 57bdfc2f7f..66c202f573 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -158,6 +158,17 @@ eval('echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; echo 5 != 6; echo 7 == 7 assert_eq!(out, "111111"); } +/// Verifies eval spaceship comparisons return boxed -1/0/1 integers. +#[test] +fn test_eval_spaceship_execute_through_bridge() { + let out = compile_and_run( + r#" 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;'); +"#, + ); + assert_eq!(out, "-1:0:1"); +} + /// Verifies loose scalar equality in eval handles strings and null/empty-string rules. #[test] fn test_eval_scalar_loose_equality_executes_through_bridge() { From 5c9406c4e99a140dc0e2ebd27c5236496a9be32a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 21:50:25 +0200 Subject: [PATCH 0048/1208] Support eval exponentiation operator --- crates/elephc-eval/src/eval_ir.rs | 1 + crates/elephc-eval/src/interpreter.rs | 44 ++++++++++++- crates/elephc-eval/src/parser.rs | 75 +++++++++++++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 13 ++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 41 ++++++++++++ tests/codegen/eval.rs | 15 +++++ 9 files changed, 188 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 63ac2bbff7..da50886dd6 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -228,6 +228,7 @@ pub enum EvalBinOp { Mul, Div, Mod, + Pow, BitAnd, BitOr, BitXor, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5efa4ce163..2c9b000ec0 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -144,6 +144,13 @@ pub trait RuntimeValueOps { right: RuntimeCellHandle, ) -> Result; + /// Raises one runtime cell to another using PHP exponentiation semantics. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + /// Applies an integer bitwise or shift operation to two runtime cells. fn bitwise( &mut self, @@ -640,6 +647,7 @@ fn eval_expr( EvalBinOp::Mul => values.mul(left, right), EvalBinOp::Div => values.div(left, right), EvalBinOp::Mod => values.modulo(left, right), + EvalBinOp::Pow => values.pow(left, right), EvalBinOp::BitAnd | EvalBinOp::BitOr | EvalBinOp::BitXor @@ -1464,7 +1472,7 @@ mod tests { ) -> Result { match (self.get(left), self.get(right)) { (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), - _ => Err(EvalStatus::UnsupportedConstruct), + (left, right) => self.float(self.fake_numeric(&left) + self.fake_numeric(&right)), } } @@ -1476,7 +1484,7 @@ mod tests { ) -> Result { match (self.get(left), self.get(right)) { (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), - _ => Err(EvalStatus::UnsupportedConstruct), + (left, right) => self.float(self.fake_numeric(&left) - self.fake_numeric(&right)), } } @@ -1488,7 +1496,7 @@ mod tests { ) -> Result { match (self.get(left), self.get(right)) { (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), - _ => Err(EvalStatus::UnsupportedConstruct), + (left, right) => self.float(self.fake_numeric(&left) * self.fake_numeric(&right)), } } @@ -1520,6 +1528,17 @@ mod tests { self.int(left % right) } + /// Raises fake numeric cells for interpreter tests. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left.powf(right)) + } + /// Applies fake integer bitwise and shift operations for interpreter tests. fn bitwise( &mut self, @@ -1588,6 +1607,7 @@ mod tests { | EvalBinOp::Mul | EvalBinOp::Div | EvalBinOp::Mod + | EvalBinOp::Pow | EvalBinOp::BitAnd | EvalBinOp::BitOr | EvalBinOp::BitXor @@ -1795,6 +1815,24 @@ mod tests { assert_eq!(values.get(result), FakeValue::Float(4.5)); } + /// Verifies exponentiation evaluates through fake runtime numeric hooks. + #[test] + fn execute_program_evaluates_exponentiation() { + let program = parse_fragment( + br#"$x = 2; $x **= 3; echo $x; echo ":"; echo -2 ** 2; return 2 ** 3 ** 2;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "8:-4"); + assert_eq!(values.get(x), FakeValue::Float(8.0)); + assert_eq!(values.get(result), FakeValue::Float(512.0)); + } + /// Verifies bitwise and shift operators evaluate through fake runtime hooks. #[test] fn execute_program_evaluates_bitwise_and_shift_ops() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 2bcce29732..35e5c771b2 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -51,6 +51,8 @@ enum TokenKind { MinusEqual, Arrow, Star, + StarStar, + StarStarEqual, StarEqual, Slash, SlashEqual, @@ -168,7 +170,15 @@ impl<'a> Lexer<'a> { } '*' => { self.bump_char(); - if self.peek_char() == Some('=') { + if self.peek_char() == Some('*') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::StarStarEqual) + } else { + Ok(TokenKind::StarStar) + } + } else if self.peek_char() == Some('=') { self.bump_char(); Ok(TokenKind::StarEqual) } else { @@ -1303,7 +1313,21 @@ impl Parser { expr: Box::new(expr), }); } - self.parse_postfix() + self.parse_power() + } + + /// Parses right-associative exponentiation with higher precedence than unary prefix operators. + fn parse_power(&mut self) -> Result { + let mut expr = self.parse_postfix()?; + if self.consume(TokenKind::StarStar) { + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) } /// Parses postfix array reads, property reads, and method calls after a primary expression. @@ -1559,6 +1583,7 @@ fn assignment_op(token: &TokenKind) -> Option> { TokenKind::PlusEqual => Some(Some(EvalBinOp::Add)), TokenKind::MinusEqual => Some(Some(EvalBinOp::Sub)), TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), + TokenKind::StarStarEqual => Some(Some(EvalBinOp::Pow)), TokenKind::SlashEqual => Some(Some(EvalBinOp::Div)), TokenKind::PercentEqual => Some(Some(EvalBinOp::Mod)), TokenKind::AmpEqual => Some(Some(EvalBinOp::BitAnd)), @@ -1640,6 +1665,35 @@ mod tests { ); } + /// Verifies exponentiation is right-associative and binds tighter than unary negation. + #[test] + fn parse_fragment_accepts_power_source() { + let program = + parse_fragment(b"return -2 ** 2; return 2 ** 3 ** 2;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + })), + EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(3))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + })), + ] + ); + } + /// Verifies bitwise operators preserve PHP precedence. #[test] fn parse_fragment_accepts_bitwise_source() { @@ -1744,6 +1798,23 @@ mod tests { ); } + /// Verifies exponentiation compound assignment lowers through the binary power operator. + #[test] + fn parse_fragment_accepts_power_compound_assignment_source() { + let program = parse_fragment(br#"$x **= 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }] + ); + } + /// Verifies bitwise compound assignments lower to StoreVar with binary expressions. #[test] fn parse_fragment_accepts_bitwise_compound_assignment_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 8b0efa5f96..eaeb23455c 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -72,6 +72,8 @@ unsafe extern "C" { -> *mut RuntimeCell; fn __elephc_eval_value_mod(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_pow(left: *mut RuntimeCell, right: *mut RuntimeCell) + -> *mut RuntimeCell; fn __elephc_eval_value_bitwise( left: *mut RuntimeCell, right: *mut RuntimeCell, @@ -324,6 +326,15 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_mod(left.as_ptr(), right.as_ptr()) }) } + /// Raises two boxed Mixed cells using elephc runtime numeric exponentiation semantics. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_pow(left.as_ptr(), right.as_ptr()) }) + } + /// Applies an integer bitwise or shift operation through the generated runtime wrapper. fn bitwise( &mut self, @@ -419,6 +430,7 @@ fn compare_op_tag(op: EvalBinOp) -> u64 { | EvalBinOp::Mul | EvalBinOp::Div | EvalBinOp::Mod + | EvalBinOp::Pow | EvalBinOp::BitAnd | EvalBinOp::BitOr | EvalBinOp::BitXor @@ -446,6 +458,7 @@ fn bitwise_op_tag(op: EvalBinOp) -> u64 { | EvalBinOp::Mul | EvalBinOp::Div | EvalBinOp::Mod + | EvalBinOp::Pow | EvalBinOp::Concat | EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 157405bd9a..22de44a71c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar numeric arithmetic including division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ae32ce8595..01ae32838f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 9ec2a12c7b..cc75bb58a7 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -65,6 +65,7 @@ public function echoLabelThroughEval(): void { $negative = eval('return -5 + +2;'); $quotient = eval('return 9 / 2;'); $modulo = eval('$n = 20; $n /= 2; return $n % 6;'); +$power = eval('return 2 ** 3;'); $bitwise = eval('return (5 & 3) | (1 << 2);'); $spaceship = eval('return 3 <=> 2;'); $magic_line = eval(" @@ -98,6 +99,7 @@ public function echoLabelThroughEval(): void { echo "negative=" . $negative . "\n"; echo "quotient=" . $quotient . "\n"; echo "modulo=" . $modulo . "\n"; +echo "power=" . $power . "\n"; echo "bitwise=" . $bitwise . "\n"; echo "spaceship=" . $spaceship . "\n"; echo "magic-line=" . $magic_line . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 49399cb813..aafdce94c8 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -339,6 +339,26 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #32"); // release the modulo wrapper frame emitter.instruction("ret"); // return the boxed modulo result to Rust + label_c_global(emitter, "__elephc_eval_value_pow"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the exponentiation base across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("fmov d1, d0"); // move the exponent into libc pow's second argument + emitter.instruction("ldr d0, [sp, #8]"); // reload the base into libc pow's first argument + emitter.bl_c("pow"); + emitter.instruction("fmov x1, d0"); // move the pow result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the exponentiation result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the exponentiation wrapper frame + emitter.instruction("ret"); // return the boxed exponentiation result to Rust + label_c_global(emitter, "__elephc_eval_value_bit_not"); emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame for the cast helper call emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across the cast @@ -1032,6 +1052,27 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed modulo result to Rust + label_c_global(emitter, "__elephc_eval_value_pow"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the exponentiation base across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("movapd xmm1, xmm0"); // move the exponent into libc pow's second argument + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // reload the base into libc pow's first argument + emitter.bl_c("pow"); + emitter.instruction("movq rdi, xmm0"); // move the pow result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the exponentiation result into a Mixed cell + emitter.instruction("add rsp, 32"); // release the exponentiation wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed exponentiation result to Rust + label_c_global(emitter, "__elephc_eval_value_bit_not"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 66c202f573..c62b5e6d6d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -127,6 +127,21 @@ eval('$x = 20; $x /= 2; $x %= 6; echo $x;'); assert_eq!(out, "4.5:2:4"); } +/// Verifies eval exponentiation executes through the target-specific bridge wrapper. +#[test] +fn test_eval_exponentiation_execute_through_bridge() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 21:55:33 +0200 Subject: [PATCH 0049/1208] Document eval scope magic constants --- crates/elephc-eval/src/interpreter.rs | 15 +++++++++++++++ crates/elephc-eval/src/parser.rs | 23 +++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 17 +++++++++++++++++ 6 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2c9b000ec0..094ce07e8f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -2440,6 +2440,21 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, ); } + /// Verifies eval class, namespace, and trait magic constants are empty in eval scope. + #[test] + fn execute_program_scope_magic_constants_are_empty_strings() { + let program = parse_fragment( + br#"return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("[||]".to_string())); + } + /// Verifies eval-declared functions can be called by the same fragment. #[test] fn execute_program_calls_declared_function() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 35e5c771b2..cdc392bbbc 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -2130,6 +2130,29 @@ mod tests { ); } + /// Verifies eval scope magic constants lower to explicit EvalIR nodes. + #[test] + fn parse_fragment_accepts_scope_magic_constants() { + let program = parse_fragment(b"return __CLASS__ . __NAMESPACE__ . __TRAIT__ . __METHOD__;") + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Magic(EvalMagicConst::Class)), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Namespace)), + }), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Trait)), + }), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Method)), + }))] + ); + } + /// Verifies PHP comments are skipped while preserving fragment line numbers. #[test] fn parse_fragment_skips_comments_and_preserves_line_metadata() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 22de44a71c..b97653a0b0 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 01ae32838f..5a5e1e59a0 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Eval caller class/namespace metadata is still pending. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index cc75bb58a7..83c69b0eac 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -77,6 +77,7 @@ public function echoLabelThroughEval(): void { $magic_method = eval('return evalmagicmethodname();'); $magic_file_has_path = eval('return strlen(__FILE__) > strlen(__DIR__);'); $magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); +$magic_scope = eval('return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -107,6 +108,7 @@ public function echoLabelThroughEval(): void { echo "magic-method=" . $magic_method . "\n"; echo "magic-file=" . $magic_file_has_path . "\n"; echo "magic-dir=" . $magic_dir_has_path . "\n"; +echo "magic-scope=" . $magic_scope . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c62b5e6d6d..a982ef7067 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -720,6 +720,23 @@ if (strlen(__FILE__) > strlen(__DIR__)) { echo "F"; } else { echo "f"; }'); assert_eq!(out, "D:F"); } +/// Verifies eval scope magic constants are empty even from namespaced method callers. +#[test] +fn test_eval_scope_magic_constants_are_empty_strings() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "[||]"); +} + /// Verifies eval-declared functions persist across eval calls in the same generated context. #[test] fn test_eval_declared_function_persists_across_eval_calls() { From db8e369d5c4ea16e2ded9d1277cc96702d25208a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 22:02:44 +0200 Subject: [PATCH 0050/1208] Support eval type predicate builtins --- crates/elephc-eval/src/interpreter.rs | 105 +++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 6 ++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 17 ++++ tests/codegen/eval.rs | 19 ++++ 7 files changed, 151 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 094ce07e8f..4ccb17e22f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -91,6 +91,9 @@ pub trait RuntimeValueOps { /// Returns whether a runtime cell holds PHP null. fn is_null(&mut self, value: RuntimeCellHandle) -> Result; + /// Returns the concrete boxed Mixed runtime tag after unwrapping nested Mixed cells. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result; + /// Releases one owned runtime cell that is no longer held by the eval scope. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; @@ -194,6 +197,16 @@ pub trait RuntimeValueOps { fn truthy(&mut self, value: RuntimeCellHandle) -> Result; } +const EVAL_TAG_INT: u64 = 0; +const EVAL_TAG_STRING: u64 = 1; +const EVAL_TAG_FLOAT: u64 = 2; +const EVAL_TAG_BOOL: u64 = 3; +const EVAL_TAG_ARRAY: u64 = 4; +const EVAL_TAG_ASSOC: u64 = 5; +#[cfg(test)] +const EVAL_TAG_OBJECT: u64 = 6; +const EVAL_TAG_NULL: u64 = 8; + /// Executes an EvalIR program and returns the eval result cell. pub fn execute_program( program: &EvalProgram, @@ -693,6 +706,10 @@ fn eval_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) } + "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" + | "is_null" | "is_real" | "is_string" => { + eval_builtin_type_predicate(name, args, context, scope, values) + } "isset" => eval_builtin_isset(args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), _ => { @@ -804,6 +821,16 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "count" | "function_exists" | "is_callable" + | "is_array" + | "is_bool" + | "is_double" + | "is_float" + | "is_int" + | "is_integer" + | "is_long" + | "is_null" + | "is_real" + | "is_string" | "strlen" ) } @@ -935,6 +962,13 @@ fn eval_builtin_with_values( let name = name.trim_start_matches('\\').to_ascii_lowercase(); values.bool_value(eval_function_probe_exists(context, &name))? } + "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" + | "is_null" | "is_real" | "is_string" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_type_predicate_result(name, *value, values)? + } "strlen" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -948,6 +982,40 @@ fn eval_builtin_with_values( Ok(Some(result)) } +/// Evaluates PHP scalar/container type predicate builtins over one eval expression. +fn eval_builtin_type_predicate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_type_predicate_result(name, value, values) +} + +/// Converts a concrete runtime tag into a PHP `is_*` predicate result. +fn eval_type_predicate_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + let result = match name { + "is_int" | "is_integer" | "is_long" => tag == EVAL_TAG_INT, + "is_float" | "is_double" | "is_real" => tag == EVAL_TAG_FLOAT, + "is_string" => tag == EVAL_TAG_STRING, + "is_bool" => tag == EVAL_TAG_BOOL, + "is_null" => tag == EVAL_TAG_NULL, + "is_array" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(result) +} + /// Evaluates nested `eval(...)` calls against the current materialized scope. fn eval_nested_eval( args: &[EvalExpr], @@ -1433,6 +1501,20 @@ mod tests { Ok(matches!(self.get(value), FakeValue::Null)) } + /// Returns the fake runtime tag corresponding to a test value. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Int(_) => EVAL_TAG_INT, + FakeValue::String(_) => EVAL_TAG_STRING, + FakeValue::Float(_) => EVAL_TAG_FLOAT, + FakeValue::Bool(_) => EVAL_TAG_BOOL, + FakeValue::Array(_) => EVAL_TAG_ARRAY, + FakeValue::Assoc(_) => EVAL_TAG_ASSOC, + FakeValue::Object(_) => EVAL_TAG_OBJECT, + FakeValue::Null => EVAL_TAG_NULL, + }) + } + /// Records fake releases without freeing handles needed for assertions. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { self.releases.push(value); @@ -2634,6 +2716,29 @@ return call_user_func_array("dyn", [4, 5]);"#, assert_eq!(values.get(result), FakeValue::Int(6)); } + /// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. + #[test] + fn execute_program_dispatches_type_predicate_builtins() { + let program = parse_fragment( + br#"echo is_int(1); echo is_integer(1); echo is_long(1); +echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); +echo is_string("x"); echo is_bool(false); echo is_null(null); +echo is_array([1]); echo is_array(["a" => 1]); +echo is_array(1) ? "bad" : "ok"; +echo ":"; echo call_user_func("is_string", "x"); +echo call_user_func_array("is_array", [[1]]); +return function_exists("is_double");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "11111111111ok:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index eaeb23455c..0ef58c9da8 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -57,6 +57,7 @@ unsafe extern "C" { fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; + fn __elephc_eval_value_type_tag(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_null() -> *mut RuntimeCell; fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; @@ -248,6 +249,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_is_null(value.as_ptr()) != 0 }) } + /// Returns the unboxed Mixed runtime tag for PHP type-predicate builtins. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_type_tag(value.as_ptr()) }) + } + /// Releases one boxed Mixed cell through the generated runtime wrapper. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { unsafe { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index b97653a0b0..268b4287ad 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 5a5e1e59a0..00171b4dc9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 83c69b0eac..d64641ada9 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -78,6 +78,7 @@ public function echoLabelThroughEval(): void { $magic_file_has_path = eval('return strlen(__FILE__) > strlen(__DIR__);'); $magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); $magic_scope = eval('return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";'); +$type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?");'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -109,6 +110,7 @@ public function echoLabelThroughEval(): void { echo "magic-file=" . $magic_file_has_path . "\n"; echo "magic-dir=" . $magic_dir_has_path . "\n"; echo "magic-scope=" . $magic_scope . "\n"; +echo "type-checks=" . $type_checks . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index aafdce94c8..276c8a397e 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -255,6 +255,15 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the null-check wrapper frame emitter.instruction("ret"); // return the boolean result to Rust + label_c_global(emitter, "__elephc_eval_value_type_tag"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the Mixed cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells and return the concrete runtime tag + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the type-tag wrapper frame + emitter.instruction("ret"); // return the unboxed runtime tag to Rust + label_c_global(emitter, "__elephc_eval_value_int"); emitter.instruction("mov x1, x0"); // move the C integer argument into the mixed payload slot emitter.instruction("mov x0, #0"); // runtime tag 0 = integer @@ -960,6 +969,14 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boolean result to Rust + label_c_global(emitter, "__elephc_eval_value_type_tag"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // pass the boxed Mixed argument to mixed_unbox + emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells and return the concrete runtime tag + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the unboxed runtime tag to Rust + label_c_global(emitter, "__elephc_eval_value_int"); emitter.instruction("mov eax, 0"); // runtime tag 0 = integer emitter.instruction("xor esi, esi"); // integer payloads do not use a high word diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a982ef7067..4af09b8fbc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -628,6 +628,25 @@ eval('echo STRLEN("abcd") . ":" . count([1, 2, 3]);'); assert_eq!(out, "4:3"); } +/// Verifies eval scalar type-predicate builtins inspect boxed Mixed runtime tags. +#[test] +fn test_eval_dispatches_type_predicate_builtin_calls() { + let out = compile_and_run( + r#" 1]); +echo is_array(1) ? "bad" : "ok"; +echo ":"; +echo call_user_func("is_string", "x"); +echo call_user_func_array("is_array", [[1]]); +echo function_exists("is_double");'); +"#, + ); + assert_eq!(out, "11111111111ok:111"); +} + /// Verifies eval `isset()` distinguishes missing, null, and falsey non-null values. #[test] fn test_eval_isset_distinguishes_missing_null_and_falsey_values() { From 1b3674d95a51c0b5ac80f9c7ebe9a1683b336d58 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 22:12:57 +0200 Subject: [PATCH 0051/1208] Support eval cast builtins --- crates/elephc-eval/src/interpreter.rs | 109 +++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 24 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 174 +++++++++++++++++++++ tests/codegen/eval.rs | 17 ++ 7 files changed, 328 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 4ccb17e22f..6308ac1ae9 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -112,6 +112,18 @@ pub trait RuntimeValueOps { /// Creates a runtime string cell. fn string(&mut self, value: &str) -> Result; + /// Casts one runtime cell to a boxed PHP integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP float cell. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP string cell. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result; + /// Adds two runtime cells using PHP addition semantics. fn add( &mut self, @@ -700,6 +712,9 @@ fn eval_call( match name { "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "boolval" | "floatval" | "intval" | "strval" => { + eval_builtin_cast(name, args, context, scope, values) + } "count" => eval_builtin_count(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), @@ -818,8 +833,11 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { name, "call_user_func" | "call_user_func_array" + | "boolval" | "count" + | "floatval" | "function_exists" + | "intval" | "is_callable" | "is_array" | "is_bool" @@ -832,6 +850,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_real" | "is_string" | "strlen" + | "strval" ) } @@ -945,6 +964,12 @@ fn eval_builtin_with_values( return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) .map(Some); } + "boolval" | "floatval" | "intval" | "strval" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_cast_result(name, *value, values)? + } "count" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -982,6 +1007,36 @@ fn eval_builtin_with_values( Ok(Some(result)) } +/// Evaluates PHP scalar cast builtins over one eval expression. +fn eval_builtin_cast( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_cast_result(name, value, values) +} + +/// Dispatches an already evaluated value through the matching PHP cast hook. +fn eval_cast_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "intval" => values.cast_int(value), + "floatval" => values.cast_float(value), + "strval" => values.cast_string(value), + "boolval" => values.cast_bool(value), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Evaluates PHP scalar/container type predicate builtins over one eval expression. fn eval_builtin_type_predicate( name: &str, @@ -1546,6 +1601,39 @@ mod tests { Ok(self.alloc(FakeValue::String(value.to_string()))) } + /// Casts a fake runtime cell to a fake integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + let value = self.fake_int(&value); + self.int(value) + } + + /// Casts a fake runtime cell to a fake float cell. + fn cast_float( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + let value = self.fake_numeric(&value); + self.float(value) + } + + /// Casts a fake runtime cell to a fake string cell. + fn cast_string( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.stringify(value); + self.string(&value) + } + + /// Casts a fake runtime cell to a fake boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + let value = self.fake_truthy(&value); + self.bool_value(value) + } + /// Adds fake numeric cells for interpreter tests. fn add( &mut self, @@ -2739,6 +2827,27 @@ return function_exists("is_double");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval cast builtins return boxed scalar cells directly and by callable. + #[test] + fn execute_program_dispatches_cast_builtins() { + let program = parse_fragment( + br#"echo intval("42"); echo ":"; +echo floatval("3.5"); echo ":"; +echo strval(12); echo ":"; +echo boolval("0") ? "bad" : "false"; +echo ":"; echo call_user_func("strval", 7); +return call_user_func_array("intval", ["9"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "42:3.5:12:false:7"); + assert_eq!(values.get(result), FakeValue::Int(9)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 0ef58c9da8..44db3a145c 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -63,6 +63,10 @@ unsafe extern "C" { fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; fn __elephc_eval_value_float(value: f64) -> *mut RuntimeCell; fn __elephc_eval_value_string(ptr: *const u8, len: u64) -> *mut RuntimeCell; + fn __elephc_eval_value_cast_int(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_cast_float(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_cast_string(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_cast_bool(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_add(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_sub(left: *mut RuntimeCell, right: *mut RuntimeCell) @@ -287,6 +291,26 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) } + /// Casts a boxed Mixed cell to a boxed integer Mixed cell through the generated runtime wrapper. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_int(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed float Mixed cell through the generated runtime wrapper. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_float(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed string Mixed cell through the generated runtime wrapper. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_string(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed boolean Mixed cell through the generated runtime wrapper. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_bool(value.as_ptr()) }) + } + /// Adds two boxed Mixed cells using elephc runtime numeric semantics. fn add( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 268b4287ad..14ee964b4a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 00171b4dc9..909630c8b2 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index d64641ada9..647379137e 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -79,6 +79,7 @@ public function echoLabelThroughEval(): void { $magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); $magic_scope = eval('return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";'); $type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?");'); +$casts = eval('return strval(intval("42")) . ":" . strval(floatval("3.5")) . ":" . (boolval("0") ? "true" : "false");'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -111,6 +112,7 @@ public function echoLabelThroughEval(): void { echo "magic-dir=" . $magic_dir_has_path . "\n"; echo "magic-scope=" . $magic_scope . "\n"; echo "type-checks=" . $type_checks . "\n"; +echo "casts=" . $casts . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 276c8a397e..193bb61dbd 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -264,6 +264,91 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the type-tag wrapper frame emitter.instruction("ret"); // return the unboxed runtime tag to Rust + label_c_global(emitter, "__elephc_eval_value_cast_int"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing the value + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_int"); // cast the boxed eval value to a PHP integer payload + emitter.instruction("mov x1, x0"); // move the integer cast result into mixed value_lo + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cast integer result for Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the cast wrapper frame + emitter.instruction("ret"); // return the boxed integer cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_float"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing the value + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval value to a PHP double payload + emitter.instruction("fmov x1, d0"); // move the double cast bits into mixed value_lo + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cast double result for Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the cast wrapper frame + emitter.instruction("ret"); // return the boxed double cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_string"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing and boxing the string result + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete payload tag and value words + emitter.instruction("cmp x0, #0"); // is the eval value an integer? + emitter.instruction("b.eq __elephc_eval_value_cast_string_int"); // integers cast through decimal formatting + emitter.instruction("cmp x0, #1"); // is the eval value already a string? + emitter.instruction("b.eq __elephc_eval_value_cast_string_box"); // strings can be boxed through the normal ownership path + emitter.instruction("cmp x0, #2"); // is the eval value a double? + emitter.instruction("b.eq __elephc_eval_value_cast_string_float"); // doubles cast through decimal formatting + emitter.instruction("cmp x0, #3"); // is the eval value a boolean? + emitter.instruction("b.eq __elephc_eval_value_cast_string_bool"); // booleans cast to "1" or the empty string + emitter.label("__elephc_eval_value_cast_string_empty"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("mov x1, xzr"); // unsupported and falsey payloads use an empty string pointer + emitter.instruction("mov x2, xzr"); // unsupported and falsey payloads use an empty string length + emitter.instruction("bl __rt_mixed_from_value"); // box the empty string result for Rust + emitter.instruction("b __elephc_eval_value_cast_string_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_int"); + emitter.instruction("mov x0, x1"); // pass the integer payload to decimal formatting + emitter.instruction("bl __rt_itoa"); // format the integer cast result as a string pair + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the formatted integer string + emitter.instruction("b __elephc_eval_value_cast_string_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_box"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the existing string payload once + emitter.instruction("b __elephc_eval_value_cast_string_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_float"); + emitter.instruction("fmov d0, x1"); // move the double payload bits into the FP argument register + emitter.instruction("bl __rt_ftoa"); // format the double cast result as a string pair + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the formatted double string + emitter.instruction("b __elephc_eval_value_cast_string_done"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_bool"); + emitter.instruction("cbz x1, __elephc_eval_value_cast_string_empty"); // false casts to the empty string + emitter.instruction("mov x0, x1"); // pass the true payload to decimal formatting + emitter.instruction("bl __rt_itoa"); // format true as the string "1" + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the true string result + emitter.label("__elephc_eval_value_cast_string_done"); + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the string-cast wrapper frame + emitter.instruction("ret"); // return the boxed string cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_bool"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing the value + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_bool"); // cast the boxed eval value to PHP truthiness + emitter.instruction("mov x1, x0"); // move the boolean cast result into mixed value_lo + emitter.instruction("mov x0, #3"); // runtime tag 3 = boolean + emitter.instruction("mov x2, xzr"); // boolean payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cast boolean result for Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the cast wrapper frame + emitter.instruction("ret"); // return the boxed boolean cast result to Rust + label_c_global(emitter, "__elephc_eval_value_int"); emitter.instruction("mov x1, x0"); // move the C integer argument into the mixed payload slot emitter.instruction("mov x0, #0"); // runtime tag 0 = integer @@ -977,6 +1062,95 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the unboxed runtime tag to Rust + label_c_global(emitter, "__elephc_eval_value_cast_int"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_int input + emitter.instruction("call __rt_mixed_cast_int"); // cast the boxed eval value to a PHP integer payload + emitter.instruction("mov rdi, rax"); // move the integer cast result into mixed value_lo + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the cast integer result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed integer cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_float"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval value to a PHP double payload + emitter.instruction("movq rdi, xmm0"); // move the double cast bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the cast double result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed double cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_string"); + emitter.instruction("push rbp"); // align the stack while unboxing and boxing the string result + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete payload tag and value words + emitter.instruction("cmp rax, 0"); // is the eval value an integer? + emitter.instruction("je __elephc_eval_value_cast_string_int_x86"); // integers cast through decimal formatting + emitter.instruction("cmp rax, 1"); // is the eval value already a string? + emitter.instruction("je __elephc_eval_value_cast_string_box_x86"); // strings can be boxed through the normal ownership path + emitter.instruction("cmp rax, 2"); // is the eval value a double? + emitter.instruction("je __elephc_eval_value_cast_string_float_x86"); // doubles cast through decimal formatting + emitter.instruction("cmp rax, 3"); // is the eval value a boolean? + emitter.instruction("je __elephc_eval_value_cast_string_bool_x86"); // booleans cast to \"1\" or the empty string + emitter.label("__elephc_eval_value_cast_string_empty_x86"); + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("xor edi, edi"); // unsupported and falsey payloads use an empty string pointer + emitter.instruction("xor esi, esi"); // unsupported and falsey payloads use an empty string length + emitter.instruction("call __rt_mixed_from_value"); // box the empty string result for Rust + emitter.instruction("jmp __elephc_eval_value_cast_string_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_int_x86"); + emitter.instruction("mov rax, rdi"); // pass the integer payload to decimal formatting + emitter.instruction("call __rt_itoa"); // format the integer cast result as a string pair + emitter.instruction("mov rdi, rax"); // move the formatted string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the formatted string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the formatted integer string + emitter.instruction("jmp __elephc_eval_value_cast_string_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_box_x86"); + emitter.instruction("mov rsi, rdx"); // move the existing string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the existing string payload once + emitter.instruction("jmp __elephc_eval_value_cast_string_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_float_x86"); + emitter.instruction("movq xmm0, rdi"); // move the double payload bits into the FP argument register + emitter.instruction("call __rt_ftoa"); // format the double cast result as a string pair + emitter.instruction("mov rdi, rax"); // move the formatted string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the formatted string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the formatted double string + emitter.instruction("jmp __elephc_eval_value_cast_string_done_x86"); // restore the wrapper frame and return + emitter.label("__elephc_eval_value_cast_string_bool_x86"); + emitter.instruction("test rdi, rdi"); // false casts to the empty string + emitter.instruction("je __elephc_eval_value_cast_string_empty_x86"); // route false to the empty string boxer + emitter.instruction("mov rax, rdi"); // pass the true payload to decimal formatting + emitter.instruction("call __rt_itoa"); // format true as the string \"1\" + emitter.instruction("mov rdi, rax"); // move the formatted string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the formatted string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the true string result + emitter.label("__elephc_eval_value_cast_string_done_x86"); + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed string cast result to Rust + + label_c_global(emitter, "__elephc_eval_value_cast_bool"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_bool input + emitter.instruction("call __rt_mixed_cast_bool"); // cast the boxed eval value to PHP truthiness + emitter.instruction("mov rdi, rax"); // move the boolean cast result into mixed value_lo + emitter.instruction("xor esi, esi"); // boolean payloads do not use a high word + emitter.instruction("mov eax, 3"); // runtime tag 3 = boolean + emitter.instruction("call __rt_mixed_from_value"); // box the cast boolean result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed boolean cast result to Rust + label_c_global(emitter, "__elephc_eval_value_int"); emitter.instruction("mov eax, 0"); // runtime tag 0 = integer emitter.instruction("xor esi, esi"); // integer payloads do not use a high word diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4af09b8fbc..f86cf3b8a5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -647,6 +647,23 @@ echo function_exists("is_double");'); assert_eq!(out, "11111111111ok:111"); } +/// Verifies eval scalar cast builtins return boxed Mixed cells through direct and callable calls. +#[test] +fn test_eval_dispatches_cast_builtin_calls() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 22:16:51 +0200 Subject: [PATCH 0052/1208] Support eval gettype builtin --- crates/elephc-eval/src/interpreter.rs | 76 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 23 ++++++++ 5 files changed, 102 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6308ac1ae9..bd81f4aef3 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -215,9 +215,9 @@ const EVAL_TAG_FLOAT: u64 = 2; const EVAL_TAG_BOOL: u64 = 3; const EVAL_TAG_ARRAY: u64 = 4; const EVAL_TAG_ASSOC: u64 = 5; -#[cfg(test)] const EVAL_TAG_OBJECT: u64 = 6; const EVAL_TAG_NULL: u64 = 8; +const EVAL_TAG_RESOURCE: u64 = 9; /// Executes an EvalIR program and returns the eval result cell. pub fn execute_program( @@ -721,6 +721,7 @@ fn eval_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) } + "gettype" => eval_builtin_gettype(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_real" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) @@ -837,6 +838,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "count" | "floatval" | "function_exists" + | "gettype" | "intval" | "is_callable" | "is_array" @@ -987,6 +989,12 @@ fn eval_builtin_with_values( let name = name.trim_start_matches('\\').to_ascii_lowercase(); values.bool_value(eval_function_probe_exists(context, &name))? } + "gettype" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gettype_result(*value, values)? + } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_real" | "is_string" => { let [value] = evaluated_args else { @@ -1037,6 +1045,44 @@ fn eval_cast_result( } } +/// Evaluates PHP's `gettype(...)` over one eval expression. +fn eval_builtin_gettype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_gettype_result(value, values) +} + +/// Converts one boxed runtime tag into PHP's `gettype()` spelling. +fn eval_gettype_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.string(eval_gettype_name(tag)) +} + +/// Returns the PHP-visible type name for a concrete eval runtime tag. +fn eval_gettype_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "integer", + EVAL_TAG_FLOAT => "double", + EVAL_TAG_STRING => "string", + EVAL_TAG_BOOL => "boolean", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_OBJECT => "object", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_NULL => "NULL", + _ => "NULL", + } +} + /// Evaluates PHP scalar/container type predicate builtins over one eval expression. fn eval_builtin_type_predicate( name: &str, @@ -2848,6 +2894,34 @@ return call_user_func_array("intval", ["9"]);"#, assert_eq!(values.get(result), FakeValue::Int(9)); } + /// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. + #[test] + fn execute_program_dispatches_gettype_builtin() { + let program = parse_fragment( + br#"echo gettype(1); echo ":"; +echo gettype(1.5); echo ":"; +echo gettype("x"); echo ":"; +echo gettype(false); echo ":"; +echo gettype(null); echo ":"; +echo gettype([1]); echo ":"; +echo gettype(["a" => 1]); echo ":"; +echo call_user_func("gettype", true); echo ":"; +echo call_user_func_array("gettype", [null]); +return function_exists("gettype");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "integer:double:string:boolean:NULL:array:array:boolean:NULL" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 14ee964b4a..34d15df570 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 909630c8b2..bbf0cea563 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 647379137e..8ffb031014 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -80,6 +80,7 @@ public function echoLabelThroughEval(): void { $magic_scope = eval('return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";'); $type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?");'); $casts = eval('return strval(intval("42")) . ":" . strval(floatval("3.5")) . ":" . (boolval("0") ? "true" : "false");'); +$type_name = eval('return gettype(["ok"]);'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -113,6 +114,7 @@ public function echoLabelThroughEval(): void { echo "magic-scope=" . $magic_scope . "\n"; echo "type-checks=" . $type_checks . "\n"; echo "casts=" . $casts . "\n"; +echo "type-name=" . $type_name . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f86cf3b8a5..d9984a8482 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -664,6 +664,29 @@ echo ":"; echo function_exists("boolval");'); assert_eq!(out, "42:3.5:12:false:7:9:1"); } +/// Verifies eval `gettype()` maps boxed Mixed runtime tags to PHP type names. +#[test] +fn test_eval_dispatches_gettype_builtin_call() { + let out = compile_and_run( + r#" 1]); echo ":"; +echo call_user_func("gettype", true); echo ":"; +echo call_user_func_array("gettype", [null]); +echo ":"; echo function_exists("gettype");'); +"#, + ); + assert_eq!( + out, + "integer:double:string:boolean:NULL:array:array:boolean:NULL:1" + ); +} + /// Verifies eval `isset()` distinguishes missing, null, and falsey non-null values. #[test] fn test_eval_isset_distinguishes_missing_null_and_falsey_values() { From 398617ba5391a720de3de137c4c3d804f10a1379 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 22:21:19 +0200 Subject: [PATCH 0053/1208] Support eval abs builtin --- crates/elephc-eval/src/interpreter.rs | 56 +++++++++++++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 6 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 7 +++ tests/codegen/eval.rs | 16 +++++++ 7 files changed, 88 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index bd81f4aef3..af17123d84 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -124,6 +124,9 @@ pub trait RuntimeValueOps { /// Casts one runtime cell to a boxed PHP boolean cell. fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result; + /// Computes PHP `abs()` for one runtime cell while preserving integer/float result typing. + fn abs(&mut self, value: RuntimeCellHandle) -> Result; + /// Adds two runtime cells using PHP addition semantics. fn add( &mut self, @@ -710,6 +713,7 @@ fn eval_call( values: &mut impl RuntimeValueOps, ) -> Result { match name { + "abs" => eval_builtin_abs(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "boolval" | "floatval" | "intval" | "strval" => { @@ -832,7 +836,8 @@ fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, - "call_user_func" + "abs" + | "call_user_func" | "call_user_func_array" | "boolval" | "count" @@ -955,6 +960,12 @@ fn eval_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { + "abs" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.abs(*value)? + } "call_user_func" => { return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) .map(Some); @@ -1015,6 +1026,20 @@ fn eval_builtin_with_values( Ok(Some(result)) } +/// Evaluates PHP's `abs(...)` over one eval expression. +fn eval_builtin_abs( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.abs(value) +} + /// Evaluates PHP scalar cast builtins over one eval expression. fn eval_builtin_cast( name: &str, @@ -1680,6 +1705,14 @@ mod tests { self.bool_value(value) } + /// Computes fake PHP absolute value while preserving float payloads. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + match self.get(value) { + FakeValue::Float(value) => self.float(value.abs()), + value => self.int(self.fake_int(&value).wrapping_abs()), + } + } + /// Adds fake numeric cells for interpreter tests. fn add( &mut self, @@ -2922,6 +2955,27 @@ return function_exists("gettype");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. + #[test] + fn execute_program_dispatches_abs_builtin() { + let program = parse_fragment( + br#"echo abs(-5); echo ":"; +echo abs(-2.5); echo ":"; +echo gettype(abs(-2.5)); echo ":"; +echo call_user_func("abs", -7); echo ":"; +echo call_user_func_array("abs", [-9]); +return function_exists("abs");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:2.5:double:7:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 44db3a145c..ab8f9032f4 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -67,6 +67,7 @@ unsafe extern "C" { fn __elephc_eval_value_cast_float(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_cast_string(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_cast_bool(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_abs(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_add(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_sub(left: *mut RuntimeCell, right: *mut RuntimeCell) @@ -311,6 +312,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_cast_bool(value.as_ptr()) }) } + /// Computes PHP `abs()` for a boxed Mixed cell through the generated runtime wrapper. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_abs(value.as_ptr()) }) + } + /// Adds two boxed Mixed cells using elephc runtime numeric semantics. fn add( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 34d15df570..af7aad5321 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index bbf0cea563..798032ddf1 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 8ffb031014..aec0143fce 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -81,6 +81,7 @@ public function echoLabelThroughEval(): void { $type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?");'); $casts = eval('return strval(intval("42")) . ":" . strval(floatval("3.5")) . ":" . (boolval("0") ? "true" : "false");'); $type_name = eval('return gettype(["ok"]);'); +$absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -115,6 +116,7 @@ public function echoLabelThroughEval(): void { echo "type-checks=" . $type_checks . "\n"; echo "casts=" . $casts . "\n"; echo "type-name=" . $type_name . "\n"; +echo "absolute=" . $absolute . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 193bb61dbd..efcc1525d3 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -367,6 +367,9 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov x0, #1"); // runtime tag 1 = string emitter.instruction("b __rt_mixed_from_value"); // persist and box the string payload for eval + label_c_global(emitter, "__elephc_eval_value_abs"); + emitter.instruction("b __rt_abs_mixed"); // compute PHP abs() for one boxed eval value + label_c_global(emitter, "__elephc_eval_value_add"); emitter.instruction("b __rt_mixed_numeric_add"); // add two boxed mixed values and return the boxed result @@ -1166,6 +1169,10 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov eax, 1"); // runtime tag 1 = string, with C ptr/len already in rdi/rsi emitter.instruction("jmp __rt_mixed_from_value"); // persist and box the string payload for eval + label_c_global(emitter, "__elephc_eval_value_abs"); + emitter.instruction("mov rax, rdi"); // move the boxed eval value into abs_mixed input + emitter.instruction("jmp __rt_abs_mixed"); // compute PHP abs() for one boxed eval value + label_c_global(emitter, "__elephc_eval_value_add"); emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d9984a8482..844d8e263d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -687,6 +687,22 @@ echo ":"; echo function_exists("gettype");'); ); } +/// Verifies eval `abs()` preserves integer/float result typing through direct and callable calls. +#[test] +fn test_eval_dispatches_abs_builtin_call() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 22:28:42 +0200 Subject: [PATCH 0054/1208] Support eval sqrt builtin --- crates/elephc-eval/src/interpreter.rs | 51 ++++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 6 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 27 ++++++++++++ tests/codegen/eval.rs | 15 +++++++ 7 files changed, 103 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index af17123d84..06915417ed 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -127,6 +127,9 @@ pub trait RuntimeValueOps { /// Computes PHP `abs()` for one runtime cell while preserving integer/float result typing. fn abs(&mut self, value: RuntimeCellHandle) -> Result; + /// Computes PHP `sqrt()` for one runtime cell after PHP numeric conversion. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result; + /// Adds two runtime cells using PHP addition semantics. fn add( &mut self, @@ -731,6 +734,7 @@ fn eval_call( eval_builtin_type_predicate(name, args, context, scope, values) } "isset" => eval_builtin_isset(args, context, scope, values), + "sqrt" => eval_builtin_sqrt(args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), _ => { if let Some(function) = context.function(name).cloned() { @@ -856,6 +860,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_null" | "is_real" | "is_string" + | "sqrt" | "strlen" | "strval" ) @@ -966,6 +971,12 @@ fn eval_builtin_with_values( }; values.abs(*value)? } + "sqrt" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.sqrt(*value)? + } "call_user_func" => { return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) .map(Some); @@ -1040,6 +1051,20 @@ fn eval_builtin_abs( values.abs(value) } +/// Evaluates PHP's `sqrt(...)` over one eval expression. +fn eval_builtin_sqrt( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.sqrt(value) +} + /// Evaluates PHP scalar cast builtins over one eval expression. fn eval_builtin_cast( name: &str, @@ -1713,6 +1738,12 @@ mod tests { } } + /// Computes fake PHP square root through numeric conversion as a float result. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).sqrt()) + } + /// Adds fake numeric cells for interpreter tests. fn add( &mut self, @@ -2976,6 +3007,26 @@ return function_exists("abs");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. + #[test] + fn execute_program_dispatches_sqrt_builtin() { + let program = parse_fragment( + br#"echo sqrt(16); echo ":"; +echo gettype(sqrt(9)); echo ":"; +echo call_user_func("sqrt", 25); echo ":"; +echo call_user_func_array("sqrt", [36]); +return function_exists("sqrt");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:double:5:6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index ab8f9032f4..8db1ea6d52 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -68,6 +68,7 @@ unsafe extern "C" { fn __elephc_eval_value_cast_string(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_cast_bool(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_abs(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_sqrt(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_add(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_sub(left: *mut RuntimeCell, right: *mut RuntimeCell) @@ -317,6 +318,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_abs(value.as_ptr()) }) } + /// Computes PHP `sqrt()` for a boxed Mixed cell through the generated runtime wrapper. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_sqrt(value.as_ptr()) }) + } + /// Adds two boxed Mixed cells using elephc runtime numeric semantics. fn add( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index af7aad5321..b2a0a61eb3 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, `sqrt()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 798032ddf1..d703c0ea20 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `sqrt()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index aec0143fce..d16f82136e 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -82,6 +82,7 @@ public function echoLabelThroughEval(): void { $casts = eval('return strval(intval("42")) . ":" . strval(floatval("3.5")) . ":" . (boolval("0") ? "true" : "false");'); $type_name = eval('return gettype(["ok"]);'); $absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); +$root = eval('return sqrt(81) . ":" . gettype(sqrt(16));'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -117,6 +118,7 @@ public function echoLabelThroughEval(): void { echo "casts=" . $casts . "\n"; echo "type-name=" . $type_name . "\n"; echo "absolute=" . $absolute . "\n"; +echo "root=" . $root . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index efcc1525d3..5e915e0719 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -370,6 +370,20 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_abs"); emitter.instruction("b __rt_abs_mixed"); // compute PHP abs() for one boxed eval value + label_c_global(emitter, "__elephc_eval_value_sqrt"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing sqrt + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for sqrt + emitter.bl_c("sqrt"); + emitter.instruction("fmov x1, d0"); // move the sqrt result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the sqrt result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the sqrt wrapper frame + emitter.instruction("ret"); // return the boxed sqrt result to Rust + label_c_global(emitter, "__elephc_eval_value_add"); emitter.instruction("b __rt_mixed_numeric_add"); // add two boxed mixed values and return the boxed result @@ -1173,6 +1187,19 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rax, rdi"); // move the boxed eval value into abs_mixed input emitter.instruction("jmp __rt_abs_mixed"); // compute PHP abs() for one boxed eval value + label_c_global(emitter, "__elephc_eval_value_sqrt"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for sqrt + emitter.bl_c("sqrt"); + emitter.instruction("movq rdi, xmm0"); // move the sqrt result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the sqrt result into a Mixed cell + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed sqrt result to Rust + label_c_global(emitter, "__elephc_eval_value_add"); emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 844d8e263d..e6ac3d0d05 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -703,6 +703,21 @@ echo ":"; echo function_exists("abs");'); assert_eq!(out, "5:2.5:double:7:9:1"); } +/// Verifies eval `sqrt()` returns boxed double cells through direct and callable calls. +#[test] +fn test_eval_dispatches_sqrt_builtin_call() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 22:34:14 +0200 Subject: [PATCH 0055/1208] Support eval floor and ceil builtins --- crates/elephc-eval/src/interpreter.rs | 85 ++++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 12 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 54 ++++++++++++++ tests/codegen/eval.rs | 17 +++++ 7 files changed, 172 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 06915417ed..40179e33d1 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -127,6 +127,12 @@ pub trait RuntimeValueOps { /// Computes PHP `abs()` for one runtime cell while preserving integer/float result typing. fn abs(&mut self, value: RuntimeCellHandle) -> Result; + /// Computes PHP `ceil()` for one runtime cell after PHP numeric conversion. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `floor()` for one runtime cell after PHP numeric conversion. + fn floor(&mut self, value: RuntimeCellHandle) -> Result; + /// Computes PHP `sqrt()` for one runtime cell after PHP numeric conversion. fn sqrt(&mut self, value: RuntimeCellHandle) -> Result; @@ -717,6 +723,7 @@ fn eval_call( ) -> Result { match name { "abs" => eval_builtin_abs(args, context, scope, values), + "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "boolval" | "floatval" | "intval" | "strval" => { @@ -725,6 +732,7 @@ fn eval_call( "count" => eval_builtin_count(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), + "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) } @@ -841,10 +849,12 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, "abs" + | "ceil" | "call_user_func" | "call_user_func_array" | "boolval" | "count" + | "floor" | "floatval" | "function_exists" | "gettype" @@ -971,6 +981,18 @@ fn eval_builtin_with_values( }; values.abs(*value)? } + "ceil" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.ceil(*value)? + } + "floor" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.floor(*value)? + } "sqrt" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1051,6 +1073,34 @@ fn eval_builtin_abs( values.abs(value) } +/// Evaluates PHP's `ceil(...)` over one eval expression. +fn eval_builtin_ceil( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.ceil(value) +} + +/// Evaluates PHP's `floor(...)` over one eval expression. +fn eval_builtin_floor( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.floor(value) +} + /// Evaluates PHP's `sqrt(...)` over one eval expression. fn eval_builtin_sqrt( args: &[EvalExpr], @@ -1738,6 +1788,18 @@ mod tests { } } + /// Computes fake PHP ceiling through numeric conversion as a float result. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).ceil()) + } + + /// Computes fake PHP floor through numeric conversion as a float result. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).floor()) + } + /// Computes fake PHP square root through numeric conversion as a float result. fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { let value = self.get(value); @@ -3007,6 +3069,29 @@ return function_exists("abs");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `floor()` and `ceil()` dispatch as double-returning math builtins. + #[test] + fn execute_program_dispatches_floor_and_ceil_builtins() { + let program = parse_fragment( + br#"echo floor(3.7); echo ":"; +echo gettype(floor(3)); echo ":"; +echo ceil(3.2); echo ":"; +echo gettype(ceil(3)); echo ":"; +echo call_user_func("floor", 4.9); echo ":"; +echo call_user_func_array("ceil", [4.1]); +echo ":"; echo function_exists("floor"); +return function_exists("ceil");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:double:4:double:4:5:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. #[test] fn execute_program_dispatches_sqrt_builtin() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 8db1ea6d52..6f13afd8d6 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -68,6 +68,8 @@ unsafe extern "C" { fn __elephc_eval_value_cast_string(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_cast_bool(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_abs(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_ceil(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_floor(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_sqrt(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_add(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; @@ -318,6 +320,16 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_abs(value.as_ptr()) }) } + /// Computes PHP `ceil()` for a boxed Mixed cell through the generated runtime wrapper. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_ceil(value.as_ptr()) }) + } + + /// Computes PHP `floor()` for a boxed Mixed cell through the generated runtime wrapper. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_floor(value.as_ptr()) }) + } + /// Computes PHP `sqrt()` for a boxed Mixed cell through the generated runtime wrapper. fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { Self::handle(unsafe { __elephc_eval_value_sqrt(value.as_ptr()) }) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index b2a0a61eb3..e839f53ec5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, `sqrt()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index d703c0ea20..649567c45d 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `sqrt()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index d16f82136e..d83d5382ed 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -83,6 +83,7 @@ public function echoLabelThroughEval(): void { $type_name = eval('return gettype(["ok"]);'); $absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); $root = eval('return sqrt(81) . ":" . gettype(sqrt(16));'); +$rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -119,6 +120,7 @@ public function echoLabelThroughEval(): void { echo "type-name=" . $type_name . "\n"; echo "absolute=" . $absolute . "\n"; echo "root=" . $root . "\n"; +echo "rounding=" . $rounding . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 5e915e0719..15f89027de 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -370,6 +370,34 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_abs"); emitter.instruction("b __rt_abs_mixed"); // compute PHP abs() for one boxed eval value + label_c_global(emitter, "__elephc_eval_value_ceil"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing ceil + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for ceil + emitter.bl_c("ceil"); + emitter.instruction("fmov x1, d0"); // move the ceil result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the ceil result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the ceil wrapper frame + emitter.instruction("ret"); // return the boxed ceil result to Rust + + label_c_global(emitter, "__elephc_eval_value_floor"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing floor + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for floor + emitter.bl_c("floor"); + emitter.instruction("fmov x1, d0"); // move the floor result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the floor result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the floor wrapper frame + emitter.instruction("ret"); // return the boxed floor result to Rust + label_c_global(emitter, "__elephc_eval_value_sqrt"); emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing sqrt emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls @@ -1187,6 +1215,32 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rax, rdi"); // move the boxed eval value into abs_mixed input emitter.instruction("jmp __rt_abs_mixed"); // compute PHP abs() for one boxed eval value + label_c_global(emitter, "__elephc_eval_value_ceil"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for ceil + emitter.bl_c("ceil"); + emitter.instruction("movq rdi, xmm0"); // move the ceil result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the ceil result into a Mixed cell + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed ceil result to Rust + + label_c_global(emitter, "__elephc_eval_value_floor"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval argument to a PHP double for floor + emitter.bl_c("floor"); + emitter.instruction("movq rdi, xmm0"); // move the floor result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the floor result into a Mixed cell + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed floor result to Rust + label_c_global(emitter, "__elephc_eval_value_sqrt"); emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e6ac3d0d05..883f1bd0e7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -703,6 +703,23 @@ echo ":"; echo function_exists("abs");'); assert_eq!(out, "5:2.5:double:7:9:1"); } +/// Verifies eval `floor()` and `ceil()` return boxed double cells through direct and callable calls. +#[test] +fn test_eval_dispatches_floor_and_ceil_builtin_calls() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 22:38:17 +0200 Subject: [PATCH 0056/1208] Support eval pow builtin --- crates/elephc-eval/src/interpreter.rs | 43 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 15 ++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 40179e33d1..125e810c55 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -741,6 +741,7 @@ fn eval_call( | "is_null" | "is_real" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } + "pow" => eval_builtin_pow(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), @@ -870,6 +871,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_null" | "is_real" | "is_string" + | "pow" | "sqrt" | "strlen" | "strval" @@ -993,6 +995,12 @@ fn eval_builtin_with_values( }; values.floor(*value)? } + "pow" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.pow(*left, *right)? + } "sqrt" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1101,6 +1109,21 @@ fn eval_builtin_floor( values.floor(value) } +/// Evaluates PHP's `pow(...)` over two eval expressions. +fn eval_builtin_pow( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + values.pow(left, right) +} + /// Evaluates PHP's `sqrt(...)` over one eval expression. fn eval_builtin_sqrt( args: &[EvalExpr], @@ -3092,6 +3115,26 @@ return function_exists("ceil");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. + #[test] + fn execute_program_dispatches_pow_builtin() { + let program = parse_fragment( + br#"echo pow(2, 3); echo ":"; +echo gettype(pow(2, 3)); echo ":"; +echo call_user_func("pow", 2, 5); echo ":"; +echo call_user_func_array("pow", [3, 3]); +return function_exists("pow");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:double:32:27"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. #[test] fn execute_program_dispatches_sqrt_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e839f53ec5..83fd7c92dc 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 649567c45d..103711f390 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index d83d5382ed..b414e03786 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -84,6 +84,7 @@ public function echoLabelThroughEval(): void { $absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); $root = eval('return sqrt(81) . ":" . gettype(sqrt(16));'); $rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); +$builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -121,6 +122,7 @@ public function echoLabelThroughEval(): void { echo "absolute=" . $absolute . "\n"; echo "root=" . $root . "\n"; echo "rounding=" . $rounding . "\n"; +echo "builtin-power=" . $builtin_power . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 883f1bd0e7..45979d1d0f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -720,6 +720,21 @@ echo ":"; echo function_exists("floor"); echo function_exists("ceil");'); assert_eq!(out, "3:double:4:double:4:5:11"); } +/// Verifies eval `pow()` reuses exponentiation runtime hooks through direct and callable calls. +#[test] +fn test_eval_dispatches_pow_builtin_call() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 22:44:21 +0200 Subject: [PATCH 0057/1208] Support eval round builtin --- crates/elephc-eval/src/interpreter.rs | 70 ++++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 19 ++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 70 ++++++++++++++++++++++ tests/codegen/eval.rs | 16 +++++ 7 files changed, 179 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 125e810c55..f53adcdb84 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -178,6 +178,13 @@ pub trait RuntimeValueOps { right: RuntimeCellHandle, ) -> Result; + /// Rounds one runtime cell using PHP `round()` semantics and optional precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result; + /// Applies an integer bitwise or shift operation to two runtime cells. fn bitwise( &mut self, @@ -742,6 +749,7 @@ fn eval_call( eval_builtin_type_predicate(name, args, context, scope, values) } "pow" => eval_builtin_pow(args, context, scope, values), + "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), @@ -872,6 +880,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_real" | "is_string" | "pow" + | "round" | "sqrt" | "strlen" | "strval" @@ -1001,6 +1010,11 @@ fn eval_builtin_with_values( }; values.pow(*left, *right)? } + "round" => match evaluated_args { + [value] => values.round(*value, None)?, + [value, precision] => values.round(*value, Some(*precision))?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "sqrt" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1124,6 +1138,27 @@ fn eval_builtin_pow( values.pow(left, right) } +/// Evaluates PHP's `round(...)` over one value and an optional precision expression. +fn eval_builtin_round( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + values.round(value, None) + } + [value, precision] => { + let value = eval_expr(value, context, scope, values)?; + let precision = eval_expr(precision, context, scope, values)?; + values.round(value, Some(precision)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Evaluates PHP's `sqrt(...)` over one eval expression. fn eval_builtin_sqrt( args: &[EvalExpr], @@ -1904,6 +1939,20 @@ mod tests { self.float(left.powf(right)) } + /// Rounds fake numeric cells with PHP's optional decimal precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + let value = self.fake_numeric(&self.get(value)); + let precision = precision + .map(|precision| self.fake_int(&self.get(precision))) + .unwrap_or(0); + let multiplier = 10_f64.powf(precision as f64); + self.float((value * multiplier).round() / multiplier) + } + /// Applies fake integer bitwise and shift operations for interpreter tests. fn bitwise( &mut self, @@ -3135,6 +3184,27 @@ return function_exists("pow");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `round()` supports default and explicit precision through callable paths. + #[test] + fn execute_program_dispatches_round_builtin() { + let program = parse_fragment( + br#"echo round(3.5); echo ":"; +echo round(3.14159, 2); echo ":"; +echo gettype(round(3)); echo ":"; +echo call_user_func("round", 2.5); echo ":"; +echo call_user_func_array("round", [1.55, 1]); +return function_exists("round");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:3.14:double:3:1.6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. #[test] fn execute_program_dispatches_sqrt_builtin() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 6f13afd8d6..7b36cf38b6 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -83,6 +83,11 @@ unsafe extern "C" { -> *mut RuntimeCell; fn __elephc_eval_value_pow(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_round( + value: *mut RuntimeCell, + precision: *mut RuntimeCell, + has_precision: u64, + ) -> *mut RuntimeCell; fn __elephc_eval_value_bitwise( left: *mut RuntimeCell, right: *mut RuntimeCell, @@ -389,6 +394,20 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_pow(left.as_ptr(), right.as_ptr()) }) } + /// Rounds a boxed Mixed cell through the generated runtime wrapper. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + let (precision, has_precision) = if let Some(precision) = precision { + (precision.as_ptr(), 1) + } else { + (core::ptr::null_mut(), 0) + }; + Self::handle(unsafe { __elephc_eval_value_round(value.as_ptr(), precision, has_precision) }) + } + /// Applies an integer bitwise or shift operation through the generated runtime wrapper. fn bitwise( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 83fd7c92dc..10dd6cdf3d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 103711f390..4b36716bff 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b414e03786..cf604b6392 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -85,6 +85,7 @@ public function echoLabelThroughEval(): void { $root = eval('return sqrt(81) . ":" . gettype(sqrt(16));'); $rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); +$rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -123,6 +124,7 @@ public function echoLabelThroughEval(): void { echo "root=" . $root . "\n"; echo "rounding=" . $rounding . "\n"; echo "builtin-power=" . $builtin_power . "\n"; +echo "rounded=" . $rounded . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 15f89027de..0dca31cda7 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -498,6 +498,40 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #32"); // release the exponentiation wrapper frame emitter.instruction("ret"); // return the boxed exponentiation result to Rust + label_c_global(emitter, "__elephc_eval_value_round"); + emitter.instruction("sub sp, sp, #48"); // allocate wrapper slots for precision state and saved doubles + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the optional precision cell while casting the value + emitter.instruction("str x2, [sp, #8]"); // save whether the caller supplied a precision argument + emitter.instruction("bl __rt_mixed_cast_float"); // cast the boxed eval value to a PHP numeric double + emitter.instruction("ldr x2, [sp, #8]"); // reload the precision-presence flag after the value cast + emitter.instruction("cbnz x2, __elephc_eval_value_round_precision"); // use the precision path when a second argument is present + emitter.bl_c("round"); + emitter.instruction("b __elephc_eval_value_round_box"); // box the default-precision round result + emitter.label("__elephc_eval_value_round_precision"); + emitter.instruction("str d0, [sp, #16]"); // save the original value while casting the precision + emitter.instruction("ldr x0, [sp, #0]"); // reload the precision cell for integer casting + emitter.instruction("bl __rt_mixed_cast_int"); // cast the optional precision to a PHP integer + emitter.instruction("scvtf d1, x0"); // convert the precision to a floating exponent for pow + emitter.instruction("fmov d0, #10.0"); // materialize 10.0 as the precision multiplier base + emitter.bl_c("pow"); + emitter.instruction("ldr d1, [sp, #16]"); // reload the original value after pow returns the multiplier + emitter.instruction("fmul d1, d1, d0"); // scale the value by the precision multiplier + emitter.instruction("str d0, [sp, #24]"); // save the multiplier for rescaling after round + emitter.instruction("fmov d0, d1"); // move the scaled value into the round argument + emitter.bl_c("round"); + emitter.instruction("ldr d1, [sp, #24]"); // reload the precision multiplier for rescaling + emitter.instruction("fdiv d0, d0, d1"); // scale the rounded value back to requested precision + emitter.label("__elephc_eval_value_round_box"); + emitter.instruction("fmov x1, d0"); // move the round result bits into mixed value_lo + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the round result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the round wrapper frame + emitter.instruction("ret"); // return the boxed round result to Rust + label_c_global(emitter, "__elephc_eval_value_bit_not"); emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame for the cast helper call emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across the cast @@ -1352,6 +1386,42 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed exponentiation result to Rust + label_c_global(emitter, "__elephc_eval_value_round"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for precision state and saved doubles + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the optional precision cell while casting the value + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save whether the caller supplied a precision argument + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the boxed eval value to a PHP numeric double + emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // check whether a precision argument was supplied + emitter.instruction("jne __elephc_eval_value_round_precision_x86"); // use the precision path when a second argument is present + emitter.bl_c("round"); + emitter.instruction("jmp __elephc_eval_value_round_box_x86"); // box the default-precision round result + emitter.label("__elephc_eval_value_round_precision_x86"); + emitter.instruction("movsd QWORD PTR [rbp - 24], xmm0"); // save the original value while casting the precision + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the precision cell for integer casting + emitter.instruction("call __rt_mixed_cast_int"); // cast the optional precision to a PHP integer + emitter.instruction("cvtsi2sd xmm1, rax"); // convert the precision to a floating exponent for pow + emitter.instruction("mov rax, 0x4024000000000000"); // materialize the IEEE-754 payload for 10.0 + emitter.instruction("movq xmm0, rax"); // move 10.0 into the pow base argument + emitter.bl_c("pow"); + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 24]"); // reload the original value after pow returns the multiplier + emitter.instruction("mulsd xmm1, xmm0"); // scale the value by the precision multiplier + emitter.instruction("movsd QWORD PTR [rbp - 32], xmm0"); // save the multiplier for rescaling after round + emitter.instruction("movsd xmm0, xmm1"); // move the scaled value into the round argument + emitter.bl_c("round"); + emitter.instruction("movsd xmm1, QWORD PTR [rbp - 32]"); // reload the precision multiplier for rescaling + emitter.instruction("divsd xmm0, xmm1"); // scale the rounded value back to requested precision + emitter.label("__elephc_eval_value_round_box_x86"); + emitter.instruction("movq rdi, xmm0"); // move the round result bits into mixed value_lo + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the round result into a Mixed cell + emitter.instruction("add rsp, 48"); // release the round wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed round result to Rust + label_c_global(emitter, "__elephc_eval_value_bit_not"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 45979d1d0f..8dcf398b0c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -735,6 +735,22 @@ echo ":"; echo function_exists("pow");'); assert_eq!(out, "8:double:32:27:1"); } +/// Verifies eval `round()` supports default and explicit precision through callable paths. +#[test] +fn test_eval_dispatches_round_builtin_call() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 22:48:37 +0200 Subject: [PATCH 0058/1208] Support eval string case builtins --- crates/elephc-eval/src/interpreter.rs | 73 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 ++++++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f53adcdb84..92ce8f8d73 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -753,6 +753,7 @@ fn eval_call( "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), + "strtolower" | "strtoupper" => eval_builtin_string_case(name, args, context, scope, values), _ => { if let Some(function) = context.function(name).cloned() { return eval_dynamic_function(&function, args, context, scope, values); @@ -883,6 +884,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "round" | "sqrt" | "strlen" + | "strtolower" + | "strtoupper" | "strval" ) } @@ -1076,6 +1079,12 @@ fn eval_builtin_with_values( let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } + "strtolower" | "strtoupper" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_case_result(name, *value, values)? + } _ => return Ok(None), }; Ok(Some(result)) @@ -1275,6 +1284,49 @@ fn eval_type_predicate_result( values.bool_value(result) } +/// Evaluates PHP ASCII case-conversion string builtins over one eval expression. +fn eval_builtin_string_case( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_string_case_result(name, value, values) +} + +/// Converts one eval value through PHP string conversion and ASCII case mapping. +fn eval_string_case_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + match name { + "strtolower" => { + for byte in &mut bytes { + if byte.is_ascii_uppercase() { + *byte += b'a' - b'A'; + } + } + } + "strtoupper" => { + for byte in &mut bytes { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} + /// Evaluates nested `eval(...)` calls against the current materialized scope. fn eval_nested_eval( args: &[EvalExpr], @@ -3048,6 +3100,27 @@ return call_user_func_array("dyn", [4, 5]);"#, assert_eq!(values.get(result), FakeValue::Int(6)); } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. + #[test] + fn execute_program_dispatches_string_case_builtins() { + let program = parse_fragment( + br#"echo strtoupper("Hello World"); echo ":"; +echo strtolower("LOUD"); echo ":"; +echo call_user_func("strtoupper", "xy"); echo ":"; +echo call_user_func_array("strtolower", ["ZZ"]); +echo ":"; echo function_exists("strtoupper"); +return function_exists("strtolower");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "HELLO WORLD:loud:XY:zz:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. #[test] fn execute_program_dispatches_type_predicate_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 10dd6cdf3d..f58b257a0a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4b36716bff..7eca5d91de 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index cf604b6392..33ba5bd048 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -86,6 +86,7 @@ public function echoLabelThroughEval(): void { $rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); +$case = eval('return strtoupper("eval") . ":" . strtolower("LOUD");'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -125,6 +126,7 @@ public function echoLabelThroughEval(): void { echo "rounding=" . $rounding . "\n"; echo "builtin-power=" . $builtin_power . "\n"; echo "rounded=" . $rounded . "\n"; +echo "case=" . $case . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8dcf398b0c..0a8c208ddf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -628,6 +628,21 @@ eval('echo STRLEN("abcd") . ":" . count([1, 2, 3]);'); assert_eq!(out, "4:3"); } +/// Verifies eval ASCII case-conversion builtins work directly and by callable dispatch. +#[test] +fn test_eval_dispatches_string_case_builtin_calls() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 22:54:28 +0200 Subject: [PATCH 0059/1208] Support eval str_contains builtin --- crates/elephc-eval/src/interpreter.rs | 59 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 16 ++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 92ce8f8d73..ec1d8bf925 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -752,6 +752,7 @@ fn eval_call( "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), + "str_contains" => eval_builtin_str_contains(args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), "strtolower" | "strtoupper" => eval_builtin_string_case(name, args, context, scope, values), _ => { @@ -883,6 +884,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "pow" | "round" | "sqrt" + | "str_contains" | "strlen" | "strtolower" | "strtoupper" @@ -1079,6 +1081,12 @@ fn eval_builtin_with_values( let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } + "str_contains" => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_contains_result(*haystack, *needle, values)? + } "strtolower" | "strtoupper" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1284,6 +1292,36 @@ fn eval_type_predicate_result( values.bool_value(result) } +/// Evaluates PHP's `str_contains(...)` over two eval expressions. +fn eval_builtin_str_contains( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_str_contains_result(haystack, needle, values) +} + +/// Checks one converted haystack for one converted needle using PHP byte-string semantics. +fn eval_str_contains_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let contains = needle.is_empty() + || haystack + .windows(needle.len()) + .any(|window| window == needle); + values.bool_value(contains) +} + /// Evaluates PHP ASCII case-conversion string builtins over one eval expression. fn eval_builtin_string_case( name: &str, @@ -3121,6 +3159,27 @@ return function_exists("strtolower");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. + #[test] + fn execute_program_dispatches_str_contains_builtin() { + let program = parse_fragment( + br#"echo str_contains("Hello World", "World") ? "Y" : "N"; +echo str_contains("Hello", "z") ? "bad" : ":N"; +echo str_contains("Hello", "") ? ":E" : "bad"; +echo call_user_func("str_contains", "abc", "b") ? ":C" : "bad"; +echo call_user_func_array("str_contains", ["abc", "x"]) ? "bad" : ":A"; +return function_exists("str_contains");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:E:C:A"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. #[test] fn execute_program_dispatches_type_predicate_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index f58b257a0a..a8859092a7 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7eca5d91de..36dabc69ed 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 33ba5bd048..a7aeab64c1 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -87,6 +87,7 @@ public function echoLabelThroughEval(): void { $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD");'); +$contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -127,6 +128,7 @@ public function echoLabelThroughEval(): void { echo "builtin-power=" . $builtin_power . "\n"; echo "rounded=" . $rounded . "\n"; echo "case=" . $case . "\n"; +echo "contains=" . $contains . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0a8c208ddf..9690274ba1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -643,6 +643,22 @@ echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower") assert_eq!(out, "HELLO WORLD:loud:XY:zz:11"); } +/// Verifies eval `str_contains()` supports direct and callable byte-string search. +#[test] +fn test_eval_dispatches_str_contains_builtin_call() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 22:58:42 +0200 Subject: [PATCH 0060/1208] Support eval string boundary builtins --- crates/elephc-eval/src/interpreter.rs | 62 +++++++++++++++++++++------ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 19 ++++++++ 5 files changed, 73 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ec1d8bf925..473966e596 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -752,7 +752,9 @@ fn eval_call( "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), - "str_contains" => eval_builtin_str_contains(args, context, scope, values), + "str_contains" | "str_starts_with" | "str_ends_with" => { + eval_builtin_string_search(name, args, context, scope, values) + } "strlen" => eval_builtin_strlen(args, context, scope, values), "strtolower" | "strtoupper" => eval_builtin_string_case(name, args, context, scope, values), _ => { @@ -885,6 +887,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "round" | "sqrt" | "str_contains" + | "str_ends_with" + | "str_starts_with" | "strlen" | "strtolower" | "strtoupper" @@ -1081,11 +1085,11 @@ fn eval_builtin_with_values( let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } - "str_contains" => { + "str_contains" | "str_starts_with" | "str_ends_with" => { let [haystack, needle] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_str_contains_result(*haystack, *needle, values)? + eval_string_search_result(name, *haystack, *needle, values)? } "strtolower" | "strtoupper" => { let [value] = evaluated_args else { @@ -1292,8 +1296,9 @@ fn eval_type_predicate_result( values.bool_value(result) } -/// Evaluates PHP's `str_contains(...)` over two eval expressions. -fn eval_builtin_str_contains( +/// Evaluates PHP's byte-string search predicates over two eval expressions. +fn eval_builtin_string_search( + name: &str, args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, @@ -1304,22 +1309,30 @@ fn eval_builtin_str_contains( }; let haystack = eval_expr(haystack, context, scope, values)?; let needle = eval_expr(needle, context, scope, values)?; - eval_str_contains_result(haystack, needle, values) + eval_string_search_result(name, haystack, needle, values) } /// Checks one converted haystack for one converted needle using PHP byte-string semantics. -fn eval_str_contains_result( +fn eval_string_search_result( + name: &str, haystack: RuntimeCellHandle, needle: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { let haystack = values.string_bytes(haystack)?; let needle = values.string_bytes(needle)?; - let contains = needle.is_empty() - || haystack - .windows(needle.len()) - .any(|window| window == needle); - values.bool_value(contains) + let matched = match name { + "str_contains" => { + needle.is_empty() + || haystack + .windows(needle.len()) + .any(|window| window == needle) + } + "str_starts_with" => haystack.starts_with(&needle), + "str_ends_with" => haystack.ends_with(&needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(matched) } /// Evaluates PHP ASCII case-conversion string builtins over one eval expression. @@ -3180,6 +3193,31 @@ return function_exists("str_contains");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval prefix/suffix string search builtins use byte-string semantics. + #[test] + fn execute_program_dispatches_string_boundary_builtins() { + let program = parse_fragment( + br#"echo str_starts_with("Hello World", "Hello") ? "S" : "bad"; +echo str_starts_with("Hello", "World") ? "bad" : ":s"; +echo str_starts_with("Hello", "") ? ":se" : "bad"; +echo str_ends_with("Hello World", "World") ? ":E" : "bad"; +echo str_ends_with("Hello", "World") ? "bad" : ":e"; +echo str_ends_with("Hello", "") ? ":ee" : "bad"; +echo call_user_func("str_starts_with", "abc", "a") ? ":CS" : "bad"; +echo call_user_func_array("str_ends_with", ["abc", "c"]) ? ":CE" : "bad"; +echo ":"; echo function_exists("str_starts_with"); +return function_exists("str_ends_with");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "S:s:se:E:e:ee:CS:CE:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. #[test] fn execute_program_dispatches_type_predicate_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a8859092a7..67306dd166 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 36dabc69ed..cbf65fc7a4 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index a7aeab64c1..f228d74a35 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -88,6 +88,7 @@ public function echoLabelThroughEval(): void { $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); +$boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -129,6 +130,7 @@ public function echoLabelThroughEval(): void { echo "rounded=" . $rounded . "\n"; echo "case=" . $case . "\n"; echo "contains=" . $contains . "\n"; +echo "boundaries=" . $boundaries . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9690274ba1..274be6764f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -659,6 +659,25 @@ echo ":"; echo function_exists("str_contains");'); assert_eq!(out, "Y:N:E:C:A:1"); } +/// Verifies eval string boundary builtins support direct and callable byte-string checks. +#[test] +fn test_eval_dispatches_string_boundary_builtin_calls() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 23:03:18 +0200 Subject: [PATCH 0061/1208] Support eval trim-like builtins --- crates/elephc-eval/src/interpreter.rs | 100 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 18 +++++ 5 files changed, 122 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 473966e596..10cdb2024f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -733,6 +733,7 @@ fn eval_call( "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "chop" => eval_builtin_trim_like(name, args, context, scope, values), "boolval" | "floatval" | "intval" | "strval" => { eval_builtin_cast(name, args, context, scope, values) } @@ -748,6 +749,7 @@ fn eval_call( | "is_null" | "is_real" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } + "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "pow" => eval_builtin_pow(args, context, scope, values), "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), @@ -757,6 +759,7 @@ fn eval_call( } "strlen" => eval_builtin_strlen(args, context, scope, values), "strtolower" | "strtoupper" => eval_builtin_string_case(name, args, context, scope, values), + "trim" => eval_builtin_trim_like(name, args, context, scope, values), _ => { if let Some(function) = context.function(name).cloned() { return eval_dynamic_function(&function, args, context, scope, values); @@ -866,12 +869,14 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "call_user_func" | "call_user_func_array" | "boolval" + | "chop" | "count" | "floor" | "floatval" | "function_exists" | "gettype" | "intval" + | "ltrim" | "is_callable" | "is_array" | "is_bool" @@ -884,6 +889,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_real" | "is_string" | "pow" + | "rtrim" | "round" | "sqrt" | "str_contains" @@ -893,6 +899,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strtolower" | "strtoupper" | "strval" + | "trim" ) } @@ -1055,6 +1062,11 @@ fn eval_builtin_with_values( let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } + "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { + [value] => eval_trim_like_result(name, *value, None, values)?, + [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "function_exists" | "is_callable" => { let [name] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1335,6 +1347,67 @@ fn eval_string_search_result( values.bool_value(matched) } +const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; + +/// Evaluates PHP trim-like string builtins over one eval expression and optional mask. +fn eval_builtin_trim_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_trim_like_result(name, value, None, values) + } + [value, mask] => { + let value = eval_expr(value, context, scope, values)?; + let mask = eval_expr(mask, context, scope, values)?; + eval_trim_like_result(name, value, Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Trims one converted string using PHP's default mask or a caller-provided byte mask. +fn eval_trim_like_result( + name: &str, + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let explicit_mask; + let trim_mask = if let Some(mask) = mask { + explicit_mask = values.string_bytes(mask)?; + explicit_mask.as_slice() + } else { + PHP_DEFAULT_TRIM_MASK + }; + + let mut start = 0; + let mut end = bytes.len(); + if matches!(name, "trim" | "ltrim") { + while start < end && trim_mask.contains(&bytes[start]) { + start += 1; + } + } + if matches!(name, "trim" | "rtrim" | "chop") { + while end > start && trim_mask.contains(&bytes[end - 1]) { + end -= 1; + } + } + if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { + return Err(EvalStatus::UnsupportedConstruct); + } + + let value = + String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} + /// Evaluates PHP ASCII case-conversion string builtins over one eval expression. fn eval_builtin_string_case( name: &str, @@ -3218,6 +3291,33 @@ return function_exists("str_ends_with");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval trim-like builtins strip default and explicit byte masks. + #[test] + fn execute_program_dispatches_trim_like_builtins() { + let program = parse_fragment( + br#"echo "[" . trim(" hello ") . "]"; +echo ":[" . ltrim(" left") . "]"; +echo ":[" . rtrim("right ") . "]"; +echo ":[" . chop("tail... ", " .") . "]"; +echo ":[" . trim("**boxed**", "*") . "]"; +echo ":[" . call_user_func("trim", " cuf ") . "]"; +echo ":[" . call_user_func_array("ltrim", ["0007", "0"]) . "]"; +echo ":"; echo function_exists("trim"); echo function_exists("ltrim"); echo function_exists("rtrim"); +return function_exists("chop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "[hello]:[left]:[right]:[tail]:[boxed]:[cuf]:[7]:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. #[test] fn execute_program_dispatches_type_predicate_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 67306dd166..86fb9bb91c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index cbf65fc7a4..54f6cd663e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index f228d74a35..87a3ec9550 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -89,6 +89,7 @@ public function echoLabelThroughEval(): void { $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); +$trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -131,6 +132,7 @@ public function echoLabelThroughEval(): void { echo "case=" . $case . "\n"; echo "contains=" . $contains . "\n"; echo "boundaries=" . $boundaries . "\n"; +echo "trimmed=" . $trimmed . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 274be6764f..49725cc8dc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -678,6 +678,24 @@ echo ":"; echo function_exists("str_starts_with"); echo function_exists("str_end assert_eq!(out, "S:s:se:E:e:ee:CS:CE:11"); } +/// Verifies eval trim-like builtins strip default and explicit masks. +#[test] +fn test_eval_dispatches_trim_like_builtin_calls() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 23:07:37 +0200 Subject: [PATCH 0062/1208] Support eval array aggregate builtins --- crates/elephc-eval/src/interpreter.rs | 74 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 18 +++++++ 5 files changed, 96 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 10cdb2024f..581a80e965 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -730,6 +730,9 @@ fn eval_call( ) -> Result { match name { "abs" => eval_builtin_abs(args, context, scope, values), + "array_product" | "array_sum" => { + eval_builtin_array_aggregate(name, args, context, scope, values) + } "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), @@ -865,6 +868,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, "abs" + | "array_product" + | "array_sum" | "ceil" | "call_user_func" | "call_user_func_array" @@ -1008,6 +1013,12 @@ fn eval_builtin_with_values( }; values.abs(*value)? } + "array_product" | "array_sum" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_aggregate_result(name, *array, values)? + } "ceil" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1128,6 +1139,45 @@ fn eval_builtin_abs( values.abs(value) } +/// Evaluates PHP array aggregate builtins over one eval array expression. +fn eval_builtin_array_aggregate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_aggregate_result(name, array, values) +} + +/// Computes `array_sum()` or `array_product()` through eval's numeric value hooks. +fn eval_array_aggregate_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = match name { + "array_sum" => values.int(0)?, + "array_product" => values.int(1)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = match name { + "array_sum" => values.add(result, value)?, + "array_product" => values.mul(result, value)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + } + Ok(result) +} + /// Evaluates PHP's `ceil(...)` over one eval expression. fn eval_builtin_ceil( args: &[EvalExpr], @@ -3224,6 +3274,30 @@ return call_user_func_array("dyn", [4, 5]);"#, assert_eq!(values.get(result), FakeValue::Int(6)); } + /// Verifies eval array aggregate builtins iterate array values and support callable dispatch. + #[test] + fn execute_program_dispatches_array_aggregate_builtins() { + let program = parse_fragment( + br#"echo array_sum([1, 2, 3]); +echo ":" . array_product([2, 3, 4]); +echo ":" . array_sum([]); +echo ":" . array_product([]); +echo ":" . array_sum(["a" => 2, "b" => 5]); +echo ":" . call_user_func("array_sum", [3, 4]); +echo ":" . call_user_func_array("array_product", [[2, 5]]); +echo ":"; echo function_exists("array_sum"); +return function_exists("array_product");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "6:24:0:1:7:7:10:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 86fb9bb91c..433466ba33 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 54f6cd663e..4fbdb26b18 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 87a3ec9550..cd28055853 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -90,6 +90,7 @@ public function echoLabelThroughEval(): void { $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); +$aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -133,6 +134,7 @@ public function echoLabelThroughEval(): void { echo "contains=" . $contains . "\n"; echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; +echo "aggregates=" . $aggregates . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 49725cc8dc..7cfbb522c6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -628,6 +628,24 @@ eval('echo STRLEN("abcd") . ":" . count([1, 2, 3]);'); assert_eq!(out, "4:3"); } +/// Verifies eval array aggregate builtins iterate values and dispatch dynamically. +#[test] +fn test_eval_dispatches_array_aggregate_builtin_calls() { + let out = compile_and_run( + r#" 2, "b" => 5]); +echo ":" . call_user_func("array_sum", [3, 4]); +echo ":" . call_user_func_array("array_product", [[2, 5]]); +echo ":"; echo function_exists("array_sum"); echo function_exists("array_product");'); +"#, + ); + assert_eq!(out, "6:24:0:1:7:7:10:11"); +} + /// Verifies eval ASCII case-conversion builtins work directly and by callable dispatch. #[test] fn test_eval_dispatches_string_case_builtin_calls() { From 2cd7312e9da263938f6e3855218b1b0f8294127d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 23:12:44 +0200 Subject: [PATCH 0063/1208] Support eval string comparison builtins --- crates/elephc-eval/src/interpreter.rs | 116 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 19 +++++ 5 files changed, 139 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 581a80e965..3582fea8b3 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -748,6 +748,7 @@ fn eval_call( eval_builtin_function_probe(args, context, scope, values) } "gettype" => eval_builtin_gettype(args, context, scope, values), + "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_real" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) @@ -760,6 +761,7 @@ fn eval_call( "str_contains" | "str_starts_with" | "str_ends_with" => { eval_builtin_string_search(name, args, context, scope, values) } + "strcmp" | "strcasecmp" => eval_builtin_string_compare(name, args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), "strtolower" | "strtoupper" => eval_builtin_string_case(name, args, context, scope, values), "trim" => eval_builtin_trim_like(name, args, context, scope, values), @@ -880,6 +882,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "floatval" | "function_exists" | "gettype" + | "hash_equals" | "intval" | "ltrim" | "is_callable" @@ -897,9 +900,11 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "rtrim" | "round" | "sqrt" + | "strcasecmp" | "str_contains" | "str_ends_with" | "str_starts_with" + | "strcmp" | "strlen" | "strtolower" | "strtoupper" @@ -1093,6 +1098,12 @@ fn eval_builtin_with_values( }; eval_gettype_result(*value, values)? } + "hash_equals" => { + let [known, user] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_equals_result(*known, *user, values)? + } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_real" | "is_string" => { let [value] = evaluated_args else { @@ -1114,6 +1125,12 @@ fn eval_builtin_with_values( }; eval_string_search_result(name, *haystack, *needle, values)? } + "strcmp" | "strcasecmp" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_compare_result(name, *left, *right, values)? + } "strtolower" | "strtoupper" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1358,6 +1375,80 @@ fn eval_type_predicate_result( values.bool_value(result) } +/// Evaluates PHP's `hash_equals(...)` over two eval expressions. +fn eval_builtin_hash_equals( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [known, user] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let known = eval_expr(known, context, scope, values)?; + let user = eval_expr(user, context, scope, values)?; + eval_hash_equals_result(known, user, values) +} + +/// Compares two converted strings with PHP `hash_equals()` semantics. +fn eval_hash_equals_result( + known: RuntimeCellHandle, + user: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let known = values.string_bytes(known)?; + let user = values.string_bytes(user)?; + if known.len() != user.len() { + return values.bool_value(false); + } + let mut diff = 0u8; + for (known, user) in known.iter().zip(user.iter()) { + diff |= known ^ user; + } + values.bool_value(diff == 0) +} + +/// Evaluates PHP string comparison builtins over two eval expressions. +fn eval_builtin_string_compare( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_string_compare_result(name, left, right, values) +} + +/// Compares two converted strings and returns -1, 0, or 1. +fn eval_string_compare_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut left = values.string_bytes(left)?; + let mut right = values.string_bytes(right)?; + match name { + "strcmp" => {} + "strcasecmp" => { + left.make_ascii_lowercase(); + right.make_ascii_lowercase(); + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let result = match left.cmp(&right) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + }; + values.int(result) +} + /// Evaluates PHP's byte-string search predicates over two eval expressions. fn eval_builtin_string_search( name: &str, @@ -3365,6 +3456,31 @@ return function_exists("str_ends_with");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval string comparison builtins return PHP-compatible scalar results. + #[test] + fn execute_program_dispatches_string_compare_builtins() { + let program = parse_fragment( + br#"echo strcmp("abc", "abc"); +echo ":"; echo strcmp("abc", "abd") < 0 ? "lt" : "bad"; +echo ":"; echo strcasecmp("Hello", "hello"); +echo ":"; echo call_user_func("strcmp", "b", "a") > 0 ? "gt" : "bad"; +echo ":"; echo call_user_func_array("strcasecmp", ["A", "a"]) === 0 ? "ci" : "bad"; +echo ":"; echo hash_equals("abc", "abc") ? "heq" : "bad"; +echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; +echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; +echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); +return function_exists("hash_equals");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:lt:0:gt:ci:heq:hlen:hneq:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval trim-like builtins strip default and explicit byte masks. #[test] fn execute_program_dispatches_trim_like_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 433466ba33..d2039c3db5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4fbdb26b18..d0bc2de464 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index cd28055853..b64657e116 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -91,6 +91,7 @@ public function echoLabelThroughEval(): void { $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); +$string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -135,6 +136,7 @@ public function echoLabelThroughEval(): void { echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; +echo "string-compare=" . $string_compare . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7cfbb522c6..a6e2a550bc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -696,6 +696,25 @@ echo ":"; echo function_exists("str_starts_with"); echo function_exists("str_end assert_eq!(out, "S:s:se:E:e:ee:CS:CE:11"); } +/// Verifies eval string comparison builtins return compatible scalar results. +#[test] +fn test_eval_dispatches_string_compare_builtin_calls() { + let out = compile_and_run( + r#" 0 ? "gt" : "bad"; +echo ":"; echo call_user_func_array("strcasecmp", ["A", "a"]) === 0 ? "ci" : "bad"; +echo ":"; echo hash_equals("abc", "abc") ? "heq" : "bad"; +echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; +echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; +echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); echo function_exists("hash_equals");'); +"#, + ); + assert_eq!(out, "0:lt:0:gt:ci:heq:hlen:hneq:111"); +} + /// Verifies eval trim-like builtins strip default and explicit masks. #[test] fn test_eval_dispatches_trim_like_builtin_calls() { From 5c73067721eabe71eb4916fe6d4669613ceddb0b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 23:18:06 +0200 Subject: [PATCH 0064/1208] Support eval is_numeric predicate --- crates/elephc-eval/src/interpreter.rs | 56 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 9 ++++- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 3582fea8b3..fee3c39861 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -750,7 +750,7 @@ fn eval_call( "gettype" => eval_builtin_gettype(args, context, scope, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" - | "is_null" | "is_real" | "is_string" => { + | "is_null" | "is_numeric" | "is_real" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), @@ -894,6 +894,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_integer" | "is_long" | "is_null" + | "is_numeric" | "is_real" | "is_string" | "pow" @@ -1105,7 +1106,7 @@ fn eval_builtin_with_values( eval_hash_equals_result(*known, *user, values)? } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" - | "is_null" | "is_real" | "is_string" => { + | "is_null" | "is_numeric" | "is_real" | "is_string" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -1370,11 +1371,54 @@ fn eval_type_predicate_result( "is_bool" => tag == EVAL_TAG_BOOL, "is_null" => tag == EVAL_TAG_NULL, "is_array" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_numeric" => { + tag == EVAL_TAG_INT + || tag == EVAL_TAG_FLOAT + || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)) + } _ => return Err(EvalStatus::UnsupportedConstruct), }; values.bool_value(result) } +/// Matches the static backend's legacy ASCII numeric-string scan. +fn eval_is_numeric_string(bytes: &[u8]) -> bool { + if bytes.is_empty() { + return false; + } + + let mut index = 0; + let mut consumed_digits = 0; + if bytes[index] == b'-' { + index += 1; + if index >= bytes.len() { + return false; + } + } + + while index < bytes.len() { + if bytes[index] == b'.' { + index += 1; + break; + } + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + while index < bytes.len() { + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + consumed_digits > 0 +} + /// Evaluates PHP's `hash_equals(...)` over two eval expressions. fn eval_builtin_hash_equals( args: &[EvalExpr], @@ -3517,8 +3561,14 @@ echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); echo is_string("x"); echo is_bool(false); echo is_null(null); echo is_array([1]); echo is_array(["a" => 1]); echo is_array(1) ? "bad" : "ok"; +echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); +echo is_numeric("-5"); echo is_numeric("3.14"); +echo is_numeric("abc") ? "bad" : "N"; +echo is_numeric(true) ? "bad" : "B"; echo ":"; echo call_user_func("is_string", "x"); echo call_user_func_array("is_array", [[1]]); +echo call_user_func("is_numeric", "12"); +echo function_exists("is_numeric"); return function_exists("is_double");"#, ) .expect("parse eval fragment"); @@ -3527,7 +3577,7 @@ return function_exists("is_double");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "11111111111ok:11"); + assert_eq!(values.output, "11111111111ok11111NB:1111"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d2039c3db5..869e80c6da 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index d0bc2de464..28137ae34c 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b64657e116..5016dde7c4 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -78,7 +78,7 @@ public function echoLabelThroughEval(): void { $magic_file_has_path = eval('return strlen(__FILE__) > strlen(__DIR__);'); $magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); $magic_scope = eval('return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";'); -$type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?");'); +$type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?") . (is_numeric("42") ? "n" : "?");'); $casts = eval('return strval(intval("42")) . ":" . strval(floatval("3.5")) . ":" . (boolval("0") ? "true" : "false");'); $type_name = eval('return gettype(["ok"]);'); $absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a6e2a550bc..ba900d3442 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -743,13 +743,18 @@ echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); echo is_string("x"); echo is_bool(false); echo is_null(null); echo is_array([1]); echo is_array(["a" => 1]); echo is_array(1) ? "bad" : "ok"; +echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); +echo is_numeric("-5"); echo is_numeric("3.14"); +echo is_numeric("abc") ? "bad" : "N"; +echo is_numeric(true) ? "bad" : "B"; echo ":"; echo call_user_func("is_string", "x"); echo call_user_func_array("is_array", [[1]]); -echo function_exists("is_double");'); +echo call_user_func("is_numeric", "12"); +echo function_exists("is_double"); echo function_exists("is_numeric");'); "#, ); - assert_eq!(out, "11111111111ok:111"); + assert_eq!(out, "11111111111ok11111NB:11111"); } /// Verifies eval scalar cast builtins return boxed Mixed cells through direct and callable calls. From ad19e8a179c2ec27c8d7dc9f116f257cb8fc23f1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 23:22:44 +0200 Subject: [PATCH 0065/1208] Support eval array projection builtins --- crates/elephc-eval/src/interpreter.rs | 73 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 20 ++++++++ 5 files changed, 97 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index fee3c39861..c618942919 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -730,6 +730,9 @@ fn eval_call( ) -> Result { match name { "abs" => eval_builtin_abs(args, context, scope, values), + "array_keys" | "array_values" => { + eval_builtin_array_projection(name, args, context, scope, values) + } "array_product" | "array_sum" => { eval_builtin_array_aggregate(name, args, context, scope, values) } @@ -870,8 +873,10 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, "abs" + | "array_keys" | "array_product" | "array_sum" + | "array_values" | "ceil" | "call_user_func" | "call_user_func_array" @@ -1025,6 +1030,12 @@ fn eval_builtin_with_values( }; eval_array_aggregate_result(name, *array, values)? } + "array_keys" | "array_values" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_projection_result(name, *array, values)? + } "ceil" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1196,6 +1207,42 @@ fn eval_array_aggregate_result( Ok(result) } +/// Evaluates PHP array projection builtins over one eval array expression. +fn eval_builtin_array_projection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_projection_result(name, array, values) +} + +/// Builds the indexed result array for `array_keys()` or `array_values()`. +fn eval_array_projection_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = match name { + "array_keys" => key, + "array_values" => values.array_get(array, key)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let index = values.int(position as i64)?; + result = values.array_set(result, index, value)?; + } + Ok(result) +} + /// Evaluates PHP's `ceil(...)` over one eval expression. fn eval_builtin_ceil( args: &[EvalExpr], @@ -3433,6 +3480,32 @@ return function_exists("array_product");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval array projection builtins produce indexed key/value arrays. + #[test] + fn execute_program_dispatches_array_projection_builtins() { + let program = parse_fragment( + br#"$values = array_values(["a" => 10, "b" => 20]); +echo $values[0] . ":" . $values[1]; +$keys = array_keys(["a" => 10, "b" => 20]); +echo ":" . $keys[0] . ":" . $keys[1]; +echo ":" . count(array_values([])); +$call_keys = call_user_func("array_keys", ["z" => 7]); +echo ":" . $call_keys[0]; +$call_values = call_user_func_array("array_values", [["q" => 8]]); +echo ":" . $call_values[0]; +echo ":"; echo function_exists("array_keys"); +return function_exists("array_values");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:a:b:0:z:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 869e80c6da..15ce172f88 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 28137ae34c..2900580c82 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 5016dde7c4..c6134ac852 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -91,6 +91,7 @@ public function echoLabelThroughEval(): void { $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); +$array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -136,6 +137,7 @@ public function echoLabelThroughEval(): void { echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; +echo "array-projection=" . $array_projection . "\n"; echo "string-compare=" . $string_compare . "\n"; $counter = new EvalCounter(); $counter->bump(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ba900d3442..a9d3e91ce1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -646,6 +646,26 @@ echo ":"; echo function_exists("array_sum"); echo function_exists("array_product assert_eq!(out, "6:24:0:1:7:7:10:11"); } +/// Verifies eval array projection builtins return indexed key/value arrays. +#[test] +fn test_eval_dispatches_array_projection_builtin_calls() { + let out = compile_and_run( + r#" 10, "b" => 20]); +echo $values[0] . ":" . $values[1]; +$keys = array_keys(["a" => 10, "b" => 20]); +echo ":" . $keys[0] . ":" . $keys[1]; +echo ":" . count(array_values([])); +$call_keys = call_user_func("array_keys", ["z" => 7]); +echo ":" . $call_keys[0]; +$call_values = call_user_func_array("array_values", [["q" => 8]]); +echo ":" . $call_values[0]; +echo ":"; echo function_exists("array_keys"); echo function_exists("array_values");'); +"#, + ); + assert_eq!(out, "10:20:a:b:0:z:8:11"); +} + /// Verifies eval ASCII case-conversion builtins work directly and by callable dispatch. #[test] fn test_eval_dispatches_string_case_builtin_calls() { From 3c36401cf21f99fc393ad59d85b21781f20f603b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 23:27:34 +0200 Subject: [PATCH 0066/1208] Support eval array search builtins --- crates/elephc-eval/src/interpreter.rs | 79 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 19 +++++++ 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c618942919..1c6c8f1d66 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -736,6 +736,9 @@ fn eval_call( "array_product" | "array_sum" => { eval_builtin_array_aggregate(name, args, context, scope, values) } + "array_search" | "in_array" => { + eval_builtin_array_search(name, args, context, scope, values) + } "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), @@ -875,6 +878,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { "abs" | "array_keys" | "array_product" + | "array_search" | "array_sum" | "array_values" | "ceil" @@ -888,6 +892,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "function_exists" | "gettype" | "hash_equals" + | "in_array" | "intval" | "ltrim" | "is_callable" @@ -1036,6 +1041,12 @@ fn eval_builtin_with_values( }; eval_array_projection_result(name, *array, values)? } + "array_search" | "in_array" => { + let [needle, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_search_result(name, *needle, *array, values)? + } "ceil" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1243,6 +1254,49 @@ fn eval_array_projection_result( Ok(result) } +/// Evaluates PHP array search builtins over a needle and haystack expression. +fn eval_builtin_array_search( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let needle = eval_expr(needle, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_array_search_result(name, needle, array, values) +} + +/// Searches an eval array with PHP's default loose comparison semantics. +fn eval_array_search_result( + name: &str, + needle: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let equal = values.compare(EvalBinOp::LooseEq, needle, value)?; + if values.truthy(equal)? { + return match name { + "in_array" => values.bool_value(true), + "array_search" => Ok(key), + _ => Err(EvalStatus::UnsupportedConstruct), + }; + } + } + match name { + "in_array" => values.bool_value(false), + "array_search" => values.bool_value(false), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Evaluates PHP's `ceil(...)` over one eval expression. fn eval_builtin_ceil( args: &[EvalExpr], @@ -3506,6 +3560,31 @@ return function_exists("array_values");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval array search builtins use loose comparison and return keys or booleans. + #[test] + fn execute_program_dispatches_array_search_builtins() { + let program = parse_fragment( + br#"echo in_array(2, [1, 2, 3]) ? "Y" : "bad"; +echo ":"; echo in_array(4, [1, 2, 3]) ? "bad" : "N"; +echo ":" . array_search(20, [10, 20, 30]); +echo ":" . array_search("Grace", ["name" => "Grace"]); +echo ":"; echo array_search("x", ["name" => "Grace"]) === false ? "F" : "bad"; +echo ":"; echo call_user_func("in_array", "b", ["a", "b"]) ? "C" : "bad"; +$found = call_user_func_array("array_search", ["v", ["k" => "v"]]); +echo ":" . $found; +echo ":"; echo function_exists("in_array"); +return function_exists("array_search");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:1:name:F:C:k:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 15ce172f88..2ba87c15c0 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 2900580c82..caff546e64 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index c6134ac852..2b448f3090 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -92,6 +92,7 @@ public function echoLabelThroughEval(): void { $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); +$array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -138,6 +139,7 @@ public function echoLabelThroughEval(): void { echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; echo "array-projection=" . $array_projection . "\n"; +echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; $counter = new EvalCounter(); $counter->bump(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a9d3e91ce1..fcdd6064eb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -666,6 +666,25 @@ echo ":"; echo function_exists("array_keys"); echo function_exists("array_values assert_eq!(out, "10:20:a:b:0:z:8:11"); } +/// Verifies eval array search builtins return booleans or matching keys. +#[test] +fn test_eval_dispatches_array_search_builtin_calls() { + let out = compile_and_run( + r#" "Grace"]); +echo ":"; echo array_search("x", ["name" => "Grace"]) === false ? "F" : "bad"; +echo ":"; echo call_user_func("in_array", "b", ["a", "b"]) ? "C" : "bad"; +$found = call_user_func_array("array_search", ["v", ["k" => "v"]]); +echo ":" . $found; +echo ":"; echo function_exists("in_array"); echo function_exists("array_search");'); +"#, + ); + assert_eq!(out, "Y:N:1:name:F:C:k:11"); +} + /// Verifies eval ASCII case-conversion builtins work directly and by callable dispatch. #[test] fn test_eval_dispatches_string_case_builtin_calls() { From b3d87cb5af89f85ae95ad0595a2a59c74a282e15 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 15 Jun 2026 23:33:10 +0200 Subject: [PATCH 0067/1208] Support eval string position builtins --- crates/elephc-eval/src/interpreter.rs | 78 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 18 +++++++ 5 files changed, 100 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 1c6c8f1d66..509810462c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -769,6 +769,7 @@ fn eval_call( } "strcmp" | "strcasecmp" => eval_builtin_string_compare(name, args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), + "strpos" | "strrpos" => eval_builtin_string_position(name, args, context, scope, values), "strtolower" | "strtoupper" => eval_builtin_string_case(name, args, context, scope, values), "trim" => eval_builtin_trim_like(name, args, context, scope, values), _ => { @@ -917,6 +918,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "str_starts_with" | "strcmp" | "strlen" + | "strpos" + | "strrpos" | "strtolower" | "strtoupper" | "strval" @@ -1142,6 +1145,12 @@ fn eval_builtin_with_values( let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } + "strpos" | "strrpos" => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_position_result(name, *haystack, *needle, values)? + } "str_contains" | "str_starts_with" | "str_ends_with" => { let [haystack, needle] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1633,6 +1642,51 @@ fn eval_string_search_result( values.bool_value(matched) } +/// Evaluates PHP byte-string position builtins over two eval expressions. +fn eval_builtin_string_position( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_position_result(name, haystack, needle, values) +} + +/// Returns the first or last byte offset of a converted needle, or PHP `false`. +fn eval_string_position_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = match name { + "strpos" if needle.is_empty() => Some(0), + "strpos" => haystack + .windows(needle.len()) + .position(|window| window == needle), + "strrpos" if needle.is_empty() => Some(haystack.len()), + "strrpos" => haystack + .windows(needle.len()) + .rposition(|window| window == needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + match position { + Some(position) => { + let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(position) + } + None => values.bool_value(false), + } +} + const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; /// Evaluates PHP trim-like string builtins over one eval expression and optional mask. @@ -3627,6 +3681,30 @@ return function_exists("str_contains");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval string position builtins return byte offsets or PHP false. + #[test] + fn execute_program_dispatches_string_position_builtins() { + let program = parse_fragment( + br#"echo strpos("banana", "na"); +echo ":" . strrpos("banana", "na"); +echo ":"; echo strpos("abc", "z") === false ? "F" : "bad"; +echo ":" . strpos("abc", ""); +echo ":" . strrpos("abc", ""); +echo ":" . call_user_func("strpos", "abc", "b"); +echo ":" . call_user_func_array("strrpos", ["ababa", "ba"]); +echo ":"; echo function_exists("strpos"); +return function_exists("strrpos");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:4:F:0:3:1:3:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval prefix/suffix string search builtins use byte-string semantics. #[test] fn execute_program_dispatches_string_boundary_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2ba87c15c0..be87951494 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index caff546e64..b6972194c3 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 2b448f3090..7e35a40cd6 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -88,6 +88,7 @@ public function echoLabelThroughEval(): void { $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); +$positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); @@ -135,6 +136,7 @@ public function echoLabelThroughEval(): void { echo "rounded=" . $rounded . "\n"; echo "case=" . $case . "\n"; echo "contains=" . $contains . "\n"; +echo "positions=" . $positions . "\n"; echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fcdd6064eb..4877e4eaaa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -716,6 +716,24 @@ echo ":"; echo function_exists("str_contains");'); assert_eq!(out, "Y:N:E:C:A:1"); } +/// Verifies eval `strpos()` and `strrpos()` return byte offsets or false. +#[test] +fn test_eval_dispatches_string_position_builtin_calls() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 23:37:07 +0200 Subject: [PATCH 0068/1208] Support eval ord builtin --- crates/elephc-eval/src/interpreter.rs | 52 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 15 ++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 509810462c..463dc3c23c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -760,6 +760,7 @@ fn eval_call( eval_builtin_type_predicate(name, args, context, scope, values) } "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), + "ord" => eval_builtin_ord(args, context, scope, values), "pow" => eval_builtin_pow(args, context, scope, values), "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), @@ -908,6 +909,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_numeric" | "is_real" | "is_string" + | "ord" | "pow" | "rtrim" | "round" @@ -1104,6 +1106,12 @@ fn eval_builtin_with_values( let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } + "ord" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ord_result(*value, values)? + } "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values)?, [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, @@ -1823,6 +1831,29 @@ fn eval_builtin_strlen( values.int(len) } +/// Evaluates the builtin `ord(...)` for the first byte of one coerced string. +fn eval_builtin_ord( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ord_result(value, values) +} + +/// Returns the first byte of one converted string, or zero for an empty string. +fn eval_ord_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(bytes.first().copied().unwrap_or(0))) +} + /// Evaluates the builtin `count(...)` for one runtime array-like argument. fn eval_builtin_count( args: &[EvalExpr], @@ -3564,6 +3595,27 @@ return call_user_func_array("dyn", [4, 5]);"#, assert_eq!(values.get(result), FakeValue::Int(6)); } + /// Verifies eval `ord()` returns the first byte and supports callable dispatch. + #[test] + fn execute_program_dispatches_ord_builtin() { + let program = parse_fragment( + br#"echo ord("A"); +echo ":" . ord(""); +echo ":" . call_user_func("ord", "B"); +echo ":" . call_user_func_array("ord", ["C"]); +echo ":"; echo function_exists("ord"); +return ord("Z");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "65:0:66:67:1"); + assert_eq!(values.get(result), FakeValue::Int(90)); + } + /// Verifies eval array aggregate builtins iterate array values and support callable dispatch. #[test] fn execute_program_dispatches_array_aggregate_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index be87951494..3c150cad8c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b6972194c3..3ea2319070 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 7e35a40cd6..21b3c0553c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -89,6 +89,7 @@ public function echoLabelThroughEval(): void { $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); +$ordinal = eval('return ord("A") . ":" . ord("");'); $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); @@ -137,6 +138,7 @@ public function echoLabelThroughEval(): void { echo "case=" . $case . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; +echo "ordinal=" . $ordinal . "\n"; echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4877e4eaaa..be09c93ea3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -628,6 +628,21 @@ eval('echo STRLEN("abcd") . ":" . count([1, 2, 3]);'); assert_eq!(out, "4:3"); } +/// Verifies eval `ord()` returns the first byte and dispatches dynamically. +#[test] +fn test_eval_dispatches_ord_builtin_call() { + let out = compile_and_run( + r#" Date: Mon, 15 Jun 2026 23:43:15 +0200 Subject: [PATCH 0069/1208] Support eval is_resource predicate --- crates/elephc-eval/src/interpreter.rs | 38 ++++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 3 ++- tests/codegen/eval.rs | 9 +++++-- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 463dc3c23c..c63073b245 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -756,7 +756,7 @@ fn eval_call( "gettype" => eval_builtin_gettype(args, context, scope, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" - | "is_null" | "is_numeric" | "is_real" | "is_string" => { + | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), @@ -908,6 +908,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_null" | "is_numeric" | "is_real" + | "is_resource" | "is_string" | "ord" | "pow" @@ -1139,7 +1140,7 @@ fn eval_builtin_with_values( eval_hash_equals_result(*known, *user, values)? } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" - | "is_null" | "is_numeric" | "is_real" | "is_string" => { + | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -1489,6 +1490,7 @@ fn eval_type_predicate_result( "is_bool" => tag == EVAL_TAG_BOOL, "is_null" => tag == EVAL_TAG_NULL, "is_array" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_resource" => tag == EVAL_TAG_RESOURCE, "is_numeric" => { tag == EVAL_TAG_INT || tag == EVAL_TAG_FLOAT @@ -2060,6 +2062,7 @@ mod tests { Array(Vec), Assoc(Vec<(FakeKey, RuntimeCellHandle)>), Object(HashMap), + Resource(i64), } /// Test runtime hooks that allocate stable fake handles and record echo output. @@ -2317,6 +2320,7 @@ mod tests { FakeValue::Array(_) => EVAL_TAG_ARRAY, FakeValue::Assoc(_) => EVAL_TAG_ASSOC, FakeValue::Object(_) => EVAL_TAG_OBJECT, + FakeValue::Resource(_) => EVAL_TAG_RESOURCE, FakeValue::Null => EVAL_TAG_NULL, }) } @@ -2626,6 +2630,7 @@ mod tests { FakeValue::Array(value) => !value.is_empty(), FakeValue::Assoc(value) => !value.is_empty(), FakeValue::Object(_) => true, + FakeValue::Resource(_) => true, }) } } @@ -2663,6 +2668,7 @@ mod tests { (FakeValue::Int(left), FakeValue::Int(right)) => left == right, (FakeValue::Float(left), FakeValue::Float(right)) => left == right, (FakeValue::String(left), FakeValue::String(right)) => left == right, + (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, _ => false, } } @@ -2684,6 +2690,7 @@ mod tests { FakeValue::Array(value) => value.len() as f64, FakeValue::Assoc(value) => value.len() as f64, FakeValue::Object(_) => 1.0, + FakeValue::Resource(value) => (*value + 1) as f64, } } @@ -2703,6 +2710,7 @@ mod tests { FakeValue::Array(value) => !value.is_empty(), FakeValue::Assoc(value) => !value.is_empty(), FakeValue::Object(_) => true, + FakeValue::Resource(_) => true, } } @@ -2718,6 +2726,7 @@ mod tests { FakeValue::Array(_) => "Array".to_string(), FakeValue::Assoc(_) => "Array".to_string(), FakeValue::Object(_) => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), } } } @@ -3847,10 +3856,11 @@ echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); echo is_numeric("-5"); echo is_numeric("3.14"); echo is_numeric("abc") ? "bad" : "N"; echo is_numeric(true) ? "bad" : "B"; +echo is_resource(1) ? "bad" : "R"; echo ":"; echo call_user_func("is_string", "x"); echo call_user_func_array("is_array", [[1]]); echo call_user_func("is_numeric", "12"); -echo function_exists("is_numeric"); +echo function_exists("is_numeric"); echo function_exists("is_resource"); return function_exists("is_double");"#, ) .expect("parse eval fragment"); @@ -3859,7 +3869,27 @@ return function_exists("is_double");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "11111111111ok11111NB:1111"); + assert_eq!(values.output, "11111111111ok11111NBR:11111"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. + #[test] + fn execute_program_dispatches_is_resource_true() { + let program = parse_fragment( + br#"echo is_resource($handle) ? "R" : "bad"; +echo ":" . gettype($handle); +return call_user_func("is_resource", $handle);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "R:resource"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3c150cad8c..6b5b66692f 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 3ea2319070..4214d1a346 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 21b3c0553c..5a28f9f939 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -78,7 +78,8 @@ public function echoLabelThroughEval(): void { $magic_file_has_path = eval('return strlen(__FILE__) > strlen(__DIR__);'); $magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); $magic_scope = eval('return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";'); -$type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?") . (is_numeric("42") ? "n" : "?");'); +$memory = fopen("php://memory", "r+"); +$type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?") . (is_numeric("42") ? "n" : "?") . (is_resource($memory) ? "r" : "?");'); $casts = eval('return strval(intval("42")) . ":" . strval(floatval("3.5")) . ":" . (boolval("0") ? "true" : "false");'); $type_name = eval('return gettype(["ok"]);'); $absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index be09c93ea3..a8052527c7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -810,6 +810,7 @@ echo ":"; echo function_exists("trim"); echo function_exists("ltrim"); echo func fn test_eval_dispatches_type_predicate_builtin_calls() { let out = compile_and_run( r#" Date: Mon, 15 Jun 2026 23:47:13 +0200 Subject: [PATCH 0070/1208] Support eval first-character case builtins --- crates/elephc-eval/src/interpreter.rs | 33 ++++++++++++++++++++++----- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 10 +++++--- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c63073b245..eeebacab7c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -771,7 +771,9 @@ fn eval_call( "strcmp" | "strcasecmp" => eval_builtin_string_compare(name, args, context, scope, values), "strlen" => eval_builtin_strlen(args, context, scope, values), "strpos" | "strrpos" => eval_builtin_string_position(name, args, context, scope, values), - "strtolower" | "strtoupper" => eval_builtin_string_case(name, args, context, scope, values), + "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { + eval_builtin_string_case(name, args, context, scope, values) + } "trim" => eval_builtin_trim_like(name, args, context, scope, values), _ => { if let Some(function) = context.function(name).cloned() { @@ -910,6 +912,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_real" | "is_resource" | "is_string" + | "lcfirst" | "ord" | "pow" | "rtrim" @@ -927,6 +930,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strtoupper" | "strval" | "trim" + | "ucfirst" ) } @@ -1172,7 +1176,7 @@ fn eval_builtin_with_values( }; eval_string_compare_result(name, *left, *right, values)? } - "strtolower" | "strtoupper" => { + "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -1795,6 +1799,16 @@ fn eval_string_case_result( } } } + "ucfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { + bytes[0] -= b'a' - b'A'; + } + } + "lcfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { + bytes[0] += b'a' - b'A'; + } + } _ => return Err(EvalStatus::UnsupportedConstruct), } let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; @@ -3706,10 +3720,14 @@ return function_exists("array_search");"#, let program = parse_fragment( br#"echo strtoupper("Hello World"); echo ":"; echo strtolower("LOUD"); echo ":"; +echo ucfirst("eval"); echo ":"; +echo lcfirst("LOUD"); echo ":"; echo call_user_func("strtoupper", "xy"); echo ":"; -echo call_user_func_array("strtolower", ["ZZ"]); -echo ":"; echo function_exists("strtoupper"); -return function_exists("strtolower");"#, +echo call_user_func_array("strtolower", ["ZZ"]); echo ":"; +echo call_user_func("ucfirst", "case"); echo ":"; +echo call_user_func_array("lcfirst", ["CASE"]); +echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower"); echo function_exists("ucfirst"); +return function_exists("lcfirst");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -3717,7 +3735,10 @@ return function_exists("strtolower");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "HELLO WORLD:loud:XY:zz:1"); + assert_eq!( + values.output, + "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:111" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 6b5b66692f..83ed740573 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4214d1a346..eb4de0d2ef 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 5a28f9f939..4cd3536af2 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -87,7 +87,7 @@ public function echoLabelThroughEval(): void { $rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); -$case = eval('return strtoupper("eval") . ":" . strtolower("LOUD");'); +$case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); $ordinal = eval('return ord("A") . ":" . ord("");'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a8052527c7..89693f376b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -707,12 +707,16 @@ fn test_eval_dispatches_string_case_builtin_calls() { r#" Date: Mon, 15 Jun 2026 23:55:44 +0200 Subject: [PATCH 0071/1208] Support eval min max builtins --- crates/elephc-eval/src/interpreter.rs | 69 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 17 +++++++ 5 files changed, 90 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index eeebacab7c..a069ead583 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -760,6 +760,7 @@ fn eval_call( eval_builtin_type_predicate(name, args, context, scope, values) } "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), + "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), "pow" => eval_builtin_pow(args, context, scope, values), "round" => eval_builtin_round(args, context, scope, values), @@ -913,6 +914,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_resource" | "is_string" | "lcfirst" + | "max" + | "min" | "ord" | "pow" | "rtrim" @@ -1117,6 +1120,7 @@ fn eval_builtin_with_values( }; eval_ord_result(*value, values)? } + "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values)?, [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, @@ -1397,6 +1401,48 @@ fn eval_builtin_sqrt( values.sqrt(value) } +/// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. +fn eval_builtin_min_max( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_min_max_result(name, &evaluated_args, values) +} + +/// Selects the smallest or largest evaluated cell using runtime comparison hooks. +fn eval_min_max_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((&first, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let op = match name { + "min" => EvalBinOp::Lt, + "max" => EvalBinOp::Gt, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let mut selected = first; + for candidate in rest { + let better = values.compare(op, *candidate, selected)?; + if values.truthy(better)? { + selected = *candidate; + } + } + Ok(selected) +} + /// Evaluates PHP scalar cast builtins over one eval expression. fn eval_builtin_cast( name: &str, @@ -4048,6 +4094,29 @@ return function_exists("round");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `min()` and `max()` select numeric values directly and by callable. + #[test] + fn execute_program_dispatches_min_max_builtins() { + let program = parse_fragment( + br#"echo min(3, 1, 2); echo ":"; +echo max(1, 3, 2); echo ":"; +echo min(2.5, 1.5); echo ":"; +echo max(1.5, 2.5); echo ":"; +echo call_user_func("min", 9, 4, 7); echo ":"; +echo call_user_func_array("max", [4, 8, 6]); echo ":"; +echo function_exists("min"); +return function_exists("max");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:1.5:2.5:4:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. #[test] fn execute_program_dispatches_sqrt_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 83ed740573..3b8435b41a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index eb4de0d2ef..cc67cb6301 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 4cd3536af2..a008faaf34 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -87,6 +87,7 @@ public function echoLabelThroughEval(): void { $rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); +$minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); @@ -136,6 +137,7 @@ public function echoLabelThroughEval(): void { echo "rounding=" . $rounding . "\n"; echo "builtin-power=" . $builtin_power . "\n"; echo "rounded=" . $rounded . "\n"; +echo "minmax=" . $minmax . "\n"; echo "case=" . $case . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 89693f376b..224c7c4401 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -942,6 +942,23 @@ echo ":"; echo function_exists("round");'); assert_eq!(out, "4:3.14:double:3:1.6:1"); } +/// Verifies eval `min()` and `max()` select numeric values directly and through callables. +#[test] +fn test_eval_dispatches_min_max_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 00:01:43 +0200 Subject: [PATCH 0072/1208] Support eval pi builtin --- crates/elephc-eval/src/interpreter.rs | 39 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 15 +++++++++++ 5 files changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index a069ead583..359afd34da 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -762,6 +762,7 @@ fn eval_call( "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), + "pi" => eval_builtin_pi(args, values), "pow" => eval_builtin_pow(args, context, scope, values), "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), @@ -917,6 +918,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "max" | "min" | "ord" + | "pi" | "pow" | "rtrim" | "round" @@ -1072,6 +1074,12 @@ fn eval_builtin_with_values( }; values.floor(*value)? } + "pi" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI)? + } "pow" => { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1351,6 +1359,17 @@ fn eval_builtin_floor( values.floor(value) } +/// Evaluates PHP's zero-argument `pi()` builtin. +fn eval_builtin_pi( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI) +} + /// Evaluates PHP's `pow(...)` over two eval expressions. fn eval_builtin_pow( args: &[EvalExpr], @@ -4117,6 +4136,26 @@ return function_exists("max");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `pi()` returns a double constant directly and through callable paths. + #[test] + fn execute_program_dispatches_pi_builtin() { + let program = parse_fragment( + br#"echo round(pi(), 2); echo ":"; +echo gettype(pi()); echo ":"; +echo round(call_user_func("pi"), 3); echo ":"; +echo round(call_user_func_array("pi", []), 4); echo ":"; +return function_exists("pi");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3.14:double:3.142:3.1416:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. #[test] fn execute_program_dispatches_sqrt_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3b8435b41a..9167d6533e 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index cc67cb6301..2adbb4168e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index a008faaf34..120e311441 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -88,6 +88,7 @@ public function echoLabelThroughEval(): void { $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5);'); +$circle = eval('return round(pi(), 2);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); @@ -138,6 +139,7 @@ public function echoLabelThroughEval(): void { echo "builtin-power=" . $builtin_power . "\n"; echo "rounded=" . $rounded . "\n"; echo "minmax=" . $minmax . "\n"; +echo "pi=" . $circle . "\n"; echo "case=" . $case . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 224c7c4401..5d97324cd3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -959,6 +959,21 @@ echo ":"; echo function_exists("min"); echo function_exists("max");'); assert_eq!(out, "1:3:1.5:2.5:4:8:11"); } +/// Verifies eval `pi()` returns the PHP math constant through direct and callable calls. +#[test] +fn test_eval_dispatches_pi_builtin_call() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 00:11:54 +0200 Subject: [PATCH 0073/1208] Support eval array key exists builtin --- crates/elephc-eval/src/interpreter.rs | 70 ++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 13 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 92 ++++++++++++++++++++++ tests/codegen/eval.rs | 18 +++++ 7 files changed, 197 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 359afd34da..7afaab8217 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -44,6 +44,13 @@ pub trait RuntimeValueOps { index: RuntimeCellHandle, ) -> Result; + /// Checks whether a normalized PHP array key exists without conflating null values with misses. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result; + /// Returns the foreach-visible key at a zero-based iteration position. fn array_iter_key( &mut self, @@ -733,6 +740,7 @@ fn eval_call( "array_keys" | "array_values" => { eval_builtin_array_projection(name, args, context, scope, values) } + "array_key_exists" => eval_builtin_array_key_exists(args, context, scope, values), "array_product" | "array_sum" => { eval_builtin_array_aggregate(name, args, context, scope, values) } @@ -882,6 +890,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, "abs" + | "array_key_exists" | "array_keys" | "array_product" | "array_search" @@ -1056,6 +1065,12 @@ fn eval_builtin_with_values( }; eval_array_projection_result(name, *array, values)? } + "array_key_exists" => { + let [key, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.array_key_exists(*key, *array)? + } "array_search" | "in_array" => { let [needle, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1288,6 +1303,21 @@ fn eval_array_projection_result( Ok(result) } +/// Evaluates PHP `array_key_exists()` over a key and array expression. +fn eval_builtin_array_key_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [key, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let key = eval_expr(key, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + values.array_key_exists(key, array) +} + /// Evaluates PHP array search builtins over a needle and haystack expression. fn eval_builtin_array_search( name: &str, @@ -2225,6 +2255,23 @@ mod tests { } } + /// Checks whether a fake array has the requested key without reading its value. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + let key = self.key(key)?; + let exists = match self.get(array) { + FakeValue::Array(elements) => { + matches!(key, FakeKey::Int(index) if index >= 0 && (index as usize) < elements.len()) + } + FakeValue::Assoc(entries) => entries.iter().any(|(entry_key, _)| entry_key == &key), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.bool_value(exists) + } + /// Returns one fake foreach key by insertion-order position. fn array_iter_key( &mut self, @@ -3754,6 +3801,29 @@ return function_exists("array_values");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. + #[test] + fn execute_program_dispatches_array_key_exists_builtin() { + let program = parse_fragment( + br#"$map = ["name" => null, "age" => 30]; +echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; +echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; +echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; +echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; +echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; +echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; echo ":"; +return function_exists("array_key_exists");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:Y:N:Y:Y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval array search builtins use loose comparison and return keys or booleans. #[test] fn execute_program_dispatches_array_search_builtins() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 7b36cf38b6..57ed0f868e 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -28,6 +28,10 @@ unsafe extern "C" { array: *mut RuntimeCell, index: *mut RuntimeCell, ) -> *mut RuntimeCell; + fn __elephc_eval_value_array_key_exists( + key: *mut RuntimeCell, + array: *mut RuntimeCell, + ) -> *mut RuntimeCell; fn __elephc_eval_value_array_iter_key( array: *mut RuntimeCell, position: u64, @@ -159,6 +163,15 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_array_get(array.as_ptr(), index.as_ptr()) }) } + /// Checks whether a boxed Mixed array contains a normalized PHP key. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_key_exists(key.as_ptr(), array.as_ptr()) }) + } + /// Returns one foreach-visible key from a boxed Mixed array by iteration position. fn array_iter_key( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9167d6533e..271eb96762 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 2adbb4168e..8f0bd307c2 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 120e311441..86e6aa644d 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -97,6 +97,7 @@ public function echoLabelThroughEval(): void { $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); +$array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); eval('function native_add($left, $right) { return $left + $right; }'); @@ -148,6 +149,7 @@ public function echoLabelThroughEval(): void { echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; echo "array-projection=" . $array_projection . "\n"; +echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; $counter = new EvalCounter(); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 0dca31cda7..b3b273c0ad 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -106,6 +106,51 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #32"); // release the array-get wrapper frame emitter.instruction("ret"); // return the boxed element to Rust + label_c_global(emitter, "__elephc_eval_value_array_key_exists"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for key existence probing + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the boxed array receiver while normalizing the key + emitter.instruction("bl __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("str x1, [sp, #8]"); // save the normalized key low word + emitter.instruction("str x2, [sp, #16]"); // save the normalized key high word + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed array receiver for tag dispatch + emitter.instruction("cbz x0, __elephc_eval_value_array_key_exists_false"); // null handles do not contain array keys + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #4"); // tag 4 = indexed array + emitter.instruction("b.eq __elephc_eval_value_array_key_exists_indexed"); // indexed arrays use bounds-based key existence + emitter.instruction("cmp x9, #5"); // tag 5 = associative array + emitter.instruction("b.eq __elephc_eval_value_array_key_exists_assoc"); // associative arrays use hash existence + emitter.instruction("b __elephc_eval_value_array_key_exists_false"); // scalar values do not contain array keys + emitter.label("__elephc_eval_value_array_key_exists_indexed"); + emitter.instruction("ldr x2, [sp, #16]"); // reload normalized key_hi for integer-key checking + emitter.instruction("cmn x2, #1"); // integer keys carry key_hi = -1 + emitter.instruction("b.ne __elephc_eval_value_array_key_exists_false"); // non-integer keys never exist in indexed arrays + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed indexed-array receiver + emitter.instruction("ldr x0, [x0, #8]"); // load the indexed-array payload pointer + emitter.instruction("cbz x0, __elephc_eval_value_array_key_exists_false"); // missing payload cannot contain a key + emitter.instruction("ldr x1, [sp, #8]"); // pass normalized integer key to the bounds helper + emitter.instruction("bl __rt_array_key_exists"); // return whether the integer key is in bounds + emitter.instruction("b __elephc_eval_value_array_key_exists_box"); // box the existence flag for Rust + emitter.label("__elephc_eval_value_array_key_exists_assoc"); + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed associative-array receiver + emitter.instruction("ldr x0, [x0, #8]"); // load the hash payload pointer + emitter.instruction("cbz x0, __elephc_eval_value_array_key_exists_false"); // missing hash payload cannot contain a key + emitter.instruction("ldr x1, [sp, #8]"); // pass normalized key_lo to the hash lookup + emitter.instruction("ldr x2, [sp, #16]"); // pass normalized key_hi to the hash lookup + emitter.instruction("bl __rt_hash_get"); // return hash found flag in x0 + emitter.instruction("b __elephc_eval_value_array_key_exists_box"); // box the hash existence flag for Rust + emitter.label("__elephc_eval_value_array_key_exists_false"); + emitter.instruction("mov x0, #0"); // report false for misses and unsupported receivers + emitter.label("__elephc_eval_value_array_key_exists_box"); + emitter.instruction("mov x1, x0"); // move the C bool result into mixed value_lo + emitter.instruction("mov x0, #3"); // runtime tag 3 = boolean + emitter.instruction("mov x2, xzr"); // boolean payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the bool result for Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the key-exists wrapper frame + emitter.instruction("ret"); // return the boxed bool result to Rust + label_c_global(emitter, "__elephc_eval_value_array_iter_key"); emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for insertion-order key iteration emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls @@ -975,6 +1020,53 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed element to Rust + label_c_global(emitter, "__elephc_eval_value_array_key_exists"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve slots for receiver and normalized key words + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the boxed array receiver while normalizing the key + emitter.instruction("call __elephc_eval_key_normalize"); // normalize eval array key to key_lo/key_hi + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the normalized key low word + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the normalized key high word + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the boxed array receiver for tag dispatch + emitter.instruction("test rdi, rdi"); // null handles do not contain array keys + emitter.instruction("jz __elephc_eval_value_array_key_exists_false"); // report false for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 4"); // tag 4 = indexed array + emitter.instruction("je __elephc_eval_value_array_key_exists_indexed"); // indexed arrays use bounds-based key existence + emitter.instruction("cmp r10, 5"); // tag 5 = associative array + emitter.instruction("je __elephc_eval_value_array_key_exists_assoc"); // associative arrays use hash existence + emitter.instruction("jmp __elephc_eval_value_array_key_exists_false"); // scalar values do not contain array keys + emitter.label("__elephc_eval_value_array_key_exists_indexed"); + emitter.instruction("cmp QWORD PTR [rbp - 24], -1"); // integer keys carry key_hi = -1 + emitter.instruction("jne __elephc_eval_value_array_key_exists_false"); // non-integer keys never exist in indexed arrays + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the boxed indexed-array receiver + emitter.instruction("mov rdi, QWORD PTR [rdi + 8]"); // load the indexed-array payload pointer + emitter.instruction("test rdi, rdi"); // missing payload cannot contain a key + emitter.instruction("jz __elephc_eval_value_array_key_exists_false"); // report false for missing indexed-array payloads + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass normalized integer key to the bounds helper + emitter.instruction("call __rt_array_key_exists"); // return whether the integer key is in bounds + emitter.instruction("jmp __elephc_eval_value_array_key_exists_box"); // box the existence flag for Rust + emitter.label("__elephc_eval_value_array_key_exists_assoc"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the boxed associative-array receiver + emitter.instruction("mov rdi, QWORD PTR [rdi + 8]"); // load the hash payload pointer + emitter.instruction("test rdi, rdi"); // missing hash payload cannot contain a key + emitter.instruction("jz __elephc_eval_value_array_key_exists_false"); // report false for missing associative-array payloads + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass normalized key_lo to the hash lookup + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // pass normalized key_hi to the hash lookup + emitter.instruction("call __rt_hash_get"); // return hash found flag in rax + emitter.instruction("jmp __elephc_eval_value_array_key_exists_box"); // box the hash existence flag for Rust + emitter.label("__elephc_eval_value_array_key_exists_false"); + emitter.instruction("xor eax, eax"); // report false for misses and unsupported receivers + emitter.label("__elephc_eval_value_array_key_exists_box"); + emitter.instruction("mov rdi, rax"); // move the C bool result into mixed value_lo + emitter.instruction("mov eax, 3"); // runtime tag 3 = boolean + emitter.instruction("xor esi, esi"); // boolean payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the bool result for Rust + emitter.instruction("add rsp, 32"); // release the key-exists wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed bool result to Rust + label_c_global(emitter, "__elephc_eval_value_array_iter_key"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls emitter.instruction("mov rbp, rsp"); // establish a stable iterator-key wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5d97324cd3..1112220172 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -681,6 +681,24 @@ echo ":"; echo function_exists("array_keys"); echo function_exists("array_values assert_eq!(out, "10:20:a:b:0:z:8:11"); } +/// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. +#[test] +fn test_eval_dispatches_array_key_exists_builtin_calls() { + let out = compile_and_run( + r#" null, "age" => 30]; +echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; +echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; +echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; +echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; +echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; +echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; +echo ":"; echo function_exists("array_key_exists");'); +"#, + ); + assert_eq!(out, "Y:N:Y:N:Y:Y:1"); +} + /// Verifies eval array search builtins return booleans or matching keys. #[test] fn test_eval_dispatches_array_search_builtin_calls() { From 0b6f71c1d741f67e5387c17400bc5d3609307337 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 00:24:45 +0200 Subject: [PATCH 0074/1208] Support eval float division builtins --- crates/elephc-eval/src/interpreter.rs | 95 ++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 26 +++++ docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 2 + examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 110 +++++++++++++++++++++ tests/codegen/eval.rs | 18 ++++ 7 files changed, 255 insertions(+) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7afaab8217..728e1e0a09 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -143,6 +143,20 @@ pub trait RuntimeValueOps { /// Computes PHP `sqrt()` for one runtime cell after PHP numeric conversion. fn sqrt(&mut self, value: RuntimeCellHandle) -> Result; + /// Divides two runtime cells using PHP `fdiv()` semantics. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Computes the floating-point remainder using PHP `fmod()` semantics. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + /// Adds two runtime cells using PHP addition semantics. fn add( &mut self, @@ -757,6 +771,7 @@ fn eval_call( "count" => eval_builtin_count(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), + "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) @@ -902,8 +917,10 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "boolval" | "chop" | "count" + | "fdiv" | "floor" | "floatval" + | "fmod" | "function_exists" | "gettype" | "hash_equals" @@ -1089,6 +1106,12 @@ fn eval_builtin_with_values( }; values.floor(*value)? } + "fdiv" | "fmod" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_binary_result(name, *left, *right, values)? + } "pi" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -1450,6 +1473,36 @@ fn eval_builtin_sqrt( values.sqrt(value) } +/// Evaluates PHP floating-point binary math builtins over two eval expressions. +fn eval_builtin_float_binary( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_float_binary_result(name, left, right, values) +} + +/// Dispatches an evaluated pair through the matching PHP float math hook. +fn eval_float_binary_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "fdiv" => values.fdiv(left, right), + "fmod" => values.fmod(left, right), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. fn eval_builtin_min_max( name: &str, @@ -2541,6 +2594,28 @@ mod tests { self.float(self.fake_numeric(&value).sqrt()) } + /// Divides fake numeric cells with PHP `fdiv()` zero handling. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left / right) + } + + /// Computes fake floating-point modulo for interpreter tests. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left % right) + } + /// Adds fake numeric cells for interpreter tests. fn add( &mut self, @@ -4142,6 +4217,26 @@ return function_exists("ceil");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `fdiv()` and `fmod()` dispatch as floating-point binary builtins. + #[test] + fn execute_program_dispatches_float_binary_builtins() { + let program = parse_fragment( + br#"echo round(fdiv(10, 4), 2); echo ":"; +echo gettype(fdiv(10, 4)); echo ":"; +echo round(fmod(10.5, 3.2), 1); echo ":"; +echo round(call_user_func("fdiv", 9, 2), 1); echo ":"; +echo round(call_user_func_array("fmod", [10.5, 3.2]), 1); echo ":"; +echo function_exists("fdiv"); +return function_exists("fmod");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "2.5:double:0.9:4.5:0.9:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. #[test] fn execute_program_dispatches_pow_builtin() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 57ed0f868e..cd8a9ad012 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -75,6 +75,14 @@ unsafe extern "C" { fn __elephc_eval_value_ceil(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_floor(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_sqrt(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_fdiv( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + fn __elephc_eval_value_fmod( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; fn __elephc_eval_value_add(left: *mut RuntimeCell, right: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_sub(left: *mut RuntimeCell, right: *mut RuntimeCell) @@ -353,6 +361,24 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_sqrt(value.as_ptr()) }) } + /// Computes PHP `fdiv()` for boxed Mixed cells through the generated runtime wrapper. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_fdiv(left.as_ptr(), right.as_ptr()) }) + } + + /// Computes PHP `fmod()` for boxed Mixed cells through the generated runtime wrapper. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_fmod(left.as_ptr(), right.as_ptr()) }) + } + /// Adds two boxed Mixed cells using elephc runtime numeric semantics. fn add( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 271eb96762..a02ddffcae 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,6 +19,8 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +Eval builtin dispatch also covers `fdiv()` and `fmod()` through target-aware bridge wrappers that return boxed double cells. + ## Why a runtime? Some operations can't be done with a few inline instructions: diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 8f0bd307c2..25a12d495f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,6 +34,8 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +Eval builtin dispatch also supports floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. + `php_uname()` supports PHP's standard one-character modes: | Mode | Result | diff --git a/examples/eval/main.php b/examples/eval/main.php index 86e6aa644d..bd4447ea4d 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -84,6 +84,7 @@ public function echoLabelThroughEval(): void { $type_name = eval('return gettype(["ok"]);'); $absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); $root = eval('return sqrt(81) . ":" . gettype(sqrt(16));'); +$float_binary = eval('return fdiv(10, 4) . ":" . round(fmod(10.5, 3.2), 1);'); $rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); @@ -136,6 +137,7 @@ public function echoLabelThroughEval(): void { echo "type-name=" . $type_name . "\n"; echo "absolute=" . $absolute . "\n"; echo "root=" . $root . "\n"; +echo "float-binary=" . $float_binary . "\n"; echo "rounding=" . $rounding . "\n"; echo "builtin-power=" . $builtin_power . "\n"; echo "rounded=" . $rounded . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index b3b273c0ad..9a62c7fd77 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -457,6 +457,62 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the sqrt wrapper frame emitter.instruction("ret"); // return the boxed sqrt result to Rust + label_c_global(emitter, "__elephc_eval_value_fdiv"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the left double across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("fmov d1, d0"); // keep the right divisor in d1 + emitter.instruction("ldr d0, [sp, #8]"); // reload the left dividend into d0 + emitter.instruction("fdiv d0, d0, d1"); // compute fdiv() with IEEE zero handling + emitter.instruction("fcmp d0, d0"); // detect NaN so PHP echo prints NAN without a sign + emitter.instruction("b.vs __elephc_eval_value_fdiv_nan"); // normalize unordered fdiv results before boxing + emitter.instruction("fmov x1, d0"); // move the fdiv result bits into mixed value_lo + emitter.instruction("b __elephc_eval_value_fdiv_box"); // skip the canonical NaN payload path + emitter.label("__elephc_eval_value_fdiv_nan"); + emitter.instruction("mov x1, xzr"); // start the canonical quiet NaN payload from zero bits + emitter.instruction("movk x1, #0x7ff8, lsl #48"); // install the positive quiet NaN exponent/significand + emitter.label("__elephc_eval_value_fdiv_box"); + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the fdiv result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the fdiv wrapper frame + emitter.instruction("ret"); // return the boxed fdiv result to Rust + + label_c_global(emitter, "__elephc_eval_value_fmod"); + emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the right boxed operand while casting the left operand + emitter.instruction("bl __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("str d0, [sp, #8]"); // save the left double across the right cast + emitter.instruction("ldr x0, [sp, #0]"); // reload the right boxed operand for numeric casting + emitter.instruction("bl __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("fmov d1, d0"); // keep the right divisor in d1 + emitter.instruction("ldr d0, [sp, #8]"); // reload the left dividend into d0 + emitter.instruction("fdiv d2, d0, d1"); // compute the fmod quotient before truncation + emitter.instruction("frintz d2, d2"); // truncate the quotient toward zero + emitter.instruction("fmsub d0, d2, d1, d0"); // compute dividend minus truncated quotient times divisor + emitter.instruction("fcmp d0, d0"); // detect NaN so PHP echo prints NAN without a sign + emitter.instruction("b.vs __elephc_eval_value_fmod_nan"); // normalize unordered fmod results before boxing + emitter.instruction("fmov x1, d0"); // move the fmod result bits into mixed value_lo + emitter.instruction("b __elephc_eval_value_fmod_box"); // skip the canonical NaN payload path + emitter.label("__elephc_eval_value_fmod_nan"); + emitter.instruction("mov x1, xzr"); // start the canonical quiet NaN payload from zero bits + emitter.instruction("movk x1, #0x7ff8, lsl #48"); // install the positive quiet NaN exponent/significand + emitter.label("__elephc_eval_value_fmod_box"); + emitter.instruction("mov x2, xzr"); // double payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = double + emitter.instruction("bl __rt_mixed_from_value"); // box the fmod result into a Mixed cell + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the fmod wrapper frame + emitter.instruction("ret"); // return the boxed fmod result to Rust + label_c_global(emitter, "__elephc_eval_value_add"); emitter.instruction("b __rt_mixed_numeric_add"); // add two boxed mixed values and return the boxed result @@ -1380,6 +1436,60 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed sqrt result to Rust + label_c_global(emitter, "__elephc_eval_value_fdiv"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the left double across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("movapd xmm1, xmm0"); // keep the right divisor in xmm1 + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // reload the left dividend into xmm0 + emitter.instruction("divsd xmm0, xmm1"); // compute fdiv() with IEEE zero handling + emitter.instruction("ucomisd xmm0, xmm0"); // detect NaN so PHP echo prints NAN without a sign + emitter.instruction("jp __elephc_eval_value_fdiv_nan_x86"); // normalize unordered fdiv results before boxing + emitter.instruction("movq rdi, xmm0"); // move the fdiv result bits into mixed value_lo + emitter.instruction("jmp __elephc_eval_value_fdiv_box_x86"); // skip the canonical NaN payload path + emitter.label("__elephc_eval_value_fdiv_nan_x86"); + emitter.instruction("movabs rdi, 0x7ff8000000000000"); // use a positive quiet NaN payload for PHP output + emitter.label("__elephc_eval_value_fdiv_box_x86"); + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the fdiv result into a Mixed cell + emitter.instruction("add rsp, 32"); // release the fdiv wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed fdiv result to Rust + + label_c_global(emitter, "__elephc_eval_value_fmod"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for the right operand and left double + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the right boxed operand while casting the left operand + emitter.instruction("mov rax, rdi"); // move the left boxed operand into mixed_cast_float input + emitter.instruction("call __rt_mixed_cast_float"); // cast the left boxed operand to a PHP numeric double + emitter.instruction("movsd QWORD PTR [rbp - 16], xmm0"); // save the left double across the right cast + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the right boxed operand for numeric casting + emitter.instruction("call __rt_mixed_cast_float"); // cast the right boxed operand to a PHP numeric double + emitter.instruction("movapd xmm1, xmm0"); // move the right divisor into the second fmod argument + emitter.instruction("movsd xmm0, QWORD PTR [rbp - 16]"); // move the left dividend into the first fmod argument + emitter.bl_c("fmod"); + emitter.instruction("ucomisd xmm0, xmm0"); // detect NaN so PHP echo prints NAN without a sign + emitter.instruction("jp __elephc_eval_value_fmod_nan_x86"); // normalize unordered fmod results before boxing + emitter.instruction("movq rdi, xmm0"); // move the fmod result bits into mixed value_lo + emitter.instruction("jmp __elephc_eval_value_fmod_box_x86"); // skip the canonical NaN payload path + emitter.label("__elephc_eval_value_fmod_nan_x86"); + emitter.instruction("movabs rdi, 0x7ff8000000000000"); // use a positive quiet NaN payload for PHP output + emitter.label("__elephc_eval_value_fmod_box_x86"); + emitter.instruction("xor esi, esi"); // double payloads do not use a high word + emitter.instruction("mov eax, 2"); // runtime tag 2 = double + emitter.instruction("call __rt_mixed_from_value"); // box the fmod result into a Mixed cell + emitter.instruction("add rsp, 32"); // release the fmod wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed fmod result to Rust + label_c_global(emitter, "__elephc_eval_value_add"); emitter.instruction("mov rax, rdi"); // move the left boxed operand into the internal result register emitter.instruction("mov rdi, rsi"); // move the right boxed operand into the internal argument register diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1112220172..7e4be1b243 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -929,6 +929,24 @@ echo ":"; echo function_exists("floor"); echo function_exists("ceil");'); assert_eq!(out, "3:double:4:double:4:5:11"); } +/// Verifies eval `fdiv()` and `fmod()` return boxed double cells through direct and callable calls. +#[test] +fn test_eval_dispatches_float_binary_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 00:34:37 +0200 Subject: [PATCH 0075/1208] Support eval strrev builtin --- crates/elephc-eval/src/interpreter.rs | 51 ++++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 6 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen_support/runtime/eval_bridge.rs | 25 +++++++++++ tests/codegen/eval.rs | 15 +++++++ 7 files changed, 101 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 728e1e0a09..e57f42e226 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -143,6 +143,9 @@ pub trait RuntimeValueOps { /// Computes PHP `sqrt()` for one runtime cell after PHP numeric conversion. fn sqrt(&mut self, value: RuntimeCellHandle) -> Result; + /// Reverses a string value using PHP `strrev()` byte-string semantics. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result; + /// Divides two runtime cells using PHP `fdiv()` semantics. fn fdiv( &mut self, @@ -790,6 +793,7 @@ fn eval_call( "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), + "strrev" => eval_builtin_strrev(args, context, scope, values), "str_contains" | "str_starts_with" | "str_ends_with" => { eval_builtin_string_search(name, args, context, scope, values) } @@ -957,6 +961,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strlen" | "strpos" | "strrpos" + | "strrev" | "strtolower" | "strtoupper" | "strval" @@ -1135,6 +1140,12 @@ fn eval_builtin_with_values( }; values.sqrt(*value)? } + "strrev" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.strrev(*value)? + } "call_user_func" => { return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) .map(Some); @@ -1473,6 +1484,20 @@ fn eval_builtin_sqrt( values.sqrt(value) } +/// Evaluates PHP's `strrev(...)` over one eval expression. +fn eval_builtin_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.strrev(value) +} + /// Evaluates PHP floating-point binary math builtins over two eval expressions. fn eval_builtin_float_binary( name: &str, @@ -2594,6 +2619,14 @@ mod tests { self.float(self.fake_numeric(&value).sqrt()) } + /// Reverses a fake string byte-wise for interpreter tests. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + let mut bytes = self.stringify(value).into_bytes(); + bytes.reverse(); + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + self.string(&value) + } + /// Divides fake numeric cells with PHP `fdiv()` zero handling. fn fdiv( &mut self, @@ -4341,6 +4374,24 @@ return function_exists("sqrt");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `strrev()` dispatches through direct and callable paths. + #[test] + fn execute_program_dispatches_strrev_builtin() { + let program = parse_fragment( + br#"echo strrev("Hello"); echo ":"; +echo strrev(123); echo ":"; +echo call_user_func("strrev", "ABC"); echo ":"; +echo call_user_func_array("strrev", ["def"]); echo ":"; +return function_exists("strrev");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "olleH:321:CBA:fed:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index cd8a9ad012..3e43fb0897 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -75,6 +75,7 @@ unsafe extern "C" { fn __elephc_eval_value_ceil(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_floor(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_sqrt(value: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_strrev(value: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_fdiv( left: *mut RuntimeCell, right: *mut RuntimeCell, @@ -361,6 +362,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_sqrt(value.as_ptr()) }) } + /// Computes PHP `strrev()` for a boxed Mixed cell through the generated runtime wrapper. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_strrev(value.as_ptr()) }) + } + /// Computes PHP `fdiv()` for boxed Mixed cells through the generated runtime wrapper. fn fdiv( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a02ddffcae..a931cd4713 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Eval builtin dispatch also covers `fdiv()` and `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 25a12d495f..6133392446 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval builtin dispatch also supports floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports `strrev()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index bd4447ea4d..353c00232a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -91,6 +91,7 @@ public function echoLabelThroughEval(): void { $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5);'); $circle = eval('return round(pi(), 2);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); +$reversed = eval('return strrev("eval");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); $ordinal = eval('return ord("A") . ":" . ord("");'); @@ -144,6 +145,7 @@ public function echoLabelThroughEval(): void { echo "minmax=" . $minmax . "\n"; echo "pi=" . $circle . "\n"; echo "case=" . $case . "\n"; +echo "reversed=" . $reversed . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; echo "ordinal=" . $ordinal . "\n"; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 9a62c7fd77..6649b49587 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -457,6 +457,18 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the sqrt wrapper frame emitter.instruction("ret"); // return the boxed sqrt result to Rust + label_c_global(emitter, "__elephc_eval_value_strrev"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and reversing + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("bl __rt_mixed_cast_string"); // cast the boxed eval argument to a PHP string pair + emitter.instruction("bl __rt_strrev"); // reverse the PHP byte string into concat storage + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the reversed string for Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the strrev wrapper frame + emitter.instruction("ret"); // return the boxed reversed string to Rust + label_c_global(emitter, "__elephc_eval_value_fdiv"); emitter.instruction("sub sp, sp, #32"); // allocate wrapper slots for the right operand and left double emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across helper calls @@ -1436,6 +1448,19 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed sqrt result to Rust + label_c_global(emitter, "__elephc_eval_value_strrev"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_cast_string input + emitter.instruction("call __rt_mixed_cast_string"); // cast the boxed eval argument to a PHP string pair + emitter.instruction("call __rt_strrev"); // reverse the PHP byte string into concat storage + emitter.instruction("mov rdi, rax"); // move the reversed string pointer into mixed value_lo + emitter.instruction("mov rsi, rdx"); // move the reversed string length into mixed value_hi + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the reversed string for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reversed string to Rust + label_c_global(emitter, "__elephc_eval_value_fdiv"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7e4be1b243..b792d9a2af 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -737,6 +737,21 @@ echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower") assert_eq!(out, "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:1111"); } +/// Verifies eval `strrev()` reverses byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_strrev_builtin_call() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 00:43:02 +0200 Subject: [PATCH 0076/1208] Support eval indexed array append --- crates/elephc-eval/src/eval_ir.rs | 4 +++ crates/elephc-eval/src/interpreter.rs | 40 ++++++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 45 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 15 +++++++++ 7 files changed, 106 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index da50886dd6..c9e34baa6e 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -46,6 +46,10 @@ impl EvalProgram { /// Dynamic eval statements that operate on a materialized activation scope. #[derive(Debug, Clone, PartialEq)] pub enum EvalStmt { + ArrayAppendVar { + name: String, + value: EvalExpr, + }, ArraySetVar { name: String, index: EvalExpr, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index e57f42e226..713d5f97f8 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -334,6 +334,33 @@ fn execute_stmt( values: &mut impl RuntimeValueOps, ) -> Result { match stmt { + EvalStmt::ArrayAppendVar { name, value } => { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some(existing) = + scope.entry(name).filter(|entry| entry.flags().is_visible()) + { + if values.is_array_like(existing.cell())? { + if values.type_tag(existing.cell())? != EVAL_TAG_ARRAY { + return Err(EvalStatus::UnsupportedConstruct); + } + ownership = existing.flags().ownership; + existing.cell() + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = values.array_len(array)?; + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let index = values.int(index)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + if let Some(replaced) = scope.set(name.clone(), array, ownership) { + values.release(replaced)?; + } + Ok(EvalControl::None) + } EvalStmt::ArraySetVar { name, index, value } => { let mut ownership = ScopeCellOwnership::Owned; let array = if let Some(existing) = @@ -4510,6 +4537,19 @@ echo function_exists("missing_probe") . "x";"#, assert_eq!(values.get(result), FakeValue::String("b".to_string())); } + /// Verifies indexed array append writes use the next visible index. + #[test] + fn execute_program_appends_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); + } + /// Verifies mutating a borrowed scope array does not make the eval scope own it. #[test] fn execute_program_preserves_borrowed_array_ownership() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index cdc392bbbc..8e530701fa 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -619,10 +619,16 @@ impl Parser { Ok(vec![EvalStmt::DoWhile { body, condition }]) } - /// Parses `$name[index] = expr;` for indexed-array eval writes. + /// Parses `$name[index] = expr;` and `$name[] = expr;` eval writes. fn parse_array_set_stmt(&mut self, name: String) -> Result, EvalParseError> { self.advance(); self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } let index = self.parse_expr()?; self.expect(TokenKind::RBracket)?; self.expect(TokenKind::Equal)?; @@ -770,10 +776,15 @@ impl Parser { } } - /// Parses `$name[index] = expr` in a `for` clause. + /// Parses `$name[index] = expr` and `$name[] = expr` in a `for` clause. fn parse_array_set_clause(&mut self, name: String) -> Result, EvalParseError> { self.advance(); self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } let index = self.parse_expr()?; self.expect(TokenKind::RBracket)?; self.expect(TokenKind::Equal)?; @@ -2524,6 +2535,36 @@ mod tests { ); } + /// Verifies indexed array append syntax parses as a variable-target append statement. + #[test] + fn parse_fragment_accepts_indexed_array_append_source() { + let program = parse_fragment(br#"$items[] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); + } + + /// Verifies array append syntax is accepted inside `for` update clauses. + #[test] + fn parse_fragment_accepts_array_append_in_for_update_source() { + let program = parse_fragment(br#"for ($i = 0; $i < 2; $items[] = $i) { $i += 1; }"#) + .expect("fragment should parse"); + let [EvalStmt::For { update, .. }] = program.statements() else { + panic!("expected for statement"); + }; + assert_eq!( + update, + &vec![EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::LoadVar("i".to_string()), + }] + ); + } + /// Verifies object property reads parse as postfix EvalIR expressions. #[test] fn parse_fragment_accepts_property_read_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a931cd4713..edc3c15ac4 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6133392446..c07bea5aba 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. Eval builtin dispatch also supports `strrev()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. diff --git a/examples/eval/main.php b/examples/eval/main.php index 353c00232a..9b5b2dfaeb 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -99,6 +99,7 @@ public function echoLabelThroughEval(): void { $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); +$append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); @@ -153,6 +154,7 @@ public function echoLabelThroughEval(): void { echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; echo "array-projection=" . $array_projection . "\n"; +echo "append-items=" . $append_items . "\n"; echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b792d9a2af..94ed002510 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -541,6 +541,21 @@ echo $items[0] . $items[1]; assert_eq!(out, "ab"); } +/// Verifies eval indexed-array append syntax writes the next visible element. +#[test] +fn test_eval_indexed_array_append_is_visible_after_eval() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 00:57:26 +0200 Subject: [PATCH 0077/1208] Support eval declared named arguments --- crates/elephc-eval/src/eval_ir.rs | 36 +++++- crates/elephc-eval/src/interpreter.rs | 167 +++++++++++++++++++++----- crates/elephc-eval/src/parser.rs | 60 ++++++--- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 11 ++ 7 files changed, 232 insertions(+), 50 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index c9e34baa6e..9486865a74 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -152,14 +152,14 @@ pub enum EvalExpr { }, Call { name: String, - args: Vec, + args: Vec, }, Const(EvalConst), LoadVar(String), MethodCall { object: Box, method: String, - args: Vec, + args: Vec, }, Magic(EvalMagicConst), NullCoalesce { @@ -187,6 +187,38 @@ pub enum EvalExpr { }, } +/// One source-order function or method call argument parsed from eval code. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalCallArg { + name: Option, + value: EvalExpr, +} + +impl EvalCallArg { + /// Creates a positional call argument from a value expression. + pub fn positional(value: EvalExpr) -> Self { + Self { name: None, value } + } + + /// Creates a named call argument from a parameter name and value expression. + pub fn named(name: impl Into, value: EvalExpr) -> Self { + Self { + name: Some(name.into()), + value, + } + } + + /// Returns the source argument name without `$`, if the argument was named. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Returns the expression that computes this argument's runtime value. + pub const fn value(&self) -> &EvalExpr { + &self.value + } +} + /// One element in a PHP array literal parsed from an eval fragment. #[derive(Debug, Clone, PartialEq)] pub enum EvalArrayElement { diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 713d5f97f8..a2ef8c4eda 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -14,8 +14,8 @@ use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::EvalStatus; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, EvalProgram, - EvalStmt, EvalSwitchCase, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, + EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership}; @@ -658,10 +658,7 @@ fn eval_expr( args, } => { let object = eval_expr(object, context, scope, values)?; - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } + let evaluated_args = eval_positional_call_arg_values(args, context, scope, values)?; values.method_call(object, method, evaluated_args) } EvalExpr::NullCoalesce { value, default } => { @@ -771,8 +768,60 @@ fn eval_expr( } } +/// Returns cloned positional argument expressions, rejecting named arguments. +fn positional_call_arg_exprs(args: &[EvalCallArg]) -> Result, EvalStatus> { + if args.iter().any(|arg| arg.name().is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(args.iter().map(|arg| arg.value().clone()).collect()) +} + +/// Evaluates a positional-only call argument list in source order. +fn eval_positional_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if args.iter().any(|arg| arg.name().is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg.value(), context, scope, values)?); + } + Ok(evaluated_args) +} + /// Evaluates supported function-like calls from a runtime eval fragment. fn eval_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_positional_expr_call_name(name) { + let args = positional_call_arg_exprs(args)?; + return eval_positional_expr_call(name, &args, context, scope, values); + } + + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Returns true for eval calls that still accept only positional expressions. +fn eval_positional_expr_call_name(name: &str) -> bool { + matches!(name, "empty" | "eval" | "isset") || eval_php_visible_builtin_exists(name) +} + +/// Evaluates builtins and language constructs after positional-only argument validation. +fn eval_positional_expr_call( name: &str, args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -831,15 +880,7 @@ fn eval_call( eval_builtin_string_case(name, args, context, scope, values) } "trim" => eval_builtin_trim_like(name, args, context, scope, values), - _ => { - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function(&function, args, context, scope, values); - } - if let Some(function) = context.native_function(name) { - return eval_native_function(function, args, context, scope, values); - } - Err(EvalStatus::UnsupportedConstruct) - } + _ => Err(EvalStatus::UnsupportedConstruct), } } @@ -2086,22 +2127,61 @@ fn eval_builtin_count( values.int(len) } -/// Evaluates an eval-declared user function with positional argument binding. +/// Evaluates an eval-declared user function with PHP-style argument binding. fn eval_dynamic_function( function: &EvalFunction, - args: &[EvalExpr], + args: &[EvalCallArg], context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - if args.len() != function.params().len() { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); + let evaluated_args = + eval_dynamic_function_call_args(function, args, context, caller_scope, values)?; + eval_dynamic_function_with_values(function, evaluated_args, context, values) +} + +/// Evaluates and binds eval-declared function arguments to parameter order. +fn eval_dynamic_function_call_args( + function: &EvalFunction, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let params = function.params(); + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + let mut saw_named = false; + for arg in args { - evaluated_args.push(eval_expr(arg, context, caller_scope, values)?); + if let Some(name) = arg.name() { + saw_named = true; + let value = eval_expr(arg.value(), context, caller_scope, values)?; + let Some(param_index) = params.iter().position(|param| param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + continue; + } + + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let value = eval_expr(arg.value(), context, caller_scope, values)?; + if next_positional >= params.len() || bound_args[next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(value); + next_positional += 1; } - eval_dynamic_function_with_values(function, evaluated_args, context, values) + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) } /// Evaluates an eval-declared function after its positional arguments are prepared. @@ -2128,18 +2208,12 @@ fn eval_dynamic_function_with_values( /// Evaluates a registered AOT function through its descriptor-compatible invoker. fn eval_native_function( function: NativeFunction, - args: &[EvalExpr], + args: &[EvalCallArg], context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - if args.len() != function.param_count() { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, caller_scope, values)?); - } + let evaluated_args = eval_positional_call_arg_values(args, context, caller_scope, values)?; eval_native_function_with_values(function, evaluated_args, values) } @@ -3699,6 +3773,37 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies eval-declared functions bind named arguments by parameter name. + #[test] + fn execute_program_calls_declared_function_with_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(y: 2, x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies named calls reject positional arguments that follow named arguments. + #[test] + fn execute_program_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, print "late");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert_eq!(values.output, ""); + } + /// Verifies function-scope magic constants keep the eval declaration spelling. #[test] fn execute_program_magic_function_and_method_use_eval_declared_name() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 8e530701fa..397947f3a4 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -15,8 +15,8 @@ use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalConst, EvalExpr, EvalMagicConst, EvalProgram, EvalStmt, - EvalSwitchCase, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalMagicConst, EvalProgram, + EvalStmt, EvalSwitchCase, EvalUnaryOp, }; /// Parses an eval fragment into by-name EvalIR statements. @@ -1451,14 +1451,14 @@ impl Parser { } /// Parses a parenthesized source-order argument list. - fn parse_call_args(&mut self) -> Result, EvalParseError> { + fn parse_call_args(&mut self) -> Result, EvalParseError> { self.expect(TokenKind::LParen)?; let mut args = Vec::new(); if self.consume(TokenKind::RParen) { return Ok(args); } loop { - args.push(self.parse_expr()?); + args.push(self.parse_call_arg()?); if !self.consume(TokenKind::Comma) { break; } @@ -1470,6 +1470,20 @@ impl Parser { Ok(args) } + /// Parses one positional or named argument within a call argument list. + fn parse_call_arg(&mut self) -> Result { + if matches!(self.peek(), TokenKind::Colon) { + if let TokenKind::Ident(name) = self.current() { + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Colon)?; + let value = self.parse_expr()?; + return Ok(EvalCallArg::named(name, value)); + } + } + self.parse_expr().map(EvalCallArg::positional) + } + /// Parses an array literal with source-order optional key/value element expressions. fn parse_array_literal(&mut self) -> Result { self.expect(TokenKind::LBracket)?; @@ -2447,7 +2461,25 @@ mod tests { program.statements(), &[EvalStmt::Return(Some(EvalExpr::Call { name: "eval".to_string(), - args: vec![EvalExpr::Const(EvalConst::String("return 1;".to_string()))], + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "return 1;".to_string() + )))], + }))] + ); + } + + /// Verifies function calls preserve named arguments in source order. + #[test] + fn parse_fragment_accepts_named_call_argument_source() { + let program = parse_fragment(br#"return add(y: 2, x: 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "add".to_string(), + args: vec![ + EvalCallArg::named("y", EvalExpr::Const(EvalConst::Int(2))), + EvalCallArg::named("x", EvalExpr::Const(EvalConst::Int(1))), + ], }))] ); } @@ -2462,11 +2494,11 @@ mod tests { &[EvalStmt::Return(Some(EvalExpr::Call { name: "isset".to_string(), args: vec![ - EvalExpr::LoadVar("x".to_string()), - EvalExpr::ArrayGet { + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::ArrayGet { array: Box::new(EvalExpr::LoadVar("items".to_string())), index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), - }, + }), ], }))] ); @@ -2481,10 +2513,10 @@ mod tests { program.statements(), &[EvalStmt::Return(Some(EvalExpr::Call { name: "empty".to_string(), - args: vec![EvalExpr::ArrayGet { + args: vec![EvalCallArg::positional(EvalExpr::ArrayGet { array: Box::new(EvalExpr::LoadVar("items".to_string())), index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), - }], + })], }))] ); } @@ -2616,11 +2648,11 @@ mod tests { &[EvalStmt::Return(Some(EvalExpr::MethodCall { object: Box::new(EvalExpr::LoadVar("this".to_string())), method: "add".to_string(), - args: vec![EvalExpr::Binary { + args: vec![EvalCallArg::positional(EvalExpr::Binary { op: EvalBinOp::Add, left: Box::new(EvalExpr::LoadVar("x".to_string())), right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }], + })], }))] ); } @@ -2636,8 +2668,8 @@ mod tests { object: Box::new(EvalExpr::LoadVar("this".to_string())), method: "label".to_string(), args: vec![ - EvalExpr::LoadVar("x".to_string()), - EvalExpr::Const(EvalConst::String("ok".to_string())), + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::Const(EvalConst::String("ok".to_string()))), ], }))] ); diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index edc3c15ac4..0847b35af7 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Calls currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Eval-declared function calls inside eval can bind positional or named arguments; native callbacks and post-barrier dispatch currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional or named arguments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c07bea5aba..69598433e6 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional or named arguments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. Eval builtin dispatch also supports `strrev()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. diff --git a/examples/eval/main.php b/examples/eval/main.php index 9b5b2dfaeb..61328a116f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -53,6 +53,7 @@ public function echoLabelThroughEval(): void { $meta_count = eval('return count($meta);'); eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); +$dynamic_named = eval('function named_pair($left, $right) { return $left . ":" . $right; } return named_pair(right: "R", left: "L");'); $dynamic_cuf = eval('return call_user_func("plus_one", 6);'); $dynamic_cufa = eval('return call_user_func_array("plus_one", [8]);'); $eval_native_call = eval('return compiled_add(2, 8);'); @@ -112,6 +113,7 @@ public function echoLabelThroughEval(): void { echo "source=" . $meta["source"] . "\n"; echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; +echo "dynamic-named=" . $dynamic_named . "\n"; echo "dynamic-cuf=" . $dynamic_cuf . "\n"; echo "dynamic-cufa=" . $dynamic_cufa . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 94ed002510..854a291ede 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1120,6 +1120,17 @@ echo eval('function dyn_eval_add($x) { return $x + 1; } return dyn_eval_add(4);' assert_eq!(out, "5"); } +/// Verifies eval-declared functions bind named arguments inside eval fragments. +#[test] +fn test_eval_declared_function_accepts_named_args_in_fragment() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 01:02:22 +0200 Subject: [PATCH 0078/1208] Support eval declared spread arguments --- crates/elephc-eval/src/eval_ir.rs | 22 ++++- crates/elephc-eval/src/interpreter.rs | 131 +++++++++++++++++++++++--- crates/elephc-eval/src/parser.rs | 23 ++++- docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 2 + examples/eval/main.php | 2 + tests/codegen/eval.rs | 11 +++ 7 files changed, 177 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 9486865a74..6c3dae329b 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -191,19 +191,34 @@ pub enum EvalExpr { #[derive(Debug, Clone, PartialEq)] pub struct EvalCallArg { name: Option, + spread: bool, value: EvalExpr, } impl EvalCallArg { /// Creates a positional call argument from a value expression. pub fn positional(value: EvalExpr) -> Self { - Self { name: None, value } + Self { + name: None, + spread: false, + value, + } } /// Creates a named call argument from a parameter name and value expression. pub fn named(name: impl Into, value: EvalExpr) -> Self { Self { name: Some(name.into()), + spread: false, + value, + } + } + + /// Creates an unpacking call argument from an array expression. + pub fn spread(value: EvalExpr) -> Self { + Self { + name: None, + spread: true, value, } } @@ -213,6 +228,11 @@ impl EvalCallArg { self.name.as_deref() } + /// Returns true when this argument came from `...expr` unpacking syntax. + pub const fn is_spread(&self) -> bool { + self.spread + } + /// Returns the expression that computes this argument's runtime value. pub const fn value(&self) -> &EvalExpr { &self.value diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index a2ef8c4eda..432e939d35 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -770,7 +770,10 @@ fn eval_expr( /// Returns cloned positional argument expressions, rejecting named arguments. fn positional_call_arg_exprs(args: &[EvalCallArg]) -> Result, EvalStatus> { - if args.iter().any(|arg| arg.name().is_some()) { + if args + .iter() + .any(|arg| arg.name().is_some() || arg.is_spread()) + { return Err(EvalStatus::RuntimeFatal); } Ok(args.iter().map(|arg| arg.value().clone()).collect()) @@ -783,7 +786,10 @@ fn eval_positional_call_arg_values( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - if args.iter().any(|arg| arg.name().is_some()) { + if args + .iter() + .any(|arg| arg.name().is_some() || arg.is_spread()) + { return Err(EvalStatus::RuntimeFatal); } let mut evaluated_args = Vec::with_capacity(args.len()); @@ -2154,16 +2160,41 @@ fn eval_dynamic_function_call_args( let mut saw_named = false; for arg in args { - if let Some(name) = arg.name() { - saw_named = true; - let value = eval_expr(arg.value(), context, caller_scope, values)?; - let Some(param_index) = params.iter().position(|param| param == name) else { + if arg.is_spread() { + if saw_named { return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[param_index].is_some() { + } + let spread = eval_expr(arg.value(), context, caller_scope, values)?; + if !values.is_array_like(spread)? { return Err(EvalStatus::RuntimeFatal); } - bound_args[param_index] = Some(value); + let len = values.array_len(spread)?; + for position in 0..len { + let key = values.array_iter_key(spread, position)?; + let value = values.array_get(spread, key)?; + match values.type_tag(key)? { + EVAL_TAG_INT => { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, value)?; + } + EVAL_TAG_STRING => { + saw_named = true; + let name = values.string_bytes(key)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + bind_dynamic_named_arg(params, &mut bound_args, &name, value)?; + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + continue; + } + + if let Some(name) = arg.name() { + saw_named = true; + let value = eval_expr(arg.value(), context, caller_scope, values)?; + bind_dynamic_named_arg(params, &mut bound_args, name, value)?; continue; } @@ -2171,11 +2202,7 @@ fn eval_dynamic_function_call_args( return Err(EvalStatus::RuntimeFatal); } let value = eval_expr(arg.value(), context, caller_scope, values)?; - if next_positional >= params.len() || bound_args[next_positional].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[next_positional] = Some(value); - next_positional += 1; + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, value)?; } bound_args @@ -2184,6 +2211,37 @@ fn eval_dynamic_function_call_args( .ok_or(EvalStatus::RuntimeFatal) } +/// Binds one positional dynamic-call value to the next declared parameter slot. +fn bind_dynamic_positional_arg( + bound_args: &mut [Option], + next_positional: &mut usize, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + if *next_positional >= bound_args.len() || bound_args[*next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[*next_positional] = Some(value); + *next_positional += 1; + Ok(()) +} + +/// Binds one named dynamic-call value to the matching declared parameter slot. +fn bind_dynamic_named_arg( + params: &[String], + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + let Some(param_index) = params.iter().position(|param| param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + Ok(()) +} + /// Evaluates an eval-declared function after its positional arguments are prepared. fn eval_dynamic_function_with_values( function: &EvalFunction, @@ -3788,6 +3846,36 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::Int(12)); } + /// Verifies eval-declared functions unpack indexed arrays as positional arguments. + #[test] + fn execute_program_calls_declared_function_with_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...[1, 2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies string keys unpack as named arguments for eval-declared functions. + #[test] + fn execute_program_calls_declared_function_with_named_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...["y" => 2], x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + /// Verifies named calls reject positional arguments that follow named arguments. #[test] fn execute_program_rejects_positional_after_named_arg() { @@ -3804,6 +3892,21 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.output, ""); } + /// Verifies named calls reject argument unpacking after named arguments. + #[test] + fn execute_program_rejects_spread_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, ...[2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + } + /// Verifies function-scope magic constants keep the eval declaration spelling. #[test] fn execute_program_magic_function_and_method_use_eval_declared_name() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 397947f3a4..a3ec7838ff 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -67,6 +67,7 @@ enum TokenKind { Tilde, Dot, DotEqual, + Ellipsis, Equal, EqualEqual, EqualEqualEqual, @@ -205,7 +206,11 @@ impl<'a> Lexer<'a> { } '.' => { self.bump_char(); - if self.peek_char() == Some('=') { + if self.peek_char() == Some('.') && self.peek_next_char() == Some('.') { + self.bump_char(); + self.bump_char(); + Ok(TokenKind::Ellipsis) + } else if self.peek_char() == Some('=') { self.bump_char(); Ok(TokenKind::DotEqual) } else { @@ -1472,6 +1477,9 @@ impl Parser { /// Parses one positional or named argument within a call argument list. fn parse_call_arg(&mut self) -> Result { + if self.consume(TokenKind::Ellipsis) { + return self.parse_expr().map(EvalCallArg::spread); + } if matches!(self.peek(), TokenKind::Colon) { if let TokenKind::Ident(name) = self.current() { let name = name.clone(); @@ -2484,6 +2492,19 @@ mod tests { ); } + /// Verifies function calls preserve spread arguments in source order. + #[test] + fn parse_fragment_accepts_spread_call_argument_source() { + let program = parse_fragment(br#"return add(...$args);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "add".to_string(), + args: vec![EvalCallArg::spread(EvalExpr::LoadVar("args".to_string()))], + }))] + ); + } + /// Verifies `isset` parses as a case-insensitive function-like expression. #[test] fn parse_fragment_accepts_isset_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 0847b35af7..7c2604fc05 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,6 +19,8 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional or named arguments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +Argument unpacking (`...`) is supported for eval-declared function calls inside eval when the unpacked value is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and unpacking after a named argument is rejected before the unpacked expression is evaluated. + Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 69598433e6..618b8cb976 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,6 +34,8 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional or named arguments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +Eval-declared function calls inside eval also support PHP argument unpacking (`...`) for array values; string keys bind as named parameters. Builtin calls, object method calls, AOT fallback calls, and post-barrier native calls still accept simple positional arguments. + Eval builtin dispatch also supports `strrev()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 61328a116f..1fdcb3c4c8 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -54,6 +54,7 @@ public function echoLabelThroughEval(): void { eval('function plus_one($value) { return $value + 1; }'); $dynamic_call = eval('return plus_one(4);'); $dynamic_named = eval('function named_pair($left, $right) { return $left . ":" . $right; } return named_pair(right: "R", left: "L");'); +$dynamic_spread = eval('function spread_pair($left, $right) { return $left . ":" . $right; } return spread_pair(...["L", "R"]);'); $dynamic_cuf = eval('return call_user_func("plus_one", 6);'); $dynamic_cufa = eval('return call_user_func_array("plus_one", [8]);'); $eval_native_call = eval('return compiled_add(2, 8);'); @@ -114,6 +115,7 @@ public function echoLabelThroughEval(): void { echo "meta-count=" . $meta_count . "\n"; echo "dynamic-call=" . $dynamic_call . "\n"; echo "dynamic-named=" . $dynamic_named . "\n"; +echo "dynamic-spread=" . $dynamic_spread . "\n"; echo "dynamic-cuf=" . $dynamic_cuf . "\n"; echo "dynamic-cufa=" . $dynamic_cufa . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 854a291ede..b28fc02501 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1131,6 +1131,17 @@ echo eval('function dyn_eval_named($x, $y) { return ($x * 10) + $y; } return dyn assert_eq!(out, "12"); } +/// Verifies eval-declared functions unpack spread arguments inside eval fragments. +#[test] +fn test_eval_declared_function_accepts_spread_args_in_fragment() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 01:11:30 +0200 Subject: [PATCH 0079/1208] Support eval native named spread calls --- crates/elephc-eval/src/context.rs | 41 +++++++++++-- crates/elephc-eval/src/interpreter.rs | 61 +++++++++++++++++-- crates/elephc-eval/src/lib.rs | 81 ++++++++++++++++++++++++- docs/internals/the-runtime.md | 6 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 4 ++ src/codegen/lower_inst/builtins/eval.rs | 53 ++++++++++++++++ tests/codegen/eval.rs | 24 ++++++++ 8 files changed, 256 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 1fcc168a41..d59de09bad 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -23,16 +23,17 @@ pub type NativeFunctionInvoker = unsafe extern "C" fn(*mut c_void, *mut RuntimeCell) -> *mut RuntimeCell; /// Native AOT function callback metadata visible to runtime eval fragments. -#[derive(Clone, Copy)] +#[derive(Clone)] pub struct NativeFunction { descriptor: *mut c_void, invoker: NativeFunctionInvoker, param_count: usize, + param_names: Vec, } impl NativeFunction { /// Creates callback metadata for a descriptor-compatible AOT function. - pub const fn new( + pub fn new( descriptor: *mut c_void, invoker: NativeFunctionInvoker, param_count: usize, @@ -41,20 +42,38 @@ impl NativeFunction { descriptor, invoker, param_count, + param_names: Vec::new(), } } /// Returns the visible positional parameter count accepted by this callback. - pub const fn param_count(self) -> usize { + pub const fn param_count(&self) -> usize { self.param_count } + /// Records the PHP parameter name for one positional callback slot. + pub fn set_param_name(&mut self, index: usize, name: impl Into) -> bool { + if index >= self.param_count { + return false; + } + if self.param_names.len() < self.param_count { + self.param_names.resize(self.param_count, String::new()); + } + self.param_names[index] = name.into(); + true + } + + /// Returns the PHP-visible parameter names registered for this callback. + pub fn param_names(&self) -> &[String] { + &self.param_names + } + /// Invokes the descriptor-compatible callback with a boxed Mixed arg array. /// /// # Safety /// `arg_array` must be a boxed Mixed indexed array whose elements are boxed /// Mixed cells following the descriptor-invoker ABI. - pub unsafe fn call(self, arg_array: RuntimeCellHandle) -> RuntimeCellHandle { + pub unsafe fn call(&self, arg_array: RuntimeCellHandle) -> RuntimeCellHandle { RuntimeCellHandle::from_raw((self.invoker)(self.descriptor, arg_array.as_ptr())) } } @@ -142,7 +161,19 @@ impl ElephcEvalContext { /// Returns a native AOT function callback by its lowercase PHP function name. pub fn native_function(&self, name: &str) -> Option { - self.native_functions.get(name).copied() + self.native_functions.get(name).cloned() + } + + /// Records one parameter name for an already registered native AOT callback. + pub fn define_native_function_param( + &mut self, + function_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_param_name(index, param_name)) } /// Returns true when the context has a dynamic or native function with this lowercase PHP name. diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 432e939d35..b21300b39b 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -2142,19 +2142,18 @@ fn eval_dynamic_function( values: &mut impl RuntimeValueOps, ) -> Result { let evaluated_args = - eval_dynamic_function_call_args(function, args, context, caller_scope, values)?; + eval_function_call_args(function.params(), args, context, caller_scope, values)?; eval_dynamic_function_with_values(function, evaluated_args, context, values) } -/// Evaluates and binds eval-declared function arguments to parameter order. -fn eval_dynamic_function_call_args( - function: &EvalFunction, +/// Evaluates and binds function-like arguments to parameter order. +fn eval_function_call_args( + params: &[String], args: &[EvalCallArg], context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let params = function.params(); let mut bound_args = vec![None; params.len()]; let mut next_positional = 0; let mut saw_named = false; @@ -2271,7 +2270,11 @@ fn eval_native_function( caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = eval_positional_call_arg_values(args, context, caller_scope, values)?; + let evaluated_args = if function.param_names().len() == function.param_count() { + eval_function_call_args(function.param_names(), args, context, caller_scope, values)? + } else { + eval_positional_call_arg_values(args, context, caller_scope, values)? + }; eval_native_function_with_values(function, evaluated_args, values) } @@ -4732,6 +4735,52 @@ echo function_exists("missing_probe") . "x";"#, assert_eq!(result, expected); } + /// Verifies direct eval calls can bind registered native parameters by name. + #[test] + fn execute_program_calls_registered_native_function_with_named_args() { + let program = parse_fragment(br#"return native_answer(right: 2, left: 1);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + + /// Verifies direct eval calls can unpack arrays into registered native parameters. + #[test] + fn execute_program_calls_registered_native_function_with_spread_args() { + let program = + parse_fragment(br#"return native_answer(...[1, 2]);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + /// Verifies indexed array writes mutate an existing scope array. #[test] fn execute_program_writes_indexed_scope_array() { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index bdaa5ddb48..42445f1aaa 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -262,6 +262,33 @@ pub unsafe extern "C" fn __elephc_eval_register_native_function( .unwrap_or(0) } +/// Registers one generated native PHP function parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + /// Calls a zero-argument function previously declared through `eval()`. /// /// # Safety @@ -361,6 +388,41 @@ unsafe fn register_native_function_inner( ) } +/// Runs the native parameter-name registration ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param`; invalid handles, +/// names, or indexes fail closed as `false`. +unsafe fn register_native_function_param_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_function_param( + &function_name.to_ascii_lowercase(), + param_index, + param_name, + )) +} + /// Runs the call-site metadata setter ABI body after installing a panic boundary. /// /// # Safety @@ -696,11 +758,12 @@ mod tests { assert_eq!(missing_result, 0); } - /// Verifies native AOT registration makes functions visible through the ABI probe. + /// Verifies native AOT registration records function and parameter metadata. #[test] fn register_native_function_reports_function_exists() { let mut ctx = ElephcEvalContext::new(); let name = b"NATIVE_PROBE"; + let param = b"value"; let descriptor = 1usize as *mut c_void; let registered = unsafe { @@ -710,13 +773,29 @@ mod tests { name.len() as u64, descriptor, Some(fake_native_invoker), + 1, + ) + }; + let param_registered = unsafe { + __elephc_eval_register_native_function_param( + &mut ctx, + name.as_ptr(), + name.len() as u64, 0, + param.as_ptr(), + param.len() as u64, ) }; let exists = unsafe { __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) }; assert_eq!(registered, 1); + let native = ctx + .native_function("native_probe") + .expect("native function should be registered"); + + assert_eq!(param_registered, 1); assert_eq!(exists, 1); + assert_eq!(native.param_names(), &["value".to_string()]); } /// Verifies scope allocation returns an empty opaque activation scope handle. diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 7c2604fc05..e7b8097674 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,11 +15,11 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Eval-declared function calls inside eval can bind positional or named arguments; native callbacks and post-barrier dispatch currently accept simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier dispatch currently accepts simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional or named arguments that share the same generated context, AOT global user functions callable from eval fragments with simple positional arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Native fallback dispatch for named arguments, spread arguments, dynamic class declarations, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Argument unpacking (`...`) is supported for eval-declared function calls inside eval when the unpacked value is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and unpacking after a named argument is rejected before the unpacked expression is evaluated. +Argument unpacking (`...`) is supported for eval-declared and registered AOT function calls inside eval when the unpacked value is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 618b8cb976..5740eec53f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional or named arguments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval-declared function calls inside eval also support PHP argument unpacking (`...`) for array values; string keys bind as named parameters. Builtin calls, object method calls, AOT fallback calls, and post-barrier native calls still accept simple positional arguments. +Eval-declared function calls and registered AOT global user-function calls inside eval also support PHP argument unpacking (`...`) for array values; string keys bind as named parameters. Builtin calls, object method calls, and post-barrier native calls still accept simple positional arguments. Eval builtin dispatch also supports `strrev()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. diff --git a/examples/eval/main.php b/examples/eval/main.php index 1fdcb3c4c8..f90fbea328 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -58,6 +58,8 @@ public function echoLabelThroughEval(): void { $dynamic_cuf = eval('return call_user_func("plus_one", 6);'); $dynamic_cufa = eval('return call_user_func_array("plus_one", [8]);'); $eval_native_call = eval('return compiled_add(2, 8);'); +$eval_native_named = eval('return compiled_add(right: 8, left: 2);'); +$eval_native_spread = eval('return compiled_add(...[2, 8]);'); $logic = eval('return true || missing_eval_rhs();'); $keyword_logic = eval('return true xor false;'); $not_false = eval('return !false;'); @@ -119,6 +121,8 @@ public function echoLabelThroughEval(): void { echo "dynamic-cuf=" . $dynamic_cuf . "\n"; echo "dynamic-cufa=" . $dynamic_cufa . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; +echo "eval-native-named=" . $eval_native_named . "\n"; +echo "eval-native-spread=" . $eval_native_spread . "\n"; echo "logic=" . $logic . "\n"; echo "keyword-logic=" . $keyword_logic . "\n"; echo "not-false=" . $not_false . "\n"; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index d42caf7c83..3d9caf5040 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -359,9 +359,62 @@ fn register_eval_native_function( .target .extern_symbol("__elephc_eval_register_native_function"); abi::emit_call_label(ctx.emitter, &symbol); + for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { + register_eval_native_function_param( + ctx, + context_offset, + &name_label, + name_len, + index, + param_name, + ); + } Ok(()) } +/// Emits one native-function parameter-name registration call. +fn register_eval_native_function_param( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + param_index: usize, + param_name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + ¶m_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + param_name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Loads the persistent eval context local into the selected integer argument register. fn load_eval_context_local_to_arg( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b28fc02501..0d885203cc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1337,6 +1337,30 @@ eval('echo native_eval_add(4, 6); echo ":"; echo function_exists("native_eval_ad assert_eq!(out, "10:1"); } +/// Verifies eval fragments bind AOT user function parameters by name. +#[test] +fn test_eval_fragment_can_call_native_user_function_with_named_args() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 01:18:24 +0200 Subject: [PATCH 0080/1208] Support eval call_user_func_array named args --- crates/elephc-eval/src/interpreter.rs | 207 ++++++++++++++++++++++---- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 24 +++ 5 files changed, 205 insertions(+), 32 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b21300b39b..7f31526901 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -29,6 +29,12 @@ enum EvalControl { Continue, } +/// One already evaluated function-like call argument. +struct EvaluatedCallArg { + name: Option, + value: RuntimeCellHandle, +} + /// Runtime value hooks required by the EvalIR interpreter. pub trait RuntimeValueOps { /// Creates a runtime indexed-array cell with room for at least `capacity` elements. @@ -1089,13 +1095,11 @@ fn eval_call_user_func_array_with_values( if callback.contains("::") { return Err(EvalStatus::UnsupportedConstruct); } - let len = values.array_len(arg_array)?; - let mut evaluated_args = Vec::with_capacity(len); - for index in 0..len { - let index = values.int(index as i64)?; - evaluated_args.push(values.array_get(arg_array, index)?); + if !values.is_array_like(arg_array)? { + return Err(EvalStatus::RuntimeFatal); } - eval_callable_with_values(&callback, evaluated_args, context, values) + let evaluated_args = eval_array_call_arg_values(arg_array, values)?; + eval_callable_with_call_array_args(&callback, evaluated_args, context, values) } /// Dispatches `call_user_func` after its callback and arguments are already evaluated. @@ -1135,6 +1139,34 @@ fn eval_callable_with_values( Err(EvalStatus::UnsupportedConstruct) } +/// Invokes a callable with arguments that may carry `call_user_func_array` names. +fn eval_callable_with_call_array_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.iter().all(|arg| arg.name.is_none()) { + let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); + return eval_callable_with_values(name, evaluated_args, context, values); + } + if eval_php_visible_builtin_exists(name) { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(function) = context.function(name).cloned() { + let evaluated_args = bind_evaluated_function_args(function.params(), evaluated_args)?; + return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + } + if let Some(function) = context.native_function(name) { + if function.param_names().len() != function.param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = bind_evaluated_function_args(function.param_names(), evaluated_args)?; + return eval_native_function_with_values(function, evaluated_args, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + /// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. fn eval_builtin_with_values( name: &str, @@ -2154,8 +2186,18 @@ fn eval_function_call_args( caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let mut bound_args = vec![None; params.len()]; - let mut next_positional = 0; + let evaluated_args = eval_call_arg_values(args, context, caller_scope, values)?; + bind_evaluated_function_args(params, evaluated_args) +} + +/// Evaluates source-order call arguments while preserving named-argument metadata. +fn eval_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut evaluated_args = Vec::with_capacity(args.len()); let mut saw_named = false; for arg in args { @@ -2167,33 +2209,17 @@ fn eval_function_call_args( if !values.is_array_like(spread)? { return Err(EvalStatus::RuntimeFatal); } - let len = values.array_len(spread)?; - for position in 0..len { - let key = values.array_iter_key(spread, position)?; - let value = values.array_get(spread, key)?; - match values.type_tag(key)? { - EVAL_TAG_INT => { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, value)?; - } - EVAL_TAG_STRING => { - saw_named = true; - let name = values.string_bytes(key)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - bind_dynamic_named_arg(params, &mut bound_args, &name, value)?; - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } + append_unpacked_call_arg_values(spread, &mut evaluated_args, &mut saw_named, values)?; continue; } if let Some(name) = arg.name() { saw_named = true; let value = eval_expr(arg.value(), context, caller_scope, values)?; - bind_dynamic_named_arg(params, &mut bound_args, name, value)?; + evaluated_args.push(EvaluatedCallArg { + name: Some(name.to_string()), + value, + }); continue; } @@ -2201,7 +2227,71 @@ fn eval_function_call_args( return Err(EvalStatus::RuntimeFatal); } let value = eval_expr(arg.value(), context, caller_scope, values)?; - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, value)?; + evaluated_args.push(EvaluatedCallArg { name: None, value }); + } + + Ok(evaluated_args) +} + +/// Converts a `call_user_func_array` argument array into ordered call arguments. +fn eval_array_call_arg_values( + arg_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(arg_array)?; + let mut evaluated_args = Vec::with_capacity(len); + let mut saw_named = false; + append_unpacked_call_arg_values(arg_array, &mut evaluated_args, &mut saw_named, values)?; + Ok(evaluated_args) +} + +/// Appends one unpacked array's values using PHP named-argument key semantics. +fn append_unpacked_call_arg_values( + array: RuntimeCellHandle, + evaluated_args: &mut Vec, + saw_named: &mut bool, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + match values.type_tag(key)? { + EVAL_TAG_INT => { + if *saw_named { + return Err(EvalStatus::RuntimeFatal); + } + evaluated_args.push(EvaluatedCallArg { name: None, value }); + } + EVAL_TAG_STRING => { + *saw_named = true; + let name = values.string_bytes(key)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + evaluated_args.push(EvaluatedCallArg { + name: Some(name), + value, + }); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(()) +} + +/// Binds evaluated positional and named values to declared parameter order. +fn bind_evaluated_function_args( + params: &[String], + evaluated_args: Vec, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_dynamic_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } } bound_args @@ -4012,6 +4102,38 @@ return call_user_func_array("dyn", [4, 5]);"#, assert_eq!(values.get(result), FakeValue::Int(9)); } + /// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. + #[test] + fn execute_program_call_user_func_array_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } +return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies `call_user_func_array` rejects positional values after named keys. + #[test] + fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", ["y" => 2, 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + } + /// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. #[test] fn execute_program_call_user_func_array_dispatches_builtin() { @@ -4046,6 +4168,31 @@ return call_user_func_array("dyn", [4, 5]);"#, assert_eq!(result, expected); } + /// Verifies `call_user_func_array` named keys can bind registered native parameters. + #[test] + fn execute_program_call_user_func_array_binds_registered_native_named_args() { + let program = parse_fragment( + br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + /// Verifies duplicate eval-declared function names fail in a shared context. #[test] fn execute_program_rejects_duplicate_declared_function() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e7b8097674..db85aa6242 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Argument unpacking (`...`) is supported for eval-declared and registered AOT function calls inside eval when the unpacked value is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and unpacking after a named argument is rejected before the unpacked expression is evaluated. +Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared and registered AOT function calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 5740eec53f..60968340cf 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional or named arguments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval-declared function calls and registered AOT global user-function calls inside eval also support PHP argument unpacking (`...`) for array values; string keys bind as named parameters. Builtin calls, object method calls, and post-barrier native calls still accept simple positional arguments. +Eval-declared function calls and registered AOT global user-function calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Builtin calls, object method calls, and post-barrier native calls still accept simple positional arguments. Eval builtin dispatch also supports `strrev()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. diff --git a/examples/eval/main.php b/examples/eval/main.php index f90fbea328..ab4ed4e688 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -60,6 +60,7 @@ public function echoLabelThroughEval(): void { $eval_native_call = eval('return compiled_add(2, 8);'); $eval_native_named = eval('return compiled_add(right: 8, left: 2);'); $eval_native_spread = eval('return compiled_add(...[2, 8]);'); +$eval_native_cufa_named = eval('return call_user_func_array("compiled_add", ["right" => 8, "left" => 2]);'); $logic = eval('return true || missing_eval_rhs();'); $keyword_logic = eval('return true xor false;'); $not_false = eval('return !false;'); @@ -123,6 +124,7 @@ public function echoLabelThroughEval(): void { echo "eval-native-call=" . $eval_native_call . "\n"; echo "eval-native-named=" . $eval_native_named . "\n"; echo "eval-native-spread=" . $eval_native_spread . "\n"; +echo "eval-native-cufa-named=" . $eval_native_cufa_named . "\n"; echo "logic=" . $logic . "\n"; echo "keyword-logic=" . $keyword_logic . "\n"; echo "not-false=" . $not_false . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0d885203cc..e962b3fba3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1300,6 +1300,18 @@ echo call_user_func_array("dyn_eval_inner_cufa", [4, 5]);'); assert_eq!(out, "9"); } +/// Verifies `call_user_func_array()` inside eval binds eval-declared named arguments. +#[test] +fn test_eval_fragment_call_user_func_array_binds_eval_declared_named_args() { + let out = compile_and_run( + r#" 2, "x" => 1]);'); +"#, + ); + assert_eq!(out, "12"); +} + /// Verifies `call_user_func_array()` inside eval dispatches to supported builtins. #[test] fn test_eval_fragment_call_user_func_array_dispatches_builtin() { @@ -1325,6 +1337,18 @@ eval('echo call_user_func_array("native_eval_cufa_add", [4, 6]);'); assert_eq!(out, "10"); } +/// Verifies `call_user_func_array()` inside eval binds registered AOT named arguments. +#[test] +fn test_eval_fragment_call_user_func_array_binds_native_user_function_named_args() { + let out = compile_and_run( + r#" "R", "left" => "L"]);'); +"#, + ); + assert_eq!(out, "L:R"); +} + /// Verifies eval fragments can call AOT user functions registered in the eval context. #[test] fn test_eval_fragment_can_call_native_user_function() { From dd75b22788b80e5370f16d561926f7d9ba057eb5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 01:26:24 +0200 Subject: [PATCH 0081/1208] Support eval associative array append --- crates/elephc-eval/src/interpreter.rs | 77 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 2 + examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 ++++++ 5 files changed, 94 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7f31526901..ba56aea9e4 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -346,7 +346,8 @@ fn execute_stmt( scope.entry(name).filter(|entry| entry.flags().is_visible()) { if values.is_array_like(existing.cell())? { - if values.type_tag(existing.cell())? != EVAL_TAG_ARRAY { + let tag = values.type_tag(existing.cell())?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { return Err(EvalStatus::UnsupportedConstruct); } ownership = existing.flags().ownership; @@ -357,9 +358,7 @@ fn execute_stmt( } else { values.array_new(1)? }; - let index = values.array_len(array)?; - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let index = values.int(index)?; + let index = eval_array_append_key(array, values)?; let value = eval_expr(value, context, scope, values)?; let array = values.array_set(array, index, value)?; if let Some(replaced) = scope.set(name.clone(), array, ownership) { @@ -631,6 +630,33 @@ fn execute_foreach_stmt( Ok(EvalControl::None) } +/// Returns PHP's next automatic integer key for `$array[]` append writes. +fn eval_array_append_key( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut next_key = None; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + continue; + } + let one = values.int(1)?; + let candidate = values.add(key, one)?; + let replace = if let Some(current) = next_key { + let is_greater = values.compare(EvalBinOp::Gt, candidate, current)?; + values.truthy(is_greater)? + } else { + true + }; + if replace { + next_key = Some(candidate); + } + } + next_key.map_or_else(|| values.int(0), Ok) +} + /// Evaluates one expression to an opaque runtime-cell handle. fn eval_expr( expr: &EvalExpr, @@ -4954,6 +4980,49 @@ echo function_exists("missing_probe") . "x";"#, assert_eq!(values.get(result), FakeValue::String("b".to_string())); } + /// Verifies associative append starts at key zero when only string keys exist. + #[test] + fn execute_program_appends_assoc_scope_array_with_string_keys() { + let program = + parse_fragment(br#"$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); + } + + /// Verifies associative append uses one plus the largest existing integer key. + #[test] + fn execute_program_appends_assoc_scope_array_after_positive_int_key() { + let program = parse_fragment( + br#"$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies associative append preserves PHP's largest-negative-key behavior. + #[test] + fn execute_program_appends_assoc_scope_array_after_negative_int_key() { + let program = + parse_fragment(br#"$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + /// Verifies mutating a borrowed scope array does not make the eval scope own it. #[test] fn execute_program_preserves_borrowed_array_ownership() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index db85aa6242..41dab93956 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -21,6 +21,8 @@ The bridge crate parses the fragment at runtime into a small EvalIR and interpre Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared and registered AOT function calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. +Eval `$array[] = value` append writes operate on indexed arrays and associative hashes. Hash appends use PHP's next automatic integer key rule by scanning existing integer keys through the eval value hooks before delegating to the shared mixed-array setter. + Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 60968340cf..7723c84d56 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -36,6 +36,8 @@ The evaluated string must be a PHP fragment without an opening ` 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); $append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); +$append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); @@ -165,6 +166,7 @@ public function echoLabelThroughEval(): void { echo "aggregates=" . $aggregates . "\n"; echo "array-projection=" . $array_projection . "\n"; echo "append-items=" . $append_items . "\n"; +echo "append-assoc=" . $append_assoc . "\n"; echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e962b3fba3..09ba88e1cf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -556,6 +556,21 @@ echo ":" . $existing[1] . ":" . count($existing); assert_eq!(out, "a:b:2:y:2"); } +/// Verifies eval associative-array append uses PHP's next automatic integer key. +#[test] +fn test_eval_assoc_array_append_uses_php_next_key() { + let out = compile_and_run( + r#" "Ada"]; $items[] = "Grace"; return $items[0];'); +echo ":"; +echo eval('$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];'); +echo ":"; +echo eval('$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];'); +"#, + ); + assert_eq!(out, "Grace:tail:tail"); +} + /// Verifies eval can read a native Mixed array through runtime array helpers. #[test] fn test_eval_reads_native_mixed_array() { From d30696da89c1b4d61db24f0dd457c61d28b5d462 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 01:33:17 +0200 Subject: [PATCH 0082/1208] Support eval builtin named spread calls --- crates/elephc-eval/src/interpreter.rs | 143 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 14 +++ 5 files changed, 157 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ba56aea9e4..5b686de7a6 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -839,10 +839,17 @@ fn eval_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - if eval_positional_expr_call_name(name) { + if eval_expr_language_construct_name(name) { let args = positional_call_arg_exprs(args)?; return eval_positional_expr_call(name, &args, context, scope, values); } + if eval_php_visible_builtin_exists(name) { + if eval_call_args_are_plain_positional(args) { + let args = positional_call_arg_exprs(args)?; + return eval_positional_expr_call(name, &args, context, scope, values); + } + return eval_builtin_call(name, args, context, scope, values); + } if let Some(function) = context.function(name).cloned() { return eval_dynamic_function(&function, args, context, scope, values); @@ -853,9 +860,15 @@ fn eval_call( Err(EvalStatus::UnsupportedConstruct) } -/// Returns true for eval calls that still accept only positional expressions. -fn eval_positional_expr_call_name(name: &str) -> bool { - matches!(name, "empty" | "eval" | "isset") || eval_php_visible_builtin_exists(name) +/// Returns true for language constructs that need unevaluated argument expressions. +fn eval_expr_language_construct_name(name: &str) -> bool { + matches!(name, "empty" | "eval" | "isset") +} + +/// Returns true when every source argument is plain positional. +fn eval_call_args_are_plain_positional(args: &[EvalCallArg]) -> bool { + args.iter() + .all(|arg| arg.name().is_none() && !arg.is_spread()) } /// Evaluates builtins and language constructs after positional-only argument validation. @@ -1076,6 +1089,109 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { ) } +/// Evaluates a direct PHP-visible builtin call with named or spread arguments. +fn eval_builtin_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + Ok(result) +} + +/// Binds evaluated builtin arguments to PHP parameter order when names are used. +fn bind_evaluated_builtin_args( + name: &str, + evaluated_args: Vec, +) -> Result, EvalStatus> { + if evaluated_args.iter().all(|arg| arg.name.is_none()) { + return Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()); + } + + let params = eval_builtin_param_names(name).ok_or(EvalStatus::RuntimeFatal)?; + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_builtin_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + collect_contiguous_bound_args(bound_args) +} + +/// Binds one named builtin-call value to the matching PHP parameter slot. +fn bind_builtin_named_arg( + params: &[&str], + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + let Some(param_index) = params.iter().position(|param| *param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + Ok(()) +} + +/// Collects ordered bound arguments, rejecting gaps where defaults would be needed. +fn collect_contiguous_bound_args( + bound_args: Vec>, +) -> Result, EvalStatus> { + let Some(last_index) = bound_args.iter().rposition(Option::is_some) else { + return Ok(Vec::new()); + }; + bound_args + .into_iter() + .take(last_index + 1) + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns PHP parameter names for builtin calls implemented by eval. +fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { + match name { + "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), + "array_keys" | "array_product" | "array_sum" | "array_values" => Some(&["array"]), + "array_key_exists" => Some(&["key", "array"]), + "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), + "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" + | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" + | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), + "call_user_func" => Some(&["callback"]), + "call_user_func_array" => Some(&["callback", "args"]), + "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), + "count" => Some(&["value", "mode"]), + "fdiv" | "fmod" => Some(&["num1", "num2"]), + "function_exists" => Some(&["function"]), + "hash_equals" => Some(&["known_string", "user_string"]), + "max" | "min" => Some(&["value"]), + "ord" => Some(&["character"]), + "pi" => Some(&[]), + "pow" => Some(&["num", "exponent"]), + "round" => Some(&["num", "precision"]), + "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), + "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), + "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), + "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { + Some(&["string"]) + } + _ => None, + } +} + /// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. fn eval_builtin_call_user_func( args: &[EvalExpr], @@ -4249,6 +4365,25 @@ return call_user_func_array("dyn", ["y" => 2, 1]);"#, assert_eq!(values.get(result), FakeValue::Int(6)); } + /// Verifies direct eval builtin calls bind named and unpacked arguments. + #[test] + fn execute_program_dispatches_named_and_spread_builtins() { + let program = parse_fragment( + br#"echo strlen(string: "abcd"); +echo ":" . (array_key_exists(array: ["name" => 1], key: "name") ? "Y" : "N"); +echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N"); +return round(precision: 1, num: 3.14);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:Y:Y"); + assert_eq!(values.get(result), FakeValue::Float(3.1)); + } + /// Verifies eval `ord()` returns the first byte and supports callable dispatch. #[test] fn execute_program_dispatches_ord_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 41dab93956..bf7355887b 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared and registered AOT function calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. +Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval `$array[] = value` append writes operate on indexed arrays and associative hashes. Hash appends use PHP's next automatic integer key rule by scanning existing integer keys through the eval value hooks before delegating to the shared mixed-array setter. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7723c84d56..fba3cf7f53 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional or named arguments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval-declared function calls and registered AOT global user-function calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Builtin calls, object method calls, and post-barrier native calls still accept simple positional arguments. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, object method calls, and post-barrier native calls still accept simple positional arguments. Eval `$array[] = value` append writes work for indexed and associative arrays. Associative appends use PHP's next automatic integer key rule. diff --git a/examples/eval/main.php b/examples/eval/main.php index adcfdc1b04..2688beb7c3 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -103,6 +103,7 @@ public function echoLabelThroughEval(): void { $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); +$named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); $append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); $append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); @@ -164,6 +165,7 @@ public function echoLabelThroughEval(): void { echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; +echo "named-builtins=" . $named_builtins . "\n"; echo "array-projection=" . $array_projection . "\n"; echo "append-items=" . $append_items . "\n"; echo "append-assoc=" . $append_assoc . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 09ba88e1cf..a956d5146c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -658,6 +658,20 @@ eval('echo STRLEN("abcd") . ":" . count([1, 2, 3]);'); assert_eq!(out, "4:3"); } +/// Verifies eval direct builtin calls bind named arguments and spread arrays. +#[test] +fn test_eval_dispatches_named_and_spread_builtin_calls() { + let out = compile_and_run( + r#" 1], key: "name") ? "Y" : "N"); +echo ":" . round(precision: 1, num: 3.14); +echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N");'); +"#, + ); + assert_eq!(out, "4:Y:3.1:Y"); +} + /// Verifies eval `ord()` returns the first byte and dispatches dynamically. #[test] fn test_eval_dispatches_ord_builtin_call() { From 45a1d65bc2253aa477167f232b3dba419e5c40a7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 01:38:48 +0200 Subject: [PATCH 0083/1208] Support eval method spread calls --- crates/elephc-eval/src/interpreter.rs | 34 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 5 ++++ tests/codegen/eval.rs | 24 +++++++++++++++++++ 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5b686de7a6..9dd80d0f89 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -690,7 +690,7 @@ fn eval_expr( args, } => { let object = eval_expr(object, context, scope, values)?; - let evaluated_args = eval_positional_call_arg_values(args, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; values.method_call(object, method, evaluated_args) } EvalExpr::NullCoalesce { value, default } => { @@ -831,6 +831,20 @@ fn eval_positional_call_arg_values( Ok(evaluated_args) } +/// Evaluates method-call arguments, allowing numeric spread but not named args. +fn eval_method_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + if evaluated_args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()) +} + /// Evaluates supported function-like calls from a runtime eval fragment. fn eval_call( name: &str, @@ -3586,6 +3600,24 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::Int(18)); } + /// Verifies eval method calls forward numerically unpacked arguments. + #[test] + fn execute_program_calls_object_method_with_spread_arguments() { + let program = + parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let mut properties = HashMap::new(); + properties.insert("x".to_string(), x); + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); + } + /// Verifies if/else executes only the PHP-truthy branch. #[test] fn execute_program_if_else_uses_php_truthiness() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index bf7355887b..48c2acd9b2 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. +Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval `$array[] = value` append writes operate on indexed arrays and associative hashes. Hash appends use PHP's next automatic integer key rule by scanning existing integer keys through the eval value hooks before delegating to the shared mixed-array setter. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index fba3cf7f53..63d5fc8bbf 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional or named arguments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, object method calls, and post-barrier native calls still accept simple positional arguments. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier native calls, still accept simple positional arguments. Eval `$array[] = value` append writes work for indexed and associative arrays. Associative appends use PHP's next automatic integer key rule. diff --git a/examples/eval/main.php b/examples/eval/main.php index 2688beb7c3..7e5519d6ef 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -31,6 +31,10 @@ public function echoAddThroughEval(): void { public function echoLabelThroughEval(): void { echo "eval-this-method-two-args=" . eval('return $this->label(5, "!");') . "\n"; } + + public function echoLabelSpreadThroughEval(): void { + echo "eval-this-method-spread=" . eval('return $this->label(...[5, "?"]);') . "\n"; + } } $x = 1; @@ -178,6 +182,7 @@ public function echoLabelThroughEval(): void { $counter->echoReadThroughEval(); $counter->echoAddThroughEval(); $counter->echoLabelThroughEval(); +$counter->echoLabelSpreadThroughEval(); echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a956d5146c..80a9b56cf5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1527,6 +1527,30 @@ $box->run(); assert_eq!(out, "50!"); } +/// Verifies eval fragments can unpack numeric arrays into public AOT method calls. +#[test] +fn test_eval_fragment_can_call_this_public_method_with_spread_args() { + let out = compile_and_run( + r#"x + $amount) . $suffix; + } + + public function run(): void { + echo eval('return $this->label(...[9, "!"]);'); + } +} + +$box = new EvalMethodSpreadBox(); +$box->run(); +"#, + ); + assert_eq!(out, "50!"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From 06a394c66f60bc4fd4d3b001efae1644cafd0dae Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 01:43:27 +0200 Subject: [PATCH 0084/1208] Support eval assoc literal auto keys --- crates/elephc-eval/src/interpreter.rs | 74 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 ++++++ 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 9dd80d0f89..29a6b4833d 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -2573,16 +2573,21 @@ fn eval_assoc_array( values: &mut impl RuntimeValueOps, ) -> Result { let array = values.assoc_new(elements.len())?; - let mut next_index = 0; + let mut next_key = None; for element in elements { let (key, value) = match element { EvalArrayElement::Value(value) => { - let key = values.int(next_index)?; - next_index += 1; + let key = match next_key { + Some(next_key) => next_key, + None => values.int(0)?, + }; + let one = values.int(1)?; + next_key = Some(values.add(key, one)?); (key, value) } EvalArrayElement::KeyValue { key, value } => { let key = eval_expr(key, context, scope, values)?; + next_key = eval_array_next_key_after_explicit_key(key, next_key, values)?; (key, value) } }; @@ -2592,6 +2597,30 @@ fn eval_assoc_array( Ok(array) } +/// Advances an array literal's automatic key after an explicit PHP integer key. +fn eval_array_next_key_after_explicit_key( + key: RuntimeCellHandle, + current_next_key: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(current_next_key); + } + let one = values.int(1)?; + let candidate = values.add(key, one)?; + let replace = if let Some(current_next_key) = current_next_key { + let is_greater = values.compare(EvalBinOp::Gt, candidate, current_next_key)?; + values.truthy(is_greater)? + } else { + true + }; + Ok(if replace { + Some(candidate) + } else { + current_next_key + }) +} + /// Converts one EvalIR constant into a runtime-cell handle. fn eval_const( value: &EvalConst, @@ -4024,6 +4053,45 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); } + /// Verifies unkeyed assoc literal entries start at zero after string keys. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_string_key_starts_at_zero() { + let program = parse_fragment(br#"return ["name" => "Ada", "Grace"][0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); + } + + /// Verifies unkeyed assoc literal entries use one plus the largest integer key. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_positive_int_key() { + let program = + parse_fragment(br#"return [2 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies unkeyed assoc literal entries preserve PHP's negative-key rule. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_negative_int_key() { + let program = + parse_fragment(br#"return [-2 => "minus", "tail"][-1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + /// Verifies nested eval calls parse and execute against the same dynamic scope. #[test] fn execute_program_nested_eval_uses_same_scope() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 48c2acd9b2..0cfb690780 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -21,7 +21,7 @@ The bridge crate parses the fragment at runtime into a small EvalIR and interpre Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. -Eval `$array[] = value` append writes operate on indexed arrays and associative hashes. Hash appends use PHP's next automatic integer key rule by scanning existing integer keys through the eval value hooks before delegating to the shared mixed-array setter. +Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit integer keys are encountered. Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 63d5fc8bbf..b9efa5a11d 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -36,7 +36,7 @@ The evaluated string must be a PHP fragment without an opening ` "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); +$mixed_literal = eval('return [2 => "two", "tail"][3];'); $append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); $append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); @@ -171,6 +172,7 @@ public function echoLabelSpreadThroughEval(): void { echo "aggregates=" . $aggregates . "\n"; echo "named-builtins=" . $named_builtins . "\n"; echo "array-projection=" . $array_projection . "\n"; +echo "mixed-literal=" . $mixed_literal . "\n"; echo "append-items=" . $append_items . "\n"; echo "append-assoc=" . $append_assoc . "\n"; echo "array-key-exists=" . $array_key_probe . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 80a9b56cf5..a06672626a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -615,6 +615,21 @@ fn test_eval_assoc_array_literal_and_string_key_read() { assert_eq!(out, "Ada"); } +/// Verifies eval associative-array literals use PHP's next automatic key. +#[test] +fn test_eval_assoc_array_literal_unkeyed_entries_use_next_key() { + let out = compile_and_run( + r#" "Ada", "Grace"][0];'); +echo ":"; +echo eval('return [2 => "two", "tail"][3];'); +echo ":"; +echo eval('return [-2 => "minus", "tail"][-1];'); +"#, + ); + assert_eq!(out, "Grace:tail:tail"); +} + /// Verifies eval-created associative arrays remain visible to native code. #[test] fn test_eval_created_assoc_array_is_visible_after_eval() { From 61f91e331c39ebcde709055dfc4bf1c319295b33 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 01:47:52 +0200 Subject: [PATCH 0085/1208] Normalize eval assoc literal string keys --- crates/elephc-eval/src/interpreter.rs | 90 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 6 +- 5 files changed, 95 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 29a6b4833d..5904eb0b15 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -2603,9 +2603,17 @@ fn eval_array_next_key_after_explicit_key( current_next_key: Option, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(current_next_key); - } + let key = match values.type_tag(key)? { + EVAL_TAG_INT => key, + EVAL_TAG_STRING => { + let bytes = values.string_bytes(key)?; + let Some(key) = eval_numeric_string_array_key(&bytes) else { + return Ok(current_next_key); + }; + values.int(key)? + } + _ => return Ok(current_next_key), + }; let one = values.int(1)?; let candidate = values.add(key, one)?; let replace = if let Some(current_next_key) = current_next_key { @@ -2621,6 +2629,56 @@ fn eval_array_next_key_after_explicit_key( }) } +/// Parses PHP integer-string array keys that normalize to integer keys. +fn eval_numeric_string_array_key(bytes: &[u8]) -> Option { + if bytes.is_empty() { + return None; + } + + let (negative, digits) = if bytes[0] == b'-' { + if bytes.len() == 1 { + return None; + } + (true, &bytes[1..]) + } else { + (false, bytes) + }; + + if digits[0] == b'0' { + return if !negative && digits.len() == 1 { + Some(0) + } else { + None + }; + } + if digits.iter().any(|byte| !byte.is_ascii_digit()) { + return None; + } + + let limit = if negative { + i64::MAX as u128 + 1 + } else { + i64::MAX as u128 + }; + let mut value = 0u128; + for digit in digits { + value = (value * 10) + u128::from(digit - b'0'); + if value > limit { + return None; + } + } + + if negative { + if value == i64::MAX as u128 + 1 { + Some(i64::MIN) + } else { + Some(-(value as i64)) + } + } else { + Some(value as i64) + } +} + /// Converts one EvalIR constant into a runtime-cell handle. fn eval_const( value: &EvalConst, @@ -4092,6 +4150,32 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::String("tail".to_string())); } + /// Verifies numeric string literal keys update the next automatic key. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_numeric_string_key() { + let program = + parse_fragment(br#"return ["2" => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies leading-zero string literal keys do not update the automatic key. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_leading_zero_string_key() { + let program = + parse_fragment(br#"return ["02" => "two", "tail"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + /// Verifies nested eval calls parse and execute against the same dynamic scope. #[test] fn execute_program_nested_eval_uses_same_scope() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 0cfb690780..bda571b5d6 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -21,7 +21,7 @@ The bridge crate parses the fragment at runtime into a small EvalIR and interpre Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. -Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit integer keys are encountered. +Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit integer keys or PHP-normalized integer-string keys are encountered. Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b9efa5a11d..8f05c56e6e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -36,7 +36,7 @@ The evaluated string must be a PHP fragment without an opening ` "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); -$mixed_literal = eval('return [2 => "two", "tail"][3];'); +$mixed_literal = eval('return [2 => "two", "tail"][3] . ":" . (["2" => "two", "next"][3]);'); $append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); $append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a06672626a..a90f91d1bd 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -625,9 +625,13 @@ echo ":"; echo eval('return [2 => "two", "tail"][3];'); echo ":"; echo eval('return [-2 => "minus", "tail"][-1];'); +echo ":"; +echo eval('return ["2" => "two", "tail"][3];'); +echo ":"; +echo eval('return ["02" => "two", "tail"][0];'); "#, ); - assert_eq!(out, "Grace:tail:tail"); + assert_eq!(out, "Grace:tail:tail:tail:tail"); } /// Verifies eval-created associative arrays remain visible to native code. From 1f767f3cb76132c4df5a314c983bf015f22e3904 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 01:53:08 +0200 Subject: [PATCH 0086/1208] Normalize eval scalar array keys --- crates/elephc-eval/src/interpreter.rs | 54 ++++++++++++++++++++++++--- tests/codegen/eval.rs | 8 +++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5904eb0b15..160546fac9 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -2597,7 +2597,7 @@ fn eval_assoc_array( Ok(array) } -/// Advances an array literal's automatic key after an explicit PHP integer key. +/// Advances an array literal's automatic key after an integer-normalized explicit key. fn eval_array_next_key_after_explicit_key( key: RuntimeCellHandle, current_next_key: Option, @@ -2612,7 +2612,7 @@ fn eval_array_next_key_after_explicit_key( }; values.int(key)? } - _ => return Ok(current_next_key), + _ => values.cast_int(key)?, }; let one = values.int(1)?; let candidate = values.add(key, one)?; @@ -2771,12 +2771,15 @@ mod tests { self.values.get(&id).cloned().expect("fake cell missing") } - /// Converts a fake runtime cell into a fake PHP array key. + /// Converts a fake runtime cell into a normalized fake PHP array key. fn key(&self, handle: RuntimeCellHandle) -> Result { - match self.get(handle) { + let value = self.get(handle); + match value { FakeValue::Int(value) => Ok(FakeKey::Int(value)), - FakeValue::String(value) => Ok(FakeKey::String(value)), - _ => Err(EvalStatus::UnsupportedConstruct), + FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) + .map(FakeKey::Int) + .map_or_else(|| Ok(FakeKey::String(value)), Ok), + value => Ok(FakeKey::Int(self.fake_int(&value))), } } @@ -4176,6 +4179,45 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::String("tail".to_string())); } + /// Verifies boolean literal keys update the next automatic key after integer normalization. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { + let program = + parse_fragment(br#"return [true => "yes", "tail"][2];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies false literal keys update the next automatic key from zero. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_false_key() { + let program = + parse_fragment(br#"return [false => "no", "tail"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies float literal keys update the next automatic key after truncation. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_float_key() { + let program = + parse_fragment(br#"return [2.7 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + /// Verifies nested eval calls parse and execute against the same dynamic scope. #[test] fn execute_program_nested_eval_uses_same_scope() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a90f91d1bd..223bac7def 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -629,9 +629,15 @@ echo ":"; echo eval('return ["2" => "two", "tail"][3];'); echo ":"; echo eval('return ["02" => "two", "tail"][0];'); +echo ":"; +echo eval('return [true => "yes", "tail"][2];'); +echo ":"; +echo eval('return [false => "no", "tail"][1];'); +echo ":"; +echo eval('return [2.7 => "two", "tail"][3];'); "#, ); - assert_eq!(out, "Grace:tail:tail:tail:tail"); + assert_eq!(out, "Grace:tail:tail:tail:tail:tail:tail:tail"); } /// Verifies eval-created associative arrays remain visible to native code. From 5a46852162fd2c86bedaccea47a0e782864d2524 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 01:59:26 +0200 Subject: [PATCH 0087/1208] Normalize eval null array keys --- crates/elephc-eval/src/interpreter.rs | 28 ++++++++++++++++++++++ docs/php/arrays.md | 2 +- src/codegen_support/runtime/eval_bridge.rs | 12 ++++++++++ tests/codegen/eval.rs | 6 ++++- 4 files changed, 46 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 160546fac9..a2ad000582 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -2612,6 +2612,7 @@ fn eval_array_next_key_after_explicit_key( }; values.int(key)? } + EVAL_TAG_NULL => return Ok(current_next_key), _ => values.cast_int(key)?, }; let one = values.int(1)?; @@ -2779,6 +2780,7 @@ mod tests { FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) .map(FakeKey::Int) .map_or_else(|| Ok(FakeKey::String(value)), Ok), + FakeValue::Null => Ok(FakeKey::String(String::new())), value => Ok(FakeKey::Int(self.fake_int(&value))), } } @@ -4179,6 +4181,32 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::String("tail".to_string())); } + /// Verifies null literal keys normalize to empty strings without advancing automatic keys. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_null_key() { + let program = parse_fragment(br#"return [null => "empty", "tail"][0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies null literal keys are readable through the empty-string key. + #[test] + fn execute_program_assoc_array_literal_reads_null_key_as_empty_string() { + let program = + parse_fragment(br#"return [null => "empty"][""];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("empty".to_string())); + } + /// Verifies boolean literal keys update the next automatic key after integer normalization. #[test] fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { diff --git a/docs/php/arrays.md b/docs/php/arrays.md index 8501b3d3c2..75de6b2393 100644 --- a/docs/php/arrays.md +++ b/docs/php/arrays.md @@ -46,7 +46,7 @@ $map["age"] = "30"; // add new key Associative arrays use a hash table runtime. If later values do not match the first value type, the checker widens to internal `mixed` runtime shape. -Keys follow PHP's array-key normalization. Integer keys remain integers, booleans and floats normalize to integer keys, numeric strings such as `"1"` normalize to the integer key `1`, and strings with leading zeroes such as `"01"` remain string keys. This applies to literals, reads and writes, `foreach`, `array_keys()`, `array_search()`, `array_key_exists()`, `array_flip()`, JSON object keys, and array union. +Keys follow PHP's array-key normalization. Integer keys remain integers, booleans and floats normalize to integer keys, `null` normalizes to the empty-string key, numeric strings such as `"1"` normalize to the integer key `1`, and strings with leading zeroes such as `"01"` remain string keys. This applies to literals, reads and writes, `foreach`, `array_keys()`, `array_search()`, `array_key_exists()`, `array_flip()`, JSON object keys, and array union. ```php "two", "tail"][3];'); echo ":"; echo eval('return ["02" => "two", "tail"][0];'); echo ":"; +echo eval('return [null => "empty"][""];'); +echo ":"; +echo eval('return [null => "empty", "tail"][0];'); +echo ":"; echo eval('return [true => "yes", "tail"][2];'); echo ":"; echo eval('return [false => "no", "tail"][1];'); @@ -637,7 +641,7 @@ echo ":"; echo eval('return [2.7 => "two", "tail"][3];'); "#, ); - assert_eq!(out, "Grace:tail:tail:tail:tail:tail:tail:tail"); + assert_eq!(out, "Grace:tail:tail:tail:tail:empty:tail:tail:tail:tail"); } /// Verifies eval-created associative arrays remain visible to native code. From b194ec3ddce65999310a3b62dbda97fecb5329df Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 02:01:47 +0200 Subject: [PATCH 0088/1208] Update eval capability docs --- docs/internals/the-runtime.md | 4 ++-- docs/php/system-and-io.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index bda571b5d6..6acd6a4ece 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,11 +17,11 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier dispatch currently accepts simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, simple builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and numeric/string-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. -Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit integer keys or PHP-normalized integer-string keys are encountered. +Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 8f05c56e6e..58517992ce 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,11 +32,11 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, simple builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional or named arguments that share the same context, AOT global user functions callable from eval fragments with simple positional arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier native calls, still accept simple positional arguments. -Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`. +Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. Eval builtin dispatch also supports `strrev()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. From ad8a6e97dde39eb9a66d8d69f17768946a7bccc3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 02:08:27 +0200 Subject: [PATCH 0089/1208] Cover eval isset empty array offsets --- crates/elephc-eval/src/interpreter.rs | 45 +++++++++++++++++++++++++-- tests/codegen/eval.rs | 33 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index a2ad000582..0ff8f458b0 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -43,7 +43,10 @@ pub trait RuntimeValueOps { /// Creates a runtime associative-array cell with room for at least `capacity` elements. fn assoc_new(&mut self, capacity: usize) -> Result; - /// Reads one element from a runtime array-like Mixed cell using an index expression. + /// Reads one element from a runtime Mixed cell using PHP array-read semantics. + /// + /// Missing keys and non-array receivers return PHP null, matching the generated + /// `__rt_mixed_array_get` runtime helper. fn array_get( &mut self, array: RuntimeCellHandle, @@ -2829,7 +2832,7 @@ mod tests { .iter() .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) .map_or_else(|| self.null(), Ok), - _ => Err(EvalStatus::UnsupportedConstruct), + _ => self.null(), } } @@ -5250,6 +5253,44 @@ if (empty($value)) { echo "1"; } else { echo "0"; }"#, assert_eq!(values.get(result), FakeValue::Null); } + /// Verifies `isset` and `empty` use PHP offset semantics for array reads. + #[test] + fn execute_program_isset_and_empty_support_array_offsets() { + let program = parse_fragment( + br#"$map = [ + "present" => "x", + "nullish" => null, + "zero" => 0, + "empty" => "", + "child" => ["leaf" => "ok", "null" => null], +]; +echo isset($map["present"]) ? "1" : "0"; +echo isset($map["nullish"]) ? "1" : "0"; +echo isset($map["missing"]) ? "1" : "0"; +echo isset($map["zero"]) ? "1" : "0"; +echo isset($map["child"]["leaf"]) ? "1" : "0"; +echo isset($map["child"]["null"]) ? "1" : "0"; +echo isset($map["missing"]["leaf"]) ? "1" : "0"; +echo ":"; +echo empty($map["present"]) ? "1" : "0"; +echo empty($map["nullish"]) ? "1" : "0"; +echo empty($map["missing"]) ? "1" : "0"; +echo empty($map["zero"]) ? "1" : "0"; +echo empty($map["empty"]) ? "1" : "0"; +echo empty($map["child"]["leaf"]) ? "1" : "0"; +echo empty($map["child"]["null"]) ? "1" : "0"; +echo empty($map["missing"]["leaf"]) ? "1" : "0";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1001100:01111011"); + assert_eq!(values.get(result), FakeValue::Null); + } + /// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. #[test] fn execute_program_function_probes_use_eval_context() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b8b7be3c7d..623971449f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1155,6 +1155,39 @@ echo function_exists("empty") . "x";'); assert_eq!(out, "111110x"); } +/// Verifies eval `isset()` and `empty()` use PHP offset semantics for array reads. +#[test] +fn test_eval_isset_and_empty_support_array_offsets() { + let out = compile_and_run( + r#" "x", + "nullish" => null, + "zero" => 0, + "empty" => "", + "child" => ["leaf" => "ok", "null" => null], +];'); +eval('echo isset($map["present"]) ? "1" : "0"; +echo isset($map["nullish"]) ? "1" : "0"; +echo isset($map["missing"]) ? "1" : "0"; +echo isset($map["zero"]) ? "1" : "0"; +echo isset($map["child"]["leaf"]) ? "1" : "0"; +echo isset($map["child"]["null"]) ? "1" : "0"; +echo isset($map["missing"]["leaf"]) ? "1" : "0"; +echo ":"; +echo empty($map["present"]) ? "1" : "0"; +echo empty($map["nullish"]) ? "1" : "0"; +echo empty($map["missing"]) ? "1" : "0"; +echo empty($map["zero"]) ? "1" : "0"; +echo empty($map["empty"]) ? "1" : "0"; +echo empty($map["child"]["leaf"]) ? "1" : "0"; +echo empty($map["child"]["null"]) ? "1" : "0"; +echo empty($map["missing"]["leaf"]) ? "1" : "0";'); +"#, + ); + assert_eq!(out, "1001100:01111011"); +} + /// Verifies eval builtin dispatch can inspect arrays from the caller scope. #[test] fn test_eval_count_reads_scope_array() { From c8ca761cccd877cf671aa175d4b8436dba98c63c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 02:12:32 +0200 Subject: [PATCH 0090/1208] Support eval array_reverse builtin --- crates/elephc-eval/src/interpreter.rs | 97 +++++++++++++++++++++++++++ docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 23 +++++++ 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0ff8f458b0..1ce94f6aa3 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -905,6 +905,7 @@ fn eval_positional_expr_call( "array_product" | "array_sum" => { eval_builtin_array_aggregate(name, args, context, scope, values) } + "array_reverse" => eval_builtin_array_reverse(args, context, scope, values), "array_search" | "in_array" => { eval_builtin_array_search(name, args, context, scope, values) } @@ -1048,6 +1049,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_key_exists" | "array_keys" | "array_product" + | "array_reverse" | "array_search" | "array_sum" | "array_values" @@ -1183,6 +1185,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), "array_keys" | "array_product" | "array_sum" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), + "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" @@ -1358,6 +1361,14 @@ fn eval_builtin_with_values( }; values.array_key_exists(*key, *array)? } + "array_reverse" => match evaluated_args { + [array] => eval_array_reverse_result(*array, false, values)?, + [array, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_reverse_result(*array, preserve_keys, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "array_search" | "in_array" => { let [needle, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1602,6 +1613,64 @@ fn eval_array_projection_result( Ok(result) } +/// Evaluates PHP `array_reverse()` over an eval array expression. +fn eval_builtin_array_reverse( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_reverse_result(array, false, values) + } + [array, preserve_keys] => { + let array = eval_expr(array, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_array_reverse_result(array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_reverse()` result while preserving PHP key rules. +fn eval_array_reverse_result( + array: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut keys = Vec::with_capacity(len); + let mut has_string_key = false; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; + keys.push(key); + } + + let mut result = if preserve_keys || has_string_key { + values.assoc_new(len)? + } else { + values.array_new(len)? + }; + let mut next_numeric_key = 0_i64; + + for key in keys.into_iter().rev() { + let value = values.array_get(array, key)?; + let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { + key + } else { + let key = values.int(next_numeric_key)?; + next_numeric_key += 1; + key + }; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + /// Evaluates PHP `array_key_exists()` over a key and array expression. fn eval_builtin_array_key_exists( args: &[EvalExpr], @@ -4712,6 +4781,34 @@ return function_exists("array_values");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_reverse()` handles PHP key preservation rules. + #[test] + fn execute_program_dispatches_array_reverse_builtin() { + let program = parse_fragment( + br#"$indexed = array_reverse([1, 2, 3]); +echo $indexed[0]; echo $indexed[1]; echo $indexed[2]; echo ":"; +$mixed = array_reverse([2 => "a", "k" => "b", 5 => "c"]); +echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; +$preserved = array_reverse([2 => "a", "k" => "b", 5 => "c"], true); +echo $preserved[5]; echo $preserved["k"]; echo $preserved[2]; echo ":"; +$named = array_reverse(array: ["x", "y"], preserve_keys: true); +echo $named[1]; echo $named[0]; echo ":"; +$call = call_user_func("array_reverse", [4, 5]); +echo $call[0]; echo $call[1]; echo ":"; +$spread = call_user_func_array("array_reverse", [[6, 7]]); +echo $spread[0]; echo $spread[1]; echo ":"; +return function_exists("array_reverse");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "321:cba:cba:yx:54:76:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. #[test] fn execute_program_dispatches_array_key_exists_builtin() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 58517992ce..56d27bc172 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier native calls, still accept simple positional arguments. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 623971449f..1b1cbb12f4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -754,6 +754,29 @@ echo ":"; echo function_exists("array_keys"); echo function_exists("array_values assert_eq!(out, "10:20:a:b:0:z:8:11"); } +/// Verifies eval `array_reverse()` supports key rules, named args, and callable dispatch. +#[test] +fn test_eval_dispatches_array_reverse_builtin_call() { + let out = compile_and_run( + r#" "a", "k" => "b", 5 => "c"]); +echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; +$preserved = array_reverse([2 => "a", "k" => "b", 5 => "c"], true); +echo $preserved[5]; echo $preserved["k"]; echo $preserved[2]; echo ":"; +$named = array_reverse(array: ["x", "y"], preserve_keys: true); +echo $named[1]; echo $named[0]; echo ":"; +$call = call_user_func("array_reverse", [4, 5]); +echo $call[0]; echo $call[1]; echo ":"; +$spread = call_user_func_array("array_reverse", [[6, 7]]); +echo $spread[0]; echo $spread[1]; echo ":"; +echo function_exists("array_reverse");'); +"#, + ); + assert_eq!(out, "321:cba:cba:yx:54:76:1"); +} + /// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. #[test] fn test_eval_dispatches_array_key_exists_builtin_calls() { From 9a5fe5a28ebf06a19681fa4eabf512c655b73ad7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 02:20:16 +0200 Subject: [PATCH 0091/1208] Support eval array_combine builtin --- crates/elephc-eval/src/interpreter.rs | 74 +++++++++++++++++++ docs/php/system-and-io.md | 2 +- .../runtime/arrays/hash_free_deep.rs | 1 + tests/codegen/eval.rs | 23 ++++++ 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 1ce94f6aa3..f04f2e6b31 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -898,6 +898,7 @@ fn eval_positional_expr_call( ) -> Result { match name { "abs" => eval_builtin_abs(args, context, scope, values), + "array_combine" => eval_builtin_array_combine(args, context, scope, values), "array_keys" | "array_values" => { eval_builtin_array_projection(name, args, context, scope, values) } @@ -1046,6 +1047,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, "abs" + | "array_combine" | "array_key_exists" | "array_keys" | "array_product" @@ -1183,6 +1185,7 @@ fn collect_contiguous_bound_args( fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { match name { "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), + "array_combine" => Some(&["keys", "values"]), "array_keys" | "array_product" | "array_sum" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), @@ -1343,6 +1346,12 @@ fn eval_builtin_with_values( }; values.abs(*value)? } + "array_combine" => { + let [keys, values_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_combine_result(*keys, *values_array, values)? + } "array_product" | "array_sum" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1577,6 +1586,44 @@ fn eval_array_aggregate_result( Ok(result) } +/// Evaluates PHP `array_combine()` over key and value array expressions. +fn eval_builtin_array_combine( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, values_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let values_array = eval_expr(values_array, context, scope, values)?; + eval_array_combine_result(keys, values_array, values) +} + +/// Builds the associative result for `array_combine()` from two eval arrays. +fn eval_array_combine_result( + keys: RuntimeCellHandle, + values_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + if len != values.array_len(values_array)? { + return Err(EvalStatus::RuntimeFatal); + } + + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + let target_key = values.cast_string(target_key)?; + let value_key = values.array_iter_key(values_array, position)?; + let value = values.array_get(values_array, value_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + /// Evaluates PHP array projection builtins over one eval array expression. fn eval_builtin_array_projection( name: &str, @@ -4755,6 +4802,33 @@ return function_exists("array_product");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_combine()` converts key values through PHP string-key rules. + #[test] + fn execute_program_dispatches_array_combine_builtin() { + let program = parse_fragment( + br#"$pairs = array_combine(["a", "b"], [10, 20]); +echo $pairs["a"] . ":" . $pairs["b"]; +$numeric = array_combine(["1", "01"], ["n", "z"]); +echo ":" . $numeric[1] . $numeric["01"]; +$scalar = array_combine([null, true, false, 2.8], ["n", "t", "f", "d"]); +echo ":" . $scalar[""] . $scalar[1] . $scalar["2.8"]; +$named = array_combine(keys: ["k"], values: ["v"]); +echo ":" . $named["k"]; +$call = call_user_func("array_combine", ["x"], [7]); +echo ":" . $call["x"]; +$spread = call_user_func_array("array_combine", [["y"], [8]]); +echo ":" . $spread["y"] . ":"; +return function_exists("array_combine");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:nz:ftd:v:7:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval array projection builtins produce indexed key/value arrays. #[test] fn execute_program_dispatches_array_projection_builtins() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 56d27bc172..162bebf3b5 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier native calls, still accept simple positional arguments. diff --git a/src/codegen_support/runtime/arrays/hash_free_deep.rs b/src/codegen_support/runtime/arrays/hash_free_deep.rs index c4478c732f..fafabc3ef1 100644 --- a/src/codegen_support/runtime/arrays/hash_free_deep.rs +++ b/src/codegen_support/runtime/arrays/hash_free_deep.rs @@ -93,6 +93,7 @@ pub fn emit_hash_free_deep(emitter: &mut Emitter) { emitter.instruction("cmn x15, #1"); // check whether this entry stores an inline integer key emitter.instruction("b.eq __rt_hash_free_deep_after_key"); // integer keys have no heap ownership to release emitter.instruction("ldr x0, [x13, #8]"); // load key pointer + emitter.instruction("cbz x0, __rt_hash_free_deep_after_key"); // empty string keys store no heap key pointer emitter.instruction("str x11, [sp, #24]"); // preserve loop index across helper call emitter.instruction("ldr w15, [x0, #-12]"); // load key refcount from heap header emitter.instruction("subs w15, w15, #1"); // decrement key refcount diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1b1cbb12f4..45ad8b84e3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -734,6 +734,29 @@ echo ":"; echo function_exists("array_sum"); echo function_exists("array_product assert_eq!(out, "6:24:0:1:7:7:10:11"); } +/// Verifies eval `array_combine()` supports PHP key conversions and callable dispatch. +#[test] +fn test_eval_dispatches_array_combine_builtin_call() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 02:25:08 +0200 Subject: [PATCH 0092/1208] Support eval array_flip builtin --- crates/elephc-eval/src/interpreter.rs | 67 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 19 ++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f04f2e6b31..aac68f4d00 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -899,6 +899,7 @@ fn eval_positional_expr_call( match name { "abs" => eval_builtin_abs(args, context, scope, values), "array_combine" => eval_builtin_array_combine(args, context, scope, values), + "array_flip" => eval_builtin_array_flip(args, context, scope, values), "array_keys" | "array_values" => { eval_builtin_array_projection(name, args, context, scope, values) } @@ -1048,6 +1049,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { name, "abs" | "array_combine" + | "array_flip" | "array_key_exists" | "array_keys" | "array_product" @@ -1186,7 +1188,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { match name { "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), "array_combine" => Some(&["keys", "values"]), - "array_keys" | "array_product" | "array_sum" | "array_values" => Some(&["array"]), + "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_values" => { + Some(&["array"]) + } "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), @@ -1352,6 +1356,12 @@ fn eval_builtin_with_values( }; eval_array_combine_result(*keys, *values_array, values)? } + "array_flip" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_flip_result(*array, values)? + } "array_product" | "array_sum" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1624,6 +1634,38 @@ fn eval_array_combine_result( Ok(result) } +/// Evaluates PHP `array_flip()` over one eval array expression. +fn eval_builtin_array_flip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_flip_result(array, values) +} + +/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. +fn eval_array_flip_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { + continue; + } + result = values.array_set(result, value, key)?; + } + Ok(result) +} + /// Evaluates PHP array projection builtins over one eval array expression. fn eval_builtin_array_projection( name: &str, @@ -4829,6 +4871,29 @@ return function_exists("array_combine");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. + #[test] + fn execute_program_dispatches_array_flip_builtin() { + let program = parse_fragment( + br#"$flipped = array_flip(["a" => "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); +echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); +$named = array_flip(array: ["k" => "v"]); +echo ":" . $named["v"]; +$call = call_user_func("array_flip", ["left" => "right"]); +echo ":" . $call["right"]; +$spread = call_user_func_array("array_flip", [["n" => 9]]); +echo ":" . $spread[9] . ":"; +return function_exists("array_flip");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "c:b:d:e:4:k:left:n:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval array projection builtins produce indexed key/value arrays. #[test] fn execute_program_dispatches_array_projection_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 6acd6a4ece..79794065f5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier dispatch currently accepts simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_keys()`, `array_values()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 162bebf3b5..4e2e279abd 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier native calls, still accept simple positional arguments. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 45ad8b84e3..3e85e67581 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -757,6 +757,25 @@ echo function_exists("array_combine");'); assert_eq!(out, "10:20:nz:ftd:v:7:8:1"); } +/// Verifies eval `array_flip()` supports PHP key rules and callable dispatch. +#[test] +fn test_eval_dispatches_array_flip_builtin_call() { + let out = compile_and_run( + r#" "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); +echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); +$named = array_flip(array: ["k" => "v"]); +echo ":" . $named["v"]; +$call = call_user_func("array_flip", ["left" => "right"]); +echo ":" . $call["right"]; +$spread = call_user_func_array("array_flip", [["n" => 9]]); +echo ":" . $spread[9] . ":"; +echo function_exists("array_flip");'); +"#, + ); + assert_eq!(out, "c:b:d:e:4:k:left:n:1"); +} + /// Verifies eval array projection builtins return indexed key/value arrays. #[test] fn test_eval_dispatches_array_projection_builtin_calls() { From 0bb34f5ffe262d985dac62c44138b4e8cef71b27 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 02:29:36 +0200 Subject: [PATCH 0093/1208] Support eval array_unique builtin --- crates/elephc-eval/src/interpreter.rs | 76 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 24 +++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index aac68f4d00..ba3bc83dbf 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -911,6 +911,7 @@ fn eval_positional_expr_call( "array_search" | "in_array" => { eval_builtin_array_search(name, args, context, scope, values) } + "array_unique" => eval_builtin_array_unique(args, context, scope, values), "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), @@ -1056,6 +1057,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_reverse" | "array_search" | "array_sum" + | "array_unique" | "array_values" | "ceil" | "call_user_func" @@ -1188,9 +1190,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { match name { "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), "array_combine" => Some(&["keys", "values"]), - "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_values" => { - Some(&["array"]) - } + "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" + | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), @@ -1394,6 +1395,12 @@ fn eval_builtin_with_values( }; eval_array_search_result(name, *needle, *array, values)? } + "array_unique" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_unique_result(*array, values)? + } "ceil" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -1666,6 +1673,41 @@ fn eval_array_flip_result( Ok(result) } +/// Evaluates PHP `array_unique()` over one eval array expression. +fn eval_builtin_array_unique( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_unique_result(array, values) +} + +/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. +fn eval_array_unique_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut seen = Vec::>::with_capacity(len); + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let unique_key = values.string_bytes(value)?; + if seen.iter().any(|seen_key| seen_key == &unique_key) { + continue; + } + seen.push(unique_key); + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Evaluates PHP array projection builtins over one eval array expression. fn eval_builtin_array_projection( name: &str, @@ -4894,6 +4936,34 @@ return function_exists("array_flip");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_unique()` preserves first keys using default string comparison. + #[test] + fn execute_program_dispatches_array_unique_builtin() { + let program = parse_fragment( + br#"$unique = array_unique(["a", "b", "a", "2", 2]); +echo count($unique) . ":" . $unique[0] . $unique[1] . $unique[3]; +$assoc = array_unique(["x" => "a", "y" => "b", "z" => "a"]); +echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; +$scalar = array_unique([1, "1", 1.0, true, false, null, ""]); +echo ":" . count($scalar) . ":" . $scalar[0] . ":"; +echo $scalar[4] ? "bad" : "F"; +$named = array_unique(array: ["k" => "v", "l" => "v"]); +echo ":" . $named["k"] . ":" . count($named); +$call = call_user_func("array_unique", ["q", "q", "r"]); +echo ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_unique", [["s", "s", "t"]]); +echo ":" . $spread[0] . $spread[2] . ":"; +return function_exists("array_unique");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:ab2:2:ab:2:1:F:v:1:qr:st:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval array projection builtins produce indexed key/value arrays. #[test] fn execute_program_dispatches_array_projection_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 79794065f5..4f47a3773c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier dispatch currently accepts simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4e2e279abd..4273590e07 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier native calls, still accept simple positional arguments. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3e85e67581..3ccd7fc4df 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -776,6 +776,30 @@ echo function_exists("array_flip");'); assert_eq!(out, "c:b:d:e:4:k:left:n:1"); } +/// Verifies eval `array_unique()` preserves keys and supports callable dispatch. +#[test] +fn test_eval_dispatches_array_unique_builtin_call() { + let out = compile_and_run( + r#" "a", "y" => "b", "z" => "a"]); +echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; +$scalar = array_unique([1, "1", 1.0, true, false, null, ""]); +echo ":" . count($scalar) . ":" . $scalar[0] . ":"; +echo $scalar[4] ? "bad" : "F"; +$named = array_unique(array: ["k" => "v", "l" => "v"]); +echo ":" . $named["k"] . ":" . count($named); +$call = call_user_func("array_unique", ["q", "q", "r"]); +echo ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_unique", [["s", "s", "t"]]); +echo ":" . $spread[0] . $spread[2] . ":"; +echo function_exists("array_unique");'); +"#, + ); + assert_eq!(out, "3:ab2:2:ab:2:1:F:v:1:qr:st:1"); +} + /// Verifies eval array projection builtins return indexed key/value arrays. #[test] fn test_eval_dispatches_array_projection_builtin_calls() { From bab4980494e92224d1056a59a3a5cb725592d38f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 02:45:55 +0200 Subject: [PATCH 0094/1208] Support post-eval call_user_func_array dispatch --- crates/elephc-eval/src/interpreter.rs | 43 ++++++++++++++ crates/elephc-eval/src/lib.rs | 68 +++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- src/codegen/context.rs | 1 + src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/builtins.rs | 8 +++ src/codegen/lower_inst/builtins/eval.rs | 43 +++++++++++++- src/ir/instr.rs | 5 +- src/ir/validator.rs | 7 +-- src/ir_lower/context.rs | 1 + src/ir_lower/expr/mod.rs | 79 +++++++++++++++++++++---- src/ir_lower/program.rs | 2 +- src/types/checker/builtins/callables.rs | 10 ++++ tests/codegen/eval.rs | 14 +++++ 15 files changed, 268 insertions(+), 22 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ba3bc83dbf..07207b255e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -319,6 +319,20 @@ pub fn execute_context_function( }) } +/// Executes a named eval-context callable with arguments from a PHP array container. +pub fn execute_context_function_call_array( + context: &mut ElephcEvalContext, + name: &str, + arg_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.is_array_like(arg_array)? { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = eval_array_call_arg_values(arg_array, values)?; + eval_callable_with_call_array_args(name, evaluated_args, context, values) +} + /// Executes statements in source order and propagates the first eval `return`. fn execute_statements( statements: &[EvalStmt], @@ -4717,6 +4731,35 @@ return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, assert_eq!(values.get(result), FakeValue::Int(12)); } + /// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. + #[test] + fn execute_context_function_call_array_binds_declared_named_args() { + let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + let arg_array = values.assoc_new(2).expect("allocate argument array"); + let key_y = values.string("y").expect("allocate y key"); + let value_y = values.int(2).expect("allocate y value"); + let _ = values + .array_set(arg_array, key_y, value_y) + .expect("store y argument"); + let key_x = values.string("x").expect("allocate x key"); + let value_x = values.int(1).expect("allocate x value"); + let _ = values + .array_set(arg_array, key_x, value_x) + .expect("store x argument"); + + let result = + execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) + .expect("execute context function call array"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + /// Verifies `call_user_func_array` rejects positional values after named keys. #[test] fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 42445f1aaa..1995fc0294 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -330,6 +330,27 @@ pub unsafe extern "C" fn __elephc_eval_call_function( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Calls a function previously declared through `eval()` with an argument array/hash. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `arg_array` must be a boxed Mixed +/// indexed or associative array cell, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function_array( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_array_inner(ctx, name_ptr, name_len, arg_array, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Runs the eval function-exists ABI body after installing a panic boundary. /// /// # Safety @@ -514,6 +535,53 @@ unsafe fn call_eval_function_inner( } } +/// Runs the dynamic function-call-array ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_call_function_array`; callers must provide a valid +/// context, readable function-name bytes, and a boxed array/hash argument cell. +#[cfg(not(test))] +unsafe fn call_eval_function_array_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_array.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + if !out.is_null() { + (*out).clear(); + } + let mut values = ElephcRuntimeOps::new(); + match interpreter::execute_context_function_call_array( + context, + &name.to_ascii_lowercase(), + RuntimeCellHandle::from_raw(arg_array), + &mut values, + ) { + Ok(result) => { + if !out.is_null() { + (*out).kind = 0; + (*out).value_cell = result.as_ptr(); + (*out).error = std::ptr::null_mut(); + } + EvalStatus::Ok.code() + } + Err(status) => status.code(), + } +} + /// Runs the eval ABI body after the exported wrapper has installed a panic boundary. /// /// # Safety diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 4f47a3773c..f0075e15fa 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier dispatch currently accepts simple positional arguments. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4273590e07..630e2e0672 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,9 +32,9 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier native calls, still accept simple positional arguments. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/src/codegen/context.rs b/src/codegen/context.rs index 31c544f7cf..af511f592c 100644 --- a/src/codegen/context.rs +++ b/src/codegen/context.rs @@ -550,6 +550,7 @@ impl<'a> FunctionContext<'a> { | Op::FunctionVariantCall | Op::BuiltinCall | Op::EvalFunctionCall + | Op::EvalFunctionCallArray | Op::RuntimeCall | Op::ExternCall | Op::MethodCall diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 260d25a937..42d23c74f6 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -198,6 +198,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::ExternCall => externs::lower_extern_call(ctx, &inst), Op::BuiltinCall => builtins::lower_builtin_call(ctx, &inst), Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), + Op::EvalFunctionCallArray => builtins::lower_eval_function_call_array(ctx, &inst), Op::EvalFunctionExists => builtins::lower_eval_function_exists(ctx, &inst), Op::ClosureCapture => lower_closure_capture(ctx, &inst), Op::ClosureNew => lower_closure_new(ctx, &inst), diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 3884fa5894..be6800fca6 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -88,6 +88,14 @@ pub(super) fn lower_eval_function_call( eval::lower_eval_function_call(ctx, inst) } +/// Lowers a post-eval function call whose arguments are packed in an array/hash container. +pub(super) fn lower_eval_function_call_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_function_call_array(ctx, inst) +} + /// Lowers post-eval dynamic function existence probes to the optional eval bridge. pub(super) fn lower_eval_function_exists( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 3d9caf5040..6a4d5a9dca 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -137,7 +137,7 @@ fn set_eval_call_site(ctx: &mut FunctionContext<'_>, inst: &Instruction) { emit_eval_status_check(ctx); } -/// Lowers a native call to a zero-argument function declared by a prior `eval()` call. +/// Lowers a native positional call to a function declared by a prior `eval()` call. pub(super) fn lower_eval_function_call( ctx: &mut FunctionContext<'_>, inst: &Instruction, @@ -182,6 +182,47 @@ pub(super) fn lower_eval_function_call( store_if_result(ctx, inst) } +/// Lowers a native call to a prior eval-declared function using an argument array/hash. +pub(super) fn lower_eval_function_call_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "eval function call array", 1)?; + let function_name = ctx.function_name_data(expect_data(inst)?)?.to_string(); + let arg_array = expect_operand(inst, 0)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + let ty = ctx.load_value_to_result(arg_array)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(function_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_load_temporary_stack_slot(ctx.emitter, args_arg, EVAL_TEMP_CELL_OFFSET); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_call_function_array"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Lowers a post-eval dynamic function existence probe to the eval bridge ABI. pub(super) fn lower_eval_function_exists( ctx: &mut FunctionContext<'_>, diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 4ade27dae0..59b4ec7ac4 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -356,6 +356,7 @@ pub enum Op { FunctionVariantCall, BuiltinCall, EvalFunctionCall, + EvalFunctionCallArray, EvalFunctionExists, RuntimeCall, ExternCall, @@ -494,7 +495,7 @@ impl Op { E::READS_HEAP | E::ALLOC_HEAP | E::MAY_THROW } EvalFunctionExists => E::READS_GLOBAL, - Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | RuntimeCall + Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | EvalFunctionCallArray | RuntimeCall | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { E::all().difference(E::REFCOUNT_OP) } @@ -519,6 +520,7 @@ impl Op { | Op::FunctionVariantCall | Op::BuiltinCall | Op::EvalFunctionCall + | Op::EvalFunctionCallArray | Op::RuntimeCall | Op::ExternCall | Op::MethodCall @@ -694,6 +696,7 @@ impl Op { FunctionVariantCall => "function_variant_call", BuiltinCall => "builtin_call", EvalFunctionCall => "eval_function_call", + EvalFunctionCallArray => "eval_function_call_array", EvalFunctionExists => "eval_function_exists", RuntimeCall => "runtime_call", ExternCall => "extern_call", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index a38edda8c3..373c5ac2bd 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -273,9 +273,8 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstF64 => require_immediate(inst_id, inst, "f64", |imm| matches!(imm, Imm::F64(_))), ConstBool => require_immediate(inst_id, inst, "bool", |imm| matches!(imm, Imm::Bool(_))), ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard - | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell - | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionExists - | EnumBackingStringToInt | EnumBackingMixedToInt => { + | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionCallArray + | EvalFunctionExists | EnumBackingStringToInt | EnumBackingMixedToInt => { require_immediate(inst_id, inst, "data id", |imm| matches!(imm, Imm::Data(_))) } LoadLocal | StoreLocal | UnsetLocal | LoadRefCell | StoreRefCell | ReleaseLocalRefCell @@ -354,11 +353,11 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio | CallableArrayNew | GeneratorNew | InvokerRefArg | ErrorSuppressBegin | ErrorSuppressEnd | TryPushHandler | TryPopHandler | CatchCurrent | CatchBind | FinallyEnter | FinallyExit | IncludeOnceMark - | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | ConcatReset | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | EvalFunctionExists | ConcatReset | GcCollect | Nop => { check_count(inst_id, inst, 0, "0") } + EvalFunctionCallArray => check_count(inst_id, inst, 1, "1"), ClosureNew => Ok(()), FirstClassCallableNew => check_count_at_most(inst_id, inst, 1, "0 or 1"), ObjectNew => Ok(()), diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index b0fa370b92..b4e4bcd7b7 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1151,6 +1151,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::Call | Op::FunctionVariantCall | Op::EvalFunctionCall + | Op::EvalFunctionCallArray | Op::RuntimeCall | Op::ExternCall | Op::MethodCall diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index a3d4cd6dcf..036e0be263 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -2416,18 +2416,21 @@ fn lower_static_call_user_func( { return None; } - let callback_args = static_call_user_func_array_args(arg_array)?; - if let Some(callback) = instance_call_user_func_callback(ctx, callback_arg) { - return lower_instance_callable_call_user_func( - ctx, - callback_arg, - callback, - &callback_args, - expr, - ); + if let Some(callback_args) = static_call_user_func_array_args(arg_array) { + if let Some(callback) = instance_call_user_func_callback(ctx, callback_arg) { + return lower_instance_callable_call_user_func( + ctx, + callback_arg, + callback, + &callback_args, + expr, + ); + } + if let Some(callback) = static_call_user_func_callback(ctx, callback_arg) { + return lower_static_callable_call(ctx, callback, &callback_args, expr); + } } - let callback = static_call_user_func_callback(ctx, callback_arg)?; - lower_static_callable_call(ctx, callback, &callback_args, expr) + lower_eval_call_user_func_array_fallback(ctx, callback_arg, arg_array, expr) } _ => None, } @@ -2464,6 +2467,60 @@ fn lower_eval_call_user_func_fallback( )) } +/// Lowers unresolved `call_user_func_array()` string callbacks through the eval table. +fn lower_eval_call_user_func_array_fallback( + ctx: &mut LoweringContext<'_, '_>, + callback_expr: &Expr, + arg_array: &Expr, + expr: &Expr, +) -> Option { + if !ctx.has_eval_barrier() { + return None; + } + let ExprKind::StringLiteral(callback_name) = &callback_expr.kind else { + return None; + }; + if callback_name.contains("::") + || resolve_static_string_callable(ctx, callback_name).is_some() + { + return None; + } + let dynamic_name = php_symbol_key(callback_name.trim_start_matches('\\')); + let data = ctx.intern_function_name(&dynamic_name); + let arg_array = lower_expr(ctx, arg_array); + let arg_array = coerce_eval_function_arg_array(ctx, arg_array, expr.span); + Some(ctx.emit_value( + Op::EvalFunctionCallArray, + vec![arg_array.value], + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalFunctionCallArray.default_effects(), + Some(expr.span), + )) +} + +/// Boxes a post-barrier dynamic-call argument container for the eval bridge ABI. +fn coerce_eval_function_arg_array( + ctx: &mut LoweringContext<'_, '_>, + value: LoweredValue, + span: Span, +) -> LoweredValue { + if matches!( + ctx.builder.value_php_type(value.value).codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { + return value; + } + ctx.emit_value( + Op::MixedBox, + vec![value.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(span), + ) +} + /// Lowers `call_user_func*` for receiver-bound first-class callables through `expr_call`. fn lower_instance_callable_call_user_func( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 263af120fe..6c27af9b87 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -132,7 +132,7 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { features.eval = true; } } - Op::EvalFunctionCall | Op::EvalFunctionExists => { + Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalFunctionExists => { features.eval = true; } Op::ExprCall | Op::CallableDescriptorInvoke => { diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index c8f40a4d80..0805faa9f4 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -964,6 +964,16 @@ pub(crate) fn check_call_user_func_array( let ret_ty = checker.check_function_call(&cb_name, &spread_args, span, env)?; return Ok(ret_ty); } + let arg_array_ty = checker.infer_type(&args[1], env)?; + if checker.eval_barrier_active && !cb_name.contains("::") { + if !call_user_func_array_arg_container_is_supported(&arg_array_ty) { + return Err(CompileError::new( + args[1].span, + "call_user_func_array() second argument must be an array", + )); + } + return Ok(PhpType::Mixed); + } // A string-literal callback that matched no extern, builtin, user function, // or fn_decl is an undefined function. Reject plain function-name callbacks // here instead of falling through to the generic `Str` acceptance below diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3ccd7fc4df..93ca47e9ba 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1431,6 +1431,20 @@ echo call_user_func('dyn_eval_cuf', 4); assert_eq!(out, "5"); } +/// Verifies post-barrier `call_user_func_array()` can dispatch to eval-declared functions. +#[test] +fn test_eval_declared_function_can_be_called_with_call_user_func_array() { + let out = compile_and_run( + r#" 2, 'x' => 1]); +$args = ['y' => 3, 'x' => 2]; +echo ":" . call_user_func_array('dyn_eval_cufa', $args); +"#, + ); + assert_eq!(out, "12:23"); +} + /// Verifies `call_user_func()` inside eval can dispatch to an eval-declared function. #[test] fn test_eval_fragment_call_user_func_dispatches_eval_declared_function() { From 4b6131af08abd00cd72d63dbf87b3fe99d87dcaf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 03:01:20 +0200 Subject: [PATCH 0095/1208] Support eval static locals --- crates/elephc-eval/src/context.rs | 23 +++++ crates/elephc-eval/src/eval_ir.rs | 4 + crates/elephc-eval/src/interpreter.rs | 139 ++++++++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 32 ++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 27 +++++ 7 files changed, 228 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index d59de09bad..e4545a68a0 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -87,6 +87,7 @@ pub struct ElephcEvalContext { abi_version: u32, functions: HashMap, native_functions: HashMap, + static_locals: HashMap<(String, String), RuntimeCellHandle>, function_stack: Vec, call_file: String, call_dir: String, @@ -100,6 +101,7 @@ impl ElephcEvalContext { abi_version: ABI_VERSION, functions: HashMap::new(), native_functions: HashMap::new(), + static_locals: HashMap::new(), function_stack: Vec::new(), call_file: String::new(), call_dir: String::new(), @@ -114,6 +116,7 @@ impl ElephcEvalContext { abi_version, functions: HashMap::new(), native_functions: HashMap::new(), + static_locals: HashMap::new(), function_stack: Vec::new(), call_file: String::new(), call_dir: String::new(), @@ -181,6 +184,26 @@ impl ElephcEvalContext { self.functions.contains_key(name) || self.native_functions.contains_key(name) } + /// Returns a stored static local cell for an eval-declared function. + pub fn static_local(&self, function_name: &str, name: &str) -> Option { + self.static_locals + .get(&(function_name.to_string(), name.to_string())) + .copied() + } + + /// Stores one static local cell and returns any replaced distinct cell. + pub fn set_static_local( + &mut self, + function_name: impl Into, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .static_locals + .insert((function_name.into(), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + /// Pushes an eval-executed function name for magic-constant resolution. pub fn push_function(&mut self, name: impl Into) { self.function_stack.push(name.into()); diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 6c3dae329b..70f73df4af 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -90,6 +90,10 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + StaticVar { + name: String, + init: EvalExpr, + }, StoreVar { name: String, value: EvalExpr, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 07207b255e..997d013d2f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -469,6 +469,10 @@ fn execute_stmt( expr, context, scope, values, )?)), EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), + EvalStmt::StaticVar { name, init } => { + execute_static_var_stmt(name, init, context, scope, values)?; + Ok(EvalControl::None) + } EvalStmt::PropertySet { object, property, @@ -515,6 +519,37 @@ fn execute_stmt( } } +/// Executes a PHP `static $name = expr;` declaration in the current eval scope. +fn execute_static_var_stmt( + name: &str, + init: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(function_name) = context.current_function().map(str::to_string) else { + let value = eval_expr(init, context, scope, values)?; + if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Owned) { + values.release(replaced)?; + } + return Ok(()); + }; + if scope.contains_visible(name) { + return Ok(()); + } + let value = if let Some(value) = context.static_local(&function_name, name) { + value + } else { + let value = eval_expr(init, context, scope, values)?; + let _ = context.set_static_local(function_name.clone(), name.to_string(), value); + value + }; + if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Borrowed) { + values.release(replaced)?; + } + Ok(()) +} + /// Executes a PHP switch with loose case matching, default fallback, and fallthrough. fn execute_switch_stmt( expr: &EvalExpr, @@ -2715,9 +2750,13 @@ fn eval_dynamic_function_with_values( for (name, value) in function.params().iter().zip(evaluated_args) { function_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); } + let static_names = static_var_names(function.body()); context.push_function(function.name()); let result = execute_statements(function.body(), context, &mut function_scope, values); + let persist_result = + persist_static_locals(context, function.name(), &static_names, &function_scope, values); context.pop_function(); + persist_result?; match result? { EvalControl::None => values.null(), EvalControl::Return(result) => Ok(result), @@ -2725,6 +2764,72 @@ fn eval_dynamic_function_with_values( } } +/// Persists static local variables from one eval-declared function activation. +fn persist_static_locals( + context: &mut ElephcEvalContext, + function_name: &str, + names: &[String], + scope: &ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for name in names { + if let Some(cell) = scope.visible_cell(name) { + if let Some(replaced) = + context.set_static_local(function_name.to_string(), name.clone(), cell) + { + values.release(replaced)?; + } + } + } + Ok(()) +} + +/// Returns the distinct static local names declared anywhere in an eval function body. +fn static_var_names(body: &[EvalStmt]) -> Vec { + let mut names = std::collections::HashSet::new(); + collect_static_var_names(body, &mut names); + names.into_iter().collect() +} + +/// Recursively collects static local declaration names from eval statements. +fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::HashSet) { + for stmt in body { + match stmt { + EvalStmt::StaticVar { name, .. } => { + names.insert(name.clone()); + } + EvalStmt::DoWhile { body, .. } + | EvalStmt::Foreach { body, .. } + | EvalStmt::For { body, .. } + | EvalStmt::While { body, .. } => collect_static_var_names(body, names), + EvalStmt::FunctionDecl { .. } => {} + EvalStmt::If { + then_branch, + else_branch, + .. + } => { + collect_static_var_names(then_branch, names); + collect_static_var_names(else_branch, names); + } + EvalStmt::Switch { cases, .. } => { + for case in cases { + collect_static_var_names(&case.body, names); + } + } + EvalStmt::ArrayAppendVar { .. } + | EvalStmt::ArraySetVar { .. } + | EvalStmt::Break + | EvalStmt::Continue + | EvalStmt::Echo(_) + | EvalStmt::Expr(_) + | EvalStmt::PropertySet { .. } + | EvalStmt::Return(_) + | EvalStmt::StoreVar { .. } + | EvalStmt::UnsetVar { .. } => {} + } + } +} + /// Evaluates a registered AOT function through its descriptor-compatible invoker. fn eval_native_function( function: NativeFunction, @@ -4582,6 +4687,40 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::Int(12)); } + /// Verifies eval-declared function static locals persist between calls. + #[test] + fn execute_program_static_var_persists_in_declared_function() { + let program = parse_fragment( + br#"function dyn() { for ($i = 0; $i < 2; $i++) { static $n = 0; $n++; } return $n; } +return (dyn() * 10) + dyn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(24)); + } + + /// Verifies top-level eval static declarations reinitialize on each eval execution. + #[test] + fn execute_program_top_level_static_var_reinitializes_per_eval() { + let program = + parse_fragment(br#"static $n = 0; $n++; return $n;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let first = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute first eval ir"); + let second = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute second eval ir"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(1)); + } + /// Verifies named calls reject positional arguments that follow named arguments. #[test] fn execute_program_rejects_positional_after_named_arg() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index a3ec7838ff..486e3ec0a8 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -581,6 +581,7 @@ impl Parser { self.expect_semicolon()?; Ok(vec![EvalStmt::Return(Some(expr))]) } + TokenKind::Ident(name) if ident_eq(name, "static") => self.parse_static_var_stmt(), TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), @@ -714,6 +715,20 @@ impl Parser { Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) } + /// Parses `static $name = expr;` declarations in eval fragments. + fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let init = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::StaticVar { name, init }]) + } + /// Parses a dynamic function declaration parameter list after `(`. fn parse_function_params(&mut self) -> Result, EvalParseError> { let mut params = Vec::new(); @@ -2134,6 +2149,23 @@ mod tests { ); } + /// Verifies static local declarations preserve the target name and initializer expression. + #[test] + fn parse_fragment_accepts_static_var_source() { + let program = parse_fragment(br#"static $n = 1 + 1;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StaticVar { + name: "n".to_string(), + init: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::Const(EvalConst::Int(1))), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }] + ); + } + /// Verifies eval magic constants lower to explicit EvalIR nodes with fragment line metadata. #[test] fn parse_fragment_accepts_magic_constants() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index f0075e15fa..50019bac00 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 630e2e0672..01e22548db 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,9 +32,9 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 93ca47e9ba..bc6ca291f0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1390,6 +1390,33 @@ echo dyn_eval_native(); assert_eq!(out, "42"); } +/// Verifies static locals in eval-declared functions persist between native calls. +#[test] +fn test_eval_declared_function_static_local_persists() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 03:25:51 +0200 Subject: [PATCH 0096/1208] Support eval global aliases --- crates/elephc-eval/src/context.rs | 20 ++ crates/elephc-eval/src/eval_ir.rs | 3 + crates/elephc-eval/src/interpreter.rs | 144 ++++++++++- crates/elephc-eval/src/lib.rs | 48 ++++ crates/elephc-eval/src/parser.rs | 31 +++ crates/elephc-eval/src/scope.rs | 19 +- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- src/codegen/eval_method_helpers.rs | 7 +- src/codegen/eval_property_helpers.rs | 7 +- src/codegen/frame.rs | 4 +- src/codegen/lower_inst/builtins/eval.rs | 322 +++++++++++++++++++++++- src/ir/function.rs | 1 + src/ir_lower/context.rs | 11 + tests/codegen/eval.rs | 34 +++ 15 files changed, 635 insertions(+), 24 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index e4545a68a0..67800de5b7 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -16,6 +16,7 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::EvalFunction; +use crate::scope::ElephcEvalScope; use crate::value::{RuntimeCell, RuntimeCellHandle}; /// Native descriptor-invoker ABI registered by generated code for AOT functions. @@ -88,6 +89,7 @@ pub struct ElephcEvalContext { functions: HashMap, native_functions: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, + global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, call_file: String, call_dir: String, @@ -102,6 +104,7 @@ impl ElephcEvalContext { functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), + global_scope: None, function_stack: Vec::new(), call_file: String::new(), call_dir: String::new(), @@ -117,6 +120,7 @@ impl ElephcEvalContext { functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), + global_scope: None, function_stack: Vec::new(), call_file: String::new(), call_dir: String::new(), @@ -204,6 +208,22 @@ impl ElephcEvalContext { previous.filter(|previous| *previous != cell) } + /// Stores the non-owned global scope handle used by eval `global` aliases. + pub fn set_global_scope(&mut self, scope: *mut ElephcEvalScope) -> bool { + if scope.is_null() { + self.global_scope = None; + false + } else { + self.global_scope = Some(scope); + true + } + } + + /// Returns the non-owned global scope handle for eval `global` aliases. + pub fn global_scope_ptr(&self) -> Option<*mut ElephcEvalScope> { + self.global_scope + } + /// Pushes an eval-executed function name for magic-constant resolution. pub fn push_function(&mut self, name: impl Into) { self.function_stack.push(name.into()); diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 70f73df4af..ecdedc2bf3 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -79,6 +79,9 @@ pub enum EvalStmt { params: Vec, body: Vec, }, + Global { + vars: Vec, + }, If { condition: EvalExpr, then_branch: Vec, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 997d013d2f..8f62029e80 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -18,7 +18,7 @@ use crate::eval_ir::{ EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; use crate::parser::parse_fragment; -use crate::scope::{ElephcEvalScope, ScopeCellOwnership}; +use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; /// Internal statement-control result used to propagate eval returns and loops. @@ -349,6 +349,88 @@ fn execute_statements( Ok(EvalControl::None) } +/// Returns the eval-visible entry for a variable, following `global` aliases. +fn scope_entry( + context: &ElephcEvalContext, + scope: &ElephcEvalScope, + name: &str, +) -> Option { + if !scope.is_global_alias(name) { + return scope.entry(name); + } + let Some(global_scope) = context.global_scope_ptr() else { + return scope.entry(name); + }; + let current_scope = scope as *const ElephcEvalScope as *mut ElephcEvalScope; + if global_scope == current_scope { + return scope.entry(name); + } + unsafe { global_scope.as_ref().and_then(|scope| scope.entry(name)) } +} + +/// Returns the eval-visible cell for a variable, following `global` aliases. +fn visible_scope_cell( + context: &ElephcEvalContext, + scope: &ElephcEvalScope, + name: &str, +) -> Option { + scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(ScopeEntry::cell) +} + +/// Stores a variable cell, redirecting `global` aliases to the global scope. +fn set_scope_cell( + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + name: impl Into, + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, +) -> Result, EvalStatus> { + let name = name.into(); + if scope.is_global_alias(&name) { + let Some(global_scope) = context.global_scope_ptr() else { + return Err(EvalStatus::RuntimeFatal); + }; + let current_scope = scope as *mut ElephcEvalScope; + if global_scope == current_scope { + return Ok(scope.set(name, cell, ownership)); + } + let Some(global_scope) = (unsafe { global_scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + return Ok(global_scope.set(name, cell, ownership)); + } + Ok(scope.set(name, cell, ownership)) +} + +/// Unsets a variable, removing only the local alias when the name is global. +fn unset_scope_cell( + scope: &mut ElephcEvalScope, + name: impl Into, +) -> Option { + let name = name.into(); + if scope.is_global_alias(&name) { + scope.clear_global_alias(&name); + } + scope.unset(name) +} + +/// Marks variables as aliases to the context global scope for later reads/writes. +fn execute_global_stmt( + vars: &[String], + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, +) -> Result<(), EvalStatus> { + if context.global_scope_ptr().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + for name in vars { + scope.mark_global_alias(name.clone()); + } + Ok(()) +} + /// Executes one statement and returns `Some` only for eval `return`. fn execute_stmt( stmt: &EvalStmt, @@ -360,7 +442,7 @@ fn execute_stmt( EvalStmt::ArrayAppendVar { name, value } => { let mut ownership = ScopeCellOwnership::Owned; let array = if let Some(existing) = - scope.entry(name).filter(|entry| entry.flags().is_visible()) + scope_entry(context, scope, name).filter(|entry| entry.flags().is_visible()) { if values.is_array_like(existing.cell())? { let tag = values.type_tag(existing.cell())?; @@ -378,7 +460,9 @@ fn execute_stmt( let index = eval_array_append_key(array, values)?; let value = eval_expr(value, context, scope, values)?; let array = values.array_set(array, index, value)?; - if let Some(replaced) = scope.set(name.clone(), array, ownership) { + if let Some(replaced) = + set_scope_cell(context, scope, name.clone(), array, ownership)? + { values.release(replaced)?; } Ok(EvalControl::None) @@ -386,7 +470,7 @@ fn execute_stmt( EvalStmt::ArraySetVar { name, index, value } => { let mut ownership = ScopeCellOwnership::Owned; let array = if let Some(existing) = - scope.entry(name).filter(|entry| entry.flags().is_visible()) + scope_entry(context, scope, name).filter(|entry| entry.flags().is_visible()) { if values.is_array_like(existing.cell())? { ownership = existing.flags().ownership; @@ -400,7 +484,9 @@ fn execute_stmt( let index = eval_expr(index, context, scope, values)?; let value = eval_expr(value, context, scope, values)?; let array = values.array_set(array, index, value)?; - if let Some(replaced) = scope.set(name.clone(), array, ownership) { + if let Some(replaced) = + set_scope_cell(context, scope, name.clone(), array, ownership)? + { values.release(replaced)?; } Ok(EvalControl::None) @@ -453,6 +539,10 @@ fn execute_stmt( .map_err(|_| EvalStatus::RuntimeFatal)?; Ok(EvalControl::None) } + EvalStmt::Global { vars } => { + execute_global_stmt(vars, context, scope)?; + Ok(EvalControl::None) + } EvalStmt::If { condition, then_branch, @@ -485,7 +575,9 @@ fn execute_stmt( } EvalStmt::StoreVar { name, value } => { let value = eval_expr(value, context, scope, values)?; - if let Some(replaced) = scope.set(name.clone(), value, ScopeCellOwnership::Owned) { + if let Some(replaced) = + set_scope_cell(context, scope, name.clone(), value, ScopeCellOwnership::Owned)? + { values.release(replaced)?; } Ok(EvalControl::None) @@ -494,7 +586,7 @@ fn execute_stmt( execute_switch_stmt(expr, cases, context, scope, values) } EvalStmt::UnsetVar { name } => { - if let Some(replaced) = scope.unset(name.clone()) { + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { values.release(replaced)?; } Ok(EvalControl::None) @@ -662,14 +754,16 @@ fn execute_foreach_stmt( let key = values.array_iter_key(array, index)?; let value = values.array_get(array, key)?; if let Some(key_name) = key_name { - if let Some(replaced) = scope.set(key_name.to_string(), key, ScopeCellOwnership::Owned) + if let Some(replaced) = + set_scope_cell(context, scope, key_name.to_string(), key, ScopeCellOwnership::Owned)? { values.release(replaced)?; } } else { values.release(key)?; } - if let Some(replaced) = scope.set(value_name.to_string(), value, ScopeCellOwnership::Owned) + if let Some(replaced) = + set_scope_cell(context, scope, value_name.to_string(), value, ScopeCellOwnership::Owned)? { values.release(replaced)?; } @@ -734,7 +828,9 @@ fn eval_expr( } EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), - EvalExpr::LoadVar(name) => scope.visible_cell(name).map_or_else(|| values.null(), Ok), + EvalExpr::LoadVar(name) => { + visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) + } EvalExpr::Magic(magic) => eval_magic_const(magic, context, values), EvalExpr::MethodCall { object, @@ -1062,7 +1158,7 @@ fn eval_empty_arg( values: &mut impl RuntimeValueOps, ) -> Result { if let EvalExpr::LoadVar(name) = arg { - let Some(value) = scope.visible_cell(name) else { + let Some(value) = visible_scope_cell(context, scope, name) else { return Ok(true); }; return Ok(!values.truthy(value)?); @@ -1079,7 +1175,7 @@ fn eval_isset_arg( values: &mut impl RuntimeValueOps, ) -> Result { if let EvalExpr::LoadVar(name) = arg { - let Some(value) = scope.visible_cell(name) else { + let Some(value) = visible_scope_cell(context, scope, name) else { return Ok(false); }; return Ok(!values.is_null(value)?); @@ -2822,6 +2918,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::Continue | EvalStmt::Echo(_) | EvalStmt::Expr(_) + | EvalStmt::Global { .. } | EvalStmt::PropertySet { .. } | EvalStmt::Return(_) | EvalStmt::StoreVar { .. } @@ -4721,6 +4818,29 @@ return (dyn() * 10) + dyn();"#, assert_eq!(values.get(second), FakeValue::Int(1)); } + /// Verifies `global` declarations read and write the context global scope. + #[test] + fn execute_program_global_alias_writes_context_global_scope() { + let program = parse_fragment(br#"global $g; $g = $g + 1; return $g;"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.get(global), FakeValue::Int(2)); + } + /// Verifies named calls reject positional arguments that follow named arguments. #[test] fn execute_program_rejects_positional_after_named_arg() { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 1995fc0294..b2b88d44db 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -85,6 +85,21 @@ pub unsafe extern "C" fn __elephc_eval_context_set_call_site( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Records the materialized program-global eval scope for `global` aliases. +/// +/// # Safety +/// `ctx` and `scope` must be valid handles allocated by the eval bridge. The +/// context does not own `scope`; generated code must keep the scope alive for +/// as long as the context can execute eval fragments that reference globals. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_set_global_scope( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_context_set_global_scope_inner(ctx, scope) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Allocates a materialized activation scope handle for generated code. #[no_mangle] pub extern "C" fn __elephc_eval_scope_new() -> *mut ElephcEvalScope { @@ -476,6 +491,27 @@ unsafe fn eval_context_set_call_site_inner( EvalStatus::Ok.code() } +/// Runs the global-scope setter ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_set_global_scope`; callers must pass valid +/// context and scope handles owned by generated code. +unsafe fn eval_context_set_global_scope_inner( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if !context.set_global_scope(scope) { + return EvalStatus::RuntimeFatal.code(); + } + EvalStatus::Ok.code() +} + /// Runs the dynamic function-call ABI body after installing a panic boundary. /// /// # Safety @@ -804,6 +840,18 @@ mod tests { assert_eq!(ctx.eval_file_magic(), "/tmp/source.php(9) : eval()'d code"); } + /// Verifies the context ABI records a non-owned global scope handle. + #[test] + fn context_set_global_scope_records_handle() { + let mut ctx = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + + let status = unsafe { __elephc_eval_context_set_global_scope(&mut ctx, &mut scope) }; + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!(ctx.global_scope_ptr(), Some(&mut scope as *mut ElephcEvalScope)); + } + /// Verifies the function-exists ABI probes eval-declared functions by folded name. #[test] fn function_exists_reports_declared_eval_function() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 486e3ec0a8..09ca27dded 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -571,6 +571,7 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), TokenKind::Ident(name) if ident_eq(name, "return") => { self.advance(); @@ -715,6 +716,24 @@ impl Parser { Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) } + /// Parses `global $name, $other;` declarations in eval fragments. + fn parse_global_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let mut vars = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + vars.push(name.clone()); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(vec![EvalStmt::Global { vars }]) + } + /// Parses `static $name = expr;` declarations in eval fragments. fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -2166,6 +2185,18 @@ mod tests { ); } + /// Verifies global declarations preserve source-order variable names. + #[test] + fn parse_fragment_accepts_global_source() { + let program = parse_fragment(br#"global $left, $right;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Global { + vars: vec!["left".to_string(), "right".to_string()], + }] + ); + } + /// Verifies eval magic constants lower to explicit EvalIR nodes with fragment line metadata. #[test] fn parse_fragment_accepts_magic_constants() { diff --git a/crates/elephc-eval/src/scope.rs b/crates/elephc-eval/src/scope.rs index 4e867ba42d..5feedbf8e2 100644 --- a/crates/elephc-eval/src/scope.rs +++ b/crates/elephc-eval/src/scope.rs @@ -11,7 +11,7 @@ //! - Scope entries store runtime-cell handles only; the eval bridge does not //! introduce a second PHP value representation. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::value::RuntimeCellHandle; @@ -116,6 +116,7 @@ impl ScopeEntry { /// Materialized activation scope passed opaquely across the eval ABI. pub struct ElephcEvalScope { entries: HashMap, + global_aliases: HashSet, generation: u64, } @@ -124,6 +125,7 @@ impl ElephcEvalScope { pub fn new() -> Self { Self { entries: HashMap::new(), + global_aliases: HashSet::new(), generation: 0, } } @@ -174,6 +176,21 @@ impl ElephcEvalScope { self.visible_cell(name).is_some() } + /// Marks a variable name as an alias to the eval context's global scope. + pub fn mark_global_alias(&mut self, name: impl Into) { + self.global_aliases.insert(name.into()); + } + + /// Removes a variable's global alias marker after local `unset()`. + pub fn clear_global_alias(&mut self, name: &str) { + self.global_aliases.remove(name); + } + + /// Returns true when the variable should resolve through the global scope. + pub fn is_global_alias(&self, name: &str) -> bool { + self.global_aliases.contains(name) + } + /// Returns the names of entries dirtied since the last synchronization. pub fn dirty_names(&self) -> Vec<&str> { self.entries diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 50019bac00..2d63e2c3ca 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus a materialized activation scope, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into that by-name scope, calls `__elephc_eval_execute`, then reloads locals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scope stores boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, `global $name` aliases for compiler-known program-global storage, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 01e22548db..0bc0894854 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,9 +32,9 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, `global $name` aliases for compiler-known global storage, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index cd2c0447a0..cfe9664a70 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -72,7 +72,12 @@ fn function_uses_eval(function: &Function) -> bool { function .locals .iter() - .any(|local| matches!(local.kind, LocalKind::EvalContext | LocalKind::EvalScope)) + .any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) } /// Collects public bridge-supported instance methods backed by emitted EIR symbols. diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index f12c372f0b..669542e312 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -69,7 +69,12 @@ fn function_uses_eval(function: &Function) -> bool { function .locals .iter() - .any(|local| matches!(local.kind, LocalKind::EvalContext | LocalKind::EvalScope)) + .any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) } /// Collects public declared properties with storage layouts the bridge can access. diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index 30e8f39bfc..e4f69b32ff 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -604,7 +604,7 @@ fn eval_scope_locals(ctx: &FunctionContext<'_>) -> Vec<(String, usize)> { .function .locals .iter() - .filter(|local| local.kind == LocalKind::EvalScope) + .filter(|local| matches!(local.kind, LocalKind::EvalScope | LocalKind::EvalGlobalScope)) .filter_map(|local| { let offset = ctx.local_offset(local.id).ok()?; let name = local @@ -643,7 +643,7 @@ fn function_has_eval_scope(function: &Function) -> bool { function .locals .iter() - .any(|local| local.kind == LocalKind::EvalScope) + .any(|local| matches!(local.kind, LocalKind::EvalScope | LocalKind::EvalGlobalScope)) } /// Returns true when a local slot is written by an explicit EIR `StoreLocal`. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 6a4d5a9dca..c3ebe50121 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -18,8 +18,8 @@ use crate::codegen::platform::Arch; use crate::codegen::{ abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, }; -use crate::ir::{Function, Instruction, LocalKind, LocalSlotId}; -use crate::names::function_symbol; +use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op}; +use crate::names::{function_symbol, ir_global_symbol}; use crate::types::{FunctionSig, PhpType}; use super::super::super::context::FunctionContext; @@ -34,13 +34,14 @@ const EVAL_PARSE_ERROR_MESSAGE: &str = "Parse error: eval() fragment is invalid\ const EVAL_UNSUPPORTED_MESSAGE: &str = "Fatal error: eval() fragment uses an unsupported construct\n"; const EVAL_RUNTIME_FATAL_MESSAGE: &str = "Fatal error: eval() runtime failed\n"; -const EVAL_STACK_BYTES: usize = 64; +const EVAL_STACK_BYTES: usize = 80; const EVAL_RESULT_VALUE_CELL_OFFSET: usize = 8; const EVAL_CONTEXT_HANDLE_OFFSET: usize = 24; const EVAL_SCOPE_HANDLE_OFFSET: usize = 32; const EVAL_TEMP_CELL_OFFSET: usize = 40; const EVAL_CODE_PTR_OFFSET: usize = 48; const EVAL_CODE_LEN_OFFSET: usize = 56; +const EVAL_GLOBAL_SCOPE_HANDLE_OFFSET: usize = 64; const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; @@ -52,6 +53,13 @@ struct EvalSyncLocal { ty: PhpType, } +/// Program-global metadata synchronized with eval `global` aliases. +#[derive(Clone)] +struct EvalSyncGlobal { + name: String, + ty: PhpType, +} + /// A module-local function that can be registered with the eval context. struct EvalNativeFunctionRegistration { name: String, @@ -75,8 +83,12 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R ensure_eval_context(ctx)?; set_eval_call_site(ctx, inst); ensure_eval_scope(ctx)?; + ensure_eval_global_scope(ctx)?; let sync_locals = eval_sync_locals(ctx); + let sync_globals = eval_sync_globals(ctx); flush_eval_scope_locals(ctx, &sync_locals)?; + flush_eval_global_scope(ctx, &sync_globals)?; + set_eval_context_global_scope(ctx); load_eval_context_to_arg(ctx, 0); load_eval_scope_to_arg(ctx, 1); move_saved_eval_code_to_eval_args(ctx); @@ -89,6 +101,7 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); reload_eval_scope_locals(ctx, &sync_locals)?; + reload_eval_global_scope(ctx, &sync_globals)?; abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); store_if_result(ctx, inst) @@ -497,6 +510,23 @@ fn ensure_eval_scope(ctx: &mut FunctionContext<'_>) -> Result<()> { Ok(()) } +/// Ensures a persistent eval global-scope exists and stores its handle in scratch. +fn ensure_eval_global_scope(ctx: &mut FunctionContext<'_>) -> Result<()> { + let slot = eval_global_scope_slot(ctx)?; + let offset = ctx.local_offset(slot)?; + let ready = ctx.next_label("eval_global_scope_ready"); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &ready); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_new"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::store_at_offset(ctx.emitter, result_reg, offset); + ctx.emitter.label(&ready); + abi::load_at_offset(ctx.emitter, result_reg, offset); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_GLOBAL_SCOPE_HANDLE_OFFSET); + Ok(()) +} + /// Returns the hidden frame slot that owns this function's persistent eval scope. fn eval_scope_slot(ctx: &FunctionContext<'_>) -> Result { ctx.function @@ -507,18 +537,47 @@ fn eval_scope_slot(ctx: &FunctionContext<'_>) -> Result { .ok_or_else(|| CodegenIrError::invalid_module("eval call missing eval scope local")) } +/// Returns the hidden frame slot that owns this function's eval global scope. +fn eval_global_scope_slot(ctx: &FunctionContext<'_>) -> Result { + ctx.function + .locals + .iter() + .find(|local| local.kind == LocalKind::EvalGlobalScope) + .map(|local| local.id) + .ok_or_else(|| CodegenIrError::invalid_module("eval call missing eval global scope local")) +} + /// Loads the current eval scope handle into the selected integer argument register. fn load_eval_scope_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); abi::emit_load_temporary_stack_slot(ctx.emitter, arg_reg, EVAL_SCOPE_HANDLE_OFFSET); } +/// Loads the current eval global-scope handle into the selected integer argument register. +fn load_eval_global_scope_to_arg(ctx: &mut FunctionContext<'_>, arg_index: usize) { + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + abi::emit_load_temporary_stack_slot(ctx.emitter, arg_reg, EVAL_GLOBAL_SCOPE_HANDLE_OFFSET); +} + +/// Installs the current eval global-scope handle into the eval context. +fn set_eval_context_global_scope(ctx: &mut FunctionContext<'_>) { + load_eval_context_to_arg(ctx, 0); + load_eval_global_scope_to_arg(ctx, 1); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_set_global_scope"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + /// Collects PHP-visible locals that the current conservative scope sync can round-trip. fn eval_sync_locals(ctx: &FunctionContext<'_>) -> Vec { ctx.function .locals .iter() .filter(|local| local.kind == LocalKind::PhpLocal) + .filter(|local| !local_uses_eval_global_sync(ctx, local.name.as_deref())) .filter_map(|local| { let name = local.name.clone()?; let ty = local.php_type.codegen_repr(); @@ -531,6 +590,89 @@ fn eval_sync_locals(ctx: &FunctionContext<'_>) -> Vec { .collect() } +/// Returns true when a local name is backed by program-global storage during eval. +fn local_uses_eval_global_sync(ctx: &FunctionContext<'_>, name: Option<&str>) -> bool { + ctx.is_main && name.is_some_and(|name| ctx.has_global_name(name)) +} + +/// Collects program globals that can be boxed into the eval global scope. +fn eval_sync_globals(ctx: &FunctionContext<'_>) -> Vec { + ctx.module + .data + .global_names + .iter() + .filter_map(|name| { + let ty = eval_sync_global_type(ctx, name)?; + eval_sync_global_type_supported(&ty).then_some(EvalSyncGlobal { + name: name.clone(), + ty, + }) + }) + .collect() +} + +/// Returns one unambiguous codegen type used for a program global, if available. +fn eval_sync_global_type(ctx: &FunctionContext<'_>, name: &str) -> Option { + let mut inferred = None; + for function in ctx.module.functions.iter().chain(ctx.module.closures.iter()) { + for inst in &function.instructions { + if global_instruction_name(ctx, inst) != Some(name) { + continue; + } + let candidate = global_instruction_value_type(function, inst)?; + let candidate = candidate.codegen_repr(); + if !eval_sync_global_type_supported(&candidate) { + return None; + } + match &inferred { + Some(existing) if existing != &candidate => return None, + Some(_) => {} + None => inferred = Some(candidate), + } + } + } + inferred +} + +/// Returns the global name referenced by a load/store-global instruction. +fn global_instruction_name<'a>( + ctx: &'a FunctionContext<'_>, + inst: &Instruction, +) -> Option<&'a str> { + let Some(Immediate::GlobalName(data)) = inst.immediate else { + return None; + }; + ctx.module.data.global_names.get(data.as_raw() as usize).map(String::as_str) +} + +/// Returns the value type carried by a global load or store instruction. +fn global_instruction_value_type(function: &Function, inst: &Instruction) -> Option { + match inst.op { + Op::LoadGlobal => { + let result = inst.result?; + function.value(result).map(|value| value.php_type.clone()) + } + Op::StoreGlobal => { + let value = *inst.operands.first()?; + function.value(value).map(|value| value.php_type.clone()) + } + _ => None, + } +} + +/// Returns true when a global type can round-trip through eval global scope sync. +fn eval_sync_global_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Union(_) + ) +} + /// Returns true when a local type can be boxed to Mixed and restored from Mixed after eval. fn eval_sync_type_supported(ty: &PhpType) -> bool { matches!( @@ -559,6 +701,31 @@ fn flush_eval_scope_locals(ctx: &mut FunctionContext<'_>, locals: &[EvalSyncLoca Ok(()) } +/// Flushes supported program globals into the eval global scope before eval. +fn flush_eval_global_scope( + ctx: &mut FunctionContext<'_>, + globals: &[EvalSyncGlobal], +) -> Result<()> { + for global in globals { + load_global_to_result(ctx, global); + if !matches!(global.ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &global.ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + emit_eval_global_scope_set(ctx, global, scope_set_flags_for_type(&global.ty)); + } + Ok(()) +} + +/// Loads a program-global symbol into result registers using its inferred type. +fn load_global_to_result(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal) { + let symbol = ir_global_symbol(&global.name); + let ty = global.ty.codegen_repr(); + ctx.data.add_comm(symbol.clone(), ty.stack_size().max(8)); + abi::emit_load_symbol_to_result(ctx.emitter, &symbol, &ty); +} + /// Returns ABI flags for a scope value produced from the given native type. fn scope_set_flags_for_type(ty: &PhpType) -> i64 { if matches!(ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { @@ -568,6 +735,32 @@ fn scope_set_flags_for_type(ty: &PhpType) -> i64 { } } +/// Calls `__elephc_eval_scope_set` for one boxed global value. +fn emit_eval_global_scope_set(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(global.name.as_bytes()); + load_eval_global_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + /// Calls `__elephc_eval_scope_set` for one boxed local value. fn emit_eval_scope_set(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal, flags: i64) { let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); @@ -612,6 +805,27 @@ fn reload_eval_scope_locals(ctx: &mut FunctionContext<'_>, locals: &[EvalSyncLoc Ok(()) } +/// Reloads synchronized program globals from the eval global scope after eval. +fn reload_eval_global_scope( + ctx: &mut FunctionContext<'_>, + globals: &[EvalSyncGlobal], +) -> Result<()> { + for global in globals { + emit_eval_global_scope_get(ctx, global); + let missing = ctx.next_label("eval_global_reload_missing"); + let done = ctx.next_label("eval_global_reload_done"); + emit_branch_if_scope_entry_missing(ctx, &missing); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 0); + store_mixed_scope_cell_to_global(ctx, global)?; + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&missing); + store_missing_scope_entry_to_global(ctx, global)?; + ctx.emitter.label(&done); + } + Ok(()) +} + /// Calls `__elephc_eval_scope_get` and stores out cell/flags at the start of eval scratch. fn emit_eval_scope_get(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal) { let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); @@ -632,6 +846,26 @@ fn emit_eval_scope_get(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal) { emit_eval_status_check(ctx); } +/// Calls `__elephc_eval_scope_get` for one program global. +fn emit_eval_global_scope_get(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal) { + let (name_label, name_len) = ctx.data.add_string(global.name.as_bytes()); + load_eval_global_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, 0); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, 8); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + /// Branches to `label` when the latest scope-get flags do not mark a visible value. fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str) { let flags_reg = abi::secondary_scratch_reg(ctx.emitter); @@ -703,6 +937,45 @@ fn store_mixed_scope_cell_to_local( Ok(()) } +/// Converts a scope Mixed cell back to a program-global storage symbol. +fn store_mixed_scope_cell_to_global( + ctx: &mut FunctionContext<'_>, + global: &EvalSyncGlobal, +) -> Result<()> { + let symbol = ir_global_symbol(&global.name); + let ty = global.ty.codegen_repr(); + ctx.data.add_comm(symbol.clone(), ty.stack_size().max(8)); + match ty { + PhpType::Mixed | PhpType::Union(_) => { + emit_retain_scope_cell_if_owned(ctx); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Mixed, false); + } + PhpType::Int => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Int, false); + } + PhpType::Bool => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Bool, false); + } + PhpType::Float => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Float, false); + } + PhpType::Str => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Str, false); + } + other => { + return Err(CodegenIrError::unsupported(format!( + "eval global reload for PHP type {:?}", + other + ))) + } + } + Ok(()) +} + /// Retains a scope-owned Mixed cell before storing it into a native local owner. fn emit_retain_scope_cell_if_owned(ctx: &mut FunctionContext<'_>) { let flags_reg = abi::secondary_scratch_reg(ctx.emitter); @@ -766,6 +1039,49 @@ fn store_missing_scope_entry_to_local( Ok(()) } +/// Stores the program-global fallback for a missing eval global entry. +fn store_missing_scope_entry_to_global( + ctx: &mut FunctionContext<'_>, + global: &EvalSyncGlobal, +) -> Result<()> { + let symbol = ir_global_symbol(&global.name); + let ty = global.ty.codegen_repr(); + ctx.data.add_comm(symbol.clone(), ty.stack_size().max(8)); + match ty { + PhpType::Mixed | PhpType::Union(_) => { + let symbol_name = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol_name); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Mixed, false); + } + PhpType::Int => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Int, false); + } + PhpType::Bool => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Bool, false); + } + PhpType::Float => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_int_result_to_float_result(ctx.emitter); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Float, false); + } + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Str, false); + } + other => { + return Err(CodegenIrError::unsupported(format!( + "eval global missing reload for PHP type {:?}", + other + ))) + } + } + Ok(()) +} + /// Emits a fatal diagnostic when the eval bridge reports any non-zero status. fn emit_eval_status_check(ctx: &mut FunctionContext<'_>) { let ok_label = ctx.next_label("eval_status_ok"); diff --git a/src/ir/function.rs b/src/ir/function.rs index 2c3cee5e13..4af360e57c 100644 --- a/src/ir/function.rs +++ b/src/ir/function.rs @@ -181,6 +181,7 @@ pub enum LocalKind { GeneratorState, EvalContext, EvalScope, + EvalGlobalScope, } /// Function-level shape flags used by lowering and later codegen. diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index b4e4bcd7b7..22f9ae743f 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -90,6 +90,7 @@ pub(crate) struct ClosureCapture { const EVAL_CONTEXT_LOCAL_NAME: &str = "__eir_eval_context"; const EVAL_SCOPE_LOCAL_NAME: &str = "__eir_eval_scope"; +const EVAL_GLOBAL_SCOPE_LOCAL_NAME: &str = "__eir_eval_global_scope"; /// Mutable state for one function body while it is lowered. pub(crate) struct LoweringContext<'m, 'f> { @@ -468,11 +469,21 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.declare_local_with_kind(EVAL_SCOPE_LOCAL_NAME, PhpType::Int, LocalKind::EvalScope) } + /// Ensures this function has a persistent eval global-scope handle slot. + pub(crate) fn declare_eval_global_scope_local(&mut self) -> LocalSlotId { + self.declare_local_with_kind( + EVAL_GLOBAL_SCOPE_LOCAL_NAME, + PhpType::Int, + LocalKind::EvalGlobalScope, + ) + } + /// Applies the static part of the eval barrier to visible PHP local storage. pub(crate) fn apply_eval_barrier(&mut self) { self.eval_barrier_active = true; self.declare_eval_context_local(); self.declare_eval_scope_local(); + self.declare_eval_global_scope_local(); let local_names = self .local_slots .iter() diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bc6ca291f0..27edb4caab 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1417,6 +1417,40 @@ eval('static $n = 0; $n++; echo $n;'); assert_eq!(out, "1:1"); } +/// Verifies `global` inside eval can write compiler-known global storage. +#[test] +fn test_eval_global_alias_updates_global_storage() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 03:41:02 +0200 Subject: [PATCH 0097/1208] Support eval CLI argument globals --- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- src/codegen/frame.rs | 72 ++++++++++++++++++++----- src/codegen/lower_inst/builtins/eval.rs | 61 ++++++++++++++++----- src/ir_lower/context.rs | 14 +++++ tests/codegen/eval.rs | 37 +++++++++++++ 6 files changed, 162 insertions(+), 30 deletions(-) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2d63e2c3ca..505deaff4c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -15,9 +15,9 @@ These routines end up in every compiled binary. In the CLI flow they are usually Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, `global $name` aliases for compiler-known program-global storage, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 0bc0894854..5159eed461 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,9 +32,9 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, `global $name` aliases for compiler-known global storage, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index e4f69b32ff..32d5799f20 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -162,12 +162,14 @@ pub(super) fn emit_main_prologue(ctx: &mut FunctionContext<'_>) { ctx.emitter.comment("enable heap debug flag"); abi::emit_enable_heap_debug_flag(ctx.emitter); } - store_argc_local_if_present(ctx); - store_argv_local_if_present(ctx); zero_initialize_main_cleanup_locals(ctx); zero_initialize_ref_cell_owner_locals(ctx); zero_initialize_eval_context_locals(ctx); zero_initialize_eval_scope_locals(ctx); + store_argc_global_if_needed(ctx); + store_argv_global_if_needed(ctx); + store_argc_local_if_present(ctx); + store_argv_local_if_present(ctx); } /// Emits a callable function prologue using an already-resolved entry label. @@ -1095,40 +1097,86 @@ fn align_to_16(bytes: usize) -> usize { /// Stores the OS argument count into `$argc` when the EIR main function has that local. fn store_argc_local_if_present(ctx: &mut FunctionContext<'_>) { - let Some(argc_slot) = ctx + let Some((argc_slot, argc_ty)) = ctx .function .locals .iter() .find(|local| local.name.as_deref() == Some("argc")) - .map(|local| local.id) + .map(|local| (local.id, local.php_type.codegen_repr())) else { return; }; let Ok(offset) = ctx.local_offset(argc_slot) else { return; }; - abi::store_at_offset( - ctx.emitter, - abi::process_argc_reg(ctx.emitter.target), - offset, - ); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, result_reg, "_global_argc", 0); + if matches!(argc_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Int); + } + abi::store_at_offset(ctx.emitter, result_reg, offset); } /// Builds and stores the PHP `$argv` array when the EIR main function has that local. fn store_argv_local_if_present(ctx: &mut FunctionContext<'_>) { - let Some(argv_slot) = ctx + let Some((argv_slot, argv_ty)) = ctx .function .locals .iter() .find(|local| local.name.as_deref() == Some("argv")) - .map(|local| local.id) + .map(|local| (local.id, local.php_type.codegen_repr())) else { return; }; let Ok(offset) = ctx.local_offset(argv_slot) else { return; }; + let array_ty = argv_array_type(); ctx.emitter.comment("build $argv array from OS argv"); abi::emit_call_label(ctx.emitter, "__rt_build_argv"); - abi::emit_store(ctx.emitter, &PhpType::Array(Box::new(PhpType::Str)), offset); + if matches!(argv_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &array_ty); + } + abi::emit_store(ctx.emitter, &argv_ty, offset); +} + +/// Initializes program-global `$argc` storage for eval or static `global $argc`. +fn store_argc_global_if_needed(ctx: &mut FunctionContext<'_>) { + if !superglobal_storage_needed(ctx, "argc") { + return; + } + let symbol = ir_global_symbol("argc"); + ctx.data.add_comm(symbol.clone(), PhpType::Int.stack_size().max(8)); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, result_reg, "_global_argc", 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Int, false); +} + +/// Initializes program-global `$argv` storage for eval or static `global $argv`. +fn store_argv_global_if_needed(ctx: &mut FunctionContext<'_>) { + if !superglobal_storage_needed(ctx, "argv") { + return; + } + let symbol = ir_global_symbol("argv"); + let array_ty = argv_array_type(); + ctx.data.add_comm(symbol.clone(), array_ty.stack_size().max(8)); + ctx.emitter.comment("build global $argv array from OS argv"); + abi::emit_call_label(ctx.emitter, "__rt_build_argv"); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &array_ty, false); +} + +/// Returns true when a process superglobal needs program-global storage. +fn superglobal_storage_needed(ctx: &FunctionContext<'_>, name: &str) -> bool { + ctx.module.required_runtime_features.eval + || ctx + .module + .data + .global_names + .iter() + .any(|candidate| candidate == name) +} + +/// Returns the PHP storage type for `$argv`. +fn argv_array_type() -> PhpType { + PhpType::Array(Box::new(PhpType::Str)) } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index c3ebe50121..ee573333fa 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -597,7 +597,8 @@ fn local_uses_eval_global_sync(ctx: &FunctionContext<'_>, name: Option<&str>) -> /// Collects program globals that can be boxed into the eval global scope. fn eval_sync_globals(ctx: &FunctionContext<'_>) -> Vec { - ctx.module + let mut globals = ctx + .module .data .global_names .iter() @@ -608,7 +609,21 @@ fn eval_sync_globals(ctx: &FunctionContext<'_>) -> Vec { ty, }) }) - .collect() + .collect::>(); + push_eval_process_superglobal(&mut globals, "argc", PhpType::Int); + push_eval_process_superglobal(&mut globals, "argv", PhpType::Array(Box::new(PhpType::Str))); + globals +} + +/// Adds a process superglobal to eval global sync unless normal globals already include it. +fn push_eval_process_superglobal(globals: &mut Vec, name: &str, ty: PhpType) { + if globals.iter().any(|global| global.name == name) { + return; + } + globals.push(EvalSyncGlobal { + name: name.to_string(), + ty, + }); } /// Returns one unambiguous codegen type used for a program global, if available. @@ -668,6 +683,8 @@ fn eval_sync_global_type_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Array(_) + | PhpType::AssocArray { .. } | PhpType::Mixed | PhpType::Union(_) ) @@ -874,12 +891,12 @@ fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local } } } @@ -945,7 +962,7 @@ fn store_mixed_scope_cell_to_global( let symbol = ir_global_symbol(&global.name); let ty = global.ty.codegen_repr(); ctx.data.add_comm(symbol.clone(), ty.stack_size().max(8)); - match ty { + match &ty { PhpType::Mixed | PhpType::Union(_) => { emit_retain_scope_cell_if_owned(ctx); abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Mixed, false); @@ -966,6 +983,18 @@ fn store_mixed_scope_cell_to_global( abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Str, false); } + PhpType::Array(_) | PhpType::AssocArray { .. } => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + let payload_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x1", + Arch::X86_64 => "rdi", + }; + let result_reg = abi::int_result_reg(ctx.emitter); + ctx.emitter + .instruction(&format!("mov {}, {}", result_reg, payload_reg)); // move the unboxed array payload into the ABI result register + abi::emit_incref_if_refcounted(ctx.emitter, &ty); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &ty, false); + } other => { return Err(CodegenIrError::unsupported(format!( "eval global reload for PHP type {:?}", @@ -985,12 +1014,12 @@ fn emit_retain_scope_cell_if_owned(ctx: &mut FunctionContext<'_>) { Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining } } abi::emit_call_label(ctx.emitter, "__rt_incref"); @@ -1047,7 +1076,7 @@ fn store_missing_scope_entry_to_global( let symbol = ir_global_symbol(&global.name); let ty = global.ty.codegen_repr(); ctx.data.add_comm(symbol.clone(), ty.stack_size().max(8)); - match ty { + match &ty { PhpType::Mixed | PhpType::Union(_) => { let symbol_name = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); abi::emit_call_label(ctx.emitter, &symbol_name); @@ -1072,6 +1101,10 @@ fn store_missing_scope_entry_to_global( abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &PhpType::Str, false); } + PhpType::Array(_) | PhpType::AssocArray { .. } => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_store_result_to_symbol(ctx.emitter, &symbol, &ty, false); + } other => { return Err(CodegenIrError::unsupported(format!( "eval global missing reload for PHP type {:?}", @@ -1105,12 +1138,12 @@ fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: Arch::AArch64 => { ctx.emitter .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler } Arch::X86_64 => { ctx.emitter .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler } } } @@ -1120,7 +1153,7 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr + ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); ctx.emitter @@ -1129,12 +1162,12 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { abi::emit_exit(ctx.emitter, 1); } Arch::X86_64 => { - ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr + ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); ctx.emitter .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length - ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting abi::emit_exit(ctx.emitter, 1); } } diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 22f9ae743f..a68893d4d0 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -91,6 +91,8 @@ pub(crate) struct ClosureCapture { const EVAL_CONTEXT_LOCAL_NAME: &str = "__eir_eval_context"; const EVAL_SCOPE_LOCAL_NAME: &str = "__eir_eval_scope"; const EVAL_GLOBAL_SCOPE_LOCAL_NAME: &str = "__eir_eval_global_scope"; +const EVAL_ARGC_LOCAL_NAME: &str = "argc"; +const EVAL_ARGV_LOCAL_NAME: &str = "argv"; /// Mutable state for one function body while it is lowered. pub(crate) struct LoweringContext<'m, 'f> { @@ -484,6 +486,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.declare_eval_context_local(); self.declare_eval_scope_local(); self.declare_eval_global_scope_local(); + self.declare_eval_main_superglobals(); let local_names = self .local_slots .iter() @@ -513,6 +516,17 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } } + /// Ensures top-level eval fragments can see `$argc` and `$argv` by name. + fn declare_eval_main_superglobals(&mut self) { + if !self.in_main { + return; + } + self.declare_local(EVAL_ARGC_LOCAL_NAME, PhpType::Int); + self.mark_local_initialized(EVAL_ARGC_LOCAL_NAME); + self.declare_local(EVAL_ARGV_LOCAL_NAME, PhpType::Array(Box::new(PhpType::Str))); + self.mark_local_initialized(EVAL_ARGV_LOCAL_NAME); + } + /// Returns true after this function has lowered an `eval()` call. pub(crate) const fn has_eval_barrier(&self) -> bool { self.eval_barrier_active diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 27edb4caab..09fa209593 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1451,6 +1451,43 @@ echo $g; assert_eq!(out, "1"); } +/// Verifies top-level eval fragments can read CLI `$argc` and `$argv`. +#[test] +fn test_eval_top_level_reads_argc_argv() { + let out = compile_and_run( + r#" 0 ? "Y" : "N");'); +"#, + ); + assert_eq!(out, "1:1:Y"); +} + +/// Verifies top-level eval can replace `$argc` after the eval barrier widens it. +#[test] +fn test_eval_top_level_can_replace_argc_type() { + let out = compile_and_run( + r#" 0 ? "Y" : "N");'); +} +show_eval_process_args(); +"#, + ); + assert_eq!(out, "1:1:Y"); +} + /// Verifies functions declared by eval from a namespace are registered globally. #[test] fn test_eval_declared_function_in_namespace_is_global() { From 483d7ebb81041edabd3f23a9da938220c6cfdbe0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 03:46:26 +0200 Subject: [PATCH 0098/1208] Cover eval optimizer barrier --- src/optimize/tests/effects/basic_calls.rs | 16 +++++++++ src/optimize/tests/propagate/straight_line.rs | 27 ++++++++++++++ .../constant_propagation/straight_line.rs | 36 +++++++++++++++++++ .../optimizer/dead_code_elimination/basics.rs | 35 ++++++++++++++++++ 4 files changed, 114 insertions(+) diff --git a/src/optimize/tests/effects/basic_calls.rs b/src/optimize/tests/effects/basic_calls.rs index 33e62e44c6..423e44abc2 100644 --- a/src/optimize/tests/effects/basic_calls.rs +++ b/src/optimize/tests/effects/basic_calls.rs @@ -27,6 +27,22 @@ fn test_effect_analysis_recognizes_pure_builtin_calls() { assert!(!expr_is_observable(&expr)); } +/// Verifies that `eval` is modeled as an observable, throwing dynamic barrier. +#[test] +fn test_effect_analysis_treats_eval_as_dynamic_barrier() { + let expr = Expr::new( + ExprKind::FunctionCall { + name: Name::from("eval"), + args: vec![Expr::string_lit("$x = 5;")], + }, + Span::dummy(), + ); + + assert!(expr_has_side_effects(&expr)); + assert!(expr_effect(&expr).may_throw); + assert!(expr_is_observable(&expr)); +} + /// Verifies that property accesses (`.`) are pure (no side effects) but may /// throw (uninitialized typed property guard), while array accesses (`[]`) /// are observable and may throw (e.g., undefined index). diff --git a/src/optimize/tests/propagate/straight_line.rs b/src/optimize/tests/propagate/straight_line.rs index 362262794c..a6e6e75012 100644 --- a/src/optimize/tests/propagate/straight_line.rs +++ b/src/optimize/tests/propagate/straight_line.rs @@ -89,6 +89,33 @@ fn test_propagate_constants_invalidates_non_scalar_reassignment() { ); } +/// Tests that `eval` invalidates all prior scalar facts because it can read, +/// write, create, or unset visible variables in the caller scope. +#[test] +fn test_propagate_constants_stops_at_eval_barrier() { + let program = vec![ + Stmt::assign("x", Expr::int_lit(2)), + Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::FunctionCall { + name: Name::from("eval"), + args: vec![Expr::string_lit("$x = 5;")], + }, + Span::dummy(), + )), + Span::dummy(), + ), + Stmt::echo(Expr::binop(Expr::var("x"), BinOp::Add, Expr::int_lit(1))), + ]; + + let propagated = propagate_constants(program); + + assert_eq!( + propagated[2], + Stmt::echo(Expr::binop(Expr::var("x"), BinOp::Add, Expr::int_lit(1))) + ); +} + /// Tests that when both branches of a ternary expression are the same constant, /// the resulting assignment is treated as a uniform constant. `base = flag ? 2 : 2` /// always yields `2`, so `base ** 3` folds to `8.0`. diff --git a/tests/codegen/optimizer/constant_propagation/straight_line.rs b/tests/codegen/optimizer/constant_propagation/straight_line.rs index 80975e702b..ab51e15ba8 100644 --- a/tests/codegen/optimizer/constant_propagation/straight_line.rs +++ b/tests/codegen/optimizer/constant_propagation/straight_line.rs @@ -45,6 +45,42 @@ echo $x ** $y; let _ = fs::remove_dir_all(&dir); } +/// Verifies that constant propagation does not substitute a scalar fact across +/// `eval`, because the fragment may replace the caller's visible local. +#[test] +fn test_constant_propagation_stops_at_eval_barrier() { + let dir = make_cli_test_dir("elephc_constant_propagation_eval_barrier"); + let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options( + r#" "a", 1 => "shadowed") produces /// output "a!" with "shadowed" absent from user assembly. #[test] From 116cc25b96d7be87d1cd20631a49fccd67ee9abc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 03:50:31 +0200 Subject: [PATCH 0099/1208] Add eval scope examples --- examples/eval-globals/.gitignore | 3 +++ examples/eval-globals/main.php | 15 +++++++++++++++ examples/eval/main.php | 9 +++++++++ 3 files changed, 27 insertions(+) create mode 100644 examples/eval-globals/.gitignore create mode 100644 examples/eval-globals/main.php diff --git a/examples/eval-globals/.gitignore b/examples/eval-globals/.gitignore new file mode 100644 index 0000000000..fc9576aeb9 --- /dev/null +++ b/examples/eval-globals/.gitignore @@ -0,0 +1,3 @@ +*.s +*.o +main diff --git a/examples/eval-globals/main.php b/examples/eval-globals/main.php new file mode 100644 index 0000000000..7e234ded12 --- /dev/null +++ b/examples/eval-globals/main.php @@ -0,0 +1,15 @@ + 0 ? "argc" : "no-argc") . ":" . (count($argv) > 0 ? "argv" : "no-argv");'); +} + class EvalCounter { public int $value = 1; @@ -56,9 +60,12 @@ public function echoLabelSpreadThroughEval(): void { $meta = eval('return ["source" => "eval"];'); $meta_count = eval('return count($meta);'); eval('function plus_one($value) { return $value + 1; }'); +eval('function eval_example_counter() { static $n = 0; $n++; return $n; }'); $dynamic_call = eval('return plus_one(4);'); $dynamic_named = eval('function named_pair($left, $right) { return $left . ":" . $right; } return named_pair(right: "R", left: "L");'); $dynamic_spread = eval('function spread_pair($left, $right) { return $left . ":" . $right; } return spread_pair(...["L", "R"]);'); +$static_first = eval('return eval_example_counter();'); +$static_second = eval('return eval_example_counter();'); $dynamic_cuf = eval('return call_user_func("plus_one", 6);'); $dynamic_cufa = eval('return call_user_func_array("plus_one", [8]);'); $eval_native_call = eval('return compiled_add(2, 8);'); @@ -126,6 +133,7 @@ public function echoLabelSpreadThroughEval(): void { echo "dynamic-call=" . $dynamic_call . "\n"; echo "dynamic-named=" . $dynamic_named . "\n"; echo "dynamic-spread=" . $dynamic_spread . "\n"; +echo "static-counter=" . $static_first . ":" . $static_second . "\n"; echo "dynamic-cuf=" . $dynamic_cuf . "\n"; echo "dynamic-cufa=" . $dynamic_cufa . "\n"; echo "eval-native-call=" . $eval_native_call . "\n"; @@ -188,4 +196,5 @@ public function echoLabelSpreadThroughEval(): void { echo "native-dynamic-call=" . native_add(40, 2) . "\n"; echo "call-user-func=" . call_user_func('native_double', 6) . "\n"; echo "function-exists=" . (function_exists('native_double') ? "yes" : "no") . "\n"; +echo "arg-globals=" . eval_arg_summary() . "\n"; echo "result=" . $result . "\n"; From 250284dd8374c3119f333780c480c330d78cf21d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 03:51:23 +0200 Subject: [PATCH 0100/1208] Cover eval global post-read --- tests/codegen/eval.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 09fa209593..a13320b267 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1434,6 +1434,24 @@ echo $g; assert_eq!(out, "2"); } +/// Verifies a function can read a global alias after eval mutates that global. +#[test] +fn test_eval_global_alias_read_after_eval_in_same_function() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 03:52:22 +0200 Subject: [PATCH 0101/1208] Cover eval unsupported constructs --- tests/codegen/eval.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a13320b267..b1eb8af411 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1877,6 +1877,26 @@ eval('function dyn_eval_dup() { return 2; }'); ); } +/// Verifies unsupported eval class declarations fail through the eval diagnostic path. +#[test] +fn test_eval_unsupported_class_declaration_fails() { + let err = compile_and_run_expect_failure(" Date: Tue, 16 Jun 2026 03:57:30 +0200 Subject: [PATCH 0102/1208] Report eval unsupported constructs --- crates/elephc-eval/src/errors.rs | 37 ++++++++++++++++++++++ crates/elephc-eval/src/interpreter.rs | 4 +-- crates/elephc-eval/src/lib.rs | 5 +-- crates/elephc-eval/src/parser.rs | 44 +++++++++++++++++++++++++++ tests/codegen/eval.rs | 8 ++--- 5 files changed, 90 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/errors.rs b/crates/elephc-eval/src/errors.rs index 847bcf7af1..08460ef7c7 100644 --- a/crates/elephc-eval/src/errors.rs +++ b/crates/elephc-eval/src/errors.rs @@ -38,6 +38,7 @@ impl EvalStatus { pub enum EvalParseError { PhpOpenTag, InvalidUtf8, + UnsupportedConstruct, UnexpectedToken, UnexpectedEof, InvalidNumber, @@ -46,3 +47,39 @@ pub enum EvalParseError { ExpectedVariable, ExpectedSemicolon, } + +impl EvalParseError { + /// Returns the ABI status that should be reported for this parse failure. + pub const fn status(self) -> EvalStatus { + match self { + Self::UnsupportedConstruct => EvalStatus::UnsupportedConstruct, + Self::PhpOpenTag + | Self::InvalidUtf8 + | Self::UnexpectedToken + | Self::UnexpectedEof + | Self::InvalidNumber + | Self::UnterminatedString + | Self::UnterminatedComment + | Self::ExpectedVariable + | Self::ExpectedSemicolon => EvalStatus::ParseError, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies only known unsupported syntax maps to the unsupported ABI status. + #[test] + fn parse_error_status_distinguishes_unsupported_constructs() { + assert_eq!( + EvalParseError::UnsupportedConstruct.status(), + EvalStatus::UnsupportedConstruct + ); + assert_eq!( + EvalParseError::UnexpectedToken.status(), + EvalStatus::ParseError + ); + } +} diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 8f62029e80..9b528f0422 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -12,7 +12,7 @@ //! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. use crate::context::{ElephcEvalContext, NativeFunction}; -use crate::errors::EvalStatus; +use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, @@ -2610,7 +2610,7 @@ fn eval_nested_eval( }; let code = eval_expr(code, context, scope, values)?; let code = values.string_bytes(code)?; - let program = parse_fragment(&code).map_err(|_| EvalStatus::ParseError)?; + let program = parse_fragment(&code).map_err(EvalParseError::status)?; execute_program_with_context(context, &program, scope, values) } diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index b2b88d44db..3a81668fea 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -644,8 +644,9 @@ unsafe fn execute_eval_inner( } else { slice::from_raw_parts(code_ptr, code_len) }; - let Ok(program) = parser::parse_fragment(code) else { - return EvalStatus::ParseError.code(); + let program = match parser::parse_fragment(code) { + Ok(program) => program, + Err(err) => return err.status().code(), }; if !out.is_null() { (*out).clear(); diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 09ca27dded..33a6035277 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -586,6 +586,9 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), + TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(true) @@ -842,6 +845,9 @@ impl Parser { return Err(EvalParseError::UnexpectedToken); }; self.advance(); + if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { + return Err(EvalParseError::UnsupportedConstruct); + } let value = self.parse_expr()?; if require_semicolon { self.expect_semicolon()?; @@ -1696,6 +1702,26 @@ fn ident_eq(actual: &str, expected: &str) -> bool { actual.eq_ignore_ascii_case(expected) } +/// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. +fn is_unsupported_statement_keyword(name: &str) -> bool { + [ + "class", + "enum", + "interface", + "namespace", + "require", + "require_once", + "include", + "include_once", + "throw", + "trait", + "try", + "use", + ] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + #[cfg(test)] mod tests { use super::*; @@ -2879,6 +2905,24 @@ mod tests { ); } + /// Verifies unsupported class declarations report the unsupported construct status. + #[test] + fn parse_fragment_rejects_class_as_unsupported_construct() { + assert_eq!( + parse_fragment(b"class DynEvalUnsupported {}"), + Err(EvalParseError::UnsupportedConstruct) + ); + } + + /// Verifies unsupported reference assignments report the unsupported construct status. + #[test] + fn parse_fragment_rejects_reference_assignment_as_unsupported_construct() { + assert_eq!( + parse_fragment(b"$left =& $right;"), + Err(EvalParseError::UnsupportedConstruct) + ); + } + /// Verifies malformed statements report parse errors instead of partial IR. #[test] fn parse_fragment_rejects_missing_semicolon() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b1eb8af411..7905e9d07d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1882,8 +1882,8 @@ eval('function dyn_eval_dup() { return 2; }'); fn test_eval_unsupported_class_declaration_fails() { let err = compile_and_run_expect_failure(" Date: Tue, 16 Jun 2026 03:59:11 +0200 Subject: [PATCH 0103/1208] Classify eval dynamic new as unsupported --- crates/elephc-eval/src/parser.rs | 20 ++++++++++++++++++++ tests/codegen/eval.rs | 15 +++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 33a6035277..bd0f4b2036 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1470,6 +1470,9 @@ impl Parser { let expr = self.parse_expr()?; Ok(EvalExpr::Print(Box::new(expr))) } + TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { self.parse_call_expr(name.clone()) } @@ -1713,6 +1716,7 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { "require_once", "include", "include_once", + "new", "throw", "trait", "try", @@ -1722,6 +1726,13 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { .any(|keyword| ident_eq(name, keyword)) } +/// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. +fn is_unsupported_expression_keyword(name: &str) -> bool { + ["clone", "match", "new", "yield"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + #[cfg(test)] mod tests { use super::*; @@ -2914,6 +2925,15 @@ mod tests { ); } + /// Verifies unsupported dynamic object construction reports the unsupported construct status. + #[test] + fn parse_fragment_rejects_new_as_unsupported_construct() { + assert_eq!( + parse_fragment(b"return new DynEvalUnsupported();"), + Err(EvalParseError::UnsupportedConstruct) + ); + } + /// Verifies unsupported reference assignments report the unsupported construct status. #[test] fn parse_fragment_rejects_reference_assignment_as_unsupported_construct() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7905e9d07d..c17cd71aef 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1887,6 +1887,21 @@ fn test_eval_unsupported_class_declaration_fails() { ); } +/// Verifies unsupported eval object construction fails through the eval diagnostic path. +#[test] +fn test_eval_unsupported_dynamic_new_fails() { + let err = compile_and_run_expect_failure( + r#" Date: Tue, 16 Jun 2026 04:00:10 +0200 Subject: [PATCH 0104/1208] Cover eval unsupported expression keywords --- crates/elephc-eval/src/parser.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index bd0f4b2036..4740a3026f 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -2934,6 +2934,21 @@ mod tests { ); } + /// Verifies unsupported expression keywords report the unsupported construct status. + #[test] + fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { + for source in [ + b"return clone $value;" as &[u8], + b"return match ($value) { 1 => 2 };" as &[u8], + b"return yield 1;" as &[u8], + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } + } + /// Verifies unsupported reference assignments report the unsupported construct status. #[test] fn parse_fragment_rejects_reference_assignment_as_unsupported_construct() { From 64d4d3d637317e158cfa3be5d2cb3ca7471feb86 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 04:08:02 +0200 Subject: [PATCH 0105/1208] Support eval reference aliases --- crates/elephc-eval/src/eval_ir.rs | 4 + crates/elephc-eval/src/interpreter.rs | 108 +++++++++--- crates/elephc-eval/src/parser.rs | 36 ++-- crates/elephc-eval/src/scope.rs | 240 ++++++++++++++++++++++++++ tests/codegen/eval.rs | 15 +- 5 files changed, 363 insertions(+), 40 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index ecdedc2bf3..fdb9720eaa 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -88,6 +88,10 @@ pub enum EvalStmt { else_branch: Vec, }, Return(Option), + ReferenceAssign { + target: String, + source: String, + }, PropertySet { object: EvalExpr, property: String, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 9b528f0422..100aa235f0 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -386,7 +386,7 @@ fn set_scope_cell( name: impl Into, cell: RuntimeCellHandle, ownership: ScopeCellOwnership, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let name = name.into(); if scope.is_global_alias(&name) { let Some(global_scope) = context.global_scope_ptr() else { @@ -394,14 +394,35 @@ fn set_scope_cell( }; let current_scope = scope as *mut ElephcEvalScope; if global_scope == current_scope { - return Ok(scope.set(name, cell, ownership)); + return Ok(scope.set_respecting_references(name, cell, ownership)); } let Some(global_scope) = (unsafe { global_scope.as_mut() }) else { return Err(EvalStatus::RuntimeFatal); }; - return Ok(global_scope.set(name, cell, ownership)); + return Ok(global_scope.set_respecting_references(name, cell, ownership)); + } + Ok(scope.set_respecting_references(name, cell, ownership)) +} + +/// Creates a PHP reference alias between two eval-visible variable names. +fn set_reference_alias( + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + target: &str, + source: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if scope.is_global_alias(source) { + scope.mark_global_alias(target.to_string()); + return Ok(Vec::new()); } - Ok(scope.set(name, cell, ownership)) + let (cell, ownership) = scope_entry(context, scope, source) + .filter(|entry| entry.flags().is_visible()) + .map_or_else( + || values.null().map(|cell| (cell, ScopeCellOwnership::Owned)), + |entry| Ok((entry.cell(), entry.flags().ownership)), + )?; + Ok(scope.set_reference(target.to_string(), source.to_string(), cell, ownership)) } /// Unsets a variable, removing only the local alias when the name is global. @@ -413,7 +434,7 @@ fn unset_scope_cell( if scope.is_global_alias(&name) { scope.clear_global_alias(&name); } - scope.unset(name) + scope.unset_respecting_references(name) } /// Marks variables as aliases to the context global scope for later reads/writes. @@ -460,9 +481,7 @@ fn execute_stmt( let index = eval_array_append_key(array, values)?; let value = eval_expr(value, context, scope, values)?; let array = values.array_set(array, index, value)?; - if let Some(replaced) = - set_scope_cell(context, scope, name.clone(), array, ownership)? - { + for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { values.release(replaced)?; } Ok(EvalControl::None) @@ -484,9 +503,7 @@ fn execute_stmt( let index = eval_expr(index, context, scope, values)?; let value = eval_expr(value, context, scope, values)?; let array = values.array_set(array, index, value)?; - if let Some(replaced) = - set_scope_cell(context, scope, name.clone(), array, ownership)? - { + for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { values.release(replaced)?; } Ok(EvalControl::None) @@ -559,6 +576,12 @@ fn execute_stmt( expr, context, scope, values, )?)), EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), + EvalStmt::ReferenceAssign { target, source } => { + for replaced in set_reference_alias(context, scope, target, source, values)? { + values.release(replaced)?; + } + Ok(EvalControl::None) + } EvalStmt::StaticVar { name, init } => { execute_static_var_stmt(name, init, context, scope, values)?; Ok(EvalControl::None) @@ -575,9 +598,13 @@ fn execute_stmt( } EvalStmt::StoreVar { name, value } => { let value = eval_expr(value, context, scope, values)?; - if let Some(replaced) = - set_scope_cell(context, scope, name.clone(), value, ScopeCellOwnership::Owned)? - { + for replaced in set_scope_cell( + context, + scope, + name.clone(), + value, + ScopeCellOwnership::Owned, + )? { values.release(replaced)?; } Ok(EvalControl::None) @@ -754,17 +781,25 @@ fn execute_foreach_stmt( let key = values.array_iter_key(array, index)?; let value = values.array_get(array, key)?; if let Some(key_name) = key_name { - if let Some(replaced) = - set_scope_cell(context, scope, key_name.to_string(), key, ScopeCellOwnership::Owned)? - { + for replaced in set_scope_cell( + context, + scope, + key_name.to_string(), + key, + ScopeCellOwnership::Owned, + )? { values.release(replaced)?; } } else { values.release(key)?; } - if let Some(replaced) = - set_scope_cell(context, scope, value_name.to_string(), value, ScopeCellOwnership::Owned)? - { + for replaced in set_scope_cell( + context, + scope, + value_name.to_string(), + value, + ScopeCellOwnership::Owned, + )? { values.release(replaced)?; } match execute_statements(body, context, scope, values)? { @@ -2849,8 +2884,13 @@ fn eval_dynamic_function_with_values( let static_names = static_var_names(function.body()); context.push_function(function.name()); let result = execute_statements(function.body(), context, &mut function_scope, values); - let persist_result = - persist_static_locals(context, function.name(), &static_names, &function_scope, values); + let persist_result = persist_static_locals( + context, + function.name(), + &static_names, + &function_scope, + values, + ); context.pop_function(); persist_result?; match result? { @@ -2920,6 +2960,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::Expr(_) | EvalStmt::Global { .. } | EvalStmt::PropertySet { .. } + | EvalStmt::ReferenceAssign { .. } | EvalStmt::Return(_) | EvalStmt::StoreVar { .. } | EvalStmt::UnsetVar { .. } => {} @@ -3906,6 +3947,25 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(7)); } + /// Verifies reference assignment aliases variable names and writes through the alias. + #[test] + fn execute_program_reference_assignment_updates_source_variable() { + let program = parse_fragment(b"$x = 1; $alias =& $x; $alias = 5; return $x;") + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + let alias = scope + .visible_cell("alias") + .expect("scope should contain alias"); + + assert_eq!(x, alias); + assert_eq!(values.get(x), FakeValue::Int(5)); + assert_eq!(values.get(result), FakeValue::Int(5)); + } + /// Verifies simple variable compound assignments read, compute, and write the scope value. #[test] fn execute_program_evaluates_compound_assignments() { @@ -4821,8 +4881,8 @@ return (dyn() * 10) + dyn();"#, /// Verifies `global` declarations read and write the context global scope. #[test] fn execute_program_global_alias_writes_context_global_scope() { - let program = parse_fragment(br#"global $g; $g = $g + 1; return $g;"#) - .expect("parse eval fragment"); + let program = + parse_fragment(br#"global $g; $g = $g + 1; return $g;"#).expect("parse eval fragment"); let mut context = ElephcEvalContext::new(); let mut scope = ElephcEvalScope::new(); let mut global_scope = ElephcEvalScope::new(); diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 4740a3026f..fbefac7183 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -846,7 +846,19 @@ impl Parser { }; self.advance(); if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { - return Err(EvalParseError::UnsupportedConstruct); + self.advance(); + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::ReferenceAssign { + target: name, + source, + }]); } let value = self.parse_expr()?; if require_semicolon { @@ -1751,6 +1763,19 @@ mod tests { ); } + /// Verifies reference assignments lower to by-name ReferenceAssign statements. + #[test] + fn parse_fragment_accepts_reference_assignment_source() { + let program = parse_fragment(b"$left =& $right;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ReferenceAssign { + target: "left".to_string(), + source: "right".to_string(), + }] + ); + } + /// Verifies multiplicative operators preserve PHP precedence and associativity. #[test] fn parse_fragment_accepts_division_and_modulo_source() { @@ -2949,15 +2974,6 @@ mod tests { } } - /// Verifies unsupported reference assignments report the unsupported construct status. - #[test] - fn parse_fragment_rejects_reference_assignment_as_unsupported_construct() { - assert_eq!( - parse_fragment(b"$left =& $right;"), - Err(EvalParseError::UnsupportedConstruct) - ); - } - /// Verifies malformed statements report parse errors instead of partial IR. #[test] fn parse_fragment_rejects_missing_semicolon() { diff --git a/crates/elephc-eval/src/scope.rs b/crates/elephc-eval/src/scope.rs index 5feedbf8e2..d5b86934af 100644 --- a/crates/elephc-eval/src/scope.rs +++ b/crates/elephc-eval/src/scope.rs @@ -44,6 +44,17 @@ impl ScopeEntryFlags { } } + /// Builds flags for a present runtime cell shared by PHP references. + pub const fn reference(ownership: ScopeCellOwnership) -> Self { + Self { + present: true, + unset: false, + dirty: true, + by_ref: true, + ownership, + } + } + /// Builds flags for a PHP variable that has been unset from the dynamic scope. pub const fn unset() -> Self { Self { @@ -83,6 +94,19 @@ impl ScopeEntry { } } + /// Creates a present entry that participates in a PHP reference alias set. + pub const fn reference( + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, + generation: u64, + ) -> Self { + Self { + cell, + flags: ScopeEntryFlags::reference(ownership), + generation, + } + } + /// Creates an unset marker at the given scope generation. pub const fn unset(generation: u64) -> Self { Self { @@ -150,6 +174,84 @@ impl ElephcEvalScope { owned_cell_except(previous, cell) } + /// Stores a variable while preserving existing PHP reference aliases. + pub fn set_respecting_references( + &mut self, + name: impl Into, + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, + ) -> Vec { + let name = name.into(); + let Some(entry) = self.entries.get(&name).copied() else { + return self.set(name, cell, ownership).into_iter().collect(); + }; + if !entry.flags().is_visible() || !entry.flags().by_ref { + return self.set(name, cell, ownership).into_iter().collect(); + } + + self.bump_generation(); + let old_cell = entry.cell(); + let mut replaced = Vec::new(); + for (entry_name, entry) in &mut self.entries { + let flags = entry.flags(); + if !flags.is_visible() || !flags.by_ref || entry.cell() != old_cell { + continue; + } + if flags.ownership == ScopeCellOwnership::Owned + && old_cell != cell + && !replaced.contains(&old_cell) + { + replaced.push(old_cell); + } + let next_ownership = if entry_name == &name { + ownership + } else { + ScopeCellOwnership::Borrowed + }; + *entry = ScopeEntry::reference(cell, next_ownership, self.generation); + } + replaced + } + + /// Binds a target variable name as a PHP reference to a source variable name. + pub fn set_reference( + &mut self, + target: impl Into, + source: impl Into, + default_cell: RuntimeCellHandle, + default_ownership: ScopeCellOwnership, + ) -> Vec { + let target = target.into(); + let source = source.into(); + self.bump_generation(); + let source_entry = self + .entries + .get(&source) + .copied() + .filter(|entry| entry.flags().is_visible()); + let (cell, source_ownership) = source_entry + .map_or((default_cell, default_ownership), |entry| { + (entry.cell(), entry.flags().ownership) + }); + if target == source { + let previous = self.entries.insert( + source, + ScopeEntry::reference(cell, source_ownership, self.generation), + ); + return owned_cell_except(previous, cell).into_iter().collect(); + } + + let previous_source = self.entries.insert( + source, + ScopeEntry::reference(cell, source_ownership, self.generation), + ); + let previous_target = self.entries.insert( + target, + ScopeEntry::reference(cell, ScopeCellOwnership::Borrowed, self.generation), + ); + owned_cells_except([previous_source, previous_target], cell) + } + /// Marks a named variable as unset while preserving the fact that eval touched it. pub fn unset(&mut self, name: impl Into) -> Option { self.bump_generation(); @@ -159,6 +261,48 @@ impl ElephcEvalScope { owned_cell(previous) } + /// Marks a variable as unset while preserving ownership for remaining references. + pub fn unset_respecting_references( + &mut self, + name: impl Into, + ) -> Option { + let name = name.into(); + let Some(entry) = self.entries.get(&name).copied() else { + return self.unset(name); + }; + if !entry.flags().is_visible() || !entry.flags().by_ref { + return self.unset(name); + } + let old_cell = entry.cell(); + let should_transfer_ownership = entry.flags().ownership == ScopeCellOwnership::Owned; + self.bump_generation(); + let mut transferred = false; + if should_transfer_ownership { + for (entry_name, entry) in &mut self.entries { + let flags = entry.flags(); + if entry_name == &name + || !flags.is_visible() + || !flags.by_ref + || entry.cell() != old_cell + { + continue; + } + *entry = + ScopeEntry::reference(old_cell, ScopeCellOwnership::Owned, self.generation); + transferred = true; + break; + } + } + let previous = self + .entries + .insert(name, ScopeEntry::unset(self.generation)); + if transferred { + None + } else { + owned_cell(previous) + } + } + /// Returns the entry for a named variable, including unset markers. pub fn entry(&self, name: &str) -> Option { self.entries.get(name).copied() @@ -235,6 +379,23 @@ fn owned_cell_except( owned_cell(entry).filter(|cell| *cell != replacement) } +/// Returns owned cells from replaced entries unless they match the replacement. +fn owned_cells_except( + entries: impl IntoIterator>, + replacement: RuntimeCellHandle, +) -> Vec { + let mut cells = Vec::new(); + for entry in entries { + let Some(cell) = owned_cell_except(entry, replacement) else { + continue; + }; + if !cells.contains(&cell) { + cells.push(cell); + } + } + cells +} + impl Default for ElephcEvalScope { /// Creates the default empty materialized activation scope. fn default() -> Self { @@ -306,6 +467,85 @@ mod tests { assert_eq!(replaced, None); } + /// Verifies reference binding points two variable names at one runtime cell. + #[test] + fn set_reference_binds_names_to_source_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + + scope.set("source", cell, ScopeCellOwnership::Owned); + let replaced = scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + + let source = scope.entry("source").expect("source entry should exist"); + let alias = scope.entry("alias").expect("alias entry should exist"); + assert!(replaced.is_empty()); + assert_eq!(source.cell(), cell); + assert_eq!(alias.cell(), cell); + assert!(source.flags().by_ref); + assert!(alias.flags().by_ref); + assert_eq!(source.flags().ownership, ScopeCellOwnership::Owned); + assert_eq!(alias.flags().ownership, ScopeCellOwnership::Borrowed); + } + + /// Verifies writing through one reference alias updates every alias in the group. + #[test] + fn set_respecting_references_updates_alias_group() { + let mut scope = ElephcEvalScope::new(); + let old_cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let new_cell = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + scope.set("source", old_cell, ScopeCellOwnership::Owned); + scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + + let replaced = + scope.set_respecting_references("alias", new_cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, vec![old_cell]); + assert_eq!(scope.visible_cell("source"), Some(new_cell)); + assert_eq!(scope.visible_cell("alias"), Some(new_cell)); + assert_eq!( + scope.entry("alias").expect("alias").flags().ownership, + ScopeCellOwnership::Owned + ); + assert_eq!( + scope.entry("source").expect("source").flags().ownership, + ScopeCellOwnership::Borrowed + ); + } + + /// Verifies unsetting an owned alias transfers ownership to a remaining reference. + #[test] + fn unset_respecting_references_transfers_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + scope.set("source", cell, ScopeCellOwnership::Owned); + scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + scope.set_respecting_references("alias", cell, ScopeCellOwnership::Owned); + + let replaced = scope.unset_respecting_references("alias"); + + assert_eq!(replaced, None); + assert!(scope.entry("alias").expect("alias").flags().unset); + assert_eq!( + scope.entry("source").expect("source").flags().ownership, + ScopeCellOwnership::Owned + ); + } + /// Verifies draining a scope returns only visible owned cells. #[test] fn drain_owned_cells_returns_visible_owned_entries() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c17cd71aef..e475c0af95 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1902,14 +1902,17 @@ eval('return new EvalDynamicNewUnsupported();'); ); } -/// Verifies unsupported eval reference assignments fail through the eval diagnostic path. +/// Verifies eval reference assignments update the referenced caller local. #[test] -fn test_eval_unsupported_reference_assignment_fails() { - let err = compile_and_run_expect_failure(" Date: Tue, 16 Jun 2026 04:08:48 +0200 Subject: [PATCH 0106/1208] Cover eval reference unset --- tests/codegen/eval.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e475c0af95..080a39dae0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1915,6 +1915,19 @@ echo $x; assert_eq!(out, "5"); } +/// Verifies eval unset breaks a reference alias without unsetting the source variable. +#[test] +fn test_eval_unset_reference_alias_keeps_source_local() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 04:16:27 +0200 Subject: [PATCH 0107/1208] Inherit eval global aliases --- crates/elephc-eval/src/interpreter.rs | 46 ++++++++++--- crates/elephc-eval/src/lib.rs | 56 +++++++++++++++- crates/elephc-eval/src/scope.rs | 37 +++++++++-- src/codegen/lower_inst/builtins/eval.rs | 88 +++++++++++++++++++++---- tests/codegen/eval.rs | 17 +++++ 5 files changed, 217 insertions(+), 27 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 100aa235f0..704c1024a1 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -355,17 +355,21 @@ fn scope_entry( scope: &ElephcEvalScope, name: &str, ) -> Option { - if !scope.is_global_alias(name) { + let Some(global_name) = scope.global_alias_target(name) else { return scope.entry(name); - } + }; let Some(global_scope) = context.global_scope_ptr() else { return scope.entry(name); }; let current_scope = scope as *const ElephcEvalScope as *mut ElephcEvalScope; if global_scope == current_scope { - return scope.entry(name); + return scope.entry(global_name); + } + unsafe { + global_scope + .as_ref() + .and_then(|scope| scope.entry(global_name)) } - unsafe { global_scope.as_ref().and_then(|scope| scope.entry(name)) } } /// Returns the eval-visible cell for a variable, following `global` aliases. @@ -388,18 +392,18 @@ fn set_scope_cell( ownership: ScopeCellOwnership, ) -> Result, EvalStatus> { let name = name.into(); - if scope.is_global_alias(&name) { + if let Some(global_name) = scope.global_alias_target(&name).map(str::to_string) { let Some(global_scope) = context.global_scope_ptr() else { return Err(EvalStatus::RuntimeFatal); }; let current_scope = scope as *mut ElephcEvalScope; if global_scope == current_scope { - return Ok(scope.set_respecting_references(name, cell, ownership)); + return Ok(scope.set_respecting_references(global_name, cell, ownership)); } let Some(global_scope) = (unsafe { global_scope.as_mut() }) else { return Err(EvalStatus::RuntimeFatal); }; - return Ok(global_scope.set_respecting_references(name, cell, ownership)); + return Ok(global_scope.set_respecting_references(global_name, cell, ownership)); } Ok(scope.set_respecting_references(name, cell, ownership)) } @@ -412,8 +416,8 @@ fn set_reference_alias( source: &str, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - if scope.is_global_alias(source) { - scope.mark_global_alias(target.to_string()); + if let Some(global_name) = scope.global_alias_target(source).map(str::to_string) { + scope.mark_global_alias_to(target.to_string(), global_name); return Ok(Vec::new()); } let (cell, ownership) = scope_entry(context, scope, source) @@ -4901,6 +4905,30 @@ return (dyn() * 10) + dyn();"#, assert_eq!(values.get(global), FakeValue::Int(2)); } + /// Verifies references to global aliases write the source global variable. + #[test] + fn execute_program_reference_alias_to_global_updates_source_global() { + let program = parse_fragment(br#"global $g; $alias =& $g; $alias = 4; return $g;"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(4)); + assert_eq!(values.get(global), FakeValue::Int(4)); + assert!(global_scope.visible_cell("alias").is_none()); + } + /// Verifies named calls reject positional arguments that follow named arguments. #[test] fn execute_program_rejects_positional_after_named_arg() { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 3a81668fea..e72431a91d 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -205,6 +205,32 @@ pub unsafe extern "C" fn __elephc_eval_scope_unset( EvalStatus::Ok.code() } +/// Marks a local eval-scope variable as an alias of a program-global variable. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. Name pointers must be readable +/// for their matching lengths when the length is greater than zero. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_mark_global_alias( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + global_name_ptr: *const u8, + global_name_len: u64, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(global_name) = abi_name_to_string(global_name_ptr, global_name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + scope.mark_global_alias_to(name, global_name); + EvalStatus::Ok.code() +} + /// Clears dirty flags for every entry in a materialized eval scope. /// /// # Safety @@ -850,7 +876,10 @@ mod tests { let status = unsafe { __elephc_eval_context_set_global_scope(&mut ctx, &mut scope) }; assert_eq!(status, EvalStatus::Ok.code()); - assert_eq!(ctx.global_scope_ptr(), Some(&mut scope as *mut ElephcEvalScope)); + assert_eq!( + ctx.global_scope_ptr(), + Some(&mut scope as *mut ElephcEvalScope) + ); } /// Verifies the function-exists ABI probes eval-declared functions by folded name. @@ -999,6 +1028,31 @@ mod tests { assert_eq!(out_flags & SCOPE_FLAG_OWNED, SCOPE_FLAG_OWNED); } + /// Verifies the alias ABI maps a local eval variable to a global name. + #[test] + fn scope_mark_global_alias_records_target_name() { + let scope = __elephc_eval_scope_new(); + let name = b"alias"; + let global_name = b"source"; + + let status = unsafe { + __elephc_eval_scope_mark_global_alias( + scope, + name.as_ptr(), + name.len() as u64, + global_name.as_ptr(), + global_name.len() as u64, + ) + }; + let target = unsafe { (*scope).global_alias_target("alias").map(str::to_string) }; + unsafe { + __elephc_eval_scope_free(scope); + } + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!(target.as_deref(), Some("source")); + } + /// Verifies scope unset and clear-dirty expose missing/clean state through the ABI. #[test] fn scope_unset_and_clear_dirty_update_flags() { diff --git a/crates/elephc-eval/src/scope.rs b/crates/elephc-eval/src/scope.rs index d5b86934af..3527a89334 100644 --- a/crates/elephc-eval/src/scope.rs +++ b/crates/elephc-eval/src/scope.rs @@ -11,7 +11,7 @@ //! - Scope entries store runtime-cell handles only; the eval bridge does not //! introduce a second PHP value representation. -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use crate::value::RuntimeCellHandle; @@ -140,7 +140,7 @@ impl ScopeEntry { /// Materialized activation scope passed opaquely across the eval ABI. pub struct ElephcEvalScope { entries: HashMap, - global_aliases: HashSet, + global_aliases: HashMap, generation: u64, } @@ -149,7 +149,7 @@ impl ElephcEvalScope { pub fn new() -> Self { Self { entries: HashMap::new(), - global_aliases: HashSet::new(), + global_aliases: HashMap::new(), generation: 0, } } @@ -322,7 +322,17 @@ impl ElephcEvalScope { /// Marks a variable name as an alias to the eval context's global scope. pub fn mark_global_alias(&mut self, name: impl Into) { - self.global_aliases.insert(name.into()); + let name = name.into(); + self.mark_global_alias_to(name.clone(), name); + } + + /// Marks a variable name as an alias to a differently named global variable. + pub fn mark_global_alias_to( + &mut self, + name: impl Into, + global_name: impl Into, + ) { + self.global_aliases.insert(name.into(), global_name.into()); } /// Removes a variable's global alias marker after local `unset()`. @@ -332,7 +342,12 @@ impl ElephcEvalScope { /// Returns true when the variable should resolve through the global scope. pub fn is_global_alias(&self, name: &str) -> bool { - self.global_aliases.contains(name) + self.global_aliases.contains_key(name) + } + + /// Returns the target global name for a local alias. + pub fn global_alias_target(&self, name: &str) -> Option<&str> { + self.global_aliases.get(name).map(String::as_str) } /// Returns the names of entries dirtied since the last synchronization. @@ -546,6 +561,18 @@ mod tests { ); } + /// Verifies local aliases can point at differently named globals. + #[test] + fn global_alias_to_records_target_name() { + let mut scope = ElephcEvalScope::new(); + + scope.mark_global_alias_to("alias", "source"); + + assert!(scope.is_global_alias("alias")); + assert_eq!(scope.global_alias_target("alias"), Some("source")); + assert_eq!(scope.global_alias_target("source"), None); + } + /// Verifies draining a scope returns only visible owned cells. #[test] fn drain_owned_cells_returns_visible_owned_entries() { diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index ee573333fa..fd72b24eb4 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -60,6 +60,13 @@ struct EvalSyncGlobal { ty: PhpType, } +/// Local-to-global alias metadata inherited by eval from the caller function scope. +#[derive(Clone)] +struct EvalGlobalAlias { + name: String, + global_name: String, +} + /// A module-local function that can be registered with the eval context. struct EvalNativeFunctionRegistration { name: String, @@ -86,8 +93,10 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R ensure_eval_global_scope(ctx)?; let sync_locals = eval_sync_locals(ctx); let sync_globals = eval_sync_globals(ctx); + let global_aliases = eval_global_aliases(ctx); flush_eval_scope_locals(ctx, &sync_locals)?; flush_eval_global_scope(ctx, &sync_globals)?; + mark_eval_scope_global_aliases(ctx, &global_aliases); set_eval_context_global_scope(ctx); load_eval_context_to_arg(ctx, 0); load_eval_scope_to_arg(ctx, 1); @@ -595,6 +604,22 @@ fn local_uses_eval_global_sync(ctx: &FunctionContext<'_>, name: Option<&str>) -> ctx.is_main && name.is_some_and(|name| ctx.has_global_name(name)) } +/// Collects caller-scope `global` aliases that eval fragments inherit by name. +fn eval_global_aliases(ctx: &FunctionContext<'_>) -> Vec { + ctx.function + .locals + .iter() + .filter(|local| local.kind == LocalKind::GlobalAlias) + .filter_map(|local| { + let name = local.name.clone()?; + Some(EvalGlobalAlias { + global_name: name.clone(), + name, + }) + }) + .collect() +} + /// Collects program globals that can be boxed into the eval global scope. fn eval_sync_globals(ctx: &FunctionContext<'_>) -> Vec { let mut globals = ctx @@ -629,7 +654,12 @@ fn push_eval_process_superglobal(globals: &mut Vec, name: &str, /// Returns one unambiguous codegen type used for a program global, if available. fn eval_sync_global_type(ctx: &FunctionContext<'_>, name: &str) -> Option { let mut inferred = None; - for function in ctx.module.functions.iter().chain(ctx.module.closures.iter()) { + for function in ctx + .module + .functions + .iter() + .chain(ctx.module.closures.iter()) + { for inst in &function.instructions { if global_instruction_name(ctx, inst) != Some(name) { continue; @@ -657,7 +687,11 @@ fn global_instruction_name<'a>( let Some(Immediate::GlobalName(data)) = inst.immediate else { return None; }; - ctx.module.data.global_names.get(data.as_raw() as usize).map(String::as_str) + ctx.module + .data + .global_names + .get(data.as_raw() as usize) + .map(String::as_str) } /// Returns the value type carried by a global load or store instruction. @@ -778,6 +812,36 @@ fn emit_eval_global_scope_set(ctx: &mut FunctionContext<'_>, global: &EvalSyncGl emit_eval_status_check(ctx); } +/// Marks caller-scope global aliases in the materialized eval scope. +fn mark_eval_scope_global_aliases(ctx: &mut FunctionContext<'_>, aliases: &[EvalGlobalAlias]) { + for alias in aliases { + let (name_label, name_len) = ctx.data.add_string(alias.name.as_bytes()); + let (global_name_label, global_name_len) = + ctx.data.add_string(alias.global_name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let global_name_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_symbol_address(ctx.emitter, global_name_arg, &global_name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + global_name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_scope_mark_global_alias"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + } +} + /// Calls `__elephc_eval_scope_set` for one boxed local value. fn emit_eval_scope_set(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal, flags: i64) { let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); @@ -891,12 +955,12 @@ fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local } } } @@ -1014,12 +1078,12 @@ fn emit_retain_scope_cell_if_owned(ctx: &mut FunctionContext<'_>) { Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining } } abi::emit_call_label(ctx.emitter, "__rt_incref"); @@ -1138,12 +1202,12 @@ fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: Arch::AArch64 => { ctx.emitter .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler } Arch::X86_64 => { ctx.emitter .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler } } } @@ -1153,7 +1217,7 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr + ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); ctx.emitter @@ -1162,12 +1226,12 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { abi::emit_exit(ctx.emitter, 1); } Arch::X86_64 => { - ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr + ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); ctx.emitter .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length - ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting abi::emit_exit(ctx.emitter, 1); } } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 080a39dae0..1540488092 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1469,6 +1469,23 @@ echo $g; assert_eq!(out, "1"); } +/// Verifies eval references to global aliases update the source global storage. +#[test] +fn test_eval_reference_alias_to_global_updates_global_storage() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 04:28:24 +0200 Subject: [PATCH 0108/1208] Support eval dynamic new for AOT classes --- crates/elephc-eval/src/eval_ir.rs | 4 ++ crates/elephc-eval/src/interpreter.rs | 38 ++++++++++++++++++ crates/elephc-eval/src/parser.rs | 29 +++++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 8 ++++ src/codegen/mod.rs | 2 +- src/codegen_support/runtime/eval_bridge.rs | 45 ++++++++++++++++++++++ tests/codegen/eval.rs | 17 ++++---- 7 files changed, 131 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index fdb9720eaa..f61dcc61e6 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -173,6 +173,10 @@ pub enum EvalExpr { args: Vec, }, Magic(EvalMagicConst), + NewObject { + class_name: String, + args: Vec, + }, NullCoalesce { value: Box, default: Box, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 704c1024a1..2f0f8cfde6 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -98,6 +98,9 @@ pub trait RuntimeValueOps { args: Vec, ) -> Result; + /// Creates a named runtime object without constructor arguments. + fn new_object(&mut self, class_name: &str) -> Result; + /// Returns the visible element count for an array-like runtime cell. fn array_len(&mut self, array: RuntimeCellHandle) -> Result; @@ -871,6 +874,12 @@ fn eval_expr( visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) } EvalExpr::Magic(magic) => eval_magic_const(magic, context, values), + EvalExpr::NewObject { class_name, args } => { + if !args.is_empty() { + return Err(EvalStatus::UnsupportedConstruct); + } + values.new_object(class_name) + } EvalExpr::MethodCall { object, method, @@ -3456,6 +3465,11 @@ mod tests { } } + /// Creates one fake object for eval `new` unit tests. + fn new_object(&mut self, _class_name: &str) -> Result { + Ok(self.alloc(FakeValue::Object(HashMap::new()))) + } + /// Returns the visible element count for fake array values. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { match self.get(array) { @@ -4193,6 +4207,30 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::Int(18)); } + /// Verifies eval object construction dispatches through runtime hooks. + #[test] + fn execute_program_constructs_named_object() { + let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Object(HashMap::new())); + } + + /// Verifies eval object construction rejects constructor arguments until the ABI supports them. + #[test] + fn execute_program_rejects_new_object_constructor_args() { + let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("constructor args"); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + } + /// Verifies if/else executes only the PHP-truthy branch. #[test] fn execute_program_if_else_uses_php_truthiness() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index fbefac7183..458ee0627f 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1482,6 +1482,7 @@ impl Parser { let expr = self.parse_expr()?; Ok(EvalExpr::Print(Box::new(expr))) } + TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { Err(EvalParseError::UnsupportedConstruct) } @@ -1510,6 +1511,18 @@ impl Parser { }) } + /// Parses `new ClassName(...)` expressions in eval fragments. + fn parse_new_object_expr(&mut self) -> Result { + self.advance(); + let TokenKind::Ident(class_name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let class_name = class_name.clone(); + self.advance(); + let args = self.parse_call_args()?; + Ok(EvalExpr::NewObject { class_name, args }) + } + /// Parses a parenthesized source-order argument list. fn parse_call_args(&mut self) -> Result, EvalParseError> { self.expect(TokenKind::LParen)?; @@ -1728,7 +1741,6 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { "require_once", "include", "include_once", - "new", "throw", "trait", "try", @@ -1740,7 +1752,7 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { /// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. fn is_unsupported_expression_keyword(name: &str) -> bool { - ["clone", "match", "new", "yield"] + ["clone", "match", "yield"] .iter() .any(|keyword| ident_eq(name, keyword)) } @@ -2784,6 +2796,19 @@ mod tests { ); } + /// Verifies object construction parses as a named EvalIR expression. + #[test] + fn parse_fragment_accepts_new_object_source() { + let program = parse_fragment(br#"return new Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Box".to_string(), + args: Vec::new(), + }))] + ); + } + /// Verifies object method calls preserve source-order argument expressions. #[test] fn parse_fragment_accepts_method_call_args_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 3e43fb0897..4de1696c44 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -58,6 +58,7 @@ unsafe extern "C" { name_len: u64, args: *mut RuntimeCell, ) -> *mut RuntimeCell; + fn __elephc_eval_value_new_object(name_ptr: *const u8, name_len: u64) -> *mut RuntimeCell; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; @@ -268,6 +269,13 @@ impl RuntimeValueOps for ElephcRuntimeOps { result } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. + fn new_object(&mut self, class_name: &str) -> Result { + Self::handle(unsafe { + __elephc_eval_value_new_object(class_name.as_ptr(), class_name.len() as u64) + }) + } + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 20ab3ef79b..118710a036 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -201,7 +201,7 @@ fn finalize_user_asm( let user_functions = runtime_user_function_sigs(module); let function_variant_groups = runtime_function_variant_groups(module); let mut allowed_class_names = runtime_referenced_class_names(module); - if module_uses_dynamic_callable_lookup(module) { + if module_uses_dynamic_callable_lookup(module) || module.required_runtime_features.eval { allowed_class_names.extend(module.class_infos.keys().cloned()); } let runtime_interfaces = runtime_referenced_interfaces(module, &allowed_class_names); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 7a7bde533a..51e402600a 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -40,6 +40,29 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov x2, xzr"); // bool payloads do not use a high word emitter.instruction("b __rt_mixed_from_value"); // box the bool payload and return to Rust + label_c_global(emitter, "__elephc_eval_value_new_object"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame across dynamic object lookup + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across runtime calls + emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("mov x2, x1"); // move the C class-name length into new_by_name's string ABI + emitter.instruction("mov x1, x0"); // move the C class-name pointer into new_by_name's string ABI + emitter.instruction("bl __rt_new_by_name"); // allocate the named AOT class object, or return null on miss + emitter.instruction("cbz x0, __elephc_eval_value_new_object_null"); // box PHP null when no runtime class matched the eval name + emitter.instruction("mov x1, x0"); // move the allocated object pointer into the Mixed payload + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the allocated object for Rust + emitter.instruction("b __elephc_eval_value_new_object_done"); // skip the null boxing path after successful allocation + emitter.label("__elephc_eval_value_new_object_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for unknown eval class names + emitter.label("__elephc_eval_value_new_object_done"); + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the dynamic-object wrapper frame + emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_value_array_new"); emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for array allocation and boxing emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls @@ -1023,6 +1046,28 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("xor esi, esi"); // bool payloads do not use a high word emitter.instruction("jmp __rt_mixed_from_value"); // box the bool payload and return to Rust + label_c_global(emitter, "__elephc_eval_value_new_object"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable dynamic-object wrapper frame + emitter.instruction("mov rax, rdi"); // move the C class-name pointer into new_by_name's string ABI + emitter.instruction("mov rdx, rsi"); // move the C class-name length into new_by_name's string ABI + emitter.instruction("call __rt_new_by_name"); // allocate the named AOT class object, or return null on miss + emitter.instruction("test rax, rax"); // did the runtime class-name lookup allocate an object? + emitter.instruction("jz __elephc_eval_value_new_object_null_x86"); // box PHP null when no runtime class matched the eval name + emitter.instruction("mov rdi, rax"); // move the allocated object pointer into the Mixed payload + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the allocated object for Rust + emitter.instruction("jmp __elephc_eval_value_new_object_done_x86"); // skip the null boxing path after successful allocation + emitter.label("__elephc_eval_value_new_object_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for unknown eval class names + emitter.label("__elephc_eval_value_new_object_done_x86"); + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_value_array_new"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1540488092..3ffeb29892 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1904,19 +1904,18 @@ fn test_eval_unsupported_class_declaration_fails() { ); } -/// Verifies unsupported eval object construction fails through the eval diagnostic path. +/// Verifies eval can construct an AOT class with no constructor arguments. #[test] -fn test_eval_unsupported_dynamic_new_fails() { - let err = compile_and_run_expect_failure( +fn test_eval_dynamic_new_constructs_aot_class() { + let out = compile_and_run( r#"x;'); "#, ); - assert!( - err.contains("Fatal error: eval() fragment uses an unsupported construct"), - "stderr did not contain eval unsupported diagnostic: {err}" - ); + assert_eq!(out, "7"); } /// Verifies eval reference assignments update the referenced caller local. From b1406d5417e1ee0e67be053d9fdb1025fb42c588 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 04:37:57 +0200 Subject: [PATCH 0109/1208] Run eval constructors for AOT dynamic new --- crates/elephc-eval/src/interpreter.rs | 42 ++- crates/elephc-eval/src/runtime_hooks.rs | 45 ++- src/codegen/eval_constructor_helpers.rs | 481 ++++++++++++++++++++++++ src/codegen/mod.rs | 2 + tests/codegen/eval.rs | 46 ++- 5 files changed, 599 insertions(+), 17 deletions(-) create mode 100644 src/codegen/eval_constructor_helpers.rs diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2f0f8cfde6..c5c9b929df 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -101,6 +101,13 @@ pub trait RuntimeValueOps { /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; + /// Calls the runtime constructor for an object when the class declares one. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus>; + /// Returns the visible element count for an array-like runtime cell. fn array_len(&mut self, array: RuntimeCellHandle) -> Result; @@ -875,10 +882,10 @@ fn eval_expr( } EvalExpr::Magic(magic) => eval_magic_const(magic, context, values), EvalExpr::NewObject { class_name, args } => { - if !args.is_empty() { - return Err(EvalStatus::UnsupportedConstruct); - } - values.new_object(class_name) + let args = eval_method_call_arg_values(args, context, scope, values)?; + values + .new_object(class_name) + .and_then(|object| values.construct_object(object, args).map(|()| object)) } EvalExpr::MethodCall { object, @@ -3470,6 +3477,22 @@ mod tests { Ok(self.alloc(FakeValue::Object(HashMap::new()))) } + /// Applies fake constructor side effects for eval `new` unit tests. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some(first) = args.first().copied() { + properties.insert("x".to_string(), first); + } + Ok(()) + } + /// Returns the visible element count for fake array values. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { match self.get(array) { @@ -4219,16 +4242,19 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::Object(HashMap::new())); } - /// Verifies eval object construction rejects constructor arguments until the ABI supports them. + /// Verifies eval object construction passes constructor arguments through runtime hooks. #[test] - fn execute_program_rejects_new_object_constructor_args() { + fn execute_program_constructs_named_object_with_args() { let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values).expect_err("constructor args"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let FakeValue::Object(properties) = values.get(result) else { + panic!("expected fake object"); + }; - assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(properties["x"]), FakeValue::Int(1)); } /// Verifies if/else executes only the PHP-truthy branch. diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 4de1696c44..8ce1f86bb0 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -59,6 +59,10 @@ unsafe extern "C" { args: *mut RuntimeCell, ) -> *mut RuntimeCell; fn __elephc_eval_value_new_object(name_ptr: *const u8, name_len: u64) -> *mut RuntimeCell; + fn __elephc_eval_value_construct_object( + object: *mut RuntimeCell, + args: *mut RuntimeCell, + ) -> u64; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; @@ -150,6 +154,19 @@ impl ElephcRuntimeOps { Ok(RuntimeCellHandle::from_raw(ptr)) } } + + /// Packs source-order argument cells into the boxed eval array ABI. + fn arg_array(args: Vec) -> Result { + let arg_array = unsafe { __elephc_eval_value_array_new(args.len() as u64) }; + let arg_array = Self::handle(arg_array)?; + for (index, value) in args.into_iter().enumerate() { + let index = Self::handle(unsafe { __elephc_eval_value_int(index as i64) })?; + Self::handle(unsafe { + __elephc_eval_value_array_set(arg_array.as_ptr(), index.as_ptr(), value.as_ptr()) + })?; + } + Ok(arg_array) + } } #[cfg(not(test))] @@ -247,14 +264,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { method: &str, args: Vec, ) -> Result { - let arg_array = unsafe { __elephc_eval_value_array_new(args.len() as u64) }; - let arg_array = Self::handle(arg_array)?; - for (index, value) in args.into_iter().enumerate() { - let index = Self::handle(unsafe { __elephc_eval_value_int(index as i64) })?; - Self::handle(unsafe { - __elephc_eval_value_array_set(arg_array.as_ptr(), index.as_ptr(), value.as_ptr()) - })?; - } + let arg_array = Self::arg_array(args)?; let result = Self::handle(unsafe { __elephc_eval_value_method_call( object.as_ptr(), @@ -276,6 +286,25 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Calls a public AOT constructor through the generated user bridge when one exists. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let arg_array = Self::arg_array(args)?; + let ok = + unsafe { __elephc_eval_value_construct_object(object.as_ptr(), arg_array.as_ptr()) }; + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + if ok == 0 { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } + } + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs new file mode 100644 index 0000000000..4e2958fd65 --- /dev/null +++ b/src/codegen/eval_constructor_helpers.rs @@ -0,0 +1,481 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-eval run public native +//! constructors after allocating AOT objects by class name. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - The cacheable runtime object can allocate by name, but only user assembly +//! knows constructor symbols and parameter ABI shapes. +//! - Classes without constructors are treated as successful no-ops, matching PHP. + +use std::collections::BTreeMap; + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::{method_symbol, php_symbol_key}; +use crate::parser::ast::Visibility; +use crate::types::{ClassInfo, FunctionSig, PhpType}; + +const MAX_EVAL_CONSTRUCTOR_ARGS: usize = 2; + +/// Constructor metadata needed by the eval constructor bridge. +#[derive(Clone)] +struct EvalConstructorSlot { + class_id: u64, + class_name: String, + impl_class: String, + params: Vec, + supported: bool, +} + +/// Emits eval constructor helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_constructor_helpers(module: &Module, emitter: &mut Emitter) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_constructor_slots(module); + emit_constructor_helper(module, emitter, &slots); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function + .locals + .iter() + .any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Collects AOT constructors backed by emitted EIR symbols in stable class-id order. +fn collect_eval_constructor_slots(module: &Module) -> Vec { + let emitted_methods = super::eir_class_method_keys(module); + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_constructor_slot(class_name, class_info, &emitted_methods, &mut slots); + } + slots +} + +/// Adds one constructor slot for a class when the constructor has emitted code. +fn collect_class_constructor_slot( + class_name: &str, + class_info: &ClassInfo, + emitted_methods: &std::collections::HashSet<(String, String, bool)>, + slots: &mut Vec, +) { + let method_key = php_symbol_key("__construct"); + let Some(sig) = class_info.methods.get(&method_key) else { + return; + }; + let impl_class = class_info + .method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + if !emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), false)) { + return; + } + let supported = constructor_is_public(class_info, &method_key) + && constructor_signature_supported(sig); + let params = if supported { + sig.params + .iter() + .map(|(_, ty)| ty.codegen_repr()) + .collect() + } else { + Vec::new() + }; + slots.push(EvalConstructorSlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + impl_class: impl_class.to_string(), + params, + supported, + }); +} + +/// Returns true when the constructor is publicly visible to runtime eval. +fn constructor_is_public(class_info: &ClassInfo, method_key: &str) -> bool { + class_info + .method_visibilities + .get(method_key) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + +/// Returns true for constructor signatures supported by this eval bridge slice. +fn constructor_signature_supported(sig: &FunctionSig) -> bool { + sig.params.len() <= MAX_EVAL_CONSTRUCTOR_ARGS + && sig.variadic.is_none() + && sig.ref_params.iter().all(|is_ref| !*is_ref) + && sig + .params + .iter() + .all(|(_, ty)| constructor_param_supported(ty)) +} + +/// Returns true for one constructor argument type supported by the bridge. +fn constructor_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str | PhpType::Mixed + ) +} + +/// Emits `__elephc_eval_value_construct_object(Mixed*, MixedArray*) -> bool`. +fn emit_constructor_helper( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalConstructorSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user constructor call ---"); + label_c_global(module, emitter, "__elephc_eval_value_construct_object"); + match module.target.arch { + Arch::AArch64 => emit_constructor_aarch64(module, emitter, slots), + Arch::X86_64 => emit_constructor_x86_64(module, emitter, slots), + } +} + +/// Emits the ARM64 constructor helper body. +fn emit_constructor_aarch64( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalConstructorSlot], +) { + let success_label = "__elephc_eval_value_construct_success"; + let fail_label = "__elephc_eval_value_construct_fail"; + let done_label = "__elephc_eval_value_construct_done"; + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for args, object, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #24]"); // save the boxed eval argument array + emitter.instruction(&format!("cbz x0, {}", success_label)); // a null object pointer means there is nothing to construct + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", success_label)); // non-object values have no constructor to run + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for constructor calls + emit_aarch64_constructor_dispatch(module, emitter, slots, fail_label, success_label); + emitter.instruction(&format!("b {}", success_label)); // no constructor metadata matched this class id + emitter.label(fail_label); + emitter.instruction("mov x0, #0"); // report constructor dispatch failure to Rust + emitter.instruction(&format!("b {}", done_label)); // skip the success result after a failure + emitter.label(success_label); + emitter.instruction("mov x0, #1"); // report successful construction or no-op + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the constructor helper frame + emitter.instruction("ret"); // return the constructor status flag to Rust +} + +/// Emits the x86_64 constructor helper body. +fn emit_constructor_x86_64( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalConstructorSlot], +) { + let success_label = "__elephc_eval_value_construct_success_x"; + let fail_label = "__elephc_eval_value_construct_fail_x"; + let done_label = "__elephc_eval_value_construct_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for object, args, and temp values + emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the boxed eval argument array + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", success_label)); // a null object pointer means there is nothing to construct + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", success_label)); // non-object values have no constructor to run + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for constructor calls + emit_x86_64_constructor_dispatch(module, emitter, slots, fail_label, success_label); + emitter.instruction(&format!("jmp {}", success_label)); // no constructor metadata matched this class id + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report constructor dispatch failure to Rust + emitter.instruction(&format!("jmp {}", done_label)); // skip the success result after a failure + emitter.label(success_label); + emitter.instruction("mov eax, 1"); // report successful construction or no-op + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the constructor status flag to Rust +} + +/// Emits ARM64 class-id dispatch for supported constructor bodies. +fn emit_aarch64_constructor_dispatch( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalConstructorSlot], + fail_label: &str, + success_label: &str, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let next_label = format!("__elephc_eval_constructor_next_{}", class_id); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer before this class test + emitter.instruction("ldr x9, [x9]"); // load the receiver class id for constructor dispatch + abi::emit_load_int_immediate(emitter, "x10", class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this constructor class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next constructor class when ids differ + for slot in class_slots { + emit_aarch64_constructor_body(module, emitter, slot, fail_label, success_label); + } + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-id dispatch for supported constructor bodies. +fn emit_x86_64_constructor_dispatch( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalConstructorSlot], + fail_label: &str, + success_label: &str, +) { + for (class_id, class_slots) in grouped_slots(slots) { + let next_label = format!("__elephc_eval_constructor_next_{}_x", class_id); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer before this class test + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the receiver class id for constructor dispatch + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this constructor class + emitter.instruction(&format!("jne {}", next_label)); // try the next constructor class when ids differ + for slot in class_slots { + emit_x86_64_constructor_body(module, emitter, slot, fail_label, success_label); + } + emitter.label(&next_label); + } +} + +/// Emits one ARM64 constructor body or failure branch for an unsupported constructor. +fn emit_aarch64_constructor_body( + module: &Module, + emitter: &mut Emitter, + slot: &EvalConstructorSlot, + fail_label: &str, + success_label: &str, +) { + if !slot.supported { + emitter.instruction(&format!("b {}", fail_label)); // reject constructors outside this bridge's supported ABI slice + return; + } + emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); + let overflow_bytes = emit_aarch64_prepare_constructor_args(module, emitter, slot); + abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emitter.instruction(&format!("b {}", success_label)); // constructor returned normally +} + +/// Emits one x86_64 constructor body or failure branch for an unsupported constructor. +fn emit_x86_64_constructor_body( + module: &Module, + emitter: &mut Emitter, + slot: &EvalConstructorSlot, + fail_label: &str, + success_label: &str, +) { + if !slot.supported { + emitter.instruction(&format!("jmp {}", fail_label)); // reject constructors outside this bridge's supported ABI slice + return; + } + emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); + let overflow_bytes = emit_x86_64_prepare_constructor_args(module, emitter, slot); + abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emitter.instruction(&format!("jmp {}", success_label)); // constructor returned normally +} + +/// Emits ARM64 arity validation for one constructor body. +fn emit_aarch64_validate_constructor_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalConstructorSlot, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); + emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the constructor signature + emitter.instruction(&format!("b.ne {}", fail_label)); // reject constructor dispatch when arity differs +} + +/// Emits x86_64 arity validation for one constructor body. +fn emit_x86_64_validate_constructor_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalConstructorSlot, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); + emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the constructor signature + emitter.instruction(&format!("jne {}", fail_label)); // reject constructor dispatch when arity differs +} + +/// Prepares ARM64 constructor ABI registers for the supported argument shapes. +fn emit_aarch64_prepare_constructor_args( + module: &Module, + emitter: &mut Emitter, + slot: &EvalConstructorSlot, +) -> usize { + let receiver_ty = PhpType::Object(slot.class_name.clone()); + emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first constructor argument + abi::emit_push_result_value(emitter, &receiver_ty); + for (index, param_ty) in slot.params.iter().enumerate() { + emit_aarch64_load_eval_arg(module, emitter, index); + emit_aarch64_cast_eval_arg(emitter, param_ty); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + materialize_constructor_args(module, emitter, &receiver_ty, &slot.params) +} + +/// Prepares x86_64 constructor ABI registers for the supported argument shapes. +fn emit_x86_64_prepare_constructor_args( + module: &Module, + emitter: &mut Emitter, + slot: &EvalConstructorSlot, +) -> usize { + let receiver_ty = PhpType::Object(slot.class_name.clone()); + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first constructor argument + abi::emit_push_result_value(emitter, &receiver_ty); + for (index, param_ty) in slot.params.iter().enumerate() { + emit_x86_64_load_eval_arg(module, emitter, index); + emit_x86_64_cast_eval_arg(emitter, param_ty); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + materialize_constructor_args(module, emitter, &receiver_ty, &slot.params) +} + +/// Materializes the pushed receiver and eval arguments into the target method ABI. +fn materialize_constructor_args( + module: &Module, + emitter: &mut Emitter, + receiver_ty: &PhpType, + params: &[PhpType], +) -> usize { + let mut arg_types = Vec::with_capacity(params.len() + 1); + arg_types.push(receiver_ty.clone()); + arg_types.extend(params.iter().map(|param| param.codegen_repr())); + let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); + abi::materialize_outgoing_args(emitter, &assignments) +} + +/// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. +fn emit_aarch64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "x0", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("str x0, [x29, #-16]"); // save the boxed index while loading from the argument array + emitter.instruction("ldr x1, [x29, #-16]"); // pass the boxed index to the eval array reader + emitter.instruction("ldr x0, [x29, #-24]"); // pass the eval argument array to the reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("str x0, [x29, #-16]"); // save the boxed eval argument for coercion +} + +/// Loads one eval argument into an x86_64 spill slot as a boxed Mixed cell. +fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "rdi", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed index while loading from the argument array + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the boxed index to the eval array reader + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // pass the eval argument array to the reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the boxed eval argument for coercion +} + +/// Casts one boxed eval argument into ARM64 result registers for temporary staging. +fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { + match param_ty.codegen_repr() { + PhpType::Int => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the eval argument to a PHP int + } + PhpType::Bool => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("bl __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool + } + PhpType::Float => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in d0 + } + PhpType::Str => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion + emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 + } + PhpType::Mixed => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for a Mixed constructor parameter + } + _ => {} + } +} + +/// Casts one boxed eval argument into x86_64 result registers for temporary staging. +fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { + match param_ty.codegen_repr() { + PhpType::Int => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce the eval argument to a PHP int + } + PhpType::Bool => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for boolean coercion + emitter.instruction("call __rt_mixed_cast_bool"); // coerce the eval argument to a PHP bool + } + PhpType::Float => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for float coercion + emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval argument to a PHP float in xmm0 + } + PhpType::Str => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion + emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair + } + PhpType::Mixed => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for a Mixed constructor parameter + } + _ => {} + } +} + +/// Groups constructor slots by class id while preserving sorted class order. +fn grouped_slots(slots: &[EvalConstructorSlot]) -> BTreeMap> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped.entry(slot.class_id).or_insert_with(Vec::new).push(slot); + } + grouped +} + +/// Emits a platform-C global label for a user assembly helper. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + emitter.label_global(&module.target.extern_symbol(name)); +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 118710a036..f6a4176449 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -11,6 +11,7 @@ mod block_emit; pub(crate) mod context; +mod eval_constructor_helpers; mod eval_method_helpers; mod eval_property_helpers; mod fibers; @@ -194,6 +195,7 @@ fn finalize_user_asm( exported_functions: &HashMap, ) -> String { eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); + eval_constructor_helpers::emit_eval_constructor_helpers(module, &mut emitter); eval_method_helpers::emit_eval_method_helpers(module, &mut emitter, &mut data); let data_output = data.emit(); let empty_globals = HashSet::::new(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3ffeb29892..56e59266c5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1904,7 +1904,7 @@ fn test_eval_unsupported_class_declaration_fails() { ); } -/// Verifies eval can construct an AOT class with no constructor arguments. +/// Verifies eval can construct an AOT class with no declared constructor. #[test] fn test_eval_dynamic_new_constructs_aot_class() { let out = compile_and_run( @@ -1918,6 +1918,50 @@ echo eval('$box = new EvalDynamicNewSupported(); return $box->x;'); assert_eq!(out, "7"); } +/// Verifies eval object construction runs an AOT zero-argument constructor. +#[test] +fn test_eval_dynamic_new_runs_zero_arg_constructor() { + let out = compile_and_run( + r#"x = 9; } +} +echo eval('$box = new EvalDynamicNewZeroArgCtor(); return $box->x;'); +"#, + ); + assert_eq!(out, "9"); +} + +/// Verifies eval object construction passes positional arguments to an AOT constructor. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_arg() { + let out = compile_and_run( + r#"x = $x; } +} +echo eval('$box = new EvalDynamicNewOneArgCtor(11); return $box->x;'); +"#, + ); + assert_eq!(out, "11"); +} + +/// Verifies eval follows PHP by accepting constructor arguments when no constructor exists. +#[test] +fn test_eval_dynamic_new_accepts_args_without_constructor() { + let out = compile_and_run( + r#"x;'); +"#, + ); + assert_eq!(out, "4"); +} + /// Verifies eval reference assignments update the referenced caller local. #[test] fn test_eval_reference_assignment_updates_caller_local() { From 7ccfb3bd9e9356a23ab42f06748a8069986340a1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 04:38:50 +0200 Subject: [PATCH 0110/1208] Refresh eval parser new-object test --- crates/elephc-eval/src/parser.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 458ee0627f..79e2880ff7 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -2975,12 +2975,12 @@ mod tests { ); } - /// Verifies unsupported dynamic object construction reports the unsupported construct status. + /// Verifies malformed object construction reports an unexpected token. #[test] - fn parse_fragment_rejects_new_as_unsupported_construct() { + fn parse_fragment_rejects_new_without_class_name() { assert_eq!( - parse_fragment(b"return new DynEvalUnsupported();"), - Err(EvalParseError::UnsupportedConstruct) + parse_fragment(b"return new ();"), + Err(EvalParseError::UnexpectedToken) ); } From 2a9861e21f18847206067eaaf4e56844b7326ae7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 04:41:08 +0200 Subject: [PATCH 0111/1208] Parse qualified eval dynamic new classes --- crates/elephc-eval/src/parser.rs | 42 +++++++++++++++++++++++++++++--- tests/codegen/eval.rs | 15 ++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 79e2880ff7..2872010898 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -97,6 +97,7 @@ enum TokenKind { RBrace, Comma, Colon, + Backslash, Eof, } @@ -369,6 +370,10 @@ impl<'a> Lexer<'a> { self.bump_char(); Ok(TokenKind::Colon) } + '\\' => { + self.bump_char(); + Ok(TokenKind::Backslash) + } _ if is_ident_start(ch) => { let ident = self.lex_ident(); Ok(magic_const_token(&ident, line).unwrap_or(TokenKind::Ident(ident))) @@ -1514,13 +1519,28 @@ impl Parser { /// Parses `new ClassName(...)` expressions in eval fragments. fn parse_new_object_expr(&mut self) -> Result { self.advance(); - let TokenKind::Ident(class_name) = self.current() else { + let class_name = self.parse_class_name()?; + let args = self.parse_call_args()?; + Ok(EvalExpr::NewObject { class_name, args }) + } + + /// Parses a simple or explicitly qualified class name after `new`. + fn parse_class_name(&mut self) -> Result { + self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; - let class_name = class_name.clone(); + let mut class_name = first.clone(); self.advance(); - let args = self.parse_call_args()?; - Ok(EvalExpr::NewObject { class_name, args }) + while self.consume(TokenKind::Backslash) { + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + class_name.push('\\'); + class_name.push_str(part); + self.advance(); + } + Ok(class_name) } /// Parses a parenthesized source-order argument list. @@ -2809,6 +2829,20 @@ mod tests { ); } + /// Verifies object construction accepts explicitly qualified class names. + #[test] + fn parse_fragment_accepts_qualified_new_object_source() { + let program = + parse_fragment(br#"return new \EvalNs\Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "EvalNs\\Box".to_string(), + args: Vec::new(), + }))] + ); + } + /// Verifies object method calls preserve source-order argument expressions. #[test] fn parse_fragment_accepts_method_call_args_source() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 56e59266c5..43169ad466 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1962,6 +1962,21 @@ echo eval('$box = new EvalDynamicNewNoCtorArgs(99); return $box->x;'); assert_eq!(out, "4"); } +/// Verifies eval can construct explicitly qualified namespaced AOT classes. +#[test] +fn test_eval_dynamic_new_constructs_qualified_aot_class() { + let out = compile_and_run( + r#"x;'); +"#, + ); + assert_eq!(out, "13"); +} + /// Verifies eval reference assignments update the referenced caller local. #[test] fn test_eval_reference_assignment_updates_caller_local() { From 935f43589058b49fe24b6ec62c707020b7a94df6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 04:42:32 +0200 Subject: [PATCH 0112/1208] Parse qualified eval function calls --- crates/elephc-eval/src/parser.rs | 35 +++++++++++++++++++++++++++++--- tests/codegen/eval.rs | 4 ++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 2872010898..e452d6a0c6 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1491,6 +1491,10 @@ impl Parser { TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { Err(EvalParseError::UnsupportedConstruct) } + TokenKind::Backslash => self.parse_qualified_call_expr(), + TokenKind::Ident(_) if matches!(self.peek(), TokenKind::Backslash) => { + self.parse_qualified_call_expr() + } TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { self.parse_call_expr(name.clone()) } @@ -1516,16 +1520,26 @@ impl Parser { }) } + /// Parses an explicitly qualified function-like call expression. + fn parse_qualified_call_expr(&mut self) -> Result { + let name = self.parse_qualified_name()?; + let args = self.parse_call_args()?; + Ok(EvalExpr::Call { + name: name.to_ascii_lowercase(), + args, + }) + } + /// Parses `new ClassName(...)` expressions in eval fragments. fn parse_new_object_expr(&mut self) -> Result { self.advance(); - let class_name = self.parse_class_name()?; + let class_name = self.parse_qualified_name()?; let args = self.parse_call_args()?; Ok(EvalExpr::NewObject { class_name, args }) } - /// Parses a simple or explicitly qualified class name after `new`. - fn parse_class_name(&mut self) -> Result { + /// Parses a simple or explicitly qualified PHP name. + fn parse_qualified_name(&mut self) -> Result { self.consume(TokenKind::Backslash); let TokenKind::Ident(first) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -2633,6 +2647,21 @@ mod tests { ); } + /// Verifies explicitly qualified call expressions normalize away the leading slash. + #[test] + fn parse_fragment_accepts_qualified_call_expression_source() { + let program = parse_fragment(br#"return \strlen("abcd");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "strlen".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "abcd".to_string() + )))], + }))] + ); + } + /// Verifies function calls preserve named arguments in source order. #[test] fn parse_fragment_accepts_named_call_argument_source() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 43169ad466..4503b13d49 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -681,10 +681,10 @@ fn test_eval_nested_eval_return_value_is_expression_result() { fn test_eval_dispatches_simple_builtin_calls() { let out = compile_and_run( r#" Date: Tue, 16 Jun 2026 04:49:27 +0200 Subject: [PATCH 0113/1208] Support eval class_exists for AOT classes --- crates/elephc-eval/src/interpreter.rs | 72 +++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 6 ++ src/codegen_support/runtime/eval_bridge.rs | 94 ++++++++++++++++++++++ tests/codegen/eval.rs | 15 ++++ 4 files changed, 187 insertions(+) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c5c9b929df..94bcb2d32b 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -108,6 +108,9 @@ pub trait RuntimeValueOps { args: Vec, ) -> Result<(), EvalStatus>; + /// Returns whether a runtime class table contains the requested class name. + fn class_exists(&mut self, name: &str) -> Result; + /// Returns the visible element count for an array-like runtime cell. fn array_len(&mut self, array: RuntimeCellHandle) -> Result; @@ -1115,6 +1118,7 @@ fn eval_positional_expr_call( "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "class_exists" => eval_builtin_class_exists(args, context, scope, values), "chop" => eval_builtin_trim_like(name, args, context, scope, values), "boolval" | "floatval" | "intval" | "strval" => { eval_builtin_cast(name, args, context, scope, values) @@ -1173,6 +1177,49 @@ fn eval_builtin_function_probe( values.bool_value(eval_function_probe_exists(context, &name)) } +/// Evaluates `class_exists(...)` against the generated AOT class-name table. +fn eval_builtin_class_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_exists_name(name, values)?; + values.bool_value(exists) +} + +/// Evaluates `class_exists(...)` from already materialized call arguments. +fn eval_class_exists_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_class_exists_name(*name, values)?, + [name, _autoload] => eval_class_exists_name(*name, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-name cell and probes the generated class table. +fn eval_class_exists_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + values.class_exists(name.trim_start_matches('\\')) +} + /// Evaluates PHP's `isset(...)` language construct over eval-visible values. fn eval_builtin_isset( args: &[EvalExpr], @@ -1262,6 +1309,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "ceil" | "call_user_func" | "call_user_func_array" + | "class_exists" | "boolval" | "chop" | "count" @@ -1400,6 +1448,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), + "class_exists" => Some(&["class", "autoload"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), @@ -1694,6 +1743,7 @@ fn eval_builtin_with_values( let name = name.trim_start_matches('\\').to_ascii_lowercase(); values.bool_value(eval_function_probe_exists(context, &name))? } + "class_exists" => eval_class_exists_result(evaluated_args, values)?, "gettype" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3493,6 +3543,11 @@ mod tests { Ok(()) } + /// Reports one fake AOT class for eval `class_exists` unit tests. + fn class_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownClass")) + } + /// Returns the visible element count for fake array values. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { match self.get(array) { @@ -6078,6 +6133,23 @@ echo function_exists("missing_probe") . "x";"#, assert_eq!(values.output, "1x1x1x1xxx"); } + /// Verifies eval class probes use the runtime class-name table. + #[test] + fn execute_program_class_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo class_exists("KnownClass") ? "Y" : "N"; +echo class_exists("\knownclass") ? "Y" : "N"; +echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYN"); + } + /// Verifies eval fragments can dispatch registered native AOT functions. #[test] fn execute_program_calls_registered_native_function() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 8ce1f86bb0..04765937b4 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -63,6 +63,7 @@ unsafe extern "C" { object: *mut RuntimeCell, args: *mut RuntimeCell, ) -> u64; + fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; @@ -305,6 +306,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } + /// Returns whether the generated AOT class-name table contains the requested class. + fn class_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_class_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 51e402600a..3e7aa615e6 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -10,6 +10,7 @@ //! referenced from Rust object files, while internal `__rt_*` calls keep the //! existing assembly ABI. +use crate::codegen_support::abi; use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; @@ -63,6 +64,53 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the dynamic-object wrapper frame emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_class_exists"); + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class-name lookup state + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across string compares + emitter.instruction("add x29, sp, #48"); // establish a stable class-exists frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + abi::emit_symbol_address(emitter, "x9", "_classes_by_name_count"); + emitter.instruction("ldr x9, [x9]"); // load the registered class-name count + emitter.instruction("cbz x9, __elephc_eval_class_exists_miss"); // an empty table cannot contain the requested class + emitter.instruction("str x9, [sp, #16]"); // save the table count across string compares + abi::emit_symbol_address(emitter, "x10", "_classes_by_name"); + emitter.instruction("str x10, [sp, #24]"); // save the current class-name table cursor + emitter.instruction("mov x11, #0"); // start scanning at table index zero + emitter.label("__elephc_eval_class_exists_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the class-name table count + emitter.instruction("cmp x11, x9"); // have all class-name entries been scanned? + emitter.instruction("b.ge __elephc_eval_class_exists_miss"); // no class matched before the end of the table + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-name table entry + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_class_exists_skip"); // length mismatch means this entry cannot match + emitter.instruction("str x11, [sp, #32]"); // save the table index across the string compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #32]"); // restore the table index after the string compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this entry? + emitter.instruction("b.eq __elephc_eval_class_exists_hit"); // report true on a class-name match + emitter.label("__elephc_eval_class_exists_skip"); + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-name table entry + emitter.instruction("add x10, x10, #32"); // advance to the next class-name table entry + emitter.instruction("str x10, [sp, #24]"); // persist the advanced table cursor + emitter.instruction("add x11, x11, #1"); // advance the table index + emitter.instruction("b __elephc_eval_class_exists_loop"); // continue scanning the class-name table + emitter.label("__elephc_eval_class_exists_hit"); + emitter.instruction("mov x0, #1"); // return true for a matched class name + emitter.instruction("b __elephc_eval_class_exists_done"); // skip the false result after a match + emitter.label("__elephc_eval_class_exists_miss"); + emitter.instruction("mov x0, #0"); // return false when no class-name entry matched + emitter.label("__elephc_eval_class_exists_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the class-exists helper frame + emitter.instruction("ret"); // return the class-exists flag to Rust + label_c_global(emitter, "__elephc_eval_value_array_new"); emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for array allocation and boxing emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls @@ -1068,6 +1116,52 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_class_exists"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable class-exists frame pointer + emitter.instruction("sub rsp, 48"); // reserve slots for name, count, cursor, and index + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + abi::emit_symbol_address(emitter, "r10", "_classes_by_name_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the registered class-name count + emitter.instruction("test r10, r10"); // is the class-name table empty? + emitter.instruction("jz __elephc_eval_class_exists_miss_x86"); // an empty table cannot contain the requested class + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the table count across string compares + abi::emit_symbol_address(emitter, "r11", "_classes_by_name"); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current class-name table cursor + emitter.instruction("xor r11d, r11d"); // start scanning at table index zero + emitter.label("__elephc_eval_class_exists_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the class-name table count + emitter.instruction("cmp r11, r10"); // have all class-name entries been scanned? + emitter.instruction("jae __elephc_eval_class_exists_miss_x86"); // no class matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-name table entry + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_class_exists_skip_x86"); // length mismatch means this entry cannot match + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the table index across the string compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // restore the table index after the string compare + emitter.instruction("test rax, rax"); // did the requested class name match this entry? + emitter.instruction("je __elephc_eval_class_exists_hit_x86"); // report true on a class-name match + emitter.label("__elephc_eval_class_exists_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-name table entry + emitter.instruction("add r10, 32"); // advance to the next class-name table entry + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the advanced table cursor + emitter.instruction("inc r11"); // advance the table index + emitter.instruction("jmp __elephc_eval_class_exists_loop_x86"); // continue scanning the class-name table + emitter.label("__elephc_eval_class_exists_hit_x86"); + emitter.instruction("mov eax, 1"); // return true for a matched class name + emitter.instruction("jmp __elephc_eval_class_exists_done_x86"); // skip the false result after a match + emitter.label("__elephc_eval_class_exists_miss_x86"); + emitter.instruction("xor eax, eax"); // return false when no class-name entry matched + emitter.label("__elephc_eval_class_exists_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the class-exists flag to Rust + label_c_global(emitter, "__elephc_eval_value_array_new"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4503b13d49..1f3c27b097 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1879,6 +1879,21 @@ echo function_exists("missing_eval_inner_probe") . "x";'); assert_eq!(out, "1x1x1xxx"); } +/// Verifies eval `class_exists()` probes generated AOT class-name metadata. +#[test] +fn test_eval_fragment_class_exists_probes_aot_classes() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 04:51:48 +0200 Subject: [PATCH 0114/1208] Bind eval callable builtin named args --- crates/elephc-eval/src/interpreter.rs | 6 +++++- tests/codegen/eval.rs | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 94bcb2d32b..ed4d4126a9 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1570,7 +1570,11 @@ fn eval_callable_with_call_array_args( return eval_callable_with_values(name, evaluated_args, context, values); } if eval_php_visible_builtin_exists(name) { - return Err(EvalStatus::RuntimeFatal); + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + return Ok(result); } if let Some(function) = context.function(name).cloned() { let evaluated_args = bind_evaluated_function_args(function.params(), evaluated_args)?; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1f3c27b097..20c81e61b5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1888,10 +1888,12 @@ class EvalClassExistsProbe {} eval('echo class_exists("EvalClassExistsProbe") ? "Y" : "N"; echo class_exists("evalclassexistsprobe") ? "Y" : "N"; echo class_exists("\EvalClassExistsProbe") ? "Y" : "N"; +echo call_user_func("class_exists", "EvalClassExistsProbe") ? "Y" : "N"; +echo call_user_func_array("class_exists", ["autoload" => false, "class" => "\EvalClassExistsProbe"]) ? "Y" : "N"; echo class_exists(class: "MissingEvalClassExistsProbe", autoload: false) ? "Y" : "N";'); "#, ); - assert_eq!(out, "YYYN"); + assert_eq!(out, "YYYYYN"); } /// Verifies duplicate eval-declared functions fail through the runtime bridge. From c346d225ff4e2bd6d84d4770cb33bf24ce10d94c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 04:56:47 +0200 Subject: [PATCH 0115/1208] Fail eval dynamic new on missing classes --- crates/elephc-eval/src/runtime_hooks.rs | 15 +++++++++++++-- tests/codegen/eval.rs | 10 ++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 04765937b4..27021f2f1a 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -282,9 +282,20 @@ impl RuntimeValueOps for ElephcRuntimeOps { /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { - Self::handle(unsafe { + let object = Self::handle(unsafe { __elephc_eval_value_new_object(class_name.as_ptr(), class_name.len() as u64) - }) + })?; + match self.is_null(object) { + Ok(false) => Ok(object), + Ok(true) => { + self.release(object)?; + Err(EvalStatus::RuntimeFatal) + } + Err(err) => { + let _ = self.release(object); + Err(err) + } + } } /// Calls a public AOT constructor through the generated user bridge when one exists. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 20c81e61b5..669beecfb1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1979,6 +1979,16 @@ echo eval('$box = new EvalDynamicNewNoCtorArgs(99); return $box->x;'); assert_eq!(out, "4"); } +/// Verifies eval object construction fails when no AOT class matches the name. +#[test] +fn test_eval_dynamic_new_missing_class_fails() { + let err = compile_and_run_expect_failure(" Date: Tue, 16 Jun 2026 05:00:02 +0200 Subject: [PATCH 0116/1208] Document eval AOT class probes --- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 505deaff4c..5bdbb5198f 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, and `is_callable()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 5159eed461..cf6ca4ab91 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT class-name probes through `class_exists()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index f04f4bc5da..27f9abb2bc 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -41,6 +41,14 @@ public function echoLabelSpreadThroughEval(): void { } } +class EvalAotBox { + public int $value = 0; + + public function __construct(int $value) { + $this->value = $value; + } +} + $x = 1; $profile = ["name" => "Ada"]; $result = eval('$x = $x + 2; $created = "dynamic"; return $x + 4;'); @@ -122,6 +130,8 @@ public function echoLabelSpreadThroughEval(): void { $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); +$eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); +$eval_dynamic_new = eval('$box = new EvalAotBox(21); return $box->value;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -186,6 +196,8 @@ public function echoLabelSpreadThroughEval(): void { echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; +echo "eval-class-exists=" . $eval_class_probe . "\n"; +echo "eval-dynamic-new=" . $eval_dynamic_new . "\n"; $counter = new EvalCounter(); $counter->bump(); echo "eval-this-property=" . $counter->value . "\n"; From 46d114a1138fe13465f3362eb7476ba8bd6531b0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 05:08:54 +0200 Subject: [PATCH 0117/1208] Register eval-declared class names --- crates/elephc-eval/src/context.rs | 25 ++++++++++- crates/elephc-eval/src/eval_ir.rs | 3 ++ crates/elephc-eval/src/interpreter.rs | 65 +++++++++++++++++++++++---- crates/elephc-eval/src/parser.rs | 36 +++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 3 ++ tests/codegen/eval.rs | 51 +++++++++++++++++++-- 8 files changed, 168 insertions(+), 19 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 67800de5b7..19cd30c262 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -11,7 +11,7 @@ //! - The handle is intentionally opaque to generated code. //! - No Rust-owned layout is promised across the C ABI. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::ffi::c_void; use crate::abi::ABI_VERSION; @@ -86,6 +86,7 @@ impl NativeFunction { /// grow dynamic registries without exposing them to generated assembly. pub struct ElephcEvalContext { abi_version: u32, + classes: HashSet, functions: HashMap, native_functions: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, @@ -101,6 +102,7 @@ impl ElephcEvalContext { pub fn new() -> Self { Self { abi_version: ABI_VERSION, + classes: HashSet::new(), functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), @@ -117,6 +119,7 @@ impl ElephcEvalContext { pub fn for_abi_version(abi_version: u32) -> Self { Self { abi_version, + classes: HashSet::new(), functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), @@ -133,6 +136,21 @@ impl ElephcEvalContext { self.abi_version } + /// Defines an eval-declared class name, failing if this context already has it. + pub fn define_class(&mut self, name: &str) -> bool { + let key = normalize_class_name(name); + if self.classes.contains(&key) { + return false; + } + self.classes.insert(key); + true + } + + /// Returns true when this eval context has a dynamic class with the requested name. + pub fn has_class(&self, name: &str) -> bool { + self.classes.contains(&normalize_class_name(name)) + } + /// Defines a dynamic user function, failing if the name already exists. pub fn define_function( &mut self, @@ -266,3 +284,8 @@ impl Default for ElephcEvalContext { Self::new() } } + +/// Normalizes PHP class names for the eval dynamic class registry. +fn normalize_class_name(name: &str) -> String { + name.trim_start_matches('\\').to_ascii_lowercase() +} diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index f61dcc61e6..a86d4d85e2 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -68,6 +68,9 @@ pub enum EvalStmt { update: Vec, body: Vec, }, + ClassDecl { + name: String, + }, Foreach { array: EvalExpr, key_name: Option, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ed4d4126a9..e766d910a3 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -549,6 +549,10 @@ fn execute_stmt( scope, values, ), + EvalStmt::ClassDecl { name } => { + execute_class_decl_stmt(name, context, values)?; + Ok(EvalControl::None) + } EvalStmt::Foreach { array, key_name, @@ -655,6 +659,23 @@ fn execute_stmt( } } +/// Registers an empty eval-declared class name in the dynamic class table. +fn execute_class_decl_stmt( + name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = name.trim_start_matches('\\'); + if context.has_class(name) || values.class_exists(name)? { + return Err(EvalStatus::RuntimeFatal); + } + if context.define_class(name) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + /// Executes a PHP `static $name = expr;` declaration in the current eval scope. fn execute_static_var_stmt( name: &str, @@ -1177,7 +1198,7 @@ fn eval_builtin_function_probe( values.bool_value(eval_function_probe_exists(context, &name)) } -/// Evaluates `class_exists(...)` against the generated AOT class-name table. +/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. fn eval_builtin_class_exists( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -1193,31 +1214,37 @@ fn eval_builtin_class_exists( } _ => return Err(EvalStatus::RuntimeFatal), }; - let exists = eval_class_exists_name(name, values)?; + let exists = eval_class_exists_name(name, context, values)?; values.bool_value(exists) } /// Evaluates `class_exists(...)` from already materialized call arguments. fn eval_class_exists_result( evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let exists = match evaluated_args { - [name] => eval_class_exists_name(*name, values)?, - [name, _autoload] => eval_class_exists_name(*name, values)?, + [name] => eval_class_exists_name(*name, context, values)?, + [name, _autoload] => eval_class_exists_name(*name, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }; values.bool_value(exists) } -/// Normalizes a PHP class-name cell and probes the generated class table. +/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. fn eval_class_exists_name( name: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let name = values.string_bytes(name)?; let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - values.class_exists(name.trim_start_matches('\\')) + let name = name.trim_start_matches('\\'); + if context.has_class(name) { + return Ok(true); + } + values.class_exists(name) } /// Evaluates PHP's `isset(...)` language construct over eval-visible values. @@ -1747,7 +1774,7 @@ fn eval_builtin_with_values( let name = name.trim_start_matches('\\').to_ascii_lowercase(); values.bool_value(eval_function_probe_exists(context, &name))? } - "class_exists" => eval_class_exists_result(evaluated_args, values)?, + "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, "gettype" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3029,6 +3056,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has EvalStmt::ArrayAppendVar { .. } | EvalStmt::ArraySetVar { .. } | EvalStmt::Break + | EvalStmt::ClassDecl { .. } | EvalStmt::Continue | EvalStmt::Echo(_) | EvalStmt::Expr(_) @@ -6141,7 +6169,10 @@ echo function_exists("missing_probe") . "x";"#, #[test] fn execute_program_class_exists_uses_runtime_probe() { let program = parse_fragment( - br#"echo class_exists("KnownClass") ? "Y" : "N"; + br#"class DynProbe {} +echo class_exists("DynProbe") ? "Y" : "N"; +echo class_exists("\dynprobe") ? "Y" : "N"; +echo class_exists("KnownClass") ? "Y" : "N"; echo class_exists("\knownclass") ? "Y" : "N"; echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, ) @@ -6151,7 +6182,23 @@ echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "YYN"); + assert_eq!(values.output, "YYYYN"); + } + + /// Verifies duplicate eval-declared class names fail through runtime status. + #[test] + fn execute_program_duplicate_class_declaration_fails() { + let program = parse_fragment( + br#"class DynProbeDup {} +class dynprobedup {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("duplicate fails"); + + assert_eq!(err, EvalStatus::RuntimeFatal); } /// Verifies eval fragments can dispatch registered native AOT functions. diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index e452d6a0c6..dadb345846 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -575,6 +575,7 @@ impl Parser { } TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), + TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), @@ -710,6 +711,22 @@ impl Parser { }]) } + /// Parses an empty `class Name {}` declaration for dynamic class-name registration. + fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LBrace)?; + if !self.consume(TokenKind::RBrace) { + return Err(EvalParseError::UnsupportedConstruct); + } + self.consume_semicolon(); + Ok(vec![EvalStmt::ClassDecl { name }]) + } + /// Parses `function name($param, ...) { ... }` declarations. fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -1767,7 +1784,6 @@ fn ident_eq(actual: &str, expected: &str) -> bool { /// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. fn is_unsupported_statement_keyword(name: &str) -> bool { [ - "class", "enum", "interface", "namespace", @@ -3029,11 +3045,23 @@ mod tests { ); } - /// Verifies unsupported class declarations report the unsupported construct status. + /// Verifies empty class declarations lower to dynamic class-registration statements. + #[test] + fn parse_fragment_accepts_empty_class_declaration_source() { + let program = parse_fragment(b"class DynEvalClass {};").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl { + name: "DynEvalClass".to_string(), + }] + ); + } + + /// Verifies non-empty class declarations stay outside the supported eval subset. #[test] - fn parse_fragment_rejects_class_as_unsupported_construct() { + fn parse_fragment_rejects_non_empty_class_as_unsupported_construct() { assert_eq!( - parse_fragment(b"class DynEvalUnsupported {}"), + parse_fragment(b"class DynEvalUnsupported { public int $x = 1; }"), Err(EvalParseError::UnsupportedConstruct) ); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 5bdbb5198f..5a41b36816 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Dynamic class declarations and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index cf6ca4ab91..6a61b26856 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT class-name probes through `class_exists()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index 27f9abb2bc..5d1e26aade 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -131,6 +131,8 @@ public function __construct(int $value) { $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); +eval('class EvalDynamicEmptyClass {}'); +$eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); $eval_dynamic_new = eval('$box = new EvalAotBox(21); return $box->value;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -197,6 +199,7 @@ public function __construct(int $value) { echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "eval-class-exists=" . $eval_class_probe . "\n"; +echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; echo "eval-dynamic-new=" . $eval_dynamic_new . "\n"; $counter = new EvalCounter(); $counter->bump(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 669beecfb1..d80acaee1f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1911,10 +1911,55 @@ eval('function dyn_eval_dup() { return 2; }'); ); } -/// Verifies unsupported eval class declarations fail through the eval diagnostic path. +/// Verifies eval-declared empty classes are registered for later class probes. #[test] -fn test_eval_unsupported_class_declaration_fails() { - let err = compile_and_run_expect_failure(" Date: Tue, 16 Jun 2026 05:17:24 +0200 Subject: [PATCH 0118/1208] Track eval dynamic constant names --- crates/elephc-eval/src/context.rs | 23 +++++ crates/elephc-eval/src/interpreter.rs | 118 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 19 +++++ 6 files changed, 168 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 19cd30c262..7d0d181e72 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -87,6 +87,7 @@ impl NativeFunction { pub struct ElephcEvalContext { abi_version: u32, classes: HashSet, + constants: HashSet, functions: HashMap, native_functions: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, @@ -103,6 +104,7 @@ impl ElephcEvalContext { Self { abi_version: ABI_VERSION, classes: HashSet::new(), + constants: HashSet::new(), functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), @@ -120,6 +122,7 @@ impl ElephcEvalContext { Self { abi_version, classes: HashSet::new(), + constants: HashSet::new(), functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), @@ -151,6 +154,21 @@ impl ElephcEvalContext { self.classes.contains(&normalize_class_name(name)) } + /// Defines an eval dynamic constant name, failing if it is invalid or already present. + pub fn define_constant(&mut self, name: &str) -> bool { + let key = normalize_constant_name(name); + if key.is_empty() || self.constants.contains(&key) { + return false; + } + self.constants.insert(key); + true + } + + /// Returns true when this eval context has a dynamic constant with the requested name. + pub fn has_constant(&self, name: &str) -> bool { + self.constants.contains(&normalize_constant_name(name)) + } + /// Defines a dynamic user function, failing if the name already exists. pub fn define_function( &mut self, @@ -289,3 +307,8 @@ impl Default for ElephcEvalContext { fn normalize_class_name(name: &str) -> String { name.trim_start_matches('\\').to_ascii_lowercase() } + +/// Normalizes PHP constant names for case-sensitive eval dynamic probes. +fn normalize_constant_name(name: &str) -> String { + name.trim_start_matches('\\').to_string() +} diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index e766d910a3..767048d727 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1145,6 +1145,8 @@ fn eval_positional_expr_call( eval_builtin_cast(name, args, context, scope, values) } "count" => eval_builtin_count(args, context, scope, values), + "define" => eval_builtin_define(args, context, scope, values), + "defined" => eval_builtin_defined(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), @@ -1198,6 +1200,95 @@ fn eval_builtin_function_probe( values.bool_value(eval_function_probe_exists(context, &name)) } +/// Evaluates `define(name, value)` for eval dynamic constant-name registration. +fn eval_builtin_define( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let _value = eval_expr(value, context, scope, values)?; + let defined = eval_define_name(name, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `defined(name)` against eval dynamic constant names. +fn eval_builtin_defined( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let exists = eval_defined_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `define(...)` from already materialized call arguments. +fn eval_define_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, _value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let defined = eval_define_name(*name, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `defined(...)` from already materialized call arguments. +fn eval_defined_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let exists = eval_defined_name(*name, context, values)?; + values.bool_value(exists) +} + +/// Normalizes and registers one eval dynamic constant name. +fn eval_define_name( + name: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + if name.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(context.define_constant(&name)) +} + +/// Normalizes and probes one eval dynamic constant name. +fn eval_defined_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + Ok(context.has_constant(&name)) +} + +/// Reads a PHP constant name from a runtime cell without changing case. +fn eval_constant_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal) +} + /// Evaluates `class_exists(...)` against dynamic and generated class-name tables. fn eval_builtin_class_exists( args: &[EvalExpr], @@ -1340,6 +1431,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "boolval" | "chop" | "count" + | "define" + | "defined" | "fdiv" | "floor" | "floatval" @@ -1478,6 +1571,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "class_exists" => Some(&["class", "autoload"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), + "define" => Some(&["constant_name", "value"]), + "defined" => Some(&["constant_name"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "function_exists" => Some(&["function"]), "hash_equals" => Some(&["known_string", "user_string"]), @@ -1753,6 +1848,8 @@ fn eval_builtin_with_values( let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } + "define" => eval_define_result(evaluated_args, context, values)?, + "defined" => eval_defined_result(evaluated_args, context, values)?, "ord" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -6165,6 +6262,27 @@ echo function_exists("missing_probe") . "x";"#, assert_eq!(values.output, "1x1x1x1xxx"); } + /// Verifies eval `define()` and `defined()` share a dynamic constant-name table. + #[test] + fn execute_program_define_and_defined_use_dynamic_constant_table() { + let program = parse_fragment( + br#"echo define("DynEvalConst", 1) ? "Y" : "N"; +echo defined("DynEvalConst") ? "Y" : "N"; +echo defined("\\DynEvalConst") ? "Y" : "N"; +echo defined("dynevalconst") ? "Y" : "N"; +echo define("DynEvalConst", 2) ? "Y" : "N"; +echo call_user_func("defined", "DynEvalConst") ? "Y" : "N"; +echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYYNNYY"); + } + /// Verifies eval class probes use the runtime class-name table. #[test] fn execute_program_class_exists_uses_runtime_probe() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 5a41b36816..d502d4cdd9 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,9 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. + +Dynamic eval constants currently store case-sensitive names for `define()` / `defined()` probes across eval fragments. Fetching eval-defined constant values from bare constant expressions and emitting duplicate-constant warnings through the eval bridge require a later value-retention and diagnostics hook. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6a61b26856..0776bcf7bd 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,9 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. + +Inside eval, `define()` and `defined()` maintain a dynamic constant-name registry that persists across later eval fragments and callable dispatch. Eval-defined constant value fetches through bare constant names, and duplicate-constant warning emission from the eval bridge, are still future work. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index 5d1e26aade..8b68296980 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -133,6 +133,7 @@ public function __construct(int $value) { $eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); eval('class EvalDynamicEmptyClass {}'); $eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); +$eval_dynamic_const_probe = eval('define("EvalDynamicConst", 1); return defined("EvalDynamicConst") ? "yes" : "no";'); $eval_dynamic_new = eval('$box = new EvalAotBox(21); return $box->value;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -200,6 +201,7 @@ public function __construct(int $value) { echo "string-compare=" . $string_compare . "\n"; echo "eval-class-exists=" . $eval_class_probe . "\n"; echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; +echo "eval-dynamic-const-exists=" . $eval_dynamic_const_probe . "\n"; echo "eval-dynamic-new=" . $eval_dynamic_new . "\n"; $counter = new EvalCounter(); $counter->bump(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d80acaee1f..7698d608b7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1073,6 +1073,25 @@ echo ":"; echo function_exists("gettype");'); ); } +/// Verifies eval `define()` and `defined()` share dynamic constant names across fragments. +#[test] +fn test_eval_define_and_defined_dynamic_constants() { + let out = compile_and_run( + r#" "DynEvalConst"]) ? "Y" : "N";'); +echo eval('return function_exists("define") && function_exists("defined") ? "Y" : "N";'); +"#, + ); + assert_eq!(out, "YYNNYYYYY"); +} + /// Verifies eval `abs()` preserves integer/float result typing through direct and callable calls. #[test] fn test_eval_dispatches_abs_builtin_call() { From 878015f4a7a4025f48640cbc964cd3487a885340 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 05:23:28 +0200 Subject: [PATCH 0119/1208] Fetch eval dynamic constant values --- crates/elephc-eval/src/context.rs | 21 +++++--- crates/elephc-eval/src/eval_ir.rs | 1 + crates/elephc-eval/src/interpreter.rs | 60 +++++++++++++++++++--- crates/elephc-eval/src/parser.rs | 38 ++++++++++---- crates/elephc-eval/src/runtime_hooks.rs | 8 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- src/codegen_support/runtime/eval_bridge.rs | 7 +++ tests/codegen/eval.rs | 14 ++++- 10 files changed, 127 insertions(+), 28 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 7d0d181e72..439e9813cc 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -87,7 +87,7 @@ impl NativeFunction { pub struct ElephcEvalContext { abi_version: u32, classes: HashSet, - constants: HashSet, + constants: HashMap, functions: HashMap, native_functions: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, @@ -104,7 +104,7 @@ impl ElephcEvalContext { Self { abi_version: ABI_VERSION, classes: HashSet::new(), - constants: HashSet::new(), + constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), @@ -122,7 +122,7 @@ impl ElephcEvalContext { Self { abi_version, classes: HashSet::new(), - constants: HashSet::new(), + constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), @@ -154,19 +154,24 @@ impl ElephcEvalContext { self.classes.contains(&normalize_class_name(name)) } - /// Defines an eval dynamic constant name, failing if it is invalid or already present. - pub fn define_constant(&mut self, name: &str) -> bool { + /// Defines an eval dynamic constant value, failing if the name is invalid or already present. + pub fn define_constant(&mut self, name: &str, value: RuntimeCellHandle) -> bool { let key = normalize_constant_name(name); - if key.is_empty() || self.constants.contains(&key) { + if key.is_empty() || self.constants.contains_key(&key) { return false; } - self.constants.insert(key); + self.constants.insert(key, value); true } /// Returns true when this eval context has a dynamic constant with the requested name. pub fn has_constant(&self, name: &str) -> bool { - self.constants.contains(&normalize_constant_name(name)) + self.constants.contains_key(&normalize_constant_name(name)) + } + + /// Returns an eval dynamic constant value by case-sensitive PHP constant name. + pub fn constant(&self, name: &str) -> Option { + self.constants.get(&normalize_constant_name(name)).copied() } /// Defines a dynamic user function, failing if the name already exists. diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index a86d4d85e2..aa73394ed0 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -169,6 +169,7 @@ pub enum EvalExpr { args: Vec, }, Const(EvalConst), + ConstFetch(String), LoadVar(String), MethodCall { object: Box, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 767048d727..a68ab0e4b5 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -126,6 +126,9 @@ pub trait RuntimeValueOps { /// Releases one owned runtime cell that is no longer held by the eval scope. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; + /// Retains one runtime cell so the eval caller receives an independent owner. + fn retain(&mut self, value: RuntimeCellHandle) -> Result; + /// Creates a runtime null cell. fn null(&mut self) -> Result; @@ -901,6 +904,7 @@ fn eval_expr( } EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), + EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), EvalExpr::LoadVar(name) => { visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) } @@ -1211,8 +1215,8 @@ fn eval_builtin_define( return Err(EvalStatus::RuntimeFatal); }; let name = eval_expr(name, context, scope, values)?; - let _value = eval_expr(value, context, scope, values)?; - let defined = eval_define_name(name, context, values)?; + let value = eval_expr(value, context, scope, values)?; + let defined = eval_define_name(name, value, context, values)?; values.bool_value(defined) } @@ -1237,10 +1241,10 @@ fn eval_define_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let [name, _value] = evaluated_args else { + let [name, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - let defined = eval_define_name(*name, context, values)?; + let defined = eval_define_name(*name, *value, context, values)?; values.bool_value(defined) } @@ -1260,6 +1264,7 @@ fn eval_defined_result( /// Normalizes and registers one eval dynamic constant name. fn eval_define_name( name: RuntimeCellHandle, + value: RuntimeCellHandle, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -1267,7 +1272,16 @@ fn eval_define_name( if name.is_empty() { return Err(EvalStatus::RuntimeFatal); } - Ok(context.define_constant(&name)) + if context.has_constant(&name) { + return Ok(false); + } + let value = values.retain(value)?; + if context.define_constant(&name, value) { + Ok(true) + } else { + values.release(value)?; + Ok(false) + } } /// Normalizes and probes one eval dynamic constant name. @@ -3353,6 +3367,18 @@ fn eval_const( } } +/// Loads a retained value for one eval-defined dynamic constant. +fn eval_const_fetch( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(value) = context.constant(name) else { + return Err(EvalStatus::RuntimeFatal); + }; + values.retain(value) +} + /// Resolves one eval magic constant against fragment and dynamic-call metadata. fn eval_magic_const( magic: &EvalMagicConst, @@ -3720,6 +3746,11 @@ mod tests { Ok(()) } + /// Returns the same fake handle because fake cells do not refcount. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + Ok(value) + } + /// Creates a fake null cell. fn null(&mut self) -> Result { Ok(self.alloc(FakeValue::Null)) @@ -6266,7 +6297,9 @@ echo function_exists("missing_probe") . "x";"#, #[test] fn execute_program_define_and_defined_use_dynamic_constant_table() { let program = parse_fragment( - br#"echo define("DynEvalConst", 1) ? "Y" : "N"; + br#"echo define("DynEvalConst", "ok") ? "Y" : "N"; +echo DynEvalConst; +echo \DynEvalConst; echo defined("DynEvalConst") ? "Y" : "N"; echo defined("\\DynEvalConst") ? "Y" : "N"; echo defined("dynevalconst") ? "Y" : "N"; @@ -6280,7 +6313,20 @@ echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "YYYNNYY"); + assert_eq!(values.output, "YokokYYNNYY"); + } + + /// Verifies missing eval dynamic constants fail through runtime status. + #[test] + fn execute_program_missing_constant_fetch_fails() { + let program = parse_fragment(br#"return MissingEvalConst;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); } /// Verifies eval class probes use the runtime class-name table. diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index dadb345846..ec35b2c1ab 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1508,13 +1508,18 @@ impl Parser { TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { Err(EvalParseError::UnsupportedConstruct) } - TokenKind::Backslash => self.parse_qualified_call_expr(), + TokenKind::Backslash => self.parse_qualified_name_expr(), TokenKind::Ident(_) if matches!(self.peek(), TokenKind::Backslash) => { - self.parse_qualified_call_expr() + self.parse_qualified_name_expr() } TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { self.parse_call_expr(name.clone()) } + TokenKind::Ident(name) => { + let name = name.clone(); + self.advance(); + Ok(EvalExpr::ConstFetch(name)) + } TokenKind::LBracket => self.parse_array_literal(), TokenKind::LParen => { self.advance(); @@ -1537,14 +1542,17 @@ impl Parser { }) } - /// Parses an explicitly qualified function-like call expression. - fn parse_qualified_call_expr(&mut self) -> Result { + /// Parses an explicitly qualified call or constant-fetch expression. + fn parse_qualified_name_expr(&mut self) -> Result { let name = self.parse_qualified_name()?; - let args = self.parse_call_args()?; - Ok(EvalExpr::Call { - name: name.to_ascii_lowercase(), - args, - }) + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(EvalExpr::Call { + name: name.to_ascii_lowercase(), + args, + }); + } + Ok(EvalExpr::ConstFetch(name)) } /// Parses `new ClassName(...)` expressions in eval fragments. @@ -2678,6 +2686,18 @@ mod tests { ); } + /// Verifies bare constant names lower to dynamic constant-fetch expressions. + #[test] + fn parse_fragment_accepts_constant_fetch_source() { + let program = parse_fragment(br#"return \Dyn\EvalConst;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ConstFetch( + "Dyn\\EvalConst".to_string() + )))] + ); + } + /// Verifies function calls preserve named arguments in source order. #[test] fn parse_fragment_accepts_named_call_argument_source() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 27021f2f1a..c22f1dd115 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -134,6 +134,7 @@ unsafe extern "C" { ) -> u64; fn __elephc_eval_value_truthy(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_release(value: *mut RuntimeCell); + fn __elephc_eval_value_retain(value: *mut RuntimeCell) -> *mut RuntimeCell; } /// Runtime hook adapter that produces and consumes boxed elephc Mixed cells. @@ -351,6 +352,13 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(()) } + /// Retains one boxed Mixed cell through the generated runtime wrapper. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + Ok(RuntimeCellHandle::from_raw(unsafe { + __elephc_eval_value_retain(value.as_ptr()) + })) + } + /// Creates a boxed null Mixed cell through the generated runtime wrapper. fn null(&mut self) -> Result { Self::handle(unsafe { __elephc_eval_value_null() }) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d502d4cdd9..be0eb8046b 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Dynamic eval constants currently store case-sensitive names for `define()` / `defined()` probes across eval fragments. Fetching eval-defined constant values from bare constant expressions and emitting duplicate-constant warnings through the eval bridge require a later value-retention and diagnostics hook. +Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Emitting duplicate-constant warnings through the eval bridge still requires a later diagnostics hook. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 0776bcf7bd..928f3f6a5b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. -Inside eval, `define()` and `defined()` maintain a dynamic constant-name registry that persists across later eval fragments and callable dispatch. Eval-defined constant value fetches through bare constant names, and duplicate-constant warning emission from the eval bridge, are still future work. +Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Duplicate eval `define()` calls return `false`; duplicate-constant warning emission from the eval bridge is still future work. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index 8b68296980..0ba24612cb 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -133,7 +133,7 @@ public function __construct(int $value) { $eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); eval('class EvalDynamicEmptyClass {}'); $eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); -$eval_dynamic_const_probe = eval('define("EvalDynamicConst", 1); return defined("EvalDynamicConst") ? "yes" : "no";'); +$eval_dynamic_const_probe = eval('define("EvalDynamicConst", "yes"); return EvalDynamicConst;'); $eval_dynamic_new = eval('$box = new EvalAotBox(21); return $box->value;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 3e7aa615e6..dcf137b6ac 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -1073,6 +1073,9 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_truthy"); emitter.instruction("b __rt_mixed_cast_bool"); // cast one boxed mixed value to PHP truthiness for eval + label_c_global(emitter, "__elephc_eval_value_retain"); + emitter.instruction("b __rt_incref"); // retain one eval-owned boxed Mixed cell + label_c_global(emitter, "__elephc_eval_value_release"); emitter.instruction("b __rt_decref_mixed"); // release one eval-owned boxed Mixed cell } @@ -2211,6 +2214,10 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rax, rdi"); // move the C boxed value argument into mixed truthiness input emitter.instruction("jmp __rt_mixed_cast_bool"); // cast one boxed mixed value to PHP truthiness for eval + label_c_global(emitter, "__elephc_eval_value_retain"); + emitter.instruction("mov rax, rdi"); // move the C boxed Mixed argument into the internal retain register + emitter.instruction("jmp __rt_incref"); // retain one eval-owned boxed Mixed cell + label_c_global(emitter, "__elephc_eval_value_release"); emitter.instruction("mov rax, rdi"); // move the C boxed Mixed argument into the internal release register emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7698d608b7..28709851cf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1080,6 +1080,8 @@ fn test_eval_define_and_defined_dynamic_constants() { r#" "DynEvalCo echo eval('return function_exists("define") && function_exists("defined") ? "Y" : "N";'); "#, ); - assert_eq!(out, "YYNNYYYYY"); + assert_eq!(out, "YY77NNYYYYY"); +} + +/// Verifies missing eval dynamic constants fail through the eval runtime path. +#[test] +fn test_eval_missing_dynamic_constant_fetch_fails() { + let err = compile_and_run_expect_failure(" Date: Tue, 16 Jun 2026 05:30:33 +0200 Subject: [PATCH 0120/1208] Warn on duplicate eval define --- crates/elephc-eval/src/interpreter.rs | 16 +++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 9 ++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen_support/runtime/eval_bridge.rs | 8 +++++++ tests/codegen/eval.rs | 26 ++++++++++++++++++++-- 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index a68ab0e4b5..b6c311fd23 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -129,6 +129,9 @@ pub trait RuntimeValueOps { /// Retains one runtime cell so the eval caller receives an independent owner. fn retain(&mut self, value: RuntimeCellHandle) -> Result; + /// Emits or suppresses one PHP runtime warning through the target runtime. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus>; + /// Creates a runtime null cell. fn null(&mut self) -> Result; @@ -286,6 +289,7 @@ const EVAL_TAG_ASSOC: u64 = 5; const EVAL_TAG_OBJECT: u64 = 6; const EVAL_TAG_NULL: u64 = 8; const EVAL_TAG_RESOURCE: u64 = 9; +const DEFINE_ALREADY_DEFINED_WARNING: &str = "Warning: define(): Constant already defined\n"; /// Executes an EvalIR program and returns the eval result cell. pub fn execute_program( @@ -1273,6 +1277,7 @@ fn eval_define_name( return Err(EvalStatus::RuntimeFatal); } if context.has_constant(&name) { + values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; return Ok(false); } let value = values.retain(value)?; @@ -3440,6 +3445,7 @@ mod tests { values: HashMap, output: String, releases: Vec, + warnings: Vec, } impl FakeOps { @@ -3751,6 +3757,12 @@ mod tests { Ok(value) } + /// Records fake PHP warnings without writing to stderr. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + self.warnings.push(message.to_string()); + Ok(()) + } + /// Creates a fake null cell. fn null(&mut self) -> Result { Ok(self.alloc(FakeValue::Null)) @@ -6314,6 +6326,10 @@ echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); assert_eq!(values.output, "YokokYYNNYY"); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); } /// Verifies missing eval dynamic constants fail through runtime status. diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index c22f1dd115..d9e1f2400b 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -68,6 +68,7 @@ unsafe extern "C" { fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_type_tag(value: *mut RuntimeCell) -> u64; + fn __elephc_eval_warning(message_ptr: *const u8, message_len: u64); fn __elephc_eval_value_null() -> *mut RuntimeCell; fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; @@ -359,6 +360,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { })) } + /// Emits one PHP warning through the generated runtime diagnostic helper. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_warning(message.as_ptr(), message.len() as u64); + } + Ok(()) + } + /// Creates a boxed null Mixed cell through the generated runtime wrapper. fn null(&mut self) -> Result { Self::handle(unsafe { __elephc_eval_value_null() }) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index be0eb8046b..3fe316a03a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Emitting duplicate-constant warnings through the eval bridge still requires a later diagnostics hook. +Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 928f3f6a5b..7d48299a0c 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. -Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Duplicate eval `define()` calls return `false`; duplicate-constant warning emission from the eval bridge is still future work. +Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index dcf137b6ac..4a2003134b 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -1076,6 +1076,11 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_retain"); emitter.instruction("b __rt_incref"); // retain one eval-owned boxed Mixed cell + label_c_global(emitter, "__elephc_eval_warning"); + emitter.instruction("mov x2, x1"); // move warning length into the runtime diagnostic length register + emitter.instruction("mov x1, x0"); // move warning pointer into the runtime diagnostic buffer register + emitter.instruction("b __rt_diag_warning"); // emit or suppress one eval runtime warning + label_c_global(emitter, "__elephc_eval_value_release"); emitter.instruction("b __rt_decref_mixed"); // release one eval-owned boxed Mixed cell } @@ -2218,6 +2223,9 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rax, rdi"); // move the C boxed Mixed argument into the internal retain register emitter.instruction("jmp __rt_incref"); // retain one eval-owned boxed Mixed cell + label_c_global(emitter, "__elephc_eval_warning"); + emitter.instruction("jmp __rt_diag_warning"); // emit or suppress one eval runtime warning + label_c_global(emitter, "__elephc_eval_value_release"); emitter.instruction("mov rax, rdi"); // move the C boxed Mixed argument into the internal release register emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 28709851cf..e998e6b8cb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1076,7 +1076,7 @@ echo ":"; echo function_exists("gettype");'); /// Verifies eval `define()` and `defined()` share dynamic constant names across fragments. #[test] fn test_eval_define_and_defined_dynamic_constants() { - let out = compile_and_run( + let out = compile_and_run_capture( r#" "DynEvalCo echo eval('return function_exists("define") && function_exists("defined") ? "Y" : "N";'); "#, ); - assert_eq!(out, "YY77NNYYYYY"); + assert!(out.success, "program failed: {}", out.stderr); + assert_eq!(out.stdout, "YY77NNYYYYY"); + assert!( + out.stderr + .contains("Warning: define(): Constant already defined"), + "expected duplicate eval define warning, got stderr={}", + out.stderr + ); +} + +/// Verifies `@eval(...)` suppresses duplicate eval `define()` warnings. +#[test] +fn test_error_control_suppresses_duplicate_eval_define_warning() { + let out = compile_and_run_capture( + r#" Date: Tue, 16 Jun 2026 05:38:03 +0200 Subject: [PATCH 0121/1208] Probe eval constants from native defined --- crates/elephc-eval/src/lib.rs | 66 +++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/builtins.rs | 8 +++ src/codegen/lower_inst/builtins/eval.rs | 26 ++++++++++ src/ir/instr.rs | 4 +- src/ir/validator.rs | 5 +- src/ir_lower/expr/constants.rs | 11 +++++ src/ir_lower/program.rs | 5 +- tests/codegen/eval.rs | 15 ++++++ 12 files changed, 141 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index e72431a91d..dec1a244ac 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -282,6 +282,21 @@ pub unsafe extern "C" fn __elephc_eval_function_exists( .unwrap_or(0) } +/// Checks whether a constant was previously defined through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_constant_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_constant_exists_inner(ctx, name_ptr, name_len) }) + .unwrap_or(0) +} + /// Registers a generated native PHP function callback in an eval context. /// /// # Safety @@ -414,6 +429,28 @@ unsafe fn eval_function_exists_inner( i32::from(context.has_function(&name.to_ascii_lowercase())) } +/// Runs the eval constant-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_constant_exists`; invalid handles or unreadable name +/// storage fail closed as `false`. +unsafe fn eval_constant_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_constant(&name)) +} + /// Runs the native registration ABI body after installing a panic boundary. /// /// # Safety @@ -904,6 +941,35 @@ mod tests { assert_eq!(missing_result, 0); } + /// Verifies the constant-exists ABI probes eval-defined constants by PHP name. + #[test] + fn constant_exists_reports_defined_eval_constant() { + let mut ctx = ElephcEvalContext::new(); + let value = RuntimeCellHandle::from_raw(1usize as *mut RuntimeCell); + assert!(ctx.define_constant("DynConstProbe", value)); + let existing = b"DynConstProbe"; + let qualified = b"\\DynConstProbe"; + let wrong_case = b"dynconstprobe"; + let missing = b"missing"; + + let existing_result = unsafe { + __elephc_eval_constant_exists(&ctx, existing.as_ptr(), existing.len() as u64) + }; + let qualified_result = unsafe { + __elephc_eval_constant_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) + }; + let wrong_case_result = unsafe { + __elephc_eval_constant_exists(&ctx, wrong_case.as_ptr(), wrong_case.len() as u64) + }; + let missing_result = + unsafe { __elephc_eval_constant_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(qualified_result, 1); + assert_eq!(wrong_case_result, 0); + assert_eq!(missing_result, 0); + } + /// Verifies native AOT registration records function and parameter metadata. #[test] fn register_native_function_reports_function_exists() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3fe316a03a..f82ae1d18c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. +Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7d48299a0c..74aaab1a3d 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. -Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. +Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")` after an eval barrier also probes those dynamic constants. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index 0ba24612cb..587fab7d1b 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -134,6 +134,7 @@ public function __construct(int $value) { eval('class EvalDynamicEmptyClass {}'); $eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); $eval_dynamic_const_probe = eval('define("EvalDynamicConst", "yes"); return EvalDynamicConst;'); +$eval_dynamic_const_native_probe = defined("EvalDynamicConst") ? "yes" : "no"; $eval_dynamic_new = eval('$box = new EvalAotBox(21); return $box->value;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -202,6 +203,7 @@ public function __construct(int $value) { echo "eval-class-exists=" . $eval_class_probe . "\n"; echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; echo "eval-dynamic-const-exists=" . $eval_dynamic_const_probe . "\n"; +echo "native-defined-eval-const=" . $eval_dynamic_const_native_probe . "\n"; echo "eval-dynamic-new=" . $eval_dynamic_new . "\n"; $counter = new EvalCounter(); $counter->bump(); diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 42d23c74f6..b3ff51f402 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -200,6 +200,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), Op::EvalFunctionCallArray => builtins::lower_eval_function_call_array(ctx, &inst), Op::EvalFunctionExists => builtins::lower_eval_function_exists(ctx, &inst), + Op::EvalConstantExists => builtins::lower_eval_constant_exists(ctx, &inst), Op::ClosureCapture => lower_closure_capture(ctx, &inst), Op::ClosureNew => lower_closure_new(ctx, &inst), Op::FirstClassCallableNew => lower_first_class_callable_new(ctx, &inst), diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index be6800fca6..69f39a3777 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -104,6 +104,14 @@ pub(super) fn lower_eval_function_exists( eval::lower_eval_function_exists(ctx, inst) } +/// Lowers post-eval dynamic constant existence probes to the optional eval bridge. +pub(super) fn lower_eval_constant_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_constant_exists(ctx, inst) +} + /// Lowers `define("NAME", value)` with the duplicate-name runtime guard. pub(crate) fn lower_define(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "define", 2)?; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index fd72b24eb4..15646a34e5 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -271,6 +271,32 @@ pub(super) fn lower_eval_function_exists( store_if_result(ctx, inst) } +/// Lowers a post-eval dynamic constant existence probe to the eval bridge ABI. +pub(super) fn lower_eval_constant_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let constant_name = ctx.global_name_data(expect_data(inst)?)?.to_string(); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(constant_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_constant_exists"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Returns the aligned scratch size for an eval-declared function call. fn eval_function_call_stack_bytes(arg_count: usize) -> usize { let bytes = EVAL_STACK_BYTES + arg_count * 8; diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 59b4ec7ac4..c94490e741 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -358,6 +358,7 @@ pub enum Op { EvalFunctionCall, EvalFunctionCallArray, EvalFunctionExists, + EvalConstantExists, RuntimeCall, ExternCall, ClosureNew, @@ -494,7 +495,7 @@ impl Op { EnumBackingStringToInt | EnumBackingMixedToInt => { E::READS_HEAP | E::ALLOC_HEAP | E::MAY_THROW } - EvalFunctionExists => E::READS_GLOBAL, + EvalFunctionExists | EvalConstantExists => E::READS_GLOBAL, Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | EvalFunctionCallArray | RuntimeCall | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { E::all().difference(E::REFCOUNT_OP) @@ -698,6 +699,7 @@ impl Op { EvalFunctionCall => "eval_function_call", EvalFunctionCallArray => "eval_function_call_array", EvalFunctionExists => "eval_function_exists", + EvalConstantExists => "eval_constant_exists", RuntimeCall => "runtime_call", ExternCall => "extern_call", ClosureNew => "closure_new", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 373c5ac2bd..bfe48b4ccf 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -274,7 +274,8 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstBool => require_immediate(inst_id, inst, "bool", |imm| matches!(imm, Imm::Bool(_))), ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionCallArray - | EvalFunctionExists | EnumBackingStringToInt | EnumBackingMixedToInt => { + | EvalFunctionExists | EvalConstantExists | EnumBackingStringToInt + | EnumBackingMixedToInt => { require_immediate(inst_id, inst, "data id", |imm| matches!(imm, Imm::Data(_))) } LoadLocal | StoreLocal | UnsetLocal | LoadRefCell | StoreRefCell | ReleaseLocalRefCell @@ -354,7 +355,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio | ErrorSuppressBegin | ErrorSuppressEnd | TryPushHandler | TryPopHandler | CatchCurrent | CatchBind | FinallyEnter | FinallyExit | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | EvalFunctionExists - | ConcatReset | GcCollect | Nop => { + | EvalConstantExists | ConcatReset | GcCollect | Nop => { check_count(inst_id, inst, 0, "0") } EvalFunctionCallArray => check_count(inst_id, inst, 1, "1"), diff --git a/src/ir_lower/expr/constants.rs b/src/ir_lower/expr/constants.rs index 6755e7937d..cbc4ad197f 100644 --- a/src/ir_lower/expr/constants.rs +++ b/src/ir_lower/expr/constants.rs @@ -48,6 +48,17 @@ pub(super) fn lower_static_defined_call( return None; }; let exists = ctx.constant_value(constant_name).is_some(); + if !exists && ctx.has_eval_barrier() { + let data = ctx.intern_global_name(constant_name); + return Some(ctx.emit_value( + Op::EvalConstantExists, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Bool, + Op::EvalConstantExists.default_effects(), + Some(expr.span), + )); + } Some(emit_typed_constant( ctx, Op::ConstBool, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 6c27af9b87..725897af56 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -132,7 +132,10 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { features.eval = true; } } - Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalFunctionExists => { + Op::EvalFunctionCall + | Op::EvalFunctionCallArray + | Op::EvalFunctionExists + | Op::EvalConstantExists => { features.eval = true; } Op::ExprCall | Op::CallableDescriptorInvoke => { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e998e6b8cb..2b5d40c1c2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1116,6 +1116,21 @@ echo eval('return DynEvalSuppressedConst;'); assert_eq!(out.stderr, ""); } +/// Verifies native `defined()` probes can see constants defined by eval after the barrier. +#[test] +fn test_eval_defined_constant_is_visible_to_native_defined_after_barrier() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 05:43:06 +0200 Subject: [PATCH 0122/1208] Fetch eval constants from native code --- crates/elephc-eval/src/lib.rs | 63 +++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen/context.rs | 1 + src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/builtins.rs | 8 ++++ src/codegen/lower_inst/builtins/eval.rs | 31 ++++++++++++ src/ir/instr.rs | 5 ++ src/ir/validator.rs | 4 +- src/ir_lower/context.rs | 1 + src/ir_lower/expr/constants.rs | 11 +++++ src/ir_lower/program.rs | 3 +- src/types/checker/inference/expr/mod.rs | 10 ++-- tests/codegen/eval.rs | 22 +++++++++ 15 files changed, 158 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index dec1a244ac..d1ddd6e0e6 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -30,6 +30,8 @@ use abi::{ use context::{NativeFunction, NativeFunctionInvoker}; use errors::EvalStatus; #[cfg(not(test))] +use interpreter::RuntimeValueOps; +#[cfg(not(test))] use runtime_hooks::ElephcRuntimeOps; use scope::{ScopeCellOwnership, ScopeEntry}; use std::ffi::c_void; @@ -297,6 +299,25 @@ pub unsafe extern "C" fn __elephc_eval_constant_exists( .unwrap_or(0) } +/// Fetches a constant previously defined through `eval()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_constant_fetch( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_constant_fetch_inner(ctx, name_ptr, name_len, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Registers a generated native PHP function callback in an eval context. /// /// # Safety @@ -451,6 +472,48 @@ unsafe fn eval_constant_exists_inner( i32::from(context.has_constant(&name)) } +/// Runs the eval constant-fetch ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_constant_fetch`; callers must provide a valid context, +/// readable constant-name bytes, and optional writable result storage. +#[cfg(not(test))] +unsafe fn eval_constant_fetch_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + if !out.is_null() { + (*out).clear(); + } + let Some(value) = context.constant(&name) else { + return EvalStatus::RuntimeFatal.code(); + }; + if out.is_null() { + return EvalStatus::Ok.code(); + } + let mut values = ElephcRuntimeOps::new(); + match values.retain(value) { + Ok(result) => { + (*out).kind = 0; + (*out).value_cell = result.as_ptr(); + (*out).error = std::ptr::null_mut(); + EvalStatus::Ok.code() + } + Err(status) => status.code(), + } +} + /// Runs the native registration ABI body after installing a panic boundary. /// /// # Safety diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index f82ae1d18c..4646146d71 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. -Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. +Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 74aaab1a3d..6760e57933 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. -Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")` after an eval barrier also probes those dynamic constants. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. +Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")` and bare constant fetches after an eval barrier also probe those dynamic constants. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index 587fab7d1b..155844c89c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -135,6 +135,7 @@ public function __construct(int $value) { $eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); $eval_dynamic_const_probe = eval('define("EvalDynamicConst", "yes"); return EvalDynamicConst;'); $eval_dynamic_const_native_probe = defined("EvalDynamicConst") ? "yes" : "no"; +$eval_dynamic_const_native_fetch = EvalDynamicConst; $eval_dynamic_new = eval('$box = new EvalAotBox(21); return $box->value;'); eval('function native_add($left, $right) { return $left + $right; }'); eval('function native_double($value) { return $value * 2; }'); @@ -204,6 +205,7 @@ public function __construct(int $value) { echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; echo "eval-dynamic-const-exists=" . $eval_dynamic_const_probe . "\n"; echo "native-defined-eval-const=" . $eval_dynamic_const_native_probe . "\n"; +echo "native-fetch-eval-const=" . $eval_dynamic_const_native_fetch . "\n"; echo "eval-dynamic-new=" . $eval_dynamic_new . "\n"; $counter = new EvalCounter(); $counter->bump(); diff --git a/src/codegen/context.rs b/src/codegen/context.rs index af511f592c..fc3e5a0822 100644 --- a/src/codegen/context.rs +++ b/src/codegen/context.rs @@ -551,6 +551,7 @@ impl<'a> FunctionContext<'a> { | Op::BuiltinCall | Op::EvalFunctionCall | Op::EvalFunctionCallArray + | Op::EvalConstantFetch | Op::RuntimeCall | Op::ExternCall | Op::MethodCall diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index b3ff51f402..b9e366440c 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -201,6 +201,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::EvalFunctionCallArray => builtins::lower_eval_function_call_array(ctx, &inst), Op::EvalFunctionExists => builtins::lower_eval_function_exists(ctx, &inst), Op::EvalConstantExists => builtins::lower_eval_constant_exists(ctx, &inst), + Op::EvalConstantFetch => builtins::lower_eval_constant_fetch(ctx, &inst), Op::ClosureCapture => lower_closure_capture(ctx, &inst), Op::ClosureNew => lower_closure_new(ctx, &inst), Op::FirstClassCallableNew => lower_first_class_callable_new(ctx, &inst), diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 69f39a3777..b20879f0b6 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -112,6 +112,14 @@ pub(super) fn lower_eval_constant_exists( eval::lower_eval_constant_exists(ctx, inst) } +/// Lowers post-eval dynamic constant fetches to the optional eval bridge. +pub(super) fn lower_eval_constant_fetch( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_constant_fetch(ctx, inst) +} + /// Lowers `define("NAME", value)` with the duplicate-name runtime guard. pub(crate) fn lower_define(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "define", 2)?; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 15646a34e5..01a7308cee 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -297,6 +297,37 @@ pub(super) fn lower_eval_constant_exists( store_if_result(ctx, inst) } +/// Lowers a post-eval dynamic constant fetch to the eval bridge ABI. +pub(super) fn lower_eval_constant_fetch( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let constant_name = ctx.global_name_data(expect_data(inst)?)?.to_string(); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (name_label, name_len) = ctx.data.add_string(constant_name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_constant_fetch"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Returns the aligned scratch size for an eval-declared function call. fn eval_function_call_stack_bytes(arg_count: usize) -> usize { let bytes = EVAL_STACK_BYTES + arg_count * 8; diff --git a/src/ir/instr.rs b/src/ir/instr.rs index c94490e741..c4ef76b607 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -359,6 +359,7 @@ pub enum Op { EvalFunctionCallArray, EvalFunctionExists, EvalConstantExists, + EvalConstantFetch, RuntimeCall, ExternCall, ClosureNew, @@ -496,6 +497,9 @@ impl Op { E::READS_HEAP | E::ALLOC_HEAP | E::MAY_THROW } EvalFunctionExists | EvalConstantExists => E::READS_GLOBAL, + EvalConstantFetch => { + E::READS_GLOBAL | E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL + } Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | EvalFunctionCallArray | RuntimeCall | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { E::all().difference(E::REFCOUNT_OP) @@ -700,6 +704,7 @@ impl Op { EvalFunctionCallArray => "eval_function_call_array", EvalFunctionExists => "eval_function_exists", EvalConstantExists => "eval_constant_exists", + EvalConstantFetch => "eval_constant_fetch", RuntimeCall => "runtime_call", ExternCall => "extern_call", ClosureNew => "closure_new", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index bfe48b4ccf..b8d0d9bfa7 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -274,7 +274,7 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstBool => require_immediate(inst_id, inst, "bool", |imm| matches!(imm, Imm::Bool(_))), ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionCallArray - | EvalFunctionExists | EvalConstantExists | EnumBackingStringToInt + | EvalFunctionExists | EvalConstantExists | EvalConstantFetch | EnumBackingStringToInt | EnumBackingMixedToInt => { require_immediate(inst_id, inst, "data id", |imm| matches!(imm, Imm::Data(_))) } @@ -355,7 +355,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio | ErrorSuppressBegin | ErrorSuppressEnd | TryPushHandler | TryPopHandler | CatchCurrent | CatchBind | FinallyEnter | FinallyExit | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | EvalFunctionExists - | EvalConstantExists | ConcatReset | GcCollect | Nop => { + | EvalConstantExists | EvalConstantFetch | ConcatReset | GcCollect | Nop => { check_count(inst_id, inst, 0, "0") } EvalFunctionCallArray => check_count(inst_id, inst, 1, "1"), diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index a68893d4d0..a4483efa52 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1177,6 +1177,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::FunctionVariantCall | Op::EvalFunctionCall | Op::EvalFunctionCallArray + | Op::EvalConstantFetch | Op::RuntimeCall | Op::ExternCall | Op::MethodCall diff --git a/src/ir_lower/expr/constants.rs b/src/ir_lower/expr/constants.rs index cbc4ad197f..96379df310 100644 --- a/src/ir_lower/expr/constants.rs +++ b/src/ir_lower/expr/constants.rs @@ -77,6 +77,17 @@ pub(super) fn lower_const_ref( if let Some((value, php_type)) = ctx.constant_value(name.as_str()) { return lower_constant_value(ctx, value, php_type, expr); } + if ctx.has_eval_barrier() { + let data = ctx.intern_global_name(name.as_str()); + return ctx.emit_value( + Op::EvalConstantFetch, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalConstantFetch.default_effects(), + Some(expr.span), + ); + } let data = ctx.intern_global_name(name.as_str()); ctx.emit_value( Op::LoadGlobal, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 725897af56..01731489fe 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -135,7 +135,8 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalFunctionExists - | Op::EvalConstantExists => { + | Op::EvalConstantExists + | Op::EvalConstantFetch => { features.eval = true; } Op::ExprCall | Op::CallableDescriptorInvoke => { diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index 442db30950..0663037265 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -463,9 +463,13 @@ impl Checker { ) } ExprKind::ConstRef(name) => { - self.constants.get(name.as_str()).cloned().ok_or_else(|| { - CompileError::new(expr.span, &format!("Undefined constant: {}", name)) - }) + self.constants + .get(name.as_str()) + .cloned() + .or_else(|| self.eval_barrier_active.then_some(PhpType::Mixed)) + .ok_or_else(|| { + CompileError::new(expr.span, &format!("Undefined constant: {}", name)) + }) } ExprKind::FirstClassCallable(target) => { self.infer_first_class_callable_target(target, expr.span, env)?; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2b5d40c1c2..413eb592ac 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1131,6 +1131,28 @@ echo defined("dynevalnativedefinedconst") ? "bad" : "N"; assert_eq!(out, "NYYN"); } +/// Verifies native constant fetch can read eval-defined constants after the barrier. +#[test] +fn test_eval_defined_constant_is_visible_to_native_constant_fetch_after_barrier() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 05:53:44 +0200 Subject: [PATCH 0123/1208] Probe eval classes from native class_exists --- crates/elephc-eval/src/lib.rs | 68 +++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/builtins.rs | 8 +++ src/codegen/lower_inst/builtins/eval.rs | 25 +++++++++ src/ir/instr.rs | 4 +- src/ir/validator.rs | 6 +-- src/ir_lower/expr/mod.rs | 50 ++++++++++++++++++ src/ir_lower/program.rs | 1 + tests/codegen/eval.rs | 29 +++++++++++ 12 files changed, 192 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index d1ddd6e0e6..be5d34d3f5 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -299,6 +299,23 @@ pub unsafe extern "C" fn __elephc_eval_constant_exists( .unwrap_or(0) } +/// Checks whether a class was previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_dynamic_class_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_dynamic_class_exists_inner(ctx, name_ptr, name_len) + }) + .unwrap_or(0) +} + /// Fetches a constant previously defined through `eval()`. /// /// # Safety @@ -472,6 +489,28 @@ unsafe fn eval_constant_exists_inner( i32::from(context.has_constant(&name)) } +/// Runs the eval dynamic-class-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_dynamic_class_exists`; invalid handles or unreadable +/// name storage fail closed as `false`. +unsafe fn eval_dynamic_class_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_class(&name)) +} + /// Runs the eval constant-fetch ABI body after installing a panic boundary. /// /// # Safety @@ -1033,6 +1072,35 @@ mod tests { assert_eq!(missing_result, 0); } + /// Verifies the dynamic-class-exists ABI probes eval-declared classes by folded PHP name. + #[test] + fn dynamic_class_exists_reports_declared_eval_class() { + let mut ctx = ElephcEvalContext::new(); + assert!(ctx.define_class("DynClassProbe")); + let existing = b"DynClassProbe"; + let qualified = b"\\DynClassProbe"; + let folded = b"dynclassprobe"; + let missing = b"missing"; + + let existing_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, existing.as_ptr(), existing.len() as u64) + }; + let qualified_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) + }; + let folded_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, folded.as_ptr(), folded.len() as u64) + }; + let missing_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, missing.as_ptr(), missing.len() as u64) + }; + + assert_eq!(existing_result, 1); + assert_eq!(qualified_result, 1); + assert_eq!(folded_result, 1); + assert_eq!(missing_result, 0); + } + /// Verifies native AOT registration records function and parameter metadata. #[test] fn register_native_function_reports_function_exists() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 4646146d71..3a90731a8d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6760e57933..e4d321bce5 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,7 +34,7 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. -Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")` and bare constant fetches after an eval barrier also probe those dynamic constants. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. +Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index 155844c89c..ba5795d53f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -133,6 +133,7 @@ public function __construct(int $value) { $eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); eval('class EvalDynamicEmptyClass {}'); $eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); +$eval_dynamic_class_native_probe = class_exists("EvalDynamicEmptyClass") ? "yes" : "no"; $eval_dynamic_const_probe = eval('define("EvalDynamicConst", "yes"); return EvalDynamicConst;'); $eval_dynamic_const_native_probe = defined("EvalDynamicConst") ? "yes" : "no"; $eval_dynamic_const_native_fetch = EvalDynamicConst; @@ -203,6 +204,7 @@ public function __construct(int $value) { echo "string-compare=" . $string_compare . "\n"; echo "eval-class-exists=" . $eval_class_probe . "\n"; echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; +echo "native-class-exists-eval-class=" . $eval_dynamic_class_native_probe . "\n"; echo "eval-dynamic-const-exists=" . $eval_dynamic_const_probe . "\n"; echo "native-defined-eval-const=" . $eval_dynamic_const_native_probe . "\n"; echo "native-fetch-eval-const=" . $eval_dynamic_const_native_fetch . "\n"; diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index b9e366440c..78ad5c42e9 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -200,6 +200,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), Op::EvalFunctionCallArray => builtins::lower_eval_function_call_array(ctx, &inst), Op::EvalFunctionExists => builtins::lower_eval_function_exists(ctx, &inst), + Op::EvalClassExists => builtins::lower_eval_class_exists(ctx, &inst), Op::EvalConstantExists => builtins::lower_eval_constant_exists(ctx, &inst), Op::EvalConstantFetch => builtins::lower_eval_constant_fetch(ctx, &inst), Op::ClosureCapture => lower_closure_capture(ctx, &inst), diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index b20879f0b6..6bfa0b325d 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -104,6 +104,14 @@ pub(super) fn lower_eval_function_exists( eval::lower_eval_function_exists(ctx, inst) } +/// Lowers post-eval dynamic class existence probes to the optional eval bridge. +pub(super) fn lower_eval_class_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_class_exists(ctx, inst) +} + /// Lowers post-eval dynamic constant existence probes to the optional eval bridge. pub(super) fn lower_eval_constant_exists( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 01a7308cee..c47aca3bb7 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -271,6 +271,31 @@ pub(super) fn lower_eval_function_exists( store_if_result(ctx, inst) } +/// Lowers a post-eval dynamic class existence probe to the eval bridge ABI. +pub(super) fn lower_eval_class_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let (name_label, name_len) = ctx.intern_class_name_data(expect_data(inst)?)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_dynamic_class_exists"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Lowers a post-eval dynamic constant existence probe to the eval bridge ABI. pub(super) fn lower_eval_constant_exists( ctx: &mut FunctionContext<'_>, diff --git a/src/ir/instr.rs b/src/ir/instr.rs index c4ef76b607..dba6e78eb6 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -358,6 +358,7 @@ pub enum Op { EvalFunctionCall, EvalFunctionCallArray, EvalFunctionExists, + EvalClassExists, EvalConstantExists, EvalConstantFetch, RuntimeCall, @@ -496,7 +497,7 @@ impl Op { EnumBackingStringToInt | EnumBackingMixedToInt => { E::READS_HEAP | E::ALLOC_HEAP | E::MAY_THROW } - EvalFunctionExists | EvalConstantExists => E::READS_GLOBAL, + EvalFunctionExists | EvalClassExists | EvalConstantExists => E::READS_GLOBAL, EvalConstantFetch => { E::READS_GLOBAL | E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL } @@ -703,6 +704,7 @@ impl Op { EvalFunctionCall => "eval_function_call", EvalFunctionCallArray => "eval_function_call_array", EvalFunctionExists => "eval_function_exists", + EvalClassExists => "eval_class_exists", EvalConstantExists => "eval_constant_exists", EvalConstantFetch => "eval_constant_fetch", RuntimeCall => "runtime_call", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index b8d0d9bfa7..8c64cb8c51 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -274,8 +274,8 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstBool => require_immediate(inst_id, inst, "bool", |imm| matches!(imm, Imm::Bool(_))), ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionCallArray - | EvalFunctionExists | EvalConstantExists | EvalConstantFetch | EnumBackingStringToInt - | EnumBackingMixedToInt => { + | EvalFunctionExists | EvalClassExists | EvalConstantExists | EvalConstantFetch + | EnumBackingStringToInt | EnumBackingMixedToInt => { require_immediate(inst_id, inst, "data id", |imm| matches!(imm, Imm::Data(_))) } LoadLocal | StoreLocal | UnsetLocal | LoadRefCell | StoreRefCell | ReleaseLocalRefCell @@ -355,7 +355,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio | ErrorSuppressBegin | ErrorSuppressEnd | TryPushHandler | TryPopHandler | CatchCurrent | CatchBind | FinallyEnter | FinallyExit | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | EvalFunctionExists - | EvalConstantExists | EvalConstantFetch | ConcatReset | GcCollect | Nop => { + | EvalClassExists | EvalConstantExists | EvalConstantFetch | ConcatReset | GcCollect | Nop => { check_count(inst_id, inst, 0, "0") } EvalFunctionCallArray => check_count(inst_id, inst, 1, "1"), diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 036e0be263..70af1c22fd 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1788,6 +1788,9 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E if let Some(value) = lower_eval_function_probe(ctx, canonical, args, expr) { return value; } + if let Some(value) = lower_eval_class_probe(ctx, canonical, args, expr) { + return value; + } let sig = call_signature(ctx, canonical, args); let is_extern = ctx.extern_functions.contains_key(canonical); let is_user_function = ctx.functions.contains_key(canonical); @@ -1920,6 +1923,53 @@ fn lower_eval_function_probe( )) } +/// Lowers post-eval class-name probes through the eval context's dynamic class table. +fn lower_eval_class_probe( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let probe_name = php_symbol_key(name.trim_start_matches('\\')); + if probe_name != "class_exists" { + return None; + } + if !ctx.has_eval_barrier() + || args.is_empty() + || args.len() > 2 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { + return None; + } + let ExprKind::StringLiteral(class_name) = &args[0].kind else { + return None; + }; + if aot_class_exists_for_eval_probe(ctx, class_name) { + return None; + } + if let Some(autoload) = args.get(1) { + lower_expr(ctx, autoload); + } + let data = ctx.intern_class_name(class_name); + Some(ctx.emit_value( + Op::EvalClassExists, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Bool, + Op::EvalClassExists.default_effects(), + Some(expr.span), + )) +} + +/// Returns true when an AOT class already satisfies a native class_exists probe. +fn aot_class_exists_for_eval_probe(ctx: &LoweringContext<'_, '_>, class_name: &str) -> bool { + let key = php_symbol_key(class_name.trim_start_matches('\\')); + ctx.classes + .keys() + .any(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == key) +} + /// Lowers `isset()` as a lazy language construct instead of an eager builtin call. fn lower_lazy_isset( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 01731489fe..6dde52f03c 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -135,6 +135,7 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalFunctionExists + | Op::EvalClassExists | Op::EvalConstantExists | Op::EvalConstantFetch => { features.eval = true; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 413eb592ac..7b0ef59556 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2014,6 +2014,35 @@ echo eval('return class_exists("dynevalclassexists") ? "Y" : "N";'); assert_eq!(out, "YY"); } +/// Verifies native `class_exists()` probes can see eval-declared classes after the barrier. +#[test] +fn test_eval_declared_empty_class_is_visible_to_native_class_exists_after_barrier() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 06:00:10 +0200 Subject: [PATCH 0124/1208] Support eval bin2hex builtin --- crates/elephc-eval/src/interpreter.rs | 58 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++++ 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b6c311fd23..74aeaec1d1 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1144,6 +1144,7 @@ fn eval_positional_expr_call( eval_builtin_array_search(name, args, context, scope, values) } "array_unique" => eval_builtin_array_unique(args, context, scope, values), + "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), @@ -1443,6 +1444,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_sum" | "array_unique" | "array_values" + | "bin2hex" | "ceil" | "call_user_func" | "call_user_func_array" @@ -1582,6 +1584,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), + "bin2hex" => Some(&["string"]), "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), @@ -1795,6 +1798,12 @@ fn eval_builtin_with_values( }; eval_array_unique_result(*array, values)? } + "bin2hex" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_bin2hex_result(*value, values)? + } "ceil" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2360,6 +2369,35 @@ fn eval_builtin_strrev( values.strrev(value) } +/// Evaluates PHP's `bin2hex(...)` over one eval expression. +fn eval_builtin_bin2hex( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_bin2hex_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. +fn eval_bin2hex_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = String::with_capacity(bytes.len() * 2); + const HEX: &[u8; 16] = b"0123456789abcdef"; + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + values.string(&output) +} + /// Evaluates PHP floating-point binary math builtins over two eval expressions. fn eval_builtin_float_binary( name: &str, @@ -6182,6 +6220,26 @@ return function_exists("strrev");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_bin2hex_builtin() { + let program = parse_fragment( + br#"echo bin2hex("Az"); echo ":"; +echo bin2hex(string: "A\n"); echo ":"; +echo call_user_func("bin2hex", "!?"); echo ":"; +echo call_user_func_array("bin2hex", ["string" => "ok"]); echo ":"; +return function_exists("bin2hex");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "417a:410a:213f:6f6b:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3a90731a8d..6e76b8a088 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index e4d321bce5..2609ede521 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports `strrev()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `strrev()` and `bin2hex()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index ba5795d53f..e8c1315134 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -130,6 +130,7 @@ public function __construct(int $value) { $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); +$hexed = eval('return bin2hex("Az");'); $eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); eval('class EvalDynamicEmptyClass {}'); $eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); @@ -202,6 +203,7 @@ public function __construct(int $value) { echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; +echo "bin2hex=" . $hexed . "\n"; echo "eval-class-exists=" . $eval_class_probe . "\n"; echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; echo "native-class-exists-eval-class=" . $eval_dynamic_class_native_probe . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7b0ef59556..806bc4e4b0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -914,6 +914,21 @@ echo ":"; echo function_exists("strrev");'); assert_eq!(out, "olleH:321:CBA:fed:1"); } +/// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_bin2hex_builtin_call() { + let out = compile_and_run( + r#" "ok"]); +echo ":"; echo function_exists("bin2hex");'); +"#, + ); + assert_eq!(out, "417a:410a:213f:6f6b:1"); +} + /// Verifies eval `str_contains()` supports direct and callable byte-string search. #[test] fn test_eval_dispatches_str_contains_builtin_call() { From 3d9094b032e04828ae1f4a7d5df675e44431583e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:03:50 +0200 Subject: [PATCH 0125/1208] Support eval base64 encode builtin --- crates/elephc-eval/src/interpreter.rs | 73 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 ++++++ 5 files changed, 92 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 74aeaec1d1..42835a0dc2 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1144,6 +1144,7 @@ fn eval_positional_expr_call( eval_builtin_array_search(name, args, context, scope, values) } "array_unique" => eval_builtin_array_unique(args, context, scope, values), + "base64_encode" => eval_builtin_base64_encode(args, context, scope, values), "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), @@ -1444,6 +1445,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_sum" | "array_unique" | "array_values" + | "base64_encode" | "bin2hex" | "ceil" | "call_user_func" @@ -1584,7 +1586,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), - "bin2hex" => Some(&["string"]), + "base64_encode" | "bin2hex" => Some(&["string"]), "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), @@ -1798,6 +1800,12 @@ fn eval_builtin_with_values( }; eval_array_unique_result(*array, values)? } + "base64_encode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_encode_result(*value, values)? + } "bin2hex" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2398,6 +2406,49 @@ fn eval_bin2hex_result( values.string(&output) } +/// Evaluates PHP's `base64_encode(...)` over one eval expression. +fn eval_builtin_base64_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_encode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns Base64 text. +fn eval_base64_encode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); + const ALPHABET: &[u8; 64] = + b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for chunk in bytes.chunks(3) { + let first = chunk[0]; + let second = chunk.get(1).copied().unwrap_or(0); + let third = chunk.get(2).copied().unwrap_or(0); + output.push(ALPHABET[(first >> 2) as usize] as char); + output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); + if chunk.len() > 1 { + output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); + } else { + output.push('='); + } + if chunk.len() > 2 { + output.push(ALPHABET[(third & 0x3f) as usize] as char); + } else { + output.push('='); + } + } + values.string(&output) +} + /// Evaluates PHP floating-point binary math builtins over two eval expressions. fn eval_builtin_float_binary( name: &str, @@ -6240,6 +6291,26 @@ return function_exists("bin2hex");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_base64_encode_builtin() { + let program = parse_fragment( + br#"echo base64_encode("Hello"); echo ":"; +echo base64_encode(string: "Hi"); echo ":"; +echo call_user_func("base64_encode", "Test 123!"); echo ":"; +echo call_user_func_array("base64_encode", ["string" => ""]); echo ":"; +return function_exists("base64_encode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "SGVsbG8=:SGk=:VGVzdCAxMjMh::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 6e76b8a088..7e6bbe1b56 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `base64_encode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 2609ede521..5a1339fc43 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `base64_encode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `strrev()` and `bin2hex()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `strrev()`, `bin2hex()`, and `base64_encode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index e8c1315134..602b80f363 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -131,6 +131,7 @@ public function __construct(int $value) { $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $hexed = eval('return bin2hex("Az");'); +$base64 = eval('return base64_encode("Hello");'); $eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); eval('class EvalDynamicEmptyClass {}'); $eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); @@ -204,6 +205,7 @@ public function __construct(int $value) { echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "bin2hex=" . $hexed . "\n"; +echo "base64=" . $base64 . "\n"; echo "eval-class-exists=" . $eval_class_probe . "\n"; echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; echo "native-class-exists-eval-class=" . $eval_dynamic_class_native_probe . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 806bc4e4b0..69a278182c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -929,6 +929,21 @@ echo ":"; echo function_exists("bin2hex");'); assert_eq!(out, "417a:410a:213f:6f6b:1"); } +/// Verifies eval `base64_encode()` encodes byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_base64_encode_builtin_call() { + let out = compile_and_run( + r#" ""]); +echo ":"; echo function_exists("base64_encode");'); +"#, + ); + assert_eq!(out, "SGVsbG8=:SGk=:VGVzdCAxMjMh::1"); +} + /// Verifies eval `str_contains()` supports direct and callable byte-string search. #[test] fn test_eval_dispatches_str_contains_builtin_call() { From 44d57f0d5ccc29b280bc68c059fccb9ca1d17bd1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:10:04 +0200 Subject: [PATCH 0126/1208] Support eval base64 decode builtin --- crates/elephc-eval/src/interpreter.rs | 114 +++++++++++++++++++++++- crates/elephc-eval/src/runtime_hooks.rs | 5 ++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 ++++ 6 files changed, 138 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 42835a0dc2..5ea48c63b8 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -147,6 +147,9 @@ pub trait RuntimeValueOps { /// Creates a runtime string cell. fn string(&mut self, value: &str) -> Result; + /// Creates a runtime byte-string cell from raw PHP string bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result; + /// Casts one runtime cell to a boxed PHP integer cell. fn cast_int(&mut self, value: RuntimeCellHandle) -> Result; @@ -1145,6 +1148,7 @@ fn eval_positional_expr_call( } "array_unique" => eval_builtin_array_unique(args, context, scope, values), "base64_encode" => eval_builtin_base64_encode(args, context, scope, values), + "base64_decode" => eval_builtin_base64_decode(args, context, scope, values), "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), "ceil" => eval_builtin_ceil(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), @@ -1445,6 +1449,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_sum" | "array_unique" | "array_values" + | "base64_decode" | "base64_encode" | "bin2hex" | "ceil" @@ -1586,7 +1591,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), - "base64_encode" | "bin2hex" => Some(&["string"]), + "base64_decode" | "base64_encode" | "bin2hex" => Some(&["string"]), "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), @@ -1806,6 +1811,12 @@ fn eval_builtin_with_values( }; eval_base64_encode_result(*value, values)? } + "base64_decode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_decode_result(*value, values)? + } "bin2hex" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2449,6 +2460,81 @@ fn eval_base64_encode_result( values.string(&output) } +/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. +fn eval_builtin_base64_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_decode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes Base64 bytes. +fn eval_base64_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(value)?; + let mut output = Vec::with_capacity((input.len() / 4) * 3); + let mut quartet = Vec::with_capacity(4); + for byte in input { + if byte.is_ascii_whitespace() { + continue; + } + if byte == b'=' { + quartet.push(None); + } else if let Some(value) = eval_base64_decode_sextet(byte) { + quartet.push(Some(value)); + } else { + continue; + } + if quartet.len() == 4 { + eval_push_base64_decoded_quartet(&quartet, &mut output); + quartet.clear(); + } + } + if !quartet.is_empty() { + while quartet.len() < 4 { + quartet.push(None); + } + eval_push_base64_decoded_quartet(&quartet, &mut output); + } + values.string_bytes_value(&output) +} + +/// Returns the six-bit Base64 value for one encoded byte. +fn eval_base64_decode_sextet(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(byte - b'A'), + b'a'..=b'z' => Some(byte - b'a' + 26), + b'0'..=b'9' => Some(byte - b'0' + 52), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } +} + +/// Appends decoded bytes for one padded or unpadded Base64 quartet. +fn eval_push_base64_decoded_quartet(quartet: &[Option], output: &mut Vec) { + let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { + return; + }; + output.push((first << 2) | (second >> 4)); + let Some(third) = quartet[2] else { + return; + }; + output.push(((second & 0x0f) << 4) | (third >> 2)); + let Some(fourth) = quartet[3] else { + return; + }; + output.push(((third & 0x03) << 6) | fourth); +} + /// Evaluates PHP floating-point binary math builtins over two eval expressions. fn eval_builtin_float_binary( name: &str, @@ -3877,6 +3963,12 @@ mod tests { Ok(self.alloc(FakeValue::String(value.to_string()))) } + /// Creates a fake string cell from UTF-8 bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; + self.string(value) + } + /// Casts a fake runtime cell to a fake integer cell. fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { let value = self.get(value); @@ -6311,6 +6403,26 @@ return function_exists("base64_encode");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `base64_decode()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_base64_decode_builtin() { + let program = parse_fragment( + br#"echo base64_decode("SGVsbG8="); echo ":"; +echo base64_decode(string: "SGk="); echo ":"; +echo call_user_func("base64_decode", "VGVzdCAxMjMh"); echo ":"; +echo call_user_func_array("base64_decode", ["string" => ""]); echo ":"; +return function_exists("base64_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hello:Hi:Test 123!::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies `isset` distinguishes missing, null, and other falsey values. #[test] fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index d9e1f2400b..78d9b9141e 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -393,6 +393,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) } + /// Creates a boxed string Mixed cell from raw PHP bytes through the generated runtime wrapper. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) + } + /// Casts a boxed Mixed cell to a boxed integer Mixed cell through the generated runtime wrapper. fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { Self::handle(unsafe { __elephc_eval_value_cast_int(value.as_ptr()) }) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 7e6bbe1b56..9a6c7bc823 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `base64_encode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 5a1339fc43..94965746a6 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `base64_encode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `strrev()`, `bin2hex()`, and `base64_encode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `strrev()`, `bin2hex()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 602b80f363..cc63e5f469 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -132,6 +132,7 @@ public function __construct(int $value) { $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $hexed = eval('return bin2hex("Az");'); $base64 = eval('return base64_encode("Hello");'); +$base64_decoded = eval('return base64_decode("SGVsbG8=");'); $eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); eval('class EvalDynamicEmptyClass {}'); $eval_dynamic_class_probe = eval('return class_exists("evaldynamicemptyclass") ? "yes" : "no";'); @@ -206,6 +207,7 @@ public function __construct(int $value) { echo "string-compare=" . $string_compare . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "base64=" . $base64 . "\n"; +echo "base64-decode=" . $base64_decoded . "\n"; echo "eval-class-exists=" . $eval_class_probe . "\n"; echo "eval-dynamic-class-exists=" . $eval_dynamic_class_probe . "\n"; echo "native-class-exists-eval-class=" . $eval_dynamic_class_native_probe . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 69a278182c..bde76c099b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -944,6 +944,21 @@ echo ":"; echo function_exists("base64_encode");'); assert_eq!(out, "SGVsbG8=:SGk=:VGVzdCAxMjMh::1"); } +/// Verifies eval `base64_decode()` decodes byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_base64_decode_builtin_call() { + let out = compile_and_run( + r#" ""]); +echo ":"; echo function_exists("base64_decode");'); +"#, + ); + assert_eq!(out, "Hello:Hi:Test 123!::1"); +} + /// Verifies eval `str_contains()` supports direct and callable byte-string search. #[test] fn test_eval_dispatches_str_contains_builtin_call() { From 4ccb4b896d0ac8e2602ecb86bcb821511a645533 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:16:06 +0200 Subject: [PATCH 0127/1208] Support eval slash escaping builtins --- crates/elephc-eval/src/interpreter.rs | 111 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 16 ++++ 5 files changed, 130 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5ea48c63b8..69cad8c6e1 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1133,6 +1133,9 @@ fn eval_positional_expr_call( ) -> Result { match name { "abs" => eval_builtin_abs(args, context, scope, values), + "addslashes" | "stripslashes" => { + eval_builtin_slashes(name, args, context, scope, values) + } "array_combine" => eval_builtin_array_combine(args, context, scope, values), "array_flip" => eval_builtin_array_flip(args, context, scope, values), "array_keys" | "array_values" => { @@ -1438,7 +1441,8 @@ fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, - "abs" + "abs" + | "addslashes" | "array_combine" | "array_flip" | "array_key_exists" @@ -1502,6 +1506,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strpos" | "strrpos" | "strrev" + | "stripslashes" | "strtolower" | "strtoupper" | "strval" @@ -1591,7 +1596,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), - "base64_decode" | "base64_encode" | "bin2hex" => Some(&["string"]), + "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "stripslashes" => { + Some(&["string"]) + } "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), @@ -1755,6 +1762,12 @@ fn eval_builtin_with_values( }; values.abs(*value)? } + "addslashes" | "stripslashes" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_slashes_result(name, *value, values)? + } "array_combine" => { let [keys, values_array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2417,6 +2430,77 @@ fn eval_bin2hex_result( values.string(&output) } +/// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. +fn eval_builtin_slashes( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_slashes_result(name, value, values) +} + +/// Applies PHP byte-string escaping or unescaping for addslashes/stripslashes. +fn eval_slashes_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "addslashes" => eval_addslashes_result(value, values), + "stripslashes" => eval_stripslashes_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. +fn eval_addslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + 0 => output.extend_from_slice(b"\\0"), + b'\'' | b'"' | b'\\' => { + output.push(b'\\'); + output.push(byte); + } + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Removes backslash quoting using PHP `stripslashes()` byte semantics. +fn eval_stripslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'\\' { + index += 1; + if let Some(byte) = bytes.get(index).copied() { + output.push(if byte == b'0' { 0 } else { byte }); + index += 1; + } + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} + /// Evaluates PHP's `base64_encode(...)` over one eval expression. fn eval_builtin_base64_encode( args: &[EvalExpr], @@ -6383,6 +6467,29 @@ return function_exists("bin2hex");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval slash escaping builtins use PHP byte-string semantics. + #[test] + fn execute_program_dispatches_slash_escape_builtins() { + let program = parse_fragment( + br#"$escaped = addslashes($source); +echo bin2hex($escaped); echo ":"; +echo bin2hex(stripslashes($escaped)); echo ":"; +echo call_user_func("addslashes", "x\"y"); echo ":"; +echo call_user_func_array("stripslashes", [addslashes("o\"k")]); echo ":"; +return function_exists("addslashes") && function_exists("stripslashes");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let source = values.string("a\0b\\c\"d'").expect("create source"); + scope.set("source", source, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_base64_encode_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9a6c7bc823..29fd2794d5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 94965746a6..7dcc23d85e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `bin2hex()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `strrev()`, `bin2hex()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `strrev()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index cc63e5f469..42d50817b6 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -130,6 +130,7 @@ public function __construct(int $value) { $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); +$slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); $hexed = eval('return bin2hex("Az");'); $base64 = eval('return base64_encode("Hello");'); $base64_decoded = eval('return base64_decode("SGVsbG8=");'); @@ -205,6 +206,7 @@ public function __construct(int $value) { echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; +echo "slashes=" . $slashes . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "base64=" . $base64 . "\n"; echo "base64-decode=" . $base64_decoded . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bde76c099b..aaf720b4bf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -929,6 +929,22 @@ echo ":"; echo function_exists("bin2hex");'); assert_eq!(out, "417a:410a:213f:6f6b:1"); } +/// Verifies eval `addslashes()` and `stripslashes()` use PHP byte escaping semantics. +#[test] +fn test_eval_dispatches_slash_escape_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 06:22:32 +0200 Subject: [PATCH 0128/1208] Support eval hex2bin builtin --- crates/elephc-eval/src/interpreter.rs | 91 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++ 5 files changed, 108 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 69cad8c6e1..0f97121ef8 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -293,6 +293,10 @@ const EVAL_TAG_OBJECT: u64 = 6; const EVAL_TAG_NULL: u64 = 8; const EVAL_TAG_RESOURCE: u64 = 9; const DEFINE_ALREADY_DEFINED_WARNING: &str = "Warning: define(): Constant already defined\n"; +const HEX2BIN_ODD_LENGTH_WARNING: &str = + "Warning: hex2bin(): Hexadecimal input string must have an even length\n"; +const HEX2BIN_INVALID_WARNING: &str = + "Warning: hex2bin(): Input string must be hexadecimal string\n"; /// Executes an EvalIR program and returns the eval result cell. pub fn execute_program( @@ -1173,6 +1177,7 @@ fn eval_positional_expr_call( } "gettype" => eval_builtin_gettype(args, context, scope, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), + "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) @@ -1472,6 +1477,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "function_exists" | "gettype" | "hash_equals" + | "hex2bin" | "in_array" | "intval" | "ltrim" @@ -1596,9 +1602,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), - "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "stripslashes" => { - Some(&["string"]) - } + "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" + | "stripslashes" => Some(&["string"]), "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), @@ -1944,6 +1949,12 @@ fn eval_builtin_with_values( }; eval_hash_equals_result(*known, *user, values)? } + "hex2bin" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hex2bin_result(*value, values)? + } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { let [value] = evaluated_args else { @@ -2430,6 +2441,55 @@ fn eval_bin2hex_result( values.string(&output) } +/// Evaluates PHP's `hex2bin(...)` over one eval expression. +fn eval_builtin_hex2bin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_hex2bin_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. +fn eval_hex2bin_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + if bytes.len() % 2 != 0 { + values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; + return values.bool_value(false); + } + let mut output = Vec::with_capacity(bytes.len() / 2); + for pair in bytes.chunks_exact(2) { + let Some(high) = eval_hex_nibble(pair[0]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + let Some(low) = eval_hex_nibble(pair[1]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + output.push((high << 4) | low); + } + values.string_bytes_value(&output) +} + +/// Returns the four-bit value for one hexadecimal byte. +fn eval_hex_nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} + /// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. fn eval_builtin_slashes( name: &str, @@ -6467,6 +6527,31 @@ return function_exists("bin2hex");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `hex2bin()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_hex2bin_builtin() { + let program = parse_fragment( + br#"echo hex2bin("417a"); echo ":"; +echo bin2hex(hex2bin(string: "410a")); echo ":"; +echo call_user_func("hex2bin", "213f"); echo ":"; +echo call_user_func_array("hex2bin", ["string" => "6f6b"]); echo ":"; +echo hex2bin("4") ? "bad" : "false"; echo ":"; +return function_exists("hex2bin");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Az:410a:!?:ok:false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + vec![HEX2BIN_ODD_LENGTH_WARNING.to_string()] + ); + } + /// Verifies eval slash escaping builtins use PHP byte-string semantics. #[test] fn execute_program_dispatches_slash_escape_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 29fd2794d5..5a1ee7f764 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7dcc23d85e..9fab337002 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `strrev()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `strrev()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 42d50817b6..469856cb6a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -132,6 +132,7 @@ public function __construct(int $value) { $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); $hexed = eval('return bin2hex("Az");'); +$unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); $base64_decoded = eval('return base64_decode("SGVsbG8=");'); $eval_class_probe = eval('return class_exists("EvalAotBox") ? "yes" : "no";'); @@ -208,6 +209,7 @@ public function __construct(int $value) { echo "string-compare=" . $string_compare . "\n"; echo "slashes=" . $slashes . "\n"; echo "bin2hex=" . $hexed . "\n"; +echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; echo "base64-decode=" . $base64_decoded . "\n"; echo "eval-class-exists=" . $eval_class_probe . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index aaf720b4bf..e5eec256c4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -929,6 +929,21 @@ echo ":"; echo function_exists("bin2hex");'); assert_eq!(out, "417a:410a:213f:6f6b:1"); } +/// Verifies eval `hex2bin()` decodes hex strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_hex2bin_builtin_call() { + let out = compile_and_run( + r#" "6f6b"]); +echo ":"; echo function_exists("hex2bin");'); +"#, + ); + assert_eq!(out, "Az:410a:!?:ok:1"); +} + /// Verifies eval `addslashes()` and `stripslashes()` use PHP byte escaping semantics. #[test] fn test_eval_dispatches_slash_escape_builtin_calls() { From 2387f3fc02c69875aee3d2088fddee22b4bc434f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:29:01 +0200 Subject: [PATCH 0129/1208] Support eval chr builtin --- crates/elephc-eval/src/interpreter.rs | 57 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0f97121ef8..ece03c36c3 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1158,6 +1158,7 @@ fn eval_positional_expr_call( "base64_decode" => eval_builtin_base64_decode(args, context, scope, values), "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), "ceil" => eval_builtin_ceil(args, context, scope, values), + "chr" => eval_builtin_chr(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "class_exists" => eval_builtin_class_exists(args, context, scope, values), @@ -1467,6 +1468,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "class_exists" | "boolval" | "chop" + | "chr" | "count" | "define" | "defined" @@ -1610,6 +1612,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), + "chr" => Some(&["codepoint"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), "define" => Some(&["constant_name", "value"]), @@ -1847,6 +1850,12 @@ fn eval_builtin_with_values( }; values.ceil(*value)? } + "chr" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chr_result(*value, values)? + } "floor" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2412,6 +2421,34 @@ fn eval_builtin_strrev( values.strrev(value) } +/// Evaluates PHP's `chr(...)` over one eval expression. +fn eval_builtin_chr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_chr_result(value, values) +} + +/// Converts one eval value to a PHP integer and returns the low byte as a string. +fn eval_chr_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_int(value)?; + let bytes = values.string_bytes(value)?; + let value = std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal)?; + values.string_bytes_value(&[value as u8]) +} + /// Evaluates PHP's `bin2hex(...)` over one eval expression. fn eval_builtin_bin2hex( args: &[EvalExpr], @@ -6507,6 +6544,26 @@ return function_exists("strrev");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `chr()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_chr_builtin() { + let program = parse_fragment( + br#"echo chr(65); echo ":"; +echo bin2hex(chr(codepoint: 256)); echo ":"; +echo bin2hex(call_user_func("chr", 257)); echo ":"; +echo call_user_func_array("chr", ["codepoint" => 321]); echo ":"; +return function_exists("chr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:00:01:A:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_bin2hex_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 5a1ee7f764..02df7067b1 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,7 +25,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers `strrev()` through the shared byte-string reversal helper and `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers byte-string helpers including `chr()`, `strrev()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 9fab337002..87131118e5 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `strrev()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `chr()`, `strrev()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 469856cb6a..a142aa3577 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -131,6 +131,7 @@ public function __construct(int $value) { $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); +$chr = eval('return chr(65) . ":" . bin2hex(chr(256));'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -208,6 +209,7 @@ public function __construct(int $value) { echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "slashes=" . $slashes . "\n"; +echo "chr=" . $chr . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e5eec256c4..6818133fd2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -914,6 +914,21 @@ echo ":"; echo function_exists("strrev");'); assert_eq!(out, "olleH:321:CBA:fed:1"); } +/// Verifies eval `chr()` returns single-byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_chr_builtin_call() { + let out = compile_and_run( + r#" 321]); +echo ":"; echo function_exists("chr");'); +"#, + ); + assert_eq!(out, "A:00:ff:A:1"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From bf80518371d8c7e0e32ccb38b7e564842c894b36 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:32:18 +0200 Subject: [PATCH 0130/1208] Support eval str_repeat builtin --- crates/elephc-eval/src/interpreter.rs | 81 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++ 5 files changed, 98 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ece03c36c3..2f571a1d90 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1192,6 +1192,7 @@ fn eval_positional_expr_call( "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), + "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), "str_contains" | "str_starts_with" | "str_ends_with" => { eval_builtin_string_search(name, args, context, scope, values) } @@ -1508,6 +1509,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strcasecmp" | "str_contains" | "str_ends_with" + | "str_repeat" | "str_starts_with" | "strcmp" | "strlen" @@ -1628,6 +1630,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), + "str_repeat" => Some(&["string", "times"]), "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } @@ -1897,6 +1900,12 @@ fn eval_builtin_with_values( }; values.strrev(*value)? } + "str_repeat" => { + let [value, times] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_repeat_result(*value, *times, values)? + } "call_user_func" => { return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) .map(Some); @@ -2440,13 +2449,59 @@ fn eval_chr_result( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { + let value = eval_int_value(value, values)?; + values.string_bytes_value(&[value as u8]) +} + +/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. +fn eval_builtin_str_repeat( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, times] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let times = eval_expr(times, context, scope, values)?; + eval_str_repeat_result(value, times, values) +} + +/// Repeats one PHP string byte sequence according to a PHP-cast integer count. +fn eval_str_repeat_result( + value: RuntimeCellHandle, + times: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let times = eval_int_value(times, values)?; + if times < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; + let capacity = bytes + .len() + .checked_mul(times) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + for _ in 0..times { + output.extend_from_slice(&bytes); + } + values.string_bytes_value(&output) +} + +/// Casts one eval value to PHP int and returns the scalar payload. +fn eval_int_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { let value = values.cast_int(value)?; let bytes = values.string_bytes(value)?; - let value = std::str::from_utf8(&bytes) + std::str::from_utf8(&bytes) .map_err(|_| EvalStatus::RuntimeFatal)? .parse::() - .map_err(|_| EvalStatus::RuntimeFatal)?; - values.string_bytes_value(&[value as u8]) + .map_err(|_| EvalStatus::RuntimeFatal) } /// Evaluates PHP's `bin2hex(...)` over one eval expression. @@ -6564,6 +6619,26 @@ return function_exists("chr");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_str_repeat_builtin() { + let program = parse_fragment( + br#"echo str_repeat("ha", 3); echo ":"; +echo strlen(str_repeat(string: "x", times: 0)); echo ":"; +echo call_user_func("str_repeat", "ab", 2); echo ":"; +echo call_user_func_array("str_repeat", ["string" => "z", "times" => 3]); echo ":"; +return function_exists("str_repeat");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hahaha:0:abab:zzz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_bin2hex_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 02df7067b1..1de9e00642 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,7 +25,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers byte-string helpers including `chr()`, `strrev()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers byte-string helpers including `chr()`, `strrev()`, `str_repeat()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 87131118e5..62e1260f66 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `chr()`, `strrev()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `chr()`, `strrev()`, `str_repeat()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index a142aa3577..9a0c4636d4 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -132,6 +132,7 @@ public function __construct(int $value) { $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); $chr = eval('return chr(65) . ":" . bin2hex(chr(256));'); +$repeated = eval('return str_repeat("ha", 3);'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -210,6 +211,7 @@ public function __construct(int $value) { echo "string-compare=" . $string_compare . "\n"; echo "slashes=" . $slashes . "\n"; echo "chr=" . $chr . "\n"; +echo "str-repeat=" . $repeated . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6818133fd2..72fddfb2b0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -929,6 +929,21 @@ echo ":"; echo function_exists("chr");'); assert_eq!(out, "A:00:ff:A:1"); } +/// Verifies eval `str_repeat()` repeats byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_str_repeat_builtin_call() { + let out = compile_and_run( + r#" "z", "times" => 3]); +echo ":"; echo function_exists("str_repeat");'); +"#, + ); + assert_eq!(out, "hahaha:0:abab:zzz:1"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 95257187d711c55e05c45507e103e75f359c8cef Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:36:41 +0200 Subject: [PATCH 0131/1208] Support eval substr builtin --- crates/elephc-eval/src/interpreter.rs | 85 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 16 +++++ 5 files changed, 106 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2f571a1d90..7268c0ec2d 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1193,6 +1193,7 @@ fn eval_positional_expr_call( "sqrt" => eval_builtin_sqrt(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), + "substr" => eval_builtin_substr(args, context, scope, values), "str_contains" | "str_starts_with" | "str_ends_with" => { eval_builtin_string_search(name, args, context, scope, values) } @@ -1516,6 +1517,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strpos" | "strrpos" | "strrev" + | "substr" | "stripslashes" | "strtolower" | "strtoupper" @@ -1631,6 +1633,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), "str_repeat" => Some(&["string", "times"]), + "substr" => Some(&["string", "offset", "length"]), "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } @@ -1906,6 +1909,11 @@ fn eval_builtin_with_values( }; eval_str_repeat_result(*value, *times, values)? } + "substr" => match evaluated_args { + [value, offset] => eval_substr_result(*value, *offset, None, values)?, + [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "call_user_func" => { return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) .map(Some); @@ -2491,6 +2499,62 @@ fn eval_str_repeat_result( values.string_bytes_value(&output) } +/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. +fn eval_builtin_substr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, offset] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_result(value, offset, None, values) + } + [value, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_result(value, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Slices a PHP byte string using PHP `substr()` offset and length rules. +fn eval_substr_result( + value: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(0) + } else { + start.saturating_add(length).min(total) + } + } + }; + let end = end.max(start); + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string_bytes_value(&bytes[start..end]) +} + /// Casts one eval value to PHP int and returns the scalar payload. fn eval_int_value( value: RuntimeCellHandle, @@ -6639,6 +6703,27 @@ return function_exists("str_repeat");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `substr()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_substr_builtin() { + let program = parse_fragment( + br#"echo substr("abcdef", 2); echo ":"; +echo substr(string: "abcdef", offset: 1, length: -1); echo ":"; +echo substr("abcdef", -2); echo ":"; +echo call_user_func("substr", "abcdef", 2, -2); echo ":"; +echo call_user_func_array("substr", ["string" => "abcdef", "offset" => -4, "length" => 2]); echo ":"; +return function_exists("substr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "cdef:bcde:ef:cd:cd:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_bin2hex_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 1de9e00642..9874da59e7 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,7 +25,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers byte-string helpers including `chr()`, `strrev()`, `str_repeat()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers byte-string helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 62e1260f66..6881155d63 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `chr()`, `strrev()`, `str_repeat()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 9a0c4636d4..47838a51ea 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -133,6 +133,7 @@ public function __construct(int $value) { $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); $chr = eval('return chr(65) . ":" . bin2hex(chr(256));'); $repeated = eval('return str_repeat("ha", 3);'); +$substring = eval('return substr("abcdef", 2) . ":" . substr("abcdef", 1, -1);'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -212,6 +213,7 @@ public function __construct(int $value) { echo "slashes=" . $slashes . "\n"; echo "chr=" . $chr . "\n"; echo "str-repeat=" . $repeated . "\n"; +echo "substr=" . $substring . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 72fddfb2b0..6a45599f32 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -944,6 +944,22 @@ echo ":"; echo function_exists("str_repeat");'); assert_eq!(out, "hahaha:0:abab:zzz:1"); } +/// Verifies eval `substr()` slices byte strings directly and by callable dispatch. +#[test] +fn test_eval_dispatches_substr_builtin_call() { + let out = compile_and_run( + r#" "abcdef", "offset" => -4, "length" => 2]); +echo ":"; echo function_exists("substr");'); +"#, + ); + assert_eq!(out, "cdef:bcde:ef:cd:cd:1"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From defd260c0239bd43ee3a3e40ce5099007abcbbf4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:40:14 +0200 Subject: [PATCH 0132/1208] Support eval nl2br builtin --- crates/elephc-eval/src/interpreter.rs | 91 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 18 ++++++ 5 files changed, 114 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7268c0ec2d..0119c7a4d8 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1185,6 +1185,7 @@ fn eval_positional_expr_call( } "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), + "nl2br" => eval_builtin_nl2br(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), "pi" => eval_builtin_pi(args, values), "pow" => eval_builtin_pow(args, context, scope, values), @@ -1501,6 +1502,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "lcfirst" | "max" | "min" + | "nl2br" | "ord" | "pi" | "pow" @@ -1625,6 +1627,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "function_exists" => Some(&["function"]), "hash_equals" => Some(&["known_string", "user_string"]), "max" | "min" => Some(&["value"]), + "nl2br" => Some(&["string", "use_xhtml"]), "ord" => Some(&["character"]), "pi" => Some(&[]), "pow" => Some(&["num", "exponent"]), @@ -1948,6 +1951,14 @@ fn eval_builtin_with_values( eval_ord_result(*value, values)? } "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, + "nl2br" => match evaluated_args { + [value] => eval_nl2br_result(*value, true, values)?, + [value, use_xhtml] => { + let use_xhtml = values.truthy(*use_xhtml)?; + eval_nl2br_result(*value, use_xhtml, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values)?, [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, @@ -2499,6 +2510,63 @@ fn eval_str_repeat_result( values.string_bytes_value(&output) } +/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. +fn eval_builtin_nl2br( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_nl2br_result(value, true, values) + } + [value, use_xhtml] => { + let value = eval_expr(value, context, scope, values)?; + let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; + let use_xhtml = values.truthy(use_xhtml)?; + eval_nl2br_result(value, use_xhtml, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. +fn eval_nl2br_result( + value: RuntimeCellHandle, + use_xhtml: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let br = if use_xhtml { + b"
".as_slice() + } else { + b"
".as_slice() + }; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + let byte = bytes[index]; + if byte == b'\r' || byte == b'\n' { + output.extend_from_slice(br); + output.push(byte); + if index + 1 < bytes.len() + && ((byte == b'\r' && bytes[index + 1] == b'\n') + || (byte == b'\n' && bytes[index + 1] == b'\r')) + { + output.push(bytes[index + 1]); + index += 2; + continue; + } + } else { + output.push(byte); + } + index += 1; + } + values.string_bytes_value(&output) +} + /// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. fn eval_builtin_substr( args: &[EvalExpr], @@ -6724,6 +6792,29 @@ return function_exists("substr");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_nl2br_builtin() { + let program = parse_fragment( + br#"echo bin2hex(nl2br("a\nb")); echo ":"; +echo bin2hex(nl2br(string: "a\nb", use_xhtml: false)); echo ":"; +echo bin2hex(call_user_func("nl2br", "a\r\nb")); echo ":"; +echo bin2hex(call_user_func_array("nl2br", ["string" => "a\n\rb", "use_xhtml" => false])); echo ":"; +return function_exists("nl2br");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_bin2hex_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9874da59e7..2d17cf9182 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,7 +25,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers byte-string helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers byte-string helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6881155d63..0db76b5e71 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `nl2br()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports byte-string helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 47838a51ea..775429ed2c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -134,6 +134,7 @@ public function __construct(int $value) { $chr = eval('return chr(65) . ":" . bin2hex(chr(256));'); $repeated = eval('return str_repeat("ha", 3);'); $substring = eval('return substr("abcdef", 2) . ":" . substr("abcdef", 1, -1);'); +$linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -214,6 +215,7 @@ public function __construct(int $value) { echo "chr=" . $chr . "\n"; echo "str-repeat=" . $repeated . "\n"; echo "substr=" . $substring . "\n"; +echo "nl2br-hex=" . $linebreaks . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6a45599f32..b9b899c82e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -960,6 +960,24 @@ echo ":"; echo function_exists("substr");'); assert_eq!(out, "cdef:bcde:ef:cd:cd:1"); } +/// Verifies eval `nl2br()` preserves newline bytes while inserting HTML breaks. +#[test] +fn test_eval_dispatches_nl2br_builtin_call() { + let out = compile_and_run( + r#" "a\n\rb", "use_xhtml" => false])); +echo ":"; echo function_exists("nl2br");'); +"#, + ); + assert_eq!( + out, + "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:1" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 62e6c7a1bc51426a36d4ffd525ca776c200629ed Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:44:56 +0200 Subject: [PATCH 0133/1208] Support eval explode implode builtins --- crates/elephc-eval/src/interpreter.rs | 139 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 19 ++++ 5 files changed, 163 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0119c7a4d8..82538f175a 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1171,6 +1171,7 @@ fn eval_positional_expr_call( "defined" => eval_builtin_defined(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), + "explode" => eval_builtin_explode(args, context, scope, values), "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { @@ -1179,6 +1180,7 @@ fn eval_positional_expr_call( "gettype" => eval_builtin_gettype(args, context, scope, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), + "implode" => eval_builtin_implode(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) @@ -1475,6 +1477,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "count" | "define" | "defined" + | "explode" | "fdiv" | "floor" | "floatval" @@ -1483,6 +1486,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "gettype" | "hash_equals" | "hex2bin" + | "implode" | "in_array" | "intval" | "ltrim" @@ -1623,9 +1627,11 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "count" => Some(&["value", "mode"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), + "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "function_exists" => Some(&["function"]), "hash_equals" => Some(&["known_string", "user_string"]), + "implode" => Some(&["separator", "array"]), "max" | "min" => Some(&["value"]), "nl2br" => Some(&["string", "use_xhtml"]), "ord" => Some(&["character"]), @@ -1944,12 +1950,24 @@ fn eval_builtin_with_values( } "define" => eval_define_result(evaluated_args, context, values)?, "defined" => eval_defined_result(evaluated_args, context, values)?, + "explode" => { + let [separator, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_explode_result(*separator, *string, values)? + } "ord" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_ord_result(*value, values)? } + "implode" => { + let [separator, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_implode_result(*separator, *array, values)? + } "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, "nl2br" => match evaluated_args { [value] => eval_nl2br_result(*value, true, values)?, @@ -2346,6 +2364,103 @@ fn eval_array_search_result( } } +/// Evaluates PHP `explode()` over separator and string expressions. +fn eval_builtin_explode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, string] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let string = eval_expr(string, context, scope, values)?; + eval_explode_result(separator, string, values) +} + +/// Splits one PHP byte string into an indexed array using a non-empty separator. +fn eval_explode_result( + separator: RuntimeCellHandle, + string: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let separator = values.string_bytes(separator)?; + if separator.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let string = values.string_bytes(string)?; + let mut result = values.array_new(0)?; + let mut start = 0; + let mut index = 0_i64; + while let Some(found) = eval_find_subslice(&string, &separator, start) { + result = eval_push_explode_segment(result, index, &string[start..found], values)?; + start = found + separator.len(); + index += 1; + } + eval_push_explode_segment(result, index, &string[start..], values) +} + +/// Appends one split segment to an indexed `explode()` result array. +fn eval_push_explode_segment( + array: RuntimeCellHandle, + index: i64, + segment: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(index)?; + let value = values.string_bytes_value(segment)?; + values.array_set(array, key, value) +} + +/// Finds `needle` inside `haystack` starting from one byte offset. +fn eval_find_subslice(haystack: &[u8], needle: &[u8], start: usize) -> Option { + haystack + .get(start..)? + .windows(needle.len()) + .position(|window| window == needle) + .map(|position| position + start) +} + +/// Evaluates PHP `implode()` over separator and array expressions. +fn eval_builtin_implode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_implode_result(separator, array, values) +} + +/// Joins array values in eval iteration order using PHP string conversion. +fn eval_implode_result( + separator: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let separator = values.string_bytes(separator)?; + let len = values.array_len(array)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.extend_from_slice(&separator); + } + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let value = values.string_bytes(value)?; + output.extend_from_slice(&value); + } + values.string_bytes_value(&output) +} + /// Evaluates PHP's `ceil(...)` over one eval expression. fn eval_builtin_ceil( args: &[EvalExpr], @@ -6296,6 +6411,30 @@ return function_exists("array_search");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. + #[test] + fn execute_program_dispatches_explode_implode_builtins() { + let program = parse_fragment( + br#"$parts = explode(",", "a,b,"); +echo count($parts); echo ":" . $parts[0] . ":" . $parts[1] . ":" . $parts[2]; +echo ":" . implode("|", $parts); +echo ":" . implode(separator: "-", array: ["x", 2, true, null]); +$call_parts = call_user_func("explode", ":", "m:n"); +echo ":" . $call_parts[1]; +echo ":" . call_user_func_array("implode", ["separator" => "/", "array" => ["p", "q"]]); +echo ":"; echo function_exists("explode"); +return function_exists("implode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:a:b::a|b|:x-2-1-:n:p/q:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2d17cf9182..a4391bcfa7 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,7 +25,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers byte-string helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 0db76b5e71..8a2bf0a7ca 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `nl2br()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports byte-string helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 775429ed2c..def683edaf 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -135,6 +135,7 @@ public function __construct(int $value) { $repeated = eval('return str_repeat("ha", 3);'); $substring = eval('return substr("abcdef", 2) . ":" . substr("abcdef", 1, -1);'); $linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); +$split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -216,6 +217,7 @@ public function __construct(int $value) { echo "str-repeat=" . $repeated . "\n"; echo "substr=" . $substring . "\n"; echo "nl2br-hex=" . $linebreaks . "\n"; +echo "explode-implode=" . $split_joined . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b9b899c82e..7f7ef1abf8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -978,6 +978,25 @@ echo ":"; echo function_exists("nl2br");'); ); } +/// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. +#[test] +fn test_eval_dispatches_explode_implode_builtin_calls() { + let out = compile_and_run( + r#" "/", "array" => ["p", "q"]]); +echo ":"; echo function_exists("explode"); +echo ":"; echo function_exists("implode");'); +"#, + ); + assert_eq!(out, "3:a:b::a|b|:x-2-1-:n:p/q:1:1"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From dfd2c81cb39fd544728438278b370b5cefa84182 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:48:12 +0200 Subject: [PATCH 0134/1208] Support eval string replace builtins --- crates/elephc-eval/src/interpreter.rs | 98 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 18 +++++ 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 82538f175a..cf476ed175 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1196,6 +1196,9 @@ fn eval_positional_expr_call( "sqrt" => eval_builtin_sqrt(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), + "str_replace" | "str_ireplace" => { + eval_builtin_str_replace(name, args, context, scope, values) + } "substr" => eval_builtin_substr(args, context, scope, values), "str_contains" | "str_starts_with" | "str_ends_with" => { eval_builtin_string_search(name, args, context, scope, values) @@ -1516,7 +1519,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strcasecmp" | "str_contains" | "str_ends_with" + | "str_ireplace" | "str_repeat" + | "str_replace" | "str_starts_with" | "strcmp" | "strlen" @@ -1640,6 +1645,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "round" => Some(&["num", "precision"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), + "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), "str_repeat" => Some(&["string", "times"]), "substr" => Some(&["string", "offset", "length"]), @@ -1918,6 +1924,12 @@ fn eval_builtin_with_values( }; eval_str_repeat_result(*value, *times, values)? } + "str_replace" | "str_ireplace" => { + let [search, replace, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_replace_result(name, *search, *replace, *subject, values)? + } "substr" => match evaluated_args { [value, offset] => eval_substr_result(*value, *offset, None, values)?, [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, @@ -2625,6 +2637,69 @@ fn eval_str_repeat_result( values.string_bytes_value(&output) } +/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. +fn eval_builtin_str_replace( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [search, replace, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let search = eval_expr(search, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_str_replace_result(name, search, replace, subject, values) +} + +/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. +fn eval_str_replace_result( + name: &str, + search: RuntimeCellHandle, + replace: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let search = values.string_bytes(search)?; + let replace = values.string_bytes(replace)?; + let subject = values.string_bytes(subject)?; + if search.is_empty() { + return values.string_bytes_value(&subject); + } + + let mut output = Vec::with_capacity(subject.len()); + let mut start = 0; + while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { + output.extend_from_slice(&subject[start..found]); + output.extend_from_slice(&replace); + start = found + search.len(); + } + output.extend_from_slice(&subject[start..]); + values.string_bytes_value(&output) +} + +/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. +fn eval_find_replace_match( + name: &str, + subject: &[u8], + search: &[u8], + start: usize, +) -> Result, EvalStatus> { + match name { + "str_replace" => Ok(eval_find_subslice(subject, search, start)), + "str_ireplace" => Ok(subject + .get(start..) + .and_then(|tail| { + tail.windows(search.len()) + .position(|window| window.eq_ignore_ascii_case(search)) + }) + .map(|position| position + start)), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. fn eval_builtin_nl2br( args: &[EvalExpr], @@ -6435,6 +6510,29 @@ return function_exists("implode");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval string replacement builtins support direct, named, and callable dispatch. + #[test] + fn execute_program_dispatches_string_replace_builtins() { + let program = parse_fragment( + br#"echo str_replace("o", "0", "Hello World"); echo ":"; +echo str_replace(search: "aa", replace: "b", subject: "aaaa"); echo ":"; +echo str_replace("", "x", "abc"); echo ":"; +echo str_ireplace("HE", "ye", "Hello he"); echo ":"; +echo call_user_func("str_replace", "l", "L", "hello"); echo ":"; +echo call_user_func_array("str_ireplace", ["search" => "x", "replace" => "Y", "subject" => "xX"]); echo ":"; +echo function_exists("str_replace"); +return function_exists("str_ireplace");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a4391bcfa7..6931261f55 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,7 +25,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 8a2bf0a7ca..c161a25078 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index def683edaf..3f54c1794b 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -136,6 +136,7 @@ public function __construct(int $value) { $substring = eval('return substr("abcdef", 2) . ":" . substr("abcdef", 1, -1);'); $linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); $split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); +$replaced = eval('return str_replace("green", "lime", "red green blue");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -218,6 +219,7 @@ public function __construct(int $value) { echo "substr=" . $substring . "\n"; echo "nl2br-hex=" . $linebreaks . "\n"; echo "explode-implode=" . $split_joined . "\n"; +echo "str-replace=" . $replaced . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7f7ef1abf8..7ad4f7c3ca 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -997,6 +997,24 @@ echo ":"; echo function_exists("implode");'); assert_eq!(out, "3:a:b::a|b|:x-2-1-:n:p/q:1:1"); } +/// Verifies eval string replacement builtins support direct and callable dispatch. +#[test] +fn test_eval_dispatches_string_replace_builtin_calls() { + let out = compile_and_run( + r#" "x", "replace" => "Y", "subject" => "xX"]); +echo ":"; echo function_exists("str_replace"); +echo ":"; echo function_exists("str_ireplace");'); +"#, + ); + assert_eq!(out, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1:1"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From cbdc9629a03451809382c88153d2b44dda35ce8c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:52:18 +0200 Subject: [PATCH 0135/1208] Support eval substr_replace builtin --- crates/elephc-eval/src/interpreter.rs | 96 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 16 +++++ 5 files changed, 117 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index cf476ed175..4e3041ac2b 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1200,6 +1200,7 @@ fn eval_positional_expr_call( eval_builtin_str_replace(name, args, context, scope, values) } "substr" => eval_builtin_substr(args, context, scope, values), + "substr_replace" => eval_builtin_substr_replace(args, context, scope, values), "str_contains" | "str_starts_with" | "str_ends_with" => { eval_builtin_string_search(name, args, context, scope, values) } @@ -1534,6 +1535,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strtoupper" | "strval" | "trim" + | "substr_replace" | "ucfirst" ) } @@ -1649,6 +1651,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), "str_repeat" => Some(&["string", "times"]), "substr" => Some(&["string", "offset", "length"]), + "substr_replace" => Some(&["string", "replace", "offset", "length"]), "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } @@ -1935,6 +1938,15 @@ fn eval_builtin_with_values( [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, _ => return Err(EvalStatus::RuntimeFatal), }, + "substr_replace" => match evaluated_args { + [value, replace, offset] => { + eval_substr_replace_result(*value, *replace, *offset, None, values)? + } + [value, replace, offset, length] => { + eval_substr_replace_result(*value, *replace, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "call_user_func" => { return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) .map(Some); @@ -2813,6 +2825,69 @@ fn eval_substr_result( values.string_bytes_value(&bytes[start..end]) } +/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. +fn eval_builtin_substr_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, replace, offset] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, None, values) + } + [value, replace, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. +fn eval_substr_replace_result( + value: RuntimeCellHandle, + replace: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let replacement = values.string_bytes(replace)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(start) + } else { + start.saturating_add(length).min(total) + } + } + }; + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(bytes.len() + replacement.len()); + output.extend_from_slice(&bytes[..start]); + output.extend_from_slice(&replacement); + output.extend_from_slice(&bytes[end..]); + values.string_bytes_value(&output) +} + /// Casts one eval value to PHP int and returns the scalar payload. fn eval_int_value( value: RuntimeCellHandle, @@ -7029,6 +7104,27 @@ return function_exists("substr");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `substr_replace()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_substr_replace_builtin() { + let program = parse_fragment( + br#"echo substr_replace("hello world", "PHP", 6, 5); echo ":"; +echo substr_replace(string: "abcdef", replace: "X", offset: 1, length: -1); echo ":"; +echo substr_replace("abcdef", "X", -2); echo ":"; +echo call_user_func("substr_replace", "abcdef", "X", 99, 1); echo ":"; +echo call_user_func_array("substr_replace", ["string" => "abcdef", "replace" => "X", "offset" => -99, "length" => 2]); echo ":"; +return function_exists("substr_replace");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hello PHP:aXf:abcdX:abcdefX:Xcdef:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_nl2br_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 6931261f55..3151bdca9c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,7 +25,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c161a25078..32c841a9ce 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 3f54c1794b..b89ad13d8a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -134,6 +134,7 @@ public function __construct(int $value) { $chr = eval('return chr(65) . ":" . bin2hex(chr(256));'); $repeated = eval('return str_repeat("ha", 3);'); $substring = eval('return substr("abcdef", 2) . ":" . substr("abcdef", 1, -1);'); +$substring_replaced = eval('return substr_replace("hello world", "PHP", 6, 5);'); $linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); $split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); $replaced = eval('return str_replace("green", "lime", "red green blue");'); @@ -217,6 +218,7 @@ public function __construct(int $value) { echo "chr=" . $chr . "\n"; echo "str-repeat=" . $repeated . "\n"; echo "substr=" . $substring . "\n"; +echo "substr-replace=" . $substring_replaced . "\n"; echo "nl2br-hex=" . $linebreaks . "\n"; echo "explode-implode=" . $split_joined . "\n"; echo "str-replace=" . $replaced . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7ad4f7c3ca..f818e60d7e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -960,6 +960,22 @@ echo ":"; echo function_exists("substr");'); assert_eq!(out, "cdef:bcde:ef:cd:cd:1"); } +/// Verifies eval `substr_replace()` replaces selected byte ranges through callable paths. +#[test] +fn test_eval_dispatches_substr_replace_builtin_call() { + let out = compile_and_run( + r#" "abcdef", "replace" => "X", "offset" => -99, "length" => 2]); +echo ":"; echo function_exists("substr_replace");'); +"#, + ); + assert_eq!(out, "hello PHP:aXf:abcdX:abcdefX:Xcdef:1"); +} + /// Verifies eval `nl2br()` preserves newline bytes while inserting HTML breaks. #[test] fn test_eval_dispatches_nl2br_builtin_call() { From c30890e6defd2bcafa5ae2a82d79a53f3a4870d1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 06:55:44 +0200 Subject: [PATCH 0136/1208] Support eval HTML entity builtins --- crates/elephc-eval/src/interpreter.rs | 125 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 21 +++++ 5 files changed, 151 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 4e3041ac2b..6865292a9c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1180,6 +1180,9 @@ fn eval_positional_expr_call( "gettype" => eval_builtin_gettype(args, context, scope, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { + eval_builtin_html_entity(name, args, context, scope, values) + } "implode" => eval_builtin_implode(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { @@ -1490,6 +1493,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "gettype" | "hash_equals" | "hex2bin" + | "html_entity_decode" + | "htmlentities" + | "htmlspecialchars" | "implode" | "in_array" | "intval" @@ -1638,6 +1644,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "fdiv" | "fmod" => Some(&["num1", "num2"]), "function_exists" => Some(&["function"]), "hash_equals" => Some(&["known_string", "user_string"]), + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), "max" | "min" => Some(&["value"]), "nl2br" => Some(&["string", "use_xhtml"]), @@ -2034,6 +2041,12 @@ fn eval_builtin_with_values( }; eval_hex2bin_result(*value, values)? } + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_html_entity_result(name, *value, values)? + } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { let [value] = evaluated_args else { @@ -2888,6 +2901,93 @@ fn eval_substr_replace_result( values.string_bytes_value(&output) } +/// Evaluates eval HTML entity encode/decode builtins over one string expression. +fn eval_builtin_html_entity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_html_entity_result(name, value, values) +} + +/// Applies the eval-supported HTML entity transform for one PHP string value. +fn eval_html_entity_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), + "html_entity_decode" => eval_html_entity_decode_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Encodes the HTML-special byte characters covered by elephc's static helper. +fn eval_htmlspecialchars_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + b'&' => output.extend_from_slice(b"&"), + b'<' => output.extend_from_slice(b"<"), + b'>' => output.extend_from_slice(b">"), + b'"' => output.extend_from_slice(b"""), + b'\'' => output.extend_from_slice(b"'"), + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Decodes one pass of the HTML entities emitted by the eval/static encoders. +fn eval_html_entity_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'&' { + if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { + output.push(decoded); + index += width; + continue; + } + } + output.push(bytes[index]); + index += 1; + } + values.string_bytes_value(&output) +} + +/// Returns the decoded byte and consumed width for one supported HTML entity. +fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { + for (entity, decoded) in [ + (b"<".as_slice(), b'<'), + (b">".as_slice(), b'>'), + (b""".as_slice(), b'"'), + (b"'".as_slice(), b'\''), + (b"'".as_slice(), b'\''), + (b"&".as_slice(), b'&'), + ] { + if bytes.starts_with(entity) { + return Some((decoded, entity.len())); + } + } + None +} + /// Casts one eval value to PHP int and returns the scalar payload. fn eval_int_value( value: RuntimeCellHandle, @@ -6608,6 +6708,31 @@ return function_exists("str_ireplace");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. + #[test] + fn execute_program_dispatches_html_entity_builtins() { + let program = parse_fragment( + br#"echo htmlspecialchars("\"Hi\" & 'bye'"); echo ":"; +echo htmlentities(string: ""); echo ":"; +echo html_entity_decode("<b>hi</b>"); echo ":"; +echo call_user_func("htmlspecialchars", ""); echo ":"; +echo call_user_func_array("html_entity_decode", ["string" => ""q""]); echo ":"; +echo function_exists("htmlspecialchars"); echo function_exists("htmlentities"); +return function_exists("html_entity_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3151bdca9c..d7d122fdd0 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,7 +25,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 32c841a9ce..f138e13031 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -40,7 +40,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b89ad13d8a..455622b003 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -138,6 +138,7 @@ public function __construct(int $value) { $linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); $split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); $replaced = eval('return str_replace("green", "lime", "red green blue");'); +$html_escaped = eval('return htmlspecialchars("bold");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -222,6 +223,7 @@ public function __construct(int $value) { echo "nl2br-hex=" . $linebreaks . "\n"; echo "explode-implode=" . $split_joined . "\n"; echo "str-replace=" . $replaced . "\n"; +echo "htmlspecialchars=" . $html_escaped . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f818e60d7e..9ad56e5661 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1031,6 +1031,27 @@ echo ":"; echo function_exists("str_ireplace");'); assert_eq!(out, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1:1"); } +/// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. +#[test] +fn test_eval_dispatches_html_entity_builtin_calls() { + let out = compile_and_run( + r#"\"Hi\" & \'bye\'"); echo ":"; +echo htmlentities(string: ""); echo ":"; +echo html_entity_decode("<b>hi</b>"); echo ":"; +echo call_user_func("htmlspecialchars", ""); echo ":"; +echo call_user_func_array("html_entity_decode", ["string" => ""q""]); +echo ":"; echo function_exists("htmlspecialchars"); +echo ":"; echo function_exists("htmlentities"); +echo ":"; echo function_exists("html_entity_decode");'); +"#, + ); + assert_eq!( + out, + "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":1:1:1" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 99eb10922dfcb52206ec5640b340a1389fe2bb6d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:02:47 +0200 Subject: [PATCH 0137/1208] Support eval URL encoding builtins --- crates/elephc-eval/src/interpreter.rs | 153 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 23 ++++ 5 files changed, 183 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6865292a9c..10c9074339 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1194,6 +1194,12 @@ fn eval_positional_expr_call( "ord" => eval_builtin_ord(args, context, scope, values), "pi" => eval_builtin_pi(args, values), "pow" => eval_builtin_pow(args, context, scope, values), + "rawurldecode" | "urldecode" => { + eval_builtin_url_decode(name, args, context, scope, values) + } + "rawurlencode" | "urlencode" => { + eval_builtin_url_encode(name, args, context, scope, values) + } "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), @@ -1520,6 +1526,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "ord" | "pi" | "pow" + | "rawurldecode" + | "rawurlencode" | "rtrim" | "round" | "sqrt" @@ -1543,6 +1551,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "trim" | "substr_replace" | "ucfirst" + | "urldecode" + | "urlencode" ) } @@ -1628,7 +1638,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" - | "stripslashes" => Some(&["string"]), + | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => { + Some(&["string"]) + } "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), @@ -1911,6 +1923,18 @@ fn eval_builtin_with_values( }; values.pow(*left, *right)? } + "rawurldecode" | "urldecode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_decode_result(name, *value, values)? + } + "rawurlencode" | "urlencode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_encode_result(name, *value, values)? + } "round" => match evaluated_args { [value] => values.round(*value, None)?, [value, precision] => values.round(*value, Some(*precision))?, @@ -2988,6 +3012,106 @@ fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { None } +/// Evaluates PHP URL encode builtins over one eval string expression. +fn eval_builtin_url_encode( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_encode_result(name, value, values) +} + +/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. +fn eval_url_encode_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + for byte in bytes { + if eval_url_encode_keeps_byte(name, byte)? { + output.push(byte); + } else if name == "urlencode" && byte == b' ' { + output.push(b'+'); + } else { + output.push(b'%'); + output.push(HEX[(byte >> 4) as usize]); + output.push(HEX[(byte & 0x0f) as usize]); + } + } + values.string_bytes_value(&output) +} + +/// Returns whether a byte remains unescaped for the selected PHP URL encoder. +fn eval_url_encode_keeps_byte(name: &str, byte: u8) -> Result { + let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); + match name { + "urlencode" => Ok(common), + "rawurlencode" => Ok(common || byte == b'~'), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP URL decode builtins over one eval string expression. +fn eval_builtin_url_decode( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_decode_result(name, value, values) +} + +/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. +fn eval_url_decode_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let plus_to_space = match name { + "urldecode" => true, + "rawurldecode" => false, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'+' && plus_to_space { + output.push(b' '); + index += 1; + } else if bytes[index] == b'%' && index + 2 < bytes.len() { + if let (Some(high), Some(low)) = ( + eval_hex_nibble(bytes[index + 1]), + eval_hex_nibble(bytes[index + 2]), + ) { + output.push((high << 4) | low); + index += 3; + continue; + } + output.push(bytes[index]); + index += 1; + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} + /// Casts one eval value to PHP int and returns the scalar payload. fn eval_int_value( value: RuntimeCellHandle, @@ -6733,6 +6857,33 @@ return function_exists("html_entity_decode");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval URL codec builtins dispatch through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_url_codec_builtins() { + let program = parse_fragment( + br#"echo urlencode("a b&=~"); echo ":"; +echo rawurlencode(string: "a b&=~"); echo ":"; +echo urldecode("a+b%26%3D%7E"); echo ":"; +echo rawurldecode("a+b%26%3D%7E"); echo ":"; +echo call_user_func("urlencode", "%zz"); echo ":"; +echo call_user_func_array("rawurldecode", ["string" => "x%2By%zz"]); echo ":"; +echo function_exists("urlencode"); echo function_exists("rawurlencode"); +echo function_exists("urldecode"); +return function_exists("rawurldecode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d7d122fdd0..2d1ad37af3 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,13 +19,15 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index f138e13031..0284026e2b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,13 +34,15 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 455622b003..6279b6c8e2 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -139,6 +139,7 @@ public function __construct(int $value) { $split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); $replaced = eval('return str_replace("green", "lime", "red green blue");'); $html_escaped = eval('return htmlspecialchars("bold");'); +$url_codec = eval('return urlencode("a b&=") . ":" . rawurldecode("a%20b%26%3D");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -224,6 +225,7 @@ public function __construct(int $value) { echo "explode-implode=" . $split_joined . "\n"; echo "str-replace=" . $replaced . "\n"; echo "htmlspecialchars=" . $html_escaped . "\n"; +echo "url-codec=" . $url_codec . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9ad56e5661..ba8a168f77 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1052,6 +1052,29 @@ echo ":"; echo function_exists("html_entity_decode");'); ); } +/// Verifies eval URL codec builtins encode, decode, and dispatch as callables. +#[test] +fn test_eval_dispatches_url_codec_builtin_calls() { + let out = compile_and_run( + r#" "x%2By%zz"]); +echo ":"; echo function_exists("urlencode"); +echo ":"; echo function_exists("rawurlencode"); +echo ":"; echo function_exists("urldecode"); +echo ":"; echo function_exists("rawurldecode");'); +"#, + ); + assert_eq!( + out, + "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:1:1:1:1" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 84ced0ef614e647a9f0a280a2c04f2c74ae3fc68 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:05:44 +0200 Subject: [PATCH 0138/1208] Support eval ctype builtins --- crates/elephc-eval/src/interpreter.rs | 85 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 24 ++++++++ 5 files changed, 117 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 10c9074339..0969a9edf4 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1167,6 +1167,9 @@ fn eval_positional_expr_call( eval_builtin_cast(name, args, context, scope, values) } "count" => eval_builtin_count(args, context, scope, values), + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { + eval_builtin_ctype(name, args, context, scope, values) + } "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), @@ -1488,6 +1491,10 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "chop" | "chr" | "count" + | "ctype_alnum" + | "ctype_alpha" + | "ctype_digit" + | "ctype_space" | "define" | "defined" | "explode" @@ -1650,6 +1657,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "chr" => Some(&["codepoint"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "explode" => Some(&["separator", "string"]), @@ -2003,6 +2011,12 @@ fn eval_builtin_with_values( let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ctype_result(name, *value, values)? + } "define" => eval_define_result(evaluated_args, context, values)?, "defined" => eval_defined_result(evaluated_args, context, values)?, "explode" => { @@ -3112,6 +3126,49 @@ fn eval_url_decode_result( values.string_bytes_value(&output) } +/// Evaluates PHP `ctype_*` predicates over one eval string expression. +fn eval_builtin_ctype( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ctype_result(name, value, values) +} + +/// Returns the PHP boolean result for one ASCII `ctype_*` byte-string check. +fn eval_ctype_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut matches = !bytes.is_empty(); + for byte in bytes { + if !eval_ctype_byte_matches(name, byte)? { + matches = false; + break; + } + } + values.bool_value(matches) +} + +/// Checks one byte against the selected PHP ASCII character class. +fn eval_ctype_byte_matches(name: &str, byte: u8) -> Result { + match name { + "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), + "ctype_digit" => Ok(byte.is_ascii_digit()), + "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), + "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Casts one eval value to PHP int and returns the scalar payload. fn eval_int_value( value: RuntimeCellHandle, @@ -6884,6 +6941,34 @@ return function_exists("rawurldecode");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_ctype_builtins() { + let program = parse_fragment( + br#"echo ctype_alpha("abc") ? "A" : "-"; echo ":"; +echo ctype_digit(text: "123") ? "D" : "-"; echo ":"; +echo ctype_alnum("a1") ? "N" : "-"; echo ":"; +echo ctype_space(" \t\n" . chr(11) . chr(12) . "\r") ? "S" : "-"; echo ":"; +echo ctype_alpha("") ? "bad" : "empty"; echo ":"; +echo call_user_func("ctype_digit", "12x") ? "bad" : "not-digit"; echo ":"; +echo call_user_func_array("ctype_space", ["text" => " x"]) ? "bad" : "not-space"; echo ":"; +echo function_exists("ctype_alpha"); echo function_exists("ctype_digit"); +echo function_exists("ctype_alnum"); +return function_exists("ctype_space");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "A:D:N:S:empty:not-digit:not-space:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2d1ad37af3..00c02448ea 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -21,13 +21,15 @@ The bridge crate parses the fragment at runtime into a small EvalIR and interpre Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. +Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 0284026e2b..6804218bd9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -36,13 +36,15 @@ The evaluated string must be a PHP fragment without an opening ` null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); +$ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); $chr = eval('return chr(65) . ":" . bin2hex(chr(256));'); $repeated = eval('return str_repeat("ha", 3);'); @@ -216,6 +217,7 @@ public function __construct(int $value) { echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "string-compare=" . $string_compare . "\n"; +echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; echo "chr=" . $chr . "\n"; echo "str-repeat=" . $repeated . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ba8a168f77..626c7e7d92 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1075,6 +1075,30 @@ echo ":"; echo function_exists("rawurldecode");'); ); } +/// Verifies eval `ctype_*` predicates inspect ASCII byte classes. +#[test] +fn test_eval_dispatches_ctype_builtin_calls() { + let out = compile_and_run( + r#" " x"]) ? "bad" : "not-space"; +echo ":"; echo function_exists("ctype_alpha"); +echo ":"; echo function_exists("ctype_digit"); +echo ":"; echo function_exists("ctype_alnum"); +echo ":"; echo function_exists("ctype_space");'); +"#, + ); + assert_eq!( + out, + "A:D:N:S:empty:not-digit:not-space:1:1:1:1" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 583697062ed11a91e563e024b479e4ec5348b311 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:08:45 +0200 Subject: [PATCH 0139/1208] Support eval ucwords builtin --- crates/elephc-eval/src/interpreter.rs | 75 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 16 ++++++ 5 files changed, 99 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0969a9edf4..26a455d7dd 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1223,6 +1223,7 @@ fn eval_positional_expr_call( eval_builtin_string_case(name, args, context, scope, values) } "trim" => eval_builtin_trim_like(name, args, context, scope, values), + "ucwords" => eval_builtin_ucwords(args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), } } @@ -1558,6 +1559,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "trim" | "substr_replace" | "ucfirst" + | "ucwords" | "urldecode" | "urlencode" ) @@ -1682,6 +1684,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } + "ucwords" => Some(&["string", "separators"]), _ => None, } } @@ -2124,6 +2127,11 @@ fn eval_builtin_with_values( }; eval_string_case_result(name, *value, values)? } + "ucwords" => match evaluated_args { + [value] => eval_ucwords_result(*value, None, values)?, + [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, _ => return Ok(None), }; Ok(Some(result)) @@ -3939,6 +3947,52 @@ fn eval_string_case_result( values.string(&value) } +/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. +fn eval_builtin_ucwords( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_ucwords_result(value, None, values) + } + [value, separators] => { + let value = eval_expr(value, context, scope, values)?; + let separators = eval_expr(separators, context, scope, values)?; + eval_ucwords_result(value, Some(separators), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. +fn eval_ucwords_result( + value: RuntimeCellHandle, + separators: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + let separators = match separators { + Some(separators) => values.string_bytes(separators)?, + None => b" \t\r\n\x0c\x0b".to_vec(), + }; + let mut word_start = true; + for byte in &mut bytes { + if separators.contains(byte) { + word_start = true; + } else if word_start { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + word_start = false; + } + } + values.string_bytes_value(&bytes) +} + /// Evaluates nested `eval(...)` calls against the current materialized scope. fn eval_nested_eval( args: &[EvalExpr], @@ -6997,6 +7051,27 @@ return function_exists("lcfirst");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `ucwords()` capitalizes word starts directly and by callable dispatch. + #[test] + fn execute_program_dispatches_ucwords_builtin() { + let program = parse_fragment( + br#"echo ucwords("hello world"); echo ":"; +echo ucwords(string: "hello-world", separators: "-"); echo ":"; +echo ucwords("hello\tworld"); echo ":"; +echo call_user_func("ucwords", "a b"); echo ":"; +echo call_user_func_array("ucwords", ["string" => "a-b", "separators" => "-"]); echo ":"; +return function_exists("ucwords");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hello World:Hello-World:Hello\tWorld:A B:A-B:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. #[test] fn execute_program_dispatches_str_contains_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 00c02448ea..6c0c6420d2 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -23,13 +23,15 @@ Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. +Eval `ucwords()` performs ASCII word-start capitalization and honors the optional `separators` argument. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6804218bd9..247919cbd8 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -38,13 +38,15 @@ The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawu Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` with the same direct, named-argument, callable, and `function_exists()` dispatch paths. +The word-case helper `ucwords()` is available inside eval with its optional `separators` parameter. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 5ebfb08113..13e94160e3 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -115,6 +115,7 @@ public function __construct(int $value) { $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5);'); $circle = eval('return round(pi(), 2);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); +$word_case = eval('return ucwords("hello eval");'); $reversed = eval('return strrev("eval");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); @@ -202,6 +203,7 @@ public function __construct(int $value) { echo "minmax=" . $minmax . "\n"; echo "pi=" . $circle . "\n"; echo "case=" . $case . "\n"; +echo "ucwords=" . $word_case . "\n"; echo "reversed=" . $reversed . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 626c7e7d92..59a184a3ea 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -899,6 +899,22 @@ echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower") assert_eq!(out, "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:1111"); } +/// Verifies eval `ucwords()` capitalizes words directly and by callable dispatch. +#[test] +fn test_eval_dispatches_ucwords_builtin_call() { + let out = compile_and_run( + r#" "a-b", "separators" => "-"]); +echo ":"; echo function_exists("ucwords");'); +"#, + ); + assert_eq!(out, "Hello World:Hello-World:Hello\tWorld:A B:A-B:1"); +} + /// Verifies eval `strrev()` reverses byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_strrev_builtin_call() { From 147f662801a10268ec446087349911d107c1cb20 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:11:52 +0200 Subject: [PATCH 0140/1208] Support eval strstr builtin --- crates/elephc-eval/src/interpreter.rs | 85 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 17 ++++++ 5 files changed, 110 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 26a455d7dd..8b1bbc29eb 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1211,6 +1211,7 @@ fn eval_positional_expr_call( "str_replace" | "str_ireplace" => { eval_builtin_str_replace(name, args, context, scope, values) } + "strstr" => eval_builtin_strstr(args, context, scope, values), "substr" => eval_builtin_substr(args, context, scope, values), "substr_replace" => eval_builtin_substr_replace(args, context, scope, values), "str_contains" | "str_starts_with" | "str_ends_with" => { @@ -1551,6 +1552,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strpos" | "strrpos" | "strrev" + | "strstr" | "substr" | "stripslashes" | "strtolower" @@ -1676,6 +1678,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "round" => Some(&["num", "precision"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), + "strstr" => Some(&["haystack", "needle", "before_needle"]), "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), "str_repeat" => Some(&["string", "times"]), @@ -2115,6 +2118,14 @@ fn eval_builtin_with_values( }; eval_string_search_result(name, *haystack, *needle, values)? } + "strstr" => match evaluated_args { + [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values)?, + [haystack, needle, before_needle] => { + let before_needle = values.truthy(*before_needle)?; + eval_strstr_result(*haystack, *needle, before_needle, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "strcmp" | "strcasecmp" => { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3833,6 +3844,55 @@ fn eval_string_position_result( } } +/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. +fn eval_builtin_strstr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [haystack, needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_strstr_result(haystack, needle, false, values) + } + [haystack, needle, before_needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + let before_needle = eval_expr(before_needle, context, scope, values)?; + let before_needle = values.truthy(before_needle)?; + eval_strstr_result(haystack, needle, before_needle, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. +fn eval_strstr_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + before_needle: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = if needle.is_empty() { + Some(0) + } else { + eval_find_subslice(&haystack, &needle, 0) + }; + let Some(position) = position else { + return values.bool_value(false); + }; + let result = if before_needle { + &haystack[..position] + } else { + &haystack[position..] + }; + values.string_bytes_value(result) +} + const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; /// Evaluates PHP trim-like string builtins over one eval expression and optional mask. @@ -7117,6 +7177,31 @@ return function_exists("strrpos");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `strstr()` returns suffixes, prefixes, or false for misses. + #[test] + fn execute_program_dispatches_strstr_builtin() { + let program = parse_fragment( + br#"echo strstr("user@example.com", "@"); echo ":"; +echo strstr(haystack: "hello world", needle: "lo", before_needle: true); echo ":"; +echo strstr("hello", "x") === false ? "F" : "bad"; echo ":"; +echo strstr("hello", ""); echo ":"; +echo call_user_func("strstr", "abcabc", "bc"); echo ":"; +echo call_user_func_array("strstr", ["haystack" => "abcabc", "needle" => "bc", "before_needle" => true]); echo ":"; +return function_exists("strstr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "@example.com:hel:F:hello:bcabc:a:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval prefix/suffix string search builtins use byte-string semantics. #[test] fn execute_program_dispatches_string_boundary_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 6c0c6420d2..37b0547491 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -25,13 +25,15 @@ Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, ` Eval `ucwords()` performs ASCII word-start capitalization and honors the optional `separators` argument. +Eval `strstr()` uses the shared byte-search helper to return the matching suffix, optional prefix, or PHP `false`. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `strstr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 247919cbd8..a4c6a75b54 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -40,13 +40,15 @@ Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digi The word-case helper `ucwords()` is available inside eval with its optional `separators` parameter. +Eval `strstr()` returns matching suffixes, `before_needle` prefixes, or `false` when the needle is absent. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `strstr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 13e94160e3..7d60badac7 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -119,6 +119,7 @@ public function __construct(int $value) { $reversed = eval('return strrev("eval");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); +$substring_from = eval('return strstr("user@example.com", "@");'); $ordinal = eval('return ord("A") . ":" . ord("");'); $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); @@ -207,6 +208,7 @@ public function __construct(int $value) { echo "reversed=" . $reversed . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; +echo "strstr=" . $substring_from . "\n"; echo "ordinal=" . $ordinal . "\n"; echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 59a184a3ea..ab0e3891cf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1225,6 +1225,23 @@ echo ":"; echo function_exists("strpos"); echo function_exists("strrpos");'); assert_eq!(out, "2:4:F:0:3:1:3:11"); } +/// Verifies eval `strstr()` returns matching suffixes, prefixes, and false for misses. +#[test] +fn test_eval_dispatches_strstr_builtin_call() { + let out = compile_and_run( + r#" "abcabc", "needle" => "bc", "before_needle" => true]); +echo ":"; echo function_exists("strstr");'); +"#, + ); + assert_eq!(out, "@example.com:hel:F:hello:bcabc:a:1"); +} + /// Verifies eval string boundary builtins support direct and callable byte-string checks. #[test] fn test_eval_dispatches_string_boundary_builtin_calls() { From 71b55604ff59f824065629674ba6ef9cd71c1d14 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:14:27 +0200 Subject: [PATCH 0141/1208] Support eval str_split builtin --- crates/elephc-eval/src/interpreter.rs | 80 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 21 +++++++ 5 files changed, 109 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 8b1bbc29eb..38364f76f0 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1211,6 +1211,7 @@ fn eval_positional_expr_call( "str_replace" | "str_ireplace" => { eval_builtin_str_replace(name, args, context, scope, values) } + "str_split" => eval_builtin_str_split(args, context, scope, values), "strstr" => eval_builtin_strstr(args, context, scope, values), "substr" => eval_builtin_substr(args, context, scope, values), "substr_replace" => eval_builtin_substr_replace(args, context, scope, values), @@ -1552,6 +1553,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strpos" | "strrpos" | "strrev" + | "str_split" | "strstr" | "substr" | "stripslashes" @@ -1682,6 +1684,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), "str_repeat" => Some(&["string", "times"]), + "str_split" => Some(&["string", "length"]), "substr" => Some(&["string", "offset", "length"]), "substr_replace" => Some(&["string", "replace", "offset", "length"]), "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { @@ -1978,6 +1981,11 @@ fn eval_builtin_with_values( }; eval_str_replace_result(name, *search, *replace, *subject, values)? } + "str_split" => match evaluated_args { + [value] => eval_str_split_result(*value, None, values)?, + [value, length] => eval_str_split_result(*value, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "substr" => match evaluated_args { [value, offset] => eval_substr_result(*value, *offset, None, values)?, [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, @@ -2782,6 +2790,52 @@ fn eval_find_replace_match( } } +/// Evaluates PHP `str_split(...)` over one string and optional chunk length. +fn eval_builtin_str_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_str_split_result(value, None, values) + } + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_split_result(value, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. +fn eval_str_split_result( + value: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let length = match length { + Some(length) => eval_int_value(length, values)?, + None => 1, + }; + if length <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = values.array_new(0)?; + for (index, chunk) in bytes.chunks(length).enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string_bytes_value(chunk)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. fn eval_builtin_nl2br( args: &[EvalExpr], @@ -6980,6 +7034,32 @@ return function_exists("implode");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `str_split()` builds indexed arrays of fixed-width chunks. + #[test] + fn execute_program_dispatches_str_split_builtin() { + let program = parse_fragment( + br#"$letters = str_split("abc"); +echo count($letters) . ":" . $letters[0] . $letters[1] . $letters[2]; echo ":"; +$pairs = str_split(string: "abcd", length: 2); +echo $pairs[0] . "-" . $pairs[1]; echo ":"; +$empty = str_split(""); +echo count($empty); echo ":"; +$call = call_user_func("str_split", "xyz", 2); +echo $call[0] . "-" . $call[1]; echo ":"; +$named = call_user_func_array("str_split", ["string" => "pqrs", "length" => 3]); +echo $named[0] . "-" . $named[1]; echo ":"; +return function_exists("str_split");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:abc:ab-cd:0:xy-z:pqr-s:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval string replacement builtins support direct, named, and callable dispatch. #[test] fn execute_program_dispatches_string_replace_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 37b0547491..03789832b8 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -27,13 +27,15 @@ Eval `ucwords()` performs ASCII word-start capitalization and honors the optiona Eval `strstr()` uses the shared byte-search helper to return the matching suffix, optional prefix, or PHP `false`. +Eval `str_split()` builds indexed chunk arrays through the eval value hooks so later eval code can read the chunks normally. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `strstr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `strstr()`, `str_split()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a4c6a75b54..59bb8d3299 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -42,13 +42,15 @@ The word-case helper `ucwords()` is available inside eval with its optional `sep Eval `strstr()` returns matching suffixes, `before_needle` prefixes, or `false` when the needle is absent. +Eval `str_split()` returns indexed arrays of fixed-width string chunks and supports the optional `length` parameter. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `strstr()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `strstr()`, `str_split()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 7d60badac7..38d95cf441 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -140,6 +140,7 @@ public function __construct(int $value) { $substring_replaced = eval('return substr_replace("hello world", "PHP", 6, 5);'); $linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); $split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); +$string_chunks = eval('$chunks = str_split("eval", 2); return $chunks[0] . ":" . $chunks[1];'); $replaced = eval('return str_replace("green", "lime", "red green blue");'); $html_escaped = eval('return htmlspecialchars("bold");'); $url_codec = eval('return urlencode("a b&=") . ":" . rawurldecode("a%20b%26%3D");'); @@ -229,6 +230,7 @@ public function __construct(int $value) { echo "substr-replace=" . $substring_replaced . "\n"; echo "nl2br-hex=" . $linebreaks . "\n"; echo "explode-implode=" . $split_joined . "\n"; +echo "str-split=" . $string_chunks . "\n"; echo "str-replace=" . $replaced . "\n"; echo "htmlspecialchars=" . $html_escaped . "\n"; echo "url-codec=" . $url_codec . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ab0e3891cf..575107b6fa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1029,6 +1029,27 @@ echo ":"; echo function_exists("implode");'); assert_eq!(out, "3:a:b::a|b|:x-2-1-:n:p/q:1:1"); } +/// Verifies eval `str_split()` builds indexed chunk arrays. +#[test] +fn test_eval_dispatches_str_split_builtin_call() { + let out = compile_and_run( + r#" "pqrs", "length" => 3]); +echo $named[0] . "-" . $named[1]; +echo ":"; echo function_exists("str_split");'); +"#, + ); + assert_eq!(out, "3:abc:ab-cd:0:xy-z:pqr-s:1"); +} + /// Verifies eval string replacement builtins support direct and callable dispatch. #[test] fn test_eval_dispatches_string_replace_builtin_calls() { From 32a1b272bb9c89eb9bc2fd3eb97f479944b721bd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:17:29 +0200 Subject: [PATCH 0142/1208] Support eval str_pad builtin --- crates/elephc-eval/src/interpreter.rs | 127 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 16 ++++ 5 files changed, 151 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 38364f76f0..d6071e2e08 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1211,6 +1211,7 @@ fn eval_positional_expr_call( "str_replace" | "str_ireplace" => { eval_builtin_str_replace(name, args, context, scope, values) } + "str_pad" => eval_builtin_str_pad(args, context, scope, values), "str_split" => eval_builtin_str_split(args, context, scope, values), "strstr" => eval_builtin_strstr(args, context, scope, values), "substr" => eval_builtin_substr(args, context, scope, values), @@ -1553,6 +1554,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strpos" | "strrpos" | "strrev" + | "str_pad" | "str_split" | "strstr" | "substr" @@ -1681,6 +1683,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), "strstr" => Some(&["haystack", "needle", "before_needle"]), + "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), "str_repeat" => Some(&["string", "times"]), @@ -1981,6 +1984,20 @@ fn eval_builtin_with_values( }; eval_str_replace_result(name, *search, *replace, *subject, values)? } + "str_pad" => match evaluated_args { + [value, length] => eval_str_pad_result(*value, *length, None, None, values)?, + [value, length, pad_string] => { + eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? + } + [value, length, pad_string, pad_type] => eval_str_pad_result( + *value, + *length, + Some(*pad_string), + Some(*pad_type), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "str_split" => match evaluated_args { [value] => eval_str_split_result(*value, None, values)?, [value, length] => eval_str_split_result(*value, Some(*length), values)?, @@ -2790,6 +2807,95 @@ fn eval_find_replace_match( } } +/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. +fn eval_builtin_str_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_pad_result(value, length, None, None, values) + } + [value, length, pad_string] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), None, values) + } + [value, length, pad_string, pad_type] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + let pad_type = eval_expr(pad_type, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Pads one byte string to a PHP target length using cyclic pad bytes. +fn eval_str_pad_result( + value: RuntimeCellHandle, + length: RuntimeCellHandle, + pad_string: Option, + pad_type: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let target_length = eval_int_value(length, values)?; + let Ok(target_length) = usize::try_from(target_length) else { + return values.string_bytes_value(&bytes); + }; + if target_length <= bytes.len() { + return values.string_bytes_value(&bytes); + } + + let pad_string = match pad_string { + Some(pad_string) => values.string_bytes(pad_string)?, + None => b" ".to_vec(), + }; + if pad_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let pad_type = match pad_type { + Some(pad_type) => eval_int_value(pad_type, values)?, + None => 1, + }; + let (left_pad, right_pad) = + eval_str_pad_sides(target_length - bytes.len(), pad_type)?; + let capacity = bytes + .len() + .checked_add(left_pad) + .and_then(|size| size.checked_add(right_pad)) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + eval_append_repeated_pad(&mut output, &pad_string, left_pad); + output.extend_from_slice(&bytes); + eval_append_repeated_pad(&mut output, &pad_string, right_pad); + values.string_bytes_value(&output) +} + +/// Splits a `str_pad()` pad budget into left and right byte counts. +fn eval_str_pad_sides(pad_budget: usize, pad_type: i64) -> Result<(usize, usize), EvalStatus> { + match pad_type { + 0 => Ok((pad_budget, 0)), + 1 => Ok((0, pad_budget)), + 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends `count` bytes by cycling through the provided non-empty pad string. +fn eval_append_repeated_pad(output: &mut Vec, pad_string: &[u8], count: usize) { + for index in 0..count { + output.push(pad_string[index % pad_string.len()]); + } +} + /// Evaluates PHP `str_split(...)` over one string and optional chunk length. fn eval_builtin_str_split( args: &[EvalExpr], @@ -7060,6 +7166,27 @@ return function_exists("str_split");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `str_pad()` supports PHP left, right, and both-side padding modes. + #[test] + fn execute_program_dispatches_str_pad_builtin() { + let program = parse_fragment( + br#"echo "[" . str_pad("hi", 5) . "]"; echo ":"; +echo "[" . str_pad(string: "hi", length: 5, pad_string: "_", pad_type: 0) . "]"; echo ":"; +echo "[" . str_pad("x", 6, "ab", 2) . "]"; echo ":"; +echo call_user_func("str_pad", "42", 5, "0", 0); echo ":"; +echo call_user_func_array("str_pad", ["string" => "x", "length" => 3, "pad_string" => "."]); echo ":"; +return function_exists("str_pad");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "[hi ]:[___hi]:[abxaba]:00042:x..:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval string replacement builtins support direct, named, and callable dispatch. #[test] fn execute_program_dispatches_string_replace_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 03789832b8..3d183ca573 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -29,13 +29,15 @@ Eval `strstr()` uses the shared byte-search helper to return the matching suffix Eval `str_split()` builds indexed chunk arrays through the eval value hooks so later eval code can read the chunks normally. +Eval `str_pad()` computes left/right pad budgets in the interpreter and emits a boxed string through the eval value hooks. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `strstr()`, `str_split()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 59bb8d3299..687c748108 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -44,13 +44,15 @@ Eval `strstr()` returns matching suffixes, `before_needle` prefixes, or `false` Eval `str_split()` returns indexed arrays of fixed-width string chunks and supports the optional `length` parameter. +Eval `str_pad()` supports the PHP pad string and pad type arguments for left, right, and both-side padding. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `strstr()`, `str_split()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 38d95cf441..935583abfa 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -138,6 +138,7 @@ public function __construct(int $value) { $repeated = eval('return str_repeat("ha", 3);'); $substring = eval('return substr("abcdef", 2) . ":" . substr("abcdef", 1, -1);'); $substring_replaced = eval('return substr_replace("hello world", "PHP", 6, 5);'); +$padded = eval('return str_pad("hi", 5, ".");'); $linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); $split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); $string_chunks = eval('$chunks = str_split("eval", 2); return $chunks[0] . ":" . $chunks[1];'); @@ -228,6 +229,7 @@ public function __construct(int $value) { echo "str-repeat=" . $repeated . "\n"; echo "substr=" . $substring . "\n"; echo "substr-replace=" . $substring_replaced . "\n"; +echo "str-pad=" . $padded . "\n"; echo "nl2br-hex=" . $linebreaks . "\n"; echo "explode-implode=" . $split_joined . "\n"; echo "str-split=" . $string_chunks . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 575107b6fa..7ffb093d4c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1050,6 +1050,22 @@ echo ":"; echo function_exists("str_split");'); assert_eq!(out, "3:abc:ab-cd:0:xy-z:pqr-s:1"); } +/// Verifies eval `str_pad()` supports all PHP pad modes and callable dispatch. +#[test] +fn test_eval_dispatches_str_pad_builtin_call() { + let out = compile_and_run( + r#" "x", "length" => 3, "pad_string" => "."]); +echo ":"; echo function_exists("str_pad");'); +"#, + ); + assert_eq!(out, "[hi ]:[___hi]:[abxaba]:00042:x..:1"); +} + /// Verifies eval string replacement builtins support direct and callable dispatch. #[test] fn test_eval_dispatches_string_replace_builtin_calls() { From f85eeaac197d3199a93588121e539c10fe3871ba Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:22:05 +0200 Subject: [PATCH 0143/1208] Support eval wordwrap builtin --- crates/elephc-eval/src/interpreter.rs | 171 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 20 +++ 5 files changed, 199 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d6071e2e08..4b7fcd11c2 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1227,6 +1227,7 @@ fn eval_positional_expr_call( } "trim" => eval_builtin_trim_like(name, args, context, scope, values), "ucwords" => eval_builtin_ucwords(args, context, scope, values), + "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), } } @@ -1568,6 +1569,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "ucwords" | "urldecode" | "urlencode" + | "wordwrap" ) } @@ -1694,6 +1696,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { Some(&["string"]) } "ucwords" => Some(&["string", "separators"]), + "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), _ => None, } } @@ -2168,6 +2171,21 @@ fn eval_builtin_with_values( [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, _ => return Err(EvalStatus::RuntimeFatal), }, + "wordwrap" => match evaluated_args { + [value] => eval_wordwrap_result(*value, None, None, None, values)?, + [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, + [value, width, break_string] => { + eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values)? + } + [value, width, break_string, cut] => eval_wordwrap_result( + *value, + Some(*width), + Some(*break_string), + Some(*cut), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, _ => return Ok(None), }; Ok(Some(result)) @@ -4213,6 +4231,133 @@ fn eval_ucwords_result( values.string_bytes_value(&bytes) } +/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. +fn eval_builtin_wordwrap( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_wordwrap_result(value, None, None, None, values) + } + [value, width] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + eval_wordwrap_result(value, Some(width), None, None, values) + } + [value, width, break_string] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), None, values) + } + [value, width, break_string, cut] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + let cut = eval_expr(cut, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Wraps a byte string at PHP word boundaries and preserves existing newlines. +fn eval_wordwrap_result( + value: RuntimeCellHandle, + width: Option, + break_string: Option, + cut: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let width = match width { + Some(width) => eval_int_value(width, values)?, + None => 75, + }; + let break_string = match break_string { + Some(break_string) => values.string_bytes(break_string)?, + None => b"\n".to_vec(), + }; + if break_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let cut = match cut { + Some(cut) => values.truthy(cut)?, + None => false, + }; + if width == 0 && cut { + return Err(EvalStatus::RuntimeFatal); + } + if bytes.is_empty() { + return values.string_bytes_value(&bytes); + } + let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); + values.string_bytes_value(&output) +} + +/// Applies the core PHP word-wrap scan over already converted byte slices. +fn eval_wordwrap_bytes(bytes: &[u8], width: i64, break_string: &[u8], cut: bool) -> Vec { + if width < 0 && cut { + let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); + for byte in bytes { + output.extend_from_slice(break_string); + output.push(*byte); + } + return output; + } + + let width = width.max(0) as usize; + let mut output = Vec::with_capacity(bytes.len()); + let mut line_start = 0; + let mut last_space = None; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'\n' => { + output.extend_from_slice(&bytes[line_start..=index]); + index += 1; + line_start = index; + last_space = None; + } + b' ' => { + if index.saturating_sub(line_start) >= width { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + index += 1; + line_start = index; + last_space = None; + } else { + last_space = Some(index); + index += 1; + } + } + _ if index.saturating_sub(line_start) >= width => { + if let Some(space) = last_space { + output.extend_from_slice(&bytes[line_start..space]); + output.extend_from_slice(break_string); + line_start = space + 1; + last_space = None; + } else if cut && width > 0 { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + line_start = index; + } else { + index += 1; + } + } + _ => { + index += 1; + } + } + } + output.extend_from_slice(&bytes[line_start..]); + output +} + /// Evaluates nested `eval(...)` calls against the current materialized scope. fn eval_nested_eval( args: &[EvalExpr], @@ -7339,6 +7484,32 @@ return function_exists("ucwords");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. + #[test] + fn execute_program_dispatches_wordwrap_builtin() { + let program = parse_fragment( + br#"echo wordwrap("The quick brown fox", 10, "|"); echo ":"; +echo wordwrap(string: "A verylongword here", width: 8, break: "|"); echo ":"; +echo wordwrap("abcdefghij", 4, "|", true); echo ":"; +echo wordwrap("preserve\nnewlines here ok", 10, "|"); echo ":"; +echo call_user_func("wordwrap", "aaa bbb ccc", 3, "
"); echo ":"; +echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); +echo ":"; +return function_exists("wordwrap");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. #[test] fn execute_program_dispatches_str_contains_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3d183ca573..65ae04dfbe 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -31,13 +31,15 @@ Eval `str_split()` builds indexed chunk arrays through the eval value hooks so l Eval `str_pad()` computes left/right pad budgets in the interpreter and emits a boxed string through the eval value hooks. +Eval `wordwrap()` performs the same word-aware byte scan as the static helper, preserving existing newlines and honoring `cut_long_words`. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 687c748108..e74b41bf5b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -46,13 +46,15 @@ Eval `str_split()` returns indexed arrays of fixed-width string chunks and suppo Eval `str_pad()` supports the PHP pad string and pad type arguments for left, right, and both-side padding. +Eval `wordwrap()` supports width, break string, and `cut_long_words` arguments while preserving existing newlines. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 935583abfa..668daa6718 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -139,6 +139,7 @@ public function __construct(int $value) { $substring = eval('return substr("abcdef", 2) . ":" . substr("abcdef", 1, -1);'); $substring_replaced = eval('return substr_replace("hello world", "PHP", 6, 5);'); $padded = eval('return str_pad("hi", 5, ".");'); +$wrapped = eval('return wordwrap("hello dynamic world", 7, "|");'); $linebreaks = eval('return bin2hex(nl2br("a\nb", false));'); $split_joined = eval('$parts = explode(",", "red,green,blue"); return implode("|", $parts);'); $string_chunks = eval('$chunks = str_split("eval", 2); return $chunks[0] . ":" . $chunks[1];'); @@ -230,6 +231,7 @@ public function __construct(int $value) { echo "substr=" . $substring . "\n"; echo "substr-replace=" . $substring_replaced . "\n"; echo "str-pad=" . $padded . "\n"; +echo "wordwrap=" . $wrapped . "\n"; echo "nl2br-hex=" . $linebreaks . "\n"; echo "explode-implode=" . $split_joined . "\n"; echo "str-split=" . $string_chunks . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7ffb093d4c..e8d0891bcb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -915,6 +915,26 @@ echo ":"; echo function_exists("ucwords");'); assert_eq!(out, "Hello World:Hello-World:Hello\tWorld:A B:A-B:1"); } +/// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. +#[test] +fn test_eval_dispatches_wordwrap_builtin_call() { + let out = compile_and_run( + r#""); echo ":"; +echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); +echo ":"; echo function_exists("wordwrap");'); +"#, + ); + assert_eq!( + out, + "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:1" + ); +} + /// Verifies eval `strrev()` reverses byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_strrev_builtin_call() { From a2f089e6a51d928ec515095490b8d3fe8c5bb797 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:27:39 +0200 Subject: [PATCH 0144/1208] Support eval number_format builtin --- crates/elephc-eval/src/interpreter.rs | 199 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 20 +++ 5 files changed, 227 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 4b7fcd11c2..d3af7f7f5f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1194,6 +1194,7 @@ fn eval_positional_expr_call( "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), "nl2br" => eval_builtin_nl2br(args, context, scope, values), + "number_format" => eval_builtin_number_format(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), "pi" => eval_builtin_pi(args, values), "pow" => eval_builtin_pow(args, context, scope, values), @@ -1535,6 +1536,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "max" | "min" | "nl2br" + | "number_format" | "ord" | "pi" | "pow" @@ -1678,6 +1680,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "implode" => Some(&["separator", "array"]), "max" | "min" => Some(&["value"]), "nl2br" => Some(&["string", "use_xhtml"]), + "number_format" => Some(&["num", "decimals", "decimal_separator", "thousands_separator"]), "ord" => Some(&["character"]), "pi" => Some(&[]), "pow" => Some(&["num", "exponent"]), @@ -2080,6 +2083,27 @@ fn eval_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "number_format" => match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values)?, + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values)? + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + )?, + [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values)?, [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, @@ -2673,6 +2697,156 @@ fn eval_builtin_round( } } +/// Evaluates PHP `number_format(...)` over one number and optional separators. +fn eval_builtin_number_format( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_number_format_result(value, None, None, None, values) + } + [value, decimals] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + eval_number_format_result(value, Some(decimals), None, None, values) + } + [value, decimals, decimal_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + eval_number_format_result(value, Some(decimals), Some(decimal_separator), None, values) + } + [value, decimals, decimal_separator, thousands_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + let thousands_separator = eval_expr(thousands_separator, context, scope, values)?; + eval_number_format_result( + value, + Some(decimals), + Some(decimal_separator), + Some(thousands_separator), + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one PHP numeric value with grouped thousands and fixed decimals. +fn eval_number_format_result( + value: RuntimeCellHandle, + decimals: Option, + decimal_separator: Option, + thousands_separator: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let decimals = match decimals { + Some(decimals) => eval_int_value(decimals, values)?, + None => 0, + }; + let decimal_separator = match decimal_separator { + Some(separator) => values.string_bytes(separator)?, + None => b".".to_vec(), + }; + let thousands_separator = match thousands_separator { + Some(separator) => values.string_bytes(separator)?, + None => b",".to_vec(), + }; + let output = + eval_number_format_bytes(value, decimals, &decimal_separator, &thousands_separator)?; + values.string_bytes_value(&output) +} + +/// Converts one eval value to PHP float and returns the scalar payload. +fn eval_float_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_float(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Produces PHP `number_format()` bytes for finite scalar values. +fn eval_number_format_bytes( + value: f64, + decimals: i64, + decimal_separator: &[u8], + thousands_separator: &[u8], +) -> Result, EvalStatus> { + if !value.is_finite() { + return Ok(value.to_string().into_bytes()); + } + let decimals = decimals.clamp(-308, 308); + let display_decimals = decimals.max(0) as usize; + let abs_value = value.abs(); + let scaled = if decimals >= 0 { + let scale = 10_f64.powi(decimals as i32); + (abs_value * scale).round() + } else { + let scale = 10_f64.powi((-decimals) as i32); + (abs_value / scale).round() * scale + }; + if scaled > (u128::MAX as f64) { + return Err(EvalStatus::RuntimeFatal); + } + let scaled = scaled as u128; + let scale = 10_u128 + .checked_pow(display_decimals as u32) + .ok_or(EvalStatus::RuntimeFatal)?; + let integer = if display_decimals == 0 { + scaled + } else { + scaled / scale + }; + let fraction = if display_decimals == 0 { + 0 + } else { + scaled % scale + }; + + let mut output = Vec::new(); + if value.is_sign_negative() && scaled != 0 { + output.push(b'-'); + } + eval_append_grouped_decimal(&mut output, integer, thousands_separator); + if display_decimals > 0 { + output.extend_from_slice(decimal_separator); + let fraction = format!("{fraction:0display_decimals$}"); + output.extend_from_slice(fraction.as_bytes()); + } + Ok(output) +} + +/// Appends one unsigned decimal integer with optional three-digit grouping. +fn eval_append_grouped_decimal(output: &mut Vec, value: u128, separator: &[u8]) { + let digits = value.to_string(); + if separator.is_empty() { + output.extend_from_slice(digits.as_bytes()); + return; + } + let first_group = match digits.len() % 3 { + 0 => 3, + len => len, + }; + output.extend_from_slice(&digits.as_bytes()[..first_group]); + let mut index = first_group; + while index < digits.len() { + output.extend_from_slice(separator); + output.extend_from_slice(&digits.as_bytes()[index..index + 3]); + index += 3; + } +} + /// Evaluates PHP's `sqrt(...)` over one eval expression. fn eval_builtin_sqrt( args: &[EvalExpr], @@ -7861,6 +8035,31 @@ return function_exists("round");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `number_format()` groups and rounds numbers through callable paths. + #[test] + fn execute_program_dispatches_number_format_builtin() { + let program = parse_fragment( + br#"echo number_format(1234567); echo ":"; +echo number_format(1234.5678, 2); echo ":"; +echo number_format(num: 1234567.89, decimals: 2, decimal_separator: ",", thousands_separator: "."); echo ":"; +echo number_format(1234567.89, 2, ".", ""); echo ":"; +echo call_user_func("number_format", -1234.5, 1); echo ":"; +echo call_user_func_array("number_format", ["num" => 1234, "decimals" => 0, "decimal_separator" => ".", "thousands_separator" => " "]); echo ":"; +return function_exists("number_format");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `min()` and `max()` select numeric values directly and by callable. #[test] fn execute_program_dispatches_min_max_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 65ae04dfbe..c7c5fcbaca 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -33,13 +33,15 @@ Eval `str_pad()` computes left/right pad budgets in the interpreter and emits a Eval `wordwrap()` performs the same word-aware byte scan as the static helper, preserving existing newlines and honoring `cut_long_words`. +Eval `number_format()` formats finite numeric cells inside the interpreter, including grouping and custom separators. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index e74b41bf5b..2c6e6bc7ef 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -48,13 +48,15 @@ Eval `str_pad()` supports the PHP pad string and pad type arguments for left, ri Eval `wordwrap()` supports width, break string, and `cut_long_words` arguments while preserving existing newlines. +Eval `number_format()` supports decimal counts plus custom decimal and thousands separators. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 668daa6718..bf9704a90f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -112,6 +112,7 @@ public function __construct(int $value) { $rounding = eval('return floor(3.7) . ":" . ceil(3.2);'); $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); +$formatted_number = eval('return number_format(1234567.89, 2, ",", ".");'); $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5);'); $circle = eval('return round(pi(), 2);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); @@ -204,6 +205,7 @@ public function __construct(int $value) { echo "rounding=" . $rounding . "\n"; echo "builtin-power=" . $builtin_power . "\n"; echo "rounded=" . $rounded . "\n"; +echo "number-format=" . $formatted_number . "\n"; echo "minmax=" . $minmax . "\n"; echo "pi=" . $circle . "\n"; echo "case=" . $case . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e8d0891bcb..cf36d3d390 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1596,6 +1596,26 @@ echo ":"; echo function_exists("round");'); assert_eq!(out, "4:3.14:double:3:1.6:1"); } +/// Verifies eval `number_format()` groups and rounds numbers through callable paths. +#[test] +fn test_eval_dispatches_number_format_builtin_call() { + let out = compile_and_run( + r#" 1234, "decimals" => 0, "decimal_separator" => ".", "thousands_separator" => " "]); +echo ":"; echo function_exists("number_format");'); +"#, + ); + assert_eq!( + out, + "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:1" + ); +} + /// Verifies eval `min()` and `max()` select numeric values directly and through callables. #[test] fn test_eval_dispatches_min_max_builtin_calls() { From 66f0bdaaa1d57408c11c893975bef7bc27c522fb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:30:31 +0200 Subject: [PATCH 0145/1208] Support eval crc32 builtin --- crates/elephc-eval/src/interpreter.rs | 65 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++++ 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d3af7f7f5f..188a94e87e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1167,6 +1167,7 @@ fn eval_positional_expr_call( eval_builtin_cast(name, args, context, scope, values) } "count" => eval_builtin_count(args, context, scope, values), + "crc32" => eval_builtin_crc32(args, context, scope, values), "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { eval_builtin_ctype(name, args, context, scope, values) } @@ -1497,6 +1498,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "chop" | "chr" | "count" + | "crc32" | "ctype_alnum" | "ctype_alpha" | "ctype_digit" @@ -1669,6 +1671,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "chr" => Some(&["codepoint"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), + "crc32" => Some(&["string"]), "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), @@ -2048,6 +2051,12 @@ fn eval_builtin_with_values( let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len)? } + "crc32" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_crc32_result(*value, values)? + } "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3540,6 +3549,42 @@ fn eval_ctype_byte_matches(name: &str, byte: u8) -> Result { } } +/// Evaluates PHP `crc32(...)` over one eval string expression. +fn eval_builtin_crc32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_crc32_result(value, values) +} + +/// Computes PHP's non-negative CRC-32 integer over one converted byte string. +fn eval_crc32_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(eval_crc32_bytes(&bytes))) +} + +/// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. +fn eval_crc32_bytes(bytes: &[u8]) -> u32 { + let mut crc = 0xffff_ffff_u32; + for byte in bytes { + crc ^= u32::from(*byte); + for _ in 0..8 { + let mask = 0_u32.wrapping_sub(crc & 1); + crc = (crc >> 1) ^ (0xedb8_8320 & mask); + } + } + !crc +} + /// Casts one eval value to PHP int and returns the scalar payload. fn eval_int_value( value: RuntimeCellHandle, @@ -7609,6 +7654,26 @@ return function_exists("ctype_space");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. + #[test] + fn execute_program_dispatches_crc32_builtin() { + let program = parse_fragment( + br#"echo crc32(""); echo ":"; +echo crc32(string: "123456789"); echo ":"; +echo call_user_func("crc32", "hello"); echo ":"; +echo call_user_func_array("crc32", ["string" => "The quick brown fox jumps over the lazy dog"]); echo ":"; +return function_exists("crc32");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:3421780262:907060870:1095738169:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c7c5fcbaca..2f0526bdc2 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -35,13 +35,15 @@ Eval `wordwrap()` performs the same word-aware byte scan as the static helper, p Eval `number_format()` formats finite numeric cells inside the interpreter, including grouping and custom separators. +Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 2c6e6bc7ef..9abb685e28 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -50,13 +50,15 @@ Eval `wordwrap()` supports width, break string, and `cut_long_words` arguments w Eval `number_format()` supports decimal counts plus custom decimal and thousands separators. +Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index bf9704a90f..71d0808542 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -147,6 +147,7 @@ public function __construct(int $value) { $replaced = eval('return str_replace("green", "lime", "red green blue");'); $html_escaped = eval('return htmlspecialchars("bold");'); $url_codec = eval('return urlencode("a b&=") . ":" . rawurldecode("a%20b%26%3D");'); +$checksum = eval('return crc32("hello");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -240,6 +241,7 @@ public function __construct(int $value) { echo "str-replace=" . $replaced . "\n"; echo "htmlspecialchars=" . $html_escaped . "\n"; echo "url-codec=" . $url_codec . "\n"; +echo "crc32=" . $checksum . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cf36d3d390..ea97ff07e8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1172,6 +1172,21 @@ echo ":"; echo function_exists("ctype_space");'); ); } +/// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. +#[test] +fn test_eval_dispatches_crc32_builtin_call() { + let out = compile_and_run( + r#" "The quick brown fox jumps over the lazy dog"]); +echo ":"; echo function_exists("crc32");'); +"#, + ); + assert_eq!(out, "0:3421780262:907060870:1095738169:1"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 92b83f64b3add3a317b77d6383fea85cdbd6ce8e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:38:32 +0200 Subject: [PATCH 0146/1208] Support eval hash_algos builtin --- crates/elephc-eval/src/interpreter.rs | 93 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 6 +- docs/php/system-and-io.md | 6 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 18 ++++++ 5 files changed, 119 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 188a94e87e..6fd021ff87 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -35,6 +35,38 @@ struct EvaluatedCallArg { value: RuntimeCellHandle, } +/// Hash algorithm names supported by eval `hash_algos()`, matching native runtime order. +const EVAL_HASH_ALGOS: &[&str] = &[ + "md2", + "md4", + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha512/224", + "sha512/256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "ripemd128", + "ripemd160", + "ripemd256", + "ripemd320", + "whirlpool", + "crc32", + "crc32b", + "crc32c", + "adler32", + "fnv132", + "fnv1a32", + "fnv164", + "fnv1a64", + "joaat", +]; + /// Runtime value hooks required by the EvalIR interpreter. pub trait RuntimeValueOps { /// Creates a runtime indexed-array cell with room for at least `capacity` elements. @@ -1182,6 +1214,7 @@ fn eval_positional_expr_call( eval_builtin_function_probe(args, context, scope, values) } "gettype" => eval_builtin_gettype(args, context, scope, values), + "hash_algos" => eval_builtin_hash_algos(args, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { @@ -1512,6 +1545,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "fmod" | "function_exists" | "gettype" + | "hash_algos" | "hash_equals" | "hex2bin" | "html_entity_decode" @@ -1678,6 +1712,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "function_exists" => Some(&["function"]), + "hash_algos" => Some(&[]), "hash_equals" => Some(&["known_string", "user_string"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), @@ -2134,6 +2169,12 @@ fn eval_builtin_with_values( }; eval_gettype_result(*value, values)? } + "hash_algos" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values)? + } "hash_equals" => { let [known, user] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3572,6 +3613,31 @@ fn eval_crc32_result( values.int(i64::from(eval_crc32_bytes(&bytes))) } +/// Evaluates PHP `hash_algos()` with no arguments. +fn eval_builtin_hash_algos( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + +/// Builds the indexed array returned by eval `hash_algos()`. +fn eval_hash_algos_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(EVAL_HASH_ALGOS.len())?; + for (index, algo) in EVAL_HASH_ALGOS.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(algo)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. fn eval_crc32_bytes(bytes: &[u8]) -> u32 { let mut crc = 0xffff_ffff_u32; @@ -7674,6 +7740,33 @@ return function_exists("crc32");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `hash_algos()` returns supported hash names through callable dispatch too. + #[test] + fn execute_program_dispatches_hash_algos_builtin() { + let program = parse_fragment( + br#"$algos = hash_algos(); +echo count($algos) . ":" . $algos[0] . ":" . $algos[5] . ":"; +echo in_array("crc32c", $algos) ? "crc" : "bad"; +$call = call_user_func("hash_algos"); +echo ":" . $call[18]; +$spread = call_user_func_array("hash_algos", []); +echo ":" . $spread[27] . ":"; +echo function_exists("hash_algos") ? "exists" : "missing"; +return count($algos);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "28:md2:sha256:crc:whirlpool:joaat:exists" + ); + assert_eq!(values.get(result), FakeValue::Int(28)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2f0526bdc2..c17ff8a4a5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -35,7 +35,7 @@ Eval `wordwrap()` performs the same word-aware byte scan as the static helper, p Eval `number_format()` formats finite numeric cells inside the interpreter, including grouping and custom separators. -Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. +Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. @@ -43,7 +43,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 9abb685e28..8d663ed9be 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -50,7 +50,7 @@ Eval `wordwrap()` supports width, break string, and `cut_long_words` arguments w Eval `number_format()` supports decimal counts plus custom decimal and thousands separators. -Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. +Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. @@ -58,7 +58,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 71d0808542..d0e66de7c2 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -148,6 +148,7 @@ public function __construct(int $value) { $html_escaped = eval('return htmlspecialchars("bold");'); $url_codec = eval('return urlencode("a b&=") . ":" . rawurldecode("a%20b%26%3D");'); $checksum = eval('return crc32("hello");'); +$hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -242,6 +243,7 @@ public function __construct(int $value) { echo "htmlspecialchars=" . $html_escaped . "\n"; echo "url-codec=" . $url_codec . "\n"; echo "crc32=" . $checksum . "\n"; +echo "hash-algos=" . $hash_algos . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ea97ff07e8..b346754fdc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1187,6 +1187,24 @@ echo ":"; echo function_exists("crc32");'); assert_eq!(out, "0:3421780262:907060870:1095738169:1"); } +/// Verifies eval `hash_algos()` exposes the native supported hash algorithm list. +#[test] +fn test_eval_dispatches_hash_algos_builtin_call() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 07:43:36 +0200 Subject: [PATCH 0147/1208] Support eval zero-arg system builtins --- crates/elephc-eval/src/interpreter.rs | 165 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 27 +++++ 5 files changed, 200 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6fd021ff87..6e31132c27 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -67,6 +67,9 @@ const EVAL_HASH_ALGOS: &[&str] = &[ "joaat", ]; +/// Root package manifest used to mirror native `phpversion()` in the eval crate. +const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../Cargo.toml"); + /// Runtime value hooks required by the EvalIR interpreter. pub trait RuntimeValueOps { /// Creates a runtime indexed-array cell with room for at least `capacity` elements. @@ -1213,6 +1216,7 @@ fn eval_positional_expr_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) } + "getcwd" => eval_builtin_getcwd(args, values), "gettype" => eval_builtin_gettype(args, context, scope, values), "hash_algos" => eval_builtin_hash_algos(args, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), @@ -1231,6 +1235,7 @@ fn eval_positional_expr_call( "number_format" => eval_builtin_number_format(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), "pi" => eval_builtin_pi(args, values), + "phpversion" => eval_builtin_phpversion(args, values), "pow" => eval_builtin_pow(args, context, scope, values), "rawurldecode" | "urldecode" => { eval_builtin_url_decode(name, args, context, scope, values) @@ -1241,6 +1246,8 @@ fn eval_positional_expr_call( "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), + "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), + "time" => eval_builtin_time(args, values), "strrev" => eval_builtin_strrev(args, context, scope, values), "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), "str_replace" | "str_ireplace" => { @@ -1544,6 +1551,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "floatval" | "fmod" | "function_exists" + | "getcwd" | "gettype" | "hash_algos" | "hash_equals" @@ -1576,6 +1584,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "ord" | "pi" | "pow" + | "phpversion" | "rawurldecode" | "rawurlencode" | "rtrim" @@ -1601,6 +1610,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strtolower" | "strtoupper" | "strval" + | "sys_get_temp_dir" + | "time" | "trim" | "substr_replace" | "ucfirst" @@ -1712,6 +1723,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "function_exists" => Some(&["function"]), + "getcwd" => Some(&[]), "hash_algos" => Some(&[]), "hash_equals" => Some(&["known_string", "user_string"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), @@ -1721,6 +1733,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "number_format" => Some(&["num", "decimals", "decimal_separator", "thousands_separator"]), "ord" => Some(&["character"]), "pi" => Some(&[]), + "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), "round" => Some(&["num", "precision"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), @@ -1733,6 +1746,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "str_split" => Some(&["string", "length"]), "substr" => Some(&["string", "offset", "length"]), "substr_replace" => Some(&["string", "replace", "offset", "length"]), + "sys_get_temp_dir" | "time" => Some(&[]), "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } @@ -2163,6 +2177,12 @@ fn eval_builtin_with_values( values.bool_value(eval_function_probe_exists(context, &name))? } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "getcwd" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values)? + } "gettype" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2193,6 +2213,12 @@ fn eval_builtin_with_values( }; eval_html_entity_result(name, *value, values)? } + "phpversion" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values)? + } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { let [value] = evaluated_args else { @@ -2200,6 +2226,18 @@ fn eval_builtin_with_values( }; eval_type_predicate_result(name, *value, values)? } + "sys_get_temp_dir" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values)? + } + "time" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values)? + } "strlen" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3638,6 +3676,101 @@ fn eval_hash_algos_result( Ok(result) } +/// Evaluates PHP `time()` with no arguments. +fn eval_builtin_time( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) +} + +/// Returns the current Unix timestamp as a boxed PHP integer. +fn eval_time_result(values: &mut impl RuntimeValueOps) -> Result { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)? + .as_secs(); + let timestamp = i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(timestamp) +} + +/// Evaluates PHP `phpversion()` with no arguments. +fn eval_builtin_phpversion( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values) +} + +/// Returns the root elephc package version as a boxed PHP string. +fn eval_phpversion_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(eval_compiler_php_version()) +} + +/// Reads the root package version from the workspace manifest used by native `phpversion()`. +fn eval_compiler_php_version() -> &'static str { + let mut in_package = false; + for line in EVAL_ROOT_CARGO_TOML.lines() { + let line = line.trim(); + if line == "[package]" { + in_package = true; + continue; + } + if in_package && line.starts_with('[') { + break; + } + if in_package { + if let Some(value) = line.strip_prefix("version = ") { + return value.trim_matches('"'); + } + } + } + env!("CARGO_PKG_VERSION") +} + +/// Evaluates PHP `getcwd()` with no arguments. +fn eval_builtin_getcwd( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values) +} + +/// Returns the process current working directory as a boxed PHP string. +fn eval_getcwd_result(values: &mut impl RuntimeValueOps) -> Result { + let cwd = std::env::current_dir().map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(cwd.to_string_lossy().as_ref()) +} + +/// Evaluates PHP `sys_get_temp_dir()` with no arguments. +fn eval_builtin_sys_get_temp_dir( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values) +} + +/// Returns the same temporary directory literal as the native static builtin. +fn eval_sys_get_temp_dir_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string("/tmp") +} + /// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. fn eval_crc32_bytes(bytes: &[u8]) -> u32 { let mut crc = 0xffff_ffff_u32; @@ -7767,6 +7900,38 @@ return count($algos);"#, assert_eq!(values.get(result), FakeValue::Int(28)); } + /// Verifies eval zero-argument system builtins return native-compatible values. + #[test] + fn execute_program_dispatches_zero_arg_system_builtins() { + let program = parse_fragment( + br#"echo time() > 1000000000 ? "time" : "bad"; echo ":"; +echo phpversion(); echo ":"; +echo sys_get_temp_dir(); echo ":"; +echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; +echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; +echo call_user_func("phpversion"); echo ":"; +echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; +echo call_user_func_array("sys_get_temp_dir", []); echo ":"; +echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); +return function_exists("sys_get_temp_dir");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + format!( + "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", + eval_compiler_php_version(), + eval_compiler_php_version() + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c17ff8a4a5..07453cc987 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -37,6 +37,8 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. +Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 8d663ed9be..ba3c9c0015 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -52,6 +52,8 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. +Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index d0e66de7c2..c9995fc20a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -149,6 +149,7 @@ public function __construct(int $value) { $url_codec = eval('return urlencode("a b&=") . ":" . rawurldecode("a%20b%26%3D");'); $checksum = eval('return crc32("hello");'); $hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); +$system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -244,6 +245,7 @@ public function __construct(int $value) { echo "url-codec=" . $url_codec . "\n"; echo "crc32=" . $checksum . "\n"; echo "hash-algos=" . $hash_algos . "\n"; +echo "system-info=" . $system_info . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b346754fdc..ad8c1782f9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1205,6 +1205,33 @@ echo function_exists("hash_algos") ? "exists" : "missing";'); assert_eq!(out, "28:md2:sha256:crc:whirlpool:joaat:exists"); } +/// Verifies eval zero-argument system builtins match native runtime conventions. +#[test] +fn test_eval_dispatches_zero_arg_system_builtin_calls() { + let out = compile_and_run( + r#" 1000000000 ? "time" : "bad"; echo ":"; +echo phpversion(); echo ":"; +echo sys_get_temp_dir(); echo ":"; +echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; +echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; +echo call_user_func("phpversion"); echo ":"; +echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; +echo call_user_func_array("sys_get_temp_dir", []); echo ":"; +echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); +echo function_exists("sys_get_temp_dir");'); +"#, + ); + assert_eq!( + out, + format!( + "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:1111", + env!("CARGO_PKG_VERSION"), + env!("CARGO_PKG_VERSION") + ) + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 40f16c11d3000eb6ca799ae665f7c055b85ea3da Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:47:29 +0200 Subject: [PATCH 0148/1208] Support eval realpath cache builtins --- crates/elephc-eval/src/interpreter.rs | 75 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 17 ++++++ 5 files changed, 100 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6e31132c27..93d918694a 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1243,6 +1243,8 @@ fn eval_positional_expr_call( "rawurlencode" | "urlencode" => { eval_builtin_url_encode(name, args, context, scope, values) } + "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), + "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), @@ -1587,6 +1589,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "phpversion" | "rawurldecode" | "rawurlencode" + | "realpath_cache_get" + | "realpath_cache_size" | "rtrim" | "round" | "sqrt" @@ -1735,6 +1739,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "pi" => Some(&[]), "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), + "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), @@ -2219,6 +2224,18 @@ fn eval_builtin_with_values( } eval_phpversion_result(values)? } + "realpath_cache_get" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values)? + } + "realpath_cache_size" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values)? + } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { let [value] = evaluated_args else { @@ -3771,6 +3788,42 @@ fn eval_sys_get_temp_dir_result( values.string("/tmp") } +/// Evaluates PHP `realpath_cache_get()` with no arguments. +fn eval_builtin_realpath_cache_get( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values) +} + +/// Returns elephc's intentionally empty realpath-cache view. +fn eval_realpath_cache_get_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.array_new(0) +} + +/// Evaluates PHP `realpath_cache_size()` with no arguments. +fn eval_builtin_realpath_cache_size( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values) +} + +/// Returns zero because elephc does not maintain a runtime realpath cache. +fn eval_realpath_cache_size_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(0) +} + /// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. fn eval_crc32_bytes(bytes: &[u8]) -> u32 { let mut crc = 0xffff_ffff_u32; @@ -7932,6 +7985,28 @@ return function_exists("sys_get_temp_dir");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. + #[test] + fn execute_program_dispatches_realpath_cache_builtins() { + let program = parse_fragment( + br#"$cache = realpath_cache_get(); +echo count($cache) . ":" . realpath_cache_size() . ":"; +$call_cache = call_user_func("realpath_cache_get"); +echo count($call_cache) . ":"; +echo call_user_func_array("realpath_cache_size", []) . ":"; +echo function_exists("realpath_cache_get"); +return function_exists("realpath_cache_size");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:0:0:0:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 07453cc987..a7719a4daf 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -39,6 +39,8 @@ Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the gener Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. +Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns zero. + Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ba3c9c0015..36114c6f31 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -54,6 +54,8 @@ Eval `crc32()` computes the same non-negative CRC-32 integer as the static runti Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. +Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. + Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. diff --git a/examples/eval/main.php b/examples/eval/main.php index c9995fc20a..aafc442987 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -150,6 +150,7 @@ public function __construct(int $value) { $checksum = eval('return crc32("hello");'); $hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); $system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); +$realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -246,6 +247,7 @@ public function __construct(int $value) { echo "crc32=" . $checksum . "\n"; echo "hash-algos=" . $hash_algos . "\n"; echo "system-info=" . $system_info . "\n"; +echo "realpath-cache=" . $realpath_cache . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ad8c1782f9..b89ada2d32 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1232,6 +1232,23 @@ echo function_exists("sys_get_temp_dir");'); ); } +/// Verifies eval realpath-cache builtins expose elephc's empty-cache convention. +#[test] +fn test_eval_dispatches_realpath_cache_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 07:51:55 +0200 Subject: [PATCH 0149/1208] Support eval environment builtins --- crates/elephc-eval/src/interpreter.rs | 102 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 21 ++++++ 5 files changed, 131 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 93d918694a..7d97a1b525 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1217,6 +1217,7 @@ fn eval_positional_expr_call( eval_builtin_function_probe(args, context, scope, values) } "getcwd" => eval_builtin_getcwd(args, values), + "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), "hash_algos" => eval_builtin_hash_algos(args, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), @@ -1237,6 +1238,7 @@ fn eval_positional_expr_call( "pi" => eval_builtin_pi(args, values), "phpversion" => eval_builtin_phpversion(args, values), "pow" => eval_builtin_pow(args, context, scope, values), + "putenv" => eval_builtin_putenv(args, context, scope, values), "rawurldecode" | "urldecode" => { eval_builtin_url_decode(name, args, context, scope, values) } @@ -1554,6 +1556,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "fmod" | "function_exists" | "getcwd" + | "getenv" | "gettype" | "hash_algos" | "hash_equals" @@ -1587,6 +1590,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "pi" | "pow" | "phpversion" + | "putenv" | "rawurldecode" | "rawurlencode" | "realpath_cache_get" @@ -1728,6 +1732,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "fdiv" | "fmod" => Some(&["num1", "num2"]), "function_exists" => Some(&["function"]), "getcwd" => Some(&[]), + "getenv" => Some(&["name"]), "hash_algos" => Some(&[]), "hash_equals" => Some(&["known_string", "user_string"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), @@ -1739,6 +1744,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "pi" => Some(&[]), "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), + "putenv" => Some(&["assignment"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), @@ -2188,6 +2194,12 @@ fn eval_builtin_with_values( } eval_getcwd_result(values)? } + "getenv" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getenv_result(*name, values)? + } "gettype" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2224,6 +2236,12 @@ fn eval_builtin_with_values( } eval_phpversion_result(values)? } + "putenv" => { + let [assignment] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_putenv_result(*assignment, values)? + } "realpath_cache_get" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -3770,6 +3788,64 @@ fn eval_getcwd_result(values: &mut impl RuntimeValueOps) -> Result Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + eval_getenv_result(name, values) +} + +/// Reads one environment variable and returns an empty string when it is unset. +fn eval_getenv_result( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8_lossy(&name); + let value = std::env::var_os(name.as_ref()) + .map(|value| value.to_string_lossy().into_owned()) + .unwrap_or_default(); + values.string(&value) +} + +/// Evaluates PHP `putenv($assignment)` over one eval expression. +fn eval_builtin_putenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [assignment] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let assignment = eval_expr(assignment, context, scope, values)?; + eval_putenv_result(assignment, values) +} + +/// Applies one `putenv()` assignment to the host environment. +fn eval_putenv_result( + assignment: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let assignment = values.string_bytes(assignment)?; + if let Some(separator) = assignment.iter().position(|byte| *byte == b'=') { + let name = String::from_utf8_lossy(&assignment[..separator]); + let value = String::from_utf8_lossy(&assignment[separator + 1..]); + std::env::set_var(name.as_ref(), value.as_ref()); + } else { + let name = String::from_utf8_lossy(&assignment); + std::env::remove_var(name.as_ref()); + } + values.bool_value(true) +} + /// Evaluates PHP `sys_get_temp_dir()` with no arguments. fn eval_builtin_sys_get_temp_dir( args: &[EvalExpr], @@ -8007,6 +8083,32 @@ return function_exists("realpath_cache_size");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval environment builtins read, write, unset, and dispatch dynamically. + #[test] + fn execute_program_dispatches_environment_builtins() { + let program = parse_fragment( + br#"putenv("ELEPHC_EVAL_ENV_TEST=direct"); +echo getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv(assignment: "ELEPHC_EVAL_ENV_TEST=named"); +echo getenv(name: "ELEPHC_EVAL_ENV_TEST") . ":"; +echo call_user_func("getenv", "ELEPHC_EVAL_ENV_TEST") . ":"; +echo call_user_func_array("putenv", ["assignment" => "ELEPHC_EVAL_ENV_TEST=spread"]) ? "set" : "bad"; +echo ":" . getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv("ELEPHC_EVAL_ENV_TEST"); +echo getenv("ELEPHC_EVAL_ENV_TEST") === "" ? "empty" : "bad"; +echo ":"; echo function_exists("getenv"); +return function_exists("putenv");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a7719a4daf..25e4e72c6a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -39,6 +39,8 @@ Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the gener Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. +Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. + Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns zero. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 36114c6f31..dc8999c145 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -54,6 +54,8 @@ Eval `crc32()` computes the same non-negative CRC-32 integer as the static runti Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. +Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. + Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index aafc442987..49ea40d1d6 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -151,6 +151,7 @@ public function __construct(int $value) { $hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); $system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); +$environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -248,6 +249,7 @@ public function __construct(int $value) { echo "hash-algos=" . $hash_algos . "\n"; echo "system-info=" . $system_info . "\n"; echo "realpath-cache=" . $realpath_cache . "\n"; +echo "environment=" . $environment . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b89ada2d32..f93041713b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1249,6 +1249,27 @@ echo function_exists("realpath_cache_size");'); assert_eq!(out, "0:0:0:0:11"); } +/// Verifies eval environment builtins read, write, unset, and dispatch as callables. +#[test] +fn test_eval_dispatches_environment_builtin_calls() { + let out = compile_and_run( + r#" "ELEPHC_EVAL_ENV_TEST=spread"]) ? "set" : "bad"; +echo ":" . getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv("ELEPHC_EVAL_ENV_TEST"); +echo getenv("ELEPHC_EVAL_ENV_TEST") === "" ? "empty" : "bad"; +echo ":"; echo function_exists("getenv"); +echo function_exists("putenv");'); +"#, + ); + assert_eq!(out, "direct:named:named:set:spread:empty:11"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From bb9d2b62974530d2225ab9dd12f001f9127da1cc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:56:10 +0200 Subject: [PATCH 0150/1208] Support eval sleep builtins --- crates/elephc-eval/src/interpreter.rs | 91 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 18 ++++++ 5 files changed, 117 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7d97a1b525..ae6d102916 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1249,6 +1249,7 @@ fn eval_positional_expr_call( "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), "round" => eval_builtin_round(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), + "sleep" => eval_builtin_sleep(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "time" => eval_builtin_time(args, values), @@ -1273,6 +1274,7 @@ fn eval_positional_expr_call( } "trim" => eval_builtin_trim_like(name, args, context, scope, values), "ucwords" => eval_builtin_ucwords(args, context, scope, values), + "usleep" => eval_builtin_usleep(args, context, scope, values), "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), } @@ -1597,6 +1599,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "realpath_cache_size" | "rtrim" | "round" + | "sleep" | "sqrt" | "strcasecmp" | "str_contains" @@ -1626,6 +1629,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "ucwords" | "urldecode" | "urlencode" + | "usleep" | "wordwrap" ) } @@ -1747,6 +1751,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "putenv" => Some(&["assignment"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), + "sleep" => Some(&["seconds"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), "strstr" => Some(&["haystack", "needle", "before_needle"]), @@ -1762,6 +1767,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { Some(&["string"]) } "ucwords" => Some(&["string", "separators"]), + "usleep" => Some(&["microseconds"]), "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), _ => None, } @@ -2267,12 +2273,24 @@ fn eval_builtin_with_values( } eval_sys_get_temp_dir_result(values)? } + "sleep" => { + let [seconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sleep_result(*seconds, values)? + } "time" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } eval_time_result(values)? } + "usleep" => { + let [microseconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_usleep_result(*microseconds, values)? + } "strlen" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3732,6 +3750,56 @@ fn eval_time_result(values: &mut impl RuntimeValueOps) -> Result Result { + let [seconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let seconds = eval_expr(seconds, context, scope, values)?; + eval_sleep_result(seconds, values) +} + +/// Sleeps for a non-negative number of seconds and returns PHP's remaining-seconds value. +fn eval_sleep_result( + seconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let seconds = eval_int_value(seconds, values)?; + let seconds = u64::try_from(seconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_secs(seconds)); + values.int(0) +} + +/// Evaluates PHP `usleep($microseconds)` over one eval expression. +fn eval_builtin_usleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [microseconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let microseconds = eval_expr(microseconds, context, scope, values)?; + eval_usleep_result(microseconds, values) +} + +/// Sleeps for a non-negative number of microseconds and returns PHP null. +fn eval_usleep_result( + microseconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let microseconds = eval_int_value(microseconds, values)?; + let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_micros(microseconds)); + values.null() +} + /// Evaluates PHP `phpversion()` with no arguments. fn eval_builtin_phpversion( args: &[EvalExpr], @@ -8109,6 +8177,29 @@ return function_exists("putenv");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval sleep builtins dispatch without delaying focused tests. + #[test] + fn execute_program_dispatches_sleep_builtins() { + let program = parse_fragment( + br#"echo sleep(0) . ":"; +echo sleep(seconds: 0) . ":"; +usleep(0); +echo "u:"; +echo call_user_func("sleep", 0) . ":"; +echo call_user_func_array("usleep", ["microseconds" => 0]) === null ? "null" : "bad"; +echo ":"; echo function_exists("sleep"); +return function_exists("usleep");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:0:u:0:null:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 25e4e72c6a..9b7b76e96a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -39,6 +39,8 @@ Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the gener Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. +Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zero after an uninterrupted sleep; `usleep()` returns null. + Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns zero. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index dc8999c145..868e73bfca 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `phpversion()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -54,6 +54,8 @@ Eval `crc32()` computes the same non-negative CRC-32 integer as the static runti Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. +Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` when the sleep completes; `usleep()` returns `null`. + Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 49ea40d1d6..b6236aee44 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -152,6 +152,7 @@ public function __construct(int $value) { $system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); +$sleeping = eval('usleep(0); return sleep(0) . ":awake";'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -250,6 +251,7 @@ public function __construct(int $value) { echo "system-info=" . $system_info . "\n"; echo "realpath-cache=" . $realpath_cache . "\n"; echo "environment=" . $environment . "\n"; +echo "sleep=" . $sleeping . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f93041713b..1d4709f8f5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1270,6 +1270,24 @@ echo function_exists("putenv");'); assert_eq!(out, "direct:named:named:set:spread:empty:11"); } +/// Verifies eval sleep builtins dispatch through direct, named, and callable paths. +#[test] +fn test_eval_dispatches_sleep_builtin_calls() { + let out = compile_and_run( + r#" 0]) === null ? "null" : "bad"; +echo ":"; echo function_exists("sleep"); +echo function_exists("usleep");'); +"#, + ); + assert_eq!(out, "0:0:u:0:null:11"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From d7ec447c60a00832452d94b3876c6bae4ba34d7e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 07:59:57 +0200 Subject: [PATCH 0151/1208] Support eval microtime builtin --- crates/elephc-eval/src/interpreter.rs | 57 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ae6d102916..fc2eb6aed2 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1232,6 +1232,7 @@ fn eval_positional_expr_call( } "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), + "microtime" => eval_builtin_microtime(args, context, scope, values), "nl2br" => eval_builtin_nl2br(args, context, scope, values), "number_format" => eval_builtin_number_format(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), @@ -1585,6 +1586,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_string" | "lcfirst" | "max" + | "microtime" | "min" | "nl2br" | "number_format" @@ -1742,6 +1744,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), "max" | "min" => Some(&["value"]), + "microtime" => Some(&["as_float"]), "nl2br" => Some(&["string", "use_xhtml"]), "number_format" => Some(&["num", "decimals", "decimal_separator", "thousands_separator"]), "ord" => Some(&["character"]), @@ -2150,6 +2153,10 @@ fn eval_builtin_with_values( eval_implode_result(*separator, *array, values)? } "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, + "microtime" => match evaluated_args { + [] | [_] => eval_microtime_result(values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "nl2br" => match evaluated_args { [value] => eval_nl2br_result(*value, true, values)?, [value, use_xhtml] => { @@ -3750,6 +3757,35 @@ fn eval_time_result(values: &mut impl RuntimeValueOps) -> Result Result { + match args { + [] => eval_microtime_result(values), + [as_float] => { + let _ = eval_expr(as_float, context, scope, values)?; + eval_microtime_result(values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the current Unix timestamp with microsecond precision as a boxed float. +fn eval_microtime_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)?; + let seconds = timestamp.as_secs() as f64; + let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; + values.float(seconds + micros) +} + /// Evaluates PHP `sleep($seconds)` over one eval expression. fn eval_builtin_sleep( args: &[EvalExpr], @@ -8129,6 +8165,27 @@ return function_exists("sys_get_temp_dir");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. + #[test] + fn execute_program_dispatches_microtime_builtin() { + let program = parse_fragment( + br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; +echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; +echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; +echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; +echo ":"; +return function_exists("microtime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "now:named:call:array:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. #[test] fn execute_program_dispatches_realpath_cache_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9b7b76e96a..803cc8bca8 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -39,6 +39,8 @@ Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the gener Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. +Eval `microtime()` returns a boxed float Unix timestamp with microsecond precision, matching the static lowering convention. + Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zero after an uninterrupted sleep; `usleep()` returns null. Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 868e73bfca..2d12a28d4b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -54,6 +54,8 @@ Eval `crc32()` computes the same non-negative CRC-32 integer as the static runti Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. +Eval `microtime()` returns a float Unix timestamp with microsecond precision, matching elephc's static `microtime()` lowering. + Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` when the sleep completes; `usleep()` returns `null`. Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. diff --git a/examples/eval/main.php b/examples/eval/main.php index b6236aee44..ff32fac92a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -150,6 +150,7 @@ public function __construct(int $value) { $checksum = eval('return crc32("hello");'); $hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); $system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); +$micro_time = eval('return microtime(true) > 1000000000 ? "ok" : "bad";'); $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); $sleeping = eval('usleep(0); return sleep(0) . ":awake";'); @@ -249,6 +250,7 @@ public function __construct(int $value) { echo "crc32=" . $checksum . "\n"; echo "hash-algos=" . $hash_algos . "\n"; echo "system-info=" . $system_info . "\n"; +echo "microtime=" . $micro_time . "\n"; echo "realpath-cache=" . $realpath_cache . "\n"; echo "environment=" . $environment . "\n"; echo "sleep=" . $sleeping . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1d4709f8f5..391c5cfe1a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1232,6 +1232,21 @@ echo function_exists("sys_get_temp_dir");'); ); } +/// Verifies eval `microtime()` returns a plausible floating timestamp by all call paths. +#[test] +fn test_eval_dispatches_microtime_builtin_call() { + let out = compile_and_run( + r#" 1000000000 ? "now" : "bad"; echo ":"; +echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; +echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; +echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; +echo ":"; echo function_exists("microtime");'); +"#, + ); + assert_eq!(out, "now:named:call:array:1"); +} + /// Verifies eval realpath-cache builtins expose elephc's empty-cache convention. #[test] fn test_eval_dispatches_realpath_cache_builtin_calls() { From 695d8c737ab9b0fe1fefaffadfc2717e14d3174a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 08:03:42 +0200 Subject: [PATCH 0152/1208] Support eval gethostbyname builtin --- crates/elephc-eval/src/interpreter.rs | 68 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 ++++++ 5 files changed, 91 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index fc2eb6aed2..1aa5a1bdb0 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -20,6 +20,7 @@ use crate::eval_ir::{ use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; +use std::net::ToSocketAddrs; /// Internal statement-control result used to propagate eval returns and loops. enum EvalControl { @@ -1216,6 +1217,7 @@ fn eval_positional_expr_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) } + "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), @@ -1558,6 +1560,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "floatval" | "fmod" | "function_exists" + | "gethostbyname" | "getcwd" | "getenv" | "gettype" @@ -1737,6 +1740,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "function_exists" => Some(&["function"]), + "gethostbyname" => Some(&["hostname"]), "getcwd" => Some(&[]), "getenv" => Some(&["name"]), "hash_algos" => Some(&[]), @@ -2201,6 +2205,12 @@ fn eval_builtin_with_values( values.bool_value(eval_function_probe_exists(context, &name))? } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "gethostbyname" => { + let [hostname] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyname_result(*hostname, values)? + } "getcwd" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -3892,6 +3902,44 @@ fn eval_getcwd_result(values: &mut impl RuntimeValueOps) -> Result Result { + let [hostname] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hostname = eval_expr(hostname, context, scope, values)?; + eval_gethostbyname_result(hostname, values) +} + +/// Resolves one host name to an IPv4 string, or returns the original input on failure. +fn eval_gethostbyname_result( + hostname: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let hostname = values.string_bytes(hostname)?; + let hostname = String::from_utf8_lossy(&hostname); + if hostname.parse::().is_ok() { + return values.string(hostname.as_ref()); + } + let resolved = (hostname.as_ref(), 0_u16) + .to_socket_addrs() + .ok() + .and_then(|addrs| { + addrs + .filter_map(|addr| match addr.ip() { + std::net::IpAddr::V4(ip) => Some(ip.to_string()), + std::net::IpAddr::V6(_) => None, + }) + .next() + }); + values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) +} + /// Evaluates PHP `getenv($name)` over one eval expression. fn eval_builtin_getenv( args: &[EvalExpr], @@ -8257,6 +8305,26 @@ return function_exists("usleep");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. + #[test] + fn execute_program_dispatches_gethostbyname_builtin() { + let program = parse_fragment( + br#"echo gethostbyname("127.0.0.1") . ":"; +echo gethostbyname(hostname: "not a host") . ":"; +echo call_user_func("gethostbyname", "127.0.0.1") . ":"; +echo call_user_func_array("gethostbyname", ["hostname" => "not a host"]) . ":"; +return function_exists("gethostbyname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "127.0.0.1:not a host:127.0.0.1:not a host:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 803cc8bca8..e03aa2caf9 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,6 +45,8 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. +Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. + Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns zero. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 2d12a28d4b..c89e39a9ee 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,6 +60,8 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. +Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. + Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index ff32fac92a..b77d23a694 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -154,6 +154,7 @@ public function __construct(int $value) { $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); $sleeping = eval('usleep(0); return sleep(0) . ":awake";'); +$host_lookup = eval('return gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -254,6 +255,7 @@ public function __construct(int $value) { echo "realpath-cache=" . $realpath_cache . "\n"; echo "environment=" . $environment . "\n"; echo "sleep=" . $sleeping . "\n"; +echo "host-lookup=" . $host_lookup . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 391c5cfe1a..4a7e955fa5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1303,6 +1303,21 @@ echo function_exists("usleep");'); assert_eq!(out, "0:0:u:0:null:11"); } +/// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. +#[test] +fn test_eval_dispatches_gethostbyname_builtin_call() { + let out = compile_and_run( + r#" "not a host"]) . ":"; +echo function_exists("gethostbyname");'); +"#, + ); + assert_eq!(out, "127.0.0.1:not a host:127.0.0.1:not a host:1"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From ad82630a86aa23fb86ec90551dc9b807099f9ef5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 08:10:34 +0200 Subject: [PATCH 0153/1208] Support eval IP conversion builtins --- crates/elephc-eval/src/interpreter.rs | 213 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 26 ++++ 5 files changed, 247 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 1aa5a1bdb0..0def4fb6b7 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1228,10 +1228,13 @@ fn eval_positional_expr_call( eval_builtin_html_entity(name, args, context, scope, values) } "implode" => eval_builtin_implode(args, context, scope, values), + "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), + "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } + "ip2long" => eval_builtin_ip2long(args, context, scope, values), "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), @@ -1275,6 +1278,7 @@ fn eval_positional_expr_call( "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { eval_builtin_string_case(name, args, context, scope, values) } + "long2ip" => eval_builtin_long2ip(args, context, scope, values), "trim" => eval_builtin_trim_like(name, args, context, scope, values), "ucwords" => eval_builtin_ucwords(args, context, scope, values), "usleep" => eval_builtin_usleep(args, context, scope, values), @@ -1572,6 +1576,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "htmlspecialchars" | "implode" | "in_array" + | "inet_ntop" + | "inet_pton" + | "ip2long" | "intval" | "ltrim" | "is_callable" @@ -1588,6 +1595,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_resource" | "is_string" | "lcfirst" + | "long2ip" | "max" | "microtime" | "min" @@ -1747,6 +1755,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "hash_equals" => Some(&["known_string", "user_string"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), + "inet_ntop" => Some(&["ip"]), + "inet_pton" => Some(&["ip"]), + "ip2long" => Some(&["ip"]), "max" | "min" => Some(&["value"]), "microtime" => Some(&["as_float"]), "nl2br" => Some(&["string", "use_xhtml"]), @@ -1773,6 +1784,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } + "long2ip" => Some(&["ip"]), "ucwords" => Some(&["string", "separators"]), "usleep" => Some(&["microseconds"]), "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), @@ -2253,6 +2265,24 @@ fn eval_builtin_with_values( }; eval_html_entity_result(name, *value, values)? } + "inet_ntop" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_ntop_result(*value, values)? + } + "inet_pton" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_pton_result(*value, values)? + } + "ip2long" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ip2long_result(*value, values)? + } "phpversion" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -2348,6 +2378,12 @@ fn eval_builtin_with_values( }; eval_string_case_result(name, *value, values)? } + "long2ip" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_long2ip_result(*value, values)? + } "ucwords" => match evaluated_args { [value] => eval_ucwords_result(*value, None, values)?, [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, @@ -3940,6 +3976,151 @@ fn eval_gethostbyname_result( values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) } +/// Evaluates PHP `long2ip($ip)` over one eval expression. +fn eval_builtin_long2ip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_long2ip_result(ip, values) +} + +/// Formats one 32-bit IPv4 integer as a dotted-quad string. +fn eval_long2ip_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip = eval_int_value(ip, values)? as u32; + values.string(&eval_format_ipv4(ip)) +} + +/// Evaluates PHP `ip2long($ip)` over one eval expression. +fn eval_builtin_ip2long( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_ip2long_result(ip, values) +} + +/// Parses a dotted-quad IPv4 string into an integer or PHP false. +fn eval_ip2long_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + match eval_parse_ipv4(&bytes) { + Some(ip) => values.int(i64::from(ip)), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `inet_pton($ip)` over one eval expression. +fn eval_builtin_inet_pton( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_inet_pton_result(ip, values) +} + +/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. +fn eval_inet_pton_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + let Some(ip) = eval_parse_ipv4(&bytes) else { + return values.bool_value(false); + }; + values.string_bytes_value(&ip.to_be_bytes()) +} + +/// Evaluates PHP `inet_ntop($binary)` over one eval expression. +fn eval_builtin_inet_ntop( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [binary] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let binary = eval_expr(binary, context, scope, values)?; + eval_inet_ntop_result(binary, values) +} + +/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. +fn eval_inet_ntop_result( + binary: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(binary)?; + let [a, b, c, d] = bytes.as_slice() else { + return values.bool_value(false); + }; + let ip = u32::from_be_bytes([*a, *b, *c, *d]); + values.string(&eval_format_ipv4(ip)) +} + +/// Parses exactly four decimal IPv4 octets separated by dots. +fn eval_parse_ipv4(bytes: &[u8]) -> Option { + let mut octets = [0_u8; 4]; + let mut position = 0_usize; + let mut index = 0_usize; + + while index < 4 { + if position >= bytes.len() { + return None; + } + let start = position; + let mut value = 0_u16; + while position < bytes.len() && bytes[position].is_ascii_digit() { + value = value + .checked_mul(10)? + .checked_add(u16::from(bytes[position] - b'0'))?; + position += 1; + if position - start > 3 || value > 255 { + return None; + } + } + if position == start { + return None; + } + octets[index] = value as u8; + index += 1; + if index == 4 { + return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); + } + if bytes.get(position).copied() != Some(b'.') { + return None; + } + position += 1; + } + None +} + +/// Formats one packed IPv4 integer into dotted-quad text. +fn eval_format_ipv4(ip: u32) -> String { + let [a, b, c, d] = ip.to_be_bytes(); + format!("{}.{}.{}.{}", a, b, c, d) +} + /// Evaluates PHP `getenv($name)` over one eval expression. fn eval_builtin_getenv( args: &[EvalExpr], @@ -8325,6 +8506,38 @@ return function_exists("gethostbyname");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. + #[test] + fn execute_program_dispatches_ip_conversion_builtins() { + let program = parse_fragment( + br#"echo long2ip(3232235777) . ":"; +echo long2ip(ip: 4294967295) . ":"; +echo ip2long("192.168.1.1") . ":"; +echo ip2long(ip: "1.2.3") === false ? "bad-ip" : "bad"; echo ":"; +$packed = inet_pton("1.2.3.4"); +echo bin2hex($packed) . ":"; +echo inet_pton(ip: "nonsense") === false ? "bad-pton" : "bad"; echo ":"; +echo inet_ntop($packed) . ":"; +echo inet_ntop(ip: "xx") === false ? "bad-ntop" : "bad"; echo ":"; +echo call_user_func("long2ip", 2130706433) . ":"; +echo call_user_func_array("ip2long", ["ip" => "0.0.0.0"]) . ":"; +echo function_exists("long2ip"); echo function_exists("ip2long"); +echo function_exists("inet_pton"); +return function_exists("inet_ntop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e03aa2caf9..1308a7b8d1 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -47,6 +47,8 @@ Eval environment calls include `getenv()` and `putenv()`. Missing variables read Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. +Eval IPv4 conversion dispatch includes `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` for direct calls, named arguments, callable dispatch, and `function_exists()` probes. The interpreter currently handles IPv4 dotted-quad strings and four-byte packed addresses. + Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns zero. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c89e39a9ee..67572b2cfe 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -62,6 +62,8 @@ Eval environment calls include `getenv()` and `putenv()`. Missing variables read Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. +Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. + Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index b77d23a694..7abc42bc7f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -155,6 +155,7 @@ public function __construct(int $value) { $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); $sleeping = eval('usleep(0); return sleep(0) . ":awake";'); $host_lookup = eval('return gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host");'); +$ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -256,6 +257,7 @@ public function __construct(int $value) { echo "environment=" . $environment . "\n"; echo "sleep=" . $sleeping . "\n"; echo "host-lookup=" . $host_lookup . "\n"; +echo "ip-conversion=" . $ip_conversion . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4a7e955fa5..1aadb5bcf3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1318,6 +1318,32 @@ echo function_exists("gethostbyname");'); assert_eq!(out, "127.0.0.1:not a host:127.0.0.1:not a host:1"); } +/// Verifies eval IPv4 conversion builtins handle integer, string, and raw-byte forms. +#[test] +fn test_eval_dispatches_ip_conversion_builtin_calls() { + let out = compile_and_run( + r#" "0.0.0.0"]) . ":"; +echo function_exists("long2ip"); echo function_exists("ip2long"); +echo function_exists("inet_pton"); echo function_exists("inet_ntop");'); +"#, + ); + assert_eq!( + out, + "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:1111" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From d28157bd14fc2ec966ba59adbd315e3de93f6d44 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 08:16:53 +0200 Subject: [PATCH 0154/1208] Support eval path component builtins --- crates/elephc-eval/src/interpreter.rs | 172 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 21 ++++ 5 files changed, 201 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0def4fb6b7..e706b94319 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1192,6 +1192,7 @@ fn eval_positional_expr_call( "array_unique" => eval_builtin_array_unique(args, context, scope, values), "base64_encode" => eval_builtin_base64_encode(args, context, scope, values), "base64_decode" => eval_builtin_base64_decode(args, context, scope, values), + "basename" => eval_builtin_basename(args, context, scope, values), "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), "ceil" => eval_builtin_ceil(args, context, scope, values), "chr" => eval_builtin_chr(args, context, scope, values), @@ -1209,6 +1210,7 @@ fn eval_positional_expr_call( } "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), + "dirname" => eval_builtin_dirname(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), @@ -1540,6 +1542,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_sum" | "array_unique" | "array_values" + | "basename" | "base64_decode" | "base64_encode" | "bin2hex" @@ -1558,6 +1561,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "ctype_space" | "define" | "defined" + | "dirname" | "explode" | "fdiv" | "floor" @@ -1728,6 +1732,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), + "basename" => Some(&["path", "suffix"]), "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => { Some(&["string"]) @@ -1745,6 +1750,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), + "dirname" => Some(&["path", "levels"]), "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "function_exists" => Some(&["function"]), @@ -2202,6 +2208,16 @@ fn eval_builtin_with_values( )?, _ => return Err(EvalStatus::RuntimeFatal), }, + "basename" => match evaluated_args { + [path] => eval_basename_result(*path, None, values)?, + [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "dirname" => match evaluated_args { + [path] => eval_dirname_result(*path, None, values)?, + [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values)?, [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, @@ -3938,6 +3954,135 @@ fn eval_getcwd_result(values: &mut impl RuntimeValueOps) -> Result Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_basename_result(path, None, values) + } + [path, suffix] => { + let path = eval_expr(path, context, scope, values)?; + let suffix = eval_expr(suffix, context, scope, values)?; + eval_basename_result(path, Some(suffix), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `basename()` bytes and returns them as a runtime string. +fn eval_basename_result( + path: RuntimeCellHandle, + suffix: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let suffix = suffix + .map(|suffix| values.string_bytes(suffix)) + .transpose()?; + let result = eval_basename_bytes(&path, suffix.as_deref()); + values.string_bytes_value(&result) +} + +/// Extracts a PHP basename from one path byte string. +fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return Vec::new(); + } + let mut start = end; + while start > 0 && path[start - 1] != b'/' { + start -= 1; + } + let mut result = path[start..end].to_vec(); + if let Some(suffix) = suffix { + if !suffix.is_empty() && suffix.len() < result.len() && result.ends_with(suffix) { + result.truncate(result.len() - suffix.len()); + } + } + result +} + +/// Evaluates PHP `dirname($path, $levels = 1)` over one eval expression. +fn eval_builtin_dirname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_dirname_result(path, None, values) + } + [path, levels] => { + let path = eval_expr(path, context, scope, values)?; + let levels = eval_expr(levels, context, scope, values)?; + eval_dirname_result(path, Some(levels), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `dirname()` bytes and returns them as a runtime string. +fn eval_dirname_result( + path: RuntimeCellHandle, + levels: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let levels = match levels { + Some(levels) => eval_int_value(levels, values)?, + None => 1, + }; + if levels < 1 { + return Err(EvalStatus::RuntimeFatal); + } + let mut current = path; + for _ in 0..levels { + current = eval_dirname_once(¤t); + } + values.string_bytes_value(¤t) +} + +/// Applies one PHP `dirname()` parent traversal to a path byte string. +fn eval_dirname_once(path: &[u8]) -> Vec { + if path.is_empty() { + return b".".to_vec(); + } + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return b"/".to_vec(); + } + let mut cursor = end; + while cursor > 0 { + cursor -= 1; + if path[cursor] == b'/' { + let mut parent_end = cursor; + while parent_end > 0 && path[parent_end - 1] == b'/' { + parent_end -= 1; + } + return if parent_end == 0 { + b"/".to_vec() + } else { + path[..parent_end].to_vec() + }; + } + } + b".".to_vec() +} + /// Evaluates PHP `gethostbyname($hostname)` over one eval expression. fn eval_builtin_gethostbyname( args: &[EvalExpr], @@ -8538,6 +8683,33 @@ return function_exists("inet_ntop");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval path component builtins mirror static basename/dirname edge cases. + #[test] + fn execute_program_dispatches_path_component_builtins() { + let program = parse_fragment( + br#"echo basename("/var/log/syslog.log", ".log") . ":"; +echo basename(path: "/usr///") . ":"; +echo basename("/", "x") === "" ? "root" : "bad"; echo ":"; +echo dirname("/usr/local/bin/tool", 2) . ":"; +echo dirname(path: "/usr///local///bin") . ":"; +echo call_user_func("basename", "foo.tar.gz", ".bz2") . ":"; +echo call_user_func_array("dirname", ["path" => "/usr", "levels" => 3]) . ":"; +echo function_exists("basename"); +return function_exists("dirname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 1308a7b8d1..0bd7854987 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -49,6 +49,8 @@ Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver Eval IPv4 conversion dispatch includes `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` for direct calls, named arguments, callable dispatch, and `function_exists()` probes. The interpreter currently handles IPv4 dotted-quad strings and four-byte packed addresses. +Eval path component dispatch includes `basename()` and `dirname()` in the interpreter, including suffix trimming and repeated parent traversal without a filesystem lookup. + Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns zero. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 67572b2cfe..5dcf87c899 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -64,6 +64,8 @@ Eval `gethostbyname()` resolves IPv4 host names through the host resolver and re Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. +Eval path component calls include `basename()` and `dirname()` with suffix trimming, repeated parent traversal, named arguments, callable dispatch, and `function_exists()` probes. + Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 7abc42bc7f..490b6d515c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -156,6 +156,7 @@ public function __construct(int $value) { $sleeping = eval('usleep(0); return sleep(0) . ":awake";'); $host_lookup = eval('return gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host");'); $ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); +$path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -258,6 +259,7 @@ public function __construct(int $value) { echo "sleep=" . $sleeping . "\n"; echo "host-lookup=" . $host_lookup . "\n"; echo "ip-conversion=" . $ip_conversion . "\n"; +echo "path-components=" . $path_components . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1aadb5bcf3..67fd6bc034 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1344,6 +1344,27 @@ echo function_exists("inet_pton"); echo function_exists("inet_ntop");'); ); } +/// Verifies eval `basename()` and `dirname()` preserve static path edge-case behavior. +#[test] +fn test_eval_dispatches_path_component_builtin_calls() { + let out = compile_and_run( + r#" "/usr", "levels" => 3]) . ":"; +echo function_exists("basename"); echo function_exists("dirname");'); +"#, + ); + assert_eq!( + out, + "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:11" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 2c8596cc8ca3272344e3cf3079039ba868c9211a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 08:20:29 +0200 Subject: [PATCH 0155/1208] Support eval realpath builtin --- crates/elephc-eval/src/interpreter.rs | 58 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++++ 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index e706b94319..800075868e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1253,6 +1253,7 @@ fn eval_positional_expr_call( "rawurlencode" | "urlencode" => { eval_builtin_url_encode(name, args, context, scope, values) } + "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), "round" => eval_builtin_round(args, context, scope, values), @@ -1612,6 +1613,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "putenv" | "rawurldecode" | "rawurlencode" + | "realpath" | "realpath_cache_get" | "realpath_cache_size" | "rtrim" @@ -1773,6 +1775,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), "putenv" => Some(&["assignment"]), + "realpath" => Some(&["path"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), @@ -2311,6 +2314,12 @@ fn eval_builtin_with_values( }; eval_putenv_result(*assignment, values)? } + "realpath" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_realpath_result(*path, values)? + } "realpath_cache_get" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -4083,6 +4092,34 @@ fn eval_dirname_once(path: &[u8]) -> Vec { b".".to_vec() } +/// Evaluates PHP `realpath($path)` over one eval expression. +fn eval_builtin_realpath( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_realpath_result(path, values) +} + +/// Canonicalizes one path or returns PHP false when the path cannot be resolved. +fn eval_realpath_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let path = String::from_utf8_lossy(&path); + let Ok(canonical) = std::fs::canonicalize(path.as_ref()) else { + return values.bool_value(false); + }; + let canonical = canonical.to_string_lossy(); + values.string(canonical.as_ref()) +} + /// Evaluates PHP `gethostbyname($hostname)` over one eval expression. fn eval_builtin_gethostbyname( args: &[EvalExpr], @@ -8710,6 +8747,27 @@ return function_exists("dirname");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `realpath()` resolves existing paths and returns false for misses. + #[test] + fn execute_program_dispatches_realpath_builtin() { + let program = parse_fragment( + br#"echo realpath(".") !== false ? "resolved" : "bad"; echo ":"; +echo realpath(path: "elephc-eval-missing-path") === false ? "false" : "bad"; echo ":"; +echo call_user_func("realpath", ".") !== false ? "call" : "bad"; echo ":"; +echo call_user_func_array("realpath", ["path" => "elephc-eval-missing-path"]) === false ? "array-false" : "bad"; +echo ":"; +return function_exists("realpath");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "resolved:false:call:array-false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 0bd7854987..12893e8d7a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -51,6 +51,8 @@ Eval IPv4 conversion dispatch includes `long2ip()`, `ip2long()`, `inet_pton()`, Eval path component dispatch includes `basename()` and `dirname()` in the interpreter, including suffix trimming and repeated parent traversal without a filesystem lookup. +Eval `realpath()` uses the host filesystem to canonicalize existing paths and boxes PHP false for unresolved paths. + Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns zero. Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 5dcf87c899..d8bf3f2cd8 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -66,6 +66,8 @@ Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and Eval path component calls include `basename()` and `dirname()` with suffix trimming, repeated parent traversal, named arguments, callable dispatch, and `function_exists()` probes. +Eval `realpath()` canonicalizes existing paths through the host filesystem and returns `false` when the path cannot be resolved. + Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 490b6d515c..afb108c275 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -157,6 +157,7 @@ public function __construct(int $value) { $host_lookup = eval('return gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host");'); $ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); $path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); +$resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -260,6 +261,7 @@ public function __construct(int $value) { echo "host-lookup=" . $host_lookup . "\n"; echo "ip-conversion=" . $ip_conversion . "\n"; echo "path-components=" . $path_components . "\n"; +echo "realpath=" . $resolved_path . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 67fd6bc034..f971e73988 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1365,6 +1365,21 @@ echo function_exists("basename"); echo function_exists("dirname");'); ); } +/// Verifies eval `realpath()` returns strings for existing paths and false for missing paths. +#[test] +fn test_eval_dispatches_realpath_builtin_call() { + let out = compile_and_run( + r#" "elephc-eval-missing-path"]) === false ? "array-false" : "bad"; +echo ":"; echo function_exists("realpath");'); +"#, + ); + assert_eq!(out, "resolved:false:call:array-false:1"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 0f8bd4bc629e0491f476f7f9ca793609166a2dcc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 08:28:32 +0200 Subject: [PATCH 0156/1208] Support eval pathinfo builtin --- crates/elephc-eval/src/interpreter.rs | 191 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 26 ++++ 5 files changed, 221 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 800075868e..515c384c2b 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -333,6 +333,11 @@ const HEX2BIN_ODD_LENGTH_WARNING: &str = "Warning: hex2bin(): Hexadecimal input string must have an even length\n"; const HEX2BIN_INVALID_WARNING: &str = "Warning: hex2bin(): Input string must be hexadecimal string\n"; +const EVAL_PATHINFO_DIRNAME: i64 = 1; +const EVAL_PATHINFO_BASENAME: i64 = 2; +const EVAL_PATHINFO_EXTENSION: i64 = 4; +const EVAL_PATHINFO_FILENAME: i64 = 8; +const EVAL_PATHINFO_ALL: i64 = 15; /// Executes an EvalIR program and returns the eval result cell. pub fn execute_program( @@ -1243,6 +1248,7 @@ fn eval_positional_expr_call( "nl2br" => eval_builtin_nl2br(args, context, scope, values), "number_format" => eval_builtin_number_format(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), + "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "pi" => eval_builtin_pi(args, values), "phpversion" => eval_builtin_phpversion(args, values), "pow" => eval_builtin_pow(args, context, scope, values), @@ -1375,7 +1381,7 @@ fn eval_define_name( if name.is_empty() { return Err(EvalStatus::RuntimeFatal); } - if context.has_constant(&name) { + if eval_predefined_int_constant(&name).is_some() || context.has_constant(&name) { values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; return Ok(false); } @@ -1395,7 +1401,7 @@ fn eval_defined_name( values: &mut impl RuntimeValueOps, ) -> Result { let name = eval_constant_name(name, values)?; - Ok(context.has_constant(&name)) + Ok(eval_predefined_int_constant(&name).is_some() || context.has_constant(&name)) } /// Reads a PHP constant name from a runtime cell without changing case. @@ -1607,6 +1613,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "nl2br" | "number_format" | "ord" + | "pathinfo" | "pi" | "pow" | "phpversion" @@ -1771,6 +1778,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "nl2br" => Some(&["string", "use_xhtml"]), "number_format" => Some(&["num", "decimals", "decimal_separator", "thousands_separator"]), "ord" => Some(&["character"]), + "pathinfo" => Some(&["path", "flags"]), "pi" => Some(&[]), "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), @@ -2308,6 +2316,11 @@ fn eval_builtin_with_values( } eval_phpversion_result(values)? } + "pathinfo" => match evaluated_args { + [path] => eval_pathinfo_result(*path, None, values)?, + [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "putenv" => { let [assignment] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -4120,6 +4133,133 @@ fn eval_realpath_result( values.string(canonical.as_ref()) } +/// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. +fn eval_builtin_pathinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_pathinfo_result(path, None, values) + } + [path, flags] => { + let path = eval_expr(path, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_pathinfo_result(path, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `pathinfo()` as either an associative array or one component string. +fn eval_pathinfo_result( + path: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let Some(flags) = flags else { + return eval_pathinfo_array_result(&path, values); + }; + let flags = eval_int_value(flags, values)?; + if flags == EVAL_PATHINFO_ALL { + return eval_pathinfo_array_result(&path, values); + } + let component = eval_pathinfo_component_bytes(&path, flags); + values.string_bytes_value(&component) +} + +/// Builds the PHP `pathinfo()` associative-array result for all components. +fn eval_pathinfo_array_result( + path: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(4)?; + if !path.is_empty() { + let dirname = eval_pathinfo_dirname_bytes(path); + result = eval_pathinfo_array_set(result, "dirname", &dirname, values)?; + } + let parts = eval_pathinfo_parts(path); + result = eval_pathinfo_array_set(result, "basename", &parts.basename, values)?; + if parts.has_extension { + result = eval_pathinfo_array_set(result, "extension", &parts.extension, values)?; + } + eval_pathinfo_array_set(result, "filename", &parts.filename, values) +} + +/// Inserts one string component into a PHP `pathinfo()` associative result. +fn eval_pathinfo_array_set( + array: RuntimeCellHandle, + key: &str, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} + +/// Returns one PHP `pathinfo()` component for a non-all bitmask. +fn eval_pathinfo_component_bytes(path: &[u8], flags: i64) -> Vec { + if flags & EVAL_PATHINFO_DIRNAME != 0 { + return eval_pathinfo_dirname_bytes(path); + } + let parts = eval_pathinfo_parts(path); + if flags & EVAL_PATHINFO_BASENAME != 0 { + return parts.basename; + } + if flags & EVAL_PATHINFO_EXTENSION != 0 { + return parts.extension; + } + if flags & EVAL_PATHINFO_FILENAME != 0 { + return parts.filename; + } + Vec::new() +} + +/// Computes the dirname component with `pathinfo("")`'s empty-string exception. +fn eval_pathinfo_dirname_bytes(path: &[u8]) -> Vec { + if path.is_empty() { + Vec::new() + } else { + eval_dirname_once(path) + } +} + +/// Splits pathinfo basename, extension, and filename components. +fn eval_pathinfo_parts(path: &[u8]) -> EvalPathInfoParts { + let basename = eval_basename_bytes(path, None); + let Some(dot) = basename.iter().rposition(|byte| *byte == b'.') else { + return EvalPathInfoParts { + filename: basename.clone(), + basename, + extension: Vec::new(), + has_extension: false, + }; + }; + EvalPathInfoParts { + filename: basename[..dot].to_vec(), + extension: basename[dot + 1..].to_vec(), + basename, + has_extension: true, + } +} + +/// Pathinfo components derived from a basename. +struct EvalPathInfoParts { + /// Full basename component. + basename: Vec, + /// Extension component after the final dot, possibly empty for trailing-dot names. + extension: Vec, + /// Filename component before the final dot. + filename: Vec, + /// Whether the basename contained a dot and therefore has an extension key. + has_extension: bool, +} + /// Evaluates PHP `gethostbyname($hostname)` over one eval expression. fn eval_builtin_gethostbyname( args: &[EvalExpr], @@ -5948,12 +6088,27 @@ fn eval_const_fetch( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if let Some(value) = eval_predefined_int_constant(name) { + return values.int(value); + } let Some(value) = context.constant(name) else { return Err(EvalStatus::RuntimeFatal); }; values.retain(value) } +/// Returns eval-visible predefined integer constants that do not live in dynamic context. +fn eval_predefined_int_constant(name: &str) -> Option { + match name { + "PATHINFO_DIRNAME" => Some(EVAL_PATHINFO_DIRNAME), + "PATHINFO_BASENAME" => Some(EVAL_PATHINFO_BASENAME), + "PATHINFO_EXTENSION" => Some(EVAL_PATHINFO_EXTENSION), + "PATHINFO_FILENAME" => Some(EVAL_PATHINFO_FILENAME), + "PATHINFO_ALL" => Some(EVAL_PATHINFO_ALL), + _ => None, + } +} + /// Resolves one eval magic constant against fragment and dynamic-call metadata. fn eval_magic_const( magic: &EvalMagicConst, @@ -8768,6 +8923,38 @@ return function_exists("realpath");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. + #[test] + fn execute_program_dispatches_pathinfo_builtin() { + let program = parse_fragment( + br#"$info = pathinfo("/var/log/syslog.log"); +echo $info["dirname"] . "|" . $info["basename"] . "|" . $info["extension"] . "|" . $info["filename"] . ":"; +echo pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":"; +echo pathinfo(".bashrc", PATHINFO_FILENAME) === "" ? "dotfile" : "bad"; echo ":"; +echo pathinfo("file.", PATHINFO_EXTENSION) === "" ? "trail" : "bad"; echo ":"; +echo pathinfo("", PATHINFO_DIRNAME) === "" ? "empty-dir" : "bad"; echo ":"; +$plain = pathinfo("/etc/hosts"); +echo array_key_exists("extension", $plain) ? "bad" : "no-ext"; echo ":"; +echo pathinfo("/a/b.php", PATHINFO_BASENAME | PATHINFO_FILENAME) . ":"; +$call = call_user_func("pathinfo", "foo.txt", PATHINFO_ALL); +echo $call["basename"] . ":"; +echo call_user_func_array("pathinfo", ["path" => "foo.txt", "flags" => 0]) === "" ? "zero" : "bad"; +echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL"); +return PATHINFO_ALL;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 12893e8d7a..387bb7af28 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -49,7 +49,7 @@ Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver Eval IPv4 conversion dispatch includes `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` for direct calls, named arguments, callable dispatch, and `function_exists()` probes. The interpreter currently handles IPv4 dotted-quad strings and four-byte packed addresses. -Eval path component dispatch includes `basename()` and `dirname()` in the interpreter, including suffix trimming and repeated parent traversal without a filesystem lookup. +Eval path component dispatch includes `basename()`, `dirname()`, and `pathinfo()` in the interpreter, including suffix trimming, repeated parent traversal, `PATHINFO_*` constants, and pathinfo associative-array construction without a filesystem lookup. Eval `realpath()` uses the host filesystem to canonicalize existing paths and boxes PHP false for unresolved paths. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index d8bf3f2cd8..71b9d902a7 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -64,7 +64,7 @@ Eval `gethostbyname()` resolves IPv4 host names through the host resolver and re Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. -Eval path component calls include `basename()` and `dirname()` with suffix trimming, repeated parent traversal, named arguments, callable dispatch, and `function_exists()` probes. +Eval path component calls include `basename()`, `dirname()`, and `pathinfo()` with suffix trimming, repeated parent traversal, `PATHINFO_*` constants, named arguments, callable dispatch, and `function_exists()` probes. Eval `realpath()` canonicalizes existing paths through the host filesystem and returns `false` when the path cannot be resolved. diff --git a/examples/eval/main.php b/examples/eval/main.php index afb108c275..1a12c8e15d 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -158,6 +158,7 @@ public function __construct(int $value) { $ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); $path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); +$path_info = eval('$info = pathinfo("/var/log/syslog.log"); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION);'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -262,6 +263,7 @@ public function __construct(int $value) { echo "ip-conversion=" . $ip_conversion . "\n"; echo "path-components=" . $path_components . "\n"; echo "realpath=" . $resolved_path . "\n"; +echo "pathinfo=" . $path_info . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f971e73988..b5025f3dca 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1380,6 +1380,32 @@ echo ":"; echo function_exists("realpath");'); assert_eq!(out, "resolved:false:call:array-false:1"); } +/// Verifies eval `pathinfo()` supports arrays, component flags, constants, and callables. +#[test] +fn test_eval_dispatches_pathinfo_builtin_call() { + let out = compile_and_run( + r#" "foo.txt", "flags" => 0]) === "" ? "zero" : "bad"; +echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL");'); +"#, + ); + assert_eq!( + out, + "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 45514364547cdc0dfa176d4aca6b851c44ca0337 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 08:40:52 +0200 Subject: [PATCH 0157/1208] Support eval filesystem builtins --- crates/elephc-eval/src/interpreter.rs | 266 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 30 +++ 5 files changed, 304 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 515c384c2b..b2a3ccbf8b 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1220,6 +1220,10 @@ fn eval_positional_expr_call( "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), + "file_exists" => eval_builtin_file_probe(name, args, context, scope, values), + "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), + "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), + "filesize" => eval_builtin_filesize(args, context, scope, values), "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) @@ -1237,6 +1241,9 @@ fn eval_positional_expr_call( "implode" => eval_builtin_implode(args, context, scope, values), "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), + "is_dir" | "is_file" | "is_readable" | "is_writable" | "is_writeable" => { + eval_builtin_file_probe(name, args, context, scope, values) + } "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) @@ -1268,6 +1275,7 @@ fn eval_positional_expr_call( "sqrt" => eval_builtin_sqrt(args, context, scope, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "time" => eval_builtin_time(args, values), + "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), "str_replace" | "str_ireplace" => { @@ -1571,6 +1579,10 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "dirname" | "explode" | "fdiv" + | "file_exists" + | "file_get_contents" + | "file_put_contents" + | "filesize" | "floor" | "floatval" | "fmod" @@ -1590,6 +1602,11 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "inet_ntop" | "inet_pton" | "ip2long" + | "is_dir" + | "is_file" + | "is_readable" + | "is_writable" + | "is_writeable" | "intval" | "ltrim" | "is_callable" @@ -1653,6 +1670,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "substr_replace" | "ucfirst" | "ucwords" + | "unlink" | "urldecode" | "urlencode" | "usleep" @@ -1762,6 +1780,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "dirname" => Some(&["path", "levels"]), "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), + "file_get_contents" | "file_exists" | "filesize" | "is_dir" | "is_file" + | "is_readable" | "is_writable" | "is_writeable" | "unlink" => Some(&["filename"]), + "file_put_contents" => Some(&["filename", "data"]), "function_exists" => Some(&["function"]), "gethostbyname" => Some(&["hostname"]), "getcwd" => Some(&[]), @@ -2042,6 +2063,30 @@ fn eval_builtin_with_values( }; eval_float_binary_result(name, *left, *right, values)? } + "file_exists" | "is_dir" | "is_file" | "is_readable" | "is_writable" | "is_writeable" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_probe_result(name, *filename, values)? + } + "file_get_contents" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_get_contents_result(*filename, values)? + } + "file_put_contents" => { + let [filename, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_put_contents_result(*filename, *data, values)? + } + "filesize" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_filesize_result(*filename, values)? + } "pi" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -2376,6 +2421,12 @@ fn eval_builtin_with_values( }; eval_usleep_result(*microseconds, values)? } + "unlink" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unlink_result(*filename, values)? + } "strlen" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3976,6 +4027,181 @@ fn eval_getcwd_result(values: &mut impl RuntimeValueOps) -> Result Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_probe_result(name, filename, values) +} + +/// Computes one local filesystem predicate and returns a PHP boolean. +fn eval_file_probe_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let path = std::path::Path::new(&path); + let result = match name { + "file_exists" => path.exists(), + "is_dir" => path.is_dir(), + "is_file" => path.is_file(), + "is_readable" => eval_path_is_readable(path), + "is_writable" | "is_writeable" => eval_path_is_writable(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(result) +} + +/// Evaluates PHP `file_get_contents($filename)` over one eval expression. +fn eval_builtin_file_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_get_contents_result(filename, values) +} + +/// Reads a local file into a PHP string, or returns false when it cannot be opened. +fn eval_file_get_contents_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(bytes) => values.string_bytes_value(&bytes), + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} + +/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. +fn eval_builtin_file_put_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_file_put_contents_result(filename, data, values) +} + +/// Writes a PHP string to a local file and returns the written byte count or false. +fn eval_file_put_contents_result( + filename: RuntimeCellHandle, + data: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let data = values.string_bytes(data)?; + match std::fs::write(path, &data) { + Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), + Err(_) => values.bool_value(false), + } +} + +/// Evaluates PHP `filesize($filename)` over one eval expression. +fn eval_builtin_filesize( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filesize_result(filename, values) +} + +/// Returns one local file size in bytes, or zero when stat fails. +fn eval_filesize_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let len = std::fs::metadata(path).map(|metadata| metadata.len()).unwrap_or(0); + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `unlink($filename)` over one eval expression. +fn eval_builtin_unlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_unlink_result(filename, values) +} + +/// Deletes one local file and returns whether it succeeded. +fn eval_unlink_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + values.bool_value(std::fs::remove_file(path).is_ok()) +} + +/// Converts one eval value to a filesystem path string. +fn eval_path_string( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let filename = values.string_bytes(filename)?; + Ok(String::from_utf8_lossy(&filename).into_owned()) +} + +/// Returns whether a path can be opened for reading by the current process. +fn eval_path_is_readable(path: &std::path::Path) -> bool { + std::fs::File::open(path).is_ok() || std::fs::read_dir(path).is_ok() +} + +/// Returns whether a path can be written by the current process. +fn eval_path_is_writable(path: &std::path::Path) -> bool { + if path.is_file() { + return std::fs::OpenOptions::new().write(true).open(path).is_ok(); + } + if !path.is_dir() { + return false; + } + let probe = path.join(format!(".elephc_eval_writable_probe_{}", std::process::id())); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&probe) + { + Ok(_) => { + let _ = std::fs::remove_file(probe); + true + } + Err(_) => false, + } +} + /// Evaluates PHP `basename($path, $suffix = "")` over one eval expression. fn eval_builtin_basename( args: &[EvalExpr], @@ -8955,6 +9181,46 @@ return PATHINFO_ALL;"#, assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); } + /// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. + #[test] + fn execute_program_dispatches_filesystem_builtins() { + let filename = format!("elephc_eval_fs_probe_{}.txt", std::process::id()); + let missing = format!("elephc_eval_fs_missing_{}.txt", std::process::id()); + let source = format!( + r#"echo file_put_contents("{filename}", "hello") . ":"; +echo file_get_contents("{filename}") . ":"; +echo file_exists("{filename}") ? "exists" : "missing"; echo ":"; +echo is_file(filename: "{filename}") ? "file" : "bad"; echo ":"; +echo is_dir(".") ? "dir" : "bad"; echo ":"; +echo is_readable("{filename}") ? "readable" : "bad"; echo ":"; +echo is_writable("{filename}") ? "writable" : "bad"; echo ":"; +echo is_writeable("{filename}") ? "writeable" : "bad"; echo ":"; +echo filesize("{filename}") . ":"; +echo file_get_contents("{missing}") === false ? "missing-false" : "bad"; echo ":"; +echo call_user_func("file_exists", "{filename}") ? "call-exists" : "bad"; echo ":"; +echo call_user_func_array("filesize", ["filename" => "{filename}"]) . ":"; +echo unlink("{filename}") ? "unlinked" : "bad"; echo ":"; +echo file_exists("{filename}") ? "bad" : "gone"; echo ":"; +echo function_exists("file_get_contents"); echo function_exists("file_put_contents"); +echo function_exists("file_exists"); echo function_exists("is_file"); echo function_exists("is_dir"); +echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); +echo function_exists("filesize"); +return function_exists("unlink");"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + assert_eq!( + values.output, + "5:hello:exists:file:dir:readable:writable:writeable:5:missing-false:call-exists:5:unlinked:gone:111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 387bb7af28..320de3835b 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `unlink()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,6 +45,8 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. +Eval local filesystem dispatch includes `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, and `unlink()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, and network URLs remain in the native runtime path rather than the eval subset. + Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. Eval IPv4 conversion dispatch includes `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` for direct calls, named arguments, callable dispatch, and `function_exists()` probes. The interpreter currently handles IPv4 dotted-quad strings and four-byte packed addresses. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 71b9d902a7..3d3fe3d70a 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `unlink()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,6 +60,8 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. +Eval local filesystem calls include `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, and `unlink()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, and network URLs remain outside this eval filesystem subset. + Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 1a12c8e15d..be850bdd57 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -159,6 +159,7 @@ public function __construct(int $value) { $path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); $path_info = eval('$info = pathinfo("/var/log/syslog.log"); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION);'); +$filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -264,6 +265,7 @@ public function __construct(int $value) { echo "path-components=" . $path_components . "\n"; echo "realpath=" . $resolved_path . "\n"; echo "pathinfo=" . $path_info . "\n"; +echo "filesystem=" . $filesystem . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b5025f3dca..47673762af 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1406,6 +1406,36 @@ echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL");'); ); } +/// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. +#[test] +fn test_eval_dispatches_filesystem_builtin_calls() { + let out = compile_and_run( + r#" "eval-fs.txt"]) . ":"; +echo unlink("eval-fs.txt") ? "unlinked" : "bad"; echo ":"; +echo file_exists("eval-fs.txt") ? "bad" : "gone"; echo ":"; +echo function_exists("file_get_contents"); echo function_exists("file_put_contents"); +echo function_exists("file_exists"); echo function_exists("is_file"); echo function_exists("is_dir"); +echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); +echo function_exists("filesize"); echo function_exists("unlink");'); +"#, + ); + assert_eq!( + out, + "5:hello:exists:file:dir:readable:writable:writeable:5:call-exists:5:unlinked:gone:1111111111" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From f781d7e381102eb87a30c432d242edd5b74a95a7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 08:50:56 +0200 Subject: [PATCH 0158/1208] Support eval stat metadata builtins --- crates/elephc-eval/src/interpreter.rs | 192 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 36 +++++ 5 files changed, 228 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b2a3ccbf8b..62b37a7c2b 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -21,6 +21,7 @@ use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; use std::net::ToSocketAddrs; +use std::os::unix::fs::{FileTypeExt, MetadataExt}; /// Internal statement-control result used to propagate eval returns and loops. enum EvalControl { @@ -1221,9 +1222,12 @@ fn eval_positional_expr_call( "explode" => eval_builtin_explode(args, context, scope, values), "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), "file_exists" => eval_builtin_file_probe(name, args, context, scope, values), + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "filesize" => eval_builtin_filesize(args, context, scope, values), + "filetype" => eval_builtin_filetype(args, context, scope, values), "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) @@ -1241,9 +1245,8 @@ fn eval_positional_expr_call( "implode" => eval_builtin_implode(args, context, scope, values), "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), - "is_dir" | "is_file" | "is_readable" | "is_writable" | "is_writeable" => { - eval_builtin_file_probe(name, args, context, scope, values) - } + "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" + | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) @@ -1580,9 +1583,17 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "explode" | "fdiv" | "file_exists" + | "fileatime" + | "filectime" + | "filegroup" | "file_get_contents" + | "fileinode" + | "filemtime" + | "fileowner" + | "fileperms" | "file_put_contents" | "filesize" + | "filetype" | "floor" | "floatval" | "fmod" @@ -1603,7 +1614,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "inet_pton" | "ip2long" | "is_dir" + | "is_executable" | "is_file" + | "is_link" | "is_readable" | "is_writable" | "is_writeable" @@ -1780,8 +1793,10 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "dirname" => Some(&["path", "levels"]), "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), - "file_get_contents" | "file_exists" | "filesize" | "is_dir" | "is_file" - | "is_readable" | "is_writable" | "is_writeable" | "unlink" => Some(&["filename"]), + "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" + | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" + | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), "function_exists" => Some(&["function"]), "gethostbyname" => Some(&["hostname"]), @@ -2063,12 +2078,20 @@ fn eval_builtin_with_values( }; eval_float_binary_result(name, *left, *right, values)? } - "file_exists" | "is_dir" | "is_file" | "is_readable" | "is_writable" | "is_writeable" => { + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_file_probe_result(name, *filename, values)? } + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_stat_scalar_result(name, *filename, values)? + } "file_get_contents" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2087,6 +2110,12 @@ fn eval_builtin_with_values( }; eval_filesize_result(*filename, values)? } + "filetype" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_filetype_result(*filename, values)? + } "pi" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -4053,7 +4082,11 @@ fn eval_file_probe_result( let result = match name { "file_exists" => path.exists(), "is_dir" => path.is_dir(), + "is_executable" => eval_path_is_executable(path), "is_file" => path.is_file(), + "is_link" => std::fs::symlink_metadata(path) + .map(|metadata| metadata.file_type().is_symlink()) + .unwrap_or(false), "is_readable" => eval_path_is_readable(path), "is_writable" | "is_writeable" => eval_path_is_writable(path), _ => return Err(EvalStatus::RuntimeFatal), @@ -4061,6 +4094,47 @@ fn eval_file_probe_result( values.bool_value(result) } +/// Evaluates one scalar PHP stat metadata builtin over an eval expression. +fn eval_builtin_file_stat_scalar( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_stat_scalar_result(name, filename, values) +} + +/// Returns scalar stat metadata, using PHP false for failure where native elephc does. +fn eval_file_stat_scalar_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(_) if name == "filemtime" => return values.int(0), + Err(_) => return values.bool_value(false), + }; + match name { + "fileatime" => values.int(metadata.atime()), + "filectime" => values.int(metadata.ctime()), + "filegroup" => values.int(i64::from(metadata.gid())), + "fileinode" => { + values.int(i64::try_from(metadata.ino()).map_err(|_| EvalStatus::RuntimeFatal)?) + } + "filemtime" => values.int(metadata.mtime()), + "fileowner" => values.int(i64::from(metadata.uid())), + "fileperms" => values.int(i64::from(metadata.mode())), + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Evaluates PHP `file_get_contents($filename)` over one eval expression. fn eval_builtin_file_get_contents( args: &[EvalExpr], @@ -4143,6 +4217,50 @@ fn eval_filesize_result( values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) } +/// Evaluates PHP `filetype($filename)` over one eval expression. +fn eval_builtin_filetype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filetype_result(filename, values) +} + +/// Returns the PHP filetype string for one path, or false when lstat fails. +fn eval_filetype_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let file_type = match std::fs::symlink_metadata(path) { + Ok(metadata) => metadata.file_type(), + Err(_) => return values.bool_value(false), + }; + let label = if file_type.is_file() { + "file" + } else if file_type.is_dir() { + "dir" + } else if file_type.is_symlink() { + "link" + } else if file_type.is_char_device() { + "char" + } else if file_type.is_block_device() { + "block" + } else if file_type.is_fifo() { + "fifo" + } else if file_type.is_socket() { + "socket" + } else { + "unknown" + }; + values.string(label) +} + /// Evaluates PHP `unlink($filename)` over one eval expression. fn eval_builtin_unlink( args: &[EvalExpr], @@ -4180,6 +4298,13 @@ fn eval_path_is_readable(path: &std::path::Path) -> bool { std::fs::File::open(path).is_ok() || std::fs::read_dir(path).is_ok() } +/// Returns whether a path has any executable bit set in its Unix mode. +fn eval_path_is_executable(path: &std::path::Path) -> bool { + std::fs::metadata(path) + .map(|metadata| metadata.mode() & 0o111 != 0) + .unwrap_or(false) +} + /// Returns whether a path can be written by the current process. fn eval_path_is_writable(path: &std::path::Path) -> bool { if path.is_file() { @@ -9221,6 +9346,61 @@ return function_exists("unlink");"# assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval stat metadata builtins expose scalar file metadata and link probes. + #[test] + fn execute_program_dispatches_stat_metadata_builtins() { + let filename = format!("elephc_eval_stat_probe_{}.txt", std::process::id()); + let missing = format!("elephc_eval_stat_missing_{}.txt", std::process::id()); + let link = format!("elephc_eval_stat_link_{}.txt", std::process::id()); + let source = format!( + r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; +echo fileatime("{filename}") > 0 ? "atime" : "bad"; echo ":"; +echo filectime("{filename}") > 0 ? "ctime" : "bad"; echo ":"; +echo fileperms("{filename}") > 0 ? "perms" : "bad"; echo ":"; +echo fileowner("{filename}") >= 0 ? "owner" : "bad"; echo ":"; +echo filegroup("{filename}") >= 0 ? "group" : "bad"; echo ":"; +echo fileinode("{filename}") > 0 ? "inode" : "bad"; echo ":"; +echo filetype("{filename}") . ":"; +echo filetype(".") . ":"; +echo filetype("{link}") . ":"; +echo is_executable("{filename}") ? "bad" : "noexec"; echo ":"; +echo is_link("{link}") ? "link" : "bad"; echo ":"; +echo fileatime("{missing}") === false ? "missing-atime" : "bad"; echo ":"; +echo filectime("{missing}") === false ? "missing-ctime" : "bad"; echo ":"; +echo fileperms("{missing}") === false ? "missing-perms" : "bad"; echo ":"; +echo fileowner("{missing}") === false ? "missing-owner" : "bad"; echo ":"; +echo filegroup("{missing}") === false ? "missing-group" : "bad"; echo ":"; +echo fileinode("{missing}") === false ? "missing-inode" : "bad"; echo ":"; +echo filetype("{missing}") === false ? "missing-type" : "bad"; echo ":"; +echo filemtime("{missing}") === 0 ? "missing-mtime" : "bad"; echo ":"; +echo call_user_func("filetype", "{filename}") . ":"; +echo call_user_func_array("fileinode", ["filename" => "{filename}"]) > 0 ? "callinode" : "bad"; echo ":"; +echo function_exists("filemtime"); echo function_exists("fileatime"); +echo function_exists("filectime"); echo function_exists("fileperms"); +echo function_exists("fileowner"); echo function_exists("filegroup"); +echo function_exists("fileinode"); echo function_exists("filetype"); +echo function_exists("is_executable"); echo function_exists("is_link"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "mtime:atime:ctime:perms:owner:group:inode:file:dir:link:noexec:link:missing-atime:missing-ctime:missing-perms:missing-owner:missing-group:missing-inode:missing-type:missing-mtime:file:callinode:1111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 320de3835b..ed95dd8d91 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `unlink()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,7 +45,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. -Eval local filesystem dispatch includes `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, and `unlink()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, and network URLs remain in the native runtime path rather than the eval subset. +Eval local filesystem dispatch includes `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, and `unlink()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 3d3fe3d70a..6e84eb4456 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `unlink()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,7 +60,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. -Eval local filesystem calls include `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, and `unlink()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, and network URLs remain outside this eval filesystem subset. +Eval local filesystem calls include `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, and `unlink()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. diff --git a/examples/eval/main.php b/examples/eval/main.php index be850bdd57..59fd233238 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -160,6 +160,7 @@ public function __construct(int $value) { $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); $path_info = eval('$info = pathinfo("/var/log/syslog.log"); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION);'); $filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); +$file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -266,6 +267,7 @@ public function __construct(int $value) { echo "realpath=" . $resolved_path . "\n"; echo "pathinfo=" . $path_info . "\n"; echo "filesystem=" . $filesystem . "\n"; +echo "file-stats=" . $file_stats . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 47673762af..2419e43875 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1436,6 +1436,42 @@ echo function_exists("filesize"); echo function_exists("unlink");'); ); } +/// Verifies eval stat metadata builtins return scalar metadata and dispatch dynamically. +#[test] +fn test_eval_dispatches_stat_metadata_builtin_calls() { + let out = compile_and_run( + r#" 0 ? "mtime" : "bad"; echo ":"; +echo fileatime(filename: "eval-stat.txt") > 0 ? "atime" : "bad"; echo ":"; +echo filectime("eval-stat.txt") > 0 ? "ctime" : "bad"; echo ":"; +echo fileperms("eval-stat.txt") > 0 ? "perms" : "bad"; echo ":"; +echo fileowner("eval-stat.txt") >= 0 ? "owner" : "bad"; echo ":"; +echo filegroup("eval-stat.txt") >= 0 ? "group" : "bad"; echo ":"; +echo fileinode("eval-stat.txt") > 0 ? "inode" : "bad"; echo ":"; +echo filetype("eval-stat.txt") . ":"; +echo filetype(".") . ":"; +echo is_executable("/bin/sh") ? "exec" : "bad"; echo ":"; +echo is_link("eval-stat.txt") ? "bad" : "notlink"; echo ":"; +echo fileatime("missing-stat.txt") === false ? "missing-atime" : "bad"; echo ":"; +echo filetype("missing-stat.txt") === false ? "missing-type" : "bad"; echo ":"; +echo filemtime("missing-stat.txt") === 0 ? "missing-mtime" : "bad"; echo ":"; +echo call_user_func("filetype", "eval-stat.txt") . ":"; +echo call_user_func_array("fileinode", ["filename" => "eval-stat.txt"]) > 0 ? "callinode" : "bad"; echo ":"; +echo function_exists("filemtime"); echo function_exists("fileatime"); +echo function_exists("filectime"); echo function_exists("fileperms"); +echo function_exists("fileowner"); echo function_exists("filegroup"); +echo function_exists("fileinode"); echo function_exists("filetype"); +echo function_exists("is_executable"); echo function_exists("is_link"); +unlink("eval-stat.txt");'); +"#, + ); + assert_eq!( + out, + "mtime:atime:ctime:perms:owner:group:inode:file:dir:exec:notlink:missing-atime:missing-type:missing-mtime:file:callinode:1111111111" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From c9db42c890aeb0c9b02ae41ecfc8d764437ab727 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 08:56:43 +0200 Subject: [PATCH 0159/1208] Support eval path operation builtins --- crates/elephc-eval/src/interpreter.rs | 247 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 34 ++++ 5 files changed, 287 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 62b37a7c2b..ddedf940f8 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1201,7 +1201,11 @@ fn eval_positional_expr_call( "basename" => eval_builtin_basename(args, context, scope, values), "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), "ceil" => eval_builtin_ceil(args, context, scope, values), + "chdir" | "mkdir" | "rmdir" => { + eval_builtin_unary_path_bool(name, args, context, scope, values) + } "chr" => eval_builtin_chr(args, context, scope, values), + "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "class_exists" => eval_builtin_class_exists(args, context, scope, values), @@ -1210,6 +1214,9 @@ fn eval_positional_expr_call( eval_builtin_cast(name, args, context, scope, values) } "count" => eval_builtin_count(args, context, scope, values), + "copy" | "link" | "rename" | "symlink" => { + eval_builtin_binary_path_bool(name, args, context, scope, values) + } "crc32" => eval_builtin_crc32(args, context, scope, values), "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { eval_builtin_ctype(name, args, context, scope, values) @@ -1252,6 +1259,7 @@ fn eval_positional_expr_call( eval_builtin_type_predicate(name, args, context, scope, values) } "ip2long" => eval_builtin_ip2long(args, context, scope, values), + "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), @@ -1269,6 +1277,7 @@ fn eval_positional_expr_call( "rawurlencode" | "urlencode" => { eval_builtin_url_encode(name, args, context, scope, values) } + "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), @@ -1565,13 +1574,16 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "base64_encode" | "bin2hex" | "ceil" + | "chdir" | "call_user_func" | "call_user_func_array" | "class_exists" | "boolval" | "chop" | "chr" + | "clearstatcache" | "count" + | "copy" | "crc32" | "ctype_alnum" | "ctype_alpha" @@ -1621,6 +1633,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_writable" | "is_writeable" | "intval" + | "link" + | "linkinfo" | "ltrim" | "is_callable" | "is_array" @@ -1640,6 +1654,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "max" | "microtime" | "min" + | "mkdir" | "nl2br" | "number_format" | "ord" @@ -1650,11 +1665,14 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "putenv" | "rawurldecode" | "rawurlencode" + | "readlink" | "realpath" | "realpath_cache_get" | "realpath_cache_size" + | "rename" | "rtrim" | "round" + | "rmdir" | "sleep" | "sqrt" | "strcasecmp" @@ -1677,6 +1695,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strtolower" | "strtoupper" | "strval" + | "symlink" | "sys_get_temp_dir" | "time" | "trim" @@ -1783,9 +1802,12 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), + "chdir" | "mkdir" | "rmdir" => Some(&["directory"]), "chr" => Some(&["codepoint"]), + "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), + "copy" | "rename" => Some(&["from", "to"]), "crc32" => Some(&["string"]), "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), "define" => Some(&["constant_name", "value"]), @@ -1809,6 +1831,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), "ip2long" => Some(&["ip"]), + "link" | "symlink" => Some(&["target", "link"]), + "linkinfo" | "readlink" => Some(&["path"]), "max" | "min" => Some(&["value"]), "microtime" => Some(&["as_float"]), "nl2br" => Some(&["string", "use_xhtml"]), @@ -2066,6 +2090,24 @@ fn eval_builtin_with_values( }; eval_chr_result(*value, values)? } + "chdir" | "mkdir" | "rmdir" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unary_path_bool_result(name, *path, values)? + } + "clearstatcache" => { + if evaluated_args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + values.null()? + } + "copy" | "link" | "rename" | "symlink" => { + let [from, to] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_binary_path_bool_result(name, *from, *to, values)? + } "floor" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2116,6 +2158,12 @@ fn eval_builtin_with_values( }; eval_filetype_result(*filename, values)? } + "linkinfo" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_linkinfo_result(*path, values)? + } "pi" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -2450,6 +2498,12 @@ fn eval_builtin_with_values( }; eval_usleep_result(*microseconds, values)? } + "readlink" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_readlink_result(*path, values)? + } "unlink" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -4261,6 +4315,141 @@ fn eval_filetype_result( values.string(label) } +/// Evaluates a one-path filesystem operation that returns a PHP boolean. +fn eval_builtin_unary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_unary_path_bool_result(name, path, values) +} + +/// Executes a one-path local filesystem operation and returns whether it succeeded. +fn eval_unary_path_bool_result( + name: &str, + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let ok = match name { + "chdir" => std::env::set_current_dir(path).is_ok(), + "mkdir" => std::fs::create_dir(path).is_ok(), + "rmdir" => std::fs::remove_dir(path).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Evaluates a two-path filesystem operation that returns a PHP boolean. +fn eval_builtin_binary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [from, to] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let from = eval_expr(from, context, scope, values)?; + let to = eval_expr(to, context, scope, values)?; + eval_binary_path_bool_result(name, from, to, values) +} + +/// Executes a two-path local filesystem operation and returns whether it succeeded. +fn eval_binary_path_bool_result( + name: &str, + from: RuntimeCellHandle, + to: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_path_string(from, values)?; + let to = eval_path_string(to, values)?; + let ok = match name { + "copy" => std::fs::copy(from, to).is_ok(), + "link" => std::fs::hard_link(from, to).is_ok(), + "rename" => std::fs::rename(from, to).is_ok(), + "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Evaluates PHP `readlink($path)` over one eval expression. +fn eval_builtin_readlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_readlink_result(path, values) +} + +/// Reads one symbolic-link target string, or returns PHP false on failure. +fn eval_readlink_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + match std::fs::read_link(path) { + Ok(target) => values.string(target.to_string_lossy().as_ref()), + Err(_) => values.bool_value(false), + } +} + +/// Evaluates PHP `linkinfo($path)` over one eval expression. +fn eval_builtin_linkinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_linkinfo_result(path, values) +} + +/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. +fn eval_linkinfo_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let dev = match std::fs::symlink_metadata(path) { + Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, + Err(_) => -1, + }; + values.int(dev) +} + +/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. +fn eval_builtin_clearstatcache( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + values.null() +} + /// Evaluates PHP `unlink($filename)` over one eval expression. fn eval_builtin_unlink( args: &[EvalExpr], @@ -9401,6 +9590,64 @@ return true;"# assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval local path operation builtins mutate filesystem state. + #[test] + fn execute_program_dispatches_path_operation_builtins() { + let pid = std::process::id(); + let dir = format!("elephc_eval_ops_dir_{pid}"); + let call_dir = format!("elephc_eval_ops_call_dir_{pid}"); + let src = format!("elephc_eval_ops_src_{pid}.txt"); + let copy = format!("elephc_eval_ops_copy_{pid}.txt"); + let moved = format!("elephc_eval_ops_moved_{pid}.txt"); + let symlink = format!("elephc_eval_ops_symlink_{pid}.txt"); + let hardlink = format!("elephc_eval_ops_hardlink_{pid}.txt"); + let source = format!( + r#"file_put_contents("{src}", "hello"); +echo mkdir("{dir}") ? "mkdir" : "bad"; echo ":"; +echo is_dir("{dir}") ? "dir" : "bad"; echo ":"; +echo copy("{src}", "{copy}") && file_get_contents("{copy}") === "hello" ? "copy" : "bad"; echo ":"; +echo rename("{copy}", "{moved}") && file_exists("{moved}") && !file_exists("{copy}") ? "rename" : "bad"; echo ":"; +echo symlink("{src}", "{symlink}") ? "symlink" : "bad"; echo ":"; +echo readlink("{symlink}") === "{src}" ? "readlink" : "bad"; echo ":"; +echo linkinfo("{symlink}") >= 0 ? "linkinfo" : "bad"; echo ":"; +echo readlink("{src}") === false ? "readlink-false" : "bad"; echo ":"; +echo linkinfo("{missing}") === -1 ? "linkinfo-missing" : "bad"; echo ":"; +echo link("{src}", "{hardlink}") && file_get_contents("{hardlink}") === "hello" ? "hardlink" : "bad"; echo ":"; +echo clearstatcache() === null ? "cache" : "bad"; echo ":"; +echo unlink("{symlink}") && unlink("{hardlink}") && unlink("{moved}") && unlink("{src}") && rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo call_user_func("mkdir", "{call_dir}") ? "callmkdir" : "bad"; echo ":"; +echo call_user_func_array("rmdir", ["directory" => "{call_dir}"]) ? "callrmdir" : "bad"; echo ":"; +echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exists("copy"); +echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); +echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); +return true;"#, + missing = format!("elephc_eval_ops_missing_{pid}.txt"), + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + assert_eq!( + values.output, + "mkdir:dir:copy:rename:symlink:readlink:linkinfo:readlink-false:linkinfo-missing:hardlink:cache:cleanup:callmkdir:callrmdir:111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index ed95dd8d91..652608e963 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,7 +45,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. -Eval local filesystem dispatch includes `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, and `unlink()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. +Eval local filesystem dispatch includes `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, and `clearstatcache()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6e84eb4456..a43d8d8eeb 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,7 +60,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. -Eval local filesystem calls include `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, and `unlink()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. +Eval local filesystem calls include `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, and `clearstatcache()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. diff --git a/examples/eval/main.php b/examples/eval/main.php index 59fd233238..276ec2c03f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -161,6 +161,7 @@ public function __construct(int $value) { $path_info = eval('$info = pathinfo("/var/log/syslog.log"); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION);'); $filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); $file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); +$path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -268,6 +269,7 @@ public function __construct(int $value) { echo "pathinfo=" . $path_info . "\n"; echo "filesystem=" . $filesystem . "\n"; echo "file-stats=" . $file_stats . "\n"; +echo "path-ops=" . $path_ops . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2419e43875..0b99b514c4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1472,6 +1472,40 @@ unlink("eval-stat.txt");'); ); } +/// Verifies eval path operation builtins mutate local filesystem state. +#[test] +fn test_eval_dispatches_path_operation_builtin_calls() { + let out = compile_and_run( + r#"= 0 ? "linkinfo" : "bad"; echo ":"; +echo link("eval-op-src.txt", "eval-op-hard.txt") ? "hardlink" : "bad"; echo ":"; +echo readlink("eval-op-src.txt") === false ? "readlink-false" : "bad"; echo ":"; +echo linkinfo("eval-op-missing.txt") === -1 ? "linkinfo-missing" : "bad"; echo ":"; +echo chdir("eval-op-dir") ? "chdir" : "bad"; echo ":"; +echo getcwd() !== "" ? "cwd" : "bad"; echo ":"; +chdir(".."); +echo clearstatcache(true, "eval-op-src.txt") === null ? "cache" : "bad"; echo ":"; +echo unlink("eval-op-link.txt") && unlink("eval-op-hard.txt") && unlink("eval-op-moved.txt") && unlink("eval-op-src.txt") && rmdir("eval-op-dir") ? "cleanup" : "bad"; echo ":"; +echo call_user_func("mkdir", "eval-op-call-dir") ? "callmkdir" : "bad"; echo ":"; +echo call_user_func_array("rmdir", ["directory" => "eval-op-call-dir"]) ? "callrmdir" : "bad"; echo ":"; +echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exists("copy"); +echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); +echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); +'); +"#, + ); + assert_eq!( + out, + "mkdir:copy:rename:symlink:readlink:linkinfo:hardlink:readlink-false:linkinfo-missing:chdir:cwd:cache:cleanup:callmkdir:callrmdir:111111111" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From da1c9cda0f246af1ef1f1954488167fef4b69fcb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 09:10:49 +0200 Subject: [PATCH 0160/1208] Support eval file listing builtins --- crates/elephc-eval/src/interpreter.rs | 225 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 36 +++++ 5 files changed, 263 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ddedf940f8..b370df16ef 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1228,6 +1228,7 @@ fn eval_positional_expr_call( "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), + "file" => eval_builtin_file(args, context, scope, values), "file_exists" => eval_builtin_file_probe(name, args, context, scope, values), "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), @@ -1277,11 +1278,13 @@ fn eval_positional_expr_call( "rawurlencode" | "urlencode" => { eval_builtin_url_encode(name, args, context, scope, values) } + "readfile" => eval_builtin_readfile(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), "round" => eval_builtin_round(args, context, scope, values), + "scandir" => eval_builtin_scandir(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sleep" => eval_builtin_sleep(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), @@ -1594,6 +1597,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "dirname" | "explode" | "fdiv" + | "file" | "file_exists" | "fileatime" | "filectime" @@ -1665,6 +1669,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "putenv" | "rawurldecode" | "rawurlencode" + | "readfile" | "readlink" | "realpath" | "realpath_cache_get" @@ -1673,6 +1678,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "rtrim" | "round" | "rmdir" + | "scandir" | "sleep" | "sqrt" | "strcasecmp" @@ -1802,7 +1808,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), - "chdir" | "mkdir" | "rmdir" => Some(&["directory"]), + "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), "chr" => Some(&["codepoint"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), @@ -1815,10 +1821,10 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "dirname" => Some(&["path", "levels"]), "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), - "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" + "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" - | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" | "unlink" => Some(&["filename"]), + | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" + | "is_writeable" | "readfile" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), "function_exists" => Some(&["function"]), "gethostbyname" => Some(&["hostname"]), @@ -2120,6 +2126,12 @@ fn eval_builtin_with_values( }; eval_float_binary_result(name, *left, *right, values)? } + "file" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_result(*filename, values)? + } "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => { let [filename] = evaluated_args else { @@ -2164,6 +2176,12 @@ fn eval_builtin_with_values( }; eval_linkinfo_result(*path, values)? } + "readfile" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_readfile_result(*filename, values)? + } "pi" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -2193,6 +2211,12 @@ fn eval_builtin_with_values( [value, precision] => values.round(*value, Some(*precision))?, _ => return Err(EvalStatus::RuntimeFatal), }, + "scandir" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_scandir_result(*directory, values)? + } "sqrt" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -4218,6 +4242,92 @@ fn eval_file_get_contents_result( } } +/// Evaluates PHP `file($filename)` over one eval expression. +fn eval_builtin_file( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_result(filename, values) +} + +/// Reads one local file and returns an indexed array of line byte strings. +fn eval_file_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let bytes = match std::fs::read(path) { + Ok(bytes) => bytes, + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + }; + eval_file_lines_array(&bytes, values) +} + +/// Splits file payload bytes into runtime array entries, preserving trailing newlines. +fn eval_file_lines_array( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(0)?; + let mut line_start = 0; + let mut line_index = 0; + for (index, byte) in bytes.iter().enumerate() { + if *byte != b'\n' { + continue; + } + result = + eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; + line_start = index + 1; + line_index += 1; + } + if line_start < bytes.len() { + result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; + } + Ok(result) +} + +/// Evaluates PHP `readfile($filename)` over one eval expression. +fn eval_builtin_readfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_readfile_result(filename, values) +} + +/// Streams one local file to eval output and returns a byte count, false, or -1. +fn eval_readfile_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let path = std::path::Path::new(&path); + if path.is_dir() { + return values.int(-1); + } + let bytes = match std::fs::read(path) { + Ok(bytes) => bytes, + Err(_) => return values.bool_value(false), + }; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) +} + /// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. fn eval_builtin_file_put_contents( args: &[EvalExpr], @@ -4381,6 +4491,54 @@ fn eval_binary_path_bool_result( values.bool_value(ok) } +/// Evaluates PHP `scandir($directory)` over one eval expression. +fn eval_builtin_scandir( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_scandir_result(directory, values) +} + +/// Lists one local directory into an indexed string array, or an empty array on failure. +fn eval_scandir_result( + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(directory, values)?; + let Ok(entries) = std::fs::read_dir(path) else { + return values.array_new(0); + }; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; + } + Ok(result) +} + +/// Writes one byte-string value into an indexed runtime array at a zero-based position. +fn eval_array_set_indexed_bytes( + array: RuntimeCellHandle, + index: usize, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} + /// Evaluates PHP `readlink($path)` over one eval expression. fn eval_builtin_readlink( args: &[EvalExpr], @@ -9648,6 +9806,65 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. + #[test] + fn execute_program_dispatches_file_listing_builtins() { + let pid = std::process::id(); + let lines = format!("elephc_eval_listing_lines_{pid}.txt"); + let empty = format!("elephc_eval_listing_empty_{pid}.txt"); + let missing = format!("elephc_eval_listing_missing_{pid}.txt"); + let dir = format!("elephc_eval_listing_dir_{pid}"); + let source = format!( + r#"file_put_contents("{lines}", "one\ntwo"); +file_put_contents("{empty}", ""); +$lines = file("{lines}"); +echo count($lines) . ":"; +echo $lines[0] === "one\n" ? "line0" : "bad"; echo ":"; +echo $lines[1] === "two" ? "line1" : "bad"; echo ":"; +echo "["; +$bytes = readfile(filename: "{empty}"); +echo "]" . $bytes . ":"; +echo readfile("{missing}") === false ? "missing-readfile" : "bad"; echo ":"; +echo count(file("{missing}")) === 0 ? "missing-file" : "bad"; echo ":"; +mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.txt", "b"); +$scan = scandir(directory: "{dir}"); +echo count($scan) . ":"; +echo in_array(".", $scan) && in_array("..", $scan) && in_array("a.txt", $scan) && in_array("b.txt", $scan) ? "scan" : "bad"; echo ":"; +$call_lines = call_user_func("file", "{lines}"); +echo $call_lines[0] === "one\n" ? "callfile" : "bad"; echo ":"; +$call_scan = call_user_func_array("scandir", ["directory" => "{dir}"]); +echo count($call_scan) . ":"; +echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") && unlink("{lines}") && unlink("{empty}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "2:line0:line1:[]0:missing-readfile:missing-file:4:scan:callfile:4:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 652608e963..e7619b7925 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,7 +45,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. -Eval local filesystem dispatch includes `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, and `clearstatcache()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. +Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, and `scandir()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a43d8d8eeb..9a231e5dac 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,7 +60,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. -Eval local filesystem calls include `file_get_contents()`, `file_put_contents()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, and `clearstatcache()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. +Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, and `scandir()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. diff --git a/examples/eval/main.php b/examples/eval/main.php index 276ec2c03f..935dc5d114 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -162,6 +162,7 @@ public function __construct(int $value) { $filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); $file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); $path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); +$file_listing = eval('file_put_contents("eval-lines.txt", "one\ntwo"); file_put_contents("eval-empty.txt", ""); mkdir("eval-list-dir"); file_put_contents("eval-list-dir/a.txt", "a"); $lines = file("eval-lines.txt"); $scan = scandir("eval-list-dir"); $bytes = readfile("eval-empty.txt"); $ok = count($lines) === 2 && $lines[0] === "one\n" && $bytes === 0 && in_array("a.txt", $scan); unlink("eval-list-dir/a.txt"); rmdir("eval-list-dir"); unlink("eval-lines.txt"); unlink("eval-empty.txt"); return $ok ? "ok" : "bad";'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -270,6 +271,7 @@ public function __construct(int $value) { echo "filesystem=" . $filesystem . "\n"; echo "file-stats=" . $file_stats . "\n"; echo "path-ops=" . $path_ops . "\n"; +echo "file-listing=" . $file_listing . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0b99b514c4..3f5fbb0353 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1506,6 +1506,42 @@ echo function_exists("readlink"); echo function_exists("linkinfo"); echo functio ); } +/// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. +#[test] +fn test_eval_dispatches_file_listing_builtin_calls() { + let out = compile_and_run( + r#" "eval-list-dir"]); +echo count($call_scan) . ":"; +echo unlink("eval-list-dir/a.txt") && unlink("eval-list-dir/b.txt") && rmdir("eval-list-dir") && unlink("eval-lines.txt") && unlink("eval-empty.txt") ? "cleanup" : "bad"; echo ":"; +echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); +'); +"#, + ); + assert_eq!( + out, + "2:line0:line1:[]0:missing-readfile:4:scan:callfile:4:cleanup:111" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 12345f2cda4460eff73210e7daee10bff57d3c55 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 09:20:21 +0200 Subject: [PATCH 0161/1208] Support eval file modify builtins --- crates/elephc-eval/src/interpreter.rs | 207 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 34 +++++ 5 files changed, 246 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b370df16ef..2e24a199ab 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -21,7 +21,7 @@ use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; use std::net::ToSocketAddrs; -use std::os::unix::fs::{FileTypeExt, MetadataExt}; +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; /// Internal statement-control result used to propagate eval returns and loops. enum EvalControl { @@ -340,6 +340,11 @@ const EVAL_PATHINFO_EXTENSION: i64 = 4; const EVAL_PATHINFO_FILENAME: i64 = 8; const EVAL_PATHINFO_ALL: i64 = 15; +unsafe extern "C" { + /// Sets the process file-creation mask and returns the previous mask. + fn umask(mask: u32) -> u32; +} + /// Executes an EvalIR program and returns the eval result cell. pub fn execute_program( program: &EvalProgram, @@ -1204,6 +1209,7 @@ fn eval_positional_expr_call( "chdir" | "mkdir" | "rmdir" => { eval_builtin_unary_path_bool(name, args, context, scope, values) } + "chmod" => eval_builtin_chmod(args, context, scope, values), "chr" => eval_builtin_chr(args, context, scope, values), "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), @@ -1289,6 +1295,7 @@ fn eval_positional_expr_call( "sleep" => eval_builtin_sleep(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), + "tempnam" => eval_builtin_tempnam(args, context, scope, values), "time" => eval_builtin_time(args, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), @@ -1313,6 +1320,7 @@ fn eval_positional_expr_call( "long2ip" => eval_builtin_long2ip(args, context, scope, values), "trim" => eval_builtin_trim_like(name, args, context, scope, values), "ucwords" => eval_builtin_ucwords(args, context, scope, values), + "umask" => eval_builtin_umask(args, context, scope, values), "usleep" => eval_builtin_usleep(args, context, scope, values), "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), @@ -1578,6 +1586,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "bin2hex" | "ceil" | "chdir" + | "chmod" | "call_user_func" | "call_user_func_array" | "class_exists" @@ -1703,12 +1712,14 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "strval" | "symlink" | "sys_get_temp_dir" + | "tempnam" | "time" | "trim" | "substr_replace" | "ucfirst" | "ucwords" | "unlink" + | "umask" | "urldecode" | "urlencode" | "usleep" @@ -1809,6 +1820,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), + "chmod" => Some(&["filename", "permissions"]), "chr" => Some(&["codepoint"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), @@ -1864,11 +1876,13 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "substr" => Some(&["string", "offset", "length"]), "substr_replace" => Some(&["string", "replace", "offset", "length"]), "sys_get_temp_dir" | "time" => Some(&[]), + "tempnam" => Some(&["directory", "prefix"]), "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } "long2ip" => Some(&["ip"]), "ucwords" => Some(&["string", "separators"]), + "umask" => Some(&["mask"]), "usleep" => Some(&["microseconds"]), "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), _ => None, @@ -2102,6 +2116,12 @@ fn eval_builtin_with_values( }; eval_unary_path_bool_result(name, *path, values)? } + "chmod" => { + let [filename, permissions] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chmod_result(*filename, *permissions, values)? + } "clearstatcache" => { if evaluated_args.len() > 2 { return Err(EvalStatus::RuntimeFatal); @@ -2504,6 +2524,12 @@ fn eval_builtin_with_values( } eval_sys_get_temp_dir_result(values)? } + "tempnam" => { + let [directory, prefix] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_tempnam_result(*directory, *prefix, values)? + } "sleep" => { let [seconds] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2516,6 +2542,11 @@ fn eval_builtin_with_values( } eval_time_result(values)? } + "umask" => match evaluated_args { + [] => eval_umask_result(None, values)?, + [mask] => eval_umask_result(Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "usleep" => { let [microseconds] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -4491,6 +4522,33 @@ fn eval_binary_path_bool_result( values.bool_value(ok) } +/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. +fn eval_builtin_chmod( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, permissions] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let permissions = eval_expr(permissions, context, scope, values)?; + eval_chmod_result(filename, permissions, values) +} + +/// Changes one local file's mode and returns whether the operation succeeded. +fn eval_chmod_result( + filename: RuntimeCellHandle, + permissions: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let mode = eval_int_value(permissions, values)? as u32; + let permissions = std::fs::Permissions::from_mode(mode); + values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) +} + /// Evaluates PHP `scandir($directory)` over one eval expression. fn eval_builtin_scandir( args: &[EvalExpr], @@ -4539,6 +4597,91 @@ fn eval_array_set_indexed_bytes( values.array_set(array, key, value) } +/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. +fn eval_builtin_tempnam( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory, prefix] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + let prefix = eval_expr(prefix, context, scope, values)?; + eval_tempnam_result(directory, prefix, values) +} + +/// Creates a unique local temporary file and returns its path, or an empty string on failure. +fn eval_tempnam_result( + directory: RuntimeCellHandle, + prefix: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + let prefix = values.string_bytes(prefix)?; + let prefix = String::from_utf8_lossy(&prefix); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + for attempt in 0..1000_u32 { + let candidate = + std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&candidate) + { + Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(_) => return values.string(""), + } + } + values.string("") +} + +/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. +fn eval_tempnam_filename(prefix: &str, nonce: u128, attempt: u32) -> String { + format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) +} + +/// Evaluates PHP `umask($mask = null)` over an optional eval expression. +fn eval_builtin_umask( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_umask_result(None, values), + [mask] => { + let mask = eval_expr(mask, context, scope, values)?; + eval_umask_result(Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `umask()` semantics and returns the previous mask. +fn eval_umask_result( + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let previous = match mask { + Some(mask) => { + let mask = eval_int_value(mask, values)? as u32; + unsafe { umask(mask) } + } + None => unsafe { + let current = umask(0); + umask(current); + current + }, + }; + values.int(i64::from(previous)) +} + /// Evaluates PHP `readlink($path)` over one eval expression. fn eval_builtin_readlink( args: &[EvalExpr], @@ -9865,6 +10008,68 @@ return true;"# assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. + #[test] + fn execute_program_dispatches_file_modify_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_modify_{pid}.txt"); + let missing = format!("elephc_eval_modify_missing_{pid}.txt"); + let prefix = format!("evm{pid}_"); + let call_prefix = format!("evc{pid}_"); + let source = format!( + r#"file_put_contents("{filename}", "x"); +echo chmod(filename: "{filename}", permissions: 384) ? "chmod" : "bad"; echo ":"; +echo (fileperms("{filename}") & 511) === 384 ? "mode" : "bad"; echo ":"; +echo chmod("{missing}", 384) ? "bad" : "chmod-false"; echo ":"; +$tmp = tempnam(directory: ".", prefix: "{prefix}"); +echo file_exists($tmp) && str_starts_with(basename($tmp), "{prefix}") ? "tempnam" : "bad"; echo ":"; +unlink($tmp); +$previous = umask(mask: 18); +$set = umask($previous); +echo $set === 18 ? "umask" : "bad"; echo ":"; +$before = umask(18); +$probe = umask(); +$restore = umask($before); +echo $probe === 18 && $restore === 18 ? "probe" : "bad"; echo ":"; +echo call_user_func("chmod", "{filename}", 420) ? "callchmod" : "bad"; echo ":"; +$call_tmp = call_user_func_array("tempnam", ["directory" => ".", "prefix" => "{call_prefix}"]); +echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "{call_prefix}") ? "calltempnam" : "bad"; echo ":"; +unlink($call_tmp); +echo unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + assert_eq!( + values.output, + "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e7619b7925..ee35f4f343 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,7 +45,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. -Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, and `scandir()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. +Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, timestamp modification, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 9a231e5dac..3bd5d6cf6c 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,7 +60,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. -Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, and `scandir()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. +Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, timestamp modification, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. diff --git a/examples/eval/main.php b/examples/eval/main.php index 935dc5d114..b9a15b8a65 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -163,6 +163,7 @@ public function __construct(int $value) { $file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); $path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); $file_listing = eval('file_put_contents("eval-lines.txt", "one\ntwo"); file_put_contents("eval-empty.txt", ""); mkdir("eval-list-dir"); file_put_contents("eval-list-dir/a.txt", "a"); $lines = file("eval-lines.txt"); $scan = scandir("eval-list-dir"); $bytes = readfile("eval-empty.txt"); $ok = count($lines) === 2 && $lines[0] === "one\n" && $bytes === 0 && in_array("a.txt", $scan); unlink("eval-list-dir/a.txt"); rmdir("eval-list-dir"); unlink("eval-lines.txt"); unlink("eval-empty.txt"); return $ok ? "ok" : "bad";'); +$file_modify = eval('file_put_contents("eval-mod.txt", "x"); $tmp = tempnam(".", "evm"); $previous = umask(18); $probe = umask(); umask($previous); $ok = chmod("eval-mod.txt", 384) && file_exists($tmp) && $probe === 18; unlink($tmp); unlink("eval-mod.txt"); return $ok ? "ok" : "bad";'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); @@ -272,6 +273,7 @@ public function __construct(int $value) { echo "file-stats=" . $file_stats . "\n"; echo "path-ops=" . $path_ops . "\n"; echo "file-listing=" . $file_listing . "\n"; +echo "file-modify=" . $file_modify . "\n"; echo "bin2hex=" . $hexed . "\n"; echo "hex2bin=" . $unhexed . "\n"; echo "base64=" . $base64 . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3f5fbb0353..f5aa482884 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1542,6 +1542,40 @@ echo function_exists("file"); echo function_exists("readfile"); echo function_ex ); } +/// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. +#[test] +fn test_eval_dispatches_file_modify_builtin_calls() { + let out = compile_and_run( + r#" ".", "prefix" => "evc"]); +echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "evc") ? "calltempnam" : "bad"; echo ":"; +unlink($call_tmp); +echo unlink("eval-mod.txt") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); +'); +"#, + ); + assert_eq!( + out, + "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 2454015ac9aef1423a65289a74e29affc2de2324 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 09:28:02 +0200 Subject: [PATCH 0162/1208] Support eval touch builtin --- crates/elephc-eval/src/interpreter.rs | 139 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 25 +++++ 5 files changed, 169 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2e24a199ab..900c5c015f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1297,6 +1297,7 @@ fn eval_positional_expr_call( "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "tempnam" => eval_builtin_tempnam(args, context, scope, values), "time" => eval_builtin_time(args, values), + "touch" => eval_builtin_touch(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), @@ -1714,6 +1715,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "sys_get_temp_dir" | "tempnam" | "time" + | "touch" | "trim" | "substr_replace" | "ucfirst" @@ -1877,6 +1879,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "substr_replace" => Some(&["string", "replace", "offset", "length"]), "sys_get_temp_dir" | "time" => Some(&[]), "tempnam" => Some(&["directory", "prefix"]), + "touch" => Some(&["filename", "mtime", "atime"]), "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } @@ -2542,6 +2545,14 @@ fn eval_builtin_with_values( } eval_time_result(values)? } + "touch" => match evaluated_args { + [filename] => eval_touch_result(*filename, None, None, values)?, + [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, + [filename, mtime, atime] => { + eval_touch_result(*filename, Some(*mtime), Some(*atime), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "umask" => match evaluated_args { [] => eval_umask_result(None, values)?, [mask] => eval_umask_result(Some(*mask), values)?, @@ -4646,6 +4657,96 @@ fn eval_tempnam_filename(prefix: &str, nonce: u128, attempt: u32) -> String { format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) } +/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. +fn eval_builtin_touch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [filename] => { + let filename = eval_expr(filename, context, scope, values)?; + eval_touch_result(filename, None, None, values) + } + [filename, mtime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), None, values) + } + [filename, mtime, atime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + let atime = eval_expr(atime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), Some(atime), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates or stamps one local file and returns whether the operation succeeded. +fn eval_touch_result( + filename: RuntimeCellHandle, + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let (mtime, atime) = eval_touch_times(mtime, atime, values)?; + let file = match std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(path) + { + Ok(file) => file, + Err(_) => return values.bool_value(false), + }; + let times = std::fs::FileTimes::new() + .set_modified(mtime) + .set_accessed(atime); + values.bool_value(file.set_times(times).is_ok()) +} + +/// Resolves PHP touch timestamp defaults into concrete system times. +fn eval_touch_times( + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { + let now = std::time::SystemTime::now(); + let Some(mtime) = mtime else { + return Ok((now, now)); + }; + if values.is_null(mtime)? { + if let Some(atime) = atime { + if !values.is_null(atime)? { + return Err(EvalStatus::RuntimeFatal); + } + } + return Ok((now, now)); + } + let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + let Some(atime) = atime else { + return Ok((mtime, mtime)); + }; + if values.is_null(atime)? { + return Ok((mtime, mtime)); + } + let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + Ok((mtime, atime)) +} + +/// Converts a Unix timestamp in seconds into a `SystemTime`. +fn eval_system_time_from_unix(seconds: i64) -> Option { + if seconds >= 0 { + std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) + } else { + std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) + } +} + /// Evaluates PHP `umask($mask = null)` over an optional eval expression. fn eval_builtin_umask( args: &[EvalExpr], @@ -10070,6 +10171,44 @@ return true;"# assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. + #[test] + fn execute_program_dispatches_touch_builtin() { + let pid = std::process::id(); + let created = format!("elephc_eval_touch_created_{pid}.txt"); + let stamped = format!("elephc_eval_touch_stamped_{pid}.txt"); + let missing = format!("elephc_eval_touch_missing_{pid}/x.txt"); + let source = format!( + r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; +file_put_contents("{stamped}", "x"); +echo touch("{stamped}", 1000000000) ? "mtime" : "bad"; echo ":"; +echo filemtime("{stamped}") === 1000000000 ? "readmtime" : "bad"; echo ":"; +echo touch("{stamped}", 1000000001, null) && filemtime("{stamped}") === 1000000001 ? "nullatime" : "bad"; echo ":"; +echo touch("{stamped}", 1000000002, 1000000003) && filemtime("{stamped}") === 1000000002 ? "both" : "bad"; echo ":"; +echo touch("{missing}") ? "bad" : "touch-false"; echo ":"; +echo call_user_func("touch", "{created}", 1000000004) ? "calltouch" : "bad"; echo ":"; +echo call_user_func_array("touch", ["filename" => "{stamped}", "mtime" => 1000000005]) ? "callarray" : "bad"; echo ":"; +echo unlink("{created}") && unlink("{stamped}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("touch"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + assert_eq!( + values.output, + "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. #[test] fn execute_program_dispatches_string_case_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index ee35f4f343..638a72d1bd 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,7 +45,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. -Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, timestamp modification, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. +Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 3bd5d6cf6c..50a0f60b46 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,7 +60,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. -Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, timestamp modification, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. +Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. diff --git a/examples/eval/main.php b/examples/eval/main.php index b9a15b8a65..eb9e6df614 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -163,7 +163,7 @@ public function __construct(int $value) { $file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); $path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); $file_listing = eval('file_put_contents("eval-lines.txt", "one\ntwo"); file_put_contents("eval-empty.txt", ""); mkdir("eval-list-dir"); file_put_contents("eval-list-dir/a.txt", "a"); $lines = file("eval-lines.txt"); $scan = scandir("eval-list-dir"); $bytes = readfile("eval-empty.txt"); $ok = count($lines) === 2 && $lines[0] === "one\n" && $bytes === 0 && in_array("a.txt", $scan); unlink("eval-list-dir/a.txt"); rmdir("eval-list-dir"); unlink("eval-lines.txt"); unlink("eval-empty.txt"); return $ok ? "ok" : "bad";'); -$file_modify = eval('file_put_contents("eval-mod.txt", "x"); $tmp = tempnam(".", "evm"); $previous = umask(18); $probe = umask(); umask($previous); $ok = chmod("eval-mod.txt", 384) && file_exists($tmp) && $probe === 18; unlink($tmp); unlink("eval-mod.txt"); return $ok ? "ok" : "bad";'); +$file_modify = eval('touch("eval-touch.txt", 1000000000); file_put_contents("eval-mod.txt", "x"); $tmp = tempnam(".", "evm"); $previous = umask(18); $probe = umask(); umask($previous); $ok = filemtime("eval-touch.txt") === 1000000000 && chmod("eval-mod.txt", 384) && file_exists($tmp) && $probe === 18; unlink($tmp); unlink("eval-touch.txt"); unlink("eval-mod.txt"); return $ok ? "ok" : "bad";'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); $base64 = eval('return base64_encode("Hello");'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f5aa482884..fda5a494ef 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1576,6 +1576,31 @@ echo function_exists("chmod"); echo function_exists("tempnam"); echo function_ex ); } +/// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. +#[test] +fn test_eval_dispatches_touch_builtin_calls() { + let out = compile_and_run( + r#" "eval-touch-stamped.txt", "mtime" => 1000000005]) ? "callarray" : "bad"; echo ":"; +echo unlink("eval-touch-created.txt") && unlink("eval-touch-stamped.txt") ? "cleanup" : "bad"; echo ":"; +echo function_exists("touch"); +'); +"#, + ); + assert_eq!( + out, + "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" + ); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From 7d5718d0463b0e9ca2bdccbe9d0b78d173759284 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 09:36:26 +0200 Subject: [PATCH 0163/1208] Support eval stat array builtins --- crates/elephc-eval/src/interpreter.rs | 143 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 28 +++++ 5 files changed, 175 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 900c5c015f..a88383c954 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1242,6 +1242,7 @@ fn eval_positional_expr_call( "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "filesize" => eval_builtin_filesize(args, context, scope, values), "filetype" => eval_builtin_filetype(args, context, scope, values), + "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) @@ -1699,6 +1700,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "str_replace" | "str_starts_with" | "strcmp" + | "stat" | "strlen" | "strpos" | "strrpos" @@ -1726,6 +1728,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "urlencode" | "usleep" | "wordwrap" + | "lstat" ) } @@ -1838,7 +1841,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" - | "is_writeable" | "readfile" | "unlink" => Some(&["filename"]), + | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), "function_exists" => Some(&["function"]), "gethostbyname" => Some(&["hostname"]), @@ -2193,6 +2196,12 @@ fn eval_builtin_with_values( }; eval_filetype_result(*filename, values)? } + "stat" | "lstat" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stat_array_result(name, *filename, values)? + } "linkinfo" => { let [path] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -4467,6 +4476,97 @@ fn eval_filetype_result( values.string(label) } +/// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. +fn eval_builtin_stat_array( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_stat_array_result(name, filename, values) +} + +/// Builds PHP's stat array for one local path, or returns false on stat failure. +fn eval_stat_array_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let metadata = match name { + "stat" => std::fs::metadata(path), + "lstat" => std::fs::symlink_metadata(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let metadata = match metadata { + Ok(metadata) => metadata, + Err(_) => return values.bool_value(false), + }; + eval_stat_metadata_array(&metadata, values) +} + +/// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. +fn eval_stat_metadata_array( + metadata: &std::fs::Metadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let fields = [ + ("dev", eval_u64_to_i64(metadata.dev())?), + ("ino", eval_u64_to_i64(metadata.ino())?), + ("mode", i64::from(metadata.mode())), + ("nlink", eval_u64_to_i64(metadata.nlink())?), + ("uid", i64::from(metadata.uid())), + ("gid", i64::from(metadata.gid())), + ("rdev", eval_u64_to_i64(metadata.rdev())?), + ("size", eval_u64_to_i64(metadata.size())?), + ("atime", metadata.atime()), + ("mtime", metadata.mtime()), + ("ctime", metadata.ctime()), + ("blksize", eval_u64_to_i64(metadata.blksize())?), + ("blocks", eval_u64_to_i64(metadata.blocks())?), + ]; + let mut result = values.assoc_new(fields.len() * 2)?; + for (index, (name, value)) in fields.iter().enumerate() { + result = eval_stat_array_set_int_key(result, index, *value, values)?; + result = eval_stat_array_set_string_key(result, name, *value, values)?; + } + Ok(result) +} + +/// Inserts one integer stat field under a numeric PHP array key. +fn eval_stat_array_set_int_key( + array: RuntimeCellHandle, + key: usize, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(key).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts one integer stat field under a string PHP array key. +fn eval_stat_array_set_string_key( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Converts unsigned stat metadata into the signed integer payload used by PHP cells. +fn eval_u64_to_i64(value: u64) -> Result { + i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} + /// Evaluates a one-path filesystem operation that returns a PHP boolean. fn eval_builtin_unary_path_bool( name: &str, @@ -9992,6 +10092,47 @@ return true;"# assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. + #[test] + fn execute_program_dispatches_stat_array_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_stat_array_{pid}.txt"); + let link = format!("elephc_eval_lstat_array_{pid}.txt"); + let missing = format!("elephc_eval_stat_array_missing_{pid}.txt"); + let source = format!( + r#"$stat = stat("{filename}"); +$lstat = lstat("{link}"); +echo $stat["size"] === 5 && $stat[7] === $stat["size"] ? "stat" : "bad"; echo ":"; +echo ($stat["mode"] & 61440) === 32768 ? "mode" : "bad"; echo ":"; +echo ($lstat["mode"] & 61440) === 40960 ? "lstat" : "bad"; echo ":"; +echo stat("{missing}") === false && lstat("{missing}") === false ? "missing" : "bad"; echo ":"; +$call = call_user_func("stat", "{filename}"); +echo $call["mtime"] === filemtime("{filename}") ? "callstat" : "bad"; echo ":"; +$call_lstat = call_user_func_array("lstat", ["filename" => "{link}"]); +echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; +echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stat"); echo function_exists("lstat"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat array fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval local path operation builtins mutate filesystem state. #[test] fn execute_program_dispatches_path_operation_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 638a72d1bd..e6739fad2f 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,7 +45,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. -Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `stat()` / `lstat()` / `fstat()` array results remain in the native runtime path rather than the eval subset. +Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain in the native runtime path rather than the eval subset. Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 50a0f60b46..a9f94c32f7 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,7 +60,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. -Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `stat()` / `lstat()` / `fstat()` array results remain outside this eval filesystem subset. +Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain outside this eval filesystem subset. Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. diff --git a/examples/eval/main.php b/examples/eval/main.php index eb9e6df614..5638df59c9 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -160,7 +160,7 @@ public function __construct(int $value) { $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); $path_info = eval('$info = pathinfo("/var/log/syslog.log"); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION);'); $filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); -$file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); +$file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $info = stat("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0 && $info["size"] === 5 && $info[7] === $info["size"]; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); $path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); $file_listing = eval('file_put_contents("eval-lines.txt", "one\ntwo"); file_put_contents("eval-empty.txt", ""); mkdir("eval-list-dir"); file_put_contents("eval-list-dir/a.txt", "a"); $lines = file("eval-lines.txt"); $scan = scandir("eval-list-dir"); $bytes = readfile("eval-empty.txt"); $ok = count($lines) === 2 && $lines[0] === "one\n" && $bytes === 0 && in_array("a.txt", $scan); unlink("eval-list-dir/a.txt"); rmdir("eval-list-dir"); unlink("eval-lines.txt"); unlink("eval-empty.txt"); return $ok ? "ok" : "bad";'); $file_modify = eval('touch("eval-touch.txt", 1000000000); file_put_contents("eval-mod.txt", "x"); $tmp = tempnam(".", "evm"); $previous = umask(18); $probe = umask(); umask($previous); $ok = filemtime("eval-touch.txt") === 1000000000 && chmod("eval-mod.txt", 384) && file_exists($tmp) && $probe === 18; unlink($tmp); unlink("eval-touch.txt"); unlink("eval-mod.txt"); return $ok ? "ok" : "bad";'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fda5a494ef..ddf23445aa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1472,6 +1472,34 @@ unlink("eval-stat.txt");'); ); } +/// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. +#[test] +fn test_eval_dispatches_stat_array_builtin_calls() { + let out = compile_and_run( + r#" "eval-lstat-array.txt"]); +echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; +echo unlink("eval-lstat-array.txt") && unlink("eval-stat-array.txt") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stat"); echo function_exists("lstat"); +'); +"#, + ); + assert_eq!( + out, + "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" + ); +} + /// Verifies eval path operation builtins mutate local filesystem state. #[test] fn test_eval_dispatches_path_operation_builtin_calls() { From 6ae1f1f324edc3db1358354584e161dd11a7bdab Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 09:48:27 +0200 Subject: [PATCH 0164/1208] Support eval fnmatch builtin --- crates/elephc-eval/src/interpreter.rs | 346 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 25 ++ 5 files changed, 376 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index a88383c954..45f020c3fb 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -339,6 +339,10 @@ const EVAL_PATHINFO_BASENAME: i64 = 2; const EVAL_PATHINFO_EXTENSION: i64 = 4; const EVAL_PATHINFO_FILENAME: i64 = 8; const EVAL_PATHINFO_ALL: i64 = 15; +const EVAL_FNM_NOESCAPE: i64 = 1; +const EVAL_FNM_PATHNAME: i64 = 2; +const EVAL_FNM_PERIOD: i64 = 4; +const EVAL_FNM_CASEFOLD: i64 = 16; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -1242,6 +1246,7 @@ fn eval_positional_expr_call( "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "filesize" => eval_builtin_filesize(args, context, scope, values), "filetype" => eval_builtin_filetype(args, context, scope, values), + "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { @@ -1621,6 +1626,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "file_put_contents" | "filesize" | "filetype" + | "fnmatch" | "floor" | "floatval" | "fmod" @@ -1838,6 +1844,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "dirname" => Some(&["path", "levels"]), "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), + "fnmatch" => Some(&["pattern", "filename", "flags"]), "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" @@ -2196,6 +2203,13 @@ fn eval_builtin_with_values( }; eval_filetype_result(*filename, values)? } + "fnmatch" => match evaluated_args { + [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, + [pattern, filename, flags] => { + eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "stat" | "lstat" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -5302,6 +5316,303 @@ struct EvalPathInfoParts { has_extension: bool, } +/// Evaluates PHP `fnmatch($pattern, $filename, $flags = 0)` over eval expressions. +fn eval_builtin_fnmatch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, filename] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let filename = eval_expr(filename, context, scope, values)?; + eval_fnmatch_result(pattern, filename, None, values) + } + [pattern, filename, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let filename = eval_expr(filename, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_fnmatch_result(pattern, filename, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Runs PHP-style shell glob matching for one pattern/name pair. +fn eval_fnmatch_result( + pattern: RuntimeCellHandle, + filename: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let filename = values.string_bytes(filename)?; + let flags = match flags { + Some(flags) => eval_int_value(flags, values)?, + None => 0, + }; + values.bool_value(eval_fnmatch_bytes(&pattern, &filename, flags)) +} + +/// Matches byte strings using the eval-supported `fnmatch()` grammar and flags. +fn eval_fnmatch_bytes(pattern: &[u8], filename: &[u8], flags: i64) -> bool { + let mut memo = vec![vec![None; filename.len() + 1]; pattern.len() + 1]; + eval_fnmatch_at(pattern, filename, flags, 0, 0, &mut memo) +} + +/// Recursively matches a pattern suffix against a filename suffix with memoization. +fn eval_fnmatch_at( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + if let Some(result) = memo[pattern_index][filename_index] { + return result; + } + let result = if pattern_index == pattern.len() { + filename_index == filename.len() + } else { + match pattern[pattern_index] { + b'*' => eval_fnmatch_star(pattern, filename, flags, pattern_index, filename_index, memo), + b'?' => { + eval_fnmatch_single_wildcard(filename, flags, filename_index) + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ) + } + b'[' => eval_fnmatch_class_or_literal( + pattern, + filename, + flags, + pattern_index, + filename_index, + memo, + ), + b'\\' if flags & EVAL_FNM_NOESCAPE == 0 => { + let (literal, next_pattern_index) = + eval_fnmatch_escaped_literal(pattern, pattern_index); + eval_fnmatch_literal(filename, flags, filename_index, literal) + && eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index + 1, + memo, + ) + } + literal => { + eval_fnmatch_literal(filename, flags, filename_index, literal) + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ) + } + } + }; + memo[pattern_index][filename_index] = Some(result); + result +} + +/// Handles `*`, including pathname and leading-period restrictions. +fn eval_fnmatch_star( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + let mut next_pattern_index = pattern_index + 1; + while next_pattern_index < pattern.len() && pattern[next_pattern_index] == b'*' { + next_pattern_index += 1; + } + if eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index, + memo, + ) { + return true; + } + let mut cursor = filename_index; + while cursor < filename.len() && eval_fnmatch_wildcard_can_consume(filename, flags, cursor) { + cursor += 1; + if eval_fnmatch_at(pattern, filename, flags, next_pattern_index, cursor, memo) { + return true; + } + } + false +} + +/// Returns whether `?` can consume the current filename byte. +fn eval_fnmatch_single_wildcard(filename: &[u8], flags: i64, filename_index: usize) -> bool { + filename_index < filename.len() + && eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) +} + +/// Handles a bracket class, or falls back to a literal `[` when the class is malformed. +fn eval_fnmatch_class_or_literal( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + if filename_index >= filename.len() + || !eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) + { + return false; + } + let Some((matches, next_pattern_index)) = + eval_fnmatch_class_matches(pattern, pattern_index + 1, filename[filename_index], flags) + else { + return eval_fnmatch_literal(filename, flags, filename_index, b'[') + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ); + }; + matches + && eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index + 1, + memo, + ) +} + +/// Matches one bracket class body against the current filename byte. +fn eval_fnmatch_class_matches( + pattern: &[u8], + mut index: usize, + candidate: u8, + flags: i64, +) -> Option<(bool, usize)> { + let negated = matches!(pattern.get(index).copied(), Some(b'!' | b'^')); + if negated { + index += 1; + } + let mut matched = false; + let mut closed = false; + while index < pattern.len() { + if pattern[index] == b']' { + closed = true; + index += 1; + break; + } + let start = eval_fnmatch_class_char(pattern, &mut index, flags)?; + if index + 1 < pattern.len() && pattern[index] == b'-' && pattern[index + 1] != b']' { + index += 1; + let end = eval_fnmatch_class_char(pattern, &mut index, flags)?; + if eval_fnmatch_byte_in_range(candidate, start, end, flags) { + matched = true; + } + } else if eval_fnmatch_byte_eq(candidate, start, flags) { + matched = true; + } + } + closed.then_some((if negated { !matched } else { matched }, index)) +} + +/// Reads one character from a bracket class, respecting escapes when enabled. +fn eval_fnmatch_class_char(pattern: &[u8], index: &mut usize, flags: i64) -> Option { + if *index >= pattern.len() { + return None; + } + if pattern[*index] == b'\\' && flags & EVAL_FNM_NOESCAPE == 0 && *index + 1 < pattern.len() { + *index += 2; + return Some(pattern[*index - 1]); + } + let byte = pattern[*index]; + *index += 1; + Some(byte) +} + +/// Returns whether one candidate byte falls within a possibly case-folded range. +fn eval_fnmatch_byte_in_range(candidate: u8, start: u8, end: u8, flags: i64) -> bool { + let candidate = eval_fnmatch_fold(candidate, flags); + let start = eval_fnmatch_fold(start, flags); + let end = eval_fnmatch_fold(end, flags); + if start <= end { + candidate >= start && candidate <= end + } else { + candidate >= end && candidate <= start + } +} + +/// Reads an escaped literal token outside bracket classes. +fn eval_fnmatch_escaped_literal(pattern: &[u8], pattern_index: usize) -> (u8, usize) { + if pattern_index + 1 < pattern.len() { + (pattern[pattern_index + 1], pattern_index + 2) + } else { + (b'\\', pattern_index + 1) + } +} + +/// Returns whether one literal pattern byte matches the current filename byte. +fn eval_fnmatch_literal(filename: &[u8], flags: i64, filename_index: usize, literal: u8) -> bool { + filename_index < filename.len() + && eval_fnmatch_byte_eq(filename[filename_index], literal, flags) +} + +/// Returns whether a wildcard token may consume the current filename byte. +fn eval_fnmatch_wildcard_can_consume(filename: &[u8], flags: i64, filename_index: usize) -> bool { + if filename_index >= filename.len() { + return false; + } + if flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index] == b'/' { + return false; + } + if flags & EVAL_FNM_PERIOD != 0 && eval_fnmatch_is_leading_period(filename, flags, filename_index) { + return false; + } + true +} + +/// Returns whether the current byte is a leading period for `FNM_PERIOD`. +fn eval_fnmatch_is_leading_period(filename: &[u8], flags: i64, filename_index: usize) -> bool { + filename[filename_index] == b'.' + && (filename_index == 0 + || (flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index - 1] == b'/')) +} + +/// Compares bytes using ASCII case folding when `FNM_CASEFOLD` is present. +fn eval_fnmatch_byte_eq(left: u8, right: u8, flags: i64) -> bool { + eval_fnmatch_fold(left, flags) == eval_fnmatch_fold(right, flags) +} + +/// Applies eval fnmatch's ASCII case folding. +fn eval_fnmatch_fold(byte: u8, flags: i64) -> u8 { + if flags & EVAL_FNM_CASEFOLD != 0 { + byte.to_ascii_lowercase() + } else { + byte + } +} + /// Evaluates PHP `gethostbyname($hostname)` over one eval expression. fn eval_builtin_gethostbyname( args: &[EvalExpr], @@ -7147,6 +7458,10 @@ fn eval_predefined_int_constant(name: &str) -> Option { "PATHINFO_EXTENSION" => Some(EVAL_PATHINFO_EXTENSION), "PATHINFO_FILENAME" => Some(EVAL_PATHINFO_FILENAME), "PATHINFO_ALL" => Some(EVAL_PATHINFO_ALL), + "FNM_NOESCAPE" => Some(EVAL_FNM_NOESCAPE), + "FNM_PATHNAME" => Some(EVAL_FNM_PATHNAME), + "FNM_PERIOD" => Some(EVAL_FNM_PERIOD), + "FNM_CASEFOLD" => Some(EVAL_FNM_CASEFOLD), _ => None, } } @@ -9965,6 +10280,37 @@ return function_exists("realpath");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. + #[test] + fn execute_program_dispatches_fnmatch_builtin() { + let program = parse_fragment( + br#"echo fnmatch("*.log", "system.log") ? "match" : "bad"; echo ":"; +echo fnmatch("*.log", "logs/system.log", FNM_PATHNAME) ? "bad" : "path"; echo ":"; +echo fnmatch("*.LOG", "system.log", FNM_CASEFOLD) ? "case" : "bad"; echo ":"; +echo fnmatch("*", ".env", FNM_PERIOD) ? "bad" : "period"; echo ":"; +echo fnmatch("[!abc]oo", "doo") && !fnmatch("[!abc]oo", "boo") ? "class" : "bad"; echo ":"; +echo fnmatch('a\\*b', 'a*b') ? "escape" : "bad"; echo ":"; +echo fnmatch('a\\*b', 'a\\xxb', FNM_NOESCAPE) ? "noescape" : "bad"; echo ":"; +$flags = FNM_PATHNAME | FNM_CASEFOLD; +echo fnmatch("dir/*.TXT", "dir/file.txt", $flags) ? "flags" : "bad"; echo ":"; +echo call_user_func("fnmatch", "*.txt", "report.txt") ? "call" : "bad"; echo ":"; +echo call_user_func_array("fnmatch", ["pattern" => "*.TXT", "filename" => "report.txt", "flags" => FNM_CASEFOLD]) ? "callarray" : "bad"; echo ":"; +echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD"); +return FNM_CASEFOLD;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "match:path:case:period:class:escape:noescape:flags:call:callarray:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_FNM_CASEFOLD)); + } + /// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. #[test] fn execute_program_dispatches_pathinfo_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e6739fad2f..996e463e2d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -51,7 +51,7 @@ Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver Eval IPv4 conversion dispatch includes `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` for direct calls, named arguments, callable dispatch, and `function_exists()` probes. The interpreter currently handles IPv4 dotted-quad strings and four-byte packed addresses. -Eval path component dispatch includes `basename()`, `dirname()`, and `pathinfo()` in the interpreter, including suffix trimming, repeated parent traversal, `PATHINFO_*` constants, and pathinfo associative-array construction without a filesystem lookup. +Eval path component dispatch includes `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` in the interpreter, including suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, pathinfo associative-array construction, and shell-glob matching without a filesystem lookup. Eval `realpath()` uses the host filesystem to canonicalize existing paths and boxes PHP false for unresolved paths. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a9f94c32f7..dbdb2cc7f6 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -66,7 +66,7 @@ Eval `gethostbyname()` resolves IPv4 host names through the host resolver and re Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. -Eval path component calls include `basename()`, `dirname()`, and `pathinfo()` with suffix trimming, repeated parent traversal, `PATHINFO_*` constants, named arguments, callable dispatch, and `function_exists()` probes. +Eval path component calls include `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` with suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, named arguments, callable dispatch, and `function_exists()` probes. Eval `realpath()` canonicalizes existing paths through the host filesystem and returns `false` when the path cannot be resolved. diff --git a/examples/eval/main.php b/examples/eval/main.php index 5638df59c9..d9ab0aa5de 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -158,7 +158,7 @@ public function __construct(int $value) { $ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); $path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); -$path_info = eval('$info = pathinfo("/var/log/syslog.log"); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION);'); +$path_info = eval('$info = pathinfo("/var/log/syslog.log"); $match = fnmatch("*.LOG", "system.log", FNM_CASEFOLD); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":" . ($match ? "match" : "bad");'); $filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); $file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $info = stat("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0 && $info["size"] === 5 && $info[7] === $info["size"]; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); $path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ddf23445aa..ee808245a0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1380,6 +1380,31 @@ echo ":"; echo function_exists("realpath");'); assert_eq!(out, "resolved:false:call:array-false:1"); } +/// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. +#[test] +fn test_eval_dispatches_fnmatch_builtin_call() { + let out = compile_and_run( + r#" "*.TXT", "filename" => "report.txt", "flags" => FNM_CASEFOLD]) ? "callarray" : "bad"; echo ":"; +echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD");'); +"#, + ); + assert_eq!( + out, + "match:path:case:period:class:escape:noescape:flags:call:callarray:11" + ); +} + /// Verifies eval `pathinfo()` supports arrays, component flags, constants, and callables. #[test] fn test_eval_dispatches_pathinfo_builtin_call() { From 60994b4169702e8bbb17280fa1415d42270071c4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 09:58:16 +0200 Subject: [PATCH 0165/1208] Support eval glob builtin --- crates/elephc-eval/src/interpreter.rs | 182 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 36 +++++ 5 files changed, 223 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 45f020c3fb..b537fac89e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1256,6 +1256,7 @@ fn eval_positional_expr_call( "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), + "glob" => eval_builtin_glob(args, context, scope, values), "hash_algos" => eval_builtin_hash_algos(args, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), @@ -1635,6 +1636,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "getcwd" | "getenv" | "gettype" + | "glob" | "hash_algos" | "hash_equals" | "hex2bin" @@ -1854,6 +1856,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "gethostbyname" => Some(&["hostname"]), "getcwd" => Some(&[]), "getenv" => Some(&["name"]), + "glob" => Some(&["pattern"]), "hash_algos" => Some(&[]), "hash_equals" => Some(&["known_string", "user_string"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), @@ -2460,6 +2463,12 @@ fn eval_builtin_with_values( }; eval_gettype_result(*value, values)? } + "glob" => { + let [pattern] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_glob_result(*pattern, values)? + } "hash_algos" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -4710,6 +4719,126 @@ fn eval_scandir_result( Ok(result) } +/// Evaluates PHP `glob($pattern)` over one eval expression. +fn eval_builtin_glob( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + eval_glob_result(pattern, values) +} + +/// Expands one local glob pattern into a sorted indexed PHP string array. +fn eval_glob_result( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = eval_path_string(pattern, values)?; + let matches = eval_glob_matches(&pattern); + let mut result = values.array_new(matches.len())?; + for (index, path) in matches.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; + } + Ok(result) +} + +/// Collects sorted matches for one local glob pattern. +fn eval_glob_matches(pattern: &str) -> Vec { + if pattern.is_empty() { + return Vec::new(); + } + if !eval_glob_component_has_magic(pattern) { + return std::path::Path::new(pattern) + .exists() + .then(|| pattern.to_string()) + .into_iter() + .collect(); + } + let absolute = pattern.starts_with('/'); + let components: Vec<&str> = pattern.split('/').filter(|component| !component.is_empty()).collect(); + let mut matches = Vec::new(); + let base = if absolute { + std::path::PathBuf::from("/") + } else { + std::path::PathBuf::from(".") + }; + let prefix = if absolute { "/" } else { "" }; + eval_glob_collect(&base, prefix, &components, &mut matches); + matches.sort(); + matches +} + +/// Recursively expands one glob path component at a time. +fn eval_glob_collect( + base: &std::path::Path, + prefix: &str, + components: &[&str], + matches: &mut Vec, +) { + let Some((component, rest)) = components.split_first() else { + if base.exists() && !prefix.is_empty() { + matches.push(prefix.to_string()); + } + return; + }; + if !eval_glob_component_has_magic(component) { + let next_base = base.join(component); + if rest.is_empty() { + if next_base.exists() { + matches.push(eval_glob_join_output(prefix, component)); + } + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, component); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + return; + } + let Ok(entries) = std::fs::read_dir(base) else { + return; + }; + let mut names = Vec::new(); + for entry in entries.flatten() { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + for name in names { + if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { + continue; + } + let next_base = base.join(&name); + if rest.is_empty() { + matches.push(eval_glob_join_output(prefix, &name)); + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, &name); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + } +} + +/// Joins a display path prefix and component while preserving absolute-root output. +fn eval_glob_join_output(prefix: &str, component: &str) -> String { + if prefix.is_empty() { + component.to_string() + } else if prefix == "/" { + format!("/{component}") + } else { + format!("{prefix}/{component}") + } +} + +/// Returns whether a glob component contains wildcard syntax. +fn eval_glob_component_has_magic(component: &str) -> bool { + component + .as_bytes() + .iter() + .any(|byte| matches!(byte, b'*' | b'?' | b'[')) +} + /// Writes one byte-string value into an indexed runtime array at a zero-based position. fn eval_array_set_indexed_bytes( array: RuntimeCellHandle, @@ -10596,6 +10725,59 @@ return true;"# assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `glob()` expands local patterns and dispatches dynamically. + #[test] + fn execute_program_dispatches_glob_builtin() { + let pid = std::process::id(); + let dir = format!("elephc_eval_glob_dir_{pid}"); + let source = format!( + r#"mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.log", "b"); +file_put_contents("{dir}/c.txt", "c"); +file_put_contents("{dir}/.hidden.txt", "h"); +$matches = glob("{dir}/*.txt"); +echo count($matches) === 2 && basename($matches[0]) === "a.txt" && basename($matches[1]) === "c.txt" ? "glob" : "bad"; echo ":"; +echo count(glob("{dir}/*.none")) === 0 ? "empty" : "bad"; echo ":"; +$literal = glob("{dir}/a.txt"); +echo count($literal) === 1 && $literal[0] === "{dir}/a.txt" ? "literal" : "bad"; echo ":"; +$all = glob("{dir}/*"); +echo in_array("{dir}/.hidden.txt", $all) ? "bad" : "hidden"; echo ":"; +$call = call_user_func("glob", "{dir}/*.log"); +echo count($call) === 1 && basename($call[0]) === "b.log" ? "callglob" : "bad"; echo ":"; +$call_array = call_user_func_array("glob", ["pattern" => "{dir}/*.txt"]); +echo count($call_array) === 2 ? "callarray" : "bad"; echo ":"; +unlink("{dir}/.hidden.txt"); +unlink("{dir}/c.txt"); +unlink("{dir}/b.log"); +unlink("{dir}/a.txt"); +echo rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("glob"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "glob:empty:literal:hidden:callglob:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. #[test] fn execute_program_dispatches_file_modify_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 996e463e2d..d936afc9de 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -45,7 +45,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zer Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. -Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain in the native runtime path rather than the eval subset. +Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain in the native runtime path rather than the eval subset. Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index dbdb2cc7f6..999600c200 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,7 +60,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. -Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain outside this eval filesystem subset. +Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain outside this eval filesystem subset. Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. diff --git a/examples/eval/main.php b/examples/eval/main.php index d9ab0aa5de..d621301dd9 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -162,7 +162,7 @@ public function __construct(int $value) { $filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); $file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $info = stat("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0 && $info["size"] === 5 && $info[7] === $info["size"]; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); $path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); -$file_listing = eval('file_put_contents("eval-lines.txt", "one\ntwo"); file_put_contents("eval-empty.txt", ""); mkdir("eval-list-dir"); file_put_contents("eval-list-dir/a.txt", "a"); $lines = file("eval-lines.txt"); $scan = scandir("eval-list-dir"); $bytes = readfile("eval-empty.txt"); $ok = count($lines) === 2 && $lines[0] === "one\n" && $bytes === 0 && in_array("a.txt", $scan); unlink("eval-list-dir/a.txt"); rmdir("eval-list-dir"); unlink("eval-lines.txt"); unlink("eval-empty.txt"); return $ok ? "ok" : "bad";'); +$file_listing = eval('file_put_contents("eval-lines.txt", "one\ntwo"); file_put_contents("eval-empty.txt", ""); mkdir("eval-list-dir"); file_put_contents("eval-list-dir/a.txt", "a"); $lines = file("eval-lines.txt"); $scan = scandir("eval-list-dir"); $glob = glob("eval-list-dir/*.txt"); $bytes = readfile("eval-empty.txt"); $ok = count($lines) === 2 && $lines[0] === "one\n" && $bytes === 0 && in_array("a.txt", $scan) && count($glob) === 1; unlink("eval-list-dir/a.txt"); rmdir("eval-list-dir"); unlink("eval-lines.txt"); unlink("eval-empty.txt"); return $ok ? "ok" : "bad";'); $file_modify = eval('touch("eval-touch.txt", 1000000000); file_put_contents("eval-mod.txt", "x"); $tmp = tempnam(".", "evm"); $previous = umask(18); $probe = umask(); umask($previous); $ok = filemtime("eval-touch.txt") === 1000000000 && chmod("eval-mod.txt", 384) && file_exists($tmp) && $probe === 18; unlink($tmp); unlink("eval-touch.txt"); unlink("eval-mod.txt"); return $ok ? "ok" : "bad";'); $hexed = eval('return bin2hex("Az");'); $unhexed = eval('return hex2bin("417a");'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ee808245a0..4a518bc3e7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1595,6 +1595,42 @@ echo function_exists("file"); echo function_exists("readfile"); echo function_ex ); } +/// Verifies eval `glob()` expands local patterns and dispatches dynamically. +#[test] +fn test_eval_dispatches_glob_builtin_calls() { + let out = compile_and_run( + r#" "eval-glob-dir/*.txt"]); +echo count($call_array) === 2 ? "callarray" : "bad"; echo ":"; +unlink("eval-glob-dir/.hidden.txt"); +unlink("eval-glob-dir/c.txt"); +unlink("eval-glob-dir/b.log"); +unlink("eval-glob-dir/a.txt"); +echo rmdir("eval-glob-dir") ? "cleanup" : "bad"; echo ":"; +echo function_exists("glob"); +'); +"#, + ); + assert_eq!( + out, + "glob:empty:literal:hidden:callglob:callarray:cleanup:1" + ); +} + /// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. #[test] fn test_eval_dispatches_file_modify_builtin_calls() { From 06116614bd41e8e841c7f9b2675621e6212ea6ed Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 10:15:46 +0200 Subject: [PATCH 0166/1208] Support eval hash digest builtins --- Cargo.lock | 3 + crates/elephc-eval/Cargo.toml | 1 + crates/elephc-eval/src/interpreter.rs | 266 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 6 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 32 ++++ 7 files changed, 299 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a53bf4415b..c456fa4d23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -619,6 +619,9 @@ dependencies = [ [[package]] name = "elephc-eval" version = "0.1.0" +dependencies = [ + "elephc-crypto", +] [[package]] name = "elephc-image" diff --git a/crates/elephc-eval/Cargo.toml b/crates/elephc-eval/Cargo.toml index f74ea88cd4..ec14e26061 100644 --- a/crates/elephc-eval/Cargo.toml +++ b/crates/elephc-eval/Cargo.toml @@ -10,3 +10,4 @@ publish = false crate-type = ["staticlib", "rlib"] [dependencies] +elephc-crypto = { path = "../elephc-crypto" } diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b537fac89e..88513cedad 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1257,6 +1257,9 @@ fn eval_positional_expr_call( "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), "glob" => eval_builtin_glob(args, context, scope, values), + "hash" | "hash_hmac" | "md5" | "sha1" => { + eval_builtin_hash_one_shot(name, args, context, scope, values) + } "hash_algos" => eval_builtin_hash_algos(args, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), @@ -1637,8 +1640,10 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "getenv" | "gettype" | "glob" + | "hash" | "hash_algos" | "hash_equals" + | "hash_hmac" | "hex2bin" | "html_entity_decode" | "htmlentities" @@ -1675,6 +1680,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "lcfirst" | "long2ip" | "max" + | "md5" | "microtime" | "min" | "mkdir" @@ -1699,6 +1705,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "rmdir" | "scandir" | "sleep" + | "sha1" | "sqrt" | "strcasecmp" | "str_contains" @@ -1857,8 +1864,10 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "getcwd" => Some(&[]), "getenv" => Some(&["name"]), "glob" => Some(&["pattern"]), + "hash" => Some(&["algo", "data", "binary"]), "hash_algos" => Some(&[]), "hash_equals" => Some(&["known_string", "user_string"]), + "hash_hmac" => Some(&["algo", "data", "key", "binary"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), "inet_ntop" => Some(&["ip"]), @@ -1867,6 +1876,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), "max" | "min" => Some(&["value"]), + "md5" | "sha1" => Some(&["string", "binary"]), "microtime" => Some(&["as_float"]), "nl2br" => Some(&["string", "use_xhtml"]), "number_format" => Some(&["num", "decimals", "decimal_separator", "thousands_separator"]), @@ -2469,6 +2479,9 @@ fn eval_builtin_with_values( }; eval_glob_result(*pattern, values)? } + "hash" | "hash_hmac" | "md5" | "sha1" => { + eval_hash_one_shot_result(name, evaluated_args, values)? + } "hash_algos" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -4027,6 +4040,138 @@ fn eval_crc32_result( values.int(i64::from(eval_crc32_bytes(&bytes))) } +/// Evaluates one-shot PHP hash digest builtins over eval expressions. +fn eval_builtin_hash_one_shot( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_hash_one_shot_result(name, &evaluated_args, values) +} + +/// Computes the result for one-shot PHP hash digest builtins from evaluated args. +fn eval_hash_one_shot_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "md5" | "sha1" => { + let (data, binary) = match evaluated_args { + [data] => (*data, false), + [data, binary] => (*data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let data = values.string_bytes(data)?; + eval_hash_digest_result(name.as_bytes(), &data, binary, values) + } + "hash" => { + let (algo, data, binary) = match evaluated_args { + [algo, data] => (*algo, *data, false), + [algo, data, binary] => (*algo, *data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + eval_hash_digest_result(&algo, &data, binary, values) + } + "hash_hmac" => { + let (algo, data, key, binary) = match evaluated_args { + [algo, data, key] => (*algo, *data, *key, false), + [algo, data, key, binary] => (*algo, *data, *key, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + let key = values.string_bytes(key)?; + eval_hash_hmac_result(&algo, &data, &key, binary, values) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. +fn eval_hash_digest_result( + algo: &[u8], + data: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hash(algo, data)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. +fn eval_hash_hmac_result( + algo: &[u8], + data: &[u8], + key: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hmac(algo, data, key)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. +fn eval_crypto_hash(algo: &[u8], data: &[u8]) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hash( + algo.as_ptr(), + algo.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. +fn eval_crypto_hmac(algo: &[u8], data: &[u8], key: &[u8]) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hmac( + algo.as_ptr(), + algo.len(), + key.as_ptr(), + key.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Converts a crypto ABI digest length into an owned digest byte vector. +fn eval_crypto_digest_bytes(len: isize, output: &[u8; 64]) -> Result, EvalStatus> { + let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + if len > output.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(output[..len].to_vec()) +} + +/// Formats a raw digest using PHP's `$binary` flag convention. +fn eval_format_digest_result( + raw: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if binary { + return values.string_bytes_value(raw); + } + values.string(&eval_lower_hex_bytes(raw)) +} + /// Evaluates PHP `hash_algos()` with no arguments. fn eval_builtin_hash_algos( args: &[EvalExpr], @@ -6083,13 +6228,18 @@ fn eval_bin2hex_result( values: &mut impl RuntimeValueOps, ) -> Result { let bytes = values.string_bytes(value)?; + values.string(&eval_lower_hex_bytes(&bytes)) +} + +/// Converts bytes to lowercase hexadecimal text. +fn eval_lower_hex_bytes(bytes: &[u8]) -> String { let mut output = String::with_capacity(bytes.len() * 2); const HEX: &[u8; 16] = b"0123456789abcdef"; for byte in bytes { output.push(HEX[(byte >> 4) as usize] as char); output.push(HEX[(byte & 0x0f) as usize] as char); } - values.string(&output) + output } /// Evaluates PHP's `hex2bin(...)` over one eval expression. @@ -7643,6 +7793,7 @@ mod tests { Int(i64), Float(f64), String(String), + Bytes(Vec), Array(Vec), Assoc(Vec<(FakeKey, RuntimeCellHandle)>), Object(HashMap), @@ -7682,6 +7833,12 @@ mod tests { FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) .map(FakeKey::Int) .map_or_else(|| Ok(FakeKey::String(value)), Ok), + FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) + .map(FakeKey::Int) + .map_or_else( + || Ok(FakeKey::String(String::from_utf8_lossy(&value).into_owned())), + Ok, + ), FakeValue::Null => Ok(FakeKey::String(String::new())), value => Ok(FakeKey::Int(self.fake_int(&value))), } @@ -7946,7 +8103,7 @@ mod tests { fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { Ok(match self.get(value) { FakeValue::Int(_) => EVAL_TAG_INT, - FakeValue::String(_) => EVAL_TAG_STRING, + FakeValue::String(_) | FakeValue::Bytes(_) => EVAL_TAG_STRING, FakeValue::Float(_) => EVAL_TAG_FLOAT, FakeValue::Bool(_) => EVAL_TAG_BOOL, FakeValue::Array(_) => EVAL_TAG_ARRAY, @@ -7999,10 +8156,12 @@ mod tests { Ok(self.alloc(FakeValue::String(value.to_string()))) } - /// Creates a fake string cell from UTF-8 bytes. + /// Creates a fake string cell from raw PHP bytes. fn string_bytes_value(&mut self, value: &[u8]) -> Result { - let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; - self.string(value) + match std::str::from_utf8(value) { + Ok(value) => self.string(value), + Err(_) => Ok(self.alloc(FakeValue::Bytes(value.to_vec()))), + } } /// Casts a fake runtime cell to a fake integer cell. @@ -8219,15 +8378,16 @@ mod tests { self.int(!value) } - /// Concatenates fake cells with simple string conversion for interpreter tests. + /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. fn concat( &mut self, left: RuntimeCellHandle, right: RuntimeCellHandle, ) -> Result { - let left = self.stringify(left); - let right = self.stringify(right); - self.string(&(left + &right)) + let mut left = self.string_bytes_for_value(&self.get(left)); + let right = self.string_bytes_for_value(&self.get(right)); + left.extend_from_slice(&right); + self.string_bytes_value(&left) } /// Compares fake scalar cells and returns a fake PHP boolean. @@ -8295,7 +8455,7 @@ mod tests { /// Casts one fake runtime cell to bytes for nested eval parsing. fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { - Ok(self.stringify(value).into_bytes()) + Ok(self.string_bytes_for_value(&self.get(value))) } /// Returns PHP-like truthiness for fake runtime cells. @@ -8306,6 +8466,7 @@ mod tests { FakeValue::Int(value) => value != 0, FakeValue::Float(value) => value != 0.0, FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", FakeValue::Array(value) => !value.is_empty(), FakeValue::Assoc(value) => !value.is_empty(), FakeValue::Object(_) => true, @@ -8323,18 +8484,31 @@ mod tests { (FakeValue::Null, FakeValue::Null) => true, (FakeValue::Null, FakeValue::String(value)) | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), + (FakeValue::Null, FakeValue::Bytes(value)) + | (FakeValue::Bytes(value), FakeValue::Null) => value.is_empty(), (FakeValue::String(left), FakeValue::String(right)) => { match (left.parse::(), right.parse::()) { (Ok(left), Ok(right)) => left == right, _ => left == right, } } + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, (FakeValue::String(left), right) => left .parse::() .is_ok_and(|left| left == self.fake_numeric(&right)), + (FakeValue::Bytes(left), right) => std::str::from_utf8(&left) + .ok() + .and_then(|left| left.parse::().ok()) + .is_some_and(|left| left == self.fake_numeric(&right)), (left, FakeValue::String(right)) => right .parse::() .is_ok_and(|right| self.fake_numeric(&left) == right), + (left, FakeValue::Bytes(right)) => std::str::from_utf8(&right) + .ok() + .and_then(|right| right.parse::().ok()) + .is_some_and(|right| self.fake_numeric(&left) == right), (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), } } @@ -8347,6 +8521,9 @@ mod tests { (FakeValue::Int(left), FakeValue::Int(right)) => left == right, (FakeValue::Float(left), FakeValue::Float(right)) => left == right, (FakeValue::String(left), FakeValue::String(right)) => left == right, + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, _ => false, } @@ -8366,6 +8543,10 @@ mod tests { FakeValue::Int(value) => *value as f64, FakeValue::Float(value) => *value, FakeValue::String(value) => value.parse::().unwrap_or(0.0), + FakeValue::Bytes(value) => std::str::from_utf8(value) + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(0.0), FakeValue::Array(value) => value.len() as f64, FakeValue::Assoc(value) => value.len() as f64, FakeValue::Object(_) => 1.0, @@ -8386,6 +8567,7 @@ mod tests { FakeValue::Int(value) => *value != 0, FakeValue::Float(value) => *value != 0.0, FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", FakeValue::Array(value) => !value.is_empty(), FakeValue::Assoc(value) => !value.is_empty(), FakeValue::Object(_) => true, @@ -8402,12 +8584,38 @@ mod tests { FakeValue::Int(value) => value.to_string(), FakeValue::Float(value) => value.to_string(), FakeValue::String(value) => value, + FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), FakeValue::Array(_) => "Array".to_string(), FakeValue::Assoc(_) => "Array".to_string(), FakeValue::Object(_) => "Object".to_string(), FakeValue::Resource(value) => format!("Resource id #{}", value + 1), } } + + /// Converts a fake PHP value to string bytes while preserving binary strings. + fn string_bytes_for_value(&self, value: &FakeValue) -> Vec { + match value { + FakeValue::String(value) => value.as_bytes().to_vec(), + FakeValue::Bytes(value) => value.clone(), + value => self.stringify_value(value).into_bytes(), + } + } + + /// Converts one loaded fake PHP value to display text for byte coercions. + fn stringify_value(&self, value: &FakeValue) -> String { + match value { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value.clone(), + FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), + FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + } + } } /// Test native invoker that returns the descriptor pointer as a runtime cell. @@ -10185,6 +10393,44 @@ return count($algos);"#, assert_eq!(values.get(result), FakeValue::Int(28)); } + /// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. + #[test] + fn execute_program_dispatches_hash_digest_builtins() { + let program = parse_fragment( + br#"echo md5("abc"); echo ":"; +echo sha1(string: "abc"); echo ":"; +echo hash("sha256", "abc"); echo ":"; +echo hash_hmac(algo: "sha256", data: "data", key: "key"); echo ":"; +echo bin2hex(md5("abc", true)); echo ":"; +echo bin2hex(call_user_func("sha1", "abc", true)); echo ":"; +echo call_user_func_array("hash", ["algo" => "md5", "data" => "abc"]); echo ":"; +echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; +echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); +return function_exists("hash_hmac");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "900150983cd24fb0d6963f7d28e17f72:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval zero-argument system builtins return native-compatible values. #[test] fn execute_program_dispatches_zero_arg_system_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d936afc9de..0ab668217d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -35,7 +35,7 @@ Eval `wordwrap()` performs the same word-aware byte scan as the static helper, p Eval `number_format()` formats finite numeric cells inside the interpreter, including grouping and custom separators. -Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. +Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. @@ -63,7 +63,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 999600c200..e7a814989e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -50,7 +50,7 @@ Eval `wordwrap()` supports width, break string, and `cut_long_words` arguments w Eval `number_format()` supports decimal counts plus custom decimal and thousands separators. -Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. +Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. diff --git a/examples/eval/main.php b/examples/eval/main.php index d621301dd9..9b55864801 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -149,6 +149,7 @@ public function __construct(int $value) { $url_codec = eval('return urlencode("a b&=") . ":" . rawurldecode("a%20b%26%3D");'); $checksum = eval('return crc32("hello");'); $hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); +$digest = eval('return md5("abc") . ":" . substr(hash("sha256", "abc"), 0, 8) . ":" . substr(hash_hmac("sha256", "data", "key"), 0, 8);'); $system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); $micro_time = eval('return microtime(true) > 1000000000 ? "ok" : "bad";'); $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); @@ -259,6 +260,7 @@ public function __construct(int $value) { echo "url-codec=" . $url_codec . "\n"; echo "crc32=" . $checksum . "\n"; echo "hash-algos=" . $hash_algos . "\n"; +echo "digest=" . $digest . "\n"; echo "system-info=" . $system_info . "\n"; echo "microtime=" . $micro_time . "\n"; echo "realpath-cache=" . $realpath_cache . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4a518bc3e7..8e3fdac4d3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1205,6 +1205,38 @@ echo function_exists("hash_algos") ? "exists" : "missing";'); assert_eq!(out, "28:md2:sha256:crc:whirlpool:joaat:exists"); } +/// Verifies eval one-shot hash digest builtins use the crypto bridge. +#[test] +fn test_eval_dispatches_hash_digest_builtin_calls() { + let out = compile_and_run( + r#" "md5", "data" => "abc"]); echo ":"; +echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; +echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_hmac");'); +"#, + ); + assert_eq!( + out, + concat!( + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "900150983cd24fb0d6963f7d28e17f72:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "1111" + ) + ); +} + /// Verifies eval zero-argument system builtins match native runtime conventions. #[test] fn test_eval_dispatches_zero_arg_system_builtin_calls() { From 46015ab0d2f83315247b0f06509a382f9f8d07d4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 10:31:55 +0200 Subject: [PATCH 0167/1208] Support eval extended math builtins --- crates/elephc-eval/src/interpreter.rs | 235 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 34 ++++ 5 files changed, 275 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 88513cedad..a37790210d 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1205,6 +1205,11 @@ fn eval_positional_expr_call( eval_builtin_array_search(name, args, context, scope, values) } "array_unique" => eval_builtin_array_unique(args, context, scope, values), + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { + eval_builtin_float_unary(name, args, context, scope, values) + } + "atan2" | "hypot" => eval_builtin_float_pair(name, args, context, scope, values), "base64_encode" => eval_builtin_base64_encode(args, context, scope, values), "base64_decode" => eval_builtin_base64_decode(args, context, scope, values), "basename" => eval_builtin_basename(args, context, scope, values), @@ -1269,6 +1274,7 @@ fn eval_positional_expr_call( "implode" => eval_builtin_implode(args, context, scope, values), "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), + "intdiv" => eval_builtin_intdiv(args, context, scope, values), "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" @@ -1278,6 +1284,7 @@ fn eval_positional_expr_call( "ip2long" => eval_builtin_ip2long(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), + "log" => eval_builtin_log(args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), "nl2br" => eval_builtin_nl2br(args, context, scope, values), @@ -1591,6 +1598,10 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_sum" | "array_unique" | "array_values" + | "acos" + | "asin" + | "atan" + | "atan2" | "basename" | "base64_decode" | "base64_encode" @@ -1607,6 +1618,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "clearstatcache" | "count" | "copy" + | "cos" + | "cosh" | "crc32" | "ctype_alnum" | "ctype_alpha" @@ -1614,7 +1627,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "ctype_space" | "define" | "defined" + | "deg2rad" | "dirname" + | "exp" | "explode" | "fdiv" | "file" @@ -1648,10 +1663,12 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "html_entity_decode" | "htmlentities" | "htmlspecialchars" + | "hypot" | "implode" | "in_array" | "inet_ntop" | "inet_pton" + | "intdiv" | "ip2long" | "is_dir" | "is_executable" @@ -1678,6 +1695,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_resource" | "is_string" | "lcfirst" + | "log" + | "log2" + | "log10" | "long2ip" | "max" | "md5" @@ -1692,6 +1712,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "pow" | "phpversion" | "putenv" + | "rad2deg" | "rawurldecode" | "rawurlencode" | "readfile" @@ -1706,6 +1727,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "scandir" | "sleep" | "sha1" + | "sin" + | "sinh" | "sqrt" | "strcasecmp" | "str_contains" @@ -1731,6 +1754,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "symlink" | "sys_get_temp_dir" | "tempnam" + | "tan" + | "tanh" | "time" | "touch" | "trim" @@ -1828,6 +1853,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_key_exists" => Some(&["key", "array"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), + "atan2" => Some(&["y", "x"]), "basename" => Some(&["path", "suffix"]), "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => { @@ -1868,13 +1896,16 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "hash_algos" => Some(&[]), "hash_equals" => Some(&["known_string", "user_string"]), "hash_hmac" => Some(&["algo", "data", "key", "binary"]), + "hypot" => Some(&["x", "y"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), + "intdiv" => Some(&["num1", "num2"]), "ip2long" => Some(&["ip"]), "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), + "log" => Some(&["num", "base"]), "max" | "min" => Some(&["value"]), "md5" | "sha1" => Some(&["string", "binary"]), "microtime" => Some(&["as_float"]), @@ -2118,6 +2149,19 @@ fn eval_builtin_with_values( }; eval_base64_decode_result(*value, values)? } + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_unary_result(name, *value, values)? + } + "atan2" | "hypot" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_pair_result(name, *left, *right, values)? + } "bin2hex" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2235,6 +2279,11 @@ fn eval_builtin_with_values( }; eval_linkinfo_result(*path, values)? } + "log" => match evaluated_args { + [num] => eval_log_result(*num, None, values)?, + [num, base] => eval_log_result(*num, Some(*base), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "readfile" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2518,6 +2567,12 @@ fn eval_builtin_with_values( }; eval_inet_pton_result(*value, values)? } + "intdiv" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_intdiv_result(*left, *right, values)? + } "ip2long" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -6480,6 +6535,146 @@ fn eval_push_base64_decoded_quartet(quartet: &[Option], output: &mut Vec output.push(((third & 0x03) << 6) | fourth); } +/// Evaluates PHP one-argument floating-point math builtins over one eval expression. +fn eval_builtin_float_unary( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_float_unary_result(name, value, values) +} + +/// Dispatches an evaluated value through the matching PHP floating-point unary math function. +fn eval_float_unary_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let result = match name { + "acos" => value.acos(), + "asin" => value.asin(), + "atan" => value.atan(), + "cos" => value.cos(), + "cosh" => value.cosh(), + "deg2rad" => value.to_radians(), + "exp" => value.exp(), + "log2" => value.log2(), + "log10" => value.log10(), + "rad2deg" => value.to_degrees(), + "sin" => value.sin(), + "sinh" => value.sinh(), + "tan" => value.tan(), + "tanh" => value.tanh(), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.float(result) +} + +/// Evaluates PHP two-argument floating-point math builtins over eval expressions. +fn eval_builtin_float_pair( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_float_pair_result(name, left, right, values) +} + +/// Dispatches an evaluated pair through PHP `atan2()` or `hypot()`. +fn eval_float_pair_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_float_value(left, values)?; + let right = eval_float_value(right, values)?; + let result = match name { + "atan2" => left.atan2(right), + "hypot" => left.hypot(right), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.float(result) +} + +/// Evaluates PHP `log($num, $base = e)` over eval expressions. +fn eval_builtin_log( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [num] => { + let num = eval_expr(num, context, scope, values)?; + eval_log_result(num, None, values) + } + [num, base] => { + let num = eval_expr(num, context, scope, values)?; + let base = eval_expr(base, context, scope, values)?; + eval_log_result(num, Some(base), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `log()` from already evaluated arguments. +fn eval_log_result( + num: RuntimeCellHandle, + base: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + let result = match base { + Some(base) => num.log(eval_float_value(base, values)?), + None => num.ln(), + }; + values.float(result) +} + +/// Evaluates PHP `intdiv(...)` over two eval expressions. +fn eval_builtin_intdiv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_intdiv_result(left, right, values) +} + +/// Computes PHP integer division from already evaluated arguments. +fn eval_intdiv_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_int_value(left, values)?; + let right = eval_int_value(right, values)?; + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; + values.int(result) +} + /// Evaluates PHP floating-point binary math builtins over two eval expressions. fn eval_builtin_float_binary( name: &str, @@ -11509,6 +11704,46 @@ return function_exists("fmod");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval extended scalar math builtins support direct, named, callable, and probe paths. + #[test] + fn execute_program_dispatches_extended_math_builtins() { + let program = parse_fragment( + br#"echo sin(0); echo ":"; +echo cos(0); echo ":"; +echo tan(0); echo ":"; +echo round(asin(1), 2); echo ":"; +echo acos(1); echo ":"; +echo round(atan(1), 2); echo ":"; +echo sinh(0); echo ":"; +echo cosh(0); echo ":"; +echo tanh(0); echo ":"; +echo log2(8); echo ":"; +echo log10(100); echo ":"; +echo exp(0); echo ":"; +echo round(deg2rad(180), 2); echo ":"; +echo round(rad2deg(pi()), 0); echo ":"; +echo log(num: 8, base: 2); echo ":"; +echo atan2(y: 0, x: 1); echo ":"; +echo hypot(3, 4); echo ":"; +echo intdiv(7, 2); echo ":"; +echo round(call_user_func("sin", pi() / 2), 0); echo ":"; +echo call_user_func_array("intdiv", ["num1" => 9, "num2" => 2]); echo ":"; +echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); +return function_exists("hypot");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. #[test] fn execute_program_dispatches_pow_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 0ab668217d..704b204bf4 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -63,7 +63,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index e7a814989e..618bbcd658 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -78,7 +78,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 9b55864801..c4baccd7e8 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -115,6 +115,7 @@ public function __construct(int $value) { $formatted_number = eval('return number_format(1234567.89, 2, ",", ".");'); $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5);'); $circle = eval('return round(pi(), 2);'); +$extended_math = eval('return round(sin(pi() / 2), 0) . ":" . log(8, 2) . ":" . hypot(3, 4) . ":" . intdiv(7, 2);'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); $word_case = eval('return ucwords("hello eval");'); $reversed = eval('return strrev("eval");'); @@ -226,6 +227,7 @@ public function __construct(int $value) { echo "number-format=" . $formatted_number . "\n"; echo "minmax=" . $minmax . "\n"; echo "pi=" . $circle . "\n"; +echo "extended-math=" . $extended_math . "\n"; echo "case=" . $case . "\n"; echo "ucwords=" . $word_case . "\n"; echo "reversed=" . $reversed . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8e3fdac4d3..ab44e9f85e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2115,6 +2115,40 @@ echo ":"; echo function_exists("fdiv"); echo function_exists("fmod");'); assert_eq!(out, "2.5:double:INF:NAN:0.9:4.5:0.9:11"); } +/// Verifies eval extended scalar math builtins through direct, named, callable, and probe paths. +#[test] +fn test_eval_dispatches_extended_math_builtin_calls() { + let out = compile_and_run( + r#" 9, "num2" => 2]); echo ":"; +echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); echo function_exists("hypot");'); +"#, + ); + assert_eq!( + out, + "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:1111" + ); +} + /// Verifies eval `pow()` reuses exponentiation runtime hooks through direct and callable calls. #[test] fn test_eval_dispatches_pow_builtin_call() { From 3488a3f97c83238505723a21af5d32775e641fed Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 10:39:34 +0200 Subject: [PATCH 0168/1208] Support eval float predicate builtins --- crates/elephc-eval/src/interpreter.rs | 31 ++++++++++++++++++++------- docs/internals/the-runtime.md | 4 ++-- docs/php/system-and-io.md | 4 ++-- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 12 +++++++++-- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index a37790210d..36be9b1ca8 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1277,8 +1277,9 @@ fn eval_positional_expr_call( "intdiv" => eval_builtin_intdiv(args, context, scope, values), "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), - "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" - | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { + "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" + | "is_int" | "is_integer" | "is_long" | "is_nan" | "is_null" | "is_numeric" + | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } "ip2long" => eval_builtin_ip2long(args, context, scope, values), @@ -1685,10 +1686,13 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_array" | "is_bool" | "is_double" + | "is_finite" | "is_float" + | "is_infinite" | "is_int" | "is_integer" | "is_long" + | "is_nan" | "is_null" | "is_numeric" | "is_real" @@ -1862,8 +1866,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { Some(&["string"]) } "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" - | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" | "is_numeric" - | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), + | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_long" + | "is_nan" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" + | "is_callable" | "strval" => Some(&["value"]), "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), @@ -2614,8 +2619,9 @@ fn eval_builtin_with_values( } eval_realpath_cache_size_result(values)? } - "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" - | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" => { + "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" + | "is_int" | "is_integer" | "is_long" | "is_nan" | "is_null" | "is_numeric" + | "is_real" | "is_resource" | "is_string" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -6845,6 +6851,9 @@ fn eval_type_predicate_result( "is_null" => tag == EVAL_TAG_NULL, "is_array" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), "is_resource" => tag == EVAL_TAG_RESOURCE, + "is_nan" => eval_float_value(value, values)?.is_nan(), + "is_infinite" => eval_float_value(value, values)?.is_infinite(), + "is_finite" => eval_float_value(value, values)?.is_finite(), "is_numeric" => { tag == EVAL_TAG_INT || tag == EVAL_TAG_FLOAT @@ -11555,11 +11564,17 @@ echo is_numeric("-5"); echo is_numeric("3.14"); echo is_numeric("abc") ? "bad" : "N"; echo is_numeric(true) ? "bad" : "B"; echo is_resource(1) ? "bad" : "R"; +echo is_nan(fdiv(0, 0)) ? "N" : "bad"; +echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; +echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; +echo is_finite(42) ? "F" : "bad"; +echo is_finite(fdiv(1, 0)) ? "bad" : "f"; echo ":"; echo call_user_func("is_string", "x"); echo call_user_func_array("is_array", [[1]]); echo call_user_func("is_numeric", "12"); echo function_exists("is_numeric"); echo function_exists("is_resource"); -return function_exists("is_double");"#, +echo function_exists("is_double"); echo function_exists("is_nan"); echo function_exists("is_finite"); +return function_exists("is_infinite");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -11567,7 +11582,7 @@ return function_exists("is_double");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "11111111111ok11111NBR:11111"); + assert_eq!(values.output, "11111111111ok11111NBRNIiFf:11111111"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 704b204bf4..5af18451f5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -63,7 +63,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 618bbcd658..69702b1baa 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -78,7 +78,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index c4baccd7e8..b882f90b84 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -116,6 +116,7 @@ public function __construct(int $value) { $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5);'); $circle = eval('return round(pi(), 2);'); $extended_math = eval('return round(sin(pi() / 2), 0) . ":" . log(8, 2) . ":" . hypot(3, 4) . ":" . intdiv(7, 2);'); +$float_predicates = eval('return (is_nan(fdiv(0, 0)) ? "nan" : "bad") . ":" . (is_infinite(fdiv(1, 0)) ? "inf" : "bad") . ":" . (is_finite(3.14) ? "finite" : "bad");'); $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); $word_case = eval('return ucwords("hello eval");'); $reversed = eval('return strrev("eval");'); @@ -228,6 +229,7 @@ public function __construct(int $value) { echo "minmax=" . $minmax . "\n"; echo "pi=" . $circle . "\n"; echo "extended-math=" . $extended_math . "\n"; +echo "float-predicates=" . $float_predicates . "\n"; echo "case=" . $case . "\n"; echo "ucwords=" . $word_case . "\n"; echo "reversed=" . $reversed . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ab44e9f85e..1b095604d7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1921,6 +1921,11 @@ echo is_numeric("-5"); echo is_numeric("3.14"); echo is_numeric("abc") ? "bad" : "N"; echo is_numeric(true) ? "bad" : "B"; echo is_resource(1) ? "bad" : "R"; +echo is_nan(fdiv(0, 0)) ? "N" : "bad"; +echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; +echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; +echo is_finite(42) ? "F" : "bad"; +echo is_finite(fdiv(1, 0)) ? "bad" : "f"; echo is_resource($h) ? "H" : "bad"; echo ":"; echo call_user_func("is_string", "x"); @@ -1928,10 +1933,13 @@ echo call_user_func_array("is_array", [[1]]); echo call_user_func("is_numeric", "12"); echo call_user_func("is_resource", $h); echo call_user_func_array("is_resource", [$h]); -echo function_exists("is_double"); echo function_exists("is_numeric"); echo function_exists("is_resource");'); +echo call_user_func("is_nan", fdiv(0, 0)) ? "N" : "bad"; +echo call_user_func_array("is_finite", [42]) ? "F" : "bad"; +echo function_exists("is_double"); echo function_exists("is_numeric"); echo function_exists("is_resource"); +echo function_exists("is_nan"); echo function_exists("is_finite"); echo function_exists("is_infinite");'); "#, ); - assert_eq!(out, "11111111111ok11111NBRH:11111111"); + assert_eq!(out, "11111111111ok11111NBRNIiFfH:11111NF111111"); } /// Verifies eval scalar cast builtins return boxed Mixed cells through direct and callable calls. From 8fd1a0ded94ba8077e91559652b5656256c3eb16 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 10:47:15 +0200 Subject: [PATCH 0169/1208] Support eval array fill builtins --- crates/elephc-eval/src/interpreter.rs | 122 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 25 ++++++ 5 files changed, 151 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 36be9b1ca8..f0bd311ea6 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1192,6 +1192,8 @@ fn eval_positional_expr_call( eval_builtin_slashes(name, args, context, scope, values) } "array_combine" => eval_builtin_array_combine(args, context, scope, values), + "array_fill" => eval_builtin_array_fill(args, context, scope, values), + "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), "array_flip" => eval_builtin_array_flip(args, context, scope, values), "array_keys" | "array_values" => { eval_builtin_array_projection(name, args, context, scope, values) @@ -1590,6 +1592,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { "abs" | "addslashes" | "array_combine" + | "array_fill" + | "array_fill_keys" | "array_flip" | "array_key_exists" | "array_keys" @@ -1852,6 +1856,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { match name { "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), "array_combine" => Some(&["keys", "values"]), + "array_fill" => Some(&["start_index", "count", "value"]), + "array_fill_keys" => Some(&["keys", "value"]), "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), @@ -2098,6 +2104,18 @@ fn eval_builtin_with_values( }; eval_array_combine_result(*keys, *values_array, values)? } + "array_fill" => { + let [start, count, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_result(*start, *count, *value, values)? + } + "array_fill_keys" => { + let [keys, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_keys_result(*keys, *value, values)? + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2844,6 +2862,80 @@ fn eval_array_combine_result( Ok(result) } +/// Evaluates PHP `array_fill()` over start, count, and value expressions. +fn eval_builtin_array_fill( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, count, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let count = eval_expr(count, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_result(start, count, value, values) +} + +/// Builds an `array_fill()` result with PHP's explicit integer key range. +fn eval_array_fill_result( + start: RuntimeCellHandle, + count: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let count = eval_int_value(count, values)?; + if count < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = if start == 0 { + values.array_new(count)? + } else { + values.assoc_new(count)? + }; + for offset in 0..count { + let offset = i64::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = start.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_fill_keys()` over key-array and value expressions. +fn eval_builtin_array_fill_keys( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_keys_result(keys, value, values) +} + +/// Builds an `array_fill_keys()` result preserving the source key iteration order. +fn eval_array_fill_keys_result( + keys: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + /// Evaluates PHP `array_flip()` over one eval array expression. fn eval_builtin_array_flip( args: &[EvalExpr], @@ -10223,6 +10315,36 @@ return function_exists("array_combine");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. + #[test] + fn execute_program_dispatches_array_fill_builtins() { + let program = parse_fragment( + br#"$filled = array_fill(2, 3, "x"); +echo count($filled) . ":" . $filled[2] . $filled[4]; +$negative = array_fill(-2, 3, 7); +echo ":" . $negative[-2] . $negative[-1] . $negative[0]; +$empty = array_fill(5, 0, "x"); +echo ":" . count($empty); +$map = array_fill_keys(["a", "1", "01"], 8); +echo ":" . $map["a"] . ":" . $map[1] . ":" . $map["01"]; +$named = array_fill(start_index: 1, count: 2, value: "n"); +echo ":" . $named[1] . $named[2]; +$call = call_user_func("array_fill", 0, 2, "c"); +echo ":" . $call[0] . $call[1]; +$spread = call_user_func_array("array_fill_keys", [["x", "y"], "z"]); +echo ":" . $spread["x"] . $spread["y"] . ":"; +return function_exists("array_fill") && function_exists("array_fill_keys");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:xx:777:0:8:8:8:nn:cc:zz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. #[test] fn execute_program_dispatches_array_flip_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 5af18451f5..056f58f55b 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 69702b1baa..f9cfc97233 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index b882f90b84..5fafdac48a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -134,6 +134,7 @@ public function __construct(int $value) { $append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); +$array_fill = eval('$filled = array_fill(2, 2, "x"); $map = array_fill_keys(["a", "b"], 7); return $filled[2] . $filled[3] . ":" . $map["b"];'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); @@ -247,6 +248,7 @@ public function __construct(int $value) { echo "append-assoc=" . $append_assoc . "\n"; echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; +echo "array-fill=" . $array_fill . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1b095604d7..8de7565cb9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -757,6 +757,31 @@ echo function_exists("array_combine");'); assert_eq!(out, "10:20:nz:ftd:v:7:8:1"); } +/// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. +#[test] +fn test_eval_dispatches_array_fill_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 10:57:12 +0200 Subject: [PATCH 0170/1208] Support eval array shape builtins --- crates/elephc-eval/src/interpreter.rs | 174 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 25 ++++ 5 files changed, 203 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f0bd311ea6..425361bfb2 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1192,6 +1192,7 @@ fn eval_positional_expr_call( eval_builtin_slashes(name, args, context, scope, values) } "array_combine" => eval_builtin_array_combine(args, context, scope, values), + "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), "array_fill" => eval_builtin_array_fill(args, context, scope, values), "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), "array_flip" => eval_builtin_array_flip(args, context, scope, values), @@ -1202,6 +1203,7 @@ fn eval_positional_expr_call( "array_product" | "array_sum" => { eval_builtin_array_aggregate(name, args, context, scope, values) } + "array_pad" => eval_builtin_array_pad(args, context, scope, values), "array_reverse" => eval_builtin_array_reverse(args, context, scope, values), "array_search" | "in_array" => { eval_builtin_array_search(name, args, context, scope, values) @@ -1591,12 +1593,14 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { name, "abs" | "addslashes" + | "array_chunk" | "array_combine" | "array_fill" | "array_fill_keys" | "array_flip" | "array_key_exists" | "array_keys" + | "array_pad" | "array_product" | "array_reverse" | "array_search" @@ -1855,12 +1859,14 @@ fn collect_contiguous_bound_args( fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { match name { "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), + "array_chunk" => Some(&["array", "length"]), "array_combine" => Some(&["keys", "values"]), "array_fill" => Some(&["start_index", "count", "value"]), "array_fill_keys" => Some(&["keys", "value"]), "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), + "array_pad" => Some(&["array", "length", "value"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" @@ -2104,6 +2110,12 @@ fn eval_builtin_with_values( }; eval_array_combine_result(*keys, *values_array, values)? } + "array_chunk" => { + let [array, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_chunk_result(*array, *length, values)? + } "array_fill" => { let [start, count, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2122,6 +2134,12 @@ fn eval_builtin_with_values( }; eval_array_flip_result(*array, values)? } + "array_pad" => { + let [array, length, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_pad_result(*array, *length, *value, values)? + } "array_product" | "array_sum" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2936,6 +2954,132 @@ fn eval_array_fill_keys_result( Ok(result) } +/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. +fn eval_builtin_array_chunk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_chunk_result(array, length, values) +} + +/// Builds an `array_chunk()` result as nested reindexed arrays. +fn eval_array_chunk_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let chunk_size = eval_int_value(length, values)?; + if chunk_size <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; + let len = values.array_len(array)?; + let chunk_count = len.div_ceil(chunk_size); + let mut result = values.array_new(chunk_count)?; + + for chunk_index in 0..chunk_count { + let start = chunk_index * chunk_size; + let end = usize::min(start + chunk_size, len); + let mut chunk = values.array_new(end - start)?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let value = values.array_get(array, source_key)?; + let target_index = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_index = values.int(target_index)?; + chunk = values.array_set(chunk, target_index, value)?; + } + let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let result_key = values.int(result_key)?; + result = values.array_set(result, result_key, chunk)?; + } + + Ok(result) +} + +/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. +fn eval_builtin_array_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_pad_result(array, length, value, values) +} + +/// Builds an `array_pad()` result by copying values and padding left or right. +fn eval_array_pad_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let target = eval_int_value(length, values)?; + let target_len = target + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + let result_len = usize::max(len, target_len); + let pad_count = result_len.saturating_sub(len); + let mut result = values.array_new(result_len)?; + let mut output_index = 0usize; + + if target < 0 { + let (padded, next_index) = + eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; + result = padded; + output_index = next_index; + } + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + output_index += 1; + } + + if target > 0 { + result = + eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; + } + + Ok(result) +} + +/// Appends the same pad value at consecutive indexed positions in an array result. +fn eval_array_pad_append_repeated( + mut array: RuntimeCellHandle, + start_index: usize, + count: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, usize), EvalStatus> { + let mut next_index = start_index; + for _ in 0..count { + let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + array = values.array_set(array, key, value)?; + next_index += 1; + } + Ok((array, next_index)) +} + /// Evaluates PHP `array_flip()` over one eval array expression. fn eval_builtin_array_flip( args: &[EvalExpr], @@ -10315,6 +10459,36 @@ return function_exists("array_combine");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. + #[test] + fn execute_program_dispatches_array_shape_builtins() { + let program = parse_fragment( + br#"$right = array_pad([1, 2], 5, 0); +echo count($right) . ":" . $right[0] . $right[1] . $right[2] . $right[4]; +$left = array_pad([1, 2], -4, 9); +echo ":" . $left[0] . $left[1] . $left[2] . $left[3]; +$copy = array_pad([7, 8], 1, 0); +echo ":" . count($copy) . ":" . $copy[0] . $copy[1]; +$chunks = array_chunk([1, 2, 3, 4, 5], 2); +echo ":" . count($chunks) . ":" . $chunks[0][1] . $chunks[2][0]; +$named = array_pad(array: ["a"], length: 2, value: "b"); +echo ":" . $named[1]; +$call = call_user_func("array_chunk", [6, 7, 8], 2); +echo ":" . $call[1][0]; +$spread = call_user_func_array("array_pad", [[1], 3, 2]); +echo ":" . $spread[2] . ":"; +return function_exists("array_pad") && function_exists("array_chunk");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:1200:9912:2:78:3:25:b:8:2:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn execute_program_dispatches_array_fill_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 056f58f55b..3d239f8669 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_pad()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index f9cfc97233..502118f0c1 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_pad()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 5fafdac48a..2a77364b3f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -135,6 +135,7 @@ public function __construct(int $value) { $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $array_fill = eval('$filled = array_fill(2, 2, "x"); $map = array_fill_keys(["a", "b"], 7); return $filled[2] . $filled[3] . ":" . $map["b"];'); +$array_shapes = eval('$padded = array_pad([1, 2], 4, 0); $chunks = array_chunk([1, 2, 3], 2); return $padded[3] . ":" . count($chunks);'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); @@ -249,6 +250,7 @@ public function __construct(int $value) { echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "array-fill=" . $array_fill . "\n"; +echo "array-shapes=" . $array_shapes . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8de7565cb9..c389ea8f09 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -757,6 +757,31 @@ echo function_exists("array_combine");'); assert_eq!(out, "10:20:nz:ftd:v:7:8:1"); } +/// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. +#[test] +fn test_eval_dispatches_array_shape_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 11:03:40 +0200 Subject: [PATCH 0171/1208] Support eval array slice builtin --- crates/elephc-eval/src/interpreter.rs | 122 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 27 ++++++ 5 files changed, 153 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 425361bfb2..576792e28e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1208,6 +1208,7 @@ fn eval_positional_expr_call( "array_search" | "in_array" => { eval_builtin_array_search(name, args, context, scope, values) } + "array_slice" => eval_builtin_array_slice(args, context, scope, values), "array_unique" => eval_builtin_array_unique(args, context, scope, values), "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { @@ -1604,6 +1605,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_product" | "array_reverse" | "array_search" + | "array_slice" | "array_sum" | "array_unique" | "array_values" @@ -1869,6 +1871,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_pad" => Some(&["array", "length", "value"]), "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), + "array_slice" => Some(&["array", "offset", "length"]), "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), "atan2" => Some(&["y", "x"]), @@ -2172,6 +2175,13 @@ fn eval_builtin_with_values( }; eval_array_search_result(name, *needle, *array, values)? } + "array_slice" => match evaluated_args { + [array, offset] => eval_array_slice_result(*array, *offset, None, values)?, + [array, offset, length] => { + eval_array_slice_result(*array, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "array_unique" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3004,6 +3014,86 @@ fn eval_array_chunk_result( Ok(result) } +/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. +fn eval_builtin_array_slice( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array, offset] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_array_slice_result(array, offset, None, values) + } + [array, offset, length] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_slice_result(array, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_slice()` result with PHP offset and length bounds. +fn eval_array_slice_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + + let mut result = values.array_new(end.saturating_sub(start))?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = i64::try_from(source_position - start) + .map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + +/// Converts a PHP array-slice offset into a bounded source position. +fn eval_slice_start(len: usize, offset: i64) -> Result { + if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(offset, len)); + } + + let tail = offset + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(len.saturating_sub(tail)) +} + +/// Converts a PHP array-slice length into a bounded exclusive end position. +fn eval_slice_end(len: usize, start: usize, length: i64) -> Result { + if length >= 0 { + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(start.saturating_add(length), len)); + } + + let tail = length + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(usize::max(start, len.saturating_sub(tail))) +} + /// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. fn eval_builtin_array_pad( args: &[EvalExpr], @@ -10489,6 +10579,38 @@ return function_exists("array_pad") && function_exists("array_chunk");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_slice()` observes PHP offset and length bounds. + #[test] + fn execute_program_dispatches_array_slice_builtin() { + let program = parse_fragment( + br#"$mid = array_slice([10, 20, 30, 40, 50], 1, 3); +echo count($mid) . ":" . $mid[0] . $mid[1] . $mid[2]; +$tail = array_slice([10, 20, 30, 40], -2, 1); +echo ":" . $tail[0]; +$open = array_slice([10, 20, 30, 40, 50], 2); +echo ":" . count($open) . ":" . $open[0] . $open[2]; +$null_len = array_slice([5, 6, 7], 1, null); +echo ":" . $null_len[0] . $null_len[1]; +$negative_len = array_slice([10, 20, 30, 40, 50], 1, -1); +echo ":" . count($negative_len) . ":" . $negative_len[0] . $negative_len[2]; +$named = array_slice(array: [1, 2, 3], offset: 1, length: 1); +echo ":" . $named[0]; +$call = call_user_func("array_slice", [6, 7, 8], 1, 2); +echo ":" . $call[1]; +$spread = call_user_func_array("array_slice", [[9, 10, 11], 1]); +echo ":" . $spread[0] . ":"; +return function_exists("array_slice");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:203040:30:3:3050:67:3:2040:2:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn execute_program_dispatches_array_fill_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3d239f8669..00edeec427 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_pad()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 502118f0c1..14ca308017 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_pad()`, `array_reverse()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 2a77364b3f..e3f7393a34 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -136,6 +136,7 @@ public function __construct(int $value) { $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $array_fill = eval('$filled = array_fill(2, 2, "x"); $map = array_fill_keys(["a", "b"], 7); return $filled[2] . $filled[3] . ":" . $map["b"];'); $array_shapes = eval('$padded = array_pad([1, 2], 4, 0); $chunks = array_chunk([1, 2, 3], 2); return $padded[3] . ":" . count($chunks);'); +$array_slice = eval('$slice = array_slice([10, 20, 30], 1); return count($slice) . ":" . $slice[0];'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); @@ -251,6 +252,7 @@ public function __construct(int $value) { echo "array-search=" . $array_search . "\n"; echo "array-fill=" . $array_fill . "\n"; echo "array-shapes=" . $array_shapes . "\n"; +echo "array-slice=" . $array_slice . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c389ea8f09..6262717e30 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -782,6 +782,33 @@ echo function_exists("array_pad"); echo function_exists("array_chunk");'); assert_eq!(out, "5:1200:9912:2:78:3:25:b:8:2:11"); } +/// Verifies eval `array_slice()` observes PHP offset and length bounds. +#[test] +fn test_eval_dispatches_array_slice_builtin_call() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 11:10:45 +0200 Subject: [PATCH 0172/1208] Support eval array merge builtin --- crates/elephc-eval/src/interpreter.rs | 91 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 23 +++++++ 5 files changed, 118 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 576792e28e..690198c44d 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1200,6 +1200,7 @@ fn eval_positional_expr_call( eval_builtin_array_projection(name, args, context, scope, values) } "array_key_exists" => eval_builtin_array_key_exists(args, context, scope, values), + "array_merge" => eval_builtin_array_merge(args, context, scope, values), "array_product" | "array_sum" => { eval_builtin_array_aggregate(name, args, context, scope, values) } @@ -1601,6 +1602,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_flip" | "array_key_exists" | "array_keys" + | "array_merge" | "array_pad" | "array_product" | "array_reverse" @@ -2161,6 +2163,12 @@ fn eval_builtin_with_values( }; values.array_key_exists(*key, *array)? } + "array_merge" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_merge_result(*left, *right, values)? + } "array_reverse" => match evaluated_args { [array] => eval_array_reverse_result(*array, false, values)?, [array, preserve_keys] => { @@ -3389,6 +3397,61 @@ fn eval_array_search_result( } } +/// Evaluates PHP `array_merge()` over two array expressions. +fn eval_builtin_array_merge( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_merge_result(left, right, values) +} + +/// Builds an `array_merge()` result with PHP numeric reindexing and string-key overwrites. +fn eval_array_merge_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let capacity = left_len.checked_add(right_len).ok_or(EvalStatus::RuntimeFatal)?; + let mut result = values.assoc_new(capacity)?; + let mut next_numeric_key = 0_i64; + result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; + eval_array_merge_append_operand(result, right, &mut next_numeric_key, values) +} + +/// Appends one source array to an `array_merge()` result using PHP key handling. +fn eval_array_merge_append_operand( + mut result: RuntimeCellHandle, + source: RuntimeCellHandle, + next_numeric_key: &mut i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(source)?; + for position in 0..len { + let source_key = values.array_iter_key(source, position)?; + let source_value = values.array_get(source, source_key)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_STRING { + source_key + } else { + let target_key = values.int(*next_numeric_key)?; + *next_numeric_key = (*next_numeric_key) + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + target_key + }; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + /// Evaluates PHP `explode()` over separator and string expressions. fn eval_builtin_explode( args: &[EvalExpr], @@ -10611,6 +10674,34 @@ return function_exists("array_slice");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. + #[test] + fn execute_program_dispatches_array_merge_builtin() { + let program = parse_fragment( + br#"$merged = array_merge([1, 2], [3, 4]); +echo count($merged) . ":" . $merged[0] . $merged[1] . $merged[2] . $merged[3]; +$left = [1, 2]; +$right = [3]; +$copy = array_merge($left, $right); +echo ":" . count($left) . ":" . $left[0] . ":" . $copy[2]; +$assoc = array_merge(["a" => 1, 2 => "x"], ["a" => 9, 5 => "y", "b" => 3]); +echo ":" . $assoc["a"] . ":" . $assoc[0] . ":" . $assoc[1] . ":" . $assoc["b"]; +$call = call_user_func("array_merge", [6], [7, 8]); +echo ":" . $call[2]; +$spread = call_user_func_array("array_merge", [[9], [10]]); +echo ":" . $spread[1] . ":"; +return function_exists("array_merge");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:1234:2:1:3:9:x:y:3:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn execute_program_dispatches_array_fill_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 00edeec427..8d28fb5085 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 14ca308017..b200118bea 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index e3f7393a34..8e743c29e7 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -137,6 +137,7 @@ public function __construct(int $value) { $array_fill = eval('$filled = array_fill(2, 2, "x"); $map = array_fill_keys(["a", "b"], 7); return $filled[2] . $filled[3] . ":" . $map["b"];'); $array_shapes = eval('$padded = array_pad([1, 2], 4, 0); $chunks = array_chunk([1, 2, 3], 2); return $padded[3] . ":" . count($chunks);'); $array_slice = eval('$slice = array_slice([10, 20, 30], 1); return count($slice) . ":" . $slice[0];'); +$array_merge = eval('$merged = array_merge([1, 2], [3]); return count($merged) . ":" . $merged[2];'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); @@ -253,6 +254,7 @@ public function __construct(int $value) { echo "array-fill=" . $array_fill . "\n"; echo "array-shapes=" . $array_shapes . "\n"; echo "array-slice=" . $array_slice . "\n"; +echo "array-merge=" . $array_merge . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6262717e30..45aad9e602 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -809,6 +809,29 @@ echo function_exists("array_slice");'); assert_eq!(out, "3:203040:30:3:3050:67:3:2040:2:8:10:1"); } +/// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. +#[test] +fn test_eval_dispatches_array_merge_builtin_call() { + let out = compile_and_run( + r#" 1, 2 => "x"], ["a" => 9, 5 => "y", "b" => 3]); +echo ":" . $assoc["a"] . ":" . $assoc[0] . ":" . $assoc[1] . ":" . $assoc["b"]; +$call = call_user_func("array_merge", [6], [7, 8]); +echo ":" . $call[2]; +$spread = call_user_func_array("array_merge", [[9], [10]]); +echo ":" . $spread[1] . ":"; +echo function_exists("array_merge");'); +"#, + ); + assert_eq!(out, "4:1234:2:1:3:9:x:y:3:8:10:1"); +} + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn test_eval_dispatches_array_fill_builtin_calls() { From 6c529632e83351808449d4acf5b5a669fc347745 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 11:24:23 +0200 Subject: [PATCH 0173/1208] Support eval array set builtins --- crates/elephc-eval/src/interpreter.rs | 87 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 31 +++++++--- 5 files changed, 114 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 690198c44d..7579c21e92 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1200,6 +1200,9 @@ fn eval_positional_expr_call( eval_builtin_array_projection(name, args, context, scope, values) } "array_key_exists" => eval_builtin_array_key_exists(args, context, scope, values), + "array_diff" | "array_intersect" => { + eval_builtin_array_value_set(name, args, context, scope, values) + } "array_merge" => eval_builtin_array_merge(args, context, scope, values), "array_product" | "array_sum" => { eval_builtin_array_aggregate(name, args, context, scope, values) @@ -1602,6 +1605,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_flip" | "array_key_exists" | "array_keys" + | "array_diff" + | "array_intersect" | "array_merge" | "array_pad" | "array_product" @@ -2163,6 +2168,12 @@ fn eval_builtin_with_values( }; values.array_key_exists(*key, *array)? } + "array_diff" | "array_intersect" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_value_set_result(name, *left, *right, values)? + } "array_merge" => { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3397,6 +3408,56 @@ fn eval_array_search_result( } } +/// Evaluates PHP value-set array builtins over two eval array expressions. +fn eval_builtin_array_value_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_value_set_result(name, left, right, values) +} + +/// Builds `array_diff()` or `array_intersect()` using PHP's default string comparison mode. +fn eval_array_value_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let mut right_values = Vec::with_capacity(right_len); + for position in 0..right_len { + let key = values.array_iter_key(right, position)?; + let value = values.array_get(right, key)?; + right_values.push(values.string_bytes(value)?); + } + + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let comparable = values.string_bytes(value)?; + let found = right_values.iter().any(|right_value| right_value == &comparable); + let keep = match name { + "array_diff" => !found, + "array_intersect" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + /// Evaluates PHP `array_merge()` over two array expressions. fn eval_builtin_array_merge( args: &[EvalExpr], @@ -10702,6 +10763,32 @@ return function_exists("array_merge");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_diff()` and `array_intersect()` compare values as strings. + #[test] + fn execute_program_dispatches_array_value_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); +echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; +echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); +echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); +$inter = array_intersect(["a" => 1, "b" => 2, "c" => "2", "d" => 3], ["2", 4]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter["c"]; +$call = call_user_func("array_diff", [1, 2, 3], [2]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); +echo ":" . count($spread) . ":" . $spread[2] . ":"; +return function_exists("array_diff") && function_exists("array_intersect");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn execute_program_dispatches_array_fill_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 8d28fb5085..8328ca9fc7 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b200118bea..4ab574ca4a 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 8e743c29e7..903eedbf18 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -138,6 +138,7 @@ public function __construct(int $value) { $array_shapes = eval('$padded = array_pad([1, 2], 4, 0); $chunks = array_chunk([1, 2, 3], 2); return $padded[3] . ":" . count($chunks);'); $array_slice = eval('$slice = array_slice([10, 20, 30], 1); return count($slice) . ":" . $slice[0];'); $array_merge = eval('$merged = array_merge([1, 2], [3]); return count($merged) . ":" . $merged[2];'); +$array_sets = eval('$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); $inter = array_intersect(["a" => 1, "b" => 2, "c" => "2"], ["2"]); return count($diff) . ":" . count($inter) . ":" . $inter["b"];'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); @@ -255,6 +256,7 @@ public function __construct(int $value) { echo "array-shapes=" . $array_shapes . "\n"; echo "array-slice=" . $array_slice . "\n"; echo "array-merge=" . $array_merge . "\n"; +echo "array-sets=" . $array_sets . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 45aad9e602..bc228de239 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -832,6 +832,27 @@ echo function_exists("array_merge");'); assert_eq!(out, "4:1234:2:1:3:9:x:y:3:8:10:1"); } +/// Verifies eval `array_diff()` and `array_intersect()` preserve left keys and compare by string value. +#[test] +fn test_eval_dispatches_array_value_set_builtin_calls() { + let out = compile_and_run( + r#" 1, "b" => 2, "c" => "2", "d" => 3], [2]); +echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; +echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); +echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); +$inter = array_intersect(["a" => 1, "b" => 2, "c" => "2", "d" => 3], ["2", 4]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter["c"]; +$call = call_user_func("array_diff", [1, 2, 3], [2]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); +echo ":" . count($spread) . ":" . $spread[2] . ":"; +echo function_exists("array_diff"); echo function_exists("array_intersect");'); +"#, + ); + assert_eq!(out, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:11"); +} + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn test_eval_dispatches_array_fill_builtin_calls() { @@ -1266,10 +1287,7 @@ echo ":"; echo function_exists("ctype_alnum"); echo ":"; echo function_exists("ctype_space");'); "#, ); - assert_eq!( - out, - "A:D:N:S:empty:not-digit:not-space:1:1:1:1" - ); + assert_eq!(out, "A:D:N:S:empty:not-digit:not-space:1:1:1:1"); } /// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. @@ -1651,10 +1669,7 @@ echo function_exists("stat"); echo function_exists("lstat"); '); "#, ); - assert_eq!( - out, - "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" - ); + assert_eq!(out, "stat:mode:lstat:missing:callstat:calllstat:cleanup:11"); } /// Verifies eval path operation builtins mutate local filesystem state. From 3f3462b851b4b411690686395cb538152e869d2b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 11:28:53 +0200 Subject: [PATCH 0174/1208] Support eval array key set builtins --- crates/elephc-eval/src/interpreter.rs | 78 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 20 +++++++ 5 files changed, 102 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7579c21e92..437aa3ef00 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1203,6 +1203,9 @@ fn eval_positional_expr_call( "array_diff" | "array_intersect" => { eval_builtin_array_value_set(name, args, context, scope, values) } + "array_diff_key" | "array_intersect_key" => { + eval_builtin_array_key_set(name, args, context, scope, values) + } "array_merge" => eval_builtin_array_merge(args, context, scope, values), "array_product" | "array_sum" => { eval_builtin_array_aggregate(name, args, context, scope, values) @@ -1607,6 +1610,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_keys" | "array_diff" | "array_intersect" + | "array_diff_key" + | "array_intersect_key" | "array_merge" | "array_pad" | "array_product" @@ -2174,6 +2179,12 @@ fn eval_builtin_with_values( }; eval_array_value_set_result(name, *left, *right, values)? } + "array_diff_key" | "array_intersect_key" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_key_set_result(name, *left, *right, values)? + } "array_merge" => { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3458,6 +3469,48 @@ fn eval_array_value_set_result( Ok(result) } +/// Evaluates PHP key-set array builtins over two eval array expressions. +fn eval_builtin_array_key_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_key_set_result(name, left, right, values) +} + +/// Builds `array_diff_key()` or `array_intersect_key()` by testing first-array keys. +fn eval_array_key_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let exists = values.array_key_exists(key, right)?; + let found = values.truthy(exists)?; + let keep = match name { + "array_diff_key" => !found, + "array_intersect_key" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + /// Evaluates PHP `array_merge()` over two array expressions. fn eval_builtin_array_merge( args: &[EvalExpr], @@ -10789,6 +10842,31 @@ return function_exists("array_diff") && function_exists("array_intersect");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. + #[test] + fn execute_program_dispatches_array_key_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff_key(["a" => 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); +echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; +echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); +$inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter[4]; +$call = call_user_func("array_diff_key", [10, 20, 30], [1 => 0]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); +echo ":" . count($spread) . ":" . $spread["y"] . ":"; +return function_exists("array_diff_key") && function_exists("array_intersect_key");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:2:3:no-a:2:2:3:2:1030:1:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn execute_program_dispatches_array_fill_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 8328ca9fc7..45ca960d9c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4ab574ca4a..fef3595e74 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 903eedbf18..826950ef96 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -139,6 +139,7 @@ public function __construct(int $value) { $array_slice = eval('$slice = array_slice([10, 20, 30], 1); return count($slice) . ":" . $slice[0];'); $array_merge = eval('$merged = array_merge([1, 2], [3]); return count($merged) . ":" . $merged[2];'); $array_sets = eval('$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); $inter = array_intersect(["a" => 1, "b" => 2, "c" => "2"], ["2"]); return count($diff) . ":" . count($inter) . ":" . $inter["b"];'); +$array_key_sets = eval('$diff = array_diff_key(["a" => 1, "b" => 2], ["a" => 0]); $inter = array_intersect_key(["x" => 7, "y" => 8], ["y" => 0]); return count($diff) . ":" . $diff["b"] . ":" . $inter["y"];'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); @@ -257,6 +258,7 @@ public function __construct(int $value) { echo "array-slice=" . $array_slice . "\n"; echo "array-merge=" . $array_merge . "\n"; echo "array-sets=" . $array_sets . "\n"; +echo "array-key-sets=" . $array_key_sets . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bc228de239..efb43354a8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -853,6 +853,26 @@ echo function_exists("array_diff"); echo function_exists("array_intersect");'); assert_eq!(out, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:11"); } +/// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. +#[test] +fn test_eval_dispatches_array_key_set_builtin_calls() { + let out = compile_and_run( + r#" 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); +echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; +echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); +$inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter[4]; +$call = call_user_func("array_diff_key", [10, 20, 30], [1 => 0]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); +echo ":" . count($spread) . ":" . $spread["y"] . ":"; +echo function_exists("array_diff_key"); echo function_exists("array_intersect_key");'); +"#, + ); + assert_eq!(out, "2:2:3:no-a:2:2:3:2:1030:1:8:11"); +} + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn test_eval_dispatches_array_fill_builtin_calls() { From cce3cebfc0c664890ede3f6b5f5707608cdacac0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 11:34:03 +0200 Subject: [PATCH 0175/1208] Support eval range builtin --- crates/elephc-eval/src/interpreter.rs | 83 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 23 ++++++++ 5 files changed, 110 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 437aa3ef00..f0067c1376 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1308,6 +1308,7 @@ fn eval_positional_expr_call( "phpversion" => eval_builtin_phpversion(args, values), "pow" => eval_builtin_pow(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), + "range" => eval_builtin_range(args, context, scope, values), "rawurldecode" | "urldecode" => { eval_builtin_url_decode(name, args, context, scope, values) } @@ -1738,6 +1739,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "pow" | "phpversion" | "putenv" + | "range" | "rad2deg" | "rawurldecode" | "rawurlencode" @@ -1949,6 +1951,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), "putenv" => Some(&["assignment"]), + "range" => Some(&["start", "end"]), "realpath" => Some(&["path"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), @@ -2218,6 +2221,12 @@ fn eval_builtin_with_values( }; eval_array_unique_result(*array, values)? } + "range" => { + let [start, end] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_range_result(*start, *end, values)? + } "base64_encode" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3511,6 +3520,52 @@ fn eval_array_key_set_result( Ok(result) } +/// Evaluates PHP `range()` over integer-compatible start and end expressions. +fn eval_builtin_range( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, end] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let end = eval_expr(end, context, scope, values)?; + eval_range_result(start, end, values) +} + +/// Builds an inclusive ascending or descending integer `range()` result. +fn eval_range_result( + start: RuntimeCellHandle, + end: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let end = eval_int_value(end, values)?; + let distance = if start <= end { + end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? + } else { + start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? + }; + let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let step = if start <= end { 1_i64 } else { -1_i64 }; + let mut current = start; + let mut result = values.array_new(count)?; + + for index in 0..count { + let key = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + let value = values.int(current)?; + result = values.array_set(result, key, value)?; + if index + 1 < count { + current = current.checked_add(step).ok_or(EvalStatus::RuntimeFatal)?; + } + } + Ok(result) +} + /// Evaluates PHP `array_merge()` over two array expressions. fn eval_builtin_array_merge( args: &[EvalExpr], @@ -10867,6 +10922,34 @@ return function_exists("array_diff_key") && function_exists("array_intersect_key assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `range()` builds inclusive ascending and descending integer arrays. + #[test] + fn execute_program_dispatches_range_builtin() { + let program = parse_fragment( + br#"$up = range(1, 4); +echo count($up) . ":" . $up[0] . $up[3]; +$down = range(4, 1); +echo ":" . count($down) . ":" . $down[0] . $down[3]; +$single = range(3, 3); +echo ":" . count($single) . ":" . $single[0]; +$named = range(start: 2, end: 4); +echo ":" . $named[0] . $named[2]; +$call = call_user_func("range", 5, 7); +echo ":" . $call[2]; +$spread = call_user_func_array("range", [8, 6]); +echo ":" . count($spread) . ":" . $spread[0] . $spread[2] . ":"; +return function_exists("range");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:14:4:41:1:3:24:7:3:86:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn execute_program_dispatches_array_fill_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 45ca960d9c..484d522b50 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index fef3595e74..1997cdac3b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 826950ef96..c57c9d340c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -140,6 +140,7 @@ public function __construct(int $value) { $array_merge = eval('$merged = array_merge([1, 2], [3]); return count($merged) . ":" . $merged[2];'); $array_sets = eval('$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); $inter = array_intersect(["a" => 1, "b" => 2, "c" => "2"], ["2"]); return count($diff) . ":" . count($inter) . ":" . $inter["b"];'); $array_key_sets = eval('$diff = array_diff_key(["a" => 1, "b" => 2], ["a" => 0]); $inter = array_intersect_key(["x" => 7, "y" => 8], ["y" => 0]); return count($diff) . ":" . $diff["b"] . ":" . $inter["y"];'); +$range = eval('$up = range(1, 3); $down = range(3, 1); return count($up) . ":" . $up[2] . ":" . $down[2];'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); @@ -259,6 +260,7 @@ public function __construct(int $value) { echo "array-merge=" . $array_merge . "\n"; echo "array-sets=" . $array_sets . "\n"; echo "array-key-sets=" . $array_key_sets . "\n"; +echo "range=" . $range . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index efb43354a8..ea06e648d8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -873,6 +873,29 @@ echo function_exists("array_diff_key"); echo function_exists("array_intersect_ke assert_eq!(out, "2:2:3:no-a:2:2:3:2:1030:1:8:11"); } +/// Verifies eval `range()` builds inclusive ascending and descending integer arrays. +#[test] +fn test_eval_dispatches_range_builtin_call() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 11:37:48 +0200 Subject: [PATCH 0176/1208] Support eval array column builtin --- crates/elephc-eval/src/interpreter.rs | 81 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 24 ++++++++ 5 files changed, 109 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f0067c1376..d211aae08f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1193,6 +1193,7 @@ fn eval_positional_expr_call( } "array_combine" => eval_builtin_array_combine(args, context, scope, values), "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), + "array_column" => eval_builtin_array_column(args, context, scope, values), "array_fill" => eval_builtin_array_fill(args, context, scope, values), "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), "array_flip" => eval_builtin_array_flip(args, context, scope, values), @@ -1603,6 +1604,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { "abs" | "addslashes" | "array_chunk" + | "array_column" | "array_combine" | "array_fill" | "array_fill_keys" @@ -1876,6 +1878,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { match name { "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), "array_chunk" => Some(&["array", "length"]), + "array_column" => Some(&["array", "column_key"]), "array_combine" => Some(&["keys", "values"]), "array_fill" => Some(&["start_index", "count", "value"]), "array_fill_keys" => Some(&["keys", "value"]), @@ -2128,6 +2131,12 @@ fn eval_builtin_with_values( }; eval_array_combine_result(*keys, *values_array, values)? } + "array_column" => { + let [array, column_key] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_column_result(*array, *column_key, values)? + } "array_chunk" => { let [array, length] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -2929,6 +2938,50 @@ fn eval_array_combine_result( Ok(result) } +/// Evaluates PHP `array_column()` over row-array and column-key expressions. +fn eval_builtin_array_column( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, column_key] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let column_key = eval_expr(column_key, context, scope, values)?; + eval_array_column_result(array, column_key, values) +} + +/// Builds `array_column()` by extracting present row columns into a reindexed array. +fn eval_array_column_result( + array: RuntimeCellHandle, + column_key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + let mut output_index = 0_i64; + for position in 0..len { + let row_key = values.array_iter_key(array, position)?; + let row = values.array_get(array, row_key)?; + if !matches!(values.type_tag(row)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + continue; + } + let exists = values.array_key_exists(column_key, row)?; + if !values.truthy(exists)? { + continue; + } + let column = values.array_get(row, column_key)?; + let target_key = values.int(output_index)?; + output_index = output_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, column)?; + } + Ok(result) +} + /// Evaluates PHP `array_fill()` over start, count, and value expressions. fn eval_builtin_array_fill( args: &[EvalExpr], @@ -10781,6 +10834,34 @@ return function_exists("array_combine");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_column()` extracts present row columns and reindexes them. + #[test] + fn execute_program_dispatches_array_column_builtin() { + let program = parse_fragment( + br#"$rows = [["name" => "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; +$names = array_column($rows, "name"); +echo count($names) . ":" . $names[0] . ":" . $names[1]; +$scores = array_column($rows, "score"); +echo ":" . count($scores) . ":" . $scores[0] . $scores[2]; +$numeric = array_column([[0 => "zero", 1 => "one"], [1 => "uno"]], 1); +echo ":" . count($numeric) . ":" . $numeric[0] . ":" . $numeric[1]; +$named = array_column(array: $rows, column_key: "score"); +echo ":" . $named[1]; +$call = call_user_func("array_column", [["x" => 5], ["x" => 6]], "x"); +echo ":" . $call[1]; +$spread = call_user_func_array("array_column", [[["y" => 7], ["z" => 0], ["y" => 9]], "y"]); +echo ":" . count($spread) . ":" . $spread[1] . ":"; +return function_exists("array_column");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. #[test] fn execute_program_dispatches_array_shape_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 484d522b50..d35e0ffd33 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 1997cdac3b..97bac00f18 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index c57c9d340c..c6b7d8d51a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -135,6 +135,7 @@ public function __construct(int $value) { $array_key_probe = eval('$m = ["name" => null]; return (array_key_exists("name", $m) ? "present" : "missing") . ":" . (array_key_exists("age", $m) ? "bad" : "absent");'); $array_search = eval('return (in_array("b", ["a", "b"]) ? "in" : "missing") . ":" . array_search("Grace", ["name" => "Grace"]);'); $array_fill = eval('$filled = array_fill(2, 2, "x"); $map = array_fill_keys(["a", "b"], 7); return $filled[2] . $filled[3] . ":" . $map["b"];'); +$array_column = eval('$rows = [["name" => "Ada"], ["name" => "Lin"]]; $names = array_column($rows, "name"); return count($names) . ":" . $names[0] . ":" . $names[1];'); $array_shapes = eval('$padded = array_pad([1, 2], 4, 0); $chunks = array_chunk([1, 2, 3], 2); return $padded[3] . ":" . count($chunks);'); $array_slice = eval('$slice = array_slice([10, 20, 30], 1); return count($slice) . ":" . $slice[0];'); $array_merge = eval('$merged = array_merge([1, 2], [3]); return count($merged) . ":" . $merged[2];'); @@ -255,6 +256,7 @@ public function __construct(int $value) { echo "array-key-exists=" . $array_key_probe . "\n"; echo "array-search=" . $array_search . "\n"; echo "array-fill=" . $array_fill . "\n"; +echo "array-column=" . $array_column . "\n"; echo "array-shapes=" . $array_shapes . "\n"; echo "array-slice=" . $array_slice . "\n"; echo "array-merge=" . $array_merge . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ea06e648d8..e1ffa33a27 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -757,6 +757,30 @@ echo function_exists("array_combine");'); assert_eq!(out, "10:20:nz:ftd:v:7:8:1"); } +/// Verifies eval `array_column()` extracts present row columns and reindexes them. +#[test] +fn test_eval_dispatches_array_column_builtin_call() { + let out = compile_and_run( + r#" "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; +$names = array_column($rows, "name"); +echo count($names) . ":" . $names[0] . ":" . $names[1]; +$scores = array_column($rows, "score"); +echo ":" . count($scores) . ":" . $scores[0] . $scores[2]; +$numeric = array_column([[0 => "zero", 1 => "one"], [1 => "uno"]], 1); +echo ":" . count($numeric) . ":" . $numeric[0] . ":" . $numeric[1]; +$named = array_column(array: $rows, column_key: "score"); +echo ":" . $named[1]; +$call = call_user_func("array_column", [["x" => 5], ["x" => 6]], "x"); +echo ":" . $call[1]; +$spread = call_user_func_array("array_column", [[["y" => 7], ["z" => 0], ["y" => 9]], "y"]); +echo ":" . count($spread) . ":" . $spread[1] . ":"; +echo function_exists("array_column");'); +"#, + ); + assert_eq!(out, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:1"); +} + /// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. #[test] fn test_eval_dispatches_array_shape_builtin_calls() { From 696159175ae1adea6a282fe572426ec0f53b2cbe Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 11:42:04 +0200 Subject: [PATCH 0177/1208] Support eval array rand builtin --- crates/elephc-eval/src/interpreter.rs | 75 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 23 ++++++++ 5 files changed, 101 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d211aae08f..f0094b2a60 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -22,6 +22,7 @@ use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; use std::net::ToSocketAddrs; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +use std::time::{SystemTime, UNIX_EPOCH}; /// Internal statement-control result used to propagate eval returns and loops. enum EvalControl { @@ -1212,6 +1213,7 @@ fn eval_positional_expr_call( eval_builtin_array_aggregate(name, args, context, scope, values) } "array_pad" => eval_builtin_array_pad(args, context, scope, values), + "array_rand" => eval_builtin_array_rand(args, context, scope, values), "array_reverse" => eval_builtin_array_reverse(args, context, scope, values), "array_search" | "in_array" => { eval_builtin_array_search(name, args, context, scope, values) @@ -1618,6 +1620,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_merge" | "array_pad" | "array_product" + | "array_rand" | "array_reverse" | "array_search" | "array_slice" @@ -1883,7 +1886,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_fill" => Some(&["start_index", "count", "value"]), "array_fill_keys" => Some(&["keys", "value"]), "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" - | "array_values" => Some(&["array"]), + | "array_rand" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), "array_pad" => Some(&["array", "length", "value"]), "array_reverse" => Some(&["array", "preserve_keys"]), @@ -2203,6 +2206,12 @@ fn eval_builtin_with_values( }; eval_array_merge_result(*left, *right, values)? } + "array_rand" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_rand_result(*array, values)? + } "array_reverse" => match evaluated_args { [array] => eval_array_reverse_result(*array, false, values)?, [array, preserve_keys] => { @@ -3573,6 +3582,42 @@ fn eval_array_key_set_result( Ok(result) } +/// Evaluates PHP `array_rand()` over one eval array expression. +fn eval_builtin_array_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_rand_result(array, values) +} + +/// Returns a valid random key from a non-empty eval array. +fn eval_array_rand_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if len == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let position = eval_random_position(len); + values.array_iter_key(array, position) +} + +/// Chooses a pseudo-random array position within `[0, len)`. +fn eval_random_position(len: usize) -> usize { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + (nanos % (len as u128)) as usize +} + /// Evaluates PHP `range()` over integer-compatible start and end expressions. fn eval_builtin_range( args: &[EvalExpr], @@ -11031,6 +11076,34 @@ return function_exists("range");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_rand()` returns a key that exists in the source array. + #[test] + fn execute_program_dispatches_array_rand_builtin() { + let program = parse_fragment( + br#"$nums = [10, 20, 30]; +$idx = array_rand($nums); +echo ($idx >= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; +$assoc = ["a" => 1, "b" => 2]; +$key = array_rand($assoc); +echo ":" . (array_key_exists($key, $assoc) ? "assoc" : "bad"); +$named = array_rand(array: [5, 6]); +echo ":" . (($named >= 0 && $named < 2) ? "named" : "bad"); +$call = call_user_func("array_rand", [7, 8]); +echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); +$spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); +echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; +return function_exists("array_rand");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "idx:assoc:named:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn execute_program_dispatches_array_fill_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d35e0ffd33..b64d355b84 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 97bac00f18..532adc595d 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index c6b7d8d51a..6af1fd8db3 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -142,6 +142,7 @@ public function __construct(int $value) { $array_sets = eval('$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); $inter = array_intersect(["a" => 1, "b" => 2, "c" => "2"], ["2"]); return count($diff) . ":" . count($inter) . ":" . $inter["b"];'); $array_key_sets = eval('$diff = array_diff_key(["a" => 1, "b" => 2], ["a" => 0]); $inter = array_intersect_key(["x" => 7, "y" => 8], ["y" => 0]); return count($diff) . ":" . $diff["b"] . ":" . $inter["y"];'); $range = eval('$up = range(1, 3); $down = range(3, 1); return count($up) . ":" . $up[2] . ":" . $down[2];'); +$array_rand = eval('$items = ["a" => 1, "b" => 2]; $key = array_rand($items); return array_key_exists($key, $items) ? "valid" : "bad";'); $string_compare = eval('return (strcmp("abc", "abd") < 0 ? "lt" : "bad") . ":" . (strcasecmp("Hello", "hello") === 0 ? "ci" : "bad") . ":" . (hash_equals("abc", "abc") ? "hash" : "bad");'); $ctype_checks = eval('return (ctype_alpha("abc") ? "alpha" : "bad") . ":" . (ctype_digit("123") ? "digit" : "bad") . ":" . (ctype_space(" \t\n") ? "space" : "bad");'); $slashes = eval('return addslashes("A\"B") . ":" . stripslashes(addslashes("A\"B"));'); @@ -263,6 +264,7 @@ public function __construct(int $value) { echo "array-sets=" . $array_sets . "\n"; echo "array-key-sets=" . $array_key_sets . "\n"; echo "range=" . $range . "\n"; +echo "array-rand=" . $array_rand . "\n"; echo "string-compare=" . $string_compare . "\n"; echo "ctype=" . $ctype_checks . "\n"; echo "slashes=" . $slashes . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e1ffa33a27..f16deec115 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -920,6 +920,29 @@ echo function_exists("range");'); assert_eq!(out, "4:14:4:41:1:3:24:7:3:86:1"); } +/// Verifies eval `array_rand()` returns a key that exists in the source array. +#[test] +fn test_eval_dispatches_array_rand_builtin_call() { + let out = compile_and_run( + r#"= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; +$assoc = ["a" => 1, "b" => 2]; +$key = array_rand($assoc); +echo ":" . (array_key_exists($key, $assoc) ? "assoc" : "bad"); +$named = array_rand(array: [5, 6]); +echo ":" . (($named >= 0 && $named < 2) ? "named" : "bad"); +$call = call_user_func("array_rand", [7, 8]); +echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); +$spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); +echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; +echo function_exists("array_rand");'); +"#, + ); + assert_eq!(out, "idx:assoc:named:call:spread:1"); +} + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn test_eval_dispatches_array_fill_builtin_calls() { From 3382d11c1e4eb48199f2f052136249e367478647 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 11:55:38 +0200 Subject: [PATCH 0178/1208] Support eval host lookup builtins --- Cargo.lock | 1 + crates/elephc-eval/Cargo.toml | 1 + crates/elephc-eval/src/interpreter.rs | 150 +++++++++++++++++++++++++- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 30 ++++++ 6 files changed, 184 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c456fa4d23..6e5bc09952 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,6 +621,7 @@ name = "elephc-eval" version = "0.1.0" dependencies = [ "elephc-crypto", + "libc", ] [[package]] diff --git a/crates/elephc-eval/Cargo.toml b/crates/elephc-eval/Cargo.toml index ec14e26061..1da35630d7 100644 --- a/crates/elephc-eval/Cargo.toml +++ b/crates/elephc-eval/Cargo.toml @@ -11,3 +11,4 @@ crate-type = ["staticlib", "rlib"] [dependencies] elephc-crypto = { path = "../elephc-crypto" } +libc = "0.2" diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f0094b2a60..f5744f5b4e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -20,6 +20,7 @@ use crate::eval_ir::{ use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; +use std::ffi::CStr; use std::net::ToSocketAddrs; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -73,6 +74,16 @@ const EVAL_HASH_ALGOS: &[&str] = &[ /// Root package manifest used to mirror native `phpversion()` in the eval crate. const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../Cargo.toml"); +unsafe extern "C" { + /// Reverse-resolves one socket address through libc's `gethostbyaddr`. + #[link_name = "gethostbyaddr"] + fn libc_gethostbyaddr( + addr: *const libc::c_void, + len: libc::socklen_t, + type_: libc::c_int, + ) -> *mut libc::hostent; +} + /// Runtime value hooks required by the EvalIR interpreter. pub trait RuntimeValueOps { /// Creates a runtime indexed-array cell with room for at least `capacity` elements. @@ -1272,7 +1283,9 @@ fn eval_positional_expr_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(args, context, scope, values) } + "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), + "gethostname" => eval_builtin_gethostname(args, values), "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), @@ -1679,7 +1692,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "floatval" | "fmod" | "function_exists" + | "gethostbyaddr" | "gethostbyname" + | "gethostname" | "getcwd" | "getenv" | "gettype" @@ -1928,7 +1943,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), "function_exists" => Some(&["function"]), + "gethostbyaddr" => Some(&["ip"]), "gethostbyname" => Some(&["hostname"]), + "gethostname" => Some(&[]), "getcwd" => Some(&[]), "getenv" => Some(&["name"]), "glob" => Some(&["pattern"]), @@ -2606,12 +2623,24 @@ fn eval_builtin_with_values( values.bool_value(eval_function_probe_exists(context, &name))? } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "gethostbyaddr" => { + let [ip] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyaddr_result(*ip, values)? + } "gethostbyname" => { let [hostname] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_gethostbyname_result(*hostname, values)? } + "gethostname" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_gethostname_result(values)? + } "getcwd" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -6604,6 +6633,51 @@ fn eval_fnmatch_fold(byte: u8, flags: i64) -> u8 { } } +/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. +fn eval_builtin_gethostbyaddr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_gethostbyaddr_result(ip, values) +} + +/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. +fn eval_gethostbyaddr_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip_bytes = values.string_bytes(ip)?; + let ip_text = String::from_utf8_lossy(&ip_bytes); + let Ok(ipv4) = ip_text.parse::() else { + return values.bool_value(false); + }; + let octets = ipv4.octets(); + let resolved = unsafe { + // libc reads the stack-owned IPv4 octets during this call and returns + // static resolver storage, which is copied before the next resolver call. + let host = libc_gethostbyaddr( + octets.as_ptr().cast::(), + octets.len() as libc::socklen_t, + libc::AF_INET, + ); + if host.is_null() || (*host).h_name.is_null() { + None + } else { + Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) + } + }; + match resolved { + Some(name) if !name.is_empty() => values.string_bytes_value(&name), + _ => values.string(ip_text.as_ref()), + } +} + /// Evaluates PHP `gethostbyname($hostname)` over one eval expression. fn eval_builtin_gethostbyname( args: &[EvalExpr], @@ -6638,10 +6712,44 @@ fn eval_gethostbyname_result( std::net::IpAddr::V6(_) => None, }) .next() - }); + }); values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) } +/// Evaluates PHP `gethostname()` over one eval expression. +fn eval_builtin_gethostname( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + let [] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostname_result(values) +} + +/// Reads the current host name through libc and returns an empty string on failure. +fn eval_gethostname_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let mut buffer = [0 as libc::c_char; 256]; + let status = unsafe { + // libc writes at most buffer.len() bytes into this stack buffer. + libc::gethostname(buffer.as_mut_ptr(), buffer.len()) + }; + if status != 0 { + return values.string(""); + } + let length = buffer + .iter() + .position(|byte| *byte == 0) + .unwrap_or(buffer.len()); + let hostname = buffer[..length] + .iter() + .map(|byte| *byte as u8) + .collect::>(); + values.string_bytes_value(&hostname) +} + /// Evaluates PHP `long2ip($ip)` over one eval expression. fn eval_builtin_long2ip( args: &[EvalExpr], @@ -11690,6 +11798,46 @@ return function_exists("gethostbyname");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. + #[test] + fn execute_program_dispatches_gethostname_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostname()) > 0 ? "host" : "empty"; echo ":"; +echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; +return function_exists("gethostname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "host:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. + #[test] + fn execute_program_dispatches_gethostbyaddr_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostbyaddr("127.0.0.1")) > 0 ? "direct" : "empty"; echo ":"; +echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; +echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; +echo strlen(call_user_func("gethostbyaddr", "127.0.0.1")) > 0 ? "call" : "empty"; echo ":"; +echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === false ? "spread" : "bad"; echo ":"; +return function_exists("gethostbyaddr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "direct:named:false:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. #[test] fn execute_program_dispatches_ip_conversion_builtins() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 532adc595d..07bce31c05 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -62,7 +62,7 @@ Eval environment calls include `getenv()` and `putenv()`. Missing variables read Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain outside this eval filesystem subset. -Eval `gethostbyname()` resolves IPv4 host names through the host resolver and returns the original string when no IPv4 address is available. +Eval host-name calls include `gethostname()`, `gethostbyname()`, and `gethostbyaddr()` with direct, named-argument, callable, and `function_exists()` dispatch paths. `gethostbyname()` resolves IPv4 host names and returns the original string when no IPv4 address is available. `gethostbyaddr()` reverse-resolves IPv4 dotted-quad addresses, returns the original address when no record exists, and returns `false` for malformed addresses. Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 6af1fd8db3..bead4758f7 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -166,7 +166,7 @@ public function __construct(int $value) { $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); $sleeping = eval('usleep(0); return sleep(0) . ":awake";'); -$host_lookup = eval('return gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host");'); +$host_lookup = eval('return (strlen(gethostname()) > 0 ? "host" : "empty") . ":" . gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host") . ":" . (strlen(gethostbyaddr("127.0.0.1")) > 0 ? "reverse" : "empty") . ":" . (gethostbyaddr("not-an-ip-address") === false ? "bad-ip" : "bad");'); $ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); $path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f16deec115..b347418fa1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1558,6 +1558,36 @@ echo function_exists("gethostbyname");'); assert_eq!(out, "127.0.0.1:not a host:127.0.0.1:not a host:1"); } +/// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. +#[test] +fn test_eval_dispatches_gethostname_builtin_call() { + let out = compile_and_run( + r#" 0 ? "host" : "empty"; echo ":"; +echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; +echo function_exists("gethostname");'); +"#, + ); + assert_eq!(out, "host:call:spread:1"); +} + +/// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. +#[test] +fn test_eval_dispatches_gethostbyaddr_builtin_call() { + let out = compile_and_run( + r#" 0 ? "direct" : "empty"; echo ":"; +echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; +echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; +echo strlen(call_user_func("gethostbyaddr", "127.0.0.1")) > 0 ? "call" : "empty"; echo ":"; +echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === false ? "spread" : "bad"; echo ":"; +echo function_exists("gethostbyaddr");'); +"#, + ); + assert_eq!(out, "direct:named:false:call:spread:1"); +} + /// Verifies eval IPv4 conversion builtins handle integer, string, and raw-byte forms. #[test] fn test_eval_dispatches_ip_conversion_builtin_calls() { From 1f6a0b4386bb6f6d8bc82788164dc34b1c40a45b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 12:04:12 +0200 Subject: [PATCH 0179/1208] Support eval stream introspection builtins --- crates/elephc-eval/src/interpreter.rs | 124 +++++++++++++++++++++++++- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 26 ++++++ 4 files changed, 152 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f5744f5b4e..b26d66b038 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -71,6 +71,45 @@ const EVAL_HASH_ALGOS: &[&str] = &[ "joaat", ]; +/// Built-in stream wrappers reported by eval `stream_get_wrappers()`. +const EVAL_STREAM_WRAPPERS: &[&str] = &[ + "file", + "php", + "data", + "ftp", + "http", + "https", + "ftps", + "compress.zlib", + "compress.bzip2", + "phar", + "glob", +]; + +/// Built-in stream transports reported by eval `stream_get_transports()`. +const EVAL_STREAM_TRANSPORTS: &[&str] = &[ + "tcp", "udp", "unix", "udg", "tls", "ssl", "sslv2", "sslv3", "tlsv1.0", "tlsv1.1", + "tlsv1.2", "tlsv1.3", +]; + +/// Built-in stream filters reported by eval `stream_get_filters()`. +const EVAL_STREAM_FILTERS: &[&str] = &[ + "string.toupper", + "string.tolower", + "string.rot13", + "string.strip_tags", + "convert.base64-encode", + "convert.base64-decode", + "convert.quoted-printable-encode", + "convert.quoted-printable-decode", + "convert.iconv.*", + "dechunk", + "zlib.deflate", + "zlib.inflate", + "bzip2.compress", + "bzip2.decompress", +]; + /// Root package manifest used to mirror native `phpversion()` in the eval crate. const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../Cargo.toml"); @@ -1345,6 +1384,9 @@ fn eval_positional_expr_call( "tempnam" => eval_builtin_tempnam(args, context, scope, values), "time" => eval_builtin_time(args, values), "touch" => eval_builtin_touch(args, context, scope, values), + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { + eval_builtin_stream_introspection(name, args, values) + } "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), @@ -1779,6 +1821,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "sinh" | "sqrt" | "strcasecmp" + | "stream_get_filters" + | "stream_get_transports" + | "stream_get_wrappers" | "str_contains" | "str_ends_with" | "str_ireplace" @@ -1979,6 +2024,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), "strstr" => Some(&["haystack", "needle", "before_needle"]), @@ -2791,6 +2837,12 @@ fn eval_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, values)? + } "umask" => match evaluated_args { [] => eval_umask_result(None, values)?, [mask] => eval_umask_result(Some(*mask), values)?, @@ -4933,16 +4985,50 @@ fn eval_builtin_hash_algos( fn eval_hash_algos_result( values: &mut impl RuntimeValueOps, ) -> Result { - let mut result = values.array_new(EVAL_HASH_ALGOS.len())?; - for (index, algo) in EVAL_HASH_ALGOS.iter().enumerate() { + eval_static_string_array_result(EVAL_HASH_ALGOS, values) +} + +/// Builds one indexed PHP array from a static string slice. +fn eval_static_string_array_result( + items: &[&str], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; let key = values.int(index)?; - let value = values.string(algo)?; + let value = values.string(item)?; result = values.array_set(result, key, value)?; } Ok(result) } +/// Evaluates PHP stream introspection list builtins with no arguments. +fn eval_builtin_stream_introspection( + name: &str, + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, values) +} + +/// Builds the static list returned by one eval stream introspection builtin. +fn eval_stream_introspection_result( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let items = match name { + "stream_get_filters" => EVAL_STREAM_FILTERS, + "stream_get_transports" => EVAL_STREAM_TRANSPORTS, + "stream_get_wrappers" => EVAL_STREAM_WRAPPERS, + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_static_string_array_result(items, values) +} + /// Evaluates PHP `time()` with no arguments. fn eval_builtin_time( args: &[EvalExpr], @@ -11729,6 +11815,38 @@ return function_exists("realpath_cache_size");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval stream introspection builtins return native-compatible static lists. + #[test] + fn execute_program_dispatches_stream_introspection_builtins() { + let program = parse_fragment( + br#"$wrappers = stream_get_wrappers(); +$transports = stream_get_transports(); +$filters = stream_get_filters(); +echo count($wrappers) . ":" . $wrappers[0] . ":" . $wrappers[5] . ":"; +echo count($transports) . ":" . $transports[0] . ":" . $transports[8] . ":"; +echo count($filters) . ":" . $filters[2] . ":"; +$call_wrappers = call_user_func("stream_get_wrappers"); +echo $call_wrappers[10] . ":"; +$call_transports = call_user_func_array("stream_get_transports", []); +echo $call_transports[11] . ":"; +$call_filters = call_user_func_array("stream_get_filters", []); +echo $call_filters[13] . ":"; +echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); +return function_exists("stream_get_filters");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval environment builtins read, write, unset, and dispatch dynamically. #[test] fn execute_program_dispatches_environment_builtins() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 07bce31c05..2176929200 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -62,6 +62,8 @@ Eval environment calls include `getenv()` and `putenv()`. Missing variables read Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain outside this eval filesystem subset. +Eval stream introspection calls include `stream_get_filters()`, `stream_get_transports()`, and `stream_get_wrappers()` with direct, callable, and `function_exists()` dispatch paths. They return the same static built-in lists documented in [Streams](streams.md). + Eval host-name calls include `gethostname()`, `gethostbyname()`, and `gethostbyaddr()` with direct, named-argument, callable, and `function_exists()` dispatch paths. `gethostbyname()` resolves IPv4 host names and returns the original string when no IPv4 address is available. `gethostbyaddr()` reverse-resolves IPv4 dotted-quad addresses, returns the original address when no record exists, and returns `false` for malformed addresses. Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. diff --git a/examples/eval/main.php b/examples/eval/main.php index bead4758f7..959c7c768b 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -168,6 +168,7 @@ public function __construct(int $value) { $sleeping = eval('usleep(0); return sleep(0) . ":awake";'); $host_lookup = eval('return (strlen(gethostname()) > 0 ? "host" : "empty") . ":" . gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host") . ":" . (strlen(gethostbyaddr("127.0.0.1")) > 0 ? "reverse" : "empty") . ":" . (gethostbyaddr("not-an-ip-address") === false ? "bad-ip" : "bad");'); $ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); +$stream_introspection = eval('$wrappers = stream_get_wrappers(); $transports = stream_get_transports(); $filters = stream_get_filters(); return count($wrappers) . ":" . $wrappers[0] . ":" . count($transports) . ":" . (in_array("string.rot13", $filters) ? "rot13" : "missing");'); $path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); $path_info = eval('$info = pathinfo("/var/log/syslog.log"); $match = fnmatch("*.LOG", "system.log", FNM_CASEFOLD); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":" . ($match ? "match" : "bad");'); @@ -290,6 +291,7 @@ public function __construct(int $value) { echo "sleep=" . $sleeping . "\n"; echo "host-lookup=" . $host_lookup . "\n"; echo "ip-conversion=" . $ip_conversion . "\n"; +echo "stream-introspection=" . $stream_introspection . "\n"; echo "path-components=" . $path_components . "\n"; echo "realpath=" . $resolved_path . "\n"; echo "pathinfo=" . $path_info . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b347418fa1..84a49dda03 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1588,6 +1588,32 @@ echo function_exists("gethostbyaddr");'); assert_eq!(out, "direct:named:false:call:spread:1"); } +/// Verifies eval stream introspection builtins return native-compatible static lists. +#[test] +fn test_eval_dispatches_stream_introspection_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 12:12:47 +0200 Subject: [PATCH 0180/1208] Support eval protocol service builtins --- crates/elephc-eval/src/interpreter.rs | 278 +++++++++++++++++++++++++- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 26 +++ 4 files changed, 308 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b26d66b038..990578b6e9 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -20,7 +20,7 @@ use crate::eval_ir::{ use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; -use std::ffi::CStr; +use std::ffi::{CStr, CString}; use std::net::ToSocketAddrs; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -121,6 +121,28 @@ unsafe extern "C" { len: libc::socklen_t, type_: libc::c_int, ) -> *mut libc::hostent; + + /// Looks up one IP protocol entry by protocol name or alias. + #[link_name = "getprotobyname"] + fn libc_getprotobyname(name: *const libc::c_char) -> *mut libc::protoent; + + /// Looks up one IP protocol entry by protocol number. + #[link_name = "getprotobynumber"] + fn libc_getprotobynumber(proto: libc::c_int) -> *mut libc::protoent; + + /// Looks up one internet service entry by service name and protocol. + #[link_name = "getservbyname"] + fn libc_getservbyname( + name: *const libc::c_char, + proto: *const libc::c_char, + ) -> *mut libc::servent; + + /// Looks up one internet service entry by port and protocol. + #[link_name = "getservbyport"] + fn libc_getservbyport( + port: libc::c_int, + proto: *const libc::c_char, + ) -> *mut libc::servent; } /// Runtime value hooks required by the EvalIR interpreter. @@ -1325,6 +1347,10 @@ fn eval_positional_expr_call( "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), "gethostname" => eval_builtin_gethostname(args, values), + "getprotobyname" => eval_builtin_getprotobyname(args, context, scope, values), + "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), + "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), + "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), @@ -1737,6 +1763,10 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "gethostbyaddr" | "gethostbyname" | "gethostname" + | "getprotobyname" + | "getprotobynumber" + | "getservbyname" + | "getservbyport" | "getcwd" | "getenv" | "gettype" @@ -1991,6 +2021,10 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "gethostbyaddr" => Some(&["ip"]), "gethostbyname" => Some(&["hostname"]), "gethostname" => Some(&[]), + "getprotobyname" => Some(&["protocol"]), + "getprotobynumber" => Some(&["protocol"]), + "getservbyname" => Some(&["service", "protocol"]), + "getservbyport" => Some(&["port", "protocol"]), "getcwd" => Some(&[]), "getenv" => Some(&["name"]), "glob" => Some(&["pattern"]), @@ -2687,6 +2721,30 @@ fn eval_builtin_with_values( } eval_gethostname_result(values)? } + "getprotobyname" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobyname_result(*protocol, values)? + } + "getprotobynumber" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobynumber_result(*protocol, values)? + } + "getservbyname" => { + let [service, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyname_result(*service, *protocol, values)? + } + "getservbyport" => { + let [port, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyport_result(*port, *protocol, values)? + } "getcwd" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -6836,6 +6894,192 @@ fn eval_gethostname_result( values.string_bytes_value(&hostname) } +/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. +fn eval_builtin_getprotobyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobyname_result(protocol, values) +} + +/// Looks up an IP protocol number by name or alias. +fn eval_getprotobyname_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy scalar fields before another lookup. + libc_getprotobyname(protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let number = unsafe { (*entry).p_proto }; + values.int(i64::from(number)) +} + +/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. +fn eval_builtin_getprotobynumber( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobynumber_result(protocol, values) +} + +/// Looks up an IP protocol name by numeric protocol id. +fn eval_getprotobynumber_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = eval_int_value(protocol, values)?; + let Ok(protocol) = libc::c_int::try_from(protocol) else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy the name before another lookup. + libc_getprotobynumber(protocol) + }; + eval_protoent_name_or_false(entry, values) +} + +/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. +fn eval_builtin_getservbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [service, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let service = eval_expr(service, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyname_result(service, protocol, values) +} + +/// Looks up an internet service port by service name and protocol. +fn eval_getservbyname_result( + service: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(service) = eval_lowercase_c_string(service, values)? else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global servent; copy scalar fields before another lookup. + libc_getservbyname(service.as_ptr(), protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let port = unsafe { u16::from_be((*entry).s_port as u16) }; + values.int(i64::from(port)) +} + +/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. +fn eval_builtin_getservbyport( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [port, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let port = eval_expr(port, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyport_result(port, protocol, values) +} + +/// Looks up an internet service name by port and protocol. +fn eval_getservbyport_result( + port: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let port = eval_int_value(port, values)?; + let Ok(port) = u16::try_from(port) else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let network_port = port.to_be() as libc::c_int; + let entry = unsafe { + // libc returns a process-global servent; copy the name before another lookup. + libc_getservbyport(network_port, protocol.as_ptr()) + }; + eval_servent_name_or_false(entry, values) +} + +/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. +fn eval_lowercase_c_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let bytes = values.string_bytes(value)?; + let bytes = bytes + .into_iter() + .map(|byte| byte.to_ascii_lowercase()) + .collect::>(); + Ok(CString::new(bytes).ok()) +} + +/// Copies a protoent canonical name into a PHP string or returns PHP false. +fn eval_protoent_name_or_false( + entry: *mut libc::protoent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).p_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} + +/// Copies a servent canonical name into a PHP string or returns PHP false. +fn eval_servent_name_or_false( + entry: *mut libc::servent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).s_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} + /// Evaluates PHP `long2ip($ip)` over one eval expression. fn eval_builtin_long2ip( args: &[EvalExpr], @@ -11956,6 +12200,38 @@ return function_exists("gethostbyaddr");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval protocol and service database lookups dispatch dynamically. + #[test] + fn execute_program_dispatches_protocol_service_builtins() { + let program = parse_fragment( + br#"echo getprotobyname("TCP") . ":"; +echo getprotobynumber(6) . ":"; +echo getprotobyname("no_such_protocol") === false ? "missing-proto" : "bad"; echo ":"; +echo getprotobynumber(999) === false ? "missing-number" : "bad"; echo ":"; +echo getservbyname("www", "tcp") . ":"; +echo getservbyport(80, "tcp") . ":"; +echo getservbyname("no_such_service", "tcp") === false ? "missing-service" : "bad"; echo ":"; +echo getservbyport(80, "no_such_proto") === false ? "missing-port" : "bad"; echo ":"; +echo call_user_func("getprotobyname", "udp") . ":"; +echo call_user_func_array("getprotobynumber", ["protocol" => 17]) . ":"; +echo call_user_func("getservbyname", "https", "tcp") . ":"; +echo call_user_func_array("getservbyport", ["port" => 443, "protocol" => "tcp"]) . ":"; +echo function_exists("getprotobyname"); echo function_exists("getprotobynumber"); echo function_exists("getservbyname"); +return function_exists("getservbyport");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. #[test] fn execute_program_dispatches_ip_conversion_builtins() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 2176929200..35d7796e23 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -66,6 +66,8 @@ Eval stream introspection calls include `stream_get_filters()`, `stream_get_tran Eval host-name calls include `gethostname()`, `gethostbyname()`, and `gethostbyaddr()` with direct, named-argument, callable, and `function_exists()` dispatch paths. `gethostbyname()` resolves IPv4 host names and returns the original string when no IPv4 address is available. `gethostbyaddr()` reverse-resolves IPv4 dotted-quad addresses, returns the original address when no record exists, and returns `false` for malformed addresses. +Eval protocol and service database calls include `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, and `getservbyport()` with direct, named-argument, callable, and `function_exists()` dispatch paths. + Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. Eval path component calls include `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` with suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, named arguments, callable dispatch, and `function_exists()` probes. diff --git a/examples/eval/main.php b/examples/eval/main.php index 959c7c768b..8ccd0e819c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -167,6 +167,7 @@ public function __construct(int $value) { $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); $sleeping = eval('usleep(0); return sleep(0) . ":awake";'); $host_lookup = eval('return (strlen(gethostname()) > 0 ? "host" : "empty") . ":" . gethostbyname("127.0.0.1") . ":" . gethostbyname("not a host") . ":" . (strlen(gethostbyaddr("127.0.0.1")) > 0 ? "reverse" : "empty") . ":" . (gethostbyaddr("not-an-ip-address") === false ? "bad-ip" : "bad");'); +$protocol_services = eval('return getprotobyname("tcp") . ":" . getprotobynumber(17) . ":" . getservbyname("http", "tcp") . ":" . getservbyport(443, "tcp");'); $ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); $stream_introspection = eval('$wrappers = stream_get_wrappers(); $transports = stream_get_transports(); $filters = stream_get_filters(); return count($wrappers) . ":" . $wrappers[0] . ":" . count($transports) . ":" . (in_array("string.rot13", $filters) ? "rot13" : "missing");'); $path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); @@ -290,6 +291,7 @@ public function __construct(int $value) { echo "environment=" . $environment . "\n"; echo "sleep=" . $sleeping . "\n"; echo "host-lookup=" . $host_lookup . "\n"; +echo "protocol-services=" . $protocol_services . "\n"; echo "ip-conversion=" . $ip_conversion . "\n"; echo "stream-introspection=" . $stream_introspection . "\n"; echo "path-components=" . $path_components . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 84a49dda03..a535fde779 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1588,6 +1588,32 @@ echo function_exists("gethostbyaddr");'); assert_eq!(out, "direct:named:false:call:spread:1"); } +/// Verifies eval protocol and service database lookups dispatch dynamically. +#[test] +fn test_eval_dispatches_protocol_service_builtin_calls() { + let out = compile_and_run( + r#" 17]) . ":"; +echo call_user_func("getservbyname", "https", "tcp") . ":"; +echo call_user_func_array("getservbyport", ["port" => 443, "protocol" => "tcp"]) . ":"; +echo function_exists("getprotobyname"); echo function_exists("getprotobynumber"); echo function_exists("getservbyname"); echo function_exists("getservbyport");'); +"#, + ); + assert_eq!( + out, + "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:1111" + ); +} + /// Verifies eval stream introspection builtins return native-compatible static lists. #[test] fn test_eval_dispatches_stream_introspection_builtin_calls() { From 78123fc00278218b2029b8c08e186d3572783570 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 12:21:19 +0200 Subject: [PATCH 0181/1208] Support eval php_uname builtin --- crates/elephc-eval/src/interpreter.rs | 116 ++++++++++++++++++++++++++ docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 23 +++++ 4 files changed, 141 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 990578b6e9..b2eef404ae 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1386,6 +1386,7 @@ fn eval_positional_expr_call( "ord" => eval_builtin_ord(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "pi" => eval_builtin_pi(args, values), + "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), "pow" => eval_builtin_pow(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), @@ -1829,6 +1830,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "pathinfo" | "pi" | "pow" + | "php_uname" | "phpversion" | "putenv" | "range" @@ -2050,6 +2052,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "ord" => Some(&["character"]), "pathinfo" => Some(&["path", "flags"]), "pi" => Some(&[]), + "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), "putenv" => Some(&["assignment"]), @@ -2501,6 +2504,11 @@ fn eval_builtin_with_values( } values.float(std::f64::consts::PI)? } + "php_uname" => match evaluated_args { + [] => eval_php_uname_result(None, values)?, + [mode] => eval_php_uname_result(Some(*mode), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "pow" => { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -5226,6 +5234,86 @@ fn eval_compiler_php_version() -> &'static str { env!("CARGO_PKG_VERSION") } +/// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. +fn eval_builtin_php_uname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_php_uname_result(None, values), + [mode] => { + let mode = eval_expr(mode, context, scope, values)?; + eval_php_uname_result(Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads the local uname fields and formats the PHP `php_uname()` mode result. +fn eval_php_uname_result( + mode: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => { + let bytes = values.string_bytes(mode)?; + let [mode] = bytes.as_slice() else { + return Err(EvalStatus::RuntimeFatal); + }; + *mode + } + None => b'a', + }; + + let mut utsname = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes all uname fields into the stack-owned utsname buffer. + libc::uname(utsname.as_mut_ptr()) + }; + if status != 0 { + return values.string(""); + } + let utsname = unsafe { + // `uname` succeeded, so libc initialized the full `utsname` structure. + utsname.assume_init() + }; + let sysname = eval_uname_field_bytes(&utsname.sysname); + let nodename = eval_uname_field_bytes(&utsname.nodename); + let release = eval_uname_field_bytes(&utsname.release); + let version = eval_uname_field_bytes(&utsname.version); + let machine = eval_uname_field_bytes(&utsname.machine); + + match mode { + b'a' => { + let mut output = Vec::new(); + for field in [&sysname, &nodename, &release, &version, &machine] { + if !output.is_empty() { + output.push(b' '); + } + output.extend_from_slice(field); + } + values.string_bytes_value(&output) + } + b's' => values.string_bytes_value(&sysname), + b'n' => values.string_bytes_value(&nodename), + b'r' => values.string_bytes_value(&release), + b'v' => values.string_bytes_value(&version), + b'm' => values.string_bytes_value(&machine), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. +fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { + let length = field + .iter() + .position(|byte| *byte == 0) + .unwrap_or(field.len()); + field[..length].iter().map(|byte| *byte as u8).collect() +} + /// Evaluates PHP `getcwd()` with no arguments. fn eval_builtin_getcwd( args: &[EvalExpr], @@ -12140,6 +12228,34 @@ return function_exists("usleep");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. + #[test] + fn execute_program_dispatches_php_uname_builtin() { + let program = parse_fragment( + br#"echo strlen(php_uname()) > 0 ? "all" : "empty"; echo ":"; +echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; +echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; +echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; +echo strlen(php_uname("r")) > 0 ? "release" : "empty"; echo ":"; +echo strlen(php_uname("v")) > 0 ? "version" : "empty"; echo ":"; +echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; +echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; +return function_exists("php_uname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "all:same:sys:node:release:version:machine:call:spread:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. #[test] fn execute_program_dispatches_gethostbyname_builtin() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 35d7796e23..12f9c7b98b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -52,7 +52,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. -Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. +Eval system and path calls include `time()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. Eval `microtime()` returns a float Unix timestamp with microsecond precision, matching elephc's static `microtime()` lowering. diff --git a/examples/eval/main.php b/examples/eval/main.php index 8ccd0e819c..846153b4ad 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -161,7 +161,7 @@ public function __construct(int $value) { $checksum = eval('return crc32("hello");'); $hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); $digest = eval('return md5("abc") . ":" . substr(hash("sha256", "abc"), 0, 8) . ":" . substr(hash_hmac("sha256", "data", "key"), 0, 8);'); -$system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); +$system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . (strlen(php_uname("s")) > 0 ? "uname" : "bad") . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); $micro_time = eval('return microtime(true) > 1000000000 ? "ok" : "bad";'); $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a535fde779..8f2e6d2e57 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1543,6 +1543,29 @@ echo function_exists("usleep");'); assert_eq!(out, "0:0:u:0:null:11"); } +/// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. +#[test] +fn test_eval_dispatches_php_uname_builtin_call() { + let out = compile_and_run( + r#" 0 ? "all" : "empty"; echo ":"; +echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; +echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; +echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; +echo strlen(php_uname("r")) > 0 ? "release" : "empty"; echo ":"; +echo strlen(php_uname("v")) > 0 ? "version" : "empty"; echo ":"; +echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; +echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; +echo function_exists("php_uname");'); +"#, + ); + assert_eq!( + out, + "all:same:sys:node:release:version:machine:call:spread:1" + ); +} + /// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. #[test] fn test_eval_dispatches_gethostbyname_builtin_call() { From a6196fc8930fbe16ba84024c3574ceae4770d546 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 12:29:23 +0200 Subject: [PATCH 0182/1208] Support eval clamp builtin --- crates/elephc-eval/src/interpreter.rs | 99 +++++++++++++++++++++++++++ docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 18 +++++ 4 files changed, 120 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b2eef404ae..97a4b89ff9 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1307,6 +1307,7 @@ fn eval_positional_expr_call( } "chmod" => eval_builtin_chmod(args, context, scope, values), "chr" => eval_builtin_chr(args, context, scope, values), + "clamp" => eval_builtin_clamp(args, context, scope, values), "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), @@ -1726,6 +1727,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "boolval" | "chop" | "chr" + | "clamp" | "clearstatcache" | "count" | "copy" @@ -2002,6 +2004,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), "chmod" => Some(&["filename", "permissions"]), "chr" => Some(&["codepoint"]), + "clamp" => Some(&["value", "min", "max"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), @@ -2406,6 +2409,12 @@ fn eval_builtin_with_values( } values.null()? } + "clamp" => { + let [value, min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_clamp_result(*value, *min, *max, values)? + } "copy" | "link" | "rename" | "symlink" => { let [from, to] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -7893,6 +7902,58 @@ fn eval_float_binary_result( } } +/// Evaluates PHP `clamp($value, $min, $max)` over three eval expressions. +fn eval_builtin_clamp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_clamp_result(value, min, max, values) +} + +/// Selects the inclusive clamp result after validating bound order and NaN bounds. +fn eval_clamp_result( + value: RuntimeCellHandle, + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; + if values.truthy(invalid_bounds)? { + return Err(EvalStatus::RuntimeFatal); + } + let above_max = values.compare(EvalBinOp::Gt, value, max)?; + if values.truthy(above_max)? { + return Ok(max); + } + let below_min = values.compare(EvalBinOp::Lt, value, min)?; + if values.truthy(below_min)? { + return Ok(min); + } + Ok(value) +} + +/// Returns whether a clamp bound is a floating-point NaN value. +fn eval_clamp_bound_is_nan( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_FLOAT { + return Ok(false); + } + Ok(eval_float_value(value, values)?.is_nan()) +} + /// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. fn eval_builtin_min_max( name: &str, @@ -13417,6 +13478,44 @@ return function_exists("max");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `clamp()` selects numeric values through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_clamp_builtin() { + let program = parse_fragment( + br#"echo clamp(5, 0, 10); echo ":"; +echo clamp(15, 0, 10); echo ":"; +echo clamp(-5, 0, 10); echo ":"; +echo clamp(2.75, 1.5, 2.5); echo ":"; +echo clamp(value: 8, min: 0, max: 5); echo ":"; +echo call_user_func("clamp", -1, 0, 10); echo ":"; +echo call_user_func_array("clamp", ["value" => 9, "min" => 0, "max" => 7]); echo ":"; +echo function_exists("clamp"); +return is_callable("clamp");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:10:0:2.5:5:0:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. + #[test] + fn execute_program_rejects_clamp_invalid_bounds() { + let program = + parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("invalid clamp bounds should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } + /// Verifies eval `pi()` returns a double constant directly and through callable paths. #[test] fn execute_program_dispatches_pi_builtin() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 12f9c7b98b..726480afbf 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -82,7 +82,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 846153b4ad..38c1d80348 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -113,7 +113,7 @@ public function __construct(int $value) { $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); $formatted_number = eval('return number_format(1234567.89, 2, ",", ".");'); -$minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5);'); +$minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5) . ":" . clamp(15, 0, 10);'); $circle = eval('return round(pi(), 2);'); $extended_math = eval('return round(sin(pi() / 2), 0) . ":" . log(8, 2) . ":" . hypot(3, 4) . ":" . intdiv(7, 2);'); $float_predicates = eval('return (is_nan(fdiv(0, 0)) ? "nan" : "bad") . ":" . (is_infinite(fdiv(1, 0)) ? "inf" : "bad") . ":" . (is_finite(3.14) ? "finite" : "bad");'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8f2e6d2e57..24f9cfa189 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2535,6 +2535,24 @@ echo ":"; echo function_exists("min"); echo function_exists("max");'); assert_eq!(out, "1:3:1.5:2.5:4:8:11"); } +/// Verifies eval `clamp()` selects numeric values directly and through callables. +#[test] +fn test_eval_dispatches_clamp_builtin_calls() { + let out = compile_and_run( + r#" 9, "min" => 0, "max" => 7]); +echo ":"; echo function_exists("clamp"); echo is_callable("clamp");'); +"#, + ); + assert_eq!(out, "5:10:0:2.5:5:0:7:11"); +} + /// Verifies eval `pi()` returns the PHP math constant through direct and callable calls. #[test] fn test_eval_dispatches_pi_builtin_call() { From d267d130ef47ce12164c3738330e499b66e699c1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 12:36:10 +0200 Subject: [PATCH 0183/1208] Support eval disk space builtins --- crates/elephc-eval/src/interpreter.rs | 85 +++++++++++++++++++++++++++ docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 17 ++++++ 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 97a4b89ff9..21f33b6097 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1327,6 +1327,9 @@ fn eval_positional_expr_call( "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "dirname" => eval_builtin_dirname(args, context, scope, values), + "disk_free_space" | "disk_total_space" => { + eval_builtin_disk_space(name, args, context, scope, values) + } "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), @@ -1742,6 +1745,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "defined" | "deg2rad" | "dirname" + | "disk_free_space" + | "disk_total_space" | "exp" | "explode" | "fdiv" @@ -2014,6 +2019,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "dirname" => Some(&["path", "levels"]), + "disk_free_space" | "disk_total_space" => Some(&["directory"]), "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "fnmatch" => Some(&["pattern", "filename", "flags"]), @@ -2705,6 +2711,12 @@ fn eval_builtin_with_values( [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, _ => return Err(EvalStatus::RuntimeFatal), }, + "disk_free_space" | "disk_total_space" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_disk_space_result(name, *directory, values)? + } "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values)?, [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, @@ -5722,6 +5734,56 @@ fn eval_u64_to_i64(value: u64) -> Result { i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) } +/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. +fn eval_builtin_disk_space( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_disk_space_result(name, directory, values) +} + +/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. +fn eval_disk_space_result( + name: &str, + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(directory)?; + let Ok(path) = CString::new(bytes) else { + return values.float(0.0); + }; + let mut stats = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes the statvfs fields for this NUL-terminated local path. + libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) + }; + if status != 0 { + return values.float(0.0); + } + let stats = unsafe { + // `statvfs` succeeded, so libc initialized the full stat buffer. + stats.assume_init() + }; + let block_size = if stats.f_frsize > 0 { + stats.f_frsize + } else { + stats.f_bsize + }; + let blocks = match name { + "disk_free_space" => stats.f_bavail, + "disk_total_space" => stats.f_blocks, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.float((block_size as f64) * (blocks as f64)) +} + /// Evaluates a one-path filesystem operation that returns a PHP boolean. fn eval_builtin_unary_path_bool( name: &str, @@ -12592,6 +12654,29 @@ return function_exists("unlink");"# assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval disk-space builtins query local filesystem capacity and dispatch dynamically. + #[test] + fn execute_program_dispatches_disk_space_builtins() { + let program = parse_fragment( + br#"echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; +echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; +echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; +echo disk_free_space("no/such/path/elephc-eval") === 0.0 ? "missing" : "bad"; echo ":"; +echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; +echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; echo ":"; +echo function_exists("disk_free_space"); +return function_exists("disk_total_space");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "free:total:ordered:missing:call:spread:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval stat metadata builtins expose scalar file metadata and link probes. #[test] fn execute_program_dispatches_stat_metadata_builtins() { diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 726480afbf..4222036c15 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,7 +60,7 @@ Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. -Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain outside this eval filesystem subset. +Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain outside this eval filesystem subset. Eval stream introspection calls include `stream_get_filters()`, `stream_get_transports()`, and `stream_get_wrappers()` with direct, callable, and `function_exists()` dispatch paths. They return the same static built-in lists documented in [Streams](streams.md). diff --git a/examples/eval/main.php b/examples/eval/main.php index 38c1d80348..698dc61cb1 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -174,6 +174,7 @@ public function __construct(int $value) { $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); $path_info = eval('$info = pathinfo("/var/log/syslog.log"); $match = fnmatch("*.LOG", "system.log", FNM_CASEFOLD); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":" . ($match ? "match" : "bad");'); $filesystem = eval('file_put_contents("eval-example.txt", "hello"); $read = file_get_contents("eval-example.txt"); $size = filesize("eval-example.txt"); $ok = file_exists("eval-example.txt") && is_file("eval-example.txt") && is_readable("eval-example.txt") && is_writable("eval-example.txt") && unlink("eval-example.txt"); return $read . ":" . $size . ":" . ($ok ? "ok" : "bad");'); +$disk_space = eval('return (disk_free_space(".") > 0 ? "free" : "bad") . ":" . (disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad");'); $file_stats = eval('file_put_contents("eval-stat.txt", "hello"); $type = filetype("eval-stat.txt"); $info = stat("eval-stat.txt"); $meta = filemtime("eval-stat.txt") > 0 && fileinode("eval-stat.txt") > 0 && fileperms("eval-stat.txt") > 0 && $info["size"] === 5 && $info[7] === $info["size"]; unlink("eval-stat.txt"); return $type . ":" . ($meta ? "meta" : "bad");'); $path_ops = eval('mkdir("eval-ops-dir"); file_put_contents("eval-ops-src.txt", "hello"); copy("eval-ops-src.txt", "eval-ops-copy.txt"); rename("eval-ops-copy.txt", "eval-ops-moved.txt"); symlink("eval-ops-src.txt", "eval-ops-link.txt"); $ok = is_dir("eval-ops-dir") && file_exists("eval-ops-moved.txt") && readlink("eval-ops-link.txt") === "eval-ops-src.txt" && linkinfo("eval-ops-link.txt") >= 0; unlink("eval-ops-link.txt"); unlink("eval-ops-moved.txt"); unlink("eval-ops-src.txt"); rmdir("eval-ops-dir"); return $ok ? "ok" : "bad";'); $file_listing = eval('file_put_contents("eval-lines.txt", "one\ntwo"); file_put_contents("eval-empty.txt", ""); mkdir("eval-list-dir"); file_put_contents("eval-list-dir/a.txt", "a"); $lines = file("eval-lines.txt"); $scan = scandir("eval-list-dir"); $glob = glob("eval-list-dir/*.txt"); $bytes = readfile("eval-empty.txt"); $ok = count($lines) === 2 && $lines[0] === "one\n" && $bytes === 0 && in_array("a.txt", $scan) && count($glob) === 1; unlink("eval-list-dir/a.txt"); rmdir("eval-list-dir"); unlink("eval-lines.txt"); unlink("eval-empty.txt"); return $ok ? "ok" : "bad";'); @@ -298,6 +299,7 @@ public function __construct(int $value) { echo "realpath=" . $resolved_path . "\n"; echo "pathinfo=" . $path_info . "\n"; echo "filesystem=" . $filesystem . "\n"; +echo "disk-space=" . $disk_space . "\n"; echo "file-stats=" . $file_stats . "\n"; echo "path-ops=" . $path_ops . "\n"; echo "file-listing=" . $file_listing . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 24f9cfa189..e976cc18cb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1806,6 +1806,23 @@ echo function_exists("filesize"); echo function_exists("unlink");'); ); } +/// Verifies eval disk-space builtins return positive local capacity and zero on failure. +#[test] +fn test_eval_dispatches_disk_space_builtin_calls() { + let out = compile_and_run( + r#" 0 ? "free" : "bad"; echo ":"; +echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; +echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; +echo disk_free_space("no/such/path/elephc-eval") === 0.0 ? "missing" : "bad"; echo ":"; +echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; +echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; +echo ":"; echo function_exists("disk_free_space"); echo function_exists("disk_total_space");'); +"#, + ); + assert_eq!(out, "free:total:ordered:missing:call:spread:11"); +} + /// Verifies eval stat metadata builtins return scalar metadata and dispatch dynamically. #[test] fn test_eval_dispatches_stat_metadata_builtin_calls() { From ddf25a1b12c1d207a7e81abe661f18825b841ad5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 12:45:24 +0200 Subject: [PATCH 0184/1208] Support eval is_iterable builtin --- crates/elephc-eval/src/interpreter.rs | 27 ++++++++++++++++++--------- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 8 ++++++-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 21f33b6097..c1006b3a17 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1375,8 +1375,8 @@ fn eval_positional_expr_call( "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" - | "is_int" | "is_integer" | "is_long" | "is_nan" | "is_null" | "is_numeric" - | "is_real" | "is_resource" | "is_string" => { + | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" + | "is_numeric" | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } "ip2long" => eval_builtin_ip2long(args, context, scope, values), @@ -1814,6 +1814,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_infinite" | "is_int" | "is_integer" + | "is_iterable" | "is_long" | "is_nan" | "is_null" @@ -2000,9 +2001,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { Some(&["string"]) } "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" - | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_long" - | "is_nan" | "is_null" | "is_numeric" | "is_real" | "is_resource" | "is_string" - | "is_callable" | "strval" => Some(&["value"]), + | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" + | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_real" + | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), @@ -2885,8 +2886,8 @@ fn eval_builtin_with_values( eval_realpath_cache_size_result(values)? } "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" - | "is_int" | "is_integer" | "is_long" | "is_nan" | "is_null" | "is_numeric" - | "is_real" | "is_resource" | "is_string" => { + | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" + | "is_numeric" | "is_real" | "is_resource" | "is_string" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -8154,7 +8155,7 @@ fn eval_type_predicate_result( "is_string" => tag == EVAL_TAG_STRING, "is_bool" => tag == EVAL_TAG_BOOL, "is_null" => tag == EVAL_TAG_NULL, - "is_array" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), "is_resource" => tag == EVAL_TAG_RESOURCE, "is_nan" => eval_float_value(value, values)?.is_nan(), "is_infinite" => eval_float_value(value, values)?.is_infinite(), @@ -13273,6 +13274,8 @@ return function_exists("chop");"#, echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); echo is_string("x"); echo is_bool(false); echo is_null(null); echo is_array([1]); echo is_array(["a" => 1]); +echo is_iterable([1]); echo is_iterable(["a" => 1]); +echo is_iterable(1) ? "bad" : "T"; echo is_array(1) ? "bad" : "ok"; echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); echo is_numeric("-5"); echo is_numeric("3.14"); @@ -13287,8 +13290,11 @@ echo is_finite(fdiv(1, 0)) ? "bad" : "f"; echo ":"; echo call_user_func("is_string", "x"); echo call_user_func_array("is_array", [[1]]); echo call_user_func("is_numeric", "12"); +echo call_user_func("is_iterable", [1]); +echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; echo function_exists("is_numeric"); echo function_exists("is_resource"); echo function_exists("is_double"); echo function_exists("is_nan"); echo function_exists("is_finite"); +echo function_exists("is_iterable"); return function_exists("is_infinite");"#, ) .expect("parse eval fragment"); @@ -13297,7 +13303,10 @@ return function_exists("is_infinite");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "11111111111ok11111NBRNIiFf:11111111"); + assert_eq!( + values.output, + "1111111111111Tok11111NBRNIiFf:1111t111111" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4222036c15..417d35256b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 698dc61cb1..44ad037654 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -103,7 +103,7 @@ public function __construct(int $value) { $magic_dir_has_path = eval('return strlen(__DIR__) > 0;'); $magic_scope = eval('return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";'); $memory = fopen("php://memory", "r+"); -$type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?") . (is_numeric("42") ? "n" : "?") . (is_resource($memory) ? "r" : "?");'); +$type_checks = eval('return (is_int(1) ? "i" : "?") . (is_string("x") ? "s" : "?") . (is_array([1]) ? "a" : "?") . (is_iterable([1]) ? "t" : "?") . (is_numeric("42") ? "n" : "?") . (is_resource($memory) ? "r" : "?");'); $casts = eval('return strval(intval("42")) . ":" . strval(floatval("3.5")) . ":" . (boolval("0") ? "true" : "false");'); $type_name = eval('return gettype(["ok"]);'); $absolute = eval('return abs(-7) . ":" . gettype(abs(-2.5));'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e976cc18cb..b5d50e0b40 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2242,6 +2242,8 @@ eval('echo is_int(1); echo is_integer(1); echo is_long(1); echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); echo is_string("x"); echo is_bool(false); echo is_null(null); echo is_array([1]); echo is_array(["a" => 1]); +echo is_iterable([1]); echo is_iterable(["a" => 1]); +echo is_iterable(1) ? "bad" : "T"; echo is_array(1) ? "bad" : "ok"; echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); echo is_numeric("-5"); echo is_numeric("3.14"); @@ -2258,15 +2260,17 @@ echo ":"; echo call_user_func("is_string", "x"); echo call_user_func_array("is_array", [[1]]); echo call_user_func("is_numeric", "12"); +echo call_user_func("is_iterable", [1]); +echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; echo call_user_func("is_resource", $h); echo call_user_func_array("is_resource", [$h]); echo call_user_func("is_nan", fdiv(0, 0)) ? "N" : "bad"; echo call_user_func_array("is_finite", [42]) ? "F" : "bad"; echo function_exists("is_double"); echo function_exists("is_numeric"); echo function_exists("is_resource"); -echo function_exists("is_nan"); echo function_exists("is_finite"); echo function_exists("is_infinite");'); +echo function_exists("is_nan"); echo function_exists("is_finite"); echo function_exists("is_iterable"); echo function_exists("is_infinite");'); "#, ); - assert_eq!(out, "11111111111ok11111NBRNIiFfH:11111NF111111"); + assert_eq!(out, "1111111111111Tok11111NBRNIiFfH:1111t11NF1111111"); } /// Verifies eval scalar cast builtins return boxed Mixed cells through direct and callable calls. From 31148c9bf4e30d8ae56c0acd61253b6cd0a99c0d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 12:55:24 +0200 Subject: [PATCH 0185/1208] Support eval rand builtins --- crates/elephc-eval/src/interpreter.rs | 94 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 23 +++++++ 5 files changed, 122 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c1006b3a17..5794e8205c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -23,6 +23,7 @@ use crate::value::RuntimeCellHandle; use std::ffi::{CStr, CString}; use std::net::ToSocketAddrs; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; /// Internal statement-control result used to propagate eval returns and loops. @@ -92,6 +93,9 @@ const EVAL_STREAM_TRANSPORTS: &[&str] = &[ "tlsv1.2", "tlsv1.3", ]; +/// Monotonic salt mixed into eval `rand()`/`mt_rand()` and array key sampling. +static EVAL_RANDOM_COUNTER: AtomicU64 = AtomicU64::new(0); + /// Built-in stream filters reported by eval `stream_get_filters()`. const EVAL_STREAM_FILTERS: &[&str] = &[ "string.toupper", @@ -1394,6 +1398,7 @@ fn eval_positional_expr_call( "phpversion" => eval_builtin_phpversion(args, values), "pow" => eval_builtin_pow(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), + "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), "range" => eval_builtin_range(args, context, scope, values), "rawurldecode" | "urldecode" => { eval_builtin_url_decode(name, args, context, scope, values) @@ -1832,6 +1837,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "microtime" | "min" | "mkdir" + | "mt_rand" | "nl2br" | "number_format" | "ord" @@ -1841,6 +1847,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "php_uname" | "phpversion" | "putenv" + | "rand" | "range" | "rad2deg" | "rawurldecode" @@ -2066,6 +2073,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), "putenv" => Some(&["assignment"]), + "rand" | "mt_rand" => Some(&["min", "max"]), "range" => Some(&["start", "end"]), "realpath" => Some(&["path"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), @@ -2531,6 +2539,11 @@ fn eval_builtin_with_values( }; values.pow(*left, *right)? } + "rand" | "mt_rand" => match evaluated_args { + [] => eval_rand_result(None, None, values)?, + [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "rawurldecode" | "urldecode" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3780,11 +3793,61 @@ fn eval_array_rand_result( /// Chooses a pseudo-random array position within `[0, len)`. fn eval_random_position(len: usize) -> usize { + (eval_random_u128() % (len as u128)) as usize +} + +/// Produces a process-local pseudo-random word for non-cryptographic eval builtins. +fn eval_random_u128() -> u128 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_nanos()) .unwrap_or(0); - (nanos % (len as u128)) as usize + let counter = u128::from(EVAL_RANDOM_COUNTER.fetch_add(1, Ordering::Relaxed)); + let pid = u128::from(std::process::id()); + let mut value = nanos ^ (counter.wrapping_mul(0x9e37_79b9_7f4a_7c15)) ^ (pid << 64); + value ^= value >> 30; + value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); + value ^= value >> 27; + value = value.wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} + +/// Evaluates PHP `rand()` and `mt_rand()` over zero args or an inclusive range. +fn eval_builtin_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_rand_result(None, None, values), + [min, max] => { + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_rand_result(Some(min), Some(max), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns one non-cryptographic random integer using PHP's inclusive range rules. +fn eval_rand_result( + min: Option, + max: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (min, max) = match (min, max) { + (None, None) => (0, i64::from(i32::MAX)), + (Some(min), Some(max)) => (eval_int_value(min, values)?, eval_int_value(max, values)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let low = min.min(max); + let high = min.max(max); + let width = (i128::from(high) - i128::from(low) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(low) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) } /// Evaluates PHP `range()` over integer-compatible start and end expressions. @@ -11754,6 +11817,35 @@ return function_exists("array_rand");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `rand()` and `mt_rand()` return values inside PHP inclusive ranges. + #[test] + fn execute_program_dispatches_rand_builtins() { + let program = parse_fragment( + br#"$plain = rand(); +echo ($plain >= 0 && $plain <= 2147483647) ? "plain" : "bad"; +$bounded = rand(2, 4); +echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); +$same = mt_rand(max: 6, min: 6); +echo ":" . ($same === 6 ? "same" : "bad"); +$swapped = rand(10, 1); +echo ":" . (($swapped >= 1 && $swapped <= 10) ? "swap" : "bad"); +$call = call_user_func("mt_rand", 1, 1); +echo ":" . ($call === 1 ? "call" : "bad"); +$spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); +echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; +echo function_exists("rand"); +return function_exists("mt_rand");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "plain:range:same:swap:call:spread:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn execute_program_dispatches_array_fill_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index b64d355b84..1e26f13efc 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -63,7 +63,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, and `mt_rand()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 417d35256b..4144aea6bd 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -82,7 +82,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, and `intdiv()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, and `mt_rand()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 44ad037654..035d5afa6a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -114,6 +114,7 @@ public function __construct(int $value) { $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); $formatted_number = eval('return number_format(1234567.89, 2, ",", ".");'); $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5) . ":" . clamp(15, 0, 10);'); +$random_range = eval('$r = rand(1, 3); return ($r >= 1 && $r <= 3) ? "bounded" : "bad";'); $circle = eval('return round(pi(), 2);'); $extended_math = eval('return round(sin(pi() / 2), 0) . ":" . log(8, 2) . ":" . hypot(3, 4) . ":" . intdiv(7, 2);'); $float_predicates = eval('return (is_nan(fdiv(0, 0)) ? "nan" : "bad") . ":" . (is_infinite(fdiv(1, 0)) ? "inf" : "bad") . ":" . (is_finite(3.14) ? "finite" : "bad");'); @@ -239,6 +240,7 @@ public function __construct(int $value) { echo "rounded=" . $rounded . "\n"; echo "number-format=" . $formatted_number . "\n"; echo "minmax=" . $minmax . "\n"; +echo "random-range=" . $random_range . "\n"; echo "pi=" . $circle . "\n"; echo "extended-math=" . $extended_math . "\n"; echo "float-predicates=" . $float_predicates . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b5d50e0b40..8330764c25 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -943,6 +943,29 @@ echo function_exists("array_rand");'); assert_eq!(out, "idx:assoc:named:call:spread:1"); } +/// Verifies eval `rand()` and `mt_rand()` produce values in their PHP-visible ranges. +#[test] +fn test_eval_dispatches_rand_builtin_calls() { + let out = compile_and_run( + r#"= 0 && $plain <= 2147483647) ? "plain" : "bad"; +$bounded = rand(2, 4); +echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); +$same = mt_rand(max: 6, min: 6); +echo ":" . ($same === 6 ? "same" : "bad"); +$swapped = rand(10, 1); +echo ":" . (($swapped >= 1 && $swapped <= 10) ? "swap" : "bad"); +$call = call_user_func("mt_rand", 1, 1); +echo ":" . ($call === 1 ? "call" : "bad"); +$spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); +echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; +echo function_exists("rand"); echo function_exists("mt_rand");'); +"#, + ); + assert_eq!(out, "plain:range:same:swap:call:spread:11"); +} + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn test_eval_dispatches_array_fill_builtin_calls() { From 113da95a79c0aa66e6012e7470e9efdc092738cf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 13:08:10 +0200 Subject: [PATCH 0186/1208] Support eval date mktime builtins --- crates/elephc-eval/src/interpreter.rs | 279 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 6 +- docs/php/system-and-io.md | 6 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 21 ++ 5 files changed, 306 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5794e8205c..58d87a46a5 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -21,6 +21,7 @@ use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; use std::ffi::{CStr, CString}; +use std::mem::MaybeUninit; use std::net::ToSocketAddrs; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::sync::atomic::{AtomicU64, Ordering}; @@ -114,6 +115,41 @@ const EVAL_STREAM_FILTERS: &[&str] = &[ "bzip2.decompress", ]; +/// Full English month names used by eval `date()`. +const EVAL_MONTH_NAMES: &[&str; 12] = &[ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +/// Short English month names used by eval `date()`. +const EVAL_MONTH_SHORT_NAMES: &[&str; 12] = &[ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + +/// Full English weekday names used by eval `date()`. +const EVAL_WEEKDAY_NAMES: &[&str; 7] = &[ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +/// Short English weekday names used by eval `date()`. +const EVAL_WEEKDAY_SHORT_NAMES: &[&str; 7] = &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + /// Root package manifest used to mirror native `phpversion()` in the eval crate. const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../Cargo.toml"); @@ -1328,6 +1364,7 @@ fn eval_positional_expr_call( "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { eval_builtin_ctype(name, args, context, scope, values) } + "date" => eval_builtin_date(args, context, scope, values), "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "dirname" => eval_builtin_dirname(args, context, scope, values), @@ -1389,6 +1426,7 @@ fn eval_positional_expr_call( "log" => eval_builtin_log(args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), + "mktime" => eval_builtin_mktime(args, context, scope, values), "nl2br" => eval_builtin_nl2br(args, context, scope, values), "number_format" => eval_builtin_number_format(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), @@ -1746,6 +1784,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "ctype_alpha" | "ctype_digit" | "ctype_space" + | "date" | "define" | "defined" | "deg2rad" @@ -1837,6 +1876,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "microtime" | "min" | "mkdir" + | "mktime" | "mt_rand" | "nl2br" | "number_format" @@ -2024,6 +2064,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "copy" | "rename" => Some(&["from", "to"]), "crc32" => Some(&["string"]), "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), + "date" => Some(&["format", "timestamp"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "dirname" => Some(&["path", "levels"]), @@ -2064,6 +2105,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "max" | "min" => Some(&["value"]), "md5" | "sha1" => Some(&["string", "binary"]), "microtime" => Some(&["as_float"]), + "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), "nl2br" => Some(&["string", "use_xhtml"]), "number_format" => Some(&["num", "decimals", "decimal_separator", "thousands_separator"]), "ord" => Some(&["character"]), @@ -2661,6 +2703,11 @@ fn eval_builtin_with_values( }; eval_ctype_result(name, *value, values)? } + "date" => match evaluated_args { + [format] => eval_date_result(*format, None, values)?, + [format, timestamp] => eval_date_result(*format, Some(*timestamp), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "define" => eval_define_result(evaluated_args, context, values)?, "defined" => eval_defined_result(evaluated_args, context, values)?, "explode" => { @@ -2686,6 +2733,12 @@ fn eval_builtin_with_values( [] | [_] => eval_microtime_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, + "mktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? + }, "nl2br" => match evaluated_args { [value] => eval_nl2br_result(*value, true, values)?, [value, use_xhtml] => { @@ -5193,14 +5246,209 @@ fn eval_builtin_time( /// Returns the current Unix timestamp as a boxed PHP integer. fn eval_time_result(values: &mut impl RuntimeValueOps) -> Result { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) + values.int(eval_current_unix_timestamp()?) +} + +/// Returns the current Unix timestamp as an integer payload. +fn eval_current_unix_timestamp() -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) .map_err(|_| EvalStatus::RuntimeFatal)? .as_secs(); + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. +fn eval_builtin_date( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [format] => { + let format = eval_expr(format, context, scope, values)?; + eval_date_result(format, None, values) + } + [format, timestamp] => { + let format = eval_expr(format, context, scope, values)?; + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_date_result(format, Some(timestamp), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. +fn eval_date_result( + format: RuntimeCellHandle, + timestamp: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let format = values.string_bytes(format)?; + let timestamp = match timestamp { + Some(timestamp) => eval_int_value(timestamp, values)?, + None => eval_current_unix_timestamp()?, + }; + let tm = eval_localtime(timestamp)?; + let output = eval_format_date_bytes(&format, &tm, timestamp)?; + values.string_bytes_value(&output) +} + +/// Converts one Unix timestamp to local broken-down time through libc. +fn eval_localtime(timestamp: i64) -> Result { + let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; + let mut tm = MaybeUninit::::uninit(); + let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) +} + +/// Applies PHP `date()` tokens to one local broken-down timestamp. +fn eval_format_date_bytes( + format: &[u8], + tm: &libc::tm, + timestamp: i64, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut escaped = false; + for byte in format { + if escaped { + output.push(*byte); + escaped = false; + continue; + } + if *byte == b'\\' { + escaped = true; + continue; + } + eval_push_date_token(&mut output, *byte, tm, timestamp)?; + } + if escaped { + output.push(b'\\'); + } + Ok(output) +} + +/// Appends the expansion for one PHP `date()` token, or the token literal. +fn eval_push_date_token( + output: &mut Vec, + token: u8, + tm: &libc::tm, + timestamp: i64, +) -> Result<(), EvalStatus> { + match token { + b'Y' => eval_push_padded_number(output, i64::from(tm.tm_year) + 1900, 4), + b'm' => eval_push_padded_number(output, i64::from(tm.tm_mon) + 1, 2), + b'd' => eval_push_padded_number(output, i64::from(tm.tm_mday), 2), + b'H' => eval_push_padded_number(output, i64::from(tm.tm_hour), 2), + b'i' => eval_push_padded_number(output, i64::from(tm.tm_min), 2), + b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), + b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), + b'D' => { + output.extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()) + } + b'M' => { + output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) + } + b'N' => { + let weekday = tm.tm_wday; + let iso_weekday = if weekday == 0 { 7 } else { weekday }; + output.extend_from_slice(iso_weekday.to_string().as_bytes()); + } + b'j' => output.extend_from_slice(tm.tm_mday.to_string().as_bytes()), + b'n' => output.extend_from_slice((tm.tm_mon + 1).to_string().as_bytes()), + b'G' => output.extend_from_slice(tm.tm_hour.to_string().as_bytes()), + b'g' => { + let hour = tm.tm_hour % 12; + let hour = if hour == 0 { 12 } else { hour }; + output.extend_from_slice(hour.to_string().as_bytes()); + } + b'A' => output.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }), + b'a' => output.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }), + b'U' => output.extend_from_slice(timestamp.to_string().as_bytes()), + _ => output.push(token), + } + Ok(()) +} + +/// Returns a checked month index for PHP `date()` name tables. +fn eval_tm_month_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_mon).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_MONTH_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Returns a checked weekday index for PHP `date()` name tables. +fn eval_tm_weekday_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_wday).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_WEEKDAY_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Appends one zero-padded decimal value with the requested minimum width. +fn eval_push_padded_number(output: &mut Vec, value: i64, width: usize) { + output.extend_from_slice(format!("{value:0width$}").as_bytes()); +} + +/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. +fn eval_builtin_mktime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hour, minute, second, month, day, year] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hour = eval_expr(hour, context, scope, values)?; + let minute = eval_expr(minute, context, scope, values)?; + let second = eval_expr(second, context, scope, values)?; + let month = eval_expr(month, context, scope, values)?; + let day = eval_expr(day, context, scope, values)?; + let year = eval_expr(year, context, scope, values)?; + eval_mktime_result(hour, minute, second, month, day, year, values) +} + +/// Converts PHP date components to a local Unix timestamp through libc `mktime`. +fn eval_mktime_result( + hour: RuntimeCellHandle, + minute: RuntimeCellHandle, + second: RuntimeCellHandle, + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; + tm.tm_hour = eval_int_cell_as_c_int(hour, values)?; + tm.tm_min = eval_int_cell_as_c_int(minute, values)?; + tm.tm_sec = eval_int_cell_as_c_int(second, values)?; + tm.tm_mon = eval_int_cell_as_c_int(month, values)? - 1; + tm.tm_mday = eval_int_cell_as_c_int(day, values)?; + tm.tm_year = eval_int_cell_as_c_int(year, values)? - 1900; + tm.tm_isdst = -1; + let timestamp = unsafe { libc::mktime(&mut tm) }; let timestamp = i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(timestamp) } +/// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. +fn eval_int_cell_as_c_int( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} + /// Evaluates PHP `microtime()` with an optional ignored argument. fn eval_builtin_microtime( args: &[EvalExpr], @@ -12320,6 +12568,33 @@ return function_exists("sys_get_temp_dir");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `date()` formats libc local timestamps and `mktime()` builds them. + #[test] + fn execute_program_dispatches_date_mktime_builtins() { + let program = parse_fragment( + br#"$ts = mktime(13, 2, 3, 1, 2, 2024); +echo date("Y-m-d H:i:s", $ts); +echo ":" . date("j-n-G-g-A-a-N-D-M-l-F", $ts); +echo ":" . (date("U", $ts) === strval($ts) ? "U" : "bad"); +echo ":" . call_user_func("date", "Y", $ts); +$named = call_user_func_array("mktime", ["hour" => 0, "minute" => 0, "second" => 0, "month" => 1, "day" => 1, "year" => 2000]); +echo ":" . date(format: "Y", timestamp: $named); +echo ":"; echo function_exists("date"); +return function_exists("mktime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. #[test] fn execute_program_dispatches_microtime_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 1e26f13efc..088eb73d4c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -37,9 +37,9 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. -Eval zero-argument system and path calls include `time()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. +Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. -Eval `microtime()` returns a boxed float Unix timestamp with microsecond precision, matching the static lowering convention. +Eval `microtime()` returns a boxed float Unix timestamp with microsecond precision, matching the static lowering convention. Eval `date()` formats libc local time with elephc's documented token subset, and `mktime()` delegates local timestamp normalization to libc. Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zero after an uninterrupted sleep; `usleep()` returns null. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 4144aea6bd..7a8813776b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -52,9 +52,9 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. -Eval system and path calls include `time()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. +Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. -Eval `microtime()` returns a float Unix timestamp with microsecond precision, matching elephc's static `microtime()` lowering. +Eval `microtime()` returns a float Unix timestamp with microsecond precision, matching elephc's static `microtime()` lowering. Eval `date()` supports elephc's documented date tokens, and `mktime()` builds local timestamps through libc normalization. Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` when the sleep completes; `usleep()` returns `null`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 035d5afa6a..4a94264fb9 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -163,6 +163,7 @@ public function __construct(int $value) { $hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); $digest = eval('return md5("abc") . ":" . substr(hash("sha256", "abc"), 0, 8) . ":" . substr(hash_hmac("sha256", "data", "key"), 0, 8);'); $system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . (strlen(php_uname("s")) > 0 ? "uname" : "bad") . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); +$date_sample = eval('$ts = mktime(0, 0, 0, 1, 2, 2024); return date("Y-m-d", $ts);'); $micro_time = eval('return microtime(true) > 1000000000 ? "ok" : "bad";'); $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); @@ -289,6 +290,7 @@ public function __construct(int $value) { echo "hash-algos=" . $hash_algos . "\n"; echo "digest=" . $digest . "\n"; echo "system-info=" . $system_info . "\n"; +echo "date-sample=" . $date_sample . "\n"; echo "microtime=" . $micro_time . "\n"; echo "realpath-cache=" . $realpath_cache . "\n"; echo "environment=" . $environment . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8330764c25..8332c0636e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1495,6 +1495,27 @@ echo function_exists("sys_get_temp_dir");'); ); } +/// Verifies eval `date()` formats timestamps and `mktime()` creates them. +#[test] +fn test_eval_dispatches_date_mktime_builtin_calls() { + let out = compile_and_run( + r#" 0, "minute" => 0, "second" => 0, "month" => 1, "day" => 1, "year" => 2000]); +echo ":" . date(format: "Y", timestamp: $named); +echo ":"; echo function_exists("date"); echo function_exists("mktime");'); +"#, + ); + assert_eq!( + out, + "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:11" + ); +} + /// Verifies eval `microtime()` returns a plausible floating timestamp by all call paths. #[test] fn test_eval_dispatches_microtime_builtin_call() { From 99fb4337f1ddb9d8b52e55773c6e6b2b0bc8b72c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 13:20:22 +0200 Subject: [PATCH 0187/1208] Support eval spl_classes builtin --- crates/elephc-eval/src/interpreter.rs | 123 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 6 +- docs/php/spl.md | 4 + docs/php/system-and-io.md | 6 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 22 +++++ 6 files changed, 161 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 58d87a46a5..0e2abe69b2 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -115,6 +115,74 @@ const EVAL_STREAM_FILTERS: &[&str] = &[ "bzip2.decompress", ]; +/// SPL/core type names reported by eval `spl_classes()`. +/// +/// Mirrors `src/codegen/builtins/spl/mod.rs::SPL_CLASS_NAMES` so dynamic eval +/// exposes the same static registry snapshot as native code. +const EVAL_SPL_CLASS_NAMES: &[&str] = &[ + "AppendIterator", + "ArrayAccess", + "ArrayIterator", + "ArrayObject", + "BadFunctionCallException", + "BadMethodCallException", + "CachingIterator", + "CallbackFilterIterator", + "Countable", + "DomainException", + "DirectoryIterator", + "EmptyIterator", + "Error", + "Exception", + "FilterIterator", + "FilesystemIterator", + "GlobIterator", + "InfiniteIterator", + "InvalidArgumentException", + "Iterator", + "IteratorAggregate", + "IteratorIterator", + "JsonSerializable", + "LengthException", + "LimitIterator", + "LogicException", + "MultipleIterator", + "NoRewindIterator", + "OuterIterator", + "OutOfBoundsException", + "OutOfRangeException", + "OverflowException", + "ParentIterator", + "RangeException", + "RecursiveArrayIterator", + "RecursiveCachingIterator", + "RecursiveCallbackFilterIterator", + "RecursiveDirectoryIterator", + "RecursiveFilterIterator", + "RecursiveIterator", + "RecursiveIteratorIterator", + "RecursiveRegexIterator", + "RegexIterator", + "RuntimeException", + "SeekableIterator", + "SplDoublyLinkedList", + "SplFixedArray", + "SplFileInfo", + "SplFileObject", + "SplObserver", + "SplQueue", + "SplStack", + "SplSubject", + "SplTempFileObject", + "Stringable", + "Throwable", + "Traversable", + "TypeError", + "UnderflowException", + "UnexpectedValueException", + "ValueError", +]; + /// Full English month names used by eval `date()`. const EVAL_MONTH_NAMES: &[&str; 12] = &[ "January", @@ -1454,6 +1522,7 @@ fn eval_positional_expr_call( "isset" => eval_builtin_isset(args, context, scope, values), "sleep" => eval_builtin_sleep(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), + "spl_classes" => eval_builtin_spl_classes(args, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "tempnam" => eval_builtin_tempnam(args, context, scope, values), "time" => eval_builtin_time(args, values), @@ -1907,6 +1976,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "sin" | "sinh" | "sqrt" + | "spl_classes" | "strcasecmp" | "stream_get_filters" | "stream_get_transports" @@ -2121,6 +2191,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), + "spl_classes" => Some(&[]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), @@ -2615,6 +2686,12 @@ fn eval_builtin_with_values( }; values.sqrt(*value)? } + "spl_classes" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values)? + } "strrev" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -5207,6 +5284,24 @@ fn eval_static_string_array_result( Ok(result) } +/// Evaluates PHP `spl_classes()` with no arguments. +fn eval_builtin_spl_classes( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values) +} + +/// Builds the static class-name list returned by eval `spl_classes()`. +fn eval_spl_classes_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) +} + /// Evaluates PHP stream introspection list builtins with no arguments. fn eval_builtin_stream_introspection( name: &str, @@ -12670,6 +12765,34 @@ return function_exists("stream_get_filters");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. + #[test] + fn execute_program_dispatches_spl_classes_builtin() { + let program = parse_fragment( + br#"$names = spl_classes(); +echo count($names) . ":" . $names[0] . ":" . $names[55] . ":"; +echo (in_array("Exception", $names) ? "exception" : "bad") . ":"; +echo (in_array("SplDoublyLinkedList", $names) ? "list" : "bad") . ":"; +$call = call_user_func("spl_classes"); +echo (in_array("Throwable", $call) ? "call" : "bad") . ":"; +$spread = call_user_func_array("spl_classes", []); +echo (count($spread) === count($names) ? "spread" : "bad") . ":"; +echo function_exists("spl_classes"); +return is_callable("spl_classes");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "61:AppendIterator:Throwable:exception:list:call:spread:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval environment builtins read, write, unset, and dispatch dynamically. #[test] fn execute_program_dispatches_environment_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 088eb73d4c..ce784d7cc7 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,12 +17,16 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, and `class_exists()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. +Eval SPL class introspection dispatch includes `spl_classes()` through direct +calls, callable dispatch, and `function_exists()` probes. The interpreter builds +the same static class-name snapshot as the native SPL builtin. + Eval `ucwords()` performs ASCII word-start capitalization and honors the optional `separators` argument. Eval `strstr()` uses the shared byte-search helper to return the matching suffix, optional prefix, or PHP `false`. diff --git a/docs/php/spl.md b/docs/php/spl.md index 052c62494e..bf6fdfc2b0 100644 --- a/docs/php/spl.md +++ b/docs/php/spl.md @@ -575,6 +575,10 @@ SPL autoload and class-introspection helpers are documented in `spl_object_id()`, `spl_object_hash()`, `class_implements()`, `class_parents()`, and `class_uses()`. +Inside `eval()`, `spl_classes()` is available through direct calls, +`call_user_func()`, `call_user_func_array()`, and `function_exists()` and +returns the same static registry snapshot as native code. + ## Iterator Helper Functions The iterator helper functions cover the PHP SPL traversal helpers: diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7a8813776b..a9d6667b80 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -64,6 +64,10 @@ Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_c Eval stream introspection calls include `stream_get_filters()`, `stream_get_transports()`, and `stream_get_wrappers()` with direct, callable, and `function_exists()` dispatch paths. They return the same static built-in lists documented in [Streams](streams.md). +Eval SPL class introspection supports `spl_classes()` with direct, callable, and +`function_exists()` dispatch paths. It returns the same static registry snapshot +documented in [SPL](spl.md). + Eval host-name calls include `gethostname()`, `gethostbyname()`, and `gethostbyaddr()` with direct, named-argument, callable, and `function_exists()` dispatch paths. `gethostbyname()` resolves IPv4 host names and returns the original string when no IPv4 address is available. `gethostbyaddr()` reverse-resolves IPv4 dotted-quad addresses, returns the original address when no record exists, and returns `false` for malformed addresses. Eval protocol and service database calls include `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, and `getservbyport()` with direct, named-argument, callable, and `function_exists()` dispatch paths. diff --git a/examples/eval/main.php b/examples/eval/main.php index 4a94264fb9..787c7f5b47 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -172,6 +172,7 @@ public function __construct(int $value) { $protocol_services = eval('return getprotobyname("tcp") . ":" . getprotobynumber(17) . ":" . getservbyname("http", "tcp") . ":" . getservbyport(443, "tcp");'); $ip_conversion = eval('$packed = inet_pton("1.2.3.4"); return long2ip(ip2long("192.168.1.1")) . ":" . bin2hex($packed) . ":" . inet_ntop($packed);'); $stream_introspection = eval('$wrappers = stream_get_wrappers(); $transports = stream_get_transports(); $filters = stream_get_filters(); return count($wrappers) . ":" . $wrappers[0] . ":" . count($transports) . ":" . (in_array("string.rot13", $filters) ? "rot13" : "missing");'); +$spl_classes = eval('$names = spl_classes(); return count($names) . ":" . (in_array("Exception", $names) ? "exception" : "missing");'); $path_components = eval('return basename("/var/log/syslog.log", ".log") . ":" . dirname("/usr/local/bin/tool", 2);'); $resolved_path = eval('return realpath(".") !== false ? "resolved" : "missing";'); $path_info = eval('$info = pathinfo("/var/log/syslog.log"); $match = fnmatch("*.LOG", "system.log", FNM_CASEFOLD); return $info["basename"] . ":" . pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":" . ($match ? "match" : "bad");'); @@ -299,6 +300,7 @@ public function __construct(int $value) { echo "protocol-services=" . $protocol_services . "\n"; echo "ip-conversion=" . $ip_conversion . "\n"; echo "stream-introspection=" . $stream_introspection . "\n"; +echo "spl-classes=" . $spl_classes . "\n"; echo "path-components=" . $path_components . "\n"; echo "realpath=" . $resolved_path . "\n"; echo "pathinfo=" . $path_info . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8332c0636e..44c94717b7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -966,6 +966,28 @@ echo function_exists("rand"); echo function_exists("mt_rand");'); assert_eq!(out, "plain:range:same:swap:call:spread:11"); } +/// Verifies eval `spl_classes()` exposes the same static SPL class list as native code. +#[test] +fn test_eval_dispatches_spl_classes_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 13:35:19 +0200 Subject: [PATCH 0188/1208] Support eval strtotime builtin --- crates/elephc-eval/src/interpreter.rs | 190 ++++++++++++++++++++++++-- docs/internals/the-runtime.md | 6 +- docs/php/system-and-io.md | 6 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 25 ++++ 5 files changed, 215 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0e2abe69b2..cf16d6006e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1530,6 +1530,7 @@ fn eval_positional_expr_call( "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { eval_builtin_stream_introspection(name, args, values) } + "strtotime" => eval_builtin_strtotime(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), @@ -1996,6 +1997,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "str_pad" | "str_split" | "strstr" + | "strtotime" | "substr" | "stripslashes" | "strtolower" @@ -2195,6 +2197,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), + "strtotime" => Some(&["datetime"]), "strstr" => Some(&["haystack", "needle", "before_needle"]), "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), @@ -3074,6 +3077,12 @@ fn eval_builtin_with_values( } eval_stream_introspection_result(name, values)? } + "strtotime" => { + let [datetime] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_strtotime_result(*datetime, values)? + } "umask" => match evaluated_args { [] => eval_umask_result(None, values)?, [mask] => eval_umask_result(Some(*mask), values)?, @@ -5522,17 +5531,36 @@ fn eval_mktime_result( year: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { + let timestamp = eval_mktime_timestamp( + eval_int_cell_as_c_int(hour, values)?, + eval_int_cell_as_c_int(minute, values)?, + eval_int_cell_as_c_int(second, values)?, + eval_int_cell_as_c_int(month, values)?, + eval_int_cell_as_c_int(day, values)?, + eval_int_cell_as_c_int(year, values)?, + )?; + values.int(timestamp) +} + +/// Converts local date components into a Unix timestamp through libc `mktime`. +fn eval_mktime_timestamp( + hour: libc::c_int, + minute: libc::c_int, + second: libc::c_int, + month: libc::c_int, + day: libc::c_int, + year: libc::c_int, +) -> Result { let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; - tm.tm_hour = eval_int_cell_as_c_int(hour, values)?; - tm.tm_min = eval_int_cell_as_c_int(minute, values)?; - tm.tm_sec = eval_int_cell_as_c_int(second, values)?; - tm.tm_mon = eval_int_cell_as_c_int(month, values)? - 1; - tm.tm_mday = eval_int_cell_as_c_int(day, values)?; - tm.tm_year = eval_int_cell_as_c_int(year, values)? - 1900; + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = second; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_year = year - 1900; tm.tm_isdst = -1; let timestamp = unsafe { libc::mktime(&mut tm) }; - let timestamp = i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(timestamp) + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) } /// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. @@ -5544,6 +5572,122 @@ fn eval_int_cell_as_c_int( libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) } +/// Evaluates PHP `strtotime(datetime)` for eval's supported date-string subset. +fn eval_builtin_strtotime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [datetime] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let datetime = eval_expr(datetime, context, scope, values)?; + eval_strtotime_result(datetime, values) +} + +/// Parses one eval `strtotime()` input and boxes the resulting timestamp. +fn eval_strtotime_result( + datetime: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(datetime)?; + let timestamp = eval_strtotime_bytes(&bytes)?; + values.int(timestamp) +} + +/// Parses eval's supported `strtotime()` strings into local Unix timestamps. +fn eval_strtotime_bytes(bytes: &[u8]) -> Result { + let bytes = eval_trim_ascii_whitespace(bytes); + if bytes.eq_ignore_ascii_case(b"now") { + return eval_current_unix_timestamp(); + } + let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { + return Ok(-1); + }; + eval_mktime_timestamp(hour, minute, second, month, day, year) +} + +/// Trims ASCII whitespace from both ends of one byte slice. +fn eval_trim_ascii_whitespace(bytes: &[u8]) -> &[u8] { + let mut start = 0; + let mut end = bytes.len(); + while start < end && bytes[start].is_ascii_whitespace() { + start += 1; + } + while end > start && bytes[end - 1].is_ascii_whitespace() { + end -= 1; + } + &bytes[start..end] +} + +/// Parses fixed-width ISO date and datetime forms supported by eval `strtotime()`. +fn eval_parse_iso_datetime( + bytes: &[u8], +) -> Option<( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, +)> { + if bytes.len() != 10 && bytes.len() != 16 && bytes.len() != 19 { + return None; + } + if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { + return None; + } + let year = eval_parse_fixed_digits(bytes, 0, 4)?; + let month = eval_parse_fixed_digits(bytes, 5, 2)?; + let day = eval_parse_fixed_digits(bytes, 8, 2)?; + let (hour, minute, second) = if bytes.len() == 10 { + (0, 0, 0) + } else { + if !matches!(bytes.get(10), Some(b' ') | Some(b'T') | Some(b't')) { + return None; + } + if bytes.get(13) != Some(&b':') { + return None; + } + let hour = eval_parse_fixed_digits(bytes, 11, 2)?; + let minute = eval_parse_fixed_digits(bytes, 14, 2)?; + let second = if bytes.len() == 19 { + if bytes.get(16) != Some(&b':') { + return None; + } + eval_parse_fixed_digits(bytes, 17, 2)? + } else { + 0 + }; + (hour, minute, second) + }; + if !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || !(0..=23).contains(&hour) + || !(0..=59).contains(&minute) + || !(0..=59).contains(&second) + { + return None; + } + Some((year, month, day, hour, minute, second)) +} + +/// Parses a fixed-width decimal field as a libc-compatible integer. +fn eval_parse_fixed_digits(bytes: &[u8], start: usize, len: usize) -> Option { + let end = start.checked_add(len)?; + let field = bytes.get(start..end)?; + let mut value: libc::c_int = 0; + for byte in field { + if !byte.is_ascii_digit() { + return None; + } + value = value.checked_mul(10)?; + value = value.checked_add(libc::c_int::from(byte - b'0'))?; + } + Some(value) +} + /// Evaluates PHP `microtime()` with an optional ignored argument. fn eval_builtin_microtime( args: &[EvalExpr], @@ -12690,6 +12834,36 @@ return function_exists("mktime");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. + #[test] + fn execute_program_dispatches_strtotime_builtin() { + let program = parse_fragment( + br#"$date = strtotime("2024-06-15"); +echo date("Y-m-d H:i:s", $date); +$full = strtotime("2024-06-15 12:30:45"); +echo ":" . date("Y-m-d H:i:s", $full); +$short = strtotime("2024-06-15T12:30"); +echo ":" . date("Y-m-d H:i:s", $short); +echo ":" . (strtotime("2024/06/15") === -1 ? "bad" : "wrong"); +$call = call_user_func("strtotime", "2024-01-02 03:04:05"); +echo ":" . date("Y-m-d H:i:s", $call); +$spread = call_user_func_array("strtotime", ["datetime" => "2024-01-02"]); +echo ":" . date("Y-m-d", $spread) . ":"; +return function_exists("strtotime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. #[test] fn execute_program_dispatches_microtime_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index ce784d7cc7..719ab43f77 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -41,9 +41,9 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. -Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. +Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. -Eval `microtime()` returns a boxed float Unix timestamp with microsecond precision, matching the static lowering convention. Eval `date()` formats libc local time with elephc's documented token subset, and `mktime()` delegates local timestamp normalization to libc. +Eval `microtime()` returns a boxed float Unix timestamp with microsecond precision, matching the static lowering convention. Eval `date()` formats libc local time with elephc's documented token subset, `mktime()` delegates local timestamp normalization to libc, and `strtotime()` parses `now`, fixed-width ISO dates, fixed-width ISO datetimes with optional seconds, and `T` datetime separators. Unsupported eval `strtotime()` inputs return `-1`. Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zero after an uninterrupted sleep; `usleep()` returns null. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a9d6667b80..e6adb2e7d2 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -52,9 +52,9 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. -Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. +Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. -Eval `microtime()` returns a float Unix timestamp with microsecond precision, matching elephc's static `microtime()` lowering. Eval `date()` supports elephc's documented date tokens, and `mktime()` builds local timestamps through libc normalization. +Eval `microtime()` returns a float Unix timestamp with microsecond precision, matching elephc's static `microtime()` lowering. Eval `date()` supports elephc's documented date tokens, `mktime()` builds local timestamps through libc normalization, and `strtotime()` accepts `now`, `YYYY-MM-DD`, `YYYY-MM-DD HH:MM`, `YYYY-MM-DD HH:MM:SS`, and the same datetime forms with `T` as the separator. Unsupported eval `strtotime()` strings return `-1`. Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` when the sleep completes; `usleep()` returns `null`. diff --git a/examples/eval/main.php b/examples/eval/main.php index 787c7f5b47..037cfb814c 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -164,6 +164,7 @@ public function __construct(int $value) { $digest = eval('return md5("abc") . ":" . substr(hash("sha256", "abc"), 0, 8) . ":" . substr(hash_hmac("sha256", "data", "key"), 0, 8);'); $system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . (strlen(php_uname("s")) > 0 ? "uname" : "bad") . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); $date_sample = eval('$ts = mktime(0, 0, 0, 1, 2, 2024); return date("Y-m-d", $ts);'); +$strtotime_sample = eval('$ts = strtotime("2024-01-02 03:04:05"); return date("Y-m-d H:i:s", $ts);'); $micro_time = eval('return microtime(true) > 1000000000 ? "ok" : "bad";'); $realpath_cache = eval('return count(realpath_cache_get()) . ":" . realpath_cache_size();'); $environment = eval('putenv("ELEPHC_EVAL_EXAMPLE=ok"); $value = getenv("ELEPHC_EVAL_EXAMPLE"); putenv("ELEPHC_EVAL_EXAMPLE"); return $value . ":" . (getenv("ELEPHC_EVAL_EXAMPLE") === "" ? "cleared" : "left");'); @@ -292,6 +293,7 @@ public function __construct(int $value) { echo "digest=" . $digest . "\n"; echo "system-info=" . $system_info . "\n"; echo "date-sample=" . $date_sample . "\n"; +echo "strtotime-sample=" . $strtotime_sample . "\n"; echo "microtime=" . $micro_time . "\n"; echo "realpath-cache=" . $realpath_cache . "\n"; echo "environment=" . $environment . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 44c94717b7..8661c70794 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1538,6 +1538,31 @@ echo ":"; echo function_exists("date"); echo function_exists("mktime");'); ); } +/// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. +#[test] +fn test_eval_dispatches_strtotime_builtin_calls() { + let out = compile_and_run( + r#" "2024-01-02"]); +echo ":" . date("Y-m-d", $spread) . ":"; +echo function_exists("strtotime");'); +"#, + ); + assert_eq!( + out, + "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:1" + ); +} + /// Verifies eval `microtime()` returns a plausible floating timestamp by all call paths. #[test] fn test_eval_dispatches_microtime_builtin_call() { From 911c64a4099f8f7c6a7b5fa8a272ff93835318b6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 13:56:44 +0200 Subject: [PATCH 0189/1208] Support eval printf builtins --- crates/elephc-eval/src/interpreter.rs | 460 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 6 +- docs/php/system-and-io.md | 6 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 25 ++ 5 files changed, 495 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index cf16d6006e..d9b9264235 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -41,6 +41,19 @@ struct EvaluatedCallArg { value: RuntimeCellHandle, } +/// Parsed flags for one eval `sprintf()` conversion specifier. +#[derive(Clone, Copy)] +struct EvalSprintfSpec { + left_align: bool, + force_sign: bool, + space_sign: bool, + zero_pad: bool, + alternate: bool, + width: Option, + precision: Option, + specifier: u8, +} + /// Hash algorithm names supported by eval `hash_algos()`, matching native runtime order. const EVAL_HASH_ALGOS: &[&str] = &[ "md2", @@ -1523,6 +1536,7 @@ fn eval_positional_expr_call( "sleep" => eval_builtin_sleep(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "spl_classes" => eval_builtin_spl_classes(args, values), + "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "tempnam" => eval_builtin_tempnam(args, context, scope, values), "time" => eval_builtin_time(args, values), @@ -1556,6 +1570,7 @@ fn eval_positional_expr_call( "ucwords" => eval_builtin_ucwords(args, context, scope, values), "umask" => eval_builtin_umask(args, context, scope, values), "usleep" => eval_builtin_usleep(args, context, scope, values), + "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), } @@ -1978,6 +1993,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "sinh" | "sqrt" | "spl_classes" + | "sprintf" | "strcasecmp" | "stream_get_filters" | "stream_get_transports" @@ -2019,6 +2035,9 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "urldecode" | "urlencode" | "usleep" + | "printf" + | "vprintf" + | "vsprintf" | "wordwrap" | "lstat" ) @@ -2194,6 +2213,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), "spl_classes" => Some(&[]), + "sprintf" | "printf" => Some(&["format", "values"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), @@ -2216,6 +2236,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "ucwords" => Some(&["string", "separators"]), "umask" => Some(&["mask"]), "usleep" => Some(&["microseconds"]), + "vsprintf" | "vprintf" => Some(&["format", "values"]), "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), _ => None, } @@ -2695,6 +2716,7 @@ fn eval_builtin_with_values( } eval_spl_classes_result(values)? } + "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, "strrev" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3157,6 +3179,7 @@ fn eval_builtin_with_values( [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, _ => return Err(EvalStatus::RuntimeFatal), }, + "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, "wordwrap" => match evaluated_args { [value] => eval_wordwrap_result(*value, None, None, None, values)?, [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, @@ -4328,6 +4351,412 @@ fn eval_number_format_result( values.string_bytes_value(&output) } +/// Evaluates direct positional `sprintf()` or `printf()` calls in source order. +fn eval_builtin_sprintf_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_sprintf_like_result(name, &evaluated_args, values) +} + +/// Evaluates direct positional `vsprintf()` or `vprintf()` calls in source order. +fn eval_builtin_vsprintf_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_vsprintf_like_result(name, &evaluated_args, values) +} + +/// Dispatches already evaluated `sprintf()` or `printf()` arguments. +fn eval_sprintf_like_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "sprintf" => eval_sprintf_result(evaluated_args, values), + "printf" => eval_printf_result(evaluated_args, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Dispatches already evaluated `vsprintf()` or `vprintf()` arguments. +fn eval_vsprintf_like_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "vsprintf" => eval_vsprintf_result(evaluated_args, values), + "vprintf" => eval_vprintf_result(evaluated_args, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Formats `sprintf()` arguments and returns the resulting PHP string. +fn eval_sprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats `printf()` arguments, echoes the result, and returns its byte count. +fn eval_printf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} + +/// Formats `vsprintf()` array arguments and returns the resulting PHP string. +fn eval_vsprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = eval_sprintf_bytes(&format, &format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. +fn eval_vprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = eval_sprintf_bytes(&format, &format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} + +/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. +fn eval_sprintf_argument_array_values( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + let mut args = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(array, position)?; + args.push(values.array_get(array, key)?); + } + Ok(args) +} + +/// Formats one printf-style byte string through eval runtime value coercions. +fn eval_sprintf_bytes( + format: &[u8], + args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut index = 0; + let mut arg_index = 0; + while index < format.len() { + if format[index] != b'%' { + output.push(format[index]); + index += 1; + continue; + } + index += 1; + if index >= format.len() { + break; + } + if format[index] == b'%' { + output.push(b'%'); + index += 1; + continue; + } + + let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; + index = next_index; + let Some(arg) = args.get(arg_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + arg_index += 1; + let bytes = eval_format_sprintf_arg(spec, arg, values)?; + output.extend_from_slice(&bytes); + } + Ok(output) +} + +/// Parses flags, width, precision, and terminal type for one format specifier. +fn eval_parse_sprintf_spec( + format: &[u8], + mut index: usize, +) -> Result<(EvalSprintfSpec, usize), EvalStatus> { + let mut spec = EvalSprintfSpec { + left_align: false, + force_sign: false, + space_sign: false, + zero_pad: false, + alternate: false, + width: None, + precision: None, + specifier: 0, + }; + while index < format.len() { + match format[index] { + b'-' => spec.left_align = true, + b'+' => spec.force_sign = true, + b' ' => spec.space_sign = true, + b'0' => spec.zero_pad = true, + b'#' => spec.alternate = true, + _ => break, + } + index += 1; + } + let (width, next_index) = eval_parse_sprintf_number(format, index)?; + spec.width = width; + index = next_index; + if index < format.len() && format[index] == b'.' { + let (precision, next_index) = eval_parse_sprintf_number(format, index + 1)?; + spec.precision = Some(precision.unwrap_or(0)); + index = next_index; + } + if index >= format.len() { + return Err(EvalStatus::RuntimeFatal); + } + spec.specifier = format[index]; + Ok((spec, index + 1)) +} + +/// Parses an unsigned decimal number from a format specifier component. +fn eval_parse_sprintf_number( + format: &[u8], + mut index: usize, +) -> Result<(Option, usize), EvalStatus> { + let start = index; + let mut value = 0usize; + while index < format.len() && format[index].is_ascii_digit() { + value = value + .checked_mul(10) + .and_then(|value| value.checked_add(usize::from(format[index] - b'0'))) + .ok_or(EvalStatus::RuntimeFatal)?; + index += 1; + } + if index == start { + Ok((None, index)) + } else { + Ok((Some(value), index)) + } +} + +/// Formats one runtime value according to a parsed eval sprintf specifier. +fn eval_format_sprintf_arg( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match spec.specifier { + b's' => eval_format_sprintf_string(spec, value, values), + b'f' | b'e' | b'g' => eval_format_sprintf_float(spec, value, values), + b'c' => eval_format_sprintf_char(spec, value, values), + _ => eval_format_sprintf_int(spec, value, values), + } +} + +/// Formats a `%s` operand after PHP string coercion. +fn eval_format_sprintf_string( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bytes = values.string_bytes(value)?; + if let Some(precision) = spec.precision { + bytes.truncate(precision); + } + Ok(eval_sprintf_apply_width(bytes, spec, false)) +} + +/// Formats an integer-like operand for decimal, unsigned, hex, and octal specifiers. +fn eval_format_sprintf_int( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + let mut output = Vec::new(); + match spec.specifier { + b'u' => { + let digits = eval_sprintf_precision_pad((value as u64).to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + b'x' | b'X' => { + let unsigned = value as u64; + if spec.alternate && unsigned != 0 { + output.extend_from_slice(if spec.specifier == b'X' { b"0X" } else { b"0x" }); + } + let digits = if spec.specifier == b'X' { + format!("{unsigned:X}").into_bytes() + } else { + format!("{unsigned:x}").into_bytes() + }; + output.extend_from_slice(&eval_sprintf_precision_pad(digits, spec)); + } + b'o' => { + let unsigned = value as u64; + let mut digits = eval_sprintf_precision_pad(format!("{unsigned:o}").into_bytes(), spec); + if spec.alternate && !digits.starts_with(b"0") { + output.push(b'0'); + } + output.append(&mut digits); + } + _ => { + let value = value as i128; + let magnitude = if value < 0 { (-value) as u128 } else { value as u128 }; + if value < 0 { + output.push(b'-'); + } else if spec.force_sign { + output.push(b'+'); + } else if spec.space_sign { + output.push(b' '); + } + let digits = eval_sprintf_precision_pad(magnitude.to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Formats a `%c` operand as the low byte of its integer value. +fn eval_format_sprintf_char( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + Ok(eval_sprintf_apply_width(vec![value as u8], spec, false)) +} + +/// Formats a floating-point operand for the eval printf-family subset. +fn eval_format_sprintf_float( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_float_value(value, values)?; + let precision = spec.precision.unwrap_or(6); + let mut output = if value.is_nan() { + b"NAN".to_vec() + } else if value.is_infinite() { + b"INF".to_vec() + } else { + match spec.specifier { + b'e' => format!("{value:.precision$e}").into_bytes(), + b'g' => format!("{value:.precision$}").into_bytes(), + _ => format!("{value:.precision$}").into_bytes(), + } + }; + if value.is_sign_negative() && !output.starts_with(b"-") { + output.insert(0, b'-'); + } else if value.is_sign_positive() && value.is_finite() { + if spec.force_sign { + output.insert(0, b'+'); + } else if spec.space_sign { + output.insert(0, b' '); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Applies integer precision by left-padding digits with zeros. +fn eval_sprintf_precision_pad(mut digits: Vec, spec: EvalSprintfSpec) -> Vec { + if matches!(spec.precision, Some(0)) && digits == b"0" { + digits.clear(); + return digits; + } + let Some(precision) = spec.precision else { + return digits; + }; + if digits.len() >= precision { + return digits; + } + let mut output = vec![b'0'; precision - digits.len()]; + output.append(&mut digits); + output +} + +/// Applies field width and alignment to one formatted eval sprintf replacement. +fn eval_sprintf_apply_width(mut bytes: Vec, spec: EvalSprintfSpec, numeric: bool) -> Vec { + let Some(width) = spec.width else { + return bytes; + }; + if bytes.len() >= width { + return bytes; + } + let pad_len = width - bytes.len(); + if spec.left_align { + bytes.extend(std::iter::repeat_n(b' ', pad_len)); + return bytes; + } + if numeric && spec.zero_pad && spec.precision.is_none() { + let prefix_len = eval_sprintf_zero_pad_prefix_len(&bytes); + let mut output = Vec::with_capacity(width); + output.extend_from_slice(&bytes[..prefix_len]); + output.extend(std::iter::repeat_n(b'0', pad_len)); + output.extend_from_slice(&bytes[prefix_len..]); + return output; + } + let mut output = Vec::with_capacity(width); + output.extend(std::iter::repeat_n(b' ', pad_len)); + output.append(&mut bytes); + output +} + +/// Returns the sign and alternate-prefix length that should precede zero padding. +fn eval_sprintf_zero_pad_prefix_len(bytes: &[u8]) -> usize { + let mut prefix_len = usize::from(matches!(bytes.first(), Some(b'+' | b'-' | b' '))); + if bytes.len() >= prefix_len + 2 + && bytes[prefix_len] == b'0' + && matches!(bytes[prefix_len + 1], b'x' | b'X') + { + prefix_len += 2; + } + prefix_len +} + /// Converts one eval value to PHP float and returns the scalar payload. fn eval_float_value( value: RuntimeCellHandle, @@ -14213,6 +14642,37 @@ return function_exists("number_format");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval printf-family builtins format, print, and dispatch through callables. + #[test] + fn execute_program_dispatches_printf_family_builtins() { + let program = parse_fragment( + br#"echo sprintf("Hello %s", "World"); echo ":"; +echo sprintf("%05d", 42); echo ":"; +echo sprintf("%.2f", 3.14159); echo ":"; +echo sprintf("%-6s|", "hi"); echo ":"; +$printed = printf("%s=%d", "n", 42); +echo ":" . $printed . ":"; +echo vsprintf("%s/%d/%.1f", ["age", 42, 3]); echo ":"; +$vprinted = vprintf("%s-%d", ["v", 7]); +echo ":" . $vprinted . ":"; +echo call_user_func("sprintf", "%+d", 42); echo ":"; +echo call_user_func_array("vsprintf", ["format" => "%s", "values" => ["spread"]]); echo ":"; +echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); +return is_callable("vprintf");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `min()` and `max()` select numeric values directly and by callable. #[test] fn execute_program_dispatches_min_max_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 719ab43f77..7df91590ae 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -39,6 +39,8 @@ Eval `wordwrap()` performs the same word-aware byte scan as the static helper, p Eval `number_format()` formats finite numeric cells inside the interpreter, including grouping and custom separators. +Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. + Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. @@ -67,7 +69,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, and `mt_rand()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()` and printf-family formatting via `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, and `mt_rand()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index e6adb2e7d2..ce1c570abe 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -50,6 +50,8 @@ Eval `wordwrap()` supports width, break string, and `cut_long_words` arguments w Eval `number_format()` supports decimal counts plus custom decimal and thousands separators. +Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. + Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. @@ -86,7 +88,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, and `mt_rand()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and printf-family formatting through `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, and `mt_rand()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index 037cfb814c..ab1d4c05a3 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -113,6 +113,7 @@ public function __construct(int $value) { $builtin_power = eval('return pow(2, 5) . ":" . gettype(pow(2, 3));'); $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); $formatted_number = eval('return number_format(1234567.89, 2, ",", ".");'); +$formatted_text = eval('return sprintf("%s:%04d:%s", "item", 7, vsprintf("%.1f", [3]));'); $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5) . ":" . clamp(15, 0, 10);'); $random_range = eval('$r = rand(1, 3); return ($r >= 1 && $r <= 3) ? "bounded" : "bad";'); $circle = eval('return round(pi(), 2);'); @@ -242,6 +243,7 @@ public function __construct(int $value) { echo "builtin-power=" . $builtin_power . "\n"; echo "rounded=" . $rounded . "\n"; echo "number-format=" . $formatted_number . "\n"; +echo "printf-format=" . $formatted_text . "\n"; echo "minmax=" . $minmax . "\n"; echo "random-range=" . $random_range . "\n"; echo "pi=" . $circle . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8661c70794..1cc41460ee 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2630,6 +2630,31 @@ echo ":"; echo function_exists("number_format");'); ); } +/// Verifies eval printf-family builtins format, print, and dispatch through callables. +#[test] +fn test_eval_dispatches_printf_family_builtin_calls() { + let out = compile_and_run( + r#" "%s", "values" => ["spread"]]); echo ":"; +echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); echo is_callable("vprintf");'); +"#, + ); + assert_eq!( + out, + "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:1111" + ); +} + /// Verifies eval `min()` and `max()` select numeric values directly and through callables. #[test] fn test_eval_dispatches_min_max_builtin_calls() { From 8aabb0f8abdd7de85f5b79866836a9c84d62df08 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 14:10:49 +0200 Subject: [PATCH 0190/1208] Support eval runtime constants --- crates/elephc-eval/src/interpreter.rs | 93 ++++++++++++++++++++++----- docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 2 + examples/eval/main.php | 2 + tests/codegen/eval.rs | 25 +++++++ 5 files changed, 108 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d9b9264235..4d61f5ee05 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -54,6 +54,12 @@ struct EvalSprintfSpec { specifier: u8, } +/// Eval-visible predefined constant payloads that are not stored in the dynamic context. +enum EvalPredefinedConstant { + Int(i64), + String(&'static str), +} + /// Hash algorithm names supported by eval `hash_algos()`, matching native runtime order. const EVAL_HASH_ALGOS: &[&str] = &[ "md2", @@ -1661,7 +1667,7 @@ fn eval_define_name( if name.is_empty() { return Err(EvalStatus::RuntimeFatal); } - if eval_predefined_int_constant(&name).is_some() || context.has_constant(&name) { + if eval_predefined_constant_value(&name).is_some() || context.has_constant(&name) { values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; return Ok(false); } @@ -1681,7 +1687,7 @@ fn eval_defined_name( values: &mut impl RuntimeValueOps, ) -> Result { let name = eval_constant_name(name, values)?; - Ok(eval_predefined_int_constant(&name).is_some() || context.has_constant(&name)) + Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) } /// Reads a PHP constant name from a runtime cell without changing case. @@ -10209,8 +10215,8 @@ fn eval_const_fetch( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if let Some(value) = eval_predefined_int_constant(name) { - return values.int(value); + if let Some(value) = eval_predefined_constant(name, values)? { + return Ok(value); } let Some(value) = context.constant(name) else { return Err(EvalStatus::RuntimeFatal); @@ -10218,22 +10224,49 @@ fn eval_const_fetch( values.retain(value) } -/// Returns eval-visible predefined integer constants that do not live in dynamic context. -fn eval_predefined_int_constant(name: &str) -> Option { - match name { - "PATHINFO_DIRNAME" => Some(EVAL_PATHINFO_DIRNAME), - "PATHINFO_BASENAME" => Some(EVAL_PATHINFO_BASENAME), - "PATHINFO_EXTENSION" => Some(EVAL_PATHINFO_EXTENSION), - "PATHINFO_FILENAME" => Some(EVAL_PATHINFO_FILENAME), - "PATHINFO_ALL" => Some(EVAL_PATHINFO_ALL), - "FNM_NOESCAPE" => Some(EVAL_FNM_NOESCAPE), - "FNM_PATHNAME" => Some(EVAL_FNM_PATHNAME), - "FNM_PERIOD" => Some(EVAL_FNM_PERIOD), - "FNM_CASEFOLD" => Some(EVAL_FNM_CASEFOLD), +/// Materializes one eval-visible predefined constant into a runtime cell. +fn eval_predefined_constant( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = eval_predefined_constant_value(name) else { + return Ok(None); + }; + match value { + EvalPredefinedConstant::Int(value) => values.int(value).map(Some), + EvalPredefinedConstant::String(value) => values.string(value).map(Some), + } +} + +/// Returns eval-visible predefined constants that do not live in dynamic context. +fn eval_predefined_constant_value(name: &str) -> Option { + match name.trim_start_matches('\\') { + "PATHINFO_DIRNAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_DIRNAME)), + "PATHINFO_BASENAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_BASENAME)), + "PATHINFO_EXTENSION" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_EXTENSION)), + "PATHINFO_FILENAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_FILENAME)), + "PATHINFO_ALL" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_ALL)), + "FNM_NOESCAPE" => Some(EvalPredefinedConstant::Int(EVAL_FNM_NOESCAPE)), + "FNM_PATHNAME" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PATHNAME)), + "FNM_PERIOD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PERIOD)), + "FNM_CASEFOLD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_CASEFOLD)), + "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), + "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), + "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), + "DIRECTORY_SEPARATOR" => Some(EvalPredefinedConstant::String("/")), _ => None, } } +/// Returns the PHP OS constant for the host platform running the eval bridge. +fn eval_php_os_name() -> &'static str { + if cfg!(target_os = "macos") { + "Darwin" + } else { + "Linux" + } +} + /// Resolves one eval magic constant against fragment and dynamic-call metadata. fn eval_magic_const( magic: &EvalMagicConst, @@ -15155,6 +15188,34 @@ echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y ); } + /// Verifies eval predefined runtime constants are fetchable and cannot be redefined. + #[test] + fn execute_program_reads_predefined_runtime_constants() { + let program = parse_fragment( + br#"echo PHP_EOL === "\n" ? "eol" : "bad"; echo ":"; +echo (PHP_OS === "Darwin" || PHP_OS === "Linux") ? "os" : "bad"; echo ":"; +echo DIRECTORY_SEPARATOR; echo ":"; +echo PHP_INT_MAX > 9000000000000000000 ? "int" : "bad"; echo ":"; +echo defined("PHP_OS") ? "defined" : "bad"; echo ":"; +echo defined("\\PHP_OS") ? "root" : "bad"; echo ":"; +echo defined("php_os") ? "bad" : "case"; echo ":"; +echo define("PHP_OS", "x") ? "bad" : "locked"; echo ":"; +return PHP_INT_MAX;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "eol:os:/:int:defined:root:case:locked:"); + assert_eq!(values.get(result), FakeValue::Int(i64::MAX)); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); + } + /// Verifies missing eval dynamic constants fail through runtime status. #[test] fn execute_program_missing_constant_fetch_fails() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 7df91590ae..15912bc878 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -65,6 +65,8 @@ Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_ Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. +Eval predefined constants bypass the dynamic context and are materialized directly by the interpreter. The current eval-visible set is `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, and `FNM_*`; `defined()` recognizes them and duplicate `define()` attempts follow the same warning path as dynamic constants. + Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ce1c570abe..bc7a4c99e1 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -78,6 +78,8 @@ Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and Eval path component calls include `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` with suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, named arguments, callable dispatch, and `function_exists()` probes. +Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, and `FNM_*`. `defined()` sees these names, including an optional leading `\`, and `define()` cannot replace them. + Eval `realpath()` canonicalizes existing paths through the host filesystem and returns `false` when the path cannot be resolved. Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. diff --git a/examples/eval/main.php b/examples/eval/main.php index ab1d4c05a3..8d4c622647 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -114,6 +114,7 @@ public function __construct(int $value) { $rounded = eval('return round(3.14159, 2) . ":" . round(2.5);'); $formatted_number = eval('return number_format(1234567.89, 2, ",", ".");'); $formatted_text = eval('return sprintf("%s:%04d:%s", "item", 7, vsprintf("%.1f", [3]));'); +$runtime_constants = eval('return PHP_OS . ":" . DIRECTORY_SEPARATOR . ":" . (PHP_INT_MAX > 0 ? "int" : "bad") . ":" . (defined("PHP_EOL") ? "eol" : "bad");'); $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5) . ":" . clamp(15, 0, 10);'); $random_range = eval('$r = rand(1, 3); return ($r >= 1 && $r <= 3) ? "bounded" : "bad";'); $circle = eval('return round(pi(), 2);'); @@ -244,6 +245,7 @@ public function __construct(int $value) { echo "rounded=" . $rounded . "\n"; echo "number-format=" . $formatted_number . "\n"; echo "printf-format=" . $formatted_text . "\n"; +echo "runtime-constants=" . $runtime_constants . "\n"; echo "minmax=" . $minmax . "\n"; echo "random-range=" . $random_range . "\n"; echo "pi=" . $circle . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1cc41460ee..16896e0e7a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2432,6 +2432,31 @@ echo eval('return function_exists("define") && function_exists("defined") ? "Y" ); } +/// Verifies eval can read predefined runtime constants and protect them from redefinition. +#[test] +fn test_eval_reads_predefined_runtime_constants() { + let out = compile_and_run_capture( + r#" 9000000000000000000 ? "int" : "bad") . ":" . + (defined("PHP_OS") ? "defined" : "bad") . ":" . + (defined("\\\\PHP_OS") ? "root" : "bad") . ":" . + (defined("php_os") ? "bad" : "case") . ":" . + (define("PHP_OS", "x") ? "bad" : "locked");'); +"#, + ); + assert!(out.success, "program failed: {}", out.stderr); + assert_eq!(out.stdout, "eol:os:/:int:defined:root:case:locked"); + assert!( + out.stderr + .contains("Warning: define(): Constant already defined"), + "expected predefined eval define warning, got stderr={}", + out.stderr + ); +} + /// Verifies `@eval(...)` suppresses duplicate eval `define()` warnings. #[test] fn test_error_control_suppresses_duplicate_eval_define_warning() { From a8dd7b6e74e54ab4ad7c7e89bbc3f7d581b1b062 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 14:22:42 +0200 Subject: [PATCH 0191/1208] Support eval hash_file builtin --- crates/elephc-eval/src/interpreter.rs | 52 ++++++++++++++++++++++----- docs/internals/the-runtime.md | 4 +-- docs/php/system-and-io.md | 4 +-- examples/eval/main.php | 2 ++ tests/codegen/eval.rs | 14 ++++++-- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 4d61f5ee05..d4302b3e17 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1487,7 +1487,7 @@ fn eval_positional_expr_call( "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), "glob" => eval_builtin_glob(args, context, scope, values), - "hash" | "hash_hmac" | "md5" | "sha1" => { + "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { eval_builtin_hash_one_shot(name, args, context, scope, values) } "hash_algos" => eval_builtin_hash_algos(args, values), @@ -1917,6 +1917,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "hash" | "hash_algos" | "hash_equals" + | "hash_file" | "hash_hmac" | "hex2bin" | "html_entity_decode" @@ -2188,6 +2189,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "hash" => Some(&["algo", "data", "binary"]), "hash_algos" => Some(&[]), "hash_equals" => Some(&["known_string", "user_string"]), + "hash_file" => Some(&["algo", "filename", "binary"]), "hash_hmac" => Some(&["algo", "data", "key", "binary"]), "hypot" => Some(&["x", "y"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), @@ -2973,7 +2975,7 @@ fn eval_builtin_with_values( }; eval_glob_result(*pattern, values)? } - "hash" | "hash_hmac" | "md5" | "sha1" => { + "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { eval_hash_one_shot_result(name, evaluated_args, values)? } "hash_algos" => { @@ -5604,6 +5606,14 @@ fn eval_hash_one_shot_result( let data = values.string_bytes(data)?; eval_hash_digest_result(&algo, &data, binary, values) } + "hash_file" => { + let (algo, filename, binary) = match evaluated_args { + [algo, filename] => (*algo, *filename, false), + [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_hash_file_result(algo, filename, binary, values) + } "hash_hmac" => { let (algo, data, key, binary) = match evaluated_args { [algo, data, key] => (*algo, *data, *key, false), @@ -5619,6 +5629,21 @@ fn eval_hash_one_shot_result( } } +/// Reads a local file and returns its PHP hash digest or false when it cannot be read. +fn eval_hash_file_result( + algo: RuntimeCellHandle, + filename: RuntimeCellHandle, + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(data) => eval_hash_digest_result(&algo, &data, binary, values), + Err(_) => values.bool_value(false), + } +} + /// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. fn eval_hash_digest_result( algo: &[u8], @@ -13202,8 +13227,9 @@ return count($algos);"#, /// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. #[test] fn execute_program_dispatches_hash_digest_builtins() { - let program = parse_fragment( - br#"echo md5("abc"); echo ":"; + let filename = format!("elephc_eval_hash_file_{}.txt", std::process::id()); + let source = format!( + r#"echo md5("abc"); echo ":"; echo sha1(string: "abc"); echo ":"; echo hash("sha256", "abc"); echo ":"; echo hash_hmac(algo: "sha256", data: "data", key: "key"); echo ":"; @@ -13211,10 +13237,16 @@ echo bin2hex(md5("abc", true)); echo ":"; echo bin2hex(call_user_func("sha1", "abc", true)); echo ":"; echo call_user_func_array("hash", ["algo" => "md5", "data" => "abc"]); echo ":"; echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; -echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); +file_put_contents("{filename}", "abc"); +echo hash_file("sha256", "{filename}"); echo ":"; +echo bin2hex(hash_file(algo: "md5", filename: "{filename}", binary: true)); echo ":"; +echo call_user_func_array("hash_file", ["algo" => "md5", "filename" => "{filename}"]); echo ":"; +echo hash_file("sha256", "{filename}.missing") === false ? "missing" : "bad"; echo ":"; +unlink("{filename}"); +echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); return function_exists("hash_hmac");"#, - ) - .expect("parse eval fragment"); + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -13231,7 +13263,11 @@ return function_exists("hash_hmac");"#, "a9993e364706816aba3e25717850c26c9cd0d89d:", "900150983cd24fb0d6963f7d28e17f72:", "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", - "111" + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "900150983cd24fb0d6963f7d28e17f72:", + "900150983cd24fb0d6963f7d28e17f72:", + "missing:", + "1111" ) ); assert_eq!(values.get(result), FakeValue::Bool(true)); diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 15912bc878..8c0c3dffdc 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -41,7 +41,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. +Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index bc7a4c99e1..b593fcd908 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -52,7 +52,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. +Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. diff --git a/examples/eval/main.php b/examples/eval/main.php index 8d4c622647..a80c2edcec 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -164,6 +164,7 @@ public function __construct(int $value) { $checksum = eval('return crc32("hello");'); $hash_algos = eval('$algos = hash_algos(); return count($algos) . ":" . (in_array("sha256", $algos) ? "sha256" : "missing");'); $digest = eval('return md5("abc") . ":" . substr(hash("sha256", "abc"), 0, 8) . ":" . substr(hash_hmac("sha256", "data", "key"), 0, 8);'); +$file_digest = eval('file_put_contents("eval-hash-file.txt", "abc"); $digest = hash_file("sha256", "eval-hash-file.txt"); unlink("eval-hash-file.txt"); return substr($digest, 0, 8);'); $system_info = eval('return (time() > 1000000000 ? "time" : "bad") . ":" . phpversion() . ":" . (strlen(php_uname("s")) > 0 ? "uname" : "bad") . ":" . sys_get_temp_dir() . ":" . (strlen(getcwd()) > 0 ? "cwd" : "bad");'); $date_sample = eval('$ts = mktime(0, 0, 0, 1, 2, 2024); return date("Y-m-d", $ts);'); $strtotime_sample = eval('$ts = strtotime("2024-01-02 03:04:05"); return date("Y-m-d H:i:s", $ts);'); @@ -295,6 +296,7 @@ public function __construct(int $value) { echo "crc32=" . $checksum . "\n"; echo "hash-algos=" . $hash_algos . "\n"; echo "digest=" . $digest . "\n"; +echo "hash-file=" . $file_digest . "\n"; echo "system-info=" . $system_info . "\n"; echo "date-sample=" . $date_sample . "\n"; echo "strtotime-sample=" . $strtotime_sample . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 16896e0e7a..619f07686e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1471,7 +1471,13 @@ echo bin2hex(md5("abc", true)); echo ":"; echo bin2hex(call_user_func("sha1", "abc", true)); echo ":"; echo call_user_func_array("hash", ["algo" => "md5", "data" => "abc"]); echo ":"; echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; -echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_hmac");'); +file_put_contents("eval-hash-file.txt", "abc"); +echo hash_file("sha256", "eval-hash-file.txt"); echo ":"; +echo bin2hex(hash_file(algo: "md5", filename: "eval-hash-file.txt", binary: true)); echo ":"; +echo call_user_func_array("hash_file", ["algo" => "md5", "filename" => "eval-hash-file.txt"]); echo ":"; +echo hash_file("sha256", "eval-hash-file.txt.missing") === false ? "missing" : "bad"; echo ":"; +unlink("eval-hash-file.txt"); +echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); echo function_exists("hash_hmac");'); "#, ); assert_eq!( @@ -1485,7 +1491,11 @@ echo function_exists("md5"); echo function_exists("sha1"); echo function_exists( "a9993e364706816aba3e25717850c26c9cd0d89d:", "900150983cd24fb0d6963f7d28e17f72:", "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", - "1111" + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "900150983cd24fb0d6963f7d28e17f72:", + "900150983cd24fb0d6963f7d28e17f72:", + "missing:", + "11111" ) ); } From 1eb0c7b0cb413aaa4e2bc44c6dd70968100d987d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 14:37:41 +0200 Subject: [PATCH 0192/1208] Support eval array_filter builtin --- crates/elephc-eval/src/interpreter.rs | 92 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 21 ++++++ 5 files changed, 120 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d4302b3e17..900241479e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1396,6 +1396,7 @@ fn eval_positional_expr_call( "array_column" => eval_builtin_array_column(args, context, scope, values), "array_fill" => eval_builtin_array_fill(args, context, scope, values), "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), + "array_filter" => eval_builtin_array_filter(args, context, scope, values), "array_flip" => eval_builtin_array_flip(args, context, scope, values), "array_keys" | "array_values" => { eval_builtin_array_projection(name, args, context, scope, values) @@ -1830,6 +1831,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_combine" | "array_fill" | "array_fill_keys" + | "array_filter" | "array_flip" | "array_key_exists" | "array_keys" @@ -2130,6 +2132,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_combine" => Some(&["keys", "values"]), "array_fill" => Some(&["start_index", "count", "value"]), "array_fill_keys" => Some(&["keys", "value"]), + "array_filter" => Some(&["array", "callback", "mode"]), "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" | "array_rand" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), @@ -2421,6 +2424,14 @@ fn eval_builtin_with_values( }; eval_array_fill_keys_result(*keys, *value, values)? } + "array_filter" => match evaluated_args { + [array] => eval_array_filter_result(*array, None, None, values)?, + [array, callback] => eval_array_filter_result(*array, Some(*callback), None, values)?, + [array, callback, mode] => { + eval_array_filter_result(*array, Some(*callback), Some(*mode), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3417,6 +3428,61 @@ fn eval_array_fill_keys_result( Ok(result) } +/// Evaluates PHP `array_filter()` for the default null-callback filtering mode. +fn eval_builtin_array_filter( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_filter_result(array, None, None, values) + } + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_filter_result(array, Some(callback), None, values) + } + [array, callback, mode] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_array_filter_result(array, Some(callback), Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Filters falsey values from an eval array while preserving original keys. +fn eval_array_filter_result( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(callback) = callback { + if !values.is_null(callback)? { + return Err(EvalStatus::RuntimeFatal); + } + } + if let Some(mode) = mode { + let _ = eval_int_value(mode, values)?; + } + + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + if values.truthy(value)? { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + /// Evaluates PHP `array_chunk()` over one array and chunk-size expression. fn eval_builtin_array_chunk( args: &[EvalExpr], @@ -12539,6 +12605,32 @@ return function_exists("array_product");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. + #[test] + fn execute_program_dispatches_array_filter_builtin() { + let program = parse_fragment( + br#"$filtered = array_filter([0, 1, 2, "", false, null, "0", "ok"]); +echo count($filtered) . ":" . $filtered[1] . ":" . $filtered[2] . ":" . $filtered[7] . ":"; +$assoc = array_filter(["a" => 0, "b" => 2, "c" => ""]); +echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; +$null = array_filter([0, 3], null, 1); +echo count($null) . ":" . $null[1] . ":"; +$call = call_user_func("array_filter", [0, 4]); +echo count($call) . ":" . $call[1] . ":"; +$spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); +echo count($spread) . ":" . $spread[1] . ":"; +return function_exists("array_filter");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:1:2:ok:drop:2:1:3:1:4:1:5:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_combine()` converts key values through PHP string-key rules. #[test] fn execute_program_dispatches_array_combine_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 8c0c3dffdc..a141418ddb 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -41,6 +41,8 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. +Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. Non-null callbacks are intentionally still outside the eval subset. + Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b593fcd908..1df3ce12c3 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -52,6 +52,8 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. +Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. Non-null callbacks remain outside the eval subset. + Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. diff --git a/examples/eval/main.php b/examples/eval/main.php index a80c2edcec..b7bb6cb759 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -130,6 +130,7 @@ public function __construct(int $value) { $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); +$array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); $mixed_literal = eval('return [2 => "two", "tail"][3] . ":" . (["2" => "two", "next"][3]);'); @@ -262,6 +263,7 @@ public function __construct(int $value) { echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; +echo "array-filter=" . $array_filter . "\n"; echo "named-builtins=" . $named_builtins . "\n"; echo "array-projection=" . $array_projection . "\n"; echo "mixed-literal=" . $mixed_literal . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 619f07686e..5dd95f88fa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -734,6 +734,27 @@ echo ":"; echo function_exists("array_sum"); echo function_exists("array_product assert_eq!(out, "6:24:0:1:7:7:10:11"); } +/// Verifies eval `array_filter()` removes falsey values and preserves source keys. +#[test] +fn test_eval_dispatches_array_filter_builtin_call() { + let out = compile_and_run( + r#" 0, "b" => 2, "c" => ""]); +echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; +$null = array_filter([0, 3], null, 1); +echo count($null) . ":" . $null[1] . ":"; +$call = call_user_func("array_filter", [0, 4]); +echo count($call) . ":" . $call[1] . ":"; +$spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); +echo count($spread) . ":" . $spread[1] . ":"; +echo function_exists("array_filter");'); +"#, + ); + assert_eq!(out, "3:1:2:ok:drop:2:1:3:1:4:1:5:1"); +} + /// Verifies eval `array_combine()` supports PHP key conversions and callable dispatch. #[test] fn test_eval_dispatches_array_combine_builtin_call() { From 342a4f0e99c9b6750f76e6ca5f222737d4a06df9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 14:48:57 +0200 Subject: [PATCH 0193/1208] Support eval random_int builtin --- crates/elephc-eval/src/interpreter.rs | 59 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 15 +++++-- 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 900241479e..c592213aca 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1525,6 +1525,7 @@ fn eval_positional_expr_call( "pow" => eval_builtin_pow(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), + "random_int" => eval_builtin_random_int(args, context, scope, values), "range" => eval_builtin_range(args, context, scope, values), "rawurldecode" | "urldecode" => { eval_builtin_url_decode(name, args, context, scope, values) @@ -1982,6 +1983,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "phpversion" | "putenv" | "rand" + | "random_int" | "range" | "rad2deg" | "rawurldecode" @@ -2217,7 +2219,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), "putenv" => Some(&["assignment"]), - "rand" | "mt_rand" => Some(&["min", "max"]), + "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "range" => Some(&["start", "end"]), "realpath" => Some(&["path"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), @@ -2700,6 +2702,12 @@ fn eval_builtin_with_values( [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, _ => return Err(EvalStatus::RuntimeFatal), }, + "random_int" => { + let [min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_random_int_result(*min, *max, values)? + } "rawurldecode" | "urldecode" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -4066,6 +4074,21 @@ fn eval_builtin_rand( } } +/// Evaluates PHP `random_int()` over an inclusive integer range. +fn eval_builtin_random_int( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_random_int_result(min, max, values) +} + /// Returns one non-cryptographic random integer using PHP's inclusive range rules. fn eval_rand_result( min: Option, @@ -4086,6 +4109,24 @@ fn eval_rand_result( values.int(sampled) } +/// Returns one eval `random_int()` value in the inclusive range `[min, max]`. +fn eval_random_int_result( + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let min = eval_int_value(min, values)?; + let max = eval_int_value(max, values)?; + if min > max { + return Err(EvalStatus::RuntimeFatal); + } + let width = (i128::from(max) - i128::from(min) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(min) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) +} + /// Evaluates PHP `range()` over integer-compatible start and end expressions. fn eval_builtin_range( args: &[EvalExpr], @@ -12883,7 +12924,7 @@ return function_exists("array_rand");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } - /// Verifies eval `rand()` and `mt_rand()` return values inside PHP inclusive ranges. + /// Verifies eval random builtins return values inside PHP inclusive ranges. #[test] fn execute_program_dispatches_rand_builtins() { let program = parse_fragment( @@ -12899,8 +12940,15 @@ $call = call_user_func("mt_rand", 1, 1); echo ":" . ($call === 1 ? "call" : "bad"); $spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; +$secure = random_int(max: 4, min: 4); +echo ($secure === 4 ? "random" : "bad") . ":"; +$secureCall = call_user_func("random_int", 5, 5); +echo ($secureCall === 5 ? "random-call" : "bad") . ":"; +$secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); +echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; echo function_exists("rand"); -return function_exists("mt_rand");"#, +echo function_exists("mt_rand"); +return function_exists("random_int");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -12908,7 +12956,10 @@ return function_exists("mt_rand");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "plain:range:same:swap:call:spread:1"); + assert_eq!( + values.output, + "plain:range:same:swap:call:spread:random:random-call:random-spread:11" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a141418ddb..2c8c60c702 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -73,7 +73,7 @@ Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supp Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()` and printf-family formatting via `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, and `mt_rand()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()` and printf-family formatting via `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, and `random_int()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 1df3ce12c3..3fa9866fc9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -92,7 +92,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and printf-family formatting through `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, and `mt_rand()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and printf-family formatting through `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, and `random_int()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/examples/eval/main.php b/examples/eval/main.php index b7bb6cb759..03bd055af4 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -116,7 +116,7 @@ public function __construct(int $value) { $formatted_text = eval('return sprintf("%s:%04d:%s", "item", 7, vsprintf("%.1f", [3]));'); $runtime_constants = eval('return PHP_OS . ":" . DIRECTORY_SEPARATOR . ":" . (PHP_INT_MAX > 0 ? "int" : "bad") . ":" . (defined("PHP_EOL") ? "eol" : "bad");'); $minmax = eval('return min(3, 1, 2) . ":" . max(1.5, 2.5) . ":" . clamp(15, 0, 10);'); -$random_range = eval('$r = rand(1, 3); return ($r >= 1 && $r <= 3) ? "bounded" : "bad";'); +$random_range = eval('$r = rand(1, 3); $secure = random_int(4, 4); return (($r >= 1 && $r <= 3) ? "bounded" : "bad") . ":" . ($secure === 4 ? "secure" : "bad");'); $circle = eval('return round(pi(), 2);'); $extended_math = eval('return round(sin(pi() / 2), 0) . ":" . log(8, 2) . ":" . hypot(3, 4) . ":" . intdiv(7, 2);'); $float_predicates = eval('return (is_nan(fdiv(0, 0)) ? "nan" : "bad") . ":" . (is_infinite(fdiv(1, 0)) ? "inf" : "bad") . ":" . (is_finite(3.14) ? "finite" : "bad");'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5dd95f88fa..007f63b4ce 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -964,7 +964,7 @@ echo function_exists("array_rand");'); assert_eq!(out, "idx:assoc:named:call:spread:1"); } -/// Verifies eval `rand()` and `mt_rand()` produce values in their PHP-visible ranges. +/// Verifies eval random builtins produce values in their PHP-visible ranges. #[test] fn test_eval_dispatches_rand_builtin_calls() { let out = compile_and_run( @@ -981,10 +981,19 @@ $call = call_user_func("mt_rand", 1, 1); echo ":" . ($call === 1 ? "call" : "bad"); $spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; -echo function_exists("rand"); echo function_exists("mt_rand");'); +$secure = random_int(max: 4, min: 4); +echo ($secure === 4 ? "random" : "bad") . ":"; +$secureCall = call_user_func("random_int", 5, 5); +echo ($secureCall === 5 ? "random-call" : "bad") . ":"; +$secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); +echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; +echo function_exists("rand"); echo function_exists("mt_rand"); echo function_exists("random_int");'); "#, ); - assert_eq!(out, "plain:range:same:swap:call:spread:11"); + assert_eq!( + out, + "plain:range:same:swap:call:spread:random:random-call:random-spread:111" + ); } /// Verifies eval `spl_classes()` exposes the same static SPL class list as native code. From 9d82eabcd781e7102bcb4d9cc87f89b52e2a77b7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 14:58:07 +0200 Subject: [PATCH 0194/1208] Support eval print_r builtin --- crates/elephc-eval/src/interpreter.rs | 59 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 1 + tests/codegen/eval.rs | 17 ++++++++ 5 files changed, 83 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c592213aca..4b7a7b2d1e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1523,6 +1523,7 @@ fn eval_positional_expr_call( "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), "pow" => eval_builtin_pow(args, context, scope, values), + "print_r" => eval_builtin_print_r(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), "random_int" => eval_builtin_random_int(args, context, scope, values), @@ -1982,6 +1983,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "php_uname" | "phpversion" | "putenv" + | "print_r" | "rand" | "random_int" | "range" @@ -2218,6 +2220,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), + "print_r" => Some(&["value"]), "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "range" => Some(&["start", "end"]), @@ -2697,6 +2700,12 @@ fn eval_builtin_with_values( }; values.pow(*left, *right)? } + "print_r" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_print_r_result(*value, values)? + } "rand" | "mt_rand" => match evaluated_args { [] => eval_rand_result(None, None, values)?, [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, @@ -9890,6 +9899,34 @@ fn eval_builtin_count( values.int(len) } +/// Evaluates PHP `print_r()` over one eval expression. +fn eval_builtin_print_r( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_print_r_result(value, values) +} + +/// Emits one eval value using elephc's supported `print_r()` output shape. +fn eval_print_r_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if matches!(values.type_tag(value)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + let output = values.string_bytes_value(b"Array\n")?; + values.echo(output)?; + } else { + values.echo(value)?; + } + values.bool_value(true) +} + /// Evaluates an eval-declared user function with PHP-style argument binding. fn eval_dynamic_function( function: &EvalFunction, @@ -11444,6 +11481,28 @@ return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, assert_eq!(values.get(result), FakeValue::Int(1)); } + /// Verifies eval `print_r()` emits supported values and returns true. + #[test] + fn execute_program_dispatches_print_r_builtin() { + let program = parse_fragment( + br#"print_r("x"); echo ":"; +print_r(value: false); echo ":"; +print_r([1, 2]); echo ":"; +$call = call_user_func("print_r", true); +$spread = call_user_func_array("print_r", ["value" => "z"]); +echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; +return function_exists("print_r");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "x::Array\n:1z:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval property reads and writes dispatch through runtime hooks. #[test] fn execute_program_reads_and_writes_object_property() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2c8c60c702..1c975d7445 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,12 +17,14 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. +Eval `print_r()` writes scalars through the eval output hook and emits `Array\n` for indexed or associative arrays, matching the native runtime's currently supported `print_r()` shape. + Eval SPL class introspection dispatch includes `spl_classes()` through direct calls, callable dispatch, and `function_exists()` probes. The interpreter builds the same static class-name snapshot as the native SPL builtin. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 3fa9866fc9..068b25232b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,12 +32,14 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` with the same direct, named-argument, callable, and `function_exists()` dispatch paths. +Eval `print_r()` supports the one-argument form through direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Scalars print through the same output path as `echo`, boolean false and null print nothing, and arrays print the same `Array\n` header shape as elephc's native `print_r()` subset. + The word-case helper `ucwords()` is available inside eval with its optional `separators` parameter. Eval `strstr()` returns matching suffixes, `before_needle` prefixes, or `false` when the needle is absent. diff --git a/examples/eval/main.php b/examples/eval/main.php index 03bd055af4..20c0ddb6a0 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -63,6 +63,7 @@ public function __construct(int $value) { eval('foreach (["a" => 1, "b" => 2] as $key => $value) { echo "pair=" . $key . ":" . $value . "\n"; }'); eval('switch (2) { case 1: echo "switch-one\n"; break; case 2: echo "switch-two\n"; break; }'); eval('echo "echo-list=", "ok\n";'); +eval('print_r("print-r=ok\n");'); eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); $meta = eval('return ["source" => "eval"];'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 007f63b4ce..bbbbd690d1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -77,6 +77,23 @@ fn test_eval_print_return_value_is_one() { assert_eq!(out, "x1"); } +/// Verifies eval `print_r()` writes supported values and returns true. +#[test] +fn test_eval_dispatches_print_r_builtin_call() { + let out = compile_and_run( + r#" "z"]); +echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; +echo function_exists("print_r");'); +"#, + ); + assert_eq!(out, "x::Array\n:1z:call:spread:1"); +} + /// Verifies eval fragments accept PHP comments and keep line metadata aligned. #[test] fn test_eval_comments_execute_through_bridge() { From 28f063b5cc10c40328a49b0f654a087101510c1e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:14:13 +0200 Subject: [PATCH 0195/1208] Support eval var_dump builtin --- crates/elephc-eval/src/interpreter.rs | 228 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 1 + tests/codegen/eval.rs | 41 +++++ 5 files changed, 275 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 4b7a7b2d1e..5605e17458 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1579,6 +1579,7 @@ fn eval_positional_expr_call( "ucwords" => eval_builtin_ucwords(args, context, scope, values), "umask" => eval_builtin_umask(args, context, scope, values), "usleep" => eval_builtin_usleep(args, context, scope, values), + "var_dump" => eval_builtin_var_dump(args, context, scope, values), "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), @@ -2048,6 +2049,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "urldecode" | "urlencode" | "usleep" + | "var_dump" | "printf" | "vprintf" | "vsprintf" @@ -2220,7 +2222,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), - "print_r" => Some(&["value"]), + "print_r" | "var_dump" => Some(&["value"]), "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "range" => Some(&["start", "end"]), @@ -2706,6 +2708,12 @@ fn eval_builtin_with_values( }; eval_print_r_result(*value, values)? } + "var_dump" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_var_dump_result(*value, values)? + } "rand" | "mt_rand" => match evaluated_args { [] => eval_rand_result(None, None, values)?, [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, @@ -9927,6 +9935,178 @@ fn eval_print_r_result( values.bool_value(true) } +/// Evaluates PHP `var_dump()` over one eval expression and returns null. +fn eval_builtin_var_dump( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_var_dump_result(value, values) +} + +/// Emits one eval value using PHP-style `var_dump()` debug formatting. +fn eval_var_dump_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut output = Vec::new(); + let mut arrays_seen = Vec::new(); + eval_var_dump_append_value(value, values, 0, &mut arrays_seen, &mut output)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.null() +} + +/// Appends one value and its nested array entries to a `var_dump()` byte buffer. +fn eval_var_dump_append_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_INT => eval_var_dump_append_scalar(b"int", value, values, depth, output), + EVAL_TAG_STRING => eval_var_dump_append_string(value, values, depth, output), + EVAL_TAG_FLOAT => eval_var_dump_append_scalar(b"float", value, values, depth, output), + EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, output), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { + eval_var_dump_append_array(value, values, depth, arrays_seen, output) + } + EVAL_TAG_OBJECT => { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"object(Object)\n"); + Ok(()) + } + EVAL_TAG_NULL => { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"NULL\n"); + Ok(()) + } + EVAL_TAG_RESOURCE => { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"resource(0) of type (stream)\n"); + Ok(()) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends one integer-like or float-like `var_dump()` scalar line. +fn eval_var_dump_append_scalar( + label: &[u8], + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(label); + output.extend_from_slice(b"("); + output.extend_from_slice(&values.string_bytes(value)?); + output.extend_from_slice(b")\n"); + Ok(()) +} + +/// Appends one string `var_dump()` line while preserving raw PHP string bytes. +fn eval_var_dump_append_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let bytes = values.string_bytes(value)?; + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"string("); + output.extend_from_slice(bytes.len().to_string().as_bytes()); + output.extend_from_slice(b") \""); + output.extend_from_slice(&bytes); + output.extend_from_slice(b"\"\n"); + Ok(()) +} + +/// Appends one boolean `var_dump()` line from PHP truthiness. +fn eval_var_dump_append_bool( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + if values.truthy(value)? { + output.extend_from_slice(b"bool(true)\n"); + } else { + output.extend_from_slice(b"bool(false)\n"); + } + Ok(()) +} + +/// Appends one array shell and recursively emits foreach-visible entries. +fn eval_var_dump_append_array( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"*RECURSION*\n"); + return Ok(()); + } + + arrays_seen.push(address); + let len = values.array_len(value)?; + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"array("); + output.extend_from_slice(len.to_string().as_bytes()); + output.extend_from_slice(b") {\n"); + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + eval_var_dump_append_key(key, values, depth + 1, output)?; + eval_var_dump_append_value(element, values, depth + 1, arrays_seen, output)?; + } + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"}\n"); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one array key line for an indexed or associative `var_dump()` entry. +fn eval_var_dump_append_key( + key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"["); + match values.type_tag(key)? { + EVAL_TAG_STRING => { + output.extend_from_slice(b"\""); + output.extend_from_slice(&values.string_bytes(key)?); + output.extend_from_slice(b"\""); + } + _ => output.extend_from_slice(&values.string_bytes(key)?), + } + output.extend_from_slice(b"]=>\n"); + Ok(()) +} + +/// Appends the two-space indentation used by PHP `var_dump()` arrays. +fn eval_var_dump_append_indent(depth: usize, output: &mut Vec) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} + /// Evaluates an eval-declared user function with PHP-style argument binding. fn eval_dynamic_function( function: &EvalFunction, @@ -11503,6 +11683,52 @@ return function_exists("print_r");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. + #[test] + fn execute_program_dispatches_var_dump_builtin() { + let program = parse_fragment( + br#"var_dump(42); +var_dump("hi"); +var_dump(false); +var_dump(null); +var_dump([10, 20]); +var_dump(["x" => true]); +$call = call_user_func("var_dump", 3.5); +$spread = call_user_func_array("var_dump", ["value" => "z"]); +echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; +return function_exists("var_dump");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "int(42)\n", + "string(2) \"hi\"\n", + "bool(false)\n", + "NULL\n", + "array(2) {\n", + " [0]=>\n", + " int(10)\n", + " [1]=>\n", + " int(20)\n", + "}\n", + "array(1) {\n", + " [\"x\"]=>\n", + " bool(true)\n", + "}\n", + "float(3.5)\n", + "string(1) \"z\"\n", + "call-null:spread-null:", + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval property reads and writes dispatch through runtime hooks. #[test] fn execute_program_reads_and_writes_object_property() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 1c975d7445..a00d94233d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -25,6 +25,8 @@ Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, ` Eval `print_r()` writes scalars through the eval output hook and emits `Array\n` for indexed or associative arrays, matching the native runtime's currently supported `print_r()` shape. +Eval `var_dump()` formats scalar tags and array contents inside the interpreter, then emits one byte string through the eval output hook. Array dumping reads foreach-visible keys and values through the same eval hooks used by `foreach`, `array_keys()`, and `array_values()`. + Eval SPL class introspection dispatch includes `spl_classes()` through direct calls, callable dispatch, and `function_exists()` probes. The interpreter builds the same static class-name snapshot as the native SPL builtin. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 068b25232b..11ef4935ec 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -40,6 +40,8 @@ Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digi Eval `print_r()` supports the one-argument form through direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Scalars print through the same output path as `echo`, boolean false and null print nothing, and arrays print the same `Array\n` header shape as elephc's native `print_r()` subset. +Eval `var_dump()` supports the one-argument form through direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Scalars print typed diagnostic lines, and indexed or associative arrays print foreach-visible keys and nested values through eval value hooks. + The word-case helper `ucwords()` is available inside eval with its optional `separators` parameter. Eval `strstr()` returns matching suffixes, `before_needle` prefixes, or `false` when the needle is absent. diff --git a/examples/eval/main.php b/examples/eval/main.php index 20c0ddb6a0..ebd784fa98 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -64,6 +64,7 @@ public function __construct(int $value) { eval('switch (2) { case 1: echo "switch-one\n"; break; case 2: echo "switch-two\n"; break; }'); eval('echo "echo-list=", "ok\n";'); eval('print_r("print-r=ok\n");'); +eval('var_dump("var-dump-ok");'); eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); $meta = eval('return ["source" => "eval"];'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bbbbd690d1..c2fdab4f7b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -94,6 +94,47 @@ echo function_exists("print_r");'); assert_eq!(out, "x::Array\n:1z:call:spread:1"); } +/// Verifies eval `var_dump()` writes PHP-style diagnostics and returns null. +#[test] +fn test_eval_dispatches_var_dump_builtin_call() { + let out = compile_and_run( + r#" true]); +$call = call_user_func("var_dump", 3.5); +$spread = call_user_func_array("var_dump", ["value" => "z"]); +echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; +echo function_exists("var_dump");'); +"#, + ); + assert_eq!( + out, + concat!( + "int(42)\n", + "string(2) \"hi\"\n", + "bool(false)\n", + "NULL\n", + "array(2) {\n", + " [0]=>\n", + " int(10)\n", + " [1]=>\n", + " int(20)\n", + "}\n", + "array(1) {\n", + " [\"x\"]=>\n", + " bool(true)\n", + "}\n", + "float(3.5)\n", + "string(1) \"z\"\n", + "call-null:spread-null:1", + ) + ); +} + /// Verifies eval fragments accept PHP comments and keep line metadata aligned. #[test] fn test_eval_comments_execute_through_bridge() { From 527217e390f20d5cf2d044d54faa35cf48bd6900 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:25:07 +0200 Subject: [PATCH 0196/1208] Support eval array_filter callbacks --- crates/elephc-eval/src/interpreter.rs | 114 ++++++++++++++++++++------ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 16 +++- 5 files changed, 109 insertions(+), 31 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5605e17458..806821edd2 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -543,6 +543,9 @@ const EVAL_FNM_NOESCAPE: i64 = 1; const EVAL_FNM_PATHNAME: i64 = 2; const EVAL_FNM_PERIOD: i64 = 4; const EVAL_FNM_CASEFOLD: i64 = 16; +const EVAL_ARRAY_FILTER_USE_VALUE: i64 = 0; +const EVAL_ARRAY_FILTER_USE_BOTH: i64 = 1; +const EVAL_ARRAY_FILTER_USE_KEY: i64 = 2; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -2299,12 +2302,7 @@ fn eval_call_user_func_array_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = values.string_bytes(callback)?; - let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; - let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); - if callback.contains("::") { - return Err(EvalStatus::UnsupportedConstruct); - } + let callback = eval_callable_name(callback, values)?; if !values.is_array_like(arg_array)? { return Err(EvalStatus::RuntimeFatal); } @@ -2321,13 +2319,22 @@ fn eval_call_user_func_with_values( let Some((callback, callback_args)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; - let callback = values.string_bytes(*callback)?; + let callback = eval_callable_name(*callback, values)?; + eval_callable_with_values(&callback, callback_args.to_vec(), context, values) +} + +/// Normalizes one string callback name for eval dynamic callable dispatch. +fn eval_callable_name( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = values.string_bytes(callback)?; let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); if callback.contains("::") { return Err(EvalStatus::UnsupportedConstruct); } - eval_callable_with_values(&callback, callback_args.to_vec(), context, values) + Ok(callback) } /// Invokes a PHP-visible callable name with source-order positional values. @@ -2432,10 +2439,12 @@ fn eval_builtin_with_values( eval_array_fill_keys_result(*keys, *value, values)? } "array_filter" => match evaluated_args { - [array] => eval_array_filter_result(*array, None, None, values)?, - [array, callback] => eval_array_filter_result(*array, Some(*callback), None, values)?, + [array] => eval_array_filter_result(*array, None, None, context, values)?, + [array, callback] => { + eval_array_filter_result(*array, Some(*callback), None, context, values)? + } [array, callback, mode] => { - eval_array_filter_result(*array, Some(*callback), Some(*mode), values)? + eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values)? } _ => return Err(EvalStatus::RuntimeFatal), }, @@ -3453,7 +3462,7 @@ fn eval_array_fill_keys_result( Ok(result) } -/// Evaluates PHP `array_filter()` for the default null-callback filtering mode. +/// Evaluates PHP `array_filter()` for null and string-callback filtering modes. fn eval_builtin_array_filter( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -3463,51 +3472,87 @@ fn eval_builtin_array_filter( match args { [array] => { let array = eval_expr(array, context, scope, values)?; - eval_array_filter_result(array, None, None, values) + eval_array_filter_result(array, None, None, context, values) } [array, callback] => { let array = eval_expr(array, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - eval_array_filter_result(array, Some(callback), None, values) + eval_array_filter_result(array, Some(callback), None, context, values) } [array, callback, mode] => { let array = eval_expr(array, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; let mode = eval_expr(mode, context, scope, values)?; - eval_array_filter_result(array, Some(callback), Some(mode), values) + eval_array_filter_result(array, Some(callback), Some(mode), context, values) } _ => Err(EvalStatus::RuntimeFatal), } } -/// Filters falsey values from an eval array while preserving original keys. +/// Filters eval array entries through PHP truthiness or a string callback. fn eval_array_filter_result( array: RuntimeCellHandle, callback: Option, mode: Option, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if let Some(callback) = callback { - if !values.is_null(callback)? { - return Err(EvalStatus::RuntimeFatal); - } - } - if let Some(mode) = mode { - let _ = eval_int_value(mode, values)?; - } + let callback = match callback { + Some(callback) if !values.is_null(callback)? => Some(eval_callable_name(callback, values)?), + _ => None, + }; + let mode = match mode { + Some(mode) => eval_array_filter_mode_value(mode, values)?, + None => EVAL_ARRAY_FILTER_USE_VALUE, + }; let len = values.array_len(array)?; let mut result = values.assoc_new(len)?; for position in 0..len { let key = values.array_iter_key(array, position)?; let value = values.array_get(array, key)?; - if values.truthy(value)? { + let keep = if let Some(callback) = callback.as_deref() { + let args = eval_array_filter_callback_args(mode, key, value)?; + let result = eval_callable_with_values(callback, args, context, values)?; + values.truthy(result)? + } else { + values.truthy(value)? + }; + if keep { result = values.array_set(result, key, value)?; } } Ok(result) } +/// Reads and validates the optional `array_filter()` callback mode. +fn eval_array_filter_mode_value( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = eval_int_value(mode, values)?; + match mode { + EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { + Ok(mode) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the callback argument list for one `array_filter()` entry. +fn eval_array_filter_callback_args( + mode: i64, + key: RuntimeCellHandle, + value: RuntimeCellHandle, +) -> Result, EvalStatus> { + match mode { + EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), + EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), + EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Evaluates PHP `array_chunk()` over one array and chunk-size expression. fn eval_builtin_array_chunk( args: &[EvalExpr], @@ -10599,6 +10644,9 @@ fn eval_predefined_constant_value(name: &str) -> Option "FNM_PATHNAME" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PATHNAME)), "FNM_PERIOD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PERIOD)), "FNM_CASEFOLD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_CASEFOLD)), + "ARRAY_FILTER_USE_VALUE" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_VALUE)), + "ARRAY_FILTER_USE_BOTH" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_BOTH)), + "ARRAY_FILTER_USE_KEY" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_KEY)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), @@ -12945,6 +12993,17 @@ $call = call_user_func("array_filter", [0, 4]); echo count($call) . ":" . $call[1] . ":"; $spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); echo count($spread) . ":" . $spread[1] . ":"; +function eval_keep_even($value) { return $value % 2 == 0; } +$evens = array_filter([1, 2, 3, 4], "eval_keep_even"); +echo count($evens) . ":" . $evens[1] . ":" . $evens[3] . ":"; +function eval_keep_key($key) { return $key === "b"; } +$keyed = array_filter(["a" => 10, "b" => 20], "eval_keep_key", ARRAY_FILTER_USE_KEY); +echo count($keyed) . ":" . $keyed["b"] . ":"; +function eval_keep_both($value, $key) { return $key === "c" || $value === 1; } +$both = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_keep_both", ARRAY_FILTER_USE_BOTH); +echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; +$ints = array_filter([1, "x", 2], "is_int"); +echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; return function_exists("array_filter");"#, ) .expect("parse eval fragment"); @@ -12953,7 +13012,10 @@ return function_exists("array_filter");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:1:2:ok:drop:2:1:3:1:4:1:5:"); + assert_eq!( + values.output, + "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a00d94233d..9c146262c2 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -45,7 +45,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. Non-null callbacks are intentionally still outside the eval subset. +Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. @@ -71,7 +71,7 @@ Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_ Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. -Eval predefined constants bypass the dynamic context and are materialized directly by the interpreter. The current eval-visible set is `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, and `FNM_*`; `defined()` recognizes them and duplicate `define()` attempts follow the same warning path as dynamic constants. +Eval predefined constants bypass the dynamic context and are materialized directly by the interpreter. The current eval-visible set is `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, and `ARRAY_FILTER_USE_*`; `defined()` recognizes them and duplicate `define()` attempts follow the same warning path as dynamic constants. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 11ef4935ec..d3745624b5 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -56,7 +56,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. Non-null callbacks remain outside the eval subset. +Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. @@ -84,7 +84,7 @@ Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and Eval path component calls include `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` with suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, named arguments, callable dispatch, and `function_exists()` probes. -Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, and `FNM_*`. `defined()` sees these names, including an optional leading `\`, and `define()` cannot replace them. +Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, and `ARRAY_FILTER_USE_*`. `defined()` sees these names, including an optional leading `\`, and `define()` cannot replace them. Eval `realpath()` canonicalizes existing paths through the host filesystem and returns `false` when the path cannot be resolved. diff --git a/examples/eval/main.php b/examples/eval/main.php index ebd784fa98..a550b2288e 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -133,6 +133,7 @@ public function __construct(int $value) { $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); +$array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); $mixed_literal = eval('return [2 => "two", "tail"][3] . ":" . (["2" => "two", "next"][3]);'); @@ -266,6 +267,7 @@ public function __construct(int $value) { echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; echo "array-filter=" . $array_filter . "\n"; +echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; echo "array-projection=" . $array_projection . "\n"; echo "mixed-literal=" . $mixed_literal . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c2fdab4f7b..44cf755066 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -807,10 +807,24 @@ $call = call_user_func("array_filter", [0, 4]); echo count($call) . ":" . $call[1] . ":"; $spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); echo count($spread) . ":" . $spread[1] . ":"; +function eval_keep_even($value) { return $value % 2 == 0; } +$evens = array_filter([1, 2, 3, 4], "eval_keep_even"); +echo count($evens) . ":" . $evens[1] . ":" . $evens[3] . ":"; +function eval_keep_key($key) { return $key === "b"; } +$keyed = array_filter(["a" => 10, "b" => 20], "eval_keep_key", ARRAY_FILTER_USE_KEY); +echo count($keyed) . ":" . $keyed["b"] . ":"; +function eval_keep_both($value, $key) { return $key === "c" || $value === 1; } +$both = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_keep_both", ARRAY_FILTER_USE_BOTH); +echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; +$ints = array_filter([1, "x", 2], "is_int"); +echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; echo function_exists("array_filter");'); "#, ); - assert_eq!(out, "3:1:2:ok:drop:2:1:3:1:4:1:5:1"); + assert_eq!( + out, + "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:1" + ); } /// Verifies eval `array_combine()` supports PHP key conversions and callable dispatch. From 4892afeb25bcd4647a705641e2ef10c1cbb1af79 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:31:20 +0200 Subject: [PATCH 0197/1208] Support eval array_map builtin --- crates/elephc-eval/src/interpreter.rs | 78 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 22 ++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 806821edd2..ac2b967f89 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1401,6 +1401,7 @@ fn eval_positional_expr_call( "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), "array_filter" => eval_builtin_array_filter(args, context, scope, values), "array_flip" => eval_builtin_array_flip(args, context, scope, values), + "array_map" => eval_builtin_array_map(args, context, scope, values), "array_keys" | "array_values" => { eval_builtin_array_projection(name, args, context, scope, values) } @@ -1839,6 +1840,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_fill_keys" | "array_filter" | "array_flip" + | "array_map" | "array_key_exists" | "array_keys" | "array_diff" @@ -2142,6 +2144,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_fill" => Some(&["start_index", "count", "value"]), "array_fill_keys" => Some(&["keys", "value"]), "array_filter" => Some(&["array", "callback", "mode"]), + "array_map" => Some(&["callback", "array"]), "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" | "array_rand" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), @@ -2448,6 +2451,12 @@ fn eval_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "array_map" => { + let [callback, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_map_result(*callback, *array, context, values)? + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3462,6 +3471,48 @@ fn eval_array_fill_keys_result( Ok(result) } +/// Evaluates PHP `array_map()` for one source array and a string or null callback. +fn eval_builtin_array_map( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [callback, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_array_map_result(callback, array, context, values) +} + +/// Maps one eval array with PHP key preservation for the one-array form. +fn eval_array_map_result( + callback: RuntimeCellHandle, + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_name(callback, values)?) + }; + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let mapped = if let Some(callback) = callback.as_deref() { + eval_callable_with_values(callback, vec![value], context, values)? + } else { + value + }; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + /// Evaluates PHP `array_filter()` for null and string-callback filtering modes. fn eval_builtin_array_filter( args: &[EvalExpr], @@ -12979,6 +13030,33 @@ return function_exists("array_product");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_map()` applies callbacks and preserves source keys. + #[test] + fn execute_program_dispatches_array_map_builtin() { + let program = parse_fragment( + br#"function eval_map_double($value) { return $value * 2; } +$mapped = array_map("eval_map_double", [1, 2, 3]); +echo $mapped[0] . ":" . $mapped[2] . ":"; +$assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); +echo $assoc["a"] . ":" . $assoc["b"] . ":"; +$identity = array_map(null, ["k" => "v"]); +echo $identity["k"] . ":"; +$call = call_user_func("array_map", "intval", ["7"]); +echo $call[0] . ":"; +$spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); +echo $spread[0] . ":"; +return function_exists("array_map");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:6:X:Y:v:7:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9c146262c2..b6104b9642 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -45,6 +45,8 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. +Eval `array_map()` iterates one source array through eval key/value hooks, invokes string callbacks through the shared eval callable dispatcher, and writes mapped results with the original keys. A null callback uses PHP's one-array identity behavior. + Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index d3745624b5..138822cc63 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -56,6 +56,8 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. +Eval `array_map()` supports the one-array form with a string callback or `null` identity callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; result arrays preserve the source keys, matching PHP's one-array `array_map()` behavior. + Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. diff --git a/examples/eval/main.php b/examples/eval/main.php index a550b2288e..fef9fe9570 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -132,6 +132,7 @@ public function __construct(int $value) { $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); +$array_map = eval('function eval_example_double($value) { return $value * 2; } $mapped = array_map("eval_example_double", ["a" => 2, "b" => 3]); return $mapped["a"] . ":" . $mapped["b"];'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -266,6 +267,7 @@ public function __construct(int $value) { echo "boundaries=" . $boundaries . "\n"; echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; +echo "array-map=" . $array_map . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 44cf755066..2d52dc8b3c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -792,6 +792,28 @@ echo ":"; echo function_exists("array_sum"); echo function_exists("array_product assert_eq!(out, "6:24:0:1:7:7:10:11"); } +/// Verifies eval `array_map()` applies callbacks and preserves source keys. +#[test] +fn test_eval_dispatches_array_map_builtin_call() { + let out = compile_and_run( + r#" "x", "b" => "y"]); +echo $assoc["a"] . ":" . $assoc["b"] . ":"; +$identity = array_map(null, ["k" => "v"]); +echo $identity["k"] . ":"; +$call = call_user_func("array_map", "intval", ["7"]); +echo $call[0] . ":"; +$spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); +echo $spread[0] . ":"; +echo function_exists("array_map");'); +"#, + ); + assert_eq!(out, "2:6:X:Y:v:7:8:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 9a2ce6ff338d89fa76aec7b530d51c17a828457c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:35:43 +0200 Subject: [PATCH 0198/1208] Support eval array_reduce builtin --- crates/elephc-eval/src/interpreter.rs | 68 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 19 ++++++++ 5 files changed, 94 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ac2b967f89..441b51a556 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1402,6 +1402,7 @@ fn eval_positional_expr_call( "array_filter" => eval_builtin_array_filter(args, context, scope, values), "array_flip" => eval_builtin_array_flip(args, context, scope, values), "array_map" => eval_builtin_array_map(args, context, scope, values), + "array_reduce" => eval_builtin_array_reduce(args, context, scope, values), "array_keys" | "array_values" => { eval_builtin_array_projection(name, args, context, scope, values) } @@ -1841,6 +1842,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_filter" | "array_flip" | "array_map" + | "array_reduce" | "array_key_exists" | "array_keys" | "array_diff" @@ -2145,6 +2147,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_fill_keys" => Some(&["keys", "value"]), "array_filter" => Some(&["array", "callback", "mode"]), "array_map" => Some(&["callback", "array"]), + "array_reduce" => Some(&["array", "callback", "initial"]), "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" | "array_rand" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), @@ -2457,6 +2460,12 @@ fn eval_builtin_with_values( }; eval_array_map_result(*callback, *array, context, values)? } + "array_reduce" => { + let [array, callback, initial] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_reduce_result(*array, *callback, *initial, context, values)? + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3513,6 +3522,41 @@ fn eval_array_map_result( Ok(result) } +/// Evaluates PHP `array_reduce()` with an explicit initial carry value. +fn eval_builtin_array_reduce( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback, initial] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let initial = eval_expr(initial, context, scope, values)?; + eval_array_reduce_result(array, callback, initial, context, values) +} + +/// Reduces one eval array by invoking a string callback with carry and item cells. +fn eval_array_reduce_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let len = values.array_len(array)?; + let mut carry = initial; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + carry = eval_callable_with_values(&callback, vec![carry, value], context, values)?; + } + Ok(carry) +} + /// Evaluates PHP `array_filter()` for null and string-callback filtering modes. fn eval_builtin_array_filter( args: &[EvalExpr], @@ -13057,6 +13101,30 @@ return function_exists("array_map");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_reduce()` folds values through a string callback. + #[test] + fn execute_program_dispatches_array_reduce_builtin() { + let program = parse_fragment( + br#"function eval_reduce_sum($carry, $item) { return $carry + $item; } +echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; +function eval_reduce_join($carry, $item) { return $carry . $item; } +echo array_reduce(["a", "b"], "eval_reduce_join", "") . ":"; +$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum", 1); +echo $call . ":"; +$spread = call_user_func_array("array_reduce", ["array" => [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); +echo $spread . ":"; +return function_exists("array_reduce");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "16:ab:10:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index b6104b9642..e48350a4cf 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -47,6 +47,8 @@ Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the inte Eval `array_map()` iterates one source array through eval key/value hooks, invokes string callbacks through the shared eval callable dispatcher, and writes mapped results with the original keys. A null callback uses PHP's one-array identity behavior. +Eval `array_reduce()` reads the initial carry once, walks the source array in PHP iteration order, and invokes the shared eval callable dispatcher with carry and current item cells for each entry. The resulting carry cell is returned directly to the eval caller. + Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 138822cc63..ba1c6c7385 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -58,6 +58,8 @@ Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, an Eval `array_map()` supports the one-array form with a string callback or `null` identity callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; result arrays preserve the source keys, matching PHP's one-array `array_map()` behavior. +Eval `array_reduce()` supports the three-argument form with an explicit initial value and string callbacks. Callback resolution is shared with `call_user_func()`, so eval-declared functions, registered AOT callbacks, and supported builtins can fold the carry and current item. + Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. diff --git a/examples/eval/main.php b/examples/eval/main.php index fef9fe9570..bee1c48252 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -133,6 +133,7 @@ public function __construct(int $value) { $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); $array_map = eval('function eval_example_double($value) { return $value * 2; } $mapped = array_map("eval_example_double", ["a" => 2, "b" => 3]); return $mapped["a"] . ":" . $mapped["b"];'); +$array_reduce = eval('function eval_example_sum($carry, $item) { return $carry + $item; } return array_reduce([1, 2, 3], "eval_example_sum", 10);'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -268,6 +269,7 @@ public function __construct(int $value) { echo "trimmed=" . $trimmed . "\n"; echo "aggregates=" . $aggregates . "\n"; echo "array-map=" . $array_map . "\n"; +echo "array-reduce=" . $array_reduce . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2d52dc8b3c..ecb606b12e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -814,6 +814,25 @@ echo function_exists("array_map");'); assert_eq!(out, "2:6:X:Y:v:7:8:1"); } +/// Verifies eval `array_reduce()` folds values through a string callback. +#[test] +fn test_eval_dispatches_array_reduce_builtin_call() { + let out = compile_and_run( + r#" [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); +echo $spread . ":"; +echo function_exists("array_reduce");'); +"#, + ); + assert_eq!(out, "16:ab:10:9:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 559a43fe4ae782534fadd447ce7bded70248ffcb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:38:55 +0200 Subject: [PATCH 0199/1208] Support eval array_reduce default initial --- crates/elephc-eval/src/interpreter.rs | 41 ++++++++++++++++++--------- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 7 +++-- 5 files changed, 36 insertions(+), 18 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 441b51a556..1a00af7d13 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -2460,12 +2460,16 @@ fn eval_builtin_with_values( }; eval_array_map_result(*callback, *array, context, values)? } - "array_reduce" => { - let [array, callback, initial] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_reduce_result(*array, *callback, *initial, context, values)? - } + "array_reduce" => match evaluated_args { + [array, callback] => { + let initial = values.null()?; + eval_array_reduce_result(*array, *callback, initial, context, values)? + } + [array, callback, initial] => { + eval_array_reduce_result(*array, *callback, *initial, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3529,12 +3533,20 @@ fn eval_builtin_array_reduce( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [array, callback, initial] = args else { - return Err(EvalStatus::RuntimeFatal); + let (array, callback, initial) = match args { + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + (array, callback, values.null()?) + } + [array, callback, initial] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let initial = eval_expr(initial, context, scope, values)?; + (array, callback, initial) + } + _ => return Err(EvalStatus::RuntimeFatal), }; - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let initial = eval_expr(initial, context, scope, values)?; eval_array_reduce_result(array, callback, initial, context, values) } @@ -13108,8 +13120,11 @@ return function_exists("array_map");"#, br#"function eval_reduce_sum($carry, $item) { return $carry + $item; } echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; function eval_reduce_join($carry, $item) { return $carry . $item; } +echo array_reduce([4, 5], "eval_reduce_sum") . ":"; echo array_reduce(["a", "b"], "eval_reduce_join", "") . ":"; -$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum", 1); +$named = array_reduce(array: [6, 7], callback: "eval_reduce_sum"); +echo $named . ":"; +$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum"); echo $call . ":"; $spread = call_user_func_array("array_reduce", ["array" => [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); echo $spread . ":"; @@ -13121,7 +13136,7 @@ return function_exists("array_reduce");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "16:ab:10:9:"); + assert_eq!(values.output, "16:9:ab:13:9:9:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e48350a4cf..48d7777237 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -47,7 +47,7 @@ Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the inte Eval `array_map()` iterates one source array through eval key/value hooks, invokes string callbacks through the shared eval callable dispatcher, and writes mapped results with the original keys. A null callback uses PHP's one-array identity behavior. -Eval `array_reduce()` reads the initial carry once, walks the source array in PHP iteration order, and invokes the shared eval callable dispatcher with carry and current item cells for each entry. The resulting carry cell is returned directly to the eval caller. +Eval `array_reduce()` reads the provided initial carry or creates a `null` carry when the argument is omitted, walks the source array in PHP iteration order, and invokes the shared eval callable dispatcher with carry and current item cells for each entry. The resulting carry cell is returned directly to the eval caller. Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ba1c6c7385..b871096d1f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -58,7 +58,7 @@ Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, an Eval `array_map()` supports the one-array form with a string callback or `null` identity callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; result arrays preserve the source keys, matching PHP's one-array `array_map()` behavior. -Eval `array_reduce()` supports the three-argument form with an explicit initial value and string callbacks. Callback resolution is shared with `call_user_func()`, so eval-declared functions, registered AOT callbacks, and supported builtins can fold the carry and current item. +Eval `array_reduce()` supports the two- or three-argument form with string callbacks, using `null` as the initial carry when the initial value is omitted. Callback resolution is shared with `call_user_func()`, so eval-declared functions, registered AOT callbacks, and supported builtins can fold the carry and current item. Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. diff --git a/examples/eval/main.php b/examples/eval/main.php index bee1c48252..edee8a8a92 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -133,7 +133,7 @@ public function __construct(int $value) { $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); $array_map = eval('function eval_example_double($value) { return $value * 2; } $mapped = array_map("eval_example_double", ["a" => 2, "b" => 3]); return $mapped["a"] . ":" . $mapped["b"];'); -$array_reduce = eval('function eval_example_sum($carry, $item) { return $carry + $item; } return array_reduce([1, 2, 3], "eval_example_sum", 10);'); +$array_reduce = eval('function eval_example_sum($carry, $item) { return $carry + $item; } return array_reduce([1, 2, 3], "eval_example_sum", 10) . ":" . array_reduce([4, 5], "eval_example_sum");'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ecb606b12e..75cd01a3d1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -822,15 +822,18 @@ fn test_eval_dispatches_array_reduce_builtin_call() { eval('function eval_reduce_sum($carry, $item) { return $carry + $item; } echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; function eval_reduce_join($carry, $item) { return $carry . $item; } +echo array_reduce([4, 5], "eval_reduce_sum") . ":"; echo array_reduce(["a", "b"], "eval_reduce_join", "") . ":"; -$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum", 1); +$named = array_reduce(array: [6, 7], callback: "eval_reduce_sum"); +echo $named . ":"; +$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum"); echo $call . ":"; $spread = call_user_func_array("array_reduce", ["array" => [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); echo $spread . ":"; echo function_exists("array_reduce");'); "#, ); - assert_eq!(out, "16:ab:10:9:1"); + assert_eq!(out, "16:9:ab:13:9:9:1"); } /// Verifies eval `array_filter()` removes falsey values and preserves source keys. From ccc5f649c8ea60724da35812fac4849a59da924c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:43:08 +0200 Subject: [PATCH 0200/1208] Support eval array_walk builtin --- crates/elephc-eval/src/interpreter.rs | 63 ++++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++++ 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 1a00af7d13..bfa9f05ebd 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1403,6 +1403,7 @@ fn eval_positional_expr_call( "array_flip" => eval_builtin_array_flip(args, context, scope, values), "array_map" => eval_builtin_array_map(args, context, scope, values), "array_reduce" => eval_builtin_array_reduce(args, context, scope, values), + "array_walk" => eval_builtin_array_walk(args, context, scope, values), "array_keys" | "array_values" => { eval_builtin_array_projection(name, args, context, scope, values) } @@ -1843,6 +1844,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_flip" | "array_map" | "array_reduce" + | "array_walk" | "array_key_exists" | "array_keys" | "array_diff" @@ -2148,6 +2150,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_filter" => Some(&["array", "callback", "mode"]), "array_map" => Some(&["callback", "array"]), "array_reduce" => Some(&["array", "callback", "initial"]), + "array_walk" => Some(&["array", "callback"]), "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" | "array_rand" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), @@ -2470,6 +2473,12 @@ fn eval_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "array_walk" => { + let [array, callback] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_walk_result(*array, *callback, context, values)? + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3526,7 +3535,7 @@ fn eval_array_map_result( Ok(result) } -/// Evaluates PHP `array_reduce()` with an explicit initial carry value. +/// Evaluates PHP `array_reduce()` with an optional initial carry value. fn eval_builtin_array_reduce( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -3569,6 +3578,38 @@ fn eval_array_reduce_result( Ok(carry) } +/// Evaluates PHP `array_walk()` for side-effect callbacks over value/key pairs. +fn eval_builtin_array_walk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_walk_result(array, callback, context, values) +} + +/// Walks one eval array by invoking a string callback with value and key cells. +fn eval_array_walk_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let _ = eval_callable_with_values(&callback, vec![value, key], context, values)?; + } + values.bool_value(true) +} + /// Evaluates PHP `array_filter()` for null and string-callback filtering modes. fn eval_builtin_array_filter( args: &[EvalExpr], @@ -13140,6 +13181,26 @@ return function_exists("array_reduce");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_walk()` invokes string callbacks with value and key cells. + #[test] + fn execute_program_dispatches_array_walk_builtin() { + let program = parse_fragment( + br#"function eval_walk_show($value, $key) { echo $key . "=" . $value . ";"; } +echo array_walk(["a" => 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; +$call = call_user_func("array_walk", [4, 5], "eval_walk_show"); +$spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); +return function_exists("array_walk");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 48d7777237..b2e554f06c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -49,6 +49,8 @@ Eval `array_map()` iterates one source array through eval key/value hooks, invok Eval `array_reduce()` reads the provided initial carry or creates a `null` carry when the argument is omitted, walks the source array in PHP iteration order, and invokes the shared eval callable dispatcher with carry and current item cells for each entry. The resulting carry cell is returned directly to the eval caller. +Eval `array_walk()` iterates keys through the eval value hooks, invokes the shared eval callable dispatcher with value and key cells, discards callback results, and returns a boxed `true` cell. + Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b871096d1f..0dc64975a3 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -60,6 +60,8 @@ Eval `array_map()` supports the one-array form with a string callback or `null` Eval `array_reduce()` supports the two- or three-argument form with string callbacks, using `null` as the initial carry when the initial value is omitted. Callback resolution is shared with `call_user_func()`, so eval-declared functions, registered AOT callbacks, and supported builtins can fold the carry and current item. +Eval `array_walk()` supports the two-argument form with string callbacks. It invokes the callback with value and key cells in PHP iteration order, ignores the callback return value, and returns `true`. + Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. diff --git a/examples/eval/main.php b/examples/eval/main.php index edee8a8a92..4e86862218 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -134,6 +134,7 @@ public function __construct(int $value) { $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); $array_map = eval('function eval_example_double($value) { return $value * 2; } $mapped = array_map("eval_example_double", ["a" => 2, "b" => 3]); return $mapped["a"] . ":" . $mapped["b"];'); $array_reduce = eval('function eval_example_sum($carry, $item) { return $carry + $item; } return array_reduce([1, 2, 3], "eval_example_sum", 10) . ":" . array_reduce([4, 5], "eval_example_sum");'); +$array_walk = eval('function eval_example_walk($value, $key) { echo "walk-" . $key . "=" . $value . "\n"; } return array_walk(["a" => 2, "b" => 3], "eval_example_walk") ? "ok" : "bad";'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -270,6 +271,7 @@ public function __construct(int $value) { echo "aggregates=" . $aggregates . "\n"; echo "array-map=" . $array_map . "\n"; echo "array-reduce=" . $array_reduce . "\n"; +echo "array-walk=" . $array_walk . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 75cd01a3d1..cfa5489634 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -836,6 +836,21 @@ echo function_exists("array_reduce");'); assert_eq!(out, "16:9:ab:13:9:9:1"); } +/// Verifies eval `array_walk()` invokes string callbacks with value and key cells. +#[test] +fn test_eval_dispatches_array_walk_builtin_call() { + let out = compile_and_run( + r#" 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; +$call = call_user_func("array_walk", [4, 5], "eval_walk_show"); +$spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); +echo function_exists("array_walk");'); +"#, + ); + assert_eq!(out, "a=2;b=3;T:0=4;1=5;z=6;1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From e122bd0a7ba3a3ff299c0dd7c0a44e8ec90883d1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:46:13 +0200 Subject: [PATCH 0201/1208] Support eval array_map variadic arrays --- crates/elephc-eval/src/interpreter.rs | 92 ++++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 9 ++- 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index bfa9f05ebd..3f30ba6262 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -2458,10 +2458,10 @@ fn eval_builtin_with_values( _ => return Err(EvalStatus::RuntimeFatal), }, "array_map" => { - let [callback, array] = evaluated_args else { + let Some((callback, arrays)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; - eval_array_map_result(*callback, *array, context, values)? + eval_array_map_result(*callback, arrays, context, values)? } "array_reduce" => match evaluated_args { [array, callback] => { @@ -3500,31 +3500,37 @@ fn eval_builtin_array_map( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [callback, array] = args else { + let Some((callback, arrays)) = args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; let callback = eval_expr(callback, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_array_map_result(callback, array, context, values) + let mut evaluated_arrays = Vec::with_capacity(arrays.len()); + for array in arrays { + evaluated_arrays.push(eval_expr(array, context, scope, values)?); + } + eval_array_map_result(callback, &evaluated_arrays, context, values) } /// Maps one eval array with PHP key preservation for the one-array form. fn eval_array_map_result( callback: RuntimeCellHandle, - array: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + let [array] = arrays else { + return eval_array_map_variadic_result(callback, arrays, context, values); + }; let callback = if values.is_null(callback)? { None } else { Some(eval_callable_name(callback, values)?) }; - let len = values.array_len(array)?; + let len = values.array_len(*array)?; let mut result = values.assoc_new(len)?; for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; + let key = values.array_iter_key(*array, position)?; + let value = values.array_get(*array, key)?; let mapped = if let Some(callback) = callback.as_deref() { eval_callable_with_values(callback, vec![value], context, values)? } else { @@ -3535,6 +3541,65 @@ fn eval_array_map_result( Ok(result) } +/// Maps multiple eval arrays with PHP's reindexed and null-padded variadic behavior. +fn eval_array_map_variadic_result( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if arrays.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_name(callback, values)?) + }; + let mut lengths = Vec::with_capacity(arrays.len()); + let mut max_len = 0; + for array in arrays { + let len = values.array_len(*array)?; + max_len = max_len.max(len); + lengths.push(len); + } + + let mut result = values.array_new(max_len)?; + for position in 0..max_len { + let mut callback_args = Vec::with_capacity(arrays.len()); + for (array, len) in arrays.iter().zip(lengths.iter()) { + let value = if position < *len { + let key = values.array_iter_key(*array, position)?; + values.array_get(*array, key)? + } else { + values.null()? + }; + callback_args.push(value); + } + let mapped = if let Some(callback) = callback.as_deref() { + eval_callable_with_values(callback, callback_args, context, values)? + } else { + eval_array_map_zipped_row(callback_args, values)? + }; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Builds one row for `array_map(null, $a, $b, ...)`. +fn eval_array_map_zipped_row( + values_row: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut row = values.array_new(values_row.len())?; + for (index, value) in values_row.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + row = values.array_set(row, key, value)?; + } + Ok(row) +} + /// Evaluates PHP `array_reduce()` with an optional initial carry value. fn eval_builtin_array_reduce( args: &[EvalExpr], @@ -13138,8 +13203,15 @@ $assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); echo $assoc["a"] . ":" . $assoc["b"] . ":"; $identity = array_map(null, ["k" => "v"]); echo $identity["k"] . ":"; +function eval_map_pair($left, $right) { return $left . "-" . ($right ?? "N"); } +$pairs = array_map("eval_map_pair", ["a" => "L", "b" => "R"], ["x" => "1"]); +echo $pairs[0] . ":" . $pairs[1] . ":"; +$zipped = array_map(null, [1, 2], [3, 4]); +echo $zipped[0][0] . $zipped[0][1] . ":" . $zipped[1][0] . $zipped[1][1] . ":"; $call = call_user_func("array_map", "intval", ["7"]); echo $call[0] . ":"; +$multi_call = call_user_func("array_map", "eval_map_pair", ["Q"], ["9"]); +echo $multi_call[0] . ":"; $spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); echo $spread[0] . ":"; return function_exists("array_map");"#, @@ -13150,7 +13222,7 @@ return function_exists("array_map");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:6:X:Y:v:7:8:"); + assert_eq!(values.output, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index b2e554f06c..c94b4baa1a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -45,7 +45,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `array_map()` iterates one source array through eval key/value hooks, invokes string callbacks through the shared eval callable dispatcher, and writes mapped results with the original keys. A null callback uses PHP's one-array identity behavior. +Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. Eval `array_reduce()` reads the provided initial carry or creates a `null` carry when the argument is omitted, walks the source array in PHP iteration order, and invokes the shared eval callable dispatcher with carry and current item cells for each entry. The resulting carry cell is returned directly to the eval caller. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 0dc64975a3..cbf00d1574 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -56,7 +56,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `array_map()` supports the one-array form with a string callback or `null` identity callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; result arrays preserve the source keys, matching PHP's one-array `array_map()` behavior. +Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. Eval `array_reduce()` supports the two- or three-argument form with string callbacks, using `null` as the initial carry when the initial value is omitted. Callback resolution is shared with `call_user_func()`, so eval-declared functions, registered AOT callbacks, and supported builtins can fold the carry and current item. diff --git a/examples/eval/main.php b/examples/eval/main.php index 4e86862218..e263133509 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -132,7 +132,7 @@ public function __construct(int $value) { $boundaries = eval('return (str_starts_with("dynamic eval", "dynamic") ? "starts" : "missing") . ":" . (str_ends_with("dynamic eval", "eval") ? "ends" : "missing");'); $trimmed = eval('return trim(" boxed ") . ":" . ltrim("0007", "0") . ":" . chop("tail...", ".");'); $aggregates = eval('return array_sum([1, 2, 3]) . ":" . array_product([2, 3, 4]);'); -$array_map = eval('function eval_example_double($value) { return $value * 2; } $mapped = array_map("eval_example_double", ["a" => 2, "b" => 3]); return $mapped["a"] . ":" . $mapped["b"];'); +$array_map = eval('function eval_example_double($value) { return $value * 2; } function eval_example_pair($left, $right) { return $left . $right; } $mapped = array_map("eval_example_double", ["a" => 2, "b" => 3]); $pairs = array_map("eval_example_pair", ["x"], ["y"]); return $mapped["a"] . ":" . $mapped["b"] . ":" . $pairs[0];'); $array_reduce = eval('function eval_example_sum($carry, $item) { return $carry + $item; } return array_reduce([1, 2, 3], "eval_example_sum", 10) . ":" . array_reduce([4, 5], "eval_example_sum");'); $array_walk = eval('function eval_example_walk($value, $key) { echo "walk-" . $key . "=" . $value . "\n"; } return array_walk(["a" => 2, "b" => 3], "eval_example_walk") ? "ok" : "bad";'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cfa5489634..a7b3233a93 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -804,14 +804,21 @@ $assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); echo $assoc["a"] . ":" . $assoc["b"] . ":"; $identity = array_map(null, ["k" => "v"]); echo $identity["k"] . ":"; +function eval_map_pair($left, $right) { return $left . "-" . ($right ?? "N"); } +$pairs = array_map("eval_map_pair", ["a" => "L", "b" => "R"], ["x" => "1"]); +echo $pairs[0] . ":" . $pairs[1] . ":"; +$zipped = array_map(null, [1, 2], [3, 4]); +echo $zipped[0][0] . $zipped[0][1] . ":" . $zipped[1][0] . $zipped[1][1] . ":"; $call = call_user_func("array_map", "intval", ["7"]); echo $call[0] . ":"; +$multi_call = call_user_func("array_map", "eval_map_pair", ["Q"], ["9"]); +echo $multi_call[0] . ":"; $spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); echo $spread[0] . ":"; echo function_exists("array_map");'); "#, ); - assert_eq!(out, "2:6:X:Y:v:7:8:1"); + assert_eq!(out, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:1"); } /// Verifies eval `array_reduce()` folds values through a string callback. From a770906047a332d4781d6179d85dd333866962cb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:51:28 +0200 Subject: [PATCH 0202/1208] Support eval count recursive mode --- crates/elephc-eval/src/interpreter.rs | 90 ++++++++++++++++++++++----- docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 2 + examples/eval/main.php | 2 +- tests/codegen/eval.rs | 8 ++- 5 files changed, 86 insertions(+), 18 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 3f30ba6262..903b2b6b72 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -546,6 +546,8 @@ const EVAL_FNM_CASEFOLD: i64 = 16; const EVAL_ARRAY_FILTER_USE_VALUE: i64 = 0; const EVAL_ARRAY_FILTER_USE_BOTH: i64 = 1; const EVAL_ARRAY_FILTER_USE_KEY: i64 = 2; +const EVAL_COUNT_NORMAL: i64 = 0; +const EVAL_COUNT_RECURSIVE: i64 = 1; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -2869,14 +2871,11 @@ fn eval_builtin_with_values( }; eval_cast_result(name, *value, values)? } - "count" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let len = values.array_len(*value)?; - let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len)? - } + "count" => match evaluated_args { + [value] => eval_count_result(*value, None, values)?, + [value, mode] => eval_count_result(*value, Some(*mode), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "crc32" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -10156,15 +10155,67 @@ fn eval_builtin_count( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_count_result(value, None, values) + } + [value, mode] => { + let value = eval_expr(value, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_count_result(value, Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Counts an eval array with PHP normal or recursive mode semantics. +fn eval_count_result( + value: RuntimeCellHandle, + mode: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => eval_int_value(mode, values)?, + None => EVAL_COUNT_NORMAL, + }; + let len = match mode { + EVAL_COUNT_NORMAL => values.array_len(value)?, + EVAL_COUNT_RECURSIVE => eval_count_recursive_len(value, values, &mut Vec::new())?, + _ => return Err(EvalStatus::RuntimeFatal), }; - let value = eval_expr(value, context, scope, values)?; - let len = values.array_len(value)?; let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len) } +/// Recursively counts nested eval arrays for `count($value, COUNT_RECURSIVE)`. +fn eval_count_recursive_len( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + arrays_seen: &mut Vec, +) -> Result { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + return Ok(0); + } + arrays_seen.push(address); + + let len = values.array_len(value)?; + let mut total = len; + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + if values.is_array_like(element)? { + total = total + .checked_add(eval_count_recursive_len(element, values, arrays_seen)?) + .ok_or(EvalStatus::RuntimeFatal)?; + } + } + + arrays_seen.pop(); + Ok(total) +} + /// Evaluates PHP `print_r()` over one eval expression. fn eval_builtin_print_r( args: &[EvalExpr], @@ -10860,6 +10911,8 @@ fn eval_predefined_constant_value(name: &str) -> Option "ARRAY_FILTER_USE_VALUE" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_VALUE)), "ARRAY_FILTER_USE_BOTH" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_BOTH)), "ARRAY_FILTER_USE_KEY" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_KEY)), + "COUNT_NORMAL" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_NORMAL)), + "COUNT_RECURSIVE" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_RECURSIVE)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), @@ -13118,14 +13171,21 @@ return call_user_func_array("dyn", ["y" => 2, 1]);"#, /// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. #[test] fn execute_program_dispatches_simple_builtins() { - let program = parse_fragment(br#"return strlen("abc") + count([1, 2, 3]);"#) - .expect("parse eval fragment"); + let program = parse_fragment( + br#"echo strlen("abc") . ":" . count([1, [2, 3], [4]]) . ":"; +echo count([1, [2, 3], [4]], COUNT_RECURSIVE) . ":"; +echo call_user_func("count", [1, [2]]) . ":"; +echo call_user_func_array("count", ["value" => [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; +return defined("COUNT_RECURSIVE");"#, + ) + .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(6)); + assert_eq!(values.output, "3:3:6:2:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies direct eval builtin calls bind named and unpacked arguments. diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c94b4baa1a..75dbfde081 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -53,6 +53,8 @@ Eval `array_walk()` iterates keys through the eval value hooks, invokes the shar Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. +Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. + Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index cbf00d1574..64284bf48b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -64,6 +64,8 @@ Eval `array_walk()` supports the two-argument form with string callbacks. It inv Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. +Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. + Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. diff --git a/examples/eval/main.php b/examples/eval/main.php index e263133509..20605c56e9 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -68,7 +68,7 @@ public function __construct(int $value) { eval('if (isset($profile["name"])) { echo "isset-name\n"; }'); eval('if (empty($profile["missing"])) { echo "empty-missing\n"; }'); $meta = eval('return ["source" => "eval"];'); -$meta_count = eval('return count($meta);'); +$meta_count = eval('return count($meta) . ":" . count([1, [2, 3], [4]], COUNT_RECURSIVE);'); eval('function plus_one($value) { return $value + 1; }'); eval('function eval_example_counter() { static $n = 0; $n++; return $n; }'); $dynamic_call = eval('return plus_one(4);'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a7b3233a93..0e9762a644 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -739,10 +739,14 @@ fn test_eval_nested_eval_return_value_is_expression_result() { fn test_eval_dispatches_simple_builtin_calls() { let out = compile_and_run( r#" [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; +echo defined("COUNT_RECURSIVE") ? "C" : "bad";'); "#, ); - assert_eq!(out, "4:2:3"); + assert_eq!(out, "4:2:3:6:2:3:C"); } /// Verifies eval direct builtin calls bind named arguments and spread arrays. From 4d76381ee2bbf062d36c545382026ad009eb8c14 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 15:58:03 +0200 Subject: [PATCH 0203/1208] Support eval json_encode builtin --- crates/elephc-eval/src/interpreter.rs | 212 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 ++ 5 files changed, 234 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 903b2b6b72..6d5f344b59 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1517,6 +1517,7 @@ fn eval_positional_expr_call( eval_builtin_type_predicate(name, args, context, scope, values) } "ip2long" => eval_builtin_ip2long(args, context, scope, values), + "json_encode" => eval_builtin_json_encode(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "log" => eval_builtin_log(args, context, scope, values), @@ -1974,6 +1975,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_real" | "is_resource" | "is_string" + | "json_encode" | "lcfirst" | "log" | "log2" @@ -2221,6 +2223,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "inet_pton" => Some(&["ip"]), "intdiv" => Some(&["num1", "num2"]), "ip2long" => Some(&["ip"]), + "json_encode" => Some(&["value", "flags", "depth"]), "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), "log" => Some(&["num", "base"]), @@ -2984,6 +2987,14 @@ fn eval_builtin_with_values( values.bool_value(eval_function_probe_exists(context, &name))? } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "json_encode" => match evaluated_args { + [value] => eval_json_encode_result(*value, None, None, values)?, + [value, flags] => eval_json_encode_result(*value, Some(*flags), None, values)?, + [value, flags, depth] => { + eval_json_encode_result(*value, Some(*flags), Some(*depth), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "gethostbyaddr" => { let [ip] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -10216,6 +10227,184 @@ fn eval_count_recursive_len( Ok(total) } +/// Evaluates PHP `json_encode()` for zero-flag scalar and array values. +fn eval_builtin_json_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_json_encode_result(value, None, None, values) + } + [value, flags] => { + let value = eval_expr(value, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_encode_result(value, Some(flags), None, values) + } + [value, flags, depth] => { + let value = eval_expr(value, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_encode_result(value, Some(flags), Some(depth), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Encodes one runtime cell as a JSON string when flags are zero. +fn eval_json_encode_result( + value: RuntimeCellHandle, + flags: Option, + depth: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0) + != 0 + { + return Err(EvalStatus::UnsupportedConstruct); + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let mut output = Vec::new(); + eval_json_encode_append(value, values, depth as usize, 0, &mut Vec::new(), &mut output)?; + values.string_bytes_value(&output) +} + +/// Appends one JSON value to the output buffer. +fn eval_json_encode_append( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => output.extend_from_slice(&values.string_bytes(value)?), + EVAL_TAG_STRING => eval_json_encode_append_string(&values.string_bytes(value)?, output), + EVAL_TAG_BOOL => { + if values.truthy(value)? { + output.extend_from_slice(b"true"); + } else { + output.extend_from_slice(b"false"); + } + } + EVAL_TAG_ARRAY => { + eval_json_encode_append_indexed_array(value, values, depth_limit, depth, arrays_seen, output)?; + } + EVAL_TAG_ASSOC => { + eval_json_encode_append_assoc(value, values, depth_limit, depth, arrays_seen, output)?; + } + EVAL_TAG_NULL | EVAL_TAG_OBJECT | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), + _ => return Err(EvalStatus::UnsupportedConstruct), + } + Ok(()) +} + +/// Appends one indexed eval array as a JSON array. +fn eval_json_encode_append_indexed_array( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + output.push(b'['); + let len = values.array_len(value)?; + for position in 0..len { + if position > 0 { + output.push(b','); + } + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + eval_json_encode_append(element, values, depth_limit, depth + 1, arrays_seen, output)?; + } + output.push(b']'); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one associative eval array as a JSON object. +fn eval_json_encode_append_assoc( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + output.push(b'{'); + let len = values.array_len(value)?; + for position in 0..len { + if position > 0 { + output.push(b','); + } + let key = values.array_iter_key(value, position)?; + eval_json_encode_append_string(&values.string_bytes(key)?, output); + output.push(b':'); + let element = values.array_get(value, key)?; + eval_json_encode_append(element, values, depth_limit, depth + 1, arrays_seen, output)?; + } + output.push(b'}'); + arrays_seen.pop(); + Ok(()) +} + +/// Records entry into one JSON array/object, rejecting depth overrun and recursion. +fn eval_json_encode_enter_array( + value: RuntimeCellHandle, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, +) -> Result<(), EvalStatus> { + if depth >= depth_limit { + return Err(EvalStatus::RuntimeFatal); + } + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + return Err(EvalStatus::RuntimeFatal); + } + arrays_seen.push(address); + Ok(()) +} + +/// Appends one JSON string with the default PHP slash and control-character escaping. +fn eval_json_encode_append_string(bytes: &[u8], output: &mut Vec) { + output.push(b'"'); + for byte in bytes { + match *byte { + b'"' => output.extend_from_slice(b"\\\""), + b'\\' => output.extend_from_slice(b"\\\\"), + b'/' => output.extend_from_slice(b"\\/"), + b'\x08' => output.extend_from_slice(b"\\b"), + b'\x0c' => output.extend_from_slice(b"\\f"), + b'\n' => output.extend_from_slice(b"\\n"), + b'\r' => output.extend_from_slice(b"\\r"), + b'\t' => output.extend_from_slice(b"\\t"), + control @ 0x00..=0x1f => { + output.extend_from_slice(format!("\\u{control:04x}").as_bytes()); + } + _ => output.push(*byte), + } + } + output.push(b'"'); +} + /// Evaluates PHP `print_r()` over one eval expression. fn eval_builtin_print_r( args: &[EvalExpr], @@ -13188,6 +13377,29 @@ return defined("COUNT_RECURSIVE");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. + #[test] + fn execute_program_dispatches_json_encode_builtin() { + let program = parse_fragment( + br#"echo json_encode(["a" => 1, "b" => "x/y"]) . ":"; +echo json_encode([1, "q", true, null]) . ":"; +echo call_user_func("json_encode", "a/b\"c") . ":"; +echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; +return function_exists("json_encode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies direct eval builtin calls bind named and unpacked arguments. #[test] fn execute_program_dispatches_named_and_spread_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 75dbfde081..bd23c81df2 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -45,6 +45,8 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. +Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. + Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. Eval `array_reduce()` reads the provided initial carry or creates a `null` carry when the argument is omitted, walks the source array in PHP iteration order, and invokes the shared eval callable dispatcher with carry and current item cells for each entry. The resulting carry cell is returned directly to the eval caller. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 64284bf48b..744e7a63e8 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -56,6 +56,8 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. +Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. + Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. Eval `array_reduce()` supports the two- or three-argument form with string callbacks, using `null` as the initial carry when the initial value is omitted. Callback resolution is shared with `call_user_func()`, so eval-declared functions, registered AOT callbacks, and supported builtins can fold the carry and current item. diff --git a/examples/eval/main.php b/examples/eval/main.php index 20605c56e9..14ad01f0ca 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -125,6 +125,7 @@ public function __construct(int $value) { $case = eval('return strtoupper("eval") . ":" . strtolower("LOUD") . ":" . ucfirst("eval") . ":" . lcfirst("LOUD");'); $word_case = eval('return ucwords("hello eval");'); $reversed = eval('return strrev("eval");'); +$json_encoded = eval('return json_encode(["name" => "Ada", "skills" => ["math", "code"]]);'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); $substring_from = eval('return strstr("user@example.com", "@");'); @@ -262,6 +263,7 @@ public function __construct(int $value) { echo "case=" . $case . "\n"; echo "ucwords=" . $word_case . "\n"; echo "reversed=" . $reversed . "\n"; +echo "json-encoded=" . $json_encoded . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; echo "strstr=" . $substring_from . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0e9762a644..7548b6681e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -749,6 +749,21 @@ echo defined("COUNT_RECURSIVE") ? "C" : "bad";'); assert_eq!(out, "4:2:3:6:2:3:C"); } +/// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. +#[test] +fn test_eval_dispatches_json_encode_builtin_call() { + let out = compile_and_run( + r#" 1, "b" => "x/y"]) . ":"; +echo json_encode([1, "q", true, null]) . ":"; +echo call_user_func("json_encode", "a/b\"c") . ":"; +echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; +echo function_exists("json_encode");'); +"#, + ); + assert_eq!(out, r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:1"#); +} + /// Verifies eval direct builtin calls bind named arguments and spread arrays. #[test] fn test_eval_dispatches_named_and_spread_builtin_calls() { From 595cae39f8ba63083398cc868f4f162837a114b5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 16:03:24 +0200 Subject: [PATCH 0204/1208] Support eval json error status builtins --- crates/elephc-eval/src/interpreter.rs | 58 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 14 +++++++ 5 files changed, 77 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6d5f344b59..5a00af6f90 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1518,6 +1518,8 @@ fn eval_positional_expr_call( } "ip2long" => eval_builtin_ip2long(args, context, scope, values), "json_encode" => eval_builtin_json_encode(args, context, scope, values), + "json_last_error" => eval_builtin_json_last_error(args, values), + "json_last_error_msg" => eval_builtin_json_last_error_msg(args, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "log" => eval_builtin_log(args, context, scope, values), @@ -1976,6 +1978,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_resource" | "is_string" | "json_encode" + | "json_last_error" + | "json_last_error_msg" | "lcfirst" | "log" | "log2" @@ -2224,6 +2228,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "intdiv" => Some(&["num1", "num2"]), "ip2long" => Some(&["ip"]), "json_encode" => Some(&["value", "flags", "depth"]), + "json_last_error" | "json_last_error_msg" => Some(&[]), "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), "log" => Some(&["num", "base"]), @@ -2995,6 +3000,18 @@ fn eval_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "json_last_error" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.int(0)? + } + "json_last_error_msg" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string_bytes_value(b"No error")? + } "gethostbyaddr" => { let [ip] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -10282,6 +10299,28 @@ fn eval_json_encode_result( values.string_bytes_value(&output) } +/// Evaluates PHP `json_last_error()` with the eval interpreter's current no-error state. +fn eval_builtin_json_last_error( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.int(0) +} + +/// Evaluates PHP `json_last_error_msg()` with the eval interpreter's current no-error state. +fn eval_builtin_json_last_error_msg( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string_bytes_value(b"No error") +} + /// Appends one JSON value to the output buffer. fn eval_json_encode_append( value: RuntimeCellHandle, @@ -13400,6 +13439,25 @@ return function_exists("json_encode");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `json_last_error()` reports the no-error JSON status. + #[test] + fn execute_program_dispatches_json_last_error_builtins() { + let program = parse_fragment( + br#"echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo call_user_func("json_last_error") . ":"; +echo call_user_func_array("json_last_error_msg", []) . ":"; +return function_exists("json_last_error") && function_exists("json_last_error_msg");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:No error:0:No error:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies direct eval builtin calls bind named and unpacked arguments. #[test] fn execute_program_dispatches_named_and_spread_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index bd23c81df2..ba29c93b3f 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -45,7 +45,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. +Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Until eval JSON decoding/validation is bridged to the native JSON error-state runtime, eval `json_last_error()` and `json_last_error_msg()` report the no-error state directly. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 744e7a63e8..6a7c22f42a 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -56,7 +56,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. +Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_last_error()` and `json_last_error_msg()` currently report `JSON_ERROR_NONE` / `"No error"` because eval JSON decoding and validation are not yet bridged into the native JSON error-state runtime. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/examples/eval/main.php b/examples/eval/main.php index 14ad01f0ca..1dff5a3341 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -126,6 +126,7 @@ public function __construct(int $value) { $word_case = eval('return ucwords("hello eval");'); $reversed = eval('return strrev("eval");'); $json_encoded = eval('return json_encode(["name" => "Ada", "skills" => ["math", "code"]]);'); +$json_status = eval('return json_last_error() . ":" . json_last_error_msg();'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); $substring_from = eval('return strstr("user@example.com", "@");'); @@ -264,6 +265,7 @@ public function __construct(int $value) { echo "ucwords=" . $word_case . "\n"; echo "reversed=" . $reversed . "\n"; echo "json-encoded=" . $json_encoded . "\n"; +echo "json-status=" . $json_status . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; echo "strstr=" . $substring_from . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7548b6681e..d1015e1659 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -764,6 +764,20 @@ echo function_exists("json_encode");'); assert_eq!(out, r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:1"#); } +/// Verifies eval `json_last_error()` and `json_last_error_msg()` report no-error status. +#[test] +fn test_eval_dispatches_json_last_error_builtin_calls() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 16:13:01 +0200 Subject: [PATCH 0205/1208] Support eval json_validate builtin --- crates/elephc-eval/src/interpreter.rs | 87 +++++++ crates/elephc-eval/src/json_validate.rs | 312 ++++++++++++++++++++++++ crates/elephc-eval/src/lib.rs | 1 + docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 16 ++ 7 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 crates/elephc-eval/src/json_validate.rs diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5a00af6f90..7e32055617 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -17,6 +17,7 @@ use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; +use crate::json_validate; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; @@ -1520,6 +1521,7 @@ fn eval_positional_expr_call( "json_encode" => eval_builtin_json_encode(args, context, scope, values), "json_last_error" => eval_builtin_json_last_error(args, values), "json_last_error_msg" => eval_builtin_json_last_error_msg(args, values), + "json_validate" => eval_builtin_json_validate(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "log" => eval_builtin_log(args, context, scope, values), @@ -1980,6 +1982,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "json_encode" | "json_last_error" | "json_last_error_msg" + | "json_validate" | "lcfirst" | "log" | "log2" @@ -2229,6 +2232,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "ip2long" => Some(&["ip"]), "json_encode" => Some(&["value", "flags", "depth"]), "json_last_error" | "json_last_error_msg" => Some(&[]), + "json_validate" => Some(&["json", "depth", "flags"]), "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), "log" => Some(&["num", "base"]), @@ -3012,6 +3016,14 @@ fn eval_builtin_with_values( } values.string_bytes_value(b"No error")? } + "json_validate" => match evaluated_args { + [json] => eval_json_validate_result(*json, None, None, values)?, + [json, depth] => eval_json_validate_result(*json, Some(*depth), None, values)?, + [json, depth, flags] => { + eval_json_validate_result(*json, Some(*depth), Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "gethostbyaddr" => { let [ip] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -10321,6 +10333,60 @@ fn eval_builtin_json_last_error_msg( values.string_bytes_value(b"No error") } +/// Evaluates PHP `json_validate()` for zero-flag JSON text validation. +fn eval_builtin_json_validate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [json] => { + let json = eval_expr(json, context, scope, values)?; + eval_json_validate_result(json, None, None, values) + } + [json, depth] => { + let json = eval_expr(json, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_validate_result(json, Some(depth), None, values) + } + [json, depth, flags] => { + let json = eval_expr(json, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_validate_result(json, Some(depth), Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Validates JSON text with eval's current zero-flag JSON subset. +fn eval_json_validate_result( + json: RuntimeCellHandle, + depth: Option, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0) + != 0 + { + return Err(EvalStatus::UnsupportedConstruct); + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let bytes = values.string_bytes(json)?; + values.bool_value(json_validate::bytes(&bytes, depth as usize)) +} + /// Appends one JSON value to the output buffer. fn eval_json_encode_append( value: RuntimeCellHandle, @@ -13458,6 +13524,27 @@ return function_exists("json_last_error") && function_exists("json_last_error_ms assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. + #[test] + fn execute_program_dispatches_json_validate_builtin() { + let program = parse_fragment( + br#"echo (json_validate("{\"a\":[1,true,null,\"caf\\u00e9\"]}") ? "Y" : "N") . ":"; +echo (json_validate("bad") ? "bad" : "N") . ":"; +echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; +echo (call_user_func("json_validate", "\"x\"") ? "C" : "bad") . ":"; +echo (call_user_func_array("json_validate", ["json" => "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; +return function_exists("json_validate");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:D:C:A:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies direct eval builtin calls bind named and unpacked arguments. #[test] fn execute_program_dispatches_named_and_spread_builtins() { diff --git a/crates/elephc-eval/src/json_validate.rs b/crates/elephc-eval/src/json_validate.rs new file mode 100644 index 0000000000..49754e4f56 --- /dev/null +++ b/crates/elephc-eval/src/json_validate.rs @@ -0,0 +1,312 @@ +//! Purpose: +//! Validates JSON byte streams for eval-side `json_validate()` calls. +//! The validator checks syntax and depth without allocating decoded PHP values. +//! +//! Called from: +//! - `crate::interpreter` when dispatching eval `json_validate()`. +//! +//! Key details: +//! - Container depth follows PHP decode/validate semantics: entering a container +//! is rejected when the active depth would reach the requested limit. +//! - String validation accepts JSON escapes, paired UTF-16 surrogate escapes, +//! and raw UTF-8 bytes while rejecting control bytes and malformed UTF-8. + +/// Returns whether one byte slice is a complete JSON document within the depth limit. +pub(crate) fn bytes(bytes: &[u8], depth_limit: usize) -> bool { + let mut parser = Validator::new(bytes, depth_limit); + parser.parse_document() +} + +/// Cursor-based JSON validator for eval `json_validate()` calls. +struct Validator<'a> { + bytes: &'a [u8], + cursor: usize, + depth_limit: usize, +} + +impl<'a> Validator<'a> { + /// Creates a JSON validator over one immutable byte slice. + fn new(bytes: &'a [u8], depth_limit: usize) -> Self { + Self { + bytes, + cursor: 0, + depth_limit, + } + } + + /// Parses one complete JSON document and rejects trailing non-whitespace bytes. + fn parse_document(&mut self) -> bool { + self.skip_ws(); + if !self.parse_value(0) { + return false; + } + self.skip_ws(); + self.cursor == self.bytes.len() + } + + /// Parses any JSON value at the given active container depth. + fn parse_value(&mut self, depth: usize) -> bool { + self.skip_ws(); + match self.peek() { + Some(b'n') => self.consume_literal(b"null"), + Some(b't') => self.consume_literal(b"true"), + Some(b'f') => self.consume_literal(b"false"), + Some(b'"') => self.parse_string(), + Some(b'[') => self.parse_array(depth), + Some(b'{') => self.parse_object(depth), + Some(b'-' | b'0'..=b'9') => self.parse_number(), + _ => false, + } + } + + /// Parses a JSON array and enforces PHP's validate/decode depth threshold. + fn parse_array(&mut self, depth: usize) -> bool { + if depth + 1 >= self.depth_limit { + return false; + } + self.cursor += 1; + self.skip_ws(); + if self.consume_byte(b']') { + return true; + } + + loop { + if !self.parse_value(depth + 1) { + return false; + } + self.skip_ws(); + if self.consume_byte(b']') { + return true; + } + if !self.consume_byte(b',') { + return false; + } + } + } + + /// Parses a JSON object and enforces PHP's validate/decode depth threshold. + fn parse_object(&mut self, depth: usize) -> bool { + if depth + 1 >= self.depth_limit { + return false; + } + self.cursor += 1; + self.skip_ws(); + if self.consume_byte(b'}') { + return true; + } + + loop { + self.skip_ws(); + if !self.parse_string() { + return false; + } + self.skip_ws(); + if !self.consume_byte(b':') { + return false; + } + if !self.parse_value(depth + 1) { + return false; + } + self.skip_ws(); + if self.consume_byte(b'}') { + return true; + } + if !self.consume_byte(b',') { + return false; + } + } + } + + /// Parses a JSON string, including escapes, UTF-8 bytes, and surrogate-pair escapes. + fn parse_string(&mut self) -> bool { + if !self.consume_byte(b'"') { + return false; + } + + while let Some(byte) = self.peek() { + match byte { + b'"' => { + self.cursor += 1; + return true; + } + b'\\' => { + if !self.parse_string_escape() { + return false; + } + } + 0x00..=0x1f => return false, + 0x00..=0x7f => self.cursor += 1, + _ => { + if !self.consume_utf8_char() { + return false; + } + } + } + } + false + } + + /// Parses one JSON string escape sequence at the current backslash. + fn parse_string_escape(&mut self) -> bool { + self.cursor += 1; + match self.peek() { + Some(b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't') => { + self.cursor += 1; + true + } + Some(b'u') => self.parse_unicode_escape(), + _ => false, + } + } + + /// Parses one JSON `\uXXXX` escape, including mandatory surrogate pairs. + fn parse_unicode_escape(&mut self) -> bool { + let Some(unit) = self.parse_unicode_unit() else { + return false; + }; + if (0xd800..=0xdbff).contains(&unit) { + let checkpoint = self.cursor; + if !self.consume_byte(b'\\') || !self.consume_byte(b'u') { + return false; + } + let Some(low) = self.parse_unicode_unit_after_u() else { + self.cursor = checkpoint; + return false; + }; + (0xdc00..=0xdfff).contains(&low) + } else { + !(0xdc00..=0xdfff).contains(&unit) + } + } + + /// Parses the `uXXXX` suffix after the backslash has already been consumed. + fn parse_unicode_unit(&mut self) -> Option { + if !self.consume_byte(b'u') { + return None; + } + self.parse_unicode_unit_after_u() + } + + /// Parses the four hex digits after a consumed JSON unicode escape marker. + fn parse_unicode_unit_after_u(&mut self) -> Option { + if self.cursor + 4 > self.bytes.len() { + return None; + } + let mut value = 0_u16; + for _ in 0..4 { + value = value.checked_mul(16)?; + value = value.checked_add(u16::from(hex_value(self.bytes[self.cursor])?))?; + self.cursor += 1; + } + Some(value) + } + + /// Parses a JSON number with RFC-compatible leading-zero, fraction, and exponent rules. + fn parse_number(&mut self) -> bool { + if self.consume_byte(b'-') && self.peek().is_none() { + return false; + } + + match self.peek() { + Some(b'0') => { + self.cursor += 1; + if matches!(self.peek(), Some(b'0'..=b'9')) { + return false; + } + } + Some(b'1'..=b'9') => { + self.cursor += 1; + while matches!(self.peek(), Some(b'0'..=b'9')) { + self.cursor += 1; + } + } + _ => return false, + } + + if self.consume_byte(b'.') { + if !matches!(self.peek(), Some(b'0'..=b'9')) { + return false; + } + while matches!(self.peek(), Some(b'0'..=b'9')) { + self.cursor += 1; + } + } + + if matches!(self.peek(), Some(b'e' | b'E')) { + self.cursor += 1; + if matches!(self.peek(), Some(b'+' | b'-')) { + self.cursor += 1; + } + if !matches!(self.peek(), Some(b'0'..=b'9')) { + return false; + } + while matches!(self.peek(), Some(b'0'..=b'9')) { + self.cursor += 1; + } + } + + true + } + + /// Consumes exactly one expected byte when it is present. + fn consume_byte(&mut self, expected: u8) -> bool { + if self.peek() == Some(expected) { + self.cursor += 1; + true + } else { + false + } + } + + /// Consumes one ASCII literal at the current cursor. + fn consume_literal(&mut self, literal: &[u8]) -> bool { + if self.bytes[self.cursor..].starts_with(literal) { + self.cursor += literal.len(); + true + } else { + false + } + } + + /// Consumes one valid UTF-8 codepoint from a raw JSON string segment. + fn consume_utf8_char(&mut self) -> bool { + let first = self.bytes[self.cursor]; + let width = match first { + 0xc2..=0xdf => 2, + 0xe0..=0xef => 3, + 0xf0..=0xf4 => 4, + _ => return false, + }; + if self.cursor + width > self.bytes.len() { + return false; + } + let slice = &self.bytes[self.cursor..self.cursor + width]; + if std::str::from_utf8(slice).is_err() { + return false; + } + self.cursor += width; + true + } + + /// Skips JSON whitespace accepted between tokens. + fn skip_ws(&mut self) { + while matches!(self.peek(), Some(b' ' | b'\n' | b'\r' | b'\t')) { + self.cursor += 1; + } + } + + /// Returns the current byte without advancing. + fn peek(&self) -> Option { + self.bytes.get(self.cursor).copied() + } +} + +/// Returns one hexadecimal digit value for JSON unicode escapes. +fn hex_value(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index be5d34d3f5..ca393d50c7 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -17,6 +17,7 @@ pub mod context; pub mod errors; pub mod eval_ir; pub mod interpreter; +mod json_validate; pub mod lower; pub mod parser; pub mod runtime_hooks; diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index ba29c93b3f..f4030c6507 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -45,7 +45,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Until eval JSON decoding/validation is bridged to the native JSON error-state runtime, eval `json_last_error()` and `json_last_error_msg()` report the no-error state directly. +Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_validate()` uses a byte-level recursive validator for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks without allocating decoded values. Until eval JSON validation/decoding is bridged to the native JSON error-state runtime, eval `json_last_error()` and `json_last_error_msg()` report the no-error state directly. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6a7c22f42a..07c4aaa615 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -56,7 +56,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_last_error()` and `json_last_error_msg()` currently report `JSON_ERROR_NONE` / `"No error"` because eval JSON decoding and validation are not yet bridged into the native JSON error-state runtime. +Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_validate()` supports zero-flags syntax validation for JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_last_error()` and `json_last_error_msg()` currently report `JSON_ERROR_NONE` / `"No error"` because eval JSON validation failures do not yet bridge into the native JSON error-state runtime. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/examples/eval/main.php b/examples/eval/main.php index 1dff5a3341..41af4b6f31 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -127,6 +127,7 @@ public function __construct(int $value) { $reversed = eval('return strrev("eval");'); $json_encoded = eval('return json_encode(["name" => "Ada", "skills" => ["math", "code"]]);'); $json_status = eval('return json_last_error() . ":" . json_last_error_msg();'); +$json_valid = eval('return (json_validate("[1,2,3]") ? "valid" : "bad") . ":" . (json_validate("[1]", 1) ? "bad" : "depth");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); $positions = eval('return strpos("banana", "na") . ":" . strrpos("banana", "na");'); $substring_from = eval('return strstr("user@example.com", "@");'); @@ -266,6 +267,7 @@ public function __construct(int $value) { echo "reversed=" . $reversed . "\n"; echo "json-encoded=" . $json_encoded . "\n"; echo "json-status=" . $json_status . "\n"; +echo "json-valid=" . $json_valid . "\n"; echo "contains=" . $contains . "\n"; echo "positions=" . $positions . "\n"; echo "strstr=" . $substring_from . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d1015e1659..ee052aaac6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -778,6 +778,22 @@ echo function_exists("json_last_error") && function_exists("json_last_error_msg" assert_eq!(out, "0:No error:0:No error:1"); } +/// Verifies eval `json_validate()` validates JSON syntax, depth, and dynamic calls. +#[test] +fn test_eval_dispatches_json_validate_builtin_call() { + let out = compile_and_run( + r#" "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; +echo function_exists("json_validate");'); +"#, + ); + assert_eq!(out, "Y:N:D:C:A:1"); +} + /// Verifies eval direct builtin calls bind named arguments and spread arrays. #[test] fn test_eval_dispatches_named_and_spread_builtin_calls() { From fcddc18957e0f17b594d21f73c11a5e248446944 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 16:20:00 +0200 Subject: [PATCH 0206/1208] Support eval json_decode builtin --- crates/elephc-eval/src/interpreter.rs | 164 +++++++++++++++++- crates/elephc-eval/src/json_validate.rs | 210 ++++++++++++++---------- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 22 +++ 6 files changed, 312 insertions(+), 92 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7e32055617..0cdd4a9e77 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -17,7 +17,7 @@ use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; -use crate::json_validate; +use crate::json_validate::{self, JsonValue}; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; @@ -1518,6 +1518,7 @@ fn eval_positional_expr_call( eval_builtin_type_predicate(name, args, context, scope, values) } "ip2long" => eval_builtin_ip2long(args, context, scope, values), + "json_decode" => eval_builtin_json_decode(args, context, scope, values), "json_encode" => eval_builtin_json_encode(args, context, scope, values), "json_last_error" => eval_builtin_json_last_error(args, values), "json_last_error_msg" => eval_builtin_json_last_error_msg(args, values), @@ -1979,6 +1980,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_real" | "is_resource" | "is_string" + | "json_decode" | "json_encode" | "json_last_error" | "json_last_error_msg" @@ -2230,6 +2232,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "inet_pton" => Some(&["ip"]), "intdiv" => Some(&["num1", "num2"]), "ip2long" => Some(&["ip"]), + "json_decode" => Some(&["json", "associative", "depth", "flags"]), "json_encode" => Some(&["value", "flags", "depth"]), "json_last_error" | "json_last_error_msg" => Some(&[]), "json_validate" => Some(&["json", "depth", "flags"]), @@ -2996,6 +2999,23 @@ fn eval_builtin_with_values( values.bool_value(eval_function_probe_exists(context, &name))? } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "json_decode" => match evaluated_args { + [json] => eval_json_decode_result(*json, None, None, None, values)?, + [json, associative] => { + eval_json_decode_result(*json, Some(*associative), None, None, values)? + } + [json, associative, depth] => { + eval_json_decode_result(*json, Some(*associative), Some(*depth), None, values)? + } + [json, associative, depth, flags] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + Some(*flags), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "json_encode" => match evaluated_args { [value] => eval_json_encode_result(*value, None, None, values)?, [value, flags] => eval_json_encode_result(*value, Some(*flags), None, values)?, @@ -10311,6 +10331,121 @@ fn eval_json_encode_result( values.string_bytes_value(&output) } +/// Evaluates PHP `json_decode()` for zero-flag scalar, array, and object JSON text. +fn eval_builtin_json_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [json] => { + let json = eval_expr(json, context, scope, values)?; + eval_json_decode_result(json, None, None, None, values) + } + [json, associative] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + eval_json_decode_result(json, Some(associative), None, None, values) + } + [json, associative, depth] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_decode_result(json, Some(associative), Some(depth), None, values) + } + [json, associative, depth, flags] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_decode_result(json, Some(associative), Some(depth), Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Decodes one JSON string into eval runtime cells, returning null on syntax/depth failure. +fn eval_json_decode_result( + json: RuntimeCellHandle, + associative: Option, + depth: Option, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0) + != 0 + { + return Err(EvalStatus::UnsupportedConstruct); + } + if let Some(associative) = associative { + let _ = values.truthy(associative)?; + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let bytes = values.string_bytes(json)?; + let Some(decoded) = json_validate::decode(&bytes, depth as usize) else { + return values.null(); + }; + eval_json_decode_to_cell(decoded, values) +} + +/// Materializes one parsed JSON value as an eval runtime cell. +fn eval_json_decode_to_cell( + value: JsonValue, + values: &mut impl RuntimeValueOps, +) -> Result { + match value { + JsonValue::Null => values.null(), + JsonValue::Bool(value) => values.bool_value(value), + JsonValue::Number(value) => eval_json_decode_number_to_cell(&value, values), + JsonValue::String(value) => values.string_bytes_value(&value), + JsonValue::Array(elements) => { + let mut result = values.array_new(elements.len())?; + for (index, element) in elements.into_iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let element = eval_json_decode_to_cell(element, values)?; + result = values.array_set(result, key, element)?; + } + Ok(result) + } + JsonValue::Object(entries) => { + let mut result = values.assoc_new(entries.len())?; + for (key, value) in entries { + let key = values.string_bytes_value(&key)?; + let value = eval_json_decode_to_cell(value, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) + } + } +} + +/// Materializes one JSON number as an int when possible and as a float otherwise. +fn eval_json_decode_number_to_cell( + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; + if !value.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E')) { + if let Ok(integer) = value.parse::() { + return values.int(integer); + } + } + let float = value.parse::().map_err(|_| EvalStatus::RuntimeFatal)?; + values.float(float) +} + /// Evaluates PHP `json_last_error()` with the eval interpreter's current no-error state. fn eval_builtin_json_last_error( args: &[EvalExpr], @@ -13505,6 +13640,33 @@ return function_exists("json_encode");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `json_decode()` materializes scalars, arrays, and associative arrays. + #[test] + fn execute_program_dispatches_json_decode_builtin() { + let program = parse_fragment( + br#"echo json_decode("\"hello\"") . ":"; +echo json_decode("42") . ":"; +echo (json_decode("true") ? "T" : "bad") . ":"; +echo (is_null(json_decode("null")) ? "NULL" : "bad") . ":"; +$decoded = json_decode("{\"a\":1,\"b\":[\"x\",false]}", true); +echo $decoded["a"] . ":" . $decoded["b"][0] . ":" . ($decoded["b"][1] ? "bad" : "F") . ":"; +$call = call_user_func("json_decode", "[3,4]"); +echo $call[1] . ":"; +$named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); +echo $named["k"] . ":"; +echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; +return function_exists("json_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hello:42:T:NULL:1:x:F:4:v:BAD:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `json_last_error()` reports the no-error JSON status. #[test] fn execute_program_dispatches_json_last_error_builtins() { diff --git a/crates/elephc-eval/src/json_validate.rs b/crates/elephc-eval/src/json_validate.rs index 49754e4f56..6cfc00b31c 100644 --- a/crates/elephc-eval/src/json_validate.rs +++ b/crates/elephc-eval/src/json_validate.rs @@ -1,31 +1,46 @@ //! Purpose: -//! Validates JSON byte streams for eval-side `json_validate()` calls. -//! The validator checks syntax and depth without allocating decoded PHP values. +//! Parses JSON byte streams for eval-side `json_validate()` and `json_decode()`. +//! The parser checks syntax and can return a small JSON tree for runtime-cell materialization. //! //! Called from: -//! - `crate::interpreter` when dispatching eval `json_validate()`. +//! - `crate::interpreter` when dispatching eval JSON builtins. //! //! Key details: //! - Container depth follows PHP decode/validate semantics: entering a container //! is rejected when the active depth would reach the requested limit. -//! - String validation accepts JSON escapes, paired UTF-16 surrogate escapes, -//! and raw UTF-8 bytes while rejecting control bytes and malformed UTF-8. +//! - String parsing accepts JSON escapes, paired UTF-16 surrogate escapes, and raw +//! UTF-8 bytes while rejecting control bytes and malformed UTF-8. + +/// Parsed JSON value used by eval JSON builtins before runtime-cell allocation. +pub(crate) enum JsonValue { + Null, + Bool(bool), + Number(Vec), + String(Vec), + Array(Vec), + Object(Vec<(Vec, JsonValue)>), +} /// Returns whether one byte slice is a complete JSON document within the depth limit. pub(crate) fn bytes(bytes: &[u8], depth_limit: usize) -> bool { - let mut parser = Validator::new(bytes, depth_limit); + decode(bytes, depth_limit).is_some() +} + +/// Parses one complete JSON document into an eval-side JSON value. +pub(crate) fn decode(bytes: &[u8], depth_limit: usize) -> Option { + let mut parser = Parser::new(bytes, depth_limit); parser.parse_document() } -/// Cursor-based JSON validator for eval `json_validate()` calls. -struct Validator<'a> { +/// Cursor-based JSON parser for eval JSON builtin calls. +struct Parser<'a> { bytes: &'a [u8], cursor: usize, depth_limit: usize, } -impl<'a> Validator<'a> { - /// Creates a JSON validator over one immutable byte slice. +impl<'a> Parser<'a> { + /// Creates a JSON parser over one immutable byte slice. fn new(bytes: &'a [u8], depth_limit: usize) -> Self { Self { bytes, @@ -35,147 +50,157 @@ impl<'a> Validator<'a> { } /// Parses one complete JSON document and rejects trailing non-whitespace bytes. - fn parse_document(&mut self) -> bool { + fn parse_document(&mut self) -> Option { self.skip_ws(); - if !self.parse_value(0) { - return false; - } + let value = self.parse_value(0)?; self.skip_ws(); - self.cursor == self.bytes.len() + (self.cursor == self.bytes.len()).then_some(value) } /// Parses any JSON value at the given active container depth. - fn parse_value(&mut self, depth: usize) -> bool { + fn parse_value(&mut self, depth: usize) -> Option { self.skip_ws(); - match self.peek() { - Some(b'n') => self.consume_literal(b"null"), - Some(b't') => self.consume_literal(b"true"), - Some(b'f') => self.consume_literal(b"false"), - Some(b'"') => self.parse_string(), - Some(b'[') => self.parse_array(depth), - Some(b'{') => self.parse_object(depth), - Some(b'-' | b'0'..=b'9') => self.parse_number(), - _ => false, + match self.peek()? { + b'n' => self.consume_literal(b"null").then_some(JsonValue::Null), + b't' => self.consume_literal(b"true").then_some(JsonValue::Bool(true)), + b'f' => self.consume_literal(b"false").then_some(JsonValue::Bool(false)), + b'"' => self.parse_string().map(JsonValue::String), + b'[' => self.parse_array(depth), + b'{' => self.parse_object(depth), + b'-' | b'0'..=b'9' => self.parse_number().map(JsonValue::Number), + _ => None, } } /// Parses a JSON array and enforces PHP's validate/decode depth threshold. - fn parse_array(&mut self, depth: usize) -> bool { + fn parse_array(&mut self, depth: usize) -> Option { if depth + 1 >= self.depth_limit { - return false; + return None; } self.cursor += 1; self.skip_ws(); + let mut elements = Vec::new(); if self.consume_byte(b']') { - return true; + return Some(JsonValue::Array(elements)); } loop { - if !self.parse_value(depth + 1) { - return false; - } + elements.push(self.parse_value(depth + 1)?); self.skip_ws(); if self.consume_byte(b']') { - return true; + return Some(JsonValue::Array(elements)); } if !self.consume_byte(b',') { - return false; + return None; } } } /// Parses a JSON object and enforces PHP's validate/decode depth threshold. - fn parse_object(&mut self, depth: usize) -> bool { + fn parse_object(&mut self, depth: usize) -> Option { if depth + 1 >= self.depth_limit { - return false; + return None; } self.cursor += 1; self.skip_ws(); + let mut entries = Vec::new(); if self.consume_byte(b'}') { - return true; + return Some(JsonValue::Object(entries)); } loop { self.skip_ws(); - if !self.parse_string() { - return false; - } + let key = self.parse_string()?; self.skip_ws(); if !self.consume_byte(b':') { - return false; - } - if !self.parse_value(depth + 1) { - return false; + return None; } + entries.push((key, self.parse_value(depth + 1)?)); self.skip_ws(); if self.consume_byte(b'}') { - return true; + return Some(JsonValue::Object(entries)); } if !self.consume_byte(b',') { - return false; + return None; } } } - /// Parses a JSON string, including escapes, UTF-8 bytes, and surrogate-pair escapes. - fn parse_string(&mut self) -> bool { + /// Parses a JSON string into UTF-8 bytes after applying JSON escapes. + fn parse_string(&mut self) -> Option> { if !self.consume_byte(b'"') { - return false; + return None; } + let mut output = Vec::new(); while let Some(byte) = self.peek() { match byte { b'"' => { self.cursor += 1; - return true; + return Some(output); } b'\\' => { - if !self.parse_string_escape() { - return false; - } + self.parse_string_escape(&mut output)?; + } + 0x00..=0x1f => return None, + 0x00..=0x7f => { + output.push(byte); + self.cursor += 1; } - 0x00..=0x1f => return false, - 0x00..=0x7f => self.cursor += 1, _ => { - if !self.consume_utf8_char() { - return false; - } + let start = self.cursor; + self.consume_utf8_char()?; + output.extend_from_slice(&self.bytes[start..self.cursor]); } } } - false + None } /// Parses one JSON string escape sequence at the current backslash. - fn parse_string_escape(&mut self) -> bool { + fn parse_string_escape(&mut self, output: &mut Vec) -> Option<()> { self.cursor += 1; - match self.peek() { - Some(b'"' | b'\\' | b'/' | b'b' | b'f' | b'n' | b'r' | b't') => { - self.cursor += 1; - true + match self.peek()? { + b'"' => output.push(b'"'), + b'\\' => output.push(b'\\'), + b'/' => output.push(b'/'), + b'b' => output.push(0x08), + b'f' => output.push(0x0c), + b'n' => output.push(b'\n'), + b'r' => output.push(b'\r'), + b't' => output.push(b'\t'), + b'u' => { + self.parse_unicode_escape(output)?; + return Some(()); } - Some(b'u') => self.parse_unicode_escape(), - _ => false, + _ => return None, } + self.cursor += 1; + Some(()) } /// Parses one JSON `\uXXXX` escape, including mandatory surrogate pairs. - fn parse_unicode_escape(&mut self) -> bool { - let Some(unit) = self.parse_unicode_unit() else { - return false; - }; + fn parse_unicode_escape(&mut self, output: &mut Vec) -> Option<()> { + let unit = self.parse_unicode_unit()?; if (0xd800..=0xdbff).contains(&unit) { let checkpoint = self.cursor; if !self.consume_byte(b'\\') || !self.consume_byte(b'u') { - return false; + return None; } let Some(low) = self.parse_unicode_unit_after_u() else { self.cursor = checkpoint; - return false; + return None; }; - (0xdc00..=0xdfff).contains(&low) + if !(0xdc00..=0xdfff).contains(&low) { + return None; + } + let high = u32::from(unit - 0xd800); + let low = u32::from(low - 0xdc00); + append_codepoint(output, 0x10000 + ((high << 10) | low)) + } else if (0xdc00..=0xdfff).contains(&unit) { + None } else { - !(0xdc00..=0xdfff).contains(&unit) + append_codepoint(output, u32::from(unit)) } } @@ -202,30 +227,31 @@ impl<'a> Validator<'a> { } /// Parses a JSON number with RFC-compatible leading-zero, fraction, and exponent rules. - fn parse_number(&mut self) -> bool { + fn parse_number(&mut self) -> Option> { + let start = self.cursor; if self.consume_byte(b'-') && self.peek().is_none() { - return false; + return None; } - match self.peek() { - Some(b'0') => { + match self.peek()? { + b'0' => { self.cursor += 1; if matches!(self.peek(), Some(b'0'..=b'9')) { - return false; + return None; } } - Some(b'1'..=b'9') => { + b'1'..=b'9' => { self.cursor += 1; while matches!(self.peek(), Some(b'0'..=b'9')) { self.cursor += 1; } } - _ => return false, + _ => return None, } if self.consume_byte(b'.') { if !matches!(self.peek(), Some(b'0'..=b'9')) { - return false; + return None; } while matches!(self.peek(), Some(b'0'..=b'9')) { self.cursor += 1; @@ -238,14 +264,14 @@ impl<'a> Validator<'a> { self.cursor += 1; } if !matches!(self.peek(), Some(b'0'..=b'9')) { - return false; + return None; } while matches!(self.peek(), Some(b'0'..=b'9')) { self.cursor += 1; } } - true + Some(self.bytes[start..self.cursor].to_vec()) } /// Consumes exactly one expected byte when it is present. @@ -269,23 +295,23 @@ impl<'a> Validator<'a> { } /// Consumes one valid UTF-8 codepoint from a raw JSON string segment. - fn consume_utf8_char(&mut self) -> bool { + fn consume_utf8_char(&mut self) -> Option<()> { let first = self.bytes[self.cursor]; let width = match first { 0xc2..=0xdf => 2, 0xe0..=0xef => 3, 0xf0..=0xf4 => 4, - _ => return false, + _ => return None, }; if self.cursor + width > self.bytes.len() { - return false; + return None; } let slice = &self.bytes[self.cursor..self.cursor + width]; if std::str::from_utf8(slice).is_err() { - return false; + return None; } self.cursor += width; - true + Some(()) } /// Skips JSON whitespace accepted between tokens. @@ -301,6 +327,14 @@ impl<'a> Validator<'a> { } } +/// Appends one Unicode codepoint to a decoded JSON string. +fn append_codepoint(output: &mut Vec, codepoint: u32) -> Option<()> { + let ch = char::from_u32(codepoint)?; + let mut buffer = [0_u8; 4]; + output.extend_from_slice(ch.encode_utf8(&mut buffer).as_bytes()); + Some(()) +} + /// Returns one hexadecimal digit value for JSON unicode escapes. fn hex_value(byte: u8) -> Option { match byte { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index f4030c6507..257d9d1cf1 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -45,7 +45,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_validate()` uses a byte-level recursive validator for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks without allocating decoded values. Until eval JSON validation/decoding is bridged to the native JSON error-state runtime, eval `json_last_error()` and `json_last_error_msg()` report the no-error state directly. +Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. Until eval JSON validation/decoding is bridged to the native JSON error-state runtime, eval `json_last_error()` and `json_last_error_msg()` report the no-error state directly. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 07c4aaa615..49c55394f5 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -56,7 +56,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_validate()` supports zero-flags syntax validation for JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_last_error()` and `json_last_error_msg()` currently report `JSON_ERROR_NONE` / `"No error"` because eval JSON validation failures do not yet bridge into the native JSON error-state runtime. +Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` currently report `JSON_ERROR_NONE` / `"No error"` because eval JSON validation/decode failures do not yet bridge into the native JSON error-state runtime. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/examples/eval/main.php b/examples/eval/main.php index 41af4b6f31..36735e5910 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -126,6 +126,7 @@ public function __construct(int $value) { $word_case = eval('return ucwords("hello eval");'); $reversed = eval('return strrev("eval");'); $json_encoded = eval('return json_encode(["name" => "Ada", "skills" => ["math", "code"]]);'); +$json_decoded = eval('$decoded = json_decode("{\"ok\":true,\"items\":[1,2]}", true); return ($decoded["ok"] ? "ok" : "bad") . ":" . $decoded["items"][1];'); $json_status = eval('return json_last_error() . ":" . json_last_error_msg();'); $json_valid = eval('return (json_validate("[1,2,3]") ? "valid" : "bad") . ":" . (json_validate("[1]", 1) ? "bad" : "depth");'); $contains = eval('return str_contains("dynamic eval", "eval") ? "contains" : "missing";'); @@ -266,6 +267,7 @@ public function __construct(int $value) { echo "ucwords=" . $word_case . "\n"; echo "reversed=" . $reversed . "\n"; echo "json-encoded=" . $json_encoded . "\n"; +echo "json-decoded=" . $json_decoded . "\n"; echo "json-status=" . $json_status . "\n"; echo "json-valid=" . $json_valid . "\n"; echo "contains=" . $contains . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ee052aaac6..d2c13e37b7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -764,6 +764,28 @@ echo function_exists("json_encode");'); assert_eq!(out, r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:1"#); } +/// Verifies eval `json_decode()` materializes scalar, indexed, and associative values. +#[test] +fn test_eval_dispatches_json_decode_builtin_call() { + let out = compile_and_run( + r#" "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); +echo $named["k"] . ":"; +echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; +echo function_exists("json_decode");'); +"#, + ); + assert_eq!(out, "hello:42:T:NULL:1:x:F:4:v:BAD:1"); +} + /// Verifies eval `json_last_error()` and `json_last_error_msg()` report no-error status. #[test] fn test_eval_dispatches_json_last_error_builtin_calls() { From bf38771ac90be08ee4da1093f80b5fb31f960fac Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 16:28:35 +0200 Subject: [PATCH 0207/1208] Support eval array pop shift builtins --- crates/elephc-eval/src/interpreter.rs | 214 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 2 + examples/eval/main.php | 2 + tests/codegen/eval.rs | 19 +++ 5 files changed, 237 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0cdd4a9e77..18a80910e6 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1356,6 +1356,9 @@ fn eval_call( let args = positional_call_arg_exprs(args)?; return eval_positional_expr_call(name, &args, context, scope, values); } + if matches!(name, "array_pop" | "array_shift") { + return eval_builtin_array_pop_shift_call(name, args, context, scope, values); + } if eval_php_visible_builtin_exists(name) { if eval_call_args_are_plain_positional(args) { let args = positional_call_arg_exprs(args)?; @@ -1861,10 +1864,12 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_intersect_key" | "array_merge" | "array_pad" + | "array_pop" | "array_product" | "array_rand" | "array_reverse" | "array_search" + | "array_shift" | "array_slice" | "array_sum" | "array_unique" @@ -2164,8 +2169,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_map" => Some(&["callback", "array"]), "array_reduce" => Some(&["array", "callback", "initial"]), "array_walk" => Some(&["array", "callback"]), - "array_flip" | "array_keys" | "array_product" | "array_sum" | "array_unique" - | "array_rand" | "array_values" => Some(&["array"]), + "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" + | "array_sum" | "array_unique" | "array_rand" | "array_values" => Some(&["array"]), "array_key_exists" => Some(&["key", "array"]), "array_pad" => Some(&["array", "length", "value"]), "array_reverse" => Some(&["array", "preserve_keys"]), @@ -2496,6 +2501,15 @@ fn eval_builtin_with_values( }; eval_array_walk_result(*array, *callback, context, values)? } + "array_pop" | "array_shift" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_pop_shift_value_result(name, *array, values)? + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3734,6 +3748,171 @@ fn eval_array_walk_result( values.bool_value(true) } +/// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. +fn eval_builtin_array_pop_shift_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(var_name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + let Some(entry) = scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; + for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. +fn eval_array_pop_shift_value_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + if len == 0 { + return values.null(); + } + let position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let key = values.array_iter_key(array, position)?; + values.array_get(array, key) +} + +/// Builds the return value plus replacement array for direct pop/shift write-back. +fn eval_array_pop_shift_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let len = values.array_len(array)?; + let tag = values.type_tag(array)?; + if len == 0 { + let replacement = match tag { + EVAL_TAG_ARRAY => values.array_new(0)?, + EVAL_TAG_ASSOC => values.assoc_new(0)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + return Ok((values.null()?, replacement)); + } + + let removed_position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let removed_key = values.array_iter_key(array, removed_position)?; + let removed_value = values.array_get(array, removed_key)?; + let replacement = match tag { + EVAL_TAG_ARRAY => { + eval_array_pop_shift_indexed_replacement(array, removed_position, len, values)? + } + EVAL_TAG_ASSOC => { + eval_array_pop_shift_assoc_replacement(name, array, removed_position, len, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + Ok((removed_value, replacement)) +} + +/// Rebuilds an indexed array after removing one position and reindexing values. +fn eval_array_pop_shift_indexed_replacement( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(len.saturating_sub(1))?; + let mut target = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after pop/shift, preserving PHP key behavior. +fn eval_array_pop_shift_assoc_replacement( + name: &str, + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "array_shift" && eval_array_remaining_keys_are_int(array, removed_position, len, values)? { + return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); + } + + let mut result = values.assoc_new(len.saturating_sub(1))?; + let mut next_int_key = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every remaining key is an integer after removing one element. +fn eval_array_remaining_keys_are_int( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if position == removed_position { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + /// Evaluates PHP `array_filter()` for null and string-callback filtering modes. fn eval_builtin_array_filter( args: &[EvalExpr], @@ -13852,6 +14031,37 @@ return function_exists("array_walk");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_pop()` and `array_shift()` write back only for direct variable calls. + #[test] + fn execute_program_dispatches_array_pop_shift_builtins() { + let program = parse_fragment( + br#"$a = [1, 2, 3]; +echo array_pop($a) . ":" . count($a) . ":" . $a[1] . ":"; +$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; +$c = [4, 5]; +echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; +$d = [6, 7]; +echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; +return function_exists("array_pop") && function_exists("array_shift");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:2:2:1:2:3:4:5:2:5:6:2:6:"); + assert_eq!( + values.warnings, + vec![ + "array_pop(): Argument #1 ($array) must be passed by reference, value given", + "array_shift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 257d9d1cf1..0e155169c6 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -53,6 +53,8 @@ Eval `array_reduce()` reads the provided initial carry or creates a `null` carry Eval `array_walk()` iterates keys through the eval value hooks, invokes the shared eval callable dispatcher with value and key cells, discards callback results, and returns a boxed `true` cell. +Eval `array_pop()` and `array_shift()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. + Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 49c55394f5..ecc9403180 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -64,6 +64,8 @@ Eval `array_reduce()` supports the two- or three-argument form with string callb Eval `array_walk()` supports the two-argument form with string callbacks. It invokes the callback with value and key cells in PHP iteration order, ignores the callback return value, and returns `true`. +Eval `array_pop()` and `array_shift()` support direct variable calls with write-back into the eval scope, including named `array:` calls. When reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the returned value is computed from the supplied array, but the caller's original array is not mutated. + Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/examples/eval/main.php b/examples/eval/main.php index 36735e5910..135d029d6f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -139,6 +139,7 @@ public function __construct(int $value) { $array_map = eval('function eval_example_double($value) { return $value * 2; } function eval_example_pair($left, $right) { return $left . $right; } $mapped = array_map("eval_example_double", ["a" => 2, "b" => 3]); $pairs = array_map("eval_example_pair", ["x"], ["y"]); return $mapped["a"] . ":" . $mapped["b"] . ":" . $pairs[0];'); $array_reduce = eval('function eval_example_sum($carry, $item) { return $carry + $item; } return array_reduce([1, 2, 3], "eval_example_sum", 10) . ":" . array_reduce([4, 5], "eval_example_sum");'); $array_walk = eval('function eval_example_walk($value, $key) { echo "walk-" . $key . "=" . $value . "\n"; } return array_walk(["a" => 2, "b" => 3], "eval_example_walk") ? "ok" : "bad";'); +$array_pop_shift = eval('$items = ["first", "middle", "last"]; $popped = array_pop($items); $shifted = array_shift($items); return $popped . ":" . $shifted . ":" . count($items) . ":" . $items[0];'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -280,6 +281,7 @@ public function __construct(int $value) { echo "array-map=" . $array_map . "\n"; echo "array-reduce=" . $array_reduce . "\n"; echo "array-walk=" . $array_walk . "\n"; +echo "array-pop-shift=" . $array_pop_shift . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d2c13e37b7..e4275b2840 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -929,6 +929,25 @@ echo function_exists("array_walk");'); assert_eq!(out, "a=2;b=3;T:0=4;1=5;z=6;1"); } +/// Verifies eval `array_pop()` and `array_shift()` mutate direct variable arguments only. +#[test] +fn test_eval_dispatches_array_pop_shift_builtin_calls() { + let out = compile_and_run( + r#" 1, 10 => 2, "y" => 3, 11 => 4]; +echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; +$c = [4, 5]; +echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; +$d = [6, 7]; +echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; +echo function_exists("array_pop") && function_exists("array_shift");'); +"#, + ); + assert_eq!(out, "3:2:2:1:2:3:4:5:2:5:6:2:6:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 918f769d9e77a714c255ca1bdca28c97b263519b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 16:33:03 +0200 Subject: [PATCH 0208/1208] Support eval array push unshift builtins --- crates/elephc-eval/src/interpreter.rs | 250 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 23 +++ 5 files changed, 276 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 18a80910e6..d825002782 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1356,7 +1356,10 @@ fn eval_call( let args = positional_call_arg_exprs(args)?; return eval_positional_expr_call(name, &args, context, scope, values); } - if matches!(name, "array_pop" | "array_shift") { + if matches!( + name, + "array_pop" | "array_push" | "array_shift" | "array_unshift" + ) { return eval_builtin_array_pop_shift_call(name, args, context, scope, values); } if eval_php_visible_builtin_exists(name) { @@ -1866,6 +1869,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_pad" | "array_pop" | "array_product" + | "array_push" | "array_rand" | "array_reverse" | "array_search" @@ -1873,6 +1877,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_slice" | "array_sum" | "array_unique" + | "array_unshift" | "array_values" | "acos" | "asin" @@ -2171,6 +2176,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_walk" => Some(&["array", "callback"]), "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" | "array_sum" | "array_unique" | "array_rand" | "array_values" => Some(&["array"]), + "array_push" | "array_unshift" => Some(&["array", "values"]), "array_key_exists" => Some(&["key", "array"]), "array_pad" => Some(&["array", "length", "value"]), "array_reverse" => Some(&["array", "preserve_keys"]), @@ -2510,6 +2516,15 @@ fn eval_builtin_with_values( ))?; eval_array_pop_shift_value_result(name, *array, values)? } + "array_push" | "array_unshift" => { + let Some((array, inserted)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_push_unshift_count_result(*array, inserted.len(), values)? + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3756,6 +3771,10 @@ fn eval_builtin_array_pop_shift_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { + if matches!(name, "array_push" | "array_unshift") { + return eval_builtin_array_push_unshift_call(name, args, context, scope, values); + } + let [arg] = args else { return Err(EvalStatus::RuntimeFatal); }; @@ -3782,6 +3801,42 @@ fn eval_builtin_array_pop_shift_call( Ok(result) } +/// Evaluates direct by-reference `array_push()` / `array_unshift()` calls. +fn eval_builtin_array_push_unshift_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 || !eval_call_args_are_plain_positional(args) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(var_name) = args[0].value() else { + return Err(EvalStatus::RuntimeFatal); + }; + let mut inserted = Vec::with_capacity(args.len() - 1); + for arg in &args[1..] { + inserted.push(eval_expr(arg.value(), context, scope, values)?); + } + let Some(entry) = scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; + let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; + for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + /// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. fn eval_array_pop_shift_value_result( name: &str, @@ -3913,6 +3968,164 @@ fn eval_array_remaining_keys_are_int( Ok(true) } +/// Returns the resulting element count for by-value push/unshift dynamic calls. +fn eval_array_push_unshift_count_result( + array: RuntimeCellHandle, + inserted_len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let total = values + .array_len(array)? + .checked_add(inserted_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let total = i64::try_from(total).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(total) +} + +/// Builds the replacement array for direct push/unshift write-back. +fn eval_array_push_unshift_replacement( + name: &str, + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, values.type_tag(array)?) { + ("array_push", EVAL_TAG_ARRAY) => { + eval_array_push_indexed_replacement(array, inserted, values) + } + ("array_push", EVAL_TAG_ASSOC) => eval_array_push_assoc_replacement(array, inserted, values), + ("array_unshift", EVAL_TAG_ARRAY) => { + eval_array_unshift_indexed_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ASSOC) => { + eval_array_unshift_assoc_replacement(array, inserted, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Rebuilds an indexed array after appending values. +fn eval_array_push_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, target_key, value)?; + } + for (offset, value) in inserted.iter().copied().enumerate() { + let position = len.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after appending values at PHP's next integer keys. +fn eval_array_push_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_key = 0_i64; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? == EVAL_TAG_INT { + next_key = next_key.max(eval_int_value(key, values)?.saturating_add(1)); + } + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + for value in inserted.iter().copied() { + let key = values.int(next_key)?; + next_key = next_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an indexed array after prepending values and reindexing the original tail. +fn eval_array_unshift_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + let mut target = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after prepending values and reindexing integer keys. +fn eval_array_unshift_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if eval_array_keys_are_int(array, len, values)? { + return eval_array_unshift_indexed_replacement(array, inserted, values); + } + + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_int_key = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(next_int_key)?; + next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in the array is integer-shaped. +fn eval_array_keys_are_int( + array: RuntimeCellHandle, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + /// Evaluates PHP `array_filter()` for null and string-callback filtering modes. fn eval_builtin_array_filter( args: &[EvalExpr], @@ -14062,6 +14275,41 @@ return function_exists("array_pop") && function_exists("array_shift");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_push()` and `array_unshift()` write back direct variable calls. + #[test] + fn execute_program_dispatches_array_push_unshift_builtins() { + let program = parse_fragment( + br#"$a = [1]; +echo array_push($a, 2, 3) . ":" . $a[2] . ":"; +$b = ["x" => 1, 10 => 2]; +echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; +$c = [2, 3]; +echo array_unshift($c, 0, 1) . ":" . $c[0] . ":" . $c[3] . ":"; +$d = ["x" => 1, 10 => 2, "y" => 3]; +echo array_unshift($d, "A") . ":" . $d[0] . ":" . $d["x"] . ":" . $d[1] . ":" . $d["y"] . ":"; +$e = [5]; +echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; +$f = [7]; +echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; +return function_exists("array_push") && function_exists("array_unshift");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:"); + assert_eq!( + values.warnings, + vec![ + "array_push(): Argument #1 ($array) must be passed by reference, value given", + "array_unshift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 0e155169c6..697a0ed452 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -53,7 +53,7 @@ Eval `array_reduce()` reads the provided initial carry or creates a `null` carry Eval `array_walk()` iterates keys through the eval value hooks, invokes the shared eval callable dispatcher with value and key cells, discards callback results, and returns a boxed `true` cell. -Eval `array_pop()` and `array_shift()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. +Eval `array_pop()`, `array_shift()`, `array_push()`, and `array_unshift()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value or resulting push/unshift count without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ecc9403180..0731a549d7 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -64,7 +64,7 @@ Eval `array_reduce()` supports the two- or three-argument form with string callb Eval `array_walk()` supports the two-argument form with string callbacks. It invokes the callback with value and key cells in PHP iteration order, ignores the callback return value, and returns `true`. -Eval `array_pop()` and `array_shift()` support direct variable calls with write-back into the eval scope, including named `array:` calls. When reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the returned value is computed from the supplied array, but the caller's original array is not mutated. +Eval `array_pop()`, `array_shift()`, `array_push()`, and `array_unshift()` support direct variable calls with write-back into the eval scope. `array_pop()` and `array_shift()` also accept named direct `array:` calls. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. diff --git a/examples/eval/main.php b/examples/eval/main.php index 135d029d6f..804c579aaf 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -140,6 +140,7 @@ public function __construct(int $value) { $array_reduce = eval('function eval_example_sum($carry, $item) { return $carry + $item; } return array_reduce([1, 2, 3], "eval_example_sum", 10) . ":" . array_reduce([4, 5], "eval_example_sum");'); $array_walk = eval('function eval_example_walk($value, $key) { echo "walk-" . $key . "=" . $value . "\n"; } return array_walk(["a" => 2, "b" => 3], "eval_example_walk") ? "ok" : "bad";'); $array_pop_shift = eval('$items = ["first", "middle", "last"]; $popped = array_pop($items); $shifted = array_shift($items); return $popped . ":" . $shifted . ":" . count($items) . ":" . $items[0];'); +$array_push_unshift = eval('$items = ["middle"]; array_push($items, "last"); array_unshift($items, "first"); return count($items) . ":" . $items[0] . ":" . $items[2];'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -282,6 +283,7 @@ public function __construct(int $value) { echo "array-reduce=" . $array_reduce . "\n"; echo "array-walk=" . $array_walk . "\n"; echo "array-pop-shift=" . $array_pop_shift . "\n"; +echo "array-push-unshift=" . $array_push_unshift . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e4275b2840..c9be686e08 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -948,6 +948,29 @@ echo function_exists("array_pop") && function_exists("array_shift");'); assert_eq!(out, "3:2:2:1:2:3:4:5:2:5:6:2:6:1"); } +/// Verifies eval `array_push()` and `array_unshift()` mutate direct variable arguments only. +#[test] +fn test_eval_dispatches_array_push_unshift_builtin_calls() { + let out = compile_and_run( + r#" 1, 10 => 2]; +echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; +$c = [2, 3]; +echo array_unshift($c, 0, 1) . ":" . $c[0] . ":" . $c[3] . ":"; +$d = ["x" => 1, 10 => 2, "y" => 3]; +echo array_unshift($d, "A") . ":" . $d[0] . ":" . $d["x"] . ":" . $d[1] . ":" . $d["y"] . ":"; +$e = [5]; +echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; +$f = [7]; +echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; +echo function_exists("array_push") && function_exists("array_unshift");'); +"#, + ); + assert_eq!(out, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 2e3c5fc500130eb7c1fc990c5665027d38b3b3a4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 16:42:36 +0200 Subject: [PATCH 0209/1208] Support eval array_splice builtin --- crates/elephc-eval/src/interpreter.rs | 314 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 23 ++ 5 files changed, 342 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d825002782..c84c361ea2 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1358,7 +1358,7 @@ fn eval_call( } if matches!( name, - "array_pop" | "array_push" | "array_shift" | "array_unshift" + "array_pop" | "array_push" | "array_shift" | "array_splice" | "array_unshift" ) { return eval_builtin_array_pop_shift_call(name, args, context, scope, values); } @@ -1875,6 +1875,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_search" | "array_shift" | "array_slice" + | "array_splice" | "array_sum" | "array_unique" | "array_unshift" @@ -2182,6 +2183,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), "array_slice" => Some(&["array", "offset", "length"]), + "array_splice" => Some(&["array", "offset", "length"]), "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), "atan2" => Some(&["y", "x"]), @@ -2525,6 +2527,21 @@ fn eval_builtin_with_values( ))?; eval_array_push_unshift_count_result(*array, inserted.len(), values)? } + "array_splice" => { + let result = match evaluated_args { + [array, offset] => { + eval_array_splice_value_result(*array, *offset, None, values)? + } + [array, offset, length] => { + eval_array_splice_value_result(*array, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.warning( + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + )?; + result + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3774,6 +3791,9 @@ fn eval_builtin_array_pop_shift_call( if matches!(name, "array_push" | "array_unshift") { return eval_builtin_array_push_unshift_call(name, args, context, scope, values); } + if name == "array_splice" { + return eval_builtin_array_splice_call(args, context, scope, values); + } let [arg] = args else { return Err(EvalStatus::RuntimeFatal); @@ -3837,6 +3857,263 @@ fn eval_builtin_array_push_unshift_call( Ok(result) } +/// Evaluates direct by-reference `array_splice()` calls in eval's supported 2/3-argument form. +fn eval_builtin_array_splice_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array_name, offset, length) = + eval_array_splice_direct_args(args, context, scope, values)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let (removed, replacement) = + eval_array_splice_removed_and_replacement(array, offset, length, values)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(removed) +} + +/// Evaluates and binds direct `array_splice()` arguments while preserving source order. +fn eval_array_splice_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(String, RuntimeCellHandle, Option), EvalStatus> { + let mut array = None; + let mut offset = None; + let mut length = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "offset", + 2 => "length", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + array = Some(name.clone()); + } + "offset" => { + if offset.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + offset = Some(eval_expr(arg.value(), context, scope, values)?); + } + "length" => { + if length.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + length = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, offset, length)) +} + +/// Returns the removed elements that `array_splice()` would produce without mutating the source. +fn eval_array_splice_value_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + eval_array_splice_removed(array, start, end, values) +} + +/// Builds both removed and replacement arrays for direct `array_splice()` write-back. +fn eval_array_splice_removed_and_replacement( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + let removed = eval_array_splice_removed(array, start, end, values)?; + let replacement = eval_array_splice_replacement(array, start, end, values)?; + Ok((removed, replacement)) +} + +/// Converts splice offset and length cells into bounded source positions. +fn eval_array_splice_bounds( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(usize, usize), EvalStatus> { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + Ok((start, end)) +} + +/// Builds the reindexed/string-key-preserving removed array returned by `array_splice()`. +fn eval_array_splice_removed( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = end.saturating_sub(start); + if eval_array_range_keys_are_int(array, start, end, values)? { + let mut result = values.array_new(len)?; + let mut target = 0_i64; + for position in start..end { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(len)?; + let mut next_int_key = 0_i64; + for position in start..end { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Builds the source replacement after removing the requested splice range. +fn eval_array_splice_replacement( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { + let mut result = values.array_new(len.saturating_sub(end.saturating_sub(start)))?; + let mut target = 0_i64; + for position in 0..len { + if (start..end).contains(&position) { + continue; + } + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(len.saturating_sub(end.saturating_sub(start)))?; + let mut next_int_key = 0_i64; + for position in 0..len { + if (start..end).contains(&position) { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in one source position range is integer-shaped. +fn eval_array_range_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in start..end { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Returns true when every key outside the removed splice range is integer-shaped. +fn eval_array_splice_remaining_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if (start..end).contains(&position) { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + /// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. fn eval_array_pop_shift_value_result( name: &str, @@ -14310,6 +14587,41 @@ return function_exists("array_push") && function_exists("array_unshift");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `array_splice()` returns removed values and writes back direct variable calls. + #[test] + fn execute_program_dispatches_array_splice_builtin() { + let program = parse_fragment( + br#"$a = [10, 20, 30, 40]; +$removed = array_splice($a, 1, 2); +echo count($removed) . ":" . $removed[0] . ":" . $removed[1] . ":" . count($a) . ":" . $a[1] . ":"; +$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$cut = array_splice(array: $b, offset: 1, length: 2); +echo $cut[0] . ":" . $cut["y"] . ":" . $b["x"] . ":" . $b[0] . ":"; +$c = [1, 2, 3, 4]; +$tail = call_user_func("array_splice", $c, -2, 1); +echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; +$d = [5, 6, 7]; +$all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); +echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; +return function_exists("array_splice");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:"); + assert_eq!( + values.warnings, + vec![ + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 697a0ed452..c2fed17305 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -53,7 +53,7 @@ Eval `array_reduce()` reads the provided initial carry or creates a `null` carry Eval `array_walk()` iterates keys through the eval value hooks, invokes the shared eval callable dispatcher with value and key cells, discards callback results, and returns a boxed `true` cell. -Eval `array_pop()`, `array_shift()`, `array_push()`, and `array_unshift()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value or resulting push/unshift count without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. +Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order; the splice path supports elephc's two- or three-argument form and returns the removed array while reindexing integer keys and preserving string keys. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value, resulting push/unshift count, or removed splice array without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 0731a549d7..6679358cc9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -64,7 +64,7 @@ Eval `array_reduce()` supports the two- or three-argument form with string callb Eval `array_walk()` supports the two-argument form with string callbacks. It invokes the callback with value and key cells in PHP iteration order, ignores the callback return value, and returns `true`. -Eval `array_pop()`, `array_shift()`, `array_push()`, and `array_unshift()` support direct variable calls with write-back into the eval scope. `array_pop()` and `array_shift()` also accept named direct `array:` calls. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. +Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` support direct variable calls with write-back into the eval scope. `array_pop()`, `array_shift()`, and `array_splice()` also accept named direct `array:` calls. Eval `array_splice()` supports the native elephc two- or three-argument form, returns the removed elements, reindexes integer keys, preserves string keys, and does not yet accept the replacement argument. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. diff --git a/examples/eval/main.php b/examples/eval/main.php index 804c579aaf..78600612da 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -141,6 +141,7 @@ public function __construct(int $value) { $array_walk = eval('function eval_example_walk($value, $key) { echo "walk-" . $key . "=" . $value . "\n"; } return array_walk(["a" => 2, "b" => 3], "eval_example_walk") ? "ok" : "bad";'); $array_pop_shift = eval('$items = ["first", "middle", "last"]; $popped = array_pop($items); $shifted = array_shift($items); return $popped . ":" . $shifted . ":" . count($items) . ":" . $items[0];'); $array_push_unshift = eval('$items = ["middle"]; array_push($items, "last"); array_unshift($items, "first"); return count($items) . ":" . $items[0] . ":" . $items[2];'); +$array_splice = eval('$items = [10, 20, 30, 40]; $removed = array_splice($items, 1, 2); return count($removed) . ":" . $removed[0] . ":" . count($items) . ":" . $items[1];'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -284,6 +285,7 @@ public function __construct(int $value) { echo "array-walk=" . $array_walk . "\n"; echo "array-pop-shift=" . $array_pop_shift . "\n"; echo "array-push-unshift=" . $array_push_unshift . "\n"; +echo "array-splice=" . $array_splice . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c9be686e08..63d407e4c5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -971,6 +971,29 @@ echo function_exists("array_push") && function_exists("array_unshift");'); assert_eq!(out, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:1"); } +/// Verifies eval `array_splice()` mutates direct variable arguments only. +#[test] +fn test_eval_dispatches_array_splice_builtin_call() { + let out = compile_and_run( + r#" 1, 10 => 2, "y" => 3, 11 => 4]; +$cut = array_splice(array: $b, offset: 1, length: 2); +echo $cut[0] . ":" . $cut["y"] . ":" . $b["x"] . ":" . $b[0] . ":"; +$c = [1, 2, 3, 4]; +$tail = call_user_func("array_splice", $c, -2, 1); +echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; +$d = [5, 6, 7]; +$all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); +echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; +echo function_exists("array_splice");'); +"#, + ); + assert_eq!(out, "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 320ef92f5e0730a7c6351dd6f4de1bd4cbf9289d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 16:48:58 +0200 Subject: [PATCH 0210/1208] Support eval sort builtins --- crates/elephc-eval/src/interpreter.rs | 211 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 22 +++ 5 files changed, 239 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c84c361ea2..7410801fa4 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1358,7 +1358,13 @@ fn eval_call( } if matches!( name, - "array_pop" | "array_push" | "array_shift" | "array_splice" | "array_unshift" + "array_pop" + | "array_push" + | "array_shift" + | "array_splice" + | "array_unshift" + | "rsort" + | "sort" ) { return eval_builtin_array_pop_shift_call(name, args, context, scope, values); } @@ -2030,6 +2036,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "realpath_cache_get" | "realpath_cache_size" | "rename" + | "rsort" | "rtrim" | "round" | "rmdir" @@ -2038,6 +2045,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "sha1" | "sin" | "sinh" + | "sort" | "sqrt" | "spl_classes" | "sprintf" @@ -2176,7 +2184,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_reduce" => Some(&["array", "callback", "initial"]), "array_walk" => Some(&["array", "callback"]), "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" - | "array_sum" | "array_unique" | "array_rand" | "array_values" => Some(&["array"]), + | "array_sum" | "array_unique" | "array_rand" | "array_values" | "rsort" | "sort" => { + Some(&["array"]) + } "array_push" | "array_unshift" => Some(&["array", "values"]), "array_key_exists" => Some(&["key", "array"]), "array_pad" => Some(&["array", "length", "value"]), @@ -2542,6 +2552,15 @@ fn eval_builtin_with_values( )?; result } + "rsort" | "sort" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_sort_value_result(*array, values)? + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3794,6 +3813,9 @@ fn eval_builtin_array_pop_shift_call( if name == "array_splice" { return eval_builtin_array_splice_call(args, context, scope, values); } + if matches!(name, "sort" | "rsort") { + return eval_builtin_array_sort_call(name, args, context, scope, values); + } let [arg] = args else { return Err(EvalStatus::RuntimeFatal); @@ -3885,6 +3907,157 @@ fn eval_builtin_array_splice_call( Ok(removed) } +/// Evaluates direct by-reference `sort()` / `rsort()` calls and writes back the array. +fn eval_builtin_array_sort_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array_name = eval_array_sort_direct_arg(args)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_array_sort_replacement(name, array, values)?; + let result = values.bool_value(true)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Extracts the direct variable argument accepted by `sort()` / `rsort()`. +fn eval_array_sort_direct_arg(args: &[EvalCallArg]) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + Ok(name.clone()) +} + +/// Returns the dynamic callable result for by-value `sort()` / `rsort()` calls. +fn eval_array_sort_value_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true) +} + +/// Sort key shape supported by eval's homogeneous `sort()` / `rsort()` implementation. +#[derive(Clone)] +enum EvalArraySortKey { + Numeric(f64), + String(Vec), +} + +/// Builds the sorted, reindexed replacement array for `sort()` / `rsort()`. +fn eval_array_sort_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut entries = eval_array_sort_entries(array, values)?; + entries.sort_by(|left, right| { + let order = eval_array_sort_key_cmp(&left.0, &right.0); + if name == "rsort" { + order.reverse() + } else { + order + } + }); + + let mut result = values.array_new(entries.len())?; + for (index, (_, value)) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Collects values and comparable sort keys from one eval array. +fn eval_array_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(value, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push((sort_key, value)); + } + + Ok(entries) +} + +/// Converts one scalar eval value into a homogeneous sort key. +fn eval_array_sort_key( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => Ok(EvalArraySortKey::Numeric(eval_float_value( + value, values, + )?)), + EVAL_TAG_STRING => { + let bytes = values.string_bytes(value)?; + match eval_array_numeric_string_sort_key(&bytes) { + Some(value) => Ok(EvalArraySortKey::Numeric(value)), + None => Ok(EvalArraySortKey::String(bytes)), + } + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Parses one PHP numeric string into the numeric sort domain when possible. +fn eval_array_numeric_string_sort_key(bytes: &[u8]) -> Option { + if !eval_is_numeric_string(bytes) { + return None; + } + std::str::from_utf8(bytes).ok()?.parse::().ok() +} + +/// Compares two precomputed eval sort keys. +fn eval_array_sort_key_cmp( + left: &EvalArraySortKey, + right: &EvalArraySortKey, +) -> std::cmp::Ordering { + match (left, right) { + (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { + left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) + } + (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), + _ => std::cmp::Ordering::Equal, + } +} + /// Evaluates and binds direct `array_splice()` arguments while preserving source order. fn eval_array_splice_direct_args( args: &[EvalCallArg], @@ -14622,6 +14795,40 @@ return function_exists("array_splice");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `sort()` and `rsort()` reindex direct variable arrays only. + #[test] + fn execute_program_dispatches_sort_builtins() { + let program = parse_fragment( + br#"$a = [3, 1, 2]; +echo sort($a) . ":" . $a[0] . $a[1] . $a[2] . ":"; +$b = ["banana", "apple", "cherry"]; +echo rsort(array: $b) . ":" . $b[0] . ":" . $b[2] . ":"; +$c = ["x" => 3, "y" => 1, "z" => 2]; +sort($c); +echo $c[0] . $c[1] . $c[2] . ":"; +$d = [3, 1, 2]; +echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; +$e = [1, 2, 3]; +echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; +return function_exists("sort") && function_exists("rsort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:123:1:cherry:apple:123:1:312:1:1:3:"); + assert_eq!( + values.warnings, + vec![ + "sort(): Argument #1 ($array) must be passed by reference, value given", + "rsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c2fed17305..af5a96b461 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -55,6 +55,8 @@ Eval `array_walk()` iterates keys through the eval value hooks, invokes the shar Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order; the splice path supports elephc's two- or three-argument form and returns the removed array while reindexing integer keys and preserving string keys. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value, resulting push/unshift count, or removed splice array without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. +Eval `sort()` and `rsort()` reuse the same direct-variable write-back path. The interpreter snapshots array values through `RuntimeValueOps`, builds homogeneous numeric or byte-string sort keys, reconstructs an indexed replacement array, and returns `true`; dynamic callable dispatch validates the array and reports PHP's by-reference warning without mutating the original caller value. + Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6679358cc9..5e9af632b4 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -66,6 +66,8 @@ Eval `array_walk()` supports the two-argument form with string callbacks. It inv Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` support direct variable calls with write-back into the eval scope. `array_pop()`, `array_shift()`, and `array_splice()` also accept named direct `array:` calls. Eval `array_splice()` supports the native elephc two- or three-argument form, returns the removed elements, reindexes integer keys, preserves string keys, and does not yet accept the replacement argument. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. +Eval `sort()` and `rsort()` support one-argument direct variable calls with write-back into the eval scope for homogeneous numeric or string arrays. They reindex integer keys, return `true`, accept named direct `array:` calls, and emit PHP's by-reference warning without mutating the caller's original array when reached through `call_user_func()` or `call_user_func_array()`. + Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/examples/eval/main.php b/examples/eval/main.php index 78600612da..a83df10a22 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -142,6 +142,7 @@ public function __construct(int $value) { $array_pop_shift = eval('$items = ["first", "middle", "last"]; $popped = array_pop($items); $shifted = array_shift($items); return $popped . ":" . $shifted . ":" . count($items) . ":" . $items[0];'); $array_push_unshift = eval('$items = ["middle"]; array_push($items, "last"); array_unshift($items, "first"); return count($items) . ":" . $items[0] . ":" . $items[2];'); $array_splice = eval('$items = [10, 20, 30, 40]; $removed = array_splice($items, 1, 2); return count($removed) . ":" . $removed[0] . ":" . count($items) . ":" . $items[1];'); +$array_sort = eval('$items = [3, 1, 2]; sort($items); rsort($items); return $items[0] . ":" . $items[2];'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -286,6 +287,7 @@ public function __construct(int $value) { echo "array-pop-shift=" . $array_pop_shift . "\n"; echo "array-push-unshift=" . $array_push_unshift . "\n"; echo "array-splice=" . $array_splice . "\n"; +echo "array-sort=" . $array_sort . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 63d407e4c5..2cba98c0a2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -994,6 +994,28 @@ echo function_exists("array_splice");'); assert_eq!(out, "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:1"); } +/// Verifies eval `sort()` and `rsort()` mutate direct variable arguments only. +#[test] +fn test_eval_dispatches_sort_builtin_calls() { + let out = compile_and_run( + r#" 3, "y" => 1, "z" => 2]; +sort($c); +echo $c[0] . $c[1] . $c[2] . ":"; +$d = [3, 1, 2]; +echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; +$e = [1, 2, 3]; +echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; +echo function_exists("sort") && function_exists("rsort");'); +"#, + ); + assert_eq!(out, "1:123:1:cherry:apple:123:1:312:1:1:3:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From fc64c54af2ff7b7067b4c91eb3530ecc64286c74 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 16:53:21 +0200 Subject: [PATCH 0211/1208] Support eval key-preserving sort builtins --- crates/elephc-eval/src/interpreter.rs | 155 +++++++++++++++++++++++--- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 34 ++++++ 5 files changed, 177 insertions(+), 22 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7410801fa4..40715c1918 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1363,6 +1363,10 @@ fn eval_call( | "array_shift" | "array_splice" | "array_unshift" + | "arsort" + | "asort" + | "krsort" + | "ksort" | "rsort" | "sort" ) { @@ -1886,6 +1890,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "array_unique" | "array_unshift" | "array_values" + | "arsort" + | "asort" | "acos" | "asin" | "atan" @@ -2002,6 +2008,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "json_last_error" | "json_last_error_msg" | "json_validate" + | "krsort" + | "ksort" | "lcfirst" | "log" | "log2" @@ -2184,7 +2192,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_reduce" => Some(&["array", "callback", "initial"]), "array_walk" => Some(&["array", "callback"]), "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" - | "array_sum" | "array_unique" | "array_rand" | "array_values" | "rsort" | "sort" => { + | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" + | "asort" | "krsort" | "ksort" | "rsort" | "sort" => { Some(&["array"]) } "array_push" | "array_unshift" => Some(&["array", "values"]), @@ -2552,7 +2561,7 @@ fn eval_builtin_with_values( )?; result } - "rsort" | "sort" => { + "arsort" | "asort" | "krsort" | "ksort" | "rsort" | "sort" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -3813,7 +3822,10 @@ fn eval_builtin_array_pop_shift_call( if name == "array_splice" { return eval_builtin_array_splice_call(args, context, scope, values); } - if matches!(name, "sort" | "rsort") { + if matches!( + name, + "arsort" | "asort" | "krsort" | "ksort" | "rsort" | "sort" + ) { return eval_builtin_array_sort_call(name, args, context, scope, values); } @@ -3907,7 +3919,7 @@ fn eval_builtin_array_splice_call( Ok(removed) } -/// Evaluates direct by-reference `sort()` / `rsort()` calls and writes back the array. +/// Evaluates direct by-reference array ordering calls and writes back the array. fn eval_builtin_array_sort_call( name: &str, args: &[EvalCallArg], @@ -3935,7 +3947,7 @@ fn eval_builtin_array_sort_call( Ok(result) } -/// Extracts the direct variable argument accepted by `sort()` / `rsort()`. +/// Extracts the direct variable argument accepted by eval array ordering builtins. fn eval_array_sort_direct_arg(args: &[EvalCallArg]) -> Result { let [arg] = args else { return Err(EvalStatus::RuntimeFatal); @@ -3949,7 +3961,7 @@ fn eval_array_sort_direct_arg(args: &[EvalCallArg]) -> Result), } -/// Builds the sorted, reindexed replacement array for `sort()` / `rsort()`. +/// One source array entry plus its precomputed ordering key. +struct EvalArraySortEntry { + sort_key: EvalArraySortKey, + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for eval array ordering builtins. fn eval_array_sort_replacement( name: &str, array: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - let mut entries = eval_array_sort_entries(array, values)?; + let mut entries = match name { + "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, + "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; entries.sort_by(|left, right| { - let order = eval_array_sort_key_cmp(&left.0, &right.0); - if name == "rsort" { + let order = eval_array_sort_key_cmp(&left.sort_key, &right.sort_key); + if matches!(name, "arsort" | "krsort" | "rsort") { order.reverse() } else { order } }); + if matches!(name, "arsort" | "asort" | "krsort" | "ksort") { + return eval_array_preserve_key_sort_result(entries, values); + } + eval_array_reindex_sort_result(entries, values) +} + +/// Builds an indexed result for `sort()` / `rsort()` after value ordering. +fn eval_array_reindex_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { let mut result = values.array_new(entries.len())?; - for (index, (_, value)) in entries.into_iter().enumerate() { + for (index, entry) in entries.into_iter().enumerate() { let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, value)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds a key-preserving associative result after value or key ordering. +fn eval_array_preserve_key_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; } Ok(result) } -/// Collects values and comparable sort keys from one eval array. -fn eval_array_sort_entries( +/// Collects values and comparable value-sort keys from one eval array. +fn eval_array_value_sort_entries( array: RuntimeCellHandle, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let len = values.array_len(array)?; let mut entries = Vec::with_capacity(len); let mut expects_numeric = None; @@ -4010,7 +4056,33 @@ fn eval_array_sort_entries( Some(_) => {} None => expects_numeric = Some(is_numeric), } - entries.push((sort_key, value)); + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and comparable key-sort keys from one eval array. +fn eval_array_key_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(source_key, values)?; + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); } Ok(entries) @@ -4054,7 +4126,8 @@ fn eval_array_sort_key_cmp( left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) } (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), - _ => std::cmp::Ordering::Equal, + (EvalArraySortKey::Numeric(_), EvalArraySortKey::String(_)) => std::cmp::Ordering::Less, + (EvalArraySortKey::String(_), EvalArraySortKey::Numeric(_)) => std::cmp::Ordering::Greater, } } @@ -14829,6 +14902,52 @@ return function_exists("sort") && function_exists("rsort");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval key-preserving array ordering builtins write back direct variable calls. + #[test] + fn execute_program_dispatches_key_preserving_sort_builtins() { + let program = parse_fragment( + br#"$a = ["x" => 3, "y" => 1, "z" => 2]; +echo asort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value; } +echo ":"; +$b = ["x" => 1, "y" => 3, "z" => 2]; +echo arsort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2, 3 => 4]; +echo ksort($c) . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = ["b" => 1, "a" => 2, 3 => 4]; +echo krsort($d) . ":"; +foreach ($d as $key => $value) { echo $key . $value; } +echo ":"; +$e = ["x" => 2, "y" => 1]; +echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; +$f = ["b" => 1, "a" => 2]; +echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; +return function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:" + ); + assert_eq!( + values.warnings, + vec![ + "asort(): Argument #1 ($array) must be passed by reference, value given", + "krsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index af5a96b461..bac2343634 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -55,7 +55,7 @@ Eval `array_walk()` iterates keys through the eval value hooks, invokes the shar Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order; the splice path supports elephc's two- or three-argument form and returns the removed array while reindexing integer keys and preserving string keys. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value, resulting push/unshift count, or removed splice array without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. -Eval `sort()` and `rsort()` reuse the same direct-variable write-back path. The interpreter snapshots array values through `RuntimeValueOps`, builds homogeneous numeric or byte-string sort keys, reconstructs an indexed replacement array, and returns `true`; dynamic callable dispatch validates the array and reports PHP's by-reference warning without mutating the original caller value. +Eval `sort()`, `rsort()`, `asort()`, `arsort()`, `ksort()`, and `krsort()` reuse the same direct-variable write-back path. The interpreter snapshots array values through `RuntimeValueOps`, builds homogeneous numeric or byte-string sort keys, reconstructs either an indexed replacement array or a key-preserving associative replacement, and returns `true`; dynamic callable dispatch validates the array and reports PHP's by-reference warning without mutating the original caller value. Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 5e9af632b4..bd3370ebb3 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -66,7 +66,7 @@ Eval `array_walk()` supports the two-argument form with string callbacks. It inv Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` support direct variable calls with write-back into the eval scope. `array_pop()`, `array_shift()`, and `array_splice()` also accept named direct `array:` calls. Eval `array_splice()` supports the native elephc two- or three-argument form, returns the removed elements, reindexes integer keys, preserves string keys, and does not yet accept the replacement argument. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. -Eval `sort()` and `rsort()` support one-argument direct variable calls with write-back into the eval scope for homogeneous numeric or string arrays. They reindex integer keys, return `true`, accept named direct `array:` calls, and emit PHP's by-reference warning without mutating the caller's original array when reached through `call_user_func()` or `call_user_func_array()`. +Eval `sort()` and `rsort()` support one-argument direct variable calls with write-back into the eval scope for homogeneous numeric or string arrays. They reindex integer keys, return `true`, accept named direct `array:` calls, and emit PHP's by-reference warning without mutating the caller's original array when reached through `call_user_func()` or `call_user_func_array()`. Eval `asort()` and `arsort()` sort by value while preserving keys, and `ksort()` / `krsort()` sort by key while preserving values. Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. diff --git a/examples/eval/main.php b/examples/eval/main.php index a83df10a22..fef8163038 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -143,6 +143,7 @@ public function __construct(int $value) { $array_push_unshift = eval('$items = ["middle"]; array_push($items, "last"); array_unshift($items, "first"); return count($items) . ":" . $items[0] . ":" . $items[2];'); $array_splice = eval('$items = [10, 20, 30, 40]; $removed = array_splice($items, 1, 2); return count($removed) . ":" . $removed[0] . ":" . count($items) . ":" . $items[1];'); $array_sort = eval('$items = [3, 1, 2]; sort($items); rsort($items); return $items[0] . ":" . $items[2];'); +$array_key_sort = eval('$items = ["b" => 1, "a" => 2]; ksort($items); return $items["a"] . ":" . $items["b"];'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -288,6 +289,7 @@ public function __construct(int $value) { echo "array-push-unshift=" . $array_push_unshift . "\n"; echo "array-splice=" . $array_splice . "\n"; echo "array-sort=" . $array_sort . "\n"; +echo "array-key-sort=" . $array_key_sort . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2cba98c0a2..11dcf19d63 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1016,6 +1016,40 @@ echo function_exists("sort") && function_exists("rsort");'); assert_eq!(out, "1:123:1:cherry:apple:123:1:312:1:1:3:1"); } +/// Verifies eval key-preserving sort builtins mutate direct variable arguments only. +#[test] +fn test_eval_dispatches_key_preserving_sort_builtin_calls() { + let out = compile_and_run( + r#" 3, "y" => 1, "z" => 2]; +echo asort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value; } +echo ":"; +$b = ["x" => 1, "y" => 3, "z" => 2]; +echo arsort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2, 3 => 4]; +echo ksort($c) . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = ["b" => 1, "a" => 2, 3 => 4]; +echo krsort($d) . ":"; +foreach ($d as $key => $value) { echo $key . $value; } +echo ":"; +$e = ["x" => 2, "y" => 1]; +echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; +$f = ["b" => 1, "a" => 2]; +echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; +echo function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");'); +"#, + ); + assert_eq!( + out, + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:1" + ); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 53d433fa042d8364167a7518fbd5178abe427caa Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 16:56:40 +0200 Subject: [PATCH 0212/1208] Support eval natural sort builtins --- crates/elephc-eval/src/interpreter.rs | 185 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 24 ++++ 5 files changed, 209 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 40715c1918..75beca41df 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1367,6 +1367,8 @@ fn eval_call( | "asort" | "krsort" | "ksort" + | "natcasesort" + | "natsort" | "rsort" | "sort" ) { @@ -2022,6 +2024,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "mkdir" | "mktime" | "mt_rand" + | "natcasesort" + | "natsort" | "nl2br" | "number_format" | "ord" @@ -2193,7 +2197,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_walk" => Some(&["array", "callback"]), "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" - | "asort" | "krsort" | "ksort" | "rsort" | "sort" => { + | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "sort" => { Some(&["array"]) } "array_push" | "array_unshift" => Some(&["array", "values"]), @@ -2561,7 +2565,8 @@ fn eval_builtin_with_values( )?; result } - "arsort" | "asort" | "krsort" | "ksort" | "rsort" | "sort" => { + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "sort" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -3824,7 +3829,8 @@ fn eval_builtin_array_pop_shift_call( } if matches!( name, - "arsort" | "asort" | "krsort" | "ksort" | "rsort" | "sort" + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "sort" ) { return eval_builtin_array_sort_call(name, args, context, scope, values); } @@ -3976,6 +3982,7 @@ fn eval_array_sort_value_result( #[derive(Clone)] enum EvalArraySortKey { Numeric(f64), + Natural(Vec), String(Vec), } @@ -3994,6 +4001,8 @@ fn eval_array_sort_replacement( ) -> Result { let mut entries = match name { "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, + "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, + "natsort" => eval_array_natural_sort_entries(array, false, values)?, "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, _ => return Err(EvalStatus::UnsupportedConstruct), }; @@ -4006,7 +4015,10 @@ fn eval_array_sort_replacement( } }); - if matches!(name, "arsort" | "asort" | "krsort" | "ksort") { + if matches!( + name, + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" + ) { return eval_array_preserve_key_sort_result(entries, values); } eval_array_reindex_sort_result(entries, values) @@ -4066,6 +4078,36 @@ fn eval_array_value_sort_entries( Ok(entries) } +/// Collects values and natural-sort keys from one eval array. +fn eval_array_natural_sort_entries( + array: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_natural_sort_key(value, case_insensitive, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + /// Collects values and comparable key-sort keys from one eval array. fn eval_array_key_sort_entries( array: RuntimeCellHandle, @@ -4088,6 +4130,27 @@ fn eval_array_key_sort_entries( Ok(entries) } +/// Converts one scalar eval value into a natural-sort key. +fn eval_array_natural_sort_key( + value: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => Ok(EvalArraySortKey::Numeric(eval_float_value( + value, values, + )?)), + EVAL_TAG_STRING => { + let mut bytes = values.string_bytes(value)?; + if case_insensitive { + bytes.make_ascii_lowercase(); + } + Ok(EvalArraySortKey::Natural(bytes)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Converts one scalar eval value into a homogeneous sort key. fn eval_array_sort_key( value: RuntimeCellHandle, @@ -4125,9 +4188,86 @@ fn eval_array_sort_key_cmp( (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) } + (EvalArraySortKey::Natural(left), EvalArraySortKey::Natural(right)) => { + eval_natural_bytes_cmp(left, right) + } (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), - (EvalArraySortKey::Numeric(_), EvalArraySortKey::String(_)) => std::cmp::Ordering::Less, - (EvalArraySortKey::String(_), EvalArraySortKey::Numeric(_)) => std::cmp::Ordering::Greater, + _ => eval_array_sort_key_rank(left).cmp(&eval_array_sort_key_rank(right)), + } +} + +/// Returns a deterministic rank for mixed key-sort domains. +fn eval_array_sort_key_rank(key: &EvalArraySortKey) -> u8 { + match key { + EvalArraySortKey::Numeric(_) => 0, + EvalArraySortKey::Natural(_) => 1, + EvalArraySortKey::String(_) => 2, + } +} + +/// Compares byte strings with a small PHP-style natural ordering. +fn eval_natural_bytes_cmp(left: &[u8], right: &[u8]) -> std::cmp::Ordering { + let mut left_index = 0; + let mut right_index = 0; + while left_index < left.len() && right_index < right.len() { + if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { + let order = + eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); + if order != std::cmp::Ordering::Equal { + return order; + } + continue; + } + + let order = left[left_index].cmp(&right[right_index]); + if order != std::cmp::Ordering::Equal { + return order; + } + left_index += 1; + right_index += 1; + } + left.len().cmp(&right.len()) +} + +/// Compares two natural-sort digit runs and advances both byte indexes past them. +fn eval_natural_digit_run_cmp( + left: &[u8], + left_index: &mut usize, + right: &[u8], + right_index: &mut usize, +) -> std::cmp::Ordering { + let left_start = *left_index; + let right_start = *right_index; + while *left_index < left.len() && left[*left_index].is_ascii_digit() { + *left_index += 1; + } + while *right_index < right.len() && right[*right_index].is_ascii_digit() { + *right_index += 1; + } + + let left_digits = &left[left_start..*left_index]; + let right_digits = &right[right_start..*right_index]; + let left_trimmed = eval_trim_leading_zeroes(left_digits); + let right_trimmed = eval_trim_leading_zeroes(right_digits); + left_trimmed + .len() + .cmp(&right_trimmed.len()) + .then_with(|| left_trimmed.cmp(right_trimmed)) + .then_with(|| left_digits.len().cmp(&right_digits.len())) +} + +/// Drops leading zero bytes while keeping one zero for an all-zero digit run. +fn eval_trim_leading_zeroes(digits: &[u8]) -> &[u8] { + let trimmed = digits + .iter() + .position(|digit| *digit != b'0') + .map_or(&digits[digits.len().saturating_sub(1)..], |index| { + &digits[index..] + }); + if trimmed.is_empty() { + digits + } else { + trimmed } } @@ -14948,6 +15088,39 @@ return function_exists("asort") && function_exists("arsort") && function_exists( assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval natural sort builtins preserve keys and use natural string order. + #[test] + fn execute_program_dispatches_natural_sort_builtins() { + let program = parse_fragment( + br#"$a = ["img10", "img2", "img1"]; +echo natsort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$b = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +echo natcasesort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$c = ["x" => "b", "y" => "a"]; +echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; +return function_exists("natsort") && function_exists("natcasesort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:" + ); + assert_eq!( + values.warnings, + vec!["natsort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index bac2343634..dd20b15f8c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -55,7 +55,7 @@ Eval `array_walk()` iterates keys through the eval value hooks, invokes the shar Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order; the splice path supports elephc's two- or three-argument form and returns the removed array while reindexing integer keys and preserving string keys. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value, resulting push/unshift count, or removed splice array without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. -Eval `sort()`, `rsort()`, `asort()`, `arsort()`, `ksort()`, and `krsort()` reuse the same direct-variable write-back path. The interpreter snapshots array values through `RuntimeValueOps`, builds homogeneous numeric or byte-string sort keys, reconstructs either an indexed replacement array or a key-preserving associative replacement, and returns `true`; dynamic callable dispatch validates the array and reports PHP's by-reference warning without mutating the original caller value. +Eval `sort()`, `rsort()`, `asort()`, `arsort()`, `ksort()`, `krsort()`, `natsort()`, and `natcasesort()` reuse the same direct-variable write-back path. The interpreter snapshots array values through `RuntimeValueOps`, builds homogeneous numeric, byte-string, or natural-sort keys, reconstructs either an indexed replacement array or a key-preserving associative replacement, and returns `true`; dynamic callable dispatch validates the array and reports PHP's by-reference warning without mutating the original caller value. Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index bd3370ebb3..155f7f6c44 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -66,7 +66,7 @@ Eval `array_walk()` supports the two-argument form with string callbacks. It inv Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` support direct variable calls with write-back into the eval scope. `array_pop()`, `array_shift()`, and `array_splice()` also accept named direct `array:` calls. Eval `array_splice()` supports the native elephc two- or three-argument form, returns the removed elements, reindexes integer keys, preserves string keys, and does not yet accept the replacement argument. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. -Eval `sort()` and `rsort()` support one-argument direct variable calls with write-back into the eval scope for homogeneous numeric or string arrays. They reindex integer keys, return `true`, accept named direct `array:` calls, and emit PHP's by-reference warning without mutating the caller's original array when reached through `call_user_func()` or `call_user_func_array()`. Eval `asort()` and `arsort()` sort by value while preserving keys, and `ksort()` / `krsort()` sort by key while preserving values. +Eval `sort()` and `rsort()` support one-argument direct variable calls with write-back into the eval scope for homogeneous numeric or string arrays. They reindex integer keys, return `true`, accept named direct `array:` calls, and emit PHP's by-reference warning without mutating the caller's original array when reached through `call_user_func()` or `call_user_func_array()`. Eval `asort()` and `arsort()` sort by value while preserving keys, `ksort()` / `krsort()` sort by key while preserving values, and `natsort()` / `natcasesort()` preserve keys while applying natural string ordering. Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. diff --git a/examples/eval/main.php b/examples/eval/main.php index fef8163038..3cc75a25ba 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -144,6 +144,7 @@ public function __construct(int $value) { $array_splice = eval('$items = [10, 20, 30, 40]; $removed = array_splice($items, 1, 2); return count($removed) . ":" . $removed[0] . ":" . count($items) . ":" . $items[1];'); $array_sort = eval('$items = [3, 1, 2]; sort($items); rsort($items); return $items[0] . ":" . $items[2];'); $array_key_sort = eval('$items = ["b" => 1, "a" => 2]; ksort($items); return $items["a"] . ":" . $items["b"];'); +$array_natural_sort = eval('$items = ["img10", "img2", "img1"]; natsort($items); return $items[2] . ":" . $items[0];'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -290,6 +291,7 @@ public function __construct(int $value) { echo "array-splice=" . $array_splice . "\n"; echo "array-sort=" . $array_sort . "\n"; echo "array-key-sort=" . $array_key_sort . "\n"; +echo "array-natural-sort=" . $array_natural_sort . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 11dcf19d63..da65af4755 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1050,6 +1050,30 @@ echo function_exists("asort") && function_exists("arsort") && function_exists("k ); } +/// Verifies eval natural sort builtins preserve keys and use natural string order. +#[test] +fn test_eval_dispatches_natural_sort_builtin_calls() { + let out = compile_and_run( + r#" $value) { echo $key . $value . ";"; } +echo ":"; +$b = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +echo natcasesort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$c = ["x" => "b", "y" => "a"]; +echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; +echo function_exists("natsort") && function_exists("natcasesort");'); +"#, + ); + assert_eq!( + out, + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:1" + ); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 7fa5adfa93cd4251a765a72e09ddff66fd6d5383 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 17:00:22 +0200 Subject: [PATCH 0213/1208] Support eval shuffle builtin --- crates/elephc-eval/src/interpreter.rs | 61 ++++++++++++++++++++++++--- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 15 +++++++ 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 75beca41df..205b8b57b6 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1370,6 +1370,7 @@ fn eval_call( | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" ) { return eval_builtin_array_pop_shift_call(name, args, context, scope, values); @@ -2055,6 +2056,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "scandir" | "sleep" | "sha1" + | "shuffle" | "sin" | "sinh" | "sort" @@ -2197,9 +2199,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_walk" => Some(&["array", "callback"]), "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" - | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "sort" => { - Some(&["array"]) - } + | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" => Some(&["array"]), "array_push" | "array_unshift" => Some(&["array", "values"]), "array_key_exists" => Some(&["key", "array"]), "array_pad" => Some(&["array", "length", "value"]), @@ -2566,7 +2567,7 @@ fn eval_builtin_with_values( result } "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" - | "sort" => { + | "shuffle" | "sort" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -3830,7 +3831,7 @@ fn eval_builtin_array_pop_shift_call( if matches!( name, "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" - | "sort" + | "shuffle" | "sort" ) { return eval_builtin_array_sort_call(name, args, context, scope, values); } @@ -4004,6 +4005,7 @@ fn eval_array_sort_replacement( "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, "natsort" => eval_array_natural_sort_entries(array, false, values)?, "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, + "shuffle" => return eval_array_shuffle_replacement(array, values), _ => return Err(EvalStatus::UnsupportedConstruct), }; entries.sort_by(|left, right| { @@ -4024,6 +4026,31 @@ fn eval_array_sort_replacement( eval_array_reindex_sort_result(entries, values) } +/// Builds a shuffled, reindexed replacement array for `shuffle()`. +fn eval_array_shuffle_replacement( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + entries.push(values.array_get(array, source_key)?); + } + + for index in (1..entries.len()).rev() { + let swap_with = (eval_random_u128() % ((index + 1) as u128)) as usize; + entries.swap(index, swap_with); + } + + let mut result = values.array_new(entries.len())?; + for (index, value) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Builds an indexed result for `sort()` / `rsort()` after value ordering. fn eval_array_reindex_sort_result( entries: Vec, @@ -15121,6 +15148,30 @@ return function_exists("natsort") && function_exists("natcasesort");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `shuffle()` reindexes direct variable arrays only. + #[test] + fn execute_program_dispatches_shuffle_builtin() { + let program = parse_fragment( + br#"$a = ["x" => 1, "y" => 2]; +echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; +$b = ["x" => 1, "y" => 2]; +echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; +return function_exists("shuffle");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:reindexed:2:3:1:12:"); + assert_eq!( + values.warnings, + vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index dd20b15f8c..2c0bb1286f 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -57,6 +57,8 @@ Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `arr Eval `sort()`, `rsort()`, `asort()`, `arsort()`, `ksort()`, `krsort()`, `natsort()`, and `natcasesort()` reuse the same direct-variable write-back path. The interpreter snapshots array values through `RuntimeValueOps`, builds homogeneous numeric, byte-string, or natural-sort keys, reconstructs either an indexed replacement array or a key-preserving associative replacement, and returns `true`; dynamic callable dispatch validates the array and reports PHP's by-reference warning without mutating the original caller value. +Eval `shuffle()` also uses that direct-variable write-back path, but collects source values, applies an eval-local Fisher-Yates pass, and reconstructs an indexed replacement array so string keys are discarded and integer keys are reindexed. + Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 155f7f6c44..6dd1dfb25f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -68,6 +68,8 @@ Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `arr Eval `sort()` and `rsort()` support one-argument direct variable calls with write-back into the eval scope for homogeneous numeric or string arrays. They reindex integer keys, return `true`, accept named direct `array:` calls, and emit PHP's by-reference warning without mutating the caller's original array when reached through `call_user_func()` or `call_user_func_array()`. Eval `asort()` and `arsort()` sort by value while preserving keys, `ksort()` / `krsort()` sort by key while preserving values, and `natsort()` / `natcasesort()` preserve keys while applying natural string ordering. +Eval `shuffle()` supports one-argument direct variable calls with write-back into the eval scope. It randomizes element order, reindexes integer keys, returns `true`, and follows the same by-reference warning/no-mutation behavior as the other array ordering builtins when called dynamically. + Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/examples/eval/main.php b/examples/eval/main.php index 3cc75a25ba..464604d4aa 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -145,6 +145,7 @@ public function __construct(int $value) { $array_sort = eval('$items = [3, 1, 2]; sort($items); rsort($items); return $items[0] . ":" . $items[2];'); $array_key_sort = eval('$items = ["b" => 1, "a" => 2]; ksort($items); return $items["a"] . ":" . $items["b"];'); $array_natural_sort = eval('$items = ["img10", "img2", "img1"]; natsort($items); return $items[2] . ":" . $items[0];'); +$array_shuffle = eval('$items = ["x" => 1, "y" => 2]; shuffle($items); return count($items) . ":" . array_sum($items) . ":" . (isset($items["x"]) ? "bad" : "reindexed");'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -292,6 +293,7 @@ public function __construct(int $value) { echo "array-sort=" . $array_sort . "\n"; echo "array-key-sort=" . $array_key_sort . "\n"; echo "array-natural-sort=" . $array_natural_sort . "\n"; +echo "array-shuffle=" . $array_shuffle . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index da65af4755..7ef53813e8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1074,6 +1074,21 @@ echo function_exists("natsort") && function_exists("natcasesort");'); ); } +/// Verifies eval `shuffle()` reindexes direct variable arrays only. +#[test] +fn test_eval_dispatches_shuffle_builtin_call() { + let out = compile_and_run( + r#" 1, "y" => 2]; +echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; +$b = ["x" => 1, "y" => 2]; +echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; +echo function_exists("shuffle");'); +"#, + ); + assert_eq!(out, "1:reindexed:2:3:1:12:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 1520bf2eb3dfcd4913ab78e758fc8f6aca24c834 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 17:04:44 +0200 Subject: [PATCH 0214/1208] Support eval user sort builtins --- crates/elephc-eval/src/interpreter.rs | 258 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 27 +++ 5 files changed, 293 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 205b8b57b6..f88fcce58a 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1372,6 +1372,9 @@ fn eval_call( | "rsort" | "shuffle" | "sort" + | "uasort" + | "uksort" + | "usort" ) { return eval_builtin_array_pop_shift_call(name, args, context, scope, values); } @@ -2099,10 +2102,13 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "substr_replace" | "ucfirst" | "ucwords" + | "uasort" + | "uksort" | "unlink" | "umask" | "urldecode" | "urlencode" + | "usort" | "usleep" | "var_dump" | "printf" @@ -2197,6 +2203,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_map" => Some(&["callback", "array"]), "array_reduce" => Some(&["array", "callback", "initial"]), "array_walk" => Some(&["array", "callback"]), + "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" @@ -2576,6 +2583,15 @@ fn eval_builtin_with_values( ))?; eval_array_sort_value_result(*array, values)? } + "uasort" | "uksort" | "usort" => { + let [array, callback] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_user_sort_value_result(name, *array, *callback, context, values)? + } "array_flip" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -3835,6 +3851,9 @@ fn eval_builtin_array_pop_shift_call( ) { return eval_builtin_array_sort_call(name, args, context, scope, values); } + if matches!(name, "uasort" | "uksort" | "usort") { + return eval_builtin_user_sort_call(name, args, context, scope, values); + } let [arg] = args else { return Err(EvalStatus::RuntimeFatal); @@ -3954,6 +3973,209 @@ fn eval_builtin_array_sort_call( Ok(result) } +/// Evaluates direct by-reference user-comparator sort calls and writes back the array. +fn eval_builtin_user_sort_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array_name, callback) = eval_user_sort_direct_args(args, context, scope, values)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + let result = values.bool_value(true)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates and binds direct user-sort arguments while preserving source order. +fn eval_user_sort_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(String, RuntimeCellHandle), EvalStatus> { + let mut array = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + array = Some(name.clone()); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, callback)) +} + +/// Returns the dynamic callable result for by-value user-comparator sort calls. +fn eval_user_sort_value_result( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + values.release(replacement)?; + values.bool_value(true) +} + +/// One source array entry used by eval user-comparator sort routines. +struct EvalUserSortEntry { + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for user-comparator sort builtins. +fn eval_user_sort_replacement( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let mut entries = eval_user_sort_entries(array, values)?; + eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; + if name == "usort" { + return eval_user_sort_reindex_result(entries, values); + } + eval_user_sort_preserve_key_result(entries, values) +} + +/// Collects source keys and values from one eval array for user sorting. +fn eval_user_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + entries.push(EvalUserSortEntry { source_key, value }); + } + Ok(entries) +} + +/// Sorts entries by repeatedly invoking the PHP comparator callback. +fn eval_user_sort_entries_in_place( + name: &str, + callback: &str, + entries: &mut [EvalUserSortEntry], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for pass in 0..entries.len() { + let upper = entries.len().saturating_sub(pass + 1); + for index in 0..upper { + let comparison = + eval_user_sort_compare(name, callback, &entries[index], &entries[index + 1], context, values)?; + if comparison > 0 { + entries.swap(index, index + 1); + } + } + } + Ok(()) +} + +/// Invokes one user-sort comparator and returns its integer ordering result. +fn eval_user_sort_compare( + name: &str, + callback: &str, + left: &EvalUserSortEntry, + right: &EvalUserSortEntry, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = if name == "uksort" { + vec![left.source_key, right.source_key] + } else { + vec![left.value, right.value] + }; + let result = eval_callable_with_values(callback, args, context, values)?; + eval_int_value(result, values) +} + +/// Builds the reindexed result for `usort()`. +fn eval_user_sort_reindex_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds the key-preserving result for `uksort()` and `uasort()`. +fn eval_user_sort_preserve_key_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + /// Extracts the direct variable argument accepted by eval array ordering builtins. fn eval_array_sort_direct_arg(args: &[EvalCallArg]) -> Result { let [arg] = args else { @@ -15172,6 +15394,42 @@ return function_exists("shuffle");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval user-comparator sort builtins call callbacks and write back direct arrays. + #[test] + fn execute_program_dispatches_user_sort_builtins() { + let program = parse_fragment( + br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } +function eval_key_cmp($left, $right) { return strcmp($left, $right); } +$a = [3, 1, 2]; +echo usort($a, "eval_sort_cmp") . ":"; +foreach ($a as $value) { echo $value; } +echo ":"; +$b = ["b" => 1, "a" => 3, "c" => 2]; +echo uasort(array: $b, callback: "eval_sort_cmp") . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2]; +echo uksort($c, "eval_key_cmp") . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = [2, 1]; +echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; +return function_exists("usort") && function_exists("uasort") && function_exists("uksort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:"); + assert_eq!( + values.warnings, + vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2c0bb1286f..e3e03ebc87 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -59,6 +59,8 @@ Eval `sort()`, `rsort()`, `asort()`, `arsort()`, `ksort()`, `krsort()`, `natsort Eval `shuffle()` also uses that direct-variable write-back path, but collects source values, applies an eval-local Fisher-Yates pass, and reconstructs an indexed replacement array so string keys are discarded and integer keys are reindexed. +Eval `usort()`, `uksort()`, and `uasort()` use explicit comparator loops instead of host-language sort callbacks because eval comparators can emit output or call back into eval/AOT functions. `usort()` rebuilds an indexed array, while `uksort()` and `uasort()` preserve source keys. Dynamic callable dispatch sorts a by-value copy for comparator side effects, releases the copy, reports the PHP by-reference warning, and leaves the caller's original array unchanged. + Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6dd1dfb25f..ed7e7900e3 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -70,6 +70,8 @@ Eval `sort()` and `rsort()` support one-argument direct variable calls with writ Eval `shuffle()` supports one-argument direct variable calls with write-back into the eval scope. It randomizes element order, reindexes integer keys, returns `true`, and follows the same by-reference warning/no-mutation behavior as the other array ordering builtins when called dynamically. +Eval `usort()`, `uksort()`, and `uasort()` support string callbacks that resolve to eval-declared functions, registered AOT callbacks, or supported builtins. Direct variable calls write the sorted replacement back into the eval scope; dynamic callable dispatch still invokes the comparator on a by-value copy, emits PHP's by-reference warning, returns `true`, and leaves the caller's original array unchanged. + Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/examples/eval/main.php b/examples/eval/main.php index 464604d4aa..07f2f7af3a 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -146,6 +146,7 @@ public function __construct(int $value) { $array_key_sort = eval('$items = ["b" => 1, "a" => 2]; ksort($items); return $items["a"] . ":" . $items["b"];'); $array_natural_sort = eval('$items = ["img10", "img2", "img1"]; natsort($items); return $items[2] . ":" . $items[0];'); $array_shuffle = eval('$items = ["x" => 1, "y" => 2]; shuffle($items); return count($items) . ":" . array_sum($items) . ":" . (isset($items["x"]) ? "bad" : "reindexed");'); +$array_user_sort = eval('function eval_example_cmp($left, $right) { return $left <=> $right; } $items = [3, 1, 2]; usort($items, "eval_example_cmp"); return $items[0] . ":" . $items[2];'); $array_filter = eval('$items = array_filter([0, 1, "", "ok"]); return count($items) . ":" . $items[1] . ":" . $items[3];'); $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); @@ -294,6 +295,7 @@ public function __construct(int $value) { echo "array-key-sort=" . $array_key_sort . "\n"; echo "array-natural-sort=" . $array_natural_sort . "\n"; echo "array-shuffle=" . $array_shuffle . "\n"; +echo "array-user-sort=" . $array_user_sort . "\n"; echo "array-filter=" . $array_filter . "\n"; echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7ef53813e8..dcd390d2b4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1089,6 +1089,33 @@ echo function_exists("shuffle");'); assert_eq!(out, "1:reindexed:2:3:1:12:1"); } +/// Verifies eval user-comparator sort builtins call callbacks and mutate direct arrays. +#[test] +fn test_eval_dispatches_user_sort_builtin_calls() { + let out = compile_and_run( + r#" $right; } +function eval_key_cmp($left, $right) { return strcmp($left, $right); } +$a = [3, 1, 2]; +echo usort($a, "eval_sort_cmp") . ":"; +foreach ($a as $value) { echo $value; } +echo ":"; +$b = ["b" => 1, "a" => 3, "c" => 2]; +echo uasort(array: $b, callback: "eval_sort_cmp") . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2]; +echo uksort($c, "eval_key_cmp") . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = [2, 1]; +echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; +echo function_exists("usort") && function_exists("uasort") && function_exists("uksort");'); +"#, + ); + assert_eq!(out, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From fc6017aa3b79ec1436fba04cfab90ebf11cf9092 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 17:12:20 +0200 Subject: [PATCH 0215/1208] Support eval iterator array builtins --- crates/elephc-eval/src/interpreter.rs | 122 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 19 ++++ 5 files changed, 149 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f88fcce58a..5f219ffcdc 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1532,6 +1532,8 @@ fn eval_positional_expr_call( "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), "intdiv" => eval_builtin_intdiv(args, context, scope, values), + "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), + "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" @@ -2009,6 +2011,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_real" | "is_resource" | "is_string" + | "iterator_count" + | "iterator_to_array" | "json_decode" | "json_encode" | "json_last_error" @@ -2275,6 +2279,8 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), "intdiv" => Some(&["num1", "num2"]), + "iterator_count" => Some(&["iterator"]), + "iterator_to_array" => Some(&["iterator", "preserve_keys"]), "ip2long" => Some(&["ip"]), "json_decode" => Some(&["json", "associative", "depth", "flags"]), "json_encode" => Some(&["value", "flags", "depth"]), @@ -3251,6 +3257,20 @@ fn eval_builtin_with_values( }; eval_intdiv_result(*left, *right, values)? } + "iterator_count" => { + let [iterator] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_iterator_count_result(*iterator, values)? + } + "iterator_to_array" => match evaluated_args { + [iterator] => eval_iterator_to_array_result(*iterator, true, values)?, + [iterator, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_iterator_to_array_result(*iterator, preserve_keys, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "ip2long" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -5438,6 +5458,84 @@ fn eval_array_projection_result( Ok(result) } +/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. +fn eval_builtin_iterator_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [iterator] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_count_result(iterator, values) +} + +/// Returns the element count for eval-supported array iterator inputs. +fn eval_iterator_count_result( + iterator: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(iterator)?; + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. +fn eval_builtin_iterator_to_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator] => { + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_to_array_result(iterator, true, values) + } + [iterator, preserve_keys] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_iterator_to_array_result(iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies eval-supported array iterator inputs into a PHP array result. +fn eval_iterator_to_array_result( + iterator: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + if preserve_keys { + return eval_array_copy_preserve_keys(iterator, values); + } + eval_array_projection_result("array_values", iterator, values) +} + +/// Copies one array-like eval value while preserving iteration keys and order. +fn eval_array_copy_preserve_keys( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Evaluates PHP `array_reverse()` over an eval array expression. fn eval_builtin_array_reverse( args: &[EvalExpr], @@ -15430,6 +15528,30 @@ return function_exists("usort") && function_exists("uasort") && function_exists( assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval iterator array helpers support direct and dynamic builtin calls. + #[test] + fn execute_program_dispatches_iterator_array_builtins() { + let program = parse_fragment( + br#"$items = ["x" => 1, "y" => 2]; +$copy = iterator_to_array($items); +echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; +$values = iterator_to_array($items, false); +echo (isset($values["x"]) ? "bad" : "reindexed") . ":" . $values[0] . $values[1] . ":"; +echo call_user_func("iterator_count", $items) . ":"; +$spread = call_user_func_array("iterator_to_array", ["iterator" => $items, "preserve_keys" => false]); +echo $spread[0] . $spread[1] . ":"; +return function_exists("iterator_count") && function_exists("iterator_to_array");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:12:reindexed:12:2:12:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e3e03ebc87..c3b006184a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -63,6 +63,8 @@ Eval `usort()`, `uksort()`, and `uasort()` use explicit comparator loops instead Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. +Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false; Traversable object dispatch for eval `iterator_apply()` remains future work. + Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ed7e7900e3..638ffb9402 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -74,6 +74,8 @@ Eval `usort()`, `uksort()`, and `uasort()` support string callbacks that resolve Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. +Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` still requires the Traversable object path and is not implemented for arrays, matching PHP's array `TypeError` rather than treating arrays as Traversable. + Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. diff --git a/examples/eval/main.php b/examples/eval/main.php index 07f2f7af3a..2c3d01e7d2 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -151,6 +151,7 @@ public function __construct(int $value) { $array_filter_callback = eval('function eval_example_keep_pair($value, $key) { return $key === "b" || $value === 3; } $items = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_example_keep_pair", ARRAY_FILTER_USE_BOTH); return count($items) . ":" . $items["b"] . ":" . $items["c"];'); $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); +$iterator_arrays = eval('$items = ["x" => 1, "y" => 2]; $copy = iterator_to_array($items); $values = iterator_to_array($items, false); return iterator_count($items) . ":" . $copy["x"] . ":" . $values[0];'); $mixed_literal = eval('return [2 => "two", "tail"][3] . ":" . (["2" => "two", "next"][3]);'); $append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); $append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); @@ -300,6 +301,7 @@ public function __construct(int $value) { echo "array-filter-callback=" . $array_filter_callback . "\n"; echo "named-builtins=" . $named_builtins . "\n"; echo "array-projection=" . $array_projection . "\n"; +echo "iterator-arrays=" . $iterator_arrays . "\n"; echo "mixed-literal=" . $mixed_literal . "\n"; echo "append-items=" . $append_items . "\n"; echo "append-assoc=" . $append_assoc . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index dcd390d2b4..5158608837 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1116,6 +1116,25 @@ echo function_exists("usort") && function_exists("uasort") && function_exists("u assert_eq!(out, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1"); } +/// Verifies eval iterator array helpers dispatch through direct and dynamic calls. +#[test] +fn test_eval_dispatches_iterator_array_builtin_calls() { + let out = compile_and_run( + r#" 1, "y" => 2]; +$copy = iterator_to_array($items); +echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; +$values = iterator_to_array($items, false); +echo (isset($values["x"]) ? "bad" : "reindexed") . ":" . $values[0] . $values[1] . ":"; +echo call_user_func("iterator_count", $items) . ":"; +$spread = call_user_func_array("iterator_to_array", ["iterator" => $items, "preserve_keys" => false]); +echo $spread[0] . $spread[1] . ":"; +echo function_exists("iterator_count") && function_exists("iterator_to_array");'); +"#, + ); + assert_eq!(out, "2:12:reindexed:12:2:12:1"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From 7f119d61afbbb2bb37c3158b886141ceae530d81 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 17:22:34 +0200 Subject: [PATCH 0216/1208] Support eval regex builtins --- Cargo.lock | 1 + crates/elephc-eval/Cargo.toml | 1 + crates/elephc-eval/src/interpreter.rs | 641 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 6 +- docs/php/system-and-io.md | 6 +- examples/eval/main.php | 2 + tests/codegen/eval.rs | 30 ++ 7 files changed, 683 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e5bc09952..5ca3dba80c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -622,6 +622,7 @@ version = "0.1.0" dependencies = [ "elephc-crypto", "libc", + "regex", ] [[package]] diff --git a/crates/elephc-eval/Cargo.toml b/crates/elephc-eval/Cargo.toml index 1da35630d7..62c4c531bc 100644 --- a/crates/elephc-eval/Cargo.toml +++ b/crates/elephc-eval/Cargo.toml @@ -12,3 +12,4 @@ crate-type = ["staticlib", "rlib"] [dependencies] elephc-crypto = { path = "../elephc-crypto" } libc = "0.2" +regex = "1" diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5f219ffcdc..8d4be5990a 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -21,6 +21,7 @@ use crate::json_validate::{self, JsonValue}; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; +use regex::bytes::{Captures, Regex, RegexBuilder}; use std::ffi::{CStr, CString}; use std::mem::MaybeUninit; use std::net::ToSocketAddrs; @@ -549,6 +550,13 @@ const EVAL_ARRAY_FILTER_USE_BOTH: i64 = 1; const EVAL_ARRAY_FILTER_USE_KEY: i64 = 2; const EVAL_COUNT_NORMAL: i64 = 0; const EVAL_COUNT_RECURSIVE: i64 = 1; +const EVAL_PREG_SPLIT_NO_EMPTY: i64 = 1; +const EVAL_PREG_SPLIT_DELIM_CAPTURE: i64 = 2; +const EVAL_PREG_SPLIT_OFFSET_CAPTURE: i64 = 4; +const EVAL_PREG_PATTERN_ORDER: i64 = 1; +const EVAL_PREG_SET_ORDER: i64 = 2; +const EVAL_PREG_OFFSET_CAPTURE: i64 = 256; +const EVAL_PREG_UNMATCHED_AS_NULL: i64 = 512; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -1561,6 +1569,13 @@ fn eval_positional_expr_call( "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), "pow" => eval_builtin_pow(args, context, scope, values), + "preg_match" => eval_builtin_preg_match(args, context, scope, values), + "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), + "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), + "preg_replace_callback" => { + eval_builtin_preg_replace_callback(args, context, scope, values) + } + "preg_split" => eval_builtin_preg_split(args, context, scope, values), "print_r" => eval_builtin_print_r(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), @@ -2042,6 +2057,11 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "pow" | "php_uname" | "phpversion" + | "preg_match" + | "preg_match_all" + | "preg_replace" + | "preg_replace_callback" + | "preg_split" | "putenv" | "print_r" | "rand" @@ -2301,6 +2321,11 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), "pow" => Some(&["num", "exponent"]), + "preg_match" => Some(&["pattern", "subject", "matches", "flags", "offset"]), + "preg_match_all" => Some(&["pattern", "subject", "matches", "flags", "offset"]), + "preg_replace" => Some(&["pattern", "replacement", "subject", "limit", "count"]), + "preg_replace_callback" => Some(&["pattern", "callback", "subject", "limit", "count"]), + "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), "print_r" | "var_dump" => Some(&["value"]), "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), @@ -2861,6 +2886,36 @@ fn eval_builtin_with_values( }; values.pow(*left, *right)? } + "preg_match" => match evaluated_args { + [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_match_all" => match evaluated_args { + [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_replace" => match evaluated_args { + [pattern, replacement, subject] => { + eval_preg_replace_result(*pattern, *replacement, *subject, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_replace_callback" => match evaluated_args { + [pattern, callback, subject] => { + eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_split" => match evaluated_args { + [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values)?, + [pattern, subject, limit] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), None, values)? + } + [pattern, subject, limit, flags] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "print_r" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -9790,6 +9845,544 @@ fn eval_fnmatch_fold(byte: u8, flags: i64) -> u8 { } } +/// Evaluates PHP `preg_match()` over eval expressions. +fn eval_builtin_preg_match( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let (result, matches_array) = eval_preg_match_capture_result(pattern, subject, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether one regex matches the subject string. +fn eval_preg_match_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + values.int(i64::from(regex.is_match(&subject))) +} + +/// Returns the match flag plus PHP `$matches` capture array for one regex search. +fn eval_preg_match_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + if let Some(captures) = regex.captures(&subject) { + let matches = eval_preg_capture_array(&subject, Some(&captures), values)?; + let matched = values.int(1)?; + return Ok((matched, matches)); + } + let matches = eval_preg_capture_array(&subject, None, values)?; + let matched = values.int(0)?; + Ok((matched, matches)) +} + +/// Evaluates PHP `preg_match_all()` count-only calls over eval expressions. +fn eval_builtin_preg_match_all( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_all_result(pattern, subject, values) +} + +/// Counts all non-overlapping regex matches in one subject string. +fn eval_preg_match_all_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let count = regex.captures_iter(&subject).count(); + values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `preg_replace()` over eval expressions. +fn eval_builtin_preg_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, replacement, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let replacement = eval_expr(replacement, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_result(pattern, replacement, subject, values) +} + +/// Replaces every regex match with a PHP-style backreference-expanded replacement. +fn eval_preg_replace_result( + pattern: RuntimeCellHandle, + replacement: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let replacement = values.string_bytes(replacement)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + +/// Evaluates PHP `preg_replace_callback()` over eval expressions. +fn eval_builtin_preg_replace_callback( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, callback, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_callback_result(pattern, callback, subject, context, values) +} + +/// Replaces every regex match by invoking an eval-supported callback with `$matches`. +fn eval_preg_replace_callback_result( + pattern: RuntimeCellHandle, + callback: RuntimeCellHandle, + subject: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let callback = eval_callable_name(callback, values)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + let matches = eval_preg_capture_array(&subject, Some(&captures), values)?; + let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; + let callback_result = values.cast_string(callback_result)?; + let callback_bytes = values.string_bytes(callback_result)?; + result.extend_from_slice(&callback_bytes); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + +/// Evaluates PHP `preg_split()` over eval expressions. +fn eval_builtin_preg_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_split_result(pattern, subject, None, None, values) + } + [pattern, subject, limit] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), None, values) + } + [pattern, subject, limit, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits a subject string with eval-supported `preg_split()` flags. +fn eval_preg_split_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + limit: Option, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let limit = eval_preg_split_limit(limit, values)?; + let flags = eval_preg_split_flags(flags, values)?; + let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; + let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; + let mut pieces = Vec::>::new(); + let mut cursor = 0; + + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + if eval_preg_split_reached_limit(&pieces, limit) { + break; + } + eval_preg_split_push_piece(&mut pieces, &subject[cursor..matched.start()], no_empty); + if capture_delimiters { + eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); + } + cursor = matched.end(); + } + eval_preg_split_push_piece(&mut pieces, &subject[cursor..], no_empty); + + let mut result = values.array_new(pieces.len())?; + for (index, piece) in pieces.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(piece)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Compiles one eval PCRE-style delimited pattern into a Rust regex. +fn eval_preg_regex( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; + let body = String::from_utf8(body).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut builder = RegexBuilder::new(&body); + builder + .case_insensitive(modifiers.case_insensitive) + .multi_line(modifiers.multi_line) + .dot_matches_new_line(modifiers.dot_matches_new_line) + .swap_greed(modifiers.swap_greed); + builder.build().map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Regex modifiers supported by eval `preg_*` pattern stripping. +#[derive(Default)] +struct EvalPregModifiers { + case_insensitive: bool, + multi_line: bool, + dot_matches_new_line: bool, + swap_greed: bool, +} + +/// Splits a PHP delimited regex into body bytes and supported modifiers. +fn eval_preg_pattern_parts(pattern: &[u8]) -> Result<(Vec, EvalPregModifiers), EvalStatus> { + if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() + { + return Err(EvalStatus::RuntimeFatal); + } + let delimiter = pattern[0]; + if delimiter == b'\\' { + return Err(EvalStatus::RuntimeFatal); + } + let closing = eval_preg_closing_delimiter(delimiter); + let close_index = + eval_preg_find_closing_delimiter(pattern, closing).ok_or(EvalStatus::RuntimeFatal)?; + let body = eval_preg_unescape_delimiter(&pattern[1..close_index], delimiter, closing); + let modifiers = eval_preg_modifiers(&pattern[close_index + 1..])?; + Ok((body, modifiers)) +} + +/// Returns the closing regex delimiter for PHP's paired delimiter forms. +fn eval_preg_closing_delimiter(delimiter: u8) -> u8 { + match delimiter { + b'(' => b')', + b'[' => b']', + b'{' => b'}', + b'<' => b'>', + _ => delimiter, + } +} + +/// Finds the first unescaped closing regex delimiter. +fn eval_preg_find_closing_delimiter(pattern: &[u8], closing: u8) -> Option { + let mut escaped = false; + for (index, byte) in pattern.iter().copied().enumerate().skip(1) { + if escaped { + escaped = false; + continue; + } + if byte == b'\\' { + escaped = true; + continue; + } + if byte == closing { + return Some(index); + } + } + None +} + +/// Removes escapes that only protect the PHP regex delimiter from pattern stripping. +fn eval_preg_unescape_delimiter(body: &[u8], delimiter: u8, closing: u8) -> Vec { + let mut result = Vec::with_capacity(body.len()); + let mut index = 0; + while index < body.len() { + if body[index] == b'\\' + && index + 1 < body.len() + && matches!(body[index + 1], byte if byte == delimiter || byte == closing) + { + result.push(body[index + 1]); + index += 2; + } else { + result.push(body[index]); + index += 1; + } + } + result +} + +/// Parses eval-supported PHP regex modifiers. +fn eval_preg_modifiers(modifiers: &[u8]) -> Result { + let mut parsed = EvalPregModifiers::default(); + for modifier in modifiers { + match *modifier { + b'i' => parsed.case_insensitive = true, + b'm' => parsed.multi_line = true, + b's' => parsed.dot_matches_new_line = true, + b'U' => parsed.swap_greed = true, + b'u' => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(parsed) +} + +/// Builds PHP's indexed `$matches` capture array for one regex result. +fn eval_preg_capture_array( + subject: &[u8], + captures: Option<&Captures<'_>>, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = captures.map_or(0, eval_preg_visible_capture_len); + let mut result = values.array_new(len)?; + if let Some(captures) = captures { + for index in 0..len { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let bytes = eval_preg_capture_bytes(subject, captures, index).unwrap_or(b""); + let value = values.string_bytes_value(bytes)?; + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Returns the capture count PHP should expose, dropping trailing unmatched groups. +fn eval_preg_visible_capture_len(captures: &Captures<'_>) -> usize { + let mut len = captures.len(); + while len > 1 && captures.get(len - 1).is_none() { + len -= 1; + } + len +} + +/// Returns one captured byte range from the original subject. +fn eval_preg_capture_bytes<'a>( + subject: &'a [u8], + captures: &Captures<'_>, + index: usize, +) -> Option<&'a [u8]> { + captures + .get(index) + .map(|matched| &subject[matched.start()..matched.end()]) +} + +/// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. +fn eval_preg_expand_replacement( + replacement: &[u8], + subject: &[u8], + captures: &Captures<'_>, + result: &mut Vec, +) { + let mut index = 0; + while index < replacement.len() { + match replacement[index] { + b'$' => { + if let Some((capture_index, next_index)) = + eval_preg_replacement_capture_index(replacement, index + 1) + { + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } else { + result.push(replacement[index]); + index += 1; + } + } + b'\\' if index + 1 < replacement.len() && replacement[index + 1].is_ascii_digit() => { + let (capture_index, next_index) = + eval_preg_decimal_capture_index(replacement, index + 1); + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } + byte => { + result.push(byte); + index += 1; + } + } + } +} + +/// Parses a dollar-style replacement capture reference. +fn eval_preg_replacement_capture_index(bytes: &[u8], index: usize) -> Option<(usize, usize)> { + if bytes.get(index).copied() == Some(b'{') { + let mut cursor = index + 1; + let start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + if cursor == start || bytes.get(cursor).copied() != Some(b'}') { + return None; + } + let capture = eval_preg_decimal_bytes_to_usize(&bytes[start..cursor])?; + return Some((capture, cursor + 1)); + } + if bytes.get(index).is_some_and(u8::is_ascii_digit) { + let (capture, next) = eval_preg_decimal_capture_index(bytes, index); + return Some((capture, next)); + } + None +} + +/// Parses a one- or two-digit replacement capture reference. +fn eval_preg_decimal_capture_index(bytes: &[u8], index: usize) -> (usize, usize) { + let mut cursor = index; + let end = usize::min(bytes.len(), index + 2); + while cursor < end && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + ( + eval_preg_decimal_bytes_to_usize(&bytes[index..cursor]).unwrap_or(0), + cursor, + ) +} + +/// Converts ASCII decimal bytes into a `usize` capture index. +fn eval_preg_decimal_bytes_to_usize(bytes: &[u8]) -> Option { + let mut value = 0usize; + for byte in bytes { + value = value.checked_mul(10)?; + value = value.checked_add(usize::from(byte - b'0'))?; + } + Some(value) +} + +/// Returns the PHP `preg_split()` limit, treating zero as unlimited. +fn eval_preg_split_limit( + limit: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(limit) = limit else { + return Ok(None); + }; + let limit = eval_int_value(limit, values)?; + if limit <= 0 { + return Ok(None); + } + usize::try_from(limit) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns supported `preg_split()` flags and rejects offset-capture shape changes. +fn eval_preg_split_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE; + if flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0 || flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Returns whether `preg_split()` should stop splitting and emit the remaining subject. +fn eval_preg_split_reached_limit(pieces: &[Vec], limit: Option) -> bool { + matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) +} + +/// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. +fn eval_preg_split_push_piece(pieces: &mut Vec>, piece: &[u8], no_empty: bool) { + if no_empty && piece.is_empty() { + return; + } + pieces.push(piece.to_vec()); +} + +/// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. +fn eval_preg_split_push_captures( + pieces: &mut Vec>, + subject: &[u8], + captures: &Captures<'_>, + no_empty: bool, +) { + for index in 1..captures.len() { + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, index) { + eval_preg_split_push_piece(pieces, bytes, no_empty); + } + } +} + /// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. fn eval_builtin_gethostbyaddr( args: &[EvalExpr], @@ -12744,6 +13337,19 @@ fn eval_predefined_constant_value(name: &str) -> Option "ARRAY_FILTER_USE_KEY" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_KEY)), "COUNT_NORMAL" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_NORMAL)), "COUNT_RECURSIVE" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_RECURSIVE)), + "PREG_SPLIT_NO_EMPTY" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_NO_EMPTY)), + "PREG_SPLIT_DELIM_CAPTURE" => { + Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_DELIM_CAPTURE)) + } + "PREG_SPLIT_OFFSET_CAPTURE" => { + Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_OFFSET_CAPTURE)) + } + "PREG_PATTERN_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_PATTERN_ORDER)), + "PREG_SET_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SET_ORDER)), + "PREG_OFFSET_CAPTURE" => Some(EvalPredefinedConstant::Int(EVAL_PREG_OFFSET_CAPTURE)), + "PREG_UNMATCHED_AS_NULL" => { + Some(EvalPredefinedConstant::Int(EVAL_PREG_UNMATCHED_AS_NULL)) + } "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), @@ -16160,6 +16766,41 @@ return function_exists("str_ireplace");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. + #[test] + fn execute_program_dispatches_preg_builtins() { + let program = parse_fragment( + br#"$ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); +echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; +echo preg_match("/xyz/", "id42") . ":"; +echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; +echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; +function eval_regex_wrap($matches) { return "[" . $matches[0] . "]"; } +echo preg_replace_callback("/[A-Z]/", "eval_regex_wrap", "AB") . ":"; +$limited = preg_split("/,/", "a,b,c", 2); +echo count($limited) . ":" . $limited[0] . ":" . $limited[1] . ":"; +$kept = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY); +echo count($kept) . ":" . $kept[1] . ":"; +echo call_user_func("preg_match", "/x/", "x") . ":"; +$replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "replacement" => "N", "subject" => "a12"]); +echo $replaced . ":"; +$captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); +echo count($captured) . ":" . $captured[1] . ":"; +return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:3:id42:id:42:0:3:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. #[test] fn execute_program_dispatches_html_entity_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c3b006184a..18c7bf3b2a 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -65,6 +65,8 @@ Eval `array_filter()` implements PHP's omitted/null callback form by iterating s Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false; Traversable object dispatch for eval `iterator_apply()` remains future work. +Eval `preg_match()`, count-only `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers and capture/replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax or offset-capture result shapes surface as eval runtime fatals instead of falling back to a partial native ABI. + Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. @@ -91,7 +93,7 @@ Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_ Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. -Eval predefined constants bypass the dynamic context and are materialized directly by the interpreter. The current eval-visible set is `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, and `ARRAY_FILTER_USE_*`; `defined()` recognizes them and duplicate `define()` attempts follow the same warning path as dynamic constants. +Eval predefined constants bypass the dynamic context and are materialized directly by the interpreter. The current eval-visible set is `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, `COUNT_*`, and the supported `PREG_*` constants; `defined()` recognizes them and duplicate `define()` attempts follow the same warning path as dynamic constants. Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 638ffb9402..73aa80e33e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. @@ -76,6 +76,8 @@ Eval `array_filter()` supports the PHP default omitted/null callback form, filte Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` still requires the Traversable object path and is not implemented for arrays, matching PHP's array `TypeError` rather than treating arrays as Traversable. +Eval regex dispatch supports `preg_match()`, count-only `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; offset-capture flags and PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). + Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. @@ -104,7 +106,7 @@ Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and Eval path component calls include `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` with suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, named arguments, callable dispatch, and `function_exists()` probes. -Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, and `ARRAY_FILTER_USE_*`. `defined()` sees these names, including an optional leading `\`, and `define()` cannot replace them. +Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, `COUNT_*`, and the supported `PREG_*` constants. `defined()` sees these names, including an optional leading `\`, and `define()` cannot replace them. Eval `realpath()` canonicalizes existing paths through the host filesystem and returns `false` when the path cannot be resolved. diff --git a/examples/eval/main.php b/examples/eval/main.php index 2c3d01e7d2..751f9b6c4f 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -152,6 +152,7 @@ public function __construct(int $value) { $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); $iterator_arrays = eval('$items = ["x" => 1, "y" => 2]; $copy = iterator_to_array($items); $values = iterator_to_array($items, false); return iterator_count($items) . ":" . $copy["x"] . ":" . $values[0];'); +$regex_builtins = eval('function eval_example_regex_wrap($matches) { return "[" . $matches[0] . "]"; } $ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); $parts = preg_split("/,/", "a,b,c", 2); return $ok . ":" . $matches[1] . ":" . preg_replace("/[0-9]+/", "N", "a12") . ":" . preg_replace_callback("/[A-Z]/", "eval_example_regex_wrap", "AB") . ":" . $parts[1];'); $mixed_literal = eval('return [2 => "two", "tail"][3] . ":" . (["2" => "two", "next"][3]);'); $append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); $append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); @@ -302,6 +303,7 @@ public function __construct(int $value) { echo "named-builtins=" . $named_builtins . "\n"; echo "array-projection=" . $array_projection . "\n"; echo "iterator-arrays=" . $iterator_arrays . "\n"; +echo "regex-builtins=" . $regex_builtins . "\n"; echo "mixed-literal=" . $mixed_literal . "\n"; echo "append-items=" . $append_items . "\n"; echo "append-assoc=" . $append_assoc . "\n"; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5158608837..ab9fc6553d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2271,6 +2271,36 @@ echo ":"; echo function_exists("realpath");'); assert_eq!(out, "resolved:false:call:array-false:1"); } +/// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. +#[test] +fn test_eval_dispatches_preg_builtin_calls() { + let out = compile_and_run( + r#" "/[0-9]+/", "replacement" => "N", "subject" => "a12"]); +echo $replaced . ":"; +$captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); +echo count($captured) . ":" . $captured[1] . ":"; +echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY");'); +"#, + ); + assert_eq!( + out, + "1:3:id42:id:42:0:3:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:1" + ); +} + /// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. #[test] fn test_eval_dispatches_fnmatch_builtin_call() { From ad391d1ee34dede6fc21a0f688fe8ea268d8af33 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 17:28:46 +0200 Subject: [PATCH 0217/1208] Test no-eval programs skip eval bridge --- tests/codegen/eval.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ab9fc6553d..8899f28f59 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -49,6 +49,27 @@ fn test_eval_codegen_requires_eval_bridge() { let _ = fs::remove_dir_all(&dir); } +/// Verifies programs without `eval` do not link or reference the optional eval bridge. +#[test] +fn test_non_eval_program_does_not_request_eval_bridge() { + let dir = make_cli_test_dir("elephc_no_eval_bridge_asm"); + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(" Date: Tue, 16 Jun 2026 17:31:25 +0200 Subject: [PATCH 0218/1208] Test eval bridge env override --- src/linker.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/linker.rs b/src/linker.rs index 8d3d4cc19f..8b198bf6c7 100644 --- a/src/linker.rs +++ b/src/linker.rs @@ -729,6 +729,10 @@ fn macos_sdk_version() -> String { #[cfg(test)] mod tests { use super::*; + use std::sync::{Mutex, OnceLock}; + + /// Serializes tests that temporarily modify process environment variables. + static ENV_LOCK: OnceLock> = OnceLock::new(); /// Verifies an empty or whitespace-only SDK path (xcrun missing or misconfigured) /// yields an actionable Xcode Command Line Tools hint instead of being silently @@ -829,4 +833,28 @@ mod tests { assert!(crate_flag_names().contains(&"pdo")); assert_eq!(crate_flag_names().len(), BRIDGES.len()); } + + /// Verifies the eval bridge honors `ELEPHC_EVAL_LIB_DIR` before filesystem discovery. + #[test] + fn eval_bridge_lib_dir_uses_env_override() { + let _guard = ENV_LOCK + .get_or_init(|| Mutex::new(())) + .lock() + .expect("env lock should not be poisoned"); + let previous = std::env::var_os("ELEPHC_EVAL_LIB_DIR"); + let override_dir = "/tmp/elephc-eval-lib-dir-override"; + std::env::set_var("ELEPHC_EVAL_LIB_DIR", override_dir); + let entry = BRIDGES + .iter() + .find(|b| b.lib_name == "elephc_eval") + .expect("elephc_eval must be a registered bridge"); + + let resolved = entry.lib_dir(); + + match previous { + Some(value) => std::env::set_var("ELEPHC_EVAL_LIB_DIR", value), + None => std::env::remove_var("ELEPHC_EVAL_LIB_DIR"), + } + assert_eq!(resolved.as_deref(), Some(override_dir)); + } } From b2eafef06f9f74bef000b11872a7b41537bf06d0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 17:32:39 +0200 Subject: [PATCH 0219/1208] Test eval parse-error diagnostic --- tests/codegen/eval.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8899f28f59..747c336c21 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3025,6 +3025,16 @@ fn test_eval_missing_dynamic_constant_fetch_fails() { ); } +/// Verifies invalid eval fragments report the dedicated parse-error diagnostic. +#[test] +fn test_eval_parse_error_reports_eval_parse_diagnostic() { + let err = compile_and_run_expect_failure(" Date: Tue, 16 Jun 2026 17:34:40 +0200 Subject: [PATCH 0220/1208] Test eval mutates native array locals --- tests/codegen/eval.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 747c336c21..a4e39534cf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -620,6 +620,19 @@ echo $items[0] . $items[1]; assert_eq!(out, "ab"); } +/// Verifies eval mutates an existing native local array instead of replacing it with a fresh one. +#[test] +fn test_eval_mutates_existing_native_array_local() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 17:39:22 +0200 Subject: [PATCH 0221/1208] Support eval preg_match_all captures --- crates/elephc-eval/src/interpreter.rs | 85 ++++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- examples/eval/main.php | 2 +- tests/codegen/eval.rs | 6 +- 5 files changed, 85 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 8d4be5990a..f871122063 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -9909,19 +9909,40 @@ fn eval_preg_match_capture_result( Ok((matched, matches)) } -/// Evaluates PHP `preg_match_all()` count-only calls over eval expressions. +/// Evaluates PHP `preg_match_all()` over eval expressions. fn eval_builtin_preg_match_all( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [pattern, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_match_all_result(pattern, subject, values) + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_all_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } } /// Counts all non-overlapping regex matches in one subject string. @@ -9936,6 +9957,50 @@ fn eval_preg_match_all_result( values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) } +/// Returns the match count plus PHP's default `PREG_PATTERN_ORDER` `$matches` array. +fn eval_preg_match_all_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let capture_count = regex.captures_len(); + let subject = values.string_bytes(subject)?; + let captures: Vec> = regex.captures_iter(&subject).collect(); + let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let matches = eval_preg_match_all_pattern_order_array( + &subject, + &captures, + capture_count, + values, + )?; + Ok((count, matches)) +} + +/// Builds PHP's default `preg_match_all()` pattern-order capture matrix. +fn eval_preg_match_all_pattern_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut outer = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let mut row = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let key = values + .int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let bytes = eval_preg_capture_bytes(subject, capture, capture_index).unwrap_or(b""); + let value = values.string_bytes_value(bytes)?; + row = values.array_set(row, key, value)?; + } + let key = values + .int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + /// Evaluates PHP `preg_replace()` over eval expressions. fn eval_builtin_preg_replace( args: &[EvalExpr], @@ -16774,6 +16839,10 @@ return function_exists("str_ireplace");"#, echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; echo preg_match("/xyz/", "id42") . ":"; echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; +$allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); +echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; +preg_match_all("/(x)(y)/", "abc", $none); +echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; function eval_regex_wrap($matches) { return "[" . $matches[0] . "]"; } echo preg_replace_callback("/[A-Z]/", "eval_regex_wrap", "AB") . ":"; @@ -16796,7 +16865,7 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun assert_eq!( values.output, - "1:3:id42:id:42:0:3:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:" + "1:3:id42:id:42:0:3:2:3:b22:a:22:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 18c7bf3b2a..197c9625b9 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -65,7 +65,7 @@ Eval `array_filter()` implements PHP's omitted/null callback form by iterating s Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false; Traversable object dispatch for eval `iterator_apply()` remains future work. -Eval `preg_match()`, count-only `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers and capture/replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax or offset-capture result shapes surface as eval runtime fatals instead of falling back to a partial native ABI. +Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax, offset-capture result shapes, or `preg_match_all()` set-order output surface as eval runtime fatals instead of falling back to a partial native ABI. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 73aa80e33e..fd60a6d901 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -76,7 +76,7 @@ Eval `array_filter()` supports the PHP default omitted/null callback form, filte Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` still requires the Traversable object path and is not implemented for arrays, matching PHP's array `TypeError` rather than treating arrays as Traversable. -Eval regex dispatch supports `preg_match()`, count-only `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; offset-capture flags and PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). +Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; offset-capture flags, `preg_match_all()` set-order output, and PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/examples/eval/main.php b/examples/eval/main.php index 751f9b6c4f..d0ec2d80d6 100644 --- a/examples/eval/main.php +++ b/examples/eval/main.php @@ -152,7 +152,7 @@ public function __construct(int $value) { $named_builtins = eval('return strlen(string: "eval") . ":" . (str_contains(...["haystack" => "dynamic eval", "needle" => "eval"]) ? "yes" : "no");'); $array_projection = eval('$vals = array_values(["a" => 10, "b" => 20]); $keys = array_keys(["a" => 10, "b" => 20]); return $keys[0] . ":" . $vals[1];'); $iterator_arrays = eval('$items = ["x" => 1, "y" => 2]; $copy = iterator_to_array($items); $values = iterator_to_array($items, false); return iterator_count($items) . ":" . $copy["x"] . ":" . $values[0];'); -$regex_builtins = eval('function eval_example_regex_wrap($matches) { return "[" . $matches[0] . "]"; } $ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); $parts = preg_split("/,/", "a,b,c", 2); return $ok . ":" . $matches[1] . ":" . preg_replace("/[0-9]+/", "N", "a12") . ":" . preg_replace_callback("/[A-Z]/", "eval_example_regex_wrap", "AB") . ":" . $parts[1];'); +$regex_builtins = eval('function eval_example_regex_wrap($matches) { return "[" . $matches[0] . "]"; } $ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); $all_count = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); $parts = preg_split("/,/", "a,b,c", 2); return $ok . ":" . $matches[1] . ":" . $all_count . ":" . $all[0][1] . ":" . $all[2][1] . ":" . preg_replace("/[0-9]+/", "N", "a12") . ":" . preg_replace_callback("/[A-Z]/", "eval_example_regex_wrap", "AB") . ":" . $parts[1];'); $mixed_literal = eval('return [2 => "two", "tail"][3] . ":" . (["2" => "two", "next"][3]);'); $append_items = eval('$items = []; $items[] = "left"; $items[] = "right"; return $items[0] . ":" . $items[1] . ":" . count($items);'); $append_assoc = eval('$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a4e39534cf..8e03c13507 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2314,6 +2314,10 @@ eval('$ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; echo preg_match("/xyz/", "id42") . ":"; echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; +$allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); +echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; +preg_match_all("/(x)(y)/", "abc", $none); +echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; function eval_regex_wrap($matches) { return "[" . $matches[0] . "]"; } echo preg_replace_callback("/[A-Z]/", "eval_regex_wrap", "AB") . ":"; @@ -2331,7 +2335,7 @@ echo function_exists("preg_match") && function_exists("preg_match_all") && funct ); assert_eq!( out, - "1:3:id42:id:42:0:3:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:1" + "1:3:id42:id:42:0:3:2:3:b22:a:22:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:1" ); } From 7a4e7dd8a6de09b2131bb42b6552e5345fcf68d6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 17:57:55 +0200 Subject: [PATCH 0222/1208] Support eval throwable propagation --- crates/elephc-eval/src/context.rs | 13 ++ crates/elephc-eval/src/eval_ir.rs | 1 + crates/elephc-eval/src/interpreter.rs | 177 +++++++++++++++++++++++- crates/elephc-eval/src/lib.rs | 38 ++++- crates/elephc-eval/src/parser.rs | 26 +++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen/lower_inst/builtins/eval.rs | 24 ++++ tests/codegen/eval.rs | 51 +++++++ 9 files changed, 318 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 439e9813cc..5f4d660ec9 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -93,6 +93,7 @@ pub struct ElephcEvalContext { static_locals: HashMap<(String, String), RuntimeCellHandle>, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, + pending_throw: Option, call_file: String, call_dir: String, call_line: i64, @@ -110,6 +111,7 @@ impl ElephcEvalContext { static_locals: HashMap::new(), global_scope: None, function_stack: Vec::new(), + pending_throw: None, call_file: String::new(), call_dir: String::new(), call_line: 0, @@ -128,6 +130,7 @@ impl ElephcEvalContext { static_locals: HashMap::new(), global_scope: None, function_stack: Vec::new(), + pending_throw: None, call_file: String::new(), call_dir: String::new(), call_line: 0, @@ -280,6 +283,16 @@ impl ElephcEvalContext { self.function_stack.last().map(String::as_str) } + /// Records a Throwable cell that escaped from an eval-executed function call. + pub fn set_pending_throw(&mut self, value: RuntimeCellHandle) { + self.pending_throw = Some(value); + } + + /// Returns and clears the Throwable cell currently escaping through eval. + pub fn take_pending_throw(&mut self) -> Option { + self.pending_throw.take() + } + /// Updates the source file, directory, and line for the current eval call site. pub fn set_call_site(&mut self, file: impl Into, dir: impl Into, line: i64) { self.call_file = file.into(); diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index aa73394ed0..babc03d089 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -112,6 +112,7 @@ pub enum EvalStmt { expr: EvalExpr, cases: Vec, }, + Throw(EvalExpr), UnsetVar { name: String, }, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f871122063..d87fd6b9d8 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -33,10 +33,17 @@ use std::time::{SystemTime, UNIX_EPOCH}; enum EvalControl { None, Return(RuntimeCellHandle), + Throw(RuntimeCellHandle), Break, Continue, } +/// Final result of executing a parsed eval program. +pub enum EvalOutcome { + Value(RuntimeCellHandle), + Throwable(RuntimeCellHandle), +} + /// One already evaluated function-like call argument. struct EvaluatedCallArg { name: Option, @@ -580,10 +587,32 @@ pub fn execute_program_with_context( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - match execute_statements(program.statements(), context, scope, values)? { - EvalControl::None => values.null(), - EvalControl::Return(result) => Ok(result), - EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + match execute_program_outcome_with_context(context, program, scope, values)? { + EvalOutcome::Value(result) => Ok(result), + EvalOutcome::Throwable(error) => { + context.set_pending_throw(error); + Err(EvalStatus::UncaughtThrowable) + } + } +} + +/// Executes an EvalIR program and preserves escaping Throwable cells. +pub fn execute_program_outcome_with_context( + context: &mut ElephcEvalContext, + program: &EvalProgram, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_statements(program.statements(), context, scope, values) { + Ok(EvalControl::None) => values.null().map(EvalOutcome::Value), + Ok(EvalControl::Return(result)) => Ok(EvalOutcome::Value(result)), + Ok(EvalControl::Throw(result)) => Ok(EvalOutcome::Throwable(result)), + Ok(EvalControl::Break | EvalControl::Continue) => Err(EvalStatus::UnsupportedConstruct), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), } } @@ -603,11 +632,34 @@ pub fn execute_context_function( args: Vec, values: &mut impl RuntimeValueOps, ) -> Result { + match execute_context_function_outcome(context, name, args, values)? { + EvalOutcome::Value(result) => Ok(result), + EvalOutcome::Throwable(error) => { + context.set_pending_throw(error); + Err(EvalStatus::UncaughtThrowable) + } + } +} + +/// Executes a function declared in the shared eval context and preserves thrown cells. +pub fn execute_context_function_outcome( + context: &mut ElephcEvalContext, + name: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { context .function(name) .cloned() .map_or(Err(EvalStatus::UnsupportedConstruct), |function| { - eval_dynamic_function_with_values(&function, args, context, values) + match eval_dynamic_function_with_values(&function, args, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } }) } @@ -618,11 +670,34 @@ pub fn execute_context_function_call_array( arg_array: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { + match execute_context_function_call_array_outcome(context, name, arg_array, values)? { + EvalOutcome::Value(result) => Ok(result), + EvalOutcome::Throwable(error) => { + context.set_pending_throw(error); + Err(EvalStatus::UncaughtThrowable) + } + } +} + +/// Executes a named eval-context callable from an argument array and preserves thrown cells. +pub fn execute_context_function_call_array_outcome( + context: &mut ElephcEvalContext, + name: &str, + arg_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { if !values.is_array_like(arg_array)? { return Err(EvalStatus::RuntimeFatal); } let evaluated_args = eval_array_call_arg_values(arg_array, values)?; - eval_callable_with_call_array_args(name, evaluated_args, context, values) + match eval_callable_with_call_array_args(name, evaluated_args, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } } /// Executes statements in source order and propagates the first eval `return`. @@ -912,6 +987,13 @@ fn execute_stmt( EvalStmt::Switch { expr, cases } => { execute_switch_stmt(expr, cases, context, scope, values) } + EvalStmt::Throw(expr) => { + let thrown = eval_expr(expr, context, scope, values)?; + if values.type_tag(thrown)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + Ok(EvalControl::Throw(thrown)) + } EvalStmt::UnsetVar { name } => { if let Some(replaced) = unset_scope_cell(scope, name.clone()) { values.release(replaced)?; @@ -926,6 +1008,7 @@ fn execute_stmt( match execute_statements(body, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } } @@ -1018,6 +1101,7 @@ fn execute_switch_stmt( match execute_statements(&case.body, context, scope, values)? { EvalControl::None => {} EvalControl::Break | EvalControl::Continue => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } } @@ -1036,6 +1120,7 @@ fn execute_do_while_stmt( match execute_statements(body, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } let condition = eval_expr(condition, context, scope, values)?; @@ -1059,6 +1144,7 @@ fn execute_for_stmt( match execute_statements(init, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => return Ok(EvalControl::None), + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } loop { @@ -1071,11 +1157,13 @@ fn execute_for_stmt( match execute_statements(body, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } match execute_statements(update, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } } @@ -1122,6 +1210,7 @@ fn execute_foreach_stmt( match execute_statements(body, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } } @@ -13097,6 +13186,10 @@ fn eval_dynamic_function_with_values( match result? { EvalControl::None => values.null(), EvalControl::Return(result) => Ok(result), + EvalControl::Throw(result) => { + context.set_pending_throw(result); + Err(EvalStatus::UncaughtThrowable) + } EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), } } @@ -13165,6 +13258,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::ReferenceAssign { .. } | EvalStmt::Return(_) | EvalStmt::StoreVar { .. } + | EvalStmt::Throw(_) | EvalStmt::UnsetVar { .. } => {} } } @@ -14346,6 +14440,77 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies eval `throw` exits the program with a retained Throwable cell. + #[test] + fn execute_program_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = execute_program_outcome_with_context( + &mut context, + &program, + &mut scope, + &mut values, + ) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)); + } + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + } + + /// Verifies throws from eval-declared functions escape through the shared context. + #[test] + fn execute_context_function_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"function dyn($e) { throw $e; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let thrown = values.new_object("Exception").expect("allocate fake exception"); + + let outcome = + execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) + .expect("throw should be an eval function outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + } + + /// Verifies nested eval preserves the thrown cell while returning an uncaught status. + #[test] + fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { + let program = parse_fragment(br#"eval("throw $e;");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let thrown = values.new_object("Exception").expect("allocate fake exception"); + scope.set("e", thrown, ScopeCellOwnership::Borrowed); + + let outcome = execute_program_outcome_with_context( + &mut context, + &program, + &mut scope, + &mut values, + ) + .expect("nested throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + } + /// Verifies simple variable compound assignments read, compute, and write the scope value. #[test] fn execute_program_evaluates_compound_assignments() { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index ca393d50c7..1fd0374e31 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -31,7 +31,7 @@ use abi::{ use context::{NativeFunction, NativeFunctionInvoker}; use errors::EvalStatus; #[cfg(not(test))] -use interpreter::RuntimeValueOps; +use interpreter::{EvalOutcome, RuntimeValueOps}; #[cfg(not(test))] use runtime_hooks::ElephcRuntimeOps; use scope::{ScopeCellOwnership, ScopeEntry}; @@ -719,13 +719,13 @@ unsafe fn call_eval_function_inner( (*out).clear(); } let mut values = ElephcRuntimeOps::new(); - match interpreter::execute_context_function( + match interpreter::execute_context_function_outcome( context, &name.to_ascii_lowercase(), args, &mut values, ) { - Ok(result) => { + Ok(EvalOutcome::Value(result)) => { if !out.is_null() { (*out).kind = 0; (*out).value_cell = result.as_ptr(); @@ -733,6 +733,14 @@ unsafe fn call_eval_function_inner( } EvalStatus::Ok.code() } + Ok(EvalOutcome::Throwable(error)) => { + if !out.is_null() { + (*out).kind = 3; + (*out).value_cell = std::ptr::null_mut(); + (*out).error = error.as_ptr(); + } + EvalStatus::UncaughtThrowable.code() + } Err(status) => status.code(), } } @@ -766,13 +774,13 @@ unsafe fn call_eval_function_array_inner( (*out).clear(); } let mut values = ElephcRuntimeOps::new(); - match interpreter::execute_context_function_call_array( + match interpreter::execute_context_function_call_array_outcome( context, &name.to_ascii_lowercase(), RuntimeCellHandle::from_raw(arg_array), &mut values, ) { - Ok(result) => { + Ok(EvalOutcome::Value(result)) => { if !out.is_null() { (*out).kind = 0; (*out).value_cell = result.as_ptr(); @@ -780,6 +788,14 @@ unsafe fn call_eval_function_array_inner( } EvalStatus::Ok.code() } + Ok(EvalOutcome::Throwable(error)) => { + if !out.is_null() { + (*out).kind = 3; + (*out).value_cell = std::ptr::null_mut(); + (*out).error = error.as_ptr(); + } + EvalStatus::UncaughtThrowable.code() + } Err(status) => status.code(), } } @@ -846,8 +862,8 @@ unsafe fn execute_parsed_eval( &mut fallback_scope }; let mut values = ElephcRuntimeOps::new(); - match interpreter::execute_program_with_context(context, program, scope, &mut values) { - Ok(result) => { + match interpreter::execute_program_outcome_with_context(context, program, scope, &mut values) { + Ok(EvalOutcome::Value(result)) => { if !out.is_null() { (*out).kind = 0; (*out).value_cell = result.as_ptr(); @@ -855,6 +871,14 @@ unsafe fn execute_parsed_eval( } EvalStatus::Ok.code() } + Ok(EvalOutcome::Throwable(error)) => { + if !out.is_null() { + (*out).kind = 3; + (*out).value_cell = std::ptr::null_mut(); + (*out).error = error.as_ptr(); + } + EvalStatus::UncaughtThrowable.code() + } Err(status) => status.code(), } } diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index ec35b2c1ab..2a1d10bda3 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -590,6 +590,7 @@ impl Parser { } TokenKind::Ident(name) if ident_eq(name, "static") => self.parse_static_var_stmt(), TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), + TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { @@ -773,6 +774,14 @@ impl Parser { Ok(vec![EvalStmt::StaticVar { name, init }]) } + /// Parses `throw expr;` statements in eval fragments. + fn parse_throw_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Throw(expr)]) + } + /// Parses a dynamic function declaration parameter list after `(`. fn parse_function_params(&mut self) -> Result, EvalParseError> { let mut params = Vec::new(); @@ -1799,7 +1808,6 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { "require_once", "include", "include_once", - "throw", "trait", "try", "use", @@ -3039,6 +3047,22 @@ mod tests { ); } + /// Verifies throw statements lower to a Throwable expression carried by EvalIR. + #[test] + fn parse_fragment_accepts_throw_source() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })] + ); + } + /// Verifies unset fragments expand to one by-name unset statement per variable. #[test] fn parse_fragment_accepts_unset_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 197c9625b9..409d85b6f0 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index fd60a6d901..da284d913b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -114,7 +114,7 @@ Eval realpath-cache calls match elephc's native no-cache convention: `realpath_c Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index c47aca3bb7..6a7b02d859 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -29,6 +29,7 @@ use super::super::{ }; const EVAL_STATUS_PARSE_ERROR: i64 = 1; +const EVAL_STATUS_UNCAUGHT_THROWABLE: i64 = 3; const EVAL_STATUS_UNSUPPORTED: i64 = 4; const EVAL_PARSE_ERROR_MESSAGE: &str = "Parse error: eval() fragment is invalid\n"; const EVAL_UNSUPPORTED_MESSAGE: &str = @@ -36,6 +37,7 @@ const EVAL_UNSUPPORTED_MESSAGE: &str = const EVAL_RUNTIME_FATAL_MESSAGE: &str = "Fatal error: eval() runtime failed\n"; const EVAL_STACK_BYTES: usize = 80; const EVAL_RESULT_VALUE_CELL_OFFSET: usize = 8; +const EVAL_RESULT_ERROR_OFFSET: usize = 16; const EVAL_CONTEXT_HANDLE_OFFSET: usize = 24; const EVAL_SCOPE_HANDLE_OFFSET: usize = 32; const EVAL_TEMP_CELL_OFFSET: usize = 40; @@ -1265,13 +1267,17 @@ fn store_missing_scope_entry_to_global( fn emit_eval_status_check(ctx: &mut FunctionContext<'_>) { let ok_label = ctx.next_label("eval_status_ok"); let parse_error_label = ctx.next_label("eval_status_parse_error"); + let throwable_label = ctx.next_label("eval_status_throwable"); let unsupported_label = ctx.next_label("eval_status_unsupported"); abi::emit_branch_if_int_result_zero(ctx.emitter, &ok_label); emit_branch_if_eval_status(ctx, EVAL_STATUS_PARSE_ERROR, &parse_error_label); + emit_branch_if_eval_status(ctx, EVAL_STATUS_UNCAUGHT_THROWABLE, &throwable_label); emit_branch_if_eval_status(ctx, EVAL_STATUS_UNSUPPORTED, &unsupported_label); emit_eval_fatal_message(ctx, EVAL_RUNTIME_FATAL_MESSAGE); ctx.emitter.label(&parse_error_label); emit_eval_fatal_message(ctx, EVAL_PARSE_ERROR_MESSAGE); + ctx.emitter.label(&throwable_label); + emit_eval_throw_current(ctx); ctx.emitter.label(&unsupported_label); emit_eval_fatal_message(ctx, EVAL_UNSUPPORTED_MESSAGE); ctx.emitter.label(&ok_label); @@ -1294,6 +1300,24 @@ fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: } } +/// Publishes an eval-thrown Throwable and enters the normal runtime unwinder. +fn emit_eval_throw_current(ctx: &mut FunctionContext<'_>) { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_ERROR_OFFSET); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + let object_reg = eval_mixed_unbox_low_payload_reg(ctx); + abi::emit_store_reg_to_symbol(ctx.emitter, object_reg, "_exc_value", 0); + abi::emit_call_label(ctx.emitter, "__rt_throw_current"); +} + +/// Returns the low payload register produced by `__rt_mixed_unbox` for eval status handling. +fn eval_mixed_unbox_low_payload_reg(ctx: &FunctionContext<'_>) -> &'static str { + match ctx.emitter.target.arch { + Arch::AArch64 => "x1", + Arch::X86_64 => "rdi", + } +} + /// Emits an eval diagnostic message and exits the process. fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8e03c13507..5f15043c8d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4290,3 +4290,54 @@ fn test_eval_fragment_with_php_opening_tag_reports_parse_error() { "stderr did not contain eval parse diagnostic: {err}" ); } + +/// Verifies Throwable objects thrown inside eval cross into the caller's catch block. +#[test] +fn test_eval_throw_crosses_caller_try_catch() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!(out, "caught:eval boom"); +} + +/// Verifies Throwable objects thrown by eval-declared functions cross native call sites. +#[test] +fn test_eval_declared_function_throw_crosses_native_try_catch() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!(out, "caught:dyn boom"); +} + +/// Verifies Throwable objects thrown by nested eval calls keep the original catch target. +#[test] +fn test_eval_nested_throw_crosses_caller_try_catch() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!(out, "caught:nested boom"); +} From 7b5c9b452f6f5d49dad3053dfc3ea75021d955c1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 18:08:32 +0200 Subject: [PATCH 0223/1208] Support eval variable callable calls --- crates/elephc-eval/src/eval_ir.rs | 4 ++ crates/elephc-eval/src/interpreter.rs | 92 +++++++++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 49 +++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 40 ++++++++++++ 6 files changed, 186 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index babc03d089..c720b34d84 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -171,6 +171,10 @@ pub enum EvalExpr { }, Const(EvalConst), ConstFetch(String), + DynamicCall { + callee: Box, + args: Vec, + }, LoadVar(String), MethodCall { object: Box, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d87fd6b9d8..b784ba5f78 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1270,6 +1270,9 @@ fn eval_expr( EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), + EvalExpr::DynamicCall { callee, args } => { + eval_dynamic_call(callee, args, context, scope, values) + } EvalExpr::LoadVar(name) => { visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) } @@ -1492,6 +1495,20 @@ fn eval_call( Err(EvalStatus::UnsupportedConstruct) } +/// Evaluates a variable or expression callable and dispatches it with source-order arguments. +fn eval_dynamic_call( + callee: &EvalExpr, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_expr(callee, context, scope, values)?; + let callback = eval_callable_name(callback, values)?; + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_callable_with_call_array_args(&callback, evaluated_args, context, values) +} + /// Returns true for language constructs that need unevaluated argument expressions. fn eval_expr_language_construct_name(name: &str) -> bool { matches!(name, "empty" | "eval" | "isset") @@ -15682,6 +15699,81 @@ return call_user_func("dyn", 4);"#, assert_eq!(result, expected); } + /// Verifies string variable calls inside eval can dispatch a supported builtin. + #[test] + fn execute_program_variable_call_dispatches_builtin() { + let program = parse_fragment( + br#"$fn = "strlen"; +return $fn("abcd");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + } + + /// Verifies callable array entries can be invoked through postfix dynamic calls. + #[test] + fn execute_program_postfix_variable_call_dispatches_builtin() { + let program = parse_fragment( + br#"$callbacks = ["strlen"]; +return $callbacks[0]("abc");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(3)); + } + + /// Verifies variable calls bind eval-declared function arguments by name. + #[test] + fn execute_program_variable_call_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } +$fn = "dyn"; +return $fn(y: 2, x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies variable calls can dispatch registered native functions with named args. + #[test] + fn execute_program_variable_call_binds_registered_native_named_args() { + let program = parse_fragment( + br#"$fn = "native_answer"; +return $fn(right: 2, left: 1);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. #[test] fn execute_program_call_user_func_array_dispatches_declared_function() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 2a1d10bda3..1c1342ea11 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1429,10 +1429,18 @@ impl Parser { Ok(expr) } - /// Parses postfix array reads, property reads, and method calls after a primary expression. + /// Parses postfix array reads, property reads, method calls, and dynamic calls. fn parse_postfix(&mut self) -> Result { let mut expr = self.parse_primary()?; loop { + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + expr = EvalExpr::DynamicCall { + callee: Box::new(expr), + args, + }; + continue; + } if self.consume(TokenKind::LBracket) { let index = self.parse_expr()?; self.expect(TokenKind::RBracket)?; @@ -2694,6 +2702,45 @@ mod tests { ); } + /// Verifies variable callable expressions lower to dynamic calls with source-order args. + #[test] + fn parse_fragment_accepts_dynamic_call_expression_source() { + let program = parse_fragment(br#"return $fn(first: "a", ...$rest);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicCall { + callee: Box::new(EvalExpr::LoadVar("fn".to_string())), + args: vec![ + EvalCallArg::named( + "first", + EvalExpr::Const(EvalConst::String("a".to_string())), + ), + EvalCallArg::spread(EvalExpr::LoadVar("rest".to_string())), + ], + }))] + ); + } + + /// Verifies dynamic calls can be applied after another postfix expression. + #[test] + fn parse_fragment_accepts_postfix_dynamic_call_source() { + let program = + parse_fragment(br#"return $callbacks[0]("abcd");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicCall { + callee: Box::new(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("callbacks".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "abcd".to_string() + )))], + }))] + ); + } + /// Verifies bare constant names lower to dynamic constant-fetch expressions. #[test] fn parse_fragment_accepts_constant_fetch_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 409d85b6f0..1d18afa37b 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -95,7 +95,7 @@ Dynamic eval constants store case-sensitive names and retained boxed Mixed value Eval predefined constants bypass the dynamic context and are materialized directly by the interpreter. The current eval-visible set is `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, `COUNT_*`, and the supported `PREG_*` constants; `defined()` recognizes them and duplicate `define()` attempts follow the same warning path as dynamic constants. -Argument unpacking (`...`) and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. +Argument unpacking (`...`), expression callable calls such as `$fn(...)`, and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index da284d913b..e9d8f7434a 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -114,7 +114,7 @@ Eval realpath-cache calls match elephc's native no-cache convention: `realpath_c Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5f15043c8d..4c68926188 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3691,6 +3691,46 @@ eval('echo call_user_func("native_eval_cuf_add", 4, 6);'); assert_eq!(out, "10"); } +/// Verifies variable call syntax inside eval dispatches supported builtin callables. +#[test] +fn test_eval_fragment_variable_callable_dispatches_builtin() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 18:16:38 +0200 Subject: [PATCH 0224/1208] Support eval iterator_apply on objects --- crates/elephc-eval/src/interpreter.rs | 189 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 24 ++++ 4 files changed, 209 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index b784ba5f78..124b4c6a77 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -45,6 +45,7 @@ pub enum EvalOutcome { } /// One already evaluated function-like call argument. +#[derive(Clone)] struct EvaluatedCallArg { name: Option, value: RuntimeCellHandle, @@ -1646,6 +1647,7 @@ fn eval_positional_expr_call( "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), "intdiv" => eval_builtin_intdiv(args, context, scope, values), + "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" @@ -2132,6 +2134,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_real" | "is_resource" | "is_string" + | "iterator_apply" | "iterator_count" | "iterator_to_array" | "json_decode" @@ -2405,6 +2408,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), "intdiv" => Some(&["num1", "num2"]), + "iterator_apply" => Some(&["iterator", "callback", "args"]), "iterator_count" => Some(&["iterator"]), "iterator_to_array" => Some(&["iterator", "preserve_keys"]), "ip2long" => Some(&["ip"]), @@ -3418,6 +3422,18 @@ fn eval_builtin_with_values( }; eval_intdiv_result(*left, *right, values)? } + "iterator_apply" => match evaluated_args { + [iterator, callback] => { + let callback = eval_callable_name(*callback, values)?; + eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? + } + [iterator, callback, args] => { + let callback = eval_callable_name(*callback, values)?; + let callback_args = eval_iterator_apply_arg_values(*args, values)?; + eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "iterator_count" => { let [iterator] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -5619,6 +5635,99 @@ fn eval_array_projection_result( Ok(result) } +/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. +fn eval_builtin_iterator_apply( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator, callback] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable_name(callback, values)?; + eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, callback_args] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable_name(callback, values)?; + let callback_args = eval_expr(callback_args, context, scope, values)?; + let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; + eval_iterator_apply_result(iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts the optional `iterator_apply()` callback-args value into call arguments. +fn eval_iterator_apply_arg_values( + args: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(args)? { + return Ok(Vec::new()); + } + if !values.is_array_like(args)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_array_call_arg_values(args, values) +} + +/// Applies a string callback to each valid position of an eval-supported Traversable object. +fn eval_iterator_apply_result( + iterator: RuntimeCellHandle, + callback: &str, + callback_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(iterator)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let count = match eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + ) { + Ok(count) => count, + Err(EvalStatus::UnsupportedConstruct) => { + let iterator = values.method_call(iterator, "getiterator", Vec::new())?; + eval_iterator_apply_iterator_object(iterator, callback, &callback_args, context, values)? + } + Err(err) => return Err(err), + }; + values.int(count) +} + +/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. +fn eval_iterator_apply_iterator_object( + iterator: RuntimeCellHandle, + callback: &str, + callback_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.method_call(iterator, "rewind", Vec::new())?; + let mut count = 0_i64; + loop { + let valid = values.method_call(iterator, "valid", Vec::new())?; + if !values.truthy(valid)? { + return Ok(count); + } + count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let result = + eval_callable_with_call_array_args(callback, callback_args.to_vec(), context, values)?; + if !values.truthy(result)? { + return Ok(count); + } + let _ = values.method_call(iterator, "next", Vec::new())?; + } +} + /// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. fn eval_builtin_iterator_count( args: &[EvalExpr], @@ -13595,6 +13704,7 @@ mod tests { Array(Vec), Assoc(Vec<(FakeKey, RuntimeCellHandle)>), Object(HashMap), + Iterator { len: i64, position: i64 }, Resource(i64), } @@ -13803,6 +13913,27 @@ mod tests { args: Vec, ) -> Result { match (self.get(object), method) { + (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) + else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position = 0; + self.null() + } + (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { + self.bool_value(position < len) + } + (FakeValue::Iterator { .. }, "next") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) + else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position += 1; + self.null() + } (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), (FakeValue::Object(properties), "read_x") => { if !args.is_empty() { @@ -13906,7 +14037,7 @@ mod tests { FakeValue::Bool(_) => EVAL_TAG_BOOL, FakeValue::Array(_) => EVAL_TAG_ARRAY, FakeValue::Assoc(_) => EVAL_TAG_ASSOC, - FakeValue::Object(_) => EVAL_TAG_OBJECT, + FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, FakeValue::Resource(_) => EVAL_TAG_RESOURCE, FakeValue::Null => EVAL_TAG_NULL, }) @@ -14267,7 +14398,7 @@ mod tests { FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", FakeValue::Array(value) => !value.is_empty(), FakeValue::Assoc(value) => !value.is_empty(), - FakeValue::Object(_) => true, + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, FakeValue::Resource(_) => true, }) } @@ -14347,7 +14478,7 @@ mod tests { .unwrap_or(0.0), FakeValue::Array(value) => value.len() as f64, FakeValue::Assoc(value) => value.len() as f64, - FakeValue::Object(_) => 1.0, + FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, FakeValue::Resource(value) => (*value + 1) as f64, } } @@ -14368,7 +14499,7 @@ mod tests { FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", FakeValue::Array(value) => !value.is_empty(), FakeValue::Assoc(value) => !value.is_empty(), - FakeValue::Object(_) => true, + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, FakeValue::Resource(_) => true, } } @@ -14385,7 +14516,7 @@ mod tests { FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), FakeValue::Array(_) => "Array".to_string(), FakeValue::Assoc(_) => "Array".to_string(), - FakeValue::Object(_) => "Object".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), FakeValue::Resource(value) => format!("Resource id #{}", value + 1), } } @@ -14410,7 +14541,7 @@ mod tests { FakeValue::String(value) => value.clone(), FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), - FakeValue::Object(_) => "Object".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), FakeValue::Resource(value) => format!("Resource id #{}", value + 1), } } @@ -16480,6 +16611,52 @@ return function_exists("iterator_count") && function_exists("iterator_to_array") assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `iterator_apply()` drives Iterator objects and callback args. + #[test] + fn execute_program_dispatches_iterator_apply_object_builtin() { + let program = parse_fragment( + br#"function eval_apply($prefix) { echo $prefix; return true; } +echo iterator_apply($it, "eval_apply", ["prefix" => "x"]) . ":"; +echo call_user_func("iterator_apply", $it, "eval_apply", ["y"]) . ":"; +return function_exists("iterator_apply");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xxx3:yyy3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `iterator_apply()` counts the position where the callback stops. + #[test] + fn execute_program_iterator_apply_stops_on_falsey_callback() { + let program = parse_fragment( + br#"function eval_stop() { echo "s"; return false; } +return iterator_apply($it, "eval_stop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "s"); + assert_eq!(values.get(result), FakeValue::Int(1)); + } + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. #[test] fn execute_program_dispatches_array_filter_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 1d18afa37b..47f6937a81 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -63,7 +63,7 @@ Eval `usort()`, `uksort()`, and `uasort()` use explicit comparator loops instead Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. -Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false; Traversable object dispatch for eval `iterator_apply()` remains future work. +Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false. Eval `iterator_apply()` drives Traversable objects through the eval method-call hooks (`rewind()`, `valid()`, and `next()`) and invokes string callbacks through the shared eval callable dispatcher. The callback receives only the optional third-argument array, matching PHP's `iterator_apply()` behavior; current value/key are not passed implicitly. Array inputs remain runtime fatals for `iterator_apply()`. Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax, offset-capture result shapes, or `preg_match_all()` set-order output surface as eval runtime fatals instead of falling back to a partial native ABI. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index e9d8f7434a..a371523318 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -74,7 +74,7 @@ Eval `usort()`, `uksort()`, and `uasort()` support string callbacks that resolve Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. -Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` still requires the Traversable object path and is not implemented for arrays, matching PHP's array `TypeError` rather than treating arrays as Traversable. +Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` supports Traversable objects through `rewind()`, `valid()`, `next()`, and string callbacks that target eval-declared functions, registered AOT callbacks, or supported builtins. Arrays still fail for `iterator_apply()`, matching PHP's array `TypeError` rather than treating arrays as Traversable. Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; offset-capture flags, `preg_match_all()` set-order output, and PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4c68926188..00ec12cf54 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1169,6 +1169,30 @@ echo function_exists("iterator_count") && function_exists("iterator_to_array");' assert_eq!(out, "2:12:reindexed:12:2:12:1"); } +/// Verifies eval `iterator_apply()` drives AOT Iterator objects through eval callbacks. +#[test] +fn test_eval_dispatches_iterator_apply_object_builtin() { + let out = compile_and_run( + r#"i = 0; $this->end = $end; } + public function rewind(): void { $this->i = 0; } + public function valid(): bool { return $this->i < $this->end; } + public function current(): int { return $this->i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } +} +eval('function eval_apply_label($prefix) { echo $prefix; return true; } +$r = new EvalApplyRange(2); +echo iterator_apply($r, "eval_apply_label", ["prefix" => "E"]) . ":"; +echo call_user_func("iterator_apply", $r, "eval_apply_label", ["C"]);'); +"#, + ); + assert_eq!(out, "EE2:CC2"); +} + /// Verifies eval `array_filter()` removes falsey values and preserves source keys. #[test] fn test_eval_dispatches_array_filter_builtin_call() { From b13237da259d865c0fde48ff27cabdb77e38c859 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 18:31:02 +0200 Subject: [PATCH 0225/1208] Support eval object method callable arrays --- crates/elephc-eval/src/interpreter.rs | 185 +++++++++++++++++++++++--- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 29 ++++ 4 files changed, 205 insertions(+), 17 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 124b4c6a77..f4eb7a96f7 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -51,6 +51,15 @@ struct EvaluatedCallArg { value: RuntimeCellHandle, } +/// One already evaluated PHP callback supported by the eval dispatcher. +enum EvaluatedCallable { + Named(String), + ObjectMethod { + object: RuntimeCellHandle, + method: String, + }, +} + /// Parsed flags for one eval `sprintf()` conversion specifier. #[derive(Clone, Copy)] struct EvalSprintfSpec { @@ -1505,9 +1514,9 @@ fn eval_dynamic_call( values: &mut impl RuntimeValueOps, ) -> Result { let callback = eval_expr(callee, context, scope, values)?; - let callback = eval_callable_name(callback, values)?; + let callback = eval_callable(callback, values)?; let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - eval_callable_with_call_array_args(&callback, evaluated_args, context, values) + eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) } /// Returns true for language constructs that need unevaluated argument expressions. @@ -2513,12 +2522,12 @@ fn eval_call_user_func_array_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable_name(callback, values)?; + let callback = eval_callable(callback, values)?; if !values.is_array_like(arg_array)? { return Err(EvalStatus::RuntimeFatal); } let evaluated_args = eval_array_call_arg_values(arg_array, values)?; - eval_callable_with_call_array_args(&callback, evaluated_args, context, values) + eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) } /// Dispatches `call_user_func` after its callback and arguments are already evaluated. @@ -2530,8 +2539,39 @@ fn eval_call_user_func_with_values( let Some((callback, callback_args)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; - let callback = eval_callable_name(*callback, values)?; - eval_callable_with_values(&callback, callback_args.to_vec(), context, values) + let callback = eval_callable(*callback, values)?; + eval_evaluated_callable_with_values(&callback, callback_args.to_vec(), context, values) +} + +/// Normalizes one PHP callback value for eval dynamic callable dispatch. +fn eval_callable( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_array_like(callback)? { + return eval_array_callable(callback, values); + } + eval_callable_name(callback, values).map(EvaluatedCallable::Named) +} + +/// Normalizes one two-element object-method callable array. +fn eval_array_callable( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.array_len(callback)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let object = values.array_get(callback, zero)?; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::UnsupportedConstruct); + } + let method = values.array_get(callback, one)?; + let method = String::from_utf8(values.string_bytes(method)?) + .map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(EvaluatedCallable::ObjectMethod { object, method }) } /// Normalizes one string callback name for eval dynamic callable dispatch. @@ -2548,6 +2588,44 @@ fn eval_callable_name( Ok(callback) } +/// Invokes an already normalized callback with source-order positional values. +fn eval_evaluated_callable_with_values( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named(name) => { + eval_callable_with_values(name, evaluated_args, context, values) + } + EvaluatedCallable::ObjectMethod { object, method } => { + values.method_call(*object, method, evaluated_args) + } + } +} + +/// Invokes an already normalized callback with optional named-argument metadata. +fn eval_evaluated_callable_with_call_array_args( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named(name) => { + eval_callable_with_call_array_args(name, evaluated_args, context, values) + } + EvaluatedCallable::ObjectMethod { object, method } => { + if evaluated_args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); + values.method_call(*object, method, evaluated_args) + } + } +} + /// Invokes a PHP-visible callable name with source-order positional values. fn eval_callable_with_values( name: &str, @@ -3424,11 +3502,11 @@ fn eval_builtin_with_values( } "iterator_apply" => match evaluated_args { [iterator, callback] => { - let callback = eval_callable_name(*callback, values)?; + let callback = eval_callable(*callback, values)?; eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? } [iterator, callback, args] => { - let callback = eval_callable_name(*callback, values)?; + let callback = eval_callable(*callback, values)?; let callback_args = eval_iterator_apply_arg_values(*args, values)?; eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? } @@ -5646,13 +5724,13 @@ fn eval_builtin_iterator_apply( [iterator, callback] => { let iterator = eval_expr(iterator, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable_name(callback, values)?; + let callback = eval_callable(callback, values)?; eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) } [iterator, callback, callback_args] => { let iterator = eval_expr(iterator, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable_name(callback, values)?; + let callback = eval_callable(callback, values)?; let callback_args = eval_expr(callback_args, context, scope, values)?; let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; eval_iterator_apply_result(iterator, &callback, callback_args, context, values) @@ -5675,10 +5753,10 @@ fn eval_iterator_apply_arg_values( eval_array_call_arg_values(args, values) } -/// Applies a string callback to each valid position of an eval-supported Traversable object. +/// Applies a callback to each valid position of an eval-supported Traversable object. fn eval_iterator_apply_result( iterator: RuntimeCellHandle, - callback: &str, + callback: &EvaluatedCallable, callback_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -5706,7 +5784,7 @@ fn eval_iterator_apply_result( /// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. fn eval_iterator_apply_iterator_object( iterator: RuntimeCellHandle, - callback: &str, + callback: &EvaluatedCallable, callback_args: &[EvaluatedCallArg], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -5719,8 +5797,12 @@ fn eval_iterator_apply_iterator_object( return Ok(count); } count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - let result = - eval_callable_with_call_array_args(callback, callback_args.to_vec(), context, values)?; + let result = eval_evaluated_callable_with_call_array_args( + callback, + callback_args.to_vec(), + context, + values, + )?; if !values.truthy(result)? { return Ok(count); } @@ -15905,6 +15987,56 @@ return $fn(right: 2, left: 1);"#, assert_eq!(result, expected); } + /// Verifies direct callable-array variable calls dispatch object methods. + #[test] + fn execute_program_callable_array_variable_dispatches_object_method() { + let program = parse_fragment( + br#"$box = new Box(41); +$cb = [$box, "add_x"]; +return $cb(1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies `call_user_func` dispatches callable arrays with object receivers. + #[test] + fn execute_program_call_user_func_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(42); +$cb = [$box, "read_x"]; +return call_user_func($cb);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies `call_user_func_array` dispatches callable arrays with positional args. + #[test] + fn execute_program_call_user_func_array_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(39); +return call_user_func_array([$box, "add2_x"], [1, 2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + } + /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. #[test] fn execute_program_call_user_func_array_dispatches_declared_function() { @@ -16635,6 +16767,29 @@ return function_exists("iterator_apply");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `iterator_apply()` accepts object-method callable arrays. + #[test] + fn execute_program_iterator_apply_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(5); +echo iterator_apply($it, [$box, "add_x"], [1]) . ":"; +return call_user_func("iterator_apply", $it, [$box, "add_x"], [1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:"); + assert_eq!(values.get(result), FakeValue::Int(3)); + } + /// Verifies eval `iterator_apply()` counts the position where the callback stops. #[test] fn execute_program_iterator_apply_stops_on_falsey_callback() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 47f6937a81..56ee1ddf2d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,6 +19,8 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The eval callable dispatcher currently normalizes string callbacks and two-element object-method callable arrays such as `[$this, "method"]`. Object-method arrays are supported for direct `$cb(...)`, `call_user_func()`, `call_user_func_array()`, and `iterator_apply()` paths with positional arguments; static-method callable arrays and closure/descriptor callback values are still handled only by the native descriptor runtime outside eval fragments. + Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. @@ -63,7 +65,7 @@ Eval `usort()`, `uksort()`, and `uasort()` use explicit comparator loops instead Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. -Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false. Eval `iterator_apply()` drives Traversable objects through the eval method-call hooks (`rewind()`, `valid()`, and `next()`) and invokes string callbacks through the shared eval callable dispatcher. The callback receives only the optional third-argument array, matching PHP's `iterator_apply()` behavior; current value/key are not passed implicitly. Array inputs remain runtime fatals for `iterator_apply()`. +Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false. Eval `iterator_apply()` drives Traversable objects through the eval method-call hooks (`rewind()`, `valid()`, and `next()`) and invokes string callbacks or object-method callable arrays through the shared eval callable dispatcher. The callback receives only the optional third-argument array, matching PHP's `iterator_apply()` behavior; current value/key are not passed implicitly. Array inputs remain runtime fatals for `iterator_apply()`. Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax, offset-capture result shapes, or `preg_match_all()` set-order output surface as eval runtime fatals instead of falling back to a partial native ABI. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a371523318..44524f0813 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,6 +34,8 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, ...)`, and `call_user_func_array($cb, [...])` with positional arguments. Static-method callable arrays and closure/descriptor callbacks are still outside the eval fragment subset. + The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` with the same direct, named-argument, callable, and `function_exists()` dispatch paths. @@ -74,7 +76,7 @@ Eval `usort()`, `uksort()`, and `uasort()` support string callbacks that resolve Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. -Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` supports Traversable objects through `rewind()`, `valid()`, `next()`, and string callbacks that target eval-declared functions, registered AOT callbacks, or supported builtins. Arrays still fail for `iterator_apply()`, matching PHP's array `TypeError` rather than treating arrays as Traversable. +Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` supports Traversable objects through `rewind()`, `valid()`, `next()`, string callbacks that target eval-declared functions, registered AOT callbacks, or supported builtins, and object-method callable arrays. Arrays still fail for `iterator_apply()`, matching PHP's array `TypeError` rather than treating arrays as Traversable. Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; offset-capture flags, `preg_match_all()` set-order output, and PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 00ec12cf54..f5edc438b2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3989,6 +3989,35 @@ $box->run(); assert_eq!(out, "50!"); } +/// Verifies eval callable arrays dispatch public AOT methods through all dynamic call surfaces. +#[test] +fn test_eval_fragment_callable_array_dispatches_this_public_method() { + let out = compile_and_run( + r#"x + $amount) . $suffix; + } + + public function run(): void { + echo eval('$cb = [$this, "label"]; +echo $cb(1, "a"); +echo ":"; +echo call_user_func($cb, 2, "b"); +echo ":"; +return call_user_func_array($cb, [3, "c"]);'); + } +} + +$box = new EvalCallableArrayBox(); +$box->run(); +"#, + ); + assert_eq!(out, "41a:42b:43c"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From e15a453d225e6193295f3349f812d768421b08d8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 18:47:22 +0200 Subject: [PATCH 0226/1208] Support eval match expressions --- crates/elephc-eval/src/eval_ir.rs | 12 ++++ crates/elephc-eval/src/interpreter.rs | 75 ++++++++++++++++++++++- crates/elephc-eval/src/parser.rs | 86 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 2 + tests/codegen/eval.rs | 14 +++++ 6 files changed, 186 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index c720b34d84..f296933298 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -176,6 +176,11 @@ pub enum EvalExpr { args: Vec, }, LoadVar(String), + Match { + subject: Box, + arms: Vec, + default: Option>, + }, MethodCall { object: Box, method: String, @@ -270,6 +275,13 @@ pub enum EvalArrayElement { KeyValue { key: EvalExpr, value: EvalExpr }, } +/// One ordered arm in a PHP `match` expression parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalMatchArm { + pub patterns: Vec, + pub value: EvalExpr, +} + /// One ordered case arm in a PHP switch parsed from an eval fragment. #[derive(Debug, Clone, PartialEq)] pub struct EvalSwitchCase { diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f4eb7a96f7..f5175c949e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -15,7 +15,7 @@ use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, - EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, + EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; use crate::json_validate::{self, JsonValue}; use crate::parser::parse_fragment; @@ -1287,6 +1287,11 @@ fn eval_expr( visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) } EvalExpr::Magic(magic) => eval_magic_const(magic, context, values), + EvalExpr::Match { + subject, + arms, + default, + } => eval_match_expr(subject, arms, default.as_deref(), context, scope, values), EvalExpr::NewObject { class_name, args } => { let args = eval_method_call_arg_values(args, context, scope, values)?; values @@ -1409,6 +1414,30 @@ fn eval_expr( } } +/// Evaluates a PHP `match` expression with strict comparison and lazy arm values. +fn eval_match_expr( + subject: &EvalExpr, + arms: &[EvalMatchArm], + default: Option<&EvalExpr>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let subject = eval_expr(subject, context, scope, values)?; + for arm in arms { + for pattern in &arm.patterns { + let pattern = eval_expr(pattern, context, scope, values)?; + let matched = values.compare(EvalBinOp::StrictEq, subject, pattern)?; + if values.truthy(matched)? { + return eval_expr(&arm.value, context, scope, values); + } + } + } + default + .map(|expr| eval_expr(expr, context, scope, values)) + .unwrap_or(Err(EvalStatus::RuntimeFatal)) +} + /// Returns cloned positional argument expressions, rejecting named arguments. fn positional_call_arg_exprs(args: &[EvalCallArg]) -> Result, EvalStatus> { if args @@ -15248,6 +15277,50 @@ return function_exists("var_dump");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies match expressions use strict comparison across comma-separated patterns. + #[test] + fn execute_program_match_uses_strict_pattern_comparison() { + let program = parse_fragment( + br#"return match ($x) { 1, "1" => "string", default => "other" };"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.string("1").expect("create fake string"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("string".to_string())); + } + + /// Verifies match expressions evaluate only the selected arm result. + #[test] + fn execute_program_match_skips_unselected_results() { + let program = + parse_fragment(br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("two".to_string())); + } + + /// Verifies match expressions without a matching arm or default fail at runtime. + #[test] + fn execute_program_match_without_default_fails_on_miss() { + let program = parse_fragment(br#"return match (3) { 1 => "one", 2 => "two" };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + } + /// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. #[test] fn execute_program_evaluates_keyword_logical_operators() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 1c1342ea11..24f66d59a1 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -15,8 +15,8 @@ use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalMagicConst, EvalProgram, - EvalStmt, EvalSwitchCase, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalMagicConst, EvalMatchArm, + EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; /// Parses an eval fragment into by-name EvalIR statements. @@ -1521,6 +1521,7 @@ impl Parser { let expr = self.parse_expr()?; Ok(EvalExpr::Print(Box::new(expr))) } + TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { Err(EvalParseError::UnsupportedConstruct) @@ -1549,6 +1550,61 @@ impl Parser { } } + /// Parses `match (expr) { pattern, other => value, default => fallback }`. + fn parse_match_expr(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let subject = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + + let mut arms = Vec::new(); + let mut default = None; + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + self.expect(TokenKind::FatArrow)?; + default = Some(Box::new(self.parse_expr()?)); + } else { + arms.push(self.parse_match_arm()?); + } + if self.consume(TokenKind::Comma) { + continue; + } + self.expect(TokenKind::RBrace)?; + break; + } + + Ok(EvalExpr::Match { + subject: Box::new(subject), + arms, + default, + }) + } + + /// Parses one non-default `match` arm and its comma-separated pattern list. + fn parse_match_arm(&mut self) -> Result { + let mut patterns = Vec::new(); + loop { + patterns.push(self.parse_expr()?); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::FatArrow) { + return Err(EvalParseError::UnexpectedToken); + } + if matches!(self.current(), TokenKind::Eof | TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + } + self.expect(TokenKind::FatArrow)?; + let value = self.parse_expr()?; + Ok(EvalMatchArm { patterns, value }) + } + /// Parses a function-like call expression and its source-order arguments. fn parse_call_expr(&mut self, name: String) -> Result { self.advance(); @@ -1826,7 +1882,7 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { /// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. fn is_unsupported_expression_keyword(name: &str) -> bool { - ["clone", "match", "yield"] + ["clone", "yield"] .iter() .any(|keyword| ident_eq(name, keyword)) } @@ -2602,6 +2658,29 @@ mod tests { ); } + /// Verifies match expressions preserve subject, patterns, and default expression. + #[test] + fn parse_fragment_accepts_match_source() { + let program = parse_fragment(br#"return match ($x) { 1, 2 => "small", default => "other" };"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Match { + subject: Box::new(EvalExpr::LoadVar("x".to_string())), + arms: vec![EvalMatchArm { + patterns: vec![ + EvalExpr::Const(EvalConst::Int(1)), + EvalExpr::Const(EvalConst::Int(2)), + ], + value: EvalExpr::Const(EvalConst::String("small".to_string())), + }], + default: Some(Box::new(EvalExpr::Const(EvalConst::String( + "other".to_string() + )))), + }))] + ); + } + /// Verifies null coalescing binds tighter than PHP ternary expressions. #[test] fn parse_fragment_null_coalesce_binds_tighter_than_ternary() { @@ -3171,7 +3250,6 @@ mod tests { fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { for source in [ b"return clone $value;" as &[u8], - b"return match ($value) { 1 => 2 };" as &[u8], b"return yield 1;" as &[u8], ] { assert_eq!( diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 56ee1ddf2d..5a85c025c6 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -21,6 +21,8 @@ The bridge crate parses the fragment at runtime into a small EvalIR and interpre The eval callable dispatcher currently normalizes string callbacks and two-element object-method callable arrays such as `[$this, "method"]`. Object-method arrays are supported for direct `$cb(...)`, `call_user_func()`, `call_user_func_array()`, and `iterator_apply()` paths with positional arguments; static-method callable arrays and closure/descriptor callback values are still handled only by the native descriptor runtime outside eval fragments. +Eval `match` expressions live entirely inside the interpreter: the subject is evaluated once, patterns are compared through the existing strict-comparison value hook, result arms are evaluated lazily, and a miss without `default` returns the eval runtime-fatal status. + Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 44524f0813..c769c879d6 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -36,6 +36,8 @@ The evaluated string must be a PHP fragment without an opening ` "int", "1" => "string", default => "other" }; +echo ":"; +echo match (3) { 1, 2 => missing(), default => "fallback" };'); +"#, + ); + assert_eq!(out, "string:fallback"); +} + /// Verifies break and continue control a loop interpreted inside eval. #[test] fn test_eval_break_and_continue_control_loop() { From 26b01c9ec0e6ca6029eea197feb64f291aa2de98 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 19:01:42 +0200 Subject: [PATCH 0227/1208] Support eval namespace declarations --- crates/elephc-eval/src/eval_ir.rs | 9 ++ crates/elephc-eval/src/interpreter.rs | 123 +++++++++++++++++ crates/elephc-eval/src/parser.rs | 185 +++++++++++++++++++++++--- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 21 +++ 6 files changed, 328 insertions(+), 18 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index f296933298..45d0bc9cab 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -181,6 +181,15 @@ pub enum EvalExpr { arms: Vec, default: Option>, }, + NamespacedCall { + name: String, + fallback_name: String, + args: Vec, + }, + NamespacedConstFetch { + name: String, + fallback_name: String, + }, MethodCall { object: Box, method: String, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f5175c949e..00de41f8a3 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1292,6 +1292,15 @@ fn eval_expr( arms, default, } => eval_match_expr(subject, arms, default.as_deref(), context, scope, values), + EvalExpr::NamespacedCall { + name, + fallback_name, + args, + } => eval_namespaced_call(name, fallback_name, args, context, scope, values), + EvalExpr::NamespacedConstFetch { + name, + fallback_name, + } => eval_namespaced_const_fetch(name, fallback_name, context, values), EvalExpr::NewObject { class_name, args } => { let args = eval_method_call_arg_values(args, context, scope, values)?; values @@ -1534,6 +1543,24 @@ fn eval_call( Err(EvalStatus::UnsupportedConstruct) } +/// Evaluates an unqualified namespaced function call with PHP's global fallback. +fn eval_namespaced_call( + name: &str, + fallback_name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + eval_call(fallback_name, args, context, scope, values) +} + /// Evaluates a variable or expression callable and dispatches it with source-order arguments. fn eval_dynamic_call( callee: &EvalExpr, @@ -13702,6 +13729,22 @@ fn eval_const_fetch( values.retain(value) } +/// Fetches a namespaced constant and falls back to the global constant namespace. +fn eval_namespaced_const_fetch( + name: &str, + fallback_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = eval_predefined_constant(name, values)? { + return Ok(value); + } + if let Some(value) = context.constant(name) { + return values.retain(value); + } + eval_const_fetch(fallback_name, context, values) +} + /// Materializes one eval-visible predefined constant into a runtime cell. fn eval_predefined_constant( name: &str, @@ -15742,6 +15785,86 @@ return function_exists("var_dump");"#, assert_eq!(values.get(result), FakeValue::Int(5)); } + /// Verifies eval namespace declarations qualify functions and namespace magic values. + #[test] + fn execute_program_namespace_qualifies_declared_function() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function dyn() { return __NAMESPACE__ . ":" . __FUNCTION__; } +return dyn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("Eval\\Ns:Eval\\Ns\\dyn".to_string()) + ); + } + + /// Verifies unqualified namespaced calls fall back to global builtins when needed. + #[test] + fn execute_program_namespace_call_falls_back_to_builtin() { + let program = parse_fragment(br#"namespace Eval\Ns; return strlen("abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + } + + /// Verifies namespaced dynamic functions take precedence over global builtin fallback. + #[test] + fn execute_program_namespace_function_overrides_builtin_fallback() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function strlen($value) { return 99; } +return strlen("abcd");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(99)); + } + + /// Verifies unqualified namespaced constants fall back to global predefined constants. + #[test] + fn execute_program_namespace_const_fetch_falls_back_to_global() { + let program = + parse_fragment(br#"namespace Eval\Ns; return PHP_EOL;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("\n".to_string())); + } + + /// Verifies namespaced dynamic constants take precedence over global fallback. + #[test] + fn execute_program_namespace_const_fetch_reads_dynamic_constant_first() { + let program = + parse_fragment(br#"namespace Eval\Ns; return LOCAL;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(7).expect("create fake int"); + assert!(context.define_constant("Eval\\Ns\\LOCAL", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); + } + /// Verifies eval-declared functions bind named arguments by parameter name. #[test] fn execute_program_calls_declared_function_with_named_args() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 24f66d59a1..e379944f25 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -529,6 +529,13 @@ struct Parser { tokens: Vec, pos: usize, source_len: usize, + namespace: String, +} + +/// A parsed PHP name plus whether it used a leading global namespace separator. +struct ParsedQualifiedName { + name: String, + absolute: bool, } impl Parser { @@ -538,6 +545,7 @@ impl Parser { tokens, pos: 0, source_len, + namespace: String::new(), } } @@ -579,6 +587,7 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), + TokenKind::Ident(name) if ident_eq(name, "namespace") => self.parse_namespace_stmt(), TokenKind::Ident(name) if ident_eq(name, "return") => { self.advance(); if self.consume_semicolon() { @@ -718,7 +727,7 @@ impl Parser { let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; - let name = name.clone(); + let name = self.qualify_name_in_current_namespace(name); self.advance(); self.expect(TokenKind::LBrace)?; if !self.consume(TokenKind::RBrace) { @@ -734,7 +743,7 @@ impl Parser { let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; - let name = name.clone(); + let name = self.qualify_name_in_current_namespace(name); self.advance(); self.expect(TokenKind::LParen)?; let params = self.parse_function_params()?; @@ -742,6 +751,39 @@ impl Parser { Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) } + /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. + fn parse_namespace_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let namespace = if self.consume(TokenKind::LBrace) { + return self.parse_namespace_block(String::new()); + } else { + self.parse_namespace_name()? + }; + if self.consume_semicolon() { + self.namespace = namespace; + return Ok(Vec::new()); + } + self.expect(TokenKind::LBrace)?; + self.parse_namespace_block(namespace) + } + + /// Parses statements inside an already opened namespace block. + fn parse_namespace_block(&mut self, namespace: String) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.namespace, namespace); + let result = self.parse_block_contents(); + self.namespace = previous; + result + } + + /// Parses a namespace declaration name without a leading global separator. + fn parse_namespace_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + /// Parses `global $name, $other;` declarations in eval fragments. fn parse_global_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -1086,6 +1128,11 @@ impl Parser { /// Parses a brace-delimited statement block. fn parse_block(&mut self) -> Result, EvalParseError> { self.expect(TokenKind::LBrace)?; + self.parse_block_contents() + } + + /// Parses statements until the closing brace for the current block. + fn parse_block_contents(&mut self) -> Result, EvalParseError> { let mut statements = Vec::new(); while !matches!(self.current(), TokenKind::RBrace) { if matches!(self.current(), TokenKind::Eof) { @@ -1499,6 +1546,11 @@ impl Parser { self.advance(); Ok(EvalExpr::LoadVar(name)) } + TokenKind::Magic(EvalMagicConst::Namespace) => { + let namespace = self.namespace.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(namespace))) + } TokenKind::Magic(magic) => { let magic = magic.clone(); self.advance(); @@ -1536,7 +1588,7 @@ impl Parser { TokenKind::Ident(name) => { let name = name.clone(); self.advance(); - Ok(EvalExpr::ConstFetch(name)) + Ok(self.const_fetch_expr(name)) } TokenKind::LBracket => self.parse_array_literal(), TokenKind::LParen => { @@ -1609,15 +1661,13 @@ impl Parser { fn parse_call_expr(&mut self, name: String) -> Result { self.advance(); let args = self.parse_call_args()?; - Ok(EvalExpr::Call { - name: name.to_ascii_lowercase(), - args, - }) + Ok(self.call_expr(name, args)) } /// Parses an explicitly qualified call or constant-fetch expression. fn parse_qualified_name_expr(&mut self) -> Result { let name = self.parse_qualified_name()?; + let name = self.resolve_qualified_name(name); if matches!(self.current(), TokenKind::LParen) { let args = self.parse_call_args()?; return Ok(EvalExpr::Call { @@ -1632,27 +1682,77 @@ impl Parser { fn parse_new_object_expr(&mut self) -> Result { self.advance(); let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_qualified_name(class_name); let args = self.parse_call_args()?; Ok(EvalExpr::NewObject { class_name, args }) } /// Parses a simple or explicitly qualified PHP name. - fn parse_qualified_name(&mut self) -> Result { - self.consume(TokenKind::Backslash); + fn parse_qualified_name(&mut self) -> Result { + let absolute = self.consume(TokenKind::Backslash); let TokenKind::Ident(first) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; - let mut class_name = first.clone(); + let mut name = first.clone(); self.advance(); while self.consume(TokenKind::Backslash) { let TokenKind::Ident(part) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; - class_name.push('\\'); - class_name.push_str(part); + name.push('\\'); + name.push_str(part); self.advance(); } - Ok(class_name) + Ok(ParsedQualifiedName { name, absolute }) + } + + /// Builds a call expression, adding namespace fallback for unqualified names. + fn call_expr(&self, name: String, args: Vec) -> EvalExpr { + let fallback_name = name.to_ascii_lowercase(); + if self.namespace.is_empty() { + EvalExpr::Call { + name: fallback_name, + args, + } + } else { + EvalExpr::NamespacedCall { + name: self + .qualify_name_in_current_namespace(&name) + .to_ascii_lowercase(), + fallback_name, + args, + } + } + } + + /// Builds a constant fetch expression, adding namespace fallback for unqualified names. + fn const_fetch_expr(&self, name: String) -> EvalExpr { + if self.namespace.is_empty() { + EvalExpr::ConstFetch(name) + } else { + EvalExpr::NamespacedConstFetch { + name: self.qualify_name_in_current_namespace(&name), + fallback_name: name, + } + } + } + + /// Prefixes a name with the parser's current namespace when one is active. + fn qualify_name_in_current_namespace(&self, name: &str) -> String { + if self.namespace.is_empty() { + name.to_string() + } else { + format!("{}\\{}", self.namespace, name) + } + } + + /// Resolves a parsed PHP name according to the current namespace. + fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute || self.namespace.is_empty() { + name.name + } else { + self.qualify_name_in_current_namespace(&name.name) + } } /// Parses a parenthesized source-order argument list. @@ -1867,7 +1967,6 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { [ "enum", "interface", - "namespace", "require", "require_once", "include", @@ -2372,6 +2471,60 @@ mod tests { ); } + /// Verifies semicolon namespace declarations qualify functions and unqualified calls. + #[test] + fn parse_fragment_accepts_semicolon_namespace_source() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function dyn() { return __NAMESPACE__; } +return dyn();"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::FunctionDecl { + name: "Eval\\Ns\\dyn".to_string(), + params: Vec::new(), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( + "Eval\\Ns".to_string() + ))))], + }, + EvalStmt::Return(Some(EvalExpr::NamespacedCall { + name: "eval\\ns\\dyn".to_string(), + fallback_name: "dyn".to_string(), + args: Vec::new(), + })), + ] + ); + } + + /// Verifies braced namespace declarations restore the previous namespace afterward. + #[test] + fn parse_fragment_accepts_braced_namespace_source() { + let program = parse_fragment( + br#"namespace Eval\Block { + class Box {} + return new Box(); +} +return Box;"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::ClassDecl { + name: "Eval\\Block\\Box".to_string(), + }, + EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Eval\\Block\\Box".to_string(), + args: Vec::new(), + })), + EvalStmt::Return(Some(EvalExpr::ConstFetch("Box".to_string()))), + ] + ); + } + /// Verifies static local declarations preserve the target name and initializer expression. #[test] fn parse_fragment_accepts_static_var_source() { @@ -2430,7 +2583,7 @@ mod tests { ); } - /// Verifies eval scope magic constants lower to explicit EvalIR nodes. + /// Verifies eval scope magic constants lower with namespace resolved at parse time. #[test] fn parse_fragment_accepts_scope_magic_constants() { let program = parse_fragment(b"return __CLASS__ . __NAMESPACE__ . __TRAIT__ . __METHOD__;") @@ -2444,7 +2597,7 @@ mod tests { left: Box::new(EvalExpr::Binary { op: EvalBinOp::Concat, left: Box::new(EvalExpr::Magic(EvalMagicConst::Class)), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Namespace)), + right: Box::new(EvalExpr::Const(EvalConst::String(String::new()))), }), right: Box::new(EvalExpr::Magic(EvalMagicConst::Trait)), }), diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 5a85c025c6..bff8cce936 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,12 +17,14 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__NAMESPACE__` / `__TRAIT__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. The eval callable dispatcher currently normalizes string callbacks and two-element object-method callable arrays such as `[$this, "method"]`. Object-method arrays are supported for direct `$cb(...)`, `call_user_func()`, `call_user_func_array()`, and `iterator_apply()` paths with positional arguments; static-method callable arrays and closure/descriptor callback values are still handled only by the native descriptor runtime outside eval fragments. Eval `match` expressions live entirely inside the interpreter: the subject is evaluated once, patterns are compared through the existing strict-comparison value hook, result arms are evaluated lazily, and a miss without `default` returns the eval runtime-fatal status. +Eval namespace declarations are resolved while the bridge parser builds EvalIR. The parser qualifies function declarations, empty class declarations, object construction names, and qualified references against the active namespace; for unqualified function calls and constant fetches it emits a namespaced lookup plus global fallback so PHP builtin functions and predefined constants remain visible from namespaced eval fragments. `__NAMESPACE__` is lowered to the active namespace string for the parsed block. + Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c769c879d6..815a10e6d9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,12 +32,14 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__`, `__NAMESPACE__`, and `__TRAIT__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, ...)`, and `call_user_func_array($cb, [...])` with positional arguments. Static-method callable arrays and closure/descriptor callbacks are still outside the eval fragment subset. Eval `match` expressions support strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default` fallback. A miss without `default` is currently reported as an eval runtime fatal. +Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and empty class declarations inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. + The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` with the same direct, named-argument, callable, and `function_exists()` dispatch paths. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8295acc407..162506af3d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3654,6 +3654,27 @@ echo \dyn_eval_ns_global(); assert_eq!(out, "0:1:42"); } +/// Verifies namespace declarations inside eval qualify dynamic declarations and fall back to builtins. +#[test] +fn test_eval_fragment_namespace_declares_qualified_function() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 19:14:59 +0200 Subject: [PATCH 0228/1208] Support eval namespace use imports --- crates/elephc-eval/src/interpreter.rs | 40 ++++ crates/elephc-eval/src/parser.rs | 262 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 22 +++ 5 files changed, 322 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 00de41f8a3..756aef3f89 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -15865,6 +15865,46 @@ return strlen("abcd");"#, assert_eq!(values.get(result), FakeValue::Int(7)); } + /// Verifies eval namespace `use function` imports dispatch to qualified dynamic functions. + #[test] + fn execute_program_namespace_use_function_import_dispatches() { + let program = parse_fragment( + br#"namespace Eval\Lib; +function target($x) { return $x + 1; } +namespace Eval\App; +use function Eval\Lib\target as AliasTarget; +return aliastarget(6);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); + } + + /// Verifies eval namespace `use const` imports fetch qualified dynamic constants. + #[test] + fn execute_program_namespace_use_const_import_fetches_dynamic_constant() { + let program = parse_fragment( + br#"namespace Eval\App; +use const Eval\Lib\VALUE as LocalValue; +return LocalValue;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(11).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(11)); + } + /// Verifies eval-declared functions bind named arguments by parameter name. #[test] fn execute_program_calls_declared_function_with_named_args() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index e379944f25..5e543029d2 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -18,6 +18,7 @@ use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; +use std::collections::HashMap; /// Parses an eval fragment into by-name EvalIR statements. pub fn parse_fragment(code: &[u8]) -> Result { @@ -530,6 +531,8 @@ struct Parser { pos: usize, source_len: usize, namespace: String, + imports: NamespaceImports, + allow_use_imports: bool, } /// A parsed PHP name plus whether it used a leading global namespace separator. @@ -538,6 +541,61 @@ struct ParsedQualifiedName { absolute: bool, } +/// Import alias tables active for the current namespace declaration region. +#[derive(Default)] +struct NamespaceImports { + classes: HashMap, + functions: HashMap, + constants: HashMap, +} + +/// The `use` declaration namespace being imported. +#[derive(Copy, Clone)] +enum UseImportKind { + Class, + Function, + Const, +} + +impl NamespaceImports { + /// Stores one class import under PHP's case-insensitive class alias key. + fn insert_class(&mut self, alias: String, name: String) { + self.classes.insert(alias.to_ascii_lowercase(), name); + } + + /// Stores one function import under PHP's case-insensitive function alias key. + fn insert_function(&mut self, alias: String, name: String) { + self.functions.insert(alias.to_ascii_lowercase(), name); + } + + /// Stores one constant import under PHP's case-sensitive constant alias key. + fn insert_constant(&mut self, alias: String, name: String) { + self.constants.insert(alias, name); + } + + /// Resolves a class import, including aliases used as the first segment of a class name. + fn resolve_class(&self, name: &str) -> Option { + let (first, tail) = split_first_name_segment(name); + let imported = self.classes.get(&first.to_ascii_lowercase())?; + Some(match tail { + Some(tail) => format!("{imported}\\{tail}"), + None => imported.clone(), + }) + } + + /// Resolves an unqualified function alias. + fn resolve_function(&self, name: &str) -> Option<&str> { + self.functions + .get(&name.to_ascii_lowercase()) + .map(String::as_str) + } + + /// Resolves a case-sensitive unqualified constant alias. + fn resolve_constant(&self, name: &str) -> Option<&str> { + self.constants.get(name).map(String::as_str) + } +} + impl Parser { /// Creates a parser over tokens produced from a source fragment. fn new(tokens: Vec, source_len: usize) -> Self { @@ -546,6 +604,8 @@ impl Parser { pos: 0, source_len, namespace: String::new(), + imports: NamespaceImports::default(), + allow_use_imports: true, } } @@ -601,6 +661,12 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), + TokenKind::Ident(name) if ident_eq(name, "use") && self.allow_use_imports => { + self.parse_use_stmt() + } + TokenKind::Ident(name) if ident_eq(name, "use") => { + Err(EvalParseError::UnsupportedConstruct) + } TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { Err(EvalParseError::UnsupportedConstruct) @@ -761,6 +827,7 @@ impl Parser { }; if self.consume_semicolon() { self.namespace = namespace; + self.imports = NamespaceImports::default(); return Ok(Vec::new()); } self.expect(TokenKind::LBrace)?; @@ -770,8 +837,12 @@ impl Parser { /// Parses statements inside an already opened namespace block. fn parse_namespace_block(&mut self, namespace: String) -> Result, EvalParseError> { let previous = std::mem::replace(&mut self.namespace, namespace); + let previous_imports = std::mem::take(&mut self.imports); + let previous_allow_use_imports = std::mem::replace(&mut self.allow_use_imports, true); let result = self.parse_block_contents(); self.namespace = previous; + self.imports = previous_imports; + self.allow_use_imports = previous_allow_use_imports; result } @@ -784,6 +855,58 @@ impl Parser { Ok(name.name) } + /// Parses PHP `use`, `use function`, and `use const` import declarations. + fn parse_use_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let kind = if matches!( + self.current(), + TokenKind::Ident(name) if ident_eq(name, "function") + ) { + self.advance(); + UseImportKind::Function + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + self.advance(); + UseImportKind::Const + } else { + UseImportKind::Class + }; + + loop { + self.parse_use_import(kind)?; + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(Vec::new()) + } + + /// Parses and registers one comma-separated import entry. + fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { + let name = self.parse_qualified_name()?.name; + let alias = if matches!( + self.current(), + TokenKind::Ident(keyword) if ident_eq(keyword, "as") + ) { + self.advance(); + let TokenKind::Ident(alias) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let alias = alias.clone(); + self.advance(); + alias + } else { + last_name_segment(&name).to_string() + }; + + match kind { + UseImportKind::Class => self.imports.insert_class(alias, name), + UseImportKind::Function => self.imports.insert_function(alias, name), + UseImportKind::Const => self.imports.insert_constant(alias, name), + } + Ok(()) + } + /// Parses `global $name, $other;` declarations in eval fragments. fn parse_global_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -1121,14 +1244,30 @@ impl Parser { if matches!(self.current(), TokenKind::LBrace) { self.parse_block() } else { - self.parse_stmt() + self.parse_nested_stmt() } } /// Parses a brace-delimited statement block. fn parse_block(&mut self) -> Result, EvalParseError> { self.expect(TokenKind::LBrace)?; - self.parse_block_contents() + self.parse_nested_block_contents() + } + + /// Parses one nested statement where import declarations are not legal. + fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_stmt(); + self.allow_use_imports = previous; + result + } + + /// Parses a nested block while preserving active imports for name resolution. + fn parse_nested_block_contents(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_block_contents(); + self.allow_use_imports = previous; + result } /// Parses statements until the closing brace for the current block. @@ -1682,7 +1821,7 @@ impl Parser { fn parse_new_object_expr(&mut self) -> Result { self.advance(); let class_name = self.parse_qualified_name()?; - let class_name = self.resolve_qualified_name(class_name); + let class_name = self.resolve_class_name(class_name); let args = self.parse_call_args()?; Ok(EvalExpr::NewObject { class_name, args }) } @@ -1708,6 +1847,12 @@ impl Parser { /// Builds a call expression, adding namespace fallback for unqualified names. fn call_expr(&self, name: String, args: Vec) -> EvalExpr { + if let Some(imported) = self.imports.resolve_function(&name) { + return EvalExpr::Call { + name: imported.to_ascii_lowercase(), + args, + }; + } let fallback_name = name.to_ascii_lowercase(); if self.namespace.is_empty() { EvalExpr::Call { @@ -1727,6 +1872,9 @@ impl Parser { /// Builds a constant fetch expression, adding namespace fallback for unqualified names. fn const_fetch_expr(&self, name: String) -> EvalExpr { + if let Some(imported) = self.imports.resolve_constant(&name) { + return EvalExpr::ConstFetch(imported.to_string()); + } if self.namespace.is_empty() { EvalExpr::ConstFetch(name) } else { @@ -1746,6 +1894,17 @@ impl Parser { } } + /// Resolves a class name through active imports before namespace qualification. + fn resolve_class_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute { + return name.name; + } + if let Some(imported) = self.imports.resolve_class(&name.name) { + return imported; + } + self.resolve_qualified_name(name) + } + /// Resolves a parsed PHP name according to the current namespace. fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { if name.absolute || self.namespace.is_empty() { @@ -1973,12 +2132,22 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { "include_once", "trait", "try", - "use", ] .iter() .any(|keyword| ident_eq(name, keyword)) } +/// Returns the first namespace segment and the optional remaining suffix. +fn split_first_name_segment(name: &str) -> (&str, Option<&str>) { + name.split_once('\\') + .map_or((name, None), |(first, tail)| (first, Some(tail))) +} + +/// Returns the final segment of a PHP qualified name. +fn last_name_segment(name: &str) -> &str { + name.rsplit('\\').next().unwrap_or(name) +} + /// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. fn is_unsupported_expression_keyword(name: &str) -> bool { ["clone", "yield"] @@ -2525,6 +2694,91 @@ return Box;"#, ); } + /// Verifies namespace import declarations resolve functions, constants, and class aliases. + #[test] + fn parse_fragment_accepts_namespace_use_imports() { + let program = parse_fragment( + br#"namespace Eval\UseNs; +use function Lib\strlen as Alias; +use const Lib\VALUE as LocalValue; +use Lib\Box as BoxAlias; +return Alias(LocalValue, new BoxAlias\Inner());"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\strlen".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\VALUE".to_string())), + EvalCallArg::positional(EvalExpr::NewObject { + class_name: "Lib\\Box\\Inner".to_string(), + args: Vec::new(), + }), + ], + }))] + ); + } + + /// Verifies namespace blocks restore imports when control returns to the outer namespace. + #[test] + fn parse_fragment_restores_use_imports_after_namespace_block() { + let program = parse_fragment( + br#"namespace Eval\Outer; +use function Lib\outer_func; +namespace Eval\Block { + use function Lib\inner_func as alias; + return alias(); +} +return outer_func();"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\inner_func".to_string(), + args: Vec::new(), + })), + EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\outer_func".to_string(), + args: Vec::new(), + })), + ] + ); + } + + /// Verifies imported aliases remain visible while parsing eval-declared function bodies. + #[test] + fn parse_fragment_applies_use_imports_inside_function_body() { + let program = parse_fragment( + br#"namespace Eval\UseNs; +use function Lib\target as alias; +function dyn() { return alias(); }"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::FunctionDecl { + name: "Eval\\UseNs\\dyn".to_string(), + params: Vec::new(), + body: vec![EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\target".to_string(), + args: Vec::new(), + }))], + }] + ); + } + + /// Verifies import declarations are rejected inside eval-declared function bodies. + #[test] + fn parse_fragment_rejects_use_import_inside_function_body() { + assert_eq!( + parse_fragment(br#"function dyn() { use function Lib\target; }"#), + Err(EvalParseError::UnsupportedConstruct) + ); + } + /// Verifies static local declarations preserve the target name and initializer expression. #[test] fn parse_fragment_accepts_static_var_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index bff8cce936..748e54d5f0 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -23,7 +23,7 @@ The eval callable dispatcher currently normalizes string callbacks and two-eleme Eval `match` expressions live entirely inside the interpreter: the subject is evaluated once, patterns are compared through the existing strict-comparison value hook, result arms are evaluated lazily, and a miss without `default` returns the eval runtime-fatal status. -Eval namespace declarations are resolved while the bridge parser builds EvalIR. The parser qualifies function declarations, empty class declarations, object construction names, and qualified references against the active namespace; for unqualified function calls and constant fetches it emits a namespaced lookup plus global fallback so PHP builtin functions and predefined constants remain visible from namespaced eval fragments. `__NAMESPACE__` is lowered to the active namespace string for the parsed block. +Eval namespace declarations are resolved while the bridge parser builds EvalIR. The parser qualifies function declarations, empty class declarations, object construction names, and qualified references against the active namespace; for unqualified function calls and constant fetches it emits a namespaced lookup plus global fallback so PHP builtin functions and predefined constants remain visible from namespaced eval fragments. Simple `use`, `use function`, and `use const` declarations are parser-time import tables for the active namespace declaration region: class imports rewrite `new` targets, function imports rewrite unqualified calls, and constant imports rewrite unqualified constant fetches before interpretation. `__NAMESPACE__` is lowered to the active namespace string for the parsed block. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 815a10e6d9..f97fde920c 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -38,7 +38,7 @@ Inside eval fragments, two-element object-method callable arrays such as `[$this Eval `match` expressions support strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default` fallback. A miss without `default` is currently reported as an eval runtime fatal. -Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and empty class declarations inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. +Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and empty class declarations inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. Simple `use`, `use function`, and `use const` declarations resolve class aliases for `new`, function aliases for unqualified calls, and constant aliases for unqualified constant fetches within the active namespace declaration region; grouped `use` syntax remains outside the eval subset. The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 162506af3d..66302c187b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4282,6 +4282,28 @@ echo eval('return (new \EvalDynamicNewNs\Box())->x;'); assert_eq!(out, "13"); } +/// Verifies eval namespace imports resolve functions, constants, and AOT class aliases. +#[test] +fn test_eval_fragment_namespace_use_imports() { + let out = compile_and_run( + r#"x;'); +"#, + ); + assert_eq!(out, "6:17"); +} + /// Verifies eval reference assignments update the referenced caller local. #[test] fn test_eval_reference_assignment_updates_caller_local() { From 48ccaf0cc308d7216d310bd46b8f1cb0195b7f6d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 19:19:01 +0200 Subject: [PATCH 0229/1208] Fix eval string escape preservation --- crates/elephc-eval/src/parser.rs | 58 +++++++++++++++++++++++++++----- tests/codegen/eval.rs | 7 ++-- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 5e543029d2..a7e4120301 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -446,15 +446,32 @@ impl<'a> Lexer<'a> { return Err(EvalParseError::UnterminatedString); }; self.bump_char(); - out.push(match escaped { - 'n' => '\n', - 'r' => '\r', - 't' => '\t', - '\\' => '\\', - '\'' => '\'', - '"' => '"', - other => other, - }); + if quote == '\'' { + match escaped { + '\\' => out.push('\\'), + '\'' => out.push('\''), + other => { + out.push('\\'); + out.push(other); + } + } + } else { + match escaped { + 'n' => out.push('\n'), + 'r' => out.push('\r'), + 't' => out.push('\t'), + 'v' => out.push('\x0b'), + 'e' => out.push('\x1b'), + 'f' => out.push('\x0c'), + '\\' => out.push('\\'), + '"' => out.push('"'), + '$' => out.push('$'), + other => { + out.push('\\'); + out.push(other); + } + } + } } else { out.push(ch); } @@ -3157,6 +3174,29 @@ function dyn() { return alias(); }"#, ); } + /// Verifies single- and double-quoted strings keep PHP-compatible simple escapes. + #[test] + fn parse_fragment_preserves_php_string_escape_semantics() { + let program = + parse_fragment(br#"return ['A\nB', "A\qB", "A\v\e\fB", 'It\'s'];"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( + "A\\nB".to_string() + ))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( + "A\\qB".to_string() + ))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( + "A\x0b\x1b\x0cB".to_string() + ))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("It's".to_string()))), + ])))] + ); + } + /// Verifies call expressions preserve their callee name and source-order arguments. #[test] fn parse_fragment_accepts_call_expression_source() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 66302c187b..62e8c9b77f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2708,12 +2708,15 @@ fn test_eval_dispatches_bin2hex_builtin_call() { r#" "ok"]); echo ":"; echo function_exists("bin2hex");'); "#, ); - assert_eq!(out, "417a:410a:213f:6f6b:1"); + assert_eq!(out, "417a:410a:5c6e:415c71:410b1b0c:213f:6f6b:1"); } /// Verifies eval `hex2bin()` decodes hex strings directly and by callable dispatch. @@ -4293,7 +4296,7 @@ class Box { } eval('namespace EvalUseExec; function imported_eval_func($x) { return $x + 1; } -define("EvalUseLib\\\\VALUE", 5); +define("EvalUseLib\\VALUE", 5); use function EvalUseExec\\imported_eval_func as AliasFunc; use const EvalUseLib\\VALUE as LocalValue; use EvalUseBridge\\Box as BoxAlias; From 8b317f3771d8d80417fc8e600db6030d2cd5ee47 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 19:33:08 +0200 Subject: [PATCH 0230/1208] Support eval include require --- crates/elephc-eval/src/context.rs | 35 ++++ crates/elephc-eval/src/eval_ir.rs | 5 + crates/elephc-eval/src/interpreter.rs | 286 ++++++++++++++++++++++++++ crates/elephc-eval/src/parser.rs | 71 +++++-- docs/internals/the-runtime.md | 2 + docs/php/system-and-io.md | 2 + tests/codegen/eval.rs | 39 ++++ 7 files changed, 428 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 5f4d660ec9..c47266007d 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -91,12 +91,14 @@ pub struct ElephcEvalContext { functions: HashMap, native_functions: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, + included_files: HashSet, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, pending_throw: Option, call_file: String, call_dir: String, call_line: i64, + file_magic_override: Option, } impl ElephcEvalContext { @@ -109,12 +111,14 @@ impl ElephcEvalContext { functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), + included_files: HashSet::new(), global_scope: None, function_stack: Vec::new(), pending_throw: None, call_file: String::new(), call_dir: String::new(), call_line: 0, + file_magic_override: None, } } @@ -128,12 +132,14 @@ impl ElephcEvalContext { functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), + included_files: HashSet::new(), global_scope: None, function_stack: Vec::new(), pending_throw: None, call_file: String::new(), call_dir: String::new(), call_line: 0, + file_magic_override: None, } } @@ -252,6 +258,16 @@ impl ElephcEvalContext { previous.filter(|previous| *previous != cell) } + /// Returns true when an eval include key was already loaded by this context. + pub fn has_included_file(&self, path: &str) -> bool { + self.included_files.contains(path) + } + + /// Records one successfully loaded eval include key for include_once/require_once. + pub fn mark_included_file(&mut self, path: impl Into) { + self.included_files.insert(path.into()); + } + /// Stores the non-owned global scope handle used by eval `global` aliases. pub fn set_global_scope(&mut self, scope: *mut ElephcEvalScope) -> bool { if scope.is_null() { @@ -298,6 +314,22 @@ impl ElephcEvalContext { self.call_file = file.into(); self.call_dir = dir.into(); self.call_line = line; + self.file_magic_override = None; + } + + /// Returns a copy of the current call-site metadata for temporary overrides. + pub fn call_site(&self) -> (String, String, i64, Option) { + ( + self.call_file.clone(), + self.call_dir.clone(), + self.call_line, + self.file_magic_override.clone(), + ) + } + + /// Overrides `__FILE__` while executing an actual file through eval include. + pub fn set_file_magic_override(&mut self, file: Option) { + self.file_magic_override = file; } /// Returns the source directory associated with the current eval call site. @@ -307,6 +339,9 @@ impl ElephcEvalContext { /// Returns PHP's `__FILE__` string for code currently running inside eval. pub fn eval_file_magic(&self) -> String { + if let Some(file) = &self.file_magic_override { + return file.clone(); + } if self.call_file.is_empty() { return String::new(); } diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 45d0bc9cab..9d62f3281e 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -175,6 +175,11 @@ pub enum EvalExpr { callee: Box, args: Vec, }, + Include { + path: Box, + required: bool, + once: bool, + }, LoadVar(String), Match { subject: Box, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 756aef3f89..f266210e44 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1283,6 +1283,11 @@ fn eval_expr( EvalExpr::DynamicCall { callee, args } => { eval_dynamic_call(callee, args, context, scope, values) } + EvalExpr::Include { + path, + required, + once, + } => eval_include_expr(path, *required, *once, context, scope, values), EvalExpr::LoadVar(name) => { visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) } @@ -12582,6 +12587,178 @@ fn eval_nested_eval( execute_program_with_context(context, &program, scope, values) } +/// Evaluates an eval-fragment include or require expression. +fn eval_include_expr( + path: &EvalExpr, + required: bool, + once: bool, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_expr(path, context, scope, values)?; + let path = eval_path_string(path, values)?; + let resolved_path = eval_resolve_include_path(&path, context); + let include_key = eval_include_key(&resolved_path); + if once && context.has_included_file(&include_key) { + return values.bool_value(true); + } + let bytes = match std::fs::read(&resolved_path) { + Ok(bytes) => bytes, + Err(_) => return eval_include_missing_file(&path, required, values), + }; + context.mark_included_file(include_key); + eval_execute_include_bytes(&bytes, &resolved_path, context, scope, values) +} + +/// Returns the include/require result for a file that cannot be opened. +fn eval_include_missing_file( + path: &str, + required: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let construct = if required { "require" } else { "include" }; + values.warning(&format!( + "Warning: {construct}({path}): Failed to open stream: No such file or directory\n" + ))?; + values.warning(&format!( + "Warning: {construct}(): Failed opening '{path}' for inclusion\n" + ))?; + if required { + Err(EvalStatus::RuntimeFatal) + } else { + values.bool_value(false) + } +} + +/// Resolves eval include paths using PHP's cwd-first and caller-directory fallback. +fn eval_resolve_include_path( + path: &str, + context: &ElephcEvalContext, +) -> std::path::PathBuf { + let raw_path = std::path::Path::new(path); + if raw_path.is_absolute() || raw_path.exists() { + return raw_path.to_path_buf(); + } + if context.call_dir().is_empty() { + return raw_path.to_path_buf(); + } + let caller_path = std::path::Path::new(context.call_dir()).join(raw_path); + if caller_path.exists() { + caller_path + } else { + raw_path.to_path_buf() + } +} + +/// Builds the stable include_once key for a resolved path. +fn eval_include_key(path: &std::path::Path) -> String { + std::fs::canonicalize(path) + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .into_owned() +} + +/// Executes a local include file, alternating raw output and PHP code blocks. +fn eval_execute_include_bytes( + bytes: &[u8], + path: &std::path::Path, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut cursor = 0; + while let Some((tag_start, code_start)) = eval_find_php_open_tag(bytes, cursor) { + eval_echo_include_bytes(&bytes[cursor..tag_start], values)?; + let close = eval_find_php_close_tag(bytes, code_start); + let code_end = close.unwrap_or(bytes.len()); + match eval_execute_include_code(&bytes[code_start..code_end], path, context, scope, values)? + { + EvalControl::None => {} + EvalControl::Return(value) => return Ok(value), + EvalControl::Throw(value) => { + context.set_pending_throw(value); + return Err(EvalStatus::UncaughtThrowable); + } + EvalControl::Break | EvalControl::Continue => { + return Err(EvalStatus::UnsupportedConstruct); + } + } + let Some(close) = close else { + return values.int(1); + }; + cursor = close + 2; + } + eval_echo_include_bytes(&bytes[cursor..], values)?; + values.int(1) +} + +/// Parses and executes one PHP code block from an included file. +fn eval_execute_include_code( + code: &[u8], + path: &std::path::Path, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let program = parse_fragment(code).map_err(EvalParseError::status)?; + let previous = context.call_site(); + let file = path.to_string_lossy().into_owned(); + let dir = path + .parent() + .map(|parent| parent.to_string_lossy().into_owned()) + .unwrap_or_default(); + context.set_call_site(file.clone(), dir, 1); + context.set_file_magic_override(Some(file)); + let result = execute_statements(program.statements(), context, scope, values); + context.set_call_site(previous.0, previous.1, previous.2); + context.set_file_magic_override(previous.3); + result +} + +/// Echoes raw non-PHP include bytes through the eval value hooks. +fn eval_echo_include_bytes( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if bytes.is_empty() { + return Ok(()); + } + let output = values.string_bytes_value(bytes)?; + values.echo(output) +} + +/// Finds the next ` Option<(usize, usize)> { + bytes + .get(start..)? + .windows(5) + .position(eval_is_php_open_tag) + .map(|offset| { + let tag_start = start + offset; + (tag_start, tag_start + 5) + }) +} + +/// Returns true when a five-byte window is a case-insensitive ` bool { + window.len() == 5 + && window[0] == b'<' + && window[1] == b'?' + && window[2].eq_ignore_ascii_case(&b'p') + && window[3].eq_ignore_ascii_case(&b'h') + && window[4].eq_ignore_ascii_case(&b'p') +} + +/// Finds the next PHP closing tag after a code block start. +fn eval_find_php_close_tag(bytes: &[u8], start: usize) -> Option { + bytes + .get(start..)? + .windows(2) + .position(|window| window == b"?>") + .map(|offset| start + offset) +} + /// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. fn eval_builtin_strlen( args: &[EvalExpr], @@ -14813,6 +14990,115 @@ mod tests { } } + /// Verifies eval include resolves caller-relative paths, shares scope, and returns file values. + #[test] + fn execute_program_include_uses_call_site_and_returns_file_result() { + let dir = std::env::temp_dir().join(format!( + "elephc-eval-include-{}-call-site", + std::process::id() + )); + let path = dir.join("piece.php"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("create include fixture directory"); + std::fs::write( + &path, + format!( + r#" self.parse_include_expr(), TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { @@ -1758,6 +1759,28 @@ impl Parser { } } + /// Parses PHP include/require expression constructs and their path expression. + fn parse_include_expr(&mut self) -> Result { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let required = ident_eq(name, "require") || ident_eq(name, "require_once"); + let once = ident_eq(name, "include_once") || ident_eq(name, "require_once"); + self.advance(); + let path = if self.consume(TokenKind::LParen) { + let path = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + path + } else { + self.parse_expr()? + }; + Ok(EvalExpr::Include { + path: Box::new(path), + required, + once, + }) + } + /// Parses `match (expr) { pattern, other => value, default => fallback }`. fn parse_match_expr(&mut self) -> Result { self.advance(); @@ -2140,18 +2163,16 @@ fn ident_eq(actual: &str, expected: &str) -> bool { /// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. fn is_unsupported_statement_keyword(name: &str) -> bool { - [ - "enum", - "interface", - "require", - "require_once", - "include", - "include_once", - "trait", - "try", - ] - .iter() - .any(|keyword| ident_eq(name, keyword)) + ["enum", "interface", "trait", "try"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + +/// Returns true when an identifier is an include/require expression construct. +fn is_include_construct_name(name: &str) -> bool { + ["include", "include_once", "require", "require_once"] + .iter() + .any(|keyword| ident_eq(name, keyword)) } /// Returns the first namespace segment and the optional remaining suffix. @@ -3213,6 +3234,32 @@ function dyn() { return alias(); }"#, ); } + /// Verifies include and require constructs parse as expressions with path metadata. + #[test] + fn parse_fragment_accepts_include_require_expression_source() { + let program = parse_fragment(br#"return include "a" . ".php"; require_once("b.php");"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Include { + path: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String(".php".to_string()))), + }), + required: false, + once: false, + })), + EvalStmt::Expr(EvalExpr::Include { + path: Box::new(EvalExpr::Const(EvalConst::String("b.php".to_string()))), + required: true, + once: true, + }), + ] + ); + } + /// Verifies explicitly qualified call expressions normalize away the leading slash. #[test] fn parse_fragment_accepts_qualified_call_expression_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 748e54d5f0..4f3f480b6f 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -87,6 +87,8 @@ Eval environment calls include `getenv()` and `putenv()`. Missing variables read Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain in the native runtime path rather than the eval subset. +Eval include/require constructs are interpreted in the eval bridge, not dispatched as callable builtins. The bridge resolves relative paths with PHP's cwd-first behavior and an eval call-site directory fallback, tracks canonical successful loads in the eval context for `include_once` and `require_once`, emits raw non-PHP bytes through the eval output hook, executes `` blocks as EvalIR against the same materialized scope, and temporarily overrides file magic metadata so included code sees the included file path and directory. + Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. Eval IPv4 conversion dispatch includes `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` for direct calls, named arguments, callable dispatch, and `function_exists()` probes. The interpreter currently handles IPv4 dotted-quad strings and four-byte packed addresses. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index f97fde920c..0c7fa501ab 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -40,6 +40,8 @@ Eval `match` expressions support strict pattern comparison, comma-separated patt Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and empty class declarations inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. Simple `use`, `use function`, and `use const` declarations resolve class aliases for `new`, function aliases for unqualified calls, and constant aliases for unqualified constant fetches within the active namespace declaration region; grouped `use` syntax remains outside the eval subset. +Eval `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. Relative paths follow PHP's cwd-first lookup and then fall back to the eval call-site directory. Included PHP files may contain normal `` blocks, raw text outside PHP tags is echoed, a `return` inside the included file becomes the include expression value, successful includes without `return` evaluate to `1`, repeated `*_once` includes evaluate to `true`, missing `include` returns `false` with warnings, and missing `require` aborts the eval fragment. + The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` with the same direct, named-argument, callable, and `function_exists()` dispatch paths. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 62e8c9b77f..849615a4fa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4307,6 +4307,45 @@ echo AliasFunc(LocalValue) . ":" . $box->x;'); assert_eq!(out, "6:17"); } +/// Verifies eval include executes PHP files through the bridge and shares caller scope. +#[test] +fn test_eval_fragment_include_executes_php_file_and_returns_value() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 19:48:01 +0200 Subject: [PATCH 0231/1208] Support eval legacy array syntax --- crates/elephc-eval/src/interpreter.rs | 13 ++++++++ crates/elephc-eval/src/parser.rs | 47 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 ++ docs/php/system-and-io.md | 2 ++ tests/codegen/eval.rs | 15 +++++++++ 5 files changed, 76 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f266210e44..d6dee4c968 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -15854,6 +15854,19 @@ return function_exists("var_dump");"#, assert_eq!(values.get(result), FakeValue::String("b".to_string())); } + /// Verifies legacy `array(...)` literals execute through the existing array runtime hooks. + #[test] + fn execute_program_reads_legacy_array_literal() { + let program = parse_fragment(br#"return array("a", "b" => "bee",)[0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("a".to_string())); + } + /// Verifies associative array literals and string-key reads execute through runtime hooks. #[test] fn execute_program_reads_assoc_array_literal() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 8960e0e775..2d2605a449 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1729,6 +1729,9 @@ impl Parser { let expr = self.parse_expr()?; Ok(EvalExpr::Print(Box::new(expr))) } + TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { + self.parse_legacy_array_literal() + } TokenKind::Ident(name) if is_include_construct_name(name) => self.parse_include_expr(), TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), @@ -1994,8 +1997,26 @@ impl Parser { /// Parses an array literal with source-order optional key/value element expressions. fn parse_array_literal(&mut self) -> Result { self.expect(TokenKind::LBracket)?; + self.parse_array_elements_until(TokenKind::RBracket) + } + + /// Parses PHP's legacy `array(...)` literal into the same EvalIR node as `[...]`. + fn parse_legacy_array_literal(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + self.parse_array_elements_until(TokenKind::RParen) + } + + /// Returns whether the current token starts PHP's legacy `array(...)` literal syntax. + fn current_starts_legacy_array_literal(&self) -> bool { + matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "array")) + && matches!(self.peek(), TokenKind::LParen) + } + + /// Parses comma-separated array elements until the supplied closing delimiter. + fn parse_array_elements_until(&mut self, close: TokenKind) -> Result { let mut elements = Vec::new(); - if self.consume(TokenKind::RBracket) { + if self.consume(close.clone()) { return Ok(EvalExpr::Array(elements)); } loop { @@ -2009,11 +2030,11 @@ impl Parser { if !self.consume(TokenKind::Comma) { break; } - if self.consume(TokenKind::RBracket) { + if self.consume(close.clone()) { return Ok(EvalExpr::Array(elements)); } } - self.expect(TokenKind::RBracket)?; + self.expect(close)?; Ok(EvalExpr::Array(elements)) } @@ -3408,6 +3429,26 @@ function dyn() { return alias(); }"#, ); } + /// Verifies legacy `array(...)` literals parse through the same EvalIR array node. + #[test] + fn parse_fragment_accepts_legacy_array_literal_source() { + let program = parse_fragment(br#"return array(1, "name" => "Ada",)[1];"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + }, + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }))] + ); + } + /// Verifies associative array literals preserve explicit key/value expressions. #[test] fn parse_fragment_accepts_assoc_array_literal_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 4f3f480b6f..e81d00ff19 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -107,6 +107,8 @@ Argument unpacking (`...`), expression callable calls such as `$fn(...)`, and `c Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. +Eval `array(...)` syntax is parsed as the same EvalIR array literal node as `[...]`, so runtime key normalization, append-key tracking, and array hook behavior are shared between both syntaxes. + Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()` and printf-family formatting via `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, and `random_int()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. ## Why a runtime? diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 0c7fa501ab..f56e0e14a4 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -42,6 +42,8 @@ Eval namespace declarations support both `namespace Name;` and `namespace Name { Eval `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. Relative paths follow PHP's cwd-first lookup and then fall back to the eval call-site directory. Included PHP files may contain normal `` blocks, raw text outside PHP tags is echoed, a `return` inside the included file becomes the include expression value, successful includes without `return` evaluate to `1`, repeated `*_once` includes evaluate to `true`, missing `include` returns `false` with warnings, and missing `require` aborts the eval fragment. +Eval array literals accept both the modern `[...]` syntax and PHP's legacy `array(...)` syntax, with both forms lowered to the same runtime array representation. + The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` with the same direct, named-argument, callable, and `function_exists()` dispatch paths. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 849615a4fa..832b003376 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -622,6 +622,21 @@ fn test_eval_indexed_array_literal_and_read() { assert_eq!(out, "2"); } +/// Verifies eval accepts PHP's legacy `array(...)` literal syntax. +#[test] +fn test_eval_legacy_array_literal_executes_through_bridge() { + let out = compile_and_run( + r#" "Ada",)["name"];'); +echo ":"; +eval('$items = array(2 => "two", "tail",); echo $items[3];'); +"#, + ); + assert_eq!(out, "b:Ada:tail"); +} + /// Verifies eval indexed-array writes mutate an array visible to native code. #[test] fn test_eval_indexed_array_write_is_visible_after_eval() { From 200cd2146c808b45984b1a0994d08cb263d45c23 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 19:55:53 +0200 Subject: [PATCH 0232/1208] Support eval grouped namespace imports --- crates/elephc-eval/src/interpreter.rs | 24 ++++ crates/elephc-eval/src/parser.rs | 157 ++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 22 ++++ 5 files changed, 194 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d6dee4c968..96125c2855 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -16204,6 +16204,30 @@ return LocalValue;"#, assert_eq!(values.get(result), FakeValue::Int(11)); } + /// Verifies eval grouped namespace imports dispatch dynamic functions and constants. + #[test] + fn execute_program_grouped_namespace_use_imports_dispatch() { + let program = parse_fragment( + br#"namespace Eval\Lib; +function target($x) { return $x + 2; } +namespace Eval\App; +use function Eval\Lib\{target as AliasTarget}; +use const Eval\Lib\{VALUE as LocalValue}; +return AliasTarget(LocalValue);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(5).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); + } + /// Verifies eval-declared functions bind named arguments by parameter name. #[test] fn execute_program_calls_declared_function_with_named_args() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 2d2605a449..01481ef261 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -567,7 +567,7 @@ struct NamespaceImports { } /// The `use` declaration namespace being imported. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Eq, PartialEq)] enum UseImportKind { Class, Function, @@ -875,10 +875,21 @@ impl Parser { /// Parses PHP `use`, `use function`, and `use const` import declarations. fn parse_use_stmt(&mut self) -> Result, EvalParseError> { self.advance(); - let kind = if matches!( - self.current(), - TokenKind::Ident(name) if ident_eq(name, "function") - ) { + let kind = self.parse_use_import_kind(); + + loop { + self.parse_use_import(kind)?; + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(Vec::new()) + } + + /// Parses an optional top-level `function` or `const` use-import kind. + fn parse_use_import_kind(&mut self) -> UseImportKind { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { self.advance(); UseImportKind::Function } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { @@ -886,21 +897,101 @@ impl Parser { UseImportKind::Const } else { UseImportKind::Class + } + } + + /// Parses and registers one comma-separated import entry. + fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { + let (name, grouped) = self.parse_use_name_or_group_start()?; + if grouped { + return self.parse_grouped_use_imports(kind, name); + } + self.parse_use_alias_and_register(kind, name) + } + + /// Parses a use-import name, stopping after a trailing namespace separator before `{`. + fn parse_use_name_or_group_start(&mut self) -> Result<(String, bool), EvalParseError> { + let _ = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + if self.consume(TokenKind::LBrace) { + return Ok((name, true)); + } + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok((name, false)) + } + /// Parses all members inside a grouped namespace import declaration. + fn parse_grouped_use_imports( + &mut self, + default_kind: UseImportKind, + prefix: String, + ) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } loop { - self.parse_use_import(kind)?; + let kind = self.parse_grouped_use_entry_kind(default_kind)?; + let member = self.parse_grouped_use_member_name()?; + let name = join_grouped_use_name(&prefix, &member); + self.parse_use_alias_and_register(kind, name)?; if !self.consume(TokenKind::Comma) { break; } + if self.consume(TokenKind::RBrace) { + return Ok(()); + } } - self.expect_semicolon()?; - Ok(Vec::new()) + self.expect(TokenKind::RBrace) } - /// Parses and registers one comma-separated import entry. - fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { - let name = self.parse_qualified_name()?.name; + /// Parses an optional per-entry grouped import kind, matching PHP's mixed group rules. + fn parse_grouped_use_entry_kind( + &mut self, + default_kind: UseImportKind, + ) -> Result { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Function); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Const); + } + Ok(default_kind) + } + + /// Parses one non-absolute member name inside a grouped use declaration. + fn parse_grouped_use_member_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses an optional alias and stores one namespace import. + fn parse_use_alias_and_register( + &mut self, + kind: UseImportKind, + name: String, + ) -> Result<(), EvalParseError> { let alias = if matches!( self.current(), TokenKind::Ident(keyword) if ident_eq(keyword, "as") @@ -2207,6 +2298,11 @@ fn last_name_segment(name: &str) -> &str { name.rsplit('\\').next().unwrap_or(name) } +/// Combines a grouped use prefix with one relative member name. +fn join_grouped_use_name(prefix: &str, member: &str) -> String { + format!("{prefix}\\{member}") +} + /// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. fn is_unsupported_expression_keyword(name: &str) -> bool { ["clone", "yield"] @@ -2779,6 +2875,45 @@ return Alias(LocalValue, new BoxAlias\Inner());"#, ); } + /// Verifies grouped namespace imports resolve functions, constants, and class aliases. + #[test] + fn parse_fragment_accepts_grouped_namespace_use_imports() { + let program = parse_fragment( + br#"namespace Eval\UseNs; +use Lib\{Box as BoxAlias, Sub\Thing, function imported_func as Alias}; +use const Lib\{VALUE as LocalValue, OTHER}; +return Alias(LocalValue, OTHER, new BoxAlias\Inner(), new Thing());"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\imported_func".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\VALUE".to_string())), + EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\OTHER".to_string())), + EvalCallArg::positional(EvalExpr::NewObject { + class_name: "Lib\\Box\\Inner".to_string(), + args: Vec::new(), + }), + EvalCallArg::positional(EvalExpr::NewObject { + class_name: "Lib\\Sub\\Thing".to_string(), + args: Vec::new(), + }), + ], + }))] + ); + } + + /// Verifies typed grouped namespace imports reject mixed per-entry kinds. + #[test] + fn parse_fragment_rejects_mixed_kind_typed_grouped_use_imports() { + assert_eq!( + parse_fragment(br#"use function Lib\{target, const VALUE};"#), + Err(EvalParseError::UnexpectedToken) + ); + } + /// Verifies namespace blocks restore imports when control returns to the outer namespace. #[test] fn parse_fragment_restores_use_imports_after_namespace_block() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e81d00ff19..e75744adea 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -23,7 +23,7 @@ The eval callable dispatcher currently normalizes string callbacks and two-eleme Eval `match` expressions live entirely inside the interpreter: the subject is evaluated once, patterns are compared through the existing strict-comparison value hook, result arms are evaluated lazily, and a miss without `default` returns the eval runtime-fatal status. -Eval namespace declarations are resolved while the bridge parser builds EvalIR. The parser qualifies function declarations, empty class declarations, object construction names, and qualified references against the active namespace; for unqualified function calls and constant fetches it emits a namespaced lookup plus global fallback so PHP builtin functions and predefined constants remain visible from namespaced eval fragments. Simple `use`, `use function`, and `use const` declarations are parser-time import tables for the active namespace declaration region: class imports rewrite `new` targets, function imports rewrite unqualified calls, and constant imports rewrite unqualified constant fetches before interpretation. `__NAMESPACE__` is lowered to the active namespace string for the parsed block. +Eval namespace declarations are resolved while the bridge parser builds EvalIR. The parser qualifies function declarations, empty class declarations, object construction names, and qualified references against the active namespace; for unqualified function calls and constant fetches it emits a namespaced lookup plus global fallback so PHP builtin functions and predefined constants remain visible from namespaced eval fragments. Simple and grouped `use`, `use function`, and `use const` declarations are parser-time import tables for the active namespace declaration region: class imports rewrite `new` targets, function imports rewrite unqualified calls, and constant imports rewrite unqualified constant fetches before interpretation. `__NAMESPACE__` is lowered to the active namespace string for the parsed block. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index f56e0e14a4..5208283783 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -38,7 +38,7 @@ Inside eval fragments, two-element object-method callable arrays such as `[$this Eval `match` expressions support strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default` fallback. A miss without `default` is currently reported as an eval runtime fatal. -Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and empty class declarations inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. Simple `use`, `use function`, and `use const` declarations resolve class aliases for `new`, function aliases for unqualified calls, and constant aliases for unqualified constant fetches within the active namespace declaration region; grouped `use` syntax remains outside the eval subset. +Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and empty class declarations inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. Simple and grouped `use`, `use function`, and `use const` declarations resolve class aliases for `new`, function aliases for unqualified calls, and constant aliases for unqualified constant fetches within the active namespace declaration region. Eval `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. Relative paths follow PHP's cwd-first lookup and then fall back to the eval call-site directory. Included PHP files may contain normal `` blocks, raw text outside PHP tags is echoed, a `return` inside the included file becomes the include expression value, successful includes without `return` evaluate to `1`, repeated `*_once` includes evaluate to `true`, missing `include` returns `false` with warnings, and missing `require` aborts the eval fragment. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 832b003376..24678823d6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4322,6 +4322,28 @@ echo AliasFunc(LocalValue) . ":" . $box->x;'); assert_eq!(out, "6:17"); } +/// Verifies eval grouped namespace imports resolve functions, constants, and AOT class aliases. +#[test] +fn test_eval_fragment_grouped_namespace_use_imports() { + let out = compile_and_run( + r#"x;'); +"#, + ); + assert_eq!(out, "8:19"); +} + /// Verifies eval include executes PHP files through the bridge and shares caller scope. #[test] fn test_eval_fragment_include_executes_php_file_and_returns_value() { From 949cfed2d514f5bfefc8a968b2a0b8df41803786 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 20:13:46 +0200 Subject: [PATCH 0233/1208] Support eval throwable try catch --- crates/elephc-eval/src/eval_ir.rs | 12 ++ crates/elephc-eval/src/interpreter.rs | 107 +++++++++++++++++- crates/elephc-eval/src/parser.rs | 151 +++++++++++++++++++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 16 +++ 6 files changed, 284 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 9d62f3281e..eefda14605 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -113,6 +113,10 @@ pub enum EvalStmt { cases: Vec, }, Throw(EvalExpr), + Try { + body: Vec, + catches: Vec, + }, UnsetVar { name: String, }, @@ -123,6 +127,14 @@ pub enum EvalStmt { Expr(EvalExpr), } +/// One `catch` block attached to an eval `try` statement. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalCatch { + pub class_name: String, + pub var_name: String, + pub body: Vec, +} + /// Runtime user function declared by an eval fragment. #[derive(Debug, Clone, PartialEq)] pub struct EvalFunction { diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 96125c2855..0f7f39cf83 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -14,8 +14,8 @@ use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, - EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalConst, EvalExpr, EvalFunction, + EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; use crate::json_validate::{self, JsonValue}; use crate::parser::parse_fragment; @@ -1004,6 +1004,7 @@ fn execute_stmt( } Ok(EvalControl::Throw(thrown)) } + EvalStmt::Try { body, catches } => execute_try_stmt(body, catches, context, scope, values), EvalStmt::UnsetVar { name } => { if let Some(replaced) = unset_scope_cell(scope, name.clone()) { values.release(replaced)?; @@ -1031,6 +1032,41 @@ fn execute_stmt( } } +/// Executes an eval `try` body and handles supported `catch` clauses. +fn execute_try_stmt( + body: &[EvalStmt], + catches: &[EvalCatch], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let thrown = match execute_statements(body, context, scope, values)? { + EvalControl::Throw(thrown) => thrown, + control => return Ok(control), + }; + let Some(catch) = catches + .iter() + .find(|catch| catch_type_matches_throwable(&catch.class_name)) + else { + return Ok(EvalControl::Throw(thrown)); + }; + for replaced in set_scope_cell( + context, + scope, + catch.var_name.clone(), + thrown, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + execute_statements(&catch.body, context, scope, values) +} + +/// Returns true for catch types that are known to accept any valid thrown object. +fn catch_type_matches_throwable(class_name: &str) -> bool { + class_name.eq_ignore_ascii_case("Throwable") +} + /// Registers an empty eval-declared class name in the dynamic class table. fn execute_class_decl_stmt( name: &str, @@ -13687,6 +13723,12 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has collect_static_var_names(&case.body, names); } } + EvalStmt::Try { body, catches } => { + collect_static_var_names(body, names); + for catch in catches { + collect_static_var_names(&catch.body, names); + } + } EvalStmt::ArrayAppendVar { .. } | EvalStmt::ArraySetVar { .. } | EvalStmt::Break @@ -14944,6 +14986,67 @@ mod tests { } } + /// Verifies eval `try/catch` catches a thrown object and binds the catch variable. + #[test] + fn execute_program_catches_throwable_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies static locals declared inside eval catch blocks persist per function context. + #[test] + fn execute_context_function_persists_static_local_inside_catch() { + let program = parse_fragment( + br#"function dyn($e) { + try { + throw $e; + } catch (Throwable $caught) { + static $n = 0; + $n++; + return $caught->answer() + $n; + } +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let first_thrown = values + .new_object("Exception") + .expect("allocate first fake exception"); + let second_thrown = values + .new_object("Exception") + .expect("allocate second fake exception"); + + let first = + execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) + .expect("execute first dynamic function call"); + let second = + execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(43)); + assert_eq!(values.get(second), FakeValue::Int(44)); + } + /// Verifies throws from eval-declared functions escape through the shared context. #[test] fn execute_context_function_propagates_throw_as_uncaught_outcome() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 01481ef261..f9ed2d369b 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -15,8 +15,8 @@ use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalMagicConst, EvalMatchArm, - EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalConst, EvalExpr, EvalMagicConst, + EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; use std::collections::HashMap; @@ -677,6 +677,7 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "static") => self.parse_static_var_stmt(), TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), + TokenKind::Ident(name) if ident_eq(name, "try") => self.parse_try_stmt(), TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), TokenKind::Ident(name) if ident_eq(name, "use") && self.allow_use_imports => { self.parse_use_stmt() @@ -1055,6 +1056,55 @@ impl Parser { Ok(vec![EvalStmt::Throw(expr)]) } + /// Parses `try { ... } catch (Throwable $name) { ... }` statements. + fn parse_try_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_block()?; + let mut catches = Vec::new(); + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { + catches.push(self.parse_catch_clause()?); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) { + return Err(EvalParseError::UnsupportedConstruct); + } + if catches.is_empty() { + return Err(EvalParseError::UnexpectedToken); + } + Ok(vec![EvalStmt::Try { body, catches }]) + } + + /// Parses one supported `catch (Throwable $name) { ... }` clause. + fn parse_catch_clause(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let class_name = self.parse_supported_catch_type()?; + let TokenKind::DollarIdent(var_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let var_name = var_name.clone(); + self.advance(); + self.expect(TokenKind::RParen)?; + let body = self.parse_block()?; + Ok(EvalCatch { + class_name, + var_name, + body, + }) + } + + /// Parses the catch type currently implemented by the EvalIR interpreter. + fn parse_supported_catch_type(&mut self) -> Result { + let class_name = self.parse_qualified_name()?; + if matches!(self.current(), TokenKind::Pipe) { + return Err(EvalParseError::UnsupportedConstruct); + } + let class_name = self.resolve_class_name(class_name); + if !is_supported_catch_type(&class_name) { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(class_name) + } + /// Parses a dynamic function declaration parameter list after `(`. fn parse_function_params(&mut self) -> Result, EvalParseError> { let mut params = Vec::new(); @@ -2275,11 +2325,16 @@ fn ident_eq(actual: &str, expected: &str) -> bool { /// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. fn is_unsupported_statement_keyword(name: &str) -> bool { - ["enum", "interface", "trait", "try"] + ["enum", "interface", "trait"] .iter() .any(|keyword| ident_eq(name, keyword)) } +/// Returns true when an eval catch type can be matched without runtime class tests. +fn is_supported_catch_type(class_name: &str) -> bool { + class_name.eq_ignore_ascii_case("Throwable") +} + /// Returns true when an identifier is an include/require expression construct. fn is_include_construct_name(name: &str) -> bool { ["include", "include_once", "require", "require_once"] @@ -3859,6 +3914,96 @@ function dyn() { return alias(); }"#, ); } + /// Verifies try/catch statements lower supported Throwable clauses into EvalIR. + #[test] + fn parse_fragment_accepts_try_catch_throwable_source() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_name: "Throwable".to_string(), + var_name: "caught".to_string(), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + }] + ); + } + + /// Verifies class imports can alias the supported Throwable catch type. + #[test] + fn parse_fragment_accepts_try_catch_imported_throwable_alias() { + let program = parse_fragment( + br#"use Throwable as T; +try { + throw $e; +} catch (T $caught) { + echo "caught"; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::LoadVar("e".to_string()))], + catches: vec![EvalCatch { + class_name: "Throwable".to_string(), + var_name: "caught".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "caught".to_string() + )))], + }], + }] + ); + } + + /// Verifies unsupported catch type narrowing stays explicit until runtime matching exists. + #[test] + fn parse_fragment_rejects_specific_eval_catch_type() { + assert_eq!( + parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Exception $caught) { + return 1; +}"#, + ), + Err(EvalParseError::UnsupportedConstruct) + ); + assert_eq!( + parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable|Exception $caught) { + return 1; +}"#, + ), + Err(EvalParseError::UnsupportedConstruct) + ); + } + + /// Verifies eval try/finally remains outside the supported EvalIR subset. + #[test] + fn parse_fragment_rejects_eval_finally_source() { + assert_eq!( + parse_fragment(br#"try { echo "try"; } finally { echo "finally"; }"#), + Err(EvalParseError::UnsupportedConstruct) + ); + } + /// Verifies unset fragments expand to one by-name unset statement per variable. #[test] fn parse_fragment_accepts_unset_source() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e75744adea..11004a1ec5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable`, `finally`, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. The eval callable dispatcher currently normalizes string callbacks and two-element object-method callable arrays such as `[$this, "method"]`. Object-method arrays are supported for direct `$cb(...)`, `call_user_func()`, `call_user_func_array()`, and `iterator_apply()` paths with positional arguments; static-method callable arrays and closure/descriptor callback values are still handled only by the native descriptor runtime outside eval fragments. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 5208283783..62ca12b8d2 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, ...)`, and `call_user_func_array($cb, [...])` with positional arguments. Static-method callable arrays and closure/descriptor callbacks are still outside the eval fragment subset. @@ -124,7 +124,7 @@ Eval realpath-cache calls match elephc's native no-cache convention: `realpath_c Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch` can catch `Throwable` clauses inside the fragment; specific catch-type narrowing such as `catch (Exception $e)`, union catches, and `finally` remain outside the EvalIR subset. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 24678823d6..f0e6e95934 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4570,3 +4570,19 @@ try { ); assert_eq!(out, "caught:nested boom"); } + +/// Verifies eval-internal try/catch consumes a thrown Throwable before returning. +#[test] +fn test_eval_try_catch_catches_throwable_inside_eval() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 20:24:21 +0200 Subject: [PATCH 0234/1208] Support eval finally --- crates/elephc-eval/src/eval_ir.rs | 1 + crates/elephc-eval/src/interpreter.rs | 182 +++++++++++++++++++++++++- crates/elephc-eval/src/parser.rs | 39 ++++-- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 15 +++ 6 files changed, 228 insertions(+), 17 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index eefda14605..d5dff41be5 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -116,6 +116,7 @@ pub enum EvalStmt { Try { body: Vec, catches: Vec, + finally_body: Vec, }, UnsetVar { name: String, diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0f7f39cf83..3b452b7489 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1004,7 +1004,11 @@ fn execute_stmt( } Ok(EvalControl::Throw(thrown)) } - EvalStmt::Try { body, catches } => execute_try_stmt(body, catches, context, scope, values), + EvalStmt::Try { + body, + catches, + finally_body, + } => execute_try_stmt(body, catches, finally_body, context, scope, values), EvalStmt::UnsetVar { name } => { if let Some(replaced) = unset_scope_cell(scope, name.clone()) { values.release(replaced)?; @@ -1036,14 +1040,52 @@ fn execute_stmt( fn execute_try_stmt( body: &[EvalStmt], catches: &[EvalCatch], + finally_body: &[EvalStmt], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let thrown = match execute_statements(body, context, scope, values)? { - EvalControl::Throw(thrown) => thrown, - control => return Ok(control), + let control = match execute_statements(body, context, scope, values)? { + EvalControl::Throw(thrown) => { + execute_matching_catch(thrown, catches, context, scope, values)? + } + control => control, }; + if finally_body.is_empty() { + return Ok(control); + } + match execute_statements(finally_body, context, scope, values) { + Ok(EvalControl::None) => Ok(control), + Ok(finally_control) => { + release_overridden_control(control, values)?; + Ok(finally_control) + } + Err(status) => { + release_overridden_control(control, values)?; + Err(status) + } + } +} + +/// Releases a pending control-flow value when `finally` replaces that action. +fn release_overridden_control( + control: EvalControl, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match control { + EvalControl::Return(value) | EvalControl::Throw(value) => values.release(value), + EvalControl::None | EvalControl::Break | EvalControl::Continue => Ok(()), + } +} + +/// Executes the first supported catch clause for a thrown eval object. +fn execute_matching_catch( + thrown: RuntimeCellHandle, + catches: &[EvalCatch], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { let Some(catch) = catches .iter() .find(|catch| catch_type_matches_throwable(&catch.class_name)) @@ -13723,11 +13765,16 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has collect_static_var_names(&case.body, names); } } - EvalStmt::Try { body, catches } => { + EvalStmt::Try { + body, + catches, + finally_body, + } => { collect_static_var_names(body, names); for catch in catches { collect_static_var_names(&catch.body, names); } + collect_static_var_names(finally_body, names); } EvalStmt::ArrayAppendVar { .. } | EvalStmt::ArraySetVar { .. } @@ -15009,6 +15056,101 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(42)); } + /// Verifies eval `finally` runs before a pending try-body return is observed. + #[test] + fn execute_program_runs_finally_before_returning_try_value() { + let program = parse_fragment( + br#"try { + return 1; +} finally { + echo "finally"; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "finally"); + assert_eq!(values.get(result), FakeValue::Int(1)); + } + + /// Verifies eval `finally` return values replace pending try-body returns. + #[test] + fn execute_program_finally_return_overrides_try_return() { + let program = parse_fragment( + br#"try { + return 1; +} finally { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.releases.len(), 1); + } + + /// Verifies eval `finally` return values replace pending uncaught throws. + #[test] + fn execute_program_finally_return_overrides_uncaught_throw() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} finally { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("overridden throw should be released"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); + } + + /// Verifies eval `finally` runs before an uncaught throw leaves the fragment. + #[test] + fn execute_program_runs_finally_before_uncaught_throw_outcome() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} finally { + echo "finally"; +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = execute_program_outcome_with_context( + &mut context, + &program, + &mut scope, + &mut values, + ) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + assert_eq!(values.output, "finally"); + } + /// Verifies static locals declared inside eval catch blocks persist per function context. #[test] fn execute_context_function_persists_static_local_inside_catch() { @@ -15047,6 +15189,36 @@ mod tests { assert_eq!(values.get(second), FakeValue::Int(44)); } + /// Verifies static locals declared inside eval finally blocks persist per function context. + #[test] + fn execute_context_function_persists_static_local_inside_finally() { + let program = parse_fragment( + br#"function dyn() { + try { + return 0; + } finally { + static $n = 0; + $n++; + return $n; + } +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + + let first = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute first dynamic function call"); + let second = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(2)); + } + /// Verifies throws from eval-declared functions escape through the shared context. #[test] fn execute_context_function_propagates_throw_as_uncaught_outcome() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index f9ed2d369b..0d356fb670 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1056,7 +1056,7 @@ impl Parser { Ok(vec![EvalStmt::Throw(expr)]) } - /// Parses `try { ... } catch (Throwable $name) { ... }` statements. + /// Parses `try { ... } catch (Throwable $name) { ... } finally { ... }` statements. fn parse_try_stmt(&mut self) -> Result, EvalParseError> { self.advance(); let body = self.parse_block()?; @@ -1064,13 +1064,21 @@ impl Parser { while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { catches.push(self.parse_catch_clause()?); } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) { - return Err(EvalParseError::UnsupportedConstruct); - } - if catches.is_empty() { + let finally_body = + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) { + self.advance(); + self.parse_block()? + } else { + Vec::new() + }; + if catches.is_empty() && finally_body.is_empty() { return Err(EvalParseError::UnexpectedToken); } - Ok(vec![EvalStmt::Try { body, catches }]) + Ok(vec![EvalStmt::Try { + body, + catches, + finally_body, + }]) } /// Parses one supported `catch (Throwable $name) { ... }` clause. @@ -3939,6 +3947,7 @@ function dyn() { return alias(); }"#, var_name: "caught".to_string(), body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], }], + finally_body: Vec::new(), }] ); } @@ -3966,6 +3975,7 @@ try { "caught".to_string() )))], }], + finally_body: Vec::new(), }] ); } @@ -3995,12 +4005,21 @@ try { ); } - /// Verifies eval try/finally remains outside the supported EvalIR subset. + /// Verifies try/finally statements lower the finalizer block into EvalIR. #[test] - fn parse_fragment_rejects_eval_finally_source() { + fn parse_fragment_accepts_eval_finally_source() { + let program = + parse_fragment(br#"try { return 1; } finally { echo "finally"; }"#) + .expect("fragment should parse"); assert_eq!( - parse_fragment(br#"try { echo "try"; } finally { echo "finally"; }"#), - Err(EvalParseError::UnsupportedConstruct) + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + catches: Vec::new(), + finally_body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "finally".to_string() + )))], + }] ); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 11004a1ec5..53d2828dd1 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,9 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable`, `finally`, and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. + +Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. The eval callable dispatcher currently normalizes string callbacks and two-element object-method callable arrays such as `[$this, "method"]`. Object-method arrays are supported for direct `$cb(...)`, `call_user_func()`, `call_user_func_array()`, and `iterator_apply()` paths with positional arguments; static-method callable arrays and closure/descriptor callback values are still handled only by the native descriptor runtime outside eval fragments. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 62ca12b8d2..e6d7378690 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,6 +34,8 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. + Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, ...)`, and `call_user_func_array($cb, [...])` with positional arguments. Static-method callable arrays and closure/descriptor callbacks are still outside the eval fragment subset. Eval `match` expressions support strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default` fallback. A miss without `default` is currently reported as an eval runtime fatal. @@ -124,7 +126,7 @@ Eval realpath-cache calls match elephc's native no-cache convention: `realpath_c Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch` can catch `Throwable` clauses inside the fragment; specific catch-type narrowing such as `catch (Exception $e)`, union catches, and `finally` remain outside the EvalIR subset. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch/finally` can catch `Throwable` clauses inside the fragment and always runs `finally` before propagating or returning; specific catch-type narrowing such as `catch (Exception $e)` and union catches remain outside the EvalIR subset. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f0e6e95934..fa3cd67276 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4586,3 +4586,18 @@ return 0;'); ); assert_eq!(out, "7"); } + +/// Verifies eval-internal finally runs before returning from the fragment. +#[test] +fn test_eval_finally_runs_before_eval_return() { + let out = compile_and_run( + r#" Date: Tue, 16 Jun 2026 20:29:40 +0200 Subject: [PATCH 0235/1208] Support eval catch without variable --- crates/elephc-eval/src/eval_ir.rs | 2 +- crates/elephc-eval/src/interpreter.rs | 46 ++++++++++++++++++++++----- crates/elephc-eval/src/parser.rs | 46 +++++++++++++++++++++++---- docs/internals/the-runtime.md | 2 ++ docs/php/system-and-io.md | 2 ++ tests/codegen/eval.rs | 16 ++++++++++ 6 files changed, 98 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index d5dff41be5..203470454e 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -132,7 +132,7 @@ pub enum EvalStmt { #[derive(Debug, Clone, PartialEq)] pub struct EvalCatch { pub class_name: String, - pub var_name: String, + pub var_name: Option, pub body: Vec, } diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 3b452b7489..6d48c28956 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1092,14 +1092,18 @@ fn execute_matching_catch( else { return Ok(EvalControl::Throw(thrown)); }; - for replaced in set_scope_cell( - context, - scope, - catch.var_name.clone(), - thrown, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; + if let Some(var_name) = &catch.var_name { + for replaced in set_scope_cell( + context, + scope, + var_name.clone(), + thrown, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } else { + values.release(thrown)?; } execute_statements(&catch.body, context, scope, values) } @@ -15056,6 +15060,32 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(42)); } + /// Verifies eval `catch (Throwable)` can handle a throw without binding a variable. + #[test] + fn execute_program_catches_throwable_without_variable_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable) { + return 9; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("unbound catch should release the thrown object"); + + assert_eq!(scope.visible_cell("caught"), None); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(9)); + } + /// Verifies eval `finally` runs before a pending try-body return is observed. #[test] fn execute_program_runs_finally_before_returning_try_value() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 0d356fb670..cf2203d15f 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1081,16 +1081,18 @@ impl Parser { }]) } - /// Parses one supported `catch (Throwable $name) { ... }` clause. + /// Parses one supported `catch (Throwable [$name]) { ... }` clause. fn parse_catch_clause(&mut self) -> Result { self.advance(); self.expect(TokenKind::LParen)?; let class_name = self.parse_supported_catch_type()?; - let TokenKind::DollarIdent(var_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); + let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { + let var_name = var_name.clone(); + self.advance(); + Some(var_name) + } else { + None }; - let var_name = var_name.clone(); - self.advance(); self.expect(TokenKind::RParen)?; let body = self.parse_block()?; Ok(EvalCatch { @@ -3944,7 +3946,7 @@ function dyn() { return alias(); }"#, })], catches: vec![EvalCatch { class_name: "Throwable".to_string(), - var_name: "caught".to_string(), + var_name: Some("caught".to_string()), body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], }], finally_body: Vec::new(), @@ -3970,7 +3972,7 @@ try { body: vec![EvalStmt::Throw(EvalExpr::LoadVar("e".to_string()))], catches: vec![EvalCatch { class_name: "Throwable".to_string(), - var_name: "caught".to_string(), + var_name: Some("caught".to_string()), body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( "caught".to_string() )))], @@ -3980,6 +3982,36 @@ try { ); } + /// Verifies Throwable catch clauses can omit the catch variable like PHP. + #[test] + fn parse_fragment_accepts_try_catch_without_variable() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_name: "Throwable".to_string(), + var_name: None, + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); + } + /// Verifies unsupported catch type narrowing stays explicit until runtime matching exists. #[test] fn parse_fragment_rejects_specific_eval_catch_type() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 53d2828dd1..ca258bd617 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -21,6 +21,8 @@ The bridge crate parses the fragment at runtime into a small EvalIR and interpre Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. +Eval `catch (Throwable)` may omit the variable binding; in that case the interpreter consumes the throwable and releases the unbound cell before executing the catch body. + The eval callable dispatcher currently normalizes string callbacks and two-element object-method callable arrays such as `[$this, "method"]`. Object-method arrays are supported for direct `$cb(...)`, `call_user_func()`, `call_user_func_array()`, and `iterator_apply()` paths with positional arguments; static-method callable arrays and closure/descriptor callback values are still handled only by the native descriptor runtime outside eval fragments. Eval `match` expressions live entirely inside the interpreter: the subject is evaluated once, patterns are compared through the existing strict-comparison value hook, result arms are evaluated lazily, and a miss without `default` returns the eval runtime-fatal status. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index e6d7378690..3dc744d100 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -36,6 +36,8 @@ The evaluated string must be a PHP fragment without an opening ` Date: Tue, 16 Jun 2026 20:42:37 +0200 Subject: [PATCH 0236/1208] Support eval array splice replacement --- crates/elephc-eval/src/interpreter.rs | 146 ++++++++++++++++++++++---- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 17 ++- 4 files changed, 143 insertions(+), 24 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6d48c28956..8fe3ed282a 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -60,6 +60,14 @@ enum EvaluatedCallable { }, } +/// Bound argument tuple for direct `array_splice()` calls. +type EvalArraySpliceDirectArgs = ( + String, + RuntimeCellHandle, + Option, + Option, +); + /// Parsed flags for one eval `sprintf()` conversion specifier. #[derive(Clone, Copy)] struct EvalSprintfSpec { @@ -2413,7 +2421,7 @@ fn eval_builtin_call( values: &mut impl RuntimeValueOps, ) -> Result { let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args)?; + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { return Err(EvalStatus::UnsupportedConstruct); }; @@ -2424,6 +2432,7 @@ fn eval_builtin_call( fn bind_evaluated_builtin_args( name: &str, evaluated_args: Vec, + values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { if evaluated_args.iter().all(|arg| arg.name.is_none()) { return Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()); @@ -2441,7 +2450,7 @@ fn bind_evaluated_builtin_args( } } - collect_contiguous_bound_args(bound_args) + collect_bound_builtin_args(name, bound_args, values) } /// Binds one named builtin-call value to the matching PHP parameter slot. @@ -2475,6 +2484,20 @@ fn collect_contiguous_bound_args( .ok_or(EvalStatus::RuntimeFatal) } +/// Collects ordered builtin arguments, applying PHP defaults for special named-call gaps. +fn collect_bound_builtin_args( + name: &str, + mut bound_args: Vec>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if name == "array_splice" && bound_args.get(3).is_some_and(Option::is_some) { + if bound_args.get(2).is_some_and(Option::is_none) { + bound_args[2] = Some(values.null()?); + } + } + collect_contiguous_bound_args(bound_args) +} + /// Returns PHP parameter names for builtin calls implemented by eval. fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { match name { @@ -2499,7 +2522,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), "array_slice" => Some(&["array", "offset", "length"]), - "array_splice" => Some(&["array", "offset", "length"]), + "array_splice" => Some(&["array", "offset", "length", "replacement"]), "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), "atan2" => Some(&["y", "x"]), @@ -2800,7 +2823,7 @@ fn eval_callable_with_call_array_args( return eval_callable_with_values(name, evaluated_args, context, values); } if eval_php_visible_builtin_exists(name) { - let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args)?; + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { return Err(EvalStatus::UnsupportedConstruct); }; @@ -2928,6 +2951,9 @@ fn eval_builtin_with_values( [array, offset, length] => { eval_array_splice_value_result(*array, *offset, Some(*length), values)? } + [array, offset, length, _replacement] => { + eval_array_splice_value_result(*array, *offset, Some(*length), values)? + } _ => return Err(EvalStatus::RuntimeFatal), }; values.warning( @@ -4335,14 +4361,14 @@ fn eval_builtin_array_push_unshift_call( Ok(result) } -/// Evaluates direct by-reference `array_splice()` calls in eval's supported 2/3-argument form. +/// Evaluates direct by-reference `array_splice()` calls and writes back the array. fn eval_builtin_array_splice_call( args: &[EvalCallArg], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let (array_name, offset, length) = + let (array_name, offset, length, replacement_arg) = eval_array_splice_direct_args(args, context, scope, values)?; let Some(entry) = scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) @@ -4356,7 +4382,7 @@ fn eval_builtin_array_splice_call( } let (removed, replacement) = - eval_array_splice_removed_and_replacement(array, offset, length, values)?; + eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { values.release(replaced)?; } @@ -4944,10 +4970,11 @@ fn eval_array_splice_direct_args( context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, -) -> Result<(String, RuntimeCellHandle, Option), EvalStatus> { +) -> Result { let mut array = None; let mut offset = None; let mut length = None; + let mut replacement = None; let mut positional_index = 0; let mut saw_named = false; @@ -4966,6 +4993,7 @@ fn eval_array_splice_direct_args( 0 => "array", 1 => "offset", 2 => "length", + 3 => "replacement", _ => return Err(EvalStatus::RuntimeFatal), }; positional_index += 1; @@ -4994,13 +5022,19 @@ fn eval_array_splice_direct_args( } length = Some(eval_expr(arg.value(), context, scope, values)?); } + "replacement" => { + if replacement.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + replacement = Some(eval_expr(arg.value(), context, scope, values)?); + } _ => return Err(EvalStatus::RuntimeFatal), } } let array = array.ok_or(EvalStatus::RuntimeFatal)?; let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, offset, length)) + Ok((array, offset, length, replacement)) } /// Returns the removed elements that `array_splice()` would produce without mutating the source. @@ -5022,11 +5056,13 @@ fn eval_array_splice_removed_and_replacement( array: RuntimeCellHandle, offset: RuntimeCellHandle, length: Option, + replacement: Option, values: &mut impl RuntimeValueOps, ) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; let removed = eval_array_splice_removed(array, start, end, values)?; - let replacement = eval_array_splice_replacement(array, start, end, values)?; + let inserted = eval_array_splice_insert_values(replacement, values)?; + let replacement = eval_array_splice_replacement(array, start, end, &inserted, values)?; Ok((removed, replacement)) } @@ -5087,21 +5123,59 @@ fn eval_array_splice_removed( Ok(result) } +/// Expands the optional `array_splice()` replacement value into inserted values. +fn eval_array_splice_insert_values( + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(replacement) = replacement else { + return Ok(Vec::new()); + }; + if !matches!( + values.type_tag(replacement)?, + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC + ) { + return Ok(vec![replacement]); + } + + let len = values.array_len(replacement)?; + let mut inserted = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(replacement, position)?; + inserted.push(values.array_get(replacement, key)?); + } + Ok(inserted) +} + /// Builds the source replacement after removing the requested splice range. fn eval_array_splice_replacement( array: RuntimeCellHandle, start: usize, end: usize, + inserted: &[RuntimeCellHandle], values: &mut impl RuntimeValueOps, ) -> Result { let len = values.array_len(array)?; + let new_len = len + .saturating_sub(end.saturating_sub(start)) + .checked_add(inserted.len()) + .ok_or(EvalStatus::RuntimeFatal)?; if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { - let mut result = values.array_new(len.saturating_sub(end.saturating_sub(start)))?; + let mut result = values.array_new(new_len)?; let mut target = 0_i64; - for position in 0..len { - if (start..end).contains(&position) { - continue; - } + for position in 0..start { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { let key = values.array_iter_key(array, position)?; let value = values.array_get(array, key)?; let target_key = values.int(target)?; @@ -5111,12 +5185,26 @@ fn eval_array_splice_replacement( return Ok(result); } - let mut result = values.assoc_new(len.saturating_sub(end.saturating_sub(start)))?; + let mut result = values.assoc_new(new_len)?; let mut next_int_key = 0_i64; - for position in 0..len { - if (start..end).contains(&position) { - continue; - } + for position in 0..start { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(next_int_key)?; + next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { let source_key = values.array_iter_key(array, position)?; let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { let key = values.int(next_int_key)?; @@ -17391,6 +17479,18 @@ echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; $d = [5, 6, 7]; $all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; +$e = [1, 2, 3, 4]; +$rep = array_splice($e, 1, 2, ["A", "B"]); +echo count($rep) . ":" . $rep[0] . ":" . $rep[1] . ":" . $e[0] . ":" . $e[1] . ":" . $e[2] . ":" . $e[3] . ":"; +$f = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$rep2 = array_splice(array: $f, offset: 1, length: 2, replacement: ["s" => "S", 9 => "N"]); +echo $rep2[0] . ":" . $rep2["y"] . ":" . $f["x"] . ":" . $f[0] . ":" . $f[1] . ":" . $f[2] . ":"; +$g = [1, 2, 3]; +$rep3 = array_splice($g, offset: 1, replacement: [9]); +echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g[1] . ":"; +$h = [1, 2, 3]; +$removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); +echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; return function_exists("array_splice");"#, ) .expect("parse eval fragment"); @@ -17399,12 +17499,16 @@ return function_exists("array_splice");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:"); + assert_eq!( + values.output, + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:" + ); assert_eq!( values.warnings, vec![ "array_splice(): Argument #1 ($array) must be passed by reference, value given", "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", ] ); assert_eq!(values.get(result), FakeValue::Bool(true)); diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index ca258bd617..22a8aa96cd 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -63,7 +63,7 @@ Eval `array_reduce()` reads the provided initial carry or creates a `null` carry Eval `array_walk()` iterates keys through the eval value hooks, invokes the shared eval callable dispatcher with value and key cells, discards callback results, and returns a boxed `true` cell. -Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order; the splice path supports elephc's two- or three-argument form and returns the removed array while reindexing integer keys and preserving string keys. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value, resulting push/unshift count, or removed splice array without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. +Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order; the splice path returns the removed array, supports optional `length` and `replacement`, expands replacement arrays in iteration order, reindexes integer keys, and preserves string keys. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value, resulting push/unshift count, or removed splice array without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. Eval `sort()`, `rsort()`, `asort()`, `arsort()`, `ksort()`, `krsort()`, `natsort()`, and `natcasesort()` reuse the same direct-variable write-back path. The interpreter snapshots array values through `RuntimeValueOps`, builds homogeneous numeric, byte-string, or natural-sort keys, reconstructs either an indexed replacement array or a key-preserving associative replacement, and returns `true`; dynamic callable dispatch validates the array and reports PHP's by-reference warning without mutating the original caller value. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 3dc744d100..211c662e3f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -78,7 +78,7 @@ Eval `array_reduce()` supports the two- or three-argument form with string callb Eval `array_walk()` supports the two-argument form with string callbacks. It invokes the callback with value and key cells in PHP iteration order, ignores the callback return value, and returns `true`. -Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` support direct variable calls with write-back into the eval scope. `array_pop()`, `array_shift()`, and `array_splice()` also accept named direct `array:` calls. Eval `array_splice()` supports the native elephc two- or three-argument form, returns the removed elements, reindexes integer keys, preserves string keys, and does not yet accept the replacement argument. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. +Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` support direct variable calls with write-back into the eval scope. `array_pop()`, `array_shift()`, and `array_splice()` also accept named direct `array:` calls. Eval `array_splice()` returns the removed elements, accepts the optional `length` and `replacement` arguments, treats omitted named `length` as `null` when `replacement:` is supplied, inserts replacement arrays by value order while discarding replacement keys, reindexes integer keys, and preserves string keys. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. Eval `sort()` and `rsort()` support one-argument direct variable calls with write-back into the eval scope for homogeneous numeric or string arrays. They reindex integer keys, return `true`, accept named direct `array:` calls, and emit PHP's by-reference warning without mutating the caller's original array when reached through `call_user_func()` or `call_user_func_array()`. Eval `asort()` and `arsort()` sort by value while preserving keys, `ksort()` / `krsort()` sort by key while preserving values, and `natsort()` / `natcasesort()` preserve keys while applying natural string ordering. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 718170b34c..b71f5c5ed7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1051,10 +1051,25 @@ echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; $d = [5, 6, 7]; $all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; +$e = [1, 2, 3, 4]; +$rep = array_splice($e, 1, 2, ["A", "B"]); +echo count($rep) . ":" . $rep[0] . ":" . $rep[1] . ":" . $e[0] . ":" . $e[1] . ":" . $e[2] . ":" . $e[3] . ":"; +$f = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$rep2 = array_splice(array: $f, offset: 1, length: 2, replacement: ["s" => "S", 9 => "N"]); +echo $rep2[0] . ":" . $rep2["y"] . ":" . $f["x"] . ":" . $f[0] . ":" . $f[1] . ":" . $f[2] . ":"; +$g = [1, 2, 3]; +$rep3 = array_splice($g, offset: 1, replacement: [9]); +echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g[1] . ":"; +$h = [1, 2, 3]; +$removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); +echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; echo function_exists("array_splice");'); "#, ); - assert_eq!(out, "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:1"); + assert_eq!( + out, + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:1" + ); } /// Verifies eval `sort()` and `rsort()` mutate direct variable arguments only. From 1a1fd15ea7eba4c1c2b93e4a2fd59a84aa55bfb0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 20:53:23 +0200 Subject: [PATCH 0237/1208] Track eval JSON error state --- crates/elephc-eval/src/context.rs | 29 ++++ crates/elephc-eval/src/interpreter.rs | 184 +++++++++++++++++++----- crates/elephc-eval/src/json_validate.rs | 155 +++++++++++--------- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 25 +++- 6 files changed, 286 insertions(+), 111 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index c47266007d..79b7870e0c 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -95,6 +95,8 @@ pub struct ElephcEvalContext { global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, pending_throw: Option, + json_last_error: i64, + json_last_error_msg: String, call_file: String, call_dir: String, call_line: i64, @@ -115,6 +117,8 @@ impl ElephcEvalContext { global_scope: None, function_stack: Vec::new(), pending_throw: None, + json_last_error: 0, + json_last_error_msg: String::from("No error"), call_file: String::new(), call_dir: String::new(), call_line: 0, @@ -136,6 +140,8 @@ impl ElephcEvalContext { global_scope: None, function_stack: Vec::new(), pending_throw: None, + json_last_error: 0, + json_last_error_msg: String::from("No error"), call_file: String::new(), call_dir: String::new(), call_line: 0, @@ -309,6 +315,29 @@ impl ElephcEvalContext { self.pending_throw.take() } + /// Clears the eval-local JSON error state after a successful JSON operation. + pub fn clear_json_error(&mut self) { + self.json_last_error = 0; + self.json_last_error_msg.clear(); + self.json_last_error_msg.push_str("No error"); + } + + /// Records the eval-local JSON error state for `json_last_error*()` calls. + pub fn set_json_error(&mut self, code: i64, message: impl Into) { + self.json_last_error = code; + self.json_last_error_msg = message.into(); + } + + /// Returns the PHP `JSON_ERROR_*` code for the last eval JSON operation. + pub const fn json_last_error(&self) -> i64 { + self.json_last_error + } + + /// Returns the PHP message for the last eval JSON operation. + pub fn json_last_error_msg(&self) -> &str { + &self.json_last_error_msg + } + /// Updates the source file, directory, and line for the current eval call site. pub fn set_call_site(&mut self, file: impl Into, dir: impl Into, line: i64) { self.call_file = file.into(); diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 8fe3ed282a..00b9ba0a9e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -17,7 +17,7 @@ use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; -use crate::json_validate::{self, JsonValue}; +use crate::json_validate::{self, JsonParseError, JsonValue}; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; @@ -582,6 +582,17 @@ const EVAL_PREG_PATTERN_ORDER: i64 = 1; const EVAL_PREG_SET_ORDER: i64 = 2; const EVAL_PREG_OFFSET_CAPTURE: i64 = 256; const EVAL_PREG_UNMATCHED_AS_NULL: i64 = 512; +const EVAL_JSON_ERROR_NONE: i64 = 0; +const EVAL_JSON_ERROR_DEPTH: i64 = 1; +const EVAL_JSON_ERROR_STATE_MISMATCH: i64 = 2; +const EVAL_JSON_ERROR_CTRL_CHAR: i64 = 3; +const EVAL_JSON_ERROR_SYNTAX: i64 = 4; +const EVAL_JSON_ERROR_UTF8: i64 = 5; +const EVAL_JSON_ERROR_RECURSION: i64 = 6; +const EVAL_JSON_ERROR_INF_OR_NAN: i64 = 7; +const EVAL_JSON_ERROR_UNSUPPORTED_TYPE: i64 = 8; +const EVAL_JSON_ERROR_INVALID_PROPERTY_NAME: i64 = 9; +const EVAL_JSON_ERROR_UTF16: i64 = 10; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -1820,8 +1831,8 @@ fn eval_positional_expr_call( "ip2long" => eval_builtin_ip2long(args, context, scope, values), "json_decode" => eval_builtin_json_decode(args, context, scope, values), "json_encode" => eval_builtin_json_encode(args, context, scope, values), - "json_last_error" => eval_builtin_json_last_error(args, values), - "json_last_error_msg" => eval_builtin_json_last_error_msg(args, values), + "json_last_error" => eval_builtin_json_last_error(args, context, values), + "json_last_error_msg" => eval_builtin_json_last_error_msg(args, context, values), "json_validate" => eval_builtin_json_validate(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), @@ -3514,27 +3525,37 @@ fn eval_builtin_with_values( } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, "json_decode" => match evaluated_args { - [json] => eval_json_decode_result(*json, None, None, None, values)?, + [json] => eval_json_decode_result(*json, None, None, None, context, values)?, [json, associative] => { - eval_json_decode_result(*json, Some(*associative), None, None, values)? + eval_json_decode_result(*json, Some(*associative), None, None, context, values)? } [json, associative, depth] => { - eval_json_decode_result(*json, Some(*associative), Some(*depth), None, values)? + eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + None, + context, + values, + )? } [json, associative, depth, flags] => eval_json_decode_result( *json, Some(*associative), Some(*depth), Some(*flags), + context, values, )?, _ => return Err(EvalStatus::RuntimeFatal), }, "json_encode" => match evaluated_args { - [value] => eval_json_encode_result(*value, None, None, values)?, - [value, flags] => eval_json_encode_result(*value, Some(*flags), None, values)?, + [value] => eval_json_encode_result(*value, None, None, context, values)?, + [value, flags] => { + eval_json_encode_result(*value, Some(*flags), None, context, values)? + } [value, flags, depth] => { - eval_json_encode_result(*value, Some(*flags), Some(*depth), values)? + eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? } _ => return Err(EvalStatus::RuntimeFatal), }, @@ -3542,19 +3563,19 @@ fn eval_builtin_with_values( if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - values.int(0)? + values.int(context.json_last_error())? } "json_last_error_msg" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - values.string_bytes_value(b"No error")? + values.string(context.json_last_error_msg())? } "json_validate" => match evaluated_args { - [json] => eval_json_validate_result(*json, None, None, values)?, - [json, depth] => eval_json_validate_result(*json, Some(*depth), None, values)?, + [json] => eval_json_validate_result(*json, None, None, context, values)?, + [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values)?, [json, depth, flags] => { - eval_json_validate_result(*json, Some(*depth), Some(*flags), values)? + eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values)? } _ => return Err(EvalStatus::RuntimeFatal), }, @@ -13046,18 +13067,18 @@ fn eval_builtin_json_encode( match args { [value] => { let value = eval_expr(value, context, scope, values)?; - eval_json_encode_result(value, None, None, values) + eval_json_encode_result(value, None, None, context, values) } [value, flags] => { let value = eval_expr(value, context, scope, values)?; let flags = eval_expr(flags, context, scope, values)?; - eval_json_encode_result(value, Some(flags), None, values) + eval_json_encode_result(value, Some(flags), None, context, values) } [value, flags, depth] => { let value = eval_expr(value, context, scope, values)?; let flags = eval_expr(flags, context, scope, values)?; let depth = eval_expr(depth, context, scope, values)?; - eval_json_encode_result(value, Some(flags), Some(depth), values) + eval_json_encode_result(value, Some(flags), Some(depth), context, values) } _ => Err(EvalStatus::RuntimeFatal), } @@ -13068,6 +13089,7 @@ fn eval_json_encode_result( value: RuntimeCellHandle, flags: Option, depth: Option, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if flags @@ -13088,6 +13110,7 @@ fn eval_json_encode_result( let mut output = Vec::new(); eval_json_encode_append(value, values, depth as usize, 0, &mut Vec::new(), &mut output)?; + context.clear_json_error(); values.string_bytes_value(&output) } @@ -13101,36 +13124,44 @@ fn eval_builtin_json_decode( match args { [json] => { let json = eval_expr(json, context, scope, values)?; - eval_json_decode_result(json, None, None, None, values) + eval_json_decode_result(json, None, None, None, context, values) } [json, associative] => { let json = eval_expr(json, context, scope, values)?; let associative = eval_expr(associative, context, scope, values)?; - eval_json_decode_result(json, Some(associative), None, None, values) + eval_json_decode_result(json, Some(associative), None, None, context, values) } [json, associative, depth] => { let json = eval_expr(json, context, scope, values)?; let associative = eval_expr(associative, context, scope, values)?; let depth = eval_expr(depth, context, scope, values)?; - eval_json_decode_result(json, Some(associative), Some(depth), None, values) + eval_json_decode_result(json, Some(associative), Some(depth), None, context, values) } [json, associative, depth, flags] => { let json = eval_expr(json, context, scope, values)?; let associative = eval_expr(associative, context, scope, values)?; let depth = eval_expr(depth, context, scope, values)?; let flags = eval_expr(flags, context, scope, values)?; - eval_json_decode_result(json, Some(associative), Some(depth), Some(flags), values) + eval_json_decode_result( + json, + Some(associative), + Some(depth), + Some(flags), + context, + values, + ) } _ => Err(EvalStatus::RuntimeFatal), } } -/// Decodes one JSON string into eval runtime cells, returning null on syntax/depth failure. +/// Decodes one JSON string into eval runtime cells and records PHP JSON parse state. fn eval_json_decode_result( json: RuntimeCellHandle, associative: Option, depth: Option, flags: Option, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if flags @@ -13153,9 +13184,14 @@ fn eval_json_decode_result( } let bytes = values.string_bytes(json)?; - let Some(decoded) = json_validate::decode(&bytes, depth as usize) else { - return values.null(); + let decoded = match json_validate::decode_result(&bytes, depth as usize) { + Ok(decoded) => decoded, + Err(error) => { + eval_record_json_parse_error(context, error); + return values.null(); + } }; + context.clear_json_error(); eval_json_decode_to_cell(decoded, values) } @@ -13206,26 +13242,28 @@ fn eval_json_decode_number_to_cell( values.float(float) } -/// Evaluates PHP `json_last_error()` with the eval interpreter's current no-error state. +/// Evaluates PHP `json_last_error()` from the eval interpreter's current JSON state. fn eval_builtin_json_last_error( args: &[EvalExpr], + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if !args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - values.int(0) + values.int(context.json_last_error()) } -/// Evaluates PHP `json_last_error_msg()` with the eval interpreter's current no-error state. +/// Evaluates PHP `json_last_error_msg()` from the eval interpreter's current JSON state. fn eval_builtin_json_last_error_msg( args: &[EvalExpr], + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if !args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - values.string_bytes_value(b"No error") + values.string(context.json_last_error_msg()) } /// Evaluates PHP `json_validate()` for zero-flag JSON text validation. @@ -13238,28 +13276,29 @@ fn eval_builtin_json_validate( match args { [json] => { let json = eval_expr(json, context, scope, values)?; - eval_json_validate_result(json, None, None, values) + eval_json_validate_result(json, None, None, context, values) } [json, depth] => { let json = eval_expr(json, context, scope, values)?; let depth = eval_expr(depth, context, scope, values)?; - eval_json_validate_result(json, Some(depth), None, values) + eval_json_validate_result(json, Some(depth), None, context, values) } [json, depth, flags] => { let json = eval_expr(json, context, scope, values)?; let depth = eval_expr(depth, context, scope, values)?; let flags = eval_expr(flags, context, scope, values)?; - eval_json_validate_result(json, Some(depth), Some(flags), values) + eval_json_validate_result(json, Some(depth), Some(flags), context, values) } _ => Err(EvalStatus::RuntimeFatal), } } -/// Validates JSON text with eval's current zero-flag JSON subset. +/// Validates JSON text with eval's current zero-flag JSON subset and records JSON state. fn eval_json_validate_result( json: RuntimeCellHandle, depth: Option, flags: Option, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if flags @@ -13279,7 +13318,42 @@ fn eval_json_validate_result( } let bytes = values.string_bytes(json)?; - values.bool_value(json_validate::bytes(&bytes, depth as usize)) + match json_validate::decode_result(&bytes, depth as usize) { + Ok(_) => { + context.clear_json_error(); + values.bool_value(true) + } + Err(error) => { + eval_record_json_parse_error(context, error); + values.bool_value(false) + } + } +} + +/// Records one parser error into the eval-local PHP JSON error slots. +fn eval_record_json_parse_error(context: &mut ElephcEvalContext, error: JsonParseError) { + let (code, message) = eval_json_parse_error_status(error); + context.set_json_error(code, message); +} + +/// Maps eval JSON parser failures to PHP `JSON_ERROR_*` codes and messages. +fn eval_json_parse_error_status(error: JsonParseError) -> (i64, &'static str) { + match error { + JsonParseError::Depth => (EVAL_JSON_ERROR_DEPTH, "Maximum stack depth exceeded"), + JsonParseError::Syntax => (EVAL_JSON_ERROR_SYNTAX, "Syntax error"), + JsonParseError::ControlChar => ( + EVAL_JSON_ERROR_CTRL_CHAR, + "Control character error, possibly incorrectly encoded", + ), + JsonParseError::Utf8 => ( + EVAL_JSON_ERROR_UTF8, + "Malformed UTF-8 characters, possibly incorrectly encoded", + ), + JsonParseError::Utf16 => ( + EVAL_JSON_ERROR_UTF16, + "Single unpaired UTF-16 surrogate in unicode escape", + ), + } } /// Appends one JSON value to the output buffer. @@ -14147,6 +14221,25 @@ fn eval_predefined_constant_value(name: &str) -> Option "PREG_UNMATCHED_AS_NULL" => { Some(EvalPredefinedConstant::Int(EVAL_PREG_UNMATCHED_AS_NULL)) } + "JSON_ERROR_NONE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_NONE)), + "JSON_ERROR_DEPTH" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_DEPTH)), + "JSON_ERROR_STATE_MISMATCH" => { + Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_STATE_MISMATCH)) + } + "JSON_ERROR_CTRL_CHAR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_CTRL_CHAR)), + "JSON_ERROR_SYNTAX" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_SYNTAX)), + "JSON_ERROR_UTF8" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF8)), + "JSON_ERROR_RECURSION" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_RECURSION)), + "JSON_ERROR_INF_OR_NAN" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_INF_OR_NAN)), + "JSON_ERROR_UNSUPPORTED_TYPE" => { + Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UNSUPPORTED_TYPE)) + } + "JSON_ERROR_INVALID_PROPERTY_NAME" => { + Some(EvalPredefinedConstant::Int( + EVAL_JSON_ERROR_INVALID_PROPERTY_NAME, + )) + } + "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), @@ -17212,14 +17305,26 @@ return function_exists("json_decode");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } - /// Verifies eval `json_last_error()` reports the no-error JSON status. + /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. #[test] fn execute_program_dispatches_json_last_error_builtins() { let program = parse_fragment( br#"echo json_last_error() . ":" . json_last_error_msg() . ":"; -echo call_user_func("json_last_error") . ":"; -echo call_user_func_array("json_last_error_msg", []) . ":"; -return function_exists("json_last_error") && function_exists("json_last_error_msg");"#, +json_decode("bad"); +echo json_last_error() . ":" . (json_last_error() === JSON_ERROR_SYNTAX ? "syntax" : "bad") . ":" . json_last_error_msg() . ":"; +json_validate("[1]", 1); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("\"ok\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("\"a" . chr(10) . "b\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("\"\\uD83D\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("\"" . chr(128) . "\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("[0]"); +echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_error_msg", []) . ":"; +return function_exists("json_last_error") && function_exists("json_last_error_msg") && defined("JSON_ERROR_SYNTAX");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17227,7 +17332,10 @@ return function_exists("json_last_error") && function_exists("json_last_error_ms let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "0:No error:0:No error:"); + assert_eq!( + values.output, + "0:No error:4:syntax:Syntax error:1:Maximum stack depth exceeded:0:No error:3:Control character error, possibly incorrectly encoded:10:Single unpaired UTF-16 surrogate in unicode escape:5:Malformed UTF-8 characters, possibly incorrectly encoded:0:No error:" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/json_validate.rs b/crates/elephc-eval/src/json_validate.rs index 6cfc00b31c..82778a42d3 100644 --- a/crates/elephc-eval/src/json_validate.rs +++ b/crates/elephc-eval/src/json_validate.rs @@ -21,13 +21,21 @@ pub(crate) enum JsonValue { Object(Vec<(Vec, JsonValue)>), } -/// Returns whether one byte slice is a complete JSON document within the depth limit. -pub(crate) fn bytes(bytes: &[u8], depth_limit: usize) -> bool { - decode(bytes, depth_limit).is_some() +/// PHP JSON error category produced while parsing eval-side JSON bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum JsonParseError { + Depth, + Syntax, + ControlChar, + Utf8, + Utf16, } -/// Parses one complete JSON document into an eval-side JSON value. -pub(crate) fn decode(bytes: &[u8], depth_limit: usize) -> Option { +/// Parses one complete JSON document and preserves the PHP-visible error category. +pub(crate) fn decode_result( + bytes: &[u8], + depth_limit: usize, +) -> Result { let mut parser = Parser::new(bytes, depth_limit); parser.parse_document() } @@ -50,62 +58,79 @@ impl<'a> Parser<'a> { } /// Parses one complete JSON document and rejects trailing non-whitespace bytes. - fn parse_document(&mut self) -> Option { + fn parse_document(&mut self) -> Result { self.skip_ws(); let value = self.parse_value(0)?; self.skip_ws(); - (self.cursor == self.bytes.len()).then_some(value) + if self.cursor == self.bytes.len() { + Ok(value) + } else { + Err(JsonParseError::Syntax) + } } /// Parses any JSON value at the given active container depth. - fn parse_value(&mut self, depth: usize) -> Option { + fn parse_value(&mut self, depth: usize) -> Result { self.skip_ws(); - match self.peek()? { - b'n' => self.consume_literal(b"null").then_some(JsonValue::Null), - b't' => self.consume_literal(b"true").then_some(JsonValue::Bool(true)), - b'f' => self.consume_literal(b"false").then_some(JsonValue::Bool(false)), + match self.peek().ok_or(JsonParseError::Syntax)? { + b'n' => self.consume_literal_value(b"null", JsonValue::Null), + b't' => self.consume_literal_value(b"true", JsonValue::Bool(true)), + b'f' => self.consume_literal_value(b"false", JsonValue::Bool(false)), b'"' => self.parse_string().map(JsonValue::String), b'[' => self.parse_array(depth), b'{' => self.parse_object(depth), b'-' | b'0'..=b'9' => self.parse_number().map(JsonValue::Number), - _ => None, + _ => Err(JsonParseError::Syntax), + } + } + + /// Consumes one JSON literal and returns its parsed value. + fn consume_literal_value( + &mut self, + literal: &[u8], + value: JsonValue, + ) -> Result { + if self.consume_literal(literal) { + Ok(value) + } else { + Err(JsonParseError::Syntax) } } /// Parses a JSON array and enforces PHP's validate/decode depth threshold. - fn parse_array(&mut self, depth: usize) -> Option { + fn parse_array(&mut self, depth: usize) -> Result { if depth + 1 >= self.depth_limit { - return None; + return Err(JsonParseError::Depth); } self.cursor += 1; self.skip_ws(); let mut elements = Vec::new(); if self.consume_byte(b']') { - return Some(JsonValue::Array(elements)); + return Ok(JsonValue::Array(elements)); } loop { elements.push(self.parse_value(depth + 1)?); self.skip_ws(); if self.consume_byte(b']') { - return Some(JsonValue::Array(elements)); + return Ok(JsonValue::Array(elements)); } if !self.consume_byte(b',') { - return None; + return Err(JsonParseError::Syntax); } } } /// Parses a JSON object and enforces PHP's validate/decode depth threshold. - fn parse_object(&mut self, depth: usize) -> Option { + fn parse_object(&mut self, depth: usize) -> Result { if depth + 1 >= self.depth_limit { - return None; + return Err(JsonParseError::Depth); } self.cursor += 1; self.skip_ws(); let mut entries = Vec::new(); if self.consume_byte(b'}') { - return Some(JsonValue::Object(entries)); + return Ok(JsonValue::Object(entries)); } loop { @@ -113,23 +138,23 @@ impl<'a> Parser<'a> { let key = self.parse_string()?; self.skip_ws(); if !self.consume_byte(b':') { - return None; + return Err(JsonParseError::Syntax); } entries.push((key, self.parse_value(depth + 1)?)); self.skip_ws(); if self.consume_byte(b'}') { - return Some(JsonValue::Object(entries)); + return Ok(JsonValue::Object(entries)); } if !self.consume_byte(b',') { - return None; + return Err(JsonParseError::Syntax); } } } /// Parses a JSON string into UTF-8 bytes after applying JSON escapes. - fn parse_string(&mut self) -> Option> { + fn parse_string(&mut self) -> Result, JsonParseError> { if !self.consume_byte(b'"') { - return None; + return Err(JsonParseError::Syntax); } let mut output = Vec::new(); @@ -137,12 +162,12 @@ impl<'a> Parser<'a> { match byte { b'"' => { self.cursor += 1; - return Some(output); + return Ok(output); } b'\\' => { self.parse_string_escape(&mut output)?; } - 0x00..=0x1f => return None, + 0x00..=0x1f => return Err(JsonParseError::ControlChar), 0x00..=0x7f => { output.push(byte); self.cursor += 1; @@ -154,13 +179,13 @@ impl<'a> Parser<'a> { } } } - None + Err(JsonParseError::Syntax) } /// Parses one JSON string escape sequence at the current backslash. - fn parse_string_escape(&mut self, output: &mut Vec) -> Option<()> { + fn parse_string_escape(&mut self, output: &mut Vec) -> Result<(), JsonParseError> { self.cursor += 1; - match self.peek()? { + match self.peek().ok_or(JsonParseError::Syntax)? { b'"' => output.push(b'"'), b'\\' => output.push(b'\\'), b'/' => output.push(b'/'), @@ -171,73 +196,71 @@ impl<'a> Parser<'a> { b't' => output.push(b'\t'), b'u' => { self.parse_unicode_escape(output)?; - return Some(()); + return Ok(()); } - _ => return None, + _ => return Err(JsonParseError::Syntax), } self.cursor += 1; - Some(()) + Ok(()) } /// Parses one JSON `\uXXXX` escape, including mandatory surrogate pairs. - fn parse_unicode_escape(&mut self, output: &mut Vec) -> Option<()> { + fn parse_unicode_escape(&mut self, output: &mut Vec) -> Result<(), JsonParseError> { let unit = self.parse_unicode_unit()?; if (0xd800..=0xdbff).contains(&unit) { - let checkpoint = self.cursor; if !self.consume_byte(b'\\') || !self.consume_byte(b'u') { - return None; + return Err(JsonParseError::Utf16); } - let Some(low) = self.parse_unicode_unit_after_u() else { - self.cursor = checkpoint; - return None; - }; + let low = self + .parse_unicode_unit_after_u() + .map_err(|_| JsonParseError::Utf16)?; if !(0xdc00..=0xdfff).contains(&low) { - return None; + return Err(JsonParseError::Utf16); } let high = u32::from(unit - 0xd800); let low = u32::from(low - 0xdc00); append_codepoint(output, 0x10000 + ((high << 10) | low)) } else if (0xdc00..=0xdfff).contains(&unit) { - None + Err(JsonParseError::Utf16) } else { append_codepoint(output, u32::from(unit)) } } /// Parses the `uXXXX` suffix after the backslash has already been consumed. - fn parse_unicode_unit(&mut self) -> Option { + fn parse_unicode_unit(&mut self) -> Result { if !self.consume_byte(b'u') { - return None; + return Err(JsonParseError::Syntax); } self.parse_unicode_unit_after_u() } /// Parses the four hex digits after a consumed JSON unicode escape marker. - fn parse_unicode_unit_after_u(&mut self) -> Option { + fn parse_unicode_unit_after_u(&mut self) -> Result { if self.cursor + 4 > self.bytes.len() { - return None; + return Err(JsonParseError::Syntax); } let mut value = 0_u16; for _ in 0..4 { - value = value.checked_mul(16)?; - value = value.checked_add(u16::from(hex_value(self.bytes[self.cursor])?))?; + let digit = hex_value(self.bytes[self.cursor]).ok_or(JsonParseError::Syntax)?; + value = value * 16 + u16::from(digit); self.cursor += 1; } - Some(value) + Ok(value) } /// Parses a JSON number with RFC-compatible leading-zero, fraction, and exponent rules. - fn parse_number(&mut self) -> Option> { + fn parse_number(&mut self) -> Result, JsonParseError> { let start = self.cursor; if self.consume_byte(b'-') && self.peek().is_none() { - return None; + return Err(JsonParseError::Syntax); } - match self.peek()? { + match self.peek().ok_or(JsonParseError::Syntax)? { b'0' => { self.cursor += 1; if matches!(self.peek(), Some(b'0'..=b'9')) { - return None; + return Err(JsonParseError::Syntax); } } b'1'..=b'9' => { @@ -246,12 +269,12 @@ impl<'a> Parser<'a> { self.cursor += 1; } } - _ => return None, + _ => return Err(JsonParseError::Syntax), } if self.consume_byte(b'.') { if !matches!(self.peek(), Some(b'0'..=b'9')) { - return None; + return Err(JsonParseError::Syntax); } while matches!(self.peek(), Some(b'0'..=b'9')) { self.cursor += 1; @@ -264,14 +287,14 @@ impl<'a> Parser<'a> { self.cursor += 1; } if !matches!(self.peek(), Some(b'0'..=b'9')) { - return None; + return Err(JsonParseError::Syntax); } while matches!(self.peek(), Some(b'0'..=b'9')) { self.cursor += 1; } } - Some(self.bytes[start..self.cursor].to_vec()) + Ok(self.bytes[start..self.cursor].to_vec()) } /// Consumes exactly one expected byte when it is present. @@ -295,23 +318,23 @@ impl<'a> Parser<'a> { } /// Consumes one valid UTF-8 codepoint from a raw JSON string segment. - fn consume_utf8_char(&mut self) -> Option<()> { + fn consume_utf8_char(&mut self) -> Result<(), JsonParseError> { let first = self.bytes[self.cursor]; let width = match first { 0xc2..=0xdf => 2, 0xe0..=0xef => 3, 0xf0..=0xf4 => 4, - _ => return None, + _ => return Err(JsonParseError::Utf8), }; if self.cursor + width > self.bytes.len() { - return None; + return Err(JsonParseError::Utf8); } let slice = &self.bytes[self.cursor..self.cursor + width]; if std::str::from_utf8(slice).is_err() { - return None; + return Err(JsonParseError::Utf8); } self.cursor += width; - Some(()) + Ok(()) } /// Skips JSON whitespace accepted between tokens. @@ -328,11 +351,11 @@ impl<'a> Parser<'a> { } /// Appends one Unicode codepoint to a decoded JSON string. -fn append_codepoint(output: &mut Vec, codepoint: u32) -> Option<()> { - let ch = char::from_u32(codepoint)?; +fn append_codepoint(output: &mut Vec, codepoint: u32) -> Result<(), JsonParseError> { + let ch = char::from_u32(codepoint).ok_or(JsonParseError::Utf16)?; let mut buffer = [0_u8; 4]; output.extend_from_slice(ch.encode_utf8(&mut buffer).as_bytes()); - Some(()) + Ok(()) } /// Returns one hexadecimal digit value for JSON unicode escapes. diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 22a8aa96cd..7e6988a6d5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. Until eval JSON validation/decoding is bridged to the native JSON error-state runtime, eval `json_last_error()` and `json_last_error_msg()` report the no-error state directly. +Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()` and reset to no-error after successful eval JSON calls. Eval still omits the native runtime's location-aware line/column suffix. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 211c662e3f..ec8ea1e611 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` currently report `JSON_ERROR_NONE` / `"No error"` because eval JSON validation/decode failures do not yet bridge into the native JSON error-state runtime. +Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval error messages do not yet include the native runtime's line/column location suffix. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b71f5c5ed7..e9b82263f9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -849,18 +849,33 @@ echo function_exists("json_decode");'); assert_eq!(out, "hello:42:T:NULL:1:x:F:4:v:BAD:1"); } -/// Verifies eval `json_last_error()` and `json_last_error_msg()` report no-error status. +/// Verifies eval `json_last_error*()` track JSON parse failures and success resets. #[test] fn test_eval_dispatches_json_last_error_builtin_calls() { let out = compile_and_run( r#" Date: Tue, 16 Jun 2026 20:59:17 +0200 Subject: [PATCH 0238/1208] Support eval preg match all set order --- crates/elephc-eval/src/interpreter.rs | 81 ++++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 6 +- 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 00b9ba0a9e..0719378e41 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -10478,7 +10478,27 @@ fn eval_builtin_preg_match_all( return Err(EvalStatus::RuntimeFatal); }; let (result, matches_array) = - eval_preg_match_all_capture_result(pattern, subject, values)?; + eval_preg_match_all_capture_result(pattern, subject, None, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; for replaced in set_scope_cell( context, scope, @@ -10510,6 +10530,7 @@ fn eval_preg_match_all_result( fn eval_preg_match_all_capture_result( pattern: RuntimeCellHandle, subject: RuntimeCellHandle, + flags: Option, values: &mut impl RuntimeValueOps, ) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { let regex = eval_preg_regex(pattern, values)?; @@ -10517,15 +10538,31 @@ fn eval_preg_match_all_capture_result( let subject = values.string_bytes(subject)?; let captures: Vec> = regex.captures_iter(&subject).collect(); let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let matches = eval_preg_match_all_pattern_order_array( - &subject, - &captures, - capture_count, - values, - )?; + let flags = eval_preg_match_all_flags(flags, values)?; + let matches = if flags & EVAL_PREG_SET_ORDER != 0 { + eval_preg_match_all_set_order_array(&subject, &captures, capture_count, values)? + } else { + eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, values)? + }; Ok((count, matches)) } +/// Returns supported `preg_match_all()` flags and rejects offset-capture shape changes. +fn eval_preg_match_all_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(EVAL_PREG_PATTERN_ORDER); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_PATTERN_ORDER | EVAL_PREG_SET_ORDER; + if flags & EVAL_PREG_OFFSET_CAPTURE != 0 || flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + /// Builds PHP's default `preg_match_all()` pattern-order capture matrix. fn eval_preg_match_all_pattern_order_array( subject: &[u8], @@ -10550,6 +10587,30 @@ fn eval_preg_match_all_pattern_order_array( Ok(outer) } +/// Builds PHP's `preg_match_all(..., PREG_SET_ORDER)` match-order capture matrix. +fn eval_preg_match_all_set_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut outer = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let mut row = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let key = values + .int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let bytes = eval_preg_capture_bytes(subject, capture, capture_index).unwrap_or(b""); + let value = values.string_bytes_value(bytes)?; + row = values.array_set(row, key, value)?; + } + let key = values + .int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + /// Evaluates PHP `preg_replace()` over eval expressions. fn eval_builtin_preg_replace( args: &[EvalExpr], @@ -18506,6 +18567,8 @@ echo preg_match("/xyz/", "id42") . ":"; echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; $allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; +$setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); +echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; preg_match_all("/(x)(y)/", "abc", $none); echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; @@ -18520,7 +18583,7 @@ $replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "repl echo $replaced . ":"; $captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); echo count($captured) . ":" . $captured[1] . ":"; -return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY");"#, +return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -18530,7 +18593,7 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun assert_eq!( values.output, - "1:3:id42:id:42:0:3:2:3:b22:a:22:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 7e6988a6d5..9c77df5e86 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -75,7 +75,7 @@ Eval `array_filter()` implements PHP's omitted/null callback form by iterating s Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false. Eval `iterator_apply()` drives Traversable objects through the eval method-call hooks (`rewind()`, `valid()`, and `next()`) and invokes string callbacks or object-method callable arrays through the shared eval callable dispatcher. The callback receives only the optional third-argument array, matching PHP's `iterator_apply()` behavior; current value/key are not passed implicitly. Array inputs remain runtime fatals for `iterator_apply()`. -Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax, offset-capture result shapes, or `preg_match_all()` set-order output surface as eval runtime fatals instead of falling back to a partial native ABI. +Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, direct `preg_match_all()` `PREG_SET_ORDER` captures, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax or offset-capture result shapes surface as eval runtime fatals instead of falling back to a partial native ABI. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ec8ea1e611..163b9fd761 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -90,7 +90,7 @@ Eval `array_filter()` supports the PHP default omitted/null callback form, filte Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` supports Traversable objects through `rewind()`, `valid()`, `next()`, string callbacks that target eval-declared functions, registered AOT callbacks, or supported builtins, and object-method callable arrays. Arrays still fail for `iterator_apply()`, matching PHP's array `TypeError` rather than treating arrays as Traversable. -Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; offset-capture flags, `preg_match_all()` set-order output, and PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). +Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form and `PREG_SET_ORDER` match-order form, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; offset-capture flags and PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e9b82263f9..07534c0008 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2399,6 +2399,8 @@ echo preg_match("/xyz/", "id42") . ":"; echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; $allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; +$setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); +echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; preg_match_all("/(x)(y)/", "abc", $none); echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; @@ -2413,12 +2415,12 @@ $replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "repl echo $replaced . ":"; $captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); echo count($captured) . ":" . $captured[1] . ":"; -echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY");'); +echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER");'); "#, ); assert_eq!( out, - "1:3:id42:id:42:0:3:2:3:b22:a:22:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:1" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:1" ); } From 3735151a4a5b714f283ab379f6797da89d6e4e5c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:04:26 +0200 Subject: [PATCH 0239/1208] Add eval JSON error locations --- crates/elephc-eval/src/interpreter.rs | 57 ++++++++++--- crates/elephc-eval/src/json_validate.rs | 105 ++++++++++++++++-------- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 4 +- 5 files changed, 117 insertions(+), 53 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0719378e41..908cf08ab1 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -17,7 +17,7 @@ use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalConst, EvalExpr, EvalFunction, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; -use crate::json_validate::{self, JsonParseError, JsonValue}; +use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; @@ -13248,7 +13248,7 @@ fn eval_json_decode_result( let decoded = match json_validate::decode_result(&bytes, depth as usize) { Ok(decoded) => decoded, Err(error) => { - eval_record_json_parse_error(context, error); + eval_record_json_parse_error(context, error, &bytes); return values.null(); } }; @@ -13385,38 +13385,69 @@ fn eval_json_validate_result( values.bool_value(true) } Err(error) => { - eval_record_json_parse_error(context, error); + eval_record_json_parse_error(context, error, &bytes); values.bool_value(false) } } } /// Records one parser error into the eval-local PHP JSON error slots. -fn eval_record_json_parse_error(context: &mut ElephcEvalContext, error: JsonParseError) { - let (code, message) = eval_json_parse_error_status(error); +fn eval_record_json_parse_error( + context: &mut ElephcEvalContext, + error: JsonParseError, + bytes: &[u8], +) { + let (code, message) = eval_json_parse_error_status(error.kind()); + let message = eval_json_error_message_with_location(message, bytes, error.offset()); context.set_json_error(code, message); } /// Maps eval JSON parser failures to PHP `JSON_ERROR_*` codes and messages. -fn eval_json_parse_error_status(error: JsonParseError) -> (i64, &'static str) { +fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str) { match error { - JsonParseError::Depth => (EVAL_JSON_ERROR_DEPTH, "Maximum stack depth exceeded"), - JsonParseError::Syntax => (EVAL_JSON_ERROR_SYNTAX, "Syntax error"), - JsonParseError::ControlChar => ( + JsonParseErrorKind::Depth => (EVAL_JSON_ERROR_DEPTH, "Maximum stack depth exceeded"), + JsonParseErrorKind::Syntax => (EVAL_JSON_ERROR_SYNTAX, "Syntax error"), + JsonParseErrorKind::ControlChar => ( EVAL_JSON_ERROR_CTRL_CHAR, "Control character error, possibly incorrectly encoded", ), - JsonParseError::Utf8 => ( + JsonParseErrorKind::Utf8 => ( EVAL_JSON_ERROR_UTF8, "Malformed UTF-8 characters, possibly incorrectly encoded", ), - JsonParseError::Utf16 => ( + JsonParseErrorKind::Utf16 => ( EVAL_JSON_ERROR_UTF16, "Single unpaired UTF-16 surrogate in unicode escape", ), } } +/// Adds PHP's JSON line/column suffix to one base error message. +fn eval_json_error_message_with_location( + message: &str, + bytes: &[u8], + offset: usize, +) -> String { + let (line, column) = eval_json_error_location(bytes, offset); + format!("{message} near location {line}:{column}") +} + +/// Converts a zero-based JSON byte offset into PHP-style one-based line and column. +fn eval_json_error_location(bytes: &[u8], offset: usize) -> (usize, usize) { + let mut line = 1; + let mut column = 1; + let offset = offset.min(bytes.len()); + for byte in &bytes[..offset] { + if *byte == b'\n' { + line += 1; + column = 1; + } else { + column += 1; + } + } + (line, column) +} + /// Appends one JSON value to the output buffer. fn eval_json_encode_append( value: RuntimeCellHandle, @@ -17381,7 +17412,7 @@ json_validate("\"a" . chr(10) . "b\""); echo json_last_error() . ":" . json_last_error_msg() . ":"; json_decode("\"\\uD83D\""); echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_decode("\"" . chr(128) . "\""); +json_decode("\"a" . chr(128) . "b\""); echo json_last_error() . ":" . json_last_error_msg() . ":"; json_validate("[0]"); echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_error_msg", []) . ":"; @@ -17395,7 +17426,7 @@ return function_exists("json_last_error") && function_exists("json_last_error_ms assert_eq!( values.output, - "0:No error:4:syntax:Syntax error:1:Maximum stack depth exceeded:0:No error:3:Control character error, possibly incorrectly encoded:10:Single unpaired UTF-16 surrogate in unicode escape:5:Malformed UTF-8 characters, possibly incorrectly encoded:0:No error:" + "0:No error:4:syntax:Syntax error near location 1:1:1:Maximum stack depth exceeded near location 1:1:0:No error:3:Control character error, possibly incorrectly encoded near location 1:3:10:Single unpaired UTF-16 surrogate in unicode escape near location 1:8:5:Malformed UTF-8 characters, possibly incorrectly encoded near location 1:3:0:No error:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/json_validate.rs b/crates/elephc-eval/src/json_validate.rs index 82778a42d3..cf6952893d 100644 --- a/crates/elephc-eval/src/json_validate.rs +++ b/crates/elephc-eval/src/json_validate.rs @@ -21,9 +21,33 @@ pub(crate) enum JsonValue { Object(Vec<(Vec, JsonValue)>), } +/// PHP JSON error produced while parsing eval-side JSON bytes. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct JsonParseError { + kind: JsonParseErrorKind, + offset: usize, +} + +impl JsonParseError { + /// Creates one parser error at a zero-based byte offset. + const fn new(kind: JsonParseErrorKind, offset: usize) -> Self { + Self { kind, offset } + } + + /// Returns the PHP JSON error category for this parse failure. + pub(crate) const fn kind(self) -> JsonParseErrorKind { + self.kind + } + + /// Returns the zero-based byte offset where parsing failed. + pub(crate) const fn offset(self) -> usize { + self.offset + } +} + /// PHP JSON error category produced while parsing eval-side JSON bytes. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub(crate) enum JsonParseError { +pub(crate) enum JsonParseErrorKind { Depth, Syntax, ControlChar, @@ -65,14 +89,14 @@ impl<'a> Parser<'a> { if self.cursor == self.bytes.len() { Ok(value) } else { - Err(JsonParseError::Syntax) + Err(self.error(JsonParseErrorKind::Syntax)) } } /// Parses any JSON value at the given active container depth. fn parse_value(&mut self, depth: usize) -> Result { self.skip_ws(); - match self.peek().ok_or(JsonParseError::Syntax)? { + match self.peek().ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? { b'n' => self.consume_literal_value(b"null", JsonValue::Null), b't' => self.consume_literal_value(b"true", JsonValue::Bool(true)), b'f' => self.consume_literal_value(b"false", JsonValue::Bool(false)), @@ -80,7 +104,7 @@ impl<'a> Parser<'a> { b'[' => self.parse_array(depth), b'{' => self.parse_object(depth), b'-' | b'0'..=b'9' => self.parse_number().map(JsonValue::Number), - _ => Err(JsonParseError::Syntax), + _ => Err(self.error(JsonParseErrorKind::Syntax)), } } @@ -93,14 +117,14 @@ impl<'a> Parser<'a> { if self.consume_literal(literal) { Ok(value) } else { - Err(JsonParseError::Syntax) + Err(self.error(JsonParseErrorKind::Syntax)) } } /// Parses a JSON array and enforces PHP's validate/decode depth threshold. fn parse_array(&mut self, depth: usize) -> Result { if depth + 1 >= self.depth_limit { - return Err(JsonParseError::Depth); + return Err(self.error(JsonParseErrorKind::Depth)); } self.cursor += 1; self.skip_ws(); @@ -116,7 +140,7 @@ impl<'a> Parser<'a> { return Ok(JsonValue::Array(elements)); } if !self.consume_byte(b',') { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } } } @@ -124,7 +148,7 @@ impl<'a> Parser<'a> { /// Parses a JSON object and enforces PHP's validate/decode depth threshold. fn parse_object(&mut self, depth: usize) -> Result { if depth + 1 >= self.depth_limit { - return Err(JsonParseError::Depth); + return Err(self.error(JsonParseErrorKind::Depth)); } self.cursor += 1; self.skip_ws(); @@ -138,7 +162,7 @@ impl<'a> Parser<'a> { let key = self.parse_string()?; self.skip_ws(); if !self.consume_byte(b':') { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } entries.push((key, self.parse_value(depth + 1)?)); self.skip_ws(); @@ -146,7 +170,7 @@ impl<'a> Parser<'a> { return Ok(JsonValue::Object(entries)); } if !self.consume_byte(b',') { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } } } @@ -154,7 +178,7 @@ impl<'a> Parser<'a> { /// Parses a JSON string into UTF-8 bytes after applying JSON escapes. fn parse_string(&mut self) -> Result, JsonParseError> { if !self.consume_byte(b'"') { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } let mut output = Vec::new(); @@ -167,7 +191,7 @@ impl<'a> Parser<'a> { b'\\' => { self.parse_string_escape(&mut output)?; } - 0x00..=0x1f => return Err(JsonParseError::ControlChar), + 0x00..=0x1f => return Err(self.error(JsonParseErrorKind::ControlChar)), 0x00..=0x7f => { output.push(byte); self.cursor += 1; @@ -179,13 +203,13 @@ impl<'a> Parser<'a> { } } } - Err(JsonParseError::Syntax) + Err(self.error(JsonParseErrorKind::Syntax)) } /// Parses one JSON string escape sequence at the current backslash. fn parse_string_escape(&mut self, output: &mut Vec) -> Result<(), JsonParseError> { self.cursor += 1; - match self.peek().ok_or(JsonParseError::Syntax)? { + match self.peek().ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? { b'"' => output.push(b'"'), b'\\' => output.push(b'\\'), b'/' => output.push(b'/'), @@ -198,7 +222,7 @@ impl<'a> Parser<'a> { self.parse_unicode_escape(output)?; return Ok(()); } - _ => return Err(JsonParseError::Syntax), + _ => return Err(self.error(JsonParseErrorKind::Syntax)), } self.cursor += 1; Ok(()) @@ -209,28 +233,31 @@ impl<'a> Parser<'a> { let unit = self.parse_unicode_unit()?; if (0xd800..=0xdbff).contains(&unit) { if !self.consume_byte(b'\\') || !self.consume_byte(b'u') { - return Err(JsonParseError::Utf16); + return Err(self.error(JsonParseErrorKind::Utf16)); } - let low = self - .parse_unicode_unit_after_u() - .map_err(|_| JsonParseError::Utf16)?; + let low = match self.parse_unicode_unit_after_u() { + Ok(low) => low, + Err(_) => return Err(self.error(JsonParseErrorKind::Utf16)), + }; if !(0xdc00..=0xdfff).contains(&low) { - return Err(JsonParseError::Utf16); + return Err(self.error(JsonParseErrorKind::Utf16)); } let high = u32::from(unit - 0xd800); let low = u32::from(low - 0xdc00); append_codepoint(output, 0x10000 + ((high << 10) | low)) + .ok_or_else(|| self.error(JsonParseErrorKind::Utf16)) } else if (0xdc00..=0xdfff).contains(&unit) { - Err(JsonParseError::Utf16) + Err(self.error(JsonParseErrorKind::Utf16)) } else { append_codepoint(output, u32::from(unit)) + .ok_or_else(|| self.error(JsonParseErrorKind::Utf16)) } } /// Parses the `uXXXX` suffix after the backslash has already been consumed. fn parse_unicode_unit(&mut self) -> Result { if !self.consume_byte(b'u') { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } self.parse_unicode_unit_after_u() } @@ -238,11 +265,12 @@ impl<'a> Parser<'a> { /// Parses the four hex digits after a consumed JSON unicode escape marker. fn parse_unicode_unit_after_u(&mut self) -> Result { if self.cursor + 4 > self.bytes.len() { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } let mut value = 0_u16; for _ in 0..4 { - let digit = hex_value(self.bytes[self.cursor]).ok_or(JsonParseError::Syntax)?; + let digit = hex_value(self.bytes[self.cursor]) + .ok_or_else(|| self.error(JsonParseErrorKind::Syntax))?; value = value * 16 + u16::from(digit); self.cursor += 1; } @@ -253,14 +281,14 @@ impl<'a> Parser<'a> { fn parse_number(&mut self) -> Result, JsonParseError> { let start = self.cursor; if self.consume_byte(b'-') && self.peek().is_none() { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } - match self.peek().ok_or(JsonParseError::Syntax)? { + match self.peek().ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? { b'0' => { self.cursor += 1; if matches!(self.peek(), Some(b'0'..=b'9')) { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } } b'1'..=b'9' => { @@ -269,12 +297,12 @@ impl<'a> Parser<'a> { self.cursor += 1; } } - _ => return Err(JsonParseError::Syntax), + _ => return Err(self.error(JsonParseErrorKind::Syntax)), } if self.consume_byte(b'.') { if !matches!(self.peek(), Some(b'0'..=b'9')) { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } while matches!(self.peek(), Some(b'0'..=b'9')) { self.cursor += 1; @@ -287,7 +315,7 @@ impl<'a> Parser<'a> { self.cursor += 1; } if !matches!(self.peek(), Some(b'0'..=b'9')) { - return Err(JsonParseError::Syntax); + return Err(self.error(JsonParseErrorKind::Syntax)); } while matches!(self.peek(), Some(b'0'..=b'9')) { self.cursor += 1; @@ -324,14 +352,14 @@ impl<'a> Parser<'a> { 0xc2..=0xdf => 2, 0xe0..=0xef => 3, 0xf0..=0xf4 => 4, - _ => return Err(JsonParseError::Utf8), + _ => return Err(self.error(JsonParseErrorKind::Utf8)), }; if self.cursor + width > self.bytes.len() { - return Err(JsonParseError::Utf8); + return Err(self.error(JsonParseErrorKind::Utf8)); } let slice = &self.bytes[self.cursor..self.cursor + width]; if std::str::from_utf8(slice).is_err() { - return Err(JsonParseError::Utf8); + return Err(self.error(JsonParseErrorKind::Utf8)); } self.cursor += width; Ok(()) @@ -348,14 +376,19 @@ impl<'a> Parser<'a> { fn peek(&self) -> Option { self.bytes.get(self.cursor).copied() } + + /// Creates a parser error at the current cursor byte offset. + fn error(&self, kind: JsonParseErrorKind) -> JsonParseError { + JsonParseError::new(kind, self.cursor) + } } /// Appends one Unicode codepoint to a decoded JSON string. -fn append_codepoint(output: &mut Vec, codepoint: u32) -> Result<(), JsonParseError> { - let ch = char::from_u32(codepoint).ok_or(JsonParseError::Utf16)?; +fn append_codepoint(output: &mut Vec, codepoint: u32) -> Option<()> { + let ch = char::from_u32(codepoint)?; let mut buffer = [0_u8; 4]; output.extend_from_slice(ch.encode_utf8(&mut buffer).as_bytes()); - Ok(()) + Some(()) } /// Returns one hexadecimal digit value for JSON unicode escapes. diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9c77df5e86..8a18a7ae5c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()` and reset to no-error after successful eval JSON calls. Eval still omits the native runtime's location-aware line/column suffix. +Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 163b9fd761..6ca4ef16ab 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval error messages do not yet include the native runtime's line/column location suffix. +Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 07534c0008..2510b7fc0e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -865,7 +865,7 @@ json_validate("\"a" . chr(10) . "b\""); echo json_last_error() . ":" . json_last_error_msg() . ":"; json_decode("\"\\uD83D\""); echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_decode("\"" . chr(128) . "\""); +json_decode("\"a" . chr(128) . "b\""); echo json_last_error() . ":" . json_last_error_msg() . ":"; json_validate("[0]"); echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_error_msg", []) . ":"; @@ -874,7 +874,7 @@ echo function_exists("json_last_error") && function_exists("json_last_error_msg" ); assert_eq!( out, - "0:No error:4:syntax:Syntax error:1:Maximum stack depth exceeded:0:No error:3:Control character error, possibly incorrectly encoded:10:Single unpaired UTF-16 surrogate in unicode escape:5:Malformed UTF-8 characters, possibly incorrectly encoded:0:No error:1" + "0:No error:4:syntax:Syntax error near location 1:1:1:Maximum stack depth exceeded near location 1:1:0:No error:3:Control character error, possibly incorrectly encoded near location 1:3:10:Single unpaired UTF-16 surrogate in unicode escape near location 1:8:5:Malformed UTF-8 characters, possibly incorrectly encoded near location 1:3:0:No error:1" ); } From b0906892176057beae53ca5827df07ca63289cc3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:10:55 +0200 Subject: [PATCH 0240/1208] Support eval JSON unescaped slashes --- crates/elephc-eval/src/interpreter.rs | 83 +++++++++++++++++++++------ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 7 ++- 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 908cf08ab1..4f5583b28c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -593,6 +593,7 @@ const EVAL_JSON_ERROR_INF_OR_NAN: i64 = 7; const EVAL_JSON_ERROR_UNSUPPORTED_TYPE: i64 = 8; const EVAL_JSON_ERROR_INVALID_PROPERTY_NAME: i64 = 9; const EVAL_JSON_ERROR_UTF16: i64 = 10; +const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -13145,7 +13146,7 @@ fn eval_builtin_json_encode( } } -/// Encodes one runtime cell as a JSON string when flags are zero. +/// Encodes one runtime cell as a JSON string for eval's supported flag subset. fn eval_json_encode_result( value: RuntimeCellHandle, flags: Option, @@ -13153,12 +13154,11 @@ fn eval_json_encode_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if flags + let flags = flags .map(|flags| eval_int_value(flags, values)) .transpose()? - .unwrap_or(0) - != 0 - { + .unwrap_or(0); + if flags & !EVAL_JSON_UNESCAPED_SLASHES != 0 { return Err(EvalStatus::UnsupportedConstruct); } let depth = depth @@ -13170,7 +13170,15 @@ fn eval_json_encode_result( } let mut output = Vec::new(); - eval_json_encode_append(value, values, depth as usize, 0, &mut Vec::new(), &mut output)?; + eval_json_encode_append( + value, + values, + flags, + depth as usize, + 0, + &mut Vec::new(), + &mut output, + )?; context.clear_json_error(); values.string_bytes_value(&output) } @@ -13452,6 +13460,7 @@ fn eval_json_error_location(bytes: &[u8], offset: usize) -> (usize, usize) { fn eval_json_encode_append( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, + flags: i64, depth_limit: usize, depth: usize, arrays_seen: &mut Vec, @@ -13459,7 +13468,7 @@ fn eval_json_encode_append( ) -> Result<(), EvalStatus> { match values.type_tag(value)? { EVAL_TAG_INT | EVAL_TAG_FLOAT => output.extend_from_slice(&values.string_bytes(value)?), - EVAL_TAG_STRING => eval_json_encode_append_string(&values.string_bytes(value)?, output), + EVAL_TAG_STRING => eval_json_encode_append_string(&values.string_bytes(value)?, flags, output), EVAL_TAG_BOOL => { if values.truthy(value)? { output.extend_from_slice(b"true"); @@ -13468,10 +13477,26 @@ fn eval_json_encode_append( } } EVAL_TAG_ARRAY => { - eval_json_encode_append_indexed_array(value, values, depth_limit, depth, arrays_seen, output)?; + eval_json_encode_append_indexed_array( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + output, + )?; } EVAL_TAG_ASSOC => { - eval_json_encode_append_assoc(value, values, depth_limit, depth, arrays_seen, output)?; + eval_json_encode_append_assoc( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + output, + )?; } EVAL_TAG_NULL | EVAL_TAG_OBJECT | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), _ => return Err(EvalStatus::UnsupportedConstruct), @@ -13483,6 +13508,7 @@ fn eval_json_encode_append( fn eval_json_encode_append_indexed_array( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, + flags: i64, depth_limit: usize, depth: usize, arrays_seen: &mut Vec, @@ -13497,7 +13523,15 @@ fn eval_json_encode_append_indexed_array( } let key = values.array_iter_key(value, position)?; let element = values.array_get(value, key)?; - eval_json_encode_append(element, values, depth_limit, depth + 1, arrays_seen, output)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + output, + )?; } output.push(b']'); arrays_seen.pop(); @@ -13508,6 +13542,7 @@ fn eval_json_encode_append_indexed_array( fn eval_json_encode_append_assoc( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, + flags: i64, depth_limit: usize, depth: usize, arrays_seen: &mut Vec, @@ -13521,10 +13556,18 @@ fn eval_json_encode_append_assoc( output.push(b','); } let key = values.array_iter_key(value, position)?; - eval_json_encode_append_string(&values.string_bytes(key)?, output); + eval_json_encode_append_string(&values.string_bytes(key)?, flags, output); output.push(b':'); let element = values.array_get(value, key)?; - eval_json_encode_append(element, values, depth_limit, depth + 1, arrays_seen, output)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + output, + )?; } output.push(b'}'); arrays_seen.pop(); @@ -13549,14 +13592,17 @@ fn eval_json_encode_enter_array( Ok(()) } -/// Appends one JSON string with the default PHP slash and control-character escaping. -fn eval_json_encode_append_string(bytes: &[u8], output: &mut Vec) { +/// Appends one JSON string with eval-supported PHP flag handling. +fn eval_json_encode_append_string(bytes: &[u8], flags: i64, output: &mut Vec) { output.push(b'"'); for byte in bytes { match *byte { b'"' => output.extend_from_slice(b"\\\""), b'\\' => output.extend_from_slice(b"\\\\"), - b'/' => output.extend_from_slice(b"\\/"), + b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { + output.extend_from_slice(b"\\/"); + } + b'/' => output.push(b'/'), b'\x08' => output.extend_from_slice(b"\\b"), b'\x0c' => output.extend_from_slice(b"\\f"), b'\n' => output.extend_from_slice(b"\\n"), @@ -14332,6 +14378,7 @@ fn eval_predefined_constant_value(name: &str) -> Option )) } "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), + "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), @@ -17355,7 +17402,9 @@ return defined("COUNT_RECURSIVE");"#, echo json_encode([1, "q", true, null]) . ":"; echo call_user_func("json_encode", "a/b\"c") . ":"; echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; -return function_exists("json_encode");"#, +echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; +echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; +return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17365,7 +17414,7 @@ return function_exists("json_encode");"#, assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 8a18a7ae5c..1d7481f53f 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a zero-flags recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells. It uses eval type tags plus array iteration hooks and intentionally rejects non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus `JSON_UNESCAPED_SLASHES`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6ca4ef16ab..45c8295903 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping and rejects non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_UNESCAPED_SLASHES` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2510b7fc0e..f9133ecd45 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -821,10 +821,15 @@ eval('echo json_encode(["a" => 1, "b" => "x/y"]) . ":"; echo json_encode([1, "q", true, null]) . ":"; echo call_user_func("json_encode", "a/b\"c") . ":"; echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; +echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; +echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; echo function_exists("json_encode");'); "#, ); - assert_eq!(out, r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:1"#); + assert_eq!( + out, + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":1"# + ); } /// Verifies eval `json_decode()` materializes scalar, indexed, and associative values. From 5c6ef0058be5e879360a152849654fbb051f4baf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:14:56 +0200 Subject: [PATCH 0241/1208] Support eval JSON bigint decode flag --- crates/elephc-eval/src/interpreter.rs | 57 +++++++++++++++++++++------ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 10 ++++- 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 4f5583b28c..14b55caf59 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -593,6 +593,7 @@ const EVAL_JSON_ERROR_INF_OR_NAN: i64 = 7; const EVAL_JSON_ERROR_UNSUPPORTED_TYPE: i64 = 8; const EVAL_JSON_ERROR_INVALID_PROPERTY_NAME: i64 = 9; const EVAL_JSON_ERROR_UTF16: i64 = 10; +const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; unsafe extern "C" { @@ -13183,7 +13184,7 @@ fn eval_json_encode_result( values.string_bytes_value(&output) } -/// Evaluates PHP `json_decode()` for zero-flag scalar, array, and object JSON text. +/// Evaluates PHP `json_decode()` for eval-supported JSON text and flags. fn eval_builtin_json_decode( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -13233,12 +13234,11 @@ fn eval_json_decode_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if flags + let flags = flags .map(|flags| eval_int_value(flags, values)) .transpose()? - .unwrap_or(0) - != 0 - { + .unwrap_or(0); + if flags & !EVAL_JSON_BIGINT_AS_STRING != 0 { return Err(EvalStatus::UnsupportedConstruct); } if let Some(associative) = associative { @@ -13261,25 +13261,26 @@ fn eval_json_decode_result( } }; context.clear_json_error(); - eval_json_decode_to_cell(decoded, values) + eval_json_decode_to_cell(decoded, flags, values) } /// Materializes one parsed JSON value as an eval runtime cell. fn eval_json_decode_to_cell( value: JsonValue, + flags: i64, values: &mut impl RuntimeValueOps, ) -> Result { match value { JsonValue::Null => values.null(), JsonValue::Bool(value) => values.bool_value(value), - JsonValue::Number(value) => eval_json_decode_number_to_cell(&value, values), + JsonValue::Number(value) => eval_json_decode_number_to_cell(&value, flags, values), JsonValue::String(value) => values.string_bytes_value(&value), JsonValue::Array(elements) => { let mut result = values.array_new(elements.len())?; for (index, element) in elements.into_iter().enumerate() { let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; let key = values.int(index)?; - let element = eval_json_decode_to_cell(element, values)?; + let element = eval_json_decode_to_cell(element, flags, values)?; result = values.array_set(result, key, element)?; } Ok(result) @@ -13288,7 +13289,7 @@ fn eval_json_decode_to_cell( let mut result = values.assoc_new(entries.len())?; for (key, value) in entries { let key = values.string_bytes_value(&key)?; - let value = eval_json_decode_to_cell(value, values)?; + let value = eval_json_decode_to_cell(value, flags, values)?; result = values.array_set(result, key, value)?; } Ok(result) @@ -13299,8 +13300,12 @@ fn eval_json_decode_to_cell( /// Materializes one JSON number as an int when possible and as a float otherwise. fn eval_json_decode_number_to_cell( value: &[u8], + flags: i64, values: &mut impl RuntimeValueOps, ) -> Result { + if flags & EVAL_JSON_BIGINT_AS_STRING != 0 && eval_json_number_overflows_i64(value) { + return values.string_bytes_value(value); + } let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; if !value.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E')) { if let Ok(integer) = value.parse::() { @@ -13311,6 +13316,27 @@ fn eval_json_decode_number_to_cell( values.float(float) } +/// Returns true when one integer-grammar JSON number exceeds PHP's int range. +fn eval_json_number_overflows_i64(value: &[u8]) -> bool { + if value + .iter() + .any(|byte| matches!(*byte, b'.' | b'e' | b'E')) + { + return false; + } + let (negative, digits) = if let Some(digits) = value.strip_prefix(b"-") { + (true, digits) + } else { + (false, value) + }; + let threshold = if negative { + b"9223372036854775808".as_slice() + } else { + b"9223372036854775807".as_slice() + }; + digits.len() > threshold.len() || digits.len() == threshold.len() && digits > threshold +} + /// Evaluates PHP `json_last_error()` from the eval interpreter's current JSON state. fn eval_builtin_json_last_error( args: &[EvalExpr], @@ -14378,6 +14404,7 @@ fn eval_predefined_constant_value(name: &str) -> Option )) } "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), + "JSON_BIGINT_AS_STRING" => Some(EvalPredefinedConstant::Int(EVAL_JSON_BIGINT_AS_STRING)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), @@ -17434,7 +17461,12 @@ echo $call[1] . ":"; $named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); echo $named["k"] . ":"; echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; -return function_exists("json_decode");"#, +$big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); +echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo gettype($big[0]) . ":" . $big[0] . ":"; +echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; +return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17442,7 +17474,10 @@ return function_exists("json_decode");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "hello:42:T:NULL:1:x:F:4:v:BAD:"); + assert_eq!( + values.output, + "hello:42:T:NULL:1:x:F:4:v:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 1d7481f53f..39b8242204 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus `JSON_UNESCAPED_SLASHES`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus `JSON_UNESCAPED_SLASHES`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 45c8295903..9455b1579f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_UNESCAPED_SLASHES` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_UNESCAPED_SLASHES` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f9133ecd45..25d7ae91f4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -848,10 +848,18 @@ echo $call[1] . ":"; $named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); echo $named["k"] . ":"; echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; +$big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); +echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo gettype($big[0]) . ":" . $big[0] . ":"; +echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; echo function_exists("json_decode");'); "#, ); - assert_eq!(out, "hello:42:T:NULL:1:x:F:4:v:BAD:1"); + assert_eq!( + out, + "hello:42:T:NULL:1:x:F:4:v:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:1" + ); } /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. From 638091c34c9bc7148db8d206c6cde5bf553227d6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:19:32 +0200 Subject: [PATCH 0242/1208] Support eval JSON force object flag --- crates/elephc-eval/src/interpreter.rs | 22 ++++++++++++++++------ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 5 ++++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 14b55caf59..f48d8f94d3 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -594,6 +594,7 @@ const EVAL_JSON_ERROR_UNSUPPORTED_TYPE: i64 = 8; const EVAL_JSON_ERROR_INVALID_PROPERTY_NAME: i64 = 9; const EVAL_JSON_ERROR_UTF16: i64 = 10; const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; +const EVAL_JSON_FORCE_OBJECT: i64 = 16; const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; unsafe extern "C" { @@ -13159,7 +13160,7 @@ fn eval_json_encode_result( .map(|flags| eval_int_value(flags, values)) .transpose()? .unwrap_or(0); - if flags & !EVAL_JSON_UNESCAPED_SLASHES != 0 { + if flags & !(EVAL_JSON_UNESCAPED_SLASHES | EVAL_JSON_FORCE_OBJECT) != 0 { return Err(EvalStatus::UnsupportedConstruct); } let depth = depth @@ -13530,7 +13531,7 @@ fn eval_json_encode_append( Ok(()) } -/// Appends one indexed eval array as a JSON array. +/// Appends one indexed eval array as a JSON array or forced JSON object. fn eval_json_encode_append_indexed_array( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, @@ -13541,13 +13542,18 @@ fn eval_json_encode_append_indexed_array( output: &mut Vec, ) -> Result<(), EvalStatus> { eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; - output.push(b'['); + let force_object = flags & EVAL_JSON_FORCE_OBJECT != 0; + output.push(if force_object { b'{' } else { b'[' }); let len = values.array_len(value)?; for position in 0..len { if position > 0 { output.push(b','); } let key = values.array_iter_key(value, position)?; + if force_object { + eval_json_encode_append_string(&values.string_bytes(key)?, flags, output); + output.push(b':'); + } let element = values.array_get(value, key)?; eval_json_encode_append( element, @@ -13559,7 +13565,7 @@ fn eval_json_encode_append_indexed_array( output, )?; } - output.push(b']'); + output.push(if force_object { b'}' } else { b']' }); arrays_seen.pop(); Ok(()) } @@ -14405,6 +14411,7 @@ fn eval_predefined_constant_value(name: &str) -> Option } "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), "JSON_BIGINT_AS_STRING" => Some(EvalPredefinedConstant::Int(EVAL_JSON_BIGINT_AS_STRING)), + "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), @@ -17431,7 +17438,10 @@ echo call_user_func("json_encode", "a/b\"c") . ":"; echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; -return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES");"#, +echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; +echo json_encode([], JSON_FORCE_OBJECT) . ":"; +echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; +return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17441,7 +17451,7 @@ return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES");"#, assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 39b8242204..df92cd450d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus `JSON_UNESCAPED_SLASHES`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus `JSON_UNESCAPED_SLASHES` and `JSON_FORCE_OBJECT`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 9455b1579f..7a11364712 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_UNESCAPED_SLASHES` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_UNESCAPED_SLASHES` and `JSON_FORCE_OBJECT` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 25d7ae91f4..bdd6de8a0b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -823,12 +823,15 @@ echo call_user_func("json_encode", "a/b\"c") . ":"; echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; +echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; +echo json_encode([], JSON_FORCE_OBJECT) . ":"; +echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; echo function_exists("json_encode");'); "#, ); assert_eq!( out, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":1"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:1"# ); } From bdaa54e5c2dd584c60bedc5547dad43693f7e9fc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:23:35 +0200 Subject: [PATCH 0243/1208] Support eval JSON hex encode flags --- crates/elephc-eval/src/interpreter.rs | 26 +++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 3 ++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f48d8f94d3..7782e6c350 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -593,6 +593,10 @@ const EVAL_JSON_ERROR_INF_OR_NAN: i64 = 7; const EVAL_JSON_ERROR_UNSUPPORTED_TYPE: i64 = 8; const EVAL_JSON_ERROR_INVALID_PROPERTY_NAME: i64 = 9; const EVAL_JSON_ERROR_UTF16: i64 = 10; +const EVAL_JSON_HEX_TAG: i64 = 1; +const EVAL_JSON_HEX_AMP: i64 = 2; +const EVAL_JSON_HEX_APOS: i64 = 4; +const EVAL_JSON_HEX_QUOT: i64 = 8; const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; const EVAL_JSON_FORCE_OBJECT: i64 = 16; const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; @@ -13160,7 +13164,13 @@ fn eval_json_encode_result( .map(|flags| eval_int_value(flags, values)) .transpose()? .unwrap_or(0); - if flags & !(EVAL_JSON_UNESCAPED_SLASHES | EVAL_JSON_FORCE_OBJECT) != 0 { + let supported_flags = EVAL_JSON_HEX_TAG + | EVAL_JSON_HEX_AMP + | EVAL_JSON_HEX_APOS + | EVAL_JSON_HEX_QUOT + | EVAL_JSON_UNESCAPED_SLASHES + | EVAL_JSON_FORCE_OBJECT; + if flags & !supported_flags != 0 { return Err(EvalStatus::UnsupportedConstruct); } let depth = depth @@ -13629,12 +13639,17 @@ fn eval_json_encode_append_string(bytes: &[u8], flags: i64, output: &mut Vec output.push(b'"'); for byte in bytes { match *byte { + b'"' if flags & EVAL_JSON_HEX_QUOT != 0 => output.extend_from_slice(b"\\u0022"), b'"' => output.extend_from_slice(b"\\\""), b'\\' => output.extend_from_slice(b"\\\\"), b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { output.extend_from_slice(b"\\/"); } b'/' => output.push(b'/'), + b'<' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003C"), + b'>' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003E"), + b'&' if flags & EVAL_JSON_HEX_AMP != 0 => output.extend_from_slice(b"\\u0026"), + b'\'' if flags & EVAL_JSON_HEX_APOS != 0 => output.extend_from_slice(b"\\u0027"), b'\x08' => output.extend_from_slice(b"\\b"), b'\x0c' => output.extend_from_slice(b"\\f"), b'\n' => output.extend_from_slice(b"\\n"), @@ -14410,6 +14425,10 @@ fn eval_predefined_constant_value(name: &str) -> Option )) } "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), + "JSON_HEX_TAG" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_TAG)), + "JSON_HEX_AMP" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_AMP)), + "JSON_HEX_APOS" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_APOS)), + "JSON_HEX_QUOT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_QUOT)), "JSON_BIGINT_AS_STRING" => Some(EvalPredefinedConstant::Int(EVAL_JSON_BIGINT_AS_STRING)), "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), @@ -17441,7 +17460,8 @@ echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNES echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; echo json_encode([], JSON_FORCE_OBJECT) . ":"; echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; -return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT");"#, +echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; +return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17451,7 +17471,7 @@ return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && de assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index df92cd450d..4c5851b46d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus `JSON_UNESCAPED_SLASHES` and `JSON_FORCE_OBJECT`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, and `JSON_FORCE_OBJECT`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7a11364712..d345525770 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_UNESCAPED_SLASHES` and `JSON_FORCE_OBJECT` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, and `JSON_FORCE_OBJECT` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bdd6de8a0b..4c8e8e8991 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -826,12 +826,13 @@ echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNES echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; echo json_encode([], JSON_FORCE_OBJECT) . ":"; echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; +echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; echo function_exists("json_encode");'); "#, ); assert_eq!( out, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:1"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":1"# ); } From e755f027bcf3a9a4812822f564518e0399f53fd6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:27:47 +0200 Subject: [PATCH 0244/1208] Support eval JSON numeric encode flag --- crates/elephc-eval/src/interpreter.rs | 48 ++++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 3 +- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 7782e6c350..0bb53c6acd 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -599,6 +599,7 @@ const EVAL_JSON_HEX_APOS: i64 = 4; const EVAL_JSON_HEX_QUOT: i64 = 8; const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; const EVAL_JSON_FORCE_OBJECT: i64 = 16; +const EVAL_JSON_NUMERIC_CHECK: i64 = 32; const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; unsafe extern "C" { @@ -13170,6 +13171,7 @@ fn eval_json_encode_result( | EVAL_JSON_HEX_QUOT | EVAL_JSON_UNESCAPED_SLASHES | EVAL_JSON_FORCE_OBJECT; + let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; if flags & !supported_flags != 0 { return Err(EvalStatus::UnsupportedConstruct); } @@ -13561,7 +13563,11 @@ fn eval_json_encode_append_indexed_array( } let key = values.array_iter_key(value, position)?; if force_object { - eval_json_encode_append_string(&values.string_bytes(key)?, flags, output); + eval_json_encode_append_string( + &values.string_bytes(key)?, + flags & !EVAL_JSON_NUMERIC_CHECK, + output, + ); output.push(b':'); } let element = values.array_get(value, key)?; @@ -13598,7 +13604,11 @@ fn eval_json_encode_append_assoc( output.push(b','); } let key = values.array_iter_key(value, position)?; - eval_json_encode_append_string(&values.string_bytes(key)?, flags, output); + eval_json_encode_append_string( + &values.string_bytes(key)?, + flags & !EVAL_JSON_NUMERIC_CHECK, + output, + ); output.push(b':'); let element = values.array_get(value, key)?; eval_json_encode_append( @@ -13636,6 +13646,12 @@ fn eval_json_encode_enter_array( /// Appends one JSON string with eval-supported PHP flag handling. fn eval_json_encode_append_string(bytes: &[u8], flags: i64, output: &mut Vec) { + if flags & EVAL_JSON_NUMERIC_CHECK != 0 { + if let Some(number) = eval_json_numeric_check_bytes(bytes) { + output.extend_from_slice(&number); + return; + } + } output.push(b'"'); for byte in bytes { match *byte { @@ -13664,6 +13680,28 @@ fn eval_json_encode_append_string(bytes: &[u8], flags: i64, output: &mut Vec output.push(b'"'); } +/// Returns the JSON number bytes for a PHP numeric string when `JSON_NUMERIC_CHECK` applies. +fn eval_json_numeric_check_bytes(bytes: &[u8]) -> Option> { + let value = std::str::from_utf8(bytes).ok()?.trim(); + if value.is_empty() { + return None; + } + let integer_grammar = value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'+' | b'-')); + if integer_grammar { + if let Ok(integer) = value.parse::() { + return Some(integer.to_string().into_bytes()); + } + } + let number = value.parse::().ok()?; + if number.is_finite() { + Some(number.to_string().into_bytes()) + } else { + None + } +} + /// Evaluates PHP `print_r()` over one eval expression. fn eval_builtin_print_r( args: &[EvalExpr], @@ -14431,6 +14469,7 @@ fn eval_predefined_constant_value(name: &str) -> Option "JSON_HEX_QUOT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_QUOT)), "JSON_BIGINT_AS_STRING" => Some(EvalPredefinedConstant::Int(EVAL_JSON_BIGINT_AS_STRING)), "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), + "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), @@ -17461,7 +17500,8 @@ echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; echo json_encode([], JSON_FORCE_OBJECT) . ":"; echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; -return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT");"#, +echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; +return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17471,7 +17511,7 @@ return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && de assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 4c5851b46d..c002384ccf 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, and `JSON_FORCE_OBJECT`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, and `JSON_NUMERIC_CHECK`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index d345525770..650102cf1f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, and `JSON_FORCE_OBJECT` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, and `JSON_NUMERIC_CHECK` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4c8e8e8991..3c48d68e4d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -827,12 +827,13 @@ echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; echo json_encode([], JSON_FORCE_OBJECT) . ":"; echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; +echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; echo function_exists("json_encode");'); "#, ); assert_eq!( out, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":1"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:1"# ); } From 3c3615f19b36a37ffd1a6dafea22e69ca2026b4a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:31:33 +0200 Subject: [PATCH 0245/1208] Support eval JSON preserve zero fraction flag --- crates/elephc-eval/src/interpreter.rs | 31 +++++++++++++++++++++++---- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 3 ++- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0bb53c6acd..6d7c90ae3b 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -601,6 +601,7 @@ const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; const EVAL_JSON_FORCE_OBJECT: i64 = 16; const EVAL_JSON_NUMERIC_CHECK: i64 = 32; const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; +const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -13170,7 +13171,8 @@ fn eval_json_encode_result( | EVAL_JSON_HEX_APOS | EVAL_JSON_HEX_QUOT | EVAL_JSON_UNESCAPED_SLASHES - | EVAL_JSON_FORCE_OBJECT; + | EVAL_JSON_FORCE_OBJECT + | EVAL_JSON_PRESERVE_ZERO_FRACTION; let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; if flags & !supported_flags != 0 { return Err(EvalStatus::UnsupportedConstruct); @@ -13506,7 +13508,10 @@ fn eval_json_encode_append( output: &mut Vec, ) -> Result<(), EvalStatus> { match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => output.extend_from_slice(&values.string_bytes(value)?), + EVAL_TAG_INT => output.extend_from_slice(&values.string_bytes(value)?), + EVAL_TAG_FLOAT => { + eval_json_encode_append_float(&values.string_bytes(value)?, flags, output); + } EVAL_TAG_STRING => eval_json_encode_append_string(&values.string_bytes(value)?, flags, output), EVAL_TAG_BOOL => { if values.truthy(value)? { @@ -13543,6 +13548,18 @@ fn eval_json_encode_append( Ok(()) } +/// Appends one JSON float while preserving a `.0` suffix when requested. +fn eval_json_encode_append_float(bytes: &[u8], flags: i64, output: &mut Vec) { + output.extend_from_slice(bytes); + if flags & EVAL_JSON_PRESERVE_ZERO_FRACTION != 0 + && !bytes + .iter() + .any(|byte| matches!(*byte, b'.' | b'e' | b'E')) + { + output.extend_from_slice(b".0"); + } +} + /// Appends one indexed eval array as a JSON array or forced JSON object. fn eval_json_encode_append_indexed_array( value: RuntimeCellHandle, @@ -14471,6 +14488,11 @@ fn eval_predefined_constant_value(name: &str) -> Option "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), + "JSON_PRESERVE_ZERO_FRACTION" => { + Some(EvalPredefinedConstant::Int( + EVAL_JSON_PRESERVE_ZERO_FRACTION, + )) + } "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), @@ -17501,7 +17523,8 @@ echo json_encode([], JSON_FORCE_OBJECT) . ":"; echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; -return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK");"#, +echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; +return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PRESERVE_ZERO_FRACTION");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17511,7 +17534,7 @@ return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && de assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c002384ccf..d85e471ea5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, and `JSON_NUMERIC_CHECK`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 650102cf1f..db7a5c244f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, and `JSON_NUMERIC_CHECK` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3c48d68e4d..8bfdfc0c1d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -828,12 +828,13 @@ echo json_encode([], JSON_FORCE_OBJECT) . ":"; echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; +echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; echo function_exists("json_encode");'); "#, ); assert_eq!( out, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:1"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:1"# ); } From 1ff48f9990e8ff422d7f59e1e2e4e1313be67a62 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:35:58 +0200 Subject: [PATCH 0246/1208] Support eval JSON pretty print flag --- crates/elephc-eval/src/interpreter.rs | 56 +++++++++++++++++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 3 +- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6d7c90ae3b..4b27966b14 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -601,6 +601,7 @@ const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; const EVAL_JSON_FORCE_OBJECT: i64 = 16; const EVAL_JSON_NUMERIC_CHECK: i64 = 32; const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; +const EVAL_JSON_PRETTY_PRINT: i64 = 128; const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; unsafe extern "C" { @@ -13172,6 +13173,7 @@ fn eval_json_encode_result( | EVAL_JSON_HEX_QUOT | EVAL_JSON_UNESCAPED_SLASHES | EVAL_JSON_FORCE_OBJECT + | EVAL_JSON_PRETTY_PRINT | EVAL_JSON_PRESERVE_ZERO_FRACTION; let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; if flags & !supported_flags != 0 { @@ -13572,11 +13574,21 @@ fn eval_json_encode_append_indexed_array( ) -> Result<(), EvalStatus> { eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; let force_object = flags & EVAL_JSON_FORCE_OBJECT != 0; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; output.push(if force_object { b'{' } else { b'[' }); let len = values.array_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } for position in 0..len { if position > 0 { output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); } let key = values.array_iter_key(value, position)?; if force_object { @@ -13585,7 +13597,7 @@ fn eval_json_encode_append_indexed_array( flags & !EVAL_JSON_NUMERIC_CHECK, output, ); - output.push(b':'); + eval_json_encode_append_colon(flags, output); } let element = values.array_get(value, key)?; eval_json_encode_append( @@ -13598,6 +13610,10 @@ fn eval_json_encode_append_indexed_array( output, )?; } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } output.push(if force_object { b'}' } else { b']' }); arrays_seen.pop(); Ok(()) @@ -13614,11 +13630,21 @@ fn eval_json_encode_append_assoc( output: &mut Vec, ) -> Result<(), EvalStatus> { eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; output.push(b'{'); let len = values.array_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } for position in 0..len { if position > 0 { output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); } let key = values.array_iter_key(value, position)?; eval_json_encode_append_string( @@ -13626,7 +13652,7 @@ fn eval_json_encode_append_assoc( flags & !EVAL_JSON_NUMERIC_CHECK, output, ); - output.push(b':'); + eval_json_encode_append_colon(flags, output); let element = values.array_get(value, key)?; eval_json_encode_append( element, @@ -13638,11 +13664,31 @@ fn eval_json_encode_append_assoc( output, )?; } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } output.push(b'}'); arrays_seen.pop(); Ok(()) } +/// Appends a JSON object colon, including pretty-print spacing when active. +fn eval_json_encode_append_colon(flags: i64, output: &mut Vec) { + if flags & EVAL_JSON_PRETTY_PRINT != 0 { + output.extend_from_slice(b": "); + } else { + output.push(b':'); + } +} + +/// Appends PHP's four-space JSON pretty-print indentation for one nesting level. +fn eval_json_encode_pretty_indent(output: &mut Vec, depth: usize) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} + /// Records entry into one JSON array/object, rejecting depth overrun and recursion. fn eval_json_encode_enter_array( value: RuntimeCellHandle, @@ -14488,6 +14534,7 @@ fn eval_predefined_constant_value(name: &str) -> Option "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), + "JSON_PRETTY_PRINT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_PRETTY_PRINT)), "JSON_PRESERVE_ZERO_FRACTION" => { Some(EvalPredefinedConstant::Int( EVAL_JSON_PRESERVE_ZERO_FRACTION, @@ -17524,7 +17571,8 @@ echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FOR echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; -return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PRESERVE_ZERO_FRACTION");"#, +echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; +return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17534,7 +17582,7 @@ return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && de assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:{| "a": [| 1,| 2| ]|}:"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d85e471ea5..8565e10b60 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index db7a5c244f..1677670dcc 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8bfdfc0c1d..0a04f71ce7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -829,12 +829,13 @@ echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FOR echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; +echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; echo function_exists("json_encode");'); "#, ); assert_eq!( out, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:1"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:{| "a": [| 1,| 2| ]|}:1"# ); } From 1458e3f765241939ee7eabe831d686529dee7fe3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:41:49 +0200 Subject: [PATCH 0247/1208] Support eval preg offset captures --- crates/elephc-eval/src/interpreter.rs | 111 ++++++++++++++++++++++---- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 10 ++- 4 files changed, 104 insertions(+), 21 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 4b27966b14..72388a733f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -10423,7 +10423,28 @@ fn eval_builtin_preg_match( let EvalExpr::LoadVar(matches_name) = matches else { return Err(EvalStatus::RuntimeFatal); }; - let (result, matches_array) = eval_preg_match_capture_result(pattern, subject, values)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, None, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; for replaced in set_scope_cell( context, scope, @@ -10454,20 +10475,38 @@ fn eval_preg_match_result( fn eval_preg_match_capture_result( pattern: RuntimeCellHandle, subject: RuntimeCellHandle, + flags: Option, values: &mut impl RuntimeValueOps, ) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { let regex = eval_preg_regex(pattern, values)?; let subject = values.string_bytes(subject)?; + let flags = eval_preg_match_flags(flags, values)?; + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; if let Some(captures) = regex.captures(&subject) { - let matches = eval_preg_capture_array(&subject, Some(&captures), values)?; + let matches = eval_preg_capture_array(&subject, Some(&captures), offset_capture, values)?; let matched = values.int(1)?; return Ok((matched, matches)); } - let matches = eval_preg_capture_array(&subject, None, values)?; + let matches = eval_preg_capture_array(&subject, None, offset_capture, values)?; let matched = values.int(0)?; Ok((matched, matches)) } +/// Returns supported `preg_match()` flags. +fn eval_preg_match_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + if flags & !EVAL_PREG_OFFSET_CAPTURE != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + /// Evaluates PHP `preg_match_all()` over eval expressions. fn eval_builtin_preg_match_all( args: &[EvalExpr], @@ -10550,14 +10589,14 @@ fn eval_preg_match_all_capture_result( let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; let flags = eval_preg_match_all_flags(flags, values)?; let matches = if flags & EVAL_PREG_SET_ORDER != 0 { - eval_preg_match_all_set_order_array(&subject, &captures, capture_count, values)? + eval_preg_match_all_set_order_array(&subject, &captures, capture_count, flags, values)? } else { - eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, values)? + eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, flags, values)? }; Ok((count, matches)) } -/// Returns supported `preg_match_all()` flags and rejects offset-capture shape changes. +/// Returns supported `preg_match_all()` flags. fn eval_preg_match_all_flags( flags: Option, values: &mut impl RuntimeValueOps, @@ -10566,8 +10605,8 @@ fn eval_preg_match_all_flags( return Ok(EVAL_PREG_PATTERN_ORDER); }; let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_PATTERN_ORDER | EVAL_PREG_SET_ORDER; - if flags & EVAL_PREG_OFFSET_CAPTURE != 0 || flags & !supported != 0 { + let supported = EVAL_PREG_PATTERN_ORDER | EVAL_PREG_SET_ORDER | EVAL_PREG_OFFSET_CAPTURE; + if flags & !supported != 0 { return Err(EvalStatus::RuntimeFatal); } Ok(flags) @@ -10578,16 +10617,18 @@ fn eval_preg_match_all_pattern_order_array( subject: &[u8], captures: &[Captures<'_>], capture_count: usize, + flags: i64, values: &mut impl RuntimeValueOps, ) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; let mut outer = values.array_new(capture_count)?; for capture_index in 0..capture_count { let mut row = values.array_new(captures.len())?; for (match_index, capture) in captures.iter().enumerate() { let key = values .int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let bytes = eval_preg_capture_bytes(subject, capture, capture_index).unwrap_or(b""); - let value = values.string_bytes_value(bytes)?; + let value = + eval_preg_capture_value(subject, capture, capture_index, offset_capture, values)?; row = values.array_set(row, key, value)?; } let key = values @@ -10602,16 +10643,18 @@ fn eval_preg_match_all_set_order_array( subject: &[u8], captures: &[Captures<'_>], capture_count: usize, + flags: i64, values: &mut impl RuntimeValueOps, ) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; let mut outer = values.array_new(captures.len())?; for (match_index, capture) in captures.iter().enumerate() { let mut row = values.array_new(capture_count)?; for capture_index in 0..capture_count { let key = values .int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let bytes = eval_preg_capture_bytes(subject, capture, capture_index).unwrap_or(b""); - let value = values.string_bytes_value(bytes)?; + let value = + eval_preg_capture_value(subject, capture, capture_index, offset_capture, values)?; row = values.array_set(row, key, value)?; } let key = values @@ -10695,7 +10738,7 @@ fn eval_preg_replace_callback_result( continue; }; result.extend_from_slice(&subject[cursor..matched.start()]); - let matches = eval_preg_capture_array(&subject, Some(&captures), values)?; + let matches = eval_preg_capture_array(&subject, Some(&captures), false, values)?; let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; let callback_result = values.cast_string(callback_result)?; let callback_bytes = values.string_bytes(callback_result)?; @@ -10890,6 +10933,7 @@ fn eval_preg_modifiers(modifiers: &[u8]) -> Result>, + offset_capture: bool, values: &mut impl RuntimeValueOps, ) -> Result { let len = captures.map_or(0, eval_preg_visible_capture_len); @@ -10897,8 +10941,7 @@ fn eval_preg_capture_array( if let Some(captures) = captures { for index in 0..len { let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let bytes = eval_preg_capture_bytes(subject, captures, index).unwrap_or(b""); - let value = values.string_bytes_value(bytes)?; + let value = eval_preg_capture_value(subject, captures, index, offset_capture, values)?; result = values.array_set(result, key, value)?; } } @@ -10925,6 +10968,34 @@ fn eval_preg_capture_bytes<'a>( .map(|matched| &subject[matched.start()..matched.end()]) } +/// Builds one capture entry as either a string or PHP's `[string, byte_offset]` pair. +fn eval_preg_capture_value( + subject: &[u8], + captures: &Captures<'_>, + index: usize, + offset_capture: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let matched = captures.get(index); + let bytes = matched + .as_ref() + .map_or(b"".as_slice(), |matched| &subject[matched.start()..matched.end()]); + let value = values.string_bytes_value(bytes)?; + if !offset_capture { + return Ok(value); + } + + let offset = matched.map_or(Ok(-1_i64), |matched| { + i64::try_from(matched.start()).map_err(|_| EvalStatus::RuntimeFatal) + })?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} + /// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. fn eval_preg_expand_replacement( replacement: &[u8], @@ -18825,6 +18896,12 @@ $allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; $setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; +preg_match("/(a)?(b)/", "b", $offsetOne, PREG_OFFSET_CAPTURE); +echo $offsetOne[0][0] . ":" . $offsetOne[0][1] . ":" . $offsetOne[1][0] . ":" . $offsetOne[1][1] . ":" . $offsetOne[2][0] . ":" . $offsetOne[2][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); +echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); +echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; preg_match_all("/(x)(y)/", "abc", $none); echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; @@ -18839,7 +18916,7 @@ $replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "repl echo $replaced . ":"; $captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); echo count($captured) . ":" . $captured[1] . ":"; -return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER");"#, +return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -18849,7 +18926,7 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun assert_eq!( values.output, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 8565e10b60..2a636b5e65 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -75,7 +75,7 @@ Eval `array_filter()` implements PHP's omitted/null callback form by iterating s Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false. Eval `iterator_apply()` drives Traversable objects through the eval method-call hooks (`rewind()`, `valid()`, and `next()`) and invokes string callbacks or object-method callable arrays through the shared eval callable dispatcher. The callback receives only the optional third-argument array, matching PHP's `iterator_apply()` behavior; current value/key are not passed implicitly. Array inputs remain runtime fatals for `iterator_apply()`. -Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, direct `preg_match_all()` `PREG_SET_ORDER` captures, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax or offset-capture result shapes surface as eval runtime fatals instead of falling back to a partial native ABI. +Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures with optional `PREG_OFFSET_CAPTURE` pairs, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, direct `preg_match_all()` `PREG_SET_ORDER` captures, offset-capture pairs for both `preg_match_all()` result orders, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax surfaces as eval runtime fatals instead of falling back to a partial native ABI. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 1677670dcc..d9877d426e 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -90,7 +90,7 @@ Eval `array_filter()` supports the PHP default omitted/null callback form, filte Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` supports Traversable objects through `rewind()`, `valid()`, `next()`, string callbacks that target eval-declared functions, registered AOT callbacks, or supported builtins, and object-method callable arrays. Arrays still fail for `iterator_apply()`, matching PHP's array `TypeError` rather than treating arrays as Traversable. -Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form and `PREG_SET_ORDER` match-order form, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; offset-capture flags and PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). +Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays with optional `PREG_OFFSET_CAPTURE` entries, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form and `PREG_SET_ORDER` match-order form with optional `PREG_OFFSET_CAPTURE` entries, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0a04f71ce7..95cb6d131a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2421,6 +2421,12 @@ $allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; $setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; +preg_match("/(a)?(b)/", "b", $offsetOne, PREG_OFFSET_CAPTURE); +echo $offsetOne[0][0] . ":" . $offsetOne[0][1] . ":" . $offsetOne[1][0] . ":" . $offsetOne[1][1] . ":" . $offsetOne[2][0] . ":" . $offsetOne[2][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); +echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); +echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; preg_match_all("/(x)(y)/", "abc", $none); echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; @@ -2435,12 +2441,12 @@ $replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "repl echo $replaced . ":"; $captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); echo count($captured) . ":" . $captured[1] . ":"; -echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER");'); +echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE");'); "#, ); assert_eq!( out, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:1" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:1" ); } From f69d81f0f7dce5e5d9137084a446396c8bc8e8ca Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:45:53 +0200 Subject: [PATCH 0248/1208] Support eval preg split offset captures --- crates/elephc-eval/src/interpreter.rs | 83 ++++++++++++++++++++++----- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 10 +++- 4 files changed, 78 insertions(+), 19 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 72388a733f..5b76ad97f7 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -10793,7 +10793,8 @@ fn eval_preg_split_result( let flags = eval_preg_split_flags(flags, values)?; let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; - let mut pieces = Vec::>::new(); + let offset_capture = flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0; + let mut pieces = Vec::::new(); let mut cursor = 0; for captures in regex.captures_iter(&subject) { @@ -10803,18 +10804,23 @@ fn eval_preg_split_result( if eval_preg_split_reached_limit(&pieces, limit) { break; } - eval_preg_split_push_piece(&mut pieces, &subject[cursor..matched.start()], no_empty); + eval_preg_split_push_piece( + &mut pieces, + &subject[cursor..matched.start()], + cursor, + no_empty, + ); if capture_delimiters { eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); } cursor = matched.end(); } - eval_preg_split_push_piece(&mut pieces, &subject[cursor..], no_empty); + eval_preg_split_push_piece(&mut pieces, &subject[cursor..], cursor, no_empty); let mut result = values.array_new(pieces.len())?; for (index, piece) in pieces.iter().enumerate() { let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.string_bytes_value(piece)?; + let value = eval_preg_split_piece_value(piece, offset_capture, values)?; result = values.array_set(result, key, value)?; } Ok(result) @@ -10846,6 +10852,12 @@ struct EvalPregModifiers { swap_greed: bool, } +/// One `preg_split()` output segment plus its byte offset in the subject. +struct EvalPregSplitPiece { + bytes: Vec, + offset: usize, +} + /// Splits a PHP delimited regex into body bytes and supported modifiers. fn eval_preg_pattern_parts(pattern: &[u8]) -> Result<(Vec, EvalPregModifiers), EvalStatus> { if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() @@ -11096,7 +11108,7 @@ fn eval_preg_split_limit( .map_err(|_| EvalStatus::RuntimeFatal) } -/// Returns supported `preg_split()` flags and rejects offset-capture shape changes. +/// Returns supported `preg_split()` flags. fn eval_preg_split_flags( flags: Option, values: &mut impl RuntimeValueOps, @@ -11105,40 +11117,75 @@ fn eval_preg_split_flags( return Ok(0); }; let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE; - if flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0 || flags & !supported != 0 { + let supported = EVAL_PREG_SPLIT_NO_EMPTY + | EVAL_PREG_SPLIT_DELIM_CAPTURE + | EVAL_PREG_SPLIT_OFFSET_CAPTURE; + if flags & !supported != 0 { return Err(EvalStatus::RuntimeFatal); } Ok(flags) } /// Returns whether `preg_split()` should stop splitting and emit the remaining subject. -fn eval_preg_split_reached_limit(pieces: &[Vec], limit: Option) -> bool { +fn eval_preg_split_reached_limit(pieces: &[EvalPregSplitPiece], limit: Option) -> bool { matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) } /// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. -fn eval_preg_split_push_piece(pieces: &mut Vec>, piece: &[u8], no_empty: bool) { +fn eval_preg_split_push_piece( + pieces: &mut Vec, + piece: &[u8], + offset: usize, + no_empty: bool, +) { if no_empty && piece.is_empty() { return; } - pieces.push(piece.to_vec()); + pieces.push(EvalPregSplitPiece { + bytes: piece.to_vec(), + offset, + }); } /// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. fn eval_preg_split_push_captures( - pieces: &mut Vec>, + pieces: &mut Vec, subject: &[u8], captures: &Captures<'_>, no_empty: bool, ) { for index in 1..captures.len() { - if let Some(bytes) = eval_preg_capture_bytes(subject, captures, index) { - eval_preg_split_push_piece(pieces, bytes, no_empty); + if let Some(matched) = captures.get(index) { + eval_preg_split_push_piece( + pieces, + &subject[matched.start()..matched.end()], + matched.start(), + no_empty, + ); } } } +/// Converts one split segment to a string or PHP `[string, byte_offset]` pair. +fn eval_preg_split_piece_value( + piece: &EvalPregSplitPiece, + offset_capture: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.string_bytes_value(&piece.bytes)?; + if !offset_capture { + return Ok(value); + } + + let offset = i64::try_from(piece.offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} + /// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. fn eval_builtin_gethostbyaddr( args: &[EvalExpr], @@ -18916,7 +18963,13 @@ $replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "repl echo $replaced . ":"; $captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); echo count($captured) . ":" . $captured[1] . ":"; -return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE");"#, +$splitOffsets = preg_split("/,/", "a,b,c", 2, PREG_SPLIT_OFFSET_CAPTURE); +echo $splitOffsets[0][0] . ":" . $splitOffsets[0][1] . ":" . $splitOffsets[1][0] . ":" . $splitOffsets[1][1] . ":"; +$splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); +echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; +$splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); +echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; +return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -18926,7 +18979,7 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun assert_eq!( values.output, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2a636b5e65..b01840eb25 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -75,7 +75,7 @@ Eval `array_filter()` implements PHP's omitted/null callback form by iterating s Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false. Eval `iterator_apply()` drives Traversable objects through the eval method-call hooks (`rewind()`, `valid()`, and `next()`) and invokes string callbacks or object-method callable arrays through the shared eval callable dispatcher. The callback receives only the optional third-argument array, matching PHP's `iterator_apply()` behavior; current value/key are not passed implicitly. Array inputs remain runtime fatals for `iterator_apply()`. -Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures with optional `PREG_OFFSET_CAPTURE` pairs, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, direct `preg_match_all()` `PREG_SET_ORDER` captures, offset-capture pairs for both `preg_match_all()` result orders, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax surfaces as eval runtime fatals instead of falling back to a partial native ABI. +Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures with optional `PREG_OFFSET_CAPTURE` pairs, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, direct `preg_match_all()` `PREG_SET_ORDER` captures, offset-capture pairs for both `preg_match_all()` result orders, `preg_split()` no-empty, delimiter-capture, and offset-capture result forms, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax surfaces as eval runtime fatals instead of falling back to a partial native ABI. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index d9877d426e..f30343b63b 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -90,7 +90,7 @@ Eval `array_filter()` supports the PHP default omitted/null callback form, filte Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` supports Traversable objects through `rewind()`, `valid()`, `next()`, string callbacks that target eval-declared functions, registered AOT callbacks, or supported builtins, and object-method callable arrays. Arrays still fail for `iterator_apply()`, matching PHP's array `TypeError` rather than treating arrays as Traversable. -Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays with optional `PREG_OFFSET_CAPTURE` entries, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form and `PREG_SET_ORDER` match-order form with optional `PREG_OFFSET_CAPTURE` entries, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY` and `PREG_SPLIT_DELIM_CAPTURE`; PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). +Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays with optional `PREG_OFFSET_CAPTURE` entries, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form and `PREG_SET_ORDER` match-order form with optional `PREG_OFFSET_CAPTURE` entries, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY`, `PREG_SPLIT_DELIM_CAPTURE`, and `PREG_SPLIT_OFFSET_CAPTURE`; PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 95cb6d131a..9295e57657 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2441,12 +2441,18 @@ $replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "repl echo $replaced . ":"; $captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); echo count($captured) . ":" . $captured[1] . ":"; -echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE");'); +$splitOffsets = preg_split("/,/", "a,b,c", 2, PREG_SPLIT_OFFSET_CAPTURE); +echo $splitOffsets[0][0] . ":" . $splitOffsets[0][1] . ":" . $splitOffsets[1][0] . ":" . $splitOffsets[1][1] . ":"; +$splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); +echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; +$splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); +echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; +echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE");'); "#, ); assert_eq!( out, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:1" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:1" ); } From 6de4d9dadd73039b0e7c4d42d32b865ab3b3087f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:50:44 +0200 Subject: [PATCH 0249/1208] Support eval preg unmatched null captures --- crates/elephc-eval/src/interpreter.rs | 86 ++++++++++++++++++++++----- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 12 +++- 4 files changed, 82 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 5b76ad97f7..170238b64d 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -10482,12 +10482,20 @@ fn eval_preg_match_capture_result( let subject = values.string_bytes(subject)?; let flags = eval_preg_match_flags(flags, values)?; let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; if let Some(captures) = regex.captures(&subject) { - let matches = eval_preg_capture_array(&subject, Some(&captures), offset_capture, values)?; + let matches = eval_preg_capture_array( + &subject, + Some(&captures), + offset_capture, + unmatched_as_null, + values, + )?; let matched = values.int(1)?; return Ok((matched, matches)); } - let matches = eval_preg_capture_array(&subject, None, offset_capture, values)?; + let matches = + eval_preg_capture_array(&subject, None, offset_capture, unmatched_as_null, values)?; let matched = values.int(0)?; Ok((matched, matches)) } @@ -10501,7 +10509,8 @@ fn eval_preg_match_flags( return Ok(0); }; let flags = eval_int_value(flags, values)?; - if flags & !EVAL_PREG_OFFSET_CAPTURE != 0 { + let supported = EVAL_PREG_OFFSET_CAPTURE | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { return Err(EvalStatus::RuntimeFatal); } Ok(flags) @@ -10605,7 +10614,10 @@ fn eval_preg_match_all_flags( return Ok(EVAL_PREG_PATTERN_ORDER); }; let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_PATTERN_ORDER | EVAL_PREG_SET_ORDER | EVAL_PREG_OFFSET_CAPTURE; + let supported = EVAL_PREG_PATTERN_ORDER + | EVAL_PREG_SET_ORDER + | EVAL_PREG_OFFSET_CAPTURE + | EVAL_PREG_UNMATCHED_AS_NULL; if flags & !supported != 0 { return Err(EvalStatus::RuntimeFatal); } @@ -10621,6 +10633,7 @@ fn eval_preg_match_all_pattern_order_array( values: &mut impl RuntimeValueOps, ) -> Result { let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; let mut outer = values.array_new(capture_count)?; for capture_index in 0..capture_count { let mut row = values.array_new(captures.len())?; @@ -10628,7 +10641,14 @@ fn eval_preg_match_all_pattern_order_array( let key = values .int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; let value = - eval_preg_capture_value(subject, capture, capture_index, offset_capture, values)?; + eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; row = values.array_set(row, key, value)?; } let key = values @@ -10647,6 +10667,7 @@ fn eval_preg_match_all_set_order_array( values: &mut impl RuntimeValueOps, ) -> Result { let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; let mut outer = values.array_new(captures.len())?; for (match_index, capture) in captures.iter().enumerate() { let mut row = values.array_new(capture_count)?; @@ -10654,7 +10675,14 @@ fn eval_preg_match_all_set_order_array( let key = values .int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; let value = - eval_preg_capture_value(subject, capture, capture_index, offset_capture, values)?; + eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; row = values.array_set(row, key, value)?; } let key = values @@ -10738,7 +10766,7 @@ fn eval_preg_replace_callback_result( continue; }; result.extend_from_slice(&subject[cursor..matched.start()]); - let matches = eval_preg_capture_array(&subject, Some(&captures), false, values)?; + let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; let callback_result = values.cast_string(callback_result)?; let callback_bytes = values.string_bytes(callback_result)?; @@ -10946,14 +10974,24 @@ fn eval_preg_capture_array( subject: &[u8], captures: Option<&Captures<'_>>, offset_capture: bool, + unmatched_as_null: bool, values: &mut impl RuntimeValueOps, ) -> Result { - let len = captures.map_or(0, eval_preg_visible_capture_len); + let len = captures.map_or(0, |captures| { + eval_preg_visible_capture_len(captures, unmatched_as_null) + }); let mut result = values.array_new(len)?; if let Some(captures) = captures { for index in 0..len { let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value(subject, captures, index, offset_capture, values)?; + let value = eval_preg_capture_value( + subject, + captures, + index, + offset_capture, + unmatched_as_null, + values, + )?; result = values.array_set(result, key, value)?; } } @@ -10961,7 +10999,10 @@ fn eval_preg_capture_array( } /// Returns the capture count PHP should expose, dropping trailing unmatched groups. -fn eval_preg_visible_capture_len(captures: &Captures<'_>) -> usize { +fn eval_preg_visible_capture_len(captures: &Captures<'_>, unmatched_as_null: bool) -> usize { + if unmatched_as_null { + return captures.len(); + } let mut len = captures.len(); while len > 1 && captures.get(len - 1).is_none() { len -= 1; @@ -10986,13 +11027,18 @@ fn eval_preg_capture_value( captures: &Captures<'_>, index: usize, offset_capture: bool, + unmatched_as_null: bool, values: &mut impl RuntimeValueOps, ) -> Result { let matched = captures.get(index); - let bytes = matched - .as_ref() - .map_or(b"".as_slice(), |matched| &subject[matched.start()..matched.end()]); - let value = values.string_bytes_value(bytes)?; + let value = if matched.is_none() && unmatched_as_null { + values.null()? + } else { + let bytes = matched + .as_ref() + .map_or(b"".as_slice(), |matched| &subject[matched.start()..matched.end()]); + values.string_bytes_value(bytes)? + }; if !offset_capture { return Ok(value); } @@ -18949,6 +18995,14 @@ preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOne, PREG_UNMATCHED_AS_NULL); +echo count($nullOne) . ":" . ($nullOne[1] === null ? "n" : "bad") . ":" . $nullOne[2] . ":" . ($nullOne[3] === null ? "n" : "bad") . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOffset, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullOffset[1][0] === null ? "n" : "bad") . ":" . $nullOffset[1][1] . ":" . ($nullOffset[3][0] === null ? "n" : "bad") . ":" . $nullOffset[3][1] . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullAll, PREG_UNMATCHED_AS_NULL); +echo ($nullAll[1][0] === null ? "n" : "bad") . ":" . $nullAll[2][0] . ":" . ($nullAll[3][0] === null ? "n" : "bad") . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullSet, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullSet[0][1][0] === null ? "n" : "bad") . ":" . $nullSet[0][1][1] . ":" . ($nullSet[0][3][0] === null ? "n" : "bad") . ":" . $nullSet[0][3][1] . ":"; preg_match_all("/(x)(y)/", "abc", $none); echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; @@ -18969,7 +19023,7 @@ $splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; $splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; -return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE");"#, +return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -18979,7 +19033,7 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun assert_eq!( values.output, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index b01840eb25..8356b5c715 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -75,7 +75,7 @@ Eval `array_filter()` implements PHP's omitted/null callback form by iterating s Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false. Eval `iterator_apply()` drives Traversable objects through the eval method-call hooks (`rewind()`, `valid()`, and `next()`) and invokes string callbacks or object-method callable arrays through the shared eval callable dispatcher. The callback receives only the optional third-argument array, matching PHP's `iterator_apply()` behavior; current value/key are not passed implicitly. Array inputs remain runtime fatals for `iterator_apply()`. -Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures with optional `PREG_OFFSET_CAPTURE` pairs, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, direct `preg_match_all()` `PREG_SET_ORDER` captures, offset-capture pairs for both `preg_match_all()` result orders, `preg_split()` no-empty, delimiter-capture, and offset-capture result forms, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax surfaces as eval runtime fatals instead of falling back to a partial native ABI. +Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures with optional `PREG_OFFSET_CAPTURE` pairs and `PREG_UNMATCHED_AS_NULL` values, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, direct `preg_match_all()` `PREG_SET_ORDER` captures, offset-capture pairs and unmatched-as-null values for both `preg_match_all()` result orders, `preg_split()` no-empty, delimiter-capture, and offset-capture result forms, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax surfaces as eval runtime fatals instead of falling back to a partial native ABI. Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index f30343b63b..188fcb8bb6 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -90,7 +90,7 @@ Eval `array_filter()` supports the PHP default omitted/null callback form, filte Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` supports Traversable objects through `rewind()`, `valid()`, `next()`, string callbacks that target eval-declared functions, registered AOT callbacks, or supported builtins, and object-method callable arrays. Arrays still fail for `iterator_apply()`, matching PHP's array `TypeError` rather than treating arrays as Traversable. -Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays with optional `PREG_OFFSET_CAPTURE` entries, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form and `PREG_SET_ORDER` match-order form with optional `PREG_OFFSET_CAPTURE` entries, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY`, `PREG_SPLIT_DELIM_CAPTURE`, and `PREG_SPLIT_OFFSET_CAPTURE`; PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). +Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays with optional `PREG_OFFSET_CAPTURE` and `PREG_UNMATCHED_AS_NULL` entries, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form and `PREG_SET_ORDER` match-order form with optional `PREG_OFFSET_CAPTURE` and `PREG_UNMATCHED_AS_NULL` entries, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY`, `PREG_SPLIT_DELIM_CAPTURE`, and `PREG_SPLIT_OFFSET_CAPTURE`; PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9295e57657..8e2fa8102d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2427,6 +2427,14 @@ preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOne, PREG_UNMATCHED_AS_NULL); +echo count($nullOne) . ":" . ($nullOne[1] === null ? "n" : "bad") . ":" . $nullOne[2] . ":" . ($nullOne[3] === null ? "n" : "bad") . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOffset, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullOffset[1][0] === null ? "n" : "bad") . ":" . $nullOffset[1][1] . ":" . ($nullOffset[3][0] === null ? "n" : "bad") . ":" . $nullOffset[3][1] . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullAll, PREG_UNMATCHED_AS_NULL); +echo ($nullAll[1][0] === null ? "n" : "bad") . ":" . $nullAll[2][0] . ":" . ($nullAll[3][0] === null ? "n" : "bad") . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullSet, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullSet[0][1][0] === null ? "n" : "bad") . ":" . $nullSet[0][1][1] . ":" . ($nullSet[0][3][0] === null ? "n" : "bad") . ":" . $nullSet[0][3][1] . ":"; preg_match_all("/(x)(y)/", "abc", $none); echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; @@ -2447,12 +2455,12 @@ $splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; $splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; -echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE");'); +echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");'); "#, ); assert_eq!( out, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:1" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:1" ); } From 88ab6922639ee8245880dafdb598e3d8451aa9ca Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 21:57:04 +0200 Subject: [PATCH 0250/1208] Support eval JSON unescaped unicode --- crates/elephc-eval/src/interpreter.rs | 102 ++++++++++++++++++++------ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 8 +- 4 files changed, 87 insertions(+), 27 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 170238b64d..233b2e226b 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -602,6 +602,7 @@ const EVAL_JSON_FORCE_OBJECT: i64 = 16; const EVAL_JSON_NUMERIC_CHECK: i64 = 32; const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; const EVAL_JSON_PRETTY_PRINT: i64 = 128; +const EVAL_JSON_UNESCAPED_UNICODE: i64 = 256; const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; unsafe extern "C" { @@ -13336,6 +13337,7 @@ fn eval_json_encode_result( | EVAL_JSON_HEX_APOS | EVAL_JSON_HEX_QUOT | EVAL_JSON_UNESCAPED_SLASHES + | EVAL_JSON_UNESCAPED_UNICODE | EVAL_JSON_FORCE_OBJECT | EVAL_JSON_PRETTY_PRINT | EVAL_JSON_PRESERVE_ZERO_FRACTION; @@ -13880,33 +13882,78 @@ fn eval_json_encode_append_string(bytes: &[u8], flags: i64, output: &mut Vec } } output.push(b'"'); - for byte in bytes { - match *byte { - b'"' if flags & EVAL_JSON_HEX_QUOT != 0 => output.extend_from_slice(b"\\u0022"), - b'"' => output.extend_from_slice(b"\\\""), - b'\\' => output.extend_from_slice(b"\\\\"), - b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { - output.extend_from_slice(b"\\/"); - } - b'/' => output.push(b'/'), - b'<' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003C"), - b'>' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003E"), - b'&' if flags & EVAL_JSON_HEX_AMP != 0 => output.extend_from_slice(b"\\u0026"), - b'\'' if flags & EVAL_JSON_HEX_APOS != 0 => output.extend_from_slice(b"\\u0027"), - b'\x08' => output.extend_from_slice(b"\\b"), - b'\x0c' => output.extend_from_slice(b"\\f"), - b'\n' => output.extend_from_slice(b"\\n"), - b'\r' => output.extend_from_slice(b"\\r"), - b'\t' => output.extend_from_slice(b"\\t"), - control @ 0x00..=0x1f => { - output.extend_from_slice(format!("\\u{control:04x}").as_bytes()); - } - _ => output.push(*byte), + if let Ok(value) = std::str::from_utf8(bytes) { + for character in value.chars() { + eval_json_encode_append_char(character, flags, output); } + } else { + eval_json_encode_append_lossy_bytes(bytes, flags, output); } output.push(b'"'); } +/// Appends one valid UTF-8 character using PHP JSON string escaping rules. +fn eval_json_encode_append_char(character: char, flags: i64, output: &mut Vec) { + if character.is_ascii() { + eval_json_encode_append_ascii_byte(character as u8, flags, output); + } else if flags & EVAL_JSON_UNESCAPED_UNICODE != 0 { + let mut buffer = [0_u8; 4]; + output.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes()); + } else { + eval_json_encode_append_unicode_escape(character as u32, output); + } +} + +/// Appends one ASCII byte using JSON escaping rules shared by UTF-8 and fallback paths. +fn eval_json_encode_append_ascii_byte(byte: u8, flags: i64, output: &mut Vec) { + match byte { + b'"' if flags & EVAL_JSON_HEX_QUOT != 0 => output.extend_from_slice(b"\\u0022"), + b'"' => output.extend_from_slice(b"\\\""), + b'\\' => output.extend_from_slice(b"\\\\"), + b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { + output.extend_from_slice(b"\\/"); + } + b'/' => output.push(b'/'), + b'<' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003C"), + b'>' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003E"), + b'&' if flags & EVAL_JSON_HEX_AMP != 0 => output.extend_from_slice(b"\\u0026"), + b'\'' if flags & EVAL_JSON_HEX_APOS != 0 => output.extend_from_slice(b"\\u0027"), + b'\x08' => output.extend_from_slice(b"\\b"), + b'\x0c' => output.extend_from_slice(b"\\f"), + b'\n' => output.extend_from_slice(b"\\n"), + b'\r' => output.extend_from_slice(b"\\r"), + b'\t' => output.extend_from_slice(b"\\t"), + control @ 0x00..=0x1f => { + output.extend_from_slice(format!("\\u{control:04x}").as_bytes()); + } + _ => output.push(byte), + } +} + +/// Appends valid scalar values as PHP JSON `\uXXXX` escapes, using surrogate pairs when needed. +fn eval_json_encode_append_unicode_escape(codepoint: u32, output: &mut Vec) { + if codepoint <= 0xffff { + output.extend_from_slice(format!("\\u{codepoint:04x}").as_bytes()); + return; + } + + let codepoint = codepoint - 0x1_0000; + let high = 0xd800 + ((codepoint >> 10) & 0x3ff); + let low = 0xdc00 + (codepoint & 0x3ff); + output.extend_from_slice(format!("\\u{high:04x}\\u{low:04x}").as_bytes()); +} + +/// Preserves existing eval behavior for malformed byte strings while still escaping ASCII. +fn eval_json_encode_append_lossy_bytes(bytes: &[u8], flags: i64, output: &mut Vec) { + for byte in bytes { + if byte.is_ascii() { + eval_json_encode_append_ascii_byte(*byte, flags, output); + } else { + output.push(*byte); + } + } +} + /// Returns the JSON number bytes for a PHP numeric string when `JSON_NUMERIC_CHECK` applies. fn eval_json_numeric_check_bytes(bytes: &[u8]) -> Option> { let value = std::str::from_utf8(bytes).ok()?.trim(); @@ -14698,6 +14745,7 @@ fn eval_predefined_constant_value(name: &str) -> Option "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), + "JSON_UNESCAPED_UNICODE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_UNICODE)), "JSON_PRETTY_PRINT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_PRETTY_PRINT)), "JSON_PRESERVE_ZERO_FRACTION" => { Some(EvalPredefinedConstant::Int( @@ -17729,6 +17777,12 @@ echo call_user_func("json_encode", "a/b\"c") . ":"; echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; +$accent = json_decode("\"\\u00e9\""); +$emoji = json_decode("\"\\ud83d\\ude00\""); +echo bin2hex(json_encode($accent . "/" . $emoji)) . ":"; +echo bin2hex(json_encode($accent . "/" . $emoji, JSON_UNESCAPED_UNICODE)) . ":"; +echo bin2hex(json_encode([$accent => $emoji])) . ":"; +echo bin2hex(json_encode([$accent => $emoji], JSON_UNESCAPED_UNICODE)) . ":"; echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; echo json_encode([], JSON_FORCE_OBJECT) . ":"; echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; @@ -17736,7 +17790,7 @@ echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; -return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION");"#, +return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17746,7 +17800,7 @@ return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && de assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:{| "a": [| 1,| 2| ]|}:"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:{| "a": [| 1,| 2| ]|}:"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 8356b5c715..582db63177 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 188fcb8bb6..6b6a0a6204 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8e2fa8102d..6df0bab911 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -823,6 +823,12 @@ echo call_user_func("json_encode", "a/b\"c") . ":"; echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; +$accent = json_decode("\"\\u00e9\""); +$emoji = json_decode("\"\\ud83d\\ude00\""); +echo bin2hex(json_encode($accent . "/" . $emoji)) . ":"; +echo bin2hex(json_encode($accent . "/" . $emoji, JSON_UNESCAPED_UNICODE)) . ":"; +echo bin2hex(json_encode([$accent => $emoji])) . ":"; +echo bin2hex(json_encode([$accent => $emoji], JSON_UNESCAPED_UNICODE)) . ":"; echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; echo json_encode([], JSON_FORCE_OBJECT) . ":"; echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; @@ -835,7 +841,7 @@ echo function_exists("json_encode");'); ); assert_eq!( out, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:{| "a": [| 1,| 2| ]|}:1"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:{| "a": [| 1,| 2| ]|}:1"# ); } From 81471b5f1884b3e84b1e47c49d86d5c0acb9f6cd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 22:06:30 +0200 Subject: [PATCH 0251/1208] Support eval JSON partial nonfinite output --- crates/elephc-eval/src/interpreter.rs | 69 ++++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 8 +++- 4 files changed, 73 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 233b2e226b..ff24ee33d1 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -84,6 +84,7 @@ struct EvalSprintfSpec { /// Eval-visible predefined constant payloads that are not stored in the dynamic context. enum EvalPredefinedConstant { Int(i64), + Float(f64), String(&'static str), } @@ -603,7 +604,9 @@ const EVAL_JSON_NUMERIC_CHECK: i64 = 32; const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; const EVAL_JSON_PRETTY_PRINT: i64 = 128; const EVAL_JSON_UNESCAPED_UNICODE: i64 = 256; +const EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR: i64 = 512; const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; +const EVAL_JSON_INF_OR_NAN_MESSAGE: &str = "Inf and NaN cannot be JSON encoded"; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -13340,6 +13343,7 @@ fn eval_json_encode_result( | EVAL_JSON_UNESCAPED_UNICODE | EVAL_JSON_FORCE_OBJECT | EVAL_JSON_PRETTY_PRINT + | EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR | EVAL_JSON_PRESERVE_ZERO_FRACTION; let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; if flags & !supported_flags != 0 { @@ -13354,6 +13358,7 @@ fn eval_json_encode_result( } let mut output = Vec::new(); + let mut error = None; eval_json_encode_append( value, values, @@ -13361,9 +13366,17 @@ fn eval_json_encode_result( depth as usize, 0, &mut Vec::new(), + &mut error, &mut output, )?; - context.clear_json_error(); + if let Some(error) = error { + context.set_json_error(error.code, error.message); + if flags & EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR == 0 { + return values.bool_value(false); + } + } else { + context.clear_json_error(); + } values.string_bytes_value(&output) } @@ -13673,12 +13686,13 @@ fn eval_json_encode_append( depth_limit: usize, depth: usize, arrays_seen: &mut Vec, + error: &mut Option, output: &mut Vec, ) -> Result<(), EvalStatus> { match values.type_tag(value)? { EVAL_TAG_INT => output.extend_from_slice(&values.string_bytes(value)?), EVAL_TAG_FLOAT => { - eval_json_encode_append_float(&values.string_bytes(value)?, flags, output); + eval_json_encode_append_float(value, values, flags, error, output)?; } EVAL_TAG_STRING => eval_json_encode_append_string(&values.string_bytes(value)?, flags, output), EVAL_TAG_BOOL => { @@ -13696,6 +13710,7 @@ fn eval_json_encode_append( depth_limit, depth, arrays_seen, + error, output, )?; } @@ -13707,6 +13722,7 @@ fn eval_json_encode_append( depth_limit, depth, arrays_seen, + error, output, )?; } @@ -13716,9 +13732,31 @@ fn eval_json_encode_append( Ok(()) } +#[derive(Clone, Copy)] +struct EvalJsonEncodeError { + code: i64, + message: &'static str, +} + /// Appends one JSON float while preserving a `.0` suffix when requested. -fn eval_json_encode_append_float(bytes: &[u8], flags: i64, output: &mut Vec) { - output.extend_from_slice(bytes); +fn eval_json_encode_append_float( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let float = eval_float_value(value, values)?; + if !float.is_finite() { + *error = Some(EvalJsonEncodeError { + code: EVAL_JSON_ERROR_INF_OR_NAN, + message: EVAL_JSON_INF_OR_NAN_MESSAGE, + }); + output.push(b'0'); + return Ok(()); + } + let bytes = values.string_bytes(value)?; + output.extend_from_slice(&bytes); if flags & EVAL_JSON_PRESERVE_ZERO_FRACTION != 0 && !bytes .iter() @@ -13726,6 +13764,7 @@ fn eval_json_encode_append_float(bytes: &[u8], flags: i64, output: &mut Vec) { output.extend_from_slice(b".0"); } + Ok(()) } /// Appends one indexed eval array as a JSON array or forced JSON object. @@ -13736,6 +13775,7 @@ fn eval_json_encode_append_indexed_array( depth_limit: usize, depth: usize, arrays_seen: &mut Vec, + error: &mut Option, output: &mut Vec, ) -> Result<(), EvalStatus> { eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; @@ -13773,6 +13813,7 @@ fn eval_json_encode_append_indexed_array( depth_limit, depth + 1, arrays_seen, + error, output, )?; } @@ -13793,6 +13834,7 @@ fn eval_json_encode_append_assoc( depth_limit: usize, depth: usize, arrays_seen: &mut Vec, + error: &mut Option, output: &mut Vec, ) -> Result<(), EvalStatus> { eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; @@ -13827,6 +13869,7 @@ fn eval_json_encode_append_assoc( depth_limit, depth + 1, arrays_seen, + error, output, )?; } @@ -14684,6 +14727,7 @@ fn eval_predefined_constant( }; match value { EvalPredefinedConstant::Int(value) => values.int(value).map(Some), + EvalPredefinedConstant::Float(value) => values.float(value).map(Some), EvalPredefinedConstant::String(value) => values.string(value).map(Some), } } @@ -14746,12 +14790,19 @@ fn eval_predefined_constant_value(name: &str) -> Option "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), "JSON_UNESCAPED_UNICODE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_UNICODE)), + "JSON_PARTIAL_OUTPUT_ON_ERROR" => { + Some(EvalPredefinedConstant::Int( + EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR, + )) + } "JSON_PRETTY_PRINT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_PRETTY_PRINT)), "JSON_PRESERVE_ZERO_FRACTION" => { Some(EvalPredefinedConstant::Int( EVAL_JSON_PRESERVE_ZERO_FRACTION, )) } + "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), + "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), @@ -17789,8 +17840,14 @@ echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FOR echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; +echo (json_encode(INF) === false ? "false" : "json") . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_encode(3.5); +echo json_last_error() . ":" . json_last_error_msg() . ":"; echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; -return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION");"#, +return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17800,7 +17857,7 @@ return function_exists("json_encode") && defined("JSON_UNESCAPED_SLASHES") && de assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:{| "a": [| 1,| 2| ]|}:"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:0:No error:{| "a": [| 1,| 2| ]|}:"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 582db63177..8f8eda40fd 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report syntax, depth, control-character, UTF-8, and UTF-16 failures from eval `json_decode()` / `json_validate()`, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6b6a0a6204..151bead055 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for `json_decode()` / `json_validate()` failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. @@ -120,7 +120,7 @@ Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and Eval path component calls include `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` with suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, named arguments, callable dispatch, and `function_exists()` probes. -Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, `COUNT_*`, and the supported `PREG_*` constants. `defined()` sees these names, including an optional leading `\`, and `define()` cannot replace them. +Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `INF`, `NAN`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, `COUNT_*`, and the supported `PREG_*` / `JSON_*` constants. `defined()` sees these names, including an optional leading `\`, and `define()` cannot replace them. Eval `realpath()` canonicalizes existing paths through the host filesystem and returns `false` when the path cannot be resolved. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6df0bab911..888d975545 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -835,13 +835,19 @@ echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FOR echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; +echo (json_encode(INF) === false ? "false" : "json") . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_encode(3.5); +echo json_last_error() . ":" . json_last_error_msg() . ":"; echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; echo function_exists("json_encode");'); "#, ); assert_eq!( out, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:{| "a": [| 1,| 2| ]|}:1"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:0:No error:{| "a": [| 1,| 2| ]|}:1"# ); } From 1150fe67279f4a23a5e5bc51a23d93175d1ff4ab Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 22:11:48 +0200 Subject: [PATCH 0252/1208] Support eval JSON validate UTF-8 ignore --- crates/elephc-eval/src/interpreter.rs | 28 +++++++++++++++------ crates/elephc-eval/src/json_validate.rs | 33 ++++++++++++++++++++++--- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 6 ++++- 5 files changed, 58 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index ff24ee33d1..6d6df4177e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -606,6 +606,7 @@ const EVAL_JSON_PRETTY_PRINT: i64 = 128; const EVAL_JSON_UNESCAPED_UNICODE: i64 = 256; const EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR: i64 = 512; const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; +const EVAL_JSON_INVALID_UTF8_IGNORE: i64 = 1_048_576; const EVAL_JSON_INF_OR_NAN_MESSAGE: &str = "Inf and NaN cannot be JSON encoded"; unsafe extern "C" { @@ -13592,12 +13593,11 @@ fn eval_json_validate_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if flags + let flags = flags .map(|flags| eval_int_value(flags, values)) .transpose()? - .unwrap_or(0) - != 0 - { + .unwrap_or(0); + if flags & !EVAL_JSON_INVALID_UTF8_IGNORE != 0 { return Err(EvalStatus::UnsupportedConstruct); } let depth = depth @@ -13609,7 +13609,12 @@ fn eval_json_validate_result( } let bytes = values.string_bytes(json)?; - match json_validate::decode_result(&bytes, depth as usize) { + let result = if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) + } else { + json_validate::decode_result(&bytes, depth as usize) + }; + match result { Ok(_) => { context.clear_json_error(); values.bool_value(true) @@ -14801,6 +14806,11 @@ fn eval_predefined_constant_value(name: &str) -> Option EVAL_JSON_PRESERVE_ZERO_FRACTION, )) } + "JSON_INVALID_UTF8_IGNORE" => { + Some(EvalPredefinedConstant::Int( + EVAL_JSON_INVALID_UTF8_IGNORE, + )) + } "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), @@ -17940,7 +17950,11 @@ echo (json_validate("bad") ? "bad" : "N") . ":"; echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; echo (call_user_func("json_validate", "\"x\"") ? "C" : "bad") . ":"; echo (call_user_func_array("json_validate", ["json" => "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; -return function_exists("json_validate");"#, +echo (json_validate("\"a" . chr(128) . "b\"", 512, JSON_INVALID_UTF8_IGNORE) ? "I" : "bad") . ":"; +echo json_last_error() . ":"; +echo (json_validate("bad", 512, JSON_INVALID_UTF8_IGNORE) ? "bad" : "S") . ":"; +echo json_last_error() . ":"; +return function_exists("json_validate") && defined("JSON_INVALID_UTF8_IGNORE");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17948,7 +17962,7 @@ return function_exists("json_validate");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Y:N:D:C:A:"); + assert_eq!(values.output, "Y:N:D:C:A:I:0:S:4:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/json_validate.rs b/crates/elephc-eval/src/json_validate.rs index cf6952893d..8234bb8607 100644 --- a/crates/elephc-eval/src/json_validate.rs +++ b/crates/elephc-eval/src/json_validate.rs @@ -9,7 +9,8 @@ //! - Container depth follows PHP decode/validate semantics: entering a container //! is rejected when the active depth would reach the requested limit. //! - String parsing accepts JSON escapes, paired UTF-16 surrogate escapes, and raw -//! UTF-8 bytes while rejecting control bytes and malformed UTF-8. +//! UTF-8 bytes while rejecting control bytes; malformed UTF-8 is rejected by +//! default and can be ignored for PHP's `JSON_INVALID_UTF8_IGNORE` validate flag. /// Parsed JSON value used by eval JSON builtins before runtime-cell allocation. pub(crate) enum JsonValue { @@ -64,11 +65,21 @@ pub(crate) fn decode_result( parser.parse_document() } +/// Parses one complete JSON document while ignoring malformed raw UTF-8 string bytes. +pub(crate) fn decode_result_ignoring_invalid_utf8( + bytes: &[u8], + depth_limit: usize, +) -> Result { + let mut parser = Parser::new_with_invalid_utf8_ignore(bytes, depth_limit); + parser.parse_document() +} + /// Cursor-based JSON parser for eval JSON builtin calls. struct Parser<'a> { bytes: &'a [u8], cursor: usize, depth_limit: usize, + ignore_invalid_utf8: bool, } impl<'a> Parser<'a> { @@ -78,6 +89,17 @@ impl<'a> Parser<'a> { bytes, cursor: 0, depth_limit, + ignore_invalid_utf8: false, + } + } + + /// Creates a JSON parser that drops malformed raw UTF-8 bytes inside strings. + fn new_with_invalid_utf8_ignore(bytes: &'a [u8], depth_limit: usize) -> Self { + Self { + bytes, + cursor: 0, + depth_limit, + ignore_invalid_utf8: true, } } @@ -198,8 +220,13 @@ impl<'a> Parser<'a> { } _ => { let start = self.cursor; - self.consume_utf8_char()?; - output.extend_from_slice(&self.bytes[start..self.cursor]); + match self.consume_utf8_char() { + Ok(()) => output.extend_from_slice(&self.bytes[start..self.cursor]), + Err(_) if self.ignore_invalid_utf8 => { + self.cursor = start + 1; + } + Err(error) => return Err(error), + } } } } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 8f8eda40fd..39c1b11357 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, with JSON objects represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. JSON objects are represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 151bead055..3f730da5a7 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, and `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 888d975545..0cd94cf73c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -920,10 +920,14 @@ echo (json_validate("bad") ? "bad" : "N") . ":"; echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; echo (call_user_func("json_validate", "\"x\"") ? "C" : "bad") . ":"; echo (call_user_func_array("json_validate", ["json" => "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; +echo (json_validate("\"a" . chr(128) . "b\"", 512, JSON_INVALID_UTF8_IGNORE) ? "I" : "bad") . ":"; +echo json_last_error() . ":"; +echo (json_validate("bad", 512, JSON_INVALID_UTF8_IGNORE) ? "bad" : "S") . ":"; +echo json_last_error() . ":"; echo function_exists("json_validate");'); "#, ); - assert_eq!(out, "Y:N:D:C:A:1"); + assert_eq!(out, "Y:N:D:C:A:I:0:S:4:1"); } /// Verifies eval direct builtin calls bind named arguments and spread arrays. From 3fd207a4f2840eaa0aeec321e75cdfc52915bb00 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 22:20:50 +0200 Subject: [PATCH 0253/1208] Support eval JSON encode invalid UTF-8 flags --- crates/elephc-eval/src/interpreter.rs | 114 ++++++++++++++++++++++---- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 15 +++- 4 files changed, 113 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6d6df4177e..2e47ab9e89 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -607,7 +607,9 @@ const EVAL_JSON_UNESCAPED_UNICODE: i64 = 256; const EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR: i64 = 512; const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; const EVAL_JSON_INVALID_UTF8_IGNORE: i64 = 1_048_576; +const EVAL_JSON_INVALID_UTF8_SUBSTITUTE: i64 = 2_097_152; const EVAL_JSON_INF_OR_NAN_MESSAGE: &str = "Inf and NaN cannot be JSON encoded"; +const EVAL_JSON_UTF8_MESSAGE: &str = "Malformed UTF-8 characters, possibly incorrectly encoded"; unsafe extern "C" { /// Sets the process file-creation mask and returns the previous mask. @@ -13345,7 +13347,9 @@ fn eval_json_encode_result( | EVAL_JSON_FORCE_OBJECT | EVAL_JSON_PRETTY_PRINT | EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR - | EVAL_JSON_PRESERVE_ZERO_FRACTION; + | EVAL_JSON_PRESERVE_ZERO_FRACTION + | EVAL_JSON_INVALID_UTF8_IGNORE + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE; let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; if flags & !supported_flags != 0 { return Err(EvalStatus::UnsupportedConstruct); @@ -13648,7 +13652,7 @@ fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str ), JsonParseErrorKind::Utf8 => ( EVAL_JSON_ERROR_UTF8, - "Malformed UTF-8 characters, possibly incorrectly encoded", + EVAL_JSON_UTF8_MESSAGE, ), JsonParseErrorKind::Utf16 => ( EVAL_JSON_ERROR_UTF16, @@ -13699,7 +13703,13 @@ fn eval_json_encode_append( EVAL_TAG_FLOAT => { eval_json_encode_append_float(value, values, flags, error, output)?; } - EVAL_TAG_STRING => eval_json_encode_append_string(&values.string_bytes(value)?, flags, output), + EVAL_TAG_STRING => eval_json_encode_append_string( + &values.string_bytes(value)?, + flags, + EvalJsonStringPosition::Value, + error, + output, + )?, EVAL_TAG_BOOL => { if values.truthy(value)? { output.extend_from_slice(b"true"); @@ -13743,6 +13753,13 @@ struct EvalJsonEncodeError { message: &'static str, } +/// Marks whether a JSON string is being encoded as a value or as an object key. +#[derive(Clone, Copy)] +enum EvalJsonStringPosition { + Value, + Key, +} + /// Appends one JSON float while preserving a `.0` suffix when requested. fn eval_json_encode_append_float( value: RuntimeCellHandle, @@ -13806,8 +13823,10 @@ fn eval_json_encode_append_indexed_array( eval_json_encode_append_string( &values.string_bytes(key)?, flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, output, - ); + )?; eval_json_encode_append_colon(flags, output); } let element = values.array_get(value, key)?; @@ -13863,8 +13882,10 @@ fn eval_json_encode_append_assoc( eval_json_encode_append_string( &values.string_bytes(key)?, flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, output, - ); + )?; eval_json_encode_append_colon(flags, output); let element = values.array_get(value, key)?; eval_json_encode_append( @@ -13922,22 +13943,41 @@ fn eval_json_encode_enter_array( } /// Appends one JSON string with eval-supported PHP flag handling. -fn eval_json_encode_append_string(bytes: &[u8], flags: i64, output: &mut Vec) { +fn eval_json_encode_append_string( + bytes: &[u8], + flags: i64, + position: EvalJsonStringPosition, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { if flags & EVAL_JSON_NUMERIC_CHECK != 0 { if let Some(number) = eval_json_numeric_check_bytes(bytes) { output.extend_from_slice(&number); - return; + return Ok(()); } } + let start_len = output.len(); output.push(b'"'); if let Ok(value) = std::str::from_utf8(bytes) { for character in value.chars() { eval_json_encode_append_char(character, flags, output); } + } else if flags & (EVAL_JSON_INVALID_UTF8_IGNORE | EVAL_JSON_INVALID_UTF8_SUBSTITUTE) == 0 { + output.truncate(start_len); + *error = Some(EvalJsonEncodeError { + code: EVAL_JSON_ERROR_UTF8, + message: EVAL_JSON_UTF8_MESSAGE, + }); + match position { + EvalJsonStringPosition::Value => output.extend_from_slice(b"null"), + EvalJsonStringPosition::Key => output.extend_from_slice(b"\"\""), + } + return Ok(()); } else { - eval_json_encode_append_lossy_bytes(bytes, flags, output); + eval_json_encode_append_invalid_utf8_bytes(bytes, flags, output)?; } output.push(b'"'); + Ok(()) } /// Appends one valid UTF-8 character using PHP JSON string escaping rules. @@ -13991,15 +14031,37 @@ fn eval_json_encode_append_unicode_escape(codepoint: u32, output: &mut Vec) output.extend_from_slice(format!("\\u{high:04x}\\u{low:04x}").as_bytes()); } -/// Preserves existing eval behavior for malformed byte strings while still escaping ASCII. -fn eval_json_encode_append_lossy_bytes(bytes: &[u8], flags: i64, output: &mut Vec) { - for byte in bytes { - if byte.is_ascii() { - eval_json_encode_append_ascii_byte(*byte, flags, output); - } else { - output.push(*byte); +/// Appends malformed UTF-8 bytes according to PHP's JSON invalid-UTF-8 flags. +fn eval_json_encode_append_invalid_utf8_bytes( + mut bytes: &[u8], + flags: i64, + output: &mut Vec, +) -> Result<(), EvalStatus> { + while !bytes.is_empty() { + match std::str::from_utf8(bytes) { + Ok(value) => { + for character in value.chars() { + eval_json_encode_append_char(character, flags, output); + } + return Ok(()); + } + Err(error) => { + let valid = &bytes[..error.valid_up_to()]; + for character in std::str::from_utf8(valid) + .map_err(|_| EvalStatus::RuntimeFatal)? + .chars() + { + eval_json_encode_append_char(character, flags, output); + } + let invalid_len = error.error_len().unwrap_or(bytes.len() - valid.len()).max(1); + if flags & EVAL_JSON_INVALID_UTF8_IGNORE == 0 { + eval_json_encode_append_char('\u{fffd}', flags, output); + } + bytes = &bytes[valid.len() + invalid_len.min(bytes.len() - valid.len())..]; + } } } + Ok(()) } /// Returns the JSON number bytes for a PHP numeric string when `JSON_NUMERIC_CHECK` applies. @@ -14811,6 +14873,11 @@ fn eval_predefined_constant_value(name: &str) -> Option EVAL_JSON_INVALID_UTF8_IGNORE, )) } + "JSON_INVALID_UTF8_SUBSTITUTE" => { + Some(EvalPredefinedConstant::Int( + EVAL_JSON_INVALID_UTF8_SUBSTITUTE, + )) + } "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), @@ -17854,10 +17921,23 @@ echo (json_encode(INF) === false ? "false" : "json") . ":"; echo json_last_error() . ":" . json_last_error_msg() . ":"; echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; echo json_last_error() . ":" . json_last_error_msg() . ":"; +$bad = "a" . hex2bin("80") . "b"; +echo (json_encode($bad) === false ? "utf8-false" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR)) . ":"; +echo json_last_error() . ":"; +echo json_encode($bad, JSON_INVALID_UTF8_IGNORE) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE)) . ":"; +echo json_last_error() . ":"; +echo json_encode([hex2bin("6b80") => hex2bin("7680")], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":"; json_encode(3.5); echo json_last_error() . ":" . json_last_error_msg() . ":"; echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; -return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION");"#, +return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17867,7 +17947,7 @@ return function_exists("json_encode") && defined("INF") && defined("NAN") && def assert_eq!( values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:0:No error:{| "a": [| 1,| 2| ]|}:"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"k\ufffd":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 39c1b11357..6ada656814 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. JSON objects are represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, and `JSON_INVALID_UTF8_SUBSTITUTE`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. JSON objects are represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 3f730da5a7..9a38654cfd 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, and `JSON_PRESERVE_ZERO_FRACTION` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, and `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, and `JSON_INVALID_UTF8_SUBSTITUTE` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, and `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0cd94cf73c..31252620a8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -839,6 +839,19 @@ echo (json_encode(INF) === false ? "false" : "json") . ":"; echo json_last_error() . ":" . json_last_error_msg() . ":"; echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; echo json_last_error() . ":" . json_last_error_msg() . ":"; +$bad = "a" . chr(128) . "b"; +echo (json_encode($bad) === false ? "utf8-false" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR)) . ":"; +echo json_last_error() . ":"; +echo json_encode($bad, JSON_INVALID_UTF8_IGNORE) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE)) . ":"; +echo json_last_error() . ":"; +echo json_encode(["k" . chr(128) => "v" . chr(128)], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":"; json_encode(3.5); echo json_last_error() . ":" . json_last_error_msg() . ":"; echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; @@ -847,7 +860,7 @@ echo function_exists("json_encode");'); ); assert_eq!( out, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:0:No error:{| "a": [| 1,| 2| ]|}:1"# + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:1"# ); } From 0cda31cd50dacf9e87776679f01de358d1a09eea Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 22:25:41 +0200 Subject: [PATCH 0254/1208] Support eval JSON decode invalid UTF-8 flags --- crates/elephc-eval/src/interpreter.rs | 31 +++++++++++-- crates/elephc-eval/src/json_validate.rs | 61 +++++++++++++++++++++---- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 15 +++++- 5 files changed, 96 insertions(+), 15 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2e47ab9e89..3feba9e081 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -13439,7 +13439,10 @@ fn eval_json_decode_result( .map(|flags| eval_int_value(flags, values)) .transpose()? .unwrap_or(0); - if flags & !EVAL_JSON_BIGINT_AS_STRING != 0 { + let supported_flags = EVAL_JSON_BIGINT_AS_STRING + | EVAL_JSON_INVALID_UTF8_IGNORE + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE; + if flags & !supported_flags != 0 { return Err(EvalStatus::UnsupportedConstruct); } if let Some(associative) = associative { @@ -13454,7 +13457,14 @@ fn eval_json_decode_result( } let bytes = values.string_bytes(json)?; - let decoded = match json_validate::decode_result(&bytes, depth as usize) { + let decoded_result = if flags & EVAL_JSON_INVALID_UTF8_SUBSTITUTE != 0 { + json_validate::decode_result_substituting_invalid_utf8(&bytes, depth as usize) + } else if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) + } else { + json_validate::decode_result(&bytes, depth as usize) + }; + let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => { eval_record_json_parse_error(context, error, &bytes); @@ -17966,13 +17976,26 @@ $call = call_user_func("json_decode", "[3,4]"); echo $call[1] . ":"; $named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); echo $named["k"] . ":"; +$badJson = "\"a" . hex2bin("80") . "b\""; +echo (is_null(json_decode($badJson)) ? "utf8-null" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_IGNORE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +$objSub = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_SUBSTITUTE); +$objSubKeys = array_keys($objSub); +echo bin2hex($objSubKeys[0]) . "=" . bin2hex($objSub[$objSubKeys[0]]) . ":"; +$objIgnore = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_IGNORE); +$objIgnoreKeys = array_keys($objIgnore); +echo bin2hex($objIgnoreKeys[0]) . "=" . bin2hex($objIgnore[$objIgnoreKeys[0]]) . ":"; echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; $big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; echo gettype($big[0]) . ":" . $big[0] . ":"; echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; -return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING");"#, +return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -17982,7 +18005,7 @@ return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING");"#, assert_eq!( values.output, - "hello:42:T:NULL:1:x:F:4:v:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" + "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/json_validate.rs b/crates/elephc-eval/src/json_validate.rs index 8234bb8607..f27d384665 100644 --- a/crates/elephc-eval/src/json_validate.rs +++ b/crates/elephc-eval/src/json_validate.rs @@ -10,7 +10,7 @@ //! is rejected when the active depth would reach the requested limit. //! - String parsing accepts JSON escapes, paired UTF-16 surrogate escapes, and raw //! UTF-8 bytes while rejecting control bytes; malformed UTF-8 is rejected by -//! default and can be ignored for PHP's `JSON_INVALID_UTF8_IGNORE` validate flag. +//! default and can be ignored or substituted for PHP JSON UTF-8 flags. /// Parsed JSON value used by eval JSON builtins before runtime-cell allocation. pub(crate) enum JsonValue { @@ -74,12 +74,29 @@ pub(crate) fn decode_result_ignoring_invalid_utf8( parser.parse_document() } +/// Parses one complete JSON document while replacing malformed raw UTF-8 with U+FFFD. +pub(crate) fn decode_result_substituting_invalid_utf8( + bytes: &[u8], + depth_limit: usize, +) -> Result { + let mut parser = Parser::new_with_invalid_utf8_substitute(bytes, depth_limit); + parser.parse_document() +} + +/// How malformed raw UTF-8 bytes inside JSON strings should be handled. +#[derive(Clone, Copy)] +enum InvalidUtf8Mode { + Reject, + Ignore, + Substitute, +} + /// Cursor-based JSON parser for eval JSON builtin calls. struct Parser<'a> { bytes: &'a [u8], cursor: usize, depth_limit: usize, - ignore_invalid_utf8: bool, + invalid_utf8_mode: InvalidUtf8Mode, } impl<'a> Parser<'a> { @@ -89,7 +106,7 @@ impl<'a> Parser<'a> { bytes, cursor: 0, depth_limit, - ignore_invalid_utf8: false, + invalid_utf8_mode: InvalidUtf8Mode::Reject, } } @@ -99,7 +116,17 @@ impl<'a> Parser<'a> { bytes, cursor: 0, depth_limit, - ignore_invalid_utf8: true, + invalid_utf8_mode: InvalidUtf8Mode::Ignore, + } + } + + /// Creates a JSON parser that substitutes malformed raw UTF-8 bytes inside strings. + fn new_with_invalid_utf8_substitute(bytes: &'a [u8], depth_limit: usize) -> Self { + Self { + bytes, + cursor: 0, + depth_limit, + invalid_utf8_mode: InvalidUtf8Mode::Substitute, } } @@ -222,10 +249,7 @@ impl<'a> Parser<'a> { let start = self.cursor; match self.consume_utf8_char() { Ok(()) => output.extend_from_slice(&self.bytes[start..self.cursor]), - Err(_) if self.ignore_invalid_utf8 => { - self.cursor = start + 1; - } - Err(error) => return Err(error), + Err(error) => self.handle_invalid_utf8(error, start, &mut output)?, } } } @@ -233,6 +257,27 @@ impl<'a> Parser<'a> { Err(self.error(JsonParseErrorKind::Syntax)) } + /// Applies the configured malformed-UTF-8 policy for a raw string byte. + fn handle_invalid_utf8( + &mut self, + error: JsonParseError, + start: usize, + output: &mut Vec, + ) -> Result<(), JsonParseError> { + match self.invalid_utf8_mode { + InvalidUtf8Mode::Reject => Err(error), + InvalidUtf8Mode::Ignore => { + self.cursor = start + 1; + Ok(()) + } + InvalidUtf8Mode::Substitute => { + append_codepoint(output, 0xfffd).ok_or_else(|| self.error(JsonParseErrorKind::Utf8))?; + self.cursor = start + 1; + Ok(()) + } + } + } + /// Parses one JSON string escape sequence at the current backslash. fn parse_string_escape(&mut self, output: &mut Vec) -> Result<(), JsonParseError> { self.cursor += 1; diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 6ada656814..3cf195e6b3 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, and `JSON_INVALID_UTF8_SUBSTITUTE`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. JSON objects are represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, and `JSON_INVALID_UTF8_SUBSTITUTE`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set and applying the malformed-UTF-8 ignore/substitute modes when those decode flags are present, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. JSON objects are represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 9a38654cfd..2751ad61ca 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, and `JSON_INVALID_UTF8_SUBSTITUTE` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, and `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, and `JSON_INVALID_UTF8_SUBSTITUTE` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, drops malformed raw UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, and substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`; `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 31252620a8..20d1b9c09b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -879,6 +879,19 @@ $call = call_user_func("json_decode", "[3,4]"); echo $call[1] . ":"; $named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); echo $named["k"] . ":"; +$badJson = "\"a" . chr(128) . "b\""; +echo (is_null(json_decode($badJson)) ? "utf8-null" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_IGNORE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +$objSub = json_decode("{\"k" . chr(128) . "\":\"v" . chr(128) . "\"}", true, 512, JSON_INVALID_UTF8_SUBSTITUTE); +$objSubKeys = array_keys($objSub); +echo bin2hex($objSubKeys[0]) . "=" . bin2hex($objSub[$objSubKeys[0]]) . ":"; +$objIgnore = json_decode("{\"k" . chr(128) . "\":\"v" . chr(128) . "\"}", true, 512, JSON_INVALID_UTF8_IGNORE); +$objIgnoreKeys = array_keys($objIgnore); +echo bin2hex($objIgnoreKeys[0]) . "=" . bin2hex($objIgnore[$objIgnoreKeys[0]]) . ":"; echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; $big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; @@ -890,7 +903,7 @@ echo function_exists("json_decode");'); ); assert_eq!( out, - "hello:42:T:NULL:1:x:F:4:v:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:1" + "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:1" ); } From e53f2991c8abff281a0209e4f67d87edee4d4959 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 22:42:07 +0200 Subject: [PATCH 0255/1208] Support eval JSON throw-on-error --- crates/elephc-eval/src/interpreter.rs | 83 +++++++++- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen/eval_constructor_helpers.rs | 197 +++++++++++++++++++++++- tests/codegen/eval.rs | 33 ++++ 5 files changed, 305 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 3feba9e081..542db2ef60 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -608,6 +608,7 @@ const EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR: i64 = 512; const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; const EVAL_JSON_INVALID_UTF8_IGNORE: i64 = 1_048_576; const EVAL_JSON_INVALID_UTF8_SUBSTITUTE: i64 = 2_097_152; +const EVAL_JSON_THROW_ON_ERROR: i64 = 4_194_304; const EVAL_JSON_INF_OR_NAN_MESSAGE: &str = "Inf and NaN cannot be JSON encoded"; const EVAL_JSON_UTF8_MESSAGE: &str = "Malformed UTF-8 characters, possibly incorrectly encoded"; @@ -1081,11 +1082,18 @@ fn execute_try_stmt( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let control = match execute_statements(body, context, scope, values)? { - EvalControl::Throw(thrown) => { + let control = match execute_statements(body, context, scope, values) { + Ok(EvalControl::Throw(thrown)) => { execute_matching_catch(thrown, catches, context, scope, values)? } - control => control, + Err(EvalStatus::UncaughtThrowable) => { + let Some(thrown) = context.take_pending_throw() else { + return Err(EvalStatus::UncaughtThrowable); + }; + execute_matching_catch(thrown, catches, context, scope, values)? + } + Ok(control) => control, + Err(status) => return Err(status), }; if finally_body.is_empty() { return Ok(control); @@ -13349,7 +13357,8 @@ fn eval_json_encode_result( | EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR | EVAL_JSON_PRESERVE_ZERO_FRACTION | EVAL_JSON_INVALID_UTF8_IGNORE - | EVAL_JSON_INVALID_UTF8_SUBSTITUTE; + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE + | EVAL_JSON_THROW_ON_ERROR; let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; if flags & !supported_flags != 0 { return Err(EvalStatus::UnsupportedConstruct); @@ -13377,6 +13386,9 @@ fn eval_json_encode_result( if let Some(error) = error { context.set_json_error(error.code, error.message); if flags & EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR == 0 { + if flags & EVAL_JSON_THROW_ON_ERROR != 0 { + return eval_throw_json_exception(error.code, error.message, context, values); + } return values.bool_value(false); } } else { @@ -13441,7 +13453,8 @@ fn eval_json_decode_result( .unwrap_or(0); let supported_flags = EVAL_JSON_BIGINT_AS_STRING | EVAL_JSON_INVALID_UTF8_IGNORE - | EVAL_JSON_INVALID_UTF8_SUBSTITUTE; + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE + | EVAL_JSON_THROW_ON_ERROR; if flags & !supported_flags != 0 { return Err(EvalStatus::UnsupportedConstruct); } @@ -13467,7 +13480,11 @@ fn eval_json_decode_result( let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => { - eval_record_json_parse_error(context, error, &bytes); + let (code, message) = eval_json_parse_error_details(error, &bytes); + if flags & EVAL_JSON_THROW_ON_ERROR != 0 { + return eval_throw_json_exception(code, &message, context, values); + } + context.set_json_error(code, message); return values.null(); } }; @@ -13646,9 +13663,31 @@ fn eval_record_json_parse_error( error: JsonParseError, bytes: &[u8], ) { + let (code, message) = eval_json_parse_error_details(error, bytes); + context.set_json_error(code, message); +} + +/// Builds the PHP JSON error code and message for one parser failure. +fn eval_json_parse_error_details(error: JsonParseError, bytes: &[u8]) -> (i64, String) { let (code, message) = eval_json_parse_error_status(error.kind()); let message = eval_json_error_message_with_location(message, bytes, error.offset()); - context.set_json_error(code, message); + (code, message) +} + +/// Creates and schedules a `JsonException` through eval's normal Throwable channel. +fn eval_throw_json_exception( + code: i64, + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.set_json_error(code, message.to_string()); + let exception = values.new_object("JsonException")?; + let message = values.string(message)?; + let code = values.int(code)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) } /// Maps eval JSON parser failures to PHP `JSON_ERROR_*` codes and messages. @@ -14888,6 +14927,7 @@ fn eval_predefined_constant_value(name: &str) -> Option EVAL_JSON_INVALID_UTF8_SUBSTITUTE, )) } + "JSON_THROW_ON_ERROR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_THROW_ON_ERROR)), "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), @@ -18044,6 +18084,35 @@ return function_exists("json_last_error") && function_exists("json_last_error_ms assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval JSON throw flags raise catchable Throwable objects. + #[test] + fn execute_program_dispatches_json_throw_on_error() { + let program = parse_fragment( + br#"try { + json_decode("bad", true, 512, JSON_THROW_ON_ERROR); + echo "bad"; +} catch (Throwable) { + echo "decode:"; +} +try { + json_encode(INF, JSON_THROW_ON_ERROR); + echo "bad"; +} catch (Throwable) { + echo "encode:"; +} +echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +return defined("JSON_THROW_ON_ERROR");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "decode:encode:0:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. #[test] fn execute_program_dispatches_json_validate_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3cf195e6b3..202cef7a8e 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, and `JSON_INVALID_UTF8_SUBSTITUTE`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set and applying the malformed-UTF-8 ignore/substitute modes when those decode flags are present, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. JSON objects are represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, schedules eval `JsonException` objects through the normal Throwable channel when the throw flag is active, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, applying the malformed-UTF-8 ignore/substitute modes when those decode flags are present, and raising `JsonException` through the eval Throwable channel for parse failures when `JSON_THROW_ON_ERROR` is active, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. JSON objects are represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 2751ad61ca..db194a951d 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, and `JSON_INVALID_UTF8_SUBSTITUTE` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, drops malformed raw UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, and substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`; `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, raises eval `JsonException` objects for non-finite floats and malformed UTF-8 under `JSON_THROW_ON_ERROR` unless partial-output keeps the substituted result, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, drops malformed raw UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and raises `JsonException` through `JSON_THROW_ON_ERROR` for syntax, depth, UTF-8, and UTF-16 parse failures; `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 4e2958fd65..65ca4c98ed 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -21,6 +21,27 @@ use crate::parser::ast::Visibility; use crate::types::{ClassInfo, FunctionSig, PhpType}; const MAX_EVAL_CONSTRUCTOR_ARGS: usize = 2; +const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ + "Error", + "TypeError", + "ValueError", + "Exception", + "LogicException", + "BadFunctionCallException", + "BadMethodCallException", + "DomainException", + "InvalidArgumentException", + "LengthException", + "OutOfRangeException", + "RuntimeException", + "OutOfBoundsException", + "OverflowException", + "RangeException", + "UnderflowException", + "UnexpectedValueException", + "JsonException", + "FiberError", +]; /// Constructor metadata needed by the eval constructor bridge. #[derive(Clone)] @@ -38,7 +59,8 @@ pub(super) fn emit_eval_constructor_helpers(module: &Module, emitter: &mut Emitt return; } let slots = collect_eval_constructor_slots(module); - emit_constructor_helper(module, emitter, &slots); + let builtin_throwable_class_ids = collect_builtin_throwable_constructor_class_ids(module); + emit_constructor_helper(module, emitter, &slots, &builtin_throwable_class_ids); } /// Returns true when the EIR module contains a function that can call eval. @@ -84,6 +106,18 @@ fn collect_eval_constructor_slots(module: &Module) -> Vec { slots } +/// Collects compact builtin Throwable class ids that eval can initialize directly. +fn collect_builtin_throwable_constructor_class_ids(module: &Module) -> Vec { + let mut class_ids = BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES + .iter() + .filter_map(|class_name| module.class_infos.get(*class_name)) + .map(|class_info| class_info.class_id) + .collect::>(); + class_ids.sort_unstable(); + class_ids.dedup(); + class_ids +} + /// Adds one constructor slot for a class when the constructor has emitted code. fn collect_class_constructor_slot( class_name: &str, @@ -154,13 +188,16 @@ fn emit_constructor_helper( module: &Module, emitter: &mut Emitter, slots: &[EvalConstructorSlot], + builtin_throwable_class_ids: &[u64], ) { emitter.blank(); emitter.comment("--- eval bridge: user constructor call ---"); label_c_global(module, emitter, "__elephc_eval_value_construct_object"); match module.target.arch { - Arch::AArch64 => emit_constructor_aarch64(module, emitter, slots), - Arch::X86_64 => emit_constructor_x86_64(module, emitter, slots), + Arch::AArch64 => { + emit_constructor_aarch64(module, emitter, slots, builtin_throwable_class_ids) + } + Arch::X86_64 => emit_constructor_x86_64(module, emitter, slots, builtin_throwable_class_ids), } } @@ -169,6 +206,7 @@ fn emit_constructor_aarch64( module: &Module, emitter: &mut Emitter, slots: &[EvalConstructorSlot], + builtin_throwable_class_ids: &[u64], ) { let success_label = "__elephc_eval_value_construct_success"; let fail_label = "__elephc_eval_value_construct_fail"; @@ -182,6 +220,13 @@ fn emit_constructor_aarch64( emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object emitter.instruction(&format!("b.ne {}", success_label)); // non-object values have no constructor to run emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for constructor calls + emit_aarch64_builtin_throwable_constructor_dispatch( + module, + emitter, + builtin_throwable_class_ids, + fail_label, + success_label, + ); emit_aarch64_constructor_dispatch(module, emitter, slots, fail_label, success_label); emitter.instruction(&format!("b {}", success_label)); // no constructor metadata matched this class id emitter.label(fail_label); @@ -200,6 +245,7 @@ fn emit_constructor_x86_64( module: &Module, emitter: &mut Emitter, slots: &[EvalConstructorSlot], + builtin_throwable_class_ids: &[u64], ) { let success_label = "__elephc_eval_value_construct_success_x"; let fail_label = "__elephc_eval_value_construct_fail_x"; @@ -215,6 +261,13 @@ fn emit_constructor_x86_64( emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object emitter.instruction(&format!("jne {}", success_label)); // non-object values have no constructor to run emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for constructor calls + emit_x86_64_builtin_throwable_constructor_dispatch( + module, + emitter, + builtin_throwable_class_ids, + fail_label, + success_label, + ); emit_x86_64_constructor_dispatch(module, emitter, slots, fail_label, success_label); emitter.instruction(&format!("jmp {}", success_label)); // no constructor metadata matched this class id emitter.label(fail_label); @@ -228,6 +281,144 @@ fn emit_constructor_x86_64( emitter.instruction("ret"); // return the constructor status flag to Rust } +/// Emits ARM64 dispatch for compact builtin Throwable constructors. +fn emit_aarch64_builtin_throwable_constructor_dispatch( + module: &Module, + emitter: &mut Emitter, + class_ids: &[u64], + fail_label: &str, + success_label: &str, +) { + for class_id in class_ids { + let next_label = format!("__elephc_eval_builtin_throwable_next_{}", class_id); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer before this builtin class test + emitter.instruction("ldr x9, [x9]"); // load the receiver class id for builtin constructor dispatch + abi::emit_load_int_immediate(emitter, "x10", *class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this builtin Throwable class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next builtin Throwable class when ids differ + emit_aarch64_builtin_throwable_constructor_body(module, emitter, fail_label, success_label); + emitter.label(&next_label); + } +} + +/// Emits x86_64 dispatch for compact builtin Throwable constructors. +fn emit_x86_64_builtin_throwable_constructor_dispatch( + module: &Module, + emitter: &mut Emitter, + class_ids: &[u64], + fail_label: &str, + success_label: &str, +) { + for class_id in class_ids { + let next_label = format!("__elephc_eval_builtin_throwable_next_{}_x", class_id); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer before this builtin class test + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the receiver class id for builtin constructor dispatch + abi::emit_load_int_immediate(emitter, "r10", *class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this builtin Throwable class + emitter.instruction(&format!("jne {}", next_label)); // try the next builtin Throwable class when ids differ + emit_x86_64_builtin_throwable_constructor_body(module, emitter, fail_label, success_label); + emitter.label(&next_label); + } +} + +/// Initializes the compact Throwable payload for eval-created ARM64 builtin exceptions. +fn emit_aarch64_builtin_throwable_constructor_body( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, + success_label: &str, +) { + emit_aarch64_validate_builtin_throwable_arg_count(module, emitter, fail_label); + emit_aarch64_default_builtin_throwable_fields(emitter); + emitter.instruction("ldr x9, [sp, #32]"); // reload constructor argc before testing the message argument + emitter.instruction("cmp x9, #0"); // did the eval call pass a message argument? + emitter.instruction(&format!("b.eq {}", success_label)); // keep the empty Throwable defaults when no message was supplied + emit_aarch64_load_eval_arg(module, emitter, 0); + emit_aarch64_cast_eval_arg(emitter, &PhpType::Str); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for message initialization + emitter.instruction("str x1, [x9, #8]"); // store the message pointer in the compact Throwable payload + emitter.instruction("str x2, [x9, #16]"); // store the message length in the compact Throwable payload + emitter.instruction("ldr x9, [sp, #32]"); // reload constructor argc before testing the code argument + emitter.instruction("cmp x9, #1"); // did the eval call pass a code argument? + emitter.instruction(&format!("b.le {}", success_label)); // keep code zero when only the message was supplied + emit_aarch64_load_eval_arg(module, emitter, 1); + emit_aarch64_cast_eval_arg(emitter, &PhpType::Int); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for code initialization + emitter.instruction("str x0, [x9, #24]"); // store the integer exception code + emitter.instruction(&format!("b {}", success_label)); // builtin Throwable construction completed +} + +/// Initializes the compact Throwable payload for eval-created x86_64 builtin exceptions. +fn emit_x86_64_builtin_throwable_constructor_body( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, + success_label: &str, +) { + emit_x86_64_validate_builtin_throwable_arg_count(module, emitter, fail_label); + emit_x86_64_default_builtin_throwable_fields(emitter); + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload constructor argc before testing the message argument + emitter.instruction("cmp r11, 0"); // did the eval call pass a message argument? + emitter.instruction(&format!("je {}", success_label)); // keep the empty Throwable defaults when no message was supplied + emit_x86_64_load_eval_arg(module, emitter, 0); + emit_x86_64_cast_eval_arg(emitter, &PhpType::Str); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for message initialization + emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store the message pointer in the compact Throwable payload + emitter.instruction("mov QWORD PTR [r11 + 16], rdx"); // store the message length in the compact Throwable payload + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload constructor argc before testing the code argument + emitter.instruction("cmp r11, 1"); // did the eval call pass a code argument? + emitter.instruction(&format!("jle {}", success_label)); // keep code zero when only the message was supplied + emit_x86_64_load_eval_arg(module, emitter, 1); + emit_x86_64_cast_eval_arg(emitter, &PhpType::Int); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for code initialization + emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store the integer exception code + emitter.instruction(&format!("jmp {}", success_label)); // builtin Throwable construction completed +} + +/// Emits ARM64 arity validation for compact builtin Throwable constructors. +fn emit_aarch64_validate_builtin_throwable_arg_count( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for builtin Throwable arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("str x0, [sp, #32]"); // save constructor argc for message/code initialization + emitter.instruction("cmp x0, #2"); // compact Throwable initialization supports message and code arguments + emitter.instruction(&format!("b.gt {}", fail_label)); // reject unsupported previous-Throwable arguments from eval +} + +/// Emits x86_64 arity validation for compact builtin Throwable constructors. +fn emit_x86_64_validate_builtin_throwable_arg_count( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for builtin Throwable arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save constructor argc for message/code initialization + emitter.instruction("cmp rax, 2"); // compact Throwable initialization supports message and code arguments + emitter.instruction(&format!("jg {}", fail_label)); // reject unsupported previous-Throwable arguments from eval +} + +/// Writes ARM64 empty-message and zero-code defaults into the compact Throwable payload. +fn emit_aarch64_default_builtin_throwable_fields(emitter: &mut Emitter) { + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for default initialization + emitter.instruction("str xzr, [x9, #8]"); // default the message pointer to an empty string payload + emitter.instruction("str xzr, [x9, #16]"); // default the message length to zero + emitter.instruction("str xzr, [x9, #24]"); // default the exception code to zero +} + +/// Writes x86_64 empty-message and zero-code defaults into the compact Throwable payload. +fn emit_x86_64_default_builtin_throwable_fields(emitter: &mut Emitter) { + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for default initialization + emitter.instruction("mov QWORD PTR [r11 + 8], 0"); // default the message pointer to an empty string payload + emitter.instruction("mov QWORD PTR [r11 + 16], 0"); // default the message length to zero + emitter.instruction("mov QWORD PTR [r11 + 24], 0"); // default the exception code to zero +} + /// Emits ARM64 class-id dispatch for supported constructor bodies. fn emit_aarch64_constructor_dispatch( module: &Module, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 20d1b9c09b..f03a2ec022 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -936,6 +936,39 @@ echo function_exists("json_last_error") && function_exists("json_last_error_msg" ); } +/// Verifies eval JSON throw flags raise catchable `JsonException` objects. +#[test] +fn test_eval_dispatches_json_throw_on_error() { + let out = compile_and_run( + r#"getCode() . ":" . (str_contains($e->getMessage(), "Syntax error") ? "syntax" : "bad") . ":"; +} +try { + eval('json_encode(INF, JSON_THROW_ON_ERROR);'); + echo "bad"; +} catch (Throwable $e) { + echo "encode:" . get_class($e) . ":" . $e->getCode() . ":" . $e->getMessage() . ":"; +} +eval('echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":";'); +eval('$json = chr(34) . "a" . chr(128) . "b" . chr(34); echo json_decode($json, true, 512, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_IGNORE) . ":";'); +"#, + ); + assert_eq!( + out, + "inner:outer:JsonException:4:syntax:encode:JsonException:7:Inf and NaN cannot be JSON encoded:0:ab:" + ); +} + /// Verifies eval `json_validate()` validates JSON syntax, depth, and dynamic calls. #[test] fn test_eval_dispatches_json_validate_builtin_call() { From ab330828acf8756e41beae3932a2f9eb80726d4f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 23:01:21 +0200 Subject: [PATCH 0256/1208] Return stdClass from eval JSON decode --- crates/elephc-eval/src/interpreter.rs | 56 +++++++++++-- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen/eval_property_helpers.rs | 91 ++++++++++++++++++++-- src/codegen/mod.rs | 8 ++ src/codegen_support/runtime/eval_bridge.rs | 52 +++++++++++++ tests/codegen/eval.rs | 20 +++++ 7 files changed, 218 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 542db2ef60..47a450fae5 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -13458,9 +13458,10 @@ fn eval_json_decode_result( if flags & !supported_flags != 0 { return Err(EvalStatus::UnsupportedConstruct); } - if let Some(associative) = associative { - let _ = values.truthy(associative)?; - } + let objects_as_assoc = associative + .map(|associative| values.truthy(associative)) + .transpose()? + .unwrap_or(false); let depth = depth .map(|depth| eval_int_value(depth, values)) .transpose()? @@ -13489,13 +13490,14 @@ fn eval_json_decode_result( } }; context.clear_json_error(); - eval_json_decode_to_cell(decoded, flags, values) + eval_json_decode_to_cell(decoded, flags, objects_as_assoc, values) } /// Materializes one parsed JSON value as an eval runtime cell. fn eval_json_decode_to_cell( value: JsonValue, flags: i64, + objects_as_assoc: bool, values: &mut impl RuntimeValueOps, ) -> Result { match value { @@ -13508,16 +13510,19 @@ fn eval_json_decode_to_cell( for (index, element) in elements.into_iter().enumerate() { let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; let key = values.int(index)?; - let element = eval_json_decode_to_cell(element, flags, values)?; + let element = eval_json_decode_to_cell(element, flags, objects_as_assoc, values)?; result = values.array_set(result, key, element)?; } Ok(result) } JsonValue::Object(entries) => { + if !objects_as_assoc { + return eval_json_decode_object_to_cell(entries, flags, values); + } let mut result = values.assoc_new(entries.len())?; for (key, value) in entries { let key = values.string_bytes_value(&key)?; - let value = eval_json_decode_to_cell(value, flags, values)?; + let value = eval_json_decode_to_cell(value, flags, objects_as_assoc, values)?; result = values.array_set(result, key, value)?; } Ok(result) @@ -13525,6 +13530,21 @@ fn eval_json_decode_to_cell( } } +/// Materializes a parsed JSON object as a `stdClass` runtime object. +fn eval_json_decode_object_to_cell( + entries: Vec<(Vec, JsonValue)>, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + for (key, value) in entries { + let key = std::str::from_utf8(&key).map_err(|_| EvalStatus::RuntimeFatal)?; + let value = eval_json_decode_to_cell(value, flags, false, values)?; + values.property_set(object, key, value)?; + } + Ok(object) +} + /// Materializes one JSON number as an int when possible and as a float otherwise. fn eval_json_decode_number_to_cell( value: &[u8], @@ -18050,6 +18070,30 @@ return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING") && def assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. + #[test] + fn execute_program_dispatches_json_decode_stdclass_default() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +echo $object->a . ":" . $object->b->c . ":"; +$objectFalse = json_decode("{\"z\":2}", false); +echo $objectFalse->z . ":"; +$objectNull = json_decode("{\"n\":{\"m\":3}}", null); +echo $objectNull->n->m . ":"; +$assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); +echo $assoc["b"]["c"] . ":"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:x:2:3:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. #[test] fn execute_program_dispatches_json_last_error_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 202cef7a8e..3ce4f21c7c 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, schedules eval `JsonException` objects through the normal Throwable channel when the throw flag is active, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, applying the malformed-UTF-8 ignore/substitute modes when those decode flags are present, and raising `JsonException` through the eval Throwable channel for parse failures when `JSON_THROW_ON_ERROR` is active, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. JSON objects are represented as associative arrays until eval stdClass materialization is bridged. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, schedules eval `JsonException` objects through the normal Throwable channel when the throw flag is active, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, applying the malformed-UTF-8 ignore/substitute modes when those decode flags are present, creating `stdClass` objects for JSON objects unless `$associative` is true, and raising `JsonException` through the eval Throwable channel for parse failures when `JSON_THROW_ON_ERROR` is active, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index db194a951d..d7493abf51 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, raises eval `JsonException` objects for non-finite floats and malformed UTF-8 under `JSON_THROW_ON_ERROR` unless partial-output keeps the substituted result, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, drops malformed raw UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and raises `JsonException` through `JSON_THROW_ON_ERROR` for syntax, depth, UTF-8, and UTF-16 parse failures; `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_decode()` materializes JSON objects as associative arrays even when `$associative` is false or null, because eval stdClass materialization is not bridged yet. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, raises eval `JsonException` objects for non-finite floats and malformed UTF-8 under `JSON_THROW_ON_ERROR` unless partial-output keeps the substituted result, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, drops malformed raw UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, returns `stdClass` objects by default or when `$associative` is false/null, returns associative arrays when `$associative` is true, and raises `JsonException` through `JSON_THROW_ON_ERROR` for syntax, depth, UTF-8, and UTF-16 parse failures; `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index 669542e312..552451f6e9 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -181,6 +181,7 @@ fn emit_property_get_aarch64( emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x0, [sp, #24]"); // save the boxed receiver for stdClass fallback reads emitter.instruction(&format!("cbz x0, {}", null_label)); // null Mixed receiver reads as PHP null emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object @@ -188,7 +189,8 @@ fn emit_property_get_aarch64( emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property loads emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id emit_aarch64_property_dispatch(module, emitter, data, slots, "get"); - emitter.instruction(&format!("b {}", null_label)); // no declared public property matched the request + emit_aarch64_stdclass_property_get_fallback(emitter); + emitter.instruction(&format!("b {}", done_label)); // return after stdClass fallback get or null result emit_aarch64_get_slot_bodies(module, emitter, slots, done_label); emitter.label(null_label); let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); @@ -213,6 +215,7 @@ fn emit_property_get_x86_64( emitter.instruction("sub rsp, 32"); // reserve aligned slots for name, length, and object emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the boxed receiver for stdClass fallback reads emitter.instruction(&format!("test rdi, rdi")); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", null_label)); // null Mixed receiver reads as PHP null emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register @@ -222,7 +225,8 @@ fn emit_property_get_x86_64( emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property loads emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id emit_x86_64_property_dispatch(module, emitter, data, slots, "get"); - emitter.instruction(&format!("jmp {}", null_label)); // no declared public property matched the request + emit_x86_64_stdclass_property_get_fallback(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // return after stdClass fallback get or null result emit_x86_64_get_slot_bodies(module, emitter, slots, done_label); emitter.label(null_label); let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); @@ -248,6 +252,7 @@ fn emit_property_set_aarch64( emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length emitter.instruction("str x3, [sp, #24]"); // save the boxed value being assigned + emitter.instruction("str x0, [sp, #32]"); // save the boxed receiver for stdClass fallback writes emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot accept a property write emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object @@ -255,7 +260,7 @@ fn emit_property_set_aarch64( emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property stores emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id emit_aarch64_property_dispatch(module, emitter, data, slots, "set"); - emitter.instruction(&format!("b {}", fail_label)); // no declared public property matched the request + emit_aarch64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); emit_aarch64_set_slot_bodies(module, emitter, slots, done_label); emitter.label(fail_label); emitter.instruction("mov x0, #0"); // report a failed eval property write to Rust @@ -277,10 +282,11 @@ fn emit_property_set_x86_64( let done_label = "__elephc_eval_value_property_set_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // reserve aligned slots for name, length, object, and value + emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, length, object, and value emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed value being assigned + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the boxed receiver for stdClass fallback writes emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot accept a property write emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register @@ -290,7 +296,7 @@ fn emit_property_set_x86_64( emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property stores emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id emit_x86_64_property_dispatch(module, emitter, data, slots, "set"); - emitter.instruction(&format!("jmp {}", fail_label)); // no declared public property matched the request + emit_x86_64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); emit_x86_64_set_slot_bodies(module, emitter, slots, done_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // report a failed eval property write to Rust @@ -301,6 +307,81 @@ fn emit_property_set_x86_64( emitter.instruction("ret"); // return the write-status flag to Rust } +/// Emits an ARM64 fallback read for stdClass dynamic properties. +fn emit_aarch64_stdclass_property_get_fallback(emitter: &mut Emitter) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed receiver for the Mixed stdClass getter + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + emitter.instruction("bl __rt_mixed_property_get"); // read stdClass dynamic property or return Mixed(null) +} + +/// Emits an x86_64 fallback read for stdClass dynamic properties. +fn emit_x86_64_stdclass_property_get_fallback(emitter: &mut Emitter) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed receiver for the Mixed stdClass getter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload requested property-name length + emitter.instruction("call __rt_mixed_property_get"); // read stdClass dynamic property or return Mixed(null) +} + +/// Emits an ARM64 fallback write for stdClass dynamic properties. +fn emit_aarch64_stdclass_property_set_fallback( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, + done_label: &str, +) { + let Some(class_id) = stdclass_class_id(module) else { + emitter.instruction(&format!("b {}", fail_label)); // reject writes when stdClass metadata is unavailable + return; + }; + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for stdClass class check + emitter.instruction("ldr x9, [x9]"); // load the object's runtime class id + abi::emit_load_int_immediate(emitter, "x10", class_id as i64); + emitter.instruction("cmp x9, x10"); // check whether the receiver is stdClass + emitter.instruction(&format!("b.ne {}", fail_label)); // non-stdClass misses remain unsupported eval writes + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed receiver for the Mixed stdClass setter + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + emitter.instruction("ldr x3, [sp, #24]"); // reload the boxed value being assigned + emitter.instruction("bl __rt_mixed_property_set"); // write the stdClass dynamic property + emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after stdClass write +} + +/// Emits an x86_64 fallback write for stdClass dynamic properties. +fn emit_x86_64_stdclass_property_set_fallback( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, + done_label: &str, +) { + let Some(class_id) = stdclass_class_id(module) else { + emitter.instruction(&format!("jmp {}", fail_label)); // reject writes when stdClass metadata is unavailable + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for stdClass class check + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the object's runtime class id + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // check whether the receiver is stdClass + emitter.instruction(&format!("jne {}", fail_label)); // non-stdClass misses remain unsupported eval writes + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed receiver for the Mixed stdClass setter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload requested property-name length + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the boxed value being assigned + emitter.instruction("call __rt_mixed_property_set"); // write the stdClass dynamic property + emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after stdClass write +} + +/// Returns the runtime class id for builtin `stdClass` in this module. +fn stdclass_class_id(module: &Module) -> Option { + module + .class_infos + .iter() + .find(|(class_name, _)| crate::types::checker::builtin_stdclass::is_stdclass(class_name)) + .map(|(_, class_info)| class_info.class_id) +} + /// Emits ARM64 class-id and property-name dispatch for helper slot bodies. fn emit_aarch64_property_dispatch( module: &Module, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index f6a4176449..2f2fa6b27a 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -516,6 +516,7 @@ fn runtime_referenced_class_names(module: &Module) -> HashSet { } } seed_runtime_throwable_class_names(module, &mut names); + seed_runtime_stdclass_name(module, &mut names); seed_builtin_reflection_class_names(module, &mut names); expand_class_dependencies(&mut names, &module.class_infos); names @@ -546,6 +547,13 @@ fn seed_runtime_throwable_class_names(module: &Module, names: &mut HashSet) { + if module.class_infos.contains_key("stdClass") { + names.insert("stdClass".to_string()); + } +} + /// Adds builtin reflection classes whose objects can be materialized by metadata helpers. fn seed_builtin_reflection_class_names(module: &Module, names: &mut HashSet) { for class_name in [ diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 4a2003134b..be8bc1c96a 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -45,10 +45,40 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame across dynamic object lookup emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across runtime calls emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("cmp x1, #8"); // stdClass has an 8-byte class name + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // use the generic factory for non-stdClass lengths + emitter.instruction("ldrb w9, [x0]"); // load candidate byte 0 for stdClass comparison + emitter.instruction("cmp w9, #115"); // byte 0 must be 's' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 0 differs + emitter.instruction("ldrb w9, [x0, #1]"); // load candidate byte 1 for stdClass comparison + emitter.instruction("cmp w9, #116"); // byte 1 must be 't' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 1 differs + emitter.instruction("ldrb w9, [x0, #2]"); // load candidate byte 2 for stdClass comparison + emitter.instruction("cmp w9, #100"); // byte 2 must be 'd' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 2 differs + emitter.instruction("ldrb w9, [x0, #3]"); // load candidate byte 3 for stdClass comparison + emitter.instruction("cmp w9, #67"); // byte 3 must be 'C' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 3 differs + emitter.instruction("ldrb w9, [x0, #4]"); // load candidate byte 4 for stdClass comparison + emitter.instruction("cmp w9, #108"); // byte 4 must be 'l' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 4 differs + emitter.instruction("ldrb w9, [x0, #5]"); // load candidate byte 5 for stdClass comparison + emitter.instruction("cmp w9, #97"); // byte 5 must be 'a' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 5 differs + emitter.instruction("ldrb w9, [x0, #6]"); // load candidate byte 6 for stdClass comparison + emitter.instruction("cmp w9, #115"); // byte 6 must be 's' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 6 differs + emitter.instruction("ldrb w9, [x0, #7]"); // load candidate byte 7 for stdClass comparison + emitter.instruction("cmp w9, #115"); // byte 7 must be 's' + emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // fall back when byte 7 differs + emitter.instruction("bl __rt_stdclass_new"); // allocate stdClass with its dynamic-property hash + emitter.instruction("b __elephc_eval_value_new_object_box"); // box the stdClass object for Rust + emitter.label("__elephc_eval_value_new_object_generic"); emitter.instruction("mov x2, x1"); // move the C class-name length into new_by_name's string ABI emitter.instruction("mov x1, x0"); // move the C class-name pointer into new_by_name's string ABI emitter.instruction("bl __rt_new_by_name"); // allocate the named AOT class object, or return null on miss emitter.instruction("cbz x0, __elephc_eval_value_new_object_null"); // box PHP null when no runtime class matched the eval name + emitter.label("__elephc_eval_value_new_object_box"); emitter.instruction("mov x1, x0"); // move the allocated object pointer into the Mixed payload emitter.instruction("mov x0, #6"); // runtime tag 6 = object emitter.instruction("mov x2, xzr"); // object payloads do not use a high word @@ -1105,11 +1135,33 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_new_object"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls emitter.instruction("mov rbp, rsp"); // establish a stable dynamic-object wrapper frame + emitter.instruction("cmp rsi, 8"); // stdClass has an 8-byte class name + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // use the generic factory for non-stdClass lengths + emitter.instruction("cmp BYTE PTR [rdi], 115"); // byte 0 must be 's' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 0 differs + emitter.instruction("cmp BYTE PTR [rdi + 1], 116"); // byte 1 must be 't' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 1 differs + emitter.instruction("cmp BYTE PTR [rdi + 2], 100"); // byte 2 must be 'd' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 2 differs + emitter.instruction("cmp BYTE PTR [rdi + 3], 67"); // byte 3 must be 'C' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 3 differs + emitter.instruction("cmp BYTE PTR [rdi + 4], 108"); // byte 4 must be 'l' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 4 differs + emitter.instruction("cmp BYTE PTR [rdi + 5], 97"); // byte 5 must be 'a' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 5 differs + emitter.instruction("cmp BYTE PTR [rdi + 6], 115"); // byte 6 must be 's' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 6 differs + emitter.instruction("cmp BYTE PTR [rdi + 7], 115"); // byte 7 must be 's' + emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // fall back when byte 7 differs + emitter.instruction("call __rt_stdclass_new"); // allocate stdClass with its dynamic-property hash + emitter.instruction("jmp __elephc_eval_value_new_object_box_x86"); // box the stdClass object for Rust + emitter.label("__elephc_eval_value_new_object_generic_x86"); emitter.instruction("mov rax, rdi"); // move the C class-name pointer into new_by_name's string ABI emitter.instruction("mov rdx, rsi"); // move the C class-name length into new_by_name's string ABI emitter.instruction("call __rt_new_by_name"); // allocate the named AOT class object, or return null on miss emitter.instruction("test rax, rax"); // did the runtime class-name lookup allocate an object? emitter.instruction("jz __elephc_eval_value_new_object_null_x86"); // box PHP null when no runtime class matched the eval name + emitter.label("__elephc_eval_value_new_object_box_x86"); emitter.instruction("mov rdi, rax"); // move the allocated object pointer into the Mixed payload emitter.instruction("mov eax, 6"); // runtime tag 6 = object emitter.instruction("xor esi, esi"); // object payloads do not use a high word diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f03a2ec022..71f515bd2d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -907,6 +907,26 @@ echo function_exists("json_decode");'); ); } +/// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. +#[test] +fn test_eval_dispatches_json_decode_stdclass_default() { + let out = compile_and_run( + r#"a . ":" . $object->b->c . ":"; +$objectFalse = json_decode("{\"z\":2}", false); +echo $objectFalse->z . ":"; +$objectNull = json_decode("{\"n\":{\"m\":3}}", null); +echo $objectNull->n->m . ":"; +$assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); +echo $assoc["b"]["c"] . ":";'); +$object = eval('return json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}");'); +echo gettype($object) . ":" . $object->a . ":" . $object->b->c; +"#, + ); + assert_eq!(out, "1:x:2:3:y:object:1:x"); +} + /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. #[test] fn test_eval_dispatches_json_last_error_builtin_calls() { From 6827e2694e7f26deffe1b7cf8549d6fc4732bf37 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 16 Jun 2026 23:20:18 +0200 Subject: [PATCH 0257/1208] Support eval JSON encoding stdClass --- crates/elephc-eval/src/interpreter.rs | 200 ++++++++++++++++++--- crates/elephc-eval/src/runtime_hooks.rs | 25 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen_support/runtime/eval_bridge.rs | 172 +++++++++++++++++- tests/codegen/eval.rs | 20 +++ 6 files changed, 386 insertions(+), 35 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 47a450fae5..6ec4149bb0 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -355,6 +355,16 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result<(), EvalStatus>; + /// Returns the number of public JSON-visible properties on a runtime object. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result; + + /// Returns the public property key at a zero-based JSON object iteration position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result; + /// Calls a named method on a runtime object held in a boxed Mixed cell. fn method_call( &mut self, @@ -13810,7 +13820,19 @@ fn eval_json_encode_append( output, )?; } - EVAL_TAG_NULL | EVAL_TAG_OBJECT | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), + EVAL_TAG_OBJECT => { + eval_json_encode_append_object( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_NULL | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), _ => return Err(EvalStatus::UnsupportedConstruct), } Ok(()) @@ -13977,6 +13999,66 @@ fn eval_json_encode_append_assoc( Ok(()) } +/// Appends one eval runtime object as a JSON object. +fn eval_json_encode_append_object( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(b'{'); + let len = values.object_property_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.object_property_iter_key(value, position)?; + let key_bytes = values.string_bytes(key)?; + eval_json_encode_append_string( + &key_bytes, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + let property = std::str::from_utf8(&key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let element = values.property_get(value, property)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(b'}'); + arrays_seen.pop(); + Ok(()) +} + /// Appends a JSON object colon, including pretty-print spacing when active. fn eval_json_encode_append_colon(flags: i64, output: &mut Vec) { if flags & EVAL_JSON_PRETTY_PRINT != 0 { @@ -15018,7 +15100,7 @@ mod tests { Bytes(Vec), Array(Vec), Assoc(Vec<(FakeKey, RuntimeCellHandle)>), - Object(HashMap), + Object(Vec<(String, RuntimeCellHandle)>), Iterator { len: i64, position: i64 }, Resource(i64), } @@ -15074,6 +15156,16 @@ mod tests { FakeKey::String(value) => self.string(value), } } + + /// Finds a fake object property by insertion-order name. + fn object_property( + properties: &[(String, RuntimeCellHandle)], + name: &str, + ) -> Option { + properties + .iter() + .find_map(|(property, value)| (property == name).then_some(*value)) + } } impl RuntimeValueOps for FakeOps { @@ -15198,8 +15290,8 @@ mod tests { ) -> Result { match self.get(object) { FakeValue::Object(properties) => properties - .get(property) - .copied() + .iter() + .find_map(|(name, value)| (name == property).then_some(*value)) .map_or_else(|| self.null(), Ok), _ => Err(EvalStatus::UnsupportedConstruct), } @@ -15216,10 +15308,45 @@ mod tests { let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { return Err(EvalStatus::UnsupportedConstruct); }; - properties.insert(property.to_string(), value); + if let Some((_, existing_value)) = + properties.iter_mut().find(|(name, _)| name == property) + { + *existing_value = value; + } else { + properties.push((property.to_string(), value)); + } Ok(()) } + /// Returns the number of fake object properties in insertion order. + fn object_property_len( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => Ok(properties.len()), + FakeValue::Iterator { .. } => Ok(0), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Returns one fake object property key by insertion-order position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + let Some((name, _)) = properties.get(position) else { + return self.null(); + }; + self.string(name) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Calls one fake object method by name. fn method_call( &mut self, @@ -15254,15 +15381,13 @@ mod tests { if !args.is_empty() { return Err(EvalStatus::UnsupportedConstruct); } - properties.get("x").copied().map_or_else(|| self.null(), Ok) + Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) } (FakeValue::Object(properties), "add_x") => { let [arg] = args.as_slice() else { return Err(EvalStatus::UnsupportedConstruct); }; - let x = properties - .get("x") - .copied() + let x = Self::object_property(&properties, "x") .ok_or(EvalStatus::RuntimeFatal)?; let FakeValue::Int(x) = self.get(x) else { return Err(EvalStatus::UnsupportedConstruct); @@ -15276,9 +15401,7 @@ mod tests { let [left, right] = args.as_slice() else { return Err(EvalStatus::UnsupportedConstruct); }; - let x = properties - .get("x") - .copied() + let x = Self::object_property(&properties, "x") .ok_or(EvalStatus::RuntimeFatal)?; let FakeValue::Int(x) = self.get(x) else { return Err(EvalStatus::UnsupportedConstruct); @@ -15297,7 +15420,7 @@ mod tests { /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { - Ok(self.alloc(FakeValue::Object(HashMap::new()))) + Ok(self.alloc(FakeValue::Object(Vec::new()))) } /// Applies fake constructor side effects for eval `new` unit tests. @@ -15311,7 +15434,11 @@ mod tests { return Err(EvalStatus::UnsupportedConstruct); }; if let Some(first) = args.first().copied() { - properties.insert("x".to_string(), first); + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { + *value = first; + } else { + properties.push(("x".to_string(), first)); + } } Ok(()) } @@ -16502,8 +16629,7 @@ return function_exists("var_dump");"#, let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let x = values.int(1).expect("create fake int"); - let mut properties = HashMap::new(); - properties.insert("x".to_string(), x); + let properties = vec![("x".to_string(), x)]; let object = values.alloc(FakeValue::Object(properties)); scope.set("this", object, ScopeCellOwnership::Borrowed); @@ -16525,7 +16651,7 @@ return function_exists("var_dump");"#, let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(HashMap::new())); + let object = values.alloc(FakeValue::Object(Vec::new())); scope.set("this", object, ScopeCellOwnership::Borrowed); let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); @@ -16540,8 +16666,7 @@ return function_exists("var_dump");"#, let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let x = values.int(7).expect("create fake int"); - let mut properties = HashMap::new(); - properties.insert("x".to_string(), x); + let properties = vec![("x".to_string(), x)]; let object = values.alloc(FakeValue::Object(properties)); scope.set("this", object, ScopeCellOwnership::Borrowed); @@ -16558,8 +16683,7 @@ return function_exists("var_dump");"#, let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let x = values.int(7).expect("create fake int"); - let mut properties = HashMap::new(); - properties.insert("x".to_string(), x); + let properties = vec![("x".to_string(), x)]; let object = values.alloc(FakeValue::Object(properties)); scope.set("this", object, ScopeCellOwnership::Borrowed); @@ -16576,8 +16700,7 @@ return function_exists("var_dump");"#, let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let x = values.int(7).expect("create fake int"); - let mut properties = HashMap::new(); - properties.insert("x".to_string(), x); + let properties = vec![("x".to_string(), x)]; let object = values.alloc(FakeValue::Object(properties)); scope.set("this", object, ScopeCellOwnership::Borrowed); @@ -16595,7 +16718,7 @@ return function_exists("var_dump");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Object(HashMap::new())); + assert_eq!(values.get(result), FakeValue::Object(Vec::new())); } /// Verifies eval object construction passes constructor arguments through runtime hooks. @@ -16609,8 +16732,9 @@ return function_exists("var_dump");"#, let FakeValue::Object(properties) = values.get(result) else { panic!("expected fake object"); }; + let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); - assert_eq!(values.get(properties["x"]), FakeValue::Int(1)); + assert_eq!(values.get(x), FakeValue::Int(1)); } /// Verifies if/else executes only the PHP-truthy branch. @@ -18094,6 +18218,32 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `json_encode()` serializes stdClass dynamic properties. + #[test] + fn execute_program_dispatches_json_encode_stdclass_object() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +echo json_encode($object) . ":"; +echo str_replace("\n", "|", json_encode($object, JSON_PRETTY_PRINT)) . ":"; +$empty = json_decode("{}"); +echo json_encode($empty) . ":"; +$empty->a = 7; +echo json_encode($empty); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. #[test] fn execute_program_dispatches_json_last_error_builtins() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 78d9b9141e..ea81614136 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -52,6 +52,11 @@ unsafe extern "C" { name_len: u64, value: *mut RuntimeCell, ) -> u64; + fn __elephc_eval_value_object_property_len(object: *mut RuntimeCell) -> u64; + fn __elephc_eval_value_object_property_iter_key( + object: *mut RuntimeCell, + position: u64, + ) -> *mut RuntimeCell; fn __elephc_eval_value_method_call( object: *mut RuntimeCell, name_ptr: *const u8, @@ -260,6 +265,26 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } + /// Returns the JSON-visible public property count for a boxed Mixed object. + fn object_property_len( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + let len = unsafe { __elephc_eval_value_object_property_len(object.as_ptr()) }; + usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns one JSON-visible public property key for a boxed Mixed object. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_object_property_iter_key(object.as_ptr(), position as u64) + }) + } + /// Calls a boxed Mixed object method through the generated user helper. fn method_call( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 3ce4f21c7c..99d345b920 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -55,7 +55,7 @@ Eval `number_format()` formats finite numeric cells inside the interpreter, incl Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, and associative-array cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR`. It uses eval type tags plus array iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, schedules eval `JsonException` objects through the normal Throwable channel when the throw flag is active, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, applying the malformed-UTF-8 ignore/substitute modes when those decode flags are present, creating `stdClass` objects for JSON objects unless `$associative` is true, and raising `JsonException` through the eval Throwable channel for parse failures when `JSON_THROW_ON_ERROR` is active, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. +Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, associative-array, and `stdClass` dynamic-property cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR`. It uses eval type tags plus array/object iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, schedules eval `JsonException` objects through the normal Throwable channel when the throw flag is active, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, applying the malformed-UTF-8 ignore/substitute modes when those decode flags are present, creating `stdClass` objects for JSON objects unless `$associative` is true, and raising `JsonException` through the eval Throwable channel for parse failures when `JSON_THROW_ON_ERROR` is active, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index d7493abf51..c7fa3147a1 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -70,7 +70,7 @@ Eval `number_format()` supports decimal counts plus custom decimal and thousands Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR` for null, booleans, integers, floats, strings, indexed arrays, and associative arrays. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, raises eval `JsonException` objects for non-finite floats and malformed UTF-8 under `JSON_THROW_ON_ERROR` unless partial-output keeps the substituted result, formats arrays/objects with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, drops malformed raw UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, returns `stdClass` objects by default or when `$associative` is false/null, returns associative arrays when `$associative` is true, and raises `JsonException` through `JSON_THROW_ON_ERROR` for syntax, depth, UTF-8, and UTF-16 parse failures; `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. +Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR` for null, booleans, integers, floats, strings, indexed arrays, associative arrays, and `stdClass` dynamic properties. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, raises eval `JsonException` objects for non-finite floats and malformed UTF-8 under `JSON_THROW_ON_ERROR` unless partial-output keeps the substituted result, formats arrays and `stdClass` dynamic properties with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, drops malformed raw UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, returns `stdClass` objects by default or when `$associative` is false/null, returns associative arrays when `$associative` is true, and raises `JsonException` through `JSON_THROW_ON_ERROR` for syntax, depth, UTF-8, and UTF-16 parse failures; `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index be8bc1c96a..0b159fd4f1 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -16,6 +16,11 @@ use crate::codegen_support::platform::Arch; const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; +/// Builds the x86_64 instruction that installs the Mixed heap-kind marker. +fn x86_64_mixed_heap_kind_instruction() -> String { + format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5) +} + /// Emits every eval value wrapper required by `libelephc-eval`. pub(crate) fn emit_eval_bridge_runtime(emitter: &mut Emitter) { emitter.blank(); @@ -349,6 +354,82 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("ldr x0, [x9]"); // load the runtime container element count emitter.instruction("ret"); // return the element count to Rust + label_c_global(emitter, "__elephc_eval_value_object_property_len"); + emitter.instruction("cbz x0, __elephc_eval_value_object_property_len_zero"); // null handles have no JSON-visible object properties + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.ne __elephc_eval_value_object_property_len_zero"); // non-objects expose no JSON-visible properties here + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_property_len_zero"); // null object payloads have no visible properties + abi::emit_symbol_address(emitter, "x10", "_stdclass_class_id"); + emitter.instruction("ldr x10, [x10]"); // load the compile-time stdClass class id + emitter.instruction("ldr x11, [x9]"); // load the object's runtime class id + emitter.instruction("cmp x11, x10"); // check whether the object is stdClass + emitter.instruction("b.ne __elephc_eval_value_object_property_len_zero"); // non-stdClass objects expose no bridge-visible properties + emitter.instruction("ldr x9, [x9, #8]"); // load stdClass dynamic-property hash pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_property_len_zero"); // null property hashes are treated as empty objects + emitter.instruction("ldr x0, [x9]"); // load the hash entry count + emitter.instruction("ret"); // return the public property count to Rust + emitter.label("__elephc_eval_value_object_property_len_zero"); + emitter.instruction("mov x0, #0"); // report zero JSON-visible object properties + emitter.instruction("ret"); // return the empty property count to Rust + + label_c_global(emitter, "__elephc_eval_value_object_property_iter_key"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for insertion-order property iteration + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable property-iterator frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed object receiver while walking properties + emitter.instruction("str x1, [sp, #8]"); // save the requested zero-based property position + emitter.instruction("cbz x0, __elephc_eval_value_object_property_iter_key_null"); // null handles produce a null property key + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.ne __elephc_eval_value_object_property_iter_key_null"); // non-objects have no JSON-visible property key + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_property_iter_key_null"); // null object payloads produce a null key + abi::emit_symbol_address(emitter, "x10", "_stdclass_class_id"); + emitter.instruction("ldr x10, [x10]"); // load the compile-time stdClass class id + emitter.instruction("ldr x11, [x9]"); // load the object's runtime class id + emitter.instruction("cmp x11, x10"); // check whether the object is stdClass + emitter.instruction("b.ne __elephc_eval_value_object_property_iter_key_null"); // non-stdClass objects have no bridge-visible key + emitter.instruction("ldr x9, [x9, #8]"); // load stdClass dynamic-property hash pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_property_iter_key_null"); // null property hashes produce a null key + emitter.instruction("str x9, [sp, #16]"); // save the hash pointer for repeated iterator helper calls + emitter.instruction("str xzr, [sp, #24]"); // start the insertion-order property counter at zero + emitter.instruction("mov x1, xzr"); // cursor 0 starts at the property hash head entry + emitter.label("__elephc_eval_value_object_property_iter_key_loop"); + emitter.instruction("ldr x0, [sp, #16]"); // reload the hash pointer before advancing the iterator + emitter.instruction("bl __rt_hash_iter_next"); // fetch the next insertion-order property key + emitter.instruction("cmn x0, #1"); // did the iterator report the done sentinel? + emitter.instruction("b.eq __elephc_eval_value_object_property_iter_key_null"); // out-of-range positions produce a null key + emitter.instruction("ldr x10, [sp, #24]"); // load the current insertion-order property position + emitter.instruction("ldr x11, [sp, #8]"); // load the requested property position + emitter.instruction("cmp x10, x11"); // is this the requested property entry? + emitter.instruction("b.eq __elephc_eval_value_object_property_iter_key_box"); // box the current property key when the position matches + emitter.instruction("add x10, x10, #1"); // advance the insertion-order property counter + emitter.instruction("str x10, [sp, #24]"); // persist the updated property counter + emitter.instruction("mov x1, x0"); // use the returned cursor for the next iterator call + emitter.instruction("b __elephc_eval_value_object_property_iter_key_loop"); // continue walking until the requested position is reached + emitter.label("__elephc_eval_value_object_property_iter_key_box"); + emitter.instruction("cmn x2, #1"); // integer hash keys carry key_hi = -1 + emitter.instruction("b.ne __elephc_eval_value_object_property_iter_key_string"); // string property keys need string-tag boxing + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer key fallback + emitter.instruction("mov x2, xzr"); // integer keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box the integer property key as Mixed + emitter.instruction("b __elephc_eval_value_object_property_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_object_property_iter_key_string"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string property key + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the string property key as Mixed + emitter.instruction("b __elephc_eval_value_object_property_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_object_property_iter_key_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null keys do not use a low payload word + emitter.instruction("mov x2, xzr"); // null keys do not use a high payload word + emitter.instruction("bl __rt_mixed_from_value"); // box null for invalid property-key requests + emitter.label("__elephc_eval_value_object_property_iter_key_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the property-iterator wrapper frame + emitter.instruction("ret"); // return the boxed property key to Rust + emitter.label("__elephc_eval_key_normalize"); emitter.instruction("sub sp, sp, #32"); // allocate a helper frame while classifying the boxed eval key emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across runtime calls @@ -1241,10 +1322,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the owned array pointer while allocating the Mixed box emitter.instruction("mov rax, 24"); // Mixed cells store tag plus two payload words emitter.instruction("call __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array - emitter.instruction(&format!( - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); // materialize the mixed-cell heap kind with the x86_64 heap marker + emitter.instruction(&x86_64_mixed_heap_kind_instruction()); // materialize the mixed-cell heap kind with the x86_64 heap marker emitter.instruction("mov QWORD PTR [rax - 8], r10"); // install the mixed-cell heap kind in the uniform header emitter.instruction("mov QWORD PTR [rax], 4"); // runtime tag 4 = indexed array emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the owned indexed-array pointer @@ -1266,10 +1344,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the owned hash pointer while allocating the Mixed box emitter.instruction("mov rax, 24"); // Mixed cells store tag plus two payload words emitter.instruction("call __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new hash - emitter.instruction(&format!( - "mov r10, 0x{:x}", - (X86_64_HEAP_MAGIC_HI32 << 32) | 5 - )); // materialize the mixed-cell heap kind with the x86_64 heap marker + emitter.instruction(&x86_64_mixed_heap_kind_instruction()); // materialize the mixed-cell heap kind with the x86_64 heap marker emitter.instruction("mov QWORD PTR [rax - 8], r10"); // install the mixed-cell heap kind in the uniform header emitter.instruction("mov QWORD PTR [rax], 5"); // runtime tag 5 = associative array emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the owned hash pointer @@ -1442,6 +1517,87 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rax, QWORD PTR [r10]"); // load the runtime container element count emitter.instruction("ret"); // return the element count to Rust + label_c_global(emitter, "__elephc_eval_value_object_property_len"); + emitter.instruction("test rdi, rdi"); // null handles have no JSON-visible object properties + emitter.instruction("jz __elephc_eval_value_object_property_len_zero"); // report zero properties for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("jne __elephc_eval_value_object_property_len_zero"); // non-objects expose no JSON-visible properties here + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // is the object payload null? + emitter.instruction("jz __elephc_eval_value_object_property_len_zero"); // null object payloads have no visible properties + abi::emit_load_symbol_to_reg(emitter, "r11", "_stdclass_class_id", 0); + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the object's runtime class id + emitter.instruction("cmp rax, r11"); // check whether the object is stdClass + emitter.instruction("jne __elephc_eval_value_object_property_len_zero"); // non-stdClass objects expose no bridge-visible properties + emitter.instruction("mov r10, QWORD PTR [r10 + 8]"); // load stdClass dynamic-property hash pointer + emitter.instruction("test r10, r10"); // is the property hash null? + emitter.instruction("jz __elephc_eval_value_object_property_len_zero"); // null property hashes are treated as empty objects + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the hash entry count + emitter.instruction("ret"); // return the public property count to Rust + emitter.label("__elephc_eval_value_object_property_len_zero"); + emitter.instruction("xor eax, eax"); // report zero JSON-visible object properties + emitter.instruction("ret"); // return the empty property count to Rust + + label_c_global(emitter, "__elephc_eval_value_object_property_iter_key"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable property-iterator wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve slots for receiver, target position, hash pointer, and counter + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed object receiver while walking properties + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested zero-based property position + emitter.instruction("test rdi, rdi"); // null handles produce a null property key + emitter.instruction("jz __elephc_eval_value_object_property_iter_key_null"); // branch to boxed null for null runtime cells + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("jne __elephc_eval_value_object_property_iter_key_null"); // non-objects have no JSON-visible property key + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // is the object payload null? + emitter.instruction("jz __elephc_eval_value_object_property_iter_key_null"); // null object payloads produce a null key + abi::emit_load_symbol_to_reg(emitter, "r11", "_stdclass_class_id", 0); + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the object's runtime class id + emitter.instruction("cmp rax, r11"); // check whether the object is stdClass + emitter.instruction("jne __elephc_eval_value_object_property_iter_key_null"); // non-stdClass objects have no bridge-visible key + emitter.instruction("mov r10, QWORD PTR [r10 + 8]"); // load stdClass dynamic-property hash pointer + emitter.instruction("test r10, r10"); // is the property hash null? + emitter.instruction("jz __elephc_eval_value_object_property_iter_key_null"); // null property hashes produce a null key + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the hash pointer for repeated iterator helper calls + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // start the insertion-order property counter at zero + emitter.instruction("xor esi, esi"); // cursor 0 starts at the property hash head entry + emitter.label("__elephc_eval_value_object_property_iter_key_loop"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the hash pointer before advancing the iterator + emitter.instruction("call __rt_hash_iter_next"); // fetch the next insertion-order property key + emitter.instruction("cmp rax, -1"); // did the iterator report the done sentinel? + emitter.instruction("je __elephc_eval_value_object_property_iter_key_null"); // out-of-range positions produce a null key + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // load the current insertion-order property position + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // load the requested property position + emitter.instruction("cmp r10, r11"); // is this the requested property entry? + emitter.instruction("je __elephc_eval_value_object_property_iter_key_box"); // box the current property key when the position matches + emitter.instruction("add r10, 1"); // advance the insertion-order property counter + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the updated property counter + emitter.instruction("mov rsi, rax"); // use the returned cursor for the next iterator call + emitter.instruction("jmp __elephc_eval_value_object_property_iter_key_loop"); // continue walking until the requested position is reached + emitter.label("__elephc_eval_value_object_property_iter_key_box"); + emitter.instruction("cmp rdx, -1"); // integer hash keys carry key_hi = -1 + emitter.instruction("jne __elephc_eval_value_object_property_iter_key_string"); // string property keys need string-tag boxing + emitter.instruction("mov eax, 0"); // runtime tag 0 = integer key fallback + emitter.instruction("xor esi, esi"); // integer keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box the integer property key as Mixed + emitter.instruction("jmp __elephc_eval_value_object_property_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_object_property_iter_key_string"); + emitter.instruction("mov rsi, rdx"); // move the string key length into the boxing high word + emitter.instruction("mov eax, 1"); // runtime tag 1 = string property key + emitter.instruction("call __rt_mixed_from_value"); // persist and box the string property key as Mixed + emitter.instruction("jmp __elephc_eval_value_object_property_iter_key_done"); // return the boxed key to Rust + emitter.label("__elephc_eval_value_object_property_iter_key_null"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null keys do not use a low payload word + emitter.instruction("xor esi, esi"); // null keys do not use a high payload word + emitter.instruction("call __rt_mixed_from_value"); // box null for invalid property-key requests + emitter.label("__elephc_eval_value_object_property_iter_key_done"); + emitter.instruction("add rsp, 32"); // release the property-iterator wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed property key to Rust + emitter.label("__elephc_eval_key_normalize"); emitter.instruction("push rbp"); // preserve the caller frame pointer while classifying the eval key emitter.instruction("mov rbp, rsp"); // establish a stable key-normalizer frame diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 71f515bd2d..72698a205f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -927,6 +927,26 @@ echo gettype($object) . ":" . $object->a . ":" . $object->b->c; assert_eq!(out, "1:x:2:3:y:object:1:x"); } +/// Verifies eval `json_encode()` serializes stdClass dynamic properties. +#[test] +fn test_eval_dispatches_json_encode_stdclass_object() { + let out = compile_and_run( + r#"a = 7; +echo json_encode($empty);'); +"#, + ); + assert_eq!( + out, + r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# + ); +} + /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. #[test] fn test_eval_dispatches_json_last_error_builtin_calls() { From ad75264ae237dd0cdd133f7ffbb9b330426b1d52 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 20:07:50 +0200 Subject: [PATCH 0258/1208] Support eval is_object predicate --- crates/elephc-eval/src/interpreter.rs | 22 ++++++++++++++++------ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 9 +++++++-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 6ec4149bb0..e06057a8d4 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1860,7 +1860,7 @@ fn eval_positional_expr_call( | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_real" | "is_resource" | "is_string" => { + | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } "ip2long" => eval_builtin_ip2long(args, context, scope, values), @@ -2337,6 +2337,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "is_nan" | "is_null" | "is_numeric" + | "is_object" | "is_real" | "is_resource" | "is_string" @@ -2579,8 +2580,10 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { } "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" - | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_real" - | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), + | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" + | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => { + Some(&["value"]) + } "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), @@ -3794,7 +3797,7 @@ fn eval_builtin_with_values( } "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_real" | "is_resource" | "is_string" => { + | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -12476,6 +12479,7 @@ fn eval_type_predicate_result( "is_bool" => tag == EVAL_TAG_BOOL, "is_null" => tag == EVAL_TAG_NULL, "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_object" => tag == EVAL_TAG_OBJECT, "is_resource" => tag == EVAL_TAG_RESOURCE, "is_nan" => eval_float_value(value, values)?.is_nan(), "is_infinite" => eval_float_value(value, values)?.is_infinite(), @@ -20876,6 +20880,8 @@ echo is_numeric("-5"); echo is_numeric("3.14"); echo is_numeric("abc") ? "bad" : "N"; echo is_numeric(true) ? "bad" : "B"; echo is_resource(1) ? "bad" : "R"; +echo is_object($object) ? "O" : "bad"; +echo is_object([1]) ? "bad" : "o"; echo is_nan(fdiv(0, 0)) ? "N" : "bad"; echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; @@ -20886,7 +20892,9 @@ echo call_user_func_array("is_array", [[1]]); echo call_user_func("is_numeric", "12"); echo call_user_func("is_iterable", [1]); echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; -echo function_exists("is_numeric"); echo function_exists("is_resource"); +echo call_user_func("is_object", $object) ? "O" : "bad"; +echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; +echo function_exists("is_numeric"); echo function_exists("is_object"); echo function_exists("is_resource"); echo function_exists("is_double"); echo function_exists("is_nan"); echo function_exists("is_finite"); echo function_exists("is_iterable"); return function_exists("is_infinite");"#, @@ -20894,12 +20902,14 @@ return function_exists("is_infinite");"#, .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); assert_eq!( values.output, - "1111111111111Tok11111NBRNIiFf:1111t111111" + "1111111111111Tok11111NBROoNIiFf:1111tOo1111111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 99d345b920..4877e128ed 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index c7fa3147a1..ae328949d0 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 72698a205f..3831fc8968 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3107,6 +3107,9 @@ echo is_numeric("-5"); echo is_numeric("3.14"); echo is_numeric("abc") ? "bad" : "N"; echo is_numeric(true) ? "bad" : "B"; echo is_resource(1) ? "bad" : "R"; +$object = json_decode("{}"); +echo is_object($object) ? "O" : "bad"; +echo is_object([1]) ? "bad" : "o"; echo is_nan(fdiv(0, 0)) ? "N" : "bad"; echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; @@ -3121,13 +3124,15 @@ echo call_user_func("is_iterable", [1]); echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; echo call_user_func("is_resource", $h); echo call_user_func_array("is_resource", [$h]); +echo call_user_func("is_object", $object) ? "O" : "bad"; +echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; echo call_user_func("is_nan", fdiv(0, 0)) ? "N" : "bad"; echo call_user_func_array("is_finite", [42]) ? "F" : "bad"; -echo function_exists("is_double"); echo function_exists("is_numeric"); echo function_exists("is_resource"); +echo function_exists("is_double"); echo function_exists("is_numeric"); echo function_exists("is_object"); echo function_exists("is_resource"); echo function_exists("is_nan"); echo function_exists("is_finite"); echo function_exists("is_iterable"); echo function_exists("is_infinite");'); "#, ); - assert_eq!(out, "1111111111111Tok11111NBRNIiFfH:1111t11NF1111111"); + assert_eq!(out, "1111111111111Tok11111NBROoNIiFfH:1111t11OoNF11111111"); } /// Verifies eval scalar cast builtins return boxed Mixed cells through direct and callable calls. From 4109acb9c6a2630a21f870309771472fc0d5fdb6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 20:19:01 +0200 Subject: [PATCH 0259/1208] Support eval get_class builtin --- crates/elephc-eval/src/interpreter.rs | 69 ++++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 9 +++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen_support/runtime/eval_bridge.rs | 50 ++++++++++++++++ tests/codegen/eval.rs | 19 ++++++ 6 files changed, 149 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index e06057a8d4..10bd91ceda 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -386,6 +386,12 @@ pub trait RuntimeValueOps { /// Returns whether a runtime class table contains the requested class name. fn class_exists(&mut self, name: &str) -> Result; + /// Returns the PHP-visible runtime class name for an object cell. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result; + /// Returns the visible element count for an array-like runtime cell. fn array_len(&mut self, array: RuntimeCellHandle) -> Result; @@ -1836,6 +1842,7 @@ fn eval_positional_expr_call( "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), + "get_class" => eval_builtin_get_class(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), @@ -2292,6 +2299,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "getprotobynumber" | "getservbyname" | "getservbyport" + | "get_class" | "getcwd" | "getenv" | "gettype" @@ -2584,6 +2592,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => { Some(&["value"]) } + "get_class" => Some(&["object"]), "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), @@ -3671,6 +3680,12 @@ fn eval_builtin_with_values( }; eval_getenv_result(*name, values)? } + "get_class" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_get_class_result(*object, values)? + } "gettype" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -12435,6 +12450,28 @@ fn eval_gettype_result( values.string(eval_gettype_name(tag)) } +/// Evaluates PHP's `get_class(...)` over one eval object expression. +fn eval_builtin_get_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_get_class_result(object, values) +} + +/// Resolves the PHP-visible class name for one already materialized object cell. +fn eval_get_class_result( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.object_class_name(object) +} + /// Returns the PHP-visible type name for a concrete eval runtime tag. fn eval_gettype_name(tag: u64) -> &'static str { match tag { @@ -15452,6 +15489,18 @@ mod tests { Ok(name.eq_ignore_ascii_case("KnownClass")) } + /// Returns a fake PHP class name for object-tagged test values. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(_) => self.string("stdClass"), + FakeValue::Iterator { .. } => self.string("Iterator"), + _ => Err(EvalStatus::RuntimeFatal), + } + } + /// Returns the visible element count for fake array values. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { match self.get(array) { @@ -20983,6 +21032,26 @@ return function_exists("gettype");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `get_class()` reads object class names directly and by callable. + #[test] + fn execute_program_dispatches_get_class_builtin() { + let program = parse_fragment( + br#"echo get_class($object); echo ":"; +echo call_user_func("get_class", $object); echo ":"; +return call_user_func_array("get_class", ["object" => $object]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stdClass:stdClass:"); + assert_eq!(values.get(result), FakeValue::String("stdClass".to_string())); + } + /// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. #[test] fn execute_program_dispatches_abs_builtin() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index ea81614136..8b7d0343c8 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -69,6 +69,7 @@ unsafe extern "C" { args: *mut RuntimeCell, ) -> u64; fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; + fn __elephc_eval_value_object_class_name(object: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; @@ -349,6 +350,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_class_exists(name.as_ptr(), name.len() as u64) != 0 }) } + /// Returns a boxed Mixed string naming a boxed Mixed object's runtime class. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_object_class_name(object.as_ptr()) }) + } + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 4877e128ed..f763ee6759 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ae328949d0..fcd322d534 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 0b159fd4f1..e58be5b3e8 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -146,6 +146,30 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #64"); // release the class-exists helper frame emitter.instruction("ret"); // return the class-exists flag to Rust + label_c_global(emitter, "__elephc_eval_value_object_class_name"); + emitter.instruction("cbz x0, __elephc_eval_value_object_class_name_miss"); // reject null boxed handles before reading their tag + emitter.instruction("ldr x9, [x0]"); // load the boxed eval value runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 is an object payload + emitter.instruction("b.ne __elephc_eval_value_object_class_name_miss"); // non-objects cannot provide a class name + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_class_name_miss"); // reject malformed object payloads + emitter.instruction("ldr x10, [x9]"); // load the object's runtime class id + abi::emit_symbol_address(emitter, "x11", "_class_name_count"); + emitter.instruction("ldr x11, [x11]"); // load the dense class-name table length + emitter.instruction("cmp x10, x11"); // check whether the class id is in table bounds + emitter.instruction("b.hs __elephc_eval_value_object_class_name_miss"); // reject missing or out-of-range class ids + abi::emit_symbol_address(emitter, "x11", "_class_name_entries"); + emitter.instruction("lsl x12, x10, #4"); // convert class id to a 16-byte table-entry offset + emitter.instruction("add x11, x11, x12"); // address the class-name entry for this class id + emitter.instruction("ldr x1, [x11]"); // load the class-name string pointer + emitter.instruction("ldr x2, [x11, #8]"); // load the class-name string length + emitter.instruction("cbz x2, __elephc_eval_value_object_class_name_miss"); // reject table holes with empty names + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("b __rt_mixed_from_value"); // persist and box the class-name string for Rust + emitter.label("__elephc_eval_value_object_class_name_miss"); + emitter.instruction("mov x0, xzr"); // report failure as a null C pointer to Rust + emitter.instruction("ret"); // return the failure sentinel + label_c_global(emitter, "__elephc_eval_value_array_new"); emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for array allocation and boxing emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls @@ -1303,6 +1327,32 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the class-exists flag to Rust + label_c_global(emitter, "__elephc_eval_value_object_class_name"); + emitter.instruction("test rdi, rdi"); // reject null boxed handles before reading their tag + emitter.instruction("jz __elephc_eval_value_object_class_name_miss_x86"); // null handles cannot provide a class name + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed eval value runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 is an object payload + emitter.instruction("jne __elephc_eval_value_object_class_name_miss_x86"); // non-objects cannot provide a class name + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // check the unboxed object pointer before dereferencing it + emitter.instruction("jz __elephc_eval_value_object_class_name_miss_x86"); // reject malformed object payloads + emitter.instruction("mov r11, QWORD PTR [r10]"); // load the object's runtime class id + abi::emit_load_symbol_to_reg(emitter, "rdx", "_class_name_count", 0); + emitter.instruction("cmp r11, rdx"); // check whether the class id is in table bounds + emitter.instruction("jae __elephc_eval_value_object_class_name_miss_x86"); // reject missing or out-of-range class ids + abi::emit_symbol_address(emitter, "rdx", "_class_name_entries"); + emitter.instruction("shl r11, 4"); // convert class id to a 16-byte table-entry offset + emitter.instruction("add rdx, r11"); // address the class-name entry for this class id + emitter.instruction("mov rdi, QWORD PTR [rdx]"); // load the class-name string pointer + emitter.instruction("mov rsi, QWORD PTR [rdx + 8]"); // load the class-name string length + emitter.instruction("test rsi, rsi"); // table holes use a zero-length name + emitter.instruction("jz __elephc_eval_value_object_class_name_miss_x86"); // reject table holes with empty names + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("jmp __rt_mixed_from_value"); // persist and box the class-name string for Rust + emitter.label("__elephc_eval_value_object_class_name_miss_x86"); + emitter.instruction("xor eax, eax"); // report failure as a null C pointer to Rust + emitter.instruction("ret"); // return the failure sentinel + label_c_global(emitter, "__elephc_eval_value_array_new"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3831fc8968..83977686bc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3175,6 +3175,25 @@ echo ":"; echo function_exists("gettype");'); ); } +/// Verifies eval `get_class()` resolves stdClass and AOT object runtime names. +#[test] +fn test_eval_dispatches_get_class_builtin_call() { + let out = compile_and_run( + r#" $probe]) . ":"; +echo function_exists("get_class");'); +"#, + ); + assert_eq!(out, "stdClass:EvalClassNameProbe:stdClass:EvalClassNameProbe:1"); +} + /// Verifies eval `define()` and `defined()` share dynamic constant names across fragments. #[test] fn test_eval_define_and_defined_dynamic_constants() { From 5fde7df4e96d9ebe7fbb1cabd2b2c139a55bb165 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 20:27:46 +0200 Subject: [PATCH 0260/1208] Support eval get_parent_class builtin --- crates/elephc-eval/src/interpreter.rs | 75 ++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 9 ++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen_support/runtime/eval_bridge.rs | 166 +++++++++++++++++++++ tests/codegen/eval.rs | 24 +++ 6 files changed, 276 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 10bd91ceda..d30a82621c 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -392,6 +392,12 @@ pub trait RuntimeValueOps { object: RuntimeCellHandle, ) -> Result; + /// Returns the PHP-visible parent class name for an object or class-name cell. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result; + /// Returns the visible element count for an array-like runtime cell. fn array_len(&mut self, array: RuntimeCellHandle) -> Result; @@ -1843,6 +1849,7 @@ fn eval_positional_expr_call( "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), "get_class" => eval_builtin_get_class(args, context, scope, values), + "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), @@ -2300,6 +2307,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "getservbyname" | "getservbyport" | "get_class" + | "get_parent_class" | "getcwd" | "getenv" | "gettype" @@ -2593,6 +2601,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { Some(&["value"]) } "get_class" => Some(&["object"]), + "get_parent_class" => Some(&["object_or_class"]), "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), @@ -3686,6 +3695,12 @@ fn eval_builtin_with_values( }; eval_get_class_result(*object, values)? } + "get_parent_class" => { + let [object_or_class] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_get_parent_class_result(*object_or_class, values)? + } "gettype" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -12472,6 +12487,28 @@ fn eval_get_class_result( values.object_class_name(object) } +/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. +fn eval_builtin_get_parent_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object_or_class] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object_or_class = eval_expr(object_or_class, context, scope, values)?; + eval_get_parent_class_result(object_or_class, values) +} + +/// Resolves the PHP-visible parent class name for one object or class-name cell. +fn eval_get_parent_class_result( + object_or_class: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.parent_class_name(object_or_class) +} + /// Returns the PHP-visible type name for a concrete eval runtime tag. fn eval_gettype_name(tag: u64) -> &'static str { match tag { @@ -15501,6 +15538,20 @@ mod tests { } } + /// Returns fake parent-class names for eval introspection unit tests. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) => self.string("ParentClass"), + FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { + self.string("ParentClass") + } + _ => self.string(""), + } + } + /// Returns the visible element count for fake array values. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { match self.get(array) { @@ -21052,6 +21103,30 @@ return call_user_func_array("get_class", ["object" => $object]);"#, assert_eq!(values.get(result), FakeValue::String("stdClass".to_string())); } + /// Verifies eval `get_parent_class()` reads object and class-string parents by callable. + #[test] + fn execute_program_dispatches_get_parent_class_builtin() { + let program = parse_fragment( + br#"echo get_parent_class($object); echo ":"; +echo get_parent_class("ChildClass"); echo ":"; +echo call_user_func("get_parent_class", $object); echo ":"; +return call_user_func_array("get_parent_class", ["object_or_class" => "ChildClass"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ParentClass:ParentClass:ParentClass:" + ); + assert_eq!(values.get(result), FakeValue::String("ParentClass".to_string())); + } + /// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. #[test] fn execute_program_dispatches_abs_builtin() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 8b7d0343c8..2e74322dd0 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -70,6 +70,7 @@ unsafe extern "C" { ) -> u64; fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; fn __elephc_eval_value_object_class_name(object: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_parent_class_name(object_or_class: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; @@ -358,6 +359,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_object_class_name(object.as_ptr()) }) } + /// Returns a boxed Mixed string naming a boxed Mixed object's or class string's parent class. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_parent_class_name(object_or_class.as_ptr()) }) + } + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. fn array_len(&mut self, array: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index f763ee6759..301459ac1f 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index fcd322d534..517d5c228f 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index e58be5b3e8..f61e08c470 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -170,6 +170,89 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov x0, xzr"); // report failure as a null C pointer to Rust emitter.instruction("ret"); // return the failure sentinel + label_c_global(emitter, "__elephc_eval_value_parent_class_name"); + emitter.instruction("sub sp, sp, #80"); // reserve lookup state and a call-preserving wrapper frame + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #64"); // establish a stable parent-class lookup frame pointer + emitter.instruction("bl __rt_mixed_unbox"); // expose the eval value tag and payload words + emitter.instruction("cmp x0, #6"); // tag 6 is an object payload + emitter.instruction("b.eq __elephc_eval_value_parent_class_name_object"); // derive the parent from the object's runtime class id + emitter.instruction("cmp x0, #1"); // tag 1 is a class-name string payload + emitter.instruction("b.eq __elephc_eval_value_parent_class_name_string"); // resolve a class string through generated metadata + emitter.instruction("b __elephc_eval_value_parent_class_name_empty"); // unsupported input types have no parent class name + emitter.label("__elephc_eval_value_parent_class_name_object"); + emitter.instruction("cbz x1, __elephc_eval_value_parent_class_name_empty"); // malformed object payloads have no parent class + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emitter.instruction("b __elephc_eval_value_parent_class_name_from_id"); // convert the class id to its parent class name + emitter.label("__elephc_eval_value_parent_class_name_string"); + emitter.instruction("str x1, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested class-name length + abi::emit_symbol_address(emitter, "x9", "_classes_by_name_count"); + emitter.instruction("ldr x9, [x9]"); // load the registered class-name count + emitter.instruction("cbz x9, __elephc_eval_value_parent_class_name_empty"); // an empty class table cannot resolve a parent name + emitter.instruction("str x9, [sp, #16]"); // save the table count across string compares + abi::emit_symbol_address(emitter, "x10", "_classes_by_name"); + emitter.instruction("str x10, [sp, #24]"); // save the current class-name table cursor + emitter.instruction("mov x11, #0"); // start scanning generated class-name entries at index zero + emitter.label("__elephc_eval_value_parent_class_name_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the class-name table count + emitter.instruction("cmp x11, x9"); // have all generated class names been checked? + emitter.instruction("b.ge __elephc_eval_value_parent_class_name_empty"); // no generated class matched the requested string + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-name metadata entry + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested name lengths first + emitter.instruction("b.ne __elephc_eval_value_parent_class_name_skip"); // length mismatch means this class entry cannot match + emitter.instruction("str x11, [sp, #32]"); // preserve the scan index across the string compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the generated class-name pointer + emitter.instruction("mov x4, x12"); // pass the generated class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #32]"); // restore the scan index after the string compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this entry? + emitter.instruction("b.eq __elephc_eval_value_parent_class_name_hit"); // resolve the matched class entry to its parent id + emitter.label("__elephc_eval_value_parent_class_name_skip"); + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-name table entry + emitter.instruction("add x10, x10, #32"); // advance to the next class-name table entry + emitter.instruction("str x10, [sp, #24]"); // persist the advanced table cursor + emitter.instruction("add x11, x11, #1"); // advance the class-name scan index + emitter.instruction("b __elephc_eval_value_parent_class_name_loop"); // continue scanning generated class names + emitter.label("__elephc_eval_value_parent_class_name_hit"); + emitter.instruction("ldr x10, [sp, #24]"); // reload the matched class-name table entry + emitter.instruction("ldr x9, [x10, #16]"); // load the matched runtime class id + emitter.label("__elephc_eval_value_parent_class_name_from_id"); + abi::emit_symbol_address(emitter, "x10", "_class_name_count"); + emitter.instruction("ldr x10, [x10]"); // load the dense class-name table length + emitter.instruction("cmp x9, x10"); // check that the class id can index parent metadata + emitter.instruction("b.hs __elephc_eval_value_parent_class_name_empty"); // unknown class ids have no parent class name + abi::emit_symbol_address(emitter, "x11", "_class_parent_ids"); + emitter.instruction("lsl x12, x9, #3"); // convert class id to a parent-id table byte offset + emitter.instruction("ldr x9, [x11, x12]"); // load the parent runtime class id + emitter.instruction("mov x13, #-1"); // materialize the parentless class sentinel + emitter.instruction("cmp x9, x13"); // check whether the runtime class has no parent + emitter.instruction("b.eq __elephc_eval_value_parent_class_name_empty"); // parentless runtime classes produce an empty string + emitter.instruction("cmp x9, x10"); // check that the parent class id can index name metadata + emitter.instruction("b.hs __elephc_eval_value_parent_class_name_empty"); // invalid parent ids produce an empty string + abi::emit_symbol_address(emitter, "x11", "_class_name_entries"); + emitter.instruction("lsl x12, x9, #4"); // convert parent id to a 16-byte name-entry offset + emitter.instruction("add x11, x11, x12"); // address the parent class-name metadata row + emitter.instruction("ldr x1, [x11]"); // load the parent class-name string pointer + emitter.instruction("ldr x2, [x11, #8]"); // load the parent class-name string length + emitter.instruction("cbz x2, __elephc_eval_value_parent_class_name_empty"); // table holes represent missing parent names + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // persist and box the parent class-name string + emitter.instruction("b __elephc_eval_value_parent_class_name_done"); // restore the wrapper frame before returning to Rust + emitter.label("__elephc_eval_value_parent_class_name_empty"); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("mov x1, xzr"); // missing parent names use an empty string pointer + emitter.instruction("mov x2, xzr"); // missing parent names use an empty string length + emitter.instruction("bl __rt_mixed_from_value"); // box the empty parent class-name string + emitter.label("__elephc_eval_value_parent_class_name_done"); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // release the parent-class lookup wrapper frame + emitter.instruction("ret"); // return the boxed parent class-name string to Rust + label_c_global(emitter, "__elephc_eval_value_array_new"); emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for array allocation and boxing emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls @@ -1353,6 +1436,89 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("xor eax, eax"); // report failure as a null C pointer to Rust emitter.instruction("ret"); // return the failure sentinel + label_c_global(emitter, "__elephc_eval_value_parent_class_name"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across helper calls + emitter.instruction("mov rbp, rsp"); // establish a stable parent-class lookup frame pointer + emitter.instruction("sub rsp, 48"); // reserve lookup state while keeping the stack call-aligned + emitter.instruction("mov rax, rdi"); // move the boxed eval value into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // expose the eval value tag and payload words + emitter.instruction("cmp rax, 6"); // tag 6 is an object payload + emitter.instruction("je __elephc_eval_value_parent_class_name_object_x86"); // derive the parent from the object's runtime class id + emitter.instruction("cmp rax, 1"); // tag 1 is a class-name string payload + emitter.instruction("je __elephc_eval_value_parent_class_name_string_x86"); // resolve a class string through generated metadata + emitter.instruction("jmp __elephc_eval_value_parent_class_name_empty_x86"); // unsupported input types have no parent class name + emitter.label("__elephc_eval_value_parent_class_name_object_x86"); + emitter.instruction("test rdi, rdi"); // check the unboxed object pointer before reading its header + emitter.instruction("jz __elephc_eval_value_parent_class_name_empty_x86"); // malformed object payloads have no parent class + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emitter.instruction("jmp __elephc_eval_value_parent_class_name_from_id_x86"); // convert the class id to its parent class name + emitter.label("__elephc_eval_value_parent_class_name_string_x86"); + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested class-name length + abi::emit_symbol_address(emitter, "r10", "_classes_by_name_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the registered class-name count + emitter.instruction("test r10, r10"); // is the generated class-name table empty? + emitter.instruction("jz __elephc_eval_value_parent_class_name_empty_x86"); // an empty class table cannot resolve a parent name + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the table count across string compares + abi::emit_symbol_address(emitter, "r11", "_classes_by_name"); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current class-name table cursor + emitter.instruction("xor r11d, r11d"); // start scanning generated class-name entries at index zero + emitter.label("__elephc_eval_value_parent_class_name_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the class-name table count + emitter.instruction("cmp r11, r10"); // have all generated class names been checked? + emitter.instruction("jae __elephc_eval_value_parent_class_name_empty_x86"); // no generated class matched the requested string + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-name metadata entry + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested name lengths first + emitter.instruction("jne __elephc_eval_value_parent_class_name_skip_x86"); // length mismatch means this class entry cannot match + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // preserve the scan index across the string compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the generated class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // restore the scan index after the string compare + emitter.instruction("test rax, rax"); // did the requested class name match this entry? + emitter.instruction("je __elephc_eval_value_parent_class_name_hit_x86"); // resolve the matched class entry to its parent id + emitter.label("__elephc_eval_value_parent_class_name_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-name table entry + emitter.instruction("add r10, 32"); // advance to the next class-name table entry + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the advanced table cursor + emitter.instruction("inc r11"); // advance the class-name scan index + emitter.instruction("jmp __elephc_eval_value_parent_class_name_loop_x86"); // continue scanning generated class names + emitter.label("__elephc_eval_value_parent_class_name_hit_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the matched class-name table entry + emitter.instruction("mov r11, QWORD PTR [r10 + 16]"); // load the matched runtime class id + emitter.label("__elephc_eval_value_parent_class_name_from_id_x86"); + abi::emit_load_symbol_to_reg(emitter, "rdx", "_class_name_count", 0); + emitter.instruction("cmp r11, rdx"); // check that the class id can index parent metadata + emitter.instruction("jae __elephc_eval_value_parent_class_name_empty_x86"); // unknown class ids have no parent class name + abi::emit_symbol_address(emitter, "rdx", "_class_parent_ids"); + emitter.instruction("mov r11, QWORD PTR [rdx + r11 * 8]"); // load the parent runtime class id + emitter.instruction("cmp r11, -1"); // check whether the runtime class has no parent + emitter.instruction("je __elephc_eval_value_parent_class_name_empty_x86"); // parentless runtime classes produce an empty string + abi::emit_load_symbol_to_reg(emitter, "rdx", "_class_name_count", 0); + emitter.instruction("cmp r11, rdx"); // check that the parent class id can index name metadata + emitter.instruction("jae __elephc_eval_value_parent_class_name_empty_x86"); // invalid parent ids produce an empty string + abi::emit_symbol_address(emitter, "rdx", "_class_name_entries"); + emitter.instruction("shl r11, 4"); // convert parent id to a 16-byte name-entry offset + emitter.instruction("add rdx, r11"); // address the parent class-name metadata row + emitter.instruction("mov rdi, QWORD PTR [rdx]"); // load the parent class-name string pointer + emitter.instruction("mov rsi, QWORD PTR [rdx + 8]"); // load the parent class-name string length + emitter.instruction("test rsi, rsi"); // table holes represent missing parent names + emitter.instruction("jz __elephc_eval_value_parent_class_name_empty_x86"); // missing parent names produce an empty string + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // persist and box the parent class-name string + emitter.instruction("jmp __elephc_eval_value_parent_class_name_done_x86"); // restore the wrapper frame before returning to Rust + emitter.label("__elephc_eval_value_parent_class_name_empty_x86"); + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("xor edi, edi"); // missing parent names use an empty string pointer + emitter.instruction("xor esi, esi"); // missing parent names use an empty string length + emitter.instruction("call __rt_mixed_from_value"); // box the empty parent class-name string + emitter.label("__elephc_eval_value_parent_class_name_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed parent class-name string to Rust + label_c_global(emitter, "__elephc_eval_value_array_new"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 83977686bc..fc2edf5a7b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3194,6 +3194,30 @@ echo function_exists("get_class");'); assert_eq!(out, "stdClass:EvalClassNameProbe:stdClass:EvalClassNameProbe:1"); } +/// Verifies eval `get_parent_class()` resolves AOT object and class-string parents. +#[test] +fn test_eval_dispatches_get_parent_class_builtin_call() { + let out = compile_and_run( + r#" "EvalParentChild"]) . ":"; +echo function_exists("get_parent_class");'); +"#, + ); + + assert_eq!( + out, + "EvalParentBase:EvalParentBase:EvalParentBase:EvalParentBase:EvalParentBase:1" + ); +} + /// Verifies eval `define()` and `defined()` share dynamic constant names across fragments. #[test] fn test_eval_define_and_defined_dynamic_constants() { From 78eb23fac503e90d2cc82bfedc6d76b266cff325 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 20:34:28 +0200 Subject: [PATCH 0261/1208] Support eval interface_exists builtin --- crates/elephc-eval/src/interpreter.rs | 75 ++++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 6 ++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen_support/runtime/eval_bridge.rs | 31 +++++++++ tests/codegen/eval.rs | 20 ++++++ 6 files changed, 134 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index d30a82621c..2f87d9dfb7 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -386,6 +386,9 @@ pub trait RuntimeValueOps { /// Returns whether a runtime class table contains the requested class name. fn class_exists(&mut self, name: &str) -> Result; + /// Returns whether a runtime interface table contains the requested interface name. + fn interface_exists(&mut self, name: &str) -> Result; + /// Returns the PHP-visible runtime class name for an object cell. fn object_class_name( &mut self, @@ -1804,6 +1807,7 @@ fn eval_positional_expr_call( "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "class_exists" => eval_builtin_class_exists(args, context, scope, values), + "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), "chop" => eval_builtin_trim_like(name, args, context, scope, values), "boolval" | "floatval" | "intval" | "strval" => { eval_builtin_cast(name, args, context, scope, values) @@ -2133,6 +2137,49 @@ fn eval_class_exists_name( values.class_exists(name) } +/// Evaluates `interface_exists(...)` against generated interface-name metadata. +fn eval_builtin_interface_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_interface_exists_name(name, values)?; + values.bool_value(exists) +} + +/// Evaluates `interface_exists(...)` from already materialized call arguments. +fn eval_interface_exists_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_interface_exists_name(*name, values)?, + [name, _autoload] => eval_interface_exists_name(*name, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP interface-name cell and probes generated interface metadata. +fn eval_interface_exists_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + values.interface_exists(name.trim_start_matches('\\')) +} + /// Evaluates PHP's `isset(...)` language construct over eval-visible values. fn eval_builtin_isset( args: &[EvalExpr], @@ -2257,6 +2304,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "call_user_func" | "call_user_func_array" | "class_exists" + | "interface_exists" | "boolval" | "chop" | "chr" @@ -2605,6 +2653,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), + "interface_exists" => Some(&["interface", "autoload"]), "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), "chmod" => Some(&["filename", "permissions"]), "chr" => Some(&["codepoint"]), @@ -3580,6 +3629,7 @@ fn eval_builtin_with_values( values.bool_value(eval_function_probe_exists(context, &name))? } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, "json_decode" => match evaluated_args { [json] => eval_json_decode_result(*json, None, None, None, context, values)?, [json, associative] => { @@ -15526,6 +15576,11 @@ mod tests { Ok(name.eq_ignore_ascii_case("KnownClass")) } + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + fn interface_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownInterface")) + } + /// Returns a fake PHP class name for object-tagged test values. fn object_class_name( &mut self, @@ -21783,6 +21838,26 @@ echo function_exists("missing_probe") . "x";"#, assert_eq!(values.output, "1x1x1x1xxx"); } + /// Verifies eval `interface_exists()` probes generated interface metadata by callable. + #[test] + fn execute_program_interface_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo interface_exists("KnownInterface") ? "Y" : "N"; +echo interface_exists("knowninterface") ? "Y" : "N"; +echo interface_exists("KnownClass") ? "Y" : "N"; +echo call_user_func("interface_exists", "KnownInterface") ? "Y" : "N"; +echo call_user_func_array("interface_exists", ["interface" => "KnownInterface"]) ? "Y" : "N"; +echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYNYYN"); + } + /// Verifies eval `define()` and `defined()` share a dynamic constant-name table. #[test] fn execute_program_define_and_defined_use_dynamic_constant_table() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index 2e74322dd0..cacf342be7 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -69,6 +69,7 @@ unsafe extern "C" { args: *mut RuntimeCell, ) -> u64; fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; + fn __elephc_eval_interface_exists(name_ptr: *const u8, name_len: u64) -> u64; fn __elephc_eval_value_object_class_name(object: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_parent_class_name(object_or_class: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; @@ -351,6 +352,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_class_exists(name.as_ptr(), name.len() as u64) != 0 }) } + /// Returns whether the generated AOT interface-name table contains the requested interface. + fn interface_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_interface_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + /// Returns a boxed Mixed string naming a boxed Mixed object's runtime class. fn object_class_name( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 301459ac1f..9b936140f5 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 517d5c228f..ddbefe30d9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `function_exists()`, `is_callable()`, `class_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index f61e08c470..eb23e39e7a 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -146,6 +146,22 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #64"); // release the class-exists helper frame emitter.instruction("ret"); // return the class-exists flag to Rust + label_c_global(emitter, "__elephc_eval_interface_exists"); + emitter.instruction("sub sp, sp, #16"); // preserve the Rust return address across the metadata lookup + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across runtime call + emitter.instruction("mov x29, sp"); // establish a stable interface-exists wrapper frame + emitter.instruction("mov x2, x1"); // move the C string length into the internal string-length register + emitter.instruction("mov x1, x0"); // move the C string pointer into the internal string-pointer register + emitter.instruction("bl __rt_instanceof_lookup"); // resolve the name against class/interface metadata + emitter.instruction("cmp x0, #0"); // did the name resolve to any class-like target? + emitter.instruction("cset x0, ne"); // keep true only when metadata lookup succeeded + emitter.instruction("cmp x2, #1"); // target kind 1 means interface metadata + emitter.instruction("cset x1, eq"); // keep true only for interface targets + emitter.instruction("and x0, x0, x1"); // return success && interface-kind + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the interface-exists wrapper frame + emitter.instruction("ret"); // return the interface-exists flag to Rust + label_c_global(emitter, "__elephc_eval_value_object_class_name"); emitter.instruction("cbz x0, __elephc_eval_value_object_class_name_miss"); // reject null boxed handles before reading their tag emitter.instruction("ldr x9, [x0]"); // load the boxed eval value runtime tag @@ -1410,6 +1426,21 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the class-exists flag to Rust + label_c_global(emitter, "__elephc_eval_interface_exists"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime call + emitter.instruction("mov rbp, rsp"); // establish a stable interface-exists wrapper frame + emitter.instruction("mov rax, rdi"); // move the C string pointer into the internal string-pointer register + emitter.instruction("mov rdx, rsi"); // move the C string length into the internal string-length register + emitter.instruction("call __rt_instanceof_lookup"); // resolve the name against class/interface metadata + emitter.instruction("test rax, rax"); // did the name resolve to any class-like target? + emitter.instruction("setne al"); // keep true only when metadata lookup succeeded + emitter.instruction("cmp rdx, 1"); // target kind 1 means interface metadata + emitter.instruction("sete cl"); // keep true only for interface targets + emitter.instruction("and al, cl"); // return success && interface-kind + emitter.instruction("movzx eax, al"); // widen the C boolean result for Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the interface-exists flag to Rust + label_c_global(emitter, "__elephc_eval_value_object_class_name"); emitter.instruction("test rdi, rdi"); // reject null boxed handles before reading their tag emitter.instruction("jz __elephc_eval_value_object_class_name_miss_x86"); // null handles cannot provide a class name diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fc2edf5a7b..4ec59540bf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4353,6 +4353,26 @@ echo class_exists(class: "MissingEvalClassExistsProbe", autoload: false) ? "Y" : assert_eq!(out, "YYYYYN"); } +/// Verifies eval `interface_exists()` probes generated AOT interface metadata. +#[test] +fn test_eval_fragment_interface_exists_probes_aot_interfaces() { + let out = compile_and_run( + r#" false, "interface" => "\EvalInterfaceExistsProbe"]) ? "Y" : "N"; +echo function_exists("interface_exists");'); +"#, + ); + assert_eq!(out, "YYYNYY1"); +} + /// Verifies duplicate eval-declared functions fail through the runtime bridge. #[test] fn test_eval_duplicate_declared_function_fails() { From 9baa09682a95370050b020c51862c1ffa00fd818 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 20:51:02 +0200 Subject: [PATCH 0262/1208] Support eval class relation builtins --- crates/elephc-eval/src/interpreter.rs | 92 ++++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 23 ++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- src/codegen_support/runtime/eval_bridge.rs | 77 ++++++++++++++++++ tests/codegen/eval.rs | 25 ++++++ 6 files changed, 219 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 2f87d9dfb7..f5709ecd57 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -389,6 +389,14 @@ pub trait RuntimeValueOps { /// Returns whether a runtime interface table contains the requested interface name. fn interface_exists(&mut self, name: &str) -> Result; + /// Tests whether a boxed object cell satisfies a class/interface relation. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result; + /// Returns the PHP-visible runtime class name for an object cell. fn object_class_name( &mut self, @@ -1808,6 +1816,7 @@ fn eval_positional_expr_call( "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "class_exists" => eval_builtin_class_exists(args, context, scope, values), "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), + "is_a" | "is_subclass_of" => eval_builtin_is_a_relation(name, args, context, scope, values), "chop" => eval_builtin_trim_like(name, args, context, scope, values), "boolval" | "floatval" | "intval" | "strval" => { eval_builtin_cast(name, args, context, scope, values) @@ -2180,6 +2189,48 @@ fn eval_interface_exists_name( values.interface_exists(name.trim_start_matches('\\')) } +/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. +fn eval_builtin_is_a_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_is_a_relation_result(name, &evaluated_args, values) +} + +/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. +fn eval_is_a_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_or_class, target_class, allow_string) = match evaluated_args { + [object_or_class, target_class] => { + (*object_or_class, *target_class, name == "is_subclass_of") + } + [object_or_class, target_class, allow_string] => { + (*object_or_class, *target_class, values.truthy(*allow_string)?) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let target_class = values.string_bytes(target_class)?; + let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_class = target_class.trim_start_matches('\\'); + let is_object = values.type_tag(object_or_class)? == 6; + let result = if is_object || allow_string { + values.object_is_a(object_or_class, target_class, name == "is_subclass_of")? + } else { + false + }; + values.bool_value(result) +} + /// Evaluates PHP's `isset(...)` language construct over eval-visible values. fn eval_builtin_isset( args: &[EvalExpr], @@ -2305,6 +2356,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "call_user_func_array" | "class_exists" | "interface_exists" + | "is_a" + | "is_subclass_of" | "boolval" | "chop" | "chr" @@ -2654,6 +2707,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), "interface_exists" => Some(&["interface", "autoload"]), + "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), "chmod" => Some(&["filename", "permissions"]), "chr" => Some(&["codepoint"]), @@ -3630,6 +3684,7 @@ fn eval_builtin_with_values( } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, + "is_a" | "is_subclass_of" => eval_is_a_relation_result(name, evaluated_args, values)?, "json_decode" => match evaluated_args { [json] => eval_json_decode_result(*json, None, None, None, context, values)?, [json, associative] => { @@ -15581,6 +15636,22 @@ mod tests { Ok(name.eq_ignore_ascii_case("KnownInterface")) } + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), + _ => Ok(false), + } + } + /// Returns a fake PHP class name for object-tagged test values. fn object_class_name( &mut self, @@ -21858,6 +21929,27 @@ echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N assert_eq!(values.output, "YYNYYN"); } + /// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. + #[test] + fn execute_program_is_a_relation_uses_runtime_probe() { + let program = parse_fragment( + br#"$object = new KnownClass(); +echo is_a($object, "KnownClass") ? "Y" : "N"; +echo is_subclass_of($object, "KnownClass") ? "Y" : "N"; +echo is_subclass_of($object, "ParentClass") ? "Y" : "N"; +echo call_user_func("is_a", $object, "ParentClass") ? "Y" : "N"; +echo call_user_func_array("is_subclass_of", ["object_or_class" => $object, "class" => "ParentClass"]) ? "Y" : "N"; +echo is_a(object_or_class: $object, class: "MissingClass", allow_string: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YNYYYN"); + } + /// Verifies eval `define()` and `defined()` share a dynamic constant-name table. #[test] fn execute_program_define_and_defined_use_dynamic_constant_table() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index cacf342be7..a5797450b0 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -70,6 +70,12 @@ unsafe extern "C" { ) -> u64; fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; fn __elephc_eval_interface_exists(name_ptr: *const u8, name_len: u64) -> u64; + fn __elephc_eval_value_is_a( + object_or_class: *mut RuntimeCell, + target_ptr: *const u8, + target_len: u64, + exclude_self: u64, + ) -> u64; fn __elephc_eval_value_object_class_name(object: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_parent_class_name(object_or_class: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; @@ -357,6 +363,23 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_interface_exists(name.as_ptr(), name.len() as u64) != 0 }) } + /// Tests a boxed Mixed object against generated class/interface metadata. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + Ok(unsafe { + __elephc_eval_value_is_a( + object_or_class.as_ptr(), + target_class.as_ptr(), + target_class.len() as u64, + u64::from(exclude_self), + ) != 0 + }) + } + /// Returns a boxed Mixed string naming a boxed Mixed object's runtime class. fn object_class_name( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 9b936140f5..76e697a6f0 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index ddbefe30d9..57a12fe6e9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index eb23e39e7a..3a1edfbe04 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -162,6 +162,44 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the interface-exists wrapper frame emitter.instruction("ret"); // return the interface-exists flag to Rust + label_c_global(emitter, "__elephc_eval_value_is_a"); + emitter.instruction("sub sp, sp, #64"); // reserve relation lookup state and preserve the Rust return address + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across runtime match helpers + emitter.instruction("add x29, sp, #48"); // establish a stable is-a relation frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed eval object-or-class cell + emitter.instruction("str x3, [sp, #8]"); // save whether exact class matches should be rejected + emitter.instruction("bl __rt_instanceof_lookup"); // resolve the target class/interface string to matcher metadata + emitter.instruction("cmp x0, #0"); // did the target string resolve to emitted metadata? + emitter.instruction("b.eq __elephc_eval_value_is_a_false"); // unresolved targets cannot match eval object values + emitter.instruction("str x1, [sp, #16]"); // save the target class/interface id + emitter.instruction("str x2, [sp, #24]"); // save the target kind: 0 class, 1 interface + emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed eval value for unboxing + emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and payload words + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the eval value is an object + emitter.instruction("b.ne __elephc_eval_value_is_a_false"); // non-object values do not satisfy class relations + emitter.instruction("mov x9, x1"); // keep the unboxed object pointer for matcher input + emitter.instruction("cbz x9, __elephc_eval_value_is_a_false"); // malformed object payloads cannot match class metadata + emitter.instruction("ldr x10, [sp, #8]"); // reload the exact-self exclusion flag + emitter.instruction("cbz x10, __elephc_eval_value_is_a_match"); // is_a() allows exact class matches + emitter.instruction("ldr x11, [sp, #24]"); // reload target kind before exact-class filtering + emitter.instruction("cbnz x11, __elephc_eval_value_is_a_match"); // interface targets cannot be exact concrete-class self matches + emitter.instruction("ldr x12, [x9]"); // load the object's concrete runtime class id + emitter.instruction("ldr x13, [sp, #16]"); // reload the target concrete class id + emitter.instruction("cmp x12, x13"); // compare object and target class ids for subclass self exclusion + emitter.instruction("b.eq __elephc_eval_value_is_a_false"); // is_subclass_of() excludes the object's exact class + emitter.label("__elephc_eval_value_is_a_match"); + emitter.instruction("mov x0, x9"); // pass the unboxed object pointer to the metadata matcher + emitter.instruction("ldr x1, [sp, #16]"); // pass the target class/interface id + emitter.instruction("ldr x2, [sp, #24]"); // pass the target kind: 0 class, 1 interface + emitter.instruction("bl __rt_exception_matches"); // test inheritance or implemented-interface metadata + emitter.instruction("b __elephc_eval_value_is_a_done"); // keep the matcher result and restore the wrapper frame + emitter.label("__elephc_eval_value_is_a_false"); + emitter.instruction("mov x0, #0"); // return false for unresolved, scalar, or exact-self subclass cases + emitter.label("__elephc_eval_value_is_a_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the relation lookup frame + emitter.instruction("ret"); // return the boolean class-relation result to Rust + label_c_global(emitter, "__elephc_eval_value_object_class_name"); emitter.instruction("cbz x0, __elephc_eval_value_object_class_name_miss"); // reject null boxed handles before reading their tag emitter.instruction("ldr x9, [x0]"); // load the boxed eval value runtime tag @@ -1441,6 +1479,45 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the interface-exists flag to Rust + label_c_global(emitter, "__elephc_eval_value_is_a"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime match helpers + emitter.instruction("mov rbp, rsp"); // establish a stable is-a relation frame pointer + emitter.instruction("sub rsp, 48"); // reserve slots for value pointer, flags, and target metadata + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed eval object-or-class cell + emitter.instruction("mov QWORD PTR [rbp - 16], rcx"); // save whether exact class matches should be rejected + emitter.instruction("mov rax, rsi"); // move the target string pointer into the lookup ABI register + emitter.instruction("call __rt_instanceof_lookup"); // resolve the target class/interface string to matcher metadata + emitter.instruction("test rax, rax"); // did the target string resolve to emitted metadata? + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // unresolved targets cannot match eval object values + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the target class/interface id + emitter.instruction("mov QWORD PTR [rbp - 32], rdx"); // save the target kind: 0 class, 1 interface + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the boxed eval value for unboxing + emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and payload words + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the eval value is an object + emitter.instruction("jne __elephc_eval_value_is_a_false_x86"); // non-object values do not satisfy class relations + emitter.instruction("test rdi, rdi"); // check the unboxed object pointer before reading its header + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // malformed object payloads cannot match class metadata + emitter.instruction("mov r8, rdi"); // keep the unboxed object pointer for matcher input + emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // does this call reject exact concrete-class matches? + emitter.instruction("je __elephc_eval_value_is_a_match_x86"); // is_a() allows exact class matches + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // is the target a concrete class rather than an interface? + emitter.instruction("jne __elephc_eval_value_is_a_match_x86"); // interface targets cannot be exact concrete-class self matches + emitter.instruction("mov r9, QWORD PTR [r8]"); // load the object's concrete runtime class id + emitter.instruction("cmp r9, QWORD PTR [rbp - 24]"); // compare object and target class ids for subclass self exclusion + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // is_subclass_of() excludes the object's exact class + emitter.label("__elephc_eval_value_is_a_match_x86"); + emitter.instruction("mov rdi, r8"); // pass the unboxed object pointer to the metadata matcher + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the target class/interface id + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // pass the target kind: 0 class, 1 interface + emitter.instruction("call __rt_exception_matches"); // test inheritance or implemented-interface metadata + emitter.instruction("jmp __elephc_eval_value_is_a_done_x86"); // keep the matcher result and restore the wrapper frame + emitter.label("__elephc_eval_value_is_a_false_x86"); + emitter.instruction("xor eax, eax"); // return false for unresolved, scalar, or exact-self subclass cases + emitter.label("__elephc_eval_value_is_a_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard relation lookup spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boolean class-relation result to Rust + label_c_global(emitter, "__elephc_eval_value_object_class_name"); emitter.instruction("test rdi, rdi"); // reject null boxed handles before reading their tag emitter.instruction("jz __elephc_eval_value_object_class_name_miss_x86"); // null handles cannot provide a class name diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4ec59540bf..c5d1399a79 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4373,6 +4373,31 @@ echo function_exists("interface_exists");'); assert_eq!(out, "YYYNYY1"); } +/// Verifies eval `is_a()` and `is_subclass_of()` use generated AOT relation metadata. +#[test] +fn test_eval_fragment_is_a_relation_probes_aot_metadata() { + let out = compile_and_run( + r#" $object, "class" => "EvalRelationParent"]) ? "Y" : "N"; +echo is_a(object_or_class: $object, class: "MissingEvalRelation", allow_string: false) ? "Y" : "N"; +echo function_exists("is_a"); echo function_exists("is_subclass_of");'); +"#, + ); + assert_eq!(out, "YYYNYYYYN11"); +} + /// Verifies duplicate eval-declared functions fail through the runtime bridge. #[test] fn test_eval_duplicate_declared_function_fails() { From e5ba0c3b9f3fb5b2e7382f8ccb6e9d07259bcc7d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 21:01:07 +0200 Subject: [PATCH 0263/1208] Support eval resource introspection builtins --- crates/elephc-eval/src/interpreter.rs | 66 +++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 16 +++++++ 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index f5709ecd57..995a328fd1 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1863,6 +1863,9 @@ fn eval_positional_expr_call( "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), "get_class" => eval_builtin_get_class(args, context, scope, values), "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), + "get_resource_id" | "get_resource_type" => { + eval_builtin_resource_introspection(name, args, context, scope, values) + } "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), @@ -2409,6 +2412,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "getservbyport" | "get_class" | "get_parent_class" + | "get_resource_id" + | "get_resource_type" | "getcwd" | "getenv" | "gettype" @@ -2739,6 +2744,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "getprotobynumber" => Some(&["protocol"]), "getservbyname" => Some(&["service", "protocol"]), "getservbyport" => Some(&["port", "protocol"]), + "get_resource_id" | "get_resource_type" => Some(&["resource"]), "getcwd" => Some(&[]), "getenv" => Some(&["name"]), "glob" => Some(&["pattern"]), @@ -3806,6 +3812,12 @@ fn eval_builtin_with_values( }; eval_get_parent_class_result(*object_or_class, values)? } + "get_resource_id" | "get_resource_type" => { + let [resource] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_resource_introspection_result(name, *resource, values)? + } "gettype" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -12614,6 +12626,37 @@ fn eval_get_parent_class_result( values.parent_class_name(object_or_class) } +/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. +fn eval_builtin_resource_introspection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [resource] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let resource = eval_expr(resource, context, scope, values)?; + eval_resource_introspection_result(name, resource, values) +} + +/// Evaluates a materialized resource introspection builtin argument. +fn eval_resource_introspection_result( + name: &str, + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + match name { + "get_resource_type" => values.string("stream"), + "get_resource_id" => values.cast_int(resource), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Returns the PHP-visible type name for a concrete eval runtime tag. fn eval_gettype_name(tag: u64) -> &'static str { match tag { @@ -21160,6 +21203,29 @@ return call_user_func("is_resource", $handle);"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval resource introspection builtins expose stream type and one-based id. + #[test] + fn execute_program_dispatches_resource_introspection_builtins() { + let program = parse_fragment( + br#"echo get_resource_type($handle); +echo ":" . get_resource_id($handle); +echo ":" . call_user_func("get_resource_type", $handle); +echo ":" . call_user_func_array("get_resource_id", ["resource" => $handle]); +echo ":" . function_exists("get_resource_type"); +return function_exists("get_resource_id");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stream:7:stream:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval cast builtins return boxed scalar cells directly and by callable. #[test] fn execute_program_dispatches_cast_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 76e697a6f0..962029a652 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 57a12fe6e9..8d792fd6d7 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c5d1399a79..e91fc4382b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3135,6 +3135,22 @@ echo function_exists("is_nan"); echo function_exists("is_finite"); echo function assert_eq!(out, "1111111111111Tok11111NBROoNIiFfH:1111t11OoNF11111111"); } +/// Verifies eval resource introspection builtins inspect boxed runtime resources. +#[test] +fn test_eval_dispatches_resource_introspection_builtin_calls() { + let out = compile_and_run( + r#" 0 ? "id" : "bad"; +echo ":"; echo call_user_func("get_resource_type", $h); +echo ":"; echo call_user_func_array("get_resource_id", ["resource" => $h]) > 0 ? "id" : "bad"; +echo ":"; echo function_exists("get_resource_type"); echo function_exists("get_resource_id");'); +"#, + ); + assert_eq!(out, "stream:id:stream:id:11"); +} + /// Verifies eval scalar cast builtins return boxed Mixed cells through direct and callable calls. #[test] fn test_eval_dispatches_cast_builtin_calls() { From 4cc1673bd7a8a341dd3f042e95b5b713ca655025 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 21:11:22 +0200 Subject: [PATCH 0264/1208] Support eval sscanf builtin --- crates/elephc-eval/src/interpreter.rs | 180 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 19 +++ 4 files changed, 202 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 995a328fd1..24571d33a4 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1942,6 +1942,7 @@ fn eval_positional_expr_call( "sleep" => eval_builtin_sleep(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "spl_classes" => eval_builtin_spl_classes(args, values), + "sscanf" => eval_builtin_sscanf(args, context, scope, values), "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "tempnam" => eval_builtin_tempnam(args, context, scope, values), @@ -2527,6 +2528,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "sort" | "sqrt" | "spl_classes" + | "sscanf" | "sprintf" | "strcasecmp" | "stream_get_filters" @@ -2796,6 +2798,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), "spl_classes" => Some(&[]), + "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), @@ -3507,6 +3510,12 @@ fn eval_builtin_with_values( } eval_spl_classes_result(values)? } + "sscanf" => { + let [input, format, ..] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sscanf_result(*input, *format, values)? + } "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, "strrev" => { let [value] = evaluated_args else { @@ -7021,6 +7030,24 @@ fn eval_builtin_vsprintf_like( eval_vsprintf_like_result(name, &evaluated_args, values) } +/// Evaluates direct positional `sscanf()` calls in source order. +fn eval_builtin_sscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let input = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_sscanf_result(input, format, values) +} + /// Dispatches already evaluated `sprintf()` or `printf()` arguments. fn eval_sprintf_like_result( name: &str, @@ -7047,6 +7074,135 @@ fn eval_vsprintf_like_result( } } +/// Parses one string through the eval `sscanf()` subset and returns an indexed array. +fn eval_sscanf_result( + input: RuntimeCellHandle, + format: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(input)?; + let format = values.string_bytes(format)?; + let matches = eval_sscanf_matches(&input, &format); + let mut result = values.array_new(matches.len())?; + for (index, matched) in matches.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(matched)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Extracts `%d`, `%f`, `%s`, and `%%` matches with the same subset as native `sscanf()`. +fn eval_sscanf_matches(input: &[u8], format: &[u8]) -> Vec> { + let mut matches = Vec::new(); + let mut input_index = 0; + let mut format_index = 0; + + while format_index < format.len() { + if format[format_index] != b'%' { + if input_index >= input.len() || input[input_index] != format[format_index] { + break; + } + input_index += 1; + format_index += 1; + continue; + } + + format_index += 1; + if format_index >= format.len() { + break; + } + + match format[format_index] { + b'%' => { + if input_index >= input.len() || input[input_index] != b'%' { + break; + } + input_index += 1; + } + b'd' => matches.push(eval_sscanf_scan_int(input, &mut input_index)), + b'f' => matches.push(eval_sscanf_scan_float(input, &mut input_index)), + b's' => matches.push(eval_sscanf_scan_word(input, &mut input_index)), + _ => {} + } + format_index += 1; + } + + matches +} + +/// Scans the native `sscanf()` `%d` subset as a matched byte slice. +fn eval_sscanf_scan_int(input: &[u8], input_index: &mut usize) -> Vec { + let start = *input_index; + if input.get(*input_index) == Some(&b'-') { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%f` subset as a matched byte slice. +fn eval_sscanf_scan_float(input: &[u8], input_index: &mut usize) -> Vec { + let start = *input_index; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + if input.get(*input_index) == Some(&b'.') { + *input_index += 1; + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'e' | b'E')) + { + *input_index += 1; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%s` subset as a non-space byte word. +fn eval_sscanf_scan_word(input: &[u8], input_index: &mut usize) -> Vec { + let start = *input_index; + while input + .get(*input_index) + .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\n')) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} + /// Formats `sprintf()` arguments and returns the resulting PHP string. fn eval_sprintf_result( evaluated_args: &[RuntimeCellHandle], @@ -21520,6 +21676,30 @@ return is_callable("vprintf");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval `sscanf()` returns indexed string matches through callable paths. + #[test] + fn execute_program_dispatches_sscanf_builtin() { + let program = parse_fragment( + br#"$result = sscanf("John 1.5 30", "%s %f %d"); +echo $result[0] . ":" . $result[1] . ":" . $result[2] . ":"; +$named = sscanf(string: "Age: -25", format: "Age: %d"); +echo $named[0] . ":"; +$call = call_user_func("sscanf", "-2.5e3", "%f"); +echo $call[0] . ":"; +$spread = call_user_func_array("sscanf", ["string" => "ok %", "format" => "%s %%"]); +echo $spread[0] . ":"; +return function_exists("sscanf");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "John:1.5:30:-25:-2.5e3:ok:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval `min()` and `max()` select numeric values directly and by callable. #[test] fn execute_program_dispatches_min_max_builtins() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 962029a652..c45e60b2fa 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 8d792fd6d7..a30698b7d9 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. @@ -132,7 +132,7 @@ Eval-declared function calls, registered AOT global user-function calls, and sup Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()` and printf-family formatting through `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, and `random_int()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. +Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, printf-family formatting through `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, string scanning through `sscanf()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, and `random_int()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. `php_uname()` supports PHP's standard one-character modes: diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e91fc4382b..470e7e3253 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3520,6 +3520,25 @@ echo function_exists("sprintf"); echo is_callable("printf"); echo function_exist ); } +/// Verifies eval `sscanf()` returns indexed string matches through direct and callable paths. +#[test] +fn test_eval_dispatches_sscanf_builtin_call() { + let out = compile_and_run( + r#" "ok %", "format" => "%s %%"]); +echo $spread[0] . ":"; +echo function_exists("sscanf");'); +"#, + ); + assert_eq!(out, "John:1.5:30:-25:-2.5e3:ok:1"); +} + /// Verifies eval `min()` and `max()` select numeric values directly and through callables. #[test] fn test_eval_dispatches_min_max_builtin_calls() { From b38541abedb6bee0f3cf99e709b2c493903a66bd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 21:18:02 +0200 Subject: [PATCH 0265/1208] Support eval settype builtin --- crates/elephc-eval/src/interpreter.rs | 159 ++++++++++++++++++++++++++ docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 17 +++ 4 files changed, 178 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 24571d33a4..c9014ffc90 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1684,6 +1684,7 @@ fn eval_call( | "rsort" | "shuffle" | "sort" + | "settype" | "uasort" | "uksort" | "usort" @@ -2520,6 +2521,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "round" | "rmdir" | "scandir" + | "settype" | "sleep" | "sha1" | "shuffle" @@ -2708,6 +2710,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => { Some(&["value"]) } + "settype" => Some(&["var", "type"]), "get_class" => Some(&["object"]), "get_parent_class" => Some(&["object_or_class"]), "call_user_func" => Some(&["callback"]), @@ -3517,6 +3520,12 @@ fn eval_builtin_with_values( eval_sscanf_result(*input, *format, values)? } "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, + "settype" => { + let [value, type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_settype_value_result(*value, *type_name, values)? + } "strrev" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -4489,6 +4498,119 @@ fn eval_array_walk_result( values.bool_value(true) } +/// Evaluates direct by-reference `settype()` calls and writes the converted cell back. +fn eval_builtin_settype_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (var_name, type_name) = eval_settype_direct_args(args, context, scope, values)?; + let value = visible_scope_cell(context, scope, &var_name).map_or_else(|| values.null(), Ok)?; + let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { + return values.bool_value(false); + }; + for replaced in set_scope_cell( + context, + scope, + var_name, + converted, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + values.bool_value(true) +} + +/// Evaluates and binds direct `settype()` arguments while preserving source order. +fn eval_settype_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(String, RuntimeCellHandle), EvalStatus> { + let mut var_name = None; + let mut type_name = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "var", + 1 => "type", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "var" => { + if var_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + var_name = Some(name.clone()); + } + "type" => { + if type_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + type_name = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let var_name = var_name.ok_or(EvalStatus::RuntimeFatal)?; + let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; + Ok((var_name, type_name)) +} + +/// Applies the eval-supported `settype()` scalar target conversion. +fn eval_settype_cast_value( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let type_name = values.string_bytes(type_name)?; + let type_name = String::from_utf8_lossy(&type_name).to_ascii_lowercase(); + let converted = match type_name.as_str() { + "bool" | "boolean" => Some(values.cast_bool(value)?), + "float" | "double" => Some(values.cast_float(value)?), + "int" | "integer" => Some(values.cast_int(value)?), + "string" => Some(values.cast_string(value)?), + _ => None, + }; + Ok(converted) +} + +/// Evaluates by-value `settype()` callable dispatch without mutating the source argument. +fn eval_settype_value_result( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.warning("settype(): Argument #1 ($var) must be passed by reference, value given")?; + if let Some(converted) = eval_settype_cast_value(value, type_name, values)? { + values.release(converted)?; + return values.bool_value(true); + } + values.bool_value(false) +} + /// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. fn eval_builtin_array_pop_shift_call( name: &str, @@ -4497,6 +4619,9 @@ fn eval_builtin_array_pop_shift_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { + if name == "settype" { + return eval_builtin_settype_call(args, context, scope, values); + } if matches!(name, "array_push" | "array_unshift") { return eval_builtin_array_push_unshift_call(name, args, context, scope, values); } @@ -21403,6 +21528,40 @@ return call_user_func_array("intval", ["9"]);"#, assert_eq!(values.get(result), FakeValue::Int(9)); } + /// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. + #[test] + fn execute_program_dispatches_settype_builtin() { + let program = parse_fragment( + br#"$x = 42; +echo settype($x, "string") ? gettype($x) . ":" . $x : "bad"; +echo ":"; +$y = "0"; +echo settype(type: "bool", var: $y) ? gettype($y) . ":" . ($y ? "true" : "false") : "bad"; +echo ":"; +echo settype($missing, "integer") ? gettype($missing) . ":" . $missing : "bad"; +echo ":"; +$z = 3.8; +echo call_user_func("settype", $z, "integer") ? gettype($z) . ":" . $z : "bad"; +echo ":"; +return function_exists("settype");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "string:42:boolean:false:integer:0:double:3.8:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + ["settype(): Argument #1 ($var) must be passed by reference, value given"] + ); + } + /// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. #[test] fn execute_program_dispatches_gettype_builtin() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c45e60b2fa..e52a1d4613 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index a30698b7d9..934a2a6917 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 470e7e3253..640e87ea55 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3168,6 +3168,23 @@ echo ":"; echo function_exists("boolval");'); assert_eq!(out, "42:3.5:12:false:7:9:1"); } +/// Verifies eval `settype()` mutates direct variables and supports named arguments. +#[test] +fn test_eval_dispatches_settype_builtin_call() { + let out = compile_and_run( + r#" Date: Wed, 17 Jun 2026 21:26:17 +0200 Subject: [PATCH 0266/1208] Support eval SPL object identity builtins --- crates/elephc-eval/src/interpreter.rs | 84 ++++++++++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 12 ++++ docs/internals/the-runtime.md | 5 +- docs/php/system-and-io.md | 4 +- src/codegen_support/runtime/eval_bridge.rs | 26 +++++++ tests/codegen/eval.rs | 25 +++++++ 6 files changed, 154 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index c9014ffc90..4d9f0eab0f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -421,6 +421,9 @@ pub trait RuntimeValueOps { /// Returns the concrete boxed Mixed runtime tag after unwrapping nested Mixed cells. fn type_tag(&mut self, value: RuntimeCellHandle) -> Result; + /// Returns the unboxed object payload pointer used for PHP object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result; + /// Releases one owned runtime cell that is no longer held by the eval scope. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; @@ -1943,6 +1946,9 @@ fn eval_positional_expr_call( "sleep" => eval_builtin_sleep(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), "spl_classes" => eval_builtin_spl_classes(args, values), + "spl_object_id" | "spl_object_hash" => { + eval_builtin_spl_object_identity(name, args, context, scope, values) + } "sscanf" => eval_builtin_sscanf(args, context, scope, values), "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), @@ -2530,6 +2536,8 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "sort" | "sqrt" | "spl_classes" + | "spl_object_hash" + | "spl_object_id" | "sscanf" | "sprintf" | "strcasecmp" @@ -2801,6 +2809,7 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), "spl_classes" => Some(&[]), + "spl_object_id" | "spl_object_hash" => Some(&["object"]), "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), @@ -3513,6 +3522,12 @@ fn eval_builtin_with_values( } eval_spl_classes_result(values)? } + "spl_object_id" | "spl_object_hash" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_spl_object_identity_result(name, *object, values)? + } "sscanf" => { let [input, format, ..] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -12885,6 +12900,38 @@ fn eval_get_class_result( values.object_class_name(object) } +/// Evaluates PHP's SPL object identity builtins over one eval object expression. +fn eval_builtin_spl_object_identity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_spl_object_identity_result(name, object, values) +} + +/// Returns the unboxed object-payload identity in the native SPL builtin spelling. +fn eval_spl_object_identity_result( + name: &str, + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)? as i64; + match name { + "spl_object_id" => values.int(identity), + "spl_object_hash" => values.string(&identity.to_string()), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. fn eval_builtin_get_parent_class( args: &[EvalExpr], @@ -16039,6 +16086,14 @@ mod tests { }) } + /// Returns the fake object handle as a stable object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + match self.get(object) { + FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), + _ => Err(EvalStatus::RuntimeFatal), + } + } + /// Records fake releases without freeing handles needed for assertions. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { self.releases.push(value); @@ -20451,6 +20506,35 @@ return is_callable("spl_classes");"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } + /// Verifies eval SPL object identity builtins are stable, unique, and callable. + #[test] + fn execute_program_dispatches_spl_object_identity_builtins() { + let program = parse_fragment( + br#"$a = new KnownClass(); +$b = new KnownClass(); +echo (spl_object_id($a) === spl_object_id($a)) ? "stable" : "drift"; +echo ":"; +echo (spl_object_id($a) !== spl_object_id($b)) ? "unique" : "same"; +echo ":"; +echo (spl_object_hash(object: $a) === spl_object_hash($a)) ? "hash" : "bad"; +echo ":"; +echo (call_user_func("spl_object_id", $a) === spl_object_id($a)) ? "call" : "bad"; +echo ":"; +echo (call_user_func_array("spl_object_hash", ["object" => $b]) === spl_object_hash($b)) ? "array" : "bad"; +echo ":"; +echo function_exists("spl_object_id"); +return function_exists("spl_object_hash");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stable:unique:hash:call:array:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + /// Verifies eval environment builtins read, write, unset, and dispatch dynamically. #[test] fn execute_program_dispatches_environment_builtins() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index a5797450b0..cdf45a3c1d 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -82,6 +82,8 @@ unsafe extern "C" { fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_type_tag(value: *mut RuntimeCell) -> u64; + /// Returns the unboxed object payload pointer for object-tagged eval values. + fn __elephc_eval_value_object_identity(value: *mut RuntimeCell) -> u64; fn __elephc_eval_warning(message_ptr: *const u8, message_len: u64); fn __elephc_eval_value_null() -> *mut RuntimeCell; fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; @@ -417,6 +419,16 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_type_tag(value.as_ptr()) }) } + /// Returns the unboxed object payload pointer for SPL object identity builtins. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + let identity = unsafe { __elephc_eval_value_object_identity(object.as_ptr()) }; + if identity == 0 { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(identity) + } + } + /// Releases one boxed Mixed cell through the generated runtime wrapper. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { unsafe { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e52a1d4613..fb49adcfb4 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -39,7 +39,10 @@ Eval `var_dump()` formats scalar tags and array contents inside the interpreter, Eval SPL class introspection dispatch includes `spl_classes()` through direct calls, callable dispatch, and `function_exists()` probes. The interpreter builds -the same static class-name snapshot as the native SPL builtin. +the same static class-name snapshot as the native SPL builtin. Eval SPL object +identity dispatch includes `spl_object_id()` and `spl_object_hash()` by deriving +the integer or decimal-string identity from the unboxed object payload after the +normal runtime tag check. Eval `ucwords()` performs ASCII word-start capitalization and honors the optional `separators` argument. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 934a2a6917..b342c45260 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -110,7 +110,9 @@ Eval stream introspection calls include `stream_get_filters()`, `stream_get_tran Eval SPL class introspection supports `spl_classes()` with direct, callable, and `function_exists()` dispatch paths. It returns the same static registry snapshot -documented in [SPL](spl.md). +documented in [SPL](spl.md). Eval SPL object identity supports +`spl_object_id()` and `spl_object_hash()` for object cells through direct, +named-argument, callable, and `function_exists()` paths. Eval host-name calls include `gethostname()`, `gethostbyname()`, and `gethostbyaddr()` with direct, named-argument, callable, and `function_exists()` dispatch paths. `gethostbyname()` resolves IPv4 host names and returns the original string when no IPv4 address is available. `gethostbyaddr()` reverse-resolves IPv4 dotted-quad addresses, returns the original address when no record exists, and returns `false` for malformed addresses. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 3a1edfbe04..60064d73b8 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -658,6 +658,17 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the type-tag wrapper frame emitter.instruction("ret"); // return the unboxed runtime tag to Rust + label_c_global(emitter, "__elephc_eval_value_object_identity"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the object cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a stable object-identity wrapper frame + emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means PHP object + emitter.instruction("csel x0, x1, xzr, eq"); // return the object payload pointer or zero on mismatch + emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the object-identity wrapper frame + emitter.instruction("ret"); // return the object identity pointer to Rust + label_c_global(emitter, "__elephc_eval_value_cast_int"); emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while casting and boxing the value emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across helper calls @@ -1992,6 +2003,21 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the unboxed runtime tag to Rust + label_c_global(emitter, "__elephc_eval_value_object_identity"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable object-identity wrapper frame + emitter.instruction("mov rax, rdi"); // pass the boxed Mixed argument to mixed_unbox + emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means PHP object + emitter.instruction("je __elephc_eval_value_object_identity_object_x86"); // return the payload pointer for object values + emitter.instruction("xor eax, eax"); // return zero for non-object values + emitter.instruction("jmp __elephc_eval_value_object_identity_done_x86"); // skip the object-payload result + emitter.label("__elephc_eval_value_object_identity_object_x86"); + emitter.instruction("mov rax, rdi"); // return the unboxed object payload pointer + emitter.label("__elephc_eval_value_object_identity_done_x86"); + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the object identity pointer to Rust + label_c_global(emitter, "__elephc_eval_value_cast_int"); emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 640e87ea55..4772ada35c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3185,6 +3185,31 @@ echo function_exists("settype");'); assert_eq!(out, "string:42:boolean:false:1"); } +/// Verifies eval SPL object identity builtins inspect AOT object cells. +#[test] +fn test_eval_dispatches_spl_object_identity_builtin_calls() { + let out = compile_and_run( + r#" $b]) === spl_object_hash($b)) ? "array" : "bad"; +echo ":"; +echo function_exists("spl_object_id"); echo function_exists("spl_object_hash");'); +"#, + ); + assert_eq!(out, "stable:unique:hash:call:array:11"); +} + /// Verifies eval `gettype()` maps boxed Mixed runtime tags to PHP type names. #[test] fn test_eval_dispatches_gettype_builtin_call() { From 7e1bd575a809f6ecd01b31c55cc77b00ca7d3971 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 21:38:26 +0200 Subject: [PATCH 0267/1208] Support eval trait and enum probes --- crates/elephc-eval/src/interpreter.rs | 101 +++++++++++++++ crates/elephc-eval/src/runtime_hooks.rs | 14 ++ docs/internals/the-runtime.md | 5 + docs/php/system-and-io.md | 5 + src/codegen/mod.rs | 1 + src/codegen_support/runtime/data/user.rs | 52 +++++++- src/codegen_support/runtime/eval_bridge.rs | 141 +++++++++++++++++++++ tests/codegen/eval.rs | 24 ++++ 8 files changed, 340 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 4d9f0eab0f..0b2824b134 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -389,6 +389,12 @@ pub trait RuntimeValueOps { /// Returns whether a runtime interface table contains the requested interface name. fn interface_exists(&mut self, name: &str) -> Result; + /// Returns whether a runtime trait table contains the requested trait name. + fn trait_exists(&mut self, name: &str) -> Result; + + /// Returns whether a runtime enum table contains the requested enum name. + fn enum_exists(&mut self, name: &str) -> Result; + /// Tests whether a boxed object cell satisfies a class/interface relation. fn object_is_a( &mut self, @@ -1820,6 +1826,9 @@ fn eval_positional_expr_call( "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "class_exists" => eval_builtin_class_exists(args, context, scope, values), "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), + "trait_exists" | "enum_exists" => { + eval_builtin_class_like_exists(name, args, context, scope, values) + } "is_a" | "is_subclass_of" => eval_builtin_is_a_relation(name, args, context, scope, values), "chop" => eval_builtin_trim_like(name, args, context, scope, values), "boolval" | "floatval" | "intval" | "strval" => { @@ -2200,6 +2209,57 @@ fn eval_interface_exists_name( values.interface_exists(name.trim_start_matches('\\')) } +/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. +fn eval_builtin_class_like_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = match args { + [symbol] => eval_expr(symbol, context, scope, values)?, + [symbol, autoload] => { + let symbol = eval_expr(symbol, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + symbol + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_like_exists_name(name, symbol, values)?; + values.bool_value(exists) +} + +/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. +fn eval_class_like_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [symbol] => eval_class_like_exists_name(name, *symbol, values)?, + [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. +fn eval_class_like_exists_name( + name: &str, + symbol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = values.string_bytes(symbol)?; + let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; + let symbol = symbol.trim_start_matches('\\'); + match name { + "trait_exists" => values.trait_exists(symbol), + "enum_exists" => values.enum_exists(symbol), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. fn eval_builtin_is_a_relation( name: &str, @@ -2366,6 +2426,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "call_user_func" | "call_user_func_array" | "class_exists" + | "enum_exists" | "interface_exists" | "is_a" | "is_subclass_of" @@ -2572,6 +2633,7 @@ fn eval_php_visible_builtin_exists(name: &str) -> bool { | "tanh" | "time" | "touch" + | "trait_exists" | "trim" | "substr_replace" | "ucfirst" @@ -2724,7 +2786,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_exists" => Some(&["class", "autoload"]), + "enum_exists" => Some(&["enum", "autoload"]), "interface_exists" => Some(&["interface", "autoload"]), + "trait_exists" => Some(&["trait", "autoload"]), "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), "chmod" => Some(&["filename", "permissions"]), @@ -3722,6 +3786,9 @@ fn eval_builtin_with_values( values.bool_value(eval_function_probe_exists(context, &name))? } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "enum_exists" | "trait_exists" => { + eval_class_like_exists_result(name, evaluated_args, values)? + } "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, "is_a" | "is_subclass_of" => eval_is_a_relation_result(name, evaluated_args, values)?, "json_decode" => match evaluated_args { @@ -16007,6 +16074,16 @@ mod tests { Ok(name.eq_ignore_ascii_case("KnownInterface")) } + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + fn trait_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownTrait")) + } + + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + fn enum_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownEnum")) + } + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. fn object_is_a( &mut self, @@ -22418,6 +22495,30 @@ echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N assert_eq!(values.output, "YYNYYN"); } + /// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. + #[test] + fn execute_program_class_like_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo trait_exists("KnownTrait") ? "T" : "t"; +echo trait_exists("knowntrait") ? "T" : "t"; +echo trait_exists("KnownEnum") ? "T" : "t"; +echo enum_exists("KnownEnum") ? "E" : "e"; +echo enum_exists("\knownenum") ? "E" : "e"; +echo enum_exists("KnownTrait") ? "E" : "e"; +echo call_user_func("trait_exists", "KnownTrait") ? "T" : "t"; +echo call_user_func_array("enum_exists", ["enum" => "KnownEnum"]) ? "E" : "e"; +echo trait_exists(trait: "MissingTrait", autoload: false) ? "T" : "t"; +echo enum_exists(enum: "MissingEnum", autoload: false) ? "E" : "e";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TTtEEeTEte"); + } + /// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. #[test] fn execute_program_is_a_relation_uses_runtime_probe() { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index cdf45a3c1d..ae1334fd08 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -78,6 +78,10 @@ unsafe extern "C" { ) -> u64; fn __elephc_eval_value_object_class_name(object: *mut RuntimeCell) -> *mut RuntimeCell; fn __elephc_eval_value_parent_class_name(object_or_class: *mut RuntimeCell) -> *mut RuntimeCell; + /// Returns whether generated trait metadata contains the requested PHP name. + fn __elephc_eval_trait_exists(name_ptr: *const u8, name_len: u64) -> u64; + /// Returns whether generated enum metadata contains the requested PHP name. + fn __elephc_eval_enum_exists(name_ptr: *const u8, name_len: u64) -> u64; fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; @@ -365,6 +369,16 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_interface_exists(name.as_ptr(), name.len() as u64) != 0 }) } + /// Returns whether the generated AOT trait-name table contains the requested trait. + fn trait_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_trait_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Returns whether the generated AOT enum-name table contains the requested enum. + fn enum_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_enum_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + /// Tests a boxed Mixed object against generated class/interface metadata. fn object_is_a( &mut self, diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index fb49adcfb4..57089ba538 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -44,6 +44,11 @@ identity dispatch includes `spl_object_id()` and `spl_object_hash()` by deriving the integer or decimal-string identity from the unboxed object payload after the normal runtime tag check. +Eval class-like metadata probe dispatch includes `trait_exists()` and +`enum_exists()` through small generated `(name_ptr, name_len)` tables in the +user runtime data section. The eval bridge scans those tables with the same +case-insensitive string comparison convention used by `class_exists()`. + Eval `ucwords()` performs ASCII word-start capitalization and honors the optional `separators` argument. Eval `strstr()` uses the shared byte-search helper to return the matching suffix, optional prefix, or PHP `false`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index b342c45260..6e23bf6a08 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -114,6 +114,11 @@ documented in [SPL](spl.md). Eval SPL object identity supports `spl_object_id()` and `spl_object_hash()` for object cells through direct, named-argument, callable, and `function_exists()` paths. +Eval class-like metadata probes include `trait_exists()` and `enum_exists()` for +generated AOT trait and enum names, with case-insensitive matching, optional +autoload argument evaluation, named arguments, callable dispatch, and +`function_exists()` visibility. + Eval host-name calls include `gethostname()`, `gethostbyname()`, and `gethostbyaddr()` with direct, named-argument, callable, and `function_exists()` dispatch paths. `gethostbyname()` resolves IPv4 host names and returns the original string when no IPv4 address is available. `gethostbyaddr()` reverse-resolves IPv4 dotted-quad addresses, returns the original address when no record exists, and returns `false` for malformed addresses. Eval protocol and service database calls include `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, and `getservbyport()` with direct, named-argument, callable, and `function_exists()` dispatch paths. diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 2f2fa6b27a..689da3405c 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -226,6 +226,7 @@ fn finalize_user_asm( &user_functions, &function_variant_groups, &runtime_interfaces, + &module.trait_table.names, &runtime_classes, &module.enum_infos, Some(&allowed_class_names), diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index ae22628320..b6394565a9 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -27,6 +27,7 @@ pub(crate) fn emit_runtime_data_user( functions: &HashMap, function_variant_groups: &HashSet, interfaces: &HashMap, + trait_names: &[String], classes: &HashMap, enums: &HashMap, allowed_class_names: Option<&HashSet>, @@ -76,14 +77,14 @@ pub(crate) fn emit_runtime_data_user( let mut sorted_enum_names: Vec<&String> = enums.keys().collect(); sorted_enum_names.sort(); - for enum_name in sorted_enum_names { - let Some(enum_info) = enums.get(enum_name) else { + for enum_name in &sorted_enum_names { + let Some(enum_info) = enums.get(*enum_name) else { continue; }; for case in &enum_info.cases { out.push_str(&format!( ".comm {}, 8, 3\n", - enum_case_symbol(enum_name, &case.name) + enum_case_symbol(*enum_name, &case.name) )); } } @@ -119,6 +120,21 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); super::instanceof::emit_instanceof_target_lookup_data(&mut out, &sorted_interfaces, &sorted_classes); emit_class_name_lookup_data(&mut out, max_class_id, &class_name_by_id); + emit_name_lookup_data( + &mut out, + "_trait_names_count", + "_trait_names", + "_trait_name", + trait_names, + ); + let enum_names: Vec = sorted_enum_names.iter().map(|name| (*name).clone()).collect(); + emit_name_lookup_data( + &mut out, + "_enum_names_count", + "_enum_names", + "_enum_name", + &enum_names, + ); // Per-program class id of the built-in `Fiber` class. The fiber runtime // checks this against the receiver's class_id in __rt_object_free_deep so @@ -880,6 +896,34 @@ fn emit_class_name_lookup_data( out.push_str(" .p2align 3\n"); } +/// Emits a compact `(name_ptr, name_len)` table for runtime class-like name probes. +fn emit_name_lookup_data( + out: &mut String, + count_symbol: &str, + table_symbol: &str, + label_prefix: &str, + names: &[String], +) { + let mut sorted_names: Vec<&String> = names.iter().collect(); + sorted_names.sort(); + for (idx, name) in sorted_names.iter().enumerate() { + out.push_str(&format!( + ".globl {0}_{1}\n{0}_{1}:\n .ascii \"{2}\"\n", + label_prefix, + idx, + escaped_ascii(name) + )); + } + out.push_str(".p2align 3\n"); + out.push_str(&format!(".globl {0}\n{0}:\n", count_symbol)); + out.push_str(&format!(" .quad {}\n", sorted_names.len())); + out.push_str(&format!(".globl {0}\n{0}:\n", table_symbol)); + for (idx, name) in sorted_names.iter().enumerate() { + out.push_str(&format!(" .quad {}_{}\n", label_prefix, idx)); + out.push_str(&format!(" .quad {}\n", name.len())); + } +} + /// Emits the callable-function name table and pointer table for user-defined functions. /// Each function name is emitted as an ASCII label; the pointer table references /// either the active variant symbol for polymorphic functions or zero. @@ -1407,6 +1451,7 @@ mod tests { &HashMap::new(), &HashSet::new(), &HashMap::new(), + &[], &classes, &HashMap::new(), Some(&allowed_class_names), @@ -1432,6 +1477,7 @@ mod tests { &HashMap::new(), &HashSet::new(), &HashMap::new(), + &[], &classes, &HashMap::new(), None, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 60064d73b8..d392a066e7 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -162,6 +162,21 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the interface-exists wrapper frame emitter.instruction("ret"); // return the interface-exists flag to Rust + emit_aarch64_eval_name_table_exists( + emitter, + "__elephc_eval_trait_exists", + "_trait_names_count", + "_trait_names", + "__elephc_eval_trait_exists", + ); + emit_aarch64_eval_name_table_exists( + emitter, + "__elephc_eval_enum_exists", + "_enum_names_count", + "_enum_names", + "__elephc_eval_enum_exists", + ); + label_c_global(emitter, "__elephc_eval_value_is_a"); emitter.instruction("sub sp, sp, #64"); // reserve relation lookup state and preserve the Rust return address emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across runtime match helpers @@ -1490,6 +1505,21 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the interface-exists flag to Rust + emit_x86_64_eval_name_table_exists( + emitter, + "__elephc_eval_trait_exists", + "_trait_names_count", + "_trait_names", + "__elephc_eval_trait_exists_x86", + ); + emit_x86_64_eval_name_table_exists( + emitter, + "__elephc_eval_enum_exists", + "_enum_names_count", + "_enum_names", + "__elephc_eval_enum_exists_x86", + ); + label_c_global(emitter, "__elephc_eval_value_is_a"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime match helpers emitter.instruction("mov rbp, rsp"); // establish a stable is-a relation frame pointer @@ -2789,6 +2819,117 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell } +/// Emits an AArch64 eval wrapper that scans one `(name_ptr, name_len)` metadata table. +fn emit_aarch64_eval_name_table_exists( + emitter: &mut Emitter, + exported_label: &str, + count_symbol: &str, + table_symbol: &str, + local_stem: &str, +) { + label_c_global(emitter, exported_label); + emitter.instruction("sub sp, sp, #64"); // reserve lookup state while comparing metadata names + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across string compares + emitter.instruction("add x29, sp, #48"); // establish a stable name-table lookup frame + emitter.instruction("str x0, [sp, #0]"); // save the requested name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested name length + abi::emit_symbol_address(emitter, "x9", count_symbol); + emitter.instruction("ldr x9, [x9]"); // load the metadata-name table count + emitter.instruction(&format!("cbz x9, {local_stem}_miss")); // an empty table cannot contain the requested name + emitter.instruction("str x9, [sp, #16]"); // save the table count across string compares + abi::emit_symbol_address(emitter, "x10", table_symbol); + emitter.instruction("str x10, [sp, #24]"); // save the current metadata-name table cursor + emitter.instruction("mov x11, #0"); // start scanning at table index zero + emitter.label(&format!("{local_stem}_loop")); + emitter.instruction("ldr x9, [sp, #16]"); // reload the metadata-name table count + emitter.instruction("cmp x11, x9"); // have all metadata-name entries been scanned? + emitter.instruction(&format!("b.ge {local_stem}_miss")); // no metadata name matched before the end + emitter.instruction("ldr x10, [sp, #24]"); // reload the current metadata-name table entry + emitter.instruction("ldr x12, [x10, #8]"); // load the stored metadata-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested name length + emitter.instruction("cmp x12, x2"); // compare stored and requested name lengths + emitter.instruction(&format!("b.ne {local_stem}_skip")); // length mismatch means this entry cannot match + emitter.instruction("str x11, [sp, #32]"); // save the table index across the string compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested name length + emitter.instruction("ldr x3, [x10]"); // pass the stored metadata-name pointer + emitter.instruction("mov x4, x12"); // pass the stored metadata-name length + emitter.instruction("bl __rt_strcasecmp"); // compare names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #32]"); // restore the table index after the string compare + emitter.instruction("cmp x0, #0"); // did the requested name match this entry? + emitter.instruction(&format!("b.eq {local_stem}_hit")); // report true on a metadata-name match + emitter.label(&format!("{local_stem}_skip")); + emitter.instruction("ldr x10, [sp, #24]"); // reload the current metadata-name table entry + emitter.instruction("add x10, x10, #16"); // advance to the next metadata-name table entry + emitter.instruction("str x10, [sp, #24]"); // persist the advanced table cursor + emitter.instruction("add x11, x11, #1"); // advance the table index + emitter.instruction(&format!("b {local_stem}_loop")); // continue scanning the metadata-name table + emitter.label(&format!("{local_stem}_hit")); + emitter.instruction("mov x0, #1"); // return true for a matched metadata name + emitter.instruction(&format!("b {local_stem}_done")); // skip the false result after a match + emitter.label(&format!("{local_stem}_miss")); + emitter.instruction("mov x0, #0"); // return false when no metadata name matched + emitter.label(&format!("{local_stem}_done")); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the name-table lookup frame + emitter.instruction("ret"); // return the metadata-name existence flag to Rust +} + +/// Emits an x86_64 eval wrapper that scans one `(name_ptr, name_len)` metadata table. +fn emit_x86_64_eval_name_table_exists( + emitter: &mut Emitter, + exported_label: &str, + count_symbol: &str, + table_symbol: &str, + local_stem: &str, +) { + label_c_global(emitter, exported_label); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable name-table lookup frame + emitter.instruction("sub rsp, 48"); // reserve slots for name, count, cursor, and index + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested name length + abi::emit_symbol_address(emitter, "r10", count_symbol); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the metadata-name table count + emitter.instruction("test r10, r10"); // is the metadata-name table empty? + emitter.instruction(&format!("jz {local_stem}_miss")); // an empty table cannot contain the requested name + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the table count across string compares + abi::emit_symbol_address(emitter, "r11", table_symbol); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current metadata-name table cursor + emitter.instruction("xor r11d, r11d"); // start scanning at table index zero + emitter.label(&format!("{local_stem}_loop")); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the metadata-name table count + emitter.instruction("cmp r11, r10"); // have all metadata-name entries been scanned? + emitter.instruction(&format!("jae {local_stem}_miss")); // no metadata name matched before the end + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current metadata-name table entry + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored metadata-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested name lengths + emitter.instruction(&format!("jne {local_stem}_skip")); // length mismatch means this entry cannot match + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the table index across the string compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored metadata-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // restore the table index after the string compare + emitter.instruction("test rax, rax"); // did the requested name match this entry? + emitter.instruction(&format!("je {local_stem}_hit")); // report true on a metadata-name match + emitter.label(&format!("{local_stem}_skip")); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current metadata-name table entry + emitter.instruction("add r10, 16"); // advance to the next metadata-name table entry + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the advanced table cursor + emitter.instruction("inc r11"); // advance the table index + emitter.instruction(&format!("jmp {local_stem}_loop")); // continue scanning the metadata-name table + emitter.label(&format!("{local_stem}_hit")); + emitter.instruction("mov eax, 1"); // return true for a matched metadata name + emitter.instruction(&format!("jmp {local_stem}_done")); // skip the false result after a match + emitter.label(&format!("{local_stem}_miss")); + emitter.instruction("xor eax, eax"); // return false when no metadata name matched + emitter.label(&format!("{local_stem}_done")); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the metadata-name existence flag to Rust +} + /// Emits a global label with platform C-symbol mangling. fn label_c_global(emitter: &mut Emitter, name: &str) { let symbol = emitter.target.extern_symbol(name); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4772ada35c..c42058db08 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4450,6 +4450,30 @@ echo function_exists("interface_exists");'); assert_eq!(out, "YYYNYY1"); } +/// Verifies eval `trait_exists()` and `enum_exists()` probe generated AOT metadata. +#[test] +fn test_eval_fragment_trait_enum_exists_probe_aot_metadata() { + let out = compile_and_run( + r#" false, "enum" => "\EvalEnumExistsProbe"]) ? "E" : "e"; +echo trait_exists(trait: "MissingEvalTrait", autoload: false) ? "T" : "t"; +echo enum_exists(enum: "MissingEvalEnum", autoload: false) ? "E" : "e"; +echo function_exists("trait_exists"); echo function_exists("enum_exists");'); +"#, + ); + assert_eq!(out, "TTtEEeTEte11"); +} + /// Verifies eval `is_a()` and `is_subclass_of()` use generated AOT relation metadata. #[test] fn test_eval_fragment_is_a_relation_probes_aot_metadata() { From dba8688deb464c3ac8cd224ed32f5e1083f560ba Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 21:52:25 +0200 Subject: [PATCH 0268/1208] Support eval declared class bodies --- crates/elephc-eval/src/context.rs | 41 +++-- crates/elephc-eval/src/eval_ir.rs | 110 ++++++++++++- crates/elephc-eval/src/interpreter.rs | 229 +++++++++++++++++++++++--- crates/elephc-eval/src/lib.rs | 6 +- crates/elephc-eval/src/parser.rs | 147 +++++++++++++++-- docs/internals/the-runtime.md | 4 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 27 ++- 8 files changed, 499 insertions(+), 69 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 79b7870e0c..e0d8f23634 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -15,7 +15,7 @@ use std::collections::{HashMap, HashSet}; use std::ffi::c_void; use crate::abi::ABI_VERSION; -use crate::eval_ir::EvalFunction; +use crate::eval_ir::{EvalClass, EvalFunction}; use crate::scope::ElephcEvalScope; use crate::value::{RuntimeCell, RuntimeCellHandle}; @@ -86,12 +86,13 @@ impl NativeFunction { /// grow dynamic registries without exposing them to generated assembly. pub struct ElephcEvalContext { abi_version: u32, - classes: HashSet, + classes: HashMap, constants: HashMap, functions: HashMap, native_functions: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, included_files: HashSet, + dynamic_objects: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, pending_throw: Option, @@ -108,12 +109,13 @@ impl ElephcEvalContext { pub fn new() -> Self { Self { abi_version: ABI_VERSION, - classes: HashSet::new(), + classes: HashMap::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), included_files: HashSet::new(), + dynamic_objects: HashMap::new(), global_scope: None, function_stack: Vec::new(), pending_throw: None, @@ -131,12 +133,13 @@ impl ElephcEvalContext { pub fn for_abi_version(abi_version: u32) -> Self { Self { abi_version, - classes: HashSet::new(), + classes: HashMap::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), included_files: HashSet::new(), + dynamic_objects: HashMap::new(), global_scope: None, function_stack: Vec::new(), pending_throw: None, @@ -154,19 +157,37 @@ impl ElephcEvalContext { self.abi_version } - /// Defines an eval-declared class name, failing if this context already has it. - pub fn define_class(&mut self, name: &str) -> bool { - let key = normalize_class_name(name); - if self.classes.contains(&key) { + /// Defines an eval-declared class, failing if this context already has it. + pub fn define_class(&mut self, class: EvalClass) -> bool { + let key = normalize_class_name(class.name()); + if self.classes.contains_key(&key) { return false; } - self.classes.insert(key); + self.classes.insert(key, class); true } /// Returns true when this eval context has a dynamic class with the requested name. pub fn has_class(&self, name: &str) -> bool { - self.classes.contains(&normalize_class_name(name)) + self.classes.contains_key(&normalize_class_name(name)) + } + + /// Returns a dynamic eval class by PHP case-insensitive class name. + pub fn class(&self, name: &str) -> Option<&EvalClass> { + self.classes.get(&normalize_class_name(name)) + } + + /// Records that one runtime object handle was created for an eval-declared class. + pub fn register_dynamic_object(&mut self, identity: u64, class_name: &str) { + self.dynamic_objects + .insert(identity, normalize_class_name(class_name)); + } + + /// Returns the dynamic eval class metadata associated with one object identity. + pub fn dynamic_object_class(&self, identity: u64) -> Option<&EvalClass> { + self.dynamic_objects + .get(&identity) + .and_then(|class_key| self.classes.get(class_key)) } /// Defines an eval dynamic constant value, failing if the name is invalid or already present. diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 203470454e..36939ec908 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -68,9 +68,7 @@ pub enum EvalStmt { update: Vec, body: Vec, }, - ClassDecl { - name: String, - }, + ClassDecl(EvalClass), Foreach { array: EvalExpr, key_name: Option, @@ -170,6 +168,112 @@ impl EvalFunction { } } +/// Runtime class declared by an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClass { + name: String, + properties: Vec, + methods: Vec, +} + +impl EvalClass { + /// Creates a dynamic eval class with public properties and methods. + pub fn new( + name: impl Into, + properties: Vec, + methods: Vec, + ) -> Self { + Self { + name: name.into(), + properties, + methods, + } + } + + /// Returns the original source spelling of this eval-declared class name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns public properties declared directly by this eval class. + pub fn properties(&self) -> &[EvalClassProperty] { + &self.properties + } + + /// Returns public methods declared directly by this eval class. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } + + /// Returns a public method by PHP case-insensitive method name. + pub fn method(&self, name: &str) -> Option<&EvalClassMethod> { + self.methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(name)) + } +} + +/// Public property metadata for a runtime eval class. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClassProperty { + name: String, + default: Option, +} + +impl EvalClassProperty { + /// Creates a public eval class property with an optional initializer. + pub fn new(name: impl Into, default: Option) -> Self { + Self { + name: name.into(), + default, + } + } + + /// Returns the PHP-visible property name without `$`. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the property initializer expression, when one was declared. + pub fn default(&self) -> Option<&EvalExpr> { + self.default.as_ref() + } +} + +/// Public method metadata for a runtime eval class. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClassMethod { + name: String, + params: Vec, + body: Vec, +} + +impl EvalClassMethod { + /// Creates a public eval class method with source-order parameters and body. + pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { + Self { + name: name.into(), + params, + body, + } + } + + /// Returns the PHP-visible method name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } + + /// Returns the dynamic EvalIR statements that form the method body. + pub fn body(&self) -> &[EvalStmt] { + &self.body + } +} + /// Dynamic eval expressions evaluated by the interpreter against runtime cells. #[derive(Debug, Clone, PartialEq)] pub enum EvalExpr { diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 0b2824b134..34856b4b2f 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -14,8 +14,9 @@ use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalConst, EvalExpr, EvalFunction, - EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, EvalConst, + EvalExpr, EvalFunction, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, + EvalUnaryOp, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; @@ -992,8 +993,8 @@ fn execute_stmt( scope, values, ), - EvalStmt::ClassDecl { name } => { - execute_class_decl_stmt(name, context, values)?; + EvalStmt::ClassDecl(class) => { + execute_class_decl_stmt(class, context, values)?; Ok(EvalControl::None) } EvalStmt::Foreach { @@ -1199,23 +1200,134 @@ fn catch_type_matches_throwable(class_name: &str) -> bool { class_name.eq_ignore_ascii_case("Throwable") } -/// Registers an empty eval-declared class name in the dynamic class table. +/// Registers an eval-declared class in the dynamic class table. fn execute_class_decl_stmt( - name: &str, + class: &EvalClass, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { - let name = name.trim_start_matches('\\'); + let name = class.name().trim_start_matches('\\'); if context.has_class(name) || values.class_exists(name)? { return Err(EvalStatus::RuntimeFatal); } - if context.define_class(name) { + if context.define_class(class.clone()) { Ok(()) } else { Err(EvalStatus::RuntimeFatal) } } +/// Creates a backing object for an eval-declared class and runs its constructor. +fn eval_dynamic_class_new_object( + class: &EvalClass, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, class.name()); + for property in class.properties() { + let value = if let Some(default) = property.default() { + eval_expr(default, context, caller_scope, values)? + } else { + values.null()? + }; + values.property_set(object, property.name(), value)?; + } + if let Some(constructor) = class.method("__construct") { + eval_dynamic_method_with_values( + class.name(), + constructor, + object, + evaluated_args, + context, + values, + )?; + } else if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(object) +} + +/// Dispatches a method call to an eval-declared class method or to the runtime hook. +fn eval_method_call_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return values.method_call(object, method_name, evaluated_args); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return values.method_call(object, method_name, evaluated_args); + }; + let class_name = class.name().to_string(); + let method = class + .method(method_name) + .cloned() + .ok_or(EvalStatus::RuntimeFatal)?; + eval_dynamic_method_with_values( + &class_name, + &method, + object, + evaluated_args, + context, + values, + ) +} + +/// Executes one eval-declared class method with `$this` bound in method scope. +fn eval_dynamic_method_with_values( + class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = + bind_evaluated_function_args(method.params(), positional_args(evaluated_args))?; + let mut method_scope = ElephcEvalScope::new(); + method_scope.set("this", object, ScopeCellOwnership::Borrowed); + for (name, value) in method.params().iter().zip(evaluated_args) { + method_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); + } + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + let result = execute_statements(method.body(), context, &mut method_scope, values); + let persist_result = persist_static_locals( + context, + &qualified_method_name, + &static_names, + &method_scope, + values, + ); + context.pop_function(); + persist_result?; + match result? { + EvalControl::None => values.null(), + EvalControl::Return(result) => Ok(result), + EvalControl::Throw(result) => { + context.set_pending_throw(result); + Err(EvalStatus::UncaughtThrowable) + } + EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Wraps positional method arguments into the shared dynamic-call binding shape. +fn positional_args(args: Vec) -> Vec { + args.into_iter() + .map(|value| EvaluatedCallArg { name: None, value }) + .collect() +} + /// Executes a PHP `static $name = expr;` declaration in the current eval scope. fn execute_static_var_stmt( name: &str, @@ -1476,9 +1588,13 @@ fn eval_expr( } => eval_namespaced_const_fetch(name, fallback_name, context, values), EvalExpr::NewObject { class_name, args } => { let args = eval_method_call_arg_values(args, context, scope, values)?; - values - .new_object(class_name) - .and_then(|object| values.construct_object(object, args).map(|()| object)) + if let Some(class) = context.class(class_name).cloned() { + eval_dynamic_class_new_object(&class, args, context, scope, values) + } else { + values + .new_object(class_name) + .and_then(|object| values.construct_object(object, args).map(|()| object)) + } } EvalExpr::MethodCall { object, @@ -1487,7 +1603,7 @@ fn eval_expr( } => { let object = eval_expr(object, context, scope, values)?; let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; - values.method_call(object, method, evaluated_args) + eval_method_call_result(object, method, evaluated_args, context, values) } EvalExpr::NullCoalesce { value, default } => { let value = eval_expr(value, context, scope, values)?; @@ -2272,36 +2388,55 @@ fn eval_builtin_is_a_relation( for arg in args { evaluated_args.push(eval_expr(arg, context, scope, values)?); } - eval_is_a_relation_result(name, &evaluated_args, values) + eval_is_a_relation_result(name, &evaluated_args, context, values) } /// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. fn eval_is_a_relation_result( name: &str, evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let (object_or_class, target_class, allow_string) = match evaluated_args { [object_or_class, target_class] => { (*object_or_class, *target_class, name == "is_subclass_of") } - [object_or_class, target_class, allow_string] => { - (*object_or_class, *target_class, values.truthy(*allow_string)?) - } + [object_or_class, target_class, allow_string] => ( + *object_or_class, + *target_class, + values.truthy(*allow_string)?, + ), _ => return Err(EvalStatus::RuntimeFatal), }; let target_class = values.string_bytes(target_class)?; let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; let target_class = target_class.trim_start_matches('\\'); let is_object = values.type_tag(object_or_class)? == 6; - let result = if is_object || allow_string { - values.object_is_a(object_or_class, target_class, name == "is_subclass_of")? - } else { - false - }; + let result = + if is_object && dynamic_object_is_a(object_or_class, target_class, context, values)? { + !matches!(name, "is_subclass_of") + } else if is_object || allow_string { + values.object_is_a(object_or_class, target_class, name == "is_subclass_of")? + } else { + false + }; values.bool_value(result) } +/// Returns whether an eval-created object matches a dynamic class name exactly. +fn dynamic_object_is_a( + object: RuntimeCellHandle, + target_class: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + Ok(context + .dynamic_object_class(identity) + .is_some_and(|class| class.name().eq_ignore_ascii_case(target_class))) +} + /// Evaluates PHP's `isset(...)` language construct over eval-visible values. fn eval_builtin_isset( args: &[EvalExpr], @@ -3021,7 +3156,7 @@ fn eval_evaluated_callable_with_values( eval_callable_with_values(name, evaluated_args, context, values) } EvaluatedCallable::ObjectMethod { object, method } => { - values.method_call(*object, method, evaluated_args) + eval_method_call_result(*object, method, evaluated_args, context, values) } } } @@ -3042,7 +3177,7 @@ fn eval_evaluated_callable_with_call_array_args( return Err(EvalStatus::RuntimeFatal); } let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); - values.method_call(*object, method, evaluated_args) + eval_method_call_result(*object, method, evaluated_args, context, values) } } } @@ -3790,7 +3925,9 @@ fn eval_builtin_with_values( eval_class_like_exists_result(name, evaluated_args, values)? } "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, - "is_a" | "is_subclass_of" => eval_is_a_relation_result(name, evaluated_args, values)?, + "is_a" | "is_subclass_of" => { + eval_is_a_relation_result(name, evaluated_args, context, values)? + } "json_decode" => match evaluated_args { [json] => eval_json_decode_result(*json, None, None, None, context, values)?, [json, associative] => { @@ -3904,7 +4041,7 @@ fn eval_builtin_with_values( let [object] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_get_class_result(*object, values)? + eval_get_class_result(*object, context, values)? } "get_parent_class" => { let [object_or_class] = evaluated_args else { @@ -12956,14 +13093,20 @@ fn eval_builtin_get_class( return Err(EvalStatus::RuntimeFatal); }; let object = eval_expr(object, context, scope, values)?; - eval_get_class_result(object, values) + eval_get_class_result(object, context, values) } /// Resolves the PHP-visible class name for one already materialized object cell. fn eval_get_class_result( object: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return values.string(class.name().trim_start_matches('\\')); + } + } values.object_class_name(object) } @@ -15324,7 +15467,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has EvalStmt::ArrayAppendVar { .. } | EvalStmt::ArraySetVar { .. } | EvalStmt::Break - | EvalStmt::ClassDecl { .. } + | EvalStmt::ClassDecl(_) | EvalStmt::Continue | EvalStmt::Echo(_) | EvalStmt::Expr(_) @@ -17423,6 +17566,38 @@ return function_exists("var_dump");"#, assert_eq!(values.get(x), FakeValue::Int(1)); } + /// Verifies eval-declared classes create objects with properties and methods. + #[test] + fn execute_program_constructs_eval_declared_class_with_method() { + let program = parse_fragment( + br#"class DynBox { + public int $x = 1; + public function __construct($x) { $this->x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +} +$box = new DynBox(4); +echo get_class($box); +echo ":"; +echo $box->bump(3); +echo ":"; +echo is_a($box, "DynBox") ? "Y" : "N"; +$call = [$box, "bump"]; +echo call_user_func($call, 1); +echo ":"; +echo call_user_func_array($call, [2]); +echo ":"; +return $box->x;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DynBox:7:Y8:10:"); + assert_eq!(values.get(result), FakeValue::Int(10)); + } + /// Verifies if/else executes only the PHP-truthy branch. #[test] fn execute_program_if_else_uses_php_truthiness() { diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 1fd0374e31..7857a0d34b 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -1101,7 +1101,11 @@ mod tests { #[test] fn dynamic_class_exists_reports_declared_eval_class() { let mut ctx = ElephcEvalContext::new(); - assert!(ctx.define_class("DynClassProbe")); + assert!(ctx.define_class(crate::eval_ir::EvalClass::new( + "DynClassProbe", + Vec::new(), + Vec::new() + ))); let existing = b"DynClassProbe"; let qualified = b"\\DynClassProbe"; let folded = b"dynclassprobe"; diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index cf2203d15f..1fb0267b69 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -15,8 +15,9 @@ use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalConst, EvalExpr, EvalMagicConst, - EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, + EvalClassProperty, EvalConst, EvalExpr, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, + EvalSwitchCase, EvalUnaryOp, }; use std::collections::HashMap; @@ -805,7 +806,7 @@ impl Parser { }]) } - /// Parses an empty `class Name {}` declaration for dynamic class-name registration. + /// Parses `class Name { ... }` declarations for dynamic class metadata. fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { self.advance(); let TokenKind::Ident(name) = self.current() else { @@ -814,11 +815,96 @@ impl Parser { let name = self.qualify_name_in_current_namespace(name); self.advance(); self.expect(TokenKind::LBrace)?; - if !self.consume(TokenKind::RBrace) { - return Err(EvalParseError::UnsupportedConstruct); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_class_member(&mut properties, &mut methods)?; } self.consume_semicolon(); - Ok(vec![EvalStmt::ClassDecl { name }]) + Ok(vec![EvalStmt::ClassDecl(EvalClass::new( + name, properties, methods, + ))]) + } + + /// Parses one public property or method from an eval class body. + fn parse_class_member( + &mut self, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + let public = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) + { + self.advance(); + true + } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) + { + return Err(EvalParseError::UnsupportedConstruct); + } else { + false + }; + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + methods.push(self.parse_class_method_decl()?); + return Ok(()); + } + + if !public { + return Err(EvalParseError::UnsupportedConstruct); + } + properties.push(self.parse_class_property_decl()?); + Ok(()) + } + + /// Parses `function name($param, ...) { ... }` inside a dynamic eval class. + fn parse_class_method_decl(&mut self) -> Result { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let params = self.parse_function_params()?; + let body = self.parse_block()?; + Ok(EvalClassMethod::new(name, params, body)) + } + + /// Parses one public property declaration with an optional initializer. + fn parse_class_property_decl(&mut self) -> Result { + self.skip_optional_property_type()?; + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let default = if self.consume(TokenKind::Equal) { + Some(self.parse_expr()?) + } else { + None + }; + self.expect_semicolon()?; + Ok(EvalClassProperty::new(name, default)) + } + + /// Consumes a simple declared property type before the `$property` token. + fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::DollarIdent(_)) { + return Ok(()); + } + if self.consume(TokenKind::Question) && matches!(self.current(), TokenKind::DollarIdent(_)) + { + return Err(EvalParseError::UnexpectedToken); + } + match self.current() { + TokenKind::Ident(_) | TokenKind::Backslash => { + let _ = self.parse_qualified_name()?; + Ok(()) + } + _ => Err(EvalParseError::UnexpectedToken), + } } /// Parses `function name($param, ...) { ... }` declarations. @@ -2340,6 +2426,13 @@ fn is_unsupported_statement_keyword(name: &str) -> bool { .any(|keyword| ident_eq(name, keyword)) } +/// Returns true for class member modifiers outside the current eval class subset. +fn is_unsupported_class_member_modifier(name: &str) -> bool { + ["private", "protected", "static", "abstract", "final"] + .iter() + .any(|modifier| ident_eq(name, modifier)) +} + /// Returns true when an eval catch type can be matched without runtime class tests. fn is_supported_catch_type(class_name: &str) -> bool { class_name.eq_ignore_ascii_case("Throwable") @@ -2902,9 +2995,11 @@ return Box;"#, assert_eq!( program.statements(), &[ - EvalStmt::ClassDecl { - name: "Eval\\Block\\Box".to_string(), - }, + EvalStmt::ClassDecl(EvalClass::new( + "Eval\\Block\\Box", + Vec::new(), + Vec::new() + )), EvalStmt::Return(Some(EvalExpr::NewObject { class_name: "Eval\\Block\\Box".to_string(), args: Vec::new(), @@ -4087,18 +4182,38 @@ try { let program = parse_fragment(b"class DynEvalClass {};").expect("fragment should parse"); assert_eq!( program.statements(), - &[EvalStmt::ClassDecl { - name: "DynEvalClass".to_string(), - }] + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalClass", + Vec::new(), + Vec::new() + ))] ); } - /// Verifies non-empty class declarations stay outside the supported eval subset. + /// Verifies public property and method class members lower into dynamic class metadata. #[test] - fn parse_fragment_rejects_non_empty_class_as_unsupported_construct() { + fn parse_fragment_accepts_public_class_members() { + let program = parse_fragment( + b"class DynEvalSupported { public int $x = 1; public function read() { return $this->x; } }", + ) + .expect("fragment should parse"); assert_eq!( - parse_fragment(b"class DynEvalUnsupported { public int $x = 1; }"), - Err(EvalParseError::UnsupportedConstruct) + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalSupported", + vec![EvalClassProperty::new( + "x", + Some(EvalExpr::Const(EvalConst::Int(1))) + )], + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + )] + ))] ); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 57089ba538..7d3f5dce5e 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, empty eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared empty classes, duplicate eval class-name rejection, and `new ClassName(...)` construction through generated class metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Non-empty dynamic class declarations, eval-defined class bodies/object construction, eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. @@ -27,7 +27,7 @@ The eval callable dispatcher currently normalizes string callbacks and two-eleme Eval `match` expressions live entirely inside the interpreter: the subject is evaluated once, patterns are compared through the existing strict-comparison value hook, result arms are evaluated lazily, and a miss without `default` returns the eval runtime-fatal status. -Eval namespace declarations are resolved while the bridge parser builds EvalIR. The parser qualifies function declarations, empty class declarations, object construction names, and qualified references against the active namespace; for unqualified function calls and constant fetches it emits a namespaced lookup plus global fallback so PHP builtin functions and predefined constants remain visible from namespaced eval fragments. Simple and grouped `use`, `use function`, and `use const` declarations are parser-time import tables for the active namespace declaration region: class imports rewrite `new` targets, function imports rewrite unqualified calls, and constant imports rewrite unqualified constant fetches before interpretation. `__NAMESPACE__` is lowered to the active namespace string for the parsed block. +Eval namespace declarations are resolved while the bridge parser builds EvalIR. The parser qualifies function declarations, class declarations, object construction names, and qualified references against the active namespace; for unqualified function calls and constant fetches it emits a namespaced lookup plus global fallback so PHP builtin functions and predefined constants remain visible from namespaced eval fragments. Simple and grouped `use`, `use function`, and `use const` declarations are parser-time import tables for the active namespace declaration region: class imports rewrite `new` targets, function imports rewrite unqualified calls, and constant imports rewrite unqualified constant fetches before interpretation. `__NAMESPACE__` is lowered to the active namespace string for the parsed block. Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6e23bf6a08..7174830691 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and empty eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs, non-empty eval class declarations, and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata and eval-created objects, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. @@ -42,7 +42,7 @@ Inside eval fragments, two-element object-method callable arrays such as `[$this Eval `match` expressions support strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default` fallback. A miss without `default` is currently reported as an eval runtime fatal. -Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and empty class declarations inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. Simple and grouped `use`, `use function`, and `use const` declarations resolve class aliases for `new`, function aliases for unqualified calls, and constant aliases for unqualified constant fetches within the active namespace declaration region. +Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and classes inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. Simple and grouped `use`, `use function`, and `use const` declarations resolve class aliases for `new`, function aliases for unqualified calls, and constant aliases for unqualified constant fetches within the active namespace declaration region. Eval `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. Relative paths follow PHP's cwd-first lookup and then fall back to the eval call-site directory. Included PHP files may contain normal `` blocks, raw text outside PHP tags is echoed, a `return` inside the included file becomes the include expression value, successful includes without `return` evaluate to `1`, repeated `*_once` includes evaluate to `true`, missing `include` returns `false` with warnings, and missing `require` aborts the eval fragment. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c42058db08..a92517c67d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4586,16 +4586,27 @@ eval('class dynevalaotclassdup {}'); ); } -/// Verifies non-empty eval class declarations remain unsupported until dynamic class bodies exist. +/// Verifies eval-declared classes support public properties, constructors, and methods. #[test] -fn test_eval_unsupported_non_empty_class_declaration_fails() { - let err = compile_and_run_expect_failure( - "x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +}'); +echo eval('$box = new DynEvalSupported(5); +echo get_class($box) . ":"; +echo $box->bump(4) . ":"; +echo is_a($box, "DynEvalSupported") ? "Y" : "N"; +$call = [$box, "bump"]; +echo call_user_func($call, 1) . ":"; +echo call_user_func_array($call, [2]) . ":"; +return $box->x;'); +"#, ); + assert_eq!(out, "DynEvalSupported:9:Y10:12:12"); } /// Verifies eval can construct an AOT class with no declared constructor. From 0503d7ddc8ab58de490e48894c79ceef382aa734 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 17 Jun 2026 23:48:24 +0200 Subject: [PATCH 0269/1208] Support eval catch type narrowing --- crates/elephc-eval/src/interpreter.rs | 77 ++++++++++++++++++++++++--- crates/elephc-eval/src/parser.rs | 53 ++++++++++-------- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 18 +++++++ 5 files changed, 123 insertions(+), 31 deletions(-) diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 34856b4b2f..9c9e486642 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1173,10 +1173,14 @@ fn execute_matching_catch( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let Some(catch) = catches - .iter() - .find(|catch| catch_type_matches_throwable(&catch.class_name)) - else { + let mut matched = None; + for catch in catches { + if catch_type_matches_thrown(thrown, &catch.class_name, values)? { + matched = Some(catch); + break; + } + } + let Some(catch) = matched else { return Ok(EvalControl::Throw(thrown)); }; if let Some(var_name) = &catch.var_name { @@ -1195,9 +1199,17 @@ fn execute_matching_catch( execute_statements(&catch.body, context, scope, values) } -/// Returns true for catch types that are known to accept any valid thrown object. -fn catch_type_matches_throwable(class_name: &str) -> bool { - class_name.eq_ignore_ascii_case("Throwable") +/// Returns true when a catch type accepts the thrown object. +fn catch_type_matches_thrown( + thrown: RuntimeCellHandle, + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = class_name.trim_start_matches('\\'); + if class_name.eq_ignore_ascii_case("Throwable") { + return Ok(true); + } + values.object_is_a(thrown, class_name, false) } /// Registers an eval-declared class in the dynamic class table. @@ -16235,6 +16247,12 @@ mod tests { exclude_self: bool, ) -> Result { match self.get(object_or_class) { + FakeValue::Object(_) + if target_class.eq_ignore_ascii_case("Exception") + || target_class.eq_ignore_ascii_case("Throwable") => + { + Ok(!exclude_self) + } FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { Ok(!exclude_self) } @@ -16933,6 +16951,51 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(9)); } + /// Verifies eval `catch (Exception)` matches thrown exception objects. + #[test] + fn execute_program_catches_specific_exception_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Exception $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies eval catch clauses keep source order and skip non-matching types. + #[test] + fn execute_program_skips_non_matching_specific_catch_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (RuntimeException $wrong) { + return 1; +} catch (Exception $caught) { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(scope.visible_cell("wrong"), None); + assert_eq!(values.get(result), FakeValue::Int(2)); + } + /// Verifies eval `finally` runs before a pending try-body return is observed. #[test] fn execute_program_runs_finally_before_returning_try_value() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index 1fb0267b69..c34e541f20 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1167,11 +1167,11 @@ impl Parser { }]) } - /// Parses one supported `catch (Throwable [$name]) { ... }` clause. + /// Parses one single-type `catch (ClassName [$name]) { ... }` clause. fn parse_catch_clause(&mut self) -> Result { self.advance(); self.expect(TokenKind::LParen)?; - let class_name = self.parse_supported_catch_type()?; + let class_name = self.parse_catch_type()?; let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { let var_name = var_name.clone(); self.advance(); @@ -1188,17 +1188,13 @@ impl Parser { }) } - /// Parses the catch type currently implemented by the EvalIR interpreter. - fn parse_supported_catch_type(&mut self) -> Result { + /// Parses a single catch type and keeps union catches outside the current EvalIR subset. + fn parse_catch_type(&mut self) -> Result { let class_name = self.parse_qualified_name()?; if matches!(self.current(), TokenKind::Pipe) { return Err(EvalParseError::UnsupportedConstruct); } - let class_name = self.resolve_class_name(class_name); - if !is_supported_catch_type(&class_name) { - return Err(EvalParseError::UnsupportedConstruct); - } - Ok(class_name) + Ok(self.resolve_class_name(class_name)) } /// Parses a dynamic function declaration parameter list after `(`. @@ -2433,11 +2429,6 @@ fn is_unsupported_class_member_modifier(name: &str) -> bool { .any(|modifier| ident_eq(name, modifier)) } -/// Returns true when an eval catch type can be matched without runtime class tests. -fn is_supported_catch_type(class_name: &str) -> bool { - class_name.eq_ignore_ascii_case("Throwable") -} - /// Returns true when an identifier is an include/require expression construct. fn is_include_construct_name(name: &str) -> bool { ["include", "include_once", "require", "require_once"] @@ -4107,19 +4098,39 @@ try { ); } - /// Verifies unsupported catch type narrowing stays explicit until runtime matching exists. + /// Verifies single catch type narrowing lowers into EvalIR. #[test] - fn parse_fragment_rejects_specific_eval_catch_type() { - assert_eq!( - parse_fragment( - br#"try { + fn parse_fragment_accepts_specific_eval_catch_type() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } catch (Exception $caught) { return 1; }"#, - ), - Err(EvalParseError::UnsupportedConstruct) + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_name: "Exception".to_string(), + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] ); + } + + /// Verifies union catch type narrowing stays explicit until runtime matching exists. + #[test] + fn parse_fragment_rejects_union_eval_catch_type() { assert_eq!( parse_fragment( br#"try { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 7d3f5dce5e..c9e3a9f6f7 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)`, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch (Throwable $e)` handling, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. eval catch-type narrowing beyond `Throwable` and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)` and class-specific single-type `catch` clauses, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch` handling with `Throwable` and class-specific single-type clauses, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Union eval catch clauses and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 7174830691..2e5f4f8e60 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)`, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata and eval-created objects, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)` and class-specific single-type `catch` clauses, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata and eval-created objects, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. @@ -135,7 +135,7 @@ Eval realpath-cache calls match elephc's native no-cache convention: `realpath_c Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch/finally` can catch `Throwable` clauses inside the fragment and always runs `finally` before propagating or returning; specific catch-type narrowing such as `catch (Exception $e)` and union catches remain outside the EvalIR subset. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch/finally` can catch `Throwable` and class-specific single-type clauses inside the fragment and always runs `finally` before propagating or returning; union catch clauses remain outside the EvalIR subset. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a92517c67d..aeb76385c0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4995,6 +4995,24 @@ return 0;'); assert_eq!(out, "8"); } +/// Verifies eval-internal catch type narrowing uses the thrown object's class. +#[test] +fn test_eval_try_catch_matches_specific_exception_inside_eval() { + let out = compile_and_run( + r#" Date: Wed, 17 Jun 2026 23:52:11 +0200 Subject: [PATCH 0270/1208] Support eval union catch clauses --- crates/elephc-eval/src/eval_ir.rs | 2 +- crates/elephc-eval/src/interpreter.rs | 44 +++++++++++++++++---- crates/elephc-eval/src/parser.rs | 57 +++++++++++++++++---------- docs/internals/the-runtime.md | 2 +- docs/php/system-and-io.md | 4 +- tests/codegen/eval.rs | 18 +++++++++ 6 files changed, 95 insertions(+), 32 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 36939ec908..c8ac54e071 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -129,7 +129,7 @@ pub enum EvalStmt { /// One `catch` block attached to an eval `try` statement. #[derive(Debug, Clone, PartialEq)] pub struct EvalCatch { - pub class_name: String, + pub class_names: Vec, pub var_name: Option, pub body: Vec, } diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 9c9e486642..428da6f7e6 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -1175,7 +1175,7 @@ fn execute_matching_catch( ) -> Result { let mut matched = None; for catch in catches { - if catch_type_matches_thrown(thrown, &catch.class_name, values)? { + if catch_types_match_thrown(thrown, &catch.class_names, values)? { matched = Some(catch); break; } @@ -1199,17 +1199,22 @@ fn execute_matching_catch( execute_statements(&catch.body, context, scope, values) } -/// Returns true when a catch type accepts the thrown object. -fn catch_type_matches_thrown( +/// Returns true when any type in one catch clause accepts the thrown object. +fn catch_types_match_thrown( thrown: RuntimeCellHandle, - class_name: &str, + class_names: &[String], values: &mut impl RuntimeValueOps, ) -> Result { - let class_name = class_name.trim_start_matches('\\'); - if class_name.eq_ignore_ascii_case("Throwable") { - return Ok(true); + for class_name in class_names { + let class_name = class_name.trim_start_matches('\\'); + if class_name.eq_ignore_ascii_case("Throwable") { + return Ok(true); + } + if values.object_is_a(thrown, class_name, false)? { + return Ok(true); + } } - values.object_is_a(thrown, class_name, false) + Ok(false) } /// Registers an eval-declared class in the dynamic class table. @@ -16996,6 +17001,29 @@ mod tests { assert_eq!(values.get(result), FakeValue::Int(2)); } + /// Verifies union catch clauses test later types in the same catch clause. + #[test] + fn execute_program_catches_union_type_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (RuntimeException|Exception $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); + } + /// Verifies eval `finally` runs before a pending try-body return is observed. #[test] fn execute_program_runs_finally_before_returning_try_value() { diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs index c34e541f20..889c1b6dc0 100644 --- a/crates/elephc-eval/src/parser.rs +++ b/crates/elephc-eval/src/parser.rs @@ -1142,7 +1142,7 @@ impl Parser { Ok(vec![EvalStmt::Throw(expr)]) } - /// Parses `try { ... } catch (Throwable $name) { ... } finally { ... }` statements. + /// Parses `try { ... } catch (Type|Other $name) { ... } finally { ... }` statements. fn parse_try_stmt(&mut self) -> Result, EvalParseError> { self.advance(); let body = self.parse_block()?; @@ -1167,11 +1167,11 @@ impl Parser { }]) } - /// Parses one single-type `catch (ClassName [$name]) { ... }` clause. + /// Parses one `catch (ClassName|Other [$name]) { ... }` clause. fn parse_catch_clause(&mut self) -> Result { self.advance(); self.expect(TokenKind::LParen)?; - let class_name = self.parse_catch_type()?; + let class_names = self.parse_catch_types()?; let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { let var_name = var_name.clone(); self.advance(); @@ -1182,19 +1182,21 @@ impl Parser { self.expect(TokenKind::RParen)?; let body = self.parse_block()?; Ok(EvalCatch { - class_name, + class_names, var_name, body, }) } - /// Parses a single catch type and keeps union catches outside the current EvalIR subset. - fn parse_catch_type(&mut self) -> Result { + /// Parses one or more unioned catch types in source order. + fn parse_catch_types(&mut self) -> Result, EvalParseError> { let class_name = self.parse_qualified_name()?; - if matches!(self.current(), TokenKind::Pipe) { - return Err(EvalParseError::UnsupportedConstruct); + let mut class_names = vec![self.resolve_class_name(class_name)]; + while self.consume(TokenKind::Pipe) { + let class_name = self.parse_qualified_name()?; + class_names.push(self.resolve_class_name(class_name)); } - Ok(self.resolve_class_name(class_name)) + Ok(class_names) } /// Parses a dynamic function declaration parameter list after `(`. @@ -4031,7 +4033,7 @@ function dyn() { return alias(); }"#, )))], })], catches: vec![EvalCatch { - class_name: "Throwable".to_string(), + class_names: vec!["Throwable".to_string()], var_name: Some("caught".to_string()), body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], }], @@ -4057,7 +4059,7 @@ try { &[EvalStmt::Try { body: vec![EvalStmt::Throw(EvalExpr::LoadVar("e".to_string()))], catches: vec![EvalCatch { - class_name: "Throwable".to_string(), + class_names: vec!["Throwable".to_string()], var_name: Some("caught".to_string()), body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( "caught".to_string() @@ -4089,7 +4091,7 @@ try { )))], })], catches: vec![EvalCatch { - class_name: "Throwable".to_string(), + class_names: vec!["Throwable".to_string()], var_name: None, body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], }], @@ -4119,7 +4121,7 @@ try { )))], })], catches: vec![EvalCatch { - class_name: "Exception".to_string(), + class_names: vec!["Exception".to_string()], var_name: Some("caught".to_string()), body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], }], @@ -4128,18 +4130,33 @@ try { ); } - /// Verifies union catch type narrowing stays explicit until runtime matching exists. + /// Verifies union catch type narrowing lowers all source-order types into one clause. #[test] - fn parse_fragment_rejects_union_eval_catch_type() { - assert_eq!( - parse_fragment( - br#"try { + fn parse_fragment_accepts_union_eval_catch_type() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } catch (Throwable|Exception $caught) { return 1; }"#, - ), - Err(EvalParseError::UnsupportedConstruct) + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string(), "Exception".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] ); } diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index c9e3a9f6f7..a2cb355310 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,7 +17,7 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)` and class-specific single-type `catch` clauses, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch` handling with `Throwable` and class-specific single-type clauses, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Union eval catch clauses and broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)` and class-specific and union `catch` clauses, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch` handling with `Throwable` and class-specific and union clauses, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 2e5f4f8e60..9499e80b6c 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -32,7 +32,7 @@ sidebar: `eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)` and class-specific single-type `catch` clauses, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata and eval-created objects, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)` and class-specific and union `catch` clauses, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata and eval-created objects, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. @@ -135,7 +135,7 @@ Eval realpath-cache calls match elephc's native no-cache convention: `realpath_c Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch/finally` can catch `Throwable` and class-specific single-type clauses inside the fragment and always runs `finally` before propagating or returning; union catch clauses remain outside the EvalIR subset. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. +Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch/finally` can catch `Throwable` and class-specific and union clauses inside the fragment and always runs `finally` before propagating or returning. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index aeb76385c0..d1cf167b8c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5013,6 +5013,24 @@ return "miss";'); assert_eq!(out, "caught"); } +/// Verifies eval-internal union catch clauses match any listed class. +#[test] +fn test_eval_try_catch_matches_union_type_inside_eval() { + let out = compile_and_run( + r#" Date: Wed, 17 Jun 2026 23:53:25 +0200 Subject: [PATCH 0271/1208] Cover eval array copy on write --- docs/php/system-and-io.md | 2 +- tests/codegen/eval.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 9499e80b6c..61304590f1 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -137,7 +137,7 @@ Inside eval, `define()` stores dynamic constants that persist across later eval Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch/finally` can catch `Throwable` and class-specific and union clauses inside the fragment and always runs `finally` before propagating or returning. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. -Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. +Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. Eval array writes preserve native PHP copy-on-write behavior for by-value aliases while still mutating reference aliases. Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, printf-family formatting through `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, string scanning through `sscanf()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, and `random_int()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d1cf167b8c..f3ee483b18 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -662,6 +662,22 @@ echo $items[0] . ":" . $items[1] . ":" . $items[2] . ":" . count($items); assert_eq!(out, "z:b:c:3"); } +/// Verifies eval array writes preserve PHP copy-on-write for by-value aliases. +#[test] +fn test_eval_array_write_preserves_native_by_value_alias() { + let out = compile_and_run( + r#" Date: Wed, 17 Jun 2026 23:54:17 +0200 Subject: [PATCH 0272/1208] Cover eval barrier type checking --- tests/error_tests/misc/system_builtins.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/error_tests/misc/system_builtins.rs b/tests/error_tests/misc/system_builtins.rs index 32e2d9ad40..14e569f915 100644 --- a/tests/error_tests/misc/system_builtins.rs +++ b/tests/error_tests/misc/system_builtins.rs @@ -27,6 +27,29 @@ expect_builtin_arity_error!( "eval() takes exactly 1 argument" ); +/// Verifies an eval barrier allows later reads of variables that eval may create dynamically. +#[test] +fn test_eval_barrier_allows_dynamic_variable_read() { + check_source(" Date: Thu, 18 Jun 2026 01:15:00 +0200 Subject: [PATCH 0273/1208] Support eval in by-ref closure captures --- src/codegen/lower_inst/builtins/eval.rs | 56 +++--- src/ir_lower/expr/mod.rs | 256 +++++++++++++++++++++++- tests/codegen/eval.rs | 44 +++- 3 files changed, 315 insertions(+), 41 deletions(-) diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 6a7b02d859..59434c7339 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -1039,12 +1039,12 @@ fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local } } } @@ -1057,40 +1057,34 @@ fn store_mixed_scope_cell_to_local( match local.ty.codegen_repr() { PhpType::Mixed | PhpType::Union(_) => { emit_retain_scope_cell_if_owned(ctx); - let result_reg = abi::int_result_reg(ctx.emitter); - let offset = ctx.local_offset(local.slot)?; - abi::store_at_offset(ctx.emitter, result_reg, offset); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Int => { abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); - let offset = ctx.local_offset(local.slot)?; - abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Bool => { abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); - let offset = ctx.local_offset(local.slot)?; - abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Float => { abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); - let offset = ctx.local_offset(local.slot)?; - abi::store_at_offset(ctx.emitter, abi::float_result_reg(ctx.emitter), offset); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Str => { abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); - let offset = ctx.local_offset(local.slot)?; - let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); - abi::store_at_offset(ctx.emitter, ptr_reg, offset); - abi::store_at_offset(ctx.emitter, len_reg, offset - 8); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Object(_) => { abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); - let offset = ctx.local_offset(local.slot)?; let object_reg = match ctx.emitter.target.arch { Arch::AArch64 => "x1", Arch::X86_64 => "rdi", }; - abi::store_at_offset(ctx.emitter, object_reg, offset); + let result_reg = abi::int_result_reg(ctx.emitter); + ctx.emitter + .instruction(&format!("mov {}, {}", result_reg, object_reg)); // move unboxed object pointer into the local-store result register + ctx.store_current_result_to_local(local.slot)?; } other => { return Err(CodegenIrError::unsupported(format!( @@ -1162,12 +1156,12 @@ fn emit_retain_scope_cell_if_owned(ctx: &mut FunctionContext<'_>) { Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining } } abi::emit_call_label(ctx.emitter, "__rt_incref"); @@ -1179,32 +1173,30 @@ fn store_missing_scope_entry_to_local( ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal, ) -> Result<()> { - let offset = ctx.local_offset(local.slot)?; match local.ty.codegen_repr() { PhpType::Mixed | PhpType::Union(_) => { let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); abi::emit_call_label(ctx.emitter, &symbol); - abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Int | PhpType::Bool => { abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); - abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Float => { abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); abi::emit_int_result_to_float_result(ctx.emitter); - abi::store_at_offset(ctx.emitter, abi::float_result_reg(ctx.emitter), offset); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Str => { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); abi::emit_load_int_immediate(ctx.emitter, ptr_reg, 0); abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); - abi::store_at_offset(ctx.emitter, ptr_reg, offset); - abi::store_at_offset(ctx.emitter, len_reg, offset - 8); + ctx.store_current_result_to_local(local.slot)?; } PhpType::Object(_) => { abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); - abi::store_at_offset(ctx.emitter, abi::int_result_reg(ctx.emitter), offset); + ctx.store_current_result_to_local(local.slot)?; } other => { return Err(CodegenIrError::unsupported(format!( @@ -1290,12 +1282,12 @@ fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: Arch::AArch64 => { ctx.emitter .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler } Arch::X86_64 => { ctx.emitter .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler } } } @@ -1323,7 +1315,7 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr + ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); ctx.emitter @@ -1332,12 +1324,12 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { abi::emit_exit(ctx.emitter, 1); } Arch::X86_64 => { - ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr + ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); ctx.emitter .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length - ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting abi::emit_exit(ctx.emitter, 1); } } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 70af1c22fd..9f6e004c08 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -7969,6 +7969,7 @@ fn lower_closure_with_context( } else { captures }; + let body_contains_eval = body_contains_eval_call(body); let mut captured_values = Vec::with_capacity(captures.len()); let mut capture_params = Vec::with_capacity(captures.len()); for capture in captures { @@ -7979,12 +7980,17 @@ fn lower_closure_with_context( // runtime against the bound object's class. (lower_null(ctx, expr), PhpType::Mixed) } else { - let captured = ctx.load_local(capture, Some(expr.span)); - let php_type = if by_ref && self_ref_callable_capture == Some(capture.as_str()) { - PhpType::Callable + let php_type_override = if by_ref && self_ref_callable_capture == Some(capture.as_str()) { + Some(PhpType::Callable) + } else if by_ref && body_contains_eval { + ctx.set_local_type(capture, PhpType::Mixed); + Some(PhpType::Mixed) } else { - ctx.builder.value_php_type(captured.value) + None }; + let captured = ctx.load_local(capture, Some(expr.span)); + let php_type = php_type_override + .unwrap_or_else(|| ctx.builder.value_php_type(captured.value)); (captured, php_type) }; let immediate = by_ref.then_some(Immediate::I64(1)); @@ -8047,6 +8053,248 @@ fn lower_closure_with_context( closure } +/// Returns true when a statement body contains an `eval(...)` call. +fn body_contains_eval_call(body: &[Stmt]) -> bool { + body.iter().any(stmt_contains_eval_call) +} + +/// Returns true when a statement or nested statement body contains an `eval(...)` call. +fn stmt_contains_eval_call(stmt: &Stmt) -> bool { + match &stmt.kind { + StmtKind::Echo(expr) + | StmtKind::Throw(expr) + | StmtKind::ExprStmt(expr) + | StmtKind::ConstDecl { value: expr, .. } + | StmtKind::ListUnpack { value: expr, .. } + | StmtKind::StaticVar { init: expr, .. } + | StmtKind::Assign { value: expr, .. } + | StmtKind::TypedAssign { value: expr, .. } + | StmtKind::ArrayPush { value: expr, .. } + | StmtKind::StaticPropertyAssign { value: expr, .. } + | StmtKind::StaticPropertyArrayPush { value: expr, .. } => expr_contains_eval_call(expr), + StmtKind::Return(expr) => expr.as_ref().is_some_and(expr_contains_eval_call), + StmtKind::ArrayAssign { index, value, .. } + | StmtKind::StaticPropertyArrayAssign { index, value, .. } + | StmtKind::PropertyArrayAssign { index, value, .. } => { + expr_contains_eval_call(index) || expr_contains_eval_call(value) + } + StmtKind::NestedArrayAssign { target, value } => { + expr_contains_eval_call(target) || expr_contains_eval_call(value) + } + StmtKind::PropertyAssign { object, value, .. } + | StmtKind::PropertyArrayPush { object, value, .. } => { + expr_contains_eval_call(object) || expr_contains_eval_call(value) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + expr_contains_eval_call(condition) + || body_contains_eval_call(then_body) + || elseif_clauses.iter().any(|(condition, body)| { + expr_contains_eval_call(condition) || body_contains_eval_call(body) + }) + || else_body.as_ref().is_some_and(|body| body_contains_eval_call(body)) + } + StmtKind::IfDef { then_body, else_body, .. } => { + body_contains_eval_call(then_body) + || else_body.as_ref().is_some_and(|body| body_contains_eval_call(body)) + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + expr_contains_eval_call(condition) || body_contains_eval_call(body) + } + StmtKind::For { init, condition, update, body } => { + init.as_deref().is_some_and(stmt_contains_eval_call) + || condition.as_ref().is_some_and(expr_contains_eval_call) + || update.as_deref().is_some_and(stmt_contains_eval_call) + || body_contains_eval_call(body) + } + StmtKind::Foreach { array, body, .. } => { + expr_contains_eval_call(array) || body_contains_eval_call(body) + } + StmtKind::Switch { subject, cases, default } => { + expr_contains_eval_call(subject) + || cases.iter().any(|(patterns, body)| { + patterns.iter().any(expr_contains_eval_call) || body_contains_eval_call(body) + }) + || default.as_ref().is_some_and(|body| body_contains_eval_call(body)) + } + StmtKind::Include { path, .. } => expr_contains_eval_call(path), + StmtKind::Synthetic(body) + | StmtKind::NamespaceBlock { body, .. } + | StmtKind::IncludeOnceGuard { body, .. } => body_contains_eval_call(body), + StmtKind::FunctionDecl { params, body, .. } => { + params + .iter() + .any(|(_, _, default, _)| default.as_ref().is_some_and(expr_contains_eval_call)) + || body_contains_eval_call(body) + } + StmtKind::ClassDecl { properties, methods, constants, .. } + | StmtKind::TraitDecl { properties, methods, constants, .. } + | StmtKind::InterfaceDecl { properties, methods, constants, .. } => { + properties.iter().any(|property| { + property.default.as_ref().is_some_and(expr_contains_eval_call) + }) || constants + .iter() + .any(|constant| expr_contains_eval_call(&constant.value)) + || methods.iter().any(|method| { + method.params.iter().any(|(_, _, default, _)| { + default.as_ref().is_some_and(expr_contains_eval_call) + }) || body_contains_eval_call(&method.body) + }) + } + StmtKind::Try { try_body, catches, finally_body } => { + body_contains_eval_call(try_body) + || catches.iter().any(|catch_clause| body_contains_eval_call(&catch_clause.body)) + || finally_body.as_ref().is_some_and(|body| body_contains_eval_call(body)) + } + StmtKind::EnumDecl { cases, .. } => cases + .iter() + .any(|case| case.value.as_ref().is_some_and(expr_contains_eval_call)), + StmtKind::RefAssign { .. } + | StmtKind::Break(_) + | StmtKind::Continue(_) + | StmtKind::NamespaceDecl { .. } + | StmtKind::UseDecl { .. } + | StmtKind::FunctionVariantGroup { .. } + | StmtKind::FunctionVariantMark { .. } + | StmtKind::IncludeOnceMark { .. } + | StmtKind::Global { .. } + | StmtKind::PackedClassDecl { .. } + | StmtKind::ExternFunctionDecl { .. } + | StmtKind::ExternClassDecl { .. } + | StmtKind::ExternGlobalDecl { .. } => false, + } +} + +/// Returns true when an expression contains an `eval(...)` call. +fn expr_contains_eval_call(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::FunctionCall { name, args } => { + is_eval_call_name(name) || args.iter().any(expr_contains_eval_call) + } + ExprKind::BinaryOp { left, right, .. } => { + expr_contains_eval_call(left) || expr_contains_eval_call(right) + } + ExprKind::InstanceOf { value, target } => { + expr_contains_eval_call(value) || instance_of_target_contains_eval_call(target) + } + ExprKind::Negate(expr) + | ExprKind::Not(expr) + | ExprKind::BitNot(expr) + | ExprKind::Throw(expr) + | ExprKind::ErrorSuppress(expr) + | ExprKind::Print(expr) + | ExprKind::Spread(expr) + | ExprKind::Cast { expr, .. } + | ExprKind::PtrCast { expr, .. } + | ExprKind::BufferNew { len: expr, .. } + | ExprKind::YieldFrom(expr) => expr_contains_eval_call(expr), + ExprKind::NullCoalesce { value, default } + | ExprKind::ShortTernary { value, default } + | ExprKind::Pipe { value, callable: default } + | ExprKind::ArrayAccess { array: value, index: default } => { + expr_contains_eval_call(value) || expr_contains_eval_call(default) + } + ExprKind::Assignment { target, value, result_target, prelude, .. } => { + expr_contains_eval_call(target) + || expr_contains_eval_call(value) + || result_target.as_ref().is_some_and(|target| expr_contains_eval_call(target)) + || body_contains_eval_call(prelude) + } + ExprKind::ArrayLiteral(items) => items.iter().any(expr_contains_eval_call), + ExprKind::ArrayLiteralAssoc(entries) => entries + .iter() + .any(|(key, value)| expr_contains_eval_call(key) || expr_contains_eval_call(value)), + ExprKind::Match { subject, arms, default } => { + expr_contains_eval_call(subject) + || arms.iter().any(|(patterns, value)| { + patterns.iter().any(expr_contains_eval_call) || expr_contains_eval_call(value) + }) + || default.as_ref().is_some_and(|default| expr_contains_eval_call(default)) + } + ExprKind::Ternary { condition, then_expr, else_expr } => { + expr_contains_eval_call(condition) + || expr_contains_eval_call(then_expr) + || expr_contains_eval_call(else_expr) + } + ExprKind::Closure { params, body, .. } => { + params + .iter() + .any(|(_, _, default, _)| default.as_ref().is_some_and(expr_contains_eval_call)) + || body_contains_eval_call(body) + } + ExprKind::NamedArg { value, .. } => expr_contains_eval_call(value), + ExprKind::ClosureCall { args, .. } + | ExprKind::StaticMethodCall { args, .. } + | ExprKind::NewObject { args, .. } + | ExprKind::NewScopedObject { args, .. } => args.iter().any(expr_contains_eval_call), + ExprKind::ExprCall { callee, args } => { + expr_contains_eval_call(callee) || args.iter().any(expr_contains_eval_call) + } + ExprKind::NewDynamic { name_expr, args } => { + expr_contains_eval_call(name_expr) || args.iter().any(expr_contains_eval_call) + } + ExprKind::NewDynamicObject { class_name, args, .. } => { + expr_contains_eval_call(class_name) || args.iter().any(expr_contains_eval_call) + } + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => expr_contains_eval_call(object), + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + expr_contains_eval_call(object) || expr_contains_eval_call(property) + } + ExprKind::MethodCall { object, args, .. } + | ExprKind::NullsafeMethodCall { object, args, .. } => { + expr_contains_eval_call(object) || args.iter().any(expr_contains_eval_call) + } + ExprKind::FirstClassCallable(target) => callable_target_contains_eval_call(target), + ExprKind::Yield { key, value } => { + key.as_ref().is_some_and(|key| expr_contains_eval_call(key)) + || value.as_ref().is_some_and(|value| expr_contains_eval_call(value)) + } + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::Variable(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + | ExprKind::PreIncrement(_) + | ExprKind::PostIncrement(_) + | ExprKind::PreDecrement(_) + | ExprKind::PostDecrement(_) + | ExprKind::ConstRef(_) + | ExprKind::StaticPropertyAccess { .. } + | ExprKind::This + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } + | ExprKind::MagicConstant(_) => false, + } +} + +/// Returns true when an `instanceof` target expression contains an `eval(...)` call. +fn instance_of_target_contains_eval_call(target: &InstanceOfTarget) -> bool { + match target { + InstanceOfTarget::Name(_) => false, + InstanceOfTarget::Expr(expr) => expr_contains_eval_call(expr), + } +} + +/// Returns true when a first-class callable target contains an `eval(...)` call. +fn callable_target_contains_eval_call(target: &CallableTarget) -> bool { + match target { + CallableTarget::Function(_) | CallableTarget::StaticMethod { .. } => false, + CallableTarget::Method { object, .. } => expr_contains_eval_call(object), + } +} + +/// Returns true when a function call name resolves to PHP's `eval` construct. +fn is_eval_call_name(name: &Name) -> bool { + php_symbol_key(name.as_str().trim_start_matches('\\')) == "eval" +} + /// Lowers a closure variable call. fn lower_closure_call(ctx: &mut LoweringContext<'_, '_>, var: &str, args: &[Expr], expr: &Expr) -> LoweredValue { if let Some(value) = lower_invokable_object_variable_call(ctx, var, args, expr) { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f3ee483b18..a63bef67d7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1,14 +1,15 @@ //! Purpose: -//! Integration tests for the initial `eval()` bridge wiring. -//! Covers language-construct visibility, conditional bridge linking, and the -//! base runtime interpreter path for scalar, branch, indexed-array, and simple builtin eval fragments. +//! Integration tests for the optional `eval()` bridge. +//! Covers language-construct visibility, conditional bridge linking, scope +//! synchronization, dynamic declarations, EvalIR execution, and supported +//! builtin dispatch through end-to-end codegen. //! //! Called from: //! - `cargo test --test codegen_tests eval` through Rust's test harness. //! //! Key details: -//! - These tests intentionally cover the first scalar/control-flow/indexed-array subset, -//! not full PHP eval scope synchronization or dynamic declaration semantics. +//! - Fixtures exercise the native/EvalIR boundary rather than the frozen legacy +//! AST backend, and many cases assert post-barrier native visibility. use crate::support::*; @@ -3877,6 +3878,39 @@ eval('static $n = 0; $n++; echo $n;'); assert_eq!(out, "1:1"); } +/// Verifies eval inside a closure can mutate the closure's by-value capture without touching the outer variable. +#[test] +fn test_eval_inside_closure_updates_by_value_capture_copy() { + let out = compile_and_run( + r#" Date: Thu, 18 Jun 2026 01:21:03 +0200 Subject: [PATCH 0274/1208] Document eval closure capture sync --- docs/internals/the-runtime.md | 2 ++ docs/php/system-and-io.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a2cb355310..d71f7f91b7 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -17,6 +17,8 @@ Programs that use PHP's `eval()` link one extra static bridge library, `elephc_e The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. +Closure bodies that can execute `eval()` participate in the same dynamic barrier rules. By-value captures flush and reload only the closure-local captured copy. By-reference captures store a ref-cell pointer shared with the source variable; when such a capture may cross an eval barrier, EIR lowering widens the shared cell to boxed `Mixed` before closure creation so the caller and closure agree on the representation after eval writes through the cell. + The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)` and class-specific and union `catch` clauses, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch` handling with `Throwable` and class-specific and union clauses, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 61304590f1..6046af4658 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -34,6 +34,8 @@ sidebar: The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)` and class-specific and union `catch` clauses, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata and eval-created objects, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. +When `eval()` runs inside a closure, by-value `use ($x)` captures synchronize only the closure's captured copy. By-reference `use (&$x)` captures write through the shared source variable, so mutations made by the evaluated fragment are visible to the outer scope. + Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. Eval `catch (Throwable)` may omit the catch variable when the fragment only needs to handle and discard the thrown object. From 588731f84587ca9a3454068b6c743468e7e8d252 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 08:50:10 +0200 Subject: [PATCH 0275/1208] Document eval separately --- docs/README.md | 1 + docs/php/arrays.md | 2 +- docs/php/classes.md | 2 +- docs/php/eval.md | 270 ++++++++++++++++++++++++++++++++++++ docs/php/fibers.md | 2 +- docs/php/generators.md | 2 +- docs/php/magic-constants.md | 2 +- docs/php/math.md | 2 +- docs/php/namespaces.md | 2 +- docs/php/pdo.md | 2 +- docs/php/regex.md | 2 +- docs/php/spl.md | 2 +- docs/php/streams.md | 2 +- docs/php/strings.md | 2 +- docs/php/system-and-io.md | 116 +--------------- 15 files changed, 284 insertions(+), 127 deletions(-) create mode 100644 docs/php/eval.md diff --git a/docs/README.md b/docs/README.md index b96808d2f2..360d40e9b7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -40,6 +40,7 @@ Standard PHP features supported by elephc. Implemented PHP syntax is intended to - [Operators](php/operators.md) — arithmetic, comparison, `instanceof`, logical, bitwise, string, assignment, ternary, null coalescing, error control - [Control Structures](php/control-structures.md) — if/else, while, for, foreach, switch, match, multi-level break/continue, try/catch/finally - [Functions](php/functions.md) — declarations, closures, arrow functions, named arguments, variadic, spread, pass-by-reference, first-class callables, static variables +- [Eval](php/eval.md) — runtime PHP fragment evaluation, scope synchronization, dynamic declarations, supported builtins, and limitations - [Strings](php/strings.md) — escape sequences, interpolation, heredoc/nowdoc, 70+ built-in string functions - [Regex](php/regex.md) — PCRE2-backed `preg_*` functions, SPL regex iterators, and native PCRE2 build requirements - [Arrays](php/arrays.md) — indexed, associative, copy-on-write, 50+ built-in array functions diff --git a/docs/php/arrays.md b/docs/php/arrays.md index 75de6b2393..30b536715e 100644 --- a/docs/php/arrays.md +++ b/docs/php/arrays.md @@ -2,7 +2,7 @@ title: "Arrays" description: "Indexed arrays, associative arrays, copy-on-write, and built-in array functions." sidebar: - order: 7 + order: 8 --- ## Indexed arrays diff --git a/docs/php/classes.md b/docs/php/classes.md index 8d7b3b1427..167732dae9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -2,7 +2,7 @@ title: "Classes" description: "Classes, interfaces, abstract classes, traits, enums, properties, and inheritance." sidebar: - order: 9 + order: 10 --- ## Class declaration diff --git a/docs/php/eval.md b/docs/php/eval.md new file mode 100644 index 0000000000..bf7b74b00f --- /dev/null +++ b/docs/php/eval.md @@ -0,0 +1,270 @@ +--- +title: "Eval" +description: "Runtime PHP fragment evaluation, dynamic scope synchronization, supported EvalIR subset, and current limitations." +sidebar: + order: 5 +--- + +`eval($code): mixed` parses and executes a PHP fragment at runtime in the +caller-visible local scope. It is a PHP language construct, not a normal +callable: `function_exists("eval")` and `is_callable("eval")` return `false`, +and first-class callable syntax for `eval` is rejected. + +Programs that call `eval()` link the optional `elephc_eval` bridge. Programs +that do not use `eval()` keep the ordinary fully native runtime path and do not +link the bridge. + +The evaluated string must be a PHP fragment without an opening `>=`, `.=`), and simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`) are supported. | +| Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | +| Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | +| Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | +| Classes | Eval fragments can declare classes with public properties, public methods, and `__construct()`. Duplicate eval class names are rejected. | +| Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | +| Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | + +`foreach` supports value-only and key-value iteration over indexed and +associative arrays. Eval associative arrays preserve PHP insertion order for +iteration. + +Includes follow PHP's cwd-first lookup and then fall back to the eval call-site +directory. Included PHP files may contain normal `` blocks, raw text +outside PHP tags is echoed, a `return` inside the included file becomes the +include expression value, successful includes without `return` evaluate to `1`, +repeated `*_once` includes evaluate to `true`, missing `include` returns +`false` with warnings, and missing `require` aborts the eval fragment. + +## Supported expressions + +| Expression area | Support | +|---|---| +| Scalars | `null`, booleans, integers, floats, and strings. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, and eval object property access through the bridge. | +| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | +| Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | +| Object construction | `new ClassName(...)` for eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime metadata. | +| Method calls | Public object method calls with positional arguments and numeric array unpacking. | +| Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | +| Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | +| Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | +| Ternaries | Full ternary and short ternary (`?:`). | +| Match | Strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default`. A miss without `default` is reported as an eval runtime fatal. | + +Supported unary operators are `+`, `-`, `!`, and integer bitwise `~`. + +Supported binary operators are: + +| Category | Operators | +|---|---| +| Arithmetic | `+`, `-`, `*`, `**`, `/`, `%` | +| String | `.` | +| Integer bitwise and shifts | `&`, `|`, `^`, `<<`, `>>` | +| Logical | `&&`, `||`, `and`, `or`, `xor` | +| Null coalescing | `??` | +| Equality | `==`, `!=`, `===`, `!==` | +| Comparison | `<`, `<=`, `>`, `>=`, `<=>` | + +Array literals and append writes use PHP's next automatic integer key rule, +including integer-string keys such as `"2"`, boolean and float keys normalized +to integers, and `null` keys normalized to the empty string. Eval array writes +preserve native PHP copy-on-write behavior for by-value aliases while still +mutating reference aliases. + +## Functions and callable dispatch + +Eval-declared functions are callable from later eval fragments, from native code +after the eval barrier, and from string-literal `call_user_func()` / +`call_user_func_array()` paths. Eval-declared functions and registered AOT +global user functions support positional, named, and spread arguments inside +eval fragments. String keys in unpacked argument arrays bind as named +parameters. + +String-variable and expression callable calls such as `$fn(...)` and +`$callbacks[0](...)` share the eval callable dispatcher for supported builtins, +eval-declared functions, and registered AOT functions. + +Inside eval fragments, two-element object-method callable arrays such as +`[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, +...)`, `call_user_func_array($cb, [...])`, and `iterator_apply()` with +positional arguments. + +Post-barrier native direct calls and string-literal `call_user_func()` callbacks +currently accept simple positional arguments. Post-barrier +`call_user_func_array()` callbacks can pass indexed or string-keyed argument +containers to eval-declared functions. + +## Classes and objects + +Eval-declared classes support public properties, public methods, and +`__construct()`. Eval object construction can allocate eval-declared classes, +`stdClass`, and emitted AOT classes visible through runtime class metadata. +Missing class names during eval object construction fail with an eval runtime +fatal diagnostic. + +AOT and eval-declared class-name probes are visible through `class_exists()`. +Eval object relation probes through `is_a()` and `is_subclass_of()` use +generated AOT class/interface metadata and eval-created object metadata. +`interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated +AOT metadata. + +Public declared property reads/writes through `$this->property` from native +methods are bridged to eval. Public zero-, one-, or two-scalar-argument method +calls through `$this->method(...)` are supported by the native method bridge. + +## Namespaces and constants + +Eval namespace declarations qualify function declarations, class declarations, +object construction names, and qualified references against the active +namespace. Unqualified function and constant references fall back to the global +builtin/constant namespace when the namespaced symbol is absent. + +Simple and grouped `use`, `use function`, and `use const` declarations are +resolved while the bridge parser builds EvalIR: class imports rewrite `new` +targets, function imports rewrite unqualified calls, and constant imports +rewrite unqualified constant fetches in the active namespace declaration +region. + +Inside eval, `define()` stores dynamic constants that persist across later eval +fragments, `defined()` probes them, and bare constant expressions fetch their +retained boxed values. Native `defined("Name")`, bare constant fetches, and +string-literal `class_exists("Name")` calls after an eval barrier also probe +eval-created dynamic symbols. Duplicate eval `define()` calls keep the first +value, return `false`, and emit the same suppressible duplicate-constant warning +as AOT `define()`. + +Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, +`PHP_INT_MAX`, `INF`, `NAN`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, +`COUNT_*`, and the supported `PREG_*` / `JSON_*` constants. `defined()` sees +these names, including an optional leading `\`, and `define()` cannot replace +them. + +## Builtins available through eval + +Eval builtin dispatch supports direct calls, named arguments, callable +dispatch, `call_user_func()`, `call_user_func_array()`, and `function_exists()` +where listed below unless a note says otherwise. + +| Area | Builtins | +|---|---| +| System, time, and environment | `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()` | +| Filesystem and paths | `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()` | +| Stream introspection | `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()` | +| Network and protocol databases | `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()` | +| Strings, bytes, and formatting | `strlen()`, `ord()`, `chr()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `ucwords()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `number_format()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()` | +| Hashing | `crc32()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()` | +| JSON | `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()` | +| Regex | `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()` | +| Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | +| Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | +| Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `is_a()`, `is_subclass_of()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()` | +| Debug output | `print_r()`, `var_dump()` | +| Constants | `define()`, `defined()` | + +## Builtin notes + +Eval `array_map()` supports one or more source arrays with a string callback or +`null` callback. One-array results preserve source keys, multi-array results +are reindexed, missing source values are padded with `null`, and +`array_map(null, ...)` returns zipped row arrays. + +Eval `array_filter()` supports the PHP default omitted/null callback form, +filters falsey values, preserves source keys, and supports +`ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and +`ARRAY_FILTER_USE_KEY`. + +Eval mutating array builtins such as `array_pop()`, `array_shift()`, +`array_push()`, `array_unshift()`, `array_splice()`, `sort()`, `rsort()`, +`asort()`, `arsort()`, `ksort()`, `krsort()`, `natsort()`, `natcasesort()`, +`shuffle()`, `usort()`, `uksort()`, and `uasort()` write back through direct +variable calls. When reached through dynamic callable dispatch, they follow +PHP's by-value callback behavior: the return value is computed from the +supplied array, a by-reference warning is emitted where PHP would emit one, and +the caller's original array is not mutated. + +Eval regex dispatch uses Rust's `regex` engine for common PCRE-style delimited +patterns. It strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` +modifiers, supports common capture array shapes and replacement references, and +supports `PREG_SPLIT_NO_EMPTY`, `PREG_SPLIT_DELIM_CAPTURE`, and +`PREG_SPLIT_OFFSET_CAPTURE`. PCRE constructs unsupported by Rust `regex` fail +as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as +documented in [Regex](regex.md). + +Eval JSON support covers null, booleans, integers, floats, strings, indexed +arrays, associative arrays, and `stdClass` dynamic properties. `json_encode()` +supports zero flags plus the documented `JSON_HEX_*`, +`JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, +`JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, +`JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, +`JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR` flags. `json_decode()` +and `json_validate()` support PHP-compatible depth handling, malformed UTF-8 +ignore/substitute modes where applicable, `JSON_BIGINT_AS_STRING` for +overflowing integer tokens in `json_decode()`, and `JsonException` through +`JSON_THROW_ON_ERROR`. + +Eval local filesystem calls operate on host filesystem paths. Stream wrappers, +PHAR URLs, network URLs, ownership/group modification, and `fstat()` array +results remain outside the eval filesystem subset. Stream wrapper functionality +for native code is documented in [Streams](streams.md). + +Eval `print_r()` supports the one-argument form. Scalars print through the same +output path as `echo`, boolean false and null print nothing, and arrays print +the same `Array\n` header shape as elephc's native `print_r()` subset. + +Eval `var_dump()` supports the one-argument form. Scalars print typed +diagnostic lines, and indexed or associative arrays print foreach-visible keys +and nested values through eval value hooks. + +## Current limitations + +Eval executes through the `elephc_eval` interpreter bridge, not through the full +static AST -> EIR -> native codegen pipeline used for ordinary elephc source. +Unsupported constructs and missing class names during eval object construction +fail at runtime with an eval fatal diagnostic. + +The fragment subset is broad but not the full elephc language surface. In +particular, advanced native callable descriptors, closure callback values, and +static-method callable arrays are still outside eval fragments. Object method +calls through eval support positional arguments and numeric array unpacking; +named method arguments remain unsupported. + +Eval class support is intentionally smaller than the full static class system: +public properties, public methods, constructors, `stdClass`, AOT object +construction, public property bridge access, and basic public method bridge +calls are supported, but broader class-system features require future EvalIR and +dynamic symbol-table expansion. + +Because `eval()` is a dynamic barrier, the compiler must be conservative after +an eval call. Values that cross the barrier may be widened to boxed `Mixed` +storage internally, and optimizer/type facts from before the call cannot be +blindly reused afterward. diff --git a/docs/php/fibers.md b/docs/php/fibers.md index 039d1eb1b1..accd17c503 100644 --- a/docs/php/fibers.md +++ b/docs/php/fibers.md @@ -2,7 +2,7 @@ title: "Fibers" description: "Cooperative coroutines (PHP 8.1+ Fiber): create, start, suspend, resume, with FiberError." sidebar: - order: 15 + order: 16 --- A `Fiber` is a cooperative coroutine: a callable that owns its own call stack and can suspend execution at arbitrary depth. Control alternates explicitly between the caller and the fiber — there is no preemption. diff --git a/docs/php/generators.md b/docs/php/generators.md index 176c53a0f3..7b03376d89 100644 --- a/docs/php/generators.md +++ b/docs/php/generators.md @@ -2,7 +2,7 @@ title: "Generators" description: "Generator functions with yield, the built-in Iterator and Generator types, foreach over Iterator objects, and Generator::send for coroutine-style flow." sidebar: - order: 16 + order: 17 --- A *generator* is a function whose body uses the `yield` keyword. Calling a diff --git a/docs/php/magic-constants.md b/docs/php/magic-constants.md index e6059a201e..a69525cbb3 100644 --- a/docs/php/magic-constants.md +++ b/docs/php/magic-constants.md @@ -2,7 +2,7 @@ title: "Magic Constants" description: "PHP magic constants (__DIR__, __FILE__, __LINE__, __FUNCTION__, __CLASS__, __METHOD__, __NAMESPACE__, __TRAIT__) resolved at compile time to plain string or integer literals." sidebar: - order: 14 + order: 15 --- PHP defines a set of *magic constants* that change value depending on where they appear in the source. elephc supports all eight, lowering each to a plain string or integer literal at compile time. They behave identically to PHP for every common case. diff --git a/docs/php/math.md b/docs/php/math.md index cd627afeb8..1891412f6b 100644 --- a/docs/php/math.md +++ b/docs/php/math.md @@ -2,7 +2,7 @@ title: "Math" description: "Mathematical functions: abs, floor, ceil, round, clamp, trigonometry, logarithms, and more." sidebar: - order: 8 + order: 9 --- ## Built-in math functions diff --git a/docs/php/namespaces.md b/docs/php/namespaces.md index bb78173d85..a4d2db3bd1 100644 --- a/docs/php/namespaces.md +++ b/docs/php/namespaces.md @@ -2,7 +2,7 @@ title: "Namespaces" description: "Namespace declarations, use imports, name resolution, include/require." sidebar: - order: 11 + order: 12 --- ## Declaring a namespace diff --git a/docs/php/pdo.md b/docs/php/pdo.md index cf290c7520..8c06df2d66 100644 --- a/docs/php/pdo.md +++ b/docs/php/pdo.md @@ -2,7 +2,7 @@ title: "PDO (Databases)" description: "PDO database access with the SQLite, PostgreSQL, and MySQL/MariaDB drivers: connections, prepared statements, fetch modes, and transactions." sidebar: - order: 17 + order: 18 --- elephc supports a practical subset of PHP's PDO database layer, with the diff --git a/docs/php/regex.md b/docs/php/regex.md index f3940cb341..e3e4fb215c 100644 --- a/docs/php/regex.md +++ b/docs/php/regex.md @@ -2,7 +2,7 @@ title: "Regex" description: "PCRE2-backed regular expressions, preg_* functions, SPL regex iterators, and native library requirements." sidebar: - order: 6 + order: 7 --- elephc implements PHP regular expressions with PCRE2 through PCRE2's diff --git a/docs/php/spl.md b/docs/php/spl.md index bf6fdfc2b0..2b4ce11360 100644 --- a/docs/php/spl.md +++ b/docs/php/spl.md @@ -2,7 +2,7 @@ title: "SPL" description: "Standard PHP Library interfaces, exceptions, and runtime-backed container classes." sidebar: - order: 10 + order: 11 --- elephc ships the SPL pieces that are needed by supported PHP code today: diff --git a/docs/php/streams.md b/docs/php/streams.md index 260b899f26..40eb9cd356 100644 --- a/docs/php/streams.md +++ b/docs/php/streams.md @@ -2,7 +2,7 @@ title: "Streams" description: "Stream resources, wrappers, contexts, filters, sockets, TLS, and process pipes." sidebar: - order: 13 + order: 14 --- ## Resource model diff --git a/docs/php/strings.md b/docs/php/strings.md index 251e739f51..c13a4c1344 100644 --- a/docs/php/strings.md +++ b/docs/php/strings.md @@ -2,7 +2,7 @@ title: "Strings" description: "String types, escape sequences, interpolation, heredoc/nowdoc, and built-in string functions." sidebar: - order: 5 + order: 6 --- ## Double-quoted strings diff --git a/docs/php/system-and-io.md b/docs/php/system-and-io.md index 6046af4658..5c8744ce97 100644 --- a/docs/php/system-and-io.md +++ b/docs/php/system-and-io.md @@ -2,7 +2,7 @@ title: "System & I/O" description: "System functions, date/time, JSON, filesystem utilities, process execution, and debugging utilities." sidebar: - order: 12 + order: 13 --- ## System functions @@ -20,7 +20,6 @@ sidebar: | `putenv()` | `putenv($assignment): bool` | Set environment variable ("KEY=VALUE") | | `define()` | `define($name, $value): bool` | Define a compile-time global constant with a string-literal name | | `defined()` | `defined($name): bool` | Check whether a string-literal constant name is defined | -| `eval()` | `eval($code): mixed` | Parse and execute a PHP fragment in the caller's local scope | | `php_uname()` | `php_uname($mode = "a"): string` | Get system information from the target runtime | | `phpversion()` | `phpversion(): string` | Get the elephc package version from `Cargo.toml` | | `exec()` | `exec($command): string` | Execute command, return output | @@ -30,119 +29,6 @@ sidebar: `define()` returns `true` the first time a constant is defined at runtime. Duplicate attempts keep the first value, return `false`, and emit a suppressible runtime warning. `defined()` currently requires a string literal in AOT mode. -`eval()` is a PHP language construct rather than a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. Programs that call `eval()` link the optional `elephc_eval` bridge; programs that do not use it keep the normal native runtime only. - -The evaluated string must be a PHP fragment without an opening `>=`, `.=`), simple variable increment/decrement statements (`++$x`, `$x++`, `--$x`, `$x--`), `echo` including comma-separated arguments, `print`, `return`, `throw`, `try`/`catch (Throwable $e)` and class-specific and union `catch` clauses, `unset()`, `isset()`, `empty()`, fragment `__LINE__`, eval call-site `__FILE__` and `__DIR__`, empty eval-scope `__CLASS__` and `__TRAIT__`, namespace-aware `__NAMESPACE__`, `__FUNCTION__` and `__METHOD__` inside eval-declared functions, nested `eval()`, supported builtin calls (`time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `chr()`, `str_repeat()`, `substr()`, `substr_replace()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, `spl_classes()`, `call_user_func()`, `call_user_func_array()`, `is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same context, static locals inside eval-declared functions that persist across calls through that context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments, AOT and eval-declared class-name probes through `class_exists()`, eval object relation probes through `is_a()` and `is_subclass_of()` for generated AOT class/interface metadata and eval-created objects, eval resource introspection through `get_resource_type()` and `get_resource_id()`, duplicate eval-declared class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT object construction with `new ClassName(...)` for emitted classes including supported public constructors, public declared property reads/writes through `$this->property` from native methods, public zero-, one-, or two-scalar-argument method calls through `$this->method(...)`, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers, post-barrier native `function_exists()` and `is_callable()` probes for eval-declared functions, global registration for functions declared by `eval()` from a namespace scope, duplicate dynamic-function rejection, braced and single-statement bodies for `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, value-only and key-value `foreach` over indexed and associative arrays, and `switch`, `break`, `continue`, indexed and associative array literals, indexed-array append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes on associative arrays, unary numeric operators (`+`, `-`), integer bitwise NOT (`~`), binary `+`, `-`, `*`, `**`, `/`, `%`, `.`, integer bitwise and shift operators (`&`, `|`, `^`, `<<`, `>>`), logical negation (`!`), short-circuit logical operators (`&&`, `||`, `and`, `or`), boolean exclusive-or (`xor`), null coalescing (`??`), ternary and short ternary expressions (`?:`), loose equality (`==`, `!=`), strict equality (`===`, `!==`), ordered numeric comparisons (`<`, `<=`, `>`, `>=`) and numeric spaceship (`<=>`), and parentheses. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. - -When `eval()` runs inside a closure, by-value `use ($x)` captures synchronize only the closure's captured copy. By-reference `use (&$x)` captures write through the shared source variable, so mutations made by the evaluated fragment are visible to the outer scope. - -Eval `finally` is part of the eval control-flow subset and runs before a fragment returns or propagates a `Throwable`. - -Eval `catch (Throwable)` may omit the catch variable when the fragment only needs to handle and discard the thrown object. - -Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, ...)`, and `call_user_func_array($cb, [...])` with positional arguments. Static-method callable arrays and closure/descriptor callbacks are still outside the eval fragment subset. - -Eval `match` expressions support strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default` fallback. A miss without `default` is currently reported as an eval runtime fatal. - -Eval namespace declarations support both `namespace Name;` and `namespace Name { ... }` forms. Eval-declared functions and classes inside a namespace are registered under their qualified names, `__NAMESPACE__` reflects the active eval namespace, and unqualified function and constant references fall back to the global builtin/constant namespace when the namespaced symbol is absent. Simple and grouped `use`, `use function`, and `use const` declarations resolve class aliases for `new`, function aliases for unqualified calls, and constant aliases for unqualified constant fetches within the active namespace declaration region. - -Eval `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. Relative paths follow PHP's cwd-first lookup and then fall back to the eval call-site directory. Included PHP files may contain normal `` blocks, raw text outside PHP tags is echoed, a `return` inside the included file becomes the include expression value, successful includes without `return` evaluate to `1`, repeated `*_once` includes evaluate to `true`, missing `include` returns `false` with warnings, and missing `require` aborts the eval fragment. - -Eval array literals accept both the modern `[...]` syntax and PHP's legacy `array(...)` syntax, with both forms lowered to the same runtime array representation. - -The URL codec builtins `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` are available through eval direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. - -Eval also supports ASCII character-class predicates `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` with the same direct, named-argument, callable, and `function_exists()` dispatch paths. - -Eval `print_r()` supports the one-argument form through direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Scalars print through the same output path as `echo`, boolean false and null print nothing, and arrays print the same `Array\n` header shape as elephc's native `print_r()` subset. - -Eval `var_dump()` supports the one-argument form through direct calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. Scalars print typed diagnostic lines, and indexed or associative arrays print foreach-visible keys and nested values through eval value hooks. - -The word-case helper `ucwords()` is available inside eval with its optional `separators` parameter. - -Eval `strstr()` returns matching suffixes, `before_needle` prefixes, or `false` when the needle is absent. - -Eval `str_split()` returns indexed arrays of fixed-width string chunks and supports the optional `length` parameter. - -Eval `str_pad()` supports the PHP pad string and pad type arguments for left, right, and both-side padding. - -Eval `wordwrap()` supports width, break string, and `cut_long_words` arguments while preserving existing newlines. - -Eval `number_format()` supports decimal counts plus custom decimal and thousands separators. - -Eval printf-family formatting supports `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()` with direct calls, named arguments for declared parameters, callable dispatch, `function_exists()` probes, scalar coercions, `%%`, and the common `%s`, integer, float, width, precision, and sign/padding flags covered by the native runtime subset. - -Eval `json_encode()` supports zero-flags encoding plus `JSON_HEX_TAG`, `JSON_HEX_AMP`, `JSON_HEX_APOS`, `JSON_HEX_QUOT`, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR` for null, booleans, integers, floats, strings, indexed arrays, associative arrays, and `stdClass` dynamic properties. The eval encoder uses default slash and control-character escaping unless `JSON_UNESCAPED_SLASHES` is set, escapes multibyte UTF-8 to `\uXXXX` or surrogate pairs unless `JSON_UNESCAPED_UNICODE` is set, converts indexed arrays to JSON objects when `JSON_FORCE_OBJECT` is set, applies the supported JSON hex escaping flags to strings and object keys, converts numeric strings to JSON numbers when `JSON_NUMERIC_CHECK` is set, substitutes non-finite floats with `0` under `JSON_PARTIAL_OUTPUT_ON_ERROR`, raises eval `JsonException` objects for non-finite floats and malformed UTF-8 under `JSON_THROW_ON_ERROR` unless partial-output keeps the substituted result, formats arrays and `stdClass` dynamic properties with PHP's four-space pretty indentation when `JSON_PRETTY_PRINT` is set, preserves `.0` for whole-number floats when `JSON_PRESERVE_ZERO_FRACTION` is set, drops malformed UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, and rejects other non-zero flags until the full native JSON flag surface is bridged. Eval `json_decode()` and `json_validate()` support zero-flags JSON objects, arrays, strings, numbers, literals, escaped unicode pairs, raw UTF-8 string bytes, and PHP-compatible depth rejection; `json_decode()` also supports `JSON_BIGINT_AS_STRING` for overflowing integer tokens, drops malformed raw UTF-8 bytes under `JSON_INVALID_UTF8_IGNORE`, substitutes them with U+FFFD under `JSON_INVALID_UTF8_SUBSTITUTE`, returns `stdClass` objects by default or when `$associative` is false/null, returns associative arrays when `$associative` is true, and raises `JsonException` through `JSON_THROW_ON_ERROR` for syntax, depth, UTF-8, and UTF-16 parse failures; `json_validate()` supports `JSON_INVALID_UTF8_IGNORE` so malformed raw UTF-8 bytes inside JSON strings are ignored while syntax/depth errors still fail. Eval `json_last_error()` and `json_last_error_msg()` track eval-local `JSON_ERROR_INF_OR_NAN`, `JSON_ERROR_SYNTAX`, `JSON_ERROR_DEPTH`, `JSON_ERROR_CTRL_CHAR`, `JSON_ERROR_UTF8`, and `JSON_ERROR_UTF16` state for JSON failures, including the same line/column location suffix shape as the native JSON decoder, and successful eval JSON calls reset the state to `JSON_ERROR_NONE` / `"No error"`. - -Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins; one-array results preserve source keys, while multi-array results are reindexed, pad missing source values with `null`, and `array_map(null, ...)` returns zipped row arrays. - -Eval `array_reduce()` supports the two- or three-argument form with string callbacks, using `null` as the initial carry when the initial value is omitted. Callback resolution is shared with `call_user_func()`, so eval-declared functions, registered AOT callbacks, and supported builtins can fold the carry and current item. - -Eval `array_walk()` supports the two-argument form with string callbacks. It invokes the callback with value and key cells in PHP iteration order, ignores the callback return value, and returns `true`. - -Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` support direct variable calls with write-back into the eval scope. `array_pop()`, `array_shift()`, and `array_splice()` also accept named direct `array:` calls. Eval `array_splice()` returns the removed elements, accepts the optional `length` and `replacement` arguments, treats omitted named `length` as `null` when `replacement:` is supplied, inserts replacement arrays by value order while discarding replacement keys, reindexes integer keys, and preserves string keys. When these by-reference builtins are reached through `call_user_func()` or `call_user_func_array()`, they follow PHP's by-value callback behavior for non-reference arguments: the return value is computed from the supplied array, but the caller's original array is not mutated. - -Eval `sort()` and `rsort()` support one-argument direct variable calls with write-back into the eval scope for homogeneous numeric or string arrays. They reindex integer keys, return `true`, accept named direct `array:` calls, and emit PHP's by-reference warning without mutating the caller's original array when reached through `call_user_func()` or `call_user_func_array()`. Eval `asort()` and `arsort()` sort by value while preserving keys, `ksort()` / `krsort()` sort by key while preserving values, and `natsort()` / `natcasesort()` preserve keys while applying natural string ordering. - -Eval `shuffle()` supports one-argument direct variable calls with write-back into the eval scope. It randomizes element order, reindexes integer keys, returns `true`, and follows the same by-reference warning/no-mutation behavior as the other array ordering builtins when called dynamically. - -Eval `usort()`, `uksort()`, and `uasort()` support string callbacks that resolve to eval-declared functions, registered AOT callbacks, or supported builtins. Direct variable calls write the sorted replacement back into the eval scope; dynamic callable dispatch still invokes the comparator on a by-value copy, emits PHP's by-reference warning, returns `true`, and leaves the caller's original array unchanged. - -Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and dispatches through direct, named-argument, `call_user_func()`, `call_user_func_array()`, and `function_exists()` paths. String callbacks can target eval-declared functions, registered AOT callbacks, or supported builtins, and the `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` modes select value-only, value-and-key, or key-only callback arguments. - -Eval `iterator_count()` and `iterator_to_array()` support array inputs, matching PHP's `Traversable|array` helper behavior for arrays. `iterator_to_array()` preserves keys by default and reindexes when `preserve_keys` is `false`. Eval `iterator_apply()` supports Traversable objects through `rewind()`, `valid()`, `next()`, string callbacks that target eval-declared functions, registered AOT callbacks, or supported builtins, and object-method callable arrays. Arrays still fail for `iterator_apply()`, matching PHP's array `TypeError` rather than treating arrays as Traversable. - -Eval regex dispatch supports `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` for common PCRE-style delimited patterns. The eval bridge strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` modifiers through Rust's `regex` engine, builds direct `preg_match()` capture arrays with optional `PREG_OFFSET_CAPTURE` and `PREG_UNMATCHED_AS_NULL` entries, builds direct `preg_match_all()` capture arrays in default `PREG_PATTERN_ORDER` form and `PREG_SET_ORDER` match-order form with optional `PREG_OFFSET_CAPTURE` and `PREG_UNMATCHED_AS_NULL` entries, expands `$n`, `${n}`, and `\n` replacements, and supports `PREG_SPLIT_NO_EMPTY`, `PREG_SPLIT_DELIM_CAPTURE`, and `PREG_SPLIT_OFFSET_CAPTURE`; PCRE constructs unsupported by Rust `regex` fail as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as documented in [Regex](regex.md). - -Eval `count()` supports normal and recursive modes, including the eval-visible `COUNT_NORMAL` and `COUNT_RECURSIVE` constants. - -Eval `crc32()` computes the same non-negative CRC-32 integer as the static runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` use the same supported algorithm table as elephc's crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and returns `false` when the file cannot be read. Eval `hash_algos()` returns the same indexed supported-algorithm name list as the native runtime helper. - -Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` reports the root elephc package version, `php_uname()` supports the same `a`/`s`/`n`/`r`/`v`/`m` modes as the native runtime helper, and `sys_get_temp_dir()` matches the native static helper's `/tmp` literal. - -Eval `microtime()` returns a float Unix timestamp with microsecond precision, matching elephc's static `microtime()` lowering. Eval `date()` supports elephc's documented date tokens, `mktime()` builds local timestamps through libc normalization, and `strtotime()` accepts `now`, `YYYY-MM-DD`, `YYYY-MM-DD HH:MM`, `YYYY-MM-DD HH:MM:SS`, and the same datetime forms with `T` as the separator. Unsupported eval `strtotime()` strings return `-1`. - -Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns `0` when the sleep completes; `usleep()` returns `null`. - -Eval environment calls include `getenv()` and `putenv()`. Missing variables read back as an empty string, matching elephc's static runtime convention. - -Eval local filesystem calls include `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain outside this eval filesystem subset. - -Eval stream introspection calls include `stream_get_filters()`, `stream_get_transports()`, and `stream_get_wrappers()` with direct, callable, and `function_exists()` dispatch paths. They return the same static built-in lists documented in [Streams](streams.md). - -Eval SPL class introspection supports `spl_classes()` with direct, callable, and -`function_exists()` dispatch paths. It returns the same static registry snapshot -documented in [SPL](spl.md). Eval SPL object identity supports -`spl_object_id()` and `spl_object_hash()` for object cells through direct, -named-argument, callable, and `function_exists()` paths. - -Eval class-like metadata probes include `trait_exists()` and `enum_exists()` for -generated AOT trait and enum names, with case-insensitive matching, optional -autoload argument evaluation, named arguments, callable dispatch, and -`function_exists()` visibility. - -Eval host-name calls include `gethostname()`, `gethostbyname()`, and `gethostbyaddr()` with direct, named-argument, callable, and `function_exists()` dispatch paths. `gethostbyname()` resolves IPv4 host names and returns the original string when no IPv4 address is available. `gethostbyaddr()` reverse-resolves IPv4 dotted-quad addresses, returns the original address when no record exists, and returns `false` for malformed addresses. - -Eval protocol and service database calls include `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, and `getservbyport()` with direct, named-argument, callable, and `function_exists()` dispatch paths. - -Eval IPv4 conversion calls include `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` with direct, named-argument, callable, and `function_exists()` dispatch paths. Invalid parses return `false`. - -Eval path component calls include `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` with suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, named arguments, callable dispatch, and `function_exists()` probes. - -Eval predefined constants include `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `INF`, `NAN`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, `COUNT_*`, and the supported `PREG_*` / `JSON_*` constants. `defined()` sees these names, including an optional leading `\`, and `define()` cannot replace them. - -Eval `realpath()` canonicalizes existing paths through the host filesystem and returns `false` when the path cannot be resolved. - -Eval realpath-cache calls match elephc's native no-cache convention: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns `0`. - -Inside eval, `define()` stores dynamic constants that persist across later eval fragments, `defined()` probes them, and bare constant expressions fetch their retained boxed values. Native `defined("Name")`, bare constant fetches, and string-literal `class_exists("Name")` calls after an eval barrier also probe eval-created dynamic symbols. Duplicate eval `define()` calls keep the first value, return `false`, and emit the same suppressible duplicate-constant warning as AOT `define()`. - -Eval-declared function calls, registered AOT global user-function calls, and supported PHP-visible builtin calls inside eval also support PHP argument unpacking (`...`) and `call_user_func_array()` argument arrays; string keys bind as named parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share that eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. Object method calls through eval support positional arguments and numeric array unpacking, while named method arguments remain unsupported. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls, while top-level `static` declarations in separate eval fragments are initialized for each eval execution. Eval `global` aliases can read and write compiler-known global storage; `global $argc` and `global $argv` inside function eval fragments read the CLI argument globals. `unset($name)` after such an alias removes the local alias without unsetting the global value, matching PHP. `throw ;` inside eval and inside eval-declared functions propagates `Throwable` objects through the normal native `try`/`catch` unwinder. Eval `try/catch/finally` can catch `Throwable` and class-specific and union clauses inside the fragment and always runs `finally` before propagating or returning. Eval language constructs such as `isset()`, `empty()`, and nested `eval()`, plus post-barrier direct native calls and `call_user_func()` callbacks, still accept simple positional arguments; post-barrier `call_user_func_array()` callbacks can pass indexed or string-keyed argument containers to eval-declared functions. - -Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule for unkeyed entries and append keys, including integer-string keys such as `"2"`, boolean and float keys that normalize to integers, and `null` keys that normalize to the empty string. Eval array writes preserve native PHP copy-on-write behavior for by-value aliases while still mutating reference aliases. - -Eval builtin dispatch also supports string and byte helpers such as `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, plus numeric formatting through `number_format()`, printf-family formatting through `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, string scanning through `sscanf()`, scalar math calls `clamp()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, and `random_int()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, and floating-point math calls `fdiv()` and `fmod()`, including `fdiv()` zero handling through `INF` and `NAN` results. - `php_uname()` supports PHP's standard one-character modes: | Mode | Result | From 2f06ce93c34db86339c731811184ae4421ca1335 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 09:45:47 +0200 Subject: [PATCH 0276/1208] Split eval FFI bridge modules --- crates/elephc-eval/src/ffi/context.rs | 127 ++ crates/elephc-eval/src/ffi/execute.rs | 127 ++ crates/elephc-eval/src/ffi/function_calls.rs | 170 +++ crates/elephc-eval/src/ffi/mod.rs | 30 + .../elephc-eval/src/ffi/native_functions.rs | 135 ++ crates/elephc-eval/src/ffi/scope.rs | 163 ++ crates/elephc-eval/src/ffi/symbols.rs | 196 +++ crates/elephc-eval/src/ffi/tests.rs | 384 +++++ crates/elephc-eval/src/ffi/util.rs | 122 ++ crates/elephc-eval/src/lib.rs | 1305 +---------------- 10 files changed, 1460 insertions(+), 1299 deletions(-) create mode 100644 crates/elephc-eval/src/ffi/context.rs create mode 100644 crates/elephc-eval/src/ffi/execute.rs create mode 100644 crates/elephc-eval/src/ffi/function_calls.rs create mode 100644 crates/elephc-eval/src/ffi/mod.rs create mode 100644 crates/elephc-eval/src/ffi/native_functions.rs create mode 100644 crates/elephc-eval/src/ffi/scope.rs create mode 100644 crates/elephc-eval/src/ffi/symbols.rs create mode 100644 crates/elephc-eval/src/ffi/tests.rs create mode 100644 crates/elephc-eval/src/ffi/util.rs diff --git a/crates/elephc-eval/src/ffi/context.rs b/crates/elephc-eval/src/ffi/context.rs new file mode 100644 index 0000000000..690a8c7b61 --- /dev/null +++ b/crates/elephc-eval/src/ffi/context.rs @@ -0,0 +1,127 @@ +//! Purpose: +//! Exports eval context handle allocation and context metadata setters. +//! These functions manage process-level eval state and call-site/global-scope +//! metadata used while executing fragments. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_context_*` symbols. +//! +//! Key details: +//! - Context handles are opaque across the ABI. +//! - Call-site metadata is UTF-8 and is validated before storing. + +use super::util::abi_name_to_string; +use crate::abi::{ElephcEvalContext, ElephcEvalScope, ABI_VERSION}; +use crate::errors::EvalStatus; + +/// Returns the ABI version expected by generated elephc eval call sites. +#[no_mangle] +pub extern "C" fn __elephc_eval_abi_version() -> u32 { + ABI_VERSION +} + +/// Allocates a process-level eval context handle for generated code. +#[no_mangle] +pub extern "C" fn __elephc_eval_context_new() -> *mut ElephcEvalContext { + Box::into_raw(Box::new(ElephcEvalContext::new())) +} + +/// Frees a process-level eval context handle allocated by the eval bridge. +/// +/// # Safety +/// `ctx` must be null or a pointer returned by `__elephc_eval_context_new` +/// that has not already been freed. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_free(ctx: *mut ElephcEvalContext) { + if !ctx.is_null() { + drop(Box::from_raw(ctx)); + } +} + +/// Records source metadata for the next eval fragment executed in this context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `file_ptr` and `dir_ptr` must be +/// readable for their matching lengths when the length is greater than zero. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_set_call_site( + ctx: *mut ElephcEvalContext, + file_ptr: *const u8, + file_len: u64, + dir_ptr: *const u8, + dir_len: u64, + line: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_context_set_call_site_inner(ctx, file_ptr, file_len, dir_ptr, dir_len, line) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Records the materialized program-global eval scope for `global` aliases. +/// +/// # Safety +/// `ctx` and `scope` must be valid handles allocated by the eval bridge. The +/// context does not own `scope`; generated code must keep the scope alive for +/// as long as the context can execute eval fragments that reference globals. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_set_global_scope( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_context_set_global_scope_inner(ctx, scope) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the call-site metadata setter ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_set_call_site`; callers must pass a valid +/// context and readable UTF-8 file/directory byte slices. +unsafe fn eval_context_set_call_site_inner( + ctx: *mut ElephcEvalContext, + file_ptr: *const u8, + file_len: u64, + dir_ptr: *const u8, + dir_len: u64, + line: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(file) = abi_name_to_string(file_ptr, file_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(dir) = abi_name_to_string(dir_ptr, dir_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(line) = i64::try_from(line) else { + return EvalStatus::RuntimeFatal.code(); + }; + context.set_call_site(file, dir, line); + EvalStatus::Ok.code() +} + +/// Runs the global-scope setter ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_set_global_scope`; callers must pass valid +/// context and scope handles owned by generated code. +unsafe fn eval_context_set_global_scope_inner( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if !context.set_global_scope(scope) { + return EvalStatus::RuntimeFatal.code(); + } + EvalStatus::Ok.code() +} diff --git a/crates/elephc-eval/src/ffi/execute.rs b/crates/elephc-eval/src/ffi/execute.rs new file mode 100644 index 0000000000..f8b556fa0d --- /dev/null +++ b/crates/elephc-eval/src/ffi/execute.rs @@ -0,0 +1,127 @@ +//! Purpose: +//! Exports eval fragment execution through the optional bridge. +//! This layer validates ABI pointers, parses fragment bytes, and dispatches +//! parsed EvalIR to the interpreter with runtime hooks in production builds. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_execute`. +//! +//! Key details: +//! - Tests keep a controlled unsupported stub because generated runtime wrappers +//! are not linked into the crate unit-test binary. + +use super::util::clear_result; +#[cfg(not(test))] +use super::util::write_outcome; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::eval_ir; +#[cfg(not(test))] +use crate::interpreter; +use crate::parser; +#[cfg(not(test))] +use crate::runtime_hooks::ElephcRuntimeOps; +use std::slice; + +/// Executes an eval fragment against a materialized caller scope. +/// +/// The FFI shape is final for the initial bridge: context/scope are opaque +/// runtime handles, `code_ptr`/`code_len` identify the PHP fragment bytes, and +/// `out` receives the eval return cell when provided. Non-test builds execute +/// the current EvalIR subset; test builds return `UnsupportedConstruct` because +/// they do not link elephc's generated runtime value wrappers. +/// +/// # Safety +/// Callers must pass valid pointers for any non-null handle and ensure +/// `code_ptr` is readable for `code_len` bytes when `code_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_execute( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, + code_ptr: *const u8, + code_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { execute_eval_inner(ctx, scope, code_ptr, code_len, out) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the eval ABI body after the exported wrapper has installed a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_execute`; callers must provide valid handles and code +/// storage for every non-null pointer argument. +unsafe fn execute_eval_inner( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, + code_ptr: *const u8, + code_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + if !ctx.is_null() && (*ctx).abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if code_len > 0 && code_ptr.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(code_len) = usize::try_from(code_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let code = if code_len == 0 { + &[] + } else { + slice::from_raw_parts(code_ptr, code_len) + }; + let program = match parser::parse_fragment(code) { + Ok(program) => program, + Err(err) => return err.status().code(), + }; + clear_result(out); + execute_parsed_eval(ctx, scope, &program, out) +} + +/// Executes a parsed eval program in production builds using elephc runtime hooks. +/// +/// # Safety +/// `scope` and `out` must be null or valid pointers supplied by generated code. +#[cfg(not(test))] +unsafe fn execute_parsed_eval( + ctx: *mut ElephcEvalContext, + scope: *mut ElephcEvalScope, + program: &eval_ir::EvalProgram, + out: *mut ElephcEvalResult, +) -> i32 { + let mut fallback_context; + let context = if let Some(ctx) = ctx.as_mut() { + ctx + } else { + fallback_context = ElephcEvalContext::new(); + &mut fallback_context + }; + let mut fallback_scope; + let scope = if let Some(scope) = scope.as_mut() { + scope + } else { + fallback_scope = ElephcEvalScope::new(); + &mut fallback_scope + }; + let mut values = ElephcRuntimeOps::new(); + match interpreter::execute_program_outcome_with_context(context, program, scope, &mut values) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Keeps crate unit tests independent from generated runtime assembly wrappers. +/// +/// # Safety +/// `out` must be null or valid result storage supplied by the test caller. +#[cfg(test)] +unsafe fn execute_parsed_eval( + _ctx: *mut ElephcEvalContext, + _scope: *mut ElephcEvalScope, + _program: &eval_ir::EvalProgram, + _out: *mut ElephcEvalResult, +) -> i32 { + EvalStatus::UnsupportedConstruct.code() +} diff --git a/crates/elephc-eval/src/ffi/function_calls.rs b/crates/elephc-eval/src/ffi/function_calls.rs new file mode 100644 index 0000000000..9c940c7dbe --- /dev/null +++ b/crates/elephc-eval/src/ffi/function_calls.rs @@ -0,0 +1,170 @@ +//! Purpose: +//! Exports post-barrier calls into functions declared by eval fragments. +//! Generated code can call zero-argument, positional, or argument-array forms +//! after probing dynamic function existence. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_call_function*`. +//! +//! Key details: +//! - Calls return either a value cell or an uncaught throwable cell through +//! `ElephcEvalResult`. +//! - Argument pointer arrays are borrowed from generated code and not released here. + +use super::util::{abi_name_to_string, clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::interpreter; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; +use std::slice; + +/// Calls a zero-argument function previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function_zero_args( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_inner(ctx, name_ptr, name_len, std::ptr::null(), 0, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a function previously declared through `eval()` with positional cells. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `args` must be readable for +/// `arg_count` runtime-cell pointers when `arg_count > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_inner(ctx, name_ptr, name_len, args, arg_count, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a function previously declared through `eval()` with an argument array/hash. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `arg_array` must be a boxed Mixed +/// indexed or associative array cell, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_call_function_array( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + call_eval_function_array_inner(ctx, name_ptr, name_len, arg_array, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the dynamic function-call ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_call_function`; callers must provide a valid context, +/// readable function-name bytes, and readable argument pointer storage. +#[cfg(not(test))] +unsafe fn call_eval_function_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(arg_count) = usize::try_from(arg_count) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_count > 0 && args.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(args, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::new(); + match interpreter::execute_context_function_outcome( + context, + &name.to_ascii_lowercase(), + args, + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the dynamic function-call-array ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_call_function_array`; callers must provide a valid +/// context, readable function-name bytes, and a boxed array/hash argument cell. +#[cfg(not(test))] +unsafe fn call_eval_function_array_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_array.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + clear_result(out); + let mut values = ElephcRuntimeOps::new(); + match interpreter::execute_context_function_call_array_outcome( + context, + &name.to_ascii_lowercase(), + RuntimeCellHandle::from_raw(arg_array), + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} diff --git a/crates/elephc-eval/src/ffi/mod.rs b/crates/elephc-eval/src/ffi/mod.rs new file mode 100644 index 0000000000..55ea34c319 --- /dev/null +++ b/crates/elephc-eval/src/ffi/mod.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Groups the exported C ABI entry points for the optional eval bridge. +//! Submodules are organized by the handle or operation family they expose. +//! +//! Called from: +//! - `crate` root re-exports for Rust tests and generated-linkage symbols. +//! +//! Key details: +//! - Every exported function installs a panic boundary before touching bridge state. +//! - Helper routines stay private to the FFI layer unless shared across families. + +pub mod context; +pub mod execute; +#[cfg(not(test))] +pub mod function_calls; +pub mod native_functions; +pub mod scope; +pub mod symbols; +pub(crate) mod util; + +pub use context::*; +pub use execute::*; +#[cfg(not(test))] +pub use function_calls::*; +pub use native_functions::*; +pub use scope::*; +pub use symbols::*; + +#[cfg(test)] +mod tests; diff --git a/crates/elephc-eval/src/ffi/native_functions.rs b/crates/elephc-eval/src/ffi/native_functions.rs new file mode 100644 index 0000000000..ce81e919b6 --- /dev/null +++ b/crates/elephc-eval/src/ffi/native_functions.rs @@ -0,0 +1,135 @@ +//! Purpose: +//! Exports registration of generated native PHP callbacks into an eval context. +//! Eval fragments use this metadata to call AOT functions through descriptor +//! invokers while preserving PHP-visible parameter names. +//! +//! Called from: +//! - Generated EIR backend assembly before fragments can call AOT functions. +//! +//! Key details: +//! - Invalid names, handles, descriptors, or indexes fail closed as `false`. +//! - Function names are stored under their PHP case-insensitive folded key. + +use super::util::abi_name_to_string; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; +use crate::context::{NativeFunction, NativeFunctionInvoker}; +use std::ffi::c_void; + +/// Registers a generated native PHP function callback in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `descriptor` and `invoker` must follow +/// the descriptor-invoker ABI emitted by generated code. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + descriptor: *mut c_void, + invoker: Option, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_inner(ctx, name_ptr, name_len, descriptor, invoker, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Runs the native registration ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function`; invalid handles, names, or +/// callback pointers fail closed as `false`. +unsafe fn register_native_function_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + descriptor: *mut c_void, + invoker: Option, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || descriptor.is_null() { + return 0; + } + let Some(invoker) = invoker else { + return 0; + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + let function = NativeFunction::new(descriptor, invoker, param_count); + i32::from( + context + .define_native_function(name.to_ascii_lowercase(), function) + .is_ok(), + ) +} + +/// Runs the native parameter-name registration ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param`; invalid handles, +/// names, or indexes fail closed as `false`. +unsafe fn register_native_function_param_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_function_param( + &function_name.to_ascii_lowercase(), + param_index, + param_name, + )) +} diff --git a/crates/elephc-eval/src/ffi/scope.rs b/crates/elephc-eval/src/ffi/scope.rs new file mode 100644 index 0000000000..4938adfa95 --- /dev/null +++ b/crates/elephc-eval/src/ffi/scope.rs @@ -0,0 +1,163 @@ +//! Purpose: +//! Exports materialized eval activation-scope handle operations. +//! Scope entries carry runtime cell pointers plus dirty, ownership, unset, and +//! by-reference metadata used by generated code around eval barriers. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_scope_*` symbols. +//! +//! Key details: +//! - Owned runtime cells are released when a scope is freed or overwritten. +//! - Global aliases store source-target names rather than copying values. + +use super::util::{ + abi_name_to_string, release_owned_scope_cells, release_scope_cell, scope_entry_abi_flags, +}; +use crate::abi::{ElephcEvalScope, SCOPE_FLAG_OWNED}; +use crate::errors::EvalStatus; +use crate::scope::ScopeCellOwnership; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +/// Allocates a materialized activation scope handle for generated code. +#[no_mangle] +pub extern "C" fn __elephc_eval_scope_new() -> *mut ElephcEvalScope { + Box::into_raw(Box::new(ElephcEvalScope::new())) +} + +/// Frees a materialized activation scope handle allocated by the eval bridge. +/// +/// # Safety +/// `scope` must be null or a pointer returned by `__elephc_eval_scope_new` +/// that has not already been freed. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_free(scope: *mut ElephcEvalScope) { + if !scope.is_null() { + let mut scope = Box::from_raw(scope); + release_owned_scope_cells(&mut scope); + drop(scope); + } +} + +/// Stores a named runtime cell in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`; names must be UTF-8 variable names. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_set( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + cell: *mut RuntimeCell, + flags: u32, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let ownership = if flags & SCOPE_FLAG_OWNED != 0 { + ScopeCellOwnership::Owned + } else { + ScopeCellOwnership::Borrowed + }; + if let Some(replaced) = scope.set(name, RuntimeCellHandle::from_raw(cell), ownership) { + release_scope_cell(replaced); + } + EvalStatus::Ok.code() +} + +/// Looks up a named runtime cell in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. Output pointers may be null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_get( + scope: *const ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + out_cell: *mut *mut RuntimeCell, + out_flags: *mut u32, +) -> i32 { + let Some(scope) = scope.as_ref() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let entry = scope.entry(&name); + if !out_cell.is_null() { + *out_cell = entry + .filter(|entry| entry.flags().is_visible()) + .map(|entry| entry.cell().as_ptr()) + .unwrap_or(std::ptr::null_mut()); + } + if !out_flags.is_null() { + *out_flags = entry.map(scope_entry_abi_flags).unwrap_or(0); + } + EvalStatus::Ok.code() +} + +/// Marks a named runtime cell as unset in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`; names must be UTF-8 variable names. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_unset( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + if let Some(replaced) = scope.unset(name) { + release_scope_cell(replaced); + } + EvalStatus::Ok.code() +} + +/// Marks a local eval-scope variable as an alias of a program-global variable. +/// +/// # Safety +/// `scope` must be a valid eval scope handle. Name pointers must be readable +/// for their matching lengths when the length is greater than zero. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_mark_global_alias( + scope: *mut ElephcEvalScope, + name_ptr: *const u8, + name_len: u64, + global_name_ptr: *const u8, + global_name_len: u64, +) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(global_name) = abi_name_to_string(global_name_ptr, global_name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + scope.mark_global_alias_to(name, global_name); + EvalStatus::Ok.code() +} + +/// Clears dirty flags for every entry in a materialized eval scope. +/// +/// # Safety +/// `scope` must be a valid eval scope handle allocated by the eval bridge. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_scope_clear_dirty(scope: *mut ElephcEvalScope) -> i32 { + let Some(scope) = scope.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + scope.mark_all_clean(); + EvalStatus::Ok.code() +} diff --git a/crates/elephc-eval/src/ffi/symbols.rs b/crates/elephc-eval/src/ffi/symbols.rs new file mode 100644 index 0000000000..7a5e9c7401 --- /dev/null +++ b/crates/elephc-eval/src/ffi/symbols.rs @@ -0,0 +1,196 @@ +//! Purpose: +//! Exports dynamic symbol probes and constant fetching for eval-created state. +//! Generated code calls these after eval barriers to observe dynamic functions, +//! constants, and classes registered by interpreted fragments. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_*exists` symbols. +//! +//! Key details: +//! - Existence probes fail closed as `false` on invalid ABI inputs. +//! - Constant fetch retains the boxed cell before handing it back to generated code. + +use super::util::abi_name_to_string; +#[cfg(not(test))] +use super::util::clear_result; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; +#[cfg(not(test))] +use crate::abi::ElephcEvalResult; +#[cfg(not(test))] +use crate::errors::EvalStatus; +#[cfg(not(test))] +use crate::interpreter::RuntimeValueOps; +#[cfg(not(test))] +use crate::runtime_hooks::ElephcRuntimeOps; + +/// Checks whether a function was previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_function_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_function_exists_inner(ctx, name_ptr, name_len) }) + .unwrap_or(0) +} + +/// Checks whether a constant was previously defined through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_constant_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_constant_exists_inner(ctx, name_ptr, name_len) }) + .unwrap_or(0) +} + +/// Checks whether a class was previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be null or a valid eval context handle. `name_ptr` must be +/// readable for `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_dynamic_class_exists( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_dynamic_class_exists_inner(ctx, name_ptr, name_len) + }) + .unwrap_or(0) +} + +/// Fetches a constant previously defined through `eval()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_constant_fetch( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_constant_fetch_inner(ctx, name_ptr, name_len, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the eval function-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_function_exists`; invalid handles or unreadable name +/// storage fail closed as `false`. +unsafe fn eval_function_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_function(&name.to_ascii_lowercase())) +} + +/// Runs the eval constant-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_constant_exists`; invalid handles or unreadable name +/// storage fail closed as `false`. +unsafe fn eval_constant_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_constant(&name)) +} + +/// Runs the eval dynamic-class-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_dynamic_class_exists`; invalid handles or unreadable +/// name storage fail closed as `false`. +unsafe fn eval_dynamic_class_exists_inner( + ctx: *const ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + let Some(context) = ctx.as_ref() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + i32::from(context.has_class(&name)) +} + +/// Runs the eval constant-fetch ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_constant_fetch`; callers must provide a valid context, +/// readable constant-name bytes, and optional writable result storage. +#[cfg(not(test))] +unsafe fn eval_constant_fetch_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + clear_result(out); + let Some(value) = context.constant(&name) else { + return EvalStatus::RuntimeFatal.code(); + }; + if out.is_null() { + return EvalStatus::Ok.code(); + } + let mut values = ElephcRuntimeOps::new(); + match values.retain(value) { + Ok(result) => { + (*out).kind = 0; + (*out).value_cell = result.as_ptr(); + (*out).error = std::ptr::null_mut(); + EvalStatus::Ok.code() + } + Err(status) => status.code(), + } +} diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs new file mode 100644 index 0000000000..837d1e9d61 --- /dev/null +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -0,0 +1,384 @@ +//! Purpose: +//! Unit tests for the eval C ABI layer. +//! They validate handle allocation, stable status codes, scope flags, and +//! dynamic symbol registration without requiring generated runtime assembly. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Execute remains a controlled unsupported stub in crate unit tests. +//! - Fake runtime-cell pointers are never dereferenced by these tests. + +use super::context::*; +use super::execute::*; +use super::native_functions::*; +use super::scope::*; +use super::symbols::*; +use crate::abi::{ + ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_DIRTY, + SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, +}; +use crate::errors::EvalStatus; +use crate::value::{RuntimeCell, RuntimeCellHandle}; +use std::ffi::c_void; + +/// Test native invoker placeholder used only to validate ABI registration. +unsafe extern "C" fn fake_native_invoker( + _descriptor: *mut c_void, + _args: *mut RuntimeCell, +) -> *mut RuntimeCell { + std::ptr::null_mut() +} + +/// Verifies the exported version entry point reports the crate ABI constant. +#[test] +fn abi_version_matches_constant() { + assert_eq!(__elephc_eval_abi_version(), ABI_VERSION); +} + +/// Verifies the initial execute stub clears result storage and returns the +/// documented unsupported status instead of panicking or succeeding. +#[test] +fn execute_stub_returns_unsupported_and_clears_result() { + let mut result = ElephcEvalResult { + kind: 99, + value_cell: 1usize as *mut std::ffi::c_void, + error: 2usize as *mut std::ffi::c_void, + }; + let status = unsafe { + __elephc_eval_execute( + std::ptr::null_mut(), + std::ptr::null_mut(), + b"$x = 1;".as_ptr(), + 7, + &mut result, + ) + }; + assert_eq!(status, EvalStatus::UnsupportedConstruct.code()); + assert_eq!(result.kind, 0); + assert!(result.value_cell.is_null()); + assert!(result.error.is_null()); +} + +/// Verifies context allocation returns a current-version opaque handle. +#[test] +fn context_new_returns_current_version_handle() { + let ctx = __elephc_eval_context_new(); + assert!(!ctx.is_null()); + let version = unsafe { (*ctx).abi_version() }; + unsafe { + __elephc_eval_context_free(ctx); + } + assert_eq!(version, ABI_VERSION); +} + +/// Verifies call-site metadata can be set through the stable context ABI. +#[test] +fn context_set_call_site_records_file_dir_and_line() { + let mut ctx = ElephcEvalContext::new(); + let file = b"/tmp/source.php"; + let dir = b"/tmp"; + + let status = unsafe { + __elephc_eval_context_set_call_site( + &mut ctx, + file.as_ptr(), + file.len() as u64, + dir.as_ptr(), + dir.len() as u64, + 9, + ) + }; + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!(ctx.call_dir(), "/tmp"); + assert_eq!(ctx.eval_file_magic(), "/tmp/source.php(9) : eval()'d code"); +} + +/// Verifies the context ABI records a non-owned global scope handle. +#[test] +fn context_set_global_scope_records_handle() { + let mut ctx = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + + let status = unsafe { __elephc_eval_context_set_global_scope(&mut ctx, &mut scope) }; + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!( + ctx.global_scope_ptr(), + Some(&mut scope as *mut ElephcEvalScope) + ); +} + +/// Verifies the function-exists ABI probes eval-declared functions by folded name. +#[test] +fn function_exists_reports_declared_eval_function() { + let mut ctx = ElephcEvalContext::new(); + ctx.define_function( + "dyn_probe", + crate::eval_ir::EvalFunction::new("dyn_probe", Vec::new(), Vec::new()), + ) + .expect("first dynamic function declaration should succeed"); + let existing = b"DYN_PROBE"; + let missing = b"missing"; + + let existing_result = unsafe { + __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) + }; + let missing_result = + unsafe { __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(missing_result, 0); +} + +/// Verifies the constant-exists ABI probes eval-defined constants by PHP name. +#[test] +fn constant_exists_reports_defined_eval_constant() { + let mut ctx = ElephcEvalContext::new(); + let value = RuntimeCellHandle::from_raw(1usize as *mut RuntimeCell); + assert!(ctx.define_constant("DynConstProbe", value)); + let existing = b"DynConstProbe"; + let qualified = b"\\DynConstProbe"; + let wrong_case = b"dynconstprobe"; + let missing = b"missing"; + + let existing_result = unsafe { + __elephc_eval_constant_exists(&ctx, existing.as_ptr(), existing.len() as u64) + }; + let qualified_result = unsafe { + __elephc_eval_constant_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) + }; + let wrong_case_result = unsafe { + __elephc_eval_constant_exists(&ctx, wrong_case.as_ptr(), wrong_case.len() as u64) + }; + let missing_result = + unsafe { __elephc_eval_constant_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(qualified_result, 1); + assert_eq!(wrong_case_result, 0); + assert_eq!(missing_result, 0); +} + +/// Verifies the dynamic-class-exists ABI probes eval-declared classes by folded PHP name. +#[test] +fn dynamic_class_exists_reports_declared_eval_class() { + let mut ctx = ElephcEvalContext::new(); + assert!(ctx.define_class(crate::eval_ir::EvalClass::new( + "DynClassProbe", + Vec::new(), + Vec::new() + ))); + let existing = b"DynClassProbe"; + let qualified = b"\\DynClassProbe"; + let folded = b"dynclassprobe"; + let missing = b"missing"; + + let existing_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, existing.as_ptr(), existing.len() as u64) + }; + let qualified_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) + }; + let folded_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, folded.as_ptr(), folded.len() as u64) + }; + let missing_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, missing.as_ptr(), missing.len() as u64) + }; + + assert_eq!(existing_result, 1); + assert_eq!(qualified_result, 1); + assert_eq!(folded_result, 1); + assert_eq!(missing_result, 0); +} + +/// Verifies native AOT registration records function and parameter metadata. +#[test] +fn register_native_function_reports_function_exists() { + let mut ctx = ElephcEvalContext::new(); + let name = b"NATIVE_PROBE"; + let param = b"value"; + let descriptor = 1usize as *mut c_void; + + let registered = unsafe { + __elephc_eval_register_native_function( + &mut ctx, + name.as_ptr(), + name.len() as u64, + descriptor, + Some(fake_native_invoker), + 1, + ) + }; + let param_registered = unsafe { + __elephc_eval_register_native_function_param( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + param.as_ptr(), + param.len() as u64, + ) + }; + let exists = unsafe { __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) }; + + assert_eq!(registered, 1); + let native = ctx + .native_function("native_probe") + .expect("native function should be registered"); + + assert_eq!(param_registered, 1); + assert_eq!(exists, 1); + assert_eq!(native.param_names(), &["value".to_string()]); +} + +/// Verifies scope allocation returns an empty opaque activation scope handle. +#[test] +fn scope_new_returns_empty_handle() { + let scope = __elephc_eval_scope_new(); + assert!(!scope.is_null()); + let generation = unsafe { (*scope).generation() }; + unsafe { + __elephc_eval_scope_free(scope); + } + assert_eq!(generation, 0); +} + +/// Verifies execute rejects contexts whose ABI version no longer matches. +#[test] +fn execute_rejects_mismatched_context_version() { + let mut ctx = ElephcEvalContext::for_abi_version(ABI_VERSION + 1); + let status = unsafe { + __elephc_eval_execute( + &mut ctx, + std::ptr::null_mut(), + std::ptr::null(), + 0, + std::ptr::null_mut(), + ) + }; + + assert_eq!(status, EvalStatus::AbiMismatch.code()); +} + +/// Verifies execute maps invalid eval fragments to the stable parse status. +#[test] +fn execute_rejects_php_opening_tags_as_parse_errors() { + let code = b" Result { + if name_len > 0 && name_ptr.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + let name_len = usize::try_from(name_len).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = if name_len == 0 { + &[] + } else { + unsafe { slice::from_raw_parts(name_ptr, name_len) } + }; + std::str::from_utf8(bytes) + .map(|name| name.to_string()) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts internal scope-entry flags into stable ABI bit flags. +pub(crate) fn scope_entry_abi_flags(entry: ScopeEntry) -> u32 { + let flags = entry.flags(); + let mut abi_flags = 0; + if flags.present { + abi_flags |= SCOPE_FLAG_PRESENT; + } + if flags.unset { + abi_flags |= SCOPE_FLAG_UNSET; + } + if flags.dirty { + abi_flags |= SCOPE_FLAG_DIRTY; + } + if flags.by_ref { + abi_flags |= SCOPE_FLAG_BY_REF; + } + if flags.ownership == ScopeCellOwnership::Owned { + abi_flags |= SCOPE_FLAG_OWNED; + } + abi_flags +} + +/// Releases every owned cell currently held by a scope. +pub(crate) fn release_owned_scope_cells(scope: &mut ElephcEvalScope) { + for cell in scope.drain_owned_cells() { + release_scope_cell(cell); + } +} + +/// Releases one scope-owned runtime cell through the generated runtime wrapper. +pub(crate) fn release_scope_cell(cell: RuntimeCellHandle) { + #[cfg(not(test))] + unsafe { + __elephc_eval_value_release(cell.as_ptr()); + } + #[cfg(test)] + { + let _ = cell; + } +} + +/// Clears optional result storage before an ABI call writes a result. +pub(crate) unsafe fn clear_result(out: *mut ElephcEvalResult) { + if !out.is_null() { + (*out).clear(); + } +} + +/// Writes an eval execution outcome into optional ABI result storage. +#[cfg(not(test))] +pub(crate) unsafe fn write_outcome( + outcome: EvalOutcome, + out: *mut ElephcEvalResult, +) -> EvalStatus { + match outcome { + EvalOutcome::Value(result) => { + if !out.is_null() { + (*out).kind = 0; + (*out).value_cell = result.as_ptr(); + (*out).error = std::ptr::null_mut(); + } + EvalStatus::Ok + } + EvalOutcome::Throwable(error) => { + if !out.is_null() { + (*out).kind = 3; + (*out).value_cell = std::ptr::null_mut(); + (*out).error = error.as_ptr(); + } + EvalStatus::UncaughtThrowable + } + } +} diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 7857a0d34b..e173b6e1b0 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -1,6 +1,7 @@ //! Purpose: -//! Optional C ABI bridge for elephc's runtime `eval()` support. -//! Exposes the stable entry points linked only into programs that use eval. +//! Optional C ABI bridge crate for elephc's runtime `eval()` support. +//! The crate root owns only the public module map and re-exports stable FFI +//! entry points whose implementations live in focused modules. //! //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_*` symbols. @@ -8,14 +9,13 @@ //! //! Key details: //! - No Rust panic or Rust-specific enum crosses the ABI boundary. -//! - Non-test builds execute the base EvalIR subset through generated runtime -//! value wrappers; crate unit tests keep a controlled stub because they do not -//! link the generated runtime assembly object. +//! - Non-test builds execute EvalIR through generated runtime value wrappers. pub mod abi; pub mod context; pub mod errors; pub mod eval_ir; +mod ffi; pub mod interpreter; mod json_validate; pub mod lower; @@ -24,1297 +24,4 @@ pub mod runtime_hooks; pub mod scope; pub mod value; -use abi::{ - ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_BY_REF, - SCOPE_FLAG_DIRTY, SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, -}; -use context::{NativeFunction, NativeFunctionInvoker}; -use errors::EvalStatus; -#[cfg(not(test))] -use interpreter::{EvalOutcome, RuntimeValueOps}; -#[cfg(not(test))] -use runtime_hooks::ElephcRuntimeOps; -use scope::{ScopeCellOwnership, ScopeEntry}; -use std::ffi::c_void; -use std::slice; -use value::{RuntimeCell, RuntimeCellHandle}; - -#[cfg(not(test))] -unsafe extern "C" { - fn __elephc_eval_value_release(value: *mut RuntimeCell); -} - -/// Returns the ABI version expected by generated elephc eval call sites. -#[no_mangle] -pub extern "C" fn __elephc_eval_abi_version() -> u32 { - ABI_VERSION -} - -/// Allocates a process-level eval context handle for generated code. -#[no_mangle] -pub extern "C" fn __elephc_eval_context_new() -> *mut ElephcEvalContext { - Box::into_raw(Box::new(ElephcEvalContext::new())) -} - -/// Frees a process-level eval context handle allocated by the eval bridge. -/// -/// # Safety -/// `ctx` must be null or a pointer returned by `__elephc_eval_context_new` -/// that has not already been freed. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_context_free(ctx: *mut ElephcEvalContext) { - if !ctx.is_null() { - drop(Box::from_raw(ctx)); - } -} - -/// Records source metadata for the next eval fragment executed in this context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `file_ptr` and `dir_ptr` must be -/// readable for their matching lengths when the length is greater than zero. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_context_set_call_site( - ctx: *mut ElephcEvalContext, - file_ptr: *const u8, - file_len: u64, - dir_ptr: *const u8, - dir_len: u64, - line: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - eval_context_set_call_site_inner(ctx, file_ptr, file_len, dir_ptr, dir_len, line) - }) - .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) -} - -/// Records the materialized program-global eval scope for `global` aliases. -/// -/// # Safety -/// `ctx` and `scope` must be valid handles allocated by the eval bridge. The -/// context does not own `scope`; generated code must keep the scope alive for -/// as long as the context can execute eval fragments that reference globals. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_context_set_global_scope( - ctx: *mut ElephcEvalContext, - scope: *mut ElephcEvalScope, -) -> i32 { - std::panic::catch_unwind(|| unsafe { eval_context_set_global_scope_inner(ctx, scope) }) - .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) -} - -/// Allocates a materialized activation scope handle for generated code. -#[no_mangle] -pub extern "C" fn __elephc_eval_scope_new() -> *mut ElephcEvalScope { - Box::into_raw(Box::new(ElephcEvalScope::new())) -} - -/// Frees a materialized activation scope handle allocated by the eval bridge. -/// -/// # Safety -/// `scope` must be null or a pointer returned by `__elephc_eval_scope_new` -/// that has not already been freed. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_scope_free(scope: *mut ElephcEvalScope) { - if !scope.is_null() { - let mut scope = Box::from_raw(scope); - release_owned_scope_cells(&mut scope); - drop(scope); - } -} - -/// Stores a named runtime cell in a materialized eval scope. -/// -/// # Safety -/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`; names must be UTF-8 variable names. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_scope_set( - scope: *mut ElephcEvalScope, - name_ptr: *const u8, - name_len: u64, - cell: *mut RuntimeCell, - flags: u32, -) -> i32 { - let Some(scope) = scope.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - let ownership = if flags & SCOPE_FLAG_OWNED != 0 { - ScopeCellOwnership::Owned - } else { - ScopeCellOwnership::Borrowed - }; - if let Some(replaced) = scope.set(name, RuntimeCellHandle::from_raw(cell), ownership) { - release_scope_cell(replaced); - } - EvalStatus::Ok.code() -} - -/// Looks up a named runtime cell in a materialized eval scope. -/// -/// # Safety -/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`. Output pointers may be null. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_scope_get( - scope: *const ElephcEvalScope, - name_ptr: *const u8, - name_len: u64, - out_cell: *mut *mut RuntimeCell, - out_flags: *mut u32, -) -> i32 { - let Some(scope) = scope.as_ref() else { - return EvalStatus::RuntimeFatal.code(); - }; - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - let entry = scope.entry(&name); - if !out_cell.is_null() { - *out_cell = entry - .filter(|entry| entry.flags().is_visible()) - .map(|entry| entry.cell().as_ptr()) - .unwrap_or(std::ptr::null_mut()); - } - if !out_flags.is_null() { - *out_flags = entry.map(scope_entry_abi_flags).unwrap_or(0); - } - EvalStatus::Ok.code() -} - -/// Marks a named runtime cell as unset in a materialized eval scope. -/// -/// # Safety -/// `scope` must be a valid eval scope handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`; names must be UTF-8 variable names. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_scope_unset( - scope: *mut ElephcEvalScope, - name_ptr: *const u8, - name_len: u64, -) -> i32 { - let Some(scope) = scope.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - if let Some(replaced) = scope.unset(name) { - release_scope_cell(replaced); - } - EvalStatus::Ok.code() -} - -/// Marks a local eval-scope variable as an alias of a program-global variable. -/// -/// # Safety -/// `scope` must be a valid eval scope handle. Name pointers must be readable -/// for their matching lengths when the length is greater than zero. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_scope_mark_global_alias( - scope: *mut ElephcEvalScope, - name_ptr: *const u8, - name_len: u64, - global_name_ptr: *const u8, - global_name_len: u64, -) -> i32 { - let Some(scope) = scope.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - let Ok(global_name) = abi_name_to_string(global_name_ptr, global_name_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - scope.mark_global_alias_to(name, global_name); - EvalStatus::Ok.code() -} - -/// Clears dirty flags for every entry in a materialized eval scope. -/// -/// # Safety -/// `scope` must be a valid eval scope handle allocated by the eval bridge. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_scope_clear_dirty(scope: *mut ElephcEvalScope) -> i32 { - let Some(scope) = scope.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - scope.mark_all_clean(); - EvalStatus::Ok.code() -} - -/// Executes an eval fragment against a materialized caller scope. -/// -/// The FFI shape is final for the initial bridge: context/scope are opaque -/// runtime handles, `code_ptr`/`code_len` identify the PHP fragment bytes, and -/// `out` receives the eval return cell when provided. Non-test builds execute -/// the current EvalIR subset; test builds return `UnsupportedConstruct` because -/// they do not link elephc's generated runtime value wrappers. -/// -/// # Safety -/// Callers must pass valid pointers for any non-null handle and ensure -/// `code_ptr` is readable for `code_len` bytes when `code_len > 0`. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_execute( - ctx: *mut ElephcEvalContext, - scope: *mut ElephcEvalScope, - code_ptr: *const u8, - code_len: u64, - out: *mut ElephcEvalResult, -) -> i32 { - std::panic::catch_unwind(|| unsafe { execute_eval_inner(ctx, scope, code_ptr, code_len, out) }) - .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) -} - -/// Checks whether a function was previously declared through `eval()`. -/// -/// # Safety -/// `ctx` must be null or a valid eval context handle. `name_ptr` must be -/// readable for `name_len` bytes when `name_len > 0`. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_function_exists( - ctx: *const ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { eval_function_exists_inner(ctx, name_ptr, name_len) }) - .unwrap_or(0) -} - -/// Checks whether a constant was previously defined through `eval()`. -/// -/// # Safety -/// `ctx` must be null or a valid eval context handle. `name_ptr` must be -/// readable for `name_len` bytes when `name_len > 0`. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_constant_exists( - ctx: *const ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { eval_constant_exists_inner(ctx, name_ptr, name_len) }) - .unwrap_or(0) -} - -/// Checks whether a class was previously declared through `eval()`. -/// -/// # Safety -/// `ctx` must be null or a valid eval context handle. `name_ptr` must be -/// readable for `name_len` bytes when `name_len > 0`. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_dynamic_class_exists( - ctx: *const ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - eval_dynamic_class_exists_inner(ctx, name_ptr, name_len) - }) - .unwrap_or(0) -} - -/// Fetches a constant previously defined through `eval()`. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`, and `out` may be null. -#[cfg(not(test))] -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_constant_fetch( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - out: *mut ElephcEvalResult, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - eval_constant_fetch_inner(ctx, name_ptr, name_len, out) - }) - .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) -} - -/// Registers a generated native PHP function callback in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`. `descriptor` and `invoker` must follow -/// the descriptor-invoker ABI emitted by generated code. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - descriptor: *mut c_void, - invoker: Option, - param_count: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_inner(ctx, name_ptr, name_len, descriptor, invoker, param_count) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP function parameter name in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function and parameter name -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_param( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_param_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - param_name_ptr, - param_name_len, - ) - }) - .unwrap_or(0) -} - -/// Calls a zero-argument function previously declared through `eval()`. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`, and `out` may be null. -#[cfg(not(test))] -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_call_function_zero_args( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - out: *mut ElephcEvalResult, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - call_eval_function_inner(ctx, name_ptr, name_len, std::ptr::null(), 0, out) - }) - .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) -} - -/// Calls a function previously declared through `eval()` with positional cells. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`. `args` must be readable for -/// `arg_count` runtime-cell pointers when `arg_count > 0`, and `out` may be null. -#[cfg(not(test))] -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_call_function( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - args: *const *mut RuntimeCell, - arg_count: u64, - out: *mut ElephcEvalResult, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - call_eval_function_inner(ctx, name_ptr, name_len, args, arg_count, out) - }) - .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) -} - -/// Calls a function previously declared through `eval()` with an argument array/hash. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`. `arg_array` must be a boxed Mixed -/// indexed or associative array cell, and `out` may be null. -#[cfg(not(test))] -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_call_function_array( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - arg_array: *mut RuntimeCell, - out: *mut ElephcEvalResult, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - call_eval_function_array_inner(ctx, name_ptr, name_len, arg_array, out) - }) - .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) -} - -/// Runs the eval function-exists ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_function_exists`; invalid handles or unreadable name -/// storage fail closed as `false`. -unsafe fn eval_function_exists_inner( - ctx: *const ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, -) -> i32 { - let Some(context) = ctx.as_ref() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return 0; - }; - i32::from(context.has_function(&name.to_ascii_lowercase())) -} - -/// Runs the eval constant-exists ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_constant_exists`; invalid handles or unreadable name -/// storage fail closed as `false`. -unsafe fn eval_constant_exists_inner( - ctx: *const ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, -) -> i32 { - let Some(context) = ctx.as_ref() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return 0; - }; - i32::from(context.has_constant(&name)) -} - -/// Runs the eval dynamic-class-exists ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_dynamic_class_exists`; invalid handles or unreadable -/// name storage fail closed as `false`. -unsafe fn eval_dynamic_class_exists_inner( - ctx: *const ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, -) -> i32 { - let Some(context) = ctx.as_ref() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return 0; - }; - i32::from(context.has_class(&name)) -} - -/// Runs the eval constant-fetch ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_constant_fetch`; callers must provide a valid context, -/// readable constant-name bytes, and optional writable result storage. -#[cfg(not(test))] -unsafe fn eval_constant_fetch_inner( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - out: *mut ElephcEvalResult, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - if context.abi_version() != ABI_VERSION { - return EvalStatus::AbiMismatch.code(); - } - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - if !out.is_null() { - (*out).clear(); - } - let Some(value) = context.constant(&name) else { - return EvalStatus::RuntimeFatal.code(); - }; - if out.is_null() { - return EvalStatus::Ok.code(); - } - let mut values = ElephcRuntimeOps::new(); - match values.retain(value) { - Ok(result) => { - (*out).kind = 0; - (*out).value_cell = result.as_ptr(); - (*out).error = std::ptr::null_mut(); - EvalStatus::Ok.code() - } - Err(status) => status.code(), - } -} - -/// Runs the native registration ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function`; invalid handles, names, or -/// callback pointers fail closed as `false`. -unsafe fn register_native_function_inner( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - descriptor: *mut c_void, - invoker: Option, - param_count: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION || descriptor.is_null() { - return 0; - } - let Some(invoker) = invoker else { - return 0; - }; - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return 0; - }; - let Ok(param_count) = usize::try_from(param_count) else { - return 0; - }; - let function = NativeFunction::new(descriptor, invoker, param_count); - i32::from( - context - .define_native_function(name.to_ascii_lowercase(), function) - .is_ok(), - ) -} - -/// Runs the native parameter-name registration ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_param`; invalid handles, -/// names, or indexes fail closed as `false`. -unsafe fn register_native_function_param_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { - return 0; - }; - let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - i32::from(context.define_native_function_param( - &function_name.to_ascii_lowercase(), - param_index, - param_name, - )) -} - -/// Runs the call-site metadata setter ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_context_set_call_site`; callers must pass a valid -/// context and readable UTF-8 file/directory byte slices. -unsafe fn eval_context_set_call_site_inner( - ctx: *mut ElephcEvalContext, - file_ptr: *const u8, - file_len: u64, - dir_ptr: *const u8, - dir_len: u64, - line: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - if context.abi_version() != ABI_VERSION { - return EvalStatus::AbiMismatch.code(); - } - let Ok(file) = abi_name_to_string(file_ptr, file_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - let Ok(dir) = abi_name_to_string(dir_ptr, dir_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - let Ok(line) = i64::try_from(line) else { - return EvalStatus::RuntimeFatal.code(); - }; - context.set_call_site(file, dir, line); - EvalStatus::Ok.code() -} - -/// Runs the global-scope setter ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_context_set_global_scope`; callers must pass valid -/// context and scope handles owned by generated code. -unsafe fn eval_context_set_global_scope_inner( - ctx: *mut ElephcEvalContext, - scope: *mut ElephcEvalScope, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - if context.abi_version() != ABI_VERSION { - return EvalStatus::AbiMismatch.code(); - } - if !context.set_global_scope(scope) { - return EvalStatus::RuntimeFatal.code(); - } - EvalStatus::Ok.code() -} - -/// Runs the dynamic function-call ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_call_function`; callers must provide a valid context, -/// readable function-name bytes, and readable argument pointer storage. -#[cfg(not(test))] -unsafe fn call_eval_function_inner( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - args: *const *mut RuntimeCell, - arg_count: u64, - out: *mut ElephcEvalResult, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - if context.abi_version() != ABI_VERSION { - return EvalStatus::AbiMismatch.code(); - } - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - let Ok(arg_count) = usize::try_from(arg_count) else { - return EvalStatus::RuntimeFatal.code(); - }; - if arg_count > 0 && args.is_null() { - return EvalStatus::RuntimeFatal.code(); - } - let args = if arg_count == 0 { - Vec::new() - } else { - slice::from_raw_parts(args, arg_count) - .iter() - .map(|arg| RuntimeCellHandle::from_raw(*arg)) - .collect() - }; - if !out.is_null() { - (*out).clear(); - } - let mut values = ElephcRuntimeOps::new(); - match interpreter::execute_context_function_outcome( - context, - &name.to_ascii_lowercase(), - args, - &mut values, - ) { - Ok(EvalOutcome::Value(result)) => { - if !out.is_null() { - (*out).kind = 0; - (*out).value_cell = result.as_ptr(); - (*out).error = std::ptr::null_mut(); - } - EvalStatus::Ok.code() - } - Ok(EvalOutcome::Throwable(error)) => { - if !out.is_null() { - (*out).kind = 3; - (*out).value_cell = std::ptr::null_mut(); - (*out).error = error.as_ptr(); - } - EvalStatus::UncaughtThrowable.code() - } - Err(status) => status.code(), - } -} - -/// Runs the dynamic function-call-array ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_call_function_array`; callers must provide a valid -/// context, readable function-name bytes, and a boxed array/hash argument cell. -#[cfg(not(test))] -unsafe fn call_eval_function_array_inner( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - arg_array: *mut RuntimeCell, - out: *mut ElephcEvalResult, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return EvalStatus::RuntimeFatal.code(); - }; - if context.abi_version() != ABI_VERSION { - return EvalStatus::AbiMismatch.code(); - } - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - if arg_array.is_null() { - return EvalStatus::RuntimeFatal.code(); - } - if !out.is_null() { - (*out).clear(); - } - let mut values = ElephcRuntimeOps::new(); - match interpreter::execute_context_function_call_array_outcome( - context, - &name.to_ascii_lowercase(), - RuntimeCellHandle::from_raw(arg_array), - &mut values, - ) { - Ok(EvalOutcome::Value(result)) => { - if !out.is_null() { - (*out).kind = 0; - (*out).value_cell = result.as_ptr(); - (*out).error = std::ptr::null_mut(); - } - EvalStatus::Ok.code() - } - Ok(EvalOutcome::Throwable(error)) => { - if !out.is_null() { - (*out).kind = 3; - (*out).value_cell = std::ptr::null_mut(); - (*out).error = error.as_ptr(); - } - EvalStatus::UncaughtThrowable.code() - } - Err(status) => status.code(), - } -} - -/// Runs the eval ABI body after the exported wrapper has installed a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_execute`; callers must provide valid handles and code -/// storage for every non-null pointer argument. -unsafe fn execute_eval_inner( - ctx: *mut ElephcEvalContext, - scope: *mut ElephcEvalScope, - code_ptr: *const u8, - code_len: u64, - out: *mut ElephcEvalResult, -) -> i32 { - if !ctx.is_null() && (*ctx).abi_version() != ABI_VERSION { - return EvalStatus::AbiMismatch.code(); - } - if code_len > 0 && code_ptr.is_null() { - return EvalStatus::RuntimeFatal.code(); - } - let Ok(code_len) = usize::try_from(code_len) else { - return EvalStatus::RuntimeFatal.code(); - }; - let code = if code_len == 0 { - &[] - } else { - slice::from_raw_parts(code_ptr, code_len) - }; - let program = match parser::parse_fragment(code) { - Ok(program) => program, - Err(err) => return err.status().code(), - }; - if !out.is_null() { - (*out).clear(); - } - execute_parsed_eval(ctx, scope, &program, out) -} - -/// Executes a parsed eval program in production builds using elephc runtime hooks. -/// -/// # Safety -/// `scope` and `out` must be null or valid pointers supplied by generated code. -#[cfg(not(test))] -unsafe fn execute_parsed_eval( - ctx: *mut ElephcEvalContext, - scope: *mut ElephcEvalScope, - program: &eval_ir::EvalProgram, - out: *mut ElephcEvalResult, -) -> i32 { - let mut fallback_context; - let context = if let Some(ctx) = ctx.as_mut() { - ctx - } else { - fallback_context = ElephcEvalContext::new(); - &mut fallback_context - }; - let mut fallback_scope; - let scope = if let Some(scope) = scope.as_mut() { - scope - } else { - fallback_scope = ElephcEvalScope::new(); - &mut fallback_scope - }; - let mut values = ElephcRuntimeOps::new(); - match interpreter::execute_program_outcome_with_context(context, program, scope, &mut values) { - Ok(EvalOutcome::Value(result)) => { - if !out.is_null() { - (*out).kind = 0; - (*out).value_cell = result.as_ptr(); - (*out).error = std::ptr::null_mut(); - } - EvalStatus::Ok.code() - } - Ok(EvalOutcome::Throwable(error)) => { - if !out.is_null() { - (*out).kind = 3; - (*out).value_cell = std::ptr::null_mut(); - (*out).error = error.as_ptr(); - } - EvalStatus::UncaughtThrowable.code() - } - Err(status) => status.code(), - } -} - -/// Keeps crate unit tests independent from generated runtime assembly wrappers. -/// -/// # Safety -/// `out` must be null or valid result storage supplied by the test caller. -#[cfg(test)] -unsafe fn execute_parsed_eval( - _ctx: *mut ElephcEvalContext, - _scope: *mut ElephcEvalScope, - _program: &eval_ir::EvalProgram, - _out: *mut ElephcEvalResult, -) -> i32 { - EvalStatus::UnsupportedConstruct.code() -} - -/// Converts an ABI name byte slice into an owned Rust string. -fn abi_name_to_string(name_ptr: *const u8, name_len: u64) -> Result { - if name_len > 0 && name_ptr.is_null() { - return Err(EvalStatus::RuntimeFatal); - } - let name_len = usize::try_from(name_len).map_err(|_| EvalStatus::RuntimeFatal)?; - let bytes = if name_len == 0 { - &[] - } else { - unsafe { slice::from_raw_parts(name_ptr, name_len) } - }; - std::str::from_utf8(bytes) - .map(|name| name.to_string()) - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Converts internal scope-entry flags into stable ABI bit flags. -fn scope_entry_abi_flags(entry: ScopeEntry) -> u32 { - let flags = entry.flags(); - let mut abi_flags = 0; - if flags.present { - abi_flags |= SCOPE_FLAG_PRESENT; - } - if flags.unset { - abi_flags |= SCOPE_FLAG_UNSET; - } - if flags.dirty { - abi_flags |= SCOPE_FLAG_DIRTY; - } - if flags.by_ref { - abi_flags |= SCOPE_FLAG_BY_REF; - } - if flags.ownership == ScopeCellOwnership::Owned { - abi_flags |= SCOPE_FLAG_OWNED; - } - abi_flags -} - -/// Releases every owned cell currently held by a scope. -fn release_owned_scope_cells(scope: &mut ElephcEvalScope) { - for cell in scope.drain_owned_cells() { - release_scope_cell(cell); - } -} - -/// Releases one scope-owned runtime cell through the generated runtime wrapper. -fn release_scope_cell(cell: RuntimeCellHandle) { - #[cfg(not(test))] - unsafe { - __elephc_eval_value_release(cell.as_ptr()); - } - #[cfg(test)] - { - let _ = cell; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Test native invoker placeholder used only to validate ABI registration. - unsafe extern "C" fn fake_native_invoker( - _descriptor: *mut c_void, - _args: *mut RuntimeCell, - ) -> *mut RuntimeCell { - std::ptr::null_mut() - } - - /// Verifies the exported version entry point reports the crate ABI constant. - #[test] - fn abi_version_matches_constant() { - assert_eq!(__elephc_eval_abi_version(), ABI_VERSION); - } - - /// Verifies the initial execute stub clears result storage and returns the - /// documented unsupported status instead of panicking or succeeding. - #[test] - fn execute_stub_returns_unsupported_and_clears_result() { - let mut result = ElephcEvalResult { - kind: 99, - value_cell: 1usize as *mut std::ffi::c_void, - error: 2usize as *mut std::ffi::c_void, - }; - let status = unsafe { - __elephc_eval_execute( - std::ptr::null_mut(), - std::ptr::null_mut(), - b"$x = 1;".as_ptr(), - 7, - &mut result, - ) - }; - assert_eq!(status, EvalStatus::UnsupportedConstruct.code()); - assert_eq!(result.kind, 0); - assert!(result.value_cell.is_null()); - assert!(result.error.is_null()); - } - - /// Verifies context allocation returns a current-version opaque handle. - #[test] - fn context_new_returns_current_version_handle() { - let ctx = __elephc_eval_context_new(); - assert!(!ctx.is_null()); - let version = unsafe { (*ctx).abi_version() }; - unsafe { - __elephc_eval_context_free(ctx); - } - assert_eq!(version, ABI_VERSION); - } - - /// Verifies call-site metadata can be set through the stable context ABI. - #[test] - fn context_set_call_site_records_file_dir_and_line() { - let mut ctx = ElephcEvalContext::new(); - let file = b"/tmp/source.php"; - let dir = b"/tmp"; - - let status = unsafe { - __elephc_eval_context_set_call_site( - &mut ctx, - file.as_ptr(), - file.len() as u64, - dir.as_ptr(), - dir.len() as u64, - 9, - ) - }; - - assert_eq!(status, EvalStatus::Ok.code()); - assert_eq!(ctx.call_dir(), "/tmp"); - assert_eq!(ctx.eval_file_magic(), "/tmp/source.php(9) : eval()'d code"); - } - - /// Verifies the context ABI records a non-owned global scope handle. - #[test] - fn context_set_global_scope_records_handle() { - let mut ctx = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - - let status = unsafe { __elephc_eval_context_set_global_scope(&mut ctx, &mut scope) }; - - assert_eq!(status, EvalStatus::Ok.code()); - assert_eq!( - ctx.global_scope_ptr(), - Some(&mut scope as *mut ElephcEvalScope) - ); - } - - /// Verifies the function-exists ABI probes eval-declared functions by folded name. - #[test] - fn function_exists_reports_declared_eval_function() { - let mut ctx = ElephcEvalContext::new(); - ctx.define_function( - "dyn_probe", - crate::eval_ir::EvalFunction::new("dyn_probe", Vec::new(), Vec::new()), - ) - .expect("first dynamic function declaration should succeed"); - let existing = b"DYN_PROBE"; - let missing = b"missing"; - - let existing_result = unsafe { - __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) - }; - let missing_result = - unsafe { __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; - - assert_eq!(existing_result, 1); - assert_eq!(missing_result, 0); - } - - /// Verifies the constant-exists ABI probes eval-defined constants by PHP name. - #[test] - fn constant_exists_reports_defined_eval_constant() { - let mut ctx = ElephcEvalContext::new(); - let value = RuntimeCellHandle::from_raw(1usize as *mut RuntimeCell); - assert!(ctx.define_constant("DynConstProbe", value)); - let existing = b"DynConstProbe"; - let qualified = b"\\DynConstProbe"; - let wrong_case = b"dynconstprobe"; - let missing = b"missing"; - - let existing_result = unsafe { - __elephc_eval_constant_exists(&ctx, existing.as_ptr(), existing.len() as u64) - }; - let qualified_result = unsafe { - __elephc_eval_constant_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) - }; - let wrong_case_result = unsafe { - __elephc_eval_constant_exists(&ctx, wrong_case.as_ptr(), wrong_case.len() as u64) - }; - let missing_result = - unsafe { __elephc_eval_constant_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; - - assert_eq!(existing_result, 1); - assert_eq!(qualified_result, 1); - assert_eq!(wrong_case_result, 0); - assert_eq!(missing_result, 0); - } - - /// Verifies the dynamic-class-exists ABI probes eval-declared classes by folded PHP name. - #[test] - fn dynamic_class_exists_reports_declared_eval_class() { - let mut ctx = ElephcEvalContext::new(); - assert!(ctx.define_class(crate::eval_ir::EvalClass::new( - "DynClassProbe", - Vec::new(), - Vec::new() - ))); - let existing = b"DynClassProbe"; - let qualified = b"\\DynClassProbe"; - let folded = b"dynclassprobe"; - let missing = b"missing"; - - let existing_result = unsafe { - __elephc_eval_dynamic_class_exists(&ctx, existing.as_ptr(), existing.len() as u64) - }; - let qualified_result = unsafe { - __elephc_eval_dynamic_class_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) - }; - let folded_result = unsafe { - __elephc_eval_dynamic_class_exists(&ctx, folded.as_ptr(), folded.len() as u64) - }; - let missing_result = unsafe { - __elephc_eval_dynamic_class_exists(&ctx, missing.as_ptr(), missing.len() as u64) - }; - - assert_eq!(existing_result, 1); - assert_eq!(qualified_result, 1); - assert_eq!(folded_result, 1); - assert_eq!(missing_result, 0); - } - - /// Verifies native AOT registration records function and parameter metadata. - #[test] - fn register_native_function_reports_function_exists() { - let mut ctx = ElephcEvalContext::new(); - let name = b"NATIVE_PROBE"; - let param = b"value"; - let descriptor = 1usize as *mut c_void; - - let registered = unsafe { - __elephc_eval_register_native_function( - &mut ctx, - name.as_ptr(), - name.len() as u64, - descriptor, - Some(fake_native_invoker), - 1, - ) - }; - let param_registered = unsafe { - __elephc_eval_register_native_function_param( - &mut ctx, - name.as_ptr(), - name.len() as u64, - 0, - param.as_ptr(), - param.len() as u64, - ) - }; - let exists = unsafe { __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) }; - - assert_eq!(registered, 1); - let native = ctx - .native_function("native_probe") - .expect("native function should be registered"); - - assert_eq!(param_registered, 1); - assert_eq!(exists, 1); - assert_eq!(native.param_names(), &["value".to_string()]); - } - - /// Verifies scope allocation returns an empty opaque activation scope handle. - #[test] - fn scope_new_returns_empty_handle() { - let scope = __elephc_eval_scope_new(); - assert!(!scope.is_null()); - let generation = unsafe { (*scope).generation() }; - unsafe { - __elephc_eval_scope_free(scope); - } - assert_eq!(generation, 0); - } - - /// Verifies execute rejects contexts whose ABI version no longer matches. - #[test] - fn execute_rejects_mismatched_context_version() { - let mut ctx = ElephcEvalContext::for_abi_version(ABI_VERSION + 1); - let status = unsafe { - __elephc_eval_execute( - &mut ctx, - std::ptr::null_mut(), - std::ptr::null(), - 0, - std::ptr::null_mut(), - ) - }; - - assert_eq!(status, EvalStatus::AbiMismatch.code()); - } - - /// Verifies execute maps invalid eval fragments to the stable parse status. - #[test] - fn execute_rejects_php_opening_tags_as_parse_errors() { - let code = b" Date: Thu, 18 Jun 2026 09:49:23 +0200 Subject: [PATCH 0277/1208] Format eval crate modules --- crates/elephc-eval/src/ffi/symbols.rs | 14 +- crates/elephc-eval/src/ffi/tests.rs | 25 +- crates/elephc-eval/src/ffi/util.rs | 10 +- crates/elephc-eval/src/interpreter.rs | 529 ++++++++++++------------ crates/elephc-eval/src/json_validate.rs | 23 +- crates/elephc-eval/src/runtime_hooks.rs | 8 +- 6 files changed, 305 insertions(+), 304 deletions(-) diff --git a/crates/elephc-eval/src/ffi/symbols.rs b/crates/elephc-eval/src/ffi/symbols.rs index 7a5e9c7401..d69ec780d4 100644 --- a/crates/elephc-eval/src/ffi/symbols.rs +++ b/crates/elephc-eval/src/ffi/symbols.rs @@ -13,9 +13,9 @@ use super::util::abi_name_to_string; #[cfg(not(test))] use super::util::clear_result; -use crate::abi::{ElephcEvalContext, ABI_VERSION}; #[cfg(not(test))] use crate::abi::ElephcEvalResult; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; #[cfg(not(test))] use crate::errors::EvalStatus; #[cfg(not(test))] @@ -64,10 +64,8 @@ pub unsafe extern "C" fn __elephc_eval_dynamic_class_exists( name_ptr: *const u8, name_len: u64, ) -> i32 { - std::panic::catch_unwind(|| unsafe { - eval_dynamic_class_exists_inner(ctx, name_ptr, name_len) - }) - .unwrap_or(0) + std::panic::catch_unwind(|| unsafe { eval_dynamic_class_exists_inner(ctx, name_ptr, name_len) }) + .unwrap_or(0) } /// Fetches a constant previously defined through `eval()`. @@ -83,10 +81,8 @@ pub unsafe extern "C" fn __elephc_eval_constant_fetch( name_len: u64, out: *mut ElephcEvalResult, ) -> i32 { - std::panic::catch_unwind(|| unsafe { - eval_constant_fetch_inner(ctx, name_ptr, name_len, out) - }) - .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) + std::panic::catch_unwind(|| unsafe { eval_constant_fetch_inner(ctx, name_ptr, name_len, out) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } /// Runs the eval function-exists ABI body after installing a panic boundary. diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs index 837d1e9d61..4408a0eea0 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -123,9 +123,8 @@ fn function_exists_reports_declared_eval_function() { let existing = b"DYN_PROBE"; let missing = b"missing"; - let existing_result = unsafe { - __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) - }; + let existing_result = + unsafe { __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; let missing_result = unsafe { __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; @@ -144,12 +143,10 @@ fn constant_exists_reports_defined_eval_constant() { let wrong_case = b"dynconstprobe"; let missing = b"missing"; - let existing_result = unsafe { - __elephc_eval_constant_exists(&ctx, existing.as_ptr(), existing.len() as u64) - }; - let qualified_result = unsafe { - __elephc_eval_constant_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) - }; + let existing_result = + unsafe { __elephc_eval_constant_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; + let qualified_result = + unsafe { __elephc_eval_constant_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) }; let wrong_case_result = unsafe { __elephc_eval_constant_exists(&ctx, wrong_case.as_ptr(), wrong_case.len() as u64) }; @@ -182,12 +179,10 @@ fn dynamic_class_exists_reports_declared_eval_class() { let qualified_result = unsafe { __elephc_eval_dynamic_class_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) }; - let folded_result = unsafe { - __elephc_eval_dynamic_class_exists(&ctx, folded.as_ptr(), folded.len() as u64) - }; - let missing_result = unsafe { - __elephc_eval_dynamic_class_exists(&ctx, missing.as_ptr(), missing.len() as u64) - }; + let folded_result = + unsafe { __elephc_eval_dynamic_class_exists(&ctx, folded.as_ptr(), folded.len() as u64) }; + let missing_result = + unsafe { __elephc_eval_dynamic_class_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; assert_eq!(existing_result, 1); assert_eq!(qualified_result, 1); diff --git a/crates/elephc-eval/src/ffi/util.rs b/crates/elephc-eval/src/ffi/util.rs index fcc8da4904..7e86a2b60b 100644 --- a/crates/elephc-eval/src/ffi/util.rs +++ b/crates/elephc-eval/src/ffi/util.rs @@ -29,10 +29,7 @@ unsafe extern "C" { } /// Converts an ABI name byte slice into an owned Rust string. -pub(crate) fn abi_name_to_string( - name_ptr: *const u8, - name_len: u64, -) -> Result { +pub(crate) fn abi_name_to_string(name_ptr: *const u8, name_len: u64) -> Result { if name_len > 0 && name_ptr.is_null() { return Err(EvalStatus::RuntimeFatal); } @@ -97,10 +94,7 @@ pub(crate) unsafe fn clear_result(out: *mut ElephcEvalResult) { /// Writes an eval execution outcome into optional ABI result storage. #[cfg(not(test))] -pub(crate) unsafe fn write_outcome( - outcome: EvalOutcome, - out: *mut ElephcEvalResult, -) -> EvalStatus { +pub(crate) unsafe fn write_outcome(outcome: EvalOutcome, out: *mut ElephcEvalResult) -> EvalStatus { match outcome { EvalOutcome::Value(result) => { if !out.is_null() { diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter.rs index 428da6f7e6..c99775ca4d 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter.rs @@ -138,8 +138,8 @@ const EVAL_STREAM_WRAPPERS: &[&str] = &[ /// Built-in stream transports reported by eval `stream_get_transports()`. const EVAL_STREAM_TRANSPORTS: &[&str] = &[ - "tcp", "udp", "unix", "udg", "tls", "ssl", "sslv2", "sslv3", "tlsv1.0", "tlsv1.1", - "tlsv1.2", "tlsv1.3", + "tcp", "udp", "unix", "udg", "tls", "ssl", "sslv2", "sslv3", "tlsv1.0", "tlsv1.1", "tlsv1.2", + "tlsv1.3", ]; /// Monotonic salt mixed into eval `rand()`/`mt_rand()` and array key sampling. @@ -295,10 +295,7 @@ unsafe extern "C" { /// Looks up one internet service entry by port and protocol. #[link_name = "getservbyport"] - fn libc_getservbyport( - port: libc::c_int, - proto: *const libc::c_char, - ) -> *mut libc::servent; + fn libc_getservbyport(port: libc::c_int, proto: *const libc::c_char) -> *mut libc::servent; } /// Runtime value hooks required by the EvalIR interpreter. @@ -1903,9 +1900,7 @@ fn eval_positional_expr_call( ) -> Result { match name { "abs" => eval_builtin_abs(args, context, scope, values), - "addslashes" | "stripslashes" => { - eval_builtin_slashes(name, args, context, scope, values) - } + "addslashes" | "stripslashes" => eval_builtin_slashes(name, args, context, scope, values), "array_combine" => eval_builtin_array_combine(args, context, scope, values), "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), "array_column" => eval_builtin_array_column(args, context, scope, values), @@ -2062,21 +2057,15 @@ fn eval_positional_expr_call( "preg_match" => eval_builtin_preg_match(args, context, scope, values), "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), - "preg_replace_callback" => { - eval_builtin_preg_replace_callback(args, context, scope, values) - } + "preg_replace_callback" => eval_builtin_preg_replace_callback(args, context, scope, values), "preg_split" => eval_builtin_preg_split(args, context, scope, values), "print_r" => eval_builtin_print_r(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), "random_int" => eval_builtin_random_int(args, context, scope, values), "range" => eval_builtin_range(args, context, scope, values), - "rawurldecode" | "urldecode" => { - eval_builtin_url_decode(name, args, context, scope, values) - } - "rawurlencode" | "urlencode" => { - eval_builtin_url_encode(name, args, context, scope, values) - } + "rawurldecode" | "urldecode" => eval_builtin_url_decode(name, args, context, scope, values), + "rawurlencode" | "urlencode" => eval_builtin_url_encode(name, args, context, scope, values), "readfile" => eval_builtin_readfile(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath" => eval_builtin_realpath(args, context, scope, values), @@ -2529,7 +2518,7 @@ fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { fn eval_php_visible_builtin_exists(name: &str) -> bool { matches!( name, - "abs" + "abs" | "addslashes" | "array_chunk" | "array_column" @@ -2908,9 +2897,10 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "array_walk" => Some(&["array", "callback"]), "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" - | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" - | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" - | "shuffle" | "sort" => Some(&["array"]), + | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" | "asort" + | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { + Some(&["array"]) + } "array_push" | "array_unshift" => Some(&["array", "values"]), "array_key_exists" => Some(&["key", "array"]), "array_pad" => Some(&["array", "length", "value"]), @@ -2927,11 +2917,9 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { Some(&["string"]) } "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" - | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" - | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" - | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => { - Some(&["value"]) - } + | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" + | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" | "is_real" + | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), "settype" => Some(&["var", "type"]), "get_class" => Some(&["object"]), "get_parent_class" => Some(&["object_or_class"]), @@ -3004,7 +2992,12 @@ fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { "microtime" => Some(&["as_float"]), "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), "nl2br" => Some(&["string", "use_xhtml"]), - "number_format" => Some(&["num", "decimals", "decimal_separator", "thousands_separator"]), + "number_format" => Some(&[ + "num", + "decimals", + "decimal_separator", + "thousands_separator", + ]), "ord" => Some(&["character"]), "pathinfo" => Some(&["path", "flags"]), "pi" => Some(&[]), @@ -3142,8 +3135,8 @@ fn eval_array_callable( return Err(EvalStatus::UnsupportedConstruct); } let method = values.array_get(callback, one)?; - let method = String::from_utf8(values.string_bytes(method)?) - .map_err(|_| EvalStatus::RuntimeFatal)?; + let method = + String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; Ok(EvaluatedCallable::ObjectMethod { object, method }) } @@ -3352,9 +3345,7 @@ fn eval_builtin_with_values( } "array_splice" => { let result = match evaluated_args { - [array, offset] => { - eval_array_splice_value_result(*array, *offset, None, values)? - } + [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, [array, offset, length] => { eval_array_splice_value_result(*array, *offset, Some(*length), values)? } @@ -3780,13 +3771,9 @@ fn eval_builtin_with_values( [value, length, pad_string] => { eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? } - [value, length, pad_string, pad_type] => eval_str_pad_result( - *value, - *length, - Some(*pad_string), - Some(*pad_type), - values, - )?, + [value, length, pad_string, pad_type] => { + eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values)? + } _ => return Err(EvalStatus::RuntimeFatal), }, "str_split" => match evaluated_args { @@ -3877,7 +3864,7 @@ fn eval_builtin_with_values( return Err(EvalStatus::RuntimeFatal); }; eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? - }, + } "nl2br" => match evaluated_args { [value] => eval_nl2br_result(*value, true, values)?, [value, use_xhtml] => { @@ -3950,16 +3937,14 @@ fn eval_builtin_with_values( [json, associative] => { eval_json_decode_result(*json, Some(*associative), None, None, context, values)? } - [json, associative, depth] => { - eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - None, - context, - values, - )? - } + [json, associative, depth] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + None, + context, + values, + )?, [json, associative, depth, flags] => eval_json_decode_result( *json, Some(*associative), @@ -3972,9 +3957,7 @@ fn eval_builtin_with_values( }, "json_encode" => match evaluated_args { [value] => eval_json_encode_result(*value, None, None, context, values)?, - [value, flags] => { - eval_json_encode_result(*value, Some(*flags), None, context, values)? - } + [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values)?, [value, flags, depth] => { eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? } @@ -4866,8 +4849,15 @@ fn eval_builtin_array_pop_shift_call( } if matches!( name, - "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" - | "shuffle" | "sort" + "arsort" + | "asort" + | "krsort" + | "ksort" + | "natcasesort" + | "natsort" + | "rsort" + | "shuffle" + | "sort" ) { return eval_builtin_array_sort_call(name, args, context, scope, values); } @@ -4884,7 +4874,8 @@ fn eval_builtin_array_pop_shift_call( let EvalExpr::LoadVar(var_name) = arg.value() else { return Err(EvalStatus::RuntimeFatal); }; - let Some(entry) = scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) + let Some(entry) = + scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) else { return Err(EvalStatus::RuntimeFatal); }; @@ -4919,7 +4910,8 @@ fn eval_builtin_array_push_unshift_call( for arg in &args[1..] { inserted.push(eval_expr(arg.value(), context, scope, values)?); } - let Some(entry) = scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) + let Some(entry) = + scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) else { return Err(EvalStatus::RuntimeFatal); }; @@ -5143,8 +5135,14 @@ fn eval_user_sort_entries_in_place( for pass in 0..entries.len() { let upper = entries.len().saturating_sub(pass + 1); for index in 0..upper { - let comparison = - eval_user_sort_compare(name, callback, &entries[index], &entries[index + 1], context, values)?; + let comparison = eval_user_sort_compare( + name, + callback, + &entries[index], + &entries[index + 1], + context, + values, + )?; if comparison > 0 { entries.swap(index, index + 1); } @@ -5406,9 +5404,9 @@ fn eval_array_natural_sort_key( values: &mut impl RuntimeValueOps, ) -> Result { match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => Ok(EvalArraySortKey::Numeric(eval_float_value( - value, values, - )?)), + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } EVAL_TAG_STRING => { let mut bytes = values.string_bytes(value)?; if case_insensitive { @@ -5426,9 +5424,9 @@ fn eval_array_sort_key( values: &mut impl RuntimeValueOps, ) -> Result { match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => Ok(EvalArraySortKey::Numeric(eval_float_value( - value, values, - )?)), + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } EVAL_TAG_STRING => { let bytes = values.string_bytes(value)?; match eval_array_numeric_string_sort_key(&bytes) { @@ -5480,8 +5478,7 @@ fn eval_natural_bytes_cmp(left: &[u8], right: &[u8]) -> std::cmp::Ordering { let mut right_index = 0; while left_index < left.len() && right_index < right.len() { if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { - let order = - eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); + let order = eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); if order != std::cmp::Ordering::Equal { return order; } @@ -5688,7 +5685,9 @@ fn eval_array_splice_removed( let source_key = values.array_iter_key(array, position)?; let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { let key = values.int(next_int_key)?; - next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; key } else { source_key @@ -5767,7 +5766,9 @@ fn eval_array_splice_replacement( let source_key = values.array_iter_key(array, position)?; let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { let key = values.int(next_int_key)?; - next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; key } else { source_key @@ -5777,14 +5778,18 @@ fn eval_array_splice_replacement( } for value in inserted { let target_key = values.int(next_int_key)?; - next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; result = values.array_set(result, target_key, *value)?; } for position in end..len { let source_key = values.array_iter_key(array, position)?; let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { let key = values.int(next_int_key)?; - next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; key } else { source_key @@ -5919,7 +5924,9 @@ fn eval_array_pop_shift_assoc_replacement( len: usize, values: &mut impl RuntimeValueOps, ) -> Result { - if name == "array_shift" && eval_array_remaining_keys_are_int(array, removed_position, len, values)? { + if name == "array_shift" + && eval_array_remaining_keys_are_int(array, removed_position, len, values)? + { return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); } @@ -5932,7 +5939,9 @@ fn eval_array_pop_shift_assoc_replacement( let source_key = values.array_iter_key(array, position)?; let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { let key = values.int(next_int_key)?; - next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; key } else { source_key @@ -5990,7 +5999,9 @@ fn eval_array_push_unshift_replacement( ("array_push", EVAL_TAG_ARRAY) => { eval_array_push_indexed_replacement(array, inserted, values) } - ("array_push", EVAL_TAG_ASSOC) => eval_array_push_assoc_replacement(array, inserted, values), + ("array_push", EVAL_TAG_ASSOC) => { + eval_array_push_assoc_replacement(array, inserted, values) + } ("array_unshift", EVAL_TAG_ARRAY) => { eval_array_unshift_indexed_replacement(array, inserted, values) } @@ -6012,7 +6023,8 @@ fn eval_array_push_indexed_replacement( for position in 0..len { let source_key = values.array_iter_key(array, position)?; let value = values.array_get(array, source_key)?; - let target_key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let target_key = + values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; result = values.array_set(result, target_key, value)?; } for (offset, value) in inserted.iter().copied().enumerate() { @@ -6087,14 +6099,18 @@ fn eval_array_unshift_assoc_replacement( let mut next_int_key = 0_i64; for value in inserted.iter().copied() { let key = values.int(next_int_key)?; - next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; result = values.array_set(result, key, value)?; } for position in 0..len { let source_key = values.array_iter_key(array, position)?; let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { let key = values.int(next_int_key)?; - next_int_key = next_int_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; key } else { source_key @@ -6305,8 +6321,8 @@ fn eval_array_slice_result( for source_position in start..end { let source_key = values.array_iter_key(array, source_position)?; let source_value = values.array_get(array, source_key)?; - let target_key = i64::try_from(source_position - start) - .map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; let target_key = values.int(target_key)?; result = values.array_set(result, target_key, source_value)?; } @@ -6392,8 +6408,7 @@ fn eval_array_pad_result( } if target > 0 { - result = - eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; + result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; } Ok(result) @@ -6581,7 +6596,13 @@ fn eval_iterator_apply_result( Ok(count) => count, Err(EvalStatus::UnsupportedConstruct) => { let iterator = values.method_call(iterator, "getiterator", Vec::new())?; - eval_iterator_apply_iterator_object(iterator, callback, &callback_args, context, values)? + eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + )? } Err(err) => return Err(err), }; @@ -6848,7 +6869,9 @@ fn eval_array_value_set_result( let key = values.array_iter_key(left, position)?; let value = values.array_get(left, key)?; let comparable = values.string_bytes(value)?; - let found = right_values.iter().any(|right_value| right_value == &comparable); + let found = right_values + .iter() + .any(|right_value| right_value == &comparable); let keep = match name { "array_diff" => !found, "array_intersect" => found, @@ -7091,7 +7114,9 @@ fn eval_array_merge_result( ) -> Result { let left_len = values.array_len(left)?; let right_len = values.array_len(right)?; - let capacity = left_len.checked_add(right_len).ok_or(EvalStatus::RuntimeFatal)?; + let capacity = left_len + .checked_add(right_len) + .ok_or(EvalStatus::RuntimeFatal)?; let mut result = values.assoc_new(capacity)?; let mut next_numeric_key = 0_i64; result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; @@ -7802,7 +7827,11 @@ fn eval_format_sprintf_int( } _ => { let value = value as i128; - let magnitude = if value < 0 { (-value) as u128 } else { value as u128 }; + let magnitude = if value < 0 { + (-value) as u128 + } else { + value as u128 + }; if value < 0 { output.push(b'-'); } else if spec.force_sign { @@ -8208,8 +8237,7 @@ fn eval_str_pad_result( Some(pad_type) => eval_int_value(pad_type, values)?, None => 1, }; - let (left_pad, right_pad) = - eval_str_pad_sides(target_length - bytes.len(), pad_type)?; + let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; let capacity = bytes .len() .checked_add(left_pad) @@ -9061,9 +9089,8 @@ fn eval_push_date_token( b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), - b'D' => { - output.extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()) - } + b'D' => output + .extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), b'M' => { output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) } @@ -9755,7 +9782,9 @@ fn eval_filesize_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; - let len = std::fs::metadata(path).map(|metadata| metadata.len()).unwrap_or(0); + let len = std::fs::metadata(path) + .map(|metadata| metadata.len()) + .unwrap_or(0); values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) } @@ -10114,7 +10143,10 @@ fn eval_glob_matches(pattern: &str) -> Vec { .collect(); } let absolute = pattern.starts_with('/'); - let components: Vec<&str> = pattern.split('/').filter(|component| !component.is_empty()).collect(); + let components: Vec<&str> = pattern + .split('/') + .filter(|component| !component.is_empty()) + .collect(); let mut matches = Vec::new(); let base = if absolute { std::path::PathBuf::from("/") @@ -10501,7 +10533,10 @@ fn eval_path_is_writable(path: &std::path::Path) -> bool { if !path.is_dir() { return false; } - let probe = path.join(format!(".elephc_eval_writable_probe_{}", std::process::id())); + let probe = path.join(format!( + ".elephc_eval_writable_probe_{}", + std::process::id() + )); match std::fs::OpenOptions::new() .write(true) .create_new(true) @@ -10860,7 +10895,14 @@ fn eval_fnmatch_at( filename_index == filename.len() } else { match pattern[pattern_index] { - b'*' => eval_fnmatch_star(pattern, filename, flags, pattern_index, filename_index, memo), + b'*' => eval_fnmatch_star( + pattern, + filename, + flags, + pattern_index, + filename_index, + memo, + ), b'?' => { eval_fnmatch_single_wildcard(filename, flags, filename_index) && eval_fnmatch_at( @@ -11069,7 +11111,9 @@ fn eval_fnmatch_wildcard_can_consume(filename: &[u8], flags: i64, filename_index if flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index] == b'/' { return false; } - if flags & EVAL_FNM_PERIOD != 0 && eval_fnmatch_is_leading_period(filename, flags, filename_index) { + if flags & EVAL_FNM_PERIOD != 0 + && eval_fnmatch_is_leading_period(filename, flags, filename_index) + { return false; } true @@ -11330,21 +11374,20 @@ fn eval_preg_match_all_pattern_order_array( for capture_index in 0..capture_count { let mut row = values.array_new(captures.len())?; for (match_index, capture) in captures.iter().enumerate() { - let key = values - .int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = - eval_preg_capture_value( - subject, - capture, - capture_index, - offset_capture, - unmatched_as_null, - values, - )?; + let key = + values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; row = values.array_set(row, key, value)?; } - let key = values - .int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; outer = values.array_set(outer, key, row)?; } Ok(outer) @@ -11364,21 +11407,19 @@ fn eval_preg_match_all_set_order_array( for (match_index, capture) in captures.iter().enumerate() { let mut row = values.array_new(capture_count)?; for capture_index in 0..capture_count { - let key = values - .int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = - eval_preg_capture_value( - subject, - capture, - capture_index, - offset_capture, - unmatched_as_null, - values, - )?; + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; row = values.array_set(row, key, value)?; } - let key = values - .int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let key = values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; outer = values.array_set(outer, key, row)?; } Ok(outer) @@ -11580,8 +11621,7 @@ struct EvalPregSplitPiece { /// Splits a PHP delimited regex into body bytes and supported modifiers. fn eval_preg_pattern_parts(pattern: &[u8]) -> Result<(Vec, EvalPregModifiers), EvalStatus> { - if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() - { + if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() { return Err(EvalStatus::RuntimeFatal); } let delimiter = pattern[0]; @@ -11726,9 +11766,9 @@ fn eval_preg_capture_value( let value = if matched.is_none() && unmatched_as_null { values.null()? } else { - let bytes = matched - .as_ref() - .map_or(b"".as_slice(), |matched| &subject[matched.start()..matched.end()]); + let bytes = matched.as_ref().map_or(b"".as_slice(), |matched| { + &subject[matched.start()..matched.end()] + }); values.string_bytes_value(bytes)? }; if !offset_capture { @@ -11855,9 +11895,8 @@ fn eval_preg_split_flags( return Ok(0); }; let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_SPLIT_NO_EMPTY - | EVAL_PREG_SPLIT_DELIM_CAPTURE - | EVAL_PREG_SPLIT_OFFSET_CAPTURE; + let supported = + EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE | EVAL_PREG_SPLIT_OFFSET_CAPTURE; if flags & !supported != 0 { return Err(EvalStatus::RuntimeFatal); } @@ -12003,7 +12042,7 @@ fn eval_gethostbyname_result( std::net::IpAddr::V6(_) => None, }) .next() - }); + }); values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) } @@ -12685,8 +12724,7 @@ fn eval_base64_encode_result( ) -> Result { let bytes = values.string_bytes(value)?; let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); - const ALPHABET: &[u8; 64] = - b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; for chunk in bytes.chunks(3) { let first = chunk[0]; let second = chunk.get(1).copied().unwrap_or(0); @@ -13864,10 +13902,7 @@ fn eval_include_missing_file( } /// Resolves eval include paths using PHP's cwd-first and caller-directory fallback. -fn eval_resolve_include_path( - path: &str, - context: &ElephcEvalContext, -) -> std::path::PathBuf { +fn eval_resolve_include_path(path: &str, context: &ElephcEvalContext) -> std::path::PathBuf { let raw_path = std::path::Path::new(path); if raw_path.is_absolute() || raw_path.exists() { return raw_path.to_path_buf(); @@ -14357,10 +14392,7 @@ fn eval_json_decode_number_to_cell( /// Returns true when one integer-grammar JSON number exceeds PHP's int range. fn eval_json_number_overflows_i64(value: &[u8]) -> bool { - if value - .iter() - .any(|byte| matches!(*byte, b'.' | b'e' | b'E')) - { + if value.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) { return false; } let (negative, digits) = if let Some(digits) = value.strip_prefix(b"-") { @@ -14510,10 +14542,7 @@ fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str EVAL_JSON_ERROR_CTRL_CHAR, "Control character error, possibly incorrectly encoded", ), - JsonParseErrorKind::Utf8 => ( - EVAL_JSON_ERROR_UTF8, - EVAL_JSON_UTF8_MESSAGE, - ), + JsonParseErrorKind::Utf8 => (EVAL_JSON_ERROR_UTF8, EVAL_JSON_UTF8_MESSAGE), JsonParseErrorKind::Utf16 => ( EVAL_JSON_ERROR_UTF16, "Single unpaired UTF-16 surrogate in unicode escape", @@ -14522,11 +14551,7 @@ fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str } /// Adds PHP's JSON line/column suffix to one base error message. -fn eval_json_error_message_with_location( - message: &str, - bytes: &[u8], - offset: usize, -) -> String { +fn eval_json_error_message_with_location(message: &str, bytes: &[u8], offset: usize) -> String { let (line, column) = eval_json_error_location(bytes, offset); format!("{message} near location {line}:{column}") } @@ -14652,9 +14677,7 @@ fn eval_json_encode_append_float( let bytes = values.string_bytes(value)?; output.extend_from_slice(&bytes); if flags & EVAL_JSON_PRESERVE_ZERO_FRACTION != 0 - && !bytes - .iter() - .any(|byte| matches!(*byte, b'.' | b'e' | b'E')) + && !bytes.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) { output.extend_from_slice(b".0"); } @@ -14985,7 +15008,10 @@ fn eval_json_encode_append_invalid_utf8_bytes( { eval_json_encode_append_char(character, flags, output); } - let invalid_len = error.error_len().unwrap_or(bytes.len() - valid.len()).max(1); + let invalid_len = error + .error_len() + .unwrap_or(bytes.len() - valid.len()) + .max(1); if flags & EVAL_JSON_INVALID_UTF8_IGNORE == 0 { eval_json_encode_append_char('\u{fffd}', flags, output); } @@ -15758,9 +15784,7 @@ fn eval_predefined_constant_value(name: &str) -> Option "PREG_PATTERN_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_PATTERN_ORDER)), "PREG_SET_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SET_ORDER)), "PREG_OFFSET_CAPTURE" => Some(EvalPredefinedConstant::Int(EVAL_PREG_OFFSET_CAPTURE)), - "PREG_UNMATCHED_AS_NULL" => { - Some(EvalPredefinedConstant::Int(EVAL_PREG_UNMATCHED_AS_NULL)) - } + "PREG_UNMATCHED_AS_NULL" => Some(EvalPredefinedConstant::Int(EVAL_PREG_UNMATCHED_AS_NULL)), "JSON_ERROR_NONE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_NONE)), "JSON_ERROR_DEPTH" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_DEPTH)), "JSON_ERROR_STATE_MISMATCH" => { @@ -15771,14 +15795,12 @@ fn eval_predefined_constant_value(name: &str) -> Option "JSON_ERROR_UTF8" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF8)), "JSON_ERROR_RECURSION" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_RECURSION)), "JSON_ERROR_INF_OR_NAN" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_INF_OR_NAN)), - "JSON_ERROR_UNSUPPORTED_TYPE" => { - Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UNSUPPORTED_TYPE)) - } - "JSON_ERROR_INVALID_PROPERTY_NAME" => { - Some(EvalPredefinedConstant::Int( - EVAL_JSON_ERROR_INVALID_PROPERTY_NAME, - )) - } + "JSON_ERROR_UNSUPPORTED_TYPE" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_ERROR_UNSUPPORTED_TYPE, + )), + "JSON_ERROR_INVALID_PROPERTY_NAME" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_ERROR_INVALID_PROPERTY_NAME, + )), "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), "JSON_HEX_TAG" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_TAG)), "JSON_HEX_AMP" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_AMP)), @@ -15789,27 +15811,19 @@ fn eval_predefined_constant_value(name: &str) -> Option "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), "JSON_UNESCAPED_UNICODE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_UNICODE)), - "JSON_PARTIAL_OUTPUT_ON_ERROR" => { - Some(EvalPredefinedConstant::Int( - EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR, - )) - } + "JSON_PARTIAL_OUTPUT_ON_ERROR" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR, + )), "JSON_PRETTY_PRINT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_PRETTY_PRINT)), - "JSON_PRESERVE_ZERO_FRACTION" => { - Some(EvalPredefinedConstant::Int( - EVAL_JSON_PRESERVE_ZERO_FRACTION, - )) - } + "JSON_PRESERVE_ZERO_FRACTION" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_PRESERVE_ZERO_FRACTION, + )), "JSON_INVALID_UTF8_IGNORE" => { - Some(EvalPredefinedConstant::Int( - EVAL_JSON_INVALID_UTF8_IGNORE, - )) - } - "JSON_INVALID_UTF8_SUBSTITUTE" => { - Some(EvalPredefinedConstant::Int( - EVAL_JSON_INVALID_UTF8_SUBSTITUTE, - )) + Some(EvalPredefinedConstant::Int(EVAL_JSON_INVALID_UTF8_IGNORE)) } + "JSON_INVALID_UTF8_SUBSTITUTE" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_INVALID_UTF8_SUBSTITUTE, + )), "JSON_THROW_ON_ERROR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_THROW_ON_ERROR)), "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), @@ -15922,7 +15936,11 @@ mod tests { FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) .map(FakeKey::Int) .map_or_else( - || Ok(FakeKey::String(String::from_utf8_lossy(&value).into_owned())), + || { + Ok(FakeKey::String( + String::from_utf8_lossy(&value).into_owned(), + )) + }, Ok, ), FakeValue::Null => Ok(FakeKey::String(String::new())), @@ -16100,10 +16118,7 @@ mod tests { } /// Returns the number of fake object properties in insertion order. - fn object_property_len( - &mut self, - object: RuntimeCellHandle, - ) -> Result { + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { match self.get(object) { FakeValue::Object(properties) => Ok(properties.len()), FakeValue::Iterator { .. } => Ok(0), @@ -16168,8 +16183,8 @@ mod tests { let [arg] = args.as_slice() else { return Err(EvalStatus::UnsupportedConstruct); }; - let x = Self::object_property(&properties, "x") - .ok_or(EvalStatus::RuntimeFatal)?; + let x = + Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; let FakeValue::Int(x) = self.get(x) else { return Err(EvalStatus::UnsupportedConstruct); }; @@ -16182,8 +16197,8 @@ mod tests { let [left, right] = args.as_slice() else { return Err(EvalStatus::UnsupportedConstruct); }; - let x = Self::object_property(&properties, "x") - .ok_or(EvalStatus::RuntimeFatal)?; + let x = + Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; let FakeValue::Int(x) = self.get(x) else { return Err(EvalStatus::UnsupportedConstruct); }; @@ -16261,7 +16276,9 @@ mod tests { FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { Ok(!exclude_self) } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => { + Ok(true) + } _ => Ok(false), } } @@ -16891,13 +16908,9 @@ mod tests { let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let outcome = execute_program_outcome_with_context( - &mut context, - &program, - &mut scope, - &mut values, - ) - .expect("throw should be an eval outcome"); + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); match outcome { EvalOutcome::Throwable(value) => { @@ -17104,16 +17117,14 @@ mod tests { let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let outcome = execute_program_outcome_with_context( - &mut context, - &program, - &mut scope, - &mut values, - ) - .expect("throw should be an eval outcome"); + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); match outcome { - EvalOutcome::Throwable(value) => assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)), + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)) + } EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), } assert_eq!(values.output, "finally"); @@ -17146,9 +17157,8 @@ mod tests { .new_object("Exception") .expect("allocate second fake exception"); - let first = - execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) - .expect("execute first dynamic function call"); + let first = execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) + .expect("execute first dynamic function call"); let second = execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) .expect("execute second dynamic function call"); @@ -17197,7 +17207,9 @@ mod tests { let mut values = FakeOps::default(); execute_program_with_context(&mut context, &program, &mut scope, &mut values) .expect("declare dynamic function"); - let thrown = values.new_object("Exception").expect("allocate fake exception"); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); let outcome = execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) @@ -17216,16 +17228,14 @@ mod tests { let mut context = ElephcEvalContext::new(); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let thrown = values.new_object("Exception").expect("allocate fake exception"); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); scope.set("e", thrown, ScopeCellOwnership::Borrowed); - let outcome = execute_program_outcome_with_context( - &mut context, - &program, - &mut scope, - &mut values, - ) - .expect("nested throw should be an eval outcome"); + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("nested throw should be an eval outcome"); match outcome { EvalOutcome::Throwable(value) => assert_eq!(value, thrown), @@ -17252,7 +17262,8 @@ mod tests { ), ) .expect("write include fixture"); - let program = parse_fragment(br#"return include "piece.php";"#).expect("parse eval fragment"); + let program = + parse_fragment(br#"return include "piece.php";"#).expect("parse eval fragment"); let mut context = ElephcEvalContext::new(); context.set_call_site( dir.join("main.php").to_string_lossy().into_owned(), @@ -17279,10 +17290,8 @@ mod tests { /// Verifies regular include marks a file so later include_once skips it and returns true. #[test] fn execute_program_include_once_skips_regularly_included_file() { - let dir = std::env::temp_dir().join(format!( - "elephc-eval-include-{}-once", - std::process::id() - )); + let dir = + std::env::temp_dir().join(format!("elephc-eval-include-{}-once", std::process::id())); let path = dir.join("once.php"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("create include_once fixture directory"); @@ -17308,8 +17317,10 @@ mod tests { /// Verifies missing include warns and returns false without aborting the eval program. #[test] fn execute_program_missing_include_warns_and_returns_false() { - let missing = std::env::temp_dir() - .join(format!("elephc-eval-missing-{}-include.php", std::process::id())); + let missing = std::env::temp_dir().join(format!( + "elephc-eval-missing-{}-include.php", + std::process::id() + )); let source = format!(r#"return include "{}";"#, missing.to_string_lossy()); let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); let mut context = ElephcEvalContext::new(); @@ -17326,17 +17337,18 @@ mod tests { /// Verifies missing require emits warnings and aborts the eval program. #[test] fn execute_program_missing_require_is_runtime_fatal() { - let missing = std::env::temp_dir() - .join(format!("elephc-eval-missing-{}-require.php", std::process::id())); + let missing = std::env::temp_dir().join(format!( + "elephc-eval-missing-{}-require.php", + std::process::id() + )); let source = format!(r#"require "{}";"#, missing.to_string_lossy()); let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); let mut context = ElephcEvalContext::new(); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect_err("missing require should fail"); + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("missing require should fail"); assert_eq!(err, EvalStatus::RuntimeFatal); assert_eq!(values.warnings.len(), 2); @@ -17881,10 +17893,9 @@ return $box->x;"#, /// Verifies match expressions use strict comparison across comma-separated patterns. #[test] fn execute_program_match_uses_strict_pattern_comparison() { - let program = parse_fragment( - br#"return match ($x) { 1, "1" => "string", default => "other" };"#, - ) - .expect("parse eval fragment"); + let program = + parse_fragment(br#"return match ($x) { 1, "1" => "string", default => "other" };"#) + .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); let x = values.string("1").expect("create fake string"); @@ -17898,9 +17909,10 @@ return $box->x;"#, /// Verifies match expressions evaluate only the selected arm result. #[test] fn execute_program_match_skips_unselected_results() { - let program = - parse_fragment(br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#) - .expect("parse eval fragment"); + let program = parse_fragment( + br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#, + ) + .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -20554,10 +20566,7 @@ return function_exists("ctype_space");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "A:D:N:S:empty:not-digit:not-space:111" - ); + assert_eq!(values.output, "A:D:N:S:empty:not-digit:not-space:111"); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -20601,10 +20610,7 @@ return count($algos);"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "28:md2:sha256:crc:whirlpool:joaat:exists" - ); + assert_eq!(values.output, "28:md2:sha256:crc:whirlpool:joaat:exists"); assert_eq!(values.get(result), FakeValue::Int(28)); } @@ -21664,7 +21670,10 @@ return function_exists("ucwords");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Hello World:Hello-World:Hello\tWorld:A B:A-B:"); + assert_eq!( + values.output, + "Hello World:Hello-World:Hello\tWorld:A B:A-B:" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -21757,10 +21766,7 @@ return function_exists("strstr");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "@example.com:hel:F:hello:bcabc:a:" - ); + assert_eq!(values.output, "@example.com:hel:F:hello:bcabc:a:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -22034,7 +22040,10 @@ return call_user_func_array("get_class", ["object" => $object]);"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); assert_eq!(values.output, "stdClass:stdClass:"); - assert_eq!(values.get(result), FakeValue::String("stdClass".to_string())); + assert_eq!( + values.get(result), + FakeValue::String("stdClass".to_string()) + ); } /// Verifies eval `get_parent_class()` reads object and class-string parents by callable. @@ -22054,11 +22063,11 @@ return call_user_func_array("get_parent_class", ["object_or_class" => "ChildClas let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "ParentClass:ParentClass:ParentClass:"); assert_eq!( - values.output, - "ParentClass:ParentClass:ParentClass:" + values.get(result), + FakeValue::String("ParentClass".to_string()) ); - assert_eq!(values.get(result), FakeValue::String("ParentClass".to_string())); } /// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. @@ -22336,8 +22345,7 @@ return is_callable("clamp");"#, /// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. #[test] fn execute_program_rejects_clamp_invalid_bounds() { - let program = - parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); + let program = parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); @@ -22574,7 +22582,10 @@ return function_exists("addslashes") && function_exists("stripslashes");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:"); + assert_eq!( + values.output, + "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/json_validate.rs b/crates/elephc-eval/src/json_validate.rs index f27d384665..3d327b0ff0 100644 --- a/crates/elephc-eval/src/json_validate.rs +++ b/crates/elephc-eval/src/json_validate.rs @@ -57,10 +57,7 @@ pub(crate) enum JsonParseErrorKind { } /// Parses one complete JSON document and preserves the PHP-visible error category. -pub(crate) fn decode_result( - bytes: &[u8], - depth_limit: usize, -) -> Result { +pub(crate) fn decode_result(bytes: &[u8], depth_limit: usize) -> Result { let mut parser = Parser::new(bytes, depth_limit); parser.parse_document() } @@ -145,7 +142,10 @@ impl<'a> Parser<'a> { /// Parses any JSON value at the given active container depth. fn parse_value(&mut self, depth: usize) -> Result { self.skip_ws(); - match self.peek().ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? { + match self + .peek() + .ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? + { b'n' => self.consume_literal_value(b"null", JsonValue::Null), b't' => self.consume_literal_value(b"true", JsonValue::Bool(true)), b'f' => self.consume_literal_value(b"false", JsonValue::Bool(false)), @@ -271,7 +271,8 @@ impl<'a> Parser<'a> { Ok(()) } InvalidUtf8Mode::Substitute => { - append_codepoint(output, 0xfffd).ok_or_else(|| self.error(JsonParseErrorKind::Utf8))?; + append_codepoint(output, 0xfffd) + .ok_or_else(|| self.error(JsonParseErrorKind::Utf8))?; self.cursor = start + 1; Ok(()) } @@ -281,7 +282,10 @@ impl<'a> Parser<'a> { /// Parses one JSON string escape sequence at the current backslash. fn parse_string_escape(&mut self, output: &mut Vec) -> Result<(), JsonParseError> { self.cursor += 1; - match self.peek().ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? { + match self + .peek() + .ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? + { b'"' => output.push(b'"'), b'\\' => output.push(b'\\'), b'/' => output.push(b'/'), @@ -356,7 +360,10 @@ impl<'a> Parser<'a> { return Err(self.error(JsonParseErrorKind::Syntax)); } - match self.peek().ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? { + match self + .peek() + .ok_or_else(|| self.error(JsonParseErrorKind::Syntax))? + { b'0' => { self.cursor += 1; if matches!(self.peek(), Some(b'0'..=b'9')) { diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks.rs index ae1334fd08..2d2ea0ce04 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks.rs @@ -77,7 +77,8 @@ unsafe extern "C" { exclude_self: u64, ) -> u64; fn __elephc_eval_value_object_class_name(object: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_parent_class_name(object_or_class: *mut RuntimeCell) -> *mut RuntimeCell; + fn __elephc_eval_value_parent_class_name(object_or_class: *mut RuntimeCell) + -> *mut RuntimeCell; /// Returns whether generated trait metadata contains the requested PHP name. fn __elephc_eval_trait_exists(name_ptr: *const u8, name_len: u64) -> u64; /// Returns whether generated enum metadata contains the requested PHP name. @@ -281,10 +282,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { } /// Returns the JSON-visible public property count for a boxed Mixed object. - fn object_property_len( - &mut self, - object: RuntimeCellHandle, - ) -> Result { + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_object_property_len(object.as_ptr()) }; usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) } From a56cdbac62367c3c483c4a591652f7557c81ea8c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 09:49:33 +0200 Subject: [PATCH 0278/1208] Split eval parser and lexer modules --- crates/elephc-eval/src/lexer/mod.rs | 17 + crates/elephc-eval/src/lexer/scan.rs | 499 +++ crates/elephc-eval/src/lexer/token.rs | 80 + crates/elephc-eval/src/lib.rs | 1 + crates/elephc-eval/src/parser.rs | 4279 ------------------------ crates/elephc-eval/src/parser/mod.rs | 38 + crates/elephc-eval/src/parser/state.rs | 1907 +++++++++++ crates/elephc-eval/src/parser/tests.rs | 1803 ++++++++++ 8 files changed, 4345 insertions(+), 4279 deletions(-) create mode 100644 crates/elephc-eval/src/lexer/mod.rs create mode 100644 crates/elephc-eval/src/lexer/scan.rs create mode 100644 crates/elephc-eval/src/lexer/token.rs delete mode 100644 crates/elephc-eval/src/parser.rs create mode 100644 crates/elephc-eval/src/parser/mod.rs create mode 100644 crates/elephc-eval/src/parser/state.rs create mode 100644 crates/elephc-eval/src/parser/tests.rs diff --git a/crates/elephc-eval/src/lexer/mod.rs b/crates/elephc-eval/src/lexer/mod.rs new file mode 100644 index 0000000000..eb56280b62 --- /dev/null +++ b/crates/elephc-eval/src/lexer/mod.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Groups tokenization for runtime PHP eval fragments. +//! The lexer owns token definitions and source scanning before parser grammar +//! state consumes those tokens. +//! +//! Called from: +//! - `crate::parser::parse_fragment()`. +//! +//! Key details: +//! - Fragment line metadata is captured while scanning magic constants. +//! - PHP opening tags are rejected before tokenization by the parser entry point. + +mod scan; +mod token; + +pub(crate) use scan::tokenize; +pub(crate) use token::TokenKind; diff --git a/crates/elephc-eval/src/lexer/scan.rs b/crates/elephc-eval/src/lexer/scan.rs new file mode 100644 index 0000000000..605962408b --- /dev/null +++ b/crates/elephc-eval/src/lexer/scan.rs @@ -0,0 +1,499 @@ +//! Purpose: +//! Scans UTF-8 eval source fragments into eval parser tokens. +//! This file owns trivia skipping, literal lexing, PHP string escapes, and +//! magic-constant token recognition. +//! +//! Called from: +//! - `crate::lexer::tokenize()` re-exported by `crate::lexer`. +//! +//! Key details: +//! - Comments and whitespace advance line metadata for `__LINE__`. +//! - Unterminated strings or block comments return parse errors before grammar parsing. + +use super::TokenKind; +use crate::errors::EvalParseError; +use crate::eval_ir::EvalMagicConst; + +/// Tokenizes a complete source fragment and appends an EOF sentinel. +pub(crate) fn tokenize(source: &str) -> Result, EvalParseError> { + Lexer::new(source).tokenize() +} + +/// Converts a UTF-8 eval source fragment into parser tokens. +struct Lexer<'a> { + source: &'a str, + pos: usize, + line: i64, +} + +impl<'a> Lexer<'a> { + /// Creates a lexer over a UTF-8 eval fragment. + fn new(source: &'a str) -> Self { + Self { + source, + pos: 0, + line: 1, + } + } + + /// Tokenizes the complete source and appends an EOF sentinel. + fn tokenize(mut self) -> Result, EvalParseError> { + let mut tokens = Vec::new(); + loop { + let token = self.next_token()?; + let done = token == TokenKind::Eof; + tokens.push(token); + if done { + break; + } + } + Ok(tokens) + } + + /// Reads the next token from the source. + fn next_token(&mut self) -> Result { + self.skip_trivia()?; + let Some(ch) = self.peek_char() else { + return Ok(TokenKind::Eof); + }; + let line = self.line; + match ch { + '$' => self.lex_variable(), + '\'' | '"' => self.lex_string(ch), + '0'..='9' => self.lex_number(), + '+' => { + self.bump_char(); + if self.peek_char() == Some('+') { + self.bump_char(); + Ok(TokenKind::PlusPlus) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PlusEqual) + } else { + Ok(TokenKind::Plus) + } + } + '-' => { + self.bump_char(); + if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::Arrow) + } else if self.peek_char() == Some('-') { + self.bump_char(); + Ok(TokenKind::MinusMinus) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::MinusEqual) + } else { + Ok(TokenKind::Minus) + } + } + '*' => { + self.bump_char(); + if self.peek_char() == Some('*') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::StarStarEqual) + } else { + Ok(TokenKind::StarStar) + } + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::StarEqual) + } else { + Ok(TokenKind::Star) + } + } + '/' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::SlashEqual) + } else { + Ok(TokenKind::Slash) + } + } + '%' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PercentEqual) + } else { + Ok(TokenKind::Percent) + } + } + '.' => { + self.bump_char(); + if self.peek_char() == Some('.') && self.peek_next_char() == Some('.') { + self.bump_char(); + self.bump_char(); + Ok(TokenKind::Ellipsis) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::DotEqual) + } else { + Ok(TokenKind::Dot) + } + } + '=' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::EqualEqualEqual) + } else { + Ok(TokenKind::EqualEqual) + } + } else if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::FatArrow) + } else { + Ok(TokenKind::Equal) + } + } + '!' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::NotEqualEqual) + } else { + Ok(TokenKind::NotEqual) + } + } else { + Ok(TokenKind::Bang) + } + } + '&' => { + self.bump_char(); + if self.peek_char() == Some('&') { + self.bump_char(); + Ok(TokenKind::AndAnd) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::AmpEqual) + } else { + Ok(TokenKind::Ampersand) + } + } + '|' => { + self.bump_char(); + if self.peek_char() == Some('|') { + self.bump_char(); + Ok(TokenKind::OrOr) + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::PipeEqual) + } else { + Ok(TokenKind::Pipe) + } + } + '^' => { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::CaretEqual) + } else { + Ok(TokenKind::Caret) + } + } + '~' => { + self.bump_char(); + Ok(TokenKind::Tilde) + } + '<' => { + self.bump_char(); + if self.peek_char() == Some('<') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::LessLessEqual) + } else { + Ok(TokenKind::LessLess) + } + } else if self.peek_char() == Some('=') { + self.bump_char(); + if self.peek_char() == Some('>') { + self.bump_char(); + Ok(TokenKind::Spaceship) + } else { + Ok(TokenKind::LessEqual) + } + } else { + Ok(TokenKind::Less) + } + } + '>' => { + self.bump_char(); + if self.peek_char() == Some('>') { + self.bump_char(); + if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::GreaterGreaterEqual) + } else { + Ok(TokenKind::GreaterGreater) + } + } else if self.peek_char() == Some('=') { + self.bump_char(); + Ok(TokenKind::GreaterEqual) + } else { + Ok(TokenKind::Greater) + } + } + '?' => { + self.bump_char(); + if self.peek_char() == Some('?') { + self.bump_char(); + Ok(TokenKind::QuestionQuestion) + } else { + Ok(TokenKind::Question) + } + } + ';' => { + self.bump_char(); + Ok(TokenKind::Semicolon) + } + '(' => { + self.bump_char(); + Ok(TokenKind::LParen) + } + ')' => { + self.bump_char(); + Ok(TokenKind::RParen) + } + '[' => { + self.bump_char(); + Ok(TokenKind::LBracket) + } + ']' => { + self.bump_char(); + Ok(TokenKind::RBracket) + } + '{' => { + self.bump_char(); + Ok(TokenKind::LBrace) + } + '}' => { + self.bump_char(); + Ok(TokenKind::RBrace) + } + ',' => { + self.bump_char(); + Ok(TokenKind::Comma) + } + ':' => { + self.bump_char(); + Ok(TokenKind::Colon) + } + '\\' => { + self.bump_char(); + Ok(TokenKind::Backslash) + } + _ if is_ident_start(ch) => { + let ident = self.lex_ident(); + Ok(magic_const_token(&ident, line).unwrap_or(TokenKind::Ident(ident))) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Reads a `$name` token. + fn lex_variable(&mut self) -> Result { + self.bump_char(); + let name = self.lex_ident(); + if name.is_empty() { + return Err(EvalParseError::ExpectedVariable); + } + Ok(TokenKind::DollarIdent(name)) + } + + /// Reads a PHP identifier body at the current byte offset. + fn lex_ident(&mut self) -> String { + let mut ident = String::new(); + while let Some(ch) = self.peek_char() { + if !is_ident_continue(ch) { + break; + } + ident.push(ch); + self.bump_char(); + } + ident + } + + /// Reads an integer or float literal. + fn lex_number(&mut self) -> Result { + let start = self.pos; + while matches!(self.peek_char(), Some('0'..='9')) { + self.bump_char(); + } + let mut is_float = false; + if self.peek_char() == Some('.') && matches!(self.peek_next_char(), Some('0'..='9')) { + is_float = true; + self.bump_char(); + while matches!(self.peek_char(), Some('0'..='9')) { + self.bump_char(); + } + } + let raw = &self.source[start..self.pos]; + if is_float { + raw.parse::() + .map(TokenKind::Float) + .map_err(|_| EvalParseError::InvalidNumber) + } else { + raw.parse::() + .map(TokenKind::Int) + .map_err(|_| EvalParseError::InvalidNumber) + } + } + + /// Reads a single- or double-quoted string literal. + fn lex_string(&mut self, quote: char) -> Result { + self.bump_char(); + let mut out = String::new(); + while let Some(ch) = self.peek_char() { + self.bump_char(); + if ch == quote { + return Ok(TokenKind::String(out)); + } + if ch == '\\' { + let Some(escaped) = self.peek_char() else { + return Err(EvalParseError::UnterminatedString); + }; + self.bump_char(); + if quote == '\'' { + match escaped { + '\\' => out.push('\\'), + '\'' => out.push('\''), + other => { + out.push('\\'); + out.push(other); + } + } + } else { + match escaped { + 'n' => out.push('\n'), + 'r' => out.push('\r'), + 't' => out.push('\t'), + 'v' => out.push('\x0b'), + 'e' => out.push('\x1b'), + 'f' => out.push('\x0c'), + '\\' => out.push('\\'), + '"' => out.push('"'), + '$' => out.push('$'), + other => { + out.push('\\'); + out.push(other); + } + } + } + } else { + out.push(ch); + } + } + Err(EvalParseError::UnterminatedString) + } + + /// Advances past ASCII/Unicode whitespace and PHP comments. + fn skip_trivia(&mut self) -> Result<(), EvalParseError> { + loop { + while self.peek_char().is_some_and(char::is_whitespace) { + self.bump_char(); + } + match (self.peek_char(), self.peek_next_char()) { + (Some('/'), Some('/')) => self.skip_line_comment(), + (Some('#'), _) => self.skip_line_comment(), + (Some('/'), Some('*')) => self.skip_block_comment()?, + _ => return Ok(()), + } + } + } + + /// Advances past a `//` or `#` comment, including its trailing newline when present. + fn skip_line_comment(&mut self) { + while let Some(ch) = self.peek_char() { + self.bump_char(); + if ch == '\n' { + break; + } + } + } + + /// Advances past a `/* ... */` comment while preserving fragment line metadata. + fn skip_block_comment(&mut self) -> Result<(), EvalParseError> { + self.bump_char(); + self.bump_char(); + while let Some(ch) = self.peek_char() { + if ch == '*' && self.peek_next_char() == Some('/') { + self.bump_char(); + self.bump_char(); + return Ok(()); + } + self.bump_char(); + } + Err(EvalParseError::UnterminatedComment) + } + + /// Returns the current char without advancing. + fn peek_char(&self) -> Option { + self.source[self.pos..].chars().next() + } + + /// Returns the char after the current char without advancing. + fn peek_next_char(&self) -> Option { + let mut chars = self.source[self.pos..].chars(); + chars.next()?; + chars.next() + } + + /// Advances by one UTF-8 char. + fn bump_char(&mut self) { + if let Some(ch) = self.peek_char() { + self.pos += ch.len_utf8(); + if ch == '\n' { + self.line += 1; + } + } + } +} + +/// Returns true for the first character of a PHP variable/function identifier. +fn is_ident_start(ch: char) -> bool { + ch == '_' || ch.is_ascii_alphabetic() +} + +/// Returns true for subsequent characters in a PHP variable/function identifier. +fn is_ident_continue(ch: char) -> bool { + is_ident_start(ch) || ch.is_ascii_digit() +} + +/// Converts a PHP magic-constant identifier into a parser token when recognized. +fn magic_const_token(name: &str, line: i64) -> Option { + let magic = if ident_eq(name, "__FILE__") { + EvalMagicConst::File + } else if ident_eq(name, "__DIR__") { + EvalMagicConst::Dir + } else if ident_eq(name, "__LINE__") { + EvalMagicConst::Line(line) + } else if ident_eq(name, "__FUNCTION__") { + EvalMagicConst::Function + } else if ident_eq(name, "__CLASS__") { + EvalMagicConst::Class + } else if ident_eq(name, "__METHOD__") { + EvalMagicConst::Method + } else if ident_eq(name, "__NAMESPACE__") { + EvalMagicConst::Namespace + } else if ident_eq(name, "__TRAIT__") { + EvalMagicConst::Trait + } else { + return None; + }; + Some(TokenKind::Magic(magic)) +} + +/// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. +fn ident_eq(actual: &str, expected: &str) -> bool { + actual.eq_ignore_ascii_case(expected) +} diff --git a/crates/elephc-eval/src/lexer/token.rs b/crates/elephc-eval/src/lexer/token.rs new file mode 100644 index 0000000000..0827d4b4c7 --- /dev/null +++ b/crates/elephc-eval/src/lexer/token.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Defines token kinds for runtime PHP eval fragment parsing. +//! Tokens are intentionally scoped to the eval subset and do not expose the +//! main compiler lexer token contract. +//! +//! Called from: +//! - `crate::lexer::scan::tokenize()` +//! - `crate::parser::state::Parser` +//! +//! Key details: +//! - Magic constants carry precomputed fragment line metadata when needed. + +use crate::eval_ir::EvalMagicConst; + +/// Token kinds used by the initial eval fragment parser. +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum TokenKind { + DollarIdent(String), + Ident(String), + Magic(EvalMagicConst), + Int(i64), + Float(f64), + String(String), + Plus, + PlusPlus, + PlusEqual, + Minus, + MinusMinus, + MinusEqual, + Arrow, + Star, + StarStar, + StarStarEqual, + StarEqual, + Slash, + SlashEqual, + Percent, + PercentEqual, + Ampersand, + AmpEqual, + Pipe, + PipeEqual, + Caret, + CaretEqual, + Tilde, + Dot, + DotEqual, + Ellipsis, + Equal, + EqualEqual, + EqualEqualEqual, + Bang, + NotEqual, + NotEqualEqual, + AndAnd, + OrOr, + Less, + LessEqual, + Spaceship, + LessLess, + LessLessEqual, + Greater, + GreaterEqual, + GreaterGreater, + GreaterGreaterEqual, + FatArrow, + Question, + QuestionQuestion, + Semicolon, + LParen, + RParen, + LBracket, + RBracket, + LBrace, + RBrace, + Comma, + Colon, + Backslash, + Eof, +} diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index e173b6e1b0..96a3d69137 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -18,6 +18,7 @@ pub mod eval_ir; mod ffi; pub mod interpreter; mod json_validate; +mod lexer; pub mod lower; pub mod parser; pub mod runtime_hooks; diff --git a/crates/elephc-eval/src/parser.rs b/crates/elephc-eval/src/parser.rs deleted file mode 100644 index 889c1b6dc0..0000000000 --- a/crates/elephc-eval/src/parser.rs +++ /dev/null @@ -1,4279 +0,0 @@ -//! Purpose: -//! Parses runtime PHP eval fragments into the initial EvalIR statement form. -//! The parser handles a small statement subset now and keeps unsupported syntax -//! as parse failure until the full eval parser is implemented. -//! -//! Called from: -//! - `crate::__elephc_eval_execute()` -//! - Future `crate::interpreter` entry points. -//! -//! Key details: -//! - PHP eval fragments are statement fragments and must not include opening -//! ` Result { - if contains_php_open_tag(code) { - return Err(EvalParseError::PhpOpenTag); - } - let source = std::str::from_utf8(code).map_err(|_| EvalParseError::InvalidUtf8)?; - let tokens = Lexer::new(source).tokenize()?; - Parser::new(tokens, code.len()).parse_program() -} - -/// Returns true when a fragment contains a PHP opening tag sequence. -fn contains_php_open_tag(code: &[u8]) -> bool { - code.windows(2).any(|window| window == b" { - source: &'a str, - pos: usize, - line: i64, -} - -impl<'a> Lexer<'a> { - /// Creates a lexer over a UTF-8 eval fragment. - fn new(source: &'a str) -> Self { - Self { - source, - pos: 0, - line: 1, - } - } - - /// Tokenizes the complete source and appends an EOF sentinel. - fn tokenize(mut self) -> Result, EvalParseError> { - let mut tokens = Vec::new(); - loop { - let token = self.next_token()?; - let done = token == TokenKind::Eof; - tokens.push(token); - if done { - break; - } - } - Ok(tokens) - } - - /// Reads the next token from the source. - fn next_token(&mut self) -> Result { - self.skip_trivia()?; - let Some(ch) = self.peek_char() else { - return Ok(TokenKind::Eof); - }; - let line = self.line; - match ch { - '$' => self.lex_variable(), - '\'' | '"' => self.lex_string(ch), - '0'..='9' => self.lex_number(), - '+' => { - self.bump_char(); - if self.peek_char() == Some('+') { - self.bump_char(); - Ok(TokenKind::PlusPlus) - } else if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::PlusEqual) - } else { - Ok(TokenKind::Plus) - } - } - '-' => { - self.bump_char(); - if self.peek_char() == Some('>') { - self.bump_char(); - Ok(TokenKind::Arrow) - } else if self.peek_char() == Some('-') { - self.bump_char(); - Ok(TokenKind::MinusMinus) - } else if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::MinusEqual) - } else { - Ok(TokenKind::Minus) - } - } - '*' => { - self.bump_char(); - if self.peek_char() == Some('*') { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::StarStarEqual) - } else { - Ok(TokenKind::StarStar) - } - } else if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::StarEqual) - } else { - Ok(TokenKind::Star) - } - } - '/' => { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::SlashEqual) - } else { - Ok(TokenKind::Slash) - } - } - '%' => { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::PercentEqual) - } else { - Ok(TokenKind::Percent) - } - } - '.' => { - self.bump_char(); - if self.peek_char() == Some('.') && self.peek_next_char() == Some('.') { - self.bump_char(); - self.bump_char(); - Ok(TokenKind::Ellipsis) - } else if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::DotEqual) - } else { - Ok(TokenKind::Dot) - } - } - '=' => { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::EqualEqualEqual) - } else { - Ok(TokenKind::EqualEqual) - } - } else if self.peek_char() == Some('>') { - self.bump_char(); - Ok(TokenKind::FatArrow) - } else { - Ok(TokenKind::Equal) - } - } - '!' => { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::NotEqualEqual) - } else { - Ok(TokenKind::NotEqual) - } - } else { - Ok(TokenKind::Bang) - } - } - '&' => { - self.bump_char(); - if self.peek_char() == Some('&') { - self.bump_char(); - Ok(TokenKind::AndAnd) - } else if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::AmpEqual) - } else { - Ok(TokenKind::Ampersand) - } - } - '|' => { - self.bump_char(); - if self.peek_char() == Some('|') { - self.bump_char(); - Ok(TokenKind::OrOr) - } else if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::PipeEqual) - } else { - Ok(TokenKind::Pipe) - } - } - '^' => { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::CaretEqual) - } else { - Ok(TokenKind::Caret) - } - } - '~' => { - self.bump_char(); - Ok(TokenKind::Tilde) - } - '<' => { - self.bump_char(); - if self.peek_char() == Some('<') { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::LessLessEqual) - } else { - Ok(TokenKind::LessLess) - } - } else if self.peek_char() == Some('=') { - self.bump_char(); - if self.peek_char() == Some('>') { - self.bump_char(); - Ok(TokenKind::Spaceship) - } else { - Ok(TokenKind::LessEqual) - } - } else { - Ok(TokenKind::Less) - } - } - '>' => { - self.bump_char(); - if self.peek_char() == Some('>') { - self.bump_char(); - if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::GreaterGreaterEqual) - } else { - Ok(TokenKind::GreaterGreater) - } - } else if self.peek_char() == Some('=') { - self.bump_char(); - Ok(TokenKind::GreaterEqual) - } else { - Ok(TokenKind::Greater) - } - } - '?' => { - self.bump_char(); - if self.peek_char() == Some('?') { - self.bump_char(); - Ok(TokenKind::QuestionQuestion) - } else { - Ok(TokenKind::Question) - } - } - ';' => { - self.bump_char(); - Ok(TokenKind::Semicolon) - } - '(' => { - self.bump_char(); - Ok(TokenKind::LParen) - } - ')' => { - self.bump_char(); - Ok(TokenKind::RParen) - } - '[' => { - self.bump_char(); - Ok(TokenKind::LBracket) - } - ']' => { - self.bump_char(); - Ok(TokenKind::RBracket) - } - '{' => { - self.bump_char(); - Ok(TokenKind::LBrace) - } - '}' => { - self.bump_char(); - Ok(TokenKind::RBrace) - } - ',' => { - self.bump_char(); - Ok(TokenKind::Comma) - } - ':' => { - self.bump_char(); - Ok(TokenKind::Colon) - } - '\\' => { - self.bump_char(); - Ok(TokenKind::Backslash) - } - _ if is_ident_start(ch) => { - let ident = self.lex_ident(); - Ok(magic_const_token(&ident, line).unwrap_or(TokenKind::Ident(ident))) - } - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Reads a `$name` token. - fn lex_variable(&mut self) -> Result { - self.bump_char(); - let name = self.lex_ident(); - if name.is_empty() { - return Err(EvalParseError::ExpectedVariable); - } - Ok(TokenKind::DollarIdent(name)) - } - - /// Reads a PHP identifier body at the current byte offset. - fn lex_ident(&mut self) -> String { - let mut ident = String::new(); - while let Some(ch) = self.peek_char() { - if !is_ident_continue(ch) { - break; - } - ident.push(ch); - self.bump_char(); - } - ident - } - - /// Reads an integer or float literal. - fn lex_number(&mut self) -> Result { - let start = self.pos; - while matches!(self.peek_char(), Some('0'..='9')) { - self.bump_char(); - } - let mut is_float = false; - if self.peek_char() == Some('.') && matches!(self.peek_next_char(), Some('0'..='9')) { - is_float = true; - self.bump_char(); - while matches!(self.peek_char(), Some('0'..='9')) { - self.bump_char(); - } - } - let raw = &self.source[start..self.pos]; - if is_float { - raw.parse::() - .map(TokenKind::Float) - .map_err(|_| EvalParseError::InvalidNumber) - } else { - raw.parse::() - .map(TokenKind::Int) - .map_err(|_| EvalParseError::InvalidNumber) - } - } - - /// Reads a single- or double-quoted string literal. - fn lex_string(&mut self, quote: char) -> Result { - self.bump_char(); - let mut out = String::new(); - while let Some(ch) = self.peek_char() { - self.bump_char(); - if ch == quote { - return Ok(TokenKind::String(out)); - } - if ch == '\\' { - let Some(escaped) = self.peek_char() else { - return Err(EvalParseError::UnterminatedString); - }; - self.bump_char(); - if quote == '\'' { - match escaped { - '\\' => out.push('\\'), - '\'' => out.push('\''), - other => { - out.push('\\'); - out.push(other); - } - } - } else { - match escaped { - 'n' => out.push('\n'), - 'r' => out.push('\r'), - 't' => out.push('\t'), - 'v' => out.push('\x0b'), - 'e' => out.push('\x1b'), - 'f' => out.push('\x0c'), - '\\' => out.push('\\'), - '"' => out.push('"'), - '$' => out.push('$'), - other => { - out.push('\\'); - out.push(other); - } - } - } - } else { - out.push(ch); - } - } - Err(EvalParseError::UnterminatedString) - } - - /// Advances past ASCII/Unicode whitespace and PHP comments. - fn skip_trivia(&mut self) -> Result<(), EvalParseError> { - loop { - while self.peek_char().is_some_and(char::is_whitespace) { - self.bump_char(); - } - match (self.peek_char(), self.peek_next_char()) { - (Some('/'), Some('/')) => self.skip_line_comment(), - (Some('#'), _) => self.skip_line_comment(), - (Some('/'), Some('*')) => self.skip_block_comment()?, - _ => return Ok(()), - } - } - } - - /// Advances past a `//` or `#` comment, including its trailing newline when present. - fn skip_line_comment(&mut self) { - while let Some(ch) = self.peek_char() { - self.bump_char(); - if ch == '\n' { - break; - } - } - } - - /// Advances past a `/* ... */` comment while preserving fragment line metadata. - fn skip_block_comment(&mut self) -> Result<(), EvalParseError> { - self.bump_char(); - self.bump_char(); - while let Some(ch) = self.peek_char() { - if ch == '*' && self.peek_next_char() == Some('/') { - self.bump_char(); - self.bump_char(); - return Ok(()); - } - self.bump_char(); - } - Err(EvalParseError::UnterminatedComment) - } - - /// Returns the current char without advancing. - fn peek_char(&self) -> Option { - self.source[self.pos..].chars().next() - } - - /// Returns the char after the current char without advancing. - fn peek_next_char(&self) -> Option { - let mut chars = self.source[self.pos..].chars(); - chars.next()?; - chars.next() - } - - /// Advances by one UTF-8 char. - fn bump_char(&mut self) { - if let Some(ch) = self.peek_char() { - self.pos += ch.len_utf8(); - if ch == '\n' { - self.line += 1; - } - } - } -} - -/// Parses tokenized eval fragments into EvalIR. -struct Parser { - tokens: Vec, - pos: usize, - source_len: usize, - namespace: String, - imports: NamespaceImports, - allow_use_imports: bool, -} - -/// A parsed PHP name plus whether it used a leading global namespace separator. -struct ParsedQualifiedName { - name: String, - absolute: bool, -} - -/// Import alias tables active for the current namespace declaration region. -#[derive(Default)] -struct NamespaceImports { - classes: HashMap, - functions: HashMap, - constants: HashMap, -} - -/// The `use` declaration namespace being imported. -#[derive(Copy, Clone, Eq, PartialEq)] -enum UseImportKind { - Class, - Function, - Const, -} - -impl NamespaceImports { - /// Stores one class import under PHP's case-insensitive class alias key. - fn insert_class(&mut self, alias: String, name: String) { - self.classes.insert(alias.to_ascii_lowercase(), name); - } - - /// Stores one function import under PHP's case-insensitive function alias key. - fn insert_function(&mut self, alias: String, name: String) { - self.functions.insert(alias.to_ascii_lowercase(), name); - } - - /// Stores one constant import under PHP's case-sensitive constant alias key. - fn insert_constant(&mut self, alias: String, name: String) { - self.constants.insert(alias, name); - } - - /// Resolves a class import, including aliases used as the first segment of a class name. - fn resolve_class(&self, name: &str) -> Option { - let (first, tail) = split_first_name_segment(name); - let imported = self.classes.get(&first.to_ascii_lowercase())?; - Some(match tail { - Some(tail) => format!("{imported}\\{tail}"), - None => imported.clone(), - }) - } - - /// Resolves an unqualified function alias. - fn resolve_function(&self, name: &str) -> Option<&str> { - self.functions - .get(&name.to_ascii_lowercase()) - .map(String::as_str) - } - - /// Resolves a case-sensitive unqualified constant alias. - fn resolve_constant(&self, name: &str) -> Option<&str> { - self.constants.get(name).map(String::as_str) - } -} - -impl Parser { - /// Creates a parser over tokens produced from a source fragment. - fn new(tokens: Vec, source_len: usize) -> Self { - Self { - tokens, - pos: 0, - source_len, - namespace: String::new(), - imports: NamespaceImports::default(), - allow_use_imports: true, - } - } - - /// Parses a complete eval fragment until EOF. - fn parse_program(mut self) -> Result { - let mut statements = Vec::new(); - while !matches!(self.current(), TokenKind::Eof) { - statements.extend(self.parse_stmt()?); - } - Ok(EvalProgram::new(self.source_len, statements)) - } - - /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. - fn parse_stmt(&mut self) -> Result, EvalParseError> { - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "break") => { - self.advance(); - self.expect_semicolon()?; - Ok(vec![EvalStmt::Break]) - } - TokenKind::Ident(name) if ident_eq(name, "continue") => { - self.advance(); - self.expect_semicolon()?; - Ok(vec![EvalStmt::Continue]) - } - TokenKind::Ident(name) if ident_eq(name, "do") => self.parse_do_while_stmt(), - TokenKind::Ident(name) if ident_eq(name, "echo") => { - self.advance(); - let mut statements = vec![EvalStmt::Echo(self.parse_expr()?)]; - while self.consume(TokenKind::Comma) { - statements.push(EvalStmt::Echo(self.parse_expr()?)); - } - self.expect_semicolon()?; - Ok(statements) - } - TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), - TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), - TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), - TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), - TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), - TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), - TokenKind::Ident(name) if ident_eq(name, "namespace") => self.parse_namespace_stmt(), - TokenKind::Ident(name) if ident_eq(name, "return") => { - self.advance(); - if self.consume_semicolon() { - return Ok(vec![EvalStmt::Return(None)]); - } - let expr = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::Return(Some(expr))]) - } - TokenKind::Ident(name) if ident_eq(name, "static") => self.parse_static_var_stmt(), - TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), - TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), - TokenKind::Ident(name) if ident_eq(name, "try") => self.parse_try_stmt(), - TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), - TokenKind::Ident(name) if ident_eq(name, "use") && self.allow_use_imports => { - self.parse_use_stmt() - } - TokenKind::Ident(name) if ident_eq(name, "use") => { - Err(EvalParseError::UnsupportedConstruct) - } - TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), - TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { - Err(EvalParseError::UnsupportedConstruct) - } - TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), - TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { - self.parse_property_stmt(true) - } - TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { - self.parse_array_set_stmt(name.clone()) - } - TokenKind::DollarIdent(name) - if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => - { - self.parse_postfix_inc_dec_stmt(name.clone(), true) - } - TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { - let name = name.clone(); - self.parse_var_store_stmt(name, true) - } - TokenKind::Eof => Err(EvalParseError::UnexpectedEof), - _ => { - let expr = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::Expr(expr)]) - } - } - } - - /// Parses `do { ... } while (expr);`. - fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let body = self.parse_statement_body()?; - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::DoWhile { body, condition }]) - } - - /// Parses `$name[index] = expr;` and `$name[] = expr;` eval writes. - fn parse_array_set_stmt(&mut self, name: String) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LBracket)?; - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - self.expect_semicolon()?; - return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) - } - - /// Parses `for (init; condition; update) { ... }`. - fn parse_for_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let init = self.parse_for_init_clause()?; - self.expect_semicolon()?; - let condition = if matches!(self.current(), TokenKind::Semicolon) { - None - } else { - Some(self.parse_expr()?) - }; - self.expect_semicolon()?; - let update = self.parse_for_update_clause()?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::For { - init, - condition, - update, - body, - }]) - } - - /// Parses `foreach (expr as $value) { ... }` or `foreach (expr as $key => $value) { ... }`. - fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let array = self.parse_expr()?; - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "as")) { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - let TokenKind::DollarIdent(value_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let value_name = value_name.clone(); - self.advance(); - let (key_name, value_name) = if matches!(self.current(), TokenKind::FatArrow) { - self.advance(); - let TokenKind::DollarIdent(next_value_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let key_name = value_name; - let value_name = next_value_name.clone(); - self.advance(); - (Some(key_name), value_name) - } else { - (None, value_name) - }; - self.expect(TokenKind::RParen)?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::Foreach { - array, - key_name, - value_name, - body, - }]) - } - - /// Parses `class Name { ... }` declarations for dynamic class metadata. - fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); - self.expect(TokenKind::LBrace)?; - let mut properties = Vec::new(); - let mut methods = Vec::new(); - while !self.consume(TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - self.parse_class_member(&mut properties, &mut methods)?; - } - self.consume_semicolon(); - Ok(vec![EvalStmt::ClassDecl(EvalClass::new( - name, properties, methods, - ))]) - } - - /// Parses one public property or method from an eval class body. - fn parse_class_member( - &mut self, - properties: &mut Vec, - methods: &mut Vec, - ) -> Result<(), EvalParseError> { - let public = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) - { - self.advance(); - true - } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) - { - return Err(EvalParseError::UnsupportedConstruct); - } else { - false - }; - - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - methods.push(self.parse_class_method_decl()?); - return Ok(()); - } - - if !public { - return Err(EvalParseError::UnsupportedConstruct); - } - properties.push(self.parse_class_property_decl()?); - Ok(()) - } - - /// Parses `function name($param, ...) { ... }` inside a dynamic eval class. - fn parse_class_method_decl(&mut self) -> Result { - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - self.expect(TokenKind::LParen)?; - let params = self.parse_function_params()?; - let body = self.parse_block()?; - Ok(EvalClassMethod::new(name, params, body)) - } - - /// Parses one public property declaration with an optional initializer. - fn parse_class_property_decl(&mut self) -> Result { - self.skip_optional_property_type()?; - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - let default = if self.consume(TokenKind::Equal) { - Some(self.parse_expr()?) - } else { - None - }; - self.expect_semicolon()?; - Ok(EvalClassProperty::new(name, default)) - } - - /// Consumes a simple declared property type before the `$property` token. - fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { - if matches!(self.current(), TokenKind::DollarIdent(_)) { - return Ok(()); - } - if self.consume(TokenKind::Question) && matches!(self.current(), TokenKind::DollarIdent(_)) - { - return Err(EvalParseError::UnexpectedToken); - } - match self.current() { - TokenKind::Ident(_) | TokenKind::Backslash => { - let _ = self.parse_qualified_name()?; - Ok(()) - } - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Parses `function name($param, ...) { ... }` declarations. - fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); - self.expect(TokenKind::LParen)?; - let params = self.parse_function_params()?; - let body = self.parse_block()?; - Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) - } - - /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. - fn parse_namespace_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let namespace = if self.consume(TokenKind::LBrace) { - return self.parse_namespace_block(String::new()); - } else { - self.parse_namespace_name()? - }; - if self.consume_semicolon() { - self.namespace = namespace; - self.imports = NamespaceImports::default(); - return Ok(Vec::new()); - } - self.expect(TokenKind::LBrace)?; - self.parse_namespace_block(namespace) - } - - /// Parses statements inside an already opened namespace block. - fn parse_namespace_block(&mut self, namespace: String) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.namespace, namespace); - let previous_imports = std::mem::take(&mut self.imports); - let previous_allow_use_imports = std::mem::replace(&mut self.allow_use_imports, true); - let result = self.parse_block_contents(); - self.namespace = previous; - self.imports = previous_imports; - self.allow_use_imports = previous_allow_use_imports; - result - } - - /// Parses a namespace declaration name without a leading global separator. - fn parse_namespace_name(&mut self) -> Result { - let name = self.parse_qualified_name()?; - if name.absolute { - return Err(EvalParseError::UnexpectedToken); - } - Ok(name.name) - } - - /// Parses PHP `use`, `use function`, and `use const` import declarations. - fn parse_use_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let kind = self.parse_use_import_kind(); - - loop { - self.parse_use_import(kind)?; - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect_semicolon()?; - Ok(Vec::new()) - } - - /// Parses an optional top-level `function` or `const` use-import kind. - fn parse_use_import_kind(&mut self) -> UseImportKind { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - self.advance(); - UseImportKind::Function - } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - self.advance(); - UseImportKind::Const - } else { - UseImportKind::Class - } - } - - /// Parses and registers one comma-separated import entry. - fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { - let (name, grouped) = self.parse_use_name_or_group_start()?; - if grouped { - return self.parse_grouped_use_imports(kind, name); - } - self.parse_use_alias_and_register(kind, name) - } - - /// Parses a use-import name, stopping after a trailing namespace separator before `{`. - fn parse_use_name_or_group_start(&mut self) -> Result<(String, bool), EvalParseError> { - let _ = self.consume(TokenKind::Backslash); - let TokenKind::Ident(first) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let mut name = first.clone(); - self.advance(); - while self.consume(TokenKind::Backslash) { - if self.consume(TokenKind::LBrace) { - return Ok((name, true)); - } - let TokenKind::Ident(part) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - name.push('\\'); - name.push_str(part); - self.advance(); - } - Ok((name, false)) - } - - /// Parses all members inside a grouped namespace import declaration. - fn parse_grouped_use_imports( - &mut self, - default_kind: UseImportKind, - prefix: String, - ) -> Result<(), EvalParseError> { - if matches!(self.current(), TokenKind::RBrace) { - return Err(EvalParseError::UnexpectedToken); - } - loop { - let kind = self.parse_grouped_use_entry_kind(default_kind)?; - let member = self.parse_grouped_use_member_name()?; - let name = join_grouped_use_name(&prefix, &member); - self.parse_use_alias_and_register(kind, name)?; - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(TokenKind::RBrace) { - return Ok(()); - } - } - self.expect(TokenKind::RBrace) - } - - /// Parses an optional per-entry grouped import kind, matching PHP's mixed group rules. - fn parse_grouped_use_entry_kind( - &mut self, - default_kind: UseImportKind, - ) -> Result { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if default_kind != UseImportKind::Class { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - return Ok(UseImportKind::Function); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if default_kind != UseImportKind::Class { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - return Ok(UseImportKind::Const); - } - Ok(default_kind) - } - - /// Parses one non-absolute member name inside a grouped use declaration. - fn parse_grouped_use_member_name(&mut self) -> Result { - let name = self.parse_qualified_name()?; - if name.absolute { - return Err(EvalParseError::UnexpectedToken); - } - Ok(name.name) - } - - /// Parses an optional alias and stores one namespace import. - fn parse_use_alias_and_register( - &mut self, - kind: UseImportKind, - name: String, - ) -> Result<(), EvalParseError> { - let alias = if matches!( - self.current(), - TokenKind::Ident(keyword) if ident_eq(keyword, "as") - ) { - self.advance(); - let TokenKind::Ident(alias) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let alias = alias.clone(); - self.advance(); - alias - } else { - last_name_segment(&name).to_string() - }; - - match kind { - UseImportKind::Class => self.imports.insert_class(alias, name), - UseImportKind::Function => self.imports.insert_function(alias, name), - UseImportKind::Const => self.imports.insert_constant(alias, name), - } - Ok(()) - } - - /// Parses `global $name, $other;` declarations in eval fragments. - fn parse_global_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let mut vars = Vec::new(); - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - vars.push(name.clone()); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect_semicolon()?; - Ok(vec![EvalStmt::Global { vars }]) - } - - /// Parses `static $name = expr;` declarations in eval fragments. - fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let name = name.clone(); - self.advance(); - self.expect(TokenKind::Equal)?; - let init = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::StaticVar { name, init }]) - } - - /// Parses `throw expr;` statements in eval fragments. - fn parse_throw_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let expr = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::Throw(expr)]) - } - - /// Parses `try { ... } catch (Type|Other $name) { ... } finally { ... }` statements. - fn parse_try_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let body = self.parse_block()?; - let mut catches = Vec::new(); - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { - catches.push(self.parse_catch_clause()?); - } - let finally_body = - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) { - self.advance(); - self.parse_block()? - } else { - Vec::new() - }; - if catches.is_empty() && finally_body.is_empty() { - return Err(EvalParseError::UnexpectedToken); - } - Ok(vec![EvalStmt::Try { - body, - catches, - finally_body, - }]) - } - - /// Parses one `catch (ClassName|Other [$name]) { ... }` clause. - fn parse_catch_clause(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - let class_names = self.parse_catch_types()?; - let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { - let var_name = var_name.clone(); - self.advance(); - Some(var_name) - } else { - None - }; - self.expect(TokenKind::RParen)?; - let body = self.parse_block()?; - Ok(EvalCatch { - class_names, - var_name, - body, - }) - } - - /// Parses one or more unioned catch types in source order. - fn parse_catch_types(&mut self) -> Result, EvalParseError> { - let class_name = self.parse_qualified_name()?; - let mut class_names = vec![self.resolve_class_name(class_name)]; - while self.consume(TokenKind::Pipe) { - let class_name = self.parse_qualified_name()?; - class_names.push(self.resolve_class_name(class_name)); - } - Ok(class_names) - } - - /// Parses a dynamic function declaration parameter list after `(`. - fn parse_function_params(&mut self) -> Result, EvalParseError> { - let mut params = Vec::new(); - if self.consume(TokenKind::RParen) { - return Ok(params); - } - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - params.push(name.clone()); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - if matches!(self.current(), TokenKind::RParen) { - return Err(EvalParseError::ExpectedVariable); - } - } - self.expect(TokenKind::RParen)?; - Ok(params) - } - - /// Parses the optional first clause of a `for` loop. - fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::Semicolon) { - return Ok(Vec::new()); - } - self.parse_for_clause_stmt() - } - - /// Parses the optional update clause of a `for` loop. - fn parse_for_update_clause(&mut self) -> Result, EvalParseError> { - if self.consume(TokenKind::RParen) { - return Ok(Vec::new()); - } - let statements = self.parse_for_clause_stmt()?; - self.expect(TokenKind::RParen)?; - Ok(statements) - } - - /// Parses one statement-like `for` clause without consuming a delimiter. - fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { - match self.current() { - TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(false), - TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { - self.parse_array_set_clause(name.clone()) - } - TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { - self.parse_property_stmt(false) - } - TokenKind::DollarIdent(name) - if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => - { - self.parse_postfix_inc_dec_stmt(name.clone(), false) - } - TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { - let name = name.clone(); - self.parse_var_store_stmt(name, false) - } - _ => { - let expr = self.parse_expr()?; - Ok(vec![EvalStmt::Expr(expr)]) - } - } - } - - /// Parses `$name[index] = expr` and `$name[] = expr` in a `for` clause. - fn parse_array_set_clause(&mut self, name: String) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LBracket)?; - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) - } - - /// Parses `$name = expr` and simple variable compound assignments. - fn parse_var_store_stmt( - &mut self, - name: String, - require_semicolon: bool, - ) -> Result, EvalParseError> { - self.advance(); - let Some(op) = assignment_op(self.current()) else { - return Err(EvalParseError::UnexpectedToken); - }; - self.advance(); - if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { - self.advance(); - let TokenKind::DollarIdent(source) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let source = source.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::ReferenceAssign { - target: name, - source, - }]); - } - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - let value = assignment_value(&name, op, value); - Ok(vec![EvalStmt::StoreVar { name, value }]) - } - - /// Parses prefix `++$name` and `--$name` as simple statement effects. - fn parse_prefix_inc_dec_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let name = name.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![inc_dec_store(name, increment)]) - } - - /// Parses postfix `$name++` and `$name--` as simple statement effects. - fn parse_postfix_inc_dec_stmt( - &mut self, - name: String, - require_semicolon: bool, - ) -> Result, EvalParseError> { - self.advance(); - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![inc_dec_store(name, increment)]) - } - - /// Parses `$object->property` as either an expression statement or property write. - fn parse_property_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let target = self.parse_expr()?; - if !self.consume(TokenKind::Equal) { - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::Expr(target)]); - } - let EvalExpr::PropertyGet { object, property } = target else { - return Err(EvalParseError::UnexpectedToken); - }; - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![EvalStmt::PropertySet { - object: *object, - property, - value, - }]) - } - - /// Parses a complete `if` statement after consuming the `if` keyword. - fn parse_if_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - Ok(vec![self.parse_if_after_keyword()?]) - } - - /// Parses the condition, then block, and optional else branch for an `if` chain. - fn parse_if_after_keyword(&mut self) -> Result { - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - let then_branch = self.parse_statement_body()?; - let else_branch = self.parse_optional_else_branch()?; - Ok(EvalStmt::If { - condition, - then_branch, - else_branch, - }) - } - - /// Parses `elseif`, `else if`, or `else` branches after an `if` body. - fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { - self.advance(); - return Ok(vec![self.parse_if_after_keyword()?]); - } - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "else")) { - return Ok(Vec::new()); - } - self.advance(); - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "if")) { - self.advance(); - Ok(vec![self.parse_if_after_keyword()?]) - } else { - self.parse_statement_body() - } - } - - /// Parses `switch (expr) { case expr: ... default: ... }`. - fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let expr = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect(TokenKind::LBrace)?; - let mut cases = Vec::new(); - while !matches!(self.current(), TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - cases.push(self.parse_switch_case()?); - } - self.expect(TokenKind::RBrace)?; - Ok(vec![EvalStmt::Switch { expr, cases }]) - } - - /// Parses one `case` or `default` arm inside a switch body. - fn parse_switch_case(&mut self) -> Result { - let condition = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) - { - self.advance(); - let condition = self.parse_expr()?; - Some(condition) - } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { - self.advance(); - None - } else { - return Err(EvalParseError::UnexpectedToken); - }; - self.expect(TokenKind::Colon)?; - let body = self.parse_switch_case_body()?; - Ok(EvalSwitchCase { condition, body }) - } - - /// Parses case body statements until the next case boundary or switch close. - fn parse_switch_case_body(&mut self) -> Result, EvalParseError> { - let mut body = Vec::new(); - while !is_switch_case_boundary(self.current()) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - body.extend(self.parse_stmt()?); - } - Ok(body) - } - - /// Parses `unset($name[, ...]);`. - fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let mut statements = Vec::new(); - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - statements.push(EvalStmt::UnsetVar { name: name.clone() }); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect(TokenKind::RParen)?; - self.expect_semicolon()?; - Ok(statements) - } - - /// Parses `while (expr) { ... }`. - fn parse_while_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::While { condition, body }]) - } - - /// Parses either a brace-delimited block or one braceless statement body. - fn parse_statement_body(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::LBrace) { - self.parse_block() - } else { - self.parse_nested_stmt() - } - } - - /// Parses a brace-delimited statement block. - fn parse_block(&mut self) -> Result, EvalParseError> { - self.expect(TokenKind::LBrace)?; - self.parse_nested_block_contents() - } - - /// Parses one nested statement where import declarations are not legal. - fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.allow_use_imports, false); - let result = self.parse_stmt(); - self.allow_use_imports = previous; - result - } - - /// Parses a nested block while preserving active imports for name resolution. - fn parse_nested_block_contents(&mut self) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.allow_use_imports, false); - let result = self.parse_block_contents(); - self.allow_use_imports = previous; - result - } - - /// Parses statements until the closing brace for the current block. - fn parse_block_contents(&mut self) -> Result, EvalParseError> { - let mut statements = Vec::new(); - while !matches!(self.current(), TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - statements.extend(self.parse_stmt()?); - } - self.expect(TokenKind::RBrace)?; - Ok(statements) - } - - /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. - fn parse_expr(&mut self) -> Result { - self.parse_keyword_or() - } - - /// Parses PHP keyword `or`, whose precedence is lower than `xor`, `and`, and ternary. - fn parse_keyword_or(&mut self) -> Result { - let mut expr = self.parse_keyword_xor()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "or")) { - self.advance(); - let right = self.parse_keyword_xor()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP keyword `xor`, whose operands are evaluated before boolean XOR. - fn parse_keyword_xor(&mut self) -> Result { - let mut expr = self.parse_keyword_and()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "xor")) { - self.advance(); - let right = self.parse_keyword_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalXor, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP keyword `and`, whose precedence is lower than ternary and `&&`. - fn parse_keyword_and(&mut self) -> Result { - let mut expr = self.parse_ternary()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "and")) { - self.advance(); - let right = self.parse_ternary()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. - fn parse_ternary(&mut self) -> Result { - let condition = self.parse_null_coalesce()?; - if !self.consume(TokenKind::Question) { - return Ok(condition); - } - let then_branch = if self.consume(TokenKind::Colon) { - None - } else { - let expr = self.parse_expr()?; - self.expect(TokenKind::Colon)?; - Some(Box::new(expr)) - }; - let else_branch = self.parse_expr()?; - Ok(EvalExpr::Ternary { - condition: Box::new(condition), - then_branch, - else_branch: Box::new(else_branch), - }) - } - - /// Parses right-associative null coalescing below logical OR and above ternary. - fn parse_null_coalesce(&mut self) -> Result { - let value = self.parse_logical_or()?; - if !self.consume(TokenKind::QuestionQuestion) { - return Ok(value); - } - let default = self.parse_null_coalesce()?; - Ok(EvalExpr::NullCoalesce { - value: Box::new(value), - default: Box::new(default), - }) - } - - /// Parses left-associative logical OR with lower precedence than logical AND. - fn parse_logical_or(&mut self) -> Result { - let mut expr = self.parse_logical_and()?; - while self.consume(TokenKind::OrOr) { - let right = self.parse_logical_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative logical AND with lower precedence than equality. - fn parse_logical_and(&mut self) -> Result { - let mut expr = self.parse_bit_or()?; - while self.consume(TokenKind::AndAnd) { - let right = self.parse_bit_or()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise OR with lower precedence than bitwise XOR. - fn parse_bit_or(&mut self) -> Result { - let mut expr = self.parse_bit_xor()?; - while self.consume(TokenKind::Pipe) { - let right = self.parse_bit_xor()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise XOR with lower precedence than bitwise AND. - fn parse_bit_xor(&mut self) -> Result { - let mut expr = self.parse_bit_and()?; - while self.consume(TokenKind::Caret) { - let right = self.parse_bit_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitXor, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise AND with lower precedence than equality. - fn parse_bit_and(&mut self) -> Result { - let mut expr = self.parse_equality()?; - while self.consume(TokenKind::Ampersand) { - let right = self.parse_equality()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative equality and inequality comparisons. - fn parse_equality(&mut self) -> Result { - let mut expr = self.parse_ordering()?; - loop { - let op = if self.consume(TokenKind::EqualEqual) { - EvalBinOp::LooseEq - } else if self.consume(TokenKind::NotEqual) { - EvalBinOp::LooseNotEq - } else if self.consume(TokenKind::EqualEqualEqual) { - EvalBinOp::StrictEq - } else if self.consume(TokenKind::NotEqualEqual) { - EvalBinOp::StrictNotEq - } else { - break; - }; - let right = self.parse_ordering()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative ordered comparisons. - fn parse_ordering(&mut self) -> Result { - let mut expr = self.parse_shift()?; - loop { - let op = if self.consume(TokenKind::Less) { - EvalBinOp::Lt - } else if self.consume(TokenKind::LessEqual) { - EvalBinOp::LtEq - } else if self.consume(TokenKind::Greater) { - EvalBinOp::Gt - } else if self.consume(TokenKind::GreaterEqual) { - EvalBinOp::GtEq - } else if self.consume(TokenKind::Spaceship) { - EvalBinOp::Spaceship - } else { - break; - }; - let right = self.parse_shift()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative integer shift operators. - fn parse_shift(&mut self) -> Result { - let mut expr = self.parse_concat()?; - loop { - let op = if self.consume(TokenKind::LessLess) { - EvalBinOp::ShiftLeft - } else if self.consume(TokenKind::GreaterGreater) { - EvalBinOp::ShiftRight - } else { - break; - }; - let right = self.parse_concat()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative string concatenation. - fn parse_concat(&mut self) -> Result { - let mut expr = self.parse_add()?; - while self.consume(TokenKind::Dot) { - let right = self.parse_add()?; - expr = EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative numeric addition and subtraction. - fn parse_add(&mut self) -> Result { - let mut expr = self.parse_mul()?; - loop { - let op = if self.consume(TokenKind::Plus) { - EvalBinOp::Add - } else if self.consume(TokenKind::Minus) { - EvalBinOp::Sub - } else { - break; - }; - let right = self.parse_mul()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative numeric multiplication, division, and modulo. - fn parse_mul(&mut self) -> Result { - let mut expr = self.parse_unary()?; - loop { - let op = if self.consume(TokenKind::Star) { - EvalBinOp::Mul - } else if self.consume(TokenKind::Slash) { - EvalBinOp::Div - } else if self.consume(TokenKind::Percent) { - EvalBinOp::Mod - } else { - break; - }; - let right = self.parse_unary()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses right-associative unary prefix expressions. - fn parse_unary(&mut self) -> Result { - if self.consume(TokenKind::Plus) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::Plus, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Minus) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Bang) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::LogicalNot, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Tilde) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::BitNot, - expr: Box::new(expr), - }); - } - self.parse_power() - } - - /// Parses right-associative exponentiation with higher precedence than unary prefix operators. - fn parse_power(&mut self) -> Result { - let mut expr = self.parse_postfix()?; - if self.consume(TokenKind::StarStar) { - let right = self.parse_unary()?; - expr = EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses postfix array reads, property reads, method calls, and dynamic calls. - fn parse_postfix(&mut self) -> Result { - let mut expr = self.parse_primary()?; - loop { - if matches!(self.current(), TokenKind::LParen) { - let args = self.parse_call_args()?; - expr = EvalExpr::DynamicCall { - callee: Box::new(expr), - args, - }; - continue; - } - if self.consume(TokenKind::LBracket) { - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - expr = EvalExpr::ArrayGet { - array: Box::new(expr), - index: Box::new(index), - }; - continue; - } - if self.consume(TokenKind::Arrow) { - let TokenKind::Ident(member) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let member = member.clone(); - self.advance(); - if matches!(self.current(), TokenKind::LParen) { - let args = self.parse_call_args()?; - expr = EvalExpr::MethodCall { - object: Box::new(expr), - method: member.to_ascii_lowercase(), - args, - }; - } else { - expr = EvalExpr::PropertyGet { - object: Box::new(expr), - property: member, - }; - } - continue; - } - break; - } - Ok(expr) - } - - /// Parses primary expressions supported by the initial eval subset. - fn parse_primary(&mut self) -> Result { - match self.current() { - TokenKind::Int(value) => { - let value = *value; - self.advance(); - Ok(EvalExpr::Const(EvalConst::Int(value))) - } - TokenKind::Float(value) => { - let value = *value; - self.advance(); - Ok(EvalExpr::Const(EvalConst::Float(value))) - } - TokenKind::String(value) => { - let value = value.clone(); - self.advance(); - Ok(EvalExpr::Const(EvalConst::String(value))) - } - TokenKind::DollarIdent(name) => { - let name = name.clone(); - self.advance(); - Ok(EvalExpr::LoadVar(name)) - } - TokenKind::Magic(EvalMagicConst::Namespace) => { - let namespace = self.namespace.clone(); - self.advance(); - Ok(EvalExpr::Const(EvalConst::String(namespace))) - } - TokenKind::Magic(magic) => { - let magic = magic.clone(); - self.advance(); - Ok(EvalExpr::Magic(magic)) - } - TokenKind::Ident(name) if ident_eq(name, "null") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Null)) - } - TokenKind::Ident(name) if ident_eq(name, "true") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Bool(true))) - } - TokenKind::Ident(name) if ident_eq(name, "false") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Bool(false))) - } - TokenKind::Ident(name) if ident_eq(name, "print") => { - self.advance(); - let expr = self.parse_expr()?; - Ok(EvalExpr::Print(Box::new(expr))) - } - TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { - self.parse_legacy_array_literal() - } - TokenKind::Ident(name) if is_include_construct_name(name) => self.parse_include_expr(), - TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), - TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), - TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { - Err(EvalParseError::UnsupportedConstruct) - } - TokenKind::Backslash => self.parse_qualified_name_expr(), - TokenKind::Ident(_) if matches!(self.peek(), TokenKind::Backslash) => { - self.parse_qualified_name_expr() - } - TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { - self.parse_call_expr(name.clone()) - } - TokenKind::Ident(name) => { - let name = name.clone(); - self.advance(); - Ok(self.const_fetch_expr(name)) - } - TokenKind::LBracket => self.parse_array_literal(), - TokenKind::LParen => { - self.advance(); - let expr = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - Ok(expr) - } - TokenKind::Eof => Err(EvalParseError::UnexpectedEof), - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Parses PHP include/require expression constructs and their path expression. - fn parse_include_expr(&mut self) -> Result { - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let required = ident_eq(name, "require") || ident_eq(name, "require_once"); - let once = ident_eq(name, "include_once") || ident_eq(name, "require_once"); - self.advance(); - let path = if self.consume(TokenKind::LParen) { - let path = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - path - } else { - self.parse_expr()? - }; - Ok(EvalExpr::Include { - path: Box::new(path), - required, - once, - }) - } - - /// Parses `match (expr) { pattern, other => value, default => fallback }`. - fn parse_match_expr(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - let subject = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect(TokenKind::LBrace)?; - - let mut arms = Vec::new(); - let mut default = None; - while !self.consume(TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { - self.advance(); - self.expect(TokenKind::FatArrow)?; - default = Some(Box::new(self.parse_expr()?)); - } else { - arms.push(self.parse_match_arm()?); - } - if self.consume(TokenKind::Comma) { - continue; - } - self.expect(TokenKind::RBrace)?; - break; - } - - Ok(EvalExpr::Match { - subject: Box::new(subject), - arms, - default, - }) - } - - /// Parses one non-default `match` arm and its comma-separated pattern list. - fn parse_match_arm(&mut self) -> Result { - let mut patterns = Vec::new(); - loop { - patterns.push(self.parse_expr()?); - if !self.consume(TokenKind::Comma) { - break; - } - if matches!(self.current(), TokenKind::FatArrow) { - return Err(EvalParseError::UnexpectedToken); - } - if matches!(self.current(), TokenKind::Eof | TokenKind::RBrace) { - return Err(EvalParseError::UnexpectedToken); - } - } - self.expect(TokenKind::FatArrow)?; - let value = self.parse_expr()?; - Ok(EvalMatchArm { patterns, value }) - } - - /// Parses a function-like call expression and its source-order arguments. - fn parse_call_expr(&mut self, name: String) -> Result { - self.advance(); - let args = self.parse_call_args()?; - Ok(self.call_expr(name, args)) - } - - /// Parses an explicitly qualified call or constant-fetch expression. - fn parse_qualified_name_expr(&mut self) -> Result { - let name = self.parse_qualified_name()?; - let name = self.resolve_qualified_name(name); - if matches!(self.current(), TokenKind::LParen) { - let args = self.parse_call_args()?; - return Ok(EvalExpr::Call { - name: name.to_ascii_lowercase(), - args, - }); - } - Ok(EvalExpr::ConstFetch(name)) - } - - /// Parses `new ClassName(...)` expressions in eval fragments. - fn parse_new_object_expr(&mut self) -> Result { - self.advance(); - let class_name = self.parse_qualified_name()?; - let class_name = self.resolve_class_name(class_name); - let args = self.parse_call_args()?; - Ok(EvalExpr::NewObject { class_name, args }) - } - - /// Parses a simple or explicitly qualified PHP name. - fn parse_qualified_name(&mut self) -> Result { - let absolute = self.consume(TokenKind::Backslash); - let TokenKind::Ident(first) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let mut name = first.clone(); - self.advance(); - while self.consume(TokenKind::Backslash) { - let TokenKind::Ident(part) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - name.push('\\'); - name.push_str(part); - self.advance(); - } - Ok(ParsedQualifiedName { name, absolute }) - } - - /// Builds a call expression, adding namespace fallback for unqualified names. - fn call_expr(&self, name: String, args: Vec) -> EvalExpr { - if let Some(imported) = self.imports.resolve_function(&name) { - return EvalExpr::Call { - name: imported.to_ascii_lowercase(), - args, - }; - } - let fallback_name = name.to_ascii_lowercase(); - if self.namespace.is_empty() { - EvalExpr::Call { - name: fallback_name, - args, - } - } else { - EvalExpr::NamespacedCall { - name: self - .qualify_name_in_current_namespace(&name) - .to_ascii_lowercase(), - fallback_name, - args, - } - } - } - - /// Builds a constant fetch expression, adding namespace fallback for unqualified names. - fn const_fetch_expr(&self, name: String) -> EvalExpr { - if let Some(imported) = self.imports.resolve_constant(&name) { - return EvalExpr::ConstFetch(imported.to_string()); - } - if self.namespace.is_empty() { - EvalExpr::ConstFetch(name) - } else { - EvalExpr::NamespacedConstFetch { - name: self.qualify_name_in_current_namespace(&name), - fallback_name: name, - } - } - } - - /// Prefixes a name with the parser's current namespace when one is active. - fn qualify_name_in_current_namespace(&self, name: &str) -> String { - if self.namespace.is_empty() { - name.to_string() - } else { - format!("{}\\{}", self.namespace, name) - } - } - - /// Resolves a class name through active imports before namespace qualification. - fn resolve_class_name(&self, name: ParsedQualifiedName) -> String { - if name.absolute { - return name.name; - } - if let Some(imported) = self.imports.resolve_class(&name.name) { - return imported; - } - self.resolve_qualified_name(name) - } - - /// Resolves a parsed PHP name according to the current namespace. - fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { - if name.absolute || self.namespace.is_empty() { - name.name - } else { - self.qualify_name_in_current_namespace(&name.name) - } - } - - /// Parses a parenthesized source-order argument list. - fn parse_call_args(&mut self) -> Result, EvalParseError> { - self.expect(TokenKind::LParen)?; - let mut args = Vec::new(); - if self.consume(TokenKind::RParen) { - return Ok(args); - } - loop { - args.push(self.parse_call_arg()?); - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(TokenKind::RParen) { - return Ok(args); - } - } - self.expect(TokenKind::RParen)?; - Ok(args) - } - - /// Parses one positional or named argument within a call argument list. - fn parse_call_arg(&mut self) -> Result { - if self.consume(TokenKind::Ellipsis) { - return self.parse_expr().map(EvalCallArg::spread); - } - if matches!(self.peek(), TokenKind::Colon) { - if let TokenKind::Ident(name) = self.current() { - let name = name.clone(); - self.advance(); - self.expect(TokenKind::Colon)?; - let value = self.parse_expr()?; - return Ok(EvalCallArg::named(name, value)); - } - } - self.parse_expr().map(EvalCallArg::positional) - } - - /// Parses an array literal with source-order optional key/value element expressions. - fn parse_array_literal(&mut self) -> Result { - self.expect(TokenKind::LBracket)?; - self.parse_array_elements_until(TokenKind::RBracket) - } - - /// Parses PHP's legacy `array(...)` literal into the same EvalIR node as `[...]`. - fn parse_legacy_array_literal(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - self.parse_array_elements_until(TokenKind::RParen) - } - - /// Returns whether the current token starts PHP's legacy `array(...)` literal syntax. - fn current_starts_legacy_array_literal(&self) -> bool { - matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "array")) - && matches!(self.peek(), TokenKind::LParen) - } - - /// Parses comma-separated array elements until the supplied closing delimiter. - fn parse_array_elements_until(&mut self, close: TokenKind) -> Result { - let mut elements = Vec::new(); - if self.consume(close.clone()) { - return Ok(EvalExpr::Array(elements)); - } - loop { - let first = self.parse_expr()?; - if self.consume(TokenKind::FatArrow) { - let value = self.parse_expr()?; - elements.push(EvalArrayElement::KeyValue { key: first, value }); - } else { - elements.push(EvalArrayElement::Value(first)); - } - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(close.clone()) { - return Ok(EvalExpr::Array(elements)); - } - } - self.expect(close)?; - Ok(EvalExpr::Array(elements)) - } - - /// Consumes `expected` or returns a parse error. - fn expect(&mut self, expected: TokenKind) -> Result<(), EvalParseError> { - if self.consume(expected) { - Ok(()) - } else { - Err(EvalParseError::UnexpectedToken) - } - } - - /// Consumes a semicolon or returns the semicolon-specific parse error. - fn expect_semicolon(&mut self) -> Result<(), EvalParseError> { - if self.consume_semicolon() { - Ok(()) - } else { - Err(EvalParseError::ExpectedSemicolon) - } - } - - /// Consumes a semicolon if present. - fn consume_semicolon(&mut self) -> bool { - self.consume(TokenKind::Semicolon) - } - - /// Consumes `expected` if the current token matches it. - fn consume(&mut self, expected: TokenKind) -> bool { - if *self.current() == expected { - self.advance(); - true - } else { - false - } - } - - /// Returns the current token. - fn current(&self) -> &TokenKind { - self.tokens.get(self.pos).unwrap_or(&TokenKind::Eof) - } - - /// Returns the next token without advancing. - fn peek(&self) -> &TokenKind { - self.tokens.get(self.pos + 1).unwrap_or(&TokenKind::Eof) - } - - /// Advances to the next token. - fn advance(&mut self) { - if self.pos < self.tokens.len() { - self.pos += 1; - } - } -} - -/// Returns true for the first character of a PHP variable/function identifier. -fn is_ident_start(ch: char) -> bool { - ch == '_' || ch.is_ascii_alphabetic() -} - -/// Returns true for subsequent characters in a PHP variable/function identifier. -fn is_ident_continue(ch: char) -> bool { - is_ident_start(ch) || ch.is_ascii_digit() -} - -/// Converts a PHP magic-constant identifier into a parser token when recognized. -fn magic_const_token(name: &str, line: i64) -> Option { - let magic = if ident_eq(name, "__FILE__") { - EvalMagicConst::File - } else if ident_eq(name, "__DIR__") { - EvalMagicConst::Dir - } else if ident_eq(name, "__LINE__") { - EvalMagicConst::Line(line) - } else if ident_eq(name, "__FUNCTION__") { - EvalMagicConst::Function - } else if ident_eq(name, "__CLASS__") { - EvalMagicConst::Class - } else if ident_eq(name, "__METHOD__") { - EvalMagicConst::Method - } else if ident_eq(name, "__NAMESPACE__") { - EvalMagicConst::Namespace - } else if ident_eq(name, "__TRAIT__") { - EvalMagicConst::Trait - } else { - return None; - }; - Some(TokenKind::Magic(magic)) -} - -/// Returns true when the current token closes or starts a switch case arm. -fn is_switch_case_boundary(token: &TokenKind) -> bool { - matches!(token, TokenKind::RBrace) - || matches!(token, TokenKind::Ident(name) if ident_eq(name, "case") || ident_eq(name, "default")) -} - -/// Maps simple variable assignment tokens to an optional compound EvalIR operator. -fn assignment_op(token: &TokenKind) -> Option> { - match token { - TokenKind::Equal => Some(None), - TokenKind::PlusEqual => Some(Some(EvalBinOp::Add)), - TokenKind::MinusEqual => Some(Some(EvalBinOp::Sub)), - TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), - TokenKind::StarStarEqual => Some(Some(EvalBinOp::Pow)), - TokenKind::SlashEqual => Some(Some(EvalBinOp::Div)), - TokenKind::PercentEqual => Some(Some(EvalBinOp::Mod)), - TokenKind::AmpEqual => Some(Some(EvalBinOp::BitAnd)), - TokenKind::PipeEqual => Some(Some(EvalBinOp::BitOr)), - TokenKind::CaretEqual => Some(Some(EvalBinOp::BitXor)), - TokenKind::LessLessEqual => Some(Some(EvalBinOp::ShiftLeft)), - TokenKind::GreaterGreaterEqual => Some(Some(EvalBinOp::ShiftRight)), - TokenKind::DotEqual => Some(Some(EvalBinOp::Concat)), - _ => None, - } -} - -/// Builds the assigned value expression for plain and compound variable assignment. -fn assignment_value(name: &str, op: Option, value: EvalExpr) -> EvalExpr { - match op { - Some(op) => EvalExpr::Binary { - op, - left: Box::new(EvalExpr::LoadVar(name.to_string())), - right: Box::new(value), - }, - None => value, - } -} - -/// Builds the StoreVar statement for a simple variable increment or decrement. -fn inc_dec_store(name: String, increment: bool) -> EvalStmt { - EvalStmt::StoreVar { - value: EvalExpr::Binary { - op: if increment { - EvalBinOp::Add - } else { - EvalBinOp::Sub - }, - left: Box::new(EvalExpr::LoadVar(name.clone())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - name, - } -} - -/// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. -fn ident_eq(actual: &str, expected: &str) -> bool { - actual.eq_ignore_ascii_case(expected) -} - -/// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. -fn is_unsupported_statement_keyword(name: &str) -> bool { - ["enum", "interface", "trait"] - .iter() - .any(|keyword| ident_eq(name, keyword)) -} - -/// Returns true for class member modifiers outside the current eval class subset. -fn is_unsupported_class_member_modifier(name: &str) -> bool { - ["private", "protected", "static", "abstract", "final"] - .iter() - .any(|modifier| ident_eq(name, modifier)) -} - -/// Returns true when an identifier is an include/require expression construct. -fn is_include_construct_name(name: &str) -> bool { - ["include", "include_once", "require", "require_once"] - .iter() - .any(|keyword| ident_eq(name, keyword)) -} - -/// Returns the first namespace segment and the optional remaining suffix. -fn split_first_name_segment(name: &str) -> (&str, Option<&str>) { - name.split_once('\\') - .map_or((name, None), |(first, tail)| (first, Some(tail))) -} - -/// Returns the final segment of a PHP qualified name. -fn last_name_segment(name: &str) -> &str { - name.rsplit('\\').next().unwrap_or(name) -} - -/// Combines a grouped use prefix with one relative member name. -fn join_grouped_use_name(prefix: &str, member: &str) -> String { - format!("{prefix}\\{member}") -} - -/// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. -fn is_unsupported_expression_keyword(name: &str) -> bool { - ["clone", "yield"] - .iter() - .any(|keyword| ident_eq(name, keyword)) -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Verifies assignment fragments lower to by-name StoreVar statements. - #[test] - fn parse_fragment_accepts_assignment_source() { - let program = parse_fragment(b"$x = 1;").expect("fragment should parse"); - assert_eq!(program.source_len(), 7); - assert_eq!( - program.statements(), - &[EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::Int(1)), - }] - ); - } - - /// Verifies reference assignments lower to by-name ReferenceAssign statements. - #[test] - fn parse_fragment_accepts_reference_assignment_source() { - let program = parse_fragment(b"$left =& $right;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ReferenceAssign { - target: "left".to_string(), - source: "right".to_string(), - }] - ); - } - - /// Verifies multiplicative operators preserve PHP precedence and associativity. - #[test] - fn parse_fragment_accepts_division_and_modulo_source() { - let program = parse_fragment(b"return 10 / 4 % 3;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Mod, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Div, - left: Box::new(EvalExpr::Const(EvalConst::Int(10))), - right: Box::new(EvalExpr::Const(EvalConst::Int(4))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }))] - ); - } - - /// Verifies exponentiation is right-associative and binds tighter than unary negation. - #[test] - fn parse_fragment_accepts_power_source() { - let program = - parse_fragment(b"return -2 ** 2; return 2 ** 3 ** 2;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::Return(Some(EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr: Box::new(EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(EvalExpr::Const(EvalConst::Int(2))), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }), - })), - EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(EvalExpr::Const(EvalConst::Int(2))), - right: Box::new(EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(EvalExpr::Const(EvalConst::Int(3))), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }), - })), - ] - ); - } - - /// Verifies bitwise operators preserve PHP precedence. - #[test] - fn parse_fragment_accepts_bitwise_source() { - let program = parse_fragment(b"return ~0 | 2 ^ 3 & 4;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::BitOr, - left: Box::new(EvalExpr::Unary { - op: EvalUnaryOp::BitNot, - expr: Box::new(EvalExpr::Const(EvalConst::Int(0))), - }), - right: Box::new(EvalExpr::Binary { - op: EvalBinOp::BitXor, - left: Box::new(EvalExpr::Const(EvalConst::Int(2))), - right: Box::new(EvalExpr::Binary { - op: EvalBinOp::BitAnd, - left: Box::new(EvalExpr::Const(EvalConst::Int(3))), - right: Box::new(EvalExpr::Const(EvalConst::Int(4))), - }), - }), - }))] - ); - } - - /// Verifies shift operators bind lower than additive expressions. - #[test] - fn parse_fragment_accepts_shift_source() { - let program = parse_fragment(b"return 1 + 2 << 3;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::ShiftLeft, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::Const(EvalConst::Int(1))), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }))] - ); - } - - /// Verifies simple variable compound assignments lower to StoreVar with binary expressions. - #[test] - fn parse_fragment_accepts_compound_assignment_source() { - let program = - parse_fragment(br#"$x += 2; $x -= 1; $x *= 3; $x /= 2; $x %= 5; $s .= "ok";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Sub, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Mul, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Div, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Mod, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(5))), - }, - }, - EvalStmt::StoreVar { - name: "s".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::LoadVar("s".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::String("ok".to_string()))), - }, - }, - ] - ); - } - - /// Verifies exponentiation compound assignment lowers through the binary power operator. - #[test] - fn parse_fragment_accepts_power_compound_assignment_source() { - let program = parse_fragment(br#"$x **= 3;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }, - }] - ); - } - - /// Verifies bitwise compound assignments lower to StoreVar with binary expressions. - #[test] - fn parse_fragment_accepts_bitwise_compound_assignment_source() { - let program = parse_fragment(br#"$x &= 3; $x |= 1; $x ^= 2; $x <<= 4; $x >>= 1;"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::BitAnd, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::BitOr, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::BitXor, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::ShiftLeft, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(4))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::ShiftRight, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }, - ] - ); - } - - /// Verifies simple variable increment and decrement statements lower to StoreVar. - #[test] - fn parse_fragment_accepts_inc_dec_statement_source() { - let program = parse_fragment(br#"$i++; ++$j; $k--; --$m;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - inc_dec_store("i".to_string(), true), - inc_dec_store("j".to_string(), true), - inc_dec_store("k".to_string(), false), - inc_dec_store("m".to_string(), false), - ] - ); - } - - /// Verifies echo fragments preserve expression source order. - #[test] - fn parse_fragment_accepts_echo_source() { - let program = parse_fragment(br#"echo "hi" . $name;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Echo(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Const(EvalConst::String("hi".to_string()))), - right: Box::new(EvalExpr::LoadVar("name".to_string())), - })] - ); - } - - /// Verifies PHP echo comma lists lower to one EvalIR echo statement per expression. - #[test] - fn parse_fragment_accepts_echo_comma_list_source() { - let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::Echo(EvalExpr::Const(EvalConst::String("a".to_string()))), - EvalStmt::Echo(EvalExpr::LoadVar("b".to_string())), - EvalStmt::Echo(EvalExpr::Const(EvalConst::String("c".to_string()))), - ] - ); - } - - /// Verifies if/else fragments lower to branch statements with nested blocks. - #[test] - fn parse_fragment_accepts_if_else_source() { - let program = parse_fragment(br#"if ($flag) { $x = "yes"; } else { $x = "no"; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::If { - condition: EvalExpr::LoadVar("flag".to_string()), - then_branch: vec![EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::String("yes".to_string())), - }], - else_branch: vec![EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::String("no".to_string())), - }], - }] - ); - } - - /// Verifies braceless if/else bodies parse as single-statement branch bodies. - #[test] - fn parse_fragment_accepts_braceless_if_else_source() { - let program = parse_fragment(br#"if ($flag) echo "yes"; else echo "no";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::If { - condition: EvalExpr::LoadVar("flag".to_string()), - then_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "yes".to_string() - )))], - else_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "no".to_string() - )))], - }] - ); - } - - /// Verifies elseif fragments lower to nested if statements in the else branch. - #[test] - fn parse_fragment_accepts_elseif_source() { - let program = parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::If { - condition: EvalExpr::LoadVar("a".to_string()), - then_branch: vec![EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::String("a".to_string())), - }], - else_branch: vec![EvalStmt::If { - condition: EvalExpr::LoadVar("b".to_string()), - then_branch: vec![EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::String("b".to_string())), - }], - else_branch: Vec::new(), - }], - }] - ); - } - - /// Verifies PHP's `else if` spelling follows the same nested branch shape. - #[test] - fn parse_fragment_accepts_else_if_source() { - let program = parse_fragment(br#"if ($a) { $x = "a"; } else if ($b) { $x = "b"; }"#) - .expect("fragment should parse"); - - assert!(matches!( - program.statements(), - [EvalStmt::If { - else_branch, - .. - }] if matches!(else_branch.as_slice(), [EvalStmt::If { .. }]) - )); - } - - /// Verifies for loops lower clauses and body statements separately. - #[test] - fn parse_fragment_accepts_for_source() { - let program = parse_fragment(br#"for ($i = 2; $i; $i = $i - 1) { echo $i; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::For { - init: vec![EvalStmt::StoreVar { - name: "i".to_string(), - value: EvalExpr::Const(EvalConst::Int(2)), - }], - condition: Some(EvalExpr::LoadVar("i".to_string())), - update: vec![EvalStmt::StoreVar { - name: "i".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Sub, - left: Box::new(EvalExpr::LoadVar("i".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }], - body: vec![EvalStmt::Echo(EvalExpr::LoadVar("i".to_string()))], - }] - ); - } - - /// Verifies switch fragments preserve ordered case and default bodies. - #[test] - fn parse_fragment_accepts_switch_source() { - let program = - parse_fragment(br#"switch ($x) { case 1: echo "one"; break; default: echo "other"; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Switch { - expr: EvalExpr::LoadVar("x".to_string()), - cases: vec![ - EvalSwitchCase { - condition: Some(EvalExpr::Const(EvalConst::Int(1))), - body: vec![ - EvalStmt::Echo(EvalExpr::Const(EvalConst::String("one".to_string()))), - EvalStmt::Break, - ], - }, - EvalSwitchCase { - condition: None, - body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "other".to_string() - )))], - }, - ], - }] - ); - } - - /// Verifies value-only foreach loops lower to an array expression, value target, and body. - #[test] - fn parse_fragment_accepts_foreach_source() { - let program = - parse_fragment(br#"foreach ($items as $item) { echo $item; }"#).expect("parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Foreach { - array: EvalExpr::LoadVar("items".to_string()), - key_name: None, - value_name: "item".to_string(), - body: vec![EvalStmt::Echo(EvalExpr::LoadVar("item".to_string()))], - }] - ); - } - - /// Verifies key-value foreach loops preserve both loop target names in EvalIR. - #[test] - fn parse_fragment_accepts_foreach_key_value_source() { - let program = - parse_fragment(br#"foreach ($items as $key => $item) { echo $key . $item; }"#) - .expect("parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Foreach { - array: EvalExpr::LoadVar("items".to_string()), - key_name: Some("key".to_string()), - value_name: "item".to_string(), - body: vec![EvalStmt::Echo(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::LoadVar("key".to_string())), - right: Box::new(EvalExpr::LoadVar("item".to_string())), - })], - }] - ); - } - - /// Verifies dynamic function declarations preserve name, parameters, and body. - #[test] - fn parse_fragment_accepts_function_declaration_source() { - let program = parse_fragment(br#"function dyn($x) { return $x + 1; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::FunctionDecl { - name: "dyn".to_string(), - params: vec!["x".to_string()], - body: vec![EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }))], - }] - ); - } - - /// Verifies semicolon namespace declarations qualify functions and unqualified calls. - #[test] - fn parse_fragment_accepts_semicolon_namespace_source() { - let program = parse_fragment( - br#"namespace Eval\Ns; -function dyn() { return __NAMESPACE__; } -return dyn();"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::FunctionDecl { - name: "Eval\\Ns\\dyn".to_string(), - params: Vec::new(), - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( - "Eval\\Ns".to_string() - ))))], - }, - EvalStmt::Return(Some(EvalExpr::NamespacedCall { - name: "eval\\ns\\dyn".to_string(), - fallback_name: "dyn".to_string(), - args: Vec::new(), - })), - ] - ); - } - - /// Verifies braced namespace declarations restore the previous namespace afterward. - #[test] - fn parse_fragment_accepts_braced_namespace_source() { - let program = parse_fragment( - br#"namespace Eval\Block { - class Box {} - return new Box(); -} -return Box;"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::ClassDecl(EvalClass::new( - "Eval\\Block\\Box", - Vec::new(), - Vec::new() - )), - EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "Eval\\Block\\Box".to_string(), - args: Vec::new(), - })), - EvalStmt::Return(Some(EvalExpr::ConstFetch("Box".to_string()))), - ] - ); - } - - /// Verifies namespace import declarations resolve functions, constants, and class aliases. - #[test] - fn parse_fragment_accepts_namespace_use_imports() { - let program = parse_fragment( - br#"namespace Eval\UseNs; -use function Lib\strlen as Alias; -use const Lib\VALUE as LocalValue; -use Lib\Box as BoxAlias; -return Alias(LocalValue, new BoxAlias\Inner());"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\strlen".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\VALUE".to_string())), - EvalCallArg::positional(EvalExpr::NewObject { - class_name: "Lib\\Box\\Inner".to_string(), - args: Vec::new(), - }), - ], - }))] - ); - } - - /// Verifies grouped namespace imports resolve functions, constants, and class aliases. - #[test] - fn parse_fragment_accepts_grouped_namespace_use_imports() { - let program = parse_fragment( - br#"namespace Eval\UseNs; -use Lib\{Box as BoxAlias, Sub\Thing, function imported_func as Alias}; -use const Lib\{VALUE as LocalValue, OTHER}; -return Alias(LocalValue, OTHER, new BoxAlias\Inner(), new Thing());"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\imported_func".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\VALUE".to_string())), - EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\OTHER".to_string())), - EvalCallArg::positional(EvalExpr::NewObject { - class_name: "Lib\\Box\\Inner".to_string(), - args: Vec::new(), - }), - EvalCallArg::positional(EvalExpr::NewObject { - class_name: "Lib\\Sub\\Thing".to_string(), - args: Vec::new(), - }), - ], - }))] - ); - } - - /// Verifies typed grouped namespace imports reject mixed per-entry kinds. - #[test] - fn parse_fragment_rejects_mixed_kind_typed_grouped_use_imports() { - assert_eq!( - parse_fragment(br#"use function Lib\{target, const VALUE};"#), - Err(EvalParseError::UnexpectedToken) - ); - } - - /// Verifies namespace blocks restore imports when control returns to the outer namespace. - #[test] - fn parse_fragment_restores_use_imports_after_namespace_block() { - let program = parse_fragment( - br#"namespace Eval\Outer; -use function Lib\outer_func; -namespace Eval\Block { - use function Lib\inner_func as alias; - return alias(); -} -return outer_func();"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\inner_func".to_string(), - args: Vec::new(), - })), - EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\outer_func".to_string(), - args: Vec::new(), - })), - ] - ); - } - - /// Verifies imported aliases remain visible while parsing eval-declared function bodies. - #[test] - fn parse_fragment_applies_use_imports_inside_function_body() { - let program = parse_fragment( - br#"namespace Eval\UseNs; -use function Lib\target as alias; -function dyn() { return alias(); }"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::FunctionDecl { - name: "Eval\\UseNs\\dyn".to_string(), - params: Vec::new(), - body: vec![EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\target".to_string(), - args: Vec::new(), - }))], - }] - ); - } - - /// Verifies import declarations are rejected inside eval-declared function bodies. - #[test] - fn parse_fragment_rejects_use_import_inside_function_body() { - assert_eq!( - parse_fragment(br#"function dyn() { use function Lib\target; }"#), - Err(EvalParseError::UnsupportedConstruct) - ); - } - - /// Verifies static local declarations preserve the target name and initializer expression. - #[test] - fn parse_fragment_accepts_static_var_source() { - let program = parse_fragment(br#"static $n = 1 + 1;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::StaticVar { - name: "n".to_string(), - init: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::Const(EvalConst::Int(1))), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }] - ); - } - - /// Verifies global declarations preserve source-order variable names. - #[test] - fn parse_fragment_accepts_global_source() { - let program = parse_fragment(br#"global $left, $right;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Global { - vars: vec!["left".to_string(), "right".to_string()], - }] - ); - } - - /// Verifies eval magic constants lower to explicit EvalIR nodes with fragment line metadata. - #[test] - fn parse_fragment_accepts_magic_constants() { - let program = - parse_fragment(b"\nreturn __line__ . __FUNCTION__;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Magic(EvalMagicConst::Line(2))), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Function)), - }))] - ); - } - - /// Verifies file-dependent eval magic constants lower to EvalIR nodes. - #[test] - fn parse_fragment_accepts_file_magic_constants() { - let program = parse_fragment(b"return __FILE__ . __dir__;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Magic(EvalMagicConst::File)), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Dir)), - }))] - ); - } - - /// Verifies eval scope magic constants lower with namespace resolved at parse time. - #[test] - fn parse_fragment_accepts_scope_magic_constants() { - let program = parse_fragment(b"return __CLASS__ . __NAMESPACE__ . __TRAIT__ . __METHOD__;") - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Magic(EvalMagicConst::Class)), - right: Box::new(EvalExpr::Const(EvalConst::String(String::new()))), - }), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Trait)), - }), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Method)), - }))] - ); - } - - /// Verifies PHP comments are skipped while preserving fragment line numbers. - #[test] - fn parse_fragment_skips_comments_and_preserves_line_metadata() { - let program = parse_fragment(b"// leading\n# hash\n/* block\ncomment */ return __LINE__;") - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Magic( - EvalMagicConst::Line(4) - )))] - ); - } - - /// Verifies unterminated block comments fail before partial EvalIR is returned. - #[test] - fn parse_fragment_rejects_unterminated_block_comment() { - assert_eq!( - parse_fragment(b"/* open").unwrap_err(), - EvalParseError::UnterminatedComment - ); - } - - /// Verifies comparison operators parse with lower precedence than arithmetic. - #[test] - fn parse_fragment_accepts_comparison_source() { - let program = parse_fragment(br#"return $i + 1 < 3;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Lt, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("i".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }))] - ); - } - - /// Verifies the spaceship operator parses at ordered-comparison precedence. - #[test] - fn parse_fragment_accepts_spaceship_source() { - let program = parse_fragment(br#"return $i + 1 <=> 3;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Spaceship, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("i".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }))] - ); - } - - /// Verifies loose equality operators parse as binary EvalIR expressions. - #[test] - fn parse_fragment_accepts_loose_equality_source() { - let program = parse_fragment(br#"return "a" != "b";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LooseNotEq, - left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), - right: Box::new(EvalExpr::Const(EvalConst::String("b".to_string()))), - }))] - ); - } - - /// Verifies strict equality operators parse as distinct EvalIR comparisons. - #[test] - fn parse_fragment_accepts_strict_equality_source() { - let program = parse_fragment(br#"return "10" === "10" && "10" !== 10;"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::StrictEq, - left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), - right: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), - }), - right: Box::new(EvalExpr::Binary { - op: EvalBinOp::StrictNotEq, - left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), - right: Box::new(EvalExpr::Const(EvalConst::Int(10))), - }), - }))] - ); - } - - /// Verifies logical operators parse with `&&` binding tighter than `||`. - #[test] - fn parse_fragment_accepts_short_circuit_logical_source() { - let program = - parse_fragment(br#"return $a && $b || false;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(EvalExpr::LoadVar("a".to_string())), - right: Box::new(EvalExpr::LoadVar("b".to_string())), - }), - right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - }))] - ); - } - - /// Verifies PHP logical keywords parse case-insensitively with their own precedence. - #[test] - fn parse_fragment_accepts_keyword_logical_source() { - let program = - parse_fragment(br#"return false || true AnD false;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - }))] - ); - } - - /// Verifies PHP `xor` binds between `or` and `and` in eval expressions. - #[test] - fn parse_fragment_accepts_keyword_xor_source() { - let program = - parse_fragment(br#"return true XoR false or false;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::LogicalXor, - left: Box::new(EvalExpr::Const(EvalConst::Bool(true))), - right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - }))] - ); - } - - /// Verifies ternary expressions parse below logical OR and preserve both branches. - #[test] - fn parse_fragment_accepts_ternary_source() { - let program = - parse_fragment(br#"return $a || $b ? "yes" : "no";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Ternary { - condition: Box::new(EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(EvalExpr::LoadVar("a".to_string())), - right: Box::new(EvalExpr::LoadVar("b".to_string())), - }), - then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( - "yes".to_string() - )))), - else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), - }))] - ); - } - - /// Verifies PHP's short ternary form omits the explicit then branch in EvalIR. - #[test] - fn parse_fragment_accepts_short_ternary_source() { - let program = - parse_fragment(br#"return $name ?: "fallback";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Ternary { - condition: Box::new(EvalExpr::LoadVar("name".to_string())), - then_branch: None, - else_branch: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), - }))] - ); - } - - /// Verifies null coalescing parses as a right-associative expression. - #[test] - fn parse_fragment_accepts_null_coalesce_source() { - let program = - parse_fragment(br#"return $a ?? $b ?? "fallback";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NullCoalesce { - value: Box::new(EvalExpr::LoadVar("a".to_string())), - default: Box::new(EvalExpr::NullCoalesce { - value: Box::new(EvalExpr::LoadVar("b".to_string())), - default: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), - }), - }))] - ); - } - - /// Verifies match expressions preserve subject, patterns, and default expression. - #[test] - fn parse_fragment_accepts_match_source() { - let program = parse_fragment(br#"return match ($x) { 1, 2 => "small", default => "other" };"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Match { - subject: Box::new(EvalExpr::LoadVar("x".to_string())), - arms: vec![EvalMatchArm { - patterns: vec![ - EvalExpr::Const(EvalConst::Int(1)), - EvalExpr::Const(EvalConst::Int(2)), - ], - value: EvalExpr::Const(EvalConst::String("small".to_string())), - }], - default: Some(Box::new(EvalExpr::Const(EvalConst::String( - "other".to_string() - )))), - }))] - ); - } - - /// Verifies null coalescing binds tighter than PHP ternary expressions. - #[test] - fn parse_fragment_null_coalesce_binds_tighter_than_ternary() { - let program = - parse_fragment(br#"return $a ?? $b ? "yes" : "no";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Ternary { - condition: Box::new(EvalExpr::NullCoalesce { - value: Box::new(EvalExpr::LoadVar("a".to_string())), - default: Box::new(EvalExpr::LoadVar("b".to_string())), - }), - then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( - "yes".to_string() - )))), - else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), - }))] - ); - } - - /// Verifies logical negation parses as a unary expression before comparisons. - #[test] - fn parse_fragment_accepts_logical_not_source() { - let program = parse_fragment(br#"return !$flag == true;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LooseEq, - left: Box::new(EvalExpr::Unary { - op: EvalUnaryOp::LogicalNot, - expr: Box::new(EvalExpr::LoadVar("flag".to_string())), - }), - right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), - }))] - ); - } - - /// Verifies unary numeric operators bind tighter than multiplication. - #[test] - fn parse_fragment_accepts_unary_numeric_source() { - let program = parse_fragment(br#"return -$x * +2;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Mul, - left: Box::new(EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr: Box::new(EvalExpr::LoadVar("x".to_string())), - }), - right: Box::new(EvalExpr::Unary { - op: EvalUnaryOp::Plus, - expr: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }), - }))] - ); - } - - /// Verifies print fragments lower to expression-form print with the printed value. - #[test] - fn parse_fragment_accepts_print_source() { - let program = parse_fragment(br#"print "hi";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Expr(EvalExpr::Print(Box::new(EvalExpr::Const( - EvalConst::String("hi".to_string()) - ))))] - ); - } - - /// Verifies single- and double-quoted strings keep PHP-compatible simple escapes. - #[test] - fn parse_fragment_preserves_php_string_escape_semantics() { - let program = - parse_fragment(br#"return ['A\nB', "A\qB", "A\v\e\fB", 'It\'s'];"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( - "A\\nB".to_string() - ))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( - "A\\qB".to_string() - ))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( - "A\x0b\x1b\x0cB".to_string() - ))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("It's".to_string()))), - ])))] - ); - } - - /// Verifies call expressions preserve their callee name and source-order arguments. - #[test] - fn parse_fragment_accepts_call_expression_source() { - let program = - parse_fragment(br#"return eval("return 1;");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "eval".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "return 1;".to_string() - )))], - }))] - ); - } - - /// Verifies include and require constructs parse as expressions with path metadata. - #[test] - fn parse_fragment_accepts_include_require_expression_source() { - let program = parse_fragment(br#"return include "a" . ".php"; require_once("b.php");"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::Return(Some(EvalExpr::Include { - path: Box::new(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), - right: Box::new(EvalExpr::Const(EvalConst::String(".php".to_string()))), - }), - required: false, - once: false, - })), - EvalStmt::Expr(EvalExpr::Include { - path: Box::new(EvalExpr::Const(EvalConst::String("b.php".to_string()))), - required: true, - once: true, - }), - ] - ); - } - - /// Verifies explicitly qualified call expressions normalize away the leading slash. - #[test] - fn parse_fragment_accepts_qualified_call_expression_source() { - let program = parse_fragment(br#"return \strlen("abcd");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "strlen".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "abcd".to_string() - )))], - }))] - ); - } - - /// Verifies variable callable expressions lower to dynamic calls with source-order args. - #[test] - fn parse_fragment_accepts_dynamic_call_expression_source() { - let program = parse_fragment(br#"return $fn(first: "a", ...$rest);"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicCall { - callee: Box::new(EvalExpr::LoadVar("fn".to_string())), - args: vec![ - EvalCallArg::named( - "first", - EvalExpr::Const(EvalConst::String("a".to_string())), - ), - EvalCallArg::spread(EvalExpr::LoadVar("rest".to_string())), - ], - }))] - ); - } - - /// Verifies dynamic calls can be applied after another postfix expression. - #[test] - fn parse_fragment_accepts_postfix_dynamic_call_source() { - let program = - parse_fragment(br#"return $callbacks[0]("abcd");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicCall { - callee: Box::new(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::LoadVar("callbacks".to_string())), - index: Box::new(EvalExpr::Const(EvalConst::Int(0))), - }), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "abcd".to_string() - )))], - }))] - ); - } - - /// Verifies bare constant names lower to dynamic constant-fetch expressions. - #[test] - fn parse_fragment_accepts_constant_fetch_source() { - let program = parse_fragment(br#"return \Dyn\EvalConst;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::ConstFetch( - "Dyn\\EvalConst".to_string() - )))] - ); - } - - /// Verifies function calls preserve named arguments in source order. - #[test] - fn parse_fragment_accepts_named_call_argument_source() { - let program = parse_fragment(br#"return add(y: 2, x: 1);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "add".to_string(), - args: vec![ - EvalCallArg::named("y", EvalExpr::Const(EvalConst::Int(2))), - EvalCallArg::named("x", EvalExpr::Const(EvalConst::Int(1))), - ], - }))] - ); - } - - /// Verifies function calls preserve spread arguments in source order. - #[test] - fn parse_fragment_accepts_spread_call_argument_source() { - let program = parse_fragment(br#"return add(...$args);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "add".to_string(), - args: vec![EvalCallArg::spread(EvalExpr::LoadVar("args".to_string()))], - }))] - ); - } - - /// Verifies `isset` parses as a case-insensitive function-like expression. - #[test] - fn parse_fragment_accepts_isset_source() { - let program = - parse_fragment(br#"return ISSET($x, $items["k"]);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "isset".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), - EvalCallArg::positional(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::LoadVar("items".to_string())), - index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), - }), - ], - }))] - ); - } - - /// Verifies `empty` parses as a case-insensitive function-like expression. - #[test] - fn parse_fragment_accepts_empty_source() { - let program = - parse_fragment(br#"return EMPTY($items["k"]);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "empty".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::LoadVar("items".to_string())), - index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), - })], - }))] - ); - } - - /// Verifies indexed array literals and reads parse as runtime array expressions. - #[test] - fn parse_fragment_accepts_indexed_array_read_source() { - let program = parse_fragment(br#"return [1, 2][0];"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(2))), - ])), - index: Box::new(EvalExpr::Const(EvalConst::Int(0))), - }))] - ); - } - - /// Verifies legacy `array(...)` literals parse through the same EvalIR array node. - #[test] - fn parse_fragment_accepts_legacy_array_literal_source() { - let program = parse_fragment(br#"return array(1, "name" => "Ada",)[1];"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), - EvalArrayElement::KeyValue { - key: EvalExpr::Const(EvalConst::String("name".to_string())), - value: EvalExpr::Const(EvalConst::String("Ada".to_string())), - }, - ])), - index: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }))] - ); - } - - /// Verifies associative array literals preserve explicit key/value expressions. - #[test] - fn parse_fragment_accepts_assoc_array_literal_source() { - let program = - parse_fragment(br#"return ["name" => "Ada"];"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::KeyValue { - key: EvalExpr::Const(EvalConst::String("name".to_string())), - value: EvalExpr::Const(EvalConst::String("Ada".to_string())), - } - ])))] - ); - } - - /// Verifies indexed array writes parse as variable-target array set statements. - #[test] - fn parse_fragment_accepts_indexed_array_write_source() { - let program = parse_fragment(br#"$items[1] = "x";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ArraySetVar { - name: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(1)), - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }] - ); - } - - /// Verifies indexed array append syntax parses as a variable-target append statement. - #[test] - fn parse_fragment_accepts_indexed_array_append_source() { - let program = parse_fragment(br#"$items[] = "x";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ArrayAppendVar { - name: "items".to_string(), - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }] - ); - } - - /// Verifies array append syntax is accepted inside `for` update clauses. - #[test] - fn parse_fragment_accepts_array_append_in_for_update_source() { - let program = parse_fragment(br#"for ($i = 0; $i < 2; $items[] = $i) { $i += 1; }"#) - .expect("fragment should parse"); - let [EvalStmt::For { update, .. }] = program.statements() else { - panic!("expected for statement"); - }; - assert_eq!( - update, - &vec![EvalStmt::ArrayAppendVar { - name: "items".to_string(), - value: EvalExpr::LoadVar("i".to_string()), - }] - ); - } - - /// Verifies object property reads parse as postfix EvalIR expressions. - #[test] - fn parse_fragment_accepts_property_read_source() { - let program = parse_fragment(br#"return $this->x;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }))] - ); - } - - /// Verifies property names preserve source case while keywords remain case-insensitive. - #[test] - fn parse_fragment_preserves_property_case_source() { - let program = - parse_fragment(br#"RETURN $this->camelName;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "camelName".to_string(), - }))] - ); - } - - /// Verifies object method calls parse as postfix EvalIR call expressions. - #[test] - fn parse_fragment_accepts_method_call_source() { - let program = parse_fragment(br#"return $this->Answer();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "answer".to_string(), - args: Vec::new(), - }))] - ); - } - - /// Verifies object construction parses as a named EvalIR expression. - #[test] - fn parse_fragment_accepts_new_object_source() { - let program = parse_fragment(br#"return new Box();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "Box".to_string(), - args: Vec::new(), - }))] - ); - } - - /// Verifies object construction accepts explicitly qualified class names. - #[test] - fn parse_fragment_accepts_qualified_new_object_source() { - let program = - parse_fragment(br#"return new \EvalNs\Box();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "EvalNs\\Box".to_string(), - args: Vec::new(), - }))] - ); - } - - /// Verifies object method calls preserve source-order argument expressions. - #[test] - fn parse_fragment_accepts_method_call_args_source() { - let program = - parse_fragment(br#"return $this->add($x + 1);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "add".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - })], - }))] - ); - } - - /// Verifies object method calls parse multiple argument expressions in source order. - #[test] - fn parse_fragment_accepts_method_call_multiple_args_source() { - let program = - parse_fragment(br#"return $this->label($x, "ok");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "label".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), - EvalCallArg::positional(EvalExpr::Const(EvalConst::String("ok".to_string()))), - ], - }))] - ); - } - - /// Verifies object property writes parse as dedicated EvalIR statements. - #[test] - fn parse_fragment_accepts_property_write_source() { - let program = - parse_fragment(br#"$this->x = $this->x + 1;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }] - ); - } - - /// Verifies while fragments lower to loop statements with a nested block. - #[test] - fn parse_fragment_accepts_while_source() { - let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::While { - condition: EvalExpr::LoadVar("flag".to_string()), - body: vec![ - EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), - EvalStmt::StoreVar { - name: "flag".to_string(), - value: EvalExpr::Const(EvalConst::Bool(false)), - }, - ], - }] - ); - } - - /// Verifies do/while fragments lower to body-first loop statements. - #[test] - fn parse_fragment_accepts_do_while_source() { - let program = parse_fragment(br#"do { echo $flag; $flag = false; } while ($flag);"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::DoWhile { - body: vec![ - EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), - EvalStmt::StoreVar { - name: "flag".to_string(), - value: EvalExpr::Const(EvalConst::Bool(false)), - }, - ], - condition: EvalExpr::LoadVar("flag".to_string()), - }] - ); - } - - /// Verifies loop control statements parse inside while blocks. - #[test] - fn parse_fragment_accepts_break_and_continue_source() { - let program = parse_fragment(br#"while ($flag) { continue; break; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::While { - condition: EvalExpr::LoadVar("flag".to_string()), - body: vec![EvalStmt::Continue, EvalStmt::Break], - }] - ); - } - - /// Verifies return fragments parse optional return expressions. - #[test] - fn parse_fragment_accepts_return_source() { - let program = parse_fragment(b"return ($x - 1) * 4;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Mul, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Sub, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(4))), - }))] - ); - } - - /// Verifies throw statements lower to a Throwable expression carried by EvalIR. - #[test] - fn parse_fragment_accepts_throw_source() { - let program = - parse_fragment(br#"throw new Exception("eval boom");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })] - ); - } - - /// Verifies try/catch statements lower supported Throwable clauses into EvalIR. - #[test] - fn parse_fragment_accepts_try_catch_throwable_source() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable $caught) { - return 1; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })], - catches: vec![EvalCatch { - class_names: vec!["Throwable".to_string()], - var_name: Some("caught".to_string()), - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - }], - finally_body: Vec::new(), - }] - ); - } - - /// Verifies class imports can alias the supported Throwable catch type. - #[test] - fn parse_fragment_accepts_try_catch_imported_throwable_alias() { - let program = parse_fragment( - br#"use Throwable as T; -try { - throw $e; -} catch (T $caught) { - echo "caught"; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::LoadVar("e".to_string()))], - catches: vec![EvalCatch { - class_names: vec!["Throwable".to_string()], - var_name: Some("caught".to_string()), - body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "caught".to_string() - )))], - }], - finally_body: Vec::new(), - }] - ); - } - - /// Verifies Throwable catch clauses can omit the catch variable like PHP. - #[test] - fn parse_fragment_accepts_try_catch_without_variable() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable) { - return 1; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })], - catches: vec![EvalCatch { - class_names: vec!["Throwable".to_string()], - var_name: None, - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - }], - finally_body: Vec::new(), - }] - ); - } - - /// Verifies single catch type narrowing lowers into EvalIR. - #[test] - fn parse_fragment_accepts_specific_eval_catch_type() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Exception $caught) { - return 1; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })], - catches: vec![EvalCatch { - class_names: vec!["Exception".to_string()], - var_name: Some("caught".to_string()), - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - }], - finally_body: Vec::new(), - }] - ); - } - - /// Verifies union catch type narrowing lowers all source-order types into one clause. - #[test] - fn parse_fragment_accepts_union_eval_catch_type() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable|Exception $caught) { - return 1; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })], - catches: vec![EvalCatch { - class_names: vec!["Throwable".to_string(), "Exception".to_string()], - var_name: Some("caught".to_string()), - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - }], - finally_body: Vec::new(), - }] - ); - } - - /// Verifies try/finally statements lower the finalizer block into EvalIR. - #[test] - fn parse_fragment_accepts_eval_finally_source() { - let program = - parse_fragment(br#"try { return 1; } finally { echo "finally"; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - catches: Vec::new(), - finally_body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "finally".to_string() - )))], - }] - ); - } - - /// Verifies unset fragments expand to one by-name unset statement per variable. - #[test] - fn parse_fragment_accepts_unset_source() { - let program = parse_fragment(b"unset($x, $y);").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::UnsetVar { - name: "x".to_string() - }, - EvalStmt::UnsetVar { - name: "y".to_string() - }, - ] - ); - } - - /// Verifies eval fragments reject PHP opening tags. - #[test] - fn parse_fragment_rejects_opening_tag() { - assert_eq!( - parse_fragment(b"x; } }", - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalSupported", - vec![EvalClassProperty::new( - "x", - Some(EvalExpr::Const(EvalConst::Int(1))) - )], - vec![EvalClassMethod::new( - "read", - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }))] - )] - ))] - ); - } - - /// Verifies malformed object construction reports an unexpected token. - #[test] - fn parse_fragment_rejects_new_without_class_name() { - assert_eq!( - parse_fragment(b"return new ();"), - Err(EvalParseError::UnexpectedToken) - ); - } - - /// Verifies unsupported expression keywords report the unsupported construct status. - #[test] - fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { - for source in [ - b"return clone $value;" as &[u8], - b"return yield 1;" as &[u8], - ] { - assert_eq!( - parse_fragment(source), - Err(EvalParseError::UnsupportedConstruct) - ); - } - } - - /// Verifies malformed statements report parse errors instead of partial IR. - #[test] - fn parse_fragment_rejects_missing_semicolon() { - assert_eq!( - parse_fragment(b"$x = 1"), - Err(EvalParseError::ExpectedSemicolon) - ); - } -} diff --git a/crates/elephc-eval/src/parser/mod.rs b/crates/elephc-eval/src/parser/mod.rs new file mode 100644 index 0000000000..b48a87b557 --- /dev/null +++ b/crates/elephc-eval/src/parser/mod.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Parses runtime PHP eval fragments into EvalIR statement form. +//! The module entry point validates fragment boundaries, delegates tokenization, +//! and hands tokens to focused parser state. +//! +//! Called from: +//! - `crate::ffi::execute::__elephc_eval_execute()` +//! - `crate::interpreter` tests and nested eval execution paths. +//! +//! Key details: +//! - PHP eval fragments are statement fragments and must not include opening +//! ` Result { + if contains_php_open_tag(code) { + return Err(EvalParseError::PhpOpenTag); + } + let source = std::str::from_utf8(code).map_err(|_| EvalParseError::InvalidUtf8)?; + let tokens = tokenize(source)?; + Parser::new(tokens, code.len()).parse_program() +} + +/// Returns true when a fragment contains a PHP opening tag sequence. +fn contains_php_open_tag(code: &[u8]) -> bool { + code.windows(2).any(|window| window == b", + pos: usize, + source_len: usize, + namespace: String, + imports: NamespaceImports, + allow_use_imports: bool, +} + +/// A parsed PHP name plus whether it used a leading global namespace separator. +struct ParsedQualifiedName { + name: String, + absolute: bool, +} + +/// Import alias tables active for the current namespace declaration region. +#[derive(Default)] +struct NamespaceImports { + classes: HashMap, + functions: HashMap, + constants: HashMap, +} + +/// The `use` declaration namespace being imported. +#[derive(Copy, Clone, Eq, PartialEq)] +enum UseImportKind { + Class, + Function, + Const, +} + +impl NamespaceImports { + /// Stores one class import under PHP's case-insensitive class alias key. + fn insert_class(&mut self, alias: String, name: String) { + self.classes.insert(alias.to_ascii_lowercase(), name); + } + + /// Stores one function import under PHP's case-insensitive function alias key. + fn insert_function(&mut self, alias: String, name: String) { + self.functions.insert(alias.to_ascii_lowercase(), name); + } + + /// Stores one constant import under PHP's case-sensitive constant alias key. + fn insert_constant(&mut self, alias: String, name: String) { + self.constants.insert(alias, name); + } + + /// Resolves a class import, including aliases used as the first segment of a class name. + fn resolve_class(&self, name: &str) -> Option { + let (first, tail) = split_first_name_segment(name); + let imported = self.classes.get(&first.to_ascii_lowercase())?; + Some(match tail { + Some(tail) => format!("{imported}\\{tail}"), + None => imported.clone(), + }) + } + + /// Resolves an unqualified function alias. + fn resolve_function(&self, name: &str) -> Option<&str> { + self.functions + .get(&name.to_ascii_lowercase()) + .map(String::as_str) + } + + /// Resolves a case-sensitive unqualified constant alias. + fn resolve_constant(&self, name: &str) -> Option<&str> { + self.constants.get(name).map(String::as_str) + } +} + +impl Parser { + /// Creates a parser over tokens produced from a source fragment. + pub(super) fn new(tokens: Vec, source_len: usize) -> Self { + Self { + tokens, + pos: 0, + source_len, + namespace: String::new(), + imports: NamespaceImports::default(), + allow_use_imports: true, + } + } + + /// Parses a complete eval fragment until EOF. + pub(super) fn parse_program(mut self) -> Result { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::Eof) { + statements.extend(self.parse_stmt()?); + } + Ok(EvalProgram::new(self.source_len, statements)) + } + + /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. + fn parse_stmt(&mut self) -> Result, EvalParseError> { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "break") => { + self.advance(); + self.expect_semicolon()?; + Ok(vec![EvalStmt::Break]) + } + TokenKind::Ident(name) if ident_eq(name, "continue") => { + self.advance(); + self.expect_semicolon()?; + Ok(vec![EvalStmt::Continue]) + } + TokenKind::Ident(name) if ident_eq(name, "do") => self.parse_do_while_stmt(), + TokenKind::Ident(name) if ident_eq(name, "echo") => { + self.advance(); + let mut statements = vec![EvalStmt::Echo(self.parse_expr()?)]; + while self.consume(TokenKind::Comma) { + statements.push(EvalStmt::Echo(self.parse_expr()?)); + } + self.expect_semicolon()?; + Ok(statements) + } + TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), + TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), + TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), + TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), + TokenKind::Ident(name) if ident_eq(name, "namespace") => self.parse_namespace_stmt(), + TokenKind::Ident(name) if ident_eq(name, "return") => { + self.advance(); + if self.consume_semicolon() { + return Ok(vec![EvalStmt::Return(None)]); + } + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Return(Some(expr))]) + } + TokenKind::Ident(name) if ident_eq(name, "static") => self.parse_static_var_stmt(), + TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), + TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), + TokenKind::Ident(name) if ident_eq(name, "try") => self.parse_try_stmt(), + TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), + TokenKind::Ident(name) if ident_eq(name, "use") && self.allow_use_imports => { + self.parse_use_stmt() + } + TokenKind::Ident(name) if ident_eq(name, "use") => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), + TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(true) + } + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_stmt(name.clone()) + } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), true) + } + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { + let name = name.clone(); + self.parse_var_store_stmt(name, true) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => { + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Expr(expr)]) + } + } + } + + /// Parses `do { ... } while (expr);`. + fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_statement_body()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::DoWhile { body, condition }]) + } + + /// Parses `$name[index] = expr;` and `$name[] = expr;` eval writes. + fn parse_array_set_stmt(&mut self, name: String) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `for (init; condition; update) { ... }`. + fn parse_for_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let init = self.parse_for_init_clause()?; + self.expect_semicolon()?; + let condition = if matches!(self.current(), TokenKind::Semicolon) { + None + } else { + Some(self.parse_expr()?) + }; + self.expect_semicolon()?; + let update = self.parse_for_update_clause()?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::For { + init, + condition, + update, + body, + }]) + } + + /// Parses `foreach (expr as $value) { ... }` or `foreach (expr as $key => $value) { ... }`. + fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let array = self.parse_expr()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "as")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + let TokenKind::DollarIdent(value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let value_name = value_name.clone(); + self.advance(); + let (key_name, value_name) = if matches!(self.current(), TokenKind::FatArrow) { + self.advance(); + let TokenKind::DollarIdent(next_value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let key_name = value_name; + let value_name = next_value_name.clone(); + self.advance(); + (Some(key_name), value_name) + } else { + (None, value_name) + }; + self.expect(TokenKind::RParen)?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::Foreach { + array, + key_name, + value_name, + body, + }]) + } + + /// Parses `class Name { ... }` declarations for dynamic class metadata. + fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + self.expect(TokenKind::LBrace)?; + let mut properties = Vec::new(); + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_class_member(&mut properties, &mut methods)?; + } + self.consume_semicolon(); + Ok(vec![EvalStmt::ClassDecl(EvalClass::new( + name, properties, methods, + ))]) + } + + /// Parses one public property or method from an eval class body. + fn parse_class_member( + &mut self, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + let public = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) + { + self.advance(); + true + } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) + { + return Err(EvalParseError::UnsupportedConstruct); + } else { + false + }; + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + methods.push(self.parse_class_method_decl()?); + return Ok(()); + } + + if !public { + return Err(EvalParseError::UnsupportedConstruct); + } + properties.push(self.parse_class_property_decl()?); + Ok(()) + } + + /// Parses `function name($param, ...) { ... }` inside a dynamic eval class. + fn parse_class_method_decl(&mut self) -> Result { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let params = self.parse_function_params()?; + let body = self.parse_block()?; + Ok(EvalClassMethod::new(name, params, body)) + } + + /// Parses one public property declaration with an optional initializer. + fn parse_class_property_decl(&mut self) -> Result { + self.skip_optional_property_type()?; + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let default = if self.consume(TokenKind::Equal) { + Some(self.parse_expr()?) + } else { + None + }; + self.expect_semicolon()?; + Ok(EvalClassProperty::new(name, default)) + } + + /// Consumes a simple declared property type before the `$property` token. + fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::DollarIdent(_)) { + return Ok(()); + } + if self.consume(TokenKind::Question) && matches!(self.current(), TokenKind::DollarIdent(_)) + { + return Err(EvalParseError::UnexpectedToken); + } + match self.current() { + TokenKind::Ident(_) | TokenKind::Backslash => { + let _ = self.parse_qualified_name()?; + Ok(()) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Parses `function name($param, ...) { ... }` declarations. + fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + self.expect(TokenKind::LParen)?; + let params = self.parse_function_params()?; + let body = self.parse_block()?; + Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) + } + + /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. + fn parse_namespace_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let namespace = if self.consume(TokenKind::LBrace) { + return self.parse_namespace_block(String::new()); + } else { + self.parse_namespace_name()? + }; + if self.consume_semicolon() { + self.namespace = namespace; + self.imports = NamespaceImports::default(); + return Ok(Vec::new()); + } + self.expect(TokenKind::LBrace)?; + self.parse_namespace_block(namespace) + } + + /// Parses statements inside an already opened namespace block. + fn parse_namespace_block( + &mut self, + namespace: String, + ) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.namespace, namespace); + let previous_imports = std::mem::take(&mut self.imports); + let previous_allow_use_imports = std::mem::replace(&mut self.allow_use_imports, true); + let result = self.parse_block_contents(); + self.namespace = previous; + self.imports = previous_imports; + self.allow_use_imports = previous_allow_use_imports; + result + } + + /// Parses a namespace declaration name without a leading global separator. + fn parse_namespace_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses PHP `use`, `use function`, and `use const` import declarations. + fn parse_use_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let kind = self.parse_use_import_kind(); + + loop { + self.parse_use_import(kind)?; + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(Vec::new()) + } + + /// Parses an optional top-level `function` or `const` use-import kind. + fn parse_use_import_kind(&mut self) -> UseImportKind { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + self.advance(); + UseImportKind::Function + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + self.advance(); + UseImportKind::Const + } else { + UseImportKind::Class + } + } + + /// Parses and registers one comma-separated import entry. + fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { + let (name, grouped) = self.parse_use_name_or_group_start()?; + if grouped { + return self.parse_grouped_use_imports(kind, name); + } + self.parse_use_alias_and_register(kind, name) + } + + /// Parses a use-import name, stopping after a trailing namespace separator before `{`. + fn parse_use_name_or_group_start(&mut self) -> Result<(String, bool), EvalParseError> { + let _ = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + if self.consume(TokenKind::LBrace) { + return Ok((name, true)); + } + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok((name, false)) + } + + /// Parses all members inside a grouped namespace import declaration. + fn parse_grouped_use_imports( + &mut self, + default_kind: UseImportKind, + prefix: String, + ) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + loop { + let kind = self.parse_grouped_use_entry_kind(default_kind)?; + let member = self.parse_grouped_use_member_name()?; + let name = join_grouped_use_name(&prefix, &member); + self.parse_use_alias_and_register(kind, name)?; + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RBrace) { + return Ok(()); + } + } + self.expect(TokenKind::RBrace) + } + + /// Parses an optional per-entry grouped import kind, matching PHP's mixed group rules. + fn parse_grouped_use_entry_kind( + &mut self, + default_kind: UseImportKind, + ) -> Result { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Function); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Const); + } + Ok(default_kind) + } + + /// Parses one non-absolute member name inside a grouped use declaration. + fn parse_grouped_use_member_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses an optional alias and stores one namespace import. + fn parse_use_alias_and_register( + &mut self, + kind: UseImportKind, + name: String, + ) -> Result<(), EvalParseError> { + let alias = if matches!( + self.current(), + TokenKind::Ident(keyword) if ident_eq(keyword, "as") + ) { + self.advance(); + let TokenKind::Ident(alias) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let alias = alias.clone(); + self.advance(); + alias + } else { + last_name_segment(&name).to_string() + }; + + match kind { + UseImportKind::Class => self.imports.insert_class(alias, name), + UseImportKind::Function => self.imports.insert_function(alias, name), + UseImportKind::Const => self.imports.insert_constant(alias, name), + } + Ok(()) + } + + /// Parses `global $name, $other;` declarations in eval fragments. + fn parse_global_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let mut vars = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + vars.push(name.clone()); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(vec![EvalStmt::Global { vars }]) + } + + /// Parses `static $name = expr;` declarations in eval fragments. + fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let init = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::StaticVar { name, init }]) + } + + /// Parses `throw expr;` statements in eval fragments. + fn parse_throw_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Throw(expr)]) + } + + /// Parses `try { ... } catch (Type|Other $name) { ... } finally { ... }` statements. + fn parse_try_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_block()?; + let mut catches = Vec::new(); + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { + catches.push(self.parse_catch_clause()?); + } + let finally_body = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) + { + self.advance(); + self.parse_block()? + } else { + Vec::new() + }; + if catches.is_empty() && finally_body.is_empty() { + return Err(EvalParseError::UnexpectedToken); + } + Ok(vec![EvalStmt::Try { + body, + catches, + finally_body, + }]) + } + + /// Parses one `catch (ClassName|Other [$name]) { ... }` clause. + fn parse_catch_clause(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let class_names = self.parse_catch_types()?; + let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { + let var_name = var_name.clone(); + self.advance(); + Some(var_name) + } else { + None + }; + self.expect(TokenKind::RParen)?; + let body = self.parse_block()?; + Ok(EvalCatch { + class_names, + var_name, + body, + }) + } + + /// Parses one or more unioned catch types in source order. + fn parse_catch_types(&mut self) -> Result, EvalParseError> { + let class_name = self.parse_qualified_name()?; + let mut class_names = vec![self.resolve_class_name(class_name)]; + while self.consume(TokenKind::Pipe) { + let class_name = self.parse_qualified_name()?; + class_names.push(self.resolve_class_name(class_name)); + } + Ok(class_names) + } + + /// Parses a dynamic function declaration parameter list after `(`. + fn parse_function_params(&mut self) -> Result, EvalParseError> { + let mut params = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(params); + } + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + params.push(name.clone()); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok(params) + } + + /// Parses the optional first clause of a `for` loop. + fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Semicolon) { + return Ok(Vec::new()); + } + self.parse_for_clause_stmt() + } + + /// Parses the optional update clause of a `for` loop. + fn parse_for_update_clause(&mut self) -> Result, EvalParseError> { + if self.consume(TokenKind::RParen) { + return Ok(Vec::new()); + } + let statements = self.parse_for_clause_stmt()?; + self.expect(TokenKind::RParen)?; + Ok(statements) + } + + /// Parses one statement-like `for` clause without consuming a delimiter. + fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { + match self.current() { + TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(false), + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_clause(name.clone()) + } + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(false) + } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), false) + } + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { + let name = name.clone(); + self.parse_var_store_stmt(name, false) + } + _ => { + let expr = self.parse_expr()?; + Ok(vec![EvalStmt::Expr(expr)]) + } + } + } + + /// Parses `$name[index] = expr` and `$name[] = expr` in a `for` clause. + fn parse_array_set_clause(&mut self, name: String) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `$name = expr` and simple variable compound assignments. + fn parse_var_store_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { + self.advance(); + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::ReferenceAssign { + target: name, + source, + }]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = assignment_value(&name, op, value); + Ok(vec![EvalStmt::StoreVar { name, value }]) + } + + /// Parses prefix `++$name` and `--$name` as simple statement effects. + fn parse_prefix_inc_dec_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![inc_dec_store(name, increment)]) + } + + /// Parses postfix `$name++` and `$name--` as simple statement effects. + fn parse_postfix_inc_dec_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![inc_dec_store(name, increment)]) + } + + /// Parses `$object->property` as either an expression statement or property write. + fn parse_property_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let target = self.parse_expr()?; + if !self.consume(TokenKind::Equal) { + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::Expr(target)]); + } + let EvalExpr::PropertyGet { object, property } = target else { + return Err(EvalParseError::UnexpectedToken); + }; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::PropertySet { + object: *object, + property, + value, + }]) + } + + /// Parses a complete `if` statement after consuming the `if` keyword. + fn parse_if_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } + + /// Parses the condition, then block, and optional else branch for an `if` chain. + fn parse_if_after_keyword(&mut self) -> Result { + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let then_branch = self.parse_statement_body()?; + let else_branch = self.parse_optional_else_branch()?; + Ok(EvalStmt::If { + condition, + then_branch, + else_branch, + }) + } + + /// Parses `elseif`, `else if`, or `else` branches after an `if` body. + fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { + self.advance(); + return Ok(vec![self.parse_if_after_keyword()?]); + } + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "else")) { + return Ok(Vec::new()); + } + self.advance(); + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "if")) { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } else { + self.parse_statement_body() + } + } + + /// Parses `switch (expr) { case expr: ... default: ... }`. + fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + let mut cases = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + cases.push(self.parse_switch_case()?); + } + self.expect(TokenKind::RBrace)?; + Ok(vec![EvalStmt::Switch { expr, cases }]) + } + + /// Parses one `case` or `default` arm inside a switch body. + fn parse_switch_case(&mut self) -> Result { + let condition = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) + { + self.advance(); + let condition = self.parse_expr()?; + Some(condition) + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + None + } else { + return Err(EvalParseError::UnexpectedToken); + }; + self.expect(TokenKind::Colon)?; + let body = self.parse_switch_case_body()?; + Ok(EvalSwitchCase { condition, body }) + } + + /// Parses case body statements until the next case boundary or switch close. + fn parse_switch_case_body(&mut self) -> Result, EvalParseError> { + let mut body = Vec::new(); + while !is_switch_case_boundary(self.current()) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + body.extend(self.parse_stmt()?); + } + Ok(body) + } + + /// Parses `unset($name[, ...]);`. + fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let mut statements = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + statements.push(EvalStmt::UnsetVar { name: name.clone() }); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(statements) + } + + /// Parses `while (expr) { ... }`. + fn parse_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::While { condition, body }]) + } + + /// Parses either a brace-delimited block or one braceless statement body. + fn parse_statement_body(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::LBrace) { + self.parse_block() + } else { + self.parse_nested_stmt() + } + } + + /// Parses a brace-delimited statement block. + fn parse_block(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LBrace)?; + self.parse_nested_block_contents() + } + + /// Parses one nested statement where import declarations are not legal. + fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_stmt(); + self.allow_use_imports = previous; + result + } + + /// Parses a nested block while preserving active imports for name resolution. + fn parse_nested_block_contents(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_block_contents(); + self.allow_use_imports = previous; + result + } + + /// Parses statements until the closing brace for the current block. + fn parse_block_contents(&mut self) -> Result, EvalParseError> { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + statements.extend(self.parse_stmt()?); + } + self.expect(TokenKind::RBrace)?; + Ok(statements) + } + + /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. + fn parse_expr(&mut self) -> Result { + self.parse_keyword_or() + } + + /// Parses PHP keyword `or`, whose precedence is lower than `xor`, `and`, and ternary. + fn parse_keyword_or(&mut self) -> Result { + let mut expr = self.parse_keyword_xor()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "or")) { + self.advance(); + let right = self.parse_keyword_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `xor`, whose operands are evaluated before boolean XOR. + fn parse_keyword_xor(&mut self) -> Result { + let mut expr = self.parse_keyword_and()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "xor")) { + self.advance(); + let right = self.parse_keyword_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `and`, whose precedence is lower than ternary and `&&`. + fn parse_keyword_and(&mut self) -> Result { + let mut expr = self.parse_ternary()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "and")) { + self.advance(); + let right = self.parse_ternary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. + fn parse_ternary(&mut self) -> Result { + let condition = self.parse_null_coalesce()?; + if !self.consume(TokenKind::Question) { + return Ok(condition); + } + let then_branch = if self.consume(TokenKind::Colon) { + None + } else { + let expr = self.parse_expr()?; + self.expect(TokenKind::Colon)?; + Some(Box::new(expr)) + }; + let else_branch = self.parse_expr()?; + Ok(EvalExpr::Ternary { + condition: Box::new(condition), + then_branch, + else_branch: Box::new(else_branch), + }) + } + + /// Parses right-associative null coalescing below logical OR and above ternary. + fn parse_null_coalesce(&mut self) -> Result { + let value = self.parse_logical_or()?; + if !self.consume(TokenKind::QuestionQuestion) { + return Ok(value); + } + let default = self.parse_null_coalesce()?; + Ok(EvalExpr::NullCoalesce { + value: Box::new(value), + default: Box::new(default), + }) + } + + /// Parses left-associative logical OR with lower precedence than logical AND. + fn parse_logical_or(&mut self) -> Result { + let mut expr = self.parse_logical_and()?; + while self.consume(TokenKind::OrOr) { + let right = self.parse_logical_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative logical AND with lower precedence than equality. + fn parse_logical_and(&mut self) -> Result { + let mut expr = self.parse_bit_or()?; + while self.consume(TokenKind::AndAnd) { + let right = self.parse_bit_or()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise OR with lower precedence than bitwise XOR. + fn parse_bit_or(&mut self) -> Result { + let mut expr = self.parse_bit_xor()?; + while self.consume(TokenKind::Pipe) { + let right = self.parse_bit_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise XOR with lower precedence than bitwise AND. + fn parse_bit_xor(&mut self) -> Result { + let mut expr = self.parse_bit_and()?; + while self.consume(TokenKind::Caret) { + let right = self.parse_bit_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise AND with lower precedence than equality. + fn parse_bit_and(&mut self) -> Result { + let mut expr = self.parse_equality()?; + while self.consume(TokenKind::Ampersand) { + let right = self.parse_equality()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative equality and inequality comparisons. + fn parse_equality(&mut self) -> Result { + let mut expr = self.parse_ordering()?; + loop { + let op = if self.consume(TokenKind::EqualEqual) { + EvalBinOp::LooseEq + } else if self.consume(TokenKind::NotEqual) { + EvalBinOp::LooseNotEq + } else if self.consume(TokenKind::EqualEqualEqual) { + EvalBinOp::StrictEq + } else if self.consume(TokenKind::NotEqualEqual) { + EvalBinOp::StrictNotEq + } else { + break; + }; + let right = self.parse_ordering()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative ordered comparisons. + fn parse_ordering(&mut self) -> Result { + let mut expr = self.parse_shift()?; + loop { + let op = if self.consume(TokenKind::Less) { + EvalBinOp::Lt + } else if self.consume(TokenKind::LessEqual) { + EvalBinOp::LtEq + } else if self.consume(TokenKind::Greater) { + EvalBinOp::Gt + } else if self.consume(TokenKind::GreaterEqual) { + EvalBinOp::GtEq + } else if self.consume(TokenKind::Spaceship) { + EvalBinOp::Spaceship + } else { + break; + }; + let right = self.parse_shift()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative integer shift operators. + fn parse_shift(&mut self) -> Result { + let mut expr = self.parse_concat()?; + loop { + let op = if self.consume(TokenKind::LessLess) { + EvalBinOp::ShiftLeft + } else if self.consume(TokenKind::GreaterGreater) { + EvalBinOp::ShiftRight + } else { + break; + }; + let right = self.parse_concat()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative string concatenation. + fn parse_concat(&mut self) -> Result { + let mut expr = self.parse_add()?; + while self.consume(TokenKind::Dot) { + let right = self.parse_add()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric addition and subtraction. + fn parse_add(&mut self) -> Result { + let mut expr = self.parse_mul()?; + loop { + let op = if self.consume(TokenKind::Plus) { + EvalBinOp::Add + } else if self.consume(TokenKind::Minus) { + EvalBinOp::Sub + } else { + break; + }; + let right = self.parse_mul()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric multiplication, division, and modulo. + fn parse_mul(&mut self) -> Result { + let mut expr = self.parse_unary()?; + loop { + let op = if self.consume(TokenKind::Star) { + EvalBinOp::Mul + } else if self.consume(TokenKind::Slash) { + EvalBinOp::Div + } else if self.consume(TokenKind::Percent) { + EvalBinOp::Mod + } else { + break; + }; + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses right-associative unary prefix expressions. + fn parse_unary(&mut self) -> Result { + if self.consume(TokenKind::Plus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Minus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Bang) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Tilde) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(expr), + }); + } + self.parse_power() + } + + /// Parses right-associative exponentiation with higher precedence than unary prefix operators. + fn parse_power(&mut self) -> Result { + let mut expr = self.parse_postfix()?; + if self.consume(TokenKind::StarStar) { + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses postfix array reads, property reads, method calls, and dynamic calls. + fn parse_postfix(&mut self) -> Result { + let mut expr = self.parse_primary()?; + loop { + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + expr = EvalExpr::DynamicCall { + callee: Box::new(expr), + args, + }; + continue; + } + if self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + continue; + } + if self.consume(TokenKind::Arrow) { + let TokenKind::Ident(member) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + expr = EvalExpr::MethodCall { + object: Box::new(expr), + method: member.to_ascii_lowercase(), + args, + }; + } else { + expr = EvalExpr::PropertyGet { + object: Box::new(expr), + property: member, + }; + } + continue; + } + break; + } + Ok(expr) + } + + /// Parses primary expressions supported by the initial eval subset. + fn parse_primary(&mut self) -> Result { + match self.current() { + TokenKind::Int(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Int(value))) + } + TokenKind::Float(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Float(value))) + } + TokenKind::String(value) => { + let value = value.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(value))) + } + TokenKind::DollarIdent(name) => { + let name = name.clone(); + self.advance(); + Ok(EvalExpr::LoadVar(name)) + } + TokenKind::Magic(EvalMagicConst::Namespace) => { + let namespace = self.namespace.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(namespace))) + } + TokenKind::Magic(magic) => { + let magic = magic.clone(); + self.advance(); + Ok(EvalExpr::Magic(magic)) + } + TokenKind::Ident(name) if ident_eq(name, "null") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Null)) + } + TokenKind::Ident(name) if ident_eq(name, "true") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(true))) + } + TokenKind::Ident(name) if ident_eq(name, "false") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(false))) + } + TokenKind::Ident(name) if ident_eq(name, "print") => { + self.advance(); + let expr = self.parse_expr()?; + Ok(EvalExpr::Print(Box::new(expr))) + } + TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { + self.parse_legacy_array_literal() + } + TokenKind::Ident(name) if is_include_construct_name(name) => self.parse_include_expr(), + TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), + TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), + TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::Backslash => self.parse_qualified_name_expr(), + TokenKind::Ident(_) if matches!(self.peek(), TokenKind::Backslash) => { + self.parse_qualified_name_expr() + } + TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { + self.parse_call_expr(name.clone()) + } + TokenKind::Ident(name) => { + let name = name.clone(); + self.advance(); + Ok(self.const_fetch_expr(name)) + } + TokenKind::LBracket => self.parse_array_literal(), + TokenKind::LParen => { + self.advance(); + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + Ok(expr) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Parses PHP include/require expression constructs and their path expression. + fn parse_include_expr(&mut self) -> Result { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let required = ident_eq(name, "require") || ident_eq(name, "require_once"); + let once = ident_eq(name, "include_once") || ident_eq(name, "require_once"); + self.advance(); + let path = if self.consume(TokenKind::LParen) { + let path = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + path + } else { + self.parse_expr()? + }; + Ok(EvalExpr::Include { + path: Box::new(path), + required, + once, + }) + } + + /// Parses `match (expr) { pattern, other => value, default => fallback }`. + fn parse_match_expr(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let subject = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + + let mut arms = Vec::new(); + let mut default = None; + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + self.expect(TokenKind::FatArrow)?; + default = Some(Box::new(self.parse_expr()?)); + } else { + arms.push(self.parse_match_arm()?); + } + if self.consume(TokenKind::Comma) { + continue; + } + self.expect(TokenKind::RBrace)?; + break; + } + + Ok(EvalExpr::Match { + subject: Box::new(subject), + arms, + default, + }) + } + + /// Parses one non-default `match` arm and its comma-separated pattern list. + fn parse_match_arm(&mut self) -> Result { + let mut patterns = Vec::new(); + loop { + patterns.push(self.parse_expr()?); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::FatArrow) { + return Err(EvalParseError::UnexpectedToken); + } + if matches!(self.current(), TokenKind::Eof | TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + } + self.expect(TokenKind::FatArrow)?; + let value = self.parse_expr()?; + Ok(EvalMatchArm { patterns, value }) + } + + /// Parses a function-like call expression and its source-order arguments. + fn parse_call_expr(&mut self, name: String) -> Result { + self.advance(); + let args = self.parse_call_args()?; + Ok(self.call_expr(name, args)) + } + + /// Parses an explicitly qualified call or constant-fetch expression. + fn parse_qualified_name_expr(&mut self) -> Result { + let name = self.parse_qualified_name()?; + let name = self.resolve_qualified_name(name); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(EvalExpr::Call { + name: name.to_ascii_lowercase(), + args, + }); + } + Ok(EvalExpr::ConstFetch(name)) + } + + /// Parses `new ClassName(...)` expressions in eval fragments. + fn parse_new_object_expr(&mut self) -> Result { + self.advance(); + let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_class_name(class_name); + let args = self.parse_call_args()?; + Ok(EvalExpr::NewObject { class_name, args }) + } + + /// Parses a simple or explicitly qualified PHP name. + fn parse_qualified_name(&mut self) -> Result { + let absolute = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok(ParsedQualifiedName { name, absolute }) + } + + /// Builds a call expression, adding namespace fallback for unqualified names. + fn call_expr(&self, name: String, args: Vec) -> EvalExpr { + if let Some(imported) = self.imports.resolve_function(&name) { + return EvalExpr::Call { + name: imported.to_ascii_lowercase(), + args, + }; + } + let fallback_name = name.to_ascii_lowercase(); + if self.namespace.is_empty() { + EvalExpr::Call { + name: fallback_name, + args, + } + } else { + EvalExpr::NamespacedCall { + name: self + .qualify_name_in_current_namespace(&name) + .to_ascii_lowercase(), + fallback_name, + args, + } + } + } + + /// Builds a constant fetch expression, adding namespace fallback for unqualified names. + fn const_fetch_expr(&self, name: String) -> EvalExpr { + if let Some(imported) = self.imports.resolve_constant(&name) { + return EvalExpr::ConstFetch(imported.to_string()); + } + if self.namespace.is_empty() { + EvalExpr::ConstFetch(name) + } else { + EvalExpr::NamespacedConstFetch { + name: self.qualify_name_in_current_namespace(&name), + fallback_name: name, + } + } + } + + /// Prefixes a name with the parser's current namespace when one is active. + fn qualify_name_in_current_namespace(&self, name: &str) -> String { + if self.namespace.is_empty() { + name.to_string() + } else { + format!("{}\\{}", self.namespace, name) + } + } + + /// Resolves a class name through active imports before namespace qualification. + fn resolve_class_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute { + return name.name; + } + if let Some(imported) = self.imports.resolve_class(&name.name) { + return imported; + } + self.resolve_qualified_name(name) + } + + /// Resolves a parsed PHP name according to the current namespace. + fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute || self.namespace.is_empty() { + name.name + } else { + self.qualify_name_in_current_namespace(&name.name) + } + } + + /// Parses a parenthesized source-order argument list. + fn parse_call_args(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LParen)?; + let mut args = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(args); + } + loop { + args.push(self.parse_call_arg()?); + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RParen) { + return Ok(args); + } + } + self.expect(TokenKind::RParen)?; + Ok(args) + } + + /// Parses one positional or named argument within a call argument list. + fn parse_call_arg(&mut self) -> Result { + if self.consume(TokenKind::Ellipsis) { + return self.parse_expr().map(EvalCallArg::spread); + } + if matches!(self.peek(), TokenKind::Colon) { + if let TokenKind::Ident(name) = self.current() { + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Colon)?; + let value = self.parse_expr()?; + return Ok(EvalCallArg::named(name, value)); + } + } + self.parse_expr().map(EvalCallArg::positional) + } + + /// Parses an array literal with source-order optional key/value element expressions. + fn parse_array_literal(&mut self) -> Result { + self.expect(TokenKind::LBracket)?; + self.parse_array_elements_until(TokenKind::RBracket) + } + + /// Parses PHP's legacy `array(...)` literal into the same EvalIR node as `[...]`. + fn parse_legacy_array_literal(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + self.parse_array_elements_until(TokenKind::RParen) + } + + /// Returns whether the current token starts PHP's legacy `array(...)` literal syntax. + fn current_starts_legacy_array_literal(&self) -> bool { + matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "array")) + && matches!(self.peek(), TokenKind::LParen) + } + + /// Parses comma-separated array elements until the supplied closing delimiter. + fn parse_array_elements_until(&mut self, close: TokenKind) -> Result { + let mut elements = Vec::new(); + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + loop { + let first = self.parse_expr()?; + if self.consume(TokenKind::FatArrow) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyValue { key: first, value }); + } else { + elements.push(EvalArrayElement::Value(first)); + } + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + } + self.expect(close)?; + Ok(EvalExpr::Array(elements)) + } + + /// Consumes `expected` or returns a parse error. + fn expect(&mut self, expected: TokenKind) -> Result<(), EvalParseError> { + if self.consume(expected) { + Ok(()) + } else { + Err(EvalParseError::UnexpectedToken) + } + } + + /// Consumes a semicolon or returns the semicolon-specific parse error. + fn expect_semicolon(&mut self) -> Result<(), EvalParseError> { + if self.consume_semicolon() { + Ok(()) + } else { + Err(EvalParseError::ExpectedSemicolon) + } + } + + /// Consumes a semicolon if present. + fn consume_semicolon(&mut self) -> bool { + self.consume(TokenKind::Semicolon) + } + + /// Consumes `expected` if the current token matches it. + fn consume(&mut self, expected: TokenKind) -> bool { + if *self.current() == expected { + self.advance(); + true + } else { + false + } + } + + /// Returns the current token. + fn current(&self) -> &TokenKind { + self.tokens.get(self.pos).unwrap_or(&TokenKind::Eof) + } + + /// Returns the next token without advancing. + fn peek(&self) -> &TokenKind { + self.tokens.get(self.pos + 1).unwrap_or(&TokenKind::Eof) + } + + /// Advances to the next token. + fn advance(&mut self) { + if self.pos < self.tokens.len() { + self.pos += 1; + } + } +} +/// Returns true when the current token closes or starts a switch case arm. +fn is_switch_case_boundary(token: &TokenKind) -> bool { + matches!(token, TokenKind::RBrace) + || matches!(token, TokenKind::Ident(name) if ident_eq(name, "case") || ident_eq(name, "default")) +} + +/// Maps simple variable assignment tokens to an optional compound EvalIR operator. +fn assignment_op(token: &TokenKind) -> Option> { + match token { + TokenKind::Equal => Some(None), + TokenKind::PlusEqual => Some(Some(EvalBinOp::Add)), + TokenKind::MinusEqual => Some(Some(EvalBinOp::Sub)), + TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), + TokenKind::StarStarEqual => Some(Some(EvalBinOp::Pow)), + TokenKind::SlashEqual => Some(Some(EvalBinOp::Div)), + TokenKind::PercentEqual => Some(Some(EvalBinOp::Mod)), + TokenKind::AmpEqual => Some(Some(EvalBinOp::BitAnd)), + TokenKind::PipeEqual => Some(Some(EvalBinOp::BitOr)), + TokenKind::CaretEqual => Some(Some(EvalBinOp::BitXor)), + TokenKind::LessLessEqual => Some(Some(EvalBinOp::ShiftLeft)), + TokenKind::GreaterGreaterEqual => Some(Some(EvalBinOp::ShiftRight)), + TokenKind::DotEqual => Some(Some(EvalBinOp::Concat)), + _ => None, + } +} + +/// Builds the assigned value expression for plain and compound variable assignment. +fn assignment_value(name: &str, op: Option, value: EvalExpr) -> EvalExpr { + match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::LoadVar(name.to_string())), + right: Box::new(value), + }, + None => value, + } +} + +/// Builds the StoreVar statement for a simple variable increment or decrement. +pub(super) fn inc_dec_store(name: String, increment: bool) -> EvalStmt { + EvalStmt::StoreVar { + value: EvalExpr::Binary { + op: if increment { + EvalBinOp::Add + } else { + EvalBinOp::Sub + }, + left: Box::new(EvalExpr::LoadVar(name.clone())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + name, + } +} + +/// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. +fn ident_eq(actual: &str, expected: &str) -> bool { + actual.eq_ignore_ascii_case(expected) +} + +/// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. +fn is_unsupported_statement_keyword(name: &str) -> bool { + ["enum", "interface", "trait"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + +/// Returns true for class member modifiers outside the current eval class subset. +fn is_unsupported_class_member_modifier(name: &str) -> bool { + ["private", "protected", "static", "abstract", "final"] + .iter() + .any(|modifier| ident_eq(name, modifier)) +} + +/// Returns true when an identifier is an include/require expression construct. +fn is_include_construct_name(name: &str) -> bool { + ["include", "include_once", "require", "require_once"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + +/// Returns the first namespace segment and the optional remaining suffix. +fn split_first_name_segment(name: &str) -> (&str, Option<&str>) { + name.split_once('\\') + .map_or((name, None), |(first, tail)| (first, Some(tail))) +} + +/// Returns the final segment of a PHP qualified name. +fn last_name_segment(name: &str) -> &str { + name.rsplit('\\').next().unwrap_or(name) +} + +/// Combines a grouped use prefix with one relative member name. +fn join_grouped_use_name(prefix: &str, member: &str) -> String { + format!("{prefix}\\{member}") +} + +/// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. +fn is_unsupported_expression_keyword(name: &str) -> bool { + ["clone", "yield"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} diff --git a/crates/elephc-eval/src/parser/tests.rs b/crates/elephc-eval/src/parser/tests.rs new file mode 100644 index 0000000000..ca2d733734 --- /dev/null +++ b/crates/elephc-eval/src/parser/tests.rs @@ -0,0 +1,1803 @@ +//! Purpose: +//! Parser unit tests for runtime PHP eval fragments. +//! The cases assert that supported syntax lowers into EvalIR and unsupported +//! syntax reports stable parse statuses. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Fixtures intentionally use eval fragments without PHP opening tags. +//! - Expected values compare against EvalIR so grammar regressions are visible. + +use super::parse_fragment; +use super::state::inc_dec_store; +use crate::errors::EvalParseError; +use crate::eval_ir::*; + +/// Verifies assignment fragments lower to by-name StoreVar statements. +#[test] +fn parse_fragment_accepts_assignment_source() { + let program = parse_fragment(b"$x = 1;").expect("fragment should parse"); + assert_eq!(program.source_len(), 7); + assert_eq!( + program.statements(), + &[EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::Int(1)), + }] + ); +} + +/// Verifies reference assignments lower to by-name ReferenceAssign statements. +#[test] +fn parse_fragment_accepts_reference_assignment_source() { + let program = parse_fragment(b"$left =& $right;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ReferenceAssign { + target: "left".to_string(), + source: "right".to_string(), + }] + ); +} + +/// Verifies multiplicative operators preserve PHP precedence and associativity. +#[test] +fn parse_fragment_accepts_division_and_modulo_source() { + let program = parse_fragment(b"return 10 / 4 % 3;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mod, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Div, + left: Box::new(EvalExpr::Const(EvalConst::Int(10))), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} + +/// Verifies exponentiation is right-associative and binds tighter than unary negation. +#[test] +fn parse_fragment_accepts_power_source() { + let program = + parse_fragment(b"return -2 ** 2; return 2 ** 3 ** 2;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + })), + EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(3))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + })), + ] + ); +} + +/// Verifies bitwise operators preserve PHP precedence. +#[test] +fn parse_fragment_accepts_bitwise_source() { + let program = parse_fragment(b"return ~0 | 2 ^ 3 & 4;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(EvalExpr::Const(EvalConst::Int(3))), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }), + }), + }))] + ); +} + +/// Verifies shift operators bind lower than additive expressions. +#[test] +fn parse_fragment_accepts_shift_source() { + let program = parse_fragment(b"return 1 + 2 << 3;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::ShiftLeft, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::Const(EvalConst::Int(1))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} + +/// Verifies simple variable compound assignments lower to StoreVar with binary expressions. +#[test] +fn parse_fragment_accepts_compound_assignment_source() { + let program = parse_fragment(br#"$x += 2; $x -= 1; $x *= 3; $x /= 2; $x %= 5; $s .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Div, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Mod, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(5))), + }, + }, + EvalStmt::StoreVar { + name: "s".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("s".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::String("ok".to_string()))), + }, + }, + ] + ); +} + +/// Verifies exponentiation compound assignment lowers through the binary power operator. +#[test] +fn parse_fragment_accepts_power_compound_assignment_source() { + let program = parse_fragment(br#"$x **= 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }] + ); +} + +/// Verifies bitwise compound assignments lower to StoreVar with binary expressions. +#[test] +fn parse_fragment_accepts_bitwise_compound_assignment_source() { + let program = parse_fragment(br#"$x &= 3; $x |= 1; $x ^= 2; $x <<= 4; $x >>= 1;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::ShiftLeft, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::ShiftRight, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + ] + ); +} + +/// Verifies simple variable increment and decrement statements lower to StoreVar. +#[test] +fn parse_fragment_accepts_inc_dec_statement_source() { + let program = parse_fragment(br#"$i++; ++$j; $k--; --$m;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + inc_dec_store("i".to_string(), true), + inc_dec_store("j".to_string(), true), + inc_dec_store("k".to_string(), false), + inc_dec_store("m".to_string(), false), + ] + ); +} + +/// Verifies echo fragments preserve expression source order. +#[test] +fn parse_fragment_accepts_echo_source() { + let program = parse_fragment(br#"echo "hi" . $name;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Echo(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Const(EvalConst::String("hi".to_string()))), + right: Box::new(EvalExpr::LoadVar("name".to_string())), + })] + ); +} + +/// Verifies PHP echo comma lists lower to one EvalIR echo statement per expression. +#[test] +fn parse_fragment_accepts_echo_comma_list_source() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("a".to_string()))), + EvalStmt::Echo(EvalExpr::LoadVar("b".to_string())), + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("c".to_string()))), + ] + ); +} + +/// Verifies if/else fragments lower to branch statements with nested blocks. +#[test] +fn parse_fragment_accepts_if_else_source() { + let program = parse_fragment(br#"if ($flag) { $x = "yes"; } else { $x = "no"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("flag".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("yes".to_string())), + }], + else_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("no".to_string())), + }], + }] + ); +} + +/// Verifies braceless if/else bodies parse as single-statement branch bodies. +#[test] +fn parse_fragment_accepts_braceless_if_else_source() { + let program = parse_fragment(br#"if ($flag) echo "yes"; else echo "no";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("flag".to_string()), + then_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))], + else_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "no".to_string() + )))], + }] + ); +} + +/// Verifies elseif fragments lower to nested if statements in the else branch. +#[test] +fn parse_fragment_accepts_elseif_source() { + let program = parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("a".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("a".to_string())), + }], + else_branch: vec![EvalStmt::If { + condition: EvalExpr::LoadVar("b".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("b".to_string())), + }], + else_branch: Vec::new(), + }], + }] + ); +} + +/// Verifies PHP's `else if` spelling follows the same nested branch shape. +#[test] +fn parse_fragment_accepts_else_if_source() { + let program = parse_fragment(br#"if ($a) { $x = "a"; } else if ($b) { $x = "b"; }"#) + .expect("fragment should parse"); + + assert!(matches!( + program.statements(), + [EvalStmt::If { + else_branch, + .. + }] if matches!(else_branch.as_slice(), [EvalStmt::If { .. }]) + )); +} + +/// Verifies for loops lower clauses and body statements separately. +#[test] +fn parse_fragment_accepts_for_source() { + let program = parse_fragment(br#"for ($i = 2; $i; $i = $i - 1) { echo $i; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::For { + init: vec![EvalStmt::StoreVar { + name: "i".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }], + condition: Some(EvalExpr::LoadVar("i".to_string())), + update: vec![EvalStmt::StoreVar { + name: "i".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }], + body: vec![EvalStmt::Echo(EvalExpr::LoadVar("i".to_string()))], + }] + ); +} + +/// Verifies switch fragments preserve ordered case and default bodies. +#[test] +fn parse_fragment_accepts_switch_source() { + let program = + parse_fragment(br#"switch ($x) { case 1: echo "one"; break; default: echo "other"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Switch { + expr: EvalExpr::LoadVar("x".to_string()), + cases: vec![ + EvalSwitchCase { + condition: Some(EvalExpr::Const(EvalConst::Int(1))), + body: vec![ + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("one".to_string()))), + EvalStmt::Break, + ], + }, + EvalSwitchCase { + condition: None, + body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "other".to_string() + )))], + }, + ], + }] + ); +} + +/// Verifies value-only foreach loops lower to an array expression, value target, and body. +#[test] +fn parse_fragment_accepts_foreach_source() { + let program = parse_fragment(br#"foreach ($items as $item) { echo $item; }"#).expect("parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Foreach { + array: EvalExpr::LoadVar("items".to_string()), + key_name: None, + value_name: "item".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::LoadVar("item".to_string()))], + }] + ); +} + +/// Verifies key-value foreach loops preserve both loop target names in EvalIR. +#[test] +fn parse_fragment_accepts_foreach_key_value_source() { + let program = parse_fragment(br#"foreach ($items as $key => $item) { echo $key . $item; }"#) + .expect("parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Foreach { + array: EvalExpr::LoadVar("items".to_string()), + key_name: Some("key".to_string()), + value_name: "item".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("key".to_string())), + right: Box::new(EvalExpr::LoadVar("item".to_string())), + })], + }] + ); +} + +/// Verifies dynamic function declarations preserve name, parameters, and body. +#[test] +fn parse_fragment_accepts_function_declaration_source() { + let program = + parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::FunctionDecl { + name: "dyn".to_string(), + params: vec!["x".to_string()], + body: vec![EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }))], + }] + ); +} + +/// Verifies semicolon namespace declarations qualify functions and unqualified calls. +#[test] +fn parse_fragment_accepts_semicolon_namespace_source() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function dyn() { return __NAMESPACE__; } +return dyn();"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::FunctionDecl { + name: "Eval\\Ns\\dyn".to_string(), + params: Vec::new(), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( + "Eval\\Ns".to_string() + ))))], + }, + EvalStmt::Return(Some(EvalExpr::NamespacedCall { + name: "eval\\ns\\dyn".to_string(), + fallback_name: "dyn".to_string(), + args: Vec::new(), + })), + ] + ); +} + +/// Verifies braced namespace declarations restore the previous namespace afterward. +#[test] +fn parse_fragment_accepts_braced_namespace_source() { + let program = parse_fragment( + br#"namespace Eval\Block { + class Box {} + return new Box(); +} +return Box;"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::ClassDecl(EvalClass::new("Eval\\Block\\Box", Vec::new(), Vec::new())), + EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Eval\\Block\\Box".to_string(), + args: Vec::new(), + })), + EvalStmt::Return(Some(EvalExpr::ConstFetch("Box".to_string()))), + ] + ); +} + +/// Verifies namespace import declarations resolve functions, constants, and class aliases. +#[test] +fn parse_fragment_accepts_namespace_use_imports() { + let program = parse_fragment( + br#"namespace Eval\UseNs; +use function Lib\strlen as Alias; +use const Lib\VALUE as LocalValue; +use Lib\Box as BoxAlias; +return Alias(LocalValue, new BoxAlias\Inner());"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\strlen".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\VALUE".to_string())), + EvalCallArg::positional(EvalExpr::NewObject { + class_name: "Lib\\Box\\Inner".to_string(), + args: Vec::new(), + }), + ], + }))] + ); +} + +/// Verifies grouped namespace imports resolve functions, constants, and class aliases. +#[test] +fn parse_fragment_accepts_grouped_namespace_use_imports() { + let program = parse_fragment( + br#"namespace Eval\UseNs; +use Lib\{Box as BoxAlias, Sub\Thing, function imported_func as Alias}; +use const Lib\{VALUE as LocalValue, OTHER}; +return Alias(LocalValue, OTHER, new BoxAlias\Inner(), new Thing());"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\imported_func".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\VALUE".to_string())), + EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\OTHER".to_string())), + EvalCallArg::positional(EvalExpr::NewObject { + class_name: "Lib\\Box\\Inner".to_string(), + args: Vec::new(), + }), + EvalCallArg::positional(EvalExpr::NewObject { + class_name: "Lib\\Sub\\Thing".to_string(), + args: Vec::new(), + }), + ], + }))] + ); +} + +/// Verifies typed grouped namespace imports reject mixed per-entry kinds. +#[test] +fn parse_fragment_rejects_mixed_kind_typed_grouped_use_imports() { + assert_eq!( + parse_fragment(br#"use function Lib\{target, const VALUE};"#), + Err(EvalParseError::UnexpectedToken) + ); +} + +/// Verifies namespace blocks restore imports when control returns to the outer namespace. +#[test] +fn parse_fragment_restores_use_imports_after_namespace_block() { + let program = parse_fragment( + br#"namespace Eval\Outer; +use function Lib\outer_func; +namespace Eval\Block { + use function Lib\inner_func as alias; + return alias(); +} +return outer_func();"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\inner_func".to_string(), + args: Vec::new(), + })), + EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\outer_func".to_string(), + args: Vec::new(), + })), + ] + ); +} + +/// Verifies imported aliases remain visible while parsing eval-declared function bodies. +#[test] +fn parse_fragment_applies_use_imports_inside_function_body() { + let program = parse_fragment( + br#"namespace Eval\UseNs; +use function Lib\target as alias; +function dyn() { return alias(); }"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::FunctionDecl { + name: "Eval\\UseNs\\dyn".to_string(), + params: Vec::new(), + body: vec![EvalStmt::Return(Some(EvalExpr::Call { + name: "lib\\target".to_string(), + args: Vec::new(), + }))], + }] + ); +} + +/// Verifies import declarations are rejected inside eval-declared function bodies. +#[test] +fn parse_fragment_rejects_use_import_inside_function_body() { + assert_eq!( + parse_fragment(br#"function dyn() { use function Lib\target; }"#), + Err(EvalParseError::UnsupportedConstruct) + ); +} + +/// Verifies static local declarations preserve the target name and initializer expression. +#[test] +fn parse_fragment_accepts_static_var_source() { + let program = parse_fragment(br#"static $n = 1 + 1;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StaticVar { + name: "n".to_string(), + init: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::Const(EvalConst::Int(1))), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }] + ); +} + +/// Verifies global declarations preserve source-order variable names. +#[test] +fn parse_fragment_accepts_global_source() { + let program = parse_fragment(br#"global $left, $right;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Global { + vars: vec!["left".to_string(), "right".to_string()], + }] + ); +} + +/// Verifies eval magic constants lower to explicit EvalIR nodes with fragment line metadata. +#[test] +fn parse_fragment_accepts_magic_constants() { + let program = + parse_fragment(b"\nreturn __line__ . __FUNCTION__;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Magic(EvalMagicConst::Line(2))), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Function)), + }))] + ); +} + +/// Verifies file-dependent eval magic constants lower to EvalIR nodes. +#[test] +fn parse_fragment_accepts_file_magic_constants() { + let program = parse_fragment(b"return __FILE__ . __dir__;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Magic(EvalMagicConst::File)), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Dir)), + }))] + ); +} + +/// Verifies eval scope magic constants lower with namespace resolved at parse time. +#[test] +fn parse_fragment_accepts_scope_magic_constants() { + let program = parse_fragment(b"return __CLASS__ . __NAMESPACE__ . __TRAIT__ . __METHOD__;") + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Magic(EvalMagicConst::Class)), + right: Box::new(EvalExpr::Const(EvalConst::String(String::new()))), + }), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Trait)), + }), + right: Box::new(EvalExpr::Magic(EvalMagicConst::Method)), + }))] + ); +} + +/// Verifies PHP comments are skipped while preserving fragment line numbers. +#[test] +fn parse_fragment_skips_comments_and_preserves_line_metadata() { + let program = parse_fragment(b"// leading\n# hash\n/* block\ncomment */ return __LINE__;") + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Magic( + EvalMagicConst::Line(4) + )))] + ); +} + +/// Verifies unterminated block comments fail before partial EvalIR is returned. +#[test] +fn parse_fragment_rejects_unterminated_block_comment() { + assert_eq!( + parse_fragment(b"/* open").unwrap_err(), + EvalParseError::UnterminatedComment + ); +} + +/// Verifies comparison operators parse with lower precedence than arithmetic. +#[test] +fn parse_fragment_accepts_comparison_source() { + let program = parse_fragment(br#"return $i + 1 < 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Lt, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} + +/// Verifies the spaceship operator parses at ordered-comparison precedence. +#[test] +fn parse_fragment_accepts_spaceship_source() { + let program = parse_fragment(br#"return $i + 1 <=> 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Spaceship, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} + +/// Verifies loose equality operators parse as binary EvalIR expressions. +#[test] +fn parse_fragment_accepts_loose_equality_source() { + let program = parse_fragment(br#"return "a" != "b";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LooseNotEq, + left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String("b".to_string()))), + }))] + ); +} + +/// Verifies strict equality operators parse as distinct EvalIR comparisons. +#[test] +fn parse_fragment_accepts_strict_equality_source() { + let program = + parse_fragment(br#"return "10" === "10" && "10" !== 10;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::StrictEq, + left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + }), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::StrictNotEq, + left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::Int(10))), + }), + }))] + ); +} + +/// Verifies logical operators parse with `&&` binding tighter than `||`. +#[test] +fn parse_fragment_accepts_short_circuit_logical_source() { + let program = parse_fragment(br#"return $a && $b || false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::LoadVar("a".to_string())), + right: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} + +/// Verifies PHP logical keywords parse case-insensitively with their own precedence. +#[test] +fn parse_fragment_accepts_keyword_logical_source() { + let program = + parse_fragment(br#"return false || true AnD false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} + +/// Verifies PHP `xor` binds between `or` and `and` in eval expressions. +#[test] +fn parse_fragment_accepts_keyword_xor_source() { + let program = + parse_fragment(br#"return true XoR false or false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} + +/// Verifies ternary expressions parse below logical OR and preserve both branches. +#[test] +fn parse_fragment_accepts_ternary_source() { + let program = + parse_fragment(br#"return $a || $b ? "yes" : "no";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::LoadVar("a".to_string())), + right: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))), + else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), + }))] + ); +} + +/// Verifies PHP's short ternary form omits the explicit then branch in EvalIR. +#[test] +fn parse_fragment_accepts_short_ternary_source() { + let program = parse_fragment(br#"return $name ?: "fallback";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::LoadVar("name".to_string())), + then_branch: None, + else_branch: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), + }))] + ); +} + +/// Verifies null coalescing parses as a right-associative expression. +#[test] +fn parse_fragment_accepts_null_coalesce_source() { + let program = + parse_fragment(br#"return $a ?? $b ?? "fallback";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("a".to_string())), + default: Box::new(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("b".to_string())), + default: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), + }), + }))] + ); +} + +/// Verifies match expressions preserve subject, patterns, and default expression. +#[test] +fn parse_fragment_accepts_match_source() { + let program = parse_fragment(br#"return match ($x) { 1, 2 => "small", default => "other" };"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Match { + subject: Box::new(EvalExpr::LoadVar("x".to_string())), + arms: vec![EvalMatchArm { + patterns: vec![ + EvalExpr::Const(EvalConst::Int(1)), + EvalExpr::Const(EvalConst::Int(2)), + ], + value: EvalExpr::Const(EvalConst::String("small".to_string())), + }], + default: Some(Box::new(EvalExpr::Const(EvalConst::String( + "other".to_string() + )))), + }))] + ); +} + +/// Verifies null coalescing binds tighter than PHP ternary expressions. +#[test] +fn parse_fragment_null_coalesce_binds_tighter_than_ternary() { + let program = + parse_fragment(br#"return $a ?? $b ? "yes" : "no";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("a".to_string())), + default: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))), + else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), + }))] + ); +} + +/// Verifies logical negation parses as a unary expression before comparisons. +#[test] +fn parse_fragment_accepts_logical_not_source() { + let program = parse_fragment(br#"return !$flag == true;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LooseEq, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(EvalExpr::LoadVar("flag".to_string())), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + }))] + ); +} + +/// Verifies unary numeric operators bind tighter than multiplication. +#[test] +fn parse_fragment_accepts_unary_numeric_source() { + let program = parse_fragment(br#"return -$x * +2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(EvalExpr::LoadVar("x".to_string())), + }), + right: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + }))] + ); +} + +/// Verifies print fragments lower to expression-form print with the printed value. +#[test] +fn parse_fragment_accepts_print_source() { + let program = parse_fragment(br#"print "hi";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Expr(EvalExpr::Print(Box::new(EvalExpr::Const( + EvalConst::String("hi".to_string()) + ))))] + ); +} + +/// Verifies single- and double-quoted strings keep PHP-compatible simple escapes. +#[test] +fn parse_fragment_preserves_php_string_escape_semantics() { + let program = parse_fragment(br#"return ['A\nB', "A\qB", "A\v\e\fB", 'It\'s'];"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("A\\nB".to_string()))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("A\\qB".to_string()))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( + "A\x0b\x1b\x0cB".to_string() + ))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("It's".to_string()))), + ])))] + ); +} + +/// Verifies call expressions preserve their callee name and source-order arguments. +#[test] +fn parse_fragment_accepts_call_expression_source() { + let program = parse_fragment(br#"return eval("return 1;");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "eval".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "return 1;".to_string() + )))], + }))] + ); +} + +/// Verifies include and require constructs parse as expressions with path metadata. +#[test] +fn parse_fragment_accepts_include_require_expression_source() { + let program = parse_fragment(br#"return include "a" . ".php"; require_once("b.php");"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Include { + path: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String(".php".to_string()))), + }), + required: false, + once: false, + })), + EvalStmt::Expr(EvalExpr::Include { + path: Box::new(EvalExpr::Const(EvalConst::String("b.php".to_string()))), + required: true, + once: true, + }), + ] + ); +} + +/// Verifies explicitly qualified call expressions normalize away the leading slash. +#[test] +fn parse_fragment_accepts_qualified_call_expression_source() { + let program = parse_fragment(br#"return \strlen("abcd");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "strlen".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "abcd".to_string() + )))], + }))] + ); +} + +/// Verifies variable callable expressions lower to dynamic calls with source-order args. +#[test] +fn parse_fragment_accepts_dynamic_call_expression_source() { + let program = + parse_fragment(br#"return $fn(first: "a", ...$rest);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicCall { + callee: Box::new(EvalExpr::LoadVar("fn".to_string())), + args: vec![ + EvalCallArg::named("first", EvalExpr::Const(EvalConst::String("a".to_string())),), + EvalCallArg::spread(EvalExpr::LoadVar("rest".to_string())), + ], + }))] + ); +} + +/// Verifies dynamic calls can be applied after another postfix expression. +#[test] +fn parse_fragment_accepts_postfix_dynamic_call_source() { + let program = + parse_fragment(br#"return $callbacks[0]("abcd");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicCall { + callee: Box::new(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("callbacks".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "abcd".to_string() + )))], + }))] + ); +} + +/// Verifies bare constant names lower to dynamic constant-fetch expressions. +#[test] +fn parse_fragment_accepts_constant_fetch_source() { + let program = parse_fragment(br#"return \Dyn\EvalConst;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ConstFetch( + "Dyn\\EvalConst".to_string() + )))] + ); +} + +/// Verifies function calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_call_argument_source() { + let program = parse_fragment(br#"return add(y: 2, x: 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "add".to_string(), + args: vec![ + EvalCallArg::named("y", EvalExpr::Const(EvalConst::Int(2))), + EvalCallArg::named("x", EvalExpr::Const(EvalConst::Int(1))), + ], + }))] + ); +} + +/// Verifies function calls preserve spread arguments in source order. +#[test] +fn parse_fragment_accepts_spread_call_argument_source() { + let program = parse_fragment(br#"return add(...$args);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "add".to_string(), + args: vec![EvalCallArg::spread(EvalExpr::LoadVar("args".to_string()))], + }))] + ); +} + +/// Verifies `isset` parses as a case-insensitive function-like expression. +#[test] +fn parse_fragment_accepts_isset_source() { + let program = + parse_fragment(br#"return ISSET($x, $items["k"]);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "isset".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("items".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), + }), + ], + }))] + ); +} + +/// Verifies `empty` parses as a case-insensitive function-like expression. +#[test] +fn parse_fragment_accepts_empty_source() { + let program = parse_fragment(br#"return EMPTY($items["k"]);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "empty".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("items".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), + })], + }))] + ); +} + +/// Verifies indexed array literals and reads parse as runtime array expressions. +#[test] +fn parse_fragment_accepts_indexed_array_read_source() { + let program = parse_fragment(br#"return [1, 2][0];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(2))), + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }))] + ); +} + +/// Verifies legacy `array(...)` literals parse through the same EvalIR array node. +#[test] +fn parse_fragment_accepts_legacy_array_literal_source() { + let program = + parse_fragment(br#"return array(1, "name" => "Ada",)[1];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + }, + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }))] + ); +} + +/// Verifies associative array literals preserve explicit key/value expressions. +#[test] +fn parse_fragment_accepts_assoc_array_literal_source() { + let program = parse_fragment(br#"return ["name" => "Ada"];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + } + ])))] + ); +} + +/// Verifies indexed array writes parse as variable-target array set statements. +#[test] +fn parse_fragment_accepts_indexed_array_write_source() { + let program = parse_fragment(br#"$items[1] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArraySetVar { + name: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(1)), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} + +/// Verifies indexed array append syntax parses as a variable-target append statement. +#[test] +fn parse_fragment_accepts_indexed_array_append_source() { + let program = parse_fragment(br#"$items[] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} + +/// Verifies array append syntax is accepted inside `for` update clauses. +#[test] +fn parse_fragment_accepts_array_append_in_for_update_source() { + let program = parse_fragment(br#"for ($i = 0; $i < 2; $items[] = $i) { $i += 1; }"#) + .expect("fragment should parse"); + let [EvalStmt::For { update, .. }] = program.statements() else { + panic!("expected for statement"); + }; + assert_eq!( + update, + &vec![EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::LoadVar("i".to_string()), + }] + ); +} + +/// Verifies object property reads parse as postfix EvalIR expressions. +#[test] +fn parse_fragment_accepts_property_read_source() { + let program = parse_fragment(br#"return $this->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + ); +} + +/// Verifies property names preserve source case while keywords remain case-insensitive. +#[test] +fn parse_fragment_preserves_property_case_source() { + let program = parse_fragment(br#"RETURN $this->camelName;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "camelName".to_string(), + }))] + ); +} + +/// Verifies object method calls parse as postfix EvalIR call expressions. +#[test] +fn parse_fragment_accepts_method_call_source() { + let program = parse_fragment(br#"return $this->Answer();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "answer".to_string(), + args: Vec::new(), + }))] + ); +} + +/// Verifies object construction parses as a named EvalIR expression. +#[test] +fn parse_fragment_accepts_new_object_source() { + let program = parse_fragment(br#"return new Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Box".to_string(), + args: Vec::new(), + }))] + ); +} + +/// Verifies object construction accepts explicitly qualified class names. +#[test] +fn parse_fragment_accepts_qualified_new_object_source() { + let program = parse_fragment(br#"return new \EvalNs\Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "EvalNs\\Box".to_string(), + args: Vec::new(), + }))] + ); +} + +/// Verifies object method calls preserve source-order argument expressions. +#[test] +fn parse_fragment_accepts_method_call_args_source() { + let program = parse_fragment(br#"return $this->add($x + 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "add".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + })], + }))] + ); +} + +/// Verifies object method calls parse multiple argument expressions in source order. +#[test] +fn parse_fragment_accepts_method_call_multiple_args_source() { + let program = + parse_fragment(br#"return $this->label($x, "ok");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "label".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::Const(EvalConst::String("ok".to_string()))), + ], + }))] + ); +} + +/// Verifies object property writes parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_write_source() { + let program = parse_fragment(br#"$this->x = $this->x + 1;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }] + ); +} + +/// Verifies while fragments lower to loop statements with a nested block. +#[test] +fn parse_fragment_accepts_while_source() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::While { + condition: EvalExpr::LoadVar("flag".to_string()), + body: vec![ + EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), + EvalStmt::StoreVar { + name: "flag".to_string(), + value: EvalExpr::Const(EvalConst::Bool(false)), + }, + ], + }] + ); +} + +/// Verifies do/while fragments lower to body-first loop statements. +#[test] +fn parse_fragment_accepts_do_while_source() { + let program = parse_fragment(br#"do { echo $flag; $flag = false; } while ($flag);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DoWhile { + body: vec![ + EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), + EvalStmt::StoreVar { + name: "flag".to_string(), + value: EvalExpr::Const(EvalConst::Bool(false)), + }, + ], + condition: EvalExpr::LoadVar("flag".to_string()), + }] + ); +} + +/// Verifies loop control statements parse inside while blocks. +#[test] +fn parse_fragment_accepts_break_and_continue_source() { + let program = + parse_fragment(br#"while ($flag) { continue; break; }"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::While { + condition: EvalExpr::LoadVar("flag".to_string()), + body: vec![EvalStmt::Continue, EvalStmt::Break], + }] + ); +} + +/// Verifies return fragments parse optional return expressions. +#[test] +fn parse_fragment_accepts_return_source() { + let program = parse_fragment(b"return ($x - 1) * 4;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }))] + ); +} + +/// Verifies throw statements lower to a Throwable expression carried by EvalIR. +#[test] +fn parse_fragment_accepts_throw_source() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })] + ); +} + +/// Verifies try/catch statements lower supported Throwable clauses into EvalIR. +#[test] +fn parse_fragment_accepts_try_catch_throwable_source() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} + +/// Verifies class imports can alias the supported Throwable catch type. +#[test] +fn parse_fragment_accepts_try_catch_imported_throwable_alias() { + let program = parse_fragment( + br#"use Throwable as T; +try { + throw $e; +} catch (T $caught) { + echo "caught"; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::LoadVar("e".to_string()))], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "caught".to_string() + )))], + }], + finally_body: Vec::new(), + }] + ); +} + +/// Verifies Throwable catch clauses can omit the catch variable like PHP. +#[test] +fn parse_fragment_accepts_try_catch_without_variable() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: None, + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} + +/// Verifies single catch type narrowing lowers into EvalIR. +#[test] +fn parse_fragment_accepts_specific_eval_catch_type() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Exception $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Exception".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} + +/// Verifies union catch type narrowing lowers all source-order types into one clause. +#[test] +fn parse_fragment_accepts_union_eval_catch_type() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable|Exception $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string(), "Exception".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} + +/// Verifies try/finally statements lower the finalizer block into EvalIR. +#[test] +fn parse_fragment_accepts_eval_finally_source() { + let program = parse_fragment(br#"try { return 1; } finally { echo "finally"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + catches: Vec::new(), + finally_body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "finally".to_string() + )))], + }] + ); +} + +/// Verifies unset fragments expand to one by-name unset statement per variable. +#[test] +fn parse_fragment_accepts_unset_source() { + let program = parse_fragment(b"unset($x, $y);").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetVar { + name: "x".to_string() + }, + EvalStmt::UnsetVar { + name: "y".to_string() + }, + ] + ); +} + +/// Verifies eval fragments reject PHP opening tags. +#[test] +fn parse_fragment_rejects_opening_tag() { + assert_eq!( + parse_fragment(b"x; } }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalSupported", + vec![EvalClassProperty::new( + "x", + Some(EvalExpr::Const(EvalConst::Int(1))) + )], + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + )] + ))] + ); +} + +/// Verifies malformed object construction reports an unexpected token. +#[test] +fn parse_fragment_rejects_new_without_class_name() { + assert_eq!( + parse_fragment(b"return new ();"), + Err(EvalParseError::UnexpectedToken) + ); +} + +/// Verifies unsupported expression keywords report the unsupported construct status. +#[test] +fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { + for source in [ + b"return clone $value;" as &[u8], + b"return yield 1;" as &[u8], + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies malformed statements report parse errors instead of partial IR. +#[test] +fn parse_fragment_rejects_missing_semicolon() { + assert_eq!( + parse_fragment(b"$x = 1"), + Err(EvalParseError::ExpectedSemicolon) + ); +} From 9043112a1dbc96bef6f3ac39b9246cc4d1905ffb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 09:51:42 +0200 Subject: [PATCH 0279/1208] Split eval runtime hooks modules --- .../elephc-eval/src/runtime_hooks/externs.rs | 171 ++++++++++++ crates/elephc-eval/src/runtime_hooks/mod.rs | 62 +++++ .../ops.rs} | 256 +----------------- crates/elephc-eval/src/runtime_hooks/tags.rs | 74 +++++ 4 files changed, 316 insertions(+), 247 deletions(-) create mode 100644 crates/elephc-eval/src/runtime_hooks/externs.rs create mode 100644 crates/elephc-eval/src/runtime_hooks/mod.rs rename crates/elephc-eval/src/{runtime_hooks.rs => runtime_hooks/ops.rs} (67%) create mode 100644 crates/elephc-eval/src/runtime_hooks/tags.rs diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs new file mode 100644 index 0000000000..c8095b9b1d --- /dev/null +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -0,0 +1,171 @@ +//! Purpose: +//! Declares generated C-ABI runtime wrapper symbols consumed by eval hooks. +//! These declarations are grouped separately so operation code can stay focused +//! on RuntimeValueOps behavior rather than linkage inventory. +//! +//! Called from: +//! - `crate::runtime_hooks::ops` runtime adapter methods. +//! - `crate::runtime_hooks::ElephcRuntimeOps` shared argument packing helpers. +//! +//! Key details: +//! - Symbols are provided by the main elephc runtime object when eval is enabled. +//! - Null return pointers are translated to `EvalStatus::RuntimeFatal` by callers. + +use crate::value::RuntimeCell; + +#[cfg(not(test))] +unsafe extern "C" { + pub(super) fn __elephc_eval_value_array_new(capacity: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_assoc_new(capacity: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_array_get( + array: *mut RuntimeCell, + index: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_array_key_exists( + key: *mut RuntimeCell, + array: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_array_iter_key( + array: *mut RuntimeCell, + position: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_array_set( + array: *mut RuntimeCell, + index: *mut RuntimeCell, + value: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_property_get( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_property_set( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + value: *mut RuntimeCell, + ) -> u64; + pub(super) fn __elephc_eval_value_object_property_len(object: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_object_property_iter_key( + object: *mut RuntimeCell, + position: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_method_call( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + args: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_new_object( + name_ptr: *const u8, + name_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_construct_object( + object: *mut RuntimeCell, + args: *mut RuntimeCell, + ) -> u64; + pub(super) fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; + pub(super) fn __elephc_eval_interface_exists(name_ptr: *const u8, name_len: u64) -> u64; + pub(super) fn __elephc_eval_value_is_a( + object_or_class: *mut RuntimeCell, + target_ptr: *const u8, + target_len: u64, + exclude_self: u64, + ) -> u64; + pub(super) fn __elephc_eval_value_object_class_name( + object: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_parent_class_name( + object_or_class: *mut RuntimeCell, + ) -> *mut RuntimeCell; + /// Returns whether generated trait metadata contains the requested PHP name. + pub(super) fn __elephc_eval_trait_exists(name_ptr: *const u8, name_len: u64) -> u64; + /// Returns whether generated enum metadata contains the requested PHP name. + pub(super) fn __elephc_eval_enum_exists(name_ptr: *const u8, name_len: u64) -> u64; + pub(super) fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_type_tag(value: *mut RuntimeCell) -> u64; + /// Returns the unboxed object payload pointer for object-tagged eval values. + pub(super) fn __elephc_eval_value_object_identity(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_warning(message_ptr: *const u8, message_len: u64); + pub(super) fn __elephc_eval_value_null() -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_float(value: f64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_string(ptr: *const u8, len: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_cast_int(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_cast_float(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_cast_string(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_cast_bool(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_abs(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_ceil(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_floor(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_sqrt(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_strrev(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_fdiv( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_fmod( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_add( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_sub( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_mul( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_div( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_mod( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_pow( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_round( + value: *mut RuntimeCell, + precision: *mut RuntimeCell, + has_precision: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_bitwise( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + op: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_bit_not(value: *mut RuntimeCell) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_concat( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_compare( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + op: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_spaceship( + left: *mut RuntimeCell, + right: *mut RuntimeCell, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_echo(value: *mut RuntimeCell); + pub(super) fn __elephc_eval_value_string_bytes( + value: *mut RuntimeCell, + out_ptr: *mut *const u8, + out_len: *mut u64, + ) -> u64; + pub(super) fn __elephc_eval_value_truthy(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_release(value: *mut RuntimeCell); + pub(super) fn __elephc_eval_value_retain(value: *mut RuntimeCell) -> *mut RuntimeCell; +} diff --git a/crates/elephc-eval/src/runtime_hooks/mod.rs b/crates/elephc-eval/src/runtime_hooks/mod.rs new file mode 100644 index 0000000000..424b43eff5 --- /dev/null +++ b/crates/elephc-eval/src/runtime_hooks/mod.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Bridges EvalIR value operations to elephc runtime values. +//! The module wires the stateless adapter type while focused submodules own +//! generated C-ABI symbols, trait operations, and opcode mappings. +//! +//! Called from: +//! - `crate::ffi::execute::__elephc_eval_execute()` in non-test builds. +//! +//! Key details: +//! - The wrapper symbols adapt to elephc's target-specific internal helper ABI. +//! - Unit tests do not link the generated runtime object, so real hooks compile +//! only outside `cfg(test)`. + +#[cfg(not(test))] +mod externs; +#[cfg(not(test))] +mod ops; +#[cfg(not(test))] +mod tags; + +#[cfg(not(test))] +use crate::errors::EvalStatus; +#[cfg(not(test))] +use crate::value::{RuntimeCell, RuntimeCellHandle}; +#[cfg(not(test))] +use externs::{ + __elephc_eval_value_array_new, __elephc_eval_value_array_set, __elephc_eval_value_int, +}; + +/// Runtime hook adapter that produces and consumes boxed elephc Mixed cells. +#[cfg(not(test))] +pub struct ElephcRuntimeOps; + +#[cfg(not(test))] +impl ElephcRuntimeOps { + /// Creates a new stateless runtime hook adapter. + pub const fn new() -> Self { + Self + } + + /// Converts a runtime wrapper result into an interpreter handle. + fn handle(ptr: *mut RuntimeCell) -> Result { + if ptr.is_null() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(RuntimeCellHandle::from_raw(ptr)) + } + } + + /// Packs source-order argument cells into the boxed eval array ABI. + fn arg_array(args: Vec) -> Result { + let arg_array = unsafe { __elephc_eval_value_array_new(args.len() as u64) }; + let arg_array = Self::handle(arg_array)?; + for (index, value) in args.into_iter().enumerate() { + let index = Self::handle(unsafe { __elephc_eval_value_int(index as i64) })?; + Self::handle(unsafe { + __elephc_eval_value_array_set(arg_array.as_ptr(), index.as_ptr(), value.as_ptr()) + })?; + } + Ok(arg_array) + } +} diff --git a/crates/elephc-eval/src/runtime_hooks.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs similarity index 67% rename from crates/elephc-eval/src/runtime_hooks.rs rename to crates/elephc-eval/src/runtime_hooks/ops.rs index 2d2ea0ce04..9d20e40716 100644 --- a/crates/elephc-eval/src/runtime_hooks.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -1,197 +1,21 @@ //! Purpose: -//! Bridges EvalIR value operations to elephc runtime values. -//! Calls C-ABI wrapper symbols emitted by the main runtime object when eval is -//! enabled, avoiding a duplicate PHP value representation inside this crate. +//! Implements RuntimeValueOps by delegating each eval value operation to the +//! generated elephc runtime wrapper symbols. //! //! Called from: -//! - `crate::__elephc_eval_execute()` in non-test builds. +//! - `crate::interpreter` when executing EvalIR in non-test builds. //! //! Key details: -//! - The wrapper symbols adapt to elephc's target-specific internal helper ABI. -//! - Unit tests do not link the generated runtime object, so this module's real -//! hook implementation is compiled only outside `cfg(test)`. +//! - Every returned runtime pointer is checked before becoming a handle. +//! - Temporary argument arrays are released after object and method bridge calls. -#[cfg(not(test))] +use super::externs::*; +use super::tags::{bitwise_op_tag, compare_op_tag}; +use super::ElephcRuntimeOps; use crate::errors::EvalStatus; -#[cfg(not(test))] use crate::eval_ir::EvalBinOp; -#[cfg(not(test))] use crate::interpreter::RuntimeValueOps; -#[cfg(not(test))] -use crate::value::{RuntimeCell, RuntimeCellHandle}; - -#[cfg(not(test))] -unsafe extern "C" { - fn __elephc_eval_value_array_new(capacity: u64) -> *mut RuntimeCell; - fn __elephc_eval_value_assoc_new(capacity: u64) -> *mut RuntimeCell; - fn __elephc_eval_value_array_get( - array: *mut RuntimeCell, - index: *mut RuntimeCell, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_array_key_exists( - key: *mut RuntimeCell, - array: *mut RuntimeCell, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_array_iter_key( - array: *mut RuntimeCell, - position: u64, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_array_set( - array: *mut RuntimeCell, - index: *mut RuntimeCell, - value: *mut RuntimeCell, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_property_get( - object: *mut RuntimeCell, - name_ptr: *const u8, - name_len: u64, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_property_set( - object: *mut RuntimeCell, - name_ptr: *const u8, - name_len: u64, - value: *mut RuntimeCell, - ) -> u64; - fn __elephc_eval_value_object_property_len(object: *mut RuntimeCell) -> u64; - fn __elephc_eval_value_object_property_iter_key( - object: *mut RuntimeCell, - position: u64, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_method_call( - object: *mut RuntimeCell, - name_ptr: *const u8, - name_len: u64, - args: *mut RuntimeCell, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_new_object(name_ptr: *const u8, name_len: u64) -> *mut RuntimeCell; - fn __elephc_eval_value_construct_object( - object: *mut RuntimeCell, - args: *mut RuntimeCell, - ) -> u64; - fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; - fn __elephc_eval_interface_exists(name_ptr: *const u8, name_len: u64) -> u64; - fn __elephc_eval_value_is_a( - object_or_class: *mut RuntimeCell, - target_ptr: *const u8, - target_len: u64, - exclude_self: u64, - ) -> u64; - fn __elephc_eval_value_object_class_name(object: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_parent_class_name(object_or_class: *mut RuntimeCell) - -> *mut RuntimeCell; - /// Returns whether generated trait metadata contains the requested PHP name. - fn __elephc_eval_trait_exists(name_ptr: *const u8, name_len: u64) -> u64; - /// Returns whether generated enum metadata contains the requested PHP name. - fn __elephc_eval_enum_exists(name_ptr: *const u8, name_len: u64) -> u64; - fn __elephc_eval_value_array_len(array: *mut RuntimeCell) -> u64; - fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; - fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; - fn __elephc_eval_value_type_tag(value: *mut RuntimeCell) -> u64; - /// Returns the unboxed object payload pointer for object-tagged eval values. - fn __elephc_eval_value_object_identity(value: *mut RuntimeCell) -> u64; - fn __elephc_eval_warning(message_ptr: *const u8, message_len: u64); - fn __elephc_eval_value_null() -> *mut RuntimeCell; - fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; - fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; - fn __elephc_eval_value_float(value: f64) -> *mut RuntimeCell; - fn __elephc_eval_value_string(ptr: *const u8, len: u64) -> *mut RuntimeCell; - fn __elephc_eval_value_cast_int(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_cast_float(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_cast_string(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_cast_bool(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_abs(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_ceil(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_floor(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_sqrt(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_strrev(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_fdiv( - left: *mut RuntimeCell, - right: *mut RuntimeCell, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_fmod( - left: *mut RuntimeCell, - right: *mut RuntimeCell, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_add(left: *mut RuntimeCell, right: *mut RuntimeCell) - -> *mut RuntimeCell; - fn __elephc_eval_value_sub(left: *mut RuntimeCell, right: *mut RuntimeCell) - -> *mut RuntimeCell; - fn __elephc_eval_value_mul(left: *mut RuntimeCell, right: *mut RuntimeCell) - -> *mut RuntimeCell; - fn __elephc_eval_value_div(left: *mut RuntimeCell, right: *mut RuntimeCell) - -> *mut RuntimeCell; - fn __elephc_eval_value_mod(left: *mut RuntimeCell, right: *mut RuntimeCell) - -> *mut RuntimeCell; - fn __elephc_eval_value_pow(left: *mut RuntimeCell, right: *mut RuntimeCell) - -> *mut RuntimeCell; - fn __elephc_eval_value_round( - value: *mut RuntimeCell, - precision: *mut RuntimeCell, - has_precision: u64, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_bitwise( - left: *mut RuntimeCell, - right: *mut RuntimeCell, - op: u64, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_bit_not(value: *mut RuntimeCell) -> *mut RuntimeCell; - fn __elephc_eval_value_concat( - left: *mut RuntimeCell, - right: *mut RuntimeCell, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_compare( - left: *mut RuntimeCell, - right: *mut RuntimeCell, - op: u64, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_spaceship( - left: *mut RuntimeCell, - right: *mut RuntimeCell, - ) -> *mut RuntimeCell; - fn __elephc_eval_value_echo(value: *mut RuntimeCell); - fn __elephc_eval_value_string_bytes( - value: *mut RuntimeCell, - out_ptr: *mut *const u8, - out_len: *mut u64, - ) -> u64; - fn __elephc_eval_value_truthy(value: *mut RuntimeCell) -> u64; - fn __elephc_eval_value_release(value: *mut RuntimeCell); - fn __elephc_eval_value_retain(value: *mut RuntimeCell) -> *mut RuntimeCell; -} - -/// Runtime hook adapter that produces and consumes boxed elephc Mixed cells. -#[cfg(not(test))] -pub struct ElephcRuntimeOps; - -#[cfg(not(test))] -impl ElephcRuntimeOps { - /// Creates a new stateless runtime hook adapter. - pub const fn new() -> Self { - Self - } - - /// Converts a runtime wrapper result into an interpreter handle. - fn handle(ptr: *mut RuntimeCell) -> Result { - if ptr.is_null() { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(RuntimeCellHandle::from_raw(ptr)) - } - } - - /// Packs source-order argument cells into the boxed eval array ABI. - fn arg_array(args: Vec) -> Result { - let arg_array = unsafe { __elephc_eval_value_array_new(args.len() as u64) }; - let arg_array = Self::handle(arg_array)?; - for (index, value) in args.into_iter().enumerate() { - let index = Self::handle(unsafe { __elephc_eval_value_int(index as i64) })?; - Self::handle(unsafe { - __elephc_eval_value_array_set(arg_array.as_ptr(), index.as_ptr(), value.as_ptr()) - })?; - } - Ok(arg_array) - } -} +use crate::value::RuntimeCellHandle; #[cfg(not(test))] impl RuntimeValueOps for ElephcRuntimeOps { @@ -702,65 +526,3 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_truthy(value.as_ptr()) != 0 }) } } - -/// Maps an EvalIR comparison operator to the bridge ABI opcode. -#[cfg(not(test))] -fn compare_op_tag(op: EvalBinOp) -> u64 { - match op { - EvalBinOp::LooseEq => 0, - EvalBinOp::LooseNotEq => 1, - EvalBinOp::Lt => 2, - EvalBinOp::LtEq => 3, - EvalBinOp::Gt => 4, - EvalBinOp::GtEq => 5, - EvalBinOp::StrictEq => 6, - EvalBinOp::StrictNotEq => 7, - EvalBinOp::Add - | EvalBinOp::Sub - | EvalBinOp::Mul - | EvalBinOp::Div - | EvalBinOp::Mod - | EvalBinOp::Pow - | EvalBinOp::BitAnd - | EvalBinOp::BitOr - | EvalBinOp::BitXor - | EvalBinOp::ShiftLeft - | EvalBinOp::ShiftRight - | EvalBinOp::Concat - | EvalBinOp::Spaceship - | EvalBinOp::LogicalAnd - | EvalBinOp::LogicalOr - | EvalBinOp::LogicalXor => 0, - } -} - -/// Maps bitwise EvalIR operators onto the generated runtime wrapper opcode table. -#[cfg(not(test))] -fn bitwise_op_tag(op: EvalBinOp) -> u64 { - match op { - EvalBinOp::BitAnd => 0, - EvalBinOp::BitOr => 1, - EvalBinOp::BitXor => 2, - EvalBinOp::ShiftLeft => 3, - EvalBinOp::ShiftRight => 4, - EvalBinOp::Add - | EvalBinOp::Sub - | EvalBinOp::Mul - | EvalBinOp::Div - | EvalBinOp::Mod - | EvalBinOp::Pow - | EvalBinOp::Concat - | EvalBinOp::LogicalAnd - | EvalBinOp::LogicalOr - | EvalBinOp::LogicalXor - | EvalBinOp::LooseEq - | EvalBinOp::LooseNotEq - | EvalBinOp::StrictEq - | EvalBinOp::StrictNotEq - | EvalBinOp::Lt - | EvalBinOp::LtEq - | EvalBinOp::Gt - | EvalBinOp::GtEq - | EvalBinOp::Spaceship => 0, - } -} diff --git a/crates/elephc-eval/src/runtime_hooks/tags.rs b/crates/elephc-eval/src/runtime_hooks/tags.rs new file mode 100644 index 0000000000..f37246bce1 --- /dev/null +++ b/crates/elephc-eval/src/runtime_hooks/tags.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Maps EvalIR binary operators onto generated runtime wrapper opcode tables. +//! The runtime wrappers expect compact numeric tags rather than Rust enum +//! discriminants. +//! +//! Called from: +//! - `crate::runtime_hooks::ops` comparison and bitwise operations. +//! +//! Key details: +//! - Non-matching operators map to zero because callers only pass matching groups. + +use crate::eval_ir::EvalBinOp; + +/// Maps an EvalIR comparison operator to the bridge ABI opcode. +#[cfg(not(test))] +pub(super) fn compare_op_tag(op: EvalBinOp) -> u64 { + match op { + EvalBinOp::LooseEq => 0, + EvalBinOp::LooseNotEq => 1, + EvalBinOp::Lt => 2, + EvalBinOp::LtEq => 3, + EvalBinOp::Gt => 4, + EvalBinOp::GtEq => 5, + EvalBinOp::StrictEq => 6, + EvalBinOp::StrictNotEq => 7, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Pow + | EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight + | EvalBinOp::Concat + | EvalBinOp::Spaceship + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor => 0, + } +} + +/// Maps bitwise EvalIR operators onto the generated runtime wrapper opcode table. +#[cfg(not(test))] +pub(super) fn bitwise_op_tag(op: EvalBinOp) -> u64 { + match op { + EvalBinOp::BitAnd => 0, + EvalBinOp::BitOr => 1, + EvalBinOp::BitXor => 2, + EvalBinOp::ShiftLeft => 3, + EvalBinOp::ShiftRight => 4, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Pow + | EvalBinOp::Concat + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor + | EvalBinOp::LooseEq + | EvalBinOp::LooseNotEq + | EvalBinOp::StrictEq + | EvalBinOp::StrictNotEq + | EvalBinOp::Lt + | EvalBinOp::LtEq + | EvalBinOp::Gt + | EvalBinOp::GtEq + | EvalBinOp::Spaceship => 0, + } +} From a1f1666be88d01dfcc0f781660ba6033722cbe7d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 09:53:12 +0200 Subject: [PATCH 0280/1208] Move eval interpreter tests into module --- .../{interpreter.rs => interpreter/mod.rs} | 7273 +--------------- crates/elephc-eval/src/interpreter/tests.rs | 7279 +++++++++++++++++ 2 files changed, 7282 insertions(+), 7270 deletions(-) rename crates/elephc-eval/src/{interpreter.rs => interpreter/mod.rs} (64%) create mode 100644 crates/elephc-eval/src/interpreter/tests.rs diff --git a/crates/elephc-eval/src/interpreter.rs b/crates/elephc-eval/src/interpreter/mod.rs similarity index 64% rename from crates/elephc-eval/src/interpreter.rs rename to crates/elephc-eval/src/interpreter/mod.rs index c99775ca4d..c07b975d1e 100644 --- a/crates/elephc-eval/src/interpreter.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -267,7 +267,7 @@ const EVAL_WEEKDAY_NAMES: &[&str; 7] = &[ const EVAL_WEEKDAY_SHORT_NAMES: &[&str; 7] = &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; /// Root package manifest used to mirror native `phpversion()` in the eval crate. -const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../Cargo.toml"); +const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../../Cargo.toml"); unsafe extern "C" { /// Reverse-resolves one socket address through libc's `gethostbyaddr`. @@ -15867,7273 +15867,6 @@ pub fn current_stub_status() -> EvalStatus { EvalStatus::UnsupportedConstruct } -#[cfg(test)] -mod tests { - use std::collections::HashMap; - use std::ffi::c_void; - - use crate::parser::parse_fragment; - use crate::value::RuntimeCell; - - use super::*; - - /// Test-only array key representation for fake indexed and associative arrays. - #[derive(Clone, Debug, PartialEq, Eq, Hash)] - enum FakeKey { - Int(i64), - String(String), - } - - /// Test-only runtime value representation used behind opaque cell handles. - #[derive(Clone, Debug, PartialEq)] - enum FakeValue { - Null, - Bool(bool), - Int(i64), - Float(f64), - String(String), - Bytes(Vec), - Array(Vec), - Assoc(Vec<(FakeKey, RuntimeCellHandle)>), - Object(Vec<(String, RuntimeCellHandle)>), - Iterator { len: i64, position: i64 }, - Resource(i64), - } - - /// Test runtime hooks that allocate stable fake handles and record echo output. - #[derive(Default)] - struct FakeOps { - next_id: usize, - values: HashMap, - output: String, - releases: Vec, - warnings: Vec, - } - - impl FakeOps { - /// Allocates one fake runtime cell and returns its opaque handle. - fn alloc(&mut self, value: FakeValue) -> RuntimeCellHandle { - self.next_id += 1; - let id = self.next_id; - self.values.insert(id, value); - RuntimeCellHandle::from_raw(id as *mut RuntimeCell) - } - - /// Reads a fake runtime cell by opaque handle. - fn get(&self, handle: RuntimeCellHandle) -> FakeValue { - let id = handle.as_ptr() as usize; - self.values.get(&id).cloned().expect("fake cell missing") - } - - /// Converts a fake runtime cell into a normalized fake PHP array key. - fn key(&self, handle: RuntimeCellHandle) -> Result { - let value = self.get(handle); - match value { - FakeValue::Int(value) => Ok(FakeKey::Int(value)), - FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) - .map(FakeKey::Int) - .map_or_else(|| Ok(FakeKey::String(value)), Ok), - FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) - .map(FakeKey::Int) - .map_or_else( - || { - Ok(FakeKey::String( - String::from_utf8_lossy(&value).into_owned(), - )) - }, - Ok, - ), - FakeValue::Null => Ok(FakeKey::String(String::new())), - value => Ok(FakeKey::Int(self.fake_int(&value))), - } - } - - /// Allocates a fake runtime cell for an existing PHP array key. - fn alloc_key(&mut self, key: &FakeKey) -> Result { - match key { - FakeKey::Int(value) => self.int(*value), - FakeKey::String(value) => self.string(value), - } - } - - /// Finds a fake object property by insertion-order name. - fn object_property( - properties: &[(String, RuntimeCellHandle)], - name: &str, - ) -> Option { - properties - .iter() - .find_map(|(property, value)| (property == name).then_some(*value)) - } - } - - impl RuntimeValueOps for FakeOps { - /// Creates a fake indexed array cell. - fn array_new(&mut self, capacity: usize) -> Result { - Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) - } - - /// Creates a fake associative array cell. - fn assoc_new(&mut self, _capacity: usize) -> Result { - Ok(self.alloc(FakeValue::Assoc(Vec::new()))) - } - - /// Reads one fake indexed array element. - fn array_get( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - ) -> Result { - let key = self.key(index)?; - match self.get(array) { - FakeValue::Array(elements) => { - let FakeKey::Int(index) = key else { - return self.null(); - }; - if index < 0 { - return self.null(); - } - elements - .get(index as usize) - .copied() - .map_or_else(|| self.null(), Ok) - } - FakeValue::Assoc(entries) => entries - .iter() - .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) - .map_or_else(|| self.null(), Ok), - _ => self.null(), - } - } - - /// Checks whether a fake array has the requested key without reading its value. - fn array_key_exists( - &mut self, - key: RuntimeCellHandle, - array: RuntimeCellHandle, - ) -> Result { - let key = self.key(key)?; - let exists = match self.get(array) { - FakeValue::Array(elements) => { - matches!(key, FakeKey::Int(index) if index >= 0 && (index as usize) < elements.len()) - } - FakeValue::Assoc(entries) => entries.iter().any(|(entry_key, _)| entry_key == &key), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - self.bool_value(exists) - } - - /// Returns one fake foreach key by insertion-order position. - fn array_iter_key( - &mut self, - array: RuntimeCellHandle, - position: usize, - ) -> Result { - match self.get(array) { - FakeValue::Array(elements) if position < elements.len() => { - self.int(position as i64) - } - FakeValue::Assoc(entries) => { - let Some((key, _)) = entries.get(position) else { - return self.null(); - }; - self.alloc_key(key) - } - FakeValue::Array(_) => self.null(), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Writes one fake indexed or associative array element. - fn array_set( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - value: RuntimeCellHandle, - ) -> Result { - let key = self.key(index)?; - let id = array.as_ptr() as usize; - match self.values.get_mut(&id) { - Some(FakeValue::Array(elements)) => { - let FakeKey::Int(index) = key else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if index < 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let index = index as usize; - while elements.len() <= index { - elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); - } - elements[index] = value; - } - Some(FakeValue::Assoc(entries)) => { - if let Some((_, existing_value)) = - entries.iter_mut().find(|(entry_key, _)| entry_key == &key) - { - *existing_value = value; - } else { - entries.push((key, value)); - } - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - Ok(array) - } - - /// Reads one fake object property by name. - fn property_get( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => properties - .iter() - .find_map(|(name, value)| (name == property).then_some(*value)) - .map_or_else(|| self.null(), Ok), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Writes one fake object property by name. - fn property_set( - &mut self, - object: RuntimeCellHandle, - property: &str, - value: RuntimeCellHandle, - ) -> Result<(), EvalStatus> { - let id = object.as_ptr() as usize; - let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if let Some((_, existing_value)) = - properties.iter_mut().find(|(name, _)| name == property) - { - *existing_value = value; - } else { - properties.push((property.to_string(), value)); - } - Ok(()) - } - - /// Returns the number of fake object properties in insertion order. - fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { - match self.get(object) { - FakeValue::Object(properties) => Ok(properties.len()), - FakeValue::Iterator { .. } => Ok(0), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Returns one fake object property key by insertion-order position. - fn object_property_iter_key( - &mut self, - object: RuntimeCellHandle, - position: usize, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => { - let Some((name, _)) = properties.get(position) else { - return self.null(); - }; - self.string(name) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Calls one fake object method by name. - fn method_call( - &mut self, - object: RuntimeCellHandle, - method: &str, - args: Vec, - ) -> Result { - match (self.get(object), method) { - (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { - let id = object.as_ptr() as usize; - let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) - else { - return Err(EvalStatus::UnsupportedConstruct); - }; - *position = 0; - self.null() - } - (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { - self.bool_value(position < len) - } - (FakeValue::Iterator { .. }, "next") if args.is_empty() => { - let id = object.as_ptr() as usize; - let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) - else { - return Err(EvalStatus::UnsupportedConstruct); - }; - *position += 1; - self.null() - } - (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), - (FakeValue::Object(properties), "read_x") => { - if !args.is_empty() { - return Err(EvalStatus::UnsupportedConstruct); - } - Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "add_x") => { - let [arg] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let x = - Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; - let FakeValue::Int(x) = self.get(x) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(arg) = self.get(*arg) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(x + arg) - } - (FakeValue::Object(properties), "add2_x") => { - let [left, right] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let x = - Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; - let FakeValue::Int(x) = self.get(x) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(left) = self.get(*left) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(right) = self.get(*right) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(x + left + right) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Creates one fake object for eval `new` unit tests. - fn new_object(&mut self, _class_name: &str) -> Result { - Ok(self.alloc(FakeValue::Object(Vec::new()))) - } - - /// Applies fake constructor side effects for eval `new` unit tests. - fn construct_object( - &mut self, - object: RuntimeCellHandle, - args: Vec, - ) -> Result<(), EvalStatus> { - let id = object.as_ptr() as usize; - let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if let Some(first) = args.first().copied() { - if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { - *value = first; - } else { - properties.push(("x".to_string(), first)); - } - } - Ok(()) - } - - /// Reports one fake AOT class for eval `class_exists` unit tests. - fn class_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownClass")) - } - - /// Reports one fake AOT interface for eval `interface_exists` unit tests. - fn interface_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownInterface")) - } - - /// Reports one fake AOT trait for eval `trait_exists` unit tests. - fn trait_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownTrait")) - } - - /// Reports one fake AOT enum for eval `enum_exists` unit tests. - fn enum_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownEnum")) - } - - /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. - fn object_is_a( - &mut self, - object_or_class: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - ) -> Result { - match self.get(object_or_class) { - FakeValue::Object(_) - if target_class.eq_ignore_ascii_case("Exception") - || target_class.eq_ignore_ascii_case("Throwable") => - { - Ok(!exclude_self) - } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { - Ok(!exclude_self) - } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => { - Ok(true) - } - _ => Ok(false), - } - } - - /// Returns a fake PHP class name for object-tagged test values. - fn object_class_name( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - match self.get(object) { - FakeValue::Object(_) => self.string("stdClass"), - FakeValue::Iterator { .. } => self.string("Iterator"), - _ => Err(EvalStatus::RuntimeFatal), - } - } - - /// Returns fake parent-class names for eval introspection unit tests. - fn parent_class_name( - &mut self, - object_or_class: RuntimeCellHandle, - ) -> Result { - match self.get(object_or_class) { - FakeValue::Object(_) => self.string("ParentClass"), - FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { - self.string("ParentClass") - } - _ => self.string(""), - } - } - - /// Returns the visible element count for fake array values. - fn array_len(&mut self, array: RuntimeCellHandle) -> Result { - match self.get(array) { - FakeValue::Array(elements) => Ok(elements.len()), - FakeValue::Assoc(entries) => Ok(entries.len()), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Returns whether a fake runtime cell is an indexed or associative array. - fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { - Ok(matches!( - self.get(value), - FakeValue::Array(_) | FakeValue::Assoc(_) - )) - } - - /// Returns whether a fake runtime cell is null. - fn is_null(&mut self, value: RuntimeCellHandle) -> Result { - Ok(matches!(self.get(value), FakeValue::Null)) - } - - /// Returns the fake runtime tag corresponding to a test value. - fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { - Ok(match self.get(value) { - FakeValue::Int(_) => EVAL_TAG_INT, - FakeValue::String(_) | FakeValue::Bytes(_) => EVAL_TAG_STRING, - FakeValue::Float(_) => EVAL_TAG_FLOAT, - FakeValue::Bool(_) => EVAL_TAG_BOOL, - FakeValue::Array(_) => EVAL_TAG_ARRAY, - FakeValue::Assoc(_) => EVAL_TAG_ASSOC, - FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, - FakeValue::Resource(_) => EVAL_TAG_RESOURCE, - FakeValue::Null => EVAL_TAG_NULL, - }) - } - - /// Returns the fake object handle as a stable object identity. - fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { - match self.get(object) { - FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), - _ => Err(EvalStatus::RuntimeFatal), - } - } - - /// Records fake releases without freeing handles needed for assertions. - fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - self.releases.push(value); - Ok(()) - } - - /// Returns the same fake handle because fake cells do not refcount. - fn retain(&mut self, value: RuntimeCellHandle) -> Result { - Ok(value) - } - - /// Records fake PHP warnings without writing to stderr. - fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { - self.warnings.push(message.to_string()); - Ok(()) - } - - /// Creates a fake null cell. - fn null(&mut self) -> Result { - Ok(self.alloc(FakeValue::Null)) - } - - /// Creates a fake bool cell. - fn bool_value(&mut self, value: bool) -> Result { - Ok(self.alloc(FakeValue::Bool(value))) - } - - /// Creates a fake int cell. - fn int(&mut self, value: i64) -> Result { - Ok(self.alloc(FakeValue::Int(value))) - } - - /// Creates a fake float cell. - fn float(&mut self, value: f64) -> Result { - Ok(self.alloc(FakeValue::Float(value))) - } - - /// Creates a fake string cell. - fn string(&mut self, value: &str) -> Result { - Ok(self.alloc(FakeValue::String(value.to_string()))) - } - - /// Creates a fake string cell from raw PHP bytes. - fn string_bytes_value(&mut self, value: &[u8]) -> Result { - match std::str::from_utf8(value) { - Ok(value) => self.string(value), - Err(_) => Ok(self.alloc(FakeValue::Bytes(value.to_vec()))), - } - } - - /// Casts a fake runtime cell to a fake integer cell. - fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - let value = self.fake_int(&value); - self.int(value) - } - - /// Casts a fake runtime cell to a fake float cell. - fn cast_float( - &mut self, - value: RuntimeCellHandle, - ) -> Result { - let value = self.get(value); - let value = self.fake_numeric(&value); - self.float(value) - } - - /// Casts a fake runtime cell to a fake string cell. - fn cast_string( - &mut self, - value: RuntimeCellHandle, - ) -> Result { - let value = self.stringify(value); - self.string(&value) - } - - /// Casts a fake runtime cell to a fake boolean cell. - fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - let value = self.fake_truthy(&value); - self.bool_value(value) - } - - /// Computes fake PHP absolute value while preserving float payloads. - fn abs(&mut self, value: RuntimeCellHandle) -> Result { - match self.get(value) { - FakeValue::Float(value) => self.float(value.abs()), - value => self.int(self.fake_int(&value).wrapping_abs()), - } - } - - /// Computes fake PHP ceiling through numeric conversion as a float result. - fn ceil(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).ceil()) - } - - /// Computes fake PHP floor through numeric conversion as a float result. - fn floor(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).floor()) - } - - /// Computes fake PHP square root through numeric conversion as a float result. - fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).sqrt()) - } - - /// Reverses a fake string byte-wise for interpreter tests. - fn strrev(&mut self, value: RuntimeCellHandle) -> Result { - let mut bytes = self.stringify(value).into_bytes(); - bytes.reverse(); - let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - self.string(&value) - } - - /// Divides fake numeric cells with PHP `fdiv()` zero handling. - fn fdiv( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left / right) - } - - /// Computes fake floating-point modulo for interpreter tests. - fn fmod( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left % right) - } - - /// Adds fake numeric cells for interpreter tests. - fn add( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), - (left, right) => self.float(self.fake_numeric(&left) + self.fake_numeric(&right)), - } - } - - /// Subtracts fake numeric cells for interpreter tests. - fn sub( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), - (left, right) => self.float(self.fake_numeric(&left) - self.fake_numeric(&right)), - } - } - - /// Multiplies fake numeric cells for interpreter tests. - fn mul( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), - (left, right) => self.float(self.fake_numeric(&left) * self.fake_numeric(&right)), - } - } - - /// Divides fake numeric cells for interpreter tests. - fn div( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let right = self.fake_numeric(&self.get(right)); - if right == 0.0 { - return Err(EvalStatus::RuntimeFatal); - } - let left = self.fake_numeric(&self.get(left)); - self.float(left / right) - } - - /// Computes fake integer modulo for interpreter tests. - fn modulo( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let right = self.fake_int(&self.get(right)); - if right == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let left = self.fake_int(&self.get(left)); - self.int(left % right) - } - - /// Raises fake numeric cells for interpreter tests. - fn pow( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left.powf(right)) - } - - /// Rounds fake numeric cells with PHP's optional decimal precision. - fn round( - &mut self, - value: RuntimeCellHandle, - precision: Option, - ) -> Result { - let value = self.fake_numeric(&self.get(value)); - let precision = precision - .map(|precision| self.fake_int(&self.get(precision))) - .unwrap_or(0); - let multiplier = 10_f64.powf(precision as f64); - self.float((value * multiplier).round() / multiplier) - } - - /// Applies fake integer bitwise and shift operations for interpreter tests. - fn bitwise( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_int(&self.get(left)); - let right = self.fake_int(&self.get(right)); - let value = match op { - EvalBinOp::BitAnd => left & right, - EvalBinOp::BitOr => left | right, - EvalBinOp::BitXor => left ^ right, - EvalBinOp::ShiftLeft => { - if right < 0 { - return Err(EvalStatus::RuntimeFatal); - } - left.wrapping_shl(right as u32) - } - EvalBinOp::ShiftRight => { - if right < 0 { - return Err(EvalStatus::RuntimeFatal); - } - left.wrapping_shr(right as u32) - } - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - self.int(value) - } - - /// Applies fake integer bitwise NOT for interpreter tests. - fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.fake_int(&self.get(value)); - self.int(!value) - } - - /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. - fn concat( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let mut left = self.string_bytes_for_value(&self.get(left)); - let right = self.string_bytes_for_value(&self.get(right)); - left.extend_from_slice(&right); - self.string_bytes_value(&left) - } - - /// Compares fake scalar cells and returns a fake PHP boolean. - fn compare( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let result = match op { - EvalBinOp::LooseEq => self.loose_eq(left, right), - EvalBinOp::LooseNotEq => !self.loose_eq(left, right), - EvalBinOp::StrictEq => self.strict_eq(left, right), - EvalBinOp::StrictNotEq => !self.strict_eq(left, right), - EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, - EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, - EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, - EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, - EvalBinOp::Add - | EvalBinOp::Sub - | EvalBinOp::Mul - | EvalBinOp::Div - | EvalBinOp::Mod - | EvalBinOp::Pow - | EvalBinOp::BitAnd - | EvalBinOp::BitOr - | EvalBinOp::BitXor - | EvalBinOp::ShiftLeft - | EvalBinOp::ShiftRight - | EvalBinOp::Concat - | EvalBinOp::Spaceship - | EvalBinOp::LogicalAnd - | EvalBinOp::LogicalOr - | EvalBinOp::LogicalXor => { - return Err(EvalStatus::UnsupportedConstruct); - } - }; - self.bool_value(result) - } - - /// Compares fake numeric cells and returns a PHP spaceship integer. - fn spaceship( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.numeric(left)?; - let right = self.numeric(right)?; - let value = if left < right { - -1 - } else if left > right { - 1 - } else { - 0 - }; - self.int(value) - } - - /// Appends fake echo output for interpreter tests. - fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - let value = self.stringify(value); - self.output.push_str(&value); - Ok(()) - } - - /// Casts one fake runtime cell to bytes for nested eval parsing. - fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { - Ok(self.string_bytes_for_value(&self.get(value))) - } - - /// Returns PHP-like truthiness for fake runtime cells. - fn truthy(&mut self, value: RuntimeCellHandle) -> Result { - Ok(match self.get(value) { - FakeValue::Null => false, - FakeValue::Bool(value) => value, - FakeValue::Int(value) => value != 0, - FakeValue::Float(value) => value != 0.0, - FakeValue::String(value) => !value.is_empty() && value != "0", - FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", - FakeValue::Array(value) => !value.is_empty(), - FakeValue::Assoc(value) => !value.is_empty(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => true, - FakeValue::Resource(_) => true, - }) - } - } - - impl FakeOps { - /// Compares fake scalar values with the same loose rules covered by eval tests. - fn loose_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { - match (self.get(left), self.get(right)) { - (FakeValue::Bool(left), right) => left == self.fake_truthy(&right), - (left, FakeValue::Bool(right)) => self.fake_truthy(&left) == right, - (FakeValue::Null, FakeValue::Null) => true, - (FakeValue::Null, FakeValue::String(value)) - | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), - (FakeValue::Null, FakeValue::Bytes(value)) - | (FakeValue::Bytes(value), FakeValue::Null) => value.is_empty(), - (FakeValue::String(left), FakeValue::String(right)) => { - match (left.parse::(), right.parse::()) { - (Ok(left), Ok(right)) => left == right, - _ => left == right, - } - } - (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, - (FakeValue::String(left), FakeValue::Bytes(right)) - | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, - (FakeValue::String(left), right) => left - .parse::() - .is_ok_and(|left| left == self.fake_numeric(&right)), - (FakeValue::Bytes(left), right) => std::str::from_utf8(&left) - .ok() - .and_then(|left| left.parse::().ok()) - .is_some_and(|left| left == self.fake_numeric(&right)), - (left, FakeValue::String(right)) => right - .parse::() - .is_ok_and(|right| self.fake_numeric(&left) == right), - (left, FakeValue::Bytes(right)) => std::str::from_utf8(&right) - .ok() - .and_then(|right| right.parse::().ok()) - .is_some_and(|right| self.fake_numeric(&left) == right), - (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), - } - } - - /// Compares fake scalar values by PHP strict tag and payload equality. - fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { - match (self.get(left), self.get(right)) { - (FakeValue::Null, FakeValue::Null) => true, - (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, - (FakeValue::Int(left), FakeValue::Int(right)) => left == right, - (FakeValue::Float(left), FakeValue::Float(right)) => left == right, - (FakeValue::String(left), FakeValue::String(right)) => left == right, - (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, - (FakeValue::String(left), FakeValue::Bytes(right)) - | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, - (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, - _ => false, - } - } - - /// Converts one fake scalar cell to a numeric value for comparison tests. - fn numeric(&self, handle: RuntimeCellHandle) -> Result { - Ok(self.fake_numeric(&self.get(handle))) - } - - /// Converts a fake value to the numeric scalar used by comparison tests. - fn fake_numeric(&self, value: &FakeValue) -> f64 { - match value { - FakeValue::Null => 0.0, - FakeValue::Bool(false) => 0.0, - FakeValue::Bool(true) => 1.0, - FakeValue::Int(value) => *value as f64, - FakeValue::Float(value) => *value, - FakeValue::String(value) => value.parse::().unwrap_or(0.0), - FakeValue::Bytes(value) => std::str::from_utf8(value) - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(0.0), - FakeValue::Array(value) => value.len() as f64, - FakeValue::Assoc(value) => value.len() as f64, - FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, - FakeValue::Resource(value) => (*value + 1) as f64, - } - } - - /// Converts a fake value to the integer scalar used by modulo tests. - fn fake_int(&self, value: &FakeValue) -> i64 { - self.fake_numeric(value) as i64 - } - - /// Returns fake PHP truthiness for already-loaded test values. - fn fake_truthy(&self, value: &FakeValue) -> bool { - match value { - FakeValue::Null => false, - FakeValue::Bool(value) => *value, - FakeValue::Int(value) => *value != 0, - FakeValue::Float(value) => *value != 0.0, - FakeValue::String(value) => !value.is_empty() && value != "0", - FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", - FakeValue::Array(value) => !value.is_empty(), - FakeValue::Assoc(value) => !value.is_empty(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => true, - FakeValue::Resource(_) => true, - } - } - - /// Converts a fake runtime cell to a PHP-like string for test echo/concat. - fn stringify(&self, handle: RuntimeCellHandle) -> String { - match self.get(handle) { - FakeValue::Null => String::new(), - FakeValue::Bool(false) => String::new(), - FakeValue::Bool(true) => "1".to_string(), - FakeValue::Int(value) => value.to_string(), - FakeValue::Float(value) => value.to_string(), - FakeValue::String(value) => value, - FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), - FakeValue::Array(_) => "Array".to_string(), - FakeValue::Assoc(_) => "Array".to_string(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), - FakeValue::Resource(value) => format!("Resource id #{}", value + 1), - } - } - - /// Converts a fake PHP value to string bytes while preserving binary strings. - fn string_bytes_for_value(&self, value: &FakeValue) -> Vec { - match value { - FakeValue::String(value) => value.as_bytes().to_vec(), - FakeValue::Bytes(value) => value.clone(), - value => self.stringify_value(value).into_bytes(), - } - } - - /// Converts one loaded fake PHP value to display text for byte coercions. - fn stringify_value(&self, value: &FakeValue) -> String { - match value { - FakeValue::Null => String::new(), - FakeValue::Bool(false) => String::new(), - FakeValue::Bool(true) => "1".to_string(), - FakeValue::Int(value) => value.to_string(), - FakeValue::Float(value) => value.to_string(), - FakeValue::String(value) => value.clone(), - FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), - FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), - FakeValue::Resource(value) => format!("Resource id #{}", value + 1), - } - } - } - - /// Test native invoker that returns the descriptor pointer as a runtime cell. - unsafe extern "C" fn fake_native_return_descriptor( - descriptor: *mut c_void, - _args: *mut RuntimeCell, - ) -> *mut RuntimeCell { - descriptor.cast() - } - - /// Verifies assignment writes a named scope entry and return reads it back. - #[test] - fn execute_program_stores_and_returns_scope_value() { - let program = parse_fragment(b"$x = 3; return $x + 4;").expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.get(x), FakeValue::Int(3)); - assert_eq!(values.get(result), FakeValue::Int(7)); - } - - /// Verifies reference assignment aliases variable names and writes through the alias. - #[test] - fn execute_program_reference_assignment_updates_source_variable() { - let program = parse_fragment(b"$x = 1; $alias =& $x; $alias = 5; return $x;") - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - let alias = scope - .visible_cell("alias") - .expect("scope should contain alias"); - - assert_eq!(x, alias); - assert_eq!(values.get(x), FakeValue::Int(5)); - assert_eq!(values.get(result), FakeValue::Int(5)); - } - - /// Verifies eval `throw` exits the program with a retained Throwable cell. - #[test] - fn execute_program_propagates_throw_as_uncaught_outcome() { - let program = - parse_fragment(br#"throw new Exception("eval boom");"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => { - assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)); - } - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } - } - - /// Verifies eval `try/catch` catches a thrown object and binds the catch variable. - #[test] - fn execute_program_catches_throwable_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable $caught) { - return $caught->answer(); -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); - } - - /// Verifies eval `catch (Throwable)` can handle a throw without binding a variable. - #[test] - fn execute_program_catches_throwable_without_variable_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable) { - return 9; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let released = values - .releases - .first() - .copied() - .expect("unbound catch should release the thrown object"); - - assert_eq!(scope.visible_cell("caught"), None); - assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(9)); - } - - /// Verifies eval `catch (Exception)` matches thrown exception objects. - #[test] - fn execute_program_catches_specific_exception_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Exception $caught) { - return $caught->answer(); -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); - } - - /// Verifies eval catch clauses keep source order and skip non-matching types. - #[test] - fn execute_program_skips_non_matching_specific_catch_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (RuntimeException $wrong) { - return 1; -} catch (Exception $caught) { - return 2; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(scope.visible_cell("wrong"), None); - assert_eq!(values.get(result), FakeValue::Int(2)); - } - - /// Verifies union catch clauses test later types in the same catch clause. - #[test] - fn execute_program_catches_union_type_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (RuntimeException|Exception $caught) { - return $caught->answer(); -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); - } - - /// Verifies eval `finally` runs before a pending try-body return is observed. - #[test] - fn execute_program_runs_finally_before_returning_try_value() { - let program = parse_fragment( - br#"try { - return 1; -} finally { - echo "finally"; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "finally"); - assert_eq!(values.get(result), FakeValue::Int(1)); - } - - /// Verifies eval `finally` return values replace pending try-body returns. - #[test] - fn execute_program_finally_return_overrides_try_return() { - let program = parse_fragment( - br#"try { - return 1; -} finally { - return 2; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.releases.len(), 1); - } - - /// Verifies eval `finally` return values replace pending uncaught throws. - #[test] - fn execute_program_finally_return_overrides_uncaught_throw() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} finally { - return 2; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let released = values - .releases - .first() - .copied() - .expect("overridden throw should be released"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); - } - - /// Verifies eval `finally` runs before an uncaught throw leaves the fragment. - #[test] - fn execute_program_runs_finally_before_uncaught_throw_outcome() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} finally { - echo "finally"; -}"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => { - assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)) - } - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } - assert_eq!(values.output, "finally"); - } - - /// Verifies static locals declared inside eval catch blocks persist per function context. - #[test] - fn execute_context_function_persists_static_local_inside_catch() { - let program = parse_fragment( - br#"function dyn($e) { - try { - throw $e; - } catch (Throwable $caught) { - static $n = 0; - $n++; - return $caught->answer() + $n; - } -}"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - let first_thrown = values - .new_object("Exception") - .expect("allocate first fake exception"); - let second_thrown = values - .new_object("Exception") - .expect("allocate second fake exception"); - - let first = execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) - .expect("execute first dynamic function call"); - let second = - execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) - .expect("execute second dynamic function call"); - - assert_eq!(values.get(first), FakeValue::Int(43)); - assert_eq!(values.get(second), FakeValue::Int(44)); - } - - /// Verifies static locals declared inside eval finally blocks persist per function context. - #[test] - fn execute_context_function_persists_static_local_inside_finally() { - let program = parse_fragment( - br#"function dyn() { - try { - return 0; - } finally { - static $n = 0; - $n++; - return $n; - } -}"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - - let first = execute_context_function_zero_args(&mut context, "dyn", &mut values) - .expect("execute first dynamic function call"); - let second = execute_context_function_zero_args(&mut context, "dyn", &mut values) - .expect("execute second dynamic function call"); - - assert_eq!(values.get(first), FakeValue::Int(1)); - assert_eq!(values.get(second), FakeValue::Int(2)); - } - - /// Verifies throws from eval-declared functions escape through the shared context. - #[test] - fn execute_context_function_propagates_throw_as_uncaught_outcome() { - let program = - parse_fragment(br#"function dyn($e) { throw $e; }"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - let thrown = values - .new_object("Exception") - .expect("allocate fake exception"); - - let outcome = - execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) - .expect("throw should be an eval function outcome"); - - match outcome { - EvalOutcome::Throwable(value) => assert_eq!(value, thrown), - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } - } - - /// Verifies nested eval preserves the thrown cell while returning an uncaught status. - #[test] - fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { - let program = parse_fragment(br#"eval("throw $e;");"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let thrown = values - .new_object("Exception") - .expect("allocate fake exception"); - scope.set("e", thrown, ScopeCellOwnership::Borrowed); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("nested throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => assert_eq!(value, thrown), - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } - } - - /// Verifies eval include resolves caller-relative paths, shares scope, and returns file values. - #[test] - fn execute_program_include_uses_call_site_and_returns_file_result() { - let dir = std::env::temp_dir().join(format!( - "elephc-eval-include-{}-call-site", - std::process::id() - )); - let path = dir.join("piece.php"); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).expect("create include fixture directory"); - std::fs::write( - &path, - format!( - r#">= 3; echo $x; echo ":"; -echo ~0; echo ":"; echo -16 >> 2; -return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:5:4:32:8:-1:-4"); - assert_eq!(values.get(result), FakeValue::Int(21)); - } - - /// Verifies simple variable increment and decrement statements update the scope value. - #[test] - fn execute_program_evaluates_inc_dec_statements() { - let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "1"); - assert_eq!(values.get(i), FakeValue::Int(1)); - } - - /// Verifies echo and unset operate through runtime hooks and scope metadata. - #[test] - fn execute_program_echoes_and_unsets_scope_value() { - let program = - parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let name = values.string(" Ada").expect("create fake string"); - scope.set("name", name, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "hi Ada"); - assert_eq!(values.get(result), FakeValue::Null); - assert!(scope.entry("name").expect("unset marker").flags().unset); - } - - /// Verifies comma-separated echo expressions are executed in source order. - #[test] - fn execute_program_echoes_comma_list() { - let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let b = values.string("b").expect("create fake string"); - scope.set("b", b, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "abc"); - } - - /// Verifies print writes output and returns integer 1. - #[test] - fn execute_program_print_returns_one() { - let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "p"); - assert_eq!(values.get(result), FakeValue::Int(1)); - } - - /// Verifies eval `print_r()` emits supported values and returns true. - #[test] - fn execute_program_dispatches_print_r_builtin() { - let program = parse_fragment( - br#"print_r("x"); echo ":"; -print_r(value: false); echo ":"; -print_r([1, 2]); echo ":"; -$call = call_user_func("print_r", true); -$spread = call_user_func_array("print_r", ["value" => "z"]); -echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; -return function_exists("print_r");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "x::Array\n:1z:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. - #[test] - fn execute_program_dispatches_var_dump_builtin() { - let program = parse_fragment( - br#"var_dump(42); -var_dump("hi"); -var_dump(false); -var_dump(null); -var_dump([10, 20]); -var_dump(["x" => true]); -$call = call_user_func("var_dump", 3.5); -$spread = call_user_func_array("var_dump", ["value" => "z"]); -echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; -return function_exists("var_dump");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - concat!( - "int(42)\n", - "string(2) \"hi\"\n", - "bool(false)\n", - "NULL\n", - "array(2) {\n", - " [0]=>\n", - " int(10)\n", - " [1]=>\n", - " int(20)\n", - "}\n", - "array(1) {\n", - " [\"x\"]=>\n", - " bool(true)\n", - "}\n", - "float(3.5)\n", - "string(1) \"z\"\n", - "call-null:spread-null:", - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval property reads and writes dispatch through runtime hooks. - #[test] - fn execute_program_reads_and_writes_object_property() { - let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(1).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!( - values - .property_get(object, "x") - .map(|value| values.get(value)) - .expect("property should be readable"), - FakeValue::Int(2) - ); - } - - /// Verifies eval method calls dispatch through the runtime method hook. - #[test] - fn execute_program_calls_object_method() { - let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); - } - - /// Verifies eval method calls forward evaluated arguments to the runtime hook. - #[test] - fn execute_program_calls_object_method_with_argument() { - let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); - } - - /// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. - #[test] - fn execute_program_calls_object_method_with_two_arguments() { - let program = - parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(18)); - } - - /// Verifies eval method calls forward numerically unpacked arguments. - #[test] - fn execute_program_calls_object_method_with_spread_arguments() { - let program = - parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(18)); - } - - /// Verifies eval object construction dispatches through runtime hooks. - #[test] - fn execute_program_constructs_named_object() { - let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Object(Vec::new())); - } - - /// Verifies eval object construction passes constructor arguments through runtime hooks. - #[test] - fn execute_program_constructs_named_object_with_args() { - let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let FakeValue::Object(properties) = values.get(result) else { - panic!("expected fake object"); - }; - let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); - - assert_eq!(values.get(x), FakeValue::Int(1)); - } - - /// Verifies eval-declared classes create objects with properties and methods. - #[test] - fn execute_program_constructs_eval_declared_class_with_method() { - let program = parse_fragment( - br#"class DynBox { - public int $x = 1; - public function __construct($x) { $this->x = $x; } - public function bump($n) { $this->x = $this->x + $n; return $this->x; } -} -$box = new DynBox(4); -echo get_class($box); -echo ":"; -echo $box->bump(3); -echo ":"; -echo is_a($box, "DynBox") ? "Y" : "N"; -$call = [$box, "bump"]; -echo call_user_func($call, 1); -echo ":"; -echo call_user_func_array($call, [2]); -echo ":"; -return $box->x;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "DynBox:7:Y8:10:"); - assert_eq!(values.get(result), FakeValue::Int(10)); - } - - /// Verifies if/else executes only the PHP-truthy branch. - #[test] - fn execute_program_if_else_uses_php_truthiness() { - let program = parse_fragment(br#"if ($flag) { $x = "then"; } else { $x = "else"; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.int(0).expect("create fake int"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.get(x), FakeValue::String("else".to_string())); - } - - /// Verifies elseif chains execute the first truthy branch and skip later branches. - #[test] - fn execute_program_elseif_uses_first_truthy_branch() { - let program = parse_fragment( - br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; } else { $x = "c"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let a = values.bool_value(false).expect("create fake bool"); - let b = values.bool_value(true).expect("create fake bool"); - scope.set("a", a, ScopeCellOwnership::Owned); - scope.set("b", b, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.get(x), FakeValue::String("b".to_string())); - } - - /// Verifies while repeats while the condition remains truthy and propagates writes. - #[test] - fn execute_program_while_uses_php_truthiness() { - let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.int(2).expect("create fake int"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let flag = scope - .visible_cell("flag") - .expect("scope should contain flag"); - - assert_eq!(values.output, "2"); - assert_eq!(values.get(flag), FakeValue::Bool(false)); - } - - /// Verifies do/while runs the body before testing the condition. - #[test] - fn execute_program_do_while_runs_body_before_condition() { - let program = parse_fragment(br#"do { echo $i; $i = $i + 1; } while (false);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let i = values.int(0).expect("create fake int"); - scope.set("i", i, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "0"); - assert_eq!(values.get(i), FakeValue::Int(1)); - } - - /// Verifies switch uses loose matching and falls through after the matching case. - #[test] - fn execute_program_switch_matches_and_falls_through() { - let program = - parse_fragment(br#"switch ($x) { case 1: echo "one"; break; case 2: echo "two"; default: echo "default"; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(2).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "twodefault"); - } - - /// Verifies for loops run init, condition, update, and body in PHP order. - #[test] - fn execute_program_for_loop_updates_after_body() { - let program = parse_fragment(br#"for ($i = 3; $i; $i = $i - 1) { echo $i; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "321"); - assert_eq!(values.get(i), FakeValue::Int(0)); - } - - /// Verifies `continue` in a for loop still runs the update clause. - #[test] - fn execute_program_for_continue_runs_update_clause() { - let program = parse_fragment( - br#"for ($i = 3; $i; $i = $i - 1) { if ($i - 1) { continue; } echo "done"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "done"); - assert_eq!(values.get(i), FakeValue::Int(0)); - } - - /// Verifies comparison operators return boolean cells usable by echo and branches. - #[test] - fn execute_program_comparisons_return_bool_cells() { - let program = parse_fragment( - br#"echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; if ("10" == 10) { echo "n"; } if ("a" != "b") { echo "s"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1111ns"); - } - - /// Verifies spaceship comparisons return PHP -1/0/1 integer cells. - #[test] - fn execute_program_spaceship_returns_int_cells() { - let program = - parse_fragment(br#"echo 1 <=> 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "-1:0:1"); - } - - /// Verifies strict equality keeps PHP type identity distinct from loose equality. - #[test] - fn execute_program_strict_equality_uses_type_identity() { - let program = parse_fragment( - br#"echo "10" == 10; echo "10" === 10; echo "10" === "10"; echo "10" !== 10;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "111"); - } - - /// Verifies logical AND skips an unsupported right-hand expression after a false left side. - #[test] - fn execute_program_short_circuits_logical_and() { - let program = - parse_fragment(br#"return false && missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Bool(false)); - } - - /// Verifies logical OR skips an unsupported right-hand expression after a true left side. - #[test] - fn execute_program_short_circuits_logical_or() { - let program = parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies match expressions use strict comparison across comma-separated patterns. - #[test] - fn execute_program_match_uses_strict_pattern_comparison() { - let program = - parse_fragment(br#"return match ($x) { 1, "1" => "string", default => "other" };"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.string("1").expect("create fake string"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("string".to_string())); - } - - /// Verifies match expressions evaluate only the selected arm result. - #[test] - fn execute_program_match_skips_unselected_results() { - let program = parse_fragment( - br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("two".to_string())); - } - - /// Verifies match expressions without a matching arm or default fail at runtime. - #[test] - fn execute_program_match_without_default_fails_on_miss() { - let program = parse_fragment(br#"return match (3) { 1 => "one", 2 => "two" };"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - } - - /// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. - #[test] - fn execute_program_evaluates_keyword_logical_operators() { - let program = parse_fragment( - br#"echo (false || true and false) ? "T" : "F"; return true or missing();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "F"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies PHP keyword `xor` evaluates both operands and returns a boolean cell. - #[test] - fn execute_program_evaluates_keyword_xor() { - let program = parse_fragment( - br#"echo (true xor false) ? "T" : "F"; echo (true xor true) ? "T" : "F";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "TF"); - } - - /// Verifies ternary expressions evaluate only the selected branch. - #[test] - fn execute_program_ternary_short_circuits_unselected_branch() { - let program = - parse_fragment(br#"echo true ? "yes" : missing(); echo false ? missing() : "no";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "yesno"); - } - - /// Verifies the short ternary form returns the condition value when it is truthy. - #[test] - fn execute_program_short_ternary_reuses_truthy_condition() { - let program = parse_fragment(br#"echo "x" ?: "fallback"; echo false ?: "fallback";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "xfallback"); - } - - /// Verifies null coalescing uses the default for missing or null values. - #[test] - fn execute_program_null_coalesce_uses_default_for_missing_or_null() { - let program = - parse_fragment(br#"echo $missing ?? "fallback"; echo $x ?? "null-fallback";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.null().expect("create fake null"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "fallbacknull-fallback"); - } - - /// Verifies null coalescing skips the default expression for non-null values. - #[test] - fn execute_program_null_coalesce_short_circuits_non_null_value() { - let program = parse_fragment(br#"echo "set" ?? missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "set"); - } - - /// Verifies logical negation returns boolean cells using PHP truthiness. - #[test] - fn execute_program_evaluates_logical_not() { - let program = parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1"); - } - - /// Verifies unary numeric operators delegate to PHP numeric runtime operations. - #[test] - fn execute_program_evaluates_unary_numeric_ops() { - let program = parse_fragment(br#"return -$x + +2;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(5).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(-3)); - } - - /// Verifies foreach assigns each indexed element to the value variable. - #[test] - fn execute_program_foreach_iterates_indexed_values() { - let program = parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let item = scope - .visible_cell("item") - .expect("scope should contain last foreach item"); - - assert_eq!(values.output, "ab"); - assert_eq!(values.get(item), FakeValue::String("b".to_string())); - } - - /// Verifies foreach key-value targets receive indexed integer keys and values. - #[test] - fn execute_program_foreach_assigns_indexed_keys() { - let program = - parse_fragment(br#"foreach (["a", "b"] as $key => $item) { echo $key . $item; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let key = scope.visible_cell("key").expect("scope should contain key"); - let item = scope - .visible_cell("item") - .expect("scope should contain last foreach item"); - - assert_eq!(values.output, "0a1b"); - assert_eq!(values.get(key), FakeValue::Int(1)); - assert_eq!(values.get(item), FakeValue::String("b".to_string())); - } - - /// Verifies foreach over associative arrays preserves insertion-order keys and values. - #[test] - fn execute_program_foreach_iterates_assoc_keys_and_values() { - let program = parse_fragment( - br#"foreach (["a" => 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "a:1;b:2;"); - } - - /// Verifies value-only foreach over associative arrays still yields values in insertion order. - #[test] - fn execute_program_foreach_iterates_assoc_values_only() { - let program = parse_fragment(br#"foreach (["a" => 1, "b" => 2] as $item) { echo $item; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "12"); - } - - /// Verifies break and continue control foreach execution inside eval. - #[test] - fn execute_program_foreach_honors_break_and_continue() { - let program = parse_fragment( - br#"foreach ([1, 2, 3] as $item) { if ($item == 1) { continue; } echo $item; break; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2"); - } - - /// Verifies indexed array literals and reads execute through runtime hooks. - #[test] - fn execute_program_reads_indexed_array_literal() { - let program = parse_fragment(br#"return ["a", "b"][1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("b".to_string())); - } - - /// Verifies legacy `array(...)` literals execute through the existing array runtime hooks. - #[test] - fn execute_program_reads_legacy_array_literal() { - let program = parse_fragment(br#"return array("a", "b" => "bee",)[0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("a".to_string())); - } - - /// Verifies associative array literals and string-key reads execute through runtime hooks. - #[test] - fn execute_program_reads_assoc_array_literal() { - let program = - parse_fragment(br#"return ["name" => "Ada"]["name"];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); - } - - /// Verifies unkeyed assoc literal entries start at zero after string keys. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_string_key_starts_at_zero() { - let program = parse_fragment(br#"return ["name" => "Ada", "Grace"][0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); - } - - /// Verifies unkeyed assoc literal entries use one plus the largest integer key. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_positive_int_key() { - let program = - parse_fragment(br#"return [2 => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies unkeyed assoc literal entries preserve PHP's negative-key rule. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_negative_int_key() { - let program = - parse_fragment(br#"return [-2 => "minus", "tail"][-1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies numeric string literal keys update the next automatic key. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_numeric_string_key() { - let program = - parse_fragment(br#"return ["2" => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies leading-zero string literal keys do not update the automatic key. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_leading_zero_string_key() { - let program = - parse_fragment(br#"return ["02" => "two", "tail"][0];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies null literal keys normalize to empty strings without advancing automatic keys. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_null_key() { - let program = parse_fragment(br#"return [null => "empty", "tail"][0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies null literal keys are readable through the empty-string key. - #[test] - fn execute_program_assoc_array_literal_reads_null_key_as_empty_string() { - let program = - parse_fragment(br#"return [null => "empty"][""];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("empty".to_string())); - } - - /// Verifies boolean literal keys update the next automatic key after integer normalization. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { - let program = - parse_fragment(br#"return [true => "yes", "tail"][2];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies false literal keys update the next automatic key from zero. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_false_key() { - let program = - parse_fragment(br#"return [false => "no", "tail"][1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies float literal keys update the next automatic key after truncation. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_float_key() { - let program = - parse_fragment(br#"return [2.7 => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies nested eval calls parse and execute against the same dynamic scope. - #[test] - fn execute_program_nested_eval_uses_same_scope() { - let program = - parse_fragment(br#"eval("$x = $x + 4;"); return $x;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(1).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); - } - - /// Verifies `__LINE__` inside eval uses the source line within the fragment. - #[test] - fn execute_program_magic_line_uses_fragment_line() { - let program = parse_fragment(b"\nreturn __LINE__;").expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - } - - /// Verifies file-dependent eval magic constants use call-site metadata from the context. - #[test] - fn execute_program_magic_file_and_dir_use_context_call_site() { - let program = - parse_fragment(br#"return __FILE__ . "|" . __DIR__;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - context.set_call_site("/tmp/main.php", "/tmp", 17); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!( - values.get(result), - FakeValue::String("/tmp/main.php(17) : eval()'d code|/tmp".to_string()) - ); - } - - /// Verifies eval class, namespace, and trait magic constants are empty in eval scope. - #[test] - fn execute_program_scope_magic_constants_are_empty_strings() { - let program = parse_fragment( - br#"return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("[||]".to_string())); - } - - /// Verifies eval-declared functions can be called by the same fragment. - #[test] - fn execute_program_calls_declared_function() { - let program = parse_fragment(br#"function dyn($x) { return $x + 1; } return dyn(4);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); - } - - /// Verifies eval namespace declarations qualify functions and namespace magic values. - #[test] - fn execute_program_namespace_qualifies_declared_function() { - let program = parse_fragment( - br#"namespace Eval\Ns; -function dyn() { return __NAMESPACE__ . ":" . __FUNCTION__; } -return dyn();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.get(result), - FakeValue::String("Eval\\Ns:Eval\\Ns\\dyn".to_string()) - ); - } - - /// Verifies unqualified namespaced calls fall back to global builtins when needed. - #[test] - fn execute_program_namespace_call_falls_back_to_builtin() { - let program = parse_fragment(br#"namespace Eval\Ns; return strlen("abcd");"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); - } - - /// Verifies namespaced dynamic functions take precedence over global builtin fallback. - #[test] - fn execute_program_namespace_function_overrides_builtin_fallback() { - let program = parse_fragment( - br#"namespace Eval\Ns; -function strlen($value) { return 99; } -return strlen("abcd");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(99)); - } - - /// Verifies unqualified namespaced constants fall back to global predefined constants. - #[test] - fn execute_program_namespace_const_fetch_falls_back_to_global() { - let program = - parse_fragment(br#"namespace Eval\Ns; return PHP_EOL;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("\n".to_string())); - } - - /// Verifies namespaced dynamic constants take precedence over global fallback. - #[test] - fn execute_program_namespace_const_fetch_reads_dynamic_constant_first() { - let program = - parse_fragment(br#"namespace Eval\Ns; return LOCAL;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(7).expect("create fake int"); - assert!(context.define_constant("Eval\\Ns\\LOCAL", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); - } - - /// Verifies eval namespace `use function` imports dispatch to qualified dynamic functions. - #[test] - fn execute_program_namespace_use_function_import_dispatches() { - let program = parse_fragment( - br#"namespace Eval\Lib; -function target($x) { return $x + 1; } -namespace Eval\App; -use function Eval\Lib\target as AliasTarget; -return aliastarget(6);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); - } - - /// Verifies eval namespace `use const` imports fetch qualified dynamic constants. - #[test] - fn execute_program_namespace_use_const_import_fetches_dynamic_constant() { - let program = parse_fragment( - br#"namespace Eval\App; -use const Eval\Lib\VALUE as LocalValue; -return LocalValue;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(11).expect("create fake int"); - assert!(context.define_constant("Eval\\Lib\\VALUE", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(11)); - } - - /// Verifies eval grouped namespace imports dispatch dynamic functions and constants. - #[test] - fn execute_program_grouped_namespace_use_imports_dispatch() { - let program = parse_fragment( - br#"namespace Eval\Lib; -function target($x) { return $x + 2; } -namespace Eval\App; -use function Eval\Lib\{target as AliasTarget}; -use const Eval\Lib\{VALUE as LocalValue}; -return AliasTarget(LocalValue);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(5).expect("create fake int"); - assert!(context.define_constant("Eval\\Lib\\VALUE", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); - } - - /// Verifies eval-declared functions bind named arguments by parameter name. - #[test] - fn execute_program_calls_declared_function_with_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(y: 2, x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); - } - - /// Verifies eval-declared functions unpack indexed arrays as positional arguments. - #[test] - fn execute_program_calls_declared_function_with_spread_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...[1, 2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); - } - - /// Verifies string keys unpack as named arguments for eval-declared functions. - #[test] - fn execute_program_calls_declared_function_with_named_spread_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...["y" => 2], x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); - } - - /// Verifies eval-declared function static locals persist between calls. - #[test] - fn execute_program_static_var_persists_in_declared_function() { - let program = parse_fragment( - br#"function dyn() { for ($i = 0; $i < 2; $i++) { static $n = 0; $n++; } return $n; } -return (dyn() * 10) + dyn();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(24)); - } - - /// Verifies top-level eval static declarations reinitialize on each eval execution. - #[test] - fn execute_program_top_level_static_var_reinitializes_per_eval() { - let program = - parse_fragment(br#"static $n = 0; $n++; return $n;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let first = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute first eval ir"); - let second = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute second eval ir"); - - assert_eq!(values.get(first), FakeValue::Int(1)); - assert_eq!(values.get(second), FakeValue::Int(1)); - } - - /// Verifies `global` declarations read and write the context global scope. - #[test] - fn execute_program_global_alias_writes_context_global_scope() { - let program = - parse_fragment(br#"global $g; $g = $g + 1; return $g;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut global_scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let initial = values.int(1).expect("allocate initial global"); - global_scope.set("g", initial, ScopeCellOwnership::Owned); - context.set_global_scope(&mut global_scope); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - let global = global_scope - .visible_cell("g") - .expect("global scope should contain g"); - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.get(global), FakeValue::Int(2)); - } - - /// Verifies references to global aliases write the source global variable. - #[test] - fn execute_program_reference_alias_to_global_updates_source_global() { - let program = parse_fragment(br#"global $g; $alias =& $g; $alias = 4; return $g;"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut global_scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let initial = values.int(1).expect("allocate initial global"); - global_scope.set("g", initial, ScopeCellOwnership::Owned); - context.set_global_scope(&mut global_scope); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - let global = global_scope - .visible_cell("g") - .expect("global scope should contain g"); - assert_eq!(values.get(result), FakeValue::Int(4)); - assert_eq!(values.get(global), FakeValue::Int(4)); - assert!(global_scope.visible_cell("alias").is_none()); - } - - /// Verifies named calls reject positional arguments that follow named arguments. - #[test] - fn execute_program_rejects_positional_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, print "late");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - assert_eq!(values.output, ""); - } - - /// Verifies named calls reject argument unpacking after named arguments. - #[test] - fn execute_program_rejects_spread_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, ...[2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - } - - /// Verifies function-scope magic constants keep the eval declaration spelling. - #[test] - fn execute_program_magic_function_and_method_use_eval_declared_name() { - let program = parse_fragment( - br#"function DynMagicCase() { return __FUNCTION__ . ":" . __METHOD__; } return dynmagiccase();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.get(result), - FakeValue::String("DynMagicCase:DynMagicCase".to_string()) - ); - } - - /// Verifies eval-declared functions persist in a shared eval context. - #[test] - fn execute_program_context_keeps_declared_function() { - let define = - parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("parse eval fragment"); - let call = parse_fragment(br#"return dyn(4);"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect("execute eval ir"); - let result = execute_program_with_context(&mut context, &call, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); - } - - /// Verifies `call_user_func` inside eval can dispatch an eval-declared function. - #[test] - fn execute_program_call_user_func_dispatches_declared_function() { - let program = parse_fragment( - br#"function dyn($x) { return $x + 1; } -return call_user_func("dyn", 4);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); - } - - /// Verifies `call_user_func` inside eval can dispatch a supported builtin. - #[test] - fn execute_program_call_user_func_dispatches_builtin() { - let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); - } - - /// Verifies `call_user_func` inside eval can dispatch a registered native function. - #[test] - fn execute_program_call_user_func_dispatches_registered_native_function() { - let program = parse_fragment(br#"return call_user_func("native_answer");"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } - - /// Verifies string variable calls inside eval can dispatch a supported builtin. - #[test] - fn execute_program_variable_call_dispatches_builtin() { - let program = parse_fragment( - br#"$fn = "strlen"; -return $fn("abcd");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); - } - - /// Verifies callable array entries can be invoked through postfix dynamic calls. - #[test] - fn execute_program_postfix_variable_call_dispatches_builtin() { - let program = parse_fragment( - br#"$callbacks = ["strlen"]; -return $callbacks[0]("abc");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(3)); - } - - /// Verifies variable calls bind eval-declared function arguments by name. - #[test] - fn execute_program_variable_call_binds_declared_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } -$fn = "dyn"; -return $fn(y: 2, x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); - } - - /// Verifies variable calls can dispatch registered native functions with named args. - #[test] - fn execute_program_variable_call_binds_registered_native_named_args() { - let program = parse_fragment( - br#"$fn = "native_answer"; -return $fn(right: 2, left: 1);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } - - /// Verifies direct callable-array variable calls dispatch object methods. - #[test] - fn execute_program_callable_array_variable_dispatches_object_method() { - let program = parse_fragment( - br#"$box = new Box(41); -$cb = [$box, "add_x"]; -return $cb(1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); - } - - /// Verifies `call_user_func` dispatches callable arrays with object receivers. - #[test] - fn execute_program_call_user_func_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(42); -$cb = [$box, "read_x"]; -return call_user_func($cb);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); - } - - /// Verifies `call_user_func_array` dispatches callable arrays with positional args. - #[test] - fn execute_program_call_user_func_array_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(39); -return call_user_func_array([$box, "add2_x"], [1, 2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); - } - - /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. - #[test] - fn execute_program_call_user_func_array_dispatches_declared_function() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } -return call_user_func_array("dyn", [4, 5]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(9)); - } - - /// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. - #[test] - fn execute_program_call_user_func_array_binds_declared_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } -return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); - } - - /// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. - #[test] - fn execute_context_function_call_array_binds_declared_named_args() { - let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - let arg_array = values.assoc_new(2).expect("allocate argument array"); - let key_y = values.string("y").expect("allocate y key"); - let value_y = values.int(2).expect("allocate y value"); - let _ = values - .array_set(arg_array, key_y, value_y) - .expect("store y argument"); - let key_x = values.string("x").expect("allocate x key"); - let value_x = values.int(1).expect("allocate x value"); - let _ = values - .array_set(arg_array, key_x, value_x) - .expect("store x argument"); - - let result = - execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) - .expect("execute context function call array"); - - assert_eq!(values.get(result), FakeValue::Int(12)); - } - - /// Verifies `call_user_func_array` rejects positional values after named keys. - #[test] - fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } -return call_user_func_array("dyn", ["y" => 2, 1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - } - - /// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. - #[test] - fn execute_program_call_user_func_array_dispatches_builtin() { - let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); - } - - /// Verifies `call_user_func_array` inside eval can dispatch a registered native function. - #[test] - fn execute_program_call_user_func_array_dispatches_registered_native_function() { - let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } - - /// Verifies `call_user_func_array` named keys can bind registered native parameters. - #[test] - fn execute_program_call_user_func_array_binds_registered_native_named_args() { - let program = parse_fragment( - br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } - - /// Verifies duplicate eval-declared function names fail in a shared context. - #[test] - fn execute_program_rejects_duplicate_declared_function() { - let define = - parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect("execute first declaration"); - let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect_err("duplicate function declaration should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } - - /// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. - #[test] - fn execute_program_dispatches_simple_builtins() { - let program = parse_fragment( - br#"echo strlen("abc") . ":" . count([1, [2, 3], [4]]) . ":"; -echo count([1, [2, 3], [4]], COUNT_RECURSIVE) . ":"; -echo call_user_func("count", [1, [2]]) . ":"; -echo call_user_func_array("count", ["value" => [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; -return defined("COUNT_RECURSIVE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:3:6:2:3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. - #[test] - fn execute_program_dispatches_json_encode_builtin() { - let program = parse_fragment( - br#"echo json_encode(["a" => 1, "b" => "x/y"]) . ":"; -echo json_encode([1, "q", true, null]) . ":"; -echo call_user_func("json_encode", "a/b\"c") . ":"; -echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; -echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; -echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; -$accent = json_decode("\"\\u00e9\""); -$emoji = json_decode("\"\\ud83d\\ude00\""); -echo bin2hex(json_encode($accent . "/" . $emoji)) . ":"; -echo bin2hex(json_encode($accent . "/" . $emoji, JSON_UNESCAPED_UNICODE)) . ":"; -echo bin2hex(json_encode([$accent => $emoji])) . ":"; -echo bin2hex(json_encode([$accent => $emoji], JSON_UNESCAPED_UNICODE)) . ":"; -echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; -echo json_encode([], JSON_FORCE_OBJECT) . ":"; -echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; -echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; -echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; -echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; -echo (json_encode(INF) === false ? "false" : "json") . ":"; -echo json_last_error() . ":" . json_last_error_msg() . ":"; -echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; -echo json_last_error() . ":" . json_last_error_msg() . ":"; -$bad = "a" . hex2bin("80") . "b"; -echo (json_encode($bad) === false ? "utf8-false" : "bad") . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR)) . ":"; -echo json_last_error() . ":"; -echo json_encode($bad, JSON_INVALID_UTF8_IGNORE) . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE)) . ":"; -echo json_last_error() . ":"; -echo json_encode([hex2bin("6b80") => hex2bin("7680")], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; -echo json_last_error() . ":"; -json_encode(3.5); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; -return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"k\ufffd":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:"# - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `json_decode()` materializes scalars, arrays, and associative arrays. - #[test] - fn execute_program_dispatches_json_decode_builtin() { - let program = parse_fragment( - br#"echo json_decode("\"hello\"") . ":"; -echo json_decode("42") . ":"; -echo (json_decode("true") ? "T" : "bad") . ":"; -echo (is_null(json_decode("null")) ? "NULL" : "bad") . ":"; -$decoded = json_decode("{\"a\":1,\"b\":[\"x\",false]}", true); -echo $decoded["a"] . ":" . $decoded["b"][0] . ":" . ($decoded["b"][1] ? "bad" : "F") . ":"; -$call = call_user_func("json_decode", "[3,4]"); -echo $call[1] . ":"; -$named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); -echo $named["k"] . ":"; -$badJson = "\"a" . hex2bin("80") . "b\""; -echo (is_null(json_decode($badJson)) ? "utf8-null" : "bad") . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_IGNORE)) . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; -echo json_last_error() . ":"; -$objSub = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_SUBSTITUTE); -$objSubKeys = array_keys($objSub); -echo bin2hex($objSubKeys[0]) . "=" . bin2hex($objSub[$objSubKeys[0]]) . ":"; -$objIgnore = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_IGNORE); -$objIgnoreKeys = array_keys($objIgnore); -echo bin2hex($objIgnoreKeys[0]) . "=" . bin2hex($objIgnore[$objIgnoreKeys[0]]) . ":"; -echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; -$big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); -echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; -echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; -echo gettype($big[0]) . ":" . $big[0] . ":"; -echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; -return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. - #[test] - fn execute_program_dispatches_json_decode_stdclass_default() { - let program = parse_fragment( - br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); -echo $object->a . ":" . $object->b->c . ":"; -$objectFalse = json_decode("{\"z\":2}", false); -echo $objectFalse->z . ":"; -$objectNull = json_decode("{\"n\":{\"m\":3}}", null); -echo $objectNull->n->m . ":"; -$assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); -echo $assoc["b"]["c"] . ":"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:x:2:3:y:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `json_encode()` serializes stdClass dynamic properties. - #[test] - fn execute_program_dispatches_json_encode_stdclass_object() { - let program = parse_fragment( - br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); -echo json_encode($object) . ":"; -echo str_replace("\n", "|", json_encode($object, JSON_PRETTY_PRINT)) . ":"; -$empty = json_decode("{}"); -echo json_encode($empty) . ":"; -$empty->a = 7; -echo json_encode($empty); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. - #[test] - fn execute_program_dispatches_json_last_error_builtins() { - let program = parse_fragment( - br#"echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_decode("bad"); -echo json_last_error() . ":" . (json_last_error() === JSON_ERROR_SYNTAX ? "syntax" : "bad") . ":" . json_last_error_msg() . ":"; -json_validate("[1]", 1); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_validate("\"ok\""); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_validate("\"a" . chr(10) . "b\""); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_decode("\"\\uD83D\""); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_decode("\"a" . chr(128) . "b\""); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_validate("[0]"); -echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_error_msg", []) . ":"; -return function_exists("json_last_error") && function_exists("json_last_error_msg") && defined("JSON_ERROR_SYNTAX");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "0:No error:4:syntax:Syntax error near location 1:1:1:Maximum stack depth exceeded near location 1:1:0:No error:3:Control character error, possibly incorrectly encoded near location 1:3:10:Single unpaired UTF-16 surrogate in unicode escape near location 1:8:5:Malformed UTF-8 characters, possibly incorrectly encoded near location 1:3:0:No error:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval JSON throw flags raise catchable Throwable objects. - #[test] - fn execute_program_dispatches_json_throw_on_error() { - let program = parse_fragment( - br#"try { - json_decode("bad", true, 512, JSON_THROW_ON_ERROR); - echo "bad"; -} catch (Throwable) { - echo "decode:"; -} -try { - json_encode(INF, JSON_THROW_ON_ERROR); - echo "bad"; -} catch (Throwable) { - echo "encode:"; -} -echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; -return defined("JSON_THROW_ON_ERROR");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "decode:encode:0:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. - #[test] - fn execute_program_dispatches_json_validate_builtin() { - let program = parse_fragment( - br#"echo (json_validate("{\"a\":[1,true,null,\"caf\\u00e9\"]}") ? "Y" : "N") . ":"; -echo (json_validate("bad") ? "bad" : "N") . ":"; -echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; -echo (call_user_func("json_validate", "\"x\"") ? "C" : "bad") . ":"; -echo (call_user_func_array("json_validate", ["json" => "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; -echo (json_validate("\"a" . chr(128) . "b\"", 512, JSON_INVALID_UTF8_IGNORE) ? "I" : "bad") . ":"; -echo json_last_error() . ":"; -echo (json_validate("bad", 512, JSON_INVALID_UTF8_IGNORE) ? "bad" : "S") . ":"; -echo json_last_error() . ":"; -return function_exists("json_validate") && defined("JSON_INVALID_UTF8_IGNORE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N:D:C:A:I:0:S:4:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies direct eval builtin calls bind named and unpacked arguments. - #[test] - fn execute_program_dispatches_named_and_spread_builtins() { - let program = parse_fragment( - br#"echo strlen(string: "abcd"); -echo ":" . (array_key_exists(array: ["name" => 1], key: "name") ? "Y" : "N"); -echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N"); -return round(precision: 1, num: 3.14);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:Y:Y"); - assert_eq!(values.get(result), FakeValue::Float(3.1)); - } - - /// Verifies eval `ord()` returns the first byte and supports callable dispatch. - #[test] - fn execute_program_dispatches_ord_builtin() { - let program = parse_fragment( - br#"echo ord("A"); -echo ":" . ord(""); -echo ":" . call_user_func("ord", "B"); -echo ":" . call_user_func_array("ord", ["C"]); -echo ":"; echo function_exists("ord"); -return ord("Z");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "65:0:66:67:1"); - assert_eq!(values.get(result), FakeValue::Int(90)); - } - - /// Verifies eval array aggregate builtins iterate array values and support callable dispatch. - #[test] - fn execute_program_dispatches_array_aggregate_builtins() { - let program = parse_fragment( - br#"echo array_sum([1, 2, 3]); -echo ":" . array_product([2, 3, 4]); -echo ":" . array_sum([]); -echo ":" . array_product([]); -echo ":" . array_sum(["a" => 2, "b" => 5]); -echo ":" . call_user_func("array_sum", [3, 4]); -echo ":" . call_user_func_array("array_product", [[2, 5]]); -echo ":"; echo function_exists("array_sum"); -return function_exists("array_product");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "6:24:0:1:7:7:10:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_map()` applies callbacks and preserves source keys. - #[test] - fn execute_program_dispatches_array_map_builtin() { - let program = parse_fragment( - br#"function eval_map_double($value) { return $value * 2; } -$mapped = array_map("eval_map_double", [1, 2, 3]); -echo $mapped[0] . ":" . $mapped[2] . ":"; -$assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); -echo $assoc["a"] . ":" . $assoc["b"] . ":"; -$identity = array_map(null, ["k" => "v"]); -echo $identity["k"] . ":"; -function eval_map_pair($left, $right) { return $left . "-" . ($right ?? "N"); } -$pairs = array_map("eval_map_pair", ["a" => "L", "b" => "R"], ["x" => "1"]); -echo $pairs[0] . ":" . $pairs[1] . ":"; -$zipped = array_map(null, [1, 2], [3, 4]); -echo $zipped[0][0] . $zipped[0][1] . ":" . $zipped[1][0] . $zipped[1][1] . ":"; -$call = call_user_func("array_map", "intval", ["7"]); -echo $call[0] . ":"; -$multi_call = call_user_func("array_map", "eval_map_pair", ["Q"], ["9"]); -echo $multi_call[0] . ":"; -$spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); -echo $spread[0] . ":"; -return function_exists("array_map");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_reduce()` folds values through a string callback. - #[test] - fn execute_program_dispatches_array_reduce_builtin() { - let program = parse_fragment( - br#"function eval_reduce_sum($carry, $item) { return $carry + $item; } -echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; -function eval_reduce_join($carry, $item) { return $carry . $item; } -echo array_reduce([4, 5], "eval_reduce_sum") . ":"; -echo array_reduce(["a", "b"], "eval_reduce_join", "") . ":"; -$named = array_reduce(array: [6, 7], callback: "eval_reduce_sum"); -echo $named . ":"; -$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum"); -echo $call . ":"; -$spread = call_user_func_array("array_reduce", ["array" => [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); -echo $spread . ":"; -return function_exists("array_reduce");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "16:9:ab:13:9:9:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_walk()` invokes string callbacks with value and key cells. - #[test] - fn execute_program_dispatches_array_walk_builtin() { - let program = parse_fragment( - br#"function eval_walk_show($value, $key) { echo $key . "=" . $value . ";"; } -echo array_walk(["a" => 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; -$call = call_user_func("array_walk", [4, 5], "eval_walk_show"); -$spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); -return function_exists("array_walk");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_pop()` and `array_shift()` write back only for direct variable calls. - #[test] - fn execute_program_dispatches_array_pop_shift_builtins() { - let program = parse_fragment( - br#"$a = [1, 2, 3]; -echo array_pop($a) . ":" . count($a) . ":" . $a[1] . ":"; -$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; -echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; -$c = [4, 5]; -echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; -$d = [6, 7]; -echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; -return function_exists("array_pop") && function_exists("array_shift");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:2:2:1:2:3:4:5:2:5:6:2:6:"); - assert_eq!( - values.warnings, - vec![ - "array_pop(): Argument #1 ($array) must be passed by reference, value given", - "array_shift(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_push()` and `array_unshift()` write back direct variable calls. - #[test] - fn execute_program_dispatches_array_push_unshift_builtins() { - let program = parse_fragment( - br#"$a = [1]; -echo array_push($a, 2, 3) . ":" . $a[2] . ":"; -$b = ["x" => 1, 10 => 2]; -echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; -$c = [2, 3]; -echo array_unshift($c, 0, 1) . ":" . $c[0] . ":" . $c[3] . ":"; -$d = ["x" => 1, 10 => 2, "y" => 3]; -echo array_unshift($d, "A") . ":" . $d[0] . ":" . $d["x"] . ":" . $d[1] . ":" . $d["y"] . ":"; -$e = [5]; -echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; -$f = [7]; -echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; -return function_exists("array_push") && function_exists("array_unshift");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:"); - assert_eq!( - values.warnings, - vec![ - "array_push(): Argument #1 ($array) must be passed by reference, value given", - "array_unshift(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_splice()` returns removed values and writes back direct variable calls. - #[test] - fn execute_program_dispatches_array_splice_builtin() { - let program = parse_fragment( - br#"$a = [10, 20, 30, 40]; -$removed = array_splice($a, 1, 2); -echo count($removed) . ":" . $removed[0] . ":" . $removed[1] . ":" . count($a) . ":" . $a[1] . ":"; -$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; -$cut = array_splice(array: $b, offset: 1, length: 2); -echo $cut[0] . ":" . $cut["y"] . ":" . $b["x"] . ":" . $b[0] . ":"; -$c = [1, 2, 3, 4]; -$tail = call_user_func("array_splice", $c, -2, 1); -echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; -$d = [5, 6, 7]; -$all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); -echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; -$e = [1, 2, 3, 4]; -$rep = array_splice($e, 1, 2, ["A", "B"]); -echo count($rep) . ":" . $rep[0] . ":" . $rep[1] . ":" . $e[0] . ":" . $e[1] . ":" . $e[2] . ":" . $e[3] . ":"; -$f = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; -$rep2 = array_splice(array: $f, offset: 1, length: 2, replacement: ["s" => "S", 9 => "N"]); -echo $rep2[0] . ":" . $rep2["y"] . ":" . $f["x"] . ":" . $f[0] . ":" . $f[1] . ":" . $f[2] . ":"; -$g = [1, 2, 3]; -$rep3 = array_splice($g, offset: 1, replacement: [9]); -echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g[1] . ":"; -$h = [1, 2, 3]; -$removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); -echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; -return function_exists("array_splice");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:" - ); - assert_eq!( - values.warnings, - vec![ - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `sort()` and `rsort()` reindex direct variable arrays only. - #[test] - fn execute_program_dispatches_sort_builtins() { - let program = parse_fragment( - br#"$a = [3, 1, 2]; -echo sort($a) . ":" . $a[0] . $a[1] . $a[2] . ":"; -$b = ["banana", "apple", "cherry"]; -echo rsort(array: $b) . ":" . $b[0] . ":" . $b[2] . ":"; -$c = ["x" => 3, "y" => 1, "z" => 2]; -sort($c); -echo $c[0] . $c[1] . $c[2] . ":"; -$d = [3, 1, 2]; -echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; -$e = [1, 2, 3]; -echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; -return function_exists("sort") && function_exists("rsort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:123:1:cherry:apple:123:1:312:1:1:3:"); - assert_eq!( - values.warnings, - vec![ - "sort(): Argument #1 ($array) must be passed by reference, value given", - "rsort(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval key-preserving array ordering builtins write back direct variable calls. - #[test] - fn execute_program_dispatches_key_preserving_sort_builtins() { - let program = parse_fragment( - br#"$a = ["x" => 3, "y" => 1, "z" => 2]; -echo asort($a) . ":"; -foreach ($a as $key => $value) { echo $key . $value; } -echo ":"; -$b = ["x" => 1, "y" => 3, "z" => 2]; -echo arsort(array: $b) . ":"; -foreach ($b as $key => $value) { echo $key . $value; } -echo ":"; -$c = ["b" => 1, "a" => 2, 3 => 4]; -echo ksort($c) . ":"; -foreach ($c as $key => $value) { echo $key . $value; } -echo ":"; -$d = ["b" => 1, "a" => 2, 3 => 4]; -echo krsort($d) . ":"; -foreach ($d as $key => $value) { echo $key . $value; } -echo ":"; -$e = ["x" => 2, "y" => 1]; -echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; -$f = ["b" => 1, "a" => 2]; -echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; -return function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:" - ); - assert_eq!( - values.warnings, - vec![ - "asort(): Argument #1 ($array) must be passed by reference, value given", - "krsort(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval natural sort builtins preserve keys and use natural string order. - #[test] - fn execute_program_dispatches_natural_sort_builtins() { - let program = parse_fragment( - br#"$a = ["img10", "img2", "img1"]; -echo natsort($a) . ":"; -foreach ($a as $key => $value) { echo $key . $value . ";"; } -echo ":"; -$b = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; -echo natcasesort(array: $b) . ":"; -foreach ($b as $key => $value) { echo $key . $value . ";"; } -echo ":"; -$c = ["x" => "b", "y" => "a"]; -echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; -return function_exists("natsort") && function_exists("natcasesort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:" - ); - assert_eq!( - values.warnings, - vec!["natsort(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `shuffle()` reindexes direct variable arrays only. - #[test] - fn execute_program_dispatches_shuffle_builtin() { - let program = parse_fragment( - br#"$a = ["x" => 1, "y" => 2]; -echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; -$b = ["x" => 1, "y" => 2]; -echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; -return function_exists("shuffle");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:reindexed:2:3:1:12:"); - assert_eq!( - values.warnings, - vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval user-comparator sort builtins call callbacks and write back direct arrays. - #[test] - fn execute_program_dispatches_user_sort_builtins() { - let program = parse_fragment( - br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } -function eval_key_cmp($left, $right) { return strcmp($left, $right); } -$a = [3, 1, 2]; -echo usort($a, "eval_sort_cmp") . ":"; -foreach ($a as $value) { echo $value; } -echo ":"; -$b = ["b" => 1, "a" => 3, "c" => 2]; -echo uasort(array: $b, callback: "eval_sort_cmp") . ":"; -foreach ($b as $key => $value) { echo $key . $value; } -echo ":"; -$c = ["b" => 1, "a" => 2]; -echo uksort($c, "eval_key_cmp") . ":"; -foreach ($c as $key => $value) { echo $key . $value; } -echo ":"; -$d = [2, 1]; -echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; -return function_exists("usort") && function_exists("uasort") && function_exists("uksort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:"); - assert_eq!( - values.warnings, - vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval iterator array helpers support direct and dynamic builtin calls. - #[test] - fn execute_program_dispatches_iterator_array_builtins() { - let program = parse_fragment( - br#"$items = ["x" => 1, "y" => 2]; -$copy = iterator_to_array($items); -echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; -$values = iterator_to_array($items, false); -echo (isset($values["x"]) ? "bad" : "reindexed") . ":" . $values[0] . $values[1] . ":"; -echo call_user_func("iterator_count", $items) . ":"; -$spread = call_user_func_array("iterator_to_array", ["iterator" => $items, "preserve_keys" => false]); -echo $spread[0] . $spread[1] . ":"; -return function_exists("iterator_count") && function_exists("iterator_to_array");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:12:reindexed:12:2:12:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `iterator_apply()` drives Iterator objects and callback args. - #[test] - fn execute_program_dispatches_iterator_apply_object_builtin() { - let program = parse_fragment( - br#"function eval_apply($prefix) { echo $prefix; return true; } -echo iterator_apply($it, "eval_apply", ["prefix" => "x"]) . ":"; -echo call_user_func("iterator_apply", $it, "eval_apply", ["y"]) . ":"; -return function_exists("iterator_apply");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "xxx3:yyy3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `iterator_apply()` accepts object-method callable arrays. - #[test] - fn execute_program_iterator_apply_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(5); -echo iterator_apply($it, [$box, "add_x"], [1]) . ":"; -return call_user_func("iterator_apply", $it, [$box, "add_x"], [1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:"); - assert_eq!(values.get(result), FakeValue::Int(3)); - } - - /// Verifies eval `iterator_apply()` counts the position where the callback stops. - #[test] - fn execute_program_iterator_apply_stops_on_falsey_callback() { - let program = parse_fragment( - br#"function eval_stop() { echo "s"; return false; } -return iterator_apply($it, "eval_stop");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "s"); - assert_eq!(values.get(result), FakeValue::Int(1)); - } - - /// Verifies eval `array_filter()` removes falsey values while preserving original keys. - #[test] - fn execute_program_dispatches_array_filter_builtin() { - let program = parse_fragment( - br#"$filtered = array_filter([0, 1, 2, "", false, null, "0", "ok"]); -echo count($filtered) . ":" . $filtered[1] . ":" . $filtered[2] . ":" . $filtered[7] . ":"; -$assoc = array_filter(["a" => 0, "b" => 2, "c" => ""]); -echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; -$null = array_filter([0, 3], null, 1); -echo count($null) . ":" . $null[1] . ":"; -$call = call_user_func("array_filter", [0, 4]); -echo count($call) . ":" . $call[1] . ":"; -$spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); -echo count($spread) . ":" . $spread[1] . ":"; -function eval_keep_even($value) { return $value % 2 == 0; } -$evens = array_filter([1, 2, 3, 4], "eval_keep_even"); -echo count($evens) . ":" . $evens[1] . ":" . $evens[3] . ":"; -function eval_keep_key($key) { return $key === "b"; } -$keyed = array_filter(["a" => 10, "b" => 20], "eval_keep_key", ARRAY_FILTER_USE_KEY); -echo count($keyed) . ":" . $keyed["b"] . ":"; -function eval_keep_both($value, $key) { return $key === "c" || $value === 1; } -$both = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_keep_both", ARRAY_FILTER_USE_BOTH); -echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; -$ints = array_filter([1, "x", 2], "is_int"); -echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; -return function_exists("array_filter");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_combine()` converts key values through PHP string-key rules. - #[test] - fn execute_program_dispatches_array_combine_builtin() { - let program = parse_fragment( - br#"$pairs = array_combine(["a", "b"], [10, 20]); -echo $pairs["a"] . ":" . $pairs["b"]; -$numeric = array_combine(["1", "01"], ["n", "z"]); -echo ":" . $numeric[1] . $numeric["01"]; -$scalar = array_combine([null, true, false, 2.8], ["n", "t", "f", "d"]); -echo ":" . $scalar[""] . $scalar[1] . $scalar["2.8"]; -$named = array_combine(keys: ["k"], values: ["v"]); -echo ":" . $named["k"]; -$call = call_user_func("array_combine", ["x"], [7]); -echo ":" . $call["x"]; -$spread = call_user_func_array("array_combine", [["y"], [8]]); -echo ":" . $spread["y"] . ":"; -return function_exists("array_combine");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "10:20:nz:ftd:v:7:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_column()` extracts present row columns and reindexes them. - #[test] - fn execute_program_dispatches_array_column_builtin() { - let program = parse_fragment( - br#"$rows = [["name" => "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; -$names = array_column($rows, "name"); -echo count($names) . ":" . $names[0] . ":" . $names[1]; -$scores = array_column($rows, "score"); -echo ":" . count($scores) . ":" . $scores[0] . $scores[2]; -$numeric = array_column([[0 => "zero", 1 => "one"], [1 => "uno"]], 1); -echo ":" . count($numeric) . ":" . $numeric[0] . ":" . $numeric[1]; -$named = array_column(array: $rows, column_key: "score"); -echo ":" . $named[1]; -$call = call_user_func("array_column", [["x" => 5], ["x" => 6]], "x"); -echo ":" . $call[1]; -$spread = call_user_func_array("array_column", [[["y" => 7], ["z" => 0], ["y" => 9]], "y"]); -echo ":" . count($spread) . ":" . $spread[1] . ":"; -return function_exists("array_column");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. - #[test] - fn execute_program_dispatches_array_shape_builtins() { - let program = parse_fragment( - br#"$right = array_pad([1, 2], 5, 0); -echo count($right) . ":" . $right[0] . $right[1] . $right[2] . $right[4]; -$left = array_pad([1, 2], -4, 9); -echo ":" . $left[0] . $left[1] . $left[2] . $left[3]; -$copy = array_pad([7, 8], 1, 0); -echo ":" . count($copy) . ":" . $copy[0] . $copy[1]; -$chunks = array_chunk([1, 2, 3, 4, 5], 2); -echo ":" . count($chunks) . ":" . $chunks[0][1] . $chunks[2][0]; -$named = array_pad(array: ["a"], length: 2, value: "b"); -echo ":" . $named[1]; -$call = call_user_func("array_chunk", [6, 7, 8], 2); -echo ":" . $call[1][0]; -$spread = call_user_func_array("array_pad", [[1], 3, 2]); -echo ":" . $spread[2] . ":"; -return function_exists("array_pad") && function_exists("array_chunk");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:1200:9912:2:78:3:25:b:8:2:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_slice()` observes PHP offset and length bounds. - #[test] - fn execute_program_dispatches_array_slice_builtin() { - let program = parse_fragment( - br#"$mid = array_slice([10, 20, 30, 40, 50], 1, 3); -echo count($mid) . ":" . $mid[0] . $mid[1] . $mid[2]; -$tail = array_slice([10, 20, 30, 40], -2, 1); -echo ":" . $tail[0]; -$open = array_slice([10, 20, 30, 40, 50], 2); -echo ":" . count($open) . ":" . $open[0] . $open[2]; -$null_len = array_slice([5, 6, 7], 1, null); -echo ":" . $null_len[0] . $null_len[1]; -$negative_len = array_slice([10, 20, 30, 40, 50], 1, -1); -echo ":" . count($negative_len) . ":" . $negative_len[0] . $negative_len[2]; -$named = array_slice(array: [1, 2, 3], offset: 1, length: 1); -echo ":" . $named[0]; -$call = call_user_func("array_slice", [6, 7, 8], 1, 2); -echo ":" . $call[1]; -$spread = call_user_func_array("array_slice", [[9, 10, 11], 1]); -echo ":" . $spread[0] . ":"; -return function_exists("array_slice");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:203040:30:3:3050:67:3:2040:2:8:10:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. - #[test] - fn execute_program_dispatches_array_merge_builtin() { - let program = parse_fragment( - br#"$merged = array_merge([1, 2], [3, 4]); -echo count($merged) . ":" . $merged[0] . $merged[1] . $merged[2] . $merged[3]; -$left = [1, 2]; -$right = [3]; -$copy = array_merge($left, $right); -echo ":" . count($left) . ":" . $left[0] . ":" . $copy[2]; -$assoc = array_merge(["a" => 1, 2 => "x"], ["a" => 9, 5 => "y", "b" => 3]); -echo ":" . $assoc["a"] . ":" . $assoc[0] . ":" . $assoc[1] . ":" . $assoc["b"]; -$call = call_user_func("array_merge", [6], [7, 8]); -echo ":" . $call[2]; -$spread = call_user_func_array("array_merge", [[9], [10]]); -echo ":" . $spread[1] . ":"; -return function_exists("array_merge");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:1234:2:1:3:9:x:y:3:8:10:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_diff()` and `array_intersect()` compare values as strings. - #[test] - fn execute_program_dispatches_array_value_set_builtins() { - let program = parse_fragment( - br#"$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); -echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; -echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); -echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); -$inter = array_intersect(["a" => 1, "b" => 2, "c" => "2", "d" => 3], ["2", 4]); -echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter["c"]; -$call = call_user_func("array_diff", [1, 2, 3], [2]); -echo ":" . count($call) . ":" . $call[0] . $call[2]; -$spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); -echo ":" . count($spread) . ":" . $spread[2] . ":"; -return function_exists("array_diff") && function_exists("array_intersect");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. - #[test] - fn execute_program_dispatches_array_key_set_builtins() { - let program = parse_fragment( - br#"$diff = array_diff_key(["a" => 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); -echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; -echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); -$inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); -echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter[4]; -$call = call_user_func("array_diff_key", [10, 20, 30], [1 => 0]); -echo ":" . count($call) . ":" . $call[0] . $call[2]; -$spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); -echo ":" . count($spread) . ":" . $spread["y"] . ":"; -return function_exists("array_diff_key") && function_exists("array_intersect_key");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:2:3:no-a:2:2:3:2:1030:1:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `range()` builds inclusive ascending and descending integer arrays. - #[test] - fn execute_program_dispatches_range_builtin() { - let program = parse_fragment( - br#"$up = range(1, 4); -echo count($up) . ":" . $up[0] . $up[3]; -$down = range(4, 1); -echo ":" . count($down) . ":" . $down[0] . $down[3]; -$single = range(3, 3); -echo ":" . count($single) . ":" . $single[0]; -$named = range(start: 2, end: 4); -echo ":" . $named[0] . $named[2]; -$call = call_user_func("range", 5, 7); -echo ":" . $call[2]; -$spread = call_user_func_array("range", [8, 6]); -echo ":" . count($spread) . ":" . $spread[0] . $spread[2] . ":"; -return function_exists("range");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:14:4:41:1:3:24:7:3:86:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_rand()` returns a key that exists in the source array. - #[test] - fn execute_program_dispatches_array_rand_builtin() { - let program = parse_fragment( - br#"$nums = [10, 20, 30]; -$idx = array_rand($nums); -echo ($idx >= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; -$assoc = ["a" => 1, "b" => 2]; -$key = array_rand($assoc); -echo ":" . (array_key_exists($key, $assoc) ? "assoc" : "bad"); -$named = array_rand(array: [5, 6]); -echo ":" . (($named >= 0 && $named < 2) ? "named" : "bad"); -$call = call_user_func("array_rand", [7, 8]); -echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); -$spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); -echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; -return function_exists("array_rand");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "idx:assoc:named:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval random builtins return values inside PHP inclusive ranges. - #[test] - fn execute_program_dispatches_rand_builtins() { - let program = parse_fragment( - br#"$plain = rand(); -echo ($plain >= 0 && $plain <= 2147483647) ? "plain" : "bad"; -$bounded = rand(2, 4); -echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); -$same = mt_rand(max: 6, min: 6); -echo ":" . ($same === 6 ? "same" : "bad"); -$swapped = rand(10, 1); -echo ":" . (($swapped >= 1 && $swapped <= 10) ? "swap" : "bad"); -$call = call_user_func("mt_rand", 1, 1); -echo ":" . ($call === 1 ? "call" : "bad"); -$spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); -echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; -$secure = random_int(max: 4, min: 4); -echo ($secure === 4 ? "random" : "bad") . ":"; -$secureCall = call_user_func("random_int", 5, 5); -echo ($secureCall === 5 ? "random-call" : "bad") . ":"; -$secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); -echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; -echo function_exists("rand"); -echo function_exists("mt_rand"); -return function_exists("random_int");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "plain:range:same:swap:call:spread:random:random-call:random-spread:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. - #[test] - fn execute_program_dispatches_array_fill_builtins() { - let program = parse_fragment( - br#"$filled = array_fill(2, 3, "x"); -echo count($filled) . ":" . $filled[2] . $filled[4]; -$negative = array_fill(-2, 3, 7); -echo ":" . $negative[-2] . $negative[-1] . $negative[0]; -$empty = array_fill(5, 0, "x"); -echo ":" . count($empty); -$map = array_fill_keys(["a", "1", "01"], 8); -echo ":" . $map["a"] . ":" . $map[1] . ":" . $map["01"]; -$named = array_fill(start_index: 1, count: 2, value: "n"); -echo ":" . $named[1] . $named[2]; -$call = call_user_func("array_fill", 0, 2, "c"); -echo ":" . $call[0] . $call[1]; -$spread = call_user_func_array("array_fill_keys", [["x", "y"], "z"]); -echo ":" . $spread["x"] . $spread["y"] . ":"; -return function_exists("array_fill") && function_exists("array_fill_keys");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:xx:777:0:8:8:8:nn:cc:zz:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. - #[test] - fn execute_program_dispatches_array_flip_builtin() { - let program = parse_fragment( - br#"$flipped = array_flip(["a" => "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); -echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); -$named = array_flip(array: ["k" => "v"]); -echo ":" . $named["v"]; -$call = call_user_func("array_flip", ["left" => "right"]); -echo ":" . $call["right"]; -$spread = call_user_func_array("array_flip", [["n" => 9]]); -echo ":" . $spread[9] . ":"; -return function_exists("array_flip");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "c:b:d:e:4:k:left:n:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_unique()` preserves first keys using default string comparison. - #[test] - fn execute_program_dispatches_array_unique_builtin() { - let program = parse_fragment( - br#"$unique = array_unique(["a", "b", "a", "2", 2]); -echo count($unique) . ":" . $unique[0] . $unique[1] . $unique[3]; -$assoc = array_unique(["x" => "a", "y" => "b", "z" => "a"]); -echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; -$scalar = array_unique([1, "1", 1.0, true, false, null, ""]); -echo ":" . count($scalar) . ":" . $scalar[0] . ":"; -echo $scalar[4] ? "bad" : "F"; -$named = array_unique(array: ["k" => "v", "l" => "v"]); -echo ":" . $named["k"] . ":" . count($named); -$call = call_user_func("array_unique", ["q", "q", "r"]); -echo ":" . $call[0] . $call[2]; -$spread = call_user_func_array("array_unique", [["s", "s", "t"]]); -echo ":" . $spread[0] . $spread[2] . ":"; -return function_exists("array_unique");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:ab2:2:ab:2:1:F:v:1:qr:st:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval array projection builtins produce indexed key/value arrays. - #[test] - fn execute_program_dispatches_array_projection_builtins() { - let program = parse_fragment( - br#"$values = array_values(["a" => 10, "b" => 20]); -echo $values[0] . ":" . $values[1]; -$keys = array_keys(["a" => 10, "b" => 20]); -echo ":" . $keys[0] . ":" . $keys[1]; -echo ":" . count(array_values([])); -$call_keys = call_user_func("array_keys", ["z" => 7]); -echo ":" . $call_keys[0]; -$call_values = call_user_func_array("array_values", [["q" => 8]]); -echo ":" . $call_values[0]; -echo ":"; echo function_exists("array_keys"); -return function_exists("array_values");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "10:20:a:b:0:z:8:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_reverse()` handles PHP key preservation rules. - #[test] - fn execute_program_dispatches_array_reverse_builtin() { - let program = parse_fragment( - br#"$indexed = array_reverse([1, 2, 3]); -echo $indexed[0]; echo $indexed[1]; echo $indexed[2]; echo ":"; -$mixed = array_reverse([2 => "a", "k" => "b", 5 => "c"]); -echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; -$preserved = array_reverse([2 => "a", "k" => "b", 5 => "c"], true); -echo $preserved[5]; echo $preserved["k"]; echo $preserved[2]; echo ":"; -$named = array_reverse(array: ["x", "y"], preserve_keys: true); -echo $named[1]; echo $named[0]; echo ":"; -$call = call_user_func("array_reverse", [4, 5]); -echo $call[0]; echo $call[1]; echo ":"; -$spread = call_user_func_array("array_reverse", [[6, 7]]); -echo $spread[0]; echo $spread[1]; echo ":"; -return function_exists("array_reverse");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "321:cba:cba:yx:54:76:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. - #[test] - fn execute_program_dispatches_array_key_exists_builtin() { - let program = parse_fragment( - br#"$map = ["name" => null, "age" => 30]; -echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; -echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; -echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; -echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; -echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; -echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; echo ":"; -return function_exists("array_key_exists");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N:Y:N:Y:Y:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval array search builtins use loose comparison and return keys or booleans. - #[test] - fn execute_program_dispatches_array_search_builtins() { - let program = parse_fragment( - br#"echo in_array(2, [1, 2, 3]) ? "Y" : "bad"; -echo ":"; echo in_array(4, [1, 2, 3]) ? "bad" : "N"; -echo ":" . array_search(20, [10, 20, 30]); -echo ":" . array_search("Grace", ["name" => "Grace"]); -echo ":"; echo array_search("x", ["name" => "Grace"]) === false ? "F" : "bad"; -echo ":"; echo call_user_func("in_array", "b", ["a", "b"]) ? "C" : "bad"; -$found = call_user_func_array("array_search", ["v", ["k" => "v"]]); -echo ":" . $found; -echo ":"; echo function_exists("in_array"); -return function_exists("array_search");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N:1:name:F:C:k:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. - #[test] - fn execute_program_dispatches_explode_implode_builtins() { - let program = parse_fragment( - br#"$parts = explode(",", "a,b,"); -echo count($parts); echo ":" . $parts[0] . ":" . $parts[1] . ":" . $parts[2]; -echo ":" . implode("|", $parts); -echo ":" . implode(separator: "-", array: ["x", 2, true, null]); -$call_parts = call_user_func("explode", ":", "m:n"); -echo ":" . $call_parts[1]; -echo ":" . call_user_func_array("implode", ["separator" => "/", "array" => ["p", "q"]]); -echo ":"; echo function_exists("explode"); -return function_exists("implode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:a:b::a|b|:x-2-1-:n:p/q:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `str_split()` builds indexed arrays of fixed-width chunks. - #[test] - fn execute_program_dispatches_str_split_builtin() { - let program = parse_fragment( - br#"$letters = str_split("abc"); -echo count($letters) . ":" . $letters[0] . $letters[1] . $letters[2]; echo ":"; -$pairs = str_split(string: "abcd", length: 2); -echo $pairs[0] . "-" . $pairs[1]; echo ":"; -$empty = str_split(""); -echo count($empty); echo ":"; -$call = call_user_func("str_split", "xyz", 2); -echo $call[0] . "-" . $call[1]; echo ":"; -$named = call_user_func_array("str_split", ["string" => "pqrs", "length" => 3]); -echo $named[0] . "-" . $named[1]; echo ":"; -return function_exists("str_split");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:abc:ab-cd:0:xy-z:pqr-s:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `str_pad()` supports PHP left, right, and both-side padding modes. - #[test] - fn execute_program_dispatches_str_pad_builtin() { - let program = parse_fragment( - br#"echo "[" . str_pad("hi", 5) . "]"; echo ":"; -echo "[" . str_pad(string: "hi", length: 5, pad_string: "_", pad_type: 0) . "]"; echo ":"; -echo "[" . str_pad("x", 6, "ab", 2) . "]"; echo ":"; -echo call_user_func("str_pad", "42", 5, "0", 0); echo ":"; -echo call_user_func_array("str_pad", ["string" => "x", "length" => 3, "pad_string" => "."]); echo ":"; -return function_exists("str_pad");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "[hi ]:[___hi]:[abxaba]:00042:x..:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval string replacement builtins support direct, named, and callable dispatch. - #[test] - fn execute_program_dispatches_string_replace_builtins() { - let program = parse_fragment( - br#"echo str_replace("o", "0", "Hello World"); echo ":"; -echo str_replace(search: "aa", replace: "b", subject: "aaaa"); echo ":"; -echo str_replace("", "x", "abc"); echo ":"; -echo str_ireplace("HE", "ye", "Hello he"); echo ":"; -echo call_user_func("str_replace", "l", "L", "hello"); echo ":"; -echo call_user_func_array("str_ireplace", ["search" => "x", "replace" => "Y", "subject" => "xX"]); echo ":"; -echo function_exists("str_replace"); -return function_exists("str_ireplace");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. - #[test] - fn execute_program_dispatches_preg_builtins() { - let program = parse_fragment( - br#"$ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); -echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; -echo preg_match("/xyz/", "id42") . ":"; -echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; -$allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); -echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; -$setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); -echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; -preg_match("/(a)?(b)/", "b", $offsetOne, PREG_OFFSET_CAPTURE); -echo $offsetOne[0][0] . ":" . $offsetOne[0][1] . ":" . $offsetOne[1][0] . ":" . $offsetOne[1][1] . ":" . $offsetOne[2][0] . ":" . $offsetOne[2][1] . ":"; -preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); -echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; -preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); -echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; -preg_match("/(a)?(b)(c)?/", "b", $nullOne, PREG_UNMATCHED_AS_NULL); -echo count($nullOne) . ":" . ($nullOne[1] === null ? "n" : "bad") . ":" . $nullOne[2] . ":" . ($nullOne[3] === null ? "n" : "bad") . ":"; -preg_match("/(a)?(b)(c)?/", "b", $nullOffset, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); -echo ($nullOffset[1][0] === null ? "n" : "bad") . ":" . $nullOffset[1][1] . ":" . ($nullOffset[3][0] === null ? "n" : "bad") . ":" . $nullOffset[3][1] . ":"; -preg_match_all("/(a)?(b)(c)?/", "b", $nullAll, PREG_UNMATCHED_AS_NULL); -echo ($nullAll[1][0] === null ? "n" : "bad") . ":" . $nullAll[2][0] . ":" . ($nullAll[3][0] === null ? "n" : "bad") . ":"; -preg_match_all("/(a)?(b)(c)?/", "b", $nullSet, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); -echo ($nullSet[0][1][0] === null ? "n" : "bad") . ":" . $nullSet[0][1][1] . ":" . ($nullSet[0][3][0] === null ? "n" : "bad") . ":" . $nullSet[0][3][1] . ":"; -preg_match_all("/(x)(y)/", "abc", $none); -echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; -echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; -function eval_regex_wrap($matches) { return "[" . $matches[0] . "]"; } -echo preg_replace_callback("/[A-Z]/", "eval_regex_wrap", "AB") . ":"; -$limited = preg_split("/,/", "a,b,c", 2); -echo count($limited) . ":" . $limited[0] . ":" . $limited[1] . ":"; -$kept = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY); -echo count($kept) . ":" . $kept[1] . ":"; -echo call_user_func("preg_match", "/x/", "x") . ":"; -$replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "replacement" => "N", "subject" => "a12"]); -echo $replaced . ":"; -$captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); -echo count($captured) . ":" . $captured[1] . ":"; -$splitOffsets = preg_split("/,/", "a,b,c", 2, PREG_SPLIT_OFFSET_CAPTURE); -echo $splitOffsets[0][0] . ":" . $splitOffsets[0][1] . ":" . $splitOffsets[1][0] . ":" . $splitOffsets[1][1] . ":"; -$splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); -echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; -$splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); -echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; -return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. - #[test] - fn execute_program_dispatches_html_entity_builtins() { - let program = parse_fragment( - br#"echo htmlspecialchars("\"Hi\" & 'bye'"); echo ":"; -echo htmlentities(string: "
"); echo ":"; -echo html_entity_decode("<b>hi</b>"); echo ":"; -echo call_user_func("htmlspecialchars", ""); echo ":"; -echo call_user_func_array("html_entity_decode", ["string" => ""q""]); echo ":"; -echo function_exists("htmlspecialchars"); echo function_exists("htmlentities"); -return function_exists("html_entity_decode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval URL codec builtins dispatch through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_url_codec_builtins() { - let program = parse_fragment( - br#"echo urlencode("a b&=~"); echo ":"; -echo rawurlencode(string: "a b&=~"); echo ":"; -echo urldecode("a+b%26%3D%7E"); echo ":"; -echo rawurldecode("a+b%26%3D%7E"); echo ":"; -echo call_user_func("urlencode", "%zz"); echo ":"; -echo call_user_func_array("rawurldecode", ["string" => "x%2By%zz"]); echo ":"; -echo function_exists("urlencode"); echo function_exists("rawurlencode"); -echo function_exists("urldecode"); -return function_exists("rawurldecode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_ctype_builtins() { - let program = parse_fragment( - br#"echo ctype_alpha("abc") ? "A" : "-"; echo ":"; -echo ctype_digit(text: "123") ? "D" : "-"; echo ":"; -echo ctype_alnum("a1") ? "N" : "-"; echo ":"; -echo ctype_space(" \t\n" . chr(11) . chr(12) . "\r") ? "S" : "-"; echo ":"; -echo ctype_alpha("") ? "bad" : "empty"; echo ":"; -echo call_user_func("ctype_digit", "12x") ? "bad" : "not-digit"; echo ":"; -echo call_user_func_array("ctype_space", ["text" => " x"]) ? "bad" : "not-space"; echo ":"; -echo function_exists("ctype_alpha"); echo function_exists("ctype_digit"); -echo function_exists("ctype_alnum"); -return function_exists("ctype_space");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A:D:N:S:empty:not-digit:not-space:111"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. - #[test] - fn execute_program_dispatches_crc32_builtin() { - let program = parse_fragment( - br#"echo crc32(""); echo ":"; -echo crc32(string: "123456789"); echo ":"; -echo call_user_func("crc32", "hello"); echo ":"; -echo call_user_func_array("crc32", ["string" => "The quick brown fox jumps over the lazy dog"]); echo ":"; -return function_exists("crc32");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "0:3421780262:907060870:1095738169:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `hash_algos()` returns supported hash names through callable dispatch too. - #[test] - fn execute_program_dispatches_hash_algos_builtin() { - let program = parse_fragment( - br#"$algos = hash_algos(); -echo count($algos) . ":" . $algos[0] . ":" . $algos[5] . ":"; -echo in_array("crc32c", $algos) ? "crc" : "bad"; -$call = call_user_func("hash_algos"); -echo ":" . $call[18]; -$spread = call_user_func_array("hash_algos", []); -echo ":" . $spread[27] . ":"; -echo function_exists("hash_algos") ? "exists" : "missing"; -return count($algos);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "28:md2:sha256:crc:whirlpool:joaat:exists"); - assert_eq!(values.get(result), FakeValue::Int(28)); - } - - /// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. - #[test] - fn execute_program_dispatches_hash_digest_builtins() { - let filename = format!("elephc_eval_hash_file_{}.txt", std::process::id()); - let source = format!( - r#"echo md5("abc"); echo ":"; -echo sha1(string: "abc"); echo ":"; -echo hash("sha256", "abc"); echo ":"; -echo hash_hmac(algo: "sha256", data: "data", key: "key"); echo ":"; -echo bin2hex(md5("abc", true)); echo ":"; -echo bin2hex(call_user_func("sha1", "abc", true)); echo ":"; -echo call_user_func_array("hash", ["algo" => "md5", "data" => "abc"]); echo ":"; -echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; -file_put_contents("{filename}", "abc"); -echo hash_file("sha256", "{filename}"); echo ":"; -echo bin2hex(hash_file(algo: "md5", filename: "{filename}", binary: true)); echo ":"; -echo call_user_func_array("hash_file", ["algo" => "md5", "filename" => "{filename}"]); echo ":"; -echo hash_file("sha256", "{filename}.missing") === false ? "missing" : "bad"; echo ":"; -unlink("{filename}"); -echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); -return function_exists("hash_hmac");"#, - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - concat!( - "900150983cd24fb0d6963f7d28e17f72:", - "a9993e364706816aba3e25717850c26c9cd0d89d:", - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", - "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", - "900150983cd24fb0d6963f7d28e17f72:", - "a9993e364706816aba3e25717850c26c9cd0d89d:", - "900150983cd24fb0d6963f7d28e17f72:", - "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", - "900150983cd24fb0d6963f7d28e17f72:", - "900150983cd24fb0d6963f7d28e17f72:", - "missing:", - "1111" - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval zero-argument system builtins return native-compatible values. - #[test] - fn execute_program_dispatches_zero_arg_system_builtins() { - let program = parse_fragment( - br#"echo time() > 1000000000 ? "time" : "bad"; echo ":"; -echo phpversion(); echo ":"; -echo sys_get_temp_dir(); echo ":"; -echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; -echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; -echo call_user_func("phpversion"); echo ":"; -echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; -echo call_user_func_array("sys_get_temp_dir", []); echo ":"; -echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); -return function_exists("sys_get_temp_dir");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - format!( - "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", - eval_compiler_php_version(), - eval_compiler_php_version() - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `date()` formats libc local timestamps and `mktime()` builds them. - #[test] - fn execute_program_dispatches_date_mktime_builtins() { - let program = parse_fragment( - br#"$ts = mktime(13, 2, 3, 1, 2, 2024); -echo date("Y-m-d H:i:s", $ts); -echo ":" . date("j-n-G-g-A-a-N-D-M-l-F", $ts); -echo ":" . (date("U", $ts) === strval($ts) ? "U" : "bad"); -echo ":" . call_user_func("date", "Y", $ts); -$named = call_user_func_array("mktime", ["hour" => 0, "minute" => 0, "second" => 0, "month" => 1, "day" => 1, "year" => 2000]); -echo ":" . date(format: "Y", timestamp: $named); -echo ":"; echo function_exists("date"); -return function_exists("mktime");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. - #[test] - fn execute_program_dispatches_strtotime_builtin() { - let program = parse_fragment( - br#"$date = strtotime("2024-06-15"); -echo date("Y-m-d H:i:s", $date); -$full = strtotime("2024-06-15 12:30:45"); -echo ":" . date("Y-m-d H:i:s", $full); -$short = strtotime("2024-06-15T12:30"); -echo ":" . date("Y-m-d H:i:s", $short); -echo ":" . (strtotime("2024/06/15") === -1 ? "bad" : "wrong"); -$call = call_user_func("strtotime", "2024-01-02 03:04:05"); -echo ":" . date("Y-m-d H:i:s", $call); -$spread = call_user_func_array("strtotime", ["datetime" => "2024-01-02"]); -echo ":" . date("Y-m-d", $spread) . ":"; -return function_exists("strtotime");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. - #[test] - fn execute_program_dispatches_microtime_builtin() { - let program = parse_fragment( - br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; -echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; -echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; -echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; -echo ":"; -return function_exists("microtime");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "now:named:call:array:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. - #[test] - fn execute_program_dispatches_realpath_cache_builtins() { - let program = parse_fragment( - br#"$cache = realpath_cache_get(); -echo count($cache) . ":" . realpath_cache_size() . ":"; -$call_cache = call_user_func("realpath_cache_get"); -echo count($call_cache) . ":"; -echo call_user_func_array("realpath_cache_size", []) . ":"; -echo function_exists("realpath_cache_get"); -return function_exists("realpath_cache_size");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "0:0:0:0:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval stream introspection builtins return native-compatible static lists. - #[test] - fn execute_program_dispatches_stream_introspection_builtins() { - let program = parse_fragment( - br#"$wrappers = stream_get_wrappers(); -$transports = stream_get_transports(); -$filters = stream_get_filters(); -echo count($wrappers) . ":" . $wrappers[0] . ":" . $wrappers[5] . ":"; -echo count($transports) . ":" . $transports[0] . ":" . $transports[8] . ":"; -echo count($filters) . ":" . $filters[2] . ":"; -$call_wrappers = call_user_func("stream_get_wrappers"); -echo $call_wrappers[10] . ":"; -$call_transports = call_user_func_array("stream_get_transports", []); -echo $call_transports[11] . ":"; -$call_filters = call_user_func_array("stream_get_filters", []); -echo $call_filters[13] . ":"; -echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); -return function_exists("stream_get_filters");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. - #[test] - fn execute_program_dispatches_spl_classes_builtin() { - let program = parse_fragment( - br#"$names = spl_classes(); -echo count($names) . ":" . $names[0] . ":" . $names[55] . ":"; -echo (in_array("Exception", $names) ? "exception" : "bad") . ":"; -echo (in_array("SplDoublyLinkedList", $names) ? "list" : "bad") . ":"; -$call = call_user_func("spl_classes"); -echo (in_array("Throwable", $call) ? "call" : "bad") . ":"; -$spread = call_user_func_array("spl_classes", []); -echo (count($spread) === count($names) ? "spread" : "bad") . ":"; -echo function_exists("spl_classes"); -return is_callable("spl_classes");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "61:AppendIterator:Throwable:exception:list:call:spread:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval SPL object identity builtins are stable, unique, and callable. - #[test] - fn execute_program_dispatches_spl_object_identity_builtins() { - let program = parse_fragment( - br#"$a = new KnownClass(); -$b = new KnownClass(); -echo (spl_object_id($a) === spl_object_id($a)) ? "stable" : "drift"; -echo ":"; -echo (spl_object_id($a) !== spl_object_id($b)) ? "unique" : "same"; -echo ":"; -echo (spl_object_hash(object: $a) === spl_object_hash($a)) ? "hash" : "bad"; -echo ":"; -echo (call_user_func("spl_object_id", $a) === spl_object_id($a)) ? "call" : "bad"; -echo ":"; -echo (call_user_func_array("spl_object_hash", ["object" => $b]) === spl_object_hash($b)) ? "array" : "bad"; -echo ":"; -echo function_exists("spl_object_id"); -return function_exists("spl_object_hash");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "stable:unique:hash:call:array:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval environment builtins read, write, unset, and dispatch dynamically. - #[test] - fn execute_program_dispatches_environment_builtins() { - let program = parse_fragment( - br#"putenv("ELEPHC_EVAL_ENV_TEST=direct"); -echo getenv("ELEPHC_EVAL_ENV_TEST") . ":"; -putenv(assignment: "ELEPHC_EVAL_ENV_TEST=named"); -echo getenv(name: "ELEPHC_EVAL_ENV_TEST") . ":"; -echo call_user_func("getenv", "ELEPHC_EVAL_ENV_TEST") . ":"; -echo call_user_func_array("putenv", ["assignment" => "ELEPHC_EVAL_ENV_TEST=spread"]) ? "set" : "bad"; -echo ":" . getenv("ELEPHC_EVAL_ENV_TEST") . ":"; -putenv("ELEPHC_EVAL_ENV_TEST"); -echo getenv("ELEPHC_EVAL_ENV_TEST") === "" ? "empty" : "bad"; -echo ":"; echo function_exists("getenv"); -return function_exists("putenv");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval sleep builtins dispatch without delaying focused tests. - #[test] - fn execute_program_dispatches_sleep_builtins() { - let program = parse_fragment( - br#"echo sleep(0) . ":"; -echo sleep(seconds: 0) . ":"; -usleep(0); -echo "u:"; -echo call_user_func("sleep", 0) . ":"; -echo call_user_func_array("usleep", ["microseconds" => 0]) === null ? "null" : "bad"; -echo ":"; echo function_exists("sleep"); -return function_exists("usleep");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "0:0:u:0:null:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. - #[test] - fn execute_program_dispatches_php_uname_builtin() { - let program = parse_fragment( - br#"echo strlen(php_uname()) > 0 ? "all" : "empty"; echo ":"; -echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; -echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; -echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; -echo strlen(php_uname("r")) > 0 ? "release" : "empty"; echo ":"; -echo strlen(php_uname("v")) > 0 ? "version" : "empty"; echo ":"; -echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; -echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; -echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; -return function_exists("php_uname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "all:same:sys:node:release:version:machine:call:spread:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. - #[test] - fn execute_program_dispatches_gethostbyname_builtin() { - let program = parse_fragment( - br#"echo gethostbyname("127.0.0.1") . ":"; -echo gethostbyname(hostname: "not a host") . ":"; -echo call_user_func("gethostbyname", "127.0.0.1") . ":"; -echo call_user_func_array("gethostbyname", ["hostname" => "not a host"]) . ":"; -return function_exists("gethostbyname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "127.0.0.1:not a host:127.0.0.1:not a host:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. - #[test] - fn execute_program_dispatches_gethostname_builtin() { - let program = parse_fragment( - br#"echo strlen(gethostname()) > 0 ? "host" : "empty"; echo ":"; -echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; -echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; -return function_exists("gethostname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "host:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. - #[test] - fn execute_program_dispatches_gethostbyaddr_builtin() { - let program = parse_fragment( - br#"echo strlen(gethostbyaddr("127.0.0.1")) > 0 ? "direct" : "empty"; echo ":"; -echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; -echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; -echo strlen(call_user_func("gethostbyaddr", "127.0.0.1")) > 0 ? "call" : "empty"; echo ":"; -echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === false ? "spread" : "bad"; echo ":"; -return function_exists("gethostbyaddr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "direct:named:false:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval protocol and service database lookups dispatch dynamically. - #[test] - fn execute_program_dispatches_protocol_service_builtins() { - let program = parse_fragment( - br#"echo getprotobyname("TCP") . ":"; -echo getprotobynumber(6) . ":"; -echo getprotobyname("no_such_protocol") === false ? "missing-proto" : "bad"; echo ":"; -echo getprotobynumber(999) === false ? "missing-number" : "bad"; echo ":"; -echo getservbyname("www", "tcp") . ":"; -echo getservbyport(80, "tcp") . ":"; -echo getservbyname("no_such_service", "tcp") === false ? "missing-service" : "bad"; echo ":"; -echo getservbyport(80, "no_such_proto") === false ? "missing-port" : "bad"; echo ":"; -echo call_user_func("getprotobyname", "udp") . ":"; -echo call_user_func_array("getprotobynumber", ["protocol" => 17]) . ":"; -echo call_user_func("getservbyname", "https", "tcp") . ":"; -echo call_user_func_array("getservbyport", ["port" => 443, "protocol" => "tcp"]) . ":"; -echo function_exists("getprotobyname"); echo function_exists("getprotobynumber"); echo function_exists("getservbyname"); -return function_exists("getservbyport");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. - #[test] - fn execute_program_dispatches_ip_conversion_builtins() { - let program = parse_fragment( - br#"echo long2ip(3232235777) . ":"; -echo long2ip(ip: 4294967295) . ":"; -echo ip2long("192.168.1.1") . ":"; -echo ip2long(ip: "1.2.3") === false ? "bad-ip" : "bad"; echo ":"; -$packed = inet_pton("1.2.3.4"); -echo bin2hex($packed) . ":"; -echo inet_pton(ip: "nonsense") === false ? "bad-pton" : "bad"; echo ":"; -echo inet_ntop($packed) . ":"; -echo inet_ntop(ip: "xx") === false ? "bad-ntop" : "bad"; echo ":"; -echo call_user_func("long2ip", 2130706433) . ":"; -echo call_user_func_array("ip2long", ["ip" => "0.0.0.0"]) . ":"; -echo function_exists("long2ip"); echo function_exists("ip2long"); -echo function_exists("inet_pton"); -return function_exists("inet_ntop");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval path component builtins mirror static basename/dirname edge cases. - #[test] - fn execute_program_dispatches_path_component_builtins() { - let program = parse_fragment( - br#"echo basename("/var/log/syslog.log", ".log") . ":"; -echo basename(path: "/usr///") . ":"; -echo basename("/", "x") === "" ? "root" : "bad"; echo ":"; -echo dirname("/usr/local/bin/tool", 2) . ":"; -echo dirname(path: "/usr///local///bin") . ":"; -echo call_user_func("basename", "foo.tar.gz", ".bz2") . ":"; -echo call_user_func_array("dirname", ["path" => "/usr", "levels" => 3]) . ":"; -echo function_exists("basename"); -return function_exists("dirname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `realpath()` resolves existing paths and returns false for misses. - #[test] - fn execute_program_dispatches_realpath_builtin() { - let program = parse_fragment( - br#"echo realpath(".") !== false ? "resolved" : "bad"; echo ":"; -echo realpath(path: "elephc-eval-missing-path") === false ? "false" : "bad"; echo ":"; -echo call_user_func("realpath", ".") !== false ? "call" : "bad"; echo ":"; -echo call_user_func_array("realpath", ["path" => "elephc-eval-missing-path"]) === false ? "array-false" : "bad"; -echo ":"; -return function_exists("realpath");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "resolved:false:call:array-false:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. - #[test] - fn execute_program_dispatches_fnmatch_builtin() { - let program = parse_fragment( - br#"echo fnmatch("*.log", "system.log") ? "match" : "bad"; echo ":"; -echo fnmatch("*.log", "logs/system.log", FNM_PATHNAME) ? "bad" : "path"; echo ":"; -echo fnmatch("*.LOG", "system.log", FNM_CASEFOLD) ? "case" : "bad"; echo ":"; -echo fnmatch("*", ".env", FNM_PERIOD) ? "bad" : "period"; echo ":"; -echo fnmatch("[!abc]oo", "doo") && !fnmatch("[!abc]oo", "boo") ? "class" : "bad"; echo ":"; -echo fnmatch('a\\*b', 'a*b') ? "escape" : "bad"; echo ":"; -echo fnmatch('a\\*b', 'a\\xxb', FNM_NOESCAPE) ? "noescape" : "bad"; echo ":"; -$flags = FNM_PATHNAME | FNM_CASEFOLD; -echo fnmatch("dir/*.TXT", "dir/file.txt", $flags) ? "flags" : "bad"; echo ":"; -echo call_user_func("fnmatch", "*.txt", "report.txt") ? "call" : "bad"; echo ":"; -echo call_user_func_array("fnmatch", ["pattern" => "*.TXT", "filename" => "report.txt", "flags" => FNM_CASEFOLD]) ? "callarray" : "bad"; echo ":"; -echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD"); -return FNM_CASEFOLD;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "match:path:case:period:class:escape:noescape:flags:call:callarray:11" - ); - assert_eq!(values.get(result), FakeValue::Int(EVAL_FNM_CASEFOLD)); - } - - /// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. - #[test] - fn execute_program_dispatches_pathinfo_builtin() { - let program = parse_fragment( - br#"$info = pathinfo("/var/log/syslog.log"); -echo $info["dirname"] . "|" . $info["basename"] . "|" . $info["extension"] . "|" . $info["filename"] . ":"; -echo pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":"; -echo pathinfo(".bashrc", PATHINFO_FILENAME) === "" ? "dotfile" : "bad"; echo ":"; -echo pathinfo("file.", PATHINFO_EXTENSION) === "" ? "trail" : "bad"; echo ":"; -echo pathinfo("", PATHINFO_DIRNAME) === "" ? "empty-dir" : "bad"; echo ":"; -$plain = pathinfo("/etc/hosts"); -echo array_key_exists("extension", $plain) ? "bad" : "no-ext"; echo ":"; -echo pathinfo("/a/b.php", PATHINFO_BASENAME | PATHINFO_FILENAME) . ":"; -$call = call_user_func("pathinfo", "foo.txt", PATHINFO_ALL); -echo $call["basename"] . ":"; -echo call_user_func_array("pathinfo", ["path" => "foo.txt", "flags" => 0]) === "" ? "zero" : "bad"; -echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL"); -return PATHINFO_ALL;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" - ); - assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); - } - - /// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. - #[test] - fn execute_program_dispatches_filesystem_builtins() { - let filename = format!("elephc_eval_fs_probe_{}.txt", std::process::id()); - let missing = format!("elephc_eval_fs_missing_{}.txt", std::process::id()); - let source = format!( - r#"echo file_put_contents("{filename}", "hello") . ":"; -echo file_get_contents("{filename}") . ":"; -echo file_exists("{filename}") ? "exists" : "missing"; echo ":"; -echo is_file(filename: "{filename}") ? "file" : "bad"; echo ":"; -echo is_dir(".") ? "dir" : "bad"; echo ":"; -echo is_readable("{filename}") ? "readable" : "bad"; echo ":"; -echo is_writable("{filename}") ? "writable" : "bad"; echo ":"; -echo is_writeable("{filename}") ? "writeable" : "bad"; echo ":"; -echo filesize("{filename}") . ":"; -echo file_get_contents("{missing}") === false ? "missing-false" : "bad"; echo ":"; -echo call_user_func("file_exists", "{filename}") ? "call-exists" : "bad"; echo ":"; -echo call_user_func_array("filesize", ["filename" => "{filename}"]) . ":"; -echo unlink("{filename}") ? "unlinked" : "bad"; echo ":"; -echo file_exists("{filename}") ? "bad" : "gone"; echo ":"; -echo function_exists("file_get_contents"); echo function_exists("file_put_contents"); -echo function_exists("file_exists"); echo function_exists("is_file"); echo function_exists("is_dir"); -echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); -echo function_exists("filesize"); -return function_exists("unlink");"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - assert_eq!( - values.output, - "5:hello:exists:file:dir:readable:writable:writeable:5:missing-false:call-exists:5:unlinked:gone:111111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval disk-space builtins query local filesystem capacity and dispatch dynamically. - #[test] - fn execute_program_dispatches_disk_space_builtins() { - let program = parse_fragment( - br#"echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; -echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; -echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; -echo disk_free_space("no/such/path/elephc-eval") === 0.0 ? "missing" : "bad"; echo ":"; -echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; -echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; echo ":"; -echo function_exists("disk_free_space"); -return function_exists("disk_total_space");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "free:total:ordered:missing:call:spread:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval stat metadata builtins expose scalar file metadata and link probes. - #[test] - fn execute_program_dispatches_stat_metadata_builtins() { - let filename = format!("elephc_eval_stat_probe_{}.txt", std::process::id()); - let missing = format!("elephc_eval_stat_missing_{}.txt", std::process::id()); - let link = format!("elephc_eval_stat_link_{}.txt", std::process::id()); - let source = format!( - r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; -echo fileatime("{filename}") > 0 ? "atime" : "bad"; echo ":"; -echo filectime("{filename}") > 0 ? "ctime" : "bad"; echo ":"; -echo fileperms("{filename}") > 0 ? "perms" : "bad"; echo ":"; -echo fileowner("{filename}") >= 0 ? "owner" : "bad"; echo ":"; -echo filegroup("{filename}") >= 0 ? "group" : "bad"; echo ":"; -echo fileinode("{filename}") > 0 ? "inode" : "bad"; echo ":"; -echo filetype("{filename}") . ":"; -echo filetype(".") . ":"; -echo filetype("{link}") . ":"; -echo is_executable("{filename}") ? "bad" : "noexec"; echo ":"; -echo is_link("{link}") ? "link" : "bad"; echo ":"; -echo fileatime("{missing}") === false ? "missing-atime" : "bad"; echo ":"; -echo filectime("{missing}") === false ? "missing-ctime" : "bad"; echo ":"; -echo fileperms("{missing}") === false ? "missing-perms" : "bad"; echo ":"; -echo fileowner("{missing}") === false ? "missing-owner" : "bad"; echo ":"; -echo filegroup("{missing}") === false ? "missing-group" : "bad"; echo ":"; -echo fileinode("{missing}") === false ? "missing-inode" : "bad"; echo ":"; -echo filetype("{missing}") === false ? "missing-type" : "bad"; echo ":"; -echo filemtime("{missing}") === 0 ? "missing-mtime" : "bad"; echo ":"; -echo call_user_func("filetype", "{filename}") . ":"; -echo call_user_func_array("fileinode", ["filename" => "{filename}"]) > 0 ? "callinode" : "bad"; echo ":"; -echo function_exists("filemtime"); echo function_exists("fileatime"); -echo function_exists("filectime"); echo function_exists("fileperms"); -echo function_exists("fileowner"); echo function_exists("filegroup"); -echo function_exists("fileinode"); echo function_exists("filetype"); -echo function_exists("is_executable"); echo function_exists("is_link"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - std::fs::write(&filename, b"hello").expect("write stat fixture"); - std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - assert_eq!( - values.output, - "mtime:atime:ctime:perms:owner:group:inode:file:dir:link:noexec:link:missing-atime:missing-ctime:missing-perms:missing-owner:missing-group:missing-inode:missing-type:missing-mtime:file:callinode:1111111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. - #[test] - fn execute_program_dispatches_stat_array_builtins() { - let pid = std::process::id(); - let filename = format!("elephc_eval_stat_array_{pid}.txt"); - let link = format!("elephc_eval_lstat_array_{pid}.txt"); - let missing = format!("elephc_eval_stat_array_missing_{pid}.txt"); - let source = format!( - r#"$stat = stat("{filename}"); -$lstat = lstat("{link}"); -echo $stat["size"] === 5 && $stat[7] === $stat["size"] ? "stat" : "bad"; echo ":"; -echo ($stat["mode"] & 61440) === 32768 ? "mode" : "bad"; echo ":"; -echo ($lstat["mode"] & 61440) === 40960 ? "lstat" : "bad"; echo ":"; -echo stat("{missing}") === false && lstat("{missing}") === false ? "missing" : "bad"; echo ":"; -$call = call_user_func("stat", "{filename}"); -echo $call["mtime"] === filemtime("{filename}") ? "callstat" : "bad"; echo ":"; -$call_lstat = call_user_func_array("lstat", ["filename" => "{link}"]); -echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; -echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("stat"); echo function_exists("lstat"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - std::fs::write(&filename, b"hello").expect("write stat array fixture"); - std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - assert_eq!( - values.output, - "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval local path operation builtins mutate filesystem state. - #[test] - fn execute_program_dispatches_path_operation_builtins() { - let pid = std::process::id(); - let dir = format!("elephc_eval_ops_dir_{pid}"); - let call_dir = format!("elephc_eval_ops_call_dir_{pid}"); - let src = format!("elephc_eval_ops_src_{pid}.txt"); - let copy = format!("elephc_eval_ops_copy_{pid}.txt"); - let moved = format!("elephc_eval_ops_moved_{pid}.txt"); - let symlink = format!("elephc_eval_ops_symlink_{pid}.txt"); - let hardlink = format!("elephc_eval_ops_hardlink_{pid}.txt"); - let source = format!( - r#"file_put_contents("{src}", "hello"); -echo mkdir("{dir}") ? "mkdir" : "bad"; echo ":"; -echo is_dir("{dir}") ? "dir" : "bad"; echo ":"; -echo copy("{src}", "{copy}") && file_get_contents("{copy}") === "hello" ? "copy" : "bad"; echo ":"; -echo rename("{copy}", "{moved}") && file_exists("{moved}") && !file_exists("{copy}") ? "rename" : "bad"; echo ":"; -echo symlink("{src}", "{symlink}") ? "symlink" : "bad"; echo ":"; -echo readlink("{symlink}") === "{src}" ? "readlink" : "bad"; echo ":"; -echo linkinfo("{symlink}") >= 0 ? "linkinfo" : "bad"; echo ":"; -echo readlink("{src}") === false ? "readlink-false" : "bad"; echo ":"; -echo linkinfo("{missing}") === -1 ? "linkinfo-missing" : "bad"; echo ":"; -echo link("{src}", "{hardlink}") && file_get_contents("{hardlink}") === "hello" ? "hardlink" : "bad"; echo ":"; -echo clearstatcache() === null ? "cache" : "bad"; echo ":"; -echo unlink("{symlink}") && unlink("{hardlink}") && unlink("{moved}") && unlink("{src}") && rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; -echo call_user_func("mkdir", "{call_dir}") ? "callmkdir" : "bad"; echo ":"; -echo call_user_func_array("rmdir", ["directory" => "{call_dir}"]) ? "callrmdir" : "bad"; echo ":"; -echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exists("copy"); -echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); -echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); -return true;"#, - missing = format!("elephc_eval_ops_missing_{pid}.txt"), - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - for path in [&symlink, &hardlink, &moved, ©, &src] { - let _ = std::fs::remove_file(path); - } - for path in [&call_dir, &dir] { - let _ = std::fs::remove_dir(path); - } - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - for path in [&symlink, &hardlink, &moved, ©, &src] { - let _ = std::fs::remove_file(path); - } - for path in [&call_dir, &dir] { - let _ = std::fs::remove_dir(path); - } - assert_eq!( - values.output, - "mkdir:dir:copy:rename:symlink:readlink:linkinfo:readlink-false:linkinfo-missing:hardlink:cache:cleanup:callmkdir:callrmdir:111111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. - #[test] - fn execute_program_dispatches_file_listing_builtins() { - let pid = std::process::id(); - let lines = format!("elephc_eval_listing_lines_{pid}.txt"); - let empty = format!("elephc_eval_listing_empty_{pid}.txt"); - let missing = format!("elephc_eval_listing_missing_{pid}.txt"); - let dir = format!("elephc_eval_listing_dir_{pid}"); - let source = format!( - r#"file_put_contents("{lines}", "one\ntwo"); -file_put_contents("{empty}", ""); -$lines = file("{lines}"); -echo count($lines) . ":"; -echo $lines[0] === "one\n" ? "line0" : "bad"; echo ":"; -echo $lines[1] === "two" ? "line1" : "bad"; echo ":"; -echo "["; -$bytes = readfile(filename: "{empty}"); -echo "]" . $bytes . ":"; -echo readfile("{missing}") === false ? "missing-readfile" : "bad"; echo ":"; -echo count(file("{missing}")) === 0 ? "missing-file" : "bad"; echo ":"; -mkdir("{dir}"); -file_put_contents("{dir}/a.txt", "a"); -file_put_contents("{dir}/b.txt", "b"); -$scan = scandir(directory: "{dir}"); -echo count($scan) . ":"; -echo in_array(".", $scan) && in_array("..", $scan) && in_array("a.txt", $scan) && in_array("b.txt", $scan) ? "scan" : "bad"; echo ":"; -$call_lines = call_user_func("file", "{lines}"); -echo $call_lines[0] === "one\n" ? "callfile" : "bad"; echo ":"; -$call_scan = call_user_func_array("scandir", ["directory" => "{dir}"]); -echo count($call_scan) . ":"; -echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") && unlink("{lines}") && unlink("{empty}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - for path in [&lines, &empty, &missing] { - let _ = std::fs::remove_file(path); - } - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.txt")); - let _ = std::fs::remove_dir(&dir); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - for path in [&lines, &empty, &missing] { - let _ = std::fs::remove_file(path); - } - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.txt")); - let _ = std::fs::remove_dir(&dir); - assert_eq!( - values.output, - "2:line0:line1:[]0:missing-readfile:missing-file:4:scan:callfile:4:cleanup:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `glob()` expands local patterns and dispatches dynamically. - #[test] - fn execute_program_dispatches_glob_builtin() { - let pid = std::process::id(); - let dir = format!("elephc_eval_glob_dir_{pid}"); - let source = format!( - r#"mkdir("{dir}"); -file_put_contents("{dir}/a.txt", "a"); -file_put_contents("{dir}/b.log", "b"); -file_put_contents("{dir}/c.txt", "c"); -file_put_contents("{dir}/.hidden.txt", "h"); -$matches = glob("{dir}/*.txt"); -echo count($matches) === 2 && basename($matches[0]) === "a.txt" && basename($matches[1]) === "c.txt" ? "glob" : "bad"; echo ":"; -echo count(glob("{dir}/*.none")) === 0 ? "empty" : "bad"; echo ":"; -$literal = glob("{dir}/a.txt"); -echo count($literal) === 1 && $literal[0] === "{dir}/a.txt" ? "literal" : "bad"; echo ":"; -$all = glob("{dir}/*"); -echo in_array("{dir}/.hidden.txt", $all) ? "bad" : "hidden"; echo ":"; -$call = call_user_func("glob", "{dir}/*.log"); -echo count($call) === 1 && basename($call[0]) === "b.log" ? "callglob" : "bad"; echo ":"; -$call_array = call_user_func_array("glob", ["pattern" => "{dir}/*.txt"]); -echo count($call_array) === 2 ? "callarray" : "bad"; echo ":"; -unlink("{dir}/.hidden.txt"); -unlink("{dir}/c.txt"); -unlink("{dir}/b.log"); -unlink("{dir}/a.txt"); -echo rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("glob"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); - let _ = std::fs::remove_file(format!("{dir}/c.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.log")); - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_dir(&dir); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); - let _ = std::fs::remove_file(format!("{dir}/c.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.log")); - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_dir(&dir); - assert_eq!( - values.output, - "glob:empty:literal:hidden:callglob:callarray:cleanup:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. - #[test] - fn execute_program_dispatches_file_modify_builtins() { - let pid = std::process::id(); - let filename = format!("elephc_eval_modify_{pid}.txt"); - let missing = format!("elephc_eval_modify_missing_{pid}.txt"); - let prefix = format!("evm{pid}_"); - let call_prefix = format!("evc{pid}_"); - let source = format!( - r#"file_put_contents("{filename}", "x"); -echo chmod(filename: "{filename}", permissions: 384) ? "chmod" : "bad"; echo ":"; -echo (fileperms("{filename}") & 511) === 384 ? "mode" : "bad"; echo ":"; -echo chmod("{missing}", 384) ? "bad" : "chmod-false"; echo ":"; -$tmp = tempnam(directory: ".", prefix: "{prefix}"); -echo file_exists($tmp) && str_starts_with(basename($tmp), "{prefix}") ? "tempnam" : "bad"; echo ":"; -unlink($tmp); -$previous = umask(mask: 18); -$set = umask($previous); -echo $set === 18 ? "umask" : "bad"; echo ":"; -$before = umask(18); -$probe = umask(); -$restore = umask($before); -echo $probe === 18 && $restore === 18 ? "probe" : "bad"; echo ":"; -echo call_user_func("chmod", "{filename}", 420) ? "callchmod" : "bad"; echo ":"; -$call_tmp = call_user_func_array("tempnam", ["directory" => ".", "prefix" => "{call_prefix}"]); -echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "{call_prefix}") ? "calltempnam" : "bad"; echo ":"; -unlink($call_tmp); -echo unlink("{filename}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&missing); - for entry in std::fs::read_dir(".").expect("read eval test cwd") { - let entry = entry.expect("read eval temp entry"); - let name = entry.file_name().to_string_lossy().into_owned(); - if name.starts_with(&prefix) || name.starts_with(&call_prefix) { - let _ = std::fs::remove_file(entry.path()); - } - } - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&missing); - for entry in std::fs::read_dir(".").expect("read eval test cwd") { - let entry = entry.expect("read eval temp entry"); - let name = entry.file_name().to_string_lossy().into_owned(); - if name.starts_with(&prefix) || name.starts_with(&call_prefix) { - let _ = std::fs::remove_file(entry.path()); - } - } - assert_eq!( - values.output, - "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. - #[test] - fn execute_program_dispatches_touch_builtin() { - let pid = std::process::id(); - let created = format!("elephc_eval_touch_created_{pid}.txt"); - let stamped = format!("elephc_eval_touch_stamped_{pid}.txt"); - let missing = format!("elephc_eval_touch_missing_{pid}/x.txt"); - let source = format!( - r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; -file_put_contents("{stamped}", "x"); -echo touch("{stamped}", 1000000000) ? "mtime" : "bad"; echo ":"; -echo filemtime("{stamped}") === 1000000000 ? "readmtime" : "bad"; echo ":"; -echo touch("{stamped}", 1000000001, null) && filemtime("{stamped}") === 1000000001 ? "nullatime" : "bad"; echo ":"; -echo touch("{stamped}", 1000000002, 1000000003) && filemtime("{stamped}") === 1000000002 ? "both" : "bad"; echo ":"; -echo touch("{missing}") ? "bad" : "touch-false"; echo ":"; -echo call_user_func("touch", "{created}", 1000000004) ? "calltouch" : "bad"; echo ":"; -echo call_user_func_array("touch", ["filename" => "{stamped}", "mtime" => 1000000005]) ? "callarray" : "bad"; echo ":"; -echo unlink("{created}") && unlink("{stamped}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("touch"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&created); - let _ = std::fs::remove_file(&stamped); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&created); - let _ = std::fs::remove_file(&stamped); - assert_eq!( - values.output, - "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval ASCII string case builtins work directly and through callable dispatch. - #[test] - fn execute_program_dispatches_string_case_builtins() { - let program = parse_fragment( - br#"echo strtoupper("Hello World"); echo ":"; -echo strtolower("LOUD"); echo ":"; -echo ucfirst("eval"); echo ":"; -echo lcfirst("LOUD"); echo ":"; -echo call_user_func("strtoupper", "xy"); echo ":"; -echo call_user_func_array("strtolower", ["ZZ"]); echo ":"; -echo call_user_func("ucfirst", "case"); echo ":"; -echo call_user_func_array("lcfirst", ["CASE"]); -echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower"); echo function_exists("ucfirst"); -return function_exists("lcfirst");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `ucwords()` capitalizes word starts directly and by callable dispatch. - #[test] - fn execute_program_dispatches_ucwords_builtin() { - let program = parse_fragment( - br#"echo ucwords("hello world"); echo ":"; -echo ucwords(string: "hello-world", separators: "-"); echo ":"; -echo ucwords("hello\tworld"); echo ":"; -echo call_user_func("ucwords", "a b"); echo ":"; -echo call_user_func_array("ucwords", ["string" => "a-b", "separators" => "-"]); echo ":"; -return function_exists("ucwords");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Hello World:Hello-World:Hello\tWorld:A B:A-B:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. - #[test] - fn execute_program_dispatches_wordwrap_builtin() { - let program = parse_fragment( - br#"echo wordwrap("The quick brown fox", 10, "|"); echo ":"; -echo wordwrap(string: "A verylongword here", width: 8, break: "|"); echo ":"; -echo wordwrap("abcdefghij", 4, "|", true); echo ":"; -echo wordwrap("preserve\nnewlines here ok", 10, "|"); echo ":"; -echo call_user_func("wordwrap", "aaa bbb ccc", 3, "
"); echo ":"; -echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); -echo ":"; -return function_exists("wordwrap");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. - #[test] - fn execute_program_dispatches_str_contains_builtin() { - let program = parse_fragment( - br#"echo str_contains("Hello World", "World") ? "Y" : "N"; -echo str_contains("Hello", "z") ? "bad" : ":N"; -echo str_contains("Hello", "") ? ":E" : "bad"; -echo call_user_func("str_contains", "abc", "b") ? ":C" : "bad"; -echo call_user_func_array("str_contains", ["abc", "x"]) ? "bad" : ":A"; -return function_exists("str_contains");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N:E:C:A"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval string position builtins return byte offsets or PHP false. - #[test] - fn execute_program_dispatches_string_position_builtins() { - let program = parse_fragment( - br#"echo strpos("banana", "na"); -echo ":" . strrpos("banana", "na"); -echo ":"; echo strpos("abc", "z") === false ? "F" : "bad"; -echo ":" . strpos("abc", ""); -echo ":" . strrpos("abc", ""); -echo ":" . call_user_func("strpos", "abc", "b"); -echo ":" . call_user_func_array("strrpos", ["ababa", "ba"]); -echo ":"; echo function_exists("strpos"); -return function_exists("strrpos");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:4:F:0:3:1:3:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `strstr()` returns suffixes, prefixes, or false for misses. - #[test] - fn execute_program_dispatches_strstr_builtin() { - let program = parse_fragment( - br#"echo strstr("user@example.com", "@"); echo ":"; -echo strstr(haystack: "hello world", needle: "lo", before_needle: true); echo ":"; -echo strstr("hello", "x") === false ? "F" : "bad"; echo ":"; -echo strstr("hello", ""); echo ":"; -echo call_user_func("strstr", "abcabc", "bc"); echo ":"; -echo call_user_func_array("strstr", ["haystack" => "abcabc", "needle" => "bc", "before_needle" => true]); echo ":"; -return function_exists("strstr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "@example.com:hel:F:hello:bcabc:a:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval prefix/suffix string search builtins use byte-string semantics. - #[test] - fn execute_program_dispatches_string_boundary_builtins() { - let program = parse_fragment( - br#"echo str_starts_with("Hello World", "Hello") ? "S" : "bad"; -echo str_starts_with("Hello", "World") ? "bad" : ":s"; -echo str_starts_with("Hello", "") ? ":se" : "bad"; -echo str_ends_with("Hello World", "World") ? ":E" : "bad"; -echo str_ends_with("Hello", "World") ? "bad" : ":e"; -echo str_ends_with("Hello", "") ? ":ee" : "bad"; -echo call_user_func("str_starts_with", "abc", "a") ? ":CS" : "bad"; -echo call_user_func_array("str_ends_with", ["abc", "c"]) ? ":CE" : "bad"; -echo ":"; echo function_exists("str_starts_with"); -return function_exists("str_ends_with");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "S:s:se:E:e:ee:CS:CE:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval string comparison builtins return PHP-compatible scalar results. - #[test] - fn execute_program_dispatches_string_compare_builtins() { - let program = parse_fragment( - br#"echo strcmp("abc", "abc"); -echo ":"; echo strcmp("abc", "abd") < 0 ? "lt" : "bad"; -echo ":"; echo strcasecmp("Hello", "hello"); -echo ":"; echo call_user_func("strcmp", "b", "a") > 0 ? "gt" : "bad"; -echo ":"; echo call_user_func_array("strcasecmp", ["A", "a"]) === 0 ? "ci" : "bad"; -echo ":"; echo hash_equals("abc", "abc") ? "heq" : "bad"; -echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; -echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; -echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); -return function_exists("hash_equals");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "0:lt:0:gt:ci:heq:hlen:hneq:11"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval trim-like builtins strip default and explicit byte masks. - #[test] - fn execute_program_dispatches_trim_like_builtins() { - let program = parse_fragment( - br#"echo "[" . trim(" hello ") . "]"; -echo ":[" . ltrim(" left") . "]"; -echo ":[" . rtrim("right ") . "]"; -echo ":[" . chop("tail... ", " .") . "]"; -echo ":[" . trim("**boxed**", "*") . "]"; -echo ":[" . call_user_func("trim", " cuf ") . "]"; -echo ":[" . call_user_func_array("ltrim", ["0007", "0"]) . "]"; -echo ":"; echo function_exists("trim"); echo function_exists("ltrim"); echo function_exists("rtrim"); -return function_exists("chop");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "[hello]:[left]:[right]:[tail]:[boxed]:[cuf]:[7]:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. - #[test] - fn execute_program_dispatches_type_predicate_builtins() { - let program = parse_fragment( - br#"echo is_int(1); echo is_integer(1); echo is_long(1); -echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); -echo is_string("x"); echo is_bool(false); echo is_null(null); -echo is_array([1]); echo is_array(["a" => 1]); -echo is_iterable([1]); echo is_iterable(["a" => 1]); -echo is_iterable(1) ? "bad" : "T"; -echo is_array(1) ? "bad" : "ok"; -echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); -echo is_numeric("-5"); echo is_numeric("3.14"); -echo is_numeric("abc") ? "bad" : "N"; -echo is_numeric(true) ? "bad" : "B"; -echo is_resource(1) ? "bad" : "R"; -echo is_object($object) ? "O" : "bad"; -echo is_object([1]) ? "bad" : "o"; -echo is_nan(fdiv(0, 0)) ? "N" : "bad"; -echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; -echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; -echo is_finite(42) ? "F" : "bad"; -echo is_finite(fdiv(1, 0)) ? "bad" : "f"; -echo ":"; echo call_user_func("is_string", "x"); -echo call_user_func_array("is_array", [[1]]); -echo call_user_func("is_numeric", "12"); -echo call_user_func("is_iterable", [1]); -echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; -echo call_user_func("is_object", $object) ? "O" : "bad"; -echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; -echo function_exists("is_numeric"); echo function_exists("is_object"); echo function_exists("is_resource"); -echo function_exists("is_double"); echo function_exists("is_nan"); echo function_exists("is_finite"); -echo function_exists("is_iterable"); -return function_exists("is_infinite");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1111111111111Tok11111NBROoNIiFf:1111tOo1111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. - #[test] - fn execute_program_dispatches_is_resource_true() { - let program = parse_fragment( - br#"echo is_resource($handle) ? "R" : "bad"; -echo ":" . gettype($handle); -return call_user_func("is_resource", $handle);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let handle = values.alloc(FakeValue::Resource(6)); - scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "R:resource"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval resource introspection builtins expose stream type and one-based id. - #[test] - fn execute_program_dispatches_resource_introspection_builtins() { - let program = parse_fragment( - br#"echo get_resource_type($handle); -echo ":" . get_resource_id($handle); -echo ":" . call_user_func("get_resource_type", $handle); -echo ":" . call_user_func_array("get_resource_id", ["resource" => $handle]); -echo ":" . function_exists("get_resource_type"); -return function_exists("get_resource_id");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let handle = values.alloc(FakeValue::Resource(6)); - scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "stream:7:stream:7:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval cast builtins return boxed scalar cells directly and by callable. - #[test] - fn execute_program_dispatches_cast_builtins() { - let program = parse_fragment( - br#"echo intval("42"); echo ":"; -echo floatval("3.5"); echo ":"; -echo strval(12); echo ":"; -echo boolval("0") ? "bad" : "false"; -echo ":"; echo call_user_func("strval", 7); -return call_user_func_array("intval", ["9"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "42:3.5:12:false:7"); - assert_eq!(values.get(result), FakeValue::Int(9)); - } - - /// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. - #[test] - fn execute_program_dispatches_settype_builtin() { - let program = parse_fragment( - br#"$x = 42; -echo settype($x, "string") ? gettype($x) . ":" . $x : "bad"; -echo ":"; -$y = "0"; -echo settype(type: "bool", var: $y) ? gettype($y) . ":" . ($y ? "true" : "false") : "bad"; -echo ":"; -echo settype($missing, "integer") ? gettype($missing) . ":" . $missing : "bad"; -echo ":"; -$z = 3.8; -echo call_user_func("settype", $z, "integer") ? gettype($z) . ":" . $z : "bad"; -echo ":"; -return function_exists("settype");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "string:42:boolean:false:integer:0:double:3.8:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - assert_eq!( - values.warnings, - ["settype(): Argument #1 ($var) must be passed by reference, value given"] - ); - } - - /// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. - #[test] - fn execute_program_dispatches_gettype_builtin() { - let program = parse_fragment( - br#"echo gettype(1); echo ":"; -echo gettype(1.5); echo ":"; -echo gettype("x"); echo ":"; -echo gettype(false); echo ":"; -echo gettype(null); echo ":"; -echo gettype([1]); echo ":"; -echo gettype(["a" => 1]); echo ":"; -echo call_user_func("gettype", true); echo ":"; -echo call_user_func_array("gettype", [null]); -return function_exists("gettype");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "integer:double:string:boolean:NULL:array:array:boolean:NULL" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `get_class()` reads object class names directly and by callable. - #[test] - fn execute_program_dispatches_get_class_builtin() { - let program = parse_fragment( - br#"echo get_class($object); echo ":"; -echo call_user_func("get_class", $object); echo ":"; -return call_user_func_array("get_class", ["object" => $object]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "stdClass:stdClass:"); - assert_eq!( - values.get(result), - FakeValue::String("stdClass".to_string()) - ); - } - - /// Verifies eval `get_parent_class()` reads object and class-string parents by callable. - #[test] - fn execute_program_dispatches_get_parent_class_builtin() { - let program = parse_fragment( - br#"echo get_parent_class($object); echo ":"; -echo get_parent_class("ChildClass"); echo ":"; -echo call_user_func("get_parent_class", $object); echo ":"; -return call_user_func_array("get_parent_class", ["object_or_class" => "ChildClass"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "ParentClass:ParentClass:ParentClass:"); - assert_eq!( - values.get(result), - FakeValue::String("ParentClass".to_string()) - ); - } - - /// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. - #[test] - fn execute_program_dispatches_abs_builtin() { - let program = parse_fragment( - br#"echo abs(-5); echo ":"; -echo abs(-2.5); echo ":"; -echo gettype(abs(-2.5)); echo ":"; -echo call_user_func("abs", -7); echo ":"; -echo call_user_func_array("abs", [-9]); -return function_exists("abs");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:2.5:double:7:9"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `floor()` and `ceil()` dispatch as double-returning math builtins. - #[test] - fn execute_program_dispatches_floor_and_ceil_builtins() { - let program = parse_fragment( - br#"echo floor(3.7); echo ":"; -echo gettype(floor(3)); echo ":"; -echo ceil(3.2); echo ":"; -echo gettype(ceil(3)); echo ":"; -echo call_user_func("floor", 4.9); echo ":"; -echo call_user_func_array("ceil", [4.1]); -echo ":"; echo function_exists("floor"); -return function_exists("ceil");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:double:4:double:4:5:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `fdiv()` and `fmod()` dispatch as floating-point binary builtins. - #[test] - fn execute_program_dispatches_float_binary_builtins() { - let program = parse_fragment( - br#"echo round(fdiv(10, 4), 2); echo ":"; -echo gettype(fdiv(10, 4)); echo ":"; -echo round(fmod(10.5, 3.2), 1); echo ":"; -echo round(call_user_func("fdiv", 9, 2), 1); echo ":"; -echo round(call_user_func_array("fmod", [10.5, 3.2]), 1); echo ":"; -echo function_exists("fdiv"); -return function_exists("fmod");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2.5:double:0.9:4.5:0.9:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval extended scalar math builtins support direct, named, callable, and probe paths. - #[test] - fn execute_program_dispatches_extended_math_builtins() { - let program = parse_fragment( - br#"echo sin(0); echo ":"; -echo cos(0); echo ":"; -echo tan(0); echo ":"; -echo round(asin(1), 2); echo ":"; -echo acos(1); echo ":"; -echo round(atan(1), 2); echo ":"; -echo sinh(0); echo ":"; -echo cosh(0); echo ":"; -echo tanh(0); echo ":"; -echo log2(8); echo ":"; -echo log10(100); echo ":"; -echo exp(0); echo ":"; -echo round(deg2rad(180), 2); echo ":"; -echo round(rad2deg(pi()), 0); echo ":"; -echo log(num: 8, base: 2); echo ":"; -echo atan2(y: 0, x: 1); echo ":"; -echo hypot(3, 4); echo ":"; -echo intdiv(7, 2); echo ":"; -echo round(call_user_func("sin", pi() / 2), 0); echo ":"; -echo call_user_func_array("intdiv", ["num1" => 9, "num2" => 2]); echo ":"; -echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); -return function_exists("hypot");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. - #[test] - fn execute_program_dispatches_pow_builtin() { - let program = parse_fragment( - br#"echo pow(2, 3); echo ":"; -echo gettype(pow(2, 3)); echo ":"; -echo call_user_func("pow", 2, 5); echo ":"; -echo call_user_func_array("pow", [3, 3]); -return function_exists("pow");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "8:double:32:27"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `round()` supports default and explicit precision through callable paths. - #[test] - fn execute_program_dispatches_round_builtin() { - let program = parse_fragment( - br#"echo round(3.5); echo ":"; -echo round(3.14159, 2); echo ":"; -echo gettype(round(3)); echo ":"; -echo call_user_func("round", 2.5); echo ":"; -echo call_user_func_array("round", [1.55, 1]); -return function_exists("round");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:3.14:double:3:1.6"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `number_format()` groups and rounds numbers through callable paths. - #[test] - fn execute_program_dispatches_number_format_builtin() { - let program = parse_fragment( - br#"echo number_format(1234567); echo ":"; -echo number_format(1234.5678, 2); echo ":"; -echo number_format(num: 1234567.89, decimals: 2, decimal_separator: ",", thousands_separator: "."); echo ":"; -echo number_format(1234567.89, 2, ".", ""); echo ":"; -echo call_user_func("number_format", -1234.5, 1); echo ":"; -echo call_user_func_array("number_format", ["num" => 1234, "decimals" => 0, "decimal_separator" => ".", "thousands_separator" => " "]); echo ":"; -return function_exists("number_format");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval printf-family builtins format, print, and dispatch through callables. - #[test] - fn execute_program_dispatches_printf_family_builtins() { - let program = parse_fragment( - br#"echo sprintf("Hello %s", "World"); echo ":"; -echo sprintf("%05d", 42); echo ":"; -echo sprintf("%.2f", 3.14159); echo ":"; -echo sprintf("%-6s|", "hi"); echo ":"; -$printed = printf("%s=%d", "n", 42); -echo ":" . $printed . ":"; -echo vsprintf("%s/%d/%.1f", ["age", 42, 3]); echo ":"; -$vprinted = vprintf("%s-%d", ["v", 7]); -echo ":" . $vprinted . ":"; -echo call_user_func("sprintf", "%+d", 42); echo ":"; -echo call_user_func_array("vsprintf", ["format" => "%s", "values" => ["spread"]]); echo ":"; -echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); -return is_callable("vprintf");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `sscanf()` returns indexed string matches through callable paths. - #[test] - fn execute_program_dispatches_sscanf_builtin() { - let program = parse_fragment( - br#"$result = sscanf("John 1.5 30", "%s %f %d"); -echo $result[0] . ":" . $result[1] . ":" . $result[2] . ":"; -$named = sscanf(string: "Age: -25", format: "Age: %d"); -echo $named[0] . ":"; -$call = call_user_func("sscanf", "-2.5e3", "%f"); -echo $call[0] . ":"; -$spread = call_user_func_array("sscanf", ["string" => "ok %", "format" => "%s %%"]); -echo $spread[0] . ":"; -return function_exists("sscanf");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "John:1.5:30:-25:-2.5e3:ok:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `min()` and `max()` select numeric values directly and by callable. - #[test] - fn execute_program_dispatches_min_max_builtins() { - let program = parse_fragment( - br#"echo min(3, 1, 2); echo ":"; -echo max(1, 3, 2); echo ":"; -echo min(2.5, 1.5); echo ":"; -echo max(1.5, 2.5); echo ":"; -echo call_user_func("min", 9, 4, 7); echo ":"; -echo call_user_func_array("max", [4, 8, 6]); echo ":"; -echo function_exists("min"); -return function_exists("max");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:3:1.5:2.5:4:8:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `clamp()` selects numeric values through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_clamp_builtin() { - let program = parse_fragment( - br#"echo clamp(5, 0, 10); echo ":"; -echo clamp(15, 0, 10); echo ":"; -echo clamp(-5, 0, 10); echo ":"; -echo clamp(2.75, 1.5, 2.5); echo ":"; -echo clamp(value: 8, min: 0, max: 5); echo ":"; -echo call_user_func("clamp", -1, 0, 10); echo ":"; -echo call_user_func_array("clamp", ["value" => 9, "min" => 0, "max" => 7]); echo ":"; -echo function_exists("clamp"); -return is_callable("clamp");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:10:0:2.5:5:0:7:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. - #[test] - fn execute_program_rejects_clamp_invalid_bounds() { - let program = parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("invalid clamp bounds should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } - - /// Verifies eval `pi()` returns a double constant directly and through callable paths. - #[test] - fn execute_program_dispatches_pi_builtin() { - let program = parse_fragment( - br#"echo round(pi(), 2); echo ":"; -echo gettype(pi()); echo ":"; -echo round(call_user_func("pi"), 3); echo ":"; -echo round(call_user_func_array("pi", []), 4); echo ":"; -return function_exists("pi");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3.14:double:3.142:3.1416:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. - #[test] - fn execute_program_dispatches_sqrt_builtin() { - let program = parse_fragment( - br#"echo sqrt(16); echo ":"; -echo gettype(sqrt(9)); echo ":"; -echo call_user_func("sqrt", 25); echo ":"; -echo call_user_func_array("sqrt", [36]); -return function_exists("sqrt");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:double:5:6"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `strrev()` dispatches through direct and callable paths. - #[test] - fn execute_program_dispatches_strrev_builtin() { - let program = parse_fragment( - br#"echo strrev("Hello"); echo ":"; -echo strrev(123); echo ":"; -echo call_user_func("strrev", "ABC"); echo ":"; -echo call_user_func_array("strrev", ["def"]); echo ":"; -return function_exists("strrev");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "olleH:321:CBA:fed:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `chr()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_chr_builtin() { - let program = parse_fragment( - br#"echo chr(65); echo ":"; -echo bin2hex(chr(codepoint: 256)); echo ":"; -echo bin2hex(call_user_func("chr", 257)); echo ":"; -echo call_user_func_array("chr", ["codepoint" => 321]); echo ":"; -return function_exists("chr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A:00:01:A:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_str_repeat_builtin() { - let program = parse_fragment( - br#"echo str_repeat("ha", 3); echo ":"; -echo strlen(str_repeat(string: "x", times: 0)); echo ":"; -echo call_user_func("str_repeat", "ab", 2); echo ":"; -echo call_user_func_array("str_repeat", ["string" => "z", "times" => 3]); echo ":"; -return function_exists("str_repeat");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "hahaha:0:abab:zzz:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `substr()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_substr_builtin() { - let program = parse_fragment( - br#"echo substr("abcdef", 2); echo ":"; -echo substr(string: "abcdef", offset: 1, length: -1); echo ":"; -echo substr("abcdef", -2); echo ":"; -echo call_user_func("substr", "abcdef", 2, -2); echo ":"; -echo call_user_func_array("substr", ["string" => "abcdef", "offset" => -4, "length" => 2]); echo ":"; -return function_exists("substr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "cdef:bcde:ef:cd:cd:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `substr_replace()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_substr_replace_builtin() { - let program = parse_fragment( - br#"echo substr_replace("hello world", "PHP", 6, 5); echo ":"; -echo substr_replace(string: "abcdef", replace: "X", offset: 1, length: -1); echo ":"; -echo substr_replace("abcdef", "X", -2); echo ":"; -echo call_user_func("substr_replace", "abcdef", "X", 99, 1); echo ":"; -echo call_user_func_array("substr_replace", ["string" => "abcdef", "replace" => "X", "offset" => -99, "length" => 2]); echo ":"; -return function_exists("substr_replace");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "hello PHP:aXf:abcdX:abcdefX:Xcdef:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_nl2br_builtin() { - let program = parse_fragment( - br#"echo bin2hex(nl2br("a\nb")); echo ":"; -echo bin2hex(nl2br(string: "a\nb", use_xhtml: false)); echo ":"; -echo bin2hex(call_user_func("nl2br", "a\r\nb")); echo ":"; -echo bin2hex(call_user_func_array("nl2br", ["string" => "a\n\rb", "use_xhtml" => false])); echo ":"; -return function_exists("nl2br");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_bin2hex_builtin() { - let program = parse_fragment( - br#"echo bin2hex("Az"); echo ":"; -echo bin2hex(string: "A\n"); echo ":"; -echo call_user_func("bin2hex", "!?"); echo ":"; -echo call_user_func_array("bin2hex", ["string" => "ok"]); echo ":"; -return function_exists("bin2hex");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "417a:410a:213f:6f6b:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `hex2bin()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_hex2bin_builtin() { - let program = parse_fragment( - br#"echo hex2bin("417a"); echo ":"; -echo bin2hex(hex2bin(string: "410a")); echo ":"; -echo call_user_func("hex2bin", "213f"); echo ":"; -echo call_user_func_array("hex2bin", ["string" => "6f6b"]); echo ":"; -echo hex2bin("4") ? "bad" : "false"; echo ":"; -return function_exists("hex2bin");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Az:410a:!?:ok:false:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - assert_eq!( - values.warnings, - vec![HEX2BIN_ODD_LENGTH_WARNING.to_string()] - ); - } - - /// Verifies eval slash escaping builtins use PHP byte-string semantics. - #[test] - fn execute_program_dispatches_slash_escape_builtins() { - let program = parse_fragment( - br#"$escaped = addslashes($source); -echo bin2hex($escaped); echo ":"; -echo bin2hex(stripslashes($escaped)); echo ":"; -echo call_user_func("addslashes", "x\"y"); echo ":"; -echo call_user_func_array("stripslashes", [addslashes("o\"k")]); echo ":"; -return function_exists("addslashes") && function_exists("stripslashes");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let source = values.string("a\0b\\c\"d'").expect("create source"); - scope.set("source", source, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_base64_encode_builtin() { - let program = parse_fragment( - br#"echo base64_encode("Hello"); echo ":"; -echo base64_encode(string: "Hi"); echo ":"; -echo call_user_func("base64_encode", "Test 123!"); echo ":"; -echo call_user_func_array("base64_encode", ["string" => ""]); echo ":"; -return function_exists("base64_encode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "SGVsbG8=:SGk=:VGVzdCAxMjMh::"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - /// Verifies eval `base64_decode()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_base64_decode_builtin() { - let program = parse_fragment( - br#"echo base64_decode("SGVsbG8="); echo ":"; -echo base64_decode(string: "SGk="); echo ":"; -echo call_user_func("base64_decode", "VGVzdCAxMjMh"); echo ":"; -echo call_user_func_array("base64_decode", ["string" => ""]); echo ":"; -return function_exists("base64_decode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Hello:Hi:Test 123!::"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } - - /// Verifies `isset` distinguishes missing, null, and other falsey values. - #[test] - fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { - let program = parse_fragment( - br#"if (isset($missing)) { echo "1"; } else { echo "0"; } -if (isset($nullish)) { echo "1"; } else { echo "0"; } -if (isset($zero)) { echo "1"; } else { echo "0"; } -if (isset($empty)) { echo "1"; } else { echo "0"; } -if (isset($zero, $empty)) { echo "1"; } else { echo "0"; } -if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let nullish = values.null().expect("create fake null"); - let zero = values.int(0).expect("create fake int"); - let empty = values.string("").expect("create fake string"); - scope.set("nullish", nullish, ScopeCellOwnership::Owned); - scope.set("zero", zero, ScopeCellOwnership::Owned); - scope.set("empty", empty, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "001110"); - assert_eq!(values.get(result), FakeValue::Null); - } - - /// Verifies `empty` treats missing, null, and falsey values as empty. - #[test] - fn execute_program_empty_uses_php_truthiness_without_missing_warnings() { - let program = parse_fragment( - br#"if (empty($missing)) { echo "1"; } else { echo "0"; } -if (empty($nullish)) { echo "1"; } else { echo "0"; } -if (empty($zero)) { echo "1"; } else { echo "0"; } -if (empty($empty_string)) { echo "1"; } else { echo "0"; } -if (empty($zero_string)) { echo "1"; } else { echo "0"; } -if (empty($value)) { echo "1"; } else { echo "0"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let nullish = values.null().expect("create fake null"); - let zero = values.int(0).expect("create fake int"); - let empty_string = values.string("").expect("create fake empty string"); - let zero_string = values.string("0").expect("create fake zero string"); - let value = values.string("x").expect("create fake non-empty string"); - scope.set("nullish", nullish, ScopeCellOwnership::Owned); - scope.set("zero", zero, ScopeCellOwnership::Owned); - scope.set("empty_string", empty_string, ScopeCellOwnership::Owned); - scope.set("zero_string", zero_string, ScopeCellOwnership::Owned); - scope.set("value", value, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "111110"); - assert_eq!(values.get(result), FakeValue::Null); - } - - /// Verifies `isset` and `empty` use PHP offset semantics for array reads. - #[test] - fn execute_program_isset_and_empty_support_array_offsets() { - let program = parse_fragment( - br#"$map = [ - "present" => "x", - "nullish" => null, - "zero" => 0, - "empty" => "", - "child" => ["leaf" => "ok", "null" => null], -]; -echo isset($map["present"]) ? "1" : "0"; -echo isset($map["nullish"]) ? "1" : "0"; -echo isset($map["missing"]) ? "1" : "0"; -echo isset($map["zero"]) ? "1" : "0"; -echo isset($map["child"]["leaf"]) ? "1" : "0"; -echo isset($map["child"]["null"]) ? "1" : "0"; -echo isset($map["missing"]["leaf"]) ? "1" : "0"; -echo ":"; -echo empty($map["present"]) ? "1" : "0"; -echo empty($map["nullish"]) ? "1" : "0"; -echo empty($map["missing"]) ? "1" : "0"; -echo empty($map["zero"]) ? "1" : "0"; -echo empty($map["empty"]) ? "1" : "0"; -echo empty($map["child"]["leaf"]) ? "1" : "0"; -echo empty($map["child"]["null"]) ? "1" : "0"; -echo empty($map["missing"]["leaf"]) ? "1" : "0";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1001100:01111011"); - assert_eq!(values.get(result), FakeValue::Null); - } - - /// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. - #[test] - fn execute_program_function_probes_use_eval_context() { - let program = parse_fragment( - br#"function dyn_probe() { return 1; } -echo function_exists("DYN_PROBE") . "x"; -echo is_callable("dyn_probe") . "x"; -echo function_exists("strlen") . "x"; -echo function_exists("native_probe") . "x"; -echo function_exists("eval") . "x"; -echo function_exists("missing_probe") . "x";"#, - ) - .expect("parse eval fragment"); - let native = NativeFunction::new(1usize as *mut c_void, fake_native_return_descriptor, 0); - let mut context = ElephcEvalContext::new(); - assert!(context - .define_native_function("native_probe", native) - .is_ok()); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.output, "1x1x1x1xxx"); - } - - /// Verifies eval `interface_exists()` probes generated interface metadata by callable. - #[test] - fn execute_program_interface_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"echo interface_exists("KnownInterface") ? "Y" : "N"; -echo interface_exists("knowninterface") ? "Y" : "N"; -echo interface_exists("KnownClass") ? "Y" : "N"; -echo call_user_func("interface_exists", "KnownInterface") ? "Y" : "N"; -echo call_user_func_array("interface_exists", ["interface" => "KnownInterface"]) ? "Y" : "N"; -echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YYNYYN"); - } - - /// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. - #[test] - fn execute_program_class_like_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"echo trait_exists("KnownTrait") ? "T" : "t"; -echo trait_exists("knowntrait") ? "T" : "t"; -echo trait_exists("KnownEnum") ? "T" : "t"; -echo enum_exists("KnownEnum") ? "E" : "e"; -echo enum_exists("\knownenum") ? "E" : "e"; -echo enum_exists("KnownTrait") ? "E" : "e"; -echo call_user_func("trait_exists", "KnownTrait") ? "T" : "t"; -echo call_user_func_array("enum_exists", ["enum" => "KnownEnum"]) ? "E" : "e"; -echo trait_exists(trait: "MissingTrait", autoload: false) ? "T" : "t"; -echo enum_exists(enum: "MissingEnum", autoload: false) ? "E" : "e";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "TTtEEeTEte"); - } - - /// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. - #[test] - fn execute_program_is_a_relation_uses_runtime_probe() { - let program = parse_fragment( - br#"$object = new KnownClass(); -echo is_a($object, "KnownClass") ? "Y" : "N"; -echo is_subclass_of($object, "KnownClass") ? "Y" : "N"; -echo is_subclass_of($object, "ParentClass") ? "Y" : "N"; -echo call_user_func("is_a", $object, "ParentClass") ? "Y" : "N"; -echo call_user_func_array("is_subclass_of", ["object_or_class" => $object, "class" => "ParentClass"]) ? "Y" : "N"; -echo is_a(object_or_class: $object, class: "MissingClass", allow_string: false) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YNYYYN"); - } - - /// Verifies eval `define()` and `defined()` share a dynamic constant-name table. - #[test] - fn execute_program_define_and_defined_use_dynamic_constant_table() { - let program = parse_fragment( - br#"echo define("DynEvalConst", "ok") ? "Y" : "N"; -echo DynEvalConst; -echo \DynEvalConst; -echo defined("DynEvalConst") ? "Y" : "N"; -echo defined("\\DynEvalConst") ? "Y" : "N"; -echo defined("dynevalconst") ? "Y" : "N"; -echo define("DynEvalConst", 2) ? "Y" : "N"; -echo call_user_func("defined", "DynEvalConst") ? "Y" : "N"; -echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YokokYYNNYY"); - assert_eq!( - values.warnings, - vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] - ); - } - - /// Verifies eval predefined runtime constants are fetchable and cannot be redefined. - #[test] - fn execute_program_reads_predefined_runtime_constants() { - let program = parse_fragment( - br#"echo PHP_EOL === "\n" ? "eol" : "bad"; echo ":"; -echo (PHP_OS === "Darwin" || PHP_OS === "Linux") ? "os" : "bad"; echo ":"; -echo DIRECTORY_SEPARATOR; echo ":"; -echo PHP_INT_MAX > 9000000000000000000 ? "int" : "bad"; echo ":"; -echo defined("PHP_OS") ? "defined" : "bad"; echo ":"; -echo defined("\\PHP_OS") ? "root" : "bad"; echo ":"; -echo defined("php_os") ? "bad" : "case"; echo ":"; -echo define("PHP_OS", "x") ? "bad" : "locked"; echo ":"; -return PHP_INT_MAX;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "eol:os:/:int:defined:root:case:locked:"); - assert_eq!(values.get(result), FakeValue::Int(i64::MAX)); - assert_eq!( - values.warnings, - vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] - ); - } - - /// Verifies missing eval dynamic constants fail through runtime status. - #[test] - fn execute_program_missing_constant_fetch_fails() { - let program = parse_fragment(br#"return MissingEvalConst;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("missing constant should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } - - /// Verifies eval class probes use the runtime class-name table. - #[test] - fn execute_program_class_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"class DynProbe {} -echo class_exists("DynProbe") ? "Y" : "N"; -echo class_exists("\dynprobe") ? "Y" : "N"; -echo class_exists("KnownClass") ? "Y" : "N"; -echo class_exists("\knownclass") ? "Y" : "N"; -echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YYYYN"); - } - - /// Verifies duplicate eval-declared class names fail through runtime status. - #[test] - fn execute_program_duplicate_class_declaration_fails() { - let program = parse_fragment( - br#"class DynProbeDup {} -class dynprobedup {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values).expect_err("duplicate fails"); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } - - /// Verifies eval fragments can dispatch registered native AOT functions. - #[test] - fn execute_program_calls_registered_native_function() { - let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } - - /// Verifies direct eval calls can bind registered native parameters by name. - #[test] - fn execute_program_calls_registered_native_function_with_named_args() { - let program = parse_fragment(br#"return native_answer(right: 2, left: 1);"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } - - /// Verifies direct eval calls can unpack arrays into registered native parameters. - #[test] - fn execute_program_calls_registered_native_function_with_spread_args() { - let program = - parse_fragment(br#"return native_answer(...[1, 2]);"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } - - /// Verifies indexed array writes mutate an existing scope array. - #[test] - fn execute_program_writes_indexed_scope_array() { - let program = parse_fragment(br#"$items = ["a"]; $items[1] = "b"; return $items[1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("b".to_string())); - } - - /// Verifies indexed array append writes use the next visible index. - #[test] - fn execute_program_appends_indexed_scope_array() { - let program = parse_fragment(br#"$items = ["a"]; $items[] = "b"; return $items[1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("b".to_string())); - } - - /// Verifies associative append starts at key zero when only string keys exist. - #[test] - fn execute_program_appends_assoc_scope_array_with_string_keys() { - let program = - parse_fragment(br#"$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); - } - - /// Verifies associative append uses one plus the largest existing integer key. - #[test] - fn execute_program_appends_assoc_scope_array_after_positive_int_key() { - let program = parse_fragment( - br#"$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies associative append preserves PHP's largest-negative-key behavior. - #[test] - fn execute_program_appends_assoc_scope_array_after_negative_int_key() { - let program = - parse_fragment(br#"$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } - - /// Verifies mutating a borrowed scope array does not make the eval scope own it. - #[test] - fn execute_program_preserves_borrowed_array_ownership() { - let program = parse_fragment(br#"$items[0] = "b";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let array = values.array_new(1).expect("create fake array"); - scope.set("items", array, ScopeCellOwnership::Borrowed); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let entry = scope.entry("items").expect("scope should contain items"); - - assert_eq!(entry.cell(), array); - assert_eq!(entry.flags().ownership, ScopeCellOwnership::Borrowed); - assert!(values.releases.is_empty()); - } - - /// Verifies replacing an eval-owned scope value releases the old cell. - #[test] - fn execute_program_releases_replaced_scope_value() { - let program = parse_fragment(br#"$x = "old"; $x = "new";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.releases.len(), 1); - assert_eq!( - values.get(values.releases[0]), - FakeValue::String("old".to_string()) - ); - } - - /// Verifies unsetting an eval-owned scope value releases the old cell. - #[test] - fn execute_program_releases_unset_scope_value() { - let program = parse_fragment(br#"$x = "old"; unset($x);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.releases.len(), 1); - assert_eq!( - values.get(values.releases[0]), - FakeValue::String("old".to_string()) - ); - } - - /// Verifies break exits a runtime eval loop before later statements run. - #[test] - fn execute_program_break_exits_loop() { - let program = parse_fragment(br#"while ($flag) { echo "a"; break; echo "b"; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.bool_value(true).expect("create fake bool"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "a"); - } - - /// Verifies continue restarts a runtime eval loop and observes later scope updates. - #[test] - fn execute_program_continue_restarts_loop() { - let program = parse_fragment( - br#"while ($flag) { $flag = false; continue; echo "unreachable"; } echo "done";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.bool_value(true).expect("create fake bool"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "done"); - } -} +#[cfg(test)] +mod tests; diff --git a/crates/elephc-eval/src/interpreter/tests.rs b/crates/elephc-eval/src/interpreter/tests.rs new file mode 100644 index 0000000000..fffacd4556 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests.rs @@ -0,0 +1,7279 @@ +//! Purpose: +//! Interpreter unit tests for EvalIR execution against fake runtime values. +//! The tests exercise scope mutation, builtins, function calls, arrays, objects, +//! control flow, and error propagation without linking generated runtime hooks. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - FakeOps owns opaque test cells and mirrors enough runtime behavior for eval. +//! - Test fixtures parse PHP eval fragments before executing them through EvalIR. + + use std::collections::HashMap; + use std::ffi::c_void; + + use crate::parser::parse_fragment; + use crate::value::RuntimeCell; + + use super::*; + + /// Test-only array key representation for fake indexed and associative arrays. + #[derive(Clone, Debug, PartialEq, Eq, Hash)] + enum FakeKey { + Int(i64), + String(String), + } + + /// Test-only runtime value representation used behind opaque cell handles. + #[derive(Clone, Debug, PartialEq)] + enum FakeValue { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), + Bytes(Vec), + Array(Vec), + Assoc(Vec<(FakeKey, RuntimeCellHandle)>), + Object(Vec<(String, RuntimeCellHandle)>), + Iterator { len: i64, position: i64 }, + Resource(i64), + } + + /// Test runtime hooks that allocate stable fake handles and record echo output. + #[derive(Default)] + struct FakeOps { + next_id: usize, + values: HashMap, + output: String, + releases: Vec, + warnings: Vec, + } + + impl FakeOps { + /// Allocates one fake runtime cell and returns its opaque handle. + fn alloc(&mut self, value: FakeValue) -> RuntimeCellHandle { + self.next_id += 1; + let id = self.next_id; + self.values.insert(id, value); + RuntimeCellHandle::from_raw(id as *mut RuntimeCell) + } + + /// Reads a fake runtime cell by opaque handle. + fn get(&self, handle: RuntimeCellHandle) -> FakeValue { + let id = handle.as_ptr() as usize; + self.values.get(&id).cloned().expect("fake cell missing") + } + + /// Converts a fake runtime cell into a normalized fake PHP array key. + fn key(&self, handle: RuntimeCellHandle) -> Result { + let value = self.get(handle); + match value { + FakeValue::Int(value) => Ok(FakeKey::Int(value)), + FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) + .map(FakeKey::Int) + .map_or_else(|| Ok(FakeKey::String(value)), Ok), + FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) + .map(FakeKey::Int) + .map_or_else( + || { + Ok(FakeKey::String( + String::from_utf8_lossy(&value).into_owned(), + )) + }, + Ok, + ), + FakeValue::Null => Ok(FakeKey::String(String::new())), + value => Ok(FakeKey::Int(self.fake_int(&value))), + } + } + + /// Allocates a fake runtime cell for an existing PHP array key. + fn alloc_key(&mut self, key: &FakeKey) -> Result { + match key { + FakeKey::Int(value) => self.int(*value), + FakeKey::String(value) => self.string(value), + } + } + + /// Finds a fake object property by insertion-order name. + fn object_property( + properties: &[(String, RuntimeCellHandle)], + name: &str, + ) -> Option { + properties + .iter() + .find_map(|(property, value)| (property == name).then_some(*value)) + } + } + + impl RuntimeValueOps for FakeOps { + /// Creates a fake indexed array cell. + fn array_new(&mut self, capacity: usize) -> Result { + Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) + } + + /// Creates a fake associative array cell. + fn assoc_new(&mut self, _capacity: usize) -> Result { + Ok(self.alloc(FakeValue::Assoc(Vec::new()))) + } + + /// Reads one fake indexed array element. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + match self.get(array) { + FakeValue::Array(elements) => { + let FakeKey::Int(index) = key else { + return self.null(); + }; + if index < 0 { + return self.null(); + } + elements + .get(index as usize) + .copied() + .map_or_else(|| self.null(), Ok) + } + FakeValue::Assoc(entries) => entries + .iter() + .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => self.null(), + } + } + + /// Checks whether a fake array has the requested key without reading its value. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + let key = self.key(key)?; + let exists = match self.get(array) { + FakeValue::Array(elements) => { + matches!(key, FakeKey::Int(index) if index >= 0 && (index as usize) < elements.len()) + } + FakeValue::Assoc(entries) => entries.iter().any(|(entry_key, _)| entry_key == &key), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.bool_value(exists) + } + + /// Returns one fake foreach key by insertion-order position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(array) { + FakeValue::Array(elements) if position < elements.len() => { + self.int(position as i64) + } + FakeValue::Assoc(entries) => { + let Some((key, _)) = entries.get(position) else { + return self.null(); + }; + self.alloc_key(key) + } + FakeValue::Array(_) => self.null(), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Writes one fake indexed or associative array element. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + let id = array.as_ptr() as usize; + match self.values.get_mut(&id) { + Some(FakeValue::Array(elements)) => { + let FakeKey::Int(index) = key else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if index < 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let index = index as usize; + while elements.len() <= index { + elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); + } + elements[index] = value; + } + Some(FakeValue::Assoc(entries)) => { + if let Some((_, existing_value)) = + entries.iter_mut().find(|(entry_key, _)| entry_key == &key) + { + *existing_value = value; + } else { + entries.push((key, value)); + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + Ok(array) + } + + /// Reads one fake object property by name. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => properties + .iter() + .find_map(|(name, value)| (name == property).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Writes one fake object property by name. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some((_, existing_value)) = + properties.iter_mut().find(|(name, _)| name == property) + { + *existing_value = value; + } else { + properties.push((property.to_string(), value)); + } + Ok(()) + } + + /// Returns the number of fake object properties in insertion order. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { + match self.get(object) { + FakeValue::Object(properties) => Ok(properties.len()), + FakeValue::Iterator { .. } => Ok(0), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Returns one fake object property key by insertion-order position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + let Some((name, _)) = properties.get(position) else { + return self.null(); + }; + self.string(name) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Calls one fake object method by name. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + match (self.get(object), method) { + (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) + else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position = 0; + self.null() + } + (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { + self.bool_value(position < len) + } + (FakeValue::Iterator { .. }, "next") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) + else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position += 1; + self.null() + } + (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), + (FakeValue::Object(properties), "read_x") => { + if !args.is_empty() { + return Err(EvalStatus::UnsupportedConstruct); + } + Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "add_x") => { + let [arg] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = + Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(arg) = self.get(*arg) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + arg) + } + (FakeValue::Object(properties), "add2_x") => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = + Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + left + right) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Creates one fake object for eval `new` unit tests. + fn new_object(&mut self, _class_name: &str) -> Result { + Ok(self.alloc(FakeValue::Object(Vec::new()))) + } + + /// Applies fake constructor side effects for eval `new` unit tests. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some(first) = args.first().copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { + *value = first; + } else { + properties.push(("x".to_string(), first)); + } + } + Ok(()) + } + + /// Reports one fake AOT class for eval `class_exists` unit tests. + fn class_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownClass")) + } + + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + fn interface_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownInterface")) + } + + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + fn trait_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownTrait")) + } + + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + fn enum_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownEnum")) + } + + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) + if target_class.eq_ignore_ascii_case("Exception") + || target_class.eq_ignore_ascii_case("Throwable") => + { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => { + Ok(true) + } + _ => Ok(false), + } + } + + /// Returns a fake PHP class name for object-tagged test values. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(_) => self.string("stdClass"), + FakeValue::Iterator { .. } => self.string("Iterator"), + _ => Err(EvalStatus::RuntimeFatal), + } + } + + /// Returns fake parent-class names for eval introspection unit tests. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) => self.string("ParentClass"), + FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { + self.string("ParentClass") + } + _ => self.string(""), + } + } + + /// Returns the visible element count for fake array values. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + match self.get(array) { + FakeValue::Array(elements) => Ok(elements.len()), + FakeValue::Assoc(entries) => Ok(entries.len()), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Returns whether a fake runtime cell is an indexed or associative array. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + Ok(matches!( + self.get(value), + FakeValue::Array(_) | FakeValue::Assoc(_) + )) + } + + /// Returns whether a fake runtime cell is null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + Ok(matches!(self.get(value), FakeValue::Null)) + } + + /// Returns the fake runtime tag corresponding to a test value. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Int(_) => EVAL_TAG_INT, + FakeValue::String(_) | FakeValue::Bytes(_) => EVAL_TAG_STRING, + FakeValue::Float(_) => EVAL_TAG_FLOAT, + FakeValue::Bool(_) => EVAL_TAG_BOOL, + FakeValue::Array(_) => EVAL_TAG_ARRAY, + FakeValue::Assoc(_) => EVAL_TAG_ASSOC, + FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, + FakeValue::Resource(_) => EVAL_TAG_RESOURCE, + FakeValue::Null => EVAL_TAG_NULL, + }) + } + + /// Returns the fake object handle as a stable object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + match self.get(object) { + FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), + _ => Err(EvalStatus::RuntimeFatal), + } + } + + /// Records fake releases without freeing handles needed for assertions. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.releases.push(value); + Ok(()) + } + + /// Returns the same fake handle because fake cells do not refcount. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + Ok(value) + } + + /// Records fake PHP warnings without writing to stderr. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + self.warnings.push(message.to_string()); + Ok(()) + } + + /// Creates a fake null cell. + fn null(&mut self) -> Result { + Ok(self.alloc(FakeValue::Null)) + } + + /// Creates a fake bool cell. + fn bool_value(&mut self, value: bool) -> Result { + Ok(self.alloc(FakeValue::Bool(value))) + } + + /// Creates a fake int cell. + fn int(&mut self, value: i64) -> Result { + Ok(self.alloc(FakeValue::Int(value))) + } + + /// Creates a fake float cell. + fn float(&mut self, value: f64) -> Result { + Ok(self.alloc(FakeValue::Float(value))) + } + + /// Creates a fake string cell. + fn string(&mut self, value: &str) -> Result { + Ok(self.alloc(FakeValue::String(value.to_string()))) + } + + /// Creates a fake string cell from raw PHP bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + match std::str::from_utf8(value) { + Ok(value) => self.string(value), + Err(_) => Ok(self.alloc(FakeValue::Bytes(value.to_vec()))), + } + } + + /// Casts a fake runtime cell to a fake integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + let value = self.fake_int(&value); + self.int(value) + } + + /// Casts a fake runtime cell to a fake float cell. + fn cast_float( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + let value = self.fake_numeric(&value); + self.float(value) + } + + /// Casts a fake runtime cell to a fake string cell. + fn cast_string( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.stringify(value); + self.string(&value) + } + + /// Casts a fake runtime cell to a fake boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + let value = self.fake_truthy(&value); + self.bool_value(value) + } + + /// Computes fake PHP absolute value while preserving float payloads. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + match self.get(value) { + FakeValue::Float(value) => self.float(value.abs()), + value => self.int(self.fake_int(&value).wrapping_abs()), + } + } + + /// Computes fake PHP ceiling through numeric conversion as a float result. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).ceil()) + } + + /// Computes fake PHP floor through numeric conversion as a float result. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).floor()) + } + + /// Computes fake PHP square root through numeric conversion as a float result. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).sqrt()) + } + + /// Reverses a fake string byte-wise for interpreter tests. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + let mut bytes = self.stringify(value).into_bytes(); + bytes.reverse(); + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + self.string(&value) + } + + /// Divides fake numeric cells with PHP `fdiv()` zero handling. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left / right) + } + + /// Computes fake floating-point modulo for interpreter tests. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left % right) + } + + /// Adds fake numeric cells for interpreter tests. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), + (left, right) => self.float(self.fake_numeric(&left) + self.fake_numeric(&right)), + } + } + + /// Subtracts fake numeric cells for interpreter tests. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), + (left, right) => self.float(self.fake_numeric(&left) - self.fake_numeric(&right)), + } + } + + /// Multiplies fake numeric cells for interpreter tests. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), + (left, right) => self.float(self.fake_numeric(&left) * self.fake_numeric(&right)), + } + } + + /// Divides fake numeric cells for interpreter tests. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_numeric(&self.get(right)); + if right == 0.0 { + return Err(EvalStatus::RuntimeFatal); + } + let left = self.fake_numeric(&self.get(left)); + self.float(left / right) + } + + /// Computes fake integer modulo for interpreter tests. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_int(&self.get(right)); + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let left = self.fake_int(&self.get(left)); + self.int(left % right) + } + + /// Raises fake numeric cells for interpreter tests. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left.powf(right)) + } + + /// Rounds fake numeric cells with PHP's optional decimal precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + let value = self.fake_numeric(&self.get(value)); + let precision = precision + .map(|precision| self.fake_int(&self.get(precision))) + .unwrap_or(0); + let multiplier = 10_f64.powf(precision as f64); + self.float((value * multiplier).round() / multiplier) + } + + /// Applies fake integer bitwise and shift operations for interpreter tests. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_int(&self.get(left)); + let right = self.fake_int(&self.get(right)); + let value = match op { + EvalBinOp::BitAnd => left & right, + EvalBinOp::BitOr => left | right, + EvalBinOp::BitXor => left ^ right, + EvalBinOp::ShiftLeft => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); + } + left.wrapping_shl(right as u32) + } + EvalBinOp::ShiftRight => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); + } + left.wrapping_shr(right as u32) + } + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.int(value) + } + + /// Applies fake integer bitwise NOT for interpreter tests. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.fake_int(&self.get(value)); + self.int(!value) + } + + /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let mut left = self.string_bytes_for_value(&self.get(left)); + let right = self.string_bytes_for_value(&self.get(right)); + left.extend_from_slice(&right); + self.string_bytes_value(&left) + } + + /// Compares fake scalar cells and returns a fake PHP boolean. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let result = match op { + EvalBinOp::LooseEq => self.loose_eq(left, right), + EvalBinOp::LooseNotEq => !self.loose_eq(left, right), + EvalBinOp::StrictEq => self.strict_eq(left, right), + EvalBinOp::StrictNotEq => !self.strict_eq(left, right), + EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, + EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, + EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, + EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Pow + | EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight + | EvalBinOp::Concat + | EvalBinOp::Spaceship + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor => { + return Err(EvalStatus::UnsupportedConstruct); + } + }; + self.bool_value(result) + } + + /// Compares fake numeric cells and returns a PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.numeric(left)?; + let right = self.numeric(right)?; + let value = if left < right { + -1 + } else if left > right { + 1 + } else { + 0 + }; + self.int(value) + } + + /// Appends fake echo output for interpreter tests. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + let value = self.stringify(value); + self.output.push_str(&value); + Ok(()) + } + + /// Casts one fake runtime cell to bytes for nested eval parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + Ok(self.string_bytes_for_value(&self.get(value))) + } + + /// Returns PHP-like truthiness for fake runtime cells. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Null => false, + FakeValue::Bool(value) => value, + FakeValue::Int(value) => value != 0, + FakeValue::Float(value) => value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, + FakeValue::Resource(_) => true, + }) + } + } + + impl FakeOps { + /// Compares fake scalar values with the same loose rules covered by eval tests. + fn loose_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Bool(left), right) => left == self.fake_truthy(&right), + (left, FakeValue::Bool(right)) => self.fake_truthy(&left) == right, + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Null, FakeValue::String(value)) + | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), + (FakeValue::Null, FakeValue::Bytes(value)) + | (FakeValue::Bytes(value), FakeValue::Null) => value.is_empty(), + (FakeValue::String(left), FakeValue::String(right)) => { + match (left.parse::(), right.parse::()) { + (Ok(left), Ok(right)) => left == right, + _ => left == right, + } + } + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, + (FakeValue::String(left), right) => left + .parse::() + .is_ok_and(|left| left == self.fake_numeric(&right)), + (FakeValue::Bytes(left), right) => std::str::from_utf8(&left) + .ok() + .and_then(|left| left.parse::().ok()) + .is_some_and(|left| left == self.fake_numeric(&right)), + (left, FakeValue::String(right)) => right + .parse::() + .is_ok_and(|right| self.fake_numeric(&left) == right), + (left, FakeValue::Bytes(right)) => std::str::from_utf8(&right) + .ok() + .and_then(|right| right.parse::().ok()) + .is_some_and(|right| self.fake_numeric(&left) == right), + (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), + } + } + + /// Compares fake scalar values by PHP strict tag and payload equality. + fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, + (FakeValue::Int(left), FakeValue::Int(right)) => left == right, + (FakeValue::Float(left), FakeValue::Float(right)) => left == right, + (FakeValue::String(left), FakeValue::String(right)) => left == right, + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, + (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, + _ => false, + } + } + + /// Converts one fake scalar cell to a numeric value for comparison tests. + fn numeric(&self, handle: RuntimeCellHandle) -> Result { + Ok(self.fake_numeric(&self.get(handle))) + } + + /// Converts a fake value to the numeric scalar used by comparison tests. + fn fake_numeric(&self, value: &FakeValue) -> f64 { + match value { + FakeValue::Null => 0.0, + FakeValue::Bool(false) => 0.0, + FakeValue::Bool(true) => 1.0, + FakeValue::Int(value) => *value as f64, + FakeValue::Float(value) => *value, + FakeValue::String(value) => value.parse::().unwrap_or(0.0), + FakeValue::Bytes(value) => std::str::from_utf8(value) + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(0.0), + FakeValue::Array(value) => value.len() as f64, + FakeValue::Assoc(value) => value.len() as f64, + FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, + FakeValue::Resource(value) => (*value + 1) as f64, + } + } + + /// Converts a fake value to the integer scalar used by modulo tests. + fn fake_int(&self, value: &FakeValue) -> i64 { + self.fake_numeric(value) as i64 + } + + /// Returns fake PHP truthiness for already-loaded test values. + fn fake_truthy(&self, value: &FakeValue) -> bool { + match value { + FakeValue::Null => false, + FakeValue::Bool(value) => *value, + FakeValue::Int(value) => *value != 0, + FakeValue::Float(value) => *value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, + FakeValue::Resource(_) => true, + } + } + + /// Converts a fake runtime cell to a PHP-like string for test echo/concat. + fn stringify(&self, handle: RuntimeCellHandle) -> String { + match self.get(handle) { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value, + FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), + FakeValue::Array(_) => "Array".to_string(), + FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + } + } + + /// Converts a fake PHP value to string bytes while preserving binary strings. + fn string_bytes_for_value(&self, value: &FakeValue) -> Vec { + match value { + FakeValue::String(value) => value.as_bytes().to_vec(), + FakeValue::Bytes(value) => value.clone(), + value => self.stringify_value(value).into_bytes(), + } + } + + /// Converts one loaded fake PHP value to display text for byte coercions. + fn stringify_value(&self, value: &FakeValue) -> String { + match value { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value.clone(), + FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), + FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + } + } + } + + /// Test native invoker that returns the descriptor pointer as a runtime cell. + unsafe extern "C" fn fake_native_return_descriptor( + descriptor: *mut c_void, + _args: *mut RuntimeCell, + ) -> *mut RuntimeCell { + descriptor.cast() + } + + /// Verifies assignment writes a named scope entry and return reads it back. + #[test] + fn execute_program_stores_and_returns_scope_value() { + let program = parse_fragment(b"$x = 3; return $x + 4;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::Int(3)); + assert_eq!(values.get(result), FakeValue::Int(7)); + } + + /// Verifies reference assignment aliases variable names and writes through the alias. + #[test] + fn execute_program_reference_assignment_updates_source_variable() { + let program = parse_fragment(b"$x = 1; $alias =& $x; $alias = 5; return $x;") + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + let alias = scope + .visible_cell("alias") + .expect("scope should contain alias"); + + assert_eq!(x, alias); + assert_eq!(values.get(x), FakeValue::Int(5)); + assert_eq!(values.get(result), FakeValue::Int(5)); + } + + /// Verifies eval `throw` exits the program with a retained Throwable cell. + #[test] + fn execute_program_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)); + } + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + } + + /// Verifies eval `try/catch` catches a thrown object and binds the catch variable. + #[test] + fn execute_program_catches_throwable_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies eval `catch (Throwable)` can handle a throw without binding a variable. + #[test] + fn execute_program_catches_throwable_without_variable_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable) { + return 9; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("unbound catch should release the thrown object"); + + assert_eq!(scope.visible_cell("caught"), None); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(9)); + } + + /// Verifies eval `catch (Exception)` matches thrown exception objects. + #[test] + fn execute_program_catches_specific_exception_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Exception $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies eval catch clauses keep source order and skip non-matching types. + #[test] + fn execute_program_skips_non_matching_specific_catch_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (RuntimeException $wrong) { + return 1; +} catch (Exception $caught) { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(scope.visible_cell("wrong"), None); + assert_eq!(values.get(result), FakeValue::Int(2)); + } + + /// Verifies union catch clauses test later types in the same catch clause. + #[test] + fn execute_program_catches_union_type_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (RuntimeException|Exception $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies eval `finally` runs before a pending try-body return is observed. + #[test] + fn execute_program_runs_finally_before_returning_try_value() { + let program = parse_fragment( + br#"try { + return 1; +} finally { + echo "finally"; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "finally"); + assert_eq!(values.get(result), FakeValue::Int(1)); + } + + /// Verifies eval `finally` return values replace pending try-body returns. + #[test] + fn execute_program_finally_return_overrides_try_return() { + let program = parse_fragment( + br#"try { + return 1; +} finally { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.releases.len(), 1); + } + + /// Verifies eval `finally` return values replace pending uncaught throws. + #[test] + fn execute_program_finally_return_overrides_uncaught_throw() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} finally { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("overridden throw should be released"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); + } + + /// Verifies eval `finally` runs before an uncaught throw leaves the fragment. + #[test] + fn execute_program_runs_finally_before_uncaught_throw_outcome() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} finally { + echo "finally"; +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)) + } + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + assert_eq!(values.output, "finally"); + } + + /// Verifies static locals declared inside eval catch blocks persist per function context. + #[test] + fn execute_context_function_persists_static_local_inside_catch() { + let program = parse_fragment( + br#"function dyn($e) { + try { + throw $e; + } catch (Throwable $caught) { + static $n = 0; + $n++; + return $caught->answer() + $n; + } +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let first_thrown = values + .new_object("Exception") + .expect("allocate first fake exception"); + let second_thrown = values + .new_object("Exception") + .expect("allocate second fake exception"); + + let first = execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) + .expect("execute first dynamic function call"); + let second = + execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(43)); + assert_eq!(values.get(second), FakeValue::Int(44)); + } + + /// Verifies static locals declared inside eval finally blocks persist per function context. + #[test] + fn execute_context_function_persists_static_local_inside_finally() { + let program = parse_fragment( + br#"function dyn() { + try { + return 0; + } finally { + static $n = 0; + $n++; + return $n; + } +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + + let first = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute first dynamic function call"); + let second = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(2)); + } + + /// Verifies throws from eval-declared functions escape through the shared context. + #[test] + fn execute_context_function_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"function dyn($e) { throw $e; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); + + let outcome = + execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) + .expect("throw should be an eval function outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + } + + /// Verifies nested eval preserves the thrown cell while returning an uncaught status. + #[test] + fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { + let program = parse_fragment(br#"eval("throw $e;");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); + scope.set("e", thrown, ScopeCellOwnership::Borrowed); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("nested throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + } + + /// Verifies eval include resolves caller-relative paths, shares scope, and returns file values. + #[test] + fn execute_program_include_uses_call_site_and_returns_file_result() { + let dir = std::env::temp_dir().join(format!( + "elephc-eval-include-{}-call-site", + std::process::id() + )); + let path = dir.join("piece.php"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("create include fixture directory"); + std::fs::write( + &path, + format!( + r#">= 3; echo $x; echo ":"; +echo ~0; echo ":"; echo -16 >> 2; +return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:5:4:32:8:-1:-4"); + assert_eq!(values.get(result), FakeValue::Int(21)); + } + + /// Verifies simple variable increment and decrement statements update the scope value. + #[test] + fn execute_program_evaluates_inc_dec_statements() { + let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "1"); + assert_eq!(values.get(i), FakeValue::Int(1)); + } + + /// Verifies echo and unset operate through runtime hooks and scope metadata. + #[test] + fn execute_program_echoes_and_unsets_scope_value() { + let program = + parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let name = values.string(" Ada").expect("create fake string"); + scope.set("name", name, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hi Ada"); + assert_eq!(values.get(result), FakeValue::Null); + assert!(scope.entry("name").expect("unset marker").flags().unset); + } + + /// Verifies comma-separated echo expressions are executed in source order. + #[test] + fn execute_program_echoes_comma_list() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let b = values.string("b").expect("create fake string"); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "abc"); + } + + /// Verifies print writes output and returns integer 1. + #[test] + fn execute_program_print_returns_one() { + let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "p"); + assert_eq!(values.get(result), FakeValue::Int(1)); + } + + /// Verifies eval `print_r()` emits supported values and returns true. + #[test] + fn execute_program_dispatches_print_r_builtin() { + let program = parse_fragment( + br#"print_r("x"); echo ":"; +print_r(value: false); echo ":"; +print_r([1, 2]); echo ":"; +$call = call_user_func("print_r", true); +$spread = call_user_func_array("print_r", ["value" => "z"]); +echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; +return function_exists("print_r");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "x::Array\n:1z:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. + #[test] + fn execute_program_dispatches_var_dump_builtin() { + let program = parse_fragment( + br#"var_dump(42); +var_dump("hi"); +var_dump(false); +var_dump(null); +var_dump([10, 20]); +var_dump(["x" => true]); +$call = call_user_func("var_dump", 3.5); +$spread = call_user_func_array("var_dump", ["value" => "z"]); +echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; +return function_exists("var_dump");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "int(42)\n", + "string(2) \"hi\"\n", + "bool(false)\n", + "NULL\n", + "array(2) {\n", + " [0]=>\n", + " int(10)\n", + " [1]=>\n", + " int(20)\n", + "}\n", + "array(1) {\n", + " [\"x\"]=>\n", + " bool(true)\n", + "}\n", + "float(3.5)\n", + "string(1) \"z\"\n", + "call-null:spread-null:", + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval property reads and writes dispatch through runtime hooks. + #[test] + fn execute_program_reads_and_writes_object_property() { + let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!( + values + .property_get(object, "x") + .map(|value| values.get(value)) + .expect("property should be readable"), + FakeValue::Int(2) + ); + } + + /// Verifies eval method calls dispatch through the runtime method hook. + #[test] + fn execute_program_calls_object_method() { + let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies eval method calls forward evaluated arguments to the runtime hook. + #[test] + fn execute_program_calls_object_method_with_argument() { + let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. + #[test] + fn execute_program_calls_object_method_with_two_arguments() { + let program = + parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); + } + + /// Verifies eval method calls forward numerically unpacked arguments. + #[test] + fn execute_program_calls_object_method_with_spread_arguments() { + let program = + parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); + } + + /// Verifies eval object construction dispatches through runtime hooks. + #[test] + fn execute_program_constructs_named_object() { + let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Object(Vec::new())); + } + + /// Verifies eval object construction passes constructor arguments through runtime hooks. + #[test] + fn execute_program_constructs_named_object_with_args() { + let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let FakeValue::Object(properties) = values.get(result) else { + panic!("expected fake object"); + }; + let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); + + assert_eq!(values.get(x), FakeValue::Int(1)); + } + + /// Verifies eval-declared classes create objects with properties and methods. + #[test] + fn execute_program_constructs_eval_declared_class_with_method() { + let program = parse_fragment( + br#"class DynBox { + public int $x = 1; + public function __construct($x) { $this->x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +} +$box = new DynBox(4); +echo get_class($box); +echo ":"; +echo $box->bump(3); +echo ":"; +echo is_a($box, "DynBox") ? "Y" : "N"; +$call = [$box, "bump"]; +echo call_user_func($call, 1); +echo ":"; +echo call_user_func_array($call, [2]); +echo ":"; +return $box->x;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DynBox:7:Y8:10:"); + assert_eq!(values.get(result), FakeValue::Int(10)); + } + + /// Verifies if/else executes only the PHP-truthy branch. + #[test] + fn execute_program_if_else_uses_php_truthiness() { + let program = parse_fragment(br#"if ($flag) { $x = "then"; } else { $x = "else"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(0).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::String("else".to_string())); + } + + /// Verifies elseif chains execute the first truthy branch and skip later branches. + #[test] + fn execute_program_elseif_uses_first_truthy_branch() { + let program = parse_fragment( + br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; } else { $x = "c"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let a = values.bool_value(false).expect("create fake bool"); + let b = values.bool_value(true).expect("create fake bool"); + scope.set("a", a, ScopeCellOwnership::Owned); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::String("b".to_string())); + } + + /// Verifies while repeats while the condition remains truthy and propagates writes. + #[test] + fn execute_program_while_uses_php_truthiness() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(2).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let flag = scope + .visible_cell("flag") + .expect("scope should contain flag"); + + assert_eq!(values.output, "2"); + assert_eq!(values.get(flag), FakeValue::Bool(false)); + } + + /// Verifies do/while runs the body before testing the condition. + #[test] + fn execute_program_do_while_runs_body_before_condition() { + let program = parse_fragment(br#"do { echo $i; $i = $i + 1; } while (false);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let i = values.int(0).expect("create fake int"); + scope.set("i", i, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "0"); + assert_eq!(values.get(i), FakeValue::Int(1)); + } + + /// Verifies switch uses loose matching and falls through after the matching case. + #[test] + fn execute_program_switch_matches_and_falls_through() { + let program = + parse_fragment(br#"switch ($x) { case 1: echo "one"; break; case 2: echo "two"; default: echo "default"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(2).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "twodefault"); + } + + /// Verifies for loops run init, condition, update, and body in PHP order. + #[test] + fn execute_program_for_loop_updates_after_body() { + let program = parse_fragment(br#"for ($i = 3; $i; $i = $i - 1) { echo $i; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "321"); + assert_eq!(values.get(i), FakeValue::Int(0)); + } + + /// Verifies `continue` in a for loop still runs the update clause. + #[test] + fn execute_program_for_continue_runs_update_clause() { + let program = parse_fragment( + br#"for ($i = 3; $i; $i = $i - 1) { if ($i - 1) { continue; } echo "done"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "done"); + assert_eq!(values.get(i), FakeValue::Int(0)); + } + + /// Verifies comparison operators return boolean cells usable by echo and branches. + #[test] + fn execute_program_comparisons_return_bool_cells() { + let program = parse_fragment( + br#"echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; if ("10" == 10) { echo "n"; } if ("a" != "b") { echo "s"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1111ns"); + } + + /// Verifies spaceship comparisons return PHP -1/0/1 integer cells. + #[test] + fn execute_program_spaceship_returns_int_cells() { + let program = + parse_fragment(br#"echo 1 <=> 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "-1:0:1"); + } + + /// Verifies strict equality keeps PHP type identity distinct from loose equality. + #[test] + fn execute_program_strict_equality_uses_type_identity() { + let program = parse_fragment( + br#"echo "10" == 10; echo "10" === 10; echo "10" === "10"; echo "10" !== 10;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111"); + } + + /// Verifies logical AND skips an unsupported right-hand expression after a false left side. + #[test] + fn execute_program_short_circuits_logical_and() { + let program = + parse_fragment(br#"return false && missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(false)); + } + + /// Verifies logical OR skips an unsupported right-hand expression after a true left side. + #[test] + fn execute_program_short_circuits_logical_or() { + let program = parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies match expressions use strict comparison across comma-separated patterns. + #[test] + fn execute_program_match_uses_strict_pattern_comparison() { + let program = + parse_fragment(br#"return match ($x) { 1, "1" => "string", default => "other" };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.string("1").expect("create fake string"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("string".to_string())); + } + + /// Verifies match expressions evaluate only the selected arm result. + #[test] + fn execute_program_match_skips_unselected_results() { + let program = parse_fragment( + br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("two".to_string())); + } + + /// Verifies match expressions without a matching arm or default fail at runtime. + #[test] + fn execute_program_match_without_default_fails_on_miss() { + let program = parse_fragment(br#"return match (3) { 1 => "one", 2 => "two" };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + } + + /// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. + #[test] + fn execute_program_evaluates_keyword_logical_operators() { + let program = parse_fragment( + br#"echo (false || true and false) ? "T" : "F"; return true or missing();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "F"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies PHP keyword `xor` evaluates both operands and returns a boolean cell. + #[test] + fn execute_program_evaluates_keyword_xor() { + let program = parse_fragment( + br#"echo (true xor false) ? "T" : "F"; echo (true xor true) ? "T" : "F";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TF"); + } + + /// Verifies ternary expressions evaluate only the selected branch. + #[test] + fn execute_program_ternary_short_circuits_unselected_branch() { + let program = + parse_fragment(br#"echo true ? "yes" : missing(); echo false ? missing() : "no";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "yesno"); + } + + /// Verifies the short ternary form returns the condition value when it is truthy. + #[test] + fn execute_program_short_ternary_reuses_truthy_condition() { + let program = parse_fragment(br#"echo "x" ?: "fallback"; echo false ?: "fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xfallback"); + } + + /// Verifies null coalescing uses the default for missing or null values. + #[test] + fn execute_program_null_coalesce_uses_default_for_missing_or_null() { + let program = + parse_fragment(br#"echo $missing ?? "fallback"; echo $x ?? "null-fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.null().expect("create fake null"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "fallbacknull-fallback"); + } + + /// Verifies null coalescing skips the default expression for non-null values. + #[test] + fn execute_program_null_coalesce_short_circuits_non_null_value() { + let program = parse_fragment(br#"echo "set" ?? missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "set"); + } + + /// Verifies logical negation returns boolean cells using PHP truthiness. + #[test] + fn execute_program_evaluates_logical_not() { + let program = parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1"); + } + + /// Verifies unary numeric operators delegate to PHP numeric runtime operations. + #[test] + fn execute_program_evaluates_unary_numeric_ops() { + let program = parse_fragment(br#"return -$x + +2;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(5).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(-3)); + } + + /// Verifies foreach assigns each indexed element to the value variable. + #[test] + fn execute_program_foreach_iterates_indexed_values() { + let program = parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "ab"); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); + } + + /// Verifies foreach key-value targets receive indexed integer keys and values. + #[test] + fn execute_program_foreach_assigns_indexed_keys() { + let program = + parse_fragment(br#"foreach (["a", "b"] as $key => $item) { echo $key . $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let key = scope.visible_cell("key").expect("scope should contain key"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "0a1b"); + assert_eq!(values.get(key), FakeValue::Int(1)); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); + } + + /// Verifies foreach over associative arrays preserves insertion-order keys and values. + #[test] + fn execute_program_foreach_iterates_assoc_keys_and_values() { + let program = parse_fragment( + br#"foreach (["a" => 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a:1;b:2;"); + } + + /// Verifies value-only foreach over associative arrays still yields values in insertion order. + #[test] + fn execute_program_foreach_iterates_assoc_values_only() { + let program = parse_fragment(br#"foreach (["a" => 1, "b" => 2] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12"); + } + + /// Verifies break and continue control foreach execution inside eval. + #[test] + fn execute_program_foreach_honors_break_and_continue() { + let program = parse_fragment( + br#"foreach ([1, 2, 3] as $item) { if ($item == 1) { continue; } echo $item; break; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2"); + } + + /// Verifies indexed array literals and reads execute through runtime hooks. + #[test] + fn execute_program_reads_indexed_array_literal() { + let program = parse_fragment(br#"return ["a", "b"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); + } + + /// Verifies legacy `array(...)` literals execute through the existing array runtime hooks. + #[test] + fn execute_program_reads_legacy_array_literal() { + let program = parse_fragment(br#"return array("a", "b" => "bee",)[0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("a".to_string())); + } + + /// Verifies associative array literals and string-key reads execute through runtime hooks. + #[test] + fn execute_program_reads_assoc_array_literal() { + let program = + parse_fragment(br#"return ["name" => "Ada"]["name"];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); + } + + /// Verifies unkeyed assoc literal entries start at zero after string keys. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_string_key_starts_at_zero() { + let program = parse_fragment(br#"return ["name" => "Ada", "Grace"][0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); + } + + /// Verifies unkeyed assoc literal entries use one plus the largest integer key. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_positive_int_key() { + let program = + parse_fragment(br#"return [2 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies unkeyed assoc literal entries preserve PHP's negative-key rule. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_negative_int_key() { + let program = + parse_fragment(br#"return [-2 => "minus", "tail"][-1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies numeric string literal keys update the next automatic key. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_numeric_string_key() { + let program = + parse_fragment(br#"return ["2" => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies leading-zero string literal keys do not update the automatic key. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_leading_zero_string_key() { + let program = + parse_fragment(br#"return ["02" => "two", "tail"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies null literal keys normalize to empty strings without advancing automatic keys. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_null_key() { + let program = parse_fragment(br#"return [null => "empty", "tail"][0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies null literal keys are readable through the empty-string key. + #[test] + fn execute_program_assoc_array_literal_reads_null_key_as_empty_string() { + let program = + parse_fragment(br#"return [null => "empty"][""];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("empty".to_string())); + } + + /// Verifies boolean literal keys update the next automatic key after integer normalization. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { + let program = + parse_fragment(br#"return [true => "yes", "tail"][2];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies false literal keys update the next automatic key from zero. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_false_key() { + let program = + parse_fragment(br#"return [false => "no", "tail"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies float literal keys update the next automatic key after truncation. + #[test] + fn execute_program_assoc_array_literal_unkeyed_after_float_key() { + let program = + parse_fragment(br#"return [2.7 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies nested eval calls parse and execute against the same dynamic scope. + #[test] + fn execute_program_nested_eval_uses_same_scope() { + let program = + parse_fragment(br#"eval("$x = $x + 4;"); return $x;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); + } + + /// Verifies `__LINE__` inside eval uses the source line within the fragment. + #[test] + fn execute_program_magic_line_uses_fragment_line() { + let program = parse_fragment(b"\nreturn __LINE__;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + } + + /// Verifies file-dependent eval magic constants use call-site metadata from the context. + #[test] + fn execute_program_magic_file_and_dir_use_context_call_site() { + let program = + parse_fragment(br#"return __FILE__ . "|" . __DIR__;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/main.php", "/tmp", 17); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("/tmp/main.php(17) : eval()'d code|/tmp".to_string()) + ); + } + + /// Verifies eval class, namespace, and trait magic constants are empty in eval scope. + #[test] + fn execute_program_scope_magic_constants_are_empty_strings() { + let program = parse_fragment( + br#"return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("[||]".to_string())); + } + + /// Verifies eval-declared functions can be called by the same fragment. + #[test] + fn execute_program_calls_declared_function() { + let program = parse_fragment(br#"function dyn($x) { return $x + 1; } return dyn(4);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); + } + + /// Verifies eval namespace declarations qualify functions and namespace magic values. + #[test] + fn execute_program_namespace_qualifies_declared_function() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function dyn() { return __NAMESPACE__ . ":" . __FUNCTION__; } +return dyn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("Eval\\Ns:Eval\\Ns\\dyn".to_string()) + ); + } + + /// Verifies unqualified namespaced calls fall back to global builtins when needed. + #[test] + fn execute_program_namespace_call_falls_back_to_builtin() { + let program = parse_fragment(br#"namespace Eval\Ns; return strlen("abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + } + + /// Verifies namespaced dynamic functions take precedence over global builtin fallback. + #[test] + fn execute_program_namespace_function_overrides_builtin_fallback() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function strlen($value) { return 99; } +return strlen("abcd");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(99)); + } + + /// Verifies unqualified namespaced constants fall back to global predefined constants. + #[test] + fn execute_program_namespace_const_fetch_falls_back_to_global() { + let program = + parse_fragment(br#"namespace Eval\Ns; return PHP_EOL;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("\n".to_string())); + } + + /// Verifies namespaced dynamic constants take precedence over global fallback. + #[test] + fn execute_program_namespace_const_fetch_reads_dynamic_constant_first() { + let program = + parse_fragment(br#"namespace Eval\Ns; return LOCAL;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(7).expect("create fake int"); + assert!(context.define_constant("Eval\\Ns\\LOCAL", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); + } + + /// Verifies eval namespace `use function` imports dispatch to qualified dynamic functions. + #[test] + fn execute_program_namespace_use_function_import_dispatches() { + let program = parse_fragment( + br#"namespace Eval\Lib; +function target($x) { return $x + 1; } +namespace Eval\App; +use function Eval\Lib\target as AliasTarget; +return aliastarget(6);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); + } + + /// Verifies eval namespace `use const` imports fetch qualified dynamic constants. + #[test] + fn execute_program_namespace_use_const_import_fetches_dynamic_constant() { + let program = parse_fragment( + br#"namespace Eval\App; +use const Eval\Lib\VALUE as LocalValue; +return LocalValue;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(11).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(11)); + } + + /// Verifies eval grouped namespace imports dispatch dynamic functions and constants. + #[test] + fn execute_program_grouped_namespace_use_imports_dispatch() { + let program = parse_fragment( + br#"namespace Eval\Lib; +function target($x) { return $x + 2; } +namespace Eval\App; +use function Eval\Lib\{target as AliasTarget}; +use const Eval\Lib\{VALUE as LocalValue}; +return AliasTarget(LocalValue);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(5).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); + } + + /// Verifies eval-declared functions bind named arguments by parameter name. + #[test] + fn execute_program_calls_declared_function_with_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(y: 2, x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies eval-declared functions unpack indexed arrays as positional arguments. + #[test] + fn execute_program_calls_declared_function_with_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...[1, 2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies string keys unpack as named arguments for eval-declared functions. + #[test] + fn execute_program_calls_declared_function_with_named_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...["y" => 2], x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies eval-declared function static locals persist between calls. + #[test] + fn execute_program_static_var_persists_in_declared_function() { + let program = parse_fragment( + br#"function dyn() { for ($i = 0; $i < 2; $i++) { static $n = 0; $n++; } return $n; } +return (dyn() * 10) + dyn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(24)); + } + + /// Verifies top-level eval static declarations reinitialize on each eval execution. + #[test] + fn execute_program_top_level_static_var_reinitializes_per_eval() { + let program = + parse_fragment(br#"static $n = 0; $n++; return $n;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let first = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute first eval ir"); + let second = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute second eval ir"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(1)); + } + + /// Verifies `global` declarations read and write the context global scope. + #[test] + fn execute_program_global_alias_writes_context_global_scope() { + let program = + parse_fragment(br#"global $g; $g = $g + 1; return $g;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.get(global), FakeValue::Int(2)); + } + + /// Verifies references to global aliases write the source global variable. + #[test] + fn execute_program_reference_alias_to_global_updates_source_global() { + let program = parse_fragment(br#"global $g; $alias =& $g; $alias = 4; return $g;"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(4)); + assert_eq!(values.get(global), FakeValue::Int(4)); + assert!(global_scope.visible_cell("alias").is_none()); + } + + /// Verifies named calls reject positional arguments that follow named arguments. + #[test] + fn execute_program_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, print "late");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert_eq!(values.output, ""); + } + + /// Verifies named calls reject argument unpacking after named arguments. + #[test] + fn execute_program_rejects_spread_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, ...[2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + } + + /// Verifies function-scope magic constants keep the eval declaration spelling. + #[test] + fn execute_program_magic_function_and_method_use_eval_declared_name() { + let program = parse_fragment( + br#"function DynMagicCase() { return __FUNCTION__ . ":" . __METHOD__; } return dynmagiccase();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("DynMagicCase:DynMagicCase".to_string()) + ); + } + + /// Verifies eval-declared functions persist in a shared eval context. + #[test] + fn execute_program_context_keeps_declared_function() { + let define = + parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("parse eval fragment"); + let call = parse_fragment(br#"return dyn(4);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute eval ir"); + let result = execute_program_with_context(&mut context, &call, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); + } + + /// Verifies `call_user_func` inside eval can dispatch an eval-declared function. + #[test] + fn execute_program_call_user_func_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x) { return $x + 1; } +return call_user_func("dyn", 4);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); + } + + /// Verifies `call_user_func` inside eval can dispatch a supported builtin. + #[test] + fn execute_program_call_user_func_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + } + + /// Verifies `call_user_func` inside eval can dispatch a registered native function. + #[test] + fn execute_program_call_user_func_dispatches_registered_native_function() { + let program = parse_fragment(br#"return call_user_func("native_answer");"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + + /// Verifies string variable calls inside eval can dispatch a supported builtin. + #[test] + fn execute_program_variable_call_dispatches_builtin() { + let program = parse_fragment( + br#"$fn = "strlen"; +return $fn("abcd");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + } + + /// Verifies callable array entries can be invoked through postfix dynamic calls. + #[test] + fn execute_program_postfix_variable_call_dispatches_builtin() { + let program = parse_fragment( + br#"$callbacks = ["strlen"]; +return $callbacks[0]("abc");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(3)); + } + + /// Verifies variable calls bind eval-declared function arguments by name. + #[test] + fn execute_program_variable_call_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } +$fn = "dyn"; +return $fn(y: 2, x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies variable calls can dispatch registered native functions with named args. + #[test] + fn execute_program_variable_call_binds_registered_native_named_args() { + let program = parse_fragment( + br#"$fn = "native_answer"; +return $fn(right: 2, left: 1);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + + /// Verifies direct callable-array variable calls dispatch object methods. + #[test] + fn execute_program_callable_array_variable_dispatches_object_method() { + let program = parse_fragment( + br#"$box = new Box(41); +$cb = [$box, "add_x"]; +return $cb(1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies `call_user_func` dispatches callable arrays with object receivers. + #[test] + fn execute_program_call_user_func_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(42); +$cb = [$box, "read_x"]; +return call_user_func($cb);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies `call_user_func_array` dispatches callable arrays with positional args. + #[test] + fn execute_program_call_user_func_array_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(39); +return call_user_func_array([$box, "add2_x"], [1, 2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + } + + /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. + #[test] + fn execute_program_call_user_func_array_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", [4, 5]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(9)); + } + + /// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. + #[test] + fn execute_program_call_user_func_array_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } +return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. + #[test] + fn execute_context_function_call_array_binds_declared_named_args() { + let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + let arg_array = values.assoc_new(2).expect("allocate argument array"); + let key_y = values.string("y").expect("allocate y key"); + let value_y = values.int(2).expect("allocate y value"); + let _ = values + .array_set(arg_array, key_y, value_y) + .expect("store y argument"); + let key_x = values.string("x").expect("allocate x key"); + let value_x = values.int(1).expect("allocate x value"); + let _ = values + .array_set(arg_array, key_x, value_x) + .expect("store x argument"); + + let result = + execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) + .expect("execute context function call array"); + + assert_eq!(values.get(result), FakeValue::Int(12)); + } + + /// Verifies `call_user_func_array` rejects positional values after named keys. + #[test] + fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", ["y" => 2, 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + } + + /// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. + #[test] + fn execute_program_call_user_func_array_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + } + + /// Verifies `call_user_func_array` inside eval can dispatch a registered native function. + #[test] + fn execute_program_call_user_func_array_dispatches_registered_native_function() { + let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + + /// Verifies `call_user_func_array` named keys can bind registered native parameters. + #[test] + fn execute_program_call_user_func_array_binds_registered_native_named_args() { + let program = parse_fragment( + br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + + /// Verifies duplicate eval-declared function names fail in a shared context. + #[test] + fn execute_program_rejects_duplicate_declared_function() { + let define = + parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute first declaration"); + let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect_err("duplicate function declaration should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } + + /// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. + #[test] + fn execute_program_dispatches_simple_builtins() { + let program = parse_fragment( + br#"echo strlen("abc") . ":" . count([1, [2, 3], [4]]) . ":"; +echo count([1, [2, 3], [4]], COUNT_RECURSIVE) . ":"; +echo call_user_func("count", [1, [2]]) . ":"; +echo call_user_func_array("count", ["value" => [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; +return defined("COUNT_RECURSIVE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:6:2:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. + #[test] + fn execute_program_dispatches_json_encode_builtin() { + let program = parse_fragment( + br#"echo json_encode(["a" => 1, "b" => "x/y"]) . ":"; +echo json_encode([1, "q", true, null]) . ":"; +echo call_user_func("json_encode", "a/b\"c") . ":"; +echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; +echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; +echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; +$accent = json_decode("\"\\u00e9\""); +$emoji = json_decode("\"\\ud83d\\ude00\""); +echo bin2hex(json_encode($accent . "/" . $emoji)) . ":"; +echo bin2hex(json_encode($accent . "/" . $emoji, JSON_UNESCAPED_UNICODE)) . ":"; +echo bin2hex(json_encode([$accent => $emoji])) . ":"; +echo bin2hex(json_encode([$accent => $emoji], JSON_UNESCAPED_UNICODE)) . ":"; +echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; +echo json_encode([], JSON_FORCE_OBJECT) . ":"; +echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; +echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; +echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; +echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; +echo (json_encode(INF) === false ? "false" : "json") . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +$bad = "a" . hex2bin("80") . "b"; +echo (json_encode($bad) === false ? "utf8-false" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR)) . ":"; +echo json_last_error() . ":"; +echo json_encode($bad, JSON_INVALID_UTF8_IGNORE) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE)) . ":"; +echo json_last_error() . ":"; +echo json_encode([hex2bin("6b80") => hex2bin("7680")], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":"; +json_encode(3.5); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; +return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"k\ufffd":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `json_decode()` materializes scalars, arrays, and associative arrays. + #[test] + fn execute_program_dispatches_json_decode_builtin() { + let program = parse_fragment( + br#"echo json_decode("\"hello\"") . ":"; +echo json_decode("42") . ":"; +echo (json_decode("true") ? "T" : "bad") . ":"; +echo (is_null(json_decode("null")) ? "NULL" : "bad") . ":"; +$decoded = json_decode("{\"a\":1,\"b\":[\"x\",false]}", true); +echo $decoded["a"] . ":" . $decoded["b"][0] . ":" . ($decoded["b"][1] ? "bad" : "F") . ":"; +$call = call_user_func("json_decode", "[3,4]"); +echo $call[1] . ":"; +$named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); +echo $named["k"] . ":"; +$badJson = "\"a" . hex2bin("80") . "b\""; +echo (is_null(json_decode($badJson)) ? "utf8-null" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_IGNORE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +$objSub = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_SUBSTITUTE); +$objSubKeys = array_keys($objSub); +echo bin2hex($objSubKeys[0]) . "=" . bin2hex($objSub[$objSubKeys[0]]) . ":"; +$objIgnore = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_IGNORE); +$objIgnoreKeys = array_keys($objIgnore); +echo bin2hex($objIgnoreKeys[0]) . "=" . bin2hex($objIgnore[$objIgnoreKeys[0]]) . ":"; +echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; +$big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); +echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo gettype($big[0]) . ":" . $big[0] . ":"; +echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; +return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. + #[test] + fn execute_program_dispatches_json_decode_stdclass_default() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +echo $object->a . ":" . $object->b->c . ":"; +$objectFalse = json_decode("{\"z\":2}", false); +echo $objectFalse->z . ":"; +$objectNull = json_decode("{\"n\":{\"m\":3}}", null); +echo $objectNull->n->m . ":"; +$assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); +echo $assoc["b"]["c"] . ":"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:x:2:3:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `json_encode()` serializes stdClass dynamic properties. + #[test] + fn execute_program_dispatches_json_encode_stdclass_object() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +echo json_encode($object) . ":"; +echo str_replace("\n", "|", json_encode($object, JSON_PRETTY_PRINT)) . ":"; +$empty = json_decode("{}"); +echo json_encode($empty) . ":"; +$empty->a = 7; +echo json_encode($empty); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. + #[test] + fn execute_program_dispatches_json_last_error_builtins() { + let program = parse_fragment( + br#"echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("bad"); +echo json_last_error() . ":" . (json_last_error() === JSON_ERROR_SYNTAX ? "syntax" : "bad") . ":" . json_last_error_msg() . ":"; +json_validate("[1]", 1); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("\"ok\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("\"a" . chr(10) . "b\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("\"\\uD83D\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("\"a" . chr(128) . "b\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("[0]"); +echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_error_msg", []) . ":"; +return function_exists("json_last_error") && function_exists("json_last_error_msg") && defined("JSON_ERROR_SYNTAX");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "0:No error:4:syntax:Syntax error near location 1:1:1:Maximum stack depth exceeded near location 1:1:0:No error:3:Control character error, possibly incorrectly encoded near location 1:3:10:Single unpaired UTF-16 surrogate in unicode escape near location 1:8:5:Malformed UTF-8 characters, possibly incorrectly encoded near location 1:3:0:No error:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval JSON throw flags raise catchable Throwable objects. + #[test] + fn execute_program_dispatches_json_throw_on_error() { + let program = parse_fragment( + br#"try { + json_decode("bad", true, 512, JSON_THROW_ON_ERROR); + echo "bad"; +} catch (Throwable) { + echo "decode:"; +} +try { + json_encode(INF, JSON_THROW_ON_ERROR); + echo "bad"; +} catch (Throwable) { + echo "encode:"; +} +echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +return defined("JSON_THROW_ON_ERROR");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "decode:encode:0:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. + #[test] + fn execute_program_dispatches_json_validate_builtin() { + let program = parse_fragment( + br#"echo (json_validate("{\"a\":[1,true,null,\"caf\\u00e9\"]}") ? "Y" : "N") . ":"; +echo (json_validate("bad") ? "bad" : "N") . ":"; +echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; +echo (call_user_func("json_validate", "\"x\"") ? "C" : "bad") . ":"; +echo (call_user_func_array("json_validate", ["json" => "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; +echo (json_validate("\"a" . chr(128) . "b\"", 512, JSON_INVALID_UTF8_IGNORE) ? "I" : "bad") . ":"; +echo json_last_error() . ":"; +echo (json_validate("bad", 512, JSON_INVALID_UTF8_IGNORE) ? "bad" : "S") . ":"; +echo json_last_error() . ":"; +return function_exists("json_validate") && defined("JSON_INVALID_UTF8_IGNORE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:D:C:A:I:0:S:4:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies direct eval builtin calls bind named and unpacked arguments. + #[test] + fn execute_program_dispatches_named_and_spread_builtins() { + let program = parse_fragment( + br#"echo strlen(string: "abcd"); +echo ":" . (array_key_exists(array: ["name" => 1], key: "name") ? "Y" : "N"); +echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N"); +return round(precision: 1, num: 3.14);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:Y:Y"); + assert_eq!(values.get(result), FakeValue::Float(3.1)); + } + + /// Verifies eval `ord()` returns the first byte and supports callable dispatch. + #[test] + fn execute_program_dispatches_ord_builtin() { + let program = parse_fragment( + br#"echo ord("A"); +echo ":" . ord(""); +echo ":" . call_user_func("ord", "B"); +echo ":" . call_user_func_array("ord", ["C"]); +echo ":"; echo function_exists("ord"); +return ord("Z");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "65:0:66:67:1"); + assert_eq!(values.get(result), FakeValue::Int(90)); + } + + /// Verifies eval array aggregate builtins iterate array values and support callable dispatch. + #[test] + fn execute_program_dispatches_array_aggregate_builtins() { + let program = parse_fragment( + br#"echo array_sum([1, 2, 3]); +echo ":" . array_product([2, 3, 4]); +echo ":" . array_sum([]); +echo ":" . array_product([]); +echo ":" . array_sum(["a" => 2, "b" => 5]); +echo ":" . call_user_func("array_sum", [3, 4]); +echo ":" . call_user_func_array("array_product", [[2, 5]]); +echo ":"; echo function_exists("array_sum"); +return function_exists("array_product");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "6:24:0:1:7:7:10:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_map()` applies callbacks and preserves source keys. + #[test] + fn execute_program_dispatches_array_map_builtin() { + let program = parse_fragment( + br#"function eval_map_double($value) { return $value * 2; } +$mapped = array_map("eval_map_double", [1, 2, 3]); +echo $mapped[0] . ":" . $mapped[2] . ":"; +$assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); +echo $assoc["a"] . ":" . $assoc["b"] . ":"; +$identity = array_map(null, ["k" => "v"]); +echo $identity["k"] . ":"; +function eval_map_pair($left, $right) { return $left . "-" . ($right ?? "N"); } +$pairs = array_map("eval_map_pair", ["a" => "L", "b" => "R"], ["x" => "1"]); +echo $pairs[0] . ":" . $pairs[1] . ":"; +$zipped = array_map(null, [1, 2], [3, 4]); +echo $zipped[0][0] . $zipped[0][1] . ":" . $zipped[1][0] . $zipped[1][1] . ":"; +$call = call_user_func("array_map", "intval", ["7"]); +echo $call[0] . ":"; +$multi_call = call_user_func("array_map", "eval_map_pair", ["Q"], ["9"]); +echo $multi_call[0] . ":"; +$spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); +echo $spread[0] . ":"; +return function_exists("array_map");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_reduce()` folds values through a string callback. + #[test] + fn execute_program_dispatches_array_reduce_builtin() { + let program = parse_fragment( + br#"function eval_reduce_sum($carry, $item) { return $carry + $item; } +echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; +function eval_reduce_join($carry, $item) { return $carry . $item; } +echo array_reduce([4, 5], "eval_reduce_sum") . ":"; +echo array_reduce(["a", "b"], "eval_reduce_join", "") . ":"; +$named = array_reduce(array: [6, 7], callback: "eval_reduce_sum"); +echo $named . ":"; +$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum"); +echo $call . ":"; +$spread = call_user_func_array("array_reduce", ["array" => [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); +echo $spread . ":"; +return function_exists("array_reduce");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "16:9:ab:13:9:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_walk()` invokes string callbacks with value and key cells. + #[test] + fn execute_program_dispatches_array_walk_builtin() { + let program = parse_fragment( + br#"function eval_walk_show($value, $key) { echo $key . "=" . $value . ";"; } +echo array_walk(["a" => 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; +$call = call_user_func("array_walk", [4, 5], "eval_walk_show"); +$spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); +return function_exists("array_walk");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_pop()` and `array_shift()` write back only for direct variable calls. + #[test] + fn execute_program_dispatches_array_pop_shift_builtins() { + let program = parse_fragment( + br#"$a = [1, 2, 3]; +echo array_pop($a) . ":" . count($a) . ":" . $a[1] . ":"; +$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; +$c = [4, 5]; +echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; +$d = [6, 7]; +echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; +return function_exists("array_pop") && function_exists("array_shift");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:2:2:1:2:3:4:5:2:5:6:2:6:"); + assert_eq!( + values.warnings, + vec![ + "array_pop(): Argument #1 ($array) must be passed by reference, value given", + "array_shift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_push()` and `array_unshift()` write back direct variable calls. + #[test] + fn execute_program_dispatches_array_push_unshift_builtins() { + let program = parse_fragment( + br#"$a = [1]; +echo array_push($a, 2, 3) . ":" . $a[2] . ":"; +$b = ["x" => 1, 10 => 2]; +echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; +$c = [2, 3]; +echo array_unshift($c, 0, 1) . ":" . $c[0] . ":" . $c[3] . ":"; +$d = ["x" => 1, 10 => 2, "y" => 3]; +echo array_unshift($d, "A") . ":" . $d[0] . ":" . $d["x"] . ":" . $d[1] . ":" . $d["y"] . ":"; +$e = [5]; +echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; +$f = [7]; +echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; +return function_exists("array_push") && function_exists("array_unshift");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:"); + assert_eq!( + values.warnings, + vec![ + "array_push(): Argument #1 ($array) must be passed by reference, value given", + "array_unshift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_splice()` returns removed values and writes back direct variable calls. + #[test] + fn execute_program_dispatches_array_splice_builtin() { + let program = parse_fragment( + br#"$a = [10, 20, 30, 40]; +$removed = array_splice($a, 1, 2); +echo count($removed) . ":" . $removed[0] . ":" . $removed[1] . ":" . count($a) . ":" . $a[1] . ":"; +$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$cut = array_splice(array: $b, offset: 1, length: 2); +echo $cut[0] . ":" . $cut["y"] . ":" . $b["x"] . ":" . $b[0] . ":"; +$c = [1, 2, 3, 4]; +$tail = call_user_func("array_splice", $c, -2, 1); +echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; +$d = [5, 6, 7]; +$all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); +echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; +$e = [1, 2, 3, 4]; +$rep = array_splice($e, 1, 2, ["A", "B"]); +echo count($rep) . ":" . $rep[0] . ":" . $rep[1] . ":" . $e[0] . ":" . $e[1] . ":" . $e[2] . ":" . $e[3] . ":"; +$f = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$rep2 = array_splice(array: $f, offset: 1, length: 2, replacement: ["s" => "S", 9 => "N"]); +echo $rep2[0] . ":" . $rep2["y"] . ":" . $f["x"] . ":" . $f[0] . ":" . $f[1] . ":" . $f[2] . ":"; +$g = [1, 2, 3]; +$rep3 = array_splice($g, offset: 1, replacement: [9]); +echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g[1] . ":"; +$h = [1, 2, 3]; +$removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); +echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; +return function_exists("array_splice");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:" + ); + assert_eq!( + values.warnings, + vec![ + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `sort()` and `rsort()` reindex direct variable arrays only. + #[test] + fn execute_program_dispatches_sort_builtins() { + let program = parse_fragment( + br#"$a = [3, 1, 2]; +echo sort($a) . ":" . $a[0] . $a[1] . $a[2] . ":"; +$b = ["banana", "apple", "cherry"]; +echo rsort(array: $b) . ":" . $b[0] . ":" . $b[2] . ":"; +$c = ["x" => 3, "y" => 1, "z" => 2]; +sort($c); +echo $c[0] . $c[1] . $c[2] . ":"; +$d = [3, 1, 2]; +echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; +$e = [1, 2, 3]; +echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; +return function_exists("sort") && function_exists("rsort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:123:1:cherry:apple:123:1:312:1:1:3:"); + assert_eq!( + values.warnings, + vec![ + "sort(): Argument #1 ($array) must be passed by reference, value given", + "rsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval key-preserving array ordering builtins write back direct variable calls. + #[test] + fn execute_program_dispatches_key_preserving_sort_builtins() { + let program = parse_fragment( + br#"$a = ["x" => 3, "y" => 1, "z" => 2]; +echo asort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value; } +echo ":"; +$b = ["x" => 1, "y" => 3, "z" => 2]; +echo arsort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2, 3 => 4]; +echo ksort($c) . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = ["b" => 1, "a" => 2, 3 => 4]; +echo krsort($d) . ":"; +foreach ($d as $key => $value) { echo $key . $value; } +echo ":"; +$e = ["x" => 2, "y" => 1]; +echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; +$f = ["b" => 1, "a" => 2]; +echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; +return function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:" + ); + assert_eq!( + values.warnings, + vec![ + "asort(): Argument #1 ($array) must be passed by reference, value given", + "krsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval natural sort builtins preserve keys and use natural string order. + #[test] + fn execute_program_dispatches_natural_sort_builtins() { + let program = parse_fragment( + br#"$a = ["img10", "img2", "img1"]; +echo natsort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$b = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +echo natcasesort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$c = ["x" => "b", "y" => "a"]; +echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; +return function_exists("natsort") && function_exists("natcasesort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:" + ); + assert_eq!( + values.warnings, + vec!["natsort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `shuffle()` reindexes direct variable arrays only. + #[test] + fn execute_program_dispatches_shuffle_builtin() { + let program = parse_fragment( + br#"$a = ["x" => 1, "y" => 2]; +echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; +$b = ["x" => 1, "y" => 2]; +echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; +return function_exists("shuffle");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:reindexed:2:3:1:12:"); + assert_eq!( + values.warnings, + vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval user-comparator sort builtins call callbacks and write back direct arrays. + #[test] + fn execute_program_dispatches_user_sort_builtins() { + let program = parse_fragment( + br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } +function eval_key_cmp($left, $right) { return strcmp($left, $right); } +$a = [3, 1, 2]; +echo usort($a, "eval_sort_cmp") . ":"; +foreach ($a as $value) { echo $value; } +echo ":"; +$b = ["b" => 1, "a" => 3, "c" => 2]; +echo uasort(array: $b, callback: "eval_sort_cmp") . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2]; +echo uksort($c, "eval_key_cmp") . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = [2, 1]; +echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; +return function_exists("usort") && function_exists("uasort") && function_exists("uksort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:"); + assert_eq!( + values.warnings, + vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval iterator array helpers support direct and dynamic builtin calls. + #[test] + fn execute_program_dispatches_iterator_array_builtins() { + let program = parse_fragment( + br#"$items = ["x" => 1, "y" => 2]; +$copy = iterator_to_array($items); +echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; +$values = iterator_to_array($items, false); +echo (isset($values["x"]) ? "bad" : "reindexed") . ":" . $values[0] . $values[1] . ":"; +echo call_user_func("iterator_count", $items) . ":"; +$spread = call_user_func_array("iterator_to_array", ["iterator" => $items, "preserve_keys" => false]); +echo $spread[0] . $spread[1] . ":"; +return function_exists("iterator_count") && function_exists("iterator_to_array");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:12:reindexed:12:2:12:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `iterator_apply()` drives Iterator objects and callback args. + #[test] + fn execute_program_dispatches_iterator_apply_object_builtin() { + let program = parse_fragment( + br#"function eval_apply($prefix) { echo $prefix; return true; } +echo iterator_apply($it, "eval_apply", ["prefix" => "x"]) . ":"; +echo call_user_func("iterator_apply", $it, "eval_apply", ["y"]) . ":"; +return function_exists("iterator_apply");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xxx3:yyy3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `iterator_apply()` accepts object-method callable arrays. + #[test] + fn execute_program_iterator_apply_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(5); +echo iterator_apply($it, [$box, "add_x"], [1]) . ":"; +return call_user_func("iterator_apply", $it, [$box, "add_x"], [1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:"); + assert_eq!(values.get(result), FakeValue::Int(3)); + } + + /// Verifies eval `iterator_apply()` counts the position where the callback stops. + #[test] + fn execute_program_iterator_apply_stops_on_falsey_callback() { + let program = parse_fragment( + br#"function eval_stop() { echo "s"; return false; } +return iterator_apply($it, "eval_stop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "s"); + assert_eq!(values.get(result), FakeValue::Int(1)); + } + + /// Verifies eval `array_filter()` removes falsey values while preserving original keys. + #[test] + fn execute_program_dispatches_array_filter_builtin() { + let program = parse_fragment( + br#"$filtered = array_filter([0, 1, 2, "", false, null, "0", "ok"]); +echo count($filtered) . ":" . $filtered[1] . ":" . $filtered[2] . ":" . $filtered[7] . ":"; +$assoc = array_filter(["a" => 0, "b" => 2, "c" => ""]); +echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; +$null = array_filter([0, 3], null, 1); +echo count($null) . ":" . $null[1] . ":"; +$call = call_user_func("array_filter", [0, 4]); +echo count($call) . ":" . $call[1] . ":"; +$spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); +echo count($spread) . ":" . $spread[1] . ":"; +function eval_keep_even($value) { return $value % 2 == 0; } +$evens = array_filter([1, 2, 3, 4], "eval_keep_even"); +echo count($evens) . ":" . $evens[1] . ":" . $evens[3] . ":"; +function eval_keep_key($key) { return $key === "b"; } +$keyed = array_filter(["a" => 10, "b" => 20], "eval_keep_key", ARRAY_FILTER_USE_KEY); +echo count($keyed) . ":" . $keyed["b"] . ":"; +function eval_keep_both($value, $key) { return $key === "c" || $value === 1; } +$both = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_keep_both", ARRAY_FILTER_USE_BOTH); +echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; +$ints = array_filter([1, "x", 2], "is_int"); +echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; +return function_exists("array_filter");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_combine()` converts key values through PHP string-key rules. + #[test] + fn execute_program_dispatches_array_combine_builtin() { + let program = parse_fragment( + br#"$pairs = array_combine(["a", "b"], [10, 20]); +echo $pairs["a"] . ":" . $pairs["b"]; +$numeric = array_combine(["1", "01"], ["n", "z"]); +echo ":" . $numeric[1] . $numeric["01"]; +$scalar = array_combine([null, true, false, 2.8], ["n", "t", "f", "d"]); +echo ":" . $scalar[""] . $scalar[1] . $scalar["2.8"]; +$named = array_combine(keys: ["k"], values: ["v"]); +echo ":" . $named["k"]; +$call = call_user_func("array_combine", ["x"], [7]); +echo ":" . $call["x"]; +$spread = call_user_func_array("array_combine", [["y"], [8]]); +echo ":" . $spread["y"] . ":"; +return function_exists("array_combine");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:nz:ftd:v:7:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_column()` extracts present row columns and reindexes them. + #[test] + fn execute_program_dispatches_array_column_builtin() { + let program = parse_fragment( + br#"$rows = [["name" => "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; +$names = array_column($rows, "name"); +echo count($names) . ":" . $names[0] . ":" . $names[1]; +$scores = array_column($rows, "score"); +echo ":" . count($scores) . ":" . $scores[0] . $scores[2]; +$numeric = array_column([[0 => "zero", 1 => "one"], [1 => "uno"]], 1); +echo ":" . count($numeric) . ":" . $numeric[0] . ":" . $numeric[1]; +$named = array_column(array: $rows, column_key: "score"); +echo ":" . $named[1]; +$call = call_user_func("array_column", [["x" => 5], ["x" => 6]], "x"); +echo ":" . $call[1]; +$spread = call_user_func_array("array_column", [[["y" => 7], ["z" => 0], ["y" => 9]], "y"]); +echo ":" . count($spread) . ":" . $spread[1] . ":"; +return function_exists("array_column");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. + #[test] + fn execute_program_dispatches_array_shape_builtins() { + let program = parse_fragment( + br#"$right = array_pad([1, 2], 5, 0); +echo count($right) . ":" . $right[0] . $right[1] . $right[2] . $right[4]; +$left = array_pad([1, 2], -4, 9); +echo ":" . $left[0] . $left[1] . $left[2] . $left[3]; +$copy = array_pad([7, 8], 1, 0); +echo ":" . count($copy) . ":" . $copy[0] . $copy[1]; +$chunks = array_chunk([1, 2, 3, 4, 5], 2); +echo ":" . count($chunks) . ":" . $chunks[0][1] . $chunks[2][0]; +$named = array_pad(array: ["a"], length: 2, value: "b"); +echo ":" . $named[1]; +$call = call_user_func("array_chunk", [6, 7, 8], 2); +echo ":" . $call[1][0]; +$spread = call_user_func_array("array_pad", [[1], 3, 2]); +echo ":" . $spread[2] . ":"; +return function_exists("array_pad") && function_exists("array_chunk");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:1200:9912:2:78:3:25:b:8:2:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_slice()` observes PHP offset and length bounds. + #[test] + fn execute_program_dispatches_array_slice_builtin() { + let program = parse_fragment( + br#"$mid = array_slice([10, 20, 30, 40, 50], 1, 3); +echo count($mid) . ":" . $mid[0] . $mid[1] . $mid[2]; +$tail = array_slice([10, 20, 30, 40], -2, 1); +echo ":" . $tail[0]; +$open = array_slice([10, 20, 30, 40, 50], 2); +echo ":" . count($open) . ":" . $open[0] . $open[2]; +$null_len = array_slice([5, 6, 7], 1, null); +echo ":" . $null_len[0] . $null_len[1]; +$negative_len = array_slice([10, 20, 30, 40, 50], 1, -1); +echo ":" . count($negative_len) . ":" . $negative_len[0] . $negative_len[2]; +$named = array_slice(array: [1, 2, 3], offset: 1, length: 1); +echo ":" . $named[0]; +$call = call_user_func("array_slice", [6, 7, 8], 1, 2); +echo ":" . $call[1]; +$spread = call_user_func_array("array_slice", [[9, 10, 11], 1]); +echo ":" . $spread[0] . ":"; +return function_exists("array_slice");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:203040:30:3:3050:67:3:2040:2:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. + #[test] + fn execute_program_dispatches_array_merge_builtin() { + let program = parse_fragment( + br#"$merged = array_merge([1, 2], [3, 4]); +echo count($merged) . ":" . $merged[0] . $merged[1] . $merged[2] . $merged[3]; +$left = [1, 2]; +$right = [3]; +$copy = array_merge($left, $right); +echo ":" . count($left) . ":" . $left[0] . ":" . $copy[2]; +$assoc = array_merge(["a" => 1, 2 => "x"], ["a" => 9, 5 => "y", "b" => 3]); +echo ":" . $assoc["a"] . ":" . $assoc[0] . ":" . $assoc[1] . ":" . $assoc["b"]; +$call = call_user_func("array_merge", [6], [7, 8]); +echo ":" . $call[2]; +$spread = call_user_func_array("array_merge", [[9], [10]]); +echo ":" . $spread[1] . ":"; +return function_exists("array_merge");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:1234:2:1:3:9:x:y:3:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_diff()` and `array_intersect()` compare values as strings. + #[test] + fn execute_program_dispatches_array_value_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); +echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; +echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); +echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); +$inter = array_intersect(["a" => 1, "b" => 2, "c" => "2", "d" => 3], ["2", 4]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter["c"]; +$call = call_user_func("array_diff", [1, 2, 3], [2]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); +echo ":" . count($spread) . ":" . $spread[2] . ":"; +return function_exists("array_diff") && function_exists("array_intersect");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. + #[test] + fn execute_program_dispatches_array_key_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff_key(["a" => 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); +echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; +echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); +$inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter[4]; +$call = call_user_func("array_diff_key", [10, 20, 30], [1 => 0]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); +echo ":" . count($spread) . ":" . $spread["y"] . ":"; +return function_exists("array_diff_key") && function_exists("array_intersect_key");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:2:3:no-a:2:2:3:2:1030:1:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `range()` builds inclusive ascending and descending integer arrays. + #[test] + fn execute_program_dispatches_range_builtin() { + let program = parse_fragment( + br#"$up = range(1, 4); +echo count($up) . ":" . $up[0] . $up[3]; +$down = range(4, 1); +echo ":" . count($down) . ":" . $down[0] . $down[3]; +$single = range(3, 3); +echo ":" . count($single) . ":" . $single[0]; +$named = range(start: 2, end: 4); +echo ":" . $named[0] . $named[2]; +$call = call_user_func("range", 5, 7); +echo ":" . $call[2]; +$spread = call_user_func_array("range", [8, 6]); +echo ":" . count($spread) . ":" . $spread[0] . $spread[2] . ":"; +return function_exists("range");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:14:4:41:1:3:24:7:3:86:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_rand()` returns a key that exists in the source array. + #[test] + fn execute_program_dispatches_array_rand_builtin() { + let program = parse_fragment( + br#"$nums = [10, 20, 30]; +$idx = array_rand($nums); +echo ($idx >= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; +$assoc = ["a" => 1, "b" => 2]; +$key = array_rand($assoc); +echo ":" . (array_key_exists($key, $assoc) ? "assoc" : "bad"); +$named = array_rand(array: [5, 6]); +echo ":" . (($named >= 0 && $named < 2) ? "named" : "bad"); +$call = call_user_func("array_rand", [7, 8]); +echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); +$spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); +echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; +return function_exists("array_rand");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "idx:assoc:named:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval random builtins return values inside PHP inclusive ranges. + #[test] + fn execute_program_dispatches_rand_builtins() { + let program = parse_fragment( + br#"$plain = rand(); +echo ($plain >= 0 && $plain <= 2147483647) ? "plain" : "bad"; +$bounded = rand(2, 4); +echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); +$same = mt_rand(max: 6, min: 6); +echo ":" . ($same === 6 ? "same" : "bad"); +$swapped = rand(10, 1); +echo ":" . (($swapped >= 1 && $swapped <= 10) ? "swap" : "bad"); +$call = call_user_func("mt_rand", 1, 1); +echo ":" . ($call === 1 ? "call" : "bad"); +$spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); +echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; +$secure = random_int(max: 4, min: 4); +echo ($secure === 4 ? "random" : "bad") . ":"; +$secureCall = call_user_func("random_int", 5, 5); +echo ($secureCall === 5 ? "random-call" : "bad") . ":"; +$secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); +echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; +echo function_exists("rand"); +echo function_exists("mt_rand"); +return function_exists("random_int");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "plain:range:same:swap:call:spread:random:random-call:random-spread:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. + #[test] + fn execute_program_dispatches_array_fill_builtins() { + let program = parse_fragment( + br#"$filled = array_fill(2, 3, "x"); +echo count($filled) . ":" . $filled[2] . $filled[4]; +$negative = array_fill(-2, 3, 7); +echo ":" . $negative[-2] . $negative[-1] . $negative[0]; +$empty = array_fill(5, 0, "x"); +echo ":" . count($empty); +$map = array_fill_keys(["a", "1", "01"], 8); +echo ":" . $map["a"] . ":" . $map[1] . ":" . $map["01"]; +$named = array_fill(start_index: 1, count: 2, value: "n"); +echo ":" . $named[1] . $named[2]; +$call = call_user_func("array_fill", 0, 2, "c"); +echo ":" . $call[0] . $call[1]; +$spread = call_user_func_array("array_fill_keys", [["x", "y"], "z"]); +echo ":" . $spread["x"] . $spread["y"] . ":"; +return function_exists("array_fill") && function_exists("array_fill_keys");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:xx:777:0:8:8:8:nn:cc:zz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. + #[test] + fn execute_program_dispatches_array_flip_builtin() { + let program = parse_fragment( + br#"$flipped = array_flip(["a" => "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); +echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); +$named = array_flip(array: ["k" => "v"]); +echo ":" . $named["v"]; +$call = call_user_func("array_flip", ["left" => "right"]); +echo ":" . $call["right"]; +$spread = call_user_func_array("array_flip", [["n" => 9]]); +echo ":" . $spread[9] . ":"; +return function_exists("array_flip");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "c:b:d:e:4:k:left:n:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_unique()` preserves first keys using default string comparison. + #[test] + fn execute_program_dispatches_array_unique_builtin() { + let program = parse_fragment( + br#"$unique = array_unique(["a", "b", "a", "2", 2]); +echo count($unique) . ":" . $unique[0] . $unique[1] . $unique[3]; +$assoc = array_unique(["x" => "a", "y" => "b", "z" => "a"]); +echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; +$scalar = array_unique([1, "1", 1.0, true, false, null, ""]); +echo ":" . count($scalar) . ":" . $scalar[0] . ":"; +echo $scalar[4] ? "bad" : "F"; +$named = array_unique(array: ["k" => "v", "l" => "v"]); +echo ":" . $named["k"] . ":" . count($named); +$call = call_user_func("array_unique", ["q", "q", "r"]); +echo ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_unique", [["s", "s", "t"]]); +echo ":" . $spread[0] . $spread[2] . ":"; +return function_exists("array_unique");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:ab2:2:ab:2:1:F:v:1:qr:st:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval array projection builtins produce indexed key/value arrays. + #[test] + fn execute_program_dispatches_array_projection_builtins() { + let program = parse_fragment( + br#"$values = array_values(["a" => 10, "b" => 20]); +echo $values[0] . ":" . $values[1]; +$keys = array_keys(["a" => 10, "b" => 20]); +echo ":" . $keys[0] . ":" . $keys[1]; +echo ":" . count(array_values([])); +$call_keys = call_user_func("array_keys", ["z" => 7]); +echo ":" . $call_keys[0]; +$call_values = call_user_func_array("array_values", [["q" => 8]]); +echo ":" . $call_values[0]; +echo ":"; echo function_exists("array_keys"); +return function_exists("array_values");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:a:b:0:z:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_reverse()` handles PHP key preservation rules. + #[test] + fn execute_program_dispatches_array_reverse_builtin() { + let program = parse_fragment( + br#"$indexed = array_reverse([1, 2, 3]); +echo $indexed[0]; echo $indexed[1]; echo $indexed[2]; echo ":"; +$mixed = array_reverse([2 => "a", "k" => "b", 5 => "c"]); +echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; +$preserved = array_reverse([2 => "a", "k" => "b", 5 => "c"], true); +echo $preserved[5]; echo $preserved["k"]; echo $preserved[2]; echo ":"; +$named = array_reverse(array: ["x", "y"], preserve_keys: true); +echo $named[1]; echo $named[0]; echo ":"; +$call = call_user_func("array_reverse", [4, 5]); +echo $call[0]; echo $call[1]; echo ":"; +$spread = call_user_func_array("array_reverse", [[6, 7]]); +echo $spread[0]; echo $spread[1]; echo ":"; +return function_exists("array_reverse");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "321:cba:cba:yx:54:76:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. + #[test] + fn execute_program_dispatches_array_key_exists_builtin() { + let program = parse_fragment( + br#"$map = ["name" => null, "age" => 30]; +echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; +echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; +echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; +echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; +echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; +echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; echo ":"; +return function_exists("array_key_exists");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:Y:N:Y:Y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval array search builtins use loose comparison and return keys or booleans. + #[test] + fn execute_program_dispatches_array_search_builtins() { + let program = parse_fragment( + br#"echo in_array(2, [1, 2, 3]) ? "Y" : "bad"; +echo ":"; echo in_array(4, [1, 2, 3]) ? "bad" : "N"; +echo ":" . array_search(20, [10, 20, 30]); +echo ":" . array_search("Grace", ["name" => "Grace"]); +echo ":"; echo array_search("x", ["name" => "Grace"]) === false ? "F" : "bad"; +echo ":"; echo call_user_func("in_array", "b", ["a", "b"]) ? "C" : "bad"; +$found = call_user_func_array("array_search", ["v", ["k" => "v"]]); +echo ":" . $found; +echo ":"; echo function_exists("in_array"); +return function_exists("array_search");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:1:name:F:C:k:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. + #[test] + fn execute_program_dispatches_explode_implode_builtins() { + let program = parse_fragment( + br#"$parts = explode(",", "a,b,"); +echo count($parts); echo ":" . $parts[0] . ":" . $parts[1] . ":" . $parts[2]; +echo ":" . implode("|", $parts); +echo ":" . implode(separator: "-", array: ["x", 2, true, null]); +$call_parts = call_user_func("explode", ":", "m:n"); +echo ":" . $call_parts[1]; +echo ":" . call_user_func_array("implode", ["separator" => "/", "array" => ["p", "q"]]); +echo ":"; echo function_exists("explode"); +return function_exists("implode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:a:b::a|b|:x-2-1-:n:p/q:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `str_split()` builds indexed arrays of fixed-width chunks. + #[test] + fn execute_program_dispatches_str_split_builtin() { + let program = parse_fragment( + br#"$letters = str_split("abc"); +echo count($letters) . ":" . $letters[0] . $letters[1] . $letters[2]; echo ":"; +$pairs = str_split(string: "abcd", length: 2); +echo $pairs[0] . "-" . $pairs[1]; echo ":"; +$empty = str_split(""); +echo count($empty); echo ":"; +$call = call_user_func("str_split", "xyz", 2); +echo $call[0] . "-" . $call[1]; echo ":"; +$named = call_user_func_array("str_split", ["string" => "pqrs", "length" => 3]); +echo $named[0] . "-" . $named[1]; echo ":"; +return function_exists("str_split");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:abc:ab-cd:0:xy-z:pqr-s:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `str_pad()` supports PHP left, right, and both-side padding modes. + #[test] + fn execute_program_dispatches_str_pad_builtin() { + let program = parse_fragment( + br#"echo "[" . str_pad("hi", 5) . "]"; echo ":"; +echo "[" . str_pad(string: "hi", length: 5, pad_string: "_", pad_type: 0) . "]"; echo ":"; +echo "[" . str_pad("x", 6, "ab", 2) . "]"; echo ":"; +echo call_user_func("str_pad", "42", 5, "0", 0); echo ":"; +echo call_user_func_array("str_pad", ["string" => "x", "length" => 3, "pad_string" => "."]); echo ":"; +return function_exists("str_pad");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "[hi ]:[___hi]:[abxaba]:00042:x..:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval string replacement builtins support direct, named, and callable dispatch. + #[test] + fn execute_program_dispatches_string_replace_builtins() { + let program = parse_fragment( + br#"echo str_replace("o", "0", "Hello World"); echo ":"; +echo str_replace(search: "aa", replace: "b", subject: "aaaa"); echo ":"; +echo str_replace("", "x", "abc"); echo ":"; +echo str_ireplace("HE", "ye", "Hello he"); echo ":"; +echo call_user_func("str_replace", "l", "L", "hello"); echo ":"; +echo call_user_func_array("str_ireplace", ["search" => "x", "replace" => "Y", "subject" => "xX"]); echo ":"; +echo function_exists("str_replace"); +return function_exists("str_ireplace");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. + #[test] + fn execute_program_dispatches_preg_builtins() { + let program = parse_fragment( + br#"$ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); +echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; +echo preg_match("/xyz/", "id42") . ":"; +echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; +$allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); +echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; +$setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); +echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; +preg_match("/(a)?(b)/", "b", $offsetOne, PREG_OFFSET_CAPTURE); +echo $offsetOne[0][0] . ":" . $offsetOne[0][1] . ":" . $offsetOne[1][0] . ":" . $offsetOne[1][1] . ":" . $offsetOne[2][0] . ":" . $offsetOne[2][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); +echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); +echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOne, PREG_UNMATCHED_AS_NULL); +echo count($nullOne) . ":" . ($nullOne[1] === null ? "n" : "bad") . ":" . $nullOne[2] . ":" . ($nullOne[3] === null ? "n" : "bad") . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOffset, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullOffset[1][0] === null ? "n" : "bad") . ":" . $nullOffset[1][1] . ":" . ($nullOffset[3][0] === null ? "n" : "bad") . ":" . $nullOffset[3][1] . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullAll, PREG_UNMATCHED_AS_NULL); +echo ($nullAll[1][0] === null ? "n" : "bad") . ":" . $nullAll[2][0] . ":" . ($nullAll[3][0] === null ? "n" : "bad") . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullSet, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullSet[0][1][0] === null ? "n" : "bad") . ":" . $nullSet[0][1][1] . ":" . ($nullSet[0][3][0] === null ? "n" : "bad") . ":" . $nullSet[0][3][1] . ":"; +preg_match_all("/(x)(y)/", "abc", $none); +echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; +echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; +function eval_regex_wrap($matches) { return "[" . $matches[0] . "]"; } +echo preg_replace_callback("/[A-Z]/", "eval_regex_wrap", "AB") . ":"; +$limited = preg_split("/,/", "a,b,c", 2); +echo count($limited) . ":" . $limited[0] . ":" . $limited[1] . ":"; +$kept = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY); +echo count($kept) . ":" . $kept[1] . ":"; +echo call_user_func("preg_match", "/x/", "x") . ":"; +$replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "replacement" => "N", "subject" => "a12"]); +echo $replaced . ":"; +$captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); +echo count($captured) . ":" . $captured[1] . ":"; +$splitOffsets = preg_split("/,/", "a,b,c", 2, PREG_SPLIT_OFFSET_CAPTURE); +echo $splitOffsets[0][0] . ":" . $splitOffsets[0][1] . ":" . $splitOffsets[1][0] . ":" . $splitOffsets[1][1] . ":"; +$splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); +echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; +$splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); +echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; +return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. + #[test] + fn execute_program_dispatches_html_entity_builtins() { + let program = parse_fragment( + br#"echo htmlspecialchars("\"Hi\" & 'bye'"); echo ":"; +echo htmlentities(string: "
"); echo ":"; +echo html_entity_decode("<b>hi</b>"); echo ":"; +echo call_user_func("htmlspecialchars", ""); echo ":"; +echo call_user_func_array("html_entity_decode", ["string" => ""q""]); echo ":"; +echo function_exists("htmlspecialchars"); echo function_exists("htmlentities"); +return function_exists("html_entity_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval URL codec builtins dispatch through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_url_codec_builtins() { + let program = parse_fragment( + br#"echo urlencode("a b&=~"); echo ":"; +echo rawurlencode(string: "a b&=~"); echo ":"; +echo urldecode("a+b%26%3D%7E"); echo ":"; +echo rawurldecode("a+b%26%3D%7E"); echo ":"; +echo call_user_func("urlencode", "%zz"); echo ":"; +echo call_user_func_array("rawurldecode", ["string" => "x%2By%zz"]); echo ":"; +echo function_exists("urlencode"); echo function_exists("rawurlencode"); +echo function_exists("urldecode"); +return function_exists("rawurldecode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_ctype_builtins() { + let program = parse_fragment( + br#"echo ctype_alpha("abc") ? "A" : "-"; echo ":"; +echo ctype_digit(text: "123") ? "D" : "-"; echo ":"; +echo ctype_alnum("a1") ? "N" : "-"; echo ":"; +echo ctype_space(" \t\n" . chr(11) . chr(12) . "\r") ? "S" : "-"; echo ":"; +echo ctype_alpha("") ? "bad" : "empty"; echo ":"; +echo call_user_func("ctype_digit", "12x") ? "bad" : "not-digit"; echo ":"; +echo call_user_func_array("ctype_space", ["text" => " x"]) ? "bad" : "not-space"; echo ":"; +echo function_exists("ctype_alpha"); echo function_exists("ctype_digit"); +echo function_exists("ctype_alnum"); +return function_exists("ctype_space");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:D:N:S:empty:not-digit:not-space:111"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. + #[test] + fn execute_program_dispatches_crc32_builtin() { + let program = parse_fragment( + br#"echo crc32(""); echo ":"; +echo crc32(string: "123456789"); echo ":"; +echo call_user_func("crc32", "hello"); echo ":"; +echo call_user_func_array("crc32", ["string" => "The quick brown fox jumps over the lazy dog"]); echo ":"; +return function_exists("crc32");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:3421780262:907060870:1095738169:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `hash_algos()` returns supported hash names through callable dispatch too. + #[test] + fn execute_program_dispatches_hash_algos_builtin() { + let program = parse_fragment( + br#"$algos = hash_algos(); +echo count($algos) . ":" . $algos[0] . ":" . $algos[5] . ":"; +echo in_array("crc32c", $algos) ? "crc" : "bad"; +$call = call_user_func("hash_algos"); +echo ":" . $call[18]; +$spread = call_user_func_array("hash_algos", []); +echo ":" . $spread[27] . ":"; +echo function_exists("hash_algos") ? "exists" : "missing"; +return count($algos);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "28:md2:sha256:crc:whirlpool:joaat:exists"); + assert_eq!(values.get(result), FakeValue::Int(28)); + } + + /// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. + #[test] + fn execute_program_dispatches_hash_digest_builtins() { + let filename = format!("elephc_eval_hash_file_{}.txt", std::process::id()); + let source = format!( + r#"echo md5("abc"); echo ":"; +echo sha1(string: "abc"); echo ":"; +echo hash("sha256", "abc"); echo ":"; +echo hash_hmac(algo: "sha256", data: "data", key: "key"); echo ":"; +echo bin2hex(md5("abc", true)); echo ":"; +echo bin2hex(call_user_func("sha1", "abc", true)); echo ":"; +echo call_user_func_array("hash", ["algo" => "md5", "data" => "abc"]); echo ":"; +echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; +file_put_contents("{filename}", "abc"); +echo hash_file("sha256", "{filename}"); echo ":"; +echo bin2hex(hash_file(algo: "md5", filename: "{filename}", binary: true)); echo ":"; +echo call_user_func_array("hash_file", ["algo" => "md5", "filename" => "{filename}"]); echo ":"; +echo hash_file("sha256", "{filename}.missing") === false ? "missing" : "bad"; echo ":"; +unlink("{filename}"); +echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); +return function_exists("hash_hmac");"#, + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "900150983cd24fb0d6963f7d28e17f72:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "900150983cd24fb0d6963f7d28e17f72:", + "900150983cd24fb0d6963f7d28e17f72:", + "missing:", + "1111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval zero-argument system builtins return native-compatible values. + #[test] + fn execute_program_dispatches_zero_arg_system_builtins() { + let program = parse_fragment( + br#"echo time() > 1000000000 ? "time" : "bad"; echo ":"; +echo phpversion(); echo ":"; +echo sys_get_temp_dir(); echo ":"; +echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; +echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; +echo call_user_func("phpversion"); echo ":"; +echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; +echo call_user_func_array("sys_get_temp_dir", []); echo ":"; +echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); +return function_exists("sys_get_temp_dir");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + format!( + "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", + eval_compiler_php_version(), + eval_compiler_php_version() + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `date()` formats libc local timestamps and `mktime()` builds them. + #[test] + fn execute_program_dispatches_date_mktime_builtins() { + let program = parse_fragment( + br#"$ts = mktime(13, 2, 3, 1, 2, 2024); +echo date("Y-m-d H:i:s", $ts); +echo ":" . date("j-n-G-g-A-a-N-D-M-l-F", $ts); +echo ":" . (date("U", $ts) === strval($ts) ? "U" : "bad"); +echo ":" . call_user_func("date", "Y", $ts); +$named = call_user_func_array("mktime", ["hour" => 0, "minute" => 0, "second" => 0, "month" => 1, "day" => 1, "year" => 2000]); +echo ":" . date(format: "Y", timestamp: $named); +echo ":"; echo function_exists("date"); +return function_exists("mktime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. + #[test] + fn execute_program_dispatches_strtotime_builtin() { + let program = parse_fragment( + br#"$date = strtotime("2024-06-15"); +echo date("Y-m-d H:i:s", $date); +$full = strtotime("2024-06-15 12:30:45"); +echo ":" . date("Y-m-d H:i:s", $full); +$short = strtotime("2024-06-15T12:30"); +echo ":" . date("Y-m-d H:i:s", $short); +echo ":" . (strtotime("2024/06/15") === -1 ? "bad" : "wrong"); +$call = call_user_func("strtotime", "2024-01-02 03:04:05"); +echo ":" . date("Y-m-d H:i:s", $call); +$spread = call_user_func_array("strtotime", ["datetime" => "2024-01-02"]); +echo ":" . date("Y-m-d", $spread) . ":"; +return function_exists("strtotime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. + #[test] + fn execute_program_dispatches_microtime_builtin() { + let program = parse_fragment( + br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; +echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; +echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; +echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; +echo ":"; +return function_exists("microtime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "now:named:call:array:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. + #[test] + fn execute_program_dispatches_realpath_cache_builtins() { + let program = parse_fragment( + br#"$cache = realpath_cache_get(); +echo count($cache) . ":" . realpath_cache_size() . ":"; +$call_cache = call_user_func("realpath_cache_get"); +echo count($call_cache) . ":"; +echo call_user_func_array("realpath_cache_size", []) . ":"; +echo function_exists("realpath_cache_get"); +return function_exists("realpath_cache_size");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:0:0:0:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval stream introspection builtins return native-compatible static lists. + #[test] + fn execute_program_dispatches_stream_introspection_builtins() { + let program = parse_fragment( + br#"$wrappers = stream_get_wrappers(); +$transports = stream_get_transports(); +$filters = stream_get_filters(); +echo count($wrappers) . ":" . $wrappers[0] . ":" . $wrappers[5] . ":"; +echo count($transports) . ":" . $transports[0] . ":" . $transports[8] . ":"; +echo count($filters) . ":" . $filters[2] . ":"; +$call_wrappers = call_user_func("stream_get_wrappers"); +echo $call_wrappers[10] . ":"; +$call_transports = call_user_func_array("stream_get_transports", []); +echo $call_transports[11] . ":"; +$call_filters = call_user_func_array("stream_get_filters", []); +echo $call_filters[13] . ":"; +echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); +return function_exists("stream_get_filters");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. + #[test] + fn execute_program_dispatches_spl_classes_builtin() { + let program = parse_fragment( + br#"$names = spl_classes(); +echo count($names) . ":" . $names[0] . ":" . $names[55] . ":"; +echo (in_array("Exception", $names) ? "exception" : "bad") . ":"; +echo (in_array("SplDoublyLinkedList", $names) ? "list" : "bad") . ":"; +$call = call_user_func("spl_classes"); +echo (in_array("Throwable", $call) ? "call" : "bad") . ":"; +$spread = call_user_func_array("spl_classes", []); +echo (count($spread) === count($names) ? "spread" : "bad") . ":"; +echo function_exists("spl_classes"); +return is_callable("spl_classes");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "61:AppendIterator:Throwable:exception:list:call:spread:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval SPL object identity builtins are stable, unique, and callable. + #[test] + fn execute_program_dispatches_spl_object_identity_builtins() { + let program = parse_fragment( + br#"$a = new KnownClass(); +$b = new KnownClass(); +echo (spl_object_id($a) === spl_object_id($a)) ? "stable" : "drift"; +echo ":"; +echo (spl_object_id($a) !== spl_object_id($b)) ? "unique" : "same"; +echo ":"; +echo (spl_object_hash(object: $a) === spl_object_hash($a)) ? "hash" : "bad"; +echo ":"; +echo (call_user_func("spl_object_id", $a) === spl_object_id($a)) ? "call" : "bad"; +echo ":"; +echo (call_user_func_array("spl_object_hash", ["object" => $b]) === spl_object_hash($b)) ? "array" : "bad"; +echo ":"; +echo function_exists("spl_object_id"); +return function_exists("spl_object_hash");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stable:unique:hash:call:array:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval environment builtins read, write, unset, and dispatch dynamically. + #[test] + fn execute_program_dispatches_environment_builtins() { + let program = parse_fragment( + br#"putenv("ELEPHC_EVAL_ENV_TEST=direct"); +echo getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv(assignment: "ELEPHC_EVAL_ENV_TEST=named"); +echo getenv(name: "ELEPHC_EVAL_ENV_TEST") . ":"; +echo call_user_func("getenv", "ELEPHC_EVAL_ENV_TEST") . ":"; +echo call_user_func_array("putenv", ["assignment" => "ELEPHC_EVAL_ENV_TEST=spread"]) ? "set" : "bad"; +echo ":" . getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv("ELEPHC_EVAL_ENV_TEST"); +echo getenv("ELEPHC_EVAL_ENV_TEST") === "" ? "empty" : "bad"; +echo ":"; echo function_exists("getenv"); +return function_exists("putenv");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval sleep builtins dispatch without delaying focused tests. + #[test] + fn execute_program_dispatches_sleep_builtins() { + let program = parse_fragment( + br#"echo sleep(0) . ":"; +echo sleep(seconds: 0) . ":"; +usleep(0); +echo "u:"; +echo call_user_func("sleep", 0) . ":"; +echo call_user_func_array("usleep", ["microseconds" => 0]) === null ? "null" : "bad"; +echo ":"; echo function_exists("sleep"); +return function_exists("usleep");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:0:u:0:null:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. + #[test] + fn execute_program_dispatches_php_uname_builtin() { + let program = parse_fragment( + br#"echo strlen(php_uname()) > 0 ? "all" : "empty"; echo ":"; +echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; +echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; +echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; +echo strlen(php_uname("r")) > 0 ? "release" : "empty"; echo ":"; +echo strlen(php_uname("v")) > 0 ? "version" : "empty"; echo ":"; +echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; +echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; +return function_exists("php_uname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "all:same:sys:node:release:version:machine:call:spread:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. + #[test] + fn execute_program_dispatches_gethostbyname_builtin() { + let program = parse_fragment( + br#"echo gethostbyname("127.0.0.1") . ":"; +echo gethostbyname(hostname: "not a host") . ":"; +echo call_user_func("gethostbyname", "127.0.0.1") . ":"; +echo call_user_func_array("gethostbyname", ["hostname" => "not a host"]) . ":"; +return function_exists("gethostbyname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "127.0.0.1:not a host:127.0.0.1:not a host:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. + #[test] + fn execute_program_dispatches_gethostname_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostname()) > 0 ? "host" : "empty"; echo ":"; +echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; +return function_exists("gethostname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "host:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. + #[test] + fn execute_program_dispatches_gethostbyaddr_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostbyaddr("127.0.0.1")) > 0 ? "direct" : "empty"; echo ":"; +echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; +echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; +echo strlen(call_user_func("gethostbyaddr", "127.0.0.1")) > 0 ? "call" : "empty"; echo ":"; +echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === false ? "spread" : "bad"; echo ":"; +return function_exists("gethostbyaddr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "direct:named:false:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval protocol and service database lookups dispatch dynamically. + #[test] + fn execute_program_dispatches_protocol_service_builtins() { + let program = parse_fragment( + br#"echo getprotobyname("TCP") . ":"; +echo getprotobynumber(6) . ":"; +echo getprotobyname("no_such_protocol") === false ? "missing-proto" : "bad"; echo ":"; +echo getprotobynumber(999) === false ? "missing-number" : "bad"; echo ":"; +echo getservbyname("www", "tcp") . ":"; +echo getservbyport(80, "tcp") . ":"; +echo getservbyname("no_such_service", "tcp") === false ? "missing-service" : "bad"; echo ":"; +echo getservbyport(80, "no_such_proto") === false ? "missing-port" : "bad"; echo ":"; +echo call_user_func("getprotobyname", "udp") . ":"; +echo call_user_func_array("getprotobynumber", ["protocol" => 17]) . ":"; +echo call_user_func("getservbyname", "https", "tcp") . ":"; +echo call_user_func_array("getservbyport", ["port" => 443, "protocol" => "tcp"]) . ":"; +echo function_exists("getprotobyname"); echo function_exists("getprotobynumber"); echo function_exists("getservbyname"); +return function_exists("getservbyport");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. + #[test] + fn execute_program_dispatches_ip_conversion_builtins() { + let program = parse_fragment( + br#"echo long2ip(3232235777) . ":"; +echo long2ip(ip: 4294967295) . ":"; +echo ip2long("192.168.1.1") . ":"; +echo ip2long(ip: "1.2.3") === false ? "bad-ip" : "bad"; echo ":"; +$packed = inet_pton("1.2.3.4"); +echo bin2hex($packed) . ":"; +echo inet_pton(ip: "nonsense") === false ? "bad-pton" : "bad"; echo ":"; +echo inet_ntop($packed) . ":"; +echo inet_ntop(ip: "xx") === false ? "bad-ntop" : "bad"; echo ":"; +echo call_user_func("long2ip", 2130706433) . ":"; +echo call_user_func_array("ip2long", ["ip" => "0.0.0.0"]) . ":"; +echo function_exists("long2ip"); echo function_exists("ip2long"); +echo function_exists("inet_pton"); +return function_exists("inet_ntop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval path component builtins mirror static basename/dirname edge cases. + #[test] + fn execute_program_dispatches_path_component_builtins() { + let program = parse_fragment( + br#"echo basename("/var/log/syslog.log", ".log") . ":"; +echo basename(path: "/usr///") . ":"; +echo basename("/", "x") === "" ? "root" : "bad"; echo ":"; +echo dirname("/usr/local/bin/tool", 2) . ":"; +echo dirname(path: "/usr///local///bin") . ":"; +echo call_user_func("basename", "foo.tar.gz", ".bz2") . ":"; +echo call_user_func_array("dirname", ["path" => "/usr", "levels" => 3]) . ":"; +echo function_exists("basename"); +return function_exists("dirname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `realpath()` resolves existing paths and returns false for misses. + #[test] + fn execute_program_dispatches_realpath_builtin() { + let program = parse_fragment( + br#"echo realpath(".") !== false ? "resolved" : "bad"; echo ":"; +echo realpath(path: "elephc-eval-missing-path") === false ? "false" : "bad"; echo ":"; +echo call_user_func("realpath", ".") !== false ? "call" : "bad"; echo ":"; +echo call_user_func_array("realpath", ["path" => "elephc-eval-missing-path"]) === false ? "array-false" : "bad"; +echo ":"; +return function_exists("realpath");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "resolved:false:call:array-false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. + #[test] + fn execute_program_dispatches_fnmatch_builtin() { + let program = parse_fragment( + br#"echo fnmatch("*.log", "system.log") ? "match" : "bad"; echo ":"; +echo fnmatch("*.log", "logs/system.log", FNM_PATHNAME) ? "bad" : "path"; echo ":"; +echo fnmatch("*.LOG", "system.log", FNM_CASEFOLD) ? "case" : "bad"; echo ":"; +echo fnmatch("*", ".env", FNM_PERIOD) ? "bad" : "period"; echo ":"; +echo fnmatch("[!abc]oo", "doo") && !fnmatch("[!abc]oo", "boo") ? "class" : "bad"; echo ":"; +echo fnmatch('a\\*b', 'a*b') ? "escape" : "bad"; echo ":"; +echo fnmatch('a\\*b', 'a\\xxb', FNM_NOESCAPE) ? "noescape" : "bad"; echo ":"; +$flags = FNM_PATHNAME | FNM_CASEFOLD; +echo fnmatch("dir/*.TXT", "dir/file.txt", $flags) ? "flags" : "bad"; echo ":"; +echo call_user_func("fnmatch", "*.txt", "report.txt") ? "call" : "bad"; echo ":"; +echo call_user_func_array("fnmatch", ["pattern" => "*.TXT", "filename" => "report.txt", "flags" => FNM_CASEFOLD]) ? "callarray" : "bad"; echo ":"; +echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD"); +return FNM_CASEFOLD;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "match:path:case:period:class:escape:noescape:flags:call:callarray:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_FNM_CASEFOLD)); + } + + /// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. + #[test] + fn execute_program_dispatches_pathinfo_builtin() { + let program = parse_fragment( + br#"$info = pathinfo("/var/log/syslog.log"); +echo $info["dirname"] . "|" . $info["basename"] . "|" . $info["extension"] . "|" . $info["filename"] . ":"; +echo pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":"; +echo pathinfo(".bashrc", PATHINFO_FILENAME) === "" ? "dotfile" : "bad"; echo ":"; +echo pathinfo("file.", PATHINFO_EXTENSION) === "" ? "trail" : "bad"; echo ":"; +echo pathinfo("", PATHINFO_DIRNAME) === "" ? "empty-dir" : "bad"; echo ":"; +$plain = pathinfo("/etc/hosts"); +echo array_key_exists("extension", $plain) ? "bad" : "no-ext"; echo ":"; +echo pathinfo("/a/b.php", PATHINFO_BASENAME | PATHINFO_FILENAME) . ":"; +$call = call_user_func("pathinfo", "foo.txt", PATHINFO_ALL); +echo $call["basename"] . ":"; +echo call_user_func_array("pathinfo", ["path" => "foo.txt", "flags" => 0]) === "" ? "zero" : "bad"; +echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL"); +return PATHINFO_ALL;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); + } + + /// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. + #[test] + fn execute_program_dispatches_filesystem_builtins() { + let filename = format!("elephc_eval_fs_probe_{}.txt", std::process::id()); + let missing = format!("elephc_eval_fs_missing_{}.txt", std::process::id()); + let source = format!( + r#"echo file_put_contents("{filename}", "hello") . ":"; +echo file_get_contents("{filename}") . ":"; +echo file_exists("{filename}") ? "exists" : "missing"; echo ":"; +echo is_file(filename: "{filename}") ? "file" : "bad"; echo ":"; +echo is_dir(".") ? "dir" : "bad"; echo ":"; +echo is_readable("{filename}") ? "readable" : "bad"; echo ":"; +echo is_writable("{filename}") ? "writable" : "bad"; echo ":"; +echo is_writeable("{filename}") ? "writeable" : "bad"; echo ":"; +echo filesize("{filename}") . ":"; +echo file_get_contents("{missing}") === false ? "missing-false" : "bad"; echo ":"; +echo call_user_func("file_exists", "{filename}") ? "call-exists" : "bad"; echo ":"; +echo call_user_func_array("filesize", ["filename" => "{filename}"]) . ":"; +echo unlink("{filename}") ? "unlinked" : "bad"; echo ":"; +echo file_exists("{filename}") ? "bad" : "gone"; echo ":"; +echo function_exists("file_get_contents"); echo function_exists("file_put_contents"); +echo function_exists("file_exists"); echo function_exists("is_file"); echo function_exists("is_dir"); +echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); +echo function_exists("filesize"); +return function_exists("unlink");"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + assert_eq!( + values.output, + "5:hello:exists:file:dir:readable:writable:writeable:5:missing-false:call-exists:5:unlinked:gone:111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval disk-space builtins query local filesystem capacity and dispatch dynamically. + #[test] + fn execute_program_dispatches_disk_space_builtins() { + let program = parse_fragment( + br#"echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; +echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; +echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; +echo disk_free_space("no/such/path/elephc-eval") === 0.0 ? "missing" : "bad"; echo ":"; +echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; +echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; echo ":"; +echo function_exists("disk_free_space"); +return function_exists("disk_total_space");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "free:total:ordered:missing:call:spread:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval stat metadata builtins expose scalar file metadata and link probes. + #[test] + fn execute_program_dispatches_stat_metadata_builtins() { + let filename = format!("elephc_eval_stat_probe_{}.txt", std::process::id()); + let missing = format!("elephc_eval_stat_missing_{}.txt", std::process::id()); + let link = format!("elephc_eval_stat_link_{}.txt", std::process::id()); + let source = format!( + r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; +echo fileatime("{filename}") > 0 ? "atime" : "bad"; echo ":"; +echo filectime("{filename}") > 0 ? "ctime" : "bad"; echo ":"; +echo fileperms("{filename}") > 0 ? "perms" : "bad"; echo ":"; +echo fileowner("{filename}") >= 0 ? "owner" : "bad"; echo ":"; +echo filegroup("{filename}") >= 0 ? "group" : "bad"; echo ":"; +echo fileinode("{filename}") > 0 ? "inode" : "bad"; echo ":"; +echo filetype("{filename}") . ":"; +echo filetype(".") . ":"; +echo filetype("{link}") . ":"; +echo is_executable("{filename}") ? "bad" : "noexec"; echo ":"; +echo is_link("{link}") ? "link" : "bad"; echo ":"; +echo fileatime("{missing}") === false ? "missing-atime" : "bad"; echo ":"; +echo filectime("{missing}") === false ? "missing-ctime" : "bad"; echo ":"; +echo fileperms("{missing}") === false ? "missing-perms" : "bad"; echo ":"; +echo fileowner("{missing}") === false ? "missing-owner" : "bad"; echo ":"; +echo filegroup("{missing}") === false ? "missing-group" : "bad"; echo ":"; +echo fileinode("{missing}") === false ? "missing-inode" : "bad"; echo ":"; +echo filetype("{missing}") === false ? "missing-type" : "bad"; echo ":"; +echo filemtime("{missing}") === 0 ? "missing-mtime" : "bad"; echo ":"; +echo call_user_func("filetype", "{filename}") . ":"; +echo call_user_func_array("fileinode", ["filename" => "{filename}"]) > 0 ? "callinode" : "bad"; echo ":"; +echo function_exists("filemtime"); echo function_exists("fileatime"); +echo function_exists("filectime"); echo function_exists("fileperms"); +echo function_exists("fileowner"); echo function_exists("filegroup"); +echo function_exists("fileinode"); echo function_exists("filetype"); +echo function_exists("is_executable"); echo function_exists("is_link"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "mtime:atime:ctime:perms:owner:group:inode:file:dir:link:noexec:link:missing-atime:missing-ctime:missing-perms:missing-owner:missing-group:missing-inode:missing-type:missing-mtime:file:callinode:1111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. + #[test] + fn execute_program_dispatches_stat_array_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_stat_array_{pid}.txt"); + let link = format!("elephc_eval_lstat_array_{pid}.txt"); + let missing = format!("elephc_eval_stat_array_missing_{pid}.txt"); + let source = format!( + r#"$stat = stat("{filename}"); +$lstat = lstat("{link}"); +echo $stat["size"] === 5 && $stat[7] === $stat["size"] ? "stat" : "bad"; echo ":"; +echo ($stat["mode"] & 61440) === 32768 ? "mode" : "bad"; echo ":"; +echo ($lstat["mode"] & 61440) === 40960 ? "lstat" : "bad"; echo ":"; +echo stat("{missing}") === false && lstat("{missing}") === false ? "missing" : "bad"; echo ":"; +$call = call_user_func("stat", "{filename}"); +echo $call["mtime"] === filemtime("{filename}") ? "callstat" : "bad"; echo ":"; +$call_lstat = call_user_func_array("lstat", ["filename" => "{link}"]); +echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; +echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stat"); echo function_exists("lstat"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat array fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval local path operation builtins mutate filesystem state. + #[test] + fn execute_program_dispatches_path_operation_builtins() { + let pid = std::process::id(); + let dir = format!("elephc_eval_ops_dir_{pid}"); + let call_dir = format!("elephc_eval_ops_call_dir_{pid}"); + let src = format!("elephc_eval_ops_src_{pid}.txt"); + let copy = format!("elephc_eval_ops_copy_{pid}.txt"); + let moved = format!("elephc_eval_ops_moved_{pid}.txt"); + let symlink = format!("elephc_eval_ops_symlink_{pid}.txt"); + let hardlink = format!("elephc_eval_ops_hardlink_{pid}.txt"); + let source = format!( + r#"file_put_contents("{src}", "hello"); +echo mkdir("{dir}") ? "mkdir" : "bad"; echo ":"; +echo is_dir("{dir}") ? "dir" : "bad"; echo ":"; +echo copy("{src}", "{copy}") && file_get_contents("{copy}") === "hello" ? "copy" : "bad"; echo ":"; +echo rename("{copy}", "{moved}") && file_exists("{moved}") && !file_exists("{copy}") ? "rename" : "bad"; echo ":"; +echo symlink("{src}", "{symlink}") ? "symlink" : "bad"; echo ":"; +echo readlink("{symlink}") === "{src}" ? "readlink" : "bad"; echo ":"; +echo linkinfo("{symlink}") >= 0 ? "linkinfo" : "bad"; echo ":"; +echo readlink("{src}") === false ? "readlink-false" : "bad"; echo ":"; +echo linkinfo("{missing}") === -1 ? "linkinfo-missing" : "bad"; echo ":"; +echo link("{src}", "{hardlink}") && file_get_contents("{hardlink}") === "hello" ? "hardlink" : "bad"; echo ":"; +echo clearstatcache() === null ? "cache" : "bad"; echo ":"; +echo unlink("{symlink}") && unlink("{hardlink}") && unlink("{moved}") && unlink("{src}") && rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo call_user_func("mkdir", "{call_dir}") ? "callmkdir" : "bad"; echo ":"; +echo call_user_func_array("rmdir", ["directory" => "{call_dir}"]) ? "callrmdir" : "bad"; echo ":"; +echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exists("copy"); +echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); +echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); +return true;"#, + missing = format!("elephc_eval_ops_missing_{pid}.txt"), + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + assert_eq!( + values.output, + "mkdir:dir:copy:rename:symlink:readlink:linkinfo:readlink-false:linkinfo-missing:hardlink:cache:cleanup:callmkdir:callrmdir:111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. + #[test] + fn execute_program_dispatches_file_listing_builtins() { + let pid = std::process::id(); + let lines = format!("elephc_eval_listing_lines_{pid}.txt"); + let empty = format!("elephc_eval_listing_empty_{pid}.txt"); + let missing = format!("elephc_eval_listing_missing_{pid}.txt"); + let dir = format!("elephc_eval_listing_dir_{pid}"); + let source = format!( + r#"file_put_contents("{lines}", "one\ntwo"); +file_put_contents("{empty}", ""); +$lines = file("{lines}"); +echo count($lines) . ":"; +echo $lines[0] === "one\n" ? "line0" : "bad"; echo ":"; +echo $lines[1] === "two" ? "line1" : "bad"; echo ":"; +echo "["; +$bytes = readfile(filename: "{empty}"); +echo "]" . $bytes . ":"; +echo readfile("{missing}") === false ? "missing-readfile" : "bad"; echo ":"; +echo count(file("{missing}")) === 0 ? "missing-file" : "bad"; echo ":"; +mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.txt", "b"); +$scan = scandir(directory: "{dir}"); +echo count($scan) . ":"; +echo in_array(".", $scan) && in_array("..", $scan) && in_array("a.txt", $scan) && in_array("b.txt", $scan) ? "scan" : "bad"; echo ":"; +$call_lines = call_user_func("file", "{lines}"); +echo $call_lines[0] === "one\n" ? "callfile" : "bad"; echo ":"; +$call_scan = call_user_func_array("scandir", ["directory" => "{dir}"]); +echo count($call_scan) . ":"; +echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") && unlink("{lines}") && unlink("{empty}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "2:line0:line1:[]0:missing-readfile:missing-file:4:scan:callfile:4:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `glob()` expands local patterns and dispatches dynamically. + #[test] + fn execute_program_dispatches_glob_builtin() { + let pid = std::process::id(); + let dir = format!("elephc_eval_glob_dir_{pid}"); + let source = format!( + r#"mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.log", "b"); +file_put_contents("{dir}/c.txt", "c"); +file_put_contents("{dir}/.hidden.txt", "h"); +$matches = glob("{dir}/*.txt"); +echo count($matches) === 2 && basename($matches[0]) === "a.txt" && basename($matches[1]) === "c.txt" ? "glob" : "bad"; echo ":"; +echo count(glob("{dir}/*.none")) === 0 ? "empty" : "bad"; echo ":"; +$literal = glob("{dir}/a.txt"); +echo count($literal) === 1 && $literal[0] === "{dir}/a.txt" ? "literal" : "bad"; echo ":"; +$all = glob("{dir}/*"); +echo in_array("{dir}/.hidden.txt", $all) ? "bad" : "hidden"; echo ":"; +$call = call_user_func("glob", "{dir}/*.log"); +echo count($call) === 1 && basename($call[0]) === "b.log" ? "callglob" : "bad"; echo ":"; +$call_array = call_user_func_array("glob", ["pattern" => "{dir}/*.txt"]); +echo count($call_array) === 2 ? "callarray" : "bad"; echo ":"; +unlink("{dir}/.hidden.txt"); +unlink("{dir}/c.txt"); +unlink("{dir}/b.log"); +unlink("{dir}/a.txt"); +echo rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("glob"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "glob:empty:literal:hidden:callglob:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. + #[test] + fn execute_program_dispatches_file_modify_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_modify_{pid}.txt"); + let missing = format!("elephc_eval_modify_missing_{pid}.txt"); + let prefix = format!("evm{pid}_"); + let call_prefix = format!("evc{pid}_"); + let source = format!( + r#"file_put_contents("{filename}", "x"); +echo chmod(filename: "{filename}", permissions: 384) ? "chmod" : "bad"; echo ":"; +echo (fileperms("{filename}") & 511) === 384 ? "mode" : "bad"; echo ":"; +echo chmod("{missing}", 384) ? "bad" : "chmod-false"; echo ":"; +$tmp = tempnam(directory: ".", prefix: "{prefix}"); +echo file_exists($tmp) && str_starts_with(basename($tmp), "{prefix}") ? "tempnam" : "bad"; echo ":"; +unlink($tmp); +$previous = umask(mask: 18); +$set = umask($previous); +echo $set === 18 ? "umask" : "bad"; echo ":"; +$before = umask(18); +$probe = umask(); +$restore = umask($before); +echo $probe === 18 && $restore === 18 ? "probe" : "bad"; echo ":"; +echo call_user_func("chmod", "{filename}", 420) ? "callchmod" : "bad"; echo ":"; +$call_tmp = call_user_func_array("tempnam", ["directory" => ".", "prefix" => "{call_prefix}"]); +echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "{call_prefix}") ? "calltempnam" : "bad"; echo ":"; +unlink($call_tmp); +echo unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + assert_eq!( + values.output, + "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. + #[test] + fn execute_program_dispatches_touch_builtin() { + let pid = std::process::id(); + let created = format!("elephc_eval_touch_created_{pid}.txt"); + let stamped = format!("elephc_eval_touch_stamped_{pid}.txt"); + let missing = format!("elephc_eval_touch_missing_{pid}/x.txt"); + let source = format!( + r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; +file_put_contents("{stamped}", "x"); +echo touch("{stamped}", 1000000000) ? "mtime" : "bad"; echo ":"; +echo filemtime("{stamped}") === 1000000000 ? "readmtime" : "bad"; echo ":"; +echo touch("{stamped}", 1000000001, null) && filemtime("{stamped}") === 1000000001 ? "nullatime" : "bad"; echo ":"; +echo touch("{stamped}", 1000000002, 1000000003) && filemtime("{stamped}") === 1000000002 ? "both" : "bad"; echo ":"; +echo touch("{missing}") ? "bad" : "touch-false"; echo ":"; +echo call_user_func("touch", "{created}", 1000000004) ? "calltouch" : "bad"; echo ":"; +echo call_user_func_array("touch", ["filename" => "{stamped}", "mtime" => 1000000005]) ? "callarray" : "bad"; echo ":"; +echo unlink("{created}") && unlink("{stamped}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("touch"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + assert_eq!( + values.output, + "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval ASCII string case builtins work directly and through callable dispatch. + #[test] + fn execute_program_dispatches_string_case_builtins() { + let program = parse_fragment( + br#"echo strtoupper("Hello World"); echo ":"; +echo strtolower("LOUD"); echo ":"; +echo ucfirst("eval"); echo ":"; +echo lcfirst("LOUD"); echo ":"; +echo call_user_func("strtoupper", "xy"); echo ":"; +echo call_user_func_array("strtolower", ["ZZ"]); echo ":"; +echo call_user_func("ucfirst", "case"); echo ":"; +echo call_user_func_array("lcfirst", ["CASE"]); +echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower"); echo function_exists("ucfirst"); +return function_exists("lcfirst");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `ucwords()` capitalizes word starts directly and by callable dispatch. + #[test] + fn execute_program_dispatches_ucwords_builtin() { + let program = parse_fragment( + br#"echo ucwords("hello world"); echo ":"; +echo ucwords(string: "hello-world", separators: "-"); echo ":"; +echo ucwords("hello\tworld"); echo ":"; +echo call_user_func("ucwords", "a b"); echo ":"; +echo call_user_func_array("ucwords", ["string" => "a-b", "separators" => "-"]); echo ":"; +return function_exists("ucwords");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:Hello-World:Hello\tWorld:A B:A-B:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. + #[test] + fn execute_program_dispatches_wordwrap_builtin() { + let program = parse_fragment( + br#"echo wordwrap("The quick brown fox", 10, "|"); echo ":"; +echo wordwrap(string: "A verylongword here", width: 8, break: "|"); echo ":"; +echo wordwrap("abcdefghij", 4, "|", true); echo ":"; +echo wordwrap("preserve\nnewlines here ok", 10, "|"); echo ":"; +echo call_user_func("wordwrap", "aaa bbb ccc", 3, "
"); echo ":"; +echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); +echo ":"; +return function_exists("wordwrap");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. + #[test] + fn execute_program_dispatches_str_contains_builtin() { + let program = parse_fragment( + br#"echo str_contains("Hello World", "World") ? "Y" : "N"; +echo str_contains("Hello", "z") ? "bad" : ":N"; +echo str_contains("Hello", "") ? ":E" : "bad"; +echo call_user_func("str_contains", "abc", "b") ? ":C" : "bad"; +echo call_user_func_array("str_contains", ["abc", "x"]) ? "bad" : ":A"; +return function_exists("str_contains");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:E:C:A"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval string position builtins return byte offsets or PHP false. + #[test] + fn execute_program_dispatches_string_position_builtins() { + let program = parse_fragment( + br#"echo strpos("banana", "na"); +echo ":" . strrpos("banana", "na"); +echo ":"; echo strpos("abc", "z") === false ? "F" : "bad"; +echo ":" . strpos("abc", ""); +echo ":" . strrpos("abc", ""); +echo ":" . call_user_func("strpos", "abc", "b"); +echo ":" . call_user_func_array("strrpos", ["ababa", "ba"]); +echo ":"; echo function_exists("strpos"); +return function_exists("strrpos");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:4:F:0:3:1:3:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `strstr()` returns suffixes, prefixes, or false for misses. + #[test] + fn execute_program_dispatches_strstr_builtin() { + let program = parse_fragment( + br#"echo strstr("user@example.com", "@"); echo ":"; +echo strstr(haystack: "hello world", needle: "lo", before_needle: true); echo ":"; +echo strstr("hello", "x") === false ? "F" : "bad"; echo ":"; +echo strstr("hello", ""); echo ":"; +echo call_user_func("strstr", "abcabc", "bc"); echo ":"; +echo call_user_func_array("strstr", ["haystack" => "abcabc", "needle" => "bc", "before_needle" => true]); echo ":"; +return function_exists("strstr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "@example.com:hel:F:hello:bcabc:a:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval prefix/suffix string search builtins use byte-string semantics. + #[test] + fn execute_program_dispatches_string_boundary_builtins() { + let program = parse_fragment( + br#"echo str_starts_with("Hello World", "Hello") ? "S" : "bad"; +echo str_starts_with("Hello", "World") ? "bad" : ":s"; +echo str_starts_with("Hello", "") ? ":se" : "bad"; +echo str_ends_with("Hello World", "World") ? ":E" : "bad"; +echo str_ends_with("Hello", "World") ? "bad" : ":e"; +echo str_ends_with("Hello", "") ? ":ee" : "bad"; +echo call_user_func("str_starts_with", "abc", "a") ? ":CS" : "bad"; +echo call_user_func_array("str_ends_with", ["abc", "c"]) ? ":CE" : "bad"; +echo ":"; echo function_exists("str_starts_with"); +return function_exists("str_ends_with");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "S:s:se:E:e:ee:CS:CE:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval string comparison builtins return PHP-compatible scalar results. + #[test] + fn execute_program_dispatches_string_compare_builtins() { + let program = parse_fragment( + br#"echo strcmp("abc", "abc"); +echo ":"; echo strcmp("abc", "abd") < 0 ? "lt" : "bad"; +echo ":"; echo strcasecmp("Hello", "hello"); +echo ":"; echo call_user_func("strcmp", "b", "a") > 0 ? "gt" : "bad"; +echo ":"; echo call_user_func_array("strcasecmp", ["A", "a"]) === 0 ? "ci" : "bad"; +echo ":"; echo hash_equals("abc", "abc") ? "heq" : "bad"; +echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; +echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; +echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); +return function_exists("hash_equals");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:lt:0:gt:ci:heq:hlen:hneq:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval trim-like builtins strip default and explicit byte masks. + #[test] + fn execute_program_dispatches_trim_like_builtins() { + let program = parse_fragment( + br#"echo "[" . trim(" hello ") . "]"; +echo ":[" . ltrim(" left") . "]"; +echo ":[" . rtrim("right ") . "]"; +echo ":[" . chop("tail... ", " .") . "]"; +echo ":[" . trim("**boxed**", "*") . "]"; +echo ":[" . call_user_func("trim", " cuf ") . "]"; +echo ":[" . call_user_func_array("ltrim", ["0007", "0"]) . "]"; +echo ":"; echo function_exists("trim"); echo function_exists("ltrim"); echo function_exists("rtrim"); +return function_exists("chop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "[hello]:[left]:[right]:[tail]:[boxed]:[cuf]:[7]:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. + #[test] + fn execute_program_dispatches_type_predicate_builtins() { + let program = parse_fragment( + br#"echo is_int(1); echo is_integer(1); echo is_long(1); +echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); +echo is_string("x"); echo is_bool(false); echo is_null(null); +echo is_array([1]); echo is_array(["a" => 1]); +echo is_iterable([1]); echo is_iterable(["a" => 1]); +echo is_iterable(1) ? "bad" : "T"; +echo is_array(1) ? "bad" : "ok"; +echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); +echo is_numeric("-5"); echo is_numeric("3.14"); +echo is_numeric("abc") ? "bad" : "N"; +echo is_numeric(true) ? "bad" : "B"; +echo is_resource(1) ? "bad" : "R"; +echo is_object($object) ? "O" : "bad"; +echo is_object([1]) ? "bad" : "o"; +echo is_nan(fdiv(0, 0)) ? "N" : "bad"; +echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; +echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; +echo is_finite(42) ? "F" : "bad"; +echo is_finite(fdiv(1, 0)) ? "bad" : "f"; +echo ":"; echo call_user_func("is_string", "x"); +echo call_user_func_array("is_array", [[1]]); +echo call_user_func("is_numeric", "12"); +echo call_user_func("is_iterable", [1]); +echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; +echo call_user_func("is_object", $object) ? "O" : "bad"; +echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; +echo function_exists("is_numeric"); echo function_exists("is_object"); echo function_exists("is_resource"); +echo function_exists("is_double"); echo function_exists("is_nan"); echo function_exists("is_finite"); +echo function_exists("is_iterable"); +return function_exists("is_infinite");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1111111111111Tok11111NBROoNIiFf:1111tOo1111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. + #[test] + fn execute_program_dispatches_is_resource_true() { + let program = parse_fragment( + br#"echo is_resource($handle) ? "R" : "bad"; +echo ":" . gettype($handle); +return call_user_func("is_resource", $handle);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "R:resource"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval resource introspection builtins expose stream type and one-based id. + #[test] + fn execute_program_dispatches_resource_introspection_builtins() { + let program = parse_fragment( + br#"echo get_resource_type($handle); +echo ":" . get_resource_id($handle); +echo ":" . call_user_func("get_resource_type", $handle); +echo ":" . call_user_func_array("get_resource_id", ["resource" => $handle]); +echo ":" . function_exists("get_resource_type"); +return function_exists("get_resource_id");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stream:7:stream:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval cast builtins return boxed scalar cells directly and by callable. + #[test] + fn execute_program_dispatches_cast_builtins() { + let program = parse_fragment( + br#"echo intval("42"); echo ":"; +echo floatval("3.5"); echo ":"; +echo strval(12); echo ":"; +echo boolval("0") ? "bad" : "false"; +echo ":"; echo call_user_func("strval", 7); +return call_user_func_array("intval", ["9"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "42:3.5:12:false:7"); + assert_eq!(values.get(result), FakeValue::Int(9)); + } + + /// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. + #[test] + fn execute_program_dispatches_settype_builtin() { + let program = parse_fragment( + br#"$x = 42; +echo settype($x, "string") ? gettype($x) . ":" . $x : "bad"; +echo ":"; +$y = "0"; +echo settype(type: "bool", var: $y) ? gettype($y) . ":" . ($y ? "true" : "false") : "bad"; +echo ":"; +echo settype($missing, "integer") ? gettype($missing) . ":" . $missing : "bad"; +echo ":"; +$z = 3.8; +echo call_user_func("settype", $z, "integer") ? gettype($z) . ":" . $z : "bad"; +echo ":"; +return function_exists("settype");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "string:42:boolean:false:integer:0:double:3.8:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + ["settype(): Argument #1 ($var) must be passed by reference, value given"] + ); + } + + /// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. + #[test] + fn execute_program_dispatches_gettype_builtin() { + let program = parse_fragment( + br#"echo gettype(1); echo ":"; +echo gettype(1.5); echo ":"; +echo gettype("x"); echo ":"; +echo gettype(false); echo ":"; +echo gettype(null); echo ":"; +echo gettype([1]); echo ":"; +echo gettype(["a" => 1]); echo ":"; +echo call_user_func("gettype", true); echo ":"; +echo call_user_func_array("gettype", [null]); +return function_exists("gettype");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "integer:double:string:boolean:NULL:array:array:boolean:NULL" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `get_class()` reads object class names directly and by callable. + #[test] + fn execute_program_dispatches_get_class_builtin() { + let program = parse_fragment( + br#"echo get_class($object); echo ":"; +echo call_user_func("get_class", $object); echo ":"; +return call_user_func_array("get_class", ["object" => $object]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stdClass:stdClass:"); + assert_eq!( + values.get(result), + FakeValue::String("stdClass".to_string()) + ); + } + + /// Verifies eval `get_parent_class()` reads object and class-string parents by callable. + #[test] + fn execute_program_dispatches_get_parent_class_builtin() { + let program = parse_fragment( + br#"echo get_parent_class($object); echo ":"; +echo get_parent_class("ChildClass"); echo ":"; +echo call_user_func("get_parent_class", $object); echo ":"; +return call_user_func_array("get_parent_class", ["object_or_class" => "ChildClass"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ParentClass:ParentClass:ParentClass:"); + assert_eq!( + values.get(result), + FakeValue::String("ParentClass".to_string()) + ); + } + + /// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. + #[test] + fn execute_program_dispatches_abs_builtin() { + let program = parse_fragment( + br#"echo abs(-5); echo ":"; +echo abs(-2.5); echo ":"; +echo gettype(abs(-2.5)); echo ":"; +echo call_user_func("abs", -7); echo ":"; +echo call_user_func_array("abs", [-9]); +return function_exists("abs");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:2.5:double:7:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `floor()` and `ceil()` dispatch as double-returning math builtins. + #[test] + fn execute_program_dispatches_floor_and_ceil_builtins() { + let program = parse_fragment( + br#"echo floor(3.7); echo ":"; +echo gettype(floor(3)); echo ":"; +echo ceil(3.2); echo ":"; +echo gettype(ceil(3)); echo ":"; +echo call_user_func("floor", 4.9); echo ":"; +echo call_user_func_array("ceil", [4.1]); +echo ":"; echo function_exists("floor"); +return function_exists("ceil");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:double:4:double:4:5:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `fdiv()` and `fmod()` dispatch as floating-point binary builtins. + #[test] + fn execute_program_dispatches_float_binary_builtins() { + let program = parse_fragment( + br#"echo round(fdiv(10, 4), 2); echo ":"; +echo gettype(fdiv(10, 4)); echo ":"; +echo round(fmod(10.5, 3.2), 1); echo ":"; +echo round(call_user_func("fdiv", 9, 2), 1); echo ":"; +echo round(call_user_func_array("fmod", [10.5, 3.2]), 1); echo ":"; +echo function_exists("fdiv"); +return function_exists("fmod");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "2.5:double:0.9:4.5:0.9:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval extended scalar math builtins support direct, named, callable, and probe paths. + #[test] + fn execute_program_dispatches_extended_math_builtins() { + let program = parse_fragment( + br#"echo sin(0); echo ":"; +echo cos(0); echo ":"; +echo tan(0); echo ":"; +echo round(asin(1), 2); echo ":"; +echo acos(1); echo ":"; +echo round(atan(1), 2); echo ":"; +echo sinh(0); echo ":"; +echo cosh(0); echo ":"; +echo tanh(0); echo ":"; +echo log2(8); echo ":"; +echo log10(100); echo ":"; +echo exp(0); echo ":"; +echo round(deg2rad(180), 2); echo ":"; +echo round(rad2deg(pi()), 0); echo ":"; +echo log(num: 8, base: 2); echo ":"; +echo atan2(y: 0, x: 1); echo ":"; +echo hypot(3, 4); echo ":"; +echo intdiv(7, 2); echo ":"; +echo round(call_user_func("sin", pi() / 2), 0); echo ":"; +echo call_user_func_array("intdiv", ["num1" => 9, "num2" => 2]); echo ":"; +echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); +return function_exists("hypot");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. + #[test] + fn execute_program_dispatches_pow_builtin() { + let program = parse_fragment( + br#"echo pow(2, 3); echo ":"; +echo gettype(pow(2, 3)); echo ":"; +echo call_user_func("pow", 2, 5); echo ":"; +echo call_user_func_array("pow", [3, 3]); +return function_exists("pow");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:double:32:27"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `round()` supports default and explicit precision through callable paths. + #[test] + fn execute_program_dispatches_round_builtin() { + let program = parse_fragment( + br#"echo round(3.5); echo ":"; +echo round(3.14159, 2); echo ":"; +echo gettype(round(3)); echo ":"; +echo call_user_func("round", 2.5); echo ":"; +echo call_user_func_array("round", [1.55, 1]); +return function_exists("round");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:3.14:double:3:1.6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `number_format()` groups and rounds numbers through callable paths. + #[test] + fn execute_program_dispatches_number_format_builtin() { + let program = parse_fragment( + br#"echo number_format(1234567); echo ":"; +echo number_format(1234.5678, 2); echo ":"; +echo number_format(num: 1234567.89, decimals: 2, decimal_separator: ",", thousands_separator: "."); echo ":"; +echo number_format(1234567.89, 2, ".", ""); echo ":"; +echo call_user_func("number_format", -1234.5, 1); echo ":"; +echo call_user_func_array("number_format", ["num" => 1234, "decimals" => 0, "decimal_separator" => ".", "thousands_separator" => " "]); echo ":"; +return function_exists("number_format");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval printf-family builtins format, print, and dispatch through callables. + #[test] + fn execute_program_dispatches_printf_family_builtins() { + let program = parse_fragment( + br#"echo sprintf("Hello %s", "World"); echo ":"; +echo sprintf("%05d", 42); echo ":"; +echo sprintf("%.2f", 3.14159); echo ":"; +echo sprintf("%-6s|", "hi"); echo ":"; +$printed = printf("%s=%d", "n", 42); +echo ":" . $printed . ":"; +echo vsprintf("%s/%d/%.1f", ["age", 42, 3]); echo ":"; +$vprinted = vprintf("%s-%d", ["v", 7]); +echo ":" . $vprinted . ":"; +echo call_user_func("sprintf", "%+d", 42); echo ":"; +echo call_user_func_array("vsprintf", ["format" => "%s", "values" => ["spread"]]); echo ":"; +echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); +return is_callable("vprintf");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `sscanf()` returns indexed string matches through callable paths. + #[test] + fn execute_program_dispatches_sscanf_builtin() { + let program = parse_fragment( + br#"$result = sscanf("John 1.5 30", "%s %f %d"); +echo $result[0] . ":" . $result[1] . ":" . $result[2] . ":"; +$named = sscanf(string: "Age: -25", format: "Age: %d"); +echo $named[0] . ":"; +$call = call_user_func("sscanf", "-2.5e3", "%f"); +echo $call[0] . ":"; +$spread = call_user_func_array("sscanf", ["string" => "ok %", "format" => "%s %%"]); +echo $spread[0] . ":"; +return function_exists("sscanf");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "John:1.5:30:-25:-2.5e3:ok:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `min()` and `max()` select numeric values directly and by callable. + #[test] + fn execute_program_dispatches_min_max_builtins() { + let program = parse_fragment( + br#"echo min(3, 1, 2); echo ":"; +echo max(1, 3, 2); echo ":"; +echo min(2.5, 1.5); echo ":"; +echo max(1.5, 2.5); echo ":"; +echo call_user_func("min", 9, 4, 7); echo ":"; +echo call_user_func_array("max", [4, 8, 6]); echo ":"; +echo function_exists("min"); +return function_exists("max");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:1.5:2.5:4:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `clamp()` selects numeric values through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_clamp_builtin() { + let program = parse_fragment( + br#"echo clamp(5, 0, 10); echo ":"; +echo clamp(15, 0, 10); echo ":"; +echo clamp(-5, 0, 10); echo ":"; +echo clamp(2.75, 1.5, 2.5); echo ":"; +echo clamp(value: 8, min: 0, max: 5); echo ":"; +echo call_user_func("clamp", -1, 0, 10); echo ":"; +echo call_user_func_array("clamp", ["value" => 9, "min" => 0, "max" => 7]); echo ":"; +echo function_exists("clamp"); +return is_callable("clamp");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:10:0:2.5:5:0:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. + #[test] + fn execute_program_rejects_clamp_invalid_bounds() { + let program = parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("invalid clamp bounds should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } + + /// Verifies eval `pi()` returns a double constant directly and through callable paths. + #[test] + fn execute_program_dispatches_pi_builtin() { + let program = parse_fragment( + br#"echo round(pi(), 2); echo ":"; +echo gettype(pi()); echo ":"; +echo round(call_user_func("pi"), 3); echo ":"; +echo round(call_user_func_array("pi", []), 4); echo ":"; +return function_exists("pi");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3.14:double:3.142:3.1416:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. + #[test] + fn execute_program_dispatches_sqrt_builtin() { + let program = parse_fragment( + br#"echo sqrt(16); echo ":"; +echo gettype(sqrt(9)); echo ":"; +echo call_user_func("sqrt", 25); echo ":"; +echo call_user_func_array("sqrt", [36]); +return function_exists("sqrt");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:double:5:6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `strrev()` dispatches through direct and callable paths. + #[test] + fn execute_program_dispatches_strrev_builtin() { + let program = parse_fragment( + br#"echo strrev("Hello"); echo ":"; +echo strrev(123); echo ":"; +echo call_user_func("strrev", "ABC"); echo ":"; +echo call_user_func_array("strrev", ["def"]); echo ":"; +return function_exists("strrev");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "olleH:321:CBA:fed:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `chr()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_chr_builtin() { + let program = parse_fragment( + br#"echo chr(65); echo ":"; +echo bin2hex(chr(codepoint: 256)); echo ":"; +echo bin2hex(call_user_func("chr", 257)); echo ":"; +echo call_user_func_array("chr", ["codepoint" => 321]); echo ":"; +return function_exists("chr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:00:01:A:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_str_repeat_builtin() { + let program = parse_fragment( + br#"echo str_repeat("ha", 3); echo ":"; +echo strlen(str_repeat(string: "x", times: 0)); echo ":"; +echo call_user_func("str_repeat", "ab", 2); echo ":"; +echo call_user_func_array("str_repeat", ["string" => "z", "times" => 3]); echo ":"; +return function_exists("str_repeat");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hahaha:0:abab:zzz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `substr()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_substr_builtin() { + let program = parse_fragment( + br#"echo substr("abcdef", 2); echo ":"; +echo substr(string: "abcdef", offset: 1, length: -1); echo ":"; +echo substr("abcdef", -2); echo ":"; +echo call_user_func("substr", "abcdef", 2, -2); echo ":"; +echo call_user_func_array("substr", ["string" => "abcdef", "offset" => -4, "length" => 2]); echo ":"; +return function_exists("substr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "cdef:bcde:ef:cd:cd:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `substr_replace()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_substr_replace_builtin() { + let program = parse_fragment( + br#"echo substr_replace("hello world", "PHP", 6, 5); echo ":"; +echo substr_replace(string: "abcdef", replace: "X", offset: 1, length: -1); echo ":"; +echo substr_replace("abcdef", "X", -2); echo ":"; +echo call_user_func("substr_replace", "abcdef", "X", 99, 1); echo ":"; +echo call_user_func_array("substr_replace", ["string" => "abcdef", "replace" => "X", "offset" => -99, "length" => 2]); echo ":"; +return function_exists("substr_replace");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hello PHP:aXf:abcdX:abcdefX:Xcdef:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_nl2br_builtin() { + let program = parse_fragment( + br#"echo bin2hex(nl2br("a\nb")); echo ":"; +echo bin2hex(nl2br(string: "a\nb", use_xhtml: false)); echo ":"; +echo bin2hex(call_user_func("nl2br", "a\r\nb")); echo ":"; +echo bin2hex(call_user_func_array("nl2br", ["string" => "a\n\rb", "use_xhtml" => false])); echo ":"; +return function_exists("nl2br");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_bin2hex_builtin() { + let program = parse_fragment( + br#"echo bin2hex("Az"); echo ":"; +echo bin2hex(string: "A\n"); echo ":"; +echo call_user_func("bin2hex", "!?"); echo ":"; +echo call_user_func_array("bin2hex", ["string" => "ok"]); echo ":"; +return function_exists("bin2hex");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "417a:410a:213f:6f6b:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `hex2bin()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_hex2bin_builtin() { + let program = parse_fragment( + br#"echo hex2bin("417a"); echo ":"; +echo bin2hex(hex2bin(string: "410a")); echo ":"; +echo call_user_func("hex2bin", "213f"); echo ":"; +echo call_user_func_array("hex2bin", ["string" => "6f6b"]); echo ":"; +echo hex2bin("4") ? "bad" : "false"; echo ":"; +return function_exists("hex2bin");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Az:410a:!?:ok:false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + vec![HEX2BIN_ODD_LENGTH_WARNING.to_string()] + ); + } + + /// Verifies eval slash escaping builtins use PHP byte-string semantics. + #[test] + fn execute_program_dispatches_slash_escape_builtins() { + let program = parse_fragment( + br#"$escaped = addslashes($source); +echo bin2hex($escaped); echo ":"; +echo bin2hex(stripslashes($escaped)); echo ":"; +echo call_user_func("addslashes", "x\"y"); echo ":"; +echo call_user_func_array("stripslashes", [addslashes("o\"k")]); echo ":"; +return function_exists("addslashes") && function_exists("stripslashes");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let source = values.string("a\0b\\c\"d'").expect("create source"); + scope.set("source", source, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_base64_encode_builtin() { + let program = parse_fragment( + br#"echo base64_encode("Hello"); echo ":"; +echo base64_encode(string: "Hi"); echo ":"; +echo call_user_func("base64_encode", "Test 123!"); echo ":"; +echo call_user_func_array("base64_encode", ["string" => ""]); echo ":"; +return function_exists("base64_encode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "SGVsbG8=:SGk=:VGVzdCAxMjMh::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies eval `base64_decode()` dispatches through direct, named, and callable paths. + #[test] + fn execute_program_dispatches_base64_decode_builtin() { + let program = parse_fragment( + br#"echo base64_decode("SGVsbG8="); echo ":"; +echo base64_decode(string: "SGk="); echo ":"; +echo call_user_func("base64_decode", "VGVzdCAxMjMh"); echo ":"; +echo call_user_func_array("base64_decode", ["string" => ""]); echo ":"; +return function_exists("base64_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hello:Hi:Test 123!::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + } + + /// Verifies `isset` distinguishes missing, null, and other falsey values. + #[test] + fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { + let program = parse_fragment( + br#"if (isset($missing)) { echo "1"; } else { echo "0"; } +if (isset($nullish)) { echo "1"; } else { echo "0"; } +if (isset($zero)) { echo "1"; } else { echo "0"; } +if (isset($empty)) { echo "1"; } else { echo "0"; } +if (isset($zero, $empty)) { echo "1"; } else { echo "0"; } +if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty = values.string("").expect("create fake string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty", empty, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "001110"); + assert_eq!(values.get(result), FakeValue::Null); + } + + /// Verifies `empty` treats missing, null, and falsey values as empty. + #[test] + fn execute_program_empty_uses_php_truthiness_without_missing_warnings() { + let program = parse_fragment( + br#"if (empty($missing)) { echo "1"; } else { echo "0"; } +if (empty($nullish)) { echo "1"; } else { echo "0"; } +if (empty($zero)) { echo "1"; } else { echo "0"; } +if (empty($empty_string)) { echo "1"; } else { echo "0"; } +if (empty($zero_string)) { echo "1"; } else { echo "0"; } +if (empty($value)) { echo "1"; } else { echo "0"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty_string = values.string("").expect("create fake empty string"); + let zero_string = values.string("0").expect("create fake zero string"); + let value = values.string("x").expect("create fake non-empty string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty_string", empty_string, ScopeCellOwnership::Owned); + scope.set("zero_string", zero_string, ScopeCellOwnership::Owned); + scope.set("value", value, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111110"); + assert_eq!(values.get(result), FakeValue::Null); + } + + /// Verifies `isset` and `empty` use PHP offset semantics for array reads. + #[test] + fn execute_program_isset_and_empty_support_array_offsets() { + let program = parse_fragment( + br#"$map = [ + "present" => "x", + "nullish" => null, + "zero" => 0, + "empty" => "", + "child" => ["leaf" => "ok", "null" => null], +]; +echo isset($map["present"]) ? "1" : "0"; +echo isset($map["nullish"]) ? "1" : "0"; +echo isset($map["missing"]) ? "1" : "0"; +echo isset($map["zero"]) ? "1" : "0"; +echo isset($map["child"]["leaf"]) ? "1" : "0"; +echo isset($map["child"]["null"]) ? "1" : "0"; +echo isset($map["missing"]["leaf"]) ? "1" : "0"; +echo ":"; +echo empty($map["present"]) ? "1" : "0"; +echo empty($map["nullish"]) ? "1" : "0"; +echo empty($map["missing"]) ? "1" : "0"; +echo empty($map["zero"]) ? "1" : "0"; +echo empty($map["empty"]) ? "1" : "0"; +echo empty($map["child"]["leaf"]) ? "1" : "0"; +echo empty($map["child"]["null"]) ? "1" : "0"; +echo empty($map["missing"]["leaf"]) ? "1" : "0";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1001100:01111011"); + assert_eq!(values.get(result), FakeValue::Null); + } + + /// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. + #[test] + fn execute_program_function_probes_use_eval_context() { + let program = parse_fragment( + br#"function dyn_probe() { return 1; } +echo function_exists("DYN_PROBE") . "x"; +echo is_callable("dyn_probe") . "x"; +echo function_exists("strlen") . "x"; +echo function_exists("native_probe") . "x"; +echo function_exists("eval") . "x"; +echo function_exists("missing_probe") . "x";"#, + ) + .expect("parse eval fragment"); + let native = NativeFunction::new(1usize as *mut c_void, fake_native_return_descriptor, 0); + let mut context = ElephcEvalContext::new(); + assert!(context + .define_native_function("native_probe", native) + .is_ok()); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "1x1x1x1xxx"); + } + + /// Verifies eval `interface_exists()` probes generated interface metadata by callable. + #[test] + fn execute_program_interface_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo interface_exists("KnownInterface") ? "Y" : "N"; +echo interface_exists("knowninterface") ? "Y" : "N"; +echo interface_exists("KnownClass") ? "Y" : "N"; +echo call_user_func("interface_exists", "KnownInterface") ? "Y" : "N"; +echo call_user_func_array("interface_exists", ["interface" => "KnownInterface"]) ? "Y" : "N"; +echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYNYYN"); + } + + /// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. + #[test] + fn execute_program_class_like_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo trait_exists("KnownTrait") ? "T" : "t"; +echo trait_exists("knowntrait") ? "T" : "t"; +echo trait_exists("KnownEnum") ? "T" : "t"; +echo enum_exists("KnownEnum") ? "E" : "e"; +echo enum_exists("\knownenum") ? "E" : "e"; +echo enum_exists("KnownTrait") ? "E" : "e"; +echo call_user_func("trait_exists", "KnownTrait") ? "T" : "t"; +echo call_user_func_array("enum_exists", ["enum" => "KnownEnum"]) ? "E" : "e"; +echo trait_exists(trait: "MissingTrait", autoload: false) ? "T" : "t"; +echo enum_exists(enum: "MissingEnum", autoload: false) ? "E" : "e";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TTtEEeTEte"); + } + + /// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. + #[test] + fn execute_program_is_a_relation_uses_runtime_probe() { + let program = parse_fragment( + br#"$object = new KnownClass(); +echo is_a($object, "KnownClass") ? "Y" : "N"; +echo is_subclass_of($object, "KnownClass") ? "Y" : "N"; +echo is_subclass_of($object, "ParentClass") ? "Y" : "N"; +echo call_user_func("is_a", $object, "ParentClass") ? "Y" : "N"; +echo call_user_func_array("is_subclass_of", ["object_or_class" => $object, "class" => "ParentClass"]) ? "Y" : "N"; +echo is_a(object_or_class: $object, class: "MissingClass", allow_string: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YNYYYN"); + } + + /// Verifies eval `define()` and `defined()` share a dynamic constant-name table. + #[test] + fn execute_program_define_and_defined_use_dynamic_constant_table() { + let program = parse_fragment( + br#"echo define("DynEvalConst", "ok") ? "Y" : "N"; +echo DynEvalConst; +echo \DynEvalConst; +echo defined("DynEvalConst") ? "Y" : "N"; +echo defined("\\DynEvalConst") ? "Y" : "N"; +echo defined("dynevalconst") ? "Y" : "N"; +echo define("DynEvalConst", 2) ? "Y" : "N"; +echo call_user_func("defined", "DynEvalConst") ? "Y" : "N"; +echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YokokYYNNYY"); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); + } + + /// Verifies eval predefined runtime constants are fetchable and cannot be redefined. + #[test] + fn execute_program_reads_predefined_runtime_constants() { + let program = parse_fragment( + br#"echo PHP_EOL === "\n" ? "eol" : "bad"; echo ":"; +echo (PHP_OS === "Darwin" || PHP_OS === "Linux") ? "os" : "bad"; echo ":"; +echo DIRECTORY_SEPARATOR; echo ":"; +echo PHP_INT_MAX > 9000000000000000000 ? "int" : "bad"; echo ":"; +echo defined("PHP_OS") ? "defined" : "bad"; echo ":"; +echo defined("\\PHP_OS") ? "root" : "bad"; echo ":"; +echo defined("php_os") ? "bad" : "case"; echo ":"; +echo define("PHP_OS", "x") ? "bad" : "locked"; echo ":"; +return PHP_INT_MAX;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "eol:os:/:int:defined:root:case:locked:"); + assert_eq!(values.get(result), FakeValue::Int(i64::MAX)); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); + } + + /// Verifies missing eval dynamic constants fail through runtime status. + #[test] + fn execute_program_missing_constant_fetch_fails() { + let program = parse_fragment(br#"return MissingEvalConst;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } + + /// Verifies eval class probes use the runtime class-name table. + #[test] + fn execute_program_class_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"class DynProbe {} +echo class_exists("DynProbe") ? "Y" : "N"; +echo class_exists("\dynprobe") ? "Y" : "N"; +echo class_exists("KnownClass") ? "Y" : "N"; +echo class_exists("\knownclass") ? "Y" : "N"; +echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYYYN"); + } + + /// Verifies duplicate eval-declared class names fail through runtime status. + #[test] + fn execute_program_duplicate_class_declaration_fails() { + let program = parse_fragment( + br#"class DynProbeDup {} +class dynprobedup {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("duplicate fails"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } + + /// Verifies eval fragments can dispatch registered native AOT functions. + #[test] + fn execute_program_calls_registered_native_function() { + let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + + /// Verifies direct eval calls can bind registered native parameters by name. + #[test] + fn execute_program_calls_registered_native_function_with_named_args() { + let program = parse_fragment(br#"return native_answer(right: 2, left: 1);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + + /// Verifies direct eval calls can unpack arrays into registered native parameters. + #[test] + fn execute_program_calls_registered_native_function_with_spread_args() { + let program = + parse_fragment(br#"return native_answer(...[1, 2]);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); + } + + /// Verifies indexed array writes mutate an existing scope array. + #[test] + fn execute_program_writes_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[1] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); + } + + /// Verifies indexed array append writes use the next visible index. + #[test] + fn execute_program_appends_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); + } + + /// Verifies associative append starts at key zero when only string keys exist. + #[test] + fn execute_program_appends_assoc_scope_array_with_string_keys() { + let program = + parse_fragment(br#"$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); + } + + /// Verifies associative append uses one plus the largest existing integer key. + #[test] + fn execute_program_appends_assoc_scope_array_after_positive_int_key() { + let program = parse_fragment( + br#"$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies associative append preserves PHP's largest-negative-key behavior. + #[test] + fn execute_program_appends_assoc_scope_array_after_negative_int_key() { + let program = + parse_fragment(br#"$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); + } + + /// Verifies mutating a borrowed scope array does not make the eval scope own it. + #[test] + fn execute_program_preserves_borrowed_array_ownership() { + let program = parse_fragment(br#"$items[0] = "b";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let array = values.array_new(1).expect("create fake array"); + scope.set("items", array, ScopeCellOwnership::Borrowed); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let entry = scope.entry("items").expect("scope should contain items"); + + assert_eq!(entry.cell(), array); + assert_eq!(entry.flags().ownership, ScopeCellOwnership::Borrowed); + assert!(values.releases.is_empty()); + } + + /// Verifies replacing an eval-owned scope value releases the old cell. + #[test] + fn execute_program_releases_replaced_scope_value() { + let program = parse_fragment(br#"$x = "old"; $x = "new";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); + } + + /// Verifies unsetting an eval-owned scope value releases the old cell. + #[test] + fn execute_program_releases_unset_scope_value() { + let program = parse_fragment(br#"$x = "old"; unset($x);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); + } + + /// Verifies break exits a runtime eval loop before later statements run. + #[test] + fn execute_program_break_exits_loop() { + let program = parse_fragment(br#"while ($flag) { echo "a"; break; echo "b"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a"); + } + + /// Verifies continue restarts a runtime eval loop and observes later scope updates. + #[test] + fn execute_program_continue_restarts_loop() { + let program = parse_fragment( + br#"while ($flag) { $flag = false; continue; echo "unreachable"; } echo "done";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "done"); + } From 508477fe3a2ba1de13f89c09be53971a50925c06 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 09:55:14 +0200 Subject: [PATCH 0281/1208] Format eval interpreter tests --- crates/elephc-eval/src/interpreter/tests.rs | 9759 +++++++++---------- 1 file changed, 4863 insertions(+), 4896 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests.rs b/crates/elephc-eval/src/interpreter/tests.rs index fffacd4556..858fa1c4af 100644 --- a/crates/elephc-eval/src/interpreter/tests.rs +++ b/crates/elephc-eval/src/interpreter/tests.rs @@ -10,1272 +10,1257 @@ //! - FakeOps owns opaque test cells and mirrors enough runtime behavior for eval. //! - Test fixtures parse PHP eval fragments before executing them through EvalIR. - use std::collections::HashMap; - use std::ffi::c_void; +use std::collections::HashMap; +use std::ffi::c_void; - use crate::parser::parse_fragment; - use crate::value::RuntimeCell; +use crate::parser::parse_fragment; +use crate::value::RuntimeCell; - use super::*; +use super::*; - /// Test-only array key representation for fake indexed and associative arrays. - #[derive(Clone, Debug, PartialEq, Eq, Hash)] - enum FakeKey { - Int(i64), - String(String), - } +/// Test-only array key representation for fake indexed and associative arrays. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum FakeKey { + Int(i64), + String(String), +} - /// Test-only runtime value representation used behind opaque cell handles. - #[derive(Clone, Debug, PartialEq)] - enum FakeValue { - Null, - Bool(bool), - Int(i64), - Float(f64), - String(String), - Bytes(Vec), - Array(Vec), - Assoc(Vec<(FakeKey, RuntimeCellHandle)>), - Object(Vec<(String, RuntimeCellHandle)>), - Iterator { len: i64, position: i64 }, - Resource(i64), - } +/// Test-only runtime value representation used behind opaque cell handles. +#[derive(Clone, Debug, PartialEq)] +enum FakeValue { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), + Bytes(Vec), + Array(Vec), + Assoc(Vec<(FakeKey, RuntimeCellHandle)>), + Object(Vec<(String, RuntimeCellHandle)>), + Iterator { len: i64, position: i64 }, + Resource(i64), +} - /// Test runtime hooks that allocate stable fake handles and record echo output. - #[derive(Default)] - struct FakeOps { - next_id: usize, - values: HashMap, - output: String, - releases: Vec, - warnings: Vec, - } +/// Test runtime hooks that allocate stable fake handles and record echo output. +#[derive(Default)] +struct FakeOps { + next_id: usize, + values: HashMap, + output: String, + releases: Vec, + warnings: Vec, +} - impl FakeOps { - /// Allocates one fake runtime cell and returns its opaque handle. - fn alloc(&mut self, value: FakeValue) -> RuntimeCellHandle { - self.next_id += 1; - let id = self.next_id; - self.values.insert(id, value); - RuntimeCellHandle::from_raw(id as *mut RuntimeCell) +impl FakeOps { + /// Allocates one fake runtime cell and returns its opaque handle. + fn alloc(&mut self, value: FakeValue) -> RuntimeCellHandle { + self.next_id += 1; + let id = self.next_id; + self.values.insert(id, value); + RuntimeCellHandle::from_raw(id as *mut RuntimeCell) + } + + /// Reads a fake runtime cell by opaque handle. + fn get(&self, handle: RuntimeCellHandle) -> FakeValue { + let id = handle.as_ptr() as usize; + self.values.get(&id).cloned().expect("fake cell missing") + } + + /// Converts a fake runtime cell into a normalized fake PHP array key. + fn key(&self, handle: RuntimeCellHandle) -> Result { + let value = self.get(handle); + match value { + FakeValue::Int(value) => Ok(FakeKey::Int(value)), + FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) + .map(FakeKey::Int) + .map_or_else(|| Ok(FakeKey::String(value)), Ok), + FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) + .map(FakeKey::Int) + .map_or_else( + || { + Ok(FakeKey::String( + String::from_utf8_lossy(&value).into_owned(), + )) + }, + Ok, + ), + FakeValue::Null => Ok(FakeKey::String(String::new())), + value => Ok(FakeKey::Int(self.fake_int(&value))), } + } - /// Reads a fake runtime cell by opaque handle. - fn get(&self, handle: RuntimeCellHandle) -> FakeValue { - let id = handle.as_ptr() as usize; - self.values.get(&id).cloned().expect("fake cell missing") + /// Allocates a fake runtime cell for an existing PHP array key. + fn alloc_key(&mut self, key: &FakeKey) -> Result { + match key { + FakeKey::Int(value) => self.int(*value), + FakeKey::String(value) => self.string(value), } + } - /// Converts a fake runtime cell into a normalized fake PHP array key. - fn key(&self, handle: RuntimeCellHandle) -> Result { - let value = self.get(handle); - match value { - FakeValue::Int(value) => Ok(FakeKey::Int(value)), - FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) - .map(FakeKey::Int) - .map_or_else(|| Ok(FakeKey::String(value)), Ok), - FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) - .map(FakeKey::Int) - .map_or_else( - || { - Ok(FakeKey::String( - String::from_utf8_lossy(&value).into_owned(), - )) - }, - Ok, - ), - FakeValue::Null => Ok(FakeKey::String(String::new())), - value => Ok(FakeKey::Int(self.fake_int(&value))), - } - } + /// Finds a fake object property by insertion-order name. + fn object_property( + properties: &[(String, RuntimeCellHandle)], + name: &str, + ) -> Option { + properties + .iter() + .find_map(|(property, value)| (property == name).then_some(*value)) + } +} - /// Allocates a fake runtime cell for an existing PHP array key. - fn alloc_key(&mut self, key: &FakeKey) -> Result { - match key { - FakeKey::Int(value) => self.int(*value), - FakeKey::String(value) => self.string(value), +impl RuntimeValueOps for FakeOps { + /// Creates a fake indexed array cell. + fn array_new(&mut self, capacity: usize) -> Result { + Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) + } + + /// Creates a fake associative array cell. + fn assoc_new(&mut self, _capacity: usize) -> Result { + Ok(self.alloc(FakeValue::Assoc(Vec::new()))) + } + + /// Reads one fake indexed array element. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + match self.get(array) { + FakeValue::Array(elements) => { + let FakeKey::Int(index) = key else { + return self.null(); + }; + if index < 0 { + return self.null(); + } + elements + .get(index as usize) + .copied() + .map_or_else(|| self.null(), Ok) } - } - - /// Finds a fake object property by insertion-order name. - fn object_property( - properties: &[(String, RuntimeCellHandle)], - name: &str, - ) -> Option { - properties + FakeValue::Assoc(entries) => entries .iter() - .find_map(|(property, value)| (property == name).then_some(*value)) + .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => self.null(), } } - impl RuntimeValueOps for FakeOps { - /// Creates a fake indexed array cell. - fn array_new(&mut self, capacity: usize) -> Result { - Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) - } - - /// Creates a fake associative array cell. - fn assoc_new(&mut self, _capacity: usize) -> Result { - Ok(self.alloc(FakeValue::Assoc(Vec::new()))) - } - - /// Reads one fake indexed array element. - fn array_get( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - ) -> Result { - let key = self.key(index)?; - match self.get(array) { - FakeValue::Array(elements) => { - let FakeKey::Int(index) = key else { - return self.null(); - }; - if index < 0 { - return self.null(); - } - elements - .get(index as usize) - .copied() - .map_or_else(|| self.null(), Ok) - } - FakeValue::Assoc(entries) => entries - .iter() - .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) - .map_or_else(|| self.null(), Ok), - _ => self.null(), + /// Checks whether a fake array has the requested key without reading its value. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + let key = self.key(key)?; + let exists = match self.get(array) { + FakeValue::Array(elements) => { + matches!(key, FakeKey::Int(index) if index >= 0 && (index as usize) < elements.len()) } + FakeValue::Assoc(entries) => entries.iter().any(|(entry_key, _)| entry_key == &key), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.bool_value(exists) + } + + /// Returns one fake foreach key by insertion-order position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(array) { + FakeValue::Array(elements) if position < elements.len() => self.int(position as i64), + FakeValue::Assoc(entries) => { + let Some((key, _)) = entries.get(position) else { + return self.null(); + }; + self.alloc_key(key) + } + FakeValue::Array(_) => self.null(), + _ => Err(EvalStatus::UnsupportedConstruct), } + } - /// Checks whether a fake array has the requested key without reading its value. - fn array_key_exists( - &mut self, - key: RuntimeCellHandle, - array: RuntimeCellHandle, - ) -> Result { - let key = self.key(key)?; - let exists = match self.get(array) { - FakeValue::Array(elements) => { - matches!(key, FakeKey::Int(index) if index >= 0 && (index as usize) < elements.len()) - } - FakeValue::Assoc(entries) => entries.iter().any(|(entry_key, _)| entry_key == &key), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - self.bool_value(exists) - } - - /// Returns one fake foreach key by insertion-order position. - fn array_iter_key( - &mut self, - array: RuntimeCellHandle, - position: usize, - ) -> Result { - match self.get(array) { - FakeValue::Array(elements) if position < elements.len() => { - self.int(position as i64) + /// Writes one fake indexed or associative array element. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + let id = array.as_ptr() as usize; + match self.values.get_mut(&id) { + Some(FakeValue::Array(elements)) => { + let FakeKey::Int(index) = key else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if index < 0 { + return Err(EvalStatus::UnsupportedConstruct); } - FakeValue::Assoc(entries) => { - let Some((key, _)) = entries.get(position) else { - return self.null(); - }; - self.alloc_key(key) + let index = index as usize; + while elements.len() <= index { + elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); } - FakeValue::Array(_) => self.null(), - _ => Err(EvalStatus::UnsupportedConstruct), + elements[index] = value; } - } - - /// Writes one fake indexed or associative array element. - fn array_set( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - value: RuntimeCellHandle, - ) -> Result { - let key = self.key(index)?; - let id = array.as_ptr() as usize; - match self.values.get_mut(&id) { - Some(FakeValue::Array(elements)) => { - let FakeKey::Int(index) = key else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if index < 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let index = index as usize; - while elements.len() <= index { - elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); - } - elements[index] = value; - } - Some(FakeValue::Assoc(entries)) => { - if let Some((_, existing_value)) = - entries.iter_mut().find(|(entry_key, _)| entry_key == &key) - { - *existing_value = value; - } else { - entries.push((key, value)); - } + Some(FakeValue::Assoc(entries)) => { + if let Some((_, existing_value)) = + entries.iter_mut().find(|(entry_key, _)| entry_key == &key) + { + *existing_value = value; + } else { + entries.push((key, value)); } - _ => return Err(EvalStatus::UnsupportedConstruct), } - Ok(array) + _ => return Err(EvalStatus::UnsupportedConstruct), } + Ok(array) + } - /// Reads one fake object property by name. - fn property_get( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => properties - .iter() - .find_map(|(name, value)| (name == property).then_some(*value)) - .map_or_else(|| self.null(), Ok), - _ => Err(EvalStatus::UnsupportedConstruct), - } + /// Reads one fake object property by name. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => properties + .iter() + .find_map(|(name, value)| (name == property).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => Err(EvalStatus::UnsupportedConstruct), } + } - /// Writes one fake object property by name. - fn property_set( - &mut self, - object: RuntimeCellHandle, - property: &str, - value: RuntimeCellHandle, - ) -> Result<(), EvalStatus> { - let id = object.as_ptr() as usize; - let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if let Some((_, existing_value)) = - properties.iter_mut().find(|(name, _)| name == property) - { - *existing_value = value; - } else { - properties.push((property.to_string(), value)); - } - Ok(()) + /// Writes one fake object property by name. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some((_, existing_value)) = properties.iter_mut().find(|(name, _)| name == property) + { + *existing_value = value; + } else { + properties.push((property.to_string(), value)); } + Ok(()) + } - /// Returns the number of fake object properties in insertion order. - fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { - match self.get(object) { - FakeValue::Object(properties) => Ok(properties.len()), - FakeValue::Iterator { .. } => Ok(0), - _ => Err(EvalStatus::UnsupportedConstruct), - } + /// Returns the number of fake object properties in insertion order. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { + match self.get(object) { + FakeValue::Object(properties) => Ok(properties.len()), + FakeValue::Iterator { .. } => Ok(0), + _ => Err(EvalStatus::UnsupportedConstruct), } + } - /// Returns one fake object property key by insertion-order position. - fn object_property_iter_key( - &mut self, - object: RuntimeCellHandle, - position: usize, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => { - let Some((name, _)) = properties.get(position) else { - return self.null(); - }; - self.string(name) - } - _ => Err(EvalStatus::UnsupportedConstruct), + /// Returns one fake object property key by insertion-order position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + let Some((name, _)) = properties.get(position) else { + return self.null(); + }; + self.string(name) } + _ => Err(EvalStatus::UnsupportedConstruct), } + } - /// Calls one fake object method by name. - fn method_call( - &mut self, - object: RuntimeCellHandle, - method: &str, - args: Vec, - ) -> Result { - match (self.get(object), method) { - (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { - let id = object.as_ptr() as usize; - let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) - else { - return Err(EvalStatus::UnsupportedConstruct); - }; - *position = 0; - self.null() - } - (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { - self.bool_value(position < len) - } - (FakeValue::Iterator { .. }, "next") if args.is_empty() => { - let id = object.as_ptr() as usize; - let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) - else { - return Err(EvalStatus::UnsupportedConstruct); - }; - *position += 1; - self.null() - } - (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), - (FakeValue::Object(properties), "read_x") => { - if !args.is_empty() { - return Err(EvalStatus::UnsupportedConstruct); - } - Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "add_x") => { - let [arg] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let x = - Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; - let FakeValue::Int(x) = self.get(x) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(arg) = self.get(*arg) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(x + arg) - } - (FakeValue::Object(properties), "add2_x") => { - let [left, right] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let x = - Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; - let FakeValue::Int(x) = self.get(x) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(left) = self.get(*left) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(right) = self.get(*right) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(x + left + right) + /// Calls one fake object method by name. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + match (self.get(object), method) { + (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position = 0; + self.null() + } + (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { + self.bool_value(position < len) + } + (FakeValue::Iterator { .. }, "next") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position += 1; + self.null() + } + (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), + (FakeValue::Object(properties), "read_x") => { + if !args.is_empty() { + return Err(EvalStatus::UnsupportedConstruct); } - _ => Err(EvalStatus::UnsupportedConstruct), + Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "add_x") => { + let [arg] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(arg) = self.get(*arg) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + arg) + } + (FakeValue::Object(properties), "add2_x") => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + left + right) } + _ => Err(EvalStatus::UnsupportedConstruct), } + } - /// Creates one fake object for eval `new` unit tests. - fn new_object(&mut self, _class_name: &str) -> Result { - Ok(self.alloc(FakeValue::Object(Vec::new()))) - } + /// Creates one fake object for eval `new` unit tests. + fn new_object(&mut self, _class_name: &str) -> Result { + Ok(self.alloc(FakeValue::Object(Vec::new()))) + } - /// Applies fake constructor side effects for eval `new` unit tests. - fn construct_object( - &mut self, - object: RuntimeCellHandle, - args: Vec, - ) -> Result<(), EvalStatus> { - let id = object.as_ptr() as usize; - let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if let Some(first) = args.first().copied() { - if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { - *value = first; - } else { - properties.push(("x".to_string(), first)); - } + /// Applies fake constructor side effects for eval `new` unit tests. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some(first) = args.first().copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { + *value = first; + } else { + properties.push(("x".to_string(), first)); } - Ok(()) } + Ok(()) + } - /// Reports one fake AOT class for eval `class_exists` unit tests. - fn class_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownClass")) - } + /// Reports one fake AOT class for eval `class_exists` unit tests. + fn class_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownClass")) + } - /// Reports one fake AOT interface for eval `interface_exists` unit tests. - fn interface_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownInterface")) - } + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + fn interface_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownInterface")) + } - /// Reports one fake AOT trait for eval `trait_exists` unit tests. - fn trait_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownTrait")) - } + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + fn trait_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownTrait")) + } - /// Reports one fake AOT enum for eval `enum_exists` unit tests. - fn enum_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownEnum")) - } + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + fn enum_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownEnum")) + } - /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. - fn object_is_a( - &mut self, - object_or_class: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - ) -> Result { - match self.get(object_or_class) { - FakeValue::Object(_) - if target_class.eq_ignore_ascii_case("Exception") - || target_class.eq_ignore_ascii_case("Throwable") => - { - Ok(!exclude_self) - } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { - Ok(!exclude_self) - } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => { - Ok(true) - } - _ => Ok(false), + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) + if target_class.eq_ignore_ascii_case("Exception") + || target_class.eq_ignore_ascii_case("Throwable") => + { + Ok(!exclude_self) } - } - - /// Returns a fake PHP class name for object-tagged test values. - fn object_class_name( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - match self.get(object) { - FakeValue::Object(_) => self.string("stdClass"), - FakeValue::Iterator { .. } => self.string("Iterator"), - _ => Err(EvalStatus::RuntimeFatal), + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { + Ok(!exclude_self) } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), + _ => Ok(false), } + } - /// Returns fake parent-class names for eval introspection unit tests. - fn parent_class_name( - &mut self, - object_or_class: RuntimeCellHandle, - ) -> Result { - match self.get(object_or_class) { - FakeValue::Object(_) => self.string("ParentClass"), - FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { - self.string("ParentClass") - } - _ => self.string(""), - } + /// Returns a fake PHP class name for object-tagged test values. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(_) => self.string("stdClass"), + FakeValue::Iterator { .. } => self.string("Iterator"), + _ => Err(EvalStatus::RuntimeFatal), } + } - /// Returns the visible element count for fake array values. - fn array_len(&mut self, array: RuntimeCellHandle) -> Result { - match self.get(array) { - FakeValue::Array(elements) => Ok(elements.len()), - FakeValue::Assoc(entries) => Ok(entries.len()), - _ => Err(EvalStatus::UnsupportedConstruct), + /// Returns fake parent-class names for eval introspection unit tests. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) => self.string("ParentClass"), + FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { + self.string("ParentClass") } + _ => self.string(""), } + } - /// Returns whether a fake runtime cell is an indexed or associative array. - fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { - Ok(matches!( - self.get(value), - FakeValue::Array(_) | FakeValue::Assoc(_) - )) - } - - /// Returns whether a fake runtime cell is null. - fn is_null(&mut self, value: RuntimeCellHandle) -> Result { - Ok(matches!(self.get(value), FakeValue::Null)) + /// Returns the visible element count for fake array values. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + match self.get(array) { + FakeValue::Array(elements) => Ok(elements.len()), + FakeValue::Assoc(entries) => Ok(entries.len()), + _ => Err(EvalStatus::UnsupportedConstruct), } + } - /// Returns the fake runtime tag corresponding to a test value. - fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { - Ok(match self.get(value) { - FakeValue::Int(_) => EVAL_TAG_INT, - FakeValue::String(_) | FakeValue::Bytes(_) => EVAL_TAG_STRING, - FakeValue::Float(_) => EVAL_TAG_FLOAT, - FakeValue::Bool(_) => EVAL_TAG_BOOL, - FakeValue::Array(_) => EVAL_TAG_ARRAY, - FakeValue::Assoc(_) => EVAL_TAG_ASSOC, - FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, - FakeValue::Resource(_) => EVAL_TAG_RESOURCE, - FakeValue::Null => EVAL_TAG_NULL, - }) - } + /// Returns whether a fake runtime cell is an indexed or associative array. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + Ok(matches!( + self.get(value), + FakeValue::Array(_) | FakeValue::Assoc(_) + )) + } - /// Returns the fake object handle as a stable object identity. - fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { - match self.get(object) { - FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), - _ => Err(EvalStatus::RuntimeFatal), - } - } + /// Returns whether a fake runtime cell is null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + Ok(matches!(self.get(value), FakeValue::Null)) + } - /// Records fake releases without freeing handles needed for assertions. - fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - self.releases.push(value); - Ok(()) - } + /// Returns the fake runtime tag corresponding to a test value. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Int(_) => EVAL_TAG_INT, + FakeValue::String(_) | FakeValue::Bytes(_) => EVAL_TAG_STRING, + FakeValue::Float(_) => EVAL_TAG_FLOAT, + FakeValue::Bool(_) => EVAL_TAG_BOOL, + FakeValue::Array(_) => EVAL_TAG_ARRAY, + FakeValue::Assoc(_) => EVAL_TAG_ASSOC, + FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, + FakeValue::Resource(_) => EVAL_TAG_RESOURCE, + FakeValue::Null => EVAL_TAG_NULL, + }) + } - /// Returns the same fake handle because fake cells do not refcount. - fn retain(&mut self, value: RuntimeCellHandle) -> Result { - Ok(value) + /// Returns the fake object handle as a stable object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + match self.get(object) { + FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), + _ => Err(EvalStatus::RuntimeFatal), } + } - /// Records fake PHP warnings without writing to stderr. - fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { - self.warnings.push(message.to_string()); - Ok(()) - } + /// Records fake releases without freeing handles needed for assertions. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.releases.push(value); + Ok(()) + } - /// Creates a fake null cell. - fn null(&mut self) -> Result { - Ok(self.alloc(FakeValue::Null)) - } + /// Returns the same fake handle because fake cells do not refcount. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + Ok(value) + } - /// Creates a fake bool cell. - fn bool_value(&mut self, value: bool) -> Result { - Ok(self.alloc(FakeValue::Bool(value))) - } + /// Records fake PHP warnings without writing to stderr. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + self.warnings.push(message.to_string()); + Ok(()) + } - /// Creates a fake int cell. - fn int(&mut self, value: i64) -> Result { - Ok(self.alloc(FakeValue::Int(value))) - } + /// Creates a fake null cell. + fn null(&mut self) -> Result { + Ok(self.alloc(FakeValue::Null)) + } - /// Creates a fake float cell. - fn float(&mut self, value: f64) -> Result { - Ok(self.alloc(FakeValue::Float(value))) - } + /// Creates a fake bool cell. + fn bool_value(&mut self, value: bool) -> Result { + Ok(self.alloc(FakeValue::Bool(value))) + } - /// Creates a fake string cell. - fn string(&mut self, value: &str) -> Result { - Ok(self.alloc(FakeValue::String(value.to_string()))) - } + /// Creates a fake int cell. + fn int(&mut self, value: i64) -> Result { + Ok(self.alloc(FakeValue::Int(value))) + } - /// Creates a fake string cell from raw PHP bytes. - fn string_bytes_value(&mut self, value: &[u8]) -> Result { - match std::str::from_utf8(value) { - Ok(value) => self.string(value), - Err(_) => Ok(self.alloc(FakeValue::Bytes(value.to_vec()))), - } - } + /// Creates a fake float cell. + fn float(&mut self, value: f64) -> Result { + Ok(self.alloc(FakeValue::Float(value))) + } - /// Casts a fake runtime cell to a fake integer cell. - fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - let value = self.fake_int(&value); - self.int(value) - } + /// Creates a fake string cell. + fn string(&mut self, value: &str) -> Result { + Ok(self.alloc(FakeValue::String(value.to_string()))) + } - /// Casts a fake runtime cell to a fake float cell. - fn cast_float( - &mut self, - value: RuntimeCellHandle, - ) -> Result { - let value = self.get(value); - let value = self.fake_numeric(&value); - self.float(value) + /// Creates a fake string cell from raw PHP bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + match std::str::from_utf8(value) { + Ok(value) => self.string(value), + Err(_) => Ok(self.alloc(FakeValue::Bytes(value.to_vec()))), } + } - /// Casts a fake runtime cell to a fake string cell. - fn cast_string( - &mut self, - value: RuntimeCellHandle, - ) -> Result { - let value = self.stringify(value); - self.string(&value) - } + /// Casts a fake runtime cell to a fake integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + let value = self.fake_int(&value); + self.int(value) + } - /// Casts a fake runtime cell to a fake boolean cell. - fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - let value = self.fake_truthy(&value); - self.bool_value(value) - } + /// Casts a fake runtime cell to a fake float cell. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + let value = self.fake_numeric(&value); + self.float(value) + } - /// Computes fake PHP absolute value while preserving float payloads. - fn abs(&mut self, value: RuntimeCellHandle) -> Result { - match self.get(value) { - FakeValue::Float(value) => self.float(value.abs()), - value => self.int(self.fake_int(&value).wrapping_abs()), - } - } + /// Casts a fake runtime cell to a fake string cell. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.stringify(value); + self.string(&value) + } - /// Computes fake PHP ceiling through numeric conversion as a float result. - fn ceil(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).ceil()) - } + /// Casts a fake runtime cell to a fake boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + let value = self.fake_truthy(&value); + self.bool_value(value) + } - /// Computes fake PHP floor through numeric conversion as a float result. - fn floor(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).floor()) + /// Computes fake PHP absolute value while preserving float payloads. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + match self.get(value) { + FakeValue::Float(value) => self.float(value.abs()), + value => self.int(self.fake_int(&value).wrapping_abs()), } + } - /// Computes fake PHP square root through numeric conversion as a float result. - fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).sqrt()) - } + /// Computes fake PHP ceiling through numeric conversion as a float result. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).ceil()) + } - /// Reverses a fake string byte-wise for interpreter tests. - fn strrev(&mut self, value: RuntimeCellHandle) -> Result { - let mut bytes = self.stringify(value).into_bytes(); - bytes.reverse(); - let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - self.string(&value) - } + /// Computes fake PHP floor through numeric conversion as a float result. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).floor()) + } - /// Divides fake numeric cells with PHP `fdiv()` zero handling. - fn fdiv( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left / right) - } + /// Computes fake PHP square root through numeric conversion as a float result. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).sqrt()) + } - /// Computes fake floating-point modulo for interpreter tests. - fn fmod( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left % right) - } + /// Reverses a fake string byte-wise for interpreter tests. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + let mut bytes = self.stringify(value).into_bytes(); + bytes.reverse(); + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + self.string(&value) + } - /// Adds fake numeric cells for interpreter tests. - fn add( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), - (left, right) => self.float(self.fake_numeric(&left) + self.fake_numeric(&right)), - } - } + /// Divides fake numeric cells with PHP `fdiv()` zero handling. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left / right) + } - /// Subtracts fake numeric cells for interpreter tests. - fn sub( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), - (left, right) => self.float(self.fake_numeric(&left) - self.fake_numeric(&right)), - } - } + /// Computes fake floating-point modulo for interpreter tests. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left % right) + } - /// Multiplies fake numeric cells for interpreter tests. - fn mul( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), - (left, right) => self.float(self.fake_numeric(&left) * self.fake_numeric(&right)), - } + /// Adds fake numeric cells for interpreter tests. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), + (left, right) => self.float(self.fake_numeric(&left) + self.fake_numeric(&right)), } + } - /// Divides fake numeric cells for interpreter tests. - fn div( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let right = self.fake_numeric(&self.get(right)); - if right == 0.0 { - return Err(EvalStatus::RuntimeFatal); - } - let left = self.fake_numeric(&self.get(left)); - self.float(left / right) + /// Subtracts fake numeric cells for interpreter tests. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), + (left, right) => self.float(self.fake_numeric(&left) - self.fake_numeric(&right)), } + } - /// Computes fake integer modulo for interpreter tests. - fn modulo( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let right = self.fake_int(&self.get(right)); - if right == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let left = self.fake_int(&self.get(left)); - self.int(left % right) + /// Multiplies fake numeric cells for interpreter tests. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), + (left, right) => self.float(self.fake_numeric(&left) * self.fake_numeric(&right)), } + } - /// Raises fake numeric cells for interpreter tests. - fn pow( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left.powf(right)) + /// Divides fake numeric cells for interpreter tests. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_numeric(&self.get(right)); + if right == 0.0 { + return Err(EvalStatus::RuntimeFatal); } - - /// Rounds fake numeric cells with PHP's optional decimal precision. - fn round( - &mut self, - value: RuntimeCellHandle, - precision: Option, - ) -> Result { - let value = self.fake_numeric(&self.get(value)); - let precision = precision - .map(|precision| self.fake_int(&self.get(precision))) - .unwrap_or(0); - let multiplier = 10_f64.powf(precision as f64); - self.float((value * multiplier).round() / multiplier) + let left = self.fake_numeric(&self.get(left)); + self.float(left / right) + } + + /// Computes fake integer modulo for interpreter tests. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_int(&self.get(right)); + if right == 0 { + return Err(EvalStatus::RuntimeFatal); } - - /// Applies fake integer bitwise and shift operations for interpreter tests. - fn bitwise( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_int(&self.get(left)); - let right = self.fake_int(&self.get(right)); - let value = match op { - EvalBinOp::BitAnd => left & right, - EvalBinOp::BitOr => left | right, - EvalBinOp::BitXor => left ^ right, - EvalBinOp::ShiftLeft => { - if right < 0 { - return Err(EvalStatus::RuntimeFatal); - } - left.wrapping_shl(right as u32) - } - EvalBinOp::ShiftRight => { - if right < 0 { - return Err(EvalStatus::RuntimeFatal); - } - left.wrapping_shr(right as u32) + let left = self.fake_int(&self.get(left)); + self.int(left % right) + } + + /// Raises fake numeric cells for interpreter tests. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left.powf(right)) + } + + /// Rounds fake numeric cells with PHP's optional decimal precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + let value = self.fake_numeric(&self.get(value)); + let precision = precision + .map(|precision| self.fake_int(&self.get(precision))) + .unwrap_or(0); + let multiplier = 10_f64.powf(precision as f64); + self.float((value * multiplier).round() / multiplier) + } + + /// Applies fake integer bitwise and shift operations for interpreter tests. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_int(&self.get(left)); + let right = self.fake_int(&self.get(right)); + let value = match op { + EvalBinOp::BitAnd => left & right, + EvalBinOp::BitOr => left | right, + EvalBinOp::BitXor => left ^ right, + EvalBinOp::ShiftLeft => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); } - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - self.int(value) - } - - /// Applies fake integer bitwise NOT for interpreter tests. - fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.fake_int(&self.get(value)); - self.int(!value) - } - - /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. - fn concat( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let mut left = self.string_bytes_for_value(&self.get(left)); - let right = self.string_bytes_for_value(&self.get(right)); - left.extend_from_slice(&right); - self.string_bytes_value(&left) - } - - /// Compares fake scalar cells and returns a fake PHP boolean. - fn compare( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let result = match op { - EvalBinOp::LooseEq => self.loose_eq(left, right), - EvalBinOp::LooseNotEq => !self.loose_eq(left, right), - EvalBinOp::StrictEq => self.strict_eq(left, right), - EvalBinOp::StrictNotEq => !self.strict_eq(left, right), - EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, - EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, - EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, - EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, - EvalBinOp::Add - | EvalBinOp::Sub - | EvalBinOp::Mul - | EvalBinOp::Div - | EvalBinOp::Mod - | EvalBinOp::Pow - | EvalBinOp::BitAnd - | EvalBinOp::BitOr - | EvalBinOp::BitXor - | EvalBinOp::ShiftLeft - | EvalBinOp::ShiftRight - | EvalBinOp::Concat - | EvalBinOp::Spaceship - | EvalBinOp::LogicalAnd - | EvalBinOp::LogicalOr - | EvalBinOp::LogicalXor => { - return Err(EvalStatus::UnsupportedConstruct); + left.wrapping_shl(right as u32) + } + EvalBinOp::ShiftRight => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); } - }; - self.bool_value(result) - } - - /// Compares fake numeric cells and returns a PHP spaceship integer. - fn spaceship( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.numeric(left)?; - let right = self.numeric(right)?; - let value = if left < right { - -1 - } else if left > right { - 1 - } else { - 0 - }; - self.int(value) - } + left.wrapping_shr(right as u32) + } + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.int(value) + } + + /// Applies fake integer bitwise NOT for interpreter tests. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + let value = self.fake_int(&self.get(value)); + self.int(!value) + } + + /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let mut left = self.string_bytes_for_value(&self.get(left)); + let right = self.string_bytes_for_value(&self.get(right)); + left.extend_from_slice(&right); + self.string_bytes_value(&left) + } + + /// Compares fake scalar cells and returns a fake PHP boolean. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let result = match op { + EvalBinOp::LooseEq => self.loose_eq(left, right), + EvalBinOp::LooseNotEq => !self.loose_eq(left, right), + EvalBinOp::StrictEq => self.strict_eq(left, right), + EvalBinOp::StrictNotEq => !self.strict_eq(left, right), + EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, + EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, + EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, + EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Pow + | EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight + | EvalBinOp::Concat + | EvalBinOp::Spaceship + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor => { + return Err(EvalStatus::UnsupportedConstruct); + } + }; + self.bool_value(result) + } + + /// Compares fake numeric cells and returns a PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.numeric(left)?; + let right = self.numeric(right)?; + let value = if left < right { + -1 + } else if left > right { + 1 + } else { + 0 + }; + self.int(value) + } - /// Appends fake echo output for interpreter tests. - fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - let value = self.stringify(value); - self.output.push_str(&value); - Ok(()) - } + /// Appends fake echo output for interpreter tests. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + let value = self.stringify(value); + self.output.push_str(&value); + Ok(()) + } - /// Casts one fake runtime cell to bytes for nested eval parsing. - fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { - Ok(self.string_bytes_for_value(&self.get(value))) - } + /// Casts one fake runtime cell to bytes for nested eval parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + Ok(self.string_bytes_for_value(&self.get(value))) + } - /// Returns PHP-like truthiness for fake runtime cells. - fn truthy(&mut self, value: RuntimeCellHandle) -> Result { - Ok(match self.get(value) { - FakeValue::Null => false, - FakeValue::Bool(value) => value, - FakeValue::Int(value) => value != 0, - FakeValue::Float(value) => value != 0.0, - FakeValue::String(value) => !value.is_empty() && value != "0", - FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", - FakeValue::Array(value) => !value.is_empty(), - FakeValue::Assoc(value) => !value.is_empty(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => true, - FakeValue::Resource(_) => true, - }) - } + /// Returns PHP-like truthiness for fake runtime cells. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Null => false, + FakeValue::Bool(value) => value, + FakeValue::Int(value) => value != 0, + FakeValue::Float(value) => value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, + FakeValue::Resource(_) => true, + }) } +} - impl FakeOps { - /// Compares fake scalar values with the same loose rules covered by eval tests. - fn loose_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { - match (self.get(left), self.get(right)) { - (FakeValue::Bool(left), right) => left == self.fake_truthy(&right), - (left, FakeValue::Bool(right)) => self.fake_truthy(&left) == right, - (FakeValue::Null, FakeValue::Null) => true, - (FakeValue::Null, FakeValue::String(value)) - | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), - (FakeValue::Null, FakeValue::Bytes(value)) - | (FakeValue::Bytes(value), FakeValue::Null) => value.is_empty(), - (FakeValue::String(left), FakeValue::String(right)) => { - match (left.parse::(), right.parse::()) { - (Ok(left), Ok(right)) => left == right, - _ => left == right, - } +impl FakeOps { + /// Compares fake scalar values with the same loose rules covered by eval tests. + fn loose_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Bool(left), right) => left == self.fake_truthy(&right), + (left, FakeValue::Bool(right)) => self.fake_truthy(&left) == right, + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Null, FakeValue::String(value)) + | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), + (FakeValue::Null, FakeValue::Bytes(value)) + | (FakeValue::Bytes(value), FakeValue::Null) => value.is_empty(), + (FakeValue::String(left), FakeValue::String(right)) => { + match (left.parse::(), right.parse::()) { + (Ok(left), Ok(right)) => left == right, + _ => left == right, } - (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, - (FakeValue::String(left), FakeValue::Bytes(right)) - | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, - (FakeValue::String(left), right) => left - .parse::() - .is_ok_and(|left| left == self.fake_numeric(&right)), - (FakeValue::Bytes(left), right) => std::str::from_utf8(&left) - .ok() - .and_then(|left| left.parse::().ok()) - .is_some_and(|left| left == self.fake_numeric(&right)), - (left, FakeValue::String(right)) => right - .parse::() - .is_ok_and(|right| self.fake_numeric(&left) == right), - (left, FakeValue::Bytes(right)) => std::str::from_utf8(&right) - .ok() - .and_then(|right| right.parse::().ok()) - .is_some_and(|right| self.fake_numeric(&left) == right), - (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), } + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, + (FakeValue::String(left), right) => left + .parse::() + .is_ok_and(|left| left == self.fake_numeric(&right)), + (FakeValue::Bytes(left), right) => std::str::from_utf8(&left) + .ok() + .and_then(|left| left.parse::().ok()) + .is_some_and(|left| left == self.fake_numeric(&right)), + (left, FakeValue::String(right)) => right + .parse::() + .is_ok_and(|right| self.fake_numeric(&left) == right), + (left, FakeValue::Bytes(right)) => std::str::from_utf8(&right) + .ok() + .and_then(|right| right.parse::().ok()) + .is_some_and(|right| self.fake_numeric(&left) == right), + (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), } + } - /// Compares fake scalar values by PHP strict tag and payload equality. - fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { - match (self.get(left), self.get(right)) { - (FakeValue::Null, FakeValue::Null) => true, - (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, - (FakeValue::Int(left), FakeValue::Int(right)) => left == right, - (FakeValue::Float(left), FakeValue::Float(right)) => left == right, - (FakeValue::String(left), FakeValue::String(right)) => left == right, - (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, - (FakeValue::String(left), FakeValue::Bytes(right)) - | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, - (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, - _ => false, - } + /// Compares fake scalar values by PHP strict tag and payload equality. + fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, + (FakeValue::Int(left), FakeValue::Int(right)) => left == right, + (FakeValue::Float(left), FakeValue::Float(right)) => left == right, + (FakeValue::String(left), FakeValue::String(right)) => left == right, + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, + (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, + _ => false, } + } - /// Converts one fake scalar cell to a numeric value for comparison tests. - fn numeric(&self, handle: RuntimeCellHandle) -> Result { - Ok(self.fake_numeric(&self.get(handle))) + /// Converts one fake scalar cell to a numeric value for comparison tests. + fn numeric(&self, handle: RuntimeCellHandle) -> Result { + Ok(self.fake_numeric(&self.get(handle))) + } + + /// Converts a fake value to the numeric scalar used by comparison tests. + fn fake_numeric(&self, value: &FakeValue) -> f64 { + match value { + FakeValue::Null => 0.0, + FakeValue::Bool(false) => 0.0, + FakeValue::Bool(true) => 1.0, + FakeValue::Int(value) => *value as f64, + FakeValue::Float(value) => *value, + FakeValue::String(value) => value.parse::().unwrap_or(0.0), + FakeValue::Bytes(value) => std::str::from_utf8(value) + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(0.0), + FakeValue::Array(value) => value.len() as f64, + FakeValue::Assoc(value) => value.len() as f64, + FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, + FakeValue::Resource(value) => (*value + 1) as f64, } + } - /// Converts a fake value to the numeric scalar used by comparison tests. - fn fake_numeric(&self, value: &FakeValue) -> f64 { - match value { - FakeValue::Null => 0.0, - FakeValue::Bool(false) => 0.0, - FakeValue::Bool(true) => 1.0, - FakeValue::Int(value) => *value as f64, - FakeValue::Float(value) => *value, - FakeValue::String(value) => value.parse::().unwrap_or(0.0), - FakeValue::Bytes(value) => std::str::from_utf8(value) - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(0.0), - FakeValue::Array(value) => value.len() as f64, - FakeValue::Assoc(value) => value.len() as f64, - FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, - FakeValue::Resource(value) => (*value + 1) as f64, - } + /// Converts a fake value to the integer scalar used by modulo tests. + fn fake_int(&self, value: &FakeValue) -> i64 { + self.fake_numeric(value) as i64 + } + + /// Returns fake PHP truthiness for already-loaded test values. + fn fake_truthy(&self, value: &FakeValue) -> bool { + match value { + FakeValue::Null => false, + FakeValue::Bool(value) => *value, + FakeValue::Int(value) => *value != 0, + FakeValue::Float(value) => *value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, + FakeValue::Resource(_) => true, } + } - /// Converts a fake value to the integer scalar used by modulo tests. - fn fake_int(&self, value: &FakeValue) -> i64 { - self.fake_numeric(value) as i64 + /// Converts a fake runtime cell to a PHP-like string for test echo/concat. + fn stringify(&self, handle: RuntimeCellHandle) -> String { + match self.get(handle) { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value, + FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), + FakeValue::Array(_) => "Array".to_string(), + FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), } + } - /// Returns fake PHP truthiness for already-loaded test values. - fn fake_truthy(&self, value: &FakeValue) -> bool { - match value { - FakeValue::Null => false, - FakeValue::Bool(value) => *value, - FakeValue::Int(value) => *value != 0, - FakeValue::Float(value) => *value != 0.0, - FakeValue::String(value) => !value.is_empty() && value != "0", - FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", - FakeValue::Array(value) => !value.is_empty(), - FakeValue::Assoc(value) => !value.is_empty(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => true, - FakeValue::Resource(_) => true, - } + /// Converts a fake PHP value to string bytes while preserving binary strings. + fn string_bytes_for_value(&self, value: &FakeValue) -> Vec { + match value { + FakeValue::String(value) => value.as_bytes().to_vec(), + FakeValue::Bytes(value) => value.clone(), + value => self.stringify_value(value).into_bytes(), } + } - /// Converts a fake runtime cell to a PHP-like string for test echo/concat. - fn stringify(&self, handle: RuntimeCellHandle) -> String { - match self.get(handle) { - FakeValue::Null => String::new(), - FakeValue::Bool(false) => String::new(), - FakeValue::Bool(true) => "1".to_string(), - FakeValue::Int(value) => value.to_string(), - FakeValue::Float(value) => value.to_string(), - FakeValue::String(value) => value, - FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), - FakeValue::Array(_) => "Array".to_string(), - FakeValue::Assoc(_) => "Array".to_string(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), - FakeValue::Resource(value) => format!("Resource id #{}", value + 1), - } + /// Converts one loaded fake PHP value to display text for byte coercions. + fn stringify_value(&self, value: &FakeValue) -> String { + match value { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value.clone(), + FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), + FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), } + } +} - /// Converts a fake PHP value to string bytes while preserving binary strings. - fn string_bytes_for_value(&self, value: &FakeValue) -> Vec { - match value { - FakeValue::String(value) => value.as_bytes().to_vec(), - FakeValue::Bytes(value) => value.clone(), - value => self.stringify_value(value).into_bytes(), - } - } +/// Test native invoker that returns the descriptor pointer as a runtime cell. +unsafe extern "C" fn fake_native_return_descriptor( + descriptor: *mut c_void, + _args: *mut RuntimeCell, +) -> *mut RuntimeCell { + descriptor.cast() +} - /// Converts one loaded fake PHP value to display text for byte coercions. - fn stringify_value(&self, value: &FakeValue) -> String { - match value { - FakeValue::Null => String::new(), - FakeValue::Bool(false) => String::new(), - FakeValue::Bool(true) => "1".to_string(), - FakeValue::Int(value) => value.to_string(), - FakeValue::Float(value) => value.to_string(), - FakeValue::String(value) => value.clone(), - FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), - FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), - FakeValue::Resource(value) => format!("Resource id #{}", value + 1), - } - } - } +/// Verifies assignment writes a named scope entry and return reads it back. +#[test] +fn execute_program_stores_and_returns_scope_value() { + let program = parse_fragment(b"$x = 3; return $x + 4;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - /// Test native invoker that returns the descriptor pointer as a runtime cell. - unsafe extern "C" fn fake_native_return_descriptor( - descriptor: *mut c_void, - _args: *mut RuntimeCell, - ) -> *mut RuntimeCell { - descriptor.cast() - } + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); - /// Verifies assignment writes a named scope entry and return reads it back. - #[test] - fn execute_program_stores_and_returns_scope_value() { - let program = parse_fragment(b"$x = 3; return $x + 4;").expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + assert_eq!(values.get(x), FakeValue::Int(3)); + assert_eq!(values.get(result), FakeValue::Int(7)); +} - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); +/// Verifies reference assignment aliases variable names and writes through the alias. +#[test] +fn execute_program_reference_assignment_updates_source_variable() { + let program = parse_fragment(b"$x = 1; $alias =& $x; $alias = 5; return $x;") + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - assert_eq!(values.get(x), FakeValue::Int(3)); - assert_eq!(values.get(result), FakeValue::Int(7)); - } + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + let alias = scope + .visible_cell("alias") + .expect("scope should contain alias"); - /// Verifies reference assignment aliases variable names and writes through the alias. - #[test] - fn execute_program_reference_assignment_updates_source_variable() { - let program = parse_fragment(b"$x = 1; $alias =& $x; $alias = 5; return $x;") - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - let alias = scope - .visible_cell("alias") - .expect("scope should contain alias"); - - assert_eq!(x, alias); - assert_eq!(values.get(x), FakeValue::Int(5)); - assert_eq!(values.get(result), FakeValue::Int(5)); - } + assert_eq!(x, alias); + assert_eq!(values.get(x), FakeValue::Int(5)); + assert_eq!(values.get(result), FakeValue::Int(5)); +} - /// Verifies eval `throw` exits the program with a retained Throwable cell. - #[test] - fn execute_program_propagates_throw_as_uncaught_outcome() { - let program = - parse_fragment(br#"throw new Exception("eval boom");"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => { - assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)); - } - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), +/// Verifies eval `throw` exits the program with a retained Throwable cell. +#[test] +fn execute_program_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)); } + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), } +} - /// Verifies eval `try/catch` catches a thrown object and binds the catch variable. - #[test] - fn execute_program_catches_throwable_inside_eval() { - let program = parse_fragment( - br#"try { +/// Verifies eval `try/catch` catches a thrown object and binds the catch variable. +#[test] +fn execute_program_catches_throwable_inside_eval() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } catch (Throwable $caught) { return $caught->answer(); }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} - /// Verifies eval `catch (Throwable)` can handle a throw without binding a variable. - #[test] - fn execute_program_catches_throwable_without_variable_inside_eval() { - let program = parse_fragment( - br#"try { +/// Verifies eval `catch (Throwable)` can handle a throw without binding a variable. +#[test] +fn execute_program_catches_throwable_without_variable_inside_eval() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } catch (Throwable) { return 9; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let released = values - .releases - .first() - .copied() - .expect("unbound catch should release the thrown object"); - - assert_eq!(scope.visible_cell("caught"), None); - assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(9)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("unbound catch should release the thrown object"); + + assert_eq!(scope.visible_cell("caught"), None); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(9)); +} - /// Verifies eval `catch (Exception)` matches thrown exception objects. - #[test] - fn execute_program_catches_specific_exception_inside_eval() { - let program = parse_fragment( - br#"try { +/// Verifies eval `catch (Exception)` matches thrown exception objects. +#[test] +fn execute_program_catches_specific_exception_inside_eval() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } catch (Exception $caught) { return $caught->answer(); }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} - /// Verifies eval catch clauses keep source order and skip non-matching types. - #[test] - fn execute_program_skips_non_matching_specific_catch_inside_eval() { - let program = parse_fragment( - br#"try { +/// Verifies eval catch clauses keep source order and skip non-matching types. +#[test] +fn execute_program_skips_non_matching_specific_catch_inside_eval() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } catch (RuntimeException $wrong) { return 1; } catch (Exception $caught) { return 2; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(scope.visible_cell("wrong"), None); - assert_eq!(values.get(result), FakeValue::Int(2)); - } + assert_eq!(scope.visible_cell("wrong"), None); + assert_eq!(values.get(result), FakeValue::Int(2)); +} - /// Verifies union catch clauses test later types in the same catch clause. - #[test] - fn execute_program_catches_union_type_inside_eval() { - let program = parse_fragment( - br#"try { +/// Verifies union catch clauses test later types in the same catch clause. +#[test] +fn execute_program_catches_union_type_inside_eval() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } catch (RuntimeException|Exception $caught) { return $caught->answer(); }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} - /// Verifies eval `finally` runs before a pending try-body return is observed. - #[test] - fn execute_program_runs_finally_before_returning_try_value() { - let program = parse_fragment( - br#"try { +/// Verifies eval `finally` runs before a pending try-body return is observed. +#[test] +fn execute_program_runs_finally_before_returning_try_value() { + let program = parse_fragment( + br#"try { return 1; } finally { echo "finally"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "finally"); - assert_eq!(values.get(result), FakeValue::Int(1)); - } + assert_eq!(values.output, "finally"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} - /// Verifies eval `finally` return values replace pending try-body returns. - #[test] - fn execute_program_finally_return_overrides_try_return() { - let program = parse_fragment( - br#"try { +/// Verifies eval `finally` return values replace pending try-body returns. +#[test] +fn execute_program_finally_return_overrides_try_return() { + let program = parse_fragment( + br#"try { return 1; } finally { return 2; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.releases.len(), 1); - } + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.releases.len(), 1); +} - /// Verifies eval `finally` return values replace pending uncaught throws. - #[test] - fn execute_program_finally_return_overrides_uncaught_throw() { - let program = parse_fragment( - br#"try { +/// Verifies eval `finally` return values replace pending uncaught throws. +#[test] +fn execute_program_finally_return_overrides_uncaught_throw() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } finally { return 2; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let released = values - .releases - .first() - .copied() - .expect("overridden throw should be released"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("overridden throw should be released"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); +} - /// Verifies eval `finally` runs before an uncaught throw leaves the fragment. - #[test] - fn execute_program_runs_finally_before_uncaught_throw_outcome() { - let program = parse_fragment( - br#"try { +/// Verifies eval `finally` runs before an uncaught throw leaves the fragment. +#[test] +fn execute_program_runs_finally_before_uncaught_throw_outcome() { + let program = parse_fragment( + br#"try { throw new Exception("eval boom"); } finally { echo "finally"; }"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => { - assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)) - } - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)) } - assert_eq!(values.output, "finally"); + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), } + assert_eq!(values.output, "finally"); +} - /// Verifies static locals declared inside eval catch blocks persist per function context. - #[test] - fn execute_context_function_persists_static_local_inside_catch() { - let program = parse_fragment( - br#"function dyn($e) { +/// Verifies static locals declared inside eval catch blocks persist per function context. +#[test] +fn execute_context_function_persists_static_local_inside_catch() { + let program = parse_fragment( + br#"function dyn($e) { try { throw $e; } catch (Throwable $caught) { @@ -1284,35 +1269,34 @@ return $caught->answer() + $n; } }"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - let first_thrown = values - .new_object("Exception") - .expect("allocate first fake exception"); - let second_thrown = values - .new_object("Exception") - .expect("allocate second fake exception"); - - let first = execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) - .expect("execute first dynamic function call"); - let second = - execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) - .expect("execute second dynamic function call"); - - assert_eq!(values.get(first), FakeValue::Int(43)); - assert_eq!(values.get(second), FakeValue::Int(44)); - } + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let first_thrown = values + .new_object("Exception") + .expect("allocate first fake exception"); + let second_thrown = values + .new_object("Exception") + .expect("allocate second fake exception"); + + let first = execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) + .expect("execute first dynamic function call"); + let second = execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(43)); + assert_eq!(values.get(second), FakeValue::Int(44)); +} - /// Verifies static locals declared inside eval finally blocks persist per function context. - #[test] - fn execute_context_function_persists_static_local_inside_finally() { - let program = parse_fragment( - br#"function dyn() { +/// Verifies static locals declared inside eval finally blocks persist per function context. +#[test] +fn execute_context_function_persists_static_local_inside_finally() { + let program = parse_fragment( + br#"function dyn() { try { return 0; } finally { @@ -1321,80 +1305,79 @@ return $n; } }"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - - let first = execute_context_function_zero_args(&mut context, "dyn", &mut values) - .expect("execute first dynamic function call"); - let second = execute_context_function_zero_args(&mut context, "dyn", &mut values) - .expect("execute second dynamic function call"); - - assert_eq!(values.get(first), FakeValue::Int(1)); - assert_eq!(values.get(second), FakeValue::Int(2)); - } + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + + let first = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute first dynamic function call"); + let second = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(2)); +} - /// Verifies throws from eval-declared functions escape through the shared context. - #[test] - fn execute_context_function_propagates_throw_as_uncaught_outcome() { - let program = - parse_fragment(br#"function dyn($e) { throw $e; }"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - let thrown = values - .new_object("Exception") - .expect("allocate fake exception"); - - let outcome = - execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) - .expect("throw should be an eval function outcome"); - - match outcome { - EvalOutcome::Throwable(value) => assert_eq!(value, thrown), - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } +/// Verifies throws from eval-declared functions escape through the shared context. +#[test] +fn execute_context_function_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"function dyn($e) { throw $e; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); + + let outcome = execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) + .expect("throw should be an eval function outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), } +} - /// Verifies nested eval preserves the thrown cell while returning an uncaught status. - #[test] - fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { - let program = parse_fragment(br#"eval("throw $e;");"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let thrown = values - .new_object("Exception") - .expect("allocate fake exception"); - scope.set("e", thrown, ScopeCellOwnership::Borrowed); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("nested throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => assert_eq!(value, thrown), - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } +/// Verifies nested eval preserves the thrown cell while returning an uncaught status. +#[test] +fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { + let program = parse_fragment(br#"eval("throw $e;");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); + scope.set("e", thrown, ScopeCellOwnership::Borrowed); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("nested throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), } +} - /// Verifies eval include resolves caller-relative paths, shares scope, and returns file values. - #[test] - fn execute_program_include_uses_call_site_and_returns_file_result() { - let dir = std::env::temp_dir().join(format!( - "elephc-eval-include-{}-call-site", - std::process::id() - )); - let path = dir.join("piece.php"); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).expect("create include fixture directory"); - std::fs::write( +/// Verifies eval include resolves caller-relative paths, shares scope, and returns file values. +#[test] +fn execute_program_include_uses_call_site_and_returns_file_result() { + let dir = std::env::temp_dir().join(format!( + "elephc-eval-include-{}-call-site", + std::process::id() + )); + let path = dir.join("piece.php"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("create include fixture directory"); + std::fs::write( &path, format!( r#">= 3; echo $x; echo ":"; echo ~0; echo ":"; echo -16 >> 2; return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:5:4:32:8:-1:-4"); - assert_eq!(values.get(result), FakeValue::Int(21)); - } + assert_eq!(values.output, "2:5:4:32:8:-1:-4"); + assert_eq!(values.get(result), FakeValue::Int(21)); +} - /// Verifies simple variable increment and decrement statements update the scope value. - #[test] - fn execute_program_evaluates_inc_dec_statements() { - let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies simple variable increment and decrement statements update the scope value. +#[test] +fn execute_program_evaluates_inc_dec_statements() { + let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); - assert_eq!(values.output, "1"); - assert_eq!(values.get(i), FakeValue::Int(1)); - } + assert_eq!(values.output, "1"); + assert_eq!(values.get(i), FakeValue::Int(1)); +} - /// Verifies echo and unset operate through runtime hooks and scope metadata. - #[test] - fn execute_program_echoes_and_unsets_scope_value() { - let program = - parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let name = values.string(" Ada").expect("create fake string"); - scope.set("name", name, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "hi Ada"); - assert_eq!(values.get(result), FakeValue::Null); - assert!(scope.entry("name").expect("unset marker").flags().unset); - } +/// Verifies echo and unset operate through runtime hooks and scope metadata. +#[test] +fn execute_program_echoes_and_unsets_scope_value() { + let program = + parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let name = values.string(" Ada").expect("create fake string"); + scope.set("name", name, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hi Ada"); + assert_eq!(values.get(result), FakeValue::Null); + assert!(scope.entry("name").expect("unset marker").flags().unset); +} - /// Verifies comma-separated echo expressions are executed in source order. - #[test] - fn execute_program_echoes_comma_list() { - let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let b = values.string("b").expect("create fake string"); - scope.set("b", b, ScopeCellOwnership::Owned); +/// Verifies comma-separated echo expressions are executed in source order. +#[test] +fn execute_program_echoes_comma_list() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let b = values.string("b").expect("create fake string"); + scope.set("b", b, ScopeCellOwnership::Owned); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "abc"); - } + assert_eq!(values.output, "abc"); +} - /// Verifies print writes output and returns integer 1. - #[test] - fn execute_program_print_returns_one() { - let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies print writes output and returns integer 1. +#[test] +fn execute_program_print_returns_one() { + let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "p"); - assert_eq!(values.get(result), FakeValue::Int(1)); - } + assert_eq!(values.output, "p"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} - /// Verifies eval `print_r()` emits supported values and returns true. - #[test] - fn execute_program_dispatches_print_r_builtin() { - let program = parse_fragment( - br#"print_r("x"); echo ":"; +/// Verifies eval `print_r()` emits supported values and returns true. +#[test] +fn execute_program_dispatches_print_r_builtin() { + let program = parse_fragment( + br#"print_r("x"); echo ":"; print_r(value: false); echo ":"; print_r([1, 2]); echo ":"; $call = call_user_func("print_r", true); $spread = call_user_func_array("print_r", ["value" => "z"]); echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; return function_exists("print_r");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "x::Array\n:1z:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "x::Array\n:1z:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. - #[test] - fn execute_program_dispatches_var_dump_builtin() { - let program = parse_fragment( +/// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. +#[test] +fn execute_program_dispatches_var_dump_builtin() { + let program = parse_fragment( br#"var_dump(42); var_dump("hi"); var_dump(false); @@ -1664,157 +1645,156 @@ echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread- return function_exists("var_dump");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "int(42)\n", + "string(2) \"hi\"\n", + "bool(false)\n", + "NULL\n", + "array(2) {\n", + " [0]=>\n", + " int(10)\n", + " [1]=>\n", + " int(20)\n", + "}\n", + "array(1) {\n", + " [\"x\"]=>\n", + " bool(true)\n", + "}\n", + "float(3.5)\n", + "string(1) \"z\"\n", + "call-null:spread-null:", + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); +/// Verifies eval property reads and writes dispatch through runtime hooks. +#[test] +fn execute_program_reads_and_writes_object_property() { + let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!( + values + .property_get(object, "x") + .map(|value| values.get(value)) + .expect("property should be readable"), + FakeValue::Int(2) + ); +} - assert_eq!( - values.output, - concat!( - "int(42)\n", - "string(2) \"hi\"\n", - "bool(false)\n", - "NULL\n", - "array(2) {\n", - " [0]=>\n", - " int(10)\n", - " [1]=>\n", - " int(20)\n", - "}\n", - "array(1) {\n", - " [\"x\"]=>\n", - " bool(true)\n", - "}\n", - "float(3.5)\n", - "string(1) \"z\"\n", - "call-null:spread-null:", - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } +/// Verifies eval method calls dispatch through the runtime method hook. +#[test] +fn execute_program_calls_object_method() { + let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); - /// Verifies eval property reads and writes dispatch through runtime hooks. - #[test] - fn execute_program_reads_and_writes_object_property() { - let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(1).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!( - values - .property_get(object, "x") - .map(|value| values.get(value)) - .expect("property should be readable"), - FakeValue::Int(2) - ); - } + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - /// Verifies eval method calls dispatch through the runtime method hook. - #[test] - fn execute_program_calls_object_method() { - let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("this", object, ScopeCellOwnership::Borrowed); + assert_eq!(values.get(result), FakeValue::Int(42)); +} - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); +/// Verifies eval method calls forward evaluated arguments to the runtime hook. +#[test] +fn execute_program_calls_object_method_with_argument() { + let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); - assert_eq!(values.get(result), FakeValue::Int(42)); - } + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - /// Verifies eval method calls forward evaluated arguments to the runtime hook. - #[test] - fn execute_program_calls_object_method_with_argument() { - let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); + assert_eq!(values.get(result), FakeValue::Int(12)); +} - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); +/// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. +#[test] +fn execute_program_calls_object_method_with_two_arguments() { + let program = parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); - assert_eq!(values.get(result), FakeValue::Int(12)); - } + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - /// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. - #[test] - fn execute_program_calls_object_method_with_two_arguments() { - let program = - parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(18)); - } + assert_eq!(values.get(result), FakeValue::Int(18)); +} - /// Verifies eval method calls forward numerically unpacked arguments. - #[test] - fn execute_program_calls_object_method_with_spread_arguments() { - let program = - parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(18)); - } +/// Verifies eval method calls forward numerically unpacked arguments. +#[test] +fn execute_program_calls_object_method_with_spread_arguments() { + let program = + parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); +} - /// Verifies eval object construction dispatches through runtime hooks. - #[test] - fn execute_program_constructs_named_object() { - let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies eval object construction dispatches through runtime hooks. +#[test] +fn execute_program_constructs_named_object() { + let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Object(Vec::new())); - } + assert_eq!(values.get(result), FakeValue::Object(Vec::new())); +} - /// Verifies eval object construction passes constructor arguments through runtime hooks. - #[test] - fn execute_program_constructs_named_object_with_args() { - let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies eval object construction passes constructor arguments through runtime hooks. +#[test] +fn execute_program_constructs_named_object_with_args() { + let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let FakeValue::Object(properties) = values.get(result) else { - panic!("expected fake object"); - }; - let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let FakeValue::Object(properties) = values.get(result) else { + panic!("expected fake object"); + }; + let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); - assert_eq!(values.get(x), FakeValue::Int(1)); - } + assert_eq!(values.get(x), FakeValue::Int(1)); +} - /// Verifies eval-declared classes create objects with properties and methods. - #[test] - fn execute_program_constructs_eval_declared_class_with_method() { - let program = parse_fragment( - br#"class DynBox { +/// Verifies eval-declared classes create objects with properties and methods. +#[test] +fn execute_program_constructs_eval_declared_class_with_method() { + let program = parse_fragment( + br#"class DynBox { public int $x = 1; public function __construct($x) { $this->x = $x; } public function bump($n) { $this->x = $this->x + $n; return $this->x; } @@ -1831,1373 +1811,1361 @@ echo ":"; echo call_user_func_array($call, [2]); echo ":"; return $box->x;"#, - ) + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DynBox:7:Y8:10:"); + assert_eq!(values.get(result), FakeValue::Int(10)); +} + +/// Verifies if/else executes only the PHP-truthy branch. +#[test] +fn execute_program_if_else_uses_php_truthiness() { + let program = parse_fragment(br#"if ($flag) { $x = "then"; } else { $x = "else"; }"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(0).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); - assert_eq!(values.output, "DynBox:7:Y8:10:"); - assert_eq!(values.get(result), FakeValue::Int(10)); - } + assert_eq!(values.get(x), FakeValue::String("else".to_string())); +} - /// Verifies if/else executes only the PHP-truthy branch. - #[test] - fn execute_program_if_else_uses_php_truthiness() { - let program = parse_fragment(br#"if ($flag) { $x = "then"; } else { $x = "else"; }"#) +/// Verifies elseif chains execute the first truthy branch and skip later branches. +#[test] +fn execute_program_elseif_uses_first_truthy_branch() { + let program = + parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; } else { $x = "c"; }"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.int(0).expect("create fake int"); - scope.set("flag", flag, ScopeCellOwnership::Owned); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let a = values.bool_value(false).expect("create fake bool"); + let b = values.bool_value(true).expect("create fake bool"); + scope.set("a", a, ScopeCellOwnership::Owned); + scope.set("b", b, ScopeCellOwnership::Owned); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); - assert_eq!(values.get(x), FakeValue::String("else".to_string())); - } + assert_eq!(values.get(x), FakeValue::String("b".to_string())); +} - /// Verifies elseif chains execute the first truthy branch and skip later branches. - #[test] - fn execute_program_elseif_uses_first_truthy_branch() { - let program = parse_fragment( - br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; } else { $x = "c"; }"#, - ) +/// Verifies while repeats while the condition remains truthy and propagates writes. +#[test] +fn execute_program_while_uses_php_truthiness() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let a = values.bool_value(false).expect("create fake bool"); - let b = values.bool_value(true).expect("create fake bool"); - scope.set("a", a, ScopeCellOwnership::Owned); - scope.set("b", b, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(2).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); - assert_eq!(values.get(x), FakeValue::String("b".to_string())); - } + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let flag = scope + .visible_cell("flag") + .expect("scope should contain flag"); - /// Verifies while repeats while the condition remains truthy and propagates writes. - #[test] - fn execute_program_while_uses_php_truthiness() { - let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.int(2).expect("create fake int"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let flag = scope - .visible_cell("flag") - .expect("scope should contain flag"); - - assert_eq!(values.output, "2"); - assert_eq!(values.get(flag), FakeValue::Bool(false)); - } + assert_eq!(values.output, "2"); + assert_eq!(values.get(flag), FakeValue::Bool(false)); +} - /// Verifies do/while runs the body before testing the condition. - #[test] - fn execute_program_do_while_runs_body_before_condition() { - let program = parse_fragment(br#"do { echo $i; $i = $i + 1; } while (false);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let i = values.int(0).expect("create fake int"); - scope.set("i", i, ScopeCellOwnership::Owned); +/// Verifies do/while runs the body before testing the condition. +#[test] +fn execute_program_do_while_runs_body_before_condition() { + let program = parse_fragment(br#"do { echo $i; $i = $i + 1; } while (false);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let i = values.int(0).expect("create fake int"); + scope.set("i", i, ScopeCellOwnership::Owned); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); - assert_eq!(values.output, "0"); - assert_eq!(values.get(i), FakeValue::Int(1)); - } + assert_eq!(values.output, "0"); + assert_eq!(values.get(i), FakeValue::Int(1)); +} - /// Verifies switch uses loose matching and falls through after the matching case. - #[test] - fn execute_program_switch_matches_and_falls_through() { - let program = +/// Verifies switch uses loose matching and falls through after the matching case. +#[test] +fn execute_program_switch_matches_and_falls_through() { + let program = parse_fragment(br#"switch ($x) { case 1: echo "one"; break; case 2: echo "two"; default: echo "default"; }"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(2).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(2).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "twodefault"); - } + assert_eq!(values.output, "twodefault"); +} - /// Verifies for loops run init, condition, update, and body in PHP order. - #[test] - fn execute_program_for_loop_updates_after_body() { - let program = parse_fragment(br#"for ($i = 3; $i; $i = $i - 1) { echo $i; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies for loops run init, condition, update, and body in PHP order. +#[test] +fn execute_program_for_loop_updates_after_body() { + let program = parse_fragment(br#"for ($i = 3; $i; $i = $i - 1) { echo $i; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); - assert_eq!(values.output, "321"); - assert_eq!(values.get(i), FakeValue::Int(0)); - } + assert_eq!(values.output, "321"); + assert_eq!(values.get(i), FakeValue::Int(0)); +} + +/// Verifies `continue` in a for loop still runs the update clause. +#[test] +fn execute_program_for_continue_runs_update_clause() { + let program = parse_fragment( + br#"for ($i = 3; $i; $i = $i - 1) { if ($i - 1) { continue; } echo "done"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "done"); + assert_eq!(values.get(i), FakeValue::Int(0)); +} - /// Verifies `continue` in a for loop still runs the update clause. - #[test] - fn execute_program_for_continue_runs_update_clause() { - let program = parse_fragment( - br#"for ($i = 3; $i; $i = $i - 1) { if ($i - 1) { continue; } echo "done"; }"#, +/// Verifies comparison operators return boolean cells usable by echo and branches. +#[test] +fn execute_program_comparisons_return_bool_cells() { + let program = parse_fragment( + br#"echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; if ("10" == 10) { echo "n"; } if ("a" != "b") { echo "s"; }"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "done"); - assert_eq!(values.get(i), FakeValue::Int(0)); - } + assert_eq!(values.output, "1111ns"); +} - /// Verifies comparison operators return boolean cells usable by echo and branches. - #[test] - fn execute_program_comparisons_return_bool_cells() { - let program = parse_fragment( - br#"echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; if ("10" == 10) { echo "n"; } if ("a" != "b") { echo "s"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies spaceship comparisons return PHP -1/0/1 integer cells. +#[test] +fn execute_program_spaceship_returns_int_cells() { + let program = + parse_fragment(br#"echo 1 <=> 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1111ns"); - } + assert_eq!(values.output, "-1:0:1"); +} - /// Verifies spaceship comparisons return PHP -1/0/1 integer cells. - #[test] - fn execute_program_spaceship_returns_int_cells() { - let program = - parse_fragment(br#"echo 1 <=> 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies strict equality keeps PHP type identity distinct from loose equality. +#[test] +fn execute_program_strict_equality_uses_type_identity() { + let program = parse_fragment( + br#"echo "10" == 10; echo "10" === 10; echo "10" === "10"; echo "10" !== 10;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "-1:0:1"); - } + assert_eq!(values.output, "111"); +} - /// Verifies strict equality keeps PHP type identity distinct from loose equality. - #[test] - fn execute_program_strict_equality_uses_type_identity() { - let program = parse_fragment( - br#"echo "10" == 10; echo "10" === 10; echo "10" === "10"; echo "10" !== 10;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies logical AND skips an unsupported right-hand expression after a false left side. +#[test] +fn execute_program_short_circuits_logical_and() { + let program = parse_fragment(br#"return false && missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "111"); - } + assert_eq!(values.get(result), FakeValue::Bool(false)); +} - /// Verifies logical AND skips an unsupported right-hand expression after a false left side. - #[test] - fn execute_program_short_circuits_logical_and() { - let program = - parse_fragment(br#"return false && missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies logical OR skips an unsupported right-hand expression after a true left side. +#[test] +fn execute_program_short_circuits_logical_or() { + let program = parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Bool(false)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies logical OR skips an unsupported right-hand expression after a true left side. - #[test] - fn execute_program_short_circuits_logical_or() { - let program = parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies match expressions use strict comparison across comma-separated patterns. +#[test] +fn execute_program_match_uses_strict_pattern_comparison() { + let program = + parse_fragment(br#"return match ($x) { 1, "1" => "string", default => "other" };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.string("1").expect("create fake string"); + scope.set("x", x, ScopeCellOwnership::Owned); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::String("string".to_string())); +} - /// Verifies match expressions use strict comparison across comma-separated patterns. - #[test] - fn execute_program_match_uses_strict_pattern_comparison() { - let program = - parse_fragment(br#"return match ($x) { 1, "1" => "string", default => "other" };"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.string("1").expect("create fake string"); - scope.set("x", x, ScopeCellOwnership::Owned); +/// Verifies match expressions evaluate only the selected arm result. +#[test] +fn execute_program_match_skips_unselected_results() { + let program = parse_fragment( + br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("string".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("two".to_string())); +} - /// Verifies match expressions evaluate only the selected arm result. - #[test] - fn execute_program_match_skips_unselected_results() { - let program = parse_fragment( - br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#, - ) +/// Verifies match expressions without a matching arm or default fail at runtime. +#[test] +fn execute_program_match_without_default_fails_on_miss() { + let program = parse_fragment(br#"return match (3) { 1 => "one", 2 => "two" };"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values); - assert_eq!(values.get(result), FakeValue::String("two".to_string())); - } + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} - /// Verifies match expressions without a matching arm or default fail at runtime. - #[test] - fn execute_program_match_without_default_fails_on_miss() { - let program = parse_fragment(br#"return match (3) { 1 => "one", 2 => "two" };"#) +/// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. +#[test] +fn execute_program_evaluates_keyword_logical_operators() { + let program = + parse_fragment(br#"echo (false || true and false) ? "T" : "F"; return true or missing();"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - } + assert_eq!(values.output, "F"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. - #[test] - fn execute_program_evaluates_keyword_logical_operators() { - let program = parse_fragment( - br#"echo (false || true and false) ? "T" : "F"; return true or missing();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies PHP keyword `xor` evaluates both operands and returns a boolean cell. +#[test] +fn execute_program_evaluates_keyword_xor() { + let program = + parse_fragment(br#"echo (true xor false) ? "T" : "F"; echo (true xor true) ? "T" : "F";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "F"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "TF"); +} - /// Verifies PHP keyword `xor` evaluates both operands and returns a boolean cell. - #[test] - fn execute_program_evaluates_keyword_xor() { - let program = parse_fragment( - br#"echo (true xor false) ? "T" : "F"; echo (true xor true) ? "T" : "F";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies ternary expressions evaluate only the selected branch. +#[test] +fn execute_program_ternary_short_circuits_unselected_branch() { + let program = + parse_fragment(br#"echo true ? "yes" : missing(); echo false ? missing() : "no";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "TF"); - } + assert_eq!(values.output, "yesno"); +} - /// Verifies ternary expressions evaluate only the selected branch. - #[test] - fn execute_program_ternary_short_circuits_unselected_branch() { - let program = - parse_fragment(br#"echo true ? "yes" : missing(); echo false ? missing() : "no";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies the short ternary form returns the condition value when it is truthy. +#[test] +fn execute_program_short_ternary_reuses_truthy_condition() { + let program = parse_fragment(br#"echo "x" ?: "fallback"; echo false ?: "fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "yesno"); - } + assert_eq!(values.output, "xfallback"); +} - /// Verifies the short ternary form returns the condition value when it is truthy. - #[test] - fn execute_program_short_ternary_reuses_truthy_condition() { - let program = parse_fragment(br#"echo "x" ?: "fallback"; echo false ?: "fallback";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies null coalescing uses the default for missing or null values. +#[test] +fn execute_program_null_coalesce_uses_default_for_missing_or_null() { + let program = parse_fragment(br#"echo $missing ?? "fallback"; echo $x ?? "null-fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.null().expect("create fake null"); + scope.set("x", x, ScopeCellOwnership::Owned); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "xfallback"); - } + assert_eq!(values.output, "fallbacknull-fallback"); +} - /// Verifies null coalescing uses the default for missing or null values. - #[test] - fn execute_program_null_coalesce_uses_default_for_missing_or_null() { - let program = - parse_fragment(br#"echo $missing ?? "fallback"; echo $x ?? "null-fallback";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.null().expect("create fake null"); - scope.set("x", x, ScopeCellOwnership::Owned); +/// Verifies null coalescing skips the default expression for non-null values. +#[test] +fn execute_program_null_coalesce_short_circuits_non_null_value() { + let program = parse_fragment(br#"echo "set" ?? missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "fallbacknull-fallback"); - } + assert_eq!(values.output, "set"); +} - /// Verifies null coalescing skips the default expression for non-null values. - #[test] - fn execute_program_null_coalesce_short_circuits_non_null_value() { - let program = parse_fragment(br#"echo "set" ?? missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies logical negation returns boolean cells using PHP truthiness. +#[test] +fn execute_program_evaluates_logical_not() { + let program = parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "set"); - } + assert_eq!(values.output, "1"); +} - /// Verifies logical negation returns boolean cells using PHP truthiness. - #[test] - fn execute_program_evaluates_logical_not() { - let program = parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies unary numeric operators delegate to PHP numeric runtime operations. +#[test] +fn execute_program_evaluates_unary_numeric_ops() { + let program = parse_fragment(br#"return -$x + +2;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(5).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1"); - } + assert_eq!(values.get(result), FakeValue::Int(-3)); +} - /// Verifies unary numeric operators delegate to PHP numeric runtime operations. - #[test] - fn execute_program_evaluates_unary_numeric_ops() { - let program = parse_fragment(br#"return -$x + +2;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(5).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); +/// Verifies foreach assigns each indexed element to the value variable. +#[test] +fn execute_program_foreach_iterates_indexed_values() { + let program = parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); - assert_eq!(values.get(result), FakeValue::Int(-3)); - } + assert_eq!(values.output, "ab"); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); +} - /// Verifies foreach assigns each indexed element to the value variable. - #[test] - fn execute_program_foreach_iterates_indexed_values() { - let program = parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) +/// Verifies foreach key-value targets receive indexed integer keys and values. +#[test] +fn execute_program_foreach_assigns_indexed_keys() { + let program = + parse_fragment(br#"foreach (["a", "b"] as $key => $item) { echo $key . $item; }"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let key = scope.visible_cell("key").expect("scope should contain key"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "0a1b"); + assert_eq!(values.get(key), FakeValue::Int(1)); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); +} - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let item = scope - .visible_cell("item") - .expect("scope should contain last foreach item"); +/// Verifies foreach over associative arrays preserves insertion-order keys and values. +#[test] +fn execute_program_foreach_iterates_assoc_keys_and_values() { + let program = parse_fragment( + br#"foreach (["a" => 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - assert_eq!(values.output, "ab"); - assert_eq!(values.get(item), FakeValue::String("b".to_string())); - } + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - /// Verifies foreach key-value targets receive indexed integer keys and values. - #[test] - fn execute_program_foreach_assigns_indexed_keys() { - let program = - parse_fragment(br#"foreach (["a", "b"] as $key => $item) { echo $key . $item; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let key = scope.visible_cell("key").expect("scope should contain key"); - let item = scope - .visible_cell("item") - .expect("scope should contain last foreach item"); - - assert_eq!(values.output, "0a1b"); - assert_eq!(values.get(key), FakeValue::Int(1)); - assert_eq!(values.get(item), FakeValue::String("b".to_string())); - } + assert_eq!(values.output, "a:1;b:2;"); +} - /// Verifies foreach over associative arrays preserves insertion-order keys and values. - #[test] - fn execute_program_foreach_iterates_assoc_keys_and_values() { - let program = parse_fragment( - br#"foreach (["a" => 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }"#, - ) +/// Verifies value-only foreach over associative arrays still yields values in insertion order. +#[test] +fn execute_program_foreach_iterates_assoc_values_only() { + let program = parse_fragment(br#"foreach (["a" => 1, "b" => 2] as $item) { echo $item; }"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "a:1;b:2;"); - } + assert_eq!(values.output, "12"); +} - /// Verifies value-only foreach over associative arrays still yields values in insertion order. - #[test] - fn execute_program_foreach_iterates_assoc_values_only() { - let program = parse_fragment(br#"foreach (["a" => 1, "b" => 2] as $item) { echo $item; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies break and continue control foreach execution inside eval. +#[test] +fn execute_program_foreach_honors_break_and_continue() { + let program = parse_fragment( + br#"foreach ([1, 2, 3] as $item) { if ($item == 1) { continue; } echo $item; break; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "12"); - } + assert_eq!(values.output, "2"); +} - /// Verifies break and continue control foreach execution inside eval. - #[test] - fn execute_program_foreach_honors_break_and_continue() { - let program = parse_fragment( - br#"foreach ([1, 2, 3] as $item) { if ($item == 1) { continue; } echo $item; break; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies indexed array literals and reads execute through runtime hooks. +#[test] +fn execute_program_reads_indexed_array_literal() { + let program = parse_fragment(br#"return ["a", "b"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2"); - } + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} - /// Verifies indexed array literals and reads execute through runtime hooks. - #[test] - fn execute_program_reads_indexed_array_literal() { - let program = parse_fragment(br#"return ["a", "b"][1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies legacy `array(...)` literals execute through the existing array runtime hooks. +#[test] +fn execute_program_reads_legacy_array_literal() { + let program = + parse_fragment(br#"return array("a", "b" => "bee",)[0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("b".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("a".to_string())); +} - /// Verifies legacy `array(...)` literals execute through the existing array runtime hooks. - #[test] - fn execute_program_reads_legacy_array_literal() { - let program = parse_fragment(br#"return array("a", "b" => "bee",)[0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies associative array literals and string-key reads execute through runtime hooks. +#[test] +fn execute_program_reads_assoc_array_literal() { + let program = + parse_fragment(br#"return ["name" => "Ada"]["name"];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("a".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); +} - /// Verifies associative array literals and string-key reads execute through runtime hooks. - #[test] - fn execute_program_reads_assoc_array_literal() { - let program = - parse_fragment(br#"return ["name" => "Ada"]["name"];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies unkeyed assoc literal entries start at zero after string keys. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_string_key_starts_at_zero() { + let program = + parse_fragment(br#"return ["name" => "Ada", "Grace"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} - /// Verifies unkeyed assoc literal entries start at zero after string keys. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_string_key_starts_at_zero() { - let program = parse_fragment(br#"return ["name" => "Ada", "Grace"][0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies unkeyed assoc literal entries use one plus the largest integer key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_positive_int_key() { + let program = + parse_fragment(br#"return [2 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies unkeyed assoc literal entries use one plus the largest integer key. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_positive_int_key() { - let program = - parse_fragment(br#"return [2 => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies unkeyed assoc literal entries preserve PHP's negative-key rule. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_negative_int_key() { + let program = + parse_fragment(br#"return [-2 => "minus", "tail"][-1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies unkeyed assoc literal entries preserve PHP's negative-key rule. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_negative_int_key() { - let program = - parse_fragment(br#"return [-2 => "minus", "tail"][-1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies numeric string literal keys update the next automatic key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_numeric_string_key() { + let program = + parse_fragment(br#"return ["2" => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies numeric string literal keys update the next automatic key. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_numeric_string_key() { - let program = - parse_fragment(br#"return ["2" => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies leading-zero string literal keys do not update the automatic key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_leading_zero_string_key() { + let program = + parse_fragment(br#"return ["02" => "two", "tail"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies leading-zero string literal keys do not update the automatic key. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_leading_zero_string_key() { - let program = - parse_fragment(br#"return ["02" => "two", "tail"][0];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies null literal keys normalize to empty strings without advancing automatic keys. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_null_key() { + let program = + parse_fragment(br#"return [null => "empty", "tail"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies null literal keys normalize to empty strings without advancing automatic keys. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_null_key() { - let program = parse_fragment(br#"return [null => "empty", "tail"][0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies null literal keys are readable through the empty-string key. +#[test] +fn execute_program_assoc_array_literal_reads_null_key_as_empty_string() { + let program = parse_fragment(br#"return [null => "empty"][""];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("empty".to_string())); +} - /// Verifies null literal keys are readable through the empty-string key. - #[test] - fn execute_program_assoc_array_literal_reads_null_key_as_empty_string() { - let program = - parse_fragment(br#"return [null => "empty"][""];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies boolean literal keys update the next automatic key after integer normalization. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { + let program = + parse_fragment(br#"return [true => "yes", "tail"][2];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("empty".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies boolean literal keys update the next automatic key after integer normalization. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { - let program = - parse_fragment(br#"return [true => "yes", "tail"][2];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies false literal keys update the next automatic key from zero. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_false_key() { + let program = + parse_fragment(br#"return [false => "no", "tail"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies false literal keys update the next automatic key from zero. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_false_key() { - let program = - parse_fragment(br#"return [false => "no", "tail"][1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies float literal keys update the next automatic key after truncation. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_float_key() { + let program = + parse_fragment(br#"return [2.7 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies float literal keys update the next automatic key after truncation. - #[test] - fn execute_program_assoc_array_literal_unkeyed_after_float_key() { - let program = - parse_fragment(br#"return [2.7 => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies nested eval calls parse and execute against the same dynamic scope. +#[test] +fn execute_program_nested_eval_uses_same_scope() { + let program = + parse_fragment(br#"eval("$x = $x + 4;"); return $x;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::Int(5)); +} - /// Verifies nested eval calls parse and execute against the same dynamic scope. - #[test] - fn execute_program_nested_eval_uses_same_scope() { - let program = - parse_fragment(br#"eval("$x = $x + 4;"); return $x;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(1).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); +/// Verifies `__LINE__` inside eval uses the source line within the fragment. +#[test] +fn execute_program_magic_line_uses_fragment_line() { + let program = parse_fragment(b"\nreturn __LINE__;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(5)); - } + assert_eq!(values.get(result), FakeValue::Int(2)); +} - /// Verifies `__LINE__` inside eval uses the source line within the fragment. - #[test] - fn execute_program_magic_line_uses_fragment_line() { - let program = parse_fragment(b"\nreturn __LINE__;").expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies file-dependent eval magic constants use call-site metadata from the context. +#[test] +fn execute_program_magic_file_and_dir_use_context_call_site() { + let program = + parse_fragment(br#"return __FILE__ . "|" . __DIR__;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/main.php", "/tmp", 17); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("/tmp/main.php(17) : eval()'d code|/tmp".to_string()) + ); +} - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); +/// Verifies eval class, namespace, and trait magic constants are empty in eval scope. +#[test] +fn execute_program_scope_magic_constants_are_empty_strings() { + let program = + parse_fragment(br#"return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - assert_eq!(values.get(result), FakeValue::Int(2)); - } + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - /// Verifies file-dependent eval magic constants use call-site metadata from the context. - #[test] - fn execute_program_magic_file_and_dir_use_context_call_site() { - let program = - parse_fragment(br#"return __FILE__ . "|" . __DIR__;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - context.set_call_site("/tmp/main.php", "/tmp", 17); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!( - values.get(result), - FakeValue::String("/tmp/main.php(17) : eval()'d code|/tmp".to_string()) - ); - } + assert_eq!(values.get(result), FakeValue::String("[||]".to_string())); +} - /// Verifies eval class, namespace, and trait magic constants are empty in eval scope. - #[test] - fn execute_program_scope_magic_constants_are_empty_strings() { - let program = parse_fragment( - br#"return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";"#, - ) +/// Verifies eval-declared functions can be called by the same fragment. +#[test] +fn execute_program_calls_declared_function() { + let program = parse_fragment(br#"function dyn($x) { return $x + 1; } return dyn(4);"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("[||]".to_string())); - } - - /// Verifies eval-declared functions can be called by the same fragment. - #[test] - fn execute_program_calls_declared_function() { - let program = parse_fragment(br#"function dyn($x) { return $x + 1; } return dyn(4);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(5)); - } + assert_eq!(values.get(result), FakeValue::Int(5)); +} - /// Verifies eval namespace declarations qualify functions and namespace magic values. - #[test] - fn execute_program_namespace_qualifies_declared_function() { - let program = parse_fragment( - br#"namespace Eval\Ns; +/// Verifies eval namespace declarations qualify functions and namespace magic values. +#[test] +fn execute_program_namespace_qualifies_declared_function() { + let program = parse_fragment( + br#"namespace Eval\Ns; function dyn() { return __NAMESPACE__ . ":" . __FUNCTION__; } return dyn();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.get(result), - FakeValue::String("Eval\\Ns:Eval\\Ns\\dyn".to_string()) - ); - } + assert_eq!( + values.get(result), + FakeValue::String("Eval\\Ns:Eval\\Ns\\dyn".to_string()) + ); +} - /// Verifies unqualified namespaced calls fall back to global builtins when needed. - #[test] - fn execute_program_namespace_call_falls_back_to_builtin() { - let program = parse_fragment(br#"namespace Eval\Ns; return strlen("abcd");"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies unqualified namespaced calls fall back to global builtins when needed. +#[test] +fn execute_program_namespace_call_falls_back_to_builtin() { + let program = parse_fragment(br#"namespace Eval\Ns; return strlen("abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(4)); - } + assert_eq!(values.get(result), FakeValue::Int(4)); +} - /// Verifies namespaced dynamic functions take precedence over global builtin fallback. - #[test] - fn execute_program_namespace_function_overrides_builtin_fallback() { - let program = parse_fragment( - br#"namespace Eval\Ns; +/// Verifies namespaced dynamic functions take precedence over global builtin fallback. +#[test] +fn execute_program_namespace_function_overrides_builtin_fallback() { + let program = parse_fragment( + br#"namespace Eval\Ns; function strlen($value) { return 99; } return strlen("abcd");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(99)); - } + assert_eq!(values.get(result), FakeValue::Int(99)); +} - /// Verifies unqualified namespaced constants fall back to global predefined constants. - #[test] - fn execute_program_namespace_const_fetch_falls_back_to_global() { - let program = - parse_fragment(br#"namespace Eval\Ns; return PHP_EOL;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies unqualified namespaced constants fall back to global predefined constants. +#[test] +fn execute_program_namespace_const_fetch_falls_back_to_global() { + let program = + parse_fragment(br#"namespace Eval\Ns; return PHP_EOL;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("\n".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("\n".to_string())); +} - /// Verifies namespaced dynamic constants take precedence over global fallback. - #[test] - fn execute_program_namespace_const_fetch_reads_dynamic_constant_first() { - let program = - parse_fragment(br#"namespace Eval\Ns; return LOCAL;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(7).expect("create fake int"); - assert!(context.define_constant("Eval\\Ns\\LOCAL", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); - } +/// Verifies namespaced dynamic constants take precedence over global fallback. +#[test] +fn execute_program_namespace_const_fetch_reads_dynamic_constant_first() { + let program = + parse_fragment(br#"namespace Eval\Ns; return LOCAL;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(7).expect("create fake int"); + assert!(context.define_constant("Eval\\Ns\\LOCAL", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} - /// Verifies eval namespace `use function` imports dispatch to qualified dynamic functions. - #[test] - fn execute_program_namespace_use_function_import_dispatches() { - let program = parse_fragment( - br#"namespace Eval\Lib; +/// Verifies eval namespace `use function` imports dispatch to qualified dynamic functions. +#[test] +fn execute_program_namespace_use_function_import_dispatches() { + let program = parse_fragment( + br#"namespace Eval\Lib; function target($x) { return $x + 1; } namespace Eval\App; use function Eval\Lib\target as AliasTarget; return aliastarget(6);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(7)); - } + assert_eq!(values.get(result), FakeValue::Int(7)); +} - /// Verifies eval namespace `use const` imports fetch qualified dynamic constants. - #[test] - fn execute_program_namespace_use_const_import_fetches_dynamic_constant() { - let program = parse_fragment( - br#"namespace Eval\App; +/// Verifies eval namespace `use const` imports fetch qualified dynamic constants. +#[test] +fn execute_program_namespace_use_const_import_fetches_dynamic_constant() { + let program = parse_fragment( + br#"namespace Eval\App; use const Eval\Lib\VALUE as LocalValue; return LocalValue;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(11).expect("create fake int"); - assert!(context.define_constant("Eval\\Lib\\VALUE", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(11)); - } + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(11).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(11)); +} - /// Verifies eval grouped namespace imports dispatch dynamic functions and constants. - #[test] - fn execute_program_grouped_namespace_use_imports_dispatch() { - let program = parse_fragment( - br#"namespace Eval\Lib; +/// Verifies eval grouped namespace imports dispatch dynamic functions and constants. +#[test] +fn execute_program_grouped_namespace_use_imports_dispatch() { + let program = parse_fragment( + br#"namespace Eval\Lib; function target($x) { return $x + 2; } namespace Eval\App; use function Eval\Lib\{target as AliasTarget}; use const Eval\Lib\{VALUE as LocalValue}; return AliasTarget(LocalValue);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(5).expect("create fake int"); - assert!(context.define_constant("Eval\\Lib\\VALUE", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); - } + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(5).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} - /// Verifies eval-declared functions bind named arguments by parameter name. - #[test] - fn execute_program_calls_declared_function_with_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(y: 2, x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies eval-declared functions bind named arguments by parameter name. +#[test] +fn execute_program_calls_declared_function_with_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(y: 2, x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(12)); - } + assert_eq!(values.get(result), FakeValue::Int(12)); +} - /// Verifies eval-declared functions unpack indexed arrays as positional arguments. - #[test] - fn execute_program_calls_declared_function_with_spread_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...[1, 2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies eval-declared functions unpack indexed arrays as positional arguments. +#[test] +fn execute_program_calls_declared_function_with_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...[1, 2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(12)); - } + assert_eq!(values.get(result), FakeValue::Int(12)); +} - /// Verifies string keys unpack as named arguments for eval-declared functions. - #[test] - fn execute_program_calls_declared_function_with_named_spread_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...["y" => 2], x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies string keys unpack as named arguments for eval-declared functions. +#[test] +fn execute_program_calls_declared_function_with_named_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...["y" => 2], x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(12)); - } + assert_eq!(values.get(result), FakeValue::Int(12)); +} - /// Verifies eval-declared function static locals persist between calls. - #[test] - fn execute_program_static_var_persists_in_declared_function() { - let program = parse_fragment( - br#"function dyn() { for ($i = 0; $i < 2; $i++) { static $n = 0; $n++; } return $n; } +/// Verifies eval-declared function static locals persist between calls. +#[test] +fn execute_program_static_var_persists_in_declared_function() { + let program = parse_fragment( + br#"function dyn() { for ($i = 0; $i < 2; $i++) { static $n = 0; $n++; } return $n; } return (dyn() * 10) + dyn();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(24)); - } + assert_eq!(values.get(result), FakeValue::Int(24)); +} - /// Verifies top-level eval static declarations reinitialize on each eval execution. - #[test] - fn execute_program_top_level_static_var_reinitializes_per_eval() { - let program = - parse_fragment(br#"static $n = 0; $n++; return $n;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let first = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute first eval ir"); - let second = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute second eval ir"); - - assert_eq!(values.get(first), FakeValue::Int(1)); - assert_eq!(values.get(second), FakeValue::Int(1)); - } +/// Verifies top-level eval static declarations reinitialize on each eval execution. +#[test] +fn execute_program_top_level_static_var_reinitializes_per_eval() { + let program = + parse_fragment(br#"static $n = 0; $n++; return $n;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let first = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute first eval ir"); + let second = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute second eval ir"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(1)); +} - /// Verifies `global` declarations read and write the context global scope. - #[test] - fn execute_program_global_alias_writes_context_global_scope() { - let program = - parse_fragment(br#"global $g; $g = $g + 1; return $g;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut global_scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let initial = values.int(1).expect("allocate initial global"); - global_scope.set("g", initial, ScopeCellOwnership::Owned); - context.set_global_scope(&mut global_scope); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - let global = global_scope - .visible_cell("g") - .expect("global scope should contain g"); - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.get(global), FakeValue::Int(2)); - } +/// Verifies `global` declarations read and write the context global scope. +#[test] +fn execute_program_global_alias_writes_context_global_scope() { + let program = + parse_fragment(br#"global $g; $g = $g + 1; return $g;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.get(global), FakeValue::Int(2)); +} - /// Verifies references to global aliases write the source global variable. - #[test] - fn execute_program_reference_alias_to_global_updates_source_global() { - let program = parse_fragment(br#"global $g; $alias =& $g; $alias = 4; return $g;"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut global_scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let initial = values.int(1).expect("allocate initial global"); - global_scope.set("g", initial, ScopeCellOwnership::Owned); - context.set_global_scope(&mut global_scope); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - let global = global_scope - .visible_cell("g") - .expect("global scope should contain g"); - assert_eq!(values.get(result), FakeValue::Int(4)); - assert_eq!(values.get(global), FakeValue::Int(4)); - assert!(global_scope.visible_cell("alias").is_none()); - } +/// Verifies references to global aliases write the source global variable. +#[test] +fn execute_program_reference_alias_to_global_updates_source_global() { + let program = parse_fragment(br#"global $g; $alias =& $g; $alias = 4; return $g;"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(4)); + assert_eq!(values.get(global), FakeValue::Int(4)); + assert!(global_scope.visible_cell("alias").is_none()); +} - /// Verifies named calls reject positional arguments that follow named arguments. - #[test] - fn execute_program_rejects_positional_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, print "late");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies named calls reject positional arguments that follow named arguments. +#[test] +fn execute_program_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, print "late");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values); + let result = execute_program(&program, &mut scope, &mut values); - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - assert_eq!(values.output, ""); - } + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert_eq!(values.output, ""); +} - /// Verifies named calls reject argument unpacking after named arguments. - #[test] - fn execute_program_rejects_spread_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, ...[2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies named calls reject argument unpacking after named arguments. +#[test] +fn execute_program_rejects_spread_after_named_arg() { + let program = + parse_fragment(br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, ...[2]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values); + let result = execute_program(&program, &mut scope, &mut values); - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - } + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} - /// Verifies function-scope magic constants keep the eval declaration spelling. - #[test] - fn execute_program_magic_function_and_method_use_eval_declared_name() { - let program = parse_fragment( +/// Verifies function-scope magic constants keep the eval declaration spelling. +#[test] +fn execute_program_magic_function_and_method_use_eval_declared_name() { + let program = parse_fragment( br#"function DynMagicCase() { return __FUNCTION__ . ":" . __METHOD__; } return dynmagiccase();"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.get(result), - FakeValue::String("DynMagicCase:DynMagicCase".to_string()) - ); - } + assert_eq!( + values.get(result), + FakeValue::String("DynMagicCase:DynMagicCase".to_string()) + ); +} - /// Verifies eval-declared functions persist in a shared eval context. - #[test] - fn execute_program_context_keeps_declared_function() { - let define = - parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("parse eval fragment"); - let call = parse_fragment(br#"return dyn(4);"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect("execute eval ir"); - let result = execute_program_with_context(&mut context, &call, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); - } +/// Verifies eval-declared functions persist in a shared eval context. +#[test] +fn execute_program_context_keeps_declared_function() { + let define = + parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("parse eval fragment"); + let call = parse_fragment(br#"return dyn(4);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute eval ir"); + let result = execute_program_with_context(&mut context, &call, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} - /// Verifies `call_user_func` inside eval can dispatch an eval-declared function. - #[test] - fn execute_program_call_user_func_dispatches_declared_function() { - let program = parse_fragment( - br#"function dyn($x) { return $x + 1; } +/// Verifies `call_user_func` inside eval can dispatch an eval-declared function. +#[test] +fn execute_program_call_user_func_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x) { return $x + 1; } return call_user_func("dyn", 4);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(5)); - } + assert_eq!(values.get(result), FakeValue::Int(5)); +} - /// Verifies `call_user_func` inside eval can dispatch a supported builtin. - #[test] - fn execute_program_call_user_func_dispatches_builtin() { - let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies `call_user_func` inside eval can dispatch a supported builtin. +#[test] +fn execute_program_call_user_func_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(4)); - } + assert_eq!(values.get(result), FakeValue::Int(4)); +} - /// Verifies `call_user_func` inside eval can dispatch a registered native function. - #[test] - fn execute_program_call_user_func_dispatches_registered_native_function() { - let program = parse_fragment(br#"return call_user_func("native_answer");"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } +/// Verifies `call_user_func` inside eval can dispatch a registered native function. +#[test] +fn execute_program_call_user_func_dispatches_registered_native_function() { + let program = + parse_fragment(br#"return call_user_func("native_answer");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} - /// Verifies string variable calls inside eval can dispatch a supported builtin. - #[test] - fn execute_program_variable_call_dispatches_builtin() { - let program = parse_fragment( - br#"$fn = "strlen"; +/// Verifies string variable calls inside eval can dispatch a supported builtin. +#[test] +fn execute_program_variable_call_dispatches_builtin() { + let program = parse_fragment( + br#"$fn = "strlen"; return $fn("abcd");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(4)); - } + assert_eq!(values.get(result), FakeValue::Int(4)); +} - /// Verifies callable array entries can be invoked through postfix dynamic calls. - #[test] - fn execute_program_postfix_variable_call_dispatches_builtin() { - let program = parse_fragment( - br#"$callbacks = ["strlen"]; +/// Verifies callable array entries can be invoked through postfix dynamic calls. +#[test] +fn execute_program_postfix_variable_call_dispatches_builtin() { + let program = parse_fragment( + br#"$callbacks = ["strlen"]; return $callbacks[0]("abc");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(3)); - } + assert_eq!(values.get(result), FakeValue::Int(3)); +} - /// Verifies variable calls bind eval-declared function arguments by name. - #[test] - fn execute_program_variable_call_binds_declared_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } +/// Verifies variable calls bind eval-declared function arguments by name. +#[test] +fn execute_program_variable_call_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } $fn = "dyn"; return $fn(y: 2, x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(12)); - } + assert_eq!(values.get(result), FakeValue::Int(12)); +} - /// Verifies variable calls can dispatch registered native functions with named args. - #[test] - fn execute_program_variable_call_binds_registered_native_named_args() { - let program = parse_fragment( - br#"$fn = "native_answer"; +/// Verifies variable calls can dispatch registered native functions with named args. +#[test] +fn execute_program_variable_call_binds_registered_native_named_args() { + let program = parse_fragment( + br#"$fn = "native_answer"; return $fn(right: 2, left: 1);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} - /// Verifies direct callable-array variable calls dispatch object methods. - #[test] - fn execute_program_callable_array_variable_dispatches_object_method() { - let program = parse_fragment( - br#"$box = new Box(41); +/// Verifies direct callable-array variable calls dispatch object methods. +#[test] +fn execute_program_callable_array_variable_dispatches_object_method() { + let program = parse_fragment( + br#"$box = new Box(41); $cb = [$box, "add_x"]; return $cb(1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(42)); - } + assert_eq!(values.get(result), FakeValue::Int(42)); +} - /// Verifies `call_user_func` dispatches callable arrays with object receivers. - #[test] - fn execute_program_call_user_func_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(42); +/// Verifies `call_user_func` dispatches callable arrays with object receivers. +#[test] +fn execute_program_call_user_func_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(42); $cb = [$box, "read_x"]; return call_user_func($cb);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(42)); - } + assert_eq!(values.get(result), FakeValue::Int(42)); +} - /// Verifies `call_user_func_array` dispatches callable arrays with positional args. - #[test] - fn execute_program_call_user_func_array_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(39); +/// Verifies `call_user_func_array` dispatches callable arrays with positional args. +#[test] +fn execute_program_call_user_func_array_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(39); return call_user_func_array([$box, "add2_x"], [1, 2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(42)); - } + assert_eq!(values.get(result), FakeValue::Int(42)); +} - /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. - #[test] - fn execute_program_call_user_func_array_dispatches_declared_function() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } +/// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. +#[test] +fn execute_program_call_user_func_array_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return call_user_func_array("dyn", [4, 5]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(9)); - } + assert_eq!(values.get(result), FakeValue::Int(9)); +} - /// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. - #[test] - fn execute_program_call_user_func_array_binds_declared_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } +/// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. +#[test] +fn execute_program_call_user_func_array_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(12)); - } + assert_eq!(values.get(result), FakeValue::Int(12)); +} - /// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. - #[test] - fn execute_context_function_call_array_binds_declared_named_args() { - let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - let arg_array = values.assoc_new(2).expect("allocate argument array"); - let key_y = values.string("y").expect("allocate y key"); - let value_y = values.int(2).expect("allocate y value"); - let _ = values - .array_set(arg_array, key_y, value_y) - .expect("store y argument"); - let key_x = values.string("x").expect("allocate x key"); - let value_x = values.int(1).expect("allocate x value"); - let _ = values - .array_set(arg_array, key_x, value_x) - .expect("store x argument"); - - let result = - execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) - .expect("execute context function call array"); - - assert_eq!(values.get(result), FakeValue::Int(12)); - } +/// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. +#[test] +fn execute_context_function_call_array_binds_declared_named_args() { + let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + let arg_array = values.assoc_new(2).expect("allocate argument array"); + let key_y = values.string("y").expect("allocate y key"); + let value_y = values.int(2).expect("allocate y value"); + let _ = values + .array_set(arg_array, key_y, value_y) + .expect("store y argument"); + let key_x = values.string("x").expect("allocate x key"); + let value_x = values.int(1).expect("allocate x value"); + let _ = values + .array_set(arg_array, key_x, value_x) + .expect("store x argument"); + + let result = execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) + .expect("execute context function call array"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} - /// Verifies `call_user_func_array` rejects positional values after named keys. - #[test] - fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } +/// Verifies `call_user_func_array` rejects positional values after named keys. +#[test] +fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return call_user_func_array("dyn", ["y" => 2, 1]);"#, - ) + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} + +/// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. +#[test] +fn execute_program_call_user_func_array_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - } + assert_eq!(values.get(result), FakeValue::Int(4)); +} - /// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. - #[test] - fn execute_program_call_user_func_array_dispatches_builtin() { - let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies `call_user_func_array` inside eval can dispatch a registered native function. +#[test] +fn execute_program_call_user_func_array_dispatches_registered_native_function() { + let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Int(4)); - } + assert_eq!(result, expected); +} - /// Verifies `call_user_func_array` inside eval can dispatch a registered native function. - #[test] - fn execute_program_call_user_func_array_dispatches_registered_native_function() { - let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } +/// Verifies `call_user_func_array` named keys can bind registered native parameters. +#[test] +fn execute_program_call_user_func_array_binds_registered_native_named_args() { + let program = parse_fragment( + br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} - /// Verifies `call_user_func_array` named keys can bind registered native parameters. - #[test] - fn execute_program_call_user_func_array_binds_registered_native_named_args() { - let program = parse_fragment( - br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } +/// Verifies duplicate eval-declared function names fail in a shared context. +#[test] +fn execute_program_rejects_duplicate_declared_function() { + let define = parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - /// Verifies duplicate eval-declared function names fail in a shared context. - #[test] - fn execute_program_rejects_duplicate_declared_function() { - let define = - parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect("execute first declaration"); - let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect_err("duplicate function declaration should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute first declaration"); + let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect_err("duplicate function declaration should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} - /// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. - #[test] - fn execute_program_dispatches_simple_builtins() { - let program = parse_fragment( - br#"echo strlen("abc") . ":" . count([1, [2, 3], [4]]) . ":"; +/// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. +#[test] +fn execute_program_dispatches_simple_builtins() { + let program = parse_fragment( + br#"echo strlen("abc") . ":" . count([1, [2, 3], [4]]) . ":"; echo count([1, [2, 3], [4]], COUNT_RECURSIVE) . ":"; echo call_user_func("count", [1, [2]]) . ":"; echo call_user_func_array("count", ["value" => [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; return defined("COUNT_RECURSIVE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:3:6:2:3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "3:3:6:2:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. - #[test] - fn execute_program_dispatches_json_encode_builtin() { - let program = parse_fragment( +/// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. +#[test] +fn execute_program_dispatches_json_encode_builtin() { + let program = parse_fragment( br#"echo json_encode(["a" => 1, "b" => "x/y"]) . ":"; echo json_encode([1, "q", true, null]) . ":"; echo call_user_func("json_encode", "a/b\"c") . ":"; @@ -3239,22 +3207,22 @@ echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . " return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"k\ufffd":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:"# - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!( + values.output, + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"k\ufffd":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `json_decode()` materializes scalars, arrays, and associative arrays. - #[test] - fn execute_program_dispatches_json_decode_builtin() { - let program = parse_fragment( +/// Verifies eval `json_decode()` materializes scalars, arrays, and associative arrays. +#[test] +fn execute_program_dispatches_json_decode_builtin() { + let program = parse_fragment( br#"echo json_decode("\"hello\"") . ":"; echo json_decode("42") . ":"; echo (json_decode("true") ? "T" : "bad") . ":"; @@ -3287,23 +3255,23 @@ echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "asso return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. - #[test] - fn execute_program_dispatches_json_decode_stdclass_default() { - let program = parse_fragment( - br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +/// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. +#[test] +fn execute_program_dispatches_json_decode_stdclass_default() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); echo $object->a . ":" . $object->b->c . ":"; $objectFalse = json_decode("{\"z\":2}", false); echo $objectFalse->z . ":"; @@ -3312,22 +3280,22 @@ echo $objectNull->n->m . ":"; $assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); echo $assoc["b"]["c"] . ":"; return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1:x:2:3:y:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "1:x:2:3:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `json_encode()` serializes stdClass dynamic properties. - #[test] - fn execute_program_dispatches_json_encode_stdclass_object() { - let program = parse_fragment( - br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +/// Verifies eval `json_encode()` serializes stdClass dynamic properties. +#[test] +fn execute_program_dispatches_json_encode_stdclass_object() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); echo json_encode($object) . ":"; echo str_replace("\n", "|", json_encode($object, JSON_PRETTY_PRINT)) . ":"; $empty = json_decode("{}"); @@ -3335,24 +3303,24 @@ echo json_encode($empty) . ":"; $empty->a = 7; echo json_encode($empty); return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `json_last_error*()` track JSON parse failures and success resets. - #[test] - fn execute_program_dispatches_json_last_error_builtins() { - let program = parse_fragment( +/// Verifies eval `json_last_error*()` track JSON parse failures and success resets. +#[test] +fn execute_program_dispatches_json_last_error_builtins() { + let program = parse_fragment( br#"echo json_last_error() . ":" . json_last_error_msg() . ":"; json_decode("bad"); echo json_last_error() . ":" . (json_last_error() === JSON_ERROR_SYNTAX ? "syntax" : "bad") . ":" . json_last_error_msg() . ":"; @@ -3371,23 +3339,23 @@ echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_e return function_exists("json_last_error") && function_exists("json_last_error_msg") && defined("JSON_ERROR_SYNTAX");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, "0:No error:4:syntax:Syntax error near location 1:1:1:Maximum stack depth exceeded near location 1:1:0:No error:3:Control character error, possibly incorrectly encoded near location 1:3:10:Single unpaired UTF-16 surrogate in unicode escape near location 1:8:5:Malformed UTF-8 characters, possibly incorrectly encoded near location 1:3:0:No error:" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval JSON throw flags raise catchable Throwable objects. - #[test] - fn execute_program_dispatches_json_throw_on_error() { - let program = parse_fragment( - br#"try { +/// Verifies eval JSON throw flags raise catchable Throwable objects. +#[test] +fn execute_program_dispatches_json_throw_on_error() { + let program = parse_fragment( + br#"try { json_decode("bad", true, 512, JSON_THROW_ON_ERROR); echo "bad"; } catch (Throwable) { @@ -3401,21 +3369,21 @@ try { } echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; return defined("JSON_THROW_ON_ERROR");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "decode:encode:0:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "decode:encode:0:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. - #[test] - fn execute_program_dispatches_json_validate_builtin() { - let program = parse_fragment( +/// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. +#[test] +fn execute_program_dispatches_json_validate_builtin() { + let program = parse_fragment( br#"echo (json_validate("{\"a\":[1,true,null,\"caf\\u00e9\"]}") ? "Y" : "N") . ":"; echo (json_validate("bad") ? "bad" : "N") . ":"; echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; @@ -3428,60 +3396,60 @@ echo json_last_error() . ":"; return function_exists("json_validate") && defined("JSON_INVALID_UTF8_IGNORE");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Y:N:D:C:A:I:0:S:4:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "Y:N:D:C:A:I:0:S:4:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies direct eval builtin calls bind named and unpacked arguments. - #[test] - fn execute_program_dispatches_named_and_spread_builtins() { - let program = parse_fragment( - br#"echo strlen(string: "abcd"); +/// Verifies direct eval builtin calls bind named and unpacked arguments. +#[test] +fn execute_program_dispatches_named_and_spread_builtins() { + let program = parse_fragment( + br#"echo strlen(string: "abcd"); echo ":" . (array_key_exists(array: ["name" => 1], key: "name") ? "Y" : "N"); echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N"); return round(precision: 1, num: 3.14);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "4:Y:Y"); - assert_eq!(values.get(result), FakeValue::Float(3.1)); - } + assert_eq!(values.output, "4:Y:Y"); + assert_eq!(values.get(result), FakeValue::Float(3.1)); +} - /// Verifies eval `ord()` returns the first byte and supports callable dispatch. - #[test] - fn execute_program_dispatches_ord_builtin() { - let program = parse_fragment( - br#"echo ord("A"); +/// Verifies eval `ord()` returns the first byte and supports callable dispatch. +#[test] +fn execute_program_dispatches_ord_builtin() { + let program = parse_fragment( + br#"echo ord("A"); echo ":" . ord(""); echo ":" . call_user_func("ord", "B"); echo ":" . call_user_func_array("ord", ["C"]); echo ":"; echo function_exists("ord"); return ord("Z");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "65:0:66:67:1"); - assert_eq!(values.get(result), FakeValue::Int(90)); - } + assert_eq!(values.output, "65:0:66:67:1"); + assert_eq!(values.get(result), FakeValue::Int(90)); +} - /// Verifies eval array aggregate builtins iterate array values and support callable dispatch. - #[test] - fn execute_program_dispatches_array_aggregate_builtins() { - let program = parse_fragment( - br#"echo array_sum([1, 2, 3]); +/// Verifies eval array aggregate builtins iterate array values and support callable dispatch. +#[test] +fn execute_program_dispatches_array_aggregate_builtins() { + let program = parse_fragment( + br#"echo array_sum([1, 2, 3]); echo ":" . array_product([2, 3, 4]); echo ":" . array_sum([]); echo ":" . array_product([]); @@ -3490,22 +3458,22 @@ echo ":" . call_user_func("array_sum", [3, 4]); echo ":" . call_user_func_array("array_product", [[2, 5]]); echo ":"; echo function_exists("array_sum"); return function_exists("array_product");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "6:24:0:1:7:7:10:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "6:24:0:1:7:7:10:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_map()` applies callbacks and preserves source keys. - #[test] - fn execute_program_dispatches_array_map_builtin() { - let program = parse_fragment( - br#"function eval_map_double($value) { return $value * 2; } +/// Verifies eval `array_map()` applies callbacks and preserves source keys. +#[test] +fn execute_program_dispatches_array_map_builtin() { + let program = parse_fragment( + br#"function eval_map_double($value) { return $value * 2; } $mapped = array_map("eval_map_double", [1, 2, 3]); echo $mapped[0] . ":" . $mapped[2] . ":"; $assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); @@ -3524,21 +3492,21 @@ echo $multi_call[0] . ":"; $spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); echo $spread[0] . ":"; return function_exists("array_map");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_reduce()` folds values through a string callback. - #[test] - fn execute_program_dispatches_array_reduce_builtin() { - let program = parse_fragment( +/// Verifies eval `array_reduce()` folds values through a string callback. +#[test] +fn execute_program_dispatches_array_reduce_builtin() { + let program = parse_fragment( br#"function eval_reduce_sum($carry, $item) { return $carry + $item; } echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; function eval_reduce_join($carry, $item) { return $carry . $item; } @@ -3553,19 +3521,19 @@ echo $spread . ":"; return function_exists("array_reduce");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "16:9:ab:13:9:9:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "16:9:ab:13:9:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_walk()` invokes string callbacks with value and key cells. - #[test] - fn execute_program_dispatches_array_walk_builtin() { - let program = parse_fragment( +/// Verifies eval `array_walk()` invokes string callbacks with value and key cells. +#[test] +fn execute_program_dispatches_array_walk_builtin() { + let program = parse_fragment( br#"function eval_walk_show($value, $key) { echo $key . "=" . $value . ";"; } echo array_walk(["a" => 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; $call = call_user_func("array_walk", [4, 5], "eval_walk_show"); @@ -3573,20 +3541,20 @@ $spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" return function_exists("array_walk");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_pop()` and `array_shift()` write back only for direct variable calls. - #[test] - fn execute_program_dispatches_array_pop_shift_builtins() { - let program = parse_fragment( - br#"$a = [1, 2, 3]; +/// Verifies eval `array_pop()` and `array_shift()` write back only for direct variable calls. +#[test] +fn execute_program_dispatches_array_pop_shift_builtins() { + let program = parse_fragment( + br#"$a = [1, 2, 3]; echo array_pop($a) . ":" . count($a) . ":" . $a[1] . ":"; $b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; @@ -3595,29 +3563,29 @@ echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; $d = [6, 7]; echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; return function_exists("array_pop") && function_exists("array_shift");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:2:2:1:2:3:4:5:2:5:6:2:6:"); - assert_eq!( - values.warnings, - vec![ - "array_pop(): Argument #1 ($array) must be passed by reference, value given", - "array_shift(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:2:2:1:2:3:4:5:2:5:6:2:6:"); + assert_eq!( + values.warnings, + vec![ + "array_pop(): Argument #1 ($array) must be passed by reference, value given", + "array_shift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_push()` and `array_unshift()` write back direct variable calls. - #[test] - fn execute_program_dispatches_array_push_unshift_builtins() { - let program = parse_fragment( - br#"$a = [1]; +/// Verifies eval `array_push()` and `array_unshift()` write back direct variable calls. +#[test] +fn execute_program_dispatches_array_push_unshift_builtins() { + let program = parse_fragment( + br#"$a = [1]; echo array_push($a, 2, 3) . ":" . $a[2] . ":"; $b = ["x" => 1, 10 => 2]; echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; @@ -3630,28 +3598,28 @@ echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; $f = [7]; echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; return function_exists("array_push") && function_exists("array_unshift");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:"); - assert_eq!( - values.warnings, - vec![ - "array_push(): Argument #1 ($array) must be passed by reference, value given", - "array_unshift(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:"); + assert_eq!( + values.warnings, + vec![ + "array_push(): Argument #1 ($array) must be passed by reference, value given", + "array_unshift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_splice()` returns removed values and writes back direct variable calls. - #[test] - fn execute_program_dispatches_array_splice_builtin() { - let program = parse_fragment( +/// Verifies eval `array_splice()` returns removed values and writes back direct variable calls. +#[test] +fn execute_program_dispatches_array_splice_builtin() { + let program = parse_fragment( br#"$a = [10, 20, 30, 40]; $removed = array_splice($a, 1, 2); echo count($removed) . ":" . $removed[0] . ":" . $removed[1] . ":" . count($a) . ":" . $a[1] . ":"; @@ -3679,31 +3647,31 @@ echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h return function_exists("array_splice");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:" - ); - assert_eq!( - values.warnings, - vec![ - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:" + ); + assert_eq!( + values.warnings, + vec![ + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `sort()` and `rsort()` reindex direct variable arrays only. - #[test] - fn execute_program_dispatches_sort_builtins() { - let program = parse_fragment( - br#"$a = [3, 1, 2]; +/// Verifies eval `sort()` and `rsort()` reindex direct variable arrays only. +#[test] +fn execute_program_dispatches_sort_builtins() { + let program = parse_fragment( + br#"$a = [3, 1, 2]; echo sort($a) . ":" . $a[0] . $a[1] . $a[2] . ":"; $b = ["banana", "apple", "cherry"]; echo rsort(array: $b) . ":" . $b[0] . ":" . $b[2] . ":"; @@ -3715,28 +3683,28 @@ echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; $e = [1, 2, 3]; echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; return function_exists("sort") && function_exists("rsort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:123:1:cherry:apple:123:1:312:1:1:3:"); - assert_eq!( - values.warnings, - vec![ - "sort(): Argument #1 ($array) must be passed by reference, value given", - "rsort(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:123:1:cherry:apple:123:1:312:1:1:3:"); + assert_eq!( + values.warnings, + vec![ + "sort(): Argument #1 ($array) must be passed by reference, value given", + "rsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval key-preserving array ordering builtins write back direct variable calls. - #[test] - fn execute_program_dispatches_key_preserving_sort_builtins() { - let program = parse_fragment( +/// Verifies eval key-preserving array ordering builtins write back direct variable calls. +#[test] +fn execute_program_dispatches_key_preserving_sort_builtins() { + let program = parse_fragment( br#"$a = ["x" => 3, "y" => 1, "z" => 2]; echo asort($a) . ":"; foreach ($a as $key => $value) { echo $key . $value; } @@ -3760,30 +3728,30 @@ echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . return function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:" - ); - assert_eq!( - values.warnings, - vec![ - "asort(): Argument #1 ($array) must be passed by reference, value given", - "krsort(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!( + values.output, + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:" + ); + assert_eq!( + values.warnings, + vec![ + "asort(): Argument #1 ($array) must be passed by reference, value given", + "krsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval natural sort builtins preserve keys and use natural string order. - #[test] - fn execute_program_dispatches_natural_sort_builtins() { - let program = parse_fragment( - br#"$a = ["img10", "img2", "img1"]; +/// Verifies eval natural sort builtins preserve keys and use natural string order. +#[test] +fn execute_program_dispatches_natural_sort_builtins() { + let program = parse_fragment( + br#"$a = ["img10", "img2", "img1"]; echo natsort($a) . ":"; foreach ($a as $key => $value) { echo $key . $value . ";"; } echo ":"; @@ -3794,28 +3762,28 @@ echo ":"; $c = ["x" => "b", "y" => "a"]; echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; return function_exists("natsort") && function_exists("natcasesort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:" - ); - assert_eq!( - values.warnings, - vec!["natsort(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:" + ); + assert_eq!( + values.warnings, + vec!["natsort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `shuffle()` reindexes direct variable arrays only. - #[test] - fn execute_program_dispatches_shuffle_builtin() { - let program = parse_fragment( +/// Verifies eval `shuffle()` reindexes direct variable arrays only. +#[test] +fn execute_program_dispatches_shuffle_builtin() { + let program = parse_fragment( br#"$a = ["x" => 1, "y" => 2]; echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; $b = ["x" => 1, "y" => 2]; @@ -3823,24 +3791,24 @@ echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; return function_exists("shuffle");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1:reindexed:2:3:1:12:"); - assert_eq!( - values.warnings, - vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "1:reindexed:2:3:1:12:"); + assert_eq!( + values.warnings, + vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval user-comparator sort builtins call callbacks and write back direct arrays. - #[test] - fn execute_program_dispatches_user_sort_builtins() { - let program = parse_fragment( - br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } +/// Verifies eval user-comparator sort builtins call callbacks and write back direct arrays. +#[test] +fn execute_program_dispatches_user_sort_builtins() { + let program = parse_fragment( + br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } function eval_key_cmp($left, $right) { return strcmp($left, $right); } $a = [3, 1, 2]; echo usort($a, "eval_sort_cmp") . ":"; @@ -3857,25 +3825,25 @@ echo ":"; $d = [2, 1]; echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; return function_exists("usort") && function_exists("uasort") && function_exists("uksort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:"); - assert_eq!( - values.warnings, - vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:"); + assert_eq!( + values.warnings, + vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval iterator array helpers support direct and dynamic builtin calls. - #[test] - fn execute_program_dispatches_iterator_array_builtins() { - let program = parse_fragment( +/// Verifies eval iterator array helpers support direct and dynamic builtin calls. +#[test] +fn execute_program_dispatches_iterator_array_builtins() { + let program = parse_fragment( br#"$items = ["x" => 1, "y" => 2]; $copy = iterator_to_array($items); echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; @@ -3887,89 +3855,89 @@ echo $spread[0] . $spread[1] . ":"; return function_exists("iterator_count") && function_exists("iterator_to_array");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:12:reindexed:12:2:12:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "2:12:reindexed:12:2:12:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `iterator_apply()` drives Iterator objects and callback args. - #[test] - fn execute_program_dispatches_iterator_apply_object_builtin() { - let program = parse_fragment( - br#"function eval_apply($prefix) { echo $prefix; return true; } +/// Verifies eval `iterator_apply()` drives Iterator objects and callback args. +#[test] +fn execute_program_dispatches_iterator_apply_object_builtin() { + let program = parse_fragment( + br#"function eval_apply($prefix) { echo $prefix; return true; } echo iterator_apply($it, "eval_apply", ["prefix" => "x"]) . ":"; echo call_user_func("iterator_apply", $it, "eval_apply", ["y"]) . ":"; return function_exists("iterator_apply");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "xxx3:yyy3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xxx3:yyy3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `iterator_apply()` accepts object-method callable arrays. - #[test] - fn execute_program_iterator_apply_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(5); +/// Verifies eval `iterator_apply()` accepts object-method callable arrays. +#[test] +fn execute_program_iterator_apply_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(5); echo iterator_apply($it, [$box, "add_x"], [1]) . ":"; return call_user_func("iterator_apply", $it, [$box, "add_x"], [1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:"); - assert_eq!(values.get(result), FakeValue::Int(3)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} - /// Verifies eval `iterator_apply()` counts the position where the callback stops. - #[test] - fn execute_program_iterator_apply_stops_on_falsey_callback() { - let program = parse_fragment( - br#"function eval_stop() { echo "s"; return false; } +/// Verifies eval `iterator_apply()` counts the position where the callback stops. +#[test] +fn execute_program_iterator_apply_stops_on_falsey_callback() { + let program = parse_fragment( + br#"function eval_stop() { echo "s"; return false; } return iterator_apply($it, "eval_stop");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "s"); - assert_eq!(values.get(result), FakeValue::Int(1)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "s"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} - /// Verifies eval `array_filter()` removes falsey values while preserving original keys. - #[test] - fn execute_program_dispatches_array_filter_builtin() { - let program = parse_fragment( - br#"$filtered = array_filter([0, 1, 2, "", false, null, "0", "ok"]); +/// Verifies eval `array_filter()` removes falsey values while preserving original keys. +#[test] +fn execute_program_dispatches_array_filter_builtin() { + let program = parse_fragment( + br#"$filtered = array_filter([0, 1, 2, "", false, null, "0", "ok"]); echo count($filtered) . ":" . $filtered[1] . ":" . $filtered[2] . ":" . $filtered[7] . ":"; $assoc = array_filter(["a" => 0, "b" => 2, "c" => ""]); echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; @@ -3991,25 +3959,25 @@ echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; $ints = array_filter([1, "x", 2], "is_int"); echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; return function_exists("array_filter");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_combine()` converts key values through PHP string-key rules. - #[test] - fn execute_program_dispatches_array_combine_builtin() { - let program = parse_fragment( - br#"$pairs = array_combine(["a", "b"], [10, 20]); +/// Verifies eval `array_combine()` converts key values through PHP string-key rules. +#[test] +fn execute_program_dispatches_array_combine_builtin() { + let program = parse_fragment( + br#"$pairs = array_combine(["a", "b"], [10, 20]); echo $pairs["a"] . ":" . $pairs["b"]; $numeric = array_combine(["1", "01"], ["n", "z"]); echo ":" . $numeric[1] . $numeric["01"]; @@ -4022,20 +3990,20 @@ echo ":" . $call["x"]; $spread = call_user_func_array("array_combine", [["y"], [8]]); echo ":" . $spread["y"] . ":"; return function_exists("array_combine");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "10:20:nz:ftd:v:7:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:nz:ftd:v:7:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_column()` extracts present row columns and reindexes them. - #[test] - fn execute_program_dispatches_array_column_builtin() { - let program = parse_fragment( +/// Verifies eval `array_column()` extracts present row columns and reindexes them. +#[test] +fn execute_program_dispatches_array_column_builtin() { + let program = parse_fragment( br#"$rows = [["name" => "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; $names = array_column($rows, "name"); echo count($names) . ":" . $names[0] . ":" . $names[1]; @@ -4052,19 +4020,19 @@ echo ":" . count($spread) . ":" . $spread[1] . ":"; return function_exists("array_column");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. - #[test] - fn execute_program_dispatches_array_shape_builtins() { - let program = parse_fragment( - br#"$right = array_pad([1, 2], 5, 0); +/// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. +#[test] +fn execute_program_dispatches_array_shape_builtins() { + let program = parse_fragment( + br#"$right = array_pad([1, 2], 5, 0); echo count($right) . ":" . $right[0] . $right[1] . $right[2] . $right[4]; $left = array_pad([1, 2], -4, 9); echo ":" . $left[0] . $left[1] . $left[2] . $left[3]; @@ -4079,22 +4047,22 @@ echo ":" . $call[1][0]; $spread = call_user_func_array("array_pad", [[1], 3, 2]); echo ":" . $spread[2] . ":"; return function_exists("array_pad") && function_exists("array_chunk");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "5:1200:9912:2:78:3:25:b:8:2:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "5:1200:9912:2:78:3:25:b:8:2:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_slice()` observes PHP offset and length bounds. - #[test] - fn execute_program_dispatches_array_slice_builtin() { - let program = parse_fragment( - br#"$mid = array_slice([10, 20, 30, 40, 50], 1, 3); +/// Verifies eval `array_slice()` observes PHP offset and length bounds. +#[test] +fn execute_program_dispatches_array_slice_builtin() { + let program = parse_fragment( + br#"$mid = array_slice([10, 20, 30, 40, 50], 1, 3); echo count($mid) . ":" . $mid[0] . $mid[1] . $mid[2]; $tail = array_slice([10, 20, 30, 40], -2, 1); echo ":" . $tail[0]; @@ -4111,22 +4079,22 @@ echo ":" . $call[1]; $spread = call_user_func_array("array_slice", [[9, 10, 11], 1]); echo ":" . $spread[0] . ":"; return function_exists("array_slice");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:203040:30:3:3050:67:3:2040:2:8:10:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "3:203040:30:3:3050:67:3:2040:2:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. - #[test] - fn execute_program_dispatches_array_merge_builtin() { - let program = parse_fragment( - br#"$merged = array_merge([1, 2], [3, 4]); +/// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. +#[test] +fn execute_program_dispatches_array_merge_builtin() { + let program = parse_fragment( + br#"$merged = array_merge([1, 2], [3, 4]); echo count($merged) . ":" . $merged[0] . $merged[1] . $merged[2] . $merged[3]; $left = [1, 2]; $right = [3]; @@ -4139,22 +4107,22 @@ echo ":" . $call[2]; $spread = call_user_func_array("array_merge", [[9], [10]]); echo ":" . $spread[1] . ":"; return function_exists("array_merge");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "4:1234:2:1:3:9:x:y:3:8:10:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "4:1234:2:1:3:9:x:y:3:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_diff()` and `array_intersect()` compare values as strings. - #[test] - fn execute_program_dispatches_array_value_set_builtins() { - let program = parse_fragment( - br#"$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); +/// Verifies eval `array_diff()` and `array_intersect()` compare values as strings. +#[test] +fn execute_program_dispatches_array_value_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); @@ -4165,22 +4133,22 @@ echo ":" . count($call) . ":" . $call[0] . $call[2]; $spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); echo ":" . count($spread) . ":" . $spread[2] . ":"; return function_exists("array_diff") && function_exists("array_intersect");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. - #[test] - fn execute_program_dispatches_array_key_set_builtins() { - let program = parse_fragment( - br#"$diff = array_diff_key(["a" => 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); +/// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. +#[test] +fn execute_program_dispatches_array_key_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff_key(["a" => 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); $inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); @@ -4190,22 +4158,22 @@ echo ":" . count($call) . ":" . $call[0] . $call[2]; $spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); echo ":" . count($spread) . ":" . $spread["y"] . ":"; return function_exists("array_diff_key") && function_exists("array_intersect_key");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:2:3:no-a:2:2:3:2:1030:1:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "2:2:3:no-a:2:2:3:2:1030:1:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `range()` builds inclusive ascending and descending integer arrays. - #[test] - fn execute_program_dispatches_range_builtin() { - let program = parse_fragment( - br#"$up = range(1, 4); +/// Verifies eval `range()` builds inclusive ascending and descending integer arrays. +#[test] +fn execute_program_dispatches_range_builtin() { + let program = parse_fragment( + br#"$up = range(1, 4); echo count($up) . ":" . $up[0] . $up[3]; $down = range(4, 1); echo ":" . count($down) . ":" . $down[0] . $down[3]; @@ -4218,22 +4186,22 @@ echo ":" . $call[2]; $spread = call_user_func_array("range", [8, 6]); echo ":" . count($spread) . ":" . $spread[0] . $spread[2] . ":"; return function_exists("range");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "4:14:4:41:1:3:24:7:3:86:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "4:14:4:41:1:3:24:7:3:86:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_rand()` returns a key that exists in the source array. - #[test] - fn execute_program_dispatches_array_rand_builtin() { - let program = parse_fragment( - br#"$nums = [10, 20, 30]; +/// Verifies eval `array_rand()` returns a key that exists in the source array. +#[test] +fn execute_program_dispatches_array_rand_builtin() { + let program = parse_fragment( + br#"$nums = [10, 20, 30]; $idx = array_rand($nums); echo ($idx >= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; $assoc = ["a" => 1, "b" => 2]; @@ -4246,22 +4214,22 @@ echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); $spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; return function_exists("array_rand");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "idx:assoc:named:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "idx:assoc:named:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval random builtins return values inside PHP inclusive ranges. - #[test] - fn execute_program_dispatches_rand_builtins() { - let program = parse_fragment( - br#"$plain = rand(); +/// Verifies eval random builtins return values inside PHP inclusive ranges. +#[test] +fn execute_program_dispatches_rand_builtins() { + let program = parse_fragment( + br#"$plain = rand(); echo ($plain >= 0 && $plain <= 2147483647) ? "plain" : "bad"; $bounded = rand(2, 4); echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); @@ -4279,28 +4247,28 @@ $secureCall = call_user_func("random_int", 5, 5); echo ($secureCall === 5 ? "random-call" : "bad") . ":"; $secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; -echo function_exists("rand"); -echo function_exists("mt_rand"); -return function_exists("random_int");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "plain:range:same:swap:call:spread:random:random-call:random-spread:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } +echo function_exists("rand"); +echo function_exists("mt_rand"); +return function_exists("random_int");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "plain:range:same:swap:call:spread:random:random-call:random-spread:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. - #[test] - fn execute_program_dispatches_array_fill_builtins() { - let program = parse_fragment( - br#"$filled = array_fill(2, 3, "x"); +/// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. +#[test] +fn execute_program_dispatches_array_fill_builtins() { + let program = parse_fragment( + br#"$filled = array_fill(2, 3, "x"); echo count($filled) . ":" . $filled[2] . $filled[4]; $negative = array_fill(-2, 3, 7); echo ":" . $negative[-2] . $negative[-1] . $negative[0]; @@ -4315,21 +4283,21 @@ echo ":" . $call[0] . $call[1]; $spread = call_user_func_array("array_fill_keys", [["x", "y"], "z"]); echo ":" . $spread["x"] . $spread["y"] . ":"; return function_exists("array_fill") && function_exists("array_fill_keys");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:xx:777:0:8:8:8:nn:cc:zz:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "3:xx:777:0:8:8:8:nn:cc:zz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. - #[test] - fn execute_program_dispatches_array_flip_builtin() { - let program = parse_fragment( +/// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. +#[test] +fn execute_program_dispatches_array_flip_builtin() { + let program = parse_fragment( br#"$flipped = array_flip(["a" => "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); $named = array_flip(array: ["k" => "v"]); @@ -4341,19 +4309,19 @@ echo ":" . $spread[9] . ":"; return function_exists("array_flip");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "c:b:d:e:4:k:left:n:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "c:b:d:e:4:k:left:n:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_unique()` preserves first keys using default string comparison. - #[test] - fn execute_program_dispatches_array_unique_builtin() { - let program = parse_fragment( - br#"$unique = array_unique(["a", "b", "a", "2", 2]); +/// Verifies eval `array_unique()` preserves first keys using default string comparison. +#[test] +fn execute_program_dispatches_array_unique_builtin() { + let program = parse_fragment( + br#"$unique = array_unique(["a", "b", "a", "2", 2]); echo count($unique) . ":" . $unique[0] . $unique[1] . $unique[3]; $assoc = array_unique(["x" => "a", "y" => "b", "z" => "a"]); echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; @@ -4367,21 +4335,21 @@ echo ":" . $call[0] . $call[2]; $spread = call_user_func_array("array_unique", [["s", "s", "t"]]); echo ":" . $spread[0] . $spread[2] . ":"; return function_exists("array_unique");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:ab2:2:ab:2:1:F:v:1:qr:st:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:ab2:2:ab:2:1:F:v:1:qr:st:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval array projection builtins produce indexed key/value arrays. - #[test] - fn execute_program_dispatches_array_projection_builtins() { - let program = parse_fragment( - br#"$values = array_values(["a" => 10, "b" => 20]); +/// Verifies eval array projection builtins produce indexed key/value arrays. +#[test] +fn execute_program_dispatches_array_projection_builtins() { + let program = parse_fragment( + br#"$values = array_values(["a" => 10, "b" => 20]); echo $values[0] . ":" . $values[1]; $keys = array_keys(["a" => 10, "b" => 20]); echo ":" . $keys[0] . ":" . $keys[1]; @@ -4392,22 +4360,22 @@ $call_values = call_user_func_array("array_values", [["q" => 8]]); echo ":" . $call_values[0]; echo ":"; echo function_exists("array_keys"); return function_exists("array_values");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "10:20:a:b:0:z:8:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "10:20:a:b:0:z:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_reverse()` handles PHP key preservation rules. - #[test] - fn execute_program_dispatches_array_reverse_builtin() { - let program = parse_fragment( - br#"$indexed = array_reverse([1, 2, 3]); +/// Verifies eval `array_reverse()` handles PHP key preservation rules. +#[test] +fn execute_program_dispatches_array_reverse_builtin() { + let program = parse_fragment( + br#"$indexed = array_reverse([1, 2, 3]); echo $indexed[0]; echo $indexed[1]; echo $indexed[2]; echo ":"; $mixed = array_reverse([2 => "a", "k" => "b", 5 => "c"]); echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; @@ -4420,22 +4388,22 @@ echo $call[0]; echo $call[1]; echo ":"; $spread = call_user_func_array("array_reverse", [[6, 7]]); echo $spread[0]; echo $spread[1]; echo ":"; return function_exists("array_reverse");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "321:cba:cba:yx:54:76:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "321:cba:cba:yx:54:76:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. - #[test] - fn execute_program_dispatches_array_key_exists_builtin() { - let program = parse_fragment( - br#"$map = ["name" => null, "age" => 30]; +/// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. +#[test] +fn execute_program_dispatches_array_key_exists_builtin() { + let program = parse_fragment( + br#"$map = ["name" => null, "age" => 30]; echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; @@ -4443,22 +4411,22 @@ echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; echo ":"; return function_exists("array_key_exists");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Y:N:Y:N:Y:Y:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "Y:N:Y:N:Y:Y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval array search builtins use loose comparison and return keys or booleans. - #[test] - fn execute_program_dispatches_array_search_builtins() { - let program = parse_fragment( - br#"echo in_array(2, [1, 2, 3]) ? "Y" : "bad"; +/// Verifies eval array search builtins use loose comparison and return keys or booleans. +#[test] +fn execute_program_dispatches_array_search_builtins() { + let program = parse_fragment( + br#"echo in_array(2, [1, 2, 3]) ? "Y" : "bad"; echo ":"; echo in_array(4, [1, 2, 3]) ? "bad" : "N"; echo ":" . array_search(20, [10, 20, 30]); echo ":" . array_search("Grace", ["name" => "Grace"]); @@ -4468,22 +4436,22 @@ $found = call_user_func_array("array_search", ["v", ["k" => "v"]]); echo ":" . $found; echo ":"; echo function_exists("in_array"); return function_exists("array_search");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Y:N:1:name:F:C:k:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "Y:N:1:name:F:C:k:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. - #[test] - fn execute_program_dispatches_explode_implode_builtins() { - let program = parse_fragment( - br#"$parts = explode(",", "a,b,"); +/// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. +#[test] +fn execute_program_dispatches_explode_implode_builtins() { + let program = parse_fragment( + br#"$parts = explode(",", "a,b,"); echo count($parts); echo ":" . $parts[0] . ":" . $parts[1] . ":" . $parts[2]; echo ":" . implode("|", $parts); echo ":" . implode(separator: "-", array: ["x", 2, true, null]); @@ -4492,22 +4460,22 @@ echo ":" . $call_parts[1]; echo ":" . call_user_func_array("implode", ["separator" => "/", "array" => ["p", "q"]]); echo ":"; echo function_exists("explode"); return function_exists("implode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:a:b::a|b|:x-2-1-:n:p/q:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "3:a:b::a|b|:x-2-1-:n:p/q:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `str_split()` builds indexed arrays of fixed-width chunks. - #[test] - fn execute_program_dispatches_str_split_builtin() { - let program = parse_fragment( - br#"$letters = str_split("abc"); +/// Verifies eval `str_split()` builds indexed arrays of fixed-width chunks. +#[test] +fn execute_program_dispatches_str_split_builtin() { + let program = parse_fragment( + br#"$letters = str_split("abc"); echo count($letters) . ":" . $letters[0] . $letters[1] . $letters[2]; echo ":"; $pairs = str_split(string: "abcd", length: 2); echo $pairs[0] . "-" . $pairs[1]; echo ":"; @@ -4518,21 +4486,21 @@ echo $call[0] . "-" . $call[1]; echo ":"; $named = call_user_func_array("str_split", ["string" => "pqrs", "length" => 3]); echo $named[0] . "-" . $named[1]; echo ":"; return function_exists("str_split");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:abc:ab-cd:0:xy-z:pqr-s:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "3:abc:ab-cd:0:xy-z:pqr-s:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `str_pad()` supports PHP left, right, and both-side padding modes. - #[test] - fn execute_program_dispatches_str_pad_builtin() { - let program = parse_fragment( +/// Verifies eval `str_pad()` supports PHP left, right, and both-side padding modes. +#[test] +fn execute_program_dispatches_str_pad_builtin() { + let program = parse_fragment( br#"echo "[" . str_pad("hi", 5) . "]"; echo ":"; echo "[" . str_pad(string: "hi", length: 5, pad_string: "_", pad_type: 0) . "]"; echo ":"; echo "[" . str_pad("x", 6, "ab", 2) . "]"; echo ":"; @@ -4541,19 +4509,19 @@ echo call_user_func_array("str_pad", ["string" => "x", "length" => 3, "pad_strin return function_exists("str_pad");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "[hi ]:[___hi]:[abxaba]:00042:x..:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "[hi ]:[___hi]:[abxaba]:00042:x..:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval string replacement builtins support direct, named, and callable dispatch. - #[test] - fn execute_program_dispatches_string_replace_builtins() { - let program = parse_fragment( +/// Verifies eval string replacement builtins support direct, named, and callable dispatch. +#[test] +fn execute_program_dispatches_string_replace_builtins() { + let program = parse_fragment( br#"echo str_replace("o", "0", "Hello World"); echo ":"; echo str_replace(search: "aa", replace: "b", subject: "aaaa"); echo ":"; echo str_replace("", "x", "abc"); echo ":"; @@ -4564,19 +4532,19 @@ echo function_exists("str_replace"); return function_exists("str_ireplace");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. - #[test] - fn execute_program_dispatches_preg_builtins() { - let program = parse_fragment( +/// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. +#[test] +fn execute_program_dispatches_preg_builtins() { + let program = parse_fragment( br#"$ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; echo preg_match("/xyz/", "id42") . ":"; @@ -4622,48 +4590,48 @@ echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. - #[test] - fn execute_program_dispatches_html_entity_builtins() { - let program = parse_fragment( - br#"echo htmlspecialchars("\"Hi\" & 'bye'"); echo ":"; +/// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. +#[test] +fn execute_program_dispatches_html_entity_builtins() { + let program = parse_fragment( + br#"echo htmlspecialchars("\"Hi\" & 'bye'"); echo ":"; echo htmlentities(string: "
"); echo ":"; echo html_entity_decode("<b>hi</b>"); echo ":"; echo call_user_func("htmlspecialchars", ""); echo ":"; echo call_user_func_array("html_entity_decode", ["string" => ""q""]); echo ":"; echo function_exists("htmlspecialchars"); echo function_exists("htmlentities"); return function_exists("html_entity_decode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":11" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval URL codec builtins dispatch through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_url_codec_builtins() { - let program = parse_fragment( - br#"echo urlencode("a b&=~"); echo ":"; +/// Verifies eval URL codec builtins dispatch through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_url_codec_builtins() { + let program = parse_fragment( + br#"echo urlencode("a b&=~"); echo ":"; echo rawurlencode(string: "a b&=~"); echo ":"; echo urldecode("a+b%26%3D%7E"); echo ":"; echo rawurldecode("a+b%26%3D%7E"); echo ":"; @@ -4672,25 +4640,25 @@ echo call_user_func_array("rawurldecode", ["string" => "x%2By%zz"]); echo ":"; echo function_exists("urlencode"); echo function_exists("rawurlencode"); echo function_exists("urldecode"); return function_exists("rawurldecode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_ctype_builtins() { - let program = parse_fragment( - br#"echo ctype_alpha("abc") ? "A" : "-"; echo ":"; +/// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_ctype_builtins() { + let program = parse_fragment( + br#"echo ctype_alpha("abc") ? "A" : "-"; echo ":"; echo ctype_digit(text: "123") ? "D" : "-"; echo ":"; echo ctype_alnum("a1") ? "N" : "-"; echo ":"; echo ctype_space(" \t\n" . chr(11) . chr(12) . "\r") ? "S" : "-"; echo ":"; @@ -4700,21 +4668,21 @@ echo call_user_func_array("ctype_space", ["text" => " x"]) ? "bad" : "not-space" echo function_exists("ctype_alpha"); echo function_exists("ctype_digit"); echo function_exists("ctype_alnum"); return function_exists("ctype_space");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "A:D:N:S:empty:not-digit:not-space:111"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "A:D:N:S:empty:not-digit:not-space:111"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. - #[test] - fn execute_program_dispatches_crc32_builtin() { - let program = parse_fragment( +/// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. +#[test] +fn execute_program_dispatches_crc32_builtin() { + let program = parse_fragment( br#"echo crc32(""); echo ":"; echo crc32(string: "123456789"); echo ":"; echo call_user_func("crc32", "hello"); echo ":"; @@ -4722,20 +4690,20 @@ echo call_user_func_array("crc32", ["string" => "The quick brown fox jumps over return function_exists("crc32");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "0:3421780262:907060870:1095738169:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "0:3421780262:907060870:1095738169:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `hash_algos()` returns supported hash names through callable dispatch too. - #[test] - fn execute_program_dispatches_hash_algos_builtin() { - let program = parse_fragment( - br#"$algos = hash_algos(); +/// Verifies eval `hash_algos()` returns supported hash names through callable dispatch too. +#[test] +fn execute_program_dispatches_hash_algos_builtin() { + let program = parse_fragment( + br#"$algos = hash_algos(); echo count($algos) . ":" . $algos[0] . ":" . $algos[5] . ":"; echo in_array("crc32c", $algos) ? "crc" : "bad"; $call = call_user_func("hash_algos"); @@ -4744,23 +4712,23 @@ $spread = call_user_func_array("hash_algos", []); echo ":" . $spread[27] . ":"; echo function_exists("hash_algos") ? "exists" : "missing"; return count($algos);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "28:md2:sha256:crc:whirlpool:joaat:exists"); - assert_eq!(values.get(result), FakeValue::Int(28)); - } + assert_eq!(values.output, "28:md2:sha256:crc:whirlpool:joaat:exists"); + assert_eq!(values.get(result), FakeValue::Int(28)); +} - /// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. - #[test] - fn execute_program_dispatches_hash_digest_builtins() { - let filename = format!("elephc_eval_hash_file_{}.txt", std::process::id()); - let source = format!( - r#"echo md5("abc"); echo ":"; +/// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. +#[test] +fn execute_program_dispatches_hash_digest_builtins() { + let filename = format!("elephc_eval_hash_file_{}.txt", std::process::id()); + let source = format!( + r#"echo md5("abc"); echo ":"; echo sha1(string: "abc"); echo ":"; echo hash("sha256", "abc"); echo ":"; echo hash_hmac(algo: "sha256", data: "data", key: "key"); echo ":"; @@ -4776,39 +4744,39 @@ echo hash_file("sha256", "{filename}.missing") === false ? "missing" : "bad"; ec unlink("{filename}"); echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); return function_exists("hash_hmac");"#, - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - concat!( - "900150983cd24fb0d6963f7d28e17f72:", - "a9993e364706816aba3e25717850c26c9cd0d89d:", - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", - "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", - "900150983cd24fb0d6963f7d28e17f72:", - "a9993e364706816aba3e25717850c26c9cd0d89d:", - "900150983cd24fb0d6963f7d28e17f72:", - "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", - "900150983cd24fb0d6963f7d28e17f72:", - "900150983cd24fb0d6963f7d28e17f72:", - "missing:", - "1111" - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "900150983cd24fb0d6963f7d28e17f72:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "900150983cd24fb0d6963f7d28e17f72:", + "900150983cd24fb0d6963f7d28e17f72:", + "missing:", + "1111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval zero-argument system builtins return native-compatible values. - #[test] - fn execute_program_dispatches_zero_arg_system_builtins() { - let program = parse_fragment( - br#"echo time() > 1000000000 ? "time" : "bad"; echo ":"; +/// Verifies eval zero-argument system builtins return native-compatible values. +#[test] +fn execute_program_dispatches_zero_arg_system_builtins() { + let program = parse_fragment( + br#"echo time() > 1000000000 ? "time" : "bad"; echo ":"; echo phpversion(); echo ":"; echo sys_get_temp_dir(); echo ":"; echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; @@ -4818,28 +4786,28 @@ echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; echo call_user_func_array("sys_get_temp_dir", []); echo ":"; echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); return function_exists("sys_get_temp_dir");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - format!( - "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", - eval_compiler_php_version(), - eval_compiler_php_version() - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + format!( + "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", + eval_compiler_php_version(), + eval_compiler_php_version() + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `date()` formats libc local timestamps and `mktime()` builds them. - #[test] - fn execute_program_dispatches_date_mktime_builtins() { - let program = parse_fragment( +/// Verifies eval `date()` formats libc local timestamps and `mktime()` builds them. +#[test] +fn execute_program_dispatches_date_mktime_builtins() { + let program = parse_fragment( br#"$ts = mktime(13, 2, 3, 1, 2, 2024); echo date("Y-m-d H:i:s", $ts); echo ":" . date("j-n-G-g-A-a-N-D-M-l-F", $ts); @@ -4851,23 +4819,23 @@ echo ":"; echo function_exists("date"); return function_exists("mktime");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!( + values.output, + "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. - #[test] - fn execute_program_dispatches_strtotime_builtin() { - let program = parse_fragment( - br#"$date = strtotime("2024-06-15"); +/// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. +#[test] +fn execute_program_dispatches_strtotime_builtin() { + let program = parse_fragment( + br#"$date = strtotime("2024-06-15"); echo date("Y-m-d H:i:s", $date); $full = strtotime("2024-06-15 12:30:45"); echo ":" . date("Y-m-d H:i:s", $full); @@ -4879,68 +4847,68 @@ echo ":" . date("Y-m-d H:i:s", $call); $spread = call_user_func_array("strtotime", ["datetime" => "2024-01-02"]); echo ":" . date("Y-m-d", $spread) . ":"; return function_exists("strtotime");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. - #[test] - fn execute_program_dispatches_microtime_builtin() { - let program = parse_fragment( - br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; +/// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. +#[test] +fn execute_program_dispatches_microtime_builtin() { + let program = parse_fragment( + br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; echo ":"; return function_exists("microtime");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "now:named:call:array:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "now:named:call:array:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. - #[test] - fn execute_program_dispatches_realpath_cache_builtins() { - let program = parse_fragment( - br#"$cache = realpath_cache_get(); +/// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. +#[test] +fn execute_program_dispatches_realpath_cache_builtins() { + let program = parse_fragment( + br#"$cache = realpath_cache_get(); echo count($cache) . ":" . realpath_cache_size() . ":"; $call_cache = call_user_func("realpath_cache_get"); echo count($call_cache) . ":"; echo call_user_func_array("realpath_cache_size", []) . ":"; echo function_exists("realpath_cache_get"); return function_exists("realpath_cache_size");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "0:0:0:0:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "0:0:0:0:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval stream introspection builtins return native-compatible static lists. - #[test] - fn execute_program_dispatches_stream_introspection_builtins() { - let program = parse_fragment( - br#"$wrappers = stream_get_wrappers(); +/// Verifies eval stream introspection builtins return native-compatible static lists. +#[test] +fn execute_program_dispatches_stream_introspection_builtins() { + let program = parse_fragment( + br#"$wrappers = stream_get_wrappers(); $transports = stream_get_transports(); $filters = stream_get_filters(); echo count($wrappers) . ":" . $wrappers[0] . ":" . $wrappers[5] . ":"; @@ -4954,25 +4922,25 @@ $call_filters = call_user_func_array("stream_get_filters", []); echo $call_filters[13] . ":"; echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); return function_exists("stream_get_filters");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. - #[test] - fn execute_program_dispatches_spl_classes_builtin() { - let program = parse_fragment( - br#"$names = spl_classes(); +/// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. +#[test] +fn execute_program_dispatches_spl_classes_builtin() { + let program = parse_fragment( + br#"$names = spl_classes(); echo count($names) . ":" . $names[0] . ":" . $names[55] . ":"; echo (in_array("Exception", $names) ? "exception" : "bad") . ":"; echo (in_array("SplDoublyLinkedList", $names) ? "list" : "bad") . ":"; @@ -4982,24 +4950,24 @@ $spread = call_user_func_array("spl_classes", []); echo (count($spread) === count($names) ? "spread" : "bad") . ":"; echo function_exists("spl_classes"); return is_callable("spl_classes");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "61:AppendIterator:Throwable:exception:list:call:spread:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "61:AppendIterator:Throwable:exception:list:call:spread:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval SPL object identity builtins are stable, unique, and callable. - #[test] - fn execute_program_dispatches_spl_object_identity_builtins() { - let program = parse_fragment( +/// Verifies eval SPL object identity builtins are stable, unique, and callable. +#[test] +fn execute_program_dispatches_spl_object_identity_builtins() { + let program = parse_fragment( br#"$a = new KnownClass(); $b = new KnownClass(); echo (spl_object_id($a) === spl_object_id($a)) ? "stable" : "drift"; @@ -5016,19 +4984,19 @@ echo function_exists("spl_object_id"); return function_exists("spl_object_hash");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "stable:unique:hash:call:array:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "stable:unique:hash:call:array:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval environment builtins read, write, unset, and dispatch dynamically. - #[test] - fn execute_program_dispatches_environment_builtins() { - let program = parse_fragment( +/// Verifies eval environment builtins read, write, unset, and dispatch dynamically. +#[test] +fn execute_program_dispatches_environment_builtins() { + let program = parse_fragment( br#"putenv("ELEPHC_EVAL_ENV_TEST=direct"); echo getenv("ELEPHC_EVAL_ENV_TEST") . ":"; putenv(assignment: "ELEPHC_EVAL_ENV_TEST=named"); @@ -5042,20 +5010,20 @@ echo ":"; echo function_exists("getenv"); return function_exists("putenv");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval sleep builtins dispatch without delaying focused tests. - #[test] - fn execute_program_dispatches_sleep_builtins() { - let program = parse_fragment( - br#"echo sleep(0) . ":"; +/// Verifies eval sleep builtins dispatch without delaying focused tests. +#[test] +fn execute_program_dispatches_sleep_builtins() { + let program = parse_fragment( + br#"echo sleep(0) . ":"; echo sleep(seconds: 0) . ":"; usleep(0); echo "u:"; @@ -5063,22 +5031,22 @@ echo call_user_func("sleep", 0) . ":"; echo call_user_func_array("usleep", ["microseconds" => 0]) === null ? "null" : "bad"; echo ":"; echo function_exists("sleep"); return function_exists("usleep");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "0:0:u:0:null:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "0:0:u:0:null:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. - #[test] - fn execute_program_dispatches_php_uname_builtin() { - let program = parse_fragment( - br#"echo strlen(php_uname()) > 0 ? "all" : "empty"; echo ":"; +/// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. +#[test] +fn execute_program_dispatches_php_uname_builtin() { + let program = parse_fragment( + br#"echo strlen(php_uname()) > 0 ? "all" : "empty"; echo ":"; echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; @@ -5088,63 +5056,63 @@ echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; return function_exists("php_uname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "all:same:sys:node:release:version:machine:call:spread:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "all:same:sys:node:release:version:machine:call:spread:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. - #[test] - fn execute_program_dispatches_gethostbyname_builtin() { - let program = parse_fragment( - br#"echo gethostbyname("127.0.0.1") . ":"; +/// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. +#[test] +fn execute_program_dispatches_gethostbyname_builtin() { + let program = parse_fragment( + br#"echo gethostbyname("127.0.0.1") . ":"; echo gethostbyname(hostname: "not a host") . ":"; echo call_user_func("gethostbyname", "127.0.0.1") . ":"; echo call_user_func_array("gethostbyname", ["hostname" => "not a host"]) . ":"; return function_exists("gethostbyname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "127.0.0.1:not a host:127.0.0.1:not a host:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "127.0.0.1:not a host:127.0.0.1:not a host:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. - #[test] - fn execute_program_dispatches_gethostname_builtin() { - let program = parse_fragment( - br#"echo strlen(gethostname()) > 0 ? "host" : "empty"; echo ":"; +/// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. +#[test] +fn execute_program_dispatches_gethostname_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostname()) > 0 ? "host" : "empty"; echo ":"; echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; return function_exists("gethostname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "host:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "host:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. - #[test] - fn execute_program_dispatches_gethostbyaddr_builtin() { - let program = parse_fragment( +/// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. +#[test] +fn execute_program_dispatches_gethostbyaddr_builtin() { + let program = parse_fragment( br#"echo strlen(gethostbyaddr("127.0.0.1")) > 0 ? "direct" : "empty"; echo ":"; echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; @@ -5153,19 +5121,19 @@ echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === fa return function_exists("gethostbyaddr");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "direct:named:false:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "direct:named:false:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval protocol and service database lookups dispatch dynamically. - #[test] - fn execute_program_dispatches_protocol_service_builtins() { - let program = parse_fragment( +/// Verifies eval protocol and service database lookups dispatch dynamically. +#[test] +fn execute_program_dispatches_protocol_service_builtins() { + let program = parse_fragment( br#"echo getprotobyname("TCP") . ":"; echo getprotobynumber(6) . ":"; echo getprotobyname("no_such_protocol") === false ? "missing-proto" : "bad"; echo ":"; @@ -5182,23 +5150,23 @@ echo function_exists("getprotobyname"); echo function_exists("getprotobynumber") return function_exists("getservbyport");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:111" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. - #[test] - fn execute_program_dispatches_ip_conversion_builtins() { - let program = parse_fragment( - br#"echo long2ip(3232235777) . ":"; +/// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. +#[test] +fn execute_program_dispatches_ip_conversion_builtins() { + let program = parse_fragment( + br#"echo long2ip(3232235777) . ":"; echo long2ip(ip: 4294967295) . ":"; echo ip2long("192.168.1.1") . ":"; echo ip2long(ip: "1.2.3") === false ? "bad-ip" : "bad"; echo ":"; @@ -5212,25 +5180,25 @@ echo call_user_func_array("ip2long", ["ip" => "0.0.0.0"]) . ":"; echo function_exists("long2ip"); echo function_exists("ip2long"); echo function_exists("inet_pton"); return function_exists("inet_ntop");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:111" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval path component builtins mirror static basename/dirname edge cases. - #[test] - fn execute_program_dispatches_path_component_builtins() { - let program = parse_fragment( - br#"echo basename("/var/log/syslog.log", ".log") . ":"; +/// Verifies eval path component builtins mirror static basename/dirname edge cases. +#[test] +fn execute_program_dispatches_path_component_builtins() { + let program = parse_fragment( + br#"echo basename("/var/log/syslog.log", ".log") . ":"; echo basename(path: "/usr///") . ":"; echo basename("/", "x") === "" ? "root" : "bad"; echo ":"; echo dirname("/usr/local/bin/tool", 2) . ":"; @@ -5239,24 +5207,24 @@ echo call_user_func("basename", "foo.tar.gz", ".bz2") . ":"; echo call_user_func_array("dirname", ["path" => "/usr", "levels" => 3]) . ":"; echo function_exists("basename"); return function_exists("dirname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `realpath()` resolves existing paths and returns false for misses. - #[test] - fn execute_program_dispatches_realpath_builtin() { - let program = parse_fragment( +/// Verifies eval `realpath()` resolves existing paths and returns false for misses. +#[test] +fn execute_program_dispatches_realpath_builtin() { + let program = parse_fragment( br#"echo realpath(".") !== false ? "resolved" : "bad"; echo ":"; echo realpath(path: "elephc-eval-missing-path") === false ? "false" : "bad"; echo ":"; echo call_user_func("realpath", ".") !== false ? "call" : "bad"; echo ":"; @@ -5265,19 +5233,19 @@ echo ":"; return function_exists("realpath");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "resolved:false:call:array-false:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "resolved:false:call:array-false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. - #[test] - fn execute_program_dispatches_fnmatch_builtin() { - let program = parse_fragment( +/// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. +#[test] +fn execute_program_dispatches_fnmatch_builtin() { + let program = parse_fragment( br#"echo fnmatch("*.log", "system.log") ? "match" : "bad"; echo ":"; echo fnmatch("*.log", "logs/system.log", FNM_PATHNAME) ? "bad" : "path"; echo ":"; echo fnmatch("*.LOG", "system.log", FNM_CASEFOLD) ? "case" : "bad"; echo ":"; @@ -5293,22 +5261,22 @@ echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD"); return FNM_CASEFOLD;"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "match:path:case:period:class:escape:noescape:flags:call:callarray:11" - ); - assert_eq!(values.get(result), FakeValue::Int(EVAL_FNM_CASEFOLD)); - } + assert_eq!( + values.output, + "match:path:case:period:class:escape:noescape:flags:call:callarray:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_FNM_CASEFOLD)); +} - /// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. - #[test] - fn execute_program_dispatches_pathinfo_builtin() { - let program = parse_fragment( +/// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. +#[test] +fn execute_program_dispatches_pathinfo_builtin() { + let program = parse_fragment( br#"$info = pathinfo("/var/log/syslog.log"); echo $info["dirname"] . "|" . $info["basename"] . "|" . $info["extension"] . "|" . $info["filename"] . ":"; echo pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":"; @@ -5325,25 +5293,25 @@ echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL"); return PATHINFO_ALL;"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" - ); - assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); - } + assert_eq!( + values.output, + "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); +} - /// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. - #[test] - fn execute_program_dispatches_filesystem_builtins() { - let filename = format!("elephc_eval_fs_probe_{}.txt", std::process::id()); - let missing = format!("elephc_eval_fs_missing_{}.txt", std::process::id()); - let source = format!( - r#"echo file_put_contents("{filename}", "hello") . ":"; +/// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. +#[test] +fn execute_program_dispatches_filesystem_builtins() { + let filename = format!("elephc_eval_fs_probe_{}.txt", std::process::id()); + let missing = format!("elephc_eval_fs_missing_{}.txt", std::process::id()); + let source = format!( + r#"echo file_put_contents("{filename}", "hello") . ":"; echo file_get_contents("{filename}") . ":"; echo file_exists("{filename}") ? "exists" : "missing"; echo ":"; echo is_file(filename: "{filename}") ? "file" : "bad"; echo ":"; @@ -5362,25 +5330,25 @@ echo function_exists("file_exists"); echo function_exists("is_file"); echo funct echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); echo function_exists("filesize"); return function_exists("unlink");"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let _ = std::fs::remove_file(&filename); - assert_eq!( + let _ = std::fs::remove_file(&filename); + assert_eq!( values.output, "5:hello:exists:file:dir:readable:writable:writeable:5:missing-false:call-exists:5:unlinked:gone:111111111" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval disk-space builtins query local filesystem capacity and dispatch dynamically. - #[test] - fn execute_program_dispatches_disk_space_builtins() { - let program = parse_fragment( +/// Verifies eval disk-space builtins query local filesystem capacity and dispatch dynamically. +#[test] +fn execute_program_dispatches_disk_space_builtins() { + let program = parse_fragment( br#"echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; @@ -5391,23 +5359,23 @@ echo function_exists("disk_free_space"); return function_exists("disk_total_space");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "free:total:ordered:missing:call:spread:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "free:total:ordered:missing:call:spread:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval stat metadata builtins expose scalar file metadata and link probes. - #[test] - fn execute_program_dispatches_stat_metadata_builtins() { - let filename = format!("elephc_eval_stat_probe_{}.txt", std::process::id()); - let missing = format!("elephc_eval_stat_missing_{}.txt", std::process::id()); - let link = format!("elephc_eval_stat_link_{}.txt", std::process::id()); - let source = format!( - r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; +/// Verifies eval stat metadata builtins expose scalar file metadata and link probes. +#[test] +fn execute_program_dispatches_stat_metadata_builtins() { + let filename = format!("elephc_eval_stat_probe_{}.txt", std::process::id()); + let missing = format!("elephc_eval_stat_missing_{}.txt", std::process::id()); + let link = format!("elephc_eval_stat_link_{}.txt", std::process::id()); + let source = format!( + r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; echo fileatime("{filename}") > 0 ? "atime" : "bad"; echo ":"; echo filectime("{filename}") > 0 ? "ctime" : "bad"; echo ":"; echo fileperms("{filename}") > 0 ? "perms" : "bad"; echo ":"; @@ -5435,35 +5403,35 @@ echo function_exists("fileowner"); echo function_exists("filegroup"); echo function_exists("fileinode"); echo function_exists("filetype"); echo function_exists("is_executable"); echo function_exists("is_link"); return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - std::fs::write(&filename, b"hello").expect("write stat fixture"); - std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - assert_eq!( + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( values.output, "mtime:atime:ctime:perms:owner:group:inode:file:dir:link:noexec:link:missing-atime:missing-ctime:missing-perms:missing-owner:missing-group:missing-inode:missing-type:missing-mtime:file:callinode:1111111111" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. - #[test] - fn execute_program_dispatches_stat_array_builtins() { - let pid = std::process::id(); - let filename = format!("elephc_eval_stat_array_{pid}.txt"); - let link = format!("elephc_eval_lstat_array_{pid}.txt"); - let missing = format!("elephc_eval_stat_array_missing_{pid}.txt"); - let source = format!( - r#"$stat = stat("{filename}"); +/// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. +#[test] +fn execute_program_dispatches_stat_array_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_stat_array_{pid}.txt"); + let link = format!("elephc_eval_lstat_array_{pid}.txt"); + let missing = format!("elephc_eval_stat_array_missing_{pid}.txt"); + let source = format!( + r#"$stat = stat("{filename}"); $lstat = lstat("{link}"); echo $stat["size"] === 5 && $stat[7] === $stat["size"] ? "stat" : "bad"; echo ":"; echo ($stat["mode"] & 61440) === 32768 ? "mode" : "bad"; echo ":"; @@ -5476,39 +5444,39 @@ echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; echo function_exists("stat"); echo function_exists("lstat"); return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - std::fs::write(&filename, b"hello").expect("write stat array fixture"); - std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - assert_eq!( - values.output, - "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat array fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval local path operation builtins mutate filesystem state. - #[test] - fn execute_program_dispatches_path_operation_builtins() { - let pid = std::process::id(); - let dir = format!("elephc_eval_ops_dir_{pid}"); - let call_dir = format!("elephc_eval_ops_call_dir_{pid}"); - let src = format!("elephc_eval_ops_src_{pid}.txt"); - let copy = format!("elephc_eval_ops_copy_{pid}.txt"); - let moved = format!("elephc_eval_ops_moved_{pid}.txt"); - let symlink = format!("elephc_eval_ops_symlink_{pid}.txt"); - let hardlink = format!("elephc_eval_ops_hardlink_{pid}.txt"); - let source = format!( - r#"file_put_contents("{src}", "hello"); +/// Verifies eval local path operation builtins mutate filesystem state. +#[test] +fn execute_program_dispatches_path_operation_builtins() { + let pid = std::process::id(); + let dir = format!("elephc_eval_ops_dir_{pid}"); + let call_dir = format!("elephc_eval_ops_call_dir_{pid}"); + let src = format!("elephc_eval_ops_src_{pid}.txt"); + let copy = format!("elephc_eval_ops_copy_{pid}.txt"); + let moved = format!("elephc_eval_ops_moved_{pid}.txt"); + let symlink = format!("elephc_eval_ops_symlink_{pid}.txt"); + let hardlink = format!("elephc_eval_ops_hardlink_{pid}.txt"); + let source = format!( + r#"file_put_contents("{src}", "hello"); echo mkdir("{dir}") ? "mkdir" : "bad"; echo ":"; echo is_dir("{dir}") ? "dir" : "bad"; echo ":"; echo copy("{src}", "{copy}") && file_get_contents("{copy}") === "hello" ? "copy" : "bad"; echo ":"; @@ -5527,43 +5495,43 @@ echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exis echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); return true;"#, - missing = format!("elephc_eval_ops_missing_{pid}.txt"), - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - for path in [&symlink, &hardlink, &moved, ©, &src] { - let _ = std::fs::remove_file(path); - } - for path in [&call_dir, &dir] { - let _ = std::fs::remove_dir(path); - } - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + missing = format!("elephc_eval_ops_missing_{pid}.txt"), + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - for path in [&symlink, &hardlink, &moved, ©, &src] { - let _ = std::fs::remove_file(path); - } - for path in [&call_dir, &dir] { - let _ = std::fs::remove_dir(path); - } - assert_eq!( + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + assert_eq!( values.output, "mkdir:dir:copy:rename:symlink:readlink:linkinfo:readlink-false:linkinfo-missing:hardlink:cache:cleanup:callmkdir:callrmdir:111111111" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. - #[test] - fn execute_program_dispatches_file_listing_builtins() { - let pid = std::process::id(); - let lines = format!("elephc_eval_listing_lines_{pid}.txt"); - let empty = format!("elephc_eval_listing_empty_{pid}.txt"); - let missing = format!("elephc_eval_listing_missing_{pid}.txt"); - let dir = format!("elephc_eval_listing_dir_{pid}"); - let source = format!( - r#"file_put_contents("{lines}", "one\ntwo"); +/// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. +#[test] +fn execute_program_dispatches_file_listing_builtins() { + let pid = std::process::id(); + let lines = format!("elephc_eval_listing_lines_{pid}.txt"); + let empty = format!("elephc_eval_listing_empty_{pid}.txt"); + let missing = format!("elephc_eval_listing_missing_{pid}.txt"); + let dir = format!("elephc_eval_listing_dir_{pid}"); + let source = format!( + r#"file_put_contents("{lines}", "one\ntwo"); file_put_contents("{empty}", ""); $lines = file("{lines}"); echo count($lines) . ":"; @@ -5587,39 +5555,39 @@ echo count($call_scan) . ":"; echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") && unlink("{lines}") && unlink("{empty}") ? "cleanup" : "bad"; echo ":"; echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - for path in [&lines, &empty, &missing] { - let _ = std::fs::remove_file(path); - } - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.txt")); - let _ = std::fs::remove_dir(&dir); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - for path in [&lines, &empty, &missing] { - let _ = std::fs::remove_file(path); - } - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.txt")); - let _ = std::fs::remove_dir(&dir); - assert_eq!( - values.output, - "2:line0:line1:[]0:missing-readfile:missing-file:4:scan:callfile:4:cleanup:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "2:line0:line1:[]0:missing-readfile:missing-file:4:scan:callfile:4:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `glob()` expands local patterns and dispatches dynamically. - #[test] - fn execute_program_dispatches_glob_builtin() { - let pid = std::process::id(); - let dir = format!("elephc_eval_glob_dir_{pid}"); - let source = format!( - r#"mkdir("{dir}"); +/// Verifies eval `glob()` expands local patterns and dispatches dynamically. +#[test] +fn execute_program_dispatches_glob_builtin() { + let pid = std::process::id(); + let dir = format!("elephc_eval_glob_dir_{pid}"); + let source = format!( + r#"mkdir("{dir}"); file_put_contents("{dir}/a.txt", "a"); file_put_contents("{dir}/b.log", "b"); file_put_contents("{dir}/c.txt", "c"); @@ -5642,40 +5610,40 @@ unlink("{dir}/a.txt"); echo rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; echo function_exists("glob"); return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); - let _ = std::fs::remove_file(format!("{dir}/c.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.log")); - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_dir(&dir); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); - let _ = std::fs::remove_file(format!("{dir}/c.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.log")); - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_dir(&dir); - assert_eq!( - values.output, - "glob:empty:literal:hidden:callglob:callarray:cleanup:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "glob:empty:literal:hidden:callglob:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. - #[test] - fn execute_program_dispatches_file_modify_builtins() { - let pid = std::process::id(); - let filename = format!("elephc_eval_modify_{pid}.txt"); - let missing = format!("elephc_eval_modify_missing_{pid}.txt"); - let prefix = format!("evm{pid}_"); - let call_prefix = format!("evc{pid}_"); - let source = format!( - r#"file_put_contents("{filename}", "x"); +/// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. +#[test] +fn execute_program_dispatches_file_modify_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_modify_{pid}.txt"); + let missing = format!("elephc_eval_modify_missing_{pid}.txt"); + let prefix = format!("evm{pid}_"); + let call_prefix = format!("evc{pid}_"); + let source = format!( + r#"file_put_contents("{filename}", "x"); echo chmod(filename: "{filename}", permissions: 384) ? "chmod" : "bad"; echo ":"; echo (fileperms("{filename}") & 511) === 384 ? "mode" : "bad"; echo ":"; echo chmod("{missing}", 384) ? "bad" : "chmod-false"; echo ":"; @@ -5696,47 +5664,47 @@ unlink($call_tmp); echo unlink("{filename}") ? "cleanup" : "bad"; echo ":"; echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&missing); - for entry in std::fs::read_dir(".").expect("read eval test cwd") { - let entry = entry.expect("read eval temp entry"); - let name = entry.file_name().to_string_lossy().into_owned(); - if name.starts_with(&prefix) || name.starts_with(&call_prefix) { - let _ = std::fs::remove_file(entry.path()); - } + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); } - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&missing); - for entry in std::fs::read_dir(".").expect("read eval test cwd") { - let entry = entry.expect("read eval temp entry"); - let name = entry.file_name().to_string_lossy().into_owned(); - if name.starts_with(&prefix) || name.starts_with(&call_prefix) { - let _ = std::fs::remove_file(entry.path()); - } + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); } - assert_eq!( - values.output, - "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); } + assert_eq!( + values.output, + "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. - #[test] - fn execute_program_dispatches_touch_builtin() { - let pid = std::process::id(); - let created = format!("elephc_eval_touch_created_{pid}.txt"); - let stamped = format!("elephc_eval_touch_stamped_{pid}.txt"); - let missing = format!("elephc_eval_touch_missing_{pid}/x.txt"); - let source = format!( - r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; +/// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. +#[test] +fn execute_program_dispatches_touch_builtin() { + let pid = std::process::id(); + let created = format!("elephc_eval_touch_created_{pid}.txt"); + let stamped = format!("elephc_eval_touch_stamped_{pid}.txt"); + let missing = format!("elephc_eval_touch_missing_{pid}/x.txt"); + let source = format!( + r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; file_put_contents("{stamped}", "x"); echo touch("{stamped}", 1000000000) ? "mtime" : "bad"; echo ":"; echo filemtime("{stamped}") === 1000000000 ? "readmtime" : "bad"; echo ":"; @@ -5748,28 +5716,28 @@ echo call_user_func_array("touch", ["filename" => "{stamped}", "mtime" => 100000 echo unlink("{created}") && unlink("{stamped}") ? "cleanup" : "bad"; echo ":"; echo function_exists("touch"); return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&created); - let _ = std::fs::remove_file(&stamped); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&created); - let _ = std::fs::remove_file(&stamped); - assert_eq!( - values.output, - "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + assert_eq!( + values.output, + "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval ASCII string case builtins work directly and through callable dispatch. - #[test] - fn execute_program_dispatches_string_case_builtins() { - let program = parse_fragment( +/// Verifies eval ASCII string case builtins work directly and through callable dispatch. +#[test] +fn execute_program_dispatches_string_case_builtins() { + let program = parse_fragment( br#"echo strtoupper("Hello World"); echo ":"; echo strtolower("LOUD"); echo ":"; echo ucfirst("eval"); echo ":"; @@ -5782,47 +5750,47 @@ echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower") return function_exists("lcfirst");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!( + values.output, + "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `ucwords()` capitalizes word starts directly and by callable dispatch. - #[test] - fn execute_program_dispatches_ucwords_builtin() { - let program = parse_fragment( - br#"echo ucwords("hello world"); echo ":"; +/// Verifies eval `ucwords()` capitalizes word starts directly and by callable dispatch. +#[test] +fn execute_program_dispatches_ucwords_builtin() { + let program = parse_fragment( + br#"echo ucwords("hello world"); echo ":"; echo ucwords(string: "hello-world", separators: "-"); echo ":"; echo ucwords("hello\tworld"); echo ":"; echo call_user_func("ucwords", "a b"); echo ":"; echo call_user_func_array("ucwords", ["string" => "a-b", "separators" => "-"]); echo ":"; return function_exists("ucwords");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Hello World:Hello-World:Hello\tWorld:A B:A-B:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:Hello-World:Hello\tWorld:A B:A-B:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. - #[test] - fn execute_program_dispatches_wordwrap_builtin() { - let program = parse_fragment( - br#"echo wordwrap("The quick brown fox", 10, "|"); echo ":"; +/// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. +#[test] +fn execute_program_dispatches_wordwrap_builtin() { + let program = parse_fragment( + br#"echo wordwrap("The quick brown fox", 10, "|"); echo ":"; echo wordwrap(string: "A verylongword here", width: 8, break: "|"); echo ":"; echo wordwrap("abcdefghij", 4, "|", true); echo ":"; echo wordwrap("preserve\nnewlines here ok", 10, "|"); echo ":"; @@ -5830,46 +5798,46 @@ echo call_user_func("wordwrap", "aaa bbb ccc", 3, "
"); echo ":"; echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); echo ":"; return function_exists("wordwrap");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:" ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. - #[test] - fn execute_program_dispatches_str_contains_builtin() { - let program = parse_fragment( - br#"echo str_contains("Hello World", "World") ? "Y" : "N"; +/// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. +#[test] +fn execute_program_dispatches_str_contains_builtin() { + let program = parse_fragment( + br#"echo str_contains("Hello World", "World") ? "Y" : "N"; echo str_contains("Hello", "z") ? "bad" : ":N"; echo str_contains("Hello", "") ? ":E" : "bad"; echo call_user_func("str_contains", "abc", "b") ? ":C" : "bad"; echo call_user_func_array("str_contains", ["abc", "x"]) ? "bad" : ":A"; return function_exists("str_contains");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Y:N:E:C:A"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "Y:N:E:C:A"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval string position builtins return byte offsets or PHP false. - #[test] - fn execute_program_dispatches_string_position_builtins() { - let program = parse_fragment( - br#"echo strpos("banana", "na"); +/// Verifies eval string position builtins return byte offsets or PHP false. +#[test] +fn execute_program_dispatches_string_position_builtins() { + let program = parse_fragment( + br#"echo strpos("banana", "na"); echo ":" . strrpos("banana", "na"); echo ":"; echo strpos("abc", "z") === false ? "F" : "bad"; echo ":" . strpos("abc", ""); @@ -5878,21 +5846,21 @@ echo ":" . call_user_func("strpos", "abc", "b"); echo ":" . call_user_func_array("strrpos", ["ababa", "ba"]); echo ":"; echo function_exists("strpos"); return function_exists("strrpos");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:4:F:0:3:1:3:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "2:4:F:0:3:1:3:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `strstr()` returns suffixes, prefixes, or false for misses. - #[test] - fn execute_program_dispatches_strstr_builtin() { - let program = parse_fragment( +/// Verifies eval `strstr()` returns suffixes, prefixes, or false for misses. +#[test] +fn execute_program_dispatches_strstr_builtin() { + let program = parse_fragment( br#"echo strstr("user@example.com", "@"); echo ":"; echo strstr(haystack: "hello world", needle: "lo", before_needle: true); echo ":"; echo strstr("hello", "x") === false ? "F" : "bad"; echo ":"; @@ -5902,20 +5870,20 @@ echo call_user_func_array("strstr", ["haystack" => "abcabc", "needle" => "bc", " return function_exists("strstr");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "@example.com:hel:F:hello:bcabc:a:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "@example.com:hel:F:hello:bcabc:a:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval prefix/suffix string search builtins use byte-string semantics. - #[test] - fn execute_program_dispatches_string_boundary_builtins() { - let program = parse_fragment( - br#"echo str_starts_with("Hello World", "Hello") ? "S" : "bad"; +/// Verifies eval prefix/suffix string search builtins use byte-string semantics. +#[test] +fn execute_program_dispatches_string_boundary_builtins() { + let program = parse_fragment( + br#"echo str_starts_with("Hello World", "Hello") ? "S" : "bad"; echo str_starts_with("Hello", "World") ? "bad" : ":s"; echo str_starts_with("Hello", "") ? ":se" : "bad"; echo str_ends_with("Hello World", "World") ? ":E" : "bad"; @@ -5925,22 +5893,22 @@ echo call_user_func("str_starts_with", "abc", "a") ? ":CS" : "bad"; echo call_user_func_array("str_ends_with", ["abc", "c"]) ? ":CE" : "bad"; echo ":"; echo function_exists("str_starts_with"); return function_exists("str_ends_with");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "S:s:se:E:e:ee:CS:CE:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "S:s:se:E:e:ee:CS:CE:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval string comparison builtins return PHP-compatible scalar results. - #[test] - fn execute_program_dispatches_string_compare_builtins() { - let program = parse_fragment( - br#"echo strcmp("abc", "abc"); +/// Verifies eval string comparison builtins return PHP-compatible scalar results. +#[test] +fn execute_program_dispatches_string_compare_builtins() { + let program = parse_fragment( + br#"echo strcmp("abc", "abc"); echo ":"; echo strcmp("abc", "abd") < 0 ? "lt" : "bad"; echo ":"; echo strcasecmp("Hello", "hello"); echo ":"; echo call_user_func("strcmp", "b", "a") > 0 ? "gt" : "bad"; @@ -5950,21 +5918,21 @@ echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); return function_exists("hash_equals");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "0:lt:0:gt:ci:heq:hlen:hneq:11"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "0:lt:0:gt:ci:heq:hlen:hneq:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval trim-like builtins strip default and explicit byte masks. - #[test] - fn execute_program_dispatches_trim_like_builtins() { - let program = parse_fragment( +/// Verifies eval trim-like builtins strip default and explicit byte masks. +#[test] +fn execute_program_dispatches_trim_like_builtins() { + let program = parse_fragment( br#"echo "[" . trim(" hello ") . "]"; echo ":[" . ltrim(" left") . "]"; echo ":[" . rtrim("right ") . "]"; @@ -5976,22 +5944,22 @@ echo ":"; echo function_exists("trim"); echo function_exists("ltrim"); echo func return function_exists("chop");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "[hello]:[left]:[right]:[tail]:[boxed]:[cuf]:[7]:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!( + values.output, + "[hello]:[left]:[right]:[tail]:[boxed]:[cuf]:[7]:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. - #[test] - fn execute_program_dispatches_type_predicate_builtins() { - let program = parse_fragment( +/// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. +#[test] +fn execute_program_dispatches_type_predicate_builtins() { + let program = parse_fragment( br#"echo is_int(1); echo is_integer(1); echo is_long(1); echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); echo is_string("x"); echo is_bool(false); echo is_null(null); @@ -6024,89 +5992,89 @@ echo function_exists("is_iterable"); return function_exists("is_infinite");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "1111111111111Tok11111NBROoNIiFf:1111tOo1111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!( + values.output, + "1111111111111Tok11111NBROoNIiFf:1111tOo1111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. - #[test] - fn execute_program_dispatches_is_resource_true() { - let program = parse_fragment( - br#"echo is_resource($handle) ? "R" : "bad"; +/// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. +#[test] +fn execute_program_dispatches_is_resource_true() { + let program = parse_fragment( + br#"echo is_resource($handle) ? "R" : "bad"; echo ":" . gettype($handle); return call_user_func("is_resource", $handle);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let handle = values.alloc(FakeValue::Resource(6)); - scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "R:resource"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "R:resource"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval resource introspection builtins expose stream type and one-based id. - #[test] - fn execute_program_dispatches_resource_introspection_builtins() { - let program = parse_fragment( - br#"echo get_resource_type($handle); +/// Verifies eval resource introspection builtins expose stream type and one-based id. +#[test] +fn execute_program_dispatches_resource_introspection_builtins() { + let program = parse_fragment( + br#"echo get_resource_type($handle); echo ":" . get_resource_id($handle); echo ":" . call_user_func("get_resource_type", $handle); echo ":" . call_user_func_array("get_resource_id", ["resource" => $handle]); echo ":" . function_exists("get_resource_type"); return function_exists("get_resource_id");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let handle = values.alloc(FakeValue::Resource(6)); - scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "stream:7:stream:7:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "stream:7:stream:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval cast builtins return boxed scalar cells directly and by callable. - #[test] - fn execute_program_dispatches_cast_builtins() { - let program = parse_fragment( - br#"echo intval("42"); echo ":"; +/// Verifies eval cast builtins return boxed scalar cells directly and by callable. +#[test] +fn execute_program_dispatches_cast_builtins() { + let program = parse_fragment( + br#"echo intval("42"); echo ":"; echo floatval("3.5"); echo ":"; echo strval(12); echo ":"; echo boolval("0") ? "bad" : "false"; echo ":"; echo call_user_func("strval", 7); return call_user_func_array("intval", ["9"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "42:3.5:12:false:7"); - assert_eq!(values.get(result), FakeValue::Int(9)); - } + assert_eq!(values.output, "42:3.5:12:false:7"); + assert_eq!(values.get(result), FakeValue::Int(9)); +} - /// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. - #[test] - fn execute_program_dispatches_settype_builtin() { - let program = parse_fragment( - br#"$x = 42; +/// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. +#[test] +fn execute_program_dispatches_settype_builtin() { + let program = parse_fragment( + br#"$x = 42; echo settype($x, "string") ? gettype($x) . ":" . $x : "bad"; echo ":"; $y = "0"; @@ -6118,29 +6086,29 @@ $z = 3.8; echo call_user_func("settype", $z, "integer") ? gettype($z) . ":" . $z : "bad"; echo ":"; return function_exists("settype");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "string:42:boolean:false:integer:0:double:3.8:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - assert_eq!( - values.warnings, - ["settype(): Argument #1 ($var) must be passed by reference, value given"] - ); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "string:42:boolean:false:integer:0:double:3.8:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + ["settype(): Argument #1 ($var) must be passed by reference, value given"] + ); +} - /// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. - #[test] - fn execute_program_dispatches_gettype_builtin() { - let program = parse_fragment( - br#"echo gettype(1); echo ":"; +/// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. +#[test] +fn execute_program_dispatches_gettype_builtin() { + let program = parse_fragment( + br#"echo gettype(1); echo ":"; echo gettype(1.5); echo ":"; echo gettype("x"); echo ":"; echo gettype(false); echo ":"; @@ -6150,93 +6118,93 @@ echo gettype(["a" => 1]); echo ":"; echo call_user_func("gettype", true); echo ":"; echo call_user_func_array("gettype", [null]); return function_exists("gettype");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "integer:double:string:boolean:NULL:array:array:boolean:NULL" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "integer:double:string:boolean:NULL:array:array:boolean:NULL" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `get_class()` reads object class names directly and by callable. - #[test] - fn execute_program_dispatches_get_class_builtin() { - let program = parse_fragment( - br#"echo get_class($object); echo ":"; +/// Verifies eval `get_class()` reads object class names directly and by callable. +#[test] +fn execute_program_dispatches_get_class_builtin() { + let program = parse_fragment( + br#"echo get_class($object); echo ":"; echo call_user_func("get_class", $object); echo ":"; return call_user_func_array("get_class", ["object" => $object]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "stdClass:stdClass:"); - assert_eq!( - values.get(result), - FakeValue::String("stdClass".to_string()) - ); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stdClass:stdClass:"); + assert_eq!( + values.get(result), + FakeValue::String("stdClass".to_string()) + ); +} - /// Verifies eval `get_parent_class()` reads object and class-string parents by callable. - #[test] - fn execute_program_dispatches_get_parent_class_builtin() { - let program = parse_fragment( - br#"echo get_parent_class($object); echo ":"; +/// Verifies eval `get_parent_class()` reads object and class-string parents by callable. +#[test] +fn execute_program_dispatches_get_parent_class_builtin() { + let program = parse_fragment( + br#"echo get_parent_class($object); echo ":"; echo get_parent_class("ChildClass"); echo ":"; echo call_user_func("get_parent_class", $object); echo ":"; return call_user_func_array("get_parent_class", ["object_or_class" => "ChildClass"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "ParentClass:ParentClass:ParentClass:"); - assert_eq!( - values.get(result), - FakeValue::String("ParentClass".to_string()) - ); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ParentClass:ParentClass:ParentClass:"); + assert_eq!( + values.get(result), + FakeValue::String("ParentClass".to_string()) + ); +} - /// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. - #[test] - fn execute_program_dispatches_abs_builtin() { - let program = parse_fragment( - br#"echo abs(-5); echo ":"; +/// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. +#[test] +fn execute_program_dispatches_abs_builtin() { + let program = parse_fragment( + br#"echo abs(-5); echo ":"; echo abs(-2.5); echo ":"; echo gettype(abs(-2.5)); echo ":"; echo call_user_func("abs", -7); echo ":"; echo call_user_func_array("abs", [-9]); return function_exists("abs");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "5:2.5:double:7:9"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "5:2.5:double:7:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `floor()` and `ceil()` dispatch as double-returning math builtins. - #[test] - fn execute_program_dispatches_floor_and_ceil_builtins() { - let program = parse_fragment( - br#"echo floor(3.7); echo ":"; +/// Verifies eval `floor()` and `ceil()` dispatch as double-returning math builtins. +#[test] +fn execute_program_dispatches_floor_and_ceil_builtins() { + let program = parse_fragment( + br#"echo floor(3.7); echo ":"; echo gettype(floor(3)); echo ":"; echo ceil(3.2); echo ":"; echo gettype(ceil(3)); echo ":"; @@ -6244,42 +6212,42 @@ echo call_user_func("floor", 4.9); echo ":"; echo call_user_func_array("ceil", [4.1]); echo ":"; echo function_exists("floor"); return function_exists("ceil");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:double:4:double:4:5:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "3:double:4:double:4:5:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `fdiv()` and `fmod()` dispatch as floating-point binary builtins. - #[test] - fn execute_program_dispatches_float_binary_builtins() { - let program = parse_fragment( - br#"echo round(fdiv(10, 4), 2); echo ":"; +/// Verifies eval `fdiv()` and `fmod()` dispatch as floating-point binary builtins. +#[test] +fn execute_program_dispatches_float_binary_builtins() { + let program = parse_fragment( + br#"echo round(fdiv(10, 4), 2); echo ":"; echo gettype(fdiv(10, 4)); echo ":"; echo round(fmod(10.5, 3.2), 1); echo ":"; echo round(call_user_func("fdiv", 9, 2), 1); echo ":"; echo round(call_user_func_array("fmod", [10.5, 3.2]), 1); echo ":"; echo function_exists("fdiv"); return function_exists("fmod");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2.5:double:0.9:4.5:0.9:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "2.5:double:0.9:4.5:0.9:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval extended scalar math builtins support direct, named, callable, and probe paths. - #[test] - fn execute_program_dispatches_extended_math_builtins() { - let program = parse_fragment( - br#"echo sin(0); echo ":"; +/// Verifies eval extended scalar math builtins support direct, named, callable, and probe paths. +#[test] +fn execute_program_dispatches_extended_math_builtins() { + let program = parse_fragment( + br#"echo sin(0); echo ":"; echo cos(0); echo ":"; echo tan(0); echo ":"; echo round(asin(1), 2); echo ":"; @@ -6301,65 +6269,65 @@ echo round(call_user_func("sin", pi() / 2), 0); echo ":"; echo call_user_func_array("intdiv", ["num1" => 9, "num2" => 2]); echo ":"; echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); return function_exists("hypot");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. - #[test] - fn execute_program_dispatches_pow_builtin() { - let program = parse_fragment( - br#"echo pow(2, 3); echo ":"; +/// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. +#[test] +fn execute_program_dispatches_pow_builtin() { + let program = parse_fragment( + br#"echo pow(2, 3); echo ":"; echo gettype(pow(2, 3)); echo ":"; echo call_user_func("pow", 2, 5); echo ":"; echo call_user_func_array("pow", [3, 3]); return function_exists("pow");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "8:double:32:27"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "8:double:32:27"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `round()` supports default and explicit precision through callable paths. - #[test] - fn execute_program_dispatches_round_builtin() { - let program = parse_fragment( - br#"echo round(3.5); echo ":"; +/// Verifies eval `round()` supports default and explicit precision through callable paths. +#[test] +fn execute_program_dispatches_round_builtin() { + let program = parse_fragment( + br#"echo round(3.5); echo ":"; echo round(3.14159, 2); echo ":"; echo gettype(round(3)); echo ":"; echo call_user_func("round", 2.5); echo ":"; echo call_user_func_array("round", [1.55, 1]); return function_exists("round");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "4:3.14:double:3:1.6"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "4:3.14:double:3:1.6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `number_format()` groups and rounds numbers through callable paths. - #[test] - fn execute_program_dispatches_number_format_builtin() { - let program = parse_fragment( +/// Verifies eval `number_format()` groups and rounds numbers through callable paths. +#[test] +fn execute_program_dispatches_number_format_builtin() { + let program = parse_fragment( br#"echo number_format(1234567); echo ":"; echo number_format(1234.5678, 2); echo ":"; echo number_format(num: 1234567.89, decimals: 2, decimal_separator: ",", thousands_separator: "."); echo ":"; @@ -6369,23 +6337,23 @@ echo call_user_func_array("number_format", ["num" => 1234, "decimals" => 0, "dec return function_exists("number_format");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!( + values.output, + "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval printf-family builtins format, print, and dispatch through callables. - #[test] - fn execute_program_dispatches_printf_family_builtins() { - let program = parse_fragment( - br#"echo sprintf("Hello %s", "World"); echo ":"; +/// Verifies eval printf-family builtins format, print, and dispatch through callables. +#[test] +fn execute_program_dispatches_printf_family_builtins() { + let program = parse_fragment( + br#"echo sprintf("Hello %s", "World"); echo ":"; echo sprintf("%05d", 42); echo ":"; echo sprintf("%.2f", 3.14159); echo ":"; echo sprintf("%-6s|", "hi"); echo ":"; @@ -6398,25 +6366,25 @@ echo call_user_func("sprintf", "%+d", 42); echo ":"; echo call_user_func_array("vsprintf", ["format" => "%s", "values" => ["spread"]]); echo ":"; echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); return is_callable("vprintf");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `sscanf()` returns indexed string matches through callable paths. - #[test] - fn execute_program_dispatches_sscanf_builtin() { - let program = parse_fragment( - br#"$result = sscanf("John 1.5 30", "%s %f %d"); +/// Verifies eval `sscanf()` returns indexed string matches through callable paths. +#[test] +fn execute_program_dispatches_sscanf_builtin() { + let program = parse_fragment( + br#"$result = sscanf("John 1.5 30", "%s %f %d"); echo $result[0] . ":" . $result[1] . ":" . $result[2] . ":"; $named = sscanf(string: "Age: -25", format: "Age: %d"); echo $named[0] . ":"; @@ -6425,22 +6393,22 @@ echo $call[0] . ":"; $spread = call_user_func_array("sscanf", ["string" => "ok %", "format" => "%s %%"]); echo $spread[0] . ":"; return function_exists("sscanf");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "John:1.5:30:-25:-2.5e3:ok:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "John:1.5:30:-25:-2.5e3:ok:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `min()` and `max()` select numeric values directly and by callable. - #[test] - fn execute_program_dispatches_min_max_builtins() { - let program = parse_fragment( - br#"echo min(3, 1, 2); echo ":"; +/// Verifies eval `min()` and `max()` select numeric values directly and by callable. +#[test] +fn execute_program_dispatches_min_max_builtins() { + let program = parse_fragment( + br#"echo min(3, 1, 2); echo ":"; echo max(1, 3, 2); echo ":"; echo min(2.5, 1.5); echo ":"; echo max(1.5, 2.5); echo ":"; @@ -6448,22 +6416,22 @@ echo call_user_func("min", 9, 4, 7); echo ":"; echo call_user_func_array("max", [4, 8, 6]); echo ":"; echo function_exists("min"); return function_exists("max");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1:3:1.5:2.5:4:8:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "1:3:1.5:2.5:4:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `clamp()` selects numeric values through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_clamp_builtin() { - let program = parse_fragment( - br#"echo clamp(5, 0, 10); echo ":"; +/// Verifies eval `clamp()` selects numeric values through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_clamp_builtin() { + let program = parse_fragment( + br#"echo clamp(5, 0, 10); echo ":"; echo clamp(15, 0, 10); echo ":"; echo clamp(-5, 0, 10); echo ":"; echo clamp(2.75, 1.5, 2.5); echo ":"; @@ -6472,132 +6440,132 @@ echo call_user_func("clamp", -1, 0, 10); echo ":"; echo call_user_func_array("clamp", ["value" => 9, "min" => 0, "max" => 7]); echo ":"; echo function_exists("clamp"); return is_callable("clamp");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "5:10:0:2.5:5:0:7:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "5:10:0:2.5:5:0:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. - #[test] - fn execute_program_rejects_clamp_invalid_bounds() { - let program = parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. +#[test] +fn execute_program_rejects_clamp_invalid_bounds() { + let program = parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("invalid clamp bounds should fail"); + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("invalid clamp bounds should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - } + assert_eq!(err, EvalStatus::RuntimeFatal); +} - /// Verifies eval `pi()` returns a double constant directly and through callable paths. - #[test] - fn execute_program_dispatches_pi_builtin() { - let program = parse_fragment( - br#"echo round(pi(), 2); echo ":"; +/// Verifies eval `pi()` returns a double constant directly and through callable paths. +#[test] +fn execute_program_dispatches_pi_builtin() { + let program = parse_fragment( + br#"echo round(pi(), 2); echo ":"; echo gettype(pi()); echo ":"; echo round(call_user_func("pi"), 3); echo ":"; echo round(call_user_func_array("pi", []), 4); echo ":"; return function_exists("pi");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3.14:double:3.142:3.1416:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "3.14:double:3.142:3.1416:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. - #[test] - fn execute_program_dispatches_sqrt_builtin() { - let program = parse_fragment( - br#"echo sqrt(16); echo ":"; +/// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. +#[test] +fn execute_program_dispatches_sqrt_builtin() { + let program = parse_fragment( + br#"echo sqrt(16); echo ":"; echo gettype(sqrt(9)); echo ":"; echo call_user_func("sqrt", 25); echo ":"; echo call_user_func_array("sqrt", [36]); return function_exists("sqrt");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "4:double:5:6"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "4:double:5:6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `strrev()` dispatches through direct and callable paths. - #[test] - fn execute_program_dispatches_strrev_builtin() { - let program = parse_fragment( - br#"echo strrev("Hello"); echo ":"; +/// Verifies eval `strrev()` dispatches through direct and callable paths. +#[test] +fn execute_program_dispatches_strrev_builtin() { + let program = parse_fragment( + br#"echo strrev("Hello"); echo ":"; echo strrev(123); echo ":"; echo call_user_func("strrev", "ABC"); echo ":"; echo call_user_func_array("strrev", ["def"]); echo ":"; return function_exists("strrev");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "olleH:321:CBA:fed:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "olleH:321:CBA:fed:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `chr()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_chr_builtin() { - let program = parse_fragment( - br#"echo chr(65); echo ":"; +/// Verifies eval `chr()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_chr_builtin() { + let program = parse_fragment( + br#"echo chr(65); echo ":"; echo bin2hex(chr(codepoint: 256)); echo ":"; echo bin2hex(call_user_func("chr", 257)); echo ":"; echo call_user_func_array("chr", ["codepoint" => 321]); echo ":"; return function_exists("chr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "A:00:01:A:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "A:00:01:A:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_str_repeat_builtin() { - let program = parse_fragment( - br#"echo str_repeat("ha", 3); echo ":"; +/// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_str_repeat_builtin() { + let program = parse_fragment( + br#"echo str_repeat("ha", 3); echo ":"; echo strlen(str_repeat(string: "x", times: 0)); echo ":"; echo call_user_func("str_repeat", "ab", 2); echo ":"; echo call_user_func_array("str_repeat", ["string" => "z", "times" => 3]); echo ":"; return function_exists("str_repeat");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "hahaha:0:abab:zzz:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "hahaha:0:abab:zzz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `substr()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_substr_builtin() { - let program = parse_fragment( +/// Verifies eval `substr()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_substr_builtin() { + let program = parse_fragment( br#"echo substr("abcdef", 2); echo ":"; echo substr(string: "abcdef", offset: 1, length: -1); echo ":"; echo substr("abcdef", -2); echo ":"; @@ -6606,19 +6574,19 @@ echo call_user_func_array("substr", ["string" => "abcdef", "offset" => -4, "leng return function_exists("substr");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "cdef:bcde:ef:cd:cd:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "cdef:bcde:ef:cd:cd:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `substr_replace()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_substr_replace_builtin() { - let program = parse_fragment( +/// Verifies eval `substr_replace()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_substr_replace_builtin() { + let program = parse_fragment( br#"echo substr_replace("hello world", "PHP", 6, 5); echo ":"; echo substr_replace(string: "abcdef", replace: "X", offset: 1, length: -1); echo ":"; echo substr_replace("abcdef", "X", -2); echo ":"; @@ -6627,212 +6595,212 @@ echo call_user_func_array("substr_replace", ["string" => "abcdef", "replace" => return function_exists("substr_replace");"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "hello PHP:aXf:abcdX:abcdefX:Xcdef:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "hello PHP:aXf:abcdX:abcdefX:Xcdef:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_nl2br_builtin() { - let program = parse_fragment( - br#"echo bin2hex(nl2br("a\nb")); echo ":"; +/// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_nl2br_builtin() { + let program = parse_fragment( + br#"echo bin2hex(nl2br("a\nb")); echo ":"; echo bin2hex(nl2br(string: "a\nb", use_xhtml: false)); echo ":"; echo bin2hex(call_user_func("nl2br", "a\r\nb")); echo ":"; echo bin2hex(call_user_func_array("nl2br", ["string" => "a\n\rb", "use_xhtml" => false])); echo ":"; return function_exists("nl2br");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_bin2hex_builtin() { - let program = parse_fragment( - br#"echo bin2hex("Az"); echo ":"; +/// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_bin2hex_builtin() { + let program = parse_fragment( + br#"echo bin2hex("Az"); echo ":"; echo bin2hex(string: "A\n"); echo ":"; echo call_user_func("bin2hex", "!?"); echo ":"; echo call_user_func_array("bin2hex", ["string" => "ok"]); echo ":"; return function_exists("bin2hex");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "417a:410a:213f:6f6b:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "417a:410a:213f:6f6b:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `hex2bin()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_hex2bin_builtin() { - let program = parse_fragment( - br#"echo hex2bin("417a"); echo ":"; +/// Verifies eval `hex2bin()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_hex2bin_builtin() { + let program = parse_fragment( + br#"echo hex2bin("417a"); echo ":"; echo bin2hex(hex2bin(string: "410a")); echo ":"; echo call_user_func("hex2bin", "213f"); echo ":"; echo call_user_func_array("hex2bin", ["string" => "6f6b"]); echo ":"; echo hex2bin("4") ? "bad" : "false"; echo ":"; return function_exists("hex2bin");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Az:410a:!?:ok:false:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - assert_eq!( - values.warnings, - vec![HEX2BIN_ODD_LENGTH_WARNING.to_string()] - ); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Az:410a:!?:ok:false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + vec![HEX2BIN_ODD_LENGTH_WARNING.to_string()] + ); +} - /// Verifies eval slash escaping builtins use PHP byte-string semantics. - #[test] - fn execute_program_dispatches_slash_escape_builtins() { - let program = parse_fragment( - br#"$escaped = addslashes($source); +/// Verifies eval slash escaping builtins use PHP byte-string semantics. +#[test] +fn execute_program_dispatches_slash_escape_builtins() { + let program = parse_fragment( + br#"$escaped = addslashes($source); echo bin2hex($escaped); echo ":"; echo bin2hex(stripslashes($escaped)); echo ":"; echo call_user_func("addslashes", "x\"y"); echo ":"; echo call_user_func_array("stripslashes", [addslashes("o\"k")]); echo ":"; return function_exists("addslashes") && function_exists("stripslashes");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let source = values.string("a\0b\\c\"d'").expect("create source"); - scope.set("source", source, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let source = values.string("a\0b\\c\"d'").expect("create source"); + scope.set("source", source, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_base64_encode_builtin() { - let program = parse_fragment( - br#"echo base64_encode("Hello"); echo ":"; +/// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_base64_encode_builtin() { + let program = parse_fragment( + br#"echo base64_encode("Hello"); echo ":"; echo base64_encode(string: "Hi"); echo ":"; echo call_user_func("base64_encode", "Test 123!"); echo ":"; echo call_user_func_array("base64_encode", ["string" => ""]); echo ":"; return function_exists("base64_encode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "SGVsbG8=:SGk=:VGVzdCAxMjMh::"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "SGVsbG8=:SGk=:VGVzdCAxMjMh::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies eval `base64_decode()` dispatches through direct, named, and callable paths. - #[test] - fn execute_program_dispatches_base64_decode_builtin() { - let program = parse_fragment( - br#"echo base64_decode("SGVsbG8="); echo ":"; +/// Verifies eval `base64_decode()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_base64_decode_builtin() { + let program = parse_fragment( + br#"echo base64_decode("SGVsbG8="); echo ":"; echo base64_decode(string: "SGk="); echo ":"; echo call_user_func("base64_decode", "VGVzdCAxMjMh"); echo ":"; echo call_user_func_array("base64_decode", ["string" => ""]); echo ":"; return function_exists("base64_decode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Hello:Hi:Test 123!::"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - } + assert_eq!(values.output, "Hello:Hi:Test 123!::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} - /// Verifies `isset` distinguishes missing, null, and other falsey values. - #[test] - fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { - let program = parse_fragment( - br#"if (isset($missing)) { echo "1"; } else { echo "0"; } +/// Verifies `isset` distinguishes missing, null, and other falsey values. +#[test] +fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { + let program = parse_fragment( + br#"if (isset($missing)) { echo "1"; } else { echo "0"; } if (isset($nullish)) { echo "1"; } else { echo "0"; } if (isset($zero)) { echo "1"; } else { echo "0"; } if (isset($empty)) { echo "1"; } else { echo "0"; } if (isset($zero, $empty)) { echo "1"; } else { echo "0"; } if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let nullish = values.null().expect("create fake null"); - let zero = values.int(0).expect("create fake int"); - let empty = values.string("").expect("create fake string"); - scope.set("nullish", nullish, ScopeCellOwnership::Owned); - scope.set("zero", zero, ScopeCellOwnership::Owned); - scope.set("empty", empty, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "001110"); - assert_eq!(values.get(result), FakeValue::Null); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty = values.string("").expect("create fake string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty", empty, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "001110"); + assert_eq!(values.get(result), FakeValue::Null); +} - /// Verifies `empty` treats missing, null, and falsey values as empty. - #[test] - fn execute_program_empty_uses_php_truthiness_without_missing_warnings() { - let program = parse_fragment( - br#"if (empty($missing)) { echo "1"; } else { echo "0"; } +/// Verifies `empty` treats missing, null, and falsey values as empty. +#[test] +fn execute_program_empty_uses_php_truthiness_without_missing_warnings() { + let program = parse_fragment( + br#"if (empty($missing)) { echo "1"; } else { echo "0"; } if (empty($nullish)) { echo "1"; } else { echo "0"; } if (empty($zero)) { echo "1"; } else { echo "0"; } if (empty($empty_string)) { echo "1"; } else { echo "0"; } if (empty($zero_string)) { echo "1"; } else { echo "0"; } if (empty($value)) { echo "1"; } else { echo "0"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let nullish = values.null().expect("create fake null"); - let zero = values.int(0).expect("create fake int"); - let empty_string = values.string("").expect("create fake empty string"); - let zero_string = values.string("0").expect("create fake zero string"); - let value = values.string("x").expect("create fake non-empty string"); - scope.set("nullish", nullish, ScopeCellOwnership::Owned); - scope.set("zero", zero, ScopeCellOwnership::Owned); - scope.set("empty_string", empty_string, ScopeCellOwnership::Owned); - scope.set("zero_string", zero_string, ScopeCellOwnership::Owned); - scope.set("value", value, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "111110"); - assert_eq!(values.get(result), FakeValue::Null); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty_string = values.string("").expect("create fake empty string"); + let zero_string = values.string("0").expect("create fake zero string"); + let value = values.string("x").expect("create fake non-empty string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty_string", empty_string, ScopeCellOwnership::Owned); + scope.set("zero_string", zero_string, ScopeCellOwnership::Owned); + scope.set("value", value, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111110"); + assert_eq!(values.get(result), FakeValue::Null); +} - /// Verifies `isset` and `empty` use PHP offset semantics for array reads. - #[test] - fn execute_program_isset_and_empty_support_array_offsets() { - let program = parse_fragment( - br#"$map = [ +/// Verifies `isset` and `empty` use PHP offset semantics for array reads. +#[test] +fn execute_program_isset_and_empty_support_array_offsets() { + let program = parse_fragment( + br#"$map = [ "present" => "x", "nullish" => null, "zero" => 0, @@ -6855,69 +6823,69 @@ echo empty($map["empty"]) ? "1" : "0"; echo empty($map["child"]["leaf"]) ? "1" : "0"; echo empty($map["child"]["null"]) ? "1" : "0"; echo empty($map["missing"]["leaf"]) ? "1" : "0";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1001100:01111011"); - assert_eq!(values.get(result), FakeValue::Null); - } + assert_eq!(values.output, "1001100:01111011"); + assert_eq!(values.get(result), FakeValue::Null); +} - /// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. - #[test] - fn execute_program_function_probes_use_eval_context() { - let program = parse_fragment( - br#"function dyn_probe() { return 1; } +/// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. +#[test] +fn execute_program_function_probes_use_eval_context() { + let program = parse_fragment( + br#"function dyn_probe() { return 1; } echo function_exists("DYN_PROBE") . "x"; echo is_callable("dyn_probe") . "x"; echo function_exists("strlen") . "x"; echo function_exists("native_probe") . "x"; echo function_exists("eval") . "x"; echo function_exists("missing_probe") . "x";"#, - ) - .expect("parse eval fragment"); - let native = NativeFunction::new(1usize as *mut c_void, fake_native_return_descriptor, 0); - let mut context = ElephcEvalContext::new(); - assert!(context - .define_native_function("native_probe", native) - .is_ok()); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.output, "1x1x1x1xxx"); - } + ) + .expect("parse eval fragment"); + let native = NativeFunction::new(1usize as *mut c_void, fake_native_return_descriptor, 0); + let mut context = ElephcEvalContext::new(); + assert!(context + .define_native_function("native_probe", native) + .is_ok()); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "1x1x1x1xxx"); +} - /// Verifies eval `interface_exists()` probes generated interface metadata by callable. - #[test] - fn execute_program_interface_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"echo interface_exists("KnownInterface") ? "Y" : "N"; +/// Verifies eval `interface_exists()` probes generated interface metadata by callable. +#[test] +fn execute_program_interface_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo interface_exists("KnownInterface") ? "Y" : "N"; echo interface_exists("knowninterface") ? "Y" : "N"; echo interface_exists("KnownClass") ? "Y" : "N"; echo call_user_func("interface_exists", "KnownInterface") ? "Y" : "N"; echo call_user_func_array("interface_exists", ["interface" => "KnownInterface"]) ? "Y" : "N"; echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "YYNYYN"); - } + assert_eq!(values.output, "YYNYYN"); +} - /// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. - #[test] - fn execute_program_class_like_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"echo trait_exists("KnownTrait") ? "T" : "t"; +/// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. +#[test] +fn execute_program_class_like_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo trait_exists("KnownTrait") ? "T" : "t"; echo trait_exists("knowntrait") ? "T" : "t"; echo trait_exists("KnownEnum") ? "T" : "t"; echo enum_exists("KnownEnum") ? "E" : "e"; @@ -6927,20 +6895,20 @@ echo call_user_func("trait_exists", "KnownTrait") ? "T" : "t"; echo call_user_func_array("enum_exists", ["enum" => "KnownEnum"]) ? "E" : "e"; echo trait_exists(trait: "MissingTrait", autoload: false) ? "T" : "t"; echo enum_exists(enum: "MissingEnum", autoload: false) ? "E" : "e";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "TTtEEeTEte"); - } + assert_eq!(values.output, "TTtEEeTEte"); +} - /// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. - #[test] - fn execute_program_is_a_relation_uses_runtime_probe() { - let program = parse_fragment( +/// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. +#[test] +fn execute_program_is_a_relation_uses_runtime_probe() { + let program = parse_fragment( br#"$object = new KnownClass(); echo is_a($object, "KnownClass") ? "Y" : "N"; echo is_subclass_of($object, "KnownClass") ? "Y" : "N"; @@ -6950,19 +6918,19 @@ echo call_user_func_array("is_subclass_of", ["object_or_class" => $object, "clas echo is_a(object_or_class: $object, class: "MissingClass", allow_string: false) ? "Y" : "N";"#, ) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "YNYYYN"); - } + assert_eq!(values.output, "YNYYYN"); +} - /// Verifies eval `define()` and `defined()` share a dynamic constant-name table. - #[test] - fn execute_program_define_and_defined_use_dynamic_constant_table() { - let program = parse_fragment( - br#"echo define("DynEvalConst", "ok") ? "Y" : "N"; +/// Verifies eval `define()` and `defined()` share a dynamic constant-name table. +#[test] +fn execute_program_define_and_defined_use_dynamic_constant_table() { + let program = parse_fragment( + br#"echo define("DynEvalConst", "ok") ? "Y" : "N"; echo DynEvalConst; echo \DynEvalConst; echo defined("DynEvalConst") ? "Y" : "N"; @@ -6971,25 +6939,25 @@ echo defined("dynevalconst") ? "Y" : "N"; echo define("DynEvalConst", 2) ? "Y" : "N"; echo call_user_func("defined", "DynEvalConst") ? "Y" : "N"; echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YokokYYNNYY"); - assert_eq!( - values.warnings, - vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] - ); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YokokYYNNYY"); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); +} - /// Verifies eval predefined runtime constants are fetchable and cannot be redefined. - #[test] - fn execute_program_reads_predefined_runtime_constants() { - let program = parse_fragment( - br#"echo PHP_EOL === "\n" ? "eol" : "bad"; echo ":"; +/// Verifies eval predefined runtime constants are fetchable and cannot be redefined. +#[test] +fn execute_program_reads_predefined_runtime_constants() { + let program = parse_fragment( + br#"echo PHP_EOL === "\n" ? "eol" : "bad"; echo ":"; echo (PHP_OS === "Darwin" || PHP_OS === "Linux") ? "os" : "bad"; echo ":"; echo DIRECTORY_SEPARATOR; echo ":"; echo PHP_INT_MAX > 9000000000000000000 ? "int" : "bad"; echo ":"; @@ -6998,282 +6966,281 @@ echo defined("\\PHP_OS") ? "root" : "bad"; echo ":"; echo defined("php_os") ? "bad" : "case"; echo ":"; echo define("PHP_OS", "x") ? "bad" : "locked"; echo ":"; return PHP_INT_MAX;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "eol:os:/:int:defined:root:case:locked:"); - assert_eq!(values.get(result), FakeValue::Int(i64::MAX)); - assert_eq!( - values.warnings, - vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] - ); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "eol:os:/:int:defined:root:case:locked:"); + assert_eq!(values.get(result), FakeValue::Int(i64::MAX)); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); +} - /// Verifies missing eval dynamic constants fail through runtime status. - #[test] - fn execute_program_missing_constant_fetch_fails() { - let program = parse_fragment(br#"return MissingEvalConst;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies missing eval dynamic constants fail through runtime status. +#[test] +fn execute_program_missing_constant_fetch_fails() { + let program = parse_fragment(br#"return MissingEvalConst;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("missing constant should fail"); + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing constant should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - } + assert_eq!(err, EvalStatus::RuntimeFatal); +} - /// Verifies eval class probes use the runtime class-name table. - #[test] - fn execute_program_class_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"class DynProbe {} +/// Verifies eval class probes use the runtime class-name table. +#[test] +fn execute_program_class_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"class DynProbe {} echo class_exists("DynProbe") ? "Y" : "N"; echo class_exists("\dynprobe") ? "Y" : "N"; echo class_exists("KnownClass") ? "Y" : "N"; echo class_exists("\knownclass") ? "Y" : "N"; echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "YYYYN"); - } + assert_eq!(values.output, "YYYYN"); +} - /// Verifies duplicate eval-declared class names fail through runtime status. - #[test] - fn execute_program_duplicate_class_declaration_fails() { - let program = parse_fragment( - br#"class DynProbeDup {} +/// Verifies duplicate eval-declared class names fail through runtime status. +#[test] +fn execute_program_duplicate_class_declaration_fails() { + let program = parse_fragment( + br#"class DynProbeDup {} class dynprobedup {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values).expect_err("duplicate fails"); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - /// Verifies eval fragments can dispatch registered native AOT functions. - #[test] - fn execute_program_calls_registered_native_function() { - let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } - - /// Verifies direct eval calls can bind registered native parameters by name. - #[test] - fn execute_program_calls_registered_native_function_with_named_args() { - let program = parse_fragment(br#"return native_answer(right: 2, left: 1);"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } + let err = execute_program(&program, &mut scope, &mut values).expect_err("duplicate fails"); - /// Verifies direct eval calls can unpack arrays into registered native parameters. - #[test] - fn execute_program_calls_registered_native_function_with_spread_args() { - let program = - parse_fragment(br#"return native_answer(...[1, 2]);"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); - } + assert_eq!(err, EvalStatus::RuntimeFatal); +} - /// Verifies indexed array writes mutate an existing scope array. - #[test] - fn execute_program_writes_indexed_scope_array() { - let program = parse_fragment(br#"$items = ["a"]; $items[1] = "b"; return $items[1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies eval fragments can dispatch registered native AOT functions. +#[test] +fn execute_program_calls_registered_native_function() { + let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); +/// Verifies direct eval calls can bind registered native parameters by name. +#[test] +fn execute_program_calls_registered_native_function_with_named_args() { + let program = parse_fragment(br#"return native_answer(right: 2, left: 1);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} - assert_eq!(values.get(result), FakeValue::String("b".to_string())); - } +/// Verifies direct eval calls can unpack arrays into registered native parameters. +#[test] +fn execute_program_calls_registered_native_function_with_spread_args() { + let program = + parse_fragment(br#"return native_answer(...[1, 2]);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} - /// Verifies indexed array append writes use the next visible index. - #[test] - fn execute_program_appends_indexed_scope_array() { - let program = parse_fragment(br#"$items = ["a"]; $items[] = "b"; return $items[1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies indexed array writes mutate an existing scope array. +#[test] +fn execute_program_writes_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[1] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("b".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} - /// Verifies associative append starts at key zero when only string keys exist. - #[test] - fn execute_program_appends_assoc_scope_array_with_string_keys() { - let program = - parse_fragment(br#"$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies indexed array append writes use the next visible index. +#[test] +fn execute_program_appends_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} - /// Verifies associative append uses one plus the largest existing integer key. - #[test] - fn execute_program_appends_assoc_scope_array_after_positive_int_key() { - let program = parse_fragment( - br#"$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies associative append starts at key zero when only string keys exist. +#[test] +fn execute_program_appends_assoc_scope_array_with_string_keys() { + let program = + parse_fragment(br#"$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} - /// Verifies associative append preserves PHP's largest-negative-key behavior. - #[test] - fn execute_program_appends_assoc_scope_array_after_negative_int_key() { - let program = - parse_fragment(br#"$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies associative append uses one plus the largest existing integer key. +#[test] +fn execute_program_appends_assoc_scope_array_after_positive_int_key() { + let program = parse_fragment( + br#"$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); - } + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - /// Verifies mutating a borrowed scope array does not make the eval scope own it. - #[test] - fn execute_program_preserves_borrowed_array_ownership() { - let program = parse_fragment(br#"$items[0] = "b";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let array = values.array_new(1).expect("create fake array"); - scope.set("items", array, ScopeCellOwnership::Borrowed); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let entry = scope.entry("items").expect("scope should contain items"); - - assert_eq!(entry.cell(), array); - assert_eq!(entry.flags().ownership, ScopeCellOwnership::Borrowed); - assert!(values.releases.is_empty()); - } +/// Verifies associative append preserves PHP's largest-negative-key behavior. +#[test] +fn execute_program_appends_assoc_scope_array_after_negative_int_key() { + let program = + parse_fragment(br#"$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - /// Verifies replacing an eval-owned scope value releases the old cell. - #[test] - fn execute_program_releases_replaced_scope_value() { - let program = parse_fragment(br#"$x = "old"; $x = "new";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} - assert_eq!(values.releases.len(), 1); - assert_eq!( - values.get(values.releases[0]), - FakeValue::String("old".to_string()) - ); - } +/// Verifies mutating a borrowed scope array does not make the eval scope own it. +#[test] +fn execute_program_preserves_borrowed_array_ownership() { + let program = parse_fragment(br#"$items[0] = "b";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let array = values.array_new(1).expect("create fake array"); + scope.set("items", array, ScopeCellOwnership::Borrowed); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let entry = scope.entry("items").expect("scope should contain items"); + + assert_eq!(entry.cell(), array); + assert_eq!(entry.flags().ownership, ScopeCellOwnership::Borrowed); + assert!(values.releases.is_empty()); +} - /// Verifies unsetting an eval-owned scope value releases the old cell. - #[test] - fn execute_program_releases_unset_scope_value() { - let program = parse_fragment(br#"$x = "old"; unset($x);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); +/// Verifies replacing an eval-owned scope value releases the old cell. +#[test] +fn execute_program_releases_replaced_scope_value() { + let program = parse_fragment(br#"$x = "old"; $x = "new";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.releases.len(), 1); - assert_eq!( - values.get(values.releases[0]), - FakeValue::String("old".to_string()) - ); - } + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); +} - /// Verifies break exits a runtime eval loop before later statements run. - #[test] - fn execute_program_break_exits_loop() { - let program = parse_fragment(br#"while ($flag) { echo "a"; break; echo "b"; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.bool_value(true).expect("create fake bool"); - scope.set("flag", flag, ScopeCellOwnership::Owned); +/// Verifies unsetting an eval-owned scope value releases the old cell. +#[test] +fn execute_program_releases_unset_scope_value() { + let program = parse_fragment(br#"$x = "old"; unset($x);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "a"); - } + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); +} - /// Verifies continue restarts a runtime eval loop and observes later scope updates. - #[test] - fn execute_program_continue_restarts_loop() { - let program = parse_fragment( - br#"while ($flag) { $flag = false; continue; echo "unreachable"; } echo "done";"#, - ) +/// Verifies break exits a runtime eval loop before later statements run. +#[test] +fn execute_program_break_exits_loop() { + let program = parse_fragment(br#"while ($flag) { echo "a"; break; echo "b"; }"#) .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.bool_value(true).expect("create fake bool"); - scope.set("flag", flag, ScopeCellOwnership::Owned); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "done"); - } + assert_eq!(values.output, "a"); +} + +/// Verifies continue restarts a runtime eval loop and observes later scope updates. +#[test] +fn execute_program_continue_restarts_loop() { + let program = parse_fragment( + br#"while ($flag) { $flag = false; continue; echo "unreachable"; } echo "done";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "done"); +} From c0a38704f0fff23cbeb2d5a8c8203380e7488782 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 09:55:18 +0200 Subject: [PATCH 0282/1208] Extract eval interpreter builtins module --- .../elephc-eval/src/interpreter/builtins.rs | 11733 +++++++++++++++ crates/elephc-eval/src/interpreter/mod.rs | 12083 +--------------- 2 files changed, 11916 insertions(+), 11900 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins.rs diff --git a/crates/elephc-eval/src/interpreter/builtins.rs b/crates/elephc-eval/src/interpreter/builtins.rs new file mode 100644 index 0000000000..cea21038d4 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins.rs @@ -0,0 +1,11733 @@ +//! Purpose: +//! Implements eval support for PHP-visible builtins and language-construct helpers. +//! This module owns builtin argument binding, direct builtin execution, callable +//! builtin dispatch, and per-domain helper routines. +//! +//! Called from: +//! - `crate::interpreter::eval_call()` and positional builtin dispatch paths. +//! +//! Key details: +//! - The module is a child of `interpreter` so it can reuse core EvalIR execution +//! helpers without widening crate-level visibility. +//! - Runtime value creation and PHP coercions still flow through `RuntimeValueOps`. + +use super::*; + +/// Evaluates string-name function probes against eval and supported builtin tables. +pub(super) fn eval_builtin_function_probe( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\').to_ascii_lowercase(); + values.bool_value(eval_function_probe_exists(context, &name)) +} + +/// Evaluates `define(name, value)` for eval dynamic constant-name registration. +pub(super) fn eval_builtin_define( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + let defined = eval_define_name(name, value, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `defined(name)` against eval dynamic constant names. +pub(super) fn eval_builtin_defined( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let exists = eval_defined_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `define(...)` from already materialized call arguments. +fn eval_define_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let defined = eval_define_name(*name, *value, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `defined(...)` from already materialized call arguments. +fn eval_defined_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let exists = eval_defined_name(*name, context, values)?; + values.bool_value(exists) +} + +/// Normalizes and registers one eval dynamic constant name. +fn eval_define_name( + name: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + if name.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if eval_predefined_constant_value(&name).is_some() || context.has_constant(&name) { + values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; + return Ok(false); + } + let value = values.retain(value)?; + if context.define_constant(&name, value) { + Ok(true) + } else { + values.release(value)?; + Ok(false) + } +} + +/// Normalizes and probes one eval dynamic constant name. +fn eval_defined_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) +} + +/// Reads a PHP constant name from a runtime cell without changing case. +fn eval_constant_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. +pub(super) fn eval_builtin_class_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_exists_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `class_exists(...)` from already materialized call arguments. +fn eval_class_exists_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_class_exists_name(*name, context, values)?, + [name, _autoload] => eval_class_exists_name(*name, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. +fn eval_class_exists_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\'); + if context.has_class(name) { + return Ok(true); + } + values.class_exists(name) +} + +/// Evaluates `interface_exists(...)` against generated interface-name metadata. +pub(super) fn eval_builtin_interface_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_interface_exists_name(name, values)?; + values.bool_value(exists) +} + +/// Evaluates `interface_exists(...)` from already materialized call arguments. +fn eval_interface_exists_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_interface_exists_name(*name, values)?, + [name, _autoload] => eval_interface_exists_name(*name, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP interface-name cell and probes generated interface metadata. +fn eval_interface_exists_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + values.interface_exists(name.trim_start_matches('\\')) +} + +/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. +pub(super) fn eval_builtin_class_like_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = match args { + [symbol] => eval_expr(symbol, context, scope, values)?, + [symbol, autoload] => { + let symbol = eval_expr(symbol, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + symbol + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_like_exists_name(name, symbol, values)?; + values.bool_value(exists) +} + +/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. +fn eval_class_like_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [symbol] => eval_class_like_exists_name(name, *symbol, values)?, + [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. +fn eval_class_like_exists_name( + name: &str, + symbol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = values.string_bytes(symbol)?; + let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; + let symbol = symbol.trim_start_matches('\\'); + match name { + "trait_exists" => values.trait_exists(symbol), + "enum_exists" => values.enum_exists(symbol), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. +pub(super) fn eval_builtin_is_a_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_is_a_relation_result(name, &evaluated_args, context, values) +} + +/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. +fn eval_is_a_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_or_class, target_class, allow_string) = match evaluated_args { + [object_or_class, target_class] => { + (*object_or_class, *target_class, name == "is_subclass_of") + } + [object_or_class, target_class, allow_string] => ( + *object_or_class, + *target_class, + values.truthy(*allow_string)?, + ), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let target_class = values.string_bytes(target_class)?; + let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_class = target_class.trim_start_matches('\\'); + let is_object = values.type_tag(object_or_class)? == 6; + let result = + if is_object && dynamic_object_is_a(object_or_class, target_class, context, values)? { + !matches!(name, "is_subclass_of") + } else if is_object || allow_string { + values.object_is_a(object_or_class, target_class, name == "is_subclass_of")? + } else { + false + }; + values.bool_value(result) +} + +/// Returns whether an eval-created object matches a dynamic class name exactly. +fn dynamic_object_is_a( + object: RuntimeCellHandle, + target_class: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + Ok(context + .dynamic_object_class(identity) + .is_some_and(|class| class.name().eq_ignore_ascii_case(target_class))) +} + +/// Evaluates PHP's `isset(...)` language construct over eval-visible values. +pub(super) fn eval_builtin_isset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return values.bool_value(false); + } + for arg in args { + if !eval_isset_arg(arg, context, scope, values)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates PHP's `empty(...)` language construct over eval-visible values. +pub(super) fn eval_builtin_empty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = eval_empty_arg(arg, context, scope, values)?; + values.bool_value(empty) +} + +/// Evaluates one `empty` operand without warning or failing on missing variables. +fn eval_empty_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(true); + }; + return Ok(!values.truthy(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.truthy(value)?) +} + +/// Evaluates one `isset` operand without allocating a null cell for missing variables. +fn eval_isset_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(false); + }; + return Ok(!values.is_null(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.is_null(value)?) +} + +/// Returns true when a PHP function name is visible to eval builtin probes. +fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { + !name.contains("::") && (context.has_function(name) || eval_php_visible_builtin_exists(name)) +} + +/// Returns true for PHP-visible builtin names implemented by the eval interpreter. +pub(super) fn eval_php_visible_builtin_exists(name: &str) -> bool { + matches!( + name, + "abs" + | "addslashes" + | "array_chunk" + | "array_column" + | "array_combine" + | "array_fill" + | "array_fill_keys" + | "array_filter" + | "array_flip" + | "array_map" + | "array_reduce" + | "array_walk" + | "array_key_exists" + | "array_keys" + | "array_diff" + | "array_intersect" + | "array_diff_key" + | "array_intersect_key" + | "array_merge" + | "array_pad" + | "array_pop" + | "array_product" + | "array_push" + | "array_rand" + | "array_reverse" + | "array_search" + | "array_shift" + | "array_slice" + | "array_splice" + | "array_sum" + | "array_unique" + | "array_unshift" + | "array_values" + | "arsort" + | "asort" + | "acos" + | "asin" + | "atan" + | "atan2" + | "basename" + | "base64_decode" + | "base64_encode" + | "bin2hex" + | "ceil" + | "chdir" + | "chmod" + | "call_user_func" + | "call_user_func_array" + | "class_exists" + | "enum_exists" + | "interface_exists" + | "is_a" + | "is_subclass_of" + | "boolval" + | "chop" + | "chr" + | "clamp" + | "clearstatcache" + | "count" + | "copy" + | "cos" + | "cosh" + | "crc32" + | "ctype_alnum" + | "ctype_alpha" + | "ctype_digit" + | "ctype_space" + | "date" + | "define" + | "defined" + | "deg2rad" + | "dirname" + | "disk_free_space" + | "disk_total_space" + | "exp" + | "explode" + | "fdiv" + | "file" + | "file_exists" + | "fileatime" + | "filectime" + | "filegroup" + | "file_get_contents" + | "fileinode" + | "filemtime" + | "fileowner" + | "fileperms" + | "file_put_contents" + | "filesize" + | "filetype" + | "fnmatch" + | "floor" + | "floatval" + | "fmod" + | "function_exists" + | "gethostbyaddr" + | "gethostbyname" + | "gethostname" + | "getprotobyname" + | "getprotobynumber" + | "getservbyname" + | "getservbyport" + | "get_class" + | "get_parent_class" + | "get_resource_id" + | "get_resource_type" + | "getcwd" + | "getenv" + | "gettype" + | "glob" + | "hash" + | "hash_algos" + | "hash_equals" + | "hash_file" + | "hash_hmac" + | "hex2bin" + | "html_entity_decode" + | "htmlentities" + | "htmlspecialchars" + | "hypot" + | "implode" + | "in_array" + | "inet_ntop" + | "inet_pton" + | "intdiv" + | "ip2long" + | "is_dir" + | "is_executable" + | "is_file" + | "is_link" + | "is_readable" + | "is_writable" + | "is_writeable" + | "intval" + | "link" + | "linkinfo" + | "ltrim" + | "is_callable" + | "is_array" + | "is_bool" + | "is_double" + | "is_finite" + | "is_float" + | "is_infinite" + | "is_int" + | "is_integer" + | "is_iterable" + | "is_long" + | "is_nan" + | "is_null" + | "is_numeric" + | "is_object" + | "is_real" + | "is_resource" + | "is_string" + | "iterator_apply" + | "iterator_count" + | "iterator_to_array" + | "json_decode" + | "json_encode" + | "json_last_error" + | "json_last_error_msg" + | "json_validate" + | "krsort" + | "ksort" + | "lcfirst" + | "log" + | "log2" + | "log10" + | "long2ip" + | "max" + | "md5" + | "microtime" + | "min" + | "mkdir" + | "mktime" + | "mt_rand" + | "natcasesort" + | "natsort" + | "nl2br" + | "number_format" + | "ord" + | "pathinfo" + | "pi" + | "pow" + | "php_uname" + | "phpversion" + | "preg_match" + | "preg_match_all" + | "preg_replace" + | "preg_replace_callback" + | "preg_split" + | "putenv" + | "print_r" + | "rand" + | "random_int" + | "range" + | "rad2deg" + | "rawurldecode" + | "rawurlencode" + | "readfile" + | "readlink" + | "realpath" + | "realpath_cache_get" + | "realpath_cache_size" + | "rename" + | "rsort" + | "rtrim" + | "round" + | "rmdir" + | "scandir" + | "settype" + | "sleep" + | "sha1" + | "shuffle" + | "sin" + | "sinh" + | "sort" + | "sqrt" + | "spl_classes" + | "spl_object_hash" + | "spl_object_id" + | "sscanf" + | "sprintf" + | "strcasecmp" + | "stream_get_filters" + | "stream_get_transports" + | "stream_get_wrappers" + | "str_contains" + | "str_ends_with" + | "str_ireplace" + | "str_repeat" + | "str_replace" + | "str_starts_with" + | "strcmp" + | "stat" + | "strlen" + | "strpos" + | "strrpos" + | "strrev" + | "str_pad" + | "str_split" + | "strstr" + | "strtotime" + | "substr" + | "stripslashes" + | "strtolower" + | "strtoupper" + | "strval" + | "symlink" + | "sys_get_temp_dir" + | "tempnam" + | "tan" + | "tanh" + | "time" + | "touch" + | "trait_exists" + | "trim" + | "substr_replace" + | "ucfirst" + | "ucwords" + | "uasort" + | "uksort" + | "unlink" + | "umask" + | "urldecode" + | "urlencode" + | "usort" + | "usleep" + | "var_dump" + | "printf" + | "vprintf" + | "vsprintf" + | "wordwrap" + | "lstat" + ) +} + +/// Evaluates a direct PHP-visible builtin call with named or spread arguments. +pub(super) fn eval_builtin_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + Ok(result) +} + +/// Binds evaluated builtin arguments to PHP parameter order when names are used. +fn bind_evaluated_builtin_args( + name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if evaluated_args.iter().all(|arg| arg.name.is_none()) { + return Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()); + } + + let params = eval_builtin_param_names(name).ok_or(EvalStatus::RuntimeFatal)?; + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_builtin_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + collect_bound_builtin_args(name, bound_args, values) +} + +/// Binds one named builtin-call value to the matching PHP parameter slot. +fn bind_builtin_named_arg( + params: &[&str], + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + let Some(param_index) = params.iter().position(|param| *param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + Ok(()) +} + +/// Collects ordered bound arguments, rejecting gaps where defaults would be needed. +fn collect_contiguous_bound_args( + bound_args: Vec>, +) -> Result, EvalStatus> { + let Some(last_index) = bound_args.iter().rposition(Option::is_some) else { + return Ok(Vec::new()); + }; + bound_args + .into_iter() + .take(last_index + 1) + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Collects ordered builtin arguments, applying PHP defaults for special named-call gaps. +fn collect_bound_builtin_args( + name: &str, + mut bound_args: Vec>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if name == "array_splice" && bound_args.get(3).is_some_and(Option::is_some) { + if bound_args.get(2).is_some_and(Option::is_none) { + bound_args[2] = Some(values.null()?); + } + } + collect_contiguous_bound_args(bound_args) +} + +/// Returns PHP parameter names for builtin calls implemented by eval. +fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { + match name { + "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), + "array_chunk" => Some(&["array", "length"]), + "array_column" => Some(&["array", "column_key"]), + "array_combine" => Some(&["keys", "values"]), + "array_fill" => Some(&["start_index", "count", "value"]), + "array_fill_keys" => Some(&["keys", "value"]), + "array_filter" => Some(&["array", "callback", "mode"]), + "array_map" => Some(&["callback", "array"]), + "array_reduce" => Some(&["array", "callback", "initial"]), + "array_walk" => Some(&["array", "callback"]), + "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), + "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" + | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" | "asort" + | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { + Some(&["array"]) + } + "array_push" | "array_unshift" => Some(&["array", "values"]), + "array_key_exists" => Some(&["key", "array"]), + "array_pad" => Some(&["array", "length", "value"]), + "array_reverse" => Some(&["array", "preserve_keys"]), + "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), + "array_slice" => Some(&["array", "offset", "length"]), + "array_splice" => Some(&["array", "offset", "length", "replacement"]), + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), + "atan2" => Some(&["y", "x"]), + "basename" => Some(&["path", "suffix"]), + "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" + | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => { + Some(&["string"]) + } + "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" + | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" + | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" | "is_real" + | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), + "settype" => Some(&["var", "type"]), + "get_class" => Some(&["object"]), + "get_parent_class" => Some(&["object_or_class"]), + "call_user_func" => Some(&["callback"]), + "call_user_func_array" => Some(&["callback", "args"]), + "class_exists" => Some(&["class", "autoload"]), + "enum_exists" => Some(&["enum", "autoload"]), + "interface_exists" => Some(&["interface", "autoload"]), + "trait_exists" => Some(&["trait", "autoload"]), + "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), + "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), + "chmod" => Some(&["filename", "permissions"]), + "chr" => Some(&["codepoint"]), + "clamp" => Some(&["value", "min", "max"]), + "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), + "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), + "count" => Some(&["value", "mode"]), + "copy" | "rename" => Some(&["from", "to"]), + "crc32" => Some(&["string"]), + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), + "date" => Some(&["format", "timestamp"]), + "define" => Some(&["constant_name", "value"]), + "defined" => Some(&["constant_name"]), + "dirname" => Some(&["path", "levels"]), + "disk_free_space" | "disk_total_space" => Some(&["directory"]), + "explode" => Some(&["separator", "string"]), + "fdiv" | "fmod" => Some(&["num1", "num2"]), + "fnmatch" => Some(&["pattern", "filename", "flags"]), + "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" + | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" + | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" + | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), + "file_put_contents" => Some(&["filename", "data"]), + "function_exists" => Some(&["function"]), + "gethostbyaddr" => Some(&["ip"]), + "gethostbyname" => Some(&["hostname"]), + "gethostname" => Some(&[]), + "getprotobyname" => Some(&["protocol"]), + "getprotobynumber" => Some(&["protocol"]), + "getservbyname" => Some(&["service", "protocol"]), + "getservbyport" => Some(&["port", "protocol"]), + "get_resource_id" | "get_resource_type" => Some(&["resource"]), + "getcwd" => Some(&[]), + "getenv" => Some(&["name"]), + "glob" => Some(&["pattern"]), + "hash" => Some(&["algo", "data", "binary"]), + "hash_algos" => Some(&[]), + "hash_equals" => Some(&["known_string", "user_string"]), + "hash_file" => Some(&["algo", "filename", "binary"]), + "hash_hmac" => Some(&["algo", "data", "key", "binary"]), + "hypot" => Some(&["x", "y"]), + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), + "implode" => Some(&["separator", "array"]), + "inet_ntop" => Some(&["ip"]), + "inet_pton" => Some(&["ip"]), + "intdiv" => Some(&["num1", "num2"]), + "iterator_apply" => Some(&["iterator", "callback", "args"]), + "iterator_count" => Some(&["iterator"]), + "iterator_to_array" => Some(&["iterator", "preserve_keys"]), + "ip2long" => Some(&["ip"]), + "json_decode" => Some(&["json", "associative", "depth", "flags"]), + "json_encode" => Some(&["value", "flags", "depth"]), + "json_last_error" | "json_last_error_msg" => Some(&[]), + "json_validate" => Some(&["json", "depth", "flags"]), + "link" | "symlink" => Some(&["target", "link"]), + "linkinfo" | "readlink" => Some(&["path"]), + "log" => Some(&["num", "base"]), + "max" | "min" => Some(&["value"]), + "md5" | "sha1" => Some(&["string", "binary"]), + "microtime" => Some(&["as_float"]), + "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), + "nl2br" => Some(&["string", "use_xhtml"]), + "number_format" => Some(&[ + "num", + "decimals", + "decimal_separator", + "thousands_separator", + ]), + "ord" => Some(&["character"]), + "pathinfo" => Some(&["path", "flags"]), + "pi" => Some(&[]), + "php_uname" => Some(&["mode"]), + "phpversion" => Some(&[]), + "pow" => Some(&["num", "exponent"]), + "preg_match" => Some(&["pattern", "subject", "matches", "flags", "offset"]), + "preg_match_all" => Some(&["pattern", "subject", "matches", "flags", "offset"]), + "preg_replace" => Some(&["pattern", "replacement", "subject", "limit", "count"]), + "preg_replace_callback" => Some(&["pattern", "callback", "subject", "limit", "count"]), + "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), + "print_r" | "var_dump" => Some(&["value"]), + "putenv" => Some(&["assignment"]), + "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), + "range" => Some(&["start", "end"]), + "realpath" => Some(&["path"]), + "realpath_cache_get" | "realpath_cache_size" => Some(&[]), + "round" => Some(&["num", "precision"]), + "sleep" => Some(&["seconds"]), + "spl_classes" => Some(&[]), + "spl_object_id" | "spl_object_hash" => Some(&["object"]), + "sscanf" => Some(&["string", "format", "vars"]), + "sprintf" | "printf" => Some(&["format", "values"]), + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), + "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), + "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), + "strtotime" => Some(&["datetime"]), + "strstr" => Some(&["haystack", "needle", "before_needle"]), + "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), + "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), + "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), + "str_repeat" => Some(&["string", "times"]), + "str_split" => Some(&["string", "length"]), + "substr" => Some(&["string", "offset", "length"]), + "substr_replace" => Some(&["string", "replace", "offset", "length"]), + "sys_get_temp_dir" | "time" => Some(&[]), + "tempnam" => Some(&["directory", "prefix"]), + "touch" => Some(&["filename", "mtime", "atime"]), + "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { + Some(&["string"]) + } + "long2ip" => Some(&["ip"]), + "ucwords" => Some(&["string", "separators"]), + "umask" => Some(&["mask"]), + "usleep" => Some(&["microseconds"]), + "vsprintf" | "vprintf" => Some(&["format", "values"]), + "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), + _ => None, + } +} + +/// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. +pub(super) fn eval_builtin_call_user_func( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_call_user_func_with_values(evaluated_args, context, values) +} + +/// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. +pub(super) fn eval_builtin_call_user_func_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [callback, arg_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let arg_array = eval_expr(arg_array, context, scope, values)?; + eval_call_user_func_array_with_values(callback, arg_array, context, values) +} + +/// Dispatches `call_user_func_array` after callback and array arguments are evaluated. +fn eval_call_user_func_array_with_values( + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable(callback, values)?; + if !values.is_array_like(arg_array)? { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = eval_array_call_arg_values(arg_array, values)?; + eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) +} + +/// Dispatches `call_user_func` after its callback and arguments are already evaluated. +fn eval_call_user_func_with_values( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, callback_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_callable(*callback, values)?; + eval_evaluated_callable_with_values(&callback, callback_args.to_vec(), context, values) +} + +/// Normalizes one PHP callback value for eval dynamic callable dispatch. +pub(super) fn eval_callable( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_array_like(callback)? { + return eval_array_callable(callback, values); + } + eval_callable_name(callback, values).map(EvaluatedCallable::Named) +} + +/// Normalizes one two-element object-method callable array. +fn eval_array_callable( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.array_len(callback)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let object = values.array_get(callback, zero)?; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::UnsupportedConstruct); + } + let method = values.array_get(callback, one)?; + let method = + String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(EvaluatedCallable::ObjectMethod { object, method }) +} + +/// Normalizes one string callback name for eval dynamic callable dispatch. +fn eval_callable_name( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = values.string_bytes(callback)?; + let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; + let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); + if callback.contains("::") { + return Err(EvalStatus::UnsupportedConstruct); + } + Ok(callback) +} + +/// Invokes an already normalized callback with source-order positional values. +fn eval_evaluated_callable_with_values( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named(name) => { + eval_callable_with_values(name, evaluated_args, context, values) + } + EvaluatedCallable::ObjectMethod { object, method } => { + eval_method_call_result(*object, method, evaluated_args, context, values) + } + } +} + +/// Invokes an already normalized callback with optional named-argument metadata. +pub(super) fn eval_evaluated_callable_with_call_array_args( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named(name) => { + eval_callable_with_call_array_args(name, evaluated_args, context, values) + } + EvaluatedCallable::ObjectMethod { object, method } => { + if evaluated_args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); + eval_method_call_result(*object, method, evaluated_args, context, values) + } + } +} + +/// Invokes a PHP-visible callable name with source-order positional values. +fn eval_callable_with_values( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { + return Ok(result); + } + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function_with_values(function, evaluated_args, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Invokes a callable with arguments that may carry `call_user_func_array` names. +pub(super) fn eval_callable_with_call_array_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.iter().all(|arg| arg.name.is_none()) { + let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); + return eval_callable_with_values(name, evaluated_args, context, values); + } + if eval_php_visible_builtin_exists(name) { + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + return Ok(result); + } + if let Some(function) = context.function(name).cloned() { + let evaluated_args = bind_evaluated_function_args(function.params(), evaluated_args)?; + return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + } + if let Some(function) = context.native_function(name) { + if function.param_names().len() != function.param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = bind_evaluated_function_args(function.param_names(), evaluated_args)?; + return eval_native_function_with_values(function, evaluated_args, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. +fn eval_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "abs" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.abs(*value)? + } + "addslashes" | "stripslashes" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_slashes_result(name, *value, values)? + } + "array_combine" => { + let [keys, values_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_combine_result(*keys, *values_array, values)? + } + "array_column" => { + let [array, column_key] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_column_result(*array, *column_key, values)? + } + "array_chunk" => { + let [array, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_chunk_result(*array, *length, values)? + } + "array_fill" => { + let [start, count, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_result(*start, *count, *value, values)? + } + "array_fill_keys" => { + let [keys, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_keys_result(*keys, *value, values)? + } + "array_filter" => match evaluated_args { + [array] => eval_array_filter_result(*array, None, None, context, values)?, + [array, callback] => { + eval_array_filter_result(*array, Some(*callback), None, context, values)? + } + [array, callback, mode] => { + eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_map" => { + let Some((callback, arrays)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_map_result(*callback, arrays, context, values)? + } + "array_reduce" => match evaluated_args { + [array, callback] => { + let initial = values.null()?; + eval_array_reduce_result(*array, *callback, initial, context, values)? + } + [array, callback, initial] => { + eval_array_reduce_result(*array, *callback, *initial, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_walk" => { + let [array, callback] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_walk_result(*array, *callback, context, values)? + } + "array_pop" | "array_shift" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_pop_shift_value_result(name, *array, values)? + } + "array_push" | "array_unshift" => { + let Some((array, inserted)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_push_unshift_count_result(*array, inserted.len(), values)? + } + "array_splice" => { + let result = match evaluated_args { + [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, + [array, offset, length] => { + eval_array_splice_value_result(*array, *offset, Some(*length), values)? + } + [array, offset, length, _replacement] => { + eval_array_splice_value_result(*array, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.warning( + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + )?; + result + } + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_sort_value_result(*array, values)? + } + "uasort" | "uksort" | "usort" => { + let [array, callback] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_user_sort_value_result(name, *array, *callback, context, values)? + } + "array_flip" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_flip_result(*array, values)? + } + "array_pad" => { + let [array, length, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_pad_result(*array, *length, *value, values)? + } + "array_product" | "array_sum" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_aggregate_result(name, *array, values)? + } + "array_keys" | "array_values" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_projection_result(name, *array, values)? + } + "array_key_exists" => { + let [key, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.array_key_exists(*key, *array)? + } + "array_diff" | "array_intersect" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_value_set_result(name, *left, *right, values)? + } + "array_diff_key" | "array_intersect_key" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_key_set_result(name, *left, *right, values)? + } + "array_merge" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_merge_result(*left, *right, values)? + } + "array_rand" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_rand_result(*array, values)? + } + "array_reverse" => match evaluated_args { + [array] => eval_array_reverse_result(*array, false, values)?, + [array, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_reverse_result(*array, preserve_keys, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_search" | "in_array" => { + let [needle, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_search_result(name, *needle, *array, values)? + } + "array_slice" => match evaluated_args { + [array, offset] => eval_array_slice_result(*array, *offset, None, values)?, + [array, offset, length] => { + eval_array_slice_result(*array, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_unique" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_unique_result(*array, values)? + } + "range" => { + let [start, end] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_range_result(*start, *end, values)? + } + "base64_encode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_encode_result(*value, values)? + } + "base64_decode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_decode_result(*value, values)? + } + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_unary_result(name, *value, values)? + } + "atan2" | "hypot" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_pair_result(name, *left, *right, values)? + } + "bin2hex" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_bin2hex_result(*value, values)? + } + "ceil" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.ceil(*value)? + } + "chr" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chr_result(*value, values)? + } + "chdir" | "mkdir" | "rmdir" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unary_path_bool_result(name, *path, values)? + } + "chmod" => { + let [filename, permissions] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chmod_result(*filename, *permissions, values)? + } + "clearstatcache" => { + if evaluated_args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + values.null()? + } + "clamp" => { + let [value, min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_clamp_result(*value, *min, *max, values)? + } + "copy" | "link" | "rename" | "symlink" => { + let [from, to] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_binary_path_bool_result(name, *from, *to, values)? + } + "floor" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.floor(*value)? + } + "fdiv" | "fmod" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_binary_result(name, *left, *right, values)? + } + "file" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_result(*filename, values)? + } + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_probe_result(name, *filename, values)? + } + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_stat_scalar_result(name, *filename, values)? + } + "file_get_contents" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_get_contents_result(*filename, values)? + } + "file_put_contents" => { + let [filename, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_put_contents_result(*filename, *data, values)? + } + "filesize" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_filesize_result(*filename, values)? + } + "filetype" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_filetype_result(*filename, values)? + } + "fnmatch" => match evaluated_args { + [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, + [pattern, filename, flags] => { + eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stat" | "lstat" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stat_array_result(name, *filename, values)? + } + "linkinfo" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_linkinfo_result(*path, values)? + } + "log" => match evaluated_args { + [num] => eval_log_result(*num, None, values)?, + [num, base] => eval_log_result(*num, Some(*base), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "readfile" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_readfile_result(*filename, values)? + } + "pi" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI)? + } + "php_uname" => match evaluated_args { + [] => eval_php_uname_result(None, values)?, + [mode] => eval_php_uname_result(Some(*mode), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "pow" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.pow(*left, *right)? + } + "preg_match" => match evaluated_args { + [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_match_all" => match evaluated_args { + [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_replace" => match evaluated_args { + [pattern, replacement, subject] => { + eval_preg_replace_result(*pattern, *replacement, *subject, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_replace_callback" => match evaluated_args { + [pattern, callback, subject] => { + eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_split" => match evaluated_args { + [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values)?, + [pattern, subject, limit] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), None, values)? + } + [pattern, subject, limit, flags] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "print_r" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_print_r_result(*value, values)? + } + "var_dump" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_var_dump_result(*value, values)? + } + "rand" | "mt_rand" => match evaluated_args { + [] => eval_rand_result(None, None, values)?, + [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "random_int" => { + let [min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_random_int_result(*min, *max, values)? + } + "rawurldecode" | "urldecode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_decode_result(name, *value, values)? + } + "rawurlencode" | "urlencode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_encode_result(name, *value, values)? + } + "round" => match evaluated_args { + [value] => values.round(*value, None)?, + [value, precision] => values.round(*value, Some(*precision))?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "scandir" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_scandir_result(*directory, values)? + } + "sqrt" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.sqrt(*value)? + } + "spl_classes" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values)? + } + "spl_object_id" | "spl_object_hash" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_spl_object_identity_result(name, *object, values)? + } + "sscanf" => { + let [input, format, ..] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sscanf_result(*input, *format, values)? + } + "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, + "settype" => { + let [value, type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_settype_value_result(*value, *type_name, values)? + } + "strrev" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.strrev(*value)? + } + "str_repeat" => { + let [value, times] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_repeat_result(*value, *times, values)? + } + "str_replace" | "str_ireplace" => { + let [search, replace, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_replace_result(name, *search, *replace, *subject, values)? + } + "str_pad" => match evaluated_args { + [value, length] => eval_str_pad_result(*value, *length, None, None, values)?, + [value, length, pad_string] => { + eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? + } + [value, length, pad_string, pad_type] => { + eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "str_split" => match evaluated_args { + [value] => eval_str_split_result(*value, None, values)?, + [value, length] => eval_str_split_result(*value, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "substr" => match evaluated_args { + [value, offset] => eval_substr_result(*value, *offset, None, values)?, + [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "substr_replace" => match evaluated_args { + [value, replace, offset] => { + eval_substr_replace_result(*value, *replace, *offset, None, values)? + } + [value, replace, offset, length] => { + eval_substr_replace_result(*value, *replace, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "call_user_func" => { + return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) + .map(Some); + } + "call_user_func_array" => { + let [callback, arg_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) + .map(Some); + } + "boolval" | "floatval" | "intval" | "strval" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_cast_result(name, *value, values)? + } + "count" => match evaluated_args { + [value] => eval_count_result(*value, None, values)?, + [value, mode] => eval_count_result(*value, Some(*mode), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "crc32" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_crc32_result(*value, values)? + } + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ctype_result(name, *value, values)? + } + "date" => match evaluated_args { + [format] => eval_date_result(*format, None, values)?, + [format, timestamp] => eval_date_result(*format, Some(*timestamp), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "define" => eval_define_result(evaluated_args, context, values)?, + "defined" => eval_defined_result(evaluated_args, context, values)?, + "explode" => { + let [separator, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_explode_result(*separator, *string, values)? + } + "ord" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ord_result(*value, values)? + } + "implode" => { + let [separator, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_implode_result(*separator, *array, values)? + } + "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, + "microtime" => match evaluated_args { + [] | [_] => eval_microtime_result(values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "mktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? + } + "nl2br" => match evaluated_args { + [value] => eval_nl2br_result(*value, true, values)?, + [value, use_xhtml] => { + let use_xhtml = values.truthy(*use_xhtml)?; + eval_nl2br_result(*value, use_xhtml, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "number_format" => match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values)?, + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values)? + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + )?, + [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "basename" => match evaluated_args { + [path] => eval_basename_result(*path, None, values)?, + [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "dirname" => match evaluated_args { + [path] => eval_dirname_result(*path, None, values)?, + [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "disk_free_space" | "disk_total_space" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_disk_space_result(name, *directory, values)? + } + "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { + [value] => eval_trim_like_result(name, *value, None, values)?, + [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "function_exists" | "is_callable" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = values.string_bytes(*name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\').to_ascii_lowercase(); + values.bool_value(eval_function_probe_exists(context, &name))? + } + "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "enum_exists" | "trait_exists" => { + eval_class_like_exists_result(name, evaluated_args, values)? + } + "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, + "is_a" | "is_subclass_of" => { + eval_is_a_relation_result(name, evaluated_args, context, values)? + } + "json_decode" => match evaluated_args { + [json] => eval_json_decode_result(*json, None, None, None, context, values)?, + [json, associative] => { + eval_json_decode_result(*json, Some(*associative), None, None, context, values)? + } + [json, associative, depth] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + None, + context, + values, + )?, + [json, associative, depth, flags] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + Some(*flags), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "json_encode" => match evaluated_args { + [value] => eval_json_encode_result(*value, None, None, context, values)?, + [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values)?, + [value, flags, depth] => { + eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "json_last_error" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.int(context.json_last_error())? + } + "json_last_error_msg" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string(context.json_last_error_msg())? + } + "json_validate" => match evaluated_args { + [json] => eval_json_validate_result(*json, None, None, context, values)?, + [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values)?, + [json, depth, flags] => { + eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "gethostbyaddr" => { + let [ip] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyaddr_result(*ip, values)? + } + "gethostbyname" => { + let [hostname] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyname_result(*hostname, values)? + } + "gethostname" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_gethostname_result(values)? + } + "getprotobyname" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobyname_result(*protocol, values)? + } + "getprotobynumber" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobynumber_result(*protocol, values)? + } + "getservbyname" => { + let [service, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyname_result(*service, *protocol, values)? + } + "getservbyport" => { + let [port, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyport_result(*port, *protocol, values)? + } + "getcwd" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values)? + } + "getenv" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getenv_result(*name, values)? + } + "get_class" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_get_class_result(*object, context, values)? + } + "get_parent_class" => { + let [object_or_class] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_get_parent_class_result(*object_or_class, values)? + } + "get_resource_id" | "get_resource_type" => { + let [resource] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_resource_introspection_result(name, *resource, values)? + } + "gettype" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gettype_result(*value, values)? + } + "glob" => { + let [pattern] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_glob_result(*pattern, values)? + } + "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { + eval_hash_one_shot_result(name, evaluated_args, values)? + } + "hash_algos" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values)? + } + "hash_equals" => { + let [known, user] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_equals_result(*known, *user, values)? + } + "hex2bin" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hex2bin_result(*value, values)? + } + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_html_entity_result(name, *value, values)? + } + "inet_ntop" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_ntop_result(*value, values)? + } + "inet_pton" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_pton_result(*value, values)? + } + "intdiv" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_intdiv_result(*left, *right, values)? + } + "iterator_apply" => match evaluated_args { + [iterator, callback] => { + let callback = eval_callable(*callback, values)?; + eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? + } + [iterator, callback, args] => { + let callback = eval_callable(*callback, values)?; + let callback_args = eval_iterator_apply_arg_values(*args, values)?; + eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "iterator_count" => { + let [iterator] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_iterator_count_result(*iterator, values)? + } + "iterator_to_array" => match evaluated_args { + [iterator] => eval_iterator_to_array_result(*iterator, true, values)?, + [iterator, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_iterator_to_array_result(*iterator, preserve_keys, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "ip2long" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ip2long_result(*value, values)? + } + "phpversion" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values)? + } + "pathinfo" => match evaluated_args { + [path] => eval_pathinfo_result(*path, None, values)?, + [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "putenv" => { + let [assignment] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_putenv_result(*assignment, values)? + } + "realpath" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_realpath_result(*path, values)? + } + "realpath_cache_get" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values)? + } + "realpath_cache_size" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values)? + } + "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" + | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" + | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_type_predicate_result(name, *value, values)? + } + "sys_get_temp_dir" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values)? + } + "tempnam" => { + let [directory, prefix] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_tempnam_result(*directory, *prefix, values)? + } + "sleep" => { + let [seconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sleep_result(*seconds, values)? + } + "time" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values)? + } + "touch" => match evaluated_args { + [filename] => eval_touch_result(*filename, None, None, values)?, + [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, + [filename, mtime, atime] => { + eval_touch_result(*filename, Some(*mtime), Some(*atime), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, values)? + } + "strtotime" => { + let [datetime] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_strtotime_result(*datetime, values)? + } + "umask" => match evaluated_args { + [] => eval_umask_result(None, values)?, + [mask] => eval_umask_result(Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "usleep" => { + let [microseconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_usleep_result(*microseconds, values)? + } + "readlink" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_readlink_result(*path, values)? + } + "unlink" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unlink_result(*filename, values)? + } + "strlen" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let bytes = values.string_bytes(*value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len)? + } + "strpos" | "strrpos" => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_position_result(name, *haystack, *needle, values)? + } + "str_contains" | "str_starts_with" | "str_ends_with" => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_search_result(name, *haystack, *needle, values)? + } + "strstr" => match evaluated_args { + [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values)?, + [haystack, needle, before_needle] => { + let before_needle = values.truthy(*before_needle)?; + eval_strstr_result(*haystack, *needle, before_needle, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "strcmp" | "strcasecmp" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_compare_result(name, *left, *right, values)? + } + "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_case_result(name, *value, values)? + } + "long2ip" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_long2ip_result(*value, values)? + } + "ucwords" => match evaluated_args { + [value] => eval_ucwords_result(*value, None, values)?, + [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, + "wordwrap" => match evaluated_args { + [value] => eval_wordwrap_result(*value, None, None, None, values)?, + [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, + [value, width, break_string] => { + eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values)? + } + [value, width, break_string, cut] => eval_wordwrap_result( + *value, + Some(*width), + Some(*break_string), + Some(*cut), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + _ => return Ok(None), + }; + Ok(Some(result)) +} + +/// Evaluates PHP's `abs(...)` over one eval expression. +pub(super) fn eval_builtin_abs( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.abs(value) +} + +/// Evaluates PHP array aggregate builtins over one eval array expression. +pub(super) fn eval_builtin_array_aggregate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_aggregate_result(name, array, values) +} + +/// Computes `array_sum()` or `array_product()` through eval's numeric value hooks. +fn eval_array_aggregate_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = match name { + "array_sum" => values.int(0)?, + "array_product" => values.int(1)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = match name { + "array_sum" => values.add(result, value)?, + "array_product" => values.mul(result, value)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + } + Ok(result) +} + +/// Evaluates PHP `array_combine()` over key and value array expressions. +pub(super) fn eval_builtin_array_combine( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, values_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let values_array = eval_expr(values_array, context, scope, values)?; + eval_array_combine_result(keys, values_array, values) +} + +/// Builds the associative result for `array_combine()` from two eval arrays. +fn eval_array_combine_result( + keys: RuntimeCellHandle, + values_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + if len != values.array_len(values_array)? { + return Err(EvalStatus::RuntimeFatal); + } + + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + let target_key = values.cast_string(target_key)?; + let value_key = values.array_iter_key(values_array, position)?; + let value = values.array_get(values_array, value_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_column()` over row-array and column-key expressions. +pub(super) fn eval_builtin_array_column( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, column_key] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let column_key = eval_expr(column_key, context, scope, values)?; + eval_array_column_result(array, column_key, values) +} + +/// Builds `array_column()` by extracting present row columns into a reindexed array. +fn eval_array_column_result( + array: RuntimeCellHandle, + column_key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + let mut output_index = 0_i64; + for position in 0..len { + let row_key = values.array_iter_key(array, position)?; + let row = values.array_get(array, row_key)?; + if !matches!(values.type_tag(row)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + continue; + } + let exists = values.array_key_exists(column_key, row)?; + if !values.truthy(exists)? { + continue; + } + let column = values.array_get(row, column_key)?; + let target_key = values.int(output_index)?; + output_index = output_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, column)?; + } + Ok(result) +} + +/// Evaluates PHP `array_fill()` over start, count, and value expressions. +pub(super) fn eval_builtin_array_fill( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, count, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let count = eval_expr(count, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_result(start, count, value, values) +} + +/// Builds an `array_fill()` result with PHP's explicit integer key range. +fn eval_array_fill_result( + start: RuntimeCellHandle, + count: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let count = eval_int_value(count, values)?; + if count < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = if start == 0 { + values.array_new(count)? + } else { + values.assoc_new(count)? + }; + for offset in 0..count { + let offset = i64::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = start.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_fill_keys()` over key-array and value expressions. +pub(super) fn eval_builtin_array_fill_keys( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_keys_result(keys, value, values) +} + +/// Builds an `array_fill_keys()` result preserving the source key iteration order. +fn eval_array_fill_keys_result( + keys: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_map()` for one source array and a string or null callback. +pub(super) fn eval_builtin_array_map( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, arrays)) = args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let mut evaluated_arrays = Vec::with_capacity(arrays.len()); + for array in arrays { + evaluated_arrays.push(eval_expr(array, context, scope, values)?); + } + eval_array_map_result(callback, &evaluated_arrays, context, values) +} + +/// Maps one eval array with PHP key preservation for the one-array form. +fn eval_array_map_result( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = arrays else { + return eval_array_map_variadic_result(callback, arrays, context, values); + }; + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_name(callback, values)?) + }; + let len = values.array_len(*array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(*array, position)?; + let value = values.array_get(*array, key)?; + let mapped = if let Some(callback) = callback.as_deref() { + eval_callable_with_values(callback, vec![value], context, values)? + } else { + value + }; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Maps multiple eval arrays with PHP's reindexed and null-padded variadic behavior. +fn eval_array_map_variadic_result( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if arrays.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_name(callback, values)?) + }; + let mut lengths = Vec::with_capacity(arrays.len()); + let mut max_len = 0; + for array in arrays { + let len = values.array_len(*array)?; + max_len = max_len.max(len); + lengths.push(len); + } + + let mut result = values.array_new(max_len)?; + for position in 0..max_len { + let mut callback_args = Vec::with_capacity(arrays.len()); + for (array, len) in arrays.iter().zip(lengths.iter()) { + let value = if position < *len { + let key = values.array_iter_key(*array, position)?; + values.array_get(*array, key)? + } else { + values.null()? + }; + callback_args.push(value); + } + let mapped = if let Some(callback) = callback.as_deref() { + eval_callable_with_values(callback, callback_args, context, values)? + } else { + eval_array_map_zipped_row(callback_args, values)? + }; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Builds one row for `array_map(null, $a, $b, ...)`. +fn eval_array_map_zipped_row( + values_row: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut row = values.array_new(values_row.len())?; + for (index, value) in values_row.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + row = values.array_set(row, key, value)?; + } + Ok(row) +} + +/// Evaluates PHP `array_reduce()` with an optional initial carry value. +pub(super) fn eval_builtin_array_reduce( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, callback, initial) = match args { + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + (array, callback, values.null()?) + } + [array, callback, initial] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let initial = eval_expr(initial, context, scope, values)?; + (array, callback, initial) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_array_reduce_result(array, callback, initial, context, values) +} + +/// Reduces one eval array by invoking a string callback with carry and item cells. +fn eval_array_reduce_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let len = values.array_len(array)?; + let mut carry = initial; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + carry = eval_callable_with_values(&callback, vec![carry, value], context, values)?; + } + Ok(carry) +} + +/// Evaluates PHP `array_walk()` for side-effect callbacks over value/key pairs. +pub(super) fn eval_builtin_array_walk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_walk_result(array, callback, context, values) +} + +/// Walks one eval array by invoking a string callback with value and key cells. +fn eval_array_walk_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let _ = eval_callable_with_values(&callback, vec![value, key], context, values)?; + } + values.bool_value(true) +} + +/// Evaluates direct by-reference `settype()` calls and writes the converted cell back. +fn eval_builtin_settype_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (var_name, type_name) = eval_settype_direct_args(args, context, scope, values)?; + let value = visible_scope_cell(context, scope, &var_name).map_or_else(|| values.null(), Ok)?; + let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { + return values.bool_value(false); + }; + for replaced in set_scope_cell( + context, + scope, + var_name, + converted, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + values.bool_value(true) +} + +/// Evaluates and binds direct `settype()` arguments while preserving source order. +fn eval_settype_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(String, RuntimeCellHandle), EvalStatus> { + let mut var_name = None; + let mut type_name = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "var", + 1 => "type", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "var" => { + if var_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + var_name = Some(name.clone()); + } + "type" => { + if type_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + type_name = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let var_name = var_name.ok_or(EvalStatus::RuntimeFatal)?; + let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; + Ok((var_name, type_name)) +} + +/// Applies the eval-supported `settype()` scalar target conversion. +fn eval_settype_cast_value( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let type_name = values.string_bytes(type_name)?; + let type_name = String::from_utf8_lossy(&type_name).to_ascii_lowercase(); + let converted = match type_name.as_str() { + "bool" | "boolean" => Some(values.cast_bool(value)?), + "float" | "double" => Some(values.cast_float(value)?), + "int" | "integer" => Some(values.cast_int(value)?), + "string" => Some(values.cast_string(value)?), + _ => None, + }; + Ok(converted) +} + +/// Evaluates by-value `settype()` callable dispatch without mutating the source argument. +fn eval_settype_value_result( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.warning("settype(): Argument #1 ($var) must be passed by reference, value given")?; + if let Some(converted) = eval_settype_cast_value(value, type_name, values)? { + values.release(converted)?; + return values.bool_value(true); + } + values.bool_value(false) +} + +/// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. +pub(super) fn eval_builtin_array_pop_shift_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "settype" { + return eval_builtin_settype_call(args, context, scope, values); + } + if matches!(name, "array_push" | "array_unshift") { + return eval_builtin_array_push_unshift_call(name, args, context, scope, values); + } + if name == "array_splice" { + return eval_builtin_array_splice_call(args, context, scope, values); + } + if matches!( + name, + "arsort" + | "asort" + | "krsort" + | "ksort" + | "natcasesort" + | "natsort" + | "rsort" + | "shuffle" + | "sort" + ) { + return eval_builtin_array_sort_call(name, args, context, scope, values); + } + if matches!(name, "uasort" | "uksort" | "usort") { + return eval_builtin_user_sort_call(name, args, context, scope, values); + } + + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(var_name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + let Some(entry) = + scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; + for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates direct by-reference `array_push()` / `array_unshift()` calls. +fn eval_builtin_array_push_unshift_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 || !eval_call_args_are_plain_positional(args) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(var_name) = args[0].value() else { + return Err(EvalStatus::RuntimeFatal); + }; + let mut inserted = Vec::with_capacity(args.len() - 1); + for arg in &args[1..] { + inserted.push(eval_expr(arg.value(), context, scope, values)?); + } + let Some(entry) = + scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; + let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; + for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates direct by-reference `array_splice()` calls and writes back the array. +fn eval_builtin_array_splice_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array_name, offset, length, replacement_arg) = + eval_array_splice_direct_args(args, context, scope, values)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let (removed, replacement) = + eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(removed) +} + +/// Evaluates direct by-reference array ordering calls and writes back the array. +fn eval_builtin_array_sort_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array_name = eval_array_sort_direct_arg(args)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_array_sort_replacement(name, array, values)?; + let result = values.bool_value(true)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates direct by-reference user-comparator sort calls and writes back the array. +fn eval_builtin_user_sort_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array_name, callback) = eval_user_sort_direct_args(args, context, scope, values)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + let result = values.bool_value(true)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates and binds direct user-sort arguments while preserving source order. +fn eval_user_sort_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(String, RuntimeCellHandle), EvalStatus> { + let mut array = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + array = Some(name.clone()); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, callback)) +} + +/// Returns the dynamic callable result for by-value user-comparator sort calls. +fn eval_user_sort_value_result( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + values.release(replacement)?; + values.bool_value(true) +} + +/// One source array entry used by eval user-comparator sort routines. +struct EvalUserSortEntry { + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for user-comparator sort builtins. +fn eval_user_sort_replacement( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let mut entries = eval_user_sort_entries(array, values)?; + eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; + if name == "usort" { + return eval_user_sort_reindex_result(entries, values); + } + eval_user_sort_preserve_key_result(entries, values) +} + +/// Collects source keys and values from one eval array for user sorting. +fn eval_user_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + entries.push(EvalUserSortEntry { source_key, value }); + } + Ok(entries) +} + +/// Sorts entries by repeatedly invoking the PHP comparator callback. +fn eval_user_sort_entries_in_place( + name: &str, + callback: &str, + entries: &mut [EvalUserSortEntry], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for pass in 0..entries.len() { + let upper = entries.len().saturating_sub(pass + 1); + for index in 0..upper { + let comparison = eval_user_sort_compare( + name, + callback, + &entries[index], + &entries[index + 1], + context, + values, + )?; + if comparison > 0 { + entries.swap(index, index + 1); + } + } + } + Ok(()) +} + +/// Invokes one user-sort comparator and returns its integer ordering result. +fn eval_user_sort_compare( + name: &str, + callback: &str, + left: &EvalUserSortEntry, + right: &EvalUserSortEntry, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = if name == "uksort" { + vec![left.source_key, right.source_key] + } else { + vec![left.value, right.value] + }; + let result = eval_callable_with_values(callback, args, context, values)?; + eval_int_value(result, values) +} + +/// Builds the reindexed result for `usort()`. +fn eval_user_sort_reindex_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds the key-preserving result for `uksort()` and `uasort()`. +fn eval_user_sort_preserve_key_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + +/// Extracts the direct variable argument accepted by eval array ordering builtins. +fn eval_array_sort_direct_arg(args: &[EvalCallArg]) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + Ok(name.clone()) +} + +/// Returns the dynamic callable result for by-value array ordering calls. +fn eval_array_sort_value_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true) +} + +/// Sort key shape supported by eval's homogeneous array ordering implementation. +#[derive(Clone)] +enum EvalArraySortKey { + Numeric(f64), + Natural(Vec), + String(Vec), +} + +/// One source array entry plus its precomputed ordering key. +struct EvalArraySortEntry { + sort_key: EvalArraySortKey, + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for eval array ordering builtins. +fn eval_array_sort_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut entries = match name { + "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, + "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, + "natsort" => eval_array_natural_sort_entries(array, false, values)?, + "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, + "shuffle" => return eval_array_shuffle_replacement(array, values), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + entries.sort_by(|left, right| { + let order = eval_array_sort_key_cmp(&left.sort_key, &right.sort_key); + if matches!(name, "arsort" | "krsort" | "rsort") { + order.reverse() + } else { + order + } + }); + + if matches!( + name, + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" + ) { + return eval_array_preserve_key_sort_result(entries, values); + } + eval_array_reindex_sort_result(entries, values) +} + +/// Builds a shuffled, reindexed replacement array for `shuffle()`. +fn eval_array_shuffle_replacement( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + entries.push(values.array_get(array, source_key)?); + } + + for index in (1..entries.len()).rev() { + let swap_with = (eval_random_u128() % ((index + 1) as u128)) as usize; + entries.swap(index, swap_with); + } + + let mut result = values.array_new(entries.len())?; + for (index, value) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds an indexed result for `sort()` / `rsort()` after value ordering. +fn eval_array_reindex_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds a key-preserving associative result after value or key ordering. +fn eval_array_preserve_key_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + +/// Collects values and comparable value-sort keys from one eval array. +fn eval_array_value_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(value, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and natural-sort keys from one eval array. +fn eval_array_natural_sort_entries( + array: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_natural_sort_key(value, case_insensitive, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and comparable key-sort keys from one eval array. +fn eval_array_key_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(source_key, values)?; + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Converts one scalar eval value into a natural-sort key. +fn eval_array_natural_sort_key( + value: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } + EVAL_TAG_STRING => { + let mut bytes = values.string_bytes(value)?; + if case_insensitive { + bytes.make_ascii_lowercase(); + } + Ok(EvalArraySortKey::Natural(bytes)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts one scalar eval value into a homogeneous sort key. +fn eval_array_sort_key( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } + EVAL_TAG_STRING => { + let bytes = values.string_bytes(value)?; + match eval_array_numeric_string_sort_key(&bytes) { + Some(value) => Ok(EvalArraySortKey::Numeric(value)), + None => Ok(EvalArraySortKey::String(bytes)), + } + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Parses one PHP numeric string into the numeric sort domain when possible. +fn eval_array_numeric_string_sort_key(bytes: &[u8]) -> Option { + if !eval_is_numeric_string(bytes) { + return None; + } + std::str::from_utf8(bytes).ok()?.parse::().ok() +} + +/// Compares two precomputed eval sort keys. +fn eval_array_sort_key_cmp( + left: &EvalArraySortKey, + right: &EvalArraySortKey, +) -> std::cmp::Ordering { + match (left, right) { + (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { + left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) + } + (EvalArraySortKey::Natural(left), EvalArraySortKey::Natural(right)) => { + eval_natural_bytes_cmp(left, right) + } + (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), + _ => eval_array_sort_key_rank(left).cmp(&eval_array_sort_key_rank(right)), + } +} + +/// Returns a deterministic rank for mixed key-sort domains. +fn eval_array_sort_key_rank(key: &EvalArraySortKey) -> u8 { + match key { + EvalArraySortKey::Numeric(_) => 0, + EvalArraySortKey::Natural(_) => 1, + EvalArraySortKey::String(_) => 2, + } +} + +/// Compares byte strings with a small PHP-style natural ordering. +fn eval_natural_bytes_cmp(left: &[u8], right: &[u8]) -> std::cmp::Ordering { + let mut left_index = 0; + let mut right_index = 0; + while left_index < left.len() && right_index < right.len() { + if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { + let order = eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); + if order != std::cmp::Ordering::Equal { + return order; + } + continue; + } + + let order = left[left_index].cmp(&right[right_index]); + if order != std::cmp::Ordering::Equal { + return order; + } + left_index += 1; + right_index += 1; + } + left.len().cmp(&right.len()) +} + +/// Compares two natural-sort digit runs and advances both byte indexes past them. +fn eval_natural_digit_run_cmp( + left: &[u8], + left_index: &mut usize, + right: &[u8], + right_index: &mut usize, +) -> std::cmp::Ordering { + let left_start = *left_index; + let right_start = *right_index; + while *left_index < left.len() && left[*left_index].is_ascii_digit() { + *left_index += 1; + } + while *right_index < right.len() && right[*right_index].is_ascii_digit() { + *right_index += 1; + } + + let left_digits = &left[left_start..*left_index]; + let right_digits = &right[right_start..*right_index]; + let left_trimmed = eval_trim_leading_zeroes(left_digits); + let right_trimmed = eval_trim_leading_zeroes(right_digits); + left_trimmed + .len() + .cmp(&right_trimmed.len()) + .then_with(|| left_trimmed.cmp(right_trimmed)) + .then_with(|| left_digits.len().cmp(&right_digits.len())) +} + +/// Drops leading zero bytes while keeping one zero for an all-zero digit run. +fn eval_trim_leading_zeroes(digits: &[u8]) -> &[u8] { + let trimmed = digits + .iter() + .position(|digit| *digit != b'0') + .map_or(&digits[digits.len().saturating_sub(1)..], |index| { + &digits[index..] + }); + if trimmed.is_empty() { + digits + } else { + trimmed + } +} + +/// Evaluates and binds direct `array_splice()` arguments while preserving source order. +fn eval_array_splice_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut array = None; + let mut offset = None; + let mut length = None; + let mut replacement = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "offset", + 2 => "length", + 3 => "replacement", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + array = Some(name.clone()); + } + "offset" => { + if offset.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + offset = Some(eval_expr(arg.value(), context, scope, values)?); + } + "length" => { + if length.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + length = Some(eval_expr(arg.value(), context, scope, values)?); + } + "replacement" => { + if replacement.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + replacement = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, offset, length, replacement)) +} + +/// Returns the removed elements that `array_splice()` would produce without mutating the source. +fn eval_array_splice_value_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + eval_array_splice_removed(array, start, end, values) +} + +/// Builds both removed and replacement arrays for direct `array_splice()` write-back. +fn eval_array_splice_removed_and_replacement( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + let removed = eval_array_splice_removed(array, start, end, values)?; + let inserted = eval_array_splice_insert_values(replacement, values)?; + let replacement = eval_array_splice_replacement(array, start, end, &inserted, values)?; + Ok((removed, replacement)) +} + +/// Converts splice offset and length cells into bounded source positions. +fn eval_array_splice_bounds( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(usize, usize), EvalStatus> { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + Ok((start, end)) +} + +/// Builds the reindexed/string-key-preserving removed array returned by `array_splice()`. +fn eval_array_splice_removed( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = end.saturating_sub(start); + if eval_array_range_keys_are_int(array, start, end, values)? { + let mut result = values.array_new(len)?; + let mut target = 0_i64; + for position in start..end { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(len)?; + let mut next_int_key = 0_i64; + for position in start..end { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Expands the optional `array_splice()` replacement value into inserted values. +fn eval_array_splice_insert_values( + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(replacement) = replacement else { + return Ok(Vec::new()); + }; + if !matches!( + values.type_tag(replacement)?, + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC + ) { + return Ok(vec![replacement]); + } + + let len = values.array_len(replacement)?; + let mut inserted = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(replacement, position)?; + inserted.push(values.array_get(replacement, key)?); + } + Ok(inserted) +} + +/// Builds the source replacement after removing the requested splice range. +fn eval_array_splice_replacement( + array: RuntimeCellHandle, + start: usize, + end: usize, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let new_len = len + .saturating_sub(end.saturating_sub(start)) + .checked_add(inserted.len()) + .ok_or(EvalStatus::RuntimeFatal)?; + if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { + let mut result = values.array_new(new_len)?; + let mut target = 0_i64; + for position in 0..start { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(new_len)?; + let mut next_int_key = 0_i64; + for position in 0..start { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in one source position range is integer-shaped. +fn eval_array_range_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in start..end { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Returns true when every key outside the removed splice range is integer-shaped. +fn eval_array_splice_remaining_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if (start..end).contains(&position) { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. +fn eval_array_pop_shift_value_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + if len == 0 { + return values.null(); + } + let position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let key = values.array_iter_key(array, position)?; + values.array_get(array, key) +} + +/// Builds the return value plus replacement array for direct pop/shift write-back. +fn eval_array_pop_shift_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let len = values.array_len(array)?; + let tag = values.type_tag(array)?; + if len == 0 { + let replacement = match tag { + EVAL_TAG_ARRAY => values.array_new(0)?, + EVAL_TAG_ASSOC => values.assoc_new(0)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + return Ok((values.null()?, replacement)); + } + + let removed_position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let removed_key = values.array_iter_key(array, removed_position)?; + let removed_value = values.array_get(array, removed_key)?; + let replacement = match tag { + EVAL_TAG_ARRAY => { + eval_array_pop_shift_indexed_replacement(array, removed_position, len, values)? + } + EVAL_TAG_ASSOC => { + eval_array_pop_shift_assoc_replacement(name, array, removed_position, len, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + Ok((removed_value, replacement)) +} + +/// Rebuilds an indexed array after removing one position and reindexing values. +fn eval_array_pop_shift_indexed_replacement( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(len.saturating_sub(1))?; + let mut target = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after pop/shift, preserving PHP key behavior. +fn eval_array_pop_shift_assoc_replacement( + name: &str, + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "array_shift" + && eval_array_remaining_keys_are_int(array, removed_position, len, values)? + { + return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); + } + + let mut result = values.assoc_new(len.saturating_sub(1))?; + let mut next_int_key = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every remaining key is an integer after removing one element. +fn eval_array_remaining_keys_are_int( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if position == removed_position { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Returns the resulting element count for by-value push/unshift dynamic calls. +fn eval_array_push_unshift_count_result( + array: RuntimeCellHandle, + inserted_len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let total = values + .array_len(array)? + .checked_add(inserted_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let total = i64::try_from(total).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(total) +} + +/// Builds the replacement array for direct push/unshift write-back. +fn eval_array_push_unshift_replacement( + name: &str, + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, values.type_tag(array)?) { + ("array_push", EVAL_TAG_ARRAY) => { + eval_array_push_indexed_replacement(array, inserted, values) + } + ("array_push", EVAL_TAG_ASSOC) => { + eval_array_push_assoc_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ARRAY) => { + eval_array_unshift_indexed_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ASSOC) => { + eval_array_unshift_assoc_replacement(array, inserted, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Rebuilds an indexed array after appending values. +fn eval_array_push_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = + values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, target_key, value)?; + } + for (offset, value) in inserted.iter().copied().enumerate() { + let position = len.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after appending values at PHP's next integer keys. +fn eval_array_push_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_key = 0_i64; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? == EVAL_TAG_INT { + next_key = next_key.max(eval_int_value(key, values)?.saturating_add(1)); + } + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + for value in inserted.iter().copied() { + let key = values.int(next_key)?; + next_key = next_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an indexed array after prepending values and reindexing the original tail. +fn eval_array_unshift_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + let mut target = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after prepending values and reindexing integer keys. +fn eval_array_unshift_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if eval_array_keys_are_int(array, len, values)? { + return eval_array_unshift_indexed_replacement(array, inserted, values); + } + + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_int_key = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in the array is integer-shaped. +fn eval_array_keys_are_int( + array: RuntimeCellHandle, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Evaluates PHP `array_filter()` for null and string-callback filtering modes. +pub(super) fn eval_builtin_array_filter( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_filter_result(array, None, None, context, values) + } + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_filter_result(array, Some(callback), None, context, values) + } + [array, callback, mode] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_array_filter_result(array, Some(callback), Some(mode), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Filters eval array entries through PHP truthiness or a string callback. +fn eval_array_filter_result( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = match callback { + Some(callback) if !values.is_null(callback)? => Some(eval_callable_name(callback, values)?), + _ => None, + }; + let mode = match mode { + Some(mode) => eval_array_filter_mode_value(mode, values)?, + None => EVAL_ARRAY_FILTER_USE_VALUE, + }; + + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let keep = if let Some(callback) = callback.as_deref() { + let args = eval_array_filter_callback_args(mode, key, value)?; + let result = eval_callable_with_values(callback, args, context, values)?; + values.truthy(result)? + } else { + values.truthy(value)? + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Reads and validates the optional `array_filter()` callback mode. +fn eval_array_filter_mode_value( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = eval_int_value(mode, values)?; + match mode { + EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { + Ok(mode) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the callback argument list for one `array_filter()` entry. +fn eval_array_filter_callback_args( + mode: i64, + key: RuntimeCellHandle, + value: RuntimeCellHandle, +) -> Result, EvalStatus> { + match mode { + EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), + EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), + EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. +pub(super) fn eval_builtin_array_chunk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_chunk_result(array, length, values) +} + +/// Builds an `array_chunk()` result as nested reindexed arrays. +fn eval_array_chunk_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let chunk_size = eval_int_value(length, values)?; + if chunk_size <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; + let len = values.array_len(array)?; + let chunk_count = len.div_ceil(chunk_size); + let mut result = values.array_new(chunk_count)?; + + for chunk_index in 0..chunk_count { + let start = chunk_index * chunk_size; + let end = usize::min(start + chunk_size, len); + let mut chunk = values.array_new(end - start)?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let value = values.array_get(array, source_key)?; + let target_index = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_index = values.int(target_index)?; + chunk = values.array_set(chunk, target_index, value)?; + } + let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let result_key = values.int(result_key)?; + result = values.array_set(result, result_key, chunk)?; + } + + Ok(result) +} + +/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. +pub(super) fn eval_builtin_array_slice( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array, offset] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_array_slice_result(array, offset, None, values) + } + [array, offset, length] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_slice_result(array, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_slice()` result with PHP offset and length bounds. +fn eval_array_slice_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + + let mut result = values.array_new(end.saturating_sub(start))?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + +/// Converts a PHP array-slice offset into a bounded source position. +fn eval_slice_start(len: usize, offset: i64) -> Result { + if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(offset, len)); + } + + let tail = offset + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(len.saturating_sub(tail)) +} + +/// Converts a PHP array-slice length into a bounded exclusive end position. +fn eval_slice_end(len: usize, start: usize, length: i64) -> Result { + if length >= 0 { + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(start.saturating_add(length), len)); + } + + let tail = length + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(usize::max(start, len.saturating_sub(tail))) +} + +/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. +pub(super) fn eval_builtin_array_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_pad_result(array, length, value, values) +} + +/// Builds an `array_pad()` result by copying values and padding left or right. +fn eval_array_pad_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let target = eval_int_value(length, values)?; + let target_len = target + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + let result_len = usize::max(len, target_len); + let pad_count = result_len.saturating_sub(len); + let mut result = values.array_new(result_len)?; + let mut output_index = 0usize; + + if target < 0 { + let (padded, next_index) = + eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; + result = padded; + output_index = next_index; + } + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + output_index += 1; + } + + if target > 0 { + result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; + } + + Ok(result) +} + +/// Appends the same pad value at consecutive indexed positions in an array result. +fn eval_array_pad_append_repeated( + mut array: RuntimeCellHandle, + start_index: usize, + count: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, usize), EvalStatus> { + let mut next_index = start_index; + for _ in 0..count { + let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + array = values.array_set(array, key, value)?; + next_index += 1; + } + Ok((array, next_index)) +} + +/// Evaluates PHP `array_flip()` over one eval array expression. +pub(super) fn eval_builtin_array_flip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_flip_result(array, values) +} + +/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. +fn eval_array_flip_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { + continue; + } + result = values.array_set(result, value, key)?; + } + Ok(result) +} + +/// Evaluates PHP `array_unique()` over one eval array expression. +pub(super) fn eval_builtin_array_unique( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_unique_result(array, values) +} + +/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. +fn eval_array_unique_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut seen = Vec::>::with_capacity(len); + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let unique_key = values.string_bytes(value)?; + if seen.iter().any(|seen_key| seen_key == &unique_key) { + continue; + } + seen.push(unique_key); + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP array projection builtins over one eval array expression. +pub(super) fn eval_builtin_array_projection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_projection_result(name, array, values) +} + +/// Builds the indexed result array for `array_keys()` or `array_values()`. +fn eval_array_projection_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = match name { + "array_keys" => key, + "array_values" => values.array_get(array, key)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let index = values.int(position as i64)?; + result = values.array_set(result, index, value)?; + } + Ok(result) +} + +/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. +pub(super) fn eval_builtin_iterator_apply( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator, callback] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable(callback, values)?; + eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, callback_args] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable(callback, values)?; + let callback_args = eval_expr(callback_args, context, scope, values)?; + let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; + eval_iterator_apply_result(iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts the optional `iterator_apply()` callback-args value into call arguments. +fn eval_iterator_apply_arg_values( + args: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(args)? { + return Ok(Vec::new()); + } + if !values.is_array_like(args)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_array_call_arg_values(args, values) +} + +/// Applies a callback to each valid position of an eval-supported Traversable object. +fn eval_iterator_apply_result( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(iterator)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let count = match eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + ) { + Ok(count) => count, + Err(EvalStatus::UnsupportedConstruct) => { + let iterator = values.method_call(iterator, "getiterator", Vec::new())?; + eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + )? + } + Err(err) => return Err(err), + }; + values.int(count) +} + +/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. +fn eval_iterator_apply_iterator_object( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.method_call(iterator, "rewind", Vec::new())?; + let mut count = 0_i64; + loop { + let valid = values.method_call(iterator, "valid", Vec::new())?; + if !values.truthy(valid)? { + return Ok(count); + } + count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let result = eval_evaluated_callable_with_call_array_args( + callback, + callback_args.to_vec(), + context, + values, + )?; + if !values.truthy(result)? { + return Ok(count); + } + let _ = values.method_call(iterator, "next", Vec::new())?; + } +} + +/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. +pub(super) fn eval_builtin_iterator_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [iterator] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_count_result(iterator, values) +} + +/// Returns the element count for eval-supported array iterator inputs. +fn eval_iterator_count_result( + iterator: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(iterator)?; + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. +pub(super) fn eval_builtin_iterator_to_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator] => { + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_to_array_result(iterator, true, values) + } + [iterator, preserve_keys] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_iterator_to_array_result(iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies eval-supported array iterator inputs into a PHP array result. +fn eval_iterator_to_array_result( + iterator: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + if preserve_keys { + return eval_array_copy_preserve_keys(iterator, values); + } + eval_array_projection_result("array_values", iterator, values) +} + +/// Copies one array-like eval value while preserving iteration keys and order. +fn eval_array_copy_preserve_keys( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_reverse()` over an eval array expression. +pub(super) fn eval_builtin_array_reverse( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_reverse_result(array, false, values) + } + [array, preserve_keys] => { + let array = eval_expr(array, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_array_reverse_result(array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_reverse()` result while preserving PHP key rules. +fn eval_array_reverse_result( + array: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut keys = Vec::with_capacity(len); + let mut has_string_key = false; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; + keys.push(key); + } + + let mut result = if preserve_keys || has_string_key { + values.assoc_new(len)? + } else { + values.array_new(len)? + }; + let mut next_numeric_key = 0_i64; + + for key in keys.into_iter().rev() { + let value = values.array_get(array, key)?; + let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { + key + } else { + let key = values.int(next_numeric_key)?; + next_numeric_key += 1; + key + }; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_key_exists()` over a key and array expression. +pub(super) fn eval_builtin_array_key_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [key, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let key = eval_expr(key, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + values.array_key_exists(key, array) +} + +/// Evaluates PHP array search builtins over a needle and haystack expression. +pub(super) fn eval_builtin_array_search( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let needle = eval_expr(needle, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_array_search_result(name, needle, array, values) +} + +/// Searches an eval array with PHP's default loose comparison semantics. +fn eval_array_search_result( + name: &str, + needle: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let equal = values.compare(EvalBinOp::LooseEq, needle, value)?; + if values.truthy(equal)? { + return match name { + "in_array" => values.bool_value(true), + "array_search" => Ok(key), + _ => Err(EvalStatus::UnsupportedConstruct), + }; + } + } + match name { + "in_array" => values.bool_value(false), + "array_search" => values.bool_value(false), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP value-set array builtins over two eval array expressions. +pub(super) fn eval_builtin_array_value_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_value_set_result(name, left, right, values) +} + +/// Builds `array_diff()` or `array_intersect()` using PHP's default string comparison mode. +fn eval_array_value_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let mut right_values = Vec::with_capacity(right_len); + for position in 0..right_len { + let key = values.array_iter_key(right, position)?; + let value = values.array_get(right, key)?; + right_values.push(values.string_bytes(value)?); + } + + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let comparable = values.string_bytes(value)?; + let found = right_values + .iter() + .any(|right_value| right_value == &comparable); + let keep = match name { + "array_diff" => !found, + "array_intersect" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Evaluates PHP key-set array builtins over two eval array expressions. +pub(super) fn eval_builtin_array_key_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_key_set_result(name, left, right, values) +} + +/// Builds `array_diff_key()` or `array_intersect_key()` by testing first-array keys. +fn eval_array_key_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let exists = values.array_key_exists(key, right)?; + let found = values.truthy(exists)?; + let keep = match name { + "array_diff_key" => !found, + "array_intersect_key" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Evaluates PHP `array_rand()` over one eval array expression. +pub(super) fn eval_builtin_array_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_rand_result(array, values) +} + +/// Returns a valid random key from a non-empty eval array. +fn eval_array_rand_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if len == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let position = eval_random_position(len); + values.array_iter_key(array, position) +} + +/// Chooses a pseudo-random array position within `[0, len)`. +fn eval_random_position(len: usize) -> usize { + (eval_random_u128() % (len as u128)) as usize +} + +/// Produces a process-local pseudo-random word for non-cryptographic eval builtins. +fn eval_random_u128() -> u128 { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + let counter = u128::from(EVAL_RANDOM_COUNTER.fetch_add(1, Ordering::Relaxed)); + let pid = u128::from(std::process::id()); + let mut value = nanos ^ (counter.wrapping_mul(0x9e37_79b9_7f4a_7c15)) ^ (pid << 64); + value ^= value >> 30; + value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); + value ^= value >> 27; + value = value.wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} + +/// Evaluates PHP `rand()` and `mt_rand()` over zero args or an inclusive range. +pub(super) fn eval_builtin_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_rand_result(None, None, values), + [min, max] => { + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_rand_result(Some(min), Some(max), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `random_int()` over an inclusive integer range. +pub(super) fn eval_builtin_random_int( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_random_int_result(min, max, values) +} + +/// Returns one non-cryptographic random integer using PHP's inclusive range rules. +fn eval_rand_result( + min: Option, + max: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (min, max) = match (min, max) { + (None, None) => (0, i64::from(i32::MAX)), + (Some(min), Some(max)) => (eval_int_value(min, values)?, eval_int_value(max, values)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let low = min.min(max); + let high = min.max(max); + let width = (i128::from(high) - i128::from(low) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(low) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) +} + +/// Returns one eval `random_int()` value in the inclusive range `[min, max]`. +fn eval_random_int_result( + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let min = eval_int_value(min, values)?; + let max = eval_int_value(max, values)?; + if min > max { + return Err(EvalStatus::RuntimeFatal); + } + let width = (i128::from(max) - i128::from(min) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(min) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) +} + +/// Evaluates PHP `range()` over integer-compatible start and end expressions. +pub(super) fn eval_builtin_range( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, end] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let end = eval_expr(end, context, scope, values)?; + eval_range_result(start, end, values) +} + +/// Builds an inclusive ascending or descending integer `range()` result. +fn eval_range_result( + start: RuntimeCellHandle, + end: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let end = eval_int_value(end, values)?; + let distance = if start <= end { + end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? + } else { + start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? + }; + let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let step = if start <= end { 1_i64 } else { -1_i64 }; + let mut current = start; + let mut result = values.array_new(count)?; + + for index in 0..count { + let key = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + let value = values.int(current)?; + result = values.array_set(result, key, value)?; + if index + 1 < count { + current = current.checked_add(step).ok_or(EvalStatus::RuntimeFatal)?; + } + } + Ok(result) +} + +/// Evaluates PHP `array_merge()` over two array expressions. +pub(super) fn eval_builtin_array_merge( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_merge_result(left, right, values) +} + +/// Builds an `array_merge()` result with PHP numeric reindexing and string-key overwrites. +fn eval_array_merge_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let capacity = left_len + .checked_add(right_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut result = values.assoc_new(capacity)?; + let mut next_numeric_key = 0_i64; + result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; + eval_array_merge_append_operand(result, right, &mut next_numeric_key, values) +} + +/// Appends one source array to an `array_merge()` result using PHP key handling. +fn eval_array_merge_append_operand( + mut result: RuntimeCellHandle, + source: RuntimeCellHandle, + next_numeric_key: &mut i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(source)?; + for position in 0..len { + let source_key = values.array_iter_key(source, position)?; + let source_value = values.array_get(source, source_key)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_STRING { + source_key + } else { + let target_key = values.int(*next_numeric_key)?; + *next_numeric_key = (*next_numeric_key) + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + target_key + }; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + +/// Evaluates PHP `explode()` over separator and string expressions. +pub(super) fn eval_builtin_explode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, string] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let string = eval_expr(string, context, scope, values)?; + eval_explode_result(separator, string, values) +} + +/// Splits one PHP byte string into an indexed array using a non-empty separator. +fn eval_explode_result( + separator: RuntimeCellHandle, + string: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let separator = values.string_bytes(separator)?; + if separator.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let string = values.string_bytes(string)?; + let mut result = values.array_new(0)?; + let mut start = 0; + let mut index = 0_i64; + while let Some(found) = eval_find_subslice(&string, &separator, start) { + result = eval_push_explode_segment(result, index, &string[start..found], values)?; + start = found + separator.len(); + index += 1; + } + eval_push_explode_segment(result, index, &string[start..], values) +} + +/// Appends one split segment to an indexed `explode()` result array. +fn eval_push_explode_segment( + array: RuntimeCellHandle, + index: i64, + segment: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(index)?; + let value = values.string_bytes_value(segment)?; + values.array_set(array, key, value) +} + +/// Finds `needle` inside `haystack` starting from one byte offset. +fn eval_find_subslice(haystack: &[u8], needle: &[u8], start: usize) -> Option { + haystack + .get(start..)? + .windows(needle.len()) + .position(|window| window == needle) + .map(|position| position + start) +} + +/// Evaluates PHP `implode()` over separator and array expressions. +pub(super) fn eval_builtin_implode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_implode_result(separator, array, values) +} + +/// Joins array values in eval iteration order using PHP string conversion. +fn eval_implode_result( + separator: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let separator = values.string_bytes(separator)?; + let len = values.array_len(array)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.extend_from_slice(&separator); + } + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let value = values.string_bytes(value)?; + output.extend_from_slice(&value); + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP's `ceil(...)` over one eval expression. +pub(super) fn eval_builtin_ceil( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.ceil(value) +} + +/// Evaluates PHP's `floor(...)` over one eval expression. +pub(super) fn eval_builtin_floor( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.floor(value) +} + +/// Evaluates PHP's zero-argument `pi()` builtin. +pub(super) fn eval_builtin_pi( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI) +} + +/// Evaluates PHP's `pow(...)` over two eval expressions. +pub(super) fn eval_builtin_pow( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + values.pow(left, right) +} + +/// Evaluates PHP's `round(...)` over one value and an optional precision expression. +pub(super) fn eval_builtin_round( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + values.round(value, None) + } + [value, precision] => { + let value = eval_expr(value, context, scope, values)?; + let precision = eval_expr(precision, context, scope, values)?; + values.round(value, Some(precision)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `number_format(...)` over one number and optional separators. +pub(super) fn eval_builtin_number_format( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_number_format_result(value, None, None, None, values) + } + [value, decimals] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + eval_number_format_result(value, Some(decimals), None, None, values) + } + [value, decimals, decimal_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + eval_number_format_result(value, Some(decimals), Some(decimal_separator), None, values) + } + [value, decimals, decimal_separator, thousands_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + let thousands_separator = eval_expr(thousands_separator, context, scope, values)?; + eval_number_format_result( + value, + Some(decimals), + Some(decimal_separator), + Some(thousands_separator), + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one PHP numeric value with grouped thousands and fixed decimals. +fn eval_number_format_result( + value: RuntimeCellHandle, + decimals: Option, + decimal_separator: Option, + thousands_separator: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let decimals = match decimals { + Some(decimals) => eval_int_value(decimals, values)?, + None => 0, + }; + let decimal_separator = match decimal_separator { + Some(separator) => values.string_bytes(separator)?, + None => b".".to_vec(), + }; + let thousands_separator = match thousands_separator { + Some(separator) => values.string_bytes(separator)?, + None => b",".to_vec(), + }; + let output = + eval_number_format_bytes(value, decimals, &decimal_separator, &thousands_separator)?; + values.string_bytes_value(&output) +} + +/// Evaluates direct positional `sprintf()` or `printf()` calls in source order. +pub(super) fn eval_builtin_sprintf_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_sprintf_like_result(name, &evaluated_args, values) +} + +/// Evaluates direct positional `vsprintf()` or `vprintf()` calls in source order. +pub(super) fn eval_builtin_vsprintf_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_vsprintf_like_result(name, &evaluated_args, values) +} + +/// Evaluates direct positional `sscanf()` calls in source order. +pub(super) fn eval_builtin_sscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let input = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_sscanf_result(input, format, values) +} + +/// Dispatches already evaluated `sprintf()` or `printf()` arguments. +fn eval_sprintf_like_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "sprintf" => eval_sprintf_result(evaluated_args, values), + "printf" => eval_printf_result(evaluated_args, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Dispatches already evaluated `vsprintf()` or `vprintf()` arguments. +fn eval_vsprintf_like_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "vsprintf" => eval_vsprintf_result(evaluated_args, values), + "vprintf" => eval_vprintf_result(evaluated_args, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Parses one string through the eval `sscanf()` subset and returns an indexed array. +fn eval_sscanf_result( + input: RuntimeCellHandle, + format: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(input)?; + let format = values.string_bytes(format)?; + let matches = eval_sscanf_matches(&input, &format); + let mut result = values.array_new(matches.len())?; + for (index, matched) in matches.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(matched)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Extracts `%d`, `%f`, `%s`, and `%%` matches with the same subset as native `sscanf()`. +fn eval_sscanf_matches(input: &[u8], format: &[u8]) -> Vec> { + let mut matches = Vec::new(); + let mut input_index = 0; + let mut format_index = 0; + + while format_index < format.len() { + if format[format_index] != b'%' { + if input_index >= input.len() || input[input_index] != format[format_index] { + break; + } + input_index += 1; + format_index += 1; + continue; + } + + format_index += 1; + if format_index >= format.len() { + break; + } + + match format[format_index] { + b'%' => { + if input_index >= input.len() || input[input_index] != b'%' { + break; + } + input_index += 1; + } + b'd' => matches.push(eval_sscanf_scan_int(input, &mut input_index)), + b'f' => matches.push(eval_sscanf_scan_float(input, &mut input_index)), + b's' => matches.push(eval_sscanf_scan_word(input, &mut input_index)), + _ => {} + } + format_index += 1; + } + + matches +} + +/// Scans the native `sscanf()` `%d` subset as a matched byte slice. +fn eval_sscanf_scan_int(input: &[u8], input_index: &mut usize) -> Vec { + let start = *input_index; + if input.get(*input_index) == Some(&b'-') { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%f` subset as a matched byte slice. +fn eval_sscanf_scan_float(input: &[u8], input_index: &mut usize) -> Vec { + let start = *input_index; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + if input.get(*input_index) == Some(&b'.') { + *input_index += 1; + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'e' | b'E')) + { + *input_index += 1; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%s` subset as a non-space byte word. +fn eval_sscanf_scan_word(input: &[u8], input_index: &mut usize) -> Vec { + let start = *input_index; + while input + .get(*input_index) + .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\n')) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} + +/// Formats `sprintf()` arguments and returns the resulting PHP string. +fn eval_sprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats `printf()` arguments, echoes the result, and returns its byte count. +fn eval_printf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} + +/// Formats `vsprintf()` array arguments and returns the resulting PHP string. +fn eval_vsprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = eval_sprintf_bytes(&format, &format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. +fn eval_vprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = eval_sprintf_bytes(&format, &format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} + +/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. +fn eval_sprintf_argument_array_values( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + let mut args = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(array, position)?; + args.push(values.array_get(array, key)?); + } + Ok(args) +} + +/// Formats one printf-style byte string through eval runtime value coercions. +fn eval_sprintf_bytes( + format: &[u8], + args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut index = 0; + let mut arg_index = 0; + while index < format.len() { + if format[index] != b'%' { + output.push(format[index]); + index += 1; + continue; + } + index += 1; + if index >= format.len() { + break; + } + if format[index] == b'%' { + output.push(b'%'); + index += 1; + continue; + } + + let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; + index = next_index; + let Some(arg) = args.get(arg_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + arg_index += 1; + let bytes = eval_format_sprintf_arg(spec, arg, values)?; + output.extend_from_slice(&bytes); + } + Ok(output) +} + +/// Parses flags, width, precision, and terminal type for one format specifier. +fn eval_parse_sprintf_spec( + format: &[u8], + mut index: usize, +) -> Result<(EvalSprintfSpec, usize), EvalStatus> { + let mut spec = EvalSprintfSpec { + left_align: false, + force_sign: false, + space_sign: false, + zero_pad: false, + alternate: false, + width: None, + precision: None, + specifier: 0, + }; + while index < format.len() { + match format[index] { + b'-' => spec.left_align = true, + b'+' => spec.force_sign = true, + b' ' => spec.space_sign = true, + b'0' => spec.zero_pad = true, + b'#' => spec.alternate = true, + _ => break, + } + index += 1; + } + let (width, next_index) = eval_parse_sprintf_number(format, index)?; + spec.width = width; + index = next_index; + if index < format.len() && format[index] == b'.' { + let (precision, next_index) = eval_parse_sprintf_number(format, index + 1)?; + spec.precision = Some(precision.unwrap_or(0)); + index = next_index; + } + if index >= format.len() { + return Err(EvalStatus::RuntimeFatal); + } + spec.specifier = format[index]; + Ok((spec, index + 1)) +} + +/// Parses an unsigned decimal number from a format specifier component. +fn eval_parse_sprintf_number( + format: &[u8], + mut index: usize, +) -> Result<(Option, usize), EvalStatus> { + let start = index; + let mut value = 0usize; + while index < format.len() && format[index].is_ascii_digit() { + value = value + .checked_mul(10) + .and_then(|value| value.checked_add(usize::from(format[index] - b'0'))) + .ok_or(EvalStatus::RuntimeFatal)?; + index += 1; + } + if index == start { + Ok((None, index)) + } else { + Ok((Some(value), index)) + } +} + +/// Formats one runtime value according to a parsed eval sprintf specifier. +fn eval_format_sprintf_arg( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match spec.specifier { + b's' => eval_format_sprintf_string(spec, value, values), + b'f' | b'e' | b'g' => eval_format_sprintf_float(spec, value, values), + b'c' => eval_format_sprintf_char(spec, value, values), + _ => eval_format_sprintf_int(spec, value, values), + } +} + +/// Formats a `%s` operand after PHP string coercion. +fn eval_format_sprintf_string( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bytes = values.string_bytes(value)?; + if let Some(precision) = spec.precision { + bytes.truncate(precision); + } + Ok(eval_sprintf_apply_width(bytes, spec, false)) +} + +/// Formats an integer-like operand for decimal, unsigned, hex, and octal specifiers. +fn eval_format_sprintf_int( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + let mut output = Vec::new(); + match spec.specifier { + b'u' => { + let digits = eval_sprintf_precision_pad((value as u64).to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + b'x' | b'X' => { + let unsigned = value as u64; + if spec.alternate && unsigned != 0 { + output.extend_from_slice(if spec.specifier == b'X' { b"0X" } else { b"0x" }); + } + let digits = if spec.specifier == b'X' { + format!("{unsigned:X}").into_bytes() + } else { + format!("{unsigned:x}").into_bytes() + }; + output.extend_from_slice(&eval_sprintf_precision_pad(digits, spec)); + } + b'o' => { + let unsigned = value as u64; + let mut digits = eval_sprintf_precision_pad(format!("{unsigned:o}").into_bytes(), spec); + if spec.alternate && !digits.starts_with(b"0") { + output.push(b'0'); + } + output.append(&mut digits); + } + _ => { + let value = value as i128; + let magnitude = if value < 0 { + (-value) as u128 + } else { + value as u128 + }; + if value < 0 { + output.push(b'-'); + } else if spec.force_sign { + output.push(b'+'); + } else if spec.space_sign { + output.push(b' '); + } + let digits = eval_sprintf_precision_pad(magnitude.to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Formats a `%c` operand as the low byte of its integer value. +fn eval_format_sprintf_char( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + Ok(eval_sprintf_apply_width(vec![value as u8], spec, false)) +} + +/// Formats a floating-point operand for the eval printf-family subset. +fn eval_format_sprintf_float( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_float_value(value, values)?; + let precision = spec.precision.unwrap_or(6); + let mut output = if value.is_nan() { + b"NAN".to_vec() + } else if value.is_infinite() { + b"INF".to_vec() + } else { + match spec.specifier { + b'e' => format!("{value:.precision$e}").into_bytes(), + b'g' => format!("{value:.precision$}").into_bytes(), + _ => format!("{value:.precision$}").into_bytes(), + } + }; + if value.is_sign_negative() && !output.starts_with(b"-") { + output.insert(0, b'-'); + } else if value.is_sign_positive() && value.is_finite() { + if spec.force_sign { + output.insert(0, b'+'); + } else if spec.space_sign { + output.insert(0, b' '); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Applies integer precision by left-padding digits with zeros. +fn eval_sprintf_precision_pad(mut digits: Vec, spec: EvalSprintfSpec) -> Vec { + if matches!(spec.precision, Some(0)) && digits == b"0" { + digits.clear(); + return digits; + } + let Some(precision) = spec.precision else { + return digits; + }; + if digits.len() >= precision { + return digits; + } + let mut output = vec![b'0'; precision - digits.len()]; + output.append(&mut digits); + output +} + +/// Applies field width and alignment to one formatted eval sprintf replacement. +fn eval_sprintf_apply_width(mut bytes: Vec, spec: EvalSprintfSpec, numeric: bool) -> Vec { + let Some(width) = spec.width else { + return bytes; + }; + if bytes.len() >= width { + return bytes; + } + let pad_len = width - bytes.len(); + if spec.left_align { + bytes.extend(std::iter::repeat_n(b' ', pad_len)); + return bytes; + } + if numeric && spec.zero_pad && spec.precision.is_none() { + let prefix_len = eval_sprintf_zero_pad_prefix_len(&bytes); + let mut output = Vec::with_capacity(width); + output.extend_from_slice(&bytes[..prefix_len]); + output.extend(std::iter::repeat_n(b'0', pad_len)); + output.extend_from_slice(&bytes[prefix_len..]); + return output; + } + let mut output = Vec::with_capacity(width); + output.extend(std::iter::repeat_n(b' ', pad_len)); + output.append(&mut bytes); + output +} + +/// Returns the sign and alternate-prefix length that should precede zero padding. +fn eval_sprintf_zero_pad_prefix_len(bytes: &[u8]) -> usize { + let mut prefix_len = usize::from(matches!(bytes.first(), Some(b'+' | b'-' | b' '))); + if bytes.len() >= prefix_len + 2 + && bytes[prefix_len] == b'0' + && matches!(bytes[prefix_len + 1], b'x' | b'X') + { + prefix_len += 2; + } + prefix_len +} + +/// Converts one eval value to PHP float and returns the scalar payload. +pub(super) fn eval_float_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_float(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Produces PHP `number_format()` bytes for finite scalar values. +fn eval_number_format_bytes( + value: f64, + decimals: i64, + decimal_separator: &[u8], + thousands_separator: &[u8], +) -> Result, EvalStatus> { + if !value.is_finite() { + return Ok(value.to_string().into_bytes()); + } + let decimals = decimals.clamp(-308, 308); + let display_decimals = decimals.max(0) as usize; + let abs_value = value.abs(); + let scaled = if decimals >= 0 { + let scale = 10_f64.powi(decimals as i32); + (abs_value * scale).round() + } else { + let scale = 10_f64.powi((-decimals) as i32); + (abs_value / scale).round() * scale + }; + if scaled > (u128::MAX as f64) { + return Err(EvalStatus::RuntimeFatal); + } + let scaled = scaled as u128; + let scale = 10_u128 + .checked_pow(display_decimals as u32) + .ok_or(EvalStatus::RuntimeFatal)?; + let integer = if display_decimals == 0 { + scaled + } else { + scaled / scale + }; + let fraction = if display_decimals == 0 { + 0 + } else { + scaled % scale + }; + + let mut output = Vec::new(); + if value.is_sign_negative() && scaled != 0 { + output.push(b'-'); + } + eval_append_grouped_decimal(&mut output, integer, thousands_separator); + if display_decimals > 0 { + output.extend_from_slice(decimal_separator); + let fraction = format!("{fraction:0display_decimals$}"); + output.extend_from_slice(fraction.as_bytes()); + } + Ok(output) +} + +/// Appends one unsigned decimal integer with optional three-digit grouping. +fn eval_append_grouped_decimal(output: &mut Vec, value: u128, separator: &[u8]) { + let digits = value.to_string(); + if separator.is_empty() { + output.extend_from_slice(digits.as_bytes()); + return; + } + let first_group = match digits.len() % 3 { + 0 => 3, + len => len, + }; + output.extend_from_slice(&digits.as_bytes()[..first_group]); + let mut index = first_group; + while index < digits.len() { + output.extend_from_slice(separator); + output.extend_from_slice(&digits.as_bytes()[index..index + 3]); + index += 3; + } +} + +/// Evaluates PHP's `sqrt(...)` over one eval expression. +pub(super) fn eval_builtin_sqrt( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.sqrt(value) +} + +/// Evaluates PHP's `strrev(...)` over one eval expression. +pub(super) fn eval_builtin_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.strrev(value) +} + +/// Evaluates PHP's `chr(...)` over one eval expression. +pub(super) fn eval_builtin_chr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_chr_result(value, values) +} + +/// Converts one eval value to a PHP integer and returns the low byte as a string. +fn eval_chr_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + values.string_bytes_value(&[value as u8]) +} + +/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. +pub(super) fn eval_builtin_str_repeat( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, times] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let times = eval_expr(times, context, scope, values)?; + eval_str_repeat_result(value, times, values) +} + +/// Repeats one PHP string byte sequence according to a PHP-cast integer count. +fn eval_str_repeat_result( + value: RuntimeCellHandle, + times: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let times = eval_int_value(times, values)?; + if times < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; + let capacity = bytes + .len() + .checked_mul(times) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + for _ in 0..times { + output.extend_from_slice(&bytes); + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. +pub(super) fn eval_builtin_str_replace( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [search, replace, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let search = eval_expr(search, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_str_replace_result(name, search, replace, subject, values) +} + +/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. +fn eval_str_replace_result( + name: &str, + search: RuntimeCellHandle, + replace: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let search = values.string_bytes(search)?; + let replace = values.string_bytes(replace)?; + let subject = values.string_bytes(subject)?; + if search.is_empty() { + return values.string_bytes_value(&subject); + } + + let mut output = Vec::with_capacity(subject.len()); + let mut start = 0; + while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { + output.extend_from_slice(&subject[start..found]); + output.extend_from_slice(&replace); + start = found + search.len(); + } + output.extend_from_slice(&subject[start..]); + values.string_bytes_value(&output) +} + +/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. +fn eval_find_replace_match( + name: &str, + subject: &[u8], + search: &[u8], + start: usize, +) -> Result, EvalStatus> { + match name { + "str_replace" => Ok(eval_find_subslice(subject, search, start)), + "str_ireplace" => Ok(subject + .get(start..) + .and_then(|tail| { + tail.windows(search.len()) + .position(|window| window.eq_ignore_ascii_case(search)) + }) + .map(|position| position + start)), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. +pub(super) fn eval_builtin_str_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_pad_result(value, length, None, None, values) + } + [value, length, pad_string] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), None, values) + } + [value, length, pad_string, pad_type] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + let pad_type = eval_expr(pad_type, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Pads one byte string to a PHP target length using cyclic pad bytes. +fn eval_str_pad_result( + value: RuntimeCellHandle, + length: RuntimeCellHandle, + pad_string: Option, + pad_type: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let target_length = eval_int_value(length, values)?; + let Ok(target_length) = usize::try_from(target_length) else { + return values.string_bytes_value(&bytes); + }; + if target_length <= bytes.len() { + return values.string_bytes_value(&bytes); + } + + let pad_string = match pad_string { + Some(pad_string) => values.string_bytes(pad_string)?, + None => b" ".to_vec(), + }; + if pad_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let pad_type = match pad_type { + Some(pad_type) => eval_int_value(pad_type, values)?, + None => 1, + }; + let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; + let capacity = bytes + .len() + .checked_add(left_pad) + .and_then(|size| size.checked_add(right_pad)) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + eval_append_repeated_pad(&mut output, &pad_string, left_pad); + output.extend_from_slice(&bytes); + eval_append_repeated_pad(&mut output, &pad_string, right_pad); + values.string_bytes_value(&output) +} + +/// Splits a `str_pad()` pad budget into left and right byte counts. +fn eval_str_pad_sides(pad_budget: usize, pad_type: i64) -> Result<(usize, usize), EvalStatus> { + match pad_type { + 0 => Ok((pad_budget, 0)), + 1 => Ok((0, pad_budget)), + 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends `count` bytes by cycling through the provided non-empty pad string. +fn eval_append_repeated_pad(output: &mut Vec, pad_string: &[u8], count: usize) { + for index in 0..count { + output.push(pad_string[index % pad_string.len()]); + } +} + +/// Evaluates PHP `str_split(...)` over one string and optional chunk length. +pub(super) fn eval_builtin_str_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_str_split_result(value, None, values) + } + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_split_result(value, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. +fn eval_str_split_result( + value: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let length = match length { + Some(length) => eval_int_value(length, values)?, + None => 1, + }; + if length <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = values.array_new(0)?; + for (index, chunk) in bytes.chunks(length).enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string_bytes_value(chunk)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. +pub(super) fn eval_builtin_nl2br( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_nl2br_result(value, true, values) + } + [value, use_xhtml] => { + let value = eval_expr(value, context, scope, values)?; + let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; + let use_xhtml = values.truthy(use_xhtml)?; + eval_nl2br_result(value, use_xhtml, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. +fn eval_nl2br_result( + value: RuntimeCellHandle, + use_xhtml: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let br = if use_xhtml { + b"
".as_slice() + } else { + b"
".as_slice() + }; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + let byte = bytes[index]; + if byte == b'\r' || byte == b'\n' { + output.extend_from_slice(br); + output.push(byte); + if index + 1 < bytes.len() + && ((byte == b'\r' && bytes[index + 1] == b'\n') + || (byte == b'\n' && bytes[index + 1] == b'\r')) + { + output.push(bytes[index + 1]); + index += 2; + continue; + } + } else { + output.push(byte); + } + index += 1; + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. +pub(super) fn eval_builtin_substr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, offset] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_result(value, offset, None, values) + } + [value, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_result(value, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Slices a PHP byte string using PHP `substr()` offset and length rules. +fn eval_substr_result( + value: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(0) + } else { + start.saturating_add(length).min(total) + } + } + }; + let end = end.max(start); + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string_bytes_value(&bytes[start..end]) +} + +/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. +pub(super) fn eval_builtin_substr_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, replace, offset] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, None, values) + } + [value, replace, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. +fn eval_substr_replace_result( + value: RuntimeCellHandle, + replace: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let replacement = values.string_bytes(replace)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(start) + } else { + start.saturating_add(length).min(total) + } + } + }; + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(bytes.len() + replacement.len()); + output.extend_from_slice(&bytes[..start]); + output.extend_from_slice(&replacement); + output.extend_from_slice(&bytes[end..]); + values.string_bytes_value(&output) +} + +/// Evaluates eval HTML entity encode/decode builtins over one string expression. +pub(super) fn eval_builtin_html_entity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_html_entity_result(name, value, values) +} + +/// Applies the eval-supported HTML entity transform for one PHP string value. +fn eval_html_entity_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), + "html_entity_decode" => eval_html_entity_decode_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Encodes the HTML-special byte characters covered by elephc's static helper. +fn eval_htmlspecialchars_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + b'&' => output.extend_from_slice(b"&"), + b'<' => output.extend_from_slice(b"<"), + b'>' => output.extend_from_slice(b">"), + b'"' => output.extend_from_slice(b"""), + b'\'' => output.extend_from_slice(b"'"), + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Decodes one pass of the HTML entities emitted by the eval/static encoders. +fn eval_html_entity_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'&' { + if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { + output.push(decoded); + index += width; + continue; + } + } + output.push(bytes[index]); + index += 1; + } + values.string_bytes_value(&output) +} + +/// Returns the decoded byte and consumed width for one supported HTML entity. +fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { + for (entity, decoded) in [ + (b"<".as_slice(), b'<'), + (b">".as_slice(), b'>'), + (b""".as_slice(), b'"'), + (b"'".as_slice(), b'\''), + (b"'".as_slice(), b'\''), + (b"&".as_slice(), b'&'), + ] { + if bytes.starts_with(entity) { + return Some((decoded, entity.len())); + } + } + None +} + +/// Evaluates PHP URL encode builtins over one eval string expression. +pub(super) fn eval_builtin_url_encode( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_encode_result(name, value, values) +} + +/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. +fn eval_url_encode_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + for byte in bytes { + if eval_url_encode_keeps_byte(name, byte)? { + output.push(byte); + } else if name == "urlencode" && byte == b' ' { + output.push(b'+'); + } else { + output.push(b'%'); + output.push(HEX[(byte >> 4) as usize]); + output.push(HEX[(byte & 0x0f) as usize]); + } + } + values.string_bytes_value(&output) +} + +/// Returns whether a byte remains unescaped for the selected PHP URL encoder. +fn eval_url_encode_keeps_byte(name: &str, byte: u8) -> Result { + let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); + match name { + "urlencode" => Ok(common), + "rawurlencode" => Ok(common || byte == b'~'), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP URL decode builtins over one eval string expression. +pub(super) fn eval_builtin_url_decode( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_decode_result(name, value, values) +} + +/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. +fn eval_url_decode_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let plus_to_space = match name { + "urldecode" => true, + "rawurldecode" => false, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'+' && plus_to_space { + output.push(b' '); + index += 1; + } else if bytes[index] == b'%' && index + 2 < bytes.len() { + if let (Some(high), Some(low)) = ( + eval_hex_nibble(bytes[index + 1]), + eval_hex_nibble(bytes[index + 2]), + ) { + output.push((high << 4) | low); + index += 3; + continue; + } + output.push(bytes[index]); + index += 1; + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP `ctype_*` predicates over one eval string expression. +pub(super) fn eval_builtin_ctype( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ctype_result(name, value, values) +} + +/// Returns the PHP boolean result for one ASCII `ctype_*` byte-string check. +fn eval_ctype_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut matches = !bytes.is_empty(); + for byte in bytes { + if !eval_ctype_byte_matches(name, byte)? { + matches = false; + break; + } + } + values.bool_value(matches) +} + +/// Checks one byte against the selected PHP ASCII character class. +fn eval_ctype_byte_matches(name: &str, byte: u8) -> Result { + match name { + "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), + "ctype_digit" => Ok(byte.is_ascii_digit()), + "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), + "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP `crc32(...)` over one eval string expression. +pub(super) fn eval_builtin_crc32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_crc32_result(value, values) +} + +/// Computes PHP's non-negative CRC-32 integer over one converted byte string. +fn eval_crc32_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(eval_crc32_bytes(&bytes))) +} + +/// Evaluates one-shot PHP hash digest builtins over eval expressions. +pub(super) fn eval_builtin_hash_one_shot( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_hash_one_shot_result(name, &evaluated_args, values) +} + +/// Computes the result for one-shot PHP hash digest builtins from evaluated args. +fn eval_hash_one_shot_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "md5" | "sha1" => { + let (data, binary) = match evaluated_args { + [data] => (*data, false), + [data, binary] => (*data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let data = values.string_bytes(data)?; + eval_hash_digest_result(name.as_bytes(), &data, binary, values) + } + "hash" => { + let (algo, data, binary) = match evaluated_args { + [algo, data] => (*algo, *data, false), + [algo, data, binary] => (*algo, *data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + eval_hash_digest_result(&algo, &data, binary, values) + } + "hash_file" => { + let (algo, filename, binary) = match evaluated_args { + [algo, filename] => (*algo, *filename, false), + [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_hash_file_result(algo, filename, binary, values) + } + "hash_hmac" => { + let (algo, data, key, binary) = match evaluated_args { + [algo, data, key] => (*algo, *data, *key, false), + [algo, data, key, binary] => (*algo, *data, *key, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + let key = values.string_bytes(key)?; + eval_hash_hmac_result(&algo, &data, &key, binary, values) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Reads a local file and returns its PHP hash digest or false when it cannot be read. +fn eval_hash_file_result( + algo: RuntimeCellHandle, + filename: RuntimeCellHandle, + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(data) => eval_hash_digest_result(&algo, &data, binary, values), + Err(_) => values.bool_value(false), + } +} + +/// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. +fn eval_hash_digest_result( + algo: &[u8], + data: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hash(algo, data)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. +fn eval_hash_hmac_result( + algo: &[u8], + data: &[u8], + key: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hmac(algo, data, key)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. +fn eval_crypto_hash(algo: &[u8], data: &[u8]) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hash( + algo.as_ptr(), + algo.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. +fn eval_crypto_hmac(algo: &[u8], data: &[u8], key: &[u8]) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hmac( + algo.as_ptr(), + algo.len(), + key.as_ptr(), + key.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Converts a crypto ABI digest length into an owned digest byte vector. +fn eval_crypto_digest_bytes(len: isize, output: &[u8; 64]) -> Result, EvalStatus> { + let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + if len > output.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(output[..len].to_vec()) +} + +/// Formats a raw digest using PHP's `$binary` flag convention. +fn eval_format_digest_result( + raw: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if binary { + return values.string_bytes_value(raw); + } + values.string(&eval_lower_hex_bytes(raw)) +} + +/// Evaluates PHP `hash_algos()` with no arguments. +pub(super) fn eval_builtin_hash_algos( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + +/// Builds the indexed array returned by eval `hash_algos()`. +fn eval_hash_algos_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_HASH_ALGOS, values) +} + +/// Builds one indexed PHP array from a static string slice. +fn eval_static_string_array_result( + items: &[&str], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `spl_classes()` with no arguments. +pub(super) fn eval_builtin_spl_classes( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values) +} + +/// Builds the static class-name list returned by eval `spl_classes()`. +fn eval_spl_classes_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) +} + +/// Evaluates PHP stream introspection list builtins with no arguments. +pub(super) fn eval_builtin_stream_introspection( + name: &str, + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, values) +} + +/// Builds the static list returned by one eval stream introspection builtin. +fn eval_stream_introspection_result( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let items = match name { + "stream_get_filters" => EVAL_STREAM_FILTERS, + "stream_get_transports" => EVAL_STREAM_TRANSPORTS, + "stream_get_wrappers" => EVAL_STREAM_WRAPPERS, + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_static_string_array_result(items, values) +} + +/// Evaluates PHP `time()` with no arguments. +pub(super) fn eval_builtin_time( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) +} + +/// Returns the current Unix timestamp as a boxed PHP integer. +fn eval_time_result(values: &mut impl RuntimeValueOps) -> Result { + values.int(eval_current_unix_timestamp()?) +} + +/// Returns the current Unix timestamp as an integer payload. +fn eval_current_unix_timestamp() -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)? + .as_secs(); + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. +pub(super) fn eval_builtin_date( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [format] => { + let format = eval_expr(format, context, scope, values)?; + eval_date_result(format, None, values) + } + [format, timestamp] => { + let format = eval_expr(format, context, scope, values)?; + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_date_result(format, Some(timestamp), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. +fn eval_date_result( + format: RuntimeCellHandle, + timestamp: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let format = values.string_bytes(format)?; + let timestamp = match timestamp { + Some(timestamp) => eval_int_value(timestamp, values)?, + None => eval_current_unix_timestamp()?, + }; + let tm = eval_localtime(timestamp)?; + let output = eval_format_date_bytes(&format, &tm, timestamp)?; + values.string_bytes_value(&output) +} + +/// Converts one Unix timestamp to local broken-down time through libc. +fn eval_localtime(timestamp: i64) -> Result { + let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; + let mut tm = MaybeUninit::::uninit(); + let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) +} + +/// Applies PHP `date()` tokens to one local broken-down timestamp. +fn eval_format_date_bytes( + format: &[u8], + tm: &libc::tm, + timestamp: i64, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut escaped = false; + for byte in format { + if escaped { + output.push(*byte); + escaped = false; + continue; + } + if *byte == b'\\' { + escaped = true; + continue; + } + eval_push_date_token(&mut output, *byte, tm, timestamp)?; + } + if escaped { + output.push(b'\\'); + } + Ok(output) +} + +/// Appends the expansion for one PHP `date()` token, or the token literal. +fn eval_push_date_token( + output: &mut Vec, + token: u8, + tm: &libc::tm, + timestamp: i64, +) -> Result<(), EvalStatus> { + match token { + b'Y' => eval_push_padded_number(output, i64::from(tm.tm_year) + 1900, 4), + b'm' => eval_push_padded_number(output, i64::from(tm.tm_mon) + 1, 2), + b'd' => eval_push_padded_number(output, i64::from(tm.tm_mday), 2), + b'H' => eval_push_padded_number(output, i64::from(tm.tm_hour), 2), + b'i' => eval_push_padded_number(output, i64::from(tm.tm_min), 2), + b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), + b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), + b'D' => output + .extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'M' => { + output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) + } + b'N' => { + let weekday = tm.tm_wday; + let iso_weekday = if weekday == 0 { 7 } else { weekday }; + output.extend_from_slice(iso_weekday.to_string().as_bytes()); + } + b'j' => output.extend_from_slice(tm.tm_mday.to_string().as_bytes()), + b'n' => output.extend_from_slice((tm.tm_mon + 1).to_string().as_bytes()), + b'G' => output.extend_from_slice(tm.tm_hour.to_string().as_bytes()), + b'g' => { + let hour = tm.tm_hour % 12; + let hour = if hour == 0 { 12 } else { hour }; + output.extend_from_slice(hour.to_string().as_bytes()); + } + b'A' => output.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }), + b'a' => output.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }), + b'U' => output.extend_from_slice(timestamp.to_string().as_bytes()), + _ => output.push(token), + } + Ok(()) +} + +/// Returns a checked month index for PHP `date()` name tables. +fn eval_tm_month_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_mon).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_MONTH_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Returns a checked weekday index for PHP `date()` name tables. +fn eval_tm_weekday_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_wday).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_WEEKDAY_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Appends one zero-padded decimal value with the requested minimum width. +fn eval_push_padded_number(output: &mut Vec, value: i64, width: usize) { + output.extend_from_slice(format!("{value:0width$}").as_bytes()); +} + +/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. +pub(super) fn eval_builtin_mktime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hour, minute, second, month, day, year] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hour = eval_expr(hour, context, scope, values)?; + let minute = eval_expr(minute, context, scope, values)?; + let second = eval_expr(second, context, scope, values)?; + let month = eval_expr(month, context, scope, values)?; + let day = eval_expr(day, context, scope, values)?; + let year = eval_expr(year, context, scope, values)?; + eval_mktime_result(hour, minute, second, month, day, year, values) +} + +/// Converts PHP date components to a local Unix timestamp through libc `mktime`. +fn eval_mktime_result( + hour: RuntimeCellHandle, + minute: RuntimeCellHandle, + second: RuntimeCellHandle, + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_mktime_timestamp( + eval_int_cell_as_c_int(hour, values)?, + eval_int_cell_as_c_int(minute, values)?, + eval_int_cell_as_c_int(second, values)?, + eval_int_cell_as_c_int(month, values)?, + eval_int_cell_as_c_int(day, values)?, + eval_int_cell_as_c_int(year, values)?, + )?; + values.int(timestamp) +} + +/// Converts local date components into a Unix timestamp through libc `mktime`. +fn eval_mktime_timestamp( + hour: libc::c_int, + minute: libc::c_int, + second: libc::c_int, + month: libc::c_int, + day: libc::c_int, + year: libc::c_int, +) -> Result { + let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = second; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_year = year - 1900; + tm.tm_isdst = -1; + let timestamp = unsafe { libc::mktime(&mut tm) }; + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. +fn eval_int_cell_as_c_int( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP `strtotime(datetime)` for eval's supported date-string subset. +pub(super) fn eval_builtin_strtotime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [datetime] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let datetime = eval_expr(datetime, context, scope, values)?; + eval_strtotime_result(datetime, values) +} + +/// Parses one eval `strtotime()` input and boxes the resulting timestamp. +fn eval_strtotime_result( + datetime: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(datetime)?; + let timestamp = eval_strtotime_bytes(&bytes)?; + values.int(timestamp) +} + +/// Parses eval's supported `strtotime()` strings into local Unix timestamps. +fn eval_strtotime_bytes(bytes: &[u8]) -> Result { + let bytes = eval_trim_ascii_whitespace(bytes); + if bytes.eq_ignore_ascii_case(b"now") { + return eval_current_unix_timestamp(); + } + let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { + return Ok(-1); + }; + eval_mktime_timestamp(hour, minute, second, month, day, year) +} + +/// Trims ASCII whitespace from both ends of one byte slice. +fn eval_trim_ascii_whitespace(bytes: &[u8]) -> &[u8] { + let mut start = 0; + let mut end = bytes.len(); + while start < end && bytes[start].is_ascii_whitespace() { + start += 1; + } + while end > start && bytes[end - 1].is_ascii_whitespace() { + end -= 1; + } + &bytes[start..end] +} + +/// Parses fixed-width ISO date and datetime forms supported by eval `strtotime()`. +fn eval_parse_iso_datetime( + bytes: &[u8], +) -> Option<( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, +)> { + if bytes.len() != 10 && bytes.len() != 16 && bytes.len() != 19 { + return None; + } + if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { + return None; + } + let year = eval_parse_fixed_digits(bytes, 0, 4)?; + let month = eval_parse_fixed_digits(bytes, 5, 2)?; + let day = eval_parse_fixed_digits(bytes, 8, 2)?; + let (hour, minute, second) = if bytes.len() == 10 { + (0, 0, 0) + } else { + if !matches!(bytes.get(10), Some(b' ') | Some(b'T') | Some(b't')) { + return None; + } + if bytes.get(13) != Some(&b':') { + return None; + } + let hour = eval_parse_fixed_digits(bytes, 11, 2)?; + let minute = eval_parse_fixed_digits(bytes, 14, 2)?; + let second = if bytes.len() == 19 { + if bytes.get(16) != Some(&b':') { + return None; + } + eval_parse_fixed_digits(bytes, 17, 2)? + } else { + 0 + }; + (hour, minute, second) + }; + if !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || !(0..=23).contains(&hour) + || !(0..=59).contains(&minute) + || !(0..=59).contains(&second) + { + return None; + } + Some((year, month, day, hour, minute, second)) +} + +/// Parses a fixed-width decimal field as a libc-compatible integer. +fn eval_parse_fixed_digits(bytes: &[u8], start: usize, len: usize) -> Option { + let end = start.checked_add(len)?; + let field = bytes.get(start..end)?; + let mut value: libc::c_int = 0; + for byte in field { + if !byte.is_ascii_digit() { + return None; + } + value = value.checked_mul(10)?; + value = value.checked_add(libc::c_int::from(byte - b'0'))?; + } + Some(value) +} + +/// Evaluates PHP `microtime()` with an optional ignored argument. +pub(super) fn eval_builtin_microtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_microtime_result(values), + [as_float] => { + let _ = eval_expr(as_float, context, scope, values)?; + eval_microtime_result(values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the current Unix timestamp with microsecond precision as a boxed float. +fn eval_microtime_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)?; + let seconds = timestamp.as_secs() as f64; + let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; + values.float(seconds + micros) +} + +/// Evaluates PHP `sleep($seconds)` over one eval expression. +pub(super) fn eval_builtin_sleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [seconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let seconds = eval_expr(seconds, context, scope, values)?; + eval_sleep_result(seconds, values) +} + +/// Sleeps for a non-negative number of seconds and returns PHP's remaining-seconds value. +fn eval_sleep_result( + seconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let seconds = eval_int_value(seconds, values)?; + let seconds = u64::try_from(seconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_secs(seconds)); + values.int(0) +} + +/// Evaluates PHP `usleep($microseconds)` over one eval expression. +pub(super) fn eval_builtin_usleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [microseconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let microseconds = eval_expr(microseconds, context, scope, values)?; + eval_usleep_result(microseconds, values) +} + +/// Sleeps for a non-negative number of microseconds and returns PHP null. +fn eval_usleep_result( + microseconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let microseconds = eval_int_value(microseconds, values)?; + let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_micros(microseconds)); + values.null() +} + +/// Evaluates PHP `phpversion()` with no arguments. +pub(super) fn eval_builtin_phpversion( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values) +} + +/// Returns the root elephc package version as a boxed PHP string. +fn eval_phpversion_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(eval_compiler_php_version()) +} + +/// Reads the root package version from the workspace manifest used by native `phpversion()`. +pub(super) fn eval_compiler_php_version() -> &'static str { + let mut in_package = false; + for line in EVAL_ROOT_CARGO_TOML.lines() { + let line = line.trim(); + if line == "[package]" { + in_package = true; + continue; + } + if in_package && line.starts_with('[') { + break; + } + if in_package { + if let Some(value) = line.strip_prefix("version = ") { + return value.trim_matches('"'); + } + } + } + env!("CARGO_PKG_VERSION") +} + +/// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. +pub(super) fn eval_builtin_php_uname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_php_uname_result(None, values), + [mode] => { + let mode = eval_expr(mode, context, scope, values)?; + eval_php_uname_result(Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads the local uname fields and formats the PHP `php_uname()` mode result. +fn eval_php_uname_result( + mode: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => { + let bytes = values.string_bytes(mode)?; + let [mode] = bytes.as_slice() else { + return Err(EvalStatus::RuntimeFatal); + }; + *mode + } + None => b'a', + }; + + let mut utsname = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes all uname fields into the stack-owned utsname buffer. + libc::uname(utsname.as_mut_ptr()) + }; + if status != 0 { + return values.string(""); + } + let utsname = unsafe { + // `uname` succeeded, so libc initialized the full `utsname` structure. + utsname.assume_init() + }; + let sysname = eval_uname_field_bytes(&utsname.sysname); + let nodename = eval_uname_field_bytes(&utsname.nodename); + let release = eval_uname_field_bytes(&utsname.release); + let version = eval_uname_field_bytes(&utsname.version); + let machine = eval_uname_field_bytes(&utsname.machine); + + match mode { + b'a' => { + let mut output = Vec::new(); + for field in [&sysname, &nodename, &release, &version, &machine] { + if !output.is_empty() { + output.push(b' '); + } + output.extend_from_slice(field); + } + values.string_bytes_value(&output) + } + b's' => values.string_bytes_value(&sysname), + b'n' => values.string_bytes_value(&nodename), + b'r' => values.string_bytes_value(&release), + b'v' => values.string_bytes_value(&version), + b'm' => values.string_bytes_value(&machine), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. +fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { + let length = field + .iter() + .position(|byte| *byte == 0) + .unwrap_or(field.len()); + field[..length].iter().map(|byte| *byte as u8).collect() +} + +/// Evaluates PHP `getcwd()` with no arguments. +pub(super) fn eval_builtin_getcwd( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values) +} + +/// Returns the process current working directory as a boxed PHP string. +fn eval_getcwd_result(values: &mut impl RuntimeValueOps) -> Result { + let cwd = std::env::current_dir().map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(cwd.to_string_lossy().as_ref()) +} + +/// Evaluates one PHP filesystem predicate over an eval expression. +pub(super) fn eval_builtin_file_probe( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_probe_result(name, filename, values) +} + +/// Computes one local filesystem predicate and returns a PHP boolean. +fn eval_file_probe_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let path = std::path::Path::new(&path); + let result = match name { + "file_exists" => path.exists(), + "is_dir" => path.is_dir(), + "is_executable" => eval_path_is_executable(path), + "is_file" => path.is_file(), + "is_link" => std::fs::symlink_metadata(path) + .map(|metadata| metadata.file_type().is_symlink()) + .unwrap_or(false), + "is_readable" => eval_path_is_readable(path), + "is_writable" | "is_writeable" => eval_path_is_writable(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(result) +} + +/// Evaluates one scalar PHP stat metadata builtin over an eval expression. +pub(super) fn eval_builtin_file_stat_scalar( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_stat_scalar_result(name, filename, values) +} + +/// Returns scalar stat metadata, using PHP false for failure where native elephc does. +fn eval_file_stat_scalar_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(_) if name == "filemtime" => return values.int(0), + Err(_) => return values.bool_value(false), + }; + match name { + "fileatime" => values.int(metadata.atime()), + "filectime" => values.int(metadata.ctime()), + "filegroup" => values.int(i64::from(metadata.gid())), + "fileinode" => { + values.int(i64::try_from(metadata.ino()).map_err(|_| EvalStatus::RuntimeFatal)?) + } + "filemtime" => values.int(metadata.mtime()), + "fileowner" => values.int(i64::from(metadata.uid())), + "fileperms" => values.int(i64::from(metadata.mode())), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `file_get_contents($filename)` over one eval expression. +pub(super) fn eval_builtin_file_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_get_contents_result(filename, values) +} + +/// Reads a local file into a PHP string, or returns false when it cannot be opened. +fn eval_file_get_contents_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(bytes) => values.string_bytes_value(&bytes), + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} + +/// Evaluates PHP `file($filename)` over one eval expression. +pub(super) fn eval_builtin_file( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_result(filename, values) +} + +/// Reads one local file and returns an indexed array of line byte strings. +fn eval_file_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let bytes = match std::fs::read(path) { + Ok(bytes) => bytes, + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + }; + eval_file_lines_array(&bytes, values) +} + +/// Splits file payload bytes into runtime array entries, preserving trailing newlines. +fn eval_file_lines_array( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(0)?; + let mut line_start = 0; + let mut line_index = 0; + for (index, byte) in bytes.iter().enumerate() { + if *byte != b'\n' { + continue; + } + result = + eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; + line_start = index + 1; + line_index += 1; + } + if line_start < bytes.len() { + result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; + } + Ok(result) +} + +/// Evaluates PHP `readfile($filename)` over one eval expression. +pub(super) fn eval_builtin_readfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_readfile_result(filename, values) +} + +/// Streams one local file to eval output and returns a byte count, false, or -1. +fn eval_readfile_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let path = std::path::Path::new(&path); + if path.is_dir() { + return values.int(-1); + } + let bytes = match std::fs::read(path) { + Ok(bytes) => bytes, + Err(_) => return values.bool_value(false), + }; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. +pub(super) fn eval_builtin_file_put_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_file_put_contents_result(filename, data, values) +} + +/// Writes a PHP string to a local file and returns the written byte count or false. +fn eval_file_put_contents_result( + filename: RuntimeCellHandle, + data: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let data = values.string_bytes(data)?; + match std::fs::write(path, &data) { + Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), + Err(_) => values.bool_value(false), + } +} + +/// Evaluates PHP `filesize($filename)` over one eval expression. +pub(super) fn eval_builtin_filesize( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filesize_result(filename, values) +} + +/// Returns one local file size in bytes, or zero when stat fails. +fn eval_filesize_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let len = std::fs::metadata(path) + .map(|metadata| metadata.len()) + .unwrap_or(0); + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `filetype($filename)` over one eval expression. +pub(super) fn eval_builtin_filetype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filetype_result(filename, values) +} + +/// Returns the PHP filetype string for one path, or false when lstat fails. +fn eval_filetype_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let file_type = match std::fs::symlink_metadata(path) { + Ok(metadata) => metadata.file_type(), + Err(_) => return values.bool_value(false), + }; + let label = if file_type.is_file() { + "file" + } else if file_type.is_dir() { + "dir" + } else if file_type.is_symlink() { + "link" + } else if file_type.is_char_device() { + "char" + } else if file_type.is_block_device() { + "block" + } else if file_type.is_fifo() { + "fifo" + } else if file_type.is_socket() { + "socket" + } else { + "unknown" + }; + values.string(label) +} + +/// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. +pub(super) fn eval_builtin_stat_array( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_stat_array_result(name, filename, values) +} + +/// Builds PHP's stat array for one local path, or returns false on stat failure. +fn eval_stat_array_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let metadata = match name { + "stat" => std::fs::metadata(path), + "lstat" => std::fs::symlink_metadata(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let metadata = match metadata { + Ok(metadata) => metadata, + Err(_) => return values.bool_value(false), + }; + eval_stat_metadata_array(&metadata, values) +} + +/// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. +fn eval_stat_metadata_array( + metadata: &std::fs::Metadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let fields = [ + ("dev", eval_u64_to_i64(metadata.dev())?), + ("ino", eval_u64_to_i64(metadata.ino())?), + ("mode", i64::from(metadata.mode())), + ("nlink", eval_u64_to_i64(metadata.nlink())?), + ("uid", i64::from(metadata.uid())), + ("gid", i64::from(metadata.gid())), + ("rdev", eval_u64_to_i64(metadata.rdev())?), + ("size", eval_u64_to_i64(metadata.size())?), + ("atime", metadata.atime()), + ("mtime", metadata.mtime()), + ("ctime", metadata.ctime()), + ("blksize", eval_u64_to_i64(metadata.blksize())?), + ("blocks", eval_u64_to_i64(metadata.blocks())?), + ]; + let mut result = values.assoc_new(fields.len() * 2)?; + for (index, (name, value)) in fields.iter().enumerate() { + result = eval_stat_array_set_int_key(result, index, *value, values)?; + result = eval_stat_array_set_string_key(result, name, *value, values)?; + } + Ok(result) +} + +/// Inserts one integer stat field under a numeric PHP array key. +fn eval_stat_array_set_int_key( + array: RuntimeCellHandle, + key: usize, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(key).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts one integer stat field under a string PHP array key. +fn eval_stat_array_set_string_key( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Converts unsigned stat metadata into the signed integer payload used by PHP cells. +fn eval_u64_to_i64(value: u64) -> Result { + i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. +pub(super) fn eval_builtin_disk_space( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_disk_space_result(name, directory, values) +} + +/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. +fn eval_disk_space_result( + name: &str, + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(directory)?; + let Ok(path) = CString::new(bytes) else { + return values.float(0.0); + }; + let mut stats = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes the statvfs fields for this NUL-terminated local path. + libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) + }; + if status != 0 { + return values.float(0.0); + } + let stats = unsafe { + // `statvfs` succeeded, so libc initialized the full stat buffer. + stats.assume_init() + }; + let block_size = if stats.f_frsize > 0 { + stats.f_frsize + } else { + stats.f_bsize + }; + let blocks = match name { + "disk_free_space" => stats.f_bavail, + "disk_total_space" => stats.f_blocks, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.float((block_size as f64) * (blocks as f64)) +} + +/// Evaluates a one-path filesystem operation that returns a PHP boolean. +pub(super) fn eval_builtin_unary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_unary_path_bool_result(name, path, values) +} + +/// Executes a one-path local filesystem operation and returns whether it succeeded. +fn eval_unary_path_bool_result( + name: &str, + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let ok = match name { + "chdir" => std::env::set_current_dir(path).is_ok(), + "mkdir" => std::fs::create_dir(path).is_ok(), + "rmdir" => std::fs::remove_dir(path).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Evaluates a two-path filesystem operation that returns a PHP boolean. +pub(super) fn eval_builtin_binary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [from, to] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let from = eval_expr(from, context, scope, values)?; + let to = eval_expr(to, context, scope, values)?; + eval_binary_path_bool_result(name, from, to, values) +} + +/// Executes a two-path local filesystem operation and returns whether it succeeded. +fn eval_binary_path_bool_result( + name: &str, + from: RuntimeCellHandle, + to: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_path_string(from, values)?; + let to = eval_path_string(to, values)?; + let ok = match name { + "copy" => std::fs::copy(from, to).is_ok(), + "link" => std::fs::hard_link(from, to).is_ok(), + "rename" => std::fs::rename(from, to).is_ok(), + "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. +pub(super) fn eval_builtin_chmod( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, permissions] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let permissions = eval_expr(permissions, context, scope, values)?; + eval_chmod_result(filename, permissions, values) +} + +/// Changes one local file's mode and returns whether the operation succeeded. +fn eval_chmod_result( + filename: RuntimeCellHandle, + permissions: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let mode = eval_int_value(permissions, values)? as u32; + let permissions = std::fs::Permissions::from_mode(mode); + values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) +} + +/// Evaluates PHP `scandir($directory)` over one eval expression. +pub(super) fn eval_builtin_scandir( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_scandir_result(directory, values) +} + +/// Lists one local directory into an indexed string array, or an empty array on failure. +fn eval_scandir_result( + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(directory, values)?; + let Ok(entries) = std::fs::read_dir(path) else { + return values.array_new(0); + }; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; + } + Ok(result) +} + +/// Evaluates PHP `glob($pattern)` over one eval expression. +pub(super) fn eval_builtin_glob( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + eval_glob_result(pattern, values) +} + +/// Expands one local glob pattern into a sorted indexed PHP string array. +fn eval_glob_result( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = eval_path_string(pattern, values)?; + let matches = eval_glob_matches(&pattern); + let mut result = values.array_new(matches.len())?; + for (index, path) in matches.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; + } + Ok(result) +} + +/// Collects sorted matches for one local glob pattern. +fn eval_glob_matches(pattern: &str) -> Vec { + if pattern.is_empty() { + return Vec::new(); + } + if !eval_glob_component_has_magic(pattern) { + return std::path::Path::new(pattern) + .exists() + .then(|| pattern.to_string()) + .into_iter() + .collect(); + } + let absolute = pattern.starts_with('/'); + let components: Vec<&str> = pattern + .split('/') + .filter(|component| !component.is_empty()) + .collect(); + let mut matches = Vec::new(); + let base = if absolute { + std::path::PathBuf::from("/") + } else { + std::path::PathBuf::from(".") + }; + let prefix = if absolute { "/" } else { "" }; + eval_glob_collect(&base, prefix, &components, &mut matches); + matches.sort(); + matches +} + +/// Recursively expands one glob path component at a time. +fn eval_glob_collect( + base: &std::path::Path, + prefix: &str, + components: &[&str], + matches: &mut Vec, +) { + let Some((component, rest)) = components.split_first() else { + if base.exists() && !prefix.is_empty() { + matches.push(prefix.to_string()); + } + return; + }; + if !eval_glob_component_has_magic(component) { + let next_base = base.join(component); + if rest.is_empty() { + if next_base.exists() { + matches.push(eval_glob_join_output(prefix, component)); + } + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, component); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + return; + } + let Ok(entries) = std::fs::read_dir(base) else { + return; + }; + let mut names = Vec::new(); + for entry in entries.flatten() { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + for name in names { + if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { + continue; + } + let next_base = base.join(&name); + if rest.is_empty() { + matches.push(eval_glob_join_output(prefix, &name)); + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, &name); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + } +} + +/// Joins a display path prefix and component while preserving absolute-root output. +fn eval_glob_join_output(prefix: &str, component: &str) -> String { + if prefix.is_empty() { + component.to_string() + } else if prefix == "/" { + format!("/{component}") + } else { + format!("{prefix}/{component}") + } +} + +/// Returns whether a glob component contains wildcard syntax. +fn eval_glob_component_has_magic(component: &str) -> bool { + component + .as_bytes() + .iter() + .any(|byte| matches!(byte, b'*' | b'?' | b'[')) +} + +/// Writes one byte-string value into an indexed runtime array at a zero-based position. +fn eval_array_set_indexed_bytes( + array: RuntimeCellHandle, + index: usize, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} + +/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. +pub(super) fn eval_builtin_tempnam( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory, prefix] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + let prefix = eval_expr(prefix, context, scope, values)?; + eval_tempnam_result(directory, prefix, values) +} + +/// Creates a unique local temporary file and returns its path, or an empty string on failure. +fn eval_tempnam_result( + directory: RuntimeCellHandle, + prefix: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + let prefix = values.string_bytes(prefix)?; + let prefix = String::from_utf8_lossy(&prefix); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + for attempt in 0..1000_u32 { + let candidate = + std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&candidate) + { + Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(_) => return values.string(""), + } + } + values.string("") +} + +/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. +fn eval_tempnam_filename(prefix: &str, nonce: u128, attempt: u32) -> String { + format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) +} + +/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. +pub(super) fn eval_builtin_touch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [filename] => { + let filename = eval_expr(filename, context, scope, values)?; + eval_touch_result(filename, None, None, values) + } + [filename, mtime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), None, values) + } + [filename, mtime, atime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + let atime = eval_expr(atime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), Some(atime), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates or stamps one local file and returns whether the operation succeeded. +fn eval_touch_result( + filename: RuntimeCellHandle, + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let (mtime, atime) = eval_touch_times(mtime, atime, values)?; + let file = match std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(path) + { + Ok(file) => file, + Err(_) => return values.bool_value(false), + }; + let times = std::fs::FileTimes::new() + .set_modified(mtime) + .set_accessed(atime); + values.bool_value(file.set_times(times).is_ok()) +} + +/// Resolves PHP touch timestamp defaults into concrete system times. +fn eval_touch_times( + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { + let now = std::time::SystemTime::now(); + let Some(mtime) = mtime else { + return Ok((now, now)); + }; + if values.is_null(mtime)? { + if let Some(atime) = atime { + if !values.is_null(atime)? { + return Err(EvalStatus::RuntimeFatal); + } + } + return Ok((now, now)); + } + let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + let Some(atime) = atime else { + return Ok((mtime, mtime)); + }; + if values.is_null(atime)? { + return Ok((mtime, mtime)); + } + let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + Ok((mtime, atime)) +} + +/// Converts a Unix timestamp in seconds into a `SystemTime`. +fn eval_system_time_from_unix(seconds: i64) -> Option { + if seconds >= 0 { + std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) + } else { + std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) + } +} + +/// Evaluates PHP `umask($mask = null)` over an optional eval expression. +pub(super) fn eval_builtin_umask( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_umask_result(None, values), + [mask] => { + let mask = eval_expr(mask, context, scope, values)?; + eval_umask_result(Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `umask()` semantics and returns the previous mask. +fn eval_umask_result( + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let previous = match mask { + Some(mask) => { + let mask = eval_int_value(mask, values)? as u32; + unsafe { umask(mask) } + } + None => unsafe { + let current = umask(0); + umask(current); + current + }, + }; + values.int(i64::from(previous)) +} + +/// Evaluates PHP `readlink($path)` over one eval expression. +pub(super) fn eval_builtin_readlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_readlink_result(path, values) +} + +/// Reads one symbolic-link target string, or returns PHP false on failure. +fn eval_readlink_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + match std::fs::read_link(path) { + Ok(target) => values.string(target.to_string_lossy().as_ref()), + Err(_) => values.bool_value(false), + } +} + +/// Evaluates PHP `linkinfo($path)` over one eval expression. +pub(super) fn eval_builtin_linkinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_linkinfo_result(path, values) +} + +/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. +fn eval_linkinfo_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let dev = match std::fs::symlink_metadata(path) { + Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, + Err(_) => -1, + }; + values.int(dev) +} + +/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. +pub(super) fn eval_builtin_clearstatcache( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + values.null() +} + +/// Evaluates PHP `unlink($filename)` over one eval expression. +pub(super) fn eval_builtin_unlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_unlink_result(filename, values) +} + +/// Deletes one local file and returns whether it succeeded. +fn eval_unlink_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + values.bool_value(std::fs::remove_file(path).is_ok()) +} + +/// Converts one eval value to a filesystem path string. +pub(super) fn eval_path_string( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let filename = values.string_bytes(filename)?; + Ok(String::from_utf8_lossy(&filename).into_owned()) +} + +/// Returns whether a path can be opened for reading by the current process. +fn eval_path_is_readable(path: &std::path::Path) -> bool { + std::fs::File::open(path).is_ok() || std::fs::read_dir(path).is_ok() +} + +/// Returns whether a path has any executable bit set in its Unix mode. +fn eval_path_is_executable(path: &std::path::Path) -> bool { + std::fs::metadata(path) + .map(|metadata| metadata.mode() & 0o111 != 0) + .unwrap_or(false) +} + +/// Returns whether a path can be written by the current process. +fn eval_path_is_writable(path: &std::path::Path) -> bool { + if path.is_file() { + return std::fs::OpenOptions::new().write(true).open(path).is_ok(); + } + if !path.is_dir() { + return false; + } + let probe = path.join(format!( + ".elephc_eval_writable_probe_{}", + std::process::id() + )); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&probe) + { + Ok(_) => { + let _ = std::fs::remove_file(probe); + true + } + Err(_) => false, + } +} + +/// Evaluates PHP `basename($path, $suffix = "")` over one eval expression. +pub(super) fn eval_builtin_basename( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_basename_result(path, None, values) + } + [path, suffix] => { + let path = eval_expr(path, context, scope, values)?; + let suffix = eval_expr(suffix, context, scope, values)?; + eval_basename_result(path, Some(suffix), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `basename()` bytes and returns them as a runtime string. +fn eval_basename_result( + path: RuntimeCellHandle, + suffix: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let suffix = suffix + .map(|suffix| values.string_bytes(suffix)) + .transpose()?; + let result = eval_basename_bytes(&path, suffix.as_deref()); + values.string_bytes_value(&result) +} + +/// Extracts a PHP basename from one path byte string. +fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return Vec::new(); + } + let mut start = end; + while start > 0 && path[start - 1] != b'/' { + start -= 1; + } + let mut result = path[start..end].to_vec(); + if let Some(suffix) = suffix { + if !suffix.is_empty() && suffix.len() < result.len() && result.ends_with(suffix) { + result.truncate(result.len() - suffix.len()); + } + } + result +} + +/// Evaluates PHP `dirname($path, $levels = 1)` over one eval expression. +pub(super) fn eval_builtin_dirname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_dirname_result(path, None, values) + } + [path, levels] => { + let path = eval_expr(path, context, scope, values)?; + let levels = eval_expr(levels, context, scope, values)?; + eval_dirname_result(path, Some(levels), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `dirname()` bytes and returns them as a runtime string. +fn eval_dirname_result( + path: RuntimeCellHandle, + levels: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let levels = match levels { + Some(levels) => eval_int_value(levels, values)?, + None => 1, + }; + if levels < 1 { + return Err(EvalStatus::RuntimeFatal); + } + let mut current = path; + for _ in 0..levels { + current = eval_dirname_once(¤t); + } + values.string_bytes_value(¤t) +} + +/// Applies one PHP `dirname()` parent traversal to a path byte string. +fn eval_dirname_once(path: &[u8]) -> Vec { + if path.is_empty() { + return b".".to_vec(); + } + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return b"/".to_vec(); + } + let mut cursor = end; + while cursor > 0 { + cursor -= 1; + if path[cursor] == b'/' { + let mut parent_end = cursor; + while parent_end > 0 && path[parent_end - 1] == b'/' { + parent_end -= 1; + } + return if parent_end == 0 { + b"/".to_vec() + } else { + path[..parent_end].to_vec() + }; + } + } + b".".to_vec() +} + +/// Evaluates PHP `realpath($path)` over one eval expression. +pub(super) fn eval_builtin_realpath( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_realpath_result(path, values) +} + +/// Canonicalizes one path or returns PHP false when the path cannot be resolved. +fn eval_realpath_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let path = String::from_utf8_lossy(&path); + let Ok(canonical) = std::fs::canonicalize(path.as_ref()) else { + return values.bool_value(false); + }; + let canonical = canonical.to_string_lossy(); + values.string(canonical.as_ref()) +} + +/// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. +pub(super) fn eval_builtin_pathinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_pathinfo_result(path, None, values) + } + [path, flags] => { + let path = eval_expr(path, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_pathinfo_result(path, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `pathinfo()` as either an associative array or one component string. +fn eval_pathinfo_result( + path: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let Some(flags) = flags else { + return eval_pathinfo_array_result(&path, values); + }; + let flags = eval_int_value(flags, values)?; + if flags == EVAL_PATHINFO_ALL { + return eval_pathinfo_array_result(&path, values); + } + let component = eval_pathinfo_component_bytes(&path, flags); + values.string_bytes_value(&component) +} + +/// Builds the PHP `pathinfo()` associative-array result for all components. +fn eval_pathinfo_array_result( + path: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(4)?; + if !path.is_empty() { + let dirname = eval_pathinfo_dirname_bytes(path); + result = eval_pathinfo_array_set(result, "dirname", &dirname, values)?; + } + let parts = eval_pathinfo_parts(path); + result = eval_pathinfo_array_set(result, "basename", &parts.basename, values)?; + if parts.has_extension { + result = eval_pathinfo_array_set(result, "extension", &parts.extension, values)?; + } + eval_pathinfo_array_set(result, "filename", &parts.filename, values) +} + +/// Inserts one string component into a PHP `pathinfo()` associative result. +fn eval_pathinfo_array_set( + array: RuntimeCellHandle, + key: &str, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} + +/// Returns one PHP `pathinfo()` component for a non-all bitmask. +fn eval_pathinfo_component_bytes(path: &[u8], flags: i64) -> Vec { + if flags & EVAL_PATHINFO_DIRNAME != 0 { + return eval_pathinfo_dirname_bytes(path); + } + let parts = eval_pathinfo_parts(path); + if flags & EVAL_PATHINFO_BASENAME != 0 { + return parts.basename; + } + if flags & EVAL_PATHINFO_EXTENSION != 0 { + return parts.extension; + } + if flags & EVAL_PATHINFO_FILENAME != 0 { + return parts.filename; + } + Vec::new() +} + +/// Computes the dirname component with `pathinfo("")`'s empty-string exception. +fn eval_pathinfo_dirname_bytes(path: &[u8]) -> Vec { + if path.is_empty() { + Vec::new() + } else { + eval_dirname_once(path) + } +} + +/// Splits pathinfo basename, extension, and filename components. +fn eval_pathinfo_parts(path: &[u8]) -> EvalPathInfoParts { + let basename = eval_basename_bytes(path, None); + let Some(dot) = basename.iter().rposition(|byte| *byte == b'.') else { + return EvalPathInfoParts { + filename: basename.clone(), + basename, + extension: Vec::new(), + has_extension: false, + }; + }; + EvalPathInfoParts { + filename: basename[..dot].to_vec(), + extension: basename[dot + 1..].to_vec(), + basename, + has_extension: true, + } +} + +/// Pathinfo components derived from a basename. +struct EvalPathInfoParts { + /// Full basename component. + basename: Vec, + /// Extension component after the final dot, possibly empty for trailing-dot names. + extension: Vec, + /// Filename component before the final dot. + filename: Vec, + /// Whether the basename contained a dot and therefore has an extension key. + has_extension: bool, +} + +/// Evaluates PHP `fnmatch($pattern, $filename, $flags = 0)` over eval expressions. +pub(super) fn eval_builtin_fnmatch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, filename] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let filename = eval_expr(filename, context, scope, values)?; + eval_fnmatch_result(pattern, filename, None, values) + } + [pattern, filename, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let filename = eval_expr(filename, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_fnmatch_result(pattern, filename, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Runs PHP-style shell glob matching for one pattern/name pair. +fn eval_fnmatch_result( + pattern: RuntimeCellHandle, + filename: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let filename = values.string_bytes(filename)?; + let flags = match flags { + Some(flags) => eval_int_value(flags, values)?, + None => 0, + }; + values.bool_value(eval_fnmatch_bytes(&pattern, &filename, flags)) +} + +/// Matches byte strings using the eval-supported `fnmatch()` grammar and flags. +fn eval_fnmatch_bytes(pattern: &[u8], filename: &[u8], flags: i64) -> bool { + let mut memo = vec![vec![None; filename.len() + 1]; pattern.len() + 1]; + eval_fnmatch_at(pattern, filename, flags, 0, 0, &mut memo) +} + +/// Recursively matches a pattern suffix against a filename suffix with memoization. +fn eval_fnmatch_at( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + if let Some(result) = memo[pattern_index][filename_index] { + return result; + } + let result = if pattern_index == pattern.len() { + filename_index == filename.len() + } else { + match pattern[pattern_index] { + b'*' => eval_fnmatch_star( + pattern, + filename, + flags, + pattern_index, + filename_index, + memo, + ), + b'?' => { + eval_fnmatch_single_wildcard(filename, flags, filename_index) + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ) + } + b'[' => eval_fnmatch_class_or_literal( + pattern, + filename, + flags, + pattern_index, + filename_index, + memo, + ), + b'\\' if flags & EVAL_FNM_NOESCAPE == 0 => { + let (literal, next_pattern_index) = + eval_fnmatch_escaped_literal(pattern, pattern_index); + eval_fnmatch_literal(filename, flags, filename_index, literal) + && eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index + 1, + memo, + ) + } + literal => { + eval_fnmatch_literal(filename, flags, filename_index, literal) + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ) + } + } + }; + memo[pattern_index][filename_index] = Some(result); + result +} + +/// Handles `*`, including pathname and leading-period restrictions. +fn eval_fnmatch_star( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + let mut next_pattern_index = pattern_index + 1; + while next_pattern_index < pattern.len() && pattern[next_pattern_index] == b'*' { + next_pattern_index += 1; + } + if eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index, + memo, + ) { + return true; + } + let mut cursor = filename_index; + while cursor < filename.len() && eval_fnmatch_wildcard_can_consume(filename, flags, cursor) { + cursor += 1; + if eval_fnmatch_at(pattern, filename, flags, next_pattern_index, cursor, memo) { + return true; + } + } + false +} + +/// Returns whether `?` can consume the current filename byte. +fn eval_fnmatch_single_wildcard(filename: &[u8], flags: i64, filename_index: usize) -> bool { + filename_index < filename.len() + && eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) +} + +/// Handles a bracket class, or falls back to a literal `[` when the class is malformed. +fn eval_fnmatch_class_or_literal( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + if filename_index >= filename.len() + || !eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) + { + return false; + } + let Some((matches, next_pattern_index)) = + eval_fnmatch_class_matches(pattern, pattern_index + 1, filename[filename_index], flags) + else { + return eval_fnmatch_literal(filename, flags, filename_index, b'[') + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ); + }; + matches + && eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index + 1, + memo, + ) +} + +/// Matches one bracket class body against the current filename byte. +fn eval_fnmatch_class_matches( + pattern: &[u8], + mut index: usize, + candidate: u8, + flags: i64, +) -> Option<(bool, usize)> { + let negated = matches!(pattern.get(index).copied(), Some(b'!' | b'^')); + if negated { + index += 1; + } + let mut matched = false; + let mut closed = false; + while index < pattern.len() { + if pattern[index] == b']' { + closed = true; + index += 1; + break; + } + let start = eval_fnmatch_class_char(pattern, &mut index, flags)?; + if index + 1 < pattern.len() && pattern[index] == b'-' && pattern[index + 1] != b']' { + index += 1; + let end = eval_fnmatch_class_char(pattern, &mut index, flags)?; + if eval_fnmatch_byte_in_range(candidate, start, end, flags) { + matched = true; + } + } else if eval_fnmatch_byte_eq(candidate, start, flags) { + matched = true; + } + } + closed.then_some((if negated { !matched } else { matched }, index)) +} + +/// Reads one character from a bracket class, respecting escapes when enabled. +fn eval_fnmatch_class_char(pattern: &[u8], index: &mut usize, flags: i64) -> Option { + if *index >= pattern.len() { + return None; + } + if pattern[*index] == b'\\' && flags & EVAL_FNM_NOESCAPE == 0 && *index + 1 < pattern.len() { + *index += 2; + return Some(pattern[*index - 1]); + } + let byte = pattern[*index]; + *index += 1; + Some(byte) +} + +/// Returns whether one candidate byte falls within a possibly case-folded range. +fn eval_fnmatch_byte_in_range(candidate: u8, start: u8, end: u8, flags: i64) -> bool { + let candidate = eval_fnmatch_fold(candidate, flags); + let start = eval_fnmatch_fold(start, flags); + let end = eval_fnmatch_fold(end, flags); + if start <= end { + candidate >= start && candidate <= end + } else { + candidate >= end && candidate <= start + } +} + +/// Reads an escaped literal token outside bracket classes. +fn eval_fnmatch_escaped_literal(pattern: &[u8], pattern_index: usize) -> (u8, usize) { + if pattern_index + 1 < pattern.len() { + (pattern[pattern_index + 1], pattern_index + 2) + } else { + (b'\\', pattern_index + 1) + } +} + +/// Returns whether one literal pattern byte matches the current filename byte. +fn eval_fnmatch_literal(filename: &[u8], flags: i64, filename_index: usize, literal: u8) -> bool { + filename_index < filename.len() + && eval_fnmatch_byte_eq(filename[filename_index], literal, flags) +} + +/// Returns whether a wildcard token may consume the current filename byte. +fn eval_fnmatch_wildcard_can_consume(filename: &[u8], flags: i64, filename_index: usize) -> bool { + if filename_index >= filename.len() { + return false; + } + if flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index] == b'/' { + return false; + } + if flags & EVAL_FNM_PERIOD != 0 + && eval_fnmatch_is_leading_period(filename, flags, filename_index) + { + return false; + } + true +} + +/// Returns whether the current byte is a leading period for `FNM_PERIOD`. +fn eval_fnmatch_is_leading_period(filename: &[u8], flags: i64, filename_index: usize) -> bool { + filename[filename_index] == b'.' + && (filename_index == 0 + || (flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index - 1] == b'/')) +} + +/// Compares bytes using ASCII case folding when `FNM_CASEFOLD` is present. +fn eval_fnmatch_byte_eq(left: u8, right: u8, flags: i64) -> bool { + eval_fnmatch_fold(left, flags) == eval_fnmatch_fold(right, flags) +} + +/// Applies eval fnmatch's ASCII case folding. +fn eval_fnmatch_fold(byte: u8, flags: i64) -> u8 { + if flags & EVAL_FNM_CASEFOLD != 0 { + byte.to_ascii_lowercase() + } else { + byte + } +} + +/// Evaluates PHP `preg_match()` over eval expressions. +pub(super) fn eval_builtin_preg_match( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, None, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether one regex matches the subject string. +fn eval_preg_match_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + values.int(i64::from(regex.is_match(&subject))) +} + +/// Returns the match flag plus PHP `$matches` capture array for one regex search. +fn eval_preg_match_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let flags = eval_preg_match_flags(flags, values)?; + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + if let Some(captures) = regex.captures(&subject) { + let matches = eval_preg_capture_array( + &subject, + Some(&captures), + offset_capture, + unmatched_as_null, + values, + )?; + let matched = values.int(1)?; + return Ok((matched, matches)); + } + let matches = + eval_preg_capture_array(&subject, None, offset_capture, unmatched_as_null, values)?; + let matched = values.int(0)?; + Ok((matched, matches)) +} + +/// Returns supported `preg_match()` flags. +fn eval_preg_match_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_OFFSET_CAPTURE | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Evaluates PHP `preg_match_all()` over eval expressions. +pub(super) fn eval_builtin_preg_match_all( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_all_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, None, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Counts all non-overlapping regex matches in one subject string. +fn eval_preg_match_all_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let count = regex.captures_iter(&subject).count(); + values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Returns the match count plus PHP's default `PREG_PATTERN_ORDER` `$matches` array. +fn eval_preg_match_all_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let capture_count = regex.captures_len(); + let subject = values.string_bytes(subject)?; + let captures: Vec> = regex.captures_iter(&subject).collect(); + let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let flags = eval_preg_match_all_flags(flags, values)?; + let matches = if flags & EVAL_PREG_SET_ORDER != 0 { + eval_preg_match_all_set_order_array(&subject, &captures, capture_count, flags, values)? + } else { + eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, flags, values)? + }; + Ok((count, matches)) +} + +/// Returns supported `preg_match_all()` flags. +fn eval_preg_match_all_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(EVAL_PREG_PATTERN_ORDER); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_PATTERN_ORDER + | EVAL_PREG_SET_ORDER + | EVAL_PREG_OFFSET_CAPTURE + | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Builds PHP's default `preg_match_all()` pattern-order capture matrix. +fn eval_preg_match_all_pattern_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + let mut outer = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let mut row = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let key = + values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; + row = values.array_set(row, key, value)?; + } + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + +/// Builds PHP's `preg_match_all(..., PREG_SET_ORDER)` match-order capture matrix. +fn eval_preg_match_all_set_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + let mut outer = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let mut row = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; + row = values.array_set(row, key, value)?; + } + let key = values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + +/// Evaluates PHP `preg_replace()` over eval expressions. +pub(super) fn eval_builtin_preg_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, replacement, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let replacement = eval_expr(replacement, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_result(pattern, replacement, subject, values) +} + +/// Replaces every regex match with a PHP-style backreference-expanded replacement. +fn eval_preg_replace_result( + pattern: RuntimeCellHandle, + replacement: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let replacement = values.string_bytes(replacement)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + +/// Evaluates PHP `preg_replace_callback()` over eval expressions. +pub(super) fn eval_builtin_preg_replace_callback( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, callback, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_callback_result(pattern, callback, subject, context, values) +} + +/// Replaces every regex match by invoking an eval-supported callback with `$matches`. +fn eval_preg_replace_callback_result( + pattern: RuntimeCellHandle, + callback: RuntimeCellHandle, + subject: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let callback = eval_callable_name(callback, values)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; + let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; + let callback_result = values.cast_string(callback_result)?; + let callback_bytes = values.string_bytes(callback_result)?; + result.extend_from_slice(&callback_bytes); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + +/// Evaluates PHP `preg_split()` over eval expressions. +pub(super) fn eval_builtin_preg_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_split_result(pattern, subject, None, None, values) + } + [pattern, subject, limit] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), None, values) + } + [pattern, subject, limit, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits a subject string with eval-supported `preg_split()` flags. +fn eval_preg_split_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + limit: Option, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let limit = eval_preg_split_limit(limit, values)?; + let flags = eval_preg_split_flags(flags, values)?; + let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; + let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; + let offset_capture = flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0; + let mut pieces = Vec::::new(); + let mut cursor = 0; + + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + if eval_preg_split_reached_limit(&pieces, limit) { + break; + } + eval_preg_split_push_piece( + &mut pieces, + &subject[cursor..matched.start()], + cursor, + no_empty, + ); + if capture_delimiters { + eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); + } + cursor = matched.end(); + } + eval_preg_split_push_piece(&mut pieces, &subject[cursor..], cursor, no_empty); + + let mut result = values.array_new(pieces.len())?; + for (index, piece) in pieces.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_split_piece_value(piece, offset_capture, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Compiles one eval PCRE-style delimited pattern into a Rust regex. +fn eval_preg_regex( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; + let body = String::from_utf8(body).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut builder = RegexBuilder::new(&body); + builder + .case_insensitive(modifiers.case_insensitive) + .multi_line(modifiers.multi_line) + .dot_matches_new_line(modifiers.dot_matches_new_line) + .swap_greed(modifiers.swap_greed); + builder.build().map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Regex modifiers supported by eval `preg_*` pattern stripping. +#[derive(Default)] +struct EvalPregModifiers { + case_insensitive: bool, + multi_line: bool, + dot_matches_new_line: bool, + swap_greed: bool, +} + +/// One `preg_split()` output segment plus its byte offset in the subject. +struct EvalPregSplitPiece { + bytes: Vec, + offset: usize, +} + +/// Splits a PHP delimited regex into body bytes and supported modifiers. +fn eval_preg_pattern_parts(pattern: &[u8]) -> Result<(Vec, EvalPregModifiers), EvalStatus> { + if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() { + return Err(EvalStatus::RuntimeFatal); + } + let delimiter = pattern[0]; + if delimiter == b'\\' { + return Err(EvalStatus::RuntimeFatal); + } + let closing = eval_preg_closing_delimiter(delimiter); + let close_index = + eval_preg_find_closing_delimiter(pattern, closing).ok_or(EvalStatus::RuntimeFatal)?; + let body = eval_preg_unescape_delimiter(&pattern[1..close_index], delimiter, closing); + let modifiers = eval_preg_modifiers(&pattern[close_index + 1..])?; + Ok((body, modifiers)) +} + +/// Returns the closing regex delimiter for PHP's paired delimiter forms. +fn eval_preg_closing_delimiter(delimiter: u8) -> u8 { + match delimiter { + b'(' => b')', + b'[' => b']', + b'{' => b'}', + b'<' => b'>', + _ => delimiter, + } +} + +/// Finds the first unescaped closing regex delimiter. +fn eval_preg_find_closing_delimiter(pattern: &[u8], closing: u8) -> Option { + let mut escaped = false; + for (index, byte) in pattern.iter().copied().enumerate().skip(1) { + if escaped { + escaped = false; + continue; + } + if byte == b'\\' { + escaped = true; + continue; + } + if byte == closing { + return Some(index); + } + } + None +} + +/// Removes escapes that only protect the PHP regex delimiter from pattern stripping. +fn eval_preg_unescape_delimiter(body: &[u8], delimiter: u8, closing: u8) -> Vec { + let mut result = Vec::with_capacity(body.len()); + let mut index = 0; + while index < body.len() { + if body[index] == b'\\' + && index + 1 < body.len() + && matches!(body[index + 1], byte if byte == delimiter || byte == closing) + { + result.push(body[index + 1]); + index += 2; + } else { + result.push(body[index]); + index += 1; + } + } + result +} + +/// Parses eval-supported PHP regex modifiers. +fn eval_preg_modifiers(modifiers: &[u8]) -> Result { + let mut parsed = EvalPregModifiers::default(); + for modifier in modifiers { + match *modifier { + b'i' => parsed.case_insensitive = true, + b'm' => parsed.multi_line = true, + b's' => parsed.dot_matches_new_line = true, + b'U' => parsed.swap_greed = true, + b'u' => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(parsed) +} + +/// Builds PHP's indexed `$matches` capture array for one regex result. +fn eval_preg_capture_array( + subject: &[u8], + captures: Option<&Captures<'_>>, + offset_capture: bool, + unmatched_as_null: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = captures.map_or(0, |captures| { + eval_preg_visible_capture_len(captures, unmatched_as_null) + }); + let mut result = values.array_new(len)?; + if let Some(captures) = captures { + for index in 0..len { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + captures, + index, + offset_capture, + unmatched_as_null, + values, + )?; + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Returns the capture count PHP should expose, dropping trailing unmatched groups. +fn eval_preg_visible_capture_len(captures: &Captures<'_>, unmatched_as_null: bool) -> usize { + if unmatched_as_null { + return captures.len(); + } + let mut len = captures.len(); + while len > 1 && captures.get(len - 1).is_none() { + len -= 1; + } + len +} + +/// Returns one captured byte range from the original subject. +fn eval_preg_capture_bytes<'a>( + subject: &'a [u8], + captures: &Captures<'_>, + index: usize, +) -> Option<&'a [u8]> { + captures + .get(index) + .map(|matched| &subject[matched.start()..matched.end()]) +} + +/// Builds one capture entry as either a string or PHP's `[string, byte_offset]` pair. +fn eval_preg_capture_value( + subject: &[u8], + captures: &Captures<'_>, + index: usize, + offset_capture: bool, + unmatched_as_null: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let matched = captures.get(index); + let value = if matched.is_none() && unmatched_as_null { + values.null()? + } else { + let bytes = matched.as_ref().map_or(b"".as_slice(), |matched| { + &subject[matched.start()..matched.end()] + }); + values.string_bytes_value(bytes)? + }; + if !offset_capture { + return Ok(value); + } + + let offset = matched.map_or(Ok(-1_i64), |matched| { + i64::try_from(matched.start()).map_err(|_| EvalStatus::RuntimeFatal) + })?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} + +/// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. +fn eval_preg_expand_replacement( + replacement: &[u8], + subject: &[u8], + captures: &Captures<'_>, + result: &mut Vec, +) { + let mut index = 0; + while index < replacement.len() { + match replacement[index] { + b'$' => { + if let Some((capture_index, next_index)) = + eval_preg_replacement_capture_index(replacement, index + 1) + { + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } else { + result.push(replacement[index]); + index += 1; + } + } + b'\\' if index + 1 < replacement.len() && replacement[index + 1].is_ascii_digit() => { + let (capture_index, next_index) = + eval_preg_decimal_capture_index(replacement, index + 1); + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } + byte => { + result.push(byte); + index += 1; + } + } + } +} + +/// Parses a dollar-style replacement capture reference. +fn eval_preg_replacement_capture_index(bytes: &[u8], index: usize) -> Option<(usize, usize)> { + if bytes.get(index).copied() == Some(b'{') { + let mut cursor = index + 1; + let start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + if cursor == start || bytes.get(cursor).copied() != Some(b'}') { + return None; + } + let capture = eval_preg_decimal_bytes_to_usize(&bytes[start..cursor])?; + return Some((capture, cursor + 1)); + } + if bytes.get(index).is_some_and(u8::is_ascii_digit) { + let (capture, next) = eval_preg_decimal_capture_index(bytes, index); + return Some((capture, next)); + } + None +} + +/// Parses a one- or two-digit replacement capture reference. +fn eval_preg_decimal_capture_index(bytes: &[u8], index: usize) -> (usize, usize) { + let mut cursor = index; + let end = usize::min(bytes.len(), index + 2); + while cursor < end && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + ( + eval_preg_decimal_bytes_to_usize(&bytes[index..cursor]).unwrap_or(0), + cursor, + ) +} + +/// Converts ASCII decimal bytes into a `usize` capture index. +fn eval_preg_decimal_bytes_to_usize(bytes: &[u8]) -> Option { + let mut value = 0usize; + for byte in bytes { + value = value.checked_mul(10)?; + value = value.checked_add(usize::from(byte - b'0'))?; + } + Some(value) +} + +/// Returns the PHP `preg_split()` limit, treating zero as unlimited. +fn eval_preg_split_limit( + limit: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(limit) = limit else { + return Ok(None); + }; + let limit = eval_int_value(limit, values)?; + if limit <= 0 { + return Ok(None); + } + usize::try_from(limit) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns supported `preg_split()` flags. +fn eval_preg_split_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = + EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE | EVAL_PREG_SPLIT_OFFSET_CAPTURE; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Returns whether `preg_split()` should stop splitting and emit the remaining subject. +fn eval_preg_split_reached_limit(pieces: &[EvalPregSplitPiece], limit: Option) -> bool { + matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) +} + +/// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. +fn eval_preg_split_push_piece( + pieces: &mut Vec, + piece: &[u8], + offset: usize, + no_empty: bool, +) { + if no_empty && piece.is_empty() { + return; + } + pieces.push(EvalPregSplitPiece { + bytes: piece.to_vec(), + offset, + }); +} + +/// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. +fn eval_preg_split_push_captures( + pieces: &mut Vec, + subject: &[u8], + captures: &Captures<'_>, + no_empty: bool, +) { + for index in 1..captures.len() { + if let Some(matched) = captures.get(index) { + eval_preg_split_push_piece( + pieces, + &subject[matched.start()..matched.end()], + matched.start(), + no_empty, + ); + } + } +} + +/// Converts one split segment to a string or PHP `[string, byte_offset]` pair. +fn eval_preg_split_piece_value( + piece: &EvalPregSplitPiece, + offset_capture: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.string_bytes_value(&piece.bytes)?; + if !offset_capture { + return Ok(value); + } + + let offset = i64::try_from(piece.offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} + +/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. +pub(super) fn eval_builtin_gethostbyaddr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_gethostbyaddr_result(ip, values) +} + +/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. +fn eval_gethostbyaddr_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip_bytes = values.string_bytes(ip)?; + let ip_text = String::from_utf8_lossy(&ip_bytes); + let Ok(ipv4) = ip_text.parse::() else { + return values.bool_value(false); + }; + let octets = ipv4.octets(); + let resolved = unsafe { + // libc reads the stack-owned IPv4 octets during this call and returns + // static resolver storage, which is copied before the next resolver call. + let host = libc_gethostbyaddr( + octets.as_ptr().cast::(), + octets.len() as libc::socklen_t, + libc::AF_INET, + ); + if host.is_null() || (*host).h_name.is_null() { + None + } else { + Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) + } + }; + match resolved { + Some(name) if !name.is_empty() => values.string_bytes_value(&name), + _ => values.string(ip_text.as_ref()), + } +} + +/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. +pub(super) fn eval_builtin_gethostbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hostname] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hostname = eval_expr(hostname, context, scope, values)?; + eval_gethostbyname_result(hostname, values) +} + +/// Resolves one host name to an IPv4 string, or returns the original input on failure. +fn eval_gethostbyname_result( + hostname: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let hostname = values.string_bytes(hostname)?; + let hostname = String::from_utf8_lossy(&hostname); + if hostname.parse::().is_ok() { + return values.string(hostname.as_ref()); + } + let resolved = (hostname.as_ref(), 0_u16) + .to_socket_addrs() + .ok() + .and_then(|addrs| { + addrs + .filter_map(|addr| match addr.ip() { + std::net::IpAddr::V4(ip) => Some(ip.to_string()), + std::net::IpAddr::V6(_) => None, + }) + .next() + }); + values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) +} + +/// Evaluates PHP `gethostname()` over one eval expression. +pub(super) fn eval_builtin_gethostname( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + let [] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostname_result(values) +} + +/// Reads the current host name through libc and returns an empty string on failure. +fn eval_gethostname_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let mut buffer = [0 as libc::c_char; 256]; + let status = unsafe { + // libc writes at most buffer.len() bytes into this stack buffer. + libc::gethostname(buffer.as_mut_ptr(), buffer.len()) + }; + if status != 0 { + return values.string(""); + } + let length = buffer + .iter() + .position(|byte| *byte == 0) + .unwrap_or(buffer.len()); + let hostname = buffer[..length] + .iter() + .map(|byte| *byte as u8) + .collect::>(); + values.string_bytes_value(&hostname) +} + +/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. +pub(super) fn eval_builtin_getprotobyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobyname_result(protocol, values) +} + +/// Looks up an IP protocol number by name or alias. +fn eval_getprotobyname_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy scalar fields before another lookup. + libc_getprotobyname(protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let number = unsafe { (*entry).p_proto }; + values.int(i64::from(number)) +} + +/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. +pub(super) fn eval_builtin_getprotobynumber( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobynumber_result(protocol, values) +} + +/// Looks up an IP protocol name by numeric protocol id. +fn eval_getprotobynumber_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = eval_int_value(protocol, values)?; + let Ok(protocol) = libc::c_int::try_from(protocol) else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy the name before another lookup. + libc_getprotobynumber(protocol) + }; + eval_protoent_name_or_false(entry, values) +} + +/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. +pub(super) fn eval_builtin_getservbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [service, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let service = eval_expr(service, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyname_result(service, protocol, values) +} + +/// Looks up an internet service port by service name and protocol. +fn eval_getservbyname_result( + service: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(service) = eval_lowercase_c_string(service, values)? else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global servent; copy scalar fields before another lookup. + libc_getservbyname(service.as_ptr(), protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let port = unsafe { u16::from_be((*entry).s_port as u16) }; + values.int(i64::from(port)) +} + +/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. +pub(super) fn eval_builtin_getservbyport( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [port, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let port = eval_expr(port, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyport_result(port, protocol, values) +} + +/// Looks up an internet service name by port and protocol. +fn eval_getservbyport_result( + port: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let port = eval_int_value(port, values)?; + let Ok(port) = u16::try_from(port) else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let network_port = port.to_be() as libc::c_int; + let entry = unsafe { + // libc returns a process-global servent; copy the name before another lookup. + libc_getservbyport(network_port, protocol.as_ptr()) + }; + eval_servent_name_or_false(entry, values) +} + +/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. +fn eval_lowercase_c_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let bytes = values.string_bytes(value)?; + let bytes = bytes + .into_iter() + .map(|byte| byte.to_ascii_lowercase()) + .collect::>(); + Ok(CString::new(bytes).ok()) +} + +/// Copies a protoent canonical name into a PHP string or returns PHP false. +fn eval_protoent_name_or_false( + entry: *mut libc::protoent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).p_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} + +/// Copies a servent canonical name into a PHP string or returns PHP false. +fn eval_servent_name_or_false( + entry: *mut libc::servent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).s_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} + +/// Evaluates PHP `long2ip($ip)` over one eval expression. +pub(super) fn eval_builtin_long2ip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_long2ip_result(ip, values) +} + +/// Formats one 32-bit IPv4 integer as a dotted-quad string. +fn eval_long2ip_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip = eval_int_value(ip, values)? as u32; + values.string(&eval_format_ipv4(ip)) +} + +/// Evaluates PHP `ip2long($ip)` over one eval expression. +pub(super) fn eval_builtin_ip2long( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_ip2long_result(ip, values) +} + +/// Parses a dotted-quad IPv4 string into an integer or PHP false. +fn eval_ip2long_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + match eval_parse_ipv4(&bytes) { + Some(ip) => values.int(i64::from(ip)), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `inet_pton($ip)` over one eval expression. +pub(super) fn eval_builtin_inet_pton( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_inet_pton_result(ip, values) +} + +/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. +fn eval_inet_pton_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + let Some(ip) = eval_parse_ipv4(&bytes) else { + return values.bool_value(false); + }; + values.string_bytes_value(&ip.to_be_bytes()) +} + +/// Evaluates PHP `inet_ntop($binary)` over one eval expression. +pub(super) fn eval_builtin_inet_ntop( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [binary] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let binary = eval_expr(binary, context, scope, values)?; + eval_inet_ntop_result(binary, values) +} + +/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. +fn eval_inet_ntop_result( + binary: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(binary)?; + let [a, b, c, d] = bytes.as_slice() else { + return values.bool_value(false); + }; + let ip = u32::from_be_bytes([*a, *b, *c, *d]); + values.string(&eval_format_ipv4(ip)) +} + +/// Parses exactly four decimal IPv4 octets separated by dots. +fn eval_parse_ipv4(bytes: &[u8]) -> Option { + let mut octets = [0_u8; 4]; + let mut position = 0_usize; + let mut index = 0_usize; + + while index < 4 { + if position >= bytes.len() { + return None; + } + let start = position; + let mut value = 0_u16; + while position < bytes.len() && bytes[position].is_ascii_digit() { + value = value + .checked_mul(10)? + .checked_add(u16::from(bytes[position] - b'0'))?; + position += 1; + if position - start > 3 || value > 255 { + return None; + } + } + if position == start { + return None; + } + octets[index] = value as u8; + index += 1; + if index == 4 { + return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); + } + if bytes.get(position).copied() != Some(b'.') { + return None; + } + position += 1; + } + None +} + +/// Formats one packed IPv4 integer into dotted-quad text. +fn eval_format_ipv4(ip: u32) -> String { + let [a, b, c, d] = ip.to_be_bytes(); + format!("{}.{}.{}.{}", a, b, c, d) +} + +/// Evaluates PHP `getenv($name)` over one eval expression. +pub(super) fn eval_builtin_getenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + eval_getenv_result(name, values) +} + +/// Reads one environment variable and returns an empty string when it is unset. +fn eval_getenv_result( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8_lossy(&name); + let value = std::env::var_os(name.as_ref()) + .map(|value| value.to_string_lossy().into_owned()) + .unwrap_or_default(); + values.string(&value) +} + +/// Evaluates PHP `putenv($assignment)` over one eval expression. +pub(super) fn eval_builtin_putenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [assignment] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let assignment = eval_expr(assignment, context, scope, values)?; + eval_putenv_result(assignment, values) +} + +/// Applies one `putenv()` assignment to the host environment. +fn eval_putenv_result( + assignment: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let assignment = values.string_bytes(assignment)?; + if let Some(separator) = assignment.iter().position(|byte| *byte == b'=') { + let name = String::from_utf8_lossy(&assignment[..separator]); + let value = String::from_utf8_lossy(&assignment[separator + 1..]); + std::env::set_var(name.as_ref(), value.as_ref()); + } else { + let name = String::from_utf8_lossy(&assignment); + std::env::remove_var(name.as_ref()); + } + values.bool_value(true) +} + +/// Evaluates PHP `sys_get_temp_dir()` with no arguments. +pub(super) fn eval_builtin_sys_get_temp_dir( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values) +} + +/// Returns the same temporary directory literal as the native static builtin. +fn eval_sys_get_temp_dir_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string("/tmp") +} + +/// Evaluates PHP `realpath_cache_get()` with no arguments. +pub(super) fn eval_builtin_realpath_cache_get( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values) +} + +/// Returns elephc's intentionally empty realpath-cache view. +fn eval_realpath_cache_get_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.array_new(0) +} + +/// Evaluates PHP `realpath_cache_size()` with no arguments. +pub(super) fn eval_builtin_realpath_cache_size( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values) +} + +/// Returns zero because elephc does not maintain a runtime realpath cache. +fn eval_realpath_cache_size_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(0) +} + +/// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. +fn eval_crc32_bytes(bytes: &[u8]) -> u32 { + let mut crc = 0xffff_ffff_u32; + for byte in bytes { + crc ^= u32::from(*byte); + for _ in 0..8 { + let mask = 0_u32.wrapping_sub(crc & 1); + crc = (crc >> 1) ^ (0xedb8_8320 & mask); + } + } + !crc +} + +/// Casts one eval value to PHP int and returns the scalar payload. +pub(super) fn eval_int_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_int(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP's `bin2hex(...)` over one eval expression. +pub(super) fn eval_builtin_bin2hex( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_bin2hex_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. +fn eval_bin2hex_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.string(&eval_lower_hex_bytes(&bytes)) +} + +/// Converts bytes to lowercase hexadecimal text. +fn eval_lower_hex_bytes(bytes: &[u8]) -> String { + let mut output = String::with_capacity(bytes.len() * 2); + const HEX: &[u8; 16] = b"0123456789abcdef"; + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} + +/// Evaluates PHP's `hex2bin(...)` over one eval expression. +pub(super) fn eval_builtin_hex2bin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_hex2bin_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. +fn eval_hex2bin_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + if bytes.len() % 2 != 0 { + values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; + return values.bool_value(false); + } + let mut output = Vec::with_capacity(bytes.len() / 2); + for pair in bytes.chunks_exact(2) { + let Some(high) = eval_hex_nibble(pair[0]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + let Some(low) = eval_hex_nibble(pair[1]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + output.push((high << 4) | low); + } + values.string_bytes_value(&output) +} + +/// Returns the four-bit value for one hexadecimal byte. +fn eval_hex_nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} + +/// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. +pub(super) fn eval_builtin_slashes( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_slashes_result(name, value, values) +} + +/// Applies PHP byte-string escaping or unescaping for addslashes/stripslashes. +fn eval_slashes_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "addslashes" => eval_addslashes_result(value, values), + "stripslashes" => eval_stripslashes_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. +fn eval_addslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + 0 => output.extend_from_slice(b"\\0"), + b'\'' | b'"' | b'\\' => { + output.push(b'\\'); + output.push(byte); + } + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Removes backslash quoting using PHP `stripslashes()` byte semantics. +fn eval_stripslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'\\' { + index += 1; + if let Some(byte) = bytes.get(index).copied() { + output.push(if byte == b'0' { 0 } else { byte }); + index += 1; + } + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP's `base64_encode(...)` over one eval expression. +pub(super) fn eval_builtin_base64_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_encode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns Base64 text. +fn eval_base64_encode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for chunk in bytes.chunks(3) { + let first = chunk[0]; + let second = chunk.get(1).copied().unwrap_or(0); + let third = chunk.get(2).copied().unwrap_or(0); + output.push(ALPHABET[(first >> 2) as usize] as char); + output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); + if chunk.len() > 1 { + output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); + } else { + output.push('='); + } + if chunk.len() > 2 { + output.push(ALPHABET[(third & 0x3f) as usize] as char); + } else { + output.push('='); + } + } + values.string(&output) +} + +/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. +pub(super) fn eval_builtin_base64_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_decode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes Base64 bytes. +fn eval_base64_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(value)?; + let mut output = Vec::with_capacity((input.len() / 4) * 3); + let mut quartet = Vec::with_capacity(4); + for byte in input { + if byte.is_ascii_whitespace() { + continue; + } + if byte == b'=' { + quartet.push(None); + } else if let Some(value) = eval_base64_decode_sextet(byte) { + quartet.push(Some(value)); + } else { + continue; + } + if quartet.len() == 4 { + eval_push_base64_decoded_quartet(&quartet, &mut output); + quartet.clear(); + } + } + if !quartet.is_empty() { + while quartet.len() < 4 { + quartet.push(None); + } + eval_push_base64_decoded_quartet(&quartet, &mut output); + } + values.string_bytes_value(&output) +} + +/// Returns the six-bit Base64 value for one encoded byte. +fn eval_base64_decode_sextet(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(byte - b'A'), + b'a'..=b'z' => Some(byte - b'a' + 26), + b'0'..=b'9' => Some(byte - b'0' + 52), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } +} + +/// Appends decoded bytes for one padded or unpadded Base64 quartet. +fn eval_push_base64_decoded_quartet(quartet: &[Option], output: &mut Vec) { + let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { + return; + }; + output.push((first << 2) | (second >> 4)); + let Some(third) = quartet[2] else { + return; + }; + output.push(((second & 0x0f) << 4) | (third >> 2)); + let Some(fourth) = quartet[3] else { + return; + }; + output.push(((third & 0x03) << 6) | fourth); +} + +/// Evaluates PHP one-argument floating-point math builtins over one eval expression. +pub(super) fn eval_builtin_float_unary( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_float_unary_result(name, value, values) +} + +/// Dispatches an evaluated value through the matching PHP floating-point unary math function. +fn eval_float_unary_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let result = match name { + "acos" => value.acos(), + "asin" => value.asin(), + "atan" => value.atan(), + "cos" => value.cos(), + "cosh" => value.cosh(), + "deg2rad" => value.to_radians(), + "exp" => value.exp(), + "log2" => value.log2(), + "log10" => value.log10(), + "rad2deg" => value.to_degrees(), + "sin" => value.sin(), + "sinh" => value.sinh(), + "tan" => value.tan(), + "tanh" => value.tanh(), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.float(result) +} + +/// Evaluates PHP two-argument floating-point math builtins over eval expressions. +pub(super) fn eval_builtin_float_pair( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_float_pair_result(name, left, right, values) +} + +/// Dispatches an evaluated pair through PHP `atan2()` or `hypot()`. +fn eval_float_pair_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_float_value(left, values)?; + let right = eval_float_value(right, values)?; + let result = match name { + "atan2" => left.atan2(right), + "hypot" => left.hypot(right), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.float(result) +} + +/// Evaluates PHP `log($num, $base = e)` over eval expressions. +pub(super) fn eval_builtin_log( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [num] => { + let num = eval_expr(num, context, scope, values)?; + eval_log_result(num, None, values) + } + [num, base] => { + let num = eval_expr(num, context, scope, values)?; + let base = eval_expr(base, context, scope, values)?; + eval_log_result(num, Some(base), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `log()` from already evaluated arguments. +fn eval_log_result( + num: RuntimeCellHandle, + base: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + let result = match base { + Some(base) => num.log(eval_float_value(base, values)?), + None => num.ln(), + }; + values.float(result) +} + +/// Evaluates PHP `intdiv(...)` over two eval expressions. +pub(super) fn eval_builtin_intdiv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_intdiv_result(left, right, values) +} + +/// Computes PHP integer division from already evaluated arguments. +fn eval_intdiv_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_int_value(left, values)?; + let right = eval_int_value(right, values)?; + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; + values.int(result) +} + +/// Evaluates PHP floating-point binary math builtins over two eval expressions. +pub(super) fn eval_builtin_float_binary( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_float_binary_result(name, left, right, values) +} + +/// Dispatches an evaluated pair through the matching PHP float math hook. +fn eval_float_binary_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "fdiv" => values.fdiv(left, right), + "fmod" => values.fmod(left, right), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP `clamp($value, $min, $max)` over three eval expressions. +pub(super) fn eval_builtin_clamp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_clamp_result(value, min, max, values) +} + +/// Selects the inclusive clamp result after validating bound order and NaN bounds. +fn eval_clamp_result( + value: RuntimeCellHandle, + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; + if values.truthy(invalid_bounds)? { + return Err(EvalStatus::RuntimeFatal); + } + let above_max = values.compare(EvalBinOp::Gt, value, max)?; + if values.truthy(above_max)? { + return Ok(max); + } + let below_min = values.compare(EvalBinOp::Lt, value, min)?; + if values.truthy(below_min)? { + return Ok(min); + } + Ok(value) +} + +/// Returns whether a clamp bound is a floating-point NaN value. +fn eval_clamp_bound_is_nan( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_FLOAT { + return Ok(false); + } + Ok(eval_float_value(value, values)?.is_nan()) +} + +/// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. +pub(super) fn eval_builtin_min_max( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_min_max_result(name, &evaluated_args, values) +} + +/// Selects the smallest or largest evaluated cell using runtime comparison hooks. +fn eval_min_max_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((&first, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let op = match name { + "min" => EvalBinOp::Lt, + "max" => EvalBinOp::Gt, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let mut selected = first; + for candidate in rest { + let better = values.compare(op, *candidate, selected)?; + if values.truthy(better)? { + selected = *candidate; + } + } + Ok(selected) +} + +/// Evaluates PHP scalar cast builtins over one eval expression. +pub(super) fn eval_builtin_cast( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_cast_result(name, value, values) +} + +/// Dispatches an already evaluated value through the matching PHP cast hook. +fn eval_cast_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "intval" => values.cast_int(value), + "floatval" => values.cast_float(value), + "strval" => values.cast_string(value), + "boolval" => values.cast_bool(value), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP's `gettype(...)` over one eval expression. +pub(super) fn eval_builtin_gettype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_gettype_result(value, values) +} + +/// Converts one boxed runtime tag into PHP's `gettype()` spelling. +fn eval_gettype_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.string(eval_gettype_name(tag)) +} + +/// Evaluates PHP's `get_class(...)` over one eval object expression. +pub(super) fn eval_builtin_get_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_get_class_result(object, context, values) +} + +/// Resolves the PHP-visible class name for one already materialized object cell. +fn eval_get_class_result( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return values.string(class.name().trim_start_matches('\\')); + } + } + values.object_class_name(object) +} + +/// Evaluates PHP's SPL object identity builtins over one eval object expression. +pub(super) fn eval_builtin_spl_object_identity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_spl_object_identity_result(name, object, values) +} + +/// Returns the unboxed object-payload identity in the native SPL builtin spelling. +fn eval_spl_object_identity_result( + name: &str, + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)? as i64; + match name { + "spl_object_id" => values.int(identity), + "spl_object_hash" => values.string(&identity.to_string()), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. +pub(super) fn eval_builtin_get_parent_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object_or_class] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object_or_class = eval_expr(object_or_class, context, scope, values)?; + eval_get_parent_class_result(object_or_class, values) +} + +/// Resolves the PHP-visible parent class name for one object or class-name cell. +fn eval_get_parent_class_result( + object_or_class: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.parent_class_name(object_or_class) +} + +/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. +pub(super) fn eval_builtin_resource_introspection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [resource] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let resource = eval_expr(resource, context, scope, values)?; + eval_resource_introspection_result(name, resource, values) +} + +/// Evaluates a materialized resource introspection builtin argument. +fn eval_resource_introspection_result( + name: &str, + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + match name { + "get_resource_type" => values.string("stream"), + "get_resource_id" => values.cast_int(resource), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Returns the PHP-visible type name for a concrete eval runtime tag. +fn eval_gettype_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "integer", + EVAL_TAG_FLOAT => "double", + EVAL_TAG_STRING => "string", + EVAL_TAG_BOOL => "boolean", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_OBJECT => "object", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_NULL => "NULL", + _ => "NULL", + } +} + +/// Evaluates PHP scalar/container type predicate builtins over one eval expression. +pub(super) fn eval_builtin_type_predicate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_type_predicate_result(name, value, values) +} + +/// Converts a concrete runtime tag into a PHP `is_*` predicate result. +fn eval_type_predicate_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + let result = match name { + "is_int" | "is_integer" | "is_long" => tag == EVAL_TAG_INT, + "is_float" | "is_double" | "is_real" => tag == EVAL_TAG_FLOAT, + "is_string" => tag == EVAL_TAG_STRING, + "is_bool" => tag == EVAL_TAG_BOOL, + "is_null" => tag == EVAL_TAG_NULL, + "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_object" => tag == EVAL_TAG_OBJECT, + "is_resource" => tag == EVAL_TAG_RESOURCE, + "is_nan" => eval_float_value(value, values)?.is_nan(), + "is_infinite" => eval_float_value(value, values)?.is_infinite(), + "is_finite" => eval_float_value(value, values)?.is_finite(), + "is_numeric" => { + tag == EVAL_TAG_INT + || tag == EVAL_TAG_FLOAT + || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)) + } + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(result) +} + +/// Matches the static backend's legacy ASCII numeric-string scan. +fn eval_is_numeric_string(bytes: &[u8]) -> bool { + if bytes.is_empty() { + return false; + } + + let mut index = 0; + let mut consumed_digits = 0; + if bytes[index] == b'-' { + index += 1; + if index >= bytes.len() { + return false; + } + } + + while index < bytes.len() { + if bytes[index] == b'.' { + index += 1; + break; + } + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + while index < bytes.len() { + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + consumed_digits > 0 +} + +/// Evaluates PHP's `hash_equals(...)` over two eval expressions. +pub(super) fn eval_builtin_hash_equals( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [known, user] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let known = eval_expr(known, context, scope, values)?; + let user = eval_expr(user, context, scope, values)?; + eval_hash_equals_result(known, user, values) +} + +/// Compares two converted strings with PHP `hash_equals()` semantics. +fn eval_hash_equals_result( + known: RuntimeCellHandle, + user: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let known = values.string_bytes(known)?; + let user = values.string_bytes(user)?; + if known.len() != user.len() { + return values.bool_value(false); + } + let mut diff = 0u8; + for (known, user) in known.iter().zip(user.iter()) { + diff |= known ^ user; + } + values.bool_value(diff == 0) +} + +/// Evaluates PHP string comparison builtins over two eval expressions. +pub(super) fn eval_builtin_string_compare( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_string_compare_result(name, left, right, values) +} + +/// Compares two converted strings and returns -1, 0, or 1. +fn eval_string_compare_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut left = values.string_bytes(left)?; + let mut right = values.string_bytes(right)?; + match name { + "strcmp" => {} + "strcasecmp" => { + left.make_ascii_lowercase(); + right.make_ascii_lowercase(); + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let result = match left.cmp(&right) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + }; + values.int(result) +} + +/// Evaluates PHP's byte-string search predicates over two eval expressions. +pub(super) fn eval_builtin_string_search( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_search_result(name, haystack, needle, values) +} + +/// Checks one converted haystack for one converted needle using PHP byte-string semantics. +fn eval_string_search_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let matched = match name { + "str_contains" => { + needle.is_empty() + || haystack + .windows(needle.len()) + .any(|window| window == needle) + } + "str_starts_with" => haystack.starts_with(&needle), + "str_ends_with" => haystack.ends_with(&needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(matched) +} + +/// Evaluates PHP byte-string position builtins over two eval expressions. +pub(super) fn eval_builtin_string_position( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_position_result(name, haystack, needle, values) +} + +/// Returns the first or last byte offset of a converted needle, or PHP `false`. +fn eval_string_position_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = match name { + "strpos" if needle.is_empty() => Some(0), + "strpos" => haystack + .windows(needle.len()) + .position(|window| window == needle), + "strrpos" if needle.is_empty() => Some(haystack.len()), + "strrpos" => haystack + .windows(needle.len()) + .rposition(|window| window == needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + match position { + Some(position) => { + let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(position) + } + None => values.bool_value(false), + } +} + +/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. +pub(super) fn eval_builtin_strstr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [haystack, needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_strstr_result(haystack, needle, false, values) + } + [haystack, needle, before_needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + let before_needle = eval_expr(before_needle, context, scope, values)?; + let before_needle = values.truthy(before_needle)?; + eval_strstr_result(haystack, needle, before_needle, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. +fn eval_strstr_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + before_needle: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = if needle.is_empty() { + Some(0) + } else { + eval_find_subslice(&haystack, &needle, 0) + }; + let Some(position) = position else { + return values.bool_value(false); + }; + let result = if before_needle { + &haystack[..position] + } else { + &haystack[position..] + }; + values.string_bytes_value(result) +} + +const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; + +/// Evaluates PHP trim-like string builtins over one eval expression and optional mask. +pub(super) fn eval_builtin_trim_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_trim_like_result(name, value, None, values) + } + [value, mask] => { + let value = eval_expr(value, context, scope, values)?; + let mask = eval_expr(mask, context, scope, values)?; + eval_trim_like_result(name, value, Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Trims one converted string using PHP's default mask or a caller-provided byte mask. +fn eval_trim_like_result( + name: &str, + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let explicit_mask; + let trim_mask = if let Some(mask) = mask { + explicit_mask = values.string_bytes(mask)?; + explicit_mask.as_slice() + } else { + PHP_DEFAULT_TRIM_MASK + }; + + let mut start = 0; + let mut end = bytes.len(); + if matches!(name, "trim" | "ltrim") { + while start < end && trim_mask.contains(&bytes[start]) { + start += 1; + } + } + if matches!(name, "trim" | "rtrim" | "chop") { + while end > start && trim_mask.contains(&bytes[end - 1]) { + end -= 1; + } + } + if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { + return Err(EvalStatus::UnsupportedConstruct); + } + + let value = + String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} + +/// Evaluates PHP ASCII case-conversion string builtins over one eval expression. +pub(super) fn eval_builtin_string_case( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_string_case_result(name, value, values) +} + +/// Converts one eval value through PHP string conversion and ASCII case mapping. +fn eval_string_case_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + match name { + "strtolower" => { + for byte in &mut bytes { + if byte.is_ascii_uppercase() { + *byte += b'a' - b'A'; + } + } + } + "strtoupper" => { + for byte in &mut bytes { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + } + } + "ucfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { + bytes[0] -= b'a' - b'A'; + } + } + "lcfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { + bytes[0] += b'a' - b'A'; + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} + +/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. +pub(super) fn eval_builtin_ucwords( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_ucwords_result(value, None, values) + } + [value, separators] => { + let value = eval_expr(value, context, scope, values)?; + let separators = eval_expr(separators, context, scope, values)?; + eval_ucwords_result(value, Some(separators), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. +fn eval_ucwords_result( + value: RuntimeCellHandle, + separators: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + let separators = match separators { + Some(separators) => values.string_bytes(separators)?, + None => b" \t\r\n\x0c\x0b".to_vec(), + }; + let mut word_start = true; + for byte in &mut bytes { + if separators.contains(byte) { + word_start = true; + } else if word_start { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + word_start = false; + } + } + values.string_bytes_value(&bytes) +} + +/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. +pub(super) fn eval_builtin_wordwrap( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_wordwrap_result(value, None, None, None, values) + } + [value, width] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + eval_wordwrap_result(value, Some(width), None, None, values) + } + [value, width, break_string] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), None, values) + } + [value, width, break_string, cut] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + let cut = eval_expr(cut, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Wraps a byte string at PHP word boundaries and preserves existing newlines. +fn eval_wordwrap_result( + value: RuntimeCellHandle, + width: Option, + break_string: Option, + cut: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let width = match width { + Some(width) => eval_int_value(width, values)?, + None => 75, + }; + let break_string = match break_string { + Some(break_string) => values.string_bytes(break_string)?, + None => b"\n".to_vec(), + }; + if break_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let cut = match cut { + Some(cut) => values.truthy(cut)?, + None => false, + }; + if width == 0 && cut { + return Err(EvalStatus::RuntimeFatal); + } + if bytes.is_empty() { + return values.string_bytes_value(&bytes); + } + let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); + values.string_bytes_value(&output) +} + +/// Applies the core PHP word-wrap scan over already converted byte slices. +fn eval_wordwrap_bytes(bytes: &[u8], width: i64, break_string: &[u8], cut: bool) -> Vec { + if width < 0 && cut { + let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); + for byte in bytes { + output.extend_from_slice(break_string); + output.push(*byte); + } + return output; + } + + let width = width.max(0) as usize; + let mut output = Vec::with_capacity(bytes.len()); + let mut line_start = 0; + let mut last_space = None; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'\n' => { + output.extend_from_slice(&bytes[line_start..=index]); + index += 1; + line_start = index; + last_space = None; + } + b' ' => { + if index.saturating_sub(line_start) >= width { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + index += 1; + line_start = index; + last_space = None; + } else { + last_space = Some(index); + index += 1; + } + } + _ if index.saturating_sub(line_start) >= width => { + if let Some(space) = last_space { + output.extend_from_slice(&bytes[line_start..space]); + output.extend_from_slice(break_string); + line_start = space + 1; + last_space = None; + } else if cut && width > 0 { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + line_start = index; + } else { + index += 1; + } + } + _ => { + index += 1; + } + } + } + output.extend_from_slice(&bytes[line_start..]); + output +} diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index c07b975d1e..8ec7f365af 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -11,6 +11,8 @@ //! - This module does not own PHP values. Constants and operations are delegated //! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. +mod builtins; + use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ @@ -22,6 +24,7 @@ use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; +use builtins::*; use regex::bytes::{Captures, Regex, RegexBuilder}; use std::ffi::{CStr, CString}; use std::mem::MaybeUninit; @@ -2122,11979 +2125,260 @@ fn eval_positional_expr_call( } } -/// Evaluates string-name function probes against eval and supported builtin tables. -fn eval_builtin_function_probe( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\').to_ascii_lowercase(); - values.bool_value(eval_function_probe_exists(context, &name)) -} - -/// Evaluates `define(name, value)` for eval dynamic constant-name registration. -fn eval_builtin_define( +/// Evaluates nested `eval(...)` calls against the current materialized scope. +fn eval_nested_eval( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [name, value] = args else { + let [code] = args else { return Err(EvalStatus::RuntimeFatal); }; - let name = eval_expr(name, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - let defined = eval_define_name(name, value, context, values)?; - values.bool_value(defined) + let code = eval_expr(code, context, scope, values)?; + let code = values.string_bytes(code)?; + let program = parse_fragment(&code).map_err(EvalParseError::status)?; + execute_program_with_context(context, &program, scope, values) } -/// Evaluates `defined(name)` against eval dynamic constant names. -fn eval_builtin_defined( - args: &[EvalExpr], +/// Evaluates an eval-fragment include or require expression. +fn eval_include_expr( + path: &EvalExpr, + required: bool, + once: bool, context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - let exists = eval_defined_name(name, context, values)?; - values.bool_value(exists) -} - -/// Evaluates `define(...)` from already materialized call arguments. -fn eval_define_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); + let path = eval_expr(path, context, scope, values)?; + let path = eval_path_string(path, values)?; + let resolved_path = eval_resolve_include_path(&path, context); + let include_key = eval_include_key(&resolved_path); + if once && context.has_included_file(&include_key) { + return values.bool_value(true); + } + let bytes = match std::fs::read(&resolved_path) { + Ok(bytes) => bytes, + Err(_) => return eval_include_missing_file(&path, required, values), }; - let defined = eval_define_name(*name, *value, context, values)?; - values.bool_value(defined) + context.mark_included_file(include_key); + eval_execute_include_bytes(&bytes, &resolved_path, context, scope, values) } -/// Evaluates `defined(...)` from already materialized call arguments. -fn eval_defined_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, +/// Returns the include/require result for a file that cannot be opened. +fn eval_include_missing_file( + path: &str, + required: bool, values: &mut impl RuntimeValueOps, ) -> Result { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let exists = eval_defined_name(*name, context, values)?; - values.bool_value(exists) + let construct = if required { "require" } else { "include" }; + values.warning(&format!( + "Warning: {construct}({path}): Failed to open stream: No such file or directory\n" + ))?; + values.warning(&format!( + "Warning: {construct}(): Failed opening '{path}' for inclusion\n" + ))?; + if required { + Err(EvalStatus::RuntimeFatal) + } else { + values.bool_value(false) + } } -/// Normalizes and registers one eval dynamic constant name. -fn eval_define_name( - name: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_constant_name(name, values)?; - if name.is_empty() { - return Err(EvalStatus::RuntimeFatal); +/// Resolves eval include paths using PHP's cwd-first and caller-directory fallback. +fn eval_resolve_include_path(path: &str, context: &ElephcEvalContext) -> std::path::PathBuf { + let raw_path = std::path::Path::new(path); + if raw_path.is_absolute() || raw_path.exists() { + return raw_path.to_path_buf(); } - if eval_predefined_constant_value(&name).is_some() || context.has_constant(&name) { - values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; - return Ok(false); + if context.call_dir().is_empty() { + return raw_path.to_path_buf(); } - let value = values.retain(value)?; - if context.define_constant(&name, value) { - Ok(true) + let caller_path = std::path::Path::new(context.call_dir()).join(raw_path); + if caller_path.exists() { + caller_path } else { - values.release(value)?; - Ok(false) + raw_path.to_path_buf() } } -/// Normalizes and probes one eval dynamic constant name. -fn eval_defined_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_constant_name(name, values)?; - Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) -} - -/// Reads a PHP constant name from a runtime cell without changing case. -fn eval_constant_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal) +/// Builds the stable include_once key for a resolved path. +fn eval_include_key(path: &std::path::Path) -> String { + std::fs::canonicalize(path) + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .into_owned() } -/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. -fn eval_builtin_class_exists( - args: &[EvalExpr], +/// Executes a local include file, alternating raw output and PHP code blocks. +fn eval_execute_include_bytes( + bytes: &[u8], + path: &std::path::Path, context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let name = match args { - [name] => eval_expr(name, context, scope, values)?, - [name, autoload] => { - let name = eval_expr(name, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - name + let mut cursor = 0; + while let Some((tag_start, code_start)) = eval_find_php_open_tag(bytes, cursor) { + eval_echo_include_bytes(&bytes[cursor..tag_start], values)?; + let close = eval_find_php_close_tag(bytes, code_start); + let code_end = close.unwrap_or(bytes.len()); + match eval_execute_include_code(&bytes[code_start..code_end], path, context, scope, values)? + { + EvalControl::None => {} + EvalControl::Return(value) => return Ok(value), + EvalControl::Throw(value) => { + context.set_pending_throw(value); + return Err(EvalStatus::UncaughtThrowable); + } + EvalControl::Break | EvalControl::Continue => { + return Err(EvalStatus::UnsupportedConstruct); + } } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_class_exists_name(name, context, values)?; - values.bool_value(exists) + let Some(close) = close else { + return values.int(1); + }; + cursor = close + 2; + } + eval_echo_include_bytes(&bytes[cursor..], values)?; + values.int(1) } -/// Evaluates `class_exists(...)` from already materialized call arguments. -fn eval_class_exists_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, +/// Parses and executes one PHP code block from an included file. +fn eval_execute_include_code( + code: &[u8], + path: &std::path::Path, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [name] => eval_class_exists_name(*name, context, values)?, - [name, _autoload] => eval_class_exists_name(*name, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) +) -> Result { + let program = parse_fragment(code).map_err(EvalParseError::status)?; + let previous = context.call_site(); + let file = path.to_string_lossy().into_owned(); + let dir = path + .parent() + .map(|parent| parent.to_string_lossy().into_owned()) + .unwrap_or_default(); + context.set_call_site(file.clone(), dir, 1); + context.set_file_magic_override(Some(file)); + let result = execute_statements(program.statements(), context, scope, values); + context.set_call_site(previous.0, previous.1, previous.2); + context.set_file_magic_override(previous.3); + result } -/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. -fn eval_class_exists_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, +/// Echoes raw non-PHP include bytes through the eval value hooks. +fn eval_echo_include_bytes( + bytes: &[u8], values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\'); - if context.has_class(name) { - return Ok(true); +) -> Result<(), EvalStatus> { + if bytes.is_empty() { + return Ok(()); } - values.class_exists(name) + let output = values.string_bytes_value(bytes)?; + values.echo(output) } -/// Evaluates `interface_exists(...)` against generated interface-name metadata. -fn eval_builtin_interface_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = match args { - [name] => eval_expr(name, context, scope, values)?, - [name, autoload] => { - let name = eval_expr(name, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - name - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_interface_exists_name(name, values)?; - values.bool_value(exists) +/// Finds the next ` Option<(usize, usize)> { + bytes + .get(start..)? + .windows(5) + .position(eval_is_php_open_tag) + .map(|offset| { + let tag_start = start + offset; + (tag_start, tag_start + 5) + }) } -/// Evaluates `interface_exists(...)` from already materialized call arguments. -fn eval_interface_exists_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [name] => eval_interface_exists_name(*name, values)?, - [name, _autoload] => eval_interface_exists_name(*name, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) +/// Returns true when a five-byte window is a case-insensitive ` bool { + window.len() == 5 + && window[0] == b'<' + && window[1] == b'?' + && window[2].eq_ignore_ascii_case(&b'p') + && window[3].eq_ignore_ascii_case(&b'h') + && window[4].eq_ignore_ascii_case(&b'p') } -/// Normalizes a PHP interface-name cell and probes generated interface metadata. -fn eval_interface_exists_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - values.interface_exists(name.trim_start_matches('\\')) +/// Finds the next PHP closing tag after a code block start. +fn eval_find_php_close_tag(bytes: &[u8], start: usize) -> Option { + bytes + .get(start..)? + .windows(2) + .position(|window| window == b"?>") + .map(|offset| start + offset) } -/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. -fn eval_builtin_class_like_exists( - name: &str, +/// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. +fn eval_builtin_strlen( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let symbol = match args { - [symbol] => eval_expr(symbol, context, scope, values)?, - [symbol, autoload] => { - let symbol = eval_expr(symbol, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - symbol - } - _ => return Err(EvalStatus::RuntimeFatal), + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); }; - let exists = eval_class_like_exists_name(name, symbol, values)?; - values.bool_value(exists) + let value = eval_expr(value, context, scope, values)?; + let bytes = values.string_bytes(value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) } -/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. -fn eval_class_like_exists_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], +/// Evaluates the builtin `ord(...)` for the first byte of one coerced string. +fn eval_builtin_ord( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let exists = match evaluated_args { - [symbol] => eval_class_like_exists_name(name, *symbol, values)?, - [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, values)?, - _ => return Err(EvalStatus::RuntimeFatal), + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); }; - values.bool_value(exists) + let value = eval_expr(value, context, scope, values)?; + eval_ord_result(value, values) } -/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. -fn eval_class_like_exists_name( - name: &str, - symbol: RuntimeCellHandle, +/// Returns the first byte of one converted string, or zero for an empty string. +fn eval_ord_result( + value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, -) -> Result { - let symbol = values.string_bytes(symbol)?; - let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; - let symbol = symbol.trim_start_matches('\\'); - match name { - "trait_exists" => values.trait_exists(symbol), - "enum_exists" => values.enum_exists(symbol), - _ => Err(EvalStatus::UnsupportedConstruct), - } +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(bytes.first().copied().unwrap_or(0))) } -/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. -fn eval_builtin_is_a_relation( - name: &str, +/// Evaluates the builtin `count(...)` for one runtime array-like argument. +fn eval_builtin_count( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_count_result(value, None, values) + } + [value, mode] => { + let value = eval_expr(value, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_count_result(value, Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), } - eval_is_a_relation_result(name, &evaluated_args, context, values) } -/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. -fn eval_is_a_relation_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, +/// Counts an eval array with PHP normal or recursive mode semantics. +fn eval_count_result( + value: RuntimeCellHandle, + mode: Option, values: &mut impl RuntimeValueOps, ) -> Result { - let (object_or_class, target_class, allow_string) = match evaluated_args { - [object_or_class, target_class] => { - (*object_or_class, *target_class, name == "is_subclass_of") - } - [object_or_class, target_class, allow_string] => ( - *object_or_class, - *target_class, - values.truthy(*allow_string)?, - ), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let target_class = values.string_bytes(target_class)?; - let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_class = target_class.trim_start_matches('\\'); - let is_object = values.type_tag(object_or_class)? == 6; - let result = - if is_object && dynamic_object_is_a(object_or_class, target_class, context, values)? { - !matches!(name, "is_subclass_of") - } else if is_object || allow_string { - values.object_is_a(object_or_class, target_class, name == "is_subclass_of")? - } else { - false - }; - values.bool_value(result) -} - -/// Returns whether an eval-created object matches a dynamic class name exactly. -fn dynamic_object_is_a( - object: RuntimeCellHandle, - target_class: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - Ok(context - .dynamic_object_class(identity) - .is_some_and(|class| class.name().eq_ignore_ascii_case(target_class))) -} - -/// Evaluates PHP's `isset(...)` language construct over eval-visible values. -fn eval_builtin_isset( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return values.bool_value(false); - } - for arg in args { - if !eval_isset_arg(arg, context, scope, values)? { - return values.bool_value(false); - } - } - values.bool_value(true) -} - -/// Evaluates PHP's `empty(...)` language construct over eval-visible values. -fn eval_builtin_empty( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let empty = eval_empty_arg(arg, context, scope, values)?; - values.bool_value(empty) -} - -/// Evaluates one `empty` operand without warning or failing on missing variables. -fn eval_empty_arg( - arg: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let EvalExpr::LoadVar(name) = arg { - let Some(value) = visible_scope_cell(context, scope, name) else { - return Ok(true); - }; - return Ok(!values.truthy(value)?); - } - let value = eval_expr(arg, context, scope, values)?; - Ok(!values.truthy(value)?) -} - -/// Evaluates one `isset` operand without allocating a null cell for missing variables. -fn eval_isset_arg( - arg: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let EvalExpr::LoadVar(name) = arg { - let Some(value) = visible_scope_cell(context, scope, name) else { - return Ok(false); - }; - return Ok(!values.is_null(value)?); - } - let value = eval_expr(arg, context, scope, values)?; - Ok(!values.is_null(value)?) -} - -/// Returns true when a PHP function name is visible to eval builtin probes. -fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { - !name.contains("::") && (context.has_function(name) || eval_php_visible_builtin_exists(name)) -} - -/// Returns true for PHP-visible builtin names implemented by the eval interpreter. -fn eval_php_visible_builtin_exists(name: &str) -> bool { - matches!( - name, - "abs" - | "addslashes" - | "array_chunk" - | "array_column" - | "array_combine" - | "array_fill" - | "array_fill_keys" - | "array_filter" - | "array_flip" - | "array_map" - | "array_reduce" - | "array_walk" - | "array_key_exists" - | "array_keys" - | "array_diff" - | "array_intersect" - | "array_diff_key" - | "array_intersect_key" - | "array_merge" - | "array_pad" - | "array_pop" - | "array_product" - | "array_push" - | "array_rand" - | "array_reverse" - | "array_search" - | "array_shift" - | "array_slice" - | "array_splice" - | "array_sum" - | "array_unique" - | "array_unshift" - | "array_values" - | "arsort" - | "asort" - | "acos" - | "asin" - | "atan" - | "atan2" - | "basename" - | "base64_decode" - | "base64_encode" - | "bin2hex" - | "ceil" - | "chdir" - | "chmod" - | "call_user_func" - | "call_user_func_array" - | "class_exists" - | "enum_exists" - | "interface_exists" - | "is_a" - | "is_subclass_of" - | "boolval" - | "chop" - | "chr" - | "clamp" - | "clearstatcache" - | "count" - | "copy" - | "cos" - | "cosh" - | "crc32" - | "ctype_alnum" - | "ctype_alpha" - | "ctype_digit" - | "ctype_space" - | "date" - | "define" - | "defined" - | "deg2rad" - | "dirname" - | "disk_free_space" - | "disk_total_space" - | "exp" - | "explode" - | "fdiv" - | "file" - | "file_exists" - | "fileatime" - | "filectime" - | "filegroup" - | "file_get_contents" - | "fileinode" - | "filemtime" - | "fileowner" - | "fileperms" - | "file_put_contents" - | "filesize" - | "filetype" - | "fnmatch" - | "floor" - | "floatval" - | "fmod" - | "function_exists" - | "gethostbyaddr" - | "gethostbyname" - | "gethostname" - | "getprotobyname" - | "getprotobynumber" - | "getservbyname" - | "getservbyport" - | "get_class" - | "get_parent_class" - | "get_resource_id" - | "get_resource_type" - | "getcwd" - | "getenv" - | "gettype" - | "glob" - | "hash" - | "hash_algos" - | "hash_equals" - | "hash_file" - | "hash_hmac" - | "hex2bin" - | "html_entity_decode" - | "htmlentities" - | "htmlspecialchars" - | "hypot" - | "implode" - | "in_array" - | "inet_ntop" - | "inet_pton" - | "intdiv" - | "ip2long" - | "is_dir" - | "is_executable" - | "is_file" - | "is_link" - | "is_readable" - | "is_writable" - | "is_writeable" - | "intval" - | "link" - | "linkinfo" - | "ltrim" - | "is_callable" - | "is_array" - | "is_bool" - | "is_double" - | "is_finite" - | "is_float" - | "is_infinite" - | "is_int" - | "is_integer" - | "is_iterable" - | "is_long" - | "is_nan" - | "is_null" - | "is_numeric" - | "is_object" - | "is_real" - | "is_resource" - | "is_string" - | "iterator_apply" - | "iterator_count" - | "iterator_to_array" - | "json_decode" - | "json_encode" - | "json_last_error" - | "json_last_error_msg" - | "json_validate" - | "krsort" - | "ksort" - | "lcfirst" - | "log" - | "log2" - | "log10" - | "long2ip" - | "max" - | "md5" - | "microtime" - | "min" - | "mkdir" - | "mktime" - | "mt_rand" - | "natcasesort" - | "natsort" - | "nl2br" - | "number_format" - | "ord" - | "pathinfo" - | "pi" - | "pow" - | "php_uname" - | "phpversion" - | "preg_match" - | "preg_match_all" - | "preg_replace" - | "preg_replace_callback" - | "preg_split" - | "putenv" - | "print_r" - | "rand" - | "random_int" - | "range" - | "rad2deg" - | "rawurldecode" - | "rawurlencode" - | "readfile" - | "readlink" - | "realpath" - | "realpath_cache_get" - | "realpath_cache_size" - | "rename" - | "rsort" - | "rtrim" - | "round" - | "rmdir" - | "scandir" - | "settype" - | "sleep" - | "sha1" - | "shuffle" - | "sin" - | "sinh" - | "sort" - | "sqrt" - | "spl_classes" - | "spl_object_hash" - | "spl_object_id" - | "sscanf" - | "sprintf" - | "strcasecmp" - | "stream_get_filters" - | "stream_get_transports" - | "stream_get_wrappers" - | "str_contains" - | "str_ends_with" - | "str_ireplace" - | "str_repeat" - | "str_replace" - | "str_starts_with" - | "strcmp" - | "stat" - | "strlen" - | "strpos" - | "strrpos" - | "strrev" - | "str_pad" - | "str_split" - | "strstr" - | "strtotime" - | "substr" - | "stripslashes" - | "strtolower" - | "strtoupper" - | "strval" - | "symlink" - | "sys_get_temp_dir" - | "tempnam" - | "tan" - | "tanh" - | "time" - | "touch" - | "trait_exists" - | "trim" - | "substr_replace" - | "ucfirst" - | "ucwords" - | "uasort" - | "uksort" - | "unlink" - | "umask" - | "urldecode" - | "urlencode" - | "usort" - | "usleep" - | "var_dump" - | "printf" - | "vprintf" - | "vsprintf" - | "wordwrap" - | "lstat" - ) -} - -/// Evaluates a direct PHP-visible builtin call with named or spread arguments. -fn eval_builtin_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; - let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { - return Err(EvalStatus::UnsupportedConstruct); - }; - Ok(result) -} - -/// Binds evaluated builtin arguments to PHP parameter order when names are used. -fn bind_evaluated_builtin_args( - name: &str, - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if evaluated_args.iter().all(|arg| arg.name.is_none()) { - return Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()); - } - - let params = eval_builtin_param_names(name).ok_or(EvalStatus::RuntimeFatal)?; - let mut bound_args = vec![None; params.len()]; - let mut next_positional = 0; - - for arg in evaluated_args { - if let Some(name) = arg.name { - bind_builtin_named_arg(params, &mut bound_args, &name, arg.value)?; - } else { - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; - } - } - - collect_bound_builtin_args(name, bound_args, values) -} - -/// Binds one named builtin-call value to the matching PHP parameter slot. -fn bind_builtin_named_arg( - params: &[&str], - bound_args: &mut [Option], - name: &str, - value: RuntimeCellHandle, -) -> Result<(), EvalStatus> { - let Some(param_index) = params.iter().position(|param| *param == name) else { - return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[param_index] = Some(value); - Ok(()) -} - -/// Collects ordered bound arguments, rejecting gaps where defaults would be needed. -fn collect_contiguous_bound_args( - bound_args: Vec>, -) -> Result, EvalStatus> { - let Some(last_index) = bound_args.iter().rposition(Option::is_some) else { - return Ok(Vec::new()); - }; - bound_args - .into_iter() - .take(last_index + 1) - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Collects ordered builtin arguments, applying PHP defaults for special named-call gaps. -fn collect_bound_builtin_args( - name: &str, - mut bound_args: Vec>, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if name == "array_splice" && bound_args.get(3).is_some_and(Option::is_some) { - if bound_args.get(2).is_some_and(Option::is_none) { - bound_args[2] = Some(values.null()?); - } - } - collect_contiguous_bound_args(bound_args) -} - -/// Returns PHP parameter names for builtin calls implemented by eval. -fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { - match name { - "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), - "array_chunk" => Some(&["array", "length"]), - "array_column" => Some(&["array", "column_key"]), - "array_combine" => Some(&["keys", "values"]), - "array_fill" => Some(&["start_index", "count", "value"]), - "array_fill_keys" => Some(&["keys", "value"]), - "array_filter" => Some(&["array", "callback", "mode"]), - "array_map" => Some(&["callback", "array"]), - "array_reduce" => Some(&["array", "callback", "initial"]), - "array_walk" => Some(&["array", "callback"]), - "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), - "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" - | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" | "asort" - | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { - Some(&["array"]) - } - "array_push" | "array_unshift" => Some(&["array", "values"]), - "array_key_exists" => Some(&["key", "array"]), - "array_pad" => Some(&["array", "length", "value"]), - "array_reverse" => Some(&["array", "preserve_keys"]), - "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), - "array_slice" => Some(&["array", "offset", "length"]), - "array_splice" => Some(&["array", "offset", "length", "replacement"]), - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), - "atan2" => Some(&["y", "x"]), - "basename" => Some(&["path", "suffix"]), - "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" - | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => { - Some(&["string"]) - } - "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" - | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" - | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" | "is_real" - | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), - "settype" => Some(&["var", "type"]), - "get_class" => Some(&["object"]), - "get_parent_class" => Some(&["object_or_class"]), - "call_user_func" => Some(&["callback"]), - "call_user_func_array" => Some(&["callback", "args"]), - "class_exists" => Some(&["class", "autoload"]), - "enum_exists" => Some(&["enum", "autoload"]), - "interface_exists" => Some(&["interface", "autoload"]), - "trait_exists" => Some(&["trait", "autoload"]), - "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), - "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), - "chmod" => Some(&["filename", "permissions"]), - "chr" => Some(&["codepoint"]), - "clamp" => Some(&["value", "min", "max"]), - "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), - "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), - "count" => Some(&["value", "mode"]), - "copy" | "rename" => Some(&["from", "to"]), - "crc32" => Some(&["string"]), - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), - "date" => Some(&["format", "timestamp"]), - "define" => Some(&["constant_name", "value"]), - "defined" => Some(&["constant_name"]), - "dirname" => Some(&["path", "levels"]), - "disk_free_space" | "disk_total_space" => Some(&["directory"]), - "explode" => Some(&["separator", "string"]), - "fdiv" | "fmod" => Some(&["num1", "num2"]), - "fnmatch" => Some(&["pattern", "filename", "flags"]), - "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" - | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" - | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" - | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), - "file_put_contents" => Some(&["filename", "data"]), - "function_exists" => Some(&["function"]), - "gethostbyaddr" => Some(&["ip"]), - "gethostbyname" => Some(&["hostname"]), - "gethostname" => Some(&[]), - "getprotobyname" => Some(&["protocol"]), - "getprotobynumber" => Some(&["protocol"]), - "getservbyname" => Some(&["service", "protocol"]), - "getservbyport" => Some(&["port", "protocol"]), - "get_resource_id" | "get_resource_type" => Some(&["resource"]), - "getcwd" => Some(&[]), - "getenv" => Some(&["name"]), - "glob" => Some(&["pattern"]), - "hash" => Some(&["algo", "data", "binary"]), - "hash_algos" => Some(&[]), - "hash_equals" => Some(&["known_string", "user_string"]), - "hash_file" => Some(&["algo", "filename", "binary"]), - "hash_hmac" => Some(&["algo", "data", "key", "binary"]), - "hypot" => Some(&["x", "y"]), - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), - "implode" => Some(&["separator", "array"]), - "inet_ntop" => Some(&["ip"]), - "inet_pton" => Some(&["ip"]), - "intdiv" => Some(&["num1", "num2"]), - "iterator_apply" => Some(&["iterator", "callback", "args"]), - "iterator_count" => Some(&["iterator"]), - "iterator_to_array" => Some(&["iterator", "preserve_keys"]), - "ip2long" => Some(&["ip"]), - "json_decode" => Some(&["json", "associative", "depth", "flags"]), - "json_encode" => Some(&["value", "flags", "depth"]), - "json_last_error" | "json_last_error_msg" => Some(&[]), - "json_validate" => Some(&["json", "depth", "flags"]), - "link" | "symlink" => Some(&["target", "link"]), - "linkinfo" | "readlink" => Some(&["path"]), - "log" => Some(&["num", "base"]), - "max" | "min" => Some(&["value"]), - "md5" | "sha1" => Some(&["string", "binary"]), - "microtime" => Some(&["as_float"]), - "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), - "nl2br" => Some(&["string", "use_xhtml"]), - "number_format" => Some(&[ - "num", - "decimals", - "decimal_separator", - "thousands_separator", - ]), - "ord" => Some(&["character"]), - "pathinfo" => Some(&["path", "flags"]), - "pi" => Some(&[]), - "php_uname" => Some(&["mode"]), - "phpversion" => Some(&[]), - "pow" => Some(&["num", "exponent"]), - "preg_match" => Some(&["pattern", "subject", "matches", "flags", "offset"]), - "preg_match_all" => Some(&["pattern", "subject", "matches", "flags", "offset"]), - "preg_replace" => Some(&["pattern", "replacement", "subject", "limit", "count"]), - "preg_replace_callback" => Some(&["pattern", "callback", "subject", "limit", "count"]), - "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), - "print_r" | "var_dump" => Some(&["value"]), - "putenv" => Some(&["assignment"]), - "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), - "range" => Some(&["start", "end"]), - "realpath" => Some(&["path"]), - "realpath_cache_get" | "realpath_cache_size" => Some(&[]), - "round" => Some(&["num", "precision"]), - "sleep" => Some(&["seconds"]), - "spl_classes" => Some(&[]), - "spl_object_id" | "spl_object_hash" => Some(&["object"]), - "sscanf" => Some(&["string", "format", "vars"]), - "sprintf" | "printf" => Some(&["format", "values"]), - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), - "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), - "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), - "strtotime" => Some(&["datetime"]), - "strstr" => Some(&["haystack", "needle", "before_needle"]), - "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), - "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), - "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), - "str_repeat" => Some(&["string", "times"]), - "str_split" => Some(&["string", "length"]), - "substr" => Some(&["string", "offset", "length"]), - "substr_replace" => Some(&["string", "replace", "offset", "length"]), - "sys_get_temp_dir" | "time" => Some(&[]), - "tempnam" => Some(&["directory", "prefix"]), - "touch" => Some(&["filename", "mtime", "atime"]), - "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { - Some(&["string"]) - } - "long2ip" => Some(&["ip"]), - "ucwords" => Some(&["string", "separators"]), - "umask" => Some(&["mask"]), - "usleep" => Some(&["microseconds"]), - "vsprintf" | "vprintf" => Some(&["format", "values"]), - "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), - _ => None, - } -} - -/// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. -fn eval_builtin_call_user_func( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_call_user_func_with_values(evaluated_args, context, values) -} - -/// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. -fn eval_builtin_call_user_func_array( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [callback, arg_array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let callback = eval_expr(callback, context, scope, values)?; - let arg_array = eval_expr(arg_array, context, scope, values)?; - eval_call_user_func_array_with_values(callback, arg_array, context, values) -} - -/// Dispatches `call_user_func_array` after callback and array arguments are evaluated. -fn eval_call_user_func_array_with_values( - callback: RuntimeCellHandle, - arg_array: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable(callback, values)?; - if !values.is_array_like(arg_array)? { - return Err(EvalStatus::RuntimeFatal); - } - let evaluated_args = eval_array_call_arg_values(arg_array, values)?; - eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) -} - -/// Dispatches `call_user_func` after its callback and arguments are already evaluated. -fn eval_call_user_func_with_values( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((callback, callback_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let callback = eval_callable(*callback, values)?; - eval_evaluated_callable_with_values(&callback, callback_args.to_vec(), context, values) -} - -/// Normalizes one PHP callback value for eval dynamic callable dispatch. -fn eval_callable( - callback: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.is_array_like(callback)? { - return eval_array_callable(callback, values); - } - eval_callable_name(callback, values).map(EvaluatedCallable::Named) -} - -/// Normalizes one two-element object-method callable array. -fn eval_array_callable( - callback: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.array_len(callback)? != 2 { - return Err(EvalStatus::RuntimeFatal); - } - let zero = values.int(0)?; - let one = values.int(1)?; - let object = values.array_get(callback, zero)?; - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::UnsupportedConstruct); - } - let method = values.array_get(callback, one)?; - let method = - String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(EvaluatedCallable::ObjectMethod { object, method }) -} - -/// Normalizes one string callback name for eval dynamic callable dispatch. -fn eval_callable_name( - callback: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = values.string_bytes(callback)?; - let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; - let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); - if callback.contains("::") { - return Err(EvalStatus::UnsupportedConstruct); - } - Ok(callback) -} - -/// Invokes an already normalized callback with source-order positional values. -fn eval_evaluated_callable_with_values( - callback: &EvaluatedCallable, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named(name) => { - eval_callable_with_values(name, evaluated_args, context, values) - } - EvaluatedCallable::ObjectMethod { object, method } => { - eval_method_call_result(*object, method, evaluated_args, context, values) - } - } -} - -/// Invokes an already normalized callback with optional named-argument metadata. -fn eval_evaluated_callable_with_call_array_args( - callback: &EvaluatedCallable, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named(name) => { - eval_callable_with_call_array_args(name, evaluated_args, context, values) - } - EvaluatedCallable::ObjectMethod { object, method } => { - if evaluated_args.iter().any(|arg| arg.name.is_some()) { - return Err(EvalStatus::RuntimeFatal); - } - let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); - eval_method_call_result(*object, method, evaluated_args, context, values) - } - } -} - -/// Invokes a PHP-visible callable name with source-order positional values. -fn eval_callable_with_values( - name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { - return Ok(result); - } - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function_with_values(&function, evaluated_args, context, values); - } - if let Some(function) = context.native_function(name) { - return eval_native_function_with_values(function, evaluated_args, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Invokes a callable with arguments that may carry `call_user_func_array` names. -fn eval_callable_with_call_array_args( - name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if evaluated_args.iter().all(|arg| arg.name.is_none()) { - let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); - return eval_callable_with_values(name, evaluated_args, context, values); - } - if eval_php_visible_builtin_exists(name) { - let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; - let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { - return Err(EvalStatus::UnsupportedConstruct); - }; - return Ok(result); - } - if let Some(function) = context.function(name).cloned() { - let evaluated_args = bind_evaluated_function_args(function.params(), evaluated_args)?; - return eval_dynamic_function_with_values(&function, evaluated_args, context, values); - } - if let Some(function) = context.native_function(name) { - if function.param_names().len() != function.param_count() { - return Err(EvalStatus::RuntimeFatal); - } - let evaluated_args = bind_evaluated_function_args(function.param_names(), evaluated_args)?; - return eval_native_function_with_values(function, evaluated_args, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. -fn eval_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "abs" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.abs(*value)? - } - "addslashes" | "stripslashes" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_slashes_result(name, *value, values)? - } - "array_combine" => { - let [keys, values_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_combine_result(*keys, *values_array, values)? - } - "array_column" => { - let [array, column_key] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_column_result(*array, *column_key, values)? - } - "array_chunk" => { - let [array, length] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_chunk_result(*array, *length, values)? - } - "array_fill" => { - let [start, count, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_result(*start, *count, *value, values)? - } - "array_fill_keys" => { - let [keys, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_keys_result(*keys, *value, values)? - } - "array_filter" => match evaluated_args { - [array] => eval_array_filter_result(*array, None, None, context, values)?, - [array, callback] => { - eval_array_filter_result(*array, Some(*callback), None, context, values)? - } - [array, callback, mode] => { - eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_map" => { - let Some((callback, arrays)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_map_result(*callback, arrays, context, values)? - } - "array_reduce" => match evaluated_args { - [array, callback] => { - let initial = values.null()?; - eval_array_reduce_result(*array, *callback, initial, context, values)? - } - [array, callback, initial] => { - eval_array_reduce_result(*array, *callback, *initial, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_walk" => { - let [array, callback] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_walk_result(*array, *callback, context, values)? - } - "array_pop" | "array_shift" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_pop_shift_value_result(name, *array, values)? - } - "array_push" | "array_unshift" => { - let Some((array, inserted)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_push_unshift_count_result(*array, inserted.len(), values)? - } - "array_splice" => { - let result = match evaluated_args { - [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, - [array, offset, length] => { - eval_array_splice_value_result(*array, *offset, Some(*length), values)? - } - [array, offset, length, _replacement] => { - eval_array_splice_value_result(*array, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.warning( - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - )?; - result - } - "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" - | "shuffle" | "sort" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_sort_value_result(*array, values)? - } - "uasort" | "uksort" | "usort" => { - let [array, callback] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_user_sort_value_result(name, *array, *callback, context, values)? - } - "array_flip" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_flip_result(*array, values)? - } - "array_pad" => { - let [array, length, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_pad_result(*array, *length, *value, values)? - } - "array_product" | "array_sum" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_aggregate_result(name, *array, values)? - } - "array_keys" | "array_values" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_projection_result(name, *array, values)? - } - "array_key_exists" => { - let [key, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.array_key_exists(*key, *array)? - } - "array_diff" | "array_intersect" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_value_set_result(name, *left, *right, values)? - } - "array_diff_key" | "array_intersect_key" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_key_set_result(name, *left, *right, values)? - } - "array_merge" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_merge_result(*left, *right, values)? - } - "array_rand" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_rand_result(*array, values)? - } - "array_reverse" => match evaluated_args { - [array] => eval_array_reverse_result(*array, false, values)?, - [array, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_array_reverse_result(*array, preserve_keys, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_search" | "in_array" => { - let [needle, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_search_result(name, *needle, *array, values)? - } - "array_slice" => match evaluated_args { - [array, offset] => eval_array_slice_result(*array, *offset, None, values)?, - [array, offset, length] => { - eval_array_slice_result(*array, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_unique" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_unique_result(*array, values)? - } - "range" => { - let [start, end] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_range_result(*start, *end, values)? - } - "base64_encode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_encode_result(*value, values)? - } - "base64_decode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_decode_result(*value, values)? - } - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_unary_result(name, *value, values)? - } - "atan2" | "hypot" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_pair_result(name, *left, *right, values)? - } - "bin2hex" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_bin2hex_result(*value, values)? - } - "ceil" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.ceil(*value)? - } - "chr" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chr_result(*value, values)? - } - "chdir" | "mkdir" | "rmdir" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unary_path_bool_result(name, *path, values)? - } - "chmod" => { - let [filename, permissions] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chmod_result(*filename, *permissions, values)? - } - "clearstatcache" => { - if evaluated_args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - values.null()? - } - "clamp" => { - let [value, min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_clamp_result(*value, *min, *max, values)? - } - "copy" | "link" | "rename" | "symlink" => { - let [from, to] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_binary_path_bool_result(name, *from, *to, values)? - } - "floor" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.floor(*value)? - } - "fdiv" | "fmod" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_binary_result(name, *left, *right, values)? - } - "file" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_result(*filename, values)? - } - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_probe_result(name, *filename, values)? - } - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_stat_scalar_result(name, *filename, values)? - } - "file_get_contents" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_get_contents_result(*filename, values)? - } - "file_put_contents" => { - let [filename, data] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_put_contents_result(*filename, *data, values)? - } - "filesize" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_filesize_result(*filename, values)? - } - "filetype" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_filetype_result(*filename, values)? - } - "fnmatch" => match evaluated_args { - [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, - [pattern, filename, flags] => { - eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stat" | "lstat" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stat_array_result(name, *filename, values)? - } - "linkinfo" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_linkinfo_result(*path, values)? - } - "log" => match evaluated_args { - [num] => eval_log_result(*num, None, values)?, - [num, base] => eval_log_result(*num, Some(*base), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "readfile" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_readfile_result(*filename, values)? - } - "pi" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI)? - } - "php_uname" => match evaluated_args { - [] => eval_php_uname_result(None, values)?, - [mode] => eval_php_uname_result(Some(*mode), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "pow" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.pow(*left, *right)? - } - "preg_match" => match evaluated_args { - [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_match_all" => match evaluated_args { - [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_replace" => match evaluated_args { - [pattern, replacement, subject] => { - eval_preg_replace_result(*pattern, *replacement, *subject, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_replace_callback" => match evaluated_args { - [pattern, callback, subject] => { - eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_split" => match evaluated_args { - [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values)?, - [pattern, subject, limit] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), None, values)? - } - [pattern, subject, limit, flags] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "print_r" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_print_r_result(*value, values)? - } - "var_dump" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_var_dump_result(*value, values)? - } - "rand" | "mt_rand" => match evaluated_args { - [] => eval_rand_result(None, None, values)?, - [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "random_int" => { - let [min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_random_int_result(*min, *max, values)? - } - "rawurldecode" | "urldecode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_decode_result(name, *value, values)? - } - "rawurlencode" | "urlencode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_encode_result(name, *value, values)? - } - "round" => match evaluated_args { - [value] => values.round(*value, None)?, - [value, precision] => values.round(*value, Some(*precision))?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "scandir" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_scandir_result(*directory, values)? - } - "sqrt" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.sqrt(*value)? - } - "spl_classes" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values)? - } - "spl_object_id" | "spl_object_hash" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_spl_object_identity_result(name, *object, values)? - } - "sscanf" => { - let [input, format, ..] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sscanf_result(*input, *format, values)? - } - "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, - "settype" => { - let [value, type_name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_settype_value_result(*value, *type_name, values)? - } - "strrev" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.strrev(*value)? - } - "str_repeat" => { - let [value, times] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_repeat_result(*value, *times, values)? - } - "str_replace" | "str_ireplace" => { - let [search, replace, subject] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_replace_result(name, *search, *replace, *subject, values)? - } - "str_pad" => match evaluated_args { - [value, length] => eval_str_pad_result(*value, *length, None, None, values)?, - [value, length, pad_string] => { - eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? - } - [value, length, pad_string, pad_type] => { - eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "str_split" => match evaluated_args { - [value] => eval_str_split_result(*value, None, values)?, - [value, length] => eval_str_split_result(*value, Some(*length), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "substr" => match evaluated_args { - [value, offset] => eval_substr_result(*value, *offset, None, values)?, - [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "substr_replace" => match evaluated_args { - [value, replace, offset] => { - eval_substr_replace_result(*value, *replace, *offset, None, values)? - } - [value, replace, offset, length] => { - eval_substr_replace_result(*value, *replace, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "call_user_func" => { - return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) - .map(Some); - } - "call_user_func_array" => { - let [callback, arg_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) - .map(Some); - } - "boolval" | "floatval" | "intval" | "strval" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_cast_result(name, *value, values)? - } - "count" => match evaluated_args { - [value] => eval_count_result(*value, None, values)?, - [value, mode] => eval_count_result(*value, Some(*mode), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "crc32" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_crc32_result(*value, values)? - } - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ctype_result(name, *value, values)? - } - "date" => match evaluated_args { - [format] => eval_date_result(*format, None, values)?, - [format, timestamp] => eval_date_result(*format, Some(*timestamp), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "define" => eval_define_result(evaluated_args, context, values)?, - "defined" => eval_defined_result(evaluated_args, context, values)?, - "explode" => { - let [separator, string] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_explode_result(*separator, *string, values)? - } - "ord" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ord_result(*value, values)? - } - "implode" => { - let [separator, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_implode_result(*separator, *array, values)? - } - "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, - "microtime" => match evaluated_args { - [] | [_] => eval_microtime_result(values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "mktime" => { - let [hour, minute, second, month, day, year] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? - } - "nl2br" => match evaluated_args { - [value] => eval_nl2br_result(*value, true, values)?, - [value, use_xhtml] => { - let use_xhtml = values.truthy(*use_xhtml)?; - eval_nl2br_result(*value, use_xhtml, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "number_format" => match evaluated_args { - [value] => eval_number_format_result(*value, None, None, None, values)?, - [value, decimals] => { - eval_number_format_result(*value, Some(*decimals), None, None, values)? - } - [value, decimals, decimal_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - None, - values, - )?, - [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - Some(*thousands_separator), - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "basename" => match evaluated_args { - [path] => eval_basename_result(*path, None, values)?, - [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "dirname" => match evaluated_args { - [path] => eval_dirname_result(*path, None, values)?, - [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "disk_free_space" | "disk_total_space" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_disk_space_result(name, *directory, values)? - } - "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { - [value] => eval_trim_like_result(name, *value, None, values)?, - [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "function_exists" | "is_callable" => { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = values.string_bytes(*name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\').to_ascii_lowercase(); - values.bool_value(eval_function_probe_exists(context, &name))? - } - "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, - "enum_exists" | "trait_exists" => { - eval_class_like_exists_result(name, evaluated_args, values)? - } - "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, - "is_a" | "is_subclass_of" => { - eval_is_a_relation_result(name, evaluated_args, context, values)? - } - "json_decode" => match evaluated_args { - [json] => eval_json_decode_result(*json, None, None, None, context, values)?, - [json, associative] => { - eval_json_decode_result(*json, Some(*associative), None, None, context, values)? - } - [json, associative, depth] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - None, - context, - values, - )?, - [json, associative, depth, flags] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - Some(*flags), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "json_encode" => match evaluated_args { - [value] => eval_json_encode_result(*value, None, None, context, values)?, - [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values)?, - [value, flags, depth] => { - eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "json_last_error" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.int(context.json_last_error())? - } - "json_last_error_msg" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.string(context.json_last_error_msg())? - } - "json_validate" => match evaluated_args { - [json] => eval_json_validate_result(*json, None, None, context, values)?, - [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values)?, - [json, depth, flags] => { - eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "gethostbyaddr" => { - let [ip] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyaddr_result(*ip, values)? - } - "gethostbyname" => { - let [hostname] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyname_result(*hostname, values)? - } - "gethostname" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_gethostname_result(values)? - } - "getprotobyname" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobyname_result(*protocol, values)? - } - "getprotobynumber" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobynumber_result(*protocol, values)? - } - "getservbyname" => { - let [service, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyname_result(*service, *protocol, values)? - } - "getservbyport" => { - let [port, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyport_result(*port, *protocol, values)? - } - "getcwd" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_getcwd_result(values)? - } - "getenv" => { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getenv_result(*name, values)? - } - "get_class" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_get_class_result(*object, context, values)? - } - "get_parent_class" => { - let [object_or_class] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_get_parent_class_result(*object_or_class, values)? - } - "get_resource_id" | "get_resource_type" => { - let [resource] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_resource_introspection_result(name, *resource, values)? - } - "gettype" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gettype_result(*value, values)? - } - "glob" => { - let [pattern] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_glob_result(*pattern, values)? - } - "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { - eval_hash_one_shot_result(name, evaluated_args, values)? - } - "hash_algos" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values)? - } - "hash_equals" => { - let [known, user] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_equals_result(*known, *user, values)? - } - "hex2bin" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hex2bin_result(*value, values)? - } - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_html_entity_result(name, *value, values)? - } - "inet_ntop" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_ntop_result(*value, values)? - } - "inet_pton" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_pton_result(*value, values)? - } - "intdiv" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_intdiv_result(*left, *right, values)? - } - "iterator_apply" => match evaluated_args { - [iterator, callback] => { - let callback = eval_callable(*callback, values)?; - eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? - } - [iterator, callback, args] => { - let callback = eval_callable(*callback, values)?; - let callback_args = eval_iterator_apply_arg_values(*args, values)?; - eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "iterator_count" => { - let [iterator] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_iterator_count_result(*iterator, values)? - } - "iterator_to_array" => match evaluated_args { - [iterator] => eval_iterator_to_array_result(*iterator, true, values)?, - [iterator, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_iterator_to_array_result(*iterator, preserve_keys, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "ip2long" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ip2long_result(*value, values)? - } - "phpversion" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_phpversion_result(values)? - } - "pathinfo" => match evaluated_args { - [path] => eval_pathinfo_result(*path, None, values)?, - [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "putenv" => { - let [assignment] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_putenv_result(*assignment, values)? - } - "realpath" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_realpath_result(*path, values)? - } - "realpath_cache_get" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_get_result(values)? - } - "realpath_cache_size" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_size_result(values)? - } - "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" - | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_type_predicate_result(name, *value, values)? - } - "sys_get_temp_dir" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_sys_get_temp_dir_result(values)? - } - "tempnam" => { - let [directory, prefix] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_tempnam_result(*directory, *prefix, values)? - } - "sleep" => { - let [seconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sleep_result(*seconds, values)? - } - "time" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values)? - } - "touch" => match evaluated_args { - [filename] => eval_touch_result(*filename, None, None, values)?, - [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, - [filename, mtime, atime] => { - eval_touch_result(*filename, Some(*mtime), Some(*atime), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_introspection_result(name, values)? - } - "strtotime" => { - let [datetime] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_strtotime_result(*datetime, values)? - } - "umask" => match evaluated_args { - [] => eval_umask_result(None, values)?, - [mask] => eval_umask_result(Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "usleep" => { - let [microseconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_usleep_result(*microseconds, values)? - } - "readlink" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_readlink_result(*path, values)? - } - "unlink" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unlink_result(*filename, values)? - } - "strlen" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let bytes = values.string_bytes(*value)?; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len)? - } - "strpos" | "strrpos" => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_position_result(name, *haystack, *needle, values)? - } - "str_contains" | "str_starts_with" | "str_ends_with" => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_search_result(name, *haystack, *needle, values)? - } - "strstr" => match evaluated_args { - [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values)?, - [haystack, needle, before_needle] => { - let before_needle = values.truthy(*before_needle)?; - eval_strstr_result(*haystack, *needle, before_needle, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "strcmp" | "strcasecmp" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_compare_result(name, *left, *right, values)? - } - "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_case_result(name, *value, values)? - } - "long2ip" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_long2ip_result(*value, values)? - } - "ucwords" => match evaluated_args { - [value] => eval_ucwords_result(*value, None, values)?, - [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, - "wordwrap" => match evaluated_args { - [value] => eval_wordwrap_result(*value, None, None, None, values)?, - [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, - [value, width, break_string] => { - eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values)? - } - [value, width, break_string, cut] => eval_wordwrap_result( - *value, - Some(*width), - Some(*break_string), - Some(*cut), - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - _ => return Ok(None), - }; - Ok(Some(result)) -} - -/// Evaluates PHP's `abs(...)` over one eval expression. -fn eval_builtin_abs( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.abs(value) -} - -/// Evaluates PHP array aggregate builtins over one eval array expression. -fn eval_builtin_array_aggregate( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_aggregate_result(name, array, values) -} - -/// Computes `array_sum()` or `array_product()` through eval's numeric value hooks. -fn eval_array_aggregate_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = match name { - "array_sum" => values.int(0)?, - "array_product" => values.int(1)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - result = match name { - "array_sum" => values.add(result, value)?, - "array_product" => values.mul(result, value)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - } - Ok(result) -} - -/// Evaluates PHP `array_combine()` over key and value array expressions. -fn eval_builtin_array_combine( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [keys, values_array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let keys = eval_expr(keys, context, scope, values)?; - let values_array = eval_expr(values_array, context, scope, values)?; - eval_array_combine_result(keys, values_array, values) -} - -/// Builds the associative result for `array_combine()` from two eval arrays. -fn eval_array_combine_result( - keys: RuntimeCellHandle, - values_array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(keys)?; - if len != values.array_len(values_array)? { - return Err(EvalStatus::RuntimeFatal); - } - - let mut result = values.assoc_new(len)?; - for position in 0..len { - let source_key = values.array_iter_key(keys, position)?; - let target_key = values.array_get(keys, source_key)?; - let target_key = values.cast_string(target_key)?; - let value_key = values.array_iter_key(values_array, position)?; - let value = values.array_get(values_array, value_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_column()` over row-array and column-key expressions. -fn eval_builtin_array_column( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, column_key] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let column_key = eval_expr(column_key, context, scope, values)?; - eval_array_column_result(array, column_key, values) -} - -/// Builds `array_column()` by extracting present row columns into a reindexed array. -fn eval_array_column_result( - array: RuntimeCellHandle, - column_key: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len)?; - let mut output_index = 0_i64; - for position in 0..len { - let row_key = values.array_iter_key(array, position)?; - let row = values.array_get(array, row_key)?; - if !matches!(values.type_tag(row)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - continue; - } - let exists = values.array_key_exists(column_key, row)?; - if !values.truthy(exists)? { - continue; - } - let column = values.array_get(row, column_key)?; - let target_key = values.int(output_index)?; - output_index = output_index - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, column)?; - } - Ok(result) -} - -/// Evaluates PHP `array_fill()` over start, count, and value expressions. -fn eval_builtin_array_fill( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [start, count, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let start = eval_expr(start, context, scope, values)?; - let count = eval_expr(count, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_fill_result(start, count, value, values) -} - -/// Builds an `array_fill()` result with PHP's explicit integer key range. -fn eval_array_fill_result( - start: RuntimeCellHandle, - count: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let start = eval_int_value(start, values)?; - let count = eval_int_value(count, values)?; - if count < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut result = if start == 0 { - values.array_new(count)? - } else { - values.assoc_new(count)? - }; - for offset in 0..count { - let offset = i64::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = start.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_fill_keys()` over key-array and value expressions. -fn eval_builtin_array_fill_keys( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [keys, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let keys = eval_expr(keys, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_fill_keys_result(keys, value, values) -} - -/// Builds an `array_fill_keys()` result preserving the source key iteration order. -fn eval_array_fill_keys_result( - keys: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(keys)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let source_key = values.array_iter_key(keys, position)?; - let target_key = values.array_get(keys, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_map()` for one source array and a string or null callback. -fn eval_builtin_array_map( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((callback, arrays)) = args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let callback = eval_expr(callback, context, scope, values)?; - let mut evaluated_arrays = Vec::with_capacity(arrays.len()); - for array in arrays { - evaluated_arrays.push(eval_expr(array, context, scope, values)?); - } - eval_array_map_result(callback, &evaluated_arrays, context, values) -} - -/// Maps one eval array with PHP key preservation for the one-array form. -fn eval_array_map_result( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = arrays else { - return eval_array_map_variadic_result(callback, arrays, context, values); - }; - let callback = if values.is_null(callback)? { - None - } else { - Some(eval_callable_name(callback, values)?) - }; - let len = values.array_len(*array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(*array, position)?; - let value = values.array_get(*array, key)?; - let mapped = if let Some(callback) = callback.as_deref() { - eval_callable_with_values(callback, vec![value], context, values)? - } else { - value - }; - result = values.array_set(result, key, mapped)?; - } - Ok(result) -} - -/// Maps multiple eval arrays with PHP's reindexed and null-padded variadic behavior. -fn eval_array_map_variadic_result( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if arrays.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let callback = if values.is_null(callback)? { - None - } else { - Some(eval_callable_name(callback, values)?) - }; - let mut lengths = Vec::with_capacity(arrays.len()); - let mut max_len = 0; - for array in arrays { - let len = values.array_len(*array)?; - max_len = max_len.max(len); - lengths.push(len); - } - - let mut result = values.array_new(max_len)?; - for position in 0..max_len { - let mut callback_args = Vec::with_capacity(arrays.len()); - for (array, len) in arrays.iter().zip(lengths.iter()) { - let value = if position < *len { - let key = values.array_iter_key(*array, position)?; - values.array_get(*array, key)? - } else { - values.null()? - }; - callback_args.push(value); - } - let mapped = if let Some(callback) = callback.as_deref() { - eval_callable_with_values(callback, callback_args, context, values)? - } else { - eval_array_map_zipped_row(callback_args, values)? - }; - let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, mapped)?; - } - Ok(result) -} - -/// Builds one row for `array_map(null, $a, $b, ...)`. -fn eval_array_map_zipped_row( - values_row: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut row = values.array_new(values_row.len())?; - for (index, value) in values_row.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - row = values.array_set(row, key, value)?; - } - Ok(row) -} - -/// Evaluates PHP `array_reduce()` with an optional initial carry value. -fn eval_builtin_array_reduce( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, callback, initial) = match args { - [array, callback] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - (array, callback, values.null()?) - } - [array, callback, initial] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let initial = eval_expr(initial, context, scope, values)?; - (array, callback, initial) - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_array_reduce_result(array, callback, initial, context, values) -} - -/// Reduces one eval array by invoking a string callback with carry and item cells. -fn eval_array_reduce_result( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - initial: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_name(callback, values)?; - let len = values.array_len(array)?; - let mut carry = initial; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - carry = eval_callable_with_values(&callback, vec![carry, value], context, values)?; - } - Ok(carry) -} - -/// Evaluates PHP `array_walk()` for side-effect callbacks over value/key pairs. -fn eval_builtin_array_walk( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, callback] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - eval_array_walk_result(array, callback, context, values) -} - -/// Walks one eval array by invoking a string callback with value and key cells. -fn eval_array_walk_result( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_name(callback, values)?; - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let _ = eval_callable_with_values(&callback, vec![value, key], context, values)?; - } - values.bool_value(true) -} - -/// Evaluates direct by-reference `settype()` calls and writes the converted cell back. -fn eval_builtin_settype_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (var_name, type_name) = eval_settype_direct_args(args, context, scope, values)?; - let value = visible_scope_cell(context, scope, &var_name).map_or_else(|| values.null(), Ok)?; - let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { - return values.bool_value(false); - }; - for replaced in set_scope_cell( - context, - scope, - var_name, - converted, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - values.bool_value(true) -} - -/// Evaluates and binds direct `settype()` arguments while preserving source order. -fn eval_settype_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(String, RuntimeCellHandle), EvalStatus> { - let mut var_name = None; - let mut type_name = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "var", - 1 => "type", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "var" => { - if var_name.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - var_name = Some(name.clone()); - } - "type" => { - if type_name.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - type_name = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let var_name = var_name.ok_or(EvalStatus::RuntimeFatal)?; - let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; - Ok((var_name, type_name)) -} - -/// Applies the eval-supported `settype()` scalar target conversion. -fn eval_settype_cast_value( - value: RuntimeCellHandle, - type_name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let type_name = values.string_bytes(type_name)?; - let type_name = String::from_utf8_lossy(&type_name).to_ascii_lowercase(); - let converted = match type_name.as_str() { - "bool" | "boolean" => Some(values.cast_bool(value)?), - "float" | "double" => Some(values.cast_float(value)?), - "int" | "integer" => Some(values.cast_int(value)?), - "string" => Some(values.cast_string(value)?), - _ => None, - }; - Ok(converted) -} - -/// Evaluates by-value `settype()` callable dispatch without mutating the source argument. -fn eval_settype_value_result( - value: RuntimeCellHandle, - type_name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - values.warning("settype(): Argument #1 ($var) must be passed by reference, value given")?; - if let Some(converted) = eval_settype_cast_value(value, type_name, values)? { - values.release(converted)?; - return values.bool_value(true); - } - values.bool_value(false) -} - -/// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. -fn eval_builtin_array_pop_shift_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if name == "settype" { - return eval_builtin_settype_call(args, context, scope, values); - } - if matches!(name, "array_push" | "array_unshift") { - return eval_builtin_array_push_unshift_call(name, args, context, scope, values); - } - if name == "array_splice" { - return eval_builtin_array_splice_call(args, context, scope, values); - } - if matches!( - name, - "arsort" - | "asort" - | "krsort" - | "ksort" - | "natcasesort" - | "natsort" - | "rsort" - | "shuffle" - | "sort" - ) { - return eval_builtin_array_sort_call(name, args, context, scope, values); - } - if matches!(name, "uasort" | "uksort" | "usort") { - return eval_builtin_user_sort_call(name, args, context, scope, values); - } - - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(var_name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - let Some(entry) = - scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; - for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates direct by-reference `array_push()` / `array_unshift()` calls. -fn eval_builtin_array_push_unshift_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 || !eval_call_args_are_plain_positional(args) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(var_name) = args[0].value() else { - return Err(EvalStatus::RuntimeFatal); - }; - let mut inserted = Vec::with_capacity(args.len() - 1); - for arg in &args[1..] { - inserted.push(eval_expr(arg.value(), context, scope, values)?); - } - let Some(entry) = - scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; - let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; - for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates direct by-reference `array_splice()` calls and writes back the array. -fn eval_builtin_array_splice_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array_name, offset, length, replacement_arg) = - eval_array_splice_direct_args(args, context, scope, values)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let (removed, replacement) = - eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } - Ok(removed) -} - -/// Evaluates direct by-reference array ordering calls and writes back the array. -fn eval_builtin_array_sort_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let array_name = eval_array_sort_direct_arg(args)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let replacement = eval_array_sort_replacement(name, array, values)?; - let result = values.bool_value(true)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates direct by-reference user-comparator sort calls and writes back the array. -fn eval_builtin_user_sort_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array_name, callback) = eval_user_sort_direct_args(args, context, scope, values)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; - let result = values.bool_value(true)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates and binds direct user-sort arguments while preserving source order. -fn eval_user_sort_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(String, RuntimeCellHandle), EvalStatus> { - let mut array = None; - let mut callback = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "callback", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - array = Some(name.clone()); - } - "callback" => { - if callback.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - callback = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let array = array.ok_or(EvalStatus::RuntimeFatal)?; - let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, callback)) -} - -/// Returns the dynamic callable result for by-value user-comparator sort calls. -fn eval_user_sort_value_result( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; - values.release(replacement)?; - values.bool_value(true) -} - -/// One source array entry used by eval user-comparator sort routines. -struct EvalUserSortEntry { - source_key: RuntimeCellHandle, - value: RuntimeCellHandle, -} - -/// Builds the sorted replacement array for user-comparator sort builtins. -fn eval_user_sort_replacement( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_name(callback, values)?; - let mut entries = eval_user_sort_entries(array, values)?; - eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; - if name == "usort" { - return eval_user_sort_reindex_result(entries, values); - } - eval_user_sort_preserve_key_result(entries, values) -} - -/// Collects source keys and values from one eval array for user sorting. -fn eval_user_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - entries.push(EvalUserSortEntry { source_key, value }); - } - Ok(entries) -} - -/// Sorts entries by repeatedly invoking the PHP comparator callback. -fn eval_user_sort_entries_in_place( - name: &str, - callback: &str, - entries: &mut [EvalUserSortEntry], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for pass in 0..entries.len() { - let upper = entries.len().saturating_sub(pass + 1); - for index in 0..upper { - let comparison = eval_user_sort_compare( - name, - callback, - &entries[index], - &entries[index + 1], - context, - values, - )?; - if comparison > 0 { - entries.swap(index, index + 1); - } - } - } - Ok(()) -} - -/// Invokes one user-sort comparator and returns its integer ordering result. -fn eval_user_sort_compare( - name: &str, - callback: &str, - left: &EvalUserSortEntry, - right: &EvalUserSortEntry, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let args = if name == "uksort" { - vec![left.source_key, right.source_key] - } else { - vec![left.value, right.value] - }; - let result = eval_callable_with_values(callback, args, context, values)?; - eval_int_value(result, values) -} - -/// Builds the reindexed result for `usort()`. -fn eval_user_sort_reindex_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(entries.len())?; - for (index, entry) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, entry.value)?; - } - Ok(result) -} - -/// Builds the key-preserving result for `uksort()` and `uasort()`. -fn eval_user_sort_preserve_key_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(entries.len())?; - for entry in entries { - result = values.array_set(result, entry.source_key, entry.value)?; - } - Ok(result) -} - -/// Extracts the direct variable argument accepted by eval array ordering builtins. -fn eval_array_sort_direct_arg(args: &[EvalCallArg]) -> Result { - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - Ok(name.clone()) -} - -/// Returns the dynamic callable result for by-value array ordering calls. -fn eval_array_sort_value_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - values.bool_value(true) -} - -/// Sort key shape supported by eval's homogeneous array ordering implementation. -#[derive(Clone)] -enum EvalArraySortKey { - Numeric(f64), - Natural(Vec), - String(Vec), -} - -/// One source array entry plus its precomputed ordering key. -struct EvalArraySortEntry { - sort_key: EvalArraySortKey, - source_key: RuntimeCellHandle, - value: RuntimeCellHandle, -} - -/// Builds the sorted replacement array for eval array ordering builtins. -fn eval_array_sort_replacement( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut entries = match name { - "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, - "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, - "natsort" => eval_array_natural_sort_entries(array, false, values)?, - "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, - "shuffle" => return eval_array_shuffle_replacement(array, values), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - entries.sort_by(|left, right| { - let order = eval_array_sort_key_cmp(&left.sort_key, &right.sort_key); - if matches!(name, "arsort" | "krsort" | "rsort") { - order.reverse() - } else { - order - } - }); - - if matches!( - name, - "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" - ) { - return eval_array_preserve_key_sort_result(entries, values); - } - eval_array_reindex_sort_result(entries, values) -} - -/// Builds a shuffled, reindexed replacement array for `shuffle()`. -fn eval_array_shuffle_replacement( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - entries.push(values.array_get(array, source_key)?); - } - - for index in (1..entries.len()).rev() { - let swap_with = (eval_random_u128() % ((index + 1) as u128)) as usize; - entries.swap(index, swap_with); - } - - let mut result = values.array_new(entries.len())?; - for (index, value) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Builds an indexed result for `sort()` / `rsort()` after value ordering. -fn eval_array_reindex_sort_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(entries.len())?; - for (index, entry) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, entry.value)?; - } - Ok(result) -} - -/// Builds a key-preserving associative result after value or key ordering. -fn eval_array_preserve_key_sort_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(entries.len())?; - for entry in entries { - result = values.array_set(result, entry.source_key, entry.value)?; - } - Ok(result) -} - -/// Collects values and comparable value-sort keys from one eval array. -fn eval_array_value_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - let mut expects_numeric = None; - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_sort_key(value, values)?; - let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); - match expects_numeric { - Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), - Some(_) => {} - None => expects_numeric = Some(is_numeric), - } - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Collects values and natural-sort keys from one eval array. -fn eval_array_natural_sort_entries( - array: RuntimeCellHandle, - case_insensitive: bool, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - let mut expects_numeric = None; - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_natural_sort_key(value, case_insensitive, values)?; - let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); - match expects_numeric { - Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), - Some(_) => {} - None => expects_numeric = Some(is_numeric), - } - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Collects values and comparable key-sort keys from one eval array. -fn eval_array_key_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_sort_key(source_key, values)?; - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Converts one scalar eval value into a natural-sort key. -fn eval_array_natural_sort_key( - value: RuntimeCellHandle, - case_insensitive: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => { - Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) - } - EVAL_TAG_STRING => { - let mut bytes = values.string_bytes(value)?; - if case_insensitive { - bytes.make_ascii_lowercase(); - } - Ok(EvalArraySortKey::Natural(bytes)) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts one scalar eval value into a homogeneous sort key. -fn eval_array_sort_key( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => { - Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) - } - EVAL_TAG_STRING => { - let bytes = values.string_bytes(value)?; - match eval_array_numeric_string_sort_key(&bytes) { - Some(value) => Ok(EvalArraySortKey::Numeric(value)), - None => Ok(EvalArraySortKey::String(bytes)), - } - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Parses one PHP numeric string into the numeric sort domain when possible. -fn eval_array_numeric_string_sort_key(bytes: &[u8]) -> Option { - if !eval_is_numeric_string(bytes) { - return None; - } - std::str::from_utf8(bytes).ok()?.parse::().ok() -} - -/// Compares two precomputed eval sort keys. -fn eval_array_sort_key_cmp( - left: &EvalArraySortKey, - right: &EvalArraySortKey, -) -> std::cmp::Ordering { - match (left, right) { - (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { - left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) - } - (EvalArraySortKey::Natural(left), EvalArraySortKey::Natural(right)) => { - eval_natural_bytes_cmp(left, right) - } - (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), - _ => eval_array_sort_key_rank(left).cmp(&eval_array_sort_key_rank(right)), - } -} - -/// Returns a deterministic rank for mixed key-sort domains. -fn eval_array_sort_key_rank(key: &EvalArraySortKey) -> u8 { - match key { - EvalArraySortKey::Numeric(_) => 0, - EvalArraySortKey::Natural(_) => 1, - EvalArraySortKey::String(_) => 2, - } -} - -/// Compares byte strings with a small PHP-style natural ordering. -fn eval_natural_bytes_cmp(left: &[u8], right: &[u8]) -> std::cmp::Ordering { - let mut left_index = 0; - let mut right_index = 0; - while left_index < left.len() && right_index < right.len() { - if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { - let order = eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); - if order != std::cmp::Ordering::Equal { - return order; - } - continue; - } - - let order = left[left_index].cmp(&right[right_index]); - if order != std::cmp::Ordering::Equal { - return order; - } - left_index += 1; - right_index += 1; - } - left.len().cmp(&right.len()) -} - -/// Compares two natural-sort digit runs and advances both byte indexes past them. -fn eval_natural_digit_run_cmp( - left: &[u8], - left_index: &mut usize, - right: &[u8], - right_index: &mut usize, -) -> std::cmp::Ordering { - let left_start = *left_index; - let right_start = *right_index; - while *left_index < left.len() && left[*left_index].is_ascii_digit() { - *left_index += 1; - } - while *right_index < right.len() && right[*right_index].is_ascii_digit() { - *right_index += 1; - } - - let left_digits = &left[left_start..*left_index]; - let right_digits = &right[right_start..*right_index]; - let left_trimmed = eval_trim_leading_zeroes(left_digits); - let right_trimmed = eval_trim_leading_zeroes(right_digits); - left_trimmed - .len() - .cmp(&right_trimmed.len()) - .then_with(|| left_trimmed.cmp(right_trimmed)) - .then_with(|| left_digits.len().cmp(&right_digits.len())) -} - -/// Drops leading zero bytes while keeping one zero for an all-zero digit run. -fn eval_trim_leading_zeroes(digits: &[u8]) -> &[u8] { - let trimmed = digits - .iter() - .position(|digit| *digit != b'0') - .map_or(&digits[digits.len().saturating_sub(1)..], |index| { - &digits[index..] - }); - if trimmed.is_empty() { - digits - } else { - trimmed - } -} - -/// Evaluates and binds direct `array_splice()` arguments while preserving source order. -fn eval_array_splice_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut array = None; - let mut offset = None; - let mut length = None; - let mut replacement = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "offset", - 2 => "length", - 3 => "replacement", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - array = Some(name.clone()); - } - "offset" => { - if offset.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - offset = Some(eval_expr(arg.value(), context, scope, values)?); - } - "length" => { - if length.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - length = Some(eval_expr(arg.value(), context, scope, values)?); - } - "replacement" => { - if replacement.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - replacement = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let array = array.ok_or(EvalStatus::RuntimeFatal)?; - let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, offset, length, replacement)) -} - -/// Returns the removed elements that `array_splice()` would produce without mutating the source. -fn eval_array_splice_value_result( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; - eval_array_splice_removed(array, start, end, values) -} - -/// Builds both removed and replacement arrays for direct `array_splice()` write-back. -fn eval_array_splice_removed_and_replacement( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - replacement: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; - let removed = eval_array_splice_removed(array, start, end, values)?; - let inserted = eval_array_splice_insert_values(replacement, values)?; - let replacement = eval_array_splice_replacement(array, start, end, &inserted, values)?; - Ok((removed, replacement)) -} - -/// Converts splice offset and length cells into bounded source positions. -fn eval_array_splice_bounds( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(usize, usize), EvalStatus> { - let len = values.array_len(array)?; - let offset = eval_int_value(offset, values)?; - let start = eval_slice_start(len, offset)?; - let end = match length { - Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { - eval_slice_end(len, start, eval_int_value(length, values)?)? - } - _ => len, - }; - Ok((start, end)) -} - -/// Builds the reindexed/string-key-preserving removed array returned by `array_splice()`. -fn eval_array_splice_removed( - array: RuntimeCellHandle, - start: usize, - end: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = end.saturating_sub(start); - if eval_array_range_keys_are_int(array, start, end, values)? { - let mut result = values.array_new(len)?; - let mut target = 0_i64; - for position in start..end { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - return Ok(result); - } - - let mut result = values.assoc_new(len)?; - let mut next_int_key = 0_i64; - for position in start..end { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Expands the optional `array_splice()` replacement value into inserted values. -fn eval_array_splice_insert_values( - replacement: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(replacement) = replacement else { - return Ok(Vec::new()); - }; - if !matches!( - values.type_tag(replacement)?, - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC - ) { - return Ok(vec![replacement]); - } - - let len = values.array_len(replacement)?; - let mut inserted = Vec::with_capacity(len); - for position in 0..len { - let key = values.array_iter_key(replacement, position)?; - inserted.push(values.array_get(replacement, key)?); - } - Ok(inserted) -} - -/// Builds the source replacement after removing the requested splice range. -fn eval_array_splice_replacement( - array: RuntimeCellHandle, - start: usize, - end: usize, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let new_len = len - .saturating_sub(end.saturating_sub(start)) - .checked_add(inserted.len()) - .ok_or(EvalStatus::RuntimeFatal)?; - if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { - let mut result = values.array_new(new_len)?; - let mut target = 0_i64; - for position in 0..start { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - for value in inserted { - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, *value)?; - } - for position in end..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - return Ok(result); - } - - let mut result = values.assoc_new(new_len)?; - let mut next_int_key = 0_i64; - for position in 0..start { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - for value in inserted { - let target_key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, *value)?; - } - for position in end..len { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every key in one source position range is integer-shaped. -fn eval_array_range_keys_are_int( - array: RuntimeCellHandle, - start: usize, - end: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in start..end { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Returns true when every key outside the removed splice range is integer-shaped. -fn eval_array_splice_remaining_keys_are_int( - array: RuntimeCellHandle, - start: usize, - end: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - if (start..end).contains(&position) { - continue; - } - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. -fn eval_array_pop_shift_value_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(array)?; - if len == 0 { - return values.null(); - } - let position = match name { - "array_pop" => len - 1, - "array_shift" => 0, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let key = values.array_iter_key(array, position)?; - values.array_get(array, key) -} - -/// Builds the return value plus replacement array for direct pop/shift write-back. -fn eval_array_pop_shift_replacement( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let len = values.array_len(array)?; - let tag = values.type_tag(array)?; - if len == 0 { - let replacement = match tag { - EVAL_TAG_ARRAY => values.array_new(0)?, - EVAL_TAG_ASSOC => values.assoc_new(0)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - return Ok((values.null()?, replacement)); - } - - let removed_position = match name { - "array_pop" => len - 1, - "array_shift" => 0, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let removed_key = values.array_iter_key(array, removed_position)?; - let removed_value = values.array_get(array, removed_key)?; - let replacement = match tag { - EVAL_TAG_ARRAY => { - eval_array_pop_shift_indexed_replacement(array, removed_position, len, values)? - } - EVAL_TAG_ASSOC => { - eval_array_pop_shift_assoc_replacement(name, array, removed_position, len, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - Ok((removed_value, replacement)) -} - -/// Rebuilds an indexed array after removing one position and reindexing values. -fn eval_array_pop_shift_indexed_replacement( - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(len.saturating_sub(1))?; - let mut target = 0_i64; - for position in 0..len { - if position == removed_position { - continue; - } - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after pop/shift, preserving PHP key behavior. -fn eval_array_pop_shift_assoc_replacement( - name: &str, - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - if name == "array_shift" - && eval_array_remaining_keys_are_int(array, removed_position, len, values)? - { - return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); - } - - let mut result = values.assoc_new(len.saturating_sub(1))?; - let mut next_int_key = 0_i64; - for position in 0..len { - if position == removed_position { - continue; - } - let source_key = values.array_iter_key(array, position)?; - let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every remaining key is an integer after removing one element. -fn eval_array_remaining_keys_are_int( - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - if position == removed_position { - continue; - } - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Returns the resulting element count for by-value push/unshift dynamic calls. -fn eval_array_push_unshift_count_result( - array: RuntimeCellHandle, - inserted_len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let total = values - .array_len(array)? - .checked_add(inserted_len) - .ok_or(EvalStatus::RuntimeFatal)?; - let total = i64::try_from(total).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(total) -} - -/// Builds the replacement array for direct push/unshift write-back. -fn eval_array_push_unshift_replacement( - name: &str, - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match (name, values.type_tag(array)?) { - ("array_push", EVAL_TAG_ARRAY) => { - eval_array_push_indexed_replacement(array, inserted, values) - } - ("array_push", EVAL_TAG_ASSOC) => { - eval_array_push_assoc_replacement(array, inserted, values) - } - ("array_unshift", EVAL_TAG_ARRAY) => { - eval_array_unshift_indexed_replacement(array, inserted, values) - } - ("array_unshift", EVAL_TAG_ASSOC) => { - eval_array_unshift_assoc_replacement(array, inserted, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Rebuilds an indexed array after appending values. -fn eval_array_push_indexed_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len.saturating_add(inserted.len()))?; - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let target_key = - values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, target_key, value)?; - } - for (offset, value) in inserted.iter().copied().enumerate() { - let position = len.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; - let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after appending values at PHP's next integer keys. -fn eval_array_push_assoc_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; - let mut next_key = 0_i64; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? == EVAL_TAG_INT { - next_key = next_key.max(eval_int_value(key, values)?.saturating_add(1)); - } - let value = values.array_get(array, key)?; - result = values.array_set(result, key, value)?; - } - for value in inserted.iter().copied() { - let key = values.int(next_key)?; - next_key = next_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an indexed array after prepending values and reindexing the original tail. -fn eval_array_unshift_indexed_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len.saturating_add(inserted.len()))?; - let mut target = 0_i64; - for value in inserted.iter().copied() { - let key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after prepending values and reindexing integer keys. -fn eval_array_unshift_assoc_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - if eval_array_keys_are_int(array, len, values)? { - return eval_array_unshift_indexed_replacement(array, inserted, values); - } - - let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; - let mut next_int_key = 0_i64; - for value in inserted.iter().copied() { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every key in the array is integer-shaped. -fn eval_array_keys_are_int( - array: RuntimeCellHandle, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Evaluates PHP `array_filter()` for null and string-callback filtering modes. -fn eval_builtin_array_filter( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array] => { - let array = eval_expr(array, context, scope, values)?; - eval_array_filter_result(array, None, None, context, values) - } - [array, callback] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - eval_array_filter_result(array, Some(callback), None, context, values) - } - [array, callback, mode] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_array_filter_result(array, Some(callback), Some(mode), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Filters eval array entries through PHP truthiness or a string callback. -fn eval_array_filter_result( - array: RuntimeCellHandle, - callback: Option, - mode: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = match callback { - Some(callback) if !values.is_null(callback)? => Some(eval_callable_name(callback, values)?), - _ => None, - }; - let mode = match mode { - Some(mode) => eval_array_filter_mode_value(mode, values)?, - None => EVAL_ARRAY_FILTER_USE_VALUE, - }; - - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let keep = if let Some(callback) = callback.as_deref() { - let args = eval_array_filter_callback_args(mode, key, value)?; - let result = eval_callable_with_values(callback, args, context, values)?; - values.truthy(result)? - } else { - values.truthy(value)? - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Reads and validates the optional `array_filter()` callback mode. -fn eval_array_filter_mode_value( - mode: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = eval_int_value(mode, values)?; - match mode { - EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { - Ok(mode) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds the callback argument list for one `array_filter()` entry. -fn eval_array_filter_callback_args( - mode: i64, - key: RuntimeCellHandle, - value: RuntimeCellHandle, -) -> Result, EvalStatus> { - match mode { - EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), - EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), - EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. -fn eval_builtin_array_chunk( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, length] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_chunk_result(array, length, values) -} - -/// Builds an `array_chunk()` result as nested reindexed arrays. -fn eval_array_chunk_result( - array: RuntimeCellHandle, - length: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let chunk_size = eval_int_value(length, values)?; - if chunk_size <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; - let len = values.array_len(array)?; - let chunk_count = len.div_ceil(chunk_size); - let mut result = values.array_new(chunk_count)?; - - for chunk_index in 0..chunk_count { - let start = chunk_index * chunk_size; - let end = usize::min(start + chunk_size, len); - let mut chunk = values.array_new(end - start)?; - for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let value = values.array_get(array, source_key)?; - let target_index = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_index = values.int(target_index)?; - chunk = values.array_set(chunk, target_index, value)?; - } - let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let result_key = values.int(result_key)?; - result = values.array_set(result, result_key, chunk)?; - } - - Ok(result) -} - -/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. -fn eval_builtin_array_slice( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array, offset] => { - let array = eval_expr(array, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_array_slice_result(array, offset, None, values) - } - [array, offset, length] => { - let array = eval_expr(array, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_slice_result(array, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds an `array_slice()` result with PHP offset and length bounds. -fn eval_array_slice_result( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let offset = eval_int_value(offset, values)?; - let start = eval_slice_start(len, offset)?; - let end = match length { - Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { - eval_slice_end(len, start, eval_int_value(length, values)?)? - } - _ => len, - }; - - let mut result = values.array_new(end.saturating_sub(start))?; - for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; - result = values.array_set(result, target_key, source_value)?; - } - Ok(result) -} - -/// Converts a PHP array-slice offset into a bounded source position. -fn eval_slice_start(len: usize, offset: i64) -> Result { - if offset >= 0 { - let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; - return Ok(usize::min(offset, len)); - } - - let tail = offset - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - Ok(len.saturating_sub(tail)) -} - -/// Converts a PHP array-slice length into a bounded exclusive end position. -fn eval_slice_end(len: usize, start: usize, length: i64) -> Result { - if length >= 0 { - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - return Ok(usize::min(start.saturating_add(length), len)); - } - - let tail = length - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - Ok(usize::max(start, len.saturating_sub(tail))) -} - -/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. -fn eval_builtin_array_pad( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, length, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_pad_result(array, length, value, values) -} - -/// Builds an `array_pad()` result by copying values and padding left or right. -fn eval_array_pad_result( - array: RuntimeCellHandle, - length: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let target = eval_int_value(length, values)?; - let target_len = target - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - let result_len = usize::max(len, target_len); - let pad_count = result_len.saturating_sub(len); - let mut result = values.array_new(result_len)?; - let mut output_index = 0usize; - - if target < 0 { - let (padded, next_index) = - eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; - result = padded; - output_index = next_index; - } - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; - result = values.array_set(result, target_key, source_value)?; - output_index += 1; - } - - if target > 0 { - result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; - } - - Ok(result) -} - -/// Appends the same pad value at consecutive indexed positions in an array result. -fn eval_array_pad_append_repeated( - mut array: RuntimeCellHandle, - start_index: usize, - count: usize, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, usize), EvalStatus> { - let mut next_index = start_index; - for _ in 0..count { - let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - array = values.array_set(array, key, value)?; - next_index += 1; - } - Ok((array, next_index)) -} - -/// Evaluates PHP `array_flip()` over one eval array expression. -fn eval_builtin_array_flip( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_flip_result(array, values) -} - -/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. -fn eval_array_flip_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { - continue; - } - result = values.array_set(result, value, key)?; - } - Ok(result) -} - -/// Evaluates PHP `array_unique()` over one eval array expression. -fn eval_builtin_array_unique( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_unique_result(array, values) -} - -/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. -fn eval_array_unique_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut seen = Vec::>::with_capacity(len); - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let unique_key = values.string_bytes(value)?; - if seen.iter().any(|seen_key| seen_key == &unique_key) { - continue; - } - seen.push(unique_key); - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP array projection builtins over one eval array expression. -fn eval_builtin_array_projection( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_projection_result(name, array, values) -} - -/// Builds the indexed result array for `array_keys()` or `array_values()`. -fn eval_array_projection_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = match name { - "array_keys" => key, - "array_values" => values.array_get(array, key)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let index = values.int(position as i64)?; - result = values.array_set(result, index, value)?; - } - Ok(result) -} - -/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. -fn eval_builtin_iterator_apply( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [iterator, callback] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, values)?; - eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) - } - [iterator, callback, callback_args] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, values)?; - let callback_args = eval_expr(callback_args, context, scope, values)?; - let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; - eval_iterator_apply_result(iterator, &callback, callback_args, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts the optional `iterator_apply()` callback-args value into call arguments. -fn eval_iterator_apply_arg_values( - args: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.is_null(args)? { - return Ok(Vec::new()); - } - if !values.is_array_like(args)? { - return Err(EvalStatus::RuntimeFatal); - } - eval_array_call_arg_values(args, values) -} - -/// Applies a callback to each valid position of an eval-supported Traversable object. -fn eval_iterator_apply_result( - iterator: RuntimeCellHandle, - callback: &EvaluatedCallable, - callback_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(iterator)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let count = match eval_iterator_apply_iterator_object( - iterator, - callback, - &callback_args, - context, - values, - ) { - Ok(count) => count, - Err(EvalStatus::UnsupportedConstruct) => { - let iterator = values.method_call(iterator, "getiterator", Vec::new())?; - eval_iterator_apply_iterator_object( - iterator, - callback, - &callback_args, - context, - values, - )? - } - Err(err) => return Err(err), - }; - values.int(count) -} - -/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. -fn eval_iterator_apply_iterator_object( - iterator: RuntimeCellHandle, - callback: &EvaluatedCallable, - callback_args: &[EvaluatedCallArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let _ = values.method_call(iterator, "rewind", Vec::new())?; - let mut count = 0_i64; - loop { - let valid = values.method_call(iterator, "valid", Vec::new())?; - if !values.truthy(valid)? { - return Ok(count); - } - count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - let result = eval_evaluated_callable_with_call_array_args( - callback, - callback_args.to_vec(), - context, - values, - )?; - if !values.truthy(result)? { - return Ok(count); - } - let _ = values.method_call(iterator, "next", Vec::new())?; - } -} - -/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. -fn eval_builtin_iterator_count( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [iterator] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let iterator = eval_expr(iterator, context, scope, values)?; - eval_iterator_count_result(iterator, values) -} - -/// Returns the element count for eval-supported array iterator inputs. -fn eval_iterator_count_result( - iterator: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(iterator)?; - values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. -fn eval_builtin_iterator_to_array( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [iterator] => { - let iterator = eval_expr(iterator, context, scope, values)?; - eval_iterator_to_array_result(iterator, true, values) - } - [iterator, preserve_keys] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; - let preserve_keys = values.truthy(preserve_keys)?; - eval_iterator_to_array_result(iterator, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Copies eval-supported array iterator inputs into a PHP array result. -fn eval_iterator_to_array_result( - iterator: RuntimeCellHandle, - preserve_keys: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - if preserve_keys { - return eval_array_copy_preserve_keys(iterator, values); - } - eval_array_projection_result("array_values", iterator, values) -} - -/// Copies one array-like eval value while preserving iteration keys and order. -fn eval_array_copy_preserve_keys( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_reverse()` over an eval array expression. -fn eval_builtin_array_reverse( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array] => { - let array = eval_expr(array, context, scope, values)?; - eval_array_reverse_result(array, false, values) - } - [array, preserve_keys] => { - let array = eval_expr(array, context, scope, values)?; - let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; - let preserve_keys = values.truthy(preserve_keys)?; - eval_array_reverse_result(array, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds an `array_reverse()` result while preserving PHP key rules. -fn eval_array_reverse_result( - array: RuntimeCellHandle, - preserve_keys: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut keys = Vec::with_capacity(len); - let mut has_string_key = false; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; - keys.push(key); - } - - let mut result = if preserve_keys || has_string_key { - values.assoc_new(len)? - } else { - values.array_new(len)? - }; - let mut next_numeric_key = 0_i64; - - for key in keys.into_iter().rev() { - let value = values.array_get(array, key)?; - let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { - key - } else { - let key = values.int(next_numeric_key)?; - next_numeric_key += 1; - key - }; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_key_exists()` over a key and array expression. -fn eval_builtin_array_key_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [key, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let key = eval_expr(key, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - values.array_key_exists(key, array) -} - -/// Evaluates PHP array search builtins over a needle and haystack expression. -fn eval_builtin_array_search( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [needle, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let needle = eval_expr(needle, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_array_search_result(name, needle, array, values) -} - -/// Searches an eval array with PHP's default loose comparison semantics. -fn eval_array_search_result( - name: &str, - needle: RuntimeCellHandle, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let equal = values.compare(EvalBinOp::LooseEq, needle, value)?; - if values.truthy(equal)? { - return match name { - "in_array" => values.bool_value(true), - "array_search" => Ok(key), - _ => Err(EvalStatus::UnsupportedConstruct), - }; - } - } - match name { - "in_array" => values.bool_value(false), - "array_search" => values.bool_value(false), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP value-set array builtins over two eval array expressions. -fn eval_builtin_array_value_set( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_value_set_result(name, left, right, values) -} - -/// Builds `array_diff()` or `array_intersect()` using PHP's default string comparison mode. -fn eval_array_value_set_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let right_len = values.array_len(right)?; - let mut right_values = Vec::with_capacity(right_len); - for position in 0..right_len { - let key = values.array_iter_key(right, position)?; - let value = values.array_get(right, key)?; - right_values.push(values.string_bytes(value)?); - } - - let mut result = values.assoc_new(left_len)?; - for position in 0..left_len { - let key = values.array_iter_key(left, position)?; - let value = values.array_get(left, key)?; - let comparable = values.string_bytes(value)?; - let found = right_values - .iter() - .any(|right_value| right_value == &comparable); - let keep = match name { - "array_diff" => !found, - "array_intersect" => found, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Evaluates PHP key-set array builtins over two eval array expressions. -fn eval_builtin_array_key_set( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_key_set_result(name, left, right, values) -} - -/// Builds `array_diff_key()` or `array_intersect_key()` by testing first-array keys. -fn eval_array_key_set_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let mut result = values.assoc_new(left_len)?; - for position in 0..left_len { - let key = values.array_iter_key(left, position)?; - let value = values.array_get(left, key)?; - let exists = values.array_key_exists(key, right)?; - let found = values.truthy(exists)?; - let keep = match name { - "array_diff_key" => !found, - "array_intersect_key" => found, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Evaluates PHP `array_rand()` over one eval array expression. -fn eval_builtin_array_rand( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_rand_result(array, values) -} - -/// Returns a valid random key from a non-empty eval array. -fn eval_array_rand_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - if len == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let position = eval_random_position(len); - values.array_iter_key(array, position) -} - -/// Chooses a pseudo-random array position within `[0, len)`. -fn eval_random_position(len: usize) -> usize { - (eval_random_u128() % (len as u128)) as usize -} - -/// Produces a process-local pseudo-random word for non-cryptographic eval builtins. -fn eval_random_u128() -> u128 { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - let counter = u128::from(EVAL_RANDOM_COUNTER.fetch_add(1, Ordering::Relaxed)); - let pid = u128::from(std::process::id()); - let mut value = nanos ^ (counter.wrapping_mul(0x9e37_79b9_7f4a_7c15)) ^ (pid << 64); - value ^= value >> 30; - value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); - value ^= value >> 27; - value = value.wrapping_mul(0x94d0_49bb_1331_11eb); - value ^ (value >> 31) -} - -/// Evaluates PHP `rand()` and `mt_rand()` over zero args or an inclusive range. -fn eval_builtin_rand( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_rand_result(None, None, values), - [min, max] => { - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_rand_result(Some(min), Some(max), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `random_int()` over an inclusive integer range. -fn eval_builtin_random_int( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [min, max] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_random_int_result(min, max, values) -} - -/// Returns one non-cryptographic random integer using PHP's inclusive range rules. -fn eval_rand_result( - min: Option, - max: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let (min, max) = match (min, max) { - (None, None) => (0, i64::from(i32::MAX)), - (Some(min), Some(max)) => (eval_int_value(min, values)?, eval_int_value(max, values)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let low = min.min(max); - let high = min.max(max); - let width = (i128::from(high) - i128::from(low) + 1) as u128; - let offset = (eval_random_u128() % width) as i128; - let sampled = i128::from(low) + offset; - let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(sampled) -} - -/// Returns one eval `random_int()` value in the inclusive range `[min, max]`. -fn eval_random_int_result( - min: RuntimeCellHandle, - max: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let min = eval_int_value(min, values)?; - let max = eval_int_value(max, values)?; - if min > max { - return Err(EvalStatus::RuntimeFatal); - } - let width = (i128::from(max) - i128::from(min) + 1) as u128; - let offset = (eval_random_u128() % width) as i128; - let sampled = i128::from(min) + offset; - let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(sampled) -} - -/// Evaluates PHP `range()` over integer-compatible start and end expressions. -fn eval_builtin_range( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [start, end] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let start = eval_expr(start, context, scope, values)?; - let end = eval_expr(end, context, scope, values)?; - eval_range_result(start, end, values) -} - -/// Builds an inclusive ascending or descending integer `range()` result. -fn eval_range_result( - start: RuntimeCellHandle, - end: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let start = eval_int_value(start, values)?; - let end = eval_int_value(end, values)?; - let distance = if start <= end { - end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? - } else { - start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? - }; - let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; - let step = if start <= end { 1_i64 } else { -1_i64 }; - let mut current = start; - let mut result = values.array_new(count)?; - - for index in 0..count { - let key = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - let value = values.int(current)?; - result = values.array_set(result, key, value)?; - if index + 1 < count { - current = current.checked_add(step).ok_or(EvalStatus::RuntimeFatal)?; - } - } - Ok(result) -} - -/// Evaluates PHP `array_merge()` over two array expressions. -fn eval_builtin_array_merge( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_merge_result(left, right, values) -} - -/// Builds an `array_merge()` result with PHP numeric reindexing and string-key overwrites. -fn eval_array_merge_result( - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let right_len = values.array_len(right)?; - let capacity = left_len - .checked_add(right_len) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut result = values.assoc_new(capacity)?; - let mut next_numeric_key = 0_i64; - result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; - eval_array_merge_append_operand(result, right, &mut next_numeric_key, values) -} - -/// Appends one source array to an `array_merge()` result using PHP key handling. -fn eval_array_merge_append_operand( - mut result: RuntimeCellHandle, - source: RuntimeCellHandle, - next_numeric_key: &mut i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(source)?; - for position in 0..len { - let source_key = values.array_iter_key(source, position)?; - let source_value = values.array_get(source, source_key)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_STRING { - source_key - } else { - let target_key = values.int(*next_numeric_key)?; - *next_numeric_key = (*next_numeric_key) - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - target_key - }; - result = values.array_set(result, target_key, source_value)?; - } - Ok(result) -} - -/// Evaluates PHP `explode()` over separator and string expressions. -fn eval_builtin_explode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [separator, string] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let separator = eval_expr(separator, context, scope, values)?; - let string = eval_expr(string, context, scope, values)?; - eval_explode_result(separator, string, values) -} - -/// Splits one PHP byte string into an indexed array using a non-empty separator. -fn eval_explode_result( - separator: RuntimeCellHandle, - string: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let separator = values.string_bytes(separator)?; - if separator.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let string = values.string_bytes(string)?; - let mut result = values.array_new(0)?; - let mut start = 0; - let mut index = 0_i64; - while let Some(found) = eval_find_subslice(&string, &separator, start) { - result = eval_push_explode_segment(result, index, &string[start..found], values)?; - start = found + separator.len(); - index += 1; - } - eval_push_explode_segment(result, index, &string[start..], values) -} - -/// Appends one split segment to an indexed `explode()` result array. -fn eval_push_explode_segment( - array: RuntimeCellHandle, - index: i64, - segment: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(index)?; - let value = values.string_bytes_value(segment)?; - values.array_set(array, key, value) -} - -/// Finds `needle` inside `haystack` starting from one byte offset. -fn eval_find_subslice(haystack: &[u8], needle: &[u8], start: usize) -> Option { - haystack - .get(start..)? - .windows(needle.len()) - .position(|window| window == needle) - .map(|position| position + start) -} - -/// Evaluates PHP `implode()` over separator and array expressions. -fn eval_builtin_implode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [separator, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let separator = eval_expr(separator, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_implode_result(separator, array, values) -} - -/// Joins array values in eval iteration order using PHP string conversion. -fn eval_implode_result( - separator: RuntimeCellHandle, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !values.is_array_like(array)? { - return Err(EvalStatus::RuntimeFatal); - } - let separator = values.string_bytes(separator)?; - let len = values.array_len(array)?; - let mut output = Vec::new(); - for position in 0..len { - if position > 0 { - output.extend_from_slice(&separator); - } - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let value = values.string_bytes(value)?; - output.extend_from_slice(&value); - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `ceil(...)` over one eval expression. -fn eval_builtin_ceil( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.ceil(value) -} - -/// Evaluates PHP's `floor(...)` over one eval expression. -fn eval_builtin_floor( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.floor(value) -} - -/// Evaluates PHP's zero-argument `pi()` builtin. -fn eval_builtin_pi( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI) -} - -/// Evaluates PHP's `pow(...)` over two eval expressions. -fn eval_builtin_pow( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - values.pow(left, right) -} - -/// Evaluates PHP's `round(...)` over one value and an optional precision expression. -fn eval_builtin_round( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - values.round(value, None) - } - [value, precision] => { - let value = eval_expr(value, context, scope, values)?; - let precision = eval_expr(precision, context, scope, values)?; - values.round(value, Some(precision)) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `number_format(...)` over one number and optional separators. -fn eval_builtin_number_format( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_number_format_result(value, None, None, None, values) - } - [value, decimals] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - eval_number_format_result(value, Some(decimals), None, None, values) - } - [value, decimals, decimal_separator] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; - eval_number_format_result(value, Some(decimals), Some(decimal_separator), None, values) - } - [value, decimals, decimal_separator, thousands_separator] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; - let thousands_separator = eval_expr(thousands_separator, context, scope, values)?; - eval_number_format_result( - value, - Some(decimals), - Some(decimal_separator), - Some(thousands_separator), - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Formats one PHP numeric value with grouped thousands and fixed decimals. -fn eval_number_format_result( - value: RuntimeCellHandle, - decimals: Option, - decimal_separator: Option, - thousands_separator: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_float_value(value, values)?; - let decimals = match decimals { - Some(decimals) => eval_int_value(decimals, values)?, - None => 0, - }; - let decimal_separator = match decimal_separator { - Some(separator) => values.string_bytes(separator)?, - None => b".".to_vec(), - }; - let thousands_separator = match thousands_separator { - Some(separator) => values.string_bytes(separator)?, - None => b",".to_vec(), - }; - let output = - eval_number_format_bytes(value, decimals, &decimal_separator, &thousands_separator)?; - values.string_bytes_value(&output) -} - -/// Evaluates direct positional `sprintf()` or `printf()` calls in source order. -fn eval_builtin_sprintf_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_sprintf_like_result(name, &evaluated_args, values) -} - -/// Evaluates direct positional `vsprintf()` or `vprintf()` calls in source order. -fn eval_builtin_vsprintf_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_vsprintf_like_result(name, &evaluated_args, values) -} - -/// Evaluates direct positional `sscanf()` calls in source order. -fn eval_builtin_sscanf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let input = eval_expr(&args[0], context, scope, values)?; - let format = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - eval_expr(arg, context, scope, values)?; - } - eval_sscanf_result(input, format, values) -} - -/// Dispatches already evaluated `sprintf()` or `printf()` arguments. -fn eval_sprintf_like_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "sprintf" => eval_sprintf_result(evaluated_args, values), - "printf" => eval_printf_result(evaluated_args, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Dispatches already evaluated `vsprintf()` or `vprintf()` arguments. -fn eval_vsprintf_like_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "vsprintf" => eval_vsprintf_result(evaluated_args, values), - "vprintf" => eval_vprintf_result(evaluated_args, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Parses one string through the eval `sscanf()` subset and returns an indexed array. -fn eval_sscanf_result( - input: RuntimeCellHandle, - format: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let input = values.string_bytes(input)?; - let format = values.string_bytes(format)?; - let matches = eval_sscanf_matches(&input, &format); - let mut result = values.array_new(matches.len())?; - for (index, matched) in matches.iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.string_bytes_value(matched)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Extracts `%d`, `%f`, `%s`, and `%%` matches with the same subset as native `sscanf()`. -fn eval_sscanf_matches(input: &[u8], format: &[u8]) -> Vec> { - let mut matches = Vec::new(); - let mut input_index = 0; - let mut format_index = 0; - - while format_index < format.len() { - if format[format_index] != b'%' { - if input_index >= input.len() || input[input_index] != format[format_index] { - break; - } - input_index += 1; - format_index += 1; - continue; - } - - format_index += 1; - if format_index >= format.len() { - break; - } - - match format[format_index] { - b'%' => { - if input_index >= input.len() || input[input_index] != b'%' { - break; - } - input_index += 1; - } - b'd' => matches.push(eval_sscanf_scan_int(input, &mut input_index)), - b'f' => matches.push(eval_sscanf_scan_float(input, &mut input_index)), - b's' => matches.push(eval_sscanf_scan_word(input, &mut input_index)), - _ => {} - } - format_index += 1; - } - - matches -} - -/// Scans the native `sscanf()` `%d` subset as a matched byte slice. -fn eval_sscanf_scan_int(input: &[u8], input_index: &mut usize) -> Vec { - let start = *input_index; - if input.get(*input_index) == Some(&b'-') { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - input[start..*input_index].to_vec() -} - -/// Scans the native `sscanf()` `%f` subset as a matched byte slice. -fn eval_sscanf_scan_float(input: &[u8], input_index: &mut usize) -> Vec { - let start = *input_index; - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'+' | b'-')) - { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - if input.get(*input_index) == Some(&b'.') { - *input_index += 1; - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - } - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'e' | b'E')) - { - *input_index += 1; - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'+' | b'-')) - { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - } - input[start..*input_index].to_vec() -} - -/// Scans the native `sscanf()` `%s` subset as a non-space byte word. -fn eval_sscanf_scan_word(input: &[u8], input_index: &mut usize) -> Vec { - let start = *input_index; - while input - .get(*input_index) - .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\n')) - { - *input_index += 1; - } - input[start..*input_index].to_vec() -} - -/// Formats `sprintf()` arguments and returns the resulting PHP string. -fn eval_sprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((format, format_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - values.string_bytes_value(&output) -} - -/// Formats `printf()` arguments, echoes the result, and returns its byte count. -fn eval_printf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((format, format_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.int(len) -} - -/// Formats `vsprintf()` array arguments and returns the resulting PHP string. -fn eval_vsprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let format_args = eval_sprintf_argument_array_values(*array, values)?; - let output = eval_sprintf_bytes(&format, &format_args, values)?; - values.string_bytes_value(&output) -} - -/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. -fn eval_vprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let format_args = eval_sprintf_argument_array_values(*array, values)?; - let output = eval_sprintf_bytes(&format, &format_args, values)?; - let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.int(len) -} - -/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. -fn eval_sprintf_argument_array_values( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !values.is_array_like(array)? { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(array)?; - let mut args = Vec::with_capacity(len); - for position in 0..len { - let key = values.array_iter_key(array, position)?; - args.push(values.array_get(array, key)?); - } - Ok(args) -} - -/// Formats one printf-style byte string through eval runtime value coercions. -fn eval_sprintf_bytes( - format: &[u8], - args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut output = Vec::new(); - let mut index = 0; - let mut arg_index = 0; - while index < format.len() { - if format[index] != b'%' { - output.push(format[index]); - index += 1; - continue; - } - index += 1; - if index >= format.len() { - break; - } - if format[index] == b'%' { - output.push(b'%'); - index += 1; - continue; - } - - let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; - index = next_index; - let Some(arg) = args.get(arg_index).copied() else { - return Err(EvalStatus::RuntimeFatal); - }; - arg_index += 1; - let bytes = eval_format_sprintf_arg(spec, arg, values)?; - output.extend_from_slice(&bytes); - } - Ok(output) -} - -/// Parses flags, width, precision, and terminal type for one format specifier. -fn eval_parse_sprintf_spec( - format: &[u8], - mut index: usize, -) -> Result<(EvalSprintfSpec, usize), EvalStatus> { - let mut spec = EvalSprintfSpec { - left_align: false, - force_sign: false, - space_sign: false, - zero_pad: false, - alternate: false, - width: None, - precision: None, - specifier: 0, - }; - while index < format.len() { - match format[index] { - b'-' => spec.left_align = true, - b'+' => spec.force_sign = true, - b' ' => spec.space_sign = true, - b'0' => spec.zero_pad = true, - b'#' => spec.alternate = true, - _ => break, - } - index += 1; - } - let (width, next_index) = eval_parse_sprintf_number(format, index)?; - spec.width = width; - index = next_index; - if index < format.len() && format[index] == b'.' { - let (precision, next_index) = eval_parse_sprintf_number(format, index + 1)?; - spec.precision = Some(precision.unwrap_or(0)); - index = next_index; - } - if index >= format.len() { - return Err(EvalStatus::RuntimeFatal); - } - spec.specifier = format[index]; - Ok((spec, index + 1)) -} - -/// Parses an unsigned decimal number from a format specifier component. -fn eval_parse_sprintf_number( - format: &[u8], - mut index: usize, -) -> Result<(Option, usize), EvalStatus> { - let start = index; - let mut value = 0usize; - while index < format.len() && format[index].is_ascii_digit() { - value = value - .checked_mul(10) - .and_then(|value| value.checked_add(usize::from(format[index] - b'0'))) - .ok_or(EvalStatus::RuntimeFatal)?; - index += 1; - } - if index == start { - Ok((None, index)) - } else { - Ok((Some(value), index)) - } -} - -/// Formats one runtime value according to a parsed eval sprintf specifier. -fn eval_format_sprintf_arg( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - match spec.specifier { - b's' => eval_format_sprintf_string(spec, value, values), - b'f' | b'e' | b'g' => eval_format_sprintf_float(spec, value, values), - b'c' => eval_format_sprintf_char(spec, value, values), - _ => eval_format_sprintf_int(spec, value, values), - } -} - -/// Formats a `%s` operand after PHP string coercion. -fn eval_format_sprintf_string( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut bytes = values.string_bytes(value)?; - if let Some(precision) = spec.precision { - bytes.truncate(precision); - } - Ok(eval_sprintf_apply_width(bytes, spec, false)) -} - -/// Formats an integer-like operand for decimal, unsigned, hex, and octal specifiers. -fn eval_format_sprintf_int( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_int_value(value, values)?; - let mut output = Vec::new(); - match spec.specifier { - b'u' => { - let digits = eval_sprintf_precision_pad((value as u64).to_string().into_bytes(), spec); - output.extend_from_slice(&digits); - } - b'x' | b'X' => { - let unsigned = value as u64; - if spec.alternate && unsigned != 0 { - output.extend_from_slice(if spec.specifier == b'X' { b"0X" } else { b"0x" }); - } - let digits = if spec.specifier == b'X' { - format!("{unsigned:X}").into_bytes() - } else { - format!("{unsigned:x}").into_bytes() - }; - output.extend_from_slice(&eval_sprintf_precision_pad(digits, spec)); - } - b'o' => { - let unsigned = value as u64; - let mut digits = eval_sprintf_precision_pad(format!("{unsigned:o}").into_bytes(), spec); - if spec.alternate && !digits.starts_with(b"0") { - output.push(b'0'); - } - output.append(&mut digits); - } - _ => { - let value = value as i128; - let magnitude = if value < 0 { - (-value) as u128 - } else { - value as u128 - }; - if value < 0 { - output.push(b'-'); - } else if spec.force_sign { - output.push(b'+'); - } else if spec.space_sign { - output.push(b' '); - } - let digits = eval_sprintf_precision_pad(magnitude.to_string().into_bytes(), spec); - output.extend_from_slice(&digits); - } - } - Ok(eval_sprintf_apply_width(output, spec, true)) -} - -/// Formats a `%c` operand as the low byte of its integer value. -fn eval_format_sprintf_char( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_int_value(value, values)?; - Ok(eval_sprintf_apply_width(vec![value as u8], spec, false)) -} - -/// Formats a floating-point operand for the eval printf-family subset. -fn eval_format_sprintf_float( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_float_value(value, values)?; - let precision = spec.precision.unwrap_or(6); - let mut output = if value.is_nan() { - b"NAN".to_vec() - } else if value.is_infinite() { - b"INF".to_vec() - } else { - match spec.specifier { - b'e' => format!("{value:.precision$e}").into_bytes(), - b'g' => format!("{value:.precision$}").into_bytes(), - _ => format!("{value:.precision$}").into_bytes(), - } - }; - if value.is_sign_negative() && !output.starts_with(b"-") { - output.insert(0, b'-'); - } else if value.is_sign_positive() && value.is_finite() { - if spec.force_sign { - output.insert(0, b'+'); - } else if spec.space_sign { - output.insert(0, b' '); - } - } - Ok(eval_sprintf_apply_width(output, spec, true)) -} - -/// Applies integer precision by left-padding digits with zeros. -fn eval_sprintf_precision_pad(mut digits: Vec, spec: EvalSprintfSpec) -> Vec { - if matches!(spec.precision, Some(0)) && digits == b"0" { - digits.clear(); - return digits; - } - let Some(precision) = spec.precision else { - return digits; - }; - if digits.len() >= precision { - return digits; - } - let mut output = vec![b'0'; precision - digits.len()]; - output.append(&mut digits); - output -} - -/// Applies field width and alignment to one formatted eval sprintf replacement. -fn eval_sprintf_apply_width(mut bytes: Vec, spec: EvalSprintfSpec, numeric: bool) -> Vec { - let Some(width) = spec.width else { - return bytes; - }; - if bytes.len() >= width { - return bytes; - } - let pad_len = width - bytes.len(); - if spec.left_align { - bytes.extend(std::iter::repeat_n(b' ', pad_len)); - return bytes; - } - if numeric && spec.zero_pad && spec.precision.is_none() { - let prefix_len = eval_sprintf_zero_pad_prefix_len(&bytes); - let mut output = Vec::with_capacity(width); - output.extend_from_slice(&bytes[..prefix_len]); - output.extend(std::iter::repeat_n(b'0', pad_len)); - output.extend_from_slice(&bytes[prefix_len..]); - return output; - } - let mut output = Vec::with_capacity(width); - output.extend(std::iter::repeat_n(b' ', pad_len)); - output.append(&mut bytes); - output -} - -/// Returns the sign and alternate-prefix length that should precede zero padding. -fn eval_sprintf_zero_pad_prefix_len(bytes: &[u8]) -> usize { - let mut prefix_len = usize::from(matches!(bytes.first(), Some(b'+' | b'-' | b' '))); - if bytes.len() >= prefix_len + 2 - && bytes[prefix_len] == b'0' - && matches!(bytes[prefix_len + 1], b'x' | b'X') - { - prefix_len += 2; - } - prefix_len -} - -/// Converts one eval value to PHP float and returns the scalar payload. -fn eval_float_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.cast_float(value)?; - let bytes = values.string_bytes(value)?; - std::str::from_utf8(&bytes) - .map_err(|_| EvalStatus::RuntimeFatal)? - .parse::() - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Produces PHP `number_format()` bytes for finite scalar values. -fn eval_number_format_bytes( - value: f64, - decimals: i64, - decimal_separator: &[u8], - thousands_separator: &[u8], -) -> Result, EvalStatus> { - if !value.is_finite() { - return Ok(value.to_string().into_bytes()); - } - let decimals = decimals.clamp(-308, 308); - let display_decimals = decimals.max(0) as usize; - let abs_value = value.abs(); - let scaled = if decimals >= 0 { - let scale = 10_f64.powi(decimals as i32); - (abs_value * scale).round() - } else { - let scale = 10_f64.powi((-decimals) as i32); - (abs_value / scale).round() * scale - }; - if scaled > (u128::MAX as f64) { - return Err(EvalStatus::RuntimeFatal); - } - let scaled = scaled as u128; - let scale = 10_u128 - .checked_pow(display_decimals as u32) - .ok_or(EvalStatus::RuntimeFatal)?; - let integer = if display_decimals == 0 { - scaled - } else { - scaled / scale - }; - let fraction = if display_decimals == 0 { - 0 - } else { - scaled % scale - }; - - let mut output = Vec::new(); - if value.is_sign_negative() && scaled != 0 { - output.push(b'-'); - } - eval_append_grouped_decimal(&mut output, integer, thousands_separator); - if display_decimals > 0 { - output.extend_from_slice(decimal_separator); - let fraction = format!("{fraction:0display_decimals$}"); - output.extend_from_slice(fraction.as_bytes()); - } - Ok(output) -} - -/// Appends one unsigned decimal integer with optional three-digit grouping. -fn eval_append_grouped_decimal(output: &mut Vec, value: u128, separator: &[u8]) { - let digits = value.to_string(); - if separator.is_empty() { - output.extend_from_slice(digits.as_bytes()); - return; - } - let first_group = match digits.len() % 3 { - 0 => 3, - len => len, - }; - output.extend_from_slice(&digits.as_bytes()[..first_group]); - let mut index = first_group; - while index < digits.len() { - output.extend_from_slice(separator); - output.extend_from_slice(&digits.as_bytes()[index..index + 3]); - index += 3; - } -} - -/// Evaluates PHP's `sqrt(...)` over one eval expression. -fn eval_builtin_sqrt( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.sqrt(value) -} - -/// Evaluates PHP's `strrev(...)` over one eval expression. -fn eval_builtin_strrev( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.strrev(value) -} - -/// Evaluates PHP's `chr(...)` over one eval expression. -fn eval_builtin_chr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_chr_result(value, values) -} - -/// Converts one eval value to a PHP integer and returns the low byte as a string. -fn eval_chr_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - values.string_bytes_value(&[value as u8]) -} - -/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. -fn eval_builtin_str_repeat( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value, times] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let times = eval_expr(times, context, scope, values)?; - eval_str_repeat_result(value, times, values) -} - -/// Repeats one PHP string byte sequence according to a PHP-cast integer count. -fn eval_str_repeat_result( - value: RuntimeCellHandle, - times: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let times = eval_int_value(times, values)?; - if times < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; - let capacity = bytes - .len() - .checked_mul(times) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(capacity); - for _ in 0..times { - output.extend_from_slice(&bytes); - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. -fn eval_builtin_str_replace( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [search, replace, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let search = eval_expr(search, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_str_replace_result(name, search, replace, subject, values) -} - -/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. -fn eval_str_replace_result( - name: &str, - search: RuntimeCellHandle, - replace: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let search = values.string_bytes(search)?; - let replace = values.string_bytes(replace)?; - let subject = values.string_bytes(subject)?; - if search.is_empty() { - return values.string_bytes_value(&subject); - } - - let mut output = Vec::with_capacity(subject.len()); - let mut start = 0; - while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { - output.extend_from_slice(&subject[start..found]); - output.extend_from_slice(&replace); - start = found + search.len(); - } - output.extend_from_slice(&subject[start..]); - values.string_bytes_value(&output) -} - -/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. -fn eval_find_replace_match( - name: &str, - subject: &[u8], - search: &[u8], - start: usize, -) -> Result, EvalStatus> { - match name { - "str_replace" => Ok(eval_find_subslice(subject, search, start)), - "str_ireplace" => Ok(subject - .get(start..) - .and_then(|tail| { - tail.windows(search.len()) - .position(|window| window.eq_ignore_ascii_case(search)) - }) - .map(|position| position + start)), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. -fn eval_builtin_str_pad( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, length] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_str_pad_result(value, length, None, None, values) - } - [value, length, pad_string] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let pad_string = eval_expr(pad_string, context, scope, values)?; - eval_str_pad_result(value, length, Some(pad_string), None, values) - } - [value, length, pad_string, pad_type] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let pad_string = eval_expr(pad_string, context, scope, values)?; - let pad_type = eval_expr(pad_type, context, scope, values)?; - eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Pads one byte string to a PHP target length using cyclic pad bytes. -fn eval_str_pad_result( - value: RuntimeCellHandle, - length: RuntimeCellHandle, - pad_string: Option, - pad_type: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let target_length = eval_int_value(length, values)?; - let Ok(target_length) = usize::try_from(target_length) else { - return values.string_bytes_value(&bytes); - }; - if target_length <= bytes.len() { - return values.string_bytes_value(&bytes); - } - - let pad_string = match pad_string { - Some(pad_string) => values.string_bytes(pad_string)?, - None => b" ".to_vec(), - }; - if pad_string.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let pad_type = match pad_type { - Some(pad_type) => eval_int_value(pad_type, values)?, - None => 1, - }; - let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; - let capacity = bytes - .len() - .checked_add(left_pad) - .and_then(|size| size.checked_add(right_pad)) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(capacity); - eval_append_repeated_pad(&mut output, &pad_string, left_pad); - output.extend_from_slice(&bytes); - eval_append_repeated_pad(&mut output, &pad_string, right_pad); - values.string_bytes_value(&output) -} - -/// Splits a `str_pad()` pad budget into left and right byte counts. -fn eval_str_pad_sides(pad_budget: usize, pad_type: i64) -> Result<(usize, usize), EvalStatus> { - match pad_type { - 0 => Ok((pad_budget, 0)), - 1 => Ok((0, pad_budget)), - 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Appends `count` bytes by cycling through the provided non-empty pad string. -fn eval_append_repeated_pad(output: &mut Vec, pad_string: &[u8], count: usize) { - for index in 0..count { - output.push(pad_string[index % pad_string.len()]); - } -} - -/// Evaluates PHP `str_split(...)` over one string and optional chunk length. -fn eval_builtin_str_split( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_str_split_result(value, None, values) - } - [value, length] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_str_split_result(value, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. -fn eval_str_split_result( - value: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let length = match length { - Some(length) => eval_int_value(length, values)?, - None => 1, - }; - if length <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut result = values.array_new(0)?; - for (index, chunk) in bytes.chunks(length).enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string_bytes_value(chunk)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. -fn eval_builtin_nl2br( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_nl2br_result(value, true, values) - } - [value, use_xhtml] => { - let value = eval_expr(value, context, scope, values)?; - let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; - let use_xhtml = values.truthy(use_xhtml)?; - eval_nl2br_result(value, use_xhtml, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. -fn eval_nl2br_result( - value: RuntimeCellHandle, - use_xhtml: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let br = if use_xhtml { - b"
".as_slice() - } else { - b"
".as_slice() - }; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - let byte = bytes[index]; - if byte == b'\r' || byte == b'\n' { - output.extend_from_slice(br); - output.push(byte); - if index + 1 < bytes.len() - && ((byte == b'\r' && bytes[index + 1] == b'\n') - || (byte == b'\n' && bytes[index + 1] == b'\r')) - { - output.push(bytes[index + 1]); - index += 2; - continue; - } - } else { - output.push(byte); - } - index += 1; - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. -fn eval_builtin_substr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, offset] => { - let value = eval_expr(value, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_substr_result(value, offset, None, values) - } - [value, offset, length] => { - let value = eval_expr(value, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_substr_result(value, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Slices a PHP byte string using PHP `substr()` offset and length rules. -fn eval_substr_result( - value: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = eval_int_value(offset, values)?; - let start = if offset < 0 { - (total + offset).max(0) - } else { - offset.min(total) - }; - let end = match length { - None => total, - Some(length) if values.is_null(length)? => total, - Some(length) => { - let length = eval_int_value(length, values)?; - if length < 0 { - (total + length).max(0) - } else { - start.saturating_add(length).min(total) - } - } - }; - let end = end.max(start); - let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; - let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string_bytes_value(&bytes[start..end]) -} - -/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. -fn eval_builtin_substr_replace( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, replace, offset] => { - let value = eval_expr(value, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_substr_replace_result(value, replace, offset, None, values) - } - [value, replace, offset, length] => { - let value = eval_expr(value, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_substr_replace_result(value, replace, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. -fn eval_substr_replace_result( - value: RuntimeCellHandle, - replace: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let replacement = values.string_bytes(replace)?; - let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = eval_int_value(offset, values)?; - let start = if offset < 0 { - (total + offset).max(0) - } else { - offset.min(total) - }; - let end = match length { - None => total, - Some(length) if values.is_null(length)? => total, - Some(length) => { - let length = eval_int_value(length, values)?; - if length < 0 { - (total + length).max(start) - } else { - start.saturating_add(length).min(total) - } - } - }; - let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; - let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(bytes.len() + replacement.len()); - output.extend_from_slice(&bytes[..start]); - output.extend_from_slice(&replacement); - output.extend_from_slice(&bytes[end..]); - values.string_bytes_value(&output) -} - -/// Evaluates eval HTML entity encode/decode builtins over one string expression. -fn eval_builtin_html_entity( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_html_entity_result(name, value, values) -} - -/// Applies the eval-supported HTML entity transform for one PHP string value. -fn eval_html_entity_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), - "html_entity_decode" => eval_html_entity_decode_result(value, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Encodes the HTML-special byte characters covered by elephc's static helper. -fn eval_htmlspecialchars_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - for byte in bytes { - match byte { - b'&' => output.extend_from_slice(b"&"), - b'<' => output.extend_from_slice(b"<"), - b'>' => output.extend_from_slice(b">"), - b'"' => output.extend_from_slice(b"""), - b'\'' => output.extend_from_slice(b"'"), - _ => output.push(byte), - } - } - values.string_bytes_value(&output) -} - -/// Decodes one pass of the HTML entities emitted by the eval/static encoders. -fn eval_html_entity_decode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'&' { - if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { - output.push(decoded); - index += width; - continue; - } - } - output.push(bytes[index]); - index += 1; - } - values.string_bytes_value(&output) -} - -/// Returns the decoded byte and consumed width for one supported HTML entity. -fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { - for (entity, decoded) in [ - (b"<".as_slice(), b'<'), - (b">".as_slice(), b'>'), - (b""".as_slice(), b'"'), - (b"'".as_slice(), b'\''), - (b"'".as_slice(), b'\''), - (b"&".as_slice(), b'&'), - ] { - if bytes.starts_with(entity) { - return Some((decoded, entity.len())); - } - } - None -} - -/// Evaluates PHP URL encode builtins over one eval string expression. -fn eval_builtin_url_encode( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_url_encode_result(name, value, values) -} - -/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. -fn eval_url_encode_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - const HEX: &[u8; 16] = b"0123456789ABCDEF"; - for byte in bytes { - if eval_url_encode_keeps_byte(name, byte)? { - output.push(byte); - } else if name == "urlencode" && byte == b' ' { - output.push(b'+'); - } else { - output.push(b'%'); - output.push(HEX[(byte >> 4) as usize]); - output.push(HEX[(byte & 0x0f) as usize]); - } - } - values.string_bytes_value(&output) -} - -/// Returns whether a byte remains unescaped for the selected PHP URL encoder. -fn eval_url_encode_keeps_byte(name: &str, byte: u8) -> Result { - let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); - match name { - "urlencode" => Ok(common), - "rawurlencode" => Ok(common || byte == b'~'), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP URL decode builtins over one eval string expression. -fn eval_builtin_url_decode( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_url_decode_result(name, value, values) -} - -/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. -fn eval_url_decode_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let plus_to_space = match name { - "urldecode" => true, - "rawurldecode" => false, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'+' && plus_to_space { - output.push(b' '); - index += 1; - } else if bytes[index] == b'%' && index + 2 < bytes.len() { - if let (Some(high), Some(low)) = ( - eval_hex_nibble(bytes[index + 1]), - eval_hex_nibble(bytes[index + 2]), - ) { - output.push((high << 4) | low); - index += 3; - continue; - } - output.push(bytes[index]); - index += 1; - } else { - output.push(bytes[index]); - index += 1; - } - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP `ctype_*` predicates over one eval string expression. -fn eval_builtin_ctype( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_ctype_result(name, value, values) -} - -/// Returns the PHP boolean result for one ASCII `ctype_*` byte-string check. -fn eval_ctype_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut matches = !bytes.is_empty(); - for byte in bytes { - if !eval_ctype_byte_matches(name, byte)? { - matches = false; - break; - } - } - values.bool_value(matches) -} - -/// Checks one byte against the selected PHP ASCII character class. -fn eval_ctype_byte_matches(name: &str, byte: u8) -> Result { - match name { - "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), - "ctype_digit" => Ok(byte.is_ascii_digit()), - "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), - "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `crc32(...)` over one eval string expression. -fn eval_builtin_crc32( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_crc32_result(value, values) -} - -/// Computes PHP's non-negative CRC-32 integer over one converted byte string. -fn eval_crc32_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.int(i64::from(eval_crc32_bytes(&bytes))) -} - -/// Evaluates one-shot PHP hash digest builtins over eval expressions. -fn eval_builtin_hash_one_shot( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_hash_one_shot_result(name, &evaluated_args, values) -} - -/// Computes the result for one-shot PHP hash digest builtins from evaluated args. -fn eval_hash_one_shot_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "md5" | "sha1" => { - let (data, binary) = match evaluated_args { - [data] => (*data, false), - [data, binary] => (*data, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let data = values.string_bytes(data)?; - eval_hash_digest_result(name.as_bytes(), &data, binary, values) - } - "hash" => { - let (algo, data, binary) = match evaluated_args { - [algo, data] => (*algo, *data, false), - [algo, data, binary] => (*algo, *data, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let algo = values.string_bytes(algo)?; - let data = values.string_bytes(data)?; - eval_hash_digest_result(&algo, &data, binary, values) - } - "hash_file" => { - let (algo, filename, binary) = match evaluated_args { - [algo, filename] => (*algo, *filename, false), - [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_hash_file_result(algo, filename, binary, values) - } - "hash_hmac" => { - let (algo, data, key, binary) = match evaluated_args { - [algo, data, key] => (*algo, *data, *key, false), - [algo, data, key, binary] => (*algo, *data, *key, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let algo = values.string_bytes(algo)?; - let data = values.string_bytes(data)?; - let key = values.string_bytes(key)?; - eval_hash_hmac_result(&algo, &data, &key, binary, values) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Reads a local file and returns its PHP hash digest or false when it cannot be read. -fn eval_hash_file_result( - algo: RuntimeCellHandle, - filename: RuntimeCellHandle, - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let algo = values.string_bytes(algo)?; - let path = eval_path_string(filename, values)?; - match std::fs::read(path) { - Ok(data) => eval_hash_digest_result(&algo, &data, binary, values), - Err(_) => values.bool_value(false), - } -} - -/// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. -fn eval_hash_digest_result( - algo: &[u8], - data: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let raw = eval_crypto_hash(algo, data)?; - eval_format_digest_result(&raw, binary, values) -} - -/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. -fn eval_hash_hmac_result( - algo: &[u8], - data: &[u8], - key: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let raw = eval_crypto_hmac(algo, data, key)?; - eval_format_digest_result(&raw, binary, values) -} - -/// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. -fn eval_crypto_hash(algo: &[u8], data: &[u8]) -> Result, EvalStatus> { - let mut output = [0_u8; 64]; - let len = unsafe { - elephc_crypto::elephc_crypto_hash( - algo.as_ptr(), - algo.len(), - data.as_ptr(), - data.len(), - output.as_mut_ptr(), - ) - }; - eval_crypto_digest_bytes(len, &output) -} - -/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. -fn eval_crypto_hmac(algo: &[u8], data: &[u8], key: &[u8]) -> Result, EvalStatus> { - let mut output = [0_u8; 64]; - let len = unsafe { - elephc_crypto::elephc_crypto_hmac( - algo.as_ptr(), - algo.len(), - key.as_ptr(), - key.len(), - data.as_ptr(), - data.len(), - output.as_mut_ptr(), - ) - }; - eval_crypto_digest_bytes(len, &output) -} - -/// Converts a crypto ABI digest length into an owned digest byte vector. -fn eval_crypto_digest_bytes(len: isize, output: &[u8; 64]) -> Result, EvalStatus> { - let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; - if len > output.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(output[..len].to_vec()) -} - -/// Formats a raw digest using PHP's `$binary` flag convention. -fn eval_format_digest_result( - raw: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - if binary { - return values.string_bytes_value(raw); - } - values.string(&eval_lower_hex_bytes(raw)) -} - -/// Evaluates PHP `hash_algos()` with no arguments. -fn eval_builtin_hash_algos( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values) -} - -/// Builds the indexed array returned by eval `hash_algos()`. -fn eval_hash_algos_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_HASH_ALGOS, values) -} - -/// Builds one indexed PHP array from a static string slice. -fn eval_static_string_array_result( - items: &[&str], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(items.len())?; - for (index, item) in items.iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string(item)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `spl_classes()` with no arguments. -fn eval_builtin_spl_classes( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values) -} - -/// Builds the static class-name list returned by eval `spl_classes()`. -fn eval_spl_classes_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) -} - -/// Evaluates PHP stream introspection list builtins with no arguments. -fn eval_builtin_stream_introspection( - name: &str, - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_introspection_result(name, values) -} - -/// Builds the static list returned by one eval stream introspection builtin. -fn eval_stream_introspection_result( - name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let items = match name { - "stream_get_filters" => EVAL_STREAM_FILTERS, - "stream_get_transports" => EVAL_STREAM_TRANSPORTS, - "stream_get_wrappers" => EVAL_STREAM_WRAPPERS, - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_static_string_array_result(items, values) -} - -/// Evaluates PHP `time()` with no arguments. -fn eval_builtin_time( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values) -} - -/// Returns the current Unix timestamp as a boxed PHP integer. -fn eval_time_result(values: &mut impl RuntimeValueOps) -> Result { - values.int(eval_current_unix_timestamp()?) -} - -/// Returns the current Unix timestamp as an integer payload. -fn eval_current_unix_timestamp() -> Result { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| EvalStatus::RuntimeFatal)? - .as_secs(); - i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. -fn eval_builtin_date( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [format] => { - let format = eval_expr(format, context, scope, values)?; - eval_date_result(format, None, values) - } - [format, timestamp] => { - let format = eval_expr(format, context, scope, values)?; - let timestamp = eval_expr(timestamp, context, scope, values)?; - eval_date_result(format, Some(timestamp), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. -fn eval_date_result( - format: RuntimeCellHandle, - timestamp: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let format = values.string_bytes(format)?; - let timestamp = match timestamp { - Some(timestamp) => eval_int_value(timestamp, values)?, - None => eval_current_unix_timestamp()?, - }; - let tm = eval_localtime(timestamp)?; - let output = eval_format_date_bytes(&format, &tm, timestamp)?; - values.string_bytes_value(&output) -} - -/// Converts one Unix timestamp to local broken-down time through libc. -fn eval_localtime(timestamp: i64) -> Result { - let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; - let mut tm = MaybeUninit::::uninit(); - let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; - if result.is_null() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(unsafe { tm.assume_init() }) -} - -/// Applies PHP `date()` tokens to one local broken-down timestamp. -fn eval_format_date_bytes( - format: &[u8], - tm: &libc::tm, - timestamp: i64, -) -> Result, EvalStatus> { - let mut output = Vec::new(); - let mut escaped = false; - for byte in format { - if escaped { - output.push(*byte); - escaped = false; - continue; - } - if *byte == b'\\' { - escaped = true; - continue; - } - eval_push_date_token(&mut output, *byte, tm, timestamp)?; - } - if escaped { - output.push(b'\\'); - } - Ok(output) -} - -/// Appends the expansion for one PHP `date()` token, or the token literal. -fn eval_push_date_token( - output: &mut Vec, - token: u8, - tm: &libc::tm, - timestamp: i64, -) -> Result<(), EvalStatus> { - match token { - b'Y' => eval_push_padded_number(output, i64::from(tm.tm_year) + 1900, 4), - b'm' => eval_push_padded_number(output, i64::from(tm.tm_mon) + 1, 2), - b'd' => eval_push_padded_number(output, i64::from(tm.tm_mday), 2), - b'H' => eval_push_padded_number(output, i64::from(tm.tm_hour), 2), - b'i' => eval_push_padded_number(output, i64::from(tm.tm_min), 2), - b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), - b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), - b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), - b'D' => output - .extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), - b'M' => { - output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) - } - b'N' => { - let weekday = tm.tm_wday; - let iso_weekday = if weekday == 0 { 7 } else { weekday }; - output.extend_from_slice(iso_weekday.to_string().as_bytes()); - } - b'j' => output.extend_from_slice(tm.tm_mday.to_string().as_bytes()), - b'n' => output.extend_from_slice((tm.tm_mon + 1).to_string().as_bytes()), - b'G' => output.extend_from_slice(tm.tm_hour.to_string().as_bytes()), - b'g' => { - let hour = tm.tm_hour % 12; - let hour = if hour == 0 { 12 } else { hour }; - output.extend_from_slice(hour.to_string().as_bytes()); - } - b'A' => output.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }), - b'a' => output.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }), - b'U' => output.extend_from_slice(timestamp.to_string().as_bytes()), - _ => output.push(token), - } - Ok(()) -} - -/// Returns a checked month index for PHP `date()` name tables. -fn eval_tm_month_index(tm: &libc::tm) -> Result { - let index = usize::try_from(tm.tm_mon).map_err(|_| EvalStatus::RuntimeFatal)?; - if index >= EVAL_MONTH_NAMES.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(index) -} - -/// Returns a checked weekday index for PHP `date()` name tables. -fn eval_tm_weekday_index(tm: &libc::tm) -> Result { - let index = usize::try_from(tm.tm_wday).map_err(|_| EvalStatus::RuntimeFatal)?; - if index >= EVAL_WEEKDAY_NAMES.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(index) -} - -/// Appends one zero-padded decimal value with the requested minimum width. -fn eval_push_padded_number(output: &mut Vec, value: i64, width: usize) { - output.extend_from_slice(format!("{value:0width$}").as_bytes()); -} - -/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. -fn eval_builtin_mktime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hour, minute, second, month, day, year] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hour = eval_expr(hour, context, scope, values)?; - let minute = eval_expr(minute, context, scope, values)?; - let second = eval_expr(second, context, scope, values)?; - let month = eval_expr(month, context, scope, values)?; - let day = eval_expr(day, context, scope, values)?; - let year = eval_expr(year, context, scope, values)?; - eval_mktime_result(hour, minute, second, month, day, year, values) -} - -/// Converts PHP date components to a local Unix timestamp through libc `mktime`. -fn eval_mktime_result( - hour: RuntimeCellHandle, - minute: RuntimeCellHandle, - second: RuntimeCellHandle, - month: RuntimeCellHandle, - day: RuntimeCellHandle, - year: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let timestamp = eval_mktime_timestamp( - eval_int_cell_as_c_int(hour, values)?, - eval_int_cell_as_c_int(minute, values)?, - eval_int_cell_as_c_int(second, values)?, - eval_int_cell_as_c_int(month, values)?, - eval_int_cell_as_c_int(day, values)?, - eval_int_cell_as_c_int(year, values)?, - )?; - values.int(timestamp) -} - -/// Converts local date components into a Unix timestamp through libc `mktime`. -fn eval_mktime_timestamp( - hour: libc::c_int, - minute: libc::c_int, - second: libc::c_int, - month: libc::c_int, - day: libc::c_int, - year: libc::c_int, -) -> Result { - let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; - tm.tm_hour = hour; - tm.tm_min = minute; - tm.tm_sec = second; - tm.tm_mon = month - 1; - tm.tm_mday = day; - tm.tm_year = year - 1900; - tm.tm_isdst = -1; - let timestamp = unsafe { libc::mktime(&mut tm) }; - i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. -fn eval_int_cell_as_c_int( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP `strtotime(datetime)` for eval's supported date-string subset. -fn eval_builtin_strtotime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [datetime] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let datetime = eval_expr(datetime, context, scope, values)?; - eval_strtotime_result(datetime, values) -} - -/// Parses one eval `strtotime()` input and boxes the resulting timestamp. -fn eval_strtotime_result( - datetime: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(datetime)?; - let timestamp = eval_strtotime_bytes(&bytes)?; - values.int(timestamp) -} - -/// Parses eval's supported `strtotime()` strings into local Unix timestamps. -fn eval_strtotime_bytes(bytes: &[u8]) -> Result { - let bytes = eval_trim_ascii_whitespace(bytes); - if bytes.eq_ignore_ascii_case(b"now") { - return eval_current_unix_timestamp(); - } - let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { - return Ok(-1); - }; - eval_mktime_timestamp(hour, minute, second, month, day, year) -} - -/// Trims ASCII whitespace from both ends of one byte slice. -fn eval_trim_ascii_whitespace(bytes: &[u8]) -> &[u8] { - let mut start = 0; - let mut end = bytes.len(); - while start < end && bytes[start].is_ascii_whitespace() { - start += 1; - } - while end > start && bytes[end - 1].is_ascii_whitespace() { - end -= 1; - } - &bytes[start..end] -} - -/// Parses fixed-width ISO date and datetime forms supported by eval `strtotime()`. -fn eval_parse_iso_datetime( - bytes: &[u8], -) -> Option<( - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, -)> { - if bytes.len() != 10 && bytes.len() != 16 && bytes.len() != 19 { - return None; - } - if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { - return None; - } - let year = eval_parse_fixed_digits(bytes, 0, 4)?; - let month = eval_parse_fixed_digits(bytes, 5, 2)?; - let day = eval_parse_fixed_digits(bytes, 8, 2)?; - let (hour, minute, second) = if bytes.len() == 10 { - (0, 0, 0) - } else { - if !matches!(bytes.get(10), Some(b' ') | Some(b'T') | Some(b't')) { - return None; - } - if bytes.get(13) != Some(&b':') { - return None; - } - let hour = eval_parse_fixed_digits(bytes, 11, 2)?; - let minute = eval_parse_fixed_digits(bytes, 14, 2)?; - let second = if bytes.len() == 19 { - if bytes.get(16) != Some(&b':') { - return None; - } - eval_parse_fixed_digits(bytes, 17, 2)? - } else { - 0 - }; - (hour, minute, second) - }; - if !(1..=12).contains(&month) - || !(1..=31).contains(&day) - || !(0..=23).contains(&hour) - || !(0..=59).contains(&minute) - || !(0..=59).contains(&second) - { - return None; - } - Some((year, month, day, hour, minute, second)) -} - -/// Parses a fixed-width decimal field as a libc-compatible integer. -fn eval_parse_fixed_digits(bytes: &[u8], start: usize, len: usize) -> Option { - let end = start.checked_add(len)?; - let field = bytes.get(start..end)?; - let mut value: libc::c_int = 0; - for byte in field { - if !byte.is_ascii_digit() { - return None; - } - value = value.checked_mul(10)?; - value = value.checked_add(libc::c_int::from(byte - b'0'))?; - } - Some(value) -} - -/// Evaluates PHP `microtime()` with an optional ignored argument. -fn eval_builtin_microtime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_microtime_result(values), - [as_float] => { - let _ = eval_expr(as_float, context, scope, values)?; - eval_microtime_result(values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the current Unix timestamp with microsecond precision as a boxed float. -fn eval_microtime_result( - values: &mut impl RuntimeValueOps, -) -> Result { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_err(|_| EvalStatus::RuntimeFatal)?; - let seconds = timestamp.as_secs() as f64; - let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; - values.float(seconds + micros) -} - -/// Evaluates PHP `sleep($seconds)` over one eval expression. -fn eval_builtin_sleep( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [seconds] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let seconds = eval_expr(seconds, context, scope, values)?; - eval_sleep_result(seconds, values) -} - -/// Sleeps for a non-negative number of seconds and returns PHP's remaining-seconds value. -fn eval_sleep_result( - seconds: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let seconds = eval_int_value(seconds, values)?; - let seconds = u64::try_from(seconds).map_err(|_| EvalStatus::RuntimeFatal)?; - std::thread::sleep(std::time::Duration::from_secs(seconds)); - values.int(0) -} - -/// Evaluates PHP `usleep($microseconds)` over one eval expression. -fn eval_builtin_usleep( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [microseconds] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let microseconds = eval_expr(microseconds, context, scope, values)?; - eval_usleep_result(microseconds, values) -} - -/// Sleeps for a non-negative number of microseconds and returns PHP null. -fn eval_usleep_result( - microseconds: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let microseconds = eval_int_value(microseconds, values)?; - let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; - std::thread::sleep(std::time::Duration::from_micros(microseconds)); - values.null() -} - -/// Evaluates PHP `phpversion()` with no arguments. -fn eval_builtin_phpversion( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_phpversion_result(values) -} - -/// Returns the root elephc package version as a boxed PHP string. -fn eval_phpversion_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.string(eval_compiler_php_version()) -} - -/// Reads the root package version from the workspace manifest used by native `phpversion()`. -fn eval_compiler_php_version() -> &'static str { - let mut in_package = false; - for line in EVAL_ROOT_CARGO_TOML.lines() { - let line = line.trim(); - if line == "[package]" { - in_package = true; - continue; - } - if in_package && line.starts_with('[') { - break; - } - if in_package { - if let Some(value) = line.strip_prefix("version = ") { - return value.trim_matches('"'); - } - } - } - env!("CARGO_PKG_VERSION") -} - -/// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. -fn eval_builtin_php_uname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_php_uname_result(None, values), - [mode] => { - let mode = eval_expr(mode, context, scope, values)?; - eval_php_uname_result(Some(mode), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Reads the local uname fields and formats the PHP `php_uname()` mode result. -fn eval_php_uname_result( - mode: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = match mode { - Some(mode) => { - let bytes = values.string_bytes(mode)?; - let [mode] = bytes.as_slice() else { - return Err(EvalStatus::RuntimeFatal); - }; - *mode - } - None => b'a', - }; - - let mut utsname = std::mem::MaybeUninit::::zeroed(); - let status = unsafe { - // libc writes all uname fields into the stack-owned utsname buffer. - libc::uname(utsname.as_mut_ptr()) - }; - if status != 0 { - return values.string(""); - } - let utsname = unsafe { - // `uname` succeeded, so libc initialized the full `utsname` structure. - utsname.assume_init() - }; - let sysname = eval_uname_field_bytes(&utsname.sysname); - let nodename = eval_uname_field_bytes(&utsname.nodename); - let release = eval_uname_field_bytes(&utsname.release); - let version = eval_uname_field_bytes(&utsname.version); - let machine = eval_uname_field_bytes(&utsname.machine); - - match mode { - b'a' => { - let mut output = Vec::new(); - for field in [&sysname, &nodename, &release, &version, &machine] { - if !output.is_empty() { - output.push(b' '); - } - output.extend_from_slice(field); - } - values.string_bytes_value(&output) - } - b's' => values.string_bytes_value(&sysname), - b'n' => values.string_bytes_value(&nodename), - b'r' => values.string_bytes_value(&release), - b'v' => values.string_bytes_value(&version), - b'm' => values.string_bytes_value(&machine), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. -fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { - let length = field - .iter() - .position(|byte| *byte == 0) - .unwrap_or(field.len()); - field[..length].iter().map(|byte| *byte as u8).collect() -} - -/// Evaluates PHP `getcwd()` with no arguments. -fn eval_builtin_getcwd( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_getcwd_result(values) -} - -/// Returns the process current working directory as a boxed PHP string. -fn eval_getcwd_result(values: &mut impl RuntimeValueOps) -> Result { - let cwd = std::env::current_dir().map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(cwd.to_string_lossy().as_ref()) -} - -/// Evaluates one PHP filesystem predicate over an eval expression. -fn eval_builtin_file_probe( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_probe_result(name, filename, values) -} - -/// Computes one local filesystem predicate and returns a PHP boolean. -fn eval_file_probe_result( - name: &str, - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let path = std::path::Path::new(&path); - let result = match name { - "file_exists" => path.exists(), - "is_dir" => path.is_dir(), - "is_executable" => eval_path_is_executable(path), - "is_file" => path.is_file(), - "is_link" => std::fs::symlink_metadata(path) - .map(|metadata| metadata.file_type().is_symlink()) - .unwrap_or(false), - "is_readable" => eval_path_is_readable(path), - "is_writable" | "is_writeable" => eval_path_is_writable(path), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(result) -} - -/// Evaluates one scalar PHP stat metadata builtin over an eval expression. -fn eval_builtin_file_stat_scalar( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_stat_scalar_result(name, filename, values) -} - -/// Returns scalar stat metadata, using PHP false for failure where native elephc does. -fn eval_file_stat_scalar_result( - name: &str, - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let metadata = match std::fs::metadata(path) { - Ok(metadata) => metadata, - Err(_) if name == "filemtime" => return values.int(0), - Err(_) => return values.bool_value(false), - }; - match name { - "fileatime" => values.int(metadata.atime()), - "filectime" => values.int(metadata.ctime()), - "filegroup" => values.int(i64::from(metadata.gid())), - "fileinode" => { - values.int(i64::try_from(metadata.ino()).map_err(|_| EvalStatus::RuntimeFatal)?) - } - "filemtime" => values.int(metadata.mtime()), - "fileowner" => values.int(i64::from(metadata.uid())), - "fileperms" => values.int(i64::from(metadata.mode())), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `file_get_contents($filename)` over one eval expression. -fn eval_builtin_file_get_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_get_contents_result(filename, values) -} - -/// Reads a local file into a PHP string, or returns false when it cannot be opened. -fn eval_file_get_contents_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - match std::fs::read(path) { - Ok(bytes) => values.string_bytes_value(&bytes), - Err(_) => { - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - values.bool_value(false) - } - } -} - -/// Evaluates PHP `file($filename)` over one eval expression. -fn eval_builtin_file( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_result(filename, values) -} - -/// Reads one local file and returns an indexed array of line byte strings. -fn eval_file_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let bytes = match std::fs::read(path) { - Ok(bytes) => bytes, - Err(_) => { - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - return values.array_new(0); - } - }; - eval_file_lines_array(&bytes, values) -} - -/// Splits file payload bytes into runtime array entries, preserving trailing newlines. -fn eval_file_lines_array( - bytes: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(0)?; - let mut line_start = 0; - let mut line_index = 0; - for (index, byte) in bytes.iter().enumerate() { - if *byte != b'\n' { - continue; - } - result = - eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; - line_start = index + 1; - line_index += 1; - } - if line_start < bytes.len() { - result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; - } - Ok(result) -} - -/// Evaluates PHP `readfile($filename)` over one eval expression. -fn eval_builtin_readfile( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_readfile_result(filename, values) -} - -/// Streams one local file to eval output and returns a byte count, false, or -1. -fn eval_readfile_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let path = std::path::Path::new(&path); - if path.is_dir() { - return values.int(-1); - } - let bytes = match std::fs::read(path) { - Ok(bytes) => bytes, - Err(_) => return values.bool_value(false), - }; - let output = values.string_bytes_value(&bytes)?; - values.echo(output)?; - values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. -fn eval_builtin_file_put_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, data] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let data = eval_expr(data, context, scope, values)?; - eval_file_put_contents_result(filename, data, values) -} - -/// Writes a PHP string to a local file and returns the written byte count or false. -fn eval_file_put_contents_result( - filename: RuntimeCellHandle, - data: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let data = values.string_bytes(data)?; - match std::fs::write(path, &data) { - Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), - Err(_) => values.bool_value(false), - } -} - -/// Evaluates PHP `filesize($filename)` over one eval expression. -fn eval_builtin_filesize( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_filesize_result(filename, values) -} - -/// Returns one local file size in bytes, or zero when stat fails. -fn eval_filesize_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let len = std::fs::metadata(path) - .map(|metadata| metadata.len()) - .unwrap_or(0); - values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `filetype($filename)` over one eval expression. -fn eval_builtin_filetype( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_filetype_result(filename, values) -} - -/// Returns the PHP filetype string for one path, or false when lstat fails. -fn eval_filetype_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let file_type = match std::fs::symlink_metadata(path) { - Ok(metadata) => metadata.file_type(), - Err(_) => return values.bool_value(false), - }; - let label = if file_type.is_file() { - "file" - } else if file_type.is_dir() { - "dir" - } else if file_type.is_symlink() { - "link" - } else if file_type.is_char_device() { - "char" - } else if file_type.is_block_device() { - "block" - } else if file_type.is_fifo() { - "fifo" - } else if file_type.is_socket() { - "socket" - } else { - "unknown" - }; - values.string(label) -} - -/// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. -fn eval_builtin_stat_array( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_stat_array_result(name, filename, values) -} - -/// Builds PHP's stat array for one local path, or returns false on stat failure. -fn eval_stat_array_result( - name: &str, - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let metadata = match name { - "stat" => std::fs::metadata(path), - "lstat" => std::fs::symlink_metadata(path), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let metadata = match metadata { - Ok(metadata) => metadata, - Err(_) => return values.bool_value(false), - }; - eval_stat_metadata_array(&metadata, values) -} - -/// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. -fn eval_stat_metadata_array( - metadata: &std::fs::Metadata, - values: &mut impl RuntimeValueOps, -) -> Result { - let fields = [ - ("dev", eval_u64_to_i64(metadata.dev())?), - ("ino", eval_u64_to_i64(metadata.ino())?), - ("mode", i64::from(metadata.mode())), - ("nlink", eval_u64_to_i64(metadata.nlink())?), - ("uid", i64::from(metadata.uid())), - ("gid", i64::from(metadata.gid())), - ("rdev", eval_u64_to_i64(metadata.rdev())?), - ("size", eval_u64_to_i64(metadata.size())?), - ("atime", metadata.atime()), - ("mtime", metadata.mtime()), - ("ctime", metadata.ctime()), - ("blksize", eval_u64_to_i64(metadata.blksize())?), - ("blocks", eval_u64_to_i64(metadata.blocks())?), - ]; - let mut result = values.assoc_new(fields.len() * 2)?; - for (index, (name, value)) in fields.iter().enumerate() { - result = eval_stat_array_set_int_key(result, index, *value, values)?; - result = eval_stat_array_set_string_key(result, name, *value, values)?; - } - Ok(result) -} - -/// Inserts one integer stat field under a numeric PHP array key. -fn eval_stat_array_set_int_key( - array: RuntimeCellHandle, - key: usize, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(i64::try_from(key).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Inserts one integer stat field under a string PHP array key. -fn eval_stat_array_set_string_key( - array: RuntimeCellHandle, - key: &str, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Converts unsigned stat metadata into the signed integer payload used by PHP cells. -fn eval_u64_to_i64(value: u64) -> Result { - i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. -fn eval_builtin_disk_space( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_disk_space_result(name, directory, values) -} - -/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. -fn eval_disk_space_result( - name: &str, - directory: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(directory)?; - let Ok(path) = CString::new(bytes) else { - return values.float(0.0); - }; - let mut stats = std::mem::MaybeUninit::::zeroed(); - let status = unsafe { - // libc writes the statvfs fields for this NUL-terminated local path. - libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) - }; - if status != 0 { - return values.float(0.0); - } - let stats = unsafe { - // `statvfs` succeeded, so libc initialized the full stat buffer. - stats.assume_init() - }; - let block_size = if stats.f_frsize > 0 { - stats.f_frsize - } else { - stats.f_bsize - }; - let blocks = match name { - "disk_free_space" => stats.f_bavail, - "disk_total_space" => stats.f_blocks, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.float((block_size as f64) * (blocks as f64)) -} - -/// Evaluates a one-path filesystem operation that returns a PHP boolean. -fn eval_builtin_unary_path_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_unary_path_bool_result(name, path, values) -} - -/// Executes a one-path local filesystem operation and returns whether it succeeded. -fn eval_unary_path_bool_result( - name: &str, - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - let ok = match name { - "chdir" => std::env::set_current_dir(path).is_ok(), - "mkdir" => std::fs::create_dir(path).is_ok(), - "rmdir" => std::fs::remove_dir(path).is_ok(), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Evaluates a two-path filesystem operation that returns a PHP boolean. -fn eval_builtin_binary_path_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [from, to] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let from = eval_expr(from, context, scope, values)?; - let to = eval_expr(to, context, scope, values)?; - eval_binary_path_bool_result(name, from, to, values) -} - -/// Executes a two-path local filesystem operation and returns whether it succeeded. -fn eval_binary_path_bool_result( - name: &str, - from: RuntimeCellHandle, - to: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let from = eval_path_string(from, values)?; - let to = eval_path_string(to, values)?; - let ok = match name { - "copy" => std::fs::copy(from, to).is_ok(), - "link" => std::fs::hard_link(from, to).is_ok(), - "rename" => std::fs::rename(from, to).is_ok(), - "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. -fn eval_builtin_chmod( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, permissions] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let permissions = eval_expr(permissions, context, scope, values)?; - eval_chmod_result(filename, permissions, values) -} - -/// Changes one local file's mode and returns whether the operation succeeded. -fn eval_chmod_result( - filename: RuntimeCellHandle, - permissions: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let mode = eval_int_value(permissions, values)? as u32; - let permissions = std::fs::Permissions::from_mode(mode); - values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) -} - -/// Evaluates PHP `scandir($directory)` over one eval expression. -fn eval_builtin_scandir( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_scandir_result(directory, values) -} - -/// Lists one local directory into an indexed string array, or an empty array on failure. -fn eval_scandir_result( - directory: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(directory, values)?; - let Ok(entries) = std::fs::read_dir(path) else { - return values.array_new(0); - }; - let mut names = vec![".".to_string(), "..".to_string()]; - for entry in entries { - let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; - names.push(entry.file_name().to_string_lossy().into_owned()); - } - names.sort(); - let mut result = values.array_new(names.len())?; - for (index, name) in names.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; - } - Ok(result) -} - -/// Evaluates PHP `glob($pattern)` over one eval expression. -fn eval_builtin_glob( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - eval_glob_result(pattern, values) -} - -/// Expands one local glob pattern into a sorted indexed PHP string array. -fn eval_glob_result( - pattern: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = eval_path_string(pattern, values)?; - let matches = eval_glob_matches(&pattern); - let mut result = values.array_new(matches.len())?; - for (index, path) in matches.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; - } - Ok(result) -} - -/// Collects sorted matches for one local glob pattern. -fn eval_glob_matches(pattern: &str) -> Vec { - if pattern.is_empty() { - return Vec::new(); - } - if !eval_glob_component_has_magic(pattern) { - return std::path::Path::new(pattern) - .exists() - .then(|| pattern.to_string()) - .into_iter() - .collect(); - } - let absolute = pattern.starts_with('/'); - let components: Vec<&str> = pattern - .split('/') - .filter(|component| !component.is_empty()) - .collect(); - let mut matches = Vec::new(); - let base = if absolute { - std::path::PathBuf::from("/") - } else { - std::path::PathBuf::from(".") - }; - let prefix = if absolute { "/" } else { "" }; - eval_glob_collect(&base, prefix, &components, &mut matches); - matches.sort(); - matches -} - -/// Recursively expands one glob path component at a time. -fn eval_glob_collect( - base: &std::path::Path, - prefix: &str, - components: &[&str], - matches: &mut Vec, -) { - let Some((component, rest)) = components.split_first() else { - if base.exists() && !prefix.is_empty() { - matches.push(prefix.to_string()); - } - return; - }; - if !eval_glob_component_has_magic(component) { - let next_base = base.join(component); - if rest.is_empty() { - if next_base.exists() { - matches.push(eval_glob_join_output(prefix, component)); - } - } else if next_base.is_dir() { - let next_prefix = eval_glob_join_output(prefix, component); - eval_glob_collect(&next_base, &next_prefix, rest, matches); - } - return; - } - let Ok(entries) = std::fs::read_dir(base) else { - return; - }; - let mut names = Vec::new(); - for entry in entries.flatten() { - names.push(entry.file_name().to_string_lossy().into_owned()); - } - names.sort(); - for name in names { - if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { - continue; - } - let next_base = base.join(&name); - if rest.is_empty() { - matches.push(eval_glob_join_output(prefix, &name)); - } else if next_base.is_dir() { - let next_prefix = eval_glob_join_output(prefix, &name); - eval_glob_collect(&next_base, &next_prefix, rest, matches); - } - } -} - -/// Joins a display path prefix and component while preserving absolute-root output. -fn eval_glob_join_output(prefix: &str, component: &str) -> String { - if prefix.is_empty() { - component.to_string() - } else if prefix == "/" { - format!("/{component}") - } else { - format!("{prefix}/{component}") - } -} - -/// Returns whether a glob component contains wildcard syntax. -fn eval_glob_component_has_magic(component: &str) -> bool { - component - .as_bytes() - .iter() - .any(|byte| matches!(byte, b'*' | b'?' | b'[')) -} - -/// Writes one byte-string value into an indexed runtime array at a zero-based position. -fn eval_array_set_indexed_bytes( - array: RuntimeCellHandle, - index: usize, - value: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.string_bytes_value(value)?; - values.array_set(array, key, value) -} - -/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. -fn eval_builtin_tempnam( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory, prefix] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - let prefix = eval_expr(prefix, context, scope, values)?; - eval_tempnam_result(directory, prefix, values) -} - -/// Creates a unique local temporary file and returns its path, or an empty string on failure. -fn eval_tempnam_result( - directory: RuntimeCellHandle, - prefix: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let directory = eval_path_string(directory, values)?; - let prefix = values.string_bytes(prefix)?; - let prefix = String::from_utf8_lossy(&prefix); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - for attempt in 0..1000_u32 { - let candidate = - std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); - match std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&candidate) - { - Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), - Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, - Err(_) => return values.string(""), - } - } - values.string("") -} - -/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. -fn eval_tempnam_filename(prefix: &str, nonce: u128, attempt: u32) -> String { - format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) -} - -/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. -fn eval_builtin_touch( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [filename] => { - let filename = eval_expr(filename, context, scope, values)?; - eval_touch_result(filename, None, None, values) - } - [filename, mtime] => { - let filename = eval_expr(filename, context, scope, values)?; - let mtime = eval_expr(mtime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), None, values) - } - [filename, mtime, atime] => { - let filename = eval_expr(filename, context, scope, values)?; - let mtime = eval_expr(mtime, context, scope, values)?; - let atime = eval_expr(atime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), Some(atime), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Creates or stamps one local file and returns whether the operation succeeded. -fn eval_touch_result( - filename: RuntimeCellHandle, - mtime: Option, - atime: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let (mtime, atime) = eval_touch_times(mtime, atime, values)?; - let file = match std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(path) - { - Ok(file) => file, - Err(_) => return values.bool_value(false), - }; - let times = std::fs::FileTimes::new() - .set_modified(mtime) - .set_accessed(atime); - values.bool_value(file.set_times(times).is_ok()) -} - -/// Resolves PHP touch timestamp defaults into concrete system times. -fn eval_touch_times( - mtime: Option, - atime: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { - let now = std::time::SystemTime::now(); - let Some(mtime) = mtime else { - return Ok((now, now)); - }; - if values.is_null(mtime)? { - if let Some(atime) = atime { - if !values.is_null(atime)? { - return Err(EvalStatus::RuntimeFatal); - } - } - return Ok((now, now)); - } - let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) - .ok_or(EvalStatus::RuntimeFatal)?; - let Some(atime) = atime else { - return Ok((mtime, mtime)); - }; - if values.is_null(atime)? { - return Ok((mtime, mtime)); - } - let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) - .ok_or(EvalStatus::RuntimeFatal)?; - Ok((mtime, atime)) -} - -/// Converts a Unix timestamp in seconds into a `SystemTime`. -fn eval_system_time_from_unix(seconds: i64) -> Option { - if seconds >= 0 { - std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) - } else { - std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) - } -} - -/// Evaluates PHP `umask($mask = null)` over an optional eval expression. -fn eval_builtin_umask( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_umask_result(None, values), - [mask] => { - let mask = eval_expr(mask, context, scope, values)?; - eval_umask_result(Some(mask), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Applies PHP `umask()` semantics and returns the previous mask. -fn eval_umask_result( - mask: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let previous = match mask { - Some(mask) => { - let mask = eval_int_value(mask, values)? as u32; - unsafe { umask(mask) } - } - None => unsafe { - let current = umask(0); - umask(current); - current - }, - }; - values.int(i64::from(previous)) -} - -/// Evaluates PHP `readlink($path)` over one eval expression. -fn eval_builtin_readlink( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_readlink_result(path, values) -} - -/// Reads one symbolic-link target string, or returns PHP false on failure. -fn eval_readlink_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - match std::fs::read_link(path) { - Ok(target) => values.string(target.to_string_lossy().as_ref()), - Err(_) => values.bool_value(false), - } -} - -/// Evaluates PHP `linkinfo($path)` over one eval expression. -fn eval_builtin_linkinfo( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_linkinfo_result(path, values) -} - -/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. -fn eval_linkinfo_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - let dev = match std::fs::symlink_metadata(path) { - Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, - Err(_) => -1, - }; - values.int(dev) -} - -/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. -fn eval_builtin_clearstatcache( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - eval_expr(arg, context, scope, values)?; - } - values.null() -} - -/// Evaluates PHP `unlink($filename)` over one eval expression. -fn eval_builtin_unlink( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_unlink_result(filename, values) -} - -/// Deletes one local file and returns whether it succeeded. -fn eval_unlink_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - values.bool_value(std::fs::remove_file(path).is_ok()) -} - -/// Converts one eval value to a filesystem path string. -fn eval_path_string( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let filename = values.string_bytes(filename)?; - Ok(String::from_utf8_lossy(&filename).into_owned()) -} - -/// Returns whether a path can be opened for reading by the current process. -fn eval_path_is_readable(path: &std::path::Path) -> bool { - std::fs::File::open(path).is_ok() || std::fs::read_dir(path).is_ok() -} - -/// Returns whether a path has any executable bit set in its Unix mode. -fn eval_path_is_executable(path: &std::path::Path) -> bool { - std::fs::metadata(path) - .map(|metadata| metadata.mode() & 0o111 != 0) - .unwrap_or(false) -} - -/// Returns whether a path can be written by the current process. -fn eval_path_is_writable(path: &std::path::Path) -> bool { - if path.is_file() { - return std::fs::OpenOptions::new().write(true).open(path).is_ok(); - } - if !path.is_dir() { - return false; - } - let probe = path.join(format!( - ".elephc_eval_writable_probe_{}", - std::process::id() - )); - match std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&probe) - { - Ok(_) => { - let _ = std::fs::remove_file(probe); - true - } - Err(_) => false, - } -} - -/// Evaluates PHP `basename($path, $suffix = "")` over one eval expression. -fn eval_builtin_basename( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_basename_result(path, None, values) - } - [path, suffix] => { - let path = eval_expr(path, context, scope, values)?; - let suffix = eval_expr(suffix, context, scope, values)?; - eval_basename_result(path, Some(suffix), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `basename()` bytes and returns them as a runtime string. -fn eval_basename_result( - path: RuntimeCellHandle, - suffix: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let suffix = suffix - .map(|suffix| values.string_bytes(suffix)) - .transpose()?; - let result = eval_basename_bytes(&path, suffix.as_deref()); - values.string_bytes_value(&result) -} - -/// Extracts a PHP basename from one path byte string. -fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { - let mut end = path.len(); - while end > 0 && path[end - 1] == b'/' { - end -= 1; - } - if end == 0 { - return Vec::new(); - } - let mut start = end; - while start > 0 && path[start - 1] != b'/' { - start -= 1; - } - let mut result = path[start..end].to_vec(); - if let Some(suffix) = suffix { - if !suffix.is_empty() && suffix.len() < result.len() && result.ends_with(suffix) { - result.truncate(result.len() - suffix.len()); - } - } - result -} - -/// Evaluates PHP `dirname($path, $levels = 1)` over one eval expression. -fn eval_builtin_dirname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_dirname_result(path, None, values) - } - [path, levels] => { - let path = eval_expr(path, context, scope, values)?; - let levels = eval_expr(levels, context, scope, values)?; - eval_dirname_result(path, Some(levels), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `dirname()` bytes and returns them as a runtime string. -fn eval_dirname_result( - path: RuntimeCellHandle, - levels: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let levels = match levels { - Some(levels) => eval_int_value(levels, values)?, - None => 1, - }; - if levels < 1 { - return Err(EvalStatus::RuntimeFatal); - } - let mut current = path; - for _ in 0..levels { - current = eval_dirname_once(¤t); - } - values.string_bytes_value(¤t) -} - -/// Applies one PHP `dirname()` parent traversal to a path byte string. -fn eval_dirname_once(path: &[u8]) -> Vec { - if path.is_empty() { - return b".".to_vec(); - } - let mut end = path.len(); - while end > 0 && path[end - 1] == b'/' { - end -= 1; - } - if end == 0 { - return b"/".to_vec(); - } - let mut cursor = end; - while cursor > 0 { - cursor -= 1; - if path[cursor] == b'/' { - let mut parent_end = cursor; - while parent_end > 0 && path[parent_end - 1] == b'/' { - parent_end -= 1; - } - return if parent_end == 0 { - b"/".to_vec() - } else { - path[..parent_end].to_vec() - }; - } - } - b".".to_vec() -} - -/// Evaluates PHP `realpath($path)` over one eval expression. -fn eval_builtin_realpath( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_realpath_result(path, values) -} - -/// Canonicalizes one path or returns PHP false when the path cannot be resolved. -fn eval_realpath_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let path = String::from_utf8_lossy(&path); - let Ok(canonical) = std::fs::canonicalize(path.as_ref()) else { - return values.bool_value(false); - }; - let canonical = canonical.to_string_lossy(); - values.string(canonical.as_ref()) -} - -/// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. -fn eval_builtin_pathinfo( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_pathinfo_result(path, None, values) - } - [path, flags] => { - let path = eval_expr(path, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_pathinfo_result(path, Some(flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `pathinfo()` as either an associative array or one component string. -fn eval_pathinfo_result( - path: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let Some(flags) = flags else { - return eval_pathinfo_array_result(&path, values); - }; - let flags = eval_int_value(flags, values)?; - if flags == EVAL_PATHINFO_ALL { - return eval_pathinfo_array_result(&path, values); - } - let component = eval_pathinfo_component_bytes(&path, flags); - values.string_bytes_value(&component) -} - -/// Builds the PHP `pathinfo()` associative-array result for all components. -fn eval_pathinfo_array_result( - path: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(4)?; - if !path.is_empty() { - let dirname = eval_pathinfo_dirname_bytes(path); - result = eval_pathinfo_array_set(result, "dirname", &dirname, values)?; - } - let parts = eval_pathinfo_parts(path); - result = eval_pathinfo_array_set(result, "basename", &parts.basename, values)?; - if parts.has_extension { - result = eval_pathinfo_array_set(result, "extension", &parts.extension, values)?; - } - eval_pathinfo_array_set(result, "filename", &parts.filename, values) -} - -/// Inserts one string component into a PHP `pathinfo()` associative result. -fn eval_pathinfo_array_set( - array: RuntimeCellHandle, - key: &str, - value: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.string_bytes_value(value)?; - values.array_set(array, key, value) -} - -/// Returns one PHP `pathinfo()` component for a non-all bitmask. -fn eval_pathinfo_component_bytes(path: &[u8], flags: i64) -> Vec { - if flags & EVAL_PATHINFO_DIRNAME != 0 { - return eval_pathinfo_dirname_bytes(path); - } - let parts = eval_pathinfo_parts(path); - if flags & EVAL_PATHINFO_BASENAME != 0 { - return parts.basename; - } - if flags & EVAL_PATHINFO_EXTENSION != 0 { - return parts.extension; - } - if flags & EVAL_PATHINFO_FILENAME != 0 { - return parts.filename; - } - Vec::new() -} - -/// Computes the dirname component with `pathinfo("")`'s empty-string exception. -fn eval_pathinfo_dirname_bytes(path: &[u8]) -> Vec { - if path.is_empty() { - Vec::new() - } else { - eval_dirname_once(path) - } -} - -/// Splits pathinfo basename, extension, and filename components. -fn eval_pathinfo_parts(path: &[u8]) -> EvalPathInfoParts { - let basename = eval_basename_bytes(path, None); - let Some(dot) = basename.iter().rposition(|byte| *byte == b'.') else { - return EvalPathInfoParts { - filename: basename.clone(), - basename, - extension: Vec::new(), - has_extension: false, - }; - }; - EvalPathInfoParts { - filename: basename[..dot].to_vec(), - extension: basename[dot + 1..].to_vec(), - basename, - has_extension: true, - } -} - -/// Pathinfo components derived from a basename. -struct EvalPathInfoParts { - /// Full basename component. - basename: Vec, - /// Extension component after the final dot, possibly empty for trailing-dot names. - extension: Vec, - /// Filename component before the final dot. - filename: Vec, - /// Whether the basename contained a dot and therefore has an extension key. - has_extension: bool, -} - -/// Evaluates PHP `fnmatch($pattern, $filename, $flags = 0)` over eval expressions. -fn eval_builtin_fnmatch( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, filename] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let filename = eval_expr(filename, context, scope, values)?; - eval_fnmatch_result(pattern, filename, None, values) - } - [pattern, filename, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let filename = eval_expr(filename, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_fnmatch_result(pattern, filename, Some(flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Runs PHP-style shell glob matching for one pattern/name pair. -fn eval_fnmatch_result( - pattern: RuntimeCellHandle, - filename: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = values.string_bytes(pattern)?; - let filename = values.string_bytes(filename)?; - let flags = match flags { - Some(flags) => eval_int_value(flags, values)?, - None => 0, - }; - values.bool_value(eval_fnmatch_bytes(&pattern, &filename, flags)) -} - -/// Matches byte strings using the eval-supported `fnmatch()` grammar and flags. -fn eval_fnmatch_bytes(pattern: &[u8], filename: &[u8], flags: i64) -> bool { - let mut memo = vec![vec![None; filename.len() + 1]; pattern.len() + 1]; - eval_fnmatch_at(pattern, filename, flags, 0, 0, &mut memo) -} - -/// Recursively matches a pattern suffix against a filename suffix with memoization. -fn eval_fnmatch_at( - pattern: &[u8], - filename: &[u8], - flags: i64, - pattern_index: usize, - filename_index: usize, - memo: &mut [Vec>], -) -> bool { - if let Some(result) = memo[pattern_index][filename_index] { - return result; - } - let result = if pattern_index == pattern.len() { - filename_index == filename.len() - } else { - match pattern[pattern_index] { - b'*' => eval_fnmatch_star( - pattern, - filename, - flags, - pattern_index, - filename_index, - memo, - ), - b'?' => { - eval_fnmatch_single_wildcard(filename, flags, filename_index) - && eval_fnmatch_at( - pattern, - filename, - flags, - pattern_index + 1, - filename_index + 1, - memo, - ) - } - b'[' => eval_fnmatch_class_or_literal( - pattern, - filename, - flags, - pattern_index, - filename_index, - memo, - ), - b'\\' if flags & EVAL_FNM_NOESCAPE == 0 => { - let (literal, next_pattern_index) = - eval_fnmatch_escaped_literal(pattern, pattern_index); - eval_fnmatch_literal(filename, flags, filename_index, literal) - && eval_fnmatch_at( - pattern, - filename, - flags, - next_pattern_index, - filename_index + 1, - memo, - ) - } - literal => { - eval_fnmatch_literal(filename, flags, filename_index, literal) - && eval_fnmatch_at( - pattern, - filename, - flags, - pattern_index + 1, - filename_index + 1, - memo, - ) - } - } - }; - memo[pattern_index][filename_index] = Some(result); - result -} - -/// Handles `*`, including pathname and leading-period restrictions. -fn eval_fnmatch_star( - pattern: &[u8], - filename: &[u8], - flags: i64, - pattern_index: usize, - filename_index: usize, - memo: &mut [Vec>], -) -> bool { - let mut next_pattern_index = pattern_index + 1; - while next_pattern_index < pattern.len() && pattern[next_pattern_index] == b'*' { - next_pattern_index += 1; - } - if eval_fnmatch_at( - pattern, - filename, - flags, - next_pattern_index, - filename_index, - memo, - ) { - return true; - } - let mut cursor = filename_index; - while cursor < filename.len() && eval_fnmatch_wildcard_can_consume(filename, flags, cursor) { - cursor += 1; - if eval_fnmatch_at(pattern, filename, flags, next_pattern_index, cursor, memo) { - return true; - } - } - false -} - -/// Returns whether `?` can consume the current filename byte. -fn eval_fnmatch_single_wildcard(filename: &[u8], flags: i64, filename_index: usize) -> bool { - filename_index < filename.len() - && eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) -} - -/// Handles a bracket class, or falls back to a literal `[` when the class is malformed. -fn eval_fnmatch_class_or_literal( - pattern: &[u8], - filename: &[u8], - flags: i64, - pattern_index: usize, - filename_index: usize, - memo: &mut [Vec>], -) -> bool { - if filename_index >= filename.len() - || !eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) - { - return false; - } - let Some((matches, next_pattern_index)) = - eval_fnmatch_class_matches(pattern, pattern_index + 1, filename[filename_index], flags) - else { - return eval_fnmatch_literal(filename, flags, filename_index, b'[') - && eval_fnmatch_at( - pattern, - filename, - flags, - pattern_index + 1, - filename_index + 1, - memo, - ); - }; - matches - && eval_fnmatch_at( - pattern, - filename, - flags, - next_pattern_index, - filename_index + 1, - memo, - ) -} - -/// Matches one bracket class body against the current filename byte. -fn eval_fnmatch_class_matches( - pattern: &[u8], - mut index: usize, - candidate: u8, - flags: i64, -) -> Option<(bool, usize)> { - let negated = matches!(pattern.get(index).copied(), Some(b'!' | b'^')); - if negated { - index += 1; - } - let mut matched = false; - let mut closed = false; - while index < pattern.len() { - if pattern[index] == b']' { - closed = true; - index += 1; - break; - } - let start = eval_fnmatch_class_char(pattern, &mut index, flags)?; - if index + 1 < pattern.len() && pattern[index] == b'-' && pattern[index + 1] != b']' { - index += 1; - let end = eval_fnmatch_class_char(pattern, &mut index, flags)?; - if eval_fnmatch_byte_in_range(candidate, start, end, flags) { - matched = true; - } - } else if eval_fnmatch_byte_eq(candidate, start, flags) { - matched = true; - } - } - closed.then_some((if negated { !matched } else { matched }, index)) -} - -/// Reads one character from a bracket class, respecting escapes when enabled. -fn eval_fnmatch_class_char(pattern: &[u8], index: &mut usize, flags: i64) -> Option { - if *index >= pattern.len() { - return None; - } - if pattern[*index] == b'\\' && flags & EVAL_FNM_NOESCAPE == 0 && *index + 1 < pattern.len() { - *index += 2; - return Some(pattern[*index - 1]); - } - let byte = pattern[*index]; - *index += 1; - Some(byte) -} - -/// Returns whether one candidate byte falls within a possibly case-folded range. -fn eval_fnmatch_byte_in_range(candidate: u8, start: u8, end: u8, flags: i64) -> bool { - let candidate = eval_fnmatch_fold(candidate, flags); - let start = eval_fnmatch_fold(start, flags); - let end = eval_fnmatch_fold(end, flags); - if start <= end { - candidate >= start && candidate <= end - } else { - candidate >= end && candidate <= start - } -} - -/// Reads an escaped literal token outside bracket classes. -fn eval_fnmatch_escaped_literal(pattern: &[u8], pattern_index: usize) -> (u8, usize) { - if pattern_index + 1 < pattern.len() { - (pattern[pattern_index + 1], pattern_index + 2) - } else { - (b'\\', pattern_index + 1) - } -} - -/// Returns whether one literal pattern byte matches the current filename byte. -fn eval_fnmatch_literal(filename: &[u8], flags: i64, filename_index: usize, literal: u8) -> bool { - filename_index < filename.len() - && eval_fnmatch_byte_eq(filename[filename_index], literal, flags) -} - -/// Returns whether a wildcard token may consume the current filename byte. -fn eval_fnmatch_wildcard_can_consume(filename: &[u8], flags: i64, filename_index: usize) -> bool { - if filename_index >= filename.len() { - return false; - } - if flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index] == b'/' { - return false; - } - if flags & EVAL_FNM_PERIOD != 0 - && eval_fnmatch_is_leading_period(filename, flags, filename_index) - { - return false; - } - true -} - -/// Returns whether the current byte is a leading period for `FNM_PERIOD`. -fn eval_fnmatch_is_leading_period(filename: &[u8], flags: i64, filename_index: usize) -> bool { - filename[filename_index] == b'.' - && (filename_index == 0 - || (flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index - 1] == b'/')) -} - -/// Compares bytes using ASCII case folding when `FNM_CASEFOLD` is present. -fn eval_fnmatch_byte_eq(left: u8, right: u8, flags: i64) -> bool { - eval_fnmatch_fold(left, flags) == eval_fnmatch_fold(right, flags) -} - -/// Applies eval fnmatch's ASCII case folding. -fn eval_fnmatch_fold(byte: u8, flags: i64) -> u8 { - if flags & EVAL_FNM_CASEFOLD != 0 { - byte.to_ascii_lowercase() - } else { - byte - } -} - -/// Evaluates PHP `preg_match()` over eval expressions. -fn eval_builtin_preg_match( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_match_result(pattern, subject, values) - } - [pattern, subject, matches] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let (result, matches_array) = - eval_preg_match_capture_result(pattern, subject, None, values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - [pattern, subject, matches, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let flags = eval_expr(flags, context, scope, values)?; - let (result, matches_array) = - eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns whether one regex matches the subject string. -fn eval_preg_match_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - values.int(i64::from(regex.is_match(&subject))) -} - -/// Returns the match flag plus PHP `$matches` capture array for one regex search. -fn eval_preg_match_capture_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let flags = eval_preg_match_flags(flags, values)?; - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - if let Some(captures) = regex.captures(&subject) { - let matches = eval_preg_capture_array( - &subject, - Some(&captures), - offset_capture, - unmatched_as_null, - values, - )?; - let matched = values.int(1)?; - return Ok((matched, matches)); - } - let matches = - eval_preg_capture_array(&subject, None, offset_capture, unmatched_as_null, values)?; - let matched = values.int(0)?; - Ok((matched, matches)) -} - -/// Returns supported `preg_match()` flags. -fn eval_preg_match_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(0); - }; - let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_OFFSET_CAPTURE | EVAL_PREG_UNMATCHED_AS_NULL; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Evaluates PHP `preg_match_all()` over eval expressions. -fn eval_builtin_preg_match_all( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_match_all_result(pattern, subject, values) - } - [pattern, subject, matches] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let (result, matches_array) = - eval_preg_match_all_capture_result(pattern, subject, None, values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - [pattern, subject, matches, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let flags = eval_expr(flags, context, scope, values)?; - let (result, matches_array) = - eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Counts all non-overlapping regex matches in one subject string. -fn eval_preg_match_all_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let count = regex.captures_iter(&subject).count(); - values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Returns the match count plus PHP's default `PREG_PATTERN_ORDER` `$matches` array. -fn eval_preg_match_all_capture_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let regex = eval_preg_regex(pattern, values)?; - let capture_count = regex.captures_len(); - let subject = values.string_bytes(subject)?; - let captures: Vec> = regex.captures_iter(&subject).collect(); - let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let flags = eval_preg_match_all_flags(flags, values)?; - let matches = if flags & EVAL_PREG_SET_ORDER != 0 { - eval_preg_match_all_set_order_array(&subject, &captures, capture_count, flags, values)? - } else { - eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, flags, values)? - }; - Ok((count, matches)) -} - -/// Returns supported `preg_match_all()` flags. -fn eval_preg_match_all_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(EVAL_PREG_PATTERN_ORDER); - }; - let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_PATTERN_ORDER - | EVAL_PREG_SET_ORDER - | EVAL_PREG_OFFSET_CAPTURE - | EVAL_PREG_UNMATCHED_AS_NULL; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Builds PHP's default `preg_match_all()` pattern-order capture matrix. -fn eval_preg_match_all_pattern_order_array( - subject: &[u8], - captures: &[Captures<'_>], - capture_count: usize, - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - let mut outer = values.array_new(capture_count)?; - for capture_index in 0..capture_count { - let mut row = values.array_new(captures.len())?; - for (match_index, capture) in captures.iter().enumerate() { - let key = - values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - capture, - capture_index, - offset_capture, - unmatched_as_null, - values, - )?; - row = values.array_set(row, key, value)?; - } - let key = - values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - outer = values.array_set(outer, key, row)?; - } - Ok(outer) -} - -/// Builds PHP's `preg_match_all(..., PREG_SET_ORDER)` match-order capture matrix. -fn eval_preg_match_all_set_order_array( - subject: &[u8], - captures: &[Captures<'_>], - capture_count: usize, - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - let mut outer = values.array_new(captures.len())?; - for (match_index, capture) in captures.iter().enumerate() { - let mut row = values.array_new(capture_count)?; - for capture_index in 0..capture_count { - let key = - values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - capture, - capture_index, - offset_capture, - unmatched_as_null, - values, - )?; - row = values.array_set(row, key, value)?; - } - let key = values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - outer = values.array_set(outer, key, row)?; - } - Ok(outer) -} - -/// Evaluates PHP `preg_replace()` over eval expressions. -fn eval_builtin_preg_replace( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern, replacement, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - let replacement = eval_expr(replacement, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_replace_result(pattern, replacement, subject, values) -} - -/// Replaces every regex match with a PHP-style backreference-expanded replacement. -fn eval_preg_replace_result( - pattern: RuntimeCellHandle, - replacement: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let replacement = values.string_bytes(replacement)?; - let subject = values.string_bytes(subject)?; - let mut result = Vec::with_capacity(subject.len()); - let mut cursor = 0; - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - result.extend_from_slice(&subject[cursor..matched.start()]); - eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); - cursor = matched.end(); - } - result.extend_from_slice(&subject[cursor..]); - values.string_bytes_value(&result) -} - -/// Evaluates PHP `preg_replace_callback()` over eval expressions. -fn eval_builtin_preg_replace_callback( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern, callback, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_replace_callback_result(pattern, callback, subject, context, values) -} - -/// Replaces every regex match by invoking an eval-supported callback with `$matches`. -fn eval_preg_replace_callback_result( - pattern: RuntimeCellHandle, - callback: RuntimeCellHandle, - subject: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let callback = eval_callable_name(callback, values)?; - let subject = values.string_bytes(subject)?; - let mut result = Vec::with_capacity(subject.len()); - let mut cursor = 0; - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - result.extend_from_slice(&subject[cursor..matched.start()]); - let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; - let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; - let callback_result = values.cast_string(callback_result)?; - let callback_bytes = values.string_bytes(callback_result)?; - result.extend_from_slice(&callback_bytes); - cursor = matched.end(); - } - result.extend_from_slice(&subject[cursor..]); - values.string_bytes_value(&result) -} - -/// Evaluates PHP `preg_split()` over eval expressions. -fn eval_builtin_preg_split( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_split_result(pattern, subject, None, None, values) - } - [pattern, subject, limit] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let limit = eval_expr(limit, context, scope, values)?; - eval_preg_split_result(pattern, subject, Some(limit), None, values) - } - [pattern, subject, limit, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let limit = eval_expr(limit, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_preg_split_result(pattern, subject, Some(limit), Some(flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Splits a subject string with eval-supported `preg_split()` flags. -fn eval_preg_split_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - limit: Option, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let limit = eval_preg_split_limit(limit, values)?; - let flags = eval_preg_split_flags(flags, values)?; - let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; - let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; - let offset_capture = flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0; - let mut pieces = Vec::::new(); - let mut cursor = 0; - - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - if eval_preg_split_reached_limit(&pieces, limit) { - break; - } - eval_preg_split_push_piece( - &mut pieces, - &subject[cursor..matched.start()], - cursor, - no_empty, - ); - if capture_delimiters { - eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); - } - cursor = matched.end(); - } - eval_preg_split_push_piece(&mut pieces, &subject[cursor..], cursor, no_empty); - - let mut result = values.array_new(pieces.len())?; - for (index, piece) in pieces.iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_split_piece_value(piece, offset_capture, values)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Compiles one eval PCRE-style delimited pattern into a Rust regex. -fn eval_preg_regex( - pattern: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = values.string_bytes(pattern)?; - let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; - let body = String::from_utf8(body).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut builder = RegexBuilder::new(&body); - builder - .case_insensitive(modifiers.case_insensitive) - .multi_line(modifiers.multi_line) - .dot_matches_new_line(modifiers.dot_matches_new_line) - .swap_greed(modifiers.swap_greed); - builder.build().map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Regex modifiers supported by eval `preg_*` pattern stripping. -#[derive(Default)] -struct EvalPregModifiers { - case_insensitive: bool, - multi_line: bool, - dot_matches_new_line: bool, - swap_greed: bool, -} - -/// One `preg_split()` output segment plus its byte offset in the subject. -struct EvalPregSplitPiece { - bytes: Vec, - offset: usize, -} - -/// Splits a PHP delimited regex into body bytes and supported modifiers. -fn eval_preg_pattern_parts(pattern: &[u8]) -> Result<(Vec, EvalPregModifiers), EvalStatus> { - if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() { - return Err(EvalStatus::RuntimeFatal); - } - let delimiter = pattern[0]; - if delimiter == b'\\' { - return Err(EvalStatus::RuntimeFatal); - } - let closing = eval_preg_closing_delimiter(delimiter); - let close_index = - eval_preg_find_closing_delimiter(pattern, closing).ok_or(EvalStatus::RuntimeFatal)?; - let body = eval_preg_unescape_delimiter(&pattern[1..close_index], delimiter, closing); - let modifiers = eval_preg_modifiers(&pattern[close_index + 1..])?; - Ok((body, modifiers)) -} - -/// Returns the closing regex delimiter for PHP's paired delimiter forms. -fn eval_preg_closing_delimiter(delimiter: u8) -> u8 { - match delimiter { - b'(' => b')', - b'[' => b']', - b'{' => b'}', - b'<' => b'>', - _ => delimiter, - } -} - -/// Finds the first unescaped closing regex delimiter. -fn eval_preg_find_closing_delimiter(pattern: &[u8], closing: u8) -> Option { - let mut escaped = false; - for (index, byte) in pattern.iter().copied().enumerate().skip(1) { - if escaped { - escaped = false; - continue; - } - if byte == b'\\' { - escaped = true; - continue; - } - if byte == closing { - return Some(index); - } - } - None -} - -/// Removes escapes that only protect the PHP regex delimiter from pattern stripping. -fn eval_preg_unescape_delimiter(body: &[u8], delimiter: u8, closing: u8) -> Vec { - let mut result = Vec::with_capacity(body.len()); - let mut index = 0; - while index < body.len() { - if body[index] == b'\\' - && index + 1 < body.len() - && matches!(body[index + 1], byte if byte == delimiter || byte == closing) - { - result.push(body[index + 1]); - index += 2; - } else { - result.push(body[index]); - index += 1; - } - } - result -} - -/// Parses eval-supported PHP regex modifiers. -fn eval_preg_modifiers(modifiers: &[u8]) -> Result { - let mut parsed = EvalPregModifiers::default(); - for modifier in modifiers { - match *modifier { - b'i' => parsed.case_insensitive = true, - b'm' => parsed.multi_line = true, - b's' => parsed.dot_matches_new_line = true, - b'U' => parsed.swap_greed = true, - b'u' => {} - _ => return Err(EvalStatus::RuntimeFatal), - } - } - Ok(parsed) -} - -/// Builds PHP's indexed `$matches` capture array for one regex result. -fn eval_preg_capture_array( - subject: &[u8], - captures: Option<&Captures<'_>>, - offset_capture: bool, - unmatched_as_null: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = captures.map_or(0, |captures| { - eval_preg_visible_capture_len(captures, unmatched_as_null) - }); - let mut result = values.array_new(len)?; - if let Some(captures) = captures { - for index in 0..len { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - captures, - index, - offset_capture, - unmatched_as_null, - values, - )?; - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Returns the capture count PHP should expose, dropping trailing unmatched groups. -fn eval_preg_visible_capture_len(captures: &Captures<'_>, unmatched_as_null: bool) -> usize { - if unmatched_as_null { - return captures.len(); - } - let mut len = captures.len(); - while len > 1 && captures.get(len - 1).is_none() { - len -= 1; - } - len -} - -/// Returns one captured byte range from the original subject. -fn eval_preg_capture_bytes<'a>( - subject: &'a [u8], - captures: &Captures<'_>, - index: usize, -) -> Option<&'a [u8]> { - captures - .get(index) - .map(|matched| &subject[matched.start()..matched.end()]) -} - -/// Builds one capture entry as either a string or PHP's `[string, byte_offset]` pair. -fn eval_preg_capture_value( - subject: &[u8], - captures: &Captures<'_>, - index: usize, - offset_capture: bool, - unmatched_as_null: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let matched = captures.get(index); - let value = if matched.is_none() && unmatched_as_null { - values.null()? - } else { - let bytes = matched.as_ref().map_or(b"".as_slice(), |matched| { - &subject[matched.start()..matched.end()] - }); - values.string_bytes_value(bytes)? - }; - if !offset_capture { - return Ok(value); - } - - let offset = matched.map_or(Ok(-1_i64), |matched| { - i64::try_from(matched.start()).map_err(|_| EvalStatus::RuntimeFatal) - })?; - let offset = values.int(offset)?; - let mut pair = values.array_new(2)?; - let value_key = values.int(0)?; - pair = values.array_set(pair, value_key, value)?; - let offset_key = values.int(1)?; - values.array_set(pair, offset_key, offset) -} - -/// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. -fn eval_preg_expand_replacement( - replacement: &[u8], - subject: &[u8], - captures: &Captures<'_>, - result: &mut Vec, -) { - let mut index = 0; - while index < replacement.len() { - match replacement[index] { - b'$' => { - if let Some((capture_index, next_index)) = - eval_preg_replacement_capture_index(replacement, index + 1) - { - if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { - result.extend_from_slice(bytes); - } - index = next_index; - } else { - result.push(replacement[index]); - index += 1; - } - } - b'\\' if index + 1 < replacement.len() && replacement[index + 1].is_ascii_digit() => { - let (capture_index, next_index) = - eval_preg_decimal_capture_index(replacement, index + 1); - if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { - result.extend_from_slice(bytes); - } - index = next_index; - } - byte => { - result.push(byte); - index += 1; - } - } - } -} - -/// Parses a dollar-style replacement capture reference. -fn eval_preg_replacement_capture_index(bytes: &[u8], index: usize) -> Option<(usize, usize)> { - if bytes.get(index).copied() == Some(b'{') { - let mut cursor = index + 1; - let start = cursor; - while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { - cursor += 1; - } - if cursor == start || bytes.get(cursor).copied() != Some(b'}') { - return None; - } - let capture = eval_preg_decimal_bytes_to_usize(&bytes[start..cursor])?; - return Some((capture, cursor + 1)); - } - if bytes.get(index).is_some_and(u8::is_ascii_digit) { - let (capture, next) = eval_preg_decimal_capture_index(bytes, index); - return Some((capture, next)); - } - None -} - -/// Parses a one- or two-digit replacement capture reference. -fn eval_preg_decimal_capture_index(bytes: &[u8], index: usize) -> (usize, usize) { - let mut cursor = index; - let end = usize::min(bytes.len(), index + 2); - while cursor < end && bytes[cursor].is_ascii_digit() { - cursor += 1; - } - ( - eval_preg_decimal_bytes_to_usize(&bytes[index..cursor]).unwrap_or(0), - cursor, - ) -} - -/// Converts ASCII decimal bytes into a `usize` capture index. -fn eval_preg_decimal_bytes_to_usize(bytes: &[u8]) -> Option { - let mut value = 0usize; - for byte in bytes { - value = value.checked_mul(10)?; - value = value.checked_add(usize::from(byte - b'0'))?; - } - Some(value) -} - -/// Returns the PHP `preg_split()` limit, treating zero as unlimited. -fn eval_preg_split_limit( - limit: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(limit) = limit else { - return Ok(None); - }; - let limit = eval_int_value(limit, values)?; - if limit <= 0 { - return Ok(None); - } - usize::try_from(limit) - .map(Some) - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Returns supported `preg_split()` flags. -fn eval_preg_split_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(0); - }; - let flags = eval_int_value(flags, values)?; - let supported = - EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE | EVAL_PREG_SPLIT_OFFSET_CAPTURE; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Returns whether `preg_split()` should stop splitting and emit the remaining subject. -fn eval_preg_split_reached_limit(pieces: &[EvalPregSplitPiece], limit: Option) -> bool { - matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) -} - -/// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. -fn eval_preg_split_push_piece( - pieces: &mut Vec, - piece: &[u8], - offset: usize, - no_empty: bool, -) { - if no_empty && piece.is_empty() { - return; - } - pieces.push(EvalPregSplitPiece { - bytes: piece.to_vec(), - offset, - }); -} - -/// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. -fn eval_preg_split_push_captures( - pieces: &mut Vec, - subject: &[u8], - captures: &Captures<'_>, - no_empty: bool, -) { - for index in 1..captures.len() { - if let Some(matched) = captures.get(index) { - eval_preg_split_push_piece( - pieces, - &subject[matched.start()..matched.end()], - matched.start(), - no_empty, - ); - } - } -} - -/// Converts one split segment to a string or PHP `[string, byte_offset]` pair. -fn eval_preg_split_piece_value( - piece: &EvalPregSplitPiece, - offset_capture: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.string_bytes_value(&piece.bytes)?; - if !offset_capture { - return Ok(value); - } - - let offset = i64::try_from(piece.offset).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = values.int(offset)?; - let mut pair = values.array_new(2)?; - let value_key = values.int(0)?; - pair = values.array_set(pair, value_key, value)?; - let offset_key = values.int(1)?; - values.array_set(pair, offset_key, offset) -} - -/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. -fn eval_builtin_gethostbyaddr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_gethostbyaddr_result(ip, values) -} - -/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. -fn eval_gethostbyaddr_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let ip_bytes = values.string_bytes(ip)?; - let ip_text = String::from_utf8_lossy(&ip_bytes); - let Ok(ipv4) = ip_text.parse::() else { - return values.bool_value(false); - }; - let octets = ipv4.octets(); - let resolved = unsafe { - // libc reads the stack-owned IPv4 octets during this call and returns - // static resolver storage, which is copied before the next resolver call. - let host = libc_gethostbyaddr( - octets.as_ptr().cast::(), - octets.len() as libc::socklen_t, - libc::AF_INET, - ); - if host.is_null() || (*host).h_name.is_null() { - None - } else { - Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) - } - }; - match resolved { - Some(name) if !name.is_empty() => values.string_bytes_value(&name), - _ => values.string(ip_text.as_ref()), - } -} - -/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. -fn eval_builtin_gethostbyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hostname] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hostname = eval_expr(hostname, context, scope, values)?; - eval_gethostbyname_result(hostname, values) -} - -/// Resolves one host name to an IPv4 string, or returns the original input on failure. -fn eval_gethostbyname_result( - hostname: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let hostname = values.string_bytes(hostname)?; - let hostname = String::from_utf8_lossy(&hostname); - if hostname.parse::().is_ok() { - return values.string(hostname.as_ref()); - } - let resolved = (hostname.as_ref(), 0_u16) - .to_socket_addrs() - .ok() - .and_then(|addrs| { - addrs - .filter_map(|addr| match addr.ip() { - std::net::IpAddr::V4(ip) => Some(ip.to_string()), - std::net::IpAddr::V6(_) => None, - }) - .next() - }); - values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) -} - -/// Evaluates PHP `gethostname()` over one eval expression. -fn eval_builtin_gethostname( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - let [] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostname_result(values) -} - -/// Reads the current host name through libc and returns an empty string on failure. -fn eval_gethostname_result( - values: &mut impl RuntimeValueOps, -) -> Result { - let mut buffer = [0 as libc::c_char; 256]; - let status = unsafe { - // libc writes at most buffer.len() bytes into this stack buffer. - libc::gethostname(buffer.as_mut_ptr(), buffer.len()) - }; - if status != 0 { - return values.string(""); - } - let length = buffer - .iter() - .position(|byte| *byte == 0) - .unwrap_or(buffer.len()); - let hostname = buffer[..length] - .iter() - .map(|byte| *byte as u8) - .collect::>(); - values.string_bytes_value(&hostname) -} - -/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. -fn eval_builtin_getprotobyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getprotobyname_result(protocol, values) -} - -/// Looks up an IP protocol number by name or alias. -fn eval_getprotobyname_result( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global protoent; copy scalar fields before another lookup. - libc_getprotobyname(protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); - } - let number = unsafe { (*entry).p_proto }; - values.int(i64::from(number)) -} - -/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. -fn eval_builtin_getprotobynumber( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getprotobynumber_result(protocol, values) -} - -/// Looks up an IP protocol name by numeric protocol id. -fn eval_getprotobynumber_result( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let protocol = eval_int_value(protocol, values)?; - let Ok(protocol) = libc::c_int::try_from(protocol) else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global protoent; copy the name before another lookup. - libc_getprotobynumber(protocol) - }; - eval_protoent_name_or_false(entry, values) -} - -/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. -fn eval_builtin_getservbyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [service, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let service = eval_expr(service, context, scope, values)?; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getservbyname_result(service, protocol, values) -} - -/// Looks up an internet service port by service name and protocol. -fn eval_getservbyname_result( - service: RuntimeCellHandle, - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(service) = eval_lowercase_c_string(service, values)? else { - return values.bool_value(false); - }; - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global servent; copy scalar fields before another lookup. - libc_getservbyname(service.as_ptr(), protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); - } - let port = unsafe { u16::from_be((*entry).s_port as u16) }; - values.int(i64::from(port)) -} - -/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. -fn eval_builtin_getservbyport( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [port, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let port = eval_expr(port, context, scope, values)?; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getservbyport_result(port, protocol, values) -} - -/// Looks up an internet service name by port and protocol. -fn eval_getservbyport_result( - port: RuntimeCellHandle, - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let port = eval_int_value(port, values)?; - let Ok(port) = u16::try_from(port) else { - return values.bool_value(false); - }; - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let network_port = port.to_be() as libc::c_int; - let entry = unsafe { - // libc returns a process-global servent; copy the name before another lookup. - libc_getservbyport(network_port, protocol.as_ptr()) - }; - eval_servent_name_or_false(entry, values) -} - -/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. -fn eval_lowercase_c_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let bytes = values.string_bytes(value)?; - let bytes = bytes - .into_iter() - .map(|byte| byte.to_ascii_lowercase()) - .collect::>(); - Ok(CString::new(bytes).ok()) -} - -/// Copies a protoent canonical name into a PHP string or returns PHP false. -fn eval_protoent_name_or_false( - entry: *mut libc::protoent, - values: &mut impl RuntimeValueOps, -) -> Result { - if entry.is_null() { - return values.bool_value(false); - } - let name = unsafe { - let name = (*entry).p_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) -} - -/// Copies a servent canonical name into a PHP string or returns PHP false. -fn eval_servent_name_or_false( - entry: *mut libc::servent, - values: &mut impl RuntimeValueOps, -) -> Result { - if entry.is_null() { - return values.bool_value(false); - } - let name = unsafe { - let name = (*entry).s_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) -} - -/// Evaluates PHP `long2ip($ip)` over one eval expression. -fn eval_builtin_long2ip( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_long2ip_result(ip, values) -} - -/// Formats one 32-bit IPv4 integer as a dotted-quad string. -fn eval_long2ip_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let ip = eval_int_value(ip, values)? as u32; - values.string(&eval_format_ipv4(ip)) -} - -/// Evaluates PHP `ip2long($ip)` over one eval expression. -fn eval_builtin_ip2long( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_ip2long_result(ip, values) -} - -/// Parses a dotted-quad IPv4 string into an integer or PHP false. -fn eval_ip2long_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(ip)?; - match eval_parse_ipv4(&bytes) { - Some(ip) => values.int(i64::from(ip)), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `inet_pton($ip)` over one eval expression. -fn eval_builtin_inet_pton( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_inet_pton_result(ip, values) -} - -/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. -fn eval_inet_pton_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(ip)?; - let Some(ip) = eval_parse_ipv4(&bytes) else { - return values.bool_value(false); - }; - values.string_bytes_value(&ip.to_be_bytes()) -} - -/// Evaluates PHP `inet_ntop($binary)` over one eval expression. -fn eval_builtin_inet_ntop( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [binary] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let binary = eval_expr(binary, context, scope, values)?; - eval_inet_ntop_result(binary, values) -} - -/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. -fn eval_inet_ntop_result( - binary: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(binary)?; - let [a, b, c, d] = bytes.as_slice() else { - return values.bool_value(false); - }; - let ip = u32::from_be_bytes([*a, *b, *c, *d]); - values.string(&eval_format_ipv4(ip)) -} - -/// Parses exactly four decimal IPv4 octets separated by dots. -fn eval_parse_ipv4(bytes: &[u8]) -> Option { - let mut octets = [0_u8; 4]; - let mut position = 0_usize; - let mut index = 0_usize; - - while index < 4 { - if position >= bytes.len() { - return None; - } - let start = position; - let mut value = 0_u16; - while position < bytes.len() && bytes[position].is_ascii_digit() { - value = value - .checked_mul(10)? - .checked_add(u16::from(bytes[position] - b'0'))?; - position += 1; - if position - start > 3 || value > 255 { - return None; - } - } - if position == start { - return None; - } - octets[index] = value as u8; - index += 1; - if index == 4 { - return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); - } - if bytes.get(position).copied() != Some(b'.') { - return None; - } - position += 1; - } - None -} - -/// Formats one packed IPv4 integer into dotted-quad text. -fn eval_format_ipv4(ip: u32) -> String { - let [a, b, c, d] = ip.to_be_bytes(); - format!("{}.{}.{}.{}", a, b, c, d) -} - -/// Evaluates PHP `getenv($name)` over one eval expression. -fn eval_builtin_getenv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - eval_getenv_result(name, values) -} - -/// Reads one environment variable and returns an empty string when it is unset. -fn eval_getenv_result( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8_lossy(&name); - let value = std::env::var_os(name.as_ref()) - .map(|value| value.to_string_lossy().into_owned()) - .unwrap_or_default(); - values.string(&value) -} - -/// Evaluates PHP `putenv($assignment)` over one eval expression. -fn eval_builtin_putenv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [assignment] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let assignment = eval_expr(assignment, context, scope, values)?; - eval_putenv_result(assignment, values) -} - -/// Applies one `putenv()` assignment to the host environment. -fn eval_putenv_result( - assignment: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let assignment = values.string_bytes(assignment)?; - if let Some(separator) = assignment.iter().position(|byte| *byte == b'=') { - let name = String::from_utf8_lossy(&assignment[..separator]); - let value = String::from_utf8_lossy(&assignment[separator + 1..]); - std::env::set_var(name.as_ref(), value.as_ref()); - } else { - let name = String::from_utf8_lossy(&assignment); - std::env::remove_var(name.as_ref()); - } - values.bool_value(true) -} - -/// Evaluates PHP `sys_get_temp_dir()` with no arguments. -fn eval_builtin_sys_get_temp_dir( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_sys_get_temp_dir_result(values) -} - -/// Returns the same temporary directory literal as the native static builtin. -fn eval_sys_get_temp_dir_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.string("/tmp") -} - -/// Evaluates PHP `realpath_cache_get()` with no arguments. -fn eval_builtin_realpath_cache_get( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_get_result(values) -} - -/// Returns elephc's intentionally empty realpath-cache view. -fn eval_realpath_cache_get_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.array_new(0) -} - -/// Evaluates PHP `realpath_cache_size()` with no arguments. -fn eval_builtin_realpath_cache_size( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_size_result(values) -} - -/// Returns zero because elephc does not maintain a runtime realpath cache. -fn eval_realpath_cache_size_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.int(0) -} - -/// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. -fn eval_crc32_bytes(bytes: &[u8]) -> u32 { - let mut crc = 0xffff_ffff_u32; - for byte in bytes { - crc ^= u32::from(*byte); - for _ in 0..8 { - let mask = 0_u32.wrapping_sub(crc & 1); - crc = (crc >> 1) ^ (0xedb8_8320 & mask); - } - } - !crc -} - -/// Casts one eval value to PHP int and returns the scalar payload. -fn eval_int_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.cast_int(value)?; - let bytes = values.string_bytes(value)?; - std::str::from_utf8(&bytes) - .map_err(|_| EvalStatus::RuntimeFatal)? - .parse::() - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP's `bin2hex(...)` over one eval expression. -fn eval_builtin_bin2hex( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_bin2hex_result(value, values) -} - -/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. -fn eval_bin2hex_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.string(&eval_lower_hex_bytes(&bytes)) -} - -/// Converts bytes to lowercase hexadecimal text. -fn eval_lower_hex_bytes(bytes: &[u8]) -> String { - let mut output = String::with_capacity(bytes.len() * 2); - const HEX: &[u8; 16] = b"0123456789abcdef"; - for byte in bytes { - output.push(HEX[(byte >> 4) as usize] as char); - output.push(HEX[(byte & 0x0f) as usize] as char); - } - output -} - -/// Evaluates PHP's `hex2bin(...)` over one eval expression. -fn eval_builtin_hex2bin( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_hex2bin_result(value, values) -} - -/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. -fn eval_hex2bin_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - if bytes.len() % 2 != 0 { - values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; - return values.bool_value(false); - } - let mut output = Vec::with_capacity(bytes.len() / 2); - for pair in bytes.chunks_exact(2) { - let Some(high) = eval_hex_nibble(pair[0]) else { - values.warning(HEX2BIN_INVALID_WARNING)?; - return values.bool_value(false); - }; - let Some(low) = eval_hex_nibble(pair[1]) else { - values.warning(HEX2BIN_INVALID_WARNING)?; - return values.bool_value(false); - }; - output.push((high << 4) | low); - } - values.string_bytes_value(&output) -} - -/// Returns the four-bit value for one hexadecimal byte. -fn eval_hex_nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } -} - -/// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. -fn eval_builtin_slashes( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_slashes_result(name, value, values) -} - -/// Applies PHP byte-string escaping or unescaping for addslashes/stripslashes. -fn eval_slashes_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "addslashes" => eval_addslashes_result(value, values), - "stripslashes" => eval_stripslashes_result(value, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. -fn eval_addslashes_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - for byte in bytes { - match byte { - 0 => output.extend_from_slice(b"\\0"), - b'\'' | b'"' | b'\\' => { - output.push(b'\\'); - output.push(byte); - } - _ => output.push(byte), - } - } - values.string_bytes_value(&output) -} - -/// Removes backslash quoting using PHP `stripslashes()` byte semantics. -fn eval_stripslashes_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'\\' { - index += 1; - if let Some(byte) = bytes.get(index).copied() { - output.push(if byte == b'0' { 0 } else { byte }); - index += 1; - } - } else { - output.push(bytes[index]); - index += 1; - } - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `base64_encode(...)` over one eval expression. -fn eval_builtin_base64_encode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_encode_result(value, values) -} - -/// Converts one eval value through PHP string conversion and returns Base64 text. -fn eval_base64_encode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); - const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for chunk in bytes.chunks(3) { - let first = chunk[0]; - let second = chunk.get(1).copied().unwrap_or(0); - let third = chunk.get(2).copied().unwrap_or(0); - output.push(ALPHABET[(first >> 2) as usize] as char); - output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); - if chunk.len() > 1 { - output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); - } else { - output.push('='); - } - if chunk.len() > 2 { - output.push(ALPHABET[(third & 0x3f) as usize] as char); - } else { - output.push('='); - } - } - values.string(&output) -} - -/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. -fn eval_builtin_base64_decode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_decode_result(value, values) -} - -/// Converts one eval value through PHP string conversion and decodes Base64 bytes. -fn eval_base64_decode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let input = values.string_bytes(value)?; - let mut output = Vec::with_capacity((input.len() / 4) * 3); - let mut quartet = Vec::with_capacity(4); - for byte in input { - if byte.is_ascii_whitespace() { - continue; - } - if byte == b'=' { - quartet.push(None); - } else if let Some(value) = eval_base64_decode_sextet(byte) { - quartet.push(Some(value)); - } else { - continue; - } - if quartet.len() == 4 { - eval_push_base64_decoded_quartet(&quartet, &mut output); - quartet.clear(); - } - } - if !quartet.is_empty() { - while quartet.len() < 4 { - quartet.push(None); - } - eval_push_base64_decoded_quartet(&quartet, &mut output); - } - values.string_bytes_value(&output) -} - -/// Returns the six-bit Base64 value for one encoded byte. -fn eval_base64_decode_sextet(byte: u8) -> Option { - match byte { - b'A'..=b'Z' => Some(byte - b'A'), - b'a'..=b'z' => Some(byte - b'a' + 26), - b'0'..=b'9' => Some(byte - b'0' + 52), - b'+' => Some(62), - b'/' => Some(63), - _ => None, - } -} - -/// Appends decoded bytes for one padded or unpadded Base64 quartet. -fn eval_push_base64_decoded_quartet(quartet: &[Option], output: &mut Vec) { - let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { - return; - }; - output.push((first << 2) | (second >> 4)); - let Some(third) = quartet[2] else { - return; - }; - output.push(((second & 0x0f) << 4) | (third >> 2)); - let Some(fourth) = quartet[3] else { - return; - }; - output.push(((third & 0x03) << 6) | fourth); -} - -/// Evaluates PHP one-argument floating-point math builtins over one eval expression. -fn eval_builtin_float_unary( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_float_unary_result(name, value, values) -} - -/// Dispatches an evaluated value through the matching PHP floating-point unary math function. -fn eval_float_unary_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_float_value(value, values)?; - let result = match name { - "acos" => value.acos(), - "asin" => value.asin(), - "atan" => value.atan(), - "cos" => value.cos(), - "cosh" => value.cosh(), - "deg2rad" => value.to_radians(), - "exp" => value.exp(), - "log2" => value.log2(), - "log10" => value.log10(), - "rad2deg" => value.to_degrees(), - "sin" => value.sin(), - "sinh" => value.sinh(), - "tan" => value.tan(), - "tanh" => value.tanh(), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.float(result) -} - -/// Evaluates PHP two-argument floating-point math builtins over eval expressions. -fn eval_builtin_float_pair( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_float_pair_result(name, left, right, values) -} - -/// Dispatches an evaluated pair through PHP `atan2()` or `hypot()`. -fn eval_float_pair_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left = eval_float_value(left, values)?; - let right = eval_float_value(right, values)?; - let result = match name { - "atan2" => left.atan2(right), - "hypot" => left.hypot(right), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.float(result) -} - -/// Evaluates PHP `log($num, $base = e)` over eval expressions. -fn eval_builtin_log( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [num] => { - let num = eval_expr(num, context, scope, values)?; - eval_log_result(num, None, values) - } - [num, base] => { - let num = eval_expr(num, context, scope, values)?; - let base = eval_expr(base, context, scope, values)?; - eval_log_result(num, Some(base), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `log()` from already evaluated arguments. -fn eval_log_result( - num: RuntimeCellHandle, - base: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let num = eval_float_value(num, values)?; - let result = match base { - Some(base) => num.log(eval_float_value(base, values)?), - None => num.ln(), - }; - values.float(result) -} - -/// Evaluates PHP `intdiv(...)` over two eval expressions. -fn eval_builtin_intdiv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_intdiv_result(left, right, values) -} - -/// Computes PHP integer division from already evaluated arguments. -fn eval_intdiv_result( - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left = eval_int_value(left, values)?; - let right = eval_int_value(right, values)?; - if right == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; - values.int(result) -} - -/// Evaluates PHP floating-point binary math builtins over two eval expressions. -fn eval_builtin_float_binary( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_float_binary_result(name, left, right, values) -} - -/// Dispatches an evaluated pair through the matching PHP float math hook. -fn eval_float_binary_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "fdiv" => values.fdiv(left, right), - "fmod" => values.fmod(left, right), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `clamp($value, $min, $max)` over three eval expressions. -fn eval_builtin_clamp( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value, min, max] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_clamp_result(value, min, max, values) -} - -/// Selects the inclusive clamp result after validating bound order and NaN bounds. -fn eval_clamp_result( - value: RuntimeCellHandle, - min: RuntimeCellHandle, - max: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; - if values.truthy(invalid_bounds)? { - return Err(EvalStatus::RuntimeFatal); - } - let above_max = values.compare(EvalBinOp::Gt, value, max)?; - if values.truthy(above_max)? { - return Ok(max); - } - let below_min = values.compare(EvalBinOp::Lt, value, min)?; - if values.truthy(below_min)? { - return Ok(min); - } - Ok(value) -} - -/// Returns whether a clamp bound is a floating-point NaN value. -fn eval_clamp_bound_is_nan( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? != EVAL_TAG_FLOAT { - return Ok(false); - } - Ok(eval_float_value(value, values)?.is_nan()) -} - -/// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. -fn eval_builtin_min_max( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_min_max_result(name, &evaluated_args, values) -} - -/// Selects the smallest or largest evaluated cell using runtime comparison hooks. -fn eval_min_max_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((&first, rest)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let op = match name { - "min" => EvalBinOp::Lt, - "max" => EvalBinOp::Gt, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let mut selected = first; - for candidate in rest { - let better = values.compare(op, *candidate, selected)?; - if values.truthy(better)? { - selected = *candidate; - } - } - Ok(selected) -} - -/// Evaluates PHP scalar cast builtins over one eval expression. -fn eval_builtin_cast( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_cast_result(name, value, values) -} - -/// Dispatches an already evaluated value through the matching PHP cast hook. -fn eval_cast_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "intval" => values.cast_int(value), - "floatval" => values.cast_float(value), - "strval" => values.cast_string(value), - "boolval" => values.cast_bool(value), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP's `gettype(...)` over one eval expression. -fn eval_builtin_gettype( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_gettype_result(value, values) -} - -/// Converts one boxed runtime tag into PHP's `gettype()` spelling. -fn eval_gettype_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - values.string(eval_gettype_name(tag)) -} - -/// Evaluates PHP's `get_class(...)` over one eval object expression. -fn eval_builtin_get_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_get_class_result(object, context, values) -} - -/// Resolves the PHP-visible class name for one already materialized object cell. -fn eval_get_class_result( - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Ok(identity) = values.object_identity(object) { - if let Some(class) = context.dynamic_object_class(identity) { - return values.string(class.name().trim_start_matches('\\')); - } - } - values.object_class_name(object) -} - -/// Evaluates PHP's SPL object identity builtins over one eval object expression. -fn eval_builtin_spl_object_identity( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_spl_object_identity_result(name, object, values) -} - -/// Returns the unboxed object-payload identity in the native SPL builtin spelling. -fn eval_spl_object_identity_result( - name: &str, - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let identity = values.object_identity(object)? as i64; - match name { - "spl_object_id" => values.int(identity), - "spl_object_hash" => values.string(&identity.to_string()), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. -fn eval_builtin_get_parent_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object_or_class] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object_or_class = eval_expr(object_or_class, context, scope, values)?; - eval_get_parent_class_result(object_or_class, values) -} - -/// Resolves the PHP-visible parent class name for one object or class-name cell. -fn eval_get_parent_class_result( - object_or_class: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - values.parent_class_name(object_or_class) -} - -/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. -fn eval_builtin_resource_introspection( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [resource] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let resource = eval_expr(resource, context, scope, values)?; - eval_resource_introspection_result(name, resource, values) -} - -/// Evaluates a materialized resource introspection builtin argument. -fn eval_resource_introspection_result( - name: &str, - resource: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(resource)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - match name { - "get_resource_type" => values.string("stream"), - "get_resource_id" => values.cast_int(resource), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Returns the PHP-visible type name for a concrete eval runtime tag. -fn eval_gettype_name(tag: u64) -> &'static str { - match tag { - EVAL_TAG_INT => "integer", - EVAL_TAG_FLOAT => "double", - EVAL_TAG_STRING => "string", - EVAL_TAG_BOOL => "boolean", - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", - EVAL_TAG_OBJECT => "object", - EVAL_TAG_RESOURCE => "resource", - EVAL_TAG_NULL => "NULL", - _ => "NULL", - } -} - -/// Evaluates PHP scalar/container type predicate builtins over one eval expression. -fn eval_builtin_type_predicate( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_type_predicate_result(name, value, values) -} - -/// Converts a concrete runtime tag into a PHP `is_*` predicate result. -fn eval_type_predicate_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - let result = match name { - "is_int" | "is_integer" | "is_long" => tag == EVAL_TAG_INT, - "is_float" | "is_double" | "is_real" => tag == EVAL_TAG_FLOAT, - "is_string" => tag == EVAL_TAG_STRING, - "is_bool" => tag == EVAL_TAG_BOOL, - "is_null" => tag == EVAL_TAG_NULL, - "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), - "is_object" => tag == EVAL_TAG_OBJECT, - "is_resource" => tag == EVAL_TAG_RESOURCE, - "is_nan" => eval_float_value(value, values)?.is_nan(), - "is_infinite" => eval_float_value(value, values)?.is_infinite(), - "is_finite" => eval_float_value(value, values)?.is_finite(), - "is_numeric" => { - tag == EVAL_TAG_INT - || tag == EVAL_TAG_FLOAT - || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)) - } - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(result) -} - -/// Matches the static backend's legacy ASCII numeric-string scan. -fn eval_is_numeric_string(bytes: &[u8]) -> bool { - if bytes.is_empty() { - return false; - } - - let mut index = 0; - let mut consumed_digits = 0; - if bytes[index] == b'-' { - index += 1; - if index >= bytes.len() { - return false; - } - } - - while index < bytes.len() { - if bytes[index] == b'.' { - index += 1; - break; - } - if !bytes[index].is_ascii_digit() { - return false; - } - consumed_digits += 1; - index += 1; - } - - while index < bytes.len() { - if !bytes[index].is_ascii_digit() { - return false; - } - consumed_digits += 1; - index += 1; - } - - consumed_digits > 0 -} - -/// Evaluates PHP's `hash_equals(...)` over two eval expressions. -fn eval_builtin_hash_equals( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [known, user] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let known = eval_expr(known, context, scope, values)?; - let user = eval_expr(user, context, scope, values)?; - eval_hash_equals_result(known, user, values) -} - -/// Compares two converted strings with PHP `hash_equals()` semantics. -fn eval_hash_equals_result( - known: RuntimeCellHandle, - user: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let known = values.string_bytes(known)?; - let user = values.string_bytes(user)?; - if known.len() != user.len() { - return values.bool_value(false); - } - let mut diff = 0u8; - for (known, user) in known.iter().zip(user.iter()) { - diff |= known ^ user; - } - values.bool_value(diff == 0) -} - -/// Evaluates PHP string comparison builtins over two eval expressions. -fn eval_builtin_string_compare( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_string_compare_result(name, left, right, values) -} - -/// Compares two converted strings and returns -1, 0, or 1. -fn eval_string_compare_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut left = values.string_bytes(left)?; - let mut right = values.string_bytes(right)?; - match name { - "strcmp" => {} - "strcasecmp" => { - left.make_ascii_lowercase(); - right.make_ascii_lowercase(); - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - let result = match left.cmp(&right) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - }; - values.int(result) -} - -/// Evaluates PHP's byte-string search predicates over two eval expressions. -fn eval_builtin_string_search( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_string_search_result(name, haystack, needle, values) -} - -/// Checks one converted haystack for one converted needle using PHP byte-string semantics. -fn eval_string_search_result( - name: &str, - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let matched = match name { - "str_contains" => { - needle.is_empty() - || haystack - .windows(needle.len()) - .any(|window| window == needle) - } - "str_starts_with" => haystack.starts_with(&needle), - "str_ends_with" => haystack.ends_with(&needle), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(matched) -} - -/// Evaluates PHP byte-string position builtins over two eval expressions. -fn eval_builtin_string_position( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_string_position_result(name, haystack, needle, values) -} - -/// Returns the first or last byte offset of a converted needle, or PHP `false`. -fn eval_string_position_result( - name: &str, - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let position = match name { - "strpos" if needle.is_empty() => Some(0), - "strpos" => haystack - .windows(needle.len()) - .position(|window| window == needle), - "strrpos" if needle.is_empty() => Some(haystack.len()), - "strrpos" => haystack - .windows(needle.len()) - .rposition(|window| window == needle), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - match position { - Some(position) => { - let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(position) - } - None => values.bool_value(false), - } -} - -/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. -fn eval_builtin_strstr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [haystack, needle] => { - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_strstr_result(haystack, needle, false, values) - } - [haystack, needle, before_needle] => { - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - let before_needle = eval_expr(before_needle, context, scope, values)?; - let before_needle = values.truthy(before_needle)?; - eval_strstr_result(haystack, needle, before_needle, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. -fn eval_strstr_result( - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - before_needle: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let position = if needle.is_empty() { - Some(0) - } else { - eval_find_subslice(&haystack, &needle, 0) - }; - let Some(position) = position else { - return values.bool_value(false); - }; - let result = if before_needle { - &haystack[..position] - } else { - &haystack[position..] - }; - values.string_bytes_value(result) -} - -const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; - -/// Evaluates PHP trim-like string builtins over one eval expression and optional mask. -fn eval_builtin_trim_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_trim_like_result(name, value, None, values) - } - [value, mask] => { - let value = eval_expr(value, context, scope, values)?; - let mask = eval_expr(mask, context, scope, values)?; - eval_trim_like_result(name, value, Some(mask), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Trims one converted string using PHP's default mask or a caller-provided byte mask. -fn eval_trim_like_result( - name: &str, - value: RuntimeCellHandle, - mask: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let explicit_mask; - let trim_mask = if let Some(mask) = mask { - explicit_mask = values.string_bytes(mask)?; - explicit_mask.as_slice() - } else { - PHP_DEFAULT_TRIM_MASK - }; - - let mut start = 0; - let mut end = bytes.len(); - if matches!(name, "trim" | "ltrim") { - while start < end && trim_mask.contains(&bytes[start]) { - start += 1; - } - } - if matches!(name, "trim" | "rtrim" | "chop") { - while end > start && trim_mask.contains(&bytes[end - 1]) { - end -= 1; - } - } - if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { - return Err(EvalStatus::UnsupportedConstruct); - } - - let value = - String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(&value) -} - -/// Evaluates PHP ASCII case-conversion string builtins over one eval expression. -fn eval_builtin_string_case( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_string_case_result(name, value, values) -} - -/// Converts one eval value through PHP string conversion and ASCII case mapping. -fn eval_string_case_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bytes = values.string_bytes(value)?; - match name { - "strtolower" => { - for byte in &mut bytes { - if byte.is_ascii_uppercase() { - *byte += b'a' - b'A'; - } - } - } - "strtoupper" => { - for byte in &mut bytes { - if byte.is_ascii_lowercase() { - *byte -= b'a' - b'A'; - } - } - } - "ucfirst" => { - if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { - bytes[0] -= b'a' - b'A'; - } - } - "lcfirst" => { - if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { - bytes[0] += b'a' - b'A'; - } - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(&value) -} - -/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. -fn eval_builtin_ucwords( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_ucwords_result(value, None, values) - } - [value, separators] => { - let value = eval_expr(value, context, scope, values)?; - let separators = eval_expr(separators, context, scope, values)?; - eval_ucwords_result(value, Some(separators), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. -fn eval_ucwords_result( - value: RuntimeCellHandle, - separators: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bytes = values.string_bytes(value)?; - let separators = match separators { - Some(separators) => values.string_bytes(separators)?, - None => b" \t\r\n\x0c\x0b".to_vec(), - }; - let mut word_start = true; - for byte in &mut bytes { - if separators.contains(byte) { - word_start = true; - } else if word_start { - if byte.is_ascii_lowercase() { - *byte -= b'a' - b'A'; - } - word_start = false; - } - } - values.string_bytes_value(&bytes) -} - -/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. -fn eval_builtin_wordwrap( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_wordwrap_result(value, None, None, None, values) - } - [value, width] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - eval_wordwrap_result(value, Some(width), None, None, values) - } - [value, width, break_string] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - let break_string = eval_expr(break_string, context, scope, values)?; - eval_wordwrap_result(value, Some(width), Some(break_string), None, values) - } - [value, width, break_string, cut] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - let break_string = eval_expr(break_string, context, scope, values)?; - let cut = eval_expr(cut, context, scope, values)?; - eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Wraps a byte string at PHP word boundaries and preserves existing newlines. -fn eval_wordwrap_result( - value: RuntimeCellHandle, - width: Option, - break_string: Option, - cut: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let width = match width { - Some(width) => eval_int_value(width, values)?, - None => 75, - }; - let break_string = match break_string { - Some(break_string) => values.string_bytes(break_string)?, - None => b"\n".to_vec(), - }; - if break_string.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let cut = match cut { - Some(cut) => values.truthy(cut)?, - None => false, - }; - if width == 0 && cut { - return Err(EvalStatus::RuntimeFatal); - } - if bytes.is_empty() { - return values.string_bytes_value(&bytes); - } - let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); - values.string_bytes_value(&output) -} - -/// Applies the core PHP word-wrap scan over already converted byte slices. -fn eval_wordwrap_bytes(bytes: &[u8], width: i64, break_string: &[u8], cut: bool) -> Vec { - if width < 0 && cut { - let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); - for byte in bytes { - output.extend_from_slice(break_string); - output.push(*byte); - } - return output; - } - - let width = width.max(0) as usize; - let mut output = Vec::with_capacity(bytes.len()); - let mut line_start = 0; - let mut last_space = None; - let mut index = 0; - while index < bytes.len() { - match bytes[index] { - b'\n' => { - output.extend_from_slice(&bytes[line_start..=index]); - index += 1; - line_start = index; - last_space = None; - } - b' ' => { - if index.saturating_sub(line_start) >= width { - output.extend_from_slice(&bytes[line_start..index]); - output.extend_from_slice(break_string); - index += 1; - line_start = index; - last_space = None; - } else { - last_space = Some(index); - index += 1; - } - } - _ if index.saturating_sub(line_start) >= width => { - if let Some(space) = last_space { - output.extend_from_slice(&bytes[line_start..space]); - output.extend_from_slice(break_string); - line_start = space + 1; - last_space = None; - } else if cut && width > 0 { - output.extend_from_slice(&bytes[line_start..index]); - output.extend_from_slice(break_string); - line_start = index; - } else { - index += 1; - } - } - _ => { - index += 1; - } - } - } - output.extend_from_slice(&bytes[line_start..]); - output -} - -/// Evaluates nested `eval(...)` calls against the current materialized scope. -fn eval_nested_eval( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [code] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let code = eval_expr(code, context, scope, values)?; - let code = values.string_bytes(code)?; - let program = parse_fragment(&code).map_err(EvalParseError::status)?; - execute_program_with_context(context, &program, scope, values) -} - -/// Evaluates an eval-fragment include or require expression. -fn eval_include_expr( - path: &EvalExpr, - required: bool, - once: bool, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_expr(path, context, scope, values)?; - let path = eval_path_string(path, values)?; - let resolved_path = eval_resolve_include_path(&path, context); - let include_key = eval_include_key(&resolved_path); - if once && context.has_included_file(&include_key) { - return values.bool_value(true); - } - let bytes = match std::fs::read(&resolved_path) { - Ok(bytes) => bytes, - Err(_) => return eval_include_missing_file(&path, required, values), - }; - context.mark_included_file(include_key); - eval_execute_include_bytes(&bytes, &resolved_path, context, scope, values) -} - -/// Returns the include/require result for a file that cannot be opened. -fn eval_include_missing_file( - path: &str, - required: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let construct = if required { "require" } else { "include" }; - values.warning(&format!( - "Warning: {construct}({path}): Failed to open stream: No such file or directory\n" - ))?; - values.warning(&format!( - "Warning: {construct}(): Failed opening '{path}' for inclusion\n" - ))?; - if required { - Err(EvalStatus::RuntimeFatal) - } else { - values.bool_value(false) - } -} - -/// Resolves eval include paths using PHP's cwd-first and caller-directory fallback. -fn eval_resolve_include_path(path: &str, context: &ElephcEvalContext) -> std::path::PathBuf { - let raw_path = std::path::Path::new(path); - if raw_path.is_absolute() || raw_path.exists() { - return raw_path.to_path_buf(); - } - if context.call_dir().is_empty() { - return raw_path.to_path_buf(); - } - let caller_path = std::path::Path::new(context.call_dir()).join(raw_path); - if caller_path.exists() { - caller_path - } else { - raw_path.to_path_buf() - } -} - -/// Builds the stable include_once key for a resolved path. -fn eval_include_key(path: &std::path::Path) -> String { - std::fs::canonicalize(path) - .unwrap_or_else(|_| path.to_path_buf()) - .to_string_lossy() - .into_owned() -} - -/// Executes a local include file, alternating raw output and PHP code blocks. -fn eval_execute_include_bytes( - bytes: &[u8], - path: &std::path::Path, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut cursor = 0; - while let Some((tag_start, code_start)) = eval_find_php_open_tag(bytes, cursor) { - eval_echo_include_bytes(&bytes[cursor..tag_start], values)?; - let close = eval_find_php_close_tag(bytes, code_start); - let code_end = close.unwrap_or(bytes.len()); - match eval_execute_include_code(&bytes[code_start..code_end], path, context, scope, values)? - { - EvalControl::None => {} - EvalControl::Return(value) => return Ok(value), - EvalControl::Throw(value) => { - context.set_pending_throw(value); - return Err(EvalStatus::UncaughtThrowable); - } - EvalControl::Break | EvalControl::Continue => { - return Err(EvalStatus::UnsupportedConstruct); - } - } - let Some(close) = close else { - return values.int(1); - }; - cursor = close + 2; - } - eval_echo_include_bytes(&bytes[cursor..], values)?; - values.int(1) -} - -/// Parses and executes one PHP code block from an included file. -fn eval_execute_include_code( - code: &[u8], - path: &std::path::Path, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let program = parse_fragment(code).map_err(EvalParseError::status)?; - let previous = context.call_site(); - let file = path.to_string_lossy().into_owned(); - let dir = path - .parent() - .map(|parent| parent.to_string_lossy().into_owned()) - .unwrap_or_default(); - context.set_call_site(file.clone(), dir, 1); - context.set_file_magic_override(Some(file)); - let result = execute_statements(program.statements(), context, scope, values); - context.set_call_site(previous.0, previous.1, previous.2); - context.set_file_magic_override(previous.3); - result -} - -/// Echoes raw non-PHP include bytes through the eval value hooks. -fn eval_echo_include_bytes( - bytes: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if bytes.is_empty() { - return Ok(()); - } - let output = values.string_bytes_value(bytes)?; - values.echo(output) -} - -/// Finds the next ` Option<(usize, usize)> { - bytes - .get(start..)? - .windows(5) - .position(eval_is_php_open_tag) - .map(|offset| { - let tag_start = start + offset; - (tag_start, tag_start + 5) - }) -} - -/// Returns true when a five-byte window is a case-insensitive ` bool { - window.len() == 5 - && window[0] == b'<' - && window[1] == b'?' - && window[2].eq_ignore_ascii_case(&b'p') - && window[3].eq_ignore_ascii_case(&b'h') - && window[4].eq_ignore_ascii_case(&b'p') -} - -/// Finds the next PHP closing tag after a code block start. -fn eval_find_php_close_tag(bytes: &[u8], start: usize) -> Option { - bytes - .get(start..)? - .windows(2) - .position(|window| window == b"?>") - .map(|offset| start + offset) -} - -/// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. -fn eval_builtin_strlen( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let bytes = values.string_bytes(value)?; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len) -} - -/// Evaluates the builtin `ord(...)` for the first byte of one coerced string. -fn eval_builtin_ord( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_ord_result(value, values) -} - -/// Returns the first byte of one converted string, or zero for an empty string. -fn eval_ord_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.int(i64::from(bytes.first().copied().unwrap_or(0))) -} - -/// Evaluates the builtin `count(...)` for one runtime array-like argument. -fn eval_builtin_count( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_count_result(value, None, values) - } - [value, mode] => { - let value = eval_expr(value, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_count_result(value, Some(mode), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Counts an eval array with PHP normal or recursive mode semantics. -fn eval_count_result( - value: RuntimeCellHandle, - mode: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = match mode { - Some(mode) => eval_int_value(mode, values)?, - None => EVAL_COUNT_NORMAL, + let mode = match mode { + Some(mode) => eval_int_value(mode, values)?, + None => EVAL_COUNT_NORMAL, }; let len = match mode { EVAL_COUNT_NORMAL => values.array_len(value)?, @@ -15867,6 +4151,5 @@ pub fn current_stub_status() -> EvalStatus { EvalStatus::UnsupportedConstruct } - #[cfg(test)] mod tests; From b3864a3cd5bc51dee4398c634e16ca39ec47a9c0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:03:53 +0200 Subject: [PATCH 0283/1208] Split eval interpreter builtins by domain --- .../elephc-eval/src/interpreter/builtins.rs | 11733 ---------------- .../src/interpreter/builtins/arrays/access.rs | 487 + .../src/interpreter/builtins/arrays/core.rs | 402 + .../interpreter/builtins/arrays/filters.rs | 657 + .../src/interpreter/builtins/arrays/mod.rs | 27 + .../interpreter/builtins/arrays/mutation.rs | 224 + .../interpreter/builtins/arrays/push_pop.rs | 312 + .../src/interpreter/builtins/arrays/sort.rs | 598 + .../src/interpreter/builtins/arrays/splice.rs | 340 + .../builtins/filesystem/file_io.rs | 414 + .../builtins/filesystem/fnmatch.rs | 350 + .../interpreter/builtins/filesystem/mod.rs | 19 + .../interpreter/builtins/filesystem/ops.rs | 599 + .../interpreter/builtins/filesystem/path.rs | 341 + .../src/interpreter/builtins/formatting.rs | 814 ++ .../src/interpreter/builtins/mod.rs | 34 + .../src/interpreter/builtins/network_env.rs | 572 + .../src/interpreter/builtins/regex.rs | 858 ++ .../interpreter/builtins/registry/binding.rs | 267 + .../interpreter/builtins/registry/callable.rs | 206 + .../interpreter/builtins/registry/dispatch.rs | 1096 ++ .../src/interpreter/builtins/registry/mod.rs | 20 + .../interpreter/builtins/registry/names.rs | 291 + .../src/interpreter/builtins/scalars.rs | 1338 ++ .../src/interpreter/builtins/strings.rs | 982 ++ .../src/interpreter/builtins/symbols.rs | 406 + .../src/interpreter/builtins/time.rs | 570 + 27 files changed, 12224 insertions(+), 11733 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/access.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/core.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/mutation.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/push_pop.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/sort.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/splice.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/file_io.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/fnmatch.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/binding.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/callable.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/names.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/symbols.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/time.rs diff --git a/crates/elephc-eval/src/interpreter/builtins.rs b/crates/elephc-eval/src/interpreter/builtins.rs deleted file mode 100644 index cea21038d4..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins.rs +++ /dev/null @@ -1,11733 +0,0 @@ -//! Purpose: -//! Implements eval support for PHP-visible builtins and language-construct helpers. -//! This module owns builtin argument binding, direct builtin execution, callable -//! builtin dispatch, and per-domain helper routines. -//! -//! Called from: -//! - `crate::interpreter::eval_call()` and positional builtin dispatch paths. -//! -//! Key details: -//! - The module is a child of `interpreter` so it can reuse core EvalIR execution -//! helpers without widening crate-level visibility. -//! - Runtime value creation and PHP coercions still flow through `RuntimeValueOps`. - -use super::*; - -/// Evaluates string-name function probes against eval and supported builtin tables. -pub(super) fn eval_builtin_function_probe( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\').to_ascii_lowercase(); - values.bool_value(eval_function_probe_exists(context, &name)) -} - -/// Evaluates `define(name, value)` for eval dynamic constant-name registration. -pub(super) fn eval_builtin_define( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - let defined = eval_define_name(name, value, context, values)?; - values.bool_value(defined) -} - -/// Evaluates `defined(name)` against eval dynamic constant names. -pub(super) fn eval_builtin_defined( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - let exists = eval_defined_name(name, context, values)?; - values.bool_value(exists) -} - -/// Evaluates `define(...)` from already materialized call arguments. -fn eval_define_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let defined = eval_define_name(*name, *value, context, values)?; - values.bool_value(defined) -} - -/// Evaluates `defined(...)` from already materialized call arguments. -fn eval_defined_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let exists = eval_defined_name(*name, context, values)?; - values.bool_value(exists) -} - -/// Normalizes and registers one eval dynamic constant name. -fn eval_define_name( - name: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_constant_name(name, values)?; - if name.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - if eval_predefined_constant_value(&name).is_some() || context.has_constant(&name) { - values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; - return Ok(false); - } - let value = values.retain(value)?; - if context.define_constant(&name, value) { - Ok(true) - } else { - values.release(value)?; - Ok(false) - } -} - -/// Normalizes and probes one eval dynamic constant name. -fn eval_defined_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_constant_name(name, values)?; - Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) -} - -/// Reads a PHP constant name from a runtime cell without changing case. -fn eval_constant_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. -pub(super) fn eval_builtin_class_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = match args { - [name] => eval_expr(name, context, scope, values)?, - [name, autoload] => { - let name = eval_expr(name, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - name - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_class_exists_name(name, context, values)?; - values.bool_value(exists) -} - -/// Evaluates `class_exists(...)` from already materialized call arguments. -fn eval_class_exists_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [name] => eval_class_exists_name(*name, context, values)?, - [name, _autoload] => eval_class_exists_name(*name, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. -fn eval_class_exists_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\'); - if context.has_class(name) { - return Ok(true); - } - values.class_exists(name) -} - -/// Evaluates `interface_exists(...)` against generated interface-name metadata. -pub(super) fn eval_builtin_interface_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = match args { - [name] => eval_expr(name, context, scope, values)?, - [name, autoload] => { - let name = eval_expr(name, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - name - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_interface_exists_name(name, values)?; - values.bool_value(exists) -} - -/// Evaluates `interface_exists(...)` from already materialized call arguments. -fn eval_interface_exists_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [name] => eval_interface_exists_name(*name, values)?, - [name, _autoload] => eval_interface_exists_name(*name, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP interface-name cell and probes generated interface metadata. -fn eval_interface_exists_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - values.interface_exists(name.trim_start_matches('\\')) -} - -/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. -pub(super) fn eval_builtin_class_like_exists( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let symbol = match args { - [symbol] => eval_expr(symbol, context, scope, values)?, - [symbol, autoload] => { - let symbol = eval_expr(symbol, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - symbol - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_class_like_exists_name(name, symbol, values)?; - values.bool_value(exists) -} - -/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. -fn eval_class_like_exists_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [symbol] => eval_class_like_exists_name(name, *symbol, values)?, - [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. -fn eval_class_like_exists_name( - name: &str, - symbol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let symbol = values.string_bytes(symbol)?; - let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; - let symbol = symbol.trim_start_matches('\\'); - match name { - "trait_exists" => values.trait_exists(symbol), - "enum_exists" => values.enum_exists(symbol), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. -pub(super) fn eval_builtin_is_a_relation( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_is_a_relation_result(name, &evaluated_args, context, values) -} - -/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. -fn eval_is_a_relation_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (object_or_class, target_class, allow_string) = match evaluated_args { - [object_or_class, target_class] => { - (*object_or_class, *target_class, name == "is_subclass_of") - } - [object_or_class, target_class, allow_string] => ( - *object_or_class, - *target_class, - values.truthy(*allow_string)?, - ), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let target_class = values.string_bytes(target_class)?; - let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_class = target_class.trim_start_matches('\\'); - let is_object = values.type_tag(object_or_class)? == 6; - let result = - if is_object && dynamic_object_is_a(object_or_class, target_class, context, values)? { - !matches!(name, "is_subclass_of") - } else if is_object || allow_string { - values.object_is_a(object_or_class, target_class, name == "is_subclass_of")? - } else { - false - }; - values.bool_value(result) -} - -/// Returns whether an eval-created object matches a dynamic class name exactly. -fn dynamic_object_is_a( - object: RuntimeCellHandle, - target_class: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - Ok(context - .dynamic_object_class(identity) - .is_some_and(|class| class.name().eq_ignore_ascii_case(target_class))) -} - -/// Evaluates PHP's `isset(...)` language construct over eval-visible values. -pub(super) fn eval_builtin_isset( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return values.bool_value(false); - } - for arg in args { - if !eval_isset_arg(arg, context, scope, values)? { - return values.bool_value(false); - } - } - values.bool_value(true) -} - -/// Evaluates PHP's `empty(...)` language construct over eval-visible values. -pub(super) fn eval_builtin_empty( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let empty = eval_empty_arg(arg, context, scope, values)?; - values.bool_value(empty) -} - -/// Evaluates one `empty` operand without warning or failing on missing variables. -fn eval_empty_arg( - arg: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let EvalExpr::LoadVar(name) = arg { - let Some(value) = visible_scope_cell(context, scope, name) else { - return Ok(true); - }; - return Ok(!values.truthy(value)?); - } - let value = eval_expr(arg, context, scope, values)?; - Ok(!values.truthy(value)?) -} - -/// Evaluates one `isset` operand without allocating a null cell for missing variables. -fn eval_isset_arg( - arg: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let EvalExpr::LoadVar(name) = arg { - let Some(value) = visible_scope_cell(context, scope, name) else { - return Ok(false); - }; - return Ok(!values.is_null(value)?); - } - let value = eval_expr(arg, context, scope, values)?; - Ok(!values.is_null(value)?) -} - -/// Returns true when a PHP function name is visible to eval builtin probes. -fn eval_function_probe_exists(context: &ElephcEvalContext, name: &str) -> bool { - !name.contains("::") && (context.has_function(name) || eval_php_visible_builtin_exists(name)) -} - -/// Returns true for PHP-visible builtin names implemented by the eval interpreter. -pub(super) fn eval_php_visible_builtin_exists(name: &str) -> bool { - matches!( - name, - "abs" - | "addslashes" - | "array_chunk" - | "array_column" - | "array_combine" - | "array_fill" - | "array_fill_keys" - | "array_filter" - | "array_flip" - | "array_map" - | "array_reduce" - | "array_walk" - | "array_key_exists" - | "array_keys" - | "array_diff" - | "array_intersect" - | "array_diff_key" - | "array_intersect_key" - | "array_merge" - | "array_pad" - | "array_pop" - | "array_product" - | "array_push" - | "array_rand" - | "array_reverse" - | "array_search" - | "array_shift" - | "array_slice" - | "array_splice" - | "array_sum" - | "array_unique" - | "array_unshift" - | "array_values" - | "arsort" - | "asort" - | "acos" - | "asin" - | "atan" - | "atan2" - | "basename" - | "base64_decode" - | "base64_encode" - | "bin2hex" - | "ceil" - | "chdir" - | "chmod" - | "call_user_func" - | "call_user_func_array" - | "class_exists" - | "enum_exists" - | "interface_exists" - | "is_a" - | "is_subclass_of" - | "boolval" - | "chop" - | "chr" - | "clamp" - | "clearstatcache" - | "count" - | "copy" - | "cos" - | "cosh" - | "crc32" - | "ctype_alnum" - | "ctype_alpha" - | "ctype_digit" - | "ctype_space" - | "date" - | "define" - | "defined" - | "deg2rad" - | "dirname" - | "disk_free_space" - | "disk_total_space" - | "exp" - | "explode" - | "fdiv" - | "file" - | "file_exists" - | "fileatime" - | "filectime" - | "filegroup" - | "file_get_contents" - | "fileinode" - | "filemtime" - | "fileowner" - | "fileperms" - | "file_put_contents" - | "filesize" - | "filetype" - | "fnmatch" - | "floor" - | "floatval" - | "fmod" - | "function_exists" - | "gethostbyaddr" - | "gethostbyname" - | "gethostname" - | "getprotobyname" - | "getprotobynumber" - | "getservbyname" - | "getservbyport" - | "get_class" - | "get_parent_class" - | "get_resource_id" - | "get_resource_type" - | "getcwd" - | "getenv" - | "gettype" - | "glob" - | "hash" - | "hash_algos" - | "hash_equals" - | "hash_file" - | "hash_hmac" - | "hex2bin" - | "html_entity_decode" - | "htmlentities" - | "htmlspecialchars" - | "hypot" - | "implode" - | "in_array" - | "inet_ntop" - | "inet_pton" - | "intdiv" - | "ip2long" - | "is_dir" - | "is_executable" - | "is_file" - | "is_link" - | "is_readable" - | "is_writable" - | "is_writeable" - | "intval" - | "link" - | "linkinfo" - | "ltrim" - | "is_callable" - | "is_array" - | "is_bool" - | "is_double" - | "is_finite" - | "is_float" - | "is_infinite" - | "is_int" - | "is_integer" - | "is_iterable" - | "is_long" - | "is_nan" - | "is_null" - | "is_numeric" - | "is_object" - | "is_real" - | "is_resource" - | "is_string" - | "iterator_apply" - | "iterator_count" - | "iterator_to_array" - | "json_decode" - | "json_encode" - | "json_last_error" - | "json_last_error_msg" - | "json_validate" - | "krsort" - | "ksort" - | "lcfirst" - | "log" - | "log2" - | "log10" - | "long2ip" - | "max" - | "md5" - | "microtime" - | "min" - | "mkdir" - | "mktime" - | "mt_rand" - | "natcasesort" - | "natsort" - | "nl2br" - | "number_format" - | "ord" - | "pathinfo" - | "pi" - | "pow" - | "php_uname" - | "phpversion" - | "preg_match" - | "preg_match_all" - | "preg_replace" - | "preg_replace_callback" - | "preg_split" - | "putenv" - | "print_r" - | "rand" - | "random_int" - | "range" - | "rad2deg" - | "rawurldecode" - | "rawurlencode" - | "readfile" - | "readlink" - | "realpath" - | "realpath_cache_get" - | "realpath_cache_size" - | "rename" - | "rsort" - | "rtrim" - | "round" - | "rmdir" - | "scandir" - | "settype" - | "sleep" - | "sha1" - | "shuffle" - | "sin" - | "sinh" - | "sort" - | "sqrt" - | "spl_classes" - | "spl_object_hash" - | "spl_object_id" - | "sscanf" - | "sprintf" - | "strcasecmp" - | "stream_get_filters" - | "stream_get_transports" - | "stream_get_wrappers" - | "str_contains" - | "str_ends_with" - | "str_ireplace" - | "str_repeat" - | "str_replace" - | "str_starts_with" - | "strcmp" - | "stat" - | "strlen" - | "strpos" - | "strrpos" - | "strrev" - | "str_pad" - | "str_split" - | "strstr" - | "strtotime" - | "substr" - | "stripslashes" - | "strtolower" - | "strtoupper" - | "strval" - | "symlink" - | "sys_get_temp_dir" - | "tempnam" - | "tan" - | "tanh" - | "time" - | "touch" - | "trait_exists" - | "trim" - | "substr_replace" - | "ucfirst" - | "ucwords" - | "uasort" - | "uksort" - | "unlink" - | "umask" - | "urldecode" - | "urlencode" - | "usort" - | "usleep" - | "var_dump" - | "printf" - | "vprintf" - | "vsprintf" - | "wordwrap" - | "lstat" - ) -} - -/// Evaluates a direct PHP-visible builtin call with named or spread arguments. -pub(super) fn eval_builtin_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; - let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { - return Err(EvalStatus::UnsupportedConstruct); - }; - Ok(result) -} - -/// Binds evaluated builtin arguments to PHP parameter order when names are used. -fn bind_evaluated_builtin_args( - name: &str, - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if evaluated_args.iter().all(|arg| arg.name.is_none()) { - return Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()); - } - - let params = eval_builtin_param_names(name).ok_or(EvalStatus::RuntimeFatal)?; - let mut bound_args = vec![None; params.len()]; - let mut next_positional = 0; - - for arg in evaluated_args { - if let Some(name) = arg.name { - bind_builtin_named_arg(params, &mut bound_args, &name, arg.value)?; - } else { - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; - } - } - - collect_bound_builtin_args(name, bound_args, values) -} - -/// Binds one named builtin-call value to the matching PHP parameter slot. -fn bind_builtin_named_arg( - params: &[&str], - bound_args: &mut [Option], - name: &str, - value: RuntimeCellHandle, -) -> Result<(), EvalStatus> { - let Some(param_index) = params.iter().position(|param| *param == name) else { - return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[param_index] = Some(value); - Ok(()) -} - -/// Collects ordered bound arguments, rejecting gaps where defaults would be needed. -fn collect_contiguous_bound_args( - bound_args: Vec>, -) -> Result, EvalStatus> { - let Some(last_index) = bound_args.iter().rposition(Option::is_some) else { - return Ok(Vec::new()); - }; - bound_args - .into_iter() - .take(last_index + 1) - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Collects ordered builtin arguments, applying PHP defaults for special named-call gaps. -fn collect_bound_builtin_args( - name: &str, - mut bound_args: Vec>, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if name == "array_splice" && bound_args.get(3).is_some_and(Option::is_some) { - if bound_args.get(2).is_some_and(Option::is_none) { - bound_args[2] = Some(values.null()?); - } - } - collect_contiguous_bound_args(bound_args) -} - -/// Returns PHP parameter names for builtin calls implemented by eval. -fn eval_builtin_param_names(name: &str) -> Option<&'static [&'static str]> { - match name { - "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), - "array_chunk" => Some(&["array", "length"]), - "array_column" => Some(&["array", "column_key"]), - "array_combine" => Some(&["keys", "values"]), - "array_fill" => Some(&["start_index", "count", "value"]), - "array_fill_keys" => Some(&["keys", "value"]), - "array_filter" => Some(&["array", "callback", "mode"]), - "array_map" => Some(&["callback", "array"]), - "array_reduce" => Some(&["array", "callback", "initial"]), - "array_walk" => Some(&["array", "callback"]), - "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), - "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" - | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" | "asort" - | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { - Some(&["array"]) - } - "array_push" | "array_unshift" => Some(&["array", "values"]), - "array_key_exists" => Some(&["key", "array"]), - "array_pad" => Some(&["array", "length", "value"]), - "array_reverse" => Some(&["array", "preserve_keys"]), - "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), - "array_slice" => Some(&["array", "offset", "length"]), - "array_splice" => Some(&["array", "offset", "length", "replacement"]), - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), - "atan2" => Some(&["y", "x"]), - "basename" => Some(&["path", "suffix"]), - "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" - | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => { - Some(&["string"]) - } - "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" - | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" - | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" | "is_real" - | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), - "settype" => Some(&["var", "type"]), - "get_class" => Some(&["object"]), - "get_parent_class" => Some(&["object_or_class"]), - "call_user_func" => Some(&["callback"]), - "call_user_func_array" => Some(&["callback", "args"]), - "class_exists" => Some(&["class", "autoload"]), - "enum_exists" => Some(&["enum", "autoload"]), - "interface_exists" => Some(&["interface", "autoload"]), - "trait_exists" => Some(&["trait", "autoload"]), - "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), - "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), - "chmod" => Some(&["filename", "permissions"]), - "chr" => Some(&["codepoint"]), - "clamp" => Some(&["value", "min", "max"]), - "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), - "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), - "count" => Some(&["value", "mode"]), - "copy" | "rename" => Some(&["from", "to"]), - "crc32" => Some(&["string"]), - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), - "date" => Some(&["format", "timestamp"]), - "define" => Some(&["constant_name", "value"]), - "defined" => Some(&["constant_name"]), - "dirname" => Some(&["path", "levels"]), - "disk_free_space" | "disk_total_space" => Some(&["directory"]), - "explode" => Some(&["separator", "string"]), - "fdiv" | "fmod" => Some(&["num1", "num2"]), - "fnmatch" => Some(&["pattern", "filename", "flags"]), - "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" - | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" - | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" - | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), - "file_put_contents" => Some(&["filename", "data"]), - "function_exists" => Some(&["function"]), - "gethostbyaddr" => Some(&["ip"]), - "gethostbyname" => Some(&["hostname"]), - "gethostname" => Some(&[]), - "getprotobyname" => Some(&["protocol"]), - "getprotobynumber" => Some(&["protocol"]), - "getservbyname" => Some(&["service", "protocol"]), - "getservbyport" => Some(&["port", "protocol"]), - "get_resource_id" | "get_resource_type" => Some(&["resource"]), - "getcwd" => Some(&[]), - "getenv" => Some(&["name"]), - "glob" => Some(&["pattern"]), - "hash" => Some(&["algo", "data", "binary"]), - "hash_algos" => Some(&[]), - "hash_equals" => Some(&["known_string", "user_string"]), - "hash_file" => Some(&["algo", "filename", "binary"]), - "hash_hmac" => Some(&["algo", "data", "key", "binary"]), - "hypot" => Some(&["x", "y"]), - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), - "implode" => Some(&["separator", "array"]), - "inet_ntop" => Some(&["ip"]), - "inet_pton" => Some(&["ip"]), - "intdiv" => Some(&["num1", "num2"]), - "iterator_apply" => Some(&["iterator", "callback", "args"]), - "iterator_count" => Some(&["iterator"]), - "iterator_to_array" => Some(&["iterator", "preserve_keys"]), - "ip2long" => Some(&["ip"]), - "json_decode" => Some(&["json", "associative", "depth", "flags"]), - "json_encode" => Some(&["value", "flags", "depth"]), - "json_last_error" | "json_last_error_msg" => Some(&[]), - "json_validate" => Some(&["json", "depth", "flags"]), - "link" | "symlink" => Some(&["target", "link"]), - "linkinfo" | "readlink" => Some(&["path"]), - "log" => Some(&["num", "base"]), - "max" | "min" => Some(&["value"]), - "md5" | "sha1" => Some(&["string", "binary"]), - "microtime" => Some(&["as_float"]), - "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), - "nl2br" => Some(&["string", "use_xhtml"]), - "number_format" => Some(&[ - "num", - "decimals", - "decimal_separator", - "thousands_separator", - ]), - "ord" => Some(&["character"]), - "pathinfo" => Some(&["path", "flags"]), - "pi" => Some(&[]), - "php_uname" => Some(&["mode"]), - "phpversion" => Some(&[]), - "pow" => Some(&["num", "exponent"]), - "preg_match" => Some(&["pattern", "subject", "matches", "flags", "offset"]), - "preg_match_all" => Some(&["pattern", "subject", "matches", "flags", "offset"]), - "preg_replace" => Some(&["pattern", "replacement", "subject", "limit", "count"]), - "preg_replace_callback" => Some(&["pattern", "callback", "subject", "limit", "count"]), - "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), - "print_r" | "var_dump" => Some(&["value"]), - "putenv" => Some(&["assignment"]), - "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), - "range" => Some(&["start", "end"]), - "realpath" => Some(&["path"]), - "realpath_cache_get" | "realpath_cache_size" => Some(&[]), - "round" => Some(&["num", "precision"]), - "sleep" => Some(&["seconds"]), - "spl_classes" => Some(&[]), - "spl_object_id" | "spl_object_hash" => Some(&["object"]), - "sscanf" => Some(&["string", "format", "vars"]), - "sprintf" | "printf" => Some(&["format", "values"]), - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), - "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), - "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), - "strtotime" => Some(&["datetime"]), - "strstr" => Some(&["haystack", "needle", "before_needle"]), - "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), - "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), - "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), - "str_repeat" => Some(&["string", "times"]), - "str_split" => Some(&["string", "length"]), - "substr" => Some(&["string", "offset", "length"]), - "substr_replace" => Some(&["string", "replace", "offset", "length"]), - "sys_get_temp_dir" | "time" => Some(&[]), - "tempnam" => Some(&["directory", "prefix"]), - "touch" => Some(&["filename", "mtime", "atime"]), - "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { - Some(&["string"]) - } - "long2ip" => Some(&["ip"]), - "ucwords" => Some(&["string", "separators"]), - "umask" => Some(&["mask"]), - "usleep" => Some(&["microseconds"]), - "vsprintf" | "vprintf" => Some(&["format", "values"]), - "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), - _ => None, - } -} - -/// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. -pub(super) fn eval_builtin_call_user_func( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_call_user_func_with_values(evaluated_args, context, values) -} - -/// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. -pub(super) fn eval_builtin_call_user_func_array( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [callback, arg_array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let callback = eval_expr(callback, context, scope, values)?; - let arg_array = eval_expr(arg_array, context, scope, values)?; - eval_call_user_func_array_with_values(callback, arg_array, context, values) -} - -/// Dispatches `call_user_func_array` after callback and array arguments are evaluated. -fn eval_call_user_func_array_with_values( - callback: RuntimeCellHandle, - arg_array: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable(callback, values)?; - if !values.is_array_like(arg_array)? { - return Err(EvalStatus::RuntimeFatal); - } - let evaluated_args = eval_array_call_arg_values(arg_array, values)?; - eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) -} - -/// Dispatches `call_user_func` after its callback and arguments are already evaluated. -fn eval_call_user_func_with_values( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((callback, callback_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let callback = eval_callable(*callback, values)?; - eval_evaluated_callable_with_values(&callback, callback_args.to_vec(), context, values) -} - -/// Normalizes one PHP callback value for eval dynamic callable dispatch. -pub(super) fn eval_callable( - callback: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.is_array_like(callback)? { - return eval_array_callable(callback, values); - } - eval_callable_name(callback, values).map(EvaluatedCallable::Named) -} - -/// Normalizes one two-element object-method callable array. -fn eval_array_callable( - callback: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.array_len(callback)? != 2 { - return Err(EvalStatus::RuntimeFatal); - } - let zero = values.int(0)?; - let one = values.int(1)?; - let object = values.array_get(callback, zero)?; - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::UnsupportedConstruct); - } - let method = values.array_get(callback, one)?; - let method = - String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(EvaluatedCallable::ObjectMethod { object, method }) -} - -/// Normalizes one string callback name for eval dynamic callable dispatch. -fn eval_callable_name( - callback: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = values.string_bytes(callback)?; - let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; - let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); - if callback.contains("::") { - return Err(EvalStatus::UnsupportedConstruct); - } - Ok(callback) -} - -/// Invokes an already normalized callback with source-order positional values. -fn eval_evaluated_callable_with_values( - callback: &EvaluatedCallable, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named(name) => { - eval_callable_with_values(name, evaluated_args, context, values) - } - EvaluatedCallable::ObjectMethod { object, method } => { - eval_method_call_result(*object, method, evaluated_args, context, values) - } - } -} - -/// Invokes an already normalized callback with optional named-argument metadata. -pub(super) fn eval_evaluated_callable_with_call_array_args( - callback: &EvaluatedCallable, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named(name) => { - eval_callable_with_call_array_args(name, evaluated_args, context, values) - } - EvaluatedCallable::ObjectMethod { object, method } => { - if evaluated_args.iter().any(|arg| arg.name.is_some()) { - return Err(EvalStatus::RuntimeFatal); - } - let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); - eval_method_call_result(*object, method, evaluated_args, context, values) - } - } -} - -/// Invokes a PHP-visible callable name with source-order positional values. -fn eval_callable_with_values( - name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { - return Ok(result); - } - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function_with_values(&function, evaluated_args, context, values); - } - if let Some(function) = context.native_function(name) { - return eval_native_function_with_values(function, evaluated_args, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Invokes a callable with arguments that may carry `call_user_func_array` names. -pub(super) fn eval_callable_with_call_array_args( - name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if evaluated_args.iter().all(|arg| arg.name.is_none()) { - let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); - return eval_callable_with_values(name, evaluated_args, context, values); - } - if eval_php_visible_builtin_exists(name) { - let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; - let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { - return Err(EvalStatus::UnsupportedConstruct); - }; - return Ok(result); - } - if let Some(function) = context.function(name).cloned() { - let evaluated_args = bind_evaluated_function_args(function.params(), evaluated_args)?; - return eval_dynamic_function_with_values(&function, evaluated_args, context, values); - } - if let Some(function) = context.native_function(name) { - if function.param_names().len() != function.param_count() { - return Err(EvalStatus::RuntimeFatal); - } - let evaluated_args = bind_evaluated_function_args(function.param_names(), evaluated_args)?; - return eval_native_function_with_values(function, evaluated_args, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. -fn eval_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "abs" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.abs(*value)? - } - "addslashes" | "stripslashes" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_slashes_result(name, *value, values)? - } - "array_combine" => { - let [keys, values_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_combine_result(*keys, *values_array, values)? - } - "array_column" => { - let [array, column_key] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_column_result(*array, *column_key, values)? - } - "array_chunk" => { - let [array, length] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_chunk_result(*array, *length, values)? - } - "array_fill" => { - let [start, count, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_result(*start, *count, *value, values)? - } - "array_fill_keys" => { - let [keys, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_keys_result(*keys, *value, values)? - } - "array_filter" => match evaluated_args { - [array] => eval_array_filter_result(*array, None, None, context, values)?, - [array, callback] => { - eval_array_filter_result(*array, Some(*callback), None, context, values)? - } - [array, callback, mode] => { - eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_map" => { - let Some((callback, arrays)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_map_result(*callback, arrays, context, values)? - } - "array_reduce" => match evaluated_args { - [array, callback] => { - let initial = values.null()?; - eval_array_reduce_result(*array, *callback, initial, context, values)? - } - [array, callback, initial] => { - eval_array_reduce_result(*array, *callback, *initial, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_walk" => { - let [array, callback] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_walk_result(*array, *callback, context, values)? - } - "array_pop" | "array_shift" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_pop_shift_value_result(name, *array, values)? - } - "array_push" | "array_unshift" => { - let Some((array, inserted)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_push_unshift_count_result(*array, inserted.len(), values)? - } - "array_splice" => { - let result = match evaluated_args { - [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, - [array, offset, length] => { - eval_array_splice_value_result(*array, *offset, Some(*length), values)? - } - [array, offset, length, _replacement] => { - eval_array_splice_value_result(*array, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.warning( - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - )?; - result - } - "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" - | "shuffle" | "sort" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_sort_value_result(*array, values)? - } - "uasort" | "uksort" | "usort" => { - let [array, callback] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_user_sort_value_result(name, *array, *callback, context, values)? - } - "array_flip" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_flip_result(*array, values)? - } - "array_pad" => { - let [array, length, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_pad_result(*array, *length, *value, values)? - } - "array_product" | "array_sum" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_aggregate_result(name, *array, values)? - } - "array_keys" | "array_values" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_projection_result(name, *array, values)? - } - "array_key_exists" => { - let [key, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.array_key_exists(*key, *array)? - } - "array_diff" | "array_intersect" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_value_set_result(name, *left, *right, values)? - } - "array_diff_key" | "array_intersect_key" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_key_set_result(name, *left, *right, values)? - } - "array_merge" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_merge_result(*left, *right, values)? - } - "array_rand" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_rand_result(*array, values)? - } - "array_reverse" => match evaluated_args { - [array] => eval_array_reverse_result(*array, false, values)?, - [array, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_array_reverse_result(*array, preserve_keys, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_search" | "in_array" => { - let [needle, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_search_result(name, *needle, *array, values)? - } - "array_slice" => match evaluated_args { - [array, offset] => eval_array_slice_result(*array, *offset, None, values)?, - [array, offset, length] => { - eval_array_slice_result(*array, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_unique" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_unique_result(*array, values)? - } - "range" => { - let [start, end] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_range_result(*start, *end, values)? - } - "base64_encode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_encode_result(*value, values)? - } - "base64_decode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_decode_result(*value, values)? - } - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_unary_result(name, *value, values)? - } - "atan2" | "hypot" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_pair_result(name, *left, *right, values)? - } - "bin2hex" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_bin2hex_result(*value, values)? - } - "ceil" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.ceil(*value)? - } - "chr" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chr_result(*value, values)? - } - "chdir" | "mkdir" | "rmdir" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unary_path_bool_result(name, *path, values)? - } - "chmod" => { - let [filename, permissions] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chmod_result(*filename, *permissions, values)? - } - "clearstatcache" => { - if evaluated_args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - values.null()? - } - "clamp" => { - let [value, min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_clamp_result(*value, *min, *max, values)? - } - "copy" | "link" | "rename" | "symlink" => { - let [from, to] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_binary_path_bool_result(name, *from, *to, values)? - } - "floor" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.floor(*value)? - } - "fdiv" | "fmod" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_binary_result(name, *left, *right, values)? - } - "file" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_result(*filename, values)? - } - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_probe_result(name, *filename, values)? - } - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_stat_scalar_result(name, *filename, values)? - } - "file_get_contents" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_get_contents_result(*filename, values)? - } - "file_put_contents" => { - let [filename, data] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_put_contents_result(*filename, *data, values)? - } - "filesize" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_filesize_result(*filename, values)? - } - "filetype" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_filetype_result(*filename, values)? - } - "fnmatch" => match evaluated_args { - [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, - [pattern, filename, flags] => { - eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stat" | "lstat" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stat_array_result(name, *filename, values)? - } - "linkinfo" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_linkinfo_result(*path, values)? - } - "log" => match evaluated_args { - [num] => eval_log_result(*num, None, values)?, - [num, base] => eval_log_result(*num, Some(*base), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "readfile" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_readfile_result(*filename, values)? - } - "pi" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI)? - } - "php_uname" => match evaluated_args { - [] => eval_php_uname_result(None, values)?, - [mode] => eval_php_uname_result(Some(*mode), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "pow" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.pow(*left, *right)? - } - "preg_match" => match evaluated_args { - [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_match_all" => match evaluated_args { - [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_replace" => match evaluated_args { - [pattern, replacement, subject] => { - eval_preg_replace_result(*pattern, *replacement, *subject, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_replace_callback" => match evaluated_args { - [pattern, callback, subject] => { - eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_split" => match evaluated_args { - [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values)?, - [pattern, subject, limit] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), None, values)? - } - [pattern, subject, limit, flags] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "print_r" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_print_r_result(*value, values)? - } - "var_dump" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_var_dump_result(*value, values)? - } - "rand" | "mt_rand" => match evaluated_args { - [] => eval_rand_result(None, None, values)?, - [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "random_int" => { - let [min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_random_int_result(*min, *max, values)? - } - "rawurldecode" | "urldecode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_decode_result(name, *value, values)? - } - "rawurlencode" | "urlencode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_encode_result(name, *value, values)? - } - "round" => match evaluated_args { - [value] => values.round(*value, None)?, - [value, precision] => values.round(*value, Some(*precision))?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "scandir" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_scandir_result(*directory, values)? - } - "sqrt" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.sqrt(*value)? - } - "spl_classes" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values)? - } - "spl_object_id" | "spl_object_hash" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_spl_object_identity_result(name, *object, values)? - } - "sscanf" => { - let [input, format, ..] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sscanf_result(*input, *format, values)? - } - "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, - "settype" => { - let [value, type_name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_settype_value_result(*value, *type_name, values)? - } - "strrev" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.strrev(*value)? - } - "str_repeat" => { - let [value, times] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_repeat_result(*value, *times, values)? - } - "str_replace" | "str_ireplace" => { - let [search, replace, subject] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_replace_result(name, *search, *replace, *subject, values)? - } - "str_pad" => match evaluated_args { - [value, length] => eval_str_pad_result(*value, *length, None, None, values)?, - [value, length, pad_string] => { - eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? - } - [value, length, pad_string, pad_type] => { - eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "str_split" => match evaluated_args { - [value] => eval_str_split_result(*value, None, values)?, - [value, length] => eval_str_split_result(*value, Some(*length), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "substr" => match evaluated_args { - [value, offset] => eval_substr_result(*value, *offset, None, values)?, - [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "substr_replace" => match evaluated_args { - [value, replace, offset] => { - eval_substr_replace_result(*value, *replace, *offset, None, values)? - } - [value, replace, offset, length] => { - eval_substr_replace_result(*value, *replace, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "call_user_func" => { - return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) - .map(Some); - } - "call_user_func_array" => { - let [callback, arg_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) - .map(Some); - } - "boolval" | "floatval" | "intval" | "strval" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_cast_result(name, *value, values)? - } - "count" => match evaluated_args { - [value] => eval_count_result(*value, None, values)?, - [value, mode] => eval_count_result(*value, Some(*mode), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "crc32" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_crc32_result(*value, values)? - } - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ctype_result(name, *value, values)? - } - "date" => match evaluated_args { - [format] => eval_date_result(*format, None, values)?, - [format, timestamp] => eval_date_result(*format, Some(*timestamp), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "define" => eval_define_result(evaluated_args, context, values)?, - "defined" => eval_defined_result(evaluated_args, context, values)?, - "explode" => { - let [separator, string] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_explode_result(*separator, *string, values)? - } - "ord" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ord_result(*value, values)? - } - "implode" => { - let [separator, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_implode_result(*separator, *array, values)? - } - "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, - "microtime" => match evaluated_args { - [] | [_] => eval_microtime_result(values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "mktime" => { - let [hour, minute, second, month, day, year] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? - } - "nl2br" => match evaluated_args { - [value] => eval_nl2br_result(*value, true, values)?, - [value, use_xhtml] => { - let use_xhtml = values.truthy(*use_xhtml)?; - eval_nl2br_result(*value, use_xhtml, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "number_format" => match evaluated_args { - [value] => eval_number_format_result(*value, None, None, None, values)?, - [value, decimals] => { - eval_number_format_result(*value, Some(*decimals), None, None, values)? - } - [value, decimals, decimal_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - None, - values, - )?, - [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - Some(*thousands_separator), - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "basename" => match evaluated_args { - [path] => eval_basename_result(*path, None, values)?, - [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "dirname" => match evaluated_args { - [path] => eval_dirname_result(*path, None, values)?, - [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "disk_free_space" | "disk_total_space" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_disk_space_result(name, *directory, values)? - } - "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { - [value] => eval_trim_like_result(name, *value, None, values)?, - [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "function_exists" | "is_callable" => { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = values.string_bytes(*name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\').to_ascii_lowercase(); - values.bool_value(eval_function_probe_exists(context, &name))? - } - "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, - "enum_exists" | "trait_exists" => { - eval_class_like_exists_result(name, evaluated_args, values)? - } - "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, - "is_a" | "is_subclass_of" => { - eval_is_a_relation_result(name, evaluated_args, context, values)? - } - "json_decode" => match evaluated_args { - [json] => eval_json_decode_result(*json, None, None, None, context, values)?, - [json, associative] => { - eval_json_decode_result(*json, Some(*associative), None, None, context, values)? - } - [json, associative, depth] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - None, - context, - values, - )?, - [json, associative, depth, flags] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - Some(*flags), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "json_encode" => match evaluated_args { - [value] => eval_json_encode_result(*value, None, None, context, values)?, - [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values)?, - [value, flags, depth] => { - eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "json_last_error" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.int(context.json_last_error())? - } - "json_last_error_msg" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.string(context.json_last_error_msg())? - } - "json_validate" => match evaluated_args { - [json] => eval_json_validate_result(*json, None, None, context, values)?, - [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values)?, - [json, depth, flags] => { - eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "gethostbyaddr" => { - let [ip] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyaddr_result(*ip, values)? - } - "gethostbyname" => { - let [hostname] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyname_result(*hostname, values)? - } - "gethostname" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_gethostname_result(values)? - } - "getprotobyname" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobyname_result(*protocol, values)? - } - "getprotobynumber" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobynumber_result(*protocol, values)? - } - "getservbyname" => { - let [service, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyname_result(*service, *protocol, values)? - } - "getservbyport" => { - let [port, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyport_result(*port, *protocol, values)? - } - "getcwd" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_getcwd_result(values)? - } - "getenv" => { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getenv_result(*name, values)? - } - "get_class" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_get_class_result(*object, context, values)? - } - "get_parent_class" => { - let [object_or_class] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_get_parent_class_result(*object_or_class, values)? - } - "get_resource_id" | "get_resource_type" => { - let [resource] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_resource_introspection_result(name, *resource, values)? - } - "gettype" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gettype_result(*value, values)? - } - "glob" => { - let [pattern] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_glob_result(*pattern, values)? - } - "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { - eval_hash_one_shot_result(name, evaluated_args, values)? - } - "hash_algos" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values)? - } - "hash_equals" => { - let [known, user] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_equals_result(*known, *user, values)? - } - "hex2bin" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hex2bin_result(*value, values)? - } - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_html_entity_result(name, *value, values)? - } - "inet_ntop" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_ntop_result(*value, values)? - } - "inet_pton" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_pton_result(*value, values)? - } - "intdiv" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_intdiv_result(*left, *right, values)? - } - "iterator_apply" => match evaluated_args { - [iterator, callback] => { - let callback = eval_callable(*callback, values)?; - eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? - } - [iterator, callback, args] => { - let callback = eval_callable(*callback, values)?; - let callback_args = eval_iterator_apply_arg_values(*args, values)?; - eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "iterator_count" => { - let [iterator] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_iterator_count_result(*iterator, values)? - } - "iterator_to_array" => match evaluated_args { - [iterator] => eval_iterator_to_array_result(*iterator, true, values)?, - [iterator, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_iterator_to_array_result(*iterator, preserve_keys, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "ip2long" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ip2long_result(*value, values)? - } - "phpversion" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_phpversion_result(values)? - } - "pathinfo" => match evaluated_args { - [path] => eval_pathinfo_result(*path, None, values)?, - [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "putenv" => { - let [assignment] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_putenv_result(*assignment, values)? - } - "realpath" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_realpath_result(*path, values)? - } - "realpath_cache_get" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_get_result(values)? - } - "realpath_cache_size" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_size_result(values)? - } - "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" - | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_type_predicate_result(name, *value, values)? - } - "sys_get_temp_dir" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_sys_get_temp_dir_result(values)? - } - "tempnam" => { - let [directory, prefix] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_tempnam_result(*directory, *prefix, values)? - } - "sleep" => { - let [seconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sleep_result(*seconds, values)? - } - "time" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values)? - } - "touch" => match evaluated_args { - [filename] => eval_touch_result(*filename, None, None, values)?, - [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, - [filename, mtime, atime] => { - eval_touch_result(*filename, Some(*mtime), Some(*atime), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_introspection_result(name, values)? - } - "strtotime" => { - let [datetime] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_strtotime_result(*datetime, values)? - } - "umask" => match evaluated_args { - [] => eval_umask_result(None, values)?, - [mask] => eval_umask_result(Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "usleep" => { - let [microseconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_usleep_result(*microseconds, values)? - } - "readlink" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_readlink_result(*path, values)? - } - "unlink" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unlink_result(*filename, values)? - } - "strlen" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let bytes = values.string_bytes(*value)?; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len)? - } - "strpos" | "strrpos" => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_position_result(name, *haystack, *needle, values)? - } - "str_contains" | "str_starts_with" | "str_ends_with" => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_search_result(name, *haystack, *needle, values)? - } - "strstr" => match evaluated_args { - [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values)?, - [haystack, needle, before_needle] => { - let before_needle = values.truthy(*before_needle)?; - eval_strstr_result(*haystack, *needle, before_needle, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "strcmp" | "strcasecmp" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_compare_result(name, *left, *right, values)? - } - "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_case_result(name, *value, values)? - } - "long2ip" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_long2ip_result(*value, values)? - } - "ucwords" => match evaluated_args { - [value] => eval_ucwords_result(*value, None, values)?, - [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, - "wordwrap" => match evaluated_args { - [value] => eval_wordwrap_result(*value, None, None, None, values)?, - [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, - [value, width, break_string] => { - eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values)? - } - [value, width, break_string, cut] => eval_wordwrap_result( - *value, - Some(*width), - Some(*break_string), - Some(*cut), - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - _ => return Ok(None), - }; - Ok(Some(result)) -} - -/// Evaluates PHP's `abs(...)` over one eval expression. -pub(super) fn eval_builtin_abs( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.abs(value) -} - -/// Evaluates PHP array aggregate builtins over one eval array expression. -pub(super) fn eval_builtin_array_aggregate( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_aggregate_result(name, array, values) -} - -/// Computes `array_sum()` or `array_product()` through eval's numeric value hooks. -fn eval_array_aggregate_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = match name { - "array_sum" => values.int(0)?, - "array_product" => values.int(1)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - result = match name { - "array_sum" => values.add(result, value)?, - "array_product" => values.mul(result, value)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - } - Ok(result) -} - -/// Evaluates PHP `array_combine()` over key and value array expressions. -pub(super) fn eval_builtin_array_combine( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [keys, values_array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let keys = eval_expr(keys, context, scope, values)?; - let values_array = eval_expr(values_array, context, scope, values)?; - eval_array_combine_result(keys, values_array, values) -} - -/// Builds the associative result for `array_combine()` from two eval arrays. -fn eval_array_combine_result( - keys: RuntimeCellHandle, - values_array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(keys)?; - if len != values.array_len(values_array)? { - return Err(EvalStatus::RuntimeFatal); - } - - let mut result = values.assoc_new(len)?; - for position in 0..len { - let source_key = values.array_iter_key(keys, position)?; - let target_key = values.array_get(keys, source_key)?; - let target_key = values.cast_string(target_key)?; - let value_key = values.array_iter_key(values_array, position)?; - let value = values.array_get(values_array, value_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_column()` over row-array and column-key expressions. -pub(super) fn eval_builtin_array_column( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, column_key] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let column_key = eval_expr(column_key, context, scope, values)?; - eval_array_column_result(array, column_key, values) -} - -/// Builds `array_column()` by extracting present row columns into a reindexed array. -fn eval_array_column_result( - array: RuntimeCellHandle, - column_key: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len)?; - let mut output_index = 0_i64; - for position in 0..len { - let row_key = values.array_iter_key(array, position)?; - let row = values.array_get(array, row_key)?; - if !matches!(values.type_tag(row)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - continue; - } - let exists = values.array_key_exists(column_key, row)?; - if !values.truthy(exists)? { - continue; - } - let column = values.array_get(row, column_key)?; - let target_key = values.int(output_index)?; - output_index = output_index - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, column)?; - } - Ok(result) -} - -/// Evaluates PHP `array_fill()` over start, count, and value expressions. -pub(super) fn eval_builtin_array_fill( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [start, count, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let start = eval_expr(start, context, scope, values)?; - let count = eval_expr(count, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_fill_result(start, count, value, values) -} - -/// Builds an `array_fill()` result with PHP's explicit integer key range. -fn eval_array_fill_result( - start: RuntimeCellHandle, - count: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let start = eval_int_value(start, values)?; - let count = eval_int_value(count, values)?; - if count < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut result = if start == 0 { - values.array_new(count)? - } else { - values.assoc_new(count)? - }; - for offset in 0..count { - let offset = i64::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = start.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_fill_keys()` over key-array and value expressions. -pub(super) fn eval_builtin_array_fill_keys( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [keys, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let keys = eval_expr(keys, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_fill_keys_result(keys, value, values) -} - -/// Builds an `array_fill_keys()` result preserving the source key iteration order. -fn eval_array_fill_keys_result( - keys: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(keys)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let source_key = values.array_iter_key(keys, position)?; - let target_key = values.array_get(keys, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_map()` for one source array and a string or null callback. -pub(super) fn eval_builtin_array_map( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((callback, arrays)) = args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let callback = eval_expr(callback, context, scope, values)?; - let mut evaluated_arrays = Vec::with_capacity(arrays.len()); - for array in arrays { - evaluated_arrays.push(eval_expr(array, context, scope, values)?); - } - eval_array_map_result(callback, &evaluated_arrays, context, values) -} - -/// Maps one eval array with PHP key preservation for the one-array form. -fn eval_array_map_result( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = arrays else { - return eval_array_map_variadic_result(callback, arrays, context, values); - }; - let callback = if values.is_null(callback)? { - None - } else { - Some(eval_callable_name(callback, values)?) - }; - let len = values.array_len(*array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(*array, position)?; - let value = values.array_get(*array, key)?; - let mapped = if let Some(callback) = callback.as_deref() { - eval_callable_with_values(callback, vec![value], context, values)? - } else { - value - }; - result = values.array_set(result, key, mapped)?; - } - Ok(result) -} - -/// Maps multiple eval arrays with PHP's reindexed and null-padded variadic behavior. -fn eval_array_map_variadic_result( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if arrays.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let callback = if values.is_null(callback)? { - None - } else { - Some(eval_callable_name(callback, values)?) - }; - let mut lengths = Vec::with_capacity(arrays.len()); - let mut max_len = 0; - for array in arrays { - let len = values.array_len(*array)?; - max_len = max_len.max(len); - lengths.push(len); - } - - let mut result = values.array_new(max_len)?; - for position in 0..max_len { - let mut callback_args = Vec::with_capacity(arrays.len()); - for (array, len) in arrays.iter().zip(lengths.iter()) { - let value = if position < *len { - let key = values.array_iter_key(*array, position)?; - values.array_get(*array, key)? - } else { - values.null()? - }; - callback_args.push(value); - } - let mapped = if let Some(callback) = callback.as_deref() { - eval_callable_with_values(callback, callback_args, context, values)? - } else { - eval_array_map_zipped_row(callback_args, values)? - }; - let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, mapped)?; - } - Ok(result) -} - -/// Builds one row for `array_map(null, $a, $b, ...)`. -fn eval_array_map_zipped_row( - values_row: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut row = values.array_new(values_row.len())?; - for (index, value) in values_row.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - row = values.array_set(row, key, value)?; - } - Ok(row) -} - -/// Evaluates PHP `array_reduce()` with an optional initial carry value. -pub(super) fn eval_builtin_array_reduce( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, callback, initial) = match args { - [array, callback] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - (array, callback, values.null()?) - } - [array, callback, initial] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let initial = eval_expr(initial, context, scope, values)?; - (array, callback, initial) - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_array_reduce_result(array, callback, initial, context, values) -} - -/// Reduces one eval array by invoking a string callback with carry and item cells. -fn eval_array_reduce_result( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - initial: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_name(callback, values)?; - let len = values.array_len(array)?; - let mut carry = initial; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - carry = eval_callable_with_values(&callback, vec![carry, value], context, values)?; - } - Ok(carry) -} - -/// Evaluates PHP `array_walk()` for side-effect callbacks over value/key pairs. -pub(super) fn eval_builtin_array_walk( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, callback] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - eval_array_walk_result(array, callback, context, values) -} - -/// Walks one eval array by invoking a string callback with value and key cells. -fn eval_array_walk_result( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_name(callback, values)?; - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let _ = eval_callable_with_values(&callback, vec![value, key], context, values)?; - } - values.bool_value(true) -} - -/// Evaluates direct by-reference `settype()` calls and writes the converted cell back. -fn eval_builtin_settype_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (var_name, type_name) = eval_settype_direct_args(args, context, scope, values)?; - let value = visible_scope_cell(context, scope, &var_name).map_or_else(|| values.null(), Ok)?; - let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { - return values.bool_value(false); - }; - for replaced in set_scope_cell( - context, - scope, - var_name, - converted, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - values.bool_value(true) -} - -/// Evaluates and binds direct `settype()` arguments while preserving source order. -fn eval_settype_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(String, RuntimeCellHandle), EvalStatus> { - let mut var_name = None; - let mut type_name = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "var", - 1 => "type", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "var" => { - if var_name.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - var_name = Some(name.clone()); - } - "type" => { - if type_name.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - type_name = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let var_name = var_name.ok_or(EvalStatus::RuntimeFatal)?; - let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; - Ok((var_name, type_name)) -} - -/// Applies the eval-supported `settype()` scalar target conversion. -fn eval_settype_cast_value( - value: RuntimeCellHandle, - type_name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let type_name = values.string_bytes(type_name)?; - let type_name = String::from_utf8_lossy(&type_name).to_ascii_lowercase(); - let converted = match type_name.as_str() { - "bool" | "boolean" => Some(values.cast_bool(value)?), - "float" | "double" => Some(values.cast_float(value)?), - "int" | "integer" => Some(values.cast_int(value)?), - "string" => Some(values.cast_string(value)?), - _ => None, - }; - Ok(converted) -} - -/// Evaluates by-value `settype()` callable dispatch without mutating the source argument. -fn eval_settype_value_result( - value: RuntimeCellHandle, - type_name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - values.warning("settype(): Argument #1 ($var) must be passed by reference, value given")?; - if let Some(converted) = eval_settype_cast_value(value, type_name, values)? { - values.release(converted)?; - return values.bool_value(true); - } - values.bool_value(false) -} - -/// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. -pub(super) fn eval_builtin_array_pop_shift_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if name == "settype" { - return eval_builtin_settype_call(args, context, scope, values); - } - if matches!(name, "array_push" | "array_unshift") { - return eval_builtin_array_push_unshift_call(name, args, context, scope, values); - } - if name == "array_splice" { - return eval_builtin_array_splice_call(args, context, scope, values); - } - if matches!( - name, - "arsort" - | "asort" - | "krsort" - | "ksort" - | "natcasesort" - | "natsort" - | "rsort" - | "shuffle" - | "sort" - ) { - return eval_builtin_array_sort_call(name, args, context, scope, values); - } - if matches!(name, "uasort" | "uksort" | "usort") { - return eval_builtin_user_sort_call(name, args, context, scope, values); - } - - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(var_name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - let Some(entry) = - scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; - for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates direct by-reference `array_push()` / `array_unshift()` calls. -fn eval_builtin_array_push_unshift_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 || !eval_call_args_are_plain_positional(args) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(var_name) = args[0].value() else { - return Err(EvalStatus::RuntimeFatal); - }; - let mut inserted = Vec::with_capacity(args.len() - 1); - for arg in &args[1..] { - inserted.push(eval_expr(arg.value(), context, scope, values)?); - } - let Some(entry) = - scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; - let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; - for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates direct by-reference `array_splice()` calls and writes back the array. -fn eval_builtin_array_splice_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array_name, offset, length, replacement_arg) = - eval_array_splice_direct_args(args, context, scope, values)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let (removed, replacement) = - eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } - Ok(removed) -} - -/// Evaluates direct by-reference array ordering calls and writes back the array. -fn eval_builtin_array_sort_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let array_name = eval_array_sort_direct_arg(args)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let replacement = eval_array_sort_replacement(name, array, values)?; - let result = values.bool_value(true)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates direct by-reference user-comparator sort calls and writes back the array. -fn eval_builtin_user_sort_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array_name, callback) = eval_user_sort_direct_args(args, context, scope, values)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; - let result = values.bool_value(true)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates and binds direct user-sort arguments while preserving source order. -fn eval_user_sort_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(String, RuntimeCellHandle), EvalStatus> { - let mut array = None; - let mut callback = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "callback", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - array = Some(name.clone()); - } - "callback" => { - if callback.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - callback = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let array = array.ok_or(EvalStatus::RuntimeFatal)?; - let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, callback)) -} - -/// Returns the dynamic callable result for by-value user-comparator sort calls. -fn eval_user_sort_value_result( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; - values.release(replacement)?; - values.bool_value(true) -} - -/// One source array entry used by eval user-comparator sort routines. -struct EvalUserSortEntry { - source_key: RuntimeCellHandle, - value: RuntimeCellHandle, -} - -/// Builds the sorted replacement array for user-comparator sort builtins. -fn eval_user_sort_replacement( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_name(callback, values)?; - let mut entries = eval_user_sort_entries(array, values)?; - eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; - if name == "usort" { - return eval_user_sort_reindex_result(entries, values); - } - eval_user_sort_preserve_key_result(entries, values) -} - -/// Collects source keys and values from one eval array for user sorting. -fn eval_user_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - entries.push(EvalUserSortEntry { source_key, value }); - } - Ok(entries) -} - -/// Sorts entries by repeatedly invoking the PHP comparator callback. -fn eval_user_sort_entries_in_place( - name: &str, - callback: &str, - entries: &mut [EvalUserSortEntry], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for pass in 0..entries.len() { - let upper = entries.len().saturating_sub(pass + 1); - for index in 0..upper { - let comparison = eval_user_sort_compare( - name, - callback, - &entries[index], - &entries[index + 1], - context, - values, - )?; - if comparison > 0 { - entries.swap(index, index + 1); - } - } - } - Ok(()) -} - -/// Invokes one user-sort comparator and returns its integer ordering result. -fn eval_user_sort_compare( - name: &str, - callback: &str, - left: &EvalUserSortEntry, - right: &EvalUserSortEntry, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let args = if name == "uksort" { - vec![left.source_key, right.source_key] - } else { - vec![left.value, right.value] - }; - let result = eval_callable_with_values(callback, args, context, values)?; - eval_int_value(result, values) -} - -/// Builds the reindexed result for `usort()`. -fn eval_user_sort_reindex_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(entries.len())?; - for (index, entry) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, entry.value)?; - } - Ok(result) -} - -/// Builds the key-preserving result for `uksort()` and `uasort()`. -fn eval_user_sort_preserve_key_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(entries.len())?; - for entry in entries { - result = values.array_set(result, entry.source_key, entry.value)?; - } - Ok(result) -} - -/// Extracts the direct variable argument accepted by eval array ordering builtins. -fn eval_array_sort_direct_arg(args: &[EvalCallArg]) -> Result { - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - Ok(name.clone()) -} - -/// Returns the dynamic callable result for by-value array ordering calls. -fn eval_array_sort_value_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - values.bool_value(true) -} - -/// Sort key shape supported by eval's homogeneous array ordering implementation. -#[derive(Clone)] -enum EvalArraySortKey { - Numeric(f64), - Natural(Vec), - String(Vec), -} - -/// One source array entry plus its precomputed ordering key. -struct EvalArraySortEntry { - sort_key: EvalArraySortKey, - source_key: RuntimeCellHandle, - value: RuntimeCellHandle, -} - -/// Builds the sorted replacement array for eval array ordering builtins. -fn eval_array_sort_replacement( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut entries = match name { - "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, - "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, - "natsort" => eval_array_natural_sort_entries(array, false, values)?, - "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, - "shuffle" => return eval_array_shuffle_replacement(array, values), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - entries.sort_by(|left, right| { - let order = eval_array_sort_key_cmp(&left.sort_key, &right.sort_key); - if matches!(name, "arsort" | "krsort" | "rsort") { - order.reverse() - } else { - order - } - }); - - if matches!( - name, - "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" - ) { - return eval_array_preserve_key_sort_result(entries, values); - } - eval_array_reindex_sort_result(entries, values) -} - -/// Builds a shuffled, reindexed replacement array for `shuffle()`. -fn eval_array_shuffle_replacement( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - entries.push(values.array_get(array, source_key)?); - } - - for index in (1..entries.len()).rev() { - let swap_with = (eval_random_u128() % ((index + 1) as u128)) as usize; - entries.swap(index, swap_with); - } - - let mut result = values.array_new(entries.len())?; - for (index, value) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Builds an indexed result for `sort()` / `rsort()` after value ordering. -fn eval_array_reindex_sort_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(entries.len())?; - for (index, entry) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, entry.value)?; - } - Ok(result) -} - -/// Builds a key-preserving associative result after value or key ordering. -fn eval_array_preserve_key_sort_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(entries.len())?; - for entry in entries { - result = values.array_set(result, entry.source_key, entry.value)?; - } - Ok(result) -} - -/// Collects values and comparable value-sort keys from one eval array. -fn eval_array_value_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - let mut expects_numeric = None; - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_sort_key(value, values)?; - let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); - match expects_numeric { - Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), - Some(_) => {} - None => expects_numeric = Some(is_numeric), - } - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Collects values and natural-sort keys from one eval array. -fn eval_array_natural_sort_entries( - array: RuntimeCellHandle, - case_insensitive: bool, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - let mut expects_numeric = None; - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_natural_sort_key(value, case_insensitive, values)?; - let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); - match expects_numeric { - Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), - Some(_) => {} - None => expects_numeric = Some(is_numeric), - } - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Collects values and comparable key-sort keys from one eval array. -fn eval_array_key_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_sort_key(source_key, values)?; - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Converts one scalar eval value into a natural-sort key. -fn eval_array_natural_sort_key( - value: RuntimeCellHandle, - case_insensitive: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => { - Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) - } - EVAL_TAG_STRING => { - let mut bytes = values.string_bytes(value)?; - if case_insensitive { - bytes.make_ascii_lowercase(); - } - Ok(EvalArraySortKey::Natural(bytes)) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts one scalar eval value into a homogeneous sort key. -fn eval_array_sort_key( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => { - Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) - } - EVAL_TAG_STRING => { - let bytes = values.string_bytes(value)?; - match eval_array_numeric_string_sort_key(&bytes) { - Some(value) => Ok(EvalArraySortKey::Numeric(value)), - None => Ok(EvalArraySortKey::String(bytes)), - } - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Parses one PHP numeric string into the numeric sort domain when possible. -fn eval_array_numeric_string_sort_key(bytes: &[u8]) -> Option { - if !eval_is_numeric_string(bytes) { - return None; - } - std::str::from_utf8(bytes).ok()?.parse::().ok() -} - -/// Compares two precomputed eval sort keys. -fn eval_array_sort_key_cmp( - left: &EvalArraySortKey, - right: &EvalArraySortKey, -) -> std::cmp::Ordering { - match (left, right) { - (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { - left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) - } - (EvalArraySortKey::Natural(left), EvalArraySortKey::Natural(right)) => { - eval_natural_bytes_cmp(left, right) - } - (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), - _ => eval_array_sort_key_rank(left).cmp(&eval_array_sort_key_rank(right)), - } -} - -/// Returns a deterministic rank for mixed key-sort domains. -fn eval_array_sort_key_rank(key: &EvalArraySortKey) -> u8 { - match key { - EvalArraySortKey::Numeric(_) => 0, - EvalArraySortKey::Natural(_) => 1, - EvalArraySortKey::String(_) => 2, - } -} - -/// Compares byte strings with a small PHP-style natural ordering. -fn eval_natural_bytes_cmp(left: &[u8], right: &[u8]) -> std::cmp::Ordering { - let mut left_index = 0; - let mut right_index = 0; - while left_index < left.len() && right_index < right.len() { - if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { - let order = eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); - if order != std::cmp::Ordering::Equal { - return order; - } - continue; - } - - let order = left[left_index].cmp(&right[right_index]); - if order != std::cmp::Ordering::Equal { - return order; - } - left_index += 1; - right_index += 1; - } - left.len().cmp(&right.len()) -} - -/// Compares two natural-sort digit runs and advances both byte indexes past them. -fn eval_natural_digit_run_cmp( - left: &[u8], - left_index: &mut usize, - right: &[u8], - right_index: &mut usize, -) -> std::cmp::Ordering { - let left_start = *left_index; - let right_start = *right_index; - while *left_index < left.len() && left[*left_index].is_ascii_digit() { - *left_index += 1; - } - while *right_index < right.len() && right[*right_index].is_ascii_digit() { - *right_index += 1; - } - - let left_digits = &left[left_start..*left_index]; - let right_digits = &right[right_start..*right_index]; - let left_trimmed = eval_trim_leading_zeroes(left_digits); - let right_trimmed = eval_trim_leading_zeroes(right_digits); - left_trimmed - .len() - .cmp(&right_trimmed.len()) - .then_with(|| left_trimmed.cmp(right_trimmed)) - .then_with(|| left_digits.len().cmp(&right_digits.len())) -} - -/// Drops leading zero bytes while keeping one zero for an all-zero digit run. -fn eval_trim_leading_zeroes(digits: &[u8]) -> &[u8] { - let trimmed = digits - .iter() - .position(|digit| *digit != b'0') - .map_or(&digits[digits.len().saturating_sub(1)..], |index| { - &digits[index..] - }); - if trimmed.is_empty() { - digits - } else { - trimmed - } -} - -/// Evaluates and binds direct `array_splice()` arguments while preserving source order. -fn eval_array_splice_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut array = None; - let mut offset = None; - let mut length = None; - let mut replacement = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "offset", - 2 => "length", - 3 => "replacement", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - array = Some(name.clone()); - } - "offset" => { - if offset.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - offset = Some(eval_expr(arg.value(), context, scope, values)?); - } - "length" => { - if length.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - length = Some(eval_expr(arg.value(), context, scope, values)?); - } - "replacement" => { - if replacement.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - replacement = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let array = array.ok_or(EvalStatus::RuntimeFatal)?; - let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, offset, length, replacement)) -} - -/// Returns the removed elements that `array_splice()` would produce without mutating the source. -fn eval_array_splice_value_result( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; - eval_array_splice_removed(array, start, end, values) -} - -/// Builds both removed and replacement arrays for direct `array_splice()` write-back. -fn eval_array_splice_removed_and_replacement( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - replacement: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; - let removed = eval_array_splice_removed(array, start, end, values)?; - let inserted = eval_array_splice_insert_values(replacement, values)?; - let replacement = eval_array_splice_replacement(array, start, end, &inserted, values)?; - Ok((removed, replacement)) -} - -/// Converts splice offset and length cells into bounded source positions. -fn eval_array_splice_bounds( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(usize, usize), EvalStatus> { - let len = values.array_len(array)?; - let offset = eval_int_value(offset, values)?; - let start = eval_slice_start(len, offset)?; - let end = match length { - Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { - eval_slice_end(len, start, eval_int_value(length, values)?)? - } - _ => len, - }; - Ok((start, end)) -} - -/// Builds the reindexed/string-key-preserving removed array returned by `array_splice()`. -fn eval_array_splice_removed( - array: RuntimeCellHandle, - start: usize, - end: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = end.saturating_sub(start); - if eval_array_range_keys_are_int(array, start, end, values)? { - let mut result = values.array_new(len)?; - let mut target = 0_i64; - for position in start..end { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - return Ok(result); - } - - let mut result = values.assoc_new(len)?; - let mut next_int_key = 0_i64; - for position in start..end { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Expands the optional `array_splice()` replacement value into inserted values. -fn eval_array_splice_insert_values( - replacement: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(replacement) = replacement else { - return Ok(Vec::new()); - }; - if !matches!( - values.type_tag(replacement)?, - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC - ) { - return Ok(vec![replacement]); - } - - let len = values.array_len(replacement)?; - let mut inserted = Vec::with_capacity(len); - for position in 0..len { - let key = values.array_iter_key(replacement, position)?; - inserted.push(values.array_get(replacement, key)?); - } - Ok(inserted) -} - -/// Builds the source replacement after removing the requested splice range. -fn eval_array_splice_replacement( - array: RuntimeCellHandle, - start: usize, - end: usize, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let new_len = len - .saturating_sub(end.saturating_sub(start)) - .checked_add(inserted.len()) - .ok_or(EvalStatus::RuntimeFatal)?; - if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { - let mut result = values.array_new(new_len)?; - let mut target = 0_i64; - for position in 0..start { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - for value in inserted { - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, *value)?; - } - for position in end..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - return Ok(result); - } - - let mut result = values.assoc_new(new_len)?; - let mut next_int_key = 0_i64; - for position in 0..start { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - for value in inserted { - let target_key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, *value)?; - } - for position in end..len { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every key in one source position range is integer-shaped. -fn eval_array_range_keys_are_int( - array: RuntimeCellHandle, - start: usize, - end: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in start..end { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Returns true when every key outside the removed splice range is integer-shaped. -fn eval_array_splice_remaining_keys_are_int( - array: RuntimeCellHandle, - start: usize, - end: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - if (start..end).contains(&position) { - continue; - } - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. -fn eval_array_pop_shift_value_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(array)?; - if len == 0 { - return values.null(); - } - let position = match name { - "array_pop" => len - 1, - "array_shift" => 0, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let key = values.array_iter_key(array, position)?; - values.array_get(array, key) -} - -/// Builds the return value plus replacement array for direct pop/shift write-back. -fn eval_array_pop_shift_replacement( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let len = values.array_len(array)?; - let tag = values.type_tag(array)?; - if len == 0 { - let replacement = match tag { - EVAL_TAG_ARRAY => values.array_new(0)?, - EVAL_TAG_ASSOC => values.assoc_new(0)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - return Ok((values.null()?, replacement)); - } - - let removed_position = match name { - "array_pop" => len - 1, - "array_shift" => 0, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let removed_key = values.array_iter_key(array, removed_position)?; - let removed_value = values.array_get(array, removed_key)?; - let replacement = match tag { - EVAL_TAG_ARRAY => { - eval_array_pop_shift_indexed_replacement(array, removed_position, len, values)? - } - EVAL_TAG_ASSOC => { - eval_array_pop_shift_assoc_replacement(name, array, removed_position, len, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - Ok((removed_value, replacement)) -} - -/// Rebuilds an indexed array after removing one position and reindexing values. -fn eval_array_pop_shift_indexed_replacement( - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(len.saturating_sub(1))?; - let mut target = 0_i64; - for position in 0..len { - if position == removed_position { - continue; - } - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after pop/shift, preserving PHP key behavior. -fn eval_array_pop_shift_assoc_replacement( - name: &str, - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - if name == "array_shift" - && eval_array_remaining_keys_are_int(array, removed_position, len, values)? - { - return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); - } - - let mut result = values.assoc_new(len.saturating_sub(1))?; - let mut next_int_key = 0_i64; - for position in 0..len { - if position == removed_position { - continue; - } - let source_key = values.array_iter_key(array, position)?; - let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every remaining key is an integer after removing one element. -fn eval_array_remaining_keys_are_int( - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - if position == removed_position { - continue; - } - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Returns the resulting element count for by-value push/unshift dynamic calls. -fn eval_array_push_unshift_count_result( - array: RuntimeCellHandle, - inserted_len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let total = values - .array_len(array)? - .checked_add(inserted_len) - .ok_or(EvalStatus::RuntimeFatal)?; - let total = i64::try_from(total).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(total) -} - -/// Builds the replacement array for direct push/unshift write-back. -fn eval_array_push_unshift_replacement( - name: &str, - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match (name, values.type_tag(array)?) { - ("array_push", EVAL_TAG_ARRAY) => { - eval_array_push_indexed_replacement(array, inserted, values) - } - ("array_push", EVAL_TAG_ASSOC) => { - eval_array_push_assoc_replacement(array, inserted, values) - } - ("array_unshift", EVAL_TAG_ARRAY) => { - eval_array_unshift_indexed_replacement(array, inserted, values) - } - ("array_unshift", EVAL_TAG_ASSOC) => { - eval_array_unshift_assoc_replacement(array, inserted, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Rebuilds an indexed array after appending values. -fn eval_array_push_indexed_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len.saturating_add(inserted.len()))?; - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let target_key = - values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, target_key, value)?; - } - for (offset, value) in inserted.iter().copied().enumerate() { - let position = len.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; - let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after appending values at PHP's next integer keys. -fn eval_array_push_assoc_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; - let mut next_key = 0_i64; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? == EVAL_TAG_INT { - next_key = next_key.max(eval_int_value(key, values)?.saturating_add(1)); - } - let value = values.array_get(array, key)?; - result = values.array_set(result, key, value)?; - } - for value in inserted.iter().copied() { - let key = values.int(next_key)?; - next_key = next_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an indexed array after prepending values and reindexing the original tail. -fn eval_array_unshift_indexed_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len.saturating_add(inserted.len()))?; - let mut target = 0_i64; - for value in inserted.iter().copied() { - let key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after prepending values and reindexing integer keys. -fn eval_array_unshift_assoc_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - if eval_array_keys_are_int(array, len, values)? { - return eval_array_unshift_indexed_replacement(array, inserted, values); - } - - let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; - let mut next_int_key = 0_i64; - for value in inserted.iter().copied() { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every key in the array is integer-shaped. -fn eval_array_keys_are_int( - array: RuntimeCellHandle, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Evaluates PHP `array_filter()` for null and string-callback filtering modes. -pub(super) fn eval_builtin_array_filter( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array] => { - let array = eval_expr(array, context, scope, values)?; - eval_array_filter_result(array, None, None, context, values) - } - [array, callback] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - eval_array_filter_result(array, Some(callback), None, context, values) - } - [array, callback, mode] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_array_filter_result(array, Some(callback), Some(mode), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Filters eval array entries through PHP truthiness or a string callback. -fn eval_array_filter_result( - array: RuntimeCellHandle, - callback: Option, - mode: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = match callback { - Some(callback) if !values.is_null(callback)? => Some(eval_callable_name(callback, values)?), - _ => None, - }; - let mode = match mode { - Some(mode) => eval_array_filter_mode_value(mode, values)?, - None => EVAL_ARRAY_FILTER_USE_VALUE, - }; - - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let keep = if let Some(callback) = callback.as_deref() { - let args = eval_array_filter_callback_args(mode, key, value)?; - let result = eval_callable_with_values(callback, args, context, values)?; - values.truthy(result)? - } else { - values.truthy(value)? - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Reads and validates the optional `array_filter()` callback mode. -fn eval_array_filter_mode_value( - mode: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = eval_int_value(mode, values)?; - match mode { - EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { - Ok(mode) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds the callback argument list for one `array_filter()` entry. -fn eval_array_filter_callback_args( - mode: i64, - key: RuntimeCellHandle, - value: RuntimeCellHandle, -) -> Result, EvalStatus> { - match mode { - EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), - EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), - EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. -pub(super) fn eval_builtin_array_chunk( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, length] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_chunk_result(array, length, values) -} - -/// Builds an `array_chunk()` result as nested reindexed arrays. -fn eval_array_chunk_result( - array: RuntimeCellHandle, - length: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let chunk_size = eval_int_value(length, values)?; - if chunk_size <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; - let len = values.array_len(array)?; - let chunk_count = len.div_ceil(chunk_size); - let mut result = values.array_new(chunk_count)?; - - for chunk_index in 0..chunk_count { - let start = chunk_index * chunk_size; - let end = usize::min(start + chunk_size, len); - let mut chunk = values.array_new(end - start)?; - for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let value = values.array_get(array, source_key)?; - let target_index = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_index = values.int(target_index)?; - chunk = values.array_set(chunk, target_index, value)?; - } - let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let result_key = values.int(result_key)?; - result = values.array_set(result, result_key, chunk)?; - } - - Ok(result) -} - -/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. -pub(super) fn eval_builtin_array_slice( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array, offset] => { - let array = eval_expr(array, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_array_slice_result(array, offset, None, values) - } - [array, offset, length] => { - let array = eval_expr(array, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_slice_result(array, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds an `array_slice()` result with PHP offset and length bounds. -fn eval_array_slice_result( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let offset = eval_int_value(offset, values)?; - let start = eval_slice_start(len, offset)?; - let end = match length { - Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { - eval_slice_end(len, start, eval_int_value(length, values)?)? - } - _ => len, - }; - - let mut result = values.array_new(end.saturating_sub(start))?; - for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; - result = values.array_set(result, target_key, source_value)?; - } - Ok(result) -} - -/// Converts a PHP array-slice offset into a bounded source position. -fn eval_slice_start(len: usize, offset: i64) -> Result { - if offset >= 0 { - let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; - return Ok(usize::min(offset, len)); - } - - let tail = offset - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - Ok(len.saturating_sub(tail)) -} - -/// Converts a PHP array-slice length into a bounded exclusive end position. -fn eval_slice_end(len: usize, start: usize, length: i64) -> Result { - if length >= 0 { - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - return Ok(usize::min(start.saturating_add(length), len)); - } - - let tail = length - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - Ok(usize::max(start, len.saturating_sub(tail))) -} - -/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. -pub(super) fn eval_builtin_array_pad( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, length, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_pad_result(array, length, value, values) -} - -/// Builds an `array_pad()` result by copying values and padding left or right. -fn eval_array_pad_result( - array: RuntimeCellHandle, - length: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let target = eval_int_value(length, values)?; - let target_len = target - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - let result_len = usize::max(len, target_len); - let pad_count = result_len.saturating_sub(len); - let mut result = values.array_new(result_len)?; - let mut output_index = 0usize; - - if target < 0 { - let (padded, next_index) = - eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; - result = padded; - output_index = next_index; - } - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; - result = values.array_set(result, target_key, source_value)?; - output_index += 1; - } - - if target > 0 { - result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; - } - - Ok(result) -} - -/// Appends the same pad value at consecutive indexed positions in an array result. -fn eval_array_pad_append_repeated( - mut array: RuntimeCellHandle, - start_index: usize, - count: usize, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, usize), EvalStatus> { - let mut next_index = start_index; - for _ in 0..count { - let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - array = values.array_set(array, key, value)?; - next_index += 1; - } - Ok((array, next_index)) -} - -/// Evaluates PHP `array_flip()` over one eval array expression. -pub(super) fn eval_builtin_array_flip( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_flip_result(array, values) -} - -/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. -fn eval_array_flip_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { - continue; - } - result = values.array_set(result, value, key)?; - } - Ok(result) -} - -/// Evaluates PHP `array_unique()` over one eval array expression. -pub(super) fn eval_builtin_array_unique( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_unique_result(array, values) -} - -/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. -fn eval_array_unique_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut seen = Vec::>::with_capacity(len); - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let unique_key = values.string_bytes(value)?; - if seen.iter().any(|seen_key| seen_key == &unique_key) { - continue; - } - seen.push(unique_key); - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP array projection builtins over one eval array expression. -pub(super) fn eval_builtin_array_projection( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_projection_result(name, array, values) -} - -/// Builds the indexed result array for `array_keys()` or `array_values()`. -fn eval_array_projection_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = match name { - "array_keys" => key, - "array_values" => values.array_get(array, key)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let index = values.int(position as i64)?; - result = values.array_set(result, index, value)?; - } - Ok(result) -} - -/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. -pub(super) fn eval_builtin_iterator_apply( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [iterator, callback] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, values)?; - eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) - } - [iterator, callback, callback_args] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, values)?; - let callback_args = eval_expr(callback_args, context, scope, values)?; - let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; - eval_iterator_apply_result(iterator, &callback, callback_args, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts the optional `iterator_apply()` callback-args value into call arguments. -fn eval_iterator_apply_arg_values( - args: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.is_null(args)? { - return Ok(Vec::new()); - } - if !values.is_array_like(args)? { - return Err(EvalStatus::RuntimeFatal); - } - eval_array_call_arg_values(args, values) -} - -/// Applies a callback to each valid position of an eval-supported Traversable object. -fn eval_iterator_apply_result( - iterator: RuntimeCellHandle, - callback: &EvaluatedCallable, - callback_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(iterator)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let count = match eval_iterator_apply_iterator_object( - iterator, - callback, - &callback_args, - context, - values, - ) { - Ok(count) => count, - Err(EvalStatus::UnsupportedConstruct) => { - let iterator = values.method_call(iterator, "getiterator", Vec::new())?; - eval_iterator_apply_iterator_object( - iterator, - callback, - &callback_args, - context, - values, - )? - } - Err(err) => return Err(err), - }; - values.int(count) -} - -/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. -fn eval_iterator_apply_iterator_object( - iterator: RuntimeCellHandle, - callback: &EvaluatedCallable, - callback_args: &[EvaluatedCallArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let _ = values.method_call(iterator, "rewind", Vec::new())?; - let mut count = 0_i64; - loop { - let valid = values.method_call(iterator, "valid", Vec::new())?; - if !values.truthy(valid)? { - return Ok(count); - } - count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - let result = eval_evaluated_callable_with_call_array_args( - callback, - callback_args.to_vec(), - context, - values, - )?; - if !values.truthy(result)? { - return Ok(count); - } - let _ = values.method_call(iterator, "next", Vec::new())?; - } -} - -/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. -pub(super) fn eval_builtin_iterator_count( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [iterator] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let iterator = eval_expr(iterator, context, scope, values)?; - eval_iterator_count_result(iterator, values) -} - -/// Returns the element count for eval-supported array iterator inputs. -fn eval_iterator_count_result( - iterator: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(iterator)?; - values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. -pub(super) fn eval_builtin_iterator_to_array( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [iterator] => { - let iterator = eval_expr(iterator, context, scope, values)?; - eval_iterator_to_array_result(iterator, true, values) - } - [iterator, preserve_keys] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; - let preserve_keys = values.truthy(preserve_keys)?; - eval_iterator_to_array_result(iterator, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Copies eval-supported array iterator inputs into a PHP array result. -fn eval_iterator_to_array_result( - iterator: RuntimeCellHandle, - preserve_keys: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - if preserve_keys { - return eval_array_copy_preserve_keys(iterator, values); - } - eval_array_projection_result("array_values", iterator, values) -} - -/// Copies one array-like eval value while preserving iteration keys and order. -fn eval_array_copy_preserve_keys( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_reverse()` over an eval array expression. -pub(super) fn eval_builtin_array_reverse( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array] => { - let array = eval_expr(array, context, scope, values)?; - eval_array_reverse_result(array, false, values) - } - [array, preserve_keys] => { - let array = eval_expr(array, context, scope, values)?; - let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; - let preserve_keys = values.truthy(preserve_keys)?; - eval_array_reverse_result(array, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds an `array_reverse()` result while preserving PHP key rules. -fn eval_array_reverse_result( - array: RuntimeCellHandle, - preserve_keys: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut keys = Vec::with_capacity(len); - let mut has_string_key = false; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; - keys.push(key); - } - - let mut result = if preserve_keys || has_string_key { - values.assoc_new(len)? - } else { - values.array_new(len)? - }; - let mut next_numeric_key = 0_i64; - - for key in keys.into_iter().rev() { - let value = values.array_get(array, key)?; - let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { - key - } else { - let key = values.int(next_numeric_key)?; - next_numeric_key += 1; - key - }; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_key_exists()` over a key and array expression. -pub(super) fn eval_builtin_array_key_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [key, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let key = eval_expr(key, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - values.array_key_exists(key, array) -} - -/// Evaluates PHP array search builtins over a needle and haystack expression. -pub(super) fn eval_builtin_array_search( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [needle, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let needle = eval_expr(needle, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_array_search_result(name, needle, array, values) -} - -/// Searches an eval array with PHP's default loose comparison semantics. -fn eval_array_search_result( - name: &str, - needle: RuntimeCellHandle, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let equal = values.compare(EvalBinOp::LooseEq, needle, value)?; - if values.truthy(equal)? { - return match name { - "in_array" => values.bool_value(true), - "array_search" => Ok(key), - _ => Err(EvalStatus::UnsupportedConstruct), - }; - } - } - match name { - "in_array" => values.bool_value(false), - "array_search" => values.bool_value(false), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP value-set array builtins over two eval array expressions. -pub(super) fn eval_builtin_array_value_set( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_value_set_result(name, left, right, values) -} - -/// Builds `array_diff()` or `array_intersect()` using PHP's default string comparison mode. -fn eval_array_value_set_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let right_len = values.array_len(right)?; - let mut right_values = Vec::with_capacity(right_len); - for position in 0..right_len { - let key = values.array_iter_key(right, position)?; - let value = values.array_get(right, key)?; - right_values.push(values.string_bytes(value)?); - } - - let mut result = values.assoc_new(left_len)?; - for position in 0..left_len { - let key = values.array_iter_key(left, position)?; - let value = values.array_get(left, key)?; - let comparable = values.string_bytes(value)?; - let found = right_values - .iter() - .any(|right_value| right_value == &comparable); - let keep = match name { - "array_diff" => !found, - "array_intersect" => found, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Evaluates PHP key-set array builtins over two eval array expressions. -pub(super) fn eval_builtin_array_key_set( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_key_set_result(name, left, right, values) -} - -/// Builds `array_diff_key()` or `array_intersect_key()` by testing first-array keys. -fn eval_array_key_set_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let mut result = values.assoc_new(left_len)?; - for position in 0..left_len { - let key = values.array_iter_key(left, position)?; - let value = values.array_get(left, key)?; - let exists = values.array_key_exists(key, right)?; - let found = values.truthy(exists)?; - let keep = match name { - "array_diff_key" => !found, - "array_intersect_key" => found, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Evaluates PHP `array_rand()` over one eval array expression. -pub(super) fn eval_builtin_array_rand( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_rand_result(array, values) -} - -/// Returns a valid random key from a non-empty eval array. -fn eval_array_rand_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - if len == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let position = eval_random_position(len); - values.array_iter_key(array, position) -} - -/// Chooses a pseudo-random array position within `[0, len)`. -fn eval_random_position(len: usize) -> usize { - (eval_random_u128() % (len as u128)) as usize -} - -/// Produces a process-local pseudo-random word for non-cryptographic eval builtins. -fn eval_random_u128() -> u128 { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - let counter = u128::from(EVAL_RANDOM_COUNTER.fetch_add(1, Ordering::Relaxed)); - let pid = u128::from(std::process::id()); - let mut value = nanos ^ (counter.wrapping_mul(0x9e37_79b9_7f4a_7c15)) ^ (pid << 64); - value ^= value >> 30; - value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); - value ^= value >> 27; - value = value.wrapping_mul(0x94d0_49bb_1331_11eb); - value ^ (value >> 31) -} - -/// Evaluates PHP `rand()` and `mt_rand()` over zero args or an inclusive range. -pub(super) fn eval_builtin_rand( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_rand_result(None, None, values), - [min, max] => { - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_rand_result(Some(min), Some(max), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `random_int()` over an inclusive integer range. -pub(super) fn eval_builtin_random_int( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [min, max] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_random_int_result(min, max, values) -} - -/// Returns one non-cryptographic random integer using PHP's inclusive range rules. -fn eval_rand_result( - min: Option, - max: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let (min, max) = match (min, max) { - (None, None) => (0, i64::from(i32::MAX)), - (Some(min), Some(max)) => (eval_int_value(min, values)?, eval_int_value(max, values)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let low = min.min(max); - let high = min.max(max); - let width = (i128::from(high) - i128::from(low) + 1) as u128; - let offset = (eval_random_u128() % width) as i128; - let sampled = i128::from(low) + offset; - let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(sampled) -} - -/// Returns one eval `random_int()` value in the inclusive range `[min, max]`. -fn eval_random_int_result( - min: RuntimeCellHandle, - max: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let min = eval_int_value(min, values)?; - let max = eval_int_value(max, values)?; - if min > max { - return Err(EvalStatus::RuntimeFatal); - } - let width = (i128::from(max) - i128::from(min) + 1) as u128; - let offset = (eval_random_u128() % width) as i128; - let sampled = i128::from(min) + offset; - let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(sampled) -} - -/// Evaluates PHP `range()` over integer-compatible start and end expressions. -pub(super) fn eval_builtin_range( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [start, end] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let start = eval_expr(start, context, scope, values)?; - let end = eval_expr(end, context, scope, values)?; - eval_range_result(start, end, values) -} - -/// Builds an inclusive ascending or descending integer `range()` result. -fn eval_range_result( - start: RuntimeCellHandle, - end: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let start = eval_int_value(start, values)?; - let end = eval_int_value(end, values)?; - let distance = if start <= end { - end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? - } else { - start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? - }; - let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; - let step = if start <= end { 1_i64 } else { -1_i64 }; - let mut current = start; - let mut result = values.array_new(count)?; - - for index in 0..count { - let key = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - let value = values.int(current)?; - result = values.array_set(result, key, value)?; - if index + 1 < count { - current = current.checked_add(step).ok_or(EvalStatus::RuntimeFatal)?; - } - } - Ok(result) -} - -/// Evaluates PHP `array_merge()` over two array expressions. -pub(super) fn eval_builtin_array_merge( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_merge_result(left, right, values) -} - -/// Builds an `array_merge()` result with PHP numeric reindexing and string-key overwrites. -fn eval_array_merge_result( - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let right_len = values.array_len(right)?; - let capacity = left_len - .checked_add(right_len) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut result = values.assoc_new(capacity)?; - let mut next_numeric_key = 0_i64; - result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; - eval_array_merge_append_operand(result, right, &mut next_numeric_key, values) -} - -/// Appends one source array to an `array_merge()` result using PHP key handling. -fn eval_array_merge_append_operand( - mut result: RuntimeCellHandle, - source: RuntimeCellHandle, - next_numeric_key: &mut i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(source)?; - for position in 0..len { - let source_key = values.array_iter_key(source, position)?; - let source_value = values.array_get(source, source_key)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_STRING { - source_key - } else { - let target_key = values.int(*next_numeric_key)?; - *next_numeric_key = (*next_numeric_key) - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - target_key - }; - result = values.array_set(result, target_key, source_value)?; - } - Ok(result) -} - -/// Evaluates PHP `explode()` over separator and string expressions. -pub(super) fn eval_builtin_explode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [separator, string] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let separator = eval_expr(separator, context, scope, values)?; - let string = eval_expr(string, context, scope, values)?; - eval_explode_result(separator, string, values) -} - -/// Splits one PHP byte string into an indexed array using a non-empty separator. -fn eval_explode_result( - separator: RuntimeCellHandle, - string: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let separator = values.string_bytes(separator)?; - if separator.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let string = values.string_bytes(string)?; - let mut result = values.array_new(0)?; - let mut start = 0; - let mut index = 0_i64; - while let Some(found) = eval_find_subslice(&string, &separator, start) { - result = eval_push_explode_segment(result, index, &string[start..found], values)?; - start = found + separator.len(); - index += 1; - } - eval_push_explode_segment(result, index, &string[start..], values) -} - -/// Appends one split segment to an indexed `explode()` result array. -fn eval_push_explode_segment( - array: RuntimeCellHandle, - index: i64, - segment: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(index)?; - let value = values.string_bytes_value(segment)?; - values.array_set(array, key, value) -} - -/// Finds `needle` inside `haystack` starting from one byte offset. -fn eval_find_subslice(haystack: &[u8], needle: &[u8], start: usize) -> Option { - haystack - .get(start..)? - .windows(needle.len()) - .position(|window| window == needle) - .map(|position| position + start) -} - -/// Evaluates PHP `implode()` over separator and array expressions. -pub(super) fn eval_builtin_implode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [separator, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let separator = eval_expr(separator, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_implode_result(separator, array, values) -} - -/// Joins array values in eval iteration order using PHP string conversion. -fn eval_implode_result( - separator: RuntimeCellHandle, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !values.is_array_like(array)? { - return Err(EvalStatus::RuntimeFatal); - } - let separator = values.string_bytes(separator)?; - let len = values.array_len(array)?; - let mut output = Vec::new(); - for position in 0..len { - if position > 0 { - output.extend_from_slice(&separator); - } - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let value = values.string_bytes(value)?; - output.extend_from_slice(&value); - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `ceil(...)` over one eval expression. -pub(super) fn eval_builtin_ceil( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.ceil(value) -} - -/// Evaluates PHP's `floor(...)` over one eval expression. -pub(super) fn eval_builtin_floor( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.floor(value) -} - -/// Evaluates PHP's zero-argument `pi()` builtin. -pub(super) fn eval_builtin_pi( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI) -} - -/// Evaluates PHP's `pow(...)` over two eval expressions. -pub(super) fn eval_builtin_pow( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - values.pow(left, right) -} - -/// Evaluates PHP's `round(...)` over one value and an optional precision expression. -pub(super) fn eval_builtin_round( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - values.round(value, None) - } - [value, precision] => { - let value = eval_expr(value, context, scope, values)?; - let precision = eval_expr(precision, context, scope, values)?; - values.round(value, Some(precision)) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `number_format(...)` over one number and optional separators. -pub(super) fn eval_builtin_number_format( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_number_format_result(value, None, None, None, values) - } - [value, decimals] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - eval_number_format_result(value, Some(decimals), None, None, values) - } - [value, decimals, decimal_separator] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; - eval_number_format_result(value, Some(decimals), Some(decimal_separator), None, values) - } - [value, decimals, decimal_separator, thousands_separator] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; - let thousands_separator = eval_expr(thousands_separator, context, scope, values)?; - eval_number_format_result( - value, - Some(decimals), - Some(decimal_separator), - Some(thousands_separator), - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Formats one PHP numeric value with grouped thousands and fixed decimals. -fn eval_number_format_result( - value: RuntimeCellHandle, - decimals: Option, - decimal_separator: Option, - thousands_separator: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_float_value(value, values)?; - let decimals = match decimals { - Some(decimals) => eval_int_value(decimals, values)?, - None => 0, - }; - let decimal_separator = match decimal_separator { - Some(separator) => values.string_bytes(separator)?, - None => b".".to_vec(), - }; - let thousands_separator = match thousands_separator { - Some(separator) => values.string_bytes(separator)?, - None => b",".to_vec(), - }; - let output = - eval_number_format_bytes(value, decimals, &decimal_separator, &thousands_separator)?; - values.string_bytes_value(&output) -} - -/// Evaluates direct positional `sprintf()` or `printf()` calls in source order. -pub(super) fn eval_builtin_sprintf_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_sprintf_like_result(name, &evaluated_args, values) -} - -/// Evaluates direct positional `vsprintf()` or `vprintf()` calls in source order. -pub(super) fn eval_builtin_vsprintf_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_vsprintf_like_result(name, &evaluated_args, values) -} - -/// Evaluates direct positional `sscanf()` calls in source order. -pub(super) fn eval_builtin_sscanf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let input = eval_expr(&args[0], context, scope, values)?; - let format = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - eval_expr(arg, context, scope, values)?; - } - eval_sscanf_result(input, format, values) -} - -/// Dispatches already evaluated `sprintf()` or `printf()` arguments. -fn eval_sprintf_like_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "sprintf" => eval_sprintf_result(evaluated_args, values), - "printf" => eval_printf_result(evaluated_args, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Dispatches already evaluated `vsprintf()` or `vprintf()` arguments. -fn eval_vsprintf_like_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "vsprintf" => eval_vsprintf_result(evaluated_args, values), - "vprintf" => eval_vprintf_result(evaluated_args, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Parses one string through the eval `sscanf()` subset and returns an indexed array. -fn eval_sscanf_result( - input: RuntimeCellHandle, - format: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let input = values.string_bytes(input)?; - let format = values.string_bytes(format)?; - let matches = eval_sscanf_matches(&input, &format); - let mut result = values.array_new(matches.len())?; - for (index, matched) in matches.iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.string_bytes_value(matched)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Extracts `%d`, `%f`, `%s`, and `%%` matches with the same subset as native `sscanf()`. -fn eval_sscanf_matches(input: &[u8], format: &[u8]) -> Vec> { - let mut matches = Vec::new(); - let mut input_index = 0; - let mut format_index = 0; - - while format_index < format.len() { - if format[format_index] != b'%' { - if input_index >= input.len() || input[input_index] != format[format_index] { - break; - } - input_index += 1; - format_index += 1; - continue; - } - - format_index += 1; - if format_index >= format.len() { - break; - } - - match format[format_index] { - b'%' => { - if input_index >= input.len() || input[input_index] != b'%' { - break; - } - input_index += 1; - } - b'd' => matches.push(eval_sscanf_scan_int(input, &mut input_index)), - b'f' => matches.push(eval_sscanf_scan_float(input, &mut input_index)), - b's' => matches.push(eval_sscanf_scan_word(input, &mut input_index)), - _ => {} - } - format_index += 1; - } - - matches -} - -/// Scans the native `sscanf()` `%d` subset as a matched byte slice. -fn eval_sscanf_scan_int(input: &[u8], input_index: &mut usize) -> Vec { - let start = *input_index; - if input.get(*input_index) == Some(&b'-') { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - input[start..*input_index].to_vec() -} - -/// Scans the native `sscanf()` `%f` subset as a matched byte slice. -fn eval_sscanf_scan_float(input: &[u8], input_index: &mut usize) -> Vec { - let start = *input_index; - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'+' | b'-')) - { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - if input.get(*input_index) == Some(&b'.') { - *input_index += 1; - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - } - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'e' | b'E')) - { - *input_index += 1; - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'+' | b'-')) - { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - } - input[start..*input_index].to_vec() -} - -/// Scans the native `sscanf()` `%s` subset as a non-space byte word. -fn eval_sscanf_scan_word(input: &[u8], input_index: &mut usize) -> Vec { - let start = *input_index; - while input - .get(*input_index) - .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\n')) - { - *input_index += 1; - } - input[start..*input_index].to_vec() -} - -/// Formats `sprintf()` arguments and returns the resulting PHP string. -fn eval_sprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((format, format_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - values.string_bytes_value(&output) -} - -/// Formats `printf()` arguments, echoes the result, and returns its byte count. -fn eval_printf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((format, format_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.int(len) -} - -/// Formats `vsprintf()` array arguments and returns the resulting PHP string. -fn eval_vsprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let format_args = eval_sprintf_argument_array_values(*array, values)?; - let output = eval_sprintf_bytes(&format, &format_args, values)?; - values.string_bytes_value(&output) -} - -/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. -fn eval_vprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let format_args = eval_sprintf_argument_array_values(*array, values)?; - let output = eval_sprintf_bytes(&format, &format_args, values)?; - let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.int(len) -} - -/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. -fn eval_sprintf_argument_array_values( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !values.is_array_like(array)? { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(array)?; - let mut args = Vec::with_capacity(len); - for position in 0..len { - let key = values.array_iter_key(array, position)?; - args.push(values.array_get(array, key)?); - } - Ok(args) -} - -/// Formats one printf-style byte string through eval runtime value coercions. -fn eval_sprintf_bytes( - format: &[u8], - args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut output = Vec::new(); - let mut index = 0; - let mut arg_index = 0; - while index < format.len() { - if format[index] != b'%' { - output.push(format[index]); - index += 1; - continue; - } - index += 1; - if index >= format.len() { - break; - } - if format[index] == b'%' { - output.push(b'%'); - index += 1; - continue; - } - - let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; - index = next_index; - let Some(arg) = args.get(arg_index).copied() else { - return Err(EvalStatus::RuntimeFatal); - }; - arg_index += 1; - let bytes = eval_format_sprintf_arg(spec, arg, values)?; - output.extend_from_slice(&bytes); - } - Ok(output) -} - -/// Parses flags, width, precision, and terminal type for one format specifier. -fn eval_parse_sprintf_spec( - format: &[u8], - mut index: usize, -) -> Result<(EvalSprintfSpec, usize), EvalStatus> { - let mut spec = EvalSprintfSpec { - left_align: false, - force_sign: false, - space_sign: false, - zero_pad: false, - alternate: false, - width: None, - precision: None, - specifier: 0, - }; - while index < format.len() { - match format[index] { - b'-' => spec.left_align = true, - b'+' => spec.force_sign = true, - b' ' => spec.space_sign = true, - b'0' => spec.zero_pad = true, - b'#' => spec.alternate = true, - _ => break, - } - index += 1; - } - let (width, next_index) = eval_parse_sprintf_number(format, index)?; - spec.width = width; - index = next_index; - if index < format.len() && format[index] == b'.' { - let (precision, next_index) = eval_parse_sprintf_number(format, index + 1)?; - spec.precision = Some(precision.unwrap_or(0)); - index = next_index; - } - if index >= format.len() { - return Err(EvalStatus::RuntimeFatal); - } - spec.specifier = format[index]; - Ok((spec, index + 1)) -} - -/// Parses an unsigned decimal number from a format specifier component. -fn eval_parse_sprintf_number( - format: &[u8], - mut index: usize, -) -> Result<(Option, usize), EvalStatus> { - let start = index; - let mut value = 0usize; - while index < format.len() && format[index].is_ascii_digit() { - value = value - .checked_mul(10) - .and_then(|value| value.checked_add(usize::from(format[index] - b'0'))) - .ok_or(EvalStatus::RuntimeFatal)?; - index += 1; - } - if index == start { - Ok((None, index)) - } else { - Ok((Some(value), index)) - } -} - -/// Formats one runtime value according to a parsed eval sprintf specifier. -fn eval_format_sprintf_arg( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - match spec.specifier { - b's' => eval_format_sprintf_string(spec, value, values), - b'f' | b'e' | b'g' => eval_format_sprintf_float(spec, value, values), - b'c' => eval_format_sprintf_char(spec, value, values), - _ => eval_format_sprintf_int(spec, value, values), - } -} - -/// Formats a `%s` operand after PHP string coercion. -fn eval_format_sprintf_string( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut bytes = values.string_bytes(value)?; - if let Some(precision) = spec.precision { - bytes.truncate(precision); - } - Ok(eval_sprintf_apply_width(bytes, spec, false)) -} - -/// Formats an integer-like operand for decimal, unsigned, hex, and octal specifiers. -fn eval_format_sprintf_int( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_int_value(value, values)?; - let mut output = Vec::new(); - match spec.specifier { - b'u' => { - let digits = eval_sprintf_precision_pad((value as u64).to_string().into_bytes(), spec); - output.extend_from_slice(&digits); - } - b'x' | b'X' => { - let unsigned = value as u64; - if spec.alternate && unsigned != 0 { - output.extend_from_slice(if spec.specifier == b'X' { b"0X" } else { b"0x" }); - } - let digits = if spec.specifier == b'X' { - format!("{unsigned:X}").into_bytes() - } else { - format!("{unsigned:x}").into_bytes() - }; - output.extend_from_slice(&eval_sprintf_precision_pad(digits, spec)); - } - b'o' => { - let unsigned = value as u64; - let mut digits = eval_sprintf_precision_pad(format!("{unsigned:o}").into_bytes(), spec); - if spec.alternate && !digits.starts_with(b"0") { - output.push(b'0'); - } - output.append(&mut digits); - } - _ => { - let value = value as i128; - let magnitude = if value < 0 { - (-value) as u128 - } else { - value as u128 - }; - if value < 0 { - output.push(b'-'); - } else if spec.force_sign { - output.push(b'+'); - } else if spec.space_sign { - output.push(b' '); - } - let digits = eval_sprintf_precision_pad(magnitude.to_string().into_bytes(), spec); - output.extend_from_slice(&digits); - } - } - Ok(eval_sprintf_apply_width(output, spec, true)) -} - -/// Formats a `%c` operand as the low byte of its integer value. -fn eval_format_sprintf_char( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_int_value(value, values)?; - Ok(eval_sprintf_apply_width(vec![value as u8], spec, false)) -} - -/// Formats a floating-point operand for the eval printf-family subset. -fn eval_format_sprintf_float( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_float_value(value, values)?; - let precision = spec.precision.unwrap_or(6); - let mut output = if value.is_nan() { - b"NAN".to_vec() - } else if value.is_infinite() { - b"INF".to_vec() - } else { - match spec.specifier { - b'e' => format!("{value:.precision$e}").into_bytes(), - b'g' => format!("{value:.precision$}").into_bytes(), - _ => format!("{value:.precision$}").into_bytes(), - } - }; - if value.is_sign_negative() && !output.starts_with(b"-") { - output.insert(0, b'-'); - } else if value.is_sign_positive() && value.is_finite() { - if spec.force_sign { - output.insert(0, b'+'); - } else if spec.space_sign { - output.insert(0, b' '); - } - } - Ok(eval_sprintf_apply_width(output, spec, true)) -} - -/// Applies integer precision by left-padding digits with zeros. -fn eval_sprintf_precision_pad(mut digits: Vec, spec: EvalSprintfSpec) -> Vec { - if matches!(spec.precision, Some(0)) && digits == b"0" { - digits.clear(); - return digits; - } - let Some(precision) = spec.precision else { - return digits; - }; - if digits.len() >= precision { - return digits; - } - let mut output = vec![b'0'; precision - digits.len()]; - output.append(&mut digits); - output -} - -/// Applies field width and alignment to one formatted eval sprintf replacement. -fn eval_sprintf_apply_width(mut bytes: Vec, spec: EvalSprintfSpec, numeric: bool) -> Vec { - let Some(width) = spec.width else { - return bytes; - }; - if bytes.len() >= width { - return bytes; - } - let pad_len = width - bytes.len(); - if spec.left_align { - bytes.extend(std::iter::repeat_n(b' ', pad_len)); - return bytes; - } - if numeric && spec.zero_pad && spec.precision.is_none() { - let prefix_len = eval_sprintf_zero_pad_prefix_len(&bytes); - let mut output = Vec::with_capacity(width); - output.extend_from_slice(&bytes[..prefix_len]); - output.extend(std::iter::repeat_n(b'0', pad_len)); - output.extend_from_slice(&bytes[prefix_len..]); - return output; - } - let mut output = Vec::with_capacity(width); - output.extend(std::iter::repeat_n(b' ', pad_len)); - output.append(&mut bytes); - output -} - -/// Returns the sign and alternate-prefix length that should precede zero padding. -fn eval_sprintf_zero_pad_prefix_len(bytes: &[u8]) -> usize { - let mut prefix_len = usize::from(matches!(bytes.first(), Some(b'+' | b'-' | b' '))); - if bytes.len() >= prefix_len + 2 - && bytes[prefix_len] == b'0' - && matches!(bytes[prefix_len + 1], b'x' | b'X') - { - prefix_len += 2; - } - prefix_len -} - -/// Converts one eval value to PHP float and returns the scalar payload. -pub(super) fn eval_float_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.cast_float(value)?; - let bytes = values.string_bytes(value)?; - std::str::from_utf8(&bytes) - .map_err(|_| EvalStatus::RuntimeFatal)? - .parse::() - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Produces PHP `number_format()` bytes for finite scalar values. -fn eval_number_format_bytes( - value: f64, - decimals: i64, - decimal_separator: &[u8], - thousands_separator: &[u8], -) -> Result, EvalStatus> { - if !value.is_finite() { - return Ok(value.to_string().into_bytes()); - } - let decimals = decimals.clamp(-308, 308); - let display_decimals = decimals.max(0) as usize; - let abs_value = value.abs(); - let scaled = if decimals >= 0 { - let scale = 10_f64.powi(decimals as i32); - (abs_value * scale).round() - } else { - let scale = 10_f64.powi((-decimals) as i32); - (abs_value / scale).round() * scale - }; - if scaled > (u128::MAX as f64) { - return Err(EvalStatus::RuntimeFatal); - } - let scaled = scaled as u128; - let scale = 10_u128 - .checked_pow(display_decimals as u32) - .ok_or(EvalStatus::RuntimeFatal)?; - let integer = if display_decimals == 0 { - scaled - } else { - scaled / scale - }; - let fraction = if display_decimals == 0 { - 0 - } else { - scaled % scale - }; - - let mut output = Vec::new(); - if value.is_sign_negative() && scaled != 0 { - output.push(b'-'); - } - eval_append_grouped_decimal(&mut output, integer, thousands_separator); - if display_decimals > 0 { - output.extend_from_slice(decimal_separator); - let fraction = format!("{fraction:0display_decimals$}"); - output.extend_from_slice(fraction.as_bytes()); - } - Ok(output) -} - -/// Appends one unsigned decimal integer with optional three-digit grouping. -fn eval_append_grouped_decimal(output: &mut Vec, value: u128, separator: &[u8]) { - let digits = value.to_string(); - if separator.is_empty() { - output.extend_from_slice(digits.as_bytes()); - return; - } - let first_group = match digits.len() % 3 { - 0 => 3, - len => len, - }; - output.extend_from_slice(&digits.as_bytes()[..first_group]); - let mut index = first_group; - while index < digits.len() { - output.extend_from_slice(separator); - output.extend_from_slice(&digits.as_bytes()[index..index + 3]); - index += 3; - } -} - -/// Evaluates PHP's `sqrt(...)` over one eval expression. -pub(super) fn eval_builtin_sqrt( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.sqrt(value) -} - -/// Evaluates PHP's `strrev(...)` over one eval expression. -pub(super) fn eval_builtin_strrev( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.strrev(value) -} - -/// Evaluates PHP's `chr(...)` over one eval expression. -pub(super) fn eval_builtin_chr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_chr_result(value, values) -} - -/// Converts one eval value to a PHP integer and returns the low byte as a string. -fn eval_chr_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - values.string_bytes_value(&[value as u8]) -} - -/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. -pub(super) fn eval_builtin_str_repeat( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value, times] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let times = eval_expr(times, context, scope, values)?; - eval_str_repeat_result(value, times, values) -} - -/// Repeats one PHP string byte sequence according to a PHP-cast integer count. -fn eval_str_repeat_result( - value: RuntimeCellHandle, - times: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let times = eval_int_value(times, values)?; - if times < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; - let capacity = bytes - .len() - .checked_mul(times) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(capacity); - for _ in 0..times { - output.extend_from_slice(&bytes); - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. -pub(super) fn eval_builtin_str_replace( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [search, replace, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let search = eval_expr(search, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_str_replace_result(name, search, replace, subject, values) -} - -/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. -fn eval_str_replace_result( - name: &str, - search: RuntimeCellHandle, - replace: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let search = values.string_bytes(search)?; - let replace = values.string_bytes(replace)?; - let subject = values.string_bytes(subject)?; - if search.is_empty() { - return values.string_bytes_value(&subject); - } - - let mut output = Vec::with_capacity(subject.len()); - let mut start = 0; - while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { - output.extend_from_slice(&subject[start..found]); - output.extend_from_slice(&replace); - start = found + search.len(); - } - output.extend_from_slice(&subject[start..]); - values.string_bytes_value(&output) -} - -/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. -fn eval_find_replace_match( - name: &str, - subject: &[u8], - search: &[u8], - start: usize, -) -> Result, EvalStatus> { - match name { - "str_replace" => Ok(eval_find_subslice(subject, search, start)), - "str_ireplace" => Ok(subject - .get(start..) - .and_then(|tail| { - tail.windows(search.len()) - .position(|window| window.eq_ignore_ascii_case(search)) - }) - .map(|position| position + start)), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. -pub(super) fn eval_builtin_str_pad( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, length] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_str_pad_result(value, length, None, None, values) - } - [value, length, pad_string] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let pad_string = eval_expr(pad_string, context, scope, values)?; - eval_str_pad_result(value, length, Some(pad_string), None, values) - } - [value, length, pad_string, pad_type] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let pad_string = eval_expr(pad_string, context, scope, values)?; - let pad_type = eval_expr(pad_type, context, scope, values)?; - eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Pads one byte string to a PHP target length using cyclic pad bytes. -fn eval_str_pad_result( - value: RuntimeCellHandle, - length: RuntimeCellHandle, - pad_string: Option, - pad_type: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let target_length = eval_int_value(length, values)?; - let Ok(target_length) = usize::try_from(target_length) else { - return values.string_bytes_value(&bytes); - }; - if target_length <= bytes.len() { - return values.string_bytes_value(&bytes); - } - - let pad_string = match pad_string { - Some(pad_string) => values.string_bytes(pad_string)?, - None => b" ".to_vec(), - }; - if pad_string.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let pad_type = match pad_type { - Some(pad_type) => eval_int_value(pad_type, values)?, - None => 1, - }; - let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; - let capacity = bytes - .len() - .checked_add(left_pad) - .and_then(|size| size.checked_add(right_pad)) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(capacity); - eval_append_repeated_pad(&mut output, &pad_string, left_pad); - output.extend_from_slice(&bytes); - eval_append_repeated_pad(&mut output, &pad_string, right_pad); - values.string_bytes_value(&output) -} - -/// Splits a `str_pad()` pad budget into left and right byte counts. -fn eval_str_pad_sides(pad_budget: usize, pad_type: i64) -> Result<(usize, usize), EvalStatus> { - match pad_type { - 0 => Ok((pad_budget, 0)), - 1 => Ok((0, pad_budget)), - 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Appends `count` bytes by cycling through the provided non-empty pad string. -fn eval_append_repeated_pad(output: &mut Vec, pad_string: &[u8], count: usize) { - for index in 0..count { - output.push(pad_string[index % pad_string.len()]); - } -} - -/// Evaluates PHP `str_split(...)` over one string and optional chunk length. -pub(super) fn eval_builtin_str_split( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_str_split_result(value, None, values) - } - [value, length] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_str_split_result(value, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. -fn eval_str_split_result( - value: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let length = match length { - Some(length) => eval_int_value(length, values)?, - None => 1, - }; - if length <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut result = values.array_new(0)?; - for (index, chunk) in bytes.chunks(length).enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string_bytes_value(chunk)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. -pub(super) fn eval_builtin_nl2br( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_nl2br_result(value, true, values) - } - [value, use_xhtml] => { - let value = eval_expr(value, context, scope, values)?; - let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; - let use_xhtml = values.truthy(use_xhtml)?; - eval_nl2br_result(value, use_xhtml, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. -fn eval_nl2br_result( - value: RuntimeCellHandle, - use_xhtml: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let br = if use_xhtml { - b"
".as_slice() - } else { - b"
".as_slice() - }; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - let byte = bytes[index]; - if byte == b'\r' || byte == b'\n' { - output.extend_from_slice(br); - output.push(byte); - if index + 1 < bytes.len() - && ((byte == b'\r' && bytes[index + 1] == b'\n') - || (byte == b'\n' && bytes[index + 1] == b'\r')) - { - output.push(bytes[index + 1]); - index += 2; - continue; - } - } else { - output.push(byte); - } - index += 1; - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. -pub(super) fn eval_builtin_substr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, offset] => { - let value = eval_expr(value, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_substr_result(value, offset, None, values) - } - [value, offset, length] => { - let value = eval_expr(value, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_substr_result(value, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Slices a PHP byte string using PHP `substr()` offset and length rules. -fn eval_substr_result( - value: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = eval_int_value(offset, values)?; - let start = if offset < 0 { - (total + offset).max(0) - } else { - offset.min(total) - }; - let end = match length { - None => total, - Some(length) if values.is_null(length)? => total, - Some(length) => { - let length = eval_int_value(length, values)?; - if length < 0 { - (total + length).max(0) - } else { - start.saturating_add(length).min(total) - } - } - }; - let end = end.max(start); - let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; - let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string_bytes_value(&bytes[start..end]) -} - -/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. -pub(super) fn eval_builtin_substr_replace( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, replace, offset] => { - let value = eval_expr(value, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_substr_replace_result(value, replace, offset, None, values) - } - [value, replace, offset, length] => { - let value = eval_expr(value, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_substr_replace_result(value, replace, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. -fn eval_substr_replace_result( - value: RuntimeCellHandle, - replace: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let replacement = values.string_bytes(replace)?; - let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = eval_int_value(offset, values)?; - let start = if offset < 0 { - (total + offset).max(0) - } else { - offset.min(total) - }; - let end = match length { - None => total, - Some(length) if values.is_null(length)? => total, - Some(length) => { - let length = eval_int_value(length, values)?; - if length < 0 { - (total + length).max(start) - } else { - start.saturating_add(length).min(total) - } - } - }; - let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; - let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(bytes.len() + replacement.len()); - output.extend_from_slice(&bytes[..start]); - output.extend_from_slice(&replacement); - output.extend_from_slice(&bytes[end..]); - values.string_bytes_value(&output) -} - -/// Evaluates eval HTML entity encode/decode builtins over one string expression. -pub(super) fn eval_builtin_html_entity( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_html_entity_result(name, value, values) -} - -/// Applies the eval-supported HTML entity transform for one PHP string value. -fn eval_html_entity_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), - "html_entity_decode" => eval_html_entity_decode_result(value, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Encodes the HTML-special byte characters covered by elephc's static helper. -fn eval_htmlspecialchars_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - for byte in bytes { - match byte { - b'&' => output.extend_from_slice(b"&"), - b'<' => output.extend_from_slice(b"<"), - b'>' => output.extend_from_slice(b">"), - b'"' => output.extend_from_slice(b"""), - b'\'' => output.extend_from_slice(b"'"), - _ => output.push(byte), - } - } - values.string_bytes_value(&output) -} - -/// Decodes one pass of the HTML entities emitted by the eval/static encoders. -fn eval_html_entity_decode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'&' { - if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { - output.push(decoded); - index += width; - continue; - } - } - output.push(bytes[index]); - index += 1; - } - values.string_bytes_value(&output) -} - -/// Returns the decoded byte and consumed width for one supported HTML entity. -fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { - for (entity, decoded) in [ - (b"<".as_slice(), b'<'), - (b">".as_slice(), b'>'), - (b""".as_slice(), b'"'), - (b"'".as_slice(), b'\''), - (b"'".as_slice(), b'\''), - (b"&".as_slice(), b'&'), - ] { - if bytes.starts_with(entity) { - return Some((decoded, entity.len())); - } - } - None -} - -/// Evaluates PHP URL encode builtins over one eval string expression. -pub(super) fn eval_builtin_url_encode( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_url_encode_result(name, value, values) -} - -/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. -fn eval_url_encode_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - const HEX: &[u8; 16] = b"0123456789ABCDEF"; - for byte in bytes { - if eval_url_encode_keeps_byte(name, byte)? { - output.push(byte); - } else if name == "urlencode" && byte == b' ' { - output.push(b'+'); - } else { - output.push(b'%'); - output.push(HEX[(byte >> 4) as usize]); - output.push(HEX[(byte & 0x0f) as usize]); - } - } - values.string_bytes_value(&output) -} - -/// Returns whether a byte remains unescaped for the selected PHP URL encoder. -fn eval_url_encode_keeps_byte(name: &str, byte: u8) -> Result { - let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); - match name { - "urlencode" => Ok(common), - "rawurlencode" => Ok(common || byte == b'~'), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP URL decode builtins over one eval string expression. -pub(super) fn eval_builtin_url_decode( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_url_decode_result(name, value, values) -} - -/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. -fn eval_url_decode_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let plus_to_space = match name { - "urldecode" => true, - "rawurldecode" => false, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'+' && plus_to_space { - output.push(b' '); - index += 1; - } else if bytes[index] == b'%' && index + 2 < bytes.len() { - if let (Some(high), Some(low)) = ( - eval_hex_nibble(bytes[index + 1]), - eval_hex_nibble(bytes[index + 2]), - ) { - output.push((high << 4) | low); - index += 3; - continue; - } - output.push(bytes[index]); - index += 1; - } else { - output.push(bytes[index]); - index += 1; - } - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP `ctype_*` predicates over one eval string expression. -pub(super) fn eval_builtin_ctype( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_ctype_result(name, value, values) -} - -/// Returns the PHP boolean result for one ASCII `ctype_*` byte-string check. -fn eval_ctype_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut matches = !bytes.is_empty(); - for byte in bytes { - if !eval_ctype_byte_matches(name, byte)? { - matches = false; - break; - } - } - values.bool_value(matches) -} - -/// Checks one byte against the selected PHP ASCII character class. -fn eval_ctype_byte_matches(name: &str, byte: u8) -> Result { - match name { - "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), - "ctype_digit" => Ok(byte.is_ascii_digit()), - "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), - "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `crc32(...)` over one eval string expression. -pub(super) fn eval_builtin_crc32( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_crc32_result(value, values) -} - -/// Computes PHP's non-negative CRC-32 integer over one converted byte string. -fn eval_crc32_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.int(i64::from(eval_crc32_bytes(&bytes))) -} - -/// Evaluates one-shot PHP hash digest builtins over eval expressions. -pub(super) fn eval_builtin_hash_one_shot( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_hash_one_shot_result(name, &evaluated_args, values) -} - -/// Computes the result for one-shot PHP hash digest builtins from evaluated args. -fn eval_hash_one_shot_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "md5" | "sha1" => { - let (data, binary) = match evaluated_args { - [data] => (*data, false), - [data, binary] => (*data, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let data = values.string_bytes(data)?; - eval_hash_digest_result(name.as_bytes(), &data, binary, values) - } - "hash" => { - let (algo, data, binary) = match evaluated_args { - [algo, data] => (*algo, *data, false), - [algo, data, binary] => (*algo, *data, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let algo = values.string_bytes(algo)?; - let data = values.string_bytes(data)?; - eval_hash_digest_result(&algo, &data, binary, values) - } - "hash_file" => { - let (algo, filename, binary) = match evaluated_args { - [algo, filename] => (*algo, *filename, false), - [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_hash_file_result(algo, filename, binary, values) - } - "hash_hmac" => { - let (algo, data, key, binary) = match evaluated_args { - [algo, data, key] => (*algo, *data, *key, false), - [algo, data, key, binary] => (*algo, *data, *key, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let algo = values.string_bytes(algo)?; - let data = values.string_bytes(data)?; - let key = values.string_bytes(key)?; - eval_hash_hmac_result(&algo, &data, &key, binary, values) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Reads a local file and returns its PHP hash digest or false when it cannot be read. -fn eval_hash_file_result( - algo: RuntimeCellHandle, - filename: RuntimeCellHandle, - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let algo = values.string_bytes(algo)?; - let path = eval_path_string(filename, values)?; - match std::fs::read(path) { - Ok(data) => eval_hash_digest_result(&algo, &data, binary, values), - Err(_) => values.bool_value(false), - } -} - -/// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. -fn eval_hash_digest_result( - algo: &[u8], - data: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let raw = eval_crypto_hash(algo, data)?; - eval_format_digest_result(&raw, binary, values) -} - -/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. -fn eval_hash_hmac_result( - algo: &[u8], - data: &[u8], - key: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let raw = eval_crypto_hmac(algo, data, key)?; - eval_format_digest_result(&raw, binary, values) -} - -/// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. -fn eval_crypto_hash(algo: &[u8], data: &[u8]) -> Result, EvalStatus> { - let mut output = [0_u8; 64]; - let len = unsafe { - elephc_crypto::elephc_crypto_hash( - algo.as_ptr(), - algo.len(), - data.as_ptr(), - data.len(), - output.as_mut_ptr(), - ) - }; - eval_crypto_digest_bytes(len, &output) -} - -/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. -fn eval_crypto_hmac(algo: &[u8], data: &[u8], key: &[u8]) -> Result, EvalStatus> { - let mut output = [0_u8; 64]; - let len = unsafe { - elephc_crypto::elephc_crypto_hmac( - algo.as_ptr(), - algo.len(), - key.as_ptr(), - key.len(), - data.as_ptr(), - data.len(), - output.as_mut_ptr(), - ) - }; - eval_crypto_digest_bytes(len, &output) -} - -/// Converts a crypto ABI digest length into an owned digest byte vector. -fn eval_crypto_digest_bytes(len: isize, output: &[u8; 64]) -> Result, EvalStatus> { - let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; - if len > output.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(output[..len].to_vec()) -} - -/// Formats a raw digest using PHP's `$binary` flag convention. -fn eval_format_digest_result( - raw: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - if binary { - return values.string_bytes_value(raw); - } - values.string(&eval_lower_hex_bytes(raw)) -} - -/// Evaluates PHP `hash_algos()` with no arguments. -pub(super) fn eval_builtin_hash_algos( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values) -} - -/// Builds the indexed array returned by eval `hash_algos()`. -fn eval_hash_algos_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_HASH_ALGOS, values) -} - -/// Builds one indexed PHP array from a static string slice. -fn eval_static_string_array_result( - items: &[&str], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(items.len())?; - for (index, item) in items.iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string(item)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `spl_classes()` with no arguments. -pub(super) fn eval_builtin_spl_classes( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values) -} - -/// Builds the static class-name list returned by eval `spl_classes()`. -fn eval_spl_classes_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) -} - -/// Evaluates PHP stream introspection list builtins with no arguments. -pub(super) fn eval_builtin_stream_introspection( - name: &str, - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_introspection_result(name, values) -} - -/// Builds the static list returned by one eval stream introspection builtin. -fn eval_stream_introspection_result( - name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let items = match name { - "stream_get_filters" => EVAL_STREAM_FILTERS, - "stream_get_transports" => EVAL_STREAM_TRANSPORTS, - "stream_get_wrappers" => EVAL_STREAM_WRAPPERS, - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_static_string_array_result(items, values) -} - -/// Evaluates PHP `time()` with no arguments. -pub(super) fn eval_builtin_time( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values) -} - -/// Returns the current Unix timestamp as a boxed PHP integer. -fn eval_time_result(values: &mut impl RuntimeValueOps) -> Result { - values.int(eval_current_unix_timestamp()?) -} - -/// Returns the current Unix timestamp as an integer payload. -fn eval_current_unix_timestamp() -> Result { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| EvalStatus::RuntimeFatal)? - .as_secs(); - i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. -pub(super) fn eval_builtin_date( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [format] => { - let format = eval_expr(format, context, scope, values)?; - eval_date_result(format, None, values) - } - [format, timestamp] => { - let format = eval_expr(format, context, scope, values)?; - let timestamp = eval_expr(timestamp, context, scope, values)?; - eval_date_result(format, Some(timestamp), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. -fn eval_date_result( - format: RuntimeCellHandle, - timestamp: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let format = values.string_bytes(format)?; - let timestamp = match timestamp { - Some(timestamp) => eval_int_value(timestamp, values)?, - None => eval_current_unix_timestamp()?, - }; - let tm = eval_localtime(timestamp)?; - let output = eval_format_date_bytes(&format, &tm, timestamp)?; - values.string_bytes_value(&output) -} - -/// Converts one Unix timestamp to local broken-down time through libc. -fn eval_localtime(timestamp: i64) -> Result { - let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; - let mut tm = MaybeUninit::::uninit(); - let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; - if result.is_null() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(unsafe { tm.assume_init() }) -} - -/// Applies PHP `date()` tokens to one local broken-down timestamp. -fn eval_format_date_bytes( - format: &[u8], - tm: &libc::tm, - timestamp: i64, -) -> Result, EvalStatus> { - let mut output = Vec::new(); - let mut escaped = false; - for byte in format { - if escaped { - output.push(*byte); - escaped = false; - continue; - } - if *byte == b'\\' { - escaped = true; - continue; - } - eval_push_date_token(&mut output, *byte, tm, timestamp)?; - } - if escaped { - output.push(b'\\'); - } - Ok(output) -} - -/// Appends the expansion for one PHP `date()` token, or the token literal. -fn eval_push_date_token( - output: &mut Vec, - token: u8, - tm: &libc::tm, - timestamp: i64, -) -> Result<(), EvalStatus> { - match token { - b'Y' => eval_push_padded_number(output, i64::from(tm.tm_year) + 1900, 4), - b'm' => eval_push_padded_number(output, i64::from(tm.tm_mon) + 1, 2), - b'd' => eval_push_padded_number(output, i64::from(tm.tm_mday), 2), - b'H' => eval_push_padded_number(output, i64::from(tm.tm_hour), 2), - b'i' => eval_push_padded_number(output, i64::from(tm.tm_min), 2), - b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), - b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), - b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), - b'D' => output - .extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), - b'M' => { - output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) - } - b'N' => { - let weekday = tm.tm_wday; - let iso_weekday = if weekday == 0 { 7 } else { weekday }; - output.extend_from_slice(iso_weekday.to_string().as_bytes()); - } - b'j' => output.extend_from_slice(tm.tm_mday.to_string().as_bytes()), - b'n' => output.extend_from_slice((tm.tm_mon + 1).to_string().as_bytes()), - b'G' => output.extend_from_slice(tm.tm_hour.to_string().as_bytes()), - b'g' => { - let hour = tm.tm_hour % 12; - let hour = if hour == 0 { 12 } else { hour }; - output.extend_from_slice(hour.to_string().as_bytes()); - } - b'A' => output.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }), - b'a' => output.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }), - b'U' => output.extend_from_slice(timestamp.to_string().as_bytes()), - _ => output.push(token), - } - Ok(()) -} - -/// Returns a checked month index for PHP `date()` name tables. -fn eval_tm_month_index(tm: &libc::tm) -> Result { - let index = usize::try_from(tm.tm_mon).map_err(|_| EvalStatus::RuntimeFatal)?; - if index >= EVAL_MONTH_NAMES.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(index) -} - -/// Returns a checked weekday index for PHP `date()` name tables. -fn eval_tm_weekday_index(tm: &libc::tm) -> Result { - let index = usize::try_from(tm.tm_wday).map_err(|_| EvalStatus::RuntimeFatal)?; - if index >= EVAL_WEEKDAY_NAMES.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(index) -} - -/// Appends one zero-padded decimal value with the requested minimum width. -fn eval_push_padded_number(output: &mut Vec, value: i64, width: usize) { - output.extend_from_slice(format!("{value:0width$}").as_bytes()); -} - -/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. -pub(super) fn eval_builtin_mktime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hour, minute, second, month, day, year] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hour = eval_expr(hour, context, scope, values)?; - let minute = eval_expr(minute, context, scope, values)?; - let second = eval_expr(second, context, scope, values)?; - let month = eval_expr(month, context, scope, values)?; - let day = eval_expr(day, context, scope, values)?; - let year = eval_expr(year, context, scope, values)?; - eval_mktime_result(hour, minute, second, month, day, year, values) -} - -/// Converts PHP date components to a local Unix timestamp through libc `mktime`. -fn eval_mktime_result( - hour: RuntimeCellHandle, - minute: RuntimeCellHandle, - second: RuntimeCellHandle, - month: RuntimeCellHandle, - day: RuntimeCellHandle, - year: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let timestamp = eval_mktime_timestamp( - eval_int_cell_as_c_int(hour, values)?, - eval_int_cell_as_c_int(minute, values)?, - eval_int_cell_as_c_int(second, values)?, - eval_int_cell_as_c_int(month, values)?, - eval_int_cell_as_c_int(day, values)?, - eval_int_cell_as_c_int(year, values)?, - )?; - values.int(timestamp) -} - -/// Converts local date components into a Unix timestamp through libc `mktime`. -fn eval_mktime_timestamp( - hour: libc::c_int, - minute: libc::c_int, - second: libc::c_int, - month: libc::c_int, - day: libc::c_int, - year: libc::c_int, -) -> Result { - let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; - tm.tm_hour = hour; - tm.tm_min = minute; - tm.tm_sec = second; - tm.tm_mon = month - 1; - tm.tm_mday = day; - tm.tm_year = year - 1900; - tm.tm_isdst = -1; - let timestamp = unsafe { libc::mktime(&mut tm) }; - i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. -fn eval_int_cell_as_c_int( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP `strtotime(datetime)` for eval's supported date-string subset. -pub(super) fn eval_builtin_strtotime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [datetime] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let datetime = eval_expr(datetime, context, scope, values)?; - eval_strtotime_result(datetime, values) -} - -/// Parses one eval `strtotime()` input and boxes the resulting timestamp. -fn eval_strtotime_result( - datetime: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(datetime)?; - let timestamp = eval_strtotime_bytes(&bytes)?; - values.int(timestamp) -} - -/// Parses eval's supported `strtotime()` strings into local Unix timestamps. -fn eval_strtotime_bytes(bytes: &[u8]) -> Result { - let bytes = eval_trim_ascii_whitespace(bytes); - if bytes.eq_ignore_ascii_case(b"now") { - return eval_current_unix_timestamp(); - } - let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { - return Ok(-1); - }; - eval_mktime_timestamp(hour, minute, second, month, day, year) -} - -/// Trims ASCII whitespace from both ends of one byte slice. -fn eval_trim_ascii_whitespace(bytes: &[u8]) -> &[u8] { - let mut start = 0; - let mut end = bytes.len(); - while start < end && bytes[start].is_ascii_whitespace() { - start += 1; - } - while end > start && bytes[end - 1].is_ascii_whitespace() { - end -= 1; - } - &bytes[start..end] -} - -/// Parses fixed-width ISO date and datetime forms supported by eval `strtotime()`. -fn eval_parse_iso_datetime( - bytes: &[u8], -) -> Option<( - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, -)> { - if bytes.len() != 10 && bytes.len() != 16 && bytes.len() != 19 { - return None; - } - if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { - return None; - } - let year = eval_parse_fixed_digits(bytes, 0, 4)?; - let month = eval_parse_fixed_digits(bytes, 5, 2)?; - let day = eval_parse_fixed_digits(bytes, 8, 2)?; - let (hour, minute, second) = if bytes.len() == 10 { - (0, 0, 0) - } else { - if !matches!(bytes.get(10), Some(b' ') | Some(b'T') | Some(b't')) { - return None; - } - if bytes.get(13) != Some(&b':') { - return None; - } - let hour = eval_parse_fixed_digits(bytes, 11, 2)?; - let minute = eval_parse_fixed_digits(bytes, 14, 2)?; - let second = if bytes.len() == 19 { - if bytes.get(16) != Some(&b':') { - return None; - } - eval_parse_fixed_digits(bytes, 17, 2)? - } else { - 0 - }; - (hour, minute, second) - }; - if !(1..=12).contains(&month) - || !(1..=31).contains(&day) - || !(0..=23).contains(&hour) - || !(0..=59).contains(&minute) - || !(0..=59).contains(&second) - { - return None; - } - Some((year, month, day, hour, minute, second)) -} - -/// Parses a fixed-width decimal field as a libc-compatible integer. -fn eval_parse_fixed_digits(bytes: &[u8], start: usize, len: usize) -> Option { - let end = start.checked_add(len)?; - let field = bytes.get(start..end)?; - let mut value: libc::c_int = 0; - for byte in field { - if !byte.is_ascii_digit() { - return None; - } - value = value.checked_mul(10)?; - value = value.checked_add(libc::c_int::from(byte - b'0'))?; - } - Some(value) -} - -/// Evaluates PHP `microtime()` with an optional ignored argument. -pub(super) fn eval_builtin_microtime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_microtime_result(values), - [as_float] => { - let _ = eval_expr(as_float, context, scope, values)?; - eval_microtime_result(values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the current Unix timestamp with microsecond precision as a boxed float. -fn eval_microtime_result( - values: &mut impl RuntimeValueOps, -) -> Result { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_err(|_| EvalStatus::RuntimeFatal)?; - let seconds = timestamp.as_secs() as f64; - let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; - values.float(seconds + micros) -} - -/// Evaluates PHP `sleep($seconds)` over one eval expression. -pub(super) fn eval_builtin_sleep( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [seconds] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let seconds = eval_expr(seconds, context, scope, values)?; - eval_sleep_result(seconds, values) -} - -/// Sleeps for a non-negative number of seconds and returns PHP's remaining-seconds value. -fn eval_sleep_result( - seconds: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let seconds = eval_int_value(seconds, values)?; - let seconds = u64::try_from(seconds).map_err(|_| EvalStatus::RuntimeFatal)?; - std::thread::sleep(std::time::Duration::from_secs(seconds)); - values.int(0) -} - -/// Evaluates PHP `usleep($microseconds)` over one eval expression. -pub(super) fn eval_builtin_usleep( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [microseconds] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let microseconds = eval_expr(microseconds, context, scope, values)?; - eval_usleep_result(microseconds, values) -} - -/// Sleeps for a non-negative number of microseconds and returns PHP null. -fn eval_usleep_result( - microseconds: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let microseconds = eval_int_value(microseconds, values)?; - let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; - std::thread::sleep(std::time::Duration::from_micros(microseconds)); - values.null() -} - -/// Evaluates PHP `phpversion()` with no arguments. -pub(super) fn eval_builtin_phpversion( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_phpversion_result(values) -} - -/// Returns the root elephc package version as a boxed PHP string. -fn eval_phpversion_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.string(eval_compiler_php_version()) -} - -/// Reads the root package version from the workspace manifest used by native `phpversion()`. -pub(super) fn eval_compiler_php_version() -> &'static str { - let mut in_package = false; - for line in EVAL_ROOT_CARGO_TOML.lines() { - let line = line.trim(); - if line == "[package]" { - in_package = true; - continue; - } - if in_package && line.starts_with('[') { - break; - } - if in_package { - if let Some(value) = line.strip_prefix("version = ") { - return value.trim_matches('"'); - } - } - } - env!("CARGO_PKG_VERSION") -} - -/// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. -pub(super) fn eval_builtin_php_uname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_php_uname_result(None, values), - [mode] => { - let mode = eval_expr(mode, context, scope, values)?; - eval_php_uname_result(Some(mode), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Reads the local uname fields and formats the PHP `php_uname()` mode result. -fn eval_php_uname_result( - mode: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = match mode { - Some(mode) => { - let bytes = values.string_bytes(mode)?; - let [mode] = bytes.as_slice() else { - return Err(EvalStatus::RuntimeFatal); - }; - *mode - } - None => b'a', - }; - - let mut utsname = std::mem::MaybeUninit::::zeroed(); - let status = unsafe { - // libc writes all uname fields into the stack-owned utsname buffer. - libc::uname(utsname.as_mut_ptr()) - }; - if status != 0 { - return values.string(""); - } - let utsname = unsafe { - // `uname` succeeded, so libc initialized the full `utsname` structure. - utsname.assume_init() - }; - let sysname = eval_uname_field_bytes(&utsname.sysname); - let nodename = eval_uname_field_bytes(&utsname.nodename); - let release = eval_uname_field_bytes(&utsname.release); - let version = eval_uname_field_bytes(&utsname.version); - let machine = eval_uname_field_bytes(&utsname.machine); - - match mode { - b'a' => { - let mut output = Vec::new(); - for field in [&sysname, &nodename, &release, &version, &machine] { - if !output.is_empty() { - output.push(b' '); - } - output.extend_from_slice(field); - } - values.string_bytes_value(&output) - } - b's' => values.string_bytes_value(&sysname), - b'n' => values.string_bytes_value(&nodename), - b'r' => values.string_bytes_value(&release), - b'v' => values.string_bytes_value(&version), - b'm' => values.string_bytes_value(&machine), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. -fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { - let length = field - .iter() - .position(|byte| *byte == 0) - .unwrap_or(field.len()); - field[..length].iter().map(|byte| *byte as u8).collect() -} - -/// Evaluates PHP `getcwd()` with no arguments. -pub(super) fn eval_builtin_getcwd( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_getcwd_result(values) -} - -/// Returns the process current working directory as a boxed PHP string. -fn eval_getcwd_result(values: &mut impl RuntimeValueOps) -> Result { - let cwd = std::env::current_dir().map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(cwd.to_string_lossy().as_ref()) -} - -/// Evaluates one PHP filesystem predicate over an eval expression. -pub(super) fn eval_builtin_file_probe( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_probe_result(name, filename, values) -} - -/// Computes one local filesystem predicate and returns a PHP boolean. -fn eval_file_probe_result( - name: &str, - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let path = std::path::Path::new(&path); - let result = match name { - "file_exists" => path.exists(), - "is_dir" => path.is_dir(), - "is_executable" => eval_path_is_executable(path), - "is_file" => path.is_file(), - "is_link" => std::fs::symlink_metadata(path) - .map(|metadata| metadata.file_type().is_symlink()) - .unwrap_or(false), - "is_readable" => eval_path_is_readable(path), - "is_writable" | "is_writeable" => eval_path_is_writable(path), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(result) -} - -/// Evaluates one scalar PHP stat metadata builtin over an eval expression. -pub(super) fn eval_builtin_file_stat_scalar( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_stat_scalar_result(name, filename, values) -} - -/// Returns scalar stat metadata, using PHP false for failure where native elephc does. -fn eval_file_stat_scalar_result( - name: &str, - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let metadata = match std::fs::metadata(path) { - Ok(metadata) => metadata, - Err(_) if name == "filemtime" => return values.int(0), - Err(_) => return values.bool_value(false), - }; - match name { - "fileatime" => values.int(metadata.atime()), - "filectime" => values.int(metadata.ctime()), - "filegroup" => values.int(i64::from(metadata.gid())), - "fileinode" => { - values.int(i64::try_from(metadata.ino()).map_err(|_| EvalStatus::RuntimeFatal)?) - } - "filemtime" => values.int(metadata.mtime()), - "fileowner" => values.int(i64::from(metadata.uid())), - "fileperms" => values.int(i64::from(metadata.mode())), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `file_get_contents($filename)` over one eval expression. -pub(super) fn eval_builtin_file_get_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_get_contents_result(filename, values) -} - -/// Reads a local file into a PHP string, or returns false when it cannot be opened. -fn eval_file_get_contents_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - match std::fs::read(path) { - Ok(bytes) => values.string_bytes_value(&bytes), - Err(_) => { - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - values.bool_value(false) - } - } -} - -/// Evaluates PHP `file($filename)` over one eval expression. -pub(super) fn eval_builtin_file( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_result(filename, values) -} - -/// Reads one local file and returns an indexed array of line byte strings. -fn eval_file_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let bytes = match std::fs::read(path) { - Ok(bytes) => bytes, - Err(_) => { - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - return values.array_new(0); - } - }; - eval_file_lines_array(&bytes, values) -} - -/// Splits file payload bytes into runtime array entries, preserving trailing newlines. -fn eval_file_lines_array( - bytes: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(0)?; - let mut line_start = 0; - let mut line_index = 0; - for (index, byte) in bytes.iter().enumerate() { - if *byte != b'\n' { - continue; - } - result = - eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; - line_start = index + 1; - line_index += 1; - } - if line_start < bytes.len() { - result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; - } - Ok(result) -} - -/// Evaluates PHP `readfile($filename)` over one eval expression. -pub(super) fn eval_builtin_readfile( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_readfile_result(filename, values) -} - -/// Streams one local file to eval output and returns a byte count, false, or -1. -fn eval_readfile_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let path = std::path::Path::new(&path); - if path.is_dir() { - return values.int(-1); - } - let bytes = match std::fs::read(path) { - Ok(bytes) => bytes, - Err(_) => return values.bool_value(false), - }; - let output = values.string_bytes_value(&bytes)?; - values.echo(output)?; - values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. -pub(super) fn eval_builtin_file_put_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, data] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let data = eval_expr(data, context, scope, values)?; - eval_file_put_contents_result(filename, data, values) -} - -/// Writes a PHP string to a local file and returns the written byte count or false. -fn eval_file_put_contents_result( - filename: RuntimeCellHandle, - data: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let data = values.string_bytes(data)?; - match std::fs::write(path, &data) { - Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), - Err(_) => values.bool_value(false), - } -} - -/// Evaluates PHP `filesize($filename)` over one eval expression. -pub(super) fn eval_builtin_filesize( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_filesize_result(filename, values) -} - -/// Returns one local file size in bytes, or zero when stat fails. -fn eval_filesize_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let len = std::fs::metadata(path) - .map(|metadata| metadata.len()) - .unwrap_or(0); - values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `filetype($filename)` over one eval expression. -pub(super) fn eval_builtin_filetype( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_filetype_result(filename, values) -} - -/// Returns the PHP filetype string for one path, or false when lstat fails. -fn eval_filetype_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let file_type = match std::fs::symlink_metadata(path) { - Ok(metadata) => metadata.file_type(), - Err(_) => return values.bool_value(false), - }; - let label = if file_type.is_file() { - "file" - } else if file_type.is_dir() { - "dir" - } else if file_type.is_symlink() { - "link" - } else if file_type.is_char_device() { - "char" - } else if file_type.is_block_device() { - "block" - } else if file_type.is_fifo() { - "fifo" - } else if file_type.is_socket() { - "socket" - } else { - "unknown" - }; - values.string(label) -} - -/// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. -pub(super) fn eval_builtin_stat_array( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_stat_array_result(name, filename, values) -} - -/// Builds PHP's stat array for one local path, or returns false on stat failure. -fn eval_stat_array_result( - name: &str, - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let metadata = match name { - "stat" => std::fs::metadata(path), - "lstat" => std::fs::symlink_metadata(path), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let metadata = match metadata { - Ok(metadata) => metadata, - Err(_) => return values.bool_value(false), - }; - eval_stat_metadata_array(&metadata, values) -} - -/// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. -fn eval_stat_metadata_array( - metadata: &std::fs::Metadata, - values: &mut impl RuntimeValueOps, -) -> Result { - let fields = [ - ("dev", eval_u64_to_i64(metadata.dev())?), - ("ino", eval_u64_to_i64(metadata.ino())?), - ("mode", i64::from(metadata.mode())), - ("nlink", eval_u64_to_i64(metadata.nlink())?), - ("uid", i64::from(metadata.uid())), - ("gid", i64::from(metadata.gid())), - ("rdev", eval_u64_to_i64(metadata.rdev())?), - ("size", eval_u64_to_i64(metadata.size())?), - ("atime", metadata.atime()), - ("mtime", metadata.mtime()), - ("ctime", metadata.ctime()), - ("blksize", eval_u64_to_i64(metadata.blksize())?), - ("blocks", eval_u64_to_i64(metadata.blocks())?), - ]; - let mut result = values.assoc_new(fields.len() * 2)?; - for (index, (name, value)) in fields.iter().enumerate() { - result = eval_stat_array_set_int_key(result, index, *value, values)?; - result = eval_stat_array_set_string_key(result, name, *value, values)?; - } - Ok(result) -} - -/// Inserts one integer stat field under a numeric PHP array key. -fn eval_stat_array_set_int_key( - array: RuntimeCellHandle, - key: usize, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(i64::try_from(key).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Inserts one integer stat field under a string PHP array key. -fn eval_stat_array_set_string_key( - array: RuntimeCellHandle, - key: &str, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Converts unsigned stat metadata into the signed integer payload used by PHP cells. -fn eval_u64_to_i64(value: u64) -> Result { - i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. -pub(super) fn eval_builtin_disk_space( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_disk_space_result(name, directory, values) -} - -/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. -fn eval_disk_space_result( - name: &str, - directory: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(directory)?; - let Ok(path) = CString::new(bytes) else { - return values.float(0.0); - }; - let mut stats = std::mem::MaybeUninit::::zeroed(); - let status = unsafe { - // libc writes the statvfs fields for this NUL-terminated local path. - libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) - }; - if status != 0 { - return values.float(0.0); - } - let stats = unsafe { - // `statvfs` succeeded, so libc initialized the full stat buffer. - stats.assume_init() - }; - let block_size = if stats.f_frsize > 0 { - stats.f_frsize - } else { - stats.f_bsize - }; - let blocks = match name { - "disk_free_space" => stats.f_bavail, - "disk_total_space" => stats.f_blocks, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.float((block_size as f64) * (blocks as f64)) -} - -/// Evaluates a one-path filesystem operation that returns a PHP boolean. -pub(super) fn eval_builtin_unary_path_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_unary_path_bool_result(name, path, values) -} - -/// Executes a one-path local filesystem operation and returns whether it succeeded. -fn eval_unary_path_bool_result( - name: &str, - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - let ok = match name { - "chdir" => std::env::set_current_dir(path).is_ok(), - "mkdir" => std::fs::create_dir(path).is_ok(), - "rmdir" => std::fs::remove_dir(path).is_ok(), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Evaluates a two-path filesystem operation that returns a PHP boolean. -pub(super) fn eval_builtin_binary_path_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [from, to] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let from = eval_expr(from, context, scope, values)?; - let to = eval_expr(to, context, scope, values)?; - eval_binary_path_bool_result(name, from, to, values) -} - -/// Executes a two-path local filesystem operation and returns whether it succeeded. -fn eval_binary_path_bool_result( - name: &str, - from: RuntimeCellHandle, - to: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let from = eval_path_string(from, values)?; - let to = eval_path_string(to, values)?; - let ok = match name { - "copy" => std::fs::copy(from, to).is_ok(), - "link" => std::fs::hard_link(from, to).is_ok(), - "rename" => std::fs::rename(from, to).is_ok(), - "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. -pub(super) fn eval_builtin_chmod( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, permissions] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let permissions = eval_expr(permissions, context, scope, values)?; - eval_chmod_result(filename, permissions, values) -} - -/// Changes one local file's mode and returns whether the operation succeeded. -fn eval_chmod_result( - filename: RuntimeCellHandle, - permissions: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let mode = eval_int_value(permissions, values)? as u32; - let permissions = std::fs::Permissions::from_mode(mode); - values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) -} - -/// Evaluates PHP `scandir($directory)` over one eval expression. -pub(super) fn eval_builtin_scandir( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_scandir_result(directory, values) -} - -/// Lists one local directory into an indexed string array, or an empty array on failure. -fn eval_scandir_result( - directory: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(directory, values)?; - let Ok(entries) = std::fs::read_dir(path) else { - return values.array_new(0); - }; - let mut names = vec![".".to_string(), "..".to_string()]; - for entry in entries { - let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; - names.push(entry.file_name().to_string_lossy().into_owned()); - } - names.sort(); - let mut result = values.array_new(names.len())?; - for (index, name) in names.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; - } - Ok(result) -} - -/// Evaluates PHP `glob($pattern)` over one eval expression. -pub(super) fn eval_builtin_glob( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - eval_glob_result(pattern, values) -} - -/// Expands one local glob pattern into a sorted indexed PHP string array. -fn eval_glob_result( - pattern: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = eval_path_string(pattern, values)?; - let matches = eval_glob_matches(&pattern); - let mut result = values.array_new(matches.len())?; - for (index, path) in matches.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; - } - Ok(result) -} - -/// Collects sorted matches for one local glob pattern. -fn eval_glob_matches(pattern: &str) -> Vec { - if pattern.is_empty() { - return Vec::new(); - } - if !eval_glob_component_has_magic(pattern) { - return std::path::Path::new(pattern) - .exists() - .then(|| pattern.to_string()) - .into_iter() - .collect(); - } - let absolute = pattern.starts_with('/'); - let components: Vec<&str> = pattern - .split('/') - .filter(|component| !component.is_empty()) - .collect(); - let mut matches = Vec::new(); - let base = if absolute { - std::path::PathBuf::from("/") - } else { - std::path::PathBuf::from(".") - }; - let prefix = if absolute { "/" } else { "" }; - eval_glob_collect(&base, prefix, &components, &mut matches); - matches.sort(); - matches -} - -/// Recursively expands one glob path component at a time. -fn eval_glob_collect( - base: &std::path::Path, - prefix: &str, - components: &[&str], - matches: &mut Vec, -) { - let Some((component, rest)) = components.split_first() else { - if base.exists() && !prefix.is_empty() { - matches.push(prefix.to_string()); - } - return; - }; - if !eval_glob_component_has_magic(component) { - let next_base = base.join(component); - if rest.is_empty() { - if next_base.exists() { - matches.push(eval_glob_join_output(prefix, component)); - } - } else if next_base.is_dir() { - let next_prefix = eval_glob_join_output(prefix, component); - eval_glob_collect(&next_base, &next_prefix, rest, matches); - } - return; - } - let Ok(entries) = std::fs::read_dir(base) else { - return; - }; - let mut names = Vec::new(); - for entry in entries.flatten() { - names.push(entry.file_name().to_string_lossy().into_owned()); - } - names.sort(); - for name in names { - if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { - continue; - } - let next_base = base.join(&name); - if rest.is_empty() { - matches.push(eval_glob_join_output(prefix, &name)); - } else if next_base.is_dir() { - let next_prefix = eval_glob_join_output(prefix, &name); - eval_glob_collect(&next_base, &next_prefix, rest, matches); - } - } -} - -/// Joins a display path prefix and component while preserving absolute-root output. -fn eval_glob_join_output(prefix: &str, component: &str) -> String { - if prefix.is_empty() { - component.to_string() - } else if prefix == "/" { - format!("/{component}") - } else { - format!("{prefix}/{component}") - } -} - -/// Returns whether a glob component contains wildcard syntax. -fn eval_glob_component_has_magic(component: &str) -> bool { - component - .as_bytes() - .iter() - .any(|byte| matches!(byte, b'*' | b'?' | b'[')) -} - -/// Writes one byte-string value into an indexed runtime array at a zero-based position. -fn eval_array_set_indexed_bytes( - array: RuntimeCellHandle, - index: usize, - value: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.string_bytes_value(value)?; - values.array_set(array, key, value) -} - -/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. -pub(super) fn eval_builtin_tempnam( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory, prefix] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - let prefix = eval_expr(prefix, context, scope, values)?; - eval_tempnam_result(directory, prefix, values) -} - -/// Creates a unique local temporary file and returns its path, or an empty string on failure. -fn eval_tempnam_result( - directory: RuntimeCellHandle, - prefix: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let directory = eval_path_string(directory, values)?; - let prefix = values.string_bytes(prefix)?; - let prefix = String::from_utf8_lossy(&prefix); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - for attempt in 0..1000_u32 { - let candidate = - std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); - match std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&candidate) - { - Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), - Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, - Err(_) => return values.string(""), - } - } - values.string("") -} - -/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. -fn eval_tempnam_filename(prefix: &str, nonce: u128, attempt: u32) -> String { - format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) -} - -/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. -pub(super) fn eval_builtin_touch( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [filename] => { - let filename = eval_expr(filename, context, scope, values)?; - eval_touch_result(filename, None, None, values) - } - [filename, mtime] => { - let filename = eval_expr(filename, context, scope, values)?; - let mtime = eval_expr(mtime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), None, values) - } - [filename, mtime, atime] => { - let filename = eval_expr(filename, context, scope, values)?; - let mtime = eval_expr(mtime, context, scope, values)?; - let atime = eval_expr(atime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), Some(atime), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Creates or stamps one local file and returns whether the operation succeeded. -fn eval_touch_result( - filename: RuntimeCellHandle, - mtime: Option, - atime: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let (mtime, atime) = eval_touch_times(mtime, atime, values)?; - let file = match std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(path) - { - Ok(file) => file, - Err(_) => return values.bool_value(false), - }; - let times = std::fs::FileTimes::new() - .set_modified(mtime) - .set_accessed(atime); - values.bool_value(file.set_times(times).is_ok()) -} - -/// Resolves PHP touch timestamp defaults into concrete system times. -fn eval_touch_times( - mtime: Option, - atime: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { - let now = std::time::SystemTime::now(); - let Some(mtime) = mtime else { - return Ok((now, now)); - }; - if values.is_null(mtime)? { - if let Some(atime) = atime { - if !values.is_null(atime)? { - return Err(EvalStatus::RuntimeFatal); - } - } - return Ok((now, now)); - } - let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) - .ok_or(EvalStatus::RuntimeFatal)?; - let Some(atime) = atime else { - return Ok((mtime, mtime)); - }; - if values.is_null(atime)? { - return Ok((mtime, mtime)); - } - let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) - .ok_or(EvalStatus::RuntimeFatal)?; - Ok((mtime, atime)) -} - -/// Converts a Unix timestamp in seconds into a `SystemTime`. -fn eval_system_time_from_unix(seconds: i64) -> Option { - if seconds >= 0 { - std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) - } else { - std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) - } -} - -/// Evaluates PHP `umask($mask = null)` over an optional eval expression. -pub(super) fn eval_builtin_umask( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_umask_result(None, values), - [mask] => { - let mask = eval_expr(mask, context, scope, values)?; - eval_umask_result(Some(mask), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Applies PHP `umask()` semantics and returns the previous mask. -fn eval_umask_result( - mask: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let previous = match mask { - Some(mask) => { - let mask = eval_int_value(mask, values)? as u32; - unsafe { umask(mask) } - } - None => unsafe { - let current = umask(0); - umask(current); - current - }, - }; - values.int(i64::from(previous)) -} - -/// Evaluates PHP `readlink($path)` over one eval expression. -pub(super) fn eval_builtin_readlink( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_readlink_result(path, values) -} - -/// Reads one symbolic-link target string, or returns PHP false on failure. -fn eval_readlink_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - match std::fs::read_link(path) { - Ok(target) => values.string(target.to_string_lossy().as_ref()), - Err(_) => values.bool_value(false), - } -} - -/// Evaluates PHP `linkinfo($path)` over one eval expression. -pub(super) fn eval_builtin_linkinfo( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_linkinfo_result(path, values) -} - -/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. -fn eval_linkinfo_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - let dev = match std::fs::symlink_metadata(path) { - Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, - Err(_) => -1, - }; - values.int(dev) -} - -/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. -pub(super) fn eval_builtin_clearstatcache( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - eval_expr(arg, context, scope, values)?; - } - values.null() -} - -/// Evaluates PHP `unlink($filename)` over one eval expression. -pub(super) fn eval_builtin_unlink( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_unlink_result(filename, values) -} - -/// Deletes one local file and returns whether it succeeded. -fn eval_unlink_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - values.bool_value(std::fs::remove_file(path).is_ok()) -} - -/// Converts one eval value to a filesystem path string. -pub(super) fn eval_path_string( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let filename = values.string_bytes(filename)?; - Ok(String::from_utf8_lossy(&filename).into_owned()) -} - -/// Returns whether a path can be opened for reading by the current process. -fn eval_path_is_readable(path: &std::path::Path) -> bool { - std::fs::File::open(path).is_ok() || std::fs::read_dir(path).is_ok() -} - -/// Returns whether a path has any executable bit set in its Unix mode. -fn eval_path_is_executable(path: &std::path::Path) -> bool { - std::fs::metadata(path) - .map(|metadata| metadata.mode() & 0o111 != 0) - .unwrap_or(false) -} - -/// Returns whether a path can be written by the current process. -fn eval_path_is_writable(path: &std::path::Path) -> bool { - if path.is_file() { - return std::fs::OpenOptions::new().write(true).open(path).is_ok(); - } - if !path.is_dir() { - return false; - } - let probe = path.join(format!( - ".elephc_eval_writable_probe_{}", - std::process::id() - )); - match std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&probe) - { - Ok(_) => { - let _ = std::fs::remove_file(probe); - true - } - Err(_) => false, - } -} - -/// Evaluates PHP `basename($path, $suffix = "")` over one eval expression. -pub(super) fn eval_builtin_basename( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_basename_result(path, None, values) - } - [path, suffix] => { - let path = eval_expr(path, context, scope, values)?; - let suffix = eval_expr(suffix, context, scope, values)?; - eval_basename_result(path, Some(suffix), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `basename()` bytes and returns them as a runtime string. -fn eval_basename_result( - path: RuntimeCellHandle, - suffix: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let suffix = suffix - .map(|suffix| values.string_bytes(suffix)) - .transpose()?; - let result = eval_basename_bytes(&path, suffix.as_deref()); - values.string_bytes_value(&result) -} - -/// Extracts a PHP basename from one path byte string. -fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { - let mut end = path.len(); - while end > 0 && path[end - 1] == b'/' { - end -= 1; - } - if end == 0 { - return Vec::new(); - } - let mut start = end; - while start > 0 && path[start - 1] != b'/' { - start -= 1; - } - let mut result = path[start..end].to_vec(); - if let Some(suffix) = suffix { - if !suffix.is_empty() && suffix.len() < result.len() && result.ends_with(suffix) { - result.truncate(result.len() - suffix.len()); - } - } - result -} - -/// Evaluates PHP `dirname($path, $levels = 1)` over one eval expression. -pub(super) fn eval_builtin_dirname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_dirname_result(path, None, values) - } - [path, levels] => { - let path = eval_expr(path, context, scope, values)?; - let levels = eval_expr(levels, context, scope, values)?; - eval_dirname_result(path, Some(levels), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `dirname()` bytes and returns them as a runtime string. -fn eval_dirname_result( - path: RuntimeCellHandle, - levels: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let levels = match levels { - Some(levels) => eval_int_value(levels, values)?, - None => 1, - }; - if levels < 1 { - return Err(EvalStatus::RuntimeFatal); - } - let mut current = path; - for _ in 0..levels { - current = eval_dirname_once(¤t); - } - values.string_bytes_value(¤t) -} - -/// Applies one PHP `dirname()` parent traversal to a path byte string. -fn eval_dirname_once(path: &[u8]) -> Vec { - if path.is_empty() { - return b".".to_vec(); - } - let mut end = path.len(); - while end > 0 && path[end - 1] == b'/' { - end -= 1; - } - if end == 0 { - return b"/".to_vec(); - } - let mut cursor = end; - while cursor > 0 { - cursor -= 1; - if path[cursor] == b'/' { - let mut parent_end = cursor; - while parent_end > 0 && path[parent_end - 1] == b'/' { - parent_end -= 1; - } - return if parent_end == 0 { - b"/".to_vec() - } else { - path[..parent_end].to_vec() - }; - } - } - b".".to_vec() -} - -/// Evaluates PHP `realpath($path)` over one eval expression. -pub(super) fn eval_builtin_realpath( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_realpath_result(path, values) -} - -/// Canonicalizes one path or returns PHP false when the path cannot be resolved. -fn eval_realpath_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let path = String::from_utf8_lossy(&path); - let Ok(canonical) = std::fs::canonicalize(path.as_ref()) else { - return values.bool_value(false); - }; - let canonical = canonical.to_string_lossy(); - values.string(canonical.as_ref()) -} - -/// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. -pub(super) fn eval_builtin_pathinfo( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_pathinfo_result(path, None, values) - } - [path, flags] => { - let path = eval_expr(path, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_pathinfo_result(path, Some(flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `pathinfo()` as either an associative array or one component string. -fn eval_pathinfo_result( - path: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let Some(flags) = flags else { - return eval_pathinfo_array_result(&path, values); - }; - let flags = eval_int_value(flags, values)?; - if flags == EVAL_PATHINFO_ALL { - return eval_pathinfo_array_result(&path, values); - } - let component = eval_pathinfo_component_bytes(&path, flags); - values.string_bytes_value(&component) -} - -/// Builds the PHP `pathinfo()` associative-array result for all components. -fn eval_pathinfo_array_result( - path: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(4)?; - if !path.is_empty() { - let dirname = eval_pathinfo_dirname_bytes(path); - result = eval_pathinfo_array_set(result, "dirname", &dirname, values)?; - } - let parts = eval_pathinfo_parts(path); - result = eval_pathinfo_array_set(result, "basename", &parts.basename, values)?; - if parts.has_extension { - result = eval_pathinfo_array_set(result, "extension", &parts.extension, values)?; - } - eval_pathinfo_array_set(result, "filename", &parts.filename, values) -} - -/// Inserts one string component into a PHP `pathinfo()` associative result. -fn eval_pathinfo_array_set( - array: RuntimeCellHandle, - key: &str, - value: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.string_bytes_value(value)?; - values.array_set(array, key, value) -} - -/// Returns one PHP `pathinfo()` component for a non-all bitmask. -fn eval_pathinfo_component_bytes(path: &[u8], flags: i64) -> Vec { - if flags & EVAL_PATHINFO_DIRNAME != 0 { - return eval_pathinfo_dirname_bytes(path); - } - let parts = eval_pathinfo_parts(path); - if flags & EVAL_PATHINFO_BASENAME != 0 { - return parts.basename; - } - if flags & EVAL_PATHINFO_EXTENSION != 0 { - return parts.extension; - } - if flags & EVAL_PATHINFO_FILENAME != 0 { - return parts.filename; - } - Vec::new() -} - -/// Computes the dirname component with `pathinfo("")`'s empty-string exception. -fn eval_pathinfo_dirname_bytes(path: &[u8]) -> Vec { - if path.is_empty() { - Vec::new() - } else { - eval_dirname_once(path) - } -} - -/// Splits pathinfo basename, extension, and filename components. -fn eval_pathinfo_parts(path: &[u8]) -> EvalPathInfoParts { - let basename = eval_basename_bytes(path, None); - let Some(dot) = basename.iter().rposition(|byte| *byte == b'.') else { - return EvalPathInfoParts { - filename: basename.clone(), - basename, - extension: Vec::new(), - has_extension: false, - }; - }; - EvalPathInfoParts { - filename: basename[..dot].to_vec(), - extension: basename[dot + 1..].to_vec(), - basename, - has_extension: true, - } -} - -/// Pathinfo components derived from a basename. -struct EvalPathInfoParts { - /// Full basename component. - basename: Vec, - /// Extension component after the final dot, possibly empty for trailing-dot names. - extension: Vec, - /// Filename component before the final dot. - filename: Vec, - /// Whether the basename contained a dot and therefore has an extension key. - has_extension: bool, -} - -/// Evaluates PHP `fnmatch($pattern, $filename, $flags = 0)` over eval expressions. -pub(super) fn eval_builtin_fnmatch( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, filename] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let filename = eval_expr(filename, context, scope, values)?; - eval_fnmatch_result(pattern, filename, None, values) - } - [pattern, filename, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let filename = eval_expr(filename, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_fnmatch_result(pattern, filename, Some(flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Runs PHP-style shell glob matching for one pattern/name pair. -fn eval_fnmatch_result( - pattern: RuntimeCellHandle, - filename: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = values.string_bytes(pattern)?; - let filename = values.string_bytes(filename)?; - let flags = match flags { - Some(flags) => eval_int_value(flags, values)?, - None => 0, - }; - values.bool_value(eval_fnmatch_bytes(&pattern, &filename, flags)) -} - -/// Matches byte strings using the eval-supported `fnmatch()` grammar and flags. -fn eval_fnmatch_bytes(pattern: &[u8], filename: &[u8], flags: i64) -> bool { - let mut memo = vec![vec![None; filename.len() + 1]; pattern.len() + 1]; - eval_fnmatch_at(pattern, filename, flags, 0, 0, &mut memo) -} - -/// Recursively matches a pattern suffix against a filename suffix with memoization. -fn eval_fnmatch_at( - pattern: &[u8], - filename: &[u8], - flags: i64, - pattern_index: usize, - filename_index: usize, - memo: &mut [Vec>], -) -> bool { - if let Some(result) = memo[pattern_index][filename_index] { - return result; - } - let result = if pattern_index == pattern.len() { - filename_index == filename.len() - } else { - match pattern[pattern_index] { - b'*' => eval_fnmatch_star( - pattern, - filename, - flags, - pattern_index, - filename_index, - memo, - ), - b'?' => { - eval_fnmatch_single_wildcard(filename, flags, filename_index) - && eval_fnmatch_at( - pattern, - filename, - flags, - pattern_index + 1, - filename_index + 1, - memo, - ) - } - b'[' => eval_fnmatch_class_or_literal( - pattern, - filename, - flags, - pattern_index, - filename_index, - memo, - ), - b'\\' if flags & EVAL_FNM_NOESCAPE == 0 => { - let (literal, next_pattern_index) = - eval_fnmatch_escaped_literal(pattern, pattern_index); - eval_fnmatch_literal(filename, flags, filename_index, literal) - && eval_fnmatch_at( - pattern, - filename, - flags, - next_pattern_index, - filename_index + 1, - memo, - ) - } - literal => { - eval_fnmatch_literal(filename, flags, filename_index, literal) - && eval_fnmatch_at( - pattern, - filename, - flags, - pattern_index + 1, - filename_index + 1, - memo, - ) - } - } - }; - memo[pattern_index][filename_index] = Some(result); - result -} - -/// Handles `*`, including pathname and leading-period restrictions. -fn eval_fnmatch_star( - pattern: &[u8], - filename: &[u8], - flags: i64, - pattern_index: usize, - filename_index: usize, - memo: &mut [Vec>], -) -> bool { - let mut next_pattern_index = pattern_index + 1; - while next_pattern_index < pattern.len() && pattern[next_pattern_index] == b'*' { - next_pattern_index += 1; - } - if eval_fnmatch_at( - pattern, - filename, - flags, - next_pattern_index, - filename_index, - memo, - ) { - return true; - } - let mut cursor = filename_index; - while cursor < filename.len() && eval_fnmatch_wildcard_can_consume(filename, flags, cursor) { - cursor += 1; - if eval_fnmatch_at(pattern, filename, flags, next_pattern_index, cursor, memo) { - return true; - } - } - false -} - -/// Returns whether `?` can consume the current filename byte. -fn eval_fnmatch_single_wildcard(filename: &[u8], flags: i64, filename_index: usize) -> bool { - filename_index < filename.len() - && eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) -} - -/// Handles a bracket class, or falls back to a literal `[` when the class is malformed. -fn eval_fnmatch_class_or_literal( - pattern: &[u8], - filename: &[u8], - flags: i64, - pattern_index: usize, - filename_index: usize, - memo: &mut [Vec>], -) -> bool { - if filename_index >= filename.len() - || !eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) - { - return false; - } - let Some((matches, next_pattern_index)) = - eval_fnmatch_class_matches(pattern, pattern_index + 1, filename[filename_index], flags) - else { - return eval_fnmatch_literal(filename, flags, filename_index, b'[') - && eval_fnmatch_at( - pattern, - filename, - flags, - pattern_index + 1, - filename_index + 1, - memo, - ); - }; - matches - && eval_fnmatch_at( - pattern, - filename, - flags, - next_pattern_index, - filename_index + 1, - memo, - ) -} - -/// Matches one bracket class body against the current filename byte. -fn eval_fnmatch_class_matches( - pattern: &[u8], - mut index: usize, - candidate: u8, - flags: i64, -) -> Option<(bool, usize)> { - let negated = matches!(pattern.get(index).copied(), Some(b'!' | b'^')); - if negated { - index += 1; - } - let mut matched = false; - let mut closed = false; - while index < pattern.len() { - if pattern[index] == b']' { - closed = true; - index += 1; - break; - } - let start = eval_fnmatch_class_char(pattern, &mut index, flags)?; - if index + 1 < pattern.len() && pattern[index] == b'-' && pattern[index + 1] != b']' { - index += 1; - let end = eval_fnmatch_class_char(pattern, &mut index, flags)?; - if eval_fnmatch_byte_in_range(candidate, start, end, flags) { - matched = true; - } - } else if eval_fnmatch_byte_eq(candidate, start, flags) { - matched = true; - } - } - closed.then_some((if negated { !matched } else { matched }, index)) -} - -/// Reads one character from a bracket class, respecting escapes when enabled. -fn eval_fnmatch_class_char(pattern: &[u8], index: &mut usize, flags: i64) -> Option { - if *index >= pattern.len() { - return None; - } - if pattern[*index] == b'\\' && flags & EVAL_FNM_NOESCAPE == 0 && *index + 1 < pattern.len() { - *index += 2; - return Some(pattern[*index - 1]); - } - let byte = pattern[*index]; - *index += 1; - Some(byte) -} - -/// Returns whether one candidate byte falls within a possibly case-folded range. -fn eval_fnmatch_byte_in_range(candidate: u8, start: u8, end: u8, flags: i64) -> bool { - let candidate = eval_fnmatch_fold(candidate, flags); - let start = eval_fnmatch_fold(start, flags); - let end = eval_fnmatch_fold(end, flags); - if start <= end { - candidate >= start && candidate <= end - } else { - candidate >= end && candidate <= start - } -} - -/// Reads an escaped literal token outside bracket classes. -fn eval_fnmatch_escaped_literal(pattern: &[u8], pattern_index: usize) -> (u8, usize) { - if pattern_index + 1 < pattern.len() { - (pattern[pattern_index + 1], pattern_index + 2) - } else { - (b'\\', pattern_index + 1) - } -} - -/// Returns whether one literal pattern byte matches the current filename byte. -fn eval_fnmatch_literal(filename: &[u8], flags: i64, filename_index: usize, literal: u8) -> bool { - filename_index < filename.len() - && eval_fnmatch_byte_eq(filename[filename_index], literal, flags) -} - -/// Returns whether a wildcard token may consume the current filename byte. -fn eval_fnmatch_wildcard_can_consume(filename: &[u8], flags: i64, filename_index: usize) -> bool { - if filename_index >= filename.len() { - return false; - } - if flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index] == b'/' { - return false; - } - if flags & EVAL_FNM_PERIOD != 0 - && eval_fnmatch_is_leading_period(filename, flags, filename_index) - { - return false; - } - true -} - -/// Returns whether the current byte is a leading period for `FNM_PERIOD`. -fn eval_fnmatch_is_leading_period(filename: &[u8], flags: i64, filename_index: usize) -> bool { - filename[filename_index] == b'.' - && (filename_index == 0 - || (flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index - 1] == b'/')) -} - -/// Compares bytes using ASCII case folding when `FNM_CASEFOLD` is present. -fn eval_fnmatch_byte_eq(left: u8, right: u8, flags: i64) -> bool { - eval_fnmatch_fold(left, flags) == eval_fnmatch_fold(right, flags) -} - -/// Applies eval fnmatch's ASCII case folding. -fn eval_fnmatch_fold(byte: u8, flags: i64) -> u8 { - if flags & EVAL_FNM_CASEFOLD != 0 { - byte.to_ascii_lowercase() - } else { - byte - } -} - -/// Evaluates PHP `preg_match()` over eval expressions. -pub(super) fn eval_builtin_preg_match( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_match_result(pattern, subject, values) - } - [pattern, subject, matches] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let (result, matches_array) = - eval_preg_match_capture_result(pattern, subject, None, values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - [pattern, subject, matches, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let flags = eval_expr(flags, context, scope, values)?; - let (result, matches_array) = - eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns whether one regex matches the subject string. -fn eval_preg_match_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - values.int(i64::from(regex.is_match(&subject))) -} - -/// Returns the match flag plus PHP `$matches` capture array for one regex search. -fn eval_preg_match_capture_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let flags = eval_preg_match_flags(flags, values)?; - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - if let Some(captures) = regex.captures(&subject) { - let matches = eval_preg_capture_array( - &subject, - Some(&captures), - offset_capture, - unmatched_as_null, - values, - )?; - let matched = values.int(1)?; - return Ok((matched, matches)); - } - let matches = - eval_preg_capture_array(&subject, None, offset_capture, unmatched_as_null, values)?; - let matched = values.int(0)?; - Ok((matched, matches)) -} - -/// Returns supported `preg_match()` flags. -fn eval_preg_match_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(0); - }; - let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_OFFSET_CAPTURE | EVAL_PREG_UNMATCHED_AS_NULL; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Evaluates PHP `preg_match_all()` over eval expressions. -pub(super) fn eval_builtin_preg_match_all( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_match_all_result(pattern, subject, values) - } - [pattern, subject, matches] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let (result, matches_array) = - eval_preg_match_all_capture_result(pattern, subject, None, values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - [pattern, subject, matches, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let flags = eval_expr(flags, context, scope, values)?; - let (result, matches_array) = - eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Counts all non-overlapping regex matches in one subject string. -fn eval_preg_match_all_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let count = regex.captures_iter(&subject).count(); - values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Returns the match count plus PHP's default `PREG_PATTERN_ORDER` `$matches` array. -fn eval_preg_match_all_capture_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let regex = eval_preg_regex(pattern, values)?; - let capture_count = regex.captures_len(); - let subject = values.string_bytes(subject)?; - let captures: Vec> = regex.captures_iter(&subject).collect(); - let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let flags = eval_preg_match_all_flags(flags, values)?; - let matches = if flags & EVAL_PREG_SET_ORDER != 0 { - eval_preg_match_all_set_order_array(&subject, &captures, capture_count, flags, values)? - } else { - eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, flags, values)? - }; - Ok((count, matches)) -} - -/// Returns supported `preg_match_all()` flags. -fn eval_preg_match_all_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(EVAL_PREG_PATTERN_ORDER); - }; - let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_PATTERN_ORDER - | EVAL_PREG_SET_ORDER - | EVAL_PREG_OFFSET_CAPTURE - | EVAL_PREG_UNMATCHED_AS_NULL; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Builds PHP's default `preg_match_all()` pattern-order capture matrix. -fn eval_preg_match_all_pattern_order_array( - subject: &[u8], - captures: &[Captures<'_>], - capture_count: usize, - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - let mut outer = values.array_new(capture_count)?; - for capture_index in 0..capture_count { - let mut row = values.array_new(captures.len())?; - for (match_index, capture) in captures.iter().enumerate() { - let key = - values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - capture, - capture_index, - offset_capture, - unmatched_as_null, - values, - )?; - row = values.array_set(row, key, value)?; - } - let key = - values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - outer = values.array_set(outer, key, row)?; - } - Ok(outer) -} - -/// Builds PHP's `preg_match_all(..., PREG_SET_ORDER)` match-order capture matrix. -fn eval_preg_match_all_set_order_array( - subject: &[u8], - captures: &[Captures<'_>], - capture_count: usize, - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - let mut outer = values.array_new(captures.len())?; - for (match_index, capture) in captures.iter().enumerate() { - let mut row = values.array_new(capture_count)?; - for capture_index in 0..capture_count { - let key = - values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - capture, - capture_index, - offset_capture, - unmatched_as_null, - values, - )?; - row = values.array_set(row, key, value)?; - } - let key = values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - outer = values.array_set(outer, key, row)?; - } - Ok(outer) -} - -/// Evaluates PHP `preg_replace()` over eval expressions. -pub(super) fn eval_builtin_preg_replace( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern, replacement, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - let replacement = eval_expr(replacement, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_replace_result(pattern, replacement, subject, values) -} - -/// Replaces every regex match with a PHP-style backreference-expanded replacement. -fn eval_preg_replace_result( - pattern: RuntimeCellHandle, - replacement: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let replacement = values.string_bytes(replacement)?; - let subject = values.string_bytes(subject)?; - let mut result = Vec::with_capacity(subject.len()); - let mut cursor = 0; - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - result.extend_from_slice(&subject[cursor..matched.start()]); - eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); - cursor = matched.end(); - } - result.extend_from_slice(&subject[cursor..]); - values.string_bytes_value(&result) -} - -/// Evaluates PHP `preg_replace_callback()` over eval expressions. -pub(super) fn eval_builtin_preg_replace_callback( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern, callback, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_replace_callback_result(pattern, callback, subject, context, values) -} - -/// Replaces every regex match by invoking an eval-supported callback with `$matches`. -fn eval_preg_replace_callback_result( - pattern: RuntimeCellHandle, - callback: RuntimeCellHandle, - subject: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let callback = eval_callable_name(callback, values)?; - let subject = values.string_bytes(subject)?; - let mut result = Vec::with_capacity(subject.len()); - let mut cursor = 0; - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - result.extend_from_slice(&subject[cursor..matched.start()]); - let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; - let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; - let callback_result = values.cast_string(callback_result)?; - let callback_bytes = values.string_bytes(callback_result)?; - result.extend_from_slice(&callback_bytes); - cursor = matched.end(); - } - result.extend_from_slice(&subject[cursor..]); - values.string_bytes_value(&result) -} - -/// Evaluates PHP `preg_split()` over eval expressions. -pub(super) fn eval_builtin_preg_split( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_split_result(pattern, subject, None, None, values) - } - [pattern, subject, limit] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let limit = eval_expr(limit, context, scope, values)?; - eval_preg_split_result(pattern, subject, Some(limit), None, values) - } - [pattern, subject, limit, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let limit = eval_expr(limit, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_preg_split_result(pattern, subject, Some(limit), Some(flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Splits a subject string with eval-supported `preg_split()` flags. -fn eval_preg_split_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - limit: Option, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let limit = eval_preg_split_limit(limit, values)?; - let flags = eval_preg_split_flags(flags, values)?; - let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; - let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; - let offset_capture = flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0; - let mut pieces = Vec::::new(); - let mut cursor = 0; - - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - if eval_preg_split_reached_limit(&pieces, limit) { - break; - } - eval_preg_split_push_piece( - &mut pieces, - &subject[cursor..matched.start()], - cursor, - no_empty, - ); - if capture_delimiters { - eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); - } - cursor = matched.end(); - } - eval_preg_split_push_piece(&mut pieces, &subject[cursor..], cursor, no_empty); - - let mut result = values.array_new(pieces.len())?; - for (index, piece) in pieces.iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_split_piece_value(piece, offset_capture, values)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Compiles one eval PCRE-style delimited pattern into a Rust regex. -fn eval_preg_regex( - pattern: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = values.string_bytes(pattern)?; - let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; - let body = String::from_utf8(body).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut builder = RegexBuilder::new(&body); - builder - .case_insensitive(modifiers.case_insensitive) - .multi_line(modifiers.multi_line) - .dot_matches_new_line(modifiers.dot_matches_new_line) - .swap_greed(modifiers.swap_greed); - builder.build().map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Regex modifiers supported by eval `preg_*` pattern stripping. -#[derive(Default)] -struct EvalPregModifiers { - case_insensitive: bool, - multi_line: bool, - dot_matches_new_line: bool, - swap_greed: bool, -} - -/// One `preg_split()` output segment plus its byte offset in the subject. -struct EvalPregSplitPiece { - bytes: Vec, - offset: usize, -} - -/// Splits a PHP delimited regex into body bytes and supported modifiers. -fn eval_preg_pattern_parts(pattern: &[u8]) -> Result<(Vec, EvalPregModifiers), EvalStatus> { - if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() { - return Err(EvalStatus::RuntimeFatal); - } - let delimiter = pattern[0]; - if delimiter == b'\\' { - return Err(EvalStatus::RuntimeFatal); - } - let closing = eval_preg_closing_delimiter(delimiter); - let close_index = - eval_preg_find_closing_delimiter(pattern, closing).ok_or(EvalStatus::RuntimeFatal)?; - let body = eval_preg_unescape_delimiter(&pattern[1..close_index], delimiter, closing); - let modifiers = eval_preg_modifiers(&pattern[close_index + 1..])?; - Ok((body, modifiers)) -} - -/// Returns the closing regex delimiter for PHP's paired delimiter forms. -fn eval_preg_closing_delimiter(delimiter: u8) -> u8 { - match delimiter { - b'(' => b')', - b'[' => b']', - b'{' => b'}', - b'<' => b'>', - _ => delimiter, - } -} - -/// Finds the first unescaped closing regex delimiter. -fn eval_preg_find_closing_delimiter(pattern: &[u8], closing: u8) -> Option { - let mut escaped = false; - for (index, byte) in pattern.iter().copied().enumerate().skip(1) { - if escaped { - escaped = false; - continue; - } - if byte == b'\\' { - escaped = true; - continue; - } - if byte == closing { - return Some(index); - } - } - None -} - -/// Removes escapes that only protect the PHP regex delimiter from pattern stripping. -fn eval_preg_unescape_delimiter(body: &[u8], delimiter: u8, closing: u8) -> Vec { - let mut result = Vec::with_capacity(body.len()); - let mut index = 0; - while index < body.len() { - if body[index] == b'\\' - && index + 1 < body.len() - && matches!(body[index + 1], byte if byte == delimiter || byte == closing) - { - result.push(body[index + 1]); - index += 2; - } else { - result.push(body[index]); - index += 1; - } - } - result -} - -/// Parses eval-supported PHP regex modifiers. -fn eval_preg_modifiers(modifiers: &[u8]) -> Result { - let mut parsed = EvalPregModifiers::default(); - for modifier in modifiers { - match *modifier { - b'i' => parsed.case_insensitive = true, - b'm' => parsed.multi_line = true, - b's' => parsed.dot_matches_new_line = true, - b'U' => parsed.swap_greed = true, - b'u' => {} - _ => return Err(EvalStatus::RuntimeFatal), - } - } - Ok(parsed) -} - -/// Builds PHP's indexed `$matches` capture array for one regex result. -fn eval_preg_capture_array( - subject: &[u8], - captures: Option<&Captures<'_>>, - offset_capture: bool, - unmatched_as_null: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = captures.map_or(0, |captures| { - eval_preg_visible_capture_len(captures, unmatched_as_null) - }); - let mut result = values.array_new(len)?; - if let Some(captures) = captures { - for index in 0..len { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - captures, - index, - offset_capture, - unmatched_as_null, - values, - )?; - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Returns the capture count PHP should expose, dropping trailing unmatched groups. -fn eval_preg_visible_capture_len(captures: &Captures<'_>, unmatched_as_null: bool) -> usize { - if unmatched_as_null { - return captures.len(); - } - let mut len = captures.len(); - while len > 1 && captures.get(len - 1).is_none() { - len -= 1; - } - len -} - -/// Returns one captured byte range from the original subject. -fn eval_preg_capture_bytes<'a>( - subject: &'a [u8], - captures: &Captures<'_>, - index: usize, -) -> Option<&'a [u8]> { - captures - .get(index) - .map(|matched| &subject[matched.start()..matched.end()]) -} - -/// Builds one capture entry as either a string or PHP's `[string, byte_offset]` pair. -fn eval_preg_capture_value( - subject: &[u8], - captures: &Captures<'_>, - index: usize, - offset_capture: bool, - unmatched_as_null: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let matched = captures.get(index); - let value = if matched.is_none() && unmatched_as_null { - values.null()? - } else { - let bytes = matched.as_ref().map_or(b"".as_slice(), |matched| { - &subject[matched.start()..matched.end()] - }); - values.string_bytes_value(bytes)? - }; - if !offset_capture { - return Ok(value); - } - - let offset = matched.map_or(Ok(-1_i64), |matched| { - i64::try_from(matched.start()).map_err(|_| EvalStatus::RuntimeFatal) - })?; - let offset = values.int(offset)?; - let mut pair = values.array_new(2)?; - let value_key = values.int(0)?; - pair = values.array_set(pair, value_key, value)?; - let offset_key = values.int(1)?; - values.array_set(pair, offset_key, offset) -} - -/// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. -fn eval_preg_expand_replacement( - replacement: &[u8], - subject: &[u8], - captures: &Captures<'_>, - result: &mut Vec, -) { - let mut index = 0; - while index < replacement.len() { - match replacement[index] { - b'$' => { - if let Some((capture_index, next_index)) = - eval_preg_replacement_capture_index(replacement, index + 1) - { - if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { - result.extend_from_slice(bytes); - } - index = next_index; - } else { - result.push(replacement[index]); - index += 1; - } - } - b'\\' if index + 1 < replacement.len() && replacement[index + 1].is_ascii_digit() => { - let (capture_index, next_index) = - eval_preg_decimal_capture_index(replacement, index + 1); - if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { - result.extend_from_slice(bytes); - } - index = next_index; - } - byte => { - result.push(byte); - index += 1; - } - } - } -} - -/// Parses a dollar-style replacement capture reference. -fn eval_preg_replacement_capture_index(bytes: &[u8], index: usize) -> Option<(usize, usize)> { - if bytes.get(index).copied() == Some(b'{') { - let mut cursor = index + 1; - let start = cursor; - while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { - cursor += 1; - } - if cursor == start || bytes.get(cursor).copied() != Some(b'}') { - return None; - } - let capture = eval_preg_decimal_bytes_to_usize(&bytes[start..cursor])?; - return Some((capture, cursor + 1)); - } - if bytes.get(index).is_some_and(u8::is_ascii_digit) { - let (capture, next) = eval_preg_decimal_capture_index(bytes, index); - return Some((capture, next)); - } - None -} - -/// Parses a one- or two-digit replacement capture reference. -fn eval_preg_decimal_capture_index(bytes: &[u8], index: usize) -> (usize, usize) { - let mut cursor = index; - let end = usize::min(bytes.len(), index + 2); - while cursor < end && bytes[cursor].is_ascii_digit() { - cursor += 1; - } - ( - eval_preg_decimal_bytes_to_usize(&bytes[index..cursor]).unwrap_or(0), - cursor, - ) -} - -/// Converts ASCII decimal bytes into a `usize` capture index. -fn eval_preg_decimal_bytes_to_usize(bytes: &[u8]) -> Option { - let mut value = 0usize; - for byte in bytes { - value = value.checked_mul(10)?; - value = value.checked_add(usize::from(byte - b'0'))?; - } - Some(value) -} - -/// Returns the PHP `preg_split()` limit, treating zero as unlimited. -fn eval_preg_split_limit( - limit: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(limit) = limit else { - return Ok(None); - }; - let limit = eval_int_value(limit, values)?; - if limit <= 0 { - return Ok(None); - } - usize::try_from(limit) - .map(Some) - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Returns supported `preg_split()` flags. -fn eval_preg_split_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(0); - }; - let flags = eval_int_value(flags, values)?; - let supported = - EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE | EVAL_PREG_SPLIT_OFFSET_CAPTURE; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Returns whether `preg_split()` should stop splitting and emit the remaining subject. -fn eval_preg_split_reached_limit(pieces: &[EvalPregSplitPiece], limit: Option) -> bool { - matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) -} - -/// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. -fn eval_preg_split_push_piece( - pieces: &mut Vec, - piece: &[u8], - offset: usize, - no_empty: bool, -) { - if no_empty && piece.is_empty() { - return; - } - pieces.push(EvalPregSplitPiece { - bytes: piece.to_vec(), - offset, - }); -} - -/// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. -fn eval_preg_split_push_captures( - pieces: &mut Vec, - subject: &[u8], - captures: &Captures<'_>, - no_empty: bool, -) { - for index in 1..captures.len() { - if let Some(matched) = captures.get(index) { - eval_preg_split_push_piece( - pieces, - &subject[matched.start()..matched.end()], - matched.start(), - no_empty, - ); - } - } -} - -/// Converts one split segment to a string or PHP `[string, byte_offset]` pair. -fn eval_preg_split_piece_value( - piece: &EvalPregSplitPiece, - offset_capture: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.string_bytes_value(&piece.bytes)?; - if !offset_capture { - return Ok(value); - } - - let offset = i64::try_from(piece.offset).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = values.int(offset)?; - let mut pair = values.array_new(2)?; - let value_key = values.int(0)?; - pair = values.array_set(pair, value_key, value)?; - let offset_key = values.int(1)?; - values.array_set(pair, offset_key, offset) -} - -/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. -pub(super) fn eval_builtin_gethostbyaddr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_gethostbyaddr_result(ip, values) -} - -/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. -fn eval_gethostbyaddr_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let ip_bytes = values.string_bytes(ip)?; - let ip_text = String::from_utf8_lossy(&ip_bytes); - let Ok(ipv4) = ip_text.parse::() else { - return values.bool_value(false); - }; - let octets = ipv4.octets(); - let resolved = unsafe { - // libc reads the stack-owned IPv4 octets during this call and returns - // static resolver storage, which is copied before the next resolver call. - let host = libc_gethostbyaddr( - octets.as_ptr().cast::(), - octets.len() as libc::socklen_t, - libc::AF_INET, - ); - if host.is_null() || (*host).h_name.is_null() { - None - } else { - Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) - } - }; - match resolved { - Some(name) if !name.is_empty() => values.string_bytes_value(&name), - _ => values.string(ip_text.as_ref()), - } -} - -/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. -pub(super) fn eval_builtin_gethostbyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hostname] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hostname = eval_expr(hostname, context, scope, values)?; - eval_gethostbyname_result(hostname, values) -} - -/// Resolves one host name to an IPv4 string, or returns the original input on failure. -fn eval_gethostbyname_result( - hostname: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let hostname = values.string_bytes(hostname)?; - let hostname = String::from_utf8_lossy(&hostname); - if hostname.parse::().is_ok() { - return values.string(hostname.as_ref()); - } - let resolved = (hostname.as_ref(), 0_u16) - .to_socket_addrs() - .ok() - .and_then(|addrs| { - addrs - .filter_map(|addr| match addr.ip() { - std::net::IpAddr::V4(ip) => Some(ip.to_string()), - std::net::IpAddr::V6(_) => None, - }) - .next() - }); - values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) -} - -/// Evaluates PHP `gethostname()` over one eval expression. -pub(super) fn eval_builtin_gethostname( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - let [] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostname_result(values) -} - -/// Reads the current host name through libc and returns an empty string on failure. -fn eval_gethostname_result( - values: &mut impl RuntimeValueOps, -) -> Result { - let mut buffer = [0 as libc::c_char; 256]; - let status = unsafe { - // libc writes at most buffer.len() bytes into this stack buffer. - libc::gethostname(buffer.as_mut_ptr(), buffer.len()) - }; - if status != 0 { - return values.string(""); - } - let length = buffer - .iter() - .position(|byte| *byte == 0) - .unwrap_or(buffer.len()); - let hostname = buffer[..length] - .iter() - .map(|byte| *byte as u8) - .collect::>(); - values.string_bytes_value(&hostname) -} - -/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. -pub(super) fn eval_builtin_getprotobyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getprotobyname_result(protocol, values) -} - -/// Looks up an IP protocol number by name or alias. -fn eval_getprotobyname_result( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global protoent; copy scalar fields before another lookup. - libc_getprotobyname(protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); - } - let number = unsafe { (*entry).p_proto }; - values.int(i64::from(number)) -} - -/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. -pub(super) fn eval_builtin_getprotobynumber( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getprotobynumber_result(protocol, values) -} - -/// Looks up an IP protocol name by numeric protocol id. -fn eval_getprotobynumber_result( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let protocol = eval_int_value(protocol, values)?; - let Ok(protocol) = libc::c_int::try_from(protocol) else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global protoent; copy the name before another lookup. - libc_getprotobynumber(protocol) - }; - eval_protoent_name_or_false(entry, values) -} - -/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. -pub(super) fn eval_builtin_getservbyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [service, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let service = eval_expr(service, context, scope, values)?; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getservbyname_result(service, protocol, values) -} - -/// Looks up an internet service port by service name and protocol. -fn eval_getservbyname_result( - service: RuntimeCellHandle, - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(service) = eval_lowercase_c_string(service, values)? else { - return values.bool_value(false); - }; - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global servent; copy scalar fields before another lookup. - libc_getservbyname(service.as_ptr(), protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); - } - let port = unsafe { u16::from_be((*entry).s_port as u16) }; - values.int(i64::from(port)) -} - -/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. -pub(super) fn eval_builtin_getservbyport( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [port, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let port = eval_expr(port, context, scope, values)?; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getservbyport_result(port, protocol, values) -} - -/// Looks up an internet service name by port and protocol. -fn eval_getservbyport_result( - port: RuntimeCellHandle, - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let port = eval_int_value(port, values)?; - let Ok(port) = u16::try_from(port) else { - return values.bool_value(false); - }; - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let network_port = port.to_be() as libc::c_int; - let entry = unsafe { - // libc returns a process-global servent; copy the name before another lookup. - libc_getservbyport(network_port, protocol.as_ptr()) - }; - eval_servent_name_or_false(entry, values) -} - -/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. -fn eval_lowercase_c_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let bytes = values.string_bytes(value)?; - let bytes = bytes - .into_iter() - .map(|byte| byte.to_ascii_lowercase()) - .collect::>(); - Ok(CString::new(bytes).ok()) -} - -/// Copies a protoent canonical name into a PHP string or returns PHP false. -fn eval_protoent_name_or_false( - entry: *mut libc::protoent, - values: &mut impl RuntimeValueOps, -) -> Result { - if entry.is_null() { - return values.bool_value(false); - } - let name = unsafe { - let name = (*entry).p_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) -} - -/// Copies a servent canonical name into a PHP string or returns PHP false. -fn eval_servent_name_or_false( - entry: *mut libc::servent, - values: &mut impl RuntimeValueOps, -) -> Result { - if entry.is_null() { - return values.bool_value(false); - } - let name = unsafe { - let name = (*entry).s_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) -} - -/// Evaluates PHP `long2ip($ip)` over one eval expression. -pub(super) fn eval_builtin_long2ip( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_long2ip_result(ip, values) -} - -/// Formats one 32-bit IPv4 integer as a dotted-quad string. -fn eval_long2ip_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let ip = eval_int_value(ip, values)? as u32; - values.string(&eval_format_ipv4(ip)) -} - -/// Evaluates PHP `ip2long($ip)` over one eval expression. -pub(super) fn eval_builtin_ip2long( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_ip2long_result(ip, values) -} - -/// Parses a dotted-quad IPv4 string into an integer or PHP false. -fn eval_ip2long_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(ip)?; - match eval_parse_ipv4(&bytes) { - Some(ip) => values.int(i64::from(ip)), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `inet_pton($ip)` over one eval expression. -pub(super) fn eval_builtin_inet_pton( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_inet_pton_result(ip, values) -} - -/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. -fn eval_inet_pton_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(ip)?; - let Some(ip) = eval_parse_ipv4(&bytes) else { - return values.bool_value(false); - }; - values.string_bytes_value(&ip.to_be_bytes()) -} - -/// Evaluates PHP `inet_ntop($binary)` over one eval expression. -pub(super) fn eval_builtin_inet_ntop( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [binary] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let binary = eval_expr(binary, context, scope, values)?; - eval_inet_ntop_result(binary, values) -} - -/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. -fn eval_inet_ntop_result( - binary: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(binary)?; - let [a, b, c, d] = bytes.as_slice() else { - return values.bool_value(false); - }; - let ip = u32::from_be_bytes([*a, *b, *c, *d]); - values.string(&eval_format_ipv4(ip)) -} - -/// Parses exactly four decimal IPv4 octets separated by dots. -fn eval_parse_ipv4(bytes: &[u8]) -> Option { - let mut octets = [0_u8; 4]; - let mut position = 0_usize; - let mut index = 0_usize; - - while index < 4 { - if position >= bytes.len() { - return None; - } - let start = position; - let mut value = 0_u16; - while position < bytes.len() && bytes[position].is_ascii_digit() { - value = value - .checked_mul(10)? - .checked_add(u16::from(bytes[position] - b'0'))?; - position += 1; - if position - start > 3 || value > 255 { - return None; - } - } - if position == start { - return None; - } - octets[index] = value as u8; - index += 1; - if index == 4 { - return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); - } - if bytes.get(position).copied() != Some(b'.') { - return None; - } - position += 1; - } - None -} - -/// Formats one packed IPv4 integer into dotted-quad text. -fn eval_format_ipv4(ip: u32) -> String { - let [a, b, c, d] = ip.to_be_bytes(); - format!("{}.{}.{}.{}", a, b, c, d) -} - -/// Evaluates PHP `getenv($name)` over one eval expression. -pub(super) fn eval_builtin_getenv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - eval_getenv_result(name, values) -} - -/// Reads one environment variable and returns an empty string when it is unset. -fn eval_getenv_result( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8_lossy(&name); - let value = std::env::var_os(name.as_ref()) - .map(|value| value.to_string_lossy().into_owned()) - .unwrap_or_default(); - values.string(&value) -} - -/// Evaluates PHP `putenv($assignment)` over one eval expression. -pub(super) fn eval_builtin_putenv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [assignment] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let assignment = eval_expr(assignment, context, scope, values)?; - eval_putenv_result(assignment, values) -} - -/// Applies one `putenv()` assignment to the host environment. -fn eval_putenv_result( - assignment: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let assignment = values.string_bytes(assignment)?; - if let Some(separator) = assignment.iter().position(|byte| *byte == b'=') { - let name = String::from_utf8_lossy(&assignment[..separator]); - let value = String::from_utf8_lossy(&assignment[separator + 1..]); - std::env::set_var(name.as_ref(), value.as_ref()); - } else { - let name = String::from_utf8_lossy(&assignment); - std::env::remove_var(name.as_ref()); - } - values.bool_value(true) -} - -/// Evaluates PHP `sys_get_temp_dir()` with no arguments. -pub(super) fn eval_builtin_sys_get_temp_dir( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_sys_get_temp_dir_result(values) -} - -/// Returns the same temporary directory literal as the native static builtin. -fn eval_sys_get_temp_dir_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.string("/tmp") -} - -/// Evaluates PHP `realpath_cache_get()` with no arguments. -pub(super) fn eval_builtin_realpath_cache_get( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_get_result(values) -} - -/// Returns elephc's intentionally empty realpath-cache view. -fn eval_realpath_cache_get_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.array_new(0) -} - -/// Evaluates PHP `realpath_cache_size()` with no arguments. -pub(super) fn eval_builtin_realpath_cache_size( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_size_result(values) -} - -/// Returns zero because elephc does not maintain a runtime realpath cache. -fn eval_realpath_cache_size_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.int(0) -} - -/// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. -fn eval_crc32_bytes(bytes: &[u8]) -> u32 { - let mut crc = 0xffff_ffff_u32; - for byte in bytes { - crc ^= u32::from(*byte); - for _ in 0..8 { - let mask = 0_u32.wrapping_sub(crc & 1); - crc = (crc >> 1) ^ (0xedb8_8320 & mask); - } - } - !crc -} - -/// Casts one eval value to PHP int and returns the scalar payload. -pub(super) fn eval_int_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.cast_int(value)?; - let bytes = values.string_bytes(value)?; - std::str::from_utf8(&bytes) - .map_err(|_| EvalStatus::RuntimeFatal)? - .parse::() - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP's `bin2hex(...)` over one eval expression. -pub(super) fn eval_builtin_bin2hex( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_bin2hex_result(value, values) -} - -/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. -fn eval_bin2hex_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.string(&eval_lower_hex_bytes(&bytes)) -} - -/// Converts bytes to lowercase hexadecimal text. -fn eval_lower_hex_bytes(bytes: &[u8]) -> String { - let mut output = String::with_capacity(bytes.len() * 2); - const HEX: &[u8; 16] = b"0123456789abcdef"; - for byte in bytes { - output.push(HEX[(byte >> 4) as usize] as char); - output.push(HEX[(byte & 0x0f) as usize] as char); - } - output -} - -/// Evaluates PHP's `hex2bin(...)` over one eval expression. -pub(super) fn eval_builtin_hex2bin( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_hex2bin_result(value, values) -} - -/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. -fn eval_hex2bin_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - if bytes.len() % 2 != 0 { - values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; - return values.bool_value(false); - } - let mut output = Vec::with_capacity(bytes.len() / 2); - for pair in bytes.chunks_exact(2) { - let Some(high) = eval_hex_nibble(pair[0]) else { - values.warning(HEX2BIN_INVALID_WARNING)?; - return values.bool_value(false); - }; - let Some(low) = eval_hex_nibble(pair[1]) else { - values.warning(HEX2BIN_INVALID_WARNING)?; - return values.bool_value(false); - }; - output.push((high << 4) | low); - } - values.string_bytes_value(&output) -} - -/// Returns the four-bit value for one hexadecimal byte. -fn eval_hex_nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } -} - -/// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. -pub(super) fn eval_builtin_slashes( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_slashes_result(name, value, values) -} - -/// Applies PHP byte-string escaping or unescaping for addslashes/stripslashes. -fn eval_slashes_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "addslashes" => eval_addslashes_result(value, values), - "stripslashes" => eval_stripslashes_result(value, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. -fn eval_addslashes_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - for byte in bytes { - match byte { - 0 => output.extend_from_slice(b"\\0"), - b'\'' | b'"' | b'\\' => { - output.push(b'\\'); - output.push(byte); - } - _ => output.push(byte), - } - } - values.string_bytes_value(&output) -} - -/// Removes backslash quoting using PHP `stripslashes()` byte semantics. -fn eval_stripslashes_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'\\' { - index += 1; - if let Some(byte) = bytes.get(index).copied() { - output.push(if byte == b'0' { 0 } else { byte }); - index += 1; - } - } else { - output.push(bytes[index]); - index += 1; - } - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `base64_encode(...)` over one eval expression. -pub(super) fn eval_builtin_base64_encode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_encode_result(value, values) -} - -/// Converts one eval value through PHP string conversion and returns Base64 text. -fn eval_base64_encode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); - const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for chunk in bytes.chunks(3) { - let first = chunk[0]; - let second = chunk.get(1).copied().unwrap_or(0); - let third = chunk.get(2).copied().unwrap_or(0); - output.push(ALPHABET[(first >> 2) as usize] as char); - output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); - if chunk.len() > 1 { - output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); - } else { - output.push('='); - } - if chunk.len() > 2 { - output.push(ALPHABET[(third & 0x3f) as usize] as char); - } else { - output.push('='); - } - } - values.string(&output) -} - -/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. -pub(super) fn eval_builtin_base64_decode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_decode_result(value, values) -} - -/// Converts one eval value through PHP string conversion and decodes Base64 bytes. -fn eval_base64_decode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let input = values.string_bytes(value)?; - let mut output = Vec::with_capacity((input.len() / 4) * 3); - let mut quartet = Vec::with_capacity(4); - for byte in input { - if byte.is_ascii_whitespace() { - continue; - } - if byte == b'=' { - quartet.push(None); - } else if let Some(value) = eval_base64_decode_sextet(byte) { - quartet.push(Some(value)); - } else { - continue; - } - if quartet.len() == 4 { - eval_push_base64_decoded_quartet(&quartet, &mut output); - quartet.clear(); - } - } - if !quartet.is_empty() { - while quartet.len() < 4 { - quartet.push(None); - } - eval_push_base64_decoded_quartet(&quartet, &mut output); - } - values.string_bytes_value(&output) -} - -/// Returns the six-bit Base64 value for one encoded byte. -fn eval_base64_decode_sextet(byte: u8) -> Option { - match byte { - b'A'..=b'Z' => Some(byte - b'A'), - b'a'..=b'z' => Some(byte - b'a' + 26), - b'0'..=b'9' => Some(byte - b'0' + 52), - b'+' => Some(62), - b'/' => Some(63), - _ => None, - } -} - -/// Appends decoded bytes for one padded or unpadded Base64 quartet. -fn eval_push_base64_decoded_quartet(quartet: &[Option], output: &mut Vec) { - let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { - return; - }; - output.push((first << 2) | (second >> 4)); - let Some(third) = quartet[2] else { - return; - }; - output.push(((second & 0x0f) << 4) | (third >> 2)); - let Some(fourth) = quartet[3] else { - return; - }; - output.push(((third & 0x03) << 6) | fourth); -} - -/// Evaluates PHP one-argument floating-point math builtins over one eval expression. -pub(super) fn eval_builtin_float_unary( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_float_unary_result(name, value, values) -} - -/// Dispatches an evaluated value through the matching PHP floating-point unary math function. -fn eval_float_unary_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_float_value(value, values)?; - let result = match name { - "acos" => value.acos(), - "asin" => value.asin(), - "atan" => value.atan(), - "cos" => value.cos(), - "cosh" => value.cosh(), - "deg2rad" => value.to_radians(), - "exp" => value.exp(), - "log2" => value.log2(), - "log10" => value.log10(), - "rad2deg" => value.to_degrees(), - "sin" => value.sin(), - "sinh" => value.sinh(), - "tan" => value.tan(), - "tanh" => value.tanh(), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.float(result) -} - -/// Evaluates PHP two-argument floating-point math builtins over eval expressions. -pub(super) fn eval_builtin_float_pair( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_float_pair_result(name, left, right, values) -} - -/// Dispatches an evaluated pair through PHP `atan2()` or `hypot()`. -fn eval_float_pair_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left = eval_float_value(left, values)?; - let right = eval_float_value(right, values)?; - let result = match name { - "atan2" => left.atan2(right), - "hypot" => left.hypot(right), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.float(result) -} - -/// Evaluates PHP `log($num, $base = e)` over eval expressions. -pub(super) fn eval_builtin_log( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [num] => { - let num = eval_expr(num, context, scope, values)?; - eval_log_result(num, None, values) - } - [num, base] => { - let num = eval_expr(num, context, scope, values)?; - let base = eval_expr(base, context, scope, values)?; - eval_log_result(num, Some(base), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `log()` from already evaluated arguments. -fn eval_log_result( - num: RuntimeCellHandle, - base: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let num = eval_float_value(num, values)?; - let result = match base { - Some(base) => num.log(eval_float_value(base, values)?), - None => num.ln(), - }; - values.float(result) -} - -/// Evaluates PHP `intdiv(...)` over two eval expressions. -pub(super) fn eval_builtin_intdiv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_intdiv_result(left, right, values) -} - -/// Computes PHP integer division from already evaluated arguments. -fn eval_intdiv_result( - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left = eval_int_value(left, values)?; - let right = eval_int_value(right, values)?; - if right == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; - values.int(result) -} - -/// Evaluates PHP floating-point binary math builtins over two eval expressions. -pub(super) fn eval_builtin_float_binary( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_float_binary_result(name, left, right, values) -} - -/// Dispatches an evaluated pair through the matching PHP float math hook. -fn eval_float_binary_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "fdiv" => values.fdiv(left, right), - "fmod" => values.fmod(left, right), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `clamp($value, $min, $max)` over three eval expressions. -pub(super) fn eval_builtin_clamp( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value, min, max] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_clamp_result(value, min, max, values) -} - -/// Selects the inclusive clamp result after validating bound order and NaN bounds. -fn eval_clamp_result( - value: RuntimeCellHandle, - min: RuntimeCellHandle, - max: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; - if values.truthy(invalid_bounds)? { - return Err(EvalStatus::RuntimeFatal); - } - let above_max = values.compare(EvalBinOp::Gt, value, max)?; - if values.truthy(above_max)? { - return Ok(max); - } - let below_min = values.compare(EvalBinOp::Lt, value, min)?; - if values.truthy(below_min)? { - return Ok(min); - } - Ok(value) -} - -/// Returns whether a clamp bound is a floating-point NaN value. -fn eval_clamp_bound_is_nan( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? != EVAL_TAG_FLOAT { - return Ok(false); - } - Ok(eval_float_value(value, values)?.is_nan()) -} - -/// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. -pub(super) fn eval_builtin_min_max( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_min_max_result(name, &evaluated_args, values) -} - -/// Selects the smallest or largest evaluated cell using runtime comparison hooks. -fn eval_min_max_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((&first, rest)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let op = match name { - "min" => EvalBinOp::Lt, - "max" => EvalBinOp::Gt, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let mut selected = first; - for candidate in rest { - let better = values.compare(op, *candidate, selected)?; - if values.truthy(better)? { - selected = *candidate; - } - } - Ok(selected) -} - -/// Evaluates PHP scalar cast builtins over one eval expression. -pub(super) fn eval_builtin_cast( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_cast_result(name, value, values) -} - -/// Dispatches an already evaluated value through the matching PHP cast hook. -fn eval_cast_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "intval" => values.cast_int(value), - "floatval" => values.cast_float(value), - "strval" => values.cast_string(value), - "boolval" => values.cast_bool(value), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP's `gettype(...)` over one eval expression. -pub(super) fn eval_builtin_gettype( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_gettype_result(value, values) -} - -/// Converts one boxed runtime tag into PHP's `gettype()` spelling. -fn eval_gettype_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - values.string(eval_gettype_name(tag)) -} - -/// Evaluates PHP's `get_class(...)` over one eval object expression. -pub(super) fn eval_builtin_get_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_get_class_result(object, context, values) -} - -/// Resolves the PHP-visible class name for one already materialized object cell. -fn eval_get_class_result( - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Ok(identity) = values.object_identity(object) { - if let Some(class) = context.dynamic_object_class(identity) { - return values.string(class.name().trim_start_matches('\\')); - } - } - values.object_class_name(object) -} - -/// Evaluates PHP's SPL object identity builtins over one eval object expression. -pub(super) fn eval_builtin_spl_object_identity( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_spl_object_identity_result(name, object, values) -} - -/// Returns the unboxed object-payload identity in the native SPL builtin spelling. -fn eval_spl_object_identity_result( - name: &str, - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let identity = values.object_identity(object)? as i64; - match name { - "spl_object_id" => values.int(identity), - "spl_object_hash" => values.string(&identity.to_string()), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. -pub(super) fn eval_builtin_get_parent_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object_or_class] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object_or_class = eval_expr(object_or_class, context, scope, values)?; - eval_get_parent_class_result(object_or_class, values) -} - -/// Resolves the PHP-visible parent class name for one object or class-name cell. -fn eval_get_parent_class_result( - object_or_class: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - values.parent_class_name(object_or_class) -} - -/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. -pub(super) fn eval_builtin_resource_introspection( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [resource] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let resource = eval_expr(resource, context, scope, values)?; - eval_resource_introspection_result(name, resource, values) -} - -/// Evaluates a materialized resource introspection builtin argument. -fn eval_resource_introspection_result( - name: &str, - resource: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(resource)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - match name { - "get_resource_type" => values.string("stream"), - "get_resource_id" => values.cast_int(resource), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Returns the PHP-visible type name for a concrete eval runtime tag. -fn eval_gettype_name(tag: u64) -> &'static str { - match tag { - EVAL_TAG_INT => "integer", - EVAL_TAG_FLOAT => "double", - EVAL_TAG_STRING => "string", - EVAL_TAG_BOOL => "boolean", - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", - EVAL_TAG_OBJECT => "object", - EVAL_TAG_RESOURCE => "resource", - EVAL_TAG_NULL => "NULL", - _ => "NULL", - } -} - -/// Evaluates PHP scalar/container type predicate builtins over one eval expression. -pub(super) fn eval_builtin_type_predicate( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_type_predicate_result(name, value, values) -} - -/// Converts a concrete runtime tag into a PHP `is_*` predicate result. -fn eval_type_predicate_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - let result = match name { - "is_int" | "is_integer" | "is_long" => tag == EVAL_TAG_INT, - "is_float" | "is_double" | "is_real" => tag == EVAL_TAG_FLOAT, - "is_string" => tag == EVAL_TAG_STRING, - "is_bool" => tag == EVAL_TAG_BOOL, - "is_null" => tag == EVAL_TAG_NULL, - "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), - "is_object" => tag == EVAL_TAG_OBJECT, - "is_resource" => tag == EVAL_TAG_RESOURCE, - "is_nan" => eval_float_value(value, values)?.is_nan(), - "is_infinite" => eval_float_value(value, values)?.is_infinite(), - "is_finite" => eval_float_value(value, values)?.is_finite(), - "is_numeric" => { - tag == EVAL_TAG_INT - || tag == EVAL_TAG_FLOAT - || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)) - } - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(result) -} - -/// Matches the static backend's legacy ASCII numeric-string scan. -fn eval_is_numeric_string(bytes: &[u8]) -> bool { - if bytes.is_empty() { - return false; - } - - let mut index = 0; - let mut consumed_digits = 0; - if bytes[index] == b'-' { - index += 1; - if index >= bytes.len() { - return false; - } - } - - while index < bytes.len() { - if bytes[index] == b'.' { - index += 1; - break; - } - if !bytes[index].is_ascii_digit() { - return false; - } - consumed_digits += 1; - index += 1; - } - - while index < bytes.len() { - if !bytes[index].is_ascii_digit() { - return false; - } - consumed_digits += 1; - index += 1; - } - - consumed_digits > 0 -} - -/// Evaluates PHP's `hash_equals(...)` over two eval expressions. -pub(super) fn eval_builtin_hash_equals( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [known, user] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let known = eval_expr(known, context, scope, values)?; - let user = eval_expr(user, context, scope, values)?; - eval_hash_equals_result(known, user, values) -} - -/// Compares two converted strings with PHP `hash_equals()` semantics. -fn eval_hash_equals_result( - known: RuntimeCellHandle, - user: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let known = values.string_bytes(known)?; - let user = values.string_bytes(user)?; - if known.len() != user.len() { - return values.bool_value(false); - } - let mut diff = 0u8; - for (known, user) in known.iter().zip(user.iter()) { - diff |= known ^ user; - } - values.bool_value(diff == 0) -} - -/// Evaluates PHP string comparison builtins over two eval expressions. -pub(super) fn eval_builtin_string_compare( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_string_compare_result(name, left, right, values) -} - -/// Compares two converted strings and returns -1, 0, or 1. -fn eval_string_compare_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut left = values.string_bytes(left)?; - let mut right = values.string_bytes(right)?; - match name { - "strcmp" => {} - "strcasecmp" => { - left.make_ascii_lowercase(); - right.make_ascii_lowercase(); - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - let result = match left.cmp(&right) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - }; - values.int(result) -} - -/// Evaluates PHP's byte-string search predicates over two eval expressions. -pub(super) fn eval_builtin_string_search( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_string_search_result(name, haystack, needle, values) -} - -/// Checks one converted haystack for one converted needle using PHP byte-string semantics. -fn eval_string_search_result( - name: &str, - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let matched = match name { - "str_contains" => { - needle.is_empty() - || haystack - .windows(needle.len()) - .any(|window| window == needle) - } - "str_starts_with" => haystack.starts_with(&needle), - "str_ends_with" => haystack.ends_with(&needle), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(matched) -} - -/// Evaluates PHP byte-string position builtins over two eval expressions. -pub(super) fn eval_builtin_string_position( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_string_position_result(name, haystack, needle, values) -} - -/// Returns the first or last byte offset of a converted needle, or PHP `false`. -fn eval_string_position_result( - name: &str, - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let position = match name { - "strpos" if needle.is_empty() => Some(0), - "strpos" => haystack - .windows(needle.len()) - .position(|window| window == needle), - "strrpos" if needle.is_empty() => Some(haystack.len()), - "strrpos" => haystack - .windows(needle.len()) - .rposition(|window| window == needle), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - match position { - Some(position) => { - let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(position) - } - None => values.bool_value(false), - } -} - -/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. -pub(super) fn eval_builtin_strstr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [haystack, needle] => { - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_strstr_result(haystack, needle, false, values) - } - [haystack, needle, before_needle] => { - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - let before_needle = eval_expr(before_needle, context, scope, values)?; - let before_needle = values.truthy(before_needle)?; - eval_strstr_result(haystack, needle, before_needle, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. -fn eval_strstr_result( - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - before_needle: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let position = if needle.is_empty() { - Some(0) - } else { - eval_find_subslice(&haystack, &needle, 0) - }; - let Some(position) = position else { - return values.bool_value(false); - }; - let result = if before_needle { - &haystack[..position] - } else { - &haystack[position..] - }; - values.string_bytes_value(result) -} - -const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; - -/// Evaluates PHP trim-like string builtins over one eval expression and optional mask. -pub(super) fn eval_builtin_trim_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_trim_like_result(name, value, None, values) - } - [value, mask] => { - let value = eval_expr(value, context, scope, values)?; - let mask = eval_expr(mask, context, scope, values)?; - eval_trim_like_result(name, value, Some(mask), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Trims one converted string using PHP's default mask or a caller-provided byte mask. -fn eval_trim_like_result( - name: &str, - value: RuntimeCellHandle, - mask: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let explicit_mask; - let trim_mask = if let Some(mask) = mask { - explicit_mask = values.string_bytes(mask)?; - explicit_mask.as_slice() - } else { - PHP_DEFAULT_TRIM_MASK - }; - - let mut start = 0; - let mut end = bytes.len(); - if matches!(name, "trim" | "ltrim") { - while start < end && trim_mask.contains(&bytes[start]) { - start += 1; - } - } - if matches!(name, "trim" | "rtrim" | "chop") { - while end > start && trim_mask.contains(&bytes[end - 1]) { - end -= 1; - } - } - if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { - return Err(EvalStatus::UnsupportedConstruct); - } - - let value = - String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(&value) -} - -/// Evaluates PHP ASCII case-conversion string builtins over one eval expression. -pub(super) fn eval_builtin_string_case( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_string_case_result(name, value, values) -} - -/// Converts one eval value through PHP string conversion and ASCII case mapping. -fn eval_string_case_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bytes = values.string_bytes(value)?; - match name { - "strtolower" => { - for byte in &mut bytes { - if byte.is_ascii_uppercase() { - *byte += b'a' - b'A'; - } - } - } - "strtoupper" => { - for byte in &mut bytes { - if byte.is_ascii_lowercase() { - *byte -= b'a' - b'A'; - } - } - } - "ucfirst" => { - if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { - bytes[0] -= b'a' - b'A'; - } - } - "lcfirst" => { - if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { - bytes[0] += b'a' - b'A'; - } - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(&value) -} - -/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. -pub(super) fn eval_builtin_ucwords( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_ucwords_result(value, None, values) - } - [value, separators] => { - let value = eval_expr(value, context, scope, values)?; - let separators = eval_expr(separators, context, scope, values)?; - eval_ucwords_result(value, Some(separators), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. -fn eval_ucwords_result( - value: RuntimeCellHandle, - separators: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bytes = values.string_bytes(value)?; - let separators = match separators { - Some(separators) => values.string_bytes(separators)?, - None => b" \t\r\n\x0c\x0b".to_vec(), - }; - let mut word_start = true; - for byte in &mut bytes { - if separators.contains(byte) { - word_start = true; - } else if word_start { - if byte.is_ascii_lowercase() { - *byte -= b'a' - b'A'; - } - word_start = false; - } - } - values.string_bytes_value(&bytes) -} - -/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. -pub(super) fn eval_builtin_wordwrap( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_wordwrap_result(value, None, None, None, values) - } - [value, width] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - eval_wordwrap_result(value, Some(width), None, None, values) - } - [value, width, break_string] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - let break_string = eval_expr(break_string, context, scope, values)?; - eval_wordwrap_result(value, Some(width), Some(break_string), None, values) - } - [value, width, break_string, cut] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - let break_string = eval_expr(break_string, context, scope, values)?; - let cut = eval_expr(cut, context, scope, values)?; - eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Wraps a byte string at PHP word boundaries and preserves existing newlines. -fn eval_wordwrap_result( - value: RuntimeCellHandle, - width: Option, - break_string: Option, - cut: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let width = match width { - Some(width) => eval_int_value(width, values)?, - None => 75, - }; - let break_string = match break_string { - Some(break_string) => values.string_bytes(break_string)?, - None => b"\n".to_vec(), - }; - if break_string.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let cut = match cut { - Some(cut) => values.truthy(cut)?, - None => false, - }; - if width == 0 && cut { - return Err(EvalStatus::RuntimeFatal); - } - if bytes.is_empty() { - return values.string_bytes_value(&bytes); - } - let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); - values.string_bytes_value(&output) -} - -/// Applies the core PHP word-wrap scan over already converted byte slices. -fn eval_wordwrap_bytes(bytes: &[u8], width: i64, break_string: &[u8], cut: bool) -> Vec { - if width < 0 && cut { - let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); - for byte in bytes { - output.extend_from_slice(break_string); - output.push(*byte); - } - return output; - } - - let width = width.max(0) as usize; - let mut output = Vec::with_capacity(bytes.len()); - let mut line_start = 0; - let mut last_space = None; - let mut index = 0; - while index < bytes.len() { - match bytes[index] { - b'\n' => { - output.extend_from_slice(&bytes[line_start..=index]); - index += 1; - line_start = index; - last_space = None; - } - b' ' => { - if index.saturating_sub(line_start) >= width { - output.extend_from_slice(&bytes[line_start..index]); - output.extend_from_slice(break_string); - index += 1; - line_start = index; - last_space = None; - } else { - last_space = Some(index); - index += 1; - } - } - _ if index.saturating_sub(line_start) >= width => { - if let Some(space) = last_space { - output.extend_from_slice(&bytes[line_start..space]); - output.extend_from_slice(break_string); - line_start = space + 1; - last_space = None; - } else if cut && width > 0 { - output.extend_from_slice(&bytes[line_start..index]); - output.extend_from_slice(break_string); - line_start = index; - } else { - index += 1; - } - } - _ => { - index += 1; - } - } - } - output.extend_from_slice(&bytes[line_start..]); - output -} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/access.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/access.rs new file mode 100644 index 0000000000..018f0d695d --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/access.rs @@ -0,0 +1,487 @@ +//! Purpose: +//! Array key lookup, search, random, range, merge, explode, and implode helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Array cells remain opaque runtime handles and are manipulated through +//! `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `array_key_exists()` over a key and array expression. +pub(in crate::interpreter) fn eval_builtin_array_key_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [key, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let key = eval_expr(key, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + values.array_key_exists(key, array) +} + +/// Evaluates PHP array search builtins over a needle and haystack expression. +pub(in crate::interpreter) fn eval_builtin_array_search( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let needle = eval_expr(needle, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_array_search_result(name, needle, array, values) +} + +/// Searches an eval array with PHP's default loose comparison semantics. +pub(in crate::interpreter) fn eval_array_search_result( + name: &str, + needle: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let equal = values.compare(EvalBinOp::LooseEq, needle, value)?; + if values.truthy(equal)? { + return match name { + "in_array" => values.bool_value(true), + "array_search" => Ok(key), + _ => Err(EvalStatus::UnsupportedConstruct), + }; + } + } + match name { + "in_array" => values.bool_value(false), + "array_search" => values.bool_value(false), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP value-set array builtins over two eval array expressions. +pub(in crate::interpreter) fn eval_builtin_array_value_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_value_set_result(name, left, right, values) +} + +/// Builds `array_diff()` or `array_intersect()` using PHP's default string comparison mode. +pub(in crate::interpreter) fn eval_array_value_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let mut right_values = Vec::with_capacity(right_len); + for position in 0..right_len { + let key = values.array_iter_key(right, position)?; + let value = values.array_get(right, key)?; + right_values.push(values.string_bytes(value)?); + } + + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let comparable = values.string_bytes(value)?; + let found = right_values + .iter() + .any(|right_value| right_value == &comparable); + let keep = match name { + "array_diff" => !found, + "array_intersect" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Evaluates PHP key-set array builtins over two eval array expressions. +pub(in crate::interpreter) fn eval_builtin_array_key_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_key_set_result(name, left, right, values) +} + +/// Builds `array_diff_key()` or `array_intersect_key()` by testing first-array keys. +pub(in crate::interpreter) fn eval_array_key_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let exists = values.array_key_exists(key, right)?; + let found = values.truthy(exists)?; + let keep = match name { + "array_diff_key" => !found, + "array_intersect_key" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Evaluates PHP `array_rand()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_rand_result(array, values) +} + +/// Returns a valid random key from a non-empty eval array. +pub(in crate::interpreter) fn eval_array_rand_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if len == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let position = eval_random_position(len); + values.array_iter_key(array, position) +} + +/// Chooses a pseudo-random array position within `[0, len)`. +pub(in crate::interpreter) fn eval_random_position(len: usize) -> usize { + (eval_random_u128() % (len as u128)) as usize +} + +/// Produces a process-local pseudo-random word for non-cryptographic eval builtins. +pub(in crate::interpreter) fn eval_random_u128() -> u128 { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + let counter = u128::from(EVAL_RANDOM_COUNTER.fetch_add(1, Ordering::Relaxed)); + let pid = u128::from(std::process::id()); + let mut value = nanos ^ (counter.wrapping_mul(0x9e37_79b9_7f4a_7c15)) ^ (pid << 64); + value ^= value >> 30; + value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); + value ^= value >> 27; + value = value.wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} + +/// Evaluates PHP `rand()` and `mt_rand()` over zero args or an inclusive range. +pub(in crate::interpreter) fn eval_builtin_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_rand_result(None, None, values), + [min, max] => { + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_rand_result(Some(min), Some(max), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `random_int()` over an inclusive integer range. +pub(in crate::interpreter) fn eval_builtin_random_int( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_random_int_result(min, max, values) +} + +/// Returns one non-cryptographic random integer using PHP's inclusive range rules. +pub(in crate::interpreter) fn eval_rand_result( + min: Option, + max: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (min, max) = match (min, max) { + (None, None) => (0, i64::from(i32::MAX)), + (Some(min), Some(max)) => (eval_int_value(min, values)?, eval_int_value(max, values)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let low = min.min(max); + let high = min.max(max); + let width = (i128::from(high) - i128::from(low) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(low) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) +} + +/// Returns one eval `random_int()` value in the inclusive range `[min, max]`. +pub(in crate::interpreter) fn eval_random_int_result( + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let min = eval_int_value(min, values)?; + let max = eval_int_value(max, values)?; + if min > max { + return Err(EvalStatus::RuntimeFatal); + } + let width = (i128::from(max) - i128::from(min) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(min) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) +} + +/// Evaluates PHP `range()` over integer-compatible start and end expressions. +pub(in crate::interpreter) fn eval_builtin_range( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, end] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let end = eval_expr(end, context, scope, values)?; + eval_range_result(start, end, values) +} + +/// Builds an inclusive ascending or descending integer `range()` result. +pub(in crate::interpreter) fn eval_range_result( + start: RuntimeCellHandle, + end: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let end = eval_int_value(end, values)?; + let distance = if start <= end { + end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? + } else { + start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? + }; + let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let step = if start <= end { 1_i64 } else { -1_i64 }; + let mut current = start; + let mut result = values.array_new(count)?; + + for index in 0..count { + let key = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + let value = values.int(current)?; + result = values.array_set(result, key, value)?; + if index + 1 < count { + current = current.checked_add(step).ok_or(EvalStatus::RuntimeFatal)?; + } + } + Ok(result) +} + +/// Evaluates PHP `array_merge()` over two array expressions. +pub(in crate::interpreter) fn eval_builtin_array_merge( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_merge_result(left, right, values) +} + +/// Builds an `array_merge()` result with PHP numeric reindexing and string-key overwrites. +pub(in crate::interpreter) fn eval_array_merge_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let capacity = left_len + .checked_add(right_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut result = values.assoc_new(capacity)?; + let mut next_numeric_key = 0_i64; + result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; + eval_array_merge_append_operand(result, right, &mut next_numeric_key, values) +} + +/// Appends one source array to an `array_merge()` result using PHP key handling. +pub(in crate::interpreter) fn eval_array_merge_append_operand( + mut result: RuntimeCellHandle, + source: RuntimeCellHandle, + next_numeric_key: &mut i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(source)?; + for position in 0..len { + let source_key = values.array_iter_key(source, position)?; + let source_value = values.array_get(source, source_key)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_STRING { + source_key + } else { + let target_key = values.int(*next_numeric_key)?; + *next_numeric_key = (*next_numeric_key) + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + target_key + }; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + +/// Evaluates PHP `explode()` over separator and string expressions. +pub(in crate::interpreter) fn eval_builtin_explode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, string] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let string = eval_expr(string, context, scope, values)?; + eval_explode_result(separator, string, values) +} + +/// Splits one PHP byte string into an indexed array using a non-empty separator. +pub(in crate::interpreter) fn eval_explode_result( + separator: RuntimeCellHandle, + string: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let separator = values.string_bytes(separator)?; + if separator.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let string = values.string_bytes(string)?; + let mut result = values.array_new(0)?; + let mut start = 0; + let mut index = 0_i64; + while let Some(found) = eval_find_subslice(&string, &separator, start) { + result = eval_push_explode_segment(result, index, &string[start..found], values)?; + start = found + separator.len(); + index += 1; + } + eval_push_explode_segment(result, index, &string[start..], values) +} + +/// Appends one split segment to an indexed `explode()` result array. +pub(in crate::interpreter) fn eval_push_explode_segment( + array: RuntimeCellHandle, + index: i64, + segment: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(index)?; + let value = values.string_bytes_value(segment)?; + values.array_set(array, key, value) +} + +/// Finds `needle` inside `haystack` starting from one byte offset. +pub(in crate::interpreter) fn eval_find_subslice( + haystack: &[u8], + needle: &[u8], + start: usize, +) -> Option { + haystack + .get(start..)? + .windows(needle.len()) + .position(|window| window == needle) + .map(|position| position + start) +} + +/// Evaluates PHP `implode()` over separator and array expressions. +pub(in crate::interpreter) fn eval_builtin_implode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_implode_result(separator, array, values) +} + +/// Joins array values in eval iteration order using PHP string conversion. +pub(in crate::interpreter) fn eval_implode_result( + separator: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let separator = values.string_bytes(separator)?; + let len = values.array_len(array)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.extend_from_slice(&separator); + } + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let value = values.string_bytes(value)?; + output.extend_from_slice(&value); + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/core.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/core.rs new file mode 100644 index 0000000000..99eb6332cf --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/core.rs @@ -0,0 +1,402 @@ +//! Purpose: +//! Core non-mutating array builtins such as aggregate, fill, map, reduce, and walk. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Array cells remain opaque runtime handles and are manipulated through +//! `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +pub(in crate::interpreter) fn eval_builtin_abs( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.abs(value) +} + +/// Evaluates PHP array aggregate builtins over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_aggregate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_aggregate_result(name, array, values) +} + +/// Computes `array_sum()` or `array_product()` through eval's numeric value hooks. +pub(in crate::interpreter) fn eval_array_aggregate_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = match name { + "array_sum" => values.int(0)?, + "array_product" => values.int(1)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = match name { + "array_sum" => values.add(result, value)?, + "array_product" => values.mul(result, value)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + } + Ok(result) +} + +/// Evaluates PHP `array_combine()` over key and value array expressions. +pub(in crate::interpreter) fn eval_builtin_array_combine( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, values_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let values_array = eval_expr(values_array, context, scope, values)?; + eval_array_combine_result(keys, values_array, values) +} + +/// Builds the associative result for `array_combine()` from two eval arrays. +pub(in crate::interpreter) fn eval_array_combine_result( + keys: RuntimeCellHandle, + values_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + if len != values.array_len(values_array)? { + return Err(EvalStatus::RuntimeFatal); + } + + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + let target_key = values.cast_string(target_key)?; + let value_key = values.array_iter_key(values_array, position)?; + let value = values.array_get(values_array, value_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_column()` over row-array and column-key expressions. +pub(in crate::interpreter) fn eval_builtin_array_column( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, column_key] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let column_key = eval_expr(column_key, context, scope, values)?; + eval_array_column_result(array, column_key, values) +} + +/// Builds `array_column()` by extracting present row columns into a reindexed array. +pub(in crate::interpreter) fn eval_array_column_result( + array: RuntimeCellHandle, + column_key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + let mut output_index = 0_i64; + for position in 0..len { + let row_key = values.array_iter_key(array, position)?; + let row = values.array_get(array, row_key)?; + if !matches!(values.type_tag(row)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + continue; + } + let exists = values.array_key_exists(column_key, row)?; + if !values.truthy(exists)? { + continue; + } + let column = values.array_get(row, column_key)?; + let target_key = values.int(output_index)?; + output_index = output_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, column)?; + } + Ok(result) +} + +/// Evaluates PHP `array_fill()` over start, count, and value expressions. +pub(in crate::interpreter) fn eval_builtin_array_fill( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, count, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let count = eval_expr(count, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_result(start, count, value, values) +} + +/// Builds an `array_fill()` result with PHP's explicit integer key range. +pub(in crate::interpreter) fn eval_array_fill_result( + start: RuntimeCellHandle, + count: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let count = eval_int_value(count, values)?; + if count < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = if start == 0 { + values.array_new(count)? + } else { + values.assoc_new(count)? + }; + for offset in 0..count { + let offset = i64::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = start.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_fill_keys()` over key-array and value expressions. +pub(in crate::interpreter) fn eval_builtin_array_fill_keys( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_keys_result(keys, value, values) +} + +/// Builds an `array_fill_keys()` result preserving the source key iteration order. +pub(in crate::interpreter) fn eval_array_fill_keys_result( + keys: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_map()` for one source array and a string or null callback. +pub(in crate::interpreter) fn eval_builtin_array_map( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, arrays)) = args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let mut evaluated_arrays = Vec::with_capacity(arrays.len()); + for array in arrays { + evaluated_arrays.push(eval_expr(array, context, scope, values)?); + } + eval_array_map_result(callback, &evaluated_arrays, context, values) +} + +/// Maps one eval array with PHP key preservation for the one-array form. +pub(in crate::interpreter) fn eval_array_map_result( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = arrays else { + return eval_array_map_variadic_result(callback, arrays, context, values); + }; + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_name(callback, values)?) + }; + let len = values.array_len(*array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(*array, position)?; + let value = values.array_get(*array, key)?; + let mapped = if let Some(callback) = callback.as_deref() { + eval_callable_with_values(callback, vec![value], context, values)? + } else { + value + }; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Maps multiple eval arrays with PHP's reindexed and null-padded variadic behavior. +pub(in crate::interpreter) fn eval_array_map_variadic_result( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if arrays.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_name(callback, values)?) + }; + let mut lengths = Vec::with_capacity(arrays.len()); + let mut max_len = 0; + for array in arrays { + let len = values.array_len(*array)?; + max_len = max_len.max(len); + lengths.push(len); + } + + let mut result = values.array_new(max_len)?; + for position in 0..max_len { + let mut callback_args = Vec::with_capacity(arrays.len()); + for (array, len) in arrays.iter().zip(lengths.iter()) { + let value = if position < *len { + let key = values.array_iter_key(*array, position)?; + values.array_get(*array, key)? + } else { + values.null()? + }; + callback_args.push(value); + } + let mapped = if let Some(callback) = callback.as_deref() { + eval_callable_with_values(callback, callback_args, context, values)? + } else { + eval_array_map_zipped_row(callback_args, values)? + }; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Builds one row for `array_map(null, $a, $b, ...)`. +pub(in crate::interpreter) fn eval_array_map_zipped_row( + values_row: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut row = values.array_new(values_row.len())?; + for (index, value) in values_row.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + row = values.array_set(row, key, value)?; + } + Ok(row) +} + +/// Evaluates PHP `array_reduce()` with an optional initial carry value. +pub(in crate::interpreter) fn eval_builtin_array_reduce( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, callback, initial) = match args { + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + (array, callback, values.null()?) + } + [array, callback, initial] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let initial = eval_expr(initial, context, scope, values)?; + (array, callback, initial) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_array_reduce_result(array, callback, initial, context, values) +} + +/// Reduces one eval array by invoking a string callback with carry and item cells. +pub(in crate::interpreter) fn eval_array_reduce_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let len = values.array_len(array)?; + let mut carry = initial; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + carry = eval_callable_with_values(&callback, vec![carry, value], context, values)?; + } + Ok(carry) +} + +/// Evaluates PHP `array_walk()` for side-effect callbacks over value/key pairs. +pub(in crate::interpreter) fn eval_builtin_array_walk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_walk_result(array, callback, context, values) +} + +/// Walks one eval array by invoking a string callback with value and key cells. +pub(in crate::interpreter) fn eval_array_walk_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let _ = eval_callable_with_values(&callback, vec![value, key], context, values)?; + } + values.bool_value(true) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters.rs new file mode 100644 index 0000000000..60fa01aa1e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters.rs @@ -0,0 +1,657 @@ +//! Purpose: +//! Array filter, chunk, slice, pad, projection, iterator, and reverse helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Array cells remain opaque runtime handles and are manipulated through +//! `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `array_filter()` for null and string-callback filtering modes. +pub(in crate::interpreter) fn eval_builtin_array_filter( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_filter_result(array, None, None, context, values) + } + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_filter_result(array, Some(callback), None, context, values) + } + [array, callback, mode] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_array_filter_result(array, Some(callback), Some(mode), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Filters eval array entries through PHP truthiness or a string callback. +pub(in crate::interpreter) fn eval_array_filter_result( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = match callback { + Some(callback) if !values.is_null(callback)? => Some(eval_callable_name(callback, values)?), + _ => None, + }; + let mode = match mode { + Some(mode) => eval_array_filter_mode_value(mode, values)?, + None => EVAL_ARRAY_FILTER_USE_VALUE, + }; + + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let keep = if let Some(callback) = callback.as_deref() { + let args = eval_array_filter_callback_args(mode, key, value)?; + let result = eval_callable_with_values(callback, args, context, values)?; + values.truthy(result)? + } else { + values.truthy(value)? + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Reads and validates the optional `array_filter()` callback mode. +pub(in crate::interpreter) fn eval_array_filter_mode_value( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = eval_int_value(mode, values)?; + match mode { + EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { + Ok(mode) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the callback argument list for one `array_filter()` entry. +pub(in crate::interpreter) fn eval_array_filter_callback_args( + mode: i64, + key: RuntimeCellHandle, + value: RuntimeCellHandle, +) -> Result, EvalStatus> { + match mode { + EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), + EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), + EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. +pub(in crate::interpreter) fn eval_builtin_array_chunk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_chunk_result(array, length, values) +} + +/// Builds an `array_chunk()` result as nested reindexed arrays. +pub(in crate::interpreter) fn eval_array_chunk_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let chunk_size = eval_int_value(length, values)?; + if chunk_size <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; + let len = values.array_len(array)?; + let chunk_count = len.div_ceil(chunk_size); + let mut result = values.array_new(chunk_count)?; + + for chunk_index in 0..chunk_count { + let start = chunk_index * chunk_size; + let end = usize::min(start + chunk_size, len); + let mut chunk = values.array_new(end - start)?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let value = values.array_get(array, source_key)?; + let target_index = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_index = values.int(target_index)?; + chunk = values.array_set(chunk, target_index, value)?; + } + let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let result_key = values.int(result_key)?; + result = values.array_set(result, result_key, chunk)?; + } + + Ok(result) +} + +/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. +pub(in crate::interpreter) fn eval_builtin_array_slice( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array, offset] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_array_slice_result(array, offset, None, values) + } + [array, offset, length] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_slice_result(array, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_slice()` result with PHP offset and length bounds. +pub(in crate::interpreter) fn eval_array_slice_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + + let mut result = values.array_new(end.saturating_sub(start))?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + +/// Converts a PHP array-slice offset into a bounded source position. +pub(in crate::interpreter) fn eval_slice_start( + len: usize, + offset: i64, +) -> Result { + if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(offset, len)); + } + + let tail = offset + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(len.saturating_sub(tail)) +} + +/// Converts a PHP array-slice length into a bounded exclusive end position. +pub(in crate::interpreter) fn eval_slice_end( + len: usize, + start: usize, + length: i64, +) -> Result { + if length >= 0 { + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(start.saturating_add(length), len)); + } + + let tail = length + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(usize::max(start, len.saturating_sub(tail))) +} + +/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. +pub(in crate::interpreter) fn eval_builtin_array_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_pad_result(array, length, value, values) +} + +/// Builds an `array_pad()` result by copying values and padding left or right. +pub(in crate::interpreter) fn eval_array_pad_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let target = eval_int_value(length, values)?; + let target_len = target + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + let result_len = usize::max(len, target_len); + let pad_count = result_len.saturating_sub(len); + let mut result = values.array_new(result_len)?; + let mut output_index = 0usize; + + if target < 0 { + let (padded, next_index) = + eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; + result = padded; + output_index = next_index; + } + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + output_index += 1; + } + + if target > 0 { + result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; + } + + Ok(result) +} + +/// Appends the same pad value at consecutive indexed positions in an array result. +pub(in crate::interpreter) fn eval_array_pad_append_repeated( + mut array: RuntimeCellHandle, + start_index: usize, + count: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, usize), EvalStatus> { + let mut next_index = start_index; + for _ in 0..count { + let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + array = values.array_set(array, key, value)?; + next_index += 1; + } + Ok((array, next_index)) +} + +/// Evaluates PHP `array_flip()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_flip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_flip_result(array, values) +} + +/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. +pub(in crate::interpreter) fn eval_array_flip_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { + continue; + } + result = values.array_set(result, value, key)?; + } + Ok(result) +} + +/// Evaluates PHP `array_unique()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_unique( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_unique_result(array, values) +} + +/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. +pub(in crate::interpreter) fn eval_array_unique_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut seen = Vec::>::with_capacity(len); + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let unique_key = values.string_bytes(value)?; + if seen.iter().any(|seen_key| seen_key == &unique_key) { + continue; + } + seen.push(unique_key); + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP array projection builtins over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_projection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_projection_result(name, array, values) +} + +/// Builds the indexed result array for `array_keys()` or `array_values()`. +pub(in crate::interpreter) fn eval_array_projection_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = match name { + "array_keys" => key, + "array_values" => values.array_get(array, key)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let index = values.int(position as i64)?; + result = values.array_set(result, index, value)?; + } + Ok(result) +} + +/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_apply( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator, callback] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable(callback, values)?; + eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, callback_args] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable(callback, values)?; + let callback_args = eval_expr(callback_args, context, scope, values)?; + let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; + eval_iterator_apply_result(iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts the optional `iterator_apply()` callback-args value into call arguments. +pub(in crate::interpreter) fn eval_iterator_apply_arg_values( + args: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(args)? { + return Ok(Vec::new()); + } + if !values.is_array_like(args)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_array_call_arg_values(args, values) +} + +/// Applies a callback to each valid position of an eval-supported Traversable object. +pub(in crate::interpreter) fn eval_iterator_apply_result( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(iterator)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let count = match eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + ) { + Ok(count) => count, + Err(EvalStatus::UnsupportedConstruct) => { + let iterator = values.method_call(iterator, "getiterator", Vec::new())?; + eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + )? + } + Err(err) => return Err(err), + }; + values.int(count) +} + +/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. +pub(in crate::interpreter) fn eval_iterator_apply_iterator_object( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.method_call(iterator, "rewind", Vec::new())?; + let mut count = 0_i64; + loop { + let valid = values.method_call(iterator, "valid", Vec::new())?; + if !values.truthy(valid)? { + return Ok(count); + } + count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let result = eval_evaluated_callable_with_call_array_args( + callback, + callback_args.to_vec(), + context, + values, + )?; + if !values.truthy(result)? { + return Ok(count); + } + let _ = values.method_call(iterator, "next", Vec::new())?; + } +} + +/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [iterator] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_count_result(iterator, values) +} + +/// Returns the element count for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_iterator_count_result( + iterator: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(iterator)?; + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_to_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator] => { + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_to_array_result(iterator, true, values) + } + [iterator, preserve_keys] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_iterator_to_array_result(iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies eval-supported array iterator inputs into a PHP array result. +pub(in crate::interpreter) fn eval_iterator_to_array_result( + iterator: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + if preserve_keys { + return eval_array_copy_preserve_keys(iterator, values); + } + eval_array_projection_result("array_values", iterator, values) +} + +/// Copies one array-like eval value while preserving iteration keys and order. +pub(in crate::interpreter) fn eval_array_copy_preserve_keys( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `array_reverse()` over an eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_reverse( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_reverse_result(array, false, values) + } + [array, preserve_keys] => { + let array = eval_expr(array, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_array_reverse_result(array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_reverse()` result while preserving PHP key rules. +pub(in crate::interpreter) fn eval_array_reverse_result( + array: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut keys = Vec::with_capacity(len); + let mut has_string_key = false; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; + keys.push(key); + } + + let mut result = if preserve_keys || has_string_key { + values.assoc_new(len)? + } else { + values.array_new(len)? + }; + let mut next_numeric_key = 0_i64; + + for key in keys.into_iter().rev() { + let value = values.array_get(array, key)?; + let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { + key + } else { + let key = values.int(next_numeric_key)?; + next_numeric_key += 1; + key + }; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/mod.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/mod.rs new file mode 100644 index 0000000000..d99c076633 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/mod.rs @@ -0,0 +1,27 @@ +//! Purpose: +//! Groups PHP array and iterator builtins implemented by eval. +//! Submodules separate pure construction, mutating by-reference calls, sorting, +//! splicing, filtering, and key/value access helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins` array-related dispatch. +//! +//! Key details: +//! - Helpers preserve PHP key normalization and use `RuntimeValueOps` for all +//! runtime cell allocation and coercion. + +mod access; +mod core; +mod filters; +mod mutation; +mod push_pop; +mod sort; +mod splice; + +pub(in crate::interpreter) use access::*; +pub(in crate::interpreter) use core::*; +pub(in crate::interpreter) use filters::*; +pub(in crate::interpreter) use mutation::*; +pub(in crate::interpreter) use push_pop::*; +pub(in crate::interpreter) use sort::*; +pub(in crate::interpreter) use splice::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/mutation.rs new file mode 100644 index 0000000000..a645336a9b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/mutation.rs @@ -0,0 +1,224 @@ +//! Purpose: +//! By-reference settype and array mutation dispatch for eval builtin calls. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Array cells remain opaque runtime handles and are manipulated through +//! `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates direct by-reference `settype()` calls and writes the converted cell back. +pub(in crate::interpreter) fn eval_builtin_settype_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (var_name, type_name) = eval_settype_direct_args(args, context, scope, values)?; + let value = visible_scope_cell(context, scope, &var_name).map_or_else(|| values.null(), Ok)?; + let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { + return values.bool_value(false); + }; + for replaced in set_scope_cell( + context, + scope, + var_name, + converted, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + values.bool_value(true) +} + +/// Evaluates and binds direct `settype()` arguments while preserving source order. +pub(in crate::interpreter) fn eval_settype_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(String, RuntimeCellHandle), EvalStatus> { + let mut var_name = None; + let mut type_name = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "var", + 1 => "type", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "var" => { + if var_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + var_name = Some(name.clone()); + } + "type" => { + if type_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + type_name = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let var_name = var_name.ok_or(EvalStatus::RuntimeFatal)?; + let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; + Ok((var_name, type_name)) +} + +/// Applies the eval-supported `settype()` scalar target conversion. +pub(in crate::interpreter) fn eval_settype_cast_value( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let type_name = values.string_bytes(type_name)?; + let type_name = String::from_utf8_lossy(&type_name).to_ascii_lowercase(); + let converted = match type_name.as_str() { + "bool" | "boolean" => Some(values.cast_bool(value)?), + "float" | "double" => Some(values.cast_float(value)?), + "int" | "integer" => Some(values.cast_int(value)?), + "string" => Some(values.cast_string(value)?), + _ => None, + }; + Ok(converted) +} + +/// Evaluates by-value `settype()` callable dispatch without mutating the source argument. +pub(in crate::interpreter) fn eval_settype_value_result( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.warning("settype(): Argument #1 ($var) must be passed by reference, value given")?; + if let Some(converted) = eval_settype_cast_value(value, type_name, values)? { + values.release(converted)?; + return values.bool_value(true); + } + values.bool_value(false) +} + +/// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. +pub(in crate::interpreter) fn eval_builtin_array_pop_shift_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "settype" { + return eval_builtin_settype_call(args, context, scope, values); + } + if matches!(name, "array_push" | "array_unshift") { + return eval_builtin_array_push_unshift_call(name, args, context, scope, values); + } + if name == "array_splice" { + return eval_builtin_array_splice_call(args, context, scope, values); + } + if matches!( + name, + "arsort" + | "asort" + | "krsort" + | "ksort" + | "natcasesort" + | "natsort" + | "rsort" + | "shuffle" + | "sort" + ) { + return eval_builtin_array_sort_call(name, args, context, scope, values); + } + if matches!(name, "uasort" | "uksort" | "usort") { + return eval_builtin_user_sort_call(name, args, context, scope, values); + } + + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(var_name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + let Some(entry) = + scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; + for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates direct by-reference `array_push()` / `array_unshift()` calls. +pub(in crate::interpreter) fn eval_builtin_array_push_unshift_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 || !eval_call_args_are_plain_positional(args) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(var_name) = args[0].value() else { + return Err(EvalStatus::RuntimeFatal); + }; + let mut inserted = Vec::with_capacity(args.len() - 1); + for arg in &args[1..] { + inserted.push(eval_expr(arg.value(), context, scope, values)?); + } + let Some(entry) = + scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; + let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; + for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/push_pop.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/push_pop.rs new file mode 100644 index 0000000000..446f8f3400 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/push_pop.rs @@ -0,0 +1,312 @@ +//! Purpose: +//! array_pop, array_shift, array_push, and array_unshift replacement helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Array cells remain opaque runtime handles and are manipulated through +//! `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. +pub(in crate::interpreter) fn eval_array_pop_shift_value_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + if len == 0 { + return values.null(); + } + let position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let key = values.array_iter_key(array, position)?; + values.array_get(array, key) +} + +/// Builds the return value plus replacement array for direct pop/shift write-back. +pub(in crate::interpreter) fn eval_array_pop_shift_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let len = values.array_len(array)?; + let tag = values.type_tag(array)?; + if len == 0 { + let replacement = match tag { + EVAL_TAG_ARRAY => values.array_new(0)?, + EVAL_TAG_ASSOC => values.assoc_new(0)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + return Ok((values.null()?, replacement)); + } + + let removed_position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let removed_key = values.array_iter_key(array, removed_position)?; + let removed_value = values.array_get(array, removed_key)?; + let replacement = match tag { + EVAL_TAG_ARRAY => { + eval_array_pop_shift_indexed_replacement(array, removed_position, len, values)? + } + EVAL_TAG_ASSOC => { + eval_array_pop_shift_assoc_replacement(name, array, removed_position, len, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + Ok((removed_value, replacement)) +} + +/// Rebuilds an indexed array after removing one position and reindexing values. +pub(in crate::interpreter) fn eval_array_pop_shift_indexed_replacement( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(len.saturating_sub(1))?; + let mut target = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after pop/shift, preserving PHP key behavior. +pub(in crate::interpreter) fn eval_array_pop_shift_assoc_replacement( + name: &str, + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "array_shift" + && eval_array_remaining_keys_are_int(array, removed_position, len, values)? + { + return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); + } + + let mut result = values.assoc_new(len.saturating_sub(1))?; + let mut next_int_key = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every remaining key is an integer after removing one element. +pub(in crate::interpreter) fn eval_array_remaining_keys_are_int( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if position == removed_position { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Returns the resulting element count for by-value push/unshift dynamic calls. +pub(in crate::interpreter) fn eval_array_push_unshift_count_result( + array: RuntimeCellHandle, + inserted_len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let total = values + .array_len(array)? + .checked_add(inserted_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let total = i64::try_from(total).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(total) +} + +/// Builds the replacement array for direct push/unshift write-back. +pub(in crate::interpreter) fn eval_array_push_unshift_replacement( + name: &str, + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, values.type_tag(array)?) { + ("array_push", EVAL_TAG_ARRAY) => { + eval_array_push_indexed_replacement(array, inserted, values) + } + ("array_push", EVAL_TAG_ASSOC) => { + eval_array_push_assoc_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ARRAY) => { + eval_array_unshift_indexed_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ASSOC) => { + eval_array_unshift_assoc_replacement(array, inserted, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Rebuilds an indexed array after appending values. +pub(in crate::interpreter) fn eval_array_push_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = + values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, target_key, value)?; + } + for (offset, value) in inserted.iter().copied().enumerate() { + let position = len.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after appending values at PHP's next integer keys. +pub(in crate::interpreter) fn eval_array_push_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_key = 0_i64; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? == EVAL_TAG_INT { + next_key = next_key.max(eval_int_value(key, values)?.saturating_add(1)); + } + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + for value in inserted.iter().copied() { + let key = values.int(next_key)?; + next_key = next_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an indexed array after prepending values and reindexing the original tail. +pub(in crate::interpreter) fn eval_array_unshift_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + let mut target = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after prepending values and reindexing integer keys. +pub(in crate::interpreter) fn eval_array_unshift_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if eval_array_keys_are_int(array, len, values)? { + return eval_array_unshift_indexed_replacement(array, inserted, values); + } + + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_int_key = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in the array is integer-shaped. +pub(in crate::interpreter) fn eval_array_keys_are_int( + array: RuntimeCellHandle, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/sort.rs new file mode 100644 index 0000000000..7593d2996e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/sort.rs @@ -0,0 +1,598 @@ +//! Purpose: +//! Array and user-sort implementations with PHP key-preservation rules. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Array cells remain opaque runtime handles and are manipulated through +//! `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates direct by-reference array ordering calls and writes back the array. +pub(in crate::interpreter) fn eval_builtin_array_sort_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array_name = eval_array_sort_direct_arg(args)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_array_sort_replacement(name, array, values)?; + let result = values.bool_value(true)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates direct by-reference user-comparator sort calls and writes back the array. +pub(in crate::interpreter) fn eval_builtin_user_sort_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array_name, callback) = eval_user_sort_direct_args(args, context, scope, values)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + let result = values.bool_value(true)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates and binds direct user-sort arguments while preserving source order. +pub(in crate::interpreter) fn eval_user_sort_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(String, RuntimeCellHandle), EvalStatus> { + let mut array = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + array = Some(name.clone()); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, callback)) +} + +/// Returns the dynamic callable result for by-value user-comparator sort calls. +pub(in crate::interpreter) fn eval_user_sort_value_result( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + values.release(replacement)?; + values.bool_value(true) +} + +/// One source array entry used by eval user-comparator sort routines. +pub(in crate::interpreter) struct EvalUserSortEntry { + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for user-comparator sort builtins. +pub(in crate::interpreter) fn eval_user_sort_replacement( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let mut entries = eval_user_sort_entries(array, values)?; + eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; + if name == "usort" { + return eval_user_sort_reindex_result(entries, values); + } + eval_user_sort_preserve_key_result(entries, values) +} + +/// Collects source keys and values from one eval array for user sorting. +pub(in crate::interpreter) fn eval_user_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + entries.push(EvalUserSortEntry { source_key, value }); + } + Ok(entries) +} + +/// Sorts entries by repeatedly invoking the PHP comparator callback. +pub(in crate::interpreter) fn eval_user_sort_entries_in_place( + name: &str, + callback: &str, + entries: &mut [EvalUserSortEntry], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for pass in 0..entries.len() { + let upper = entries.len().saturating_sub(pass + 1); + for index in 0..upper { + let comparison = eval_user_sort_compare( + name, + callback, + &entries[index], + &entries[index + 1], + context, + values, + )?; + if comparison > 0 { + entries.swap(index, index + 1); + } + } + } + Ok(()) +} + +/// Invokes one user-sort comparator and returns its integer ordering result. +pub(in crate::interpreter) fn eval_user_sort_compare( + name: &str, + callback: &str, + left: &EvalUserSortEntry, + right: &EvalUserSortEntry, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = if name == "uksort" { + vec![left.source_key, right.source_key] + } else { + vec![left.value, right.value] + }; + let result = eval_callable_with_values(callback, args, context, values)?; + eval_int_value(result, values) +} + +/// Builds the reindexed result for `usort()`. +pub(in crate::interpreter) fn eval_user_sort_reindex_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds the key-preserving result for `uksort()` and `uasort()`. +pub(in crate::interpreter) fn eval_user_sort_preserve_key_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + +/// Extracts the direct variable argument accepted by eval array ordering builtins. +pub(in crate::interpreter) fn eval_array_sort_direct_arg( + args: &[EvalCallArg], +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + Ok(name.clone()) +} + +/// Returns the dynamic callable result for by-value array ordering calls. +pub(in crate::interpreter) fn eval_array_sort_value_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true) +} + +/// Sort key shape supported by eval's homogeneous array ordering implementation. +#[derive(Clone)] +pub(in crate::interpreter) enum EvalArraySortKey { + Numeric(f64), + Natural(Vec), + String(Vec), +} + +/// One source array entry plus its precomputed ordering key. +pub(in crate::interpreter) struct EvalArraySortEntry { + sort_key: EvalArraySortKey, + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for eval array ordering builtins. +pub(in crate::interpreter) fn eval_array_sort_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut entries = match name { + "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, + "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, + "natsort" => eval_array_natural_sort_entries(array, false, values)?, + "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, + "shuffle" => return eval_array_shuffle_replacement(array, values), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + entries.sort_by(|left, right| { + let order = eval_array_sort_key_cmp(&left.sort_key, &right.sort_key); + if matches!(name, "arsort" | "krsort" | "rsort") { + order.reverse() + } else { + order + } + }); + + if matches!( + name, + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" + ) { + return eval_array_preserve_key_sort_result(entries, values); + } + eval_array_reindex_sort_result(entries, values) +} + +/// Builds a shuffled, reindexed replacement array for `shuffle()`. +pub(in crate::interpreter) fn eval_array_shuffle_replacement( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + entries.push(values.array_get(array, source_key)?); + } + + for index in (1..entries.len()).rev() { + let swap_with = (eval_random_u128() % ((index + 1) as u128)) as usize; + entries.swap(index, swap_with); + } + + let mut result = values.array_new(entries.len())?; + for (index, value) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds an indexed result for `sort()` / `rsort()` after value ordering. +pub(in crate::interpreter) fn eval_array_reindex_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds a key-preserving associative result after value or key ordering. +pub(in crate::interpreter) fn eval_array_preserve_key_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + +/// Collects values and comparable value-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_value_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(value, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and natural-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_natural_sort_entries( + array: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_natural_sort_key(value, case_insensitive, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and comparable key-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_key_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(source_key, values)?; + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Converts one scalar eval value into a natural-sort key. +pub(in crate::interpreter) fn eval_array_natural_sort_key( + value: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } + EVAL_TAG_STRING => { + let mut bytes = values.string_bytes(value)?; + if case_insensitive { + bytes.make_ascii_lowercase(); + } + Ok(EvalArraySortKey::Natural(bytes)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts one scalar eval value into a homogeneous sort key. +pub(in crate::interpreter) fn eval_array_sort_key( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } + EVAL_TAG_STRING => { + let bytes = values.string_bytes(value)?; + match eval_array_numeric_string_sort_key(&bytes) { + Some(value) => Ok(EvalArraySortKey::Numeric(value)), + None => Ok(EvalArraySortKey::String(bytes)), + } + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Parses one PHP numeric string into the numeric sort domain when possible. +pub(in crate::interpreter) fn eval_array_numeric_string_sort_key(bytes: &[u8]) -> Option { + if !eval_is_numeric_string(bytes) { + return None; + } + std::str::from_utf8(bytes).ok()?.parse::().ok() +} + +/// Compares two precomputed eval sort keys. +pub(in crate::interpreter) fn eval_array_sort_key_cmp( + left: &EvalArraySortKey, + right: &EvalArraySortKey, +) -> std::cmp::Ordering { + match (left, right) { + (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { + left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) + } + (EvalArraySortKey::Natural(left), EvalArraySortKey::Natural(right)) => { + eval_natural_bytes_cmp(left, right) + } + (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), + _ => eval_array_sort_key_rank(left).cmp(&eval_array_sort_key_rank(right)), + } +} + +/// Returns a deterministic rank for mixed key-sort domains. +pub(in crate::interpreter) fn eval_array_sort_key_rank(key: &EvalArraySortKey) -> u8 { + match key { + EvalArraySortKey::Numeric(_) => 0, + EvalArraySortKey::Natural(_) => 1, + EvalArraySortKey::String(_) => 2, + } +} + +/// Compares byte strings with a small PHP-style natural ordering. +pub(in crate::interpreter) fn eval_natural_bytes_cmp( + left: &[u8], + right: &[u8], +) -> std::cmp::Ordering { + let mut left_index = 0; + let mut right_index = 0; + while left_index < left.len() && right_index < right.len() { + if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { + let order = eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); + if order != std::cmp::Ordering::Equal { + return order; + } + continue; + } + + let order = left[left_index].cmp(&right[right_index]); + if order != std::cmp::Ordering::Equal { + return order; + } + left_index += 1; + right_index += 1; + } + left.len().cmp(&right.len()) +} + +/// Compares two natural-sort digit runs and advances both byte indexes past them. +pub(in crate::interpreter) fn eval_natural_digit_run_cmp( + left: &[u8], + left_index: &mut usize, + right: &[u8], + right_index: &mut usize, +) -> std::cmp::Ordering { + let left_start = *left_index; + let right_start = *right_index; + while *left_index < left.len() && left[*left_index].is_ascii_digit() { + *left_index += 1; + } + while *right_index < right.len() && right[*right_index].is_ascii_digit() { + *right_index += 1; + } + + let left_digits = &left[left_start..*left_index]; + let right_digits = &right[right_start..*right_index]; + let left_trimmed = eval_trim_leading_zeroes(left_digits); + let right_trimmed = eval_trim_leading_zeroes(right_digits); + left_trimmed + .len() + .cmp(&right_trimmed.len()) + .then_with(|| left_trimmed.cmp(right_trimmed)) + .then_with(|| left_digits.len().cmp(&right_digits.len())) +} + +/// Drops leading zero bytes while keeping one zero for an all-zero digit run. +pub(in crate::interpreter) fn eval_trim_leading_zeroes(digits: &[u8]) -> &[u8] { + let trimmed = digits + .iter() + .position(|digit| *digit != b'0') + .map_or(&digits[digits.len().saturating_sub(1)..], |index| { + &digits[index..] + }); + if trimmed.is_empty() { + digits + } else { + trimmed + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/splice.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/splice.rs new file mode 100644 index 0000000000..dee9c6551a --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/splice.rs @@ -0,0 +1,340 @@ +//! Purpose: +//! array_splice argument handling and replacement construction helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Array cells remain opaque runtime handles and are manipulated through +//! `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates direct by-reference `array_splice()` calls and writes back the array. +pub(in crate::interpreter) fn eval_builtin_array_splice_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array_name, offset, length, replacement_arg) = + eval_array_splice_direct_args(args, context, scope, values)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let (removed, replacement) = + eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(removed) +} + +/// Evaluates and binds direct `array_splice()` arguments while preserving source order. +pub(in crate::interpreter) fn eval_array_splice_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut array = None; + let mut offset = None; + let mut length = None; + let mut replacement = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "offset", + 2 => "length", + 3 => "replacement", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + array = Some(name.clone()); + } + "offset" => { + if offset.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + offset = Some(eval_expr(arg.value(), context, scope, values)?); + } + "length" => { + if length.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + length = Some(eval_expr(arg.value(), context, scope, values)?); + } + "replacement" => { + if replacement.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + replacement = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, offset, length, replacement)) +} + +/// Returns the removed elements that `array_splice()` would produce without mutating the source. +pub(in crate::interpreter) fn eval_array_splice_value_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + eval_array_splice_removed(array, start, end, values) +} + +/// Builds both removed and replacement arrays for direct `array_splice()` write-back. +pub(in crate::interpreter) fn eval_array_splice_removed_and_replacement( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + let removed = eval_array_splice_removed(array, start, end, values)?; + let inserted = eval_array_splice_insert_values(replacement, values)?; + let replacement = eval_array_splice_replacement(array, start, end, &inserted, values)?; + Ok((removed, replacement)) +} + +/// Converts splice offset and length cells into bounded source positions. +pub(in crate::interpreter) fn eval_array_splice_bounds( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(usize, usize), EvalStatus> { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + Ok((start, end)) +} + +/// Builds the reindexed/string-key-preserving removed array returned by `array_splice()`. +pub(in crate::interpreter) fn eval_array_splice_removed( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = end.saturating_sub(start); + if eval_array_range_keys_are_int(array, start, end, values)? { + let mut result = values.array_new(len)?; + let mut target = 0_i64; + for position in start..end { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(len)?; + let mut next_int_key = 0_i64; + for position in start..end { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Expands the optional `array_splice()` replacement value into inserted values. +pub(in crate::interpreter) fn eval_array_splice_insert_values( + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(replacement) = replacement else { + return Ok(Vec::new()); + }; + if !matches!( + values.type_tag(replacement)?, + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC + ) { + return Ok(vec![replacement]); + } + + let len = values.array_len(replacement)?; + let mut inserted = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(replacement, position)?; + inserted.push(values.array_get(replacement, key)?); + } + Ok(inserted) +} + +/// Builds the source replacement after removing the requested splice range. +pub(in crate::interpreter) fn eval_array_splice_replacement( + array: RuntimeCellHandle, + start: usize, + end: usize, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let new_len = len + .saturating_sub(end.saturating_sub(start)) + .checked_add(inserted.len()) + .ok_or(EvalStatus::RuntimeFatal)?; + if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { + let mut result = values.array_new(new_len)?; + let mut target = 0_i64; + for position in 0..start { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(new_len)?; + let mut next_int_key = 0_i64; + for position in 0..start { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in one source position range is integer-shaped. +pub(in crate::interpreter) fn eval_array_range_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in start..end { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Returns true when every key outside the removed splice range is integer-shaped. +pub(in crate::interpreter) fn eval_array_splice_remaining_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if (start..end).contains(&position) { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/file_io.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/file_io.rs new file mode 100644 index 0000000000..07805bc15c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/file_io.rs @@ -0,0 +1,414 @@ +//! Purpose: +//! getcwd, file reads/writes, filesize, filetype, stat, and disk-space builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem` re-exports. +//! +//! Key details: +//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. + +use super::super::super::*; +use super::*; + +/// Evaluates PHP `getcwd()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_getcwd( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values) +} + +/// Returns the process current working directory as a boxed PHP string. +pub(in crate::interpreter) fn eval_getcwd_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let cwd = std::env::current_dir().map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(cwd.to_string_lossy().as_ref()) +} + +/// Evaluates one PHP filesystem predicate over an eval expression. +pub(in crate::interpreter) fn eval_builtin_file_probe( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_probe_result(name, filename, values) +} + +/// Computes one local filesystem predicate and returns a PHP boolean. +pub(in crate::interpreter) fn eval_file_probe_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let path = std::path::Path::new(&path); + let result = match name { + "file_exists" => path.exists(), + "is_dir" => path.is_dir(), + "is_executable" => eval_path_is_executable(path), + "is_file" => path.is_file(), + "is_link" => std::fs::symlink_metadata(path) + .map(|metadata| metadata.file_type().is_symlink()) + .unwrap_or(false), + "is_readable" => eval_path_is_readable(path), + "is_writable" | "is_writeable" => eval_path_is_writable(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(result) +} + +/// Evaluates one scalar PHP stat metadata builtin over an eval expression. +pub(in crate::interpreter) fn eval_builtin_file_stat_scalar( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_stat_scalar_result(name, filename, values) +} + +/// Returns scalar stat metadata, using PHP false for failure where native elephc does. +pub(in crate::interpreter) fn eval_file_stat_scalar_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(_) if name == "filemtime" => return values.int(0), + Err(_) => return values.bool_value(false), + }; + match name { + "fileatime" => values.int(metadata.atime()), + "filectime" => values.int(metadata.ctime()), + "filegroup" => values.int(i64::from(metadata.gid())), + "fileinode" => { + values.int(i64::try_from(metadata.ino()).map_err(|_| EvalStatus::RuntimeFatal)?) + } + "filemtime" => values.int(metadata.mtime()), + "fileowner" => values.int(i64::from(metadata.uid())), + "fileperms" => values.int(i64::from(metadata.mode())), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `file_get_contents($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_get_contents_result(filename, values) +} + +/// Reads a local file into a PHP string, or returns false when it cannot be opened. +pub(in crate::interpreter) fn eval_file_get_contents_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(bytes) => values.string_bytes_value(&bytes), + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} + +/// Evaluates PHP `file($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_result(filename, values) +} + +/// Reads one local file and returns an indexed array of line byte strings. +pub(in crate::interpreter) fn eval_file_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let bytes = match std::fs::read(path) { + Ok(bytes) => bytes, + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + }; + eval_file_lines_array(&bytes, values) +} + +/// Splits file payload bytes into runtime array entries, preserving trailing newlines. +pub(in crate::interpreter) fn eval_file_lines_array( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(0)?; + let mut line_start = 0; + let mut line_index = 0; + for (index, byte) in bytes.iter().enumerate() { + if *byte != b'\n' { + continue; + } + result = + eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; + line_start = index + 1; + line_index += 1; + } + if line_start < bytes.len() { + result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; + } + Ok(result) +} + +/// Evaluates PHP `readfile($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_readfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_readfile_result(filename, values) +} + +/// Streams one local file to eval output and returns a byte count, false, or -1. +pub(in crate::interpreter) fn eval_readfile_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let path = std::path::Path::new(&path); + if path.is_dir() { + return values.int(-1); + } + let bytes = match std::fs::read(path) { + Ok(bytes) => bytes, + Err(_) => return values.bool_value(false), + }; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file_put_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_file_put_contents_result(filename, data, values) +} + +/// Writes a PHP string to a local file and returns the written byte count or false. +pub(in crate::interpreter) fn eval_file_put_contents_result( + filename: RuntimeCellHandle, + data: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let data = values.string_bytes(data)?; + match std::fs::write(path, &data) { + Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), + Err(_) => values.bool_value(false), + } +} + +/// Evaluates PHP `filesize($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_filesize( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filesize_result(filename, values) +} + +/// Returns one local file size in bytes, or zero when stat fails. +pub(in crate::interpreter) fn eval_filesize_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let len = std::fs::metadata(path) + .map(|metadata| metadata.len()) + .unwrap_or(0); + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `filetype($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_filetype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filetype_result(filename, values) +} + +/// Returns the PHP filetype string for one path, or false when lstat fails. +pub(in crate::interpreter) fn eval_filetype_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let file_type = match std::fs::symlink_metadata(path) { + Ok(metadata) => metadata.file_type(), + Err(_) => return values.bool_value(false), + }; + let label = if file_type.is_file() { + "file" + } else if file_type.is_dir() { + "dir" + } else if file_type.is_symlink() { + "link" + } else if file_type.is_char_device() { + "char" + } else if file_type.is_block_device() { + "block" + } else if file_type.is_fifo() { + "fifo" + } else if file_type.is_socket() { + "socket" + } else { + "unknown" + }; + values.string(label) +} + +/// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stat_array( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_stat_array_result(name, filename, values) +} + +/// Builds PHP's stat array for one local path, or returns false on stat failure. +pub(in crate::interpreter) fn eval_stat_array_result( + name: &str, + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let metadata = match name { + "stat" => std::fs::metadata(path), + "lstat" => std::fs::symlink_metadata(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let metadata = match metadata { + Ok(metadata) => metadata, + Err(_) => return values.bool_value(false), + }; + eval_stat_metadata_array(&metadata, values) +} + +/// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. +pub(in crate::interpreter) fn eval_stat_metadata_array( + metadata: &std::fs::Metadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let fields = [ + ("dev", eval_u64_to_i64(metadata.dev())?), + ("ino", eval_u64_to_i64(metadata.ino())?), + ("mode", i64::from(metadata.mode())), + ("nlink", eval_u64_to_i64(metadata.nlink())?), + ("uid", i64::from(metadata.uid())), + ("gid", i64::from(metadata.gid())), + ("rdev", eval_u64_to_i64(metadata.rdev())?), + ("size", eval_u64_to_i64(metadata.size())?), + ("atime", metadata.atime()), + ("mtime", metadata.mtime()), + ("ctime", metadata.ctime()), + ("blksize", eval_u64_to_i64(metadata.blksize())?), + ("blocks", eval_u64_to_i64(metadata.blocks())?), + ]; + let mut result = values.assoc_new(fields.len() * 2)?; + for (index, (name, value)) in fields.iter().enumerate() { + result = eval_stat_array_set_int_key(result, index, *value, values)?; + result = eval_stat_array_set_string_key(result, name, *value, values)?; + } + Ok(result) +} + +/// Inserts one integer stat field under a numeric PHP array key. +pub(in crate::interpreter) fn eval_stat_array_set_int_key( + array: RuntimeCellHandle, + key: usize, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(key).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts one integer stat field under a string PHP array key. +pub(in crate::interpreter) fn eval_stat_array_set_string_key( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Converts unsigned stat metadata into the signed integer payload used by PHP cells. +pub(in crate::interpreter) fn eval_u64_to_i64(value: u64) -> Result { + i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/fnmatch.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/fnmatch.rs new file mode 100644 index 0000000000..37550ebbc4 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/fnmatch.rs @@ -0,0 +1,350 @@ +//! Purpose: +//! fnmatch implementation and wildcard/class matching helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem` re-exports. +//! +//! Key details: +//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `fnmatch($pattern, $filename, $flags = 0)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fnmatch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, filename] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let filename = eval_expr(filename, context, scope, values)?; + eval_fnmatch_result(pattern, filename, None, values) + } + [pattern, filename, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let filename = eval_expr(filename, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_fnmatch_result(pattern, filename, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Runs PHP-style shell glob matching for one pattern/name pair. +pub(in crate::interpreter) fn eval_fnmatch_result( + pattern: RuntimeCellHandle, + filename: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let filename = values.string_bytes(filename)?; + let flags = match flags { + Some(flags) => eval_int_value(flags, values)?, + None => 0, + }; + values.bool_value(eval_fnmatch_bytes(&pattern, &filename, flags)) +} + +/// Matches byte strings using the eval-supported `fnmatch()` grammar and flags. +pub(in crate::interpreter) fn eval_fnmatch_bytes( + pattern: &[u8], + filename: &[u8], + flags: i64, +) -> bool { + let mut memo = vec![vec![None; filename.len() + 1]; pattern.len() + 1]; + eval_fnmatch_at(pattern, filename, flags, 0, 0, &mut memo) +} + +/// Recursively matches a pattern suffix against a filename suffix with memoization. +pub(in crate::interpreter) fn eval_fnmatch_at( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + if let Some(result) = memo[pattern_index][filename_index] { + return result; + } + let result = if pattern_index == pattern.len() { + filename_index == filename.len() + } else { + match pattern[pattern_index] { + b'*' => eval_fnmatch_star( + pattern, + filename, + flags, + pattern_index, + filename_index, + memo, + ), + b'?' => { + eval_fnmatch_single_wildcard(filename, flags, filename_index) + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ) + } + b'[' => eval_fnmatch_class_or_literal( + pattern, + filename, + flags, + pattern_index, + filename_index, + memo, + ), + b'\\' if flags & EVAL_FNM_NOESCAPE == 0 => { + let (literal, next_pattern_index) = + eval_fnmatch_escaped_literal(pattern, pattern_index); + eval_fnmatch_literal(filename, flags, filename_index, literal) + && eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index + 1, + memo, + ) + } + literal => { + eval_fnmatch_literal(filename, flags, filename_index, literal) + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ) + } + } + }; + memo[pattern_index][filename_index] = Some(result); + result +} + +/// Handles `*`, including pathname and leading-period restrictions. +pub(in crate::interpreter) fn eval_fnmatch_star( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + let mut next_pattern_index = pattern_index + 1; + while next_pattern_index < pattern.len() && pattern[next_pattern_index] == b'*' { + next_pattern_index += 1; + } + if eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index, + memo, + ) { + return true; + } + let mut cursor = filename_index; + while cursor < filename.len() && eval_fnmatch_wildcard_can_consume(filename, flags, cursor) { + cursor += 1; + if eval_fnmatch_at(pattern, filename, flags, next_pattern_index, cursor, memo) { + return true; + } + } + false +} + +/// Returns whether `?` can consume the current filename byte. +pub(in crate::interpreter) fn eval_fnmatch_single_wildcard( + filename: &[u8], + flags: i64, + filename_index: usize, +) -> bool { + filename_index < filename.len() + && eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) +} + +/// Handles a bracket class, or falls back to a literal `[` when the class is malformed. +pub(in crate::interpreter) fn eval_fnmatch_class_or_literal( + pattern: &[u8], + filename: &[u8], + flags: i64, + pattern_index: usize, + filename_index: usize, + memo: &mut [Vec>], +) -> bool { + if filename_index >= filename.len() + || !eval_fnmatch_wildcard_can_consume(filename, flags, filename_index) + { + return false; + } + let Some((matches, next_pattern_index)) = + eval_fnmatch_class_matches(pattern, pattern_index + 1, filename[filename_index], flags) + else { + return eval_fnmatch_literal(filename, flags, filename_index, b'[') + && eval_fnmatch_at( + pattern, + filename, + flags, + pattern_index + 1, + filename_index + 1, + memo, + ); + }; + matches + && eval_fnmatch_at( + pattern, + filename, + flags, + next_pattern_index, + filename_index + 1, + memo, + ) +} + +/// Matches one bracket class body against the current filename byte. +pub(in crate::interpreter) fn eval_fnmatch_class_matches( + pattern: &[u8], + mut index: usize, + candidate: u8, + flags: i64, +) -> Option<(bool, usize)> { + let negated = matches!(pattern.get(index).copied(), Some(b'!' | b'^')); + if negated { + index += 1; + } + let mut matched = false; + let mut closed = false; + while index < pattern.len() { + if pattern[index] == b']' { + closed = true; + index += 1; + break; + } + let start = eval_fnmatch_class_char(pattern, &mut index, flags)?; + if index + 1 < pattern.len() && pattern[index] == b'-' && pattern[index + 1] != b']' { + index += 1; + let end = eval_fnmatch_class_char(pattern, &mut index, flags)?; + if eval_fnmatch_byte_in_range(candidate, start, end, flags) { + matched = true; + } + } else if eval_fnmatch_byte_eq(candidate, start, flags) { + matched = true; + } + } + closed.then_some((if negated { !matched } else { matched }, index)) +} + +/// Reads one character from a bracket class, respecting escapes when enabled. +pub(in crate::interpreter) fn eval_fnmatch_class_char( + pattern: &[u8], + index: &mut usize, + flags: i64, +) -> Option { + if *index >= pattern.len() { + return None; + } + if pattern[*index] == b'\\' && flags & EVAL_FNM_NOESCAPE == 0 && *index + 1 < pattern.len() { + *index += 2; + return Some(pattern[*index - 1]); + } + let byte = pattern[*index]; + *index += 1; + Some(byte) +} + +/// Returns whether one candidate byte falls within a possibly case-folded range. +pub(in crate::interpreter) fn eval_fnmatch_byte_in_range( + candidate: u8, + start: u8, + end: u8, + flags: i64, +) -> bool { + let candidate = eval_fnmatch_fold(candidate, flags); + let start = eval_fnmatch_fold(start, flags); + let end = eval_fnmatch_fold(end, flags); + if start <= end { + candidate >= start && candidate <= end + } else { + candidate >= end && candidate <= start + } +} + +/// Reads an escaped literal token outside bracket classes. +pub(in crate::interpreter) fn eval_fnmatch_escaped_literal( + pattern: &[u8], + pattern_index: usize, +) -> (u8, usize) { + if pattern_index + 1 < pattern.len() { + (pattern[pattern_index + 1], pattern_index + 2) + } else { + (b'\\', pattern_index + 1) + } +} + +/// Returns whether one literal pattern byte matches the current filename byte. +pub(in crate::interpreter) fn eval_fnmatch_literal( + filename: &[u8], + flags: i64, + filename_index: usize, + literal: u8, +) -> bool { + filename_index < filename.len() + && eval_fnmatch_byte_eq(filename[filename_index], literal, flags) +} + +/// Returns whether a wildcard token may consume the current filename byte. +pub(in crate::interpreter) fn eval_fnmatch_wildcard_can_consume( + filename: &[u8], + flags: i64, + filename_index: usize, +) -> bool { + if filename_index >= filename.len() { + return false; + } + if flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index] == b'/' { + return false; + } + if flags & EVAL_FNM_PERIOD != 0 + && eval_fnmatch_is_leading_period(filename, flags, filename_index) + { + return false; + } + true +} + +/// Returns whether the current byte is a leading period for `FNM_PERIOD`. +pub(in crate::interpreter) fn eval_fnmatch_is_leading_period( + filename: &[u8], + flags: i64, + filename_index: usize, +) -> bool { + filename[filename_index] == b'.' + && (filename_index == 0 + || (flags & EVAL_FNM_PATHNAME != 0 && filename[filename_index - 1] == b'/')) +} + +/// Compares bytes using ASCII case folding when `FNM_CASEFOLD` is present. +pub(in crate::interpreter) fn eval_fnmatch_byte_eq(left: u8, right: u8, flags: i64) -> bool { + eval_fnmatch_fold(left, flags) == eval_fnmatch_fold(right, flags) +} + +/// Applies eval fnmatch's ASCII case folding. +pub(in crate::interpreter) fn eval_fnmatch_fold(byte: u8, flags: i64) -> u8 { + if flags & EVAL_FNM_CASEFOLD != 0 { + byte.to_ascii_lowercase() + } else { + byte + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs new file mode 100644 index 0000000000..4dd32f5314 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -0,0 +1,19 @@ +//! Purpose: +//! Groups filesystem, path, glob, stat, and fnmatch builtins for eval. +//! +//! Called from: +//! - `crate::interpreter::builtins` filesystem-related dispatch. +//! +//! Key details: +//! - Path arguments are converted through PHP string coercion before touching the +//! host filesystem. + +mod file_io; +mod fnmatch; +mod ops; +mod path; + +pub(in crate::interpreter) use file_io::*; +pub(in crate::interpreter) use fnmatch::*; +pub(in crate::interpreter) use ops::*; +pub(in crate::interpreter) use path::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops.rs new file mode 100644 index 0000000000..42c649db89 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops.rs @@ -0,0 +1,599 @@ +//! Purpose: +//! Directory, chmod, glob, tempnam, touch, umask, link, clearstatcache, and unlink builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem` re-exports. +//! +//! Key details: +//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. +pub(in crate::interpreter) fn eval_builtin_disk_space( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_disk_space_result(name, directory, values) +} + +/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. +pub(in crate::interpreter) fn eval_disk_space_result( + name: &str, + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(directory)?; + let Ok(path) = CString::new(bytes) else { + return values.float(0.0); + }; + let mut stats = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes the statvfs fields for this NUL-terminated local path. + libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) + }; + if status != 0 { + return values.float(0.0); + } + let stats = unsafe { + // `statvfs` succeeded, so libc initialized the full stat buffer. + stats.assume_init() + }; + let block_size = if stats.f_frsize > 0 { + stats.f_frsize + } else { + stats.f_bsize + }; + let blocks = match name { + "disk_free_space" => stats.f_bavail, + "disk_total_space" => stats.f_blocks, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.float((block_size as f64) * (blocks as f64)) +} + +/// Evaluates a one-path filesystem operation that returns a PHP boolean. +pub(in crate::interpreter) fn eval_builtin_unary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_unary_path_bool_result(name, path, values) +} + +/// Executes a one-path local filesystem operation and returns whether it succeeded. +pub(in crate::interpreter) fn eval_unary_path_bool_result( + name: &str, + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let ok = match name { + "chdir" => std::env::set_current_dir(path).is_ok(), + "mkdir" => std::fs::create_dir(path).is_ok(), + "rmdir" => std::fs::remove_dir(path).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Evaluates a two-path filesystem operation that returns a PHP boolean. +pub(in crate::interpreter) fn eval_builtin_binary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [from, to] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let from = eval_expr(from, context, scope, values)?; + let to = eval_expr(to, context, scope, values)?; + eval_binary_path_bool_result(name, from, to, values) +} + +/// Executes a two-path local filesystem operation and returns whether it succeeded. +pub(in crate::interpreter) fn eval_binary_path_bool_result( + name: &str, + from: RuntimeCellHandle, + to: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_path_string(from, values)?; + let to = eval_path_string(to, values)?; + let ok = match name { + "copy" => std::fs::copy(from, to).is_ok(), + "link" => std::fs::hard_link(from, to).is_ok(), + "rename" => std::fs::rename(from, to).is_ok(), + "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_chmod( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, permissions] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let permissions = eval_expr(permissions, context, scope, values)?; + eval_chmod_result(filename, permissions, values) +} + +/// Changes one local file's mode and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_chmod_result( + filename: RuntimeCellHandle, + permissions: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let mode = eval_int_value(permissions, values)? as u32; + let permissions = std::fs::Permissions::from_mode(mode); + values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) +} + +/// Evaluates PHP `scandir($directory)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_scandir( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_scandir_result(directory, values) +} + +/// Lists one local directory into an indexed string array, or an empty array on failure. +pub(in crate::interpreter) fn eval_scandir_result( + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(directory, values)?; + let Ok(entries) = std::fs::read_dir(path) else { + return values.array_new(0); + }; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; + } + Ok(result) +} + +/// Evaluates PHP `glob($pattern)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_glob( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + eval_glob_result(pattern, values) +} + +/// Expands one local glob pattern into a sorted indexed PHP string array. +pub(in crate::interpreter) fn eval_glob_result( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = eval_path_string(pattern, values)?; + let matches = eval_glob_matches(&pattern); + let mut result = values.array_new(matches.len())?; + for (index, path) in matches.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; + } + Ok(result) +} + +/// Collects sorted matches for one local glob pattern. +pub(in crate::interpreter) fn eval_glob_matches(pattern: &str) -> Vec { + if pattern.is_empty() { + return Vec::new(); + } + if !eval_glob_component_has_magic(pattern) { + return std::path::Path::new(pattern) + .exists() + .then(|| pattern.to_string()) + .into_iter() + .collect(); + } + let absolute = pattern.starts_with('/'); + let components: Vec<&str> = pattern + .split('/') + .filter(|component| !component.is_empty()) + .collect(); + let mut matches = Vec::new(); + let base = if absolute { + std::path::PathBuf::from("/") + } else { + std::path::PathBuf::from(".") + }; + let prefix = if absolute { "/" } else { "" }; + eval_glob_collect(&base, prefix, &components, &mut matches); + matches.sort(); + matches +} + +/// Recursively expands one glob path component at a time. +pub(in crate::interpreter) fn eval_glob_collect( + base: &std::path::Path, + prefix: &str, + components: &[&str], + matches: &mut Vec, +) { + let Some((component, rest)) = components.split_first() else { + if base.exists() && !prefix.is_empty() { + matches.push(prefix.to_string()); + } + return; + }; + if !eval_glob_component_has_magic(component) { + let next_base = base.join(component); + if rest.is_empty() { + if next_base.exists() { + matches.push(eval_glob_join_output(prefix, component)); + } + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, component); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + return; + } + let Ok(entries) = std::fs::read_dir(base) else { + return; + }; + let mut names = Vec::new(); + for entry in entries.flatten() { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + for name in names { + if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { + continue; + } + let next_base = base.join(&name); + if rest.is_empty() { + matches.push(eval_glob_join_output(prefix, &name)); + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, &name); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + } +} + +/// Joins a display path prefix and component while preserving absolute-root output. +pub(in crate::interpreter) fn eval_glob_join_output(prefix: &str, component: &str) -> String { + if prefix.is_empty() { + component.to_string() + } else if prefix == "/" { + format!("/{component}") + } else { + format!("{prefix}/{component}") + } +} + +/// Returns whether a glob component contains wildcard syntax. +pub(in crate::interpreter) fn eval_glob_component_has_magic(component: &str) -> bool { + component + .as_bytes() + .iter() + .any(|byte| matches!(byte, b'*' | b'?' | b'[')) +} + +/// Writes one byte-string value into an indexed runtime array at a zero-based position. +pub(in crate::interpreter) fn eval_array_set_indexed_bytes( + array: RuntimeCellHandle, + index: usize, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} + +/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_tempnam( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory, prefix] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + let prefix = eval_expr(prefix, context, scope, values)?; + eval_tempnam_result(directory, prefix, values) +} + +/// Creates a unique local temporary file and returns its path, or an empty string on failure. +pub(in crate::interpreter) fn eval_tempnam_result( + directory: RuntimeCellHandle, + prefix: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + let prefix = values.string_bytes(prefix)?; + let prefix = String::from_utf8_lossy(&prefix); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + for attempt in 0..1000_u32 { + let candidate = + std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&candidate) + { + Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(_) => return values.string(""), + } + } + values.string("") +} + +/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. +pub(in crate::interpreter) fn eval_tempnam_filename( + prefix: &str, + nonce: u128, + attempt: u32, +) -> String { + format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) +} + +/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_touch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [filename] => { + let filename = eval_expr(filename, context, scope, values)?; + eval_touch_result(filename, None, None, values) + } + [filename, mtime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), None, values) + } + [filename, mtime, atime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + let atime = eval_expr(atime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), Some(atime), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates or stamps one local file and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_touch_result( + filename: RuntimeCellHandle, + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let (mtime, atime) = eval_touch_times(mtime, atime, values)?; + let file = match std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(path) + { + Ok(file) => file, + Err(_) => return values.bool_value(false), + }; + let times = std::fs::FileTimes::new() + .set_modified(mtime) + .set_accessed(atime); + values.bool_value(file.set_times(times).is_ok()) +} + +/// Resolves PHP touch timestamp defaults into concrete system times. +pub(in crate::interpreter) fn eval_touch_times( + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { + let now = std::time::SystemTime::now(); + let Some(mtime) = mtime else { + return Ok((now, now)); + }; + if values.is_null(mtime)? { + if let Some(atime) = atime { + if !values.is_null(atime)? { + return Err(EvalStatus::RuntimeFatal); + } + } + return Ok((now, now)); + } + let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + let Some(atime) = atime else { + return Ok((mtime, mtime)); + }; + if values.is_null(atime)? { + return Ok((mtime, mtime)); + } + let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + Ok((mtime, atime)) +} + +/// Converts a Unix timestamp in seconds into a `SystemTime`. +pub(in crate::interpreter) fn eval_system_time_from_unix( + seconds: i64, +) -> Option { + if seconds >= 0 { + std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) + } else { + std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) + } +} + +/// Evaluates PHP `umask($mask = null)` over an optional eval expression. +pub(in crate::interpreter) fn eval_builtin_umask( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_umask_result(None, values), + [mask] => { + let mask = eval_expr(mask, context, scope, values)?; + eval_umask_result(Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `umask()` semantics and returns the previous mask. +pub(in crate::interpreter) fn eval_umask_result( + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let previous = match mask { + Some(mask) => { + let mask = eval_int_value(mask, values)? as u32; + unsafe { umask(mask) } + } + None => unsafe { + let current = umask(0); + umask(current); + current + }, + }; + values.int(i64::from(previous)) +} + +/// Evaluates PHP `readlink($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_readlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_readlink_result(path, values) +} + +/// Reads one symbolic-link target string, or returns PHP false on failure. +pub(in crate::interpreter) fn eval_readlink_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + match std::fs::read_link(path) { + Ok(target) => values.string(target.to_string_lossy().as_ref()), + Err(_) => values.bool_value(false), + } +} + +/// Evaluates PHP `linkinfo($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_linkinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_linkinfo_result(path, values) +} + +/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. +pub(in crate::interpreter) fn eval_linkinfo_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let dev = match std::fs::symlink_metadata(path) { + Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, + Err(_) => -1, + }; + values.int(dev) +} + +/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. +pub(in crate::interpreter) fn eval_builtin_clearstatcache( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + values.null() +} + +/// Evaluates PHP `unlink($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_unlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_unlink_result(filename, values) +} + +/// Deletes one local file and returns whether it succeeded. +pub(in crate::interpreter) fn eval_unlink_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + values.bool_value(std::fs::remove_file(path).is_ok()) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs new file mode 100644 index 0000000000..243e012d2e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs @@ -0,0 +1,341 @@ +//! Purpose: +//! Path conversion and basename, dirname, realpath, and pathinfo helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem` re-exports. +//! +//! Key details: +//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Converts one eval value to a filesystem path string. +pub(in crate::interpreter) fn eval_path_string( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let filename = values.string_bytes(filename)?; + Ok(String::from_utf8_lossy(&filename).into_owned()) +} + +/// Returns whether a path can be opened for reading by the current process. +pub(in crate::interpreter) fn eval_path_is_readable(path: &std::path::Path) -> bool { + std::fs::File::open(path).is_ok() || std::fs::read_dir(path).is_ok() +} + +/// Returns whether a path has any executable bit set in its Unix mode. +pub(in crate::interpreter) fn eval_path_is_executable(path: &std::path::Path) -> bool { + std::fs::metadata(path) + .map(|metadata| metadata.mode() & 0o111 != 0) + .unwrap_or(false) +} + +/// Returns whether a path can be written by the current process. +pub(in crate::interpreter) fn eval_path_is_writable(path: &std::path::Path) -> bool { + if path.is_file() { + return std::fs::OpenOptions::new().write(true).open(path).is_ok(); + } + if !path.is_dir() { + return false; + } + let probe = path.join(format!( + ".elephc_eval_writable_probe_{}", + std::process::id() + )); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&probe) + { + Ok(_) => { + let _ = std::fs::remove_file(probe); + true + } + Err(_) => false, + } +} + +/// Evaluates PHP `basename($path, $suffix = "")` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_basename( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_basename_result(path, None, values) + } + [path, suffix] => { + let path = eval_expr(path, context, scope, values)?; + let suffix = eval_expr(suffix, context, scope, values)?; + eval_basename_result(path, Some(suffix), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `basename()` bytes and returns them as a runtime string. +pub(in crate::interpreter) fn eval_basename_result( + path: RuntimeCellHandle, + suffix: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let suffix = suffix + .map(|suffix| values.string_bytes(suffix)) + .transpose()?; + let result = eval_basename_bytes(&path, suffix.as_deref()); + values.string_bytes_value(&result) +} + +/// Extracts a PHP basename from one path byte string. +pub(in crate::interpreter) fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return Vec::new(); + } + let mut start = end; + while start > 0 && path[start - 1] != b'/' { + start -= 1; + } + let mut result = path[start..end].to_vec(); + if let Some(suffix) = suffix { + if !suffix.is_empty() && suffix.len() < result.len() && result.ends_with(suffix) { + result.truncate(result.len() - suffix.len()); + } + } + result +} + +/// Evaluates PHP `dirname($path, $levels = 1)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_dirname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_dirname_result(path, None, values) + } + [path, levels] => { + let path = eval_expr(path, context, scope, values)?; + let levels = eval_expr(levels, context, scope, values)?; + eval_dirname_result(path, Some(levels), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `dirname()` bytes and returns them as a runtime string. +pub(in crate::interpreter) fn eval_dirname_result( + path: RuntimeCellHandle, + levels: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let levels = match levels { + Some(levels) => eval_int_value(levels, values)?, + None => 1, + }; + if levels < 1 { + return Err(EvalStatus::RuntimeFatal); + } + let mut current = path; + for _ in 0..levels { + current = eval_dirname_once(¤t); + } + values.string_bytes_value(¤t) +} + +/// Applies one PHP `dirname()` parent traversal to a path byte string. +pub(in crate::interpreter) fn eval_dirname_once(path: &[u8]) -> Vec { + if path.is_empty() { + return b".".to_vec(); + } + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return b"/".to_vec(); + } + let mut cursor = end; + while cursor > 0 { + cursor -= 1; + if path[cursor] == b'/' { + let mut parent_end = cursor; + while parent_end > 0 && path[parent_end - 1] == b'/' { + parent_end -= 1; + } + return if parent_end == 0 { + b"/".to_vec() + } else { + path[..parent_end].to_vec() + }; + } + } + b".".to_vec() +} + +/// Evaluates PHP `realpath($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_realpath( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_realpath_result(path, values) +} + +/// Canonicalizes one path or returns PHP false when the path cannot be resolved. +pub(in crate::interpreter) fn eval_realpath_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let path = String::from_utf8_lossy(&path); + let Ok(canonical) = std::fs::canonicalize(path.as_ref()) else { + return values.bool_value(false); + }; + let canonical = canonical.to_string_lossy(); + values.string(canonical.as_ref()) +} + +/// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_pathinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_pathinfo_result(path, None, values) + } + [path, flags] => { + let path = eval_expr(path, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_pathinfo_result(path, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `pathinfo()` as either an associative array or one component string. +pub(in crate::interpreter) fn eval_pathinfo_result( + path: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let Some(flags) = flags else { + return eval_pathinfo_array_result(&path, values); + }; + let flags = eval_int_value(flags, values)?; + if flags == EVAL_PATHINFO_ALL { + return eval_pathinfo_array_result(&path, values); + } + let component = eval_pathinfo_component_bytes(&path, flags); + values.string_bytes_value(&component) +} + +/// Builds the PHP `pathinfo()` associative-array result for all components. +pub(in crate::interpreter) fn eval_pathinfo_array_result( + path: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(4)?; + if !path.is_empty() { + let dirname = eval_pathinfo_dirname_bytes(path); + result = eval_pathinfo_array_set(result, "dirname", &dirname, values)?; + } + let parts = eval_pathinfo_parts(path); + result = eval_pathinfo_array_set(result, "basename", &parts.basename, values)?; + if parts.has_extension { + result = eval_pathinfo_array_set(result, "extension", &parts.extension, values)?; + } + eval_pathinfo_array_set(result, "filename", &parts.filename, values) +} + +/// Inserts one string component into a PHP `pathinfo()` associative result. +pub(in crate::interpreter) fn eval_pathinfo_array_set( + array: RuntimeCellHandle, + key: &str, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} + +/// Returns one PHP `pathinfo()` component for a non-all bitmask. +pub(in crate::interpreter) fn eval_pathinfo_component_bytes(path: &[u8], flags: i64) -> Vec { + if flags & EVAL_PATHINFO_DIRNAME != 0 { + return eval_pathinfo_dirname_bytes(path); + } + let parts = eval_pathinfo_parts(path); + if flags & EVAL_PATHINFO_BASENAME != 0 { + return parts.basename; + } + if flags & EVAL_PATHINFO_EXTENSION != 0 { + return parts.extension; + } + if flags & EVAL_PATHINFO_FILENAME != 0 { + return parts.filename; + } + Vec::new() +} + +/// Computes the dirname component with `pathinfo("")`'s empty-string exception. +pub(in crate::interpreter) fn eval_pathinfo_dirname_bytes(path: &[u8]) -> Vec { + if path.is_empty() { + Vec::new() + } else { + eval_dirname_once(path) + } +} + +/// Splits pathinfo basename, extension, and filename components. +pub(in crate::interpreter) fn eval_pathinfo_parts(path: &[u8]) -> EvalPathInfoParts { + let basename = eval_basename_bytes(path, None); + let Some(dot) = basename.iter().rposition(|byte| *byte == b'.') else { + return EvalPathInfoParts { + filename: basename.clone(), + basename, + extension: Vec::new(), + has_extension: false, + }; + }; + EvalPathInfoParts { + filename: basename[..dot].to_vec(), + extension: basename[dot + 1..].to_vec(), + basename, + has_extension: true, + } +} + +/// Pathinfo components derived from a basename. +pub(in crate::interpreter) struct EvalPathInfoParts { + /// Full basename component. + basename: Vec, + /// Extension component after the final dot, possibly empty for trailing-dot names. + extension: Vec, + /// Filename component before the final dot. + filename: Vec, + /// Whether the basename contained a dot and therefore has an extension key. + has_extension: bool, +} diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting.rs b/crates/elephc-eval/src/interpreter/builtins/formatting.rs new file mode 100644 index 0000000000..e47c18fa5a --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting.rs @@ -0,0 +1,814 @@ +//! Purpose: +//! Numeric formatting, sprintf-family, and math wrapper builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +use super::super::*; +use super::*; + +/// Evaluates PHP's `ceil(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ceil( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.ceil(value) +} + +/// Evaluates PHP's `floor(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_floor( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.floor(value) +} + +/// Evaluates PHP's zero-argument `pi()` builtin. +pub(in crate::interpreter) fn eval_builtin_pi( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI) +} + +/// Evaluates PHP's `pow(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_pow( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + values.pow(left, right) +} + +/// Evaluates PHP's `round(...)` over one value and an optional precision expression. +pub(in crate::interpreter) fn eval_builtin_round( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + values.round(value, None) + } + [value, precision] => { + let value = eval_expr(value, context, scope, values)?; + let precision = eval_expr(precision, context, scope, values)?; + values.round(value, Some(precision)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `number_format(...)` over one number and optional separators. +pub(in crate::interpreter) fn eval_builtin_number_format( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_number_format_result(value, None, None, None, values) + } + [value, decimals] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + eval_number_format_result(value, Some(decimals), None, None, values) + } + [value, decimals, decimal_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + eval_number_format_result(value, Some(decimals), Some(decimal_separator), None, values) + } + [value, decimals, decimal_separator, thousands_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + let thousands_separator = eval_expr(thousands_separator, context, scope, values)?; + eval_number_format_result( + value, + Some(decimals), + Some(decimal_separator), + Some(thousands_separator), + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one PHP numeric value with grouped thousands and fixed decimals. +pub(in crate::interpreter) fn eval_number_format_result( + value: RuntimeCellHandle, + decimals: Option, + decimal_separator: Option, + thousands_separator: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let decimals = match decimals { + Some(decimals) => eval_int_value(decimals, values)?, + None => 0, + }; + let decimal_separator = match decimal_separator { + Some(separator) => values.string_bytes(separator)?, + None => b".".to_vec(), + }; + let thousands_separator = match thousands_separator { + Some(separator) => values.string_bytes(separator)?, + None => b",".to_vec(), + }; + let output = + eval_number_format_bytes(value, decimals, &decimal_separator, &thousands_separator)?; + values.string_bytes_value(&output) +} + +/// Evaluates direct positional `sprintf()` or `printf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_sprintf_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_sprintf_like_result(name, &evaluated_args, values) +} + +/// Evaluates direct positional `vsprintf()` or `vprintf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_vsprintf_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_vsprintf_like_result(name, &evaluated_args, values) +} + +/// Evaluates direct positional `sscanf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_sscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let input = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_sscanf_result(input, format, values) +} + +/// Dispatches already evaluated `sprintf()` or `printf()` arguments. +pub(in crate::interpreter) fn eval_sprintf_like_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "sprintf" => eval_sprintf_result(evaluated_args, values), + "printf" => eval_printf_result(evaluated_args, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Dispatches already evaluated `vsprintf()` or `vprintf()` arguments. +pub(in crate::interpreter) fn eval_vsprintf_like_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "vsprintf" => eval_vsprintf_result(evaluated_args, values), + "vprintf" => eval_vprintf_result(evaluated_args, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Parses one string through the eval `sscanf()` subset and returns an indexed array. +pub(in crate::interpreter) fn eval_sscanf_result( + input: RuntimeCellHandle, + format: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(input)?; + let format = values.string_bytes(format)?; + let matches = eval_sscanf_matches(&input, &format); + let mut result = values.array_new(matches.len())?; + for (index, matched) in matches.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(matched)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Extracts `%d`, `%f`, `%s`, and `%%` matches with the same subset as native `sscanf()`. +pub(in crate::interpreter) fn eval_sscanf_matches(input: &[u8], format: &[u8]) -> Vec> { + let mut matches = Vec::new(); + let mut input_index = 0; + let mut format_index = 0; + + while format_index < format.len() { + if format[format_index] != b'%' { + if input_index >= input.len() || input[input_index] != format[format_index] { + break; + } + input_index += 1; + format_index += 1; + continue; + } + + format_index += 1; + if format_index >= format.len() { + break; + } + + match format[format_index] { + b'%' => { + if input_index >= input.len() || input[input_index] != b'%' { + break; + } + input_index += 1; + } + b'd' => matches.push(eval_sscanf_scan_int(input, &mut input_index)), + b'f' => matches.push(eval_sscanf_scan_float(input, &mut input_index)), + b's' => matches.push(eval_sscanf_scan_word(input, &mut input_index)), + _ => {} + } + format_index += 1; + } + + matches +} + +/// Scans the native `sscanf()` `%d` subset as a matched byte slice. +pub(in crate::interpreter) fn eval_sscanf_scan_int( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + if input.get(*input_index) == Some(&b'-') { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%f` subset as a matched byte slice. +pub(in crate::interpreter) fn eval_sscanf_scan_float( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + if input.get(*input_index) == Some(&b'.') { + *input_index += 1; + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'e' | b'E')) + { + *input_index += 1; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%s` subset as a non-space byte word. +pub(in crate::interpreter) fn eval_sscanf_scan_word( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + while input + .get(*input_index) + .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\n')) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} + +/// Formats `sprintf()` arguments and returns the resulting PHP string. +pub(in crate::interpreter) fn eval_sprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats `printf()` arguments, echoes the result, and returns its byte count. +pub(in crate::interpreter) fn eval_printf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} + +/// Formats `vsprintf()` array arguments and returns the resulting PHP string. +pub(in crate::interpreter) fn eval_vsprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = eval_sprintf_bytes(&format, &format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. +pub(in crate::interpreter) fn eval_vprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = eval_sprintf_bytes(&format, &format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} + +/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. +pub(in crate::interpreter) fn eval_sprintf_argument_array_values( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + let mut args = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(array, position)?; + args.push(values.array_get(array, key)?); + } + Ok(args) +} + +/// Formats one printf-style byte string through eval runtime value coercions. +pub(in crate::interpreter) fn eval_sprintf_bytes( + format: &[u8], + args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut index = 0; + let mut arg_index = 0; + while index < format.len() { + if format[index] != b'%' { + output.push(format[index]); + index += 1; + continue; + } + index += 1; + if index >= format.len() { + break; + } + if format[index] == b'%' { + output.push(b'%'); + index += 1; + continue; + } + + let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; + index = next_index; + let Some(arg) = args.get(arg_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + arg_index += 1; + let bytes = eval_format_sprintf_arg(spec, arg, values)?; + output.extend_from_slice(&bytes); + } + Ok(output) +} + +/// Parses flags, width, precision, and terminal type for one format specifier. +pub(in crate::interpreter) fn eval_parse_sprintf_spec( + format: &[u8], + mut index: usize, +) -> Result<(EvalSprintfSpec, usize), EvalStatus> { + let mut spec = EvalSprintfSpec { + left_align: false, + force_sign: false, + space_sign: false, + zero_pad: false, + alternate: false, + width: None, + precision: None, + specifier: 0, + }; + while index < format.len() { + match format[index] { + b'-' => spec.left_align = true, + b'+' => spec.force_sign = true, + b' ' => spec.space_sign = true, + b'0' => spec.zero_pad = true, + b'#' => spec.alternate = true, + _ => break, + } + index += 1; + } + let (width, next_index) = eval_parse_sprintf_number(format, index)?; + spec.width = width; + index = next_index; + if index < format.len() && format[index] == b'.' { + let (precision, next_index) = eval_parse_sprintf_number(format, index + 1)?; + spec.precision = Some(precision.unwrap_or(0)); + index = next_index; + } + if index >= format.len() { + return Err(EvalStatus::RuntimeFatal); + } + spec.specifier = format[index]; + Ok((spec, index + 1)) +} + +/// Parses an unsigned decimal number from a format specifier component. +pub(in crate::interpreter) fn eval_parse_sprintf_number( + format: &[u8], + mut index: usize, +) -> Result<(Option, usize), EvalStatus> { + let start = index; + let mut value = 0usize; + while index < format.len() && format[index].is_ascii_digit() { + value = value + .checked_mul(10) + .and_then(|value| value.checked_add(usize::from(format[index] - b'0'))) + .ok_or(EvalStatus::RuntimeFatal)?; + index += 1; + } + if index == start { + Ok((None, index)) + } else { + Ok((Some(value), index)) + } +} + +/// Formats one runtime value according to a parsed eval sprintf specifier. +pub(in crate::interpreter) fn eval_format_sprintf_arg( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match spec.specifier { + b's' => eval_format_sprintf_string(spec, value, values), + b'f' | b'e' | b'g' => eval_format_sprintf_float(spec, value, values), + b'c' => eval_format_sprintf_char(spec, value, values), + _ => eval_format_sprintf_int(spec, value, values), + } +} + +/// Formats a `%s` operand after PHP string coercion. +pub(in crate::interpreter) fn eval_format_sprintf_string( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bytes = values.string_bytes(value)?; + if let Some(precision) = spec.precision { + bytes.truncate(precision); + } + Ok(eval_sprintf_apply_width(bytes, spec, false)) +} + +/// Formats an integer-like operand for decimal, unsigned, hex, and octal specifiers. +pub(in crate::interpreter) fn eval_format_sprintf_int( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + let mut output = Vec::new(); + match spec.specifier { + b'u' => { + let digits = eval_sprintf_precision_pad((value as u64).to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + b'x' | b'X' => { + let unsigned = value as u64; + if spec.alternate && unsigned != 0 { + output.extend_from_slice(if spec.specifier == b'X' { b"0X" } else { b"0x" }); + } + let digits = if spec.specifier == b'X' { + format!("{unsigned:X}").into_bytes() + } else { + format!("{unsigned:x}").into_bytes() + }; + output.extend_from_slice(&eval_sprintf_precision_pad(digits, spec)); + } + b'o' => { + let unsigned = value as u64; + let mut digits = eval_sprintf_precision_pad(format!("{unsigned:o}").into_bytes(), spec); + if spec.alternate && !digits.starts_with(b"0") { + output.push(b'0'); + } + output.append(&mut digits); + } + _ => { + let value = value as i128; + let magnitude = if value < 0 { + (-value) as u128 + } else { + value as u128 + }; + if value < 0 { + output.push(b'-'); + } else if spec.force_sign { + output.push(b'+'); + } else if spec.space_sign { + output.push(b' '); + } + let digits = eval_sprintf_precision_pad(magnitude.to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Formats a `%c` operand as the low byte of its integer value. +pub(in crate::interpreter) fn eval_format_sprintf_char( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + Ok(eval_sprintf_apply_width(vec![value as u8], spec, false)) +} + +/// Formats a floating-point operand for the eval printf-family subset. +pub(in crate::interpreter) fn eval_format_sprintf_float( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_float_value(value, values)?; + let precision = spec.precision.unwrap_or(6); + let mut output = if value.is_nan() { + b"NAN".to_vec() + } else if value.is_infinite() { + b"INF".to_vec() + } else { + match spec.specifier { + b'e' => format!("{value:.precision$e}").into_bytes(), + b'g' => format!("{value:.precision$}").into_bytes(), + _ => format!("{value:.precision$}").into_bytes(), + } + }; + if value.is_sign_negative() && !output.starts_with(b"-") { + output.insert(0, b'-'); + } else if value.is_sign_positive() && value.is_finite() { + if spec.force_sign { + output.insert(0, b'+'); + } else if spec.space_sign { + output.insert(0, b' '); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Applies integer precision by left-padding digits with zeros. +pub(in crate::interpreter) fn eval_sprintf_precision_pad( + mut digits: Vec, + spec: EvalSprintfSpec, +) -> Vec { + if matches!(spec.precision, Some(0)) && digits == b"0" { + digits.clear(); + return digits; + } + let Some(precision) = spec.precision else { + return digits; + }; + if digits.len() >= precision { + return digits; + } + let mut output = vec![b'0'; precision - digits.len()]; + output.append(&mut digits); + output +} + +/// Applies field width and alignment to one formatted eval sprintf replacement. +pub(in crate::interpreter) fn eval_sprintf_apply_width( + mut bytes: Vec, + spec: EvalSprintfSpec, + numeric: bool, +) -> Vec { + let Some(width) = spec.width else { + return bytes; + }; + if bytes.len() >= width { + return bytes; + } + let pad_len = width - bytes.len(); + if spec.left_align { + bytes.extend(std::iter::repeat_n(b' ', pad_len)); + return bytes; + } + if numeric && spec.zero_pad && spec.precision.is_none() { + let prefix_len = eval_sprintf_zero_pad_prefix_len(&bytes); + let mut output = Vec::with_capacity(width); + output.extend_from_slice(&bytes[..prefix_len]); + output.extend(std::iter::repeat_n(b'0', pad_len)); + output.extend_from_slice(&bytes[prefix_len..]); + return output; + } + let mut output = Vec::with_capacity(width); + output.extend(std::iter::repeat_n(b' ', pad_len)); + output.append(&mut bytes); + output +} + +/// Returns the sign and alternate-prefix length that should precede zero padding. +pub(in crate::interpreter) fn eval_sprintf_zero_pad_prefix_len(bytes: &[u8]) -> usize { + let mut prefix_len = usize::from(matches!(bytes.first(), Some(b'+' | b'-' | b' '))); + if bytes.len() >= prefix_len + 2 + && bytes[prefix_len] == b'0' + && matches!(bytes[prefix_len + 1], b'x' | b'X') + { + prefix_len += 2; + } + prefix_len +} + +/// Converts one eval value to PHP float and returns the scalar payload. +pub(in crate::interpreter) fn eval_float_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_float(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Produces PHP `number_format()` bytes for finite scalar values. +pub(in crate::interpreter) fn eval_number_format_bytes( + value: f64, + decimals: i64, + decimal_separator: &[u8], + thousands_separator: &[u8], +) -> Result, EvalStatus> { + if !value.is_finite() { + return Ok(value.to_string().into_bytes()); + } + let decimals = decimals.clamp(-308, 308); + let display_decimals = decimals.max(0) as usize; + let abs_value = value.abs(); + let scaled = if decimals >= 0 { + let scale = 10_f64.powi(decimals as i32); + (abs_value * scale).round() + } else { + let scale = 10_f64.powi((-decimals) as i32); + (abs_value / scale).round() * scale + }; + if scaled > (u128::MAX as f64) { + return Err(EvalStatus::RuntimeFatal); + } + let scaled = scaled as u128; + let scale = 10_u128 + .checked_pow(display_decimals as u32) + .ok_or(EvalStatus::RuntimeFatal)?; + let integer = if display_decimals == 0 { + scaled + } else { + scaled / scale + }; + let fraction = if display_decimals == 0 { + 0 + } else { + scaled % scale + }; + + let mut output = Vec::new(); + if value.is_sign_negative() && scaled != 0 { + output.push(b'-'); + } + eval_append_grouped_decimal(&mut output, integer, thousands_separator); + if display_decimals > 0 { + output.extend_from_slice(decimal_separator); + let fraction = format!("{fraction:0display_decimals$}"); + output.extend_from_slice(fraction.as_bytes()); + } + Ok(output) +} + +/// Appends one unsigned decimal integer with optional three-digit grouping. +pub(in crate::interpreter) fn eval_append_grouped_decimal( + output: &mut Vec, + value: u128, + separator: &[u8], +) { + let digits = value.to_string(); + if separator.is_empty() { + output.extend_from_slice(digits.as_bytes()); + return; + } + let first_group = match digits.len() % 3 { + 0 => 3, + len => len, + }; + output.extend_from_slice(&digits.as_bytes()[..first_group]); + let mut index = first_group; + while index < digits.len() { + output.extend_from_slice(separator); + output.extend_from_slice(&digits.as_bytes()[index..index + 3]); + index += 3; + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/mod.rs b/crates/elephc-eval/src/interpreter/builtins/mod.rs new file mode 100644 index 0000000000..63e6447d8b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/mod.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Groups eval implementations for PHP-visible builtins and related helpers. +//! Submodules are organized by builtin domain while this module re-exports the +//! callable surface expected by the core interpreter. +//! +//! Called from: +//! - `crate::interpreter::eval_call()` and positional builtin dispatch paths. +//! +//! Key details: +//! - Builtin modules are children of `interpreter`, so they can reuse core EvalIR +//! execution helpers without widening crate-level visibility. +//! - Runtime value creation and PHP coercions still flow through `RuntimeValueOps`. + +mod arrays; +mod filesystem; +mod formatting; +mod network_env; +mod regex; +mod registry; +mod scalars; +mod strings; +mod symbols; +mod time; + +pub(super) use arrays::*; +pub(super) use filesystem::*; +pub(super) use formatting::*; +pub(super) use network_env::*; +pub(super) use regex::*; +pub(super) use registry::*; +pub(super) use scalars::*; +pub(super) use strings::*; +pub(super) use symbols::*; +pub(super) use time::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env.rs b/crates/elephc-eval/src/interpreter/builtins/network_env.rs new file mode 100644 index 0000000000..123d47307e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/network_env.rs @@ -0,0 +1,572 @@ +//! Purpose: +//! Network lookup, IP conversion, environment, and realpath-cache builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +use super::super::*; +use super::*; + +/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostbyaddr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_gethostbyaddr_result(ip, values) +} + +/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. +pub(in crate::interpreter) fn eval_gethostbyaddr_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip_bytes = values.string_bytes(ip)?; + let ip_text = String::from_utf8_lossy(&ip_bytes); + let Ok(ipv4) = ip_text.parse::() else { + return values.bool_value(false); + }; + let octets = ipv4.octets(); + let resolved = unsafe { + // libc reads the stack-owned IPv4 octets during this call and returns + // static resolver storage, which is copied before the next resolver call. + let host = libc_gethostbyaddr( + octets.as_ptr().cast::(), + octets.len() as libc::socklen_t, + libc::AF_INET, + ); + if host.is_null() || (*host).h_name.is_null() { + None + } else { + Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) + } + }; + match resolved { + Some(name) if !name.is_empty() => values.string_bytes_value(&name), + _ => values.string(ip_text.as_ref()), + } +} + +/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hostname] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hostname = eval_expr(hostname, context, scope, values)?; + eval_gethostbyname_result(hostname, values) +} + +/// Resolves one host name to an IPv4 string, or returns the original input on failure. +pub(in crate::interpreter) fn eval_gethostbyname_result( + hostname: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let hostname = values.string_bytes(hostname)?; + let hostname = String::from_utf8_lossy(&hostname); + if hostname.parse::().is_ok() { + return values.string(hostname.as_ref()); + } + let resolved = (hostname.as_ref(), 0_u16) + .to_socket_addrs() + .ok() + .and_then(|addrs| { + addrs + .filter_map(|addr| match addr.ip() { + std::net::IpAddr::V4(ip) => Some(ip.to_string()), + std::net::IpAddr::V6(_) => None, + }) + .next() + }); + values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) +} + +/// Evaluates PHP `gethostname()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostname( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + let [] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostname_result(values) +} + +/// Reads the current host name through libc and returns an empty string on failure. +pub(in crate::interpreter) fn eval_gethostname_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let mut buffer = [0 as libc::c_char; 256]; + let status = unsafe { + // libc writes at most buffer.len() bytes into this stack buffer. + libc::gethostname(buffer.as_mut_ptr(), buffer.len()) + }; + if status != 0 { + return values.string(""); + } + let length = buffer + .iter() + .position(|byte| *byte == 0) + .unwrap_or(buffer.len()); + let hostname = buffer[..length] + .iter() + .map(|byte| *byte as u8) + .collect::>(); + values.string_bytes_value(&hostname) +} + +/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getprotobyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobyname_result(protocol, values) +} + +/// Looks up an IP protocol number by name or alias. +pub(in crate::interpreter) fn eval_getprotobyname_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy scalar fields before another lookup. + libc_getprotobyname(protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let number = unsafe { (*entry).p_proto }; + values.int(i64::from(number)) +} + +/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getprotobynumber( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobynumber_result(protocol, values) +} + +/// Looks up an IP protocol name by numeric protocol id. +pub(in crate::interpreter) fn eval_getprotobynumber_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = eval_int_value(protocol, values)?; + let Ok(protocol) = libc::c_int::try_from(protocol) else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy the name before another lookup. + libc_getprotobynumber(protocol) + }; + eval_protoent_name_or_false(entry, values) +} + +/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_getservbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [service, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let service = eval_expr(service, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyname_result(service, protocol, values) +} + +/// Looks up an internet service port by service name and protocol. +pub(in crate::interpreter) fn eval_getservbyname_result( + service: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(service) = eval_lowercase_c_string(service, values)? else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global servent; copy scalar fields before another lookup. + libc_getservbyname(service.as_ptr(), protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let port = unsafe { u16::from_be((*entry).s_port as u16) }; + values.int(i64::from(port)) +} + +/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_getservbyport( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [port, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let port = eval_expr(port, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyport_result(port, protocol, values) +} + +/// Looks up an internet service name by port and protocol. +pub(in crate::interpreter) fn eval_getservbyport_result( + port: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let port = eval_int_value(port, values)?; + let Ok(port) = u16::try_from(port) else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let network_port = port.to_be() as libc::c_int; + let entry = unsafe { + // libc returns a process-global servent; copy the name before another lookup. + libc_getservbyport(network_port, protocol.as_ptr()) + }; + eval_servent_name_or_false(entry, values) +} + +/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. +pub(in crate::interpreter) fn eval_lowercase_c_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let bytes = values.string_bytes(value)?; + let bytes = bytes + .into_iter() + .map(|byte| byte.to_ascii_lowercase()) + .collect::>(); + Ok(CString::new(bytes).ok()) +} + +/// Copies a protoent canonical name into a PHP string or returns PHP false. +pub(in crate::interpreter) fn eval_protoent_name_or_false( + entry: *mut libc::protoent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).p_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} + +/// Copies a servent canonical name into a PHP string or returns PHP false. +pub(in crate::interpreter) fn eval_servent_name_or_false( + entry: *mut libc::servent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).s_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} + +/// Evaluates PHP `long2ip($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_long2ip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_long2ip_result(ip, values) +} + +/// Formats one 32-bit IPv4 integer as a dotted-quad string. +pub(in crate::interpreter) fn eval_long2ip_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip = eval_int_value(ip, values)? as u32; + values.string(&eval_format_ipv4(ip)) +} + +/// Evaluates PHP `ip2long($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ip2long( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_ip2long_result(ip, values) +} + +/// Parses a dotted-quad IPv4 string into an integer or PHP false. +pub(in crate::interpreter) fn eval_ip2long_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + match eval_parse_ipv4(&bytes) { + Some(ip) => values.int(i64::from(ip)), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `inet_pton($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_inet_pton( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_inet_pton_result(ip, values) +} + +/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. +pub(in crate::interpreter) fn eval_inet_pton_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + let Some(ip) = eval_parse_ipv4(&bytes) else { + return values.bool_value(false); + }; + values.string_bytes_value(&ip.to_be_bytes()) +} + +/// Evaluates PHP `inet_ntop($binary)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_inet_ntop( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [binary] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let binary = eval_expr(binary, context, scope, values)?; + eval_inet_ntop_result(binary, values) +} + +/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. +pub(in crate::interpreter) fn eval_inet_ntop_result( + binary: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(binary)?; + let [a, b, c, d] = bytes.as_slice() else { + return values.bool_value(false); + }; + let ip = u32::from_be_bytes([*a, *b, *c, *d]); + values.string(&eval_format_ipv4(ip)) +} + +/// Parses exactly four decimal IPv4 octets separated by dots. +pub(in crate::interpreter) fn eval_parse_ipv4(bytes: &[u8]) -> Option { + let mut octets = [0_u8; 4]; + let mut position = 0_usize; + let mut index = 0_usize; + + while index < 4 { + if position >= bytes.len() { + return None; + } + let start = position; + let mut value = 0_u16; + while position < bytes.len() && bytes[position].is_ascii_digit() { + value = value + .checked_mul(10)? + .checked_add(u16::from(bytes[position] - b'0'))?; + position += 1; + if position - start > 3 || value > 255 { + return None; + } + } + if position == start { + return None; + } + octets[index] = value as u8; + index += 1; + if index == 4 { + return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); + } + if bytes.get(position).copied() != Some(b'.') { + return None; + } + position += 1; + } + None +} + +/// Formats one packed IPv4 integer into dotted-quad text. +pub(in crate::interpreter) fn eval_format_ipv4(ip: u32) -> String { + let [a, b, c, d] = ip.to_be_bytes(); + format!("{}.{}.{}.{}", a, b, c, d) +} + +/// Evaluates PHP `getenv($name)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + eval_getenv_result(name, values) +} + +/// Reads one environment variable and returns an empty string when it is unset. +pub(in crate::interpreter) fn eval_getenv_result( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8_lossy(&name); + let value = std::env::var_os(name.as_ref()) + .map(|value| value.to_string_lossy().into_owned()) + .unwrap_or_default(); + values.string(&value) +} + +/// Evaluates PHP `putenv($assignment)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_putenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [assignment] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let assignment = eval_expr(assignment, context, scope, values)?; + eval_putenv_result(assignment, values) +} + +/// Applies one `putenv()` assignment to the host environment. +pub(in crate::interpreter) fn eval_putenv_result( + assignment: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let assignment = values.string_bytes(assignment)?; + if let Some(separator) = assignment.iter().position(|byte| *byte == b'=') { + let name = String::from_utf8_lossy(&assignment[..separator]); + let value = String::from_utf8_lossy(&assignment[separator + 1..]); + std::env::set_var(name.as_ref(), value.as_ref()); + } else { + let name = String::from_utf8_lossy(&assignment); + std::env::remove_var(name.as_ref()); + } + values.bool_value(true) +} + +/// Evaluates PHP `sys_get_temp_dir()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_sys_get_temp_dir( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values) +} + +/// Returns the same temporary directory literal as the native static builtin. +pub(in crate::interpreter) fn eval_sys_get_temp_dir_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string("/tmp") +} + +/// Evaluates PHP `realpath_cache_get()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_realpath_cache_get( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values) +} + +/// Returns elephc's intentionally empty realpath-cache view. +pub(in crate::interpreter) fn eval_realpath_cache_get_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.array_new(0) +} + +/// Evaluates PHP `realpath_cache_size()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_realpath_cache_size( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values) +} + +/// Returns zero because elephc does not maintain a runtime realpath cache. +pub(in crate::interpreter) fn eval_realpath_cache_size_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(0) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex.rs b/crates/elephc-eval/src/interpreter/builtins/regex.rs new file mode 100644 index 0000000000..24c85469b3 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex.rs @@ -0,0 +1,858 @@ +//! Purpose: +//! PCRE-style preg builtins and regex capture helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +use super::super::*; +use super::*; + +/// Evaluates PHP `preg_match()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_match( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, None, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether one regex matches the subject string. +pub(in crate::interpreter) fn eval_preg_match_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + values.int(i64::from(regex.is_match(&subject))) +} + +/// Returns the match flag plus PHP `$matches` capture array for one regex search. +pub(in crate::interpreter) fn eval_preg_match_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let flags = eval_preg_match_flags(flags, values)?; + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + if let Some(captures) = regex.captures(&subject) { + let matches = eval_preg_capture_array( + &subject, + Some(&captures), + offset_capture, + unmatched_as_null, + values, + )?; + let matched = values.int(1)?; + return Ok((matched, matches)); + } + let matches = + eval_preg_capture_array(&subject, None, offset_capture, unmatched_as_null, values)?; + let matched = values.int(0)?; + Ok((matched, matches)) +} + +/// Returns supported `preg_match()` flags. +pub(in crate::interpreter) fn eval_preg_match_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_OFFSET_CAPTURE | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Evaluates PHP `preg_match_all()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_match_all( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_all_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, None, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Counts all non-overlapping regex matches in one subject string. +pub(in crate::interpreter) fn eval_preg_match_all_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let count = regex.captures_iter(&subject).count(); + values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Returns the match count plus PHP's default `PREG_PATTERN_ORDER` `$matches` array. +pub(in crate::interpreter) fn eval_preg_match_all_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let capture_count = regex.captures_len(); + let subject = values.string_bytes(subject)?; + let captures: Vec> = regex.captures_iter(&subject).collect(); + let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let flags = eval_preg_match_all_flags(flags, values)?; + let matches = if flags & EVAL_PREG_SET_ORDER != 0 { + eval_preg_match_all_set_order_array(&subject, &captures, capture_count, flags, values)? + } else { + eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, flags, values)? + }; + Ok((count, matches)) +} + +/// Returns supported `preg_match_all()` flags. +pub(in crate::interpreter) fn eval_preg_match_all_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(EVAL_PREG_PATTERN_ORDER); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_PATTERN_ORDER + | EVAL_PREG_SET_ORDER + | EVAL_PREG_OFFSET_CAPTURE + | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Builds PHP's default `preg_match_all()` pattern-order capture matrix. +pub(in crate::interpreter) fn eval_preg_match_all_pattern_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + let mut outer = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let mut row = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let key = + values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; + row = values.array_set(row, key, value)?; + } + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + +/// Builds PHP's `preg_match_all(..., PREG_SET_ORDER)` match-order capture matrix. +pub(in crate::interpreter) fn eval_preg_match_all_set_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + let mut outer = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let mut row = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; + row = values.array_set(row, key, value)?; + } + let key = values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + +/// Evaluates PHP `preg_replace()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, replacement, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let replacement = eval_expr(replacement, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_result(pattern, replacement, subject, values) +} + +/// Replaces every regex match with a PHP-style backreference-expanded replacement. +pub(in crate::interpreter) fn eval_preg_replace_result( + pattern: RuntimeCellHandle, + replacement: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let replacement = values.string_bytes(replacement)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + +/// Evaluates PHP `preg_replace_callback()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_replace_callback( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, callback, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_callback_result(pattern, callback, subject, context, values) +} + +/// Replaces every regex match by invoking an eval-supported callback with `$matches`. +pub(in crate::interpreter) fn eval_preg_replace_callback_result( + pattern: RuntimeCellHandle, + callback: RuntimeCellHandle, + subject: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let callback = eval_callable_name(callback, values)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; + let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; + let callback_result = values.cast_string(callback_result)?; + let callback_bytes = values.string_bytes(callback_result)?; + result.extend_from_slice(&callback_bytes); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + +/// Evaluates PHP `preg_split()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_split_result(pattern, subject, None, None, values) + } + [pattern, subject, limit] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), None, values) + } + [pattern, subject, limit, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits a subject string with eval-supported `preg_split()` flags. +pub(in crate::interpreter) fn eval_preg_split_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + limit: Option, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let limit = eval_preg_split_limit(limit, values)?; + let flags = eval_preg_split_flags(flags, values)?; + let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; + let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; + let offset_capture = flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0; + let mut pieces = Vec::::new(); + let mut cursor = 0; + + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + if eval_preg_split_reached_limit(&pieces, limit) { + break; + } + eval_preg_split_push_piece( + &mut pieces, + &subject[cursor..matched.start()], + cursor, + no_empty, + ); + if capture_delimiters { + eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); + } + cursor = matched.end(); + } + eval_preg_split_push_piece(&mut pieces, &subject[cursor..], cursor, no_empty); + + let mut result = values.array_new(pieces.len())?; + for (index, piece) in pieces.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_split_piece_value(piece, offset_capture, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Compiles one eval PCRE-style delimited pattern into a Rust regex. +pub(in crate::interpreter) fn eval_preg_regex( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; + let body = String::from_utf8(body).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut builder = RegexBuilder::new(&body); + builder + .case_insensitive(modifiers.case_insensitive) + .multi_line(modifiers.multi_line) + .dot_matches_new_line(modifiers.dot_matches_new_line) + .swap_greed(modifiers.swap_greed); + builder.build().map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Regex modifiers supported by eval `preg_*` pattern stripping. +#[derive(Default)] +pub(in crate::interpreter) struct EvalPregModifiers { + case_insensitive: bool, + multi_line: bool, + dot_matches_new_line: bool, + swap_greed: bool, +} + +/// One `preg_split()` output segment plus its byte offset in the subject. +pub(in crate::interpreter) struct EvalPregSplitPiece { + bytes: Vec, + offset: usize, +} + +/// Splits a PHP delimited regex into body bytes and supported modifiers. +pub(in crate::interpreter) fn eval_preg_pattern_parts( + pattern: &[u8], +) -> Result<(Vec, EvalPregModifiers), EvalStatus> { + if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() { + return Err(EvalStatus::RuntimeFatal); + } + let delimiter = pattern[0]; + if delimiter == b'\\' { + return Err(EvalStatus::RuntimeFatal); + } + let closing = eval_preg_closing_delimiter(delimiter); + let close_index = + eval_preg_find_closing_delimiter(pattern, closing).ok_or(EvalStatus::RuntimeFatal)?; + let body = eval_preg_unescape_delimiter(&pattern[1..close_index], delimiter, closing); + let modifiers = eval_preg_modifiers(&pattern[close_index + 1..])?; + Ok((body, modifiers)) +} + +/// Returns the closing regex delimiter for PHP's paired delimiter forms. +pub(in crate::interpreter) fn eval_preg_closing_delimiter(delimiter: u8) -> u8 { + match delimiter { + b'(' => b')', + b'[' => b']', + b'{' => b'}', + b'<' => b'>', + _ => delimiter, + } +} + +/// Finds the first unescaped closing regex delimiter. +pub(in crate::interpreter) fn eval_preg_find_closing_delimiter( + pattern: &[u8], + closing: u8, +) -> Option { + let mut escaped = false; + for (index, byte) in pattern.iter().copied().enumerate().skip(1) { + if escaped { + escaped = false; + continue; + } + if byte == b'\\' { + escaped = true; + continue; + } + if byte == closing { + return Some(index); + } + } + None +} + +/// Removes escapes that only protect the PHP regex delimiter from pattern stripping. +pub(in crate::interpreter) fn eval_preg_unescape_delimiter( + body: &[u8], + delimiter: u8, + closing: u8, +) -> Vec { + let mut result = Vec::with_capacity(body.len()); + let mut index = 0; + while index < body.len() { + if body[index] == b'\\' + && index + 1 < body.len() + && matches!(body[index + 1], byte if byte == delimiter || byte == closing) + { + result.push(body[index + 1]); + index += 2; + } else { + result.push(body[index]); + index += 1; + } + } + result +} + +/// Parses eval-supported PHP regex modifiers. +pub(in crate::interpreter) fn eval_preg_modifiers( + modifiers: &[u8], +) -> Result { + let mut parsed = EvalPregModifiers::default(); + for modifier in modifiers { + match *modifier { + b'i' => parsed.case_insensitive = true, + b'm' => parsed.multi_line = true, + b's' => parsed.dot_matches_new_line = true, + b'U' => parsed.swap_greed = true, + b'u' => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(parsed) +} + +/// Builds PHP's indexed `$matches` capture array for one regex result. +pub(in crate::interpreter) fn eval_preg_capture_array( + subject: &[u8], + captures: Option<&Captures<'_>>, + offset_capture: bool, + unmatched_as_null: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = captures.map_or(0, |captures| { + eval_preg_visible_capture_len(captures, unmatched_as_null) + }); + let mut result = values.array_new(len)?; + if let Some(captures) = captures { + for index in 0..len { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + captures, + index, + offset_capture, + unmatched_as_null, + values, + )?; + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Returns the capture count PHP should expose, dropping trailing unmatched groups. +pub(in crate::interpreter) fn eval_preg_visible_capture_len( + captures: &Captures<'_>, + unmatched_as_null: bool, +) -> usize { + if unmatched_as_null { + return captures.len(); + } + let mut len = captures.len(); + while len > 1 && captures.get(len - 1).is_none() { + len -= 1; + } + len +} + +/// Returns one captured byte range from the original subject. +pub(in crate::interpreter) fn eval_preg_capture_bytes<'a>( + subject: &'a [u8], + captures: &Captures<'_>, + index: usize, +) -> Option<&'a [u8]> { + captures + .get(index) + .map(|matched| &subject[matched.start()..matched.end()]) +} + +/// Builds one capture entry as either a string or PHP's `[string, byte_offset]` pair. +pub(in crate::interpreter) fn eval_preg_capture_value( + subject: &[u8], + captures: &Captures<'_>, + index: usize, + offset_capture: bool, + unmatched_as_null: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let matched = captures.get(index); + let value = if matched.is_none() && unmatched_as_null { + values.null()? + } else { + let bytes = matched.as_ref().map_or(b"".as_slice(), |matched| { + &subject[matched.start()..matched.end()] + }); + values.string_bytes_value(bytes)? + }; + if !offset_capture { + return Ok(value); + } + + let offset = matched.map_or(Ok(-1_i64), |matched| { + i64::try_from(matched.start()).map_err(|_| EvalStatus::RuntimeFatal) + })?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} + +/// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. +pub(in crate::interpreter) fn eval_preg_expand_replacement( + replacement: &[u8], + subject: &[u8], + captures: &Captures<'_>, + result: &mut Vec, +) { + let mut index = 0; + while index < replacement.len() { + match replacement[index] { + b'$' => { + if let Some((capture_index, next_index)) = + eval_preg_replacement_capture_index(replacement, index + 1) + { + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } else { + result.push(replacement[index]); + index += 1; + } + } + b'\\' if index + 1 < replacement.len() && replacement[index + 1].is_ascii_digit() => { + let (capture_index, next_index) = + eval_preg_decimal_capture_index(replacement, index + 1); + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } + byte => { + result.push(byte); + index += 1; + } + } + } +} + +/// Parses a dollar-style replacement capture reference. +pub(in crate::interpreter) fn eval_preg_replacement_capture_index( + bytes: &[u8], + index: usize, +) -> Option<(usize, usize)> { + if bytes.get(index).copied() == Some(b'{') { + let mut cursor = index + 1; + let start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + if cursor == start || bytes.get(cursor).copied() != Some(b'}') { + return None; + } + let capture = eval_preg_decimal_bytes_to_usize(&bytes[start..cursor])?; + return Some((capture, cursor + 1)); + } + if bytes.get(index).is_some_and(u8::is_ascii_digit) { + let (capture, next) = eval_preg_decimal_capture_index(bytes, index); + return Some((capture, next)); + } + None +} + +/// Parses a one- or two-digit replacement capture reference. +pub(in crate::interpreter) fn eval_preg_decimal_capture_index( + bytes: &[u8], + index: usize, +) -> (usize, usize) { + let mut cursor = index; + let end = usize::min(bytes.len(), index + 2); + while cursor < end && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + ( + eval_preg_decimal_bytes_to_usize(&bytes[index..cursor]).unwrap_or(0), + cursor, + ) +} + +/// Converts ASCII decimal bytes into a `usize` capture index. +pub(in crate::interpreter) fn eval_preg_decimal_bytes_to_usize(bytes: &[u8]) -> Option { + let mut value = 0usize; + for byte in bytes { + value = value.checked_mul(10)?; + value = value.checked_add(usize::from(byte - b'0'))?; + } + Some(value) +} + +/// Returns the PHP `preg_split()` limit, treating zero as unlimited. +pub(in crate::interpreter) fn eval_preg_split_limit( + limit: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(limit) = limit else { + return Ok(None); + }; + let limit = eval_int_value(limit, values)?; + if limit <= 0 { + return Ok(None); + } + usize::try_from(limit) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns supported `preg_split()` flags. +pub(in crate::interpreter) fn eval_preg_split_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = + EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE | EVAL_PREG_SPLIT_OFFSET_CAPTURE; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Returns whether `preg_split()` should stop splitting and emit the remaining subject. +pub(in crate::interpreter) fn eval_preg_split_reached_limit( + pieces: &[EvalPregSplitPiece], + limit: Option, +) -> bool { + matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) +} + +/// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. +pub(in crate::interpreter) fn eval_preg_split_push_piece( + pieces: &mut Vec, + piece: &[u8], + offset: usize, + no_empty: bool, +) { + if no_empty && piece.is_empty() { + return; + } + pieces.push(EvalPregSplitPiece { + bytes: piece.to_vec(), + offset, + }); +} + +/// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. +pub(in crate::interpreter) fn eval_preg_split_push_captures( + pieces: &mut Vec, + subject: &[u8], + captures: &Captures<'_>, + no_empty: bool, +) { + for index in 1..captures.len() { + if let Some(matched) = captures.get(index) { + eval_preg_split_push_piece( + pieces, + &subject[matched.start()..matched.end()], + matched.start(), + no_empty, + ); + } + } +} + +/// Converts one split segment to a string or PHP `[string, byte_offset]` pair. +pub(in crate::interpreter) fn eval_preg_split_piece_value( + piece: &EvalPregSplitPiece, + offset_capture: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.string_bytes_value(&piece.bytes)?; + if !offset_capture { + return Ok(value); + } + + let offset = i64::try_from(piece.offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs new file mode 100644 index 0000000000..852e0245a7 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -0,0 +1,267 @@ +//! Purpose: +//! Named and spread argument binding for builtin calls. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Helpers are scoped to the eval interpreter and operate on already parsed +//! EvalIR call metadata or evaluated runtime-cell handles. + +use super::super::super::*; +use super::*; + +/// Evaluates a direct PHP-visible builtin call with named or spread arguments. +pub(in crate::interpreter) fn eval_builtin_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + Ok(result) +} + +/// Binds evaluated builtin arguments to PHP parameter order when names are used. +pub(in crate::interpreter) fn bind_evaluated_builtin_args( + name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if evaluated_args.iter().all(|arg| arg.name.is_none()) { + return Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()); + } + + let params = eval_builtin_param_names(name).ok_or(EvalStatus::RuntimeFatal)?; + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_builtin_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + collect_bound_builtin_args(name, bound_args, values) +} + +/// Binds one named builtin-call value to the matching PHP parameter slot. +pub(in crate::interpreter) fn bind_builtin_named_arg( + params: &[&str], + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + let Some(param_index) = params.iter().position(|param| *param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + Ok(()) +} + +/// Collects ordered bound arguments, rejecting gaps where defaults would be needed. +pub(in crate::interpreter) fn collect_contiguous_bound_args( + bound_args: Vec>, +) -> Result, EvalStatus> { + let Some(last_index) = bound_args.iter().rposition(Option::is_some) else { + return Ok(Vec::new()); + }; + bound_args + .into_iter() + .take(last_index + 1) + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Collects ordered builtin arguments, applying PHP defaults for special named-call gaps. +pub(in crate::interpreter) fn collect_bound_builtin_args( + name: &str, + mut bound_args: Vec>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if name == "array_splice" && bound_args.get(3).is_some_and(Option::is_some) { + if bound_args.get(2).is_some_and(Option::is_none) { + bound_args[2] = Some(values.null()?); + } + } + collect_contiguous_bound_args(bound_args) +} + +/// Returns PHP parameter names for builtin calls implemented by eval. +pub(in crate::interpreter) fn eval_builtin_param_names( + name: &str, +) -> Option<&'static [&'static str]> { + match name { + "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), + "array_chunk" => Some(&["array", "length"]), + "array_column" => Some(&["array", "column_key"]), + "array_combine" => Some(&["keys", "values"]), + "array_fill" => Some(&["start_index", "count", "value"]), + "array_fill_keys" => Some(&["keys", "value"]), + "array_filter" => Some(&["array", "callback", "mode"]), + "array_map" => Some(&["callback", "array"]), + "array_reduce" => Some(&["array", "callback", "initial"]), + "array_walk" => Some(&["array", "callback"]), + "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), + "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" + | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" | "asort" + | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { + Some(&["array"]) + } + "array_push" | "array_unshift" => Some(&["array", "values"]), + "array_key_exists" => Some(&["key", "array"]), + "array_pad" => Some(&["array", "length", "value"]), + "array_reverse" => Some(&["array", "preserve_keys"]), + "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), + "array_slice" => Some(&["array", "offset", "length"]), + "array_splice" => Some(&["array", "offset", "length", "replacement"]), + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), + "atan2" => Some(&["y", "x"]), + "basename" => Some(&["path", "suffix"]), + "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" + | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => { + Some(&["string"]) + } + "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" + | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" + | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" | "is_real" + | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), + "settype" => Some(&["var", "type"]), + "get_class" => Some(&["object"]), + "get_parent_class" => Some(&["object_or_class"]), + "call_user_func" => Some(&["callback"]), + "call_user_func_array" => Some(&["callback", "args"]), + "class_exists" => Some(&["class", "autoload"]), + "enum_exists" => Some(&["enum", "autoload"]), + "interface_exists" => Some(&["interface", "autoload"]), + "trait_exists" => Some(&["trait", "autoload"]), + "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), + "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), + "chmod" => Some(&["filename", "permissions"]), + "chr" => Some(&["codepoint"]), + "clamp" => Some(&["value", "min", "max"]), + "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), + "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), + "count" => Some(&["value", "mode"]), + "copy" | "rename" => Some(&["from", "to"]), + "crc32" => Some(&["string"]), + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), + "date" => Some(&["format", "timestamp"]), + "define" => Some(&["constant_name", "value"]), + "defined" => Some(&["constant_name"]), + "dirname" => Some(&["path", "levels"]), + "disk_free_space" | "disk_total_space" => Some(&["directory"]), + "explode" => Some(&["separator", "string"]), + "fdiv" | "fmod" => Some(&["num1", "num2"]), + "fnmatch" => Some(&["pattern", "filename", "flags"]), + "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" + | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" + | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" + | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), + "file_put_contents" => Some(&["filename", "data"]), + "function_exists" => Some(&["function"]), + "gethostbyaddr" => Some(&["ip"]), + "gethostbyname" => Some(&["hostname"]), + "gethostname" => Some(&[]), + "getprotobyname" => Some(&["protocol"]), + "getprotobynumber" => Some(&["protocol"]), + "getservbyname" => Some(&["service", "protocol"]), + "getservbyport" => Some(&["port", "protocol"]), + "get_resource_id" | "get_resource_type" => Some(&["resource"]), + "getcwd" => Some(&[]), + "getenv" => Some(&["name"]), + "glob" => Some(&["pattern"]), + "hash" => Some(&["algo", "data", "binary"]), + "hash_algos" => Some(&[]), + "hash_equals" => Some(&["known_string", "user_string"]), + "hash_file" => Some(&["algo", "filename", "binary"]), + "hash_hmac" => Some(&["algo", "data", "key", "binary"]), + "hypot" => Some(&["x", "y"]), + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), + "implode" => Some(&["separator", "array"]), + "inet_ntop" => Some(&["ip"]), + "inet_pton" => Some(&["ip"]), + "intdiv" => Some(&["num1", "num2"]), + "iterator_apply" => Some(&["iterator", "callback", "args"]), + "iterator_count" => Some(&["iterator"]), + "iterator_to_array" => Some(&["iterator", "preserve_keys"]), + "ip2long" => Some(&["ip"]), + "json_decode" => Some(&["json", "associative", "depth", "flags"]), + "json_encode" => Some(&["value", "flags", "depth"]), + "json_last_error" | "json_last_error_msg" => Some(&[]), + "json_validate" => Some(&["json", "depth", "flags"]), + "link" | "symlink" => Some(&["target", "link"]), + "linkinfo" | "readlink" => Some(&["path"]), + "log" => Some(&["num", "base"]), + "max" | "min" => Some(&["value"]), + "md5" | "sha1" => Some(&["string", "binary"]), + "microtime" => Some(&["as_float"]), + "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), + "nl2br" => Some(&["string", "use_xhtml"]), + "number_format" => Some(&[ + "num", + "decimals", + "decimal_separator", + "thousands_separator", + ]), + "ord" => Some(&["character"]), + "pathinfo" => Some(&["path", "flags"]), + "pi" => Some(&[]), + "php_uname" => Some(&["mode"]), + "phpversion" => Some(&[]), + "pow" => Some(&["num", "exponent"]), + "preg_match" => Some(&["pattern", "subject", "matches", "flags", "offset"]), + "preg_match_all" => Some(&["pattern", "subject", "matches", "flags", "offset"]), + "preg_replace" => Some(&["pattern", "replacement", "subject", "limit", "count"]), + "preg_replace_callback" => Some(&["pattern", "callback", "subject", "limit", "count"]), + "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), + "print_r" | "var_dump" => Some(&["value"]), + "putenv" => Some(&["assignment"]), + "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), + "range" => Some(&["start", "end"]), + "realpath" => Some(&["path"]), + "realpath_cache_get" | "realpath_cache_size" => Some(&[]), + "round" => Some(&["num", "precision"]), + "sleep" => Some(&["seconds"]), + "spl_classes" => Some(&[]), + "spl_object_id" | "spl_object_hash" => Some(&["object"]), + "sscanf" => Some(&["string", "format", "vars"]), + "sprintf" | "printf" => Some(&["format", "values"]), + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), + "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), + "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), + "strtotime" => Some(&["datetime"]), + "strstr" => Some(&["haystack", "needle", "before_needle"]), + "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), + "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), + "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), + "str_repeat" => Some(&["string", "times"]), + "str_split" => Some(&["string", "length"]), + "substr" => Some(&["string", "offset", "length"]), + "substr_replace" => Some(&["string", "replace", "offset", "length"]), + "sys_get_temp_dir" | "time" => Some(&[]), + "tempnam" => Some(&["directory", "prefix"]), + "touch" => Some(&["filename", "mtime", "atime"]), + "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { + Some(&["string"]) + } + "long2ip" => Some(&["ip"]), + "ucwords" => Some(&["string", "separators"]), + "umask" => Some(&["mask"]), + "usleep" => Some(&["microseconds"]), + "vsprintf" | "vprintf" => Some(&["format", "values"]), + "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), + _ => None, + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs b/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs new file mode 100644 index 0000000000..38cb9c2278 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs @@ -0,0 +1,206 @@ +//! Purpose: +//! call_user_func and callable normalization helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Helpers are scoped to the eval interpreter and operate on already parsed +//! EvalIR call metadata or evaluated runtime-cell handles. + +use super::super::super::*; +use super::*; + +/// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. +pub(in crate::interpreter) fn eval_builtin_call_user_func( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_call_user_func_with_values(evaluated_args, context, values) +} + +/// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. +pub(in crate::interpreter) fn eval_builtin_call_user_func_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [callback, arg_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let arg_array = eval_expr(arg_array, context, scope, values)?; + eval_call_user_func_array_with_values(callback, arg_array, context, values) +} + +/// Dispatches `call_user_func_array` after callback and array arguments are evaluated. +pub(in crate::interpreter) fn eval_call_user_func_array_with_values( + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable(callback, values)?; + if !values.is_array_like(arg_array)? { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = eval_array_call_arg_values(arg_array, values)?; + eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) +} + +/// Dispatches `call_user_func` after its callback and arguments are already evaluated. +pub(in crate::interpreter) fn eval_call_user_func_with_values( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, callback_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_callable(*callback, values)?; + eval_evaluated_callable_with_values(&callback, callback_args.to_vec(), context, values) +} + +/// Normalizes one PHP callback value for eval dynamic callable dispatch. +pub(in crate::interpreter) fn eval_callable( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_array_like(callback)? { + return eval_array_callable(callback, values); + } + eval_callable_name(callback, values).map(EvaluatedCallable::Named) +} + +/// Normalizes one two-element object-method callable array. +pub(in crate::interpreter) fn eval_array_callable( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.array_len(callback)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let object = values.array_get(callback, zero)?; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::UnsupportedConstruct); + } + let method = values.array_get(callback, one)?; + let method = + String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(EvaluatedCallable::ObjectMethod { object, method }) +} + +/// Normalizes one string callback name for eval dynamic callable dispatch. +pub(in crate::interpreter) fn eval_callable_name( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = values.string_bytes(callback)?; + let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; + let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); + if callback.contains("::") { + return Err(EvalStatus::UnsupportedConstruct); + } + Ok(callback) +} + +/// Invokes an already normalized callback with source-order positional values. +pub(in crate::interpreter) fn eval_evaluated_callable_with_values( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named(name) => { + eval_callable_with_values(name, evaluated_args, context, values) + } + EvaluatedCallable::ObjectMethod { object, method } => { + eval_method_call_result(*object, method, evaluated_args, context, values) + } + } +} + +/// Invokes an already normalized callback with optional named-argument metadata. +pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named(name) => { + eval_callable_with_call_array_args(name, evaluated_args, context, values) + } + EvaluatedCallable::ObjectMethod { object, method } => { + if evaluated_args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); + eval_method_call_result(*object, method, evaluated_args, context, values) + } + } +} + +/// Invokes a PHP-visible callable name with source-order positional values. +pub(in crate::interpreter) fn eval_callable_with_values( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { + return Ok(result); + } + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function_with_values(function, evaluated_args, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Invokes a callable with arguments that may carry `call_user_func_array` names. +pub(in crate::interpreter) fn eval_callable_with_call_array_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.iter().all(|arg| arg.name.is_none()) { + let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); + return eval_callable_with_values(name, evaluated_args, context, values); + } + if eval_php_visible_builtin_exists(name) { + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + return Ok(result); + } + if let Some(function) = context.function(name).cloned() { + let evaluated_args = bind_evaluated_function_args(function.params(), evaluated_args)?; + return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + } + if let Some(function) = context.native_function(name) { + if function.param_names().len() != function.param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let evaluated_args = bind_evaluated_function_args(function.param_names(), evaluated_args)?; + return eval_native_function_with_values(function, evaluated_args, values); + } + Err(EvalStatus::UnsupportedConstruct) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch.rs new file mode 100644 index 0000000000..284795b934 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch.rs @@ -0,0 +1,1096 @@ +//! Purpose: +//! By-value dynamic builtin dispatch for evaluated argument lists. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Helpers are scoped to the eval interpreter and operate on already parsed +//! EvalIR call metadata or evaluated runtime-cell handles. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. +pub(in crate::interpreter) fn eval_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "abs" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.abs(*value)? + } + "addslashes" | "stripslashes" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_slashes_result(name, *value, values)? + } + "array_combine" => { + let [keys, values_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_combine_result(*keys, *values_array, values)? + } + "array_column" => { + let [array, column_key] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_column_result(*array, *column_key, values)? + } + "array_chunk" => { + let [array, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_chunk_result(*array, *length, values)? + } + "array_fill" => { + let [start, count, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_result(*start, *count, *value, values)? + } + "array_fill_keys" => { + let [keys, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_keys_result(*keys, *value, values)? + } + "array_filter" => match evaluated_args { + [array] => eval_array_filter_result(*array, None, None, context, values)?, + [array, callback] => { + eval_array_filter_result(*array, Some(*callback), None, context, values)? + } + [array, callback, mode] => { + eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_map" => { + let Some((callback, arrays)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_map_result(*callback, arrays, context, values)? + } + "array_reduce" => match evaluated_args { + [array, callback] => { + let initial = values.null()?; + eval_array_reduce_result(*array, *callback, initial, context, values)? + } + [array, callback, initial] => { + eval_array_reduce_result(*array, *callback, *initial, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_walk" => { + let [array, callback] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_walk_result(*array, *callback, context, values)? + } + "array_pop" | "array_shift" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_pop_shift_value_result(name, *array, values)? + } + "array_push" | "array_unshift" => { + let Some((array, inserted)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_push_unshift_count_result(*array, inserted.len(), values)? + } + "array_splice" => { + let result = match evaluated_args { + [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, + [array, offset, length] => { + eval_array_splice_value_result(*array, *offset, Some(*length), values)? + } + [array, offset, length, _replacement] => { + eval_array_splice_value_result(*array, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.warning( + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + )?; + result + } + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_sort_value_result(*array, values)? + } + "uasort" | "uksort" | "usort" => { + let [array, callback] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_user_sort_value_result(name, *array, *callback, context, values)? + } + "array_flip" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_flip_result(*array, values)? + } + "array_pad" => { + let [array, length, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_pad_result(*array, *length, *value, values)? + } + "array_product" | "array_sum" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_aggregate_result(name, *array, values)? + } + "array_keys" | "array_values" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_projection_result(name, *array, values)? + } + "array_key_exists" => { + let [key, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.array_key_exists(*key, *array)? + } + "array_diff" | "array_intersect" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_value_set_result(name, *left, *right, values)? + } + "array_diff_key" | "array_intersect_key" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_key_set_result(name, *left, *right, values)? + } + "array_merge" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_merge_result(*left, *right, values)? + } + "array_rand" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_rand_result(*array, values)? + } + "array_reverse" => match evaluated_args { + [array] => eval_array_reverse_result(*array, false, values)?, + [array, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_reverse_result(*array, preserve_keys, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_search" | "in_array" => { + let [needle, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_search_result(name, *needle, *array, values)? + } + "array_slice" => match evaluated_args { + [array, offset] => eval_array_slice_result(*array, *offset, None, values)?, + [array, offset, length] => { + eval_array_slice_result(*array, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_unique" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_unique_result(*array, values)? + } + "range" => { + let [start, end] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_range_result(*start, *end, values)? + } + "base64_encode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_encode_result(*value, values)? + } + "base64_decode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_decode_result(*value, values)? + } + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_unary_result(name, *value, values)? + } + "atan2" | "hypot" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_pair_result(name, *left, *right, values)? + } + "bin2hex" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_bin2hex_result(*value, values)? + } + "ceil" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.ceil(*value)? + } + "chr" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chr_result(*value, values)? + } + "chdir" | "mkdir" | "rmdir" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unary_path_bool_result(name, *path, values)? + } + "chmod" => { + let [filename, permissions] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chmod_result(*filename, *permissions, values)? + } + "clearstatcache" => { + if evaluated_args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + values.null()? + } + "clamp" => { + let [value, min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_clamp_result(*value, *min, *max, values)? + } + "copy" | "link" | "rename" | "symlink" => { + let [from, to] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_binary_path_bool_result(name, *from, *to, values)? + } + "floor" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.floor(*value)? + } + "fdiv" | "fmod" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_binary_result(name, *left, *right, values)? + } + "file" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_result(*filename, values)? + } + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_probe_result(name, *filename, values)? + } + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_stat_scalar_result(name, *filename, values)? + } + "file_get_contents" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_get_contents_result(*filename, values)? + } + "file_put_contents" => { + let [filename, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_put_contents_result(*filename, *data, values)? + } + "filesize" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_filesize_result(*filename, values)? + } + "filetype" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_filetype_result(*filename, values)? + } + "fnmatch" => match evaluated_args { + [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, + [pattern, filename, flags] => { + eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stat" | "lstat" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stat_array_result(name, *filename, values)? + } + "linkinfo" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_linkinfo_result(*path, values)? + } + "log" => match evaluated_args { + [num] => eval_log_result(*num, None, values)?, + [num, base] => eval_log_result(*num, Some(*base), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "readfile" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_readfile_result(*filename, values)? + } + "pi" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI)? + } + "php_uname" => match evaluated_args { + [] => eval_php_uname_result(None, values)?, + [mode] => eval_php_uname_result(Some(*mode), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "pow" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.pow(*left, *right)? + } + "preg_match" => match evaluated_args { + [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_match_all" => match evaluated_args { + [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_replace" => match evaluated_args { + [pattern, replacement, subject] => { + eval_preg_replace_result(*pattern, *replacement, *subject, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_replace_callback" => match evaluated_args { + [pattern, callback, subject] => { + eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_split" => match evaluated_args { + [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values)?, + [pattern, subject, limit] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), None, values)? + } + [pattern, subject, limit, flags] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "print_r" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_print_r_result(*value, values)? + } + "var_dump" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_var_dump_result(*value, values)? + } + "rand" | "mt_rand" => match evaluated_args { + [] => eval_rand_result(None, None, values)?, + [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "random_int" => { + let [min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_random_int_result(*min, *max, values)? + } + "rawurldecode" | "urldecode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_decode_result(name, *value, values)? + } + "rawurlencode" | "urlencode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_encode_result(name, *value, values)? + } + "round" => match evaluated_args { + [value] => values.round(*value, None)?, + [value, precision] => values.round(*value, Some(*precision))?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "scandir" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_scandir_result(*directory, values)? + } + "sqrt" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.sqrt(*value)? + } + "spl_classes" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values)? + } + "spl_object_id" | "spl_object_hash" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_spl_object_identity_result(name, *object, values)? + } + "sscanf" => { + let [input, format, ..] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sscanf_result(*input, *format, values)? + } + "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, + "settype" => { + let [value, type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_settype_value_result(*value, *type_name, values)? + } + "strrev" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.strrev(*value)? + } + "str_repeat" => { + let [value, times] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_repeat_result(*value, *times, values)? + } + "str_replace" | "str_ireplace" => { + let [search, replace, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_replace_result(name, *search, *replace, *subject, values)? + } + "str_pad" => match evaluated_args { + [value, length] => eval_str_pad_result(*value, *length, None, None, values)?, + [value, length, pad_string] => { + eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? + } + [value, length, pad_string, pad_type] => { + eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "str_split" => match evaluated_args { + [value] => eval_str_split_result(*value, None, values)?, + [value, length] => eval_str_split_result(*value, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "substr" => match evaluated_args { + [value, offset] => eval_substr_result(*value, *offset, None, values)?, + [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "substr_replace" => match evaluated_args { + [value, replace, offset] => { + eval_substr_replace_result(*value, *replace, *offset, None, values)? + } + [value, replace, offset, length] => { + eval_substr_replace_result(*value, *replace, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "call_user_func" => { + return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) + .map(Some); + } + "call_user_func_array" => { + let [callback, arg_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) + .map(Some); + } + "boolval" | "floatval" | "intval" | "strval" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_cast_result(name, *value, values)? + } + "count" => match evaluated_args { + [value] => eval_count_result(*value, None, values)?, + [value, mode] => eval_count_result(*value, Some(*mode), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "crc32" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_crc32_result(*value, values)? + } + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ctype_result(name, *value, values)? + } + "date" => match evaluated_args { + [format] => eval_date_result(*format, None, values)?, + [format, timestamp] => eval_date_result(*format, Some(*timestamp), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "define" => eval_define_result(evaluated_args, context, values)?, + "defined" => eval_defined_result(evaluated_args, context, values)?, + "explode" => { + let [separator, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_explode_result(*separator, *string, values)? + } + "ord" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ord_result(*value, values)? + } + "implode" => { + let [separator, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_implode_result(*separator, *array, values)? + } + "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, + "microtime" => match evaluated_args { + [] | [_] => eval_microtime_result(values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "mktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? + } + "nl2br" => match evaluated_args { + [value] => eval_nl2br_result(*value, true, values)?, + [value, use_xhtml] => { + let use_xhtml = values.truthy(*use_xhtml)?; + eval_nl2br_result(*value, use_xhtml, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "number_format" => match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values)?, + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values)? + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + )?, + [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "basename" => match evaluated_args { + [path] => eval_basename_result(*path, None, values)?, + [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "dirname" => match evaluated_args { + [path] => eval_dirname_result(*path, None, values)?, + [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "disk_free_space" | "disk_total_space" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_disk_space_result(name, *directory, values)? + } + "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { + [value] => eval_trim_like_result(name, *value, None, values)?, + [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "function_exists" | "is_callable" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = values.string_bytes(*name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\').to_ascii_lowercase(); + values.bool_value(eval_function_probe_exists(context, &name))? + } + "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "enum_exists" | "trait_exists" => { + eval_class_like_exists_result(name, evaluated_args, values)? + } + "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, + "is_a" | "is_subclass_of" => { + eval_is_a_relation_result(name, evaluated_args, context, values)? + } + "json_decode" => match evaluated_args { + [json] => eval_json_decode_result(*json, None, None, None, context, values)?, + [json, associative] => { + eval_json_decode_result(*json, Some(*associative), None, None, context, values)? + } + [json, associative, depth] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + None, + context, + values, + )?, + [json, associative, depth, flags] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + Some(*flags), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "json_encode" => match evaluated_args { + [value] => eval_json_encode_result(*value, None, None, context, values)?, + [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values)?, + [value, flags, depth] => { + eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "json_last_error" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.int(context.json_last_error())? + } + "json_last_error_msg" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string(context.json_last_error_msg())? + } + "json_validate" => match evaluated_args { + [json] => eval_json_validate_result(*json, None, None, context, values)?, + [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values)?, + [json, depth, flags] => { + eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "gethostbyaddr" => { + let [ip] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyaddr_result(*ip, values)? + } + "gethostbyname" => { + let [hostname] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyname_result(*hostname, values)? + } + "gethostname" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_gethostname_result(values)? + } + "getprotobyname" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobyname_result(*protocol, values)? + } + "getprotobynumber" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobynumber_result(*protocol, values)? + } + "getservbyname" => { + let [service, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyname_result(*service, *protocol, values)? + } + "getservbyport" => { + let [port, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyport_result(*port, *protocol, values)? + } + "getcwd" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values)? + } + "getenv" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getenv_result(*name, values)? + } + "get_class" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_get_class_result(*object, context, values)? + } + "get_parent_class" => { + let [object_or_class] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_get_parent_class_result(*object_or_class, values)? + } + "get_resource_id" | "get_resource_type" => { + let [resource] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_resource_introspection_result(name, *resource, values)? + } + "gettype" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gettype_result(*value, values)? + } + "glob" => { + let [pattern] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_glob_result(*pattern, values)? + } + "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { + eval_hash_one_shot_result(name, evaluated_args, values)? + } + "hash_algos" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values)? + } + "hash_equals" => { + let [known, user] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_equals_result(*known, *user, values)? + } + "hex2bin" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hex2bin_result(*value, values)? + } + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_html_entity_result(name, *value, values)? + } + "inet_ntop" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_ntop_result(*value, values)? + } + "inet_pton" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_pton_result(*value, values)? + } + "intdiv" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_intdiv_result(*left, *right, values)? + } + "iterator_apply" => match evaluated_args { + [iterator, callback] => { + let callback = eval_callable(*callback, values)?; + eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? + } + [iterator, callback, args] => { + let callback = eval_callable(*callback, values)?; + let callback_args = eval_iterator_apply_arg_values(*args, values)?; + eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "iterator_count" => { + let [iterator] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_iterator_count_result(*iterator, values)? + } + "iterator_to_array" => match evaluated_args { + [iterator] => eval_iterator_to_array_result(*iterator, true, values)?, + [iterator, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_iterator_to_array_result(*iterator, preserve_keys, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "ip2long" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ip2long_result(*value, values)? + } + "phpversion" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values)? + } + "pathinfo" => match evaluated_args { + [path] => eval_pathinfo_result(*path, None, values)?, + [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "putenv" => { + let [assignment] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_putenv_result(*assignment, values)? + } + "realpath" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_realpath_result(*path, values)? + } + "realpath_cache_get" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values)? + } + "realpath_cache_size" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values)? + } + "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" + | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" + | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_type_predicate_result(name, *value, values)? + } + "sys_get_temp_dir" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values)? + } + "tempnam" => { + let [directory, prefix] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_tempnam_result(*directory, *prefix, values)? + } + "sleep" => { + let [seconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sleep_result(*seconds, values)? + } + "time" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values)? + } + "touch" => match evaluated_args { + [filename] => eval_touch_result(*filename, None, None, values)?, + [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, + [filename, mtime, atime] => { + eval_touch_result(*filename, Some(*mtime), Some(*atime), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, values)? + } + "strtotime" => { + let [datetime] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_strtotime_result(*datetime, values)? + } + "umask" => match evaluated_args { + [] => eval_umask_result(None, values)?, + [mask] => eval_umask_result(Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "usleep" => { + let [microseconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_usleep_result(*microseconds, values)? + } + "readlink" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_readlink_result(*path, values)? + } + "unlink" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unlink_result(*filename, values)? + } + "strlen" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let bytes = values.string_bytes(*value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len)? + } + "strpos" | "strrpos" => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_position_result(name, *haystack, *needle, values)? + } + "str_contains" | "str_starts_with" | "str_ends_with" => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_search_result(name, *haystack, *needle, values)? + } + "strstr" => match evaluated_args { + [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values)?, + [haystack, needle, before_needle] => { + let before_needle = values.truthy(*before_needle)?; + eval_strstr_result(*haystack, *needle, before_needle, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "strcmp" | "strcasecmp" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_compare_result(name, *left, *right, values)? + } + "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_case_result(name, *value, values)? + } + "long2ip" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_long2ip_result(*value, values)? + } + "ucwords" => match evaluated_args { + [value] => eval_ucwords_result(*value, None, values)?, + [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, + "wordwrap" => match evaluated_args { + [value] => eval_wordwrap_result(*value, None, None, None, values)?, + [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, + [value, width, break_string] => { + eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values)? + } + [value, width, break_string, cut] => eval_wordwrap_result( + *value, + Some(*width), + Some(*break_string), + Some(*cut), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/mod.rs b/crates/elephc-eval/src/interpreter/builtins/registry/mod.rs new file mode 100644 index 0000000000..df9a2c1891 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/mod.rs @@ -0,0 +1,20 @@ +//! Purpose: +//! Groups builtin registry lookup, argument binding, callable dispatch, and +//! evaluated-argument builtin dispatch. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core eval call paths. +//! +//! Key details: +//! - The large by-value dispatch match is isolated from argument planning and +//! callable normalization. + +mod binding; +mod callable; +mod dispatch; +mod names; + +pub(in crate::interpreter) use binding::*; +pub(in crate::interpreter) use callable::*; +pub(in crate::interpreter) use dispatch::*; +pub(in crate::interpreter) use names::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs new file mode 100644 index 0000000000..4628a23698 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -0,0 +1,291 @@ +//! Purpose: +//! Builtin existence name table used by eval function probes. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Helpers are scoped to the eval interpreter and operate on already parsed +//! EvalIR call metadata or evaluated runtime-cell handles. + +/// Returns true for PHP-visible builtin names implemented by the eval interpreter. +pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> bool { + matches!( + name, + "abs" + | "addslashes" + | "array_chunk" + | "array_column" + | "array_combine" + | "array_fill" + | "array_fill_keys" + | "array_filter" + | "array_flip" + | "array_map" + | "array_reduce" + | "array_walk" + | "array_key_exists" + | "array_keys" + | "array_diff" + | "array_intersect" + | "array_diff_key" + | "array_intersect_key" + | "array_merge" + | "array_pad" + | "array_pop" + | "array_product" + | "array_push" + | "array_rand" + | "array_reverse" + | "array_search" + | "array_shift" + | "array_slice" + | "array_splice" + | "array_sum" + | "array_unique" + | "array_unshift" + | "array_values" + | "arsort" + | "asort" + | "acos" + | "asin" + | "atan" + | "atan2" + | "basename" + | "base64_decode" + | "base64_encode" + | "bin2hex" + | "ceil" + | "chdir" + | "chmod" + | "call_user_func" + | "call_user_func_array" + | "class_exists" + | "enum_exists" + | "interface_exists" + | "is_a" + | "is_subclass_of" + | "boolval" + | "chop" + | "chr" + | "clamp" + | "clearstatcache" + | "count" + | "copy" + | "cos" + | "cosh" + | "crc32" + | "ctype_alnum" + | "ctype_alpha" + | "ctype_digit" + | "ctype_space" + | "date" + | "define" + | "defined" + | "deg2rad" + | "dirname" + | "disk_free_space" + | "disk_total_space" + | "exp" + | "explode" + | "fdiv" + | "file" + | "file_exists" + | "fileatime" + | "filectime" + | "filegroup" + | "file_get_contents" + | "fileinode" + | "filemtime" + | "fileowner" + | "fileperms" + | "file_put_contents" + | "filesize" + | "filetype" + | "fnmatch" + | "floor" + | "floatval" + | "fmod" + | "function_exists" + | "gethostbyaddr" + | "gethostbyname" + | "gethostname" + | "getprotobyname" + | "getprotobynumber" + | "getservbyname" + | "getservbyport" + | "get_class" + | "get_parent_class" + | "get_resource_id" + | "get_resource_type" + | "getcwd" + | "getenv" + | "gettype" + | "glob" + | "hash" + | "hash_algos" + | "hash_equals" + | "hash_file" + | "hash_hmac" + | "hex2bin" + | "html_entity_decode" + | "htmlentities" + | "htmlspecialchars" + | "hypot" + | "implode" + | "in_array" + | "inet_ntop" + | "inet_pton" + | "intdiv" + | "ip2long" + | "is_dir" + | "is_executable" + | "is_file" + | "is_link" + | "is_readable" + | "is_writable" + | "is_writeable" + | "intval" + | "link" + | "linkinfo" + | "ltrim" + | "is_callable" + | "is_array" + | "is_bool" + | "is_double" + | "is_finite" + | "is_float" + | "is_infinite" + | "is_int" + | "is_integer" + | "is_iterable" + | "is_long" + | "is_nan" + | "is_null" + | "is_numeric" + | "is_object" + | "is_real" + | "is_resource" + | "is_string" + | "iterator_apply" + | "iterator_count" + | "iterator_to_array" + | "json_decode" + | "json_encode" + | "json_last_error" + | "json_last_error_msg" + | "json_validate" + | "krsort" + | "ksort" + | "lcfirst" + | "log" + | "log2" + | "log10" + | "long2ip" + | "max" + | "md5" + | "microtime" + | "min" + | "mkdir" + | "mktime" + | "mt_rand" + | "natcasesort" + | "natsort" + | "nl2br" + | "number_format" + | "ord" + | "pathinfo" + | "pi" + | "pow" + | "php_uname" + | "phpversion" + | "preg_match" + | "preg_match_all" + | "preg_replace" + | "preg_replace_callback" + | "preg_split" + | "putenv" + | "print_r" + | "rand" + | "random_int" + | "range" + | "rad2deg" + | "rawurldecode" + | "rawurlencode" + | "readfile" + | "readlink" + | "realpath" + | "realpath_cache_get" + | "realpath_cache_size" + | "rename" + | "rsort" + | "rtrim" + | "round" + | "rmdir" + | "scandir" + | "settype" + | "sleep" + | "sha1" + | "shuffle" + | "sin" + | "sinh" + | "sort" + | "sqrt" + | "spl_classes" + | "spl_object_hash" + | "spl_object_id" + | "sscanf" + | "sprintf" + | "strcasecmp" + | "stream_get_filters" + | "stream_get_transports" + | "stream_get_wrappers" + | "str_contains" + | "str_ends_with" + | "str_ireplace" + | "str_repeat" + | "str_replace" + | "str_starts_with" + | "strcmp" + | "stat" + | "strlen" + | "strpos" + | "strrpos" + | "strrev" + | "str_pad" + | "str_split" + | "strstr" + | "strtotime" + | "substr" + | "stripslashes" + | "strtolower" + | "strtoupper" + | "strval" + | "symlink" + | "sys_get_temp_dir" + | "tempnam" + | "tan" + | "tanh" + | "time" + | "touch" + | "trait_exists" + | "trim" + | "substr_replace" + | "ucfirst" + | "ucwords" + | "uasort" + | "uksort" + | "unlink" + | "umask" + | "urldecode" + | "urlencode" + | "usort" + | "usleep" + | "var_dump" + | "printf" + | "vprintf" + | "vsprintf" + | "wordwrap" + | "lstat" + ) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars.rs b/crates/elephc-eval/src/interpreter/builtins/scalars.rs new file mode 100644 index 0000000000..9ff2a0e868 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars.rs @@ -0,0 +1,1338 @@ +//! Purpose: +//! Scalar conversion, math helpers, type predicates, and trailing string-search builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +use super::super::*; +use super::*; + +/// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. +pub(in crate::interpreter) fn eval_crc32_bytes(bytes: &[u8]) -> u32 { + let mut crc = 0xffff_ffff_u32; + for byte in bytes { + crc ^= u32::from(*byte); + for _ in 0..8 { + let mask = 0_u32.wrapping_sub(crc & 1); + crc = (crc >> 1) ^ (0xedb8_8320 & mask); + } + } + !crc +} + +/// Casts one eval value to PHP int and returns the scalar payload. +pub(in crate::interpreter) fn eval_int_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_int(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP's `bin2hex(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_bin2hex( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_bin2hex_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. +pub(in crate::interpreter) fn eval_bin2hex_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.string(&eval_lower_hex_bytes(&bytes)) +} + +/// Converts bytes to lowercase hexadecimal text. +pub(in crate::interpreter) fn eval_lower_hex_bytes(bytes: &[u8]) -> String { + let mut output = String::with_capacity(bytes.len() * 2); + const HEX: &[u8; 16] = b"0123456789abcdef"; + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} + +/// Evaluates PHP's `hex2bin(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hex2bin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_hex2bin_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. +pub(in crate::interpreter) fn eval_hex2bin_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + if bytes.len() % 2 != 0 { + values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; + return values.bool_value(false); + } + let mut output = Vec::with_capacity(bytes.len() / 2); + for pair in bytes.chunks_exact(2) { + let Some(high) = eval_hex_nibble(pair[0]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + let Some(low) = eval_hex_nibble(pair[1]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + output.push((high << 4) | low); + } + values.string_bytes_value(&output) +} + +/// Returns the four-bit value for one hexadecimal byte. +pub(in crate::interpreter) fn eval_hex_nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} + +/// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_slashes( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_slashes_result(name, value, values) +} + +/// Applies PHP byte-string escaping or unescaping for addslashes/stripslashes. +pub(in crate::interpreter) fn eval_slashes_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "addslashes" => eval_addslashes_result(value, values), + "stripslashes" => eval_stripslashes_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. +pub(in crate::interpreter) fn eval_addslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + 0 => output.extend_from_slice(b"\\0"), + b'\'' | b'"' | b'\\' => { + output.push(b'\\'); + output.push(byte); + } + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Removes backslash quoting using PHP `stripslashes()` byte semantics. +pub(in crate::interpreter) fn eval_stripslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'\\' { + index += 1; + if let Some(byte) = bytes.get(index).copied() { + output.push(if byte == b'0' { 0 } else { byte }); + index += 1; + } + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP's `base64_encode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_base64_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_encode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns Base64 text. +pub(in crate::interpreter) fn eval_base64_encode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for chunk in bytes.chunks(3) { + let first = chunk[0]; + let second = chunk.get(1).copied().unwrap_or(0); + let third = chunk.get(2).copied().unwrap_or(0); + output.push(ALPHABET[(first >> 2) as usize] as char); + output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); + if chunk.len() > 1 { + output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); + } else { + output.push('='); + } + if chunk.len() > 2 { + output.push(ALPHABET[(third & 0x3f) as usize] as char); + } else { + output.push('='); + } + } + values.string(&output) +} + +/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_base64_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_decode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes Base64 bytes. +pub(in crate::interpreter) fn eval_base64_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(value)?; + let mut output = Vec::with_capacity((input.len() / 4) * 3); + let mut quartet = Vec::with_capacity(4); + for byte in input { + if byte.is_ascii_whitespace() { + continue; + } + if byte == b'=' { + quartet.push(None); + } else if let Some(value) = eval_base64_decode_sextet(byte) { + quartet.push(Some(value)); + } else { + continue; + } + if quartet.len() == 4 { + eval_push_base64_decoded_quartet(&quartet, &mut output); + quartet.clear(); + } + } + if !quartet.is_empty() { + while quartet.len() < 4 { + quartet.push(None); + } + eval_push_base64_decoded_quartet(&quartet, &mut output); + } + values.string_bytes_value(&output) +} + +/// Returns the six-bit Base64 value for one encoded byte. +pub(in crate::interpreter) fn eval_base64_decode_sextet(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(byte - b'A'), + b'a'..=b'z' => Some(byte - b'a' + 26), + b'0'..=b'9' => Some(byte - b'0' + 52), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } +} + +/// Appends decoded bytes for one padded or unpadded Base64 quartet. +pub(in crate::interpreter) fn eval_push_base64_decoded_quartet( + quartet: &[Option], + output: &mut Vec, +) { + let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { + return; + }; + output.push((first << 2) | (second >> 4)); + let Some(third) = quartet[2] else { + return; + }; + output.push(((second & 0x0f) << 4) | (third >> 2)); + let Some(fourth) = quartet[3] else { + return; + }; + output.push(((third & 0x03) << 6) | fourth); +} + +/// Evaluates PHP one-argument floating-point math builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_float_unary( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_float_unary_result(name, value, values) +} + +/// Dispatches an evaluated value through the matching PHP floating-point unary math function. +pub(in crate::interpreter) fn eval_float_unary_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let result = match name { + "acos" => value.acos(), + "asin" => value.asin(), + "atan" => value.atan(), + "cos" => value.cos(), + "cosh" => value.cosh(), + "deg2rad" => value.to_radians(), + "exp" => value.exp(), + "log2" => value.log2(), + "log10" => value.log10(), + "rad2deg" => value.to_degrees(), + "sin" => value.sin(), + "sinh" => value.sinh(), + "tan" => value.tan(), + "tanh" => value.tanh(), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.float(result) +} + +/// Evaluates PHP two-argument floating-point math builtins over eval expressions. +pub(in crate::interpreter) fn eval_builtin_float_pair( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_float_pair_result(name, left, right, values) +} + +/// Dispatches an evaluated pair through PHP `atan2()` or `hypot()`. +pub(in crate::interpreter) fn eval_float_pair_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_float_value(left, values)?; + let right = eval_float_value(right, values)?; + let result = match name { + "atan2" => left.atan2(right), + "hypot" => left.hypot(right), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.float(result) +} + +/// Evaluates PHP `log($num, $base = e)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_log( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [num] => { + let num = eval_expr(num, context, scope, values)?; + eval_log_result(num, None, values) + } + [num, base] => { + let num = eval_expr(num, context, scope, values)?; + let base = eval_expr(base, context, scope, values)?; + eval_log_result(num, Some(base), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `log()` from already evaluated arguments. +pub(in crate::interpreter) fn eval_log_result( + num: RuntimeCellHandle, + base: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + let result = match base { + Some(base) => num.log(eval_float_value(base, values)?), + None => num.ln(), + }; + values.float(result) +} + +/// Evaluates PHP `intdiv(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_intdiv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_intdiv_result(left, right, values) +} + +/// Computes PHP integer division from already evaluated arguments. +pub(in crate::interpreter) fn eval_intdiv_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_int_value(left, values)?; + let right = eval_int_value(right, values)?; + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; + values.int(result) +} + +/// Evaluates PHP floating-point binary math builtins over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_float_binary( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_float_binary_result(name, left, right, values) +} + +/// Dispatches an evaluated pair through the matching PHP float math hook. +pub(in crate::interpreter) fn eval_float_binary_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "fdiv" => values.fdiv(left, right), + "fmod" => values.fmod(left, right), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP `clamp($value, $min, $max)` over three eval expressions. +pub(in crate::interpreter) fn eval_builtin_clamp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_clamp_result(value, min, max, values) +} + +/// Selects the inclusive clamp result after validating bound order and NaN bounds. +pub(in crate::interpreter) fn eval_clamp_result( + value: RuntimeCellHandle, + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; + if values.truthy(invalid_bounds)? { + return Err(EvalStatus::RuntimeFatal); + } + let above_max = values.compare(EvalBinOp::Gt, value, max)?; + if values.truthy(above_max)? { + return Ok(max); + } + let below_min = values.compare(EvalBinOp::Lt, value, min)?; + if values.truthy(below_min)? { + return Ok(min); + } + Ok(value) +} + +/// Returns whether a clamp bound is a floating-point NaN value. +pub(in crate::interpreter) fn eval_clamp_bound_is_nan( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_FLOAT { + return Ok(false); + } + Ok(eval_float_value(value, values)?.is_nan()) +} + +/// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_min_max( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_min_max_result(name, &evaluated_args, values) +} + +/// Selects the smallest or largest evaluated cell using runtime comparison hooks. +pub(in crate::interpreter) fn eval_min_max_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((&first, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let op = match name { + "min" => EvalBinOp::Lt, + "max" => EvalBinOp::Gt, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let mut selected = first; + for candidate in rest { + let better = values.compare(op, *candidate, selected)?; + if values.truthy(better)? { + selected = *candidate; + } + } + Ok(selected) +} + +/// Evaluates PHP scalar cast builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_cast( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_cast_result(name, value, values) +} + +/// Dispatches an already evaluated value through the matching PHP cast hook. +pub(in crate::interpreter) fn eval_cast_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "intval" => values.cast_int(value), + "floatval" => values.cast_float(value), + "strval" => values.cast_string(value), + "boolval" => values.cast_bool(value), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP's `gettype(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gettype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_gettype_result(value, values) +} + +/// Converts one boxed runtime tag into PHP's `gettype()` spelling. +pub(in crate::interpreter) fn eval_gettype_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.string(eval_gettype_name(tag)) +} + +/// Evaluates PHP's `get_class(...)` over one eval object expression. +pub(in crate::interpreter) fn eval_builtin_get_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_get_class_result(object, context, values) +} + +/// Resolves the PHP-visible class name for one already materialized object cell. +pub(in crate::interpreter) fn eval_get_class_result( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return values.string(class.name().trim_start_matches('\\')); + } + } + values.object_class_name(object) +} + +/// Evaluates PHP's SPL object identity builtins over one eval object expression. +pub(in crate::interpreter) fn eval_builtin_spl_object_identity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_spl_object_identity_result(name, object, values) +} + +/// Returns the unboxed object-payload identity in the native SPL builtin spelling. +pub(in crate::interpreter) fn eval_spl_object_identity_result( + name: &str, + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)? as i64; + match name { + "spl_object_id" => values.int(identity), + "spl_object_hash" => values.string(&identity.to_string()), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. +pub(in crate::interpreter) fn eval_builtin_get_parent_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object_or_class] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object_or_class = eval_expr(object_or_class, context, scope, values)?; + eval_get_parent_class_result(object_or_class, values) +} + +/// Resolves the PHP-visible parent class name for one object or class-name cell. +pub(in crate::interpreter) fn eval_get_parent_class_result( + object_or_class: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.parent_class_name(object_or_class) +} + +/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. +pub(in crate::interpreter) fn eval_builtin_resource_introspection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [resource] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let resource = eval_expr(resource, context, scope, values)?; + eval_resource_introspection_result(name, resource, values) +} + +/// Evaluates a materialized resource introspection builtin argument. +pub(in crate::interpreter) fn eval_resource_introspection_result( + name: &str, + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + match name { + "get_resource_type" => values.string("stream"), + "get_resource_id" => values.cast_int(resource), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Returns the PHP-visible type name for a concrete eval runtime tag. +pub(in crate::interpreter) fn eval_gettype_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "integer", + EVAL_TAG_FLOAT => "double", + EVAL_TAG_STRING => "string", + EVAL_TAG_BOOL => "boolean", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_OBJECT => "object", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_NULL => "NULL", + _ => "NULL", + } +} + +/// Evaluates PHP scalar/container type predicate builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_type_predicate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_type_predicate_result(name, value, values) +} + +/// Converts a concrete runtime tag into a PHP `is_*` predicate result. +pub(in crate::interpreter) fn eval_type_predicate_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + let result = match name { + "is_int" | "is_integer" | "is_long" => tag == EVAL_TAG_INT, + "is_float" | "is_double" | "is_real" => tag == EVAL_TAG_FLOAT, + "is_string" => tag == EVAL_TAG_STRING, + "is_bool" => tag == EVAL_TAG_BOOL, + "is_null" => tag == EVAL_TAG_NULL, + "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_object" => tag == EVAL_TAG_OBJECT, + "is_resource" => tag == EVAL_TAG_RESOURCE, + "is_nan" => eval_float_value(value, values)?.is_nan(), + "is_infinite" => eval_float_value(value, values)?.is_infinite(), + "is_finite" => eval_float_value(value, values)?.is_finite(), + "is_numeric" => { + tag == EVAL_TAG_INT + || tag == EVAL_TAG_FLOAT + || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)) + } + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(result) +} + +/// Matches the static backend's legacy ASCII numeric-string scan. +pub(in crate::interpreter) fn eval_is_numeric_string(bytes: &[u8]) -> bool { + if bytes.is_empty() { + return false; + } + + let mut index = 0; + let mut consumed_digits = 0; + if bytes[index] == b'-' { + index += 1; + if index >= bytes.len() { + return false; + } + } + + while index < bytes.len() { + if bytes[index] == b'.' { + index += 1; + break; + } + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + while index < bytes.len() { + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + consumed_digits > 0 +} + +/// Evaluates PHP's `hash_equals(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_equals( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [known, user] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let known = eval_expr(known, context, scope, values)?; + let user = eval_expr(user, context, scope, values)?; + eval_hash_equals_result(known, user, values) +} + +/// Compares two converted strings with PHP `hash_equals()` semantics. +pub(in crate::interpreter) fn eval_hash_equals_result( + known: RuntimeCellHandle, + user: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let known = values.string_bytes(known)?; + let user = values.string_bytes(user)?; + if known.len() != user.len() { + return values.bool_value(false); + } + let mut diff = 0u8; + for (known, user) in known.iter().zip(user.iter()) { + diff |= known ^ user; + } + values.bool_value(diff == 0) +} + +/// Evaluates PHP string comparison builtins over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_string_compare( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_string_compare_result(name, left, right, values) +} + +/// Compares two converted strings and returns -1, 0, or 1. +pub(in crate::interpreter) fn eval_string_compare_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut left = values.string_bytes(left)?; + let mut right = values.string_bytes(right)?; + match name { + "strcmp" => {} + "strcasecmp" => { + left.make_ascii_lowercase(); + right.make_ascii_lowercase(); + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let result = match left.cmp(&right) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + }; + values.int(result) +} + +/// Evaluates PHP's byte-string search predicates over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_string_search( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_search_result(name, haystack, needle, values) +} + +/// Checks one converted haystack for one converted needle using PHP byte-string semantics. +pub(in crate::interpreter) fn eval_string_search_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let matched = match name { + "str_contains" => { + needle.is_empty() + || haystack + .windows(needle.len()) + .any(|window| window == needle) + } + "str_starts_with" => haystack.starts_with(&needle), + "str_ends_with" => haystack.ends_with(&needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(matched) +} + +/// Evaluates PHP byte-string position builtins over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_string_position( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_position_result(name, haystack, needle, values) +} + +/// Returns the first or last byte offset of a converted needle, or PHP `false`. +pub(in crate::interpreter) fn eval_string_position_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = match name { + "strpos" if needle.is_empty() => Some(0), + "strpos" => haystack + .windows(needle.len()) + .position(|window| window == needle), + "strrpos" if needle.is_empty() => Some(haystack.len()), + "strrpos" => haystack + .windows(needle.len()) + .rposition(|window| window == needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + match position { + Some(position) => { + let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(position) + } + None => values.bool_value(false), + } +} + +/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. +pub(in crate::interpreter) fn eval_builtin_strstr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [haystack, needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_strstr_result(haystack, needle, false, values) + } + [haystack, needle, before_needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + let before_needle = eval_expr(before_needle, context, scope, values)?; + let before_needle = values.truthy(before_needle)?; + eval_strstr_result(haystack, needle, before_needle, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. +pub(in crate::interpreter) fn eval_strstr_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + before_needle: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = if needle.is_empty() { + Some(0) + } else { + eval_find_subslice(&haystack, &needle, 0) + }; + let Some(position) = position else { + return values.bool_value(false); + }; + let result = if before_needle { + &haystack[..position] + } else { + &haystack[position..] + }; + values.string_bytes_value(result) +} + +pub(in crate::interpreter) const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; + +/// Evaluates PHP trim-like string builtins over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_trim_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_trim_like_result(name, value, None, values) + } + [value, mask] => { + let value = eval_expr(value, context, scope, values)?; + let mask = eval_expr(mask, context, scope, values)?; + eval_trim_like_result(name, value, Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Trims one converted string using PHP's default mask or a caller-provided byte mask. +pub(in crate::interpreter) fn eval_trim_like_result( + name: &str, + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let explicit_mask; + let trim_mask = if let Some(mask) = mask { + explicit_mask = values.string_bytes(mask)?; + explicit_mask.as_slice() + } else { + PHP_DEFAULT_TRIM_MASK + }; + + let mut start = 0; + let mut end = bytes.len(); + if matches!(name, "trim" | "ltrim") { + while start < end && trim_mask.contains(&bytes[start]) { + start += 1; + } + } + if matches!(name, "trim" | "rtrim" | "chop") { + while end > start && trim_mask.contains(&bytes[end - 1]) { + end -= 1; + } + } + if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { + return Err(EvalStatus::UnsupportedConstruct); + } + + let value = + String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} + +/// Evaluates PHP ASCII case-conversion string builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_string_case( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_string_case_result(name, value, values) +} + +/// Converts one eval value through PHP string conversion and ASCII case mapping. +pub(in crate::interpreter) fn eval_string_case_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + match name { + "strtolower" => { + for byte in &mut bytes { + if byte.is_ascii_uppercase() { + *byte += b'a' - b'A'; + } + } + } + "strtoupper" => { + for byte in &mut bytes { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + } + } + "ucfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { + bytes[0] -= b'a' - b'A'; + } + } + "lcfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { + bytes[0] += b'a' - b'A'; + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} + +/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. +pub(in crate::interpreter) fn eval_builtin_ucwords( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_ucwords_result(value, None, values) + } + [value, separators] => { + let value = eval_expr(value, context, scope, values)?; + let separators = eval_expr(separators, context, scope, values)?; + eval_ucwords_result(value, Some(separators), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. +pub(in crate::interpreter) fn eval_ucwords_result( + value: RuntimeCellHandle, + separators: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + let separators = match separators { + Some(separators) => values.string_bytes(separators)?, + None => b" \t\r\n\x0c\x0b".to_vec(), + }; + let mut word_start = true; + for byte in &mut bytes { + if separators.contains(byte) { + word_start = true; + } else if word_start { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + word_start = false; + } + } + values.string_bytes_value(&bytes) +} + +/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. +pub(in crate::interpreter) fn eval_builtin_wordwrap( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_wordwrap_result(value, None, None, None, values) + } + [value, width] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + eval_wordwrap_result(value, Some(width), None, None, values) + } + [value, width, break_string] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), None, values) + } + [value, width, break_string, cut] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + let cut = eval_expr(cut, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Wraps a byte string at PHP word boundaries and preserves existing newlines. +pub(in crate::interpreter) fn eval_wordwrap_result( + value: RuntimeCellHandle, + width: Option, + break_string: Option, + cut: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let width = match width { + Some(width) => eval_int_value(width, values)?, + None => 75, + }; + let break_string = match break_string { + Some(break_string) => values.string_bytes(break_string)?, + None => b"\n".to_vec(), + }; + if break_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let cut = match cut { + Some(cut) => values.truthy(cut)?, + None => false, + }; + if width == 0 && cut { + return Err(EvalStatus::RuntimeFatal); + } + if bytes.is_empty() { + return values.string_bytes_value(&bytes); + } + let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); + values.string_bytes_value(&output) +} + +/// Applies the core PHP word-wrap scan over already converted byte slices. +pub(in crate::interpreter) fn eval_wordwrap_bytes( + bytes: &[u8], + width: i64, + break_string: &[u8], + cut: bool, +) -> Vec { + if width < 0 && cut { + let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); + for byte in bytes { + output.extend_from_slice(break_string); + output.push(*byte); + } + return output; + } + + let width = width.max(0) as usize; + let mut output = Vec::with_capacity(bytes.len()); + let mut line_start = 0; + let mut last_space = None; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'\n' => { + output.extend_from_slice(&bytes[line_start..=index]); + index += 1; + line_start = index; + last_space = None; + } + b' ' => { + if index.saturating_sub(line_start) >= width { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + index += 1; + line_start = index; + last_space = None; + } else { + last_space = Some(index); + index += 1; + } + } + _ if index.saturating_sub(line_start) >= width => { + if let Some(space) = last_space { + output.extend_from_slice(&bytes[line_start..space]); + output.extend_from_slice(break_string); + line_start = space + 1; + last_space = None; + } else if cut && width > 0 { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + line_start = index; + } else { + index += 1; + } + } + _ => { + index += 1; + } + } + } + output.extend_from_slice(&bytes[line_start..]); + output +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings.rs b/crates/elephc-eval/src/interpreter/builtins/strings.rs new file mode 100644 index 0000000000..2cd6595188 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings.rs @@ -0,0 +1,982 @@ +//! Purpose: +//! String, hash, ctype, SPL registry, and stream-introspection builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +use super::super::*; +use super::*; + +/// Evaluates PHP's `sqrt(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sqrt( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.sqrt(value) +} + +/// Evaluates PHP's `strrev(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.strrev(value) +} + +/// Evaluates PHP's `chr(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_chr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_chr_result(value, values) +} + +/// Converts one eval value to a PHP integer and returns the low byte as a string. +pub(in crate::interpreter) fn eval_chr_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + values.string_bytes_value(&[value as u8]) +} + +/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. +pub(in crate::interpreter) fn eval_builtin_str_repeat( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, times] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let times = eval_expr(times, context, scope, values)?; + eval_str_repeat_result(value, times, values) +} + +/// Repeats one PHP string byte sequence according to a PHP-cast integer count. +pub(in crate::interpreter) fn eval_str_repeat_result( + value: RuntimeCellHandle, + times: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let times = eval_int_value(times, values)?; + if times < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; + let capacity = bytes + .len() + .checked_mul(times) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + for _ in 0..times { + output.extend_from_slice(&bytes); + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_str_replace( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [search, replace, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let search = eval_expr(search, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_str_replace_result(name, search, replace, subject, values) +} + +/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. +pub(in crate::interpreter) fn eval_str_replace_result( + name: &str, + search: RuntimeCellHandle, + replace: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let search = values.string_bytes(search)?; + let replace = values.string_bytes(replace)?; + let subject = values.string_bytes(subject)?; + if search.is_empty() { + return values.string_bytes_value(&subject); + } + + let mut output = Vec::with_capacity(subject.len()); + let mut start = 0; + while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { + output.extend_from_slice(&subject[start..found]); + output.extend_from_slice(&replace); + start = found + search.len(); + } + output.extend_from_slice(&subject[start..]); + values.string_bytes_value(&output) +} + +/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. +pub(in crate::interpreter) fn eval_find_replace_match( + name: &str, + subject: &[u8], + search: &[u8], + start: usize, +) -> Result, EvalStatus> { + match name { + "str_replace" => Ok(eval_find_subslice(subject, search, start)), + "str_ireplace" => Ok(subject + .get(start..) + .and_then(|tail| { + tail.windows(search.len()) + .position(|window| window.eq_ignore_ascii_case(search)) + }) + .map(|position| position + start)), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. +pub(in crate::interpreter) fn eval_builtin_str_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_pad_result(value, length, None, None, values) + } + [value, length, pad_string] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), None, values) + } + [value, length, pad_string, pad_type] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + let pad_type = eval_expr(pad_type, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Pads one byte string to a PHP target length using cyclic pad bytes. +pub(in crate::interpreter) fn eval_str_pad_result( + value: RuntimeCellHandle, + length: RuntimeCellHandle, + pad_string: Option, + pad_type: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let target_length = eval_int_value(length, values)?; + let Ok(target_length) = usize::try_from(target_length) else { + return values.string_bytes_value(&bytes); + }; + if target_length <= bytes.len() { + return values.string_bytes_value(&bytes); + } + + let pad_string = match pad_string { + Some(pad_string) => values.string_bytes(pad_string)?, + None => b" ".to_vec(), + }; + if pad_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let pad_type = match pad_type { + Some(pad_type) => eval_int_value(pad_type, values)?, + None => 1, + }; + let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; + let capacity = bytes + .len() + .checked_add(left_pad) + .and_then(|size| size.checked_add(right_pad)) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + eval_append_repeated_pad(&mut output, &pad_string, left_pad); + output.extend_from_slice(&bytes); + eval_append_repeated_pad(&mut output, &pad_string, right_pad); + values.string_bytes_value(&output) +} + +/// Splits a `str_pad()` pad budget into left and right byte counts. +pub(in crate::interpreter) fn eval_str_pad_sides( + pad_budget: usize, + pad_type: i64, +) -> Result<(usize, usize), EvalStatus> { + match pad_type { + 0 => Ok((pad_budget, 0)), + 1 => Ok((0, pad_budget)), + 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends `count` bytes by cycling through the provided non-empty pad string. +pub(in crate::interpreter) fn eval_append_repeated_pad( + output: &mut Vec, + pad_string: &[u8], + count: usize, +) { + for index in 0..count { + output.push(pad_string[index % pad_string.len()]); + } +} + +/// Evaluates PHP `str_split(...)` over one string and optional chunk length. +pub(in crate::interpreter) fn eval_builtin_str_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_str_split_result(value, None, values) + } + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_split_result(value, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. +pub(in crate::interpreter) fn eval_str_split_result( + value: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let length = match length { + Some(length) => eval_int_value(length, values)?, + None => 1, + }; + if length <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = values.array_new(0)?; + for (index, chunk) in bytes.chunks(length).enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string_bytes_value(chunk)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. +pub(in crate::interpreter) fn eval_builtin_nl2br( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_nl2br_result(value, true, values) + } + [value, use_xhtml] => { + let value = eval_expr(value, context, scope, values)?; + let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; + let use_xhtml = values.truthy(use_xhtml)?; + eval_nl2br_result(value, use_xhtml, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. +pub(in crate::interpreter) fn eval_nl2br_result( + value: RuntimeCellHandle, + use_xhtml: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let br = if use_xhtml { + b"
".as_slice() + } else { + b"
".as_slice() + }; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + let byte = bytes[index]; + if byte == b'\r' || byte == b'\n' { + output.extend_from_slice(br); + output.push(byte); + if index + 1 < bytes.len() + && ((byte == b'\r' && bytes[index + 1] == b'\n') + || (byte == b'\n' && bytes[index + 1] == b'\r')) + { + output.push(bytes[index + 1]); + index += 2; + continue; + } + } else { + output.push(byte); + } + index += 1; + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. +pub(in crate::interpreter) fn eval_builtin_substr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, offset] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_result(value, offset, None, values) + } + [value, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_result(value, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Slices a PHP byte string using PHP `substr()` offset and length rules. +pub(in crate::interpreter) fn eval_substr_result( + value: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(0) + } else { + start.saturating_add(length).min(total) + } + } + }; + let end = end.max(start); + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string_bytes_value(&bytes[start..end]) +} + +/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. +pub(in crate::interpreter) fn eval_builtin_substr_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, replace, offset] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, None, values) + } + [value, replace, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. +pub(in crate::interpreter) fn eval_substr_replace_result( + value: RuntimeCellHandle, + replace: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let replacement = values.string_bytes(replace)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(start) + } else { + start.saturating_add(length).min(total) + } + } + }; + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(bytes.len() + replacement.len()); + output.extend_from_slice(&bytes[..start]); + output.extend_from_slice(&replacement); + output.extend_from_slice(&bytes[end..]); + values.string_bytes_value(&output) +} + +/// Evaluates eval HTML entity encode/decode builtins over one string expression. +pub(in crate::interpreter) fn eval_builtin_html_entity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_html_entity_result(name, value, values) +} + +/// Applies the eval-supported HTML entity transform for one PHP string value. +pub(in crate::interpreter) fn eval_html_entity_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), + "html_entity_decode" => eval_html_entity_decode_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Encodes the HTML-special byte characters covered by elephc's static helper. +pub(in crate::interpreter) fn eval_htmlspecialchars_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + b'&' => output.extend_from_slice(b"&"), + b'<' => output.extend_from_slice(b"<"), + b'>' => output.extend_from_slice(b">"), + b'"' => output.extend_from_slice(b"""), + b'\'' => output.extend_from_slice(b"'"), + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Decodes one pass of the HTML entities emitted by the eval/static encoders. +pub(in crate::interpreter) fn eval_html_entity_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'&' { + if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { + output.push(decoded); + index += width; + continue; + } + } + output.push(bytes[index]); + index += 1; + } + values.string_bytes_value(&output) +} + +/// Returns the decoded byte and consumed width for one supported HTML entity. +pub(in crate::interpreter) fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { + for (entity, decoded) in [ + (b"<".as_slice(), b'<'), + (b">".as_slice(), b'>'), + (b""".as_slice(), b'"'), + (b"'".as_slice(), b'\''), + (b"'".as_slice(), b'\''), + (b"&".as_slice(), b'&'), + ] { + if bytes.starts_with(entity) { + return Some((decoded, entity.len())); + } + } + None +} + +/// Evaluates PHP URL encode builtins over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_url_encode( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_encode_result(name, value, values) +} + +/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. +pub(in crate::interpreter) fn eval_url_encode_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + for byte in bytes { + if eval_url_encode_keeps_byte(name, byte)? { + output.push(byte); + } else if name == "urlencode" && byte == b' ' { + output.push(b'+'); + } else { + output.push(b'%'); + output.push(HEX[(byte >> 4) as usize]); + output.push(HEX[(byte & 0x0f) as usize]); + } + } + values.string_bytes_value(&output) +} + +/// Returns whether a byte remains unescaped for the selected PHP URL encoder. +pub(in crate::interpreter) fn eval_url_encode_keeps_byte( + name: &str, + byte: u8, +) -> Result { + let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); + match name { + "urlencode" => Ok(common), + "rawurlencode" => Ok(common || byte == b'~'), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP URL decode builtins over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_url_decode( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_decode_result(name, value, values) +} + +/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. +pub(in crate::interpreter) fn eval_url_decode_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let plus_to_space = match name { + "urldecode" => true, + "rawurldecode" => false, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'+' && plus_to_space { + output.push(b' '); + index += 1; + } else if bytes[index] == b'%' && index + 2 < bytes.len() { + if let (Some(high), Some(low)) = ( + eval_hex_nibble(bytes[index + 1]), + eval_hex_nibble(bytes[index + 2]), + ) { + output.push((high << 4) | low); + index += 3; + continue; + } + output.push(bytes[index]); + index += 1; + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP `ctype_*` predicates over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ctype_result(name, value, values) +} + +/// Returns the PHP boolean result for one ASCII `ctype_*` byte-string check. +pub(in crate::interpreter) fn eval_ctype_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut matches = !bytes.is_empty(); + for byte in bytes { + if !eval_ctype_byte_matches(name, byte)? { + matches = false; + break; + } + } + values.bool_value(matches) +} + +/// Checks one byte against the selected PHP ASCII character class. +pub(in crate::interpreter) fn eval_ctype_byte_matches( + name: &str, + byte: u8, +) -> Result { + match name { + "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), + "ctype_digit" => Ok(byte.is_ascii_digit()), + "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), + "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP `crc32(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_crc32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_crc32_result(value, values) +} + +/// Computes PHP's non-negative CRC-32 integer over one converted byte string. +pub(in crate::interpreter) fn eval_crc32_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(eval_crc32_bytes(&bytes))) +} + +/// Evaluates one-shot PHP hash digest builtins over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_one_shot( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_hash_one_shot_result(name, &evaluated_args, values) +} + +/// Computes the result for one-shot PHP hash digest builtins from evaluated args. +pub(in crate::interpreter) fn eval_hash_one_shot_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "md5" | "sha1" => { + let (data, binary) = match evaluated_args { + [data] => (*data, false), + [data, binary] => (*data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let data = values.string_bytes(data)?; + eval_hash_digest_result(name.as_bytes(), &data, binary, values) + } + "hash" => { + let (algo, data, binary) = match evaluated_args { + [algo, data] => (*algo, *data, false), + [algo, data, binary] => (*algo, *data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + eval_hash_digest_result(&algo, &data, binary, values) + } + "hash_file" => { + let (algo, filename, binary) = match evaluated_args { + [algo, filename] => (*algo, *filename, false), + [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_hash_file_result(algo, filename, binary, values) + } + "hash_hmac" => { + let (algo, data, key, binary) = match evaluated_args { + [algo, data, key] => (*algo, *data, *key, false), + [algo, data, key, binary] => (*algo, *data, *key, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + let key = values.string_bytes(key)?; + eval_hash_hmac_result(&algo, &data, &key, binary, values) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Reads a local file and returns its PHP hash digest or false when it cannot be read. +pub(in crate::interpreter) fn eval_hash_file_result( + algo: RuntimeCellHandle, + filename: RuntimeCellHandle, + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(data) => eval_hash_digest_result(&algo, &data, binary, values), + Err(_) => values.bool_value(false), + } +} + +/// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. +pub(in crate::interpreter) fn eval_hash_digest_result( + algo: &[u8], + data: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hash(algo, data)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. +pub(in crate::interpreter) fn eval_hash_hmac_result( + algo: &[u8], + data: &[u8], + key: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hmac(algo, data, key)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. +pub(in crate::interpreter) fn eval_crypto_hash( + algo: &[u8], + data: &[u8], +) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hash( + algo.as_ptr(), + algo.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. +pub(in crate::interpreter) fn eval_crypto_hmac( + algo: &[u8], + data: &[u8], + key: &[u8], +) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hmac( + algo.as_ptr(), + algo.len(), + key.as_ptr(), + key.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Converts a crypto ABI digest length into an owned digest byte vector. +pub(in crate::interpreter) fn eval_crypto_digest_bytes( + len: isize, + output: &[u8; 64], +) -> Result, EvalStatus> { + let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + if len > output.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(output[..len].to_vec()) +} + +/// Formats a raw digest using PHP's `$binary` flag convention. +pub(in crate::interpreter) fn eval_format_digest_result( + raw: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if binary { + return values.string_bytes_value(raw); + } + values.string(&eval_lower_hex_bytes(raw)) +} + +/// Evaluates PHP `hash_algos()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_hash_algos( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + +/// Builds the indexed array returned by eval `hash_algos()`. +pub(in crate::interpreter) fn eval_hash_algos_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_HASH_ALGOS, values) +} + +/// Builds one indexed PHP array from a static string slice. +pub(in crate::interpreter) fn eval_static_string_array_result( + items: &[&str], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `spl_classes()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_spl_classes( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values) +} + +/// Builds the static class-name list returned by eval `spl_classes()`. +pub(in crate::interpreter) fn eval_spl_classes_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) +} + +/// Evaluates PHP stream introspection list builtins with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_introspection( + name: &str, + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, values) +} + +/// Builds the static list returned by one eval stream introspection builtin. +pub(in crate::interpreter) fn eval_stream_introspection_result( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let items = match name { + "stream_get_filters" => EVAL_STREAM_FILTERS, + "stream_get_transports" => EVAL_STREAM_TRANSPORTS, + "stream_get_wrappers" => EVAL_STREAM_WRAPPERS, + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_static_string_array_result(items, values) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs new file mode 100644 index 0000000000..ec2de32fd7 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -0,0 +1,406 @@ +//! Purpose: +//! Symbol, constant, class, and language-construct builtin probes. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +use super::super::*; +use super::*; + +pub(in crate::interpreter) fn eval_builtin_function_probe( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\').to_ascii_lowercase(); + values.bool_value(eval_function_probe_exists(context, &name)) +} + +/// Evaluates `define(name, value)` for eval dynamic constant-name registration. +pub(in crate::interpreter) fn eval_builtin_define( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + let defined = eval_define_name(name, value, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `defined(name)` against eval dynamic constant names. +pub(in crate::interpreter) fn eval_builtin_defined( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let exists = eval_defined_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `define(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_define_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let defined = eval_define_name(*name, *value, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `defined(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_defined_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let exists = eval_defined_name(*name, context, values)?; + values.bool_value(exists) +} + +/// Normalizes and registers one eval dynamic constant name. +pub(in crate::interpreter) fn eval_define_name( + name: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + if name.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if eval_predefined_constant_value(&name).is_some() || context.has_constant(&name) { + values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; + return Ok(false); + } + let value = values.retain(value)?; + if context.define_constant(&name, value) { + Ok(true) + } else { + values.release(value)?; + Ok(false) + } +} + +/// Normalizes and probes one eval dynamic constant name. +pub(in crate::interpreter) fn eval_defined_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) +} + +/// Reads a PHP constant name from a runtime cell without changing case. +pub(in crate::interpreter) fn eval_constant_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. +pub(in crate::interpreter) fn eval_builtin_class_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_exists_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `class_exists(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_class_exists_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_class_exists_name(*name, context, values)?, + [name, _autoload] => eval_class_exists_name(*name, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. +pub(in crate::interpreter) fn eval_class_exists_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\'); + if context.has_class(name) { + return Ok(true); + } + values.class_exists(name) +} + +/// Evaluates `interface_exists(...)` against generated interface-name metadata. +pub(in crate::interpreter) fn eval_builtin_interface_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_interface_exists_name(name, values)?; + values.bool_value(exists) +} + +/// Evaluates `interface_exists(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_interface_exists_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_interface_exists_name(*name, values)?, + [name, _autoload] => eval_interface_exists_name(*name, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP interface-name cell and probes generated interface metadata. +pub(in crate::interpreter) fn eval_interface_exists_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + values.interface_exists(name.trim_start_matches('\\')) +} + +/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. +pub(in crate::interpreter) fn eval_builtin_class_like_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = match args { + [symbol] => eval_expr(symbol, context, scope, values)?, + [symbol, autoload] => { + let symbol = eval_expr(symbol, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + symbol + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_like_exists_name(name, symbol, values)?; + values.bool_value(exists) +} + +/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. +pub(in crate::interpreter) fn eval_class_like_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [symbol] => eval_class_like_exists_name(name, *symbol, values)?, + [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. +pub(in crate::interpreter) fn eval_class_like_exists_name( + name: &str, + symbol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = values.string_bytes(symbol)?; + let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; + let symbol = symbol.trim_start_matches('\\'); + match name { + "trait_exists" => values.trait_exists(symbol), + "enum_exists" => values.enum_exists(symbol), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. +pub(in crate::interpreter) fn eval_builtin_is_a_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_is_a_relation_result(name, &evaluated_args, context, values) +} + +/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. +pub(in crate::interpreter) fn eval_is_a_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_or_class, target_class, allow_string) = match evaluated_args { + [object_or_class, target_class] => { + (*object_or_class, *target_class, name == "is_subclass_of") + } + [object_or_class, target_class, allow_string] => ( + *object_or_class, + *target_class, + values.truthy(*allow_string)?, + ), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let target_class = values.string_bytes(target_class)?; + let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_class = target_class.trim_start_matches('\\'); + let is_object = values.type_tag(object_or_class)? == 6; + let result = + if is_object && dynamic_object_is_a(object_or_class, target_class, context, values)? { + !matches!(name, "is_subclass_of") + } else if is_object || allow_string { + values.object_is_a(object_or_class, target_class, name == "is_subclass_of")? + } else { + false + }; + values.bool_value(result) +} + +/// Returns whether an eval-created object matches a dynamic class name exactly. +pub(in crate::interpreter) fn dynamic_object_is_a( + object: RuntimeCellHandle, + target_class: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + Ok(context + .dynamic_object_class(identity) + .is_some_and(|class| class.name().eq_ignore_ascii_case(target_class))) +} + +/// Evaluates PHP's `isset(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_builtin_isset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return values.bool_value(false); + } + for arg in args { + if !eval_isset_arg(arg, context, scope, values)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates PHP's `empty(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_builtin_empty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = eval_empty_arg(arg, context, scope, values)?; + values.bool_value(empty) +} + +/// Evaluates one `empty` operand without warning or failing on missing variables. +pub(in crate::interpreter) fn eval_empty_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(true); + }; + return Ok(!values.truthy(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.truthy(value)?) +} + +/// Evaluates one `isset` operand without allocating a null cell for missing variables. +pub(in crate::interpreter) fn eval_isset_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(false); + }; + return Ok(!values.is_null(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.is_null(value)?) +} + +/// Returns true when a PHP function name is visible to eval builtin probes. +pub(in crate::interpreter) fn eval_function_probe_exists( + context: &ElephcEvalContext, + name: &str, +) -> bool { + !name.contains("::") && (context.has_function(name) || eval_php_visible_builtin_exists(name)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/time.rs b/crates/elephc-eval/src/interpreter/builtins/time.rs new file mode 100644 index 0000000000..656eb84ab8 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/time.rs @@ -0,0 +1,570 @@ +//! Purpose: +//! Time, date, sleep, PHP version, and uname builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +use super::super::*; +use super::*; + +/// Evaluates PHP `time()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_time( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) +} + +/// Returns the current Unix timestamp as a boxed PHP integer. +pub(in crate::interpreter) fn eval_time_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(eval_current_unix_timestamp()?) +} + +/// Returns the current Unix timestamp as an integer payload. +pub(in crate::interpreter) fn eval_current_unix_timestamp() -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)? + .as_secs(); + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. +pub(in crate::interpreter) fn eval_builtin_date( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [format] => { + let format = eval_expr(format, context, scope, values)?; + eval_date_result(format, None, values) + } + [format, timestamp] => { + let format = eval_expr(format, context, scope, values)?; + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_date_result(format, Some(timestamp), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. +pub(in crate::interpreter) fn eval_date_result( + format: RuntimeCellHandle, + timestamp: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let format = values.string_bytes(format)?; + let timestamp = match timestamp { + Some(timestamp) => eval_int_value(timestamp, values)?, + None => eval_current_unix_timestamp()?, + }; + let tm = eval_localtime(timestamp)?; + let output = eval_format_date_bytes(&format, &tm, timestamp)?; + values.string_bytes_value(&output) +} + +/// Converts one Unix timestamp to local broken-down time through libc. +pub(in crate::interpreter) fn eval_localtime(timestamp: i64) -> Result { + let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; + let mut tm = MaybeUninit::::uninit(); + let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) +} + +/// Applies PHP `date()` tokens to one local broken-down timestamp. +pub(in crate::interpreter) fn eval_format_date_bytes( + format: &[u8], + tm: &libc::tm, + timestamp: i64, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut escaped = false; + for byte in format { + if escaped { + output.push(*byte); + escaped = false; + continue; + } + if *byte == b'\\' { + escaped = true; + continue; + } + eval_push_date_token(&mut output, *byte, tm, timestamp)?; + } + if escaped { + output.push(b'\\'); + } + Ok(output) +} + +/// Appends the expansion for one PHP `date()` token, or the token literal. +pub(in crate::interpreter) fn eval_push_date_token( + output: &mut Vec, + token: u8, + tm: &libc::tm, + timestamp: i64, +) -> Result<(), EvalStatus> { + match token { + b'Y' => eval_push_padded_number(output, i64::from(tm.tm_year) + 1900, 4), + b'm' => eval_push_padded_number(output, i64::from(tm.tm_mon) + 1, 2), + b'd' => eval_push_padded_number(output, i64::from(tm.tm_mday), 2), + b'H' => eval_push_padded_number(output, i64::from(tm.tm_hour), 2), + b'i' => eval_push_padded_number(output, i64::from(tm.tm_min), 2), + b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), + b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), + b'D' => output + .extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'M' => { + output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) + } + b'N' => { + let weekday = tm.tm_wday; + let iso_weekday = if weekday == 0 { 7 } else { weekday }; + output.extend_from_slice(iso_weekday.to_string().as_bytes()); + } + b'j' => output.extend_from_slice(tm.tm_mday.to_string().as_bytes()), + b'n' => output.extend_from_slice((tm.tm_mon + 1).to_string().as_bytes()), + b'G' => output.extend_from_slice(tm.tm_hour.to_string().as_bytes()), + b'g' => { + let hour = tm.tm_hour % 12; + let hour = if hour == 0 { 12 } else { hour }; + output.extend_from_slice(hour.to_string().as_bytes()); + } + b'A' => output.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }), + b'a' => output.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }), + b'U' => output.extend_from_slice(timestamp.to_string().as_bytes()), + _ => output.push(token), + } + Ok(()) +} + +/// Returns a checked month index for PHP `date()` name tables. +pub(in crate::interpreter) fn eval_tm_month_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_mon).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_MONTH_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Returns a checked weekday index for PHP `date()` name tables. +pub(in crate::interpreter) fn eval_tm_weekday_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_wday).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_WEEKDAY_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Appends one zero-padded decimal value with the requested minimum width. +pub(in crate::interpreter) fn eval_push_padded_number( + output: &mut Vec, + value: i64, + width: usize, +) { + output.extend_from_slice(format!("{value:0width$}").as_bytes()); +} + +/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. +pub(in crate::interpreter) fn eval_builtin_mktime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hour, minute, second, month, day, year] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hour = eval_expr(hour, context, scope, values)?; + let minute = eval_expr(minute, context, scope, values)?; + let second = eval_expr(second, context, scope, values)?; + let month = eval_expr(month, context, scope, values)?; + let day = eval_expr(day, context, scope, values)?; + let year = eval_expr(year, context, scope, values)?; + eval_mktime_result(hour, minute, second, month, day, year, values) +} + +/// Converts PHP date components to a local Unix timestamp through libc `mktime`. +pub(in crate::interpreter) fn eval_mktime_result( + hour: RuntimeCellHandle, + minute: RuntimeCellHandle, + second: RuntimeCellHandle, + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_mktime_timestamp( + eval_int_cell_as_c_int(hour, values)?, + eval_int_cell_as_c_int(minute, values)?, + eval_int_cell_as_c_int(second, values)?, + eval_int_cell_as_c_int(month, values)?, + eval_int_cell_as_c_int(day, values)?, + eval_int_cell_as_c_int(year, values)?, + )?; + values.int(timestamp) +} + +/// Converts local date components into a Unix timestamp through libc `mktime`. +pub(in crate::interpreter) fn eval_mktime_timestamp( + hour: libc::c_int, + minute: libc::c_int, + second: libc::c_int, + month: libc::c_int, + day: libc::c_int, + year: libc::c_int, +) -> Result { + let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = second; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_year = year - 1900; + tm.tm_isdst = -1; + let timestamp = unsafe { libc::mktime(&mut tm) }; + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. +pub(in crate::interpreter) fn eval_int_cell_as_c_int( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP `strtotime(datetime)` for eval's supported date-string subset. +pub(in crate::interpreter) fn eval_builtin_strtotime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [datetime] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let datetime = eval_expr(datetime, context, scope, values)?; + eval_strtotime_result(datetime, values) +} + +/// Parses one eval `strtotime()` input and boxes the resulting timestamp. +pub(in crate::interpreter) fn eval_strtotime_result( + datetime: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(datetime)?; + let timestamp = eval_strtotime_bytes(&bytes)?; + values.int(timestamp) +} + +/// Parses eval's supported `strtotime()` strings into local Unix timestamps. +pub(in crate::interpreter) fn eval_strtotime_bytes(bytes: &[u8]) -> Result { + let bytes = eval_trim_ascii_whitespace(bytes); + if bytes.eq_ignore_ascii_case(b"now") { + return eval_current_unix_timestamp(); + } + let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { + return Ok(-1); + }; + eval_mktime_timestamp(hour, minute, second, month, day, year) +} + +/// Trims ASCII whitespace from both ends of one byte slice. +pub(in crate::interpreter) fn eval_trim_ascii_whitespace(bytes: &[u8]) -> &[u8] { + let mut start = 0; + let mut end = bytes.len(); + while start < end && bytes[start].is_ascii_whitespace() { + start += 1; + } + while end > start && bytes[end - 1].is_ascii_whitespace() { + end -= 1; + } + &bytes[start..end] +} + +/// Parses fixed-width ISO date and datetime forms supported by eval `strtotime()`. +pub(in crate::interpreter) fn eval_parse_iso_datetime( + bytes: &[u8], +) -> Option<( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, +)> { + if bytes.len() != 10 && bytes.len() != 16 && bytes.len() != 19 { + return None; + } + if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { + return None; + } + let year = eval_parse_fixed_digits(bytes, 0, 4)?; + let month = eval_parse_fixed_digits(bytes, 5, 2)?; + let day = eval_parse_fixed_digits(bytes, 8, 2)?; + let (hour, minute, second) = if bytes.len() == 10 { + (0, 0, 0) + } else { + if !matches!(bytes.get(10), Some(b' ') | Some(b'T') | Some(b't')) { + return None; + } + if bytes.get(13) != Some(&b':') { + return None; + } + let hour = eval_parse_fixed_digits(bytes, 11, 2)?; + let minute = eval_parse_fixed_digits(bytes, 14, 2)?; + let second = if bytes.len() == 19 { + if bytes.get(16) != Some(&b':') { + return None; + } + eval_parse_fixed_digits(bytes, 17, 2)? + } else { + 0 + }; + (hour, minute, second) + }; + if !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || !(0..=23).contains(&hour) + || !(0..=59).contains(&minute) + || !(0..=59).contains(&second) + { + return None; + } + Some((year, month, day, hour, minute, second)) +} + +/// Parses a fixed-width decimal field as a libc-compatible integer. +pub(in crate::interpreter) fn eval_parse_fixed_digits( + bytes: &[u8], + start: usize, + len: usize, +) -> Option { + let end = start.checked_add(len)?; + let field = bytes.get(start..end)?; + let mut value: libc::c_int = 0; + for byte in field { + if !byte.is_ascii_digit() { + return None; + } + value = value.checked_mul(10)?; + value = value.checked_add(libc::c_int::from(byte - b'0'))?; + } + Some(value) +} + +/// Evaluates PHP `microtime()` with an optional ignored argument. +pub(in crate::interpreter) fn eval_builtin_microtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_microtime_result(values), + [as_float] => { + let _ = eval_expr(as_float, context, scope, values)?; + eval_microtime_result(values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the current Unix timestamp with microsecond precision as a boxed float. +pub(in crate::interpreter) fn eval_microtime_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)?; + let seconds = timestamp.as_secs() as f64; + let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; + values.float(seconds + micros) +} + +/// Evaluates PHP `sleep($seconds)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [seconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let seconds = eval_expr(seconds, context, scope, values)?; + eval_sleep_result(seconds, values) +} + +/// Sleeps for a non-negative number of seconds and returns PHP's remaining-seconds value. +pub(in crate::interpreter) fn eval_sleep_result( + seconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let seconds = eval_int_value(seconds, values)?; + let seconds = u64::try_from(seconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_secs(seconds)); + values.int(0) +} + +/// Evaluates PHP `usleep($microseconds)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_usleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [microseconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let microseconds = eval_expr(microseconds, context, scope, values)?; + eval_usleep_result(microseconds, values) +} + +/// Sleeps for a non-negative number of microseconds and returns PHP null. +pub(in crate::interpreter) fn eval_usleep_result( + microseconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let microseconds = eval_int_value(microseconds, values)?; + let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_micros(microseconds)); + values.null() +} + +/// Evaluates PHP `phpversion()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_phpversion( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values) +} + +/// Returns the root elephc package version as a boxed PHP string. +pub(in crate::interpreter) fn eval_phpversion_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(eval_compiler_php_version()) +} + +/// Reads the root package version from the workspace manifest used by native `phpversion()`. +pub(in crate::interpreter) fn eval_compiler_php_version() -> &'static str { + let mut in_package = false; + for line in EVAL_ROOT_CARGO_TOML.lines() { + let line = line.trim(); + if line == "[package]" { + in_package = true; + continue; + } + if in_package && line.starts_with('[') { + break; + } + if in_package { + if let Some(value) = line.strip_prefix("version = ") { + return value.trim_matches('"'); + } + } + } + env!("CARGO_PKG_VERSION") +} + +/// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. +pub(in crate::interpreter) fn eval_builtin_php_uname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_php_uname_result(None, values), + [mode] => { + let mode = eval_expr(mode, context, scope, values)?; + eval_php_uname_result(Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads the local uname fields and formats the PHP `php_uname()` mode result. +pub(in crate::interpreter) fn eval_php_uname_result( + mode: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => { + let bytes = values.string_bytes(mode)?; + let [mode] = bytes.as_slice() else { + return Err(EvalStatus::RuntimeFatal); + }; + *mode + } + None => b'a', + }; + + let mut utsname = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes all uname fields into the stack-owned utsname buffer. + libc::uname(utsname.as_mut_ptr()) + }; + if status != 0 { + return values.string(""); + } + let utsname = unsafe { + // `uname` succeeded, so libc initialized the full `utsname` structure. + utsname.assume_init() + }; + let sysname = eval_uname_field_bytes(&utsname.sysname); + let nodename = eval_uname_field_bytes(&utsname.nodename); + let release = eval_uname_field_bytes(&utsname.release); + let version = eval_uname_field_bytes(&utsname.version); + let machine = eval_uname_field_bytes(&utsname.machine); + + match mode { + b'a' => { + let mut output = Vec::new(); + for field in [&sysname, &nodename, &release, &version, &machine] { + if !output.is_empty() { + output.push(b' '); + } + output.extend_from_slice(field); + } + values.string_bytes_value(&output) + } + b's' => values.string_bytes_value(&sysname), + b'n' => values.string_bytes_value(&nodename), + b'r' => values.string_bytes_value(&release), + b'v' => values.string_bytes_value(&version), + b'm' => values.string_bytes_value(&machine), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. +pub(in crate::interpreter) fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { + let length = field + .iter() + .position(|byte| *byte == 0) + .unwrap_or(field.len()); + field[..length].iter().map(|byte| *byte as u8).collect() +} From 23530a739e604767dcdf25c4f0db5ff26ab3312f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:16:14 +0200 Subject: [PATCH 0284/1208] Split eval interpreter core modules --- .../src/interpreter/array_literals.rs | 145 + .../src/interpreter/constant_eval.rs | 178 + .../elephc-eval/src/interpreter/constants.rs | 250 + crates/elephc-eval/src/interpreter/control.rs | 70 + .../src/interpreter/core_builtins.rs | 117 + .../src/interpreter/debug_output.rs | 211 + .../src/interpreter/dynamic_functions.rs | 330 ++ .../src/interpreter/expressions.rs | 588 +++ .../src/interpreter/include_exec.rs | 198 + crates/elephc-eval/src/interpreter/json.rs | 922 ++++ .../elephc-eval/src/interpreter/libc_shims.rs | 45 + crates/elephc-eval/src/interpreter/mod.rs | 4018 +---------------- .../src/interpreter/runtime_ops.rs | 315 ++ .../src/interpreter/scope_cells.rs | 118 + .../elephc-eval/src/interpreter/statements.rs | 667 +++ 15 files changed, 4188 insertions(+), 3984 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/array_literals.rs create mode 100644 crates/elephc-eval/src/interpreter/constant_eval.rs create mode 100644 crates/elephc-eval/src/interpreter/constants.rs create mode 100644 crates/elephc-eval/src/interpreter/control.rs create mode 100644 crates/elephc-eval/src/interpreter/core_builtins.rs create mode 100644 crates/elephc-eval/src/interpreter/debug_output.rs create mode 100644 crates/elephc-eval/src/interpreter/dynamic_functions.rs create mode 100644 crates/elephc-eval/src/interpreter/expressions.rs create mode 100644 crates/elephc-eval/src/interpreter/include_exec.rs create mode 100644 crates/elephc-eval/src/interpreter/json.rs create mode 100644 crates/elephc-eval/src/interpreter/libc_shims.rs create mode 100644 crates/elephc-eval/src/interpreter/runtime_ops.rs create mode 100644 crates/elephc-eval/src/interpreter/scope_cells.rs create mode 100644 crates/elephc-eval/src/interpreter/statements.rs diff --git a/crates/elephc-eval/src/interpreter/array_literals.rs b/crates/elephc-eval/src/interpreter/array_literals.rs new file mode 100644 index 0000000000..d0673c6ff7 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/array_literals.rs @@ -0,0 +1,145 @@ +//! Purpose: +//! Builds EvalIR array literals and computes PHP-compatible next keys for mixed array construction. +//! +//! Called from: +//! - `crate::interpreter::eval_expr()` for indexed and associative array literal nodes. +//! +//! Key details: +//! - Explicit keys are normalized through runtime string conversion to match PHP array-key rules. +//! - Unkeyed elements continue from the next PHP integer key after explicit keys. + +use super::*; + +/// Evaluates an indexed array literal into a boxed runtime Mixed array. +pub(super) fn eval_indexed_array( + elements: &[EvalArrayElement], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array = values.array_new(elements.len())?; + for (index, element) in elements.iter().enumerate() { + let EvalArrayElement::Value(element) = element else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let index = values.int(index as i64)?; + let value = eval_expr(element, context, scope, values)?; + let _ = values.array_set(array, index, value)?; + } + Ok(array) +} + +/// Evaluates an associative array literal into a boxed runtime Mixed hash. +pub(super) fn eval_assoc_array( + elements: &[EvalArrayElement], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array = values.assoc_new(elements.len())?; + let mut next_key = None; + for element in elements { + let (key, value) = match element { + EvalArrayElement::Value(value) => { + let key = match next_key { + Some(next_key) => next_key, + None => values.int(0)?, + }; + let one = values.int(1)?; + next_key = Some(values.add(key, one)?); + (key, value) + } + EvalArrayElement::KeyValue { key, value } => { + let key = eval_expr(key, context, scope, values)?; + next_key = eval_array_next_key_after_explicit_key(key, next_key, values)?; + (key, value) + } + }; + let value = eval_expr(value, context, scope, values)?; + let _ = values.array_set(array, key, value)?; + } + Ok(array) +} + +/// Advances an array literal's automatic key after an integer-normalized explicit key. +fn eval_array_next_key_after_explicit_key( + key: RuntimeCellHandle, + current_next_key: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let key = match values.type_tag(key)? { + EVAL_TAG_INT => key, + EVAL_TAG_STRING => { + let bytes = values.string_bytes(key)?; + let Some(key) = eval_numeric_string_array_key(&bytes) else { + return Ok(current_next_key); + }; + values.int(key)? + } + EVAL_TAG_NULL => return Ok(current_next_key), + _ => values.cast_int(key)?, + }; + let one = values.int(1)?; + let candidate = values.add(key, one)?; + let replace = if let Some(current_next_key) = current_next_key { + let is_greater = values.compare(EvalBinOp::Gt, candidate, current_next_key)?; + values.truthy(is_greater)? + } else { + true + }; + Ok(if replace { + Some(candidate) + } else { + current_next_key + }) +} + +/// Parses PHP integer-string array keys that normalize to integer keys. +pub(in crate::interpreter) fn eval_numeric_string_array_key(bytes: &[u8]) -> Option { + if bytes.is_empty() { + return None; + } + + let (negative, digits) = if bytes[0] == b'-' { + if bytes.len() == 1 { + return None; + } + (true, &bytes[1..]) + } else { + (false, bytes) + }; + + if digits[0] == b'0' { + return if !negative && digits.len() == 1 { + Some(0) + } else { + None + }; + } + if digits.iter().any(|byte| !byte.is_ascii_digit()) { + return None; + } + + let limit = if negative { + i64::MAX as u128 + 1 + } else { + i64::MAX as u128 + }; + let mut value = 0u128; + for digit in digits { + value = (value * 10) + u128::from(digit - b'0'); + if value > limit { + return None; + } + } + + if negative { + if value == i64::MAX as u128 + 1 { + Some(i64::MIN) + } else { + Some(-(value as i64)) + } + } else { + Some(value as i64) + } +} diff --git a/crates/elephc-eval/src/interpreter/constant_eval.rs b/crates/elephc-eval/src/interpreter/constant_eval.rs new file mode 100644 index 0000000000..6e0617e992 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/constant_eval.rs @@ -0,0 +1,178 @@ +//! Purpose: +//! Evaluates EvalIR constants, dynamic constant fetches, predefined constants, and magic constants. +//! +//! Called from: +//! - `crate::interpreter::eval_expr()` for constant and magic-constant expression nodes. +//! +//! Key details: +//! - Dynamic constants prefer eval context declarations before predefined fallback constants. +//! - Magic file and directory values come from the current eval call-site context. + +use super::*; + +/// Converts one EvalIR constant into a runtime-cell handle. +pub(super) fn eval_const( + value: &EvalConst, + values: &mut impl RuntimeValueOps, +) -> Result { + match value { + EvalConst::Null => values.null(), + EvalConst::Bool(value) => values.bool_value(*value), + EvalConst::Int(value) => values.int(*value), + EvalConst::Float(value) => values.float(*value), + EvalConst::String(value) => values.string(value), + } +} + +/// Loads a retained value for one eval-defined dynamic constant. +pub(super) fn eval_const_fetch( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = eval_predefined_constant(name, values)? { + return Ok(value); + } + let Some(value) = context.constant(name) else { + return Err(EvalStatus::RuntimeFatal); + }; + values.retain(value) +} + +/// Fetches a namespaced constant and falls back to the global constant namespace. +pub(super) fn eval_namespaced_const_fetch( + name: &str, + fallback_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = eval_predefined_constant(name, values)? { + return Ok(value); + } + if let Some(value) = context.constant(name) { + return values.retain(value); + } + eval_const_fetch(fallback_name, context, values) +} + +/// Materializes one eval-visible predefined constant into a runtime cell. +fn eval_predefined_constant( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = eval_predefined_constant_value(name) else { + return Ok(None); + }; + match value { + EvalPredefinedConstant::Int(value) => values.int(value).map(Some), + EvalPredefinedConstant::Float(value) => values.float(value).map(Some), + EvalPredefinedConstant::String(value) => values.string(value).map(Some), + } +} + +/// Returns eval-visible predefined constants that do not live in dynamic context. +pub(in crate::interpreter) fn eval_predefined_constant_value( + name: &str, +) -> Option { + match name.trim_start_matches('\\') { + "PATHINFO_DIRNAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_DIRNAME)), + "PATHINFO_BASENAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_BASENAME)), + "PATHINFO_EXTENSION" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_EXTENSION)), + "PATHINFO_FILENAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_FILENAME)), + "PATHINFO_ALL" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_ALL)), + "FNM_NOESCAPE" => Some(EvalPredefinedConstant::Int(EVAL_FNM_NOESCAPE)), + "FNM_PATHNAME" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PATHNAME)), + "FNM_PERIOD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PERIOD)), + "FNM_CASEFOLD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_CASEFOLD)), + "ARRAY_FILTER_USE_VALUE" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_VALUE)), + "ARRAY_FILTER_USE_BOTH" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_BOTH)), + "ARRAY_FILTER_USE_KEY" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_KEY)), + "COUNT_NORMAL" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_NORMAL)), + "COUNT_RECURSIVE" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_RECURSIVE)), + "PREG_SPLIT_NO_EMPTY" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_NO_EMPTY)), + "PREG_SPLIT_DELIM_CAPTURE" => { + Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_DELIM_CAPTURE)) + } + "PREG_SPLIT_OFFSET_CAPTURE" => { + Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_OFFSET_CAPTURE)) + } + "PREG_PATTERN_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_PATTERN_ORDER)), + "PREG_SET_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SET_ORDER)), + "PREG_OFFSET_CAPTURE" => Some(EvalPredefinedConstant::Int(EVAL_PREG_OFFSET_CAPTURE)), + "PREG_UNMATCHED_AS_NULL" => Some(EvalPredefinedConstant::Int(EVAL_PREG_UNMATCHED_AS_NULL)), + "JSON_ERROR_NONE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_NONE)), + "JSON_ERROR_DEPTH" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_DEPTH)), + "JSON_ERROR_STATE_MISMATCH" => { + Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_STATE_MISMATCH)) + } + "JSON_ERROR_CTRL_CHAR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_CTRL_CHAR)), + "JSON_ERROR_SYNTAX" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_SYNTAX)), + "JSON_ERROR_UTF8" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF8)), + "JSON_ERROR_RECURSION" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_RECURSION)), + "JSON_ERROR_INF_OR_NAN" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_INF_OR_NAN)), + "JSON_ERROR_UNSUPPORTED_TYPE" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_ERROR_UNSUPPORTED_TYPE, + )), + "JSON_ERROR_INVALID_PROPERTY_NAME" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_ERROR_INVALID_PROPERTY_NAME, + )), + "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), + "JSON_HEX_TAG" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_TAG)), + "JSON_HEX_AMP" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_AMP)), + "JSON_HEX_APOS" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_APOS)), + "JSON_HEX_QUOT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_QUOT)), + "JSON_BIGINT_AS_STRING" => Some(EvalPredefinedConstant::Int(EVAL_JSON_BIGINT_AS_STRING)), + "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), + "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), + "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), + "JSON_UNESCAPED_UNICODE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_UNICODE)), + "JSON_PARTIAL_OUTPUT_ON_ERROR" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR, + )), + "JSON_PRETTY_PRINT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_PRETTY_PRINT)), + "JSON_PRESERVE_ZERO_FRACTION" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_PRESERVE_ZERO_FRACTION, + )), + "JSON_INVALID_UTF8_IGNORE" => { + Some(EvalPredefinedConstant::Int(EVAL_JSON_INVALID_UTF8_IGNORE)) + } + "JSON_INVALID_UTF8_SUBSTITUTE" => Some(EvalPredefinedConstant::Int( + EVAL_JSON_INVALID_UTF8_SUBSTITUTE, + )), + "JSON_THROW_ON_ERROR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_THROW_ON_ERROR)), + "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), + "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), + "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), + "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), + "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), + "DIRECTORY_SEPARATOR" => Some(EvalPredefinedConstant::String("/")), + _ => None, + } +} + +/// Returns the PHP OS constant for the host platform running the eval bridge. +fn eval_php_os_name() -> &'static str { + if cfg!(target_os = "macos") { + "Darwin" + } else { + "Linux" + } +} + +/// Resolves one eval magic constant against fragment and dynamic-call metadata. +pub(super) fn eval_magic_const( + magic: &EvalMagicConst, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match magic { + EvalMagicConst::File => values.string(&context.eval_file_magic()), + EvalMagicConst::Dir => values.string(context.call_dir()), + EvalMagicConst::Line(line) => values.int(*line), + EvalMagicConst::Function => values.string(context.current_function().unwrap_or("")), + EvalMagicConst::Method => values.string(context.current_function().unwrap_or("")), + EvalMagicConst::Class | EvalMagicConst::Namespace | EvalMagicConst::Trait => { + values.string("") + } + } +} diff --git a/crates/elephc-eval/src/interpreter/constants.rs b/crates/elephc-eval/src/interpreter/constants.rs new file mode 100644 index 0000000000..fe51911667 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/constants.rs @@ -0,0 +1,250 @@ +//! Purpose: +//! Defines eval-local PHP compatibility constants and static lookup tables. +//! Builtin modules read these tables to mirror native elephc behavior for dynamic eval. +//! +//! Called from: +//! - `crate::interpreter::builtins` domain modules. +//! - `crate::interpreter` constant and JSON helpers. +//! +//! Key details: +//! - Values here are PHP-visible compatibility data; changing them changes eval semantics. + +use std::sync::atomic::AtomicU64; + +/// Hash algorithm names supported by eval `hash_algos()`, matching native runtime order. +pub(super) const EVAL_HASH_ALGOS: &[&str] = &[ + "md2", + "md4", + "md5", + "sha1", + "sha224", + "sha256", + "sha384", + "sha512", + "sha512/224", + "sha512/256", + "sha3-224", + "sha3-256", + "sha3-384", + "sha3-512", + "ripemd128", + "ripemd160", + "ripemd256", + "ripemd320", + "whirlpool", + "crc32", + "crc32b", + "crc32c", + "adler32", + "fnv132", + "fnv1a32", + "fnv164", + "fnv1a64", + "joaat", +]; + +/// Built-in stream wrappers reported by eval `stream_get_wrappers()`. +pub(super) const EVAL_STREAM_WRAPPERS: &[&str] = &[ + "file", + "php", + "data", + "ftp", + "http", + "https", + "ftps", + "compress.zlib", + "compress.bzip2", + "phar", + "glob", +]; + +/// Built-in stream transports reported by eval `stream_get_transports()`. +pub(super) const EVAL_STREAM_TRANSPORTS: &[&str] = &[ + "tcp", "udp", "unix", "udg", "tls", "ssl", "sslv2", "sslv3", "tlsv1.0", "tlsv1.1", "tlsv1.2", + "tlsv1.3", +]; + +/// Monotonic salt mixed into eval `rand()`/`mt_rand()` and array key sampling. +pub(super) static EVAL_RANDOM_COUNTER: AtomicU64 = AtomicU64::new(0); + +/// Built-in stream filters reported by eval `stream_get_filters()`. +pub(super) const EVAL_STREAM_FILTERS: &[&str] = &[ + "string.toupper", + "string.tolower", + "string.rot13", + "string.strip_tags", + "convert.base64-encode", + "convert.base64-decode", + "convert.quoted-printable-encode", + "convert.quoted-printable-decode", + "convert.iconv.*", + "dechunk", + "zlib.deflate", + "zlib.inflate", + "bzip2.compress", + "bzip2.decompress", +]; + +/// SPL/core type names reported by eval `spl_classes()`. +/// +/// Mirrors `src/codegen/builtins/spl/mod.rs::SPL_CLASS_NAMES` so dynamic eval +/// exposes the same static registry snapshot as native code. +pub(super) const EVAL_SPL_CLASS_NAMES: &[&str] = &[ + "AppendIterator", + "ArrayAccess", + "ArrayIterator", + "ArrayObject", + "BadFunctionCallException", + "BadMethodCallException", + "CachingIterator", + "CallbackFilterIterator", + "Countable", + "DomainException", + "DirectoryIterator", + "EmptyIterator", + "Error", + "Exception", + "FilterIterator", + "FilesystemIterator", + "GlobIterator", + "InfiniteIterator", + "InvalidArgumentException", + "Iterator", + "IteratorAggregate", + "IteratorIterator", + "JsonSerializable", + "LengthException", + "LimitIterator", + "LogicException", + "MultipleIterator", + "NoRewindIterator", + "OuterIterator", + "OutOfBoundsException", + "OutOfRangeException", + "OverflowException", + "ParentIterator", + "RangeException", + "RecursiveArrayIterator", + "RecursiveCachingIterator", + "RecursiveCallbackFilterIterator", + "RecursiveDirectoryIterator", + "RecursiveFilterIterator", + "RecursiveIterator", + "RecursiveIteratorIterator", + "RecursiveRegexIterator", + "RegexIterator", + "RuntimeException", + "SeekableIterator", + "SplDoublyLinkedList", + "SplFixedArray", + "SplFileInfo", + "SplFileObject", + "SplObserver", + "SplQueue", + "SplStack", + "SplSubject", + "SplTempFileObject", + "Stringable", + "Throwable", + "Traversable", + "TypeError", + "UnderflowException", + "UnexpectedValueException", + "ValueError", +]; + +/// Full English month names used by eval `date()`. +pub(super) const EVAL_MONTH_NAMES: &[&str; 12] = &[ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +/// Short English month names used by eval `date()`. +pub(super) const EVAL_MONTH_SHORT_NAMES: &[&str; 12] = &[ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + +/// Full English weekday names used by eval `date()`. +pub(super) const EVAL_WEEKDAY_NAMES: &[&str; 7] = &[ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", +]; + +/// Short English weekday names used by eval `date()`. +pub(super) const EVAL_WEEKDAY_SHORT_NAMES: &[&str; 7] = + &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +/// Root package manifest used to mirror native `phpversion()` in the eval crate. +pub(super) const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../../Cargo.toml"); + +pub(super) const DEFINE_ALREADY_DEFINED_WARNING: &str = + "Warning: define(): Constant already defined\n"; +pub(super) const HEX2BIN_ODD_LENGTH_WARNING: &str = + "Warning: hex2bin(): Hexadecimal input string must have an even length\n"; +pub(super) const HEX2BIN_INVALID_WARNING: &str = + "Warning: hex2bin(): Input string must be hexadecimal string\n"; +pub(super) const EVAL_PATHINFO_DIRNAME: i64 = 1; +pub(super) const EVAL_PATHINFO_BASENAME: i64 = 2; +pub(super) const EVAL_PATHINFO_EXTENSION: i64 = 4; +pub(super) const EVAL_PATHINFO_FILENAME: i64 = 8; +pub(super) const EVAL_PATHINFO_ALL: i64 = 15; +pub(super) const EVAL_FNM_NOESCAPE: i64 = 1; +pub(super) const EVAL_FNM_PATHNAME: i64 = 2; +pub(super) const EVAL_FNM_PERIOD: i64 = 4; +pub(super) const EVAL_FNM_CASEFOLD: i64 = 16; +pub(super) const EVAL_ARRAY_FILTER_USE_VALUE: i64 = 0; +pub(super) const EVAL_ARRAY_FILTER_USE_BOTH: i64 = 1; +pub(super) const EVAL_ARRAY_FILTER_USE_KEY: i64 = 2; +pub(super) const EVAL_COUNT_NORMAL: i64 = 0; +pub(super) const EVAL_COUNT_RECURSIVE: i64 = 1; +pub(super) const EVAL_PREG_SPLIT_NO_EMPTY: i64 = 1; +pub(super) const EVAL_PREG_SPLIT_DELIM_CAPTURE: i64 = 2; +pub(super) const EVAL_PREG_SPLIT_OFFSET_CAPTURE: i64 = 4; +pub(super) const EVAL_PREG_PATTERN_ORDER: i64 = 1; +pub(super) const EVAL_PREG_SET_ORDER: i64 = 2; +pub(super) const EVAL_PREG_OFFSET_CAPTURE: i64 = 256; +pub(super) const EVAL_PREG_UNMATCHED_AS_NULL: i64 = 512; +pub(super) const EVAL_JSON_ERROR_NONE: i64 = 0; +pub(super) const EVAL_JSON_ERROR_DEPTH: i64 = 1; +pub(super) const EVAL_JSON_ERROR_STATE_MISMATCH: i64 = 2; +pub(super) const EVAL_JSON_ERROR_CTRL_CHAR: i64 = 3; +pub(super) const EVAL_JSON_ERROR_SYNTAX: i64 = 4; +pub(super) const EVAL_JSON_ERROR_UTF8: i64 = 5; +pub(super) const EVAL_JSON_ERROR_RECURSION: i64 = 6; +pub(super) const EVAL_JSON_ERROR_INF_OR_NAN: i64 = 7; +pub(super) const EVAL_JSON_ERROR_UNSUPPORTED_TYPE: i64 = 8; +pub(super) const EVAL_JSON_ERROR_INVALID_PROPERTY_NAME: i64 = 9; +pub(super) const EVAL_JSON_ERROR_UTF16: i64 = 10; +pub(super) const EVAL_JSON_HEX_TAG: i64 = 1; +pub(super) const EVAL_JSON_HEX_AMP: i64 = 2; +pub(super) const EVAL_JSON_HEX_APOS: i64 = 4; +pub(super) const EVAL_JSON_HEX_QUOT: i64 = 8; +pub(super) const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; +pub(super) const EVAL_JSON_FORCE_OBJECT: i64 = 16; +pub(super) const EVAL_JSON_NUMERIC_CHECK: i64 = 32; +pub(super) const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; +pub(super) const EVAL_JSON_PRETTY_PRINT: i64 = 128; +pub(super) const EVAL_JSON_UNESCAPED_UNICODE: i64 = 256; +pub(super) const EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR: i64 = 512; +pub(super) const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; +pub(super) const EVAL_JSON_INVALID_UTF8_IGNORE: i64 = 1_048_576; +pub(super) const EVAL_JSON_INVALID_UTF8_SUBSTITUTE: i64 = 2_097_152; +pub(super) const EVAL_JSON_THROW_ON_ERROR: i64 = 4_194_304; +pub(super) const EVAL_JSON_INF_OR_NAN_MESSAGE: &str = "Inf and NaN cannot be JSON encoded"; +pub(super) const EVAL_JSON_UTF8_MESSAGE: &str = + "Malformed UTF-8 characters, possibly incorrectly encoded"; diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-eval/src/interpreter/control.rs new file mode 100644 index 0000000000..5bed445ac6 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/control.rs @@ -0,0 +1,70 @@ +//! Purpose: +//! Holds small interpreter-local control and call-shape types shared across eval execution modules. +//! These types describe control-flow escape values, evaluated call arguments, and parsed builtin state. +//! +//! Called from: +//! - `crate::interpreter` execution, builtin, and call-dispatch helpers. +//! +//! Key details: +//! - Runtime cells are opaque handles; these types do not own or release values by themselves. + +use crate::value::RuntimeCellHandle; + +/// Internal statement-control result used to propagate eval returns and loops. +pub(super) enum EvalControl { + None, + Return(RuntimeCellHandle), + Throw(RuntimeCellHandle), + Break, + Continue, +} + +/// Final result of executing a parsed eval program. +pub enum EvalOutcome { + Value(RuntimeCellHandle), + Throwable(RuntimeCellHandle), +} + +/// One already evaluated function-like call argument. +#[derive(Clone)] +pub(super) struct EvaluatedCallArg { + pub(super) name: Option, + pub(super) value: RuntimeCellHandle, +} + +/// One already evaluated PHP callback supported by the eval dispatcher. +pub(super) enum EvaluatedCallable { + Named(String), + ObjectMethod { + object: RuntimeCellHandle, + method: String, + }, +} + +/// Bound argument tuple for direct `array_splice()` calls. +pub(super) type EvalArraySpliceDirectArgs = ( + String, + RuntimeCellHandle, + Option, + Option, +); + +/// Parsed flags for one eval `sprintf()` conversion specifier. +#[derive(Clone, Copy)] +pub(super) struct EvalSprintfSpec { + pub(super) left_align: bool, + pub(super) force_sign: bool, + pub(super) space_sign: bool, + pub(super) zero_pad: bool, + pub(super) alternate: bool, + pub(super) width: Option, + pub(super) precision: Option, + pub(super) specifier: u8, +} + +/// Eval-visible predefined constant payloads that are not stored in the dynamic context. +pub(super) enum EvalPredefinedConstant { + Int(i64), + Float(f64), + String(&'static str), +} diff --git a/crates/elephc-eval/src/interpreter/core_builtins.rs b/crates/elephc-eval/src/interpreter/core_builtins.rs new file mode 100644 index 0000000000..e357fb785f --- /dev/null +++ b/crates/elephc-eval/src/interpreter/core_builtins.rs @@ -0,0 +1,117 @@ +//! Purpose: +//! Implements small core builtins that are tightly coupled to interpreter expression helpers. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! +//! Key details: +//! - These helpers are kept out of large domain builtin files because they are short and rely on core eval traversal. + +use super::*; + +/// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. +pub(in crate::interpreter) fn eval_builtin_strlen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let bytes = values.string_bytes(value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) +} + +/// Evaluates the builtin `ord(...)` for the first byte of one coerced string. +pub(in crate::interpreter) fn eval_builtin_ord( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ord_result(value, values) +} + +/// Returns the first byte of one converted string, or zero for an empty string. +pub(in crate::interpreter) fn eval_ord_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(bytes.first().copied().unwrap_or(0))) +} + +/// Evaluates the builtin `count(...)` for one runtime array-like argument. +pub(in crate::interpreter) fn eval_builtin_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_count_result(value, None, values) + } + [value, mode] => { + let value = eval_expr(value, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_count_result(value, Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Counts an eval array with PHP normal or recursive mode semantics. +pub(in crate::interpreter) fn eval_count_result( + value: RuntimeCellHandle, + mode: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => eval_int_value(mode, values)?, + None => EVAL_COUNT_NORMAL, + }; + let len = match mode { + EVAL_COUNT_NORMAL => values.array_len(value)?, + EVAL_COUNT_RECURSIVE => eval_count_recursive_len(value, values, &mut Vec::new())?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) +} + +/// Recursively counts nested eval arrays for `count($value, COUNT_RECURSIVE)`. +pub(in crate::interpreter) fn eval_count_recursive_len( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + arrays_seen: &mut Vec, +) -> Result { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + return Ok(0); + } + arrays_seen.push(address); + + let len = values.array_len(value)?; + let mut total = len; + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + if values.is_array_like(element)? { + total = total + .checked_add(eval_count_recursive_len(element, values, arrays_seen)?) + .ok_or(EvalStatus::RuntimeFatal)?; + } + } + + arrays_seen.pop(); + Ok(total) +} diff --git a/crates/elephc-eval/src/interpreter/debug_output.rs b/crates/elephc-eval/src/interpreter/debug_output.rs new file mode 100644 index 0000000000..ca7c1a5f46 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/debug_output.rs @@ -0,0 +1,211 @@ +//! Purpose: +//! Implements eval-side debug output builtins such as `print_r()` and `var_dump()`. +//! +//! Called from: +//! - `crate::interpreter::eval_positional_expr_call()` for debug-output builtin dispatch. +//! +//! Key details: +//! - Output formatting walks runtime arrays and scalar values only through `RuntimeValueOps`. +//! - The builtins either echo output directly or return captured string output according to PHP flags. + +use super::*; + +/// Evaluates PHP `print_r()` over one eval expression. +pub(super) fn eval_builtin_print_r( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_print_r_result(value, values) +} + +/// Emits one eval value using elephc's supported `print_r()` output shape. +pub(in crate::interpreter) fn eval_print_r_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if matches!(values.type_tag(value)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + let output = values.string_bytes_value(b"Array\n")?; + values.echo(output)?; + } else { + values.echo(value)?; + } + values.bool_value(true) +} + +/// Evaluates PHP `var_dump()` over one eval expression and returns null. +pub(super) fn eval_builtin_var_dump( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_var_dump_result(value, values) +} + +/// Emits one eval value using PHP-style `var_dump()` debug formatting. +pub(in crate::interpreter) fn eval_var_dump_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut output = Vec::new(); + let mut arrays_seen = Vec::new(); + eval_var_dump_append_value(value, values, 0, &mut arrays_seen, &mut output)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.null() +} + +/// Appends one value and its nested array entries to a `var_dump()` byte buffer. +fn eval_var_dump_append_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_INT => eval_var_dump_append_scalar(b"int", value, values, depth, output), + EVAL_TAG_STRING => eval_var_dump_append_string(value, values, depth, output), + EVAL_TAG_FLOAT => eval_var_dump_append_scalar(b"float", value, values, depth, output), + EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, output), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { + eval_var_dump_append_array(value, values, depth, arrays_seen, output) + } + EVAL_TAG_OBJECT => { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"object(Object)\n"); + Ok(()) + } + EVAL_TAG_NULL => { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"NULL\n"); + Ok(()) + } + EVAL_TAG_RESOURCE => { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"resource(0) of type (stream)\n"); + Ok(()) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends one integer-like or float-like `var_dump()` scalar line. +fn eval_var_dump_append_scalar( + label: &[u8], + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(label); + output.extend_from_slice(b"("); + output.extend_from_slice(&values.string_bytes(value)?); + output.extend_from_slice(b")\n"); + Ok(()) +} + +/// Appends one string `var_dump()` line while preserving raw PHP string bytes. +fn eval_var_dump_append_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let bytes = values.string_bytes(value)?; + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"string("); + output.extend_from_slice(bytes.len().to_string().as_bytes()); + output.extend_from_slice(b") \""); + output.extend_from_slice(&bytes); + output.extend_from_slice(b"\"\n"); + Ok(()) +} + +/// Appends one boolean `var_dump()` line from PHP truthiness. +fn eval_var_dump_append_bool( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + if values.truthy(value)? { + output.extend_from_slice(b"bool(true)\n"); + } else { + output.extend_from_slice(b"bool(false)\n"); + } + Ok(()) +} + +/// Appends one array shell and recursively emits foreach-visible entries. +fn eval_var_dump_append_array( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"*RECURSION*\n"); + return Ok(()); + } + + arrays_seen.push(address); + let len = values.array_len(value)?; + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"array("); + output.extend_from_slice(len.to_string().as_bytes()); + output.extend_from_slice(b") {\n"); + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + eval_var_dump_append_key(key, values, depth + 1, output)?; + eval_var_dump_append_value(element, values, depth + 1, arrays_seen, output)?; + } + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"}\n"); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one array key line for an indexed or associative `var_dump()` entry. +fn eval_var_dump_append_key( + key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"["); + match values.type_tag(key)? { + EVAL_TAG_STRING => { + output.extend_from_slice(b"\""); + output.extend_from_slice(&values.string_bytes(key)?); + output.extend_from_slice(b"\""); + } + _ => output.extend_from_slice(&values.string_bytes(key)?), + } + output.extend_from_slice(b"]=>\n"); + Ok(()) +} + +/// Appends the two-space indentation used by PHP `var_dump()` arrays. +fn eval_var_dump_append_indent(depth: usize, output: &mut Vec) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs new file mode 100644 index 0000000000..927a331d32 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -0,0 +1,330 @@ +//! Purpose: +//! Evaluates user-declared and native dynamic functions, including named/spread argument binding. +//! +//! Called from: +//! - `crate::interpreter::eval_call()` and dynamic callable dispatch helpers. +//! +//! Key details: +//! - PHP source evaluation order is preserved before argument binding. +//! - Static locals are persisted through `ElephcEvalContext` after function execution. + +use super::*; + +/// Evaluates an eval-declared user function with PHP-style argument binding. +pub(in crate::interpreter) fn eval_dynamic_function( + function: &EvalFunction, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = + eval_function_call_args(function.params(), args, context, caller_scope, values)?; + eval_dynamic_function_with_values(function, evaluated_args, context, values) +} + +/// Evaluates and binds function-like arguments to parameter order. +pub(in crate::interpreter) fn eval_function_call_args( + params: &[String], + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let evaluated_args = eval_call_arg_values(args, context, caller_scope, values)?; + bind_evaluated_function_args(params, evaluated_args) +} + +/// Evaluates source-order call arguments while preserving named-argument metadata. +pub(in crate::interpreter) fn eval_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut evaluated_args = Vec::with_capacity(args.len()); + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let spread = eval_expr(arg.value(), context, caller_scope, values)?; + if !values.is_array_like(spread)? { + return Err(EvalStatus::RuntimeFatal); + } + append_unpacked_call_arg_values(spread, &mut evaluated_args, &mut saw_named, values)?; + continue; + } + + if let Some(name) = arg.name() { + saw_named = true; + let value = eval_expr(arg.value(), context, caller_scope, values)?; + evaluated_args.push(EvaluatedCallArg { + name: Some(name.to_string()), + value, + }); + continue; + } + + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let value = eval_expr(arg.value(), context, caller_scope, values)?; + evaluated_args.push(EvaluatedCallArg { name: None, value }); + } + + Ok(evaluated_args) +} + +/// Converts a `call_user_func_array` argument array into ordered call arguments. +pub(in crate::interpreter) fn eval_array_call_arg_values( + arg_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(arg_array)?; + let mut evaluated_args = Vec::with_capacity(len); + let mut saw_named = false; + append_unpacked_call_arg_values(arg_array, &mut evaluated_args, &mut saw_named, values)?; + Ok(evaluated_args) +} + +/// Appends one unpacked array's values using PHP named-argument key semantics. +pub(in crate::interpreter) fn append_unpacked_call_arg_values( + array: RuntimeCellHandle, + evaluated_args: &mut Vec, + saw_named: &mut bool, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + match values.type_tag(key)? { + EVAL_TAG_INT => { + if *saw_named { + return Err(EvalStatus::RuntimeFatal); + } + evaluated_args.push(EvaluatedCallArg { name: None, value }); + } + EVAL_TAG_STRING => { + *saw_named = true; + let name = values.string_bytes(key)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + evaluated_args.push(EvaluatedCallArg { + name: Some(name), + value, + }); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(()) +} + +/// Binds evaluated positional and named values to declared parameter order. +pub(in crate::interpreter) fn bind_evaluated_function_args( + params: &[String], + evaluated_args: Vec, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_dynamic_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Binds one positional dynamic-call value to the next declared parameter slot. +pub(in crate::interpreter) fn bind_dynamic_positional_arg( + bound_args: &mut [Option], + next_positional: &mut usize, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + if *next_positional >= bound_args.len() || bound_args[*next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[*next_positional] = Some(value); + *next_positional += 1; + Ok(()) +} + +/// Binds one named dynamic-call value to the matching declared parameter slot. +pub(in crate::interpreter) fn bind_dynamic_named_arg( + params: &[String], + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + let Some(param_index) = params.iter().position(|param| param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + Ok(()) +} + +/// Evaluates an eval-declared function after its positional arguments are prepared. +pub(super) fn eval_dynamic_function_with_values( + function: &EvalFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut function_scope = ElephcEvalScope::new(); + for (name, value) in function.params().iter().zip(evaluated_args) { + function_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); + } + let static_names = static_var_names(function.body()); + context.push_function(function.name()); + let result = execute_statements(function.body(), context, &mut function_scope, values); + let persist_result = persist_static_locals( + context, + function.name(), + &static_names, + &function_scope, + values, + ); + context.pop_function(); + persist_result?; + match result? { + EvalControl::None => values.null(), + EvalControl::Return(result) => Ok(result), + EvalControl::Throw(result) => { + context.set_pending_throw(result); + Err(EvalStatus::UncaughtThrowable) + } + EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Persists static local variables from one eval-declared function activation. +pub(super) fn persist_static_locals( + context: &mut ElephcEvalContext, + function_name: &str, + names: &[String], + scope: &ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for name in names { + if let Some(cell) = scope.visible_cell(name) { + if let Some(replaced) = + context.set_static_local(function_name.to_string(), name.clone(), cell) + { + values.release(replaced)?; + } + } + } + Ok(()) +} + +/// Returns the distinct static local names declared anywhere in an eval function body. +pub(in crate::interpreter) fn static_var_names(body: &[EvalStmt]) -> Vec { + let mut names = std::collections::HashSet::new(); + collect_static_var_names(body, &mut names); + names.into_iter().collect() +} + +/// Recursively collects static local declaration names from eval statements. +fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::HashSet) { + for stmt in body { + match stmt { + EvalStmt::StaticVar { name, .. } => { + names.insert(name.clone()); + } + EvalStmt::DoWhile { body, .. } + | EvalStmt::Foreach { body, .. } + | EvalStmt::For { body, .. } + | EvalStmt::While { body, .. } => collect_static_var_names(body, names), + EvalStmt::FunctionDecl { .. } => {} + EvalStmt::If { + then_branch, + else_branch, + .. + } => { + collect_static_var_names(then_branch, names); + collect_static_var_names(else_branch, names); + } + EvalStmt::Switch { cases, .. } => { + for case in cases { + collect_static_var_names(&case.body, names); + } + } + EvalStmt::Try { + body, + catches, + finally_body, + } => { + collect_static_var_names(body, names); + for catch in catches { + collect_static_var_names(&catch.body, names); + } + collect_static_var_names(finally_body, names); + } + EvalStmt::ArrayAppendVar { .. } + | EvalStmt::ArraySetVar { .. } + | EvalStmt::Break + | EvalStmt::ClassDecl(_) + | EvalStmt::Continue + | EvalStmt::Echo(_) + | EvalStmt::Expr(_) + | EvalStmt::Global { .. } + | EvalStmt::PropertySet { .. } + | EvalStmt::ReferenceAssign { .. } + | EvalStmt::Return(_) + | EvalStmt::StoreVar { .. } + | EvalStmt::Throw(_) + | EvalStmt::UnsetVar { .. } => {} + } + } +} + +/// Evaluates a registered AOT function through its descriptor-compatible invoker. +pub(super) fn eval_native_function( + function: NativeFunction, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = if function.param_names().len() == function.param_count() { + eval_function_call_args(function.param_names(), args, context, caller_scope, values)? + } else { + eval_positional_call_arg_values(args, context, caller_scope, values)? + }; + eval_native_function_with_values(function, evaluated_args, values) +} + +/// Invokes a registered AOT function after its positional arguments are prepared. +pub(super) fn eval_native_function_with_values( + function: NativeFunction, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.len() != function.param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let arg_array = values.array_new(evaluated_args.len())?; + for (index, value) in evaluated_args.into_iter().enumerate() { + let index = values.int(index as i64)?; + let _ = values.array_set(arg_array, index, value)?; + } + let result = unsafe { function.call(arg_array) }; + values.release(arg_array)?; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs new file mode 100644 index 0000000000..401d5b5299 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -0,0 +1,588 @@ +//! Purpose: +//! Evaluates EvalIR expressions, match expressions, function-like calls, and positional builtin dispatch. +//! +//! Called from: +//! - `crate::interpreter::statements` for expression statements and expression-bearing statements. +//! - Eval builtin modules when they need to evaluate unevaluated argument expressions. +//! +//! Key details: +//! - PHP call argument evaluation order is preserved before binding or ABI-like materialization. +//! - Language constructs such as `eval`, `isset`, and `empty` receive unevaluated expressions. + +use super::*; + +/// Evaluates one expression to an opaque runtime-cell handle. +pub(in crate::interpreter) fn eval_expr( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match expr { + EvalExpr::Array(elements) => { + if elements + .iter() + .any(|element| matches!(element, EvalArrayElement::KeyValue { .. })) + { + eval_assoc_array(elements, context, scope, values) + } else { + eval_indexed_array(elements, context, scope, values) + } + } + EvalExpr::ArrayGet { array, index } => { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + values.array_get(array, index) + } + EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), + EvalExpr::Const(value) => eval_const(value, values), + EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), + EvalExpr::DynamicCall { callee, args } => { + eval_dynamic_call(callee, args, context, scope, values) + } + EvalExpr::Include { + path, + required, + once, + } => eval_include_expr(path, *required, *once, context, scope, values), + EvalExpr::LoadVar(name) => { + visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) + } + EvalExpr::Magic(magic) => eval_magic_const(magic, context, values), + EvalExpr::Match { + subject, + arms, + default, + } => eval_match_expr(subject, arms, default.as_deref(), context, scope, values), + EvalExpr::NamespacedCall { + name, + fallback_name, + args, + } => eval_namespaced_call(name, fallback_name, args, context, scope, values), + EvalExpr::NamespacedConstFetch { + name, + fallback_name, + } => eval_namespaced_const_fetch(name, fallback_name, context, values), + EvalExpr::NewObject { class_name, args } => { + let args = eval_method_call_arg_values(args, context, scope, values)?; + if let Some(class) = context.class(class_name).cloned() { + eval_dynamic_class_new_object(&class, args, context, scope, values) + } else { + values + .new_object(class_name) + .and_then(|object| values.construct_object(object, args).map(|()| object)) + } + } + EvalExpr::MethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_method_call_result(object, method, evaluated_args, context, values) + } + EvalExpr::NullCoalesce { value, default } => { + let value = eval_expr(value, context, scope, values)?; + if values.is_null(value)? { + eval_expr(default, context, scope, values) + } else { + Ok(value) + } + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + values.property_get(object, property) + } + EvalExpr::Print(inner) => { + let value = eval_expr(inner, context, scope, values)?; + values.echo(value)?; + values.int(1) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + let condition = eval_expr(condition, context, scope, values)?; + if values.truthy(condition)? { + if let Some(then_branch) = then_branch { + eval_expr(then_branch, context, scope, values) + } else { + Ok(condition) + } + } else { + eval_expr(else_branch, context, scope, values) + } + } + EvalExpr::Unary { op, expr } => { + let value = eval_expr(expr, context, scope, values)?; + match op { + EvalUnaryOp::Plus => { + let zero = values.int(0)?; + values.add(zero, value) + } + EvalUnaryOp::Negate => { + let zero = values.int(0)?; + values.sub(zero, value) + } + EvalUnaryOp::LogicalNot => { + let truthy = values.truthy(value)?; + values.bool_value(!truthy) + } + EvalUnaryOp::BitNot => values.bit_not(value), + } + } + EvalExpr::Binary { op, left, right } => { + if *op == EvalBinOp::LogicalAnd { + let left = eval_expr(left, context, scope, values)?; + if !values.truthy(left)? { + return values.bool_value(false); + } + let right = eval_expr(right, context, scope, values)?; + let truthy = values.truthy(right)?; + return values.bool_value(truthy); + } + if *op == EvalBinOp::LogicalOr { + let left = eval_expr(left, context, scope, values)?; + if values.truthy(left)? { + return values.bool_value(true); + } + let right = eval_expr(right, context, scope, values)?; + let truthy = values.truthy(right)?; + return values.bool_value(truthy); + } + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + match op { + EvalBinOp::Add => values.add(left, right), + EvalBinOp::Sub => values.sub(left, right), + EvalBinOp::Mul => values.mul(left, right), + EvalBinOp::Div => values.div(left, right), + EvalBinOp::Mod => values.modulo(left, right), + EvalBinOp::Pow => values.pow(left, right), + EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight => values.bitwise(*op, left, right), + EvalBinOp::Concat => values.concat(left, right), + EvalBinOp::LogicalXor => { + let left_truthy = values.truthy(left)?; + let right_truthy = values.truthy(right)?; + values.bool_value(left_truthy ^ right_truthy) + } + EvalBinOp::LooseEq + | EvalBinOp::LooseNotEq + | EvalBinOp::StrictEq + | EvalBinOp::StrictNotEq + | EvalBinOp::Lt + | EvalBinOp::LtEq + | EvalBinOp::Gt + | EvalBinOp::GtEq => values.compare(*op, left, right), + EvalBinOp::Spaceship => values.spaceship(left, right), + EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => { + Err(EvalStatus::UnsupportedConstruct) + } + } + } + } +} + +/// Evaluates a PHP `match` expression with strict comparison and lazy arm values. +pub(in crate::interpreter) fn eval_match_expr( + subject: &EvalExpr, + arms: &[EvalMatchArm], + default: Option<&EvalExpr>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let subject = eval_expr(subject, context, scope, values)?; + for arm in arms { + for pattern in &arm.patterns { + let pattern = eval_expr(pattern, context, scope, values)?; + let matched = values.compare(EvalBinOp::StrictEq, subject, pattern)?; + if values.truthy(matched)? { + return eval_expr(&arm.value, context, scope, values); + } + } + } + default + .map(|expr| eval_expr(expr, context, scope, values)) + .unwrap_or(Err(EvalStatus::RuntimeFatal)) +} + +/// Returns cloned positional argument expressions, rejecting named arguments. +pub(in crate::interpreter) fn positional_call_arg_exprs( + args: &[EvalCallArg], +) -> Result, EvalStatus> { + if args + .iter() + .any(|arg| arg.name().is_some() || arg.is_spread()) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(args.iter().map(|arg| arg.value().clone()).collect()) +} + +/// Evaluates a positional-only call argument list in source order. +pub(in crate::interpreter) fn eval_positional_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if args + .iter() + .any(|arg| arg.name().is_some() || arg.is_spread()) + { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg.value(), context, scope, values)?); + } + Ok(evaluated_args) +} + +/// Evaluates method-call arguments, allowing numeric spread but not named args. +pub(in crate::interpreter) fn eval_method_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + if evaluated_args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()) +} + +/// Evaluates supported function-like calls from a runtime eval fragment. +pub(in crate::interpreter) fn eval_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_expr_language_construct_name(name) { + let args = positional_call_arg_exprs(args)?; + return eval_positional_expr_call(name, &args, context, scope, values); + } + if matches!( + name, + "array_pop" + | "array_push" + | "array_shift" + | "array_splice" + | "array_unshift" + | "arsort" + | "asort" + | "krsort" + | "ksort" + | "natcasesort" + | "natsort" + | "rsort" + | "shuffle" + | "sort" + | "settype" + | "uasort" + | "uksort" + | "usort" + ) { + return eval_builtin_array_pop_shift_call(name, args, context, scope, values); + } + if eval_php_visible_builtin_exists(name) { + if eval_call_args_are_plain_positional(args) { + let args = positional_call_arg_exprs(args)?; + return eval_positional_expr_call(name, &args, context, scope, values); + } + return eval_builtin_call(name, args, context, scope, values); + } + + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Evaluates an unqualified namespaced function call with PHP's global fallback. +pub(in crate::interpreter) fn eval_namespaced_call( + name: &str, + fallback_name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + eval_call(fallback_name, args, context, scope, values) +} + +/// Evaluates a variable or expression callable and dispatches it with source-order arguments. +pub(in crate::interpreter) fn eval_dynamic_call( + callee: &EvalExpr, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_expr(callee, context, scope, values)?; + let callback = eval_callable(callback, values)?; + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) +} + +/// Returns true for language constructs that need unevaluated argument expressions. +pub(in crate::interpreter) fn eval_expr_language_construct_name(name: &str) -> bool { + matches!(name, "empty" | "eval" | "isset") +} + +/// Returns true when every source argument is plain positional. +pub(in crate::interpreter) fn eval_call_args_are_plain_positional(args: &[EvalCallArg]) -> bool { + args.iter() + .all(|arg| arg.name().is_none() && !arg.is_spread()) +} + +/// Evaluates builtins and language constructs after positional-only argument validation. +pub(in crate::interpreter) fn eval_positional_expr_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "abs" => eval_builtin_abs(args, context, scope, values), + "addslashes" | "stripslashes" => eval_builtin_slashes(name, args, context, scope, values), + "array_combine" => eval_builtin_array_combine(args, context, scope, values), + "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), + "array_column" => eval_builtin_array_column(args, context, scope, values), + "array_fill" => eval_builtin_array_fill(args, context, scope, values), + "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), + "array_filter" => eval_builtin_array_filter(args, context, scope, values), + "array_flip" => eval_builtin_array_flip(args, context, scope, values), + "array_map" => eval_builtin_array_map(args, context, scope, values), + "array_reduce" => eval_builtin_array_reduce(args, context, scope, values), + "array_walk" => eval_builtin_array_walk(args, context, scope, values), + "array_keys" | "array_values" => { + eval_builtin_array_projection(name, args, context, scope, values) + } + "array_key_exists" => eval_builtin_array_key_exists(args, context, scope, values), + "array_diff" | "array_intersect" => { + eval_builtin_array_value_set(name, args, context, scope, values) + } + "array_diff_key" | "array_intersect_key" => { + eval_builtin_array_key_set(name, args, context, scope, values) + } + "array_merge" => eval_builtin_array_merge(args, context, scope, values), + "array_product" | "array_sum" => { + eval_builtin_array_aggregate(name, args, context, scope, values) + } + "array_pad" => eval_builtin_array_pad(args, context, scope, values), + "array_rand" => eval_builtin_array_rand(args, context, scope, values), + "array_reverse" => eval_builtin_array_reverse(args, context, scope, values), + "array_search" | "in_array" => { + eval_builtin_array_search(name, args, context, scope, values) + } + "array_slice" => eval_builtin_array_slice(args, context, scope, values), + "array_unique" => eval_builtin_array_unique(args, context, scope, values), + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { + eval_builtin_float_unary(name, args, context, scope, values) + } + "atan2" | "hypot" => eval_builtin_float_pair(name, args, context, scope, values), + "base64_encode" => eval_builtin_base64_encode(args, context, scope, values), + "base64_decode" => eval_builtin_base64_decode(args, context, scope, values), + "basename" => eval_builtin_basename(args, context, scope, values), + "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), + "ceil" => eval_builtin_ceil(args, context, scope, values), + "chdir" | "mkdir" | "rmdir" => { + eval_builtin_unary_path_bool(name, args, context, scope, values) + } + "chmod" => eval_builtin_chmod(args, context, scope, values), + "chr" => eval_builtin_chr(args, context, scope, values), + "clamp" => eval_builtin_clamp(args, context, scope, values), + "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), + "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), + "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "class_exists" => eval_builtin_class_exists(args, context, scope, values), + "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), + "trait_exists" | "enum_exists" => { + eval_builtin_class_like_exists(name, args, context, scope, values) + } + "is_a" | "is_subclass_of" => eval_builtin_is_a_relation(name, args, context, scope, values), + "chop" => eval_builtin_trim_like(name, args, context, scope, values), + "boolval" | "floatval" | "intval" | "strval" => { + eval_builtin_cast(name, args, context, scope, values) + } + "count" => eval_builtin_count(args, context, scope, values), + "copy" | "link" | "rename" | "symlink" => { + eval_builtin_binary_path_bool(name, args, context, scope, values) + } + "crc32" => eval_builtin_crc32(args, context, scope, values), + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { + eval_builtin_ctype(name, args, context, scope, values) + } + "date" => eval_builtin_date(args, context, scope, values), + "define" => eval_builtin_define(args, context, scope, values), + "defined" => eval_builtin_defined(args, context, scope, values), + "dirname" => eval_builtin_dirname(args, context, scope, values), + "disk_free_space" | "disk_total_space" => { + eval_builtin_disk_space(name, args, context, scope, values) + } + "empty" => eval_builtin_empty(args, context, scope, values), + "eval" => eval_nested_eval(args, context, scope, values), + "explode" => eval_builtin_explode(args, context, scope, values), + "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), + "file" => eval_builtin_file(args, context, scope, values), + "file_exists" => eval_builtin_file_probe(name, args, context, scope, values), + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), + "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), + "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), + "filesize" => eval_builtin_filesize(args, context, scope, values), + "filetype" => eval_builtin_filetype(args, context, scope, values), + "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), + "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), + "floor" => eval_builtin_floor(args, context, scope, values), + "function_exists" | "is_callable" => { + eval_builtin_function_probe(args, context, scope, values) + } + "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), + "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), + "gethostname" => eval_builtin_gethostname(args, values), + "getprotobyname" => eval_builtin_getprotobyname(args, context, scope, values), + "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), + "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), + "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), + "get_class" => eval_builtin_get_class(args, context, scope, values), + "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), + "get_resource_id" | "get_resource_type" => { + eval_builtin_resource_introspection(name, args, context, scope, values) + } + "getcwd" => eval_builtin_getcwd(args, values), + "getenv" => eval_builtin_getenv(args, context, scope, values), + "gettype" => eval_builtin_gettype(args, context, scope, values), + "glob" => eval_builtin_glob(args, context, scope, values), + "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { + eval_builtin_hash_one_shot(name, args, context, scope, values) + } + "hash_algos" => eval_builtin_hash_algos(args, values), + "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), + "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { + eval_builtin_html_entity(name, args, context, scope, values) + } + "implode" => eval_builtin_implode(args, context, scope, values), + "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), + "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), + "intdiv" => eval_builtin_intdiv(args, context, scope, values), + "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), + "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), + "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), + "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" + | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), + "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" + | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" + | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { + eval_builtin_type_predicate(name, args, context, scope, values) + } + "ip2long" => eval_builtin_ip2long(args, context, scope, values), + "json_decode" => eval_builtin_json_decode(args, context, scope, values), + "json_encode" => eval_builtin_json_encode(args, context, scope, values), + "json_last_error" => eval_builtin_json_last_error(args, context, values), + "json_last_error_msg" => eval_builtin_json_last_error_msg(args, context, values), + "json_validate" => eval_builtin_json_validate(args, context, scope, values), + "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), + "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), + "log" => eval_builtin_log(args, context, scope, values), + "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), + "microtime" => eval_builtin_microtime(args, context, scope, values), + "mktime" => eval_builtin_mktime(args, context, scope, values), + "nl2br" => eval_builtin_nl2br(args, context, scope, values), + "number_format" => eval_builtin_number_format(args, context, scope, values), + "ord" => eval_builtin_ord(args, context, scope, values), + "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), + "pi" => eval_builtin_pi(args, values), + "php_uname" => eval_builtin_php_uname(args, context, scope, values), + "phpversion" => eval_builtin_phpversion(args, values), + "pow" => eval_builtin_pow(args, context, scope, values), + "preg_match" => eval_builtin_preg_match(args, context, scope, values), + "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), + "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), + "preg_replace_callback" => eval_builtin_preg_replace_callback(args, context, scope, values), + "preg_split" => eval_builtin_preg_split(args, context, scope, values), + "print_r" => eval_builtin_print_r(args, context, scope, values), + "putenv" => eval_builtin_putenv(args, context, scope, values), + "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), + "random_int" => eval_builtin_random_int(args, context, scope, values), + "range" => eval_builtin_range(args, context, scope, values), + "rawurldecode" | "urldecode" => eval_builtin_url_decode(name, args, context, scope, values), + "rawurlencode" | "urlencode" => eval_builtin_url_encode(name, args, context, scope, values), + "readfile" => eval_builtin_readfile(args, context, scope, values), + "readlink" => eval_builtin_readlink(args, context, scope, values), + "realpath" => eval_builtin_realpath(args, context, scope, values), + "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), + "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), + "round" => eval_builtin_round(args, context, scope, values), + "scandir" => eval_builtin_scandir(args, context, scope, values), + "isset" => eval_builtin_isset(args, context, scope, values), + "sleep" => eval_builtin_sleep(args, context, scope, values), + "sqrt" => eval_builtin_sqrt(args, context, scope, values), + "spl_classes" => eval_builtin_spl_classes(args, values), + "spl_object_id" | "spl_object_hash" => { + eval_builtin_spl_object_identity(name, args, context, scope, values) + } + "sscanf" => eval_builtin_sscanf(args, context, scope, values), + "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), + "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), + "tempnam" => eval_builtin_tempnam(args, context, scope, values), + "time" => eval_builtin_time(args, values), + "touch" => eval_builtin_touch(args, context, scope, values), + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { + eval_builtin_stream_introspection(name, args, values) + } + "strtotime" => eval_builtin_strtotime(args, context, scope, values), + "unlink" => eval_builtin_unlink(args, context, scope, values), + "strrev" => eval_builtin_strrev(args, context, scope, values), + "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), + "str_replace" | "str_ireplace" => { + eval_builtin_str_replace(name, args, context, scope, values) + } + "str_pad" => eval_builtin_str_pad(args, context, scope, values), + "str_split" => eval_builtin_str_split(args, context, scope, values), + "strstr" => eval_builtin_strstr(args, context, scope, values), + "substr" => eval_builtin_substr(args, context, scope, values), + "substr_replace" => eval_builtin_substr_replace(args, context, scope, values), + "str_contains" | "str_starts_with" | "str_ends_with" => { + eval_builtin_string_search(name, args, context, scope, values) + } + "strcmp" | "strcasecmp" => eval_builtin_string_compare(name, args, context, scope, values), + "strlen" => eval_builtin_strlen(args, context, scope, values), + "strpos" | "strrpos" => eval_builtin_string_position(name, args, context, scope, values), + "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { + eval_builtin_string_case(name, args, context, scope, values) + } + "long2ip" => eval_builtin_long2ip(args, context, scope, values), + "trim" => eval_builtin_trim_like(name, args, context, scope, values), + "ucwords" => eval_builtin_ucwords(args, context, scope, values), + "umask" => eval_builtin_umask(args, context, scope, values), + "usleep" => eval_builtin_usleep(args, context, scope, values), + "var_dump" => eval_builtin_var_dump(args, context, scope, values), + "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), + "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-eval/src/interpreter/include_exec.rs b/crates/elephc-eval/src/interpreter/include_exec.rs new file mode 100644 index 0000000000..5730d81c6f --- /dev/null +++ b/crates/elephc-eval/src/interpreter/include_exec.rs @@ -0,0 +1,198 @@ +//! Purpose: +//! Executes nested `eval(...)`, include, include_once, require, and require_once expressions. +//! This keeps source-file loading and PHP open/close-tag handling outside the core interpreter loop. +//! +//! Called from: +//! - `crate::interpreter::eval_positional_expr_call()` for `eval(...)`. +//! - `crate::interpreter::eval_expr()` for include/require expression nodes. +//! +//! Key details: +//! - Included code runs against the current eval context and materialized scope. +//! - Missing include emits a warning and returns false; missing require is fatal. + +use super::*; + +/// Evaluates nested `eval(...)` calls against the current materialized scope. +pub(super) fn eval_nested_eval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [code] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let code = eval_expr(code, context, scope, values)?; + let code = values.string_bytes(code)?; + let program = parse_fragment(&code).map_err(EvalParseError::status)?; + execute_program_with_context(context, &program, scope, values) +} + +/// Evaluates an eval-fragment include or require expression. +pub(super) fn eval_include_expr( + path: &EvalExpr, + required: bool, + once: bool, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_expr(path, context, scope, values)?; + let path = eval_path_string(path, values)?; + let resolved_path = eval_resolve_include_path(&path, context); + let include_key = eval_include_key(&resolved_path); + if once && context.has_included_file(&include_key) { + return values.bool_value(true); + } + let bytes = match std::fs::read(&resolved_path) { + Ok(bytes) => bytes, + Err(_) => return eval_include_missing_file(&path, required, values), + }; + context.mark_included_file(include_key); + eval_execute_include_bytes(&bytes, &resolved_path, context, scope, values) +} + +/// Returns the include/require result for a file that cannot be opened. +fn eval_include_missing_file( + path: &str, + required: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let construct = if required { "require" } else { "include" }; + values.warning(&format!( + "Warning: {construct}({path}): Failed to open stream: No such file or directory\n" + ))?; + values.warning(&format!( + "Warning: {construct}(): Failed opening '{path}' for inclusion\n" + ))?; + if required { + Err(EvalStatus::RuntimeFatal) + } else { + values.bool_value(false) + } +} + +/// Resolves eval include paths using PHP's cwd-first and caller-directory fallback. +fn eval_resolve_include_path(path: &str, context: &ElephcEvalContext) -> std::path::PathBuf { + let raw_path = std::path::Path::new(path); + if raw_path.is_absolute() || raw_path.exists() { + return raw_path.to_path_buf(); + } + if context.call_dir().is_empty() { + return raw_path.to_path_buf(); + } + let caller_path = std::path::Path::new(context.call_dir()).join(raw_path); + if caller_path.exists() { + caller_path + } else { + raw_path.to_path_buf() + } +} + +/// Builds the stable include_once key for a resolved path. +fn eval_include_key(path: &std::path::Path) -> String { + std::fs::canonicalize(path) + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .into_owned() +} + +/// Executes a local include file, alternating raw output and PHP code blocks. +fn eval_execute_include_bytes( + bytes: &[u8], + path: &std::path::Path, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut cursor = 0; + while let Some((tag_start, code_start)) = eval_find_php_open_tag(bytes, cursor) { + eval_echo_include_bytes(&bytes[cursor..tag_start], values)?; + let close = eval_find_php_close_tag(bytes, code_start); + let code_end = close.unwrap_or(bytes.len()); + match eval_execute_include_code(&bytes[code_start..code_end], path, context, scope, values)? + { + EvalControl::None => {} + EvalControl::Return(value) => return Ok(value), + EvalControl::Throw(value) => { + context.set_pending_throw(value); + return Err(EvalStatus::UncaughtThrowable); + } + EvalControl::Break | EvalControl::Continue => { + return Err(EvalStatus::UnsupportedConstruct); + } + } + let Some(close) = close else { + return values.int(1); + }; + cursor = close + 2; + } + eval_echo_include_bytes(&bytes[cursor..], values)?; + values.int(1) +} + +/// Parses and executes one PHP code block from an included file. +fn eval_execute_include_code( + code: &[u8], + path: &std::path::Path, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let program = parse_fragment(code).map_err(EvalParseError::status)?; + let previous = context.call_site(); + let file = path.to_string_lossy().into_owned(); + let dir = path + .parent() + .map(|parent| parent.to_string_lossy().into_owned()) + .unwrap_or_default(); + context.set_call_site(file.clone(), dir, 1); + context.set_file_magic_override(Some(file)); + let result = execute_statements(program.statements(), context, scope, values); + context.set_call_site(previous.0, previous.1, previous.2); + context.set_file_magic_override(previous.3); + result +} + +/// Echoes raw non-PHP include bytes through the eval value hooks. +fn eval_echo_include_bytes( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if bytes.is_empty() { + return Ok(()); + } + let output = values.string_bytes_value(bytes)?; + values.echo(output) +} + +/// Finds the next ` Option<(usize, usize)> { + bytes + .get(start..)? + .windows(5) + .position(eval_is_php_open_tag) + .map(|offset| { + let tag_start = start + offset; + (tag_start, tag_start + 5) + }) +} + +/// Returns true when a five-byte window is a case-insensitive ` bool { + window.len() == 5 + && window[0] == b'<' + && window[1] == b'?' + && window[2].eq_ignore_ascii_case(&b'p') + && window[3].eq_ignore_ascii_case(&b'h') + && window[4].eq_ignore_ascii_case(&b'p') +} + +/// Finds the next PHP closing tag after a code block start. +fn eval_find_php_close_tag(bytes: &[u8], start: usize) -> Option { + bytes + .get(start..)? + .windows(2) + .position(|window| window == b"?>") + .map(|offset| start + offset) +} diff --git a/crates/elephc-eval/src/interpreter/json.rs b/crates/elephc-eval/src/interpreter/json.rs new file mode 100644 index 0000000000..cc0721d606 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/json.rs @@ -0,0 +1,922 @@ +//! Purpose: +//! Implements eval-side JSON builtins and JSON encode/decode helper routines. +//! +//! Called from: +//! - `crate::interpreter::eval_positional_expr_call()` for JSON builtin dispatch. +//! +//! Key details: +//! - JSON parser errors are recorded on `ElephcEvalContext` to mirror PHP `json_last_error()`. +//! - Runtime values are traversed through `RuntimeValueOps`; this module never inspects cells directly. + +use super::*; + +/// Evaluates PHP `json_encode()` for zero-flag scalar and array values. +pub(super) fn eval_builtin_json_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_json_encode_result(value, None, None, context, values) + } + [value, flags] => { + let value = eval_expr(value, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_encode_result(value, Some(flags), None, context, values) + } + [value, flags, depth] => { + let value = eval_expr(value, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_encode_result(value, Some(flags), Some(depth), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Encodes one runtime cell as a JSON string for eval's supported flag subset. +pub(super) fn eval_json_encode_result( + value: RuntimeCellHandle, + flags: Option, + depth: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + let supported_flags = EVAL_JSON_HEX_TAG + | EVAL_JSON_HEX_AMP + | EVAL_JSON_HEX_APOS + | EVAL_JSON_HEX_QUOT + | EVAL_JSON_UNESCAPED_SLASHES + | EVAL_JSON_UNESCAPED_UNICODE + | EVAL_JSON_FORCE_OBJECT + | EVAL_JSON_PRETTY_PRINT + | EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR + | EVAL_JSON_PRESERVE_ZERO_FRACTION + | EVAL_JSON_INVALID_UTF8_IGNORE + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE + | EVAL_JSON_THROW_ON_ERROR; + let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; + if flags & !supported_flags != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let mut output = Vec::new(); + let mut error = None; + eval_json_encode_append( + value, + values, + flags, + depth as usize, + 0, + &mut Vec::new(), + &mut error, + &mut output, + )?; + if let Some(error) = error { + context.set_json_error(error.code, error.message); + if flags & EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR == 0 { + if flags & EVAL_JSON_THROW_ON_ERROR != 0 { + return eval_throw_json_exception(error.code, error.message, context, values); + } + return values.bool_value(false); + } + } else { + context.clear_json_error(); + } + values.string_bytes_value(&output) +} + +/// Evaluates PHP `json_decode()` for eval-supported JSON text and flags. +pub(super) fn eval_builtin_json_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [json] => { + let json = eval_expr(json, context, scope, values)?; + eval_json_decode_result(json, None, None, None, context, values) + } + [json, associative] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + eval_json_decode_result(json, Some(associative), None, None, context, values) + } + [json, associative, depth] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_decode_result(json, Some(associative), Some(depth), None, context, values) + } + [json, associative, depth, flags] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_decode_result( + json, + Some(associative), + Some(depth), + Some(flags), + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Decodes one JSON string into eval runtime cells and records PHP JSON parse state. +pub(super) fn eval_json_decode_result( + json: RuntimeCellHandle, + associative: Option, + depth: Option, + flags: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + let supported_flags = EVAL_JSON_BIGINT_AS_STRING + | EVAL_JSON_INVALID_UTF8_IGNORE + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE + | EVAL_JSON_THROW_ON_ERROR; + if flags & !supported_flags != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let objects_as_assoc = associative + .map(|associative| values.truthy(associative)) + .transpose()? + .unwrap_or(false); + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let bytes = values.string_bytes(json)?; + let decoded_result = if flags & EVAL_JSON_INVALID_UTF8_SUBSTITUTE != 0 { + json_validate::decode_result_substituting_invalid_utf8(&bytes, depth as usize) + } else if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) + } else { + json_validate::decode_result(&bytes, depth as usize) + }; + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => { + let (code, message) = eval_json_parse_error_details(error, &bytes); + if flags & EVAL_JSON_THROW_ON_ERROR != 0 { + return eval_throw_json_exception(code, &message, context, values); + } + context.set_json_error(code, message); + return values.null(); + } + }; + context.clear_json_error(); + eval_json_decode_to_cell(decoded, flags, objects_as_assoc, values) +} + +/// Materializes one parsed JSON value as an eval runtime cell. +fn eval_json_decode_to_cell( + value: JsonValue, + flags: i64, + objects_as_assoc: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + match value { + JsonValue::Null => values.null(), + JsonValue::Bool(value) => values.bool_value(value), + JsonValue::Number(value) => eval_json_decode_number_to_cell(&value, flags, values), + JsonValue::String(value) => values.string_bytes_value(&value), + JsonValue::Array(elements) => { + let mut result = values.array_new(elements.len())?; + for (index, element) in elements.into_iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let element = eval_json_decode_to_cell(element, flags, objects_as_assoc, values)?; + result = values.array_set(result, key, element)?; + } + Ok(result) + } + JsonValue::Object(entries) => { + if !objects_as_assoc { + return eval_json_decode_object_to_cell(entries, flags, values); + } + let mut result = values.assoc_new(entries.len())?; + for (key, value) in entries { + let key = values.string_bytes_value(&key)?; + let value = eval_json_decode_to_cell(value, flags, objects_as_assoc, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) + } + } +} + +/// Materializes a parsed JSON object as a `stdClass` runtime object. +fn eval_json_decode_object_to_cell( + entries: Vec<(Vec, JsonValue)>, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + for (key, value) in entries { + let key = std::str::from_utf8(&key).map_err(|_| EvalStatus::RuntimeFatal)?; + let value = eval_json_decode_to_cell(value, flags, false, values)?; + values.property_set(object, key, value)?; + } + Ok(object) +} + +/// Materializes one JSON number as an int when possible and as a float otherwise. +fn eval_json_decode_number_to_cell( + value: &[u8], + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + if flags & EVAL_JSON_BIGINT_AS_STRING != 0 && eval_json_number_overflows_i64(value) { + return values.string_bytes_value(value); + } + let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; + if !value.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E')) { + if let Ok(integer) = value.parse::() { + return values.int(integer); + } + } + let float = value.parse::().map_err(|_| EvalStatus::RuntimeFatal)?; + values.float(float) +} + +/// Returns true when one integer-grammar JSON number exceeds PHP's int range. +fn eval_json_number_overflows_i64(value: &[u8]) -> bool { + if value.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) { + return false; + } + let (negative, digits) = if let Some(digits) = value.strip_prefix(b"-") { + (true, digits) + } else { + (false, value) + }; + let threshold = if negative { + b"9223372036854775808".as_slice() + } else { + b"9223372036854775807".as_slice() + }; + digits.len() > threshold.len() || digits.len() == threshold.len() && digits > threshold +} + +/// Evaluates PHP `json_last_error()` from the eval interpreter's current JSON state. +pub(super) fn eval_builtin_json_last_error( + args: &[EvalExpr], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.int(context.json_last_error()) +} + +/// Evaluates PHP `json_last_error_msg()` from the eval interpreter's current JSON state. +pub(super) fn eval_builtin_json_last_error_msg( + args: &[EvalExpr], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string(context.json_last_error_msg()) +} + +/// Evaluates PHP `json_validate()` for zero-flag JSON text validation. +pub(super) fn eval_builtin_json_validate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [json] => { + let json = eval_expr(json, context, scope, values)?; + eval_json_validate_result(json, None, None, context, values) + } + [json, depth] => { + let json = eval_expr(json, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_validate_result(json, Some(depth), None, context, values) + } + [json, depth, flags] => { + let json = eval_expr(json, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_validate_result(json, Some(depth), Some(flags), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Validates JSON text with eval's current zero-flag JSON subset and records JSON state. +pub(super) fn eval_json_validate_result( + json: RuntimeCellHandle, + depth: Option, + flags: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + if flags & !EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let bytes = values.string_bytes(json)?; + let result = if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) + } else { + json_validate::decode_result(&bytes, depth as usize) + }; + match result { + Ok(_) => { + context.clear_json_error(); + values.bool_value(true) + } + Err(error) => { + eval_record_json_parse_error(context, error, &bytes); + values.bool_value(false) + } + } +} + +/// Records one parser error into the eval-local PHP JSON error slots. +fn eval_record_json_parse_error( + context: &mut ElephcEvalContext, + error: JsonParseError, + bytes: &[u8], +) { + let (code, message) = eval_json_parse_error_details(error, bytes); + context.set_json_error(code, message); +} + +/// Builds the PHP JSON error code and message for one parser failure. +fn eval_json_parse_error_details(error: JsonParseError, bytes: &[u8]) -> (i64, String) { + let (code, message) = eval_json_parse_error_status(error.kind()); + let message = eval_json_error_message_with_location(message, bytes, error.offset()); + (code, message) +} + +/// Creates and schedules a `JsonException` through eval's normal Throwable channel. +fn eval_throw_json_exception( + code: i64, + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.set_json_error(code, message.to_string()); + let exception = values.new_object("JsonException")?; + let message = values.string(message)?; + let code = values.int(code)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Maps eval JSON parser failures to PHP `JSON_ERROR_*` codes and messages. +fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str) { + match error { + JsonParseErrorKind::Depth => (EVAL_JSON_ERROR_DEPTH, "Maximum stack depth exceeded"), + JsonParseErrorKind::Syntax => (EVAL_JSON_ERROR_SYNTAX, "Syntax error"), + JsonParseErrorKind::ControlChar => ( + EVAL_JSON_ERROR_CTRL_CHAR, + "Control character error, possibly incorrectly encoded", + ), + JsonParseErrorKind::Utf8 => (EVAL_JSON_ERROR_UTF8, EVAL_JSON_UTF8_MESSAGE), + JsonParseErrorKind::Utf16 => ( + EVAL_JSON_ERROR_UTF16, + "Single unpaired UTF-16 surrogate in unicode escape", + ), + } +} + +/// Adds PHP's JSON line/column suffix to one base error message. +fn eval_json_error_message_with_location(message: &str, bytes: &[u8], offset: usize) -> String { + let (line, column) = eval_json_error_location(bytes, offset); + format!("{message} near location {line}:{column}") +} + +/// Converts a zero-based JSON byte offset into PHP-style one-based line and column. +fn eval_json_error_location(bytes: &[u8], offset: usize) -> (usize, usize) { + let mut line = 1; + let mut column = 1; + let offset = offset.min(bytes.len()); + for byte in &bytes[..offset] { + if *byte == b'\n' { + line += 1; + column = 1; + } else { + column += 1; + } + } + (line, column) +} + +/// Appends one JSON value to the output buffer. +fn eval_json_encode_append( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_INT => output.extend_from_slice(&values.string_bytes(value)?), + EVAL_TAG_FLOAT => { + eval_json_encode_append_float(value, values, flags, error, output)?; + } + EVAL_TAG_STRING => eval_json_encode_append_string( + &values.string_bytes(value)?, + flags, + EvalJsonStringPosition::Value, + error, + output, + )?, + EVAL_TAG_BOOL => { + if values.truthy(value)? { + output.extend_from_slice(b"true"); + } else { + output.extend_from_slice(b"false"); + } + } + EVAL_TAG_ARRAY => { + eval_json_encode_append_indexed_array( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_ASSOC => { + eval_json_encode_append_assoc( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_OBJECT => { + eval_json_encode_append_object( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_NULL | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), + _ => return Err(EvalStatus::UnsupportedConstruct), + } + Ok(()) +} + +#[derive(Clone, Copy)] +struct EvalJsonEncodeError { + code: i64, + message: &'static str, +} + +/// Marks whether a JSON string is being encoded as a value or as an object key. +#[derive(Clone, Copy)] +enum EvalJsonStringPosition { + Value, + Key, +} + +/// Appends one JSON float while preserving a `.0` suffix when requested. +fn eval_json_encode_append_float( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let float = eval_float_value(value, values)?; + if !float.is_finite() { + *error = Some(EvalJsonEncodeError { + code: EVAL_JSON_ERROR_INF_OR_NAN, + message: EVAL_JSON_INF_OR_NAN_MESSAGE, + }); + output.push(b'0'); + return Ok(()); + } + let bytes = values.string_bytes(value)?; + output.extend_from_slice(&bytes); + if flags & EVAL_JSON_PRESERVE_ZERO_FRACTION != 0 + && !bytes.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) + { + output.extend_from_slice(b".0"); + } + Ok(()) +} + +/// Appends one indexed eval array as a JSON array or forced JSON object. +fn eval_json_encode_append_indexed_array( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let force_object = flags & EVAL_JSON_FORCE_OBJECT != 0; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(if force_object { b'{' } else { b'[' }); + let len = values.array_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.array_iter_key(value, position)?; + if force_object { + eval_json_encode_append_string( + &values.string_bytes(key)?, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + } + let element = values.array_get(value, key)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(if force_object { b'}' } else { b']' }); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one associative eval array as a JSON object. +fn eval_json_encode_append_assoc( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(b'{'); + let len = values.array_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.array_iter_key(value, position)?; + eval_json_encode_append_string( + &values.string_bytes(key)?, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + let element = values.array_get(value, key)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(b'}'); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one eval runtime object as a JSON object. +fn eval_json_encode_append_object( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(b'{'); + let len = values.object_property_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.object_property_iter_key(value, position)?; + let key_bytes = values.string_bytes(key)?; + eval_json_encode_append_string( + &key_bytes, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + let property = std::str::from_utf8(&key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let element = values.property_get(value, property)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(b'}'); + arrays_seen.pop(); + Ok(()) +} + +/// Appends a JSON object colon, including pretty-print spacing when active. +fn eval_json_encode_append_colon(flags: i64, output: &mut Vec) { + if flags & EVAL_JSON_PRETTY_PRINT != 0 { + output.extend_from_slice(b": "); + } else { + output.push(b':'); + } +} + +/// Appends PHP's four-space JSON pretty-print indentation for one nesting level. +fn eval_json_encode_pretty_indent(output: &mut Vec, depth: usize) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} + +/// Records entry into one JSON array/object, rejecting depth overrun and recursion. +fn eval_json_encode_enter_array( + value: RuntimeCellHandle, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, +) -> Result<(), EvalStatus> { + if depth >= depth_limit { + return Err(EvalStatus::RuntimeFatal); + } + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + return Err(EvalStatus::RuntimeFatal); + } + arrays_seen.push(address); + Ok(()) +} + +/// Appends one JSON string with eval-supported PHP flag handling. +fn eval_json_encode_append_string( + bytes: &[u8], + flags: i64, + position: EvalJsonStringPosition, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + if flags & EVAL_JSON_NUMERIC_CHECK != 0 { + if let Some(number) = eval_json_numeric_check_bytes(bytes) { + output.extend_from_slice(&number); + return Ok(()); + } + } + let start_len = output.len(); + output.push(b'"'); + if let Ok(value) = std::str::from_utf8(bytes) { + for character in value.chars() { + eval_json_encode_append_char(character, flags, output); + } + } else if flags & (EVAL_JSON_INVALID_UTF8_IGNORE | EVAL_JSON_INVALID_UTF8_SUBSTITUTE) == 0 { + output.truncate(start_len); + *error = Some(EvalJsonEncodeError { + code: EVAL_JSON_ERROR_UTF8, + message: EVAL_JSON_UTF8_MESSAGE, + }); + match position { + EvalJsonStringPosition::Value => output.extend_from_slice(b"null"), + EvalJsonStringPosition::Key => output.extend_from_slice(b"\"\""), + } + return Ok(()); + } else { + eval_json_encode_append_invalid_utf8_bytes(bytes, flags, output)?; + } + output.push(b'"'); + Ok(()) +} + +/// Appends one valid UTF-8 character using PHP JSON string escaping rules. +fn eval_json_encode_append_char(character: char, flags: i64, output: &mut Vec) { + if character.is_ascii() { + eval_json_encode_append_ascii_byte(character as u8, flags, output); + } else if flags & EVAL_JSON_UNESCAPED_UNICODE != 0 { + let mut buffer = [0_u8; 4]; + output.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes()); + } else { + eval_json_encode_append_unicode_escape(character as u32, output); + } +} + +/// Appends one ASCII byte using JSON escaping rules shared by UTF-8 and fallback paths. +fn eval_json_encode_append_ascii_byte(byte: u8, flags: i64, output: &mut Vec) { + match byte { + b'"' if flags & EVAL_JSON_HEX_QUOT != 0 => output.extend_from_slice(b"\\u0022"), + b'"' => output.extend_from_slice(b"\\\""), + b'\\' => output.extend_from_slice(b"\\\\"), + b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { + output.extend_from_slice(b"\\/"); + } + b'/' => output.push(b'/'), + b'<' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003C"), + b'>' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003E"), + b'&' if flags & EVAL_JSON_HEX_AMP != 0 => output.extend_from_slice(b"\\u0026"), + b'\'' if flags & EVAL_JSON_HEX_APOS != 0 => output.extend_from_slice(b"\\u0027"), + b'\x08' => output.extend_from_slice(b"\\b"), + b'\x0c' => output.extend_from_slice(b"\\f"), + b'\n' => output.extend_from_slice(b"\\n"), + b'\r' => output.extend_from_slice(b"\\r"), + b'\t' => output.extend_from_slice(b"\\t"), + control @ 0x00..=0x1f => { + output.extend_from_slice(format!("\\u{control:04x}").as_bytes()); + } + _ => output.push(byte), + } +} + +/// Appends valid scalar values as PHP JSON `\uXXXX` escapes, using surrogate pairs when needed. +fn eval_json_encode_append_unicode_escape(codepoint: u32, output: &mut Vec) { + if codepoint <= 0xffff { + output.extend_from_slice(format!("\\u{codepoint:04x}").as_bytes()); + return; + } + + let codepoint = codepoint - 0x1_0000; + let high = 0xd800 + ((codepoint >> 10) & 0x3ff); + let low = 0xdc00 + (codepoint & 0x3ff); + output.extend_from_slice(format!("\\u{high:04x}\\u{low:04x}").as_bytes()); +} + +/// Appends malformed UTF-8 bytes according to PHP's JSON invalid-UTF-8 flags. +fn eval_json_encode_append_invalid_utf8_bytes( + mut bytes: &[u8], + flags: i64, + output: &mut Vec, +) -> Result<(), EvalStatus> { + while !bytes.is_empty() { + match std::str::from_utf8(bytes) { + Ok(value) => { + for character in value.chars() { + eval_json_encode_append_char(character, flags, output); + } + return Ok(()); + } + Err(error) => { + let valid = &bytes[..error.valid_up_to()]; + for character in std::str::from_utf8(valid) + .map_err(|_| EvalStatus::RuntimeFatal)? + .chars() + { + eval_json_encode_append_char(character, flags, output); + } + let invalid_len = error + .error_len() + .unwrap_or(bytes.len() - valid.len()) + .max(1); + if flags & EVAL_JSON_INVALID_UTF8_IGNORE == 0 { + eval_json_encode_append_char('\u{fffd}', flags, output); + } + bytes = &bytes[valid.len() + invalid_len.min(bytes.len() - valid.len())..]; + } + } + } + Ok(()) +} + +/// Returns the JSON number bytes for a PHP numeric string when `JSON_NUMERIC_CHECK` applies. +fn eval_json_numeric_check_bytes(bytes: &[u8]) -> Option> { + let value = std::str::from_utf8(bytes).ok()?.trim(); + if value.is_empty() { + return None; + } + let integer_grammar = value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'+' | b'-')); + if integer_grammar { + if let Ok(integer) = value.parse::() { + return Some(integer.to_string().into_bytes()); + } + } + let number = value.parse::().ok()?; + if number.is_finite() { + Some(number.to_string().into_bytes()) + } else { + None + } +} diff --git a/crates/elephc-eval/src/interpreter/libc_shims.rs b/crates/elephc-eval/src/interpreter/libc_shims.rs new file mode 100644 index 0000000000..1f6898483c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/libc_shims.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Declares libc routines used by eval builtins that mirror PHP process and network helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` +//! - `crate::interpreter::builtins::filesystem` +//! +//! Key details: +//! - These bindings are unsafe FFI declarations only; call sites own pointer validation and +//! PHP-compatible fallback behavior. + +unsafe extern "C" { + /// Reverse-resolves one socket address through libc's `gethostbyaddr`. + #[link_name = "gethostbyaddr"] + pub(super) fn libc_gethostbyaddr( + addr: *const libc::c_void, + len: libc::socklen_t, + type_: libc::c_int, + ) -> *mut libc::hostent; + + /// Looks up one IP protocol entry by protocol name or alias. + #[link_name = "getprotobyname"] + pub(super) fn libc_getprotobyname(name: *const libc::c_char) -> *mut libc::protoent; + + /// Looks up one IP protocol entry by protocol number. + #[link_name = "getprotobynumber"] + pub(super) fn libc_getprotobynumber(proto: libc::c_int) -> *mut libc::protoent; + + /// Looks up one internet service entry by service name and protocol. + #[link_name = "getservbyname"] + pub(super) fn libc_getservbyname( + name: *const libc::c_char, + proto: *const libc::c_char, + ) -> *mut libc::servent; + + /// Looks up one internet service entry by port and protocol. + #[link_name = "getservbyport"] + pub(super) fn libc_getservbyport( + port: libc::c_int, + proto: *const libc::c_char, + ) -> *mut libc::servent; + + /// Sets the process file-creation mask and returns the previous mask. + pub(super) fn umask(mask: u32) -> u32; +} diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index 8ec7f365af..e5d0d0036e 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -11,7 +11,21 @@ //! - This module does not own PHP values. Constants and operations are delegated //! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. +mod array_literals; mod builtins; +mod constant_eval; +mod constants; +mod control; +mod core_builtins; +mod debug_output; +mod dynamic_functions; +mod expressions; +mod include_exec; +mod json; +mod libc_shims; +mod runtime_ops; +mod scope_cells; +mod statements; use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; @@ -24,642 +38,34 @@ use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; +use array_literals::*; use builtins::*; +use constant_eval::*; +use constants::*; +pub use control::EvalOutcome; +use control::{ + EvalArraySpliceDirectArgs, EvalControl, EvalPredefinedConstant, EvalSprintfSpec, + EvaluatedCallArg, EvaluatedCallable, +}; +use core_builtins::*; +use debug_output::*; +use dynamic_functions::*; +use expressions::*; +use include_exec::*; +use json::*; +use libc_shims::*; use regex::bytes::{Captures, Regex, RegexBuilder}; +pub use runtime_ops::RuntimeValueOps; +use runtime_ops::*; +use scope_cells::*; +use statements::*; use std::ffi::{CStr, CString}; use std::mem::MaybeUninit; use std::net::ToSocketAddrs; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::Ordering; use std::time::{SystemTime, UNIX_EPOCH}; -/// Internal statement-control result used to propagate eval returns and loops. -enum EvalControl { - None, - Return(RuntimeCellHandle), - Throw(RuntimeCellHandle), - Break, - Continue, -} - -/// Final result of executing a parsed eval program. -pub enum EvalOutcome { - Value(RuntimeCellHandle), - Throwable(RuntimeCellHandle), -} - -/// One already evaluated function-like call argument. -#[derive(Clone)] -struct EvaluatedCallArg { - name: Option, - value: RuntimeCellHandle, -} - -/// One already evaluated PHP callback supported by the eval dispatcher. -enum EvaluatedCallable { - Named(String), - ObjectMethod { - object: RuntimeCellHandle, - method: String, - }, -} - -/// Bound argument tuple for direct `array_splice()` calls. -type EvalArraySpliceDirectArgs = ( - String, - RuntimeCellHandle, - Option, - Option, -); - -/// Parsed flags for one eval `sprintf()` conversion specifier. -#[derive(Clone, Copy)] -struct EvalSprintfSpec { - left_align: bool, - force_sign: bool, - space_sign: bool, - zero_pad: bool, - alternate: bool, - width: Option, - precision: Option, - specifier: u8, -} - -/// Eval-visible predefined constant payloads that are not stored in the dynamic context. -enum EvalPredefinedConstant { - Int(i64), - Float(f64), - String(&'static str), -} - -/// Hash algorithm names supported by eval `hash_algos()`, matching native runtime order. -const EVAL_HASH_ALGOS: &[&str] = &[ - "md2", - "md4", - "md5", - "sha1", - "sha224", - "sha256", - "sha384", - "sha512", - "sha512/224", - "sha512/256", - "sha3-224", - "sha3-256", - "sha3-384", - "sha3-512", - "ripemd128", - "ripemd160", - "ripemd256", - "ripemd320", - "whirlpool", - "crc32", - "crc32b", - "crc32c", - "adler32", - "fnv132", - "fnv1a32", - "fnv164", - "fnv1a64", - "joaat", -]; - -/// Built-in stream wrappers reported by eval `stream_get_wrappers()`. -const EVAL_STREAM_WRAPPERS: &[&str] = &[ - "file", - "php", - "data", - "ftp", - "http", - "https", - "ftps", - "compress.zlib", - "compress.bzip2", - "phar", - "glob", -]; - -/// Built-in stream transports reported by eval `stream_get_transports()`. -const EVAL_STREAM_TRANSPORTS: &[&str] = &[ - "tcp", "udp", "unix", "udg", "tls", "ssl", "sslv2", "sslv3", "tlsv1.0", "tlsv1.1", "tlsv1.2", - "tlsv1.3", -]; - -/// Monotonic salt mixed into eval `rand()`/`mt_rand()` and array key sampling. -static EVAL_RANDOM_COUNTER: AtomicU64 = AtomicU64::new(0); - -/// Built-in stream filters reported by eval `stream_get_filters()`. -const EVAL_STREAM_FILTERS: &[&str] = &[ - "string.toupper", - "string.tolower", - "string.rot13", - "string.strip_tags", - "convert.base64-encode", - "convert.base64-decode", - "convert.quoted-printable-encode", - "convert.quoted-printable-decode", - "convert.iconv.*", - "dechunk", - "zlib.deflate", - "zlib.inflate", - "bzip2.compress", - "bzip2.decompress", -]; - -/// SPL/core type names reported by eval `spl_classes()`. -/// -/// Mirrors `src/codegen/builtins/spl/mod.rs::SPL_CLASS_NAMES` so dynamic eval -/// exposes the same static registry snapshot as native code. -const EVAL_SPL_CLASS_NAMES: &[&str] = &[ - "AppendIterator", - "ArrayAccess", - "ArrayIterator", - "ArrayObject", - "BadFunctionCallException", - "BadMethodCallException", - "CachingIterator", - "CallbackFilterIterator", - "Countable", - "DomainException", - "DirectoryIterator", - "EmptyIterator", - "Error", - "Exception", - "FilterIterator", - "FilesystemIterator", - "GlobIterator", - "InfiniteIterator", - "InvalidArgumentException", - "Iterator", - "IteratorAggregate", - "IteratorIterator", - "JsonSerializable", - "LengthException", - "LimitIterator", - "LogicException", - "MultipleIterator", - "NoRewindIterator", - "OuterIterator", - "OutOfBoundsException", - "OutOfRangeException", - "OverflowException", - "ParentIterator", - "RangeException", - "RecursiveArrayIterator", - "RecursiveCachingIterator", - "RecursiveCallbackFilterIterator", - "RecursiveDirectoryIterator", - "RecursiveFilterIterator", - "RecursiveIterator", - "RecursiveIteratorIterator", - "RecursiveRegexIterator", - "RegexIterator", - "RuntimeException", - "SeekableIterator", - "SplDoublyLinkedList", - "SplFixedArray", - "SplFileInfo", - "SplFileObject", - "SplObserver", - "SplQueue", - "SplStack", - "SplSubject", - "SplTempFileObject", - "Stringable", - "Throwable", - "Traversable", - "TypeError", - "UnderflowException", - "UnexpectedValueException", - "ValueError", -]; - -/// Full English month names used by eval `date()`. -const EVAL_MONTH_NAMES: &[&str; 12] = &[ - "January", - "February", - "March", - "April", - "May", - "June", - "July", - "August", - "September", - "October", - "November", - "December", -]; - -/// Short English month names used by eval `date()`. -const EVAL_MONTH_SHORT_NAMES: &[&str; 12] = &[ - "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", -]; - -/// Full English weekday names used by eval `date()`. -const EVAL_WEEKDAY_NAMES: &[&str; 7] = &[ - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", -]; - -/// Short English weekday names used by eval `date()`. -const EVAL_WEEKDAY_SHORT_NAMES: &[&str; 7] = &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - -/// Root package manifest used to mirror native `phpversion()` in the eval crate. -const EVAL_ROOT_CARGO_TOML: &str = include_str!("../../../../Cargo.toml"); - -unsafe extern "C" { - /// Reverse-resolves one socket address through libc's `gethostbyaddr`. - #[link_name = "gethostbyaddr"] - fn libc_gethostbyaddr( - addr: *const libc::c_void, - len: libc::socklen_t, - type_: libc::c_int, - ) -> *mut libc::hostent; - - /// Looks up one IP protocol entry by protocol name or alias. - #[link_name = "getprotobyname"] - fn libc_getprotobyname(name: *const libc::c_char) -> *mut libc::protoent; - - /// Looks up one IP protocol entry by protocol number. - #[link_name = "getprotobynumber"] - fn libc_getprotobynumber(proto: libc::c_int) -> *mut libc::protoent; - - /// Looks up one internet service entry by service name and protocol. - #[link_name = "getservbyname"] - fn libc_getservbyname( - name: *const libc::c_char, - proto: *const libc::c_char, - ) -> *mut libc::servent; - - /// Looks up one internet service entry by port and protocol. - #[link_name = "getservbyport"] - fn libc_getservbyport(port: libc::c_int, proto: *const libc::c_char) -> *mut libc::servent; -} - -/// Runtime value hooks required by the EvalIR interpreter. -pub trait RuntimeValueOps { - /// Creates a runtime indexed-array cell with room for at least `capacity` elements. - fn array_new(&mut self, capacity: usize) -> Result; - - /// Creates a runtime associative-array cell with room for at least `capacity` elements. - fn assoc_new(&mut self, capacity: usize) -> Result; - - /// Reads one element from a runtime Mixed cell using PHP array-read semantics. - /// - /// Missing keys and non-array receivers return PHP null, matching the generated - /// `__rt_mixed_array_get` runtime helper. - fn array_get( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - ) -> Result; - - /// Checks whether a normalized PHP array key exists without conflating null values with misses. - fn array_key_exists( - &mut self, - key: RuntimeCellHandle, - array: RuntimeCellHandle, - ) -> Result; - - /// Returns the foreach-visible key at a zero-based iteration position. - fn array_iter_key( - &mut self, - array: RuntimeCellHandle, - position: usize, - ) -> Result; - - /// Writes one element to a runtime array-like Mixed cell and returns the target cell. - fn array_set( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - value: RuntimeCellHandle, - ) -> Result; - - /// Reads a named property from a runtime object held in a boxed Mixed cell. - fn property_get( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result; - - /// Writes a named property on a runtime object held in a boxed Mixed cell. - fn property_set( - &mut self, - object: RuntimeCellHandle, - property: &str, - value: RuntimeCellHandle, - ) -> Result<(), EvalStatus>; - - /// Returns the number of public JSON-visible properties on a runtime object. - fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result; - - /// Returns the public property key at a zero-based JSON object iteration position. - fn object_property_iter_key( - &mut self, - object: RuntimeCellHandle, - position: usize, - ) -> Result; - - /// Calls a named method on a runtime object held in a boxed Mixed cell. - fn method_call( - &mut self, - object: RuntimeCellHandle, - method: &str, - args: Vec, - ) -> Result; - - /// Creates a named runtime object without constructor arguments. - fn new_object(&mut self, class_name: &str) -> Result; - - /// Calls the runtime constructor for an object when the class declares one. - fn construct_object( - &mut self, - object: RuntimeCellHandle, - args: Vec, - ) -> Result<(), EvalStatus>; - - /// Returns whether a runtime class table contains the requested class name. - fn class_exists(&mut self, name: &str) -> Result; - - /// Returns whether a runtime interface table contains the requested interface name. - fn interface_exists(&mut self, name: &str) -> Result; - - /// Returns whether a runtime trait table contains the requested trait name. - fn trait_exists(&mut self, name: &str) -> Result; - - /// Returns whether a runtime enum table contains the requested enum name. - fn enum_exists(&mut self, name: &str) -> Result; - - /// Tests whether a boxed object cell satisfies a class/interface relation. - fn object_is_a( - &mut self, - object_or_class: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - ) -> Result; - - /// Returns the PHP-visible runtime class name for an object cell. - fn object_class_name( - &mut self, - object: RuntimeCellHandle, - ) -> Result; - - /// Returns the PHP-visible parent class name for an object or class-name cell. - fn parent_class_name( - &mut self, - object_or_class: RuntimeCellHandle, - ) -> Result; - - /// Returns the visible element count for an array-like runtime cell. - fn array_len(&mut self, array: RuntimeCellHandle) -> Result; - - /// Returns whether a runtime cell can be indexed like an array by eval writes. - fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result; - - /// Returns whether a runtime cell holds PHP null. - fn is_null(&mut self, value: RuntimeCellHandle) -> Result; - - /// Returns the concrete boxed Mixed runtime tag after unwrapping nested Mixed cells. - fn type_tag(&mut self, value: RuntimeCellHandle) -> Result; - - /// Returns the unboxed object payload pointer used for PHP object identity. - fn object_identity(&mut self, object: RuntimeCellHandle) -> Result; - - /// Releases one owned runtime cell that is no longer held by the eval scope. - fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; - - /// Retains one runtime cell so the eval caller receives an independent owner. - fn retain(&mut self, value: RuntimeCellHandle) -> Result; - - /// Emits or suppresses one PHP runtime warning through the target runtime. - fn warning(&mut self, message: &str) -> Result<(), EvalStatus>; - - /// Creates a runtime null cell. - fn null(&mut self) -> Result; - - /// Creates a runtime bool cell. - fn bool_value(&mut self, value: bool) -> Result; - - /// Creates a runtime int cell. - fn int(&mut self, value: i64) -> Result; - - /// Creates a runtime float cell. - fn float(&mut self, value: f64) -> Result; - - /// Creates a runtime string cell. - fn string(&mut self, value: &str) -> Result; - - /// Creates a runtime byte-string cell from raw PHP string bytes. - fn string_bytes_value(&mut self, value: &[u8]) -> Result; - - /// Casts one runtime cell to a boxed PHP integer cell. - fn cast_int(&mut self, value: RuntimeCellHandle) -> Result; - - /// Casts one runtime cell to a boxed PHP float cell. - fn cast_float(&mut self, value: RuntimeCellHandle) -> Result; - - /// Casts one runtime cell to a boxed PHP string cell. - fn cast_string(&mut self, value: RuntimeCellHandle) -> Result; - - /// Casts one runtime cell to a boxed PHP boolean cell. - fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result; - - /// Computes PHP `abs()` for one runtime cell while preserving integer/float result typing. - fn abs(&mut self, value: RuntimeCellHandle) -> Result; - - /// Computes PHP `ceil()` for one runtime cell after PHP numeric conversion. - fn ceil(&mut self, value: RuntimeCellHandle) -> Result; - - /// Computes PHP `floor()` for one runtime cell after PHP numeric conversion. - fn floor(&mut self, value: RuntimeCellHandle) -> Result; - - /// Computes PHP `sqrt()` for one runtime cell after PHP numeric conversion. - fn sqrt(&mut self, value: RuntimeCellHandle) -> Result; - - /// Reverses a string value using PHP `strrev()` byte-string semantics. - fn strrev(&mut self, value: RuntimeCellHandle) -> Result; - - /// Divides two runtime cells using PHP `fdiv()` semantics. - fn fdiv( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Computes the floating-point remainder using PHP `fmod()` semantics. - fn fmod( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Adds two runtime cells using PHP addition semantics. - fn add( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Subtracts two runtime cells using PHP numeric semantics. - fn sub( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Multiplies two runtime cells using PHP numeric semantics. - fn mul( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Divides two runtime cells using PHP numeric semantics. - fn div( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Computes modulo for two runtime cells using PHP integer modulo semantics. - fn modulo( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Raises one runtime cell to another using PHP exponentiation semantics. - fn pow( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Rounds one runtime cell using PHP `round()` semantics and optional precision. - fn round( - &mut self, - value: RuntimeCellHandle, - precision: Option, - ) -> Result; - - /// Applies an integer bitwise or shift operation to two runtime cells. - fn bitwise( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Applies integer bitwise NOT to one runtime cell. - fn bit_not(&mut self, value: RuntimeCellHandle) -> Result; - - /// Concatenates two runtime cells using PHP string conversion semantics. - fn concat( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Compares two runtime cells and returns a boxed PHP boolean cell. - fn compare( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Compares two runtime cells and returns a boxed PHP spaceship integer. - fn spaceship( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result; - - /// Emits one runtime cell to stdout using PHP echo semantics. - fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; - - /// Casts one runtime cell to a PHP string and copies its bytes for parsing. - fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus>; - - /// Converts one runtime cell to PHP boolean truthiness. - fn truthy(&mut self, value: RuntimeCellHandle) -> Result; -} - -const EVAL_TAG_INT: u64 = 0; -const EVAL_TAG_STRING: u64 = 1; -const EVAL_TAG_FLOAT: u64 = 2; -const EVAL_TAG_BOOL: u64 = 3; -const EVAL_TAG_ARRAY: u64 = 4; -const EVAL_TAG_ASSOC: u64 = 5; -const EVAL_TAG_OBJECT: u64 = 6; -const EVAL_TAG_NULL: u64 = 8; -const EVAL_TAG_RESOURCE: u64 = 9; -const DEFINE_ALREADY_DEFINED_WARNING: &str = "Warning: define(): Constant already defined\n"; -const HEX2BIN_ODD_LENGTH_WARNING: &str = - "Warning: hex2bin(): Hexadecimal input string must have an even length\n"; -const HEX2BIN_INVALID_WARNING: &str = - "Warning: hex2bin(): Input string must be hexadecimal string\n"; -const EVAL_PATHINFO_DIRNAME: i64 = 1; -const EVAL_PATHINFO_BASENAME: i64 = 2; -const EVAL_PATHINFO_EXTENSION: i64 = 4; -const EVAL_PATHINFO_FILENAME: i64 = 8; -const EVAL_PATHINFO_ALL: i64 = 15; -const EVAL_FNM_NOESCAPE: i64 = 1; -const EVAL_FNM_PATHNAME: i64 = 2; -const EVAL_FNM_PERIOD: i64 = 4; -const EVAL_FNM_CASEFOLD: i64 = 16; -const EVAL_ARRAY_FILTER_USE_VALUE: i64 = 0; -const EVAL_ARRAY_FILTER_USE_BOTH: i64 = 1; -const EVAL_ARRAY_FILTER_USE_KEY: i64 = 2; -const EVAL_COUNT_NORMAL: i64 = 0; -const EVAL_COUNT_RECURSIVE: i64 = 1; -const EVAL_PREG_SPLIT_NO_EMPTY: i64 = 1; -const EVAL_PREG_SPLIT_DELIM_CAPTURE: i64 = 2; -const EVAL_PREG_SPLIT_OFFSET_CAPTURE: i64 = 4; -const EVAL_PREG_PATTERN_ORDER: i64 = 1; -const EVAL_PREG_SET_ORDER: i64 = 2; -const EVAL_PREG_OFFSET_CAPTURE: i64 = 256; -const EVAL_PREG_UNMATCHED_AS_NULL: i64 = 512; -const EVAL_JSON_ERROR_NONE: i64 = 0; -const EVAL_JSON_ERROR_DEPTH: i64 = 1; -const EVAL_JSON_ERROR_STATE_MISMATCH: i64 = 2; -const EVAL_JSON_ERROR_CTRL_CHAR: i64 = 3; -const EVAL_JSON_ERROR_SYNTAX: i64 = 4; -const EVAL_JSON_ERROR_UTF8: i64 = 5; -const EVAL_JSON_ERROR_RECURSION: i64 = 6; -const EVAL_JSON_ERROR_INF_OR_NAN: i64 = 7; -const EVAL_JSON_ERROR_UNSUPPORTED_TYPE: i64 = 8; -const EVAL_JSON_ERROR_INVALID_PROPERTY_NAME: i64 = 9; -const EVAL_JSON_ERROR_UTF16: i64 = 10; -const EVAL_JSON_HEX_TAG: i64 = 1; -const EVAL_JSON_HEX_AMP: i64 = 2; -const EVAL_JSON_HEX_APOS: i64 = 4; -const EVAL_JSON_HEX_QUOT: i64 = 8; -const EVAL_JSON_BIGINT_AS_STRING: i64 = 2; -const EVAL_JSON_FORCE_OBJECT: i64 = 16; -const EVAL_JSON_NUMERIC_CHECK: i64 = 32; -const EVAL_JSON_UNESCAPED_SLASHES: i64 = 64; -const EVAL_JSON_PRETTY_PRINT: i64 = 128; -const EVAL_JSON_UNESCAPED_UNICODE: i64 = 256; -const EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR: i64 = 512; -const EVAL_JSON_PRESERVE_ZERO_FRACTION: i64 = 1024; -const EVAL_JSON_INVALID_UTF8_IGNORE: i64 = 1_048_576; -const EVAL_JSON_INVALID_UTF8_SUBSTITUTE: i64 = 2_097_152; -const EVAL_JSON_THROW_ON_ERROR: i64 = 4_194_304; -const EVAL_JSON_INF_OR_NAN_MESSAGE: &str = "Inf and NaN cannot be JSON encoded"; -const EVAL_JSON_UTF8_MESSAGE: &str = "Malformed UTF-8 characters, possibly incorrectly encoded"; - -unsafe extern "C" { - /// Sets the process file-creation mask and returns the previous mask. - fn umask(mask: u32) -> u32; -} - /// Executes an EvalIR program and returns the eval result cell. pub fn execute_program( program: &EvalProgram, @@ -790,3362 +196,6 @@ pub fn execute_context_function_call_array_outcome( } } -/// Executes statements in source order and propagates the first eval `return`. -fn execute_statements( - statements: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - for stmt in statements { - match execute_stmt(stmt, context, scope, values)? { - EvalControl::None => {} - control => return Ok(control), - } - } - Ok(EvalControl::None) -} - -/// Returns the eval-visible entry for a variable, following `global` aliases. -fn scope_entry( - context: &ElephcEvalContext, - scope: &ElephcEvalScope, - name: &str, -) -> Option { - let Some(global_name) = scope.global_alias_target(name) else { - return scope.entry(name); - }; - let Some(global_scope) = context.global_scope_ptr() else { - return scope.entry(name); - }; - let current_scope = scope as *const ElephcEvalScope as *mut ElephcEvalScope; - if global_scope == current_scope { - return scope.entry(global_name); - } - unsafe { - global_scope - .as_ref() - .and_then(|scope| scope.entry(global_name)) - } -} - -/// Returns the eval-visible cell for a variable, following `global` aliases. -fn visible_scope_cell( - context: &ElephcEvalContext, - scope: &ElephcEvalScope, - name: &str, -) -> Option { - scope_entry(context, scope, name) - .filter(|entry| entry.flags().is_visible()) - .map(ScopeEntry::cell) -} - -/// Stores a variable cell, redirecting `global` aliases to the global scope. -fn set_scope_cell( - context: &ElephcEvalContext, - scope: &mut ElephcEvalScope, - name: impl Into, - cell: RuntimeCellHandle, - ownership: ScopeCellOwnership, -) -> Result, EvalStatus> { - let name = name.into(); - if let Some(global_name) = scope.global_alias_target(&name).map(str::to_string) { - let Some(global_scope) = context.global_scope_ptr() else { - return Err(EvalStatus::RuntimeFatal); - }; - let current_scope = scope as *mut ElephcEvalScope; - if global_scope == current_scope { - return Ok(scope.set_respecting_references(global_name, cell, ownership)); - } - let Some(global_scope) = (unsafe { global_scope.as_mut() }) else { - return Err(EvalStatus::RuntimeFatal); - }; - return Ok(global_scope.set_respecting_references(global_name, cell, ownership)); - } - Ok(scope.set_respecting_references(name, cell, ownership)) -} - -/// Creates a PHP reference alias between two eval-visible variable names. -fn set_reference_alias( - context: &ElephcEvalContext, - scope: &mut ElephcEvalScope, - target: &str, - source: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(global_name) = scope.global_alias_target(source).map(str::to_string) { - scope.mark_global_alias_to(target.to_string(), global_name); - return Ok(Vec::new()); - } - let (cell, ownership) = scope_entry(context, scope, source) - .filter(|entry| entry.flags().is_visible()) - .map_or_else( - || values.null().map(|cell| (cell, ScopeCellOwnership::Owned)), - |entry| Ok((entry.cell(), entry.flags().ownership)), - )?; - Ok(scope.set_reference(target.to_string(), source.to_string(), cell, ownership)) -} - -/// Unsets a variable, removing only the local alias when the name is global. -fn unset_scope_cell( - scope: &mut ElephcEvalScope, - name: impl Into, -) -> Option { - let name = name.into(); - if scope.is_global_alias(&name) { - scope.clear_global_alias(&name); - } - scope.unset_respecting_references(name) -} - -/// Marks variables as aliases to the context global scope for later reads/writes. -fn execute_global_stmt( - vars: &[String], - context: &ElephcEvalContext, - scope: &mut ElephcEvalScope, -) -> Result<(), EvalStatus> { - if context.global_scope_ptr().is_none() { - return Err(EvalStatus::RuntimeFatal); - } - for name in vars { - scope.mark_global_alias(name.clone()); - } - Ok(()) -} - -/// Executes one statement and returns `Some` only for eval `return`. -fn execute_stmt( - stmt: &EvalStmt, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match stmt { - EvalStmt::ArrayAppendVar { name, value } => { - let mut ownership = ScopeCellOwnership::Owned; - let array = if let Some(existing) = - scope_entry(context, scope, name).filter(|entry| entry.flags().is_visible()) - { - if values.is_array_like(existing.cell())? { - let tag = values.type_tag(existing.cell())?; - if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::UnsupportedConstruct); - } - ownership = existing.flags().ownership; - existing.cell() - } else { - values.array_new(1)? - } - } else { - values.array_new(1)? - }; - let index = eval_array_append_key(array, values)?; - let value = eval_expr(value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { - values.release(replaced)?; - } - Ok(EvalControl::None) - } - EvalStmt::ArraySetVar { name, index, value } => { - let mut ownership = ScopeCellOwnership::Owned; - let array = if let Some(existing) = - scope_entry(context, scope, name).filter(|entry| entry.flags().is_visible()) - { - if values.is_array_like(existing.cell())? { - ownership = existing.flags().ownership; - existing.cell() - } else { - values.array_new(1)? - } - } else { - values.array_new(1)? - }; - let index = eval_expr(index, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { - values.release(replaced)?; - } - Ok(EvalControl::None) - } - EvalStmt::Break => Ok(EvalControl::Break), - EvalStmt::Continue => Ok(EvalControl::Continue), - EvalStmt::DoWhile { body, condition } => { - execute_do_while_stmt(body, condition, context, scope, values) - } - EvalStmt::Echo(expr) => { - let value = eval_expr(expr, context, scope, values)?; - values.echo(value)?; - Ok(EvalControl::None) - } - EvalStmt::For { - init, - condition, - update, - body, - } => execute_for_stmt( - init, - condition.as_ref(), - update, - body, - context, - scope, - values, - ), - EvalStmt::ClassDecl(class) => { - execute_class_decl_stmt(class, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::Foreach { - array, - key_name, - value_name, - body, - } => execute_foreach_stmt( - array, - key_name.as_deref(), - value_name, - body, - context, - scope, - values, - ), - EvalStmt::FunctionDecl { name, params, body } => { - let key = name.to_ascii_lowercase(); - context - .define_function( - key, - EvalFunction::new(name.clone(), params.clone(), body.clone()), - ) - .map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(EvalControl::None) - } - EvalStmt::Global { vars } => { - execute_global_stmt(vars, context, scope)?; - Ok(EvalControl::None) - } - EvalStmt::If { - condition, - then_branch, - else_branch, - } => { - let condition = eval_expr(condition, context, scope, values)?; - if values.truthy(condition)? { - execute_statements(then_branch, context, scope, values) - } else { - execute_statements(else_branch, context, scope, values) - } - } - EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr( - expr, context, scope, values, - )?)), - EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), - EvalStmt::ReferenceAssign { target, source } => { - for replaced in set_reference_alias(context, scope, target, source, values)? { - values.release(replaced)?; - } - Ok(EvalControl::None) - } - EvalStmt::StaticVar { name, init } => { - execute_static_var_stmt(name, init, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::PropertySet { - object, - property, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - values.property_set(object, property, value)?; - Ok(EvalControl::None) - } - EvalStmt::StoreVar { name, value } => { - let value = eval_expr(value, context, scope, values)?; - for replaced in set_scope_cell( - context, - scope, - name.clone(), - value, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(EvalControl::None) - } - EvalStmt::Switch { expr, cases } => { - execute_switch_stmt(expr, cases, context, scope, values) - } - EvalStmt::Throw(expr) => { - let thrown = eval_expr(expr, context, scope, values)?; - if values.type_tag(thrown)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - Ok(EvalControl::Throw(thrown)) - } - EvalStmt::Try { - body, - catches, - finally_body, - } => execute_try_stmt(body, catches, finally_body, context, scope, values), - EvalStmt::UnsetVar { name } => { - if let Some(replaced) = unset_scope_cell(scope, name.clone()) { - values.release(replaced)?; - } - Ok(EvalControl::None) - } - EvalStmt::While { condition, body } => { - while { - let condition = eval_expr(condition, context, scope, values)?; - values.truthy(condition)? - } { - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } - Ok(EvalControl::None) - } - EvalStmt::Expr(expr) => { - let _ = eval_expr(expr, context, scope, values)?; - Ok(EvalControl::None) - } - } -} - -/// Executes an eval `try` body and handles supported `catch` clauses. -fn execute_try_stmt( - body: &[EvalStmt], - catches: &[EvalCatch], - finally_body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let control = match execute_statements(body, context, scope, values) { - Ok(EvalControl::Throw(thrown)) => { - execute_matching_catch(thrown, catches, context, scope, values)? - } - Err(EvalStatus::UncaughtThrowable) => { - let Some(thrown) = context.take_pending_throw() else { - return Err(EvalStatus::UncaughtThrowable); - }; - execute_matching_catch(thrown, catches, context, scope, values)? - } - Ok(control) => control, - Err(status) => return Err(status), - }; - if finally_body.is_empty() { - return Ok(control); - } - match execute_statements(finally_body, context, scope, values) { - Ok(EvalControl::None) => Ok(control), - Ok(finally_control) => { - release_overridden_control(control, values)?; - Ok(finally_control) - } - Err(status) => { - release_overridden_control(control, values)?; - Err(status) - } - } -} - -/// Releases a pending control-flow value when `finally` replaces that action. -fn release_overridden_control( - control: EvalControl, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - match control { - EvalControl::Return(value) | EvalControl::Throw(value) => values.release(value), - EvalControl::None | EvalControl::Break | EvalControl::Continue => Ok(()), - } -} - -/// Executes the first supported catch clause for a thrown eval object. -fn execute_matching_catch( - thrown: RuntimeCellHandle, - catches: &[EvalCatch], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut matched = None; - for catch in catches { - if catch_types_match_thrown(thrown, &catch.class_names, values)? { - matched = Some(catch); - break; - } - } - let Some(catch) = matched else { - return Ok(EvalControl::Throw(thrown)); - }; - if let Some(var_name) = &catch.var_name { - for replaced in set_scope_cell( - context, - scope, - var_name.clone(), - thrown, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - } else { - values.release(thrown)?; - } - execute_statements(&catch.body, context, scope, values) -} - -/// Returns true when any type in one catch clause accepts the thrown object. -fn catch_types_match_thrown( - thrown: RuntimeCellHandle, - class_names: &[String], - values: &mut impl RuntimeValueOps, -) -> Result { - for class_name in class_names { - let class_name = class_name.trim_start_matches('\\'); - if class_name.eq_ignore_ascii_case("Throwable") { - return Ok(true); - } - if values.object_is_a(thrown, class_name, false)? { - return Ok(true); - } - } - Ok(false) -} - -/// Registers an eval-declared class in the dynamic class table. -fn execute_class_decl_stmt( - class: &EvalClass, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let name = class.name().trim_start_matches('\\'); - if context.has_class(name) || values.class_exists(name)? { - return Err(EvalStatus::RuntimeFatal); - } - if context.define_class(class.clone()) { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Creates a backing object for an eval-declared class and runs its constructor. -fn eval_dynamic_class_new_object( - class: &EvalClass, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = values.new_object("stdClass")?; - let identity = values.object_identity(object)?; - context.register_dynamic_object(identity, class.name()); - for property in class.properties() { - let value = if let Some(default) = property.default() { - eval_expr(default, context, caller_scope, values)? - } else { - values.null()? - }; - values.property_set(object, property.name(), value)?; - } - if let Some(constructor) = class.method("__construct") { - eval_dynamic_method_with_values( - class.name(), - constructor, - object, - evaluated_args, - context, - values, - )?; - } else if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(object) -} - -/// Dispatches a method call to an eval-declared class method or to the runtime hook. -fn eval_method_call_result( - object: RuntimeCellHandle, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Ok(identity) = values.object_identity(object) else { - return values.method_call(object, method_name, evaluated_args); - }; - let Some(class) = context.dynamic_object_class(identity) else { - return values.method_call(object, method_name, evaluated_args); - }; - let class_name = class.name().to_string(); - let method = class - .method(method_name) - .cloned() - .ok_or(EvalStatus::RuntimeFatal)?; - eval_dynamic_method_with_values( - &class_name, - &method, - object, - evaluated_args, - context, - values, - ) -} - -/// Executes one eval-declared class method with `$this` bound in method scope. -fn eval_dynamic_method_with_values( - class_name: &str, - method: &EvalClassMethod, - object: RuntimeCellHandle, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = - bind_evaluated_function_args(method.params(), positional_args(evaluated_args))?; - let mut method_scope = ElephcEvalScope::new(); - method_scope.set("this", object, ScopeCellOwnership::Borrowed); - for (name, value) in method.params().iter().zip(evaluated_args) { - method_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); - } - let qualified_method_name = - format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); - let static_names = static_var_names(method.body()); - context.push_function(qualified_method_name.clone()); - let result = execute_statements(method.body(), context, &mut method_scope, values); - let persist_result = persist_static_locals( - context, - &qualified_method_name, - &static_names, - &method_scope, - values, - ); - context.pop_function(); - persist_result?; - match result? { - EvalControl::None => values.null(), - EvalControl::Return(result) => Ok(result), - EvalControl::Throw(result) => { - context.set_pending_throw(result); - Err(EvalStatus::UncaughtThrowable) - } - EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Wraps positional method arguments into the shared dynamic-call binding shape. -fn positional_args(args: Vec) -> Vec { - args.into_iter() - .map(|value| EvaluatedCallArg { name: None, value }) - .collect() -} - -/// Executes a PHP `static $name = expr;` declaration in the current eval scope. -fn execute_static_var_stmt( - name: &str, - init: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(function_name) = context.current_function().map(str::to_string) else { - let value = eval_expr(init, context, scope, values)?; - if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Owned) { - values.release(replaced)?; - } - return Ok(()); - }; - if scope.contains_visible(name) { - return Ok(()); - } - let value = if let Some(value) = context.static_local(&function_name, name) { - value - } else { - let value = eval_expr(init, context, scope, values)?; - let _ = context.set_static_local(function_name.clone(), name.to_string(), value); - value - }; - if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Borrowed) { - values.release(replaced)?; - } - Ok(()) -} - -/// Executes a PHP switch with loose case matching, default fallback, and fallthrough. -fn execute_switch_stmt( - expr: &EvalExpr, - cases: &[EvalSwitchCase], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let subject = eval_expr(expr, context, scope, values)?; - let mut default_index = None; - let mut matched_index = None; - for (index, case) in cases.iter().enumerate() { - let Some(condition) = &case.condition else { - if default_index.is_none() { - default_index = Some(index); - } - continue; - }; - let condition = eval_expr(condition, context, scope, values)?; - let matches = values.compare(EvalBinOp::LooseEq, subject, condition)?; - if values.truthy(matches)? { - matched_index = Some(index); - break; - } - } - let Some(start_index) = matched_index.or(default_index) else { - return Ok(EvalControl::None); - }; - for case in &cases[start_index..] { - match execute_statements(&case.body, context, scope, values)? { - EvalControl::None => {} - EvalControl::Break | EvalControl::Continue => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } - Ok(EvalControl::None) -} - -/// Executes a PHP `do/while` loop, evaluating the condition after every body run. -fn execute_do_while_stmt( - body: &[EvalStmt], - condition: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - loop { - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - let condition = eval_expr(condition, context, scope, values)?; - if !values.truthy(condition)? { - break; - } - } - Ok(EvalControl::None) -} - -/// Executes a PHP `for` loop while preserving update-on-continue semantics. -fn execute_for_stmt( - init: &[EvalStmt], - condition: Option<&EvalExpr>, - update: &[EvalStmt], - body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match execute_statements(init, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => return Ok(EvalControl::None), - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - loop { - if let Some(condition) = condition { - let condition = eval_expr(condition, context, scope, values)?; - if !values.truthy(condition)? { - break; - } - } - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - match execute_statements(update, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } - Ok(EvalControl::None) -} - -/// Executes a PHP `foreach` loop over eval array values. -fn execute_foreach_stmt( - array: &EvalExpr, - key_name: Option<&str>, - value_name: &str, - body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let array = eval_expr(array, context, scope, values)?; - let len = values.array_len(array)?; - for index in 0..len { - let key = values.array_iter_key(array, index)?; - let value = values.array_get(array, key)?; - if let Some(key_name) = key_name { - for replaced in set_scope_cell( - context, - scope, - key_name.to_string(), - key, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - } else { - values.release(key)?; - } - for replaced in set_scope_cell( - context, - scope, - value_name.to_string(), - value, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } - Ok(EvalControl::None) -} - -/// Returns PHP's next automatic integer key for `$array[]` append writes. -fn eval_array_append_key( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut next_key = None; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - continue; - } - let one = values.int(1)?; - let candidate = values.add(key, one)?; - let replace = if let Some(current) = next_key { - let is_greater = values.compare(EvalBinOp::Gt, candidate, current)?; - values.truthy(is_greater)? - } else { - true - }; - if replace { - next_key = Some(candidate); - } - } - next_key.map_or_else(|| values.int(0), Ok) -} - -/// Evaluates one expression to an opaque runtime-cell handle. -fn eval_expr( - expr: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match expr { - EvalExpr::Array(elements) => { - if elements - .iter() - .any(|element| matches!(element, EvalArrayElement::KeyValue { .. })) - { - eval_assoc_array(elements, context, scope, values) - } else { - eval_indexed_array(elements, context, scope, values) - } - } - EvalExpr::ArrayGet { array, index } => { - let array = eval_expr(array, context, scope, values)?; - let index = eval_expr(index, context, scope, values)?; - values.array_get(array, index) - } - EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), - EvalExpr::Const(value) => eval_const(value, values), - EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), - EvalExpr::DynamicCall { callee, args } => { - eval_dynamic_call(callee, args, context, scope, values) - } - EvalExpr::Include { - path, - required, - once, - } => eval_include_expr(path, *required, *once, context, scope, values), - EvalExpr::LoadVar(name) => { - visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) - } - EvalExpr::Magic(magic) => eval_magic_const(magic, context, values), - EvalExpr::Match { - subject, - arms, - default, - } => eval_match_expr(subject, arms, default.as_deref(), context, scope, values), - EvalExpr::NamespacedCall { - name, - fallback_name, - args, - } => eval_namespaced_call(name, fallback_name, args, context, scope, values), - EvalExpr::NamespacedConstFetch { - name, - fallback_name, - } => eval_namespaced_const_fetch(name, fallback_name, context, values), - EvalExpr::NewObject { class_name, args } => { - let args = eval_method_call_arg_values(args, context, scope, values)?; - if let Some(class) = context.class(class_name).cloned() { - eval_dynamic_class_new_object(&class, args, context, scope, values) - } else { - values - .new_object(class_name) - .and_then(|object| values.construct_object(object, args).map(|()| object)) - } - } - EvalExpr::MethodCall { - object, - method, - args, - } => { - let object = eval_expr(object, context, scope, values)?; - let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; - eval_method_call_result(object, method, evaluated_args, context, values) - } - EvalExpr::NullCoalesce { value, default } => { - let value = eval_expr(value, context, scope, values)?; - if values.is_null(value)? { - eval_expr(default, context, scope, values) - } else { - Ok(value) - } - } - EvalExpr::PropertyGet { object, property } => { - let object = eval_expr(object, context, scope, values)?; - values.property_get(object, property) - } - EvalExpr::Print(inner) => { - let value = eval_expr(inner, context, scope, values)?; - values.echo(value)?; - values.int(1) - } - EvalExpr::Ternary { - condition, - then_branch, - else_branch, - } => { - let condition = eval_expr(condition, context, scope, values)?; - if values.truthy(condition)? { - if let Some(then_branch) = then_branch { - eval_expr(then_branch, context, scope, values) - } else { - Ok(condition) - } - } else { - eval_expr(else_branch, context, scope, values) - } - } - EvalExpr::Unary { op, expr } => { - let value = eval_expr(expr, context, scope, values)?; - match op { - EvalUnaryOp::Plus => { - let zero = values.int(0)?; - values.add(zero, value) - } - EvalUnaryOp::Negate => { - let zero = values.int(0)?; - values.sub(zero, value) - } - EvalUnaryOp::LogicalNot => { - let truthy = values.truthy(value)?; - values.bool_value(!truthy) - } - EvalUnaryOp::BitNot => values.bit_not(value), - } - } - EvalExpr::Binary { op, left, right } => { - if *op == EvalBinOp::LogicalAnd { - let left = eval_expr(left, context, scope, values)?; - if !values.truthy(left)? { - return values.bool_value(false); - } - let right = eval_expr(right, context, scope, values)?; - let truthy = values.truthy(right)?; - return values.bool_value(truthy); - } - if *op == EvalBinOp::LogicalOr { - let left = eval_expr(left, context, scope, values)?; - if values.truthy(left)? { - return values.bool_value(true); - } - let right = eval_expr(right, context, scope, values)?; - let truthy = values.truthy(right)?; - return values.bool_value(truthy); - } - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - match op { - EvalBinOp::Add => values.add(left, right), - EvalBinOp::Sub => values.sub(left, right), - EvalBinOp::Mul => values.mul(left, right), - EvalBinOp::Div => values.div(left, right), - EvalBinOp::Mod => values.modulo(left, right), - EvalBinOp::Pow => values.pow(left, right), - EvalBinOp::BitAnd - | EvalBinOp::BitOr - | EvalBinOp::BitXor - | EvalBinOp::ShiftLeft - | EvalBinOp::ShiftRight => values.bitwise(*op, left, right), - EvalBinOp::Concat => values.concat(left, right), - EvalBinOp::LogicalXor => { - let left_truthy = values.truthy(left)?; - let right_truthy = values.truthy(right)?; - values.bool_value(left_truthy ^ right_truthy) - } - EvalBinOp::LooseEq - | EvalBinOp::LooseNotEq - | EvalBinOp::StrictEq - | EvalBinOp::StrictNotEq - | EvalBinOp::Lt - | EvalBinOp::LtEq - | EvalBinOp::Gt - | EvalBinOp::GtEq => values.compare(*op, left, right), - EvalBinOp::Spaceship => values.spaceship(left, right), - EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => { - Err(EvalStatus::UnsupportedConstruct) - } - } - } - } -} - -/// Evaluates a PHP `match` expression with strict comparison and lazy arm values. -fn eval_match_expr( - subject: &EvalExpr, - arms: &[EvalMatchArm], - default: Option<&EvalExpr>, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let subject = eval_expr(subject, context, scope, values)?; - for arm in arms { - for pattern in &arm.patterns { - let pattern = eval_expr(pattern, context, scope, values)?; - let matched = values.compare(EvalBinOp::StrictEq, subject, pattern)?; - if values.truthy(matched)? { - return eval_expr(&arm.value, context, scope, values); - } - } - } - default - .map(|expr| eval_expr(expr, context, scope, values)) - .unwrap_or(Err(EvalStatus::RuntimeFatal)) -} - -/// Returns cloned positional argument expressions, rejecting named arguments. -fn positional_call_arg_exprs(args: &[EvalCallArg]) -> Result, EvalStatus> { - if args - .iter() - .any(|arg| arg.name().is_some() || arg.is_spread()) - { - return Err(EvalStatus::RuntimeFatal); - } - Ok(args.iter().map(|arg| arg.value().clone()).collect()) -} - -/// Evaluates a positional-only call argument list in source order. -fn eval_positional_call_arg_values( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if args - .iter() - .any(|arg| arg.name().is_some() || arg.is_spread()) - { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg.value(), context, scope, values)?); - } - Ok(evaluated_args) -} - -/// Evaluates method-call arguments, allowing numeric spread but not named args. -fn eval_method_call_arg_values( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - if evaluated_args.iter().any(|arg| arg.name.is_some()) { - return Err(EvalStatus::RuntimeFatal); - } - Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()) -} - -/// Evaluates supported function-like calls from a runtime eval fragment. -fn eval_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_expr_language_construct_name(name) { - let args = positional_call_arg_exprs(args)?; - return eval_positional_expr_call(name, &args, context, scope, values); - } - if matches!( - name, - "array_pop" - | "array_push" - | "array_shift" - | "array_splice" - | "array_unshift" - | "arsort" - | "asort" - | "krsort" - | "ksort" - | "natcasesort" - | "natsort" - | "rsort" - | "shuffle" - | "sort" - | "settype" - | "uasort" - | "uksort" - | "usort" - ) { - return eval_builtin_array_pop_shift_call(name, args, context, scope, values); - } - if eval_php_visible_builtin_exists(name) { - if eval_call_args_are_plain_positional(args) { - let args = positional_call_arg_exprs(args)?; - return eval_positional_expr_call(name, &args, context, scope, values); - } - return eval_builtin_call(name, args, context, scope, values); - } - - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function(&function, args, context, scope, values); - } - if let Some(function) = context.native_function(name) { - return eval_native_function(function, args, context, scope, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Evaluates an unqualified namespaced function call with PHP's global fallback. -fn eval_namespaced_call( - name: &str, - fallback_name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function(&function, args, context, scope, values); - } - if let Some(function) = context.native_function(name) { - return eval_native_function(function, args, context, scope, values); - } - eval_call(fallback_name, args, context, scope, values) -} - -/// Evaluates a variable or expression callable and dispatches it with source-order arguments. -fn eval_dynamic_call( - callee: &EvalExpr, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_expr(callee, context, scope, values)?; - let callback = eval_callable(callback, values)?; - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) -} - -/// Returns true for language constructs that need unevaluated argument expressions. -fn eval_expr_language_construct_name(name: &str) -> bool { - matches!(name, "empty" | "eval" | "isset") -} - -/// Returns true when every source argument is plain positional. -fn eval_call_args_are_plain_positional(args: &[EvalCallArg]) -> bool { - args.iter() - .all(|arg| arg.name().is_none() && !arg.is_spread()) -} - -/// Evaluates builtins and language constructs after positional-only argument validation. -fn eval_positional_expr_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "abs" => eval_builtin_abs(args, context, scope, values), - "addslashes" | "stripslashes" => eval_builtin_slashes(name, args, context, scope, values), - "array_combine" => eval_builtin_array_combine(args, context, scope, values), - "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), - "array_column" => eval_builtin_array_column(args, context, scope, values), - "array_fill" => eval_builtin_array_fill(args, context, scope, values), - "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), - "array_filter" => eval_builtin_array_filter(args, context, scope, values), - "array_flip" => eval_builtin_array_flip(args, context, scope, values), - "array_map" => eval_builtin_array_map(args, context, scope, values), - "array_reduce" => eval_builtin_array_reduce(args, context, scope, values), - "array_walk" => eval_builtin_array_walk(args, context, scope, values), - "array_keys" | "array_values" => { - eval_builtin_array_projection(name, args, context, scope, values) - } - "array_key_exists" => eval_builtin_array_key_exists(args, context, scope, values), - "array_diff" | "array_intersect" => { - eval_builtin_array_value_set(name, args, context, scope, values) - } - "array_diff_key" | "array_intersect_key" => { - eval_builtin_array_key_set(name, args, context, scope, values) - } - "array_merge" => eval_builtin_array_merge(args, context, scope, values), - "array_product" | "array_sum" => { - eval_builtin_array_aggregate(name, args, context, scope, values) - } - "array_pad" => eval_builtin_array_pad(args, context, scope, values), - "array_rand" => eval_builtin_array_rand(args, context, scope, values), - "array_reverse" => eval_builtin_array_reverse(args, context, scope, values), - "array_search" | "in_array" => { - eval_builtin_array_search(name, args, context, scope, values) - } - "array_slice" => eval_builtin_array_slice(args, context, scope, values), - "array_unique" => eval_builtin_array_unique(args, context, scope, values), - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { - eval_builtin_float_unary(name, args, context, scope, values) - } - "atan2" | "hypot" => eval_builtin_float_pair(name, args, context, scope, values), - "base64_encode" => eval_builtin_base64_encode(args, context, scope, values), - "base64_decode" => eval_builtin_base64_decode(args, context, scope, values), - "basename" => eval_builtin_basename(args, context, scope, values), - "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), - "ceil" => eval_builtin_ceil(args, context, scope, values), - "chdir" | "mkdir" | "rmdir" => { - eval_builtin_unary_path_bool(name, args, context, scope, values) - } - "chmod" => eval_builtin_chmod(args, context, scope, values), - "chr" => eval_builtin_chr(args, context, scope, values), - "clamp" => eval_builtin_clamp(args, context, scope, values), - "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), - "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), - "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), - "class_exists" => eval_builtin_class_exists(args, context, scope, values), - "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), - "trait_exists" | "enum_exists" => { - eval_builtin_class_like_exists(name, args, context, scope, values) - } - "is_a" | "is_subclass_of" => eval_builtin_is_a_relation(name, args, context, scope, values), - "chop" => eval_builtin_trim_like(name, args, context, scope, values), - "boolval" | "floatval" | "intval" | "strval" => { - eval_builtin_cast(name, args, context, scope, values) - } - "count" => eval_builtin_count(args, context, scope, values), - "copy" | "link" | "rename" | "symlink" => { - eval_builtin_binary_path_bool(name, args, context, scope, values) - } - "crc32" => eval_builtin_crc32(args, context, scope, values), - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { - eval_builtin_ctype(name, args, context, scope, values) - } - "date" => eval_builtin_date(args, context, scope, values), - "define" => eval_builtin_define(args, context, scope, values), - "defined" => eval_builtin_defined(args, context, scope, values), - "dirname" => eval_builtin_dirname(args, context, scope, values), - "disk_free_space" | "disk_total_space" => { - eval_builtin_disk_space(name, args, context, scope, values) - } - "empty" => eval_builtin_empty(args, context, scope, values), - "eval" => eval_nested_eval(args, context, scope, values), - "explode" => eval_builtin_explode(args, context, scope, values), - "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), - "file" => eval_builtin_file(args, context, scope, values), - "file_exists" => eval_builtin_file_probe(name, args, context, scope, values), - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), - "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), - "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), - "filesize" => eval_builtin_filesize(args, context, scope, values), - "filetype" => eval_builtin_filetype(args, context, scope, values), - "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), - "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), - "floor" => eval_builtin_floor(args, context, scope, values), - "function_exists" | "is_callable" => { - eval_builtin_function_probe(args, context, scope, values) - } - "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), - "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), - "gethostname" => eval_builtin_gethostname(args, values), - "getprotobyname" => eval_builtin_getprotobyname(args, context, scope, values), - "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), - "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), - "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), - "get_class" => eval_builtin_get_class(args, context, scope, values), - "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), - "get_resource_id" | "get_resource_type" => { - eval_builtin_resource_introspection(name, args, context, scope, values) - } - "getcwd" => eval_builtin_getcwd(args, values), - "getenv" => eval_builtin_getenv(args, context, scope, values), - "gettype" => eval_builtin_gettype(args, context, scope, values), - "glob" => eval_builtin_glob(args, context, scope, values), - "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { - eval_builtin_hash_one_shot(name, args, context, scope, values) - } - "hash_algos" => eval_builtin_hash_algos(args, values), - "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), - "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { - eval_builtin_html_entity(name, args, context, scope, values) - } - "implode" => eval_builtin_implode(args, context, scope, values), - "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), - "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), - "intdiv" => eval_builtin_intdiv(args, context, scope, values), - "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), - "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), - "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), - "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" - | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), - "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" - | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { - eval_builtin_type_predicate(name, args, context, scope, values) - } - "ip2long" => eval_builtin_ip2long(args, context, scope, values), - "json_decode" => eval_builtin_json_decode(args, context, scope, values), - "json_encode" => eval_builtin_json_encode(args, context, scope, values), - "json_last_error" => eval_builtin_json_last_error(args, context, values), - "json_last_error_msg" => eval_builtin_json_last_error_msg(args, context, values), - "json_validate" => eval_builtin_json_validate(args, context, scope, values), - "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), - "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), - "log" => eval_builtin_log(args, context, scope, values), - "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), - "microtime" => eval_builtin_microtime(args, context, scope, values), - "mktime" => eval_builtin_mktime(args, context, scope, values), - "nl2br" => eval_builtin_nl2br(args, context, scope, values), - "number_format" => eval_builtin_number_format(args, context, scope, values), - "ord" => eval_builtin_ord(args, context, scope, values), - "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), - "pi" => eval_builtin_pi(args, values), - "php_uname" => eval_builtin_php_uname(args, context, scope, values), - "phpversion" => eval_builtin_phpversion(args, values), - "pow" => eval_builtin_pow(args, context, scope, values), - "preg_match" => eval_builtin_preg_match(args, context, scope, values), - "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), - "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), - "preg_replace_callback" => eval_builtin_preg_replace_callback(args, context, scope, values), - "preg_split" => eval_builtin_preg_split(args, context, scope, values), - "print_r" => eval_builtin_print_r(args, context, scope, values), - "putenv" => eval_builtin_putenv(args, context, scope, values), - "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), - "random_int" => eval_builtin_random_int(args, context, scope, values), - "range" => eval_builtin_range(args, context, scope, values), - "rawurldecode" | "urldecode" => eval_builtin_url_decode(name, args, context, scope, values), - "rawurlencode" | "urlencode" => eval_builtin_url_encode(name, args, context, scope, values), - "readfile" => eval_builtin_readfile(args, context, scope, values), - "readlink" => eval_builtin_readlink(args, context, scope, values), - "realpath" => eval_builtin_realpath(args, context, scope, values), - "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), - "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "round" => eval_builtin_round(args, context, scope, values), - "scandir" => eval_builtin_scandir(args, context, scope, values), - "isset" => eval_builtin_isset(args, context, scope, values), - "sleep" => eval_builtin_sleep(args, context, scope, values), - "sqrt" => eval_builtin_sqrt(args, context, scope, values), - "spl_classes" => eval_builtin_spl_classes(args, values), - "spl_object_id" | "spl_object_hash" => { - eval_builtin_spl_object_identity(name, args, context, scope, values) - } - "sscanf" => eval_builtin_sscanf(args, context, scope, values), - "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), - "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), - "tempnam" => eval_builtin_tempnam(args, context, scope, values), - "time" => eval_builtin_time(args, values), - "touch" => eval_builtin_touch(args, context, scope, values), - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { - eval_builtin_stream_introspection(name, args, values) - } - "strtotime" => eval_builtin_strtotime(args, context, scope, values), - "unlink" => eval_builtin_unlink(args, context, scope, values), - "strrev" => eval_builtin_strrev(args, context, scope, values), - "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), - "str_replace" | "str_ireplace" => { - eval_builtin_str_replace(name, args, context, scope, values) - } - "str_pad" => eval_builtin_str_pad(args, context, scope, values), - "str_split" => eval_builtin_str_split(args, context, scope, values), - "strstr" => eval_builtin_strstr(args, context, scope, values), - "substr" => eval_builtin_substr(args, context, scope, values), - "substr_replace" => eval_builtin_substr_replace(args, context, scope, values), - "str_contains" | "str_starts_with" | "str_ends_with" => { - eval_builtin_string_search(name, args, context, scope, values) - } - "strcmp" | "strcasecmp" => eval_builtin_string_compare(name, args, context, scope, values), - "strlen" => eval_builtin_strlen(args, context, scope, values), - "strpos" | "strrpos" => eval_builtin_string_position(name, args, context, scope, values), - "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { - eval_builtin_string_case(name, args, context, scope, values) - } - "long2ip" => eval_builtin_long2ip(args, context, scope, values), - "trim" => eval_builtin_trim_like(name, args, context, scope, values), - "ucwords" => eval_builtin_ucwords(args, context, scope, values), - "umask" => eval_builtin_umask(args, context, scope, values), - "usleep" => eval_builtin_usleep(args, context, scope, values), - "var_dump" => eval_builtin_var_dump(args, context, scope, values), - "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), - "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates nested `eval(...)` calls against the current materialized scope. -fn eval_nested_eval( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [code] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let code = eval_expr(code, context, scope, values)?; - let code = values.string_bytes(code)?; - let program = parse_fragment(&code).map_err(EvalParseError::status)?; - execute_program_with_context(context, &program, scope, values) -} - -/// Evaluates an eval-fragment include or require expression. -fn eval_include_expr( - path: &EvalExpr, - required: bool, - once: bool, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_expr(path, context, scope, values)?; - let path = eval_path_string(path, values)?; - let resolved_path = eval_resolve_include_path(&path, context); - let include_key = eval_include_key(&resolved_path); - if once && context.has_included_file(&include_key) { - return values.bool_value(true); - } - let bytes = match std::fs::read(&resolved_path) { - Ok(bytes) => bytes, - Err(_) => return eval_include_missing_file(&path, required, values), - }; - context.mark_included_file(include_key); - eval_execute_include_bytes(&bytes, &resolved_path, context, scope, values) -} - -/// Returns the include/require result for a file that cannot be opened. -fn eval_include_missing_file( - path: &str, - required: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let construct = if required { "require" } else { "include" }; - values.warning(&format!( - "Warning: {construct}({path}): Failed to open stream: No such file or directory\n" - ))?; - values.warning(&format!( - "Warning: {construct}(): Failed opening '{path}' for inclusion\n" - ))?; - if required { - Err(EvalStatus::RuntimeFatal) - } else { - values.bool_value(false) - } -} - -/// Resolves eval include paths using PHP's cwd-first and caller-directory fallback. -fn eval_resolve_include_path(path: &str, context: &ElephcEvalContext) -> std::path::PathBuf { - let raw_path = std::path::Path::new(path); - if raw_path.is_absolute() || raw_path.exists() { - return raw_path.to_path_buf(); - } - if context.call_dir().is_empty() { - return raw_path.to_path_buf(); - } - let caller_path = std::path::Path::new(context.call_dir()).join(raw_path); - if caller_path.exists() { - caller_path - } else { - raw_path.to_path_buf() - } -} - -/// Builds the stable include_once key for a resolved path. -fn eval_include_key(path: &std::path::Path) -> String { - std::fs::canonicalize(path) - .unwrap_or_else(|_| path.to_path_buf()) - .to_string_lossy() - .into_owned() -} - -/// Executes a local include file, alternating raw output and PHP code blocks. -fn eval_execute_include_bytes( - bytes: &[u8], - path: &std::path::Path, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut cursor = 0; - while let Some((tag_start, code_start)) = eval_find_php_open_tag(bytes, cursor) { - eval_echo_include_bytes(&bytes[cursor..tag_start], values)?; - let close = eval_find_php_close_tag(bytes, code_start); - let code_end = close.unwrap_or(bytes.len()); - match eval_execute_include_code(&bytes[code_start..code_end], path, context, scope, values)? - { - EvalControl::None => {} - EvalControl::Return(value) => return Ok(value), - EvalControl::Throw(value) => { - context.set_pending_throw(value); - return Err(EvalStatus::UncaughtThrowable); - } - EvalControl::Break | EvalControl::Continue => { - return Err(EvalStatus::UnsupportedConstruct); - } - } - let Some(close) = close else { - return values.int(1); - }; - cursor = close + 2; - } - eval_echo_include_bytes(&bytes[cursor..], values)?; - values.int(1) -} - -/// Parses and executes one PHP code block from an included file. -fn eval_execute_include_code( - code: &[u8], - path: &std::path::Path, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let program = parse_fragment(code).map_err(EvalParseError::status)?; - let previous = context.call_site(); - let file = path.to_string_lossy().into_owned(); - let dir = path - .parent() - .map(|parent| parent.to_string_lossy().into_owned()) - .unwrap_or_default(); - context.set_call_site(file.clone(), dir, 1); - context.set_file_magic_override(Some(file)); - let result = execute_statements(program.statements(), context, scope, values); - context.set_call_site(previous.0, previous.1, previous.2); - context.set_file_magic_override(previous.3); - result -} - -/// Echoes raw non-PHP include bytes through the eval value hooks. -fn eval_echo_include_bytes( - bytes: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if bytes.is_empty() { - return Ok(()); - } - let output = values.string_bytes_value(bytes)?; - values.echo(output) -} - -/// Finds the next ` Option<(usize, usize)> { - bytes - .get(start..)? - .windows(5) - .position(eval_is_php_open_tag) - .map(|offset| { - let tag_start = start + offset; - (tag_start, tag_start + 5) - }) -} - -/// Returns true when a five-byte window is a case-insensitive ` bool { - window.len() == 5 - && window[0] == b'<' - && window[1] == b'?' - && window[2].eq_ignore_ascii_case(&b'p') - && window[3].eq_ignore_ascii_case(&b'h') - && window[4].eq_ignore_ascii_case(&b'p') -} - -/// Finds the next PHP closing tag after a code block start. -fn eval_find_php_close_tag(bytes: &[u8], start: usize) -> Option { - bytes - .get(start..)? - .windows(2) - .position(|window| window == b"?>") - .map(|offset| start + offset) -} - -/// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. -fn eval_builtin_strlen( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let bytes = values.string_bytes(value)?; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len) -} - -/// Evaluates the builtin `ord(...)` for the first byte of one coerced string. -fn eval_builtin_ord( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_ord_result(value, values) -} - -/// Returns the first byte of one converted string, or zero for an empty string. -fn eval_ord_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.int(i64::from(bytes.first().copied().unwrap_or(0))) -} - -/// Evaluates the builtin `count(...)` for one runtime array-like argument. -fn eval_builtin_count( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_count_result(value, None, values) - } - [value, mode] => { - let value = eval_expr(value, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_count_result(value, Some(mode), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Counts an eval array with PHP normal or recursive mode semantics. -fn eval_count_result( - value: RuntimeCellHandle, - mode: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = match mode { - Some(mode) => eval_int_value(mode, values)?, - None => EVAL_COUNT_NORMAL, - }; - let len = match mode { - EVAL_COUNT_NORMAL => values.array_len(value)?, - EVAL_COUNT_RECURSIVE => eval_count_recursive_len(value, values, &mut Vec::new())?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len) -} - -/// Recursively counts nested eval arrays for `count($value, COUNT_RECURSIVE)`. -fn eval_count_recursive_len( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - arrays_seen: &mut Vec, -) -> Result { - let address = value.as_ptr() as usize; - if arrays_seen.contains(&address) { - return Ok(0); - } - arrays_seen.push(address); - - let len = values.array_len(value)?; - let mut total = len; - for position in 0..len { - let key = values.array_iter_key(value, position)?; - let element = values.array_get(value, key)?; - if values.is_array_like(element)? { - total = total - .checked_add(eval_count_recursive_len(element, values, arrays_seen)?) - .ok_or(EvalStatus::RuntimeFatal)?; - } - } - - arrays_seen.pop(); - Ok(total) -} - -/// Evaluates PHP `json_encode()` for zero-flag scalar and array values. -fn eval_builtin_json_encode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_json_encode_result(value, None, None, context, values) - } - [value, flags] => { - let value = eval_expr(value, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_json_encode_result(value, Some(flags), None, context, values) - } - [value, flags, depth] => { - let value = eval_expr(value, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - eval_json_encode_result(value, Some(flags), Some(depth), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Encodes one runtime cell as a JSON string for eval's supported flag subset. -fn eval_json_encode_result( - value: RuntimeCellHandle, - flags: Option, - depth: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let flags = flags - .map(|flags| eval_int_value(flags, values)) - .transpose()? - .unwrap_or(0); - let supported_flags = EVAL_JSON_HEX_TAG - | EVAL_JSON_HEX_AMP - | EVAL_JSON_HEX_APOS - | EVAL_JSON_HEX_QUOT - | EVAL_JSON_UNESCAPED_SLASHES - | EVAL_JSON_UNESCAPED_UNICODE - | EVAL_JSON_FORCE_OBJECT - | EVAL_JSON_PRETTY_PRINT - | EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR - | EVAL_JSON_PRESERVE_ZERO_FRACTION - | EVAL_JSON_INVALID_UTF8_IGNORE - | EVAL_JSON_INVALID_UTF8_SUBSTITUTE - | EVAL_JSON_THROW_ON_ERROR; - let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; - if flags & !supported_flags != 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let depth = depth - .map(|depth| eval_int_value(depth, values)) - .transpose()? - .unwrap_or(512); - if depth <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - - let mut output = Vec::new(); - let mut error = None; - eval_json_encode_append( - value, - values, - flags, - depth as usize, - 0, - &mut Vec::new(), - &mut error, - &mut output, - )?; - if let Some(error) = error { - context.set_json_error(error.code, error.message); - if flags & EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR == 0 { - if flags & EVAL_JSON_THROW_ON_ERROR != 0 { - return eval_throw_json_exception(error.code, error.message, context, values); - } - return values.bool_value(false); - } - } else { - context.clear_json_error(); - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP `json_decode()` for eval-supported JSON text and flags. -fn eval_builtin_json_decode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [json] => { - let json = eval_expr(json, context, scope, values)?; - eval_json_decode_result(json, None, None, None, context, values) - } - [json, associative] => { - let json = eval_expr(json, context, scope, values)?; - let associative = eval_expr(associative, context, scope, values)?; - eval_json_decode_result(json, Some(associative), None, None, context, values) - } - [json, associative, depth] => { - let json = eval_expr(json, context, scope, values)?; - let associative = eval_expr(associative, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - eval_json_decode_result(json, Some(associative), Some(depth), None, context, values) - } - [json, associative, depth, flags] => { - let json = eval_expr(json, context, scope, values)?; - let associative = eval_expr(associative, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_json_decode_result( - json, - Some(associative), - Some(depth), - Some(flags), - context, - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Decodes one JSON string into eval runtime cells and records PHP JSON parse state. -fn eval_json_decode_result( - json: RuntimeCellHandle, - associative: Option, - depth: Option, - flags: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let flags = flags - .map(|flags| eval_int_value(flags, values)) - .transpose()? - .unwrap_or(0); - let supported_flags = EVAL_JSON_BIGINT_AS_STRING - | EVAL_JSON_INVALID_UTF8_IGNORE - | EVAL_JSON_INVALID_UTF8_SUBSTITUTE - | EVAL_JSON_THROW_ON_ERROR; - if flags & !supported_flags != 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let objects_as_assoc = associative - .map(|associative| values.truthy(associative)) - .transpose()? - .unwrap_or(false); - let depth = depth - .map(|depth| eval_int_value(depth, values)) - .transpose()? - .unwrap_or(512); - if depth <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - - let bytes = values.string_bytes(json)?; - let decoded_result = if flags & EVAL_JSON_INVALID_UTF8_SUBSTITUTE != 0 { - json_validate::decode_result_substituting_invalid_utf8(&bytes, depth as usize) - } else if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { - json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) - } else { - json_validate::decode_result(&bytes, depth as usize) - }; - let decoded = match decoded_result { - Ok(decoded) => decoded, - Err(error) => { - let (code, message) = eval_json_parse_error_details(error, &bytes); - if flags & EVAL_JSON_THROW_ON_ERROR != 0 { - return eval_throw_json_exception(code, &message, context, values); - } - context.set_json_error(code, message); - return values.null(); - } - }; - context.clear_json_error(); - eval_json_decode_to_cell(decoded, flags, objects_as_assoc, values) -} - -/// Materializes one parsed JSON value as an eval runtime cell. -fn eval_json_decode_to_cell( - value: JsonValue, - flags: i64, - objects_as_assoc: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - match value { - JsonValue::Null => values.null(), - JsonValue::Bool(value) => values.bool_value(value), - JsonValue::Number(value) => eval_json_decode_number_to_cell(&value, flags, values), - JsonValue::String(value) => values.string_bytes_value(&value), - JsonValue::Array(elements) => { - let mut result = values.array_new(elements.len())?; - for (index, element) in elements.into_iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let element = eval_json_decode_to_cell(element, flags, objects_as_assoc, values)?; - result = values.array_set(result, key, element)?; - } - Ok(result) - } - JsonValue::Object(entries) => { - if !objects_as_assoc { - return eval_json_decode_object_to_cell(entries, flags, values); - } - let mut result = values.assoc_new(entries.len())?; - for (key, value) in entries { - let key = values.string_bytes_value(&key)?; - let value = eval_json_decode_to_cell(value, flags, objects_as_assoc, values)?; - result = values.array_set(result, key, value)?; - } - Ok(result) - } - } -} - -/// Materializes a parsed JSON object as a `stdClass` runtime object. -fn eval_json_decode_object_to_cell( - entries: Vec<(Vec, JsonValue)>, - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = values.new_object("stdClass")?; - for (key, value) in entries { - let key = std::str::from_utf8(&key).map_err(|_| EvalStatus::RuntimeFatal)?; - let value = eval_json_decode_to_cell(value, flags, false, values)?; - values.property_set(object, key, value)?; - } - Ok(object) -} - -/// Materializes one JSON number as an int when possible and as a float otherwise. -fn eval_json_decode_number_to_cell( - value: &[u8], - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - if flags & EVAL_JSON_BIGINT_AS_STRING != 0 && eval_json_number_overflows_i64(value) { - return values.string_bytes_value(value); - } - let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; - if !value.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E')) { - if let Ok(integer) = value.parse::() { - return values.int(integer); - } - } - let float = value.parse::().map_err(|_| EvalStatus::RuntimeFatal)?; - values.float(float) -} - -/// Returns true when one integer-grammar JSON number exceeds PHP's int range. -fn eval_json_number_overflows_i64(value: &[u8]) -> bool { - if value.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) { - return false; - } - let (negative, digits) = if let Some(digits) = value.strip_prefix(b"-") { - (true, digits) - } else { - (false, value) - }; - let threshold = if negative { - b"9223372036854775808".as_slice() - } else { - b"9223372036854775807".as_slice() - }; - digits.len() > threshold.len() || digits.len() == threshold.len() && digits > threshold -} - -/// Evaluates PHP `json_last_error()` from the eval interpreter's current JSON state. -fn eval_builtin_json_last_error( - args: &[EvalExpr], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.int(context.json_last_error()) -} - -/// Evaluates PHP `json_last_error_msg()` from the eval interpreter's current JSON state. -fn eval_builtin_json_last_error_msg( - args: &[EvalExpr], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.string(context.json_last_error_msg()) -} - -/// Evaluates PHP `json_validate()` for zero-flag JSON text validation. -fn eval_builtin_json_validate( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [json] => { - let json = eval_expr(json, context, scope, values)?; - eval_json_validate_result(json, None, None, context, values) - } - [json, depth] => { - let json = eval_expr(json, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - eval_json_validate_result(json, Some(depth), None, context, values) - } - [json, depth, flags] => { - let json = eval_expr(json, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_json_validate_result(json, Some(depth), Some(flags), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Validates JSON text with eval's current zero-flag JSON subset and records JSON state. -fn eval_json_validate_result( - json: RuntimeCellHandle, - depth: Option, - flags: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let flags = flags - .map(|flags| eval_int_value(flags, values)) - .transpose()? - .unwrap_or(0); - if flags & !EVAL_JSON_INVALID_UTF8_IGNORE != 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let depth = depth - .map(|depth| eval_int_value(depth, values)) - .transpose()? - .unwrap_or(512); - if depth <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - - let bytes = values.string_bytes(json)?; - let result = if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { - json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) - } else { - json_validate::decode_result(&bytes, depth as usize) - }; - match result { - Ok(_) => { - context.clear_json_error(); - values.bool_value(true) - } - Err(error) => { - eval_record_json_parse_error(context, error, &bytes); - values.bool_value(false) - } - } -} - -/// Records one parser error into the eval-local PHP JSON error slots. -fn eval_record_json_parse_error( - context: &mut ElephcEvalContext, - error: JsonParseError, - bytes: &[u8], -) { - let (code, message) = eval_json_parse_error_details(error, bytes); - context.set_json_error(code, message); -} - -/// Builds the PHP JSON error code and message for one parser failure. -fn eval_json_parse_error_details(error: JsonParseError, bytes: &[u8]) -> (i64, String) { - let (code, message) = eval_json_parse_error_status(error.kind()); - let message = eval_json_error_message_with_location(message, bytes, error.offset()); - (code, message) -} - -/// Creates and schedules a `JsonException` through eval's normal Throwable channel. -fn eval_throw_json_exception( - code: i64, - message: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - context.set_json_error(code, message.to_string()); - let exception = values.new_object("JsonException")?; - let message = values.string(message)?; - let code = values.int(code)?; - values.construct_object(exception, vec![message, code])?; - context.set_pending_throw(exception); - Err(EvalStatus::UncaughtThrowable) -} - -/// Maps eval JSON parser failures to PHP `JSON_ERROR_*` codes and messages. -fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str) { - match error { - JsonParseErrorKind::Depth => (EVAL_JSON_ERROR_DEPTH, "Maximum stack depth exceeded"), - JsonParseErrorKind::Syntax => (EVAL_JSON_ERROR_SYNTAX, "Syntax error"), - JsonParseErrorKind::ControlChar => ( - EVAL_JSON_ERROR_CTRL_CHAR, - "Control character error, possibly incorrectly encoded", - ), - JsonParseErrorKind::Utf8 => (EVAL_JSON_ERROR_UTF8, EVAL_JSON_UTF8_MESSAGE), - JsonParseErrorKind::Utf16 => ( - EVAL_JSON_ERROR_UTF16, - "Single unpaired UTF-16 surrogate in unicode escape", - ), - } -} - -/// Adds PHP's JSON line/column suffix to one base error message. -fn eval_json_error_message_with_location(message: &str, bytes: &[u8], offset: usize) -> String { - let (line, column) = eval_json_error_location(bytes, offset); - format!("{message} near location {line}:{column}") -} - -/// Converts a zero-based JSON byte offset into PHP-style one-based line and column. -fn eval_json_error_location(bytes: &[u8], offset: usize) -> (usize, usize) { - let mut line = 1; - let mut column = 1; - let offset = offset.min(bytes.len()); - for byte in &bytes[..offset] { - if *byte == b'\n' { - line += 1; - column = 1; - } else { - column += 1; - } - } - (line, column) -} - -/// Appends one JSON value to the output buffer. -fn eval_json_encode_append( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - match values.type_tag(value)? { - EVAL_TAG_INT => output.extend_from_slice(&values.string_bytes(value)?), - EVAL_TAG_FLOAT => { - eval_json_encode_append_float(value, values, flags, error, output)?; - } - EVAL_TAG_STRING => eval_json_encode_append_string( - &values.string_bytes(value)?, - flags, - EvalJsonStringPosition::Value, - error, - output, - )?, - EVAL_TAG_BOOL => { - if values.truthy(value)? { - output.extend_from_slice(b"true"); - } else { - output.extend_from_slice(b"false"); - } - } - EVAL_TAG_ARRAY => { - eval_json_encode_append_indexed_array( - value, - values, - flags, - depth_limit, - depth, - arrays_seen, - error, - output, - )?; - } - EVAL_TAG_ASSOC => { - eval_json_encode_append_assoc( - value, - values, - flags, - depth_limit, - depth, - arrays_seen, - error, - output, - )?; - } - EVAL_TAG_OBJECT => { - eval_json_encode_append_object( - value, - values, - flags, - depth_limit, - depth, - arrays_seen, - error, - output, - )?; - } - EVAL_TAG_NULL | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), - _ => return Err(EvalStatus::UnsupportedConstruct), - } - Ok(()) -} - -#[derive(Clone, Copy)] -struct EvalJsonEncodeError { - code: i64, - message: &'static str, -} - -/// Marks whether a JSON string is being encoded as a value or as an object key. -#[derive(Clone, Copy)] -enum EvalJsonStringPosition { - Value, - Key, -} - -/// Appends one JSON float while preserving a `.0` suffix when requested. -fn eval_json_encode_append_float( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - let float = eval_float_value(value, values)?; - if !float.is_finite() { - *error = Some(EvalJsonEncodeError { - code: EVAL_JSON_ERROR_INF_OR_NAN, - message: EVAL_JSON_INF_OR_NAN_MESSAGE, - }); - output.push(b'0'); - return Ok(()); - } - let bytes = values.string_bytes(value)?; - output.extend_from_slice(&bytes); - if flags & EVAL_JSON_PRESERVE_ZERO_FRACTION != 0 - && !bytes.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) - { - output.extend_from_slice(b".0"); - } - Ok(()) -} - -/// Appends one indexed eval array as a JSON array or forced JSON object. -fn eval_json_encode_append_indexed_array( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; - let force_object = flags & EVAL_JSON_FORCE_OBJECT != 0; - let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; - output.push(if force_object { b'{' } else { b'[' }); - let len = values.array_len(value)?; - if pretty && len > 0 { - output.push(b'\n'); - } - for position in 0..len { - if position > 0 { - output.push(b','); - if pretty { - output.push(b'\n'); - } - } - if pretty { - eval_json_encode_pretty_indent(output, depth + 1); - } - let key = values.array_iter_key(value, position)?; - if force_object { - eval_json_encode_append_string( - &values.string_bytes(key)?, - flags & !EVAL_JSON_NUMERIC_CHECK, - EvalJsonStringPosition::Key, - error, - output, - )?; - eval_json_encode_append_colon(flags, output); - } - let element = values.array_get(value, key)?; - eval_json_encode_append( - element, - values, - flags, - depth_limit, - depth + 1, - arrays_seen, - error, - output, - )?; - } - if pretty && len > 0 { - output.push(b'\n'); - eval_json_encode_pretty_indent(output, depth); - } - output.push(if force_object { b'}' } else { b']' }); - arrays_seen.pop(); - Ok(()) -} - -/// Appends one associative eval array as a JSON object. -fn eval_json_encode_append_assoc( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; - let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; - output.push(b'{'); - let len = values.array_len(value)?; - if pretty && len > 0 { - output.push(b'\n'); - } - for position in 0..len { - if position > 0 { - output.push(b','); - if pretty { - output.push(b'\n'); - } - } - if pretty { - eval_json_encode_pretty_indent(output, depth + 1); - } - let key = values.array_iter_key(value, position)?; - eval_json_encode_append_string( - &values.string_bytes(key)?, - flags & !EVAL_JSON_NUMERIC_CHECK, - EvalJsonStringPosition::Key, - error, - output, - )?; - eval_json_encode_append_colon(flags, output); - let element = values.array_get(value, key)?; - eval_json_encode_append( - element, - values, - flags, - depth_limit, - depth + 1, - arrays_seen, - error, - output, - )?; - } - if pretty && len > 0 { - output.push(b'\n'); - eval_json_encode_pretty_indent(output, depth); - } - output.push(b'}'); - arrays_seen.pop(); - Ok(()) -} - -/// Appends one eval runtime object as a JSON object. -fn eval_json_encode_append_object( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; - let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; - output.push(b'{'); - let len = values.object_property_len(value)?; - if pretty && len > 0 { - output.push(b'\n'); - } - for position in 0..len { - if position > 0 { - output.push(b','); - if pretty { - output.push(b'\n'); - } - } - if pretty { - eval_json_encode_pretty_indent(output, depth + 1); - } - let key = values.object_property_iter_key(value, position)?; - let key_bytes = values.string_bytes(key)?; - eval_json_encode_append_string( - &key_bytes, - flags & !EVAL_JSON_NUMERIC_CHECK, - EvalJsonStringPosition::Key, - error, - output, - )?; - eval_json_encode_append_colon(flags, output); - let property = std::str::from_utf8(&key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - let element = values.property_get(value, property)?; - eval_json_encode_append( - element, - values, - flags, - depth_limit, - depth + 1, - arrays_seen, - error, - output, - )?; - } - if pretty && len > 0 { - output.push(b'\n'); - eval_json_encode_pretty_indent(output, depth); - } - output.push(b'}'); - arrays_seen.pop(); - Ok(()) -} - -/// Appends a JSON object colon, including pretty-print spacing when active. -fn eval_json_encode_append_colon(flags: i64, output: &mut Vec) { - if flags & EVAL_JSON_PRETTY_PRINT != 0 { - output.extend_from_slice(b": "); - } else { - output.push(b':'); - } -} - -/// Appends PHP's four-space JSON pretty-print indentation for one nesting level. -fn eval_json_encode_pretty_indent(output: &mut Vec, depth: usize) { - for _ in 0..depth { - output.extend_from_slice(b" "); - } -} - -/// Records entry into one JSON array/object, rejecting depth overrun and recursion. -fn eval_json_encode_enter_array( - value: RuntimeCellHandle, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, -) -> Result<(), EvalStatus> { - if depth >= depth_limit { - return Err(EvalStatus::RuntimeFatal); - } - let address = value.as_ptr() as usize; - if arrays_seen.contains(&address) { - return Err(EvalStatus::RuntimeFatal); - } - arrays_seen.push(address); - Ok(()) -} - -/// Appends one JSON string with eval-supported PHP flag handling. -fn eval_json_encode_append_string( - bytes: &[u8], - flags: i64, - position: EvalJsonStringPosition, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - if flags & EVAL_JSON_NUMERIC_CHECK != 0 { - if let Some(number) = eval_json_numeric_check_bytes(bytes) { - output.extend_from_slice(&number); - return Ok(()); - } - } - let start_len = output.len(); - output.push(b'"'); - if let Ok(value) = std::str::from_utf8(bytes) { - for character in value.chars() { - eval_json_encode_append_char(character, flags, output); - } - } else if flags & (EVAL_JSON_INVALID_UTF8_IGNORE | EVAL_JSON_INVALID_UTF8_SUBSTITUTE) == 0 { - output.truncate(start_len); - *error = Some(EvalJsonEncodeError { - code: EVAL_JSON_ERROR_UTF8, - message: EVAL_JSON_UTF8_MESSAGE, - }); - match position { - EvalJsonStringPosition::Value => output.extend_from_slice(b"null"), - EvalJsonStringPosition::Key => output.extend_from_slice(b"\"\""), - } - return Ok(()); - } else { - eval_json_encode_append_invalid_utf8_bytes(bytes, flags, output)?; - } - output.push(b'"'); - Ok(()) -} - -/// Appends one valid UTF-8 character using PHP JSON string escaping rules. -fn eval_json_encode_append_char(character: char, flags: i64, output: &mut Vec) { - if character.is_ascii() { - eval_json_encode_append_ascii_byte(character as u8, flags, output); - } else if flags & EVAL_JSON_UNESCAPED_UNICODE != 0 { - let mut buffer = [0_u8; 4]; - output.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes()); - } else { - eval_json_encode_append_unicode_escape(character as u32, output); - } -} - -/// Appends one ASCII byte using JSON escaping rules shared by UTF-8 and fallback paths. -fn eval_json_encode_append_ascii_byte(byte: u8, flags: i64, output: &mut Vec) { - match byte { - b'"' if flags & EVAL_JSON_HEX_QUOT != 0 => output.extend_from_slice(b"\\u0022"), - b'"' => output.extend_from_slice(b"\\\""), - b'\\' => output.extend_from_slice(b"\\\\"), - b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { - output.extend_from_slice(b"\\/"); - } - b'/' => output.push(b'/'), - b'<' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003C"), - b'>' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003E"), - b'&' if flags & EVAL_JSON_HEX_AMP != 0 => output.extend_from_slice(b"\\u0026"), - b'\'' if flags & EVAL_JSON_HEX_APOS != 0 => output.extend_from_slice(b"\\u0027"), - b'\x08' => output.extend_from_slice(b"\\b"), - b'\x0c' => output.extend_from_slice(b"\\f"), - b'\n' => output.extend_from_slice(b"\\n"), - b'\r' => output.extend_from_slice(b"\\r"), - b'\t' => output.extend_from_slice(b"\\t"), - control @ 0x00..=0x1f => { - output.extend_from_slice(format!("\\u{control:04x}").as_bytes()); - } - _ => output.push(byte), - } -} - -/// Appends valid scalar values as PHP JSON `\uXXXX` escapes, using surrogate pairs when needed. -fn eval_json_encode_append_unicode_escape(codepoint: u32, output: &mut Vec) { - if codepoint <= 0xffff { - output.extend_from_slice(format!("\\u{codepoint:04x}").as_bytes()); - return; - } - - let codepoint = codepoint - 0x1_0000; - let high = 0xd800 + ((codepoint >> 10) & 0x3ff); - let low = 0xdc00 + (codepoint & 0x3ff); - output.extend_from_slice(format!("\\u{high:04x}\\u{low:04x}").as_bytes()); -} - -/// Appends malformed UTF-8 bytes according to PHP's JSON invalid-UTF-8 flags. -fn eval_json_encode_append_invalid_utf8_bytes( - mut bytes: &[u8], - flags: i64, - output: &mut Vec, -) -> Result<(), EvalStatus> { - while !bytes.is_empty() { - match std::str::from_utf8(bytes) { - Ok(value) => { - for character in value.chars() { - eval_json_encode_append_char(character, flags, output); - } - return Ok(()); - } - Err(error) => { - let valid = &bytes[..error.valid_up_to()]; - for character in std::str::from_utf8(valid) - .map_err(|_| EvalStatus::RuntimeFatal)? - .chars() - { - eval_json_encode_append_char(character, flags, output); - } - let invalid_len = error - .error_len() - .unwrap_or(bytes.len() - valid.len()) - .max(1); - if flags & EVAL_JSON_INVALID_UTF8_IGNORE == 0 { - eval_json_encode_append_char('\u{fffd}', flags, output); - } - bytes = &bytes[valid.len() + invalid_len.min(bytes.len() - valid.len())..]; - } - } - } - Ok(()) -} - -/// Returns the JSON number bytes for a PHP numeric string when `JSON_NUMERIC_CHECK` applies. -fn eval_json_numeric_check_bytes(bytes: &[u8]) -> Option> { - let value = std::str::from_utf8(bytes).ok()?.trim(); - if value.is_empty() { - return None; - } - let integer_grammar = value - .bytes() - .all(|byte| byte.is_ascii_digit() || matches!(byte, b'+' | b'-')); - if integer_grammar { - if let Ok(integer) = value.parse::() { - return Some(integer.to_string().into_bytes()); - } - } - let number = value.parse::().ok()?; - if number.is_finite() { - Some(number.to_string().into_bytes()) - } else { - None - } -} - -/// Evaluates PHP `print_r()` over one eval expression. -fn eval_builtin_print_r( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_print_r_result(value, values) -} - -/// Emits one eval value using elephc's supported `print_r()` output shape. -fn eval_print_r_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if matches!(values.type_tag(value)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - let output = values.string_bytes_value(b"Array\n")?; - values.echo(output)?; - } else { - values.echo(value)?; - } - values.bool_value(true) -} - -/// Evaluates PHP `var_dump()` over one eval expression and returns null. -fn eval_builtin_var_dump( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_var_dump_result(value, values) -} - -/// Emits one eval value using PHP-style `var_dump()` debug formatting. -fn eval_var_dump_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut output = Vec::new(); - let mut arrays_seen = Vec::new(); - eval_var_dump_append_value(value, values, 0, &mut arrays_seen, &mut output)?; - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.null() -} - -/// Appends one value and its nested array entries to a `var_dump()` byte buffer. -fn eval_var_dump_append_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - arrays_seen: &mut Vec, - output: &mut Vec, -) -> Result<(), EvalStatus> { - match values.type_tag(value)? { - EVAL_TAG_INT => eval_var_dump_append_scalar(b"int", value, values, depth, output), - EVAL_TAG_STRING => eval_var_dump_append_string(value, values, depth, output), - EVAL_TAG_FLOAT => eval_var_dump_append_scalar(b"float", value, values, depth, output), - EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, output), - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { - eval_var_dump_append_array(value, values, depth, arrays_seen, output) - } - EVAL_TAG_OBJECT => { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"object(Object)\n"); - Ok(()) - } - EVAL_TAG_NULL => { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"NULL\n"); - Ok(()) - } - EVAL_TAG_RESOURCE => { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"resource(0) of type (stream)\n"); - Ok(()) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Appends one integer-like or float-like `var_dump()` scalar line. -fn eval_var_dump_append_scalar( - label: &[u8], - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(label); - output.extend_from_slice(b"("); - output.extend_from_slice(&values.string_bytes(value)?); - output.extend_from_slice(b")\n"); - Ok(()) -} - -/// Appends one string `var_dump()` line while preserving raw PHP string bytes. -fn eval_var_dump_append_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - output: &mut Vec, -) -> Result<(), EvalStatus> { - let bytes = values.string_bytes(value)?; - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"string("); - output.extend_from_slice(bytes.len().to_string().as_bytes()); - output.extend_from_slice(b") \""); - output.extend_from_slice(&bytes); - output.extend_from_slice(b"\"\n"); - Ok(()) -} - -/// Appends one boolean `var_dump()` line from PHP truthiness. -fn eval_var_dump_append_bool( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_var_dump_append_indent(depth, output); - if values.truthy(value)? { - output.extend_from_slice(b"bool(true)\n"); - } else { - output.extend_from_slice(b"bool(false)\n"); - } - Ok(()) -} - -/// Appends one array shell and recursively emits foreach-visible entries. -fn eval_var_dump_append_array( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - arrays_seen: &mut Vec, - output: &mut Vec, -) -> Result<(), EvalStatus> { - let address = value.as_ptr() as usize; - if arrays_seen.contains(&address) { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"*RECURSION*\n"); - return Ok(()); - } - - arrays_seen.push(address); - let len = values.array_len(value)?; - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"array("); - output.extend_from_slice(len.to_string().as_bytes()); - output.extend_from_slice(b") {\n"); - for position in 0..len { - let key = values.array_iter_key(value, position)?; - let element = values.array_get(value, key)?; - eval_var_dump_append_key(key, values, depth + 1, output)?; - eval_var_dump_append_value(element, values, depth + 1, arrays_seen, output)?; - } - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"}\n"); - arrays_seen.pop(); - Ok(()) -} - -/// Appends one array key line for an indexed or associative `var_dump()` entry. -fn eval_var_dump_append_key( - key: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"["); - match values.type_tag(key)? { - EVAL_TAG_STRING => { - output.extend_from_slice(b"\""); - output.extend_from_slice(&values.string_bytes(key)?); - output.extend_from_slice(b"\""); - } - _ => output.extend_from_slice(&values.string_bytes(key)?), - } - output.extend_from_slice(b"]=>\n"); - Ok(()) -} - -/// Appends the two-space indentation used by PHP `var_dump()` arrays. -fn eval_var_dump_append_indent(depth: usize, output: &mut Vec) { - for _ in 0..depth { - output.extend_from_slice(b" "); - } -} - -/// Evaluates an eval-declared user function with PHP-style argument binding. -fn eval_dynamic_function( - function: &EvalFunction, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = - eval_function_call_args(function.params(), args, context, caller_scope, values)?; - eval_dynamic_function_with_values(function, evaluated_args, context, values) -} - -/// Evaluates and binds function-like arguments to parameter order. -fn eval_function_call_args( - params: &[String], - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let evaluated_args = eval_call_arg_values(args, context, caller_scope, values)?; - bind_evaluated_function_args(params, evaluated_args) -} - -/// Evaluates source-order call arguments while preserving named-argument metadata. -fn eval_call_arg_values( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut evaluated_args = Vec::with_capacity(args.len()); - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let spread = eval_expr(arg.value(), context, caller_scope, values)?; - if !values.is_array_like(spread)? { - return Err(EvalStatus::RuntimeFatal); - } - append_unpacked_call_arg_values(spread, &mut evaluated_args, &mut saw_named, values)?; - continue; - } - - if let Some(name) = arg.name() { - saw_named = true; - let value = eval_expr(arg.value(), context, caller_scope, values)?; - evaluated_args.push(EvaluatedCallArg { - name: Some(name.to_string()), - value, - }); - continue; - } - - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let value = eval_expr(arg.value(), context, caller_scope, values)?; - evaluated_args.push(EvaluatedCallArg { name: None, value }); - } - - Ok(evaluated_args) -} - -/// Converts a `call_user_func_array` argument array into ordered call arguments. -fn eval_array_call_arg_values( - arg_array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(arg_array)?; - let mut evaluated_args = Vec::with_capacity(len); - let mut saw_named = false; - append_unpacked_call_arg_values(arg_array, &mut evaluated_args, &mut saw_named, values)?; - Ok(evaluated_args) -} - -/// Appends one unpacked array's values using PHP named-argument key semantics. -fn append_unpacked_call_arg_values( - array: RuntimeCellHandle, - evaluated_args: &mut Vec, - saw_named: &mut bool, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - match values.type_tag(key)? { - EVAL_TAG_INT => { - if *saw_named { - return Err(EvalStatus::RuntimeFatal); - } - evaluated_args.push(EvaluatedCallArg { name: None, value }); - } - EVAL_TAG_STRING => { - *saw_named = true; - let name = values.string_bytes(key)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - evaluated_args.push(EvaluatedCallArg { - name: Some(name), - value, - }); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - Ok(()) -} - -/// Binds evaluated positional and named values to declared parameter order. -fn bind_evaluated_function_args( - params: &[String], - evaluated_args: Vec, -) -> Result, EvalStatus> { - let mut bound_args = vec![None; params.len()]; - let mut next_positional = 0; - - for arg in evaluated_args { - if let Some(name) = arg.name { - bind_dynamic_named_arg(params, &mut bound_args, &name, arg.value)?; - } else { - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; - } - } - - bound_args - .into_iter() - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Binds one positional dynamic-call value to the next declared parameter slot. -fn bind_dynamic_positional_arg( - bound_args: &mut [Option], - next_positional: &mut usize, - value: RuntimeCellHandle, -) -> Result<(), EvalStatus> { - if *next_positional >= bound_args.len() || bound_args[*next_positional].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[*next_positional] = Some(value); - *next_positional += 1; - Ok(()) -} - -/// Binds one named dynamic-call value to the matching declared parameter slot. -fn bind_dynamic_named_arg( - params: &[String], - bound_args: &mut [Option], - name: &str, - value: RuntimeCellHandle, -) -> Result<(), EvalStatus> { - let Some(param_index) = params.iter().position(|param| param == name) else { - return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[param_index] = Some(value); - Ok(()) -} - -/// Evaluates an eval-declared function after its positional arguments are prepared. -fn eval_dynamic_function_with_values( - function: &EvalFunction, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut function_scope = ElephcEvalScope::new(); - for (name, value) in function.params().iter().zip(evaluated_args) { - function_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); - } - let static_names = static_var_names(function.body()); - context.push_function(function.name()); - let result = execute_statements(function.body(), context, &mut function_scope, values); - let persist_result = persist_static_locals( - context, - function.name(), - &static_names, - &function_scope, - values, - ); - context.pop_function(); - persist_result?; - match result? { - EvalControl::None => values.null(), - EvalControl::Return(result) => Ok(result), - EvalControl::Throw(result) => { - context.set_pending_throw(result); - Err(EvalStatus::UncaughtThrowable) - } - EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Persists static local variables from one eval-declared function activation. -fn persist_static_locals( - context: &mut ElephcEvalContext, - function_name: &str, - names: &[String], - scope: &ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for name in names { - if let Some(cell) = scope.visible_cell(name) { - if let Some(replaced) = - context.set_static_local(function_name.to_string(), name.clone(), cell) - { - values.release(replaced)?; - } - } - } - Ok(()) -} - -/// Returns the distinct static local names declared anywhere in an eval function body. -fn static_var_names(body: &[EvalStmt]) -> Vec { - let mut names = std::collections::HashSet::new(); - collect_static_var_names(body, &mut names); - names.into_iter().collect() -} - -/// Recursively collects static local declaration names from eval statements. -fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::HashSet) { - for stmt in body { - match stmt { - EvalStmt::StaticVar { name, .. } => { - names.insert(name.clone()); - } - EvalStmt::DoWhile { body, .. } - | EvalStmt::Foreach { body, .. } - | EvalStmt::For { body, .. } - | EvalStmt::While { body, .. } => collect_static_var_names(body, names), - EvalStmt::FunctionDecl { .. } => {} - EvalStmt::If { - then_branch, - else_branch, - .. - } => { - collect_static_var_names(then_branch, names); - collect_static_var_names(else_branch, names); - } - EvalStmt::Switch { cases, .. } => { - for case in cases { - collect_static_var_names(&case.body, names); - } - } - EvalStmt::Try { - body, - catches, - finally_body, - } => { - collect_static_var_names(body, names); - for catch in catches { - collect_static_var_names(&catch.body, names); - } - collect_static_var_names(finally_body, names); - } - EvalStmt::ArrayAppendVar { .. } - | EvalStmt::ArraySetVar { .. } - | EvalStmt::Break - | EvalStmt::ClassDecl(_) - | EvalStmt::Continue - | EvalStmt::Echo(_) - | EvalStmt::Expr(_) - | EvalStmt::Global { .. } - | EvalStmt::PropertySet { .. } - | EvalStmt::ReferenceAssign { .. } - | EvalStmt::Return(_) - | EvalStmt::StoreVar { .. } - | EvalStmt::Throw(_) - | EvalStmt::UnsetVar { .. } => {} - } - } -} - -/// Evaluates a registered AOT function through its descriptor-compatible invoker. -fn eval_native_function( - function: NativeFunction, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = if function.param_names().len() == function.param_count() { - eval_function_call_args(function.param_names(), args, context, caller_scope, values)? - } else { - eval_positional_call_arg_values(args, context, caller_scope, values)? - }; - eval_native_function_with_values(function, evaluated_args, values) -} - -/// Invokes a registered AOT function after its positional arguments are prepared. -fn eval_native_function_with_values( - function: NativeFunction, - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - if evaluated_args.len() != function.param_count() { - return Err(EvalStatus::RuntimeFatal); - } - let arg_array = values.array_new(evaluated_args.len())?; - for (index, value) in evaluated_args.into_iter().enumerate() { - let index = values.int(index as i64)?; - let _ = values.array_set(arg_array, index, value)?; - } - let result = unsafe { function.call(arg_array) }; - values.release(arg_array)?; - if result.is_null() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(result) -} - -/// Evaluates an indexed array literal into a boxed runtime Mixed array. -fn eval_indexed_array( - elements: &[EvalArrayElement], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let array = values.array_new(elements.len())?; - for (index, element) in elements.iter().enumerate() { - let EvalArrayElement::Value(element) = element else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let index = values.int(index as i64)?; - let value = eval_expr(element, context, scope, values)?; - let _ = values.array_set(array, index, value)?; - } - Ok(array) -} - -/// Evaluates an associative array literal into a boxed runtime Mixed hash. -fn eval_assoc_array( - elements: &[EvalArrayElement], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let array = values.assoc_new(elements.len())?; - let mut next_key = None; - for element in elements { - let (key, value) = match element { - EvalArrayElement::Value(value) => { - let key = match next_key { - Some(next_key) => next_key, - None => values.int(0)?, - }; - let one = values.int(1)?; - next_key = Some(values.add(key, one)?); - (key, value) - } - EvalArrayElement::KeyValue { key, value } => { - let key = eval_expr(key, context, scope, values)?; - next_key = eval_array_next_key_after_explicit_key(key, next_key, values)?; - (key, value) - } - }; - let value = eval_expr(value, context, scope, values)?; - let _ = values.array_set(array, key, value)?; - } - Ok(array) -} - -/// Advances an array literal's automatic key after an integer-normalized explicit key. -fn eval_array_next_key_after_explicit_key( - key: RuntimeCellHandle, - current_next_key: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let key = match values.type_tag(key)? { - EVAL_TAG_INT => key, - EVAL_TAG_STRING => { - let bytes = values.string_bytes(key)?; - let Some(key) = eval_numeric_string_array_key(&bytes) else { - return Ok(current_next_key); - }; - values.int(key)? - } - EVAL_TAG_NULL => return Ok(current_next_key), - _ => values.cast_int(key)?, - }; - let one = values.int(1)?; - let candidate = values.add(key, one)?; - let replace = if let Some(current_next_key) = current_next_key { - let is_greater = values.compare(EvalBinOp::Gt, candidate, current_next_key)?; - values.truthy(is_greater)? - } else { - true - }; - Ok(if replace { - Some(candidate) - } else { - current_next_key - }) -} - -/// Parses PHP integer-string array keys that normalize to integer keys. -fn eval_numeric_string_array_key(bytes: &[u8]) -> Option { - if bytes.is_empty() { - return None; - } - - let (negative, digits) = if bytes[0] == b'-' { - if bytes.len() == 1 { - return None; - } - (true, &bytes[1..]) - } else { - (false, bytes) - }; - - if digits[0] == b'0' { - return if !negative && digits.len() == 1 { - Some(0) - } else { - None - }; - } - if digits.iter().any(|byte| !byte.is_ascii_digit()) { - return None; - } - - let limit = if negative { - i64::MAX as u128 + 1 - } else { - i64::MAX as u128 - }; - let mut value = 0u128; - for digit in digits { - value = (value * 10) + u128::from(digit - b'0'); - if value > limit { - return None; - } - } - - if negative { - if value == i64::MAX as u128 + 1 { - Some(i64::MIN) - } else { - Some(-(value as i64)) - } - } else { - Some(value as i64) - } -} - -/// Converts one EvalIR constant into a runtime-cell handle. -fn eval_const( - value: &EvalConst, - values: &mut impl RuntimeValueOps, -) -> Result { - match value { - EvalConst::Null => values.null(), - EvalConst::Bool(value) => values.bool_value(*value), - EvalConst::Int(value) => values.int(*value), - EvalConst::Float(value) => values.float(*value), - EvalConst::String(value) => values.string(value), - } -} - -/// Loads a retained value for one eval-defined dynamic constant. -fn eval_const_fetch( - name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(value) = eval_predefined_constant(name, values)? { - return Ok(value); - } - let Some(value) = context.constant(name) else { - return Err(EvalStatus::RuntimeFatal); - }; - values.retain(value) -} - -/// Fetches a namespaced constant and falls back to the global constant namespace. -fn eval_namespaced_const_fetch( - name: &str, - fallback_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(value) = eval_predefined_constant(name, values)? { - return Ok(value); - } - if let Some(value) = context.constant(name) { - return values.retain(value); - } - eval_const_fetch(fallback_name, context, values) -} - -/// Materializes one eval-visible predefined constant into a runtime cell. -fn eval_predefined_constant( - name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(value) = eval_predefined_constant_value(name) else { - return Ok(None); - }; - match value { - EvalPredefinedConstant::Int(value) => values.int(value).map(Some), - EvalPredefinedConstant::Float(value) => values.float(value).map(Some), - EvalPredefinedConstant::String(value) => values.string(value).map(Some), - } -} - -/// Returns eval-visible predefined constants that do not live in dynamic context. -fn eval_predefined_constant_value(name: &str) -> Option { - match name.trim_start_matches('\\') { - "PATHINFO_DIRNAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_DIRNAME)), - "PATHINFO_BASENAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_BASENAME)), - "PATHINFO_EXTENSION" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_EXTENSION)), - "PATHINFO_FILENAME" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_FILENAME)), - "PATHINFO_ALL" => Some(EvalPredefinedConstant::Int(EVAL_PATHINFO_ALL)), - "FNM_NOESCAPE" => Some(EvalPredefinedConstant::Int(EVAL_FNM_NOESCAPE)), - "FNM_PATHNAME" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PATHNAME)), - "FNM_PERIOD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PERIOD)), - "FNM_CASEFOLD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_CASEFOLD)), - "ARRAY_FILTER_USE_VALUE" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_VALUE)), - "ARRAY_FILTER_USE_BOTH" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_BOTH)), - "ARRAY_FILTER_USE_KEY" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_KEY)), - "COUNT_NORMAL" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_NORMAL)), - "COUNT_RECURSIVE" => Some(EvalPredefinedConstant::Int(EVAL_COUNT_RECURSIVE)), - "PREG_SPLIT_NO_EMPTY" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_NO_EMPTY)), - "PREG_SPLIT_DELIM_CAPTURE" => { - Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_DELIM_CAPTURE)) - } - "PREG_SPLIT_OFFSET_CAPTURE" => { - Some(EvalPredefinedConstant::Int(EVAL_PREG_SPLIT_OFFSET_CAPTURE)) - } - "PREG_PATTERN_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_PATTERN_ORDER)), - "PREG_SET_ORDER" => Some(EvalPredefinedConstant::Int(EVAL_PREG_SET_ORDER)), - "PREG_OFFSET_CAPTURE" => Some(EvalPredefinedConstant::Int(EVAL_PREG_OFFSET_CAPTURE)), - "PREG_UNMATCHED_AS_NULL" => Some(EvalPredefinedConstant::Int(EVAL_PREG_UNMATCHED_AS_NULL)), - "JSON_ERROR_NONE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_NONE)), - "JSON_ERROR_DEPTH" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_DEPTH)), - "JSON_ERROR_STATE_MISMATCH" => { - Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_STATE_MISMATCH)) - } - "JSON_ERROR_CTRL_CHAR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_CTRL_CHAR)), - "JSON_ERROR_SYNTAX" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_SYNTAX)), - "JSON_ERROR_UTF8" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF8)), - "JSON_ERROR_RECURSION" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_RECURSION)), - "JSON_ERROR_INF_OR_NAN" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_INF_OR_NAN)), - "JSON_ERROR_UNSUPPORTED_TYPE" => Some(EvalPredefinedConstant::Int( - EVAL_JSON_ERROR_UNSUPPORTED_TYPE, - )), - "JSON_ERROR_INVALID_PROPERTY_NAME" => Some(EvalPredefinedConstant::Int( - EVAL_JSON_ERROR_INVALID_PROPERTY_NAME, - )), - "JSON_ERROR_UTF16" => Some(EvalPredefinedConstant::Int(EVAL_JSON_ERROR_UTF16)), - "JSON_HEX_TAG" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_TAG)), - "JSON_HEX_AMP" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_AMP)), - "JSON_HEX_APOS" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_APOS)), - "JSON_HEX_QUOT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_HEX_QUOT)), - "JSON_BIGINT_AS_STRING" => Some(EvalPredefinedConstant::Int(EVAL_JSON_BIGINT_AS_STRING)), - "JSON_FORCE_OBJECT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_FORCE_OBJECT)), - "JSON_NUMERIC_CHECK" => Some(EvalPredefinedConstant::Int(EVAL_JSON_NUMERIC_CHECK)), - "JSON_UNESCAPED_SLASHES" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_SLASHES)), - "JSON_UNESCAPED_UNICODE" => Some(EvalPredefinedConstant::Int(EVAL_JSON_UNESCAPED_UNICODE)), - "JSON_PARTIAL_OUTPUT_ON_ERROR" => Some(EvalPredefinedConstant::Int( - EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR, - )), - "JSON_PRETTY_PRINT" => Some(EvalPredefinedConstant::Int(EVAL_JSON_PRETTY_PRINT)), - "JSON_PRESERVE_ZERO_FRACTION" => Some(EvalPredefinedConstant::Int( - EVAL_JSON_PRESERVE_ZERO_FRACTION, - )), - "JSON_INVALID_UTF8_IGNORE" => { - Some(EvalPredefinedConstant::Int(EVAL_JSON_INVALID_UTF8_IGNORE)) - } - "JSON_INVALID_UTF8_SUBSTITUTE" => Some(EvalPredefinedConstant::Int( - EVAL_JSON_INVALID_UTF8_SUBSTITUTE, - )), - "JSON_THROW_ON_ERROR" => Some(EvalPredefinedConstant::Int(EVAL_JSON_THROW_ON_ERROR)), - "INF" => Some(EvalPredefinedConstant::Float(f64::INFINITY)), - "NAN" => Some(EvalPredefinedConstant::Float(f64::NAN)), - "PHP_INT_MAX" => Some(EvalPredefinedConstant::Int(i64::MAX)), - "PHP_EOL" => Some(EvalPredefinedConstant::String("\n")), - "PHP_OS" => Some(EvalPredefinedConstant::String(eval_php_os_name())), - "DIRECTORY_SEPARATOR" => Some(EvalPredefinedConstant::String("/")), - _ => None, - } -} - -/// Returns the PHP OS constant for the host platform running the eval bridge. -fn eval_php_os_name() -> &'static str { - if cfg!(target_os = "macos") { - "Darwin" - } else { - "Linux" - } -} - -/// Resolves one eval magic constant against fragment and dynamic-call metadata. -fn eval_magic_const( - magic: &EvalMagicConst, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match magic { - EvalMagicConst::File => values.string(&context.eval_file_magic()), - EvalMagicConst::Dir => values.string(context.call_dir()), - EvalMagicConst::Line(line) => values.int(*line), - EvalMagicConst::Function => values.string(context.current_function().unwrap_or("")), - EvalMagicConst::Method => values.string(context.current_function().unwrap_or("")), - EvalMagicConst::Class | EvalMagicConst::Namespace | EvalMagicConst::Trait => { - values.string("") - } - } -} - /// Returns the current interpreter availability status for the ABI stub. pub fn current_stub_status() -> EvalStatus { EvalStatus::UnsupportedConstruct diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs new file mode 100644 index 0000000000..3e0bbbc307 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -0,0 +1,315 @@ +//! Purpose: +//! Defines the runtime value operation contract used by the EvalIR interpreter. +//! The trait abstracts over opaque elephc runtime cells while eval drives PHP semantics. +//! +//! Called from: +//! - `crate::interpreter::execute_program_with_context()` +//! - Eval builtin and expression execution helpers. +//! +//! Key details: +//! - Implementors own allocation, retain/release, casting, arithmetic, and target runtime calls. +//! - Tag constants mirror boxed Mixed runtime tags consumed by eval-only helpers. + +use crate::errors::EvalStatus; +use crate::eval_ir::EvalBinOp; +use crate::value::RuntimeCellHandle; + +/// Runtime value hooks required by the EvalIR interpreter. +pub trait RuntimeValueOps { + /// Creates a runtime indexed-array cell with room for at least `capacity` elements. + fn array_new(&mut self, capacity: usize) -> Result; + + /// Creates a runtime associative-array cell with room for at least `capacity` elements. + fn assoc_new(&mut self, capacity: usize) -> Result; + + /// Reads one element from a runtime Mixed cell using PHP array-read semantics. + /// + /// Missing keys and non-array receivers return PHP null, matching the generated + /// `__rt_mixed_array_get` runtime helper. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result; + + /// Checks whether a normalized PHP array key exists without conflating null values with misses. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result; + + /// Returns the foreach-visible key at a zero-based iteration position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result; + + /// Writes one element to a runtime array-like Mixed cell and returns the target cell. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result; + + /// Reads a named property from a runtime object held in a boxed Mixed cell. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result; + + /// Writes a named property on a runtime object held in a boxed Mixed cell. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus>; + + /// Returns the number of public JSON-visible properties on a runtime object. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result; + + /// Returns the public property key at a zero-based JSON object iteration position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result; + + /// Calls a named method on a runtime object held in a boxed Mixed cell. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result; + + /// Creates a named runtime object without constructor arguments. + fn new_object(&mut self, class_name: &str) -> Result; + + /// Calls the runtime constructor for an object when the class declares one. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus>; + + /// Returns whether a runtime class table contains the requested class name. + fn class_exists(&mut self, name: &str) -> Result; + + /// Returns whether a runtime interface table contains the requested interface name. + fn interface_exists(&mut self, name: &str) -> Result; + + /// Returns whether a runtime trait table contains the requested trait name. + fn trait_exists(&mut self, name: &str) -> Result; + + /// Returns whether a runtime enum table contains the requested enum name. + fn enum_exists(&mut self, name: &str) -> Result; + + /// Tests whether a boxed object cell satisfies a class/interface relation. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result; + + /// Returns the PHP-visible runtime class name for an object cell. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result; + + /// Returns the PHP-visible parent class name for an object or class-name cell. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result; + + /// Returns the visible element count for an array-like runtime cell. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result; + + /// Returns whether a runtime cell can be indexed like an array by eval writes. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result; + + /// Returns whether a runtime cell holds PHP null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result; + + /// Returns the concrete boxed Mixed runtime tag after unwrapping nested Mixed cells. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result; + + /// Returns the unboxed object payload pointer used for PHP object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result; + + /// Releases one owned runtime cell that is no longer held by the eval scope. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; + + /// Retains one runtime cell so the eval caller receives an independent owner. + fn retain(&mut self, value: RuntimeCellHandle) -> Result; + + /// Emits or suppresses one PHP runtime warning through the target runtime. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus>; + + /// Creates a runtime null cell. + fn null(&mut self) -> Result; + + /// Creates a runtime bool cell. + fn bool_value(&mut self, value: bool) -> Result; + + /// Creates a runtime int cell. + fn int(&mut self, value: i64) -> Result; + + /// Creates a runtime float cell. + fn float(&mut self, value: f64) -> Result; + + /// Creates a runtime string cell. + fn string(&mut self, value: &str) -> Result; + + /// Creates a runtime byte-string cell from raw PHP string bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result; + + /// Casts one runtime cell to a boxed PHP integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP float cell. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP string cell. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result; + + /// Casts one runtime cell to a boxed PHP boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `abs()` for one runtime cell while preserving integer/float result typing. + fn abs(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `ceil()` for one runtime cell after PHP numeric conversion. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `floor()` for one runtime cell after PHP numeric conversion. + fn floor(&mut self, value: RuntimeCellHandle) -> Result; + + /// Computes PHP `sqrt()` for one runtime cell after PHP numeric conversion. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result; + + /// Reverses a string value using PHP `strrev()` byte-string semantics. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result; + + /// Divides two runtime cells using PHP `fdiv()` semantics. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Computes the floating-point remainder using PHP `fmod()` semantics. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Adds two runtime cells using PHP addition semantics. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Subtracts two runtime cells using PHP numeric semantics. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Multiplies two runtime cells using PHP numeric semantics. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Divides two runtime cells using PHP numeric semantics. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Computes modulo for two runtime cells using PHP integer modulo semantics. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Raises one runtime cell to another using PHP exponentiation semantics. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Rounds one runtime cell using PHP `round()` semantics and optional precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result; + + /// Applies an integer bitwise or shift operation to two runtime cells. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Applies integer bitwise NOT to one runtime cell. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result; + + /// Concatenates two runtime cells using PHP string conversion semantics. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Compares two runtime cells and returns a boxed PHP boolean cell. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Compares two runtime cells and returns a boxed PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result; + + /// Emits one runtime cell to stdout using PHP echo semantics. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; + + /// Casts one runtime cell to a PHP string and copies its bytes for parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus>; + + /// Converts one runtime cell to PHP boolean truthiness. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result; +} + +pub(super) const EVAL_TAG_INT: u64 = 0; +pub(super) const EVAL_TAG_STRING: u64 = 1; +pub(super) const EVAL_TAG_FLOAT: u64 = 2; +pub(super) const EVAL_TAG_BOOL: u64 = 3; +pub(super) const EVAL_TAG_ARRAY: u64 = 4; +pub(super) const EVAL_TAG_ASSOC: u64 = 5; +pub(super) const EVAL_TAG_OBJECT: u64 = 6; +pub(super) const EVAL_TAG_NULL: u64 = 8; +pub(super) const EVAL_TAG_RESOURCE: u64 = 9; diff --git a/crates/elephc-eval/src/interpreter/scope_cells.rs b/crates/elephc-eval/src/interpreter/scope_cells.rs new file mode 100644 index 0000000000..2b0542618e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/scope_cells.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Provides scope-cell read, write, unset, global-alias, and reference-alias helpers for eval execution. +//! +//! Called from: +//! - `crate::interpreter::statements` and `crate::interpreter::expressions`. +//! +//! Key details: +//! - Global aliases redirect through `ElephcEvalContext` while local aliases stay in the materialized eval scope. +//! - Replaced owned cells are released by callers through existing scope APIs. + +use super::*; + +/// Returns the eval-visible entry for a variable, following `global` aliases. +pub(in crate::interpreter) fn scope_entry( + context: &ElephcEvalContext, + scope: &ElephcEvalScope, + name: &str, +) -> Option { + let Some(global_name) = scope.global_alias_target(name) else { + return scope.entry(name); + }; + let Some(global_scope) = context.global_scope_ptr() else { + return scope.entry(name); + }; + let current_scope = scope as *const ElephcEvalScope as *mut ElephcEvalScope; + if global_scope == current_scope { + return scope.entry(global_name); + } + unsafe { + global_scope + .as_ref() + .and_then(|scope| scope.entry(global_name)) + } +} + +/// Returns the eval-visible cell for a variable, following `global` aliases. +pub(in crate::interpreter) fn visible_scope_cell( + context: &ElephcEvalContext, + scope: &ElephcEvalScope, + name: &str, +) -> Option { + scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(ScopeEntry::cell) +} + +/// Stores a variable cell, redirecting `global` aliases to the global scope. +pub(in crate::interpreter) fn set_scope_cell( + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + name: impl Into, + cell: RuntimeCellHandle, + ownership: ScopeCellOwnership, +) -> Result, EvalStatus> { + let name = name.into(); + if let Some(global_name) = scope.global_alias_target(&name).map(str::to_string) { + let Some(global_scope) = context.global_scope_ptr() else { + return Err(EvalStatus::RuntimeFatal); + }; + let current_scope = scope as *mut ElephcEvalScope; + if global_scope == current_scope { + return Ok(scope.set_respecting_references(global_name, cell, ownership)); + } + let Some(global_scope) = (unsafe { global_scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + return Ok(global_scope.set_respecting_references(global_name, cell, ownership)); + } + Ok(scope.set_respecting_references(name, cell, ownership)) +} + +/// Creates a PHP reference alias between two eval-visible variable names. +pub(in crate::interpreter) fn set_reference_alias( + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + target: &str, + source: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(global_name) = scope.global_alias_target(source).map(str::to_string) { + scope.mark_global_alias_to(target.to_string(), global_name); + return Ok(Vec::new()); + } + let (cell, ownership) = scope_entry(context, scope, source) + .filter(|entry| entry.flags().is_visible()) + .map_or_else( + || values.null().map(|cell| (cell, ScopeCellOwnership::Owned)), + |entry| Ok((entry.cell(), entry.flags().ownership)), + )?; + Ok(scope.set_reference(target.to_string(), source.to_string(), cell, ownership)) +} + +/// Unsets a variable, removing only the local alias when the name is global. +pub(in crate::interpreter) fn unset_scope_cell( + scope: &mut ElephcEvalScope, + name: impl Into, +) -> Option { + let name = name.into(); + if scope.is_global_alias(&name) { + scope.clear_global_alias(&name); + } + scope.unset_respecting_references(name) +} + +/// Marks variables as aliases to the context global scope for later reads/writes. +pub(in crate::interpreter) fn execute_global_stmt( + vars: &[String], + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, +) -> Result<(), EvalStatus> { + if context.global_scope_ptr().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + for name in vars { + scope.mark_global_alias(name.clone()); + } + Ok(()) +} diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs new file mode 100644 index 0000000000..96bb9f3255 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -0,0 +1,667 @@ +//! Purpose: +//! Executes EvalIR statements, loops, exception handling, static locals, and eval-declared classes. +//! +//! Called from: +//! - `crate::interpreter::execute_program_outcome_with_context()` and dynamic function execution. +//! +//! Key details: +//! - Statement execution propagates `EvalControl` instead of flattening returns, throws, breaks, or continues. +//! - Scope writes flow through shared scope-cell helpers so global aliases and reference aliases stay coherent. + +use super::*; + +/// Executes statements in source order and propagates the first eval `return`. +pub(in crate::interpreter) fn execute_statements( + statements: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + for stmt in statements { + match execute_stmt(stmt, context, scope, values)? { + EvalControl::None => {} + control => return Ok(control), + } + } + Ok(EvalControl::None) +} + +/// Executes one statement and returns `Some` only for eval `return`. +pub(in crate::interpreter) fn execute_stmt( + stmt: &EvalStmt, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match stmt { + EvalStmt::ArrayAppendVar { name, value } => { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some(existing) = + scope_entry(context, scope, name).filter(|entry| entry.flags().is_visible()) + { + if values.is_array_like(existing.cell())? { + let tag = values.type_tag(existing.cell())?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + ownership = existing.flags().ownership; + existing.cell() + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::ArraySetVar { name, index, value } => { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some(existing) = + scope_entry(context, scope, name).filter(|entry| entry.flags().is_visible()) + { + if values.is_array_like(existing.cell())? { + ownership = existing.flags().ownership; + existing.cell() + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_expr(index, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::Break => Ok(EvalControl::Break), + EvalStmt::Continue => Ok(EvalControl::Continue), + EvalStmt::DoWhile { body, condition } => { + execute_do_while_stmt(body, condition, context, scope, values) + } + EvalStmt::Echo(expr) => { + let value = eval_expr(expr, context, scope, values)?; + values.echo(value)?; + Ok(EvalControl::None) + } + EvalStmt::For { + init, + condition, + update, + body, + } => execute_for_stmt( + init, + condition.as_ref(), + update, + body, + context, + scope, + values, + ), + EvalStmt::ClassDecl(class) => { + execute_class_decl_stmt(class, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::Foreach { + array, + key_name, + value_name, + body, + } => execute_foreach_stmt( + array, + key_name.as_deref(), + value_name, + body, + context, + scope, + values, + ), + EvalStmt::FunctionDecl { name, params, body } => { + let key = name.to_ascii_lowercase(); + context + .define_function( + key, + EvalFunction::new(name.clone(), params.clone(), body.clone()), + ) + .map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(EvalControl::None) + } + EvalStmt::Global { vars } => { + execute_global_stmt(vars, context, scope)?; + Ok(EvalControl::None) + } + EvalStmt::If { + condition, + then_branch, + else_branch, + } => { + let condition = eval_expr(condition, context, scope, values)?; + if values.truthy(condition)? { + execute_statements(then_branch, context, scope, values) + } else { + execute_statements(else_branch, context, scope, values) + } + } + EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr( + expr, context, scope, values, + )?)), + EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), + EvalStmt::ReferenceAssign { target, source } => { + for replaced in set_reference_alias(context, scope, target, source, values)? { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::StaticVar { name, init } => { + execute_static_var_stmt(name, init, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertySet { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + values.property_set(object, property, value)?; + Ok(EvalControl::None) + } + EvalStmt::StoreVar { name, value } => { + let value = eval_expr(value, context, scope, values)?; + for replaced in set_scope_cell( + context, + scope, + name.clone(), + value, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::Switch { expr, cases } => { + execute_switch_stmt(expr, cases, context, scope, values) + } + EvalStmt::Throw(expr) => { + let thrown = eval_expr(expr, context, scope, values)?; + if values.type_tag(thrown)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + Ok(EvalControl::Throw(thrown)) + } + EvalStmt::Try { + body, + catches, + finally_body, + } => execute_try_stmt(body, catches, finally_body, context, scope, values), + EvalStmt::UnsetVar { name } => { + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::While { condition, body } => { + while { + let condition = eval_expr(condition, context, scope, values)?; + values.truthy(condition)? + } { + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) + } + EvalStmt::Expr(expr) => { + let _ = eval_expr(expr, context, scope, values)?; + Ok(EvalControl::None) + } + } +} + +/// Executes an eval `try` body and handles supported `catch` clauses. +pub(in crate::interpreter) fn execute_try_stmt( + body: &[EvalStmt], + catches: &[EvalCatch], + finally_body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let control = match execute_statements(body, context, scope, values) { + Ok(EvalControl::Throw(thrown)) => { + execute_matching_catch(thrown, catches, context, scope, values)? + } + Err(EvalStatus::UncaughtThrowable) => { + let Some(thrown) = context.take_pending_throw() else { + return Err(EvalStatus::UncaughtThrowable); + }; + execute_matching_catch(thrown, catches, context, scope, values)? + } + Ok(control) => control, + Err(status) => return Err(status), + }; + if finally_body.is_empty() { + return Ok(control); + } + match execute_statements(finally_body, context, scope, values) { + Ok(EvalControl::None) => Ok(control), + Ok(finally_control) => { + release_overridden_control(control, values)?; + Ok(finally_control) + } + Err(status) => { + release_overridden_control(control, values)?; + Err(status) + } + } +} + +/// Releases a pending control-flow value when `finally` replaces that action. +pub(in crate::interpreter) fn release_overridden_control( + control: EvalControl, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match control { + EvalControl::Return(value) | EvalControl::Throw(value) => values.release(value), + EvalControl::None | EvalControl::Break | EvalControl::Continue => Ok(()), + } +} + +/// Executes the first supported catch clause for a thrown eval object. +pub(in crate::interpreter) fn execute_matching_catch( + thrown: RuntimeCellHandle, + catches: &[EvalCatch], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut matched = None; + for catch in catches { + if catch_types_match_thrown(thrown, &catch.class_names, values)? { + matched = Some(catch); + break; + } + } + let Some(catch) = matched else { + return Ok(EvalControl::Throw(thrown)); + }; + if let Some(var_name) = &catch.var_name { + for replaced in set_scope_cell( + context, + scope, + var_name.clone(), + thrown, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } else { + values.release(thrown)?; + } + execute_statements(&catch.body, context, scope, values) +} + +/// Returns true when any type in one catch clause accepts the thrown object. +pub(in crate::interpreter) fn catch_types_match_thrown( + thrown: RuntimeCellHandle, + class_names: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + for class_name in class_names { + let class_name = class_name.trim_start_matches('\\'); + if class_name.eq_ignore_ascii_case("Throwable") { + return Ok(true); + } + if values.object_is_a(thrown, class_name, false)? { + return Ok(true); + } + } + Ok(false) +} + +/// Registers an eval-declared class in the dynamic class table. +pub(in crate::interpreter) fn execute_class_decl_stmt( + class: &EvalClass, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = class.name().trim_start_matches('\\'); + if context.has_class(name) || values.class_exists(name)? { + return Err(EvalStatus::RuntimeFatal); + } + if context.define_class(class.clone()) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Creates a backing object for an eval-declared class and runs its constructor. +pub(in crate::interpreter) fn eval_dynamic_class_new_object( + class: &EvalClass, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, class.name()); + for property in class.properties() { + let value = if let Some(default) = property.default() { + eval_expr(default, context, caller_scope, values)? + } else { + values.null()? + }; + values.property_set(object, property.name(), value)?; + } + if let Some(constructor) = class.method("__construct") { + eval_dynamic_method_with_values( + class.name(), + constructor, + object, + evaluated_args, + context, + values, + )?; + } else if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(object) +} + +/// Dispatches a method call to an eval-declared class method or to the runtime hook. +pub(in crate::interpreter) fn eval_method_call_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return values.method_call(object, method_name, evaluated_args); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return values.method_call(object, method_name, evaluated_args); + }; + let class_name = class.name().to_string(); + let method = class + .method(method_name) + .cloned() + .ok_or(EvalStatus::RuntimeFatal)?; + eval_dynamic_method_with_values( + &class_name, + &method, + object, + evaluated_args, + context, + values, + ) +} + +/// Executes one eval-declared class method with `$this` bound in method scope. +pub(in crate::interpreter) fn eval_dynamic_method_with_values( + class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = + bind_evaluated_function_args(method.params(), positional_args(evaluated_args))?; + let mut method_scope = ElephcEvalScope::new(); + method_scope.set("this", object, ScopeCellOwnership::Borrowed); + for (name, value) in method.params().iter().zip(evaluated_args) { + method_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); + } + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + let result = execute_statements(method.body(), context, &mut method_scope, values); + let persist_result = persist_static_locals( + context, + &qualified_method_name, + &static_names, + &method_scope, + values, + ); + context.pop_function(); + persist_result?; + match result? { + EvalControl::None => values.null(), + EvalControl::Return(result) => Ok(result), + EvalControl::Throw(result) => { + context.set_pending_throw(result); + Err(EvalStatus::UncaughtThrowable) + } + EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Wraps positional method arguments into the shared dynamic-call binding shape. +pub(in crate::interpreter) fn positional_args( + args: Vec, +) -> Vec { + args.into_iter() + .map(|value| EvaluatedCallArg { name: None, value }) + .collect() +} + +/// Executes a PHP `static $name = expr;` declaration in the current eval scope. +pub(in crate::interpreter) fn execute_static_var_stmt( + name: &str, + init: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(function_name) = context.current_function().map(str::to_string) else { + let value = eval_expr(init, context, scope, values)?; + if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Owned) { + values.release(replaced)?; + } + return Ok(()); + }; + if scope.contains_visible(name) { + return Ok(()); + } + let value = if let Some(value) = context.static_local(&function_name, name) { + value + } else { + let value = eval_expr(init, context, scope, values)?; + let _ = context.set_static_local(function_name.clone(), name.to_string(), value); + value + }; + if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Borrowed) { + values.release(replaced)?; + } + Ok(()) +} + +/// Executes a PHP switch with loose case matching, default fallback, and fallthrough. +pub(in crate::interpreter) fn execute_switch_stmt( + expr: &EvalExpr, + cases: &[EvalSwitchCase], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let subject = eval_expr(expr, context, scope, values)?; + let mut default_index = None; + let mut matched_index = None; + for (index, case) in cases.iter().enumerate() { + let Some(condition) = &case.condition else { + if default_index.is_none() { + default_index = Some(index); + } + continue; + }; + let condition = eval_expr(condition, context, scope, values)?; + let matches = values.compare(EvalBinOp::LooseEq, subject, condition)?; + if values.truthy(matches)? { + matched_index = Some(index); + break; + } + } + let Some(start_index) = matched_index.or(default_index) else { + return Ok(EvalControl::None); + }; + for case in &cases[start_index..] { + match execute_statements(&case.body, context, scope, values)? { + EvalControl::None => {} + EvalControl::Break | EvalControl::Continue => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `do/while` loop, evaluating the condition after every body run. +pub(in crate::interpreter) fn execute_do_while_stmt( + body: &[EvalStmt], + condition: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + loop { + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + let condition = eval_expr(condition, context, scope, values)?; + if !values.truthy(condition)? { + break; + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `for` loop while preserving update-on-continue semantics. +pub(in crate::interpreter) fn execute_for_stmt( + init: &[EvalStmt], + condition: Option<&EvalExpr>, + update: &[EvalStmt], + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_statements(init, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => return Ok(EvalControl::None), + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + loop { + if let Some(condition) = condition { + let condition = eval_expr(condition, context, scope, values)?; + if !values.truthy(condition)? { + break; + } + } + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + match execute_statements(update, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `foreach` loop over eval array values. +pub(in crate::interpreter) fn execute_foreach_stmt( + array: &EvalExpr, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array = eval_expr(array, context, scope, values)?; + let len = values.array_len(array)?; + for index in 0..len { + let key = values.array_iter_key(array, index)?; + let value = values.array_get(array, key)?; + if let Some(key_name) = key_name { + for replaced in set_scope_cell( + context, + scope, + key_name.to_string(), + key, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } else { + values.release(key)?; + } + for replaced in set_scope_cell( + context, + scope, + value_name.to_string(), + value, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Returns PHP's next automatic integer key for `$array[]` append writes. +pub(in crate::interpreter) fn eval_array_append_key( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut next_key = None; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + continue; + } + let one = values.int(1)?; + let candidate = values.add(key, one)?; + let replace = if let Some(current) = next_key { + let is_greater = values.compare(EvalBinOp::Gt, candidate, current)?; + values.truthy(is_greater)? + } else { + true + }; + if replace { + next_key = Some(candidate); + } + } + next_key.map_or_else(|| values.int(0), Ok) +} From 8d255c3c58b2bd4afb7960cf0f1f6389eae2cf29 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:18:33 +0200 Subject: [PATCH 0285/1208] Split eval parser state modules --- crates/elephc-eval/src/parser/cursor.rs | 169 ++ crates/elephc-eval/src/parser/expressions.rs | 761 ++++++++ crates/elephc-eval/src/parser/mod.rs | 3 + crates/elephc-eval/src/parser/state.rs | 1833 +----------------- crates/elephc-eval/src/parser/statements.rs | 931 +++++++++ crates/elephc-eval/src/parser/tests.rs | 2 +- 6 files changed, 1884 insertions(+), 1815 deletions(-) create mode 100644 crates/elephc-eval/src/parser/cursor.rs create mode 100644 crates/elephc-eval/src/parser/expressions.rs create mode 100644 crates/elephc-eval/src/parser/statements.rs diff --git a/crates/elephc-eval/src/parser/cursor.rs b/crates/elephc-eval/src/parser/cursor.rs new file mode 100644 index 0000000000..947c85caad --- /dev/null +++ b/crates/elephc-eval/src/parser/cursor.rs @@ -0,0 +1,169 @@ +//! Purpose: +//! Provides parser cursor primitives and small grammar helper functions shared by statement and expression parsing. +//! +//! Called from: +//! - `crate::parser::statements` and `crate::parser::expressions`. +//! +//! Key details: +//! - Cursor helpers are intentionally thin wrappers over the token stream. +//! - Helper predicates centralize PHP keyword and assignment-token recognition. + +use super::state::Parser; +use crate::errors::EvalParseError; +use crate::eval_ir::{EvalBinOp, EvalConst, EvalExpr, EvalStmt}; +use crate::lexer::TokenKind; + +impl Parser { + /// Consumes `expected` or returns a parse error. + pub(super) fn expect(&mut self, expected: TokenKind) -> Result<(), EvalParseError> { + if self.consume(expected) { + Ok(()) + } else { + Err(EvalParseError::UnexpectedToken) + } + } + + /// Consumes a semicolon or returns the semicolon-specific parse error. + pub(super) fn expect_semicolon(&mut self) -> Result<(), EvalParseError> { + if self.consume_semicolon() { + Ok(()) + } else { + Err(EvalParseError::ExpectedSemicolon) + } + } + + /// Consumes a semicolon if present. + pub(super) fn consume_semicolon(&mut self) -> bool { + self.consume(TokenKind::Semicolon) + } + + /// Consumes `expected` if the current token matches it. + pub(super) fn consume(&mut self, expected: TokenKind) -> bool { + if *self.current() == expected { + self.advance(); + true + } else { + false + } + } + + /// Returns the current token. + pub(super) fn current(&self) -> &TokenKind { + self.tokens.get(self.pos).unwrap_or(&TokenKind::Eof) + } + + /// Returns the next token without advancing. + pub(super) fn peek(&self) -> &TokenKind { + self.tokens.get(self.pos + 1).unwrap_or(&TokenKind::Eof) + } + + /// Advances to the next token. + pub(super) fn advance(&mut self) { + if self.pos < self.tokens.len() { + self.pos += 1; + } + } +} + +/// Returns true when the current token closes or starts a switch case arm. +pub(super) fn is_switch_case_boundary(token: &TokenKind) -> bool { + matches!(token, TokenKind::RBrace) + || matches!(token, TokenKind::Ident(name) if ident_eq(name, "case") || ident_eq(name, "default")) +} + +/// Maps simple variable assignment tokens to an optional compound EvalIR operator. +pub(super) fn assignment_op(token: &TokenKind) -> Option> { + match token { + TokenKind::Equal => Some(None), + TokenKind::PlusEqual => Some(Some(EvalBinOp::Add)), + TokenKind::MinusEqual => Some(Some(EvalBinOp::Sub)), + TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), + TokenKind::StarStarEqual => Some(Some(EvalBinOp::Pow)), + TokenKind::SlashEqual => Some(Some(EvalBinOp::Div)), + TokenKind::PercentEqual => Some(Some(EvalBinOp::Mod)), + TokenKind::AmpEqual => Some(Some(EvalBinOp::BitAnd)), + TokenKind::PipeEqual => Some(Some(EvalBinOp::BitOr)), + TokenKind::CaretEqual => Some(Some(EvalBinOp::BitXor)), + TokenKind::LessLessEqual => Some(Some(EvalBinOp::ShiftLeft)), + TokenKind::GreaterGreaterEqual => Some(Some(EvalBinOp::ShiftRight)), + TokenKind::DotEqual => Some(Some(EvalBinOp::Concat)), + _ => None, + } +} + +/// Builds the assigned value expression for plain and compound variable assignment. +pub(super) fn assignment_value(name: &str, op: Option, value: EvalExpr) -> EvalExpr { + match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::LoadVar(name.to_string())), + right: Box::new(value), + }, + None => value, + } +} + +/// Builds the StoreVar statement for a simple variable increment or decrement. +pub(super) fn inc_dec_store(name: String, increment: bool) -> EvalStmt { + EvalStmt::StoreVar { + value: EvalExpr::Binary { + op: if increment { + EvalBinOp::Add + } else { + EvalBinOp::Sub + }, + left: Box::new(EvalExpr::LoadVar(name.clone())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + name, + } +} + +/// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. +pub(super) fn ident_eq(actual: &str, expected: &str) -> bool { + actual.eq_ignore_ascii_case(expected) +} + +/// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. +pub(super) fn is_unsupported_statement_keyword(name: &str) -> bool { + ["enum", "interface", "trait"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + +/// Returns true for class member modifiers outside the current eval class subset. +pub(super) fn is_unsupported_class_member_modifier(name: &str) -> bool { + ["private", "protected", "static", "abstract", "final"] + .iter() + .any(|modifier| ident_eq(name, modifier)) +} + +/// Returns true when an identifier is an include/require expression construct. +pub(super) fn is_include_construct_name(name: &str) -> bool { + ["include", "include_once", "require", "require_once"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + +/// Returns the first namespace segment and the optional remaining suffix. +pub(super) fn split_first_name_segment(name: &str) -> (&str, Option<&str>) { + name.split_once('\\') + .map_or((name, None), |(first, tail)| (first, Some(tail))) +} + +/// Returns the final segment of a PHP qualified name. +pub(super) fn last_name_segment(name: &str) -> &str { + name.rsplit('\\').next().unwrap_or(name) +} + +/// Combines a grouped use prefix with one relative member name. +pub(super) fn join_grouped_use_name(prefix: &str, member: &str) -> String { + format!("{prefix}\\{member}") +} + +/// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. +pub(super) fn is_unsupported_expression_keyword(name: &str) -> bool { + ["clone", "yield"] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs new file mode 100644 index 0000000000..8f5c568a99 --- /dev/null +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -0,0 +1,761 @@ +//! Purpose: +//! Parses PHP eval expressions using PHP-compatible precedence and postfix syntax. +//! +//! Called from: +//! - `crate::parser::statements` for expression-bearing statements. +//! +//! Key details: +//! - Logical keyword precedence, ternary associativity, coalesce, and exponentiation follow PHP grammar. +//! - Name resolution uses parser namespace/import state while building EvalIR call and constant nodes. + +use super::cursor::*; +use super::state::*; +use crate::errors::EvalParseError; +use crate::eval_ir::{ + EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalMagicConst, EvalMatchArm, + EvalUnaryOp, +}; +use crate::lexer::TokenKind; + +impl Parser { + /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. + pub(super) fn parse_expr(&mut self) -> Result { + self.parse_keyword_or() + } + + /// Parses PHP keyword `or`, whose precedence is lower than `xor`, `and`, and ternary. + pub(super) fn parse_keyword_or(&mut self) -> Result { + let mut expr = self.parse_keyword_xor()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "or")) { + self.advance(); + let right = self.parse_keyword_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `xor`, whose operands are evaluated before boolean XOR. + pub(super) fn parse_keyword_xor(&mut self) -> Result { + let mut expr = self.parse_keyword_and()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "xor")) { + self.advance(); + let right = self.parse_keyword_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `and`, whose precedence is lower than ternary and `&&`. + pub(super) fn parse_keyword_and(&mut self) -> Result { + let mut expr = self.parse_ternary()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "and")) { + self.advance(); + let right = self.parse_ternary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. + pub(super) fn parse_ternary(&mut self) -> Result { + let condition = self.parse_null_coalesce()?; + if !self.consume(TokenKind::Question) { + return Ok(condition); + } + let then_branch = if self.consume(TokenKind::Colon) { + None + } else { + let expr = self.parse_expr()?; + self.expect(TokenKind::Colon)?; + Some(Box::new(expr)) + }; + let else_branch = self.parse_expr()?; + Ok(EvalExpr::Ternary { + condition: Box::new(condition), + then_branch, + else_branch: Box::new(else_branch), + }) + } + + /// Parses right-associative null coalescing below logical OR and above ternary. + pub(super) fn parse_null_coalesce(&mut self) -> Result { + let value = self.parse_logical_or()?; + if !self.consume(TokenKind::QuestionQuestion) { + return Ok(value); + } + let default = self.parse_null_coalesce()?; + Ok(EvalExpr::NullCoalesce { + value: Box::new(value), + default: Box::new(default), + }) + } + + /// Parses left-associative logical OR with lower precedence than logical AND. + pub(super) fn parse_logical_or(&mut self) -> Result { + let mut expr = self.parse_logical_and()?; + while self.consume(TokenKind::OrOr) { + let right = self.parse_logical_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative logical AND with lower precedence than equality. + pub(super) fn parse_logical_and(&mut self) -> Result { + let mut expr = self.parse_bit_or()?; + while self.consume(TokenKind::AndAnd) { + let right = self.parse_bit_or()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise OR with lower precedence than bitwise XOR. + pub(super) fn parse_bit_or(&mut self) -> Result { + let mut expr = self.parse_bit_xor()?; + while self.consume(TokenKind::Pipe) { + let right = self.parse_bit_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise XOR with lower precedence than bitwise AND. + pub(super) fn parse_bit_xor(&mut self) -> Result { + let mut expr = self.parse_bit_and()?; + while self.consume(TokenKind::Caret) { + let right = self.parse_bit_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise AND with lower precedence than equality. + pub(super) fn parse_bit_and(&mut self) -> Result { + let mut expr = self.parse_equality()?; + while self.consume(TokenKind::Ampersand) { + let right = self.parse_equality()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative equality and inequality comparisons. + pub(super) fn parse_equality(&mut self) -> Result { + let mut expr = self.parse_ordering()?; + loop { + let op = if self.consume(TokenKind::EqualEqual) { + EvalBinOp::LooseEq + } else if self.consume(TokenKind::NotEqual) { + EvalBinOp::LooseNotEq + } else if self.consume(TokenKind::EqualEqualEqual) { + EvalBinOp::StrictEq + } else if self.consume(TokenKind::NotEqualEqual) { + EvalBinOp::StrictNotEq + } else { + break; + }; + let right = self.parse_ordering()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative ordered comparisons. + pub(super) fn parse_ordering(&mut self) -> Result { + let mut expr = self.parse_shift()?; + loop { + let op = if self.consume(TokenKind::Less) { + EvalBinOp::Lt + } else if self.consume(TokenKind::LessEqual) { + EvalBinOp::LtEq + } else if self.consume(TokenKind::Greater) { + EvalBinOp::Gt + } else if self.consume(TokenKind::GreaterEqual) { + EvalBinOp::GtEq + } else if self.consume(TokenKind::Spaceship) { + EvalBinOp::Spaceship + } else { + break; + }; + let right = self.parse_shift()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative integer shift operators. + pub(super) fn parse_shift(&mut self) -> Result { + let mut expr = self.parse_concat()?; + loop { + let op = if self.consume(TokenKind::LessLess) { + EvalBinOp::ShiftLeft + } else if self.consume(TokenKind::GreaterGreater) { + EvalBinOp::ShiftRight + } else { + break; + }; + let right = self.parse_concat()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative string concatenation. + pub(super) fn parse_concat(&mut self) -> Result { + let mut expr = self.parse_add()?; + while self.consume(TokenKind::Dot) { + let right = self.parse_add()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric addition and subtraction. + pub(super) fn parse_add(&mut self) -> Result { + let mut expr = self.parse_mul()?; + loop { + let op = if self.consume(TokenKind::Plus) { + EvalBinOp::Add + } else if self.consume(TokenKind::Minus) { + EvalBinOp::Sub + } else { + break; + }; + let right = self.parse_mul()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric multiplication, division, and modulo. + pub(super) fn parse_mul(&mut self) -> Result { + let mut expr = self.parse_unary()?; + loop { + let op = if self.consume(TokenKind::Star) { + EvalBinOp::Mul + } else if self.consume(TokenKind::Slash) { + EvalBinOp::Div + } else if self.consume(TokenKind::Percent) { + EvalBinOp::Mod + } else { + break; + }; + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses right-associative unary prefix expressions. + pub(super) fn parse_unary(&mut self) -> Result { + if self.consume(TokenKind::Plus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Minus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Bang) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Tilde) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(expr), + }); + } + self.parse_power() + } + + /// Parses right-associative exponentiation with higher precedence than unary prefix operators. + pub(super) fn parse_power(&mut self) -> Result { + let mut expr = self.parse_postfix()?; + if self.consume(TokenKind::StarStar) { + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses postfix array reads, property reads, method calls, and dynamic calls. + pub(super) fn parse_postfix(&mut self) -> Result { + let mut expr = self.parse_primary()?; + loop { + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + expr = EvalExpr::DynamicCall { + callee: Box::new(expr), + args, + }; + continue; + } + if self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + continue; + } + if self.consume(TokenKind::Arrow) { + let TokenKind::Ident(member) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + expr = EvalExpr::MethodCall { + object: Box::new(expr), + method: member.to_ascii_lowercase(), + args, + }; + } else { + expr = EvalExpr::PropertyGet { + object: Box::new(expr), + property: member, + }; + } + continue; + } + break; + } + Ok(expr) + } + + /// Parses primary expressions supported by the initial eval subset. + pub(super) fn parse_primary(&mut self) -> Result { + match self.current() { + TokenKind::Int(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Int(value))) + } + TokenKind::Float(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Float(value))) + } + TokenKind::String(value) => { + let value = value.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(value))) + } + TokenKind::DollarIdent(name) => { + let name = name.clone(); + self.advance(); + Ok(EvalExpr::LoadVar(name)) + } + TokenKind::Magic(EvalMagicConst::Namespace) => { + let namespace = self.namespace.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(namespace))) + } + TokenKind::Magic(magic) => { + let magic = magic.clone(); + self.advance(); + Ok(EvalExpr::Magic(magic)) + } + TokenKind::Ident(name) if ident_eq(name, "null") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Null)) + } + TokenKind::Ident(name) if ident_eq(name, "true") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(true))) + } + TokenKind::Ident(name) if ident_eq(name, "false") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(false))) + } + TokenKind::Ident(name) if ident_eq(name, "print") => { + self.advance(); + let expr = self.parse_expr()?; + Ok(EvalExpr::Print(Box::new(expr))) + } + TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { + self.parse_legacy_array_literal() + } + TokenKind::Ident(name) if is_include_construct_name(name) => self.parse_include_expr(), + TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), + TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), + TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::Backslash => self.parse_qualified_name_expr(), + TokenKind::Ident(_) if matches!(self.peek(), TokenKind::Backslash) => { + self.parse_qualified_name_expr() + } + TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { + self.parse_call_expr(name.clone()) + } + TokenKind::Ident(name) => { + let name = name.clone(); + self.advance(); + Ok(self.const_fetch_expr(name)) + } + TokenKind::LBracket => self.parse_array_literal(), + TokenKind::LParen => { + self.advance(); + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + Ok(expr) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Parses PHP include/require expression constructs and their path expression. + pub(super) fn parse_include_expr(&mut self) -> Result { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let required = ident_eq(name, "require") || ident_eq(name, "require_once"); + let once = ident_eq(name, "include_once") || ident_eq(name, "require_once"); + self.advance(); + let path = if self.consume(TokenKind::LParen) { + let path = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + path + } else { + self.parse_expr()? + }; + Ok(EvalExpr::Include { + path: Box::new(path), + required, + once, + }) + } + + /// Parses `match (expr) { pattern, other => value, default => fallback }`. + pub(super) fn parse_match_expr(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let subject = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + + let mut arms = Vec::new(); + let mut default = None; + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + self.expect(TokenKind::FatArrow)?; + default = Some(Box::new(self.parse_expr()?)); + } else { + arms.push(self.parse_match_arm()?); + } + if self.consume(TokenKind::Comma) { + continue; + } + self.expect(TokenKind::RBrace)?; + break; + } + + Ok(EvalExpr::Match { + subject: Box::new(subject), + arms, + default, + }) + } + + /// Parses one non-default `match` arm and its comma-separated pattern list. + pub(super) fn parse_match_arm(&mut self) -> Result { + let mut patterns = Vec::new(); + loop { + patterns.push(self.parse_expr()?); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::FatArrow) { + return Err(EvalParseError::UnexpectedToken); + } + if matches!(self.current(), TokenKind::Eof | TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + } + self.expect(TokenKind::FatArrow)?; + let value = self.parse_expr()?; + Ok(EvalMatchArm { patterns, value }) + } + + /// Parses a function-like call expression and its source-order arguments. + pub(super) fn parse_call_expr(&mut self, name: String) -> Result { + self.advance(); + let args = self.parse_call_args()?; + Ok(self.call_expr(name, args)) + } + + /// Parses an explicitly qualified call or constant-fetch expression. + pub(super) fn parse_qualified_name_expr(&mut self) -> Result { + let name = self.parse_qualified_name()?; + let name = self.resolve_qualified_name(name); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(EvalExpr::Call { + name: name.to_ascii_lowercase(), + args, + }); + } + Ok(EvalExpr::ConstFetch(name)) + } + + /// Parses `new ClassName(...)` expressions in eval fragments. + pub(super) fn parse_new_object_expr(&mut self) -> Result { + self.advance(); + let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_class_name(class_name); + let args = self.parse_call_args()?; + Ok(EvalExpr::NewObject { class_name, args }) + } + + /// Parses a simple or explicitly qualified PHP name. + pub(super) fn parse_qualified_name(&mut self) -> Result { + let absolute = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok(ParsedQualifiedName { name, absolute }) + } + + /// Builds a call expression, adding namespace fallback for unqualified names. + pub(super) fn call_expr(&self, name: String, args: Vec) -> EvalExpr { + if let Some(imported) = self.imports.resolve_function(&name) { + return EvalExpr::Call { + name: imported.to_ascii_lowercase(), + args, + }; + } + let fallback_name = name.to_ascii_lowercase(); + if self.namespace.is_empty() { + EvalExpr::Call { + name: fallback_name, + args, + } + } else { + EvalExpr::NamespacedCall { + name: self + .qualify_name_in_current_namespace(&name) + .to_ascii_lowercase(), + fallback_name, + args, + } + } + } + + /// Builds a constant fetch expression, adding namespace fallback for unqualified names. + pub(super) fn const_fetch_expr(&self, name: String) -> EvalExpr { + if let Some(imported) = self.imports.resolve_constant(&name) { + return EvalExpr::ConstFetch(imported.to_string()); + } + if self.namespace.is_empty() { + EvalExpr::ConstFetch(name) + } else { + EvalExpr::NamespacedConstFetch { + name: self.qualify_name_in_current_namespace(&name), + fallback_name: name, + } + } + } + + /// Prefixes a name with the parser's current namespace when one is active. + pub(super) fn qualify_name_in_current_namespace(&self, name: &str) -> String { + if self.namespace.is_empty() { + name.to_string() + } else { + format!("{}\\{}", self.namespace, name) + } + } + + /// Resolves a class name through active imports before namespace qualification. + pub(super) fn resolve_class_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute { + return name.name; + } + if let Some(imported) = self.imports.resolve_class(&name.name) { + return imported; + } + self.resolve_qualified_name(name) + } + + /// Resolves a parsed PHP name according to the current namespace. + pub(super) fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute || self.namespace.is_empty() { + name.name + } else { + self.qualify_name_in_current_namespace(&name.name) + } + } + + /// Parses a parenthesized source-order argument list. + pub(super) fn parse_call_args(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LParen)?; + let mut args = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(args); + } + loop { + args.push(self.parse_call_arg()?); + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RParen) { + return Ok(args); + } + } + self.expect(TokenKind::RParen)?; + Ok(args) + } + + /// Parses one positional or named argument within a call argument list. + pub(super) fn parse_call_arg(&mut self) -> Result { + if self.consume(TokenKind::Ellipsis) { + return self.parse_expr().map(EvalCallArg::spread); + } + if matches!(self.peek(), TokenKind::Colon) { + if let TokenKind::Ident(name) = self.current() { + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Colon)?; + let value = self.parse_expr()?; + return Ok(EvalCallArg::named(name, value)); + } + } + self.parse_expr().map(EvalCallArg::positional) + } + + /// Parses an array literal with source-order optional key/value element expressions. + pub(super) fn parse_array_literal(&mut self) -> Result { + self.expect(TokenKind::LBracket)?; + self.parse_array_elements_until(TokenKind::RBracket) + } + + /// Parses PHP's legacy `array(...)` literal into the same EvalIR node as `[...]`. + pub(super) fn parse_legacy_array_literal(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + self.parse_array_elements_until(TokenKind::RParen) + } + + /// Returns whether the current token starts PHP's legacy `array(...)` literal syntax. + pub(super) fn current_starts_legacy_array_literal(&self) -> bool { + matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "array")) + && matches!(self.peek(), TokenKind::LParen) + } + + /// Parses comma-separated array elements until the supplied closing delimiter. + pub(super) fn parse_array_elements_until( + &mut self, + close: TokenKind, + ) -> Result { + let mut elements = Vec::new(); + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + loop { + let first = self.parse_expr()?; + if self.consume(TokenKind::FatArrow) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyValue { key: first, value }); + } else { + elements.push(EvalArrayElement::Value(first)); + } + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + } + self.expect(close)?; + Ok(EvalExpr::Array(elements)) + } +} diff --git a/crates/elephc-eval/src/parser/mod.rs b/crates/elephc-eval/src/parser/mod.rs index b48a87b557..ab9bda44fa 100644 --- a/crates/elephc-eval/src/parser/mod.rs +++ b/crates/elephc-eval/src/parser/mod.rs @@ -12,7 +12,10 @@ //! `, - pos: usize, - source_len: usize, - namespace: String, - imports: NamespaceImports, - allow_use_imports: bool, + pub(super) tokens: Vec, + pub(super) pos: usize, + pub(super) source_len: usize, + pub(super) namespace: String, + pub(super) imports: NamespaceImports, + pub(super) allow_use_imports: bool, } /// A parsed PHP name plus whether it used a leading global namespace separator. -struct ParsedQualifiedName { - name: String, - absolute: bool, +pub(super) struct ParsedQualifiedName { + pub(super) name: String, + pub(super) absolute: bool, } /// Import alias tables active for the current namespace declaration region. #[derive(Default)] -struct NamespaceImports { +pub(super) struct NamespaceImports { classes: HashMap, functions: HashMap, constants: HashMap, @@ -46,7 +43,7 @@ struct NamespaceImports { /// The `use` declaration namespace being imported. #[derive(Copy, Clone, Eq, PartialEq)] -enum UseImportKind { +pub(super) enum UseImportKind { Class, Function, Const, @@ -54,22 +51,22 @@ enum UseImportKind { impl NamespaceImports { /// Stores one class import under PHP's case-insensitive class alias key. - fn insert_class(&mut self, alias: String, name: String) { + pub(super) fn insert_class(&mut self, alias: String, name: String) { self.classes.insert(alias.to_ascii_lowercase(), name); } /// Stores one function import under PHP's case-insensitive function alias key. - fn insert_function(&mut self, alias: String, name: String) { + pub(super) fn insert_function(&mut self, alias: String, name: String) { self.functions.insert(alias.to_ascii_lowercase(), name); } /// Stores one constant import under PHP's case-sensitive constant alias key. - fn insert_constant(&mut self, alias: String, name: String) { + pub(super) fn insert_constant(&mut self, alias: String, name: String) { self.constants.insert(alias, name); } /// Resolves a class import, including aliases used as the first segment of a class name. - fn resolve_class(&self, name: &str) -> Option { + pub(super) fn resolve_class(&self, name: &str) -> Option { let (first, tail) = split_first_name_segment(name); let imported = self.classes.get(&first.to_ascii_lowercase())?; Some(match tail { @@ -79,14 +76,14 @@ impl NamespaceImports { } /// Resolves an unqualified function alias. - fn resolve_function(&self, name: &str) -> Option<&str> { + pub(super) fn resolve_function(&self, name: &str) -> Option<&str> { self.functions .get(&name.to_ascii_lowercase()) .map(String::as_str) } /// Resolves a case-sensitive unqualified constant alias. - fn resolve_constant(&self, name: &str) -> Option<&str> { + pub(super) fn resolve_constant(&self, name: &str) -> Option<&str> { self.constants.get(name).map(String::as_str) } } @@ -112,1796 +109,4 @@ impl Parser { } Ok(EvalProgram::new(self.source_len, statements)) } - - /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. - fn parse_stmt(&mut self) -> Result, EvalParseError> { - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "break") => { - self.advance(); - self.expect_semicolon()?; - Ok(vec![EvalStmt::Break]) - } - TokenKind::Ident(name) if ident_eq(name, "continue") => { - self.advance(); - self.expect_semicolon()?; - Ok(vec![EvalStmt::Continue]) - } - TokenKind::Ident(name) if ident_eq(name, "do") => self.parse_do_while_stmt(), - TokenKind::Ident(name) if ident_eq(name, "echo") => { - self.advance(); - let mut statements = vec![EvalStmt::Echo(self.parse_expr()?)]; - while self.consume(TokenKind::Comma) { - statements.push(EvalStmt::Echo(self.parse_expr()?)); - } - self.expect_semicolon()?; - Ok(statements) - } - TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), - TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), - TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), - TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), - TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), - TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), - TokenKind::Ident(name) if ident_eq(name, "namespace") => self.parse_namespace_stmt(), - TokenKind::Ident(name) if ident_eq(name, "return") => { - self.advance(); - if self.consume_semicolon() { - return Ok(vec![EvalStmt::Return(None)]); - } - let expr = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::Return(Some(expr))]) - } - TokenKind::Ident(name) if ident_eq(name, "static") => self.parse_static_var_stmt(), - TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), - TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), - TokenKind::Ident(name) if ident_eq(name, "try") => self.parse_try_stmt(), - TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), - TokenKind::Ident(name) if ident_eq(name, "use") && self.allow_use_imports => { - self.parse_use_stmt() - } - TokenKind::Ident(name) if ident_eq(name, "use") => { - Err(EvalParseError::UnsupportedConstruct) - } - TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), - TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { - Err(EvalParseError::UnsupportedConstruct) - } - TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), - TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { - self.parse_property_stmt(true) - } - TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { - self.parse_array_set_stmt(name.clone()) - } - TokenKind::DollarIdent(name) - if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => - { - self.parse_postfix_inc_dec_stmt(name.clone(), true) - } - TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { - let name = name.clone(); - self.parse_var_store_stmt(name, true) - } - TokenKind::Eof => Err(EvalParseError::UnexpectedEof), - _ => { - let expr = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::Expr(expr)]) - } - } - } - - /// Parses `do { ... } while (expr);`. - fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let body = self.parse_statement_body()?; - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::DoWhile { body, condition }]) - } - - /// Parses `$name[index] = expr;` and `$name[] = expr;` eval writes. - fn parse_array_set_stmt(&mut self, name: String) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LBracket)?; - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - self.expect_semicolon()?; - return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) - } - - /// Parses `for (init; condition; update) { ... }`. - fn parse_for_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let init = self.parse_for_init_clause()?; - self.expect_semicolon()?; - let condition = if matches!(self.current(), TokenKind::Semicolon) { - None - } else { - Some(self.parse_expr()?) - }; - self.expect_semicolon()?; - let update = self.parse_for_update_clause()?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::For { - init, - condition, - update, - body, - }]) - } - - /// Parses `foreach (expr as $value) { ... }` or `foreach (expr as $key => $value) { ... }`. - fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let array = self.parse_expr()?; - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "as")) { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - let TokenKind::DollarIdent(value_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let value_name = value_name.clone(); - self.advance(); - let (key_name, value_name) = if matches!(self.current(), TokenKind::FatArrow) { - self.advance(); - let TokenKind::DollarIdent(next_value_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let key_name = value_name; - let value_name = next_value_name.clone(); - self.advance(); - (Some(key_name), value_name) - } else { - (None, value_name) - }; - self.expect(TokenKind::RParen)?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::Foreach { - array, - key_name, - value_name, - body, - }]) - } - - /// Parses `class Name { ... }` declarations for dynamic class metadata. - fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); - self.expect(TokenKind::LBrace)?; - let mut properties = Vec::new(); - let mut methods = Vec::new(); - while !self.consume(TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - self.parse_class_member(&mut properties, &mut methods)?; - } - self.consume_semicolon(); - Ok(vec![EvalStmt::ClassDecl(EvalClass::new( - name, properties, methods, - ))]) - } - - /// Parses one public property or method from an eval class body. - fn parse_class_member( - &mut self, - properties: &mut Vec, - methods: &mut Vec, - ) -> Result<(), EvalParseError> { - let public = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) - { - self.advance(); - true - } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) - { - return Err(EvalParseError::UnsupportedConstruct); - } else { - false - }; - - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - methods.push(self.parse_class_method_decl()?); - return Ok(()); - } - - if !public { - return Err(EvalParseError::UnsupportedConstruct); - } - properties.push(self.parse_class_property_decl()?); - Ok(()) - } - - /// Parses `function name($param, ...) { ... }` inside a dynamic eval class. - fn parse_class_method_decl(&mut self) -> Result { - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - self.expect(TokenKind::LParen)?; - let params = self.parse_function_params()?; - let body = self.parse_block()?; - Ok(EvalClassMethod::new(name, params, body)) - } - - /// Parses one public property declaration with an optional initializer. - fn parse_class_property_decl(&mut self) -> Result { - self.skip_optional_property_type()?; - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - let default = if self.consume(TokenKind::Equal) { - Some(self.parse_expr()?) - } else { - None - }; - self.expect_semicolon()?; - Ok(EvalClassProperty::new(name, default)) - } - - /// Consumes a simple declared property type before the `$property` token. - fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { - if matches!(self.current(), TokenKind::DollarIdent(_)) { - return Ok(()); - } - if self.consume(TokenKind::Question) && matches!(self.current(), TokenKind::DollarIdent(_)) - { - return Err(EvalParseError::UnexpectedToken); - } - match self.current() { - TokenKind::Ident(_) | TokenKind::Backslash => { - let _ = self.parse_qualified_name()?; - Ok(()) - } - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Parses `function name($param, ...) { ... }` declarations. - fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); - self.expect(TokenKind::LParen)?; - let params = self.parse_function_params()?; - let body = self.parse_block()?; - Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) - } - - /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. - fn parse_namespace_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let namespace = if self.consume(TokenKind::LBrace) { - return self.parse_namespace_block(String::new()); - } else { - self.parse_namespace_name()? - }; - if self.consume_semicolon() { - self.namespace = namespace; - self.imports = NamespaceImports::default(); - return Ok(Vec::new()); - } - self.expect(TokenKind::LBrace)?; - self.parse_namespace_block(namespace) - } - - /// Parses statements inside an already opened namespace block. - fn parse_namespace_block( - &mut self, - namespace: String, - ) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.namespace, namespace); - let previous_imports = std::mem::take(&mut self.imports); - let previous_allow_use_imports = std::mem::replace(&mut self.allow_use_imports, true); - let result = self.parse_block_contents(); - self.namespace = previous; - self.imports = previous_imports; - self.allow_use_imports = previous_allow_use_imports; - result - } - - /// Parses a namespace declaration name without a leading global separator. - fn parse_namespace_name(&mut self) -> Result { - let name = self.parse_qualified_name()?; - if name.absolute { - return Err(EvalParseError::UnexpectedToken); - } - Ok(name.name) - } - - /// Parses PHP `use`, `use function`, and `use const` import declarations. - fn parse_use_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let kind = self.parse_use_import_kind(); - - loop { - self.parse_use_import(kind)?; - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect_semicolon()?; - Ok(Vec::new()) - } - - /// Parses an optional top-level `function` or `const` use-import kind. - fn parse_use_import_kind(&mut self) -> UseImportKind { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - self.advance(); - UseImportKind::Function - } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - self.advance(); - UseImportKind::Const - } else { - UseImportKind::Class - } - } - - /// Parses and registers one comma-separated import entry. - fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { - let (name, grouped) = self.parse_use_name_or_group_start()?; - if grouped { - return self.parse_grouped_use_imports(kind, name); - } - self.parse_use_alias_and_register(kind, name) - } - - /// Parses a use-import name, stopping after a trailing namespace separator before `{`. - fn parse_use_name_or_group_start(&mut self) -> Result<(String, bool), EvalParseError> { - let _ = self.consume(TokenKind::Backslash); - let TokenKind::Ident(first) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let mut name = first.clone(); - self.advance(); - while self.consume(TokenKind::Backslash) { - if self.consume(TokenKind::LBrace) { - return Ok((name, true)); - } - let TokenKind::Ident(part) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - name.push('\\'); - name.push_str(part); - self.advance(); - } - Ok((name, false)) - } - - /// Parses all members inside a grouped namespace import declaration. - fn parse_grouped_use_imports( - &mut self, - default_kind: UseImportKind, - prefix: String, - ) -> Result<(), EvalParseError> { - if matches!(self.current(), TokenKind::RBrace) { - return Err(EvalParseError::UnexpectedToken); - } - loop { - let kind = self.parse_grouped_use_entry_kind(default_kind)?; - let member = self.parse_grouped_use_member_name()?; - let name = join_grouped_use_name(&prefix, &member); - self.parse_use_alias_and_register(kind, name)?; - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(TokenKind::RBrace) { - return Ok(()); - } - } - self.expect(TokenKind::RBrace) - } - - /// Parses an optional per-entry grouped import kind, matching PHP's mixed group rules. - fn parse_grouped_use_entry_kind( - &mut self, - default_kind: UseImportKind, - ) -> Result { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if default_kind != UseImportKind::Class { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - return Ok(UseImportKind::Function); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if default_kind != UseImportKind::Class { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - return Ok(UseImportKind::Const); - } - Ok(default_kind) - } - - /// Parses one non-absolute member name inside a grouped use declaration. - fn parse_grouped_use_member_name(&mut self) -> Result { - let name = self.parse_qualified_name()?; - if name.absolute { - return Err(EvalParseError::UnexpectedToken); - } - Ok(name.name) - } - - /// Parses an optional alias and stores one namespace import. - fn parse_use_alias_and_register( - &mut self, - kind: UseImportKind, - name: String, - ) -> Result<(), EvalParseError> { - let alias = if matches!( - self.current(), - TokenKind::Ident(keyword) if ident_eq(keyword, "as") - ) { - self.advance(); - let TokenKind::Ident(alias) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let alias = alias.clone(); - self.advance(); - alias - } else { - last_name_segment(&name).to_string() - }; - - match kind { - UseImportKind::Class => self.imports.insert_class(alias, name), - UseImportKind::Function => self.imports.insert_function(alias, name), - UseImportKind::Const => self.imports.insert_constant(alias, name), - } - Ok(()) - } - - /// Parses `global $name, $other;` declarations in eval fragments. - fn parse_global_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let mut vars = Vec::new(); - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - vars.push(name.clone()); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect_semicolon()?; - Ok(vec![EvalStmt::Global { vars }]) - } - - /// Parses `static $name = expr;` declarations in eval fragments. - fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let name = name.clone(); - self.advance(); - self.expect(TokenKind::Equal)?; - let init = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::StaticVar { name, init }]) - } - - /// Parses `throw expr;` statements in eval fragments. - fn parse_throw_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let expr = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::Throw(expr)]) - } - - /// Parses `try { ... } catch (Type|Other $name) { ... } finally { ... }` statements. - fn parse_try_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let body = self.parse_block()?; - let mut catches = Vec::new(); - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { - catches.push(self.parse_catch_clause()?); - } - let finally_body = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) - { - self.advance(); - self.parse_block()? - } else { - Vec::new() - }; - if catches.is_empty() && finally_body.is_empty() { - return Err(EvalParseError::UnexpectedToken); - } - Ok(vec![EvalStmt::Try { - body, - catches, - finally_body, - }]) - } - - /// Parses one `catch (ClassName|Other [$name]) { ... }` clause. - fn parse_catch_clause(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - let class_names = self.parse_catch_types()?; - let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { - let var_name = var_name.clone(); - self.advance(); - Some(var_name) - } else { - None - }; - self.expect(TokenKind::RParen)?; - let body = self.parse_block()?; - Ok(EvalCatch { - class_names, - var_name, - body, - }) - } - - /// Parses one or more unioned catch types in source order. - fn parse_catch_types(&mut self) -> Result, EvalParseError> { - let class_name = self.parse_qualified_name()?; - let mut class_names = vec![self.resolve_class_name(class_name)]; - while self.consume(TokenKind::Pipe) { - let class_name = self.parse_qualified_name()?; - class_names.push(self.resolve_class_name(class_name)); - } - Ok(class_names) - } - - /// Parses a dynamic function declaration parameter list after `(`. - fn parse_function_params(&mut self) -> Result, EvalParseError> { - let mut params = Vec::new(); - if self.consume(TokenKind::RParen) { - return Ok(params); - } - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - params.push(name.clone()); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - if matches!(self.current(), TokenKind::RParen) { - return Err(EvalParseError::ExpectedVariable); - } - } - self.expect(TokenKind::RParen)?; - Ok(params) - } - - /// Parses the optional first clause of a `for` loop. - fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::Semicolon) { - return Ok(Vec::new()); - } - self.parse_for_clause_stmt() - } - - /// Parses the optional update clause of a `for` loop. - fn parse_for_update_clause(&mut self) -> Result, EvalParseError> { - if self.consume(TokenKind::RParen) { - return Ok(Vec::new()); - } - let statements = self.parse_for_clause_stmt()?; - self.expect(TokenKind::RParen)?; - Ok(statements) - } - - /// Parses one statement-like `for` clause without consuming a delimiter. - fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { - match self.current() { - TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(false), - TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { - self.parse_array_set_clause(name.clone()) - } - TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { - self.parse_property_stmt(false) - } - TokenKind::DollarIdent(name) - if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => - { - self.parse_postfix_inc_dec_stmt(name.clone(), false) - } - TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { - let name = name.clone(); - self.parse_var_store_stmt(name, false) - } - _ => { - let expr = self.parse_expr()?; - Ok(vec![EvalStmt::Expr(expr)]) - } - } - } - - /// Parses `$name[index] = expr` and `$name[] = expr` in a `for` clause. - fn parse_array_set_clause(&mut self, name: String) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LBracket)?; - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) - } - - /// Parses `$name = expr` and simple variable compound assignments. - fn parse_var_store_stmt( - &mut self, - name: String, - require_semicolon: bool, - ) -> Result, EvalParseError> { - self.advance(); - let Some(op) = assignment_op(self.current()) else { - return Err(EvalParseError::UnexpectedToken); - }; - self.advance(); - if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { - self.advance(); - let TokenKind::DollarIdent(source) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let source = source.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::ReferenceAssign { - target: name, - source, - }]); - } - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - let value = assignment_value(&name, op, value); - Ok(vec![EvalStmt::StoreVar { name, value }]) - } - - /// Parses prefix `++$name` and `--$name` as simple statement effects. - fn parse_prefix_inc_dec_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let name = name.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![inc_dec_store(name, increment)]) - } - - /// Parses postfix `$name++` and `$name--` as simple statement effects. - fn parse_postfix_inc_dec_stmt( - &mut self, - name: String, - require_semicolon: bool, - ) -> Result, EvalParseError> { - self.advance(); - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![inc_dec_store(name, increment)]) - } - - /// Parses `$object->property` as either an expression statement or property write. - fn parse_property_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let target = self.parse_expr()?; - if !self.consume(TokenKind::Equal) { - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::Expr(target)]); - } - let EvalExpr::PropertyGet { object, property } = target else { - return Err(EvalParseError::UnexpectedToken); - }; - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![EvalStmt::PropertySet { - object: *object, - property, - value, - }]) - } - - /// Parses a complete `if` statement after consuming the `if` keyword. - fn parse_if_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - Ok(vec![self.parse_if_after_keyword()?]) - } - - /// Parses the condition, then block, and optional else branch for an `if` chain. - fn parse_if_after_keyword(&mut self) -> Result { - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - let then_branch = self.parse_statement_body()?; - let else_branch = self.parse_optional_else_branch()?; - Ok(EvalStmt::If { - condition, - then_branch, - else_branch, - }) - } - - /// Parses `elseif`, `else if`, or `else` branches after an `if` body. - fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { - self.advance(); - return Ok(vec![self.parse_if_after_keyword()?]); - } - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "else")) { - return Ok(Vec::new()); - } - self.advance(); - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "if")) { - self.advance(); - Ok(vec![self.parse_if_after_keyword()?]) - } else { - self.parse_statement_body() - } - } - - /// Parses `switch (expr) { case expr: ... default: ... }`. - fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let expr = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect(TokenKind::LBrace)?; - let mut cases = Vec::new(); - while !matches!(self.current(), TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - cases.push(self.parse_switch_case()?); - } - self.expect(TokenKind::RBrace)?; - Ok(vec![EvalStmt::Switch { expr, cases }]) - } - - /// Parses one `case` or `default` arm inside a switch body. - fn parse_switch_case(&mut self) -> Result { - let condition = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) - { - self.advance(); - let condition = self.parse_expr()?; - Some(condition) - } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { - self.advance(); - None - } else { - return Err(EvalParseError::UnexpectedToken); - }; - self.expect(TokenKind::Colon)?; - let body = self.parse_switch_case_body()?; - Ok(EvalSwitchCase { condition, body }) - } - - /// Parses case body statements until the next case boundary or switch close. - fn parse_switch_case_body(&mut self) -> Result, EvalParseError> { - let mut body = Vec::new(); - while !is_switch_case_boundary(self.current()) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - body.extend(self.parse_stmt()?); - } - Ok(body) - } - - /// Parses `unset($name[, ...]);`. - fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let mut statements = Vec::new(); - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - statements.push(EvalStmt::UnsetVar { name: name.clone() }); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect(TokenKind::RParen)?; - self.expect_semicolon()?; - Ok(statements) - } - - /// Parses `while (expr) { ... }`. - fn parse_while_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::While { condition, body }]) - } - - /// Parses either a brace-delimited block or one braceless statement body. - fn parse_statement_body(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::LBrace) { - self.parse_block() - } else { - self.parse_nested_stmt() - } - } - - /// Parses a brace-delimited statement block. - fn parse_block(&mut self) -> Result, EvalParseError> { - self.expect(TokenKind::LBrace)?; - self.parse_nested_block_contents() - } - - /// Parses one nested statement where import declarations are not legal. - fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.allow_use_imports, false); - let result = self.parse_stmt(); - self.allow_use_imports = previous; - result - } - - /// Parses a nested block while preserving active imports for name resolution. - fn parse_nested_block_contents(&mut self) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.allow_use_imports, false); - let result = self.parse_block_contents(); - self.allow_use_imports = previous; - result - } - - /// Parses statements until the closing brace for the current block. - fn parse_block_contents(&mut self) -> Result, EvalParseError> { - let mut statements = Vec::new(); - while !matches!(self.current(), TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - statements.extend(self.parse_stmt()?); - } - self.expect(TokenKind::RBrace)?; - Ok(statements) - } - - /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. - fn parse_expr(&mut self) -> Result { - self.parse_keyword_or() - } - - /// Parses PHP keyword `or`, whose precedence is lower than `xor`, `and`, and ternary. - fn parse_keyword_or(&mut self) -> Result { - let mut expr = self.parse_keyword_xor()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "or")) { - self.advance(); - let right = self.parse_keyword_xor()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP keyword `xor`, whose operands are evaluated before boolean XOR. - fn parse_keyword_xor(&mut self) -> Result { - let mut expr = self.parse_keyword_and()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "xor")) { - self.advance(); - let right = self.parse_keyword_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalXor, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP keyword `and`, whose precedence is lower than ternary and `&&`. - fn parse_keyword_and(&mut self) -> Result { - let mut expr = self.parse_ternary()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "and")) { - self.advance(); - let right = self.parse_ternary()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. - fn parse_ternary(&mut self) -> Result { - let condition = self.parse_null_coalesce()?; - if !self.consume(TokenKind::Question) { - return Ok(condition); - } - let then_branch = if self.consume(TokenKind::Colon) { - None - } else { - let expr = self.parse_expr()?; - self.expect(TokenKind::Colon)?; - Some(Box::new(expr)) - }; - let else_branch = self.parse_expr()?; - Ok(EvalExpr::Ternary { - condition: Box::new(condition), - then_branch, - else_branch: Box::new(else_branch), - }) - } - - /// Parses right-associative null coalescing below logical OR and above ternary. - fn parse_null_coalesce(&mut self) -> Result { - let value = self.parse_logical_or()?; - if !self.consume(TokenKind::QuestionQuestion) { - return Ok(value); - } - let default = self.parse_null_coalesce()?; - Ok(EvalExpr::NullCoalesce { - value: Box::new(value), - default: Box::new(default), - }) - } - - /// Parses left-associative logical OR with lower precedence than logical AND. - fn parse_logical_or(&mut self) -> Result { - let mut expr = self.parse_logical_and()?; - while self.consume(TokenKind::OrOr) { - let right = self.parse_logical_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative logical AND with lower precedence than equality. - fn parse_logical_and(&mut self) -> Result { - let mut expr = self.parse_bit_or()?; - while self.consume(TokenKind::AndAnd) { - let right = self.parse_bit_or()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise OR with lower precedence than bitwise XOR. - fn parse_bit_or(&mut self) -> Result { - let mut expr = self.parse_bit_xor()?; - while self.consume(TokenKind::Pipe) { - let right = self.parse_bit_xor()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise XOR with lower precedence than bitwise AND. - fn parse_bit_xor(&mut self) -> Result { - let mut expr = self.parse_bit_and()?; - while self.consume(TokenKind::Caret) { - let right = self.parse_bit_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitXor, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise AND with lower precedence than equality. - fn parse_bit_and(&mut self) -> Result { - let mut expr = self.parse_equality()?; - while self.consume(TokenKind::Ampersand) { - let right = self.parse_equality()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative equality and inequality comparisons. - fn parse_equality(&mut self) -> Result { - let mut expr = self.parse_ordering()?; - loop { - let op = if self.consume(TokenKind::EqualEqual) { - EvalBinOp::LooseEq - } else if self.consume(TokenKind::NotEqual) { - EvalBinOp::LooseNotEq - } else if self.consume(TokenKind::EqualEqualEqual) { - EvalBinOp::StrictEq - } else if self.consume(TokenKind::NotEqualEqual) { - EvalBinOp::StrictNotEq - } else { - break; - }; - let right = self.parse_ordering()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative ordered comparisons. - fn parse_ordering(&mut self) -> Result { - let mut expr = self.parse_shift()?; - loop { - let op = if self.consume(TokenKind::Less) { - EvalBinOp::Lt - } else if self.consume(TokenKind::LessEqual) { - EvalBinOp::LtEq - } else if self.consume(TokenKind::Greater) { - EvalBinOp::Gt - } else if self.consume(TokenKind::GreaterEqual) { - EvalBinOp::GtEq - } else if self.consume(TokenKind::Spaceship) { - EvalBinOp::Spaceship - } else { - break; - }; - let right = self.parse_shift()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative integer shift operators. - fn parse_shift(&mut self) -> Result { - let mut expr = self.parse_concat()?; - loop { - let op = if self.consume(TokenKind::LessLess) { - EvalBinOp::ShiftLeft - } else if self.consume(TokenKind::GreaterGreater) { - EvalBinOp::ShiftRight - } else { - break; - }; - let right = self.parse_concat()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative string concatenation. - fn parse_concat(&mut self) -> Result { - let mut expr = self.parse_add()?; - while self.consume(TokenKind::Dot) { - let right = self.parse_add()?; - expr = EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative numeric addition and subtraction. - fn parse_add(&mut self) -> Result { - let mut expr = self.parse_mul()?; - loop { - let op = if self.consume(TokenKind::Plus) { - EvalBinOp::Add - } else if self.consume(TokenKind::Minus) { - EvalBinOp::Sub - } else { - break; - }; - let right = self.parse_mul()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative numeric multiplication, division, and modulo. - fn parse_mul(&mut self) -> Result { - let mut expr = self.parse_unary()?; - loop { - let op = if self.consume(TokenKind::Star) { - EvalBinOp::Mul - } else if self.consume(TokenKind::Slash) { - EvalBinOp::Div - } else if self.consume(TokenKind::Percent) { - EvalBinOp::Mod - } else { - break; - }; - let right = self.parse_unary()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses right-associative unary prefix expressions. - fn parse_unary(&mut self) -> Result { - if self.consume(TokenKind::Plus) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::Plus, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Minus) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Bang) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::LogicalNot, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Tilde) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::BitNot, - expr: Box::new(expr), - }); - } - self.parse_power() - } - - /// Parses right-associative exponentiation with higher precedence than unary prefix operators. - fn parse_power(&mut self) -> Result { - let mut expr = self.parse_postfix()?; - if self.consume(TokenKind::StarStar) { - let right = self.parse_unary()?; - expr = EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses postfix array reads, property reads, method calls, and dynamic calls. - fn parse_postfix(&mut self) -> Result { - let mut expr = self.parse_primary()?; - loop { - if matches!(self.current(), TokenKind::LParen) { - let args = self.parse_call_args()?; - expr = EvalExpr::DynamicCall { - callee: Box::new(expr), - args, - }; - continue; - } - if self.consume(TokenKind::LBracket) { - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - expr = EvalExpr::ArrayGet { - array: Box::new(expr), - index: Box::new(index), - }; - continue; - } - if self.consume(TokenKind::Arrow) { - let TokenKind::Ident(member) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let member = member.clone(); - self.advance(); - if matches!(self.current(), TokenKind::LParen) { - let args = self.parse_call_args()?; - expr = EvalExpr::MethodCall { - object: Box::new(expr), - method: member.to_ascii_lowercase(), - args, - }; - } else { - expr = EvalExpr::PropertyGet { - object: Box::new(expr), - property: member, - }; - } - continue; - } - break; - } - Ok(expr) - } - - /// Parses primary expressions supported by the initial eval subset. - fn parse_primary(&mut self) -> Result { - match self.current() { - TokenKind::Int(value) => { - let value = *value; - self.advance(); - Ok(EvalExpr::Const(EvalConst::Int(value))) - } - TokenKind::Float(value) => { - let value = *value; - self.advance(); - Ok(EvalExpr::Const(EvalConst::Float(value))) - } - TokenKind::String(value) => { - let value = value.clone(); - self.advance(); - Ok(EvalExpr::Const(EvalConst::String(value))) - } - TokenKind::DollarIdent(name) => { - let name = name.clone(); - self.advance(); - Ok(EvalExpr::LoadVar(name)) - } - TokenKind::Magic(EvalMagicConst::Namespace) => { - let namespace = self.namespace.clone(); - self.advance(); - Ok(EvalExpr::Const(EvalConst::String(namespace))) - } - TokenKind::Magic(magic) => { - let magic = magic.clone(); - self.advance(); - Ok(EvalExpr::Magic(magic)) - } - TokenKind::Ident(name) if ident_eq(name, "null") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Null)) - } - TokenKind::Ident(name) if ident_eq(name, "true") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Bool(true))) - } - TokenKind::Ident(name) if ident_eq(name, "false") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Bool(false))) - } - TokenKind::Ident(name) if ident_eq(name, "print") => { - self.advance(); - let expr = self.parse_expr()?; - Ok(EvalExpr::Print(Box::new(expr))) - } - TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { - self.parse_legacy_array_literal() - } - TokenKind::Ident(name) if is_include_construct_name(name) => self.parse_include_expr(), - TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), - TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), - TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { - Err(EvalParseError::UnsupportedConstruct) - } - TokenKind::Backslash => self.parse_qualified_name_expr(), - TokenKind::Ident(_) if matches!(self.peek(), TokenKind::Backslash) => { - self.parse_qualified_name_expr() - } - TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { - self.parse_call_expr(name.clone()) - } - TokenKind::Ident(name) => { - let name = name.clone(); - self.advance(); - Ok(self.const_fetch_expr(name)) - } - TokenKind::LBracket => self.parse_array_literal(), - TokenKind::LParen => { - self.advance(); - let expr = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - Ok(expr) - } - TokenKind::Eof => Err(EvalParseError::UnexpectedEof), - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Parses PHP include/require expression constructs and their path expression. - fn parse_include_expr(&mut self) -> Result { - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let required = ident_eq(name, "require") || ident_eq(name, "require_once"); - let once = ident_eq(name, "include_once") || ident_eq(name, "require_once"); - self.advance(); - let path = if self.consume(TokenKind::LParen) { - let path = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - path - } else { - self.parse_expr()? - }; - Ok(EvalExpr::Include { - path: Box::new(path), - required, - once, - }) - } - - /// Parses `match (expr) { pattern, other => value, default => fallback }`. - fn parse_match_expr(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - let subject = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect(TokenKind::LBrace)?; - - let mut arms = Vec::new(); - let mut default = None; - while !self.consume(TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { - self.advance(); - self.expect(TokenKind::FatArrow)?; - default = Some(Box::new(self.parse_expr()?)); - } else { - arms.push(self.parse_match_arm()?); - } - if self.consume(TokenKind::Comma) { - continue; - } - self.expect(TokenKind::RBrace)?; - break; - } - - Ok(EvalExpr::Match { - subject: Box::new(subject), - arms, - default, - }) - } - - /// Parses one non-default `match` arm and its comma-separated pattern list. - fn parse_match_arm(&mut self) -> Result { - let mut patterns = Vec::new(); - loop { - patterns.push(self.parse_expr()?); - if !self.consume(TokenKind::Comma) { - break; - } - if matches!(self.current(), TokenKind::FatArrow) { - return Err(EvalParseError::UnexpectedToken); - } - if matches!(self.current(), TokenKind::Eof | TokenKind::RBrace) { - return Err(EvalParseError::UnexpectedToken); - } - } - self.expect(TokenKind::FatArrow)?; - let value = self.parse_expr()?; - Ok(EvalMatchArm { patterns, value }) - } - - /// Parses a function-like call expression and its source-order arguments. - fn parse_call_expr(&mut self, name: String) -> Result { - self.advance(); - let args = self.parse_call_args()?; - Ok(self.call_expr(name, args)) - } - - /// Parses an explicitly qualified call or constant-fetch expression. - fn parse_qualified_name_expr(&mut self) -> Result { - let name = self.parse_qualified_name()?; - let name = self.resolve_qualified_name(name); - if matches!(self.current(), TokenKind::LParen) { - let args = self.parse_call_args()?; - return Ok(EvalExpr::Call { - name: name.to_ascii_lowercase(), - args, - }); - } - Ok(EvalExpr::ConstFetch(name)) - } - - /// Parses `new ClassName(...)` expressions in eval fragments. - fn parse_new_object_expr(&mut self) -> Result { - self.advance(); - let class_name = self.parse_qualified_name()?; - let class_name = self.resolve_class_name(class_name); - let args = self.parse_call_args()?; - Ok(EvalExpr::NewObject { class_name, args }) - } - - /// Parses a simple or explicitly qualified PHP name. - fn parse_qualified_name(&mut self) -> Result { - let absolute = self.consume(TokenKind::Backslash); - let TokenKind::Ident(first) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let mut name = first.clone(); - self.advance(); - while self.consume(TokenKind::Backslash) { - let TokenKind::Ident(part) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - name.push('\\'); - name.push_str(part); - self.advance(); - } - Ok(ParsedQualifiedName { name, absolute }) - } - - /// Builds a call expression, adding namespace fallback for unqualified names. - fn call_expr(&self, name: String, args: Vec) -> EvalExpr { - if let Some(imported) = self.imports.resolve_function(&name) { - return EvalExpr::Call { - name: imported.to_ascii_lowercase(), - args, - }; - } - let fallback_name = name.to_ascii_lowercase(); - if self.namespace.is_empty() { - EvalExpr::Call { - name: fallback_name, - args, - } - } else { - EvalExpr::NamespacedCall { - name: self - .qualify_name_in_current_namespace(&name) - .to_ascii_lowercase(), - fallback_name, - args, - } - } - } - - /// Builds a constant fetch expression, adding namespace fallback for unqualified names. - fn const_fetch_expr(&self, name: String) -> EvalExpr { - if let Some(imported) = self.imports.resolve_constant(&name) { - return EvalExpr::ConstFetch(imported.to_string()); - } - if self.namespace.is_empty() { - EvalExpr::ConstFetch(name) - } else { - EvalExpr::NamespacedConstFetch { - name: self.qualify_name_in_current_namespace(&name), - fallback_name: name, - } - } - } - - /// Prefixes a name with the parser's current namespace when one is active. - fn qualify_name_in_current_namespace(&self, name: &str) -> String { - if self.namespace.is_empty() { - name.to_string() - } else { - format!("{}\\{}", self.namespace, name) - } - } - - /// Resolves a class name through active imports before namespace qualification. - fn resolve_class_name(&self, name: ParsedQualifiedName) -> String { - if name.absolute { - return name.name; - } - if let Some(imported) = self.imports.resolve_class(&name.name) { - return imported; - } - self.resolve_qualified_name(name) - } - - /// Resolves a parsed PHP name according to the current namespace. - fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { - if name.absolute || self.namespace.is_empty() { - name.name - } else { - self.qualify_name_in_current_namespace(&name.name) - } - } - - /// Parses a parenthesized source-order argument list. - fn parse_call_args(&mut self) -> Result, EvalParseError> { - self.expect(TokenKind::LParen)?; - let mut args = Vec::new(); - if self.consume(TokenKind::RParen) { - return Ok(args); - } - loop { - args.push(self.parse_call_arg()?); - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(TokenKind::RParen) { - return Ok(args); - } - } - self.expect(TokenKind::RParen)?; - Ok(args) - } - - /// Parses one positional or named argument within a call argument list. - fn parse_call_arg(&mut self) -> Result { - if self.consume(TokenKind::Ellipsis) { - return self.parse_expr().map(EvalCallArg::spread); - } - if matches!(self.peek(), TokenKind::Colon) { - if let TokenKind::Ident(name) = self.current() { - let name = name.clone(); - self.advance(); - self.expect(TokenKind::Colon)?; - let value = self.parse_expr()?; - return Ok(EvalCallArg::named(name, value)); - } - } - self.parse_expr().map(EvalCallArg::positional) - } - - /// Parses an array literal with source-order optional key/value element expressions. - fn parse_array_literal(&mut self) -> Result { - self.expect(TokenKind::LBracket)?; - self.parse_array_elements_until(TokenKind::RBracket) - } - - /// Parses PHP's legacy `array(...)` literal into the same EvalIR node as `[...]`. - fn parse_legacy_array_literal(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - self.parse_array_elements_until(TokenKind::RParen) - } - - /// Returns whether the current token starts PHP's legacy `array(...)` literal syntax. - fn current_starts_legacy_array_literal(&self) -> bool { - matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "array")) - && matches!(self.peek(), TokenKind::LParen) - } - - /// Parses comma-separated array elements until the supplied closing delimiter. - fn parse_array_elements_until(&mut self, close: TokenKind) -> Result { - let mut elements = Vec::new(); - if self.consume(close.clone()) { - return Ok(EvalExpr::Array(elements)); - } - loop { - let first = self.parse_expr()?; - if self.consume(TokenKind::FatArrow) { - let value = self.parse_expr()?; - elements.push(EvalArrayElement::KeyValue { key: first, value }); - } else { - elements.push(EvalArrayElement::Value(first)); - } - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(close.clone()) { - return Ok(EvalExpr::Array(elements)); - } - } - self.expect(close)?; - Ok(EvalExpr::Array(elements)) - } - - /// Consumes `expected` or returns a parse error. - fn expect(&mut self, expected: TokenKind) -> Result<(), EvalParseError> { - if self.consume(expected) { - Ok(()) - } else { - Err(EvalParseError::UnexpectedToken) - } - } - - /// Consumes a semicolon or returns the semicolon-specific parse error. - fn expect_semicolon(&mut self) -> Result<(), EvalParseError> { - if self.consume_semicolon() { - Ok(()) - } else { - Err(EvalParseError::ExpectedSemicolon) - } - } - - /// Consumes a semicolon if present. - fn consume_semicolon(&mut self) -> bool { - self.consume(TokenKind::Semicolon) - } - - /// Consumes `expected` if the current token matches it. - fn consume(&mut self, expected: TokenKind) -> bool { - if *self.current() == expected { - self.advance(); - true - } else { - false - } - } - - /// Returns the current token. - fn current(&self) -> &TokenKind { - self.tokens.get(self.pos).unwrap_or(&TokenKind::Eof) - } - - /// Returns the next token without advancing. - fn peek(&self) -> &TokenKind { - self.tokens.get(self.pos + 1).unwrap_or(&TokenKind::Eof) - } - - /// Advances to the next token. - fn advance(&mut self) { - if self.pos < self.tokens.len() { - self.pos += 1; - } - } -} -/// Returns true when the current token closes or starts a switch case arm. -fn is_switch_case_boundary(token: &TokenKind) -> bool { - matches!(token, TokenKind::RBrace) - || matches!(token, TokenKind::Ident(name) if ident_eq(name, "case") || ident_eq(name, "default")) -} - -/// Maps simple variable assignment tokens to an optional compound EvalIR operator. -fn assignment_op(token: &TokenKind) -> Option> { - match token { - TokenKind::Equal => Some(None), - TokenKind::PlusEqual => Some(Some(EvalBinOp::Add)), - TokenKind::MinusEqual => Some(Some(EvalBinOp::Sub)), - TokenKind::StarEqual => Some(Some(EvalBinOp::Mul)), - TokenKind::StarStarEqual => Some(Some(EvalBinOp::Pow)), - TokenKind::SlashEqual => Some(Some(EvalBinOp::Div)), - TokenKind::PercentEqual => Some(Some(EvalBinOp::Mod)), - TokenKind::AmpEqual => Some(Some(EvalBinOp::BitAnd)), - TokenKind::PipeEqual => Some(Some(EvalBinOp::BitOr)), - TokenKind::CaretEqual => Some(Some(EvalBinOp::BitXor)), - TokenKind::LessLessEqual => Some(Some(EvalBinOp::ShiftLeft)), - TokenKind::GreaterGreaterEqual => Some(Some(EvalBinOp::ShiftRight)), - TokenKind::DotEqual => Some(Some(EvalBinOp::Concat)), - _ => None, - } -} - -/// Builds the assigned value expression for plain and compound variable assignment. -fn assignment_value(name: &str, op: Option, value: EvalExpr) -> EvalExpr { - match op { - Some(op) => EvalExpr::Binary { - op, - left: Box::new(EvalExpr::LoadVar(name.to_string())), - right: Box::new(value), - }, - None => value, - } -} - -/// Builds the StoreVar statement for a simple variable increment or decrement. -pub(super) fn inc_dec_store(name: String, increment: bool) -> EvalStmt { - EvalStmt::StoreVar { - value: EvalExpr::Binary { - op: if increment { - EvalBinOp::Add - } else { - EvalBinOp::Sub - }, - left: Box::new(EvalExpr::LoadVar(name.clone())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - name, - } -} - -/// Compares a source identifier to a PHP keyword using ASCII case-insensitive rules. -fn ident_eq(actual: &str, expected: &str) -> bool { - actual.eq_ignore_ascii_case(expected) -} - -/// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. -fn is_unsupported_statement_keyword(name: &str) -> bool { - ["enum", "interface", "trait"] - .iter() - .any(|keyword| ident_eq(name, keyword)) -} - -/// Returns true for class member modifiers outside the current eval class subset. -fn is_unsupported_class_member_modifier(name: &str) -> bool { - ["private", "protected", "static", "abstract", "final"] - .iter() - .any(|modifier| ident_eq(name, modifier)) -} - -/// Returns true when an identifier is an include/require expression construct. -fn is_include_construct_name(name: &str) -> bool { - ["include", "include_once", "require", "require_once"] - .iter() - .any(|keyword| ident_eq(name, keyword)) -} - -/// Returns the first namespace segment and the optional remaining suffix. -fn split_first_name_segment(name: &str) -> (&str, Option<&str>) { - name.split_once('\\') - .map_or((name, None), |(first, tail)| (first, Some(tail))) -} - -/// Returns the final segment of a PHP qualified name. -fn last_name_segment(name: &str) -> &str { - name.rsplit('\\').next().unwrap_or(name) -} - -/// Combines a grouped use prefix with one relative member name. -fn join_grouped_use_name(prefix: &str, member: &str) -> String { - format!("{prefix}\\{member}") -} - -/// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. -fn is_unsupported_expression_keyword(name: &str) -> bool { - ["clone", "yield"] - .iter() - .any(|keyword| ident_eq(name, keyword)) } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs new file mode 100644 index 0000000000..8f9ca22e0c --- /dev/null +++ b/crates/elephc-eval/src/parser/statements.rs @@ -0,0 +1,931 @@ +//! Purpose: +//! Parses PHP eval statements, declarations, control structures, and statement blocks into EvalIR. +//! +//! Called from: +//! - `crate::parser::state::Parser::parse_program()`. +//! +//! Key details: +//! - Statement parsing expands multi-variable constructs such as `unset($a, $b)` into multiple EvalIR statements. +//! - Namespace/use parsing lives here because declarations are statement-level syntax in PHP. + +use super::cursor::*; +use super::state::*; +use crate::errors::EvalParseError; +use crate::eval_ir::{ + EvalCatch, EvalClass, EvalClassMethod, EvalClassProperty, EvalExpr, EvalStmt, EvalSwitchCase, +}; +use crate::lexer::TokenKind; + +impl Parser { + /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. + pub(super) fn parse_stmt(&mut self) -> Result, EvalParseError> { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "break") => { + self.advance(); + self.expect_semicolon()?; + Ok(vec![EvalStmt::Break]) + } + TokenKind::Ident(name) if ident_eq(name, "continue") => { + self.advance(); + self.expect_semicolon()?; + Ok(vec![EvalStmt::Continue]) + } + TokenKind::Ident(name) if ident_eq(name, "do") => self.parse_do_while_stmt(), + TokenKind::Ident(name) if ident_eq(name, "echo") => { + self.advance(); + let mut statements = vec![EvalStmt::Echo(self.parse_expr()?)]; + while self.consume(TokenKind::Comma) { + statements.push(EvalStmt::Echo(self.parse_expr()?)); + } + self.expect_semicolon()?; + Ok(statements) + } + TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), + TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), + TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), + TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), + TokenKind::Ident(name) if ident_eq(name, "namespace") => self.parse_namespace_stmt(), + TokenKind::Ident(name) if ident_eq(name, "return") => { + self.advance(); + if self.consume_semicolon() { + return Ok(vec![EvalStmt::Return(None)]); + } + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Return(Some(expr))]) + } + TokenKind::Ident(name) if ident_eq(name, "static") => self.parse_static_var_stmt(), + TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), + TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), + TokenKind::Ident(name) if ident_eq(name, "try") => self.parse_try_stmt(), + TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), + TokenKind::Ident(name) if ident_eq(name, "use") && self.allow_use_imports => { + self.parse_use_stmt() + } + TokenKind::Ident(name) if ident_eq(name, "use") => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::Ident(name) if ident_eq(name, "while") => self.parse_while_stmt(), + TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(true) + } + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_stmt(name.clone()) + } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), true) + } + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { + let name = name.clone(); + self.parse_var_store_stmt(name, true) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => { + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Expr(expr)]) + } + } + } + + /// Parses `do { ... } while (expr);`. + pub(super) fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_statement_body()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::DoWhile { body, condition }]) + } + + /// Parses `$name[index] = expr;` and `$name[] = expr;` eval writes. + pub(super) fn parse_array_set_stmt( + &mut self, + name: String, + ) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `for (init; condition; update) { ... }`. + pub(super) fn parse_for_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let init = self.parse_for_init_clause()?; + self.expect_semicolon()?; + let condition = if matches!(self.current(), TokenKind::Semicolon) { + None + } else { + Some(self.parse_expr()?) + }; + self.expect_semicolon()?; + let update = self.parse_for_update_clause()?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::For { + init, + condition, + update, + body, + }]) + } + + /// Parses `foreach (expr as $value) { ... }` or `foreach (expr as $key => $value) { ... }`. + pub(super) fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let array = self.parse_expr()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "as")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + let TokenKind::DollarIdent(value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let value_name = value_name.clone(); + self.advance(); + let (key_name, value_name) = if matches!(self.current(), TokenKind::FatArrow) { + self.advance(); + let TokenKind::DollarIdent(next_value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let key_name = value_name; + let value_name = next_value_name.clone(); + self.advance(); + (Some(key_name), value_name) + } else { + (None, value_name) + }; + self.expect(TokenKind::RParen)?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::Foreach { + array, + key_name, + value_name, + body, + }]) + } + + /// Parses `class Name { ... }` declarations for dynamic class metadata. + pub(super) fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + self.expect(TokenKind::LBrace)?; + let mut properties = Vec::new(); + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_class_member(&mut properties, &mut methods)?; + } + self.consume_semicolon(); + Ok(vec![EvalStmt::ClassDecl(EvalClass::new( + name, properties, methods, + ))]) + } + + /// Parses one public property or method from an eval class body. + pub(super) fn parse_class_member( + &mut self, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + let public = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) + { + self.advance(); + true + } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) + { + return Err(EvalParseError::UnsupportedConstruct); + } else { + false + }; + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + methods.push(self.parse_class_method_decl()?); + return Ok(()); + } + + if !public { + return Err(EvalParseError::UnsupportedConstruct); + } + properties.push(self.parse_class_property_decl()?); + Ok(()) + } + + /// Parses `function name($param, ...) { ... }` inside a dynamic eval class. + pub(super) fn parse_class_method_decl(&mut self) -> Result { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let params = self.parse_function_params()?; + let body = self.parse_block()?; + Ok(EvalClassMethod::new(name, params, body)) + } + + /// Parses one public property declaration with an optional initializer. + pub(super) fn parse_class_property_decl( + &mut self, + ) -> Result { + self.skip_optional_property_type()?; + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let default = if self.consume(TokenKind::Equal) { + Some(self.parse_expr()?) + } else { + None + }; + self.expect_semicolon()?; + Ok(EvalClassProperty::new(name, default)) + } + + /// Consumes a simple declared property type before the `$property` token. + pub(super) fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::DollarIdent(_)) { + return Ok(()); + } + if self.consume(TokenKind::Question) && matches!(self.current(), TokenKind::DollarIdent(_)) + { + return Err(EvalParseError::UnexpectedToken); + } + match self.current() { + TokenKind::Ident(_) | TokenKind::Backslash => { + let _ = self.parse_qualified_name()?; + Ok(()) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Parses `function name($param, ...) { ... }` declarations. + pub(super) fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + self.expect(TokenKind::LParen)?; + let params = self.parse_function_params()?; + let body = self.parse_block()?; + Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) + } + + /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. + pub(super) fn parse_namespace_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let namespace = if self.consume(TokenKind::LBrace) { + return self.parse_namespace_block(String::new()); + } else { + self.parse_namespace_name()? + }; + if self.consume_semicolon() { + self.namespace = namespace; + self.imports = NamespaceImports::default(); + return Ok(Vec::new()); + } + self.expect(TokenKind::LBrace)?; + self.parse_namespace_block(namespace) + } + + /// Parses statements inside an already opened namespace block. + pub(super) fn parse_namespace_block( + &mut self, + namespace: String, + ) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.namespace, namespace); + let previous_imports = std::mem::take(&mut self.imports); + let previous_allow_use_imports = std::mem::replace(&mut self.allow_use_imports, true); + let result = self.parse_block_contents(); + self.namespace = previous; + self.imports = previous_imports; + self.allow_use_imports = previous_allow_use_imports; + result + } + + /// Parses a namespace declaration name without a leading global separator. + pub(super) fn parse_namespace_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses PHP `use`, `use function`, and `use const` import declarations. + pub(super) fn parse_use_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let kind = self.parse_use_import_kind(); + + loop { + self.parse_use_import(kind)?; + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(Vec::new()) + } + + /// Parses an optional top-level `function` or `const` use-import kind. + pub(super) fn parse_use_import_kind(&mut self) -> UseImportKind { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + self.advance(); + UseImportKind::Function + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + self.advance(); + UseImportKind::Const + } else { + UseImportKind::Class + } + } + + /// Parses and registers one comma-separated import entry. + pub(super) fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { + let (name, grouped) = self.parse_use_name_or_group_start()?; + if grouped { + return self.parse_grouped_use_imports(kind, name); + } + self.parse_use_alias_and_register(kind, name) + } + + /// Parses a use-import name, stopping after a trailing namespace separator before `{`. + pub(super) fn parse_use_name_or_group_start( + &mut self, + ) -> Result<(String, bool), EvalParseError> { + let _ = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + if self.consume(TokenKind::LBrace) { + return Ok((name, true)); + } + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok((name, false)) + } + + /// Parses all members inside a grouped namespace import declaration. + pub(super) fn parse_grouped_use_imports( + &mut self, + default_kind: UseImportKind, + prefix: String, + ) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + loop { + let kind = self.parse_grouped_use_entry_kind(default_kind)?; + let member = self.parse_grouped_use_member_name()?; + let name = join_grouped_use_name(&prefix, &member); + self.parse_use_alias_and_register(kind, name)?; + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RBrace) { + return Ok(()); + } + } + self.expect(TokenKind::RBrace) + } + + /// Parses an optional per-entry grouped import kind, matching PHP's mixed group rules. + pub(super) fn parse_grouped_use_entry_kind( + &mut self, + default_kind: UseImportKind, + ) -> Result { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Function); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Const); + } + Ok(default_kind) + } + + /// Parses one non-absolute member name inside a grouped use declaration. + pub(super) fn parse_grouped_use_member_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses an optional alias and stores one namespace import. + pub(super) fn parse_use_alias_and_register( + &mut self, + kind: UseImportKind, + name: String, + ) -> Result<(), EvalParseError> { + let alias = if matches!( + self.current(), + TokenKind::Ident(keyword) if ident_eq(keyword, "as") + ) { + self.advance(); + let TokenKind::Ident(alias) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let alias = alias.clone(); + self.advance(); + alias + } else { + last_name_segment(&name).to_string() + }; + + match kind { + UseImportKind::Class => self.imports.insert_class(alias, name), + UseImportKind::Function => self.imports.insert_function(alias, name), + UseImportKind::Const => self.imports.insert_constant(alias, name), + } + Ok(()) + } + + /// Parses `global $name, $other;` declarations in eval fragments. + pub(super) fn parse_global_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let mut vars = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + vars.push(name.clone()); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(vec![EvalStmt::Global { vars }]) + } + + /// Parses `static $name = expr;` declarations in eval fragments. + pub(super) fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let init = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::StaticVar { name, init }]) + } + + /// Parses `throw expr;` statements in eval fragments. + pub(super) fn parse_throw_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Throw(expr)]) + } + + /// Parses `try { ... } catch (Type|Other $name) { ... } finally { ... }` statements. + pub(super) fn parse_try_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_block()?; + let mut catches = Vec::new(); + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { + catches.push(self.parse_catch_clause()?); + } + let finally_body = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) + { + self.advance(); + self.parse_block()? + } else { + Vec::new() + }; + if catches.is_empty() && finally_body.is_empty() { + return Err(EvalParseError::UnexpectedToken); + } + Ok(vec![EvalStmt::Try { + body, + catches, + finally_body, + }]) + } + + /// Parses one `catch (ClassName|Other [$name]) { ... }` clause. + pub(super) fn parse_catch_clause(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let class_names = self.parse_catch_types()?; + let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { + let var_name = var_name.clone(); + self.advance(); + Some(var_name) + } else { + None + }; + self.expect(TokenKind::RParen)?; + let body = self.parse_block()?; + Ok(EvalCatch { + class_names, + var_name, + body, + }) + } + + /// Parses one or more unioned catch types in source order. + pub(super) fn parse_catch_types(&mut self) -> Result, EvalParseError> { + let class_name = self.parse_qualified_name()?; + let mut class_names = vec![self.resolve_class_name(class_name)]; + while self.consume(TokenKind::Pipe) { + let class_name = self.parse_qualified_name()?; + class_names.push(self.resolve_class_name(class_name)); + } + Ok(class_names) + } + + /// Parses a dynamic function declaration parameter list after `(`. + pub(super) fn parse_function_params(&mut self) -> Result, EvalParseError> { + let mut params = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(params); + } + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + params.push(name.clone()); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok(params) + } + + /// Parses the optional first clause of a `for` loop. + pub(super) fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Semicolon) { + return Ok(Vec::new()); + } + self.parse_for_clause_stmt() + } + + /// Parses the optional update clause of a `for` loop. + pub(super) fn parse_for_update_clause(&mut self) -> Result, EvalParseError> { + if self.consume(TokenKind::RParen) { + return Ok(Vec::new()); + } + let statements = self.parse_for_clause_stmt()?; + self.expect(TokenKind::RParen)?; + Ok(statements) + } + + /// Parses one statement-like `for` clause without consuming a delimiter. + pub(super) fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { + match self.current() { + TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(false), + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_clause(name.clone()) + } + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(false) + } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), false) + } + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { + let name = name.clone(); + self.parse_var_store_stmt(name, false) + } + _ => { + let expr = self.parse_expr()?; + Ok(vec![EvalStmt::Expr(expr)]) + } + } + } + + /// Parses `$name[index] = expr` and `$name[] = expr` in a `for` clause. + pub(super) fn parse_array_set_clause( + &mut self, + name: String, + ) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `$name = expr` and simple variable compound assignments. + pub(super) fn parse_var_store_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { + self.advance(); + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::ReferenceAssign { + target: name, + source, + }]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = assignment_value(&name, op, value); + Ok(vec![EvalStmt::StoreVar { name, value }]) + } + + /// Parses prefix `++$name` and `--$name` as simple statement effects. + pub(super) fn parse_prefix_inc_dec_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![inc_dec_store(name, increment)]) + } + + /// Parses postfix `$name++` and `$name--` as simple statement effects. + pub(super) fn parse_postfix_inc_dec_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![inc_dec_store(name, increment)]) + } + + /// Parses `$object->property` as either an expression statement or property write. + pub(super) fn parse_property_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let target = self.parse_expr()?; + if !self.consume(TokenKind::Equal) { + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::Expr(target)]); + } + let EvalExpr::PropertyGet { object, property } = target else { + return Err(EvalParseError::UnexpectedToken); + }; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::PropertySet { + object: *object, + property, + value, + }]) + } + + /// Parses a complete `if` statement after consuming the `if` keyword. + pub(super) fn parse_if_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } + + /// Parses the condition, then block, and optional else branch for an `if` chain. + pub(super) fn parse_if_after_keyword(&mut self) -> Result { + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let then_branch = self.parse_statement_body()?; + let else_branch = self.parse_optional_else_branch()?; + Ok(EvalStmt::If { + condition, + then_branch, + else_branch, + }) + } + + /// Parses `elseif`, `else if`, or `else` branches after an `if` body. + pub(super) fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { + self.advance(); + return Ok(vec![self.parse_if_after_keyword()?]); + } + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "else")) { + return Ok(Vec::new()); + } + self.advance(); + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "if")) { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } else { + self.parse_statement_body() + } + } + + /// Parses `switch (expr) { case expr: ... default: ... }`. + pub(super) fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + let mut cases = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + cases.push(self.parse_switch_case()?); + } + self.expect(TokenKind::RBrace)?; + Ok(vec![EvalStmt::Switch { expr, cases }]) + } + + /// Parses one `case` or `default` arm inside a switch body. + pub(super) fn parse_switch_case(&mut self) -> Result { + let condition = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) + { + self.advance(); + let condition = self.parse_expr()?; + Some(condition) + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + None + } else { + return Err(EvalParseError::UnexpectedToken); + }; + self.expect(TokenKind::Colon)?; + let body = self.parse_switch_case_body()?; + Ok(EvalSwitchCase { condition, body }) + } + + /// Parses case body statements until the next case boundary or switch close. + pub(super) fn parse_switch_case_body(&mut self) -> Result, EvalParseError> { + let mut body = Vec::new(); + while !is_switch_case_boundary(self.current()) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + body.extend(self.parse_stmt()?); + } + Ok(body) + } + + /// Parses `unset($name[, ...]);`. + pub(super) fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let mut statements = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + statements.push(EvalStmt::UnsetVar { name: name.clone() }); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(statements) + } + + /// Parses `while (expr) { ... }`. + pub(super) fn parse_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::While { condition, body }]) + } + + /// Parses either a brace-delimited block or one braceless statement body. + pub(super) fn parse_statement_body(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::LBrace) { + self.parse_block() + } else { + self.parse_nested_stmt() + } + } + + /// Parses a brace-delimited statement block. + pub(super) fn parse_block(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LBrace)?; + self.parse_nested_block_contents() + } + + /// Parses one nested statement where import declarations are not legal. + pub(super) fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_stmt(); + self.allow_use_imports = previous; + result + } + + /// Parses a nested block while preserving active imports for name resolution. + pub(super) fn parse_nested_block_contents(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_block_contents(); + self.allow_use_imports = previous; + result + } + + /// Parses statements until the closing brace for the current block. + pub(super) fn parse_block_contents(&mut self) -> Result, EvalParseError> { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + statements.extend(self.parse_stmt()?); + } + self.expect(TokenKind::RBrace)?; + Ok(statements) + } +} diff --git a/crates/elephc-eval/src/parser/tests.rs b/crates/elephc-eval/src/parser/tests.rs index ca2d733734..f87c088a0a 100644 --- a/crates/elephc-eval/src/parser/tests.rs +++ b/crates/elephc-eval/src/parser/tests.rs @@ -10,8 +10,8 @@ //! - Fixtures intentionally use eval fragments without PHP opening tags. //! - Expected values compare against EvalIR so grammar regressions are visible. +use super::cursor::inc_dec_store; use super::parse_fragment; -use super::state::inc_dec_store; use crate::errors::EvalParseError; use crate::eval_ir::*; From 72af8c944cca1f8d535a887b966ff20de8a61ee4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:30:25 +0200 Subject: [PATCH 0286/1208] Split eval scalar builtins --- .../src/interpreter/builtins/scalars.rs | 1338 ----------------- .../interpreter/builtins/scalars/base64.rs | 130 ++ .../interpreter/builtins/scalars/common.rs | 36 + .../src/interpreter/builtins/scalars/hex.rs | 93 ++ .../src/interpreter/builtins/scalars/math.rs | 276 ++++ .../src/interpreter/builtins/scalars/mod.rs | 27 + .../interpreter/builtins/scalars/search.rs | 218 +++ .../interpreter/builtins/scalars/slashes.rs | 81 + .../interpreter/builtins/scalars/trim_case.rs | 303 ++++ .../src/interpreter/builtins/scalars/types.rs | 274 ++++ 10 files changed, 1438 insertions(+), 1338 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/base64.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/common.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/hex.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/math.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/search.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/slashes.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/trim_case.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/scalars/types.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars.rs b/crates/elephc-eval/src/interpreter/builtins/scalars.rs deleted file mode 100644 index 9ff2a0e868..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/scalars.rs +++ /dev/null @@ -1,1338 +0,0 @@ -//! Purpose: -//! Scalar conversion, math helpers, type predicates, and trailing string-search builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins` re-exports used by core call dispatch. -//! -//! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime -//! behavior through `RuntimeValueOps`. - -use super::super::*; -use super::*; - -/// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. -pub(in crate::interpreter) fn eval_crc32_bytes(bytes: &[u8]) -> u32 { - let mut crc = 0xffff_ffff_u32; - for byte in bytes { - crc ^= u32::from(*byte); - for _ in 0..8 { - let mask = 0_u32.wrapping_sub(crc & 1); - crc = (crc >> 1) ^ (0xedb8_8320 & mask); - } - } - !crc -} - -/// Casts one eval value to PHP int and returns the scalar payload. -pub(in crate::interpreter) fn eval_int_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.cast_int(value)?; - let bytes = values.string_bytes(value)?; - std::str::from_utf8(&bytes) - .map_err(|_| EvalStatus::RuntimeFatal)? - .parse::() - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP's `bin2hex(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_bin2hex( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_bin2hex_result(value, values) -} - -/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. -pub(in crate::interpreter) fn eval_bin2hex_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.string(&eval_lower_hex_bytes(&bytes)) -} - -/// Converts bytes to lowercase hexadecimal text. -pub(in crate::interpreter) fn eval_lower_hex_bytes(bytes: &[u8]) -> String { - let mut output = String::with_capacity(bytes.len() * 2); - const HEX: &[u8; 16] = b"0123456789abcdef"; - for byte in bytes { - output.push(HEX[(byte >> 4) as usize] as char); - output.push(HEX[(byte & 0x0f) as usize] as char); - } - output -} - -/// Evaluates PHP's `hex2bin(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_hex2bin( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_hex2bin_result(value, values) -} - -/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. -pub(in crate::interpreter) fn eval_hex2bin_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - if bytes.len() % 2 != 0 { - values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; - return values.bool_value(false); - } - let mut output = Vec::with_capacity(bytes.len() / 2); - for pair in bytes.chunks_exact(2) { - let Some(high) = eval_hex_nibble(pair[0]) else { - values.warning(HEX2BIN_INVALID_WARNING)?; - return values.bool_value(false); - }; - let Some(low) = eval_hex_nibble(pair[1]) else { - values.warning(HEX2BIN_INVALID_WARNING)?; - return values.bool_value(false); - }; - output.push((high << 4) | low); - } - values.string_bytes_value(&output) -} - -/// Returns the four-bit value for one hexadecimal byte. -pub(in crate::interpreter) fn eval_hex_nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } -} - -/// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_slashes( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_slashes_result(name, value, values) -} - -/// Applies PHP byte-string escaping or unescaping for addslashes/stripslashes. -pub(in crate::interpreter) fn eval_slashes_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "addslashes" => eval_addslashes_result(value, values), - "stripslashes" => eval_stripslashes_result(value, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. -pub(in crate::interpreter) fn eval_addslashes_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - for byte in bytes { - match byte { - 0 => output.extend_from_slice(b"\\0"), - b'\'' | b'"' | b'\\' => { - output.push(b'\\'); - output.push(byte); - } - _ => output.push(byte), - } - } - values.string_bytes_value(&output) -} - -/// Removes backslash quoting using PHP `stripslashes()` byte semantics. -pub(in crate::interpreter) fn eval_stripslashes_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'\\' { - index += 1; - if let Some(byte) = bytes.get(index).copied() { - output.push(if byte == b'0' { 0 } else { byte }); - index += 1; - } - } else { - output.push(bytes[index]); - index += 1; - } - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `base64_encode(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_base64_encode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_encode_result(value, values) -} - -/// Converts one eval value through PHP string conversion and returns Base64 text. -pub(in crate::interpreter) fn eval_base64_encode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); - const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for chunk in bytes.chunks(3) { - let first = chunk[0]; - let second = chunk.get(1).copied().unwrap_or(0); - let third = chunk.get(2).copied().unwrap_or(0); - output.push(ALPHABET[(first >> 2) as usize] as char); - output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); - if chunk.len() > 1 { - output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); - } else { - output.push('='); - } - if chunk.len() > 2 { - output.push(ALPHABET[(third & 0x3f) as usize] as char); - } else { - output.push('='); - } - } - values.string(&output) -} - -/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_base64_decode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_decode_result(value, values) -} - -/// Converts one eval value through PHP string conversion and decodes Base64 bytes. -pub(in crate::interpreter) fn eval_base64_decode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let input = values.string_bytes(value)?; - let mut output = Vec::with_capacity((input.len() / 4) * 3); - let mut quartet = Vec::with_capacity(4); - for byte in input { - if byte.is_ascii_whitespace() { - continue; - } - if byte == b'=' { - quartet.push(None); - } else if let Some(value) = eval_base64_decode_sextet(byte) { - quartet.push(Some(value)); - } else { - continue; - } - if quartet.len() == 4 { - eval_push_base64_decoded_quartet(&quartet, &mut output); - quartet.clear(); - } - } - if !quartet.is_empty() { - while quartet.len() < 4 { - quartet.push(None); - } - eval_push_base64_decoded_quartet(&quartet, &mut output); - } - values.string_bytes_value(&output) -} - -/// Returns the six-bit Base64 value for one encoded byte. -pub(in crate::interpreter) fn eval_base64_decode_sextet(byte: u8) -> Option { - match byte { - b'A'..=b'Z' => Some(byte - b'A'), - b'a'..=b'z' => Some(byte - b'a' + 26), - b'0'..=b'9' => Some(byte - b'0' + 52), - b'+' => Some(62), - b'/' => Some(63), - _ => None, - } -} - -/// Appends decoded bytes for one padded or unpadded Base64 quartet. -pub(in crate::interpreter) fn eval_push_base64_decoded_quartet( - quartet: &[Option], - output: &mut Vec, -) { - let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { - return; - }; - output.push((first << 2) | (second >> 4)); - let Some(third) = quartet[2] else { - return; - }; - output.push(((second & 0x0f) << 4) | (third >> 2)); - let Some(fourth) = quartet[3] else { - return; - }; - output.push(((third & 0x03) << 6) | fourth); -} - -/// Evaluates PHP one-argument floating-point math builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_float_unary( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_float_unary_result(name, value, values) -} - -/// Dispatches an evaluated value through the matching PHP floating-point unary math function. -pub(in crate::interpreter) fn eval_float_unary_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_float_value(value, values)?; - let result = match name { - "acos" => value.acos(), - "asin" => value.asin(), - "atan" => value.atan(), - "cos" => value.cos(), - "cosh" => value.cosh(), - "deg2rad" => value.to_radians(), - "exp" => value.exp(), - "log2" => value.log2(), - "log10" => value.log10(), - "rad2deg" => value.to_degrees(), - "sin" => value.sin(), - "sinh" => value.sinh(), - "tan" => value.tan(), - "tanh" => value.tanh(), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.float(result) -} - -/// Evaluates PHP two-argument floating-point math builtins over eval expressions. -pub(in crate::interpreter) fn eval_builtin_float_pair( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_float_pair_result(name, left, right, values) -} - -/// Dispatches an evaluated pair through PHP `atan2()` or `hypot()`. -pub(in crate::interpreter) fn eval_float_pair_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left = eval_float_value(left, values)?; - let right = eval_float_value(right, values)?; - let result = match name { - "atan2" => left.atan2(right), - "hypot" => left.hypot(right), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.float(result) -} - -/// Evaluates PHP `log($num, $base = e)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_log( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [num] => { - let num = eval_expr(num, context, scope, values)?; - eval_log_result(num, None, values) - } - [num, base] => { - let num = eval_expr(num, context, scope, values)?; - let base = eval_expr(base, context, scope, values)?; - eval_log_result(num, Some(base), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `log()` from already evaluated arguments. -pub(in crate::interpreter) fn eval_log_result( - num: RuntimeCellHandle, - base: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let num = eval_float_value(num, values)?; - let result = match base { - Some(base) => num.log(eval_float_value(base, values)?), - None => num.ln(), - }; - values.float(result) -} - -/// Evaluates PHP `intdiv(...)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_intdiv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_intdiv_result(left, right, values) -} - -/// Computes PHP integer division from already evaluated arguments. -pub(in crate::interpreter) fn eval_intdiv_result( - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left = eval_int_value(left, values)?; - let right = eval_int_value(right, values)?; - if right == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; - values.int(result) -} - -/// Evaluates PHP floating-point binary math builtins over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_float_binary( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_float_binary_result(name, left, right, values) -} - -/// Dispatches an evaluated pair through the matching PHP float math hook. -pub(in crate::interpreter) fn eval_float_binary_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "fdiv" => values.fdiv(left, right), - "fmod" => values.fmod(left, right), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `clamp($value, $min, $max)` over three eval expressions. -pub(in crate::interpreter) fn eval_builtin_clamp( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value, min, max] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_clamp_result(value, min, max, values) -} - -/// Selects the inclusive clamp result after validating bound order and NaN bounds. -pub(in crate::interpreter) fn eval_clamp_result( - value: RuntimeCellHandle, - min: RuntimeCellHandle, - max: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; - if values.truthy(invalid_bounds)? { - return Err(EvalStatus::RuntimeFatal); - } - let above_max = values.compare(EvalBinOp::Gt, value, max)?; - if values.truthy(above_max)? { - return Ok(max); - } - let below_min = values.compare(EvalBinOp::Lt, value, min)?; - if values.truthy(below_min)? { - return Ok(min); - } - Ok(value) -} - -/// Returns whether a clamp bound is a floating-point NaN value. -pub(in crate::interpreter) fn eval_clamp_bound_is_nan( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? != EVAL_TAG_FLOAT { - return Ok(false); - } - Ok(eval_float_value(value, values)?.is_nan()) -} - -/// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_min_max( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_min_max_result(name, &evaluated_args, values) -} - -/// Selects the smallest or largest evaluated cell using runtime comparison hooks. -pub(in crate::interpreter) fn eval_min_max_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((&first, rest)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let op = match name { - "min" => EvalBinOp::Lt, - "max" => EvalBinOp::Gt, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let mut selected = first; - for candidate in rest { - let better = values.compare(op, *candidate, selected)?; - if values.truthy(better)? { - selected = *candidate; - } - } - Ok(selected) -} - -/// Evaluates PHP scalar cast builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_cast( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_cast_result(name, value, values) -} - -/// Dispatches an already evaluated value through the matching PHP cast hook. -pub(in crate::interpreter) fn eval_cast_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "intval" => values.cast_int(value), - "floatval" => values.cast_float(value), - "strval" => values.cast_string(value), - "boolval" => values.cast_bool(value), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP's `gettype(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_gettype( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_gettype_result(value, values) -} - -/// Converts one boxed runtime tag into PHP's `gettype()` spelling. -pub(in crate::interpreter) fn eval_gettype_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - values.string(eval_gettype_name(tag)) -} - -/// Evaluates PHP's `get_class(...)` over one eval object expression. -pub(in crate::interpreter) fn eval_builtin_get_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_get_class_result(object, context, values) -} - -/// Resolves the PHP-visible class name for one already materialized object cell. -pub(in crate::interpreter) fn eval_get_class_result( - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Ok(identity) = values.object_identity(object) { - if let Some(class) = context.dynamic_object_class(identity) { - return values.string(class.name().trim_start_matches('\\')); - } - } - values.object_class_name(object) -} - -/// Evaluates PHP's SPL object identity builtins over one eval object expression. -pub(in crate::interpreter) fn eval_builtin_spl_object_identity( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_spl_object_identity_result(name, object, values) -} - -/// Returns the unboxed object-payload identity in the native SPL builtin spelling. -pub(in crate::interpreter) fn eval_spl_object_identity_result( - name: &str, - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let identity = values.object_identity(object)? as i64; - match name { - "spl_object_id" => values.int(identity), - "spl_object_hash" => values.string(&identity.to_string()), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. -pub(in crate::interpreter) fn eval_builtin_get_parent_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object_or_class] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object_or_class = eval_expr(object_or_class, context, scope, values)?; - eval_get_parent_class_result(object_or_class, values) -} - -/// Resolves the PHP-visible parent class name for one object or class-name cell. -pub(in crate::interpreter) fn eval_get_parent_class_result( - object_or_class: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - values.parent_class_name(object_or_class) -} - -/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. -pub(in crate::interpreter) fn eval_builtin_resource_introspection( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [resource] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let resource = eval_expr(resource, context, scope, values)?; - eval_resource_introspection_result(name, resource, values) -} - -/// Evaluates a materialized resource introspection builtin argument. -pub(in crate::interpreter) fn eval_resource_introspection_result( - name: &str, - resource: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(resource)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - match name { - "get_resource_type" => values.string("stream"), - "get_resource_id" => values.cast_int(resource), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Returns the PHP-visible type name for a concrete eval runtime tag. -pub(in crate::interpreter) fn eval_gettype_name(tag: u64) -> &'static str { - match tag { - EVAL_TAG_INT => "integer", - EVAL_TAG_FLOAT => "double", - EVAL_TAG_STRING => "string", - EVAL_TAG_BOOL => "boolean", - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", - EVAL_TAG_OBJECT => "object", - EVAL_TAG_RESOURCE => "resource", - EVAL_TAG_NULL => "NULL", - _ => "NULL", - } -} - -/// Evaluates PHP scalar/container type predicate builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_type_predicate( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_type_predicate_result(name, value, values) -} - -/// Converts a concrete runtime tag into a PHP `is_*` predicate result. -pub(in crate::interpreter) fn eval_type_predicate_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - let result = match name { - "is_int" | "is_integer" | "is_long" => tag == EVAL_TAG_INT, - "is_float" | "is_double" | "is_real" => tag == EVAL_TAG_FLOAT, - "is_string" => tag == EVAL_TAG_STRING, - "is_bool" => tag == EVAL_TAG_BOOL, - "is_null" => tag == EVAL_TAG_NULL, - "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), - "is_object" => tag == EVAL_TAG_OBJECT, - "is_resource" => tag == EVAL_TAG_RESOURCE, - "is_nan" => eval_float_value(value, values)?.is_nan(), - "is_infinite" => eval_float_value(value, values)?.is_infinite(), - "is_finite" => eval_float_value(value, values)?.is_finite(), - "is_numeric" => { - tag == EVAL_TAG_INT - || tag == EVAL_TAG_FLOAT - || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)) - } - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(result) -} - -/// Matches the static backend's legacy ASCII numeric-string scan. -pub(in crate::interpreter) fn eval_is_numeric_string(bytes: &[u8]) -> bool { - if bytes.is_empty() { - return false; - } - - let mut index = 0; - let mut consumed_digits = 0; - if bytes[index] == b'-' { - index += 1; - if index >= bytes.len() { - return false; - } - } - - while index < bytes.len() { - if bytes[index] == b'.' { - index += 1; - break; - } - if !bytes[index].is_ascii_digit() { - return false; - } - consumed_digits += 1; - index += 1; - } - - while index < bytes.len() { - if !bytes[index].is_ascii_digit() { - return false; - } - consumed_digits += 1; - index += 1; - } - - consumed_digits > 0 -} - -/// Evaluates PHP's `hash_equals(...)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_hash_equals( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [known, user] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let known = eval_expr(known, context, scope, values)?; - let user = eval_expr(user, context, scope, values)?; - eval_hash_equals_result(known, user, values) -} - -/// Compares two converted strings with PHP `hash_equals()` semantics. -pub(in crate::interpreter) fn eval_hash_equals_result( - known: RuntimeCellHandle, - user: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let known = values.string_bytes(known)?; - let user = values.string_bytes(user)?; - if known.len() != user.len() { - return values.bool_value(false); - } - let mut diff = 0u8; - for (known, user) in known.iter().zip(user.iter()) { - diff |= known ^ user; - } - values.bool_value(diff == 0) -} - -/// Evaluates PHP string comparison builtins over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_string_compare( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_string_compare_result(name, left, right, values) -} - -/// Compares two converted strings and returns -1, 0, or 1. -pub(in crate::interpreter) fn eval_string_compare_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut left = values.string_bytes(left)?; - let mut right = values.string_bytes(right)?; - match name { - "strcmp" => {} - "strcasecmp" => { - left.make_ascii_lowercase(); - right.make_ascii_lowercase(); - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - let result = match left.cmp(&right) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - }; - values.int(result) -} - -/// Evaluates PHP's byte-string search predicates over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_string_search( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_string_search_result(name, haystack, needle, values) -} - -/// Checks one converted haystack for one converted needle using PHP byte-string semantics. -pub(in crate::interpreter) fn eval_string_search_result( - name: &str, - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let matched = match name { - "str_contains" => { - needle.is_empty() - || haystack - .windows(needle.len()) - .any(|window| window == needle) - } - "str_starts_with" => haystack.starts_with(&needle), - "str_ends_with" => haystack.ends_with(&needle), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(matched) -} - -/// Evaluates PHP byte-string position builtins over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_string_position( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_string_position_result(name, haystack, needle, values) -} - -/// Returns the first or last byte offset of a converted needle, or PHP `false`. -pub(in crate::interpreter) fn eval_string_position_result( - name: &str, - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let position = match name { - "strpos" if needle.is_empty() => Some(0), - "strpos" => haystack - .windows(needle.len()) - .position(|window| window == needle), - "strrpos" if needle.is_empty() => Some(haystack.len()), - "strrpos" => haystack - .windows(needle.len()) - .rposition(|window| window == needle), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - match position { - Some(position) => { - let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(position) - } - None => values.bool_value(false), - } -} - -/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. -pub(in crate::interpreter) fn eval_builtin_strstr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [haystack, needle] => { - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_strstr_result(haystack, needle, false, values) - } - [haystack, needle, before_needle] => { - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - let before_needle = eval_expr(before_needle, context, scope, values)?; - let before_needle = values.truthy(before_needle)?; - eval_strstr_result(haystack, needle, before_needle, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. -pub(in crate::interpreter) fn eval_strstr_result( - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - before_needle: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let position = if needle.is_empty() { - Some(0) - } else { - eval_find_subslice(&haystack, &needle, 0) - }; - let Some(position) = position else { - return values.bool_value(false); - }; - let result = if before_needle { - &haystack[..position] - } else { - &haystack[position..] - }; - values.string_bytes_value(result) -} - -pub(in crate::interpreter) const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; - -/// Evaluates PHP trim-like string builtins over one eval expression and optional mask. -pub(in crate::interpreter) fn eval_builtin_trim_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_trim_like_result(name, value, None, values) - } - [value, mask] => { - let value = eval_expr(value, context, scope, values)?; - let mask = eval_expr(mask, context, scope, values)?; - eval_trim_like_result(name, value, Some(mask), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Trims one converted string using PHP's default mask or a caller-provided byte mask. -pub(in crate::interpreter) fn eval_trim_like_result( - name: &str, - value: RuntimeCellHandle, - mask: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let explicit_mask; - let trim_mask = if let Some(mask) = mask { - explicit_mask = values.string_bytes(mask)?; - explicit_mask.as_slice() - } else { - PHP_DEFAULT_TRIM_MASK - }; - - let mut start = 0; - let mut end = bytes.len(); - if matches!(name, "trim" | "ltrim") { - while start < end && trim_mask.contains(&bytes[start]) { - start += 1; - } - } - if matches!(name, "trim" | "rtrim" | "chop") { - while end > start && trim_mask.contains(&bytes[end - 1]) { - end -= 1; - } - } - if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { - return Err(EvalStatus::UnsupportedConstruct); - } - - let value = - String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(&value) -} - -/// Evaluates PHP ASCII case-conversion string builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_string_case( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_string_case_result(name, value, values) -} - -/// Converts one eval value through PHP string conversion and ASCII case mapping. -pub(in crate::interpreter) fn eval_string_case_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bytes = values.string_bytes(value)?; - match name { - "strtolower" => { - for byte in &mut bytes { - if byte.is_ascii_uppercase() { - *byte += b'a' - b'A'; - } - } - } - "strtoupper" => { - for byte in &mut bytes { - if byte.is_ascii_lowercase() { - *byte -= b'a' - b'A'; - } - } - } - "ucfirst" => { - if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { - bytes[0] -= b'a' - b'A'; - } - } - "lcfirst" => { - if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { - bytes[0] += b'a' - b'A'; - } - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(&value) -} - -/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. -pub(in crate::interpreter) fn eval_builtin_ucwords( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_ucwords_result(value, None, values) - } - [value, separators] => { - let value = eval_expr(value, context, scope, values)?; - let separators = eval_expr(separators, context, scope, values)?; - eval_ucwords_result(value, Some(separators), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. -pub(in crate::interpreter) fn eval_ucwords_result( - value: RuntimeCellHandle, - separators: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bytes = values.string_bytes(value)?; - let separators = match separators { - Some(separators) => values.string_bytes(separators)?, - None => b" \t\r\n\x0c\x0b".to_vec(), - }; - let mut word_start = true; - for byte in &mut bytes { - if separators.contains(byte) { - word_start = true; - } else if word_start { - if byte.is_ascii_lowercase() { - *byte -= b'a' - b'A'; - } - word_start = false; - } - } - values.string_bytes_value(&bytes) -} - -/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. -pub(in crate::interpreter) fn eval_builtin_wordwrap( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_wordwrap_result(value, None, None, None, values) - } - [value, width] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - eval_wordwrap_result(value, Some(width), None, None, values) - } - [value, width, break_string] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - let break_string = eval_expr(break_string, context, scope, values)?; - eval_wordwrap_result(value, Some(width), Some(break_string), None, values) - } - [value, width, break_string, cut] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - let break_string = eval_expr(break_string, context, scope, values)?; - let cut = eval_expr(cut, context, scope, values)?; - eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Wraps a byte string at PHP word boundaries and preserves existing newlines. -pub(in crate::interpreter) fn eval_wordwrap_result( - value: RuntimeCellHandle, - width: Option, - break_string: Option, - cut: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let width = match width { - Some(width) => eval_int_value(width, values)?, - None => 75, - }; - let break_string = match break_string { - Some(break_string) => values.string_bytes(break_string)?, - None => b"\n".to_vec(), - }; - if break_string.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let cut = match cut { - Some(cut) => values.truthy(cut)?, - None => false, - }; - if width == 0 && cut { - return Err(EvalStatus::RuntimeFatal); - } - if bytes.is_empty() { - return values.string_bytes_value(&bytes); - } - let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); - values.string_bytes_value(&output) -} - -/// Applies the core PHP word-wrap scan over already converted byte slices. -pub(in crate::interpreter) fn eval_wordwrap_bytes( - bytes: &[u8], - width: i64, - break_string: &[u8], - cut: bool, -) -> Vec { - if width < 0 && cut { - let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); - for byte in bytes { - output.extend_from_slice(break_string); - output.push(*byte); - } - return output; - } - - let width = width.max(0) as usize; - let mut output = Vec::with_capacity(bytes.len()); - let mut line_start = 0; - let mut last_space = None; - let mut index = 0; - while index < bytes.len() { - match bytes[index] { - b'\n' => { - output.extend_from_slice(&bytes[line_start..=index]); - index += 1; - line_start = index; - last_space = None; - } - b' ' => { - if index.saturating_sub(line_start) >= width { - output.extend_from_slice(&bytes[line_start..index]); - output.extend_from_slice(break_string); - index += 1; - line_start = index; - last_space = None; - } else { - last_space = Some(index); - index += 1; - } - } - _ if index.saturating_sub(line_start) >= width => { - if let Some(space) = last_space { - output.extend_from_slice(&bytes[line_start..space]); - output.extend_from_slice(break_string); - line_start = space + 1; - last_space = None; - } else if cut && width > 0 { - output.extend_from_slice(&bytes[line_start..index]); - output.extend_from_slice(break_string); - line_start = index; - } else { - index += 1; - } - } - _ => { - index += 1; - } - } - } - output.extend_from_slice(&bytes[line_start..]); - output -} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/base64.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/base64.rs new file mode 100644 index 0000000000..2ce773919b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/base64.rs @@ -0,0 +1,130 @@ +//! Purpose: +//! Base64 encoding and decoding builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; + +/// Evaluates PHP's `base64_encode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_base64_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_encode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns Base64 text. +pub(in crate::interpreter) fn eval_base64_encode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for chunk in bytes.chunks(3) { + let first = chunk[0]; + let second = chunk.get(1).copied().unwrap_or(0); + let third = chunk.get(2).copied().unwrap_or(0); + output.push(ALPHABET[(first >> 2) as usize] as char); + output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); + if chunk.len() > 1 { + output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); + } else { + output.push('='); + } + if chunk.len() > 2 { + output.push(ALPHABET[(third & 0x3f) as usize] as char); + } else { + output.push('='); + } + } + values.string(&output) +} + +/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_base64_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_decode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes Base64 bytes. +pub(in crate::interpreter) fn eval_base64_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(value)?; + let mut output = Vec::with_capacity((input.len() / 4) * 3); + let mut quartet = Vec::with_capacity(4); + for byte in input { + if byte.is_ascii_whitespace() { + continue; + } + if byte == b'=' { + quartet.push(None); + } else if let Some(value) = eval_base64_decode_sextet(byte) { + quartet.push(Some(value)); + } else { + continue; + } + if quartet.len() == 4 { + eval_push_base64_decoded_quartet(&quartet, &mut output); + quartet.clear(); + } + } + if !quartet.is_empty() { + while quartet.len() < 4 { + quartet.push(None); + } + eval_push_base64_decoded_quartet(&quartet, &mut output); + } + values.string_bytes_value(&output) +} + +/// Returns the six-bit Base64 value for one encoded byte. +pub(in crate::interpreter) fn eval_base64_decode_sextet(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(byte - b'A'), + b'a'..=b'z' => Some(byte - b'a' + 26), + b'0'..=b'9' => Some(byte - b'0' + 52), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } +} + +/// Appends decoded bytes for one padded or unpadded Base64 quartet. +pub(in crate::interpreter) fn eval_push_base64_decoded_quartet( + quartet: &[Option], + output: &mut Vec, +) { + let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { + return; + }; + output.push((first << 2) | (second >> 4)); + let Some(third) = quartet[2] else { + return; + }; + output.push(((second & 0x0f) << 4) | (third >> 2)); + let Some(fourth) = quartet[3] else { + return; + }; + output.push(((third & 0x03) << 6) | fourth); +} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/common.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/common.rs new file mode 100644 index 0000000000..1080d4b525 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/common.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Common scalar conversion and checksum helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; + +/// Returns the standard zlib/PHP CRC-32 checksum for a byte slice. +pub(in crate::interpreter) fn eval_crc32_bytes(bytes: &[u8]) -> u32 { + let mut crc = 0xffff_ffff_u32; + for byte in bytes { + crc ^= u32::from(*byte); + for _ in 0..8 { + let mask = 0_u32.wrapping_sub(crc & 1); + crc = (crc >> 1) ^ (0xedb8_8320 & mask); + } + } + !crc +} + +/// Casts one eval value to PHP int and returns the scalar payload. +pub(in crate::interpreter) fn eval_int_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_int(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/hex.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/hex.rs new file mode 100644 index 0000000000..1f906d6476 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/hex.rs @@ -0,0 +1,93 @@ +//! Purpose: +//! Hex encoding and decoding builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; + +/// Evaluates PHP's `bin2hex(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_bin2hex( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_bin2hex_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. +pub(in crate::interpreter) fn eval_bin2hex_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.string(&eval_lower_hex_bytes(&bytes)) +} + +/// Converts bytes to lowercase hexadecimal text. +pub(in crate::interpreter) fn eval_lower_hex_bytes(bytes: &[u8]) -> String { + let mut output = String::with_capacity(bytes.len() * 2); + const HEX: &[u8; 16] = b"0123456789abcdef"; + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} + +/// Evaluates PHP's `hex2bin(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hex2bin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_hex2bin_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. +pub(in crate::interpreter) fn eval_hex2bin_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + if bytes.len() % 2 != 0 { + values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; + return values.bool_value(false); + } + let mut output = Vec::with_capacity(bytes.len() / 2); + for pair in bytes.chunks_exact(2) { + let Some(high) = eval_hex_nibble(pair[0]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + let Some(low) = eval_hex_nibble(pair[1]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + output.push((high << 4) | low); + } + values.string_bytes_value(&output) +} + +/// Returns the four-bit value for one hexadecimal byte. +pub(in crate::interpreter) fn eval_hex_nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/math.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/math.rs new file mode 100644 index 0000000000..801e91ac35 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/math.rs @@ -0,0 +1,276 @@ +//! Purpose: +//! Numeric math, clamp, min, and max helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP one-argument floating-point math builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_float_unary( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_float_unary_result(name, value, values) +} + +/// Dispatches an evaluated value through the matching PHP floating-point unary math function. +pub(in crate::interpreter) fn eval_float_unary_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let result = match name { + "acos" => value.acos(), + "asin" => value.asin(), + "atan" => value.atan(), + "cos" => value.cos(), + "cosh" => value.cosh(), + "deg2rad" => value.to_radians(), + "exp" => value.exp(), + "log2" => value.log2(), + "log10" => value.log10(), + "rad2deg" => value.to_degrees(), + "sin" => value.sin(), + "sinh" => value.sinh(), + "tan" => value.tan(), + "tanh" => value.tanh(), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.float(result) +} + +/// Evaluates PHP two-argument floating-point math builtins over eval expressions. +pub(in crate::interpreter) fn eval_builtin_float_pair( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_float_pair_result(name, left, right, values) +} + +/// Dispatches an evaluated pair through PHP `atan2()` or `hypot()`. +pub(in crate::interpreter) fn eval_float_pair_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_float_value(left, values)?; + let right = eval_float_value(right, values)?; + let result = match name { + "atan2" => left.atan2(right), + "hypot" => left.hypot(right), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.float(result) +} + +/// Evaluates PHP `log($num, $base = e)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_log( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [num] => { + let num = eval_expr(num, context, scope, values)?; + eval_log_result(num, None, values) + } + [num, base] => { + let num = eval_expr(num, context, scope, values)?; + let base = eval_expr(base, context, scope, values)?; + eval_log_result(num, Some(base), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `log()` from already evaluated arguments. +pub(in crate::interpreter) fn eval_log_result( + num: RuntimeCellHandle, + base: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + let result = match base { + Some(base) => num.log(eval_float_value(base, values)?), + None => num.ln(), + }; + values.float(result) +} + +/// Evaluates PHP `intdiv(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_intdiv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_intdiv_result(left, right, values) +} + +/// Computes PHP integer division from already evaluated arguments. +pub(in crate::interpreter) fn eval_intdiv_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_int_value(left, values)?; + let right = eval_int_value(right, values)?; + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; + values.int(result) +} + +/// Evaluates PHP floating-point binary math builtins over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_float_binary( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_float_binary_result(name, left, right, values) +} + +/// Dispatches an evaluated pair through the matching PHP float math hook. +pub(in crate::interpreter) fn eval_float_binary_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "fdiv" => values.fdiv(left, right), + "fmod" => values.fmod(left, right), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP `clamp($value, $min, $max)` over three eval expressions. +pub(in crate::interpreter) fn eval_builtin_clamp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_clamp_result(value, min, max, values) +} + +/// Selects the inclusive clamp result after validating bound order and NaN bounds. +pub(in crate::interpreter) fn eval_clamp_result( + value: RuntimeCellHandle, + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; + if values.truthy(invalid_bounds)? { + return Err(EvalStatus::RuntimeFatal); + } + let above_max = values.compare(EvalBinOp::Gt, value, max)?; + if values.truthy(above_max)? { + return Ok(max); + } + let below_min = values.compare(EvalBinOp::Lt, value, min)?; + if values.truthy(below_min)? { + return Ok(min); + } + Ok(value) +} + +/// Returns whether a clamp bound is a floating-point NaN value. +pub(in crate::interpreter) fn eval_clamp_bound_is_nan( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_FLOAT { + return Ok(false); + } + Ok(eval_float_value(value, values)?.is_nan()) +} + +/// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_min_max( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_min_max_result(name, &evaluated_args, values) +} + +/// Selects the smallest or largest evaluated cell using runtime comparison hooks. +pub(in crate::interpreter) fn eval_min_max_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((&first, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let op = match name { + "min" => EvalBinOp::Lt, + "max" => EvalBinOp::Gt, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let mut selected = first; + for candidate in rest { + let better = values.compare(op, *candidate, selected)?; + if values.truthy(better)? { + selected = *candidate; + } + } + Ok(selected) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/mod.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/mod.rs new file mode 100644 index 0000000000..050b3f3d3b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/mod.rs @@ -0,0 +1,27 @@ +//! Purpose: +//! Groups scalar, encoding, math, type, string-search, and trim/case eval builtins. +//! Each submodule owns one PHP builtin family while this module re-exports the callable surface. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime behavior. + +mod base64; +mod common; +mod hex; +mod math; +mod search; +mod slashes; +mod trim_case; +mod types; + +pub(in crate::interpreter) use base64::*; +pub(in crate::interpreter) use common::*; +pub(in crate::interpreter) use hex::*; +pub(in crate::interpreter) use math::*; +pub(in crate::interpreter) use search::*; +pub(in crate::interpreter) use slashes::*; +pub(in crate::interpreter) use trim_case::*; +pub(in crate::interpreter) use types::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/search.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/search.rs new file mode 100644 index 0000000000..7ed21663d1 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/search.rs @@ -0,0 +1,218 @@ +//! Purpose: +//! String comparison, search, position, and strstr helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP's `hash_equals(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_equals( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [known, user] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let known = eval_expr(known, context, scope, values)?; + let user = eval_expr(user, context, scope, values)?; + eval_hash_equals_result(known, user, values) +} + +/// Compares two converted strings with PHP `hash_equals()` semantics. +pub(in crate::interpreter) fn eval_hash_equals_result( + known: RuntimeCellHandle, + user: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let known = values.string_bytes(known)?; + let user = values.string_bytes(user)?; + if known.len() != user.len() { + return values.bool_value(false); + } + let mut diff = 0u8; + for (known, user) in known.iter().zip(user.iter()) { + diff |= known ^ user; + } + values.bool_value(diff == 0) +} + +/// Evaluates PHP string comparison builtins over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_string_compare( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_string_compare_result(name, left, right, values) +} + +/// Compares two converted strings and returns -1, 0, or 1. +pub(in crate::interpreter) fn eval_string_compare_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut left = values.string_bytes(left)?; + let mut right = values.string_bytes(right)?; + match name { + "strcmp" => {} + "strcasecmp" => { + left.make_ascii_lowercase(); + right.make_ascii_lowercase(); + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let result = match left.cmp(&right) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + }; + values.int(result) +} + +/// Evaluates PHP's byte-string search predicates over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_string_search( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_search_result(name, haystack, needle, values) +} + +/// Checks one converted haystack for one converted needle using PHP byte-string semantics. +pub(in crate::interpreter) fn eval_string_search_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let matched = match name { + "str_contains" => { + needle.is_empty() + || haystack + .windows(needle.len()) + .any(|window| window == needle) + } + "str_starts_with" => haystack.starts_with(&needle), + "str_ends_with" => haystack.ends_with(&needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(matched) +} + +/// Evaluates PHP byte-string position builtins over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_string_position( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_position_result(name, haystack, needle, values) +} + +/// Returns the first or last byte offset of a converted needle, or PHP `false`. +pub(in crate::interpreter) fn eval_string_position_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = match name { + "strpos" if needle.is_empty() => Some(0), + "strpos" => haystack + .windows(needle.len()) + .position(|window| window == needle), + "strrpos" if needle.is_empty() => Some(haystack.len()), + "strrpos" => haystack + .windows(needle.len()) + .rposition(|window| window == needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + match position { + Some(position) => { + let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(position) + } + None => values.bool_value(false), + } +} + +/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. +pub(in crate::interpreter) fn eval_builtin_strstr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [haystack, needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_strstr_result(haystack, needle, false, values) + } + [haystack, needle, before_needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + let before_needle = eval_expr(before_needle, context, scope, values)?; + let before_needle = values.truthy(before_needle)?; + eval_strstr_result(haystack, needle, before_needle, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. +pub(in crate::interpreter) fn eval_strstr_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + before_needle: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = if needle.is_empty() { + Some(0) + } else { + eval_find_subslice(&haystack, &needle, 0) + }; + let Some(position) = position else { + return values.bool_value(false); + }; + let result = if before_needle { + &haystack[..position] + } else { + &haystack[position..] + }; + values.string_bytes_value(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/slashes.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/slashes.rs new file mode 100644 index 0000000000..430961ef3e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/slashes.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Slash escaping and unescaping builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; + +/// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_slashes( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_slashes_result(name, value, values) +} + +/// Applies PHP byte-string escaping or unescaping for addslashes/stripslashes. +pub(in crate::interpreter) fn eval_slashes_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "addslashes" => eval_addslashes_result(value, values), + "stripslashes" => eval_stripslashes_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. +pub(in crate::interpreter) fn eval_addslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + 0 => output.extend_from_slice(b"\\0"), + b'\'' | b'"' | b'\\' => { + output.push(b'\\'); + output.push(byte); + } + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Removes backslash quoting using PHP `stripslashes()` byte semantics. +pub(in crate::interpreter) fn eval_stripslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'\\' { + index += 1; + if let Some(byte) = bytes.get(index).copied() { + output.push(if byte == b'0' { 0 } else { byte }); + index += 1; + } + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/trim_case.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/trim_case.rs new file mode 100644 index 0000000000..3e6209ce77 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/trim_case.rs @@ -0,0 +1,303 @@ +//! Purpose: +//! Trim, case conversion, ucwords, and wordwrap helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; +use super::*; + +pub(in crate::interpreter) const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; + +/// Evaluates PHP trim-like string builtins over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_trim_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_trim_like_result(name, value, None, values) + } + [value, mask] => { + let value = eval_expr(value, context, scope, values)?; + let mask = eval_expr(mask, context, scope, values)?; + eval_trim_like_result(name, value, Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Trims one converted string using PHP's default mask or a caller-provided byte mask. +pub(in crate::interpreter) fn eval_trim_like_result( + name: &str, + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let explicit_mask; + let trim_mask = if let Some(mask) = mask { + explicit_mask = values.string_bytes(mask)?; + explicit_mask.as_slice() + } else { + PHP_DEFAULT_TRIM_MASK + }; + + let mut start = 0; + let mut end = bytes.len(); + if matches!(name, "trim" | "ltrim") { + while start < end && trim_mask.contains(&bytes[start]) { + start += 1; + } + } + if matches!(name, "trim" | "rtrim" | "chop") { + while end > start && trim_mask.contains(&bytes[end - 1]) { + end -= 1; + } + } + if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { + return Err(EvalStatus::UnsupportedConstruct); + } + + let value = + String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} + +/// Evaluates PHP ASCII case-conversion string builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_string_case( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_string_case_result(name, value, values) +} + +/// Converts one eval value through PHP string conversion and ASCII case mapping. +pub(in crate::interpreter) fn eval_string_case_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + match name { + "strtolower" => { + for byte in &mut bytes { + if byte.is_ascii_uppercase() { + *byte += b'a' - b'A'; + } + } + } + "strtoupper" => { + for byte in &mut bytes { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + } + } + "ucfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { + bytes[0] -= b'a' - b'A'; + } + } + "lcfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { + bytes[0] += b'a' - b'A'; + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} + +/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. +pub(in crate::interpreter) fn eval_builtin_ucwords( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_ucwords_result(value, None, values) + } + [value, separators] => { + let value = eval_expr(value, context, scope, values)?; + let separators = eval_expr(separators, context, scope, values)?; + eval_ucwords_result(value, Some(separators), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. +pub(in crate::interpreter) fn eval_ucwords_result( + value: RuntimeCellHandle, + separators: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + let separators = match separators { + Some(separators) => values.string_bytes(separators)?, + None => b" \t\r\n\x0c\x0b".to_vec(), + }; + let mut word_start = true; + for byte in &mut bytes { + if separators.contains(byte) { + word_start = true; + } else if word_start { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + word_start = false; + } + } + values.string_bytes_value(&bytes) +} + +/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. +pub(in crate::interpreter) fn eval_builtin_wordwrap( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_wordwrap_result(value, None, None, None, values) + } + [value, width] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + eval_wordwrap_result(value, Some(width), None, None, values) + } + [value, width, break_string] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), None, values) + } + [value, width, break_string, cut] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + let cut = eval_expr(cut, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Wraps a byte string at PHP word boundaries and preserves existing newlines. +pub(in crate::interpreter) fn eval_wordwrap_result( + value: RuntimeCellHandle, + width: Option, + break_string: Option, + cut: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let width = match width { + Some(width) => eval_int_value(width, values)?, + None => 75, + }; + let break_string = match break_string { + Some(break_string) => values.string_bytes(break_string)?, + None => b"\n".to_vec(), + }; + if break_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let cut = match cut { + Some(cut) => values.truthy(cut)?, + None => false, + }; + if width == 0 && cut { + return Err(EvalStatus::RuntimeFatal); + } + if bytes.is_empty() { + return values.string_bytes_value(&bytes); + } + let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); + values.string_bytes_value(&output) +} + +/// Applies the core PHP word-wrap scan over already converted byte slices. +pub(in crate::interpreter) fn eval_wordwrap_bytes( + bytes: &[u8], + width: i64, + break_string: &[u8], + cut: bool, +) -> Vec { + if width < 0 && cut { + let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); + for byte in bytes { + output.extend_from_slice(break_string); + output.push(*byte); + } + return output; + } + + let width = width.max(0) as usize; + let mut output = Vec::with_capacity(bytes.len()); + let mut line_start = 0; + let mut last_space = None; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'\n' => { + output.extend_from_slice(&bytes[line_start..=index]); + index += 1; + line_start = index; + last_space = None; + } + b' ' => { + if index.saturating_sub(line_start) >= width { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + index += 1; + line_start = index; + last_space = None; + } else { + last_space = Some(index); + index += 1; + } + } + _ if index.saturating_sub(line_start) >= width => { + if let Some(space) = last_space { + output.extend_from_slice(&bytes[line_start..space]); + output.extend_from_slice(break_string); + line_start = space + 1; + last_space = None; + } else if cut && width > 0 { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + line_start = index; + } else { + index += 1; + } + } + _ => { + index += 1; + } + } + } + output.extend_from_slice(&bytes[line_start..]); + output +} diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs new file mode 100644 index 0000000000..596b32a459 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs @@ -0,0 +1,274 @@ +//! Purpose: +//! Scalar casts, type names, object metadata, and type predicate builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::scalars` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP scalar cast builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_cast( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_cast_result(name, value, values) +} + +/// Dispatches an already evaluated value through the matching PHP cast hook. +pub(in crate::interpreter) fn eval_cast_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "intval" => values.cast_int(value), + "floatval" => values.cast_float(value), + "strval" => values.cast_string(value), + "boolval" => values.cast_bool(value), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP's `gettype(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gettype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_gettype_result(value, values) +} + +/// Converts one boxed runtime tag into PHP's `gettype()` spelling. +pub(in crate::interpreter) fn eval_gettype_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.string(eval_gettype_name(tag)) +} + +/// Evaluates PHP's `get_class(...)` over one eval object expression. +pub(in crate::interpreter) fn eval_builtin_get_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_get_class_result(object, context, values) +} + +/// Resolves the PHP-visible class name for one already materialized object cell. +pub(in crate::interpreter) fn eval_get_class_result( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return values.string(class.name().trim_start_matches('\\')); + } + } + values.object_class_name(object) +} + +/// Evaluates PHP's SPL object identity builtins over one eval object expression. +pub(in crate::interpreter) fn eval_builtin_spl_object_identity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_spl_object_identity_result(name, object, values) +} + +/// Returns the unboxed object-payload identity in the native SPL builtin spelling. +pub(in crate::interpreter) fn eval_spl_object_identity_result( + name: &str, + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)? as i64; + match name { + "spl_object_id" => values.int(identity), + "spl_object_hash" => values.string(&identity.to_string()), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. +pub(in crate::interpreter) fn eval_builtin_get_parent_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object_or_class] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object_or_class = eval_expr(object_or_class, context, scope, values)?; + eval_get_parent_class_result(object_or_class, values) +} + +/// Resolves the PHP-visible parent class name for one object or class-name cell. +pub(in crate::interpreter) fn eval_get_parent_class_result( + object_or_class: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.parent_class_name(object_or_class) +} + +/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. +pub(in crate::interpreter) fn eval_builtin_resource_introspection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [resource] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let resource = eval_expr(resource, context, scope, values)?; + eval_resource_introspection_result(name, resource, values) +} + +/// Evaluates a materialized resource introspection builtin argument. +pub(in crate::interpreter) fn eval_resource_introspection_result( + name: &str, + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + match name { + "get_resource_type" => values.string("stream"), + "get_resource_id" => values.cast_int(resource), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Returns the PHP-visible type name for a concrete eval runtime tag. +pub(in crate::interpreter) fn eval_gettype_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "integer", + EVAL_TAG_FLOAT => "double", + EVAL_TAG_STRING => "string", + EVAL_TAG_BOOL => "boolean", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_OBJECT => "object", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_NULL => "NULL", + _ => "NULL", + } +} + +/// Evaluates PHP scalar/container type predicate builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_type_predicate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_type_predicate_result(name, value, values) +} + +/// Converts a concrete runtime tag into a PHP `is_*` predicate result. +pub(in crate::interpreter) fn eval_type_predicate_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + let result = match name { + "is_int" | "is_integer" | "is_long" => tag == EVAL_TAG_INT, + "is_float" | "is_double" | "is_real" => tag == EVAL_TAG_FLOAT, + "is_string" => tag == EVAL_TAG_STRING, + "is_bool" => tag == EVAL_TAG_BOOL, + "is_null" => tag == EVAL_TAG_NULL, + "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_object" => tag == EVAL_TAG_OBJECT, + "is_resource" => tag == EVAL_TAG_RESOURCE, + "is_nan" => eval_float_value(value, values)?.is_nan(), + "is_infinite" => eval_float_value(value, values)?.is_infinite(), + "is_finite" => eval_float_value(value, values)?.is_finite(), + "is_numeric" => { + tag == EVAL_TAG_INT + || tag == EVAL_TAG_FLOAT + || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)) + } + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(result) +} + +/// Matches the static backend's legacy ASCII numeric-string scan. +pub(in crate::interpreter) fn eval_is_numeric_string(bytes: &[u8]) -> bool { + if bytes.is_empty() { + return false; + } + + let mut index = 0; + let mut consumed_digits = 0; + if bytes[index] == b'-' { + index += 1; + if index >= bytes.len() { + return false; + } + } + + while index < bytes.len() { + if bytes[index] == b'.' { + index += 1; + break; + } + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + while index < bytes.len() { + if !bytes[index].is_ascii_digit() { + return false; + } + consumed_digits += 1; + index += 1; + } + + consumed_digits > 0 +} From 62501effa4588c274f1fb0919802c805d342f28d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:32:04 +0200 Subject: [PATCH 0287/1208] Split eval string builtins --- .../src/interpreter/builtins/strings.rs | 982 ------------------ .../src/interpreter/builtins/strings/ctype.rs | 56 + .../src/interpreter/builtins/strings/hash.rs | 199 ++++ .../src/interpreter/builtins/strings/html.rs | 97 ++ .../builtins/strings/introspection.rs | 87 ++ .../src/interpreter/builtins/strings/mod.rs | 35 + .../src/interpreter/builtins/strings/nl2br.rs | 67 ++ .../src/interpreter/builtins/strings/pad.rs | 106 ++ .../interpreter/builtins/strings/repeat.rs | 49 + .../interpreter/builtins/strings/replace.rs | 74 ++ .../interpreter/builtins/strings/simple.rs | 62 ++ .../src/interpreter/builtins/strings/split.rs | 57 + .../interpreter/builtins/strings/substr.rs | 130 +++ .../src/interpreter/builtins/strings/url.rs | 114 ++ 14 files changed, 1133 insertions(+), 982 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/strings.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/ctype.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/hash.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/html.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/nl2br.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/pad.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/repeat.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/replace.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/simple.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/split.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/substr.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/url.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings.rs b/crates/elephc-eval/src/interpreter/builtins/strings.rs deleted file mode 100644 index 2cd6595188..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/strings.rs +++ /dev/null @@ -1,982 +0,0 @@ -//! Purpose: -//! String, hash, ctype, SPL registry, and stream-introspection builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins` re-exports used by core call dispatch. -//! -//! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime -//! behavior through `RuntimeValueOps`. - -use super::super::*; -use super::*; - -/// Evaluates PHP's `sqrt(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_sqrt( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.sqrt(value) -} - -/// Evaluates PHP's `strrev(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_strrev( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.strrev(value) -} - -/// Evaluates PHP's `chr(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_chr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_chr_result(value, values) -} - -/// Converts one eval value to a PHP integer and returns the low byte as a string. -pub(in crate::interpreter) fn eval_chr_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - values.string_bytes_value(&[value as u8]) -} - -/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. -pub(in crate::interpreter) fn eval_builtin_str_repeat( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value, times] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let times = eval_expr(times, context, scope, values)?; - eval_str_repeat_result(value, times, values) -} - -/// Repeats one PHP string byte sequence according to a PHP-cast integer count. -pub(in crate::interpreter) fn eval_str_repeat_result( - value: RuntimeCellHandle, - times: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let times = eval_int_value(times, values)?; - if times < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; - let capacity = bytes - .len() - .checked_mul(times) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(capacity); - for _ in 0..times { - output.extend_from_slice(&bytes); - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_str_replace( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [search, replace, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let search = eval_expr(search, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_str_replace_result(name, search, replace, subject, values) -} - -/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. -pub(in crate::interpreter) fn eval_str_replace_result( - name: &str, - search: RuntimeCellHandle, - replace: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let search = values.string_bytes(search)?; - let replace = values.string_bytes(replace)?; - let subject = values.string_bytes(subject)?; - if search.is_empty() { - return values.string_bytes_value(&subject); - } - - let mut output = Vec::with_capacity(subject.len()); - let mut start = 0; - while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { - output.extend_from_slice(&subject[start..found]); - output.extend_from_slice(&replace); - start = found + search.len(); - } - output.extend_from_slice(&subject[start..]); - values.string_bytes_value(&output) -} - -/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. -pub(in crate::interpreter) fn eval_find_replace_match( - name: &str, - subject: &[u8], - search: &[u8], - start: usize, -) -> Result, EvalStatus> { - match name { - "str_replace" => Ok(eval_find_subslice(subject, search, start)), - "str_ireplace" => Ok(subject - .get(start..) - .and_then(|tail| { - tail.windows(search.len()) - .position(|window| window.eq_ignore_ascii_case(search)) - }) - .map(|position| position + start)), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. -pub(in crate::interpreter) fn eval_builtin_str_pad( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, length] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_str_pad_result(value, length, None, None, values) - } - [value, length, pad_string] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let pad_string = eval_expr(pad_string, context, scope, values)?; - eval_str_pad_result(value, length, Some(pad_string), None, values) - } - [value, length, pad_string, pad_type] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let pad_string = eval_expr(pad_string, context, scope, values)?; - let pad_type = eval_expr(pad_type, context, scope, values)?; - eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Pads one byte string to a PHP target length using cyclic pad bytes. -pub(in crate::interpreter) fn eval_str_pad_result( - value: RuntimeCellHandle, - length: RuntimeCellHandle, - pad_string: Option, - pad_type: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let target_length = eval_int_value(length, values)?; - let Ok(target_length) = usize::try_from(target_length) else { - return values.string_bytes_value(&bytes); - }; - if target_length <= bytes.len() { - return values.string_bytes_value(&bytes); - } - - let pad_string = match pad_string { - Some(pad_string) => values.string_bytes(pad_string)?, - None => b" ".to_vec(), - }; - if pad_string.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let pad_type = match pad_type { - Some(pad_type) => eval_int_value(pad_type, values)?, - None => 1, - }; - let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; - let capacity = bytes - .len() - .checked_add(left_pad) - .and_then(|size| size.checked_add(right_pad)) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(capacity); - eval_append_repeated_pad(&mut output, &pad_string, left_pad); - output.extend_from_slice(&bytes); - eval_append_repeated_pad(&mut output, &pad_string, right_pad); - values.string_bytes_value(&output) -} - -/// Splits a `str_pad()` pad budget into left and right byte counts. -pub(in crate::interpreter) fn eval_str_pad_sides( - pad_budget: usize, - pad_type: i64, -) -> Result<(usize, usize), EvalStatus> { - match pad_type { - 0 => Ok((pad_budget, 0)), - 1 => Ok((0, pad_budget)), - 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Appends `count` bytes by cycling through the provided non-empty pad string. -pub(in crate::interpreter) fn eval_append_repeated_pad( - output: &mut Vec, - pad_string: &[u8], - count: usize, -) { - for index in 0..count { - output.push(pad_string[index % pad_string.len()]); - } -} - -/// Evaluates PHP `str_split(...)` over one string and optional chunk length. -pub(in crate::interpreter) fn eval_builtin_str_split( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_str_split_result(value, None, values) - } - [value, length] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_str_split_result(value, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. -pub(in crate::interpreter) fn eval_str_split_result( - value: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let length = match length { - Some(length) => eval_int_value(length, values)?, - None => 1, - }; - if length <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut result = values.array_new(0)?; - for (index, chunk) in bytes.chunks(length).enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string_bytes_value(chunk)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. -pub(in crate::interpreter) fn eval_builtin_nl2br( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_nl2br_result(value, true, values) - } - [value, use_xhtml] => { - let value = eval_expr(value, context, scope, values)?; - let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; - let use_xhtml = values.truthy(use_xhtml)?; - eval_nl2br_result(value, use_xhtml, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. -pub(in crate::interpreter) fn eval_nl2br_result( - value: RuntimeCellHandle, - use_xhtml: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let br = if use_xhtml { - b"
".as_slice() - } else { - b"
".as_slice() - }; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - let byte = bytes[index]; - if byte == b'\r' || byte == b'\n' { - output.extend_from_slice(br); - output.push(byte); - if index + 1 < bytes.len() - && ((byte == b'\r' && bytes[index + 1] == b'\n') - || (byte == b'\n' && bytes[index + 1] == b'\r')) - { - output.push(bytes[index + 1]); - index += 2; - continue; - } - } else { - output.push(byte); - } - index += 1; - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. -pub(in crate::interpreter) fn eval_builtin_substr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, offset] => { - let value = eval_expr(value, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_substr_result(value, offset, None, values) - } - [value, offset, length] => { - let value = eval_expr(value, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_substr_result(value, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Slices a PHP byte string using PHP `substr()` offset and length rules. -pub(in crate::interpreter) fn eval_substr_result( - value: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = eval_int_value(offset, values)?; - let start = if offset < 0 { - (total + offset).max(0) - } else { - offset.min(total) - }; - let end = match length { - None => total, - Some(length) if values.is_null(length)? => total, - Some(length) => { - let length = eval_int_value(length, values)?; - if length < 0 { - (total + length).max(0) - } else { - start.saturating_add(length).min(total) - } - } - }; - let end = end.max(start); - let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; - let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string_bytes_value(&bytes[start..end]) -} - -/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. -pub(in crate::interpreter) fn eval_builtin_substr_replace( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, replace, offset] => { - let value = eval_expr(value, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_substr_replace_result(value, replace, offset, None, values) - } - [value, replace, offset, length] => { - let value = eval_expr(value, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_substr_replace_result(value, replace, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. -pub(in crate::interpreter) fn eval_substr_replace_result( - value: RuntimeCellHandle, - replace: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let replacement = values.string_bytes(replace)?; - let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = eval_int_value(offset, values)?; - let start = if offset < 0 { - (total + offset).max(0) - } else { - offset.min(total) - }; - let end = match length { - None => total, - Some(length) if values.is_null(length)? => total, - Some(length) => { - let length = eval_int_value(length, values)?; - if length < 0 { - (total + length).max(start) - } else { - start.saturating_add(length).min(total) - } - } - }; - let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; - let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(bytes.len() + replacement.len()); - output.extend_from_slice(&bytes[..start]); - output.extend_from_slice(&replacement); - output.extend_from_slice(&bytes[end..]); - values.string_bytes_value(&output) -} - -/// Evaluates eval HTML entity encode/decode builtins over one string expression. -pub(in crate::interpreter) fn eval_builtin_html_entity( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_html_entity_result(name, value, values) -} - -/// Applies the eval-supported HTML entity transform for one PHP string value. -pub(in crate::interpreter) fn eval_html_entity_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), - "html_entity_decode" => eval_html_entity_decode_result(value, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Encodes the HTML-special byte characters covered by elephc's static helper. -pub(in crate::interpreter) fn eval_htmlspecialchars_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - for byte in bytes { - match byte { - b'&' => output.extend_from_slice(b"&"), - b'<' => output.extend_from_slice(b"<"), - b'>' => output.extend_from_slice(b">"), - b'"' => output.extend_from_slice(b"""), - b'\'' => output.extend_from_slice(b"'"), - _ => output.push(byte), - } - } - values.string_bytes_value(&output) -} - -/// Decodes one pass of the HTML entities emitted by the eval/static encoders. -pub(in crate::interpreter) fn eval_html_entity_decode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'&' { - if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { - output.push(decoded); - index += width; - continue; - } - } - output.push(bytes[index]); - index += 1; - } - values.string_bytes_value(&output) -} - -/// Returns the decoded byte and consumed width for one supported HTML entity. -pub(in crate::interpreter) fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { - for (entity, decoded) in [ - (b"<".as_slice(), b'<'), - (b">".as_slice(), b'>'), - (b""".as_slice(), b'"'), - (b"'".as_slice(), b'\''), - (b"'".as_slice(), b'\''), - (b"&".as_slice(), b'&'), - ] { - if bytes.starts_with(entity) { - return Some((decoded, entity.len())); - } - } - None -} - -/// Evaluates PHP URL encode builtins over one eval string expression. -pub(in crate::interpreter) fn eval_builtin_url_encode( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_url_encode_result(name, value, values) -} - -/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. -pub(in crate::interpreter) fn eval_url_encode_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - const HEX: &[u8; 16] = b"0123456789ABCDEF"; - for byte in bytes { - if eval_url_encode_keeps_byte(name, byte)? { - output.push(byte); - } else if name == "urlencode" && byte == b' ' { - output.push(b'+'); - } else { - output.push(b'%'); - output.push(HEX[(byte >> 4) as usize]); - output.push(HEX[(byte & 0x0f) as usize]); - } - } - values.string_bytes_value(&output) -} - -/// Returns whether a byte remains unescaped for the selected PHP URL encoder. -pub(in crate::interpreter) fn eval_url_encode_keeps_byte( - name: &str, - byte: u8, -) -> Result { - let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); - match name { - "urlencode" => Ok(common), - "rawurlencode" => Ok(common || byte == b'~'), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP URL decode builtins over one eval string expression. -pub(in crate::interpreter) fn eval_builtin_url_decode( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_url_decode_result(name, value, values) -} - -/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. -pub(in crate::interpreter) fn eval_url_decode_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let plus_to_space = match name { - "urldecode" => true, - "rawurldecode" => false, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'+' && plus_to_space { - output.push(b' '); - index += 1; - } else if bytes[index] == b'%' && index + 2 < bytes.len() { - if let (Some(high), Some(low)) = ( - eval_hex_nibble(bytes[index + 1]), - eval_hex_nibble(bytes[index + 2]), - ) { - output.push((high << 4) | low); - index += 3; - continue; - } - output.push(bytes[index]); - index += 1; - } else { - output.push(bytes[index]); - index += 1; - } - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP `ctype_*` predicates over one eval string expression. -pub(in crate::interpreter) fn eval_builtin_ctype( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_ctype_result(name, value, values) -} - -/// Returns the PHP boolean result for one ASCII `ctype_*` byte-string check. -pub(in crate::interpreter) fn eval_ctype_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut matches = !bytes.is_empty(); - for byte in bytes { - if !eval_ctype_byte_matches(name, byte)? { - matches = false; - break; - } - } - values.bool_value(matches) -} - -/// Checks one byte against the selected PHP ASCII character class. -pub(in crate::interpreter) fn eval_ctype_byte_matches( - name: &str, - byte: u8, -) -> Result { - match name { - "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), - "ctype_digit" => Ok(byte.is_ascii_digit()), - "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), - "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `crc32(...)` over one eval string expression. -pub(in crate::interpreter) fn eval_builtin_crc32( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_crc32_result(value, values) -} - -/// Computes PHP's non-negative CRC-32 integer over one converted byte string. -pub(in crate::interpreter) fn eval_crc32_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.int(i64::from(eval_crc32_bytes(&bytes))) -} - -/// Evaluates one-shot PHP hash digest builtins over eval expressions. -pub(in crate::interpreter) fn eval_builtin_hash_one_shot( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_hash_one_shot_result(name, &evaluated_args, values) -} - -/// Computes the result for one-shot PHP hash digest builtins from evaluated args. -pub(in crate::interpreter) fn eval_hash_one_shot_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "md5" | "sha1" => { - let (data, binary) = match evaluated_args { - [data] => (*data, false), - [data, binary] => (*data, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let data = values.string_bytes(data)?; - eval_hash_digest_result(name.as_bytes(), &data, binary, values) - } - "hash" => { - let (algo, data, binary) = match evaluated_args { - [algo, data] => (*algo, *data, false), - [algo, data, binary] => (*algo, *data, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let algo = values.string_bytes(algo)?; - let data = values.string_bytes(data)?; - eval_hash_digest_result(&algo, &data, binary, values) - } - "hash_file" => { - let (algo, filename, binary) = match evaluated_args { - [algo, filename] => (*algo, *filename, false), - [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_hash_file_result(algo, filename, binary, values) - } - "hash_hmac" => { - let (algo, data, key, binary) = match evaluated_args { - [algo, data, key] => (*algo, *data, *key, false), - [algo, data, key, binary] => (*algo, *data, *key, values.truthy(*binary)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let algo = values.string_bytes(algo)?; - let data = values.string_bytes(data)?; - let key = values.string_bytes(key)?; - eval_hash_hmac_result(&algo, &data, &key, binary, values) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Reads a local file and returns its PHP hash digest or false when it cannot be read. -pub(in crate::interpreter) fn eval_hash_file_result( - algo: RuntimeCellHandle, - filename: RuntimeCellHandle, - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let algo = values.string_bytes(algo)?; - let path = eval_path_string(filename, values)?; - match std::fs::read(path) { - Ok(data) => eval_hash_digest_result(&algo, &data, binary, values), - Err(_) => values.bool_value(false), - } -} - -/// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. -pub(in crate::interpreter) fn eval_hash_digest_result( - algo: &[u8], - data: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let raw = eval_crypto_hash(algo, data)?; - eval_format_digest_result(&raw, binary, values) -} - -/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. -pub(in crate::interpreter) fn eval_hash_hmac_result( - algo: &[u8], - data: &[u8], - key: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let raw = eval_crypto_hmac(algo, data, key)?; - eval_format_digest_result(&raw, binary, values) -} - -/// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. -pub(in crate::interpreter) fn eval_crypto_hash( - algo: &[u8], - data: &[u8], -) -> Result, EvalStatus> { - let mut output = [0_u8; 64]; - let len = unsafe { - elephc_crypto::elephc_crypto_hash( - algo.as_ptr(), - algo.len(), - data.as_ptr(), - data.len(), - output.as_mut_ptr(), - ) - }; - eval_crypto_digest_bytes(len, &output) -} - -/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. -pub(in crate::interpreter) fn eval_crypto_hmac( - algo: &[u8], - data: &[u8], - key: &[u8], -) -> Result, EvalStatus> { - let mut output = [0_u8; 64]; - let len = unsafe { - elephc_crypto::elephc_crypto_hmac( - algo.as_ptr(), - algo.len(), - key.as_ptr(), - key.len(), - data.as_ptr(), - data.len(), - output.as_mut_ptr(), - ) - }; - eval_crypto_digest_bytes(len, &output) -} - -/// Converts a crypto ABI digest length into an owned digest byte vector. -pub(in crate::interpreter) fn eval_crypto_digest_bytes( - len: isize, - output: &[u8; 64], -) -> Result, EvalStatus> { - let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; - if len > output.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(output[..len].to_vec()) -} - -/// Formats a raw digest using PHP's `$binary` flag convention. -pub(in crate::interpreter) fn eval_format_digest_result( - raw: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - if binary { - return values.string_bytes_value(raw); - } - values.string(&eval_lower_hex_bytes(raw)) -} - -/// Evaluates PHP `hash_algos()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_hash_algos( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values) -} - -/// Builds the indexed array returned by eval `hash_algos()`. -pub(in crate::interpreter) fn eval_hash_algos_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_HASH_ALGOS, values) -} - -/// Builds one indexed PHP array from a static string slice. -pub(in crate::interpreter) fn eval_static_string_array_result( - items: &[&str], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(items.len())?; - for (index, item) in items.iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string(item)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `spl_classes()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_spl_classes( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values) -} - -/// Builds the static class-name list returned by eval `spl_classes()`. -pub(in crate::interpreter) fn eval_spl_classes_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) -} - -/// Evaluates PHP stream introspection list builtins with no arguments. -pub(in crate::interpreter) fn eval_builtin_stream_introspection( - name: &str, - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_introspection_result(name, values) -} - -/// Builds the static list returned by one eval stream introspection builtin. -pub(in crate::interpreter) fn eval_stream_introspection_result( - name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let items = match name { - "stream_get_filters" => EVAL_STREAM_FILTERS, - "stream_get_transports" => EVAL_STREAM_TRANSPORTS, - "stream_get_wrappers" => EVAL_STREAM_WRAPPERS, - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_static_string_array_result(items, values) -} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/ctype.rs b/crates/elephc-eval/src/interpreter/builtins/strings/ctype.rs new file mode 100644 index 0000000000..78a4d5daa8 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/ctype.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! ASCII ctype predicate builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; + +/// Evaluates PHP `ctype_*` predicates over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ctype_result(name, value, values) +} + +/// Returns the PHP boolean result for one ASCII `ctype_*` byte-string check. +pub(in crate::interpreter) fn eval_ctype_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut matches = !bytes.is_empty(); + for byte in bytes { + if !eval_ctype_byte_matches(name, byte)? { + matches = false; + break; + } + } + values.bool_value(matches) +} + +/// Checks one byte against the selected PHP ASCII character class. +pub(in crate::interpreter) fn eval_ctype_byte_matches( + name: &str, + byte: u8, +) -> Result { + match name { + "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), + "ctype_digit" => Ok(byte.is_ascii_digit()), + "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), + "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/hash.rs b/crates/elephc-eval/src/interpreter/builtins/strings/hash.rs new file mode 100644 index 0000000000..01e0a0ee82 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/hash.rs @@ -0,0 +1,199 @@ +//! Purpose: +//! CRC32 and one-shot hash digest builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `crc32(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_crc32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_crc32_result(value, values) +} + +/// Computes PHP's non-negative CRC-32 integer over one converted byte string. +pub(in crate::interpreter) fn eval_crc32_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(eval_crc32_bytes(&bytes))) +} + +/// Evaluates one-shot PHP hash digest builtins over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_one_shot( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_hash_one_shot_result(name, &evaluated_args, values) +} + +/// Computes the result for one-shot PHP hash digest builtins from evaluated args. +pub(in crate::interpreter) fn eval_hash_one_shot_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "md5" | "sha1" => { + let (data, binary) = match evaluated_args { + [data] => (*data, false), + [data, binary] => (*data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let data = values.string_bytes(data)?; + eval_hash_digest_result(name.as_bytes(), &data, binary, values) + } + "hash" => { + let (algo, data, binary) = match evaluated_args { + [algo, data] => (*algo, *data, false), + [algo, data, binary] => (*algo, *data, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + eval_hash_digest_result(&algo, &data, binary, values) + } + "hash_file" => { + let (algo, filename, binary) = match evaluated_args { + [algo, filename] => (*algo, *filename, false), + [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_hash_file_result(algo, filename, binary, values) + } + "hash_hmac" => { + let (algo, data, key, binary) = match evaluated_args { + [algo, data, key] => (*algo, *data, *key, false), + [algo, data, key, binary] => (*algo, *data, *key, values.truthy(*binary)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let algo = values.string_bytes(algo)?; + let data = values.string_bytes(data)?; + let key = values.string_bytes(key)?; + eval_hash_hmac_result(&algo, &data, &key, binary, values) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Reads a local file and returns its PHP hash digest or false when it cannot be read. +pub(in crate::interpreter) fn eval_hash_file_result( + algo: RuntimeCellHandle, + filename: RuntimeCellHandle, + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(data) => eval_hash_digest_result(&algo, &data, binary, values), + Err(_) => values.bool_value(false), + } +} + +/// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. +pub(in crate::interpreter) fn eval_hash_digest_result( + algo: &[u8], + data: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hash(algo, data)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. +pub(in crate::interpreter) fn eval_hash_hmac_result( + algo: &[u8], + data: &[u8], + key: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hmac(algo, data, key)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. +pub(in crate::interpreter) fn eval_crypto_hash( + algo: &[u8], + data: &[u8], +) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hash( + algo.as_ptr(), + algo.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. +pub(in crate::interpreter) fn eval_crypto_hmac( + algo: &[u8], + data: &[u8], + key: &[u8], +) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hmac( + algo.as_ptr(), + algo.len(), + key.as_ptr(), + key.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + eval_crypto_digest_bytes(len, &output) +} + +/// Converts a crypto ABI digest length into an owned digest byte vector. +pub(in crate::interpreter) fn eval_crypto_digest_bytes( + len: isize, + output: &[u8; 64], +) -> Result, EvalStatus> { + let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + if len > output.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(output[..len].to_vec()) +} + +/// Formats a raw digest using PHP's `$binary` flag convention. +pub(in crate::interpreter) fn eval_format_digest_result( + raw: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if binary { + return values.string_bytes_value(raw); + } + values.string(&eval_lower_hex_bytes(raw)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/html.rs b/crates/elephc-eval/src/interpreter/builtins/strings/html.rs new file mode 100644 index 0000000000..84ca0f1211 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/html.rs @@ -0,0 +1,97 @@ +//! Purpose: +//! HTML entity encode/decode builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; + +/// Evaluates eval HTML entity encode/decode builtins over one string expression. +pub(in crate::interpreter) fn eval_builtin_html_entity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_html_entity_result(name, value, values) +} + +/// Applies the eval-supported HTML entity transform for one PHP string value. +pub(in crate::interpreter) fn eval_html_entity_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), + "html_entity_decode" => eval_html_entity_decode_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Encodes the HTML-special byte characters covered by elephc's static helper. +pub(in crate::interpreter) fn eval_htmlspecialchars_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + b'&' => output.extend_from_slice(b"&"), + b'<' => output.extend_from_slice(b"<"), + b'>' => output.extend_from_slice(b">"), + b'"' => output.extend_from_slice(b"""), + b'\'' => output.extend_from_slice(b"'"), + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Decodes one pass of the HTML entities emitted by the eval/static encoders. +pub(in crate::interpreter) fn eval_html_entity_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'&' { + if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { + output.push(decoded); + index += width; + continue; + } + } + output.push(bytes[index]); + index += 1; + } + values.string_bytes_value(&output) +} + +/// Returns the decoded byte and consumed width for one supported HTML entity. +pub(in crate::interpreter) fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { + for (entity, decoded) in [ + (b"<".as_slice(), b'<'), + (b">".as_slice(), b'>'), + (b""".as_slice(), b'"'), + (b"'".as_slice(), b'\''), + (b"'".as_slice(), b'\''), + (b"&".as_slice(), b'&'), + ] { + if bytes.starts_with(entity) { + return Some((decoded, entity.len())); + } + } + None +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs b/crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs new file mode 100644 index 0000000000..1adba529aa --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs @@ -0,0 +1,87 @@ +//! Purpose: +//! Hash algorithm, SPL class, and stream introspection list builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; + +/// Evaluates PHP `hash_algos()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_hash_algos( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + +/// Builds the indexed array returned by eval `hash_algos()`. +pub(in crate::interpreter) fn eval_hash_algos_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_HASH_ALGOS, values) +} + +/// Builds one indexed PHP array from a static string slice. +pub(in crate::interpreter) fn eval_static_string_array_result( + items: &[&str], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates PHP `spl_classes()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_spl_classes( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values) +} + +/// Builds the static class-name list returned by eval `spl_classes()`. +pub(in crate::interpreter) fn eval_spl_classes_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) +} + +/// Evaluates PHP stream introspection list builtins with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_introspection( + name: &str, + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, values) +} + +/// Builds the static list returned by one eval stream introspection builtin. +pub(in crate::interpreter) fn eval_stream_introspection_result( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let items = match name { + "stream_get_filters" => EVAL_STREAM_FILTERS, + "stream_get_transports" => EVAL_STREAM_TRANSPORTS, + "stream_get_wrappers" => EVAL_STREAM_WRAPPERS, + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_static_string_array_result(items, values) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs b/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs new file mode 100644 index 0000000000..ef506badfb --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Groups string, hash, ctype, SPL, and stream-introspection eval builtins. +//! Submodules are split by PHP builtin family and re-exported for call dispatch. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime behavior. + +mod ctype; +mod hash; +mod html; +mod introspection; +mod nl2br; +mod pad; +mod repeat; +mod replace; +mod simple; +mod split; +mod substr; +mod url; + +pub(in crate::interpreter) use ctype::*; +pub(in crate::interpreter) use hash::*; +pub(in crate::interpreter) use html::*; +pub(in crate::interpreter) use introspection::*; +pub(in crate::interpreter) use nl2br::*; +pub(in crate::interpreter) use pad::*; +pub(in crate::interpreter) use repeat::*; +pub(in crate::interpreter) use replace::*; +pub(in crate::interpreter) use simple::*; +pub(in crate::interpreter) use split::*; +pub(in crate::interpreter) use substr::*; +pub(in crate::interpreter) use url::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/nl2br.rs b/crates/elephc-eval/src/interpreter/builtins/strings/nl2br.rs new file mode 100644 index 0000000000..0ce573bed5 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/nl2br.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Newline-to-break string builtin. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; + +/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. +pub(in crate::interpreter) fn eval_builtin_nl2br( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_nl2br_result(value, true, values) + } + [value, use_xhtml] => { + let value = eval_expr(value, context, scope, values)?; + let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; + let use_xhtml = values.truthy(use_xhtml)?; + eval_nl2br_result(value, use_xhtml, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. +pub(in crate::interpreter) fn eval_nl2br_result( + value: RuntimeCellHandle, + use_xhtml: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let br = if use_xhtml { + b"
".as_slice() + } else { + b"
".as_slice() + }; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + let byte = bytes[index]; + if byte == b'\r' || byte == b'\n' { + output.extend_from_slice(br); + output.push(byte); + if index + 1 < bytes.len() + && ((byte == b'\r' && bytes[index + 1] == b'\n') + || (byte == b'\n' && bytes[index + 1] == b'\r')) + { + output.push(bytes[index + 1]); + index += 2; + continue; + } + } else { + output.push(byte); + } + index += 1; + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/pad.rs b/crates/elephc-eval/src/interpreter/builtins/strings/pad.rs new file mode 100644 index 0000000000..9189031406 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/pad.rs @@ -0,0 +1,106 @@ +//! Purpose: +//! String padding builtin. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. +pub(in crate::interpreter) fn eval_builtin_str_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_pad_result(value, length, None, None, values) + } + [value, length, pad_string] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), None, values) + } + [value, length, pad_string, pad_type] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + let pad_type = eval_expr(pad_type, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Pads one byte string to a PHP target length using cyclic pad bytes. +pub(in crate::interpreter) fn eval_str_pad_result( + value: RuntimeCellHandle, + length: RuntimeCellHandle, + pad_string: Option, + pad_type: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let target_length = eval_int_value(length, values)?; + let Ok(target_length) = usize::try_from(target_length) else { + return values.string_bytes_value(&bytes); + }; + if target_length <= bytes.len() { + return values.string_bytes_value(&bytes); + } + + let pad_string = match pad_string { + Some(pad_string) => values.string_bytes(pad_string)?, + None => b" ".to_vec(), + }; + if pad_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let pad_type = match pad_type { + Some(pad_type) => eval_int_value(pad_type, values)?, + None => 1, + }; + let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; + let capacity = bytes + .len() + .checked_add(left_pad) + .and_then(|size| size.checked_add(right_pad)) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + eval_append_repeated_pad(&mut output, &pad_string, left_pad); + output.extend_from_slice(&bytes); + eval_append_repeated_pad(&mut output, &pad_string, right_pad); + values.string_bytes_value(&output) +} + +/// Splits a `str_pad()` pad budget into left and right byte counts. +pub(in crate::interpreter) fn eval_str_pad_sides( + pad_budget: usize, + pad_type: i64, +) -> Result<(usize, usize), EvalStatus> { + match pad_type { + 0 => Ok((pad_budget, 0)), + 1 => Ok((0, pad_budget)), + 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends `count` bytes by cycling through the provided non-empty pad string. +pub(in crate::interpreter) fn eval_append_repeated_pad( + output: &mut Vec, + pad_string: &[u8], + count: usize, +) { + for index in 0..count { + output.push(pad_string[index % pad_string.len()]); + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/repeat.rs b/crates/elephc-eval/src/interpreter/builtins/strings/repeat.rs new file mode 100644 index 0000000000..68c1e2e670 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/repeat.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! String repeat builtin. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. +pub(in crate::interpreter) fn eval_builtin_str_repeat( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, times] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let times = eval_expr(times, context, scope, values)?; + eval_str_repeat_result(value, times, values) +} + +/// Repeats one PHP string byte sequence according to a PHP-cast integer count. +pub(in crate::interpreter) fn eval_str_repeat_result( + value: RuntimeCellHandle, + times: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let times = eval_int_value(times, values)?; + if times < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; + let capacity = bytes + .len() + .checked_mul(times) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + for _ in 0..times { + output.extend_from_slice(&bytes); + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/replace.rs b/crates/elephc-eval/src/interpreter/builtins/strings/replace.rs new file mode 100644 index 0000000000..d074e80a07 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/replace.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! String replace builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_str_replace( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [search, replace, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let search = eval_expr(search, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_str_replace_result(name, search, replace, subject, values) +} + +/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. +pub(in crate::interpreter) fn eval_str_replace_result( + name: &str, + search: RuntimeCellHandle, + replace: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let search = values.string_bytes(search)?; + let replace = values.string_bytes(replace)?; + let subject = values.string_bytes(subject)?; + if search.is_empty() { + return values.string_bytes_value(&subject); + } + + let mut output = Vec::with_capacity(subject.len()); + let mut start = 0; + while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { + output.extend_from_slice(&subject[start..found]); + output.extend_from_slice(&replace); + start = found + search.len(); + } + output.extend_from_slice(&subject[start..]); + values.string_bytes_value(&output) +} + +/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. +pub(in crate::interpreter) fn eval_find_replace_match( + name: &str, + subject: &[u8], + search: &[u8], + start: usize, +) -> Result, EvalStatus> { + match name { + "str_replace" => Ok(eval_find_subslice(subject, search, start)), + "str_ireplace" => Ok(subject + .get(start..) + .and_then(|tail| { + tail.windows(search.len()) + .position(|window| window.eq_ignore_ascii_case(search)) + }) + .map(|position| position + start)), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/simple.rs b/crates/elephc-eval/src/interpreter/builtins/strings/simple.rs new file mode 100644 index 0000000000..52568f1a2c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/simple.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Small scalar string wrappers such as sqrt, strrev, and chr. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP's `sqrt(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sqrt( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.sqrt(value) +} + +/// Evaluates PHP's `strrev(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.strrev(value) +} + +/// Evaluates PHP's `chr(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_chr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_chr_result(value, values) +} + +/// Converts one eval value to a PHP integer and returns the low byte as a string. +pub(in crate::interpreter) fn eval_chr_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + values.string_bytes_value(&[value as u8]) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/split.rs b/crates/elephc-eval/src/interpreter/builtins/strings/split.rs new file mode 100644 index 0000000000..9615e57124 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/split.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! String split builtin. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `str_split(...)` over one string and optional chunk length. +pub(in crate::interpreter) fn eval_builtin_str_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_str_split_result(value, None, values) + } + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_split_result(value, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. +pub(in crate::interpreter) fn eval_str_split_result( + value: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let length = match length { + Some(length) => eval_int_value(length, values)?, + None => 1, + }; + if length <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = values.array_new(0)?; + for (index, chunk) in bytes.chunks(length).enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string_bytes_value(chunk)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/substr.rs b/crates/elephc-eval/src/interpreter/builtins/strings/substr.rs new file mode 100644 index 0000000000..44345a18a8 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/substr.rs @@ -0,0 +1,130 @@ +//! Purpose: +//! Substring and substring replacement builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. +pub(in crate::interpreter) fn eval_builtin_substr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, offset] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_result(value, offset, None, values) + } + [value, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_result(value, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Slices a PHP byte string using PHP `substr()` offset and length rules. +pub(in crate::interpreter) fn eval_substr_result( + value: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(0) + } else { + start.saturating_add(length).min(total) + } + } + }; + let end = end.max(start); + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string_bytes_value(&bytes[start..end]) +} + +/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. +pub(in crate::interpreter) fn eval_builtin_substr_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, replace, offset] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, None, values) + } + [value, replace, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. +pub(in crate::interpreter) fn eval_substr_replace_result( + value: RuntimeCellHandle, + replace: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let replacement = values.string_bytes(replace)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(start) + } else { + start.saturating_add(length).min(total) + } + } + }; + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(bytes.len() + replacement.len()); + output.extend_from_slice(&bytes[..start]); + output.extend_from_slice(&replacement); + output.extend_from_slice(&bytes[end..]); + values.string_bytes_value(&output) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/url.rs b/crates/elephc-eval/src/interpreter/builtins/strings/url.rs new file mode 100644 index 0000000000..42171253fd --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/url.rs @@ -0,0 +1,114 @@ +//! Purpose: +//! URL encode and decode builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports. +//! +//! Key details: +//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP URL encode builtins over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_url_encode( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_encode_result(name, value, values) +} + +/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. +pub(in crate::interpreter) fn eval_url_encode_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + for byte in bytes { + if eval_url_encode_keeps_byte(name, byte)? { + output.push(byte); + } else if name == "urlencode" && byte == b' ' { + output.push(b'+'); + } else { + output.push(b'%'); + output.push(HEX[(byte >> 4) as usize]); + output.push(HEX[(byte & 0x0f) as usize]); + } + } + values.string_bytes_value(&output) +} + +/// Returns whether a byte remains unescaped for the selected PHP URL encoder. +pub(in crate::interpreter) fn eval_url_encode_keeps_byte( + name: &str, + byte: u8, +) -> Result { + let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); + match name { + "urlencode" => Ok(common), + "rawurlencode" => Ok(common || byte == b'~'), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates PHP URL decode builtins over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_url_decode( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_decode_result(name, value, values) +} + +/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. +pub(in crate::interpreter) fn eval_url_decode_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let plus_to_space = match name { + "urldecode" => true, + "rawurldecode" => false, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'+' && plus_to_space { + output.push(b' '); + index += 1; + } else if bytes[index] == b'%' && index + 2 < bytes.len() { + if let (Some(high), Some(low)) = ( + eval_hex_nibble(bytes[index + 1]), + eval_hex_nibble(bytes[index + 2]), + ) { + output.push((high << 4) | low); + index += 3; + continue; + } + output.push(bytes[index]); + index += 1; + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} From 0e2c6946ccbf300b3f4cc4d301aa0f0b400bc4d7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:35:45 +0200 Subject: [PATCH 0288/1208] Split eval regex builtins --- .../src/interpreter/builtins/regex.rs | 858 ------------------ .../interpreter/builtins/regex/captures.rs | 99 ++ .../interpreter/builtins/regex/match_all.rs | 186 ++++ .../interpreter/builtins/regex/match_one.rs | 126 +++ .../src/interpreter/builtins/regex/mod.rs | 29 + .../src/interpreter/builtins/regex/pattern.rs | 130 +++ .../src/interpreter/builtins/regex/replace.rs | 98 ++ .../interpreter/builtins/regex/replacement.rs | 101 +++ .../src/interpreter/builtins/regex/split.rs | 89 ++ .../builtins/regex/split_helpers.rs | 115 +++ 10 files changed, 973 insertions(+), 858 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/regex.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/captures.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/match_all.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/match_one.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/pattern.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/replace.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/replacement.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/split.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/regex/split_helpers.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex.rs b/crates/elephc-eval/src/interpreter/builtins/regex.rs deleted file mode 100644 index 24c85469b3..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/regex.rs +++ /dev/null @@ -1,858 +0,0 @@ -//! Purpose: -//! PCRE-style preg builtins and regex capture helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins` re-exports used by core call dispatch. -//! -//! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime -//! behavior through `RuntimeValueOps`. - -use super::super::*; -use super::*; - -/// Evaluates PHP `preg_match()` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_preg_match( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_match_result(pattern, subject, values) - } - [pattern, subject, matches] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let (result, matches_array) = - eval_preg_match_capture_result(pattern, subject, None, values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - [pattern, subject, matches, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let flags = eval_expr(flags, context, scope, values)?; - let (result, matches_array) = - eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns whether one regex matches the subject string. -pub(in crate::interpreter) fn eval_preg_match_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - values.int(i64::from(regex.is_match(&subject))) -} - -/// Returns the match flag plus PHP `$matches` capture array for one regex search. -pub(in crate::interpreter) fn eval_preg_match_capture_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let flags = eval_preg_match_flags(flags, values)?; - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - if let Some(captures) = regex.captures(&subject) { - let matches = eval_preg_capture_array( - &subject, - Some(&captures), - offset_capture, - unmatched_as_null, - values, - )?; - let matched = values.int(1)?; - return Ok((matched, matches)); - } - let matches = - eval_preg_capture_array(&subject, None, offset_capture, unmatched_as_null, values)?; - let matched = values.int(0)?; - Ok((matched, matches)) -} - -/// Returns supported `preg_match()` flags. -pub(in crate::interpreter) fn eval_preg_match_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(0); - }; - let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_OFFSET_CAPTURE | EVAL_PREG_UNMATCHED_AS_NULL; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Evaluates PHP `preg_match_all()` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_preg_match_all( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_match_all_result(pattern, subject, values) - } - [pattern, subject, matches] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let (result, matches_array) = - eval_preg_match_all_capture_result(pattern, subject, None, values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - [pattern, subject, matches, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; - let flags = eval_expr(flags, context, scope, values)?; - let (result, matches_array) = - eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - Ok(result) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Counts all non-overlapping regex matches in one subject string. -pub(in crate::interpreter) fn eval_preg_match_all_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let count = regex.captures_iter(&subject).count(); - values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Returns the match count plus PHP's default `PREG_PATTERN_ORDER` `$matches` array. -pub(in crate::interpreter) fn eval_preg_match_all_capture_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let regex = eval_preg_regex(pattern, values)?; - let capture_count = regex.captures_len(); - let subject = values.string_bytes(subject)?; - let captures: Vec> = regex.captures_iter(&subject).collect(); - let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let flags = eval_preg_match_all_flags(flags, values)?; - let matches = if flags & EVAL_PREG_SET_ORDER != 0 { - eval_preg_match_all_set_order_array(&subject, &captures, capture_count, flags, values)? - } else { - eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, flags, values)? - }; - Ok((count, matches)) -} - -/// Returns supported `preg_match_all()` flags. -pub(in crate::interpreter) fn eval_preg_match_all_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(EVAL_PREG_PATTERN_ORDER); - }; - let flags = eval_int_value(flags, values)?; - let supported = EVAL_PREG_PATTERN_ORDER - | EVAL_PREG_SET_ORDER - | EVAL_PREG_OFFSET_CAPTURE - | EVAL_PREG_UNMATCHED_AS_NULL; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Builds PHP's default `preg_match_all()` pattern-order capture matrix. -pub(in crate::interpreter) fn eval_preg_match_all_pattern_order_array( - subject: &[u8], - captures: &[Captures<'_>], - capture_count: usize, - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - let mut outer = values.array_new(capture_count)?; - for capture_index in 0..capture_count { - let mut row = values.array_new(captures.len())?; - for (match_index, capture) in captures.iter().enumerate() { - let key = - values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - capture, - capture_index, - offset_capture, - unmatched_as_null, - values, - )?; - row = values.array_set(row, key, value)?; - } - let key = - values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - outer = values.array_set(outer, key, row)?; - } - Ok(outer) -} - -/// Builds PHP's `preg_match_all(..., PREG_SET_ORDER)` match-order capture matrix. -pub(in crate::interpreter) fn eval_preg_match_all_set_order_array( - subject: &[u8], - captures: &[Captures<'_>], - capture_count: usize, - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; - let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; - let mut outer = values.array_new(captures.len())?; - for (match_index, capture) in captures.iter().enumerate() { - let mut row = values.array_new(capture_count)?; - for capture_index in 0..capture_count { - let key = - values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - capture, - capture_index, - offset_capture, - unmatched_as_null, - values, - )?; - row = values.array_set(row, key, value)?; - } - let key = values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - outer = values.array_set(outer, key, row)?; - } - Ok(outer) -} - -/// Evaluates PHP `preg_replace()` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_preg_replace( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern, replacement, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - let replacement = eval_expr(replacement, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_replace_result(pattern, replacement, subject, values) -} - -/// Replaces every regex match with a PHP-style backreference-expanded replacement. -pub(in crate::interpreter) fn eval_preg_replace_result( - pattern: RuntimeCellHandle, - replacement: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let replacement = values.string_bytes(replacement)?; - let subject = values.string_bytes(subject)?; - let mut result = Vec::with_capacity(subject.len()); - let mut cursor = 0; - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - result.extend_from_slice(&subject[cursor..matched.start()]); - eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); - cursor = matched.end(); - } - result.extend_from_slice(&subject[cursor..]); - values.string_bytes_value(&result) -} - -/// Evaluates PHP `preg_replace_callback()` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_preg_replace_callback( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern, callback, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_replace_callback_result(pattern, callback, subject, context, values) -} - -/// Replaces every regex match by invoking an eval-supported callback with `$matches`. -pub(in crate::interpreter) fn eval_preg_replace_callback_result( - pattern: RuntimeCellHandle, - callback: RuntimeCellHandle, - subject: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let callback = eval_callable_name(callback, values)?; - let subject = values.string_bytes(subject)?; - let mut result = Vec::with_capacity(subject.len()); - let mut cursor = 0; - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - result.extend_from_slice(&subject[cursor..matched.start()]); - let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; - let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; - let callback_result = values.cast_string(callback_result)?; - let callback_bytes = values.string_bytes(callback_result)?; - result.extend_from_slice(&callback_bytes); - cursor = matched.end(); - } - result.extend_from_slice(&subject[cursor..]); - values.string_bytes_value(&result) -} - -/// Evaluates PHP `preg_split()` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_preg_split( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [pattern, subject] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_split_result(pattern, subject, None, None, values) - } - [pattern, subject, limit] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let limit = eval_expr(limit, context, scope, values)?; - eval_preg_split_result(pattern, subject, Some(limit), None, values) - } - [pattern, subject, limit, flags] => { - let pattern = eval_expr(pattern, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - let limit = eval_expr(limit, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_preg_split_result(pattern, subject, Some(limit), Some(flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Splits a subject string with eval-supported `preg_split()` flags. -pub(in crate::interpreter) fn eval_preg_split_result( - pattern: RuntimeCellHandle, - subject: RuntimeCellHandle, - limit: Option, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let subject = values.string_bytes(subject)?; - let limit = eval_preg_split_limit(limit, values)?; - let flags = eval_preg_split_flags(flags, values)?; - let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; - let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; - let offset_capture = flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0; - let mut pieces = Vec::::new(); - let mut cursor = 0; - - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - if eval_preg_split_reached_limit(&pieces, limit) { - break; - } - eval_preg_split_push_piece( - &mut pieces, - &subject[cursor..matched.start()], - cursor, - no_empty, - ); - if capture_delimiters { - eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); - } - cursor = matched.end(); - } - eval_preg_split_push_piece(&mut pieces, &subject[cursor..], cursor, no_empty); - - let mut result = values.array_new(pieces.len())?; - for (index, piece) in pieces.iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_split_piece_value(piece, offset_capture, values)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Compiles one eval PCRE-style delimited pattern into a Rust regex. -pub(in crate::interpreter) fn eval_preg_regex( - pattern: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = values.string_bytes(pattern)?; - let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; - let body = String::from_utf8(body).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut builder = RegexBuilder::new(&body); - builder - .case_insensitive(modifiers.case_insensitive) - .multi_line(modifiers.multi_line) - .dot_matches_new_line(modifiers.dot_matches_new_line) - .swap_greed(modifiers.swap_greed); - builder.build().map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Regex modifiers supported by eval `preg_*` pattern stripping. -#[derive(Default)] -pub(in crate::interpreter) struct EvalPregModifiers { - case_insensitive: bool, - multi_line: bool, - dot_matches_new_line: bool, - swap_greed: bool, -} - -/// One `preg_split()` output segment plus its byte offset in the subject. -pub(in crate::interpreter) struct EvalPregSplitPiece { - bytes: Vec, - offset: usize, -} - -/// Splits a PHP delimited regex into body bytes and supported modifiers. -pub(in crate::interpreter) fn eval_preg_pattern_parts( - pattern: &[u8], -) -> Result<(Vec, EvalPregModifiers), EvalStatus> { - if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() { - return Err(EvalStatus::RuntimeFatal); - } - let delimiter = pattern[0]; - if delimiter == b'\\' { - return Err(EvalStatus::RuntimeFatal); - } - let closing = eval_preg_closing_delimiter(delimiter); - let close_index = - eval_preg_find_closing_delimiter(pattern, closing).ok_or(EvalStatus::RuntimeFatal)?; - let body = eval_preg_unescape_delimiter(&pattern[1..close_index], delimiter, closing); - let modifiers = eval_preg_modifiers(&pattern[close_index + 1..])?; - Ok((body, modifiers)) -} - -/// Returns the closing regex delimiter for PHP's paired delimiter forms. -pub(in crate::interpreter) fn eval_preg_closing_delimiter(delimiter: u8) -> u8 { - match delimiter { - b'(' => b')', - b'[' => b']', - b'{' => b'}', - b'<' => b'>', - _ => delimiter, - } -} - -/// Finds the first unescaped closing regex delimiter. -pub(in crate::interpreter) fn eval_preg_find_closing_delimiter( - pattern: &[u8], - closing: u8, -) -> Option { - let mut escaped = false; - for (index, byte) in pattern.iter().copied().enumerate().skip(1) { - if escaped { - escaped = false; - continue; - } - if byte == b'\\' { - escaped = true; - continue; - } - if byte == closing { - return Some(index); - } - } - None -} - -/// Removes escapes that only protect the PHP regex delimiter from pattern stripping. -pub(in crate::interpreter) fn eval_preg_unescape_delimiter( - body: &[u8], - delimiter: u8, - closing: u8, -) -> Vec { - let mut result = Vec::with_capacity(body.len()); - let mut index = 0; - while index < body.len() { - if body[index] == b'\\' - && index + 1 < body.len() - && matches!(body[index + 1], byte if byte == delimiter || byte == closing) - { - result.push(body[index + 1]); - index += 2; - } else { - result.push(body[index]); - index += 1; - } - } - result -} - -/// Parses eval-supported PHP regex modifiers. -pub(in crate::interpreter) fn eval_preg_modifiers( - modifiers: &[u8], -) -> Result { - let mut parsed = EvalPregModifiers::default(); - for modifier in modifiers { - match *modifier { - b'i' => parsed.case_insensitive = true, - b'm' => parsed.multi_line = true, - b's' => parsed.dot_matches_new_line = true, - b'U' => parsed.swap_greed = true, - b'u' => {} - _ => return Err(EvalStatus::RuntimeFatal), - } - } - Ok(parsed) -} - -/// Builds PHP's indexed `$matches` capture array for one regex result. -pub(in crate::interpreter) fn eval_preg_capture_array( - subject: &[u8], - captures: Option<&Captures<'_>>, - offset_capture: bool, - unmatched_as_null: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = captures.map_or(0, |captures| { - eval_preg_visible_capture_len(captures, unmatched_as_null) - }); - let mut result = values.array_new(len)?; - if let Some(captures) = captures { - for index in 0..len { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = eval_preg_capture_value( - subject, - captures, - index, - offset_capture, - unmatched_as_null, - values, - )?; - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Returns the capture count PHP should expose, dropping trailing unmatched groups. -pub(in crate::interpreter) fn eval_preg_visible_capture_len( - captures: &Captures<'_>, - unmatched_as_null: bool, -) -> usize { - if unmatched_as_null { - return captures.len(); - } - let mut len = captures.len(); - while len > 1 && captures.get(len - 1).is_none() { - len -= 1; - } - len -} - -/// Returns one captured byte range from the original subject. -pub(in crate::interpreter) fn eval_preg_capture_bytes<'a>( - subject: &'a [u8], - captures: &Captures<'_>, - index: usize, -) -> Option<&'a [u8]> { - captures - .get(index) - .map(|matched| &subject[matched.start()..matched.end()]) -} - -/// Builds one capture entry as either a string or PHP's `[string, byte_offset]` pair. -pub(in crate::interpreter) fn eval_preg_capture_value( - subject: &[u8], - captures: &Captures<'_>, - index: usize, - offset_capture: bool, - unmatched_as_null: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let matched = captures.get(index); - let value = if matched.is_none() && unmatched_as_null { - values.null()? - } else { - let bytes = matched.as_ref().map_or(b"".as_slice(), |matched| { - &subject[matched.start()..matched.end()] - }); - values.string_bytes_value(bytes)? - }; - if !offset_capture { - return Ok(value); - } - - let offset = matched.map_or(Ok(-1_i64), |matched| { - i64::try_from(matched.start()).map_err(|_| EvalStatus::RuntimeFatal) - })?; - let offset = values.int(offset)?; - let mut pair = values.array_new(2)?; - let value_key = values.int(0)?; - pair = values.array_set(pair, value_key, value)?; - let offset_key = values.int(1)?; - values.array_set(pair, offset_key, offset) -} - -/// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. -pub(in crate::interpreter) fn eval_preg_expand_replacement( - replacement: &[u8], - subject: &[u8], - captures: &Captures<'_>, - result: &mut Vec, -) { - let mut index = 0; - while index < replacement.len() { - match replacement[index] { - b'$' => { - if let Some((capture_index, next_index)) = - eval_preg_replacement_capture_index(replacement, index + 1) - { - if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { - result.extend_from_slice(bytes); - } - index = next_index; - } else { - result.push(replacement[index]); - index += 1; - } - } - b'\\' if index + 1 < replacement.len() && replacement[index + 1].is_ascii_digit() => { - let (capture_index, next_index) = - eval_preg_decimal_capture_index(replacement, index + 1); - if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { - result.extend_from_slice(bytes); - } - index = next_index; - } - byte => { - result.push(byte); - index += 1; - } - } - } -} - -/// Parses a dollar-style replacement capture reference. -pub(in crate::interpreter) fn eval_preg_replacement_capture_index( - bytes: &[u8], - index: usize, -) -> Option<(usize, usize)> { - if bytes.get(index).copied() == Some(b'{') { - let mut cursor = index + 1; - let start = cursor; - while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { - cursor += 1; - } - if cursor == start || bytes.get(cursor).copied() != Some(b'}') { - return None; - } - let capture = eval_preg_decimal_bytes_to_usize(&bytes[start..cursor])?; - return Some((capture, cursor + 1)); - } - if bytes.get(index).is_some_and(u8::is_ascii_digit) { - let (capture, next) = eval_preg_decimal_capture_index(bytes, index); - return Some((capture, next)); - } - None -} - -/// Parses a one- or two-digit replacement capture reference. -pub(in crate::interpreter) fn eval_preg_decimal_capture_index( - bytes: &[u8], - index: usize, -) -> (usize, usize) { - let mut cursor = index; - let end = usize::min(bytes.len(), index + 2); - while cursor < end && bytes[cursor].is_ascii_digit() { - cursor += 1; - } - ( - eval_preg_decimal_bytes_to_usize(&bytes[index..cursor]).unwrap_or(0), - cursor, - ) -} - -/// Converts ASCII decimal bytes into a `usize` capture index. -pub(in crate::interpreter) fn eval_preg_decimal_bytes_to_usize(bytes: &[u8]) -> Option { - let mut value = 0usize; - for byte in bytes { - value = value.checked_mul(10)?; - value = value.checked_add(usize::from(byte - b'0'))?; - } - Some(value) -} - -/// Returns the PHP `preg_split()` limit, treating zero as unlimited. -pub(in crate::interpreter) fn eval_preg_split_limit( - limit: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(limit) = limit else { - return Ok(None); - }; - let limit = eval_int_value(limit, values)?; - if limit <= 0 { - return Ok(None); - } - usize::try_from(limit) - .map(Some) - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Returns supported `preg_split()` flags. -pub(in crate::interpreter) fn eval_preg_split_flags( - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = flags else { - return Ok(0); - }; - let flags = eval_int_value(flags, values)?; - let supported = - EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE | EVAL_PREG_SPLIT_OFFSET_CAPTURE; - if flags & !supported != 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(flags) -} - -/// Returns whether `preg_split()` should stop splitting and emit the remaining subject. -pub(in crate::interpreter) fn eval_preg_split_reached_limit( - pieces: &[EvalPregSplitPiece], - limit: Option, -) -> bool { - matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) -} - -/// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. -pub(in crate::interpreter) fn eval_preg_split_push_piece( - pieces: &mut Vec, - piece: &[u8], - offset: usize, - no_empty: bool, -) { - if no_empty && piece.is_empty() { - return; - } - pieces.push(EvalPregSplitPiece { - bytes: piece.to_vec(), - offset, - }); -} - -/// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. -pub(in crate::interpreter) fn eval_preg_split_push_captures( - pieces: &mut Vec, - subject: &[u8], - captures: &Captures<'_>, - no_empty: bool, -) { - for index in 1..captures.len() { - if let Some(matched) = captures.get(index) { - eval_preg_split_push_piece( - pieces, - &subject[matched.start()..matched.end()], - matched.start(), - no_empty, - ); - } - } -} - -/// Converts one split segment to a string or PHP `[string, byte_offset]` pair. -pub(in crate::interpreter) fn eval_preg_split_piece_value( - piece: &EvalPregSplitPiece, - offset_capture: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.string_bytes_value(&piece.bytes)?; - if !offset_capture { - return Ok(value); - } - - let offset = i64::try_from(piece.offset).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = values.int(offset)?; - let mut pair = values.array_new(2)?; - let value_key = values.int(0)?; - pair = values.array_set(pair, value_key, value)?; - let offset_key = values.int(1)?; - values.array_set(pair, offset_key, offset) -} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/captures.rs b/crates/elephc-eval/src/interpreter/builtins/regex/captures.rs new file mode 100644 index 0000000000..0cf6ffb672 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/captures.rs @@ -0,0 +1,99 @@ +//! Purpose: +//! Converts Rust regex captures into PHP-compatible eval arrays and values. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` match and replacement modules. +//! +//! Key details: +//! - Optional offset capture uses PHP's `[string, byte_offset]` representation and +//! unmatched captures follow `PREG_UNMATCHED_AS_NULL`. + +use super::super::super::*; + +/// Builds PHP's indexed `$matches` capture array for one regex result. +pub(in crate::interpreter) fn eval_preg_capture_array( + subject: &[u8], + captures: Option<&Captures<'_>>, + offset_capture: bool, + unmatched_as_null: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = captures.map_or(0, |captures| { + eval_preg_visible_capture_len(captures, unmatched_as_null) + }); + let mut result = values.array_new(len)?; + if let Some(captures) = captures { + for index in 0..len { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + captures, + index, + offset_capture, + unmatched_as_null, + values, + )?; + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Returns the capture count PHP should expose, dropping trailing unmatched groups. +pub(in crate::interpreter) fn eval_preg_visible_capture_len( + captures: &Captures<'_>, + unmatched_as_null: bool, +) -> usize { + if unmatched_as_null { + return captures.len(); + } + let mut len = captures.len(); + while len > 1 && captures.get(len - 1).is_none() { + len -= 1; + } + len +} + +/// Returns one captured byte range from the original subject. +pub(in crate::interpreter) fn eval_preg_capture_bytes<'a>( + subject: &'a [u8], + captures: &Captures<'_>, + index: usize, +) -> Option<&'a [u8]> { + captures + .get(index) + .map(|matched| &subject[matched.start()..matched.end()]) +} + +/// Builds one capture entry as either a string or PHP's `[string, byte_offset]` pair. +pub(in crate::interpreter) fn eval_preg_capture_value( + subject: &[u8], + captures: &Captures<'_>, + index: usize, + offset_capture: bool, + unmatched_as_null: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let matched = captures.get(index); + let value = if matched.is_none() && unmatched_as_null { + values.null()? + } else { + let bytes = matched.as_ref().map_or(b"".as_slice(), |matched| { + &subject[matched.start()..matched.end()] + }); + values.string_bytes_value(bytes)? + }; + if !offset_capture { + return Ok(value); + } + + let offset = matched.map_or(Ok(-1_i64), |matched| { + i64::try_from(matched.start()).map_err(|_| EvalStatus::RuntimeFatal) + })?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/match_all.rs b/crates/elephc-eval/src/interpreter/builtins/regex/match_all.rs new file mode 100644 index 0000000000..9a014ece3e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/match_all.rs @@ -0,0 +1,186 @@ +//! Purpose: +//! Implements eval support for PHP `preg_match_all()` and capture-matrix assembly. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` re-exports. +//! +//! Key details: +//! - Pattern-order and set-order arrays share the common capture-value helper so +//! offset and unmatched-null flags remain consistent. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP `preg_match_all()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_match_all( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_all_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, None, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Counts all non-overlapping regex matches in one subject string. +pub(in crate::interpreter) fn eval_preg_match_all_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let count = regex.captures_iter(&subject).count(); + values.int(i64::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Returns the match count plus PHP's default `PREG_PATTERN_ORDER` `$matches` array. +pub(in crate::interpreter) fn eval_preg_match_all_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let capture_count = regex.captures_len(); + let subject = values.string_bytes(subject)?; + let captures: Vec> = regex.captures_iter(&subject).collect(); + let count = values.int(i64::try_from(captures.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let flags = eval_preg_match_all_flags(flags, values)?; + let matches = if flags & EVAL_PREG_SET_ORDER != 0 { + eval_preg_match_all_set_order_array(&subject, &captures, capture_count, flags, values)? + } else { + eval_preg_match_all_pattern_order_array(&subject, &captures, capture_count, flags, values)? + }; + Ok((count, matches)) +} + +/// Returns supported `preg_match_all()` flags. +pub(in crate::interpreter) fn eval_preg_match_all_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(EVAL_PREG_PATTERN_ORDER); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_PATTERN_ORDER + | EVAL_PREG_SET_ORDER + | EVAL_PREG_OFFSET_CAPTURE + | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Builds PHP's default `preg_match_all()` pattern-order capture matrix. +pub(in crate::interpreter) fn eval_preg_match_all_pattern_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + let mut outer = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let mut row = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let key = + values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; + row = values.array_set(row, key, value)?; + } + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} + +/// Builds PHP's `preg_match_all(..., PREG_SET_ORDER)` match-order capture matrix. +pub(in crate::interpreter) fn eval_preg_match_all_set_order_array( + subject: &[u8], + captures: &[Captures<'_>], + capture_count: usize, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + let mut outer = values.array_new(captures.len())?; + for (match_index, capture) in captures.iter().enumerate() { + let mut row = values.array_new(capture_count)?; + for capture_index in 0..capture_count { + let key = + values.int(i64::try_from(capture_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_capture_value( + subject, + capture, + capture_index, + offset_capture, + unmatched_as_null, + values, + )?; + row = values.array_set(row, key, value)?; + } + let key = values.int(i64::try_from(match_index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + outer = values.array_set(outer, key, row)?; + } + Ok(outer) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/match_one.rs b/crates/elephc-eval/src/interpreter/builtins/regex/match_one.rs new file mode 100644 index 0000000000..33ed83e092 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/match_one.rs @@ -0,0 +1,126 @@ +//! Purpose: +//! Implements eval support for PHP `preg_match()` and its immediate flags/result +//! helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` re-exports. +//! +//! Key details: +//! - `$matches` assignment writes back through eval scope cells and preserves runtime +//! ownership by releasing replaced values. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP `preg_match()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_match( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_match_result(pattern, subject, values) + } + [pattern, subject, matches] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, None, values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + [pattern, subject, matches, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let EvalExpr::LoadVar(matches_name) = matches else { + return Err(EvalStatus::RuntimeFatal); + }; + let flags = eval_expr(flags, context, scope, values)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; + for replaced in set_scope_cell( + context, + scope, + matches_name.clone(), + matches_array, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(result) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether one regex matches the subject string. +pub(in crate::interpreter) fn eval_preg_match_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + values.int(i64::from(regex.is_match(&subject))) +} + +/// Returns the match flag plus PHP `$matches` capture array for one regex search. +pub(in crate::interpreter) fn eval_preg_match_capture_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let flags = eval_preg_match_flags(flags, values)?; + let offset_capture = flags & EVAL_PREG_OFFSET_CAPTURE != 0; + let unmatched_as_null = flags & EVAL_PREG_UNMATCHED_AS_NULL != 0; + if let Some(captures) = regex.captures(&subject) { + let matches = eval_preg_capture_array( + &subject, + Some(&captures), + offset_capture, + unmatched_as_null, + values, + )?; + let matched = values.int(1)?; + return Ok((matched, matches)); + } + let matches = + eval_preg_capture_array(&subject, None, offset_capture, unmatched_as_null, values)?; + let matched = values.int(0)?; + Ok((matched, matches)) +} + +/// Returns supported `preg_match()` flags. +pub(in crate::interpreter) fn eval_preg_match_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = EVAL_PREG_OFFSET_CAPTURE | EVAL_PREG_UNMATCHED_AS_NULL; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/mod.rs b/crates/elephc-eval/src/interpreter/builtins/regex/mod.rs new file mode 100644 index 0000000000..988f1c48d9 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/mod.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Groups PCRE-style preg eval builtins by entrypoint and shared helper domain. +//! Submodules keep `preg_match`, `preg_replace`, and `preg_split` behavior +//! separated while this module re-exports the interpreter-visible surface. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +mod captures; +mod match_all; +mod match_one; +mod pattern; +mod replace; +mod replacement; +mod split; +mod split_helpers; + +pub(in crate::interpreter) use captures::*; +pub(in crate::interpreter) use match_all::*; +pub(in crate::interpreter) use match_one::*; +pub(in crate::interpreter) use pattern::*; +pub(in crate::interpreter) use replace::*; +pub(in crate::interpreter) use replacement::*; +pub(in crate::interpreter) use split::*; +pub(in crate::interpreter) use split_helpers::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/pattern.rs b/crates/elephc-eval/src/interpreter/builtins/regex/pattern.rs new file mode 100644 index 0000000000..596b45a9b6 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/pattern.rs @@ -0,0 +1,130 @@ +//! Purpose: +//! Parses PHP delimited preg patterns and compiles them into Rust byte regexes. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` entrypoint modules. +//! +//! Key details: +//! - Only eval-supported modifiers are accepted; unsupported delimiters or +//! malformed patterns produce runtime fatal status. + +use super::super::super::*; + +/// Compiles one eval PCRE-style delimited pattern into a Rust regex. +pub(in crate::interpreter) fn eval_preg_regex( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = values.string_bytes(pattern)?; + let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; + let body = String::from_utf8(body).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut builder = RegexBuilder::new(&body); + builder + .case_insensitive(modifiers.case_insensitive) + .multi_line(modifiers.multi_line) + .dot_matches_new_line(modifiers.dot_matches_new_line) + .swap_greed(modifiers.swap_greed); + builder.build().map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Regex modifiers supported by eval `preg_*` pattern stripping. +#[derive(Default)] +pub(in crate::interpreter) struct EvalPregModifiers { + case_insensitive: bool, + multi_line: bool, + dot_matches_new_line: bool, + swap_greed: bool, +} + +/// Splits a PHP delimited regex into body bytes and supported modifiers. +pub(in crate::interpreter) fn eval_preg_pattern_parts( + pattern: &[u8], +) -> Result<(Vec, EvalPregModifiers), EvalStatus> { + if pattern.len() < 2 || pattern[0].is_ascii_alphanumeric() || pattern[0].is_ascii_whitespace() { + return Err(EvalStatus::RuntimeFatal); + } + let delimiter = pattern[0]; + if delimiter == b'\\' { + return Err(EvalStatus::RuntimeFatal); + } + let closing = eval_preg_closing_delimiter(delimiter); + let close_index = + eval_preg_find_closing_delimiter(pattern, closing).ok_or(EvalStatus::RuntimeFatal)?; + let body = eval_preg_unescape_delimiter(&pattern[1..close_index], delimiter, closing); + let modifiers = eval_preg_modifiers(&pattern[close_index + 1..])?; + Ok((body, modifiers)) +} + +/// Returns the closing regex delimiter for PHP's paired delimiter forms. +pub(in crate::interpreter) fn eval_preg_closing_delimiter(delimiter: u8) -> u8 { + match delimiter { + b'(' => b')', + b'[' => b']', + b'{' => b'}', + b'<' => b'>', + _ => delimiter, + } +} + +/// Finds the first unescaped closing regex delimiter. +pub(in crate::interpreter) fn eval_preg_find_closing_delimiter( + pattern: &[u8], + closing: u8, +) -> Option { + let mut escaped = false; + for (index, byte) in pattern.iter().copied().enumerate().skip(1) { + if escaped { + escaped = false; + continue; + } + if byte == b'\\' { + escaped = true; + continue; + } + if byte == closing { + return Some(index); + } + } + None +} + +/// Removes escapes that only protect the PHP regex delimiter from pattern stripping. +pub(in crate::interpreter) fn eval_preg_unescape_delimiter( + body: &[u8], + delimiter: u8, + closing: u8, +) -> Vec { + let mut result = Vec::with_capacity(body.len()); + let mut index = 0; + while index < body.len() { + if body[index] == b'\\' + && index + 1 < body.len() + && matches!(body[index + 1], byte if byte == delimiter || byte == closing) + { + result.push(body[index + 1]); + index += 2; + } else { + result.push(body[index]); + index += 1; + } + } + result +} + +/// Parses eval-supported PHP regex modifiers. +pub(in crate::interpreter) fn eval_preg_modifiers( + modifiers: &[u8], +) -> Result { + let mut parsed = EvalPregModifiers::default(); + for modifier in modifiers { + match *modifier { + b'i' => parsed.case_insensitive = true, + b'm' => parsed.multi_line = true, + b's' => parsed.dot_matches_new_line = true, + b'U' => parsed.swap_greed = true, + b'u' => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(parsed) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/replace.rs b/crates/elephc-eval/src/interpreter/builtins/regex/replace.rs new file mode 100644 index 0000000000..a5dbbe89db --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/replace.rs @@ -0,0 +1,98 @@ +//! Purpose: +//! Implements eval support for PHP `preg_replace()` and `preg_replace_callback()`. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` re-exports. +//! +//! Key details: +//! - Callback replacement evaluates through the persistent eval context and casts +//! callback results with runtime string coercion. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP `preg_replace()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, replacement, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let replacement = eval_expr(replacement, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_result(pattern, replacement, subject, values) +} + +/// Replaces every regex match with a PHP-style backreference-expanded replacement. +pub(in crate::interpreter) fn eval_preg_replace_result( + pattern: RuntimeCellHandle, + replacement: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let replacement = values.string_bytes(replacement)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + +/// Evaluates PHP `preg_replace_callback()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_replace_callback( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, callback, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_callback_result(pattern, callback, subject, context, values) +} + +/// Replaces every regex match by invoking an eval-supported callback with `$matches`. +pub(in crate::interpreter) fn eval_preg_replace_callback_result( + pattern: RuntimeCellHandle, + callback: RuntimeCellHandle, + subject: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let callback = eval_callable_name(callback, values)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; + let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; + let callback_result = values.cast_string(callback_result)?; + let callback_bytes = values.string_bytes(callback_result)?; + result.extend_from_slice(&callback_bytes); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/replacement.rs b/crates/elephc-eval/src/interpreter/builtins/regex/replacement.rs new file mode 100644 index 0000000000..c236a9d1a5 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/replacement.rs @@ -0,0 +1,101 @@ +//! Purpose: +//! Expands PHP preg replacement backreferences against one regex capture set. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::replace`. +//! +//! Key details: +//! - Supports `$n`, `${n}`, and `\n` capture references without allocating +//! intermediate runtime cells. + +use super::super::super::*; +use super::*; + +/// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. +pub(in crate::interpreter) fn eval_preg_expand_replacement( + replacement: &[u8], + subject: &[u8], + captures: &Captures<'_>, + result: &mut Vec, +) { + let mut index = 0; + while index < replacement.len() { + match replacement[index] { + b'$' => { + if let Some((capture_index, next_index)) = + eval_preg_replacement_capture_index(replacement, index + 1) + { + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } else { + result.push(replacement[index]); + index += 1; + } + } + b'\\' if index + 1 < replacement.len() && replacement[index + 1].is_ascii_digit() => { + let (capture_index, next_index) = + eval_preg_decimal_capture_index(replacement, index + 1); + if let Some(bytes) = eval_preg_capture_bytes(subject, captures, capture_index) { + result.extend_from_slice(bytes); + } + index = next_index; + } + byte => { + result.push(byte); + index += 1; + } + } + } +} + +/// Parses a dollar-style replacement capture reference. +pub(in crate::interpreter) fn eval_preg_replacement_capture_index( + bytes: &[u8], + index: usize, +) -> Option<(usize, usize)> { + if bytes.get(index).copied() == Some(b'{') { + let mut cursor = index + 1; + let start = cursor; + while cursor < bytes.len() && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + if cursor == start || bytes.get(cursor).copied() != Some(b'}') { + return None; + } + let capture = eval_preg_decimal_bytes_to_usize(&bytes[start..cursor])?; + return Some((capture, cursor + 1)); + } + if bytes.get(index).is_some_and(u8::is_ascii_digit) { + let (capture, next) = eval_preg_decimal_capture_index(bytes, index); + return Some((capture, next)); + } + None +} + +/// Parses a one- or two-digit replacement capture reference. +pub(in crate::interpreter) fn eval_preg_decimal_capture_index( + bytes: &[u8], + index: usize, +) -> (usize, usize) { + let mut cursor = index; + let end = usize::min(bytes.len(), index + 2); + while cursor < end && bytes[cursor].is_ascii_digit() { + cursor += 1; + } + ( + eval_preg_decimal_bytes_to_usize(&bytes[index..cursor]).unwrap_or(0), + cursor, + ) +} + +/// Converts ASCII decimal bytes into a `usize` capture index. +pub(in crate::interpreter) fn eval_preg_decimal_bytes_to_usize(bytes: &[u8]) -> Option { + let mut value = 0usize; + for byte in bytes { + value = value.checked_mul(10)?; + value = value.checked_add(usize::from(byte - b'0'))?; + } + Some(value) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/split.rs b/crates/elephc-eval/src/interpreter/builtins/regex/split.rs new file mode 100644 index 0000000000..5e4d43b210 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/split.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Implements eval support for PHP `preg_split()` entrypoint and result assembly. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` re-exports. +//! +//! Key details: +//! - Split flags are decoded before collecting pieces so delimiter capture, empty +//! filtering, and offset capture share one result path. + +use super::super::super::*; +use super::*; + +/// Evaluates PHP `preg_split()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_split_result(pattern, subject, None, None, values) + } + [pattern, subject, limit] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), None, values) + } + [pattern, subject, limit, flags] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let limit = eval_expr(limit, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_preg_split_result(pattern, subject, Some(limit), Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits a subject string with eval-supported `preg_split()` flags. +pub(in crate::interpreter) fn eval_preg_split_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + limit: Option, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let subject = values.string_bytes(subject)?; + let limit = eval_preg_split_limit(limit, values)?; + let flags = eval_preg_split_flags(flags, values)?; + let no_empty = flags & EVAL_PREG_SPLIT_NO_EMPTY != 0; + let capture_delimiters = flags & EVAL_PREG_SPLIT_DELIM_CAPTURE != 0; + let offset_capture = flags & EVAL_PREG_SPLIT_OFFSET_CAPTURE != 0; + let mut pieces = Vec::::new(); + let mut cursor = 0; + + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + if eval_preg_split_reached_limit(&pieces, limit) { + break; + } + eval_preg_split_push_piece( + &mut pieces, + &subject[cursor..matched.start()], + cursor, + no_empty, + ); + if capture_delimiters { + eval_preg_split_push_captures(&mut pieces, &subject, &captures, no_empty); + } + cursor = matched.end(); + } + eval_preg_split_push_piece(&mut pieces, &subject[cursor..], cursor, no_empty); + + let mut result = values.array_new(pieces.len())?; + for (index, piece) in pieces.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = eval_preg_split_piece_value(piece, offset_capture, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/split_helpers.rs b/crates/elephc-eval/src/interpreter/builtins/regex/split_helpers.rs new file mode 100644 index 0000000000..80d0fa2556 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/regex/split_helpers.rs @@ -0,0 +1,115 @@ +//! Purpose: +//! Owns `preg_split()` flag parsing and split-piece conversion helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::split`. +//! +//! Key details: +//! - Split pieces retain byte offsets so `PREG_SPLIT_OFFSET_CAPTURE` can be applied +//! after piece collection. + +use super::super::super::*; +use super::super::*; + +/// One `preg_split()` output segment plus its byte offset in the subject. +pub(in crate::interpreter) struct EvalPregSplitPiece { + bytes: Vec, + offset: usize, +} + +/// Returns the PHP `preg_split()` limit, treating zero as unlimited. +pub(in crate::interpreter) fn eval_preg_split_limit( + limit: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(limit) = limit else { + return Ok(None); + }; + let limit = eval_int_value(limit, values)?; + if limit <= 0 { + return Ok(None); + } + usize::try_from(limit) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns supported `preg_split()` flags. +pub(in crate::interpreter) fn eval_preg_split_flags( + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = flags else { + return Ok(0); + }; + let flags = eval_int_value(flags, values)?; + let supported = + EVAL_PREG_SPLIT_NO_EMPTY | EVAL_PREG_SPLIT_DELIM_CAPTURE | EVAL_PREG_SPLIT_OFFSET_CAPTURE; + if flags & !supported != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(flags) +} + +/// Returns whether `preg_split()` should stop splitting and emit the remaining subject. +pub(in crate::interpreter) fn eval_preg_split_reached_limit( + pieces: &[EvalPregSplitPiece], + limit: Option, +) -> bool { + matches!(limit, Some(limit) if limit > 0 && pieces.len() + 1 >= limit) +} + +/// Pushes one `preg_split()` output piece, honoring `PREG_SPLIT_NO_EMPTY`. +pub(in crate::interpreter) fn eval_preg_split_push_piece( + pieces: &mut Vec, + piece: &[u8], + offset: usize, + no_empty: bool, +) { + if no_empty && piece.is_empty() { + return; + } + pieces.push(EvalPregSplitPiece { + bytes: piece.to_vec(), + offset, + }); +} + +/// Pushes captured delimiters for `PREG_SPLIT_DELIM_CAPTURE`. +pub(in crate::interpreter) fn eval_preg_split_push_captures( + pieces: &mut Vec, + subject: &[u8], + captures: &Captures<'_>, + no_empty: bool, +) { + for index in 1..captures.len() { + if let Some(matched) = captures.get(index) { + eval_preg_split_push_piece( + pieces, + &subject[matched.start()..matched.end()], + matched.start(), + no_empty, + ); + } + } +} + +/// Converts one split segment to a string or PHP `[string, byte_offset]` pair. +pub(in crate::interpreter) fn eval_preg_split_piece_value( + piece: &EvalPregSplitPiece, + offset_capture: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.string_bytes_value(&piece.bytes)?; + if !offset_capture { + return Ok(value); + } + + let offset = i64::try_from(piece.offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = values.int(offset)?; + let mut pair = values.array_new(2)?; + let value_key = values.int(0)?; + pair = values.array_set(pair, value_key, value)?; + let offset_key = values.int(1)?; + values.array_set(pair, offset_key, offset) +} From 9dae3ca6975fdf9dbceafd342c039c21545bedb3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:37:34 +0200 Subject: [PATCH 0289/1208] Split eval formatting builtins --- .../src/interpreter/builtins/formatting.rs | 814 ------------------ .../interpreter/builtins/formatting/common.rs | 24 + .../builtins/formatting/dispatch.rs | 69 ++ .../interpreter/builtins/formatting/math.rs | 86 ++ .../interpreter/builtins/formatting/mod.rs | 26 + .../builtins/formatting/number_format.rs | 154 ++++ .../interpreter/builtins/formatting/printf.rs | 127 +++ .../builtins/formatting/sprintf_format.rs | 260 ++++++ .../interpreter/builtins/formatting/sscanf.rs | 167 ++++ 9 files changed, 913 insertions(+), 814 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting/common.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting/dispatch.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting/math.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting/number_format.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting/printf.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting/sprintf_format.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/formatting/sscanf.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting.rs b/crates/elephc-eval/src/interpreter/builtins/formatting.rs deleted file mode 100644 index e47c18fa5a..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/formatting.rs +++ /dev/null @@ -1,814 +0,0 @@ -//! Purpose: -//! Numeric formatting, sprintf-family, and math wrapper builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins` re-exports used by core call dispatch. -//! -//! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime -//! behavior through `RuntimeValueOps`. - -use super::super::*; -use super::*; - -/// Evaluates PHP's `ceil(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_ceil( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.ceil(value) -} - -/// Evaluates PHP's `floor(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_floor( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.floor(value) -} - -/// Evaluates PHP's zero-argument `pi()` builtin. -pub(in crate::interpreter) fn eval_builtin_pi( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI) -} - -/// Evaluates PHP's `pow(...)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_pow( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - values.pow(left, right) -} - -/// Evaluates PHP's `round(...)` over one value and an optional precision expression. -pub(in crate::interpreter) fn eval_builtin_round( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - values.round(value, None) - } - [value, precision] => { - let value = eval_expr(value, context, scope, values)?; - let precision = eval_expr(precision, context, scope, values)?; - values.round(value, Some(precision)) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `number_format(...)` over one number and optional separators. -pub(in crate::interpreter) fn eval_builtin_number_format( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_number_format_result(value, None, None, None, values) - } - [value, decimals] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - eval_number_format_result(value, Some(decimals), None, None, values) - } - [value, decimals, decimal_separator] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; - eval_number_format_result(value, Some(decimals), Some(decimal_separator), None, values) - } - [value, decimals, decimal_separator, thousands_separator] => { - let value = eval_expr(value, context, scope, values)?; - let decimals = eval_expr(decimals, context, scope, values)?; - let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; - let thousands_separator = eval_expr(thousands_separator, context, scope, values)?; - eval_number_format_result( - value, - Some(decimals), - Some(decimal_separator), - Some(thousands_separator), - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Formats one PHP numeric value with grouped thousands and fixed decimals. -pub(in crate::interpreter) fn eval_number_format_result( - value: RuntimeCellHandle, - decimals: Option, - decimal_separator: Option, - thousands_separator: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_float_value(value, values)?; - let decimals = match decimals { - Some(decimals) => eval_int_value(decimals, values)?, - None => 0, - }; - let decimal_separator = match decimal_separator { - Some(separator) => values.string_bytes(separator)?, - None => b".".to_vec(), - }; - let thousands_separator = match thousands_separator { - Some(separator) => values.string_bytes(separator)?, - None => b",".to_vec(), - }; - let output = - eval_number_format_bytes(value, decimals, &decimal_separator, &thousands_separator)?; - values.string_bytes_value(&output) -} - -/// Evaluates direct positional `sprintf()` or `printf()` calls in source order. -pub(in crate::interpreter) fn eval_builtin_sprintf_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_sprintf_like_result(name, &evaluated_args, values) -} - -/// Evaluates direct positional `vsprintf()` or `vprintf()` calls in source order. -pub(in crate::interpreter) fn eval_builtin_vsprintf_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_vsprintf_like_result(name, &evaluated_args, values) -} - -/// Evaluates direct positional `sscanf()` calls in source order. -pub(in crate::interpreter) fn eval_builtin_sscanf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let input = eval_expr(&args[0], context, scope, values)?; - let format = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - eval_expr(arg, context, scope, values)?; - } - eval_sscanf_result(input, format, values) -} - -/// Dispatches already evaluated `sprintf()` or `printf()` arguments. -pub(in crate::interpreter) fn eval_sprintf_like_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "sprintf" => eval_sprintf_result(evaluated_args, values), - "printf" => eval_printf_result(evaluated_args, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Dispatches already evaluated `vsprintf()` or `vprintf()` arguments. -pub(in crate::interpreter) fn eval_vsprintf_like_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "vsprintf" => eval_vsprintf_result(evaluated_args, values), - "vprintf" => eval_vprintf_result(evaluated_args, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Parses one string through the eval `sscanf()` subset and returns an indexed array. -pub(in crate::interpreter) fn eval_sscanf_result( - input: RuntimeCellHandle, - format: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let input = values.string_bytes(input)?; - let format = values.string_bytes(format)?; - let matches = eval_sscanf_matches(&input, &format); - let mut result = values.array_new(matches.len())?; - for (index, matched) in matches.iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.string_bytes_value(matched)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Extracts `%d`, `%f`, `%s`, and `%%` matches with the same subset as native `sscanf()`. -pub(in crate::interpreter) fn eval_sscanf_matches(input: &[u8], format: &[u8]) -> Vec> { - let mut matches = Vec::new(); - let mut input_index = 0; - let mut format_index = 0; - - while format_index < format.len() { - if format[format_index] != b'%' { - if input_index >= input.len() || input[input_index] != format[format_index] { - break; - } - input_index += 1; - format_index += 1; - continue; - } - - format_index += 1; - if format_index >= format.len() { - break; - } - - match format[format_index] { - b'%' => { - if input_index >= input.len() || input[input_index] != b'%' { - break; - } - input_index += 1; - } - b'd' => matches.push(eval_sscanf_scan_int(input, &mut input_index)), - b'f' => matches.push(eval_sscanf_scan_float(input, &mut input_index)), - b's' => matches.push(eval_sscanf_scan_word(input, &mut input_index)), - _ => {} - } - format_index += 1; - } - - matches -} - -/// Scans the native `sscanf()` `%d` subset as a matched byte slice. -pub(in crate::interpreter) fn eval_sscanf_scan_int( - input: &[u8], - input_index: &mut usize, -) -> Vec { - let start = *input_index; - if input.get(*input_index) == Some(&b'-') { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - input[start..*input_index].to_vec() -} - -/// Scans the native `sscanf()` `%f` subset as a matched byte slice. -pub(in crate::interpreter) fn eval_sscanf_scan_float( - input: &[u8], - input_index: &mut usize, -) -> Vec { - let start = *input_index; - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'+' | b'-')) - { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - if input.get(*input_index) == Some(&b'.') { - *input_index += 1; - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - } - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'e' | b'E')) - { - *input_index += 1; - if input - .get(*input_index) - .is_some_and(|byte| matches!(byte, b'+' | b'-')) - { - *input_index += 1; - } - while input - .get(*input_index) - .is_some_and(|byte| byte.is_ascii_digit()) - { - *input_index += 1; - } - } - input[start..*input_index].to_vec() -} - -/// Scans the native `sscanf()` `%s` subset as a non-space byte word. -pub(in crate::interpreter) fn eval_sscanf_scan_word( - input: &[u8], - input_index: &mut usize, -) -> Vec { - let start = *input_index; - while input - .get(*input_index) - .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\n')) - { - *input_index += 1; - } - input[start..*input_index].to_vec() -} - -/// Formats `sprintf()` arguments and returns the resulting PHP string. -pub(in crate::interpreter) fn eval_sprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((format, format_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - values.string_bytes_value(&output) -} - -/// Formats `printf()` arguments, echoes the result, and returns its byte count. -pub(in crate::interpreter) fn eval_printf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((format, format_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.int(len) -} - -/// Formats `vsprintf()` array arguments and returns the resulting PHP string. -pub(in crate::interpreter) fn eval_vsprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let format_args = eval_sprintf_argument_array_values(*array, values)?; - let output = eval_sprintf_bytes(&format, &format_args, values)?; - values.string_bytes_value(&output) -} - -/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. -pub(in crate::interpreter) fn eval_vprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let format_args = eval_sprintf_argument_array_values(*array, values)?; - let output = eval_sprintf_bytes(&format, &format_args, values)?; - let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.int(len) -} - -/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. -pub(in crate::interpreter) fn eval_sprintf_argument_array_values( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !values.is_array_like(array)? { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(array)?; - let mut args = Vec::with_capacity(len); - for position in 0..len { - let key = values.array_iter_key(array, position)?; - args.push(values.array_get(array, key)?); - } - Ok(args) -} - -/// Formats one printf-style byte string through eval runtime value coercions. -pub(in crate::interpreter) fn eval_sprintf_bytes( - format: &[u8], - args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut output = Vec::new(); - let mut index = 0; - let mut arg_index = 0; - while index < format.len() { - if format[index] != b'%' { - output.push(format[index]); - index += 1; - continue; - } - index += 1; - if index >= format.len() { - break; - } - if format[index] == b'%' { - output.push(b'%'); - index += 1; - continue; - } - - let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; - index = next_index; - let Some(arg) = args.get(arg_index).copied() else { - return Err(EvalStatus::RuntimeFatal); - }; - arg_index += 1; - let bytes = eval_format_sprintf_arg(spec, arg, values)?; - output.extend_from_slice(&bytes); - } - Ok(output) -} - -/// Parses flags, width, precision, and terminal type for one format specifier. -pub(in crate::interpreter) fn eval_parse_sprintf_spec( - format: &[u8], - mut index: usize, -) -> Result<(EvalSprintfSpec, usize), EvalStatus> { - let mut spec = EvalSprintfSpec { - left_align: false, - force_sign: false, - space_sign: false, - zero_pad: false, - alternate: false, - width: None, - precision: None, - specifier: 0, - }; - while index < format.len() { - match format[index] { - b'-' => spec.left_align = true, - b'+' => spec.force_sign = true, - b' ' => spec.space_sign = true, - b'0' => spec.zero_pad = true, - b'#' => spec.alternate = true, - _ => break, - } - index += 1; - } - let (width, next_index) = eval_parse_sprintf_number(format, index)?; - spec.width = width; - index = next_index; - if index < format.len() && format[index] == b'.' { - let (precision, next_index) = eval_parse_sprintf_number(format, index + 1)?; - spec.precision = Some(precision.unwrap_or(0)); - index = next_index; - } - if index >= format.len() { - return Err(EvalStatus::RuntimeFatal); - } - spec.specifier = format[index]; - Ok((spec, index + 1)) -} - -/// Parses an unsigned decimal number from a format specifier component. -pub(in crate::interpreter) fn eval_parse_sprintf_number( - format: &[u8], - mut index: usize, -) -> Result<(Option, usize), EvalStatus> { - let start = index; - let mut value = 0usize; - while index < format.len() && format[index].is_ascii_digit() { - value = value - .checked_mul(10) - .and_then(|value| value.checked_add(usize::from(format[index] - b'0'))) - .ok_or(EvalStatus::RuntimeFatal)?; - index += 1; - } - if index == start { - Ok((None, index)) - } else { - Ok((Some(value), index)) - } -} - -/// Formats one runtime value according to a parsed eval sprintf specifier. -pub(in crate::interpreter) fn eval_format_sprintf_arg( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - match spec.specifier { - b's' => eval_format_sprintf_string(spec, value, values), - b'f' | b'e' | b'g' => eval_format_sprintf_float(spec, value, values), - b'c' => eval_format_sprintf_char(spec, value, values), - _ => eval_format_sprintf_int(spec, value, values), - } -} - -/// Formats a `%s` operand after PHP string coercion. -pub(in crate::interpreter) fn eval_format_sprintf_string( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut bytes = values.string_bytes(value)?; - if let Some(precision) = spec.precision { - bytes.truncate(precision); - } - Ok(eval_sprintf_apply_width(bytes, spec, false)) -} - -/// Formats an integer-like operand for decimal, unsigned, hex, and octal specifiers. -pub(in crate::interpreter) fn eval_format_sprintf_int( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_int_value(value, values)?; - let mut output = Vec::new(); - match spec.specifier { - b'u' => { - let digits = eval_sprintf_precision_pad((value as u64).to_string().into_bytes(), spec); - output.extend_from_slice(&digits); - } - b'x' | b'X' => { - let unsigned = value as u64; - if spec.alternate && unsigned != 0 { - output.extend_from_slice(if spec.specifier == b'X' { b"0X" } else { b"0x" }); - } - let digits = if spec.specifier == b'X' { - format!("{unsigned:X}").into_bytes() - } else { - format!("{unsigned:x}").into_bytes() - }; - output.extend_from_slice(&eval_sprintf_precision_pad(digits, spec)); - } - b'o' => { - let unsigned = value as u64; - let mut digits = eval_sprintf_precision_pad(format!("{unsigned:o}").into_bytes(), spec); - if spec.alternate && !digits.starts_with(b"0") { - output.push(b'0'); - } - output.append(&mut digits); - } - _ => { - let value = value as i128; - let magnitude = if value < 0 { - (-value) as u128 - } else { - value as u128 - }; - if value < 0 { - output.push(b'-'); - } else if spec.force_sign { - output.push(b'+'); - } else if spec.space_sign { - output.push(b' '); - } - let digits = eval_sprintf_precision_pad(magnitude.to_string().into_bytes(), spec); - output.extend_from_slice(&digits); - } - } - Ok(eval_sprintf_apply_width(output, spec, true)) -} - -/// Formats a `%c` operand as the low byte of its integer value. -pub(in crate::interpreter) fn eval_format_sprintf_char( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_int_value(value, values)?; - Ok(eval_sprintf_apply_width(vec![value as u8], spec, false)) -} - -/// Formats a floating-point operand for the eval printf-family subset. -pub(in crate::interpreter) fn eval_format_sprintf_float( - spec: EvalSprintfSpec, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let value = eval_float_value(value, values)?; - let precision = spec.precision.unwrap_or(6); - let mut output = if value.is_nan() { - b"NAN".to_vec() - } else if value.is_infinite() { - b"INF".to_vec() - } else { - match spec.specifier { - b'e' => format!("{value:.precision$e}").into_bytes(), - b'g' => format!("{value:.precision$}").into_bytes(), - _ => format!("{value:.precision$}").into_bytes(), - } - }; - if value.is_sign_negative() && !output.starts_with(b"-") { - output.insert(0, b'-'); - } else if value.is_sign_positive() && value.is_finite() { - if spec.force_sign { - output.insert(0, b'+'); - } else if spec.space_sign { - output.insert(0, b' '); - } - } - Ok(eval_sprintf_apply_width(output, spec, true)) -} - -/// Applies integer precision by left-padding digits with zeros. -pub(in crate::interpreter) fn eval_sprintf_precision_pad( - mut digits: Vec, - spec: EvalSprintfSpec, -) -> Vec { - if matches!(spec.precision, Some(0)) && digits == b"0" { - digits.clear(); - return digits; - } - let Some(precision) = spec.precision else { - return digits; - }; - if digits.len() >= precision { - return digits; - } - let mut output = vec![b'0'; precision - digits.len()]; - output.append(&mut digits); - output -} - -/// Applies field width and alignment to one formatted eval sprintf replacement. -pub(in crate::interpreter) fn eval_sprintf_apply_width( - mut bytes: Vec, - spec: EvalSprintfSpec, - numeric: bool, -) -> Vec { - let Some(width) = spec.width else { - return bytes; - }; - if bytes.len() >= width { - return bytes; - } - let pad_len = width - bytes.len(); - if spec.left_align { - bytes.extend(std::iter::repeat_n(b' ', pad_len)); - return bytes; - } - if numeric && spec.zero_pad && spec.precision.is_none() { - let prefix_len = eval_sprintf_zero_pad_prefix_len(&bytes); - let mut output = Vec::with_capacity(width); - output.extend_from_slice(&bytes[..prefix_len]); - output.extend(std::iter::repeat_n(b'0', pad_len)); - output.extend_from_slice(&bytes[prefix_len..]); - return output; - } - let mut output = Vec::with_capacity(width); - output.extend(std::iter::repeat_n(b' ', pad_len)); - output.append(&mut bytes); - output -} - -/// Returns the sign and alternate-prefix length that should precede zero padding. -pub(in crate::interpreter) fn eval_sprintf_zero_pad_prefix_len(bytes: &[u8]) -> usize { - let mut prefix_len = usize::from(matches!(bytes.first(), Some(b'+' | b'-' | b' '))); - if bytes.len() >= prefix_len + 2 - && bytes[prefix_len] == b'0' - && matches!(bytes[prefix_len + 1], b'x' | b'X') - { - prefix_len += 2; - } - prefix_len -} - -/// Converts one eval value to PHP float and returns the scalar payload. -pub(in crate::interpreter) fn eval_float_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.cast_float(value)?; - let bytes = values.string_bytes(value)?; - std::str::from_utf8(&bytes) - .map_err(|_| EvalStatus::RuntimeFatal)? - .parse::() - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Produces PHP `number_format()` bytes for finite scalar values. -pub(in crate::interpreter) fn eval_number_format_bytes( - value: f64, - decimals: i64, - decimal_separator: &[u8], - thousands_separator: &[u8], -) -> Result, EvalStatus> { - if !value.is_finite() { - return Ok(value.to_string().into_bytes()); - } - let decimals = decimals.clamp(-308, 308); - let display_decimals = decimals.max(0) as usize; - let abs_value = value.abs(); - let scaled = if decimals >= 0 { - let scale = 10_f64.powi(decimals as i32); - (abs_value * scale).round() - } else { - let scale = 10_f64.powi((-decimals) as i32); - (abs_value / scale).round() * scale - }; - if scaled > (u128::MAX as f64) { - return Err(EvalStatus::RuntimeFatal); - } - let scaled = scaled as u128; - let scale = 10_u128 - .checked_pow(display_decimals as u32) - .ok_or(EvalStatus::RuntimeFatal)?; - let integer = if display_decimals == 0 { - scaled - } else { - scaled / scale - }; - let fraction = if display_decimals == 0 { - 0 - } else { - scaled % scale - }; - - let mut output = Vec::new(); - if value.is_sign_negative() && scaled != 0 { - output.push(b'-'); - } - eval_append_grouped_decimal(&mut output, integer, thousands_separator); - if display_decimals > 0 { - output.extend_from_slice(decimal_separator); - let fraction = format!("{fraction:0display_decimals$}"); - output.extend_from_slice(fraction.as_bytes()); - } - Ok(output) -} - -/// Appends one unsigned decimal integer with optional three-digit grouping. -pub(in crate::interpreter) fn eval_append_grouped_decimal( - output: &mut Vec, - value: u128, - separator: &[u8], -) { - let digits = value.to_string(); - if separator.is_empty() { - output.extend_from_slice(digits.as_bytes()); - return; - } - let first_group = match digits.len() % 3 { - 0 => 3, - len => len, - }; - output.extend_from_slice(&digits.as_bytes()[..first_group]); - let mut index = first_group; - while index < digits.len() { - output.extend_from_slice(separator); - output.extend_from_slice(&digits.as_bytes()[index..index + 3]); - index += 3; - } -} diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/common.rs b/crates/elephc-eval/src/interpreter/builtins/formatting/common.rs new file mode 100644 index 0000000000..b3f346c2ab --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting/common.rs @@ -0,0 +1,24 @@ +//! Purpose: +//! Shared scalar formatting coercions used by formatting-related eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting` number-format and printf helpers. +//! +//! Key details: +//! - Float conversion delegates to runtime coercion first, then parses the runtime +//! string payload into the scalar value used by formatting logic. + +use super::super::super::*; + +/// Converts one eval value to PHP float and returns the scalar payload. +pub(in crate::interpreter) fn eval_float_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.cast_float(value)?; + let bytes = values.string_bytes(value)?; + std::str::from_utf8(&bytes) + .map_err(|_| EvalStatus::RuntimeFatal)? + .parse::() + .map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/dispatch.rs b/crates/elephc-eval/src/interpreter/builtins/formatting/dispatch.rs new file mode 100644 index 0000000000..48f09afc0d --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting/dispatch.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Evaluates direct printf-family eval arguments before dispatching to formatted +//! result helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` builtin dispatch paths. +//! +//! Key details: +//! - Argument expressions are evaluated once in source order before builtin-family +//! selection is applied. + +use super::super::super::*; +use super::*; + +/// Evaluates direct positional `sprintf()` or `printf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_sprintf_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_sprintf_like_result(name, &evaluated_args, values) +} + +/// Evaluates direct positional `vsprintf()` or `vprintf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_vsprintf_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_vsprintf_like_result(name, &evaluated_args, values) +} + +/// Dispatches already evaluated `sprintf()` or `printf()` arguments. +pub(in crate::interpreter) fn eval_sprintf_like_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "sprintf" => eval_sprintf_result(evaluated_args, values), + "printf" => eval_printf_result(evaluated_args, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Dispatches already evaluated `vsprintf()` or `vprintf()` arguments. +pub(in crate::interpreter) fn eval_vsprintf_like_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "vsprintf" => eval_vsprintf_result(evaluated_args, values), + "vprintf" => eval_vprintf_result(evaluated_args, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/math.rs b/crates/elephc-eval/src/interpreter/builtins/formatting/math.rs new file mode 100644 index 0000000000..fb496881dd --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting/math.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Implements small numeric math wrapper builtins for eval execution. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting` re-exports. +//! +//! Key details: +//! - Argument evaluation stays in PHP source order before delegating scalar math +//! behavior to `RuntimeValueOps`. + +use super::super::super::*; + +/// Evaluates PHP's `ceil(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ceil( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.ceil(value) +} + +/// Evaluates PHP's `floor(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_floor( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.floor(value) +} + +/// Evaluates PHP's zero-argument `pi()` builtin. +pub(in crate::interpreter) fn eval_builtin_pi( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI) +} + +/// Evaluates PHP's `pow(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_pow( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + values.pow(left, right) +} + +/// Evaluates PHP's `round(...)` over one value and an optional precision expression. +pub(in crate::interpreter) fn eval_builtin_round( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + values.round(value, None) + } + [value, precision] => { + let value = eval_expr(value, context, scope, values)?; + let precision = eval_expr(precision, context, scope, values)?; + values.round(value, Some(precision)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/mod.rs b/crates/elephc-eval/src/interpreter/builtins/formatting/mod.rs new file mode 100644 index 0000000000..7efc488eb6 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting/mod.rs @@ -0,0 +1,26 @@ +//! Purpose: +//! Groups numeric formatting, printf-family, scanf, and math wrapper eval builtins. +//! Submodules are split by builtin family and shared formatting helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime +//! behavior through `RuntimeValueOps`. + +mod common; +mod dispatch; +mod math; +mod number_format; +mod printf; +mod sprintf_format; +mod sscanf; + +pub(in crate::interpreter) use common::*; +pub(in crate::interpreter) use dispatch::*; +pub(in crate::interpreter) use math::*; +pub(in crate::interpreter) use number_format::*; +pub(in crate::interpreter) use printf::*; +pub(in crate::interpreter) use sprintf_format::*; +pub(in crate::interpreter) use sscanf::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/number_format.rs b/crates/elephc-eval/src/interpreter/builtins/formatting/number_format.rs new file mode 100644 index 0000000000..d28e7968fb --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting/number_format.rs @@ -0,0 +1,154 @@ +//! Purpose: +//! Implements PHP `number_format()` evaluation and decimal grouping helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting` re-exports. +//! +//! Key details: +//! - Float coercion is shared with printf formatting, while separator coercion +//! remains local to `number_format()`. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP `number_format(...)` over one number and optional separators. +pub(in crate::interpreter) fn eval_builtin_number_format( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_number_format_result(value, None, None, None, values) + } + [value, decimals] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + eval_number_format_result(value, Some(decimals), None, None, values) + } + [value, decimals, decimal_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + eval_number_format_result(value, Some(decimals), Some(decimal_separator), None, values) + } + [value, decimals, decimal_separator, thousands_separator] => { + let value = eval_expr(value, context, scope, values)?; + let decimals = eval_expr(decimals, context, scope, values)?; + let decimal_separator = eval_expr(decimal_separator, context, scope, values)?; + let thousands_separator = eval_expr(thousands_separator, context, scope, values)?; + eval_number_format_result( + value, + Some(decimals), + Some(decimal_separator), + Some(thousands_separator), + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one PHP numeric value with grouped thousands and fixed decimals. +pub(in crate::interpreter) fn eval_number_format_result( + value: RuntimeCellHandle, + decimals: Option, + decimal_separator: Option, + thousands_separator: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_float_value(value, values)?; + let decimals = match decimals { + Some(decimals) => eval_int_value(decimals, values)?, + None => 0, + }; + let decimal_separator = match decimal_separator { + Some(separator) => values.string_bytes(separator)?, + None => b".".to_vec(), + }; + let thousands_separator = match thousands_separator { + Some(separator) => values.string_bytes(separator)?, + None => b",".to_vec(), + }; + let output = + eval_number_format_bytes(value, decimals, &decimal_separator, &thousands_separator)?; + values.string_bytes_value(&output) +} + +/// Produces PHP `number_format()` bytes for finite scalar values. +pub(in crate::interpreter) fn eval_number_format_bytes( + value: f64, + decimals: i64, + decimal_separator: &[u8], + thousands_separator: &[u8], +) -> Result, EvalStatus> { + if !value.is_finite() { + return Ok(value.to_string().into_bytes()); + } + let decimals = decimals.clamp(-308, 308); + let display_decimals = decimals.max(0) as usize; + let abs_value = value.abs(); + let scaled = if decimals >= 0 { + let scale = 10_f64.powi(decimals as i32); + (abs_value * scale).round() + } else { + let scale = 10_f64.powi((-decimals) as i32); + (abs_value / scale).round() * scale + }; + if scaled > (u128::MAX as f64) { + return Err(EvalStatus::RuntimeFatal); + } + let scaled = scaled as u128; + let scale = 10_u128 + .checked_pow(display_decimals as u32) + .ok_or(EvalStatus::RuntimeFatal)?; + let integer = if display_decimals == 0 { + scaled + } else { + scaled / scale + }; + let fraction = if display_decimals == 0 { + 0 + } else { + scaled % scale + }; + + let mut output = Vec::new(); + if value.is_sign_negative() && scaled != 0 { + output.push(b'-'); + } + eval_append_grouped_decimal(&mut output, integer, thousands_separator); + if display_decimals > 0 { + output.extend_from_slice(decimal_separator); + let fraction = format!("{fraction:0display_decimals$}"); + output.extend_from_slice(fraction.as_bytes()); + } + Ok(output) +} + +/// Appends one unsigned decimal integer with optional three-digit grouping. +pub(in crate::interpreter) fn eval_append_grouped_decimal( + output: &mut Vec, + value: u128, + separator: &[u8], +) { + let digits = value.to_string(); + if separator.is_empty() { + output.extend_from_slice(digits.as_bytes()); + return; + } + let first_group = match digits.len() % 3 { + 0 => 3, + len => len, + }; + output.extend_from_slice(&digits.as_bytes()[..first_group]); + let mut index = first_group; + while index < digits.len() { + output.extend_from_slice(separator); + output.extend_from_slice(&digits.as_bytes()[index..index + 3]); + index += 3; + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/printf.rs b/crates/elephc-eval/src/interpreter/builtins/formatting/printf.rs new file mode 100644 index 0000000000..1d097d94c8 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting/printf.rs @@ -0,0 +1,127 @@ +//! Purpose: +//! Implements result construction for PHP `sprintf`, `printf`, `vsprintf`, and +//! `vprintf` eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting::dispatch`. +//! +//! Key details: +//! - The formatted byte stream is shared by string-returning and echoing variants; +//! echoing variants return the emitted byte count. + +use super::super::super::*; +use super::*; + +/// Formats `sprintf()` arguments and returns the resulting PHP string. +pub(in crate::interpreter) fn eval_sprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats `printf()` arguments, echoes the result, and returns its byte count. +pub(in crate::interpreter) fn eval_printf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} + +/// Formats `vsprintf()` array arguments and returns the resulting PHP string. +pub(in crate::interpreter) fn eval_vsprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = eval_sprintf_bytes(&format, &format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. +pub(in crate::interpreter) fn eval_vprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = eval_sprintf_bytes(&format, &format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} + +/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. +pub(in crate::interpreter) fn eval_sprintf_argument_array_values( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + let mut args = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(array, position)?; + args.push(values.array_get(array, key)?); + } + Ok(args) +} + +/// Formats one printf-style byte string through eval runtime value coercions. +pub(in crate::interpreter) fn eval_sprintf_bytes( + format: &[u8], + args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut index = 0; + let mut arg_index = 0; + while index < format.len() { + if format[index] != b'%' { + output.push(format[index]); + index += 1; + continue; + } + index += 1; + if index >= format.len() { + break; + } + if format[index] == b'%' { + output.push(b'%'); + index += 1; + continue; + } + + let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; + index = next_index; + let Some(arg) = args.get(arg_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + arg_index += 1; + let bytes = eval_format_sprintf_arg(spec, arg, values)?; + output.extend_from_slice(&bytes); + } + Ok(output) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/sprintf_format.rs b/crates/elephc-eval/src/interpreter/builtins/formatting/sprintf_format.rs new file mode 100644 index 0000000000..e86546cc47 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting/sprintf_format.rs @@ -0,0 +1,260 @@ +//! Purpose: +//! Parses and applies printf-style format specifiers for eval printf-family calls. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting::printf`. +//! +//! Key details: +//! - Formatting uses PHP runtime coercions through `RuntimeValueOps` before width, +//! precision, sign, and alternate-form handling are applied. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Parses flags, width, precision, and terminal type for one format specifier. +pub(in crate::interpreter) fn eval_parse_sprintf_spec( + format: &[u8], + mut index: usize, +) -> Result<(EvalSprintfSpec, usize), EvalStatus> { + let mut spec = EvalSprintfSpec { + left_align: false, + force_sign: false, + space_sign: false, + zero_pad: false, + alternate: false, + width: None, + precision: None, + specifier: 0, + }; + while index < format.len() { + match format[index] { + b'-' => spec.left_align = true, + b'+' => spec.force_sign = true, + b' ' => spec.space_sign = true, + b'0' => spec.zero_pad = true, + b'#' => spec.alternate = true, + _ => break, + } + index += 1; + } + let (width, next_index) = eval_parse_sprintf_number(format, index)?; + spec.width = width; + index = next_index; + if index < format.len() && format[index] == b'.' { + let (precision, next_index) = eval_parse_sprintf_number(format, index + 1)?; + spec.precision = Some(precision.unwrap_or(0)); + index = next_index; + } + if index >= format.len() { + return Err(EvalStatus::RuntimeFatal); + } + spec.specifier = format[index]; + Ok((spec, index + 1)) +} + +/// Parses an unsigned decimal number from a format specifier component. +pub(in crate::interpreter) fn eval_parse_sprintf_number( + format: &[u8], + mut index: usize, +) -> Result<(Option, usize), EvalStatus> { + let start = index; + let mut value = 0usize; + while index < format.len() && format[index].is_ascii_digit() { + value = value + .checked_mul(10) + .and_then(|value| value.checked_add(usize::from(format[index] - b'0'))) + .ok_or(EvalStatus::RuntimeFatal)?; + index += 1; + } + if index == start { + Ok((None, index)) + } else { + Ok((Some(value), index)) + } +} + +/// Formats one runtime value according to a parsed eval sprintf specifier. +pub(in crate::interpreter) fn eval_format_sprintf_arg( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match spec.specifier { + b's' => eval_format_sprintf_string(spec, value, values), + b'f' | b'e' | b'g' => eval_format_sprintf_float(spec, value, values), + b'c' => eval_format_sprintf_char(spec, value, values), + _ => eval_format_sprintf_int(spec, value, values), + } +} + +/// Formats a `%s` operand after PHP string coercion. +pub(in crate::interpreter) fn eval_format_sprintf_string( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bytes = values.string_bytes(value)?; + if let Some(precision) = spec.precision { + bytes.truncate(precision); + } + Ok(eval_sprintf_apply_width(bytes, spec, false)) +} + +/// Formats an integer-like operand for decimal, unsigned, hex, and octal specifiers. +pub(in crate::interpreter) fn eval_format_sprintf_int( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + let mut output = Vec::new(); + match spec.specifier { + b'u' => { + let digits = eval_sprintf_precision_pad((value as u64).to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + b'x' | b'X' => { + let unsigned = value as u64; + if spec.alternate && unsigned != 0 { + output.extend_from_slice(if spec.specifier == b'X' { b"0X" } else { b"0x" }); + } + let digits = if spec.specifier == b'X' { + format!("{unsigned:X}").into_bytes() + } else { + format!("{unsigned:x}").into_bytes() + }; + output.extend_from_slice(&eval_sprintf_precision_pad(digits, spec)); + } + b'o' => { + let unsigned = value as u64; + let mut digits = eval_sprintf_precision_pad(format!("{unsigned:o}").into_bytes(), spec); + if spec.alternate && !digits.starts_with(b"0") { + output.push(b'0'); + } + output.append(&mut digits); + } + _ => { + let value = value as i128; + let magnitude = if value < 0 { + (-value) as u128 + } else { + value as u128 + }; + if value < 0 { + output.push(b'-'); + } else if spec.force_sign { + output.push(b'+'); + } else if spec.space_sign { + output.push(b' '); + } + let digits = eval_sprintf_precision_pad(magnitude.to_string().into_bytes(), spec); + output.extend_from_slice(&digits); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Formats a `%c` operand as the low byte of its integer value. +pub(in crate::interpreter) fn eval_format_sprintf_char( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_int_value(value, values)?; + Ok(eval_sprintf_apply_width(vec![value as u8], spec, false)) +} + +/// Formats a floating-point operand for the eval printf-family subset. +pub(in crate::interpreter) fn eval_format_sprintf_float( + spec: EvalSprintfSpec, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let value = eval_float_value(value, values)?; + let precision = spec.precision.unwrap_or(6); + let mut output = if value.is_nan() { + b"NAN".to_vec() + } else if value.is_infinite() { + b"INF".to_vec() + } else { + match spec.specifier { + b'e' => format!("{value:.precision$e}").into_bytes(), + b'g' => format!("{value:.precision$}").into_bytes(), + _ => format!("{value:.precision$}").into_bytes(), + } + }; + if value.is_sign_negative() && !output.starts_with(b"-") { + output.insert(0, b'-'); + } else if value.is_sign_positive() && value.is_finite() { + if spec.force_sign { + output.insert(0, b'+'); + } else if spec.space_sign { + output.insert(0, b' '); + } + } + Ok(eval_sprintf_apply_width(output, spec, true)) +} + +/// Applies integer precision by left-padding digits with zeros. +pub(in crate::interpreter) fn eval_sprintf_precision_pad( + mut digits: Vec, + spec: EvalSprintfSpec, +) -> Vec { + if matches!(spec.precision, Some(0)) && digits == b"0" { + digits.clear(); + return digits; + } + let Some(precision) = spec.precision else { + return digits; + }; + if digits.len() >= precision { + return digits; + } + let mut output = vec![b'0'; precision - digits.len()]; + output.append(&mut digits); + output +} + +/// Applies field width and alignment to one formatted eval sprintf replacement. +pub(in crate::interpreter) fn eval_sprintf_apply_width( + mut bytes: Vec, + spec: EvalSprintfSpec, + numeric: bool, +) -> Vec { + let Some(width) = spec.width else { + return bytes; + }; + if bytes.len() >= width { + return bytes; + } + let pad_len = width - bytes.len(); + if spec.left_align { + bytes.extend(std::iter::repeat_n(b' ', pad_len)); + return bytes; + } + if numeric && spec.zero_pad && spec.precision.is_none() { + let prefix_len = eval_sprintf_zero_pad_prefix_len(&bytes); + let mut output = Vec::with_capacity(width); + output.extend_from_slice(&bytes[..prefix_len]); + output.extend(std::iter::repeat_n(b'0', pad_len)); + output.extend_from_slice(&bytes[prefix_len..]); + return output; + } + let mut output = Vec::with_capacity(width); + output.extend(std::iter::repeat_n(b' ', pad_len)); + output.append(&mut bytes); + output +} + +/// Returns the sign and alternate-prefix length that should precede zero padding. +pub(in crate::interpreter) fn eval_sprintf_zero_pad_prefix_len(bytes: &[u8]) -> usize { + let mut prefix_len = usize::from(matches!(bytes.first(), Some(b'+' | b'-' | b' '))); + if bytes.len() >= prefix_len + 2 + && bytes[prefix_len] == b'0' + && matches!(bytes[prefix_len + 1], b'x' | b'X') + { + prefix_len += 2; + } + prefix_len +} diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/sscanf.rs b/crates/elephc-eval/src/interpreter/builtins/formatting/sscanf.rs new file mode 100644 index 0000000000..8e40c2bd94 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/formatting/sscanf.rs @@ -0,0 +1,167 @@ +//! Purpose: +//! Implements eval support for PHP `sscanf()` and its small scanning subset. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting` re-exports. +//! +//! Key details: +//! - Only the currently supported `%d`, `%f`, `%s`, and `%%` subset is parsed; +//! extra output variables are evaluated for side effects and ignored. + +use super::super::super::*; + +/// Evaluates direct positional `sscanf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_sscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let input = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_sscanf_result(input, format, values) +} + +/// Parses one string through the eval `sscanf()` subset and returns an indexed array. +pub(in crate::interpreter) fn eval_sscanf_result( + input: RuntimeCellHandle, + format: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(input)?; + let format = values.string_bytes(format)?; + let matches = eval_sscanf_matches(&input, &format); + let mut result = values.array_new(matches.len())?; + for (index, matched) in matches.iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(matched)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Extracts `%d`, `%f`, `%s`, and `%%` matches with the same subset as native `sscanf()`. +pub(in crate::interpreter) fn eval_sscanf_matches(input: &[u8], format: &[u8]) -> Vec> { + let mut matches = Vec::new(); + let mut input_index = 0; + let mut format_index = 0; + + while format_index < format.len() { + if format[format_index] != b'%' { + if input_index >= input.len() || input[input_index] != format[format_index] { + break; + } + input_index += 1; + format_index += 1; + continue; + } + + format_index += 1; + if format_index >= format.len() { + break; + } + + match format[format_index] { + b'%' => { + if input_index >= input.len() || input[input_index] != b'%' { + break; + } + input_index += 1; + } + b'd' => matches.push(eval_sscanf_scan_int(input, &mut input_index)), + b'f' => matches.push(eval_sscanf_scan_float(input, &mut input_index)), + b's' => matches.push(eval_sscanf_scan_word(input, &mut input_index)), + _ => {} + } + format_index += 1; + } + + matches +} + +/// Scans the native `sscanf()` `%d` subset as a matched byte slice. +pub(in crate::interpreter) fn eval_sscanf_scan_int( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + if input.get(*input_index) == Some(&b'-') { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%f` subset as a matched byte slice. +pub(in crate::interpreter) fn eval_sscanf_scan_float( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + if input.get(*input_index) == Some(&b'.') { + *input_index += 1; + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'e' | b'E')) + { + *input_index += 1; + if input + .get(*input_index) + .is_some_and(|byte| matches!(byte, b'+' | b'-')) + { + *input_index += 1; + } + while input + .get(*input_index) + .is_some_and(|byte| byte.is_ascii_digit()) + { + *input_index += 1; + } + } + input[start..*input_index].to_vec() +} + +/// Scans the native `sscanf()` `%s` subset as a non-space byte word. +pub(in crate::interpreter) fn eval_sscanf_scan_word( + input: &[u8], + input_index: &mut usize, +) -> Vec { + let start = *input_index; + while input + .get(*input_index) + .is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\n')) + { + *input_index += 1; + } + input[start..*input_index].to_vec() +} From 488f7215e66b0575cd50211ffec7c594f6fea373 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:41:34 +0200 Subject: [PATCH 0290/1208] Split eval builtin dispatch by domain --- .../interpreter/builtins/registry/dispatch.rs | 1096 ----------------- .../builtins/registry/dispatch/arrays.rs | 259 ++++ .../builtins/registry/dispatch/core.rs | 51 + .../builtins/registry/dispatch/filesystem.rs | 212 ++++ .../builtins/registry/dispatch/formatting.rs | 83 ++ .../builtins/registry/dispatch/json.rs | 75 ++ .../builtins/registry/dispatch/mod.rs | 84 ++ .../builtins/registry/dispatch/network_env.rs | 120 ++ .../builtins/registry/dispatch/regex.rs | 55 + .../builtins/registry/dispatch/scalars.rs | 111 ++ .../builtins/registry/dispatch/strings.rs | 244 ++++ .../builtins/registry/dispatch/symbols.rs | 72 ++ .../builtins/registry/dispatch/time.rs | 64 + 13 files changed, 1430 insertions(+), 1096 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/formatting.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/json.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/regex.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/registry/dispatch/time.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch.rs deleted file mode 100644 index 284795b934..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch.rs +++ /dev/null @@ -1,1096 +0,0 @@ -//! Purpose: -//! By-value dynamic builtin dispatch for evaluated argument lists. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry` re-exports. -//! -//! Key details: -//! - Helpers are scoped to the eval interpreter and operate on already parsed -//! EvalIR call metadata or evaluated runtime-cell handles. - -use super::super::super::*; -use super::super::*; -use super::*; - -/// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. -pub(in crate::interpreter) fn eval_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "abs" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.abs(*value)? - } - "addslashes" | "stripslashes" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_slashes_result(name, *value, values)? - } - "array_combine" => { - let [keys, values_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_combine_result(*keys, *values_array, values)? - } - "array_column" => { - let [array, column_key] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_column_result(*array, *column_key, values)? - } - "array_chunk" => { - let [array, length] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_chunk_result(*array, *length, values)? - } - "array_fill" => { - let [start, count, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_result(*start, *count, *value, values)? - } - "array_fill_keys" => { - let [keys, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_keys_result(*keys, *value, values)? - } - "array_filter" => match evaluated_args { - [array] => eval_array_filter_result(*array, None, None, context, values)?, - [array, callback] => { - eval_array_filter_result(*array, Some(*callback), None, context, values)? - } - [array, callback, mode] => { - eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_map" => { - let Some((callback, arrays)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_map_result(*callback, arrays, context, values)? - } - "array_reduce" => match evaluated_args { - [array, callback] => { - let initial = values.null()?; - eval_array_reduce_result(*array, *callback, initial, context, values)? - } - [array, callback, initial] => { - eval_array_reduce_result(*array, *callback, *initial, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_walk" => { - let [array, callback] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_walk_result(*array, *callback, context, values)? - } - "array_pop" | "array_shift" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_pop_shift_value_result(name, *array, values)? - } - "array_push" | "array_unshift" => { - let Some((array, inserted)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_push_unshift_count_result(*array, inserted.len(), values)? - } - "array_splice" => { - let result = match evaluated_args { - [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, - [array, offset, length] => { - eval_array_splice_value_result(*array, *offset, Some(*length), values)? - } - [array, offset, length, _replacement] => { - eval_array_splice_value_result(*array, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.warning( - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - )?; - result - } - "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" - | "shuffle" | "sort" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_sort_value_result(*array, values)? - } - "uasort" | "uksort" | "usort" => { - let [array, callback] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_user_sort_value_result(name, *array, *callback, context, values)? - } - "array_flip" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_flip_result(*array, values)? - } - "array_pad" => { - let [array, length, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_pad_result(*array, *length, *value, values)? - } - "array_product" | "array_sum" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_aggregate_result(name, *array, values)? - } - "array_keys" | "array_values" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_projection_result(name, *array, values)? - } - "array_key_exists" => { - let [key, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.array_key_exists(*key, *array)? - } - "array_diff" | "array_intersect" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_value_set_result(name, *left, *right, values)? - } - "array_diff_key" | "array_intersect_key" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_key_set_result(name, *left, *right, values)? - } - "array_merge" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_merge_result(*left, *right, values)? - } - "array_rand" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_rand_result(*array, values)? - } - "array_reverse" => match evaluated_args { - [array] => eval_array_reverse_result(*array, false, values)?, - [array, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_array_reverse_result(*array, preserve_keys, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_search" | "in_array" => { - let [needle, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_search_result(name, *needle, *array, values)? - } - "array_slice" => match evaluated_args { - [array, offset] => eval_array_slice_result(*array, *offset, None, values)?, - [array, offset, length] => { - eval_array_slice_result(*array, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_unique" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_unique_result(*array, values)? - } - "range" => { - let [start, end] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_range_result(*start, *end, values)? - } - "base64_encode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_encode_result(*value, values)? - } - "base64_decode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_decode_result(*value, values)? - } - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_unary_result(name, *value, values)? - } - "atan2" | "hypot" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_pair_result(name, *left, *right, values)? - } - "bin2hex" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_bin2hex_result(*value, values)? - } - "ceil" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.ceil(*value)? - } - "chr" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chr_result(*value, values)? - } - "chdir" | "mkdir" | "rmdir" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unary_path_bool_result(name, *path, values)? - } - "chmod" => { - let [filename, permissions] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chmod_result(*filename, *permissions, values)? - } - "clearstatcache" => { - if evaluated_args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - values.null()? - } - "clamp" => { - let [value, min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_clamp_result(*value, *min, *max, values)? - } - "copy" | "link" | "rename" | "symlink" => { - let [from, to] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_binary_path_bool_result(name, *from, *to, values)? - } - "floor" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.floor(*value)? - } - "fdiv" | "fmod" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_binary_result(name, *left, *right, values)? - } - "file" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_result(*filename, values)? - } - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_probe_result(name, *filename, values)? - } - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_stat_scalar_result(name, *filename, values)? - } - "file_get_contents" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_get_contents_result(*filename, values)? - } - "file_put_contents" => { - let [filename, data] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_put_contents_result(*filename, *data, values)? - } - "filesize" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_filesize_result(*filename, values)? - } - "filetype" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_filetype_result(*filename, values)? - } - "fnmatch" => match evaluated_args { - [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, - [pattern, filename, flags] => { - eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stat" | "lstat" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stat_array_result(name, *filename, values)? - } - "linkinfo" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_linkinfo_result(*path, values)? - } - "log" => match evaluated_args { - [num] => eval_log_result(*num, None, values)?, - [num, base] => eval_log_result(*num, Some(*base), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "readfile" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_readfile_result(*filename, values)? - } - "pi" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI)? - } - "php_uname" => match evaluated_args { - [] => eval_php_uname_result(None, values)?, - [mode] => eval_php_uname_result(Some(*mode), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "pow" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.pow(*left, *right)? - } - "preg_match" => match evaluated_args { - [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_match_all" => match evaluated_args { - [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_replace" => match evaluated_args { - [pattern, replacement, subject] => { - eval_preg_replace_result(*pattern, *replacement, *subject, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_replace_callback" => match evaluated_args { - [pattern, callback, subject] => { - eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_split" => match evaluated_args { - [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values)?, - [pattern, subject, limit] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), None, values)? - } - [pattern, subject, limit, flags] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "print_r" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_print_r_result(*value, values)? - } - "var_dump" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_var_dump_result(*value, values)? - } - "rand" | "mt_rand" => match evaluated_args { - [] => eval_rand_result(None, None, values)?, - [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "random_int" => { - let [min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_random_int_result(*min, *max, values)? - } - "rawurldecode" | "urldecode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_decode_result(name, *value, values)? - } - "rawurlencode" | "urlencode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_encode_result(name, *value, values)? - } - "round" => match evaluated_args { - [value] => values.round(*value, None)?, - [value, precision] => values.round(*value, Some(*precision))?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "scandir" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_scandir_result(*directory, values)? - } - "sqrt" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.sqrt(*value)? - } - "spl_classes" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values)? - } - "spl_object_id" | "spl_object_hash" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_spl_object_identity_result(name, *object, values)? - } - "sscanf" => { - let [input, format, ..] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sscanf_result(*input, *format, values)? - } - "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, - "settype" => { - let [value, type_name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_settype_value_result(*value, *type_name, values)? - } - "strrev" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.strrev(*value)? - } - "str_repeat" => { - let [value, times] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_repeat_result(*value, *times, values)? - } - "str_replace" | "str_ireplace" => { - let [search, replace, subject] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_replace_result(name, *search, *replace, *subject, values)? - } - "str_pad" => match evaluated_args { - [value, length] => eval_str_pad_result(*value, *length, None, None, values)?, - [value, length, pad_string] => { - eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? - } - [value, length, pad_string, pad_type] => { - eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "str_split" => match evaluated_args { - [value] => eval_str_split_result(*value, None, values)?, - [value, length] => eval_str_split_result(*value, Some(*length), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "substr" => match evaluated_args { - [value, offset] => eval_substr_result(*value, *offset, None, values)?, - [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "substr_replace" => match evaluated_args { - [value, replace, offset] => { - eval_substr_replace_result(*value, *replace, *offset, None, values)? - } - [value, replace, offset, length] => { - eval_substr_replace_result(*value, *replace, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "call_user_func" => { - return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) - .map(Some); - } - "call_user_func_array" => { - let [callback, arg_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) - .map(Some); - } - "boolval" | "floatval" | "intval" | "strval" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_cast_result(name, *value, values)? - } - "count" => match evaluated_args { - [value] => eval_count_result(*value, None, values)?, - [value, mode] => eval_count_result(*value, Some(*mode), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "crc32" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_crc32_result(*value, values)? - } - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ctype_result(name, *value, values)? - } - "date" => match evaluated_args { - [format] => eval_date_result(*format, None, values)?, - [format, timestamp] => eval_date_result(*format, Some(*timestamp), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "define" => eval_define_result(evaluated_args, context, values)?, - "defined" => eval_defined_result(evaluated_args, context, values)?, - "explode" => { - let [separator, string] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_explode_result(*separator, *string, values)? - } - "ord" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ord_result(*value, values)? - } - "implode" => { - let [separator, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_implode_result(*separator, *array, values)? - } - "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, - "microtime" => match evaluated_args { - [] | [_] => eval_microtime_result(values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "mktime" => { - let [hour, minute, second, month, day, year] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? - } - "nl2br" => match evaluated_args { - [value] => eval_nl2br_result(*value, true, values)?, - [value, use_xhtml] => { - let use_xhtml = values.truthy(*use_xhtml)?; - eval_nl2br_result(*value, use_xhtml, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "number_format" => match evaluated_args { - [value] => eval_number_format_result(*value, None, None, None, values)?, - [value, decimals] => { - eval_number_format_result(*value, Some(*decimals), None, None, values)? - } - [value, decimals, decimal_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - None, - values, - )?, - [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - Some(*thousands_separator), - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "basename" => match evaluated_args { - [path] => eval_basename_result(*path, None, values)?, - [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "dirname" => match evaluated_args { - [path] => eval_dirname_result(*path, None, values)?, - [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "disk_free_space" | "disk_total_space" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_disk_space_result(name, *directory, values)? - } - "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { - [value] => eval_trim_like_result(name, *value, None, values)?, - [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "function_exists" | "is_callable" => { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = values.string_bytes(*name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\').to_ascii_lowercase(); - values.bool_value(eval_function_probe_exists(context, &name))? - } - "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, - "enum_exists" | "trait_exists" => { - eval_class_like_exists_result(name, evaluated_args, values)? - } - "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, - "is_a" | "is_subclass_of" => { - eval_is_a_relation_result(name, evaluated_args, context, values)? - } - "json_decode" => match evaluated_args { - [json] => eval_json_decode_result(*json, None, None, None, context, values)?, - [json, associative] => { - eval_json_decode_result(*json, Some(*associative), None, None, context, values)? - } - [json, associative, depth] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - None, - context, - values, - )?, - [json, associative, depth, flags] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - Some(*flags), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "json_encode" => match evaluated_args { - [value] => eval_json_encode_result(*value, None, None, context, values)?, - [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values)?, - [value, flags, depth] => { - eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "json_last_error" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.int(context.json_last_error())? - } - "json_last_error_msg" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.string(context.json_last_error_msg())? - } - "json_validate" => match evaluated_args { - [json] => eval_json_validate_result(*json, None, None, context, values)?, - [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values)?, - [json, depth, flags] => { - eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "gethostbyaddr" => { - let [ip] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyaddr_result(*ip, values)? - } - "gethostbyname" => { - let [hostname] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyname_result(*hostname, values)? - } - "gethostname" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_gethostname_result(values)? - } - "getprotobyname" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobyname_result(*protocol, values)? - } - "getprotobynumber" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobynumber_result(*protocol, values)? - } - "getservbyname" => { - let [service, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyname_result(*service, *protocol, values)? - } - "getservbyport" => { - let [port, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyport_result(*port, *protocol, values)? - } - "getcwd" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_getcwd_result(values)? - } - "getenv" => { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getenv_result(*name, values)? - } - "get_class" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_get_class_result(*object, context, values)? - } - "get_parent_class" => { - let [object_or_class] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_get_parent_class_result(*object_or_class, values)? - } - "get_resource_id" | "get_resource_type" => { - let [resource] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_resource_introspection_result(name, *resource, values)? - } - "gettype" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gettype_result(*value, values)? - } - "glob" => { - let [pattern] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_glob_result(*pattern, values)? - } - "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { - eval_hash_one_shot_result(name, evaluated_args, values)? - } - "hash_algos" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values)? - } - "hash_equals" => { - let [known, user] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_equals_result(*known, *user, values)? - } - "hex2bin" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hex2bin_result(*value, values)? - } - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_html_entity_result(name, *value, values)? - } - "inet_ntop" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_ntop_result(*value, values)? - } - "inet_pton" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_pton_result(*value, values)? - } - "intdiv" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_intdiv_result(*left, *right, values)? - } - "iterator_apply" => match evaluated_args { - [iterator, callback] => { - let callback = eval_callable(*callback, values)?; - eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? - } - [iterator, callback, args] => { - let callback = eval_callable(*callback, values)?; - let callback_args = eval_iterator_apply_arg_values(*args, values)?; - eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "iterator_count" => { - let [iterator] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_iterator_count_result(*iterator, values)? - } - "iterator_to_array" => match evaluated_args { - [iterator] => eval_iterator_to_array_result(*iterator, true, values)?, - [iterator, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_iterator_to_array_result(*iterator, preserve_keys, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "ip2long" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ip2long_result(*value, values)? - } - "phpversion" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_phpversion_result(values)? - } - "pathinfo" => match evaluated_args { - [path] => eval_pathinfo_result(*path, None, values)?, - [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "putenv" => { - let [assignment] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_putenv_result(*assignment, values)? - } - "realpath" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_realpath_result(*path, values)? - } - "realpath_cache_get" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_get_result(values)? - } - "realpath_cache_size" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_size_result(values)? - } - "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" - | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_type_predicate_result(name, *value, values)? - } - "sys_get_temp_dir" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_sys_get_temp_dir_result(values)? - } - "tempnam" => { - let [directory, prefix] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_tempnam_result(*directory, *prefix, values)? - } - "sleep" => { - let [seconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sleep_result(*seconds, values)? - } - "time" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values)? - } - "touch" => match evaluated_args { - [filename] => eval_touch_result(*filename, None, None, values)?, - [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, - [filename, mtime, atime] => { - eval_touch_result(*filename, Some(*mtime), Some(*atime), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_introspection_result(name, values)? - } - "strtotime" => { - let [datetime] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_strtotime_result(*datetime, values)? - } - "umask" => match evaluated_args { - [] => eval_umask_result(None, values)?, - [mask] => eval_umask_result(Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "usleep" => { - let [microseconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_usleep_result(*microseconds, values)? - } - "readlink" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_readlink_result(*path, values)? - } - "unlink" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unlink_result(*filename, values)? - } - "strlen" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let bytes = values.string_bytes(*value)?; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len)? - } - "strpos" | "strrpos" => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_position_result(name, *haystack, *needle, values)? - } - "str_contains" | "str_starts_with" | "str_ends_with" => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_search_result(name, *haystack, *needle, values)? - } - "strstr" => match evaluated_args { - [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values)?, - [haystack, needle, before_needle] => { - let before_needle = values.truthy(*before_needle)?; - eval_strstr_result(*haystack, *needle, before_needle, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "strcmp" | "strcasecmp" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_compare_result(name, *left, *right, values)? - } - "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_case_result(name, *value, values)? - } - "long2ip" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_long2ip_result(*value, values)? - } - "ucwords" => match evaluated_args { - [value] => eval_ucwords_result(*value, None, values)?, - [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, - "wordwrap" => match evaluated_args { - [value] => eval_wordwrap_result(*value, None, None, None, values)?, - [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, - [value, width, break_string] => { - eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values)? - } - [value, width, break_string, cut] => eval_wordwrap_result( - *value, - Some(*width), - Some(*break_string), - Some(*cut), - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs new file mode 100644 index 0000000000..b187681991 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs @@ -0,0 +1,259 @@ +//! Purpose: +//! Dispatches already evaluated array and iterator builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; +use super::super::*; + +/// Attempts to dispatch evaluated array and iterator builtins. +pub(in crate::interpreter) fn eval_arrays_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "array_combine" => { + let [keys, values_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_combine_result(*keys, *values_array, values)? + } + "array_column" => { + let [array, column_key] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_column_result(*array, *column_key, values)? + } + "array_chunk" => { + let [array, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_chunk_result(*array, *length, values)? + } + "array_fill" => { + let [start, count, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_result(*start, *count, *value, values)? + } + "array_fill_keys" => { + let [keys, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_keys_result(*keys, *value, values)? + } + "array_filter" => match evaluated_args { + [array] => eval_array_filter_result(*array, None, None, context, values)?, + [array, callback] => { + eval_array_filter_result(*array, Some(*callback), None, context, values)? + } + [array, callback, mode] => { + eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_map" => { + let Some((callback, arrays)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_map_result(*callback, arrays, context, values)? + } + "array_reduce" => match evaluated_args { + [array, callback] => { + let initial = values.null()?; + eval_array_reduce_result(*array, *callback, initial, context, values)? + } + [array, callback, initial] => { + eval_array_reduce_result(*array, *callback, *initial, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_walk" => { + let [array, callback] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_walk_result(*array, *callback, context, values)? + } + "array_pop" | "array_shift" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_pop_shift_value_result(name, *array, values)? + } + "array_push" | "array_unshift" => { + let Some((array, inserted)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_push_unshift_count_result(*array, inserted.len(), values)? + } + "array_splice" => { + let result = match evaluated_args { + [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, + [array, offset, length] => { + eval_array_splice_value_result(*array, *offset, Some(*length), values)? + } + [array, offset, length, _replacement] => { + eval_array_splice_value_result(*array, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.warning( + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + )?; + result + } + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_array_sort_value_result(*array, values)? + } + "uasort" | "uksort" | "usort" => { + let [array, callback] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + ))?; + eval_user_sort_value_result(name, *array, *callback, context, values)? + } + "array_flip" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_flip_result(*array, values)? + } + "array_pad" => { + let [array, length, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_pad_result(*array, *length, *value, values)? + } + "array_product" | "array_sum" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_aggregate_result(name, *array, values)? + } + "array_keys" | "array_values" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_projection_result(name, *array, values)? + } + "array_key_exists" => { + let [key, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.array_key_exists(*key, *array)? + } + "array_diff" | "array_intersect" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_value_set_result(name, *left, *right, values)? + } + "array_diff_key" | "array_intersect_key" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_key_set_result(name, *left, *right, values)? + } + "array_merge" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_merge_result(*left, *right, values)? + } + "array_rand" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_rand_result(*array, values)? + } + "array_reverse" => match evaluated_args { + [array] => eval_array_reverse_result(*array, false, values)?, + [array, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_reverse_result(*array, preserve_keys, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_search" | "in_array" => { + let [needle, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_search_result(name, *needle, *array, values)? + } + "array_slice" => match evaluated_args { + [array, offset] => eval_array_slice_result(*array, *offset, None, values)?, + [array, offset, length] => { + eval_array_slice_result(*array, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "array_unique" => { + let [array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_unique_result(*array, values)? + } + "range" => { + let [start, end] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_range_result(*start, *end, values)? + } + "count" => match evaluated_args { + [value] => eval_count_result(*value, None, values)?, + [value, mode] => eval_count_result(*value, Some(*mode), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "iterator_apply" => match evaluated_args { + [iterator, callback] => { + let callback = eval_callable(*callback, values)?; + eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? + } + [iterator, callback, args] => { + let callback = eval_callable(*callback, values)?; + let callback_args = eval_iterator_apply_arg_values(*args, values)?; + eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "iterator_count" => { + let [iterator] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_iterator_count_result(*iterator, values)? + } + "iterator_to_array" => match evaluated_args { + [iterator] => eval_iterator_to_array_result(*iterator, true, values)?, + [iterator, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_iterator_to_array_result(*iterator, preserve_keys, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs new file mode 100644 index 0000000000..902474e157 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Dispatches already evaluated core callable, constant, and debug-output builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; +use super::super::*; + +/// Attempts to dispatch evaluated core callable, constant, and debug-output builtins. +pub(in crate::interpreter) fn eval_core_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "print_r" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_print_r_result(*value, values)? + } + "var_dump" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_var_dump_result(*value, values)? + } + "call_user_func" => { + return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) + .map(Some); + } + "call_user_func_array" => { + let [callback, arg_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) + .map(Some); + } + "define" => eval_define_result(evaluated_args, context, values)?, + "defined" => eval_defined_result(evaluated_args, context, values)?, + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs new file mode 100644 index 0000000000..f19eb39e6e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -0,0 +1,212 @@ +//! Purpose: +//! Dispatches already evaluated filesystem and path builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; + +/// Attempts to dispatch evaluated filesystem and path builtins. +pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "chdir" | "mkdir" | "rmdir" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unary_path_bool_result(name, *path, values)? + } + "chmod" => { + let [filename, permissions] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chmod_result(*filename, *permissions, values)? + } + "clearstatcache" => { + if evaluated_args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + values.null()? + } + "copy" | "link" | "rename" | "symlink" => { + let [from, to] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_binary_path_bool_result(name, *from, *to, values)? + } + "file" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_result(*filename, values)? + } + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_probe_result(name, *filename, values)? + } + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_stat_scalar_result(name, *filename, values)? + } + "file_get_contents" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_get_contents_result(*filename, values)? + } + "file_put_contents" => { + let [filename, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_file_put_contents_result(*filename, *data, values)? + } + "filesize" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_filesize_result(*filename, values)? + } + "filetype" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_filetype_result(*filename, values)? + } + "fnmatch" => match evaluated_args { + [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, + [pattern, filename, flags] => { + eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stat" | "lstat" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stat_array_result(name, *filename, values)? + } + "linkinfo" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_linkinfo_result(*path, values)? + } + "readfile" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_readfile_result(*filename, values)? + } + "scandir" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_scandir_result(*directory, values)? + } + "basename" => match evaluated_args { + [path] => eval_basename_result(*path, None, values)?, + [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "dirname" => match evaluated_args { + [path] => eval_dirname_result(*path, None, values)?, + [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "disk_free_space" | "disk_total_space" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_disk_space_result(name, *directory, values)? + } + "getcwd" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values)? + } + "glob" => { + let [pattern] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_glob_result(*pattern, values)? + } + "pathinfo" => match evaluated_args { + [path] => eval_pathinfo_result(*path, None, values)?, + [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "realpath" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_realpath_result(*path, values)? + } + "realpath_cache_get" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values)? + } + "realpath_cache_size" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values)? + } + "sys_get_temp_dir" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values)? + } + "tempnam" => { + let [directory, prefix] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_tempnam_result(*directory, *prefix, values)? + } + "touch" => match evaluated_args { + [filename] => eval_touch_result(*filename, None, None, values)?, + [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, + [filename, mtime, atime] => { + eval_touch_result(*filename, Some(*mtime), Some(*atime), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "umask" => match evaluated_args { + [] => eval_umask_result(None, values)?, + [mask] => eval_umask_result(Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "readlink" => { + let [path] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_readlink_result(*path, values)? + } + "unlink" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unlink_result(*filename, values)? + } + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/formatting.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/formatting.rs new file mode 100644 index 0000000000..933c8904fe --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/formatting.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Dispatches already evaluated numeric formatting and printf-family builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; + +/// Attempts to dispatch evaluated numeric formatting and printf-family builtins. +pub(in crate::interpreter) fn eval_formatting_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "ceil" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.ceil(*value)? + } + "floor" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.floor(*value)? + } + "pi" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI)? + } + "pow" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.pow(*left, *right)? + } + "round" => match evaluated_args { + [value] => values.round(*value, None)?, + [value, precision] => values.round(*value, Some(*precision))?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "sscanf" => { + let [input, format, ..] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sscanf_result(*input, *format, values)? + } + "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, + "number_format" => match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values)?, + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values)? + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + )?, + [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/json.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/json.rs new file mode 100644 index 0000000000..c546079920 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/json.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Dispatches already evaluated JSON builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; + +/// Attempts to dispatch evaluated JSON builtins. +pub(in crate::interpreter) fn eval_json_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "json_decode" => match evaluated_args { + [json] => eval_json_decode_result(*json, None, None, None, context, values)?, + [json, associative] => { + eval_json_decode_result(*json, Some(*associative), None, None, context, values)? + } + [json, associative, depth] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + None, + context, + values, + )?, + [json, associative, depth, flags] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + Some(*flags), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "json_encode" => match evaluated_args { + [value] => eval_json_encode_result(*value, None, None, context, values)?, + [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values)?, + [value, flags, depth] => { + eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "json_last_error" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.int(context.json_last_error())? + } + "json_last_error_msg" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string(context.json_last_error_msg())? + } + "json_validate" => match evaluated_args { + [json] => eval_json_validate_result(*json, None, None, context, values)?, + [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values)?, + [json, depth, flags] => { + eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/mod.rs new file mode 100644 index 0000000000..14b90293de --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/mod.rs @@ -0,0 +1,84 @@ +//! Purpose: +//! Routes by-value dynamic builtin dispatch to focused builtin-family dispatchers. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` re-exports. +//! +//! Key details: +//! - Each child dispatcher handles already evaluated runtime-cell arguments for one +//! builtin family and returns `Ok(None)` when the name is outside its domain. + +mod arrays; +mod core; +mod filesystem; +mod formatting; +mod json; +mod network_env; +mod regex; +mod scalars; +mod strings; +mod symbols; +mod time; + +use super::super::super::*; + +use arrays::*; +use core::*; +use filesystem::*; +use formatting::*; +use json::*; +use network_env::*; +use regex::*; +use scalars::*; +use strings::*; +use symbols::*; +use time::*; + +/// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. +pub(in crate::interpreter) fn eval_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(result) = eval_arrays_builtin_with_values(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + if let Some(result) = + eval_filesystem_builtin_with_values(name, evaluated_args, context, values)? + { + return Ok(Some(result)); + } + if let Some(result) = + eval_formatting_builtin_with_values(name, evaluated_args, context, values)? + { + return Ok(Some(result)); + } + if let Some(result) = eval_regex_builtin_with_values(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + if let Some(result) = eval_strings_builtin_with_values(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + if let Some(result) = eval_scalars_builtin_with_values(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + if let Some(result) = eval_time_builtin_with_values(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + if let Some(result) = + eval_network_env_builtin_with_values(name, evaluated_args, context, values)? + { + return Ok(Some(result)); + } + if let Some(result) = eval_symbols_builtin_with_values(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + if let Some(result) = eval_core_builtin_with_values(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + if let Some(result) = eval_json_builtin_with_values(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + Ok(None) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs new file mode 100644 index 0000000000..d50b37c8e7 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs @@ -0,0 +1,120 @@ +//! Purpose: +//! Dispatches already evaluated network, environment, and stream-introspection builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; + +/// Attempts to dispatch evaluated network, environment, and stream-introspection builtins. +pub(in crate::interpreter) fn eval_network_env_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "php_uname" => match evaluated_args { + [] => eval_php_uname_result(None, values)?, + [mode] => eval_php_uname_result(Some(*mode), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "gethostbyaddr" => { + let [ip] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyaddr_result(*ip, values)? + } + "gethostbyname" => { + let [hostname] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyname_result(*hostname, values)? + } + "gethostname" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_gethostname_result(values)? + } + "getprotobyname" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobyname_result(*protocol, values)? + } + "getprotobynumber" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobynumber_result(*protocol, values)? + } + "getservbyname" => { + let [service, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyname_result(*service, *protocol, values)? + } + "getservbyport" => { + let [port, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyport_result(*port, *protocol, values)? + } + "getenv" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getenv_result(*name, values)? + } + "inet_ntop" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_ntop_result(*value, values)? + } + "inet_pton" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_pton_result(*value, values)? + } + "ip2long" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ip2long_result(*value, values)? + } + "phpversion" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values)? + } + "putenv" => { + let [assignment] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_putenv_result(*assignment, values)? + } + "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, values)? + } + "long2ip" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_long2ip_result(*value, values)? + } + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/regex.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/regex.rs new file mode 100644 index 0000000000..c184358ea7 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/regex.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Dispatches already evaluated preg regex builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; + +/// Attempts to dispatch evaluated preg regex builtins. +pub(in crate::interpreter) fn eval_regex_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "preg_match" => match evaluated_args { + [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_match_all" => match evaluated_args { + [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_replace" => match evaluated_args { + [pattern, replacement, subject] => { + eval_preg_replace_result(*pattern, *replacement, *subject, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_replace_callback" => match evaluated_args { + [pattern, callback, subject] => { + eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "preg_split" => match evaluated_args { + [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values)?, + [pattern, subject, limit] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), None, values)? + } + [pattern, subject, limit, flags] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs new file mode 100644 index 0000000000..62bb82e61b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs @@ -0,0 +1,111 @@ +//! Purpose: +//! Dispatches already evaluated scalar math, casts, predicates, and random builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; + +/// Attempts to dispatch evaluated scalar math, casts, predicates, and random builtins. +pub(in crate::interpreter) fn eval_scalars_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "abs" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.abs(*value)? + } + "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" + | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_unary_result(name, *value, values)? + } + "atan2" | "hypot" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_pair_result(name, *left, *right, values)? + } + "clamp" => { + let [value, min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_clamp_result(*value, *min, *max, values)? + } + "fdiv" | "fmod" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_binary_result(name, *left, *right, values)? + } + "log" => match evaluated_args { + [num] => eval_log_result(*num, None, values)?, + [num, base] => eval_log_result(*num, Some(*base), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "rand" | "mt_rand" => match evaluated_args { + [] => eval_rand_result(None, None, values)?, + [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "random_int" => { + let [min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_random_int_result(*min, *max, values)? + } + "sqrt" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.sqrt(*value)? + } + "settype" => { + let [value, type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_settype_value_result(*value, *type_name, values)? + } + "boolval" | "floatval" | "intval" | "strval" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_cast_result(name, *value, values)? + } + "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, + "gettype" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gettype_result(*value, values)? + } + "intdiv" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_intdiv_result(*left, *right, values)? + } + "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" + | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" + | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_type_predicate_result(name, *value, values)? + } + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs new file mode 100644 index 0000000000..d3e7057ae2 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs @@ -0,0 +1,244 @@ +//! Purpose: +//! Dispatches already evaluated string, hash, encoding, and ctype builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; + +/// Attempts to dispatch evaluated string, hash, encoding, and ctype builtins. +pub(in crate::interpreter) fn eval_strings_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "addslashes" | "stripslashes" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_slashes_result(name, *value, values)? + } + "base64_encode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_encode_result(*value, values)? + } + "base64_decode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_decode_result(*value, values)? + } + "bin2hex" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_bin2hex_result(*value, values)? + } + "chr" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chr_result(*value, values)? + } + "rawurldecode" | "urldecode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_decode_result(name, *value, values)? + } + "rawurlencode" | "urlencode" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_encode_result(name, *value, values)? + } + "strrev" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.strrev(*value)? + } + "str_repeat" => { + let [value, times] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_repeat_result(*value, *times, values)? + } + "str_replace" | "str_ireplace" => { + let [search, replace, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_replace_result(name, *search, *replace, *subject, values)? + } + "str_pad" => match evaluated_args { + [value, length] => eval_str_pad_result(*value, *length, None, None, values)?, + [value, length, pad_string] => { + eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? + } + [value, length, pad_string, pad_type] => { + eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "str_split" => match evaluated_args { + [value] => eval_str_split_result(*value, None, values)?, + [value, length] => eval_str_split_result(*value, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "substr" => match evaluated_args { + [value, offset] => eval_substr_result(*value, *offset, None, values)?, + [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "substr_replace" => match evaluated_args { + [value, replace, offset] => { + eval_substr_replace_result(*value, *replace, *offset, None, values)? + } + [value, replace, offset, length] => { + eval_substr_replace_result(*value, *replace, *offset, Some(*length), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "crc32" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_crc32_result(*value, values)? + } + "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ctype_result(name, *value, values)? + } + "explode" => { + let [separator, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_explode_result(*separator, *string, values)? + } + "ord" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ord_result(*value, values)? + } + "implode" => { + let [separator, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_implode_result(*separator, *array, values)? + } + "nl2br" => match evaluated_args { + [value] => eval_nl2br_result(*value, true, values)?, + [value, use_xhtml] => { + let use_xhtml = values.truthy(*use_xhtml)?; + eval_nl2br_result(*value, use_xhtml, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { + [value] => eval_trim_like_result(name, *value, None, values)?, + [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { + eval_hash_one_shot_result(name, evaluated_args, values)? + } + "hash_algos" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values)? + } + "hash_equals" => { + let [known, user] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_equals_result(*known, *user, values)? + } + "hex2bin" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hex2bin_result(*value, values)? + } + "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_html_entity_result(name, *value, values)? + } + "strlen" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let bytes = values.string_bytes(*value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len)? + } + "strpos" | "strrpos" => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_position_result(name, *haystack, *needle, values)? + } + "str_contains" | "str_starts_with" | "str_ends_with" => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_search_result(name, *haystack, *needle, values)? + } + "strstr" => match evaluated_args { + [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values)?, + [haystack, needle, before_needle] => { + let before_needle = values.truthy(*before_needle)?; + eval_strstr_result(*haystack, *needle, before_needle, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "strcmp" | "strcasecmp" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_compare_result(name, *left, *right, values)? + } + "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_case_result(name, *value, values)? + } + "ucwords" => match evaluated_args { + [value] => eval_ucwords_result(*value, None, values)?, + [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "wordwrap" => match evaluated_args { + [value] => eval_wordwrap_result(*value, None, None, None, values)?, + [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, + [value, width, break_string] => { + eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values)? + } + [value, width, break_string, cut] => eval_wordwrap_result( + *value, + Some(*width), + Some(*break_string), + Some(*cut), + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs new file mode 100644 index 0000000000..e76a74ccc0 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Dispatches already evaluated symbol, class, object, and resource introspection builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; + +/// Attempts to dispatch evaluated symbol, class, object, and resource introspection builtins. +pub(in crate::interpreter) fn eval_symbols_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "spl_classes" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values)? + } + "spl_object_id" | "spl_object_hash" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_spl_object_identity_result(name, *object, values)? + } + "function_exists" | "is_callable" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = values.string_bytes(*name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\').to_ascii_lowercase(); + values.bool_value(eval_function_probe_exists(context, &name))? + } + "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "enum_exists" | "trait_exists" => { + eval_class_like_exists_result(name, evaluated_args, values)? + } + "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, + "is_a" | "is_subclass_of" => { + eval_is_a_relation_result(name, evaluated_args, context, values)? + } + "get_class" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_get_class_result(*object, context, values)? + } + "get_parent_class" => { + let [object_or_class] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_get_parent_class_result(*object_or_class, values)? + } + "get_resource_id" | "get_resource_type" => { + let [resource] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_resource_introspection_result(name, *resource, values)? + } + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/time.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/time.rs new file mode 100644 index 0000000000..54cbfd9aad --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/time.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Dispatches already evaluated date, time, and sleep builtins by dynamic callable name. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::dispatch`. +//! +//! Key details: +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can +//! continue probing other builtin families. + +use super::super::super::super::*; +use super::super::super::*; + +/// Attempts to dispatch evaluated date, time, and sleep builtins. +pub(in crate::interpreter) fn eval_time_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "date" => match evaluated_args { + [format] => eval_date_result(*format, None, values)?, + [format, timestamp] => eval_date_result(*format, Some(*timestamp), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "microtime" => match evaluated_args { + [] | [_] => eval_microtime_result(values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "mktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? + } + "sleep" => { + let [seconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sleep_result(*seconds, values)? + } + "time" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values)? + } + "strtotime" => { + let [datetime] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_strtotime_result(*datetime, values)? + } + "usleep" => { + let [microseconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_usleep_result(*microseconds, values)? + } + _ => return Ok(None), + }; + Ok(Some(result)) +} From f2d291875d730dc8341d22faaeea77f5b1514b31 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:43:10 +0200 Subject: [PATCH 0291/1208] Split eval time builtins --- .../src/interpreter/builtins/time.rs | 570 ------------------ .../src/interpreter/builtins/time/clock.rs | 67 ++ .../src/interpreter/builtins/time/date.rs | 156 +++++ .../src/interpreter/builtins/time/mktime.rs | 81 +++ .../src/interpreter/builtins/time/mod.rs | 24 + .../src/interpreter/builtins/time/sleep.rs | 62 ++ .../interpreter/builtins/time/strtotime.rs | 132 ++++ .../src/interpreter/builtins/time/system.rs | 131 ++++ 8 files changed, 653 insertions(+), 570 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/time.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/time/clock.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/time/date.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/time/mktime.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/time/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/time/sleep.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/time/strtotime.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/time/system.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/time.rs b/crates/elephc-eval/src/interpreter/builtins/time.rs deleted file mode 100644 index 656eb84ab8..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/time.rs +++ /dev/null @@ -1,570 +0,0 @@ -//! Purpose: -//! Time, date, sleep, PHP version, and uname builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins` re-exports used by core call dispatch. -//! -//! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime -//! behavior through `RuntimeValueOps`. - -use super::super::*; -use super::*; - -/// Evaluates PHP `time()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_time( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values) -} - -/// Returns the current Unix timestamp as a boxed PHP integer. -pub(in crate::interpreter) fn eval_time_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.int(eval_current_unix_timestamp()?) -} - -/// Returns the current Unix timestamp as an integer payload. -pub(in crate::interpreter) fn eval_current_unix_timestamp() -> Result { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| EvalStatus::RuntimeFatal)? - .as_secs(); - i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. -pub(in crate::interpreter) fn eval_builtin_date( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [format] => { - let format = eval_expr(format, context, scope, values)?; - eval_date_result(format, None, values) - } - [format, timestamp] => { - let format = eval_expr(format, context, scope, values)?; - let timestamp = eval_expr(timestamp, context, scope, values)?; - eval_date_result(format, Some(timestamp), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. -pub(in crate::interpreter) fn eval_date_result( - format: RuntimeCellHandle, - timestamp: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let format = values.string_bytes(format)?; - let timestamp = match timestamp { - Some(timestamp) => eval_int_value(timestamp, values)?, - None => eval_current_unix_timestamp()?, - }; - let tm = eval_localtime(timestamp)?; - let output = eval_format_date_bytes(&format, &tm, timestamp)?; - values.string_bytes_value(&output) -} - -/// Converts one Unix timestamp to local broken-down time through libc. -pub(in crate::interpreter) fn eval_localtime(timestamp: i64) -> Result { - let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; - let mut tm = MaybeUninit::::uninit(); - let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; - if result.is_null() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(unsafe { tm.assume_init() }) -} - -/// Applies PHP `date()` tokens to one local broken-down timestamp. -pub(in crate::interpreter) fn eval_format_date_bytes( - format: &[u8], - tm: &libc::tm, - timestamp: i64, -) -> Result, EvalStatus> { - let mut output = Vec::new(); - let mut escaped = false; - for byte in format { - if escaped { - output.push(*byte); - escaped = false; - continue; - } - if *byte == b'\\' { - escaped = true; - continue; - } - eval_push_date_token(&mut output, *byte, tm, timestamp)?; - } - if escaped { - output.push(b'\\'); - } - Ok(output) -} - -/// Appends the expansion for one PHP `date()` token, or the token literal. -pub(in crate::interpreter) fn eval_push_date_token( - output: &mut Vec, - token: u8, - tm: &libc::tm, - timestamp: i64, -) -> Result<(), EvalStatus> { - match token { - b'Y' => eval_push_padded_number(output, i64::from(tm.tm_year) + 1900, 4), - b'm' => eval_push_padded_number(output, i64::from(tm.tm_mon) + 1, 2), - b'd' => eval_push_padded_number(output, i64::from(tm.tm_mday), 2), - b'H' => eval_push_padded_number(output, i64::from(tm.tm_hour), 2), - b'i' => eval_push_padded_number(output, i64::from(tm.tm_min), 2), - b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), - b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), - b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), - b'D' => output - .extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), - b'M' => { - output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) - } - b'N' => { - let weekday = tm.tm_wday; - let iso_weekday = if weekday == 0 { 7 } else { weekday }; - output.extend_from_slice(iso_weekday.to_string().as_bytes()); - } - b'j' => output.extend_from_slice(tm.tm_mday.to_string().as_bytes()), - b'n' => output.extend_from_slice((tm.tm_mon + 1).to_string().as_bytes()), - b'G' => output.extend_from_slice(tm.tm_hour.to_string().as_bytes()), - b'g' => { - let hour = tm.tm_hour % 12; - let hour = if hour == 0 { 12 } else { hour }; - output.extend_from_slice(hour.to_string().as_bytes()); - } - b'A' => output.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }), - b'a' => output.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }), - b'U' => output.extend_from_slice(timestamp.to_string().as_bytes()), - _ => output.push(token), - } - Ok(()) -} - -/// Returns a checked month index for PHP `date()` name tables. -pub(in crate::interpreter) fn eval_tm_month_index(tm: &libc::tm) -> Result { - let index = usize::try_from(tm.tm_mon).map_err(|_| EvalStatus::RuntimeFatal)?; - if index >= EVAL_MONTH_NAMES.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(index) -} - -/// Returns a checked weekday index for PHP `date()` name tables. -pub(in crate::interpreter) fn eval_tm_weekday_index(tm: &libc::tm) -> Result { - let index = usize::try_from(tm.tm_wday).map_err(|_| EvalStatus::RuntimeFatal)?; - if index >= EVAL_WEEKDAY_NAMES.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(index) -} - -/// Appends one zero-padded decimal value with the requested minimum width. -pub(in crate::interpreter) fn eval_push_padded_number( - output: &mut Vec, - value: i64, - width: usize, -) { - output.extend_from_slice(format!("{value:0width$}").as_bytes()); -} - -/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. -pub(in crate::interpreter) fn eval_builtin_mktime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hour, minute, second, month, day, year] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hour = eval_expr(hour, context, scope, values)?; - let minute = eval_expr(minute, context, scope, values)?; - let second = eval_expr(second, context, scope, values)?; - let month = eval_expr(month, context, scope, values)?; - let day = eval_expr(day, context, scope, values)?; - let year = eval_expr(year, context, scope, values)?; - eval_mktime_result(hour, minute, second, month, day, year, values) -} - -/// Converts PHP date components to a local Unix timestamp through libc `mktime`. -pub(in crate::interpreter) fn eval_mktime_result( - hour: RuntimeCellHandle, - minute: RuntimeCellHandle, - second: RuntimeCellHandle, - month: RuntimeCellHandle, - day: RuntimeCellHandle, - year: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let timestamp = eval_mktime_timestamp( - eval_int_cell_as_c_int(hour, values)?, - eval_int_cell_as_c_int(minute, values)?, - eval_int_cell_as_c_int(second, values)?, - eval_int_cell_as_c_int(month, values)?, - eval_int_cell_as_c_int(day, values)?, - eval_int_cell_as_c_int(year, values)?, - )?; - values.int(timestamp) -} - -/// Converts local date components into a Unix timestamp through libc `mktime`. -pub(in crate::interpreter) fn eval_mktime_timestamp( - hour: libc::c_int, - minute: libc::c_int, - second: libc::c_int, - month: libc::c_int, - day: libc::c_int, - year: libc::c_int, -) -> Result { - let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; - tm.tm_hour = hour; - tm.tm_min = minute; - tm.tm_sec = second; - tm.tm_mon = month - 1; - tm.tm_mday = day; - tm.tm_year = year - 1900; - tm.tm_isdst = -1; - let timestamp = unsafe { libc::mktime(&mut tm) }; - i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. -pub(in crate::interpreter) fn eval_int_cell_as_c_int( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates PHP `strtotime(datetime)` for eval's supported date-string subset. -pub(in crate::interpreter) fn eval_builtin_strtotime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [datetime] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let datetime = eval_expr(datetime, context, scope, values)?; - eval_strtotime_result(datetime, values) -} - -/// Parses one eval `strtotime()` input and boxes the resulting timestamp. -pub(in crate::interpreter) fn eval_strtotime_result( - datetime: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(datetime)?; - let timestamp = eval_strtotime_bytes(&bytes)?; - values.int(timestamp) -} - -/// Parses eval's supported `strtotime()` strings into local Unix timestamps. -pub(in crate::interpreter) fn eval_strtotime_bytes(bytes: &[u8]) -> Result { - let bytes = eval_trim_ascii_whitespace(bytes); - if bytes.eq_ignore_ascii_case(b"now") { - return eval_current_unix_timestamp(); - } - let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { - return Ok(-1); - }; - eval_mktime_timestamp(hour, minute, second, month, day, year) -} - -/// Trims ASCII whitespace from both ends of one byte slice. -pub(in crate::interpreter) fn eval_trim_ascii_whitespace(bytes: &[u8]) -> &[u8] { - let mut start = 0; - let mut end = bytes.len(); - while start < end && bytes[start].is_ascii_whitespace() { - start += 1; - } - while end > start && bytes[end - 1].is_ascii_whitespace() { - end -= 1; - } - &bytes[start..end] -} - -/// Parses fixed-width ISO date and datetime forms supported by eval `strtotime()`. -pub(in crate::interpreter) fn eval_parse_iso_datetime( - bytes: &[u8], -) -> Option<( - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, - libc::c_int, -)> { - if bytes.len() != 10 && bytes.len() != 16 && bytes.len() != 19 { - return None; - } - if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { - return None; - } - let year = eval_parse_fixed_digits(bytes, 0, 4)?; - let month = eval_parse_fixed_digits(bytes, 5, 2)?; - let day = eval_parse_fixed_digits(bytes, 8, 2)?; - let (hour, minute, second) = if bytes.len() == 10 { - (0, 0, 0) - } else { - if !matches!(bytes.get(10), Some(b' ') | Some(b'T') | Some(b't')) { - return None; - } - if bytes.get(13) != Some(&b':') { - return None; - } - let hour = eval_parse_fixed_digits(bytes, 11, 2)?; - let minute = eval_parse_fixed_digits(bytes, 14, 2)?; - let second = if bytes.len() == 19 { - if bytes.get(16) != Some(&b':') { - return None; - } - eval_parse_fixed_digits(bytes, 17, 2)? - } else { - 0 - }; - (hour, minute, second) - }; - if !(1..=12).contains(&month) - || !(1..=31).contains(&day) - || !(0..=23).contains(&hour) - || !(0..=59).contains(&minute) - || !(0..=59).contains(&second) - { - return None; - } - Some((year, month, day, hour, minute, second)) -} - -/// Parses a fixed-width decimal field as a libc-compatible integer. -pub(in crate::interpreter) fn eval_parse_fixed_digits( - bytes: &[u8], - start: usize, - len: usize, -) -> Option { - let end = start.checked_add(len)?; - let field = bytes.get(start..end)?; - let mut value: libc::c_int = 0; - for byte in field { - if !byte.is_ascii_digit() { - return None; - } - value = value.checked_mul(10)?; - value = value.checked_add(libc::c_int::from(byte - b'0'))?; - } - Some(value) -} - -/// Evaluates PHP `microtime()` with an optional ignored argument. -pub(in crate::interpreter) fn eval_builtin_microtime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_microtime_result(values), - [as_float] => { - let _ = eval_expr(as_float, context, scope, values)?; - eval_microtime_result(values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the current Unix timestamp with microsecond precision as a boxed float. -pub(in crate::interpreter) fn eval_microtime_result( - values: &mut impl RuntimeValueOps, -) -> Result { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_err(|_| EvalStatus::RuntimeFatal)?; - let seconds = timestamp.as_secs() as f64; - let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; - values.float(seconds + micros) -} - -/// Evaluates PHP `sleep($seconds)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_sleep( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [seconds] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let seconds = eval_expr(seconds, context, scope, values)?; - eval_sleep_result(seconds, values) -} - -/// Sleeps for a non-negative number of seconds and returns PHP's remaining-seconds value. -pub(in crate::interpreter) fn eval_sleep_result( - seconds: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let seconds = eval_int_value(seconds, values)?; - let seconds = u64::try_from(seconds).map_err(|_| EvalStatus::RuntimeFatal)?; - std::thread::sleep(std::time::Duration::from_secs(seconds)); - values.int(0) -} - -/// Evaluates PHP `usleep($microseconds)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_usleep( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [microseconds] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let microseconds = eval_expr(microseconds, context, scope, values)?; - eval_usleep_result(microseconds, values) -} - -/// Sleeps for a non-negative number of microseconds and returns PHP null. -pub(in crate::interpreter) fn eval_usleep_result( - microseconds: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let microseconds = eval_int_value(microseconds, values)?; - let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; - std::thread::sleep(std::time::Duration::from_micros(microseconds)); - values.null() -} - -/// Evaluates PHP `phpversion()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_phpversion( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_phpversion_result(values) -} - -/// Returns the root elephc package version as a boxed PHP string. -pub(in crate::interpreter) fn eval_phpversion_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.string(eval_compiler_php_version()) -} - -/// Reads the root package version from the workspace manifest used by native `phpversion()`. -pub(in crate::interpreter) fn eval_compiler_php_version() -> &'static str { - let mut in_package = false; - for line in EVAL_ROOT_CARGO_TOML.lines() { - let line = line.trim(); - if line == "[package]" { - in_package = true; - continue; - } - if in_package && line.starts_with('[') { - break; - } - if in_package { - if let Some(value) = line.strip_prefix("version = ") { - return value.trim_matches('"'); - } - } - } - env!("CARGO_PKG_VERSION") -} - -/// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. -pub(in crate::interpreter) fn eval_builtin_php_uname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_php_uname_result(None, values), - [mode] => { - let mode = eval_expr(mode, context, scope, values)?; - eval_php_uname_result(Some(mode), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Reads the local uname fields and formats the PHP `php_uname()` mode result. -pub(in crate::interpreter) fn eval_php_uname_result( - mode: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = match mode { - Some(mode) => { - let bytes = values.string_bytes(mode)?; - let [mode] = bytes.as_slice() else { - return Err(EvalStatus::RuntimeFatal); - }; - *mode - } - None => b'a', - }; - - let mut utsname = std::mem::MaybeUninit::::zeroed(); - let status = unsafe { - // libc writes all uname fields into the stack-owned utsname buffer. - libc::uname(utsname.as_mut_ptr()) - }; - if status != 0 { - return values.string(""); - } - let utsname = unsafe { - // `uname` succeeded, so libc initialized the full `utsname` structure. - utsname.assume_init() - }; - let sysname = eval_uname_field_bytes(&utsname.sysname); - let nodename = eval_uname_field_bytes(&utsname.nodename); - let release = eval_uname_field_bytes(&utsname.release); - let version = eval_uname_field_bytes(&utsname.version); - let machine = eval_uname_field_bytes(&utsname.machine); - - match mode { - b'a' => { - let mut output = Vec::new(); - for field in [&sysname, &nodename, &release, &version, &machine] { - if !output.is_empty() { - output.push(b' '); - } - output.extend_from_slice(field); - } - values.string_bytes_value(&output) - } - b's' => values.string_bytes_value(&sysname), - b'n' => values.string_bytes_value(&nodename), - b'r' => values.string_bytes_value(&release), - b'v' => values.string_bytes_value(&version), - b'm' => values.string_bytes_value(&machine), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. -pub(in crate::interpreter) fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { - let length = field - .iter() - .position(|byte| *byte == 0) - .unwrap_or(field.len()); - field[..length].iter().map(|byte| *byte as u8).collect() -} diff --git a/crates/elephc-eval/src/interpreter/builtins/time/clock.rs b/crates/elephc-eval/src/interpreter/builtins/time/clock.rs new file mode 100644 index 0000000000..9888181f86 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/time/clock.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Implements `time()` and `microtime()` eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` re-exports. +//! +//! Key details: +//! - Current timestamps are read from `SystemTime::now()` and converted into PHP +//! integer or float runtime cells. + +use super::super::super::*; + +/// Evaluates PHP `time()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_time( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) +} + +/// Returns the current Unix timestamp as a boxed PHP integer. +pub(in crate::interpreter) fn eval_time_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(eval_current_unix_timestamp()?) +} + +/// Returns the current Unix timestamp as an integer payload. +pub(in crate::interpreter) fn eval_current_unix_timestamp() -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)? + .as_secs(); + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Evaluates PHP `microtime()` with an optional ignored argument. +pub(in crate::interpreter) fn eval_builtin_microtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_microtime_result(values), + [as_float] => { + let _ = eval_expr(as_float, context, scope, values)?; + eval_microtime_result(values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the current Unix timestamp with microsecond precision as a boxed float. +pub(in crate::interpreter) fn eval_microtime_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)?; + let seconds = timestamp.as_secs() as f64; + let micros = f64::from(timestamp.subsec_micros()) / 1_000_000.0; + values.float(seconds + micros) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/time/date.rs b/crates/elephc-eval/src/interpreter/builtins/time/date.rs new file mode 100644 index 0000000000..ba5636b052 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/time/date.rs @@ -0,0 +1,156 @@ +//! Purpose: +//! Implements PHP `date()` formatting for the eval-supported token subset. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` re-exports. +//! +//! Key details: +//! - Unix timestamps are converted through libc `localtime_r` before PHP date +//! tokens are expanded. + +use super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. +pub(in crate::interpreter) fn eval_builtin_date( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [format] => { + let format = eval_expr(format, context, scope, values)?; + eval_date_result(format, None, values) + } + [format, timestamp] => { + let format = eval_expr(format, context, scope, values)?; + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_date_result(format, Some(timestamp), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. +pub(in crate::interpreter) fn eval_date_result( + format: RuntimeCellHandle, + timestamp: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let format = values.string_bytes(format)?; + let timestamp = match timestamp { + Some(timestamp) => eval_int_value(timestamp, values)?, + None => eval_current_unix_timestamp()?, + }; + let tm = eval_localtime(timestamp)?; + let output = eval_format_date_bytes(&format, &tm, timestamp)?; + values.string_bytes_value(&output) +} + +/// Converts one Unix timestamp to local broken-down time through libc. +pub(in crate::interpreter) fn eval_localtime(timestamp: i64) -> Result { + let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; + let mut tm = MaybeUninit::::uninit(); + let result = unsafe { libc::localtime_r(&raw, tm.as_mut_ptr()) }; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) +} + +/// Applies PHP `date()` tokens to one local broken-down timestamp. +pub(in crate::interpreter) fn eval_format_date_bytes( + format: &[u8], + tm: &libc::tm, + timestamp: i64, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut escaped = false; + for byte in format { + if escaped { + output.push(*byte); + escaped = false; + continue; + } + if *byte == b'\\' { + escaped = true; + continue; + } + eval_push_date_token(&mut output, *byte, tm, timestamp)?; + } + if escaped { + output.push(b'\\'); + } + Ok(output) +} + +/// Appends the expansion for one PHP `date()` token, or the token literal. +pub(in crate::interpreter) fn eval_push_date_token( + output: &mut Vec, + token: u8, + tm: &libc::tm, + timestamp: i64, +) -> Result<(), EvalStatus> { + match token { + b'Y' => eval_push_padded_number(output, i64::from(tm.tm_year) + 1900, 4), + b'm' => eval_push_padded_number(output, i64::from(tm.tm_mon) + 1, 2), + b'd' => eval_push_padded_number(output, i64::from(tm.tm_mday), 2), + b'H' => eval_push_padded_number(output, i64::from(tm.tm_hour), 2), + b'i' => eval_push_padded_number(output, i64::from(tm.tm_min), 2), + b's' => eval_push_padded_number(output, i64::from(tm.tm_sec), 2), + b'l' => output.extend_from_slice(EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'F' => output.extend_from_slice(EVAL_MONTH_NAMES[eval_tm_month_index(tm)?].as_bytes()), + b'D' => output + .extend_from_slice(EVAL_WEEKDAY_SHORT_NAMES[eval_tm_weekday_index(tm)?].as_bytes()), + b'M' => { + output.extend_from_slice(EVAL_MONTH_SHORT_NAMES[eval_tm_month_index(tm)?].as_bytes()) + } + b'N' => { + let weekday = tm.tm_wday; + let iso_weekday = if weekday == 0 { 7 } else { weekday }; + output.extend_from_slice(iso_weekday.to_string().as_bytes()); + } + b'j' => output.extend_from_slice(tm.tm_mday.to_string().as_bytes()), + b'n' => output.extend_from_slice((tm.tm_mon + 1).to_string().as_bytes()), + b'G' => output.extend_from_slice(tm.tm_hour.to_string().as_bytes()), + b'g' => { + let hour = tm.tm_hour % 12; + let hour = if hour == 0 { 12 } else { hour }; + output.extend_from_slice(hour.to_string().as_bytes()); + } + b'A' => output.extend_from_slice(if tm.tm_hour < 12 { b"AM" } else { b"PM" }), + b'a' => output.extend_from_slice(if tm.tm_hour < 12 { b"am" } else { b"pm" }), + b'U' => output.extend_from_slice(timestamp.to_string().as_bytes()), + _ => output.push(token), + } + Ok(()) +} + +/// Returns a checked month index for PHP `date()` name tables. +pub(in crate::interpreter) fn eval_tm_month_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_mon).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_MONTH_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Returns a checked weekday index for PHP `date()` name tables. +pub(in crate::interpreter) fn eval_tm_weekday_index(tm: &libc::tm) -> Result { + let index = usize::try_from(tm.tm_wday).map_err(|_| EvalStatus::RuntimeFatal)?; + if index >= EVAL_WEEKDAY_NAMES.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(index) +} + +/// Appends one zero-padded decimal value with the requested minimum width. +pub(in crate::interpreter) fn eval_push_padded_number( + output: &mut Vec, + value: i64, + width: usize, +) { + output.extend_from_slice(format!("{value:0width$}").as_bytes()); +} diff --git a/crates/elephc-eval/src/interpreter/builtins/time/mktime.rs b/crates/elephc-eval/src/interpreter/builtins/time/mktime.rs new file mode 100644 index 0000000000..9dec237852 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/time/mktime.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Implements PHP `mktime()` conversion from local date components. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` re-exports. +//! +//! Key details: +//! - Component coercion checks libc integer bounds before calling `mktime`. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. +pub(in crate::interpreter) fn eval_builtin_mktime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hour, minute, second, month, day, year] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hour = eval_expr(hour, context, scope, values)?; + let minute = eval_expr(minute, context, scope, values)?; + let second = eval_expr(second, context, scope, values)?; + let month = eval_expr(month, context, scope, values)?; + let day = eval_expr(day, context, scope, values)?; + let year = eval_expr(year, context, scope, values)?; + eval_mktime_result(hour, minute, second, month, day, year, values) +} + +/// Converts PHP date components to a local Unix timestamp through libc `mktime`. +pub(in crate::interpreter) fn eval_mktime_result( + hour: RuntimeCellHandle, + minute: RuntimeCellHandle, + second: RuntimeCellHandle, + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_mktime_timestamp( + eval_int_cell_as_c_int(hour, values)?, + eval_int_cell_as_c_int(minute, values)?, + eval_int_cell_as_c_int(second, values)?, + eval_int_cell_as_c_int(month, values)?, + eval_int_cell_as_c_int(day, values)?, + eval_int_cell_as_c_int(year, values)?, + )?; + values.int(timestamp) +} + +/// Converts local date components into a Unix timestamp through libc `mktime`. +pub(in crate::interpreter) fn eval_mktime_timestamp( + hour: libc::c_int, + minute: libc::c_int, + second: libc::c_int, + month: libc::c_int, + day: libc::c_int, + year: libc::c_int, +) -> Result { + let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = second; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_year = year - 1900; + tm.tm_isdst = -1; + let timestamp = unsafe { libc::mktime(&mut tm) }; + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. +pub(in crate::interpreter) fn eval_int_cell_as_c_int( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + libc::c_int::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/time/mod.rs b/crates/elephc-eval/src/interpreter/builtins/time/mod.rs new file mode 100644 index 0000000000..137746f809 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/time/mod.rs @@ -0,0 +1,24 @@ +//! Purpose: +//! Groups eval implementations for PHP time, date, sleep, version, and uname +//! related builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - Time conversion helpers stay scoped to the eval interpreter and use libc for +//! local calendar conversions where PHP behavior depends on host locale/timezone. + +mod clock; +mod date; +mod mktime; +mod sleep; +mod strtotime; +mod system; + +pub(in crate::interpreter) use clock::*; +pub(in crate::interpreter) use date::*; +pub(in crate::interpreter) use mktime::*; +pub(in crate::interpreter) use sleep::*; +pub(in crate::interpreter) use strtotime::*; +pub(in crate::interpreter) use system::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/time/sleep.rs b/crates/elephc-eval/src/interpreter/builtins/time/sleep.rs new file mode 100644 index 0000000000..9e83253e0b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/time/sleep.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Implements PHP `sleep()` and `usleep()` eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` re-exports. +//! +//! Key details: +//! - Negative durations are rejected as runtime fatals; successful sleeps return +//! PHP-compatible values. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `sleep($seconds)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [seconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let seconds = eval_expr(seconds, context, scope, values)?; + eval_sleep_result(seconds, values) +} + +/// Sleeps for a non-negative number of seconds and returns PHP's remaining-seconds value. +pub(in crate::interpreter) fn eval_sleep_result( + seconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let seconds = eval_int_value(seconds, values)?; + let seconds = u64::try_from(seconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_secs(seconds)); + values.int(0) +} + +/// Evaluates PHP `usleep($microseconds)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_usleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [microseconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let microseconds = eval_expr(microseconds, context, scope, values)?; + eval_usleep_result(microseconds, values) +} + +/// Sleeps for a non-negative number of microseconds and returns PHP null. +pub(in crate::interpreter) fn eval_usleep_result( + microseconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let microseconds = eval_int_value(microseconds, values)?; + let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_micros(microseconds)); + values.null() +} diff --git a/crates/elephc-eval/src/interpreter/builtins/time/strtotime.rs b/crates/elephc-eval/src/interpreter/builtins/time/strtotime.rs new file mode 100644 index 0000000000..7334ad6ceb --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/time/strtotime.rs @@ -0,0 +1,132 @@ +//! Purpose: +//! Implements the eval-supported `strtotime()` date-string subset. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` re-exports. +//! +//! Key details: +//! - Supported strings are fixed-width ISO date/datetime forms normalized through +//! the same local `mktime` path as explicit date components. + +use super::super::super::*; +use super::*; + +/// Evaluates PHP `strtotime(datetime)` for eval's supported date-string subset. +pub(in crate::interpreter) fn eval_builtin_strtotime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [datetime] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let datetime = eval_expr(datetime, context, scope, values)?; + eval_strtotime_result(datetime, values) +} + +/// Parses one eval `strtotime()` input and boxes the resulting timestamp. +pub(in crate::interpreter) fn eval_strtotime_result( + datetime: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(datetime)?; + let timestamp = eval_strtotime_bytes(&bytes)?; + values.int(timestamp) +} + +/// Parses eval's supported `strtotime()` strings into local Unix timestamps. +pub(in crate::interpreter) fn eval_strtotime_bytes(bytes: &[u8]) -> Result { + let bytes = eval_trim_ascii_whitespace(bytes); + if bytes.eq_ignore_ascii_case(b"now") { + return eval_current_unix_timestamp(); + } + let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { + return Ok(-1); + }; + eval_mktime_timestamp(hour, minute, second, month, day, year) +} + +/// Trims ASCII whitespace from both ends of one byte slice. +pub(in crate::interpreter) fn eval_trim_ascii_whitespace(bytes: &[u8]) -> &[u8] { + let mut start = 0; + let mut end = bytes.len(); + while start < end && bytes[start].is_ascii_whitespace() { + start += 1; + } + while end > start && bytes[end - 1].is_ascii_whitespace() { + end -= 1; + } + &bytes[start..end] +} + +/// Parses fixed-width ISO date and datetime forms supported by eval `strtotime()`. +pub(in crate::interpreter) fn eval_parse_iso_datetime( + bytes: &[u8], +) -> Option<( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, +)> { + if bytes.len() != 10 && bytes.len() != 16 && bytes.len() != 19 { + return None; + } + if bytes.get(4) != Some(&b'-') || bytes.get(7) != Some(&b'-') { + return None; + } + let year = eval_parse_fixed_digits(bytes, 0, 4)?; + let month = eval_parse_fixed_digits(bytes, 5, 2)?; + let day = eval_parse_fixed_digits(bytes, 8, 2)?; + let (hour, minute, second) = if bytes.len() == 10 { + (0, 0, 0) + } else { + if !matches!(bytes.get(10), Some(b' ') | Some(b'T') | Some(b't')) { + return None; + } + if bytes.get(13) != Some(&b':') { + return None; + } + let hour = eval_parse_fixed_digits(bytes, 11, 2)?; + let minute = eval_parse_fixed_digits(bytes, 14, 2)?; + let second = if bytes.len() == 19 { + if bytes.get(16) != Some(&b':') { + return None; + } + eval_parse_fixed_digits(bytes, 17, 2)? + } else { + 0 + }; + (hour, minute, second) + }; + if !(1..=12).contains(&month) + || !(1..=31).contains(&day) + || !(0..=23).contains(&hour) + || !(0..=59).contains(&minute) + || !(0..=59).contains(&second) + { + return None; + } + Some((year, month, day, hour, minute, second)) +} + +/// Parses a fixed-width decimal field as a libc-compatible integer. +pub(in crate::interpreter) fn eval_parse_fixed_digits( + bytes: &[u8], + start: usize, + len: usize, +) -> Option { + let end = start.checked_add(len)?; + let field = bytes.get(start..end)?; + let mut value: libc::c_int = 0; + for byte in field { + if !byte.is_ascii_digit() { + return None; + } + value = value.checked_mul(10)?; + value = value.checked_add(libc::c_int::from(byte - b'0'))?; + } + Some(value) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/time/system.rs b/crates/elephc-eval/src/interpreter/builtins/time/system.rs new file mode 100644 index 0000000000..be3f3f24af --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/time/system.rs @@ -0,0 +1,131 @@ +//! Purpose: +//! Implements version and uname-related system builtins currently grouped with +//! time/system eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` re-exports. +//! +//! Key details: +//! - `phpversion()` reads the workspace package version at compile time and +//! `php_uname()` formats libc `uname` fields. + +use super::super::super::*; + +/// Evaluates PHP `phpversion()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_phpversion( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values) +} + +/// Returns the root elephc package version as a boxed PHP string. +pub(in crate::interpreter) fn eval_phpversion_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(eval_compiler_php_version()) +} + +/// Reads the root package version from the workspace manifest used by native `phpversion()`. +pub(in crate::interpreter) fn eval_compiler_php_version() -> &'static str { + let mut in_package = false; + for line in EVAL_ROOT_CARGO_TOML.lines() { + let line = line.trim(); + if line == "[package]" { + in_package = true; + continue; + } + if in_package && line.starts_with('[') { + break; + } + if in_package { + if let Some(value) = line.strip_prefix("version = ") { + return value.trim_matches('"'); + } + } + } + env!("CARGO_PKG_VERSION") +} + +/// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. +pub(in crate::interpreter) fn eval_builtin_php_uname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_php_uname_result(None, values), + [mode] => { + let mode = eval_expr(mode, context, scope, values)?; + eval_php_uname_result(Some(mode), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads the local uname fields and formats the PHP `php_uname()` mode result. +pub(in crate::interpreter) fn eval_php_uname_result( + mode: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => { + let bytes = values.string_bytes(mode)?; + let [mode] = bytes.as_slice() else { + return Err(EvalStatus::RuntimeFatal); + }; + *mode + } + None => b'a', + }; + + let mut utsname = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes all uname fields into the stack-owned utsname buffer. + libc::uname(utsname.as_mut_ptr()) + }; + if status != 0 { + return values.string(""); + } + let utsname = unsafe { + // `uname` succeeded, so libc initialized the full `utsname` structure. + utsname.assume_init() + }; + let sysname = eval_uname_field_bytes(&utsname.sysname); + let nodename = eval_uname_field_bytes(&utsname.nodename); + let release = eval_uname_field_bytes(&utsname.release); + let version = eval_uname_field_bytes(&utsname.version); + let machine = eval_uname_field_bytes(&utsname.machine); + + match mode { + b'a' => { + let mut output = Vec::new(); + for field in [&sysname, &nodename, &release, &version, &machine] { + if !output.is_empty() { + output.push(b' '); + } + output.extend_from_slice(field); + } + values.string_bytes_value(&output) + } + b's' => values.string_bytes_value(&sysname), + b'n' => values.string_bytes_value(&nodename), + b'r' => values.string_bytes_value(&release), + b'v' => values.string_bytes_value(&version), + b'm' => values.string_bytes_value(&machine), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies one NUL-terminated `utsname` field into raw PHP string bytes. +pub(in crate::interpreter) fn eval_uname_field_bytes(field: &[libc::c_char]) -> Vec { + let length = field + .iter() + .position(|byte| *byte == 0) + .unwrap_or(field.len()); + field[..length].iter().map(|byte| *byte as u8).collect() +} From 1f0cbb212c8d1ecdc2c19ad1538abc476afb97e5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:44:32 +0200 Subject: [PATCH 0292/1208] Split eval network and environment builtins --- .../src/interpreter/builtins/network_env.rs | 572 ------------------ .../interpreter/builtins/network_env/cache.rs | 65 ++ .../interpreter/builtins/network_env/env.rs | 69 +++ .../interpreter/builtins/network_env/hosts.rs | 128 ++++ .../interpreter/builtins/network_env/ip.rs | 158 +++++ .../interpreter/builtins/network_env/mod.rs | 22 + .../builtins/network_env/protocols.rs | 198 ++++++ 7 files changed, 640 insertions(+), 572 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env/cache.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env/env.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env/hosts.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env/ip.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env/protocols.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env.rs b/crates/elephc-eval/src/interpreter/builtins/network_env.rs deleted file mode 100644 index 123d47307e..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/network_env.rs +++ /dev/null @@ -1,572 +0,0 @@ -//! Purpose: -//! Network lookup, IP conversion, environment, and realpath-cache builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins` re-exports used by core call dispatch. -//! -//! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime -//! behavior through `RuntimeValueOps`. - -use super::super::*; -use super::*; - -/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_gethostbyaddr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_gethostbyaddr_result(ip, values) -} - -/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. -pub(in crate::interpreter) fn eval_gethostbyaddr_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let ip_bytes = values.string_bytes(ip)?; - let ip_text = String::from_utf8_lossy(&ip_bytes); - let Ok(ipv4) = ip_text.parse::() else { - return values.bool_value(false); - }; - let octets = ipv4.octets(); - let resolved = unsafe { - // libc reads the stack-owned IPv4 octets during this call and returns - // static resolver storage, which is copied before the next resolver call. - let host = libc_gethostbyaddr( - octets.as_ptr().cast::(), - octets.len() as libc::socklen_t, - libc::AF_INET, - ); - if host.is_null() || (*host).h_name.is_null() { - None - } else { - Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) - } - }; - match resolved { - Some(name) if !name.is_empty() => values.string_bytes_value(&name), - _ => values.string(ip_text.as_ref()), - } -} - -/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_gethostbyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hostname] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hostname = eval_expr(hostname, context, scope, values)?; - eval_gethostbyname_result(hostname, values) -} - -/// Resolves one host name to an IPv4 string, or returns the original input on failure. -pub(in crate::interpreter) fn eval_gethostbyname_result( - hostname: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let hostname = values.string_bytes(hostname)?; - let hostname = String::from_utf8_lossy(&hostname); - if hostname.parse::().is_ok() { - return values.string(hostname.as_ref()); - } - let resolved = (hostname.as_ref(), 0_u16) - .to_socket_addrs() - .ok() - .and_then(|addrs| { - addrs - .filter_map(|addr| match addr.ip() { - std::net::IpAddr::V4(ip) => Some(ip.to_string()), - std::net::IpAddr::V6(_) => None, - }) - .next() - }); - values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) -} - -/// Evaluates PHP `gethostname()` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_gethostname( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - let [] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostname_result(values) -} - -/// Reads the current host name through libc and returns an empty string on failure. -pub(in crate::interpreter) fn eval_gethostname_result( - values: &mut impl RuntimeValueOps, -) -> Result { - let mut buffer = [0 as libc::c_char; 256]; - let status = unsafe { - // libc writes at most buffer.len() bytes into this stack buffer. - libc::gethostname(buffer.as_mut_ptr(), buffer.len()) - }; - if status != 0 { - return values.string(""); - } - let length = buffer - .iter() - .position(|byte| *byte == 0) - .unwrap_or(buffer.len()); - let hostname = buffer[..length] - .iter() - .map(|byte| *byte as u8) - .collect::>(); - values.string_bytes_value(&hostname) -} - -/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_getprotobyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getprotobyname_result(protocol, values) -} - -/// Looks up an IP protocol number by name or alias. -pub(in crate::interpreter) fn eval_getprotobyname_result( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global protoent; copy scalar fields before another lookup. - libc_getprotobyname(protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); - } - let number = unsafe { (*entry).p_proto }; - values.int(i64::from(number)) -} - -/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_getprotobynumber( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getprotobynumber_result(protocol, values) -} - -/// Looks up an IP protocol name by numeric protocol id. -pub(in crate::interpreter) fn eval_getprotobynumber_result( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let protocol = eval_int_value(protocol, values)?; - let Ok(protocol) = libc::c_int::try_from(protocol) else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global protoent; copy the name before another lookup. - libc_getprotobynumber(protocol) - }; - eval_protoent_name_or_false(entry, values) -} - -/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_getservbyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [service, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let service = eval_expr(service, context, scope, values)?; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getservbyname_result(service, protocol, values) -} - -/// Looks up an internet service port by service name and protocol. -pub(in crate::interpreter) fn eval_getservbyname_result( - service: RuntimeCellHandle, - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(service) = eval_lowercase_c_string(service, values)? else { - return values.bool_value(false); - }; - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global servent; copy scalar fields before another lookup. - libc_getservbyname(service.as_ptr(), protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); - } - let port = unsafe { u16::from_be((*entry).s_port as u16) }; - values.int(i64::from(port)) -} - -/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_getservbyport( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [port, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let port = eval_expr(port, context, scope, values)?; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getservbyport_result(port, protocol, values) -} - -/// Looks up an internet service name by port and protocol. -pub(in crate::interpreter) fn eval_getservbyport_result( - port: RuntimeCellHandle, - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let port = eval_int_value(port, values)?; - let Ok(port) = u16::try_from(port) else { - return values.bool_value(false); - }; - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let network_port = port.to_be() as libc::c_int; - let entry = unsafe { - // libc returns a process-global servent; copy the name before another lookup. - libc_getservbyport(network_port, protocol.as_ptr()) - }; - eval_servent_name_or_false(entry, values) -} - -/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. -pub(in crate::interpreter) fn eval_lowercase_c_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let bytes = values.string_bytes(value)?; - let bytes = bytes - .into_iter() - .map(|byte| byte.to_ascii_lowercase()) - .collect::>(); - Ok(CString::new(bytes).ok()) -} - -/// Copies a protoent canonical name into a PHP string or returns PHP false. -pub(in crate::interpreter) fn eval_protoent_name_or_false( - entry: *mut libc::protoent, - values: &mut impl RuntimeValueOps, -) -> Result { - if entry.is_null() { - return values.bool_value(false); - } - let name = unsafe { - let name = (*entry).p_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) -} - -/// Copies a servent canonical name into a PHP string or returns PHP false. -pub(in crate::interpreter) fn eval_servent_name_or_false( - entry: *mut libc::servent, - values: &mut impl RuntimeValueOps, -) -> Result { - if entry.is_null() { - return values.bool_value(false); - } - let name = unsafe { - let name = (*entry).s_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) -} - -/// Evaluates PHP `long2ip($ip)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_long2ip( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_long2ip_result(ip, values) -} - -/// Formats one 32-bit IPv4 integer as a dotted-quad string. -pub(in crate::interpreter) fn eval_long2ip_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let ip = eval_int_value(ip, values)? as u32; - values.string(&eval_format_ipv4(ip)) -} - -/// Evaluates PHP `ip2long($ip)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_ip2long( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_ip2long_result(ip, values) -} - -/// Parses a dotted-quad IPv4 string into an integer or PHP false. -pub(in crate::interpreter) fn eval_ip2long_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(ip)?; - match eval_parse_ipv4(&bytes) { - Some(ip) => values.int(i64::from(ip)), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `inet_pton($ip)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_inet_pton( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_inet_pton_result(ip, values) -} - -/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. -pub(in crate::interpreter) fn eval_inet_pton_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(ip)?; - let Some(ip) = eval_parse_ipv4(&bytes) else { - return values.bool_value(false); - }; - values.string_bytes_value(&ip.to_be_bytes()) -} - -/// Evaluates PHP `inet_ntop($binary)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_inet_ntop( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [binary] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let binary = eval_expr(binary, context, scope, values)?; - eval_inet_ntop_result(binary, values) -} - -/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. -pub(in crate::interpreter) fn eval_inet_ntop_result( - binary: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(binary)?; - let [a, b, c, d] = bytes.as_slice() else { - return values.bool_value(false); - }; - let ip = u32::from_be_bytes([*a, *b, *c, *d]); - values.string(&eval_format_ipv4(ip)) -} - -/// Parses exactly four decimal IPv4 octets separated by dots. -pub(in crate::interpreter) fn eval_parse_ipv4(bytes: &[u8]) -> Option { - let mut octets = [0_u8; 4]; - let mut position = 0_usize; - let mut index = 0_usize; - - while index < 4 { - if position >= bytes.len() { - return None; - } - let start = position; - let mut value = 0_u16; - while position < bytes.len() && bytes[position].is_ascii_digit() { - value = value - .checked_mul(10)? - .checked_add(u16::from(bytes[position] - b'0'))?; - position += 1; - if position - start > 3 || value > 255 { - return None; - } - } - if position == start { - return None; - } - octets[index] = value as u8; - index += 1; - if index == 4 { - return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); - } - if bytes.get(position).copied() != Some(b'.') { - return None; - } - position += 1; - } - None -} - -/// Formats one packed IPv4 integer into dotted-quad text. -pub(in crate::interpreter) fn eval_format_ipv4(ip: u32) -> String { - let [a, b, c, d] = ip.to_be_bytes(); - format!("{}.{}.{}.{}", a, b, c, d) -} - -/// Evaluates PHP `getenv($name)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_getenv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - eval_getenv_result(name, values) -} - -/// Reads one environment variable and returns an empty string when it is unset. -pub(in crate::interpreter) fn eval_getenv_result( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8_lossy(&name); - let value = std::env::var_os(name.as_ref()) - .map(|value| value.to_string_lossy().into_owned()) - .unwrap_or_default(); - values.string(&value) -} - -/// Evaluates PHP `putenv($assignment)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_putenv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [assignment] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let assignment = eval_expr(assignment, context, scope, values)?; - eval_putenv_result(assignment, values) -} - -/// Applies one `putenv()` assignment to the host environment. -pub(in crate::interpreter) fn eval_putenv_result( - assignment: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let assignment = values.string_bytes(assignment)?; - if let Some(separator) = assignment.iter().position(|byte| *byte == b'=') { - let name = String::from_utf8_lossy(&assignment[..separator]); - let value = String::from_utf8_lossy(&assignment[separator + 1..]); - std::env::set_var(name.as_ref(), value.as_ref()); - } else { - let name = String::from_utf8_lossy(&assignment); - std::env::remove_var(name.as_ref()); - } - values.bool_value(true) -} - -/// Evaluates PHP `sys_get_temp_dir()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_sys_get_temp_dir( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_sys_get_temp_dir_result(values) -} - -/// Returns the same temporary directory literal as the native static builtin. -pub(in crate::interpreter) fn eval_sys_get_temp_dir_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.string("/tmp") -} - -/// Evaluates PHP `realpath_cache_get()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_realpath_cache_get( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_get_result(values) -} - -/// Returns elephc's intentionally empty realpath-cache view. -pub(in crate::interpreter) fn eval_realpath_cache_get_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.array_new(0) -} - -/// Evaluates PHP `realpath_cache_size()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_realpath_cache_size( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_size_result(values) -} - -/// Returns zero because elephc does not maintain a runtime realpath cache. -pub(in crate::interpreter) fn eval_realpath_cache_size_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.int(0) -} diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/cache.rs b/crates/elephc-eval/src/interpreter/builtins/network_env/cache.rs new file mode 100644 index 0000000000..34cf4c1a37 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/network_env/cache.rs @@ -0,0 +1,65 @@ +//! Purpose: +//! Implements temp-dir and intentionally empty realpath-cache eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` re-exports. +//! +//! Key details: +//! - The eval runtime does not maintain a PHP realpath cache, so cache probes +//! return stable empty/zero values. + +use super::super::super::*; + +/// Evaluates PHP `sys_get_temp_dir()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_sys_get_temp_dir( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values) +} + +/// Returns the same temporary directory literal as the native static builtin. +pub(in crate::interpreter) fn eval_sys_get_temp_dir_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string("/tmp") +} + +/// Evaluates PHP `realpath_cache_get()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_realpath_cache_get( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values) +} + +/// Returns elephc's intentionally empty realpath-cache view. +pub(in crate::interpreter) fn eval_realpath_cache_get_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.array_new(0) +} + +/// Evaluates PHP `realpath_cache_size()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_realpath_cache_size( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values) +} + +/// Returns zero because elephc does not maintain a runtime realpath cache. +pub(in crate::interpreter) fn eval_realpath_cache_size_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(0) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/env.rs b/crates/elephc-eval/src/interpreter/builtins/network_env/env.rs new file mode 100644 index 0000000000..c29afeac11 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/network_env/env.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Implements environment variable eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` re-exports. +//! +//! Key details: +//! - `getenv` returns an empty string for unset variables and `putenv` mutates the +//! host process environment. + +use super::super::super::*; + +/// Evaluates PHP `getenv($name)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + eval_getenv_result(name, values) +} + +/// Reads one environment variable and returns an empty string when it is unset. +pub(in crate::interpreter) fn eval_getenv_result( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8_lossy(&name); + let value = std::env::var_os(name.as_ref()) + .map(|value| value.to_string_lossy().into_owned()) + .unwrap_or_default(); + values.string(&value) +} + +/// Evaluates PHP `putenv($assignment)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_putenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [assignment] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let assignment = eval_expr(assignment, context, scope, values)?; + eval_putenv_result(assignment, values) +} + +/// Applies one `putenv()` assignment to the host environment. +pub(in crate::interpreter) fn eval_putenv_result( + assignment: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let assignment = values.string_bytes(assignment)?; + if let Some(separator) = assignment.iter().position(|byte| *byte == b'=') { + let name = String::from_utf8_lossy(&assignment[..separator]); + let value = String::from_utf8_lossy(&assignment[separator + 1..]); + std::env::set_var(name.as_ref(), value.as_ref()); + } else { + let name = String::from_utf8_lossy(&assignment); + std::env::remove_var(name.as_ref()); + } + values.bool_value(true) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/hosts.rs b/crates/elephc-eval/src/interpreter/builtins/network_env/hosts.rs new file mode 100644 index 0000000000..6b82a25b9e --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/network_env/hosts.rs @@ -0,0 +1,128 @@ +//! Purpose: +//! Implements host-name lookup eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` re-exports. +//! +//! Key details: +//! - Host resolver results from libc are copied immediately because they point at +//! process-global static storage. + +use super::super::super::*; + +/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostbyaddr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_gethostbyaddr_result(ip, values) +} + +/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. +pub(in crate::interpreter) fn eval_gethostbyaddr_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip_bytes = values.string_bytes(ip)?; + let ip_text = String::from_utf8_lossy(&ip_bytes); + let Ok(ipv4) = ip_text.parse::() else { + return values.bool_value(false); + }; + let octets = ipv4.octets(); + let resolved = unsafe { + // libc reads the stack-owned IPv4 octets during this call and returns + // static resolver storage, which is copied before the next resolver call. + let host = libc_gethostbyaddr( + octets.as_ptr().cast::(), + octets.len() as libc::socklen_t, + libc::AF_INET, + ); + if host.is_null() || (*host).h_name.is_null() { + None + } else { + Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) + } + }; + match resolved { + Some(name) if !name.is_empty() => values.string_bytes_value(&name), + _ => values.string(ip_text.as_ref()), + } +} + +/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hostname] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hostname = eval_expr(hostname, context, scope, values)?; + eval_gethostbyname_result(hostname, values) +} + +/// Resolves one host name to an IPv4 string, or returns the original input on failure. +pub(in crate::interpreter) fn eval_gethostbyname_result( + hostname: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let hostname = values.string_bytes(hostname)?; + let hostname = String::from_utf8_lossy(&hostname); + if hostname.parse::().is_ok() { + return values.string(hostname.as_ref()); + } + let resolved = (hostname.as_ref(), 0_u16) + .to_socket_addrs() + .ok() + .and_then(|addrs| { + addrs + .filter_map(|addr| match addr.ip() { + std::net::IpAddr::V4(ip) => Some(ip.to_string()), + std::net::IpAddr::V6(_) => None, + }) + .next() + }); + values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) +} + +/// Evaluates PHP `gethostname()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostname( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + let [] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostname_result(values) +} + +/// Reads the current host name through libc and returns an empty string on failure. +pub(in crate::interpreter) fn eval_gethostname_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let mut buffer = [0 as libc::c_char; 256]; + let status = unsafe { + // libc writes at most buffer.len() bytes into this stack buffer. + libc::gethostname(buffer.as_mut_ptr(), buffer.len()) + }; + if status != 0 { + return values.string(""); + } + let length = buffer + .iter() + .position(|byte| *byte == 0) + .unwrap_or(buffer.len()); + let hostname = buffer[..length] + .iter() + .map(|byte| *byte as u8) + .collect::>(); + values.string_bytes_value(&hostname) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/ip.rs b/crates/elephc-eval/src/interpreter/builtins/network_env/ip.rs new file mode 100644 index 0000000000..48cf864232 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/network_env/ip.rs @@ -0,0 +1,158 @@ +//! Purpose: +//! Implements IPv4 conversion eval builtins such as `ip2long`, `long2ip`, +//! `inet_pton`, and `inet_ntop`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` re-exports. +//! +//! Key details: +//! - The supported eval subset is IPv4-only and returns PHP false for malformed +//! addresses or binary payloads. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `long2ip($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_long2ip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_long2ip_result(ip, values) +} + +/// Formats one 32-bit IPv4 integer as a dotted-quad string. +pub(in crate::interpreter) fn eval_long2ip_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip = eval_int_value(ip, values)? as u32; + values.string(&eval_format_ipv4(ip)) +} + +/// Evaluates PHP `ip2long($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ip2long( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_ip2long_result(ip, values) +} + +/// Parses a dotted-quad IPv4 string into an integer or PHP false. +pub(in crate::interpreter) fn eval_ip2long_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + match eval_parse_ipv4(&bytes) { + Some(ip) => values.int(i64::from(ip)), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `inet_pton($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_inet_pton( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_inet_pton_result(ip, values) +} + +/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. +pub(in crate::interpreter) fn eval_inet_pton_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + let Some(ip) = eval_parse_ipv4(&bytes) else { + return values.bool_value(false); + }; + values.string_bytes_value(&ip.to_be_bytes()) +} + +/// Evaluates PHP `inet_ntop($binary)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_inet_ntop( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [binary] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let binary = eval_expr(binary, context, scope, values)?; + eval_inet_ntop_result(binary, values) +} + +/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. +pub(in crate::interpreter) fn eval_inet_ntop_result( + binary: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(binary)?; + let [a, b, c, d] = bytes.as_slice() else { + return values.bool_value(false); + }; + let ip = u32::from_be_bytes([*a, *b, *c, *d]); + values.string(&eval_format_ipv4(ip)) +} + +/// Parses exactly four decimal IPv4 octets separated by dots. +pub(in crate::interpreter) fn eval_parse_ipv4(bytes: &[u8]) -> Option { + let mut octets = [0_u8; 4]; + let mut position = 0_usize; + let mut index = 0_usize; + + while index < 4 { + if position >= bytes.len() { + return None; + } + let start = position; + let mut value = 0_u16; + while position < bytes.len() && bytes[position].is_ascii_digit() { + value = value + .checked_mul(10)? + .checked_add(u16::from(bytes[position] - b'0'))?; + position += 1; + if position - start > 3 || value > 255 { + return None; + } + } + if position == start { + return None; + } + octets[index] = value as u8; + index += 1; + if index == 4 { + return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); + } + if bytes.get(position).copied() != Some(b'.') { + return None; + } + position += 1; + } + None +} + +/// Formats one packed IPv4 integer into dotted-quad text. +pub(in crate::interpreter) fn eval_format_ipv4(ip: u32) -> String { + let [a, b, c, d] = ip.to_be_bytes(); + format!("{}.{}.{}.{}", a, b, c, d) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs b/crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs new file mode 100644 index 0000000000..b03aa8cac0 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Groups network lookup, IP conversion, environment, and realpath-cache eval +//! builtins by focused runtime domain. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by core call dispatch. +//! +//! Key details: +//! - libc lookup results are copied before subsequent lookups can overwrite +//! process-global resolver storage. + +mod cache; +mod env; +mod hosts; +mod ip; +mod protocols; + +pub(in crate::interpreter) use cache::*; +pub(in crate::interpreter) use env::*; +pub(in crate::interpreter) use hosts::*; +pub(in crate::interpreter) use ip::*; +pub(in crate::interpreter) use protocols::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/protocols.rs b/crates/elephc-eval/src/interpreter/builtins/network_env/protocols.rs new file mode 100644 index 0000000000..ed0ff691dc --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/network_env/protocols.rs @@ -0,0 +1,198 @@ +//! Purpose: +//! Implements protocol and service database lookup eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` re-exports. +//! +//! Key details: +//! - PHP values are converted to lowercase NUL-free C strings before libc lookup +//! and returned database names are copied into runtime strings. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getprotobyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobyname_result(protocol, values) +} + +/// Looks up an IP protocol number by name or alias. +pub(in crate::interpreter) fn eval_getprotobyname_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy scalar fields before another lookup. + libc_getprotobyname(protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let number = unsafe { (*entry).p_proto }; + values.int(i64::from(number)) +} + +/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getprotobynumber( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobynumber_result(protocol, values) +} + +/// Looks up an IP protocol name by numeric protocol id. +pub(in crate::interpreter) fn eval_getprotobynumber_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = eval_int_value(protocol, values)?; + let Ok(protocol) = libc::c_int::try_from(protocol) else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy the name before another lookup. + libc_getprotobynumber(protocol) + }; + eval_protoent_name_or_false(entry, values) +} + +/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_getservbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [service, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let service = eval_expr(service, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyname_result(service, protocol, values) +} + +/// Looks up an internet service port by service name and protocol. +pub(in crate::interpreter) fn eval_getservbyname_result( + service: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(service) = eval_lowercase_c_string(service, values)? else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global servent; copy scalar fields before another lookup. + libc_getservbyname(service.as_ptr(), protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let port = unsafe { u16::from_be((*entry).s_port as u16) }; + values.int(i64::from(port)) +} + +/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_getservbyport( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [port, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let port = eval_expr(port, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyport_result(port, protocol, values) +} + +/// Looks up an internet service name by port and protocol. +pub(in crate::interpreter) fn eval_getservbyport_result( + port: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let port = eval_int_value(port, values)?; + let Ok(port) = u16::try_from(port) else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let network_port = port.to_be() as libc::c_int; + let entry = unsafe { + // libc returns a process-global servent; copy the name before another lookup. + libc_getservbyport(network_port, protocol.as_ptr()) + }; + eval_servent_name_or_false(entry, values) +} + +/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. +pub(in crate::interpreter) fn eval_lowercase_c_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let bytes = values.string_bytes(value)?; + let bytes = bytes + .into_iter() + .map(|byte| byte.to_ascii_lowercase()) + .collect::>(); + Ok(CString::new(bytes).ok()) +} + +/// Copies a protoent canonical name into a PHP string or returns PHP false. +pub(in crate::interpreter) fn eval_protoent_name_or_false( + entry: *mut libc::protoent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).p_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} + +/// Copies a servent canonical name into a PHP string or returns PHP false. +pub(in crate::interpreter) fn eval_servent_name_or_false( + entry: *mut libc::servent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).s_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} From c30fbc75bf6dc6be1cd8d91d5d651543d1bc864c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:46:22 +0200 Subject: [PATCH 0293/1208] Split eval filesystem ops builtins --- .../interpreter/builtins/filesystem/ops.rs | 599 ------------------ .../builtins/filesystem/ops/disk.rs | 60 ++ .../builtins/filesystem/ops/glob.rs | 136 ++++ .../builtins/filesystem/ops/links.rs | 104 +++ .../builtins/filesystem/ops/listing.rs | 60 ++ .../builtins/filesystem/ops/mod.rs | 27 + .../builtins/filesystem/ops/path_bool.rs | 106 ++++ .../builtins/filesystem/ops/tempnam.rs | 65 ++ .../builtins/filesystem/ops/touch.rs | 105 +++ .../builtins/filesystem/ops/umask.rs | 47 ++ 10 files changed, 710 insertions(+), 599 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/disk.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/glob.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/links.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/listing.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/tempnam.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/touch.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/ops/umask.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops.rs deleted file mode 100644 index 42c649db89..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops.rs +++ /dev/null @@ -1,599 +0,0 @@ -//! Purpose: -//! Directory, chmod, glob, tempnam, touch, umask, link, clearstatcache, and unlink builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem` re-exports. -//! -//! Key details: -//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; -use super::*; - -/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. -pub(in crate::interpreter) fn eval_builtin_disk_space( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_disk_space_result(name, directory, values) -} - -/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. -pub(in crate::interpreter) fn eval_disk_space_result( - name: &str, - directory: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(directory)?; - let Ok(path) = CString::new(bytes) else { - return values.float(0.0); - }; - let mut stats = std::mem::MaybeUninit::::zeroed(); - let status = unsafe { - // libc writes the statvfs fields for this NUL-terminated local path. - libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) - }; - if status != 0 { - return values.float(0.0); - } - let stats = unsafe { - // `statvfs` succeeded, so libc initialized the full stat buffer. - stats.assume_init() - }; - let block_size = if stats.f_frsize > 0 { - stats.f_frsize - } else { - stats.f_bsize - }; - let blocks = match name { - "disk_free_space" => stats.f_bavail, - "disk_total_space" => stats.f_blocks, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.float((block_size as f64) * (blocks as f64)) -} - -/// Evaluates a one-path filesystem operation that returns a PHP boolean. -pub(in crate::interpreter) fn eval_builtin_unary_path_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_unary_path_bool_result(name, path, values) -} - -/// Executes a one-path local filesystem operation and returns whether it succeeded. -pub(in crate::interpreter) fn eval_unary_path_bool_result( - name: &str, - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - let ok = match name { - "chdir" => std::env::set_current_dir(path).is_ok(), - "mkdir" => std::fs::create_dir(path).is_ok(), - "rmdir" => std::fs::remove_dir(path).is_ok(), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Evaluates a two-path filesystem operation that returns a PHP boolean. -pub(in crate::interpreter) fn eval_builtin_binary_path_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [from, to] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let from = eval_expr(from, context, scope, values)?; - let to = eval_expr(to, context, scope, values)?; - eval_binary_path_bool_result(name, from, to, values) -} - -/// Executes a two-path local filesystem operation and returns whether it succeeded. -pub(in crate::interpreter) fn eval_binary_path_bool_result( - name: &str, - from: RuntimeCellHandle, - to: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let from = eval_path_string(from, values)?; - let to = eval_path_string(to, values)?; - let ok = match name { - "copy" => std::fs::copy(from, to).is_ok(), - "link" => std::fs::hard_link(from, to).is_ok(), - "rename" => std::fs::rename(from, to).is_ok(), - "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_chmod( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, permissions] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let permissions = eval_expr(permissions, context, scope, values)?; - eval_chmod_result(filename, permissions, values) -} - -/// Changes one local file's mode and returns whether the operation succeeded. -pub(in crate::interpreter) fn eval_chmod_result( - filename: RuntimeCellHandle, - permissions: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let mode = eval_int_value(permissions, values)? as u32; - let permissions = std::fs::Permissions::from_mode(mode); - values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) -} - -/// Evaluates PHP `scandir($directory)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_scandir( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_scandir_result(directory, values) -} - -/// Lists one local directory into an indexed string array, or an empty array on failure. -pub(in crate::interpreter) fn eval_scandir_result( - directory: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(directory, values)?; - let Ok(entries) = std::fs::read_dir(path) else { - return values.array_new(0); - }; - let mut names = vec![".".to_string(), "..".to_string()]; - for entry in entries { - let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; - names.push(entry.file_name().to_string_lossy().into_owned()); - } - names.sort(); - let mut result = values.array_new(names.len())?; - for (index, name) in names.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; - } - Ok(result) -} - -/// Evaluates PHP `glob($pattern)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_glob( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - eval_glob_result(pattern, values) -} - -/// Expands one local glob pattern into a sorted indexed PHP string array. -pub(in crate::interpreter) fn eval_glob_result( - pattern: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = eval_path_string(pattern, values)?; - let matches = eval_glob_matches(&pattern); - let mut result = values.array_new(matches.len())?; - for (index, path) in matches.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; - } - Ok(result) -} - -/// Collects sorted matches for one local glob pattern. -pub(in crate::interpreter) fn eval_glob_matches(pattern: &str) -> Vec { - if pattern.is_empty() { - return Vec::new(); - } - if !eval_glob_component_has_magic(pattern) { - return std::path::Path::new(pattern) - .exists() - .then(|| pattern.to_string()) - .into_iter() - .collect(); - } - let absolute = pattern.starts_with('/'); - let components: Vec<&str> = pattern - .split('/') - .filter(|component| !component.is_empty()) - .collect(); - let mut matches = Vec::new(); - let base = if absolute { - std::path::PathBuf::from("/") - } else { - std::path::PathBuf::from(".") - }; - let prefix = if absolute { "/" } else { "" }; - eval_glob_collect(&base, prefix, &components, &mut matches); - matches.sort(); - matches -} - -/// Recursively expands one glob path component at a time. -pub(in crate::interpreter) fn eval_glob_collect( - base: &std::path::Path, - prefix: &str, - components: &[&str], - matches: &mut Vec, -) { - let Some((component, rest)) = components.split_first() else { - if base.exists() && !prefix.is_empty() { - matches.push(prefix.to_string()); - } - return; - }; - if !eval_glob_component_has_magic(component) { - let next_base = base.join(component); - if rest.is_empty() { - if next_base.exists() { - matches.push(eval_glob_join_output(prefix, component)); - } - } else if next_base.is_dir() { - let next_prefix = eval_glob_join_output(prefix, component); - eval_glob_collect(&next_base, &next_prefix, rest, matches); - } - return; - } - let Ok(entries) = std::fs::read_dir(base) else { - return; - }; - let mut names = Vec::new(); - for entry in entries.flatten() { - names.push(entry.file_name().to_string_lossy().into_owned()); - } - names.sort(); - for name in names { - if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { - continue; - } - let next_base = base.join(&name); - if rest.is_empty() { - matches.push(eval_glob_join_output(prefix, &name)); - } else if next_base.is_dir() { - let next_prefix = eval_glob_join_output(prefix, &name); - eval_glob_collect(&next_base, &next_prefix, rest, matches); - } - } -} - -/// Joins a display path prefix and component while preserving absolute-root output. -pub(in crate::interpreter) fn eval_glob_join_output(prefix: &str, component: &str) -> String { - if prefix.is_empty() { - component.to_string() - } else if prefix == "/" { - format!("/{component}") - } else { - format!("{prefix}/{component}") - } -} - -/// Returns whether a glob component contains wildcard syntax. -pub(in crate::interpreter) fn eval_glob_component_has_magic(component: &str) -> bool { - component - .as_bytes() - .iter() - .any(|byte| matches!(byte, b'*' | b'?' | b'[')) -} - -/// Writes one byte-string value into an indexed runtime array at a zero-based position. -pub(in crate::interpreter) fn eval_array_set_indexed_bytes( - array: RuntimeCellHandle, - index: usize, - value: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.string_bytes_value(value)?; - values.array_set(array, key, value) -} - -/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_tempnam( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory, prefix] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - let prefix = eval_expr(prefix, context, scope, values)?; - eval_tempnam_result(directory, prefix, values) -} - -/// Creates a unique local temporary file and returns its path, or an empty string on failure. -pub(in crate::interpreter) fn eval_tempnam_result( - directory: RuntimeCellHandle, - prefix: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let directory = eval_path_string(directory, values)?; - let prefix = values.string_bytes(prefix)?; - let prefix = String::from_utf8_lossy(&prefix); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - for attempt in 0..1000_u32 { - let candidate = - std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); - match std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&candidate) - { - Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), - Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, - Err(_) => return values.string(""), - } - } - values.string("") -} - -/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. -pub(in crate::interpreter) fn eval_tempnam_filename( - prefix: &str, - nonce: u128, - attempt: u32, -) -> String { - format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) -} - -/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_touch( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [filename] => { - let filename = eval_expr(filename, context, scope, values)?; - eval_touch_result(filename, None, None, values) - } - [filename, mtime] => { - let filename = eval_expr(filename, context, scope, values)?; - let mtime = eval_expr(mtime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), None, values) - } - [filename, mtime, atime] => { - let filename = eval_expr(filename, context, scope, values)?; - let mtime = eval_expr(mtime, context, scope, values)?; - let atime = eval_expr(atime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), Some(atime), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Creates or stamps one local file and returns whether the operation succeeded. -pub(in crate::interpreter) fn eval_touch_result( - filename: RuntimeCellHandle, - mtime: Option, - atime: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let (mtime, atime) = eval_touch_times(mtime, atime, values)?; - let file = match std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(path) - { - Ok(file) => file, - Err(_) => return values.bool_value(false), - }; - let times = std::fs::FileTimes::new() - .set_modified(mtime) - .set_accessed(atime); - values.bool_value(file.set_times(times).is_ok()) -} - -/// Resolves PHP touch timestamp defaults into concrete system times. -pub(in crate::interpreter) fn eval_touch_times( - mtime: Option, - atime: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { - let now = std::time::SystemTime::now(); - let Some(mtime) = mtime else { - return Ok((now, now)); - }; - if values.is_null(mtime)? { - if let Some(atime) = atime { - if !values.is_null(atime)? { - return Err(EvalStatus::RuntimeFatal); - } - } - return Ok((now, now)); - } - let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) - .ok_or(EvalStatus::RuntimeFatal)?; - let Some(atime) = atime else { - return Ok((mtime, mtime)); - }; - if values.is_null(atime)? { - return Ok((mtime, mtime)); - } - let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) - .ok_or(EvalStatus::RuntimeFatal)?; - Ok((mtime, atime)) -} - -/// Converts a Unix timestamp in seconds into a `SystemTime`. -pub(in crate::interpreter) fn eval_system_time_from_unix( - seconds: i64, -) -> Option { - if seconds >= 0 { - std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) - } else { - std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) - } -} - -/// Evaluates PHP `umask($mask = null)` over an optional eval expression. -pub(in crate::interpreter) fn eval_builtin_umask( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_umask_result(None, values), - [mask] => { - let mask = eval_expr(mask, context, scope, values)?; - eval_umask_result(Some(mask), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Applies PHP `umask()` semantics and returns the previous mask. -pub(in crate::interpreter) fn eval_umask_result( - mask: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let previous = match mask { - Some(mask) => { - let mask = eval_int_value(mask, values)? as u32; - unsafe { umask(mask) } - } - None => unsafe { - let current = umask(0); - umask(current); - current - }, - }; - values.int(i64::from(previous)) -} - -/// Evaluates PHP `readlink($path)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_readlink( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_readlink_result(path, values) -} - -/// Reads one symbolic-link target string, or returns PHP false on failure. -pub(in crate::interpreter) fn eval_readlink_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - match std::fs::read_link(path) { - Ok(target) => values.string(target.to_string_lossy().as_ref()), - Err(_) => values.bool_value(false), - } -} - -/// Evaluates PHP `linkinfo($path)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_linkinfo( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_linkinfo_result(path, values) -} - -/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. -pub(in crate::interpreter) fn eval_linkinfo_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - let dev = match std::fs::symlink_metadata(path) { - Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, - Err(_) => -1, - }; - values.int(dev) -} - -/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. -pub(in crate::interpreter) fn eval_builtin_clearstatcache( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - eval_expr(arg, context, scope, values)?; - } - values.null() -} - -/// Evaluates PHP `unlink($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_unlink( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_unlink_result(filename, values) -} - -/// Deletes one local file and returns whether it succeeded. -pub(in crate::interpreter) fn eval_unlink_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - values.bool_value(std::fs::remove_file(path).is_ok()) -} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/disk.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/disk.rs new file mode 100644 index 0000000000..4ed51e447c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/disk.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Implements disk-space filesystem eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` re-exports. +//! +//! Key details: +//! - `statvfs` failures map to PHP-compatible `0.0` results. + +use super::super::super::super::*; + +/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. +pub(in crate::interpreter) fn eval_builtin_disk_space( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_disk_space_result(name, directory, values) +} + +/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. +pub(in crate::interpreter) fn eval_disk_space_result( + name: &str, + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(directory)?; + let Ok(path) = CString::new(bytes) else { + return values.float(0.0); + }; + let mut stats = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes the statvfs fields for this NUL-terminated local path. + libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) + }; + if status != 0 { + return values.float(0.0); + } + let stats = unsafe { + // `statvfs` succeeded, so libc initialized the full stat buffer. + stats.assume_init() + }; + let block_size = if stats.f_frsize > 0 { + stats.f_frsize + } else { + stats.f_bsize + }; + let blocks = match name { + "disk_free_space" => stats.f_bavail, + "disk_total_space" => stats.f_blocks, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.float((block_size as f64) * (blocks as f64)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/glob.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/glob.rs new file mode 100644 index 0000000000..77e35df918 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/glob.rs @@ -0,0 +1,136 @@ +//! Purpose: +//! Implements local filesystem glob expansion for eval. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` re-exports. +//! +//! Key details: +//! - Components are expanded recursively and sorted for deterministic PHP array +//! output. + +use super::super::super::super::*; +use super::super::*; +use super::*; + +/// Evaluates PHP `glob($pattern)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_glob( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + eval_glob_result(pattern, values) +} + +/// Expands one local glob pattern into a sorted indexed PHP string array. +pub(in crate::interpreter) fn eval_glob_result( + pattern: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let pattern = eval_path_string(pattern, values)?; + let matches = eval_glob_matches(&pattern); + let mut result = values.array_new(matches.len())?; + for (index, path) in matches.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; + } + Ok(result) +} + +/// Collects sorted matches for one local glob pattern. +pub(in crate::interpreter) fn eval_glob_matches(pattern: &str) -> Vec { + if pattern.is_empty() { + return Vec::new(); + } + if !eval_glob_component_has_magic(pattern) { + return std::path::Path::new(pattern) + .exists() + .then(|| pattern.to_string()) + .into_iter() + .collect(); + } + let absolute = pattern.starts_with('/'); + let components: Vec<&str> = pattern + .split('/') + .filter(|component| !component.is_empty()) + .collect(); + let mut matches = Vec::new(); + let base = if absolute { + std::path::PathBuf::from("/") + } else { + std::path::PathBuf::from(".") + }; + let prefix = if absolute { "/" } else { "" }; + eval_glob_collect(&base, prefix, &components, &mut matches); + matches.sort(); + matches +} + +/// Recursively expands one glob path component at a time. +pub(in crate::interpreter) fn eval_glob_collect( + base: &std::path::Path, + prefix: &str, + components: &[&str], + matches: &mut Vec, +) { + let Some((component, rest)) = components.split_first() else { + if base.exists() && !prefix.is_empty() { + matches.push(prefix.to_string()); + } + return; + }; + if !eval_glob_component_has_magic(component) { + let next_base = base.join(component); + if rest.is_empty() { + if next_base.exists() { + matches.push(eval_glob_join_output(prefix, component)); + } + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, component); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + return; + } + let Ok(entries) = std::fs::read_dir(base) else { + return; + }; + let mut names = Vec::new(); + for entry in entries.flatten() { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + for name in names { + if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { + continue; + } + let next_base = base.join(&name); + if rest.is_empty() { + matches.push(eval_glob_join_output(prefix, &name)); + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, &name); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + } +} + +/// Joins a display path prefix and component while preserving absolute-root output. +pub(in crate::interpreter) fn eval_glob_join_output(prefix: &str, component: &str) -> String { + if prefix.is_empty() { + component.to_string() + } else if prefix == "/" { + format!("/{component}") + } else { + format!("{prefix}/{component}") + } +} + +/// Returns whether a glob component contains wildcard syntax. +pub(in crate::interpreter) fn eval_glob_component_has_magic(component: &str) -> bool { + component + .as_bytes() + .iter() + .any(|byte| matches!(byte, b'*' | b'?' | b'[')) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/links.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/links.rs new file mode 100644 index 0000000000..42925a2f9c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/links.rs @@ -0,0 +1,104 @@ +//! Purpose: +//! Implements symbolic-link, clearstatcache, and unlink eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` re-exports. +//! +//! Key details: +//! - Link failures map to PHP false or documented sentinel values without raising +//! host IO errors through Rust panics. + +use super::super::super::super::*; +use super::super::*; + +/// Evaluates PHP `readlink($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_readlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_readlink_result(path, values) +} + +/// Reads one symbolic-link target string, or returns PHP false on failure. +pub(in crate::interpreter) fn eval_readlink_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + match std::fs::read_link(path) { + Ok(target) => values.string(target.to_string_lossy().as_ref()), + Err(_) => values.bool_value(false), + } +} + +/// Evaluates PHP `linkinfo($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_linkinfo( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_linkinfo_result(path, values) +} + +/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. +pub(in crate::interpreter) fn eval_linkinfo_result( + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let dev = match std::fs::symlink_metadata(path) { + Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, + Err(_) => -1, + }; + values.int(dev) +} + +/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. +pub(in crate::interpreter) fn eval_builtin_clearstatcache( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + values.null() +} + +/// Evaluates PHP `unlink($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_unlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_unlink_result(filename, values) +} + +/// Deletes one local file and returns whether it succeeded. +pub(in crate::interpreter) fn eval_unlink_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + values.bool_value(std::fs::remove_file(path).is_ok()) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/listing.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/listing.rs new file mode 100644 index 0000000000..b14c6695be --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/listing.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Implements directory listing helpers and indexed byte-array insertion. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` and file-reading helpers. +//! +//! Key details: +//! - Byte-array insertion is reused by `file()` and glob/listing builtins to +//! produce sequential PHP arrays. + +use super::super::super::super::*; +use super::super::*; + +/// Evaluates PHP `scandir($directory)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_scandir( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_scandir_result(directory, values) +} + +/// Lists one local directory into an indexed string array, or an empty array on failure. +pub(in crate::interpreter) fn eval_scandir_result( + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(directory, values)?; + let Ok(entries) = std::fs::read_dir(path) else { + return values.array_new(0); + }; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; + } + Ok(result) +} + +/// Writes one byte-string value into an indexed runtime array at a zero-based position. +pub(in crate::interpreter) fn eval_array_set_indexed_bytes( + array: RuntimeCellHandle, + index: usize, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/mod.rs new file mode 100644 index 0000000000..fa7df70e8a --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/mod.rs @@ -0,0 +1,27 @@ +//! Purpose: +//! Groups miscellaneous filesystem operation eval builtins by operation family. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem` re-exports. +//! +//! Key details: +//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps` +//! while path coercion remains shared with sibling filesystem modules. + +mod disk; +mod glob; +mod links; +mod listing; +mod path_bool; +mod tempnam; +mod touch; +mod umask; + +pub(in crate::interpreter) use disk::*; +pub(in crate::interpreter) use glob::*; +pub(in crate::interpreter) use links::*; +pub(in crate::interpreter) use listing::*; +pub(in crate::interpreter) use path_bool::*; +pub(in crate::interpreter) use tempnam::*; +pub(in crate::interpreter) use touch::*; +pub(in crate::interpreter) use umask::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs new file mode 100644 index 0000000000..231a2d3383 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs @@ -0,0 +1,106 @@ +//! Purpose: +//! Implements path operations that return PHP booleans, plus `chmod`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` re-exports. +//! +//! Key details: +//! - Paths are coerced through the shared filesystem path helper before host +//! filesystem operations are attempted. + +use super::super::super::super::*; +use super::super::super::*; +use super::super::*; + +/// Evaluates a one-path filesystem operation that returns a PHP boolean. +pub(in crate::interpreter) fn eval_builtin_unary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_unary_path_bool_result(name, path, values) +} + +/// Executes a one-path local filesystem operation and returns whether it succeeded. +pub(in crate::interpreter) fn eval_unary_path_bool_result( + name: &str, + path: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + let ok = match name { + "chdir" => std::env::set_current_dir(path).is_ok(), + "mkdir" => std::fs::create_dir(path).is_ok(), + "rmdir" => std::fs::remove_dir(path).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Evaluates a two-path filesystem operation that returns a PHP boolean. +pub(in crate::interpreter) fn eval_builtin_binary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [from, to] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let from = eval_expr(from, context, scope, values)?; + let to = eval_expr(to, context, scope, values)?; + eval_binary_path_bool_result(name, from, to, values) +} + +/// Executes a two-path local filesystem operation and returns whether it succeeded. +pub(in crate::interpreter) fn eval_binary_path_bool_result( + name: &str, + from: RuntimeCellHandle, + to: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_path_string(from, values)?; + let to = eval_path_string(to, values)?; + let ok = match name { + "copy" => std::fs::copy(from, to).is_ok(), + "link" => std::fs::hard_link(from, to).is_ok(), + "rename" => std::fs::rename(from, to).is_ok(), + "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_chmod( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, permissions] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let permissions = eval_expr(permissions, context, scope, values)?; + eval_chmod_result(filename, permissions, values) +} + +/// Changes one local file's mode and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_chmod_result( + filename: RuntimeCellHandle, + permissions: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let mode = eval_int_value(permissions, values)? as u32; + let permissions = std::fs::Permissions::from_mode(mode); + values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/tempnam.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/tempnam.rs new file mode 100644 index 0000000000..3372b2de28 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/tempnam.rs @@ -0,0 +1,65 @@ +//! Purpose: +//! Implements PHP `tempnam()` eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` re-exports. +//! +//! Key details: +//! - Candidate names are deterministic within process/attempt data and created +//! with exclusive file creation. + +use super::super::super::super::*; +use super::super::*; + +/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_tempnam( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory, prefix] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + let prefix = eval_expr(prefix, context, scope, values)?; + eval_tempnam_result(directory, prefix, values) +} + +/// Creates a unique local temporary file and returns its path, or an empty string on failure. +pub(in crate::interpreter) fn eval_tempnam_result( + directory: RuntimeCellHandle, + prefix: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + let prefix = values.string_bytes(prefix)?; + let prefix = String::from_utf8_lossy(&prefix); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + for attempt in 0..1000_u32 { + let candidate = + std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&candidate) + { + Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(_) => return values.string(""), + } + } + values.string("") +} + +/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. +pub(in crate::interpreter) fn eval_tempnam_filename( + prefix: &str, + nonce: u128, + attempt: u32, +) -> String { + format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/touch.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/touch.rs new file mode 100644 index 0000000000..ab55f1a3f4 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/touch.rs @@ -0,0 +1,105 @@ +//! Purpose: +//! Implements PHP `touch()` eval support and timestamp conversion helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` re-exports. +//! +//! Key details: +//! - Existing files are preserved, missing files are created, and timestamp +//! arguments are resolved to `SystemTime` values. + +use super::super::super::super::*; +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_touch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [filename] => { + let filename = eval_expr(filename, context, scope, values)?; + eval_touch_result(filename, None, None, values) + } + [filename, mtime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), None, values) + } + [filename, mtime, atime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + let atime = eval_expr(atime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), Some(atime), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates or stamps one local file and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_touch_result( + filename: RuntimeCellHandle, + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let (mtime, atime) = eval_touch_times(mtime, atime, values)?; + let file = match std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(path) + { + Ok(file) => file, + Err(_) => return values.bool_value(false), + }; + let times = std::fs::FileTimes::new() + .set_modified(mtime) + .set_accessed(atime); + values.bool_value(file.set_times(times).is_ok()) +} + +/// Resolves PHP touch timestamp defaults into concrete system times. +pub(in crate::interpreter) fn eval_touch_times( + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { + let now = std::time::SystemTime::now(); + let Some(mtime) = mtime else { + return Ok((now, now)); + }; + if values.is_null(mtime)? { + if let Some(atime) = atime { + if !values.is_null(atime)? { + return Err(EvalStatus::RuntimeFatal); + } + } + return Ok((now, now)); + } + let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + let Some(atime) = atime else { + return Ok((mtime, mtime)); + }; + if values.is_null(atime)? { + return Ok((mtime, mtime)); + } + let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + Ok((mtime, atime)) +} + +/// Converts a Unix timestamp in seconds into a `SystemTime`. +pub(in crate::interpreter) fn eval_system_time_from_unix( + seconds: i64, +) -> Option { + if seconds >= 0 { + std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) + } else { + std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/umask.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/umask.rs new file mode 100644 index 0000000000..1cc6fbfa73 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/umask.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Implements PHP `umask()` eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` re-exports. +//! +//! Key details: +//! - The previous process umask is returned after optionally applying a new mask. + +use super::super::super::super::*; +use super::super::super::*; + +/// Evaluates PHP `umask($mask = null)` over an optional eval expression. +pub(in crate::interpreter) fn eval_builtin_umask( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_umask_result(None, values), + [mask] => { + let mask = eval_expr(mask, context, scope, values)?; + eval_umask_result(Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `umask()` semantics and returns the previous mask. +pub(in crate::interpreter) fn eval_umask_result( + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let previous = match mask { + Some(mask) => { + let mask = eval_int_value(mask, values)? as u32; + unsafe { umask(mask) } + } + None => unsafe { + let current = umask(0); + umask(current); + current + }, + }; + values.int(i64::from(previous)) +} From e59a0c972bd2a6aa32891579d2b79cbb83577971 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:48:08 +0200 Subject: [PATCH 0294/1208] Split eval array filter builtins --- .../interpreter/builtins/arrays/filters.rs | 657 ------------------ .../builtins/arrays/filters/chunk.rs | 61 ++ .../builtins/arrays/filters/filter.rs | 103 +++ .../builtins/arrays/filters/flip.rs | 42 ++ .../builtins/arrays/filters/iterator.rs | 194 ++++++ .../builtins/arrays/filters/mod.rs | 30 + .../builtins/arrays/filters/pad.rs | 86 +++ .../builtins/arrays/filters/projection.rs | 46 ++ .../builtins/arrays/filters/reverse.rs | 68 ++ .../builtins/arrays/filters/slice.rs | 98 +++ .../builtins/arrays/filters/unique.rs | 45 ++ 11 files changed, 773 insertions(+), 657 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/chunk.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/filter.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/flip.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/pad.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/projection.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/reverse.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/slice.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/filters/unique.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters.rs deleted file mode 100644 index 60fa01aa1e..0000000000 --- a/crates/elephc-eval/src/interpreter/builtins/arrays/filters.rs +++ /dev/null @@ -1,657 +0,0 @@ -//! Purpose: -//! Array filter, chunk, slice, pad, projection, iterator, and reverse helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! -//! Key details: -//! - Array cells remain opaque runtime handles and are manipulated through -//! `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP `array_filter()` for null and string-callback filtering modes. -pub(in crate::interpreter) fn eval_builtin_array_filter( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array] => { - let array = eval_expr(array, context, scope, values)?; - eval_array_filter_result(array, None, None, context, values) - } - [array, callback] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - eval_array_filter_result(array, Some(callback), None, context, values) - } - [array, callback, mode] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_array_filter_result(array, Some(callback), Some(mode), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Filters eval array entries through PHP truthiness or a string callback. -pub(in crate::interpreter) fn eval_array_filter_result( - array: RuntimeCellHandle, - callback: Option, - mode: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = match callback { - Some(callback) if !values.is_null(callback)? => Some(eval_callable_name(callback, values)?), - _ => None, - }; - let mode = match mode { - Some(mode) => eval_array_filter_mode_value(mode, values)?, - None => EVAL_ARRAY_FILTER_USE_VALUE, - }; - - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let keep = if let Some(callback) = callback.as_deref() { - let args = eval_array_filter_callback_args(mode, key, value)?; - let result = eval_callable_with_values(callback, args, context, values)?; - values.truthy(result)? - } else { - values.truthy(value)? - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Reads and validates the optional `array_filter()` callback mode. -pub(in crate::interpreter) fn eval_array_filter_mode_value( - mode: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = eval_int_value(mode, values)?; - match mode { - EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { - Ok(mode) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds the callback argument list for one `array_filter()` entry. -pub(in crate::interpreter) fn eval_array_filter_callback_args( - mode: i64, - key: RuntimeCellHandle, - value: RuntimeCellHandle, -) -> Result, EvalStatus> { - match mode { - EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), - EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), - EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. -pub(in crate::interpreter) fn eval_builtin_array_chunk( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, length] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_chunk_result(array, length, values) -} - -/// Builds an `array_chunk()` result as nested reindexed arrays. -pub(in crate::interpreter) fn eval_array_chunk_result( - array: RuntimeCellHandle, - length: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let chunk_size = eval_int_value(length, values)?; - if chunk_size <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; - let len = values.array_len(array)?; - let chunk_count = len.div_ceil(chunk_size); - let mut result = values.array_new(chunk_count)?; - - for chunk_index in 0..chunk_count { - let start = chunk_index * chunk_size; - let end = usize::min(start + chunk_size, len); - let mut chunk = values.array_new(end - start)?; - for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let value = values.array_get(array, source_key)?; - let target_index = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_index = values.int(target_index)?; - chunk = values.array_set(chunk, target_index, value)?; - } - let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let result_key = values.int(result_key)?; - result = values.array_set(result, result_key, chunk)?; - } - - Ok(result) -} - -/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. -pub(in crate::interpreter) fn eval_builtin_array_slice( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array, offset] => { - let array = eval_expr(array, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_array_slice_result(array, offset, None, values) - } - [array, offset, length] => { - let array = eval_expr(array, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_slice_result(array, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds an `array_slice()` result with PHP offset and length bounds. -pub(in crate::interpreter) fn eval_array_slice_result( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let offset = eval_int_value(offset, values)?; - let start = eval_slice_start(len, offset)?; - let end = match length { - Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { - eval_slice_end(len, start, eval_int_value(length, values)?)? - } - _ => len, - }; - - let mut result = values.array_new(end.saturating_sub(start))?; - for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; - result = values.array_set(result, target_key, source_value)?; - } - Ok(result) -} - -/// Converts a PHP array-slice offset into a bounded source position. -pub(in crate::interpreter) fn eval_slice_start( - len: usize, - offset: i64, -) -> Result { - if offset >= 0 { - let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; - return Ok(usize::min(offset, len)); - } - - let tail = offset - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - Ok(len.saturating_sub(tail)) -} - -/// Converts a PHP array-slice length into a bounded exclusive end position. -pub(in crate::interpreter) fn eval_slice_end( - len: usize, - start: usize, - length: i64, -) -> Result { - if length >= 0 { - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - return Ok(usize::min(start.saturating_add(length), len)); - } - - let tail = length - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - Ok(usize::max(start, len.saturating_sub(tail))) -} - -/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. -pub(in crate::interpreter) fn eval_builtin_array_pad( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, length, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_pad_result(array, length, value, values) -} - -/// Builds an `array_pad()` result by copying values and padding left or right. -pub(in crate::interpreter) fn eval_array_pad_result( - array: RuntimeCellHandle, - length: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let target = eval_int_value(length, values)?; - let target_len = target - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - let result_len = usize::max(len, target_len); - let pad_count = result_len.saturating_sub(len); - let mut result = values.array_new(result_len)?; - let mut output_index = 0usize; - - if target < 0 { - let (padded, next_index) = - eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; - result = padded; - output_index = next_index; - } - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; - result = values.array_set(result, target_key, source_value)?; - output_index += 1; - } - - if target > 0 { - result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; - } - - Ok(result) -} - -/// Appends the same pad value at consecutive indexed positions in an array result. -pub(in crate::interpreter) fn eval_array_pad_append_repeated( - mut array: RuntimeCellHandle, - start_index: usize, - count: usize, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, usize), EvalStatus> { - let mut next_index = start_index; - for _ in 0..count { - let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - array = values.array_set(array, key, value)?; - next_index += 1; - } - Ok((array, next_index)) -} - -/// Evaluates PHP `array_flip()` over one eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_flip( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_flip_result(array, values) -} - -/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. -pub(in crate::interpreter) fn eval_array_flip_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { - continue; - } - result = values.array_set(result, value, key)?; - } - Ok(result) -} - -/// Evaluates PHP `array_unique()` over one eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_unique( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_unique_result(array, values) -} - -/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. -pub(in crate::interpreter) fn eval_array_unique_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut seen = Vec::>::with_capacity(len); - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let unique_key = values.string_bytes(value)?; - if seen.iter().any(|seen_key| seen_key == &unique_key) { - continue; - } - seen.push(unique_key); - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP array projection builtins over one eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_projection( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_projection_result(name, array, values) -} - -/// Builds the indexed result array for `array_keys()` or `array_values()`. -pub(in crate::interpreter) fn eval_array_projection_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = match name { - "array_keys" => key, - "array_values" => values.array_get(array, key)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let index = values.int(position as i64)?; - result = values.array_set(result, index, value)?; - } - Ok(result) -} - -/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. -pub(in crate::interpreter) fn eval_builtin_iterator_apply( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [iterator, callback] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, values)?; - eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) - } - [iterator, callback, callback_args] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, values)?; - let callback_args = eval_expr(callback_args, context, scope, values)?; - let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; - eval_iterator_apply_result(iterator, &callback, callback_args, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts the optional `iterator_apply()` callback-args value into call arguments. -pub(in crate::interpreter) fn eval_iterator_apply_arg_values( - args: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.is_null(args)? { - return Ok(Vec::new()); - } - if !values.is_array_like(args)? { - return Err(EvalStatus::RuntimeFatal); - } - eval_array_call_arg_values(args, values) -} - -/// Applies a callback to each valid position of an eval-supported Traversable object. -pub(in crate::interpreter) fn eval_iterator_apply_result( - iterator: RuntimeCellHandle, - callback: &EvaluatedCallable, - callback_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(iterator)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let count = match eval_iterator_apply_iterator_object( - iterator, - callback, - &callback_args, - context, - values, - ) { - Ok(count) => count, - Err(EvalStatus::UnsupportedConstruct) => { - let iterator = values.method_call(iterator, "getiterator", Vec::new())?; - eval_iterator_apply_iterator_object( - iterator, - callback, - &callback_args, - context, - values, - )? - } - Err(err) => return Err(err), - }; - values.int(count) -} - -/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. -pub(in crate::interpreter) fn eval_iterator_apply_iterator_object( - iterator: RuntimeCellHandle, - callback: &EvaluatedCallable, - callback_args: &[EvaluatedCallArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let _ = values.method_call(iterator, "rewind", Vec::new())?; - let mut count = 0_i64; - loop { - let valid = values.method_call(iterator, "valid", Vec::new())?; - if !values.truthy(valid)? { - return Ok(count); - } - count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - let result = eval_evaluated_callable_with_call_array_args( - callback, - callback_args.to_vec(), - context, - values, - )?; - if !values.truthy(result)? { - return Ok(count); - } - let _ = values.method_call(iterator, "next", Vec::new())?; - } -} - -/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. -pub(in crate::interpreter) fn eval_builtin_iterator_count( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [iterator] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let iterator = eval_expr(iterator, context, scope, values)?; - eval_iterator_count_result(iterator, values) -} - -/// Returns the element count for eval-supported array iterator inputs. -pub(in crate::interpreter) fn eval_iterator_count_result( - iterator: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(iterator)?; - values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. -pub(in crate::interpreter) fn eval_builtin_iterator_to_array( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [iterator] => { - let iterator = eval_expr(iterator, context, scope, values)?; - eval_iterator_to_array_result(iterator, true, values) - } - [iterator, preserve_keys] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; - let preserve_keys = values.truthy(preserve_keys)?; - eval_iterator_to_array_result(iterator, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Copies eval-supported array iterator inputs into a PHP array result. -pub(in crate::interpreter) fn eval_iterator_to_array_result( - iterator: RuntimeCellHandle, - preserve_keys: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - if preserve_keys { - return eval_array_copy_preserve_keys(iterator, values); - } - eval_array_projection_result("array_values", iterator, values) -} - -/// Copies one array-like eval value while preserving iteration keys and order. -pub(in crate::interpreter) fn eval_array_copy_preserve_keys( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_reverse()` over an eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_reverse( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array] => { - let array = eval_expr(array, context, scope, values)?; - eval_array_reverse_result(array, false, values) - } - [array, preserve_keys] => { - let array = eval_expr(array, context, scope, values)?; - let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; - let preserve_keys = values.truthy(preserve_keys)?; - eval_array_reverse_result(array, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds an `array_reverse()` result while preserving PHP key rules. -pub(in crate::interpreter) fn eval_array_reverse_result( - array: RuntimeCellHandle, - preserve_keys: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut keys = Vec::with_capacity(len); - let mut has_string_key = false; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; - keys.push(key); - } - - let mut result = if preserve_keys || has_string_key { - values.assoc_new(len)? - } else { - values.array_new(len)? - }; - let mut next_numeric_key = 0_i64; - - for key in keys.into_iter().rev() { - let value = values.array_get(array, key)?; - let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { - key - } else { - let key = values.int(next_numeric_key)?; - next_numeric_key += 1; - key - }; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/chunk.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/chunk.rs new file mode 100644 index 0000000000..b6c558f309 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/chunk.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Implements PHP `array_chunk()` eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` re-exports. +//! +//! Key details: +//! - Chunks are reindexed arrays and invalid sizes produce runtime fatal status. + +use super::super::super::super::*; +use super::super::super::*; + +/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. +pub(in crate::interpreter) fn eval_builtin_array_chunk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_chunk_result(array, length, values) +} + +/// Builds an `array_chunk()` result as nested reindexed arrays. +pub(in crate::interpreter) fn eval_array_chunk_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let chunk_size = eval_int_value(length, values)?; + if chunk_size <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; + let len = values.array_len(array)?; + let chunk_count = len.div_ceil(chunk_size); + let mut result = values.array_new(chunk_count)?; + + for chunk_index in 0..chunk_count { + let start = chunk_index * chunk_size; + let end = usize::min(start + chunk_size, len); + let mut chunk = values.array_new(end - start)?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let value = values.array_get(array, source_key)?; + let target_index = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_index = values.int(target_index)?; + chunk = values.array_set(chunk, target_index, value)?; + } + let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let result_key = values.int(result_key)?; + result = values.array_set(result, result_key, chunk)?; + } + + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/filter.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/filter.rs new file mode 100644 index 0000000000..14471ebc47 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/filter.rs @@ -0,0 +1,103 @@ +//! Purpose: +//! Implements PHP `array_filter()` eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` re-exports. +//! +//! Key details: +//! - Callback argument shape follows the supported filter mode and null callbacks +//! use PHP truthiness. + +use super::super::super::super::*; +use super::super::super::*; + +/// Evaluates PHP `array_filter()` for null and string-callback filtering modes. +pub(in crate::interpreter) fn eval_builtin_array_filter( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_filter_result(array, None, None, context, values) + } + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_filter_result(array, Some(callback), None, context, values) + } + [array, callback, mode] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_array_filter_result(array, Some(callback), Some(mode), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Filters eval array entries through PHP truthiness or a string callback. +pub(in crate::interpreter) fn eval_array_filter_result( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = match callback { + Some(callback) if !values.is_null(callback)? => Some(eval_callable_name(callback, values)?), + _ => None, + }; + let mode = match mode { + Some(mode) => eval_array_filter_mode_value(mode, values)?, + None => EVAL_ARRAY_FILTER_USE_VALUE, + }; + + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let keep = if let Some(callback) = callback.as_deref() { + let args = eval_array_filter_callback_args(mode, key, value)?; + let result = eval_callable_with_values(callback, args, context, values)?; + values.truthy(result)? + } else { + values.truthy(value)? + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Reads and validates the optional `array_filter()` callback mode. +pub(in crate::interpreter) fn eval_array_filter_mode_value( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = eval_int_value(mode, values)?; + match mode { + EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { + Ok(mode) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the callback argument list for one `array_filter()` entry. +pub(in crate::interpreter) fn eval_array_filter_callback_args( + mode: i64, + key: RuntimeCellHandle, + value: RuntimeCellHandle, +) -> Result, EvalStatus> { + match mode { + EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), + EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), + EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/flip.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/flip.rs new file mode 100644 index 0000000000..895ca9532f --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/flip.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Implements PHP `array_flip()` eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` re-exports. +//! +//! Key details: +//! - Only PHP-valid scalar key values are inserted into the flipped result. + +use super::super::super::super::*; + +/// Evaluates PHP `array_flip()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_flip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_flip_result(array, values) +} + +/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. +pub(in crate::interpreter) fn eval_array_flip_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { + continue; + } + result = values.array_set(result, value, key)?; + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs new file mode 100644 index 0000000000..777d927fa4 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs @@ -0,0 +1,194 @@ +//! Purpose: +//! Implements iterator-related array eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` re-exports. +//! +//! Key details: +//! - Iterator objects are driven through `rewind`, `valid`, callback, and `next` +//! calls in PHP-observable order. + +use super::super::super::super::*; +use super::super::super::*; +use super::*; + +/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_apply( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator, callback] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable(callback, values)?; + eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, callback_args] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable(callback, values)?; + let callback_args = eval_expr(callback_args, context, scope, values)?; + let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; + eval_iterator_apply_result(iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts the optional `iterator_apply()` callback-args value into call arguments. +pub(in crate::interpreter) fn eval_iterator_apply_arg_values( + args: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(args)? { + return Ok(Vec::new()); + } + if !values.is_array_like(args)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_array_call_arg_values(args, values) +} + +/// Applies a callback to each valid position of an eval-supported Traversable object. +pub(in crate::interpreter) fn eval_iterator_apply_result( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(iterator)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let count = match eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + ) { + Ok(count) => count, + Err(EvalStatus::UnsupportedConstruct) => { + let iterator = values.method_call(iterator, "getiterator", Vec::new())?; + eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + )? + } + Err(err) => return Err(err), + }; + values.int(count) +} + +/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. +pub(in crate::interpreter) fn eval_iterator_apply_iterator_object( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.method_call(iterator, "rewind", Vec::new())?; + let mut count = 0_i64; + loop { + let valid = values.method_call(iterator, "valid", Vec::new())?; + if !values.truthy(valid)? { + return Ok(count); + } + count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let result = eval_evaluated_callable_with_call_array_args( + callback, + callback_args.to_vec(), + context, + values, + )?; + if !values.truthy(result)? { + return Ok(count); + } + let _ = values.method_call(iterator, "next", Vec::new())?; + } +} + +/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [iterator] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_count_result(iterator, values) +} + +/// Returns the element count for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_iterator_count_result( + iterator: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(iterator)?; + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_to_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator] => { + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_to_array_result(iterator, true, values) + } + [iterator, preserve_keys] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_iterator_to_array_result(iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies eval-supported array iterator inputs into a PHP array result. +pub(in crate::interpreter) fn eval_iterator_to_array_result( + iterator: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + if preserve_keys { + return eval_array_copy_preserve_keys(iterator, values); + } + eval_array_projection_result("array_values", iterator, values) +} + +/// Copies one array-like eval value while preserving iteration keys and order. +pub(in crate::interpreter) fn eval_array_copy_preserve_keys( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/mod.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/mod.rs new file mode 100644 index 0000000000..2d898b6de4 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/mod.rs @@ -0,0 +1,30 @@ +//! Purpose: +//! Groups array filtering, slicing, projection, iterator, and reversal eval +//! builtins by focused operation family. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Helpers preserve PHP key normalization and iteration order through +//! `RuntimeValueOps`. + +mod chunk; +mod filter; +mod flip; +mod iterator; +mod pad; +mod projection; +mod reverse; +mod slice; +mod unique; + +pub(in crate::interpreter) use chunk::*; +pub(in crate::interpreter) use filter::*; +pub(in crate::interpreter) use flip::*; +pub(in crate::interpreter) use iterator::*; +pub(in crate::interpreter) use pad::*; +pub(in crate::interpreter) use projection::*; +pub(in crate::interpreter) use reverse::*; +pub(in crate::interpreter) use slice::*; +pub(in crate::interpreter) use unique::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/pad.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/pad.rs new file mode 100644 index 0000000000..f8e4e487a0 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/pad.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Implements PHP `array_pad()` eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` re-exports. +//! +//! Key details: +//! - Padding can be prepended or appended while preserving copied source values. + +use super::super::super::super::*; +use super::super::super::*; + +/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. +pub(in crate::interpreter) fn eval_builtin_array_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_pad_result(array, length, value, values) +} + +/// Builds an `array_pad()` result by copying values and padding left or right. +pub(in crate::interpreter) fn eval_array_pad_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let target = eval_int_value(length, values)?; + let target_len = target + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + let result_len = usize::max(len, target_len); + let pad_count = result_len.saturating_sub(len); + let mut result = values.array_new(result_len)?; + let mut output_index = 0usize; + + if target < 0 { + let (padded, next_index) = + eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; + result = padded; + output_index = next_index; + } + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + output_index += 1; + } + + if target > 0 { + result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; + } + + Ok(result) +} + +/// Appends the same pad value at consecutive indexed positions in an array result. +pub(in crate::interpreter) fn eval_array_pad_append_repeated( + mut array: RuntimeCellHandle, + start_index: usize, + count: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, usize), EvalStatus> { + let mut next_index = start_index; + for _ in 0..count { + let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + array = values.array_set(array, key, value)?; + next_index += 1; + } + Ok((array, next_index)) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/projection.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/projection.rs new file mode 100644 index 0000000000..9a58072f4c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/projection.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Implements `array_keys()` and `array_values()` eval projections. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` re-exports. +//! +//! Key details: +//! - Projection output is always a sequential indexed runtime array. + +use super::super::super::super::*; + +/// Evaluates PHP array projection builtins over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_projection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_projection_result(name, array, values) +} + +/// Builds the indexed result array for `array_keys()` or `array_values()`. +pub(in crate::interpreter) fn eval_array_projection_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = match name { + "array_keys" => key, + "array_values" => values.array_get(array, key)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let index = values.int(position as i64)?; + result = values.array_set(result, index, value)?; + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/reverse.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/reverse.rs new file mode 100644 index 0000000000..d94d5e8954 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/reverse.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Implements PHP `array_reverse()` eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` re-exports. +//! +//! Key details: +//! - Key preservation follows PHP rules for string and integer keys. + +use super::super::super::super::*; + +/// Evaluates PHP `array_reverse()` over an eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_reverse( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_reverse_result(array, false, values) + } + [array, preserve_keys] => { + let array = eval_expr(array, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_array_reverse_result(array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_reverse()` result while preserving PHP key rules. +pub(in crate::interpreter) fn eval_array_reverse_result( + array: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut keys = Vec::with_capacity(len); + let mut has_string_key = false; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; + keys.push(key); + } + + let mut result = if preserve_keys || has_string_key { + values.assoc_new(len)? + } else { + values.array_new(len)? + }; + let mut next_numeric_key = 0_i64; + + for key in keys.into_iter().rev() { + let value = values.array_get(array, key)?; + let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { + key + } else { + let key = values.int(next_numeric_key)?; + next_numeric_key += 1; + key + }; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/slice.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/slice.rs new file mode 100644 index 0000000000..bfc28d11b0 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/slice.rs @@ -0,0 +1,98 @@ +//! Purpose: +//! Implements PHP `array_slice()` eval support and shared slice-bound helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` and splice helpers. +//! +//! Key details: +//! - Negative offsets and lengths are normalized to bounded source positions. + +use super::super::super::super::*; +use super::super::super::*; + +/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. +pub(in crate::interpreter) fn eval_builtin_array_slice( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array, offset] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_array_slice_result(array, offset, None, values) + } + [array, offset, length] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_slice_result(array, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_slice()` result with PHP offset and length bounds. +pub(in crate::interpreter) fn eval_array_slice_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + + let mut result = values.array_new(end.saturating_sub(start))?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + +/// Converts a PHP array-slice offset into a bounded source position. +pub(in crate::interpreter) fn eval_slice_start( + len: usize, + offset: i64, +) -> Result { + if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(offset, len)); + } + + let tail = offset + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(len.saturating_sub(tail)) +} + +/// Converts a PHP array-slice length into a bounded exclusive end position. +pub(in crate::interpreter) fn eval_slice_end( + len: usize, + start: usize, + length: i64, +) -> Result { + if length >= 0 { + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(start.saturating_add(length), len)); + } + + let tail = length + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(usize::max(start, len.saturating_sub(tail))) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/unique.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/unique.rs new file mode 100644 index 0000000000..6d2708fb87 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/unique.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Implements PHP `array_unique()` eval support. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::filters` re-exports. +//! +//! Key details: +//! - Values are compared through PHP's default string comparison mode. + +use super::super::super::super::*; + +/// Evaluates PHP `array_unique()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_unique( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_unique_result(array, values) +} + +/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. +pub(in crate::interpreter) fn eval_array_unique_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut seen = Vec::>::with_capacity(len); + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let unique_key = values.string_bytes(value)?; + if seen.iter().any(|seen_key| seen_key == &unique_key) { + continue; + } + seen.push(unique_key); + result = values.array_set(result, key, value)?; + } + Ok(result) +} From ac464987f631f9d46477e87a34c1af158b6cea05 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 10:50:31 +0200 Subject: [PATCH 0295/1208] Split eval array sort builtins --- .../builtins/arrays/sort/direct.rs | 141 ++++++++++ .../interpreter/builtins/arrays/sort/mod.rs | 18 ++ .../arrays/{sort.rs => sort/standard.rs} | 263 +----------------- .../interpreter/builtins/arrays/sort/user.rs | 136 +++++++++ 4 files changed, 300 insertions(+), 258 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/sort/direct.rs create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/sort/mod.rs rename crates/elephc-eval/src/interpreter/builtins/arrays/{sort.rs => sort/standard.rs} (56%) create mode 100644 crates/elephc-eval/src/interpreter/builtins/arrays/sort/user.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort/direct.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/sort/direct.rs new file mode 100644 index 0000000000..a05af5a7fc --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/sort/direct.rs @@ -0,0 +1,141 @@ +//! Purpose: +//! Binds direct by-reference array sort calls before delegating to sort engines. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::sort` re-exports. +//! +//! Key details: +//! - Direct calls extract a writable variable cell and write back the sorted +//! replacement while preserving source-order evaluation of callback arguments. + +use super::super::super::super::*; +use super::*; + +/// Evaluates direct by-reference array ordering calls and writes back the array. +pub(in crate::interpreter) fn eval_builtin_array_sort_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array_name = eval_array_sort_direct_arg(args)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_array_sort_replacement(name, array, values)?; + let result = values.bool_value(true)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates direct by-reference user-comparator sort calls and writes back the array. +pub(in crate::interpreter) fn eval_builtin_user_sort_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array_name, callback) = eval_user_sort_direct_args(args, context, scope, values)?; + let Some(entry) = + scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = entry.cell(); + let ownership = entry.flags().ownership; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + let result = values.bool_value(true)?; + for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { + values.release(replaced)?; + } + Ok(result) +} + +/// Evaluates and binds direct user-sort arguments while preserving source order. +pub(in crate::interpreter) fn eval_user_sort_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(String, RuntimeCellHandle), EvalStatus> { + let mut array = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + array = Some(name.clone()); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, callback)) +} + +/// Extracts the direct variable argument accepted by eval array ordering builtins. +pub(in crate::interpreter) fn eval_array_sort_direct_arg( + args: &[EvalCallArg], +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + Ok(name.clone()) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort/mod.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/sort/mod.rs new file mode 100644 index 0000000000..f2bb0ca419 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/sort/mod.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Groups eval array sorting builtins into direct-call binding, user-comparator +//! sorting, and standard sorting engines. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! +//! Key details: +//! - Direct by-reference calls update scope cells, while dynamic by-value dispatch +//! returns sorted replacement arrays plus PHP-compatible warnings. + +mod direct; +mod standard; +mod user; + +pub(in crate::interpreter) use direct::*; +pub(in crate::interpreter) use standard::*; +pub(in crate::interpreter) use user::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/sort/standard.rs similarity index 56% rename from crates/elephc-eval/src/interpreter/builtins/arrays/sort.rs rename to crates/elephc-eval/src/interpreter/builtins/arrays/sort/standard.rs index 7593d2996e..0071c50d06 100644 --- a/crates/elephc-eval/src/interpreter/builtins/arrays/sort.rs +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/sort/standard.rs @@ -1,269 +1,16 @@ //! Purpose: -//! Array and user-sort implementations with PHP key-preservation rules. +//! Implements standard array sorting, key extraction, and natural ordering helpers. //! //! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. +//! - `crate::interpreter::builtins::arrays::sort` re-exports. //! //! Key details: -//! - Array cells remain opaque runtime handles and are manipulated through -//! `RuntimeValueOps`. +//! - Homogeneous sort keys model the eval-supported PHP ordering subset and +//! shuffled output remains deterministic through runtime value operations. +use super::super::super::super::*; use super::super::super::*; use super::super::*; -use super::*; - -/// Evaluates direct by-reference array ordering calls and writes back the array. -pub(in crate::interpreter) fn eval_builtin_array_sort_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let array_name = eval_array_sort_direct_arg(args)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let replacement = eval_array_sort_replacement(name, array, values)?; - let result = values.bool_value(true)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates direct by-reference user-comparator sort calls and writes back the array. -pub(in crate::interpreter) fn eval_builtin_user_sort_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array_name, callback) = eval_user_sort_direct_args(args, context, scope, values)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - - let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; - let result = values.bool_value(true)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } - Ok(result) -} - -/// Evaluates and binds direct user-sort arguments while preserving source order. -pub(in crate::interpreter) fn eval_user_sort_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(String, RuntimeCellHandle), EvalStatus> { - let mut array = None; - let mut callback = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "callback", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - array = Some(name.clone()); - } - "callback" => { - if callback.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - callback = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let array = array.ok_or(EvalStatus::RuntimeFatal)?; - let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, callback)) -} - -/// Returns the dynamic callable result for by-value user-comparator sort calls. -pub(in crate::interpreter) fn eval_user_sort_value_result( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; - values.release(replacement)?; - values.bool_value(true) -} - -/// One source array entry used by eval user-comparator sort routines. -pub(in crate::interpreter) struct EvalUserSortEntry { - source_key: RuntimeCellHandle, - value: RuntimeCellHandle, -} - -/// Builds the sorted replacement array for user-comparator sort builtins. -pub(in crate::interpreter) fn eval_user_sort_replacement( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_name(callback, values)?; - let mut entries = eval_user_sort_entries(array, values)?; - eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; - if name == "usort" { - return eval_user_sort_reindex_result(entries, values); - } - eval_user_sort_preserve_key_result(entries, values) -} - -/// Collects source keys and values from one eval array for user sorting. -pub(in crate::interpreter) fn eval_user_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - entries.push(EvalUserSortEntry { source_key, value }); - } - Ok(entries) -} - -/// Sorts entries by repeatedly invoking the PHP comparator callback. -pub(in crate::interpreter) fn eval_user_sort_entries_in_place( - name: &str, - callback: &str, - entries: &mut [EvalUserSortEntry], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for pass in 0..entries.len() { - let upper = entries.len().saturating_sub(pass + 1); - for index in 0..upper { - let comparison = eval_user_sort_compare( - name, - callback, - &entries[index], - &entries[index + 1], - context, - values, - )?; - if comparison > 0 { - entries.swap(index, index + 1); - } - } - } - Ok(()) -} - -/// Invokes one user-sort comparator and returns its integer ordering result. -pub(in crate::interpreter) fn eval_user_sort_compare( - name: &str, - callback: &str, - left: &EvalUserSortEntry, - right: &EvalUserSortEntry, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let args = if name == "uksort" { - vec![left.source_key, right.source_key] - } else { - vec![left.value, right.value] - }; - let result = eval_callable_with_values(callback, args, context, values)?; - eval_int_value(result, values) -} - -/// Builds the reindexed result for `usort()`. -pub(in crate::interpreter) fn eval_user_sort_reindex_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(entries.len())?; - for (index, entry) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, entry.value)?; - } - Ok(result) -} - -/// Builds the key-preserving result for `uksort()` and `uasort()`. -pub(in crate::interpreter) fn eval_user_sort_preserve_key_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(entries.len())?; - for entry in entries { - result = values.array_set(result, entry.source_key, entry.value)?; - } - Ok(result) -} - -/// Extracts the direct variable argument accepted by eval array ordering builtins. -pub(in crate::interpreter) fn eval_array_sort_direct_arg( - args: &[EvalCallArg], -) -> Result { - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - Ok(name.clone()) -} /// Returns the dynamic callable result for by-value array ordering calls. pub(in crate::interpreter) fn eval_array_sort_value_result( diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort/user.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/sort/user.rs new file mode 100644 index 0000000000..2fef10ae00 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/sort/user.rs @@ -0,0 +1,136 @@ +//! Purpose: +//! Implements user-comparator array sorting for `usort`, `uasort`, and `uksort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays::sort` re-exports. +//! +//! Key details: +//! - Comparator callbacks are invoked through the eval context and their integer +//! return values drive a stable entry ordering. + +use super::super::super::super::*; +use super::super::super::*; + +/// Returns the dynamic callable result for by-value user-comparator sort calls. +pub(in crate::interpreter) fn eval_user_sort_value_result( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + values.release(replacement)?; + values.bool_value(true) +} + +/// One source array entry used by eval user-comparator sort routines. +pub(in crate::interpreter) struct EvalUserSortEntry { + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for user-comparator sort builtins. +pub(in crate::interpreter) fn eval_user_sort_replacement( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_name(callback, values)?; + let mut entries = eval_user_sort_entries(array, values)?; + eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; + if name == "usort" { + return eval_user_sort_reindex_result(entries, values); + } + eval_user_sort_preserve_key_result(entries, values) +} + +/// Collects source keys and values from one eval array for user sorting. +pub(in crate::interpreter) fn eval_user_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + entries.push(EvalUserSortEntry { source_key, value }); + } + Ok(entries) +} + +/// Sorts entries by repeatedly invoking the PHP comparator callback. +pub(in crate::interpreter) fn eval_user_sort_entries_in_place( + name: &str, + callback: &str, + entries: &mut [EvalUserSortEntry], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for pass in 0..entries.len() { + let upper = entries.len().saturating_sub(pass + 1); + for index in 0..upper { + let comparison = eval_user_sort_compare( + name, + callback, + &entries[index], + &entries[index + 1], + context, + values, + )?; + if comparison > 0 { + entries.swap(index, index + 1); + } + } + } + Ok(()) +} + +/// Invokes one user-sort comparator and returns its integer ordering result. +pub(in crate::interpreter) fn eval_user_sort_compare( + name: &str, + callback: &str, + left: &EvalUserSortEntry, + right: &EvalUserSortEntry, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = if name == "uksort" { + vec![left.source_key, right.source_key] + } else { + vec![left.value, right.value] + }; + let result = eval_callable_with_values(callback, args, context, values)?; + eval_int_value(result, values) +} + +/// Builds the reindexed result for `usort()`. +pub(in crate::interpreter) fn eval_user_sort_reindex_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds the key-preserving result for `uksort()` and `uasort()`. +pub(in crate::interpreter) fn eval_user_sort_preserve_key_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} From 975ac1f57b1318beacd8868520d7a6a0537ed706 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 11:21:38 +0200 Subject: [PATCH 0296/1208] Split eval parser and interpreter tests --- crates/elephc-eval/src/interpreter/tests.rs | 7246 ----------------- .../src/interpreter/tests/array_literals.rs | 166 + .../interpreter/tests/builtins_arrays_core.rs | 415 + .../tests/builtins_arrays_iterators.rs | 140 + .../interpreter/tests/builtins_arrays_sets.rs | 469 ++ .../tests/builtins_filesystem_metadata.rs | 274 + .../tests/builtins_filesystem_ops.rs | 277 + .../src/interpreter/tests/builtins_json.rs | 285 + .../tests/builtins_math_formatting.rs | 323 + .../src/interpreter/tests/builtins_scalars.rs | 227 + .../tests/builtins_strings_binary.rs | 235 + .../tests/builtins_strings_encoding.rs | 326 + .../tests/builtins_strings_text.rs | 225 + .../src/interpreter/tests/builtins_symbols.rs | 293 + .../tests/builtins_system_network.rs | 418 + .../src/interpreter/tests/control_flow.rs | 404 + .../elephc-eval/src/interpreter/tests/core.rs | 476 ++ .../src/interpreter/tests/dynamic_calls.rs | 318 + .../src/interpreter/tests/expressions.rs | 340 + .../interpreter/tests/functions_namespaces.rs | 397 + .../elephc-eval/src/interpreter/tests/mod.rs | 33 + .../src/interpreter/tests/native_scope.rs | 214 + .../interpreter/tests/support/array_ops.rs | 145 + .../src/interpreter/tests/support/cell_ops.rs | 86 + .../interpreter/tests/support/conversions.rs | 155 + .../tests/support/lifecycle_ops.rs | 36 + .../src/interpreter/tests/support/mod.rs | 124 + .../interpreter/tests/support/numeric_ops.rs | 296 + .../interpreter/tests/support/object_ops.rs | 236 + .../interpreter/tests/support/runtime_ops.rs | 360 + crates/elephc-eval/src/parser/tests.rs | 1803 ---- .../src/parser/tests/arrays_objects.rs | 215 + .../src/parser/tests/assignments.rs | 288 + crates/elephc-eval/src/parser/tests/calls.rs | 198 + .../src/parser/tests/classes_errors.rs | 79 + .../src/parser/tests/control_statements.rs | 173 + .../src/parser/tests/exceptions_control.rs | 277 + .../src/parser/tests/magic_comments.rs | 107 + crates/elephc-eval/src/parser/tests/mod.rs | 22 + .../src/parser/tests/namespaces.rs | 197 + .../elephc-eval/src/parser/tests/operators.rs | 258 + .../elephc-eval/src/parser/tests/support.rs | 15 + 42 files changed, 9522 insertions(+), 9049 deletions(-) delete mode 100644 crates/elephc-eval/src/interpreter/tests.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/array_literals.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_arrays_core.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_arrays_iterators.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_arrays_sets.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_filesystem_metadata.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_json.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_math_formatting.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_strings_text.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/control_flow.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/core.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/expressions.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/functions_namespaces.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/native_scope.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/support/array_ops.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/support/conversions.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/support/lifecycle_ops.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/support/mod.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/support/numeric_ops.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/support/object_ops.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs delete mode 100644 crates/elephc-eval/src/parser/tests.rs create mode 100644 crates/elephc-eval/src/parser/tests/arrays_objects.rs create mode 100644 crates/elephc-eval/src/parser/tests/assignments.rs create mode 100644 crates/elephc-eval/src/parser/tests/calls.rs create mode 100644 crates/elephc-eval/src/parser/tests/classes_errors.rs create mode 100644 crates/elephc-eval/src/parser/tests/control_statements.rs create mode 100644 crates/elephc-eval/src/parser/tests/exceptions_control.rs create mode 100644 crates/elephc-eval/src/parser/tests/magic_comments.rs create mode 100644 crates/elephc-eval/src/parser/tests/mod.rs create mode 100644 crates/elephc-eval/src/parser/tests/namespaces.rs create mode 100644 crates/elephc-eval/src/parser/tests/operators.rs create mode 100644 crates/elephc-eval/src/parser/tests/support.rs diff --git a/crates/elephc-eval/src/interpreter/tests.rs b/crates/elephc-eval/src/interpreter/tests.rs deleted file mode 100644 index 858fa1c4af..0000000000 --- a/crates/elephc-eval/src/interpreter/tests.rs +++ /dev/null @@ -1,7246 +0,0 @@ -//! Purpose: -//! Interpreter unit tests for EvalIR execution against fake runtime values. -//! The tests exercise scope mutation, builtins, function calls, arrays, objects, -//! control flow, and error propagation without linking generated runtime hooks. -//! -//! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. -//! -//! Key details: -//! - FakeOps owns opaque test cells and mirrors enough runtime behavior for eval. -//! - Test fixtures parse PHP eval fragments before executing them through EvalIR. - -use std::collections::HashMap; -use std::ffi::c_void; - -use crate::parser::parse_fragment; -use crate::value::RuntimeCell; - -use super::*; - -/// Test-only array key representation for fake indexed and associative arrays. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -enum FakeKey { - Int(i64), - String(String), -} - -/// Test-only runtime value representation used behind opaque cell handles. -#[derive(Clone, Debug, PartialEq)] -enum FakeValue { - Null, - Bool(bool), - Int(i64), - Float(f64), - String(String), - Bytes(Vec), - Array(Vec), - Assoc(Vec<(FakeKey, RuntimeCellHandle)>), - Object(Vec<(String, RuntimeCellHandle)>), - Iterator { len: i64, position: i64 }, - Resource(i64), -} - -/// Test runtime hooks that allocate stable fake handles and record echo output. -#[derive(Default)] -struct FakeOps { - next_id: usize, - values: HashMap, - output: String, - releases: Vec, - warnings: Vec, -} - -impl FakeOps { - /// Allocates one fake runtime cell and returns its opaque handle. - fn alloc(&mut self, value: FakeValue) -> RuntimeCellHandle { - self.next_id += 1; - let id = self.next_id; - self.values.insert(id, value); - RuntimeCellHandle::from_raw(id as *mut RuntimeCell) - } - - /// Reads a fake runtime cell by opaque handle. - fn get(&self, handle: RuntimeCellHandle) -> FakeValue { - let id = handle.as_ptr() as usize; - self.values.get(&id).cloned().expect("fake cell missing") - } - - /// Converts a fake runtime cell into a normalized fake PHP array key. - fn key(&self, handle: RuntimeCellHandle) -> Result { - let value = self.get(handle); - match value { - FakeValue::Int(value) => Ok(FakeKey::Int(value)), - FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) - .map(FakeKey::Int) - .map_or_else(|| Ok(FakeKey::String(value)), Ok), - FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) - .map(FakeKey::Int) - .map_or_else( - || { - Ok(FakeKey::String( - String::from_utf8_lossy(&value).into_owned(), - )) - }, - Ok, - ), - FakeValue::Null => Ok(FakeKey::String(String::new())), - value => Ok(FakeKey::Int(self.fake_int(&value))), - } - } - - /// Allocates a fake runtime cell for an existing PHP array key. - fn alloc_key(&mut self, key: &FakeKey) -> Result { - match key { - FakeKey::Int(value) => self.int(*value), - FakeKey::String(value) => self.string(value), - } - } - - /// Finds a fake object property by insertion-order name. - fn object_property( - properties: &[(String, RuntimeCellHandle)], - name: &str, - ) -> Option { - properties - .iter() - .find_map(|(property, value)| (property == name).then_some(*value)) - } -} - -impl RuntimeValueOps for FakeOps { - /// Creates a fake indexed array cell. - fn array_new(&mut self, capacity: usize) -> Result { - Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) - } - - /// Creates a fake associative array cell. - fn assoc_new(&mut self, _capacity: usize) -> Result { - Ok(self.alloc(FakeValue::Assoc(Vec::new()))) - } - - /// Reads one fake indexed array element. - fn array_get( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - ) -> Result { - let key = self.key(index)?; - match self.get(array) { - FakeValue::Array(elements) => { - let FakeKey::Int(index) = key else { - return self.null(); - }; - if index < 0 { - return self.null(); - } - elements - .get(index as usize) - .copied() - .map_or_else(|| self.null(), Ok) - } - FakeValue::Assoc(entries) => entries - .iter() - .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) - .map_or_else(|| self.null(), Ok), - _ => self.null(), - } - } - - /// Checks whether a fake array has the requested key without reading its value. - fn array_key_exists( - &mut self, - key: RuntimeCellHandle, - array: RuntimeCellHandle, - ) -> Result { - let key = self.key(key)?; - let exists = match self.get(array) { - FakeValue::Array(elements) => { - matches!(key, FakeKey::Int(index) if index >= 0 && (index as usize) < elements.len()) - } - FakeValue::Assoc(entries) => entries.iter().any(|(entry_key, _)| entry_key == &key), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - self.bool_value(exists) - } - - /// Returns one fake foreach key by insertion-order position. - fn array_iter_key( - &mut self, - array: RuntimeCellHandle, - position: usize, - ) -> Result { - match self.get(array) { - FakeValue::Array(elements) if position < elements.len() => self.int(position as i64), - FakeValue::Assoc(entries) => { - let Some((key, _)) = entries.get(position) else { - return self.null(); - }; - self.alloc_key(key) - } - FakeValue::Array(_) => self.null(), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Writes one fake indexed or associative array element. - fn array_set( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - value: RuntimeCellHandle, - ) -> Result { - let key = self.key(index)?; - let id = array.as_ptr() as usize; - match self.values.get_mut(&id) { - Some(FakeValue::Array(elements)) => { - let FakeKey::Int(index) = key else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if index < 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let index = index as usize; - while elements.len() <= index { - elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); - } - elements[index] = value; - } - Some(FakeValue::Assoc(entries)) => { - if let Some((_, existing_value)) = - entries.iter_mut().find(|(entry_key, _)| entry_key == &key) - { - *existing_value = value; - } else { - entries.push((key, value)); - } - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - Ok(array) - } - - /// Reads one fake object property by name. - fn property_get( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => properties - .iter() - .find_map(|(name, value)| (name == property).then_some(*value)) - .map_or_else(|| self.null(), Ok), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Writes one fake object property by name. - fn property_set( - &mut self, - object: RuntimeCellHandle, - property: &str, - value: RuntimeCellHandle, - ) -> Result<(), EvalStatus> { - let id = object.as_ptr() as usize; - let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if let Some((_, existing_value)) = properties.iter_mut().find(|(name, _)| name == property) - { - *existing_value = value; - } else { - properties.push((property.to_string(), value)); - } - Ok(()) - } - - /// Returns the number of fake object properties in insertion order. - fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { - match self.get(object) { - FakeValue::Object(properties) => Ok(properties.len()), - FakeValue::Iterator { .. } => Ok(0), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Returns one fake object property key by insertion-order position. - fn object_property_iter_key( - &mut self, - object: RuntimeCellHandle, - position: usize, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => { - let Some((name, _)) = properties.get(position) else { - return self.null(); - }; - self.string(name) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Calls one fake object method by name. - fn method_call( - &mut self, - object: RuntimeCellHandle, - method: &str, - args: Vec, - ) -> Result { - match (self.get(object), method) { - (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { - let id = object.as_ptr() as usize; - let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - *position = 0; - self.null() - } - (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { - self.bool_value(position < len) - } - (FakeValue::Iterator { .. }, "next") if args.is_empty() => { - let id = object.as_ptr() as usize; - let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - *position += 1; - self.null() - } - (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), - (FakeValue::Object(properties), "read_x") => { - if !args.is_empty() { - return Err(EvalStatus::UnsupportedConstruct); - } - Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "add_x") => { - let [arg] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; - let FakeValue::Int(x) = self.get(x) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(arg) = self.get(*arg) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(x + arg) - } - (FakeValue::Object(properties), "add2_x") => { - let [left, right] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; - let FakeValue::Int(x) = self.get(x) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(left) = self.get(*left) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(right) = self.get(*right) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(x + left + right) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Creates one fake object for eval `new` unit tests. - fn new_object(&mut self, _class_name: &str) -> Result { - Ok(self.alloc(FakeValue::Object(Vec::new()))) - } - - /// Applies fake constructor side effects for eval `new` unit tests. - fn construct_object( - &mut self, - object: RuntimeCellHandle, - args: Vec, - ) -> Result<(), EvalStatus> { - let id = object.as_ptr() as usize; - let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if let Some(first) = args.first().copied() { - if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { - *value = first; - } else { - properties.push(("x".to_string(), first)); - } - } - Ok(()) - } - - /// Reports one fake AOT class for eval `class_exists` unit tests. - fn class_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownClass")) - } - - /// Reports one fake AOT interface for eval `interface_exists` unit tests. - fn interface_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownInterface")) - } - - /// Reports one fake AOT trait for eval `trait_exists` unit tests. - fn trait_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownTrait")) - } - - /// Reports one fake AOT enum for eval `enum_exists` unit tests. - fn enum_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownEnum")) - } - - /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. - fn object_is_a( - &mut self, - object_or_class: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - ) -> Result { - match self.get(object_or_class) { - FakeValue::Object(_) - if target_class.eq_ignore_ascii_case("Exception") - || target_class.eq_ignore_ascii_case("Throwable") => - { - Ok(!exclude_self) - } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { - Ok(!exclude_self) - } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), - _ => Ok(false), - } - } - - /// Returns a fake PHP class name for object-tagged test values. - fn object_class_name( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - match self.get(object) { - FakeValue::Object(_) => self.string("stdClass"), - FakeValue::Iterator { .. } => self.string("Iterator"), - _ => Err(EvalStatus::RuntimeFatal), - } - } - - /// Returns fake parent-class names for eval introspection unit tests. - fn parent_class_name( - &mut self, - object_or_class: RuntimeCellHandle, - ) -> Result { - match self.get(object_or_class) { - FakeValue::Object(_) => self.string("ParentClass"), - FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { - self.string("ParentClass") - } - _ => self.string(""), - } - } - - /// Returns the visible element count for fake array values. - fn array_len(&mut self, array: RuntimeCellHandle) -> Result { - match self.get(array) { - FakeValue::Array(elements) => Ok(elements.len()), - FakeValue::Assoc(entries) => Ok(entries.len()), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Returns whether a fake runtime cell is an indexed or associative array. - fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { - Ok(matches!( - self.get(value), - FakeValue::Array(_) | FakeValue::Assoc(_) - )) - } - - /// Returns whether a fake runtime cell is null. - fn is_null(&mut self, value: RuntimeCellHandle) -> Result { - Ok(matches!(self.get(value), FakeValue::Null)) - } - - /// Returns the fake runtime tag corresponding to a test value. - fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { - Ok(match self.get(value) { - FakeValue::Int(_) => EVAL_TAG_INT, - FakeValue::String(_) | FakeValue::Bytes(_) => EVAL_TAG_STRING, - FakeValue::Float(_) => EVAL_TAG_FLOAT, - FakeValue::Bool(_) => EVAL_TAG_BOOL, - FakeValue::Array(_) => EVAL_TAG_ARRAY, - FakeValue::Assoc(_) => EVAL_TAG_ASSOC, - FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, - FakeValue::Resource(_) => EVAL_TAG_RESOURCE, - FakeValue::Null => EVAL_TAG_NULL, - }) - } - - /// Returns the fake object handle as a stable object identity. - fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { - match self.get(object) { - FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), - _ => Err(EvalStatus::RuntimeFatal), - } - } - - /// Records fake releases without freeing handles needed for assertions. - fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - self.releases.push(value); - Ok(()) - } - - /// Returns the same fake handle because fake cells do not refcount. - fn retain(&mut self, value: RuntimeCellHandle) -> Result { - Ok(value) - } - - /// Records fake PHP warnings without writing to stderr. - fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { - self.warnings.push(message.to_string()); - Ok(()) - } - - /// Creates a fake null cell. - fn null(&mut self) -> Result { - Ok(self.alloc(FakeValue::Null)) - } - - /// Creates a fake bool cell. - fn bool_value(&mut self, value: bool) -> Result { - Ok(self.alloc(FakeValue::Bool(value))) - } - - /// Creates a fake int cell. - fn int(&mut self, value: i64) -> Result { - Ok(self.alloc(FakeValue::Int(value))) - } - - /// Creates a fake float cell. - fn float(&mut self, value: f64) -> Result { - Ok(self.alloc(FakeValue::Float(value))) - } - - /// Creates a fake string cell. - fn string(&mut self, value: &str) -> Result { - Ok(self.alloc(FakeValue::String(value.to_string()))) - } - - /// Creates a fake string cell from raw PHP bytes. - fn string_bytes_value(&mut self, value: &[u8]) -> Result { - match std::str::from_utf8(value) { - Ok(value) => self.string(value), - Err(_) => Ok(self.alloc(FakeValue::Bytes(value.to_vec()))), - } - } - - /// Casts a fake runtime cell to a fake integer cell. - fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - let value = self.fake_int(&value); - self.int(value) - } - - /// Casts a fake runtime cell to a fake float cell. - fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - let value = self.fake_numeric(&value); - self.float(value) - } - - /// Casts a fake runtime cell to a fake string cell. - fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.stringify(value); - self.string(&value) - } - - /// Casts a fake runtime cell to a fake boolean cell. - fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - let value = self.fake_truthy(&value); - self.bool_value(value) - } - - /// Computes fake PHP absolute value while preserving float payloads. - fn abs(&mut self, value: RuntimeCellHandle) -> Result { - match self.get(value) { - FakeValue::Float(value) => self.float(value.abs()), - value => self.int(self.fake_int(&value).wrapping_abs()), - } - } - - /// Computes fake PHP ceiling through numeric conversion as a float result. - fn ceil(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).ceil()) - } - - /// Computes fake PHP floor through numeric conversion as a float result. - fn floor(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).floor()) - } - - /// Computes fake PHP square root through numeric conversion as a float result. - fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.get(value); - self.float(self.fake_numeric(&value).sqrt()) - } - - /// Reverses a fake string byte-wise for interpreter tests. - fn strrev(&mut self, value: RuntimeCellHandle) -> Result { - let mut bytes = self.stringify(value).into_bytes(); - bytes.reverse(); - let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - self.string(&value) - } - - /// Divides fake numeric cells with PHP `fdiv()` zero handling. - fn fdiv( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left / right) - } - - /// Computes fake floating-point modulo for interpreter tests. - fn fmod( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left % right) - } - - /// Adds fake numeric cells for interpreter tests. - fn add( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), - (left, right) => self.float(self.fake_numeric(&left) + self.fake_numeric(&right)), - } - } - - /// Subtracts fake numeric cells for interpreter tests. - fn sub( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), - (left, right) => self.float(self.fake_numeric(&left) - self.fake_numeric(&right)), - } - } - - /// Multiplies fake numeric cells for interpreter tests. - fn mul( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - match (self.get(left), self.get(right)) { - (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), - (left, right) => self.float(self.fake_numeric(&left) * self.fake_numeric(&right)), - } - } - - /// Divides fake numeric cells for interpreter tests. - fn div( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let right = self.fake_numeric(&self.get(right)); - if right == 0.0 { - return Err(EvalStatus::RuntimeFatal); - } - let left = self.fake_numeric(&self.get(left)); - self.float(left / right) - } - - /// Computes fake integer modulo for interpreter tests. - fn modulo( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let right = self.fake_int(&self.get(right)); - if right == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let left = self.fake_int(&self.get(left)); - self.int(left % right) - } - - /// Raises fake numeric cells for interpreter tests. - fn pow( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_numeric(&self.get(left)); - let right = self.fake_numeric(&self.get(right)); - self.float(left.powf(right)) - } - - /// Rounds fake numeric cells with PHP's optional decimal precision. - fn round( - &mut self, - value: RuntimeCellHandle, - precision: Option, - ) -> Result { - let value = self.fake_numeric(&self.get(value)); - let precision = precision - .map(|precision| self.fake_int(&self.get(precision))) - .unwrap_or(0); - let multiplier = 10_f64.powf(precision as f64); - self.float((value * multiplier).round() / multiplier) - } - - /// Applies fake integer bitwise and shift operations for interpreter tests. - fn bitwise( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.fake_int(&self.get(left)); - let right = self.fake_int(&self.get(right)); - let value = match op { - EvalBinOp::BitAnd => left & right, - EvalBinOp::BitOr => left | right, - EvalBinOp::BitXor => left ^ right, - EvalBinOp::ShiftLeft => { - if right < 0 { - return Err(EvalStatus::RuntimeFatal); - } - left.wrapping_shl(right as u32) - } - EvalBinOp::ShiftRight => { - if right < 0 { - return Err(EvalStatus::RuntimeFatal); - } - left.wrapping_shr(right as u32) - } - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - self.int(value) - } - - /// Applies fake integer bitwise NOT for interpreter tests. - fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { - let value = self.fake_int(&self.get(value)); - self.int(!value) - } - - /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. - fn concat( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let mut left = self.string_bytes_for_value(&self.get(left)); - let right = self.string_bytes_for_value(&self.get(right)); - left.extend_from_slice(&right); - self.string_bytes_value(&left) - } - - /// Compares fake scalar cells and returns a fake PHP boolean. - fn compare( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let result = match op { - EvalBinOp::LooseEq => self.loose_eq(left, right), - EvalBinOp::LooseNotEq => !self.loose_eq(left, right), - EvalBinOp::StrictEq => self.strict_eq(left, right), - EvalBinOp::StrictNotEq => !self.strict_eq(left, right), - EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, - EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, - EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, - EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, - EvalBinOp::Add - | EvalBinOp::Sub - | EvalBinOp::Mul - | EvalBinOp::Div - | EvalBinOp::Mod - | EvalBinOp::Pow - | EvalBinOp::BitAnd - | EvalBinOp::BitOr - | EvalBinOp::BitXor - | EvalBinOp::ShiftLeft - | EvalBinOp::ShiftRight - | EvalBinOp::Concat - | EvalBinOp::Spaceship - | EvalBinOp::LogicalAnd - | EvalBinOp::LogicalOr - | EvalBinOp::LogicalXor => { - return Err(EvalStatus::UnsupportedConstruct); - } - }; - self.bool_value(result) - } - - /// Compares fake numeric cells and returns a PHP spaceship integer. - fn spaceship( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - let left = self.numeric(left)?; - let right = self.numeric(right)?; - let value = if left < right { - -1 - } else if left > right { - 1 - } else { - 0 - }; - self.int(value) - } - - /// Appends fake echo output for interpreter tests. - fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - let value = self.stringify(value); - self.output.push_str(&value); - Ok(()) - } - - /// Casts one fake runtime cell to bytes for nested eval parsing. - fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { - Ok(self.string_bytes_for_value(&self.get(value))) - } - - /// Returns PHP-like truthiness for fake runtime cells. - fn truthy(&mut self, value: RuntimeCellHandle) -> Result { - Ok(match self.get(value) { - FakeValue::Null => false, - FakeValue::Bool(value) => value, - FakeValue::Int(value) => value != 0, - FakeValue::Float(value) => value != 0.0, - FakeValue::String(value) => !value.is_empty() && value != "0", - FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", - FakeValue::Array(value) => !value.is_empty(), - FakeValue::Assoc(value) => !value.is_empty(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => true, - FakeValue::Resource(_) => true, - }) - } -} - -impl FakeOps { - /// Compares fake scalar values with the same loose rules covered by eval tests. - fn loose_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { - match (self.get(left), self.get(right)) { - (FakeValue::Bool(left), right) => left == self.fake_truthy(&right), - (left, FakeValue::Bool(right)) => self.fake_truthy(&left) == right, - (FakeValue::Null, FakeValue::Null) => true, - (FakeValue::Null, FakeValue::String(value)) - | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), - (FakeValue::Null, FakeValue::Bytes(value)) - | (FakeValue::Bytes(value), FakeValue::Null) => value.is_empty(), - (FakeValue::String(left), FakeValue::String(right)) => { - match (left.parse::(), right.parse::()) { - (Ok(left), Ok(right)) => left == right, - _ => left == right, - } - } - (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, - (FakeValue::String(left), FakeValue::Bytes(right)) - | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, - (FakeValue::String(left), right) => left - .parse::() - .is_ok_and(|left| left == self.fake_numeric(&right)), - (FakeValue::Bytes(left), right) => std::str::from_utf8(&left) - .ok() - .and_then(|left| left.parse::().ok()) - .is_some_and(|left| left == self.fake_numeric(&right)), - (left, FakeValue::String(right)) => right - .parse::() - .is_ok_and(|right| self.fake_numeric(&left) == right), - (left, FakeValue::Bytes(right)) => std::str::from_utf8(&right) - .ok() - .and_then(|right| right.parse::().ok()) - .is_some_and(|right| self.fake_numeric(&left) == right), - (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), - } - } - - /// Compares fake scalar values by PHP strict tag and payload equality. - fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { - match (self.get(left), self.get(right)) { - (FakeValue::Null, FakeValue::Null) => true, - (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, - (FakeValue::Int(left), FakeValue::Int(right)) => left == right, - (FakeValue::Float(left), FakeValue::Float(right)) => left == right, - (FakeValue::String(left), FakeValue::String(right)) => left == right, - (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, - (FakeValue::String(left), FakeValue::Bytes(right)) - | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, - (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, - _ => false, - } - } - - /// Converts one fake scalar cell to a numeric value for comparison tests. - fn numeric(&self, handle: RuntimeCellHandle) -> Result { - Ok(self.fake_numeric(&self.get(handle))) - } - - /// Converts a fake value to the numeric scalar used by comparison tests. - fn fake_numeric(&self, value: &FakeValue) -> f64 { - match value { - FakeValue::Null => 0.0, - FakeValue::Bool(false) => 0.0, - FakeValue::Bool(true) => 1.0, - FakeValue::Int(value) => *value as f64, - FakeValue::Float(value) => *value, - FakeValue::String(value) => value.parse::().unwrap_or(0.0), - FakeValue::Bytes(value) => std::str::from_utf8(value) - .ok() - .and_then(|value| value.parse::().ok()) - .unwrap_or(0.0), - FakeValue::Array(value) => value.len() as f64, - FakeValue::Assoc(value) => value.len() as f64, - FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, - FakeValue::Resource(value) => (*value + 1) as f64, - } - } - - /// Converts a fake value to the integer scalar used by modulo tests. - fn fake_int(&self, value: &FakeValue) -> i64 { - self.fake_numeric(value) as i64 - } - - /// Returns fake PHP truthiness for already-loaded test values. - fn fake_truthy(&self, value: &FakeValue) -> bool { - match value { - FakeValue::Null => false, - FakeValue::Bool(value) => *value, - FakeValue::Int(value) => *value != 0, - FakeValue::Float(value) => *value != 0.0, - FakeValue::String(value) => !value.is_empty() && value != "0", - FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", - FakeValue::Array(value) => !value.is_empty(), - FakeValue::Assoc(value) => !value.is_empty(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => true, - FakeValue::Resource(_) => true, - } - } - - /// Converts a fake runtime cell to a PHP-like string for test echo/concat. - fn stringify(&self, handle: RuntimeCellHandle) -> String { - match self.get(handle) { - FakeValue::Null => String::new(), - FakeValue::Bool(false) => String::new(), - FakeValue::Bool(true) => "1".to_string(), - FakeValue::Int(value) => value.to_string(), - FakeValue::Float(value) => value.to_string(), - FakeValue::String(value) => value, - FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), - FakeValue::Array(_) => "Array".to_string(), - FakeValue::Assoc(_) => "Array".to_string(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), - FakeValue::Resource(value) => format!("Resource id #{}", value + 1), - } - } - - /// Converts a fake PHP value to string bytes while preserving binary strings. - fn string_bytes_for_value(&self, value: &FakeValue) -> Vec { - match value { - FakeValue::String(value) => value.as_bytes().to_vec(), - FakeValue::Bytes(value) => value.clone(), - value => self.stringify_value(value).into_bytes(), - } - } - - /// Converts one loaded fake PHP value to display text for byte coercions. - fn stringify_value(&self, value: &FakeValue) -> String { - match value { - FakeValue::Null => String::new(), - FakeValue::Bool(false) => String::new(), - FakeValue::Bool(true) => "1".to_string(), - FakeValue::Int(value) => value.to_string(), - FakeValue::Float(value) => value.to_string(), - FakeValue::String(value) => value.clone(), - FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), - FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), - FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), - FakeValue::Resource(value) => format!("Resource id #{}", value + 1), - } - } -} - -/// Test native invoker that returns the descriptor pointer as a runtime cell. -unsafe extern "C" fn fake_native_return_descriptor( - descriptor: *mut c_void, - _args: *mut RuntimeCell, -) -> *mut RuntimeCell { - descriptor.cast() -} - -/// Verifies assignment writes a named scope entry and return reads it back. -#[test] -fn execute_program_stores_and_returns_scope_value() { - let program = parse_fragment(b"$x = 3; return $x + 4;").expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.get(x), FakeValue::Int(3)); - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies reference assignment aliases variable names and writes through the alias. -#[test] -fn execute_program_reference_assignment_updates_source_variable() { - let program = parse_fragment(b"$x = 1; $alias =& $x; $alias = 5; return $x;") - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - let alias = scope - .visible_cell("alias") - .expect("scope should contain alias"); - - assert_eq!(x, alias); - assert_eq!(values.get(x), FakeValue::Int(5)); - assert_eq!(values.get(result), FakeValue::Int(5)); -} - -/// Verifies eval `throw` exits the program with a retained Throwable cell. -#[test] -fn execute_program_propagates_throw_as_uncaught_outcome() { - let program = - parse_fragment(br#"throw new Exception("eval boom");"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => { - assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)); - } - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } -} - -/// Verifies eval `try/catch` catches a thrown object and binds the catch variable. -#[test] -fn execute_program_catches_throwable_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable $caught) { - return $caught->answer(); -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); -} - -/// Verifies eval `catch (Throwable)` can handle a throw without binding a variable. -#[test] -fn execute_program_catches_throwable_without_variable_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable) { - return 9; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let released = values - .releases - .first() - .copied() - .expect("unbound catch should release the thrown object"); - - assert_eq!(scope.visible_cell("caught"), None); - assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(9)); -} - -/// Verifies eval `catch (Exception)` matches thrown exception objects. -#[test] -fn execute_program_catches_specific_exception_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Exception $caught) { - return $caught->answer(); -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); -} - -/// Verifies eval catch clauses keep source order and skip non-matching types. -#[test] -fn execute_program_skips_non_matching_specific_catch_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (RuntimeException $wrong) { - return 1; -} catch (Exception $caught) { - return 2; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(scope.visible_cell("wrong"), None); - assert_eq!(values.get(result), FakeValue::Int(2)); -} - -/// Verifies union catch clauses test later types in the same catch clause. -#[test] -fn execute_program_catches_union_type_inside_eval() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (RuntimeException|Exception $caught) { - return $caught->answer(); -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let caught = scope - .visible_cell("caught") - .expect("scope should contain catch variable"); - - assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); - assert_eq!(values.get(result), FakeValue::Int(42)); -} - -/// Verifies eval `finally` runs before a pending try-body return is observed. -#[test] -fn execute_program_runs_finally_before_returning_try_value() { - let program = parse_fragment( - br#"try { - return 1; -} finally { - echo "finally"; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "finally"); - assert_eq!(values.get(result), FakeValue::Int(1)); -} - -/// Verifies eval `finally` return values replace pending try-body returns. -#[test] -fn execute_program_finally_return_overrides_try_return() { - let program = parse_fragment( - br#"try { - return 1; -} finally { - return 2; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.releases.len(), 1); -} - -/// Verifies eval `finally` return values replace pending uncaught throws. -#[test] -fn execute_program_finally_return_overrides_uncaught_throw() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} finally { - return 2; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let released = values - .releases - .first() - .copied() - .expect("overridden throw should be released"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); -} - -/// Verifies eval `finally` runs before an uncaught throw leaves the fragment. -#[test] -fn execute_program_runs_finally_before_uncaught_throw_outcome() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} finally { - echo "finally"; -}"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => { - assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)) - } - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } - assert_eq!(values.output, "finally"); -} - -/// Verifies static locals declared inside eval catch blocks persist per function context. -#[test] -fn execute_context_function_persists_static_local_inside_catch() { - let program = parse_fragment( - br#"function dyn($e) { - try { - throw $e; - } catch (Throwable $caught) { - static $n = 0; - $n++; - return $caught->answer() + $n; - } -}"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - let first_thrown = values - .new_object("Exception") - .expect("allocate first fake exception"); - let second_thrown = values - .new_object("Exception") - .expect("allocate second fake exception"); - - let first = execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) - .expect("execute first dynamic function call"); - let second = execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) - .expect("execute second dynamic function call"); - - assert_eq!(values.get(first), FakeValue::Int(43)); - assert_eq!(values.get(second), FakeValue::Int(44)); -} - -/// Verifies static locals declared inside eval finally blocks persist per function context. -#[test] -fn execute_context_function_persists_static_local_inside_finally() { - let program = parse_fragment( - br#"function dyn() { - try { - return 0; - } finally { - static $n = 0; - $n++; - return $n; - } -}"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - - let first = execute_context_function_zero_args(&mut context, "dyn", &mut values) - .expect("execute first dynamic function call"); - let second = execute_context_function_zero_args(&mut context, "dyn", &mut values) - .expect("execute second dynamic function call"); - - assert_eq!(values.get(first), FakeValue::Int(1)); - assert_eq!(values.get(second), FakeValue::Int(2)); -} - -/// Verifies throws from eval-declared functions escape through the shared context. -#[test] -fn execute_context_function_propagates_throw_as_uncaught_outcome() { - let program = - parse_fragment(br#"function dyn($e) { throw $e; }"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("declare dynamic function"); - let thrown = values - .new_object("Exception") - .expect("allocate fake exception"); - - let outcome = execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) - .expect("throw should be an eval function outcome"); - - match outcome { - EvalOutcome::Throwable(value) => assert_eq!(value, thrown), - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } -} - -/// Verifies nested eval preserves the thrown cell while returning an uncaught status. -#[test] -fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { - let program = parse_fragment(br#"eval("throw $e;");"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let thrown = values - .new_object("Exception") - .expect("allocate fake exception"); - scope.set("e", thrown, ScopeCellOwnership::Borrowed); - - let outcome = - execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) - .expect("nested throw should be an eval outcome"); - - match outcome { - EvalOutcome::Throwable(value) => assert_eq!(value, thrown), - EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), - } -} - -/// Verifies eval include resolves caller-relative paths, shares scope, and returns file values. -#[test] -fn execute_program_include_uses_call_site_and_returns_file_result() { - let dir = std::env::temp_dir().join(format!( - "elephc-eval-include-{}-call-site", - std::process::id() - )); - let path = dir.join("piece.php"); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).expect("create include fixture directory"); - std::fs::write( - &path, - format!( - r#">= 3; echo $x; echo ":"; -echo ~0; echo ":"; echo -16 >> 2; -return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:5:4:32:8:-1:-4"); - assert_eq!(values.get(result), FakeValue::Int(21)); -} - -/// Verifies simple variable increment and decrement statements update the scope value. -#[test] -fn execute_program_evaluates_inc_dec_statements() { - let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "1"); - assert_eq!(values.get(i), FakeValue::Int(1)); -} - -/// Verifies echo and unset operate through runtime hooks and scope metadata. -#[test] -fn execute_program_echoes_and_unsets_scope_value() { - let program = - parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let name = values.string(" Ada").expect("create fake string"); - scope.set("name", name, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "hi Ada"); - assert_eq!(values.get(result), FakeValue::Null); - assert!(scope.entry("name").expect("unset marker").flags().unset); -} - -/// Verifies comma-separated echo expressions are executed in source order. -#[test] -fn execute_program_echoes_comma_list() { - let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let b = values.string("b").expect("create fake string"); - scope.set("b", b, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "abc"); -} - -/// Verifies print writes output and returns integer 1. -#[test] -fn execute_program_print_returns_one() { - let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "p"); - assert_eq!(values.get(result), FakeValue::Int(1)); -} - -/// Verifies eval `print_r()` emits supported values and returns true. -#[test] -fn execute_program_dispatches_print_r_builtin() { - let program = parse_fragment( - br#"print_r("x"); echo ":"; -print_r(value: false); echo ":"; -print_r([1, 2]); echo ":"; -$call = call_user_func("print_r", true); -$spread = call_user_func_array("print_r", ["value" => "z"]); -echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; -return function_exists("print_r");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "x::Array\n:1z:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. -#[test] -fn execute_program_dispatches_var_dump_builtin() { - let program = parse_fragment( - br#"var_dump(42); -var_dump("hi"); -var_dump(false); -var_dump(null); -var_dump([10, 20]); -var_dump(["x" => true]); -$call = call_user_func("var_dump", 3.5); -$spread = call_user_func_array("var_dump", ["value" => "z"]); -echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; -return function_exists("var_dump");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - concat!( - "int(42)\n", - "string(2) \"hi\"\n", - "bool(false)\n", - "NULL\n", - "array(2) {\n", - " [0]=>\n", - " int(10)\n", - " [1]=>\n", - " int(20)\n", - "}\n", - "array(1) {\n", - " [\"x\"]=>\n", - " bool(true)\n", - "}\n", - "float(3.5)\n", - "string(1) \"z\"\n", - "call-null:spread-null:", - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval property reads and writes dispatch through runtime hooks. -#[test] -fn execute_program_reads_and_writes_object_property() { - let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(1).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!( - values - .property_get(object, "x") - .map(|value| values.get(value)) - .expect("property should be readable"), - FakeValue::Int(2) - ); -} - -/// Verifies eval method calls dispatch through the runtime method hook. -#[test] -fn execute_program_calls_object_method() { - let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); -} - -/// Verifies eval method calls forward evaluated arguments to the runtime hook. -#[test] -fn execute_program_calls_object_method_with_argument() { - let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} - -/// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. -#[test] -fn execute_program_calls_object_method_with_two_arguments() { - let program = parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(18)); -} - -/// Verifies eval method calls forward numerically unpacked arguments. -#[test] -fn execute_program_calls_object_method_with_spread_arguments() { - let program = - parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(18)); -} - -/// Verifies eval object construction dispatches through runtime hooks. -#[test] -fn execute_program_constructs_named_object() { - let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Object(Vec::new())); -} - -/// Verifies eval object construction passes constructor arguments through runtime hooks. -#[test] -fn execute_program_constructs_named_object_with_args() { - let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let FakeValue::Object(properties) = values.get(result) else { - panic!("expected fake object"); - }; - let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); - - assert_eq!(values.get(x), FakeValue::Int(1)); -} - -/// Verifies eval-declared classes create objects with properties and methods. -#[test] -fn execute_program_constructs_eval_declared_class_with_method() { - let program = parse_fragment( - br#"class DynBox { - public int $x = 1; - public function __construct($x) { $this->x = $x; } - public function bump($n) { $this->x = $this->x + $n; return $this->x; } -} -$box = new DynBox(4); -echo get_class($box); -echo ":"; -echo $box->bump(3); -echo ":"; -echo is_a($box, "DynBox") ? "Y" : "N"; -$call = [$box, "bump"]; -echo call_user_func($call, 1); -echo ":"; -echo call_user_func_array($call, [2]); -echo ":"; -return $box->x;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "DynBox:7:Y8:10:"); - assert_eq!(values.get(result), FakeValue::Int(10)); -} - -/// Verifies if/else executes only the PHP-truthy branch. -#[test] -fn execute_program_if_else_uses_php_truthiness() { - let program = parse_fragment(br#"if ($flag) { $x = "then"; } else { $x = "else"; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.int(0).expect("create fake int"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.get(x), FakeValue::String("else".to_string())); -} - -/// Verifies elseif chains execute the first truthy branch and skip later branches. -#[test] -fn execute_program_elseif_uses_first_truthy_branch() { - let program = - parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; } else { $x = "c"; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let a = values.bool_value(false).expect("create fake bool"); - let b = values.bool_value(true).expect("create fake bool"); - scope.set("a", a, ScopeCellOwnership::Owned); - scope.set("b", b, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.get(x), FakeValue::String("b".to_string())); -} - -/// Verifies while repeats while the condition remains truthy and propagates writes. -#[test] -fn execute_program_while_uses_php_truthiness() { - let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.int(2).expect("create fake int"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let flag = scope - .visible_cell("flag") - .expect("scope should contain flag"); - - assert_eq!(values.output, "2"); - assert_eq!(values.get(flag), FakeValue::Bool(false)); -} - -/// Verifies do/while runs the body before testing the condition. -#[test] -fn execute_program_do_while_runs_body_before_condition() { - let program = parse_fragment(br#"do { echo $i; $i = $i + 1; } while (false);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let i = values.int(0).expect("create fake int"); - scope.set("i", i, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "0"); - assert_eq!(values.get(i), FakeValue::Int(1)); -} - -/// Verifies switch uses loose matching and falls through after the matching case. -#[test] -fn execute_program_switch_matches_and_falls_through() { - let program = - parse_fragment(br#"switch ($x) { case 1: echo "one"; break; case 2: echo "two"; default: echo "default"; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(2).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "twodefault"); -} - -/// Verifies for loops run init, condition, update, and body in PHP order. -#[test] -fn execute_program_for_loop_updates_after_body() { - let program = parse_fragment(br#"for ($i = 3; $i; $i = $i - 1) { echo $i; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "321"); - assert_eq!(values.get(i), FakeValue::Int(0)); -} - -/// Verifies `continue` in a for loop still runs the update clause. -#[test] -fn execute_program_for_continue_runs_update_clause() { - let program = parse_fragment( - br#"for ($i = 3; $i; $i = $i - 1) { if ($i - 1) { continue; } echo "done"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "done"); - assert_eq!(values.get(i), FakeValue::Int(0)); -} - -/// Verifies comparison operators return boolean cells usable by echo and branches. -#[test] -fn execute_program_comparisons_return_bool_cells() { - let program = parse_fragment( - br#"echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; if ("10" == 10) { echo "n"; } if ("a" != "b") { echo "s"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1111ns"); -} - -/// Verifies spaceship comparisons return PHP -1/0/1 integer cells. -#[test] -fn execute_program_spaceship_returns_int_cells() { - let program = - parse_fragment(br#"echo 1 <=> 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "-1:0:1"); -} - -/// Verifies strict equality keeps PHP type identity distinct from loose equality. -#[test] -fn execute_program_strict_equality_uses_type_identity() { - let program = parse_fragment( - br#"echo "10" == 10; echo "10" === 10; echo "10" === "10"; echo "10" !== 10;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "111"); -} - -/// Verifies logical AND skips an unsupported right-hand expression after a false left side. -#[test] -fn execute_program_short_circuits_logical_and() { - let program = parse_fragment(br#"return false && missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Bool(false)); -} - -/// Verifies logical OR skips an unsupported right-hand expression after a true left side. -#[test] -fn execute_program_short_circuits_logical_or() { - let program = parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies match expressions use strict comparison across comma-separated patterns. -#[test] -fn execute_program_match_uses_strict_pattern_comparison() { - let program = - parse_fragment(br#"return match ($x) { 1, "1" => "string", default => "other" };"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.string("1").expect("create fake string"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("string".to_string())); -} - -/// Verifies match expressions evaluate only the selected arm result. -#[test] -fn execute_program_match_skips_unselected_results() { - let program = parse_fragment( - br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("two".to_string())); -} - -/// Verifies match expressions without a matching arm or default fail at runtime. -#[test] -fn execute_program_match_without_default_fails_on_miss() { - let program = parse_fragment(br#"return match (3) { 1 => "one", 2 => "two" };"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); -} - -/// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. -#[test] -fn execute_program_evaluates_keyword_logical_operators() { - let program = - parse_fragment(br#"echo (false || true and false) ? "T" : "F"; return true or missing();"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "F"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies PHP keyword `xor` evaluates both operands and returns a boolean cell. -#[test] -fn execute_program_evaluates_keyword_xor() { - let program = - parse_fragment(br#"echo (true xor false) ? "T" : "F"; echo (true xor true) ? "T" : "F";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "TF"); -} - -/// Verifies ternary expressions evaluate only the selected branch. -#[test] -fn execute_program_ternary_short_circuits_unselected_branch() { - let program = - parse_fragment(br#"echo true ? "yes" : missing(); echo false ? missing() : "no";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "yesno"); -} - -/// Verifies the short ternary form returns the condition value when it is truthy. -#[test] -fn execute_program_short_ternary_reuses_truthy_condition() { - let program = parse_fragment(br#"echo "x" ?: "fallback"; echo false ?: "fallback";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "xfallback"); -} - -/// Verifies null coalescing uses the default for missing or null values. -#[test] -fn execute_program_null_coalesce_uses_default_for_missing_or_null() { - let program = parse_fragment(br#"echo $missing ?? "fallback"; echo $x ?? "null-fallback";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.null().expect("create fake null"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "fallbacknull-fallback"); -} - -/// Verifies null coalescing skips the default expression for non-null values. -#[test] -fn execute_program_null_coalesce_short_circuits_non_null_value() { - let program = parse_fragment(br#"echo "set" ?? missing();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "set"); -} - -/// Verifies logical negation returns boolean cells using PHP truthiness. -#[test] -fn execute_program_evaluates_logical_not() { - let program = parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1"); -} - -/// Verifies unary numeric operators delegate to PHP numeric runtime operations. -#[test] -fn execute_program_evaluates_unary_numeric_ops() { - let program = parse_fragment(br#"return -$x + +2;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(5).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(-3)); -} - -/// Verifies foreach assigns each indexed element to the value variable. -#[test] -fn execute_program_foreach_iterates_indexed_values() { - let program = parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let item = scope - .visible_cell("item") - .expect("scope should contain last foreach item"); - - assert_eq!(values.output, "ab"); - assert_eq!(values.get(item), FakeValue::String("b".to_string())); -} - -/// Verifies foreach key-value targets receive indexed integer keys and values. -#[test] -fn execute_program_foreach_assigns_indexed_keys() { - let program = - parse_fragment(br#"foreach (["a", "b"] as $key => $item) { echo $key . $item; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let key = scope.visible_cell("key").expect("scope should contain key"); - let item = scope - .visible_cell("item") - .expect("scope should contain last foreach item"); - - assert_eq!(values.output, "0a1b"); - assert_eq!(values.get(key), FakeValue::Int(1)); - assert_eq!(values.get(item), FakeValue::String("b".to_string())); -} - -/// Verifies foreach over associative arrays preserves insertion-order keys and values. -#[test] -fn execute_program_foreach_iterates_assoc_keys_and_values() { - let program = parse_fragment( - br#"foreach (["a" => 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "a:1;b:2;"); -} - -/// Verifies value-only foreach over associative arrays still yields values in insertion order. -#[test] -fn execute_program_foreach_iterates_assoc_values_only() { - let program = parse_fragment(br#"foreach (["a" => 1, "b" => 2] as $item) { echo $item; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "12"); -} - -/// Verifies break and continue control foreach execution inside eval. -#[test] -fn execute_program_foreach_honors_break_and_continue() { - let program = parse_fragment( - br#"foreach ([1, 2, 3] as $item) { if ($item == 1) { continue; } echo $item; break; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2"); -} - -/// Verifies indexed array literals and reads execute through runtime hooks. -#[test] -fn execute_program_reads_indexed_array_literal() { - let program = parse_fragment(br#"return ["a", "b"][1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("b".to_string())); -} - -/// Verifies legacy `array(...)` literals execute through the existing array runtime hooks. -#[test] -fn execute_program_reads_legacy_array_literal() { - let program = - parse_fragment(br#"return array("a", "b" => "bee",)[0];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("a".to_string())); -} - -/// Verifies associative array literals and string-key reads execute through runtime hooks. -#[test] -fn execute_program_reads_assoc_array_literal() { - let program = - parse_fragment(br#"return ["name" => "Ada"]["name"];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); -} - -/// Verifies unkeyed assoc literal entries start at zero after string keys. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_string_key_starts_at_zero() { - let program = - parse_fragment(br#"return ["name" => "Ada", "Grace"][0];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); -} - -/// Verifies unkeyed assoc literal entries use one plus the largest integer key. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_positive_int_key() { - let program = - parse_fragment(br#"return [2 => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies unkeyed assoc literal entries preserve PHP's negative-key rule. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_negative_int_key() { - let program = - parse_fragment(br#"return [-2 => "minus", "tail"][-1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies numeric string literal keys update the next automatic key. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_numeric_string_key() { - let program = - parse_fragment(br#"return ["2" => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies leading-zero string literal keys do not update the automatic key. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_leading_zero_string_key() { - let program = - parse_fragment(br#"return ["02" => "two", "tail"][0];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies null literal keys normalize to empty strings without advancing automatic keys. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_null_key() { - let program = - parse_fragment(br#"return [null => "empty", "tail"][0];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies null literal keys are readable through the empty-string key. -#[test] -fn execute_program_assoc_array_literal_reads_null_key_as_empty_string() { - let program = parse_fragment(br#"return [null => "empty"][""];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("empty".to_string())); -} - -/// Verifies boolean literal keys update the next automatic key after integer normalization. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { - let program = - parse_fragment(br#"return [true => "yes", "tail"][2];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies false literal keys update the next automatic key from zero. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_false_key() { - let program = - parse_fragment(br#"return [false => "no", "tail"][1];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies float literal keys update the next automatic key after truncation. -#[test] -fn execute_program_assoc_array_literal_unkeyed_after_float_key() { - let program = - parse_fragment(br#"return [2.7 => "two", "tail"][3];"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies nested eval calls parse and execute against the same dynamic scope. -#[test] -fn execute_program_nested_eval_uses_same_scope() { - let program = - parse_fragment(br#"eval("$x = $x + 4;"); return $x;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(1).expect("create fake int"); - scope.set("x", x, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); -} - -/// Verifies `__LINE__` inside eval uses the source line within the fragment. -#[test] -fn execute_program_magic_line_uses_fragment_line() { - let program = parse_fragment(b"\nreturn __LINE__;").expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); -} - -/// Verifies file-dependent eval magic constants use call-site metadata from the context. -#[test] -fn execute_program_magic_file_and_dir_use_context_call_site() { - let program = - parse_fragment(br#"return __FILE__ . "|" . __DIR__;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - context.set_call_site("/tmp/main.php", "/tmp", 17); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!( - values.get(result), - FakeValue::String("/tmp/main.php(17) : eval()'d code|/tmp".to_string()) - ); -} - -/// Verifies eval class, namespace, and trait magic constants are empty in eval scope. -#[test] -fn execute_program_scope_magic_constants_are_empty_strings() { - let program = - parse_fragment(br#"return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("[||]".to_string())); -} - -/// Verifies eval-declared functions can be called by the same fragment. -#[test] -fn execute_program_calls_declared_function() { - let program = parse_fragment(br#"function dyn($x) { return $x + 1; } return dyn(4);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); -} - -/// Verifies eval namespace declarations qualify functions and namespace magic values. -#[test] -fn execute_program_namespace_qualifies_declared_function() { - let program = parse_fragment( - br#"namespace Eval\Ns; -function dyn() { return __NAMESPACE__ . ":" . __FUNCTION__; } -return dyn();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.get(result), - FakeValue::String("Eval\\Ns:Eval\\Ns\\dyn".to_string()) - ); -} - -/// Verifies unqualified namespaced calls fall back to global builtins when needed. -#[test] -fn execute_program_namespace_call_falls_back_to_builtin() { - let program = parse_fragment(br#"namespace Eval\Ns; return strlen("abcd");"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); -} - -/// Verifies namespaced dynamic functions take precedence over global builtin fallback. -#[test] -fn execute_program_namespace_function_overrides_builtin_fallback() { - let program = parse_fragment( - br#"namespace Eval\Ns; -function strlen($value) { return 99; } -return strlen("abcd");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(99)); -} - -/// Verifies unqualified namespaced constants fall back to global predefined constants. -#[test] -fn execute_program_namespace_const_fetch_falls_back_to_global() { - let program = - parse_fragment(br#"namespace Eval\Ns; return PHP_EOL;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("\n".to_string())); -} - -/// Verifies namespaced dynamic constants take precedence over global fallback. -#[test] -fn execute_program_namespace_const_fetch_reads_dynamic_constant_first() { - let program = - parse_fragment(br#"namespace Eval\Ns; return LOCAL;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(7).expect("create fake int"); - assert!(context.define_constant("Eval\\Ns\\LOCAL", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies eval namespace `use function` imports dispatch to qualified dynamic functions. -#[test] -fn execute_program_namespace_use_function_import_dispatches() { - let program = parse_fragment( - br#"namespace Eval\Lib; -function target($x) { return $x + 1; } -namespace Eval\App; -use function Eval\Lib\target as AliasTarget; -return aliastarget(6);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies eval namespace `use const` imports fetch qualified dynamic constants. -#[test] -fn execute_program_namespace_use_const_import_fetches_dynamic_constant() { - let program = parse_fragment( - br#"namespace Eval\App; -use const Eval\Lib\VALUE as LocalValue; -return LocalValue;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(11).expect("create fake int"); - assert!(context.define_constant("Eval\\Lib\\VALUE", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(11)); -} - -/// Verifies eval grouped namespace imports dispatch dynamic functions and constants. -#[test] -fn execute_program_grouped_namespace_use_imports_dispatch() { - let program = parse_fragment( - br#"namespace Eval\Lib; -function target($x) { return $x + 2; } -namespace Eval\App; -use function Eval\Lib\{target as AliasTarget}; -use const Eval\Lib\{VALUE as LocalValue}; -return AliasTarget(LocalValue);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let value = values.int(5).expect("create fake int"); - assert!(context.define_constant("Eval\\Lib\\VALUE", value)); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies eval-declared functions bind named arguments by parameter name. -#[test] -fn execute_program_calls_declared_function_with_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(y: 2, x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} - -/// Verifies eval-declared functions unpack indexed arrays as positional arguments. -#[test] -fn execute_program_calls_declared_function_with_spread_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...[1, 2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} - -/// Verifies string keys unpack as named arguments for eval-declared functions. -#[test] -fn execute_program_calls_declared_function_with_named_spread_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...["y" => 2], x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} - -/// Verifies eval-declared function static locals persist between calls. -#[test] -fn execute_program_static_var_persists_in_declared_function() { - let program = parse_fragment( - br#"function dyn() { for ($i = 0; $i < 2; $i++) { static $n = 0; $n++; } return $n; } -return (dyn() * 10) + dyn();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(24)); -} - -/// Verifies top-level eval static declarations reinitialize on each eval execution. -#[test] -fn execute_program_top_level_static_var_reinitializes_per_eval() { - let program = - parse_fragment(br#"static $n = 0; $n++; return $n;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let first = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute first eval ir"); - let second = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute second eval ir"); - - assert_eq!(values.get(first), FakeValue::Int(1)); - assert_eq!(values.get(second), FakeValue::Int(1)); -} - -/// Verifies `global` declarations read and write the context global scope. -#[test] -fn execute_program_global_alias_writes_context_global_scope() { - let program = - parse_fragment(br#"global $g; $g = $g + 1; return $g;"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut global_scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let initial = values.int(1).expect("allocate initial global"); - global_scope.set("g", initial, ScopeCellOwnership::Owned); - context.set_global_scope(&mut global_scope); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - let global = global_scope - .visible_cell("g") - .expect("global scope should contain g"); - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!(values.get(global), FakeValue::Int(2)); -} - -/// Verifies references to global aliases write the source global variable. -#[test] -fn execute_program_reference_alias_to_global_updates_source_global() { - let program = parse_fragment(br#"global $g; $alias =& $g; $alias = 4; return $g;"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut global_scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let initial = values.int(1).expect("allocate initial global"); - global_scope.set("g", initial, ScopeCellOwnership::Owned); - context.set_global_scope(&mut global_scope); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - let global = global_scope - .visible_cell("g") - .expect("global scope should contain g"); - assert_eq!(values.get(result), FakeValue::Int(4)); - assert_eq!(values.get(global), FakeValue::Int(4)); - assert!(global_scope.visible_cell("alias").is_none()); -} - -/// Verifies named calls reject positional arguments that follow named arguments. -#[test] -fn execute_program_rejects_positional_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, print "late");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - assert_eq!(values.output, ""); -} - -/// Verifies named calls reject argument unpacking after named arguments. -#[test] -fn execute_program_rejects_spread_after_named_arg() { - let program = - parse_fragment(br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, ...[2]);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); -} - -/// Verifies function-scope magic constants keep the eval declaration spelling. -#[test] -fn execute_program_magic_function_and_method_use_eval_declared_name() { - let program = parse_fragment( - br#"function DynMagicCase() { return __FUNCTION__ . ":" . __METHOD__; } return dynmagiccase();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.get(result), - FakeValue::String("DynMagicCase:DynMagicCase".to_string()) - ); -} - -/// Verifies eval-declared functions persist in a shared eval context. -#[test] -fn execute_program_context_keeps_declared_function() { - let define = - parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("parse eval fragment"); - let call = parse_fragment(br#"return dyn(4);"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect("execute eval ir"); - let result = execute_program_with_context(&mut context, &call, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); -} - -/// Verifies `call_user_func` inside eval can dispatch an eval-declared function. -#[test] -fn execute_program_call_user_func_dispatches_declared_function() { - let program = parse_fragment( - br#"function dyn($x) { return $x + 1; } -return call_user_func("dyn", 4);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); -} - -/// Verifies `call_user_func` inside eval can dispatch a supported builtin. -#[test] -fn execute_program_call_user_func_dispatches_builtin() { - let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); -} - -/// Verifies `call_user_func` inside eval can dispatch a registered native function. -#[test] -fn execute_program_call_user_func_dispatches_registered_native_function() { - let program = - parse_fragment(br#"return call_user_func("native_answer");"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} - -/// Verifies string variable calls inside eval can dispatch a supported builtin. -#[test] -fn execute_program_variable_call_dispatches_builtin() { - let program = parse_fragment( - br#"$fn = "strlen"; -return $fn("abcd");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); -} - -/// Verifies callable array entries can be invoked through postfix dynamic calls. -#[test] -fn execute_program_postfix_variable_call_dispatches_builtin() { - let program = parse_fragment( - br#"$callbacks = ["strlen"]; -return $callbacks[0]("abc");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(3)); -} - -/// Verifies variable calls bind eval-declared function arguments by name. -#[test] -fn execute_program_variable_call_binds_declared_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } -$fn = "dyn"; -return $fn(y: 2, x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} - -/// Verifies variable calls can dispatch registered native functions with named args. -#[test] -fn execute_program_variable_call_binds_registered_native_named_args() { - let program = parse_fragment( - br#"$fn = "native_answer"; -return $fn(right: 2, left: 1);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} - -/// Verifies direct callable-array variable calls dispatch object methods. -#[test] -fn execute_program_callable_array_variable_dispatches_object_method() { - let program = parse_fragment( - br#"$box = new Box(41); -$cb = [$box, "add_x"]; -return $cb(1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); -} - -/// Verifies `call_user_func` dispatches callable arrays with object receivers. -#[test] -fn execute_program_call_user_func_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(42); -$cb = [$box, "read_x"]; -return call_user_func($cb);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); -} - -/// Verifies `call_user_func_array` dispatches callable arrays with positional args. -#[test] -fn execute_program_call_user_func_array_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(39); -return call_user_func_array([$box, "add2_x"], [1, 2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); -} - -/// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. -#[test] -fn execute_program_call_user_func_array_dispatches_declared_function() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } -return call_user_func_array("dyn", [4, 5]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(9)); -} - -/// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. -#[test] -fn execute_program_call_user_func_array_binds_declared_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } -return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} - -/// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. -#[test] -fn execute_context_function_call_array_binds_declared_named_args() { - let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - let arg_array = values.assoc_new(2).expect("allocate argument array"); - let key_y = values.string("y").expect("allocate y key"); - let value_y = values.int(2).expect("allocate y value"); - let _ = values - .array_set(arg_array, key_y, value_y) - .expect("store y argument"); - let key_x = values.string("x").expect("allocate x key"); - let value_x = values.int(1).expect("allocate x value"); - let _ = values - .array_set(arg_array, key_x, value_x) - .expect("store x argument"); - - let result = execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) - .expect("execute context function call array"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} - -/// Verifies `call_user_func_array` rejects positional values after named keys. -#[test] -fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } -return call_user_func_array("dyn", ["y" => 2, 1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); -} - -/// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. -#[test] -fn execute_program_call_user_func_array_dispatches_builtin() { - let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); -} - -/// Verifies `call_user_func_array` inside eval can dispatch a registered native function. -#[test] -fn execute_program_call_user_func_array_dispatches_registered_native_function() { - let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} - -/// Verifies `call_user_func_array` named keys can bind registered native parameters. -#[test] -fn execute_program_call_user_func_array_binds_registered_native_named_args() { - let program = parse_fragment( - br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} - -/// Verifies duplicate eval-declared function names fail in a shared context. -#[test] -fn execute_program_rejects_duplicate_declared_function() { - let define = parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect("execute first declaration"); - let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect_err("duplicate function declaration should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. -#[test] -fn execute_program_dispatches_simple_builtins() { - let program = parse_fragment( - br#"echo strlen("abc") . ":" . count([1, [2, 3], [4]]) . ":"; -echo count([1, [2, 3], [4]], COUNT_RECURSIVE) . ":"; -echo call_user_func("count", [1, [2]]) . ":"; -echo call_user_func_array("count", ["value" => [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; -return defined("COUNT_RECURSIVE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:3:6:2:3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. -#[test] -fn execute_program_dispatches_json_encode_builtin() { - let program = parse_fragment( - br#"echo json_encode(["a" => 1, "b" => "x/y"]) . ":"; -echo json_encode([1, "q", true, null]) . ":"; -echo call_user_func("json_encode", "a/b\"c") . ":"; -echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; -echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; -echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; -$accent = json_decode("\"\\u00e9\""); -$emoji = json_decode("\"\\ud83d\\ude00\""); -echo bin2hex(json_encode($accent . "/" . $emoji)) . ":"; -echo bin2hex(json_encode($accent . "/" . $emoji, JSON_UNESCAPED_UNICODE)) . ":"; -echo bin2hex(json_encode([$accent => $emoji])) . ":"; -echo bin2hex(json_encode([$accent => $emoji], JSON_UNESCAPED_UNICODE)) . ":"; -echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; -echo json_encode([], JSON_FORCE_OBJECT) . ":"; -echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; -echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; -echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; -echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; -echo (json_encode(INF) === false ? "false" : "json") . ":"; -echo json_last_error() . ":" . json_last_error_msg() . ":"; -echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; -echo json_last_error() . ":" . json_last_error_msg() . ":"; -$bad = "a" . hex2bin("80") . "b"; -echo (json_encode($bad) === false ? "utf8-false" : "bad") . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR)) . ":"; -echo json_last_error() . ":"; -echo json_encode($bad, JSON_INVALID_UTF8_IGNORE) . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE)) . ":"; -echo json_last_error() . ":"; -echo json_encode([hex2bin("6b80") => hex2bin("7680")], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; -echo json_last_error() . ":"; -json_encode(3.5); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; -return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"k\ufffd":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:"# - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `json_decode()` materializes scalars, arrays, and associative arrays. -#[test] -fn execute_program_dispatches_json_decode_builtin() { - let program = parse_fragment( - br#"echo json_decode("\"hello\"") . ":"; -echo json_decode("42") . ":"; -echo (json_decode("true") ? "T" : "bad") . ":"; -echo (is_null(json_decode("null")) ? "NULL" : "bad") . ":"; -$decoded = json_decode("{\"a\":1,\"b\":[\"x\",false]}", true); -echo $decoded["a"] . ":" . $decoded["b"][0] . ":" . ($decoded["b"][1] ? "bad" : "F") . ":"; -$call = call_user_func("json_decode", "[3,4]"); -echo $call[1] . ":"; -$named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); -echo $named["k"] . ":"; -$badJson = "\"a" . hex2bin("80") . "b\""; -echo (is_null(json_decode($badJson)) ? "utf8-null" : "bad") . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_IGNORE)) . ":"; -echo json_last_error() . ":"; -echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; -echo json_last_error() . ":"; -$objSub = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_SUBSTITUTE); -$objSubKeys = array_keys($objSub); -echo bin2hex($objSubKeys[0]) . "=" . bin2hex($objSub[$objSubKeys[0]]) . ":"; -$objIgnore = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_IGNORE); -$objIgnoreKeys = array_keys($objIgnore); -echo bin2hex($objIgnoreKeys[0]) . "=" . bin2hex($objIgnore[$objIgnoreKeys[0]]) . ":"; -echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; -$big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); -echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; -echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; -echo gettype($big[0]) . ":" . $big[0] . ":"; -echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; -return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. -#[test] -fn execute_program_dispatches_json_decode_stdclass_default() { - let program = parse_fragment( - br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); -echo $object->a . ":" . $object->b->c . ":"; -$objectFalse = json_decode("{\"z\":2}", false); -echo $objectFalse->z . ":"; -$objectNull = json_decode("{\"n\":{\"m\":3}}", null); -echo $objectNull->n->m . ":"; -$assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); -echo $assoc["b"]["c"] . ":"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:x:2:3:y:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `json_encode()` serializes stdClass dynamic properties. -#[test] -fn execute_program_dispatches_json_encode_stdclass_object() { - let program = parse_fragment( - br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); -echo json_encode($object) . ":"; -echo str_replace("\n", "|", json_encode($object, JSON_PRETTY_PRINT)) . ":"; -$empty = json_decode("{}"); -echo json_encode($empty) . ":"; -$empty->a = 7; -echo json_encode($empty); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `json_last_error*()` track JSON parse failures and success resets. -#[test] -fn execute_program_dispatches_json_last_error_builtins() { - let program = parse_fragment( - br#"echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_decode("bad"); -echo json_last_error() . ":" . (json_last_error() === JSON_ERROR_SYNTAX ? "syntax" : "bad") . ":" . json_last_error_msg() . ":"; -json_validate("[1]", 1); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_validate("\"ok\""); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_validate("\"a" . chr(10) . "b\""); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_decode("\"\\uD83D\""); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_decode("\"a" . chr(128) . "b\""); -echo json_last_error() . ":" . json_last_error_msg() . ":"; -json_validate("[0]"); -echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_error_msg", []) . ":"; -return function_exists("json_last_error") && function_exists("json_last_error_msg") && defined("JSON_ERROR_SYNTAX");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "0:No error:4:syntax:Syntax error near location 1:1:1:Maximum stack depth exceeded near location 1:1:0:No error:3:Control character error, possibly incorrectly encoded near location 1:3:10:Single unpaired UTF-16 surrogate in unicode escape near location 1:8:5:Malformed UTF-8 characters, possibly incorrectly encoded near location 1:3:0:No error:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval JSON throw flags raise catchable Throwable objects. -#[test] -fn execute_program_dispatches_json_throw_on_error() { - let program = parse_fragment( - br#"try { - json_decode("bad", true, 512, JSON_THROW_ON_ERROR); - echo "bad"; -} catch (Throwable) { - echo "decode:"; -} -try { - json_encode(INF, JSON_THROW_ON_ERROR); - echo "bad"; -} catch (Throwable) { - echo "encode:"; -} -echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; -return defined("JSON_THROW_ON_ERROR");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "decode:encode:0:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. -#[test] -fn execute_program_dispatches_json_validate_builtin() { - let program = parse_fragment( - br#"echo (json_validate("{\"a\":[1,true,null,\"caf\\u00e9\"]}") ? "Y" : "N") . ":"; -echo (json_validate("bad") ? "bad" : "N") . ":"; -echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; -echo (call_user_func("json_validate", "\"x\"") ? "C" : "bad") . ":"; -echo (call_user_func_array("json_validate", ["json" => "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; -echo (json_validate("\"a" . chr(128) . "b\"", 512, JSON_INVALID_UTF8_IGNORE) ? "I" : "bad") . ":"; -echo json_last_error() . ":"; -echo (json_validate("bad", 512, JSON_INVALID_UTF8_IGNORE) ? "bad" : "S") . ":"; -echo json_last_error() . ":"; -return function_exists("json_validate") && defined("JSON_INVALID_UTF8_IGNORE");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N:D:C:A:I:0:S:4:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies direct eval builtin calls bind named and unpacked arguments. -#[test] -fn execute_program_dispatches_named_and_spread_builtins() { - let program = parse_fragment( - br#"echo strlen(string: "abcd"); -echo ":" . (array_key_exists(array: ["name" => 1], key: "name") ? "Y" : "N"); -echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N"); -return round(precision: 1, num: 3.14);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:Y:Y"); - assert_eq!(values.get(result), FakeValue::Float(3.1)); -} - -/// Verifies eval `ord()` returns the first byte and supports callable dispatch. -#[test] -fn execute_program_dispatches_ord_builtin() { - let program = parse_fragment( - br#"echo ord("A"); -echo ":" . ord(""); -echo ":" . call_user_func("ord", "B"); -echo ":" . call_user_func_array("ord", ["C"]); -echo ":"; echo function_exists("ord"); -return ord("Z");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "65:0:66:67:1"); - assert_eq!(values.get(result), FakeValue::Int(90)); -} - -/// Verifies eval array aggregate builtins iterate array values and support callable dispatch. -#[test] -fn execute_program_dispatches_array_aggregate_builtins() { - let program = parse_fragment( - br#"echo array_sum([1, 2, 3]); -echo ":" . array_product([2, 3, 4]); -echo ":" . array_sum([]); -echo ":" . array_product([]); -echo ":" . array_sum(["a" => 2, "b" => 5]); -echo ":" . call_user_func("array_sum", [3, 4]); -echo ":" . call_user_func_array("array_product", [[2, 5]]); -echo ":"; echo function_exists("array_sum"); -return function_exists("array_product");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "6:24:0:1:7:7:10:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_map()` applies callbacks and preserves source keys. -#[test] -fn execute_program_dispatches_array_map_builtin() { - let program = parse_fragment( - br#"function eval_map_double($value) { return $value * 2; } -$mapped = array_map("eval_map_double", [1, 2, 3]); -echo $mapped[0] . ":" . $mapped[2] . ":"; -$assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); -echo $assoc["a"] . ":" . $assoc["b"] . ":"; -$identity = array_map(null, ["k" => "v"]); -echo $identity["k"] . ":"; -function eval_map_pair($left, $right) { return $left . "-" . ($right ?? "N"); } -$pairs = array_map("eval_map_pair", ["a" => "L", "b" => "R"], ["x" => "1"]); -echo $pairs[0] . ":" . $pairs[1] . ":"; -$zipped = array_map(null, [1, 2], [3, 4]); -echo $zipped[0][0] . $zipped[0][1] . ":" . $zipped[1][0] . $zipped[1][1] . ":"; -$call = call_user_func("array_map", "intval", ["7"]); -echo $call[0] . ":"; -$multi_call = call_user_func("array_map", "eval_map_pair", ["Q"], ["9"]); -echo $multi_call[0] . ":"; -$spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); -echo $spread[0] . ":"; -return function_exists("array_map");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_reduce()` folds values through a string callback. -#[test] -fn execute_program_dispatches_array_reduce_builtin() { - let program = parse_fragment( - br#"function eval_reduce_sum($carry, $item) { return $carry + $item; } -echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; -function eval_reduce_join($carry, $item) { return $carry . $item; } -echo array_reduce([4, 5], "eval_reduce_sum") . ":"; -echo array_reduce(["a", "b"], "eval_reduce_join", "") . ":"; -$named = array_reduce(array: [6, 7], callback: "eval_reduce_sum"); -echo $named . ":"; -$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum"); -echo $call . ":"; -$spread = call_user_func_array("array_reduce", ["array" => [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); -echo $spread . ":"; -return function_exists("array_reduce");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "16:9:ab:13:9:9:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_walk()` invokes string callbacks with value and key cells. -#[test] -fn execute_program_dispatches_array_walk_builtin() { - let program = parse_fragment( - br#"function eval_walk_show($value, $key) { echo $key . "=" . $value . ";"; } -echo array_walk(["a" => 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; -$call = call_user_func("array_walk", [4, 5], "eval_walk_show"); -$spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); -return function_exists("array_walk");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_pop()` and `array_shift()` write back only for direct variable calls. -#[test] -fn execute_program_dispatches_array_pop_shift_builtins() { - let program = parse_fragment( - br#"$a = [1, 2, 3]; -echo array_pop($a) . ":" . count($a) . ":" . $a[1] . ":"; -$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; -echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; -$c = [4, 5]; -echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; -$d = [6, 7]; -echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; -return function_exists("array_pop") && function_exists("array_shift");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:2:2:1:2:3:4:5:2:5:6:2:6:"); - assert_eq!( - values.warnings, - vec![ - "array_pop(): Argument #1 ($array) must be passed by reference, value given", - "array_shift(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_push()` and `array_unshift()` write back direct variable calls. -#[test] -fn execute_program_dispatches_array_push_unshift_builtins() { - let program = parse_fragment( - br#"$a = [1]; -echo array_push($a, 2, 3) . ":" . $a[2] . ":"; -$b = ["x" => 1, 10 => 2]; -echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; -$c = [2, 3]; -echo array_unshift($c, 0, 1) . ":" . $c[0] . ":" . $c[3] . ":"; -$d = ["x" => 1, 10 => 2, "y" => 3]; -echo array_unshift($d, "A") . ":" . $d[0] . ":" . $d["x"] . ":" . $d[1] . ":" . $d["y"] . ":"; -$e = [5]; -echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; -$f = [7]; -echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; -return function_exists("array_push") && function_exists("array_unshift");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:"); - assert_eq!( - values.warnings, - vec![ - "array_push(): Argument #1 ($array) must be passed by reference, value given", - "array_unshift(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_splice()` returns removed values and writes back direct variable calls. -#[test] -fn execute_program_dispatches_array_splice_builtin() { - let program = parse_fragment( - br#"$a = [10, 20, 30, 40]; -$removed = array_splice($a, 1, 2); -echo count($removed) . ":" . $removed[0] . ":" . $removed[1] . ":" . count($a) . ":" . $a[1] . ":"; -$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; -$cut = array_splice(array: $b, offset: 1, length: 2); -echo $cut[0] . ":" . $cut["y"] . ":" . $b["x"] . ":" . $b[0] . ":"; -$c = [1, 2, 3, 4]; -$tail = call_user_func("array_splice", $c, -2, 1); -echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; -$d = [5, 6, 7]; -$all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); -echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; -$e = [1, 2, 3, 4]; -$rep = array_splice($e, 1, 2, ["A", "B"]); -echo count($rep) . ":" . $rep[0] . ":" . $rep[1] . ":" . $e[0] . ":" . $e[1] . ":" . $e[2] . ":" . $e[3] . ":"; -$f = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; -$rep2 = array_splice(array: $f, offset: 1, length: 2, replacement: ["s" => "S", 9 => "N"]); -echo $rep2[0] . ":" . $rep2["y"] . ":" . $f["x"] . ":" . $f[0] . ":" . $f[1] . ":" . $f[2] . ":"; -$g = [1, 2, 3]; -$rep3 = array_splice($g, offset: 1, replacement: [9]); -echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g[1] . ":"; -$h = [1, 2, 3]; -$removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); -echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; -return function_exists("array_splice");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:" - ); - assert_eq!( - values.warnings, - vec![ - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `sort()` and `rsort()` reindex direct variable arrays only. -#[test] -fn execute_program_dispatches_sort_builtins() { - let program = parse_fragment( - br#"$a = [3, 1, 2]; -echo sort($a) . ":" . $a[0] . $a[1] . $a[2] . ":"; -$b = ["banana", "apple", "cherry"]; -echo rsort(array: $b) . ":" . $b[0] . ":" . $b[2] . ":"; -$c = ["x" => 3, "y" => 1, "z" => 2]; -sort($c); -echo $c[0] . $c[1] . $c[2] . ":"; -$d = [3, 1, 2]; -echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; -$e = [1, 2, 3]; -echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; -return function_exists("sort") && function_exists("rsort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:123:1:cherry:apple:123:1:312:1:1:3:"); - assert_eq!( - values.warnings, - vec![ - "sort(): Argument #1 ($array) must be passed by reference, value given", - "rsort(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval key-preserving array ordering builtins write back direct variable calls. -#[test] -fn execute_program_dispatches_key_preserving_sort_builtins() { - let program = parse_fragment( - br#"$a = ["x" => 3, "y" => 1, "z" => 2]; -echo asort($a) . ":"; -foreach ($a as $key => $value) { echo $key . $value; } -echo ":"; -$b = ["x" => 1, "y" => 3, "z" => 2]; -echo arsort(array: $b) . ":"; -foreach ($b as $key => $value) { echo $key . $value; } -echo ":"; -$c = ["b" => 1, "a" => 2, 3 => 4]; -echo ksort($c) . ":"; -foreach ($c as $key => $value) { echo $key . $value; } -echo ":"; -$d = ["b" => 1, "a" => 2, 3 => 4]; -echo krsort($d) . ":"; -foreach ($d as $key => $value) { echo $key . $value; } -echo ":"; -$e = ["x" => 2, "y" => 1]; -echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; -$f = ["b" => 1, "a" => 2]; -echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; -return function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:" - ); - assert_eq!( - values.warnings, - vec![ - "asort(): Argument #1 ($array) must be passed by reference, value given", - "krsort(): Argument #1 ($array) must be passed by reference, value given", - ] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval natural sort builtins preserve keys and use natural string order. -#[test] -fn execute_program_dispatches_natural_sort_builtins() { - let program = parse_fragment( - br#"$a = ["img10", "img2", "img1"]; -echo natsort($a) . ":"; -foreach ($a as $key => $value) { echo $key . $value . ";"; } -echo ":"; -$b = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; -echo natcasesort(array: $b) . ":"; -foreach ($b as $key => $value) { echo $key . $value . ";"; } -echo ":"; -$c = ["x" => "b", "y" => "a"]; -echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; -return function_exists("natsort") && function_exists("natcasesort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:" - ); - assert_eq!( - values.warnings, - vec!["natsort(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `shuffle()` reindexes direct variable arrays only. -#[test] -fn execute_program_dispatches_shuffle_builtin() { - let program = parse_fragment( - br#"$a = ["x" => 1, "y" => 2]; -echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; -$b = ["x" => 1, "y" => 2]; -echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; -return function_exists("shuffle");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:reindexed:2:3:1:12:"); - assert_eq!( - values.warnings, - vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval user-comparator sort builtins call callbacks and write back direct arrays. -#[test] -fn execute_program_dispatches_user_sort_builtins() { - let program = parse_fragment( - br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } -function eval_key_cmp($left, $right) { return strcmp($left, $right); } -$a = [3, 1, 2]; -echo usort($a, "eval_sort_cmp") . ":"; -foreach ($a as $value) { echo $value; } -echo ":"; -$b = ["b" => 1, "a" => 3, "c" => 2]; -echo uasort(array: $b, callback: "eval_sort_cmp") . ":"; -foreach ($b as $key => $value) { echo $key . $value; } -echo ":"; -$c = ["b" => 1, "a" => 2]; -echo uksort($c, "eval_key_cmp") . ":"; -foreach ($c as $key => $value) { echo $key . $value; } -echo ":"; -$d = [2, 1]; -echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; -return function_exists("usort") && function_exists("uasort") && function_exists("uksort");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:"); - assert_eq!( - values.warnings, - vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval iterator array helpers support direct and dynamic builtin calls. -#[test] -fn execute_program_dispatches_iterator_array_builtins() { - let program = parse_fragment( - br#"$items = ["x" => 1, "y" => 2]; -$copy = iterator_to_array($items); -echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; -$values = iterator_to_array($items, false); -echo (isset($values["x"]) ? "bad" : "reindexed") . ":" . $values[0] . $values[1] . ":"; -echo call_user_func("iterator_count", $items) . ":"; -$spread = call_user_func_array("iterator_to_array", ["iterator" => $items, "preserve_keys" => false]); -echo $spread[0] . $spread[1] . ":"; -return function_exists("iterator_count") && function_exists("iterator_to_array");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:12:reindexed:12:2:12:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `iterator_apply()` drives Iterator objects and callback args. -#[test] -fn execute_program_dispatches_iterator_apply_object_builtin() { - let program = parse_fragment( - br#"function eval_apply($prefix) { echo $prefix; return true; } -echo iterator_apply($it, "eval_apply", ["prefix" => "x"]) . ":"; -echo call_user_func("iterator_apply", $it, "eval_apply", ["y"]) . ":"; -return function_exists("iterator_apply");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "xxx3:yyy3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `iterator_apply()` accepts object-method callable arrays. -#[test] -fn execute_program_iterator_apply_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(5); -echo iterator_apply($it, [$box, "add_x"], [1]) . ":"; -return call_user_func("iterator_apply", $it, [$box, "add_x"], [1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:"); - assert_eq!(values.get(result), FakeValue::Int(3)); -} - -/// Verifies eval `iterator_apply()` counts the position where the callback stops. -#[test] -fn execute_program_iterator_apply_stops_on_falsey_callback() { - let program = parse_fragment( - br#"function eval_stop() { echo "s"; return false; } -return iterator_apply($it, "eval_stop");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let iterator = values.alloc(FakeValue::Iterator { - len: 3, - position: 0, - }); - scope.set("it", iterator, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "s"); - assert_eq!(values.get(result), FakeValue::Int(1)); -} - -/// Verifies eval `array_filter()` removes falsey values while preserving original keys. -#[test] -fn execute_program_dispatches_array_filter_builtin() { - let program = parse_fragment( - br#"$filtered = array_filter([0, 1, 2, "", false, null, "0", "ok"]); -echo count($filtered) . ":" . $filtered[1] . ":" . $filtered[2] . ":" . $filtered[7] . ":"; -$assoc = array_filter(["a" => 0, "b" => 2, "c" => ""]); -echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; -$null = array_filter([0, 3], null, 1); -echo count($null) . ":" . $null[1] . ":"; -$call = call_user_func("array_filter", [0, 4]); -echo count($call) . ":" . $call[1] . ":"; -$spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); -echo count($spread) . ":" . $spread[1] . ":"; -function eval_keep_even($value) { return $value % 2 == 0; } -$evens = array_filter([1, 2, 3, 4], "eval_keep_even"); -echo count($evens) . ":" . $evens[1] . ":" . $evens[3] . ":"; -function eval_keep_key($key) { return $key === "b"; } -$keyed = array_filter(["a" => 10, "b" => 20], "eval_keep_key", ARRAY_FILTER_USE_KEY); -echo count($keyed) . ":" . $keyed["b"] . ":"; -function eval_keep_both($value, $key) { return $key === "c" || $value === 1; } -$both = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_keep_both", ARRAY_FILTER_USE_BOTH); -echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; -$ints = array_filter([1, "x", 2], "is_int"); -echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; -return function_exists("array_filter");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_combine()` converts key values through PHP string-key rules. -#[test] -fn execute_program_dispatches_array_combine_builtin() { - let program = parse_fragment( - br#"$pairs = array_combine(["a", "b"], [10, 20]); -echo $pairs["a"] . ":" . $pairs["b"]; -$numeric = array_combine(["1", "01"], ["n", "z"]); -echo ":" . $numeric[1] . $numeric["01"]; -$scalar = array_combine([null, true, false, 2.8], ["n", "t", "f", "d"]); -echo ":" . $scalar[""] . $scalar[1] . $scalar["2.8"]; -$named = array_combine(keys: ["k"], values: ["v"]); -echo ":" . $named["k"]; -$call = call_user_func("array_combine", ["x"], [7]); -echo ":" . $call["x"]; -$spread = call_user_func_array("array_combine", [["y"], [8]]); -echo ":" . $spread["y"] . ":"; -return function_exists("array_combine");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "10:20:nz:ftd:v:7:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_column()` extracts present row columns and reindexes them. -#[test] -fn execute_program_dispatches_array_column_builtin() { - let program = parse_fragment( - br#"$rows = [["name" => "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; -$names = array_column($rows, "name"); -echo count($names) . ":" . $names[0] . ":" . $names[1]; -$scores = array_column($rows, "score"); -echo ":" . count($scores) . ":" . $scores[0] . $scores[2]; -$numeric = array_column([[0 => "zero", 1 => "one"], [1 => "uno"]], 1); -echo ":" . count($numeric) . ":" . $numeric[0] . ":" . $numeric[1]; -$named = array_column(array: $rows, column_key: "score"); -echo ":" . $named[1]; -$call = call_user_func("array_column", [["x" => 5], ["x" => 6]], "x"); -echo ":" . $call[1]; -$spread = call_user_func_array("array_column", [[["y" => 7], ["z" => 0], ["y" => 9]], "y"]); -echo ":" . count($spread) . ":" . $spread[1] . ":"; -return function_exists("array_column");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. -#[test] -fn execute_program_dispatches_array_shape_builtins() { - let program = parse_fragment( - br#"$right = array_pad([1, 2], 5, 0); -echo count($right) . ":" . $right[0] . $right[1] . $right[2] . $right[4]; -$left = array_pad([1, 2], -4, 9); -echo ":" . $left[0] . $left[1] . $left[2] . $left[3]; -$copy = array_pad([7, 8], 1, 0); -echo ":" . count($copy) . ":" . $copy[0] . $copy[1]; -$chunks = array_chunk([1, 2, 3, 4, 5], 2); -echo ":" . count($chunks) . ":" . $chunks[0][1] . $chunks[2][0]; -$named = array_pad(array: ["a"], length: 2, value: "b"); -echo ":" . $named[1]; -$call = call_user_func("array_chunk", [6, 7, 8], 2); -echo ":" . $call[1][0]; -$spread = call_user_func_array("array_pad", [[1], 3, 2]); -echo ":" . $spread[2] . ":"; -return function_exists("array_pad") && function_exists("array_chunk");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:1200:9912:2:78:3:25:b:8:2:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_slice()` observes PHP offset and length bounds. -#[test] -fn execute_program_dispatches_array_slice_builtin() { - let program = parse_fragment( - br#"$mid = array_slice([10, 20, 30, 40, 50], 1, 3); -echo count($mid) . ":" . $mid[0] . $mid[1] . $mid[2]; -$tail = array_slice([10, 20, 30, 40], -2, 1); -echo ":" . $tail[0]; -$open = array_slice([10, 20, 30, 40, 50], 2); -echo ":" . count($open) . ":" . $open[0] . $open[2]; -$null_len = array_slice([5, 6, 7], 1, null); -echo ":" . $null_len[0] . $null_len[1]; -$negative_len = array_slice([10, 20, 30, 40, 50], 1, -1); -echo ":" . count($negative_len) . ":" . $negative_len[0] . $negative_len[2]; -$named = array_slice(array: [1, 2, 3], offset: 1, length: 1); -echo ":" . $named[0]; -$call = call_user_func("array_slice", [6, 7, 8], 1, 2); -echo ":" . $call[1]; -$spread = call_user_func_array("array_slice", [[9, 10, 11], 1]); -echo ":" . $spread[0] . ":"; -return function_exists("array_slice");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:203040:30:3:3050:67:3:2040:2:8:10:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. -#[test] -fn execute_program_dispatches_array_merge_builtin() { - let program = parse_fragment( - br#"$merged = array_merge([1, 2], [3, 4]); -echo count($merged) . ":" . $merged[0] . $merged[1] . $merged[2] . $merged[3]; -$left = [1, 2]; -$right = [3]; -$copy = array_merge($left, $right); -echo ":" . count($left) . ":" . $left[0] . ":" . $copy[2]; -$assoc = array_merge(["a" => 1, 2 => "x"], ["a" => 9, 5 => "y", "b" => 3]); -echo ":" . $assoc["a"] . ":" . $assoc[0] . ":" . $assoc[1] . ":" . $assoc["b"]; -$call = call_user_func("array_merge", [6], [7, 8]); -echo ":" . $call[2]; -$spread = call_user_func_array("array_merge", [[9], [10]]); -echo ":" . $spread[1] . ":"; -return function_exists("array_merge");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:1234:2:1:3:9:x:y:3:8:10:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_diff()` and `array_intersect()` compare values as strings. -#[test] -fn execute_program_dispatches_array_value_set_builtins() { - let program = parse_fragment( - br#"$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); -echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; -echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); -echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); -$inter = array_intersect(["a" => 1, "b" => 2, "c" => "2", "d" => 3], ["2", 4]); -echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter["c"]; -$call = call_user_func("array_diff", [1, 2, 3], [2]); -echo ":" . count($call) . ":" . $call[0] . $call[2]; -$spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); -echo ":" . count($spread) . ":" . $spread[2] . ":"; -return function_exists("array_diff") && function_exists("array_intersect");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. -#[test] -fn execute_program_dispatches_array_key_set_builtins() { - let program = parse_fragment( - br#"$diff = array_diff_key(["a" => 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); -echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; -echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); -$inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); -echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter[4]; -$call = call_user_func("array_diff_key", [10, 20, 30], [1 => 0]); -echo ":" . count($call) . ":" . $call[0] . $call[2]; -$spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); -echo ":" . count($spread) . ":" . $spread["y"] . ":"; -return function_exists("array_diff_key") && function_exists("array_intersect_key");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:2:3:no-a:2:2:3:2:1030:1:8:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `range()` builds inclusive ascending and descending integer arrays. -#[test] -fn execute_program_dispatches_range_builtin() { - let program = parse_fragment( - br#"$up = range(1, 4); -echo count($up) . ":" . $up[0] . $up[3]; -$down = range(4, 1); -echo ":" . count($down) . ":" . $down[0] . $down[3]; -$single = range(3, 3); -echo ":" . count($single) . ":" . $single[0]; -$named = range(start: 2, end: 4); -echo ":" . $named[0] . $named[2]; -$call = call_user_func("range", 5, 7); -echo ":" . $call[2]; -$spread = call_user_func_array("range", [8, 6]); -echo ":" . count($spread) . ":" . $spread[0] . $spread[2] . ":"; -return function_exists("range");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:14:4:41:1:3:24:7:3:86:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_rand()` returns a key that exists in the source array. -#[test] -fn execute_program_dispatches_array_rand_builtin() { - let program = parse_fragment( - br#"$nums = [10, 20, 30]; -$idx = array_rand($nums); -echo ($idx >= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; -$assoc = ["a" => 1, "b" => 2]; -$key = array_rand($assoc); -echo ":" . (array_key_exists($key, $assoc) ? "assoc" : "bad"); -$named = array_rand(array: [5, 6]); -echo ":" . (($named >= 0 && $named < 2) ? "named" : "bad"); -$call = call_user_func("array_rand", [7, 8]); -echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); -$spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); -echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; -return function_exists("array_rand");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "idx:assoc:named:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval random builtins return values inside PHP inclusive ranges. -#[test] -fn execute_program_dispatches_rand_builtins() { - let program = parse_fragment( - br#"$plain = rand(); -echo ($plain >= 0 && $plain <= 2147483647) ? "plain" : "bad"; -$bounded = rand(2, 4); -echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); -$same = mt_rand(max: 6, min: 6); -echo ":" . ($same === 6 ? "same" : "bad"); -$swapped = rand(10, 1); -echo ":" . (($swapped >= 1 && $swapped <= 10) ? "swap" : "bad"); -$call = call_user_func("mt_rand", 1, 1); -echo ":" . ($call === 1 ? "call" : "bad"); -$spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); -echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; -$secure = random_int(max: 4, min: 4); -echo ($secure === 4 ? "random" : "bad") . ":"; -$secureCall = call_user_func("random_int", 5, 5); -echo ($secureCall === 5 ? "random-call" : "bad") . ":"; -$secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); -echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; -echo function_exists("rand"); -echo function_exists("mt_rand"); -return function_exists("random_int");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "plain:range:same:swap:call:spread:random:random-call:random-spread:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. -#[test] -fn execute_program_dispatches_array_fill_builtins() { - let program = parse_fragment( - br#"$filled = array_fill(2, 3, "x"); -echo count($filled) . ":" . $filled[2] . $filled[4]; -$negative = array_fill(-2, 3, 7); -echo ":" . $negative[-2] . $negative[-1] . $negative[0]; -$empty = array_fill(5, 0, "x"); -echo ":" . count($empty); -$map = array_fill_keys(["a", "1", "01"], 8); -echo ":" . $map["a"] . ":" . $map[1] . ":" . $map["01"]; -$named = array_fill(start_index: 1, count: 2, value: "n"); -echo ":" . $named[1] . $named[2]; -$call = call_user_func("array_fill", 0, 2, "c"); -echo ":" . $call[0] . $call[1]; -$spread = call_user_func_array("array_fill_keys", [["x", "y"], "z"]); -echo ":" . $spread["x"] . $spread["y"] . ":"; -return function_exists("array_fill") && function_exists("array_fill_keys");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:xx:777:0:8:8:8:nn:cc:zz:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. -#[test] -fn execute_program_dispatches_array_flip_builtin() { - let program = parse_fragment( - br#"$flipped = array_flip(["a" => "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); -echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); -$named = array_flip(array: ["k" => "v"]); -echo ":" . $named["v"]; -$call = call_user_func("array_flip", ["left" => "right"]); -echo ":" . $call["right"]; -$spread = call_user_func_array("array_flip", [["n" => 9]]); -echo ":" . $spread[9] . ":"; -return function_exists("array_flip");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "c:b:d:e:4:k:left:n:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_unique()` preserves first keys using default string comparison. -#[test] -fn execute_program_dispatches_array_unique_builtin() { - let program = parse_fragment( - br#"$unique = array_unique(["a", "b", "a", "2", 2]); -echo count($unique) . ":" . $unique[0] . $unique[1] . $unique[3]; -$assoc = array_unique(["x" => "a", "y" => "b", "z" => "a"]); -echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; -$scalar = array_unique([1, "1", 1.0, true, false, null, ""]); -echo ":" . count($scalar) . ":" . $scalar[0] . ":"; -echo $scalar[4] ? "bad" : "F"; -$named = array_unique(array: ["k" => "v", "l" => "v"]); -echo ":" . $named["k"] . ":" . count($named); -$call = call_user_func("array_unique", ["q", "q", "r"]); -echo ":" . $call[0] . $call[2]; -$spread = call_user_func_array("array_unique", [["s", "s", "t"]]); -echo ":" . $spread[0] . $spread[2] . ":"; -return function_exists("array_unique");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:ab2:2:ab:2:1:F:v:1:qr:st:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval array projection builtins produce indexed key/value arrays. -#[test] -fn execute_program_dispatches_array_projection_builtins() { - let program = parse_fragment( - br#"$values = array_values(["a" => 10, "b" => 20]); -echo $values[0] . ":" . $values[1]; -$keys = array_keys(["a" => 10, "b" => 20]); -echo ":" . $keys[0] . ":" . $keys[1]; -echo ":" . count(array_values([])); -$call_keys = call_user_func("array_keys", ["z" => 7]); -echo ":" . $call_keys[0]; -$call_values = call_user_func_array("array_values", [["q" => 8]]); -echo ":" . $call_values[0]; -echo ":"; echo function_exists("array_keys"); -return function_exists("array_values");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "10:20:a:b:0:z:8:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_reverse()` handles PHP key preservation rules. -#[test] -fn execute_program_dispatches_array_reverse_builtin() { - let program = parse_fragment( - br#"$indexed = array_reverse([1, 2, 3]); -echo $indexed[0]; echo $indexed[1]; echo $indexed[2]; echo ":"; -$mixed = array_reverse([2 => "a", "k" => "b", 5 => "c"]); -echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; -$preserved = array_reverse([2 => "a", "k" => "b", 5 => "c"], true); -echo $preserved[5]; echo $preserved["k"]; echo $preserved[2]; echo ":"; -$named = array_reverse(array: ["x", "y"], preserve_keys: true); -echo $named[1]; echo $named[0]; echo ":"; -$call = call_user_func("array_reverse", [4, 5]); -echo $call[0]; echo $call[1]; echo ":"; -$spread = call_user_func_array("array_reverse", [[6, 7]]); -echo $spread[0]; echo $spread[1]; echo ":"; -return function_exists("array_reverse");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "321:cba:cba:yx:54:76:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. -#[test] -fn execute_program_dispatches_array_key_exists_builtin() { - let program = parse_fragment( - br#"$map = ["name" => null, "age" => 30]; -echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; -echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; -echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; -echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; -echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; -echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; echo ":"; -return function_exists("array_key_exists");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N:Y:N:Y:Y:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval array search builtins use loose comparison and return keys or booleans. -#[test] -fn execute_program_dispatches_array_search_builtins() { - let program = parse_fragment( - br#"echo in_array(2, [1, 2, 3]) ? "Y" : "bad"; -echo ":"; echo in_array(4, [1, 2, 3]) ? "bad" : "N"; -echo ":" . array_search(20, [10, 20, 30]); -echo ":" . array_search("Grace", ["name" => "Grace"]); -echo ":"; echo array_search("x", ["name" => "Grace"]) === false ? "F" : "bad"; -echo ":"; echo call_user_func("in_array", "b", ["a", "b"]) ? "C" : "bad"; -$found = call_user_func_array("array_search", ["v", ["k" => "v"]]); -echo ":" . $found; -echo ":"; echo function_exists("in_array"); -return function_exists("array_search");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N:1:name:F:C:k:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. -#[test] -fn execute_program_dispatches_explode_implode_builtins() { - let program = parse_fragment( - br#"$parts = explode(",", "a,b,"); -echo count($parts); echo ":" . $parts[0] . ":" . $parts[1] . ":" . $parts[2]; -echo ":" . implode("|", $parts); -echo ":" . implode(separator: "-", array: ["x", 2, true, null]); -$call_parts = call_user_func("explode", ":", "m:n"); -echo ":" . $call_parts[1]; -echo ":" . call_user_func_array("implode", ["separator" => "/", "array" => ["p", "q"]]); -echo ":"; echo function_exists("explode"); -return function_exists("implode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:a:b::a|b|:x-2-1-:n:p/q:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `str_split()` builds indexed arrays of fixed-width chunks. -#[test] -fn execute_program_dispatches_str_split_builtin() { - let program = parse_fragment( - br#"$letters = str_split("abc"); -echo count($letters) . ":" . $letters[0] . $letters[1] . $letters[2]; echo ":"; -$pairs = str_split(string: "abcd", length: 2); -echo $pairs[0] . "-" . $pairs[1]; echo ":"; -$empty = str_split(""); -echo count($empty); echo ":"; -$call = call_user_func("str_split", "xyz", 2); -echo $call[0] . "-" . $call[1]; echo ":"; -$named = call_user_func_array("str_split", ["string" => "pqrs", "length" => 3]); -echo $named[0] . "-" . $named[1]; echo ":"; -return function_exists("str_split");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:abc:ab-cd:0:xy-z:pqr-s:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `str_pad()` supports PHP left, right, and both-side padding modes. -#[test] -fn execute_program_dispatches_str_pad_builtin() { - let program = parse_fragment( - br#"echo "[" . str_pad("hi", 5) . "]"; echo ":"; -echo "[" . str_pad(string: "hi", length: 5, pad_string: "_", pad_type: 0) . "]"; echo ":"; -echo "[" . str_pad("x", 6, "ab", 2) . "]"; echo ":"; -echo call_user_func("str_pad", "42", 5, "0", 0); echo ":"; -echo call_user_func_array("str_pad", ["string" => "x", "length" => 3, "pad_string" => "."]); echo ":"; -return function_exists("str_pad");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "[hi ]:[___hi]:[abxaba]:00042:x..:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval string replacement builtins support direct, named, and callable dispatch. -#[test] -fn execute_program_dispatches_string_replace_builtins() { - let program = parse_fragment( - br#"echo str_replace("o", "0", "Hello World"); echo ":"; -echo str_replace(search: "aa", replace: "b", subject: "aaaa"); echo ":"; -echo str_replace("", "x", "abc"); echo ":"; -echo str_ireplace("HE", "ye", "Hello he"); echo ":"; -echo call_user_func("str_replace", "l", "L", "hello"); echo ":"; -echo call_user_func_array("str_ireplace", ["search" => "x", "replace" => "Y", "subject" => "xX"]); echo ":"; -echo function_exists("str_replace"); -return function_exists("str_ireplace");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. -#[test] -fn execute_program_dispatches_preg_builtins() { - let program = parse_fragment( - br#"$ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); -echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; -echo preg_match("/xyz/", "id42") . ":"; -echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; -$allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); -echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; -$setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); -echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; -preg_match("/(a)?(b)/", "b", $offsetOne, PREG_OFFSET_CAPTURE); -echo $offsetOne[0][0] . ":" . $offsetOne[0][1] . ":" . $offsetOne[1][0] . ":" . $offsetOne[1][1] . ":" . $offsetOne[2][0] . ":" . $offsetOne[2][1] . ":"; -preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); -echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; -preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); -echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; -preg_match("/(a)?(b)(c)?/", "b", $nullOne, PREG_UNMATCHED_AS_NULL); -echo count($nullOne) . ":" . ($nullOne[1] === null ? "n" : "bad") . ":" . $nullOne[2] . ":" . ($nullOne[3] === null ? "n" : "bad") . ":"; -preg_match("/(a)?(b)(c)?/", "b", $nullOffset, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); -echo ($nullOffset[1][0] === null ? "n" : "bad") . ":" . $nullOffset[1][1] . ":" . ($nullOffset[3][0] === null ? "n" : "bad") . ":" . $nullOffset[3][1] . ":"; -preg_match_all("/(a)?(b)(c)?/", "b", $nullAll, PREG_UNMATCHED_AS_NULL); -echo ($nullAll[1][0] === null ? "n" : "bad") . ":" . $nullAll[2][0] . ":" . ($nullAll[3][0] === null ? "n" : "bad") . ":"; -preg_match_all("/(a)?(b)(c)?/", "b", $nullSet, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); -echo ($nullSet[0][1][0] === null ? "n" : "bad") . ":" . $nullSet[0][1][1] . ":" . ($nullSet[0][3][0] === null ? "n" : "bad") . ":" . $nullSet[0][3][1] . ":"; -preg_match_all("/(x)(y)/", "abc", $none); -echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; -echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; -function eval_regex_wrap($matches) { return "[" . $matches[0] . "]"; } -echo preg_replace_callback("/[A-Z]/", "eval_regex_wrap", "AB") . ":"; -$limited = preg_split("/,/", "a,b,c", 2); -echo count($limited) . ":" . $limited[0] . ":" . $limited[1] . ":"; -$kept = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY); -echo count($kept) . ":" . $kept[1] . ":"; -echo call_user_func("preg_match", "/x/", "x") . ":"; -$replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "replacement" => "N", "subject" => "a12"]); -echo $replaced . ":"; -$captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); -echo count($captured) . ":" . $captured[1] . ":"; -$splitOffsets = preg_split("/,/", "a,b,c", 2, PREG_SPLIT_OFFSET_CAPTURE); -echo $splitOffsets[0][0] . ":" . $splitOffsets[0][1] . ":" . $splitOffsets[1][0] . ":" . $splitOffsets[1][1] . ":"; -$splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); -echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; -$splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); -echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; -return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. -#[test] -fn execute_program_dispatches_html_entity_builtins() { - let program = parse_fragment( - br#"echo htmlspecialchars("\"Hi\" & 'bye'"); echo ":"; -echo htmlentities(string: "
"); echo ":"; -echo html_entity_decode("<b>hi</b>"); echo ":"; -echo call_user_func("htmlspecialchars", ""); echo ":"; -echo call_user_func_array("html_entity_decode", ["string" => ""q""]); echo ":"; -echo function_exists("htmlspecialchars"); echo function_exists("htmlentities"); -return function_exists("html_entity_decode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval URL codec builtins dispatch through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_url_codec_builtins() { - let program = parse_fragment( - br#"echo urlencode("a b&=~"); echo ":"; -echo rawurlencode(string: "a b&=~"); echo ":"; -echo urldecode("a+b%26%3D%7E"); echo ":"; -echo rawurldecode("a+b%26%3D%7E"); echo ":"; -echo call_user_func("urlencode", "%zz"); echo ":"; -echo call_user_func_array("rawurldecode", ["string" => "x%2By%zz"]); echo ":"; -echo function_exists("urlencode"); echo function_exists("rawurlencode"); -echo function_exists("urldecode"); -return function_exists("rawurldecode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_ctype_builtins() { - let program = parse_fragment( - br#"echo ctype_alpha("abc") ? "A" : "-"; echo ":"; -echo ctype_digit(text: "123") ? "D" : "-"; echo ":"; -echo ctype_alnum("a1") ? "N" : "-"; echo ":"; -echo ctype_space(" \t\n" . chr(11) . chr(12) . "\r") ? "S" : "-"; echo ":"; -echo ctype_alpha("") ? "bad" : "empty"; echo ":"; -echo call_user_func("ctype_digit", "12x") ? "bad" : "not-digit"; echo ":"; -echo call_user_func_array("ctype_space", ["text" => " x"]) ? "bad" : "not-space"; echo ":"; -echo function_exists("ctype_alpha"); echo function_exists("ctype_digit"); -echo function_exists("ctype_alnum"); -return function_exists("ctype_space");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A:D:N:S:empty:not-digit:not-space:111"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. -#[test] -fn execute_program_dispatches_crc32_builtin() { - let program = parse_fragment( - br#"echo crc32(""); echo ":"; -echo crc32(string: "123456789"); echo ":"; -echo call_user_func("crc32", "hello"); echo ":"; -echo call_user_func_array("crc32", ["string" => "The quick brown fox jumps over the lazy dog"]); echo ":"; -return function_exists("crc32");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "0:3421780262:907060870:1095738169:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `hash_algos()` returns supported hash names through callable dispatch too. -#[test] -fn execute_program_dispatches_hash_algos_builtin() { - let program = parse_fragment( - br#"$algos = hash_algos(); -echo count($algos) . ":" . $algos[0] . ":" . $algos[5] . ":"; -echo in_array("crc32c", $algos) ? "crc" : "bad"; -$call = call_user_func("hash_algos"); -echo ":" . $call[18]; -$spread = call_user_func_array("hash_algos", []); -echo ":" . $spread[27] . ":"; -echo function_exists("hash_algos") ? "exists" : "missing"; -return count($algos);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "28:md2:sha256:crc:whirlpool:joaat:exists"); - assert_eq!(values.get(result), FakeValue::Int(28)); -} - -/// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. -#[test] -fn execute_program_dispatches_hash_digest_builtins() { - let filename = format!("elephc_eval_hash_file_{}.txt", std::process::id()); - let source = format!( - r#"echo md5("abc"); echo ":"; -echo sha1(string: "abc"); echo ":"; -echo hash("sha256", "abc"); echo ":"; -echo hash_hmac(algo: "sha256", data: "data", key: "key"); echo ":"; -echo bin2hex(md5("abc", true)); echo ":"; -echo bin2hex(call_user_func("sha1", "abc", true)); echo ":"; -echo call_user_func_array("hash", ["algo" => "md5", "data" => "abc"]); echo ":"; -echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; -file_put_contents("{filename}", "abc"); -echo hash_file("sha256", "{filename}"); echo ":"; -echo bin2hex(hash_file(algo: "md5", filename: "{filename}", binary: true)); echo ":"; -echo call_user_func_array("hash_file", ["algo" => "md5", "filename" => "{filename}"]); echo ":"; -echo hash_file("sha256", "{filename}.missing") === false ? "missing" : "bad"; echo ":"; -unlink("{filename}"); -echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); -return function_exists("hash_hmac");"#, - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - concat!( - "900150983cd24fb0d6963f7d28e17f72:", - "a9993e364706816aba3e25717850c26c9cd0d89d:", - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", - "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", - "900150983cd24fb0d6963f7d28e17f72:", - "a9993e364706816aba3e25717850c26c9cd0d89d:", - "900150983cd24fb0d6963f7d28e17f72:", - "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", - "900150983cd24fb0d6963f7d28e17f72:", - "900150983cd24fb0d6963f7d28e17f72:", - "missing:", - "1111" - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval zero-argument system builtins return native-compatible values. -#[test] -fn execute_program_dispatches_zero_arg_system_builtins() { - let program = parse_fragment( - br#"echo time() > 1000000000 ? "time" : "bad"; echo ":"; -echo phpversion(); echo ":"; -echo sys_get_temp_dir(); echo ":"; -echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; -echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; -echo call_user_func("phpversion"); echo ":"; -echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; -echo call_user_func_array("sys_get_temp_dir", []); echo ":"; -echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); -return function_exists("sys_get_temp_dir");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - format!( - "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", - eval_compiler_php_version(), - eval_compiler_php_version() - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `date()` formats libc local timestamps and `mktime()` builds them. -#[test] -fn execute_program_dispatches_date_mktime_builtins() { - let program = parse_fragment( - br#"$ts = mktime(13, 2, 3, 1, 2, 2024); -echo date("Y-m-d H:i:s", $ts); -echo ":" . date("j-n-G-g-A-a-N-D-M-l-F", $ts); -echo ":" . (date("U", $ts) === strval($ts) ? "U" : "bad"); -echo ":" . call_user_func("date", "Y", $ts); -$named = call_user_func_array("mktime", ["hour" => 0, "minute" => 0, "second" => 0, "month" => 1, "day" => 1, "year" => 2000]); -echo ":" . date(format: "Y", timestamp: $named); -echo ":"; echo function_exists("date"); -return function_exists("mktime");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. -#[test] -fn execute_program_dispatches_strtotime_builtin() { - let program = parse_fragment( - br#"$date = strtotime("2024-06-15"); -echo date("Y-m-d H:i:s", $date); -$full = strtotime("2024-06-15 12:30:45"); -echo ":" . date("Y-m-d H:i:s", $full); -$short = strtotime("2024-06-15T12:30"); -echo ":" . date("Y-m-d H:i:s", $short); -echo ":" . (strtotime("2024/06/15") === -1 ? "bad" : "wrong"); -$call = call_user_func("strtotime", "2024-01-02 03:04:05"); -echo ":" . date("Y-m-d H:i:s", $call); -$spread = call_user_func_array("strtotime", ["datetime" => "2024-01-02"]); -echo ":" . date("Y-m-d", $spread) . ":"; -return function_exists("strtotime");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. -#[test] -fn execute_program_dispatches_microtime_builtin() { - let program = parse_fragment( - br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; -echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; -echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; -echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; -echo ":"; -return function_exists("microtime");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "now:named:call:array:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. -#[test] -fn execute_program_dispatches_realpath_cache_builtins() { - let program = parse_fragment( - br#"$cache = realpath_cache_get(); -echo count($cache) . ":" . realpath_cache_size() . ":"; -$call_cache = call_user_func("realpath_cache_get"); -echo count($call_cache) . ":"; -echo call_user_func_array("realpath_cache_size", []) . ":"; -echo function_exists("realpath_cache_get"); -return function_exists("realpath_cache_size");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "0:0:0:0:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval stream introspection builtins return native-compatible static lists. -#[test] -fn execute_program_dispatches_stream_introspection_builtins() { - let program = parse_fragment( - br#"$wrappers = stream_get_wrappers(); -$transports = stream_get_transports(); -$filters = stream_get_filters(); -echo count($wrappers) . ":" . $wrappers[0] . ":" . $wrappers[5] . ":"; -echo count($transports) . ":" . $transports[0] . ":" . $transports[8] . ":"; -echo count($filters) . ":" . $filters[2] . ":"; -$call_wrappers = call_user_func("stream_get_wrappers"); -echo $call_wrappers[10] . ":"; -$call_transports = call_user_func_array("stream_get_transports", []); -echo $call_transports[11] . ":"; -$call_filters = call_user_func_array("stream_get_filters", []); -echo $call_filters[13] . ":"; -echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); -return function_exists("stream_get_filters");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. -#[test] -fn execute_program_dispatches_spl_classes_builtin() { - let program = parse_fragment( - br#"$names = spl_classes(); -echo count($names) . ":" . $names[0] . ":" . $names[55] . ":"; -echo (in_array("Exception", $names) ? "exception" : "bad") . ":"; -echo (in_array("SplDoublyLinkedList", $names) ? "list" : "bad") . ":"; -$call = call_user_func("spl_classes"); -echo (in_array("Throwable", $call) ? "call" : "bad") . ":"; -$spread = call_user_func_array("spl_classes", []); -echo (count($spread) === count($names) ? "spread" : "bad") . ":"; -echo function_exists("spl_classes"); -return is_callable("spl_classes");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "61:AppendIterator:Throwable:exception:list:call:spread:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval SPL object identity builtins are stable, unique, and callable. -#[test] -fn execute_program_dispatches_spl_object_identity_builtins() { - let program = parse_fragment( - br#"$a = new KnownClass(); -$b = new KnownClass(); -echo (spl_object_id($a) === spl_object_id($a)) ? "stable" : "drift"; -echo ":"; -echo (spl_object_id($a) !== spl_object_id($b)) ? "unique" : "same"; -echo ":"; -echo (spl_object_hash(object: $a) === spl_object_hash($a)) ? "hash" : "bad"; -echo ":"; -echo (call_user_func("spl_object_id", $a) === spl_object_id($a)) ? "call" : "bad"; -echo ":"; -echo (call_user_func_array("spl_object_hash", ["object" => $b]) === spl_object_hash($b)) ? "array" : "bad"; -echo ":"; -echo function_exists("spl_object_id"); -return function_exists("spl_object_hash");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "stable:unique:hash:call:array:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval environment builtins read, write, unset, and dispatch dynamically. -#[test] -fn execute_program_dispatches_environment_builtins() { - let program = parse_fragment( - br#"putenv("ELEPHC_EVAL_ENV_TEST=direct"); -echo getenv("ELEPHC_EVAL_ENV_TEST") . ":"; -putenv(assignment: "ELEPHC_EVAL_ENV_TEST=named"); -echo getenv(name: "ELEPHC_EVAL_ENV_TEST") . ":"; -echo call_user_func("getenv", "ELEPHC_EVAL_ENV_TEST") . ":"; -echo call_user_func_array("putenv", ["assignment" => "ELEPHC_EVAL_ENV_TEST=spread"]) ? "set" : "bad"; -echo ":" . getenv("ELEPHC_EVAL_ENV_TEST") . ":"; -putenv("ELEPHC_EVAL_ENV_TEST"); -echo getenv("ELEPHC_EVAL_ENV_TEST") === "" ? "empty" : "bad"; -echo ":"; echo function_exists("getenv"); -return function_exists("putenv");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval sleep builtins dispatch without delaying focused tests. -#[test] -fn execute_program_dispatches_sleep_builtins() { - let program = parse_fragment( - br#"echo sleep(0) . ":"; -echo sleep(seconds: 0) . ":"; -usleep(0); -echo "u:"; -echo call_user_func("sleep", 0) . ":"; -echo call_user_func_array("usleep", ["microseconds" => 0]) === null ? "null" : "bad"; -echo ":"; echo function_exists("sleep"); -return function_exists("usleep");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "0:0:u:0:null:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. -#[test] -fn execute_program_dispatches_php_uname_builtin() { - let program = parse_fragment( - br#"echo strlen(php_uname()) > 0 ? "all" : "empty"; echo ":"; -echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; -echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; -echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; -echo strlen(php_uname("r")) > 0 ? "release" : "empty"; echo ":"; -echo strlen(php_uname("v")) > 0 ? "version" : "empty"; echo ":"; -echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; -echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; -echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; -return function_exists("php_uname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "all:same:sys:node:release:version:machine:call:spread:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. -#[test] -fn execute_program_dispatches_gethostbyname_builtin() { - let program = parse_fragment( - br#"echo gethostbyname("127.0.0.1") . ":"; -echo gethostbyname(hostname: "not a host") . ":"; -echo call_user_func("gethostbyname", "127.0.0.1") . ":"; -echo call_user_func_array("gethostbyname", ["hostname" => "not a host"]) . ":"; -return function_exists("gethostbyname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "127.0.0.1:not a host:127.0.0.1:not a host:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. -#[test] -fn execute_program_dispatches_gethostname_builtin() { - let program = parse_fragment( - br#"echo strlen(gethostname()) > 0 ? "host" : "empty"; echo ":"; -echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; -echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; -return function_exists("gethostname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "host:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. -#[test] -fn execute_program_dispatches_gethostbyaddr_builtin() { - let program = parse_fragment( - br#"echo strlen(gethostbyaddr("127.0.0.1")) > 0 ? "direct" : "empty"; echo ":"; -echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; -echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; -echo strlen(call_user_func("gethostbyaddr", "127.0.0.1")) > 0 ? "call" : "empty"; echo ":"; -echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === false ? "spread" : "bad"; echo ":"; -return function_exists("gethostbyaddr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "direct:named:false:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval protocol and service database lookups dispatch dynamically. -#[test] -fn execute_program_dispatches_protocol_service_builtins() { - let program = parse_fragment( - br#"echo getprotobyname("TCP") . ":"; -echo getprotobynumber(6) . ":"; -echo getprotobyname("no_such_protocol") === false ? "missing-proto" : "bad"; echo ":"; -echo getprotobynumber(999) === false ? "missing-number" : "bad"; echo ":"; -echo getservbyname("www", "tcp") . ":"; -echo getservbyport(80, "tcp") . ":"; -echo getservbyname("no_such_service", "tcp") === false ? "missing-service" : "bad"; echo ":"; -echo getservbyport(80, "no_such_proto") === false ? "missing-port" : "bad"; echo ":"; -echo call_user_func("getprotobyname", "udp") . ":"; -echo call_user_func_array("getprotobynumber", ["protocol" => 17]) . ":"; -echo call_user_func("getservbyname", "https", "tcp") . ":"; -echo call_user_func_array("getservbyport", ["port" => 443, "protocol" => "tcp"]) . ":"; -echo function_exists("getprotobyname"); echo function_exists("getprotobynumber"); echo function_exists("getservbyname"); -return function_exists("getservbyport");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. -#[test] -fn execute_program_dispatches_ip_conversion_builtins() { - let program = parse_fragment( - br#"echo long2ip(3232235777) . ":"; -echo long2ip(ip: 4294967295) . ":"; -echo ip2long("192.168.1.1") . ":"; -echo ip2long(ip: "1.2.3") === false ? "bad-ip" : "bad"; echo ":"; -$packed = inet_pton("1.2.3.4"); -echo bin2hex($packed) . ":"; -echo inet_pton(ip: "nonsense") === false ? "bad-pton" : "bad"; echo ":"; -echo inet_ntop($packed) . ":"; -echo inet_ntop(ip: "xx") === false ? "bad-ntop" : "bad"; echo ":"; -echo call_user_func("long2ip", 2130706433) . ":"; -echo call_user_func_array("ip2long", ["ip" => "0.0.0.0"]) . ":"; -echo function_exists("long2ip"); echo function_exists("ip2long"); -echo function_exists("inet_pton"); -return function_exists("inet_ntop");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval path component builtins mirror static basename/dirname edge cases. -#[test] -fn execute_program_dispatches_path_component_builtins() { - let program = parse_fragment( - br#"echo basename("/var/log/syslog.log", ".log") . ":"; -echo basename(path: "/usr///") . ":"; -echo basename("/", "x") === "" ? "root" : "bad"; echo ":"; -echo dirname("/usr/local/bin/tool", 2) . ":"; -echo dirname(path: "/usr///local///bin") . ":"; -echo call_user_func("basename", "foo.tar.gz", ".bz2") . ":"; -echo call_user_func_array("dirname", ["path" => "/usr", "levels" => 3]) . ":"; -echo function_exists("basename"); -return function_exists("dirname");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `realpath()` resolves existing paths and returns false for misses. -#[test] -fn execute_program_dispatches_realpath_builtin() { - let program = parse_fragment( - br#"echo realpath(".") !== false ? "resolved" : "bad"; echo ":"; -echo realpath(path: "elephc-eval-missing-path") === false ? "false" : "bad"; echo ":"; -echo call_user_func("realpath", ".") !== false ? "call" : "bad"; echo ":"; -echo call_user_func_array("realpath", ["path" => "elephc-eval-missing-path"]) === false ? "array-false" : "bad"; -echo ":"; -return function_exists("realpath");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "resolved:false:call:array-false:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. -#[test] -fn execute_program_dispatches_fnmatch_builtin() { - let program = parse_fragment( - br#"echo fnmatch("*.log", "system.log") ? "match" : "bad"; echo ":"; -echo fnmatch("*.log", "logs/system.log", FNM_PATHNAME) ? "bad" : "path"; echo ":"; -echo fnmatch("*.LOG", "system.log", FNM_CASEFOLD) ? "case" : "bad"; echo ":"; -echo fnmatch("*", ".env", FNM_PERIOD) ? "bad" : "period"; echo ":"; -echo fnmatch("[!abc]oo", "doo") && !fnmatch("[!abc]oo", "boo") ? "class" : "bad"; echo ":"; -echo fnmatch('a\\*b', 'a*b') ? "escape" : "bad"; echo ":"; -echo fnmatch('a\\*b', 'a\\xxb', FNM_NOESCAPE) ? "noescape" : "bad"; echo ":"; -$flags = FNM_PATHNAME | FNM_CASEFOLD; -echo fnmatch("dir/*.TXT", "dir/file.txt", $flags) ? "flags" : "bad"; echo ":"; -echo call_user_func("fnmatch", "*.txt", "report.txt") ? "call" : "bad"; echo ":"; -echo call_user_func_array("fnmatch", ["pattern" => "*.TXT", "filename" => "report.txt", "flags" => FNM_CASEFOLD]) ? "callarray" : "bad"; echo ":"; -echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD"); -return FNM_CASEFOLD;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "match:path:case:period:class:escape:noescape:flags:call:callarray:11" - ); - assert_eq!(values.get(result), FakeValue::Int(EVAL_FNM_CASEFOLD)); -} - -/// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. -#[test] -fn execute_program_dispatches_pathinfo_builtin() { - let program = parse_fragment( - br#"$info = pathinfo("/var/log/syslog.log"); -echo $info["dirname"] . "|" . $info["basename"] . "|" . $info["extension"] . "|" . $info["filename"] . ":"; -echo pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":"; -echo pathinfo(".bashrc", PATHINFO_FILENAME) === "" ? "dotfile" : "bad"; echo ":"; -echo pathinfo("file.", PATHINFO_EXTENSION) === "" ? "trail" : "bad"; echo ":"; -echo pathinfo("", PATHINFO_DIRNAME) === "" ? "empty-dir" : "bad"; echo ":"; -$plain = pathinfo("/etc/hosts"); -echo array_key_exists("extension", $plain) ? "bad" : "no-ext"; echo ":"; -echo pathinfo("/a/b.php", PATHINFO_BASENAME | PATHINFO_FILENAME) . ":"; -$call = call_user_func("pathinfo", "foo.txt", PATHINFO_ALL); -echo $call["basename"] . ":"; -echo call_user_func_array("pathinfo", ["path" => "foo.txt", "flags" => 0]) === "" ? "zero" : "bad"; -echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL"); -return PATHINFO_ALL;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" - ); - assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); -} - -/// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. -#[test] -fn execute_program_dispatches_filesystem_builtins() { - let filename = format!("elephc_eval_fs_probe_{}.txt", std::process::id()); - let missing = format!("elephc_eval_fs_missing_{}.txt", std::process::id()); - let source = format!( - r#"echo file_put_contents("{filename}", "hello") . ":"; -echo file_get_contents("{filename}") . ":"; -echo file_exists("{filename}") ? "exists" : "missing"; echo ":"; -echo is_file(filename: "{filename}") ? "file" : "bad"; echo ":"; -echo is_dir(".") ? "dir" : "bad"; echo ":"; -echo is_readable("{filename}") ? "readable" : "bad"; echo ":"; -echo is_writable("{filename}") ? "writable" : "bad"; echo ":"; -echo is_writeable("{filename}") ? "writeable" : "bad"; echo ":"; -echo filesize("{filename}") . ":"; -echo file_get_contents("{missing}") === false ? "missing-false" : "bad"; echo ":"; -echo call_user_func("file_exists", "{filename}") ? "call-exists" : "bad"; echo ":"; -echo call_user_func_array("filesize", ["filename" => "{filename}"]) . ":"; -echo unlink("{filename}") ? "unlinked" : "bad"; echo ":"; -echo file_exists("{filename}") ? "bad" : "gone"; echo ":"; -echo function_exists("file_get_contents"); echo function_exists("file_put_contents"); -echo function_exists("file_exists"); echo function_exists("is_file"); echo function_exists("is_dir"); -echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); -echo function_exists("filesize"); -return function_exists("unlink");"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - assert_eq!( - values.output, - "5:hello:exists:file:dir:readable:writable:writeable:5:missing-false:call-exists:5:unlinked:gone:111111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval disk-space builtins query local filesystem capacity and dispatch dynamically. -#[test] -fn execute_program_dispatches_disk_space_builtins() { - let program = parse_fragment( - br#"echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; -echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; -echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; -echo disk_free_space("no/such/path/elephc-eval") === 0.0 ? "missing" : "bad"; echo ":"; -echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; -echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; echo ":"; -echo function_exists("disk_free_space"); -return function_exists("disk_total_space");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "free:total:ordered:missing:call:spread:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval stat metadata builtins expose scalar file metadata and link probes. -#[test] -fn execute_program_dispatches_stat_metadata_builtins() { - let filename = format!("elephc_eval_stat_probe_{}.txt", std::process::id()); - let missing = format!("elephc_eval_stat_missing_{}.txt", std::process::id()); - let link = format!("elephc_eval_stat_link_{}.txt", std::process::id()); - let source = format!( - r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; -echo fileatime("{filename}") > 0 ? "atime" : "bad"; echo ":"; -echo filectime("{filename}") > 0 ? "ctime" : "bad"; echo ":"; -echo fileperms("{filename}") > 0 ? "perms" : "bad"; echo ":"; -echo fileowner("{filename}") >= 0 ? "owner" : "bad"; echo ":"; -echo filegroup("{filename}") >= 0 ? "group" : "bad"; echo ":"; -echo fileinode("{filename}") > 0 ? "inode" : "bad"; echo ":"; -echo filetype("{filename}") . ":"; -echo filetype(".") . ":"; -echo filetype("{link}") . ":"; -echo is_executable("{filename}") ? "bad" : "noexec"; echo ":"; -echo is_link("{link}") ? "link" : "bad"; echo ":"; -echo fileatime("{missing}") === false ? "missing-atime" : "bad"; echo ":"; -echo filectime("{missing}") === false ? "missing-ctime" : "bad"; echo ":"; -echo fileperms("{missing}") === false ? "missing-perms" : "bad"; echo ":"; -echo fileowner("{missing}") === false ? "missing-owner" : "bad"; echo ":"; -echo filegroup("{missing}") === false ? "missing-group" : "bad"; echo ":"; -echo fileinode("{missing}") === false ? "missing-inode" : "bad"; echo ":"; -echo filetype("{missing}") === false ? "missing-type" : "bad"; echo ":"; -echo filemtime("{missing}") === 0 ? "missing-mtime" : "bad"; echo ":"; -echo call_user_func("filetype", "{filename}") . ":"; -echo call_user_func_array("fileinode", ["filename" => "{filename}"]) > 0 ? "callinode" : "bad"; echo ":"; -echo function_exists("filemtime"); echo function_exists("fileatime"); -echo function_exists("filectime"); echo function_exists("fileperms"); -echo function_exists("fileowner"); echo function_exists("filegroup"); -echo function_exists("fileinode"); echo function_exists("filetype"); -echo function_exists("is_executable"); echo function_exists("is_link"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - std::fs::write(&filename, b"hello").expect("write stat fixture"); - std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - assert_eq!( - values.output, - "mtime:atime:ctime:perms:owner:group:inode:file:dir:link:noexec:link:missing-atime:missing-ctime:missing-perms:missing-owner:missing-group:missing-inode:missing-type:missing-mtime:file:callinode:1111111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. -#[test] -fn execute_program_dispatches_stat_array_builtins() { - let pid = std::process::id(); - let filename = format!("elephc_eval_stat_array_{pid}.txt"); - let link = format!("elephc_eval_lstat_array_{pid}.txt"); - let missing = format!("elephc_eval_stat_array_missing_{pid}.txt"); - let source = format!( - r#"$stat = stat("{filename}"); -$lstat = lstat("{link}"); -echo $stat["size"] === 5 && $stat[7] === $stat["size"] ? "stat" : "bad"; echo ":"; -echo ($stat["mode"] & 61440) === 32768 ? "mode" : "bad"; echo ":"; -echo ($lstat["mode"] & 61440) === 40960 ? "lstat" : "bad"; echo ":"; -echo stat("{missing}") === false && lstat("{missing}") === false ? "missing" : "bad"; echo ":"; -$call = call_user_func("stat", "{filename}"); -echo $call["mtime"] === filemtime("{filename}") ? "callstat" : "bad"; echo ":"; -$call_lstat = call_user_func_array("lstat", ["filename" => "{link}"]); -echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; -echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("stat"); echo function_exists("lstat"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - std::fs::write(&filename, b"hello").expect("write stat array fixture"); - std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&link); - assert_eq!( - values.output, - "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval local path operation builtins mutate filesystem state. -#[test] -fn execute_program_dispatches_path_operation_builtins() { - let pid = std::process::id(); - let dir = format!("elephc_eval_ops_dir_{pid}"); - let call_dir = format!("elephc_eval_ops_call_dir_{pid}"); - let src = format!("elephc_eval_ops_src_{pid}.txt"); - let copy = format!("elephc_eval_ops_copy_{pid}.txt"); - let moved = format!("elephc_eval_ops_moved_{pid}.txt"); - let symlink = format!("elephc_eval_ops_symlink_{pid}.txt"); - let hardlink = format!("elephc_eval_ops_hardlink_{pid}.txt"); - let source = format!( - r#"file_put_contents("{src}", "hello"); -echo mkdir("{dir}") ? "mkdir" : "bad"; echo ":"; -echo is_dir("{dir}") ? "dir" : "bad"; echo ":"; -echo copy("{src}", "{copy}") && file_get_contents("{copy}") === "hello" ? "copy" : "bad"; echo ":"; -echo rename("{copy}", "{moved}") && file_exists("{moved}") && !file_exists("{copy}") ? "rename" : "bad"; echo ":"; -echo symlink("{src}", "{symlink}") ? "symlink" : "bad"; echo ":"; -echo readlink("{symlink}") === "{src}" ? "readlink" : "bad"; echo ":"; -echo linkinfo("{symlink}") >= 0 ? "linkinfo" : "bad"; echo ":"; -echo readlink("{src}") === false ? "readlink-false" : "bad"; echo ":"; -echo linkinfo("{missing}") === -1 ? "linkinfo-missing" : "bad"; echo ":"; -echo link("{src}", "{hardlink}") && file_get_contents("{hardlink}") === "hello" ? "hardlink" : "bad"; echo ":"; -echo clearstatcache() === null ? "cache" : "bad"; echo ":"; -echo unlink("{symlink}") && unlink("{hardlink}") && unlink("{moved}") && unlink("{src}") && rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; -echo call_user_func("mkdir", "{call_dir}") ? "callmkdir" : "bad"; echo ":"; -echo call_user_func_array("rmdir", ["directory" => "{call_dir}"]) ? "callrmdir" : "bad"; echo ":"; -echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exists("copy"); -echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); -echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); -return true;"#, - missing = format!("elephc_eval_ops_missing_{pid}.txt"), - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - for path in [&symlink, &hardlink, &moved, ©, &src] { - let _ = std::fs::remove_file(path); - } - for path in [&call_dir, &dir] { - let _ = std::fs::remove_dir(path); - } - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - for path in [&symlink, &hardlink, &moved, ©, &src] { - let _ = std::fs::remove_file(path); - } - for path in [&call_dir, &dir] { - let _ = std::fs::remove_dir(path); - } - assert_eq!( - values.output, - "mkdir:dir:copy:rename:symlink:readlink:linkinfo:readlink-false:linkinfo-missing:hardlink:cache:cleanup:callmkdir:callrmdir:111111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. -#[test] -fn execute_program_dispatches_file_listing_builtins() { - let pid = std::process::id(); - let lines = format!("elephc_eval_listing_lines_{pid}.txt"); - let empty = format!("elephc_eval_listing_empty_{pid}.txt"); - let missing = format!("elephc_eval_listing_missing_{pid}.txt"); - let dir = format!("elephc_eval_listing_dir_{pid}"); - let source = format!( - r#"file_put_contents("{lines}", "one\ntwo"); -file_put_contents("{empty}", ""); -$lines = file("{lines}"); -echo count($lines) . ":"; -echo $lines[0] === "one\n" ? "line0" : "bad"; echo ":"; -echo $lines[1] === "two" ? "line1" : "bad"; echo ":"; -echo "["; -$bytes = readfile(filename: "{empty}"); -echo "]" . $bytes . ":"; -echo readfile("{missing}") === false ? "missing-readfile" : "bad"; echo ":"; -echo count(file("{missing}")) === 0 ? "missing-file" : "bad"; echo ":"; -mkdir("{dir}"); -file_put_contents("{dir}/a.txt", "a"); -file_put_contents("{dir}/b.txt", "b"); -$scan = scandir(directory: "{dir}"); -echo count($scan) . ":"; -echo in_array(".", $scan) && in_array("..", $scan) && in_array("a.txt", $scan) && in_array("b.txt", $scan) ? "scan" : "bad"; echo ":"; -$call_lines = call_user_func("file", "{lines}"); -echo $call_lines[0] === "one\n" ? "callfile" : "bad"; echo ":"; -$call_scan = call_user_func_array("scandir", ["directory" => "{dir}"]); -echo count($call_scan) . ":"; -echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") && unlink("{lines}") && unlink("{empty}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - for path in [&lines, &empty, &missing] { - let _ = std::fs::remove_file(path); - } - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.txt")); - let _ = std::fs::remove_dir(&dir); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - for path in [&lines, &empty, &missing] { - let _ = std::fs::remove_file(path); - } - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.txt")); - let _ = std::fs::remove_dir(&dir); - assert_eq!( - values.output, - "2:line0:line1:[]0:missing-readfile:missing-file:4:scan:callfile:4:cleanup:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `glob()` expands local patterns and dispatches dynamically. -#[test] -fn execute_program_dispatches_glob_builtin() { - let pid = std::process::id(); - let dir = format!("elephc_eval_glob_dir_{pid}"); - let source = format!( - r#"mkdir("{dir}"); -file_put_contents("{dir}/a.txt", "a"); -file_put_contents("{dir}/b.log", "b"); -file_put_contents("{dir}/c.txt", "c"); -file_put_contents("{dir}/.hidden.txt", "h"); -$matches = glob("{dir}/*.txt"); -echo count($matches) === 2 && basename($matches[0]) === "a.txt" && basename($matches[1]) === "c.txt" ? "glob" : "bad"; echo ":"; -echo count(glob("{dir}/*.none")) === 0 ? "empty" : "bad"; echo ":"; -$literal = glob("{dir}/a.txt"); -echo count($literal) === 1 && $literal[0] === "{dir}/a.txt" ? "literal" : "bad"; echo ":"; -$all = glob("{dir}/*"); -echo in_array("{dir}/.hidden.txt", $all) ? "bad" : "hidden"; echo ":"; -$call = call_user_func("glob", "{dir}/*.log"); -echo count($call) === 1 && basename($call[0]) === "b.log" ? "callglob" : "bad"; echo ":"; -$call_array = call_user_func_array("glob", ["pattern" => "{dir}/*.txt"]); -echo count($call_array) === 2 ? "callarray" : "bad"; echo ":"; -unlink("{dir}/.hidden.txt"); -unlink("{dir}/c.txt"); -unlink("{dir}/b.log"); -unlink("{dir}/a.txt"); -echo rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("glob"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); - let _ = std::fs::remove_file(format!("{dir}/c.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.log")); - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_dir(&dir); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); - let _ = std::fs::remove_file(format!("{dir}/c.txt")); - let _ = std::fs::remove_file(format!("{dir}/b.log")); - let _ = std::fs::remove_file(format!("{dir}/a.txt")); - let _ = std::fs::remove_dir(&dir); - assert_eq!( - values.output, - "glob:empty:literal:hidden:callglob:callarray:cleanup:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. -#[test] -fn execute_program_dispatches_file_modify_builtins() { - let pid = std::process::id(); - let filename = format!("elephc_eval_modify_{pid}.txt"); - let missing = format!("elephc_eval_modify_missing_{pid}.txt"); - let prefix = format!("evm{pid}_"); - let call_prefix = format!("evc{pid}_"); - let source = format!( - r#"file_put_contents("{filename}", "x"); -echo chmod(filename: "{filename}", permissions: 384) ? "chmod" : "bad"; echo ":"; -echo (fileperms("{filename}") & 511) === 384 ? "mode" : "bad"; echo ":"; -echo chmod("{missing}", 384) ? "bad" : "chmod-false"; echo ":"; -$tmp = tempnam(directory: ".", prefix: "{prefix}"); -echo file_exists($tmp) && str_starts_with(basename($tmp), "{prefix}") ? "tempnam" : "bad"; echo ":"; -unlink($tmp); -$previous = umask(mask: 18); -$set = umask($previous); -echo $set === 18 ? "umask" : "bad"; echo ":"; -$before = umask(18); -$probe = umask(); -$restore = umask($before); -echo $probe === 18 && $restore === 18 ? "probe" : "bad"; echo ":"; -echo call_user_func("chmod", "{filename}", 420) ? "callchmod" : "bad"; echo ":"; -$call_tmp = call_user_func_array("tempnam", ["directory" => ".", "prefix" => "{call_prefix}"]); -echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "{call_prefix}") ? "calltempnam" : "bad"; echo ":"; -unlink($call_tmp); -echo unlink("{filename}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&missing); - for entry in std::fs::read_dir(".").expect("read eval test cwd") { - let entry = entry.expect("read eval temp entry"); - let name = entry.file_name().to_string_lossy().into_owned(); - if name.starts_with(&prefix) || name.starts_with(&call_prefix) { - let _ = std::fs::remove_file(entry.path()); - } - } - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&filename); - let _ = std::fs::remove_file(&missing); - for entry in std::fs::read_dir(".").expect("read eval test cwd") { - let entry = entry.expect("read eval temp entry"); - let name = entry.file_name().to_string_lossy().into_owned(); - if name.starts_with(&prefix) || name.starts_with(&call_prefix) { - let _ = std::fs::remove_file(entry.path()); - } - } - assert_eq!( - values.output, - "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. -#[test] -fn execute_program_dispatches_touch_builtin() { - let pid = std::process::id(); - let created = format!("elephc_eval_touch_created_{pid}.txt"); - let stamped = format!("elephc_eval_touch_stamped_{pid}.txt"); - let missing = format!("elephc_eval_touch_missing_{pid}/x.txt"); - let source = format!( - r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; -file_put_contents("{stamped}", "x"); -echo touch("{stamped}", 1000000000) ? "mtime" : "bad"; echo ":"; -echo filemtime("{stamped}") === 1000000000 ? "readmtime" : "bad"; echo ":"; -echo touch("{stamped}", 1000000001, null) && filemtime("{stamped}") === 1000000001 ? "nullatime" : "bad"; echo ":"; -echo touch("{stamped}", 1000000002, 1000000003) && filemtime("{stamped}") === 1000000002 ? "both" : "bad"; echo ":"; -echo touch("{missing}") ? "bad" : "touch-false"; echo ":"; -echo call_user_func("touch", "{created}", 1000000004) ? "calltouch" : "bad"; echo ":"; -echo call_user_func_array("touch", ["filename" => "{stamped}", "mtime" => 1000000005]) ? "callarray" : "bad"; echo ":"; -echo unlink("{created}") && unlink("{stamped}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("touch"); -return true;"# - ); - let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - let _ = std::fs::remove_file(&created); - let _ = std::fs::remove_file(&stamped); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - let _ = std::fs::remove_file(&created); - let _ = std::fs::remove_file(&stamped); - assert_eq!( - values.output, - "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval ASCII string case builtins work directly and through callable dispatch. -#[test] -fn execute_program_dispatches_string_case_builtins() { - let program = parse_fragment( - br#"echo strtoupper("Hello World"); echo ":"; -echo strtolower("LOUD"); echo ":"; -echo ucfirst("eval"); echo ":"; -echo lcfirst("LOUD"); echo ":"; -echo call_user_func("strtoupper", "xy"); echo ":"; -echo call_user_func_array("strtolower", ["ZZ"]); echo ":"; -echo call_user_func("ucfirst", "case"); echo ":"; -echo call_user_func_array("lcfirst", ["CASE"]); -echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower"); echo function_exists("ucfirst"); -return function_exists("lcfirst");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `ucwords()` capitalizes word starts directly and by callable dispatch. -#[test] -fn execute_program_dispatches_ucwords_builtin() { - let program = parse_fragment( - br#"echo ucwords("hello world"); echo ":"; -echo ucwords(string: "hello-world", separators: "-"); echo ":"; -echo ucwords("hello\tworld"); echo ":"; -echo call_user_func("ucwords", "a b"); echo ":"; -echo call_user_func_array("ucwords", ["string" => "a-b", "separators" => "-"]); echo ":"; -return function_exists("ucwords");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Hello World:Hello-World:Hello\tWorld:A B:A-B:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. -#[test] -fn execute_program_dispatches_wordwrap_builtin() { - let program = parse_fragment( - br#"echo wordwrap("The quick brown fox", 10, "|"); echo ":"; -echo wordwrap(string: "A verylongword here", width: 8, break: "|"); echo ":"; -echo wordwrap("abcdefghij", 4, "|", true); echo ":"; -echo wordwrap("preserve\nnewlines here ok", 10, "|"); echo ":"; -echo call_user_func("wordwrap", "aaa bbb ccc", 3, "
"); echo ":"; -echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); -echo ":"; -return function_exists("wordwrap");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. -#[test] -fn execute_program_dispatches_str_contains_builtin() { - let program = parse_fragment( - br#"echo str_contains("Hello World", "World") ? "Y" : "N"; -echo str_contains("Hello", "z") ? "bad" : ":N"; -echo str_contains("Hello", "") ? ":E" : "bad"; -echo call_user_func("str_contains", "abc", "b") ? ":C" : "bad"; -echo call_user_func_array("str_contains", ["abc", "x"]) ? "bad" : ":A"; -return function_exists("str_contains");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N:E:C:A"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval string position builtins return byte offsets or PHP false. -#[test] -fn execute_program_dispatches_string_position_builtins() { - let program = parse_fragment( - br#"echo strpos("banana", "na"); -echo ":" . strrpos("banana", "na"); -echo ":"; echo strpos("abc", "z") === false ? "F" : "bad"; -echo ":" . strpos("abc", ""); -echo ":" . strrpos("abc", ""); -echo ":" . call_user_func("strpos", "abc", "b"); -echo ":" . call_user_func_array("strrpos", ["ababa", "ba"]); -echo ":"; echo function_exists("strpos"); -return function_exists("strrpos");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:4:F:0:3:1:3:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `strstr()` returns suffixes, prefixes, or false for misses. -#[test] -fn execute_program_dispatches_strstr_builtin() { - let program = parse_fragment( - br#"echo strstr("user@example.com", "@"); echo ":"; -echo strstr(haystack: "hello world", needle: "lo", before_needle: true); echo ":"; -echo strstr("hello", "x") === false ? "F" : "bad"; echo ":"; -echo strstr("hello", ""); echo ":"; -echo call_user_func("strstr", "abcabc", "bc"); echo ":"; -echo call_user_func_array("strstr", ["haystack" => "abcabc", "needle" => "bc", "before_needle" => true]); echo ":"; -return function_exists("strstr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "@example.com:hel:F:hello:bcabc:a:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval prefix/suffix string search builtins use byte-string semantics. -#[test] -fn execute_program_dispatches_string_boundary_builtins() { - let program = parse_fragment( - br#"echo str_starts_with("Hello World", "Hello") ? "S" : "bad"; -echo str_starts_with("Hello", "World") ? "bad" : ":s"; -echo str_starts_with("Hello", "") ? ":se" : "bad"; -echo str_ends_with("Hello World", "World") ? ":E" : "bad"; -echo str_ends_with("Hello", "World") ? "bad" : ":e"; -echo str_ends_with("Hello", "") ? ":ee" : "bad"; -echo call_user_func("str_starts_with", "abc", "a") ? ":CS" : "bad"; -echo call_user_func_array("str_ends_with", ["abc", "c"]) ? ":CE" : "bad"; -echo ":"; echo function_exists("str_starts_with"); -return function_exists("str_ends_with");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "S:s:se:E:e:ee:CS:CE:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval string comparison builtins return PHP-compatible scalar results. -#[test] -fn execute_program_dispatches_string_compare_builtins() { - let program = parse_fragment( - br#"echo strcmp("abc", "abc"); -echo ":"; echo strcmp("abc", "abd") < 0 ? "lt" : "bad"; -echo ":"; echo strcasecmp("Hello", "hello"); -echo ":"; echo call_user_func("strcmp", "b", "a") > 0 ? "gt" : "bad"; -echo ":"; echo call_user_func_array("strcasecmp", ["A", "a"]) === 0 ? "ci" : "bad"; -echo ":"; echo hash_equals("abc", "abc") ? "heq" : "bad"; -echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; -echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; -echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); -return function_exists("hash_equals");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "0:lt:0:gt:ci:heq:hlen:hneq:11"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval trim-like builtins strip default and explicit byte masks. -#[test] -fn execute_program_dispatches_trim_like_builtins() { - let program = parse_fragment( - br#"echo "[" . trim(" hello ") . "]"; -echo ":[" . ltrim(" left") . "]"; -echo ":[" . rtrim("right ") . "]"; -echo ":[" . chop("tail... ", " .") . "]"; -echo ":[" . trim("**boxed**", "*") . "]"; -echo ":[" . call_user_func("trim", " cuf ") . "]"; -echo ":[" . call_user_func_array("ltrim", ["0007", "0"]) . "]"; -echo ":"; echo function_exists("trim"); echo function_exists("ltrim"); echo function_exists("rtrim"); -return function_exists("chop");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "[hello]:[left]:[right]:[tail]:[boxed]:[cuf]:[7]:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. -#[test] -fn execute_program_dispatches_type_predicate_builtins() { - let program = parse_fragment( - br#"echo is_int(1); echo is_integer(1); echo is_long(1); -echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); -echo is_string("x"); echo is_bool(false); echo is_null(null); -echo is_array([1]); echo is_array(["a" => 1]); -echo is_iterable([1]); echo is_iterable(["a" => 1]); -echo is_iterable(1) ? "bad" : "T"; -echo is_array(1) ? "bad" : "ok"; -echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); -echo is_numeric("-5"); echo is_numeric("3.14"); -echo is_numeric("abc") ? "bad" : "N"; -echo is_numeric(true) ? "bad" : "B"; -echo is_resource(1) ? "bad" : "R"; -echo is_object($object) ? "O" : "bad"; -echo is_object([1]) ? "bad" : "o"; -echo is_nan(fdiv(0, 0)) ? "N" : "bad"; -echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; -echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; -echo is_finite(42) ? "F" : "bad"; -echo is_finite(fdiv(1, 0)) ? "bad" : "f"; -echo ":"; echo call_user_func("is_string", "x"); -echo call_user_func_array("is_array", [[1]]); -echo call_user_func("is_numeric", "12"); -echo call_user_func("is_iterable", [1]); -echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; -echo call_user_func("is_object", $object) ? "O" : "bad"; -echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; -echo function_exists("is_numeric"); echo function_exists("is_object"); echo function_exists("is_resource"); -echo function_exists("is_double"); echo function_exists("is_nan"); echo function_exists("is_finite"); -echo function_exists("is_iterable"); -return function_exists("is_infinite");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1111111111111Tok11111NBROoNIiFf:1111tOo1111111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. -#[test] -fn execute_program_dispatches_is_resource_true() { - let program = parse_fragment( - br#"echo is_resource($handle) ? "R" : "bad"; -echo ":" . gettype($handle); -return call_user_func("is_resource", $handle);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let handle = values.alloc(FakeValue::Resource(6)); - scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "R:resource"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval resource introspection builtins expose stream type and one-based id. -#[test] -fn execute_program_dispatches_resource_introspection_builtins() { - let program = parse_fragment( - br#"echo get_resource_type($handle); -echo ":" . get_resource_id($handle); -echo ":" . call_user_func("get_resource_type", $handle); -echo ":" . call_user_func_array("get_resource_id", ["resource" => $handle]); -echo ":" . function_exists("get_resource_type"); -return function_exists("get_resource_id");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let handle = values.alloc(FakeValue::Resource(6)); - scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "stream:7:stream:7:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval cast builtins return boxed scalar cells directly and by callable. -#[test] -fn execute_program_dispatches_cast_builtins() { - let program = parse_fragment( - br#"echo intval("42"); echo ":"; -echo floatval("3.5"); echo ":"; -echo strval(12); echo ":"; -echo boolval("0") ? "bad" : "false"; -echo ":"; echo call_user_func("strval", 7); -return call_user_func_array("intval", ["9"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "42:3.5:12:false:7"); - assert_eq!(values.get(result), FakeValue::Int(9)); -} - -/// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. -#[test] -fn execute_program_dispatches_settype_builtin() { - let program = parse_fragment( - br#"$x = 42; -echo settype($x, "string") ? gettype($x) . ":" . $x : "bad"; -echo ":"; -$y = "0"; -echo settype(type: "bool", var: $y) ? gettype($y) . ":" . ($y ? "true" : "false") : "bad"; -echo ":"; -echo settype($missing, "integer") ? gettype($missing) . ":" . $missing : "bad"; -echo ":"; -$z = 3.8; -echo call_user_func("settype", $z, "integer") ? gettype($z) . ":" . $z : "bad"; -echo ":"; -return function_exists("settype");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "string:42:boolean:false:integer:0:double:3.8:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); - assert_eq!( - values.warnings, - ["settype(): Argument #1 ($var) must be passed by reference, value given"] - ); -} - -/// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. -#[test] -fn execute_program_dispatches_gettype_builtin() { - let program = parse_fragment( - br#"echo gettype(1); echo ":"; -echo gettype(1.5); echo ":"; -echo gettype("x"); echo ":"; -echo gettype(false); echo ":"; -echo gettype(null); echo ":"; -echo gettype([1]); echo ":"; -echo gettype(["a" => 1]); echo ":"; -echo call_user_func("gettype", true); echo ":"; -echo call_user_func_array("gettype", [null]); -return function_exists("gettype");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "integer:double:string:boolean:NULL:array:array:boolean:NULL" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `get_class()` reads object class names directly and by callable. -#[test] -fn execute_program_dispatches_get_class_builtin() { - let program = parse_fragment( - br#"echo get_class($object); echo ":"; -echo call_user_func("get_class", $object); echo ":"; -return call_user_func_array("get_class", ["object" => $object]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "stdClass:stdClass:"); - assert_eq!( - values.get(result), - FakeValue::String("stdClass".to_string()) - ); -} - -/// Verifies eval `get_parent_class()` reads object and class-string parents by callable. -#[test] -fn execute_program_dispatches_get_parent_class_builtin() { - let program = parse_fragment( - br#"echo get_parent_class($object); echo ":"; -echo get_parent_class("ChildClass"); echo ":"; -echo call_user_func("get_parent_class", $object); echo ":"; -return call_user_func_array("get_parent_class", ["object_or_class" => "ChildClass"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "ParentClass:ParentClass:ParentClass:"); - assert_eq!( - values.get(result), - FakeValue::String("ParentClass".to_string()) - ); -} - -/// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. -#[test] -fn execute_program_dispatches_abs_builtin() { - let program = parse_fragment( - br#"echo abs(-5); echo ":"; -echo abs(-2.5); echo ":"; -echo gettype(abs(-2.5)); echo ":"; -echo call_user_func("abs", -7); echo ":"; -echo call_user_func_array("abs", [-9]); -return function_exists("abs");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:2.5:double:7:9"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `floor()` and `ceil()` dispatch as double-returning math builtins. -#[test] -fn execute_program_dispatches_floor_and_ceil_builtins() { - let program = parse_fragment( - br#"echo floor(3.7); echo ":"; -echo gettype(floor(3)); echo ":"; -echo ceil(3.2); echo ":"; -echo gettype(ceil(3)); echo ":"; -echo call_user_func("floor", 4.9); echo ":"; -echo call_user_func_array("ceil", [4.1]); -echo ":"; echo function_exists("floor"); -return function_exists("ceil");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:double:4:double:4:5:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `fdiv()` and `fmod()` dispatch as floating-point binary builtins. -#[test] -fn execute_program_dispatches_float_binary_builtins() { - let program = parse_fragment( - br#"echo round(fdiv(10, 4), 2); echo ":"; -echo gettype(fdiv(10, 4)); echo ":"; -echo round(fmod(10.5, 3.2), 1); echo ":"; -echo round(call_user_func("fdiv", 9, 2), 1); echo ":"; -echo round(call_user_func_array("fmod", [10.5, 3.2]), 1); echo ":"; -echo function_exists("fdiv"); -return function_exists("fmod");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2.5:double:0.9:4.5:0.9:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval extended scalar math builtins support direct, named, callable, and probe paths. -#[test] -fn execute_program_dispatches_extended_math_builtins() { - let program = parse_fragment( - br#"echo sin(0); echo ":"; -echo cos(0); echo ":"; -echo tan(0); echo ":"; -echo round(asin(1), 2); echo ":"; -echo acos(1); echo ":"; -echo round(atan(1), 2); echo ":"; -echo sinh(0); echo ":"; -echo cosh(0); echo ":"; -echo tanh(0); echo ":"; -echo log2(8); echo ":"; -echo log10(100); echo ":"; -echo exp(0); echo ":"; -echo round(deg2rad(180), 2); echo ":"; -echo round(rad2deg(pi()), 0); echo ":"; -echo log(num: 8, base: 2); echo ":"; -echo atan2(y: 0, x: 1); echo ":"; -echo hypot(3, 4); echo ":"; -echo intdiv(7, 2); echo ":"; -echo round(call_user_func("sin", pi() / 2), 0); echo ":"; -echo call_user_func_array("intdiv", ["num1" => 9, "num2" => 2]); echo ":"; -echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); -return function_exists("hypot");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. -#[test] -fn execute_program_dispatches_pow_builtin() { - let program = parse_fragment( - br#"echo pow(2, 3); echo ":"; -echo gettype(pow(2, 3)); echo ":"; -echo call_user_func("pow", 2, 5); echo ":"; -echo call_user_func_array("pow", [3, 3]); -return function_exists("pow");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "8:double:32:27"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `round()` supports default and explicit precision through callable paths. -#[test] -fn execute_program_dispatches_round_builtin() { - let program = parse_fragment( - br#"echo round(3.5); echo ":"; -echo round(3.14159, 2); echo ":"; -echo gettype(round(3)); echo ":"; -echo call_user_func("round", 2.5); echo ":"; -echo call_user_func_array("round", [1.55, 1]); -return function_exists("round");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:3.14:double:3:1.6"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `number_format()` groups and rounds numbers through callable paths. -#[test] -fn execute_program_dispatches_number_format_builtin() { - let program = parse_fragment( - br#"echo number_format(1234567); echo ":"; -echo number_format(1234.5678, 2); echo ":"; -echo number_format(num: 1234567.89, decimals: 2, decimal_separator: ",", thousands_separator: "."); echo ":"; -echo number_format(1234567.89, 2, ".", ""); echo ":"; -echo call_user_func("number_format", -1234.5, 1); echo ":"; -echo call_user_func_array("number_format", ["num" => 1234, "decimals" => 0, "decimal_separator" => ".", "thousands_separator" => " "]); echo ":"; -return function_exists("number_format");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval printf-family builtins format, print, and dispatch through callables. -#[test] -fn execute_program_dispatches_printf_family_builtins() { - let program = parse_fragment( - br#"echo sprintf("Hello %s", "World"); echo ":"; -echo sprintf("%05d", 42); echo ":"; -echo sprintf("%.2f", 3.14159); echo ":"; -echo sprintf("%-6s|", "hi"); echo ":"; -$printed = printf("%s=%d", "n", 42); -echo ":" . $printed . ":"; -echo vsprintf("%s/%d/%.1f", ["age", 42, 3]); echo ":"; -$vprinted = vprintf("%s-%d", ["v", 7]); -echo ":" . $vprinted . ":"; -echo call_user_func("sprintf", "%+d", 42); echo ":"; -echo call_user_func_array("vsprintf", ["format" => "%s", "values" => ["spread"]]); echo ":"; -echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); -return is_callable("vprintf");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `sscanf()` returns indexed string matches through callable paths. -#[test] -fn execute_program_dispatches_sscanf_builtin() { - let program = parse_fragment( - br#"$result = sscanf("John 1.5 30", "%s %f %d"); -echo $result[0] . ":" . $result[1] . ":" . $result[2] . ":"; -$named = sscanf(string: "Age: -25", format: "Age: %d"); -echo $named[0] . ":"; -$call = call_user_func("sscanf", "-2.5e3", "%f"); -echo $call[0] . ":"; -$spread = call_user_func_array("sscanf", ["string" => "ok %", "format" => "%s %%"]); -echo $spread[0] . ":"; -return function_exists("sscanf");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "John:1.5:30:-25:-2.5e3:ok:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `min()` and `max()` select numeric values directly and by callable. -#[test] -fn execute_program_dispatches_min_max_builtins() { - let program = parse_fragment( - br#"echo min(3, 1, 2); echo ":"; -echo max(1, 3, 2); echo ":"; -echo min(2.5, 1.5); echo ":"; -echo max(1.5, 2.5); echo ":"; -echo call_user_func("min", 9, 4, 7); echo ":"; -echo call_user_func_array("max", [4, 8, 6]); echo ":"; -echo function_exists("min"); -return function_exists("max");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:3:1.5:2.5:4:8:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `clamp()` selects numeric values through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_clamp_builtin() { - let program = parse_fragment( - br#"echo clamp(5, 0, 10); echo ":"; -echo clamp(15, 0, 10); echo ":"; -echo clamp(-5, 0, 10); echo ":"; -echo clamp(2.75, 1.5, 2.5); echo ":"; -echo clamp(value: 8, min: 0, max: 5); echo ":"; -echo call_user_func("clamp", -1, 0, 10); echo ":"; -echo call_user_func_array("clamp", ["value" => 9, "min" => 0, "max" => 7]); echo ":"; -echo function_exists("clamp"); -return is_callable("clamp");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:10:0:2.5:5:0:7:1"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. -#[test] -fn execute_program_rejects_clamp_invalid_bounds() { - let program = parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("invalid clamp bounds should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval `pi()` returns a double constant directly and through callable paths. -#[test] -fn execute_program_dispatches_pi_builtin() { - let program = parse_fragment( - br#"echo round(pi(), 2); echo ":"; -echo gettype(pi()); echo ":"; -echo round(call_user_func("pi"), 3); echo ":"; -echo round(call_user_func_array("pi", []), 4); echo ":"; -return function_exists("pi");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3.14:double:3.142:3.1416:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. -#[test] -fn execute_program_dispatches_sqrt_builtin() { - let program = parse_fragment( - br#"echo sqrt(16); echo ":"; -echo gettype(sqrt(9)); echo ":"; -echo call_user_func("sqrt", 25); echo ":"; -echo call_user_func_array("sqrt", [36]); -return function_exists("sqrt");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "4:double:5:6"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `strrev()` dispatches through direct and callable paths. -#[test] -fn execute_program_dispatches_strrev_builtin() { - let program = parse_fragment( - br#"echo strrev("Hello"); echo ":"; -echo strrev(123); echo ":"; -echo call_user_func("strrev", "ABC"); echo ":"; -echo call_user_func_array("strrev", ["def"]); echo ":"; -return function_exists("strrev");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "olleH:321:CBA:fed:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `chr()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_chr_builtin() { - let program = parse_fragment( - br#"echo chr(65); echo ":"; -echo bin2hex(chr(codepoint: 256)); echo ":"; -echo bin2hex(call_user_func("chr", 257)); echo ":"; -echo call_user_func_array("chr", ["codepoint" => 321]); echo ":"; -return function_exists("chr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A:00:01:A:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_str_repeat_builtin() { - let program = parse_fragment( - br#"echo str_repeat("ha", 3); echo ":"; -echo strlen(str_repeat(string: "x", times: 0)); echo ":"; -echo call_user_func("str_repeat", "ab", 2); echo ":"; -echo call_user_func_array("str_repeat", ["string" => "z", "times" => 3]); echo ":"; -return function_exists("str_repeat");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "hahaha:0:abab:zzz:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `substr()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_substr_builtin() { - let program = parse_fragment( - br#"echo substr("abcdef", 2); echo ":"; -echo substr(string: "abcdef", offset: 1, length: -1); echo ":"; -echo substr("abcdef", -2); echo ":"; -echo call_user_func("substr", "abcdef", 2, -2); echo ":"; -echo call_user_func_array("substr", ["string" => "abcdef", "offset" => -4, "length" => 2]); echo ":"; -return function_exists("substr");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "cdef:bcde:ef:cd:cd:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `substr_replace()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_substr_replace_builtin() { - let program = parse_fragment( - br#"echo substr_replace("hello world", "PHP", 6, 5); echo ":"; -echo substr_replace(string: "abcdef", replace: "X", offset: 1, length: -1); echo ":"; -echo substr_replace("abcdef", "X", -2); echo ":"; -echo call_user_func("substr_replace", "abcdef", "X", 99, 1); echo ":"; -echo call_user_func_array("substr_replace", ["string" => "abcdef", "replace" => "X", "offset" => -99, "length" => 2]); echo ":"; -return function_exists("substr_replace");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "hello PHP:aXf:abcdX:abcdefX:Xcdef:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_nl2br_builtin() { - let program = parse_fragment( - br#"echo bin2hex(nl2br("a\nb")); echo ":"; -echo bin2hex(nl2br(string: "a\nb", use_xhtml: false)); echo ":"; -echo bin2hex(call_user_func("nl2br", "a\r\nb")); echo ":"; -echo bin2hex(call_user_func_array("nl2br", ["string" => "a\n\rb", "use_xhtml" => false])); echo ":"; -return function_exists("nl2br");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_bin2hex_builtin() { - let program = parse_fragment( - br#"echo bin2hex("Az"); echo ":"; -echo bin2hex(string: "A\n"); echo ":"; -echo call_user_func("bin2hex", "!?"); echo ":"; -echo call_user_func_array("bin2hex", ["string" => "ok"]); echo ":"; -return function_exists("bin2hex");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "417a:410a:213f:6f6b:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `hex2bin()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_hex2bin_builtin() { - let program = parse_fragment( - br#"echo hex2bin("417a"); echo ":"; -echo bin2hex(hex2bin(string: "410a")); echo ":"; -echo call_user_func("hex2bin", "213f"); echo ":"; -echo call_user_func_array("hex2bin", ["string" => "6f6b"]); echo ":"; -echo hex2bin("4") ? "bad" : "false"; echo ":"; -return function_exists("hex2bin");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Az:410a:!?:ok:false:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - assert_eq!( - values.warnings, - vec![HEX2BIN_ODD_LENGTH_WARNING.to_string()] - ); -} - -/// Verifies eval slash escaping builtins use PHP byte-string semantics. -#[test] -fn execute_program_dispatches_slash_escape_builtins() { - let program = parse_fragment( - br#"$escaped = addslashes($source); -echo bin2hex($escaped); echo ":"; -echo bin2hex(stripslashes($escaped)); echo ":"; -echo call_user_func("addslashes", "x\"y"); echo ":"; -echo call_user_func_array("stripslashes", [addslashes("o\"k")]); echo ":"; -return function_exists("addslashes") && function_exists("stripslashes");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let source = values.string("a\0b\\c\"d'").expect("create source"); - scope.set("source", source, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_base64_encode_builtin() { - let program = parse_fragment( - br#"echo base64_encode("Hello"); echo ":"; -echo base64_encode(string: "Hi"); echo ":"; -echo call_user_func("base64_encode", "Test 123!"); echo ":"; -echo call_user_func_array("base64_encode", ["string" => ""]); echo ":"; -return function_exists("base64_encode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "SGVsbG8=:SGk=:VGVzdCAxMjMh::"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `base64_decode()` dispatches through direct, named, and callable paths. -#[test] -fn execute_program_dispatches_base64_decode_builtin() { - let program = parse_fragment( - br#"echo base64_decode("SGVsbG8="); echo ":"; -echo base64_decode(string: "SGk="); echo ":"; -echo call_user_func("base64_decode", "VGVzdCAxMjMh"); echo ":"; -echo call_user_func_array("base64_decode", ["string" => ""]); echo ":"; -return function_exists("base64_decode");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Hello:Hi:Test 123!::"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies `isset` distinguishes missing, null, and other falsey values. -#[test] -fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { - let program = parse_fragment( - br#"if (isset($missing)) { echo "1"; } else { echo "0"; } -if (isset($nullish)) { echo "1"; } else { echo "0"; } -if (isset($zero)) { echo "1"; } else { echo "0"; } -if (isset($empty)) { echo "1"; } else { echo "0"; } -if (isset($zero, $empty)) { echo "1"; } else { echo "0"; } -if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let nullish = values.null().expect("create fake null"); - let zero = values.int(0).expect("create fake int"); - let empty = values.string("").expect("create fake string"); - scope.set("nullish", nullish, ScopeCellOwnership::Owned); - scope.set("zero", zero, ScopeCellOwnership::Owned); - scope.set("empty", empty, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "001110"); - assert_eq!(values.get(result), FakeValue::Null); -} - -/// Verifies `empty` treats missing, null, and falsey values as empty. -#[test] -fn execute_program_empty_uses_php_truthiness_without_missing_warnings() { - let program = parse_fragment( - br#"if (empty($missing)) { echo "1"; } else { echo "0"; } -if (empty($nullish)) { echo "1"; } else { echo "0"; } -if (empty($zero)) { echo "1"; } else { echo "0"; } -if (empty($empty_string)) { echo "1"; } else { echo "0"; } -if (empty($zero_string)) { echo "1"; } else { echo "0"; } -if (empty($value)) { echo "1"; } else { echo "0"; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let nullish = values.null().expect("create fake null"); - let zero = values.int(0).expect("create fake int"); - let empty_string = values.string("").expect("create fake empty string"); - let zero_string = values.string("0").expect("create fake zero string"); - let value = values.string("x").expect("create fake non-empty string"); - scope.set("nullish", nullish, ScopeCellOwnership::Owned); - scope.set("zero", zero, ScopeCellOwnership::Owned); - scope.set("empty_string", empty_string, ScopeCellOwnership::Owned); - scope.set("zero_string", zero_string, ScopeCellOwnership::Owned); - scope.set("value", value, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "111110"); - assert_eq!(values.get(result), FakeValue::Null); -} - -/// Verifies `isset` and `empty` use PHP offset semantics for array reads. -#[test] -fn execute_program_isset_and_empty_support_array_offsets() { - let program = parse_fragment( - br#"$map = [ - "present" => "x", - "nullish" => null, - "zero" => 0, - "empty" => "", - "child" => ["leaf" => "ok", "null" => null], -]; -echo isset($map["present"]) ? "1" : "0"; -echo isset($map["nullish"]) ? "1" : "0"; -echo isset($map["missing"]) ? "1" : "0"; -echo isset($map["zero"]) ? "1" : "0"; -echo isset($map["child"]["leaf"]) ? "1" : "0"; -echo isset($map["child"]["null"]) ? "1" : "0"; -echo isset($map["missing"]["leaf"]) ? "1" : "0"; -echo ":"; -echo empty($map["present"]) ? "1" : "0"; -echo empty($map["nullish"]) ? "1" : "0"; -echo empty($map["missing"]) ? "1" : "0"; -echo empty($map["zero"]) ? "1" : "0"; -echo empty($map["empty"]) ? "1" : "0"; -echo empty($map["child"]["leaf"]) ? "1" : "0"; -echo empty($map["child"]["null"]) ? "1" : "0"; -echo empty($map["missing"]["leaf"]) ? "1" : "0";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1001100:01111011"); - assert_eq!(values.get(result), FakeValue::Null); -} - -/// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. -#[test] -fn execute_program_function_probes_use_eval_context() { - let program = parse_fragment( - br#"function dyn_probe() { return 1; } -echo function_exists("DYN_PROBE") . "x"; -echo is_callable("dyn_probe") . "x"; -echo function_exists("strlen") . "x"; -echo function_exists("native_probe") . "x"; -echo function_exists("eval") . "x"; -echo function_exists("missing_probe") . "x";"#, - ) - .expect("parse eval fragment"); - let native = NativeFunction::new(1usize as *mut c_void, fake_native_return_descriptor, 0); - let mut context = ElephcEvalContext::new(); - assert!(context - .define_native_function("native_probe", native) - .is_ok()); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.output, "1x1x1x1xxx"); -} - -/// Verifies eval `interface_exists()` probes generated interface metadata by callable. -#[test] -fn execute_program_interface_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"echo interface_exists("KnownInterface") ? "Y" : "N"; -echo interface_exists("knowninterface") ? "Y" : "N"; -echo interface_exists("KnownClass") ? "Y" : "N"; -echo call_user_func("interface_exists", "KnownInterface") ? "Y" : "N"; -echo call_user_func_array("interface_exists", ["interface" => "KnownInterface"]) ? "Y" : "N"; -echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YYNYYN"); -} - -/// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. -#[test] -fn execute_program_class_like_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"echo trait_exists("KnownTrait") ? "T" : "t"; -echo trait_exists("knowntrait") ? "T" : "t"; -echo trait_exists("KnownEnum") ? "T" : "t"; -echo enum_exists("KnownEnum") ? "E" : "e"; -echo enum_exists("\knownenum") ? "E" : "e"; -echo enum_exists("KnownTrait") ? "E" : "e"; -echo call_user_func("trait_exists", "KnownTrait") ? "T" : "t"; -echo call_user_func_array("enum_exists", ["enum" => "KnownEnum"]) ? "E" : "e"; -echo trait_exists(trait: "MissingTrait", autoload: false) ? "T" : "t"; -echo enum_exists(enum: "MissingEnum", autoload: false) ? "E" : "e";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "TTtEEeTEte"); -} - -/// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. -#[test] -fn execute_program_is_a_relation_uses_runtime_probe() { - let program = parse_fragment( - br#"$object = new KnownClass(); -echo is_a($object, "KnownClass") ? "Y" : "N"; -echo is_subclass_of($object, "KnownClass") ? "Y" : "N"; -echo is_subclass_of($object, "ParentClass") ? "Y" : "N"; -echo call_user_func("is_a", $object, "ParentClass") ? "Y" : "N"; -echo call_user_func_array("is_subclass_of", ["object_or_class" => $object, "class" => "ParentClass"]) ? "Y" : "N"; -echo is_a(object_or_class: $object, class: "MissingClass", allow_string: false) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YNYYYN"); -} - -/// Verifies eval `define()` and `defined()` share a dynamic constant-name table. -#[test] -fn execute_program_define_and_defined_use_dynamic_constant_table() { - let program = parse_fragment( - br#"echo define("DynEvalConst", "ok") ? "Y" : "N"; -echo DynEvalConst; -echo \DynEvalConst; -echo defined("DynEvalConst") ? "Y" : "N"; -echo defined("\\DynEvalConst") ? "Y" : "N"; -echo defined("dynevalconst") ? "Y" : "N"; -echo define("DynEvalConst", 2) ? "Y" : "N"; -echo call_user_func("defined", "DynEvalConst") ? "Y" : "N"; -echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YokokYYNNYY"); - assert_eq!( - values.warnings, - vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] - ); -} - -/// Verifies eval predefined runtime constants are fetchable and cannot be redefined. -#[test] -fn execute_program_reads_predefined_runtime_constants() { - let program = parse_fragment( - br#"echo PHP_EOL === "\n" ? "eol" : "bad"; echo ":"; -echo (PHP_OS === "Darwin" || PHP_OS === "Linux") ? "os" : "bad"; echo ":"; -echo DIRECTORY_SEPARATOR; echo ":"; -echo PHP_INT_MAX > 9000000000000000000 ? "int" : "bad"; echo ":"; -echo defined("PHP_OS") ? "defined" : "bad"; echo ":"; -echo defined("\\PHP_OS") ? "root" : "bad"; echo ":"; -echo defined("php_os") ? "bad" : "case"; echo ":"; -echo define("PHP_OS", "x") ? "bad" : "locked"; echo ":"; -return PHP_INT_MAX;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "eol:os:/:int:defined:root:case:locked:"); - assert_eq!(values.get(result), FakeValue::Int(i64::MAX)); - assert_eq!( - values.warnings, - vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] - ); -} - -/// Verifies missing eval dynamic constants fail through runtime status. -#[test] -fn execute_program_missing_constant_fetch_fails() { - let program = parse_fragment(br#"return MissingEvalConst;"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("missing constant should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval class probes use the runtime class-name table. -#[test] -fn execute_program_class_exists_uses_runtime_probe() { - let program = parse_fragment( - br#"class DynProbe {} -echo class_exists("DynProbe") ? "Y" : "N"; -echo class_exists("\dynprobe") ? "Y" : "N"; -echo class_exists("KnownClass") ? "Y" : "N"; -echo class_exists("\knownclass") ? "Y" : "N"; -echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "YYYYN"); -} - -/// Verifies duplicate eval-declared class names fail through runtime status. -#[test] -fn execute_program_duplicate_class_declaration_fails() { - let program = parse_fragment( - br#"class DynProbeDup {} -class dynprobedup {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values).expect_err("duplicate fails"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval fragments can dispatch registered native AOT functions. -#[test] -fn execute_program_calls_registered_native_function() { - let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} - -/// Verifies direct eval calls can bind registered native parameters by name. -#[test] -fn execute_program_calls_registered_native_function_with_named_args() { - let program = parse_fragment(br#"return native_answer(right: 2, left: 1);"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} - -/// Verifies direct eval calls can unpack arrays into registered native parameters. -#[test] -fn execute_program_calls_registered_native_function_with_spread_args() { - let program = - parse_fragment(br#"return native_answer(...[1, 2]);"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} - -/// Verifies indexed array writes mutate an existing scope array. -#[test] -fn execute_program_writes_indexed_scope_array() { - let program = parse_fragment(br#"$items = ["a"]; $items[1] = "b"; return $items[1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("b".to_string())); -} - -/// Verifies indexed array append writes use the next visible index. -#[test] -fn execute_program_appends_indexed_scope_array() { - let program = parse_fragment(br#"$items = ["a"]; $items[] = "b"; return $items[1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("b".to_string())); -} - -/// Verifies associative append starts at key zero when only string keys exist. -#[test] -fn execute_program_appends_assoc_scope_array_with_string_keys() { - let program = - parse_fragment(br#"$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); -} - -/// Verifies associative append uses one plus the largest existing integer key. -#[test] -fn execute_program_appends_assoc_scope_array_after_positive_int_key() { - let program = parse_fragment( - br#"$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies associative append preserves PHP's largest-negative-key behavior. -#[test] -fn execute_program_appends_assoc_scope_array_after_negative_int_key() { - let program = - parse_fragment(br#"$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("tail".to_string())); -} - -/// Verifies mutating a borrowed scope array does not make the eval scope own it. -#[test] -fn execute_program_preserves_borrowed_array_ownership() { - let program = parse_fragment(br#"$items[0] = "b";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let array = values.array_new(1).expect("create fake array"); - scope.set("items", array, ScopeCellOwnership::Borrowed); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let entry = scope.entry("items").expect("scope should contain items"); - - assert_eq!(entry.cell(), array); - assert_eq!(entry.flags().ownership, ScopeCellOwnership::Borrowed); - assert!(values.releases.is_empty()); -} - -/// Verifies replacing an eval-owned scope value releases the old cell. -#[test] -fn execute_program_releases_replaced_scope_value() { - let program = parse_fragment(br#"$x = "old"; $x = "new";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.releases.len(), 1); - assert_eq!( - values.get(values.releases[0]), - FakeValue::String("old".to_string()) - ); -} - -/// Verifies unsetting an eval-owned scope value releases the old cell. -#[test] -fn execute_program_releases_unset_scope_value() { - let program = parse_fragment(br#"$x = "old"; unset($x);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.releases.len(), 1); - assert_eq!( - values.get(values.releases[0]), - FakeValue::String("old".to_string()) - ); -} - -/// Verifies break exits a runtime eval loop before later statements run. -#[test] -fn execute_program_break_exits_loop() { - let program = parse_fragment(br#"while ($flag) { echo "a"; break; echo "b"; }"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.bool_value(true).expect("create fake bool"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "a"); -} - -/// Verifies continue restarts a runtime eval loop and observes later scope updates. -#[test] -fn execute_program_continue_restarts_loop() { - let program = parse_fragment( - br#"while ($flag) { $flag = false; continue; echo "unreachable"; } echo "done";"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let flag = values.bool_value(true).expect("create fake bool"); - scope.set("flag", flag, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "done"); -} diff --git a/crates/elephc-eval/src/interpreter/tests/array_literals.rs b/crates/elephc-eval/src/interpreter/tests/array_literals.rs new file mode 100644 index 0000000000..a548ff0661 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/array_literals.rs @@ -0,0 +1,166 @@ +//! Purpose: +//! Interpreter tests for EvalIR array literal key allocation and reads. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases focus on PHP array-key normalization during literal evaluation. + +use super::super::*; +use super::support::*; + +/// Verifies indexed array literals and reads execute through runtime hooks. +#[test] +fn execute_program_reads_indexed_array_literal() { + let program = parse_fragment(br#"return ["a", "b"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} +/// Verifies legacy `array(...)` literals execute through the existing array runtime hooks. +#[test] +fn execute_program_reads_legacy_array_literal() { + let program = + parse_fragment(br#"return array("a", "b" => "bee",)[0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("a".to_string())); +} +/// Verifies associative array literals and string-key reads execute through runtime hooks. +#[test] +fn execute_program_reads_assoc_array_literal() { + let program = + parse_fragment(br#"return ["name" => "Ada"]["name"];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); +} +/// Verifies unkeyed assoc literal entries start at zero after string keys. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_string_key_starts_at_zero() { + let program = + parse_fragment(br#"return ["name" => "Ada", "Grace"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} +/// Verifies unkeyed assoc literal entries use one plus the largest integer key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_positive_int_key() { + let program = + parse_fragment(br#"return [2 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies unkeyed assoc literal entries preserve PHP's negative-key rule. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_negative_int_key() { + let program = + parse_fragment(br#"return [-2 => "minus", "tail"][-1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies numeric string literal keys update the next automatic key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_numeric_string_key() { + let program = + parse_fragment(br#"return ["2" => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies leading-zero string literal keys do not update the automatic key. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_leading_zero_string_key() { + let program = + parse_fragment(br#"return ["02" => "two", "tail"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies null literal keys normalize to empty strings without advancing automatic keys. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_null_key() { + let program = + parse_fragment(br#"return [null => "empty", "tail"][0];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies null literal keys are readable through the empty-string key. +#[test] +fn execute_program_assoc_array_literal_reads_null_key_as_empty_string() { + let program = parse_fragment(br#"return [null => "empty"][""];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("empty".to_string())); +} +/// Verifies boolean literal keys update the next automatic key after integer normalization. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_bool_key() { + let program = + parse_fragment(br#"return [true => "yes", "tail"][2];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies false literal keys update the next automatic key from zero. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_false_key() { + let program = + parse_fragment(br#"return [false => "no", "tail"][1];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies float literal keys update the next automatic key after truncation. +#[test] +fn execute_program_assoc_array_literal_unkeyed_after_float_key() { + let program = + parse_fragment(br#"return [2.7 => "two", "tail"][3];"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_core.rs b/crates/elephc-eval/src/interpreter/tests/builtins_arrays_core.rs new file mode 100644 index 0000000000..4cdf123941 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_arrays_core.rs @@ -0,0 +1,415 @@ +//! Purpose: +//! Interpreter tests for array aggregation, mapping, mutation, and sorting builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover array builtins that transform or reorder array values. + +use super::super::*; +use super::support::*; + +/// Verifies eval `ord()` returns the first byte and supports callable dispatch. +#[test] +fn execute_program_dispatches_ord_builtin() { + let program = parse_fragment( + br#"echo ord("A"); +echo ":" . ord(""); +echo ":" . call_user_func("ord", "B"); +echo ":" . call_user_func_array("ord", ["C"]); +echo ":"; echo function_exists("ord"); +return ord("Z");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "65:0:66:67:1"); + assert_eq!(values.get(result), FakeValue::Int(90)); +} +/// Verifies eval array aggregate builtins iterate array values and support callable dispatch. +#[test] +fn execute_program_dispatches_array_aggregate_builtins() { + let program = parse_fragment( + br#"echo array_sum([1, 2, 3]); +echo ":" . array_product([2, 3, 4]); +echo ":" . array_sum([]); +echo ":" . array_product([]); +echo ":" . array_sum(["a" => 2, "b" => 5]); +echo ":" . call_user_func("array_sum", [3, 4]); +echo ":" . call_user_func_array("array_product", [[2, 5]]); +echo ":"; echo function_exists("array_sum"); +return function_exists("array_product");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "6:24:0:1:7:7:10:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_map()` applies callbacks and preserves source keys. +#[test] +fn execute_program_dispatches_array_map_builtin() { + let program = parse_fragment( + br#"function eval_map_double($value) { return $value * 2; } +$mapped = array_map("eval_map_double", [1, 2, 3]); +echo $mapped[0] . ":" . $mapped[2] . ":"; +$assoc = array_map("strtoupper", ["a" => "x", "b" => "y"]); +echo $assoc["a"] . ":" . $assoc["b"] . ":"; +$identity = array_map(null, ["k" => "v"]); +echo $identity["k"] . ":"; +function eval_map_pair($left, $right) { return $left . "-" . ($right ?? "N"); } +$pairs = array_map("eval_map_pair", ["a" => "L", "b" => "R"], ["x" => "1"]); +echo $pairs[0] . ":" . $pairs[1] . ":"; +$zipped = array_map(null, [1, 2], [3, 4]); +echo $zipped[0][0] . $zipped[0][1] . ":" . $zipped[1][0] . $zipped[1][1] . ":"; +$call = call_user_func("array_map", "intval", ["7"]); +echo $call[0] . ":"; +$multi_call = call_user_func("array_map", "eval_map_pair", ["Q"], ["9"]); +echo $multi_call[0] . ":"; +$spread = call_user_func_array("array_map", ["callback" => "strval", "array" => [8]]); +echo $spread[0] . ":"; +return function_exists("array_map");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:6:X:Y:v:L-1:R-N:13:24:7:Q-9:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_reduce()` folds values through a string callback. +#[test] +fn execute_program_dispatches_array_reduce_builtin() { + let program = parse_fragment( + br#"function eval_reduce_sum($carry, $item) { return $carry + $item; } +echo array_reduce([1, 2, 3], "eval_reduce_sum", 10) . ":"; +function eval_reduce_join($carry, $item) { return $carry . $item; } +echo array_reduce([4, 5], "eval_reduce_sum") . ":"; +echo array_reduce(["a", "b"], "eval_reduce_join", "") . ":"; +$named = array_reduce(array: [6, 7], callback: "eval_reduce_sum"); +echo $named . ":"; +$call = call_user_func("array_reduce", [4, 5], "eval_reduce_sum"); +echo $call . ":"; +$spread = call_user_func_array("array_reduce", ["array" => [2, 3], "callback" => "eval_reduce_sum", "initial" => 4]); +echo $spread . ":"; +return function_exists("array_reduce");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "16:9:ab:13:9:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_walk()` invokes string callbacks with value and key cells. +#[test] +fn execute_program_dispatches_array_walk_builtin() { + let program = parse_fragment( + br#"function eval_walk_show($value, $key) { echo $key . "=" . $value . ";"; } +echo array_walk(["a" => 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; +$call = call_user_func("array_walk", [4, 5], "eval_walk_show"); +$spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); +return function_exists("array_walk");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_pop()` and `array_shift()` write back only for direct variable calls. +#[test] +fn execute_program_dispatches_array_pop_shift_builtins() { + let program = parse_fragment( + br#"$a = [1, 2, 3]; +echo array_pop($a) . ":" . count($a) . ":" . $a[1] . ":"; +$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +echo array_shift(array: $b) . ":" . $b[0] . ":" . $b["y"] . ":" . $b[1] . ":"; +$c = [4, 5]; +echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; +$d = [6, 7]; +echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; +return function_exists("array_pop") && function_exists("array_shift");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:2:2:1:2:3:4:5:2:5:6:2:6:"); + assert_eq!( + values.warnings, + vec![ + "array_pop(): Argument #1 ($array) must be passed by reference, value given", + "array_shift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_push()` and `array_unshift()` write back direct variable calls. +#[test] +fn execute_program_dispatches_array_push_unshift_builtins() { + let program = parse_fragment( + br#"$a = [1]; +echo array_push($a, 2, 3) . ":" . $a[2] . ":"; +$b = ["x" => 1, 10 => 2]; +echo array_push($b, "A") . ":" . $b["x"] . ":" . $b[11] . ":"; +$c = [2, 3]; +echo array_unshift($c, 0, 1) . ":" . $c[0] . ":" . $c[3] . ":"; +$d = ["x" => 1, 10 => 2, "y" => 3]; +echo array_unshift($d, "A") . ":" . $d[0] . ":" . $d["x"] . ":" . $d[1] . ":" . $d["y"] . ":"; +$e = [5]; +echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; +$f = [7]; +echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; +return function_exists("array_push") && function_exists("array_unshift");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:"); + assert_eq!( + values.warnings, + vec![ + "array_push(): Argument #1 ($array) must be passed by reference, value given", + "array_unshift(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_splice()` returns removed values and writes back direct variable calls. +#[test] +fn execute_program_dispatches_array_splice_builtin() { + let program = parse_fragment( + br#"$a = [10, 20, 30, 40]; +$removed = array_splice($a, 1, 2); +echo count($removed) . ":" . $removed[0] . ":" . $removed[1] . ":" . count($a) . ":" . $a[1] . ":"; +$b = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$cut = array_splice(array: $b, offset: 1, length: 2); +echo $cut[0] . ":" . $cut["y"] . ":" . $b["x"] . ":" . $b[0] . ":"; +$c = [1, 2, 3, 4]; +$tail = call_user_func("array_splice", $c, -2, 1); +echo $tail[0] . ":" . count($c) . ":" . $c[2] . ":"; +$d = [5, 6, 7]; +$all = call_user_func_array("array_splice", ["array" => $d, "offset" => 1]); +echo count($all) . ":" . $all[0] . ":" . $all[1] . ":" . count($d) . ":"; +$e = [1, 2, 3, 4]; +$rep = array_splice($e, 1, 2, ["A", "B"]); +echo count($rep) . ":" . $rep[0] . ":" . $rep[1] . ":" . $e[0] . ":" . $e[1] . ":" . $e[2] . ":" . $e[3] . ":"; +$f = ["x" => 1, 10 => 2, "y" => 3, 11 => 4]; +$rep2 = array_splice(array: $f, offset: 1, length: 2, replacement: ["s" => "S", 9 => "N"]); +echo $rep2[0] . ":" . $rep2["y"] . ":" . $f["x"] . ":" . $f[0] . ":" . $f[1] . ":" . $f[2] . ":"; +$g = [1, 2, 3]; +$rep3 = array_splice($g, offset: 1, replacement: [9]); +echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g[1] . ":"; +$h = [1, 2, 3]; +$removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); +echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; +return function_exists("array_splice");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:" + ); + assert_eq!( + values.warnings, + vec![ + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + "array_splice(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `sort()` and `rsort()` reindex direct variable arrays only. +#[test] +fn execute_program_dispatches_sort_builtins() { + let program = parse_fragment( + br#"$a = [3, 1, 2]; +echo sort($a) . ":" . $a[0] . $a[1] . $a[2] . ":"; +$b = ["banana", "apple", "cherry"]; +echo rsort(array: $b) . ":" . $b[0] . ":" . $b[2] . ":"; +$c = ["x" => 3, "y" => 1, "z" => 2]; +sort($c); +echo $c[0] . $c[1] . $c[2] . ":"; +$d = [3, 1, 2]; +echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; +$e = [1, 2, 3]; +echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; +return function_exists("sort") && function_exists("rsort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:123:1:cherry:apple:123:1:312:1:1:3:"); + assert_eq!( + values.warnings, + vec![ + "sort(): Argument #1 ($array) must be passed by reference, value given", + "rsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval key-preserving array ordering builtins write back direct variable calls. +#[test] +fn execute_program_dispatches_key_preserving_sort_builtins() { + let program = parse_fragment( + br#"$a = ["x" => 3, "y" => 1, "z" => 2]; +echo asort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value; } +echo ":"; +$b = ["x" => 1, "y" => 3, "z" => 2]; +echo arsort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2, 3 => 4]; +echo ksort($c) . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = ["b" => 1, "a" => 2, 3 => 4]; +echo krsort($d) . ":"; +foreach ($d as $key => $value) { echo $key . $value; } +echo ":"; +$e = ["x" => 2, "y" => 1]; +echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; +$f = ["b" => 1, "a" => 2]; +echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; +return function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:" + ); + assert_eq!( + values.warnings, + vec![ + "asort(): Argument #1 ($array) must be passed by reference, value given", + "krsort(): Argument #1 ($array) must be passed by reference, value given", + ] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval natural sort builtins preserve keys and use natural string order. +#[test] +fn execute_program_dispatches_natural_sort_builtins() { + let program = parse_fragment( + br#"$a = ["img10", "img2", "img1"]; +echo natsort($a) . ":"; +foreach ($a as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$b = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +echo natcasesort(array: $b) . ":"; +foreach ($b as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$c = ["x" => "b", "y" => "a"]; +echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; +return function_exists("natsort") && function_exists("natcasesort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:" + ); + assert_eq!( + values.warnings, + vec!["natsort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `shuffle()` reindexes direct variable arrays only. +#[test] +fn execute_program_dispatches_shuffle_builtin() { + let program = parse_fragment( + br#"$a = ["x" => 1, "y" => 2]; +echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; +$b = ["x" => 1, "y" => 2]; +echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; +return function_exists("shuffle");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:reindexed:2:3:1:12:"); + assert_eq!( + values.warnings, + vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval user-comparator sort builtins call callbacks and write back direct arrays. +#[test] +fn execute_program_dispatches_user_sort_builtins() { + let program = parse_fragment( + br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } +function eval_key_cmp($left, $right) { return strcmp($left, $right); } +$a = [3, 1, 2]; +echo usort($a, "eval_sort_cmp") . ":"; +foreach ($a as $value) { echo $value; } +echo ":"; +$b = ["b" => 1, "a" => 3, "c" => 2]; +echo uasort(array: $b, callback: "eval_sort_cmp") . ":"; +foreach ($b as $key => $value) { echo $key . $value; } +echo ":"; +$c = ["b" => 1, "a" => 2]; +echo uksort($c, "eval_key_cmp") . ":"; +foreach ($c as $key => $value) { echo $key . $value; } +echo ":"; +$d = [2, 1]; +echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; +return function_exists("usort") && function_exists("uasort") && function_exists("uksort");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:"); + assert_eq!( + values.warnings, + vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_iterators.rs b/crates/elephc-eval/src/interpreter/tests/builtins_arrays_iterators.rs new file mode 100644 index 0000000000..fd68eaff15 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_arrays_iterators.rs @@ -0,0 +1,140 @@ +//! Purpose: +//! Interpreter tests for iterator-style array builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases exercise callback iteration and object iterator dispatch. + +use super::super::*; +use super::support::*; + +/// Verifies eval iterator array helpers support direct and dynamic builtin calls. +#[test] +fn execute_program_dispatches_iterator_array_builtins() { + let program = parse_fragment( + br#"$items = ["x" => 1, "y" => 2]; +$copy = iterator_to_array($items); +echo iterator_count($items) . ":" . $copy["x"] . $copy["y"] . ":"; +$values = iterator_to_array($items, false); +echo (isset($values["x"]) ? "bad" : "reindexed") . ":" . $values[0] . $values[1] . ":"; +echo call_user_func("iterator_count", $items) . ":"; +$spread = call_user_func_array("iterator_to_array", ["iterator" => $items, "preserve_keys" => false]); +echo $spread[0] . $spread[1] . ":"; +return function_exists("iterator_count") && function_exists("iterator_to_array");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:12:reindexed:12:2:12:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `iterator_apply()` drives Iterator objects and callback args. +#[test] +fn execute_program_dispatches_iterator_apply_object_builtin() { + let program = parse_fragment( + br#"function eval_apply($prefix) { echo $prefix; return true; } +echo iterator_apply($it, "eval_apply", ["prefix" => "x"]) . ":"; +echo call_user_func("iterator_apply", $it, "eval_apply", ["y"]) . ":"; +return function_exists("iterator_apply");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xxx3:yyy3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `iterator_apply()` accepts object-method callable arrays. +#[test] +fn execute_program_iterator_apply_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(5); +echo iterator_apply($it, [$box, "add_x"], [1]) . ":"; +return call_user_func("iterator_apply", $it, [$box, "add_x"], [1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} +/// Verifies eval `iterator_apply()` counts the position where the callback stops. +#[test] +fn execute_program_iterator_apply_stops_on_falsey_callback() { + let program = parse_fragment( + br#"function eval_stop() { echo "s"; return false; } +return iterator_apply($it, "eval_stop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let iterator = values.alloc(FakeValue::Iterator { + len: 3, + position: 0, + }); + scope.set("it", iterator, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "s"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} +/// Verifies eval `array_filter()` removes falsey values while preserving original keys. +#[test] +fn execute_program_dispatches_array_filter_builtin() { + let program = parse_fragment( + br#"$filtered = array_filter([0, 1, 2, "", false, null, "0", "ok"]); +echo count($filtered) . ":" . $filtered[1] . ":" . $filtered[2] . ":" . $filtered[7] . ":"; +$assoc = array_filter(["a" => 0, "b" => 2, "c" => ""]); +echo (array_key_exists("a", $assoc) ? "bad" : "drop") . ":" . $assoc["b"] . ":"; +$null = array_filter([0, 3], null, 1); +echo count($null) . ":" . $null[1] . ":"; +$call = call_user_func("array_filter", [0, 4]); +echo count($call) . ":" . $call[1] . ":"; +$spread = call_user_func_array("array_filter", ["array" => [0, 5], "callback" => null]); +echo count($spread) . ":" . $spread[1] . ":"; +function eval_keep_even($value) { return $value % 2 == 0; } +$evens = array_filter([1, 2, 3, 4], "eval_keep_even"); +echo count($evens) . ":" . $evens[1] . ":" . $evens[3] . ":"; +function eval_keep_key($key) { return $key === "b"; } +$keyed = array_filter(["a" => 10, "b" => 20], "eval_keep_key", ARRAY_FILTER_USE_KEY); +echo count($keyed) . ":" . $keyed["b"] . ":"; +function eval_keep_both($value, $key) { return $key === "c" || $value === 1; } +$both = array_filter(["a" => 1, "b" => 2, "c" => 3], "eval_keep_both", ARRAY_FILTER_USE_BOTH); +echo count($both) . ":" . $both["a"] . ":" . $both["c"] . ":"; +$ints = array_filter([1, "x", 2], "is_int"); +echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; +return function_exists("array_filter");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_sets.rs b/crates/elephc-eval/src/interpreter/tests/builtins_arrays_sets.rs new file mode 100644 index 0000000000..e1e9a2f768 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_arrays_sets.rs @@ -0,0 +1,469 @@ +//! Purpose: +//! Interpreter tests for array set, projection, shape, range, and lookup builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover array builtins that preserve or query collection structure. + +use super::super::*; +use super::support::*; + +/// Verifies eval `array_combine()` converts key values through PHP string-key rules. +#[test] +fn execute_program_dispatches_array_combine_builtin() { + let program = parse_fragment( + br#"$pairs = array_combine(["a", "b"], [10, 20]); +echo $pairs["a"] . ":" . $pairs["b"]; +$numeric = array_combine(["1", "01"], ["n", "z"]); +echo ":" . $numeric[1] . $numeric["01"]; +$scalar = array_combine([null, true, false, 2.8], ["n", "t", "f", "d"]); +echo ":" . $scalar[""] . $scalar[1] . $scalar["2.8"]; +$named = array_combine(keys: ["k"], values: ["v"]); +echo ":" . $named["k"]; +$call = call_user_func("array_combine", ["x"], [7]); +echo ":" . $call["x"]; +$spread = call_user_func_array("array_combine", [["y"], [8]]); +echo ":" . $spread["y"] . ":"; +return function_exists("array_combine");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:nz:ftd:v:7:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_column()` extracts present row columns and reindexes them. +#[test] +fn execute_program_dispatches_array_column_builtin() { + let program = parse_fragment( + br#"$rows = [["name" => "Ada", "score" => 10], ["score" => 20], ["name" => "Lin", "score" => 30], 42]; +$names = array_column($rows, "name"); +echo count($names) . ":" . $names[0] . ":" . $names[1]; +$scores = array_column($rows, "score"); +echo ":" . count($scores) . ":" . $scores[0] . $scores[2]; +$numeric = array_column([[0 => "zero", 1 => "one"], [1 => "uno"]], 1); +echo ":" . count($numeric) . ":" . $numeric[0] . ":" . $numeric[1]; +$named = array_column(array: $rows, column_key: "score"); +echo ":" . $named[1]; +$call = call_user_func("array_column", [["x" => 5], ["x" => 6]], "x"); +echo ":" . $call[1]; +$spread = call_user_func_array("array_column", [[["y" => 7], ["z" => 0], ["y" => 9]], "y"]); +echo ":" . count($spread) . ":" . $spread[1] . ":"; +return function_exists("array_column");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:Ada:Lin:3:1030:2:one:uno:20:6:2:9:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_pad()` and `array_chunk()` build reindexed array shapes. +#[test] +fn execute_program_dispatches_array_shape_builtins() { + let program = parse_fragment( + br#"$right = array_pad([1, 2], 5, 0); +echo count($right) . ":" . $right[0] . $right[1] . $right[2] . $right[4]; +$left = array_pad([1, 2], -4, 9); +echo ":" . $left[0] . $left[1] . $left[2] . $left[3]; +$copy = array_pad([7, 8], 1, 0); +echo ":" . count($copy) . ":" . $copy[0] . $copy[1]; +$chunks = array_chunk([1, 2, 3, 4, 5], 2); +echo ":" . count($chunks) . ":" . $chunks[0][1] . $chunks[2][0]; +$named = array_pad(array: ["a"], length: 2, value: "b"); +echo ":" . $named[1]; +$call = call_user_func("array_chunk", [6, 7, 8], 2); +echo ":" . $call[1][0]; +$spread = call_user_func_array("array_pad", [[1], 3, 2]); +echo ":" . $spread[2] . ":"; +return function_exists("array_pad") && function_exists("array_chunk");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:1200:9912:2:78:3:25:b:8:2:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_slice()` observes PHP offset and length bounds. +#[test] +fn execute_program_dispatches_array_slice_builtin() { + let program = parse_fragment( + br#"$mid = array_slice([10, 20, 30, 40, 50], 1, 3); +echo count($mid) . ":" . $mid[0] . $mid[1] . $mid[2]; +$tail = array_slice([10, 20, 30, 40], -2, 1); +echo ":" . $tail[0]; +$open = array_slice([10, 20, 30, 40, 50], 2); +echo ":" . count($open) . ":" . $open[0] . $open[2]; +$null_len = array_slice([5, 6, 7], 1, null); +echo ":" . $null_len[0] . $null_len[1]; +$negative_len = array_slice([10, 20, 30, 40, 50], 1, -1); +echo ":" . count($negative_len) . ":" . $negative_len[0] . $negative_len[2]; +$named = array_slice(array: [1, 2, 3], offset: 1, length: 1); +echo ":" . $named[0]; +$call = call_user_func("array_slice", [6, 7, 8], 1, 2); +echo ":" . $call[1]; +$spread = call_user_func_array("array_slice", [[9, 10, 11], 1]); +echo ":" . $spread[0] . ":"; +return function_exists("array_slice");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:203040:30:3:3050:67:3:2040:2:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_merge()` appends numeric keys and overwrites string keys. +#[test] +fn execute_program_dispatches_array_merge_builtin() { + let program = parse_fragment( + br#"$merged = array_merge([1, 2], [3, 4]); +echo count($merged) . ":" . $merged[0] . $merged[1] . $merged[2] . $merged[3]; +$left = [1, 2]; +$right = [3]; +$copy = array_merge($left, $right); +echo ":" . count($left) . ":" . $left[0] . ":" . $copy[2]; +$assoc = array_merge(["a" => 1, 2 => "x"], ["a" => 9, 5 => "y", "b" => 3]); +echo ":" . $assoc["a"] . ":" . $assoc[0] . ":" . $assoc[1] . ":" . $assoc["b"]; +$call = call_user_func("array_merge", [6], [7, 8]); +echo ":" . $call[2]; +$spread = call_user_func_array("array_merge", [[9], [10]]); +echo ":" . $spread[1] . ":"; +return function_exists("array_merge");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:1234:2:1:3:9:x:y:3:8:10:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_diff()` and `array_intersect()` compare values as strings. +#[test] +fn execute_program_dispatches_array_value_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff(["a" => 1, "b" => 2, "c" => "2", "d" => 3], [2]); +echo count($diff) . ":" . $diff["a"] . ":" . $diff["d"]; +echo ":" . (array_key_exists("b", $diff) ? "bad" : "no-b"); +echo ":" . (array_key_exists("c", $diff) ? "bad" : "no-c"); +$inter = array_intersect(["a" => 1, "b" => 2, "c" => "2", "d" => 3], ["2", 4]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter["c"]; +$call = call_user_func("array_diff", [1, 2, 3], [2]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect", [[1, 2, 3], [3]]); +echo ":" . count($spread) . ":" . $spread[2] . ":"; +return function_exists("array_diff") && function_exists("array_intersect");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:1:3:no-b:no-c:2:2:2:2:13:1:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_diff_key()` and `array_intersect_key()` preserve first-array keys. +#[test] +fn execute_program_dispatches_array_key_set_builtins() { + let program = parse_fragment( + br#"$diff = array_diff_key(["a" => 1, "b" => 2, 4 => 3], ["a" => 0, 5 => 0]); +echo count($diff) . ":" . $diff["b"] . ":" . $diff[4]; +echo ":" . (array_key_exists("a", $diff) ? "bad" : "no-a"); +$inter = array_intersect_key(["a" => 1, "b" => 2, 4 => 3], ["b" => 0, 4 => 0]); +echo ":" . count($inter) . ":" . $inter["b"] . ":" . $inter[4]; +$call = call_user_func("array_diff_key", [10, 20, 30], [1 => 0]); +echo ":" . count($call) . ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_intersect_key", [["x" => 7, "y" => 8], ["y" => 0]]); +echo ":" . count($spread) . ":" . $spread["y"] . ":"; +return function_exists("array_diff_key") && function_exists("array_intersect_key");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:2:3:no-a:2:2:3:2:1030:1:8:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `range()` builds inclusive ascending and descending integer arrays. +#[test] +fn execute_program_dispatches_range_builtin() { + let program = parse_fragment( + br#"$up = range(1, 4); +echo count($up) . ":" . $up[0] . $up[3]; +$down = range(4, 1); +echo ":" . count($down) . ":" . $down[0] . $down[3]; +$single = range(3, 3); +echo ":" . count($single) . ":" . $single[0]; +$named = range(start: 2, end: 4); +echo ":" . $named[0] . $named[2]; +$call = call_user_func("range", 5, 7); +echo ":" . $call[2]; +$spread = call_user_func_array("range", [8, 6]); +echo ":" . count($spread) . ":" . $spread[0] . $spread[2] . ":"; +return function_exists("range");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:14:4:41:1:3:24:7:3:86:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_rand()` returns a key that exists in the source array. +#[test] +fn execute_program_dispatches_array_rand_builtin() { + let program = parse_fragment( + br#"$nums = [10, 20, 30]; +$idx = array_rand($nums); +echo ($idx >= 0 && $idx < 3 && array_key_exists($idx, $nums)) ? "idx" : "bad"; +$assoc = ["a" => 1, "b" => 2]; +$key = array_rand($assoc); +echo ":" . (array_key_exists($key, $assoc) ? "assoc" : "bad"); +$named = array_rand(array: [5, 6]); +echo ":" . (($named >= 0 && $named < 2) ? "named" : "bad"); +$call = call_user_func("array_rand", [7, 8]); +echo ":" . (($call >= 0 && $call < 2) ? "call" : "bad"); +$spread = call_user_func_array("array_rand", [["x" => 1, "y" => 2]]); +echo ":" . (array_key_exists($spread, ["x" => 1, "y" => 2]) ? "spread" : "bad") . ":"; +return function_exists("array_rand");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "idx:assoc:named:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval random builtins return values inside PHP inclusive ranges. +#[test] +fn execute_program_dispatches_rand_builtins() { + let program = parse_fragment( + br#"$plain = rand(); +echo ($plain >= 0 && $plain <= 2147483647) ? "plain" : "bad"; +$bounded = rand(2, 4); +echo ":" . (($bounded >= 2 && $bounded <= 4) ? "range" : "bad"); +$same = mt_rand(max: 6, min: 6); +echo ":" . ($same === 6 ? "same" : "bad"); +$swapped = rand(10, 1); +echo ":" . (($swapped >= 1 && $swapped <= 10) ? "swap" : "bad"); +$call = call_user_func("mt_rand", 1, 1); +echo ":" . ($call === 1 ? "call" : "bad"); +$spread = call_user_func_array("rand", ["min" => 3, "max" => 3]); +echo ":" . ($spread === 3 ? "spread" : "bad") . ":"; +$secure = random_int(max: 4, min: 4); +echo ($secure === 4 ? "random" : "bad") . ":"; +$secureCall = call_user_func("random_int", 5, 5); +echo ($secureCall === 5 ? "random-call" : "bad") . ":"; +$secureSpread = call_user_func_array("random_int", ["min" => 6, "max" => 6]); +echo ($secureSpread === 6 ? "random-spread" : "bad") . ":"; +echo function_exists("rand"); +echo function_exists("mt_rand"); +return function_exists("random_int");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "plain:range:same:swap:call:spread:random:random-call:random-spread:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. +#[test] +fn execute_program_dispatches_array_fill_builtins() { + let program = parse_fragment( + br#"$filled = array_fill(2, 3, "x"); +echo count($filled) . ":" . $filled[2] . $filled[4]; +$negative = array_fill(-2, 3, 7); +echo ":" . $negative[-2] . $negative[-1] . $negative[0]; +$empty = array_fill(5, 0, "x"); +echo ":" . count($empty); +$map = array_fill_keys(["a", "1", "01"], 8); +echo ":" . $map["a"] . ":" . $map[1] . ":" . $map["01"]; +$named = array_fill(start_index: 1, count: 2, value: "n"); +echo ":" . $named[1] . $named[2]; +$call = call_user_func("array_fill", 0, 2, "c"); +echo ":" . $call[0] . $call[1]; +$spread = call_user_func_array("array_fill_keys", [["x", "y"], "z"]); +echo ":" . $spread["x"] . $spread["y"] . ":"; +return function_exists("array_fill") && function_exists("array_fill_keys");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:xx:777:0:8:8:8:nn:cc:zz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_flip()` swaps valid values into PHP-normalized keys. +#[test] +fn execute_program_dispatches_array_flip_builtin() { + let program = parse_fragment( + br#"$flipped = array_flip(["a" => "x", "b" => "y", "c" => "x", "d" => 1, "e" => "01", "skip" => null, "truth" => true]); +echo $flipped["x"] . ":" . $flipped["y"] . ":" . $flipped[1] . ":" . $flipped["01"] . ":" . count($flipped); +$named = array_flip(array: ["k" => "v"]); +echo ":" . $named["v"]; +$call = call_user_func("array_flip", ["left" => "right"]); +echo ":" . $call["right"]; +$spread = call_user_func_array("array_flip", [["n" => 9]]); +echo ":" . $spread[9] . ":"; +return function_exists("array_flip");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "c:b:d:e:4:k:left:n:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_unique()` preserves first keys using default string comparison. +#[test] +fn execute_program_dispatches_array_unique_builtin() { + let program = parse_fragment( + br#"$unique = array_unique(["a", "b", "a", "2", 2]); +echo count($unique) . ":" . $unique[0] . $unique[1] . $unique[3]; +$assoc = array_unique(["x" => "a", "y" => "b", "z" => "a"]); +echo ":" . count($assoc) . ":" . $assoc["x"] . $assoc["y"]; +$scalar = array_unique([1, "1", 1.0, true, false, null, ""]); +echo ":" . count($scalar) . ":" . $scalar[0] . ":"; +echo $scalar[4] ? "bad" : "F"; +$named = array_unique(array: ["k" => "v", "l" => "v"]); +echo ":" . $named["k"] . ":" . count($named); +$call = call_user_func("array_unique", ["q", "q", "r"]); +echo ":" . $call[0] . $call[2]; +$spread = call_user_func_array("array_unique", [["s", "s", "t"]]); +echo ":" . $spread[0] . $spread[2] . ":"; +return function_exists("array_unique");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:ab2:2:ab:2:1:F:v:1:qr:st:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval array projection builtins produce indexed key/value arrays. +#[test] +fn execute_program_dispatches_array_projection_builtins() { + let program = parse_fragment( + br#"$values = array_values(["a" => 10, "b" => 20]); +echo $values[0] . ":" . $values[1]; +$keys = array_keys(["a" => 10, "b" => 20]); +echo ":" . $keys[0] . ":" . $keys[1]; +echo ":" . count(array_values([])); +$call_keys = call_user_func("array_keys", ["z" => 7]); +echo ":" . $call_keys[0]; +$call_values = call_user_func_array("array_values", [["q" => 8]]); +echo ":" . $call_values[0]; +echo ":"; echo function_exists("array_keys"); +return function_exists("array_values");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "10:20:a:b:0:z:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_reverse()` handles PHP key preservation rules. +#[test] +fn execute_program_dispatches_array_reverse_builtin() { + let program = parse_fragment( + br#"$indexed = array_reverse([1, 2, 3]); +echo $indexed[0]; echo $indexed[1]; echo $indexed[2]; echo ":"; +$mixed = array_reverse([2 => "a", "k" => "b", 5 => "c"]); +echo $mixed[0]; echo $mixed["k"]; echo $mixed[1]; echo ":"; +$preserved = array_reverse([2 => "a", "k" => "b", 5 => "c"], true); +echo $preserved[5]; echo $preserved["k"]; echo $preserved[2]; echo ":"; +$named = array_reverse(array: ["x", "y"], preserve_keys: true); +echo $named[1]; echo $named[0]; echo ":"; +$call = call_user_func("array_reverse", [4, 5]); +echo $call[0]; echo $call[1]; echo ":"; +$spread = call_user_func_array("array_reverse", [[6, 7]]); +echo $spread[0]; echo $spread[1]; echo ":"; +return function_exists("array_reverse");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "321:cba:cba:yx:54:76:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `array_key_exists()` distinguishes present null values from missing keys. +#[test] +fn execute_program_dispatches_array_key_exists_builtin() { + let program = parse_fragment( + br#"$map = ["name" => null, "age" => 30]; +echo array_key_exists("name", $map) ? "Y" : "N"; echo ":"; +echo array_key_exists("missing", $map) ? "bad" : "N"; echo ":"; +echo array_key_exists(1, [10, null]) ? "Y" : "N"; echo ":"; +echo array_key_exists(2, [10, null]) ? "bad" : "N"; echo ":"; +echo call_user_func("array_key_exists", "age", $map) ? "Y" : "N"; echo ":"; +echo call_user_func_array("array_key_exists", ["age", $map]) ? "Y" : "N"; echo ":"; +return function_exists("array_key_exists");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:Y:N:Y:Y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval array search builtins use loose comparison and return keys or booleans. +#[test] +fn execute_program_dispatches_array_search_builtins() { + let program = parse_fragment( + br#"echo in_array(2, [1, 2, 3]) ? "Y" : "bad"; +echo ":"; echo in_array(4, [1, 2, 3]) ? "bad" : "N"; +echo ":" . array_search(20, [10, 20, 30]); +echo ":" . array_search("Grace", ["name" => "Grace"]); +echo ":"; echo array_search("x", ["name" => "Grace"]) === false ? "F" : "bad"; +echo ":"; echo call_user_func("in_array", "b", ["a", "b"]) ? "C" : "bad"; +$found = call_user_func_array("array_search", ["v", ["k" => "v"]]); +echo ":" . $found; +echo ":"; echo function_exists("in_array"); +return function_exists("array_search");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:1:name:F:C:k:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_metadata.rs new file mode 100644 index 0000000000..badde7709b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_metadata.rs @@ -0,0 +1,274 @@ +//! Purpose: +//! Interpreter tests for filesystem path, metadata, stat, and space builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases use temporary files while keeping filesystem metadata assertions focused. + +use super::super::*; +use super::support::*; + +/// Verifies eval path component builtins mirror static basename/dirname edge cases. +#[test] +fn execute_program_dispatches_path_component_builtins() { + let program = parse_fragment( + br#"echo basename("/var/log/syslog.log", ".log") . ":"; +echo basename(path: "/usr///") . ":"; +echo basename("/", "x") === "" ? "root" : "bad"; echo ":"; +echo dirname("/usr/local/bin/tool", 2) . ":"; +echo dirname(path: "/usr///local///bin") . ":"; +echo call_user_func("basename", "foo.tar.gz", ".bz2") . ":"; +echo call_user_func_array("dirname", ["path" => "/usr", "levels" => 3]) . ":"; +echo function_exists("basename"); +return function_exists("dirname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "syslog:usr:root:/usr/local:/usr///local:foo.tar.gz:/:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `realpath()` resolves existing paths and returns false for misses. +#[test] +fn execute_program_dispatches_realpath_builtin() { + let program = parse_fragment( + br#"echo realpath(".") !== false ? "resolved" : "bad"; echo ":"; +echo realpath(path: "elephc-eval-missing-path") === false ? "false" : "bad"; echo ":"; +echo call_user_func("realpath", ".") !== false ? "call" : "bad"; echo ":"; +echo call_user_func_array("realpath", ["path" => "elephc-eval-missing-path"]) === false ? "array-false" : "bad"; +echo ":"; +return function_exists("realpath");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "resolved:false:call:array-false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. +#[test] +fn execute_program_dispatches_fnmatch_builtin() { + let program = parse_fragment( + br#"echo fnmatch("*.log", "system.log") ? "match" : "bad"; echo ":"; +echo fnmatch("*.log", "logs/system.log", FNM_PATHNAME) ? "bad" : "path"; echo ":"; +echo fnmatch("*.LOG", "system.log", FNM_CASEFOLD) ? "case" : "bad"; echo ":"; +echo fnmatch("*", ".env", FNM_PERIOD) ? "bad" : "period"; echo ":"; +echo fnmatch("[!abc]oo", "doo") && !fnmatch("[!abc]oo", "boo") ? "class" : "bad"; echo ":"; +echo fnmatch('a\\*b', 'a*b') ? "escape" : "bad"; echo ":"; +echo fnmatch('a\\*b', 'a\\xxb', FNM_NOESCAPE) ? "noescape" : "bad"; echo ":"; +$flags = FNM_PATHNAME | FNM_CASEFOLD; +echo fnmatch("dir/*.TXT", "dir/file.txt", $flags) ? "flags" : "bad"; echo ":"; +echo call_user_func("fnmatch", "*.txt", "report.txt") ? "call" : "bad"; echo ":"; +echo call_user_func_array("fnmatch", ["pattern" => "*.TXT", "filename" => "report.txt", "flags" => FNM_CASEFOLD]) ? "callarray" : "bad"; echo ":"; +echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD"); +return FNM_CASEFOLD;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "match:path:case:period:class:escape:noescape:flags:call:callarray:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_FNM_CASEFOLD)); +} +/// Verifies eval `pathinfo()` handles arrays, component flags, constants, and callables. +#[test] +fn execute_program_dispatches_pathinfo_builtin() { + let program = parse_fragment( + br#"$info = pathinfo("/var/log/syslog.log"); +echo $info["dirname"] . "|" . $info["basename"] . "|" . $info["extension"] . "|" . $info["filename"] . ":"; +echo pathinfo("archive.tar.gz", PATHINFO_EXTENSION) . ":"; +echo pathinfo(".bashrc", PATHINFO_FILENAME) === "" ? "dotfile" : "bad"; echo ":"; +echo pathinfo("file.", PATHINFO_EXTENSION) === "" ? "trail" : "bad"; echo ":"; +echo pathinfo("", PATHINFO_DIRNAME) === "" ? "empty-dir" : "bad"; echo ":"; +$plain = pathinfo("/etc/hosts"); +echo array_key_exists("extension", $plain) ? "bad" : "no-ext"; echo ":"; +echo pathinfo("/a/b.php", PATHINFO_BASENAME | PATHINFO_FILENAME) . ":"; +$call = call_user_func("pathinfo", "foo.txt", PATHINFO_ALL); +echo $call["basename"] . ":"; +echo call_user_func_array("pathinfo", ["path" => "foo.txt", "flags" => 0]) === "" ? "zero" : "bad"; +echo ":"; echo function_exists("pathinfo"); echo defined("PATHINFO_ALL"); +return PATHINFO_ALL;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "/var/log|syslog.log|log|syslog:gz:dotfile:trail:empty-dir:no-ext:b.php:foo.txt:zero:11" + ); + assert_eq!(values.get(result), FakeValue::Int(EVAL_PATHINFO_ALL)); +} +/// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. +#[test] +fn execute_program_dispatches_filesystem_builtins() { + let filename = format!("elephc_eval_fs_probe_{}.txt", std::process::id()); + let missing = format!("elephc_eval_fs_missing_{}.txt", std::process::id()); + let source = format!( + r#"echo file_put_contents("{filename}", "hello") . ":"; +echo file_get_contents("{filename}") . ":"; +echo file_exists("{filename}") ? "exists" : "missing"; echo ":"; +echo is_file(filename: "{filename}") ? "file" : "bad"; echo ":"; +echo is_dir(".") ? "dir" : "bad"; echo ":"; +echo is_readable("{filename}") ? "readable" : "bad"; echo ":"; +echo is_writable("{filename}") ? "writable" : "bad"; echo ":"; +echo is_writeable("{filename}") ? "writeable" : "bad"; echo ":"; +echo filesize("{filename}") . ":"; +echo file_get_contents("{missing}") === false ? "missing-false" : "bad"; echo ":"; +echo call_user_func("file_exists", "{filename}") ? "call-exists" : "bad"; echo ":"; +echo call_user_func_array("filesize", ["filename" => "{filename}"]) . ":"; +echo unlink("{filename}") ? "unlinked" : "bad"; echo ":"; +echo file_exists("{filename}") ? "bad" : "gone"; echo ":"; +echo function_exists("file_get_contents"); echo function_exists("file_put_contents"); +echo function_exists("file_exists"); echo function_exists("is_file"); echo function_exists("is_dir"); +echo function_exists("is_readable"); echo function_exists("is_writable"); echo function_exists("is_writeable"); +echo function_exists("filesize"); +return function_exists("unlink");"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + assert_eq!( + values.output, + "5:hello:exists:file:dir:readable:writable:writeable:5:missing-false:call-exists:5:unlinked:gone:111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval disk-space builtins query local filesystem capacity and dispatch dynamically. +#[test] +fn execute_program_dispatches_disk_space_builtins() { + let program = parse_fragment( + br#"echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; +echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; +echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; +echo disk_free_space("no/such/path/elephc-eval") === 0.0 ? "missing" : "bad"; echo ":"; +echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; +echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; echo ":"; +echo function_exists("disk_free_space"); +return function_exists("disk_total_space");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "free:total:ordered:missing:call:spread:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval stat metadata builtins expose scalar file metadata and link probes. +#[test] +fn execute_program_dispatches_stat_metadata_builtins() { + let filename = format!("elephc_eval_stat_probe_{}.txt", std::process::id()); + let missing = format!("elephc_eval_stat_missing_{}.txt", std::process::id()); + let link = format!("elephc_eval_stat_link_{}.txt", std::process::id()); + let source = format!( + r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; +echo fileatime("{filename}") > 0 ? "atime" : "bad"; echo ":"; +echo filectime("{filename}") > 0 ? "ctime" : "bad"; echo ":"; +echo fileperms("{filename}") > 0 ? "perms" : "bad"; echo ":"; +echo fileowner("{filename}") >= 0 ? "owner" : "bad"; echo ":"; +echo filegroup("{filename}") >= 0 ? "group" : "bad"; echo ":"; +echo fileinode("{filename}") > 0 ? "inode" : "bad"; echo ":"; +echo filetype("{filename}") . ":"; +echo filetype(".") . ":"; +echo filetype("{link}") . ":"; +echo is_executable("{filename}") ? "bad" : "noexec"; echo ":"; +echo is_link("{link}") ? "link" : "bad"; echo ":"; +echo fileatime("{missing}") === false ? "missing-atime" : "bad"; echo ":"; +echo filectime("{missing}") === false ? "missing-ctime" : "bad"; echo ":"; +echo fileperms("{missing}") === false ? "missing-perms" : "bad"; echo ":"; +echo fileowner("{missing}") === false ? "missing-owner" : "bad"; echo ":"; +echo filegroup("{missing}") === false ? "missing-group" : "bad"; echo ":"; +echo fileinode("{missing}") === false ? "missing-inode" : "bad"; echo ":"; +echo filetype("{missing}") === false ? "missing-type" : "bad"; echo ":"; +echo filemtime("{missing}") === 0 ? "missing-mtime" : "bad"; echo ":"; +echo call_user_func("filetype", "{filename}") . ":"; +echo call_user_func_array("fileinode", ["filename" => "{filename}"]) > 0 ? "callinode" : "bad"; echo ":"; +echo function_exists("filemtime"); echo function_exists("fileatime"); +echo function_exists("filectime"); echo function_exists("fileperms"); +echo function_exists("fileowner"); echo function_exists("filegroup"); +echo function_exists("fileinode"); echo function_exists("filetype"); +echo function_exists("is_executable"); echo function_exists("is_link"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "mtime:atime:ctime:perms:owner:group:inode:file:dir:link:noexec:link:missing-atime:missing-ctime:missing-perms:missing-owner:missing-group:missing-inode:missing-type:missing-mtime:file:callinode:1111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `stat()` and `lstat()` build PHP-compatible metadata arrays. +#[test] +fn execute_program_dispatches_stat_array_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_stat_array_{pid}.txt"); + let link = format!("elephc_eval_lstat_array_{pid}.txt"); + let missing = format!("elephc_eval_stat_array_missing_{pid}.txt"); + let source = format!( + r#"$stat = stat("{filename}"); +$lstat = lstat("{link}"); +echo $stat["size"] === 5 && $stat[7] === $stat["size"] ? "stat" : "bad"; echo ":"; +echo ($stat["mode"] & 61440) === 32768 ? "mode" : "bad"; echo ":"; +echo ($lstat["mode"] & 61440) === 40960 ? "lstat" : "bad"; echo ":"; +echo stat("{missing}") === false && lstat("{missing}") === false ? "missing" : "bad"; echo ":"; +$call = call_user_func("stat", "{filename}"); +echo $call["mtime"] === filemtime("{filename}") ? "callstat" : "bad"; echo ":"; +$call_lstat = call_user_func_array("lstat", ["filename" => "{link}"]); +echo $call_lstat["ino"] > 0 ? "calllstat" : "bad"; echo ":"; +echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stat"); echo function_exists("lstat"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + std::fs::write(&filename, b"hello").expect("write stat array fixture"); + std::os::unix::fs::symlink(&filename, &link).expect("create stat array symlink"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&link); + assert_eq!( + values.output, + "stat:mode:lstat:missing:callstat:calllstat:cleanup:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs b/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs new file mode 100644 index 0000000000..618203b88d --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs @@ -0,0 +1,277 @@ +//! Purpose: +//! Interpreter tests for filesystem listing and mutating builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover directory traversal, globbing, file changes, and touch behavior. + +use super::super::*; +use super::support::*; + +/// Verifies eval local path operation builtins mutate filesystem state. +#[test] +fn execute_program_dispatches_path_operation_builtins() { + let pid = std::process::id(); + let dir = format!("elephc_eval_ops_dir_{pid}"); + let call_dir = format!("elephc_eval_ops_call_dir_{pid}"); + let src = format!("elephc_eval_ops_src_{pid}.txt"); + let copy = format!("elephc_eval_ops_copy_{pid}.txt"); + let moved = format!("elephc_eval_ops_moved_{pid}.txt"); + let symlink = format!("elephc_eval_ops_symlink_{pid}.txt"); + let hardlink = format!("elephc_eval_ops_hardlink_{pid}.txt"); + let source = format!( + r#"file_put_contents("{src}", "hello"); +echo mkdir("{dir}") ? "mkdir" : "bad"; echo ":"; +echo is_dir("{dir}") ? "dir" : "bad"; echo ":"; +echo copy("{src}", "{copy}") && file_get_contents("{copy}") === "hello" ? "copy" : "bad"; echo ":"; +echo rename("{copy}", "{moved}") && file_exists("{moved}") && !file_exists("{copy}") ? "rename" : "bad"; echo ":"; +echo symlink("{src}", "{symlink}") ? "symlink" : "bad"; echo ":"; +echo readlink("{symlink}") === "{src}" ? "readlink" : "bad"; echo ":"; +echo linkinfo("{symlink}") >= 0 ? "linkinfo" : "bad"; echo ":"; +echo readlink("{src}") === false ? "readlink-false" : "bad"; echo ":"; +echo linkinfo("{missing}") === -1 ? "linkinfo-missing" : "bad"; echo ":"; +echo link("{src}", "{hardlink}") && file_get_contents("{hardlink}") === "hello" ? "hardlink" : "bad"; echo ":"; +echo clearstatcache() === null ? "cache" : "bad"; echo ":"; +echo unlink("{symlink}") && unlink("{hardlink}") && unlink("{moved}") && unlink("{src}") && rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo call_user_func("mkdir", "{call_dir}") ? "callmkdir" : "bad"; echo ":"; +echo call_user_func_array("rmdir", ["directory" => "{call_dir}"]) ? "callrmdir" : "bad"; echo ":"; +echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exists("copy"); +echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); +echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); +return true;"#, + missing = format!("elephc_eval_ops_missing_{pid}.txt"), + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&symlink, &hardlink, &moved, ©, &src] { + let _ = std::fs::remove_file(path); + } + for path in [&call_dir, &dir] { + let _ = std::fs::remove_dir(path); + } + assert_eq!( + values.output, + "mkdir:dir:copy:rename:symlink:readlink:linkinfo:readlink-false:linkinfo-missing:hardlink:cache:cleanup:callmkdir:callrmdir:111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. +#[test] +fn execute_program_dispatches_file_listing_builtins() { + let pid = std::process::id(); + let lines = format!("elephc_eval_listing_lines_{pid}.txt"); + let empty = format!("elephc_eval_listing_empty_{pid}.txt"); + let missing = format!("elephc_eval_listing_missing_{pid}.txt"); + let dir = format!("elephc_eval_listing_dir_{pid}"); + let source = format!( + r#"file_put_contents("{lines}", "one\ntwo"); +file_put_contents("{empty}", ""); +$lines = file("{lines}"); +echo count($lines) . ":"; +echo $lines[0] === "one\n" ? "line0" : "bad"; echo ":"; +echo $lines[1] === "two" ? "line1" : "bad"; echo ":"; +echo "["; +$bytes = readfile(filename: "{empty}"); +echo "]" . $bytes . ":"; +echo readfile("{missing}") === false ? "missing-readfile" : "bad"; echo ":"; +echo count(file("{missing}")) === 0 ? "missing-file" : "bad"; echo ":"; +mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.txt", "b"); +$scan = scandir(directory: "{dir}"); +echo count($scan) . ":"; +echo in_array(".", $scan) && in_array("..", $scan) && in_array("a.txt", $scan) && in_array("b.txt", $scan) ? "scan" : "bad"; echo ":"; +$call_lines = call_user_func("file", "{lines}"); +echo $call_lines[0] === "one\n" ? "callfile" : "bad"; echo ":"; +$call_scan = call_user_func_array("scandir", ["directory" => "{dir}"]); +echo count($call_scan) . ":"; +echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") && unlink("{lines}") && unlink("{empty}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("file"); echo function_exists("readfile"); echo function_exists("scandir"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&lines, &empty, &missing] { + let _ = std::fs::remove_file(path); + } + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "2:line0:line1:[]0:missing-readfile:missing-file:4:scan:callfile:4:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `glob()` expands local patterns and dispatches dynamically. +#[test] +fn execute_program_dispatches_glob_builtin() { + let pid = std::process::id(); + let dir = format!("elephc_eval_glob_dir_{pid}"); + let source = format!( + r#"mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.log", "b"); +file_put_contents("{dir}/c.txt", "c"); +file_put_contents("{dir}/.hidden.txt", "h"); +$matches = glob("{dir}/*.txt"); +echo count($matches) === 2 && basename($matches[0]) === "a.txt" && basename($matches[1]) === "c.txt" ? "glob" : "bad"; echo ":"; +echo count(glob("{dir}/*.none")) === 0 ? "empty" : "bad"; echo ":"; +$literal = glob("{dir}/a.txt"); +echo count($literal) === 1 && $literal[0] === "{dir}/a.txt" ? "literal" : "bad"; echo ":"; +$all = glob("{dir}/*"); +echo in_array("{dir}/.hidden.txt", $all) ? "bad" : "hidden"; echo ":"; +$call = call_user_func("glob", "{dir}/*.log"); +echo count($call) === 1 && basename($call[0]) === "b.log" ? "callglob" : "bad"; echo ":"; +$call_array = call_user_func_array("glob", ["pattern" => "{dir}/*.txt"]); +echo count($call_array) === 2 ? "callarray" : "bad"; echo ":"; +unlink("{dir}/.hidden.txt"); +unlink("{dir}/c.txt"); +unlink("{dir}/b.log"); +unlink("{dir}/a.txt"); +echo rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("glob"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(format!("{dir}/.hidden.txt")); + let _ = std::fs::remove_file(format!("{dir}/c.txt")); + let _ = std::fs::remove_file(format!("{dir}/b.log")); + let _ = std::fs::remove_file(format!("{dir}/a.txt")); + let _ = std::fs::remove_dir(&dir); + assert_eq!( + values.output, + "glob:empty:literal:hidden:callglob:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval file-modification builtins update modes, masks, temp files, and dispatch. +#[test] +fn execute_program_dispatches_file_modify_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_modify_{pid}.txt"); + let missing = format!("elephc_eval_modify_missing_{pid}.txt"); + let prefix = format!("evm{pid}_"); + let call_prefix = format!("evc{pid}_"); + let source = format!( + r#"file_put_contents("{filename}", "x"); +echo chmod(filename: "{filename}", permissions: 384) ? "chmod" : "bad"; echo ":"; +echo (fileperms("{filename}") & 511) === 384 ? "mode" : "bad"; echo ":"; +echo chmod("{missing}", 384) ? "bad" : "chmod-false"; echo ":"; +$tmp = tempnam(directory: ".", prefix: "{prefix}"); +echo file_exists($tmp) && str_starts_with(basename($tmp), "{prefix}") ? "tempnam" : "bad"; echo ":"; +unlink($tmp); +$previous = umask(mask: 18); +$set = umask($previous); +echo $set === 18 ? "umask" : "bad"; echo ":"; +$before = umask(18); +$probe = umask(); +$restore = umask($before); +echo $probe === 18 && $restore === 18 ? "probe" : "bad"; echo ":"; +echo call_user_func("chmod", "{filename}", 420) ? "callchmod" : "bad"; echo ":"; +$call_tmp = call_user_func_array("tempnam", ["directory" => ".", "prefix" => "{call_prefix}"]); +echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "{call_prefix}") ? "calltempnam" : "bad"; echo ":"; +unlink($call_tmp); +echo unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + for entry in std::fs::read_dir(".").expect("read eval test cwd") { + let entry = entry.expect("read eval temp entry"); + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with(&prefix) || name.starts_with(&call_prefix) { + let _ = std::fs::remove_file(entry.path()); + } + } + assert_eq!( + values.output, + "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. +#[test] +fn execute_program_dispatches_touch_builtin() { + let pid = std::process::id(); + let created = format!("elephc_eval_touch_created_{pid}.txt"); + let stamped = format!("elephc_eval_touch_stamped_{pid}.txt"); + let missing = format!("elephc_eval_touch_missing_{pid}/x.txt"); + let source = format!( + r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; +file_put_contents("{stamped}", "x"); +echo touch("{stamped}", 1000000000) ? "mtime" : "bad"; echo ":"; +echo filemtime("{stamped}") === 1000000000 ? "readmtime" : "bad"; echo ":"; +echo touch("{stamped}", 1000000001, null) && filemtime("{stamped}") === 1000000001 ? "nullatime" : "bad"; echo ":"; +echo touch("{stamped}", 1000000002, 1000000003) && filemtime("{stamped}") === 1000000002 ? "both" : "bad"; echo ":"; +echo touch("{missing}") ? "bad" : "touch-false"; echo ":"; +echo call_user_func("touch", "{created}", 1000000004) ? "calltouch" : "bad"; echo ":"; +echo call_user_func_array("touch", ["filename" => "{stamped}", "mtime" => 1000000005]) ? "callarray" : "bad"; echo ":"; +echo unlink("{created}") && unlink("{stamped}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("touch"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&created); + let _ = std::fs::remove_file(&stamped); + assert_eq!( + values.output, + "create:mtime:readmtime:nullatime:both:touch-false:calltouch:callarray:cleanup:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_json.rs b/crates/elephc-eval/src/interpreter/tests/builtins_json.rs new file mode 100644 index 0000000000..85e5de8254 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_json.rs @@ -0,0 +1,285 @@ +//! Purpose: +//! Interpreter tests for core and JSON builtin dispatch. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases assert JSON state, flags, throwable behavior, and callable dispatch. + +use super::super::*; +use super::support::*; + +/// Verifies dynamic builtin calls inside eval dispatch through runtime value hooks. +#[test] +fn execute_program_dispatches_simple_builtins() { + let program = parse_fragment( + br#"echo strlen("abc") . ":" . count([1, [2, 3], [4]]) . ":"; +echo count([1, [2, 3], [4]], COUNT_RECURSIVE) . ":"; +echo call_user_func("count", [1, [2]]) . ":"; +echo call_user_func_array("count", ["value" => [1, [2]], "mode" => COUNT_RECURSIVE]) . ":"; +return defined("COUNT_RECURSIVE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:6:2:3:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. +#[test] +fn execute_program_dispatches_json_encode_builtin() { + let program = parse_fragment( + br#"echo json_encode(["a" => 1, "b" => "x/y"]) . ":"; +echo json_encode([1, "q", true, null]) . ":"; +echo call_user_func("json_encode", "a/b\"c") . ":"; +echo call_user_func_array("json_encode", ["value" => ["k" => false], "flags" => 0, "depth" => 4]) . ":"; +echo json_encode("a/b", JSON_UNESCAPED_SLASHES) . ":"; +echo call_user_func_array("json_encode", ["value" => "x/y", "flags" => JSON_UNESCAPED_SLASHES]) . ":"; +$accent = json_decode("\"\\u00e9\""); +$emoji = json_decode("\"\\ud83d\\ude00\""); +echo bin2hex(json_encode($accent . "/" . $emoji)) . ":"; +echo bin2hex(json_encode($accent . "/" . $emoji, JSON_UNESCAPED_UNICODE)) . ":"; +echo bin2hex(json_encode([$accent => $emoji])) . ":"; +echo bin2hex(json_encode([$accent => $emoji], JSON_UNESCAPED_UNICODE)) . ":"; +echo json_encode([1, 2], JSON_FORCE_OBJECT) . ":"; +echo json_encode([], JSON_FORCE_OBJECT) . ":"; +echo call_user_func_array("json_encode", ["value" => [1, 2], "flags" => JSON_FORCE_OBJECT]) . ":"; +echo json_encode("<>&\"" . chr(39), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ":"; +echo json_encode(["01", "+12", "1e3", " 7", "7x"], JSON_NUMERIC_CHECK) . ":"; +echo json_encode([1.0, 2.5, -3.0], JSON_PRESERVE_ZERO_FRACTION) . ":"; +echo (json_encode(INF) === false ? "false" : "json") . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo json_encode([1.5, INF, NAN], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":" . json_last_error_msg() . ":"; +$bad = "a" . hex2bin("80") . "b"; +echo (json_encode($bad) === false ? "utf8-false" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_PARTIAL_OUTPUT_ON_ERROR)) . ":"; +echo json_last_error() . ":"; +echo json_encode($bad, JSON_INVALID_UTF8_IGNORE) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_encode($bad, JSON_INVALID_UTF8_SUBSTITUTE | JSON_UNESCAPED_UNICODE)) . ":"; +echo json_last_error() . ":"; +echo json_encode([hex2bin("6b80") => hex2bin("7680")], JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +echo json_last_error() . ":"; +json_encode(3.5); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +echo str_replace("\n", "|", json_encode(["a" => [1, 2]], JSON_PRETTY_PRINT)) . ":"; +return function_exists("json_encode") && defined("INF") && defined("NAN") && defined("JSON_UNESCAPED_SLASHES") && defined("JSON_UNESCAPED_UNICODE") && defined("JSON_FORCE_OBJECT") && defined("JSON_HEX_TAG") && defined("JSON_HEX_AMP") && defined("JSON_HEX_APOS") && defined("JSON_HEX_QUOT") && defined("JSON_NUMERIC_CHECK") && defined("JSON_PARTIAL_OUTPUT_ON_ERROR") && defined("JSON_PRETTY_PRINT") && defined("JSON_PRESERVE_ZERO_FRACTION") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":"x\/y"}:[1,"q",true,null]:"a\/b\"c":{"k":false}:"a/b":"x/y":225c75303065395c2f5c75643833645c756465303022:22c3a95c2ff09f988022:7b225c7530306539223a225c75643833645c7564653030227d:7b22c3a9223a22f09f9880227d:{"0":1,"1":2}:{}:{"0":1,"1":2}:"\u003C\u003E\u0026\u0022\u0027":[1,12,1000,7,"7x"]:[1.0,2.5,-3.0]:false:7:Inf and NaN cannot be JSON encoded:[1.5,0,0]:7:Inf and NaN cannot be JSON encoded:utf8-false:5:6e756c6c:5:"ab":0:22615c75666666646222:0:2261efbfbd6222:0:{"k\ufffd":null}:5:0:No error:{| "a": [| 1,| 2| ]|}:"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_decode()` materializes scalars, arrays, and associative arrays. +#[test] +fn execute_program_dispatches_json_decode_builtin() { + let program = parse_fragment( + br#"echo json_decode("\"hello\"") . ":"; +echo json_decode("42") . ":"; +echo (json_decode("true") ? "T" : "bad") . ":"; +echo (is_null(json_decode("null")) ? "NULL" : "bad") . ":"; +$decoded = json_decode("{\"a\":1,\"b\":[\"x\",false]}", true); +echo $decoded["a"] . ":" . $decoded["b"][0] . ":" . ($decoded["b"][1] ? "bad" : "F") . ":"; +$call = call_user_func("json_decode", "[3,4]"); +echo $call[1] . ":"; +$named = call_user_func_array("json_decode", ["json" => "{\"k\":\"v\"}", "associative" => true, "depth" => 4, "flags" => 0]); +echo $named["k"] . ":"; +$badJson = "\"a" . hex2bin("80") . "b\""; +echo (is_null(json_decode($badJson)) ? "utf8-null" : "bad") . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_IGNORE)) . ":"; +echo json_last_error() . ":"; +echo bin2hex(json_decode($badJson, true, 512, JSON_INVALID_UTF8_SUBSTITUTE)) . ":"; +echo json_last_error() . ":"; +$objSub = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_SUBSTITUTE); +$objSubKeys = array_keys($objSub); +echo bin2hex($objSubKeys[0]) . "=" . bin2hex($objSub[$objSubKeys[0]]) . ":"; +$objIgnore = json_decode("{\"k" . hex2bin("80") . "\":\"v" . hex2bin("80") . "\"}", true, 512, JSON_INVALID_UTF8_IGNORE); +$objIgnoreKeys = array_keys($objIgnore); +echo bin2hex($objIgnoreKeys[0]) . "=" . bin2hex($objIgnore[$objIgnoreKeys[0]]) . ":"; +echo (is_null(json_decode("bad")) ? "BAD" : "wrong") . ":"; +$big = json_decode("[9223372036854775808]", true, 512, JSON_BIGINT_AS_STRING); +echo json_decode("9223372036854775808", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo json_decode("-9223372036854775809", true, 512, JSON_BIGINT_AS_STRING) . ":"; +echo gettype($big[0]) . ":" . $big[0] . ":"; +echo call_user_func_array("json_decode", ["json" => "9223372036854775808", "associative" => true, "depth" => 512, "flags" => JSON_BIGINT_AS_STRING]) . ":"; +return function_exists("json_decode") && defined("JSON_BIGINT_AS_STRING") && defined("JSON_INVALID_UTF8_IGNORE") && defined("JSON_INVALID_UTF8_SUBSTITUTE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "hello:42:T:NULL:1:x:F:4:v:utf8-null:5:6162:0:61efbfbd62:0:6befbfbd=76efbfbd:6b=76:BAD:9223372036854775808:-9223372036854775809:string:9223372036854775808:9223372036854775808:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_decode()` returns `stdClass` objects unless assoc is true. +#[test] +fn execute_program_dispatches_json_decode_stdclass_default() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +echo $object->a . ":" . $object->b->c . ":"; +$objectFalse = json_decode("{\"z\":2}", false); +echo $objectFalse->z . ":"; +$objectNull = json_decode("{\"n\":{\"m\":3}}", null); +echo $objectNull->n->m . ":"; +$assoc = json_decode("{\"b\":{\"c\":\"y\"}}", true); +echo $assoc["b"]["c"] . ":"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:x:2:3:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_encode()` serializes stdClass dynamic properties. +#[test] +fn execute_program_dispatches_json_encode_stdclass_object() { + let program = parse_fragment( + br#"$object = json_decode("{\"a\":1,\"b\":{\"c\":\"x\"}}"); +echo json_encode($object) . ":"; +echo str_replace("\n", "|", json_encode($object, JSON_PRETTY_PRINT)) . ":"; +$empty = json_decode("{}"); +echo json_encode($empty) . ":"; +$empty->a = 7; +echo json_encode($empty); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + r#"{"a":1,"b":{"c":"x"}}:{| "a": 1,| "b": {| "c": "x"| }|}:{}:{"a":7}"# + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_last_error*()` track JSON parse failures and success resets. +#[test] +fn execute_program_dispatches_json_last_error_builtins() { + let program = parse_fragment( + br#"echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("bad"); +echo json_last_error() . ":" . (json_last_error() === JSON_ERROR_SYNTAX ? "syntax" : "bad") . ":" . json_last_error_msg() . ":"; +json_validate("[1]", 1); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("\"ok\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("\"a" . chr(10) . "b\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("\"\\uD83D\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_decode("\"a" . chr(128) . "b\""); +echo json_last_error() . ":" . json_last_error_msg() . ":"; +json_validate("[0]"); +echo call_user_func("json_last_error") . ":" . call_user_func_array("json_last_error_msg", []) . ":"; +return function_exists("json_last_error") && function_exists("json_last_error_msg") && defined("JSON_ERROR_SYNTAX");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "0:No error:4:syntax:Syntax error near location 1:1:1:Maximum stack depth exceeded near location 1:1:0:No error:3:Control character error, possibly incorrectly encoded near location 1:3:10:Single unpaired UTF-16 surrogate in unicode escape near location 1:8:5:Malformed UTF-8 characters, possibly incorrectly encoded near location 1:3:0:No error:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval JSON throw flags raise catchable Throwable objects. +#[test] +fn execute_program_dispatches_json_throw_on_error() { + let program = parse_fragment( + br#"try { + json_decode("bad", true, 512, JSON_THROW_ON_ERROR); + echo "bad"; +} catch (Throwable) { + echo "decode:"; +} +try { + json_encode(INF, JSON_THROW_ON_ERROR); + echo "bad"; +} catch (Throwable) { + echo "encode:"; +} +echo json_encode(INF, JSON_THROW_ON_ERROR | JSON_PARTIAL_OUTPUT_ON_ERROR) . ":"; +return defined("JSON_THROW_ON_ERROR");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "decode:encode:0:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `json_validate()` validates documents, depth, and dynamic calls. +#[test] +fn execute_program_dispatches_json_validate_builtin() { + let program = parse_fragment( + br#"echo (json_validate("{\"a\":[1,true,null,\"caf\\u00e9\"]}") ? "Y" : "N") . ":"; +echo (json_validate("bad") ? "bad" : "N") . ":"; +echo (json_validate("[1]", 1) ? "bad" : "D") . ":"; +echo (call_user_func("json_validate", "\"x\"") ? "C" : "bad") . ":"; +echo (call_user_func_array("json_validate", ["json" => "[[1]]", "depth" => 3, "flags" => 0]) ? "A" : "bad") . ":"; +echo (json_validate("\"a" . chr(128) . "b\"", 512, JSON_INVALID_UTF8_IGNORE) ? "I" : "bad") . ":"; +echo json_last_error() . ":"; +echo (json_validate("bad", 512, JSON_INVALID_UTF8_IGNORE) ? "bad" : "S") . ":"; +echo json_last_error() . ":"; +return function_exists("json_validate") && defined("JSON_INVALID_UTF8_IGNORE");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:D:C:A:I:0:S:4:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies direct eval builtin calls bind named and unpacked arguments. +#[test] +fn execute_program_dispatches_named_and_spread_builtins() { + let program = parse_fragment( + br#"echo strlen(string: "abcd"); +echo ":" . (array_key_exists(array: ["name" => 1], key: "name") ? "Y" : "N"); +echo ":" . (str_contains(...["haystack" => "abc", "needle" => "b"]) ? "Y" : "N"); +return round(precision: 1, num: 3.14);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:Y:Y"); + assert_eq!(values.get(result), FakeValue::Float(3.1)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_math_formatting.rs b/crates/elephc-eval/src/interpreter/tests/builtins_math_formatting.rs new file mode 100644 index 0000000000..76a7813fe0 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_math_formatting.rs @@ -0,0 +1,323 @@ +//! Purpose: +//! Interpreter tests for math and formatting builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover numeric hooks, printf-family formatting, min/max, clamp, and constants. + +use super::super::*; +use super::support::*; + +/// Verifies eval `abs()` dispatches through runtime numeric hooks directly and by callable. +#[test] +fn execute_program_dispatches_abs_builtin() { + let program = parse_fragment( + br#"echo abs(-5); echo ":"; +echo abs(-2.5); echo ":"; +echo gettype(abs(-2.5)); echo ":"; +echo call_user_func("abs", -7); echo ":"; +echo call_user_func_array("abs", [-9]); +return function_exists("abs");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:2.5:double:7:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `floor()` and `ceil()` dispatch as double-returning math builtins. +#[test] +fn execute_program_dispatches_floor_and_ceil_builtins() { + let program = parse_fragment( + br#"echo floor(3.7); echo ":"; +echo gettype(floor(3)); echo ":"; +echo ceil(3.2); echo ":"; +echo gettype(ceil(3)); echo ":"; +echo call_user_func("floor", 4.9); echo ":"; +echo call_user_func_array("ceil", [4.1]); +echo ":"; echo function_exists("floor"); +return function_exists("ceil");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:double:4:double:4:5:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `fdiv()` and `fmod()` dispatch as floating-point binary builtins. +#[test] +fn execute_program_dispatches_float_binary_builtins() { + let program = parse_fragment( + br#"echo round(fdiv(10, 4), 2); echo ":"; +echo gettype(fdiv(10, 4)); echo ":"; +echo round(fmod(10.5, 3.2), 1); echo ":"; +echo round(call_user_func("fdiv", 9, 2), 1); echo ":"; +echo round(call_user_func_array("fmod", [10.5, 3.2]), 1); echo ":"; +echo function_exists("fdiv"); +return function_exists("fmod");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "2.5:double:0.9:4.5:0.9:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval extended scalar math builtins support direct, named, callable, and probe paths. +#[test] +fn execute_program_dispatches_extended_math_builtins() { + let program = parse_fragment( + br#"echo sin(0); echo ":"; +echo cos(0); echo ":"; +echo tan(0); echo ":"; +echo round(asin(1), 2); echo ":"; +echo acos(1); echo ":"; +echo round(atan(1), 2); echo ":"; +echo sinh(0); echo ":"; +echo cosh(0); echo ":"; +echo tanh(0); echo ":"; +echo log2(8); echo ":"; +echo log10(100); echo ":"; +echo exp(0); echo ":"; +echo round(deg2rad(180), 2); echo ":"; +echo round(rad2deg(pi()), 0); echo ":"; +echo log(num: 8, base: 2); echo ":"; +echo atan2(y: 0, x: 1); echo ":"; +echo hypot(3, 4); echo ":"; +echo intdiv(7, 2); echo ":"; +echo round(call_user_func("sin", pi() / 2), 0); echo ":"; +echo call_user_func_array("intdiv", ["num1" => 9, "num2" => 2]); echo ":"; +echo function_exists("sin"); echo function_exists("log"); echo function_exists("intdiv"); +return function_exists("hypot");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "0:1:0:1.57:0:0.79:0:1:0:3:2:1:3.14:180:3:0:5:3:1:4:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `pow()` dispatches through the existing exponentiation runtime hook. +#[test] +fn execute_program_dispatches_pow_builtin() { + let program = parse_fragment( + br#"echo pow(2, 3); echo ":"; +echo gettype(pow(2, 3)); echo ":"; +echo call_user_func("pow", 2, 5); echo ":"; +echo call_user_func_array("pow", [3, 3]); +return function_exists("pow");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:double:32:27"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `round()` supports default and explicit precision through callable paths. +#[test] +fn execute_program_dispatches_round_builtin() { + let program = parse_fragment( + br#"echo round(3.5); echo ":"; +echo round(3.14159, 2); echo ":"; +echo gettype(round(3)); echo ":"; +echo call_user_func("round", 2.5); echo ":"; +echo call_user_func_array("round", [1.55, 1]); +return function_exists("round");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:3.14:double:3:1.6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `number_format()` groups and rounds numbers through callable paths. +#[test] +fn execute_program_dispatches_number_format_builtin() { + let program = parse_fragment( + br#"echo number_format(1234567); echo ":"; +echo number_format(1234.5678, 2); echo ":"; +echo number_format(num: 1234567.89, decimals: 2, decimal_separator: ",", thousands_separator: "."); echo ":"; +echo number_format(1234567.89, 2, ".", ""); echo ":"; +echo call_user_func("number_format", -1234.5, 1); echo ":"; +echo call_user_func_array("number_format", ["num" => 1234, "decimals" => 0, "decimal_separator" => ".", "thousands_separator" => " "]); echo ":"; +return function_exists("number_format");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1,234,567:1,234.57:1.234.567,89:1234567.89:-1,234.5:1 234:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval printf-family builtins format, print, and dispatch through callables. +#[test] +fn execute_program_dispatches_printf_family_builtins() { + let program = parse_fragment( + br#"echo sprintf("Hello %s", "World"); echo ":"; +echo sprintf("%05d", 42); echo ":"; +echo sprintf("%.2f", 3.14159); echo ":"; +echo sprintf("%-6s|", "hi"); echo ":"; +$printed = printf("%s=%d", "n", 42); +echo ":" . $printed . ":"; +echo vsprintf("%s/%d/%.1f", ["age", 42, 3]); echo ":"; +$vprinted = vprintf("%s-%d", ["v", 7]); +echo ":" . $vprinted . ":"; +echo call_user_func("sprintf", "%+d", 42); echo ":"; +echo call_user_func_array("vsprintf", ["format" => "%s", "values" => ["spread"]]); echo ":"; +echo function_exists("sprintf"); echo is_callable("printf"); echo function_exists("vsprintf"); +return is_callable("vprintf");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:00042:3.14:hi |:n=42:4:age/42/3.0:v-7:3:+42:spread:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `sscanf()` returns indexed string matches through callable paths. +#[test] +fn execute_program_dispatches_sscanf_builtin() { + let program = parse_fragment( + br#"$result = sscanf("John 1.5 30", "%s %f %d"); +echo $result[0] . ":" . $result[1] . ":" . $result[2] . ":"; +$named = sscanf(string: "Age: -25", format: "Age: %d"); +echo $named[0] . ":"; +$call = call_user_func("sscanf", "-2.5e3", "%f"); +echo $call[0] . ":"; +$spread = call_user_func_array("sscanf", ["string" => "ok %", "format" => "%s %%"]); +echo $spread[0] . ":"; +return function_exists("sscanf");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "John:1.5:30:-25:-2.5e3:ok:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `min()` and `max()` select numeric values directly and by callable. +#[test] +fn execute_program_dispatches_min_max_builtins() { + let program = parse_fragment( + br#"echo min(3, 1, 2); echo ":"; +echo max(1, 3, 2); echo ":"; +echo min(2.5, 1.5); echo ":"; +echo max(1.5, 2.5); echo ":"; +echo call_user_func("min", 9, 4, 7); echo ":"; +echo call_user_func_array("max", [4, 8, 6]); echo ":"; +echo function_exists("min"); +return function_exists("max");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:1.5:2.5:4:8:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `clamp()` selects numeric values through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_clamp_builtin() { + let program = parse_fragment( + br#"echo clamp(5, 0, 10); echo ":"; +echo clamp(15, 0, 10); echo ":"; +echo clamp(-5, 0, 10); echo ":"; +echo clamp(2.75, 1.5, 2.5); echo ":"; +echo clamp(value: 8, min: 0, max: 5); echo ":"; +echo call_user_func("clamp", -1, 0, 10); echo ":"; +echo call_user_func_array("clamp", ["value" => 9, "min" => 0, "max" => 7]); echo ":"; +echo function_exists("clamp"); +return is_callable("clamp");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:10:0:2.5:5:0:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `clamp()` rejects a lower bound greater than the upper bound. +#[test] +fn execute_program_rejects_clamp_invalid_bounds() { + let program = parse_fragment(br#"return clamp(5, 10, 0);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("invalid clamp bounds should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval `pi()` returns a double constant directly and through callable paths. +#[test] +fn execute_program_dispatches_pi_builtin() { + let program = parse_fragment( + br#"echo round(pi(), 2); echo ":"; +echo gettype(pi()); echo ":"; +echo round(call_user_func("pi"), 3); echo ":"; +echo round(call_user_func_array("pi", []), 4); echo ":"; +return function_exists("pi");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3.14:double:3.142:3.1416:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `sqrt()` dispatches through runtime float hooks directly and by callable. +#[test] +fn execute_program_dispatches_sqrt_builtin() { + let program = parse_fragment( + br#"echo sqrt(16); echo ":"; +echo gettype(sqrt(9)); echo ":"; +echo call_user_func("sqrt", 25); echo ":"; +echo call_user_func_array("sqrt", [36]); +return function_exists("sqrt");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "4:double:5:6"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs b/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs new file mode 100644 index 0000000000..4bf47ec712 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs @@ -0,0 +1,227 @@ +//! Purpose: +//! Interpreter tests for scalar type, resource, cast, and class-introspection builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases check runtime tag inspection and mutating scalar conversions. + +use super::super::*; +use super::support::*; + +/// Verifies eval type-predicate builtins inspect boxed runtime tags directly and by callable. +#[test] +fn execute_program_dispatches_type_predicate_builtins() { + let program = parse_fragment( + br#"echo is_int(1); echo is_integer(1); echo is_long(1); +echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); +echo is_string("x"); echo is_bool(false); echo is_null(null); +echo is_array([1]); echo is_array(["a" => 1]); +echo is_iterable([1]); echo is_iterable(["a" => 1]); +echo is_iterable(1) ? "bad" : "T"; +echo is_array(1) ? "bad" : "ok"; +echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); +echo is_numeric("-5"); echo is_numeric("3.14"); +echo is_numeric("abc") ? "bad" : "N"; +echo is_numeric(true) ? "bad" : "B"; +echo is_resource(1) ? "bad" : "R"; +echo is_object($object) ? "O" : "bad"; +echo is_object([1]) ? "bad" : "o"; +echo is_nan(fdiv(0, 0)) ? "N" : "bad"; +echo is_infinite(fdiv(1, 0)) ? "I" : "bad"; +echo is_infinite(fdiv(-1, 0)) ? "i" : "bad"; +echo is_finite(42) ? "F" : "bad"; +echo is_finite(fdiv(1, 0)) ? "bad" : "f"; +echo ":"; echo call_user_func("is_string", "x"); +echo call_user_func_array("is_array", [[1]]); +echo call_user_func("is_numeric", "12"); +echo call_user_func("is_iterable", [1]); +echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; +echo call_user_func("is_object", $object) ? "O" : "bad"; +echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; +echo function_exists("is_numeric"); echo function_exists("is_object"); echo function_exists("is_resource"); +echo function_exists("is_double"); echo function_exists("is_nan"); echo function_exists("is_finite"); +echo function_exists("is_iterable"); +return function_exists("is_infinite");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1111111111111Tok11111NBROoNIiFf:1111tOo1111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. +#[test] +fn execute_program_dispatches_is_resource_true() { + let program = parse_fragment( + br#"echo is_resource($handle) ? "R" : "bad"; +echo ":" . gettype($handle); +return call_user_func("is_resource", $handle);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "R:resource"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval resource introspection builtins expose stream type and one-based id. +#[test] +fn execute_program_dispatches_resource_introspection_builtins() { + let program = parse_fragment( + br#"echo get_resource_type($handle); +echo ":" . get_resource_id($handle); +echo ":" . call_user_func("get_resource_type", $handle); +echo ":" . call_user_func_array("get_resource_id", ["resource" => $handle]); +echo ":" . function_exists("get_resource_type"); +return function_exists("get_resource_id");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle".to_string(), handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stream:7:stream:7:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval cast builtins return boxed scalar cells directly and by callable. +#[test] +fn execute_program_dispatches_cast_builtins() { + let program = parse_fragment( + br#"echo intval("42"); echo ":"; +echo floatval("3.5"); echo ":"; +echo strval(12); echo ":"; +echo boolval("0") ? "bad" : "false"; +echo ":"; echo call_user_func("strval", 7); +return call_user_func_array("intval", ["9"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "42:3.5:12:false:7"); + assert_eq!(values.get(result), FakeValue::Int(9)); +} +/// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. +#[test] +fn execute_program_dispatches_settype_builtin() { + let program = parse_fragment( + br#"$x = 42; +echo settype($x, "string") ? gettype($x) . ":" . $x : "bad"; +echo ":"; +$y = "0"; +echo settype(type: "bool", var: $y) ? gettype($y) . ":" . ($y ? "true" : "false") : "bad"; +echo ":"; +echo settype($missing, "integer") ? gettype($missing) . ":" . $missing : "bad"; +echo ":"; +$z = 3.8; +echo call_user_func("settype", $z, "integer") ? gettype($z) . ":" . $z : "bad"; +echo ":"; +return function_exists("settype");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "string:42:boolean:false:integer:0:double:3.8:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + ["settype(): Argument #1 ($var) must be passed by reference, value given"] + ); +} +/// Verifies eval `gettype()` maps runtime tags to PHP type names directly and by callable. +#[test] +fn execute_program_dispatches_gettype_builtin() { + let program = parse_fragment( + br#"echo gettype(1); echo ":"; +echo gettype(1.5); echo ":"; +echo gettype("x"); echo ":"; +echo gettype(false); echo ":"; +echo gettype(null); echo ":"; +echo gettype([1]); echo ":"; +echo gettype(["a" => 1]); echo ":"; +echo call_user_func("gettype", true); echo ":"; +echo call_user_func_array("gettype", [null]); +return function_exists("gettype");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "integer:double:string:boolean:NULL:array:array:boolean:NULL" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `get_class()` reads object class names directly and by callable. +#[test] +fn execute_program_dispatches_get_class_builtin() { + let program = parse_fragment( + br#"echo get_class($object); echo ":"; +echo call_user_func("get_class", $object); echo ":"; +return call_user_func_array("get_class", ["object" => $object]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stdClass:stdClass:"); + assert_eq!( + values.get(result), + FakeValue::String("stdClass".to_string()) + ); +} +/// Verifies eval `get_parent_class()` reads object and class-string parents by callable. +#[test] +fn execute_program_dispatches_get_parent_class_builtin() { + let program = parse_fragment( + br#"echo get_parent_class($object); echo ":"; +echo get_parent_class("ChildClass"); echo ":"; +echo call_user_func("get_parent_class", $object); echo ":"; +return call_user_func_array("get_parent_class", ["object_or_class" => "ChildClass"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ParentClass:ParentClass:ParentClass:"); + assert_eq!( + values.get(result), + FakeValue::String("ParentClass".to_string()) + ); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs b/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs new file mode 100644 index 0000000000..9ec571ff09 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs @@ -0,0 +1,235 @@ +//! Purpose: +//! Interpreter tests for binary string conversion and escaping builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover byte-preserving string helpers and base64 conversion. + +use super::super::*; +use super::support::*; + +/// Verifies eval `strrev()` dispatches through direct and callable paths. +#[test] +fn execute_program_dispatches_strrev_builtin() { + let program = parse_fragment( + br#"echo strrev("Hello"); echo ":"; +echo strrev(123); echo ":"; +echo call_user_func("strrev", "ABC"); echo ":"; +echo call_user_func_array("strrev", ["def"]); echo ":"; +return function_exists("strrev");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.output, "olleH:321:CBA:fed:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `chr()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_chr_builtin() { + let program = parse_fragment( + br#"echo chr(65); echo ":"; +echo bin2hex(chr(codepoint: 256)); echo ":"; +echo bin2hex(call_user_func("chr", 257)); echo ":"; +echo call_user_func_array("chr", ["codepoint" => 321]); echo ":"; +return function_exists("chr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:00:01:A:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_str_repeat_builtin() { + let program = parse_fragment( + br#"echo str_repeat("ha", 3); echo ":"; +echo strlen(str_repeat(string: "x", times: 0)); echo ":"; +echo call_user_func("str_repeat", "ab", 2); echo ":"; +echo call_user_func_array("str_repeat", ["string" => "z", "times" => 3]); echo ":"; +return function_exists("str_repeat");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hahaha:0:abab:zzz:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `substr()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_substr_builtin() { + let program = parse_fragment( + br#"echo substr("abcdef", 2); echo ":"; +echo substr(string: "abcdef", offset: 1, length: -1); echo ":"; +echo substr("abcdef", -2); echo ":"; +echo call_user_func("substr", "abcdef", 2, -2); echo ":"; +echo call_user_func_array("substr", ["string" => "abcdef", "offset" => -4, "length" => 2]); echo ":"; +return function_exists("substr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "cdef:bcde:ef:cd:cd:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `substr_replace()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_substr_replace_builtin() { + let program = parse_fragment( + br#"echo substr_replace("hello world", "PHP", 6, 5); echo ":"; +echo substr_replace(string: "abcdef", replace: "X", offset: 1, length: -1); echo ":"; +echo substr_replace("abcdef", "X", -2); echo ":"; +echo call_user_func("substr_replace", "abcdef", "X", 99, 1); echo ":"; +echo call_user_func_array("substr_replace", ["string" => "abcdef", "replace" => "X", "offset" => -99, "length" => 2]); echo ":"; +return function_exists("substr_replace");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hello PHP:aXf:abcdX:abcdefX:Xcdef:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `nl2br()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_nl2br_builtin() { + let program = parse_fragment( + br#"echo bin2hex(nl2br("a\nb")); echo ":"; +echo bin2hex(nl2br(string: "a\nb", use_xhtml: false)); echo ":"; +echo bin2hex(call_user_func("nl2br", "a\r\nb")); echo ":"; +echo bin2hex(call_user_func_array("nl2br", ["string" => "a\n\rb", "use_xhtml" => false])); echo ":"; +return function_exists("nl2br");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "613c6272202f3e0a62:613c62723e0a62:613c6272202f3e0d0a62:613c62723e0a0d62:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `bin2hex()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_bin2hex_builtin() { + let program = parse_fragment( + br#"echo bin2hex("Az"); echo ":"; +echo bin2hex(string: "A\n"); echo ":"; +echo call_user_func("bin2hex", "!?"); echo ":"; +echo call_user_func_array("bin2hex", ["string" => "ok"]); echo ":"; +return function_exists("bin2hex");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "417a:410a:213f:6f6b:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `hex2bin()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_hex2bin_builtin() { + let program = parse_fragment( + br#"echo hex2bin("417a"); echo ":"; +echo bin2hex(hex2bin(string: "410a")); echo ":"; +echo call_user_func("hex2bin", "213f"); echo ":"; +echo call_user_func_array("hex2bin", ["string" => "6f6b"]); echo ":"; +echo hex2bin("4") ? "bad" : "false"; echo ":"; +return function_exists("hex2bin");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Az:410a:!?:ok:false:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_eq!( + values.warnings, + vec![HEX2BIN_ODD_LENGTH_WARNING.to_string()] + ); +} +/// Verifies eval slash escaping builtins use PHP byte-string semantics. +#[test] +fn execute_program_dispatches_slash_escape_builtins() { + let program = parse_fragment( + br#"$escaped = addslashes($source); +echo bin2hex($escaped); echo ":"; +echo bin2hex(stripslashes($escaped)); echo ":"; +echo call_user_func("addslashes", "x\"y"); echo ":"; +echo call_user_func_array("stripslashes", [addslashes("o\"k")]); echo ":"; +return function_exists("addslashes") && function_exists("stripslashes");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let source = values.string("a\0b\\c\"d'").expect("create source"); + scope.set("source", source, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "615c30625c5c635c22645c27:6100625c63226427:x\\\"y:o\"k:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `base64_encode()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_base64_encode_builtin() { + let program = parse_fragment( + br#"echo base64_encode("Hello"); echo ":"; +echo base64_encode(string: "Hi"); echo ":"; +echo call_user_func("base64_encode", "Test 123!"); echo ":"; +echo call_user_func_array("base64_encode", ["string" => ""]); echo ":"; +return function_exists("base64_encode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "SGVsbG8=:SGk=:VGVzdCAxMjMh::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `base64_decode()` dispatches through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_base64_decode_builtin() { + let program = parse_fragment( + br#"echo base64_decode("SGVsbG8="); echo ":"; +echo base64_decode(string: "SGk="); echo ":"; +echo call_user_func("base64_decode", "VGVzdCAxMjMh"); echo ":"; +echo call_user_func_array("base64_decode", ["string" => ""]); echo ":"; +return function_exists("base64_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hello:Hi:Test 123!::"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs new file mode 100644 index 0000000000..b4ce28e518 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs @@ -0,0 +1,326 @@ +//! Purpose: +//! Interpreter tests for string splitting, replacing, regex, entity, URL, ctype, and hash builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover byte-string and encoded string builtin behavior. + +use super::super::*; +use super::support::*; + +/// Verifies eval `explode()` and `implode()` bridge byte strings and arrays. +#[test] +fn execute_program_dispatches_explode_implode_builtins() { + let program = parse_fragment( + br#"$parts = explode(",", "a,b,"); +echo count($parts); echo ":" . $parts[0] . ":" . $parts[1] . ":" . $parts[2]; +echo ":" . implode("|", $parts); +echo ":" . implode(separator: "-", array: ["x", 2, true, null]); +$call_parts = call_user_func("explode", ":", "m:n"); +echo ":" . $call_parts[1]; +echo ":" . call_user_func_array("implode", ["separator" => "/", "array" => ["p", "q"]]); +echo ":"; echo function_exists("explode"); +return function_exists("implode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:a:b::a|b|:x-2-1-:n:p/q:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `str_split()` builds indexed arrays of fixed-width chunks. +#[test] +fn execute_program_dispatches_str_split_builtin() { + let program = parse_fragment( + br#"$letters = str_split("abc"); +echo count($letters) . ":" . $letters[0] . $letters[1] . $letters[2]; echo ":"; +$pairs = str_split(string: "abcd", length: 2); +echo $pairs[0] . "-" . $pairs[1]; echo ":"; +$empty = str_split(""); +echo count($empty); echo ":"; +$call = call_user_func("str_split", "xyz", 2); +echo $call[0] . "-" . $call[1]; echo ":"; +$named = call_user_func_array("str_split", ["string" => "pqrs", "length" => 3]); +echo $named[0] . "-" . $named[1]; echo ":"; +return function_exists("str_split");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:abc:ab-cd:0:xy-z:pqr-s:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `str_pad()` supports PHP left, right, and both-side padding modes. +#[test] +fn execute_program_dispatches_str_pad_builtin() { + let program = parse_fragment( + br#"echo "[" . str_pad("hi", 5) . "]"; echo ":"; +echo "[" . str_pad(string: "hi", length: 5, pad_string: "_", pad_type: 0) . "]"; echo ":"; +echo "[" . str_pad("x", 6, "ab", 2) . "]"; echo ":"; +echo call_user_func("str_pad", "42", 5, "0", 0); echo ":"; +echo call_user_func_array("str_pad", ["string" => "x", "length" => 3, "pad_string" => "."]); echo ":"; +return function_exists("str_pad");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "[hi ]:[___hi]:[abxaba]:00042:x..:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval string replacement builtins support direct, named, and callable dispatch. +#[test] +fn execute_program_dispatches_string_replace_builtins() { + let program = parse_fragment( + br#"echo str_replace("o", "0", "Hello World"); echo ":"; +echo str_replace(search: "aa", replace: "b", subject: "aaaa"); echo ":"; +echo str_replace("", "x", "abc"); echo ":"; +echo str_ireplace("HE", "ye", "Hello he"); echo ":"; +echo call_user_func("str_replace", "l", "L", "hello"); echo ":"; +echo call_user_func_array("str_ireplace", ["search" => "x", "replace" => "Y", "subject" => "xX"]); echo ":"; +echo function_exists("str_replace"); +return function_exists("str_ireplace");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Hell0 W0rld:bb:abc:yello ye:heLLo:YY:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. +#[test] +fn execute_program_dispatches_preg_builtins() { + let program = parse_fragment( + br#"$ok = preg_match("/([a-z]+)([0-9]+)/", "id42", $matches); +echo $ok . ":" . count($matches) . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; +echo preg_match("/xyz/", "id42") . ":"; +echo preg_match_all("/[0-9]+/", "a1b22c333") . ":"; +$allCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $all); +echo $allCount . ":" . count($all) . ":" . $all[0][1] . ":" . $all[1][0] . ":" . $all[2][1] . ":"; +$setCount = preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $set, PREG_SET_ORDER); +echo $setCount . ":" . count($set) . ":" . $set[0][0] . ":" . $set[0][1] . ":" . $set[1][2] . ":"; +preg_match("/(a)?(b)/", "b", $offsetOne, PREG_OFFSET_CAPTURE); +echo $offsetOne[0][0] . ":" . $offsetOne[0][1] . ":" . $offsetOne[1][0] . ":" . $offsetOne[1][1] . ":" . $offsetOne[2][0] . ":" . $offsetOne[2][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetAll, PREG_OFFSET_CAPTURE); +echo $offsetAll[0][1][0] . ":" . $offsetAll[0][1][1] . ":" . $offsetAll[1][0][1] . ":" . $offsetAll[2][1][1] . ":"; +preg_match_all("/([a-z]+)([0-9]+)/", "a1 b22", $offsetSet, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); +echo $offsetSet[1][0][0] . ":" . $offsetSet[1][0][1] . ":" . $offsetSet[0][2][1] . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOne, PREG_UNMATCHED_AS_NULL); +echo count($nullOne) . ":" . ($nullOne[1] === null ? "n" : "bad") . ":" . $nullOne[2] . ":" . ($nullOne[3] === null ? "n" : "bad") . ":"; +preg_match("/(a)?(b)(c)?/", "b", $nullOffset, PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullOffset[1][0] === null ? "n" : "bad") . ":" . $nullOffset[1][1] . ":" . ($nullOffset[3][0] === null ? "n" : "bad") . ":" . $nullOffset[3][1] . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullAll, PREG_UNMATCHED_AS_NULL); +echo ($nullAll[1][0] === null ? "n" : "bad") . ":" . $nullAll[2][0] . ":" . ($nullAll[3][0] === null ? "n" : "bad") . ":"; +preg_match_all("/(a)?(b)(c)?/", "b", $nullSet, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL | PREG_OFFSET_CAPTURE); +echo ($nullSet[0][1][0] === null ? "n" : "bad") . ":" . $nullSet[0][1][1] . ":" . ($nullSet[0][3][0] === null ? "n" : "bad") . ":" . $nullSet[0][3][1] . ":"; +preg_match_all("/(x)(y)/", "abc", $none); +echo count($none) . ":" . count($none[0]) . ":" . count($none[1]) . ":" . count($none[2]) . ":"; +echo preg_replace("/([a-z])([0-9])/", "$2-$1", "a1 b2") . ":"; +function eval_regex_wrap($matches) { return "[" . $matches[0] . "]"; } +echo preg_replace_callback("/[A-Z]/", "eval_regex_wrap", "AB") . ":"; +$limited = preg_split("/,/", "a,b,c", 2); +echo count($limited) . ":" . $limited[0] . ":" . $limited[1] . ":"; +$kept = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY); +echo count($kept) . ":" . $kept[1] . ":"; +echo call_user_func("preg_match", "/x/", "x") . ":"; +$replaced = call_user_func_array("preg_replace", ["pattern" => "/[0-9]+/", "replacement" => "N", "subject" => "a12"]); +echo $replaced . ":"; +$captured = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE); +echo count($captured) . ":" . $captured[1] . ":"; +$splitOffsets = preg_split("/,/", "a,b,c", 2, PREG_SPLIT_OFFSET_CAPTURE); +echo $splitOffsets[0][0] . ":" . $splitOffsets[0][1] . ":" . $splitOffsets[1][0] . ":" . $splitOffsets[1][1] . ":"; +$splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE); +echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; +$splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); +echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; +return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. +#[test] +fn execute_program_dispatches_html_entity_builtins() { + let program = parse_fragment( + br#"echo htmlspecialchars("\"Hi\" & 'bye'"); echo ":"; +echo htmlentities(string: "
"); echo ":"; +echo html_entity_decode("<b>hi</b>"); echo ":"; +echo call_user_func("htmlspecialchars", ""); echo ":"; +echo call_user_func_array("html_entity_decode", ["string" => ""q""]); echo ":"; +echo function_exists("htmlspecialchars"); echo function_exists("htmlentities"); +return function_exists("html_entity_decode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "<b>"Hi" & 'bye'</b>:<a>:hi:<x>:\"q\":11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval URL codec builtins dispatch through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_url_codec_builtins() { + let program = parse_fragment( + br#"echo urlencode("a b&=~"); echo ":"; +echo rawurlencode(string: "a b&=~"); echo ":"; +echo urldecode("a+b%26%3D%7E"); echo ":"; +echo rawurldecode("a+b%26%3D%7E"); echo ":"; +echo call_user_func("urlencode", "%zz"); echo ":"; +echo call_user_func_array("rawurldecode", ["string" => "x%2By%zz"]); echo ":"; +echo function_exists("urlencode"); echo function_exists("rawurlencode"); +echo function_exists("urldecode"); +return function_exists("rawurldecode");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "a+b%26%3D%7E:a%20b%26%3D~:a b&=~:a+b&=~:%25zz:x+y%zz:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. +#[test] +fn execute_program_dispatches_ctype_builtins() { + let program = parse_fragment( + br#"echo ctype_alpha("abc") ? "A" : "-"; echo ":"; +echo ctype_digit(text: "123") ? "D" : "-"; echo ":"; +echo ctype_alnum("a1") ? "N" : "-"; echo ":"; +echo ctype_space(" \t\n" . chr(11) . chr(12) . "\r") ? "S" : "-"; echo ":"; +echo ctype_alpha("") ? "bad" : "empty"; echo ":"; +echo call_user_func("ctype_digit", "12x") ? "bad" : "not-digit"; echo ":"; +echo call_user_func_array("ctype_space", ["text" => " x"]) ? "bad" : "not-space"; echo ":"; +echo function_exists("ctype_alpha"); echo function_exists("ctype_digit"); +echo function_exists("ctype_alnum"); +return function_exists("ctype_space");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:D:N:S:empty:not-digit:not-space:111"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `crc32()` returns PHP-compatible non-negative checksums. +#[test] +fn execute_program_dispatches_crc32_builtin() { + let program = parse_fragment( + br#"echo crc32(""); echo ":"; +echo crc32(string: "123456789"); echo ":"; +echo call_user_func("crc32", "hello"); echo ":"; +echo call_user_func_array("crc32", ["string" => "The quick brown fox jumps over the lazy dog"]); echo ":"; +return function_exists("crc32");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:3421780262:907060870:1095738169:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `hash_algos()` returns supported hash names through callable dispatch too. +#[test] +fn execute_program_dispatches_hash_algos_builtin() { + let program = parse_fragment( + br#"$algos = hash_algos(); +echo count($algos) . ":" . $algos[0] . ":" . $algos[5] . ":"; +echo in_array("crc32c", $algos) ? "crc" : "bad"; +$call = call_user_func("hash_algos"); +echo ":" . $call[18]; +$spread = call_user_func_array("hash_algos", []); +echo ":" . $spread[27] . ":"; +echo function_exists("hash_algos") ? "exists" : "missing"; +return count($algos);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "28:md2:sha256:crc:whirlpool:joaat:exists"); + assert_eq!(values.get(result), FakeValue::Int(28)); +} +/// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. +#[test] +fn execute_program_dispatches_hash_digest_builtins() { + let filename = format!("elephc_eval_hash_file_{}.txt", std::process::id()); + let source = format!( + r#"echo md5("abc"); echo ":"; +echo sha1(string: "abc"); echo ":"; +echo hash("sha256", "abc"); echo ":"; +echo hash_hmac(algo: "sha256", data: "data", key: "key"); echo ":"; +echo bin2hex(md5("abc", true)); echo ":"; +echo bin2hex(call_user_func("sha1", "abc", true)); echo ":"; +echo call_user_func_array("hash", ["algo" => "md5", "data" => "abc"]); echo ":"; +echo call_user_func_array("hash_hmac", ["algo" => "sha256", "data" => "data", "key" => "key"]); echo ":"; +file_put_contents("{filename}", "abc"); +echo hash_file("sha256", "{filename}"); echo ":"; +echo bin2hex(hash_file(algo: "md5", filename: "{filename}", binary: true)); echo ":"; +echo call_user_func_array("hash_file", ["algo" => "md5", "filename" => "{filename}"]); echo ":"; +echo hash_file("sha256", "{filename}.missing") === false ? "missing" : "bad"; echo ":"; +unlink("{filename}"); +echo function_exists("md5"); echo function_exists("sha1"); echo function_exists("hash"); echo function_exists("hash_file"); +return function_exists("hash_hmac");"#, + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "900150983cd24fb0d6963f7d28e17f72:", + "5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "900150983cd24fb0d6963f7d28e17f72:", + "900150983cd24fb0d6963f7d28e17f72:", + "missing:", + "1111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_text.rs b/crates/elephc-eval/src/interpreter/tests/builtins_strings_text.rs new file mode 100644 index 0000000000..a1abf455d3 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_strings_text.rs @@ -0,0 +1,225 @@ +//! Purpose: +//! Interpreter tests for text-case, wrapping, search, comparison, and trim string builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover PHP byte-string behavior for text-oriented helpers. + +use super::super::*; +use super::support::*; + +/// Verifies eval ASCII string case builtins work directly and through callable dispatch. +#[test] +fn execute_program_dispatches_string_case_builtins() { + let program = parse_fragment( + br#"echo strtoupper("Hello World"); echo ":"; +echo strtolower("LOUD"); echo ":"; +echo ucfirst("eval"); echo ":"; +echo lcfirst("LOUD"); echo ":"; +echo call_user_func("strtoupper", "xy"); echo ":"; +echo call_user_func_array("strtolower", ["ZZ"]); echo ":"; +echo call_user_func("ucfirst", "case"); echo ":"; +echo call_user_func_array("lcfirst", ["CASE"]); +echo ":"; echo function_exists("strtoupper"); echo function_exists("strtolower"); echo function_exists("ucfirst"); +return function_exists("lcfirst");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "HELLO WORLD:loud:Eval:lOUD:XY:zz:Case:cASE:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `ucwords()` capitalizes word starts directly and by callable dispatch. +#[test] +fn execute_program_dispatches_ucwords_builtin() { + let program = parse_fragment( + br#"echo ucwords("hello world"); echo ":"; +echo ucwords(string: "hello-world", separators: "-"); echo ":"; +echo ucwords("hello\tworld"); echo ":"; +echo call_user_func("ucwords", "a b"); echo ":"; +echo call_user_func_array("ucwords", ["string" => "a-b", "separators" => "-"]); echo ":"; +return function_exists("ucwords");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Hello World:Hello-World:Hello\tWorld:A B:A-B:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `wordwrap()` wraps at word boundaries and can cut long words. +#[test] +fn execute_program_dispatches_wordwrap_builtin() { + let program = parse_fragment( + br#"echo wordwrap("The quick brown fox", 10, "|"); echo ":"; +echo wordwrap(string: "A verylongword here", width: 8, break: "|"); echo ":"; +echo wordwrap("abcdefghij", 4, "|", true); echo ":"; +echo wordwrap("preserve\nnewlines here ok", 10, "|"); echo ":"; +echo call_user_func("wordwrap", "aaa bbb ccc", 3, "
"); echo ":"; +echo call_user_func_array("wordwrap", ["string" => "hello world", "width" => 5, "break" => "|"]); +echo ":"; +return function_exists("wordwrap");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "The quick|brown fox:A|verylongword|here:abcd|efgh|ij:preserve\nnewlines|here ok:aaa
bbb
ccc:hello|world:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `str_contains()` uses byte-string search and supports callable dispatch. +#[test] +fn execute_program_dispatches_str_contains_builtin() { + let program = parse_fragment( + br#"echo str_contains("Hello World", "World") ? "Y" : "N"; +echo str_contains("Hello", "z") ? "bad" : ":N"; +echo str_contains("Hello", "") ? ":E" : "bad"; +echo call_user_func("str_contains", "abc", "b") ? ":C" : "bad"; +echo call_user_func_array("str_contains", ["abc", "x"]) ? "bad" : ":A"; +return function_exists("str_contains");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N:E:C:A"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval string position builtins return byte offsets or PHP false. +#[test] +fn execute_program_dispatches_string_position_builtins() { + let program = parse_fragment( + br#"echo strpos("banana", "na"); +echo ":" . strrpos("banana", "na"); +echo ":"; echo strpos("abc", "z") === false ? "F" : "bad"; +echo ":" . strpos("abc", ""); +echo ":" . strrpos("abc", ""); +echo ":" . call_user_func("strpos", "abc", "b"); +echo ":" . call_user_func_array("strrpos", ["ababa", "ba"]); +echo ":"; echo function_exists("strpos"); +return function_exists("strrpos");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:4:F:0:3:1:3:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `strstr()` returns suffixes, prefixes, or false for misses. +#[test] +fn execute_program_dispatches_strstr_builtin() { + let program = parse_fragment( + br#"echo strstr("user@example.com", "@"); echo ":"; +echo strstr(haystack: "hello world", needle: "lo", before_needle: true); echo ":"; +echo strstr("hello", "x") === false ? "F" : "bad"; echo ":"; +echo strstr("hello", ""); echo ":"; +echo call_user_func("strstr", "abcabc", "bc"); echo ":"; +echo call_user_func_array("strstr", ["haystack" => "abcabc", "needle" => "bc", "before_needle" => true]); echo ":"; +return function_exists("strstr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "@example.com:hel:F:hello:bcabc:a:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval prefix/suffix string search builtins use byte-string semantics. +#[test] +fn execute_program_dispatches_string_boundary_builtins() { + let program = parse_fragment( + br#"echo str_starts_with("Hello World", "Hello") ? "S" : "bad"; +echo str_starts_with("Hello", "World") ? "bad" : ":s"; +echo str_starts_with("Hello", "") ? ":se" : "bad"; +echo str_ends_with("Hello World", "World") ? ":E" : "bad"; +echo str_ends_with("Hello", "World") ? "bad" : ":e"; +echo str_ends_with("Hello", "") ? ":ee" : "bad"; +echo call_user_func("str_starts_with", "abc", "a") ? ":CS" : "bad"; +echo call_user_func_array("str_ends_with", ["abc", "c"]) ? ":CE" : "bad"; +echo ":"; echo function_exists("str_starts_with"); +return function_exists("str_ends_with");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "S:s:se:E:e:ee:CS:CE:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval string comparison builtins return PHP-compatible scalar results. +#[test] +fn execute_program_dispatches_string_compare_builtins() { + let program = parse_fragment( + br#"echo strcmp("abc", "abc"); +echo ":"; echo strcmp("abc", "abd") < 0 ? "lt" : "bad"; +echo ":"; echo strcasecmp("Hello", "hello"); +echo ":"; echo call_user_func("strcmp", "b", "a") > 0 ? "gt" : "bad"; +echo ":"; echo call_user_func_array("strcasecmp", ["A", "a"]) === 0 ? "ci" : "bad"; +echo ":"; echo hash_equals("abc", "abc") ? "heq" : "bad"; +echo ":"; echo hash_equals("abc", "abcd") ? "bad" : "hlen"; +echo ":"; echo call_user_func("hash_equals", "abc", "abd") ? "bad" : "hneq"; +echo ":"; echo function_exists("strcmp"); echo function_exists("strcasecmp"); +return function_exists("hash_equals");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:lt:0:gt:ci:heq:hlen:hneq:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval trim-like builtins strip default and explicit byte masks. +#[test] +fn execute_program_dispatches_trim_like_builtins() { + let program = parse_fragment( + br#"echo "[" . trim(" hello ") . "]"; +echo ":[" . ltrim(" left") . "]"; +echo ":[" . rtrim("right ") . "]"; +echo ":[" . chop("tail... ", " .") . "]"; +echo ":[" . trim("**boxed**", "*") . "]"; +echo ":[" . call_user_func("trim", " cuf ") . "]"; +echo ":[" . call_user_func_array("ltrim", ["0007", "0"]) . "]"; +echo ":"; echo function_exists("trim"); echo function_exists("ltrim"); echo function_exists("rtrim"); +return function_exists("chop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "[hello]:[left]:[right]:[tail]:[boxed]:[cuf]:[7]:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs new file mode 100644 index 0000000000..caa6bc5372 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs @@ -0,0 +1,293 @@ +//! Purpose: +//! Interpreter tests for isset, empty, function/class probes, dynamic constants, and class declarations. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover symbol-table and metadata probes backed by the eval context. + +use super::super::*; +use super::support::*; +use std::ffi::c_void; + +/// Verifies `isset` distinguishes missing, null, and other falsey values. +#[test] +fn execute_program_isset_distinguishes_missing_null_and_falsey_values() { + let program = parse_fragment( + br#"if (isset($missing)) { echo "1"; } else { echo "0"; } +if (isset($nullish)) { echo "1"; } else { echo "0"; } +if (isset($zero)) { echo "1"; } else { echo "0"; } +if (isset($empty)) { echo "1"; } else { echo "0"; } +if (isset($zero, $empty)) { echo "1"; } else { echo "0"; } +if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty = values.string("").expect("create fake string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty", empty, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "001110"); + assert_eq!(values.get(result), FakeValue::Null); +} +/// Verifies `empty` treats missing, null, and falsey values as empty. +#[test] +fn execute_program_empty_uses_php_truthiness_without_missing_warnings() { + let program = parse_fragment( + br#"if (empty($missing)) { echo "1"; } else { echo "0"; } +if (empty($nullish)) { echo "1"; } else { echo "0"; } +if (empty($zero)) { echo "1"; } else { echo "0"; } +if (empty($empty_string)) { echo "1"; } else { echo "0"; } +if (empty($zero_string)) { echo "1"; } else { echo "0"; } +if (empty($value)) { echo "1"; } else { echo "0"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let nullish = values.null().expect("create fake null"); + let zero = values.int(0).expect("create fake int"); + let empty_string = values.string("").expect("create fake empty string"); + let zero_string = values.string("0").expect("create fake zero string"); + let value = values.string("x").expect("create fake non-empty string"); + scope.set("nullish", nullish, ScopeCellOwnership::Owned); + scope.set("zero", zero, ScopeCellOwnership::Owned); + scope.set("empty_string", empty_string, ScopeCellOwnership::Owned); + scope.set("zero_string", zero_string, ScopeCellOwnership::Owned); + scope.set("value", value, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111110"); + assert_eq!(values.get(result), FakeValue::Null); +} +/// Verifies `isset` and `empty` use PHP offset semantics for array reads. +#[test] +fn execute_program_isset_and_empty_support_array_offsets() { + let program = parse_fragment( + br#"$map = [ + "present" => "x", + "nullish" => null, + "zero" => 0, + "empty" => "", + "child" => ["leaf" => "ok", "null" => null], +]; +echo isset($map["present"]) ? "1" : "0"; +echo isset($map["nullish"]) ? "1" : "0"; +echo isset($map["missing"]) ? "1" : "0"; +echo isset($map["zero"]) ? "1" : "0"; +echo isset($map["child"]["leaf"]) ? "1" : "0"; +echo isset($map["child"]["null"]) ? "1" : "0"; +echo isset($map["missing"]["leaf"]) ? "1" : "0"; +echo ":"; +echo empty($map["present"]) ? "1" : "0"; +echo empty($map["nullish"]) ? "1" : "0"; +echo empty($map["missing"]) ? "1" : "0"; +echo empty($map["zero"]) ? "1" : "0"; +echo empty($map["empty"]) ? "1" : "0"; +echo empty($map["child"]["leaf"]) ? "1" : "0"; +echo empty($map["child"]["null"]) ? "1" : "0"; +echo empty($map["missing"]["leaf"]) ? "1" : "0";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1001100:01111011"); + assert_eq!(values.get(result), FakeValue::Null); +} +/// Verifies eval builtin probes see dynamic functions and supported PHP-visible builtins. +#[test] +fn execute_program_function_probes_use_eval_context() { + let program = parse_fragment( + br#"function dyn_probe() { return 1; } +echo function_exists("DYN_PROBE") . "x"; +echo is_callable("dyn_probe") . "x"; +echo function_exists("strlen") . "x"; +echo function_exists("native_probe") . "x"; +echo function_exists("eval") . "x"; +echo function_exists("missing_probe") . "x";"#, + ) + .expect("parse eval fragment"); + let native = NativeFunction::new(1usize as *mut c_void, fake_native_return_descriptor, 0); + let mut context = ElephcEvalContext::new(); + assert!(context + .define_native_function("native_probe", native) + .is_ok()); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "1x1x1x1xxx"); +} +/// Verifies eval `interface_exists()` probes generated interface metadata by callable. +#[test] +fn execute_program_interface_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo interface_exists("KnownInterface") ? "Y" : "N"; +echo interface_exists("knowninterface") ? "Y" : "N"; +echo interface_exists("KnownClass") ? "Y" : "N"; +echo call_user_func("interface_exists", "KnownInterface") ? "Y" : "N"; +echo call_user_func_array("interface_exists", ["interface" => "KnownInterface"]) ? "Y" : "N"; +echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYNYYN"); +} +/// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. +#[test] +fn execute_program_class_like_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"echo trait_exists("KnownTrait") ? "T" : "t"; +echo trait_exists("knowntrait") ? "T" : "t"; +echo trait_exists("KnownEnum") ? "T" : "t"; +echo enum_exists("KnownEnum") ? "E" : "e"; +echo enum_exists("\knownenum") ? "E" : "e"; +echo enum_exists("KnownTrait") ? "E" : "e"; +echo call_user_func("trait_exists", "KnownTrait") ? "T" : "t"; +echo call_user_func_array("enum_exists", ["enum" => "KnownEnum"]) ? "E" : "e"; +echo trait_exists(trait: "MissingTrait", autoload: false) ? "T" : "t"; +echo enum_exists(enum: "MissingEnum", autoload: false) ? "E" : "e";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TTtEEeTEte"); +} +/// Verifies eval `is_a()` and `is_subclass_of()` dispatch through runtime class metadata. +#[test] +fn execute_program_is_a_relation_uses_runtime_probe() { + let program = parse_fragment( + br#"$object = new KnownClass(); +echo is_a($object, "KnownClass") ? "Y" : "N"; +echo is_subclass_of($object, "KnownClass") ? "Y" : "N"; +echo is_subclass_of($object, "ParentClass") ? "Y" : "N"; +echo call_user_func("is_a", $object, "ParentClass") ? "Y" : "N"; +echo call_user_func_array("is_subclass_of", ["object_or_class" => $object, "class" => "ParentClass"]) ? "Y" : "N"; +echo is_a(object_or_class: $object, class: "MissingClass", allow_string: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YNYYYN"); +} +/// Verifies eval `define()` and `defined()` share a dynamic constant-name table. +#[test] +fn execute_program_define_and_defined_use_dynamic_constant_table() { + let program = parse_fragment( + br#"echo define("DynEvalConst", "ok") ? "Y" : "N"; +echo DynEvalConst; +echo \DynEvalConst; +echo defined("DynEvalConst") ? "Y" : "N"; +echo defined("\\DynEvalConst") ? "Y" : "N"; +echo defined("dynevalconst") ? "Y" : "N"; +echo define("DynEvalConst", 2) ? "Y" : "N"; +echo call_user_func("defined", "DynEvalConst") ? "Y" : "N"; +echo call_user_func_array("defined", ["constant_name" => "\\DynEvalConst"]) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YokokYYNNYY"); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); +} +/// Verifies eval predefined runtime constants are fetchable and cannot be redefined. +#[test] +fn execute_program_reads_predefined_runtime_constants() { + let program = parse_fragment( + br#"echo PHP_EOL === "\n" ? "eol" : "bad"; echo ":"; +echo (PHP_OS === "Darwin" || PHP_OS === "Linux") ? "os" : "bad"; echo ":"; +echo DIRECTORY_SEPARATOR; echo ":"; +echo PHP_INT_MAX > 9000000000000000000 ? "int" : "bad"; echo ":"; +echo defined("PHP_OS") ? "defined" : "bad"; echo ":"; +echo defined("\\PHP_OS") ? "root" : "bad"; echo ":"; +echo defined("php_os") ? "bad" : "case"; echo ":"; +echo define("PHP_OS", "x") ? "bad" : "locked"; echo ":"; +return PHP_INT_MAX;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "eol:os:/:int:defined:root:case:locked:"); + assert_eq!(values.get(result), FakeValue::Int(i64::MAX)); + assert_eq!( + values.warnings, + vec![DEFINE_ALREADY_DEFINED_WARNING.to_string()] + ); +} +/// Verifies missing eval dynamic constants fail through runtime status. +#[test] +fn execute_program_missing_constant_fetch_fails() { + let program = parse_fragment(br#"return MissingEvalConst;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval class probes use the runtime class-name table. +#[test] +fn execute_program_class_exists_uses_runtime_probe() { + let program = parse_fragment( + br#"class DynProbe {} +echo class_exists("DynProbe") ? "Y" : "N"; +echo class_exists("\dynprobe") ? "Y" : "N"; +echo class_exists("KnownClass") ? "Y" : "N"; +echo class_exists("\knownclass") ? "Y" : "N"; +echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYYYN"); +} +/// Verifies duplicate eval-declared class names fail through runtime status. +#[test] +fn execute_program_duplicate_class_declaration_fails() { + let program = parse_fragment( + br#"class DynProbeDup {} +class dynprobedup {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("duplicate fails"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs b/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs new file mode 100644 index 0000000000..72d3b9c34c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs @@ -0,0 +1,418 @@ +//! Purpose: +//! Interpreter tests for time, system, SPL, environment, host, protocol, and IP builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases isolate platform-facing builtins behind deterministic fake assertions where possible. + +use super::super::*; +use super::support::*; + +/// Verifies eval zero-argument system builtins return native-compatible values. +#[test] +fn execute_program_dispatches_zero_arg_system_builtins() { + let program = parse_fragment( + br#"echo time() > 1000000000 ? "time" : "bad"; echo ":"; +echo phpversion(); echo ":"; +echo sys_get_temp_dir(); echo ":"; +echo strlen(getcwd()) > 0 ? "cwd" : "bad"; echo ":"; +echo call_user_func("time") > 1000000000 ? "call-time" : "bad"; echo ":"; +echo call_user_func("phpversion"); echo ":"; +echo call_user_func_array("getcwd", []) !== "" ? "call-cwd" : "bad"; echo ":"; +echo call_user_func_array("sys_get_temp_dir", []); echo ":"; +echo function_exists("time"); echo function_exists("phpversion"); echo function_exists("getcwd"); +return function_exists("sys_get_temp_dir");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + format!( + "time:{}:/tmp:cwd:call-time:{}:call-cwd:/tmp:111", + eval_compiler_php_version(), + eval_compiler_php_version() + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `date()` formats libc local timestamps and `mktime()` builds them. +#[test] +fn execute_program_dispatches_date_mktime_builtins() { + let program = parse_fragment( + br#"$ts = mktime(13, 2, 3, 1, 2, 2024); +echo date("Y-m-d H:i:s", $ts); +echo ":" . date("j-n-G-g-A-a-N-D-M-l-F", $ts); +echo ":" . (date("U", $ts) === strval($ts) ? "U" : "bad"); +echo ":" . call_user_func("date", "Y", $ts); +$named = call_user_func_array("mktime", ["hour" => 0, "minute" => 0, "second" => 0, "month" => 1, "day" => 1, "year" => 2000]); +echo ":" . date(format: "Y", timestamp: $named); +echo ":"; echo function_exists("date"); +return function_exists("mktime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2024-01-02 13:02:03:2-1-13-1-PM-pm-2-Tue-Jan-Tuesday-January:U:2024:2000:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. +#[test] +fn execute_program_dispatches_strtotime_builtin() { + let program = parse_fragment( + br#"$date = strtotime("2024-06-15"); +echo date("Y-m-d H:i:s", $date); +$full = strtotime("2024-06-15 12:30:45"); +echo ":" . date("Y-m-d H:i:s", $full); +$short = strtotime("2024-06-15T12:30"); +echo ":" . date("Y-m-d H:i:s", $short); +echo ":" . (strtotime("2024/06/15") === -1 ? "bad" : "wrong"); +$call = call_user_func("strtotime", "2024-01-02 03:04:05"); +echo ":" . date("Y-m-d H:i:s", $call); +$spread = call_user_func_array("strtotime", ["datetime" => "2024-01-02"]); +echo ":" . date("Y-m-d", $spread) . ":"; +return function_exists("strtotime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `microtime()` returns a plausible float timestamp by all call paths. +#[test] +fn execute_program_dispatches_microtime_builtin() { + let program = parse_fragment( + br#"echo microtime() > 1000000000 ? "now" : "bad"; echo ":"; +echo microtime(as_float: false) > 1000000000 ? "named" : "bad"; echo ":"; +echo call_user_func("microtime", true) > 1000000000 ? "call" : "bad"; echo ":"; +echo call_user_func_array("microtime", ["as_float" => true]) > 1000000000 ? "array" : "bad"; +echo ":"; +return function_exists("microtime");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "now:named:call:array:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. +#[test] +fn execute_program_dispatches_realpath_cache_builtins() { + let program = parse_fragment( + br#"$cache = realpath_cache_get(); +echo count($cache) . ":" . realpath_cache_size() . ":"; +$call_cache = call_user_func("realpath_cache_get"); +echo count($call_cache) . ":"; +echo call_user_func_array("realpath_cache_size", []) . ":"; +echo function_exists("realpath_cache_get"); +return function_exists("realpath_cache_size");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:0:0:0:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval stream introspection builtins return native-compatible static lists. +#[test] +fn execute_program_dispatches_stream_introspection_builtins() { + let program = parse_fragment( + br#"$wrappers = stream_get_wrappers(); +$transports = stream_get_transports(); +$filters = stream_get_filters(); +echo count($wrappers) . ":" . $wrappers[0] . ":" . $wrappers[5] . ":"; +echo count($transports) . ":" . $transports[0] . ":" . $transports[8] . ":"; +echo count($filters) . ":" . $filters[2] . ":"; +$call_wrappers = call_user_func("stream_get_wrappers"); +echo $call_wrappers[10] . ":"; +$call_transports = call_user_func_array("stream_get_transports", []); +echo $call_transports[11] . ":"; +$call_filters = call_user_func_array("stream_get_filters", []); +echo $call_filters[13] . ":"; +echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); +return function_exists("stream_get_filters");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. +#[test] +fn execute_program_dispatches_spl_classes_builtin() { + let program = parse_fragment( + br#"$names = spl_classes(); +echo count($names) . ":" . $names[0] . ":" . $names[55] . ":"; +echo (in_array("Exception", $names) ? "exception" : "bad") . ":"; +echo (in_array("SplDoublyLinkedList", $names) ? "list" : "bad") . ":"; +$call = call_user_func("spl_classes"); +echo (in_array("Throwable", $call) ? "call" : "bad") . ":"; +$spread = call_user_func_array("spl_classes", []); +echo (count($spread) === count($names) ? "spread" : "bad") . ":"; +echo function_exists("spl_classes"); +return is_callable("spl_classes");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "61:AppendIterator:Throwable:exception:list:call:spread:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval SPL object identity builtins are stable, unique, and callable. +#[test] +fn execute_program_dispatches_spl_object_identity_builtins() { + let program = parse_fragment( + br#"$a = new KnownClass(); +$b = new KnownClass(); +echo (spl_object_id($a) === spl_object_id($a)) ? "stable" : "drift"; +echo ":"; +echo (spl_object_id($a) !== spl_object_id($b)) ? "unique" : "same"; +echo ":"; +echo (spl_object_hash(object: $a) === spl_object_hash($a)) ? "hash" : "bad"; +echo ":"; +echo (call_user_func("spl_object_id", $a) === spl_object_id($a)) ? "call" : "bad"; +echo ":"; +echo (call_user_func_array("spl_object_hash", ["object" => $b]) === spl_object_hash($b)) ? "array" : "bad"; +echo ":"; +echo function_exists("spl_object_id"); +return function_exists("spl_object_hash");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stable:unique:hash:call:array:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval environment builtins read, write, unset, and dispatch dynamically. +#[test] +fn execute_program_dispatches_environment_builtins() { + let program = parse_fragment( + br#"putenv("ELEPHC_EVAL_ENV_TEST=direct"); +echo getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv(assignment: "ELEPHC_EVAL_ENV_TEST=named"); +echo getenv(name: "ELEPHC_EVAL_ENV_TEST") . ":"; +echo call_user_func("getenv", "ELEPHC_EVAL_ENV_TEST") . ":"; +echo call_user_func_array("putenv", ["assignment" => "ELEPHC_EVAL_ENV_TEST=spread"]) ? "set" : "bad"; +echo ":" . getenv("ELEPHC_EVAL_ENV_TEST") . ":"; +putenv("ELEPHC_EVAL_ENV_TEST"); +echo getenv("ELEPHC_EVAL_ENV_TEST") === "" ? "empty" : "bad"; +echo ":"; echo function_exists("getenv"); +return function_exists("putenv");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval sleep builtins dispatch without delaying focused tests. +#[test] +fn execute_program_dispatches_sleep_builtins() { + let program = parse_fragment( + br#"echo sleep(0) . ":"; +echo sleep(seconds: 0) . ":"; +usleep(0); +echo "u:"; +echo call_user_func("sleep", 0) . ":"; +echo call_user_func_array("usleep", ["microseconds" => 0]) === null ? "null" : "bad"; +echo ":"; echo function_exists("sleep"); +return function_exists("usleep");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "0:0:u:0:null:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `php_uname()` dispatches default, named, mode, and callable calls. +#[test] +fn execute_program_dispatches_php_uname_builtin() { + let program = parse_fragment( + br#"echo strlen(php_uname()) > 0 ? "all" : "empty"; echo ":"; +echo php_uname() === php_uname("a") ? "same" : "different"; echo ":"; +echo strlen(php_uname(mode: "s")) > 0 ? "sys" : "empty"; echo ":"; +echo strlen(php_uname("n")) > 0 ? "node" : "empty"; echo ":"; +echo strlen(php_uname("r")) > 0 ? "release" : "empty"; echo ":"; +echo strlen(php_uname("v")) > 0 ? "version" : "empty"; echo ":"; +echo strlen(php_uname("m")) > 0 ? "machine" : "empty"; echo ":"; +echo strlen(call_user_func("php_uname", "m")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("php_uname", ["mode" => "n"])) > 0 ? "spread" : "empty"; echo ":"; +return function_exists("php_uname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "all:same:sys:node:release:version:machine:call:spread:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `gethostbyname()` handles IPv4 literals and failed lookups. +#[test] +fn execute_program_dispatches_gethostbyname_builtin() { + let program = parse_fragment( + br#"echo gethostbyname("127.0.0.1") . ":"; +echo gethostbyname(hostname: "not a host") . ":"; +echo call_user_func("gethostbyname", "127.0.0.1") . ":"; +echo call_user_func_array("gethostbyname", ["hostname" => "not a host"]) . ":"; +return function_exists("gethostbyname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "127.0.0.1:not a host:127.0.0.1:not a host:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `gethostname()` dispatches direct and callable zero-arg calls. +#[test] +fn execute_program_dispatches_gethostname_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostname()) > 0 ? "host" : "empty"; echo ":"; +echo strlen(call_user_func("gethostname")) > 0 ? "call" : "empty"; echo ":"; +echo strlen(call_user_func_array("gethostname", [])) > 0 ? "spread" : "empty"; echo ":"; +return function_exists("gethostname");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "host:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `gethostbyaddr()` handles valid, malformed, and callable calls. +#[test] +fn execute_program_dispatches_gethostbyaddr_builtin() { + let program = parse_fragment( + br#"echo strlen(gethostbyaddr("127.0.0.1")) > 0 ? "direct" : "empty"; echo ":"; +echo strlen(gethostbyaddr(ip: "127.0.0.1")) > 0 ? "named" : "empty"; echo ":"; +echo gethostbyaddr("not-an-ip-address") === false ? "false" : "bad"; echo ":"; +echo strlen(call_user_func("gethostbyaddr", "127.0.0.1")) > 0 ? "call" : "empty"; echo ":"; +echo call_user_func_array("gethostbyaddr", ["ip" => "not-an-ip-address"]) === false ? "spread" : "bad"; echo ":"; +return function_exists("gethostbyaddr");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "direct:named:false:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval protocol and service database lookups dispatch dynamically. +#[test] +fn execute_program_dispatches_protocol_service_builtins() { + let program = parse_fragment( + br#"echo getprotobyname("TCP") . ":"; +echo getprotobynumber(6) . ":"; +echo getprotobyname("no_such_protocol") === false ? "missing-proto" : "bad"; echo ":"; +echo getprotobynumber(999) === false ? "missing-number" : "bad"; echo ":"; +echo getservbyname("www", "tcp") . ":"; +echo getservbyport(80, "tcp") . ":"; +echo getservbyname("no_such_service", "tcp") === false ? "missing-service" : "bad"; echo ":"; +echo getservbyport(80, "no_such_proto") === false ? "missing-port" : "bad"; echo ":"; +echo call_user_func("getprotobyname", "udp") . ":"; +echo call_user_func_array("getprotobynumber", ["protocol" => 17]) . ":"; +echo call_user_func("getservbyname", "https", "tcp") . ":"; +echo call_user_func_array("getservbyport", ["port" => 443, "protocol" => "tcp"]) . ":"; +echo function_exists("getprotobyname"); echo function_exists("getprotobynumber"); echo function_exists("getservbyname"); +return function_exists("getservbyport");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "6:tcp:missing-proto:missing-number:80:http:missing-service:missing-port:17:udp:443:https:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval IPv4 conversion builtins handle scalar and raw-byte paths. +#[test] +fn execute_program_dispatches_ip_conversion_builtins() { + let program = parse_fragment( + br#"echo long2ip(3232235777) . ":"; +echo long2ip(ip: 4294967295) . ":"; +echo ip2long("192.168.1.1") . ":"; +echo ip2long(ip: "1.2.3") === false ? "bad-ip" : "bad"; echo ":"; +$packed = inet_pton("1.2.3.4"); +echo bin2hex($packed) . ":"; +echo inet_pton(ip: "nonsense") === false ? "bad-pton" : "bad"; echo ":"; +echo inet_ntop($packed) . ":"; +echo inet_ntop(ip: "xx") === false ? "bad-ntop" : "bad"; echo ":"; +echo call_user_func("long2ip", 2130706433) . ":"; +echo call_user_func_array("ip2long", ["ip" => "0.0.0.0"]) . ":"; +echo function_exists("long2ip"); echo function_exists("ip2long"); +echo function_exists("inet_pton"); +return function_exists("inet_ntop");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "192.168.1.1:255.255.255.255:3232235777:bad-ip:01020304:bad-pton:1.2.3.4:bad-ntop:127.0.0.1:0:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/control_flow.rs b/crates/elephc-eval/src/interpreter/tests/control_flow.rs new file mode 100644 index 0000000000..5b9f22ce0c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/control_flow.rs @@ -0,0 +1,404 @@ +//! Purpose: +//! Interpreter tests for branching, loops, comparisons, match, logical operators, and foreach. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases exercise control-flow outcomes without leaving the fake runtime. + +use super::super::*; +use super::support::*; + +/// Verifies if/else executes only the PHP-truthy branch. +#[test] +fn execute_program_if_else_uses_php_truthiness() { + let program = parse_fragment(br#"if ($flag) { $x = "then"; } else { $x = "else"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(0).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::String("else".to_string())); +} +/// Verifies elseif chains execute the first truthy branch and skip later branches. +#[test] +fn execute_program_elseif_uses_first_truthy_branch() { + let program = + parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; } else { $x = "c"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let a = values.bool_value(false).expect("create fake bool"); + let b = values.bool_value(true).expect("create fake bool"); + scope.set("a", a, ScopeCellOwnership::Owned); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::String("b".to_string())); +} +/// Verifies while repeats while the condition remains truthy and propagates writes. +#[test] +fn execute_program_while_uses_php_truthiness() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.int(2).expect("create fake int"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let flag = scope + .visible_cell("flag") + .expect("scope should contain flag"); + + assert_eq!(values.output, "2"); + assert_eq!(values.get(flag), FakeValue::Bool(false)); +} +/// Verifies do/while runs the body before testing the condition. +#[test] +fn execute_program_do_while_runs_body_before_condition() { + let program = parse_fragment(br#"do { echo $i; $i = $i + 1; } while (false);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let i = values.int(0).expect("create fake int"); + scope.set("i", i, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "0"); + assert_eq!(values.get(i), FakeValue::Int(1)); +} +/// Verifies switch uses loose matching and falls through after the matching case. +#[test] +fn execute_program_switch_matches_and_falls_through() { + let program = + parse_fragment(br#"switch ($x) { case 1: echo "one"; break; case 2: echo "two"; default: echo "default"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(2).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "twodefault"); +} +/// Verifies for loops run init, condition, update, and body in PHP order. +#[test] +fn execute_program_for_loop_updates_after_body() { + let program = parse_fragment(br#"for ($i = 3; $i; $i = $i - 1) { echo $i; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "321"); + assert_eq!(values.get(i), FakeValue::Int(0)); +} +/// Verifies `continue` in a for loop still runs the update clause. +#[test] +fn execute_program_for_continue_runs_update_clause() { + let program = parse_fragment( + br#"for ($i = 3; $i; $i = $i - 1) { if ($i - 1) { continue; } echo "done"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "done"); + assert_eq!(values.get(i), FakeValue::Int(0)); +} +/// Verifies comparison operators return boolean cells usable by echo and branches. +#[test] +fn execute_program_comparisons_return_bool_cells() { + let program = parse_fragment( + br#"echo 2 < 3; echo 3 <= 3; echo 4 > 3; echo 4 >= 4; if ("10" == 10) { echo "n"; } if ("a" != "b") { echo "s"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1111ns"); +} +/// Verifies spaceship comparisons return PHP -1/0/1 integer cells. +#[test] +fn execute_program_spaceship_returns_int_cells() { + let program = + parse_fragment(br#"echo 1 <=> 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "-1:0:1"); +} +/// Verifies strict equality keeps PHP type identity distinct from loose equality. +#[test] +fn execute_program_strict_equality_uses_type_identity() { + let program = parse_fragment( + br#"echo "10" == 10; echo "10" === 10; echo "10" === "10"; echo "10" !== 10;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "111"); +} +/// Verifies logical AND skips an unsupported right-hand expression after a false left side. +#[test] +fn execute_program_short_circuits_logical_and() { + let program = parse_fragment(br#"return false && missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(false)); +} +/// Verifies logical OR skips an unsupported right-hand expression after a true left side. +#[test] +fn execute_program_short_circuits_logical_or() { + let program = parse_fragment(br#"return true || missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies match expressions use strict comparison across comma-separated patterns. +#[test] +fn execute_program_match_uses_strict_pattern_comparison() { + let program = + parse_fragment(br#"return match ($x) { 1, "1" => "string", default => "other" };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.string("1").expect("create fake string"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("string".to_string())); +} +/// Verifies match expressions evaluate only the selected arm result. +#[test] +fn execute_program_match_skips_unselected_results() { + let program = parse_fragment( + br#"return match (2) { 1 => missing(), 2 => "two", default => missing() };"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("two".to_string())); +} +/// Verifies match expressions without a matching arm or default fail at runtime. +#[test] +fn execute_program_match_without_default_fails_on_miss() { + let program = parse_fragment(br#"return match (3) { 1 => "one", 2 => "two" };"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} +/// Verifies PHP keyword logical operators use PHP precedence and short-circuiting. +#[test] +fn execute_program_evaluates_keyword_logical_operators() { + let program = + parse_fragment(br#"echo (false || true and false) ? "T" : "F"; return true or missing();"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "F"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies PHP keyword `xor` evaluates both operands and returns a boolean cell. +#[test] +fn execute_program_evaluates_keyword_xor() { + let program = + parse_fragment(br#"echo (true xor false) ? "T" : "F"; echo (true xor true) ? "T" : "F";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TF"); +} +/// Verifies ternary expressions evaluate only the selected branch. +#[test] +fn execute_program_ternary_short_circuits_unselected_branch() { + let program = + parse_fragment(br#"echo true ? "yes" : missing(); echo false ? missing() : "no";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "yesno"); +} +/// Verifies the short ternary form returns the condition value when it is truthy. +#[test] +fn execute_program_short_ternary_reuses_truthy_condition() { + let program = parse_fragment(br#"echo "x" ?: "fallback"; echo false ?: "fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "xfallback"); +} +/// Verifies null coalescing uses the default for missing or null values. +#[test] +fn execute_program_null_coalesce_uses_default_for_missing_or_null() { + let program = parse_fragment(br#"echo $missing ?? "fallback"; echo $x ?? "null-fallback";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.null().expect("create fake null"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "fallbacknull-fallback"); +} +/// Verifies null coalescing skips the default expression for non-null values. +#[test] +fn execute_program_null_coalesce_short_circuits_non_null_value() { + let program = parse_fragment(br#"echo "set" ?? missing();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "set"); +} +/// Verifies logical negation returns boolean cells using PHP truthiness. +#[test] +fn execute_program_evaluates_logical_not() { + let program = parse_fragment(br#"echo !false; echo !"x";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1"); +} +/// Verifies unary numeric operators delegate to PHP numeric runtime operations. +#[test] +fn execute_program_evaluates_unary_numeric_ops() { + let program = parse_fragment(br#"return -$x + +2;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(5).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(-3)); +} +/// Verifies foreach assigns each indexed element to the value variable. +#[test] +fn execute_program_foreach_iterates_indexed_values() { + let program = parse_fragment(br#"foreach (["a", "b"] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "ab"); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); +} +/// Verifies foreach key-value targets receive indexed integer keys and values. +#[test] +fn execute_program_foreach_assigns_indexed_keys() { + let program = + parse_fragment(br#"foreach (["a", "b"] as $key => $item) { echo $key . $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let key = scope.visible_cell("key").expect("scope should contain key"); + let item = scope + .visible_cell("item") + .expect("scope should contain last foreach item"); + + assert_eq!(values.output, "0a1b"); + assert_eq!(values.get(key), FakeValue::Int(1)); + assert_eq!(values.get(item), FakeValue::String("b".to_string())); +} +/// Verifies foreach over associative arrays preserves insertion-order keys and values. +#[test] +fn execute_program_foreach_iterates_assoc_keys_and_values() { + let program = parse_fragment( + br#"foreach (["a" => 1, "b" => 2] as $key => $item) { echo $key . ":" . $item . ";"; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a:1;b:2;"); +} +/// Verifies value-only foreach over associative arrays still yields values in insertion order. +#[test] +fn execute_program_foreach_iterates_assoc_values_only() { + let program = parse_fragment(br#"foreach (["a" => 1, "b" => 2] as $item) { echo $item; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12"); +} +/// Verifies break and continue control foreach execution inside eval. +#[test] +fn execute_program_foreach_honors_break_and_continue() { + let program = parse_fragment( + br#"foreach ([1, 2, 3] as $item) { if ($item == 1) { continue; } echo $item; break; }"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2"); +} diff --git a/crates/elephc-eval/src/interpreter/tests/core.rs b/crates/elephc-eval/src/interpreter/tests/core.rs new file mode 100644 index 0000000000..f7dda71fa8 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/core.rs @@ -0,0 +1,476 @@ +//! Purpose: +//! Interpreter tests for scope mutation, exceptions, includes, and early execution results. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover baseline eval execution before builtin-specific dispatch. + +use super::super::*; +use super::support::*; + +/// Verifies assignment writes a named scope entry and return reads it back. +#[test] +fn execute_program_stores_and_returns_scope_value() { + let program = parse_fragment(b"$x = 3; return $x + 4;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.get(x), FakeValue::Int(3)); + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies reference assignment aliases variable names and writes through the alias. +#[test] +fn execute_program_reference_assignment_updates_source_variable() { + let program = parse_fragment(b"$x = 1; $alias =& $x; $alias = 5; return $x;") + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + let alias = scope + .visible_cell("alias") + .expect("scope should contain alias"); + + assert_eq!(x, alias); + assert_eq!(values.get(x), FakeValue::Int(5)); + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies eval `throw` exits the program with a retained Throwable cell. +#[test] +fn execute_program_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)); + } + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } +} +/// Verifies eval `try/catch` catches a thrown object and binds the catch variable. +#[test] +fn execute_program_catches_throwable_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval `catch (Throwable)` can handle a throw without binding a variable. +#[test] +fn execute_program_catches_throwable_without_variable_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable) { + return 9; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("unbound catch should release the thrown object"); + + assert_eq!(scope.visible_cell("caught"), None); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(9)); +} +/// Verifies eval `catch (Exception)` matches thrown exception objects. +#[test] +fn execute_program_catches_specific_exception_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Exception $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval catch clauses keep source order and skip non-matching types. +#[test] +fn execute_program_skips_non_matching_specific_catch_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (RuntimeException $wrong) { + return 1; +} catch (Exception $caught) { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(scope.visible_cell("wrong"), None); + assert_eq!(values.get(result), FakeValue::Int(2)); +} +/// Verifies union catch clauses test later types in the same catch clause. +#[test] +fn execute_program_catches_union_type_inside_eval() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (RuntimeException|Exception $caught) { + return $caught->answer(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval `finally` runs before a pending try-body return is observed. +#[test] +fn execute_program_runs_finally_before_returning_try_value() { + let program = parse_fragment( + br#"try { + return 1; +} finally { + echo "finally"; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "finally"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} +/// Verifies eval `finally` return values replace pending try-body returns. +#[test] +fn execute_program_finally_return_overrides_try_return() { + let program = parse_fragment( + br#"try { + return 1; +} finally { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.releases.len(), 1); +} +/// Verifies eval `finally` return values replace pending uncaught throws. +#[test] +fn execute_program_finally_return_overrides_uncaught_throw() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} finally { + return 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let released = values + .releases + .first() + .copied() + .expect("overridden throw should be released"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.type_tag(released), Ok(EVAL_TAG_OBJECT)); +} +/// Verifies eval `finally` runs before an uncaught throw leaves the fragment. +#[test] +fn execute_program_runs_finally_before_uncaught_throw_outcome() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} finally { + echo "finally"; +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => { + assert_eq!(values.type_tag(value), Ok(EVAL_TAG_OBJECT)) + } + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } + assert_eq!(values.output, "finally"); +} +/// Verifies static locals declared inside eval catch blocks persist per function context. +#[test] +fn execute_context_function_persists_static_local_inside_catch() { + let program = parse_fragment( + br#"function dyn($e) { + try { + throw $e; + } catch (Throwable $caught) { + static $n = 0; + $n++; + return $caught->answer() + $n; + } +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let first_thrown = values + .new_object("Exception") + .expect("allocate first fake exception"); + let second_thrown = values + .new_object("Exception") + .expect("allocate second fake exception"); + + let first = execute_context_function(&mut context, "dyn", vec![first_thrown], &mut values) + .expect("execute first dynamic function call"); + let second = execute_context_function(&mut context, "dyn", vec![second_thrown], &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(43)); + assert_eq!(values.get(second), FakeValue::Int(44)); +} +/// Verifies static locals declared inside eval finally blocks persist per function context. +#[test] +fn execute_context_function_persists_static_local_inside_finally() { + let program = parse_fragment( + br#"function dyn() { + try { + return 0; + } finally { + static $n = 0; + $n++; + return $n; + } +}"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + + let first = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute first dynamic function call"); + let second = execute_context_function_zero_args(&mut context, "dyn", &mut values) + .expect("execute second dynamic function call"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(2)); +} +/// Verifies throws from eval-declared functions escape through the shared context. +#[test] +fn execute_context_function_propagates_throw_as_uncaught_outcome() { + let program = + parse_fragment(br#"function dyn($e) { throw $e; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("declare dynamic function"); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); + + let outcome = execute_context_function_outcome(&mut context, "dyn", vec![thrown], &mut values) + .expect("throw should be an eval function outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } +} +/// Verifies nested eval preserves the thrown cell while returning an uncaught status. +#[test] +fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { + let program = parse_fragment(br#"eval("throw $e;");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let thrown = values + .new_object("Exception") + .expect("allocate fake exception"); + scope.set("e", thrown, ScopeCellOwnership::Borrowed); + + let outcome = + execute_program_outcome_with_context(&mut context, &program, &mut scope, &mut values) + .expect("nested throw should be an eval outcome"); + + match outcome { + EvalOutcome::Throwable(value) => assert_eq!(value, thrown), + EvalOutcome::Value(value) => panic!("expected Throwable, got {:?}", values.get(value)), + } +} +/// Verifies eval include resolves caller-relative paths, shares scope, and returns file values. +#[test] +fn execute_program_include_uses_call_site_and_returns_file_result() { + let dir = std::env::temp_dir().join(format!( + "elephc-eval-include-{}-call-site", + std::process::id() + )); + let path = dir.join("piece.php"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).expect("create include fixture directory"); + std::fs::write( + &path, + format!( + r#" 2, "x" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. +#[test] +fn execute_context_function_call_array_binds_declared_named_args() { + let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + let arg_array = values.assoc_new(2).expect("allocate argument array"); + let key_y = values.string("y").expect("allocate y key"); + let value_y = values.int(2).expect("allocate y value"); + let _ = values + .array_set(arg_array, key_y, value_y) + .expect("store y argument"); + let key_x = values.string("x").expect("allocate x key"); + let value_x = values.int(1).expect("allocate x value"); + let _ = values + .array_set(arg_array, key_x, value_x) + .expect("store x argument"); + + let result = execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) + .expect("execute context function call array"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies `call_user_func_array` rejects positional values after named keys. +#[test] +fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", ["y" => 2, 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} +/// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. +#[test] +fn execute_program_call_user_func_array_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); +} +/// Verifies `call_user_func_array` inside eval can dispatch a registered native function. +#[test] +fn execute_program_call_user_func_array_dispatches_registered_native_function() { + let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies `call_user_func_array` named keys can bind registered native parameters. +#[test] +fn execute_program_call_user_func_array_binds_registered_native_named_args() { + let program = parse_fragment( + br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies duplicate eval-declared function names fail in a shared context. +#[test] +fn execute_program_rejects_duplicate_declared_function() { + let define = parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute first declaration"); + let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect_err("duplicate function declaration should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs new file mode 100644 index 0000000000..497ed51bd6 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -0,0 +1,340 @@ +//! Purpose: +//! Interpreter tests for scalar expressions, echo/print, objects, and construction. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases assert EvalIR expression execution against fake runtime values. + +use super::super::*; +use super::support::*; + +/// Verifies simple variable compound assignments read, compute, and write the scope value. +#[test] +fn execute_program_evaluates_compound_assignments() { + let program = + parse_fragment(br#"$x = 2; $x += 3; $x *= 4; $x -= 5; $s = "v"; $s .= $x; echo $s;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "v15"); + assert_eq!(values.get(x), FakeValue::Int(15)); +} +/// Verifies division and modulo evaluate through fake runtime numeric hooks. +#[test] +fn execute_program_evaluates_division_and_modulo() { + let program = parse_fragment(br#"$x = 20; $x /= 2; $x %= 6; echo $x; return 9 / 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "4"); + assert_eq!(values.get(x), FakeValue::Int(4)); + assert_eq!(values.get(result), FakeValue::Float(4.5)); +} +/// Verifies exponentiation evaluates through fake runtime numeric hooks. +#[test] +fn execute_program_evaluates_exponentiation() { + let program = parse_fragment( + br#"$x = 2; $x **= 3; echo $x; echo ":"; echo -2 ** 2; return 2 ** 3 ** 2;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "8:-4"); + assert_eq!(values.get(x), FakeValue::Float(8.0)); + assert_eq!(values.get(result), FakeValue::Float(512.0)); +} +/// Verifies bitwise and shift operators evaluate through fake runtime hooks. +#[test] +fn execute_program_evaluates_bitwise_and_shift_ops() { + let program = parse_fragment( + br#"$x = 6; $x &= 3; echo $x; echo ":"; +$x = 4; $x |= 1; echo $x; echo ":"; +$x = 7; $x ^= 3; echo $x; echo ":"; +$x = 1; $x <<= 5; echo $x; echo ":"; +$x = 64; $x >>= 3; echo $x; echo ":"; +echo ~0; echo ":"; echo -16 >> 2; +return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:5:4:32:8:-1:-4"); + assert_eq!(values.get(result), FakeValue::Int(21)); +} +/// Verifies simple variable increment and decrement statements update the scope value. +#[test] +fn execute_program_evaluates_inc_dec_statements() { + let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "1"); + assert_eq!(values.get(i), FakeValue::Int(1)); +} +/// Verifies echo and unset operate through runtime hooks and scope metadata. +#[test] +fn execute_program_echoes_and_unsets_scope_value() { + let program = + parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let name = values.string(" Ada").expect("create fake string"); + scope.set("name", name, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hi Ada"); + assert_eq!(values.get(result), FakeValue::Null); + assert!(scope.entry("name").expect("unset marker").flags().unset); +} +/// Verifies comma-separated echo expressions are executed in source order. +#[test] +fn execute_program_echoes_comma_list() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let b = values.string("b").expect("create fake string"); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "abc"); +} +/// Verifies print writes output and returns integer 1. +#[test] +fn execute_program_print_returns_one() { + let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "p"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} +/// Verifies eval `print_r()` emits supported values and returns true. +#[test] +fn execute_program_dispatches_print_r_builtin() { + let program = parse_fragment( + br#"print_r("x"); echo ":"; +print_r(value: false); echo ":"; +print_r([1, 2]); echo ":"; +$call = call_user_func("print_r", true); +$spread = call_user_func_array("print_r", ["value" => "z"]); +echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; +return function_exists("print_r");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "x::Array\n:1z:call:spread:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. +#[test] +fn execute_program_dispatches_var_dump_builtin() { + let program = parse_fragment( + br#"var_dump(42); +var_dump("hi"); +var_dump(false); +var_dump(null); +var_dump([10, 20]); +var_dump(["x" => true]); +$call = call_user_func("var_dump", 3.5); +$spread = call_user_func_array("var_dump", ["value" => "z"]); +echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; +return function_exists("var_dump");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "int(42)\n", + "string(2) \"hi\"\n", + "bool(false)\n", + "NULL\n", + "array(2) {\n", + " [0]=>\n", + " int(10)\n", + " [1]=>\n", + " int(20)\n", + "}\n", + "array(1) {\n", + " [\"x\"]=>\n", + " bool(true)\n", + "}\n", + "float(3.5)\n", + "string(1) \"z\"\n", + "call-null:spread-null:", + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval property reads and writes dispatch through runtime hooks. +#[test] +fn execute_program_reads_and_writes_object_property() { + let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!( + values + .property_get(object, "x") + .map(|value| values.get(value)) + .expect("property should be readable"), + FakeValue::Int(2) + ); +} +/// Verifies eval method calls dispatch through the runtime method hook. +#[test] +fn execute_program_calls_object_method() { + let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval method calls forward evaluated arguments to the runtime hook. +#[test] +fn execute_program_calls_object_method_with_argument() { + let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. +#[test] +fn execute_program_calls_object_method_with_two_arguments() { + let program = parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); +} +/// Verifies eval method calls forward numerically unpacked arguments. +#[test] +fn execute_program_calls_object_method_with_spread_arguments() { + let program = + parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); +} +/// Verifies eval object construction dispatches through runtime hooks. +#[test] +fn execute_program_constructs_named_object() { + let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Object(Vec::new())); +} +/// Verifies eval object construction passes constructor arguments through runtime hooks. +#[test] +fn execute_program_constructs_named_object_with_args() { + let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let FakeValue::Object(properties) = values.get(result) else { + panic!("expected fake object"); + }; + let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); + + assert_eq!(values.get(x), FakeValue::Int(1)); +} +/// Verifies eval-declared classes create objects with properties and methods. +#[test] +fn execute_program_constructs_eval_declared_class_with_method() { + let program = parse_fragment( + br#"class DynBox { + public int $x = 1; + public function __construct($x) { $this->x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +} +$box = new DynBox(4); +echo get_class($box); +echo ":"; +echo $box->bump(3); +echo ":"; +echo is_a($box, "DynBox") ? "Y" : "N"; +$call = [$box, "bump"]; +echo call_user_func($call, 1); +echo ":"; +echo call_user_func_array($call, [2]); +echo ":"; +return $box->x;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DynBox:7:Y8:10:"); + assert_eq!(values.get(result), FakeValue::Int(10)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/functions_namespaces.rs b/crates/elephc-eval/src/interpreter/tests/functions_namespaces.rs new file mode 100644 index 0000000000..b045a28ef3 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/functions_namespaces.rs @@ -0,0 +1,397 @@ +//! Purpose: +//! Interpreter tests for nested eval, magic constants, namespaces, functions, globals, and argument rules. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases share a persistent eval context when declarations must survive across calls. + +use super::super::*; +use super::support::*; + +/// Verifies nested eval calls parse and execute against the same dynamic scope. +#[test] +fn execute_program_nested_eval_uses_same_scope() { + let program = + parse_fragment(br#"eval("$x = $x + 4;"); return $x;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + scope.set("x", x, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies `__LINE__` inside eval uses the source line within the fragment. +#[test] +fn execute_program_magic_line_uses_fragment_line() { + let program = parse_fragment(b"\nreturn __LINE__;").expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); +} +/// Verifies file-dependent eval magic constants use call-site metadata from the context. +#[test] +fn execute_program_magic_file_and_dir_use_context_call_site() { + let program = + parse_fragment(br#"return __FILE__ . "|" . __DIR__;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/main.php", "/tmp", 17); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("/tmp/main.php(17) : eval()'d code|/tmp".to_string()) + ); +} +/// Verifies eval class, namespace, and trait magic constants are empty in eval scope. +#[test] +fn execute_program_scope_magic_constants_are_empty_strings() { + let program = + parse_fragment(br#"return "[" . __CLASS__ . "|" . __NAMESPACE__ . "|" . __TRAIT__ . "]";"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("[||]".to_string())); +} +/// Verifies eval-declared functions can be called by the same fragment. +#[test] +fn execute_program_calls_declared_function() { + let program = parse_fragment(br#"function dyn($x) { return $x + 1; } return dyn(4);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies eval namespace declarations qualify functions and namespace magic values. +#[test] +fn execute_program_namespace_qualifies_declared_function() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function dyn() { return __NAMESPACE__ . ":" . __FUNCTION__; } +return dyn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("Eval\\Ns:Eval\\Ns\\dyn".to_string()) + ); +} +/// Verifies unqualified namespaced calls fall back to global builtins when needed. +#[test] +fn execute_program_namespace_call_falls_back_to_builtin() { + let program = parse_fragment(br#"namespace Eval\Ns; return strlen("abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); +} +/// Verifies namespaced dynamic functions take precedence over global builtin fallback. +#[test] +fn execute_program_namespace_function_overrides_builtin_fallback() { + let program = parse_fragment( + br#"namespace Eval\Ns; +function strlen($value) { return 99; } +return strlen("abcd");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(99)); +} +/// Verifies unqualified namespaced constants fall back to global predefined constants. +#[test] +fn execute_program_namespace_const_fetch_falls_back_to_global() { + let program = + parse_fragment(br#"namespace Eval\Ns; return PHP_EOL;"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("\n".to_string())); +} +/// Verifies namespaced dynamic constants take precedence over global fallback. +#[test] +fn execute_program_namespace_const_fetch_reads_dynamic_constant_first() { + let program = + parse_fragment(br#"namespace Eval\Ns; return LOCAL;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(7).expect("create fake int"); + assert!(context.define_constant("Eval\\Ns\\LOCAL", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies eval namespace `use function` imports dispatch to qualified dynamic functions. +#[test] +fn execute_program_namespace_use_function_import_dispatches() { + let program = parse_fragment( + br#"namespace Eval\Lib; +function target($x) { return $x + 1; } +namespace Eval\App; +use function Eval\Lib\target as AliasTarget; +return aliastarget(6);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies eval namespace `use const` imports fetch qualified dynamic constants. +#[test] +fn execute_program_namespace_use_const_import_fetches_dynamic_constant() { + let program = parse_fragment( + br#"namespace Eval\App; +use const Eval\Lib\VALUE as LocalValue; +return LocalValue;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(11).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(11)); +} +/// Verifies eval grouped namespace imports dispatch dynamic functions and constants. +#[test] +fn execute_program_grouped_namespace_use_imports_dispatch() { + let program = parse_fragment( + br#"namespace Eval\Lib; +function target($x) { return $x + 2; } +namespace Eval\App; +use function Eval\Lib\{target as AliasTarget}; +use const Eval\Lib\{VALUE as LocalValue}; +return AliasTarget(LocalValue);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let value = values.int(5).expect("create fake int"); + assert!(context.define_constant("Eval\\Lib\\VALUE", value)); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies eval-declared functions bind named arguments by parameter name. +#[test] +fn execute_program_calls_declared_function_with_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(y: 2, x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies eval-declared functions unpack indexed arrays as positional arguments. +#[test] +fn execute_program_calls_declared_function_with_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...[1, 2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies string keys unpack as named arguments for eval-declared functions. +#[test] +fn execute_program_calls_declared_function_with_named_spread_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } return dyn(...["y" => 2], x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies eval-declared function static locals persist between calls. +#[test] +fn execute_program_static_var_persists_in_declared_function() { + let program = parse_fragment( + br#"function dyn() { for ($i = 0; $i < 2; $i++) { static $n = 0; $n++; } return $n; } +return (dyn() * 10) + dyn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(24)); +} +/// Verifies top-level eval static declarations reinitialize on each eval execution. +#[test] +fn execute_program_top_level_static_var_reinitializes_per_eval() { + let program = + parse_fragment(br#"static $n = 0; $n++; return $n;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let first = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute first eval ir"); + let second = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute second eval ir"); + + assert_eq!(values.get(first), FakeValue::Int(1)); + assert_eq!(values.get(second), FakeValue::Int(1)); +} +/// Verifies `global` declarations read and write the context global scope. +#[test] +fn execute_program_global_alias_writes_context_global_scope() { + let program = + parse_fragment(br#"global $g; $g = $g + 1; return $g;"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!(values.get(global), FakeValue::Int(2)); +} +/// Verifies references to global aliases write the source global variable. +#[test] +fn execute_program_reference_alias_to_global_updates_source_global() { + let program = parse_fragment(br#"global $g; $alias =& $g; $alias = 4; return $g;"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut global_scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let initial = values.int(1).expect("allocate initial global"); + global_scope.set("g", initial, ScopeCellOwnership::Owned); + context.set_global_scope(&mut global_scope); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + let global = global_scope + .visible_cell("g") + .expect("global scope should contain g"); + assert_eq!(values.get(result), FakeValue::Int(4)); + assert_eq!(values.get(global), FakeValue::Int(4)); + assert!(global_scope.visible_cell("alias").is_none()); +} +/// Verifies named calls reject positional arguments that follow named arguments. +#[test] +fn execute_program_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, print "late");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert_eq!(values.output, ""); +} +/// Verifies named calls reject argument unpacking after named arguments. +#[test] +fn execute_program_rejects_spread_after_named_arg() { + let program = + parse_fragment(br#"function dyn($x, $y) { return $x + $y; } return dyn(x: 1, ...[2]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} +/// Verifies function-scope magic constants keep the eval declaration spelling. +#[test] +fn execute_program_magic_function_and_method_use_eval_declared_name() { + let program = parse_fragment( + br#"function DynMagicCase() { return __FUNCTION__ . ":" . __METHOD__; } return dynmagiccase();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.get(result), + FakeValue::String("DynMagicCase:DynMagicCase".to_string()) + ); +} +/// Verifies eval-declared functions persist in a shared eval context. +#[test] +fn execute_program_context_keeps_declared_function() { + let define = + parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("parse eval fragment"); + let call = parse_fragment(br#"return dyn(4);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute eval ir"); + let result = execute_program_with_context(&mut context, &call, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs new file mode 100644 index 0000000000..3197f7b34c --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -0,0 +1,33 @@ +//! Purpose: +//! Interpreter test module wiring and shared fake runtime support. +//! The concrete tests live in focused child modules so each file owns one +//! execution surface instead of one large mixed test bucket. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - `support` exposes the fake runtime cells used by all interpreter tests. +//! - Child modules import the interpreter entry points from their parent module. + +mod array_literals; +mod builtins_arrays_core; +mod builtins_arrays_iterators; +mod builtins_arrays_sets; +mod builtins_filesystem_metadata; +mod builtins_filesystem_ops; +mod builtins_json; +mod builtins_math_formatting; +mod builtins_scalars; +mod builtins_strings_binary; +mod builtins_strings_encoding; +mod builtins_strings_text; +mod builtins_symbols; +mod builtins_system_network; +mod control_flow; +mod core; +mod dynamic_calls; +mod expressions; +mod functions_namespaces; +mod native_scope; +mod support; diff --git a/crates/elephc-eval/src/interpreter/tests/native_scope.rs b/crates/elephc-eval/src/interpreter/tests/native_scope.rs new file mode 100644 index 0000000000..cbb404dc56 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/native_scope.rs @@ -0,0 +1,214 @@ +//! Purpose: +//! Interpreter tests for native function dispatch, scope array mutation, ownership, break, and continue. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover integration edges between scope cells and fake runtime hooks. + +use super::super::*; +use super::support::*; + +/// Verifies eval fragments can dispatch registered native AOT functions. +#[test] +fn execute_program_calls_registered_native_function() { + let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies direct eval calls can bind registered native parameters by name. +#[test] +fn execute_program_calls_registered_native_function_with_named_args() { + let program = parse_fragment(br#"return native_answer(right: 2, left: 1);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies direct eval calls can unpack arrays into registered native parameters. +#[test] +fn execute_program_calls_registered_native_function_with_spread_args() { + let program = + parse_fragment(br#"return native_answer(...[1, 2]);"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies indexed array writes mutate an existing scope array. +#[test] +fn execute_program_writes_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[1] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} +/// Verifies indexed array append writes use the next visible index. +#[test] +fn execute_program_appends_indexed_scope_array() { + let program = parse_fragment(br#"$items = ["a"]; $items[] = "b"; return $items[1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("b".to_string())); +} +/// Verifies associative append starts at key zero when only string keys exist. +#[test] +fn execute_program_appends_assoc_scope_array_with_string_keys() { + let program = + parse_fragment(br#"$items = ["name" => "Ada"]; $items[] = "Grace"; return $items[0];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} +/// Verifies associative append uses one plus the largest existing integer key. +#[test] +fn execute_program_appends_assoc_scope_array_after_positive_int_key() { + let program = parse_fragment( + br#"$items = [2 => "two", "name" => "Ada"]; $items[] = "tail"; return $items[3];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies associative append preserves PHP's largest-negative-key behavior. +#[test] +fn execute_program_appends_assoc_scope_array_after_negative_int_key() { + let program = + parse_fragment(br#"$items = [-2 => "minus"]; $items[] = "tail"; return $items[-1];"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("tail".to_string())); +} +/// Verifies mutating a borrowed scope array does not make the eval scope own it. +#[test] +fn execute_program_preserves_borrowed_array_ownership() { + let program = parse_fragment(br#"$items[0] = "b";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let array = values.array_new(1).expect("create fake array"); + scope.set("items", array, ScopeCellOwnership::Borrowed); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let entry = scope.entry("items").expect("scope should contain items"); + + assert_eq!(entry.cell(), array); + assert_eq!(entry.flags().ownership, ScopeCellOwnership::Borrowed); + assert!(values.releases.is_empty()); +} +/// Verifies replacing an eval-owned scope value releases the old cell. +#[test] +fn execute_program_releases_replaced_scope_value() { + let program = parse_fragment(br#"$x = "old"; $x = "new";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); +} +/// Verifies unsetting an eval-owned scope value releases the old cell. +#[test] +fn execute_program_releases_unset_scope_value() { + let program = parse_fragment(br#"$x = "old"; unset($x);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.releases.len(), 1); + assert_eq!( + values.get(values.releases[0]), + FakeValue::String("old".to_string()) + ); +} +/// Verifies break exits a runtime eval loop before later statements run. +#[test] +fn execute_program_break_exits_loop() { + let program = parse_fragment(br#"while ($flag) { echo "a"; break; echo "b"; }"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "a"); +} +/// Verifies continue restarts a runtime eval loop and observes later scope updates. +#[test] +fn execute_program_continue_restarts_loop() { + let program = parse_fragment( + br#"while ($flag) { $flag = false; continue; echo "unreachable"; } echo "done";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let flag = values.bool_value(true).expect("create fake bool"); + scope.set("flag", flag, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "done"); +} diff --git a/crates/elephc-eval/src/interpreter/tests/support/array_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/array_ops.rs new file mode 100644 index 0000000000..f815d8de8a --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/support/array_ops.rs @@ -0,0 +1,145 @@ +//! Purpose: +//! Array-related fake runtime operations for interpreter tests. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers back RuntimeValueOps array creation, reads, writes, and array tag checks. + +use super::*; + +impl FakeOps { + /// Creates a fake indexed array cell. + pub(super) fn runtime_array_new( + &mut self, + capacity: usize, + ) -> Result { + Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) + } + /// Creates a fake associative array cell. + pub(super) fn runtime_assoc_new( + &mut self, + _capacity: usize, + ) -> Result { + Ok(self.alloc(FakeValue::Assoc(Vec::new()))) + } + /// Reads one fake indexed array element. + pub(super) fn runtime_array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + match self.get(array) { + FakeValue::Array(elements) => { + let FakeKey::Int(index) = key else { + return self.null(); + }; + if index < 0 { + return self.null(); + } + elements + .get(index as usize) + .copied() + .map_or_else(|| self.null(), Ok) + } + FakeValue::Assoc(entries) => entries + .iter() + .find_map(|(entry_key, value)| (entry_key == &key).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => self.null(), + } + } + /// Checks whether a fake array has the requested key without reading its value. + pub(super) fn runtime_array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + let key = self.key(key)?; + let exists = match self.get(array) { + FakeValue::Array(elements) => { + matches!(key, FakeKey::Int(index) if index >= 0 && (index as usize) < elements.len()) + } + FakeValue::Assoc(entries) => entries.iter().any(|(entry_key, _)| entry_key == &key), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.bool_value(exists) + } + /// Returns one fake foreach key by insertion-order position. + pub(super) fn runtime_array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(array) { + FakeValue::Array(elements) if position < elements.len() => self.int(position as i64), + FakeValue::Assoc(entries) => { + let Some((key, _)) = entries.get(position) else { + return self.null(); + }; + self.alloc_key(key) + } + FakeValue::Array(_) => self.null(), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Writes one fake indexed or associative array element. + pub(super) fn runtime_array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + let key = self.key(index)?; + let id = array.as_ptr() as usize; + match self.values.get_mut(&id) { + Some(FakeValue::Array(elements)) => { + let FakeKey::Int(index) = key else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if index < 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let index = index as usize; + while elements.len() <= index { + elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); + } + elements[index] = value; + } + Some(FakeValue::Assoc(entries)) => { + if let Some((_, existing_value)) = + entries.iter_mut().find(|(entry_key, _)| entry_key == &key) + { + *existing_value = value; + } else { + entries.push((key, value)); + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + Ok(array) + } + /// Returns the visible element count for fake array values. + pub(super) fn runtime_array_len( + &mut self, + array: RuntimeCellHandle, + ) -> Result { + match self.get(array) { + FakeValue::Array(elements) => Ok(elements.len()), + FakeValue::Assoc(entries) => Ok(entries.len()), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns whether a fake runtime cell is an indexed or associative array. + pub(super) fn runtime_is_array_like( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + Ok(matches!( + self.get(value), + FakeValue::Array(_) | FakeValue::Assoc(_) + )) + } +} diff --git a/crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs new file mode 100644 index 0000000000..1df3cc6ec2 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Scalar cell construction, type-tag, byte, and truthiness fake runtime operations. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers allocate primitive fake values and expose PHP-like truthiness. + +use super::*; + +impl FakeOps { + /// Returns whether a fake runtime cell is null. + pub(super) fn runtime_is_null(&mut self, value: RuntimeCellHandle) -> Result { + Ok(matches!(self.get(value), FakeValue::Null)) + } + /// Returns the fake runtime tag corresponding to a test value. + pub(super) fn runtime_type_tag(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Int(_) => EVAL_TAG_INT, + FakeValue::String(_) | FakeValue::Bytes(_) => EVAL_TAG_STRING, + FakeValue::Float(_) => EVAL_TAG_FLOAT, + FakeValue::Bool(_) => EVAL_TAG_BOOL, + FakeValue::Array(_) => EVAL_TAG_ARRAY, + FakeValue::Assoc(_) => EVAL_TAG_ASSOC, + FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, + FakeValue::Resource(_) => EVAL_TAG_RESOURCE, + FakeValue::Null => EVAL_TAG_NULL, + }) + } + /// Creates a fake null cell. + pub(super) fn runtime_null(&mut self) -> Result { + Ok(self.alloc(FakeValue::Null)) + } + /// Creates a fake bool cell. + pub(super) fn runtime_bool_value( + &mut self, + value: bool, + ) -> Result { + Ok(self.alloc(FakeValue::Bool(value))) + } + /// Creates a fake int cell. + pub(super) fn runtime_int(&mut self, value: i64) -> Result { + Ok(self.alloc(FakeValue::Int(value))) + } + /// Creates a fake float cell. + pub(super) fn runtime_float(&mut self, value: f64) -> Result { + Ok(self.alloc(FakeValue::Float(value))) + } + /// Creates a fake string cell. + pub(super) fn runtime_string(&mut self, value: &str) -> Result { + Ok(self.alloc(FakeValue::String(value.to_string()))) + } + /// Creates a fake string cell from raw PHP bytes. + pub(super) fn runtime_string_bytes_value( + &mut self, + value: &[u8], + ) -> Result { + match std::str::from_utf8(value) { + Ok(value) => self.string(value), + Err(_) => Ok(self.alloc(FakeValue::Bytes(value.to_vec()))), + } + } + /// Casts one fake runtime cell to bytes for nested eval parsing. + pub(super) fn runtime_string_bytes( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus> { + Ok(self.string_bytes_for_value(&self.get(value))) + } + /// Returns PHP-like truthiness for fake runtime cells. + pub(super) fn runtime_truthy(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Null => false, + FakeValue::Bool(value) => value, + FakeValue::Int(value) => value != 0, + FakeValue::Float(value) => value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, + FakeValue::Resource(_) => true, + }) + } +} diff --git a/crates/elephc-eval/src/interpreter/tests/support/conversions.rs b/crates/elephc-eval/src/interpreter/tests/support/conversions.rs new file mode 100644 index 0000000000..2c96563092 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/support/conversions.rs @@ -0,0 +1,155 @@ +//! Purpose: +//! Conversion, comparison, and stringification helpers for fake interpreter values. +//! RuntimeValueOps methods delegate here to keep scalar PHP-like coercion rules +//! out of the trait implementation file. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - Helpers intentionally cover only semantics asserted by eval interpreter tests. + +use super::*; + +impl FakeOps { + /// Compares fake scalar values with the same loose rules covered by eval tests. + pub(super) fn loose_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Bool(left), right) => left == self.fake_truthy(&right), + (left, FakeValue::Bool(right)) => self.fake_truthy(&left) == right, + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Null, FakeValue::String(value)) + | (FakeValue::String(value), FakeValue::Null) => value.is_empty(), + (FakeValue::Null, FakeValue::Bytes(value)) + | (FakeValue::Bytes(value), FakeValue::Null) => value.is_empty(), + (FakeValue::String(left), FakeValue::String(right)) => { + match (left.parse::(), right.parse::()) { + (Ok(left), Ok(right)) => left == right, + _ => left == right, + } + } + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, + (FakeValue::String(left), right) => left + .parse::() + .is_ok_and(|left| left == self.fake_numeric(&right)), + (FakeValue::Bytes(left), right) => std::str::from_utf8(&left) + .ok() + .and_then(|left| left.parse::().ok()) + .is_some_and(|left| left == self.fake_numeric(&right)), + (left, FakeValue::String(right)) => right + .parse::() + .is_ok_and(|right| self.fake_numeric(&left) == right), + (left, FakeValue::Bytes(right)) => std::str::from_utf8(&right) + .ok() + .and_then(|right| right.parse::().ok()) + .is_some_and(|right| self.fake_numeric(&left) == right), + (left, right) => self.fake_numeric(&left) == self.fake_numeric(&right), + } + } + + /// Compares fake scalar values by PHP strict tag and payload equality. + pub(super) fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + match (self.get(left), self.get(right)) { + (FakeValue::Null, FakeValue::Null) => true, + (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, + (FakeValue::Int(left), FakeValue::Int(right)) => left == right, + (FakeValue::Float(left), FakeValue::Float(right)) => left == right, + (FakeValue::String(left), FakeValue::String(right)) => left == right, + (FakeValue::Bytes(left), FakeValue::Bytes(right)) => left == right, + (FakeValue::String(left), FakeValue::Bytes(right)) + | (FakeValue::Bytes(right), FakeValue::String(left)) => left.as_bytes() == right, + (FakeValue::Resource(left), FakeValue::Resource(right)) => left == right, + _ => false, + } + } + + /// Converts one fake scalar cell to a numeric value for comparison tests. + pub(super) fn numeric(&self, handle: RuntimeCellHandle) -> Result { + Ok(self.fake_numeric(&self.get(handle))) + } + + /// Converts a fake value to the numeric scalar used by comparison tests. + pub(super) fn fake_numeric(&self, value: &FakeValue) -> f64 { + match value { + FakeValue::Null => 0.0, + FakeValue::Bool(false) => 0.0, + FakeValue::Bool(true) => 1.0, + FakeValue::Int(value) => *value as f64, + FakeValue::Float(value) => *value, + FakeValue::String(value) => value.parse::().unwrap_or(0.0), + FakeValue::Bytes(value) => std::str::from_utf8(value) + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(0.0), + FakeValue::Array(value) => value.len() as f64, + FakeValue::Assoc(value) => value.len() as f64, + FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, + FakeValue::Resource(value) => (*value + 1) as f64, + } + } + + /// Converts a fake value to the integer scalar used by modulo tests. + pub(super) fn fake_int(&self, value: &FakeValue) -> i64 { + self.fake_numeric(value) as i64 + } + + /// Returns fake PHP truthiness for already-loaded test values. + pub(super) fn fake_truthy(&self, value: &FakeValue) -> bool { + match value { + FakeValue::Null => false, + FakeValue::Bool(value) => *value, + FakeValue::Int(value) => *value != 0, + FakeValue::Float(value) => *value != 0.0, + FakeValue::String(value) => !value.is_empty() && value != "0", + FakeValue::Bytes(value) => !value.is_empty() && value.as_slice() != b"0", + FakeValue::Array(value) => !value.is_empty(), + FakeValue::Assoc(value) => !value.is_empty(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => true, + FakeValue::Resource(_) => true, + } + } + + /// Converts a fake runtime cell to a PHP-like string for test echo/concat. + pub(super) fn stringify(&self, handle: RuntimeCellHandle) -> String { + match self.get(handle) { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value, + FakeValue::Bytes(value) => String::from_utf8_lossy(&value).into_owned(), + FakeValue::Array(_) => "Array".to_string(), + FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + } + } + + /// Converts a fake PHP value to string bytes while preserving binary strings. + pub(super) fn string_bytes_for_value(&self, value: &FakeValue) -> Vec { + match value { + FakeValue::String(value) => value.as_bytes().to_vec(), + FakeValue::Bytes(value) => value.clone(), + value => self.stringify_value(value).into_bytes(), + } + } + + /// Converts one loaded fake PHP value to display text for byte coercions. + pub(super) fn stringify_value(&self, value: &FakeValue) -> String { + match value { + FakeValue::Null => String::new(), + FakeValue::Bool(false) => String::new(), + FakeValue::Bool(true) => "1".to_string(), + FakeValue::Int(value) => value.to_string(), + FakeValue::Float(value) => value.to_string(), + FakeValue::String(value) => value.clone(), + FakeValue::Bytes(value) => String::from_utf8_lossy(value).into_owned(), + FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), + FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), + FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + } + } +} diff --git a/crates/elephc-eval/src/interpreter/tests/support/lifecycle_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/lifecycle_ops.rs new file mode 100644 index 0000000000..db3b6fd135 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/support/lifecycle_ops.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Release, retain, warning, and echo fake runtime operations. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers record observable side effects for assertions without touching real runtime memory. + +use super::*; + +impl FakeOps { + /// Records fake releases without freeing handles needed for assertions. + pub(super) fn runtime_release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.releases.push(value); + Ok(()) + } + /// Returns the same fake handle because fake cells do not refcount. + pub(super) fn runtime_retain( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + Ok(value) + } + /// Records fake PHP warnings without writing to stderr. + pub(super) fn runtime_warning(&mut self, message: &str) -> Result<(), EvalStatus> { + self.warnings.push(message.to_string()); + Ok(()) + } + /// Appends fake echo output for interpreter tests. + pub(super) fn runtime_echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + let value = self.stringify(value); + self.output.push_str(&value); + Ok(()) + } +} diff --git a/crates/elephc-eval/src/interpreter/tests/support/mod.rs b/crates/elephc-eval/src/interpreter/tests/support/mod.rs new file mode 100644 index 0000000000..ae6195e370 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/support/mod.rs @@ -0,0 +1,124 @@ +//! Purpose: +//! Shared fake runtime support for interpreter unit tests. +//! The fixtures allocate opaque runtime cells and implement `RuntimeValueOps` +//! without linking generated runtime hooks. +//! +//! Called from: +//! - `crate::interpreter::tests::*` focused test modules. +//! +//! Key details: +//! - Fake handles are stable integer-backed pointers used only inside tests. +//! - Output, warnings, and releases are recorded for assertions. + +use std::collections::HashMap; +use std::ffi::c_void; + +use crate::value::RuntimeCell; + +use super::super::*; + +mod array_ops; +mod cell_ops; +mod conversions; +mod lifecycle_ops; +mod numeric_ops; +mod object_ops; +mod runtime_ops; + +/// Test-only array key representation for fake indexed and associative arrays. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub(super) enum FakeKey { + Int(i64), + String(String), +} + +/// Test-only runtime value representation used behind opaque cell handles. +#[derive(Clone, Debug, PartialEq)] +pub(super) enum FakeValue { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), + Bytes(Vec), + Array(Vec), + Assoc(Vec<(FakeKey, RuntimeCellHandle)>), + Object(Vec<(String, RuntimeCellHandle)>), + Iterator { len: i64, position: i64 }, + Resource(i64), +} + +/// Test runtime hooks that allocate stable fake handles and record echo output. +#[derive(Default)] +pub(super) struct FakeOps { + pub(super) next_id: usize, + pub(super) values: HashMap, + pub(super) output: String, + pub(super) releases: Vec, + pub(super) warnings: Vec, +} + +impl FakeOps { + /// Allocates one fake runtime cell and returns its opaque handle. + pub(super) fn alloc(&mut self, value: FakeValue) -> RuntimeCellHandle { + self.next_id += 1; + let id = self.next_id; + self.values.insert(id, value); + RuntimeCellHandle::from_raw(id as *mut RuntimeCell) + } + + /// Reads a fake runtime cell by opaque handle. + pub(super) fn get(&self, handle: RuntimeCellHandle) -> FakeValue { + let id = handle.as_ptr() as usize; + self.values.get(&id).cloned().expect("fake cell missing") + } + + /// Converts a fake runtime cell into a normalized fake PHP array key. + pub(super) fn key(&self, handle: RuntimeCellHandle) -> Result { + let value = self.get(handle); + match value { + FakeValue::Int(value) => Ok(FakeKey::Int(value)), + FakeValue::String(value) => eval_numeric_string_array_key(value.as_bytes()) + .map(FakeKey::Int) + .map_or_else(|| Ok(FakeKey::String(value)), Ok), + FakeValue::Bytes(value) => eval_numeric_string_array_key(&value) + .map(FakeKey::Int) + .map_or_else( + || { + Ok(FakeKey::String( + String::from_utf8_lossy(&value).into_owned(), + )) + }, + Ok, + ), + FakeValue::Null => Ok(FakeKey::String(String::new())), + value => Ok(FakeKey::Int(self.fake_int(&value))), + } + } + + /// Allocates a fake runtime cell for an existing PHP array key. + pub(super) fn alloc_key(&mut self, key: &FakeKey) -> Result { + match key { + FakeKey::Int(value) => self.int(*value), + FakeKey::String(value) => self.string(value), + } + } + + /// Finds a fake object property by insertion-order name. + pub(super) fn object_property( + properties: &[(String, RuntimeCellHandle)], + name: &str, + ) -> Option { + properties + .iter() + .find_map(|(property, value)| (property == name).then_some(*value)) + } +} + +/// Test native invoker that returns the descriptor pointer as a runtime cell. +pub(super) unsafe extern "C" fn fake_native_return_descriptor( + descriptor: *mut c_void, + _args: *mut RuntimeCell, +) -> *mut RuntimeCell { + descriptor.cast() +} diff --git a/crates/elephc-eval/src/interpreter/tests/support/numeric_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/numeric_ops.rs new file mode 100644 index 0000000000..18649dad7b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/support/numeric_ops.rs @@ -0,0 +1,296 @@ +//! Purpose: +//! Numeric, bitwise, comparison, concatenation, cast, and string-reversal fake runtime operations. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers implement PHP-like scalar coercions used by expression and builtin tests. + +use super::*; + +impl FakeOps { + /// Casts a fake runtime cell to a fake integer cell. + pub(super) fn runtime_cast_int( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + let value = self.fake_int(&value); + self.int(value) + } + /// Casts a fake runtime cell to a fake float cell. + pub(super) fn runtime_cast_float( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + let value = self.fake_numeric(&value); + self.float(value) + } + /// Casts a fake runtime cell to a fake string cell. + pub(super) fn runtime_cast_string( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.stringify(value); + self.string(&value) + } + /// Casts a fake runtime cell to a fake boolean cell. + pub(super) fn runtime_cast_bool( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + let value = self.fake_truthy(&value); + self.bool_value(value) + } + /// Computes fake PHP absolute value while preserving float payloads. + pub(super) fn runtime_abs( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + match self.get(value) { + FakeValue::Float(value) => self.float(value.abs()), + value => self.int(self.fake_int(&value).wrapping_abs()), + } + } + /// Computes fake PHP ceiling through numeric conversion as a float result. + pub(super) fn runtime_ceil( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).ceil()) + } + /// Computes fake PHP floor through numeric conversion as a float result. + pub(super) fn runtime_floor( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).floor()) + } + /// Computes fake PHP square root through numeric conversion as a float result. + pub(super) fn runtime_sqrt( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.get(value); + self.float(self.fake_numeric(&value).sqrt()) + } + /// Reverses a fake string byte-wise for interpreter tests. + pub(super) fn runtime_strrev( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let mut bytes = self.stringify(value).into_bytes(); + bytes.reverse(); + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + self.string(&value) + } + /// Divides fake numeric cells with PHP `fdiv()` zero handling. + pub(super) fn runtime_fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left / right) + } + /// Computes fake floating-point modulo for interpreter tests. + pub(super) fn runtime_fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left % right) + } + /// Adds fake numeric cells for interpreter tests. + pub(super) fn runtime_add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left + right), + (left, right) => self.float(self.fake_numeric(&left) + self.fake_numeric(&right)), + } + } + /// Subtracts fake numeric cells for interpreter tests. + pub(super) fn runtime_sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left - right), + (left, right) => self.float(self.fake_numeric(&left) - self.fake_numeric(&right)), + } + } + /// Multiplies fake numeric cells for interpreter tests. + pub(super) fn runtime_mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + match (self.get(left), self.get(right)) { + (FakeValue::Int(left), FakeValue::Int(right)) => self.int(left * right), + (left, right) => self.float(self.fake_numeric(&left) * self.fake_numeric(&right)), + } + } + /// Divides fake numeric cells for interpreter tests. + pub(super) fn runtime_div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_numeric(&self.get(right)); + if right == 0.0 { + return Err(EvalStatus::RuntimeFatal); + } + let left = self.fake_numeric(&self.get(left)); + self.float(left / right) + } + /// Computes fake integer modulo for interpreter tests. + pub(super) fn runtime_modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let right = self.fake_int(&self.get(right)); + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let left = self.fake_int(&self.get(left)); + self.int(left % right) + } + /// Raises fake numeric cells for interpreter tests. + pub(super) fn runtime_pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_numeric(&self.get(left)); + let right = self.fake_numeric(&self.get(right)); + self.float(left.powf(right)) + } + /// Rounds fake numeric cells with PHP's optional decimal precision. + pub(super) fn runtime_round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + let value = self.fake_numeric(&self.get(value)); + let precision = precision + .map(|precision| self.fake_int(&self.get(precision))) + .unwrap_or(0); + let multiplier = 10_f64.powf(precision as f64); + self.float((value * multiplier).round() / multiplier) + } + /// Applies fake integer bitwise and shift operations for interpreter tests. + pub(super) fn runtime_bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.fake_int(&self.get(left)); + let right = self.fake_int(&self.get(right)); + let value = match op { + EvalBinOp::BitAnd => left & right, + EvalBinOp::BitOr => left | right, + EvalBinOp::BitXor => left ^ right, + EvalBinOp::ShiftLeft => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); + } + left.wrapping_shl(right as u32) + } + EvalBinOp::ShiftRight => { + if right < 0 { + return Err(EvalStatus::RuntimeFatal); + } + left.wrapping_shr(right as u32) + } + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + self.int(value) + } + /// Applies fake integer bitwise NOT for interpreter tests. + pub(super) fn runtime_bit_not( + &mut self, + value: RuntimeCellHandle, + ) -> Result { + let value = self.fake_int(&self.get(value)); + self.int(!value) + } + /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. + pub(super) fn runtime_concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let mut left = self.string_bytes_for_value(&self.get(left)); + let right = self.string_bytes_for_value(&self.get(right)); + left.extend_from_slice(&right); + self.string_bytes_value(&left) + } + /// Compares fake scalar cells and returns a fake PHP boolean. + pub(super) fn runtime_compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let result = match op { + EvalBinOp::LooseEq => self.loose_eq(left, right), + EvalBinOp::LooseNotEq => !self.loose_eq(left, right), + EvalBinOp::StrictEq => self.strict_eq(left, right), + EvalBinOp::StrictNotEq => !self.strict_eq(left, right), + EvalBinOp::Lt => self.numeric(left)? < self.numeric(right)?, + EvalBinOp::LtEq => self.numeric(left)? <= self.numeric(right)?, + EvalBinOp::Gt => self.numeric(left)? > self.numeric(right)?, + EvalBinOp::GtEq => self.numeric(left)? >= self.numeric(right)?, + EvalBinOp::Add + | EvalBinOp::Sub + | EvalBinOp::Mul + | EvalBinOp::Div + | EvalBinOp::Mod + | EvalBinOp::Pow + | EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight + | EvalBinOp::Concat + | EvalBinOp::Spaceship + | EvalBinOp::LogicalAnd + | EvalBinOp::LogicalOr + | EvalBinOp::LogicalXor => { + return Err(EvalStatus::UnsupportedConstruct); + } + }; + self.bool_value(result) + } + /// Compares fake numeric cells and returns a PHP spaceship integer. + pub(super) fn runtime_spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + let left = self.numeric(left)?; + let right = self.numeric(right)?; + let value = if left < right { + -1 + } else if left > right { + 1 + } else { + 0 + }; + self.int(value) + } +} diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs new file mode 100644 index 0000000000..8f69b489a0 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -0,0 +1,236 @@ +//! Purpose: +//! Object, method, class-metadata, and identity fake runtime operations for interpreter tests. +//! +//! Called from: +//! - `crate::interpreter::tests::support::runtime_ops`. +//! +//! Key details: +//! - These helpers model only the object and class behavior needed by eval tests. + +use super::*; + +impl FakeOps { + /// Reads one fake object property by name. + pub(super) fn runtime_property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => properties + .iter() + .find_map(|(name, value)| (name == property).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Writes one fake object property by name. + pub(super) fn runtime_property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some((_, existing_value)) = properties.iter_mut().find(|(name, _)| name == property) + { + *existing_value = value; + } else { + properties.push((property.to_string(), value)); + } + Ok(()) + } + /// Returns the number of fake object properties in insertion order. + pub(super) fn runtime_object_property_len( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => Ok(properties.len()), + FakeValue::Iterator { .. } => Ok(0), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns one fake object property key by insertion-order position. + pub(super) fn runtime_object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + let Some((name, _)) = properties.get(position) else { + return self.null(); + }; + self.string(name) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Calls one fake object method by name. + pub(super) fn runtime_method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + match (self.get(object), method) { + (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position = 0; + self.null() + } + (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { + self.bool_value(position < len) + } + (FakeValue::Iterator { .. }, "next") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position += 1; + self.null() + } + (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), + (FakeValue::Object(properties), "read_x") => { + if !args.is_empty() { + return Err(EvalStatus::UnsupportedConstruct); + } + Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "add_x") => { + let [arg] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(arg) = self.get(*arg) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + arg) + } + (FakeValue::Object(properties), "add2_x") => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + left + right) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Creates one fake object for eval `new` unit tests. + pub(super) fn runtime_new_object( + &mut self, + _class_name: &str, + ) -> Result { + Ok(self.alloc(FakeValue::Object(Vec::new()))) + } + /// Applies fake constructor side effects for eval `new` unit tests. + pub(super) fn runtime_construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some(first) = args.first().copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { + *value = first; + } else { + properties.push(("x".to_string(), first)); + } + } + Ok(()) + } + /// Reports one fake AOT class for eval `class_exists` unit tests. + pub(super) fn runtime_class_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownClass")) + } + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownInterface")) + } + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + pub(super) fn runtime_trait_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownTrait")) + } + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + pub(super) fn runtime_enum_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownEnum")) + } + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + pub(super) fn runtime_object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) + if target_class.eq_ignore_ascii_case("Exception") + || target_class.eq_ignore_ascii_case("Throwable") => + { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), + _ => Ok(false), + } + } + /// Returns a fake PHP class name for object-tagged test values. + pub(super) fn runtime_object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(_) => self.string("stdClass"), + FakeValue::Iterator { .. } => self.string("Iterator"), + _ => Err(EvalStatus::RuntimeFatal), + } + } + /// Returns fake parent-class names for eval introspection unit tests. + pub(super) fn runtime_parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) => self.string("ParentClass"), + FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { + self.string("ParentClass") + } + _ => self.string(""), + } + } + /// Returns the fake object handle as a stable object identity. + pub(super) fn runtime_object_identity( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), + _ => Err(EvalStatus::RuntimeFatal), + } + } +} diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs new file mode 100644 index 0000000000..e472fd4609 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -0,0 +1,360 @@ +//! Purpose: +//! RuntimeValueOps implementation for interpreter test fake values. +//! This keeps the large trait surface separate from test fixture type +//! declarations and assertion-only conversion helpers. +//! +//! Called from: +//! - `crate::interpreter::tests::support::FakeOps` through trait dispatch. +//! +//! Key details: +//! - Methods intentionally model only the runtime behavior covered by eval tests. +//! - Handles are fake stable cells and must not be freed by this implementation. + +use super::*; + +impl RuntimeValueOps for FakeOps { + /// Creates a fake indexed array cell. + fn array_new(&mut self, capacity: usize) -> Result { + self.runtime_array_new(capacity) + } + /// Creates a fake associative array cell. + fn assoc_new(&mut self, _capacity: usize) -> Result { + self.runtime_assoc_new(_capacity) + } + /// Reads one fake indexed array element. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + self.runtime_array_get(array, index) + } + /// Checks whether a fake array has the requested key without reading its value. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + self.runtime_array_key_exists(key, array) + } + /// Returns one fake foreach key by insertion-order position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + self.runtime_array_iter_key(array, position) + } + /// Writes one fake indexed or associative array element. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + self.runtime_array_set(array, index, value) + } + /// Reads one fake object property by name. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + self.runtime_property_get(object, property) + } + /// Writes one fake object property by name. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + self.runtime_property_set(object, property, value) + } + /// Returns the number of fake object properties in insertion order. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { + self.runtime_object_property_len(object) + } + /// Returns one fake object property key by insertion-order position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + self.runtime_object_property_iter_key(object, position) + } + /// Calls one fake object method by name. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + self.runtime_method_call(object, method, args) + } + /// Creates one fake object for eval `new` unit tests. + fn new_object(&mut self, _class_name: &str) -> Result { + self.runtime_new_object(_class_name) + } + /// Applies fake constructor side effects for eval `new` unit tests. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + self.runtime_construct_object(object, args) + } + /// Reports one fake AOT class for eval `class_exists` unit tests. + fn class_exists(&mut self, name: &str) -> Result { + self.runtime_class_exists(name) + } + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + fn interface_exists(&mut self, name: &str) -> Result { + self.runtime_interface_exists(name) + } + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + fn trait_exists(&mut self, name: &str) -> Result { + self.runtime_trait_exists(name) + } + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + fn enum_exists(&mut self, name: &str) -> Result { + self.runtime_enum_exists(name) + } + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + self.runtime_object_is_a(object_or_class, target_class, exclude_self) + } + /// Returns a fake PHP class name for object-tagged test values. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + self.runtime_object_class_name(object) + } + /// Returns fake parent-class names for eval introspection unit tests. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + self.runtime_parent_class_name(object_or_class) + } + /// Returns the visible element count for fake array values. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + self.runtime_array_len(array) + } + /// Returns whether a fake runtime cell is an indexed or associative array. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_is_array_like(value) + } + /// Returns whether a fake runtime cell is null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_is_null(value) + } + /// Returns the fake runtime tag corresponding to a test value. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_type_tag(value) + } + /// Returns the fake object handle as a stable object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + self.runtime_object_identity(object) + } + /// Records fake releases without freeing handles needed for assertions. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.runtime_release(value) + } + /// Returns the same fake handle because fake cells do not refcount. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_retain(value) + } + /// Records fake PHP warnings without writing to stderr. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + self.runtime_warning(message) + } + /// Creates a fake null cell. + fn null(&mut self) -> Result { + self.runtime_null() + } + /// Creates a fake bool cell. + fn bool_value(&mut self, value: bool) -> Result { + self.runtime_bool_value(value) + } + /// Creates a fake int cell. + fn int(&mut self, value: i64) -> Result { + self.runtime_int(value) + } + /// Creates a fake float cell. + fn float(&mut self, value: f64) -> Result { + self.runtime_float(value) + } + /// Creates a fake string cell. + fn string(&mut self, value: &str) -> Result { + self.runtime_string(value) + } + /// Creates a fake string cell from raw PHP bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + self.runtime_string_bytes_value(value) + } + /// Casts a fake runtime cell to a fake integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_int(value) + } + /// Casts a fake runtime cell to a fake float cell. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_float(value) + } + /// Casts a fake runtime cell to a fake string cell. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_string(value) + } + /// Casts a fake runtime cell to a fake boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_bool(value) + } + /// Computes fake PHP absolute value while preserving float payloads. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_abs(value) + } + /// Computes fake PHP ceiling through numeric conversion as a float result. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_ceil(value) + } + /// Computes fake PHP floor through numeric conversion as a float result. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_floor(value) + } + /// Computes fake PHP square root through numeric conversion as a float result. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_sqrt(value) + } + /// Reverses a fake string byte-wise for interpreter tests. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_strrev(value) + } + /// Divides fake numeric cells with PHP `fdiv()` zero handling. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_fdiv(left, right) + } + /// Computes fake floating-point modulo for interpreter tests. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_fmod(left, right) + } + /// Adds fake numeric cells for interpreter tests. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_add(left, right) + } + /// Subtracts fake numeric cells for interpreter tests. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_sub(left, right) + } + /// Multiplies fake numeric cells for interpreter tests. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_mul(left, right) + } + /// Divides fake numeric cells for interpreter tests. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_div(left, right) + } + /// Computes fake integer modulo for interpreter tests. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_modulo(left, right) + } + /// Raises fake numeric cells for interpreter tests. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_pow(left, right) + } + /// Rounds fake numeric cells with PHP's optional decimal precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + self.runtime_round(value, precision) + } + /// Applies fake integer bitwise and shift operations for interpreter tests. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_bitwise(op, left, right) + } + /// Applies fake integer bitwise NOT for interpreter tests. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_bit_not(value) + } + /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_concat(left, right) + } + /// Compares fake scalar cells and returns a fake PHP boolean. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_compare(op, left, right) + } + /// Compares fake numeric cells and returns a PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_spaceship(left, right) + } + /// Appends fake echo output for interpreter tests. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.runtime_echo(value) + } + /// Casts one fake runtime cell to bytes for nested eval parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + self.runtime_string_bytes(value) + } + /// Returns PHP-like truthiness for fake runtime cells. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_truthy(value) + } +} diff --git a/crates/elephc-eval/src/parser/tests.rs b/crates/elephc-eval/src/parser/tests.rs deleted file mode 100644 index f87c088a0a..0000000000 --- a/crates/elephc-eval/src/parser/tests.rs +++ /dev/null @@ -1,1803 +0,0 @@ -//! Purpose: -//! Parser unit tests for runtime PHP eval fragments. -//! The cases assert that supported syntax lowers into EvalIR and unsupported -//! syntax reports stable parse statuses. -//! -//! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. -//! -//! Key details: -//! - Fixtures intentionally use eval fragments without PHP opening tags. -//! - Expected values compare against EvalIR so grammar regressions are visible. - -use super::cursor::inc_dec_store; -use super::parse_fragment; -use crate::errors::EvalParseError; -use crate::eval_ir::*; - -/// Verifies assignment fragments lower to by-name StoreVar statements. -#[test] -fn parse_fragment_accepts_assignment_source() { - let program = parse_fragment(b"$x = 1;").expect("fragment should parse"); - assert_eq!(program.source_len(), 7); - assert_eq!( - program.statements(), - &[EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::Int(1)), - }] - ); -} - -/// Verifies reference assignments lower to by-name ReferenceAssign statements. -#[test] -fn parse_fragment_accepts_reference_assignment_source() { - let program = parse_fragment(b"$left =& $right;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ReferenceAssign { - target: "left".to_string(), - source: "right".to_string(), - }] - ); -} - -/// Verifies multiplicative operators preserve PHP precedence and associativity. -#[test] -fn parse_fragment_accepts_division_and_modulo_source() { - let program = parse_fragment(b"return 10 / 4 % 3;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Mod, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Div, - left: Box::new(EvalExpr::Const(EvalConst::Int(10))), - right: Box::new(EvalExpr::Const(EvalConst::Int(4))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }))] - ); -} - -/// Verifies exponentiation is right-associative and binds tighter than unary negation. -#[test] -fn parse_fragment_accepts_power_source() { - let program = - parse_fragment(b"return -2 ** 2; return 2 ** 3 ** 2;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::Return(Some(EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr: Box::new(EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(EvalExpr::Const(EvalConst::Int(2))), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }), - })), - EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(EvalExpr::Const(EvalConst::Int(2))), - right: Box::new(EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(EvalExpr::Const(EvalConst::Int(3))), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }), - })), - ] - ); -} - -/// Verifies bitwise operators preserve PHP precedence. -#[test] -fn parse_fragment_accepts_bitwise_source() { - let program = parse_fragment(b"return ~0 | 2 ^ 3 & 4;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::BitOr, - left: Box::new(EvalExpr::Unary { - op: EvalUnaryOp::BitNot, - expr: Box::new(EvalExpr::Const(EvalConst::Int(0))), - }), - right: Box::new(EvalExpr::Binary { - op: EvalBinOp::BitXor, - left: Box::new(EvalExpr::Const(EvalConst::Int(2))), - right: Box::new(EvalExpr::Binary { - op: EvalBinOp::BitAnd, - left: Box::new(EvalExpr::Const(EvalConst::Int(3))), - right: Box::new(EvalExpr::Const(EvalConst::Int(4))), - }), - }), - }))] - ); -} - -/// Verifies shift operators bind lower than additive expressions. -#[test] -fn parse_fragment_accepts_shift_source() { - let program = parse_fragment(b"return 1 + 2 << 3;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::ShiftLeft, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::Const(EvalConst::Int(1))), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }))] - ); -} - -/// Verifies simple variable compound assignments lower to StoreVar with binary expressions. -#[test] -fn parse_fragment_accepts_compound_assignment_source() { - let program = parse_fragment(br#"$x += 2; $x -= 1; $x *= 3; $x /= 2; $x %= 5; $s .= "ok";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Sub, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Mul, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Div, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Mod, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(5))), - }, - }, - EvalStmt::StoreVar { - name: "s".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::LoadVar("s".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::String("ok".to_string()))), - }, - }, - ] - ); -} - -/// Verifies exponentiation compound assignment lowers through the binary power operator. -#[test] -fn parse_fragment_accepts_power_compound_assignment_source() { - let program = parse_fragment(br#"$x **= 3;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }, - }] - ); -} - -/// Verifies bitwise compound assignments lower to StoreVar with binary expressions. -#[test] -fn parse_fragment_accepts_bitwise_compound_assignment_source() { - let program = parse_fragment(br#"$x &= 3; $x |= 1; $x ^= 2; $x <<= 4; $x >>= 1;"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::BitAnd, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::BitOr, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::BitXor, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::ShiftLeft, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(4))), - }, - }, - EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::ShiftRight, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }, - ] - ); -} - -/// Verifies simple variable increment and decrement statements lower to StoreVar. -#[test] -fn parse_fragment_accepts_inc_dec_statement_source() { - let program = parse_fragment(br#"$i++; ++$j; $k--; --$m;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - inc_dec_store("i".to_string(), true), - inc_dec_store("j".to_string(), true), - inc_dec_store("k".to_string(), false), - inc_dec_store("m".to_string(), false), - ] - ); -} - -/// Verifies echo fragments preserve expression source order. -#[test] -fn parse_fragment_accepts_echo_source() { - let program = parse_fragment(br#"echo "hi" . $name;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Echo(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Const(EvalConst::String("hi".to_string()))), - right: Box::new(EvalExpr::LoadVar("name".to_string())), - })] - ); -} - -/// Verifies PHP echo comma lists lower to one EvalIR echo statement per expression. -#[test] -fn parse_fragment_accepts_echo_comma_list_source() { - let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::Echo(EvalExpr::Const(EvalConst::String("a".to_string()))), - EvalStmt::Echo(EvalExpr::LoadVar("b".to_string())), - EvalStmt::Echo(EvalExpr::Const(EvalConst::String("c".to_string()))), - ] - ); -} - -/// Verifies if/else fragments lower to branch statements with nested blocks. -#[test] -fn parse_fragment_accepts_if_else_source() { - let program = parse_fragment(br#"if ($flag) { $x = "yes"; } else { $x = "no"; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::If { - condition: EvalExpr::LoadVar("flag".to_string()), - then_branch: vec![EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::String("yes".to_string())), - }], - else_branch: vec![EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::String("no".to_string())), - }], - }] - ); -} - -/// Verifies braceless if/else bodies parse as single-statement branch bodies. -#[test] -fn parse_fragment_accepts_braceless_if_else_source() { - let program = parse_fragment(br#"if ($flag) echo "yes"; else echo "no";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::If { - condition: EvalExpr::LoadVar("flag".to_string()), - then_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "yes".to_string() - )))], - else_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "no".to_string() - )))], - }] - ); -} - -/// Verifies elseif fragments lower to nested if statements in the else branch. -#[test] -fn parse_fragment_accepts_elseif_source() { - let program = parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::If { - condition: EvalExpr::LoadVar("a".to_string()), - then_branch: vec![EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::String("a".to_string())), - }], - else_branch: vec![EvalStmt::If { - condition: EvalExpr::LoadVar("b".to_string()), - then_branch: vec![EvalStmt::StoreVar { - name: "x".to_string(), - value: EvalExpr::Const(EvalConst::String("b".to_string())), - }], - else_branch: Vec::new(), - }], - }] - ); -} - -/// Verifies PHP's `else if` spelling follows the same nested branch shape. -#[test] -fn parse_fragment_accepts_else_if_source() { - let program = parse_fragment(br#"if ($a) { $x = "a"; } else if ($b) { $x = "b"; }"#) - .expect("fragment should parse"); - - assert!(matches!( - program.statements(), - [EvalStmt::If { - else_branch, - .. - }] if matches!(else_branch.as_slice(), [EvalStmt::If { .. }]) - )); -} - -/// Verifies for loops lower clauses and body statements separately. -#[test] -fn parse_fragment_accepts_for_source() { - let program = parse_fragment(br#"for ($i = 2; $i; $i = $i - 1) { echo $i; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::For { - init: vec![EvalStmt::StoreVar { - name: "i".to_string(), - value: EvalExpr::Const(EvalConst::Int(2)), - }], - condition: Some(EvalExpr::LoadVar("i".to_string())), - update: vec![EvalStmt::StoreVar { - name: "i".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Sub, - left: Box::new(EvalExpr::LoadVar("i".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }], - body: vec![EvalStmt::Echo(EvalExpr::LoadVar("i".to_string()))], - }] - ); -} - -/// Verifies switch fragments preserve ordered case and default bodies. -#[test] -fn parse_fragment_accepts_switch_source() { - let program = - parse_fragment(br#"switch ($x) { case 1: echo "one"; break; default: echo "other"; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Switch { - expr: EvalExpr::LoadVar("x".to_string()), - cases: vec![ - EvalSwitchCase { - condition: Some(EvalExpr::Const(EvalConst::Int(1))), - body: vec![ - EvalStmt::Echo(EvalExpr::Const(EvalConst::String("one".to_string()))), - EvalStmt::Break, - ], - }, - EvalSwitchCase { - condition: None, - body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "other".to_string() - )))], - }, - ], - }] - ); -} - -/// Verifies value-only foreach loops lower to an array expression, value target, and body. -#[test] -fn parse_fragment_accepts_foreach_source() { - let program = parse_fragment(br#"foreach ($items as $item) { echo $item; }"#).expect("parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Foreach { - array: EvalExpr::LoadVar("items".to_string()), - key_name: None, - value_name: "item".to_string(), - body: vec![EvalStmt::Echo(EvalExpr::LoadVar("item".to_string()))], - }] - ); -} - -/// Verifies key-value foreach loops preserve both loop target names in EvalIR. -#[test] -fn parse_fragment_accepts_foreach_key_value_source() { - let program = parse_fragment(br#"foreach ($items as $key => $item) { echo $key . $item; }"#) - .expect("parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Foreach { - array: EvalExpr::LoadVar("items".to_string()), - key_name: Some("key".to_string()), - value_name: "item".to_string(), - body: vec![EvalStmt::Echo(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::LoadVar("key".to_string())), - right: Box::new(EvalExpr::LoadVar("item".to_string())), - })], - }] - ); -} - -/// Verifies dynamic function declarations preserve name, parameters, and body. -#[test] -fn parse_fragment_accepts_function_declaration_source() { - let program = - parse_fragment(br#"function dyn($x) { return $x + 1; }"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::FunctionDecl { - name: "dyn".to_string(), - params: vec!["x".to_string()], - body: vec![EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }))], - }] - ); -} - -/// Verifies semicolon namespace declarations qualify functions and unqualified calls. -#[test] -fn parse_fragment_accepts_semicolon_namespace_source() { - let program = parse_fragment( - br#"namespace Eval\Ns; -function dyn() { return __NAMESPACE__; } -return dyn();"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::FunctionDecl { - name: "Eval\\Ns\\dyn".to_string(), - params: Vec::new(), - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( - "Eval\\Ns".to_string() - ))))], - }, - EvalStmt::Return(Some(EvalExpr::NamespacedCall { - name: "eval\\ns\\dyn".to_string(), - fallback_name: "dyn".to_string(), - args: Vec::new(), - })), - ] - ); -} - -/// Verifies braced namespace declarations restore the previous namespace afterward. -#[test] -fn parse_fragment_accepts_braced_namespace_source() { - let program = parse_fragment( - br#"namespace Eval\Block { - class Box {} - return new Box(); -} -return Box;"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::ClassDecl(EvalClass::new("Eval\\Block\\Box", Vec::new(), Vec::new())), - EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "Eval\\Block\\Box".to_string(), - args: Vec::new(), - })), - EvalStmt::Return(Some(EvalExpr::ConstFetch("Box".to_string()))), - ] - ); -} - -/// Verifies namespace import declarations resolve functions, constants, and class aliases. -#[test] -fn parse_fragment_accepts_namespace_use_imports() { - let program = parse_fragment( - br#"namespace Eval\UseNs; -use function Lib\strlen as Alias; -use const Lib\VALUE as LocalValue; -use Lib\Box as BoxAlias; -return Alias(LocalValue, new BoxAlias\Inner());"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\strlen".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\VALUE".to_string())), - EvalCallArg::positional(EvalExpr::NewObject { - class_name: "Lib\\Box\\Inner".to_string(), - args: Vec::new(), - }), - ], - }))] - ); -} - -/// Verifies grouped namespace imports resolve functions, constants, and class aliases. -#[test] -fn parse_fragment_accepts_grouped_namespace_use_imports() { - let program = parse_fragment( - br#"namespace Eval\UseNs; -use Lib\{Box as BoxAlias, Sub\Thing, function imported_func as Alias}; -use const Lib\{VALUE as LocalValue, OTHER}; -return Alias(LocalValue, OTHER, new BoxAlias\Inner(), new Thing());"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\imported_func".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\VALUE".to_string())), - EvalCallArg::positional(EvalExpr::ConstFetch("Lib\\OTHER".to_string())), - EvalCallArg::positional(EvalExpr::NewObject { - class_name: "Lib\\Box\\Inner".to_string(), - args: Vec::new(), - }), - EvalCallArg::positional(EvalExpr::NewObject { - class_name: "Lib\\Sub\\Thing".to_string(), - args: Vec::new(), - }), - ], - }))] - ); -} - -/// Verifies typed grouped namespace imports reject mixed per-entry kinds. -#[test] -fn parse_fragment_rejects_mixed_kind_typed_grouped_use_imports() { - assert_eq!( - parse_fragment(br#"use function Lib\{target, const VALUE};"#), - Err(EvalParseError::UnexpectedToken) - ); -} - -/// Verifies namespace blocks restore imports when control returns to the outer namespace. -#[test] -fn parse_fragment_restores_use_imports_after_namespace_block() { - let program = parse_fragment( - br#"namespace Eval\Outer; -use function Lib\outer_func; -namespace Eval\Block { - use function Lib\inner_func as alias; - return alias(); -} -return outer_func();"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\inner_func".to_string(), - args: Vec::new(), - })), - EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\outer_func".to_string(), - args: Vec::new(), - })), - ] - ); -} - -/// Verifies imported aliases remain visible while parsing eval-declared function bodies. -#[test] -fn parse_fragment_applies_use_imports_inside_function_body() { - let program = parse_fragment( - br#"namespace Eval\UseNs; -use function Lib\target as alias; -function dyn() { return alias(); }"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::FunctionDecl { - name: "Eval\\UseNs\\dyn".to_string(), - params: Vec::new(), - body: vec![EvalStmt::Return(Some(EvalExpr::Call { - name: "lib\\target".to_string(), - args: Vec::new(), - }))], - }] - ); -} - -/// Verifies import declarations are rejected inside eval-declared function bodies. -#[test] -fn parse_fragment_rejects_use_import_inside_function_body() { - assert_eq!( - parse_fragment(br#"function dyn() { use function Lib\target; }"#), - Err(EvalParseError::UnsupportedConstruct) - ); -} - -/// Verifies static local declarations preserve the target name and initializer expression. -#[test] -fn parse_fragment_accepts_static_var_source() { - let program = parse_fragment(br#"static $n = 1 + 1;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::StaticVar { - name: "n".to_string(), - init: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::Const(EvalConst::Int(1))), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }] - ); -} - -/// Verifies global declarations preserve source-order variable names. -#[test] -fn parse_fragment_accepts_global_source() { - let program = parse_fragment(br#"global $left, $right;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Global { - vars: vec!["left".to_string(), "right".to_string()], - }] - ); -} - -/// Verifies eval magic constants lower to explicit EvalIR nodes with fragment line metadata. -#[test] -fn parse_fragment_accepts_magic_constants() { - let program = - parse_fragment(b"\nreturn __line__ . __FUNCTION__;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Magic(EvalMagicConst::Line(2))), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Function)), - }))] - ); -} - -/// Verifies file-dependent eval magic constants lower to EvalIR nodes. -#[test] -fn parse_fragment_accepts_file_magic_constants() { - let program = parse_fragment(b"return __FILE__ . __dir__;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Magic(EvalMagicConst::File)), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Dir)), - }))] - ); -} - -/// Verifies eval scope magic constants lower with namespace resolved at parse time. -#[test] -fn parse_fragment_accepts_scope_magic_constants() { - let program = parse_fragment(b"return __CLASS__ . __NAMESPACE__ . __TRAIT__ . __METHOD__;") - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Magic(EvalMagicConst::Class)), - right: Box::new(EvalExpr::Const(EvalConst::String(String::new()))), - }), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Trait)), - }), - right: Box::new(EvalExpr::Magic(EvalMagicConst::Method)), - }))] - ); -} - -/// Verifies PHP comments are skipped while preserving fragment line numbers. -#[test] -fn parse_fragment_skips_comments_and_preserves_line_metadata() { - let program = parse_fragment(b"// leading\n# hash\n/* block\ncomment */ return __LINE__;") - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Magic( - EvalMagicConst::Line(4) - )))] - ); -} - -/// Verifies unterminated block comments fail before partial EvalIR is returned. -#[test] -fn parse_fragment_rejects_unterminated_block_comment() { - assert_eq!( - parse_fragment(b"/* open").unwrap_err(), - EvalParseError::UnterminatedComment - ); -} - -/// Verifies comparison operators parse with lower precedence than arithmetic. -#[test] -fn parse_fragment_accepts_comparison_source() { - let program = parse_fragment(br#"return $i + 1 < 3;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Lt, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("i".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }))] - ); -} - -/// Verifies the spaceship operator parses at ordered-comparison precedence. -#[test] -fn parse_fragment_accepts_spaceship_source() { - let program = parse_fragment(br#"return $i + 1 <=> 3;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Spaceship, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("i".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }))] - ); -} - -/// Verifies loose equality operators parse as binary EvalIR expressions. -#[test] -fn parse_fragment_accepts_loose_equality_source() { - let program = parse_fragment(br#"return "a" != "b";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LooseNotEq, - left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), - right: Box::new(EvalExpr::Const(EvalConst::String("b".to_string()))), - }))] - ); -} - -/// Verifies strict equality operators parse as distinct EvalIR comparisons. -#[test] -fn parse_fragment_accepts_strict_equality_source() { - let program = - parse_fragment(br#"return "10" === "10" && "10" !== 10;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::StrictEq, - left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), - right: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), - }), - right: Box::new(EvalExpr::Binary { - op: EvalBinOp::StrictNotEq, - left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), - right: Box::new(EvalExpr::Const(EvalConst::Int(10))), - }), - }))] - ); -} - -/// Verifies logical operators parse with `&&` binding tighter than `||`. -#[test] -fn parse_fragment_accepts_short_circuit_logical_source() { - let program = parse_fragment(br#"return $a && $b || false;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(EvalExpr::LoadVar("a".to_string())), - right: Box::new(EvalExpr::LoadVar("b".to_string())), - }), - right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - }))] - ); -} - -/// Verifies PHP logical keywords parse case-insensitively with their own precedence. -#[test] -fn parse_fragment_accepts_keyword_logical_source() { - let program = - parse_fragment(br#"return false || true AnD false;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - }))] - ); -} - -/// Verifies PHP `xor` binds between `or` and `and` in eval expressions. -#[test] -fn parse_fragment_accepts_keyword_xor_source() { - let program = - parse_fragment(br#"return true XoR false or false;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::LogicalXor, - left: Box::new(EvalExpr::Const(EvalConst::Bool(true))), - right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), - }))] - ); -} - -/// Verifies ternary expressions parse below logical OR and preserve both branches. -#[test] -fn parse_fragment_accepts_ternary_source() { - let program = - parse_fragment(br#"return $a || $b ? "yes" : "no";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Ternary { - condition: Box::new(EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(EvalExpr::LoadVar("a".to_string())), - right: Box::new(EvalExpr::LoadVar("b".to_string())), - }), - then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( - "yes".to_string() - )))), - else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), - }))] - ); -} - -/// Verifies PHP's short ternary form omits the explicit then branch in EvalIR. -#[test] -fn parse_fragment_accepts_short_ternary_source() { - let program = parse_fragment(br#"return $name ?: "fallback";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Ternary { - condition: Box::new(EvalExpr::LoadVar("name".to_string())), - then_branch: None, - else_branch: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), - }))] - ); -} - -/// Verifies null coalescing parses as a right-associative expression. -#[test] -fn parse_fragment_accepts_null_coalesce_source() { - let program = - parse_fragment(br#"return $a ?? $b ?? "fallback";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NullCoalesce { - value: Box::new(EvalExpr::LoadVar("a".to_string())), - default: Box::new(EvalExpr::NullCoalesce { - value: Box::new(EvalExpr::LoadVar("b".to_string())), - default: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), - }), - }))] - ); -} - -/// Verifies match expressions preserve subject, patterns, and default expression. -#[test] -fn parse_fragment_accepts_match_source() { - let program = parse_fragment(br#"return match ($x) { 1, 2 => "small", default => "other" };"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Match { - subject: Box::new(EvalExpr::LoadVar("x".to_string())), - arms: vec![EvalMatchArm { - patterns: vec![ - EvalExpr::Const(EvalConst::Int(1)), - EvalExpr::Const(EvalConst::Int(2)), - ], - value: EvalExpr::Const(EvalConst::String("small".to_string())), - }], - default: Some(Box::new(EvalExpr::Const(EvalConst::String( - "other".to_string() - )))), - }))] - ); -} - -/// Verifies null coalescing binds tighter than PHP ternary expressions. -#[test] -fn parse_fragment_null_coalesce_binds_tighter_than_ternary() { - let program = - parse_fragment(br#"return $a ?? $b ? "yes" : "no";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Ternary { - condition: Box::new(EvalExpr::NullCoalesce { - value: Box::new(EvalExpr::LoadVar("a".to_string())), - default: Box::new(EvalExpr::LoadVar("b".to_string())), - }), - then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( - "yes".to_string() - )))), - else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), - }))] - ); -} - -/// Verifies logical negation parses as a unary expression before comparisons. -#[test] -fn parse_fragment_accepts_logical_not_source() { - let program = parse_fragment(br#"return !$flag == true;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::LooseEq, - left: Box::new(EvalExpr::Unary { - op: EvalUnaryOp::LogicalNot, - expr: Box::new(EvalExpr::LoadVar("flag".to_string())), - }), - right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), - }))] - ); -} - -/// Verifies unary numeric operators bind tighter than multiplication. -#[test] -fn parse_fragment_accepts_unary_numeric_source() { - let program = parse_fragment(br#"return -$x * +2;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Mul, - left: Box::new(EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr: Box::new(EvalExpr::LoadVar("x".to_string())), - }), - right: Box::new(EvalExpr::Unary { - op: EvalUnaryOp::Plus, - expr: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }), - }))] - ); -} - -/// Verifies print fragments lower to expression-form print with the printed value. -#[test] -fn parse_fragment_accepts_print_source() { - let program = parse_fragment(br#"print "hi";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Expr(EvalExpr::Print(Box::new(EvalExpr::Const( - EvalConst::String("hi".to_string()) - ))))] - ); -} - -/// Verifies single- and double-quoted strings keep PHP-compatible simple escapes. -#[test] -fn parse_fragment_preserves_php_string_escape_semantics() { - let program = parse_fragment(br#"return ['A\nB', "A\qB", "A\v\e\fB", 'It\'s'];"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("A\\nB".to_string()))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("A\\qB".to_string()))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( - "A\x0b\x1b\x0cB".to_string() - ))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("It's".to_string()))), - ])))] - ); -} - -/// Verifies call expressions preserve their callee name and source-order arguments. -#[test] -fn parse_fragment_accepts_call_expression_source() { - let program = parse_fragment(br#"return eval("return 1;");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "eval".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "return 1;".to_string() - )))], - }))] - ); -} - -/// Verifies include and require constructs parse as expressions with path metadata. -#[test] -fn parse_fragment_accepts_include_require_expression_source() { - let program = parse_fragment(br#"return include "a" . ".php"; require_once("b.php");"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::Return(Some(EvalExpr::Include { - path: Box::new(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), - right: Box::new(EvalExpr::Const(EvalConst::String(".php".to_string()))), - }), - required: false, - once: false, - })), - EvalStmt::Expr(EvalExpr::Include { - path: Box::new(EvalExpr::Const(EvalConst::String("b.php".to_string()))), - required: true, - once: true, - }), - ] - ); -} - -/// Verifies explicitly qualified call expressions normalize away the leading slash. -#[test] -fn parse_fragment_accepts_qualified_call_expression_source() { - let program = parse_fragment(br#"return \strlen("abcd");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "strlen".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "abcd".to_string() - )))], - }))] - ); -} - -/// Verifies variable callable expressions lower to dynamic calls with source-order args. -#[test] -fn parse_fragment_accepts_dynamic_call_expression_source() { - let program = - parse_fragment(br#"return $fn(first: "a", ...$rest);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicCall { - callee: Box::new(EvalExpr::LoadVar("fn".to_string())), - args: vec![ - EvalCallArg::named("first", EvalExpr::Const(EvalConst::String("a".to_string())),), - EvalCallArg::spread(EvalExpr::LoadVar("rest".to_string())), - ], - }))] - ); -} - -/// Verifies dynamic calls can be applied after another postfix expression. -#[test] -fn parse_fragment_accepts_postfix_dynamic_call_source() { - let program = - parse_fragment(br#"return $callbacks[0]("abcd");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicCall { - callee: Box::new(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::LoadVar("callbacks".to_string())), - index: Box::new(EvalExpr::Const(EvalConst::Int(0))), - }), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "abcd".to_string() - )))], - }))] - ); -} - -/// Verifies bare constant names lower to dynamic constant-fetch expressions. -#[test] -fn parse_fragment_accepts_constant_fetch_source() { - let program = parse_fragment(br#"return \Dyn\EvalConst;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::ConstFetch( - "Dyn\\EvalConst".to_string() - )))] - ); -} - -/// Verifies function calls preserve named arguments in source order. -#[test] -fn parse_fragment_accepts_named_call_argument_source() { - let program = parse_fragment(br#"return add(y: 2, x: 1);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "add".to_string(), - args: vec![ - EvalCallArg::named("y", EvalExpr::Const(EvalConst::Int(2))), - EvalCallArg::named("x", EvalExpr::Const(EvalConst::Int(1))), - ], - }))] - ); -} - -/// Verifies function calls preserve spread arguments in source order. -#[test] -fn parse_fragment_accepts_spread_call_argument_source() { - let program = parse_fragment(br#"return add(...$args);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "add".to_string(), - args: vec![EvalCallArg::spread(EvalExpr::LoadVar("args".to_string()))], - }))] - ); -} - -/// Verifies `isset` parses as a case-insensitive function-like expression. -#[test] -fn parse_fragment_accepts_isset_source() { - let program = - parse_fragment(br#"return ISSET($x, $items["k"]);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "isset".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), - EvalCallArg::positional(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::LoadVar("items".to_string())), - index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), - }), - ], - }))] - ); -} - -/// Verifies `empty` parses as a case-insensitive function-like expression. -#[test] -fn parse_fragment_accepts_empty_source() { - let program = parse_fragment(br#"return EMPTY($items["k"]);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Call { - name: "empty".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::LoadVar("items".to_string())), - index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), - })], - }))] - ); -} - -/// Verifies indexed array literals and reads parse as runtime array expressions. -#[test] -fn parse_fragment_accepts_indexed_array_read_source() { - let program = parse_fragment(br#"return [1, 2][0];"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(2))), - ])), - index: Box::new(EvalExpr::Const(EvalConst::Int(0))), - }))] - ); -} - -/// Verifies legacy `array(...)` literals parse through the same EvalIR array node. -#[test] -fn parse_fragment_accepts_legacy_array_literal_source() { - let program = - parse_fragment(br#"return array(1, "name" => "Ada",)[1];"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), - EvalArrayElement::KeyValue { - key: EvalExpr::Const(EvalConst::String("name".to_string())), - value: EvalExpr::Const(EvalConst::String("Ada".to_string())), - }, - ])), - index: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }))] - ); -} - -/// Verifies associative array literals preserve explicit key/value expressions. -#[test] -fn parse_fragment_accepts_assoc_array_literal_source() { - let program = parse_fragment(br#"return ["name" => "Ada"];"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::KeyValue { - key: EvalExpr::Const(EvalConst::String("name".to_string())), - value: EvalExpr::Const(EvalConst::String("Ada".to_string())), - } - ])))] - ); -} - -/// Verifies indexed array writes parse as variable-target array set statements. -#[test] -fn parse_fragment_accepts_indexed_array_write_source() { - let program = parse_fragment(br#"$items[1] = "x";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ArraySetVar { - name: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(1)), - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }] - ); -} - -/// Verifies indexed array append syntax parses as a variable-target append statement. -#[test] -fn parse_fragment_accepts_indexed_array_append_source() { - let program = parse_fragment(br#"$items[] = "x";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ArrayAppendVar { - name: "items".to_string(), - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }] - ); -} - -/// Verifies array append syntax is accepted inside `for` update clauses. -#[test] -fn parse_fragment_accepts_array_append_in_for_update_source() { - let program = parse_fragment(br#"for ($i = 0; $i < 2; $items[] = $i) { $i += 1; }"#) - .expect("fragment should parse"); - let [EvalStmt::For { update, .. }] = program.statements() else { - panic!("expected for statement"); - }; - assert_eq!( - update, - &vec![EvalStmt::ArrayAppendVar { - name: "items".to_string(), - value: EvalExpr::LoadVar("i".to_string()), - }] - ); -} - -/// Verifies object property reads parse as postfix EvalIR expressions. -#[test] -fn parse_fragment_accepts_property_read_source() { - let program = parse_fragment(br#"return $this->x;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }))] - ); -} - -/// Verifies property names preserve source case while keywords remain case-insensitive. -#[test] -fn parse_fragment_preserves_property_case_source() { - let program = parse_fragment(br#"RETURN $this->camelName;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "camelName".to_string(), - }))] - ); -} - -/// Verifies object method calls parse as postfix EvalIR call expressions. -#[test] -fn parse_fragment_accepts_method_call_source() { - let program = parse_fragment(br#"return $this->Answer();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "answer".to_string(), - args: Vec::new(), - }))] - ); -} - -/// Verifies object construction parses as a named EvalIR expression. -#[test] -fn parse_fragment_accepts_new_object_source() { - let program = parse_fragment(br#"return new Box();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "Box".to_string(), - args: Vec::new(), - }))] - ); -} - -/// Verifies object construction accepts explicitly qualified class names. -#[test] -fn parse_fragment_accepts_qualified_new_object_source() { - let program = parse_fragment(br#"return new \EvalNs\Box();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "EvalNs\\Box".to_string(), - args: Vec::new(), - }))] - ); -} - -/// Verifies object method calls preserve source-order argument expressions. -#[test] -fn parse_fragment_accepts_method_call_args_source() { - let program = parse_fragment(br#"return $this->add($x + 1);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "add".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - })], - }))] - ); -} - -/// Verifies object method calls parse multiple argument expressions in source order. -#[test] -fn parse_fragment_accepts_method_call_multiple_args_source() { - let program = - parse_fragment(br#"return $this->label($x, "ok");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "label".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), - EvalCallArg::positional(EvalExpr::Const(EvalConst::String("ok".to_string()))), - ], - }))] - ); -} - -/// Verifies object property writes parse as dedicated EvalIR statements. -#[test] -fn parse_fragment_accepts_property_write_source() { - let program = parse_fragment(br#"$this->x = $this->x + 1;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }] - ); -} - -/// Verifies while fragments lower to loop statements with a nested block. -#[test] -fn parse_fragment_accepts_while_source() { - let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::While { - condition: EvalExpr::LoadVar("flag".to_string()), - body: vec![ - EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), - EvalStmt::StoreVar { - name: "flag".to_string(), - value: EvalExpr::Const(EvalConst::Bool(false)), - }, - ], - }] - ); -} - -/// Verifies do/while fragments lower to body-first loop statements. -#[test] -fn parse_fragment_accepts_do_while_source() { - let program = parse_fragment(br#"do { echo $flag; $flag = false; } while ($flag);"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::DoWhile { - body: vec![ - EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), - EvalStmt::StoreVar { - name: "flag".to_string(), - value: EvalExpr::Const(EvalConst::Bool(false)), - }, - ], - condition: EvalExpr::LoadVar("flag".to_string()), - }] - ); -} - -/// Verifies loop control statements parse inside while blocks. -#[test] -fn parse_fragment_accepts_break_and_continue_source() { - let program = - parse_fragment(br#"while ($flag) { continue; break; }"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::While { - condition: EvalExpr::LoadVar("flag".to_string()), - body: vec![EvalStmt::Continue, EvalStmt::Break], - }] - ); -} - -/// Verifies return fragments parse optional return expressions. -#[test] -fn parse_fragment_accepts_return_source() { - let program = parse_fragment(b"return ($x - 1) * 4;").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Mul, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Sub, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(4))), - }))] - ); -} - -/// Verifies throw statements lower to a Throwable expression carried by EvalIR. -#[test] -fn parse_fragment_accepts_throw_source() { - let program = - parse_fragment(br#"throw new Exception("eval boom");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })] - ); -} - -/// Verifies try/catch statements lower supported Throwable clauses into EvalIR. -#[test] -fn parse_fragment_accepts_try_catch_throwable_source() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable $caught) { - return 1; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })], - catches: vec![EvalCatch { - class_names: vec!["Throwable".to_string()], - var_name: Some("caught".to_string()), - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - }], - finally_body: Vec::new(), - }] - ); -} - -/// Verifies class imports can alias the supported Throwable catch type. -#[test] -fn parse_fragment_accepts_try_catch_imported_throwable_alias() { - let program = parse_fragment( - br#"use Throwable as T; -try { - throw $e; -} catch (T $caught) { - echo "caught"; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::LoadVar("e".to_string()))], - catches: vec![EvalCatch { - class_names: vec!["Throwable".to_string()], - var_name: Some("caught".to_string()), - body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "caught".to_string() - )))], - }], - finally_body: Vec::new(), - }] - ); -} - -/// Verifies Throwable catch clauses can omit the catch variable like PHP. -#[test] -fn parse_fragment_accepts_try_catch_without_variable() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable) { - return 1; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })], - catches: vec![EvalCatch { - class_names: vec!["Throwable".to_string()], - var_name: None, - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - }], - finally_body: Vec::new(), - }] - ); -} - -/// Verifies single catch type narrowing lowers into EvalIR. -#[test] -fn parse_fragment_accepts_specific_eval_catch_type() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Exception $caught) { - return 1; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })], - catches: vec![EvalCatch { - class_names: vec!["Exception".to_string()], - var_name: Some("caught".to_string()), - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - }], - finally_body: Vec::new(), - }] - ); -} - -/// Verifies union catch type narrowing lowers all source-order types into one clause. -#[test] -fn parse_fragment_accepts_union_eval_catch_type() { - let program = parse_fragment( - br#"try { - throw new Exception("eval boom"); -} catch (Throwable|Exception $caught) { - return 1; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Throw(EvalExpr::NewObject { - class_name: "Exception".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "eval boom".to_string() - )))], - })], - catches: vec![EvalCatch { - class_names: vec!["Throwable".to_string(), "Exception".to_string()], - var_name: Some("caught".to_string()), - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - }], - finally_body: Vec::new(), - }] - ); -} - -/// Verifies try/finally statements lower the finalizer block into EvalIR. -#[test] -fn parse_fragment_accepts_eval_finally_source() { - let program = parse_fragment(br#"try { return 1; } finally { echo "finally"; }"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Try { - body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], - catches: Vec::new(), - finally_body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( - "finally".to_string() - )))], - }] - ); -} - -/// Verifies unset fragments expand to one by-name unset statement per variable. -#[test] -fn parse_fragment_accepts_unset_source() { - let program = parse_fragment(b"unset($x, $y);").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::UnsetVar { - name: "x".to_string() - }, - EvalStmt::UnsetVar { - name: "y".to_string() - }, - ] - ); -} - -/// Verifies eval fragments reject PHP opening tags. -#[test] -fn parse_fragment_rejects_opening_tag() { - assert_eq!( - parse_fragment(b"x; } }", - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalSupported", - vec![EvalClassProperty::new( - "x", - Some(EvalExpr::Const(EvalConst::Int(1))) - )], - vec![EvalClassMethod::new( - "read", - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }))] - )] - ))] - ); -} - -/// Verifies malformed object construction reports an unexpected token. -#[test] -fn parse_fragment_rejects_new_without_class_name() { - assert_eq!( - parse_fragment(b"return new ();"), - Err(EvalParseError::UnexpectedToken) - ); -} - -/// Verifies unsupported expression keywords report the unsupported construct status. -#[test] -fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { - for source in [ - b"return clone $value;" as &[u8], - b"return yield 1;" as &[u8], - ] { - assert_eq!( - parse_fragment(source), - Err(EvalParseError::UnsupportedConstruct) - ); - } -} - -/// Verifies malformed statements report parse errors instead of partial IR. -#[test] -fn parse_fragment_rejects_missing_semicolon() { - assert_eq!( - parse_fragment(b"$x = 1"), - Err(EvalParseError::ExpectedSemicolon) - ); -} diff --git a/crates/elephc-eval/src/parser/tests/arrays_objects.rs b/crates/elephc-eval/src/parser/tests/arrays_objects.rs new file mode 100644 index 0000000000..7a47613981 --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/arrays_objects.rs @@ -0,0 +1,215 @@ +//! Purpose: +//! Parser tests for arrays, array writes, object properties, methods, and object construction. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover postfix and aggregate expression syntax. + +use super::support::*; + +/// Verifies indexed array literals and reads parse as runtime array expressions. +#[test] +fn parse_fragment_accepts_indexed_array_read_source() { + let program = parse_fragment(br#"return [1, 2][0];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(2))), + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }))] + ); +} +/// Verifies legacy `array(...)` literals parse through the same EvalIR array node. +#[test] +fn parse_fragment_accepts_legacy_array_literal_source() { + let program = + parse_fragment(br#"return array(1, "name" => "Ada",)[1];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + }, + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }))] + ); +} +/// Verifies associative array literals preserve explicit key/value expressions. +#[test] +fn parse_fragment_accepts_assoc_array_literal_source() { + let program = parse_fragment(br#"return ["name" => "Ada"];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + } + ])))] + ); +} +/// Verifies indexed array writes parse as variable-target array set statements. +#[test] +fn parse_fragment_accepts_indexed_array_write_source() { + let program = parse_fragment(br#"$items[1] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArraySetVar { + name: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(1)), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} +/// Verifies indexed array append syntax parses as a variable-target append statement. +#[test] +fn parse_fragment_accepts_indexed_array_append_source() { + let program = parse_fragment(br#"$items[] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} +/// Verifies array append syntax is accepted inside `for` update clauses. +#[test] +fn parse_fragment_accepts_array_append_in_for_update_source() { + let program = parse_fragment(br#"for ($i = 0; $i < 2; $items[] = $i) { $i += 1; }"#) + .expect("fragment should parse"); + let [EvalStmt::For { update, .. }] = program.statements() else { + panic!("expected for statement"); + }; + assert_eq!( + update, + &vec![EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::LoadVar("i".to_string()), + }] + ); +} +/// Verifies object property reads parse as postfix EvalIR expressions. +#[test] +fn parse_fragment_accepts_property_read_source() { + let program = parse_fragment(br#"return $this->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + ); +} +/// Verifies property names preserve source case while keywords remain case-insensitive. +#[test] +fn parse_fragment_preserves_property_case_source() { + let program = parse_fragment(br#"RETURN $this->camelName;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "camelName".to_string(), + }))] + ); +} +/// Verifies object method calls parse as postfix EvalIR call expressions. +#[test] +fn parse_fragment_accepts_method_call_source() { + let program = parse_fragment(br#"return $this->Answer();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "answer".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies object construction parses as a named EvalIR expression. +#[test] +fn parse_fragment_accepts_new_object_source() { + let program = parse_fragment(br#"return new Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Box".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies object construction accepts explicitly qualified class names. +#[test] +fn parse_fragment_accepts_qualified_new_object_source() { + let program = parse_fragment(br#"return new \EvalNs\Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "EvalNs\\Box".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies object method calls preserve source-order argument expressions. +#[test] +fn parse_fragment_accepts_method_call_args_source() { + let program = parse_fragment(br#"return $this->add($x + 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "add".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + })], + }))] + ); +} +/// Verifies object method calls parse multiple argument expressions in source order. +#[test] +fn parse_fragment_accepts_method_call_multiple_args_source() { + let program = + parse_fragment(br#"return $this->label($x, "ok");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "label".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::Const(EvalConst::String("ok".to_string()))), + ], + }))] + ); +} +/// Verifies object property writes parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_write_source() { + let program = parse_fragment(br#"$this->x = $this->x + 1;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }] + ); +} diff --git a/crates/elephc-eval/src/parser/tests/assignments.rs b/crates/elephc-eval/src/parser/tests/assignments.rs new file mode 100644 index 0000000000..f77972a95f --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/assignments.rs @@ -0,0 +1,288 @@ +//! Purpose: +//! Parser tests for assignment, compound assignment, increment/decrement, and echo statements. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases assert direct statement lowering into EvalIR stores and echoes. + +use super::support::*; + +/// Verifies assignment fragments lower to by-name StoreVar statements. +#[test] +fn parse_fragment_accepts_assignment_source() { + let program = parse_fragment(b"$x = 1;").expect("fragment should parse"); + assert_eq!(program.source_len(), 7); + assert_eq!( + program.statements(), + &[EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::Int(1)), + }] + ); +} +/// Verifies reference assignments lower to by-name ReferenceAssign statements. +#[test] +fn parse_fragment_accepts_reference_assignment_source() { + let program = parse_fragment(b"$left =& $right;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ReferenceAssign { + target: "left".to_string(), + source: "right".to_string(), + }] + ); +} +/// Verifies multiplicative operators preserve PHP precedence and associativity. +#[test] +fn parse_fragment_accepts_division_and_modulo_source() { + let program = parse_fragment(b"return 10 / 4 % 3;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mod, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Div, + left: Box::new(EvalExpr::Const(EvalConst::Int(10))), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} +/// Verifies exponentiation is right-associative and binds tighter than unary negation. +#[test] +fn parse_fragment_accepts_power_source() { + let program = + parse_fragment(b"return -2 ** 2; return 2 ** 3 ** 2;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + })), + EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::Const(EvalConst::Int(3))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + })), + ] + ); +} +/// Verifies bitwise operators preserve PHP precedence. +#[test] +fn parse_fragment_accepts_bitwise_source() { + let program = parse_fragment(b"return ~0 | 2 ^ 3 & 4;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(EvalExpr::Const(EvalConst::Int(2))), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(EvalExpr::Const(EvalConst::Int(3))), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }), + }), + }))] + ); +} +/// Verifies shift operators bind lower than additive expressions. +#[test] +fn parse_fragment_accepts_shift_source() { + let program = parse_fragment(b"return 1 + 2 << 3;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::ShiftLeft, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::Const(EvalConst::Int(1))), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} +/// Verifies simple variable compound assignments lower to StoreVar with binary expressions. +#[test] +fn parse_fragment_accepts_compound_assignment_source() { + let program = parse_fragment(br#"$x += 2; $x -= 1; $x *= 3; $x /= 2; $x %= 5; $s .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Div, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Mod, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(5))), + }, + }, + EvalStmt::StoreVar { + name: "s".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("s".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::String("ok".to_string()))), + }, + }, + ] + ); +} +/// Verifies exponentiation compound assignment lowers through the binary power operator. +#[test] +fn parse_fragment_accepts_power_compound_assignment_source() { + let program = parse_fragment(br#"$x **= 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }] + ); +} +/// Verifies bitwise compound assignments lower to StoreVar with binary expressions. +#[test] +fn parse_fragment_accepts_bitwise_compound_assignment_source() { + let program = parse_fragment(br#"$x &= 3; $x |= 1; $x ^= 2; $x <<= 4; $x >>= 1;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::ShiftLeft, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }, + }, + EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::ShiftRight, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + ] + ); +} +/// Verifies simple variable increment and decrement statements lower to StoreVar. +#[test] +fn parse_fragment_accepts_inc_dec_statement_source() { + let program = parse_fragment(br#"$i++; ++$j; $k--; --$m;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + inc_dec_store("i".to_string(), true), + inc_dec_store("j".to_string(), true), + inc_dec_store("k".to_string(), false), + inc_dec_store("m".to_string(), false), + ] + ); +} +/// Verifies echo fragments preserve expression source order. +#[test] +fn parse_fragment_accepts_echo_source() { + let program = parse_fragment(br#"echo "hi" . $name;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Echo(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Const(EvalConst::String("hi".to_string()))), + right: Box::new(EvalExpr::LoadVar("name".to_string())), + })] + ); +} +/// Verifies PHP echo comma lists lower to one EvalIR echo statement per expression. +#[test] +fn parse_fragment_accepts_echo_comma_list_source() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("a".to_string()))), + EvalStmt::Echo(EvalExpr::LoadVar("b".to_string())), + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("c".to_string()))), + ] + ); +} diff --git a/crates/elephc-eval/src/parser/tests/calls.rs b/crates/elephc-eval/src/parser/tests/calls.rs new file mode 100644 index 0000000000..6dfc863bfe --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/calls.rs @@ -0,0 +1,198 @@ +//! Purpose: +//! Parser tests for print, strings, calls, includes, constants, named args, spread args, isset, and empty. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover function-like and call-expression parsing. + +use super::support::*; + +/// Verifies print fragments lower to expression-form print with the printed value. +#[test] +fn parse_fragment_accepts_print_source() { + let program = parse_fragment(br#"print "hi";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Expr(EvalExpr::Print(Box::new(EvalExpr::Const( + EvalConst::String("hi".to_string()) + ))))] + ); +} +/// Verifies single- and double-quoted strings keep PHP-compatible simple escapes. +#[test] +fn parse_fragment_preserves_php_string_escape_semantics() { + let program = parse_fragment(br#"return ['A\nB', "A\qB", "A\v\e\fB", 'It\'s'];"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("A\\nB".to_string()))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("A\\qB".to_string()))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String( + "A\x0b\x1b\x0cB".to_string() + ))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("It's".to_string()))), + ])))] + ); +} +/// Verifies call expressions preserve their callee name and source-order arguments. +#[test] +fn parse_fragment_accepts_call_expression_source() { + let program = parse_fragment(br#"return eval("return 1;");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "eval".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "return 1;".to_string() + )))], + }))] + ); +} +/// Verifies include and require constructs parse as expressions with path metadata. +#[test] +fn parse_fragment_accepts_include_require_expression_source() { + let program = parse_fragment(br#"return include "a" . ".php"; require_once("b.php");"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::Return(Some(EvalExpr::Include { + path: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String(".php".to_string()))), + }), + required: false, + once: false, + })), + EvalStmt::Expr(EvalExpr::Include { + path: Box::new(EvalExpr::Const(EvalConst::String("b.php".to_string()))), + required: true, + once: true, + }), + ] + ); +} +/// Verifies explicitly qualified call expressions normalize away the leading slash. +#[test] +fn parse_fragment_accepts_qualified_call_expression_source() { + let program = parse_fragment(br#"return \strlen("abcd");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "strlen".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "abcd".to_string() + )))], + }))] + ); +} +/// Verifies variable callable expressions lower to dynamic calls with source-order args. +#[test] +fn parse_fragment_accepts_dynamic_call_expression_source() { + let program = + parse_fragment(br#"return $fn(first: "a", ...$rest);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicCall { + callee: Box::new(EvalExpr::LoadVar("fn".to_string())), + args: vec![ + EvalCallArg::named("first", EvalExpr::Const(EvalConst::String("a".to_string())),), + EvalCallArg::spread(EvalExpr::LoadVar("rest".to_string())), + ], + }))] + ); +} +/// Verifies dynamic calls can be applied after another postfix expression. +#[test] +fn parse_fragment_accepts_postfix_dynamic_call_source() { + let program = + parse_fragment(br#"return $callbacks[0]("abcd");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicCall { + callee: Box::new(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("callbacks".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "abcd".to_string() + )))], + }))] + ); +} +/// Verifies bare constant names lower to dynamic constant-fetch expressions. +#[test] +fn parse_fragment_accepts_constant_fetch_source() { + let program = parse_fragment(br#"return \Dyn\EvalConst;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ConstFetch( + "Dyn\\EvalConst".to_string() + )))] + ); +} +/// Verifies function calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_call_argument_source() { + let program = parse_fragment(br#"return add(y: 2, x: 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "add".to_string(), + args: vec![ + EvalCallArg::named("y", EvalExpr::Const(EvalConst::Int(2))), + EvalCallArg::named("x", EvalExpr::Const(EvalConst::Int(1))), + ], + }))] + ); +} +/// Verifies function calls preserve spread arguments in source order. +#[test] +fn parse_fragment_accepts_spread_call_argument_source() { + let program = parse_fragment(br#"return add(...$args);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "add".to_string(), + args: vec![EvalCallArg::spread(EvalExpr::LoadVar("args".to_string()))], + }))] + ); +} +/// Verifies `isset` parses as a case-insensitive function-like expression. +#[test] +fn parse_fragment_accepts_isset_source() { + let program = + parse_fragment(br#"return ISSET($x, $items["k"]);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "isset".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("items".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), + }), + ], + }))] + ); +} +/// Verifies `empty` parses as a case-insensitive function-like expression. +#[test] +fn parse_fragment_accepts_empty_source() { + let program = parse_fragment(br#"return EMPTY($items["k"]);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Call { + name: "empty".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("items".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::String("k".to_string()))), + })], + }))] + ); +} diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs new file mode 100644 index 0000000000..33731ca76c --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -0,0 +1,79 @@ +//! Purpose: +//! Parser tests for class declarations and parser diagnostics. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover dynamic class metadata and malformed fragment errors. + +use super::support::*; + +/// Verifies empty class declarations lower to dynamic class-registration statements. +#[test] +fn parse_fragment_accepts_empty_class_declaration_source() { + let program = parse_fragment(b"class DynEvalClass {};").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalClass", + Vec::new(), + Vec::new() + ))] + ); +} +/// Verifies public property and method class members lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_public_class_members() { + let program = parse_fragment( + b"class DynEvalSupported { public int $x = 1; public function read() { return $this->x; } }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalSupported", + vec![EvalClassProperty::new( + "x", + Some(EvalExpr::Const(EvalConst::Int(1))) + )], + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + )] + ))] + ); +} +/// Verifies malformed object construction reports an unexpected token. +#[test] +fn parse_fragment_rejects_new_without_class_name() { + assert_eq!( + parse_fragment(b"return new ();"), + Err(EvalParseError::UnexpectedToken) + ); +} +/// Verifies unsupported expression keywords report the unsupported construct status. +#[test] +fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { + for source in [ + b"return clone $value;" as &[u8], + b"return yield 1;" as &[u8], + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} +/// Verifies malformed statements report parse errors instead of partial IR. +#[test] +fn parse_fragment_rejects_missing_semicolon() { + assert_eq!( + parse_fragment(b"$x = 1"), + Err(EvalParseError::ExpectedSemicolon) + ); +} diff --git a/crates/elephc-eval/src/parser/tests/control_statements.rs b/crates/elephc-eval/src/parser/tests/control_statements.rs new file mode 100644 index 0000000000..d319f4ba85 --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/control_statements.rs @@ -0,0 +1,173 @@ +//! Purpose: +//! Parser tests for branch, loop, switch, foreach, and function declaration statements. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases verify statement body shapes and ordered EvalIR blocks. + +use super::support::*; + +/// Verifies if/else fragments lower to branch statements with nested blocks. +#[test] +fn parse_fragment_accepts_if_else_source() { + let program = parse_fragment(br#"if ($flag) { $x = "yes"; } else { $x = "no"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("flag".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("yes".to_string())), + }], + else_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("no".to_string())), + }], + }] + ); +} +/// Verifies braceless if/else bodies parse as single-statement branch bodies. +#[test] +fn parse_fragment_accepts_braceless_if_else_source() { + let program = parse_fragment(br#"if ($flag) echo "yes"; else echo "no";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("flag".to_string()), + then_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))], + else_branch: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "no".to_string() + )))], + }] + ); +} +/// Verifies elseif fragments lower to nested if statements in the else branch. +#[test] +fn parse_fragment_accepts_elseif_source() { + let program = parse_fragment(br#"if ($a) { $x = "a"; } elseif ($b) { $x = "b"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::If { + condition: EvalExpr::LoadVar("a".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("a".to_string())), + }], + else_branch: vec![EvalStmt::If { + condition: EvalExpr::LoadVar("b".to_string()), + then_branch: vec![EvalStmt::StoreVar { + name: "x".to_string(), + value: EvalExpr::Const(EvalConst::String("b".to_string())), + }], + else_branch: Vec::new(), + }], + }] + ); +} +/// Verifies PHP's `else if` spelling follows the same nested branch shape. +#[test] +fn parse_fragment_accepts_else_if_source() { + let program = parse_fragment(br#"if ($a) { $x = "a"; } else if ($b) { $x = "b"; }"#) + .expect("fragment should parse"); + + assert!(matches!( + program.statements(), + [EvalStmt::If { + else_branch, + .. + }] if matches!(else_branch.as_slice(), [EvalStmt::If { .. }]) + )); +} +/// Verifies for loops lower clauses and body statements separately. +#[test] +fn parse_fragment_accepts_for_source() { + let program = parse_fragment(br#"for ($i = 2; $i; $i = $i - 1) { echo $i; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::For { + init: vec![EvalStmt::StoreVar { + name: "i".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }], + condition: Some(EvalExpr::LoadVar("i".to_string())), + update: vec![EvalStmt::StoreVar { + name: "i".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }], + body: vec![EvalStmt::Echo(EvalExpr::LoadVar("i".to_string()))], + }] + ); +} +/// Verifies switch fragments preserve ordered case and default bodies. +#[test] +fn parse_fragment_accepts_switch_source() { + let program = + parse_fragment(br#"switch ($x) { case 1: echo "one"; break; default: echo "other"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Switch { + expr: EvalExpr::LoadVar("x".to_string()), + cases: vec![ + EvalSwitchCase { + condition: Some(EvalExpr::Const(EvalConst::Int(1))), + body: vec![ + EvalStmt::Echo(EvalExpr::Const(EvalConst::String("one".to_string()))), + EvalStmt::Break, + ], + }, + EvalSwitchCase { + condition: None, + body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "other".to_string() + )))], + }, + ], + }] + ); +} +/// Verifies value-only foreach loops lower to an array expression, value target, and body. +#[test] +fn parse_fragment_accepts_foreach_source() { + let program = parse_fragment(br#"foreach ($items as $item) { echo $item; }"#).expect("parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Foreach { + array: EvalExpr::LoadVar("items".to_string()), + key_name: None, + value_name: "item".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::LoadVar("item".to_string()))], + }] + ); +} +/// Verifies key-value foreach loops preserve both loop target names in EvalIR. +#[test] +fn parse_fragment_accepts_foreach_key_value_source() { + let program = parse_fragment(br#"foreach ($items as $key => $item) { echo $key . $item; }"#) + .expect("parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Foreach { + array: EvalExpr::LoadVar("items".to_string()), + key_name: Some("key".to_string()), + value_name: "item".to_string(), + body: vec![EvalStmt::Echo(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("key".to_string())), + right: Box::new(EvalExpr::LoadVar("item".to_string())), + })], + }] + ); +} diff --git a/crates/elephc-eval/src/parser/tests/exceptions_control.rs b/crates/elephc-eval/src/parser/tests/exceptions_control.rs new file mode 100644 index 0000000000..ad9e364220 --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/exceptions_control.rs @@ -0,0 +1,277 @@ +//! Purpose: +//! Parser tests for while, do-while, break, continue, return, throw, try/catch/finally, and unset. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover control-transfer and exception statement parsing. + +use super::support::*; + +/// Verifies while fragments lower to loop statements with a nested block. +#[test] +fn parse_fragment_accepts_while_source() { + let program = parse_fragment(br#"while ($flag) { echo $flag; $flag = false; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::While { + condition: EvalExpr::LoadVar("flag".to_string()), + body: vec![ + EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), + EvalStmt::StoreVar { + name: "flag".to_string(), + value: EvalExpr::Const(EvalConst::Bool(false)), + }, + ], + }] + ); +} +/// Verifies do/while fragments lower to body-first loop statements. +#[test] +fn parse_fragment_accepts_do_while_source() { + let program = parse_fragment(br#"do { echo $flag; $flag = false; } while ($flag);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DoWhile { + body: vec![ + EvalStmt::Echo(EvalExpr::LoadVar("flag".to_string())), + EvalStmt::StoreVar { + name: "flag".to_string(), + value: EvalExpr::Const(EvalConst::Bool(false)), + }, + ], + condition: EvalExpr::LoadVar("flag".to_string()), + }] + ); +} +/// Verifies loop control statements parse inside while blocks. +#[test] +fn parse_fragment_accepts_break_and_continue_source() { + let program = + parse_fragment(br#"while ($flag) { continue; break; }"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::While { + condition: EvalExpr::LoadVar("flag".to_string()), + body: vec![EvalStmt::Continue, EvalStmt::Break], + }] + ); +} +/// Verifies return fragments parse optional return expressions. +#[test] +fn parse_fragment_accepts_return_source() { + let program = parse_fragment(b"return ($x - 1) * 4;").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(4))), + }))] + ); +} +/// Verifies throw statements lower to a Throwable expression carried by EvalIR. +#[test] +fn parse_fragment_accepts_throw_source() { + let program = + parse_fragment(br#"throw new Exception("eval boom");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })] + ); +} +/// Verifies try/catch statements lower supported Throwable clauses into EvalIR. +#[test] +fn parse_fragment_accepts_try_catch_throwable_source() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies class imports can alias the supported Throwable catch type. +#[test] +fn parse_fragment_accepts_try_catch_imported_throwable_alias() { + let program = parse_fragment( + br#"use Throwable as T; +try { + throw $e; +} catch (T $caught) { + echo "caught"; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::LoadVar("e".to_string()))], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "caught".to_string() + )))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies Throwable catch clauses can omit the catch variable like PHP. +#[test] +fn parse_fragment_accepts_try_catch_without_variable() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string()], + var_name: None, + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies single catch type narrowing lowers into EvalIR. +#[test] +fn parse_fragment_accepts_specific_eval_catch_type() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Exception $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Exception".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies union catch type narrowing lowers all source-order types into one clause. +#[test] +fn parse_fragment_accepts_union_eval_catch_type() { + let program = parse_fragment( + br#"try { + throw new Exception("eval boom"); +} catch (Throwable|Exception $caught) { + return 1; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Throw(EvalExpr::NewObject { + class_name: "Exception".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "eval boom".to_string() + )))], + })], + catches: vec![EvalCatch { + class_names: vec!["Throwable".to_string(), "Exception".to_string()], + var_name: Some("caught".to_string()), + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + }], + finally_body: Vec::new(), + }] + ); +} +/// Verifies try/finally statements lower the finalizer block into EvalIR. +#[test] +fn parse_fragment_accepts_eval_finally_source() { + let program = parse_fragment(br#"try { return 1; } finally { echo "finally"; }"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Try { + body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))], + catches: Vec::new(), + finally_body: vec![EvalStmt::Echo(EvalExpr::Const(EvalConst::String( + "finally".to_string() + )))], + }] + ); +} +/// Verifies unset fragments expand to one by-name unset statement per variable. +#[test] +fn parse_fragment_accepts_unset_source() { + let program = parse_fragment(b"unset($x, $y);").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetVar { + name: "x".to_string() + }, + EvalStmt::UnsetVar { + name: "y".to_string() + }, + ] + ); +} +/// Verifies eval fragments reject PHP opening tags. +#[test] +fn parse_fragment_rejects_opening_tag() { + assert_eq!( + parse_fragment(b" 3;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Spaceship, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("i".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }))] + ); +} +/// Verifies loose equality operators parse as binary EvalIR expressions. +#[test] +fn parse_fragment_accepts_loose_equality_source() { + let program = parse_fragment(br#"return "a" != "b";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LooseNotEq, + left: Box::new(EvalExpr::Const(EvalConst::String("a".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String("b".to_string()))), + }))] + ); +} +/// Verifies strict equality operators parse as distinct EvalIR comparisons. +#[test] +fn parse_fragment_accepts_strict_equality_source() { + let program = + parse_fragment(br#"return "10" === "10" && "10" !== 10;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::StrictEq, + left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + }), + right: Box::new(EvalExpr::Binary { + op: EvalBinOp::StrictNotEq, + left: Box::new(EvalExpr::Const(EvalConst::String("10".to_string()))), + right: Box::new(EvalExpr::Const(EvalConst::Int(10))), + }), + }))] + ); +} +/// Verifies logical operators parse with `&&` binding tighter than `||`. +#[test] +fn parse_fragment_accepts_short_circuit_logical_source() { + let program = parse_fragment(br#"return $a && $b || false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::LoadVar("a".to_string())), + right: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} +/// Verifies PHP logical keywords parse case-insensitively with their own precedence. +#[test] +fn parse_fragment_accepts_keyword_logical_source() { + let program = + parse_fragment(br#"return false || true AnD false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} +/// Verifies PHP `xor` binds between `or` and `and` in eval expressions. +#[test] +fn parse_fragment_accepts_keyword_xor_source() { + let program = + parse_fragment(br#"return true XoR false or false;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(false))), + }))] + ); +} +/// Verifies ternary expressions parse below logical OR and preserve both branches. +#[test] +fn parse_fragment_accepts_ternary_source() { + let program = + parse_fragment(br#"return $a || $b ? "yes" : "no";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(EvalExpr::LoadVar("a".to_string())), + right: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))), + else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), + }))] + ); +} +/// Verifies PHP's short ternary form omits the explicit then branch in EvalIR. +#[test] +fn parse_fragment_accepts_short_ternary_source() { + let program = parse_fragment(br#"return $name ?: "fallback";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::LoadVar("name".to_string())), + then_branch: None, + else_branch: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), + }))] + ); +} +/// Verifies null coalescing parses as a right-associative expression. +#[test] +fn parse_fragment_accepts_null_coalesce_source() { + let program = + parse_fragment(br#"return $a ?? $b ?? "fallback";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("a".to_string())), + default: Box::new(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("b".to_string())), + default: Box::new(EvalExpr::Const(EvalConst::String("fallback".to_string()))), + }), + }))] + ); +} +/// Verifies match expressions preserve subject, patterns, and default expression. +#[test] +fn parse_fragment_accepts_match_source() { + let program = parse_fragment(br#"return match ($x) { 1, 2 => "small", default => "other" };"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Match { + subject: Box::new(EvalExpr::LoadVar("x".to_string())), + arms: vec![EvalMatchArm { + patterns: vec![ + EvalExpr::Const(EvalConst::Int(1)), + EvalExpr::Const(EvalConst::Int(2)), + ], + value: EvalExpr::Const(EvalConst::String("small".to_string())), + }], + default: Some(Box::new(EvalExpr::Const(EvalConst::String( + "other".to_string() + )))), + }))] + ); +} +/// Verifies null coalescing binds tighter than PHP ternary expressions. +#[test] +fn parse_fragment_null_coalesce_binds_tighter_than_ternary() { + let program = + parse_fragment(br#"return $a ?? $b ? "yes" : "no";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Ternary { + condition: Box::new(EvalExpr::NullCoalesce { + value: Box::new(EvalExpr::LoadVar("a".to_string())), + default: Box::new(EvalExpr::LoadVar("b".to_string())), + }), + then_branch: Some(Box::new(EvalExpr::Const(EvalConst::String( + "yes".to_string() + )))), + else_branch: Box::new(EvalExpr::Const(EvalConst::String("no".to_string()))), + }))] + ); +} +/// Verifies logical negation parses as a unary expression before comparisons. +#[test] +fn parse_fragment_accepts_logical_not_source() { + let program = parse_fragment(br#"return !$flag == true;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::LooseEq, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(EvalExpr::LoadVar("flag".to_string())), + }), + right: Box::new(EvalExpr::Const(EvalConst::Bool(true))), + }))] + ); +} +/// Verifies unary numeric operators bind tighter than multiplication. +#[test] +fn parse_fragment_accepts_unary_numeric_source() { + let program = parse_fragment(br#"return -$x * +2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Mul, + left: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(EvalExpr::LoadVar("x".to_string())), + }), + right: Box::new(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }), + }))] + ); +} diff --git a/crates/elephc-eval/src/parser/tests/support.rs b/crates/elephc-eval/src/parser/tests/support.rs new file mode 100644 index 0000000000..05aff54f2d --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/support.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Shared imports for parser unit tests. +//! The focused test modules compare parser output against EvalIR structures +//! without repeating the parser and IR imports in each file. +//! +//! Called from: +//! - `crate::parser::tests::*` focused parser test modules. +//! +//! Key details: +//! - Re-exports are limited to parser tests through `pub(super)`. + +pub(super) use super::super::cursor::inc_dec_store; +pub(super) use super::super::parse_fragment; +pub(super) use crate::errors::EvalParseError; +pub(super) use crate::eval_ir::*; From ef3a758ebd0765940dbadcad3f3520bf5827da2c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 13:24:02 +0200 Subject: [PATCH 0297/1208] Align scalar builtin aliases --- src/codegen/lower_inst/builtins.rs | 19 +++++- src/codegen/lower_inst/conversions.rs | 5 +- src/ir_lower/expr/mod.rs | 6 +- src/optimize/effects/builtins.rs | 6 ++ src/optimize/fold/pipes.rs | 4 +- src/types/checker/builtins/catalog.rs | 5 ++ src/types/checker/builtins/numeric.rs | 19 ++++++ src/types/checker/stmt_check/narrowing.rs | 2 +- src/types/signatures.rs | 17 +++-- .../codegen/casts_and_constants/predicates.rs | 66 +++++++++++++++++++ 10 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 6bfa0b325d..2ffa10c44f 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -18,7 +18,10 @@ use crate::types::checker::builtins::is_php_visible_builtin_function; use crate::types::PhpType; use super::super::context::FunctionContext; -use super::{expect_data, expect_operand, load_value_to_first_int_arg, predicates, store_if_result}; +use super::{ + conversions, expect_data, expect_operand, load_value_to_first_int_arg, predicates, + store_if_result, +}; use crate::codegen::{CodegenIrError, Result}; pub(crate) mod attributes; @@ -61,7 +64,13 @@ pub(super) fn lower_builtin_call(ctx: &mut FunctionContext<'_>, inst: &Instructi "eval" => eval::lower_eval(ctx, inst), "buffer_len" => buffers::lower_buffer_len(ctx, inst), "buffer_free" => buffers::lower_buffer_free(ctx, inst), - + "strval" => lower_strval(ctx, inst), + "is_integer" | "is_long" => { + lower_static_type_predicate(ctx, inst, key.as_str(), PhpType::Int) + } + "is_double" | "is_real" => { + lower_static_type_predicate(ctx, inst, key.as_str(), PhpType::Float) + } "empty" => lower_empty(ctx, inst), "unset" => types::lower_unset_builtin(ctx, inst), "isset" => isset::lower_isset(ctx, inst), @@ -665,6 +674,12 @@ pub(crate) fn lower_boolval(ctx: &mut FunctionContext<'_>, inst: &Instruction) - store_if_result(ctx, inst) } +/// Lowers `strval()` through the same semantics as an explicit PHP string cast. +fn lower_strval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + ensure_arg_count(inst, "strval", 1)?; + conversions::lower_cast_to_string(ctx, inst) +} + /// Lowers `empty()` for concrete scalar and array-like operands. fn lower_empty(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "empty", 1)?; diff --git a/src/codegen/lower_inst/conversions.rs b/src/codegen/lower_inst/conversions.rs index ffd2d2ab6f..dfdfb8dc19 100644 --- a/src/codegen/lower_inst/conversions.rs +++ b/src/codegen/lower_inst/conversions.rs @@ -160,7 +160,10 @@ fn lower_cast_to_float(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Res } /// Lowers an explicit cast to PHP string for concrete scalar operands. -fn lower_cast_to_string(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_cast_to_string( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { let value = expect_operand(inst, 0)?; let raw_ty = ctx.raw_value_php_type(value)?; if matches!(raw_ty, PhpType::Resource(_)) { diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 9f6e004c08..e1fbab332d 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -6619,7 +6619,9 @@ fn builtin_return_type_override(name: &str) -> Option { "chdir" | "checkdate" | "chgrp" | "chmod" | "chown" | "lchgrp" | "lchown" | "class_alias" | "class_exists" | "copy" | "define" | "defined" | "empty" | "file_exists" | "fnmatch" | "function_exists" | "is_a" | "is_callable" - | "is_array" | "is_object" | "is_scalar" + | "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" + | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" + | "is_object" | "is_real" | "is_scalar" | "is_string" | "fdatasync" | "fflush" | "flock" | "fsync" | "ftruncate" | "interface_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_numeric" | "link" | "mkdir" | "rename" | "enum_exists" | "trait_exists" | "putenv" | "rmdir" | "is_readable" @@ -6641,7 +6643,7 @@ fn builtin_return_type_override(name: &str) -> Option { | "getcwd" | "getenv" | "gethostname" | "gethostbyname" | "php_uname" | "readline" | "shell_exec" | "sys_get_temp_dir" | "fread" | "get_resource_type" | "gzcompress" | "gzdeflate" | "hash" | "hash_final" | "hash_hmac" | "long2ip" - | "stream_get_line" | "system" | "spl_autoload_extensions" | "tempnam" | "vsprintf" + | "stream_get_line" | "system" | "spl_autoload_extensions" | "strval" | "tempnam" | "vsprintf" | "__elephc_phar_get_metadata" | "__elephc_phar_get_stub" | "__elephc_phar_get_file_metadata" | "__elephc_phar_gzip_archive" | "__elephc_phar_bzip2_archive" diff --git a/src/optimize/effects/builtins.rs b/src/optimize/effects/builtins.rs index 4aa8ee0c23..38ab9ef91a 100644 --- a/src/optimize/effects/builtins.rs +++ b/src/optimize/effects/builtins.rs @@ -37,15 +37,21 @@ pub(in crate::optimize) fn is_pure_non_throwing_builtin(name: &str) -> bool { | "intval" | "floatval" | "boolval" + | "strval" | "gettype" | "is_array" | "is_object" | "is_scalar" | "is_bool" + | "is_double" | "is_float" | "is_int" + | "is_integer" + | "is_long" | "is_null" | "is_numeric" + | "is_object" + | "is_real" | "is_string" | "is_resource" | "get_resource_type" diff --git a/src/optimize/fold/pipes.rs b/src/optimize/fold/pipes.rs index e88231e602..3aa150b030 100644 --- a/src/optimize/fold/pipes.rs +++ b/src/optimize/fold/pipes.rs @@ -84,8 +84,8 @@ pub(super) fn try_fold_pure_pipe(value: &Expr, callable: &Expr) -> Option Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::IntLiteral(_)))), - ("is_float", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::FloatLiteral(_)))), + ("is_int" | "is_integer" | "is_long", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::IntLiteral(_)))), + ("is_float" | "is_double" | "is_real", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::FloatLiteral(_)))), ("is_string", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::StringLiteral(_)))), ("is_bool", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::BoolLiteral(_)))), ("is_null", value_kind) => Some(ExprKind::BoolLiteral(matches!(value_kind, ExprKind::Null))), diff --git a/src/types/checker/builtins/catalog.rs b/src/types/checker/builtins/catalog.rs index d29b71debe..d4bcba6c94 100644 --- a/src/types/checker/builtins/catalog.rs +++ b/src/types/checker/builtins/catalog.rs @@ -20,7 +20,12 @@ const SUPPORTED_BUILTIN_FUNCTIONS: &[&str] = &[ "die", "empty", "exit", + "is_double", + "is_integer", + "is_long", + "is_real", "isset", + "strval", "unset", ]; diff --git a/src/types/checker/builtins/numeric.rs b/src/types/checker/builtins/numeric.rs index 632c420d43..264e037242 100644 --- a/src/types/checker/builtins/numeric.rs +++ b/src/types/checker/builtins/numeric.rs @@ -24,6 +24,8 @@ type BuiltinResult = Result, CompileError>; /// arity mismatch. /// /// ## Supported builtins +/// - Legacy scalar aliases not yet migrated into `src/builtins/`: `strval`, +/// `is_double`, `is_real`, `is_integer`, `is_long` /// - Control: `exit`, `die`, `empty` /// - Unset: `unset` /// - Buffers: `buffer_len`, `buffer_free` @@ -58,6 +60,23 @@ pub(super) fn check_builtin( } Ok(Some(PhpType::Void)) } + "strval" => { + if args.len() != 1 { + return Err(CompileError::new(span, "strval() takes exactly 1 argument")); + } + checker.infer_type(&args[0], env)?; + Ok(Some(PhpType::Str)) + } + "is_double" | "is_real" | "is_integer" | "is_long" => { + if args.len() != 1 { + return Err(CompileError::new( + span, + &format!("{}() takes exactly 1 argument", name), + )); + } + checker.infer_type(&args[0], env)?; + Ok(Some(PhpType::Bool)) + } "empty" => { if args.len() != 1 { return Err(CompileError::new(span, "empty() takes exactly 1 argument")); diff --git a/src/types/checker/stmt_check/narrowing.rs b/src/types/checker/stmt_check/narrowing.rs index 8d33408da1..b457236c88 100644 --- a/src/types/checker/stmt_check/narrowing.rs +++ b/src/types/checker/stmt_check/narrowing.rs @@ -233,7 +233,7 @@ fn guard_receiver_and_type(cond: &Expr) -> Option<(&Expr, PhpType)> { ExprKind::FunctionCall { name, args } if args.len() == 1 => { let target = match name.as_str().to_ascii_lowercase().as_str() { "is_int" | "is_integer" | "is_long" => PhpType::Int, - "is_float" | "is_double" => PhpType::Float, + "is_float" | "is_double" | "is_real" => PhpType::Float, "is_string" => PhpType::Str, "is_bool" => PhpType::Bool, // `is_null($x)`: same narrowing as `$x === null` — elephc models a `?T` value's diff --git a/src/types/signatures.rs b/src/types/signatures.rs index 4dd4b01d72..6b64f9853f 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -129,8 +129,9 @@ pub(crate) fn legacy_builtin_call_sig(name: &str) -> Option { Some(fixed(&["text"])) } - "intval" | "floatval" | "boolval" | "gettype" | "is_bool" | "is_null" - | "is_float" | "is_int" | "is_iterable" | "is_string" | "is_numeric" + "intval" | "floatval" | "boolval" | "strval" | "gettype" | "is_bool" + | "is_null" | "is_float" | "is_double" | "is_real" | "is_int" | "is_integer" + | "is_long" | "is_iterable" | "is_string" | "is_numeric" | "is_array" | "is_object" | "is_scalar" | "empty" => { Some(fixed(&["value"])) @@ -775,14 +776,20 @@ fn general_first_class_callable_builtin_sig(name: &str) -> Option { &[PhpType::Mixed], PhpType::Float, )), + "strval" => Some(typed_first_class_builtin_sig( + name, + &[PhpType::Mixed], + PhpType::Str, + )), // NOTE: is_array/is_object/is_scalar are intentionally NOT first-class callable. // No runtime callable wrapper is emitted for these three predicates, so listing // them here would emit an undefined `_fn_is_*` invoker reference in any program // using dynamic string callbacks. // Direct calls are fully supported; first-class/string-callback use is not (yet). - "boolval" | "is_bool" | "is_null" | "is_float" | "is_int" | "is_iterable" - | "is_string" | "is_numeric" | "is_nan" | "is_finite" | "is_infinite" - | "ctype_alpha" | "ctype_digit" | "ctype_alnum" | "ctype_space" => { + "boolval" | "is_bool" | "is_null" | "is_float" | "is_double" | "is_real" + | "is_int" | "is_integer" | "is_long" | "is_iterable" | "is_string" + | "is_numeric" | "is_nan" | "is_finite" | "is_infinite" | "ctype_alpha" + | "ctype_digit" | "ctype_alnum" | "ctype_space" => { Some(typed_first_class_builtin_sig(name, &[PhpType::Mixed], PhpType::Bool)) } "defined" => Some(typed_first_class_builtin_sig( diff --git a/tests/codegen/casts_and_constants/predicates.rs b/tests/codegen/casts_and_constants/predicates.rs index be6a55bce6..7c597b4a14 100644 --- a/tests/codegen/casts_and_constants/predicates.rs +++ b/tests/codegen/casts_and_constants/predicates.rs @@ -72,6 +72,72 @@ fn test_is_numeric_string() { assert_eq!(out, ""); } +/// Verifies PHP scalar predicate aliases and container/object predicates compile as builtin calls. +#[test] +fn test_type_predicate_aliases_array_and_object() { + let out = compile_and_run( + r#" 1]) ? "h" : "_"; +echo is_object($object) ? "o" : "_"; +echo is_object([1]) ? "bad" : "_"; +"#, + ); + assert_eq!(out, "ildraho_"); +} + +/// Verifies `is_array()` inspects boxed Mixed JSON payload tags for array values. +#[test] +fn test_is_array_recognizes_arrays_inside_mixed_array() { + let out = compile_and_run( + r#" Date: Thu, 18 Jun 2026 13:29:08 +0200 Subject: [PATCH 0298/1208] Add eval process builtins --- .../interpreter/builtins/network_env/mod.rs | 2 + .../builtins/network_env/process.rs | 84 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/network_env.rs | 6 ++ .../interpreter/builtins/registry/names.rs | 4 + .../src/interpreter/expressions.rs | 3 + .../tests/builtins_system_network.rs | 27 ++++++ 7 files changed, 127 insertions(+) create mode 100644 crates/elephc-eval/src/interpreter/builtins/network_env/process.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs b/crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs index b03aa8cac0..db58637bf0 100644 --- a/crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs @@ -13,10 +13,12 @@ mod cache; mod env; mod hosts; mod ip; +mod process; mod protocols; pub(in crate::interpreter) use cache::*; pub(in crate::interpreter) use env::*; pub(in crate::interpreter) use hosts::*; pub(in crate::interpreter) use ip::*; +pub(in crate::interpreter) use process::*; pub(in crate::interpreter) use protocols::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/process.rs b/crates/elephc-eval/src/interpreter/builtins/network_env/process.rs new file mode 100644 index 0000000000..507204021a --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/network_env/process.rs @@ -0,0 +1,84 @@ +//! Purpose: +//! Implements eval-side shell process builtins backed by the host `/bin/sh`. +//! Capturing and passthrough variants share one command runner so return-value +//! behavior stays aligned with elephc's current native backend contract. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and callable dispatch. +//! +//! Key details: +//! - `exec()` and `shell_exec()` return captured stdout as a PHP string. +//! - `system()` echoes captured stdout and returns an empty string; `passthru()` +//! echoes captured stdout and returns null. + +use std::process::Command; + +use super::super::super::*; + +/// Evaluates one eval process-control builtin over a command expression. +pub(in crate::interpreter) fn eval_builtin_process_command( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [command] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let command = eval_expr(command, context, scope, values)?; + eval_process_command_result(name, command, values) +} + +/// Evaluates one already materialized process-control command argument. +pub(in crate::interpreter) fn eval_process_command_result( + name: &str, + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let command = eval_shell_command_string(command, values)?; + let output = eval_shell_command_output(&command); + match name { + "exec" | "shell_exec" => values.string_bytes_value(&output), + "system" => { + eval_echo_process_output(&output, values)?; + values.string("") + } + "passthru" => { + eval_echo_process_output(&output, values)?; + values.null() + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Converts a PHP command cell into the host shell string accepted by `Command`. +fn eval_shell_command_string( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let command = values.string_bytes(command)?; + Ok(String::from_utf8_lossy(&command).into_owned()) +} + +/// Executes a shell command and returns stdout bytes, mapping spawn failures to an empty string. +fn eval_shell_command_output(command: &str) -> Vec { + Command::new("/bin/sh") + .arg("-c") + .arg(command) + .output() + .map(|output| output.stdout) + .unwrap_or_default() +} + +/// Echoes captured process output through the eval runtime value hooks. +fn eval_echo_process_output( + output: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if output.is_empty() { + return Ok(()); + } + let output = values.string_bytes_value(output)?; + values.echo(output) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 852e0245a7..b4ba17c66c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -162,6 +162,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "defined" => Some(&["constant_name"]), "dirname" => Some(&["path", "levels"]), "disk_free_space" | "disk_total_space" => Some(&["directory"]), + "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "fnmatch" => Some(&["pattern", "filename", "flags"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs index d50b37c8e7..fd9194b929 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs @@ -72,6 +72,12 @@ pub(in crate::interpreter) fn eval_network_env_builtin_with_values( }; eval_getenv_result(*name, values)? } + "exec" | "shell_exec" | "system" | "passthru" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_process_command_result(name, *command, values)? + } "inet_ntop" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 4628a23698..ed325153c6 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -86,6 +86,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "dirname" | "disk_free_space" | "disk_total_space" + | "exec" | "exp" | "explode" | "fdiv" @@ -194,6 +195,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "nl2br" | "number_format" | "ord" + | "passthru" | "pathinfo" | "pi" | "pow" @@ -224,6 +226,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "rmdir" | "scandir" | "settype" + | "shell_exec" | "sleep" | "sha1" | "shuffle" @@ -262,6 +265,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "strtoupper" | "strval" | "symlink" + | "system" | "sys_get_temp_dir" | "tempnam" | "tan" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 401d5b5299..a35e5fb7e5 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -443,6 +443,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_disk_space(name, args, context, scope, values) } "empty" => eval_builtin_empty(args, context, scope, values), + "exec" | "shell_exec" | "system" | "passthru" => { + eval_builtin_process_command(name, args, context, scope, values) + } "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs b/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs index 72d3b9c34c..e065f10d0d 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs @@ -248,6 +248,33 @@ return function_exists("putenv");"#, assert_eq!(values.output, "direct:named:named:set:spread:empty:1"); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval shell process builtins capture or echo stdout across all call paths. +#[test] +fn execute_program_dispatches_process_builtins() { + let program = parse_fragment( + br#"echo shell_exec("printf shell"); echo ":"; +echo exec(command: "printf exec"); echo ":"; +echo system("printf system") === "" ? "empty" : "bad"; echo ":"; +echo passthru(command: "printf pass") === null ? "null" : "bad"; echo ":"; +echo call_user_func("shell_exec", "printf call"); echo ":"; +echo call_user_func_array("exec", ["command" => "printf spread"]); echo ":"; +echo call_user_func("system", "printf dynsys") === "" ? "dyn-empty" : "bad"; echo ":"; +echo call_user_func_array("passthru", ["command" => "printf dynpass"]) === null ? "dyn-null" : "bad"; echo ":"; +echo function_exists("exec"); echo function_exists("shell_exec"); echo function_exists("system"); +return function_exists("passthru");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "shell:exec:systemempty:passnull:call:spread:dynsysdyn-empty:dynpassdyn-null:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval sleep builtins dispatch without delaying focused tests. #[test] fn execute_program_dispatches_sleep_builtins() { From 1d829209fe33cc6cef150d0beb782706346d09cc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 13:34:34 +0200 Subject: [PATCH 0299/1208] Add eval filesystem ownership builtins --- .../builtins/filesystem/ops/path_bool.rs | 116 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 2 + .../builtins/registry/dispatch/filesystem.rs | 6 + .../interpreter/builtins/registry/names.rs | 4 + .../src/interpreter/expressions.rs | 3 + .../tests/builtins_filesystem_ops.rs | 43 +++++++ 6 files changed, 174 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs index 231a2d3383..de95f3b4c9 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs @@ -8,6 +8,8 @@ //! - Paths are coerced through the shared filesystem path helper before host //! filesystem operations are attempted. +use std::ffi::CString; + use super::super::super::super::*; use super::super::super::*; use super::super::*; @@ -104,3 +106,117 @@ pub(in crate::interpreter) fn eval_chmod_result( let permissions = std::fs::Permissions::from_mode(mode); values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) } + +/// Evaluates PHP ownership/group path mutation builtins over eval expressions. +pub(in crate::interpreter) fn eval_builtin_chown_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, principal] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let principal = eval_expr(principal, context, scope, values)?; + eval_chown_like_result(name, filename, principal, values) +} + +/// Changes one local path owner or group and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_chown_like_result( + name: &str, + filename: RuntimeCellHandle, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let Some(path) = eval_c_string(&path) else { + return values.bool_value(false); + }; + let Some((uid, gid)) = eval_chown_principal_ids(name, principal, values)? else { + return values.bool_value(false); + }; + let status = unsafe { + match name { + "chown" | "chgrp" => libc::chown(path.as_ptr(), uid, gid), + "lchown" | "lchgrp" => libc::lchown(path.as_ptr(), uid, gid), + _ => return Err(EvalStatus::RuntimeFatal), + } + }; + values.bool_value(status == 0) +} + +/// Resolves one PHP owner/group argument into libc uid/gid slots. +fn eval_chown_principal_ids( + name: &str, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match (name, values.type_tag(principal)?) { + ("chown" | "lchown", EVAL_TAG_INT) => { + Ok(Some(( + eval_int_value(principal, values)? as libc::uid_t, + !0 as libc::gid_t, + ))) + } + ("chgrp" | "lchgrp", EVAL_TAG_INT) => { + Ok(Some(( + !0 as libc::uid_t, + eval_int_value(principal, values)? as libc::gid_t, + ))) + } + ("chown" | "lchown", EVAL_TAG_STRING) => { + Ok(eval_owner_name_id(principal, values)?.map(|uid| (uid, !0 as libc::gid_t))) + } + ("chgrp" | "lchgrp", EVAL_TAG_STRING) => { + Ok(eval_group_name_id(principal, values)?.map(|gid| (!0 as libc::uid_t, gid))) + } + ("chown" | "chgrp" | "lchown" | "lchgrp", _) => Err(EvalStatus::RuntimeFatal), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves a PHP user-name cell to a libc uid. +fn eval_owner_name_id( + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let name = values.string_bytes(principal)?; + let Some(name) = eval_c_bytes(&name) else { + return Ok(None); + }; + let passwd = unsafe { libc::getpwnam(name.as_ptr()) }; + if passwd.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { (*passwd).pw_uid })) + } +} + +/// Resolves a PHP group-name cell to a libc gid. +fn eval_group_name_id( + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let name = values.string_bytes(principal)?; + let Some(name) = eval_c_bytes(&name) else { + return Ok(None); + }; + let group = unsafe { libc::getgrnam(name.as_ptr()) }; + if group.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { (*group).gr_gid })) + } +} + +/// Converts a Rust path string into a C string, rejecting embedded NUL bytes. +fn eval_c_string(value: &str) -> Option { + CString::new(value).ok() +} + +/// Converts raw PHP bytes into a C string, rejecting embedded NUL bytes. +fn eval_c_bytes(value: &[u8]) -> Option { + CString::new(value).ok() +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index b4ba17c66c..59105f158c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -254,6 +254,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "sys_get_temp_dir" | "time" => Some(&[]), "tempnam" => Some(&["directory", "prefix"]), "touch" => Some(&["filename", "mtime", "atime"]), + "chown" | "lchown" => Some(&["filename", "user"]), + "chgrp" | "lchgrp" => Some(&["filename", "group"]), "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { Some(&["string"]) } diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index f19eb39e6e..3fd806c062 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -31,6 +31,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_chmod_result(*filename, *permissions, values)? } + "chown" | "chgrp" | "lchown" | "lchgrp" => { + let [filename, principal] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chown_like_result(name, *filename, *principal, values)? + } "clearstatcache" => { if evaluated_args.len() > 2 { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index ed325153c6..84d8a5deb6 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -56,8 +56,10 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "base64_encode" | "bin2hex" | "ceil" + | "chgrp" | "chdir" | "chmod" + | "chown" | "call_user_func" | "call_user_func_array" | "class_exists" @@ -149,6 +151,8 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "intval" | "link" | "linkinfo" + | "lchgrp" + | "lchown" | "ltrim" | "is_callable" | "is_array" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index a35e5fb7e5..eb2d86d859 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -482,6 +482,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { eval_builtin_hash_one_shot(name, args, context, scope, values) } + "chown" | "chgrp" | "lchown" | "lchgrp" => { + eval_builtin_chown_like(name, args, context, scope, values) + } "hash_algos" => eval_builtin_hash_algos(args, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs b/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs index 618203b88d..0d17583c34 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs @@ -238,6 +238,49 @@ return true;"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval ownership builtins mutate local files and dispatch dynamically. +#[test] +fn execute_program_dispatches_file_ownership_builtins() { + let pid = std::process::id(); + let filename = format!("elephc_eval_ownership_{pid}.txt"); + let link = format!("elephc_eval_ownership_link_{pid}.txt"); + let missing = format!("elephc_eval_ownership_missing_{pid}.txt"); + let uid = unsafe { libc::geteuid() }; + let gid = unsafe { libc::getegid() }; + let source = format!( + r#"file_put_contents("{filename}", "x"); +echo symlink("{filename}", "{link}") ? "symlink" : "bad"; echo ":"; +echo chown("{filename}", {uid}) ? "chown" : "bad"; echo ":"; +echo chgrp(filename: "{filename}", group: {gid}) ? "chgrp" : "bad"; echo ":"; +echo lchown("{link}", {uid}) ? "lchown" : "bad"; echo ":"; +echo lchgrp(filename: "{link}", group: {gid}) ? "lchgrp" : "bad"; echo ":"; +echo chown("{missing}", {uid}) ? "bad" : "missing"; echo ":"; +echo chown("{filename}", "__elephc_eval_missing_user__") ? "bad" : "user-false"; echo ":"; +echo chgrp("{filename}", "__elephc_eval_missing_group__") ? "bad" : "group-false"; echo ":"; +echo call_user_func("chgrp", "{filename}", {gid}) ? "callchgrp" : "bad"; echo ":"; +echo call_user_func_array("lchown", ["filename" => "{link}", "user" => {uid}]) ? "arraylchown" : "bad"; echo ":"; +echo unlink("{link}") && unlink("{filename}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("chown"); echo function_exists("chgrp"); echo function_exists("lchown"); +return function_exists("lchgrp");"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&link); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&link); + let _ = std::fs::remove_file(&filename); + let _ = std::fs::remove_file(&missing); + assert_eq!( + values.output, + "symlink:chown:chgrp:lchown:lchgrp:missing:user-false:group-false:callchgrp:arraylchown:cleanup:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval `touch()` creates files, stamps mtimes, and dispatches dynamically. #[test] fn execute_program_dispatches_touch_builtin() { From 3bbf2a9da22f54b5b109a09f91b1b504ca3c61dc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 13:39:58 +0200 Subject: [PATCH 0300/1208] Add eval class_alias builtin --- crates/elephc-eval/src/context.rs | 56 +++++++++++++-- .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/symbols.rs | 1 + .../interpreter/builtins/registry/names.rs | 1 + .../src/interpreter/builtins/symbols.rs | 72 ++++++++++++++++++- .../src/interpreter/expressions.rs | 6 +- .../src/interpreter/tests/builtins_symbols.rs | 37 ++++++++++ 7 files changed, 165 insertions(+), 9 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index e0d8f23634..15721da840 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -87,6 +87,7 @@ impl NativeFunction { pub struct ElephcEvalContext { abi_version: u32, classes: HashMap, + class_aliases: HashMap, constants: HashMap, functions: HashMap, native_functions: HashMap, @@ -110,6 +111,7 @@ impl ElephcEvalContext { Self { abi_version: ABI_VERSION, classes: HashMap::new(), + class_aliases: HashMap::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -134,6 +136,7 @@ impl ElephcEvalContext { Self { abi_version, classes: HashMap::new(), + class_aliases: HashMap::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -160,27 +163,68 @@ impl ElephcEvalContext { /// Defines an eval-declared class, failing if this context already has it. pub fn define_class(&mut self, class: EvalClass) -> bool { let key = normalize_class_name(class.name()); - if self.classes.contains_key(&key) { + if self.classes.contains_key(&key) || self.class_aliases.contains_key(&key) { return false; } self.classes.insert(key, class); true } - /// Returns true when this eval context has a dynamic class with the requested name. + /// Returns true when this eval context has a dynamic class or alias with the requested name. pub fn has_class(&self, name: &str) -> bool { - self.classes.contains_key(&normalize_class_name(name)) + let key = normalize_class_name(name); + self.classes.contains_key(&key) || self.class_aliases.contains_key(&key) } - /// Returns a dynamic eval class by PHP case-insensitive class name. + /// Returns a dynamic eval class by PHP case-insensitive class name or alias. pub fn class(&self, name: &str) -> Option<&EvalClass> { - self.classes.get(&normalize_class_name(name)) + let key = normalize_class_name(name); + if let Some(class) = self.classes.get(&key) { + return Some(class); + } + self.class_aliases + .get(&key) + .and_then(|target| self.classes.get(&normalize_class_name(target))) + } + + /// Resolves a PHP class name or alias to the canonical target spelling stored by eval. + pub fn resolve_class_name(&self, name: &str) -> Option { + let key = normalize_class_name(name); + if let Some(class) = self.classes.get(&key) { + return Some(class.name().to_string()); + } + self.class_aliases.get(&key).cloned() + } + + /// Defines an alias for an eval-declared class or an already known alias. + pub fn define_class_alias(&mut self, original: &str, alias: &str) -> bool { + let Some(target) = self.resolve_class_name(original) else { + return false; + }; + self.define_external_class_alias(&target, alias) + } + + /// Defines an alias for a runtime-visible class whose metadata lives outside eval. + pub fn define_external_class_alias(&mut self, original: &str, alias: &str) -> bool { + let alias_key = normalize_class_name(alias); + if alias_key.is_empty() + || self.classes.contains_key(&alias_key) + || self.class_aliases.contains_key(&alias_key) + { + return false; + } + self.class_aliases + .insert(alias_key, original.trim_start_matches('\\').to_string()); + true } /// Records that one runtime object handle was created for an eval-declared class. pub fn register_dynamic_object(&mut self, identity: u64, class_name: &str) { + let class_name = self + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.to_string()); self.dynamic_objects - .insert(identity, normalize_class_name(class_name)); + .insert(identity, normalize_class_name(&class_name)); } /// Returns the dynamic eval class metadata associated with one object identity. diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 59105f158c..25b8f2e6af 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -142,6 +142,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "get_parent_class" => Some(&["object_or_class"]), "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), + "class_alias" => Some(&["class", "alias", "autoload"]), "class_exists" => Some(&["class", "autoload"]), "enum_exists" => Some(&["enum", "autoload"]), "interface_exists" => Some(&["interface", "autoload"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index e76a74ccc0..3ebe5d83df 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -41,6 +41,7 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( values.bool_value(eval_function_probe_exists(context, &name))? } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, + "class_alias" => eval_class_alias_result(evaluated_args, context, values)?, "enum_exists" | "trait_exists" => { eval_class_like_exists_result(name, evaluated_args, values)? } diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 84d8a5deb6..92d42240b5 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -60,6 +60,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "chdir" | "chmod" | "chown" + | "class_alias" | "call_user_func" | "call_user_func_array" | "class_exists" diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index ec2de32fd7..baeea764fe 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -176,6 +176,65 @@ pub(in crate::interpreter) fn eval_class_exists_name( values.class_exists(name) } +/// Evaluates `class_alias(class, alias, autoload?)` against eval and generated class tables. +pub(in crate::interpreter) fn eval_builtin_class_alias( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (class, alias) = match args { + [class, alias] => ( + eval_expr(class, context, scope, values)?, + eval_expr(alias, context, scope, values)?, + ), + [class, alias, autoload] => { + let class = eval_expr(class, context, scope, values)?; + let alias = eval_expr(alias, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + (class, alias) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_alias_result(&[class, alias], context, values) +} + +/// Evaluates `class_alias(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_class_alias_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (class, alias) = match evaluated_args { + [class, alias] => (*class, *alias), + [class, alias, _autoload] => (*class, *alias), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let class = eval_class_alias_name(class, values)?; + let alias = eval_class_alias_name(alias, values)?; + if alias.is_empty() || context.has_class(&alias) || values.class_exists(&alias)? { + return values.bool_value(false); + } + let aliased = if context.has_class(&class) { + context.define_class_alias(&class, &alias) + } else if values.class_exists(&class)? { + context.define_external_class_alias(&class, &alias) + } else { + false + }; + values.bool_value(aliased) +} + +/// Reads and normalizes one `class_alias()` class-name argument. +fn eval_class_alias_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_string()) +} + /// Evaluates `interface_exists(...)` against generated interface-name metadata. pub(in crate::interpreter) fn eval_builtin_interface_exists( args: &[EvalExpr], @@ -306,12 +365,21 @@ pub(in crate::interpreter) fn eval_is_a_relation_result( let target_class = values.string_bytes(target_class)?; let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; let target_class = target_class.trim_start_matches('\\'); + let resolved_target_class = context + .resolve_class_name(target_class) + .unwrap_or_else(|| target_class.to_string()); let is_object = values.type_tag(object_or_class)? == 6; let result = - if is_object && dynamic_object_is_a(object_or_class, target_class, context, values)? { + if is_object + && dynamic_object_is_a(object_or_class, &resolved_target_class, context, values)? + { !matches!(name, "is_subclass_of") } else if is_object || allow_string { - values.object_is_a(object_or_class, target_class, name == "is_subclass_of")? + values.object_is_a( + object_or_class, + &resolved_target_class, + name == "is_subclass_of", + )? } else { false }; diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index eb2d86d859..617c574f33 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -68,8 +68,11 @@ pub(in crate::interpreter) fn eval_expr( if let Some(class) = context.class(class_name).cloned() { eval_dynamic_class_new_object(&class, args, context, scope, values) } else { + let class_name = context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.clone()); values - .new_object(class_name) + .new_object(&class_name) .and_then(|object| values.construct_object(object, args).map(|()| object)) } } @@ -417,6 +420,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "class_alias" => eval_builtin_class_alias(args, context, scope, values), "class_exists" => eval_builtin_class_exists(args, context, scope, values), "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), "trait_exists" | "enum_exists" => { diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs index caa6bc5372..e7a40265b5 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs @@ -276,6 +276,43 @@ echo class_exists(class: "MissingClass", autoload: false) ? "Y" : "N";"#, assert_eq!(values.output, "YYYYN"); } +/// Verifies eval `class_alias()` registers dynamic and runtime-visible aliases. +#[test] +fn execute_program_class_alias_registers_aliases() { + let program = parse_fragment( + br#"class DynAliasBox { + public int $x = 1; + public function __construct($x) { $this->x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +} +echo class_alias("DynAliasBox", "DynAliasCopy") ? "alias" : "bad"; echo ":"; +echo class_exists("DynAliasCopy") ? "exists" : "bad"; echo ":"; +$box = new DynAliasCopy(5); +echo get_class($box); echo ":"; +echo $box->bump(2); echo ":"; +echo is_a($box, "DynAliasCopy") ? "isa" : "bad"; echo ":"; +echo class_alias("DynAliasBox", "DynAliasCopy") ? "bad" : "duplicate"; echo ":"; +echo class_alias("MissingAliasSource", "MissingAliasTarget") ? "bad" : "missing"; echo ":"; +echo call_user_func("class_alias", "DynAliasBox", "DynAliasCall") ? "call" : "bad"; echo ":"; +echo class_exists("DynAliasCall") ? "call-exists" : "bad"; echo ":"; +echo call_user_func_array("class_alias", ["class" => "KnownClass", "alias" => "KnownAlias"]) ? "aot" : "bad"; echo ":"; +$known = new KnownAlias(); +echo is_a($known, "KnownAlias") ? "known" : "bad"; echo ":"; +echo function_exists("class_alias"); +return is_callable("class_alias");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "alias:exists:DynAliasBox:7:isa:duplicate:missing:call:call-exists:aot:known:1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies duplicate eval-declared class names fail through runtime status. #[test] fn execute_program_duplicate_class_declaration_fails() { From 55c16bc5c5b63840ddac54b5981aa9b82d8815a2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 13:48:07 +0200 Subject: [PATCH 0301/1208] Add eval declared symbol builtins --- crates/elephc-eval/src/context.rs | 11 +++++ .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/symbols.rs | 6 +++ .../interpreter/builtins/registry/names.rs | 3 ++ .../src/interpreter/builtins/symbols.rs | 45 +++++++++++++++++++ .../src/interpreter/expressions.rs | 3 ++ .../src/interpreter/tests/builtins_symbols.rs | 33 ++++++++++++++ 7 files changed, 102 insertions(+) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 15721da840..134425f614 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -88,6 +88,7 @@ pub struct ElephcEvalContext { abi_version: u32, classes: HashMap, class_aliases: HashMap, + declared_class_names: Vec, constants: HashMap, functions: HashMap, native_functions: HashMap, @@ -112,6 +113,7 @@ impl ElephcEvalContext { abi_version: ABI_VERSION, classes: HashMap::new(), class_aliases: HashMap::new(), + declared_class_names: Vec::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -137,6 +139,7 @@ impl ElephcEvalContext { abi_version, classes: HashMap::new(), class_aliases: HashMap::new(), + declared_class_names: Vec::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -166,6 +169,7 @@ impl ElephcEvalContext { if self.classes.contains_key(&key) || self.class_aliases.contains_key(&key) { return false; } + self.declared_class_names.push(class.name().to_string()); self.classes.insert(key, class); true } @@ -215,9 +219,16 @@ impl ElephcEvalContext { } self.class_aliases .insert(alias_key, original.trim_start_matches('\\').to_string()); + self.declared_class_names + .push(alias.trim_start_matches('\\').to_string()); true } + /// Returns class names declared or aliased through eval in PHP-visible order. + pub fn declared_class_names(&self) -> &[String] { + &self.declared_class_names + } + /// Records that one runtime object handle was created for an eval-declared class. pub fn register_dynamic_object(&mut self, identity: u64, class_name: &str) { let class_name = self diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 25b8f2e6af..f8450ce81b 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -173,6 +173,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), "function_exists" => Some(&["function"]), + "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), "gethostbyaddr" => Some(&["ip"]), "gethostbyname" => Some(&["hostname"]), "gethostname" => Some(&[]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index 3ebe5d83df..9a317844f6 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -55,6 +55,12 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( }; eval_get_class_result(*object, context, values)? } + "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_declared_symbols_result(name, context, values)? + } "get_parent_class" => { let [object_or_class] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 92d42240b5..31b24734bf 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -119,6 +119,9 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "getservbyname" | "getservbyport" | "get_class" + | "get_declared_classes" + | "get_declared_interfaces" + | "get_declared_traits" | "get_parent_class" | "get_resource_id" | "get_resource_type" diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index baeea764fe..6c5bc8994c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -235,6 +235,51 @@ fn eval_class_alias_name( Ok(name.trim_start_matches('\\').to_string()) } +/// Evaluates `get_declared_classes/interfaces/traits()` for eval-visible declarations. +pub(in crate::interpreter) fn eval_builtin_get_declared_symbols( + name: &str, + args: &[EvalExpr], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_declared_symbols_result(name, context, values) +} + +/// Builds an indexed array for eval-visible declared class-like names. +pub(in crate::interpreter) fn eval_get_declared_symbols_result( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "get_declared_classes" => { + eval_dynamic_string_array_result(context.declared_class_names(), values) + } + "get_declared_interfaces" | "get_declared_traits" => { + eval_dynamic_string_array_result(&[], values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds one indexed PHP array from runtime-owned strings. +fn eval_dynamic_string_array_result( + items: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Evaluates `interface_exists(...)` against generated interface-name metadata. pub(in crate::interpreter) fn eval_builtin_interface_exists( args: &[EvalExpr], diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 617c574f33..18fbdd1ef2 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -475,6 +475,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), "get_class" => eval_builtin_get_class(args, context, scope, values), + "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { + eval_builtin_get_declared_symbols(name, args, context, values) + } "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), "get_resource_id" | "get_resource_type" => { eval_builtin_resource_introspection(name, args, context, scope, values) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs index e7a40265b5..4a47727301 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs @@ -313,6 +313,39 @@ return is_callable("class_alias");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval `get_declared_*()` lists eval-visible class-like declarations. +#[test] +fn execute_program_get_declared_symbols_reports_eval_declarations() { + let program = parse_fragment( + br#"class DeclaredOne {} +class DeclaredTwo {} +class_alias("DeclaredOne", "DeclaredAlias"); +$classes = get_declared_classes(); +echo count($classes); echo ":"; +echo $classes[0]; echo ":"; +echo $classes[1]; echo ":"; +echo $classes[2]; echo ":"; +$call = call_user_func("get_declared_classes"); +echo count($call); echo ":"; +echo $call[2]; echo ":"; +echo count(get_declared_interfaces()); echo ":"; +echo count(call_user_func_array("get_declared_traits", [])); echo ":"; +echo function_exists("get_declared_classes"); +echo function_exists("get_declared_interfaces"); +return is_callable("get_declared_traits");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:DeclaredOne:DeclaredTwo:DeclaredAlias:3:DeclaredAlias:0:0:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies duplicate eval-declared class names fail through runtime status. #[test] fn execute_program_duplicate_class_declaration_fails() { From 4efd4c4aa77a162460da941f7facf9d873e8204e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 13:51:43 +0200 Subject: [PATCH 0302/1208] Add eval grapheme_strrev builtin --- Cargo.lock | 7 ++++ crates/elephc-eval/Cargo.toml | 1 + .../interpreter/builtins/registry/binding.rs | 7 ++-- .../builtins/registry/dispatch/strings.rs | 6 +++ .../interpreter/builtins/registry/names.rs | 1 + .../builtins/strings/grapheme_strrev.rs | 42 +++++++++++++++++++ .../src/interpreter/builtins/strings/mod.rs | 2 + .../src/interpreter/expressions.rs | 1 + .../tests/builtins_strings_binary.rs | 24 +++++++++++ 9 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/grapheme_strrev.rs diff --git a/Cargo.lock b/Cargo.lock index 5ca3dba80c..e9d38ae7a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,6 +623,7 @@ dependencies = [ "elephc-crypto", "libc", "regex", + "unicode-segmentation", ] [[package]] @@ -2598,6 +2599,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-xid" version = "0.2.6" diff --git a/crates/elephc-eval/Cargo.toml b/crates/elephc-eval/Cargo.toml index 62c4c531bc..b47e2836f6 100644 --- a/crates/elephc-eval/Cargo.toml +++ b/crates/elephc-eval/Cargo.toml @@ -13,3 +13,4 @@ crate-type = ["staticlib", "rlib"] elephc-crypto = { path = "../elephc-crypto" } libc = "0.2" regex = "1" +unicode-segmentation = "1" diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index f8450ce81b..d7e2d9343d 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -129,10 +129,9 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), "atan2" => Some(&["y", "x"]), "basename" => Some(&["path", "suffix"]), - "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "hex2bin" - | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => { - Some(&["string"]) - } + "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "grapheme_strrev" + | "hex2bin" | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" + | "urlencode" => Some(&["string"]), "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" | "is_real" diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs index d3e7057ae2..820625c25c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs @@ -49,6 +49,12 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( }; eval_chr_result(*value, values)? } + "grapheme_strrev" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_grapheme_strrev_result(*value, values)? + } "rawurldecode" | "urldecode" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 31b24734bf..45bfb765a9 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -129,6 +129,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "getenv" | "gettype" | "glob" + | "grapheme_strrev" | "hash" | "hash_algos" | "hash_equals" diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/grapheme_strrev.rs b/crates/elephc-eval/src/interpreter/builtins/strings/grapheme_strrev.rs new file mode 100644 index 0000000000..b730a9da66 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/grapheme_strrev.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Implements eval's PHP `grapheme_strrev()` builtin. +//! Reverses valid UTF-8 strings by grapheme cluster while preserving each +//! cluster's internal byte order. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports used by call dispatch. +//! +//! Key details: +//! - Invalid UTF-8 returns PHP false, matching elephc's `__rt_grapheme_strrev` +//! string-or-false contract. + +use unicode_segmentation::UnicodeSegmentation; + +use super::super::super::*; + +/// Evaluates PHP `grapheme_strrev(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_grapheme_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_grapheme_strrev_result(value, values) +} + +/// Reverses a materialized PHP string by grapheme cluster or returns false for invalid UTF-8. +pub(in crate::interpreter) fn eval_grapheme_strrev_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let Ok(source) = std::str::from_utf8(&bytes) else { + return values.bool_value(false); + }; + let reversed = source.graphemes(true).rev().collect::(); + values.string(&reversed) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs b/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs index ef506badfb..3ee17eb82e 100644 --- a/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs @@ -9,6 +9,7 @@ //! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime behavior. mod ctype; +mod grapheme_strrev; mod hash; mod html; mod introspection; @@ -22,6 +23,7 @@ mod substr; mod url; pub(in crate::interpreter) use ctype::*; +pub(in crate::interpreter) use grapheme_strrev::*; pub(in crate::interpreter) use hash::*; pub(in crate::interpreter) use html::*; pub(in crate::interpreter) use introspection::*; diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 18fbdd1ef2..4898f2ff64 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -486,6 +486,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "getenv" => eval_builtin_getenv(args, context, scope, values), "gettype" => eval_builtin_gettype(args, context, scope, values), "glob" => eval_builtin_glob(args, context, scope, values), + "grapheme_strrev" => eval_builtin_grapheme_strrev(args, context, scope, values), "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { eval_builtin_hash_one_shot(name, args, context, scope, values) } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs b/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs index 9ec571ff09..9082fd9ac5 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs @@ -27,6 +27,30 @@ return function_exists("strrev");"#, assert_eq!(values.output, "olleH:321:CBA:fed:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval `grapheme_strrev()` reverses UTF-8 grapheme clusters. +#[test] +fn execute_program_dispatches_grapheme_strrev_builtin() { + let program = parse_fragment( + br#"echo grapheme_strrev("ABCDE"); echo ":"; +echo bin2hex(grapheme_strrev(hex2bin("4165cc8142"))); echo ":"; +echo bin2hex(grapheme_strrev(hex2bin("41f09f91a9f09f8fbde2808df09f92bb42"))); echo ":"; +echo grapheme_strrev(chr(255)) === false ? "false" : "bad"; echo ":"; +echo call_user_func("grapheme_strrev", "xy"); echo ":"; +echo call_user_func_array("grapheme_strrev", ["string" => "pq"]); echo ":"; +return function_exists("grapheme_strrev") && is_callable("grapheme_strrev");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EDCBA:4265cc8141:42f09f91a9f09f8fbde2808df09f92bb41:false:yx:qp:" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval `chr()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_chr_builtin() { From b2bf5c2194ab35b436627cb379cee0224a2904fa Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 13:54:28 +0200 Subject: [PATCH 0303/1208] Add eval gzip string builtins --- Cargo.lock | 1 + crates/elephc-eval/Cargo.toml | 1 + .../interpreter/builtins/registry/binding.rs | 2 + .../builtins/registry/dispatch/strings.rs | 3 + .../interpreter/builtins/registry/names.rs | 4 + .../src/interpreter/builtins/strings/gzip.rs | 126 ++++++++++++++++++ .../src/interpreter/builtins/strings/mod.rs | 2 + .../src/interpreter/expressions.rs | 3 + .../tests/builtins_strings_binary.rs | 31 +++++ 9 files changed, 173 insertions(+) create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/gzip.rs diff --git a/Cargo.lock b/Cargo.lock index e9d38ae7a4..d1ea11e56f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,6 +621,7 @@ name = "elephc-eval" version = "0.1.0" dependencies = [ "elephc-crypto", + "flate2", "libc", "regex", "unicode-segmentation", diff --git a/crates/elephc-eval/Cargo.toml b/crates/elephc-eval/Cargo.toml index b47e2836f6..261f0f971b 100644 --- a/crates/elephc-eval/Cargo.toml +++ b/crates/elephc-eval/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["staticlib", "rlib"] [dependencies] elephc-crypto = { path = "../elephc-crypto" } +flate2 = "1" libc = "0.2" regex = "1" unicode-segmentation = "1" diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index d7e2d9343d..01d9e2d35a 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -189,6 +189,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "hash_equals" => Some(&["known_string", "user_string"]), "hash_file" => Some(&["algo", "filename", "binary"]), "hash_hmac" => Some(&["algo", "data", "key", "binary"]), + "gzcompress" | "gzdeflate" => Some(&["data", "level"]), + "gzinflate" | "gzuncompress" => Some(&["data", "max_length"]), "hypot" => Some(&["x", "y"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs index 820625c25c..29602a945c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs @@ -55,6 +55,9 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( }; eval_grapheme_strrev_result(*value, values)? } + "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { + eval_gzip_result(name, evaluated_args, values)? + } "rawurldecode" | "urldecode" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 45bfb765a9..6ffdefc4ad 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -130,6 +130,10 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "gettype" | "glob" | "grapheme_strrev" + | "gzcompress" + | "gzdeflate" + | "gzinflate" + | "gzuncompress" | "hash" | "hash_algos" | "hash_equals" diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/gzip.rs b/crates/elephc-eval/src/interpreter/builtins/strings/gzip.rs new file mode 100644 index 0000000000..37440d7437 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/gzip.rs @@ -0,0 +1,126 @@ +//! Purpose: +//! Implements eval gzip/zlib string builtins. +//! Covers zlib-wrapped `gzcompress`/`gzuncompress` and raw-DEFLATE +//! `gzdeflate`/`gzinflate` using Rust-side compression helpers. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` re-exports used by call dispatch. +//! +//! Key details: +//! - Decompression failures return PHP false, matching the compiler backend's +//! string-or-false observable contract. + +use std::io::{Read, Write}; + +use flate2::read::{DeflateDecoder, ZlibDecoder}; +use flate2::write::{DeflateEncoder, ZlibEncoder}; +use flate2::Compression; + +use super::super::super::*; +use super::super::*; + +/// Evaluates one gzip/zlib string builtin over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzip( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_gzip_result(name, &evaluated_args, values) +} + +/// Dispatches one materialized gzip/zlib builtin call. +pub(in crate::interpreter) fn eval_gzip_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let (data, option) = match evaluated_args { + [data] => (*data, None), + [data, option] => (*data, Some(*option)), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let data = values.string_bytes(data)?; + match name { + "gzcompress" => eval_gz_encode(data, option, true, values), + "gzdeflate" => eval_gz_encode(data, option, false, values), + "gzuncompress" => eval_gz_decode(data, true, values), + "gzinflate" => eval_gz_decode(data, false, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Encodes data as zlib-wrapped or raw-DEFLATE bytes. +fn eval_gz_encode( + data: Vec, + level: Option, + zlib_wrapped: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let compression = eval_gz_compression(level, values)?; + let compressed = if zlib_wrapped { + let mut encoder = ZlibEncoder::new(Vec::new(), compression); + eval_gz_write_all(&mut encoder, &data)?; + encoder.finish().map_err(|_| EvalStatus::RuntimeFatal)? + } else { + let mut encoder = DeflateEncoder::new(Vec::new(), compression); + eval_gz_write_all(&mut encoder, &data)?; + encoder.finish().map_err(|_| EvalStatus::RuntimeFatal)? + }; + values.string_bytes_value(&compressed) +} + +/// Decodes zlib-wrapped or raw-DEFLATE bytes and returns false on inflate errors. +fn eval_gz_decode( + data: Vec, + zlib_wrapped: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let decoded = if zlib_wrapped { + eval_gz_read(ZlibDecoder::new(data.as_slice())) + } else { + eval_gz_read(DeflateDecoder::new(data.as_slice())) + }; + match decoded { + Ok(decoded) => values.string_bytes_value(&decoded), + Err(_) => values.bool_value(false), + } +} + +/// Converts PHP's optional compression level to a flate2 compression value. +fn eval_gz_compression( + level: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(level) = level else { + return Ok(Compression::default()); + }; + let level = eval_int_value(level, values)?; + if level < 0 { + return Ok(Compression::default()); + } + let level = u32::try_from(level).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(Compression::new(level.min(9))) +} + +/// Writes all source bytes into a compression stream. +fn eval_gz_write_all( + encoder: &mut W, + data: &[u8], +) -> Result<(), EvalStatus> { + encoder + .write_all(data) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Reads all bytes from a decompression stream. +fn eval_gz_read(mut decoder: R) -> std::io::Result> { + let mut decoded = Vec::new(); + decoder.read_to_end(&mut decoded)?; + Ok(decoded) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs b/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs index 3ee17eb82e..7147e19279 100644 --- a/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs @@ -10,6 +10,7 @@ mod ctype; mod grapheme_strrev; +mod gzip; mod hash; mod html; mod introspection; @@ -24,6 +25,7 @@ mod url; pub(in crate::interpreter) use ctype::*; pub(in crate::interpreter) use grapheme_strrev::*; +pub(in crate::interpreter) use gzip::*; pub(in crate::interpreter) use hash::*; pub(in crate::interpreter) use html::*; pub(in crate::interpreter) use introspection::*; diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 4898f2ff64..cd32599d57 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -487,6 +487,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "gettype" => eval_builtin_gettype(args, context, scope, values), "glob" => eval_builtin_glob(args, context, scope, values), "grapheme_strrev" => eval_builtin_grapheme_strrev(args, context, scope, values), + "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { + eval_builtin_gzip(name, args, context, scope, values) + } "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { eval_builtin_hash_one_shot(name, args, context, scope, values) } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs b/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs index 9082fd9ac5..2eeff51eeb 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs @@ -70,6 +70,37 @@ return function_exists("chr");"#, assert_eq!(values.output, "A:00:01:A:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval gzip/zlib builtins round-trip strings and return false on invalid data. +#[test] +fn execute_program_dispatches_gzip_builtins() { + let program = parse_fragment( + br#"$data = "hello\0world"; +$zlib = gzcompress($data); +echo gzuncompress($zlib) === $data ? "zc" : "bad"; echo ":"; +$raw = gzdeflate($data); +echo gzinflate($raw) === $data ? "df" : "bad"; echo ":"; +echo gzuncompress("not zlib") === false ? "bad-zlib" : "bad"; echo ":"; +echo gzinflate("not raw deflate") === false ? "bad-raw" : "bad"; echo ":"; +echo gzuncompress(gzcompress(data: "abc", level: 1)); echo ":"; +echo call_user_func("gzuncompress", call_user_func("gzcompress", "call")); echo ":"; +echo call_user_func_array("gzinflate", ["data" => call_user_func_array("gzdeflate", ["data" => "spread", "level" => 1])]); echo ":"; +echo function_exists("gzcompress"); +echo function_exists("gzdeflate"); +echo function_exists("gzinflate"); +return is_callable("gzuncompress");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "zc:df:bad-zlib:bad-raw:abc:call:spread:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval `str_repeat()` dispatches through direct, named, and callable paths. #[test] fn execute_program_dispatches_str_repeat_builtin() { From ad71804c10dae99184aa0d850271d43211c1b762 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 13:56:52 +0200 Subject: [PATCH 0304/1208] Add eval stream resolve include path builtin --- .../interpreter/builtins/filesystem/path.rs | 22 +++++++++++ .../interpreter/builtins/registry/binding.rs | 2 +- .../builtins/registry/dispatch/filesystem.rs | 6 +++ .../interpreter/builtins/registry/names.rs | 1 + .../src/interpreter/expressions.rs | 3 ++ .../tests/builtins_filesystem_ops.rs | 37 ++++++++++++++++++- 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs index 243e012d2e..77a71375a7 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs @@ -213,6 +213,28 @@ pub(in crate::interpreter) fn eval_realpath_result( values.string(canonical.as_ref()) } +/// Evaluates PHP `stream_resolve_include_path($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_resolve_include_path( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_stream_resolve_include_path_result(filename, values) +} + +/// Resolves one filename using elephc's realpath-equivalent include-path semantics. +pub(in crate::interpreter) fn eval_stream_resolve_include_path_result( + filename: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_realpath_result(filename, values) +} + /// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. pub(in crate::interpreter) fn eval_builtin_pathinfo( args: &[EvalExpr], diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 01d9e2d35a..2f42bbf634 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -234,7 +234,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "range" => Some(&["start", "end"]), - "realpath" => Some(&["path"]), + "realpath" | "stream_resolve_include_path" => Some(&["path"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index 3fd806c062..d7e4599569 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -163,6 +163,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_realpath_result(*path, values)? } + "stream_resolve_include_path" => { + let [filename] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_resolve_include_path_result(*filename, values)? + } "realpath_cache_get" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 6ffdefc4ad..04dec7d3b2 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -256,6 +256,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" + | "stream_resolve_include_path" | "str_contains" | "str_ends_with" | "str_ireplace" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index cd32599d57..b6eb6238fe 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -571,6 +571,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { eval_builtin_stream_introspection(name, args, values) } + "stream_resolve_include_path" => { + eval_builtin_stream_resolve_include_path(args, context, scope, values) + } "strtotime" => eval_builtin_strtotime(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs b/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs index 0d17583c34..28d6f51a21 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs @@ -64,7 +64,42 @@ return true;"#, assert_eq!( values.output, "mkdir:dir:copy:rename:symlink:readlink:linkinfo:readlink-false:linkinfo-missing:hardlink:cache:cleanup:callmkdir:callrmdir:111111111" - ); + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval `stream_resolve_include_path()` mirrors elephc realpath semantics. +#[test] +fn execute_program_dispatches_stream_resolve_include_path_builtin() { + let pid = std::process::id(); + let file = format!("elephc_eval_stream_resolve_{pid}.txt"); + let missing = format!("elephc_eval_stream_resolve_missing_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "payload"); +$resolved = stream_resolve_include_path("{file}"); +echo is_string($resolved) && basename($resolved) === "{file}" && file_get_contents($resolved) === "payload" ? "resolved" : "bad"; echo ":"; +echo stream_resolve_include_path("{missing}") === false ? "missing" : "bad"; echo ":"; +$named = stream_resolve_include_path(path: "{file}"); +echo is_string($named) && basename($named) === "{file}" ? "named" : "bad"; echo ":"; +$call = call_user_func("stream_resolve_include_path", "{file}"); +echo is_string($call) && basename($call) === "{file}" ? "call" : "bad"; echo ":"; +$spread = call_user_func_array("stream_resolve_include_path", ["path" => "{file}"]); +echo is_string($spread) && basename($spread) === "{file}" ? "spread" : "bad"; echo ":"; +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +return function_exists("stream_resolve_include_path");"#, + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let _ = std::fs::remove_file(&missing); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + "resolved:missing:named:call:spread:cleanup:" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval file-listing builtins build arrays, stream files, and dispatch dynamically. From 4bfeff9ca41797c56bc80a74ee0e8fcdec96ea9a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 13:58:45 +0200 Subject: [PATCH 0305/1208] Add eval stream predicate builtins --- .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/strings.rs | 6 ++++ .../interpreter/builtins/registry/names.rs | 2 ++ .../builtins/strings/introspection.rs | 33 +++++++++++++++++++ .../src/interpreter/expressions.rs | 3 ++ .../tests/builtins_system_network.rs | 22 +++++++++++++ 6 files changed, 67 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 2f42bbf634..581597787c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -242,6 +242,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "spl_object_id" | "spl_object_hash" => Some(&["object"]), "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), + "stream_is_local" | "stream_supports_lock" => Some(&["stream"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs index 29602a945c..f6f895b7dd 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs @@ -169,6 +169,12 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( } eval_hash_algos_result(values)? } + "stream_is_local" | "stream_supports_lock" => { + let [stream] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bool_predicate_result(name, *stream, values)? + } "hash_equals" => { let [known, user] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 04dec7d3b2..f074bac88a 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -256,6 +256,8 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" + | "stream_is_local" + | "stream_supports_lock" | "stream_resolve_include_path" | "str_contains" | "str_ends_with" diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs b/crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs index 1adba529aa..c093683ce4 100644 --- a/crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs +++ b/crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs @@ -85,3 +85,36 @@ pub(in crate::interpreter) fn eval_stream_introspection_result( }; eval_static_string_array_result(items, values) } + +/// Evaluates PHP stream boolean-introspection builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_bool_predicate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_bool_predicate_result(name, stream, values) +} + +/// Returns elephc's fixed stream-locality and lock-support predicate values. +pub(in crate::interpreter) fn eval_stream_bool_predicate_result( + name: &str, + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "stream_is_local" => values.bool_value(true), + "stream_supports_lock" => { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index b6eb6238fe..c1a66f9554 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -568,6 +568,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "tempnam" => eval_builtin_tempnam(args, context, scope, values), "time" => eval_builtin_time(args, values), "touch" => eval_builtin_touch(args, context, scope, values), + "stream_is_local" | "stream_supports_lock" => { + eval_builtin_stream_bool_predicate(name, args, context, scope, values) + } "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { eval_builtin_stream_introspection(name, args, values) } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs b/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs index e065f10d0d..568c96f984 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs @@ -168,6 +168,28 @@ return function_exists("stream_get_filters");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval stream predicate stubs match elephc's fixed stream metadata behavior. +#[test] +fn execute_program_dispatches_stream_predicate_builtins() { + let program = parse_fragment( + br#"echo stream_is_local("php://memory") ? "local" : "bad"; echo ":"; +echo stream_supports_lock($handle) ? "lock" : "bad"; echo ":"; +echo call_user_func("stream_is_local", "file://tmp") ? "call" : "bad"; echo ":"; +echo call_user_func_array("stream_supports_lock", ["stream" => $handle]) ? "spread" : "bad"; echo ":"; +echo function_exists("stream_is_local"); +return function_exists("stream_supports_lock");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let handle = values.alloc(FakeValue::Resource(6)); + scope.set("handle", handle, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "local:lock:call:spread:1"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval `spl_classes()` returns the native-compatible SPL type snapshot. #[test] fn execute_program_dispatches_spl_classes_builtin() { From d0f43ad833a54a12c35b7175021a3b638aa43d04 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:14:20 +0200 Subject: [PATCH 0306/1208] Add eval local file stream builtins --- crates/elephc-eval/src/context.rs | 14 + .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/streams.rs | 463 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 18 +- .../builtins/registry/dispatch/filesystem.rs | 84 +++- .../interpreter/builtins/registry/names.rs | 17 + .../src/interpreter/expressions.rs | 17 + .../src/interpreter/runtime_ops.rs | 3 + .../tests/builtins_file_streams.rs | 88 ++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + .../src/interpreter/tests/support/cell_ops.rs | 4 + .../interpreter/tests/support/runtime_ops.rs | 4 + crates/elephc-eval/src/lib.rs | 1 + .../elephc-eval/src/runtime_hooks/externs.rs | 1 + crates/elephc-eval/src/runtime_hooks/ops.rs | 5 + crates/elephc-eval/src/stream_resources.rs | 326 ++++++++++++ src/codegen_support/runtime/eval_bridge.rs | 11 + 17 files changed, 1057 insertions(+), 2 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs create mode 100644 crates/elephc-eval/src/stream_resources.rs diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 134425f614..cc51c4d195 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -17,6 +17,7 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::{EvalClass, EvalFunction}; use crate::scope::ElephcEvalScope; +use crate::stream_resources::EvalStreamResources; use crate::value::{RuntimeCell, RuntimeCellHandle}; /// Native descriptor-invoker ABI registered by generated code for AOT functions. @@ -98,6 +99,7 @@ pub struct ElephcEvalContext { global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, pending_throw: Option, + streams: EvalStreamResources, json_last_error: i64, json_last_error_msg: String, call_file: String, @@ -123,6 +125,7 @@ impl ElephcEvalContext { global_scope: None, function_stack: Vec::new(), pending_throw: None, + streams: EvalStreamResources::default(), json_last_error: 0, json_last_error_msg: String::from("No error"), call_file: String::new(), @@ -149,6 +152,7 @@ impl ElephcEvalContext { global_scope: None, function_stack: Vec::new(), pending_throw: None, + streams: EvalStreamResources::default(), json_last_error: 0, json_last_error_msg: String::from("No error"), call_file: String::new(), @@ -391,6 +395,16 @@ impl ElephcEvalContext { self.pending_throw.take() } + /// Returns the eval-local stream resource table. + pub(crate) fn stream_resources(&self) -> &EvalStreamResources { + &self.streams + } + + /// Returns mutable access to the eval-local stream resource table. + pub(crate) fn stream_resources_mut(&mut self) -> &mut EvalStreamResources { + &mut self.streams + } + /// Clears the eval-local JSON error state after a successful JSON operation. pub fn clear_json_error(&mut self) { self.json_last_error = 0; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs index 4dd32f5314..9013a416bc 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -12,8 +12,10 @@ mod file_io; mod fnmatch; mod ops; mod path; +mod streams; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use ops::*; pub(in crate::interpreter) use path::*; +pub(in crate::interpreter) use streams::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs new file mode 100644 index 0000000000..7da1638f5a --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs @@ -0,0 +1,463 @@ +//! Purpose: +//! Implements eval-local file stream builtins backed by host file handles. +//! These builtins turn PHP resource cells into ids stored in the eval context's +//! stream table. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - Runtime resource payloads are zero-based; `get_resource_id()` exposes payload + 1. +//! - This module supports local file resources, not sockets, pipes, wrappers, or filters. + +use super::super::super::*; +use super::*; + +/// Evaluates PHP `fopen($filename, $mode, ...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fopen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let filename = eval_expr(&args[0], context, scope, values)?; + let mode = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_fopen_result(filename, mode, context, values) +} + +/// Opens a local file stream and returns a resource cell or PHP false. +pub(in crate::interpreter) fn eval_fopen_result( + filename: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let filename = eval_path_string(filename, values)?; + let mode = eval_stream_string(mode, values)?; + match context.stream_resources_mut().open_path(&filename, &mode) { + Some(id) => values.resource(id), + None => { + values.warning("Warning: fopen(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} + +/// Evaluates PHP `tmpfile()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_tmpfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_tmpfile_result(context, values) +} + +/// Creates an anonymous temporary file stream resource or returns PHP false. +pub(in crate::interpreter) fn eval_tmpfile_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match context.stream_resources_mut().open_tmpfile() { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates one unary stream builtin over an eval expression. +pub(in crate::interpreter) fn eval_builtin_unary_stream( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_unary_stream_result(name, stream, context, values) +} + +/// Evaluates a materialized unary stream builtin argument. +pub(in crate::interpreter) fn eval_unary_stream_result( + name: &str, + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + match name { + "fclose" => values.bool_value(context.stream_resources_mut().close(id)), + "feof" => values.bool_value(context.stream_resources().eof(id).unwrap_or(false)), + "fflush" => values.bool_value(context.stream_resources_mut().flush(id)), + "fsync" => values.bool_value(context.stream_resources_mut().sync_all(id)), + "fdatasync" => values.bool_value(context.stream_resources_mut().sync_data(id)), + "ftell" => match context.stream_resources_mut().tell(id) { + Some(position) => { + values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?) + } + None => values.bool_value(false), + }, + "rewind" => values.bool_value(context.stream_resources_mut().rewind(id)), + "fstat" => match context.stream_resources().metadata(id) { + Some(metadata) => eval_stat_metadata_array(&metadata, values), + None => values.bool_value(false), + }, + "stream_get_meta_data" => eval_stream_get_meta_data_result(id, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fread($stream, $length)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fread( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_fread_result(stream, length, context, values) +} + +/// Reads bytes from a materialized stream resource. +pub(in crate::interpreter) fn eval_fread_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_nonnegative_usize(length, values)?; + match context.stream_resources_mut().read(id, length) { + Some(bytes) => values.string_bytes_value(&bytes), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `fwrite($stream, $data)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fwrite( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_fwrite_result(stream, data, context, values) +} + +/// Writes bytes to a materialized stream resource. +pub(in crate::interpreter) fn eval_fwrite_result( + stream: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let data = values.string_bytes(data)?; + match context.stream_resources_mut().write(id, &data) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `fseek($stream, $offset, $whence = SEEK_SET)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fseek( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let offset = eval_expr(&args[1], context, scope, values)?; + let whence = match args.get(2) { + Some(whence) => Some(eval_expr(whence, context, scope, values)?), + None => None, + }; + eval_fseek_result(stream, offset, whence, context, values) +} + +/// Seeks a materialized stream and returns PHP's 0 or -1 status code. +pub(in crate::interpreter) fn eval_fseek_result( + stream: RuntimeCellHandle, + offset: RuntimeCellHandle, + whence: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let offset = eval_int_value(offset, values)?; + let whence = match whence { + Some(whence) => eval_int_value(whence, values)?, + None => 0, + }; + let status = if context.stream_resources_mut().seek(id, offset, whence) { + 0 + } else { + -1 + }; + values.int(status) +} + +/// Evaluates PHP `ftruncate($stream, $size)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_ftruncate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, size] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let size = eval_expr(size, context, scope, values)?; + eval_ftruncate_result(stream, size, context, values) +} + +/// Truncates a materialized stream resource. +pub(in crate::interpreter) fn eval_ftruncate_result( + stream: RuntimeCellHandle, + size: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let size = eval_int_value(size, values)?; + let Ok(size) = u64::try_from(size) else { + return values.bool_value(false); + }; + values.bool_value(context.stream_resources_mut().truncate(id, size)) +} + +/// Evaluates PHP `stream_get_contents($stream, $length = null, $offset = -1)`. +pub(in crate::interpreter) fn eval_builtin_stream_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = match args.get(1) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let offset = match args.get(2) { + Some(offset) => Some(eval_expr(offset, context, scope, values)?), + None => None, + }; + eval_stream_get_contents_result(stream, length, offset, context, values) +} + +/// Reads the remaining or bounded contents from a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_get_contents_result( + stream: RuntimeCellHandle, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_optional_stream_length(length, values)?; + let offset = eval_optional_stream_offset(offset, values)?; + match context + .stream_resources_mut() + .get_contents(id, length, offset) + { + Some(bytes) => values.string_bytes_value(&bytes), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `stream_copy_to_stream($from, $to, $length = null, $offset = -1)`. +pub(in crate::interpreter) fn eval_builtin_stream_copy_to_stream( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let from = eval_expr(&args[0], context, scope, values)?; + let to = eval_expr(&args[1], context, scope, values)?; + let length = match args.get(2) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let offset = match args.get(3) { + Some(offset) => Some(eval_expr(offset, context, scope, values)?), + None => None, + }; + eval_stream_copy_to_stream_result(from, to, length, offset, context, values) +} + +/// Copies bytes between two materialized stream resources. +pub(in crate::interpreter) fn eval_stream_copy_to_stream_result( + from: RuntimeCellHandle, + to: RuntimeCellHandle, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_stream_resource_id(from, values)?; + let to = eval_stream_resource_id(to, values)?; + let length = eval_optional_stream_length(length, values)?; + let offset = eval_optional_stream_offset(offset, values)?; + match context + .stream_resources_mut() + .copy_to_stream(from, to, length, offset) + { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} + +/// Builds PHP's stream metadata array for one eval-local stream resource. +fn eval_stream_get_meta_data_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(meta) = context.stream_resources().meta_data(id) else { + return values.bool_value(false); + }; + let mut result = values.assoc_new(9)?; + result = eval_stream_meta_set_bool(result, "timed_out", false, values)?; + result = eval_stream_meta_set_bool(result, "blocked", true, values)?; + result = eval_stream_meta_set_bool(result, "eof", meta.eof, values)?; + result = eval_stream_meta_set_string(result, "wrapper_type", "plainfile", values)?; + result = eval_stream_meta_set_string(result, "stream_type", "STDIO", values)?; + result = eval_stream_meta_set_string(result, "mode", &meta.mode, values)?; + result = eval_stream_meta_set_int(result, "unread_bytes", 0, values)?; + result = eval_stream_meta_set_bool(result, "seekable", true, values)?; + eval_stream_meta_set_string(result, "uri", &meta.uri, values) +} + +/// Converts a runtime resource cell into eval's zero-based stream id. +fn eval_stream_resource_id( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts a stream length argument into a non-negative `usize`. +fn eval_nonnegative_usize( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts an optional stream length where null and -1 mean "read all". +fn eval_optional_stream_length( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = value else { + return Ok(None); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(None); + } + let value = eval_int_value(value, values)?; + if value == -1 { + return Ok(None); + } + Ok(Some( + usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal)?, + )) +} + +/// Converts an optional absolute stream offset where null and -1 mean no seek. +fn eval_optional_stream_offset( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = value else { + return Ok(None); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(None); + } + let value = eval_int_value(value, values)?; + if value < 0 { + Ok(None) + } else { + Ok(Some(value)) + } +} + +/// Converts one runtime cell to a UTF-8 string for stream mode arguments. +fn eval_stream_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + Ok(String::from_utf8_lossy(&bytes).into_owned()) +} + +/// Inserts a boolean field into the stream metadata array. +fn eval_stream_meta_set_bool( + array: RuntimeCellHandle, + key: &str, + value: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.bool_value(value)?; + values.array_set(array, key, value) +} + +/// Inserts an integer field into the stream metadata array. +fn eval_stream_meta_set_int( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts a string field into the stream metadata array. +fn eval_stream_meta_set_string( + array: RuntimeCellHandle, + key: &str, + value: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string(value)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 581597787c..7892ddbb56 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -165,12 +165,26 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), + "fclose" + | "feof" + | "fflush" + | "fsync" + | "fdatasync" + | "ftell" + | "rewind" + | "fstat" + | "stream_get_meta_data" => Some(&["stream"]), "fnmatch" => Some(&["pattern", "filename", "flags"]), "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), + "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), + "fread" => Some(&["stream", "length"]), + "fseek" => Some(&["stream", "offset", "whence"]), + "ftruncate" => Some(&["stream", "size"]), + "fwrite" => Some(&["stream", "data"]), "function_exists" => Some(&["function"]), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), "gethostbyaddr" => Some(&["ip"]), @@ -243,6 +257,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), "stream_is_local" | "stream_supports_lock" => Some(&["stream"]), + "stream_copy_to_stream" => Some(&["from", "to", "length", "offset"]), + "stream_get_contents" => Some(&["stream", "length", "offset"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), @@ -255,7 +271,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "str_split" => Some(&["string", "length"]), "substr" => Some(&["string", "offset", "length"]), "substr_replace" => Some(&["string", "replace", "offset", "length"]), - "sys_get_temp_dir" | "time" => Some(&[]), + "sys_get_temp_dir" | "time" | "tmpfile" => Some(&[]), "tempnam" => Some(&["directory", "prefix"]), "touch" => Some(&["filename", "mtime", "atime"]), "chown" | "lchown" => Some(&["filename", "user"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index d7e4599569..a9c0517cee 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -15,7 +15,7 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { @@ -81,6 +81,20 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_file_put_contents_result(*filename, *data, values)? } + "fclose" + | "feof" + | "fflush" + | "fsync" + | "fdatasync" + | "ftell" + | "rewind" + | "fstat" + | "stream_get_meta_data" => { + let [stream] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unary_stream_result(name, *stream, context, values)? + } "filesize" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -100,6 +114,37 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "fopen" => { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_fopen_result(evaluated_args[0], evaluated_args[1], context, values)? + } + "fread" => { + let [stream, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_fread_result(*stream, *length, context, values)? + } + "fseek" => match evaluated_args { + [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values)?, + [stream, offset, whence] => { + eval_fseek_result(*stream, *offset, Some(*whence), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "ftruncate" => { + let [stream, size] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ftruncate_result(*stream, *size, context, values)? + } + "fwrite" => { + let [stream, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_fwrite_result(*stream, *data, context, values)? + } "stat" | "lstat" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -169,6 +214,37 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_stream_resolve_include_path_result(*filename, values)? } + "stream_copy_to_stream" => match evaluated_args { + [from, to] => { + eval_stream_copy_to_stream_result(*from, *to, None, None, context, values)? + } + [from, to, length] => { + eval_stream_copy_to_stream_result(*from, *to, Some(*length), None, context, values)? + } + [from, to, length, offset] => eval_stream_copy_to_stream_result( + *from, + *to, + Some(*length), + Some(*offset), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_get_contents" => match evaluated_args { + [stream] => eval_stream_get_contents_result(*stream, None, None, context, values)?, + [stream, length] => { + eval_stream_get_contents_result(*stream, Some(*length), None, context, values)? + } + [stream, length, offset] => eval_stream_get_contents_result( + *stream, + Some(*length), + Some(*offset), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "realpath_cache_get" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -193,6 +269,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_tempnam_result(*directory, *prefix, values)? } + "tmpfile" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_tmpfile_result(context, values)? + } "touch" => match evaluated_args { [filename] => eval_touch_result(*filename, None, None, values)?, [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index f074bac88a..d7b665e992 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -92,7 +92,11 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "exec" | "exp" | "explode" + | "fclose" + | "fdatasync" + | "feof" | "fdiv" + | "fflush" | "file" | "file_exists" | "fileatime" @@ -110,7 +114,15 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "floor" | "floatval" | "fmod" + | "fopen" + | "fread" + | "fseek" + | "fstat" + | "fsync" + | "ftell" + | "ftruncate" | "function_exists" + | "fwrite" | "gethostbyaddr" | "gethostbyname" | "gethostname" @@ -233,6 +245,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "realpath_cache_get" | "realpath_cache_size" | "rename" + | "rewind" | "rsort" | "rtrim" | "round" @@ -253,7 +266,10 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "sscanf" | "sprintf" | "strcasecmp" + | "stream_copy_to_stream" + | "stream_get_contents" | "stream_get_filters" + | "stream_get_meta_data" | "stream_get_transports" | "stream_get_wrappers" | "stream_is_local" @@ -290,6 +306,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "touch" | "trait_exists" | "trim" + | "tmpfile" | "substr_replace" | "ucfirst" | "ucwords" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index c1a66f9554..acbc2bd635 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -459,9 +459,23 @@ pub(in crate::interpreter) fn eval_positional_expr_call( | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), + "fclose" + | "feof" + | "fflush" + | "fsync" + | "fdatasync" + | "ftell" + | "rewind" + | "fstat" + | "stream_get_meta_data" => eval_builtin_unary_stream(name, args, context, scope, values), "filesize" => eval_builtin_filesize(args, context, scope, values), "filetype" => eval_builtin_filetype(args, context, scope, values), "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), + "fopen" => eval_builtin_fopen(args, context, scope, values), + "fread" => eval_builtin_fread(args, context, scope, values), + "fseek" => eval_builtin_fseek(args, context, scope, values), + "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), + "fwrite" => eval_builtin_fwrite(args, context, scope, values), "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { @@ -568,6 +582,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "tempnam" => eval_builtin_tempnam(args, context, scope, values), "time" => eval_builtin_time(args, values), "touch" => eval_builtin_touch(args, context, scope, values), + "tmpfile" => eval_builtin_tmpfile(args, context, values), "stream_is_local" | "stream_supports_lock" => { eval_builtin_stream_bool_predicate(name, args, context, scope, values) } @@ -577,6 +592,8 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_resolve_include_path" => { eval_builtin_stream_resolve_include_path(args, context, scope, values) } + "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), + "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), "strtotime" => eval_builtin_strtotime(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 3e0bbbc307..758bae48d7 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -162,6 +162,9 @@ pub trait RuntimeValueOps { /// Creates a runtime int cell. fn int(&mut self, value: i64) -> Result; + /// Creates a runtime resource cell with a zero-based native resource payload. + fn resource(&mut self, value: i64) -> Result; + /// Creates a runtime float cell. fn float(&mut self, value: f64) -> Result; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs new file mode 100644 index 0000000000..1015194de6 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Interpreter tests for eval local file stream resource builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases use process-unique local files and clean them before and after execution. +//! - Stream resources are eval-owned fake/runtime cells whose ids map into context state. + +use super::super::*; +use super::support::*; + +/// Verifies eval file stream resources support open/read/write/seek/stat/close operations. +#[test] +fn execute_program_dispatches_file_stream_builtins() { + let pid = std::process::id(); + let file = format!("elephc_eval_stream_file_{pid}.txt"); + let copy = format!("elephc_eval_stream_copy_{pid}.txt"); + let call = format!("elephc_eval_stream_call_{pid}.txt"); + let source = format!( + r#"$h = fopen(filename: "{file}", mode: "w+"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo get_resource_type($h) === "stream" ? "rtype" : "bad"; echo ":"; +echo get_resource_id($h) >= 1 ? "rid" : "bad"; echo ":"; +echo fwrite($h, "abcdef") === 6 ? "write" : "bad"; echo ":"; +echo ftell($h) === 6 ? "tell" : "bad"; echo ":"; +echo rewind($h) ? "rewind" : "bad"; echo ":"; +echo fread($h, 2) === "ab" ? "read" : "bad"; echo ":"; +echo fseek($h, 1) === 0 ? "seek" : "bad"; echo ":"; +echo stream_get_contents($h, 3) === "bcd" ? "bounded" : "bad"; echo ":"; +rewind($h); +echo stream_get_contents($h) === "abcdef" ? "contents" : "bad"; echo ":"; +echo feof($h) ? "eof" : "bad"; echo ":"; +$meta = stream_get_meta_data($h); +echo $meta["wrapper_type"] === "plainfile" && $meta["stream_type"] === "STDIO" && $meta["mode"] === "w+" ? "meta" : "bad"; echo ":"; +echo ftruncate($h, 3) ? "truncate" : "bad"; echo ":"; +$stat = fstat($h); +echo $stat["size"] === 3 ? "fstat" : "bad"; echo ":"; +echo fflush($h) && fsync($h) && fdatasync($h) ? "sync" : "bad"; echo ":"; +echo fclose($h) ? "close" : "bad"; echo ":"; +echo file_get_contents("{file}") === "abc" ? "truncated" : "bad"; echo ":"; +$src = fopen("{file}", "r"); +$dst = fopen("{copy}", "w+"); +echo stream_copy_to_stream($src, $dst, null, 1) === 2 ? "copy" : "bad"; echo ":"; +rewind($dst); +echo stream_get_contents($dst) === "bc" ? "copied" : "bad"; echo ":"; +fclose($src); +fclose($dst); +$tmp = tmpfile(); +echo is_resource($tmp) ? "tmp" : "bad"; echo ":"; +fwrite($tmp, "xy"); +rewind($tmp); +echo fread($tmp, 2) === "xy" ? "tmpread" : "bad"; echo ":"; +fclose($tmp); +$call = call_user_func_array("fopen", ["filename" => "{call}", "mode" => "w+"]); +echo call_user_func_array("fwrite", ["stream" => $call, "data" => "zz"]) === 2 ? "callwrite" : "bad"; echo ":"; +call_user_func("rewind", $call); +echo call_user_func("fread", $call, 2) === "zz" ? "callread" : "bad"; echo ":"; +echo call_user_func("fclose", $call) ? "callclose" : "bad"; echo ":"; +echo unlink("{file}") && unlink("{copy}") && unlink("{call}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("fopen"); echo function_exists("fclose"); echo function_exists("fread"); +echo function_exists("fwrite"); echo function_exists("feof"); echo function_exists("fflush"); +echo function_exists("ftell"); echo function_exists("fseek"); echo function_exists("rewind"); +echo function_exists("ftruncate"); echo function_exists("fsync"); echo function_exists("fdatasync"); +echo function_exists("fstat"); echo function_exists("stream_get_contents"); +echo function_exists("stream_copy_to_stream"); echo function_exists("stream_get_meta_data"); +echo function_exists("tmpfile"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&file, ©, &call] { + let _ = std::fs::remove_file(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&file, ©, &call] { + let _ = std::fs::remove_file(path); + } + assert_eq!( + values.output, + "open:rtype:rid:write:tell:rewind:read:seek:bounded:contents:eof:meta:truncate:fstat:sync:close:truncated:copy:copied:tmp:tmpread:callwrite:callread:callclose:cleanup:11111111111111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 3197f7b34c..c2d70d5120 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -14,6 +14,7 @@ mod array_literals; mod builtins_arrays_core; mod builtins_arrays_iterators; mod builtins_arrays_sets; +mod builtins_file_streams; mod builtins_filesystem_metadata; mod builtins_filesystem_ops; mod builtins_json; diff --git a/crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs index 1df3cc6ec2..a69340e6a2 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs @@ -43,6 +43,10 @@ impl FakeOps { pub(super) fn runtime_int(&mut self, value: i64) -> Result { Ok(self.alloc(FakeValue::Int(value))) } + /// Creates a fake resource cell. + pub(super) fn runtime_resource(&mut self, value: i64) -> Result { + Ok(self.alloc(FakeValue::Resource(value))) + } /// Creates a fake float cell. pub(super) fn runtime_float(&mut self, value: f64) -> Result { Ok(self.alloc(FakeValue::Float(value))) diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index e472fd4609..5ad8ad0f5b 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -187,6 +187,10 @@ impl RuntimeValueOps for FakeOps { fn int(&mut self, value: i64) -> Result { self.runtime_int(value) } + /// Creates a fake resource cell. + fn resource(&mut self, value: i64) -> Result { + self.runtime_resource(value) + } /// Creates a fake float cell. fn float(&mut self, value: f64) -> Result { self.runtime_float(value) diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-eval/src/lib.rs index 96a3d69137..1077e78add 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-eval/src/lib.rs @@ -23,6 +23,7 @@ pub mod lower; pub mod parser; pub mod runtime_hooks; pub mod scope; +mod stream_resources; pub mod value; pub use ffi::*; diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index c8095b9b1d..b7c2f5794a 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -92,6 +92,7 @@ unsafe extern "C" { pub(super) fn __elephc_eval_value_null() -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_bool(value: u64) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_int(value: i64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_resource(value: i64) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_float(value: f64) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_string(ptr: *const u8, len: u64) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_cast_int(value: *mut RuntimeCell) -> *mut RuntimeCell; diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 9d20e40716..4b359c4309 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -303,6 +303,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_int(value) }) } + /// Creates a boxed resource Mixed cell through the generated runtime wrapper. + fn resource(&mut self, value: i64) -> Result { + Self::handle(unsafe { __elephc_eval_value_resource(value) }) + } + /// Creates a boxed float Mixed cell through the generated runtime wrapper. fn float(&mut self, value: f64) -> Result { Self::handle(unsafe { __elephc_eval_value_float(value) }) diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs new file mode 100644 index 0000000000..5e196d89b9 --- /dev/null +++ b/crates/elephc-eval/src/stream_resources.rs @@ -0,0 +1,326 @@ +//! Purpose: +//! Owns eval-local stream resource storage backed by host file handles. +//! Builtin implementations use this table while runtime Mixed cells only carry +//! a numeric resource id. +//! +//! Called from: +//! - `crate::context::ElephcEvalContext` stream-resource accessors. +//! - `crate::interpreter::builtins::filesystem` stream builtin helpers. +//! +//! Key details: +//! - Resource ids are zero-based runtime payloads; PHP display ids are payload + 1. +//! - File handles are process-local to eval and are not visible across the C ABI. + +use std::collections::HashMap; +use std::fs::{File, Metadata, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::path::PathBuf; + +/// Eval-owned table of local file streams keyed by runtime resource payload. +#[derive(Default)] +pub(crate) struct EvalStreamResources { + next_id: i64, + streams: HashMap, +} + +impl EvalStreamResources { + /// Opens a local path using PHP's common `fopen()` mode strings. + pub(crate) fn open_path(&mut self, path: &str, mode: &str) -> Option { + let mode = EvalOpenMode::parse(mode)?; + let file = mode.open(path).ok()?; + Some(self.insert(EvalFileStream::new(file, path.to_string(), mode.label))) + } + + /// Opens an anonymous temporary file and returns its resource id. + pub(crate) fn open_tmpfile(&mut self) -> Option { + let path = eval_tmpfile_path(); + let file = OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + .ok()?; + let _ = std::fs::remove_file(&path); + Some(self.insert(EvalFileStream::new( + file, + path.to_string_lossy().into_owned(), + "w+".to_string(), + ))) + } + + /// Removes a stream resource from the table, closing its file handle. + pub(crate) fn close(&mut self, id: i64) -> bool { + self.streams.remove(&id).is_some() + } + + /// Reads up to `length` bytes from a stream resource. + pub(crate) fn read(&mut self, id: i64, length: usize) -> Option> { + let stream = self.streams.get_mut(&id)?; + let mut buffer = vec![0_u8; length]; + let read = stream.file.read(&mut buffer).ok()?; + buffer.truncate(read); + stream.eof = read == 0 || read < length; + Some(buffer) + } + + /// Writes all provided bytes to a stream resource and returns the written byte count. + pub(crate) fn write(&mut self, id: i64, data: &[u8]) -> Option { + let stream = self.streams.get_mut(&id)?; + let written = stream.file.write(data).ok()?; + stream.eof = false; + Some(written) + } + + /// Flushes buffered stream data to the host file handle. + pub(crate) fn flush(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.flush().is_ok()) + } + + /// Synchronizes stream data and metadata to storage. + pub(crate) fn sync_all(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.sync_all().is_ok()) + } + + /// Synchronizes stream data to storage where the host platform supports it. + pub(crate) fn sync_data(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.sync_data().is_ok()) + } + + /// Returns whether the stream has reached EOF after the last read attempt. + pub(crate) fn eof(&self, id: i64) -> Option { + self.streams.get(&id).map(|stream| stream.eof) + } + + /// Returns the current stream cursor offset. + pub(crate) fn tell(&mut self, id: i64) -> Option { + self.streams.get_mut(&id)?.file.stream_position().ok() + } + + /// Moves the stream cursor according to PHP `fseek()` whence values. + pub(crate) fn seek(&mut self, id: i64, offset: i64, whence: i64) -> bool { + let Some(stream) = self.streams.get_mut(&id) else { + return false; + }; + let position = match whence { + 0 => SeekFrom::Start(u64::try_from(offset).unwrap_or(u64::MAX)), + 1 => SeekFrom::Current(offset), + 2 => SeekFrom::End(offset), + _ => return false, + }; + stream.eof = false; + stream.file.seek(position).is_ok() + } + + /// Rewinds a stream to the beginning. + pub(crate) fn rewind(&mut self, id: i64) -> bool { + self.seek(id, 0, 0) + } + + /// Truncates a stream to the requested byte length. + pub(crate) fn truncate(&mut self, id: i64, size: u64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.set_len(size).is_ok()) + } + + /// Returns host metadata for a stream's backing file handle. + pub(crate) fn metadata(&self, id: i64) -> Option { + self.streams + .get(&id) + .and_then(|stream| stream.file.metadata().ok()) + } + + /// Reads a full or bounded byte sequence from the stream, with optional offset seek. + pub(crate) fn get_contents( + &mut self, + id: i64, + length: Option, + offset: Option, + ) -> Option> { + if let Some(offset) = offset { + if !self.seek(id, offset, 0) { + return None; + } + } + match length { + Some(length) => self.read(id, length), + None => { + let stream = self.streams.get_mut(&id)?; + let mut bytes = Vec::new(); + stream.file.read_to_end(&mut bytes).ok()?; + stream.eof = true; + Some(bytes) + } + } + } + + /// Copies bytes between two streams and returns the copied byte count. + pub(crate) fn copy_to_stream( + &mut self, + from: i64, + to: i64, + length: Option, + offset: Option, + ) -> Option { + let bytes = self.get_contents(from, length, offset)?; + self.write(to, &bytes) + } + + /// Returns metadata fields used by PHP `stream_get_meta_data()`. + pub(crate) fn meta_data(&self, id: i64) -> Option { + let stream = self.streams.get(&id)?; + Some(EvalStreamMetaData { + eof: stream.eof, + mode: stream.mode.clone(), + uri: stream.uri.clone(), + }) + } + + /// Inserts a file stream and returns the assigned zero-based resource payload. + fn insert(&mut self, stream: EvalFileStream) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.streams.insert(id, stream); + id + } +} + +/// PHP-visible metadata for one eval stream resource. +pub(crate) struct EvalStreamMetaData { + pub(crate) eof: bool, + pub(crate) mode: String, + pub(crate) uri: String, +} + +/// File stream stored behind one eval resource id. +struct EvalFileStream { + file: File, + uri: String, + mode: String, + eof: bool, +} + +impl EvalFileStream { + /// Creates a tracked stream around a host file handle. + fn new(file: File, uri: String, mode: String) -> Self { + Self { + file, + uri, + mode, + eof: false, + } + } +} + +/// Parsed PHP fopen mode used to configure `OpenOptions`. +struct EvalOpenMode { + read: bool, + write: bool, + append: bool, + truncate: bool, + create: bool, + create_new: bool, + label: String, +} + +impl EvalOpenMode { + /// Parses PHP's common fopen mode grammar, ignoring binary/text markers. + fn parse(mode: &str) -> Option { + let mut chars = mode.chars(); + let first = chars.next()?; + let plus = mode.contains('+'); + if !mode + .chars() + .all(|ch| matches!(ch, 'r' | 'w' | 'a' | 'x' | 'c' | '+' | 'b' | 't' | 'e')) + { + return None; + } + let mut mode = match first { + 'r' => Self { + read: true, + write: plus, + append: false, + truncate: false, + create: false, + create_new: false, + label: if plus { "r+" } else { "r" }.to_string(), + }, + 'w' => Self { + read: plus, + write: true, + append: false, + truncate: true, + create: true, + create_new: false, + label: if plus { "w+" } else { "w" }.to_string(), + }, + 'a' => Self { + read: plus, + write: true, + append: true, + truncate: false, + create: true, + create_new: false, + label: if plus { "a+" } else { "a" }.to_string(), + }, + 'x' => Self { + read: plus, + write: true, + append: false, + truncate: false, + create: false, + create_new: true, + label: if plus { "x+" } else { "x" }.to_string(), + }, + 'c' => Self { + read: plus, + write: true, + append: false, + truncate: false, + create: true, + create_new: false, + label: if plus { "c+" } else { "c" }.to_string(), + }, + _ => return None, + }; + mode.write = mode.write || plus; + Some(mode) + } + + /// Opens a path with the parsed stream mode. + fn open(&self, path: &str) -> std::io::Result { + OpenOptions::new() + .read(self.read) + .write(self.write) + .append(self.append) + .truncate(self.truncate) + .create(self.create) + .create_new(self.create_new) + .open(path) + } +} + +/// Builds a unique temporary path for eval `tmpfile()`. +fn eval_tmpfile_path() -> PathBuf { + let mut path = std::env::temp_dir(); + path.push(format!( + "elephc-eval-tmpfile-{}-{}", + std::process::id(), + eval_tmpfile_nonce() + )); + path +} + +/// Returns a monotonic-ish nonce for temporary file names. +fn eval_tmpfile_nonce() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0) +} diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index d392a066e7..9d43468c20 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -775,6 +775,12 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word emitter.instruction("b __rt_mixed_from_value"); // box the integer payload and return to Rust + label_c_global(emitter, "__elephc_eval_value_resource"); + emitter.instruction("mov x1, x0"); // move the C resource id into the mixed payload slot + emitter.instruction("mov x0, #9"); // runtime tag 9 = resource + emitter.instruction("mov x2, xzr"); // resource payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box the resource payload and return to Rust + label_c_global(emitter, "__elephc_eval_value_float"); emitter.instruction("fmov x1, d0"); // move the C double bits into the mixed payload slot emitter.instruction("mov x0, #2"); // runtime tag 2 = double @@ -2142,6 +2148,11 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("xor esi, esi"); // integer payloads do not use a high word emitter.instruction("jmp __rt_mixed_from_value"); // box the C integer payload in rdi and return + label_c_global(emitter, "__elephc_eval_value_resource"); + emitter.instruction("mov eax, 9"); // runtime tag 9 = resource, with C id already in rdi + emitter.instruction("xor esi, esi"); // resource payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box the resource payload and return to Rust + label_c_global(emitter, "__elephc_eval_value_float"); emitter.instruction("movq rdi, xmm0"); // move the C double bits into mixed value_lo emitter.instruction("mov eax, 2"); // runtime tag 2 = double From ea21d1b334531b07bd72d0791b6e35431f5790b2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:18:16 +0200 Subject: [PATCH 0307/1208] Add eval stream line builtins --- .../builtins/filesystem/streams.rs | 74 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 4 + .../builtins/registry/dispatch/filesystem.rs | 12 +++ .../interpreter/builtins/registry/names.rs | 4 + .../src/interpreter/expressions.rs | 4 + .../tests/builtins_file_streams.rs | 52 +++++++++++++ crates/elephc-eval/src/stream_resources.rs | 33 +++++++++ 7 files changed, 183 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs index 7da1638f5a..5fa4a12fb1 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs @@ -98,8 +98,22 @@ pub(in crate::interpreter) fn eval_unary_stream_result( let id = eval_stream_resource_id(stream, values)?; match name { "fclose" => values.bool_value(context.stream_resources_mut().close(id)), + "fgetc" => match context.stream_resources_mut().read(id, 1) { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + }, + "fgets" => match context + .stream_resources_mut() + .read_line(id, usize::MAX, None, true, true) + { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + }, "feof" => values.bool_value(context.stream_resources().eof(id).unwrap_or(false)), "fflush" => values.bool_value(context.stream_resources_mut().flush(id)), + "fpassthru" => eval_fpassthru_result(id, context, values), "fsync" => values.bool_value(context.stream_resources_mut().sync_all(id)), "fdatasync" => values.bool_value(context.stream_resources_mut().sync_data(id)), "ftell" => match context.stream_resources_mut().tell(id) { @@ -118,6 +132,21 @@ pub(in crate::interpreter) fn eval_unary_stream_result( } } +/// Streams all remaining bytes to eval output and returns the emitted byte count. +fn eval_fpassthru_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(bytes) = context.stream_resources_mut().get_contents(id, None, None) else { + return values.bool_value(false); + }; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(len) +} + /// Evaluates PHP `fread($stream, $length)` over eval expressions. pub(in crate::interpreter) fn eval_builtin_fread( args: &[EvalExpr], @@ -314,6 +343,51 @@ pub(in crate::interpreter) fn eval_builtin_stream_copy_to_stream( eval_stream_copy_to_stream_result(from, to, length, offset, context, values) } +/// Evaluates PHP `stream_get_line($stream, $length, $ending = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_get_line( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = eval_expr(&args[1], context, scope, values)?; + let ending = match args.get(2) { + Some(ending) => Some(eval_expr(ending, context, scope, values)?), + None => None, + }; + eval_stream_get_line_result(stream, length, ending, context, values) +} + +/// Reads one line-like byte sequence from a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_get_line_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + ending: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_nonnegative_usize(length, values)?; + let ending = match ending { + Some(ending) if values.type_tag(ending)? != EVAL_TAG_NULL => { + Some(values.string_bytes(ending)?) + } + _ => None, + }; + match context + .stream_resources_mut() + .read_line(id, length, ending.as_deref(), false, false) + { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } +} + /// Copies bytes between two materialized stream resources. pub(in crate::interpreter) fn eval_stream_copy_to_stream_result( from: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 7892ddbb56..411afd1c80 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -166,8 +166,11 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "explode" => Some(&["separator", "string"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "fclose" + | "fgetc" + | "fgets" | "feof" | "fflush" + | "fpassthru" | "fsync" | "fdatasync" | "ftell" @@ -259,6 +262,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_is_local" | "stream_supports_lock" => Some(&["stream"]), "stream_copy_to_stream" => Some(&["from", "to", "length", "offset"]), "stream_get_contents" => Some(&["stream", "length", "offset"]), + "stream_get_line" => Some(&["stream", "length", "ending"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index a9c0517cee..91e6b9a628 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -82,8 +82,11 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( eval_file_put_contents_result(*filename, *data, values)? } "fclose" + | "fgetc" + | "fgets" | "feof" | "fflush" + | "fpassthru" | "fsync" | "fdatasync" | "ftell" @@ -245,6 +248,15 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( )?, _ => return Err(EvalStatus::RuntimeFatal), }, + "stream_get_line" => match evaluated_args { + [stream, length] => { + eval_stream_get_line_result(*stream, *length, None, context, values)? + } + [stream, length, ending] => { + eval_stream_get_line_result(*stream, *length, Some(*ending), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "realpath_cache_get" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index d7b665e992..3796555c15 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -94,6 +94,8 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "explode" | "fclose" | "fdatasync" + | "fgetc" + | "fgets" | "feof" | "fdiv" | "fflush" @@ -115,6 +117,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "floatval" | "fmod" | "fopen" + | "fpassthru" | "fread" | "fseek" | "fstat" @@ -269,6 +272,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "stream_copy_to_stream" | "stream_get_contents" | "stream_get_filters" + | "stream_get_line" | "stream_get_meta_data" | "stream_get_transports" | "stream_get_wrappers" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index acbc2bd635..a479c3e807 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -460,8 +460,11 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "fclose" + | "fgetc" + | "fgets" | "feof" | "fflush" + | "fpassthru" | "fsync" | "fdatasync" | "ftell" @@ -594,6 +597,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), + "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), "strtotime" => eval_builtin_strtotime(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs index 1015194de6..b9834bec36 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs @@ -86,3 +86,55 @@ return true;"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval line, character, and passthrough stream builtins. +#[test] +fn execute_program_dispatches_file_stream_line_builtins() { + let pid = std::process::id(); + let file = format!("elephc_eval_stream_lines_{pid}.txt"); + let ending = format!("elephc_eval_stream_ending_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "a\nbc\nxyz"); +$h = fopen("{file}", "r"); +echo fgetc($h) === "a" ? "char" : "bad"; echo ":"; +echo fgets($h) === "\n" ? "line1" : "bad"; echo ":"; +echo fgets($h) === "bc\n" ? "line2" : "bad"; echo ":"; +echo stream_get_line($h, 2) === "xy" ? "getline" : "bad"; echo ":"; +echo "["; +$passed = fpassthru($h); +echo "]"; +echo $passed === 1 ? "passthru" : "bad"; echo ":"; +echo feof($h) ? "eof" : "bad"; echo ":"; +fclose($h); +file_put_contents("{ending}", "leftENDright"); +$e = fopen("{ending}", "r"); +echo stream_get_line($e, 20, "END") === "left" ? "ending" : "bad"; echo ":"; +echo stream_get_contents($e) === "right" ? "after" : "bad"; echo ":"; +fclose($e); +$call = call_user_func("fopen", "{file}", "r"); +echo call_user_func("fgetc", $call) === "a" ? "callchar" : "bad"; echo ":"; +echo call_user_func_array("stream_get_line", ["stream" => $call, "length" => 2]) === "\nb" ? "callline" : "bad"; echo ":"; +call_user_func("fclose", $call); +echo unlink("{file}") && unlink("{ending}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("fgetc"); echo function_exists("fgets"); echo function_exists("fpassthru"); +echo function_exists("stream_get_line"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&file, &ending] { + let _ = std::fs::remove_file(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&file, &ending] { + let _ = std::fs::remove_file(path); + } + assert_eq!( + values.output, + "char:line1:line2:getline:[z]passthru:eof:ending:after:callchar:callline:cleanup:1111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index 5e196d89b9..bc67df1563 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -63,6 +63,39 @@ impl EvalStreamResources { Some(buffer) } + /// Reads one stream line up to a limit, newline, or custom delimiter. + pub(crate) fn read_line( + &mut self, + id: i64, + length: usize, + ending: Option<&[u8]>, + include_ending: bool, + stop_at_newline: bool, + ) -> Option> { + let stream = self.streams.get_mut(&id)?; + let mut output = Vec::new(); + let mut byte = [0_u8; 1]; + while output.len() < length { + let read = stream.file.read(&mut byte).ok()?; + if read == 0 { + stream.eof = true; + break; + } + output.push(byte[0]); + if let Some(ending) = ending { + if !ending.is_empty() && output.ends_with(ending) { + if !include_ending { + output.truncate(output.len().saturating_sub(ending.len())); + } + break; + } + } else if stop_at_newline && byte[0] == b'\n' { + break; + } + } + Some(output) + } + /// Writes all provided bytes to a stream resource and returns the written byte count. pub(crate) fn write(&mut self, id: i64, data: &[u8]) -> Option { let stream = self.streams.get_mut(&id)?; From 7d7d5a073377a1ea47dac293a9b6f79b1b6a634f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:20:27 +0200 Subject: [PATCH 0308/1208] Add eval formatted stream writes --- .../builtins/filesystem/streams.rs | 64 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 2 + .../builtins/registry/dispatch/filesystem.rs | 15 +++++ .../interpreter/builtins/registry/names.rs | 2 + .../src/interpreter/expressions.rs | 2 + .../tests/builtins_file_streams.rs | 19 ++++-- 6 files changed, 99 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs index 5fa4a12fb1..6625bd234c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs @@ -207,6 +207,70 @@ pub(in crate::interpreter) fn eval_fwrite_result( } } +/// Evaluates PHP `fprintf($stream, $format, ...$values)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + let mut format_args = Vec::with_capacity(args.len().saturating_sub(2)); + for arg in &args[2..] { + format_args.push(eval_expr(arg, context, scope, values)?); + } + eval_fprintf_result(stream, format, &format_args, context, values) +} + +/// Formats and writes `fprintf()` arguments to a materialized stream resource. +pub(in crate::interpreter) fn eval_fprintf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + format_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let format = values.string_bytes(format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + match context.stream_resources_mut().write(id, &output) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `vfprintf($stream, $format, $values)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_vfprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, format, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let format = eval_expr(format, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_vfprintf_result(stream, format, array, context, values) +} + +/// Formats and writes `vfprintf()` array arguments to a materialized stream resource. +pub(in crate::interpreter) fn eval_vfprintf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let format_args = eval_sprintf_argument_array_values(array, values)?; + eval_fprintf_result(stream, format, &format_args, context, values) +} + /// Evaluates PHP `fseek($stream, $offset, $whence = SEEK_SET)` over eval expressions. pub(in crate::interpreter) fn eval_builtin_fseek( args: &[EvalExpr], diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 411afd1c80..c54d340b3d 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -184,6 +184,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), + "fprintf" => Some(&["stream", "format", "values"]), "fread" => Some(&["stream", "length"]), "fseek" => Some(&["stream", "offset", "whence"]), "ftruncate" => Some(&["stream", "size"]), @@ -287,6 +288,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "ucwords" => Some(&["string", "separators"]), "umask" => Some(&["mask"]), "usleep" => Some(&["microseconds"]), + "vfprintf" => Some(&["stream", "format", "values"]), "vsprintf" | "vprintf" => Some(&["format", "values"]), "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), _ => None, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index 91e6b9a628..f917d7dc4a 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -123,6 +123,15 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } eval_fopen_result(evaluated_args[0], evaluated_args[1], context, values)? } + "fprintf" => { + let Some((stream, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let Some((format, format_args)) = rest.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_fprintf_result(*stream, *format, format_args, context, values)? + } "fread" => { let [stream, length] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -287,6 +296,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } eval_tmpfile_result(context, values)? } + "vfprintf" => { + let [stream, format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_vfprintf_result(*stream, *format, *array, context, values)? + } "touch" => match evaluated_args { [filename] => eval_touch_result(*filename, None, None, values)?, [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 3796555c15..0a3a32afb4 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -118,6 +118,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "fmod" | "fopen" | "fpassthru" + | "fprintf" | "fread" | "fseek" | "fstat" @@ -323,6 +324,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "usort" | "usleep" | "var_dump" + | "vfprintf" | "printf" | "vprintf" | "vsprintf" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index a479c3e807..4eb2093f13 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -475,6 +475,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "filetype" => eval_builtin_filetype(args, context, scope, values), "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), "fopen" => eval_builtin_fopen(args, context, scope, values), + "fprintf" => eval_builtin_fprintf(args, context, scope, values), "fread" => eval_builtin_fread(args, context, scope, values), "fseek" => eval_builtin_fseek(args, context, scope, values), "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), @@ -625,6 +626,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "umask" => eval_builtin_umask(args, context, scope, values), "usleep" => eval_builtin_usleep(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), + "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs index b9834bec36..c5bf115e7c 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs @@ -93,6 +93,7 @@ fn execute_program_dispatches_file_stream_line_builtins() { let pid = std::process::id(); let file = format!("elephc_eval_stream_lines_{pid}.txt"); let ending = format!("elephc_eval_stream_ending_{pid}.txt"); + let formatted = format!("elephc_eval_stream_formatted_{pid}.txt"); let source = format!( r#"file_put_contents("{file}", "a\nbc\nxyz"); $h = fopen("{file}", "r"); @@ -115,13 +116,21 @@ $call = call_user_func("fopen", "{file}", "r"); echo call_user_func("fgetc", $call) === "a" ? "callchar" : "bad"; echo ":"; echo call_user_func_array("stream_get_line", ["stream" => $call, "length" => 2]) === "\nb" ? "callline" : "bad"; echo ":"; call_user_func("fclose", $call); -echo unlink("{file}") && unlink("{ending}") ? "cleanup" : "bad"; echo ":"; +$fmt = fopen("{formatted}", "w+"); +echo fprintf($fmt, "%s-%d", "n", 7) === 3 ? "fprintf" : "bad"; echo ":"; +echo vfprintf($fmt, "-%s", ["x"]) === 2 ? "vfprintf" : "bad"; echo ":"; +rewind($fmt); +echo stream_get_contents($fmt) === "n-7-x" ? "formatted" : "bad"; echo ":"; +echo call_user_func("fprintf", $fmt, "-%s", "c") === 2 ? "callfprintf" : "bad"; echo ":"; +echo call_user_func_array("vfprintf", ["stream" => $fmt, "format" => "-%d", "values" => [5]]) === 2 ? "callvfprintf" : "bad"; echo ":"; +fclose($fmt); +echo unlink("{file}") && unlink("{ending}") && unlink("{formatted}") ? "cleanup" : "bad"; echo ":"; echo function_exists("fgetc"); echo function_exists("fgets"); echo function_exists("fpassthru"); -echo function_exists("stream_get_line"); +echo function_exists("fprintf"); echo function_exists("stream_get_line"); echo function_exists("vfprintf"); return true;"# ); let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - for path in [&file, &ending] { + for path in [&file, &ending, &formatted] { let _ = std::fs::remove_file(path); } let mut scope = ElephcEvalScope::new(); @@ -129,12 +138,12 @@ return true;"# let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - for path in [&file, &ending] { + for path in [&file, &ending, &formatted] { let _ = std::fs::remove_file(path); } assert_eq!( values.output, - "char:line1:line2:getline:[z]passthru:eof:ending:after:callchar:callline:cleanup:1111" + "char:line1:line2:getline:[z]passthru:eof:ending:after:callchar:callline:fprintf:vfprintf:formatted:callfprintf:callvfprintf:cleanup:111111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } From 5b2c773fddc9a39ca93fdc7e616f8541c7727c88 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:23:03 +0200 Subject: [PATCH 0309/1208] Add eval CSV stream builtins --- .../builtins/filesystem/streams.rs | 202 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 2 + .../builtins/registry/dispatch/filesystem.rs | 25 +++ .../interpreter/builtins/registry/names.rs | 2 + .../src/interpreter/expressions.rs | 2 + .../tests/builtins_file_streams.rs | 50 +++++ 6 files changed, 283 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs index 6625bd234c..09aaf500c5 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs @@ -162,6 +162,53 @@ pub(in crate::interpreter) fn eval_builtin_fread( eval_fread_result(stream, length, context, values) } +/// Evaluates PHP `fgetcsv($stream, $length = null, $separator = ",")`. +pub(in crate::interpreter) fn eval_builtin_fgetcsv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = match args.get(1) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let separator = match args.get(2) { + Some(separator) => Some(eval_expr(separator, context, scope, values)?), + None => None, + }; + eval_fgetcsv_result(stream, length, separator, context, values) +} + +/// Reads and parses one CSV record from a materialized stream resource. +pub(in crate::interpreter) fn eval_fgetcsv_result( + stream: RuntimeCellHandle, + length: Option, + separator: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_optional_stream_length(length, values)?.unwrap_or(usize::MAX); + let separator = eval_optional_delimiter(separator, b',', values)?; + let Some(mut line) = context + .stream_resources_mut() + .read_line(id, length, None, true, true) + else { + return values.bool_value(false); + }; + if line.is_empty() { + return values.bool_value(false); + } + eval_trim_csv_line_end(&mut line); + let fields = eval_parse_csv_record(&line, separator, b'"'); + eval_csv_fields_array(&fields, values) +} + /// Reads bytes from a materialized stream resource. pub(in crate::interpreter) fn eval_fread_result( stream: RuntimeCellHandle, @@ -207,6 +254,48 @@ pub(in crate::interpreter) fn eval_fwrite_result( } } +/// Evaluates PHP `fputcsv($stream, $fields, $separator = ",", $enclosure = "\"")`. +pub(in crate::interpreter) fn eval_builtin_fputcsv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let fields = eval_expr(&args[1], context, scope, values)?; + let separator = match args.get(2) { + Some(separator) => Some(eval_expr(separator, context, scope, values)?), + None => None, + }; + let enclosure = match args.get(3) { + Some(enclosure) => Some(eval_expr(enclosure, context, scope, values)?), + None => None, + }; + eval_fputcsv_result(stream, fields, separator, enclosure, context, values) +} + +/// Formats and writes one CSV record to a materialized stream resource. +pub(in crate::interpreter) fn eval_fputcsv_result( + stream: RuntimeCellHandle, + fields: RuntimeCellHandle, + separator: Option, + enclosure: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let separator = eval_optional_delimiter(separator, b',', values)?; + let enclosure = eval_optional_delimiter(enclosure, b'"', values)?; + let output = eval_format_csv_record(fields, separator, enclosure, values)?; + match context.stream_resources_mut().write(id, &output) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} + /// Evaluates PHP `fprintf($stream, $format, ...$values)` over eval expressions. pub(in crate::interpreter) fn eval_builtin_fprintf( args: &[EvalExpr], @@ -564,6 +653,119 @@ fn eval_stream_string( Ok(String::from_utf8_lossy(&bytes).into_owned()) } +/// Converts an optional one-byte delimiter argument to a byte value. +fn eval_optional_delimiter( + value: Option, + default: u8, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(value) = value else { + return Ok(default); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(default); + } + Ok(values.string_bytes(value)?.first().copied().unwrap_or(default)) +} + +/// Removes CR/LF line terminators from a CSV record buffer. +fn eval_trim_csv_line_end(line: &mut Vec) { + if line.ends_with(b"\n") { + line.pop(); + } + if line.ends_with(b"\r") { + line.pop(); + } +} + +/// Parses one CSV record using PHP-style doubled-enclosure escaping. +fn eval_parse_csv_record(line: &[u8], separator: u8, enclosure: u8) -> Vec> { + let mut fields = Vec::new(); + let mut field = Vec::new(); + let mut quoted = false; + let mut index = 0; + while index < line.len() { + let byte = line[index]; + if quoted { + if byte == enclosure { + if line.get(index + 1).copied() == Some(enclosure) { + field.push(enclosure); + index += 2; + continue; + } + quoted = false; + } else { + field.push(byte); + } + } else if byte == enclosure && field.is_empty() { + quoted = true; + } else if byte == separator { + fields.push(std::mem::take(&mut field)); + } else { + field.push(byte); + } + index += 1; + } + fields.push(field); + fields +} + +/// Builds a PHP indexed array from parsed CSV field bytes. +fn eval_csv_fields_array( + fields: &[Vec], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(fields.len())?; + for (index, field) in fields.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, field, values)?; + } + Ok(result) +} + +/// Formats one PHP array-like value as a CSV record ending in LF. +fn eval_format_csv_record( + fields: RuntimeCellHandle, + separator: u8, + enclosure: u8, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(fields)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(fields)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.push(separator); + } + let key = values.array_iter_key(fields, position)?; + let value = values.array_get(fields, key)?; + let bytes = values.string_bytes(value)?; + eval_append_csv_field(&mut output, &bytes, separator, enclosure); + } + output.push(b'\n'); + Ok(output) +} + +/// Appends one CSV field, quoting and escaping only when required. +fn eval_append_csv_field(output: &mut Vec, field: &[u8], separator: u8, enclosure: u8) { + let needs_quotes = field + .iter() + .any(|byte| matches!(*byte, b'\n' | b'\r') || *byte == separator || *byte == enclosure); + if !needs_quotes { + output.extend_from_slice(field); + return; + } + output.push(enclosure); + for byte in field { + if *byte == enclosure { + output.push(enclosure); + } + output.push(*byte); + } + output.push(enclosure); +} + /// Inserts a boolean field into the stream metadata array. fn eval_stream_meta_set_bool( array: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index c54d340b3d..eb42fdb511 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -178,12 +178,14 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "fstat" | "stream_get_meta_data" => Some(&["stream"]), "fnmatch" => Some(&["pattern", "filename", "flags"]), + "fgetcsv" => Some(&["stream", "length", "separator"]), "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), + "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), "fprintf" => Some(&["stream", "format", "values"]), "fread" => Some(&["stream", "length"]), "fseek" => Some(&["stream", "offset", "whence"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index f917d7dc4a..ae5d09516c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -117,12 +117,37 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "fgetcsv" => match evaluated_args { + [stream] => eval_fgetcsv_result(*stream, None, None, context, values)?, + [stream, length] => { + eval_fgetcsv_result(*stream, Some(*length), None, context, values)? + } + [stream, length, separator] => { + eval_fgetcsv_result(*stream, Some(*length), Some(*separator), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "fopen" => { if !(2..=4).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); } eval_fopen_result(evaluated_args[0], evaluated_args[1], context, values)? } + "fputcsv" => match evaluated_args { + [stream, fields] => eval_fputcsv_result(*stream, *fields, None, None, context, values)?, + [stream, fields, separator] => { + eval_fputcsv_result(*stream, *fields, Some(*separator), None, context, values)? + } + [stream, fields, separator, enclosure] => eval_fputcsv_result( + *stream, + *fields, + Some(*separator), + Some(*enclosure), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "fprintf" => { let Some((stream, rest)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 0a3a32afb4..8d3bf36c7b 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -95,6 +95,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "fclose" | "fdatasync" | "fgetc" + | "fgetcsv" | "fgets" | "feof" | "fdiv" @@ -118,6 +119,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "fmod" | "fopen" | "fpassthru" + | "fputcsv" | "fprintf" | "fread" | "fseek" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 4eb2093f13..e772f97b36 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -474,7 +474,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "filesize" => eval_builtin_filesize(args, context, scope, values), "filetype" => eval_builtin_filetype(args, context, scope, values), "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), + "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), "fopen" => eval_builtin_fopen(args, context, scope, values), + "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), "fprintf" => eval_builtin_fprintf(args, context, scope, values), "fread" => eval_builtin_fread(args, context, scope, values), "fseek" => eval_builtin_fseek(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs index c5bf115e7c..0e8e68c641 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs @@ -147,3 +147,53 @@ return true;"# ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval CSV stream builtins format and parse local stream records. +#[test] +fn execute_program_dispatches_file_stream_csv_builtins() { + let pid = std::process::id(); + let file = format!("elephc_eval_stream_csv_{pid}.txt"); + let semi = format!("elephc_eval_stream_csv_semi_{pid}.txt"); + let call = format!("elephc_eval_stream_csv_call_{pid}.txt"); + let source = format!( + r#"$h = fopen("{file}", "w+"); +echo fputcsv($h, ["a", "b,c", "d\"e"]) > 0 ? "put" : "bad"; echo ":"; +rewind($h); +$row = fgetcsv($h); +echo $row[0] === "a" && $row[1] === "b,c" && $row[2] === "d\"e" ? "get" : "bad"; echo ":"; +echo fgetcsv($h) === false ? "eof" : "bad"; echo ":"; +fclose($h); +$semi = fopen("{semi}", "w+"); +echo fputcsv($semi, ["x;y", "z"], ";") > 0 ? "putsemi" : "bad"; echo ":"; +rewind($semi); +$semi_row = fgetcsv($semi, null, ";"); +echo $semi_row[0] === "x;y" && $semi_row[1] === "z" ? "getsemi" : "bad"; echo ":"; +fclose($semi); +$call = fopen("{call}", "w+"); +echo call_user_func_array("fputcsv", ["stream" => $call, "fields" => ["m", "n"]]) > 0 ? "callput" : "bad"; echo ":"; +rewind($call); +$call_row = call_user_func("fgetcsv", $call); +echo $call_row[0] === "m" && $call_row[1] === "n" ? "callget" : "bad"; echo ":"; +fclose($call); +echo unlink("{file}") && unlink("{semi}") && unlink("{call}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("fgetcsv"); echo function_exists("fputcsv"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + for path in [&file, &semi, &call] { + let _ = std::fs::remove_file(path); + } + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + for path in [&file, &semi, &call] { + let _ = std::fs::remove_file(path); + } + assert_eq!( + values.output, + "put:get:eof:putsemi:getsemi:callput:callget:cleanup:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} From 6230c2d694f325b56d5eddb78364a391e7310d04 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:25:01 +0200 Subject: [PATCH 0310/1208] Add eval fscanf builtin --- .../builtins/filesystem/streams.rs | 36 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/filesystem.rs | 6 ++++ .../interpreter/builtins/registry/names.rs | 1 + .../src/interpreter/expressions.rs | 1 + .../tests/builtins_file_streams.rs | 18 +++++++--- 6 files changed, 58 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs index 09aaf500c5..9998a42472 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs @@ -315,6 +315,42 @@ pub(in crate::interpreter) fn eval_builtin_fprintf( eval_fprintf_result(stream, format, &format_args, context, values) } +/// Evaluates PHP `fscanf($stream, $format, ...$vars)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_fscanf_result(stream, format, context, values) +} + +/// Reads one line from a stream and scans it with the eval `sscanf()` subset. +pub(in crate::interpreter) fn eval_fscanf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let Some(line) = context + .stream_resources_mut() + .read_line(id, usize::MAX, None, true, true) + else { + return values.bool_value(false); + }; + let input = values.string_bytes_value(&line)?; + eval_sscanf_result(input, format, values) +} + /// Formats and writes `fprintf()` arguments to a materialized stream resource. pub(in crate::interpreter) fn eval_fprintf_result( stream: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index eb42fdb511..cc43af2ba7 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -188,6 +188,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), "fprintf" => Some(&["stream", "format", "values"]), "fread" => Some(&["stream", "length"]), + "fscanf" => Some(&["stream", "format", "vars"]), "fseek" => Some(&["stream", "offset", "whence"]), "ftruncate" => Some(&["stream", "size"]), "fwrite" => Some(&["stream", "data"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index ae5d09516c..dbb8662cfa 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -163,6 +163,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_fread_result(*stream, *length, context, values)? } + "fscanf" => { + if evaluated_args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + eval_fscanf_result(evaluated_args[0], evaluated_args[1], context, values)? + } "fseek" => match evaluated_args { [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values)?, [stream, offset, whence] => { diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 8d3bf36c7b..5e263d8a13 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -122,6 +122,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "fputcsv" | "fprintf" | "fread" + | "fscanf" | "fseek" | "fstat" | "fsync" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index e772f97b36..d53dd959b0 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -479,6 +479,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), "fprintf" => eval_builtin_fprintf(args, context, scope, values), "fread" => eval_builtin_fread(args, context, scope, values), + "fscanf" => eval_builtin_fscanf(args, context, scope, values), "fseek" => eval_builtin_fseek(args, context, scope, values), "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), "fwrite" => eval_builtin_fwrite(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs index 0e8e68c641..46302b0010 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs @@ -155,6 +155,7 @@ fn execute_program_dispatches_file_stream_csv_builtins() { let file = format!("elephc_eval_stream_csv_{pid}.txt"); let semi = format!("elephc_eval_stream_csv_semi_{pid}.txt"); let call = format!("elephc_eval_stream_csv_call_{pid}.txt"); + let scan = format!("elephc_eval_stream_scan_{pid}.txt"); let source = format!( r#"$h = fopen("{file}", "w+"); echo fputcsv($h, ["a", "b,c", "d\"e"]) > 0 ? "put" : "bad"; echo ":"; @@ -175,12 +176,19 @@ rewind($call); $call_row = call_user_func("fgetcsv", $call); echo $call_row[0] === "m" && $call_row[1] === "n" ? "callget" : "bad"; echo ":"; fclose($call); -echo unlink("{file}") && unlink("{semi}") && unlink("{call}") ? "cleanup" : "bad"; echo ":"; -echo function_exists("fgetcsv"); echo function_exists("fputcsv"); +file_put_contents("{scan}", "42 alpha\n7 beta\n"); +$scan = fopen("{scan}", "r"); +$matched = fscanf($scan, "%d %s"); +echo $matched[0] === "42" && $matched[1] === "alpha" ? "scan" : "bad"; echo ":"; +$call_matched = call_user_func("fscanf", $scan, "%d %s"); +echo $call_matched[0] === "7" && $call_matched[1] === "beta" ? "callscan" : "bad"; echo ":"; +fclose($scan); +echo unlink("{file}") && unlink("{semi}") && unlink("{call}") && unlink("{scan}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("fgetcsv"); echo function_exists("fputcsv"); echo function_exists("fscanf"); return true;"# ); let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); - for path in [&file, &semi, &call] { + for path in [&file, &semi, &call, &scan] { let _ = std::fs::remove_file(path); } let mut scope = ElephcEvalScope::new(); @@ -188,12 +196,12 @@ return true;"# let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - for path in [&file, &semi, &call] { + for path in [&file, &semi, &call, &scan] { let _ = std::fs::remove_file(path); } assert_eq!( values.output, - "put:get:eof:putsemi:getsemi:callput:callget:cleanup:11" + "put:get:eof:putsemi:getsemi:callput:callget:scan:callscan:cleanup:111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } From ac1fe3dec81ea09864fa82d953cbc526a51dbfcc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:30:26 +0200 Subject: [PATCH 0311/1208] Add eval flock builtin --- .../builtins/filesystem/streams.rs | 99 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/filesystem.rs | 12 +++ .../interpreter/builtins/registry/names.rs | 1 + .../src/interpreter/constant_eval.rs | 4 + .../elephc-eval/src/interpreter/constants.rs | 4 + .../src/interpreter/expressions.rs | 3 + .../tests/builtins_file_streams.rs | 38 +++++++ crates/elephc-eval/src/stream_resources.rs | 34 +++++++ 9 files changed, 196 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs index 9998a42472..d0e7dedc54 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs @@ -368,6 +368,105 @@ pub(in crate::interpreter) fn eval_fprintf_result( } } +/// Evaluates PHP `flock($stream, $operation, &$would_block = null)` over eval call args. +pub(in crate::interpreter) fn eval_builtin_flock( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (stream, operation, would_block_var) = + eval_flock_direct_args(args, context, scope, values)?; + let (success, would_block) = eval_flock_result(stream, operation, context, values)?; + if let Some(var_name) = would_block_var { + let value = values.bool_value(would_block)?; + for replaced in set_scope_cell(context, scope, var_name, value, ScopeCellOwnership::Owned)? { + values.release(replaced)?; + } + } + values.bool_value(success) +} + +/// Applies a materialized PHP `flock()` operation to a local eval stream resource. +pub(in crate::interpreter) fn eval_flock_result( + stream: RuntimeCellHandle, + operation: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(bool, bool), EvalStatus> { + let id = eval_stream_resource_id(stream, values)?; + let operation = eval_int_value(operation, values)?; + Ok(context + .stream_resources() + .flock(id, operation) + .unwrap_or((false, false))) +} + +/// Evaluates and binds direct `flock()` arguments while keeping by-ref output writable. +fn eval_flock_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle, Option), EvalStatus> { + let mut stream = None; + let mut operation = None; + let mut would_block = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "stream", + 1 => "operation", + 2 => "would_block", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "stream" => { + if stream.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + stream = Some(eval_expr(arg.value(), context, scope, values)?); + } + "operation" => { + if operation.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + operation = Some(eval_expr(arg.value(), context, scope, values)?); + } + "would_block" => { + if would_block.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let EvalExpr::LoadVar(name) = arg.value() else { + return Err(EvalStatus::RuntimeFatal); + }; + would_block = Some(name.clone()); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let stream = stream.ok_or(EvalStatus::RuntimeFatal)?; + let operation = operation.ok_or(EvalStatus::RuntimeFatal)?; + Ok((stream, operation, would_block)) +} + /// Evaluates PHP `vfprintf($stream, $format, $values)` over eval expressions. pub(in crate::interpreter) fn eval_builtin_vfprintf( args: &[EvalExpr], diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index cc43af2ba7..4d0183ccb8 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -187,6 +187,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), "fprintf" => Some(&["stream", "format", "values"]), + "flock" => Some(&["stream", "operation", "would_block"]), "fread" => Some(&["stream", "length"]), "fscanf" => Some(&["stream", "format", "vars"]), "fseek" => Some(&["stream", "offset", "whence"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index dbb8662cfa..c6bfc1fe96 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -157,6 +157,18 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_fprintf_result(*stream, *format, format_args, context, values)? } + "flock" => { + if !(2..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let (success, _) = eval_flock_result( + evaluated_args[0], + evaluated_args[1], + context, + values, + )?; + values.bool_value(success)? + } "fread" => { let [stream, length] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 5e263d8a13..b22decc388 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -114,6 +114,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "filesize" | "filetype" | "fnmatch" + | "flock" | "floor" | "floatval" | "fmod" diff --git a/crates/elephc-eval/src/interpreter/constant_eval.rs b/crates/elephc-eval/src/interpreter/constant_eval.rs index 6e0617e992..61263e8d39 100644 --- a/crates/elephc-eval/src/interpreter/constant_eval.rs +++ b/crates/elephc-eval/src/interpreter/constant_eval.rs @@ -84,6 +84,10 @@ pub(in crate::interpreter) fn eval_predefined_constant_value( "FNM_PATHNAME" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PATHNAME)), "FNM_PERIOD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_PERIOD)), "FNM_CASEFOLD" => Some(EvalPredefinedConstant::Int(EVAL_FNM_CASEFOLD)), + "LOCK_SH" => Some(EvalPredefinedConstant::Int(EVAL_LOCK_SH)), + "LOCK_EX" => Some(EvalPredefinedConstant::Int(EVAL_LOCK_EX)), + "LOCK_UN" => Some(EvalPredefinedConstant::Int(EVAL_LOCK_UN)), + "LOCK_NB" => Some(EvalPredefinedConstant::Int(EVAL_LOCK_NB)), "ARRAY_FILTER_USE_VALUE" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_VALUE)), "ARRAY_FILTER_USE_BOTH" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_BOTH)), "ARRAY_FILTER_USE_KEY" => Some(EvalPredefinedConstant::Int(EVAL_ARRAY_FILTER_USE_KEY)), diff --git a/crates/elephc-eval/src/interpreter/constants.rs b/crates/elephc-eval/src/interpreter/constants.rs index fe51911667..281e2a56ff 100644 --- a/crates/elephc-eval/src/interpreter/constants.rs +++ b/crates/elephc-eval/src/interpreter/constants.rs @@ -207,6 +207,10 @@ pub(super) const EVAL_FNM_NOESCAPE: i64 = 1; pub(super) const EVAL_FNM_PATHNAME: i64 = 2; pub(super) const EVAL_FNM_PERIOD: i64 = 4; pub(super) const EVAL_FNM_CASEFOLD: i64 = 16; +pub(super) const EVAL_LOCK_SH: i64 = 1; +pub(super) const EVAL_LOCK_EX: i64 = 2; +pub(super) const EVAL_LOCK_UN: i64 = 3; +pub(super) const EVAL_LOCK_NB: i64 = 4; pub(super) const EVAL_ARRAY_FILTER_USE_VALUE: i64 = 0; pub(super) const EVAL_ARRAY_FILTER_USE_BOTH: i64 = 1; pub(super) const EVAL_ARRAY_FILTER_USE_KEY: i64 = 2; diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index d53dd959b0..ea57757f83 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -275,6 +275,9 @@ pub(in crate::interpreter) fn eval_call( let args = positional_call_arg_exprs(args)?; return eval_positional_expr_call(name, &args, context, scope, values); } + if name == "flock" { + return eval_builtin_flock(args, context, scope, values); + } if matches!( name, "array_pop" diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs index 46302b0010..e862f27272 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs @@ -87,6 +87,44 @@ return true;"# assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval `flock()` applies advisory locks to local file stream resources. +#[test] +fn execute_program_dispatches_file_stream_flock_builtin() { + let pid = std::process::id(); + let file = format!("elephc_eval_stream_lock_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "x"); +$h = fopen("{file}", "r+"); +$would = true; +echo flock($h, LOCK_EX, $would) ? "lock" : "bad"; echo ":"; +echo $would === false ? "would0" : "bad"; echo ":"; +echo flock(stream: $h, operation: LOCK_UN, would_block: $would) ? "unlock" : "bad"; echo ":"; +echo $would === false ? "would1" : "bad"; echo ":"; +echo call_user_func("flock", $h, LOCK_SH) ? "calllock" : "bad"; echo ":"; +flock($h, LOCK_UN); +echo flock($h, 99) === false ? "invalid" : "bad"; echo ":"; +fclose($h); +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("flock"); +echo defined("LOCK_SH"); echo defined("LOCK_EX"); echo defined("LOCK_UN"); echo defined("LOCK_NB"); +echo ":locks=" . LOCK_SH . LOCK_EX . LOCK_UN . LOCK_NB; +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + "lock:would0:unlock:would1:calllock:invalid:cleanup:11111:locks=1234" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval line, character, and passthrough stream builtins. #[test] fn execute_program_dispatches_file_stream_line_builtins() { diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index bc67df1563..261e123684 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -14,6 +14,7 @@ use std::collections::HashMap; use std::fs::{File, Metadata, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; +use std::os::fd::AsRawFd; use std::path::PathBuf; /// Eval-owned table of local file streams keyed by runtime resource payload. @@ -111,6 +112,21 @@ impl EvalStreamResources { .is_some_and(|stream| stream.file.flush().is_ok()) } + /// Applies an advisory lock operation to a stream's backing file descriptor. + pub(crate) fn flock(&self, id: i64, operation: i64) -> Option<(bool, bool)> { + let stream = self.streams.get(&id)?; + let operation = eval_flock_operation(operation)?; + let result = unsafe { + // libc only observes the borrowed raw fd during this call. + libc::flock(stream.file.as_raw_fd(), operation) + }; + if result == 0 { + Some((true, false)) + } else { + Some((false, eval_flock_would_block())) + } + } + /// Synchronizes stream data and metadata to storage. pub(crate) fn sync_all(&mut self, id: i64) -> bool { self.streams @@ -231,6 +247,24 @@ pub(crate) struct EvalStreamMetaData { pub(crate) uri: String, } +/// Converts PHP `LOCK_*` bit flags into host `flock()` flags. +fn eval_flock_operation(operation: i64) -> Option { + let non_blocking = operation & 4 != 0; + let base = match operation & !4 { + 1 => libc::LOCK_SH, + 2 => libc::LOCK_EX, + 3 => libc::LOCK_UN, + _ => return None, + }; + Some(base | if non_blocking { libc::LOCK_NB } else { 0 }) +} + +/// Returns whether the last host `flock()` failure was a non-blocking lock miss. +fn eval_flock_would_block() -> bool { + let errno = std::io::Error::last_os_error().raw_os_error(); + errno.is_some_and(|code| code == libc::EWOULDBLOCK || code == libc::EAGAIN) +} + /// File stream stored behind one eval resource id. struct EvalFileStream { file: File, From e7e0aa925b45c67d3beaf7b68c691fedc51c7f43 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:36:32 +0200 Subject: [PATCH 0312/1208] Add eval directory stream builtins --- .../builtins/filesystem/directories.rs | 92 +++++++++++++++++++ .../interpreter/builtins/filesystem/mod.rs | 2 + .../interpreter/builtins/registry/binding.rs | 3 +- .../builtins/registry/dispatch/filesystem.rs | 12 +++ .../interpreter/builtins/registry/names.rs | 4 + .../src/interpreter/expressions.rs | 4 + .../tests/builtins_directory_streams.rs | 58 ++++++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + crates/elephc-eval/src/stream_resources.rs | 67 ++++++++++++++ 9 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/directories.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_directory_streams.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/directories.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/directories.rs new file mode 100644 index 0000000000..38522cfe95 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/directories.rs @@ -0,0 +1,92 @@ +//! Purpose: +//! Implements eval-local directory resource builtins backed by host directory listings. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - Directory resources share the eval resource id namespace with file streams. +//! - Entry lists are snapshotted when `opendir()` runs and can be rewound. + +use super::super::super::*; +use super::*; + +/// Evaluates PHP `opendir($directory)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_opendir( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_opendir_result(directory, context, values) +} + +/// Opens a local directory and returns a resource cell or PHP false. +pub(in crate::interpreter) fn eval_opendir_result( + directory: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + match context.stream_resources_mut().open_directory(&directory) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates PHP directory handle builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_unary_directory( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [dir_handle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let dir_handle = eval_expr(dir_handle, context, scope, values)?; + eval_unary_directory_result(name, dir_handle, context, values) +} + +/// Evaluates a materialized directory handle builtin argument. +pub(in crate::interpreter) fn eval_unary_directory_result( + name: &str, + dir_handle: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_directory_resource_id(dir_handle, values)?; + match name { + "closedir" => { + context.stream_resources_mut().close_directory(id); + values.null() + } + "readdir" => match context.stream_resources_mut().read_directory(id) { + Some(name) => values.string(&name), + None => values.bool_value(false), + }, + "rewinddir" => { + context.stream_resources_mut().rewind_directory(id); + values.null() + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based directory id. +fn eval_directory_resource_id( + dir_handle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(dir_handle)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(dir_handle, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs index 9013a416bc..bb23550d3e 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -8,12 +8,14 @@ //! - Path arguments are converted through PHP string coercion before touching the //! host filesystem. +mod directories; mod file_io; mod fnmatch; mod ops; mod path; mod streams; +pub(in crate::interpreter) use directories::*; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use ops::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 4d0183ccb8..cbd24c614f 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -147,9 +147,10 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "interface_exists" => Some(&["interface", "autoload"]), "trait_exists" => Some(&["trait", "autoload"]), "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), - "chdir" | "mkdir" | "rmdir" | "scandir" => Some(&["directory"]), + "chdir" | "mkdir" | "opendir" | "rmdir" | "scandir" => Some(&["directory"]), "chmod" => Some(&["filename", "permissions"]), "chr" => Some(&["codepoint"]), + "closedir" | "readdir" | "rewinddir" => Some(&["dir_handle"]), "clamp" => Some(&["value", "min", "max"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index c6bfc1fe96..ac9118f491 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -37,6 +37,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_chown_like_result(name, *filename, *principal, values)? } + "closedir" | "readdir" | "rewinddir" => { + let [dir_handle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_unary_directory_result(name, *dir_handle, context, values)? + } "clearstatcache" => { if evaluated_args.len() > 2 { return Err(EvalStatus::RuntimeFatal); @@ -252,6 +258,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_glob_result(*pattern, values)? } + "opendir" => { + let [directory] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_opendir_result(*directory, context, values)? + } "pathinfo" => match evaluated_args { [path] => eval_pathinfo_result(*path, None, values)?, [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index b22decc388..0fb05d5493 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -61,6 +61,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "chmod" | "chown" | "class_alias" + | "closedir" | "call_user_func" | "call_user_func_array" | "class_exists" @@ -228,6 +229,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "nl2br" | "number_format" | "ord" + | "opendir" | "passthru" | "pathinfo" | "pi" @@ -248,12 +250,14 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "rawurldecode" | "rawurlencode" | "readfile" + | "readdir" | "readlink" | "realpath" | "realpath_cache_get" | "realpath_cache_size" | "rename" | "rewind" + | "rewinddir" | "rsort" | "rtrim" | "round" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index ea57757f83..b4d3aaad40 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -430,6 +430,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_class_like_exists(name, args, context, scope, values) } "is_a" | "is_subclass_of" => eval_builtin_is_a_relation(name, args, context, scope, values), + "closedir" | "readdir" | "rewinddir" => { + eval_builtin_unary_directory(name, args, context, scope, values) + } "chop" => eval_builtin_trim_like(name, args, context, scope, values), "boolval" | "floatval" | "intval" | "strval" => { eval_builtin_cast(name, args, context, scope, values) @@ -555,6 +558,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "nl2br" => eval_builtin_nl2br(args, context, scope, values), "number_format" => eval_builtin_number_format(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), + "opendir" => eval_builtin_opendir(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "pi" => eval_builtin_pi(args, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_directory_streams.rs b/crates/elephc-eval/src/interpreter/tests/builtins_directory_streams.rs new file mode 100644 index 0000000000..0db98c9dd2 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_directory_streams.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Interpreter tests for eval local directory resource builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Each test uses a process-unique directory and removes it before and after execution. +//! - Directory resources share eval's generic resource cell representation. + +use super::super::*; +use super::support::*; + +/// Verifies eval directory resources support open/read/rewind/close operations. +#[test] +fn execute_program_dispatches_directory_stream_builtins() { + let pid = std::process::id(); + let dir = format!("elephc_eval_dir_stream_{pid}"); + let source = format!( + r#"mkdir("{dir}"); +file_put_contents("{dir}/a.txt", "a"); +file_put_contents("{dir}/b.txt", "b"); +$dh = opendir("{dir}"); +echo is_resource($dh) ? "open" : "bad"; echo ":"; +echo get_resource_type($dh) === "stream" ? "rtype" : "bad"; echo ":"; +echo readdir($dh) === "." ? "dot" : "bad"; echo ":"; +echo readdir($dh) === ".." ? "dotdot" : "bad"; echo ":"; +$entries = [readdir($dh), readdir($dh)]; +echo in_array("a.txt", $entries) && in_array("b.txt", $entries) ? "entries" : "bad"; echo ":"; +echo readdir($dh) === false ? "eof" : "bad"; echo ":"; +rewinddir($dh); +echo readdir($dh) === "." ? "rewind" : "bad"; echo ":"; +call_user_func("rewinddir", $dh); +echo call_user_func("readdir", $dh) === "." ? "callread" : "bad"; echo ":"; +call_user_func("closedir", $dh); +echo readdir($dh) === false ? "closed" : "bad"; echo ":"; +$call = call_user_func_array("opendir", ["directory" => "{dir}"]); +echo call_user_func("readdir", $call) === "." ? "callopen" : "bad"; echo ":"; +closedir($call); +echo unlink("{dir}/a.txt") && unlink("{dir}/b.txt") && rmdir("{dir}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("opendir"); echo function_exists("readdir"); +echo function_exists("rewinddir"); echo function_exists("closedir"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_dir_all(&dir); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_dir_all(&dir); + assert_eq!( + values.output, + "open:rtype:dot:dotdot:entries:eof:rewind:callread:closed:callopen:cleanup:1111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index c2d70d5120..239227868c 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -14,6 +14,7 @@ mod array_literals; mod builtins_arrays_core; mod builtins_arrays_iterators; mod builtins_arrays_sets; +mod builtins_directory_streams; mod builtins_file_streams; mod builtins_filesystem_metadata; mod builtins_filesystem_ops; diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index 261e123684..44b1f3882b 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -21,6 +21,7 @@ use std::path::PathBuf; #[derive(Default)] pub(crate) struct EvalStreamResources { next_id: i64, + directories: HashMap, streams: HashMap, } @@ -49,11 +50,22 @@ impl EvalStreamResources { ))) } + /// Opens a local directory and returns its resource id. + pub(crate) fn open_directory(&mut self, path: &str) -> Option { + let directory = EvalDirectoryStream::open(path)?; + Some(self.insert_directory(directory)) + } + /// Removes a stream resource from the table, closing its file handle. pub(crate) fn close(&mut self, id: i64) -> bool { self.streams.remove(&id).is_some() } + /// Removes a directory resource from the table. + pub(crate) fn close_directory(&mut self, id: i64) -> bool { + self.directories.remove(&id).is_some() + } + /// Reads up to `length` bytes from a stream resource. pub(crate) fn read(&mut self, id: i64, length: usize) -> Option> { let stream = self.streams.get_mut(&id)?; @@ -64,6 +76,11 @@ impl EvalStreamResources { Some(buffer) } + /// Reads the next entry name from a directory resource. + pub(crate) fn read_directory(&mut self, id: i64) -> Option { + self.directories.get_mut(&id)?.read() + } + /// Reads one stream line up to a limit, newline, or custom delimiter. pub(crate) fn read_line( &mut self, @@ -171,6 +188,13 @@ impl EvalStreamResources { self.seek(id, 0, 0) } + /// Rewinds a directory resource to its first entry. + pub(crate) fn rewind_directory(&mut self, id: i64) -> bool { + self.directories + .get_mut(&id) + .is_some_and(EvalDirectoryStream::rewind) + } + /// Truncates a stream to the requested byte length. pub(crate) fn truncate(&mut self, id: i64, size: u64) -> bool { self.streams @@ -238,6 +262,14 @@ impl EvalStreamResources { self.streams.insert(id, stream); id } + + /// Inserts a directory stream and returns the assigned zero-based resource payload. + fn insert_directory(&mut self, directory: EvalDirectoryStream) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.directories.insert(id, directory); + id + } } /// PHP-visible metadata for one eval stream resource. @@ -285,6 +317,41 @@ impl EvalFileStream { } } +/// Directory stream stored behind one eval resource id. +struct EvalDirectoryStream { + entries: Vec, + index: usize, +} + +impl EvalDirectoryStream { + /// Opens a local directory and snapshots its entry names. + fn open(path: &str) -> Option { + let entries = std::fs::read_dir(path).ok()?; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.ok()?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + Some(Self { + entries: names, + index: 0, + }) + } + + /// Returns the next directory entry name. + fn read(&mut self) -> Option { + let name = self.entries.get(self.index)?.clone(); + self.index += 1; + Some(name) + } + + /// Moves the directory cursor back to its first entry. + fn rewind(&mut self) -> bool { + self.index = 0; + true + } +} + /// Parsed PHP fopen mode used to configure `OpenOptions`. struct EvalOpenMode { read: bool, From 116caaea3a2c3e15ee5d1008b9bfab6d1ac3395c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:40:26 +0200 Subject: [PATCH 0313/1208] Add eval hash context builtins --- .../interpreter/builtins/registry/binding.rs | 4 + .../builtins/registry/dispatch/strings.rs | 28 +++- .../interpreter/builtins/registry/names.rs | 4 + .../builtins/strings/hash_context.rs | 143 ++++++++++++++++++ .../src/interpreter/builtins/strings/mod.rs | 2 + .../src/interpreter/expressions.rs | 4 + .../tests/builtins_strings_encoding.rs | 43 ++++++ crates/elephc-eval/src/stream_resources.rs | 87 +++++++++++ 8 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/strings/hash_context.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index cbd24c614f..72828d90d5 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -209,9 +209,13 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "glob" => Some(&["pattern"]), "hash" => Some(&["algo", "data", "binary"]), "hash_algos" => Some(&[]), + "hash_copy" => Some(&["context"]), "hash_equals" => Some(&["known_string", "user_string"]), "hash_file" => Some(&["algo", "filename", "binary"]), + "hash_final" => Some(&["context", "binary"]), "hash_hmac" => Some(&["algo", "data", "key", "binary"]), + "hash_init" => Some(&["algo"]), + "hash_update" => Some(&["context", "data"]), "gzcompress" | "gzdeflate" => Some(&["data", "level"]), "gzinflate" | "gzuncompress" => Some(&["data", "max_length"]), "hypot" => Some(&["x", "y"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs index f6f895b7dd..20ab04d2a2 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs @@ -15,7 +15,7 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_strings_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { @@ -169,6 +169,32 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( } eval_hash_algos_result(values)? } + "hash_copy" => { + let [hash_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_copy_result(*hash_context, context, values)? + } + "hash_final" => match evaluated_args { + [hash_context] => eval_hash_final_result(*hash_context, false, context, values)?, + [hash_context, binary] => { + let binary = values.truthy(*binary)?; + eval_hash_final_result(*hash_context, binary, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "hash_init" => { + let [algo] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_init_result(*algo, context, values)? + } + "hash_update" => { + let [hash_context, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_update_result(*hash_context, *data, context, values)? + } "stream_is_local" | "stream_supports_lock" => { let [stream] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 0fb05d5493..4c19c3a1aa 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -157,9 +157,13 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "gzuncompress" | "hash" | "hash_algos" + | "hash_copy" | "hash_equals" | "hash_file" + | "hash_final" | "hash_hmac" + | "hash_init" + | "hash_update" | "hex2bin" | "html_entity_decode" | "htmlentities" diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/hash_context.rs b/crates/elephc-eval/src/interpreter/builtins/strings/hash_context.rs new file mode 100644 index 0000000000..3946332b54 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/strings/hash_context.rs @@ -0,0 +1,143 @@ +//! Purpose: +//! Implements eval incremental hash context builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - HashContext resources are owned by the eval resource table and backed by +//! elephc-crypto opaque handles. +//! - HMAC streaming mode is intentionally not supported, matching the main type checker. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `hash_init($algo)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hash_init( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [algo] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let algo = eval_expr(algo, context, scope, values)?; + eval_hash_init_result(algo, context, values) +} + +/// Opens an incremental hash context resource. +pub(in crate::interpreter) fn eval_hash_init_result( + algo: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + match context.stream_resources_mut().open_hash_context(&algo) { + Some(id) => values.resource(id), + None => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `hash_update($context, $data)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_update( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hash_context = eval_expr(hash_context, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_hash_update_result(hash_context, data, context, values) +} + +/// Feeds data into a materialized incremental hash context. +pub(in crate::interpreter) fn eval_hash_update_result( + hash_context: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_hash_context_resource_id(hash_context, values)?; + let data = values.string_bytes(data)?; + values.bool_value(context.stream_resources_mut().update_hash_context(id, &data)) +} + +/// Evaluates PHP `hash_final($context, $binary = false)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_final( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let hash_context = eval_expr(&args[0], context, scope, values)?; + let binary = match args.get(1) { + Some(binary) => { + let binary = eval_expr(binary, context, scope, values)?; + values.truthy(binary)? + } + None => false, + }; + eval_hash_final_result(hash_context, binary, context, values) +} + +/// Finalizes a materialized incremental hash context. +pub(in crate::interpreter) fn eval_hash_final_result( + hash_context: RuntimeCellHandle, + binary: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_hash_context_resource_id(hash_context, values)?; + let raw = context + .stream_resources_mut() + .finalize_hash_context(id) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_format_digest_result(&raw, binary, values) +} + +/// Evaluates PHP `hash_copy($context)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hash_copy( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hash_context = eval_expr(hash_context, context, scope, values)?; + eval_hash_copy_result(hash_context, context, values) +} + +/// Clones a materialized incremental hash context into a new resource. +pub(in crate::interpreter) fn eval_hash_copy_result( + hash_context: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_hash_context_resource_id(hash_context, values)?; + match context.stream_resources_mut().copy_hash_context(id) { + Some(copy_id) => values.resource(copy_id), + None => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based hash context id. +fn eval_hash_context_resource_id( + hash_context: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(hash_context)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(hash_context, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs b/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs index 7147e19279..7293821e52 100644 --- a/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs @@ -12,6 +12,7 @@ mod ctype; mod grapheme_strrev; mod gzip; mod hash; +mod hash_context; mod html; mod introspection; mod nl2br; @@ -27,6 +28,7 @@ pub(in crate::interpreter) use ctype::*; pub(in crate::interpreter) use grapheme_strrev::*; pub(in crate::interpreter) use gzip::*; pub(in crate::interpreter) use hash::*; +pub(in crate::interpreter) use hash_context::*; pub(in crate::interpreter) use html::*; pub(in crate::interpreter) use introspection::*; pub(in crate::interpreter) use nl2br::*; diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index b4d3aaad40..0c067e5aea 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -524,7 +524,11 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_chown_like(name, args, context, scope, values) } "hash_algos" => eval_builtin_hash_algos(args, values), + "hash_copy" => eval_builtin_hash_copy(args, context, scope, values), "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), + "hash_final" => eval_builtin_hash_final(args, context, scope, values), + "hash_init" => eval_builtin_hash_init(args, context, scope, values), + "hash_update" => eval_builtin_hash_update(args, context, scope, values), "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { eval_builtin_html_entity(name, args, context, scope, values) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs index b4ce28e518..ed8332be2a 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs @@ -210,6 +210,49 @@ return function_exists("rawurldecode");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval incremental hash context builtins use elephc-crypto state. +#[test] +fn execute_program_dispatches_hash_context_builtins() { + let program = parse_fragment( + br#"$ctx = hash_init("sha256"); +echo is_resource($ctx) ? "ctx" : "bad"; echo ":"; +echo get_resource_type($ctx) === "stream" ? "rtype" : "bad"; echo ":"; +echo hash_update($ctx, "ab") ? "up1" : "bad"; echo ":"; +$copy = hash_copy($ctx); +echo hash_update($ctx, "c") ? "up2" : "bad"; echo ":"; +echo hash_update($copy, "d") ? "upcopy" : "bad"; echo ":"; +echo hash_final($ctx); echo ":"; +echo hash_final($copy); echo ":"; +$raw = call_user_func("hash_init", "md5"); +hash_update(context: $raw, data: "abc"); +echo bin2hex(call_user_func("hash_final", $raw, true)); echo ":"; +$named = call_user_func_array("hash_init", ["algo" => "sha1"]); +call_user_func_array("hash_update", ["context" => $named, "data" => "abc"]); +echo call_user_func_array("hash_final", ["context" => $named]); echo ":"; +echo function_exists("hash_init"); echo function_exists("hash_update"); +echo function_exists("hash_final"); echo function_exists("hash_copy"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "ctx:rtype:up1:up2:upcopy:", + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad:", + "a52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9:", + "900150983cd24fb0d6963f7d28e17f72:", + "a9993e364706816aba3e25717850c26c9cd0d89d:", + "1111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval `ctype_*` predicates dispatch through direct, named, and callable paths. #[test] fn execute_program_dispatches_ctype_builtins() { diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index 44b1f3882b..285935f07e 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -12,6 +12,7 @@ //! - File handles are process-local to eval and are not visible across the C ABI. use std::collections::HashMap; +use std::ffi::c_void; use std::fs::{File, Metadata, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; use std::os::fd::AsRawFd; @@ -22,6 +23,7 @@ use std::path::PathBuf; pub(crate) struct EvalStreamResources { next_id: i64, directories: HashMap, + hash_contexts: HashMap, streams: HashMap, } @@ -56,6 +58,19 @@ impl EvalStreamResources { Some(self.insert_directory(directory)) } + /// Opens an incremental hash context and returns its resource id. + pub(crate) fn open_hash_context(&mut self, algo: &[u8]) -> Option { + let handle = unsafe { + // elephc-crypto reads the algorithm name during this call and returns + // an owned opaque context handle on success. + elephc_crypto::elephc_crypto_init(algo.as_ptr(), algo.len()) + }; + if handle.is_null() { + return None; + } + Some(self.insert_hash_context(EvalHashContext { handle })) + } + /// Removes a stream resource from the table, closing its file handle. pub(crate) fn close(&mut self, id: i64) -> bool { self.streams.remove(&id).is_some() @@ -81,6 +96,19 @@ impl EvalStreamResources { self.directories.get_mut(&id)?.read() } + /// Feeds bytes into an incremental hash context. + pub(crate) fn update_hash_context(&mut self, id: i64, data: &[u8]) -> bool { + let Some(context) = self.hash_contexts.get_mut(&id) else { + return false; + }; + unsafe { + // The table owns the opaque handle and this mutable borrow gives the + // crypto call exclusive access for the duration of the update. + elephc_crypto::elephc_crypto_update(context.handle, data.as_ptr(), data.len()); + } + true + } + /// Reads one stream line up to a limit, newline, or custom delimiter. pub(crate) fn read_line( &mut self, @@ -195,6 +223,30 @@ impl EvalStreamResources { .is_some_and(EvalDirectoryStream::rewind) } + /// Finalizes and removes an incremental hash context, returning raw digest bytes. + pub(crate) fn finalize_hash_context(&mut self, id: i64) -> Option> { + let context = self.hash_contexts.remove(&id)?; + let mut output = [0_u8; 64]; + let len = unsafe { + // elephc-crypto consumes and frees the owned context handle here. + elephc_crypto::elephc_crypto_final(context.handle, output.as_mut_ptr()) + }; + eval_hash_digest_bytes(len, &output) + } + + /// Clones an incremental hash context into a new resource id. + pub(crate) fn copy_hash_context(&mut self, id: i64) -> Option { + let context = self.hash_contexts.get(&id)?; + let handle = unsafe { + // elephc-crypto returns a deep clone with independent ownership. + elephc_crypto::elephc_crypto_clone(context.handle) + }; + if handle.is_null() { + return None; + } + Some(self.insert_hash_context(EvalHashContext { handle })) + } + /// Truncates a stream to the requested byte length. pub(crate) fn truncate(&mut self, id: i64, size: u64) -> bool { self.streams @@ -270,6 +322,27 @@ impl EvalStreamResources { self.directories.insert(id, directory); id } + + /// Inserts a hash context and returns the assigned zero-based resource payload. + fn insert_hash_context(&mut self, context: EvalHashContext) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.hash_contexts.insert(id, context); + id + } +} + +impl Drop for EvalStreamResources { + /// Frees any incremental hash contexts that were never finalized. + fn drop(&mut self) { + for context in self.hash_contexts.drain().map(|(_, context)| context) { + unsafe { + // The resource table owns these handles; draining prevents reuse + // after the crypto free call. + elephc_crypto::elephc_crypto_free(context.handle); + } + } + } } /// PHP-visible metadata for one eval stream resource. @@ -297,6 +370,15 @@ fn eval_flock_would_block() -> bool { errno.is_some_and(|code| code == libc::EWOULDBLOCK || code == libc::EAGAIN) } +/// Converts an elephc-crypto digest length into owned raw bytes. +fn eval_hash_digest_bytes(len: isize, output: &[u8; 64]) -> Option> { + let len = usize::try_from(len).ok()?; + if len > output.len() { + return None; + } + Some(output[..len].to_vec()) +} + /// File stream stored behind one eval resource id. struct EvalFileStream { file: File, @@ -352,6 +434,11 @@ impl EvalDirectoryStream { } } +/// Opaque elephc-crypto incremental hash context resource. +struct EvalHashContext { + handle: *mut c_void, +} + /// Parsed PHP fopen mode used to configure `OpenOptions`. struct EvalOpenMode { read: bool, From bb94270be5a4e3ec5dfff9f3472fe2f6d5533a3a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:45:07 +0200 Subject: [PATCH 0314/1208] Add eval stream context builtins --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/stream_context.rs | 257 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 8 + .../builtins/registry/dispatch/filesystem.rs | 58 ++++ .../interpreter/builtins/registry/names.rs | 7 + .../src/interpreter/expressions.rs | 21 ++ .../tests/builtins_stream_contexts.rs | 62 +++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + crates/elephc-eval/src/stream_resources.rs | 56 +++- 9 files changed, 469 insertions(+), 3 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/stream_context.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_stream_contexts.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs index bb23550d3e..58c7cd86b3 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -13,6 +13,7 @@ mod file_io; mod fnmatch; mod ops; mod path; +mod stream_context; mod streams; pub(in crate::interpreter) use directories::*; @@ -20,4 +21,5 @@ pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use ops::*; pub(in crate::interpreter) use path::*; +pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use streams::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_context.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_context.rs new file mode 100644 index 0000000000..76fc2bdb43 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_context.rs @@ -0,0 +1,257 @@ +//! Purpose: +//! Implements eval stream context metadata builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - Eval stores options per context resource, while `get_params()` mirrors the +//! main backend's current empty-array behavior. +//! - Context resources share the same generic resource id namespace as streams. + +use super::super::super::*; + +/// Evaluates `stream_context_create($options = null, $params = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_context_create( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + let options = match args.first() { + Some(options) => Some(eval_expr(options, context, scope, values)?), + None => None, + }; + if let Some(params) = args.get(1) { + eval_expr(params, context, scope, values)?; + } + eval_stream_context_create_result(options, context, values) +} + +/// Creates a stream context resource from materialized optional options. +pub(in crate::interpreter) fn eval_stream_context_create_result( + options: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let options = eval_stream_context_options_arg(options, values)?; + let id = context.stream_resources_mut().open_stream_context(options); + values.resource(id) +} + +/// Evaluates `stream_context_get_default($options = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_context_get_default( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 1 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + eval_stream_context_get_default_result(context, values) +} + +/// Returns eval's default stream context resource. +pub(in crate::interpreter) fn eval_stream_context_get_default_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = context.stream_resources_mut().default_stream_context(); + values.resource(id) +} + +/// Evaluates `stream_context_set_default($options)`. +pub(in crate::interpreter) fn eval_builtin_stream_context_set_default( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [options] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_expr(options, context, scope, values)?; + eval_stream_context_get_default_result(context, values) +} + +/// Evaluates `stream_context_set_option($context, ...)`. +pub(in crate::interpreter) fn eval_builtin_stream_context_set_option( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [stream_context, options] => { + let stream_context = eval_expr(stream_context, context, scope, values)?; + let options = eval_expr(options, context, scope, values)?; + eval_stream_context_set_options_result(stream_context, options, context, values) + } + [stream_context, wrapper, option, value] => { + let stream_context = eval_expr(stream_context, context, scope, values)?; + let wrapper = eval_expr(wrapper, context, scope, values)?; + let option = eval_expr(option, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_stream_context_set_option_result( + stream_context, + wrapper, + option, + value, + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Stores a materialized options array on a stream context resource. +pub(in crate::interpreter) fn eval_stream_context_set_options_result( + stream_context: RuntimeCellHandle, + options: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_context_resource_id(stream_context, values)?; + let options = eval_stream_context_options_arg(Some(options), values)?; + values.bool_value( + context + .stream_resources_mut() + .set_stream_context_options(id, options), + ) +} + +/// Stores one nested `options[wrapper][option] = value` entry on a stream context. +pub(in crate::interpreter) fn eval_stream_context_set_option_result( + stream_context: RuntimeCellHandle, + wrapper: RuntimeCellHandle, + option: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_context_resource_id(stream_context, values)?; + let wrapper = values.cast_string(wrapper)?; + let option = values.cast_string(option)?; + let options = match context.stream_resources().stream_context_options(id) { + Some(options) => options, + None => values.assoc_new(1)?, + }; + let wrapper_options = eval_stream_context_wrapper_options(options, wrapper, values)?; + let wrapper_options = values.array_set(wrapper_options, option, value)?; + let options = values.array_set(options, wrapper, wrapper_options)?; + values.bool_value( + context + .stream_resources_mut() + .set_stream_context_options(id, Some(options)), + ) +} + +/// Evaluates `stream_context_set_params($context, $params)` as an accepted no-op. +pub(in crate::interpreter) fn eval_builtin_stream_context_set_params( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context, params] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_expr(stream_context, context, scope, values)?; + eval_expr(params, context, scope, values)?; + values.bool_value(true) +} + +/// Evaluates `stream_context_get_options($context)`. +pub(in crate::interpreter) fn eval_builtin_stream_context_get_options( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_context = eval_expr(stream_context, context, scope, values)?; + eval_stream_context_get_options_result(stream_context, context, values) +} + +/// Returns persisted stream context options or an empty associative array. +pub(in crate::interpreter) fn eval_stream_context_get_options_result( + stream_context: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_context_resource_id(stream_context, values)?; + match context.stream_resources().stream_context_options(id) { + Some(options) => Ok(options), + None => values.assoc_new(0), + } +} + +/// Evaluates `stream_context_get_params($context)` to an empty associative array. +pub(in crate::interpreter) fn eval_builtin_stream_context_get_params( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_expr(stream_context, context, scope, values)?; + values.assoc_new(0) +} + +/// Converts an optional options argument into a stored context option handle. +fn eval_stream_context_options_arg( + options: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(options) = options else { + return Ok(None); + }; + if values.type_tag(options)? == EVAL_TAG_NULL { + return Ok(None); + } + if !values.is_array_like(options)? { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(options)) +} + +/// Returns the nested wrapper options array, creating one when missing or scalar. +fn eval_stream_context_wrapper_options( + options: RuntimeCellHandle, + wrapper: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = values.array_key_exists(wrapper, options)?; + if values.truthy(exists)? { + let wrapper_options = values.array_get(options, wrapper)?; + if values.is_array_like(wrapper_options)? { + return Ok(wrapper_options); + } + } + values.assoc_new(1) +} + +/// Converts a runtime resource cell into eval's zero-based stream context id. +fn eval_stream_context_resource_id( + stream_context: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream_context)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream_context, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 72828d90d5..8ef3aed6f6 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -271,6 +271,14 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "sprintf" | "printf" => Some(&["format", "values"]), "stream_is_local" | "stream_supports_lock" => Some(&["stream"]), "stream_copy_to_stream" => Some(&["from", "to", "length", "offset"]), + "stream_context_create" => Some(&["options", "params"]), + "stream_context_get_default" => Some(&["options"]), + "stream_context_get_options" | "stream_context_get_params" => Some(&["context"]), + "stream_context_set_default" => Some(&["options"]), + "stream_context_set_option" => { + Some(&["context", "wrapper_or_options", "option_name", "value"]) + } + "stream_context_set_params" => Some(&["context", "params"]), "stream_get_contents" => Some(&["stream", "length", "offset"]), "stream_get_line" => Some(&["stream", "length", "ending"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index ac9118f491..db1510f01e 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -298,6 +298,64 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( )?, _ => return Err(EvalStatus::RuntimeFatal), }, + "stream_context_create" => match evaluated_args { + [] => eval_stream_context_create_result(None, context, values)?, + [options] => eval_stream_context_create_result(Some(*options), context, values)?, + [options, _params] => { + eval_stream_context_create_result(Some(*options), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_context_get_default" => { + if evaluated_args.len() > 1 { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_context_get_default_result(context, values)? + } + "stream_context_get_options" => { + let [stream_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_context_get_options_result(*stream_context, context, values)? + } + "stream_context_get_params" => { + let [stream_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + if values.type_tag(*stream_context)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + values.assoc_new(0)? + } + "stream_context_set_default" => { + let [_options] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_context_get_default_result(context, values)? + } + "stream_context_set_option" => match evaluated_args { + [stream_context, options] => { + eval_stream_context_set_options_result(*stream_context, *options, context, values)? + } + [stream_context, wrapper, option, value] => eval_stream_context_set_option_result( + *stream_context, + *wrapper, + *option, + *value, + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_context_set_params" => { + let [stream_context, _params] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + if values.type_tag(*stream_context)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true)? + } "stream_get_contents" => match evaluated_args { [stream] => eval_stream_get_contents_result(*stream, None, None, context, values)?, [stream, length] => { diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 4c19c3a1aa..ee2babd9b1 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -283,6 +283,13 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "sprintf" | "strcasecmp" | "stream_copy_to_stream" + | "stream_context_create" + | "stream_context_get_default" + | "stream_context_get_options" + | "stream_context_get_params" + | "stream_context_set_default" + | "stream_context_set_option" + | "stream_context_set_params" | "stream_get_contents" | "stream_get_filters" | "stream_get_line" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 0c067e5aea..df3228ba2f 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -611,6 +611,27 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_stream_resolve_include_path(args, context, scope, values) } "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), + "stream_context_create" => { + eval_builtin_stream_context_create(args, context, scope, values) + } + "stream_context_get_default" => { + eval_builtin_stream_context_get_default(args, context, scope, values) + } + "stream_context_get_options" => { + eval_builtin_stream_context_get_options(args, context, scope, values) + } + "stream_context_get_params" => { + eval_builtin_stream_context_get_params(args, context, scope, values) + } + "stream_context_set_default" => { + eval_builtin_stream_context_set_default(args, context, scope, values) + } + "stream_context_set_option" => { + eval_builtin_stream_context_set_option(args, context, scope, values) + } + "stream_context_set_params" => { + eval_builtin_stream_context_set_params(args, context, scope, values) + } "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), "strtotime" => eval_builtin_strtotime(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_stream_contexts.rs b/crates/elephc-eval/src/interpreter/tests/builtins_stream_contexts.rs new file mode 100644 index 0000000000..4e3f574bf1 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_stream_contexts.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Interpreter tests for eval stream context metadata builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Context resources are eval-owned resource cells with inspectable options. +//! - Params currently mirror the main backend's empty-array behavior. + +use super::super::*; +use super::support::*; + +/// Verifies eval stream context builtins create resources and persist options. +#[test] +fn execute_program_dispatches_stream_context_builtins() { + let program = parse_fragment( + br#"$ctx = stream_context_create(["http" => ["method" => "POST"]]); +echo is_resource($ctx) ? "ctx" : "bad"; echo ":"; +echo get_resource_type($ctx) === "stream" ? "rtype" : "bad"; echo ":"; +$opts = stream_context_get_options($ctx); +echo $opts["http"]["method"] === "POST" ? "initial" : "bad"; echo ":"; +echo stream_context_set_option($ctx, "http", "header", "X-Test: 1") ? "setone" : "bad"; echo ":"; +$opts = stream_context_get_options($ctx); +echo $opts["http"]["header"] === "X-Test: 1" ? "gotone" : "bad"; echo ":"; +echo stream_context_set_option($ctx, ["ssl" => ["verify_peer" => false]]) ? "setall" : "bad"; echo ":"; +$opts = stream_context_get_options($ctx); +echo $opts["ssl"]["verify_peer"] === false ? "gotall" : "bad"; echo ":"; +echo stream_context_set_params($ctx, ["notification" => "noop"]) ? "paramsset" : "bad"; echo ":"; +$params = stream_context_get_params($ctx); +echo count($params) === 0 ? "params" : "bad"; echo ":"; +$default = stream_context_get_default(); +echo is_resource($default) ? "default" : "bad"; echo ":"; +$set_default = stream_context_set_default(["http" => ["timeout" => "1"]]); +echo is_resource($set_default) ? "setdefault" : "bad"; echo ":"; +$call = call_user_func_array("stream_context_create", ["options" => ["ftp" => ["user" => "u"]]]); +$call_opts = call_user_func("stream_context_get_options", $call); +echo $call_opts["ftp"]["user"] === "u" ? "callcreate" : "bad"; echo ":"; +echo call_user_func("stream_context_set_option", $call, "ftp", "mode", "passive") ? "callset" : "bad"; echo ":"; +$call_opts = call_user_func("stream_context_get_options", $call); +echo $call_opts["ftp"]["mode"] === "passive" ? "callgot" : "bad"; echo ":"; +echo function_exists("stream_context_create"); echo function_exists("stream_context_get_default"); +echo function_exists("stream_context_set_default"); echo function_exists("stream_context_set_option"); +echo function_exists("stream_context_set_params"); echo function_exists("stream_context_get_options"); +echo function_exists("stream_context_get_params"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "ctx:rtype:initial:setone:gotone:setall:gotall:paramsset:params:", + "default:setdefault:callcreate:callset:callgot:1111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 239227868c..39cc141d5f 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -21,6 +21,7 @@ mod builtins_filesystem_ops; mod builtins_json; mod builtins_math_formatting; mod builtins_scalars; +mod builtins_stream_contexts; mod builtins_strings_binary; mod builtins_strings_encoding; mod builtins_strings_text; diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index 285935f07e..0a0c67ea37 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -1,6 +1,6 @@ //! Purpose: -//! Owns eval-local stream resource storage backed by host file handles. -//! Builtin implementations use this table while runtime Mixed cells only carry +//! Owns eval-local resource storage backed by host file handles, directory +//! snapshots, stream contexts, and hash contexts. Runtime Mixed cells only carry //! a numeric resource id. //! //! Called from: @@ -9,7 +9,7 @@ //! //! Key details: //! - Resource ids are zero-based runtime payloads; PHP display ids are payload + 1. -//! - File handles are process-local to eval and are not visible across the C ABI. +//! - Resource handles are process-local to eval and are not visible across the C ABI. use std::collections::HashMap; use std::ffi::c_void; @@ -18,12 +18,16 @@ use std::io::{Read, Seek, SeekFrom, Write}; use std::os::fd::AsRawFd; use std::path::PathBuf; +use crate::value::RuntimeCellHandle; + /// Eval-owned table of local file streams keyed by runtime resource payload. #[derive(Default)] pub(crate) struct EvalStreamResources { + default_stream_context: Option, next_id: i64, directories: HashMap, hash_contexts: HashMap, + stream_contexts: HashMap, streams: HashMap, } @@ -71,6 +75,21 @@ impl EvalStreamResources { Some(self.insert_hash_context(EvalHashContext { handle })) } + /// Opens a stream context resource with optional persisted options. + pub(crate) fn open_stream_context(&mut self, options: Option) -> i64 { + self.insert_stream_context(EvalStreamContext { options }) + } + + /// Returns the default stream context resource id, creating it if needed. + pub(crate) fn default_stream_context(&mut self) -> i64 { + if let Some(id) = self.default_stream_context { + return id; + } + let id = self.open_stream_context(None); + self.default_stream_context = Some(id); + id + } + /// Removes a stream resource from the table, closing its file handle. pub(crate) fn close(&mut self, id: i64) -> bool { self.streams.remove(&id).is_some() @@ -109,6 +128,24 @@ impl EvalStreamResources { true } + /// Returns the persisted options for a stream context resource. + pub(crate) fn stream_context_options(&self, id: i64) -> Option { + self.stream_contexts.get(&id).and_then(|context| context.options) + } + + /// Replaces persisted options for a stream context resource. + pub(crate) fn set_stream_context_options( + &mut self, + id: i64, + options: Option, + ) -> bool { + let Some(context) = self.stream_contexts.get_mut(&id) else { + return false; + }; + context.options = options; + true + } + /// Reads one stream line up to a limit, newline, or custom delimiter. pub(crate) fn read_line( &mut self, @@ -330,6 +367,14 @@ impl EvalStreamResources { self.hash_contexts.insert(id, context); id } + + /// Inserts a stream context and returns the assigned zero-based resource payload. + fn insert_stream_context(&mut self, context: EvalStreamContext) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.stream_contexts.insert(id, context); + id + } } impl Drop for EvalStreamResources { @@ -439,6 +484,11 @@ struct EvalHashContext { handle: *mut c_void, } +/// Stream context metadata tracked by eval. +struct EvalStreamContext { + options: Option, +} + /// Parsed PHP fopen mode used to configure `OpenOptions`. struct EvalOpenMode { read: bool, From a5bf4e2fa85f8954af6bc0626b1db6bcef3e9c23 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:49:24 +0200 Subject: [PATCH 0315/1208] Add eval stream setting builtins --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/stream_settings.rs | 158 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 7 +- .../builtins/registry/dispatch/filesystem.rs | 31 ++++ .../interpreter/builtins/registry/names.rs | 6 + .../src/interpreter/expressions.rs | 6 + .../tests/builtins_stream_settings.rs | 56 +++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + crates/elephc-eval/src/stream_resources.rs | 52 ++++++ 9 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/stream_settings.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_stream_settings.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs index 58c7cd86b3..ca9c59881d 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -14,6 +14,7 @@ mod fnmatch; mod ops; mod path; mod stream_context; +mod stream_settings; mod streams; pub(in crate::interpreter) use directories::*; @@ -22,4 +23,5 @@ pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use ops::*; pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use stream_context::*; +pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use streams::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_settings.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_settings.rs new file mode 100644 index 0000000000..693645a600 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_settings.rs @@ -0,0 +1,158 @@ +//! Purpose: +//! Implements eval stream descriptor predicate and setting builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - `stream_isatty` and `stream_set_blocking` call host libc for local files. +//! - Buffer and chunk-size settings are eval-local metadata; timeout is false for +//! local files because the main backend applies it as a socket option. + +use super::super::super::*; + +/// Evaluates `stream_isatty($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_isatty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_isatty_result(stream, context, values) +} + +/// Returns whether a materialized stream resource is attached to a terminal. +pub(in crate::interpreter) fn eval_stream_isatty_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_settings_stream_resource_id(stream, values)?; + values.bool_value(context.stream_resources().isatty(id).unwrap_or(false)) +} + +/// Evaluates `stream_set_blocking($stream, $enable)`. +pub(in crate::interpreter) fn eval_builtin_stream_set_blocking( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, enable] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let enable = eval_expr(enable, context, scope, values)?; + eval_stream_set_blocking_result(stream, enable, context, values) +} + +/// Toggles blocking mode on a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_set_blocking_result( + stream: RuntimeCellHandle, + enable: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_settings_stream_resource_id(stream, values)?; + let enable = values.truthy(enable)?; + values.bool_value(context.stream_resources().set_blocking(id, enable).unwrap_or(false)) +} + +/// Evaluates `stream_set_timeout($stream, $seconds, $microseconds = 0)`. +pub(in crate::interpreter) fn eval_builtin_stream_set_timeout( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let seconds = eval_expr(&args[1], context, scope, values)?; + let microseconds = match args.get(2) { + Some(microseconds) => Some(eval_expr(microseconds, context, scope, values)?), + None => None, + }; + eval_stream_set_timeout_result(stream, seconds, microseconds, context, values) +} + +/// Applies a timeout request to a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_set_timeout_result( + stream: RuntimeCellHandle, + seconds: RuntimeCellHandle, + microseconds: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_settings_stream_resource_id(stream, values)?; + let seconds = eval_int_value(seconds, values)?; + let microseconds = match microseconds { + Some(microseconds) => eval_int_value(microseconds, values)?, + None => 0, + }; + values.bool_value( + context + .stream_resources() + .set_timeout(id, seconds, microseconds) + .unwrap_or(false), + ) +} + +/// Evaluates chunk/read/write buffer setting builtins. +pub(in crate::interpreter) fn eval_builtin_stream_set_buffer_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, size] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let size = eval_expr(size, context, scope, values)?; + eval_stream_set_buffer_like_result(name, stream, size, context, values) +} + +/// Applies a materialized chunk/read/write buffer setting. +pub(in crate::interpreter) fn eval_stream_set_buffer_like_result( + name: &str, + stream: RuntimeCellHandle, + size: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_settings_stream_resource_id(stream, values)?; + let size = eval_int_value(size, values)?; + match name { + "stream_set_chunk_size" => match context.stream_resources_mut().set_chunk_size(id, size) { + Some(previous) => values.int(previous), + None => values.bool_value(false), + }, + "stream_set_read_buffer" | "stream_set_write_buffer" => { + match context.stream_resources().set_buffer(id, size) { + Some(status) => values.int(status), + None => values.bool_value(false), + } + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based stream id. +fn eval_settings_stream_resource_id( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 8ef3aed6f6..55ec8f4ba2 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -269,7 +269,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "spl_object_id" | "spl_object_hash" => Some(&["object"]), "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), - "stream_is_local" | "stream_supports_lock" => Some(&["stream"]), + "stream_is_local" | "stream_isatty" | "stream_supports_lock" => Some(&["stream"]), "stream_copy_to_stream" => Some(&["from", "to", "length", "offset"]), "stream_context_create" => Some(&["options", "params"]), "stream_context_get_default" => Some(&["options"]), @@ -282,6 +282,11 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_get_contents" => Some(&["stream", "length", "offset"]), "stream_get_line" => Some(&["stream", "length", "ending"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), + "stream_set_blocking" => Some(&["stream", "enable"]), + "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { + Some(&["stream", "size"]) + } + "stream_set_timeout" => Some(&["stream", "seconds", "microseconds"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), "strtotime" => Some(&["datetime"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index db1510f01e..cc9fd41f33 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -379,6 +379,37 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "stream_isatty" => { + let [stream] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_isatty_result(*stream, context, values)? + } + "stream_set_blocking" => { + let [stream, enable] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_set_blocking_result(*stream, *enable, context, values)? + } + "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { + let [stream, size] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_set_buffer_like_result(name, *stream, *size, context, values)? + } + "stream_set_timeout" => match evaluated_args { + [stream, seconds] => { + eval_stream_set_timeout_result(*stream, *seconds, None, context, values)? + } + [stream, seconds, microseconds] => eval_stream_set_timeout_result( + *stream, + *seconds, + Some(*microseconds), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "realpath_cache_get" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index ee2babd9b1..32833f4164 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -296,7 +296,13 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "stream_get_meta_data" | "stream_get_transports" | "stream_get_wrappers" + | "stream_isatty" | "stream_is_local" + | "stream_set_blocking" + | "stream_set_chunk_size" + | "stream_set_read_buffer" + | "stream_set_timeout" + | "stream_set_write_buffer" | "stream_supports_lock" | "stream_resolve_include_path" | "str_contains" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index df3228ba2f..6910056ac4 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -634,6 +634,12 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), + "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), + "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), + "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { + eval_builtin_stream_set_buffer_like(name, args, context, scope, values) + } + "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), "strtotime" => eval_builtin_strtotime(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_stream_settings.rs b/crates/elephc-eval/src/interpreter/tests/builtins_stream_settings.rs new file mode 100644 index 0000000000..97c9734e27 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_stream_settings.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Interpreter tests for eval stream descriptor setting builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Local file streams expose terminal/blocking probes through host libc. +//! - Timeout support currently returns false for regular files, matching the +//! socket-only behavior of the main backend. + +use super::super::*; +use super::support::*; + +/// Verifies eval stream setting builtins work directly and through dynamic calls. +#[test] +fn execute_program_dispatches_stream_setting_builtins() { + let pid = std::process::id(); + let file = format!("elephc_eval_stream_settings_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "x"); +$h = fopen("{file}", "r+"); +echo stream_isatty($h) ? "bad" : "notty"; echo ":"; +echo stream_set_blocking($h, false) ? "nonblock" : "bad"; echo ":"; +echo stream_set_blocking($h, true) ? "block" : "bad"; echo ":"; +echo stream_set_chunk_size($h, 1024) === 8192 ? "chunk1" : "bad"; echo ":"; +echo stream_set_chunk_size($h, 2048) === 1024 ? "chunk2" : "bad"; echo ":"; +echo stream_set_read_buffer($h, 0) === 0 ? "readbuf" : "bad"; echo ":"; +echo stream_set_write_buffer($h, 0) === 0 ? "writebuf" : "bad"; echo ":"; +echo stream_set_timeout($h, 1) === false ? "notimeout" : "bad"; echo ":"; +echo call_user_func("stream_isatty", $h) === false ? "calltty" : "bad"; echo ":"; +echo call_user_func("stream_set_chunk_size", $h, 4096) === 2048 ? "callchunk" : "bad"; echo ":"; +fclose($h); +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stream_isatty"); echo function_exists("stream_set_blocking"); +echo function_exists("stream_set_chunk_size"); echo function_exists("stream_set_read_buffer"); +echo function_exists("stream_set_timeout"); echo function_exists("stream_set_write_buffer"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + concat!( + "notty:nonblock:block:chunk1:chunk2:readbuf:writebuf:notimeout:", + "calltty:callchunk:cleanup:111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 39cc141d5f..04949cba57 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -22,6 +22,7 @@ mod builtins_json; mod builtins_math_formatting; mod builtins_scalars; mod builtins_stream_contexts; +mod builtins_stream_settings; mod builtins_strings_binary; mod builtins_strings_encoding; mod builtins_strings_text; diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index 0a0c67ea37..6d81f13ece 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -23,6 +23,7 @@ use crate::value::RuntimeCellHandle; /// Eval-owned table of local file streams keyed by runtime resource payload. #[derive(Default)] pub(crate) struct EvalStreamResources { + chunk_sizes: HashMap, default_stream_context: Option, next_id: i64, directories: HashMap, @@ -194,6 +195,57 @@ impl EvalStreamResources { .is_some_and(|stream| stream.file.flush().is_ok()) } + /// Returns whether a stream's file descriptor is attached to a terminal. + pub(crate) fn isatty(&self, id: i64) -> Option { + let stream = self.streams.get(&id)?; + let result = unsafe { + // libc only reads the descriptor value during the terminal probe. + libc::isatty(stream.file.as_raw_fd()) + }; + Some(result == 1) + } + + /// Toggles blocking mode on a stream's file descriptor. + pub(crate) fn set_blocking(&self, id: i64, enable: bool) -> Option { + let stream = self.streams.get(&id)?; + let fd = stream.file.as_raw_fd(); + let flags = unsafe { + // fcntl reads the current descriptor flags without taking ownership. + libc::fcntl(fd, libc::F_GETFL) + }; + if flags < 0 { + return Some(false); + } + let flags = if enable { + flags & !libc::O_NONBLOCK + } else { + flags | libc::O_NONBLOCK + }; + let result = unsafe { + // fcntl updates the descriptor flags in place. + libc::fcntl(fd, libc::F_SETFL, flags) + }; + Some(result == 0) + } + + /// Reports timeout-setting support for local file streams. + pub(crate) fn set_timeout(&self, id: i64, _seconds: i64, _microseconds: i64) -> Option { + self.streams.get(&id).map(|_| false) + } + + /// Stores a per-stream chunk size and returns the previous size. + pub(crate) fn set_chunk_size(&mut self, id: i64, size: i64) -> Option { + if !self.streams.contains_key(&id) || size <= 0 { + return None; + } + Some(self.chunk_sizes.insert(id, size).unwrap_or(8192)) + } + + /// Accepts read/write buffer settings for local file streams. + pub(crate) fn set_buffer(&self, id: i64, _size: i64) -> Option { + self.streams.get(&id).map(|_| 0) + } + /// Applies an advisory lock operation to a stream's backing file descriptor. pub(crate) fn flock(&self, id: i64, operation: i64) -> Option<(bool, bool)> { let stream = self.streams.get(&id)?; From cdfea6141b4508d0bfed12471542ecee038afdb9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:52:43 +0200 Subject: [PATCH 0316/1208] Add eval class metadata builtins --- .../interpreter/builtins/class_metadata.rs | 146 ++++++++++++++++++ .../src/interpreter/builtins/mod.rs | 2 + .../interpreter/builtins/registry/binding.rs | 5 + .../builtins/registry/dispatch/symbols.rs | 6 + .../interpreter/builtins/registry/names.rs | 6 + .../src/interpreter/expressions.rs | 9 ++ .../tests/builtins_class_metadata.rs | 75 +++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + 8 files changed, 250 insertions(+) create mode 100644 crates/elephc-eval/src/interpreter/builtins/class_metadata.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs new file mode 100644 index 0000000000..8b8bf29968 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -0,0 +1,146 @@ +//! Purpose: +//! Implements eval class metadata and class-relation introspection builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - Eval-declared classes currently have no parent, interface, trait, or +//! attribute metadata, so known class-like targets return empty arrays. +//! - Missing class-like relation targets return `false`, matching the main +//! backend's unknown-target fallback. + +use super::super::*; +use super::*; + +/// Evaluates `class_implements()`, `class_parents()`, or `class_uses()`. +pub(in crate::interpreter) fn eval_builtin_class_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let target = eval_expr(&args[0], context, scope, values)?; + if let Some(autoload) = args.get(1) { + let _ = eval_expr(autoload, context, scope, values)?; + } + eval_class_relation_target_result(name, target, context, values) +} + +/// Evaluates materialized class-relation builtin arguments. +pub(in crate::interpreter) fn eval_class_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let target = match evaluated_args { + [target] => *target, + [target, _autoload] => *target, + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_relation_target_result(name, target, context, values) +} + +/// Resolves one class-relation target and returns an empty relation set or false. +pub(in crate::interpreter) fn eval_class_relation_target_result( + name: &str, + target: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(name, "class_implements" | "class_parents" | "class_uses") { + return Err(EvalStatus::RuntimeFatal); + } + if !eval_class_relation_target_exists(target, context, values)? { + return values.bool_value(false); + } + values.assoc_new(0) +} + +/// Evaluates class attribute metadata helpers. +pub(in crate::interpreter) fn eval_builtin_class_attribute_metadata( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = match (name, args) { + ("class_attribute_names" | "class_get_attributes", [class_name]) => { + vec![eval_expr(class_name, context, scope, values)?] + } + ("class_attribute_args", [class_name, attribute_name]) => vec![ + eval_expr(class_name, context, scope, values)?, + eval_expr(attribute_name, context, scope, values)?, + ], + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_attribute_metadata_result(name, &evaluated_args, values) +} + +/// Evaluates materialized class attribute metadata arguments. +pub(in crate::interpreter) fn eval_class_attribute_metadata_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, evaluated_args) { + ("class_attribute_names" | "class_get_attributes", [class_name]) => { + let _ = eval_class_metadata_name(*class_name, values)?; + values.array_new(0) + } + ("class_attribute_args", [class_name, attribute_name]) => { + let _ = eval_class_metadata_name(*class_name, values)?; + let _ = eval_class_metadata_name(*attribute_name, values)?; + values.array_new(0) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether a class-relation target refers to a known class-like symbol. +fn eval_class_relation_target_exists( + target: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(target)? == EVAL_TAG_OBJECT { + let name = eval_get_class_result(target, context, values)?; + let name = eval_class_metadata_name(name, values)?; + return eval_class_relation_name_exists(&name, context, values); + } + let name = eval_class_metadata_name(target, values)?; + eval_class_relation_name_exists(&name, context, values) +} + +/// Returns whether one normalized class-like name exists in eval or runtime metadata. +fn eval_class_relation_name_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(name) + || values.class_exists(name)? + || values.interface_exists(name)? + || values.trait_exists(name)? + { + return Ok(true); + } + values.enum_exists(name) +} + +/// Reads and normalizes one class metadata string argument. +fn eval_class_metadata_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_string()) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/mod.rs b/crates/elephc-eval/src/interpreter/builtins/mod.rs index 63e6447d8b..bf23354d2c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/mod.rs @@ -12,6 +12,7 @@ //! - Runtime value creation and PHP coercions still flow through `RuntimeValueOps`. mod arrays; +mod class_metadata; mod filesystem; mod formatting; mod network_env; @@ -23,6 +24,7 @@ mod symbols; mod time; pub(super) use arrays::*; +pub(super) use class_metadata::*; pub(super) use filesystem::*; pub(super) use formatting::*; pub(super) use network_env::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 55ec8f4ba2..f8824ecae1 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -142,7 +142,12 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), "class_alias" => Some(&["class", "alias", "autoload"]), + "class_attribute_args" => Some(&["class_name", "attribute_name"]), + "class_attribute_names" | "class_get_attributes" => Some(&["class_name"]), "class_exists" => Some(&["class", "autoload"]), + "class_implements" | "class_parents" | "class_uses" => { + Some(&["object_or_class", "autoload"]) + } "enum_exists" => Some(&["enum", "autoload"]), "interface_exists" => Some(&["interface", "autoload"]), "trait_exists" => Some(&["trait", "autoload"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index 9a317844f6..60318e1f16 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -42,6 +42,12 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( } "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, "class_alias" => eval_class_alias_result(evaluated_args, context, values)?, + "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { + eval_class_attribute_metadata_result(name, evaluated_args, values)? + } + "class_implements" | "class_parents" | "class_uses" => { + eval_class_relation_result(name, evaluated_args, context, values)? + } "enum_exists" | "trait_exists" => { eval_class_like_exists_result(name, evaluated_args, values)? } diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 32833f4164..c2754a28dc 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -61,10 +61,16 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "chmod" | "chown" | "class_alias" + | "class_attribute_args" + | "class_attribute_names" | "closedir" | "call_user_func" | "call_user_func_array" | "class_exists" + | "class_get_attributes" + | "class_implements" + | "class_parents" + | "class_uses" | "enum_exists" | "interface_exists" | "is_a" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 6910056ac4..ec55c1c112 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -424,7 +424,16 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "class_alias" => eval_builtin_class_alias(args, context, scope, values), + "class_attribute_args" => { + eval_builtin_class_attribute_metadata(name, args, context, scope, values) + } + "class_attribute_names" | "class_get_attributes" => { + eval_builtin_class_attribute_metadata(name, args, context, scope, values) + } "class_exists" => eval_builtin_class_exists(args, context, scope, values), + "class_implements" | "class_parents" | "class_uses" => { + eval_builtin_class_relation(name, args, context, scope, values) + } "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), "trait_exists" | "enum_exists" => { eval_builtin_class_like_exists(name, args, context, scope, values) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs new file mode 100644 index 0000000000..d099ad442d --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Interpreter tests for eval class metadata and relation builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Eval class declarations currently carry no parent/interface/trait/attribute metadata. +//! - Tests verify direct calls, dynamic calls, named arguments, and builtin probes. + +use super::super::*; +use super::support::*; + +/// Verifies class-relation helpers return empty arrays for known eval classes. +#[test] +fn execute_program_dispatches_class_relation_builtins() { + let program = parse_fragment( + br#"class EvalMeta {} +$object = new EvalMeta(); +$implements = class_implements("EvalMeta"); +echo is_array($implements) && count($implements) === 0 ? "impl" : "bad"; echo ":"; +$parents = class_parents($object); +echo is_array($parents) && count($parents) === 0 ? "parents" : "bad"; echo ":"; +$uses = class_uses("EvalMeta"); +echo is_array($uses) && count($uses) === 0 ? "uses" : "bad"; echo ":"; +echo class_implements("MissingMeta") === false ? "missing" : "bad"; echo ":"; +$call = call_user_func("class_implements", "EvalMeta"); +echo is_array($call) && count($call) === 0 ? "call" : "bad"; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => "EvalMeta"]); +echo is_array($named) && count($named) === 0 ? "named" : "bad"; echo ":"; +echo function_exists("class_implements"); echo function_exists("class_parents"); +echo function_exists("class_uses"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "impl:parents:uses:missing:call:named:111"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies class attribute helpers expose empty metadata arrays in eval. +#[test] +fn execute_program_dispatches_class_attribute_metadata_builtins() { + let program = parse_fragment( + br#"class EvalAttrMeta {} +$names = class_attribute_names("EvalAttrMeta"); +echo is_array($names) && count($names) === 0 ? "names" : "bad"; echo ":"; +$attrs = class_get_attributes("EvalAttrMeta"); +echo is_array($attrs) && count($attrs) === 0 ? "attrs" : "bad"; echo ":"; +$args = class_attribute_args("EvalAttrMeta", "DemoAttr"); +echo is_array($args) && count($args) === 0 ? "args" : "bad"; echo ":"; +$call_names = call_user_func("class_attribute_names", "EvalAttrMeta"); +echo is_array($call_names) && count($call_names) === 0 ? "callnames" : "bad"; echo ":"; +$call_args = call_user_func_array( + "class_attribute_args", + ["class_name" => "EvalAttrMeta", "attribute_name" => "DemoAttr"] +); +echo is_array($call_args) && count($call_args) === 0 ? "callargs" : "bad"; echo ":"; +echo function_exists("class_attribute_names"); echo function_exists("class_get_attributes"); +echo function_exists("class_attribute_args"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "names:attrs:args:callnames:callargs:111"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 04949cba57..85507702e4 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -14,6 +14,7 @@ mod array_literals; mod builtins_arrays_core; mod builtins_arrays_iterators; mod builtins_arrays_sets; +mod builtins_class_metadata; mod builtins_directory_streams; mod builtins_file_streams; mod builtins_filesystem_metadata; From e484811250a4d4ea67110827e381fe3d2efc9e44 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:56:49 +0200 Subject: [PATCH 0317/1208] Add eval SPL autoload builtins --- crates/elephc-eval/src/context.rs | 13 ++ .../src/interpreter/builtins/mod.rs | 2 + .../interpreter/builtins/registry/binding.rs | 7 +- .../builtins/registry/dispatch/symbols.rs | 12 ++ .../interpreter/builtins/registry/names.rs | 6 + .../src/interpreter/builtins/spl_autoload.rs | 132 ++++++++++++++++++ .../src/interpreter/expressions.rs | 12 ++ .../tests/builtins_spl_autoload.rs | 49 +++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + 9 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/spl_autoload.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_spl_autoload.rs diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index cc51c4d195..b2b9ccede4 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -99,6 +99,7 @@ pub struct ElephcEvalContext { global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, pending_throw: Option, + spl_autoload_extensions: String, streams: EvalStreamResources, json_last_error: i64, json_last_error_msg: String, @@ -125,6 +126,7 @@ impl ElephcEvalContext { global_scope: None, function_stack: Vec::new(), pending_throw: None, + spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), json_last_error: 0, json_last_error_msg: String::from("No error"), @@ -152,6 +154,7 @@ impl ElephcEvalContext { global_scope: None, function_stack: Vec::new(), pending_throw: None, + spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), json_last_error: 0, json_last_error_msg: String::from("No error"), @@ -395,6 +398,16 @@ impl ElephcEvalContext { self.pending_throw.take() } + /// Returns the eval-local SPL autoload extension list. + pub fn spl_autoload_extensions(&self) -> &str { + &self.spl_autoload_extensions + } + + /// Replaces the eval-local SPL autoload extension list. + pub fn set_spl_autoload_extensions(&mut self, extensions: impl Into) { + self.spl_autoload_extensions = extensions.into(); + } + /// Returns the eval-local stream resource table. pub(crate) fn stream_resources(&self) -> &EvalStreamResources { &self.streams diff --git a/crates/elephc-eval/src/interpreter/builtins/mod.rs b/crates/elephc-eval/src/interpreter/builtins/mod.rs index bf23354d2c..348f5be6da 100644 --- a/crates/elephc-eval/src/interpreter/builtins/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/mod.rs @@ -19,6 +19,7 @@ mod network_env; mod regex; mod registry; mod scalars; +mod spl_autoload; mod strings; mod symbols; mod time; @@ -31,6 +32,7 @@ pub(super) use network_env::*; pub(super) use regex::*; pub(super) use registry::*; pub(super) use scalars::*; +pub(super) use spl_autoload::*; pub(super) use strings::*; pub(super) use symbols::*; pub(super) use time::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index f8824ecae1..5ca29f8b3c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -270,7 +270,12 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), - "spl_classes" => Some(&[]), + "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), + "spl_autoload_unregister" => Some(&["callback"]), + "spl_autoload_functions" | "spl_classes" => Some(&[]), + "spl_autoload_extensions" => Some(&["file_extensions"]), + "spl_autoload_call" => Some(&["class"]), + "spl_autoload" => Some(&["class", "file_extensions"]), "spl_object_id" | "spl_object_hash" => Some(&["object"]), "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index 60318e1f16..9899d90b16 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -25,6 +25,18 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( } eval_spl_classes_result(values)? } + "spl_autoload_register" | "spl_autoload_unregister" => { + eval_spl_autoload_bool_result(name, evaluated_args, values)? + } + "spl_autoload" | "spl_autoload_call" => { + eval_spl_autoload_void_result(name, evaluated_args, values)? + } + "spl_autoload_functions" => { + eval_spl_autoload_functions_result(evaluated_args, values)? + } + "spl_autoload_extensions" => { + eval_spl_autoload_extensions_result(evaluated_args, context, values)? + } "spl_object_id" | "spl_object_hash" => { let [object] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index c2754a28dc..cadadfe17d 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -282,6 +282,12 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "sinh" | "sort" | "sqrt" + | "spl_autoload" + | "spl_autoload_call" + | "spl_autoload_extensions" + | "spl_autoload_functions" + | "spl_autoload_register" + | "spl_autoload_unregister" | "spl_classes" | "spl_object_hash" | "spl_object_id" diff --git a/crates/elephc-eval/src/interpreter/builtins/spl_autoload.rs b/crates/elephc-eval/src/interpreter/builtins/spl_autoload.rs new file mode 100644 index 0000000000..ca09135061 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/spl_autoload.rs @@ -0,0 +1,132 @@ +//! Purpose: +//! Implements eval SPL autoload helper builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - The main EIR backend models autoload registration as conservative stubs. +//! - Eval mirrors that behavior while keeping `spl_autoload_extensions()` as +//! eval-local mutable state on the context. + +use super::super::*; + +/// Evaluates boolean SPL autoload registration stubs. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_register" if args.len() <= 3 => {} + "spl_autoload_unregister" if args.len() == 1 => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + for arg in args { + let _ = eval_expr(arg, context, scope, values)?; + } + values.bool_value(true) +} + +/// Evaluates materialized boolean SPL autoload registration stubs. +pub(in crate::interpreter) fn eval_spl_autoload_bool_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_register" if evaluated_args.len() <= 3 => values.bool_value(true), + "spl_autoload_unregister" if evaluated_args.len() == 1 => values.bool_value(true), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates void SPL autoload call stubs. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_void( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_call" if args.len() == 1 => {} + "spl_autoload" if (1..=2).contains(&args.len()) => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + for arg in args { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_spl_autoload_void_result(name, args, values) +} + +/// Evaluates materialized void SPL autoload call stubs. +pub(in crate::interpreter) fn eval_spl_autoload_void_result( + name: &str, + evaluated_args: &[T], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_call" if evaluated_args.len() == 1 => values.null(), + "spl_autoload" if (1..=2).contains(&evaluated_args.len()) => values.null(), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `spl_autoload_functions()`. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_functions( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_spl_autoload_functions_result(args, values) +} + +/// Evaluates materialized `spl_autoload_functions()`. +pub(in crate::interpreter) fn eval_spl_autoload_functions_result( + evaluated_args: &[T], + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.array_new(0) +} + +/// Evaluates `spl_autoload_extensions()`. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_extensions( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = match args { + [] => Vec::new(), + [extensions] => vec![eval_expr(extensions, context, scope, values)?], + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_spl_autoload_extensions_result(&evaluated_args, context, values) +} + +/// Evaluates materialized `spl_autoload_extensions()` arguments. +pub(in crate::interpreter) fn eval_spl_autoload_extensions_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => {} + [extensions] if values.type_tag(*extensions)? == EVAL_TAG_NULL => {} + [extensions] => { + let extensions = values.string_bytes(*extensions)?; + let extensions = String::from_utf8(extensions).map_err(|_| EvalStatus::RuntimeFatal)?; + context.set_spl_autoload_extensions(extensions); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + values.string(context.spl_autoload_extensions()) +} diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index ec55c1c112..b926ff1f2e 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -599,6 +599,18 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "isset" => eval_builtin_isset(args, context, scope, values), "sleep" => eval_builtin_sleep(args, context, scope, values), "sqrt" => eval_builtin_sqrt(args, context, scope, values), + "spl_autoload_register" | "spl_autoload_unregister" => { + eval_builtin_spl_autoload_bool(name, args, context, scope, values) + } + "spl_autoload" | "spl_autoload_call" => { + eval_builtin_spl_autoload_void(name, args, context, scope, values) + } + "spl_autoload_functions" => { + eval_builtin_spl_autoload_functions(args, context, scope, values) + } + "spl_autoload_extensions" => { + eval_builtin_spl_autoload_extensions(args, context, scope, values) + } "spl_classes" => eval_builtin_spl_classes(args, values), "spl_object_id" | "spl_object_hash" => { eval_builtin_spl_object_identity(name, args, context, scope, values) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_spl_autoload.rs b/crates/elephc-eval/src/interpreter/tests/builtins_spl_autoload.rs new file mode 100644 index 0000000000..b287de1be6 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_spl_autoload.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Interpreter tests for eval SPL autoload helper builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Autoload registration/call helpers mirror the main backend's conservative stubs. +//! - `spl_autoload_extensions()` persists an eval-local extension string. + +use super::super::*; +use super::support::*; + +/// Verifies eval SPL autoload helpers expose stubbed behavior and extension state. +#[test] +fn execute_program_dispatches_spl_autoload_builtins() { + let program = parse_fragment( + br#"echo spl_autoload_extensions() === ".inc,.php" ? "default" : "bad"; echo ":"; +echo spl_autoload_extensions(".php,.inc") === ".php,.inc" ? "set" : "bad"; echo ":"; +echo spl_autoload_extensions(null) === ".php,.inc" ? "read" : "bad"; echo ":"; +echo spl_autoload_register("missing_loader") ? "register" : "bad"; echo ":"; +echo spl_autoload_unregister("missing_loader") ? "unregister" : "bad"; echo ":"; +$funcs = spl_autoload_functions(); +echo is_array($funcs) && count($funcs) === 0 ? "functions" : "bad"; echo ":"; +echo spl_autoload("MissingClass") === null ? "autoload" : "bad"; echo ":"; +echo spl_autoload_call("MissingClass") === null ? "call" : "bad"; echo ":"; +echo call_user_func("spl_autoload_register", "missing_loader") ? "callregister" : "bad"; echo ":"; +$named = call_user_func_array("spl_autoload_extensions", ["file_extensions" => ".class.php"]); +echo $named === ".class.php" ? "namedext" : "bad"; echo ":"; +echo function_exists("spl_autoload"); echo function_exists("spl_autoload_call"); +echo function_exists("spl_autoload_extensions"); echo function_exists("spl_autoload_functions"); +echo function_exists("spl_autoload_register"); echo function_exists("spl_autoload_unregister"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "default:set:read:register:unregister:functions:autoload:call:", + "callregister:namedext:111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 85507702e4..278c53075b 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -22,6 +22,7 @@ mod builtins_filesystem_ops; mod builtins_json; mod builtins_math_formatting; mod builtins_scalars; +mod builtins_spl_autoload; mod builtins_stream_contexts; mod builtins_stream_settings; mod builtins_strings_binary; From 12638d16e5a467412f6ec0e10d027fbc5744082d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 14:58:27 +0200 Subject: [PATCH 0318/1208] Add eval readline builtin --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/readline.rs | 53 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/filesystem.rs | 7 +++ .../interpreter/builtins/registry/names.rs | 1 + .../src/interpreter/expressions.rs | 1 + .../interpreter/tests/builtins_readline.rs | 31 +++++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + 8 files changed, 97 insertions(+) create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/readline.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_readline.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs index ca9c59881d..de567648dc 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -13,6 +13,7 @@ mod file_io; mod fnmatch; mod ops; mod path; +mod readline; mod stream_context; mod stream_settings; mod streams; @@ -22,6 +23,7 @@ pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use ops::*; pub(in crate::interpreter) use path::*; +pub(in crate::interpreter) use readline::*; pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use streams::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/readline.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/readline.rs new file mode 100644 index 0000000000..8978ab90f5 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/readline.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Implements eval's PHP `readline()` builtin against host stdin. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - EOF returns PHP `false`, matching the runtime `__rt_fgets` helper. +//! - Returned lines exclude a trailing LF or CRLF terminator. + +use std::io; + +use super::super::super::*; + +/// Evaluates `readline([prompt])`. +pub(in crate::interpreter) fn eval_builtin_readline( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let prompt = match args { + [] => None, + [prompt] => Some(eval_expr(prompt, context, scope, values)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_readline_result(prompt, values) +} + +/// Reads one line from host stdin after optionally echoing a prompt. +pub(in crate::interpreter) fn eval_readline_result( + prompt: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(prompt) = prompt { + values.echo(prompt)?; + } + let mut line = String::new(); + let read = io::stdin() + .read_line(&mut line) + .map_err(|_| EvalStatus::RuntimeFatal)?; + if read == 0 { + return values.bool_value(false); + } + if line.ends_with('\n') { + line.pop(); + if line.ends_with('\r') { + line.pop(); + } + } + values.string(&line) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 5ca29f8b3c..ab418f55c2 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -266,6 +266,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "range" => Some(&["start", "end"]), + "readline" => Some(&["prompt"]), "realpath" | "stream_resolve_include_path" => Some(&["path"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index cc9fd41f33..cd9164f2f7 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -224,6 +224,13 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_readfile_result(*filename, values)? } + "readline" => { + if evaluated_args.len() > 1 { + return Err(EvalStatus::RuntimeFatal); + } + let prompt = evaluated_args.first().copied(); + eval_readline_result(prompt, values)? + } "scandir" => { let [directory] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index cadadfe17d..a6e9b724c6 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -260,6 +260,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "rawurldecode" | "rawurlencode" | "readfile" + | "readline" | "readdir" | "readlink" | "realpath" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index b926ff1f2e..e94b6a5bc3 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -590,6 +590,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "rawurldecode" | "urldecode" => eval_builtin_url_decode(name, args, context, scope, values), "rawurlencode" | "urlencode" => eval_builtin_url_encode(name, args, context, scope, values), "readfile" => eval_builtin_readfile(args, context, scope, values), + "readline" => eval_builtin_readline(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_readline.rs b/crates/elephc-eval/src/interpreter/tests/builtins_readline.rs new file mode 100644 index 0000000000..21e3a7280f --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_readline.rs @@ -0,0 +1,31 @@ +//! Purpose: +//! Interpreter tests for eval's `readline()` builtin. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - The test harness runs with stdin at EOF, so `readline()` returns false +//! without blocking for terminal input. + +use super::super::*; +use super::support::*; + +/// Verifies `readline()` reports EOF and participates in callable dispatch. +#[test] +fn execute_program_dispatches_readline_builtin() { + let program = parse_fragment( + br#"echo readline() === false ? "eof" : "bad"; echo ":"; +echo call_user_func("readline") === false ? "call" : "bad"; echo ":"; +echo function_exists("readline"); echo is_callable("readline"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "eof:call:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 278c53075b..025be292ec 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -20,6 +20,7 @@ mod builtins_file_streams; mod builtins_filesystem_metadata; mod builtins_filesystem_ops; mod builtins_json; +mod builtins_readline; mod builtins_math_formatting; mod builtins_scalars; mod builtins_spl_autoload; From 3ff5e1ab4d6815843b66e03932cf93c6237c80e5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 15:01:40 +0200 Subject: [PATCH 0319/1208] Add eval process pipe builtins --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/process_pipes.rs | 99 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 2 + .../builtins/registry/dispatch/filesystem.rs | 12 +++ .../interpreter/builtins/registry/names.rs | 2 + .../src/interpreter/expressions.rs | 2 + .../tests/builtins_process_pipes.rs | 48 +++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + crates/elephc-eval/src/stream_resources.rs | 64 +++++++++++- 9 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/process_pipes.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_process_pipes.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs index de567648dc..7f3be4218b 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -13,6 +13,7 @@ mod file_io; mod fnmatch; mod ops; mod path; +mod process_pipes; mod readline; mod stream_context; mod stream_settings; @@ -23,6 +24,7 @@ pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use ops::*; pub(in crate::interpreter) use path::*; +pub(in crate::interpreter) use process_pipes::*; pub(in crate::interpreter) use readline::*; pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_settings::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/process_pipes.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/process_pipes.rs new file mode 100644 index 0000000000..b4a925128b --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/process_pipes.rs @@ -0,0 +1,99 @@ +//! Purpose: +//! Implements eval process pipe builtins `popen()` and `pclose()`. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - Process pipes are stored as normal eval stream resources plus a child +//! process table entry so existing fread/fwrite paths work unchanged. +//! - `pclose()` removes the stream first, then waits for the child exit status. + +use super::super::super::*; +use super::*; + +/// Evaluates `popen(command, mode)`. +pub(in crate::interpreter) fn eval_builtin_popen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [command, mode] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let command = eval_expr(command, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_popen_result(command, mode, context, values) +} + +/// Opens a shell process pipe and returns a stream resource or false. +pub(in crate::interpreter) fn eval_popen_result( + command: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let command = eval_path_string(command, values)?; + let mode = eval_process_pipe_mode(mode, values)?; + match context + .stream_resources_mut() + .open_process_pipe(&command, &mode) + { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates `pclose(handle)`. +pub(in crate::interpreter) fn eval_builtin_pclose( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [handle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let handle = eval_expr(handle, context, scope, values)?; + eval_pclose_result(handle, context, values) +} + +/// Closes a process pipe and returns its exit code, or false for invalid handles. +pub(in crate::interpreter) fn eval_pclose_result( + handle: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_process_pipe_resource_id(handle, values)?; + match context.stream_resources_mut().pclose(id) { + Some(status) => values.int(status), + None => values.bool_value(false), + } +} + +/// Reads a `popen()` mode string, accepting read or write pipe modes. +fn eval_process_pipe_mode( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = values.string_bytes(mode)?; + let mode = String::from_utf8(mode).map_err(|_| EvalStatus::RuntimeFatal)?; + match mode.chars().next() { + Some('r' | 'w') => Ok(mode), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based process-pipe id. +fn eval_process_pipe_resource_id( + handle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(handle)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(handle, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index ab418f55c2..06501d0e45 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -256,6 +256,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "pi" => Some(&[]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), + "pclose" => Some(&["handle"]), + "popen" => Some(&["command", "mode"]), "pow" => Some(&["num", "exponent"]), "preg_match" => Some(&["pattern", "subject", "matches", "flags", "offset"]), "preg_match_all" => Some(&["pattern", "subject", "matches", "flags", "offset"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index cd9164f2f7..c995be3a68 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -276,6 +276,18 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, _ => return Err(EvalStatus::RuntimeFatal), }, + "pclose" => { + let [handle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pclose_result(*handle, context, values)? + } + "popen" => { + let [command, mode] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_popen_result(*command, *mode, context, values)? + } "realpath" => { let [path] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index a6e9b724c6..4cc8c075f4 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -246,6 +246,8 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "pow" | "php_uname" | "phpversion" + | "pclose" + | "popen" | "preg_match" | "preg_match_all" | "preg_replace" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index e94b6a5bc3..cfe1de40a6 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -576,6 +576,8 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "pi" => eval_builtin_pi(args, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), + "pclose" => eval_builtin_pclose(args, context, scope, values), + "popen" => eval_builtin_popen(args, context, scope, values), "pow" => eval_builtin_pow(args, context, scope, values), "preg_match" => eval_builtin_preg_match(args, context, scope, values), "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_process_pipes.rs b/crates/elephc-eval/src/interpreter/tests/builtins_process_pipes.rs new file mode 100644 index 0000000000..c24fccbdf9 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_process_pipes.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Interpreter tests for eval process pipe stream builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Tests use shell commands that exit immediately and clean up their temp file. +//! - `popen()` resources are normal eval streams until `pclose()` waits for the child. + +use super::super::*; +use super::support::*; + +/// Verifies `popen()` and `pclose()` support read/write pipes and dynamic calls. +#[test] +fn execute_program_dispatches_process_pipe_builtins() { + let pid = std::process::id(); + let file = format!("elephc_eval_popen_{pid}.txt"); + let source = format!( + r#"$h = popen("printf eval-popen", "r"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo fread($h, 64) === "eval-popen" ? "read" : "bad"; echo ":"; +echo pclose($h) === 0 ? "closed" : "bad"; echo ":"; +$w = popen("cat > {file}", "w"); +echo fwrite($w, "pipeout") === 7 ? "write" : "bad"; echo ":"; +echo pclose($w) === 0 ? "wclosed" : "bad"; echo ":"; +echo file_get_contents("{file}") === "pipeout" ? "file" : "bad"; echo ":"; +$call = call_user_func("popen", "printf call-pipe", "r"); +echo stream_get_contents($call) === "call-pipe" ? "callread" : "bad"; echo ":"; +echo call_user_func("pclose", $call) === 0 ? "callclose" : "bad"; echo ":"; +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("popen"); echo function_exists("pclose"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + "open:read:closed:write:wclosed:file:callread:callclose:cleanup:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 025be292ec..c631fbcc8f 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -22,6 +22,7 @@ mod builtins_filesystem_ops; mod builtins_json; mod builtins_readline; mod builtins_math_formatting; +mod builtins_process_pipes; mod builtins_scalars; mod builtins_spl_autoload; mod builtins_stream_contexts; diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index 6d81f13ece..5592992b33 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -15,8 +15,9 @@ use std::collections::HashMap; use std::ffi::c_void; use std::fs::{File, Metadata, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; -use std::os::fd::AsRawFd; +use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; use crate::value::RuntimeCellHandle; @@ -28,6 +29,7 @@ pub(crate) struct EvalStreamResources { next_id: i64, directories: HashMap, hash_contexts: HashMap, + process_children: HashMap, stream_contexts: HashMap, streams: HashMap, } @@ -57,6 +59,52 @@ impl EvalStreamResources { ))) } + /// Opens a shell process pipe and returns its stream resource id. + pub(crate) fn open_process_pipe(&mut self, command: &str, mode: &str) -> Option { + let read_mode = match mode.chars().next()? { + 'r' => true, + 'w' => false, + _ => return None, + }; + let mut child = Command::new("/bin/sh") + .arg("-c") + .arg(command) + .stdin(if read_mode { + Stdio::null() + } else { + Stdio::piped() + }) + .stdout(if read_mode { + Stdio::piped() + } else { + Stdio::null() + }) + .spawn() + .ok()?; + let file = if read_mode { + let stdout = child.stdout.take()?; + unsafe { + // The ChildStdout pipe is converted into the File that backs + // this eval stream; no second owner keeps the fd alive. + File::from_raw_fd(stdout.into_raw_fd()) + } + } else { + let stdin = child.stdin.take()?; + unsafe { + // The ChildStdin pipe is converted into the File that backs + // this eval stream; dropping it before wait sends EOF. + File::from_raw_fd(stdin.into_raw_fd()) + } + }; + let id = self.insert(EvalFileStream::new( + file, + command.to_string(), + if read_mode { "r" } else { "w" }.to_string(), + )); + self.process_children.insert(id, child); + Some(id) + } + /// Opens a local directory and returns its resource id. pub(crate) fn open_directory(&mut self, path: &str) -> Option { let directory = EvalDirectoryStream::open(path)?; @@ -93,7 +141,19 @@ impl EvalStreamResources { /// Removes a stream resource from the table, closing its file handle. pub(crate) fn close(&mut self, id: i64) -> bool { - self.streams.remove(&id).is_some() + let closed = self.streams.remove(&id).is_some(); + if let Some(mut child) = self.process_children.remove(&id) { + let _ = child.wait(); + } + closed + } + + /// Closes a process pipe stream and returns the child exit status. + pub(crate) fn pclose(&mut self, id: i64) -> Option { + let mut child = self.process_children.remove(&id)?; + self.streams.remove(&id)?; + let status = child.wait().ok()?; + Some(status.code().unwrap_or(0) as i64) } /// Removes a directory resource from the table. From 205192656a88f14248e02dae319591276967313d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 15:05:31 +0200 Subject: [PATCH 0320/1208] Add eval stream extension builtins --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/stream_extensions.rs | 289 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 10 + .../builtins/registry/dispatch/filesystem.rs | 45 +++ .../interpreter/builtins/registry/names.rs | 11 + .../src/interpreter/expressions.rs | 19 ++ .../tests/builtins_stream_extensions.rs | 71 +++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + crates/elephc-eval/src/stream_resources.rs | 23 +- 9 files changed, 469 insertions(+), 2 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/stream_extensions.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_stream_extensions.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs index 7f3be4218b..979109de5b 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -15,6 +15,7 @@ mod ops; mod path; mod process_pipes; mod readline; +mod stream_extensions; mod stream_context; mod stream_settings; mod streams; @@ -26,6 +27,7 @@ pub(in crate::interpreter) use ops::*; pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use process_pipes::*; pub(in crate::interpreter) use readline::*; +pub(in crate::interpreter) use stream_extensions::*; pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use streams::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_extensions.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_extensions.rs new file mode 100644 index 0000000000..cabc2a2936 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_extensions.rs @@ -0,0 +1,289 @@ +//! Purpose: +//! Implements eval stream wrapper, stream filter, and stream bucket helper builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - Wrapper/filter registries are conservative eval stubs matching the main +//! backend surface without changing stream bytes. +//! - Buckets are represented as `stdClass` objects with `data`, `datalen`, and +//! brigade `_buckets` properties, mirroring the generated runtime model. + +use super::super::super::*; + +/// Evaluates stream wrapper registration builtins. +pub(in crate::interpreter) fn eval_builtin_stream_wrapper_registry( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "stream_wrapper_register" if (2..=3).contains(&args.len()) => {} + "stream_wrapper_unregister" | "stream_wrapper_restore" if args.len() == 1 => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + for arg in args { + let value = eval_expr(arg, context, scope, values)?; + if matches!(name, "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore") { + let _ = values.string_bytes(value).ok(); + } + } + values.bool_value(true) +} + +/// Evaluates stream wrapper registration builtins from materialized arguments. +pub(in crate::interpreter) fn eval_stream_wrapper_registry_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "stream_wrapper_register" if (2..=3).contains(&evaluated_args.len()) => {} + "stream_wrapper_unregister" | "stream_wrapper_restore" if evaluated_args.len() == 1 => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + for value in evaluated_args { + let _ = values.string_bytes(*value).ok(); + } + values.bool_value(true) +} + +/// Evaluates `stream_filter_register(filter_name, class)`. +pub(in crate::interpreter) fn eval_builtin_stream_filter_register( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filter_name, class] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filter_name = eval_expr(filter_name, context, scope, values)?; + let class = eval_expr(class, context, scope, values)?; + eval_stream_filter_register_result(filter_name, class, values) +} + +/// Evaluates a materialized `stream_filter_register()` call. +pub(in crate::interpreter) fn eval_stream_filter_register_result( + filter_name: RuntimeCellHandle, + class: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.string_bytes(filter_name)?; + let _ = values.string_bytes(class)?; + values.bool_value(true) +} + +/// Evaluates `stream_filter_append()` or `stream_filter_prepend()`. +pub(in crate::interpreter) fn eval_builtin_stream_filter_attach( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let filter_name = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_filter_attach_result(name, stream, filter_name, context, values) +} + +/// Creates an eval-local filter resource for a materialized stream filter attach. +pub(in crate::interpreter) fn eval_stream_filter_attach_result( + name: &str, + stream: RuntimeCellHandle, + filter_name: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(name, "stream_filter_append" | "stream_filter_prepend") { + return Err(EvalStatus::RuntimeFatal); + } + let stream_id = eval_stream_extension_resource_id(stream, values)?; + let _ = values.string_bytes(filter_name)?; + if !context.stream_resources().has_stream(stream_id) { + return values.bool_value(false); + } + let filter_id = context.stream_resources_mut().open_filter_resource(); + values.resource(filter_id) +} + +/// Evaluates `stream_filter_remove(stream_filter)`. +pub(in crate::interpreter) fn eval_builtin_stream_filter_remove( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_filter] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_filter = eval_expr(stream_filter, context, scope, values)?; + eval_stream_filter_remove_result(stream_filter, context, values) +} + +/// Removes an eval-local filter resource. +pub(in crate::interpreter) fn eval_stream_filter_remove_result( + stream_filter: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_extension_resource_id(stream_filter, values)?; + values.bool_value(context.stream_resources_mut().close_filter_resource(id)) +} + +/// Evaluates `stream_bucket_new(stream, buffer)`. +pub(in crate::interpreter) fn eval_builtin_stream_bucket_new( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, buffer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let buffer = eval_expr(buffer, context, scope, values)?; + eval_stream_bucket_new_result(stream, buffer, context, values) +} + +/// Creates a stdClass-backed stream bucket object. +pub(in crate::interpreter) fn eval_stream_bucket_new_result( + stream: RuntimeCellHandle, + buffer: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let stream_id = eval_stream_extension_resource_id(stream, values)?; + if !context.stream_resources().has_stream(stream_id) { + return values.null(); + } + let bytes = values.string_bytes(buffer)?; + let bucket = values.new_object("stdClass")?; + let data = values.string_bytes_value(&bytes)?; + let datalen = values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; + values.property_set(bucket, "data", data)?; + values.property_set(bucket, "datalen", datalen)?; + Ok(bucket) +} + +/// Evaluates `stream_bucket_make_writeable(brigade)`. +pub(in crate::interpreter) fn eval_builtin_stream_bucket_make_writeable( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let brigade = eval_expr(brigade, context, scope, values)?; + eval_stream_bucket_make_writeable_result(brigade, values) +} + +/// Returns the first bucket in a brigade, or null when none exists. +pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_result( + brigade: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let buckets = values.property_get(brigade, "_buckets")?; + if !values.is_array_like(buckets)? || values.array_len(buckets)? == 0 { + return values.null(); + } + let key = values.int(0)?; + values.array_get(buckets, key) +} + +/// Evaluates `stream_bucket_append()` or `stream_bucket_prepend()`. +pub(in crate::interpreter) fn eval_builtin_stream_bucket_push( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade, bucket] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let brigade = eval_expr(brigade, context, scope, values)?; + let bucket = eval_expr(bucket, context, scope, values)?; + eval_stream_bucket_push_result(name, brigade, bucket, values) +} + +/// Adds a bucket object to the brigade's `_buckets` array. +pub(in crate::interpreter) fn eval_stream_bucket_push_result( + name: &str, + brigade: RuntimeCellHandle, + bucket: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let prepend = match name { + "stream_bucket_append" => false, + "stream_bucket_prepend" => true, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let buckets = eval_brigade_buckets(brigade, values)?; + let buckets = if prepend { + eval_bucket_prepend(buckets, bucket, values)? + } else { + let len = values.array_len(buckets)?; + let index = values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?)?; + values.array_set(buckets, index, bucket)? + }; + values.property_set(brigade, "_buckets", buckets)?; + values.null() +} + +/// Returns an existing brigade bucket array or creates an empty one. +fn eval_brigade_buckets( + brigade: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let buckets = values.property_get(brigade, "_buckets")?; + if values.is_array_like(buckets)? { + Ok(buckets) + } else { + values.array_new(0) + } +} + +/// Builds a new bucket array with the provided bucket at index zero. +fn eval_bucket_prepend( + buckets: RuntimeCellHandle, + bucket: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(buckets)?; + let mut result = values.array_new(len + 1)?; + let zero = values.int(0)?; + result = values.array_set(result, zero, bucket)?; + for index in 0..len { + let old_key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.array_get(buckets, old_key)?; + let new_key = + values.int(i64::try_from(index + 1).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, new_key, value)?; + } + Ok(result) +} + +/// Converts a runtime resource cell into eval's zero-based stream-extension id. +fn eval_stream_extension_resource_id( + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(resource, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 06501d0e45..0f9a8d9f76 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -283,6 +283,9 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), "stream_is_local" | "stream_isatty" | "stream_supports_lock" => Some(&["stream"]), + "stream_bucket_make_writeable" => Some(&["brigade"]), + "stream_bucket_new" => Some(&["stream", "buffer"]), + "stream_bucket_append" | "stream_bucket_prepend" => Some(&["brigade", "bucket"]), "stream_copy_to_stream" => Some(&["from", "to", "length", "offset"]), "stream_context_create" => Some(&["options", "params"]), "stream_context_get_default" => Some(&["options"]), @@ -292,6 +295,11 @@ pub(in crate::interpreter) fn eval_builtin_param_names( Some(&["context", "wrapper_or_options", "option_name", "value"]) } "stream_context_set_params" => Some(&["context", "params"]), + "stream_filter_register" => Some(&["filter_name", "class"]), + "stream_filter_append" | "stream_filter_prepend" => { + Some(&["stream", "filtername", "read_write", "params"]) + } + "stream_filter_remove" => Some(&["stream_filter"]), "stream_get_contents" => Some(&["stream", "length", "offset"]), "stream_get_line" => Some(&["stream", "length", "ending"]), "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), @@ -300,6 +308,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( Some(&["stream", "size"]) } "stream_set_timeout" => Some(&["stream", "seconds", "microseconds"]), + "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), + "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), "strtotime" => Some(&["datetime"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index c995be3a68..a07092a51f 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -375,6 +375,51 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } values.bool_value(true)? } + "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { + eval_stream_wrapper_registry_result(name, evaluated_args, values)? + } + "stream_filter_register" => { + let [filter_name, class] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_filter_register_result(*filter_name, *class, values)? + } + "stream_filter_append" | "stream_filter_prepend" => { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_filter_attach_result( + name, + evaluated_args[0], + evaluated_args[1], + context, + values, + )? + } + "stream_filter_remove" => { + let [stream_filter] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_filter_remove_result(*stream_filter, context, values)? + } + "stream_bucket_new" => { + let [stream, buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_new_result(*stream, *buffer, context, values)? + } + "stream_bucket_make_writeable" => { + let [brigade] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_make_writeable_result(*brigade, values)? + } + "stream_bucket_append" | "stream_bucket_prepend" => { + let [brigade, bucket] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_push_result(name, *brigade, *bucket, values)? + } "stream_get_contents" => match evaluated_args { [stream] => eval_stream_get_contents_result(*stream, None, None, context, values)?, [stream, length] => { diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 4cc8c075f4..a7e8d53f3a 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -297,6 +297,10 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "sscanf" | "sprintf" | "strcasecmp" + | "stream_bucket_append" + | "stream_bucket_make_writeable" + | "stream_bucket_new" + | "stream_bucket_prepend" | "stream_copy_to_stream" | "stream_context_create" | "stream_context_get_default" @@ -305,6 +309,10 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "stream_context_set_default" | "stream_context_set_option" | "stream_context_set_params" + | "stream_filter_append" + | "stream_filter_prepend" + | "stream_filter_register" + | "stream_filter_remove" | "stream_get_contents" | "stream_get_filters" | "stream_get_line" @@ -320,6 +328,9 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "stream_set_write_buffer" | "stream_supports_lock" | "stream_resolve_include_path" + | "stream_wrapper_register" + | "stream_wrapper_restore" + | "stream_wrapper_unregister" | "str_contains" | "str_ends_with" | "str_ireplace" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index cfe1de40a6..365a777355 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -656,6 +656,25 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_context_set_params" => { eval_builtin_stream_context_set_params(args, context, scope, values) } + "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { + eval_builtin_stream_wrapper_registry(name, args, context, scope, values) + } + "stream_filter_register" => { + eval_builtin_stream_filter_register(args, context, scope, values) + } + "stream_filter_append" | "stream_filter_prepend" => { + eval_builtin_stream_filter_attach(name, args, context, scope, values) + } + "stream_filter_remove" => { + eval_builtin_stream_filter_remove(args, context, scope, values) + } + "stream_bucket_new" => eval_builtin_stream_bucket_new(args, context, scope, values), + "stream_bucket_make_writeable" => { + eval_builtin_stream_bucket_make_writeable(args, context, scope, values) + } + "stream_bucket_append" | "stream_bucket_prepend" => { + eval_builtin_stream_bucket_push(name, args, context, scope, values) + } "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_stream_extensions.rs b/crates/elephc-eval/src/interpreter/tests/builtins_stream_extensions.rs new file mode 100644 index 0000000000..40bd313449 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_stream_extensions.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Interpreter tests for eval stream wrapper, filter, and bucket helper builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Filter resources are eval-local handles and do not transform stream bytes. +//! - Buckets use stdClass properties so user-filter style code can inspect them. + +use super::super::*; +use super::support::*; + +/// Verifies stream wrapper/filter/bucket helper builtins are callable in eval. +#[test] +fn execute_program_dispatches_stream_extension_builtins() { + let pid = std::process::id(); + let file = format!("elephc_eval_stream_extensions_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "abc"); +$h = fopen("{file}", "r+"); +echo stream_wrapper_register("evaltest", "stdClass") ? "wreg" : "bad"; echo ":"; +echo stream_wrapper_unregister("evaltest") ? "wunreg" : "bad"; echo ":"; +echo stream_wrapper_restore("evaltest") ? "wrestore" : "bad"; echo ":"; +echo stream_filter_register("eval.filter", "stdClass") ? "freg" : "bad"; echo ":"; +$filter = stream_filter_append($h, "string.toupper"); +echo is_resource($filter) ? "fappend" : "bad"; echo ":"; +echo stream_filter_remove($filter) ? "fremove" : "bad"; echo ":"; +$filter = call_user_func("stream_filter_prepend", $h, "string.tolower"); +echo is_resource($filter) ? "fprepend" : "bad"; echo ":"; +echo call_user_func("stream_filter_remove", $filter) ? "fcallremove" : "bad"; echo ":"; +$bucket = stream_bucket_new($h, "payload"); +echo is_object($bucket) && $bucket->data === "payload" && $bucket->datalen === 7 ? "bucket" : "bad"; echo ":"; +$brigade = new stdClass(); +stream_bucket_append($brigade, $bucket); +$out = stream_bucket_make_writeable($brigade); +echo is_object($out) && $out->data === "payload" ? "make" : "bad"; echo ":"; +$brigade2 = new stdClass(); +$first = stream_bucket_new($h, "first"); +$second = stream_bucket_new($h, "second"); +stream_bucket_append($brigade2, $second); +stream_bucket_prepend($brigade2, $first); +$out = stream_bucket_make_writeable($brigade2); +echo is_object($out) && $out->data === "first" ? "prepend" : "bad"; echo ":"; +fclose($h); +echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; +echo function_exists("stream_bucket_append"); echo function_exists("stream_bucket_make_writeable"); +echo function_exists("stream_bucket_new"); echo function_exists("stream_bucket_prepend"); +echo function_exists("stream_filter_append"); echo function_exists("stream_filter_prepend"); +echo function_exists("stream_filter_register"); echo function_exists("stream_filter_remove"); +echo function_exists("stream_wrapper_register"); echo function_exists("stream_wrapper_restore"); +echo function_exists("stream_wrapper_unregister"); +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + concat!( + "wreg:wunreg:wrestore:freg:fappend:fremove:fprepend:fcallremove:", + "bucket:make:prepend:cleanup:11111111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index c631fbcc8f..d11bcc645b 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -26,6 +26,7 @@ mod builtins_process_pipes; mod builtins_scalars; mod builtins_spl_autoload; mod builtins_stream_contexts; +mod builtins_stream_extensions; mod builtins_stream_settings; mod builtins_strings_binary; mod builtins_strings_encoding; diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index 5592992b33..aacee8757d 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -11,7 +11,7 @@ //! - Resource ids are zero-based runtime payloads; PHP display ids are payload + 1. //! - Resource handles are process-local to eval and are not visible across the C ABI. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::ffi::c_void; use std::fs::{File, Metadata, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; @@ -28,6 +28,7 @@ pub(crate) struct EvalStreamResources { default_stream_context: Option, next_id: i64, directories: HashMap, + filter_resources: HashSet, hash_contexts: HashMap, process_children: HashMap, stream_contexts: HashMap, @@ -141,13 +142,31 @@ impl EvalStreamResources { /// Removes a stream resource from the table, closing its file handle. pub(crate) fn close(&mut self, id: i64) -> bool { - let closed = self.streams.remove(&id).is_some(); + let closed = self.streams.remove(&id).is_some() || self.filter_resources.remove(&id); if let Some(mut child) = self.process_children.remove(&id) { let _ = child.wait(); } closed } + /// Returns whether a file-like stream resource exists. + pub(crate) fn has_stream(&self, id: i64) -> bool { + self.streams.contains_key(&id) + } + + /// Allocates an eval-local stream filter resource handle. + pub(crate) fn open_filter_resource(&mut self) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.filter_resources.insert(id); + id + } + + /// Removes an eval-local stream filter resource handle. + pub(crate) fn close_filter_resource(&mut self, id: i64) -> bool { + self.filter_resources.remove(&id) + } + /// Closes a process pipe stream and returns the child exit status. pub(crate) fn pclose(&mut self, id: i64) -> Option { let mut child = self.process_children.remove(&id)?; From a1feee087b76a9ea6463548ab3453c821e2bdefc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 15:12:00 +0200 Subject: [PATCH 0321/1208] Add eval stream socket builtins --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/stream_sockets.rs | 358 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 14 + .../builtins/registry/dispatch/filesystem.rs | 71 ++++ .../interpreter/builtins/registry/names.rs | 12 + .../src/interpreter/expressions.rs | 21 + .../tests/builtins_stream_sockets.rs | 77 ++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + crates/elephc-eval/src/stream_resources.rs | 159 +++++++- 9 files changed, 714 insertions(+), 1 deletion(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/filesystem/stream_sockets.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_stream_sockets.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs index 979109de5b..8eebfbf1bf 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs @@ -18,6 +18,7 @@ mod readline; mod stream_extensions; mod stream_context; mod stream_settings; +mod stream_sockets; mod streams; pub(in crate::interpreter) use directories::*; @@ -30,4 +31,5 @@ pub(in crate::interpreter) use readline::*; pub(in crate::interpreter) use stream_extensions::*; pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_settings::*; +pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_sockets.rs b/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_sockets.rs new file mode 100644 index 0000000000..f969c8edc3 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_sockets.rs @@ -0,0 +1,358 @@ +//! Purpose: +//! Implements eval stream socket builtins over host TCP and local socket pairs. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - TCP streams are inserted into eval's normal File-backed stream table so +//! existing fread/fwrite/close paths keep working. +//! - TLS enablement is conservative: disabling succeeds for valid streams, +//! enabling returns false because eval does not own TLS state. + +use super::super::super::*; +use super::*; + +/// Evaluates `stream_socket_server(address)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_server( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [address] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_expr(address, context, scope, values)?; + eval_stream_socket_server_result(address, context, values) +} + +/// Opens a TCP listener resource. +pub(in crate::interpreter) fn eval_stream_socket_server_result( + address: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_path_string(address, values)?; + match context.stream_resources_mut().open_tcp_listener(&address) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates `stream_socket_client(address)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_client( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [address] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_expr(address, context, scope, values)?; + eval_stream_socket_client_result(address, context, values) +} + +/// Opens a connected TCP stream resource. +pub(in crate::interpreter) fn eval_stream_socket_client_result( + address: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_path_string(address, values)?; + match context.stream_resources_mut().open_tcp_stream(&address) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates `fsockopen()` or `pfsockopen()`. +pub(in crate::interpreter) fn eval_builtin_fsockopen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=5).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let host = eval_expr(&args[0], context, scope, values)?; + let port = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_fsockopen_result(host, port, context, values) +} + +/// Opens a connected TCP stream from host and port cells. +pub(in crate::interpreter) fn eval_fsockopen_result( + host: RuntimeCellHandle, + port: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let host = eval_path_string(host, values)?; + let port = eval_int_value(port, values)?; + match context + .stream_resources_mut() + .open_tcp_stream_host_port(&host, port) + { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates `stream_socket_accept(socket, timeout = null, peer_name = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_accept( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let socket = eval_expr(&args[0], context, scope, values)?; + for arg in &args[1..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_accept_result(socket, context, values) +} + +/// Accepts one pending TCP connection from a listener resource. +pub(in crate::interpreter) fn eval_stream_socket_accept_result( + socket: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(socket, values)?; + match context.stream_resources_mut().accept_tcp(id) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates `stream_socket_get_name(socket, remote)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_get_name( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [socket, remote] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let socket = eval_expr(socket, context, scope, values)?; + let remote = eval_expr(remote, context, scope, values)?; + eval_stream_socket_get_name_result(socket, remote, context, values) +} + +/// Returns a tracked local or remote socket endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_get_name_result( + socket: RuntimeCellHandle, + remote: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(socket, values)?; + let remote = values.truthy(remote)?; + match context.stream_resources().socket_name(id, remote) { + Some(name) => values.string(&name), + None => values.bool_value(false), + } +} + +/// Evaluates `stream_socket_shutdown(stream, mode)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_shutdown( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, mode] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_stream_socket_shutdown_result(stream, mode, context, values) +} + +/// Applies a socket shutdown mode to a stream resource. +pub(in crate::interpreter) fn eval_stream_socket_shutdown_result( + stream: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(stream, values)?; + let mode = eval_int_value(mode, values)?; + values.bool_value( + context + .stream_resources() + .socket_shutdown(id, mode) + .unwrap_or(false), + ) +} + +/// Evaluates `stream_socket_enable_crypto(...)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_enable_crypto( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let enable = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_enable_crypto_result(stream, enable, context, values) +} + +/// Returns TLS enablement status for eval socket streams. +pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_result( + stream: RuntimeCellHandle, + enable: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(stream, values)?; + if !context.stream_resources().has_stream(id) { + return values.bool_value(false); + } + let disabled = !values.truthy(enable)?; + values.bool_value(disabled) +} + +/// Evaluates `stream_socket_sendto(stream, data, flags = 0, address = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_sendto( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let data = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_sendto_result(stream, data, context, values) +} + +/// Writes bytes to a connected socket stream. +pub(in crate::interpreter) fn eval_stream_socket_sendto_result( + stream: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_fwrite_result(stream, data, context, values) +} + +/// Evaluates `stream_socket_recvfrom(stream, length, flags = 0, address = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_recvfrom_result(stream, length, context, values) +} + +/// Reads bytes from a connected socket stream. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_fread_result(stream, length, context, values) +} + +/// Evaluates `stream_socket_pair(domain, type, protocol)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_pair( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [domain, socket_type, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let _ = eval_expr(domain, context, scope, values)?; + let _ = eval_expr(socket_type, context, scope, values)?; + let _ = eval_expr(protocol, context, scope, values)?; + eval_stream_socket_pair_result(context, values) +} + +/// Creates a pair of connected local stream resources. +pub(in crate::interpreter) fn eval_stream_socket_pair_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((left, right)) = context.stream_resources_mut().open_socket_pair() else { + return values.bool_value(false); + }; + let mut result = values.array_new(2)?; + let key = values.int(0)?; + let value = values.resource(left)?; + result = values.array_set(result, key, value)?; + let key = values.int(1)?; + let value = values.resource(right)?; + values.array_set(result, key, value) +} + +/// Evaluates `stream_select(...)` as a conservative non-blocking readiness check. +pub(in crate::interpreter) fn eval_builtin_stream_select( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(4..=5).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + let _ = eval_expr(arg, context, scope, values)?; + } + values.int(0) +} + +/// Evaluates materialized `stream_select(...)` arguments. +pub(in crate::interpreter) fn eval_stream_select_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if !(4..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + values.int(0) +} + +/// Converts a runtime resource cell into eval's zero-based socket id. +fn eval_socket_resource_id( + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(resource, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 0f9a8d9f76..caf9441717 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -193,6 +193,9 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), "fprintf" => Some(&["stream", "format", "values"]), + "fsockopen" | "pfsockopen" => { + Some(&["hostname", "port", "error_code", "error_message", "timeout"]) + } "flock" => Some(&["stream", "operation", "would_block"]), "fread" => Some(&["stream", "length"]), "fscanf" => Some(&["stream", "format", "vars"]), @@ -308,6 +311,17 @@ pub(in crate::interpreter) fn eval_builtin_param_names( Some(&["stream", "size"]) } "stream_set_timeout" => Some(&["stream", "seconds", "microseconds"]), + "stream_select" => Some(&["read", "write", "except", "seconds", "microseconds"]), + "stream_socket_server" | "stream_socket_client" => Some(&["address"]), + "stream_socket_accept" => Some(&["socket", "timeout", "peer_name"]), + "stream_socket_enable_crypto" => { + Some(&["stream", "enable", "crypto_method", "session_stream"]) + } + "stream_socket_get_name" => Some(&["socket", "remote"]), + "stream_socket_pair" => Some(&["domain", "type", "protocol"]), + "stream_socket_recvfrom" => Some(&["stream", "length", "flags", "address"]), + "stream_socket_sendto" => Some(&["stream", "data", "flags", "address"]), + "stream_socket_shutdown" => Some(&["stream", "mode"]), "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs index a07092a51f..bf59e697c5 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -181,6 +181,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_fread_result(*stream, *length, context, values)? } + "fsockopen" | "pfsockopen" => { + if !(2..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values)? + } "fscanf" => { if evaluated_args.len() < 2 { return Err(EvalStatus::RuntimeFatal); @@ -420,6 +426,71 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_stream_bucket_push_result(name, *brigade, *bucket, values)? } + "stream_select" => eval_stream_select_result(evaluated_args, values)?, + "stream_socket_server" => { + let [address] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_server_result(*address, context, values)? + } + "stream_socket_client" => { + let [address] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_client_result(*address, context, values)? + } + "stream_socket_accept" => { + if !(1..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_socket_accept_result(evaluated_args[0], context, values)? + } + "stream_socket_get_name" => { + let [socket, remote] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_get_name_result(*socket, *remote, context, values)? + } + "stream_socket_shutdown" => { + let [stream, mode] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_shutdown_result(*stream, *mode, context, values)? + } + "stream_socket_enable_crypto" => { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_socket_enable_crypto_result( + evaluated_args[0], + evaluated_args[1], + context, + values, + )? + } + "stream_socket_sendto" => { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_socket_sendto_result(evaluated_args[0], evaluated_args[1], context, values)? + } + "stream_socket_recvfrom" => { + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_socket_recvfrom_result( + evaluated_args[0], + evaluated_args[1], + context, + values, + )? + } + "stream_socket_pair" => { + let [_domain, _socket_type, _protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_pair_result(context, values)? + } "stream_get_contents" => match evaluated_args { [stream] => eval_stream_get_contents_result(*stream, None, None, context, values)?, [stream, length] => { diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index a7e8d53f3a..996ff7ba90 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -134,6 +134,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "fseek" | "fstat" | "fsync" + | "fsockopen" | "ftell" | "ftruncate" | "function_exists" @@ -247,6 +248,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "php_uname" | "phpversion" | "pclose" + | "pfsockopen" | "popen" | "preg_match" | "preg_match_all" @@ -331,6 +333,16 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "stream_wrapper_register" | "stream_wrapper_restore" | "stream_wrapper_unregister" + | "stream_select" + | "stream_socket_accept" + | "stream_socket_client" + | "stream_socket_enable_crypto" + | "stream_socket_get_name" + | "stream_socket_pair" + | "stream_socket_recvfrom" + | "stream_socket_sendto" + | "stream_socket_server" + | "stream_socket_shutdown" | "str_contains" | "str_ends_with" | "str_ireplace" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 365a777355..8982a4e50c 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -494,6 +494,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), "fprintf" => eval_builtin_fprintf(args, context, scope, values), "fread" => eval_builtin_fread(args, context, scope, values), + "fsockopen" | "pfsockopen" => eval_builtin_fsockopen(args, context, scope, values), "fscanf" => eval_builtin_fscanf(args, context, scope, values), "fseek" => eval_builtin_fseek(args, context, scope, values), "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), @@ -675,6 +676,26 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_bucket_append" | "stream_bucket_prepend" => { eval_builtin_stream_bucket_push(name, args, context, scope, values) } + "stream_select" => eval_builtin_stream_select(args, context, scope, values), + "stream_socket_server" => eval_builtin_stream_socket_server(args, context, scope, values), + "stream_socket_client" => eval_builtin_stream_socket_client(args, context, scope, values), + "stream_socket_accept" => eval_builtin_stream_socket_accept(args, context, scope, values), + "stream_socket_get_name" => { + eval_builtin_stream_socket_get_name(args, context, scope, values) + } + "stream_socket_shutdown" => { + eval_builtin_stream_socket_shutdown(args, context, scope, values) + } + "stream_socket_enable_crypto" => { + eval_builtin_stream_socket_enable_crypto(args, context, scope, values) + } + "stream_socket_sendto" => { + eval_builtin_stream_socket_sendto(args, context, scope, values) + } + "stream_socket_recvfrom" => { + eval_builtin_stream_socket_recvfrom(args, context, scope, values) + } + "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_stream_sockets.rs b/crates/elephc-eval/src/interpreter/tests/builtins_stream_sockets.rs new file mode 100644 index 0000000000..2ac57fe097 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_stream_sockets.rs @@ -0,0 +1,77 @@ +//! Purpose: +//! Interpreter tests for eval TCP stream socket builtins. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Tests bind localhost port 0 so the OS chooses available ports. +//! - Socket resources are then exercised through regular eval stream reads/writes. + +use super::super::*; +use super::support::*; + +/// Verifies stream socket helpers support local TCP and socket pairs. +#[test] +fn execute_program_dispatches_stream_socket_builtins() { + let program = parse_fragment( + br#"$server = stream_socket_server("tcp://127.0.0.1:0"); +echo is_resource($server) ? "server" : "bad"; echo ":"; +$addr = stream_socket_get_name($server, false); +echo $addr !== false ? "name" : "bad"; echo ":"; +$client = stream_socket_client("tcp://" . $addr); +echo is_resource($client) ? "client" : "bad"; echo ":"; +$peer = stream_socket_accept($server); +echo is_resource($peer) ? "accept" : "bad"; echo ":"; +echo stream_socket_get_name($client, true) !== false ? "peername" : "bad"; echo ":"; +echo stream_socket_sendto($client, "ping") === 4 ? "send" : "bad"; echo ":"; +echo stream_socket_recvfrom($peer, 4) === "ping" ? "recv" : "bad"; echo ":"; +fwrite($peer, "pong"); +echo fread($client, 4) === "pong" ? "roundtrip" : "bad"; echo ":"; +echo stream_socket_enable_crypto($client, false) ? "cryptooff" : "bad"; echo ":"; +echo stream_socket_shutdown($client, 2) ? "shutdown" : "bad"; echo ":"; +fclose($peer); fclose($client); fclose($server); +$server = stream_socket_server("tcp://127.0.0.1:0"); +$addr = stream_socket_get_name($server, false); +$parts = explode(":", $addr); +$fs = fsockopen("127.0.0.1", intval($parts[1])); +$peer = stream_socket_accept($server); +echo is_resource($fs) && is_resource($peer) ? "fsock" : "bad"; echo ":"; +fclose($fs); fclose($peer); fclose($server); +$server = stream_socket_server("tcp://127.0.0.1:0"); +$addr = call_user_func("stream_socket_get_name", $server, false); +$parts = explode(":", $addr); +$pfs = pfsockopen("127.0.0.1", intval($parts[1])); +$peer = stream_socket_accept($server); +echo is_resource($pfs) && is_resource($peer) ? "pfsock" : "bad"; echo ":"; +fclose($pfs); fclose($peer); fclose($server); +$pair = stream_socket_pair(1, 1, 0); +echo is_array($pair) && is_resource($pair[0]) && is_resource($pair[1]) ? "pair" : "bad"; echo ":"; +fwrite($pair[0], "xy"); +echo fread($pair[1], 2) === "xy" ? "pairio" : "bad"; echo ":"; +fclose($pair[0]); fclose($pair[1]); +$read = []; $write = []; $except = []; +echo stream_select($read, $write, $except, 0) === 0 ? "select" : "bad"; echo ":"; +echo function_exists("fsockopen"); echo function_exists("pfsockopen"); +echo function_exists("stream_select"); echo function_exists("stream_socket_accept"); +echo function_exists("stream_socket_client"); echo function_exists("stream_socket_enable_crypto"); +echo function_exists("stream_socket_get_name"); echo function_exists("stream_socket_pair"); +echo function_exists("stream_socket_recvfrom"); echo function_exists("stream_socket_sendto"); +echo function_exists("stream_socket_server"); echo function_exists("stream_socket_shutdown"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "server:name:client:accept:peername:send:recv:roundtrip:cryptooff:", + "shutdown:fsock:pfsock:pair:pairio:select:111111111111" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index d11bcc645b..1758e1283c 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -28,6 +28,7 @@ mod builtins_spl_autoload; mod builtins_stream_contexts; mod builtins_stream_extensions; mod builtins_stream_settings; +mod builtins_stream_sockets; mod builtins_strings_binary; mod builtins_strings_encoding; mod builtins_strings_text; diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-eval/src/stream_resources.rs index aacee8757d..2ac0cb5afd 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-eval/src/stream_resources.rs @@ -15,7 +15,10 @@ use std::collections::{HashMap, HashSet}; use std::ffi::c_void; use std::fs::{File, Metadata, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; +use std::net::{Shutdown, TcpListener, TcpStream}; use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; +#[cfg(unix)] +use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; @@ -31,6 +34,8 @@ pub(crate) struct EvalStreamResources { filter_resources: HashSet, hash_contexts: HashMap, process_children: HashMap, + socket_listeners: HashMap, + socket_names: HashMap, stream_contexts: HashMap, streams: HashMap, } @@ -106,6 +111,91 @@ impl EvalStreamResources { Some(id) } + /// Opens a TCP listener resource for `stream_socket_server()`. + pub(crate) fn open_tcp_listener(&mut self, address: &str) -> Option { + let listener = TcpListener::bind(eval_tcp_address(address)).ok()?; + let local = listener.local_addr().ok()?.to_string(); + let id = self.next_id; + self.next_id += 1; + self.socket_names.insert( + id, + EvalSocketNames { + local, + peer: None, + }, + ); + self.socket_listeners.insert(id, listener); + Some(id) + } + + /// Opens a connected TCP stream resource. + pub(crate) fn open_tcp_stream(&mut self, address: &str) -> Option { + let stream = TcpStream::connect(eval_tcp_address(address)).ok()?; + self.insert_tcp_stream(stream) + } + + /// Opens a connected TCP stream from separate host and port arguments. + pub(crate) fn open_tcp_stream_host_port(&mut self, host: &str, port: i64) -> Option { + let host = host + .strip_prefix("tcp://") + .or_else(|| host.strip_prefix("ssl://")) + .or_else(|| host.strip_prefix("tls://")) + .unwrap_or(host); + self.open_tcp_stream(&format!("{host}:{port}")) + } + + /// Accepts one TCP connection from a listener resource. + pub(crate) fn accept_tcp(&mut self, id: i64) -> Option { + let listener = self.socket_listeners.get(&id)?; + let (stream, _) = listener.accept().ok()?; + self.insert_tcp_stream(stream) + } + + /// Opens a pair of connected local stream resources. + pub(crate) fn open_socket_pair(&mut self) -> Option<(i64, i64)> { + #[cfg(unix)] + { + let (left, right) = UnixStream::pair().ok()?; + let left = unsafe { + // The UnixStream endpoint is moved into the File-backed eval stream. + File::from_raw_fd(left.into_raw_fd()) + }; + let right = unsafe { + // The UnixStream endpoint is moved into the File-backed eval stream. + File::from_raw_fd(right.into_raw_fd()) + }; + let left_id = self.insert(EvalFileStream::new( + left, + "socketpair".to_string(), + "r+".to_string(), + )); + let right_id = self.insert(EvalFileStream::new( + right, + "socketpair".to_string(), + "r+".to_string(), + )); + self.socket_names.insert( + left_id, + EvalSocketNames { + local: "socketpair".to_string(), + peer: Some("socketpair".to_string()), + }, + ); + self.socket_names.insert( + right_id, + EvalSocketNames { + local: "socketpair".to_string(), + peer: Some("socketpair".to_string()), + }, + ); + Some((left_id, right_id)) + } + #[cfg(not(unix))] + { + None + } + } + /// Opens a local directory and returns its resource id. pub(crate) fn open_directory(&mut self, path: &str) -> Option { let directory = EvalDirectoryStream::open(path)?; @@ -142,7 +232,10 @@ impl EvalStreamResources { /// Removes a stream resource from the table, closing its file handle. pub(crate) fn close(&mut self, id: i64) -> bool { - let closed = self.streams.remove(&id).is_some() || self.filter_resources.remove(&id); + let closed = self.streams.remove(&id).is_some() + || self.filter_resources.remove(&id) + || self.socket_listeners.remove(&id).is_some(); + self.socket_names.remove(&id); if let Some(mut child) = self.process_children.remove(&id) { let _ = child.wait(); } @@ -154,6 +247,32 @@ impl EvalStreamResources { self.streams.contains_key(&id) } + /// Returns a local or remote socket name for a socket resource. + pub(crate) fn socket_name(&self, id: i64, remote: bool) -> Option { + let names = self.socket_names.get(&id)?; + if remote { + names.peer.clone() + } else { + Some(names.local.clone()) + } + } + + /// Applies a TCP/Unix stream shutdown operation. + pub(crate) fn socket_shutdown(&self, id: i64, mode: i64) -> Option { + let stream = self.streams.get(&id)?; + let shutdown = match mode { + 0 => Shutdown::Read, + 1 => Shutdown::Write, + 2 => Shutdown::Both, + _ => return Some(false), + }; + let result = unsafe { + // libc shutdown only observes the borrowed descriptor and mode. + libc::shutdown(stream.file.as_raw_fd(), eval_shutdown_how(shutdown)) + }; + Some(result == 0) + } + /// Allocates an eval-local stream filter resource handle. pub(crate) fn open_filter_resource(&mut self) -> i64 { let id = self.next_id; @@ -483,6 +602,20 @@ impl EvalStreamResources { id } + /// Inserts a TCP stream as a File-backed eval stream and records endpoint names. + fn insert_tcp_stream(&mut self, stream: TcpStream) -> Option { + let local = stream.local_addr().ok()?.to_string(); + let peer = stream.peer_addr().ok().map(|addr| addr.to_string()); + let file = unsafe { + // The TcpStream is moved into the File-backed eval stream. + File::from_raw_fd(stream.into_raw_fd()) + }; + let id = self.insert(EvalFileStream::new(file, local.clone(), "r+".to_string())); + self.socket_names + .insert(id, EvalSocketNames { local, peer }); + Some(id) + } + /// Inserts a directory stream and returns the assigned zero-based resource payload. fn insert_directory(&mut self, directory: EvalDirectoryStream) -> i64 { let id = self.next_id; @@ -528,6 +661,30 @@ pub(crate) struct EvalStreamMetaData { pub(crate) uri: String, } +/// Local and peer names tracked for socket-backed eval streams. +struct EvalSocketNames { + local: String, + peer: Option, +} + +/// Normalizes supported TCP-style stream socket addresses. +fn eval_tcp_address(address: &str) -> &str { + address + .strip_prefix("tcp://") + .or_else(|| address.strip_prefix("ssl://")) + .or_else(|| address.strip_prefix("tls://")) + .unwrap_or(address) +} + +/// Converts Rust's socket shutdown enum into libc constants. +fn eval_shutdown_how(shutdown: Shutdown) -> libc::c_int { + match shutdown { + Shutdown::Read => libc::SHUT_RD, + Shutdown::Write => libc::SHUT_WR, + Shutdown::Both => libc::SHUT_RDWR, + } +} + /// Converts PHP `LOCK_*` bit flags into host `flock()` flags. fn eval_flock_operation(operation: i64) -> Option { let non_blocking = operation & 4 != 0; From 55359ce1c33e1e3fb7dd4420d7d70b8c790c082d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 15:22:30 +0200 Subject: [PATCH 0322/1208] Align eval language construct builtins --- .../src/interpreter/builtins/mod.rs | 2 + .../interpreter/builtins/process_control.rs | 56 +++++++++++++ .../interpreter/builtins/registry/binding.rs | 10 ++- .../builtins/registry/dispatch/core.rs | 1 + .../builtins/registry/dispatch/symbols.rs | 3 + .../interpreter/builtins/registry/names.rs | 5 ++ .../src/interpreter/builtins/symbols.rs | 61 +++++++++++++- .../src/interpreter/expressions.rs | 6 +- .../tests/builtins_language_constructs.rs | 80 +++++++++++++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + tests/codegen/eval.rs | 4 +- 11 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/process_control.rs create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_language_constructs.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/mod.rs b/crates/elephc-eval/src/interpreter/builtins/mod.rs index 348f5be6da..50fe1d8eaf 100644 --- a/crates/elephc-eval/src/interpreter/builtins/mod.rs +++ b/crates/elephc-eval/src/interpreter/builtins/mod.rs @@ -16,6 +16,7 @@ mod class_metadata; mod filesystem; mod formatting; mod network_env; +mod process_control; mod regex; mod registry; mod scalars; @@ -29,6 +30,7 @@ pub(super) use class_metadata::*; pub(super) use filesystem::*; pub(super) use formatting::*; pub(super) use network_env::*; +pub(super) use process_control::*; pub(super) use regex::*; pub(super) use registry::*; pub(super) use scalars::*; diff --git a/crates/elephc-eval/src/interpreter/builtins/process_control.rs b/crates/elephc-eval/src/interpreter/builtins/process_control.rs new file mode 100644 index 0000000000..b02c22d2c3 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/process_control.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Implements eval-side process termination language constructs. +//! +//! Called from: +//! - `crate::interpreter::expressions` direct builtin dispatch. +//! - `crate::interpreter::builtins::registry::dispatch` for dynamic callable dispatch. +//! +//! Key details: +//! - `exit` and `die` match elephc's compiled behavior by terminating the host process. +//! - Tests must avoid executing these helpers directly because they do not return. + +use super::super::*; +use super::*; + +/// Evaluates direct `exit` or `die` calls from unevaluated EvalIR arguments. +pub(in crate::interpreter) fn eval_builtin_exit( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let status = match args { + [] => 0, + [status] => { + let status = eval_expr(status, context, scope, values)?; + eval_int_value(status, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_process_exit(status) +} + +/// Evaluates dynamic `exit` or `die` calls from already materialized arguments. +pub(in crate::interpreter) fn eval_exit_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let status = match evaluated_args { + [] => 0, + [status] => eval_int_value(*status, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_process_exit(status) +} + +/// Terminates the current process with the PHP integer status clamped to `i32`. +fn eval_process_exit(status: i64) -> Result { + let status = i32::try_from(status).unwrap_or_else(|_| { + if status.is_negative() { + i32::MIN + } else { + i32::MAX + } + }); + std::process::exit(status) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index caf9441717..27bdd8f20a 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -132,10 +132,10 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "grapheme_strrev" | "hex2bin" | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => Some(&["string"]), - "boolval" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" - | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" - | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" | "is_real" - | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), + "boolval" | "empty" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" + | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" + | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" + | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), "settype" => Some(&["var", "type"]), "get_class" => Some(&["object"]), "get_parent_class" => Some(&["object_or_class"]), @@ -166,6 +166,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "date" => Some(&["format", "timestamp"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), + "die" | "exit" => Some(&["status"]), "dirname" => Some(&["path", "levels"]), "disk_free_space" | "disk_total_space" => Some(&["directory"]), "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), @@ -236,6 +237,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "iterator_count" => Some(&["iterator"]), "iterator_to_array" => Some(&["iterator", "preserve_keys"]), "ip2long" => Some(&["ip"]), + "isset" | "unset" => Some(&["var", "vars"]), "json_decode" => Some(&["json", "associative", "depth", "flags"]), "json_encode" => Some(&["value", "flags", "depth"]), "json_last_error" | "json_last_error_msg" => Some(&[]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs index 902474e157..eaa7308418 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs @@ -45,6 +45,7 @@ pub(in crate::interpreter) fn eval_core_builtin_with_values( } "define" => eval_define_result(evaluated_args, context, values)?, "defined" => eval_defined_result(evaluated_args, context, values)?, + "die" | "exit" => return eval_exit_result(evaluated_args, values).map(Some), _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index 9899d90b16..18a230df67 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -52,6 +52,9 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( let name = name.trim_start_matches('\\').to_ascii_lowercase(); values.bool_value(eval_function_probe_exists(context, &name))? } + "empty" => eval_empty_result(evaluated_args, values)?, + "isset" => eval_isset_result(evaluated_args, values)?, + "unset" => eval_unset_result(evaluated_args, values)?, "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, "class_alias" => eval_class_alias_result(evaluated_args, context, values)?, "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index 996ff7ba90..a76d6235d9 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -93,11 +93,14 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "define" | "defined" | "deg2rad" + | "die" | "dirname" | "disk_free_space" | "disk_total_space" + | "empty" | "exec" | "exp" + | "exit" | "explode" | "fclose" | "fdatasync" @@ -205,6 +208,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "is_int" | "is_integer" | "is_iterable" + | "isset" | "is_long" | "is_nan" | "is_null" @@ -381,6 +385,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "uasort" | "uksort" | "unlink" + | "unset" | "umask" | "urldecode" | "urlencode" diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index 6c5bc8994c..ec7b89c341 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -452,7 +452,7 @@ pub(in crate::interpreter) fn eval_builtin_isset( values: &mut impl RuntimeValueOps, ) -> Result { if args.is_empty() { - return values.bool_value(false); + return Err(EvalStatus::RuntimeFatal); } for arg in args { if !eval_isset_arg(arg, context, scope, values)? { @@ -476,6 +476,65 @@ pub(in crate::interpreter) fn eval_builtin_empty( values.bool_value(empty) } +/// Evaluates direct `unset(...)` calls over eval-visible variable names. +pub(in crate::interpreter) fn eval_builtin_unset( + args: &[EvalExpr], + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + let EvalExpr::LoadVar(name) = arg else { + return Err(EvalStatus::RuntimeFatal); + }; + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { + values.release(replaced)?; + } + } + values.null() +} + +/// Evaluates callable `isset(...)` over already materialized values. +pub(in crate::interpreter) fn eval_isset_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for value in evaluated_args { + if values.is_null(*value)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates callable `empty(...)` over one already materialized value. +pub(in crate::interpreter) fn eval_empty_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = !values.truthy(*value)?; + values.bool_value(empty) +} + +/// Evaluates callable `unset(...)` after values have already been materialized. +pub(in crate::interpreter) fn eval_unset_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.null() +} + /// Evaluates one `empty` operand without warning or failing on missing variables. pub(in crate::interpreter) fn eval_empty_arg( arg: &EvalExpr, diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 8982a4e50c..69fd76208e 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -7,7 +7,7 @@ //! //! Key details: //! - PHP call argument evaluation order is preserved before binding or ABI-like materialization. -//! - Language constructs such as `eval`, `isset`, and `empty` receive unevaluated expressions. +//! - Language constructs such as `eval`, `isset`, `empty`, and `unset` receive unevaluated expressions. use super::*; @@ -352,7 +352,7 @@ pub(in crate::interpreter) fn eval_dynamic_call( /// Returns true for language constructs that need unevaluated argument expressions. pub(in crate::interpreter) fn eval_expr_language_construct_name(name: &str) -> bool { - matches!(name, "empty" | "eval" | "isset") + matches!(name, "empty" | "eval" | "isset" | "unset") } /// Returns true when every source argument is plain positional. @@ -458,6 +458,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "dirname" => eval_builtin_dirname(args, context, scope, values), + "die" | "exit" => eval_builtin_exit(args, context, scope, values), "disk_free_space" | "disk_total_space" => { eval_builtin_disk_space(name, args, context, scope, values) } @@ -728,6 +729,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "long2ip" => eval_builtin_long2ip(args, context, scope, values), "trim" => eval_builtin_trim_like(name, args, context, scope, values), "ucwords" => eval_builtin_ucwords(args, context, scope, values), + "unset" => eval_builtin_unset(args, scope, values), "umask" => eval_builtin_umask(args, context, scope, values), "usleep" => eval_builtin_usleep(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_language_constructs.rs b/crates/elephc-eval/src/interpreter/tests/builtins_language_constructs.rs new file mode 100644 index 0000000000..80cd9e583f --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_language_constructs.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Interpreter tests for PHP language-construct builtins that are also visible +//! through eval's builtin registry. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - `exit` and `die` are probed for visibility only because executing them +//! terminates the current process. + +use super::super::*; +use super::support::*; + +/// Verifies eval language-construct builtins are visible and direct semantics still work. +#[test] +fn execute_program_language_construct_builtins_are_visible() { + let program = parse_fragment( + br#"$x = 1; +$y = 2; +unset($x); +unset($y, $missing); +echo isset($x) ? "bad" : "unset"; echo ":"; +echo isset($y) ? "bad" : "multi"; echo ":"; +echo empty($missing) ? "empty" : "bad"; echo ":"; +echo empty(0) ? "zero" : "bad"; echo ":"; +echo function_exists("isset") ? "I" : "i"; +echo function_exists("empty") ? "E" : "e"; +echo function_exists("unset") ? "U" : "u"; +echo function_exists("exit") ? "X" : "x"; +echo function_exists("die") ? "D" : "d"; +echo is_callable("isset") ? "i" : "!"; +echo is_callable("empty") ? "e" : "!"; +echo is_callable("unset") ? "u" : "!"; +echo is_callable("exit") ? "x" : "!"; +return is_callable("die");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "unset:multi:empty:zero:IEUXDieux"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies callable dispatch for safe language-construct builtins uses materialized values. +#[test] +fn execute_program_language_construct_builtins_dispatch_as_callables() { + let program = parse_fragment( + br#"echo call_user_func("isset", 1, null) ? "bad" : "null"; echo ":"; +echo call_user_func("isset", 1, "x") ? "isset" : "bad"; echo ":"; +echo call_user_func("empty", 0) ? "empty" : "bad"; echo ":"; +echo call_user_func_array("empty", ["value" => "x"]) ? "bad" : "filled"; echo ":"; +$result = call_user_func("unset", 1); +echo is_null($result) ? "unset-null" : "bad"; echo ":"; +echo call_user_func_array("isset", ["var" => 1, "vars" => "x"]) ? "named" : "bad";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "null:isset:empty:filled:unset-null:named"); + assert_eq!(values.get(result), FakeValue::Null); +} + +/// Verifies `isset()` without operands is rejected like the main compiler. +#[test] +fn execute_program_isset_without_arguments_fails() { + let program = parse_fragment(br#"isset();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("isset arity fails"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 1758e1283c..251a79ceb4 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -20,6 +20,7 @@ mod builtins_file_streams; mod builtins_filesystem_metadata; mod builtins_filesystem_ops; mod builtins_json; +mod builtins_language_constructs; mod builtins_readline; mod builtins_math_formatting; mod builtins_process_pipes; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a63bef67d7..5abe7fa2d7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3680,7 +3680,7 @@ if (isset($zero, $nullish)) { echo "1"; } else { echo "0"; } echo function_exists("isset") . "x";'); "#, ); - assert_eq!(out, "001110x"); + assert_eq!(out, "0011101x"); } /// Verifies eval `empty()` uses PHP truthiness without warning on missing variables. @@ -3702,7 +3702,7 @@ if (empty($value)) { echo "1"; } else { echo "0"; } echo function_exists("empty") . "x";'); "#, ); - assert_eq!(out, "111110x"); + assert_eq!(out, "1111101x"); } /// Verifies eval `isset()` and `empty()` use PHP offset semantics for array reads. From 4fd69efdfc514a8b2413ce69c572a5cbccf12de0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 16:00:53 +0200 Subject: [PATCH 0323/1208] Support eval class inheritance metadata --- crates/elephc-eval/src/context.rs | 112 +++++++++++++++++- crates/elephc-eval/src/eval_ir.rs | 27 ++++- .../interpreter/builtins/class_metadata.rs | 40 +++++-- .../builtins/registry/dispatch/symbols.rs | 6 +- .../src/interpreter/builtins/scalars/types.rs | 21 +++- .../src/interpreter/builtins/symbols.rs | 39 +++--- .../elephc-eval/src/interpreter/statements.rs | 44 ++++--- .../tests/builtins_class_metadata.rs | 34 +++++- .../src/interpreter/tests/expressions.rs | 37 ++++++ crates/elephc-eval/src/parser/statements.rs | 35 +++++- .../src/parser/tests/classes_errors.rs | 18 +++ .../src/parser/tests/namespaces.rs | 24 ++++ src/codegen/mod.rs | 1 + src/codegen_support/runtime/data/user.rs | 10 ++ src/codegen_support/runtime/eval_bridge.rs | 43 +++---- tests/codegen/eval.rs | 63 +++++++--- 16 files changed, 462 insertions(+), 92 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index b2b9ccede4..c10f56eaba 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -15,7 +15,7 @@ use std::collections::{HashMap, HashSet}; use std::ffi::c_void; use crate::abi::ABI_VERSION; -use crate::eval_ir::{EvalClass, EvalFunction}; +use crate::eval_ir::{EvalClass, EvalClassMethod, EvalFunction}; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; use crate::value::{RuntimeCell, RuntimeCellHandle}; @@ -252,6 +252,108 @@ impl ElephcEvalContext { .and_then(|class_key| self.classes.get(class_key)) } + /// Returns eval-declared class metadata from parent to child for construction. + pub fn class_chain(&self, name: &str) -> Vec { + let mut chain = Vec::new(); + let mut seen = HashSet::new(); + self.collect_class_chain(name, &mut chain, &mut seen); + chain + } + + /// Collects one eval-declared class ancestry chain without following cycles. + fn collect_class_chain( + &self, + name: &str, + chain: &mut Vec, + seen: &mut HashSet, + ) { + let key = normalize_class_name(name); + if !seen.insert(key.clone()) { + return; + } + let Some(class) = self.classes.get(&key) else { + return; + }; + if let Some(parent) = class.parent() { + self.collect_class_chain(parent, chain, seen); + } + chain.push(class.clone()); + } + + /// Finds a method in an eval-declared class or its eval-declared parents. + pub fn class_method( + &self, + class_name: &str, + method_name: &str, + ) -> Option<(String, EvalClassMethod)> { + let mut current_name = self.resolve_class_name(class_name)?; + let mut seen = HashSet::new(); + loop { + let key = normalize_class_name(¤t_name); + if !seen.insert(key.clone()) { + return None; + } + let class = self.classes.get(&key)?; + if let Some(method) = class.method(method_name) { + return Some((class.name().to_string(), method.clone())); + } + current_name = class.parent()?.to_string(); + } + } + + /// Returns direct and inherited parent class names for an eval-declared class. + pub fn class_parent_names(&self, class_name: &str) -> Vec { + let mut parents = Vec::new(); + let mut current = self.class(class_name).and_then(EvalClass::parent); + let mut seen = HashSet::new(); + while let Some(parent) = current { + let Some(parent_class) = self.class(parent) else { + break; + }; + let key = normalize_class_name(parent_class.name()); + if !seen.insert(key) { + break; + } + parents.push(parent_class.name().trim_start_matches('\\').to_string()); + current = parent_class.parent(); + } + parents + } + + /// Returns direct and inherited interface names for an eval-declared class. + pub fn class_interface_names(&self, class_name: &str) -> Vec { + let mut interfaces = Vec::new(); + let mut seen = HashSet::new(); + for class in self.class_chain(class_name) { + for interface in class.interfaces() { + push_unique_class_name(interface, &mut interfaces, &mut seen); + } + } + interfaces + } + + /// Returns whether an eval-declared class satisfies one class/interface target. + pub fn class_is_a(&self, class_name: &str, target: &str, exclude_self: bool) -> bool { + let Some(class) = self.class(class_name) else { + return false; + }; + let target = normalize_class_name( + &self + .resolve_class_name(target) + .unwrap_or_else(|| target.trim_start_matches('\\').to_string()), + ); + if !exclude_self && normalize_class_name(class.name()) == target { + return true; + } + self.class_parent_names(class.name()) + .iter() + .any(|parent| normalize_class_name(parent) == target) + || self + .class_interface_names(class.name()) + .iter() + .any(|interface| normalize_class_name(interface) == target) + } + /// Defines an eval dynamic constant value, failing if the name is invalid or already present. pub fn define_constant(&mut self, name: &str, value: RuntimeCellHandle) -> bool { let key = normalize_constant_name(name); @@ -493,6 +595,14 @@ fn normalize_class_name(name: &str) -> String { name.trim_start_matches('\\').to_ascii_lowercase() } +/// Pushes a PHP class-like name once, preserving the first visible spelling. +fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + let key = normalize_class_name(name); + if seen.insert(key) { + names.push(name.trim_start_matches('\\').to_string()); + } +} + /// Normalizes PHP constant names for case-sensitive eval dynamic probes. fn normalize_constant_name(name: &str) -> String { name.trim_start_matches('\\').to_string() diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index c8ac54e071..b987b7ab6a 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -172,19 +172,34 @@ impl EvalFunction { #[derive(Debug, Clone, PartialEq)] pub struct EvalClass { name: String, + parent: Option, + interfaces: Vec, properties: Vec, methods: Vec, } impl EvalClass { - /// Creates a dynamic eval class with public properties and methods. + /// Creates a dynamic eval class with public properties and methods, and no relations. pub fn new( name: impl Into, properties: Vec, methods: Vec, + ) -> Self { + Self::with_relations(name, None, Vec::new(), properties, methods) + } + + /// Creates a dynamic eval class with optional parent and implemented interfaces. + pub fn with_relations( + name: impl Into, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), + parent, + interfaces, properties, methods, } @@ -195,6 +210,16 @@ impl EvalClass { &self.name } + /// Returns the parent class name declared by this eval class, when present. + pub fn parent(&self) -> Option<&str> { + self.parent.as_deref() + } + + /// Returns interface names implemented directly by this eval class. + pub fn interfaces(&self) -> &[String] { + &self.interfaces + } + /// Returns public properties declared directly by this eval class. pub fn properties(&self) -> &[EvalClassProperty] { &self.properties diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 8b8bf29968..992ab567b7 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -6,8 +6,8 @@ //! - Dynamic callable dispatch under `builtins::registry::dispatch`. //! //! Key details: -//! - Eval-declared classes currently have no parent, interface, trait, or -//! attribute metadata, so known class-like targets return empty arrays. +//! - Eval-declared classes carry parent and interface metadata; trait and +//! attribute metadata remains empty. //! - Missing class-like relation targets return `false`, matching the main //! backend's unknown-target fallback. @@ -57,8 +57,20 @@ pub(in crate::interpreter) fn eval_class_relation_target_result( if !matches!(name, "class_implements" | "class_parents" | "class_uses") { return Err(EvalStatus::RuntimeFatal); } - if !eval_class_relation_target_exists(target, context, values)? { + let Some(target) = eval_class_relation_target_name(target, context, values)? else { return values.bool_value(false); + }; + if context.class(&target).is_some() { + return match name { + "class_implements" => { + eval_class_relation_names_result(context.class_interface_names(&target), values) + } + "class_parents" => { + eval_class_relation_names_result(context.class_parent_names(&target), values) + } + "class_uses" => values.assoc_new(0), + _ => Err(EvalStatus::RuntimeFatal), + }; } values.assoc_new(0) } @@ -105,18 +117,18 @@ pub(in crate::interpreter) fn eval_class_attribute_metadata_result( } /// Returns whether a class-relation target refers to a known class-like symbol. -fn eval_class_relation_target_exists( +fn eval_class_relation_target_name( target: RuntimeCellHandle, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result { +) -> Result, EvalStatus> { if values.type_tag(target)? == EVAL_TAG_OBJECT { let name = eval_get_class_result(target, context, values)?; let name = eval_class_metadata_name(name, values)?; - return eval_class_relation_name_exists(&name, context, values); + return Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)); } let name = eval_class_metadata_name(target, values)?; - eval_class_relation_name_exists(&name, context, values) + Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)) } /// Returns whether one normalized class-like name exists in eval or runtime metadata. @@ -135,6 +147,20 @@ fn eval_class_relation_name_exists( values.enum_exists(name) } +/// Builds a PHP associative class-name array keyed by class-name strings. +fn eval_class_relation_names_result( + names: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(names.len())?; + for name in names { + let key = values.string(&name)?; + let value = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Reads and normalizes one class metadata string argument. fn eval_class_metadata_name( name: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index 18a230df67..8c358b251d 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -31,9 +31,7 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( "spl_autoload" | "spl_autoload_call" => { eval_spl_autoload_void_result(name, evaluated_args, values)? } - "spl_autoload_functions" => { - eval_spl_autoload_functions_result(evaluated_args, values)? - } + "spl_autoload_functions" => eval_spl_autoload_functions_result(evaluated_args, values)?, "spl_autoload_extensions" => { eval_spl_autoload_extensions_result(evaluated_args, context, values)? } @@ -86,7 +84,7 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( let [object_or_class] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_get_parent_class_result(*object_or_class, values)? + eval_get_parent_class_result(*object_or_class, context, values)? } "get_resource_id" | "get_resource_type" => { let [resource] = evaluated_args else { diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs index 596b32a459..141380c7b3 100644 --- a/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs @@ -134,14 +134,33 @@ pub(in crate::interpreter) fn eval_builtin_get_parent_class( return Err(EvalStatus::RuntimeFatal); }; let object_or_class = eval_expr(object_or_class, context, scope, values)?; - eval_get_parent_class_result(object_or_class, values) + eval_get_parent_class_result(object_or_class, context, values) } /// Resolves the PHP-visible parent class name for one object or class-name cell. pub(in crate::interpreter) fn eval_get_parent_class_result( object_or_class: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if let Ok(identity) = values.object_identity(object_or_class) { + if let Some(class) = context.dynamic_object_class(identity) { + if let Some(parent) = context.class_parent_names(class.name()).into_iter().next() { + return values.string(&parent); + } + return values.string(""); + } + } + if values.type_tag(object_or_class)? == EVAL_TAG_STRING { + let name = values.string_bytes(object_or_class)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + if context.class(&name).is_some() { + if let Some(parent) = context.class_parent_names(&name).into_iter().next() { + return values.string(&parent); + } + return values.string(""); + } + } values.parent_class_name(object_or_class) } diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index ec7b89c341..bad7607749 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -414,34 +414,39 @@ pub(in crate::interpreter) fn eval_is_a_relation_result( .resolve_class_name(target_class) .unwrap_or_else(|| target_class.to_string()); let is_object = values.type_tag(object_or_class)? == 6; - let result = - if is_object - && dynamic_object_is_a(object_or_class, &resolved_target_class, context, values)? - { - !matches!(name, "is_subclass_of") - } else if is_object || allow_string { - values.object_is_a( - object_or_class, - &resolved_target_class, - name == "is_subclass_of", - )? - } else { - false - }; + let exclude_self = name == "is_subclass_of"; + let result = if is_object { + dynamic_object_is_a( + object_or_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + .map_or_else( + || values.object_is_a(object_or_class, &resolved_target_class, exclude_self), + Ok, + )? + } else if allow_string { + values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? + } else { + false + }; values.bool_value(result) } -/// Returns whether an eval-created object matches a dynamic class name exactly. +/// Returns whether an eval-created object matches a dynamic class/interface target. pub(in crate::interpreter) fn dynamic_object_is_a( object: RuntimeCellHandle, target_class: &str, + exclude_self: bool, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result { +) -> Result, EvalStatus> { let identity = values.object_identity(object)?; Ok(context .dynamic_object_class(identity) - .is_some_and(|class| class.name().eq_ignore_ascii_case(target_class))) + .map(|class| context.class_is_a(class.name(), target_class, exclude_self))) } /// Evaluates PHP's `isset(...)` language construct over eval-visible values. diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 96bb9f3255..851a718fe6 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -340,6 +340,16 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( if context.has_class(name) || values.class_exists(name)? { return Err(EvalStatus::RuntimeFatal); } + if let Some(parent) = class.parent() { + if context.class(parent).is_none() || context.class_is_a(parent, name, false) { + return Err(EvalStatus::RuntimeFatal); + } + } + for interface in class.interfaces() { + if !values.interface_exists(interface)? { + return Err(EvalStatus::RuntimeFatal); + } + } if context.define_class(class.clone()) { Ok(()) } else { @@ -358,18 +368,26 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( let object = values.new_object("stdClass")?; let identity = values.object_identity(object)?; context.register_dynamic_object(identity, class.name()); - for property in class.properties() { - let value = if let Some(default) = property.default() { - eval_expr(default, context, caller_scope, values)? - } else { - values.null()? - }; - values.property_set(object, property.name(), value)?; + let mut class_chain = context.class_chain(class.name()); + if class_chain.is_empty() { + class_chain.push(class.clone()); + } + for class in &class_chain { + for property in class.properties() { + let value = if let Some(default) = property.default() { + eval_expr(default, context, caller_scope, values)? + } else { + values.null()? + }; + values.property_set(object, property.name(), value)?; + } } - if let Some(constructor) = class.method("__construct") { + if let Some((constructor_class, constructor)) = + context.class_method(class.name(), "__construct") + { eval_dynamic_method_with_values( - class.name(), - constructor, + &constructor_class, + &constructor, object, evaluated_args, context, @@ -395,10 +413,8 @@ pub(in crate::interpreter) fn eval_method_call_result( let Some(class) = context.dynamic_object_class(identity) else { return values.method_call(object, method_name, evaluated_args); }; - let class_name = class.name().to_string(); - let method = class - .method(method_name) - .cloned() + let (class_name, method) = context + .class_method(class.name(), method_name) .ok_or(EvalStatus::RuntimeFatal)?; eval_dynamic_method_with_values( &class_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index d099ad442d..7be3767b9f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -5,7 +5,8 @@ //! - `cargo test -p elephc-eval` through Rust's test harness. //! //! Key details: -//! - Eval class declarations currently carry no parent/interface/trait/attribute metadata. +//! - Eval class declarations expose parent/interface metadata while trait and +//! attribute metadata remains empty. //! - Tests verify direct calls, dynamic calls, named arguments, and builtin probes. use super::super::*; @@ -41,6 +42,37 @@ return true;"#, assert_eq!(values.output, "impl:parents:uses:missing:call:named:111"); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval-declared parent and interface metadata is exposed to relation builtins. +#[test] +fn execute_program_reports_eval_class_relation_metadata() { + let program = parse_fragment( + br#"class EvalMetaBase {} +class EvalMetaChild extends EvalMetaBase implements KnownInterface {} +$object = new EvalMetaChild(); +$implements = class_implements($object); +echo count($implements); echo ":"; +echo $implements["KnownInterface"]; echo ":"; +$parents = class_parents("EvalMetaChild"); +echo count($parents); echo ":"; +echo $parents["EvalMetaBase"]; echo ":"; +$call = call_user_func("class_implements", "EvalMetaChild"); +echo $call["KnownInterface"]; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => $object]); +echo $named["EvalMetaBase"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:KnownInterface:1:EvalMetaBase:KnownInterface:EvalMetaBase" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies class attribute helpers expose empty metadata arrays in eval. #[test] diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 497ed51bd6..b604c7b6fc 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -338,3 +338,40 @@ return $box->x;"#, assert_eq!(values.output, "DynBox:7:Y8:10:"); assert_eq!(values.get(result), FakeValue::Int(10)); } +/// Verifies eval-declared classes inherit properties, methods, and constructors. +#[test] +fn execute_program_constructs_eval_declared_class_with_inheritance() { + let program = parse_fragment( + br#"class EvalBaseBox { + public int $base = 1; + public function __construct($base) { $this->base = $base; } + public function sum($n) { return $this->base + $this->tail + $n; } +} +class EvalChildBox extends EvalBaseBox implements KnownInterface { + public int $tail = 4; + public function read($n) { return $this->sum($n); } +} +$box = new EvalChildBox(3); +echo $box->read(5); echo ":"; +echo get_parent_class($box); echo ":"; +echo is_a($box, "EvalBaseBox") ? "isa" : "bad"; echo ":"; +echo is_a($box, "KnownInterface") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalChildBox") ? "bad" : "self"; echo ":"; +echo is_subclass_of($box, "EvalBaseBox") ? "sub" : "bad"; echo ":"; +$parents = class_parents($box); +echo count($parents); echo ":"; +echo $parents["EvalBaseBox"]; +return $box->base;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "12:EvalBaseBox:isa:iface:self:sub:1:EvalBaseBox" + ); + assert_eq!(values.get(result), FakeValue::Int(3)); +} diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 8f9ca22e0c..3634a4d57a 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -190,7 +190,7 @@ impl Parser { }]) } - /// Parses `class Name { ... }` declarations for dynamic class metadata. + /// Parses `class Name [extends Parent] [implements Iface, ...] { ... }`. pub(super) fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { self.advance(); let TokenKind::Ident(name) = self.current() else { @@ -198,6 +198,8 @@ impl Parser { }; let name = self.qualify_name_in_current_namespace(name); self.advance(); + let parent = self.parse_class_parent_clause()?; + let interfaces = self.parse_class_interface_clause()?; self.expect(TokenKind::LBrace)?; let mut properties = Vec::new(); let mut methods = Vec::new(); @@ -208,11 +210,38 @@ impl Parser { self.parse_class_member(&mut properties, &mut methods)?; } self.consume_semicolon(); - Ok(vec![EvalStmt::ClassDecl(EvalClass::new( - name, properties, methods, + Ok(vec![EvalStmt::ClassDecl(EvalClass::with_relations( + name, parent, interfaces, properties, methods, ))]) } + /// Parses an optional `extends Parent` class declaration clause. + pub(super) fn parse_class_parent_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { + return Ok(None); + } + self.advance(); + let parent = self.parse_qualified_name()?; + Ok(Some(self.resolve_class_name(parent))) + } + + /// Parses an optional `implements Iface, ...` class declaration clause. + pub(super) fn parse_class_interface_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "implements")) { + return Ok(Vec::new()); + } + self.advance(); + let mut interfaces = Vec::new(); + loop { + let interface = self.parse_qualified_name()?; + interfaces.push(self.resolve_class_name(interface)); + if !self.consume(TokenKind::Comma) { + break; + } + } + Ok(interfaces) + } + /// Parses one public property or method from an eval class body. pub(super) fn parse_class_member( &mut self, diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 33731ca76c..7a5ad8aff0 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -22,6 +22,24 @@ fn parse_fragment_accepts_empty_class_declaration_source() { ))] ); } +/// Verifies class relation clauses lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_class_extends_and_implements_source() { + let program = parse_fragment( + br#"class DynEvalChild extends DynEvalBase implements DynEvalIface, \Root\Iface {}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_relations( + "DynEvalChild", + Some("DynEvalBase".to_string()), + vec!["DynEvalIface".to_string(), "Root\\Iface".to_string()], + Vec::new(), + Vec::new(), + ))] + ); +} /// Verifies public property and method class members lower into dynamic class metadata. #[test] fn parse_fragment_accepts_public_class_members() { diff --git a/crates/elephc-eval/src/parser/tests/namespaces.rs b/crates/elephc-eval/src/parser/tests/namespaces.rs index 9c873f5194..d215aafb0a 100644 --- a/crates/elephc-eval/src/parser/tests/namespaces.rs +++ b/crates/elephc-eval/src/parser/tests/namespaces.rs @@ -102,6 +102,30 @@ return Alias(LocalValue, new BoxAlias\Inner());"#, }))] ); } +/// Verifies namespace class imports apply to eval class relation clauses. +#[test] +fn parse_fragment_accepts_imported_class_relations() { + let program = parse_fragment( + br#"namespace Eval\UseNs; +use Lib\Base as BaseAlias; +use Lib\Contracts\Iface; +class Child extends BaseAlias implements Iface, \Shared\Root {}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_relations( + "Eval\\UseNs\\Child", + Some("Lib\\Base".to_string()), + vec![ + "Lib\\Contracts\\Iface".to_string(), + "Shared\\Root".to_string() + ], + Vec::new(), + Vec::new(), + ))] + ); +} /// Verifies grouped namespace imports resolve functions, constants, and class aliases. #[test] fn parse_fragment_accepts_grouped_namespace_use_imports() { diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 689da3405c..d74ebc8845 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -226,6 +226,7 @@ fn finalize_user_asm( &user_functions, &function_variant_groups, &runtime_interfaces, + &module.declared_interface_names, &module.trait_table.names, &runtime_classes, &module.enum_infos, diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index b6394565a9..81c7a30694 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -27,6 +27,7 @@ pub(crate) fn emit_runtime_data_user( functions: &HashMap, function_variant_groups: &HashSet, interfaces: &HashMap, + interface_names: &[String], trait_names: &[String], classes: &HashMap, enums: &HashMap, @@ -120,6 +121,13 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); super::instanceof::emit_instanceof_target_lookup_data(&mut out, &sorted_interfaces, &sorted_classes); emit_class_name_lookup_data(&mut out, max_class_id, &class_name_by_id); + emit_name_lookup_data( + &mut out, + "_interface_names_count", + "_interface_names", + "_interface_name", + interface_names, + ); emit_name_lookup_data( &mut out, "_trait_names_count", @@ -1452,6 +1460,7 @@ mod tests { &HashSet::new(), &HashMap::new(), &[], + &[], &classes, &HashMap::new(), Some(&allowed_class_names), @@ -1478,6 +1487,7 @@ mod tests { &HashSet::new(), &HashMap::new(), &[], + &[], &classes, &HashMap::new(), None, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 9d43468c20..58fd690ee3 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -146,21 +146,13 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #64"); // release the class-exists helper frame emitter.instruction("ret"); // return the class-exists flag to Rust - label_c_global(emitter, "__elephc_eval_interface_exists"); - emitter.instruction("sub sp, sp, #16"); // preserve the Rust return address across the metadata lookup - emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across runtime call - emitter.instruction("mov x29, sp"); // establish a stable interface-exists wrapper frame - emitter.instruction("mov x2, x1"); // move the C string length into the internal string-length register - emitter.instruction("mov x1, x0"); // move the C string pointer into the internal string-pointer register - emitter.instruction("bl __rt_instanceof_lookup"); // resolve the name against class/interface metadata - emitter.instruction("cmp x0, #0"); // did the name resolve to any class-like target? - emitter.instruction("cset x0, ne"); // keep true only when metadata lookup succeeded - emitter.instruction("cmp x2, #1"); // target kind 1 means interface metadata - emitter.instruction("cset x1, eq"); // keep true only for interface targets - emitter.instruction("and x0, x0, x1"); // return success && interface-kind - emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #16"); // release the interface-exists wrapper frame - emitter.instruction("ret"); // return the interface-exists flag to Rust + emit_aarch64_eval_name_table_exists( + emitter, + "__elephc_eval_interface_exists", + "_interface_names_count", + "_interface_names", + "__elephc_eval_interface_exists", + ); emit_aarch64_eval_name_table_exists( emitter, @@ -1496,20 +1488,13 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the class-exists flag to Rust - label_c_global(emitter, "__elephc_eval_interface_exists"); - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime call - emitter.instruction("mov rbp, rsp"); // establish a stable interface-exists wrapper frame - emitter.instruction("mov rax, rdi"); // move the C string pointer into the internal string-pointer register - emitter.instruction("mov rdx, rsi"); // move the C string length into the internal string-length register - emitter.instruction("call __rt_instanceof_lookup"); // resolve the name against class/interface metadata - emitter.instruction("test rax, rax"); // did the name resolve to any class-like target? - emitter.instruction("setne al"); // keep true only when metadata lookup succeeded - emitter.instruction("cmp rdx, 1"); // target kind 1 means interface metadata - emitter.instruction("sete cl"); // keep true only for interface targets - emitter.instruction("and al, cl"); // return success && interface-kind - emitter.instruction("movzx eax, al"); // widen the C boolean result for Rust - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the interface-exists flag to Rust + emit_x86_64_eval_name_table_exists( + emitter, + "__elephc_eval_interface_exists", + "_interface_names_count", + "_interface_names", + "__elephc_eval_interface_exists_x86", + ); emit_x86_64_eval_name_table_exists( emitter, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5abe7fa2d7..11c0b3a776 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1289,10 +1289,7 @@ echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . echo function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");'); "#, ); - assert_eq!( - out, - "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:1" - ); + assert_eq!(out, "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:1"); } /// Verifies eval natural sort builtins preserve keys and use natural string order. @@ -1313,10 +1310,7 @@ echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; echo function_exists("natsort") && function_exists("natcasesort");'); "#, ); - assert_eq!( - out, - "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:1" - ); + assert_eq!(out, "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:1"); } /// Verifies eval `shuffle()` reindexes direct variable arrays only. @@ -1433,10 +1427,7 @@ echo count($ints) . ":" . $ints[0] . ":" . $ints[2] . ":"; echo function_exists("array_filter");'); "#, ); - assert_eq!( - out, - "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:1" - ); + assert_eq!(out, "3:1:2:ok:drop:2:1:3:1:4:1:5:2:2:4:1:20:2:1:3:2:1:2:1"); } /// Verifies eval `array_combine()` supports PHP key conversions and callable dispatch. @@ -3266,7 +3257,10 @@ echo call_user_func_array("get_class", ["object" => $probe]) . ":"; echo function_exists("get_class");'); "#, ); - assert_eq!(out, "stdClass:EvalClassNameProbe:stdClass:EvalClassNameProbe:1"); + assert_eq!( + out, + "stdClass:EvalClassNameProbe:stdClass:EvalClassNameProbe:1" + ); } /// Verifies eval `get_parent_class()` resolves AOT object and class-string parents. @@ -4549,6 +4543,46 @@ echo function_exists("is_a"); echo function_exists("is_subclass_of");'); assert_eq!(out, "YYYNYYYYN11"); } +/// Verifies eval-declared class inheritance uses dynamic methods and metadata. +#[test] +fn test_eval_declared_class_inherits_methods_and_metadata() { + let out = compile_and_run_capture( + r#"base = $base; } + public function sum($n) { return $this->base + $this->tail + $n; } +} +class EvalDynChild extends EvalDynBase implements EvalDynIface { + public int $tail = 4; + public function read($n) { return $this->sum($n); } +} +$box = new EvalDynChild(3); +echo $box->read(5) . ":"; +echo get_parent_class($box) . ":"; +echo is_a($box, "EvalDynBase") ? "isa" : "bad"; echo ":"; +echo is_a($box, "EvalDynIface") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalDynChild") ? "bad" : "self"; echo ":"; +echo is_subclass_of($box, "EvalDynBase") ? "sub" : "bad"; echo ":"; +$parents = class_parents($box); +echo count($parents) . ":" . $parents["EvalDynBase"] . ":"; +$implements = class_implements("EvalDynChild"); +echo count($implements) . ":" . $implements["EvalDynIface"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "12:EvalDynBase:isa:iface:self:sub:1:EvalDynBase:1:EvalDynIface" + ); +} + /// Verifies duplicate eval-declared functions fail through the runtime bridge. #[test] fn test_eval_duplicate_declared_function_fails() { @@ -4818,7 +4852,8 @@ echo eval('return include "eval-plain-piece.txt";'); /// Verifies missing eval require aborts through the runtime eval fatal path. #[test] fn test_eval_fragment_missing_require_fails() { - let err = compile_and_run_expect_failure(" Date: Thu, 18 Jun 2026 16:09:29 +0200 Subject: [PATCH 0324/1208] Support eval declared interfaces --- crates/elephc-eval/src/context.rs | 119 +++++++++++++++++- crates/elephc-eval/src/eval_ir.rs | 66 ++++++++++ .../interpreter/builtins/class_metadata.rs | 10 ++ .../builtins/registry/dispatch/symbols.rs | 2 +- .../src/interpreter/builtins/symbols.rs | 26 +++- .../src/interpreter/dynamic_functions.rs | 1 + crates/elephc-eval/src/interpreter/mod.rs | 4 +- .../elephc-eval/src/interpreter/statements.rs | 84 ++++++++++++- .../src/interpreter/tests/builtins_symbols.rs | 21 ++++ .../elephc-eval/src/interpreter/tests/core.rs | 28 +++++ .../src/interpreter/tests/expressions.rs | 56 +++++++++ crates/elephc-eval/src/parser/cursor.rs | 2 +- crates/elephc-eval/src/parser/statements.rs | 71 ++++++++++- .../src/parser/tests/classes_errors.rs | 22 ++++ .../src/parser/tests/namespaces.rs | 23 ++++ tests/codegen/eval.rs | 38 ++++++ 16 files changed, 557 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index c10f56eaba..0f1e1f01f3 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -15,7 +15,9 @@ use std::collections::{HashMap, HashSet}; use std::ffi::c_void; use crate::abi::ABI_VERSION; -use crate::eval_ir::{EvalClass, EvalClassMethod, EvalFunction}; +use crate::eval_ir::{ + EvalClass, EvalClassMethod, EvalFunction, EvalInterface, EvalInterfaceMethod, +}; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; use crate::value::{RuntimeCell, RuntimeCellHandle}; @@ -90,6 +92,8 @@ pub struct ElephcEvalContext { classes: HashMap, class_aliases: HashMap, declared_class_names: Vec, + interfaces: HashMap, + declared_interface_names: Vec, constants: HashMap, functions: HashMap, native_functions: HashMap, @@ -117,6 +121,8 @@ impl ElephcEvalContext { classes: HashMap::new(), class_aliases: HashMap::new(), declared_class_names: Vec::new(), + interfaces: HashMap::new(), + declared_interface_names: Vec::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -145,6 +151,8 @@ impl ElephcEvalContext { classes: HashMap::new(), class_aliases: HashMap::new(), declared_class_names: Vec::new(), + interfaces: HashMap::new(), + declared_interface_names: Vec::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -173,7 +181,10 @@ impl ElephcEvalContext { /// Defines an eval-declared class, failing if this context already has it. pub fn define_class(&mut self, class: EvalClass) -> bool { let key = normalize_class_name(class.name()); - if self.classes.contains_key(&key) || self.class_aliases.contains_key(&key) { + if self.classes.contains_key(&key) + || self.class_aliases.contains_key(&key) + || self.interfaces.contains_key(&key) + { return false; } self.declared_class_names.push(class.name().to_string()); @@ -220,6 +231,7 @@ impl ElephcEvalContext { let alias_key = normalize_class_name(alias); if alias_key.is_empty() || self.classes.contains_key(&alias_key) + || self.interfaces.contains_key(&alias_key) || self.class_aliases.contains_key(&alias_key) { return false; @@ -236,6 +248,36 @@ impl ElephcEvalContext { &self.declared_class_names } + /// Defines an eval-declared interface, failing if this context already has the name. + pub fn define_interface(&mut self, interface: EvalInterface) -> bool { + let key = normalize_class_name(interface.name()); + if self.interfaces.contains_key(&key) + || self.classes.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + return false; + } + self.declared_interface_names + .push(interface.name().to_string()); + self.interfaces.insert(key, interface); + true + } + + /// Returns true when this eval context has a dynamic interface with the requested name. + pub fn has_interface(&self, name: &str) -> bool { + self.interfaces.contains_key(&normalize_class_name(name)) + } + + /// Returns a dynamic eval interface by PHP case-insensitive interface name. + pub fn interface(&self, name: &str) -> Option<&EvalInterface> { + self.interfaces.get(&normalize_class_name(name)) + } + + /// Returns interface names declared through eval in PHP-visible order. + pub fn declared_interface_names(&self) -> &[String] { + &self.declared_interface_names + } + /// Records that one runtime object handle was created for an eval-declared class. pub fn register_dynamic_object(&mut self, identity: u64, class_name: &str) { let class_name = self @@ -327,11 +369,84 @@ impl ElephcEvalContext { for class in self.class_chain(class_name) { for interface in class.interfaces() { push_unique_class_name(interface, &mut interfaces, &mut seen); + self.collect_interface_parent_names(interface, &mut interfaces, &mut seen); } } interfaces } + /// Returns parent interface names for an eval-declared interface. + pub fn interface_parent_names(&self, interface_name: &str) -> Vec { + let mut parents = Vec::new(); + let mut seen = HashSet::new(); + self.collect_interface_parent_names(interface_name, &mut parents, &mut seen); + parents + } + + /// Collects eval-declared interface parents without following cycles. + fn collect_interface_parent_names( + &self, + interface_name: &str, + names: &mut Vec, + seen: &mut HashSet, + ) { + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + push_unique_class_name(parent, names, seen); + self.collect_interface_parent_names(parent, names, seen); + } + } + + /// Returns direct and inherited method requirements for an eval interface. + pub fn interface_method_requirements( + &self, + interface_name: &str, + ) -> Vec { + let mut methods = Vec::new(); + let mut seen_interfaces = HashSet::new(); + let mut seen_methods = HashSet::new(); + self.collect_interface_method_requirements( + interface_name, + &mut methods, + &mut seen_interfaces, + &mut seen_methods, + ); + methods + } + + /// Collects eval interface methods without duplicating inherited method names. + fn collect_interface_method_requirements( + &self, + interface_name: &str, + methods: &mut Vec, + seen_interfaces: &mut HashSet, + seen_methods: &mut HashSet, + ) { + let key = normalize_class_name(interface_name); + if !seen_interfaces.insert(key) { + return; + } + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + self.collect_interface_method_requirements( + parent, + methods, + seen_interfaces, + seen_methods, + ); + } + for method in interface.methods() { + let key = method.name().to_ascii_lowercase(); + if seen_methods.insert(key) { + methods.push(method.clone()); + } + } + } + /// Returns whether an eval-declared class satisfies one class/interface target. pub fn class_is_a(&self, class_name: &str, target: &str, exclude_self: bool) -> bool { let Some(class) = self.class(class_name) else { diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index b987b7ab6a..8ce4fc323a 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -69,6 +69,7 @@ pub enum EvalStmt { body: Vec, }, ClassDecl(EvalClass), + InterfaceDecl(EvalInterface), Foreach { array: EvalExpr, key_name: Option, @@ -168,6 +169,71 @@ impl EvalFunction { } } +/// Runtime interface declared by an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalInterface { + name: String, + parents: Vec, + methods: Vec, +} + +impl EvalInterface { + /// Creates a dynamic eval interface with optional parent interfaces and methods. + pub fn new( + name: impl Into, + parents: Vec, + methods: Vec, + ) -> Self { + Self { + name: name.into(), + parents, + methods, + } + } + + /// Returns the original source spelling of this eval-declared interface name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns interface names extended directly by this eval interface. + pub fn parents(&self) -> &[String] { + &self.parents + } + + /// Returns method signatures declared directly by this eval interface. + pub fn methods(&self) -> &[EvalInterfaceMethod] { + &self.methods + } +} + +/// Method signature metadata for a runtime eval interface. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalInterfaceMethod { + name: String, + params: Vec, +} + +impl EvalInterfaceMethod { + /// Creates one dynamic eval interface method signature. + pub fn new(name: impl Into, params: Vec) -> Self { + Self { + name: name.into(), + params, + } + } + + /// Returns the PHP-visible method name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } +} + /// Runtime class declared by an eval fragment. #[derive(Debug, Clone, PartialEq)] pub struct EvalClass { diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 992ab567b7..a024e80a04 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -72,6 +72,15 @@ pub(in crate::interpreter) fn eval_class_relation_target_result( _ => Err(EvalStatus::RuntimeFatal), }; } + if context.interface(&target).is_some() { + return match name { + "class_implements" => { + eval_class_relation_names_result(context.interface_parent_names(&target), values) + } + "class_parents" | "class_uses" => values.assoc_new(0), + _ => Err(EvalStatus::RuntimeFatal), + }; + } values.assoc_new(0) } @@ -138,6 +147,7 @@ fn eval_class_relation_name_exists( values: &mut impl RuntimeValueOps, ) -> Result { if context.has_class(name) + || context.has_interface(name) || values.class_exists(name)? || values.interface_exists(name)? || values.trait_exists(name)? diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index 8c358b251d..3695416932 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -64,7 +64,7 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( "enum_exists" | "trait_exists" => { eval_class_like_exists_result(name, evaluated_args, values)? } - "interface_exists" => eval_interface_exists_result(evaluated_args, values)?, + "interface_exists" => eval_interface_exists_result(evaluated_args, context, values)?, "is_a" | "is_subclass_of" => { eval_is_a_relation_result(name, evaluated_args, context, values)? } diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index bad7607749..c852222d36 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -258,7 +258,10 @@ pub(in crate::interpreter) fn eval_get_declared_symbols_result( "get_declared_classes" => { eval_dynamic_string_array_result(context.declared_class_names(), values) } - "get_declared_interfaces" | "get_declared_traits" => { + "get_declared_interfaces" => { + eval_dynamic_string_array_result(context.declared_interface_names(), values) + } + "get_declared_traits" => { eval_dynamic_string_array_result(&[], values) } _ => Err(EvalStatus::RuntimeFatal), @@ -296,31 +299,34 @@ pub(in crate::interpreter) fn eval_builtin_interface_exists( } _ => return Err(EvalStatus::RuntimeFatal), }; - let exists = eval_interface_exists_name(name, values)?; + let exists = eval_interface_exists_name(name, context, values)?; values.bool_value(exists) } /// Evaluates `interface_exists(...)` from already materialized call arguments. pub(in crate::interpreter) fn eval_interface_exists_result( evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let exists = match evaluated_args { - [name] => eval_interface_exists_name(*name, values)?, - [name, _autoload] => eval_interface_exists_name(*name, values)?, + [name] => eval_interface_exists_name(*name, context, values)?, + [name, _autoload] => eval_interface_exists_name(*name, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }; values.bool_value(exists) } -/// Normalizes a PHP interface-name cell and probes generated interface metadata. +/// Normalizes a PHP interface-name cell and probes eval and generated interface metadata. pub(in crate::interpreter) fn eval_interface_exists_name( name: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let name = values.string_bytes(name)?; let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - values.interface_exists(name.trim_start_matches('\\')) + let name = name.trim_start_matches('\\'); + Ok(context.has_interface(name) || values.interface_exists(name)?) } /// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. @@ -427,6 +433,14 @@ pub(in crate::interpreter) fn eval_is_a_relation_result( || values.object_is_a(object_or_class, &resolved_target_class, exclude_self), Ok, )? + } else if allow_string && values.type_tag(object_or_class)? == EVAL_TAG_STRING { + let source_class = values.string_bytes(object_or_class)?; + let source_class = String::from_utf8(source_class).map_err(|_| EvalStatus::RuntimeFatal)?; + if context.class(&source_class).is_some() { + context.class_is_a(&source_class, &resolved_target_class, exclude_self) + } else { + values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? + } } else if allow_string { values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? } else { diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 927a331d32..f97929c30f 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -281,6 +281,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::Echo(_) | EvalStmt::Expr(_) | EvalStmt::Global { .. } + | EvalStmt::InterfaceDecl(_) | EvalStmt::PropertySet { .. } | EvalStmt::ReferenceAssign { .. } | EvalStmt::Return(_) diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index e5d0d0036e..f4ba59708e 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -31,8 +31,8 @@ use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, EvalConst, - EvalExpr, EvalFunction, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, - EvalUnaryOp, + EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalMagicConst, EvalMatchArm, + EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 851a718fe6..f384b375a5 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -110,6 +110,10 @@ pub(in crate::interpreter) fn execute_stmt( execute_class_decl_stmt(class, context, values)?; Ok(EvalControl::None) } + EvalStmt::InterfaceDecl(interface) => { + execute_interface_decl_stmt(interface, context, values)?; + Ok(EvalControl::None) + } EvalStmt::Foreach { array, key_name, @@ -288,7 +292,7 @@ pub(in crate::interpreter) fn execute_matching_catch( ) -> Result { let mut matched = None; for catch in catches { - if catch_types_match_thrown(thrown, &catch.class_names, values)? { + if catch_types_match_thrown(thrown, &catch.class_names, context, values)? { matched = Some(catch); break; } @@ -316,6 +320,7 @@ pub(in crate::interpreter) fn execute_matching_catch( pub(in crate::interpreter) fn catch_types_match_thrown( thrown: RuntimeCellHandle, class_names: &[String], + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { for class_name in class_names { @@ -323,6 +328,12 @@ pub(in crate::interpreter) fn catch_types_match_thrown( if class_name.eq_ignore_ascii_case("Throwable") { return Ok(true); } + if let Some(matched) = dynamic_object_is_a(thrown, class_name, false, context, values)? { + if matched { + return Ok(true); + } + continue; + } if values.object_is_a(thrown, class_name, false)? { return Ok(true); } @@ -337,7 +348,11 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let name = class.name().trim_start_matches('\\'); - if context.has_class(name) || values.class_exists(name)? { + if context.has_class(name) + || context.has_interface(name) + || values.class_exists(name)? + || values.interface_exists(name)? + { return Err(EvalStatus::RuntimeFatal); } if let Some(parent) = class.parent() { @@ -346,7 +361,9 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( } } for interface in class.interfaces() { - if !values.interface_exists(interface)? { + if context.has_interface(interface) { + validate_class_implements_eval_interface(class, interface, context)?; + } else if !values.interface_exists(interface)? { return Err(EvalStatus::RuntimeFatal); } } @@ -357,6 +374,67 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( } } +/// Registers an eval-declared interface in the dynamic interface table. +pub(in crate::interpreter) fn execute_interface_decl_stmt( + interface: &EvalInterface, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = interface.name().trim_start_matches('\\'); + if context.has_interface(name) + || context.has_class(name) + || values.interface_exists(name)? + || values.class_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + for parent in interface.parents() { + if context.interface_parent_names(parent) + .iter() + .any(|ancestor| ancestor.eq_ignore_ascii_case(name)) + { + return Err(EvalStatus::RuntimeFatal); + } + if !context.has_interface(parent) && !values.interface_exists(parent)? { + return Err(EvalStatus::RuntimeFatal); + } + } + if context.define_interface(interface.clone()) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Validates that one eval class provides methods required by one eval interface. +fn validate_class_implements_eval_interface( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for requirement in context.interface_method_requirements(interface_name) { + if !class_has_interface_method(class, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns whether a class or its eval parents satisfy one interface method signature. +fn class_has_interface_method( + class: &EvalClass, + requirement: &EvalInterfaceMethod, + context: &ElephcEvalContext, +) -> bool { + if let Some(method) = class.method(requirement.name()) { + return method.params().len() == requirement.params().len(); + } + class + .parent() + .and_then(|parent| context.class_method(parent, requirement.name())) + .is_some_and(|(_, method)| method.params().len() == requirement.params().len()) +} + /// Creates a backing object for an eval-declared class and runs its constructor. pub(in crate::interpreter) fn eval_dynamic_class_new_object( class: &EvalClass, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs index 4a47727301..fbab44c22c 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs @@ -149,6 +149,27 @@ echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N assert_eq!(values.output, "YYNYYN"); } +/// Verifies eval-declared interfaces are visible to interface symbol probes. +#[test] +fn execute_program_interface_exists_uses_dynamic_interface_table() { + let program = parse_fragment( + br#"interface DynEvalIface {} +echo interface_exists("DynEvalIface") ? "Y" : "N"; +echo interface_exists("dynevaliface") ? "Y" : "N"; +echo class_exists("DynEvalIface") ? "C" : "c"; +echo call_user_func("interface_exists", "DynEvalIface") ? "Y" : "N"; +echo call_user_func_array("interface_exists", ["interface" => "\DynEvalIface"]) ? "Y" : "N"; +$interfaces = get_declared_interfaces(); +echo count($interfaces); echo ":"; echo $interfaces[0];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "YYcYY1:DynEvalIface"); +} /// Verifies eval `trait_exists()` and `enum_exists()` probe generated metadata. #[test] fn execute_program_class_like_exists_uses_runtime_probe() { diff --git a/crates/elephc-eval/src/interpreter/tests/core.rs b/crates/elephc-eval/src/interpreter/tests/core.rs index f7dda71fa8..87ebaa12c1 100644 --- a/crates/elephc-eval/src/interpreter/tests/core.rs +++ b/crates/elephc-eval/src/interpreter/tests/core.rs @@ -130,6 +130,34 @@ fn execute_program_catches_specific_exception_inside_eval() { assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); assert_eq!(values.get(result), FakeValue::Int(42)); } +/// Verifies eval `catch` clauses can match eval-declared interfaces. +#[test] +fn execute_program_catches_eval_declared_interface_inside_eval() { + let program = parse_fragment( + br#"interface EvalCatchable { + function value(); +} +class EvalThrownBox implements EvalCatchable { + public function value() { return 42; } +} +try { + throw new EvalThrownBox(); +} catch (EvalCatchable $caught) { + return $caught->value(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let caught = scope + .visible_cell("caught") + .expect("scope should contain catch variable"); + + assert_eq!(values.type_tag(caught), Ok(EVAL_TAG_OBJECT)); + assert_eq!(values.get(result), FakeValue::Int(42)); +} /// Verifies eval catch clauses keep source order and skip non-matching types. #[test] fn execute_program_skips_non_matching_specific_catch_inside_eval() { diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index b604c7b6fc..3611f17c54 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -375,3 +375,59 @@ return $box->base;"#, ); assert_eq!(values.get(result), FakeValue::Int(3)); } +/// Verifies eval-declared classes can implement eval-declared interfaces. +#[test] +fn execute_program_constructs_eval_declared_class_with_dynamic_interface() { + let program = parse_fragment( + br#"interface EvalReader { + function read($n); +} +interface EvalNamedReader extends EvalReader { + function label(); +} +class EvalReaderBox implements EvalNamedReader { + public function read($n) { return $n + 1; } + public function label() { return "box"; } +} +$box = new EvalReaderBox(); +echo $box->read(4); echo ":"; +echo $box->label(); echo ":"; +echo is_a($box, "EvalNamedReader") ? "isa" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalReader") ? "sub" : "bad"; echo ":"; +echo is_subclass_of("EvalReaderBox", "EvalReader") ? "str" : "bad"; echo ":"; +$implements = class_implements($box); +echo count($implements); echo ":"; +echo $implements["EvalNamedReader"]; echo ":"; +echo $implements["EvalReader"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "5:box:isa:sub:str:2:EvalNamedReader:EvalReader" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval rejects classes missing methods required by eval interfaces. +#[test] +fn execute_program_rejects_missing_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsRead { + function read($n); +} +class EvalMissingRead implements EvalNeedsRead {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing interface method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/parser/cursor.rs b/crates/elephc-eval/src/parser/cursor.rs index 947c85caad..215c0b7dc3 100644 --- a/crates/elephc-eval/src/parser/cursor.rs +++ b/crates/elephc-eval/src/parser/cursor.rs @@ -126,7 +126,7 @@ pub(super) fn ident_eq(actual: &str, expected: &str) -> bool { /// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. pub(super) fn is_unsupported_statement_keyword(name: &str) -> bool { - ["enum", "interface", "trait"] + ["enum", "trait"] .iter() .any(|keyword| ident_eq(name, keyword)) } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 3634a4d57a..b44fdd2ab8 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -12,7 +12,8 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalCatch, EvalClass, EvalClassMethod, EvalClassProperty, EvalExpr, EvalStmt, EvalSwitchCase, + EvalCatch, EvalClass, EvalClassMethod, EvalClassProperty, EvalExpr, EvalInterface, + EvalInterfaceMethod, EvalStmt, EvalSwitchCase, }; use crate::lexer::TokenKind; @@ -46,6 +47,9 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), + TokenKind::Ident(name) if ident_eq(name, "interface") => { + self.parse_interface_decl_stmt() + } TokenKind::Ident(name) if ident_eq(name, "namespace") => self.parse_namespace_stmt(), TokenKind::Ident(name) if ident_eq(name, "return") => { self.advance(); @@ -304,6 +308,71 @@ impl Parser { Ok(EvalClassProperty::new(name, default)) } + /// Parses `interface Name [extends Parent, ...] { function name(...); }`. + pub(super) fn parse_interface_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + let parents = self.parse_interface_parent_clause()?; + self.expect(TokenKind::LBrace)?; + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + methods.push(self.parse_interface_method_decl()?); + } + self.consume_semicolon(); + Ok(vec![EvalStmt::InterfaceDecl(EvalInterface::new( + name, parents, methods, + ))]) + } + + /// Parses an optional `extends Parent, ...` interface declaration clause. + pub(super) fn parse_interface_parent_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { + return Ok(Vec::new()); + } + self.advance(); + let mut parents = Vec::new(); + loop { + let parent = self.parse_qualified_name()?; + parents.push(self.resolve_class_name(parent)); + if !self.consume(TokenKind::Comma) { + break; + } + } + Ok(parents) + } + + /// Parses one eval interface method signature. + pub(super) fn parse_interface_method_decl( + &mut self, + ) -> Result { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) { + self.advance(); + } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) + { + return Err(EvalParseError::UnsupportedConstruct); + } + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let params = self.parse_function_params()?; + self.expect_semicolon()?; + Ok(EvalInterfaceMethod::new(name, params)) + } + /// Consumes a simple declared property type before the `$property` token. pub(super) fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { if matches!(self.current(), TokenKind::DollarIdent(_)) { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 7a5ad8aff0..f5d4add864 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -40,6 +40,28 @@ fn parse_fragment_accepts_class_extends_and_implements_source() { ))] ); } +/// Verifies eval interface declarations lower to dynamic interface metadata. +#[test] +fn parse_fragment_accepts_interface_declaration_source() { + let program = parse_fragment( + br#"interface DynEvalIface extends ParentIface, \Root\Iface { + public function read($value); + function label(); +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl(EvalInterface::new( + "DynEvalIface", + vec!["ParentIface".to_string(), "Root\\Iface".to_string()], + vec![ + EvalInterfaceMethod::new("read", vec!["value".to_string()]), + EvalInterfaceMethod::new("label", Vec::new()), + ], + ))] + ); +} /// Verifies public property and method class members lower into dynamic class metadata. #[test] fn parse_fragment_accepts_public_class_members() { diff --git a/crates/elephc-eval/src/parser/tests/namespaces.rs b/crates/elephc-eval/src/parser/tests/namespaces.rs index d215aafb0a..89857209c0 100644 --- a/crates/elephc-eval/src/parser/tests/namespaces.rs +++ b/crates/elephc-eval/src/parser/tests/namespaces.rs @@ -126,6 +126,29 @@ class Child extends BaseAlias implements Iface, \Shared\Root {}"#, ))] ); } +/// Verifies namespace imports apply to eval interface parent clauses. +#[test] +fn parse_fragment_accepts_imported_interface_parents() { + let program = parse_fragment( + br#"namespace Eval\UseNs; +use Lib\Contracts\BaseIface; +interface LocalIface extends BaseIface, \Shared\RootIface { + function read(); +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl(EvalInterface::new( + "Eval\\UseNs\\LocalIface", + vec![ + "Lib\\Contracts\\BaseIface".to_string(), + "Shared\\RootIface".to_string() + ], + vec![EvalInterfaceMethod::new("read", Vec::new())], + ))] + ); +} /// Verifies grouped namespace imports resolve functions, constants, and class aliases. #[test] fn parse_fragment_accepts_grouped_namespace_use_imports() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 11c0b3a776..9c670ffdb6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4583,6 +4583,44 @@ echo count($implements) . ":" . $implements["EvalDynIface"];'); ); } +/// Verifies eval-declared interfaces are usable by eval-declared classes. +#[test] +fn test_eval_declared_interface_metadata_and_implementation() { + let out = compile_and_run_capture( + r#"read(4) . ":"; +echo $box->label() . ":"; +echo is_a($box, "EvalDynNamedReader") ? "isa" : "bad"; echo ":"; +echo is_subclass_of("EvalDynReaderBox", "EvalDynReader") ? "str" : "bad"; echo ":"; +$implements = class_implements($box); +echo count($implements) . ":" . $implements["EvalDynNamedReader"] . ":" . $implements["EvalDynReader"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "iface:notclass:2:5:box:isa:str:2:EvalDynNamedReader:EvalDynReader" + ); +} + /// Verifies duplicate eval-declared functions fail through the runtime bridge. #[test] fn test_eval_duplicate_declared_function_fails() { From 8d806caf9a930bb00497627db4bd74d603987ee8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 16:21:00 +0200 Subject: [PATCH 0325/1208] Support eval abstract and final classes --- crates/elephc-eval/src/eval_ir.rs | 54 +++++- .../elephc-eval/src/interpreter/statements.rs | 168 +++++++++++++++++- .../src/interpreter/tests/expressions.rs | 101 +++++++++++ crates/elephc-eval/src/parser/statements.rs | 122 +++++++++++-- .../src/parser/tests/classes_errors.rs | 40 +++++ tests/codegen/eval.rs | 46 +++++ 6 files changed, 505 insertions(+), 26 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 8ce4fc323a..c6ac853500 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -238,6 +238,8 @@ impl EvalInterfaceMethod { #[derive(Debug, Clone, PartialEq)] pub struct EvalClass { name: String, + is_abstract: bool, + is_final: bool, parent: Option, interfaces: Vec, properties: Vec, @@ -251,7 +253,7 @@ impl EvalClass { properties: Vec, methods: Vec, ) -> Self { - Self::with_relations(name, None, Vec::new(), properties, methods) + Self::with_modifiers(name, false, false, None, Vec::new(), properties, methods) } /// Creates a dynamic eval class with optional parent and implemented interfaces. @@ -261,9 +263,24 @@ impl EvalClass { interfaces: Vec, properties: Vec, methods: Vec, + ) -> Self { + Self::with_modifiers(name, false, false, parent, interfaces, properties, methods) + } + + /// Creates a dynamic eval class with optional modifiers, parent, and interfaces. + pub fn with_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), + is_abstract, + is_final, parent, interfaces, properties, @@ -276,6 +293,16 @@ impl EvalClass { &self.name } + /// Returns whether this eval-declared class was declared `abstract`. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + + /// Returns whether this eval-declared class was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + /// Returns the parent class name declared by this eval class, when present. pub fn parent(&self) -> Option<&str> { self.parent.as_deref() @@ -335,6 +362,8 @@ impl EvalClassProperty { #[derive(Debug, Clone, PartialEq)] pub struct EvalClassMethod { name: String, + is_abstract: bool, + is_final: bool, params: Vec, body: Vec, } @@ -342,8 +371,21 @@ pub struct EvalClassMethod { impl EvalClassMethod { /// Creates a public eval class method with source-order parameters and body. pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { + Self::with_modifiers(name, false, false, params, body) + } + + /// Creates a public eval class method with optional abstract/final modifiers. + pub fn with_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + params: Vec, + body: Vec, + ) -> Self { Self { name: name.into(), + is_abstract, + is_final, params, body, } @@ -354,6 +396,16 @@ impl EvalClassMethod { &self.name } + /// Returns whether this eval-declared method was declared `abstract`. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + + /// Returns whether this eval-declared method was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + /// Returns source-order parameter names without leading `$`. pub fn params(&self) -> &[String] { &self.params diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index f384b375a5..fc331c2665 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -355,18 +355,23 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( { return Err(EvalStatus::RuntimeFatal); } + validate_eval_class_modifiers(class, context)?; if let Some(parent) = class.parent() { - if context.class(parent).is_none() || context.class_is_a(parent, name, false) { + let Some(parent_class) = context.class(parent) else { + return Err(EvalStatus::RuntimeFatal); + }; + if parent_class.is_final() || context.class_is_a(parent, name, false) { return Err(EvalStatus::RuntimeFatal); } } for interface in class.interfaces() { - if context.has_interface(interface) { - validate_class_implements_eval_interface(class, interface, context)?; - } else if !values.interface_exists(interface)? { + if !context.has_interface(interface) && !values.interface_exists(interface)? { return Err(EvalStatus::RuntimeFatal); } } + if !class.is_abstract() { + validate_concrete_class_requirements(class, context)?; + } if context.define_class(class.clone()) { Ok(()) } else { @@ -389,7 +394,8 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( return Err(EvalStatus::RuntimeFatal); } for parent in interface.parents() { - if context.interface_parent_names(parent) + if context + .interface_parent_names(parent) .iter() .any(|ancestor| ancestor.eq_ignore_ascii_case(name)) { @@ -406,6 +412,146 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( } } +/// Validates abstract/final modifiers on an eval-declared class and its methods. +fn validate_eval_class_modifiers( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if class.is_abstract() && class.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + for method in class.methods() { + if method.is_abstract() && method.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && !class.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_method_parent_override(class, method, context)?; + } + Ok(()) +} + +/// Validates one method declaration against inherited eval method metadata. +fn validate_method_parent_override( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(()); + }; + let Some((_, parent_method)) = context.class_method(parent, method.name()) else { + return Ok(()); + }; + if parent_method.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && !parent_method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Validates that a concrete class has satisfied inherited abstract and interface requirements. +fn validate_concrete_class_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !pending_class_abstract_method_requirements(class, context).is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) { + validate_class_implements_eval_interface(class, &interface, context)?; + } + } + Ok(()) +} + +/// Returns inherited abstract methods that the pending class has not concretized. +fn pending_class_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Vec { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_class_abstract_method_requirements(parent, context, &mut requirements); + } + apply_class_abstract_method_requirements(class, &mut requirements); + requirements.into_values().collect() +} + +/// Collects abstract method requirements from one declared eval class ancestry chain. +fn collect_class_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) { + let Some(class) = context.class(class_name) else { + return; + }; + if let Some(parent) = class.parent() { + collect_class_abstract_method_requirements(parent, context, requirements); + } + apply_class_abstract_method_requirements(class, requirements); +} + +/// Applies one class's methods to the open abstract-method requirement set. +fn apply_class_abstract_method_requirements( + class: &EvalClass, + requirements: &mut std::collections::HashMap, +) { + for method in class.methods() { + let key = method.name().to_ascii_lowercase(); + if method.is_abstract() { + requirements.insert(key, method.clone()); + } else { + requirements.remove(&key); + } + } +} + +/// Returns interface names inherited or directly declared by a pending eval class. +fn pending_class_interface_names(class: &EvalClass, context: &ElephcEvalContext) -> Vec { + let mut interfaces = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = class.parent() { + for interface in context.class_interface_names(parent) { + push_pending_class_interface_name(&interface, &mut interfaces, &mut seen); + } + } + for interface in class.interfaces() { + push_pending_class_interface_tree(interface, context, &mut interfaces, &mut seen); + } + interfaces +} + +/// Adds one interface and its eval-declared parent interfaces to a pending class list. +fn push_pending_class_interface_tree( + interface: &str, + context: &ElephcEvalContext, + interfaces: &mut Vec, + seen: &mut std::collections::HashSet, +) { + push_pending_class_interface_name(interface, interfaces, seen); + for parent in context.interface_parent_names(interface) { + push_pending_class_interface_name(&parent, interfaces, seen); + } +} + +/// Adds one interface name once using PHP class-name case-insensitive matching. +fn push_pending_class_interface_name( + interface: &str, + interfaces: &mut Vec, + seen: &mut std::collections::HashSet, +) { + let interface = interface.trim_start_matches('\\'); + if seen.insert(interface.to_ascii_lowercase()) { + interfaces.push(interface.to_string()); + } +} + /// Validates that one eval class provides methods required by one eval interface. fn validate_class_implements_eval_interface( class: &EvalClass, @@ -427,12 +573,14 @@ fn class_has_interface_method( context: &ElephcEvalContext, ) -> bool { if let Some(method) = class.method(requirement.name()) { - return method.params().len() == requirement.params().len(); + return !method.is_abstract() && method.params().len() == requirement.params().len(); } class .parent() .and_then(|parent| context.class_method(parent, requirement.name())) - .is_some_and(|(_, method)| method.params().len() == requirement.params().len()) + .is_some_and(|(_, method)| { + !method.is_abstract() && method.params().len() == requirement.params().len() + }) } /// Creates a backing object for an eval-declared class and runs its constructor. @@ -443,6 +591,9 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { + if class.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } let object = values.new_object("stdClass")?; let identity = values.object_identity(object)?; context.register_dynamic_object(identity, class.name()); @@ -494,6 +645,9 @@ pub(in crate::interpreter) fn eval_method_call_result( let (class_name, method) = context .class_method(class.name(), method_name) .ok_or(EvalStatus::RuntimeFatal)?; + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } eval_dynamic_method_with_values( &class_name, &method, diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 3611f17c54..6523119265 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -413,6 +413,107 @@ return true;"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies concrete eval classes can implement abstract class and interface contracts. +#[test] +fn execute_program_constructs_concrete_child_from_abstract_eval_class() { + let program = parse_fragment( + br#"interface EvalAbstractReadable { + function read($n); +} +abstract class EvalAbstractBase implements EvalAbstractReadable { + abstract public function read($n); + public function wrap($n) { return $this->read($n) + 1; } +} +class EvalConcreteBox extends EvalAbstractBase { + public function read($n) { return $n + 3; } +} +$box = new EvalConcreteBox(); +echo $box->wrap(4); echo ":"; +echo is_a($box, "EvalAbstractReadable") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalAbstractBase") ? "abstract" : "bad"; +return $box->read(2);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:iface:abstract"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies eval rejects instantiation of abstract eval-declared classes. +#[test] +fn execute_program_rejects_abstract_eval_class_instantiation() { + let program = parse_fragment( + br#"abstract class EvalAbstractOnly { + public function read() { return 1; } +} +new EvalAbstractOnly();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("abstract class instantiation should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies concrete eval classes must implement inherited abstract methods. +#[test] +fn execute_program_rejects_concrete_eval_class_with_abstract_methods() { + let program = parse_fragment( + br#"abstract class EvalNeedsRead { + abstract public function read(); +} +class EvalMissingReadChild extends EvalNeedsRead {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("concrete class missing abstract method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects extending a final eval-declared class. +#[test] +fn execute_program_rejects_extending_final_eval_class() { + let program = parse_fragment( + br#"final class EvalFinalBase {} +class EvalFinalChild extends EvalFinalBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("extending final class should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects overriding a final eval-declared method. +#[test] +fn execute_program_rejects_overriding_final_eval_method() { + let program = parse_fragment( + br#"class EvalFinalMethodBase { + final public function read() { return 1; } +} +class EvalFinalMethodChild extends EvalFinalMethodBase { + public function read() { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} /// Verifies eval rejects classes missing methods required by eval interfaces. #[test] fn execute_program_rejects_missing_dynamic_interface_method() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index b44fdd2ab8..f4d5d9c7cd 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -43,6 +43,9 @@ impl Parser { } TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), + TokenKind::Ident(name) if ident_eq(name, "abstract") || ident_eq(name, "final") => { + self.parse_class_decl_stmt() + } TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), @@ -194,8 +197,12 @@ impl Parser { }]) } - /// Parses `class Name [extends Parent] [implements Iface, ...] { ... }`. + /// Parses `[abstract|final] class Name [extends Parent] [implements Iface, ...] { ... }`. pub(super) fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { + let (is_abstract, is_final) = self.parse_class_decl_modifiers()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return Err(EvalParseError::UnsupportedConstruct); + } self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -214,11 +221,42 @@ impl Parser { self.parse_class_member(&mut properties, &mut methods)?; } self.consume_semicolon(); - Ok(vec![EvalStmt::ClassDecl(EvalClass::with_relations( - name, parent, interfaces, properties, methods, + Ok(vec![EvalStmt::ClassDecl(EvalClass::with_modifiers( + name, + is_abstract, + is_final, + parent, + interfaces, + properties, + methods, ))]) } + /// Parses class-level `abstract` and `final` modifiers before `class`. + pub(super) fn parse_class_decl_modifiers(&mut self) -> Result<(bool, bool), EvalParseError> { + let mut is_abstract = false; + let mut is_final = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "abstract") => { + if is_abstract { + return Err(EvalParseError::UnsupportedConstruct); + } + is_abstract = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } + _ => return Ok((is_abstract, is_final)), + } + } + } + /// Parses an optional `extends Parent` class declaration clause. pub(super) fn parse_class_parent_clause(&mut self) -> Result, EvalParseError> { if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { @@ -252,31 +290,68 @@ impl Parser { properties: &mut Vec, methods: &mut Vec, ) -> Result<(), EvalParseError> { - let public = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) - { - self.advance(); - true - } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) - { + let (public, is_abstract, is_final) = self.parse_class_member_modifiers()?; + + if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); - } else { - false - }; + } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - methods.push(self.parse_class_method_decl()?); + methods.push(self.parse_class_method_decl(is_abstract, is_final)?); return Ok(()); } - if !public { + if !public || is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } properties.push(self.parse_class_property_decl()?); Ok(()) } - /// Parses `function name($param, ...) { ... }` inside a dynamic eval class. - pub(super) fn parse_class_method_decl(&mut self) -> Result { + /// Parses method modifiers supported by eval class declarations. + pub(super) fn parse_class_member_modifiers( + &mut self, + ) -> Result<(bool, bool, bool), EvalParseError> { + let mut public = false; + let mut is_abstract = false; + let mut is_final = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + if public { + return Err(EvalParseError::UnsupportedConstruct); + } + public = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "abstract") => { + if is_abstract { + return Err(EvalParseError::UnsupportedConstruct); + } + is_abstract = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } + TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { + return Err(EvalParseError::UnsupportedConstruct); + } + _ => return Ok((public, is_abstract, is_final)), + } + } + } + + /// Parses `function name($param, ...) { ... }` or an abstract method signature. + pub(super) fn parse_class_method_decl( + &mut self, + is_abstract: bool, + is_final: bool, + ) -> Result { self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -285,8 +360,19 @@ impl Parser { self.advance(); self.expect(TokenKind::LParen)?; let params = self.parse_function_params()?; - let body = self.parse_block()?; - Ok(EvalClassMethod::new(name, params, body)) + let body = if is_abstract { + self.expect_semicolon()?; + Vec::new() + } else { + self.parse_block()? + }; + Ok(EvalClassMethod::with_modifiers( + name, + is_abstract, + is_final, + params, + body, + )) } /// Parses one public property declaration with an optional initializer. diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index f5d4add864..bb37c17b08 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -88,6 +88,46 @@ fn parse_fragment_accepts_public_class_members() { ))] ); } +/// Verifies abstract and final class modifiers lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_abstract_and_final_class_members() { + let program = parse_fragment( + br#"abstract class DynEvalAbstract { + abstract public function read($value); + final public function label() { return "base"; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_modifiers( + "DynEvalAbstract", + true, + false, + None, + Vec::new(), + Vec::new(), + vec![ + EvalClassMethod::with_modifiers( + "read", + true, + false, + vec!["value".to_string()], + Vec::new() + ), + EvalClassMethod::with_modifiers( + "label", + false, + true, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( + "base".to_string() + ))))] + ), + ], + ))] + ); +} /// Verifies malformed object construction reports an unexpected token. #[test] fn parse_fragment_rejects_new_without_class_name() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9c670ffdb6..106ead6b27 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4621,6 +4621,52 @@ echo count($implements) . ":" . $implements["EvalDynNamedReader"] . ":" . $imple ); } +/// Verifies eval-declared abstract classes can defer interface methods to concrete children. +#[test] +fn test_eval_declared_abstract_class_and_final_method_contracts() { + let out = compile_and_run_capture( + r#"read($n) + 1; } +} +class EvalAbstractChild extends EvalAbstractBase { + public function read($n) { return $n + 2; } +} +$box = new EvalAbstractChild(); +echo $box->wrap(5) . ":"; +echo $box->label() . ":"; +echo is_a($box, "EvalAbstractContract") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalAbstractBase") ? "abstract" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "8:base:iface:abstract"); +} + +/// Verifies eval-declared final classes cannot be extended. +#[test] +fn test_eval_declared_final_class_extension_fails() { + let err = compile_and_run_expect_failure( + r#" Date: Thu, 18 Jun 2026 16:29:51 +0200 Subject: [PATCH 0326/1208] Support eval declared traits --- crates/elephc-eval/src/context.rs | 61 +++++++- crates/elephc-eval/src/eval_ir.rs | 69 +++++++++ .../interpreter/builtins/class_metadata.rs | 7 +- .../builtins/registry/dispatch/symbols.rs | 2 +- .../src/interpreter/builtins/symbols.rs | 12 +- .../src/interpreter/dynamic_functions.rs | 1 + crates/elephc-eval/src/interpreter/mod.rs | 6 +- .../elephc-eval/src/interpreter/statements.rs | 133 ++++++++++++++++++ .../src/interpreter/tests/expressions.rs | 93 ++++++++++++ crates/elephc-eval/src/parser/cursor.rs | 4 +- crates/elephc-eval/src/parser/statements.rs | 97 +++++++++++-- .../src/parser/tests/classes_errors.rs | 48 +++++++ tests/codegen/eval.rs | 53 +++++++ 13 files changed, 555 insertions(+), 31 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 0f1e1f01f3..209c3e8e62 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -1,7 +1,7 @@ //! Purpose: //! Declares the opaque process-level eval context handle. //! The full implementation will hold dynamic function, class, constant, and -//! builtin registries plus runtime hooks. +//! class-like, constant, builtin registries plus runtime hooks. //! //! Called from: //! - `crate::abi` @@ -16,7 +16,7 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::{ - EvalClass, EvalClassMethod, EvalFunction, EvalInterface, EvalInterfaceMethod, + EvalClass, EvalClassMethod, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalTrait, }; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; @@ -94,6 +94,8 @@ pub struct ElephcEvalContext { declared_class_names: Vec, interfaces: HashMap, declared_interface_names: Vec, + traits: HashMap, + declared_trait_names: Vec, constants: HashMap, functions: HashMap, native_functions: HashMap, @@ -123,6 +125,8 @@ impl ElephcEvalContext { declared_class_names: Vec::new(), interfaces: HashMap::new(), declared_interface_names: Vec::new(), + traits: HashMap::new(), + declared_trait_names: Vec::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -153,6 +157,8 @@ impl ElephcEvalContext { declared_class_names: Vec::new(), interfaces: HashMap::new(), declared_interface_names: Vec::new(), + traits: HashMap::new(), + declared_trait_names: Vec::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -184,6 +190,7 @@ impl ElephcEvalContext { if self.classes.contains_key(&key) || self.class_aliases.contains_key(&key) || self.interfaces.contains_key(&key) + || self.traits.contains_key(&key) { return false; } @@ -232,6 +239,7 @@ impl ElephcEvalContext { if alias_key.is_empty() || self.classes.contains_key(&alias_key) || self.interfaces.contains_key(&alias_key) + || self.traits.contains_key(&alias_key) || self.class_aliases.contains_key(&alias_key) { return false; @@ -253,6 +261,7 @@ impl ElephcEvalContext { let key = normalize_class_name(interface.name()); if self.interfaces.contains_key(&key) || self.classes.contains_key(&key) + || self.traits.contains_key(&key) || self.class_aliases.contains_key(&key) { return false; @@ -278,6 +287,37 @@ impl ElephcEvalContext { &self.declared_interface_names } + /// Defines an eval-declared trait, failing if this context already has the name. + pub fn define_trait(&mut self, trait_decl: EvalTrait) -> bool { + let key = normalize_class_name(trait_decl.name()); + if self.traits.contains_key(&key) + || self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + return false; + } + self.declared_trait_names + .push(trait_decl.name().to_string()); + self.traits.insert(key, trait_decl); + true + } + + /// Returns true when this eval context has a dynamic trait with the requested name. + pub fn has_trait(&self, name: &str) -> bool { + self.traits.contains_key(&normalize_class_name(name)) + } + + /// Returns a dynamic eval trait by PHP case-insensitive trait name. + pub fn trait_decl(&self, name: &str) -> Option<&EvalTrait> { + self.traits.get(&normalize_class_name(name)) + } + + /// Returns trait names declared through eval in PHP-visible order. + pub fn declared_trait_names(&self) -> &[String] { + &self.declared_trait_names + } + /// Records that one runtime object handle was created for an eval-declared class. pub fn register_dynamic_object(&mut self, identity: u64, class_name: &str) { let class_name = self @@ -375,6 +415,18 @@ impl ElephcEvalContext { interfaces } + /// Returns trait names used directly by an eval-declared class. + pub fn class_trait_names(&self, class_name: &str) -> Vec { + self.class(class_name).map_or_else(Vec::new, |class| { + let mut traits = Vec::new(); + let mut seen = HashSet::new(); + for trait_name in class.traits() { + push_unique_class_name(trait_name, &mut traits, &mut seen); + } + traits + }) + } + /// Returns parent interface names for an eval-declared interface. pub fn interface_parent_names(&self, interface_name: &str) -> Vec { let mut parents = Vec::new(); @@ -400,10 +452,7 @@ impl ElephcEvalContext { } /// Returns direct and inherited method requirements for an eval interface. - pub fn interface_method_requirements( - &self, - interface_name: &str, - ) -> Vec { + pub fn interface_method_requirements(&self, interface_name: &str) -> Vec { let mut methods = Vec::new(); let mut seen_interfaces = HashSet::new(); let mut seen_methods = HashSet::new(); diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index c6ac853500..efe4d9be20 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -70,6 +70,7 @@ pub enum EvalStmt { }, ClassDecl(EvalClass), InterfaceDecl(EvalInterface), + TraitDecl(EvalTrait), Foreach { array: EvalExpr, key_name: Option, @@ -242,6 +243,7 @@ pub struct EvalClass { is_final: bool, parent: Option, interfaces: Vec, + traits: Vec, properties: Vec, methods: Vec, } @@ -276,6 +278,29 @@ impl EvalClass { interfaces: Vec, properties: Vec, methods: Vec, + ) -> Self { + Self::with_modifiers_and_traits( + name, + is_abstract, + is_final, + parent, + interfaces, + Vec::new(), + properties, + methods, + ) + } + + /// Creates a dynamic eval class with optional modifiers, relations, and trait uses. + pub fn with_modifiers_and_traits( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + properties: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), @@ -283,6 +308,7 @@ impl EvalClass { is_final, parent, interfaces, + traits, properties, methods, } @@ -313,6 +339,11 @@ impl EvalClass { &self.interfaces } + /// Returns trait names used directly by this eval class. + pub fn traits(&self) -> &[String] { + &self.traits + } + /// Returns public properties declared directly by this eval class. pub fn properties(&self) -> &[EvalClassProperty] { &self.properties @@ -331,6 +362,44 @@ impl EvalClass { } } +/// Runtime trait declared by an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalTrait { + name: String, + properties: Vec, + methods: Vec, +} + +impl EvalTrait { + /// Creates a dynamic eval trait with public properties and methods. + pub fn new( + name: impl Into, + properties: Vec, + methods: Vec, + ) -> Self { + Self { + name: name.into(), + properties, + methods, + } + } + + /// Returns the original source spelling of this eval-declared trait name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns public properties declared directly by this eval trait. + pub fn properties(&self) -> &[EvalClassProperty] { + &self.properties + } + + /// Returns public methods declared directly by this eval trait. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } +} + /// Public property metadata for a runtime eval class. #[derive(Debug, Clone, PartialEq)] pub struct EvalClassProperty { diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index a024e80a04..116dcb28d0 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -6,7 +6,7 @@ //! - Dynamic callable dispatch under `builtins::registry::dispatch`. //! //! Key details: -//! - Eval-declared classes carry parent and interface metadata; trait and +//! - Eval-declared classes carry parent, interface, and direct trait-use metadata; //! attribute metadata remains empty. //! - Missing class-like relation targets return `false`, matching the main //! backend's unknown-target fallback. @@ -68,7 +68,9 @@ pub(in crate::interpreter) fn eval_class_relation_target_result( "class_parents" => { eval_class_relation_names_result(context.class_parent_names(&target), values) } - "class_uses" => values.assoc_new(0), + "class_uses" => { + eval_class_relation_names_result(context.class_trait_names(&target), values) + } _ => Err(EvalStatus::RuntimeFatal), }; } @@ -148,6 +150,7 @@ fn eval_class_relation_name_exists( ) -> Result { if context.has_class(name) || context.has_interface(name) + || context.has_trait(name) || values.class_exists(name)? || values.interface_exists(name)? || values.trait_exists(name)? diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index 3695416932..c732a76d37 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -62,7 +62,7 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( eval_class_relation_result(name, evaluated_args, context, values)? } "enum_exists" | "trait_exists" => { - eval_class_like_exists_result(name, evaluated_args, values)? + eval_class_like_exists_result(name, evaluated_args, context, values)? } "interface_exists" => eval_interface_exists_result(evaluated_args, context, values)?, "is_a" | "is_subclass_of" => { diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index c852222d36..2324801492 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -262,7 +262,7 @@ pub(in crate::interpreter) fn eval_get_declared_symbols_result( eval_dynamic_string_array_result(context.declared_interface_names(), values) } "get_declared_traits" => { - eval_dynamic_string_array_result(&[], values) + eval_dynamic_string_array_result(context.declared_trait_names(), values) } _ => Err(EvalStatus::RuntimeFatal), } @@ -346,7 +346,7 @@ pub(in crate::interpreter) fn eval_builtin_class_like_exists( } _ => return Err(EvalStatus::RuntimeFatal), }; - let exists = eval_class_like_exists_name(name, symbol, values)?; + let exists = eval_class_like_exists_name(name, symbol, context, values)?; values.bool_value(exists) } @@ -354,11 +354,12 @@ pub(in crate::interpreter) fn eval_builtin_class_like_exists( pub(in crate::interpreter) fn eval_class_like_exists_result( name: &str, evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let exists = match evaluated_args { - [symbol] => eval_class_like_exists_name(name, *symbol, values)?, - [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, values)?, + [symbol] => eval_class_like_exists_name(name, *symbol, context, values)?, + [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }; values.bool_value(exists) @@ -368,13 +369,14 @@ pub(in crate::interpreter) fn eval_class_like_exists_result( pub(in crate::interpreter) fn eval_class_like_exists_name( name: &str, symbol: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let symbol = values.string_bytes(symbol)?; let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; let symbol = symbol.trim_start_matches('\\'); match name { - "trait_exists" => values.trait_exists(symbol), + "trait_exists" => Ok(context.has_trait(symbol) || values.trait_exists(symbol)?), "enum_exists" => values.enum_exists(symbol), _ => Err(EvalStatus::UnsupportedConstruct), } diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index f97929c30f..5dfca7cc41 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -287,6 +287,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::Return(_) | EvalStmt::StoreVar { .. } | EvalStmt::Throw(_) + | EvalStmt::TraitDecl(_) | EvalStmt::UnsetVar { .. } => {} } } diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index f4ba59708e..bd9d05854c 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -30,9 +30,9 @@ mod statements; use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, EvalConst, - EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalMagicConst, EvalMatchArm, - EvalProgram, EvalStmt, EvalSwitchCase, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, + EvalClassProperty, EvalConst, EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, + EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, EvalUnaryOp, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index fc331c2665..6b9e52325b 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -114,6 +114,10 @@ pub(in crate::interpreter) fn execute_stmt( execute_interface_decl_stmt(interface, context, values)?; Ok(EvalControl::None) } + EvalStmt::TraitDecl(trait_decl) => { + execute_trait_decl_stmt(trait_decl, context, values)?; + Ok(EvalControl::None) + } EvalStmt::Foreach { array, key_name, @@ -350,11 +354,15 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( let name = class.name().trim_start_matches('\\'); if context.has_class(name) || context.has_interface(name) + || context.has_trait(name) || values.class_exists(name)? || values.interface_exists(name)? + || values.trait_exists(name)? { return Err(EvalStatus::RuntimeFatal); } + let class = expand_eval_class_traits(class, context)?; + let class = &class; validate_eval_class_modifiers(class, context)?; if let Some(parent) = class.parent() { let Some(parent_class) = context.class(parent) else { @@ -412,6 +420,131 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( } } +/// Registers an eval-declared trait in the dynamic trait table. +pub(in crate::interpreter) fn execute_trait_decl_stmt( + trait_decl: &EvalTrait, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = trait_decl.name().trim_start_matches('\\'); + if context.has_trait(name) + || context.has_class(name) + || context.has_interface(name) + || values.trait_exists(name)? + || values.class_exists(name)? + || values.interface_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + if context.define_trait(trait_decl.clone()) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Expands eval trait uses into the class metadata used by dynamic dispatch. +fn expand_eval_class_traits( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result { + if class.traits().is_empty() { + return Ok(class.clone()); + } + let class_method_names = class_method_name_set(class); + let class_property_names = class_property_name_set(class); + let mut trait_method_names = std::collections::HashSet::new(); + let mut trait_property_names = std::collections::HashSet::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for trait_name in class.traits() { + let Some(trait_decl) = context.trait_decl(trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_properties( + trait_decl, + &class_property_names, + &mut trait_property_names, + &mut properties, + )?; + append_eval_trait_methods( + trait_decl, + &class_method_names, + &mut trait_method_names, + &mut methods, + )?; + } + properties.extend(class.properties().iter().cloned()); + methods.extend(class.methods().iter().cloned()); + Ok(EvalClass::with_modifiers_and_traits( + class.name().to_string(), + class.is_abstract(), + class.is_final(), + class.parent().map(str::to_string), + class.interfaces().to_vec(), + class.traits().to_vec(), + properties, + methods, + )) +} + +/// Returns case-insensitive method names declared directly by a pending class. +fn class_method_name_set(class: &EvalClass) -> std::collections::HashSet { + class + .methods() + .iter() + .map(|method| method.name().to_ascii_lowercase()) + .collect() +} + +/// Returns property names declared directly by a pending class. +fn class_property_name_set(class: &EvalClass) -> std::collections::HashSet { + class + .properties() + .iter() + .map(|property| property.name().to_string()) + .collect() +} + +/// Appends trait properties unless the class provides a same-name property. +fn append_eval_trait_properties( + trait_decl: &EvalTrait, + class_property_names: &std::collections::HashSet, + trait_property_names: &mut std::collections::HashSet, + properties: &mut Vec, +) -> Result<(), EvalStatus> { + for property in trait_decl.properties() { + if class_property_names.contains(property.name()) { + continue; + } + if !trait_property_names.insert(property.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + properties.push(property.clone()); + } + Ok(()) +} + +/// Appends trait methods unless the class provides a same-name method. +fn append_eval_trait_methods( + trait_decl: &EvalTrait, + class_method_names: &std::collections::HashSet, + trait_method_names: &mut std::collections::HashSet, + methods: &mut Vec, +) -> Result<(), EvalStatus> { + for method in trait_decl.methods() { + let key = method.name().to_ascii_lowercase(); + if class_method_names.contains(&key) { + continue; + } + if !trait_method_names.insert(key) { + return Err(EvalStatus::RuntimeFatal); + } + methods.push(method.clone()); + } + Ok(()) +} + /// Validates abstract/final modifiers on an eval-declared class and its methods. fn validate_eval_class_modifiers( class: &EvalClass, diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 6523119265..222c6a1cef 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -514,6 +514,99 @@ class EvalFinalMethodChild extends EvalFinalMethodBase { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval-declared traits contribute methods, properties, and metadata. +#[test] +fn execute_program_constructs_class_using_eval_declared_trait() { + let program = parse_fragment( + br#"trait EvalReusableTrait { + public int $seed = 2; + public function add($n) { return $this->seed + $n; } +} +class EvalTraitBox { + use EvalReusableTrait; + public function read($n) { return $this->add($n) + 1; } +} +$box = new EvalTraitBox(); +echo $box->read(4); echo ":"; +echo trait_exists("EvalReusableTrait") ? "trait" : "bad"; echo ":"; +$traits = get_declared_traits(); +echo count($traits); echo ":"; echo $traits[0]; echo ":"; +$uses = class_uses($box); +echo count($uses); echo ":"; echo $uses["EvalReusableTrait"]; +return $box->seed;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "7:trait:1:EvalReusableTrait:1:EvalReusableTrait" + ); + assert_eq!(values.get(result), FakeValue::Int(2)); +} +/// Verifies eval trait abstract methods can be implemented by the using class. +#[test] +fn execute_program_constructs_class_satisfying_eval_trait_abstract_method() { + let program = parse_fragment( + br#"trait EvalTraitNeedsRead { + abstract public function read($n); + public function wrap($n) { return $this->read($n) + 1; } +} +class EvalTraitReader { + use EvalTraitNeedsRead; + public function read($n) { return $n + 4; } +} +$reader = new EvalTraitReader(); +return $reader->wrap(3);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(8)); +} +/// Verifies eval rejects a concrete class that leaves a trait abstract method open. +#[test] +fn execute_program_rejects_missing_eval_trait_abstract_method() { + let program = parse_fragment( + br#"trait EvalTraitAbstractMethod { + abstract public function read(); +} +class EvalTraitMissingRead { + use EvalTraitAbstractMethod; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("class missing trait abstract method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects classes using traits that are not eval-declared. +#[test] +fn execute_program_rejects_missing_eval_trait_use() { + let program = parse_fragment( + br#"class EvalTraitMissingUse { + use MissingEvalTraitUse; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing eval trait use should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} /// Verifies eval rejects classes missing methods required by eval interfaces. #[test] fn execute_program_rejects_missing_dynamic_interface_method() { diff --git a/crates/elephc-eval/src/parser/cursor.rs b/crates/elephc-eval/src/parser/cursor.rs index 215c0b7dc3..fc600bd0f4 100644 --- a/crates/elephc-eval/src/parser/cursor.rs +++ b/crates/elephc-eval/src/parser/cursor.rs @@ -126,9 +126,7 @@ pub(super) fn ident_eq(actual: &str, expected: &str) -> bool { /// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. pub(super) fn is_unsupported_statement_keyword(name: &str) -> bool { - ["enum", "trait"] - .iter() - .any(|keyword| ident_eq(name, keyword)) + ["enum"].iter().any(|keyword| ident_eq(name, keyword)) } /// Returns true for class member modifiers outside the current eval class subset. diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index f4d5d9c7cd..53c04c0b22 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -13,7 +13,7 @@ use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ EvalCatch, EvalClass, EvalClassMethod, EvalClassProperty, EvalExpr, EvalInterface, - EvalInterfaceMethod, EvalStmt, EvalSwitchCase, + EvalInterfaceMethod, EvalStmt, EvalSwitchCase, EvalTrait, }; use crate::lexer::TokenKind; @@ -67,6 +67,7 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), TokenKind::Ident(name) if ident_eq(name, "try") => self.parse_try_stmt(), + TokenKind::Ident(name) if ident_eq(name, "trait") => self.parse_trait_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "unset") => self.parse_unset_stmt(), TokenKind::Ident(name) if ident_eq(name, "use") && self.allow_use_imports => { self.parse_use_stmt() @@ -214,22 +215,26 @@ impl Parser { self.expect(TokenKind::LBrace)?; let mut properties = Vec::new(); let mut methods = Vec::new(); + let mut traits = Vec::new(); while !self.consume(TokenKind::RBrace) { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - self.parse_class_member(&mut properties, &mut methods)?; + self.parse_class_member(&mut properties, &mut methods, &mut traits)?; } self.consume_semicolon(); - Ok(vec![EvalStmt::ClassDecl(EvalClass::with_modifiers( - name, - is_abstract, - is_final, - parent, - interfaces, - properties, - methods, - ))]) + Ok(vec![EvalStmt::ClassDecl( + EvalClass::with_modifiers_and_traits( + name, + is_abstract, + is_final, + parent, + interfaces, + traits, + properties, + methods, + ), + )]) } /// Parses class-level `abstract` and `final` modifiers before `class`. @@ -289,6 +294,7 @@ impl Parser { &mut self, properties: &mut Vec, methods: &mut Vec, + traits: &mut Vec, ) -> Result<(), EvalParseError> { let (public, is_abstract, is_final) = self.parse_class_member_modifiers()?; @@ -296,6 +302,15 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } + if !public + && !is_abstract + && !is_final + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + self.parse_class_trait_use(traits)?; + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { methods.push(self.parse_class_method_decl(is_abstract, is_final)?); return Ok(()); @@ -308,6 +323,22 @@ impl Parser { Ok(()) } + /// Parses `use TraitName, OtherTrait;` inside an eval class body. + pub(super) fn parse_class_trait_use( + &mut self, + traits: &mut Vec, + ) -> Result<(), EvalParseError> { + self.advance(); + loop { + let trait_name = self.parse_qualified_name()?; + traits.push(self.resolve_class_name(trait_name)); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon() + } + /// Parses method modifiers supported by eval class declarations. pub(super) fn parse_class_member_modifiers( &mut self, @@ -394,6 +425,50 @@ impl Parser { Ok(EvalClassProperty::new(name, default)) } + /// Parses `trait Name { ... }` declarations into dynamic trait metadata. + pub(super) fn parse_trait_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + self.expect(TokenKind::LBrace)?; + let mut properties = Vec::new(); + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_trait_member(&mut properties, &mut methods)?; + } + self.consume_semicolon(); + Ok(vec![EvalStmt::TraitDecl(EvalTrait::new( + name, properties, methods, + ))]) + } + + /// Parses one property or method from an eval trait body. + pub(super) fn parse_trait_member( + &mut self, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + let (public, is_abstract, is_final) = self.parse_class_member_modifiers()?; + if is_abstract && is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + methods.push(self.parse_class_method_decl(is_abstract, is_final)?); + return Ok(()); + } + if !public || is_abstract || is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + properties.push(self.parse_class_property_decl()?); + Ok(()) + } + /// Parses `interface Name [extends Parent, ...] { function name(...); }`. pub(super) fn parse_interface_decl_stmt(&mut self) -> Result, EvalParseError> { self.advance(); diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index bb37c17b08..85b0c1c2cc 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -128,6 +128,54 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { ))] ); } +/// Verifies trait declarations and class trait uses lower into dynamic metadata. +#[test] +fn parse_fragment_accepts_trait_declaration_and_class_use() { + let program = parse_fragment( + br#"trait DynEvalTrait { + public int $seed = 2; + public function read($value) { return $this->seed + $value; } +} +class DynEvalUsesTrait { + use DynEvalTrait, \Root\SharedTrait; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalTrait", + vec![EvalClassProperty::new( + "seed", + Some(EvalExpr::Const(EvalConst::Int(2))) + )], + vec![EvalClassMethod::new( + "read", + vec!["value".to_string()], + vec![EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "seed".to_string(), + }), + right: Box::new(EvalExpr::LoadVar("value".to_string())), + }))] + )], + )), + EvalStmt::ClassDecl(EvalClass::with_modifiers_and_traits( + "DynEvalUsesTrait", + false, + false, + None, + Vec::new(), + vec!["DynEvalTrait".to_string(), "Root\\SharedTrait".to_string()], + Vec::new(), + Vec::new(), + )), + ] + ); +} /// Verifies malformed object construction reports an unexpected token. #[test] fn parse_fragment_rejects_new_without_class_name() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 106ead6b27..26306a8f1d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4667,6 +4667,59 @@ class EvalFinalChild extends EvalFinalBase {}'); ); } +/// Verifies eval-declared traits contribute methods, properties, and metadata through the bridge. +#[test] +fn test_eval_declared_trait_methods_properties_and_metadata() { + let out = compile_and_run_capture( + r#"seed + $n; } +} +class EvalDynamicTraitBox { + use EvalDynamicTrait; + public function read($n) { return $this->add($n) + 1; } +} +$box = new EvalDynamicTraitBox(); +echo $box->read(4) . ":"; +echo trait_exists("EvalDynamicTrait") ? "trait" : "bad"; echo ":"; +$traits = get_declared_traits(); +echo count($traits) . ":" . $traits[0] . ":"; +$uses = class_uses($box); +echo count($uses) . ":" . $uses["EvalDynamicTrait"] . ":"; +echo $box->seed;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "7:trait:1:EvalDynamicTrait:1:EvalDynamicTrait:2" + ); +} + +/// Verifies eval-declared trait abstract methods must be implemented by concrete classes. +#[test] +fn test_eval_declared_trait_abstract_method_requirement_fails() { + let err = compile_and_run_expect_failure( + r#" Date: Thu, 18 Jun 2026 16:37:10 +0200 Subject: [PATCH 0327/1208] Support eval member visibility --- crates/elephc-eval/src/context.rs | 72 +++++++- crates/elephc-eval/src/eval_ir.rs | 50 ++++++ .../src/interpreter/expressions.rs | 14 +- crates/elephc-eval/src/interpreter/mod.rs | 1 + .../elephc-eval/src/interpreter/statements.rs | 158 +++++++++++++++++- .../src/interpreter/tests/expressions.rs | 101 +++++++++++ crates/elephc-eval/src/parser/cursor.rs | 4 +- crates/elephc-eval/src/parser/statements.rs | 67 ++++++-- .../src/parser/tests/classes_errors.rs | 30 ++++ tests/codegen/eval.rs | 46 +++++ 10 files changed, 507 insertions(+), 36 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 209c3e8e62..442c82d06a 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -16,7 +16,8 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::{ - EvalClass, EvalClassMethod, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalTrait, + EvalClass, EvalClassMethod, EvalClassProperty, EvalFunction, EvalInterface, + EvalInterfaceMethod, EvalTrait, }; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; @@ -104,6 +105,7 @@ pub struct ElephcEvalContext { dynamic_objects: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, + class_stack: Vec, pending_throw: Option, spl_autoload_extensions: String, streams: EvalStreamResources, @@ -135,6 +137,7 @@ impl ElephcEvalContext { dynamic_objects: HashMap::new(), global_scope: None, function_stack: Vec::new(), + class_stack: Vec::new(), pending_throw: None, spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), @@ -167,6 +170,7 @@ impl ElephcEvalContext { dynamic_objects: HashMap::new(), global_scope: None, function_stack: Vec::new(), + class_stack: Vec::new(), pending_throw: None, spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), @@ -383,6 +387,57 @@ impl ElephcEvalContext { } } + /// Finds a method declared directly by one eval-declared class. + pub fn class_own_method( + &self, + class_name: &str, + method_name: &str, + ) -> Option<(String, EvalClassMethod)> { + let class = self.class(class_name)?; + class + .method(method_name) + .map(|method| (class.name().to_string(), method.clone())) + } + + /// Finds a property in an eval-declared class or its eval-declared parents. + pub fn class_property( + &self, + class_name: &str, + property_name: &str, + ) -> Option<(String, EvalClassProperty)> { + let mut current_name = self.resolve_class_name(class_name)?; + let mut seen = HashSet::new(); + loop { + let key = normalize_class_name(¤t_name); + if !seen.insert(key.clone()) { + return None; + } + let class = self.classes.get(&key)?; + if let Some(property) = class + .properties() + .iter() + .find(|property| property.name() == property_name) + { + return Some((class.name().to_string(), property.clone())); + } + current_name = class.parent()?.to_string(); + } + } + + /// Finds a property declared directly by one eval-declared class. + pub fn class_own_property( + &self, + class_name: &str, + property_name: &str, + ) -> Option<(String, EvalClassProperty)> { + let class = self.class(class_name)?; + class + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| (class.name().to_string(), property.clone())) + } + /// Returns direct and inherited parent class names for an eval-declared class. pub fn class_parent_names(&self, class_name: &str) -> Vec { let mut parents = Vec::new(); @@ -654,6 +709,21 @@ impl ElephcEvalContext { self.function_stack.last().map(String::as_str) } + /// Pushes the eval class whose method is currently executing. + pub fn push_class_scope(&mut self, name: impl Into) { + self.class_stack.push(name.into()); + } + + /// Pops the current eval class method scope. + pub fn pop_class_scope(&mut self) { + self.class_stack.pop(); + } + + /// Returns the current eval class scope, if execution is inside a method. + pub fn current_class_scope(&self) -> Option<&str> { + self.class_stack.last().map(String::as_str) + } + /// Records a Throwable cell that escaped from an eval-executed function call. pub fn set_pending_throw(&mut self, value: RuntimeCellHandle) { self.pending_throw = Some(value); diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index efe4d9be20..162b28d8c2 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -404,14 +404,25 @@ impl EvalTrait { #[derive(Debug, Clone, PartialEq)] pub struct EvalClassProperty { name: String, + visibility: EvalVisibility, default: Option, } impl EvalClassProperty { /// Creates a public eval class property with an optional initializer. pub fn new(name: impl Into, default: Option) -> Self { + Self::with_visibility(name, EvalVisibility::Public, default) + } + + /// Creates an eval class property with explicit PHP visibility. + pub fn with_visibility( + name: impl Into, + visibility: EvalVisibility, + default: Option, + ) -> Self { Self { name: name.into(), + visibility, default, } } @@ -421,16 +432,30 @@ impl EvalClassProperty { &self.name } + /// Returns the PHP visibility declared for this property. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + /// Returns the property initializer expression, when one was declared. pub fn default(&self) -> Option<&EvalExpr> { self.default.as_ref() } } +/// PHP visibility for eval-declared object members. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalVisibility { + Public, + Protected, + Private, +} + /// Public method metadata for a runtime eval class. #[derive(Debug, Clone, PartialEq)] pub struct EvalClassMethod { name: String, + visibility: EvalVisibility, is_abstract: bool, is_final: bool, params: Vec, @@ -450,9 +475,29 @@ impl EvalClassMethod { is_final: bool, params: Vec, body: Vec, + ) -> Self { + Self::with_visibility_and_modifiers( + name, + EvalVisibility::Public, + is_abstract, + is_final, + params, + body, + ) + } + + /// Creates an eval class method with explicit visibility and optional modifiers. + pub fn with_visibility_and_modifiers( + name: impl Into, + visibility: EvalVisibility, + is_abstract: bool, + is_final: bool, + params: Vec, + body: Vec, ) -> Self { Self { name: name.into(), + visibility, is_abstract, is_final, params, @@ -465,6 +510,11 @@ impl EvalClassMethod { &self.name } + /// Returns the PHP visibility declared for this method. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + /// Returns whether this eval-declared method was declared `abstract`. pub const fn is_abstract(&self) -> bool { self.is_abstract diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 69fd76208e..520ebeba45 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -95,7 +95,7 @@ pub(in crate::interpreter) fn eval_expr( } EvalExpr::PropertyGet { object, property } => { let object = eval_expr(object, context, scope, values)?; - values.property_get(object, property) + eval_property_get_result(object, property, context, values) } EvalExpr::Print(inner) => { let value = eval_expr(inner, context, scope, values)?; @@ -637,9 +637,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_stream_resolve_include_path(args, context, scope, values) } "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), - "stream_context_create" => { - eval_builtin_stream_context_create(args, context, scope, values) - } + "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), "stream_context_get_default" => { eval_builtin_stream_context_get_default(args, context, scope, values) } @@ -667,9 +665,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_filter_append" | "stream_filter_prepend" => { eval_builtin_stream_filter_attach(name, args, context, scope, values) } - "stream_filter_remove" => { - eval_builtin_stream_filter_remove(args, context, scope, values) - } + "stream_filter_remove" => eval_builtin_stream_filter_remove(args, context, scope, values), "stream_bucket_new" => eval_builtin_stream_bucket_new(args, context, scope, values), "stream_bucket_make_writeable" => { eval_builtin_stream_bucket_make_writeable(args, context, scope, values) @@ -690,9 +686,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_socket_enable_crypto" => { eval_builtin_stream_socket_enable_crypto(args, context, scope, values) } - "stream_socket_sendto" => { - eval_builtin_stream_socket_sendto(args, context, scope, values) - } + "stream_socket_sendto" => eval_builtin_stream_socket_sendto(args, context, scope, values), "stream_socket_recvfrom" => { eval_builtin_stream_socket_recvfrom(args, context, scope, values) } diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index bd9d05854c..c6d63c424c 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -33,6 +33,7 @@ use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, EvalClassProperty, EvalConst, EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, EvalUnaryOp, + EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 6b9e52325b..a17dcdbca9 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -179,7 +179,7 @@ pub(in crate::interpreter) fn execute_stmt( } => { let object = eval_expr(object, context, scope, values)?; let value = eval_expr(value, context, scope, values)?; - values.property_set(object, property, value)?; + eval_property_set_result(object, property, value, context, values)?; Ok(EvalControl::None) } EvalStmt::StoreVar { name, value } => { @@ -557,6 +557,9 @@ fn validate_eval_class_modifiers( if method.is_abstract() && method.is_final() { return Err(EvalStatus::RuntimeFatal); } + if method.is_abstract() && method.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } if method.is_abstract() && !class.is_abstract() { return Err(EvalStatus::RuntimeFatal); } @@ -577,6 +580,14 @@ fn validate_method_parent_override( let Some((_, parent_method)) = context.class_method(parent, method.name()) else { return Ok(()); }; + if parent_method.visibility() == EvalVisibility::Private { + return Ok(()); + } + if method_visibility_rank(method.visibility()) + < method_visibility_rank(parent_method.visibility()) + { + return Err(EvalStatus::RuntimeFatal); + } if parent_method.is_final() { return Err(EvalStatus::RuntimeFatal); } @@ -586,6 +597,15 @@ fn validate_method_parent_override( Ok(()) } +/// Returns a comparable rank where larger means less restrictive visibility. +fn method_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} + /// Validates that a concrete class has satisfied inherited abstract and interface requirements. fn validate_concrete_class_requirements( class: &EvalClass, @@ -706,16 +726,83 @@ fn class_has_interface_method( context: &ElephcEvalContext, ) -> bool { if let Some(method) = class.method(requirement.name()) { - return !method.is_abstract() && method.params().len() == requirement.params().len(); + return method.visibility() == EvalVisibility::Public + && !method.is_abstract() + && method.params().len() == requirement.params().len(); } class .parent() .and_then(|parent| context.class_method(parent, requirement.name())) .is_some_and(|(_, method)| { - !method.is_abstract() && method.params().len() == requirement.params().len() + method.visibility() == EvalVisibility::Public + && !method.is_abstract() + && method.params().len() == requirement.params().len() }) } +/// Reads one object property while enforcing eval-declared member visibility. +pub(in crate::interpreter) fn eval_property_get_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return values.property_get(object, property_name); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return values.property_get(object, property_name); + }; + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(class.name(), property_name, context) + { + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + } + values.property_get(object, property_name) +} + +/// Writes one object property while enforcing eval-declared member visibility. +pub(in crate::interpreter) fn eval_property_set_result( + object: RuntimeCellHandle, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return values.property_set(object, property_name, value); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return values.property_set(object, property_name, value); + }; + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(class.name(), property_name, context) + { + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + } + values.property_set(object, property_name, value) +} + +/// Resolves the property metadata visible from the current class scope, if any. +fn eval_dynamic_property_for_access( + object_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassProperty)> { + if let Some(current_class) = context.current_class_scope() { + if context.class_is_a(object_class_name, current_class, false) { + if let Some((declaring_class, property)) = + context.class_own_property(current_class, property_name) + { + if property.visibility() == EvalVisibility::Private { + return Some((declaring_class, property)); + } + } + } + } + context.class_property(object_class_name, property_name) +} + /// Creates a backing object for an eval-declared class and runs its constructor. pub(in crate::interpreter) fn eval_dynamic_class_new_object( class: &EvalClass, @@ -747,6 +834,7 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( if let Some((constructor_class, constructor)) = context.class_method(class.name(), "__construct") { + validate_eval_member_access(&constructor_class, constructor.visibility(), context)?; eval_dynamic_method_with_values( &constructor_class, &constructor, @@ -775,9 +863,9 @@ pub(in crate::interpreter) fn eval_method_call_result( let Some(class) = context.dynamic_object_class(identity) else { return values.method_call(object, method_name, evaluated_args); }; - let (class_name, method) = context - .class_method(class.name(), method_name) + let (class_name, method) = eval_dynamic_method_for_call(class.name(), method_name, context) .ok_or(EvalStatus::RuntimeFatal)?; + validate_eval_member_access(&class_name, method.visibility(), context)?; if method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } @@ -791,6 +879,64 @@ pub(in crate::interpreter) fn eval_method_call_result( ) } +/// Resolves the method metadata visible from the current class scope. +fn eval_dynamic_method_for_call( + object_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(current_class) = context.current_class_scope() { + if context.class_is_a(object_class_name, current_class, false) { + if let Some((declaring_class, method)) = + context.class_own_method(current_class, method_name) + { + if method.visibility() == EvalVisibility::Private { + return Some((declaring_class, method)); + } + } + } + } + context.class_method(object_class_name, method_name) +} + +/// Returns whether the current eval class scope can access one declared member. +fn validate_eval_member_access( + declaring_class: &str, + visibility: EvalVisibility, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if visibility == EvalVisibility::Public { + return Ok(()); + } + let Some(current_class) = context.current_class_scope() else { + return Err(EvalStatus::RuntimeFatal); + }; + match visibility { + EvalVisibility::Public => Ok(()), + EvalVisibility::Private => same_eval_class_name(current_class, declaring_class) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal), + EvalVisibility::Protected => { + eval_classes_are_related(current_class, declaring_class, context) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) + } + } +} + +/// Returns true when two PHP class names refer to the same eval class. +fn same_eval_class_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns true when two eval classes are in the same inheritance family. +fn eval_classes_are_related(left: &str, right: &str, context: &ElephcEvalContext) -> bool { + same_eval_class_name(left, right) + || context.class_is_a(left, right, false) + || context.class_is_a(right, left, false) +} + /// Executes one eval-declared class method with `$this` bound in method scope. pub(in crate::interpreter) fn eval_dynamic_method_with_values( class_name: &str, @@ -811,6 +957,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); let static_names = static_var_names(method.body()); context.push_function(qualified_method_name.clone()); + context.push_class_scope(class_name.to_string()); let result = execute_statements(method.body(), context, &mut method_scope, values); let persist_result = persist_static_locals( context, @@ -819,6 +966,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( &method_scope, values, ); + context.pop_class_scope(); context.pop_function(); persist_result?; match result? { diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 222c6a1cef..da8daa662a 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -607,6 +607,107 @@ fn execute_program_rejects_missing_eval_trait_use() { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval methods can access private properties and methods declared in their class. +#[test] +fn execute_program_allows_private_eval_members_inside_declaring_class() { + let program = parse_fragment( + br#"class EvalPrivateBox { + private int $secret = 4; + private function bump($n) { return $this->secret + $n; } + public function read($n) { return $this->bump($n); } +} +$box = new EvalPrivateBox(); +return $box->read(3);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies protected eval members are accessible across a class hierarchy. +#[test] +fn execute_program_allows_protected_eval_members_from_related_classes() { + let program = parse_fragment( + br#"class EvalProtectedBase { + protected int $base = 5; + protected function add($n) { return $this->base + $n; } +} +class EvalProtectedChild extends EvalProtectedBase { + public function read($n) { return $this->add($n); } +} +$box = new EvalProtectedChild(); +return $box->read(2);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies eval rejects private member access from global scope. +#[test] +fn execute_program_rejects_private_eval_member_access_from_global_scope() { + let program = parse_fragment( + br#"class EvalPrivateGlobalBox { + private int $secret = 4; + private function read() { return $this->secret; } +} +$box = new EvalPrivateGlobalBox(); +echo $box->secret;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("global private property access should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects calls to private methods from global scope. +#[test] +fn execute_program_rejects_private_eval_method_call_from_global_scope() { + let program = parse_fragment( + br#"class EvalPrivateMethodBox { + private function read() { return 4; } +} +$box = new EvalPrivateMethodBox(); +return $box->read();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("global private method call should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects overriding a public method with lower visibility. +#[test] +fn execute_program_rejects_method_override_with_reduced_visibility() { + let program = parse_fragment( + br#"class EvalVisibleBase { + public function read() { return 1; } +} +class EvalVisibleChild extends EvalVisibleBase { + protected function read() { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("reduced method visibility should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} /// Verifies eval rejects classes missing methods required by eval interfaces. #[test] fn execute_program_rejects_missing_dynamic_interface_method() { diff --git a/crates/elephc-eval/src/parser/cursor.rs b/crates/elephc-eval/src/parser/cursor.rs index fc600bd0f4..f9f415bc97 100644 --- a/crates/elephc-eval/src/parser/cursor.rs +++ b/crates/elephc-eval/src/parser/cursor.rs @@ -131,9 +131,7 @@ pub(super) fn is_unsupported_statement_keyword(name: &str) -> bool { /// Returns true for class member modifiers outside the current eval class subset. pub(super) fn is_unsupported_class_member_modifier(name: &str) -> bool { - ["private", "protected", "static", "abstract", "final"] - .iter() - .any(|modifier| ident_eq(name, modifier)) + ["static"].iter().any(|modifier| ident_eq(name, modifier)) } /// Returns true when an identifier is an include/require expression construct. diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 53c04c0b22..369a2fd884 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -13,7 +13,7 @@ use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ EvalCatch, EvalClass, EvalClassMethod, EvalClassProperty, EvalExpr, EvalInterface, - EvalInterfaceMethod, EvalStmt, EvalSwitchCase, EvalTrait, + EvalInterfaceMethod, EvalStmt, EvalSwitchCase, EvalTrait, EvalVisibility, }; use crate::lexer::TokenKind; @@ -296,13 +296,13 @@ impl Parser { methods: &mut Vec, traits: &mut Vec, ) -> Result<(), EvalParseError> { - let (public, is_abstract, is_final) = self.parse_class_member_modifiers()?; + let (visibility, is_abstract, is_final) = self.parse_class_member_modifiers()?; if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); } - if !public + if visibility.is_none() && !is_abstract && !is_final && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) @@ -312,14 +312,21 @@ impl Parser { } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - methods.push(self.parse_class_method_decl(is_abstract, is_final)?); + methods.push(self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_abstract, + is_final, + )?); return Ok(()); } - if !public || is_abstract || is_final { + let Some(visibility) = visibility else { + return Err(EvalParseError::UnsupportedConstruct); + }; + if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl()?); + properties.push(self.parse_class_property_decl(visibility)?); Ok(()) } @@ -342,17 +349,31 @@ impl Parser { /// Parses method modifiers supported by eval class declarations. pub(super) fn parse_class_member_modifiers( &mut self, - ) -> Result<(bool, bool, bool), EvalParseError> { - let mut public = false; + ) -> Result<(Option, bool, bool), EvalParseError> { + let mut visibility = None; let mut is_abstract = false; let mut is_final = false; loop { match self.current() { TokenKind::Ident(name) if ident_eq(name, "public") => { - if public { + if visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } - public = true; + visibility = Some(EvalVisibility::Public); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + visibility = Some(EvalVisibility::Protected); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + visibility = Some(EvalVisibility::Private); self.advance(); } TokenKind::Ident(name) if ident_eq(name, "abstract") => { @@ -372,7 +393,7 @@ impl Parser { TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { return Err(EvalParseError::UnsupportedConstruct); } - _ => return Ok((public, is_abstract, is_final)), + _ => return Ok((visibility, is_abstract, is_final)), } } } @@ -380,6 +401,7 @@ impl Parser { /// Parses `function name($param, ...) { ... }` or an abstract method signature. pub(super) fn parse_class_method_decl( &mut self, + visibility: EvalVisibility, is_abstract: bool, is_final: bool, ) -> Result { @@ -397,8 +419,9 @@ impl Parser { } else { self.parse_block()? }; - Ok(EvalClassMethod::with_modifiers( + Ok(EvalClassMethod::with_visibility_and_modifiers( name, + visibility, is_abstract, is_final, params, @@ -409,6 +432,7 @@ impl Parser { /// Parses one public property declaration with an optional initializer. pub(super) fn parse_class_property_decl( &mut self, + visibility: EvalVisibility, ) -> Result { self.skip_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { @@ -422,7 +446,9 @@ impl Parser { None }; self.expect_semicolon()?; - Ok(EvalClassProperty::new(name, default)) + Ok(EvalClassProperty::with_visibility( + name, visibility, default, + )) } /// Parses `trait Name { ... }` declarations into dynamic trait metadata. @@ -454,18 +480,25 @@ impl Parser { properties: &mut Vec, methods: &mut Vec, ) -> Result<(), EvalParseError> { - let (public, is_abstract, is_final) = self.parse_class_member_modifiers()?; + let (visibility, is_abstract, is_final) = self.parse_class_member_modifiers()?; if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - methods.push(self.parse_class_method_decl(is_abstract, is_final)?); + methods.push(self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_abstract, + is_final, + )?); return Ok(()); } - if !public || is_abstract || is_final { + let Some(visibility) = visibility else { + return Err(EvalParseError::UnsupportedConstruct); + }; + if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl()?); + properties.push(self.parse_class_property_decl(visibility)?); Ok(()) } diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 85b0c1c2cc..ca2faa6097 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -88,6 +88,36 @@ fn parse_fragment_accepts_public_class_members() { ))] ); } +/// Verifies private and protected class members lower with explicit visibility metadata. +#[test] +fn parse_fragment_accepts_private_and_protected_class_members() { + let program = parse_fragment( + b"class DynEvalVisibility { private int $secret = 3; protected function reveal() { return $this->secret; } }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalVisibility", + vec![EvalClassProperty::with_visibility( + "secret", + EvalVisibility::Private, + Some(EvalExpr::Const(EvalConst::Int(3))) + )], + vec![EvalClassMethod::with_visibility_and_modifiers( + "reveal", + EvalVisibility::Protected, + false, + false, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "secret".to_string(), + }))] + )] + ))] + ); +} /// Verifies abstract and final class modifiers lower into dynamic class metadata. #[test] fn parse_fragment_accepts_abstract_and_final_class_members() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 26306a8f1d..7e5729ded6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4720,6 +4720,52 @@ class EvalTraitMissingConcrete { ); } +/// Verifies eval-declared private/protected members are usable from valid class scopes. +#[test] +fn test_eval_declared_private_and_protected_members() { + let out = compile_and_run_capture( + r#"secret + $n; } + protected function add($n) { return $this->base + $n; } + public function readPrivate($n) { return $this->bump($n); } +} +class EvalVisibilityChild extends EvalVisibilityBase { + public function readProtected($n) { return $this->add($n); } +} +$box = new EvalVisibilityChild(); +echo $box->readPrivate(3) . ":"; +echo $box->readProtected(2);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:7"); +} + +/// Verifies eval rejects private member access from outside the declaring class. +#[test] +fn test_eval_declared_private_member_access_fails() { + let err = compile_and_run_expect_failure( + r#"secret;'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies duplicate eval-declared functions fail through the runtime bridge. #[test] fn test_eval_duplicate_declared_function_fails() { From 0d6fa552d707e8b75e34b5987898e2b600e9897d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 16:53:36 +0200 Subject: [PATCH 0328/1208] Support eval static members --- crates/elephc-eval/src/context.rs | 41 +++ crates/elephc-eval/src/eval_ir.rs | 40 +++ .../src/interpreter/dynamic_functions.rs | 1 + .../src/interpreter/expressions.rs | 12 + .../elephc-eval/src/interpreter/statements.rs | 233 +++++++++++++++++- .../elephc-eval/src/interpreter/tests/mod.rs | 1 + .../src/interpreter/tests/static_members.rs | 119 +++++++++ crates/elephc-eval/src/lexer/scan.rs | 7 +- crates/elephc-eval/src/lexer/token.rs | 1 + crates/elephc-eval/src/parser/cursor.rs | 3 +- crates/elephc-eval/src/parser/expressions.rs | 48 +++- crates/elephc-eval/src/parser/statements.rs | 123 +++++++-- .../src/parser/tests/classes_errors.rs | 1 + crates/elephc-eval/src/parser/tests/mod.rs | 1 + .../src/parser/tests/static_members.rs | 82 ++++++ tests/codegen/eval.rs | 58 +++++ 16 files changed, 747 insertions(+), 24 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/tests/static_members.rs create mode 100644 crates/elephc-eval/src/parser/tests/static_members.rs diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 442c82d06a..e6cf1e2f90 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -101,11 +101,13 @@ pub struct ElephcEvalContext { functions: HashMap, native_functions: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, + static_properties: HashMap<(String, String), RuntimeCellHandle>, included_files: HashSet, dynamic_objects: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, class_stack: Vec, + called_class_stack: Vec, pending_throw: Option, spl_autoload_extensions: String, streams: EvalStreamResources, @@ -133,11 +135,13 @@ impl ElephcEvalContext { functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), + static_properties: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), + called_class_stack: Vec::new(), pending_throw: None, spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), @@ -166,11 +170,13 @@ impl ElephcEvalContext { functions: HashMap::new(), native_functions: HashMap::new(), static_locals: HashMap::new(), + static_properties: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), + called_class_stack: Vec::new(), pending_throw: None, spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), @@ -668,6 +674,26 @@ impl ElephcEvalContext { previous.filter(|previous| *previous != cell) } + /// Returns a stored static property cell for an eval-declared class. + pub fn static_property(&self, class_name: &str, name: &str) -> Option { + self.static_properties + .get(&(normalize_class_name(class_name), name.to_string())) + .copied() + } + + /// Stores one eval static property cell and returns any replaced distinct cell. + pub fn set_static_property( + &mut self, + class_name: &str, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .static_properties + .insert((normalize_class_name(class_name), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + /// Returns true when an eval include key was already loaded by this context. pub fn has_included_file(&self, path: &str) -> bool { self.included_files.contains(path) @@ -724,6 +750,21 @@ impl ElephcEvalContext { self.class_stack.last().map(String::as_str) } + /// Pushes the class name used to dispatch the current eval method call. + pub fn push_called_class_scope(&mut self, name: impl Into) { + self.called_class_stack.push(name.into()); + } + + /// Pops the current late-static-bound eval class scope. + pub fn pop_called_class_scope(&mut self) { + self.called_class_stack.pop(); + } + + /// Returns the current late-static-bound eval class scope, if execution is inside a method. + pub fn current_called_class_scope(&self) -> Option<&str> { + self.called_class_stack.last().map(String::as_str) + } + /// Records a Throwable cell that escaped from an eval-executed function call. pub fn set_pending_throw(&mut self, value: RuntimeCellHandle) { self.pending_throw = Some(value); diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 162b28d8c2..5ab4fb1ff7 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -100,6 +100,11 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + StaticPropertySet { + class_name: String, + property: String, + value: EvalExpr, + }, StaticVar { name: String, init: EvalExpr, @@ -405,6 +410,7 @@ impl EvalTrait { pub struct EvalClassProperty { name: String, visibility: EvalVisibility, + is_static: bool, default: Option, } @@ -419,10 +425,21 @@ impl EvalClassProperty { name: impl Into, visibility: EvalVisibility, default: Option, + ) -> Self { + Self::with_visibility_and_static(name, visibility, false, default) + } + + /// Creates an eval class property with explicit PHP visibility and static metadata. + pub fn with_visibility_and_static( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + default: Option, ) -> Self { Self { name: name.into(), visibility, + is_static, default, } } @@ -437,6 +454,11 @@ impl EvalClassProperty { self.visibility } + /// Returns whether this property was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + /// Returns the property initializer expression, when one was declared. pub fn default(&self) -> Option<&EvalExpr> { self.default.as_ref() @@ -456,6 +478,7 @@ pub enum EvalVisibility { pub struct EvalClassMethod { name: String, visibility: EvalVisibility, + is_static: bool, is_abstract: bool, is_final: bool, params: Vec, @@ -479,6 +502,7 @@ impl EvalClassMethod { Self::with_visibility_and_modifiers( name, EvalVisibility::Public, + false, is_abstract, is_final, params, @@ -490,6 +514,7 @@ impl EvalClassMethod { pub fn with_visibility_and_modifiers( name: impl Into, visibility: EvalVisibility, + is_static: bool, is_abstract: bool, is_final: bool, params: Vec, @@ -498,6 +523,7 @@ impl EvalClassMethod { Self { name: name.into(), visibility, + is_static, is_abstract, is_final, params, @@ -515,6 +541,11 @@ impl EvalClassMethod { self.visibility } + /// Returns whether this method was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + /// Returns whether this eval-declared method was declared `abstract`. pub const fn is_abstract(&self) -> bool { self.is_abstract @@ -584,6 +615,15 @@ pub enum EvalExpr { class_name: String, args: Vec, }, + StaticMethodCall { + class_name: String, + method: String, + args: Vec, + }, + StaticPropertyGet { + class_name: String, + property: String, + }, NullCoalesce { value: Box, default: Box, diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 5dfca7cc41..33f7f759fb 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -285,6 +285,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::PropertySet { .. } | EvalStmt::ReferenceAssign { .. } | EvalStmt::Return(_) + | EvalStmt::StaticPropertySet { .. } | EvalStmt::StoreVar { .. } | EvalStmt::Throw(_) | EvalStmt::TraitDecl(_) diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 520ebeba45..ba2c48554e 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -76,6 +76,18 @@ pub(in crate::interpreter) fn eval_expr( .and_then(|object| values.construct_object(object, args).map(|()| object)) } } + EvalExpr::StaticMethodCall { + class_name, + method, + args, + } => { + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_static_method_call_result(class_name, method, evaluated_args, context, values) + } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => eval_static_property_get_result(class_name, property, context, values), EvalExpr::MethodCall { object, method, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index a17dcdbca9..005f6d0c1f 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -107,7 +107,7 @@ pub(in crate::interpreter) fn execute_stmt( values, ), EvalStmt::ClassDecl(class) => { - execute_class_decl_stmt(class, context, values)?; + execute_class_decl_stmt(class, context, scope, values)?; Ok(EvalControl::None) } EvalStmt::InterfaceDecl(interface) => { @@ -182,6 +182,15 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_set_result(object, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::StaticPropertySet { + class_name, + property, + value, + } => { + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(class_name, property, value, context, values)?; + Ok(EvalControl::None) + } EvalStmt::StoreVar { name, value } => { let value = eval_expr(value, context, scope, values)?; for replaced in set_scope_cell( @@ -349,6 +358,7 @@ pub(in crate::interpreter) fn catch_types_match_thrown( pub(in crate::interpreter) fn execute_class_decl_stmt( class: &EvalClass, context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let name = class.name().trim_start_matches('\\'); @@ -381,12 +391,36 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( validate_concrete_class_requirements(class, context)?; } if context.define_class(class.clone()) { - Ok(()) + initialize_eval_static_properties(class, context, scope, values) } else { Err(EvalStatus::RuntimeFatal) } } +/// Initializes static property cells for a newly declared eval class. +fn initialize_eval_static_properties( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for property in class + .properties() + .iter() + .filter(|property| property.is_static()) + { + let value = if let Some(default) = property.default() { + eval_expr(default, context, scope, values)? + } else { + values.null()? + }; + if let Some(replaced) = context.set_static_property(class.name(), property.name(), value) { + values.release(replaced)?; + } + } + Ok(()) +} + /// Registers an eval-declared interface in the dynamic interface table. pub(in crate::interpreter) fn execute_interface_decl_stmt( interface: &EvalInterface, @@ -560,6 +594,9 @@ fn validate_eval_class_modifiers( if method.is_abstract() && method.visibility() == EvalVisibility::Private { return Err(EvalStatus::RuntimeFatal); } + if method.is_static() && method.name().eq_ignore_ascii_case("__construct") { + return Err(EvalStatus::RuntimeFatal); + } if method.is_abstract() && !class.is_abstract() { return Err(EvalStatus::RuntimeFatal); } @@ -583,6 +620,9 @@ fn validate_method_parent_override( if parent_method.visibility() == EvalVisibility::Private { return Ok(()); } + if parent_method.is_static() != method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } if method_visibility_rank(method.visibility()) < method_visibility_rank(parent_method.visibility()) { @@ -727,6 +767,7 @@ fn class_has_interface_method( ) -> bool { if let Some(method) = class.method(requirement.name()) { return method.visibility() == EvalVisibility::Public + && !method.is_static() && !method.is_abstract() && method.params().len() == requirement.params().len(); } @@ -735,6 +776,7 @@ fn class_has_interface_method( .and_then(|parent| context.class_method(parent, requirement.name())) .is_some_and(|(_, method)| { method.visibility() == EvalVisibility::Public + && !method.is_static() && !method.is_abstract() && method.params().len() == requirement.params().len() }) @@ -803,6 +845,130 @@ fn eval_dynamic_property_for_access( context.class_property(object_class_name, property_name) } +/// Reads one eval-declared static property after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_static_property_get_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + _values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_class_name(class_name, context)?; + let (declaring_class, property) = context + .class_property(&class_name, property_name) + .ok_or(EvalStatus::RuntimeFatal)?; + if !property.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + context + .static_property(&declaring_class, property.name()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Writes one eval-declared static property after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_static_property_set_result( + class_name: &str, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_class_name(class_name, context)?; + let (declaring_class, property) = context + .class_property(&class_name, property_name) + .ok_or(EvalStatus::RuntimeFatal)?; + if !property.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + if let Some(replaced) = context.set_static_property(&declaring_class, property.name(), value) { + values.release(replaced)?; + } + Ok(()) +} + +/// Dispatches a static method call to an eval-declared static method. +pub(in crate::interpreter) fn eval_static_method_call_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_class_name(class_name, context)?; + let (declaring_class, method) = + eval_dynamic_static_method_for_call(&class_name, method_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + if !method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, method.visibility(), context)?; + eval_dynamic_static_method_with_values( + &declaring_class, + &class_name, + &method, + evaluated_args, + context, + values, + ) +} + +/// Resolves a static method using private-method scope rules. +fn eval_dynamic_static_method_for_call( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(current_class) = context.current_class_scope() { + if eval_classes_are_related(current_class, class_name, context) { + if let Some((declaring_class, method)) = + context.class_own_method(current_class, method_name) + { + if method.visibility() == EvalVisibility::Private { + return Some((declaring_class, method)); + } + } + } + } + context.class_method(class_name, method_name) +} + +/// Resolves `self`, `parent`, and `static` for eval static member access. +fn resolve_eval_static_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" => context + .current_class_scope() + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "static" => context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "parent" => { + let current = context + .current_class_scope() + .ok_or(EvalStatus::RuntimeFatal)?; + context + .class(current) + .and_then(EvalClass::parent) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal) + } + _ => context + .resolve_class_name(class_name) + .or_else(|| { + context + .has_class(class_name) + .then(|| class_name.to_string()) + }) + .ok_or(EvalStatus::RuntimeFatal), + } +} + /// Creates a backing object for an eval-declared class and runs its constructor. pub(in crate::interpreter) fn eval_dynamic_class_new_object( class: &EvalClass, @@ -822,7 +988,11 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( class_chain.push(class.clone()); } for class in &class_chain { - for property in class.properties() { + for property in class + .properties() + .iter() + .filter(|property| !property.is_static()) + { let value = if let Some(default) = property.default() { eval_expr(default, context, caller_scope, values)? } else { @@ -837,6 +1007,7 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( validate_eval_member_access(&constructor_class, constructor.visibility(), context)?; eval_dynamic_method_with_values( &constructor_class, + class.name(), &constructor, object, evaluated_args, @@ -863,14 +1034,17 @@ pub(in crate::interpreter) fn eval_method_call_result( let Some(class) = context.dynamic_object_class(identity) else { return values.method_call(object, method_name, evaluated_args); }; - let (class_name, method) = eval_dynamic_method_for_call(class.name(), method_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; + let called_class_name = class.name().to_string(); + let (class_name, method) = + eval_dynamic_method_for_call(&called_class_name, method_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; validate_eval_member_access(&class_name, method.visibility(), context)?; - if method.is_abstract() { + if method.is_static() || method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } eval_dynamic_method_with_values( &class_name, + &called_class_name, &method, object, evaluated_args, @@ -940,6 +1114,7 @@ fn eval_classes_are_related(left: &str, right: &str, context: &ElephcEvalContext /// Executes one eval-declared class method with `$this` bound in method scope. pub(in crate::interpreter) fn eval_dynamic_method_with_values( class_name: &str, + called_class_name: &str, method: &EvalClassMethod, object: RuntimeCellHandle, evaluated_args: Vec, @@ -958,6 +1133,51 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( let static_names = static_var_names(method.body()); context.push_function(qualified_method_name.clone()); context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(called_class_name.to_string()); + let result = execute_statements(method.body(), context, &mut method_scope, values); + let persist_result = persist_static_locals( + context, + &qualified_method_name, + &static_names, + &method_scope, + values, + ); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + persist_result?; + match result? { + EvalControl::None => values.null(), + EvalControl::Return(result) => Ok(result), + EvalControl::Throw(result) => { + context.set_pending_throw(result); + Err(EvalStatus::UncaughtThrowable) + } + EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Executes one eval-declared static class method without binding `$this`. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = + bind_evaluated_function_args(method.params(), positional_args(evaluated_args))?; + let mut method_scope = ElephcEvalScope::new(); + for (name, value) in method.params().iter().zip(evaluated_args) { + method_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); + } + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(called_class_name.to_string()); let result = execute_statements(method.body(), context, &mut method_scope, values); let persist_result = persist_static_locals( context, @@ -966,6 +1186,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( &method_scope, values, ); + context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); persist_result?; diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 251a79ceb4..40919af38e 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -41,4 +41,5 @@ mod dynamic_calls; mod expressions; mod functions_namespaces; mod native_scope; +mod static_members; mod support; diff --git a/crates/elephc-eval/src/interpreter/tests/static_members.rs b/crates/elephc-eval/src/interpreter/tests/static_members.rs new file mode 100644 index 0000000000..c4538d4cf4 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/static_members.rs @@ -0,0 +1,119 @@ +//! Purpose: +//! Interpreter tests for eval-declared static properties and static methods. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover storage persistence, visibility checks, and late static binding. + +use super::super::*; +use super::support::*; + +/// Verifies static properties persist and can be read and written through static methods. +#[test] +fn execute_program_reads_writes_eval_static_members() { + let program = parse_fragment( + br#"class EvalStaticCounter { + public static int $count = 1; + public static function bump($step) { + self::$count += $step; + return self::$count; + } +} +echo EvalStaticCounter::$count; echo ":"; +echo EvalStaticCounter::bump(2); echo ":"; +return EvalStaticCounter::$count;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies `static::` uses the called class while `self::` keeps the declaring class. +#[test] +fn execute_program_late_binds_eval_static_property_access() { + let program = parse_fragment( + br#"class EvalStaticBase { + protected static int $n = 2; + public static function add($x) { + static::$n += $x; + return static::$n; + } + public static function baseRead() { + return self::$n; + } +} +class EvalStaticChild extends EvalStaticBase { + protected static int $n = 10; +} +echo EvalStaticChild::add(4); echo ":"; +echo EvalStaticBase::add(3); echo ":"; +return EvalStaticBase::baseRead();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "14:5:"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies private static properties are not readable from global eval scope. +#[test] +fn execute_program_rejects_private_eval_static_property_from_global_scope() { + let program = parse_fragment( + br#"class EvalStaticPrivate { + private static int $secret = 4; +} +return EvalStaticPrivate::$secret;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values) + .expect_err("global private static property access should fail"); +} + +/// Verifies eval rejects static-style calls to non-static methods. +#[test] +fn execute_program_rejects_static_call_to_eval_instance_method() { + let program = parse_fragment( + br#"class EvalStaticCallRules { + public function read() { return 1; } +} +return EvalStaticCallRules::read();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values) + .expect_err("static call to instance method should fail"); +} + +/// Verifies eval rejects object-style calls to static methods. +#[test] +fn execute_program_rejects_instance_call_to_eval_static_method() { + let program = parse_fragment( + br#"class EvalStaticInstanceRules { + public static function read() { return 1; } +} +$box = new EvalStaticInstanceRules(); +return $box->read();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values) + .expect_err("instance call to static method should fail"); +} diff --git a/crates/elephc-eval/src/lexer/scan.rs b/crates/elephc-eval/src/lexer/scan.rs index 605962408b..a5cca800ef 100644 --- a/crates/elephc-eval/src/lexer/scan.rs +++ b/crates/elephc-eval/src/lexer/scan.rs @@ -286,7 +286,12 @@ impl<'a> Lexer<'a> { } ':' => { self.bump_char(); - Ok(TokenKind::Colon) + if self.peek_char() == Some(':') { + self.bump_char(); + Ok(TokenKind::DoubleColon) + } else { + Ok(TokenKind::Colon) + } } '\\' => { self.bump_char(); diff --git a/crates/elephc-eval/src/lexer/token.rs b/crates/elephc-eval/src/lexer/token.rs index 0827d4b4c7..6c3c25ce81 100644 --- a/crates/elephc-eval/src/lexer/token.rs +++ b/crates/elephc-eval/src/lexer/token.rs @@ -75,6 +75,7 @@ pub(crate) enum TokenKind { RBrace, Comma, Colon, + DoubleColon, Backslash, Eof, } diff --git a/crates/elephc-eval/src/parser/cursor.rs b/crates/elephc-eval/src/parser/cursor.rs index f9f415bc97..a7364f287c 100644 --- a/crates/elephc-eval/src/parser/cursor.rs +++ b/crates/elephc-eval/src/parser/cursor.rs @@ -131,7 +131,8 @@ pub(super) fn is_unsupported_statement_keyword(name: &str) -> bool { /// Returns true for class member modifiers outside the current eval class subset. pub(super) fn is_unsupported_class_member_modifier(name: &str) -> bool { - ["static"].iter().any(|modifier| ident_eq(name, modifier)) + let _ = name; + false } /// Returns true when an identifier is an include/require expression construct. diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs index 8f5c568a99..55409f6545 100644 --- a/crates/elephc-eval/src/parser/expressions.rs +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -457,7 +457,9 @@ impl Parser { Err(EvalParseError::UnsupportedConstruct) } TokenKind::Backslash => self.parse_qualified_name_expr(), - TokenKind::Ident(_) if matches!(self.peek(), TokenKind::Backslash) => { + TokenKind::Ident(_) + if matches!(self.peek(), TokenKind::Backslash | TokenKind::DoubleColon) => + { self.parse_qualified_name_expr() } TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { @@ -567,6 +569,10 @@ impl Parser { /// Parses an explicitly qualified call or constant-fetch expression. pub(super) fn parse_qualified_name_expr(&mut self) -> Result { let name = self.parse_qualified_name()?; + if self.consume(TokenKind::DoubleColon) { + let class_name = self.resolve_static_class_name(name); + return self.parse_static_member_expr(class_name); + } let name = self.resolve_qualified_name(name); if matches!(self.current(), TokenKind::LParen) { let args = self.parse_call_args()?; @@ -578,6 +584,34 @@ impl Parser { Ok(EvalExpr::ConstFetch(name)) } + /// Parses `Class::$property` and `Class::method(...)` expressions. + pub(super) fn parse_static_member_expr( + &mut self, + class_name: String, + ) -> Result { + match self.current() { + TokenKind::DollarIdent(property) => { + let property = property.clone(); + self.advance(); + Ok(EvalExpr::StaticPropertyGet { + class_name, + property, + }) + } + TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { + let method = method.to_ascii_lowercase(); + self.advance(); + let args = self.parse_call_args()?; + Ok(EvalExpr::StaticMethodCall { + class_name, + method, + args, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + /// Parses `new ClassName(...)` expressions in eval fragments. pub(super) fn parse_new_object_expr(&mut self) -> Result { self.advance(); @@ -606,6 +640,18 @@ impl Parser { Ok(ParsedQualifiedName { name, absolute }) } + /// Resolves a class name used before `::`, preserving PHP relative class keywords. + pub(super) fn resolve_static_class_name(&self, name: ParsedQualifiedName) -> String { + if !name.absolute + && ["self", "parent", "static"] + .iter() + .any(|keyword| ident_eq(&name.name, keyword)) + { + return name.name.to_ascii_lowercase(); + } + self.resolve_class_name(name) + } + /// Builds a call expression, adding namespace fallback for unqualified names. pub(super) fn call_expr(&self, name: String, args: Vec) -> EvalExpr { if let Some(imported) = self.imports.resolve_function(&name) { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 369a2fd884..94b8ad113d 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -63,7 +63,16 @@ impl Parser { self.expect_semicolon()?; Ok(vec![EvalStmt::Return(Some(expr))]) } - TokenKind::Ident(name) if ident_eq(name, "static") => self.parse_static_var_stmt(), + TokenKind::Ident(name) + if ident_eq(name, "static") && self.current_starts_static_property_assignment() => + { + self.parse_static_property_set_stmt(true) + } + TokenKind::Ident(name) + if ident_eq(name, "static") && !matches!(self.peek(), TokenKind::DoubleColon) => + { + self.parse_static_var_stmt() + } TokenKind::Ident(name) if ident_eq(name, "switch") => self.parse_switch_stmt(), TokenKind::Ident(name) if ident_eq(name, "throw") => self.parse_throw_stmt(), TokenKind::Ident(name) if ident_eq(name, "try") => self.parse_try_stmt(), @@ -79,6 +88,11 @@ impl Parser { TokenKind::Ident(name) if is_unsupported_statement_keyword(name) => { Err(EvalParseError::UnsupportedConstruct) } + TokenKind::Ident(_) | TokenKind::Backslash + if self.current_starts_static_property_assignment() => + { + self.parse_static_property_set_stmt(true) + } TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(true) @@ -104,6 +118,36 @@ impl Parser { } } + /// Returns true when the current tokens form `Class::$property `. + pub(super) fn current_starts_static_property_assignment(&self) -> bool { + let mut pos = self.pos; + if matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { + pos += 1; + } + if !matches!(self.tokens.get(pos), Some(TokenKind::Ident(_))) { + return false; + } + pos += 1; + while matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { + pos += 1; + if !matches!(self.tokens.get(pos), Some(TokenKind::Ident(_))) { + return false; + } + pos += 1; + } + if !matches!(self.tokens.get(pos), Some(TokenKind::DoubleColon)) { + return false; + } + pos += 1; + let Some(TokenKind::DollarIdent(_)) = self.tokens.get(pos) else { + return false; + }; + pos += 1; + self.tokens + .get(pos) + .is_some_and(|token| assignment_op(token).is_some()) + } + /// Parses `do { ... } while (expr);`. pub(super) fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -296,13 +340,14 @@ impl Parser { methods: &mut Vec, traits: &mut Vec, ) -> Result<(), EvalParseError> { - let (visibility, is_abstract, is_final) = self.parse_class_member_modifiers()?; + let (visibility, is_static, is_abstract, is_final) = self.parse_class_member_modifiers()?; if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); } if visibility.is_none() + && !is_static && !is_abstract && !is_final && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) @@ -314,19 +359,18 @@ impl Parser { if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { methods.push(self.parse_class_method_decl( visibility.unwrap_or(EvalVisibility::Public), + is_static, is_abstract, is_final, )?); return Ok(()); } - let Some(visibility) = visibility else { - return Err(EvalParseError::UnsupportedConstruct); - }; + let visibility = visibility.unwrap_or(EvalVisibility::Public); if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl(visibility)?); + properties.push(self.parse_class_property_decl(visibility, is_static)?); Ok(()) } @@ -349,8 +393,9 @@ impl Parser { /// Parses method modifiers supported by eval class declarations. pub(super) fn parse_class_member_modifiers( &mut self, - ) -> Result<(Option, bool, bool), EvalParseError> { + ) -> Result<(Option, bool, bool, bool), EvalParseError> { let mut visibility = None; + let mut is_static = false; let mut is_abstract = false; let mut is_final = false; loop { @@ -376,6 +421,13 @@ impl Parser { visibility = Some(EvalVisibility::Private); self.advance(); } + TokenKind::Ident(name) if ident_eq(name, "static") => { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + is_static = true; + self.advance(); + } TokenKind::Ident(name) if ident_eq(name, "abstract") => { if is_abstract { return Err(EvalParseError::UnsupportedConstruct); @@ -393,7 +445,7 @@ impl Parser { TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { return Err(EvalParseError::UnsupportedConstruct); } - _ => return Ok((visibility, is_abstract, is_final)), + _ => return Ok((visibility, is_static, is_abstract, is_final)), } } } @@ -402,6 +454,7 @@ impl Parser { pub(super) fn parse_class_method_decl( &mut self, visibility: EvalVisibility, + is_static: bool, is_abstract: bool, is_final: bool, ) -> Result { @@ -422,6 +475,7 @@ impl Parser { Ok(EvalClassMethod::with_visibility_and_modifiers( name, visibility, + is_static, is_abstract, is_final, params, @@ -433,6 +487,7 @@ impl Parser { pub(super) fn parse_class_property_decl( &mut self, visibility: EvalVisibility, + is_static: bool, ) -> Result { self.skip_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { @@ -446,8 +501,8 @@ impl Parser { None }; self.expect_semicolon()?; - Ok(EvalClassProperty::with_visibility( - name, visibility, default, + Ok(EvalClassProperty::with_visibility_and_static( + name, visibility, is_static, default, )) } @@ -480,25 +535,24 @@ impl Parser { properties: &mut Vec, methods: &mut Vec, ) -> Result<(), EvalParseError> { - let (visibility, is_abstract, is_final) = self.parse_class_member_modifiers()?; + let (visibility, is_static, is_abstract, is_final) = self.parse_class_member_modifiers()?; if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { methods.push(self.parse_class_method_decl( visibility.unwrap_or(EvalVisibility::Public), + is_static, is_abstract, is_final, )?); return Ok(()); } - let Some(visibility) = visibility else { - return Err(EvalParseError::UnsupportedConstruct); - }; + let visibility = visibility.unwrap_or(EvalVisibility::Public); if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl(visibility)?); + properties.push(self.parse_class_property_decl(visibility, is_static)?); Ok(()) } @@ -1002,6 +1056,45 @@ impl Parser { Ok(vec![EvalStmt::StoreVar { name, value }]) } + /// Parses `Class::$property = expr` and simple static-property compound assignments. + pub(super) fn parse_static_property_set_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_static_class_name(class_name); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::StaticPropertyGet { + class_name: class_name.clone(), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::StaticPropertySet { + class_name, + property, + value, + }]) + } + /// Parses prefix `++$name` and `--$name` as simple statement effects. pub(super) fn parse_prefix_inc_dec_stmt( &mut self, diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index ca2faa6097..e8ff9868f5 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -109,6 +109,7 @@ fn parse_fragment_accepts_private_and_protected_class_members() { EvalVisibility::Protected, false, false, + false, Vec::new(), vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { object: Box::new(EvalExpr::LoadVar("this".to_string())), diff --git a/crates/elephc-eval/src/parser/tests/mod.rs b/crates/elephc-eval/src/parser/tests/mod.rs index 0ffd39b8a8..2b032ac5cd 100644 --- a/crates/elephc-eval/src/parser/tests/mod.rs +++ b/crates/elephc-eval/src/parser/tests/mod.rs @@ -19,4 +19,5 @@ mod exceptions_control; mod magic_comments; mod namespaces; mod operators; +mod static_members; mod support; diff --git a/crates/elephc-eval/src/parser/tests/static_members.rs b/crates/elephc-eval/src/parser/tests/static_members.rs new file mode 100644 index 0000000000..46e184b3ff --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/static_members.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Parser tests for eval static property and static method syntax. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `::` receivers lower to EvalIR static-member nodes. + +use super::support::*; + +/// Verifies static class members lower with explicit static metadata. +#[test] +fn parse_fragment_accepts_static_class_members() { + let program = parse_fragment( + br#"class EvalStaticBox { + public static int $count = 1; + public static function read() { return self::$count; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "EvalStaticBox", + vec![EvalClassProperty::with_visibility_and_static( + "count", + EvalVisibility::Public, + true, + Some(EvalExpr::Const(EvalConst::Int(1))), + )], + vec![EvalClassMethod::with_visibility_and_modifiers( + "read", + EvalVisibility::Public, + true, + false, + false, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::StaticPropertyGet { + class_name: "self".to_string(), + property: "count".to_string(), + }))], + )], + ))] + ); +} + +/// Verifies static method calls lower to EvalIR call expressions. +#[test] +fn parse_fragment_accepts_static_method_call_expression() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { + class_name: "EvalStaticBox".to_string(), + method: "read".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + +/// Verifies static property compound assignments lower to one read-modify-write statement. +#[test] +fn parse_fragment_accepts_static_property_compound_assignment() { + let program = parse_fragment(br#"EvalStaticBox::$count += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StaticPropertySet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::StaticPropertyGet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }] + ); +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7e5729ded6..355bf0dd91 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4748,6 +4748,47 @@ echo $box->readProtected(2);'); assert_eq!(out.stdout, "7:7"); } +/// Verifies eval-declared static properties and static methods work through the bridge. +#[test] +fn test_eval_declared_static_members_and_late_static_binding() { + let out = compile_and_run_capture( + r#"secret;'); ); } +/// Verifies eval rejects private static member access from outside the declaring class. +#[test] +fn test_eval_declared_private_static_member_access_fails() { + let err = compile_and_run_expect_failure( + r#" Date: Thu, 18 Jun 2026 16:59:28 +0200 Subject: [PATCH 0329/1208] Support eval class constants --- crates/elephc-eval/src/context.rs | 58 ++++++++++++- crates/elephc-eval/src/eval_ir.rs | 86 +++++++++++++++++++ .../src/interpreter/expressions.rs | 4 + .../elephc-eval/src/interpreter/statements.rs | 48 +++++++++++ .../src/interpreter/tests/class_constants.rs | 81 +++++++++++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + crates/elephc-eval/src/parser/expressions.rs | 8 ++ crates/elephc-eval/src/parser/statements.rs | 37 +++++++- .../src/parser/tests/class_constants.rs | 86 +++++++++++++++++++ crates/elephc-eval/src/parser/tests/mod.rs | 1 + tests/codegen/eval.rs | 53 ++++++++++++ 11 files changed, 458 insertions(+), 5 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/tests/class_constants.rs create mode 100644 crates/elephc-eval/src/parser/tests/class_constants.rs diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index e6cf1e2f90..2188a1630c 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -16,7 +16,7 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::{ - EvalClass, EvalClassMethod, EvalClassProperty, EvalFunction, EvalInterface, + EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalTrait, }; use crate::scope::ElephcEvalScope; @@ -102,6 +102,7 @@ pub struct ElephcEvalContext { native_functions: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, static_properties: HashMap<(String, String), RuntimeCellHandle>, + class_constants: HashMap<(String, String), RuntimeCellHandle>, included_files: HashSet, dynamic_objects: HashMap, global_scope: Option<*mut ElephcEvalScope>, @@ -136,6 +137,7 @@ impl ElephcEvalContext { native_functions: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), + class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), global_scope: None, @@ -171,6 +173,7 @@ impl ElephcEvalContext { native_functions: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), + class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), global_scope: None, @@ -405,6 +408,39 @@ impl ElephcEvalContext { .map(|method| (class.name().to_string(), method.clone())) } + /// Finds a class constant in an eval-declared class or its eval-declared parents. + pub fn class_constant( + &self, + class_name: &str, + constant_name: &str, + ) -> Option<(String, EvalClassConstant)> { + let mut current_name = self.resolve_class_name(class_name)?; + let mut seen = HashSet::new(); + loop { + let key = normalize_class_name(¤t_name); + if !seen.insert(key.clone()) { + return None; + } + let class = self.classes.get(&key)?; + if let Some(constant) = class.constant(constant_name) { + return Some((class.name().to_string(), constant.clone())); + } + current_name = class.parent()?.to_string(); + } + } + + /// Finds a class constant declared directly by one eval-declared class. + pub fn class_own_constant( + &self, + class_name: &str, + constant_name: &str, + ) -> Option<(String, EvalClassConstant)> { + let class = self.class(class_name)?; + class + .constant(constant_name) + .map(|constant| (class.name().to_string(), constant.clone())) + } + /// Finds a property in an eval-declared class or its eval-declared parents. pub fn class_property( &self, @@ -694,6 +730,26 @@ impl ElephcEvalContext { previous.filter(|previous| *previous != cell) } + /// Returns a materialized eval class constant cell. + pub fn class_constant_cell(&self, class_name: &str, name: &str) -> Option { + self.class_constants + .get(&(normalize_class_name(class_name), name.to_string())) + .copied() + } + + /// Stores one eval class constant cell and returns any replaced distinct cell. + pub fn set_class_constant_cell( + &mut self, + class_name: &str, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .class_constants + .insert((normalize_class_name(class_name), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + /// Returns true when an eval include key was already loaded by this context. pub fn has_included_file(&self, path: &str) -> bool { self.included_files.contains(path) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 5ab4fb1ff7..9103812c32 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -249,6 +249,7 @@ pub struct EvalClass { parent: Option, interfaces: Vec, traits: Vec, + constants: Vec, properties: Vec, methods: Vec, } @@ -306,6 +307,31 @@ impl EvalClass { traits: Vec, properties: Vec, methods: Vec, + ) -> Self { + Self::with_modifiers_traits_and_constants( + name, + is_abstract, + is_final, + parent, + interfaces, + traits, + Vec::new(), + properties, + methods, + ) + } + + /// Creates a dynamic eval class with modifiers, relations, trait uses, constants, and members. + pub fn with_modifiers_traits_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + constants: Vec, + properties: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), @@ -314,6 +340,7 @@ impl EvalClass { parent, interfaces, traits, + constants, properties, methods, } @@ -349,6 +376,11 @@ impl EvalClass { &self.traits } + /// Returns class constants declared directly by this eval class. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + /// Returns public properties declared directly by this eval class. pub fn properties(&self) -> &[EvalClassProperty] { &self.properties @@ -365,6 +397,56 @@ impl EvalClass { .iter() .find(|method| method.name().eq_ignore_ascii_case(name)) } + + /// Returns a class constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } +} + +/// Constant metadata for a runtime eval class. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClassConstant { + name: String, + visibility: EvalVisibility, + value: EvalExpr, +} + +impl EvalClassConstant { + /// Creates a public eval class constant with a value expression. + pub fn new(name: impl Into, value: EvalExpr) -> Self { + Self::with_visibility(name, EvalVisibility::Public, value) + } + + /// Creates an eval class constant with explicit PHP visibility. + pub fn with_visibility( + name: impl Into, + visibility: EvalVisibility, + value: EvalExpr, + ) -> Self { + Self { + name: name.into(), + visibility, + value, + } + } + + /// Returns the PHP-visible class constant name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the PHP visibility declared for this constant. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + + /// Returns the constant initializer expression. + pub fn value(&self) -> &EvalExpr { + &self.value + } } /// Runtime trait declared by an eval fragment. @@ -624,6 +706,10 @@ pub enum EvalExpr { class_name: String, property: String, }, + ClassConstantFetch { + class_name: String, + constant: String, + }, NullCoalesce { value: Box, default: Box, diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index ba2c48554e..58d6937dda 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -88,6 +88,10 @@ pub(in crate::interpreter) fn eval_expr( class_name, property, } => eval_static_property_get_result(class_name, property, context, values), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => eval_class_constant_fetch_result(class_name, constant, context, values), EvalExpr::MethodCall { object, method, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 005f6d0c1f..d22a98ca69 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -391,12 +391,31 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( validate_concrete_class_requirements(class, context)?; } if context.define_class(class.clone()) { + initialize_eval_class_constants(class, context, scope, values)?; initialize_eval_static_properties(class, context, scope, values) } else { Err(EvalStatus::RuntimeFatal) } } +/// Initializes class constant cells for a newly declared eval class. +fn initialize_eval_class_constants( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for constant in class.constants() { + let value = eval_expr(constant.value(), context, scope, values)?; + if let Some(replaced) = + context.set_class_constant_cell(class.name(), constant.name(), value) + { + values.release(replaced)?; + } + } + Ok(()) +} + /// Initializes static property cells for a newly declared eval class. fn initialize_eval_static_properties( class: &EvalClass, @@ -587,6 +606,7 @@ fn validate_eval_class_modifiers( if class.is_abstract() && class.is_final() { return Err(EvalStatus::RuntimeFatal); } + validate_eval_class_constants(class)?; for method in class.methods() { if method.is_abstract() && method.is_final() { return Err(EvalStatus::RuntimeFatal); @@ -605,6 +625,17 @@ fn validate_eval_class_modifiers( Ok(()) } +/// Validates constant declarations that can be checked before registration. +fn validate_eval_class_constants(class: &EvalClass) -> Result<(), EvalStatus> { + let mut names = std::collections::HashSet::new(); + for constant in class.constants() { + if !names.insert(constant.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + /// Validates one method declaration against inherited eval method metadata. fn validate_method_parent_override( class: &EvalClass, @@ -865,6 +896,23 @@ pub(in crate::interpreter) fn eval_static_property_get_result( .ok_or(EvalStatus::RuntimeFatal) } +/// Reads one eval-declared class constant after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_class_constant_fetch_result( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + _values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_class_name(class_name, context)?; + let (declaring_class, constant) = context + .class_constant(&class_name, constant_name) + .ok_or(EvalStatus::RuntimeFatal)?; + validate_eval_member_access(&declaring_class, constant.visibility(), context)?; + context + .class_constant_cell(&declaring_class, constant.name()) + .ok_or(EvalStatus::RuntimeFatal) +} + /// Writes one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_set_result( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/class_constants.rs b/crates/elephc-eval/src/interpreter/tests/class_constants.rs new file mode 100644 index 0000000000..0671d621d7 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/class_constants.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Interpreter tests for eval-declared class constants. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover inherited lookup, scoped receivers, visibility, and dynamic storage. + +use super::super::*; +use super::support::*; + +/// Verifies class constants can be fetched directly and through scoped receivers. +#[test] +fn execute_program_reads_eval_class_constants() { + let program = parse_fragment( + br#"class EvalConstBase { + public const SEED = 2; + protected const HIDDEN = 5; + public static function read() { + return self::SEED + static::SEED; + } + public static function hidden() { + return self::HIDDEN; + } +} +class EvalConstChild extends EvalConstBase { + public const SEED = 7; + public static function readParent() { + return parent::SEED; + } +} +echo EvalConstBase::SEED; echo ":"; +echo EvalConstChild::SEED; echo ":"; +echo EvalConstChild::read(); echo ":"; +echo EvalConstChild::readParent(); echo ":"; +return EvalConstChild::hidden();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:7:9:2:"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies protected class constants are not readable from global eval scope. +#[test] +fn execute_program_rejects_protected_eval_class_constant_from_global_scope() { + let program = parse_fragment( + br#"class EvalConstProtected { + protected const SECRET = 4; +} +return EvalConstProtected::SECRET;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values) + .expect_err("global protected class constant access should fail"); +} + +/// Verifies duplicate class constants in one eval class are rejected. +#[test] +fn execute_program_rejects_duplicate_eval_class_constant() { + let program = parse_fragment( + br#"class EvalConstDuplicate { + public const SEED = 1; + public const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values) + .expect_err("duplicate class constant should fail"); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 40919af38e..1149eba62c 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -35,6 +35,7 @@ mod builtins_strings_encoding; mod builtins_strings_text; mod builtins_symbols; mod builtins_system_network; +mod class_constants; mod control_flow; mod core; mod dynamic_calls; diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs index 55409f6545..a1165e738a 100644 --- a/crates/elephc-eval/src/parser/expressions.rs +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -608,6 +608,14 @@ impl Parser { args, }) } + TokenKind::Ident(constant) => { + let constant = constant.clone(); + self.advance(); + Ok(EvalExpr::ClassConstantFetch { + class_name, + constant, + }) + } _ => Err(EvalParseError::UnsupportedConstruct), } } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 94b8ad113d..e14622f81c 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -12,8 +12,8 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalCatch, EvalClass, EvalClassMethod, EvalClassProperty, EvalExpr, EvalInterface, - EvalInterfaceMethod, EvalStmt, EvalSwitchCase, EvalTrait, EvalVisibility, + EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalExpr, + EvalInterface, EvalInterfaceMethod, EvalStmt, EvalSwitchCase, EvalTrait, EvalVisibility, }; use crate::lexer::TokenKind; @@ -257,6 +257,7 @@ impl Parser { let parent = self.parse_class_parent_clause()?; let interfaces = self.parse_class_interface_clause()?; self.expect(TokenKind::LBrace)?; + let mut constants = Vec::new(); let mut properties = Vec::new(); let mut methods = Vec::new(); let mut traits = Vec::new(); @@ -264,17 +265,18 @@ impl Parser { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - self.parse_class_member(&mut properties, &mut methods, &mut traits)?; + self.parse_class_member(&mut constants, &mut properties, &mut methods, &mut traits)?; } self.consume_semicolon(); Ok(vec![EvalStmt::ClassDecl( - EvalClass::with_modifiers_and_traits( + EvalClass::with_modifiers_traits_and_constants( name, is_abstract, is_final, parent, interfaces, traits, + constants, properties, methods, ), @@ -336,6 +338,7 @@ impl Parser { /// Parses one public property or method from an eval class body. pub(super) fn parse_class_member( &mut self, + constants: &mut Vec, properties: &mut Vec, methods: &mut Vec, traits: &mut Vec, @@ -356,6 +359,15 @@ impl Parser { return Ok(()); } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || is_abstract || is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + constants + .push(self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))?); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { methods.push(self.parse_class_method_decl( visibility.unwrap_or(EvalVisibility::Public), @@ -374,6 +386,23 @@ impl Parser { Ok(()) } + /// Parses one eval class constant declaration. + pub(super) fn parse_class_const_decl( + &mut self, + visibility: EvalVisibility, + ) -> Result { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + Ok(EvalClassConstant::with_visibility(name, visibility, value)) + } + /// Parses `use TraitName, OtherTrait;` inside an eval class body. pub(super) fn parse_class_trait_use( &mut self, diff --git a/crates/elephc-eval/src/parser/tests/class_constants.rs b/crates/elephc-eval/src/parser/tests/class_constants.rs new file mode 100644 index 0000000000..2e03b4152f --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/class_constants.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Parser tests for eval class constant declarations and fetches. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `const` members and `Class::CONST` lower to EvalIR. + +use super::support::*; + +/// Verifies class constants lower into eval class metadata. +#[test] +fn parse_fragment_accepts_class_constant_declarations() { + let program = parse_fragment( + br#"class EvalConstBox { + public const SEED = 2; + protected const LABEL = "box"; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::with_modifiers_traits_and_constants( + "EvalConstBox", + false, + false, + None, + Vec::new(), + Vec::new(), + vec![ + EvalClassConstant::new("SEED", EvalExpr::Const(EvalConst::Int(2))), + EvalClassConstant::with_visibility( + "LABEL", + EvalVisibility::Protected, + EvalExpr::Const(EvalConst::String("box".to_string())), + ), + ], + Vec::new(), + Vec::new(), + ) + )] + ); +} + +/// Verifies class constant fetches lower to EvalIR expressions. +#[test] +fn parse_fragment_accepts_class_constant_fetches() { + let program = parse_fragment(br#"return EvalConstBox::SEED;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ClassConstantFetch { + class_name: "EvalConstBox".to_string(), + constant: "SEED".to_string(), + }))] + ); +} + +/// Verifies scoped class constant fetches preserve the class-like receiver. +#[test] +fn parse_fragment_accepts_scoped_class_constant_fetches() { + let program = parse_fragment(br#"return self::SEED + parent::SEED + static::SEED;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::ClassConstantFetch { + class_name: "self".to_string(), + constant: "SEED".to_string(), + }), + right: Box::new(EvalExpr::ClassConstantFetch { + class_name: "parent".to_string(), + constant: "SEED".to_string(), + }), + }), + right: Box::new(EvalExpr::ClassConstantFetch { + class_name: "static".to_string(), + constant: "SEED".to_string(), + }), + }))] + ); +} diff --git a/crates/elephc-eval/src/parser/tests/mod.rs b/crates/elephc-eval/src/parser/tests/mod.rs index 2b032ac5cd..353b9eb8e3 100644 --- a/crates/elephc-eval/src/parser/tests/mod.rs +++ b/crates/elephc-eval/src/parser/tests/mod.rs @@ -13,6 +13,7 @@ mod arrays_objects; mod assignments; mod calls; +mod class_constants; mod classes_errors; mod control_statements; mod exceptions_control; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 355bf0dd91..712e716440 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4789,6 +4789,42 @@ echo EvalStaticBase::baseRead();'); assert_eq!(out.stdout, "1:3:3:14:5:5"); } +/// Verifies eval-declared class constants work through the bridge. +#[test] +fn test_eval_declared_class_constants_and_scoped_fetches() { + let out = compile_and_run_capture( + r#"secret;'); ); } +/// Verifies eval rejects protected class constant access from outside the declaring class. +#[test] +fn test_eval_declared_protected_class_constant_access_fails() { + let err = compile_and_run_expect_failure( + r#" Date: Thu, 18 Jun 2026 17:00:30 +0200 Subject: [PATCH 0330/1208] Update eval OOP documentation --- docs/php/eval.md | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index bf7b74b00f..d3d6e8d554 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with public properties, public methods, and `__construct()`. Duplicate eval class names are rejected. | +| Classes | Eval fragments can declare classes with properties, methods, `__construct()`, inheritance, visibility, abstract/final modifiers, simple trait uses, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -69,7 +69,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, and eval object property access through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime metadata. | @@ -125,17 +125,24 @@ containers to eval-declared functions. ## Classes and objects -Eval-declared classes support public properties, public methods, and -`__construct()`. Eval object construction can allocate eval-declared classes, -`stdClass`, and emitted AOT classes visible through runtime class metadata. -Missing class names during eval object construction fail with an eval runtime -fatal diagnostic. +Eval-declared classes support inheritance, public/protected/private properties +and methods, `__construct()`, abstract classes and methods, final classes and +methods, simple `use Trait;` composition, interface implementation checks, +static properties, static methods, and class constants. Member visibility is +checked at runtime for eval-declared objects and static/class-constant +accesses. `self::`, `parent::`, and late-bound `static::` work for supported +static members and class constants. + +Eval object construction can allocate eval-declared classes, `stdClass`, and +emitted AOT classes visible through runtime class metadata. Missing class names +during eval object construction fail with an eval runtime fatal diagnostic. AOT and eval-declared class-name probes are visible through `class_exists()`. Eval object relation probes through `is_a()` and `is_subclass_of()` use generated AOT class/interface metadata and eval-created object metadata. `interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated -AOT metadata. +AOT metadata. Eval-declared classes, interfaces, traits, and class aliases are +visible through the corresponding eval and post-barrier native metadata probes. Public declared property reads/writes through `$this->property` from native methods are bridged to eval. Public zero-, one-, or two-scalar-argument method @@ -258,11 +265,12 @@ static-method callable arrays are still outside eval fragments. Object method calls through eval support positional arguments and numeric array unpacking; named method arguments remain unsupported. -Eval class support is intentionally smaller than the full static class system: -public properties, public methods, constructors, `stdClass`, AOT object -construction, public property bridge access, and basic public method bridge -calls are supported, but broader class-system features require future EvalIR and -dynamic symbol-table expansion. +Eval class support is still smaller than the full static class system. The main +remaining class-system gaps are eval-declared enums, trait conflict adaptations +(`insteadof` / `as`), trait and interface constants, `ClassName::class` / +`self::class` / `static::class`, property hooks, attributes/reflection metadata, +readonly semantics, dynamic static callables, and advanced method-call forms +such as named method arguments. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` From e7ac64d54c81bbf5d29eb974fb6771bb0f1a5b9a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 17:09:30 +0200 Subject: [PATCH 0331/1208] Support eval class-like constants --- crates/elephc-eval/src/context.rs | 67 ++++++++- crates/elephc-eval/src/eval_ir.rs | 51 +++++++ .../src/interpreter/expressions.rs | 3 + crates/elephc-eval/src/interpreter/mod.rs | 6 +- .../elephc-eval/src/interpreter/statements.rs | 129 +++++++++++++++--- .../src/interpreter/tests/class_constants.rs | 86 ++++++++++++ crates/elephc-eval/src/parser/expressions.rs | 6 + crates/elephc-eval/src/parser/statements.rs | 46 ++++++- .../src/parser/tests/class_constants.rs | 67 +++++++++ tests/codegen/eval.rs | 67 +++++++++ 10 files changed, 500 insertions(+), 28 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 2188a1630c..596f7cdfc0 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -238,6 +238,21 @@ impl ElephcEvalContext { self.class_aliases.get(&key).cloned() } + /// Resolves a PHP class-like name to eval class, interface, trait, or alias spelling. + pub fn resolve_class_like_name(&self, name: &str) -> Option { + let key = normalize_class_name(name); + if let Some(class) = self.classes.get(&key) { + return Some(class.name().to_string()); + } + if let Some(interface) = self.interfaces.get(&key) { + return Some(interface.name().to_string()); + } + if let Some(trait_decl) = self.traits.get(&key) { + return Some(trait_decl.name().to_string()); + } + self.class_aliases.get(&key).cloned() + } + /// Defines an alias for an eval-declared class or an already known alias. pub fn define_class_alias(&mut self, original: &str, alias: &str) -> bool { let Some(target) = self.resolve_class_name(original) else { @@ -408,11 +423,31 @@ impl ElephcEvalContext { .map(|method| (class.name().to_string(), method.clone())) } - /// Finds a class constant in an eval-declared class or its eval-declared parents. + /// Finds a class-like constant on an eval class, interface, trait, or inherited relation. pub fn class_constant( &self, class_name: &str, constant_name: &str, + ) -> Option<(String, EvalClassConstant)> { + if self.has_class(class_name) { + return self.class_or_interface_constant(class_name, constant_name); + } + if self.has_interface(class_name) { + return self.interface_constant(class_name, constant_name); + } + if let Some(trait_decl) = self.trait_decl(class_name) { + if let Some(constant) = trait_decl.constant(constant_name) { + return Some((trait_decl.name().to_string(), constant.clone())); + } + } + None + } + + /// Finds a class constant in an eval-declared class, parents, or implemented interfaces. + fn class_or_interface_constant( + &self, + class_name: &str, + constant_name: &str, ) -> Option<(String, EvalClassConstant)> { let mut current_name = self.resolve_class_name(class_name)?; let mut seen = HashSet::new(); @@ -425,8 +460,36 @@ impl ElephcEvalContext { if let Some(constant) = class.constant(constant_name) { return Some((class.name().to_string(), constant.clone())); } - current_name = class.parent()?.to_string(); + if let Some(parent) = class.parent() { + current_name = parent.to_string(); + } else { + break; + } + } + for interface_name in self.class_interface_names(class_name) { + if let Some(found) = self.interface_constant(&interface_name, constant_name) { + return Some(found); + } + } + None + } + + /// Finds a constant declared on an eval interface or inherited parent interface. + pub fn interface_constant( + &self, + interface_name: &str, + constant_name: &str, + ) -> Option<(String, EvalClassConstant)> { + let interface = self.interface(interface_name)?; + if let Some(constant) = interface.constant(constant_name) { + return Some((interface.name().to_string(), constant.clone())); + } + for parent in interface.parents() { + if let Some(found) = self.interface_constant(parent, constant_name) { + return Some(found); + } } + None } /// Finds a class constant declared directly by one eval-declared class. diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 9103812c32..4ff2c958ca 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -180,6 +180,7 @@ impl EvalFunction { pub struct EvalInterface { name: String, parents: Vec, + constants: Vec, methods: Vec, } @@ -189,10 +190,21 @@ impl EvalInterface { name: impl Into, parents: Vec, methods: Vec, + ) -> Self { + Self::with_constants(name, parents, Vec::new(), methods) + } + + /// Creates a dynamic eval interface with optional parent interfaces, constants, and methods. + pub fn with_constants( + name: impl Into, + parents: Vec, + constants: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), parents, + constants, methods, } } @@ -207,6 +219,18 @@ impl EvalInterface { &self.parents } + /// Returns constants declared directly by this eval interface. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns one interface constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } + /// Returns method signatures declared directly by this eval interface. pub fn methods(&self) -> &[EvalInterfaceMethod] { &self.methods @@ -453,6 +477,7 @@ impl EvalClassConstant { #[derive(Debug, Clone, PartialEq)] pub struct EvalTrait { name: String, + constants: Vec, properties: Vec, methods: Vec, } @@ -463,9 +488,20 @@ impl EvalTrait { name: impl Into, properties: Vec, methods: Vec, + ) -> Self { + Self::with_constants(name, Vec::new(), properties, methods) + } + + /// Creates a dynamic eval trait with constants, properties, and methods. + pub fn with_constants( + name: impl Into, + constants: Vec, + properties: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), + constants, properties, methods, } @@ -476,6 +512,18 @@ impl EvalTrait { &self.name } + /// Returns constants declared directly by this eval trait. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns one trait constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } + /// Returns public properties declared directly by this eval trait. pub fn properties(&self) -> &[EvalClassProperty] { &self.properties @@ -710,6 +758,9 @@ pub enum EvalExpr { class_name: String, constant: String, }, + ClassNameFetch { + class_name: String, + }, NullCoalesce { value: Box, default: Box, diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 58d6937dda..200aef0934 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -92,6 +92,9 @@ pub(in crate::interpreter) fn eval_expr( class_name, constant, } => eval_class_constant_fetch_result(class_name, constant, context, values), + EvalExpr::ClassNameFetch { class_name } => { + eval_class_name_fetch_result(class_name, context, values) + } EvalExpr::MethodCall { object, method, diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index c6d63c424c..5b76175160 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -31,9 +31,9 @@ use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, - EvalClassProperty, EvalConst, EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, - EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, EvalUnaryOp, - EvalVisibility, + EvalClassConstant, EvalClassProperty, EvalConst, EvalExpr, EvalFunction, EvalInterface, + EvalInterfaceMethod, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, + EvalTrait, EvalUnaryOp, EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index d22a98ca69..9158aedc25 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -111,11 +111,11 @@ pub(in crate::interpreter) fn execute_stmt( Ok(EvalControl::None) } EvalStmt::InterfaceDecl(interface) => { - execute_interface_decl_stmt(interface, context, values)?; + execute_interface_decl_stmt(interface, context, scope, values)?; Ok(EvalControl::None) } EvalStmt::TraitDecl(trait_decl) => { - execute_trait_decl_stmt(trait_decl, context, values)?; + execute_trait_decl_stmt(trait_decl, context, scope, values)?; Ok(EvalControl::None) } EvalStmt::Foreach { @@ -391,24 +391,30 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( validate_concrete_class_requirements(class, context)?; } if context.define_class(class.clone()) { - initialize_eval_class_constants(class, context, scope, values)?; + initialize_eval_declared_constants( + class.name(), + class.constants(), + context, + scope, + values, + )?; initialize_eval_static_properties(class, context, scope, values) } else { Err(EvalStatus::RuntimeFatal) } } -/// Initializes class constant cells for a newly declared eval class. -fn initialize_eval_class_constants( - class: &EvalClass, +/// Initializes class-like constant cells for a newly declared eval class-like. +fn initialize_eval_declared_constants( + owner_name: &str, + constants: &[EvalClassConstant], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { - for constant in class.constants() { + for constant in constants { let value = eval_expr(constant.value(), context, scope, values)?; - if let Some(replaced) = - context.set_class_constant_cell(class.name(), constant.name(), value) + if let Some(replaced) = context.set_class_constant_cell(owner_name, constant.name(), value) { values.release(replaced)?; } @@ -444,6 +450,7 @@ fn initialize_eval_static_properties( pub(in crate::interpreter) fn execute_interface_decl_stmt( interface: &EvalInterface, context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let name = interface.name().trim_start_matches('\\'); @@ -466,8 +473,15 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( return Err(EvalStatus::RuntimeFatal); } } + validate_eval_declared_constants(interface.constants())?; if context.define_interface(interface.clone()) { - Ok(()) + initialize_eval_declared_constants( + interface.name(), + interface.constants(), + context, + scope, + values, + ) } else { Err(EvalStatus::RuntimeFatal) } @@ -477,6 +491,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( pub(in crate::interpreter) fn execute_trait_decl_stmt( trait_decl: &EvalTrait, context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let name = trait_decl.name().trim_start_matches('\\'); @@ -489,8 +504,15 @@ pub(in crate::interpreter) fn execute_trait_decl_stmt( { return Err(EvalStatus::RuntimeFatal); } + validate_eval_declared_constants(trait_decl.constants())?; if context.define_trait(trait_decl.clone()) { - Ok(()) + initialize_eval_declared_constants( + trait_decl.name(), + trait_decl.constants(), + context, + scope, + values, + ) } else { Err(EvalStatus::RuntimeFatal) } @@ -506,14 +528,23 @@ fn expand_eval_class_traits( } let class_method_names = class_method_name_set(class); let class_property_names = class_property_name_set(class); + let class_constant_names = class_constant_name_set(class); let mut trait_method_names = std::collections::HashSet::new(); let mut trait_property_names = std::collections::HashSet::new(); + let mut trait_constant_names = std::collections::HashSet::new(); + let mut constants = Vec::new(); let mut properties = Vec::new(); let mut methods = Vec::new(); for trait_name in class.traits() { let Some(trait_decl) = context.trait_decl(trait_name) else { return Err(EvalStatus::RuntimeFatal); }; + append_eval_trait_constants( + trait_decl, + &class_constant_names, + &mut trait_constant_names, + &mut constants, + )?; append_eval_trait_properties( trait_decl, &class_property_names, @@ -527,15 +558,17 @@ fn expand_eval_class_traits( &mut methods, )?; } + constants.extend(class.constants().iter().cloned()); properties.extend(class.properties().iter().cloned()); methods.extend(class.methods().iter().cloned()); - Ok(EvalClass::with_modifiers_and_traits( + Ok(EvalClass::with_modifiers_traits_and_constants( class.name().to_string(), class.is_abstract(), class.is_final(), class.parent().map(str::to_string), class.interfaces().to_vec(), class.traits().to_vec(), + constants, properties, methods, )) @@ -550,6 +583,15 @@ fn class_method_name_set(class: &EvalClass) -> std::collections::HashSet .collect() } +/// Returns constant names declared directly by a pending class. +fn class_constant_name_set(class: &EvalClass) -> std::collections::HashSet { + class + .constants() + .iter() + .map(|constant| constant.name().to_string()) + .collect() +} + /// Returns property names declared directly by a pending class. fn class_property_name_set(class: &EvalClass) -> std::collections::HashSet { class @@ -559,6 +601,25 @@ fn class_property_name_set(class: &EvalClass) -> std::collections::HashSet, + trait_constant_names: &mut std::collections::HashSet, + constants: &mut Vec, +) -> Result<(), EvalStatus> { + for constant in trait_decl.constants() { + if class_constant_names.contains(constant.name()) { + continue; + } + if !trait_constant_names.insert(constant.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + constants.push(constant.clone()); + } + Ok(()) +} + /// Appends trait properties unless the class provides a same-name property. fn append_eval_trait_properties( trait_decl: &EvalTrait, @@ -606,7 +667,7 @@ fn validate_eval_class_modifiers( if class.is_abstract() && class.is_final() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_class_constants(class)?; + validate_eval_declared_constants(class.constants())?; for method in class.methods() { if method.is_abstract() && method.is_final() { return Err(EvalStatus::RuntimeFatal); @@ -626,9 +687,9 @@ fn validate_eval_class_modifiers( } /// Validates constant declarations that can be checked before registration. -fn validate_eval_class_constants(class: &EvalClass) -> Result<(), EvalStatus> { +fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); - for constant in class.constants() { + for constant in constants { if !names.insert(constant.name().to_string()) { return Err(EvalStatus::RuntimeFatal); } @@ -903,7 +964,7 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( context: &mut ElephcEvalContext, _values: &mut impl RuntimeValueOps, ) -> Result { - let class_name = resolve_eval_static_class_name(class_name, context)?; + let class_name = resolve_eval_static_class_like_name(class_name, context)?; let (declaring_class, constant) = context .class_constant(&class_name, constant_name) .ok_or(EvalStatus::RuntimeFatal)?; @@ -913,6 +974,16 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( .ok_or(EvalStatus::RuntimeFatal) } +/// Returns the PHP class-name literal for `ClassName::class`-style eval expressions. +pub(in crate::interpreter) fn eval_class_name_fetch_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_class_name_literal(class_name, context)?; + values.string(&class_name) +} + /// Writes one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_set_result( class_name: &str, @@ -1017,6 +1088,32 @@ fn resolve_eval_static_class_name( } } +/// Resolves `self`, `parent`, `static`, and named class-like receivers for constant access. +fn resolve_eval_static_class_like_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => context + .resolve_class_like_name(class_name) + .ok_or(EvalStatus::RuntimeFatal), + } +} + +/// Resolves class-name literal receivers without requiring named classes to exist. +fn resolve_eval_class_name_literal( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + /// Creates a backing object for an eval-declared class and runs its constructor. pub(in crate::interpreter) fn eval_dynamic_class_new_object( class: &EvalClass, diff --git a/crates/elephc-eval/src/interpreter/tests/class_constants.rs b/crates/elephc-eval/src/interpreter/tests/class_constants.rs index 0671d621d7..5b73d363ed 100644 --- a/crates/elephc-eval/src/interpreter/tests/class_constants.rs +++ b/crates/elephc-eval/src/interpreter/tests/class_constants.rs @@ -79,3 +79,89 @@ fn execute_program_rejects_duplicate_eval_class_constant() { execute_program(&program, &mut scope, &mut values) .expect_err("duplicate class constant should fail"); } + +/// Verifies class-name literals resolve class-like receiver spelling. +#[test] +fn execute_program_reads_eval_class_name_literals() { + let program = parse_fragment( + br#"class EvalClassNameBase { + public static function selfName() { return self::class; } + public static function staticName() { return static::class; } +} +class EvalClassNameChild extends EvalClassNameBase {} +interface EvalClassNameIface {} +trait EvalClassNameTrait {} +echo EvalClassNameChild::class; echo ":"; +echo EvalClassNameIface::class; echo ":"; +echo EvalClassNameTrait::class; echo ":"; +echo EvalClassNameChild::selfName(); echo ":"; +return EvalClassNameChild::staticName();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalClassNameChild:EvalClassNameIface:EvalClassNameTrait:EvalClassNameBase:" + ); + assert_eq!( + values.get(result), + FakeValue::String("EvalClassNameChild".to_string()) + ); +} + +/// Verifies interface constants are readable directly, through inheritance, and through classes. +#[test] +fn execute_program_reads_eval_interface_constants() { + let program = parse_fragment( + br#"interface EvalConstParentIface { + public const BASE = 2; +} +interface EvalConstChildIface extends EvalConstParentIface { + public const LOCAL = 3; +} +class EvalConstIfaceImpl implements EvalConstChildIface {} +echo EvalConstParentIface::BASE; echo ":"; +echo EvalConstChildIface::BASE; echo ":"; +echo EvalConstChildIface::LOCAL; echo ":"; +return EvalConstIfaceImpl::BASE + EvalConstIfaceImpl::LOCAL;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:2:3:"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies trait constants are readable directly and from classes using the trait. +#[test] +fn execute_program_reads_eval_trait_constants() { + let program = parse_fragment( + br#"trait EvalConstReusableTrait { + public const SEED = 6; + public static function readTraitSeed() { + return self::SEED; + } +} +class EvalConstTraitBox { + use EvalConstReusableTrait; +} +echo EvalConstReusableTrait::SEED; echo ":"; +echo EvalConstTraitBox::SEED; echo ":"; +return EvalConstTraitBox::readTraitSeed();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "6:6:"); + assert_eq!(values.get(result), FakeValue::Int(6)); +} diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs index a1165e738a..890f5b52cf 100644 --- a/crates/elephc-eval/src/parser/expressions.rs +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -598,6 +598,12 @@ impl Parser { property, }) } + TokenKind::Ident(name) + if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => + { + self.advance(); + Ok(EvalExpr::ClassNameFetch { class_name }) + } TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { let method = method.to_ascii_lowercase(); self.advance(); diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index e14622f81c..624882c5ab 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -544,23 +544,25 @@ impl Parser { let name = self.qualify_name_in_current_namespace(name); self.advance(); self.expect(TokenKind::LBrace)?; + let mut constants = Vec::new(); let mut properties = Vec::new(); let mut methods = Vec::new(); while !self.consume(TokenKind::RBrace) { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - self.parse_trait_member(&mut properties, &mut methods)?; + self.parse_trait_member(&mut constants, &mut properties, &mut methods)?; } self.consume_semicolon(); - Ok(vec![EvalStmt::TraitDecl(EvalTrait::new( - name, properties, methods, + Ok(vec![EvalStmt::TraitDecl(EvalTrait::with_constants( + name, constants, properties, methods, ))]) } /// Parses one property or method from an eval trait body. pub(super) fn parse_trait_member( &mut self, + constants: &mut Vec, properties: &mut Vec, methods: &mut Vec, ) -> Result<(), EvalParseError> { @@ -568,6 +570,14 @@ impl Parser { if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || is_abstract || is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + constants + .push(self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))?); + return Ok(()); + } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { methods.push(self.parse_class_method_decl( visibility.unwrap_or(EvalVisibility::Public), @@ -595,17 +605,22 @@ impl Parser { self.advance(); let parents = self.parse_interface_parent_clause()?; self.expect(TokenKind::LBrace)?; + let mut constants = Vec::new(); let mut methods = Vec::new(); while !self.consume(TokenKind::RBrace) { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - methods.push(self.parse_interface_method_decl()?); + if let Some(constant) = self.parse_optional_interface_constant_decl()? { + constants.push(constant); + } else { + methods.push(self.parse_interface_method_decl()?); + } } self.consume_semicolon(); - Ok(vec![EvalStmt::InterfaceDecl(EvalInterface::new( - name, parents, methods, - ))]) + Ok(vec![EvalStmt::InterfaceDecl( + EvalInterface::with_constants(name, parents, constants, methods), + )]) } /// Parses an optional `extends Parent, ...` interface declaration clause. @@ -625,6 +640,23 @@ impl Parser { Ok(parents) } + /// Parses an interface constant declaration when the current member starts with `const`. + pub(super) fn parse_optional_interface_constant_decl( + &mut self, + ) -> Result, EvalParseError> { + let visibility = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) + { + self.advance(); + EvalVisibility::Public + } else { + EvalVisibility::Public + }; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + return Ok(None); + } + Ok(Some(self.parse_class_const_decl(visibility)?)) + } + /// Parses one eval interface method signature. pub(super) fn parse_interface_method_decl( &mut self, diff --git a/crates/elephc-eval/src/parser/tests/class_constants.rs b/crates/elephc-eval/src/parser/tests/class_constants.rs index 2e03b4152f..0f5186d381 100644 --- a/crates/elephc-eval/src/parser/tests/class_constants.rs +++ b/crates/elephc-eval/src/parser/tests/class_constants.rs @@ -84,3 +84,70 @@ fn parse_fragment_accepts_scoped_class_constant_fetches() { }))] ); } + +/// Verifies `ClassName::class` lowers to a class-name fetch rather than a user constant. +#[test] +fn parse_fragment_accepts_class_name_fetches() { + let program = parse_fragment(br#"return EvalConstBox::class;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ClassNameFetch { + class_name: "EvalConstBox".to_string(), + }))] + ); +} + +/// Verifies interface constants lower into eval interface metadata. +#[test] +fn parse_fragment_accepts_interface_constant_declarations() { + let program = parse_fragment( + br#"interface EvalConstIface { + public const SEED = 4; + function read(); +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl(EvalInterface::with_constants( + "EvalConstIface", + Vec::new(), + vec![EvalClassConstant::new( + "SEED", + EvalExpr::Const(EvalConst::Int(4)), + )], + vec![EvalInterfaceMethod::new("read", Vec::new())], + ))] + ); +} + +/// Verifies trait constants lower into eval trait metadata. +#[test] +fn parse_fragment_accepts_trait_constant_declarations() { + let program = parse_fragment( + br#"trait EvalConstTrait { + public const SEED = 6; + public function read() { return self::SEED; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::TraitDecl(EvalTrait::with_constants( + "EvalConstTrait", + vec![EvalClassConstant::new( + "SEED", + EvalExpr::Const(EvalConst::Int(6)), + )], + Vec::new(), + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::ClassConstantFetch { + class_name: "self".to_string(), + constant: "SEED".to_string(), + }))], + )], + ))] + ); +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 712e716440..f47e2f0641 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4825,6 +4825,73 @@ echo EvalConstChild::hidden();'); assert_eq!(out.stdout, "2:7:9:2:5"); } +/// Verifies eval class-name literals work for class-like receivers. +#[test] +fn test_eval_declared_class_name_literals() { + let out = compile_and_run_capture( + r#" Date: Thu, 18 Jun 2026 17:10:21 +0200 Subject: [PATCH 0332/1208] Update eval class-like constants docs --- docs/php/eval.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index d3d6e8d554..4cd152e920 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -128,10 +128,11 @@ containers to eval-declared functions. Eval-declared classes support inheritance, public/protected/private properties and methods, `__construct()`, abstract classes and methods, final classes and methods, simple `use Trait;` composition, interface implementation checks, -static properties, static methods, and class constants. Member visibility is -checked at runtime for eval-declared objects and static/class-constant -accesses. `self::`, `parent::`, and late-bound `static::` work for supported -static members and class constants. +static properties, static methods, class constants, interface constants, trait +constants, and `ClassName::class` literals. Member visibility is checked at +runtime for eval-declared objects and static/class-constant accesses. `self::`, +`parent::`, and late-bound `static::` work for supported static members, class +constants, and class-name literals. Eval object construction can allocate eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime class metadata. Missing class names @@ -267,10 +268,9 @@ named method arguments remain unsupported. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are eval-declared enums, trait conflict adaptations -(`insteadof` / `as`), trait and interface constants, `ClassName::class` / -`self::class` / `static::class`, property hooks, attributes/reflection metadata, -readonly semantics, dynamic static callables, and advanced method-call forms -such as named method arguments. +(`insteadof` / `as`), property hooks, attributes/reflection metadata, readonly +semantics, dynamic static callables, and advanced method-call forms such as +named method arguments. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` From 1ed56e9b53412f7bccff1760de60041e012f5253 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 17:21:18 +0200 Subject: [PATCH 0333/1208] Support eval trait adaptations --- crates/elephc-eval/src/eval_ir.rs | 64 +++++++++ crates/elephc-eval/src/interpreter/mod.rs | 2 +- .../elephc-eval/src/interpreter/statements.rs | 135 +++++++++++++++++- .../elephc-eval/src/interpreter/tests/mod.rs | 1 + .../interpreter/tests/trait_adaptations.rs | 96 +++++++++++++ crates/elephc-eval/src/parser/statements.rs | 127 +++++++++++++++- crates/elephc-eval/src/parser/tests/mod.rs | 1 + .../src/parser/tests/trait_adaptations.rs | 68 +++++++++ docs/php/eval.md | 14 +- tests/codegen/eval.rs | 58 ++++++++ 10 files changed, 550 insertions(+), 16 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/tests/trait_adaptations.rs create mode 100644 crates/elephc-eval/src/parser/tests/trait_adaptations.rs diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 4ff2c958ca..f465da97a1 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -273,6 +273,7 @@ pub struct EvalClass { parent: Option, interfaces: Vec, traits: Vec, + trait_adaptations: Vec, constants: Vec, properties: Vec, methods: Vec, @@ -356,6 +357,33 @@ impl EvalClass { constants: Vec, properties: Vec, methods: Vec, + ) -> Self { + Self::with_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + parent, + interfaces, + traits, + Vec::new(), + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with modifiers, relations, trait adaptations, constants, and members. + pub fn with_modifiers_traits_adaptations_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), @@ -364,6 +392,7 @@ impl EvalClass { parent, interfaces, traits, + trait_adaptations, constants, properties, methods, @@ -400,6 +429,11 @@ impl EvalClass { &self.traits } + /// Returns trait adaptations declared on this eval class. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + /// Returns class constants declared directly by this eval class. pub fn constants(&self) -> &[EvalClassConstant] { &self.constants @@ -430,6 +464,22 @@ impl EvalClass { } } +/// Adaptation rule declared in a runtime eval class `use Trait { ... }` block. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalTraitAdaptation { + Alias { + trait_name: Option, + method: String, + alias: Option, + visibility: Option, + }, + InsteadOf { + trait_name: Option, + method: String, + instead_of: Vec, + }, +} + /// Constant metadata for a runtime eval class. #[derive(Debug, Clone, PartialEq)] pub struct EvalClassConstant { @@ -666,6 +716,20 @@ impl EvalClassMethod { &self.name } + /// Returns a copy of this method with a different PHP-visible name. + pub fn renamed(&self, name: impl Into) -> Self { + let mut method = self.clone(); + method.name = name.into(); + method + } + + /// Returns a copy of this method with a different PHP visibility. + pub fn with_visibility_override(&self, visibility: EvalVisibility) -> Self { + let mut method = self.clone(); + method.visibility = visibility; + method + } + /// Returns the PHP visibility declared for this method. pub const fn visibility(&self) -> EvalVisibility { self.visibility diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index 5b76175160..4bba394d1d 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -33,7 +33,7 @@ use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, EvalClassConstant, EvalClassProperty, EvalConst, EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, - EvalTrait, EvalUnaryOp, EvalVisibility, + EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 9158aedc25..353ddeb820 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -553,6 +553,7 @@ fn expand_eval_class_traits( )?; append_eval_trait_methods( trait_decl, + class.trait_adaptations(), &class_method_names, &mut trait_method_names, &mut methods, @@ -561,13 +562,14 @@ fn expand_eval_class_traits( constants.extend(class.constants().iter().cloned()); properties.extend(class.properties().iter().cloned()); methods.extend(class.methods().iter().cloned()); - Ok(EvalClass::with_modifiers_traits_and_constants( + Ok(EvalClass::with_modifiers_traits_adaptations_and_constants( class.name().to_string(), class.is_abstract(), class.is_final(), class.parent().map(str::to_string), class.interfaces().to_vec(), class.traits().to_vec(), + class.trait_adaptations().to_vec(), constants, properties, methods, @@ -642,23 +644,152 @@ fn append_eval_trait_properties( /// Appends trait methods unless the class provides a same-name method. fn append_eval_trait_methods( trait_decl: &EvalTrait, + trait_adaptations: &[EvalTraitAdaptation], class_method_names: &std::collections::HashSet, trait_method_names: &mut std::collections::HashSet, methods: &mut Vec, ) -> Result<(), EvalStatus> { for method in trait_decl.methods() { + if trait_method_suppressed_by_insteadof(trait_decl.name(), method.name(), trait_adaptations) + { + continue; + } let key = method.name().to_ascii_lowercase(); if class_method_names.contains(&key) { continue; } + let method = + apply_trait_visibility_adaptations(trait_decl.name(), method, trait_adaptations); if !trait_method_names.insert(key) { return Err(EvalStatus::RuntimeFatal); } - methods.push(method.clone()); + methods.push(method); + } + append_eval_trait_method_aliases( + trait_decl, + trait_adaptations, + class_method_names, + trait_method_names, + methods, + ) +} + +/// Appends trait method aliases declared with `as`. +fn append_eval_trait_method_aliases( + trait_decl: &EvalTrait, + trait_adaptations: &[EvalTraitAdaptation], + class_method_names: &std::collections::HashSet, + trait_method_names: &mut std::collections::HashSet, + methods: &mut Vec, +) -> Result<(), EvalStatus> { + for adaptation in trait_adaptations { + let EvalTraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + visibility, + } = adaptation + else { + continue; + }; + if !trait_adaptation_target_matches( + trait_name.as_deref(), + method, + trait_decl.name(), + method, + ) { + continue; + } + let Some(source_method) = trait_decl + .methods() + .iter() + .find(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) + else { + if trait_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + continue; + }; + let mut alias_method = source_method.renamed(alias.clone()); + if let Some(visibility) = visibility { + alias_method = alias_method.with_visibility_override(*visibility); + } + let key = alias_method.name().to_ascii_lowercase(); + if class_method_names.contains(&key) || !trait_method_names.insert(key) { + return Err(EvalStatus::RuntimeFatal); + } + methods.push(alias_method); } Ok(()) } +/// Returns whether an `insteadof` adaptation suppresses this trait method import. +fn trait_method_suppressed_by_insteadof( + trait_name: &str, + method_name: &str, + trait_adaptations: &[EvalTraitAdaptation], +) -> bool { + trait_adaptations.iter().any(|adaptation| { + let EvalTraitAdaptation::InsteadOf { + trait_name: selected_trait, + method, + instead_of, + } = adaptation + else { + return false; + }; + method.eq_ignore_ascii_case(method_name) + && instead_of + .iter() + .any(|suppressed| same_eval_class_name(suppressed, trait_name)) + && !selected_trait + .as_deref() + .is_some_and(|selected| same_eval_class_name(selected, trait_name)) + }) +} + +/// Applies visibility-only `as` adaptations to an imported trait method. +fn apply_trait_visibility_adaptations( + trait_name: &str, + method: &EvalClassMethod, + trait_adaptations: &[EvalTraitAdaptation], +) -> EvalClassMethod { + let mut method = method.clone(); + for adaptation in trait_adaptations { + let EvalTraitAdaptation::Alias { + trait_name: target_trait, + method: target_method, + alias: None, + visibility: Some(visibility), + } = adaptation + else { + continue; + }; + if trait_adaptation_target_matches( + target_trait.as_deref(), + target_method, + trait_name, + method.name(), + ) { + method = method.with_visibility_override(*visibility); + } + } + method +} + +/// Returns whether an adaptation target selects one trait method. +fn trait_adaptation_target_matches( + target_trait: Option<&str>, + target_method: &str, + trait_name: &str, + method_name: &str, +) -> bool { + target_method.eq_ignore_ascii_case(method_name) + && target_trait.map_or(true, |target_trait| { + same_eval_class_name(target_trait, trait_name) + }) +} + /// Validates abstract/final modifiers on an eval-declared class and its methods. fn validate_eval_class_modifiers( class: &EvalClass, diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 1149eba62c..b4e884af81 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -44,3 +44,4 @@ mod functions_namespaces; mod native_scope; mod static_members; mod support; +mod trait_adaptations; diff --git a/crates/elephc-eval/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-eval/src/interpreter/tests/trait_adaptations.rs new file mode 100644 index 0000000000..a4c058a6b7 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/trait_adaptations.rs @@ -0,0 +1,96 @@ +//! Purpose: +//! Interpreter tests for eval-declared trait adaptations. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover conflict resolution, aliases, and visibility changes +//! applied while expanding traits into eval class metadata. + +use super::super::*; +use super::support::*; + +/// Verifies `insteadof` keeps the selected method while `as` can alias the other method. +#[test] +fn execute_program_applies_eval_trait_insteadof_and_alias() { + let program = parse_fragment( + br#"trait EvalAdaptA { + public function talk() { return "A"; } + public function hidden() { return "secret"; } +} +trait EvalAdaptB { + public function talk() { return "B"; } +} +class EvalAdaptBox { + use EvalAdaptA, EvalAdaptB { + EvalAdaptA::talk insteadof EvalAdaptB; + EvalAdaptB::talk as talkB; + EvalAdaptA::hidden as private; + } + public function read() { + return $this->talk() . ":" . $this->talkB() . ":" . $this->hidden(); + } +} +$box = new EvalAdaptBox(); +echo $box->read(); echo ":"; +return $box->talk();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:B:secret:"); + assert_eq!(values.get(result), FakeValue::String("A".to_string())); +} + +/// Verifies visibility-only `as private` hides the imported method from global calls. +#[test] +fn execute_program_applies_eval_trait_visibility_adaptation() { + let program = parse_fragment( + br#"trait EvalAdaptHidden { + public function hidden() { return "secret"; } +} +class EvalAdaptHiddenBox { + use EvalAdaptHidden { + EvalAdaptHidden::hidden as private; + } +} +$box = new EvalAdaptHiddenBox(); +return $box->hidden();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private adapted trait method should fail from global scope"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies unresolved same-name trait methods remain a declaration-time fatal. +#[test] +fn execute_program_rejects_unresolved_eval_trait_method_conflict() { + let program = parse_fragment( + br#"trait EvalConflictA { + public function talk() { return "A"; } +} +trait EvalConflictB { + public function talk() { return "B"; } +} +class EvalConflictBox { + use EvalConflictA, EvalConflictB; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unresolved trait method conflict should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 624882c5ab..9e91f0faca 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -13,7 +13,8 @@ use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalExpr, - EvalInterface, EvalInterfaceMethod, EvalStmt, EvalSwitchCase, EvalTrait, EvalVisibility, + EvalInterface, EvalInterfaceMethod, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, + EvalVisibility, }; use crate::lexer::TokenKind; @@ -261,21 +262,29 @@ impl Parser { let mut properties = Vec::new(); let mut methods = Vec::new(); let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); while !self.consume(TokenKind::RBrace) { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - self.parse_class_member(&mut constants, &mut properties, &mut methods, &mut traits)?; + self.parse_class_member( + &mut constants, + &mut properties, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; } self.consume_semicolon(); Ok(vec![EvalStmt::ClassDecl( - EvalClass::with_modifiers_traits_and_constants( + EvalClass::with_modifiers_traits_adaptations_and_constants( name, is_abstract, is_final, parent, interfaces, traits, + trait_adaptations, constants, properties, methods, @@ -342,6 +351,7 @@ impl Parser { properties: &mut Vec, methods: &mut Vec, traits: &mut Vec, + trait_adaptations: &mut Vec, ) -> Result<(), EvalParseError> { let (visibility, is_static, is_abstract, is_final) = self.parse_class_member_modifiers()?; @@ -355,7 +365,7 @@ impl Parser { && !is_final && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) { - self.parse_class_trait_use(traits)?; + self.parse_class_trait_use(traits, trait_adaptations)?; return Ok(()); } @@ -403,10 +413,11 @@ impl Parser { Ok(EvalClassConstant::with_visibility(name, visibility, value)) } - /// Parses `use TraitName, OtherTrait;` inside an eval class body. + /// Parses `use TraitName, OtherTrait;` or an adaptation block inside an eval class body. pub(super) fn parse_class_trait_use( &mut self, traits: &mut Vec, + trait_adaptations: &mut Vec, ) -> Result<(), EvalParseError> { self.advance(); loop { @@ -416,7 +427,111 @@ impl Parser { break; } } - self.expect_semicolon() + if self.consume(TokenKind::LBrace) { + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + trait_adaptations.push(self.parse_trait_adaptation()?); + self.expect_semicolon()?; + } + self.consume_semicolon(); + Ok(()) + } else { + self.expect_semicolon() + } + } + + /// Parses one `as` or `insteadof` trait adaptation clause. + pub(super) fn parse_trait_adaptation(&mut self) -> Result { + let (trait_name, method) = self.parse_trait_adaptation_target()?; + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "as") => { + self.advance(); + let visibility = self.parse_optional_trait_adaptation_visibility()?; + let alias = if let TokenKind::Ident(alias) = self.current() { + let alias = alias.clone(); + self.advance(); + Some(alias) + } else { + None + }; + if visibility.is_none() && alias.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalTraitAdaptation::Alias { + trait_name, + method, + alias, + visibility, + }) + } + TokenKind::Ident(name) if ident_eq(name, "insteadof") => { + self.advance(); + let mut instead_of = Vec::new(); + loop { + let trait_name = self.parse_qualified_name()?; + instead_of.push(self.resolve_class_name(trait_name)); + if !self.consume(TokenKind::Comma) { + break; + } + } + if instead_of.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses the target before `as` or `insteadof`. + pub(super) fn parse_trait_adaptation_target( + &mut self, + ) -> Result<(Option, String), EvalParseError> { + let first = self.parse_qualified_name()?; + if self.consume(TokenKind::DoubleColon) { + let TokenKind::Ident(method) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let method = method.clone(); + self.advance(); + Ok((Some(self.resolve_class_name(first)), method)) + } else { + let method = first + .name + .rsplit('\\') + .next() + .filter(|segment| !segment.is_empty()) + .ok_or(EvalParseError::UnexpectedToken)? + .to_string(); + Ok((None, method)) + } + } + + /// Parses an optional visibility modifier inside a trait `as` adaptation. + pub(super) fn parse_optional_trait_adaptation_visibility( + &mut self, + ) -> Result, EvalParseError> { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + self.advance(); + Ok(Some(EvalVisibility::Public)) + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + self.advance(); + Ok(Some(EvalVisibility::Protected)) + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + self.advance(); + Ok(Some(EvalVisibility::Private)) + } + _ => Ok(None), + } } /// Parses method modifiers supported by eval class declarations. diff --git a/crates/elephc-eval/src/parser/tests/mod.rs b/crates/elephc-eval/src/parser/tests/mod.rs index 353b9eb8e3..7a69de2972 100644 --- a/crates/elephc-eval/src/parser/tests/mod.rs +++ b/crates/elephc-eval/src/parser/tests/mod.rs @@ -22,3 +22,4 @@ mod namespaces; mod operators; mod static_members; mod support; +mod trait_adaptations; diff --git a/crates/elephc-eval/src/parser/tests/trait_adaptations.rs b/crates/elephc-eval/src/parser/tests/trait_adaptations.rs new file mode 100644 index 0000000000..df21c07733 --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/trait_adaptations.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Parser tests for eval class trait adaptation syntax. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `insteadof` and `as` clauses lower into class metadata. + +use super::support::*; + +/// Verifies trait `insteadof`, alias, and visibility adaptations are parsed. +#[test] +fn parse_fragment_accepts_trait_adaptations() { + let program = parse_fragment( + br#"class EvalAdaptBox { + use EvalAdaptA, EvalAdaptB { + EvalAdaptA::talk insteadof EvalAdaptB; + EvalAdaptB::talk as talkB; + EvalAdaptA::hidden as private; + talk as public talkPublic; + } +}"#, + ) + .expect("fragment should parse"); + + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::with_modifiers_traits_adaptations_and_constants( + "EvalAdaptBox", + false, + false, + None, + Vec::new(), + vec!["EvalAdaptA".to_string(), "EvalAdaptB".to_string()], + vec![ + EvalTraitAdaptation::InsteadOf { + trait_name: Some("EvalAdaptA".to_string()), + method: "talk".to_string(), + instead_of: vec!["EvalAdaptB".to_string()], + }, + EvalTraitAdaptation::Alias { + trait_name: Some("EvalAdaptB".to_string()), + method: "talk".to_string(), + alias: Some("talkB".to_string()), + visibility: None, + }, + EvalTraitAdaptation::Alias { + trait_name: Some("EvalAdaptA".to_string()), + method: "hidden".to_string(), + alias: None, + visibility: Some(EvalVisibility::Private), + }, + EvalTraitAdaptation::Alias { + trait_name: None, + method: "talk".to_string(), + alias: Some("talkPublic".to_string()), + visibility: Some(EvalVisibility::Public), + }, + ], + Vec::new(), + Vec::new(), + Vec::new(), + ) + )] + ); +} diff --git a/docs/php/eval.md b/docs/php/eval.md index 4cd152e920..ffd05fe0b2 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, methods, `__construct()`, inheritance, visibility, abstract/final modifiers, simple trait uses, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, methods, `__construct()`, inheritance, visibility, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -127,8 +127,9 @@ containers to eval-declared functions. Eval-declared classes support inheritance, public/protected/private properties and methods, `__construct()`, abstract classes and methods, final classes and -methods, simple `use Trait;` composition, interface implementation checks, -static properties, static methods, class constants, interface constants, trait +methods, trait composition with `insteadof` conflict resolution and `as` +aliases/visibility adaptations, interface implementation checks, static +properties, static methods, class constants, interface constants, trait constants, and `ClassName::class` literals. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. `self::`, `parent::`, and late-bound `static::` work for supported static members, class @@ -267,10 +268,9 @@ calls through eval support positional arguments and numeric array unpacking; named method arguments remain unsupported. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are eval-declared enums, trait conflict adaptations -(`insteadof` / `as`), property hooks, attributes/reflection metadata, readonly -semantics, dynamic static callables, and advanced method-call forms such as -named method arguments. +remaining class-system gaps are eval-declared enums, property hooks, +attributes/reflection metadata, readonly semantics, dynamic static callables, +and advanced method-call forms such as named method arguments. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f47e2f0641..75beacc66d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4701,6 +4701,64 @@ echo $box->seed;'); ); } +/// Verifies eval-declared trait adaptations resolve conflicts, aliases, and visibility. +#[test] +fn test_eval_declared_trait_adaptations() { + let out = compile_and_run_capture( + r#"talk() . ":" . $this->talkB() . ":" . $this->hidden(); + } +} +$box = new EvalAdaptBox(); +echo $box->read() . ":"; +echo $box->talk();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "A:B:secret:A"); +} + +/// Verifies eval-declared trait visibility adaptations affect bridge access checks. +#[test] +fn test_eval_declared_trait_visibility_adaptation_fails() { + let err = compile_and_run_expect_failure( + r#"hidden();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies eval-declared trait abstract methods must be implemented by concrete classes. #[test] fn test_eval_declared_trait_abstract_method_requirement_fails() { From cf632edb0ea6c7315093c70807e5f62c80df2ecc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 17:26:58 +0200 Subject: [PATCH 0334/1208] Support eval method named arguments --- .../src/interpreter/expressions.rs | 19 +++-- .../elephc-eval/src/interpreter/statements.rs | 43 ++++++++-- .../src/interpreter/tests/method_arguments.rs | 81 +++++++++++++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + .../src/parser/tests/arrays_objects.rs | 22 +++++ .../src/parser/tests/static_members.rs | 18 +++++ docs/php/eval.md | 13 +-- tests/codegen/eval.rs | 31 +++++++ 8 files changed, 206 insertions(+), 22 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/tests/method_arguments.rs diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 200aef0934..9d0363bada 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -71,6 +71,7 @@ pub(in crate::interpreter) fn eval_expr( let class_name = context .resolve_class_name(class_name) .unwrap_or_else(|| class_name.clone()); + let args = positional_evaluated_arg_values(args)?; values .new_object(&class_name) .and_then(|object| values.construct_object(object, args).map(|()| object)) @@ -102,7 +103,13 @@ pub(in crate::interpreter) fn eval_expr( } => { let object = eval_expr(object, context, scope, values)?; let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; - eval_method_call_result(object, method, evaluated_args, context, values) + eval_method_call_result_with_evaluated_args( + object, + method, + evaluated_args, + context, + values, + ) } EvalExpr::NullCoalesce { value, default } => { let value = eval_expr(value, context, scope, values)?; @@ -268,18 +275,14 @@ pub(in crate::interpreter) fn eval_positional_call_arg_values( Ok(evaluated_args) } -/// Evaluates method-call arguments, allowing numeric spread but not named args. +/// Evaluates method-call arguments, preserving named metadata for eval method binding. pub(in crate::interpreter) fn eval_method_call_arg_values( args: &[EvalCallArg], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - if evaluated_args.iter().any(|arg| arg.name.is_some()) { - return Err(EvalStatus::RuntimeFatal); - } - Ok(evaluated_args.into_iter().map(|arg| arg.value).collect()) +) -> Result, EvalStatus> { + eval_call_arg_values(args, context, scope, values) } /// Evaluates supported function-like calls from a runtime eval fragment. diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 353ddeb820..e7f6c33a35 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1141,7 +1141,7 @@ pub(in crate::interpreter) fn eval_static_property_set_result( pub(in crate::interpreter) fn eval_static_method_call_result( class_name: &str, method_name: &str, - evaluated_args: Vec, + evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -1248,7 +1248,7 @@ fn resolve_eval_class_name_literal( /// Creates a backing object for an eval-declared class and runs its constructor. pub(in crate::interpreter) fn eval_dynamic_class_new_object( class: &EvalClass, - evaluated_args: Vec, + evaluated_args: Vec, context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, @@ -1303,11 +1303,30 @@ pub(in crate::interpreter) fn eval_method_call_result( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_method_call_result_with_evaluated_args( + object, + method_name, + positional_args(evaluated_args), + context, + values, + ) +} + +/// Dispatches an object method call while preserving named-argument metadata for eval methods. +pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let Ok(identity) = values.object_identity(object) else { + let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; return values.method_call(object, method_name, evaluated_args); }; let Some(class) = context.dynamic_object_class(identity) else { + let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; return values.method_call(object, method_name, evaluated_args); }; let called_class_name = class.name().to_string(); @@ -1393,12 +1412,11 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( called_class_name: &str, method: &EvalClassMethod, object: RuntimeCellHandle, - evaluated_args: Vec, + evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = - bind_evaluated_function_args(method.params(), positional_args(evaluated_args))?; + let evaluated_args = bind_evaluated_function_args(method.params(), evaluated_args)?; let mut method_scope = ElephcEvalScope::new(); method_scope.set("this", object, ScopeCellOwnership::Borrowed); for (name, value) in method.params().iter().zip(evaluated_args) { @@ -1438,12 +1456,11 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( class_name: &str, called_class_name: &str, method: &EvalClassMethod, - evaluated_args: Vec, + evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = - bind_evaluated_function_args(method.params(), positional_args(evaluated_args))?; + let evaluated_args = bind_evaluated_function_args(method.params(), evaluated_args)?; let mut method_scope = ElephcEvalScope::new(); for (name, value) in method.params().iter().zip(evaluated_args) { method_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); @@ -1486,6 +1503,16 @@ pub(in crate::interpreter) fn positional_args( .collect() } +/// Extracts positional runtime values and rejects named args before runtime method dispatch. +pub(in crate::interpreter) fn positional_evaluated_arg_values( + args: Vec, +) -> Result, EvalStatus> { + if args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(args.into_iter().map(|arg| arg.value).collect()) +} + /// Executes a PHP `static $name = expr;` declaration in the current eval scope. pub(in crate::interpreter) fn execute_static_var_stmt( name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs new file mode 100644 index 0000000000..9181ac42c4 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -0,0 +1,81 @@ +//! Purpose: +//! Interpreter tests for eval-declared method and constructor argument binding. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover named arguments and named unpacking on instance methods, +//! static methods, and constructors declared inside eval fragments. + +use super::super::*; +use super::support::*; + +/// Verifies eval-declared instance, static, and constructor methods bind named args. +#[test] +fn execute_program_binds_eval_method_named_args() { + let program = parse_fragment( + br#"class EvalNamedMethodBox { + public function __construct($left, $right) { + $this->label = $left . $right; + } + public function read($left, $right) { + return $this->label . ":" . $left . ":" . $right; + } + public static function join($left, $right) { + return $left . "-" . $right; + } +} +$box = new EvalNamedMethodBox(right: "B", left: "A"); +echo $box->read(right: "D", left: "C"); echo ":"; +$args = ["right" => "F", "left" => "E"]; +echo $box->read(...$args); echo ":"; +return EvalNamedMethodBox::join(right: "H", left: "G");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:C:D:AB:E:F:"); + assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); +} + +/// Verifies eval-declared methods reject unknown named arguments. +#[test] +fn execute_program_rejects_unknown_eval_method_named_arg() { + let program = parse_fragment( + br#"class EvalUnknownNamedMethodBox { + public function read($left) { + return $left; + } +} +$box = new EvalUnknownNamedMethodBox(); +return $box->read(missing: "bad");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unknown named method argument should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies runtime/AOT method fallback still rejects named arguments. +#[test] +fn execute_program_rejects_named_args_for_runtime_method_fallback() { + let program = + parse_fragment(br#"return $this->answer(value: 1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("runtime method fallback named args should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index b4e884af81..b87e4aa14e 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -41,6 +41,7 @@ mod core; mod dynamic_calls; mod expressions; mod functions_namespaces; +mod method_arguments; mod native_scope; mod static_members; mod support; diff --git a/crates/elephc-eval/src/parser/tests/arrays_objects.rs b/crates/elephc-eval/src/parser/tests/arrays_objects.rs index 7a47613981..5ad39294ad 100644 --- a/crates/elephc-eval/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-eval/src/parser/tests/arrays_objects.rs @@ -193,6 +193,28 @@ fn parse_fragment_accepts_method_call_multiple_args_source() { }))] ); } + +/// Verifies object method calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_method_call_args_source() { + let program = parse_fragment(br#"return $this->label(right: "ok", left: $x);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "label".to_string(), + args: vec![ + EvalCallArg::named( + "right", + EvalExpr::Const(EvalConst::String("ok".to_string())), + ), + EvalCallArg::named("left", EvalExpr::LoadVar("x".to_string())), + ], + }))] + ); +} + /// Verifies object property writes parse as dedicated EvalIR statements. #[test] fn parse_fragment_accepts_property_write_source() { diff --git a/crates/elephc-eval/src/parser/tests/static_members.rs b/crates/elephc-eval/src/parser/tests/static_members.rs index 46e184b3ff..ef924beaff 100644 --- a/crates/elephc-eval/src/parser/tests/static_members.rs +++ b/crates/elephc-eval/src/parser/tests/static_members.rs @@ -60,6 +60,24 @@ fn parse_fragment_accepts_static_method_call_expression() { ); } +/// Verifies static method calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_static_method_call_expression() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(step: 2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { + class_name: "EvalStaticBox".to_string(), + method: "read".to_string(), + args: vec![EvalCallArg::named( + "step", + EvalExpr::Const(EvalConst::Int(2)), + )], + }))] + ); +} + /// Verifies static property compound assignments lower to one read-modify-write statement. #[test] fn parse_fragment_accepts_static_property_compound_assignment() { diff --git a/docs/php/eval.md b/docs/php/eval.md index ffd05fe0b2..1f01163bc2 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -72,8 +72,8 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime metadata. | -| Method calls | Public object method calls with positional arguments and numeric array unpacking. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata use positional constructor arguments. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method fallback remains positional. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -263,14 +263,15 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors, closure callback values, and -static-method callable arrays are still outside eval fragments. Object method -calls through eval support positional arguments and numeric array unpacking; -named method arguments remain unsupported. +static-method callable arrays are still outside eval fragments. Runtime/AOT +object-method fallback from eval remains positional, so named method arguments +are supported for eval-declared methods but not for every generated native +method bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are eval-declared enums, property hooks, attributes/reflection metadata, readonly semantics, dynamic static callables, -and advanced method-call forms such as named method arguments. +and dynamic static-method call forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 75beacc66d..e2c5b0a0e7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4847,6 +4847,37 @@ echo EvalStaticBase::baseRead();'); assert_eq!(out.stdout, "1:3:3:14:5:5"); } +/// Verifies eval-declared constructors and methods bind named arguments. +#[test] +fn test_eval_declared_method_named_arguments() { + let out = compile_and_run_capture( + r#"label = $left . $right; + } + public function read($left, $right) { + return $this->label . ":" . $left . ":" . $right; + } + public static function join($left, $right) { + return $left . "-" . $right; + } +} +$box = new EvalNamedMethodBox(right: "B", left: "A"); +echo $box->read(right: "D", left: "C") . ":"; +$args = ["right" => "F", "left" => "E"]; +echo $box->read(...$args) . ":"; +echo EvalNamedMethodBox::join(right: "H", left: "G");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "AB:C:D:AB:E:F:G-H"); +} + /// Verifies eval-declared class constants work through the bridge. #[test] fn test_eval_declared_class_constants_and_scoped_fetches() { From 19fb0f291c33790a61d1337f46de5b9bacd02d2a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 17:31:10 +0200 Subject: [PATCH 0335/1208] Support eval static method callables --- .../interpreter/builtins/registry/callable.rs | 53 ++++++++++++++++--- crates/elephc-eval/src/interpreter/control.rs | 4 ++ .../src/interpreter/tests/dynamic_calls.rs | 27 ++++++++++ docs/php/eval.md | 17 +++--- tests/codegen/eval.rs | 26 +++++++++ 5 files changed, 113 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs b/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs index 38cb9c2278..a58615de5d 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs @@ -79,10 +79,10 @@ pub(in crate::interpreter) fn eval_callable( if values.is_array_like(callback)? { return eval_array_callable(callback, values); } - eval_callable_name(callback, values).map(EvaluatedCallable::Named) + eval_string_callable(callback, values) } -/// Normalizes one two-element object-method callable array. +/// Normalizes one two-element object-method or static-method callable array. pub(in crate::interpreter) fn eval_array_callable( callback: RuntimeCellHandle, values: &mut impl RuntimeValueOps, @@ -92,17 +92,46 @@ pub(in crate::interpreter) fn eval_array_callable( } let zero = values.int(0)?; let one = values.int(1)?; - let object = values.array_get(callback, zero)?; - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::UnsupportedConstruct); - } + let receiver = values.array_get(callback, zero)?; let method = values.array_get(callback, one)?; let method = String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(EvaluatedCallable::ObjectMethod { object, method }) + match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => Ok(EvaluatedCallable::ObjectMethod { + object: receiver, + method, + }), + EVAL_TAG_STRING => { + let class_name = String::from_utf8(values.string_bytes(receiver)?) + .map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(EvaluatedCallable::StaticMethod { class_name, method }) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } } /// Normalizes one string callback name for eval dynamic callable dispatch. +pub(in crate::interpreter) fn eval_string_callable( + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = values.string_bytes(callback)?; + let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; + if let Some((class_name, method)) = callback.split_once("::") { + if class_name.is_empty() || method.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(EvaluatedCallable::StaticMethod { + class_name: class_name.trim_start_matches('\\').to_string(), + method: method.to_string(), + }); + } + Ok(EvaluatedCallable::Named( + callback.trim_start_matches('\\').to_ascii_lowercase(), + )) +} + +/// Normalizes one string function callback name for builtin callback positions. pub(in crate::interpreter) fn eval_callable_name( callback: RuntimeCellHandle, values: &mut impl RuntimeValueOps, @@ -130,6 +159,13 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( EvaluatedCallable::ObjectMethod { object, method } => { eval_method_call_result(*object, method, evaluated_args, context, values) } + EvaluatedCallable::StaticMethod { class_name, method } => eval_static_method_call_result( + class_name, + method, + positional_args(evaluated_args), + context, + values, + ), } } @@ -151,6 +187,9 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); eval_method_call_result(*object, method, evaluated_args, context, values) } + EvaluatedCallable::StaticMethod { class_name, method } => { + eval_static_method_call_result(class_name, method, evaluated_args, context, values) + } } } diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-eval/src/interpreter/control.rs index 5bed445ac6..752597a0b0 100644 --- a/crates/elephc-eval/src/interpreter/control.rs +++ b/crates/elephc-eval/src/interpreter/control.rs @@ -39,6 +39,10 @@ pub(super) enum EvaluatedCallable { object: RuntimeCellHandle, method: String, }, + StaticMethod { + class_name: String, + method: String, + }, } /// Bound argument tuple for direct `array_splice()` calls. diff --git a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs index 4411073c93..8d6f8c6730 100644 --- a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs @@ -174,6 +174,33 @@ return call_user_func_array([$box, "add2_x"], [1, 2]);"#, assert_eq!(values.get(result), FakeValue::Int(42)); } + +/// Verifies static method callable arrays dispatch eval-declared static methods. +#[test] +fn execute_program_static_callable_array_dispatches_eval_method() { + let program = parse_fragment( + br#"class EvalStaticCallableBox { + public static function join($left, $right) { + return $left . $right; + } +} +$cb = ["EvalStaticCallableBox", "join"]; +echo $cb(right: "B", left: "A"); echo ":"; +echo call_user_func($cb, "C", "D"); echo ":"; +echo call_user_func_array($cb, ["right" => "F", "left" => "E"]); echo ":"; +$named = "EvalStaticCallableBox::join"; +return $named(right: "H", left: "G");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:CD:EF:"); + assert_eq!(values.get(result), FakeValue::String("GH".to_string())); +} + /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. #[test] fn execute_program_call_user_func_array_dispatches_declared_function() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 1f01163bc2..cb4046f7a4 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -116,7 +116,10 @@ eval-declared functions, and registered AOT functions. Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, ...)`, `call_user_func_array($cb, [...])`, and `iterator_apply()` with -positional arguments. +positional arguments. Static method callables for eval-declared static methods +can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, +`call_user_func()`, and `call_user_func_array()`; `call_user_func_array()` can +also bind string-keyed named arguments for these eval-declared static methods. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -263,15 +266,15 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors, closure callback values, and -static-method callable arrays are still outside eval fragments. Runtime/AOT -object-method fallback from eval remains positional, so named method arguments -are supported for eval-declared methods but not for every generated native -method bridge. +generated/AOT static-method callable arrays are still outside eval fragments. +Runtime/AOT object-method fallback from eval remains positional, so named method +arguments are supported for eval-declared methods but not for every generated +native method bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are eval-declared enums, property hooks, -attributes/reflection metadata, readonly semantics, dynamic static callables, -and dynamic static-method call forms. +attributes/reflection metadata, readonly semantics, and generated/AOT dynamic +static-method call forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e2c5b0a0e7..de94399dca 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4878,6 +4878,32 @@ echo EvalNamedMethodBox::join(right: "H", left: "G");'); assert_eq!(out.stdout, "AB:C:D:AB:E:F:G-H"); } +/// Verifies eval dynamic static callables dispatch eval-declared static methods. +#[test] +fn test_eval_declared_static_method_dynamic_callables() { + let out = compile_and_run_capture( + r#" "F", "left" => "E"]) . ":"; +$named = "EvalStaticCallableBox::join"; +echo $named(right: "H", left: "G");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "AB:CD:EF:GH"); +} + /// Verifies eval-declared class constants work through the bridge. #[test] fn test_eval_declared_class_constants_and_scoped_fetches() { From e57754fe62dc1dee041fe362323f286ee36fee97 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 17:50:14 +0200 Subject: [PATCH 0336/1208] Support eval-declared enums --- crates/elephc-eval/src/context.rs | 118 +++++++- crates/elephc-eval/src/eval_ir.rs | 133 ++++++++ .../interpreter/builtins/class_metadata.rs | 1 + .../src/interpreter/builtins/symbols.rs | 2 +- .../src/interpreter/dynamic_functions.rs | 1 + crates/elephc-eval/src/interpreter/mod.rs | 9 +- .../elephc-eval/src/interpreter/statements.rs | 284 +++++++++++++++++- .../src/interpreter/tests/enums.rs | 124 ++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 3 +- .../interpreter/tests/support/conversions.rs | 8 + crates/elephc-eval/src/parser/cursor.rs | 3 +- crates/elephc-eval/src/parser/statements.rs | 111 ++++++- crates/elephc-eval/src/parser/tests/enums.rs | 76 +++++ crates/elephc-eval/src/parser/tests/mod.rs | 1 + docs/php/eval.md | 18 +- tests/codegen/eval.rs | 27 ++ 16 files changed, 903 insertions(+), 16 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/tests/enums.rs create mode 100644 crates/elephc-eval/src/parser/tests/enums.rs diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 596f7cdfc0..29e979b343 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -16,8 +16,8 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::{ - EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalFunction, EvalInterface, - EvalInterfaceMethod, EvalTrait, + EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, EvalFunction, + EvalInterface, EvalInterfaceMethod, EvalTrait, }; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; @@ -97,6 +97,10 @@ pub struct ElephcEvalContext { declared_interface_names: Vec, traits: HashMap, declared_trait_names: Vec, + enums: HashMap, + declared_enum_names: Vec, + enum_cases: HashMap<(String, String), RuntimeCellHandle>, + enum_case_values: HashMap<(String, String), RuntimeCellHandle>, constants: HashMap, functions: HashMap, native_functions: HashMap, @@ -132,6 +136,10 @@ impl ElephcEvalContext { declared_interface_names: Vec::new(), traits: HashMap::new(), declared_trait_names: Vec::new(), + enums: HashMap::new(), + declared_enum_names: Vec::new(), + enum_cases: HashMap::new(), + enum_case_values: HashMap::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -168,6 +176,10 @@ impl ElephcEvalContext { declared_interface_names: Vec::new(), traits: HashMap::new(), declared_trait_names: Vec::new(), + enums: HashMap::new(), + declared_enum_names: Vec::new(), + enum_cases: HashMap::new(), + enum_case_values: HashMap::new(), constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), @@ -204,6 +216,7 @@ impl ElephcEvalContext { || self.class_aliases.contains_key(&key) || self.interfaces.contains_key(&key) || self.traits.contains_key(&key) + || self.enums.contains_key(&key) { return false; } @@ -250,6 +263,9 @@ impl ElephcEvalContext { if let Some(trait_decl) = self.traits.get(&key) { return Some(trait_decl.name().to_string()); } + if let Some(enum_decl) = self.enums.get(&key) { + return Some(enum_decl.name().to_string()); + } self.class_aliases.get(&key).cloned() } @@ -268,6 +284,7 @@ impl ElephcEvalContext { || self.classes.contains_key(&alias_key) || self.interfaces.contains_key(&alias_key) || self.traits.contains_key(&alias_key) + || self.enums.contains_key(&alias_key) || self.class_aliases.contains_key(&alias_key) { return false; @@ -290,6 +307,7 @@ impl ElephcEvalContext { if self.interfaces.contains_key(&key) || self.classes.contains_key(&key) || self.traits.contains_key(&key) + || self.enums.contains_key(&key) || self.class_aliases.contains_key(&key) { return false; @@ -321,6 +339,7 @@ impl ElephcEvalContext { if self.traits.contains_key(&key) || self.classes.contains_key(&key) || self.interfaces.contains_key(&key) + || self.enums.contains_key(&key) || self.class_aliases.contains_key(&key) { return false; @@ -346,6 +365,96 @@ impl ElephcEvalContext { &self.declared_trait_names } + /// Defines an eval-declared enum plus class-shaped metadata for dispatch. + pub fn define_enum(&mut self, enum_decl: EvalEnum) -> bool { + let key = normalize_class_name(enum_decl.name()); + if self.enums.contains_key(&key) + || self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.traits.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + return false; + } + self.declared_enum_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + self.declared_class_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + self.classes + .insert(key.clone(), enum_decl.as_class_metadata()); + self.enums.insert(key, enum_decl); + true + } + + /// Returns true when this eval context has a dynamic enum with the requested name. + pub fn has_enum(&self, name: &str) -> bool { + self.enums.contains_key(&normalize_class_name(name)) + } + + /// Returns a dynamic eval enum by PHP case-insensitive enum name. + pub fn enum_decl(&self, name: &str) -> Option<&EvalEnum> { + self.enums.get(&normalize_class_name(name)) + } + + /// Returns enum names declared through eval in PHP-visible order. + pub fn declared_enum_names(&self) -> &[String] { + &self.declared_enum_names + } + + /// Returns a materialized singleton case object for one eval enum case. + pub fn enum_case(&self, enum_name: &str, case_name: &str) -> Option { + self.enum_cases + .get(&( + normalize_class_name(enum_name), + normalize_enum_case_name(case_name), + )) + .copied() + } + + /// Stores a materialized singleton case object and returns any replaced distinct cell. + pub fn set_enum_case( + &mut self, + enum_name: &str, + case_name: &str, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self.enum_cases.insert( + ( + normalize_class_name(enum_name), + normalize_enum_case_name(case_name), + ), + cell, + ); + previous.filter(|previous| *previous != cell) + } + + /// Returns a materialized backing value for one eval backed-enum case. + pub fn enum_case_value(&self, enum_name: &str, case_name: &str) -> Option { + self.enum_case_values + .get(&( + normalize_class_name(enum_name), + normalize_enum_case_name(case_name), + )) + .copied() + } + + /// Stores a materialized backing value and returns any replaced distinct cell. + pub fn set_enum_case_value( + &mut self, + enum_name: &str, + case_name: &str, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self.enum_case_values.insert( + ( + normalize_class_name(enum_name), + normalize_enum_case_name(case_name), + ), + cell, + ); + previous.filter(|previous| *previous != cell) + } + /// Records that one runtime object handle was created for an eval-declared class. pub fn register_dynamic_object(&mut self, identity: u64, class_name: &str) { let class_name = self @@ -989,6 +1098,11 @@ fn normalize_class_name(name: &str) -> String { name.trim_start_matches('\\').to_ascii_lowercase() } +/// Normalizes PHP enum case names for case-sensitive eval enum lookup. +fn normalize_enum_case_name(name: &str) -> String { + name.to_string() +} + /// Pushes a PHP class-like name once, preserving the first visible spelling. fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSet) { let key = normalize_class_name(name); diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index f465da97a1..0bccbece0b 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -69,6 +69,7 @@ pub enum EvalStmt { body: Vec, }, ClassDecl(EvalClass), + EnumDecl(EvalEnum), InterfaceDecl(EvalInterface), TraitDecl(EvalTrait), Foreach { @@ -175,6 +176,138 @@ impl EvalFunction { } } +/// Runtime enum declared by an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalEnum { + name: String, + backing_type: Option, + interfaces: Vec, + cases: Vec, + constants: Vec, + methods: Vec, +} + +impl EvalEnum { + /// Creates a dynamic eval enum with cases and optional backing type. + pub fn new( + name: impl Into, + backing_type: Option, + cases: Vec, + ) -> Self { + Self::with_members( + name, + backing_type, + Vec::new(), + cases, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval enum with interfaces, cases, constants, and methods. + pub fn with_members( + name: impl Into, + backing_type: Option, + interfaces: Vec, + cases: Vec, + constants: Vec, + methods: Vec, + ) -> Self { + Self { + name: name.into(), + backing_type, + interfaces, + cases, + constants, + methods, + } + } + + /// Returns the original source spelling of this eval-declared enum name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the optional scalar backing type for this enum. + pub const fn backing_type(&self) -> Option { + self.backing_type + } + + /// Returns interface names implemented directly by this eval enum. + pub fn interfaces(&self) -> &[String] { + &self.interfaces + } + + /// Returns cases declared directly by this eval enum. + pub fn cases(&self) -> &[EvalEnumCase] { + &self.cases + } + + /// Returns one enum case by PHP case-sensitive case name. + pub fn case(&self, name: &str) -> Option<&EvalEnumCase> { + self.cases().iter().find(|case| case.name() == name) + } + + /// Returns constants declared directly by this eval enum. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns methods declared directly by this eval enum. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } + + /// Builds class-shaped metadata used for enum method and relation dispatch. + pub fn as_class_metadata(&self) -> EvalClass { + EvalClass::with_modifiers_traits_and_constants( + self.name.clone(), + false, + true, + None, + self.interfaces.clone(), + Vec::new(), + self.constants.clone(), + Vec::new(), + self.methods.clone(), + ) + } +} + +/// Scalar backing type for a runtime eval enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalEnumBackingType { + Int, + String, +} + +/// One case declared by a runtime eval enum. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalEnumCase { + name: String, + value: Option, +} + +impl EvalEnumCase { + /// Creates an eval enum case with an optional backing value expression. + pub fn new(name: impl Into, value: Option) -> Self { + Self { + name: name.into(), + value, + } + } + + /// Returns the PHP-visible enum case name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the optional backing value expression. + pub fn value(&self) -> Option<&EvalExpr> { + self.value.as_ref() + } +} + /// Runtime interface declared by an eval fragment. #[derive(Debug, Clone, PartialEq)] pub struct EvalInterface { diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 116dcb28d0..6bc8aaf864 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -151,6 +151,7 @@ fn eval_class_relation_name_exists( if context.has_class(name) || context.has_interface(name) || context.has_trait(name) + || context.has_enum(name) || values.class_exists(name)? || values.interface_exists(name)? || values.trait_exists(name)? diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index 2324801492..a5c5ced449 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -377,7 +377,7 @@ pub(in crate::interpreter) fn eval_class_like_exists_name( let symbol = symbol.trim_start_matches('\\'); match name { "trait_exists" => Ok(context.has_trait(symbol) || values.trait_exists(symbol)?), - "enum_exists" => values.enum_exists(symbol), + "enum_exists" => Ok(context.has_enum(symbol) || values.enum_exists(symbol)?), _ => Err(EvalStatus::UnsupportedConstruct), } } diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 33f7f759fb..bdebc18617 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -279,6 +279,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::ClassDecl(_) | EvalStmt::Continue | EvalStmt::Echo(_) + | EvalStmt::EnumDecl(_) | EvalStmt::Expr(_) | EvalStmt::Global { .. } | EvalStmt::InterfaceDecl(_) diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index 4bba394d1d..f14a7295cf 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -30,10 +30,11 @@ mod statements; use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassMethod, - EvalClassConstant, EvalClassProperty, EvalConst, EvalExpr, EvalFunction, EvalInterface, - EvalInterfaceMethod, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, - EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassConstant, + EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, + EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalMagicConst, EvalMatchArm, + EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, + EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index e7f6c33a35..8bf8b6fbbf 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -110,6 +110,10 @@ pub(in crate::interpreter) fn execute_stmt( execute_class_decl_stmt(class, context, scope, values)?; Ok(EvalControl::None) } + EvalStmt::EnumDecl(enum_decl) => { + execute_enum_decl_stmt(enum_decl, context, scope, values)?; + Ok(EvalControl::None) + } EvalStmt::InterfaceDecl(interface) => { execute_interface_decl_stmt(interface, context, scope, values)?; Ok(EvalControl::None) @@ -365,9 +369,11 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( if context.has_class(name) || context.has_interface(name) || context.has_trait(name) + || context.has_enum(name) || values.class_exists(name)? || values.interface_exists(name)? || values.trait_exists(name)? + || values.enum_exists(name)? { return Err(EvalStatus::RuntimeFatal); } @@ -404,6 +410,166 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( } } +/// Registers an eval-declared enum and materializes its singleton cases. +pub(in crate::interpreter) fn execute_enum_decl_stmt( + enum_decl: &EvalEnum, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = enum_decl.name().trim_start_matches('\\'); + if context.has_enum(name) + || context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || values.enum_exists(name)? + || values.class_exists(name)? + || values.interface_exists(name)? + || values.trait_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_enum_decl(enum_decl, context, values)?; + if context.define_enum(enum_decl.clone()) { + initialize_eval_declared_constants( + enum_decl.name(), + enum_decl.constants(), + context, + scope, + values, + )?; + initialize_eval_enum_cases(enum_decl, context, scope, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Validates enum metadata before it is inserted into the dynamic context. +fn validate_eval_enum_decl( + enum_decl: &EvalEnum, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + validate_eval_declared_constants(enum_decl.constants())?; + validate_eval_enum_case_declarations(enum_decl)?; + validate_eval_enum_method_declarations(enum_decl)?; + let enum_class = enum_decl.as_class_metadata(); + validate_eval_class_modifiers(&enum_class, context)?; + for interface in enum_decl.interfaces() { + if !context.has_interface(interface) && !values.interface_exists(interface)? { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_concrete_class_requirements(&enum_class, context) +} + +/// Validates enum case names and pure/backed declaration shape. +fn validate_eval_enum_case_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + let mut case_names = std::collections::HashSet::new(); + let constant_names = enum_decl + .constants() + .iter() + .map(|constant| constant.name().to_string()) + .collect::>(); + for case in enum_decl.cases() { + if !case_names.insert(case.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + if constant_names.contains(case.name()) { + return Err(EvalStatus::RuntimeFatal); + } + match (enum_decl.backing_type(), case.value()) { + (None, None) | (Some(_), Some(_)) => {} + (None, Some(_)) | (Some(_), None) => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(()) +} + +/// Validates enum method declarations that PHP reserves or forbids on enums. +fn validate_eval_enum_method_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + for method in enum_decl.methods() { + if method.name().eq_ignore_ascii_case("__construct") + || method.name().eq_ignore_ascii_case("cases") + || method.name().eq_ignore_ascii_case("from") + || method.name().eq_ignore_ascii_case("tryFrom") + { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Initializes enum singleton case objects for a newly declared eval enum. +fn initialize_eval_enum_cases( + enum_decl: &EvalEnum, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut backing_values = Vec::new(); + for case in enum_decl.cases() { + let backing_value = if let Some(value_expr) = case.value() { + let value = eval_expr(value_expr, context, scope, values)?; + validate_eval_enum_backing_value(enum_decl.backing_type(), value, values)?; + for existing in &backing_values { + let equal = values.compare(EvalBinOp::StrictEq, value, *existing)?; + if values.truthy(equal)? { + return Err(EvalStatus::RuntimeFatal); + } + } + backing_values.push(value); + Some(value) + } else { + None + }; + initialize_eval_enum_case(enum_decl, case, backing_value, context, values)?; + } + Ok(()) +} + +/// Validates that one evaluated enum backing value matches the declared backing type. +fn validate_eval_enum_backing_value( + backing_type: Option, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(backing_type) = backing_type else { + return Err(EvalStatus::RuntimeFatal); + }; + let tag = values.type_tag(value)?; + match backing_type { + EvalEnumBackingType::Int if tag == EVAL_TAG_INT => Ok(()), + EvalEnumBackingType::String if tag == EVAL_TAG_STRING => Ok(()), + EvalEnumBackingType::Int | EvalEnumBackingType::String => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates and stores one enum case singleton object. +fn initialize_eval_enum_case( + enum_decl: &EvalEnum, + case: &EvalEnumCase, + backing_value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, enum_decl.name()); + let name = values.string(case.name())?; + values.property_set(object, "name", name)?; + if let Some(value) = backing_value { + values.property_set(object, "value", value)?; + if let Some(replaced) = context.set_enum_case_value(enum_decl.name(), case.name(), value) { + values.release(replaced)?; + } + } + if let Some(replaced) = context.set_enum_case(enum_decl.name(), case.name(), object) { + values.release(replaced)?; + } + Ok(()) +} + /// Initializes class-like constant cells for a newly declared eval class-like. fn initialize_eval_declared_constants( owner_name: &str, @@ -456,8 +622,10 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( let name = interface.name().trim_start_matches('\\'); if context.has_interface(name) || context.has_class(name) + || context.has_enum(name) || values.interface_exists(name)? || values.class_exists(name)? + || values.enum_exists(name)? { return Err(EvalStatus::RuntimeFatal); } @@ -498,9 +666,11 @@ pub(in crate::interpreter) fn execute_trait_decl_stmt( if context.has_trait(name) || context.has_class(name) || context.has_interface(name) + || context.has_enum(name) || values.trait_exists(name)? || values.class_exists(name)? || values.interface_exists(name)? + || values.enum_exists(name)? { return Err(EvalStatus::RuntimeFatal); } @@ -1040,6 +1210,9 @@ pub(in crate::interpreter) fn eval_property_set_result( let Some(class) = context.dynamic_object_class(identity) else { return values.property_set(object, property_name, value); }; + if context.has_enum(class.name()) { + return Err(EvalStatus::RuntimeFatal); + } if let Some((declaring_class, property)) = eval_dynamic_property_for_access(class.name(), property_name, context) { @@ -1096,6 +1269,9 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( _values: &mut impl RuntimeValueOps, ) -> Result { let class_name = resolve_eval_static_class_like_name(class_name, context)?; + if let Some(case) = context.enum_case(&class_name, constant_name) { + return Ok(case); + } let (declaring_class, constant) = context .class_constant(&class_name, constant_name) .ok_or(EvalStatus::RuntimeFatal)?; @@ -1146,6 +1322,15 @@ pub(in crate::interpreter) fn eval_static_method_call_result( values: &mut impl RuntimeValueOps, ) -> Result { let class_name = resolve_eval_static_class_name(class_name, context)?; + if context.has_enum(&class_name) && eval_enum_static_builtin_name(method_name).is_some() { + return eval_enum_builtin_static_method_result( + &class_name, + method_name, + evaluated_args, + context, + values, + ); + } let (declaring_class, method) = eval_dynamic_static_method_for_call(&class_name, method_name, context) .ok_or(EvalStatus::RuntimeFatal)?; @@ -1163,6 +1348,103 @@ pub(in crate::interpreter) fn eval_static_method_call_result( ) } +/// Returns a recognized enum-provided static method name. +fn eval_enum_static_builtin_name(method_name: &str) -> Option<&'static str> { + if method_name.eq_ignore_ascii_case("cases") { + Some("cases") + } else if method_name.eq_ignore_ascii_case("from") { + Some("from") + } else if method_name.eq_ignore_ascii_case("tryFrom") { + Some("tryFrom") + } else { + None + } +} + +/// Dispatches enum-provided static methods for eval-declared enums. +fn eval_enum_builtin_static_method_result( + enum_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_enum_static_builtin_name(method_name).ok_or(EvalStatus::RuntimeFatal)? { + "cases" => eval_enum_cases_result(enum_name, evaluated_args, context, values), + "from" => eval_enum_from_result(enum_name, evaluated_args, false, context, values), + "tryFrom" => eval_enum_from_result(enum_name, evaluated_args, true, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the indexed array returned by `EnumName::cases()`. +fn eval_enum_cases_result( + enum_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let enum_decl = context + .enum_decl(enum_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + let mut array = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let key = values.int(index as i64)?; + let case = context + .enum_case(enum_name, case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + array = values.array_set(array, key, case)?; + } + Ok(array) +} + +/// Evaluates `EnumName::from()` or `EnumName::tryFrom()` for eval-backed enums. +fn eval_enum_from_result( + enum_name: &str, + evaluated_args: Vec, + nullable_miss: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let enum_decl = context + .enum_decl(enum_name) + .ok_or(EvalStatus::RuntimeFatal)?; + if enum_decl.backing_type().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + let mut args = bind_evaluated_function_args(&[String::from("value")], evaluated_args)?; + let value = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + for case_name in case_names { + let case_value = context + .enum_case_value(enum_name, &case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let equal = values.compare(EvalBinOp::StrictEq, value, case_value)?; + if values.truthy(equal)? { + return context + .enum_case(enum_name, &case_name) + .ok_or(EvalStatus::RuntimeFatal); + } + } + if nullable_miss { + values.null() + } else { + Err(EvalStatus::RuntimeFatal) + } +} + /// Resolves a static method using private-method scope rules. fn eval_dynamic_static_method_for_call( class_name: &str, @@ -1253,7 +1535,7 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - if class.is_abstract() { + if class.is_abstract() || context.has_enum(class.name()) { return Err(EvalStatus::RuntimeFatal); } let object = values.new_object("stdClass")?; diff --git a/crates/elephc-eval/src/interpreter/tests/enums.rs b/crates/elephc-eval/src/interpreter/tests/enums.rs new file mode 100644 index 0000000000..a6fd30cfb7 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/enums.rs @@ -0,0 +1,124 @@ +//! Purpose: +//! Interpreter tests for eval-declared enum runtime behavior. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases verify enum singleton cases, class-like metadata, backed values, +//! and method/interface dispatch through the eval object path. + +use super::super::*; +use super::support::*; + +/// Verifies pure eval enums expose singleton cases and class-like introspection. +#[test] +fn execute_program_declares_pure_eval_enum_cases() { + let program = parse_fragment( + br#"enum EvalSuit { + case Hearts; + case Clubs; +} +$cases = EvalSuit::cases(); +echo enum_exists("evalsuit") ? "enum" : "bad"; echo ":"; +echo class_exists("EvalSuit") ? "class" : "bad"; echo ":"; +echo count($cases); echo ":"; +echo $cases[0] === EvalSuit::Hearts ? "same" : "bad"; echo ":"; +echo EvalSuit::Hearts->name; echo ":"; +return get_class(EvalSuit::Clubs);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "enum:class:2:same:Hearts:"); + assert_eq!( + values.get(result), + FakeValue::String("EvalSuit".to_string()) + ); +} + +/// Verifies backed eval enums expose values and `from` / `tryFrom` lookups. +#[test] +fn execute_program_declares_backed_eval_enum_cases() { + let program = parse_fragment( + br#"enum EvalColor: int { + case Red = 1; + case Green = 2; +} +echo EvalColor::Green->value; echo ":"; +echo EvalColor::from(value: 1) === EvalColor::Red ? "from" : "bad"; echo ":"; +return EvalColor::tryFrom(99);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:from:"); + assert_eq!(values.get(result), FakeValue::Null); +} + +/// Verifies eval enum methods, constants, and interface implementation dispatch. +#[test] +fn execute_program_dispatches_eval_enum_methods_and_interfaces() { + let program = parse_fragment( + br#"interface EvalLabel { + function label(); +} +enum EvalSuit implements EvalLabel { + case Hearts; + public const PREFIX = "suit"; + public function label() { return self::PREFIX . ":" . $this->name; } + public static function fallback() { return self::Hearts; } +} +echo is_a(EvalSuit::Hearts, "EvalLabel") ? "iface" : "bad"; echo ":"; +echo EvalSuit::Hearts->label(); echo ":"; +return EvalSuit::fallback() === EvalSuit::Hearts;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "iface:suit:Hearts:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval rejects enum members that conflict with PHP enum rules. +#[test] +fn execute_program_rejects_invalid_eval_enum_members() { + let program = parse_fragment( + br#"enum EvalInvalidEnum { + case Ready; + public const Ready = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("case and constant name collision should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let program = parse_fragment( + br#"enum EvalInvalidEnumMethod { + case Ready; + public static function cases() { return []; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("reserved enum method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index b87e4aa14e..2d9c071ca9 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -21,9 +21,9 @@ mod builtins_filesystem_metadata; mod builtins_filesystem_ops; mod builtins_json; mod builtins_language_constructs; -mod builtins_readline; mod builtins_math_formatting; mod builtins_process_pipes; +mod builtins_readline; mod builtins_scalars; mod builtins_spl_autoload; mod builtins_stream_contexts; @@ -39,6 +39,7 @@ mod class_constants; mod control_flow; mod core; mod dynamic_calls; +mod enums; mod expressions; mod functions_namespaces; mod method_arguments; diff --git a/crates/elephc-eval/src/interpreter/tests/support/conversions.rs b/crates/elephc-eval/src/interpreter/tests/support/conversions.rs index 2c96563092..538f762311 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/conversions.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/conversions.rs @@ -51,6 +51,14 @@ impl FakeOps { /// Compares fake scalar values by PHP strict tag and payload equality. pub(super) fn strict_eq(&self, left: RuntimeCellHandle, right: RuntimeCellHandle) -> bool { + if left == right + && matches!( + self.get(left), + FakeValue::Object(_) | FakeValue::Iterator { .. } + ) + { + return true; + } match (self.get(left), self.get(right)) { (FakeValue::Null, FakeValue::Null) => true, (FakeValue::Bool(left), FakeValue::Bool(right)) => left == right, diff --git a/crates/elephc-eval/src/parser/cursor.rs b/crates/elephc-eval/src/parser/cursor.rs index a7364f287c..13e602380b 100644 --- a/crates/elephc-eval/src/parser/cursor.rs +++ b/crates/elephc-eval/src/parser/cursor.rs @@ -126,7 +126,8 @@ pub(super) fn ident_eq(actual: &str, expected: &str) -> bool { /// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. pub(super) fn is_unsupported_statement_keyword(name: &str) -> bool { - ["enum"].iter().any(|keyword| ident_eq(name, keyword)) + let _ = name; + false } /// Returns true for class member modifiers outside the current eval class subset. diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 9e91f0faca..1351005c41 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -12,9 +12,9 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalExpr, - EvalInterface, EvalInterfaceMethod, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, - EvalVisibility, + EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, + EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInterface, EvalInterfaceMethod, EvalStmt, + EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalVisibility, }; use crate::lexer::TokenKind; @@ -48,6 +48,7 @@ impl Parser { self.parse_class_decl_stmt() } TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), + TokenKind::Ident(name) if ident_eq(name, "enum") => self.parse_enum_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_function_decl_stmt(), TokenKind::Ident(name) if ident_eq(name, "global") => self.parse_global_stmt(), TokenKind::Ident(name) if ident_eq(name, "if") => self.parse_if_stmt(), @@ -710,6 +711,110 @@ impl Parser { Ok(()) } + /// Parses `enum Name [: int|string] [implements Iface, ...] { ... }`. + pub(super) fn parse_enum_decl_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + let backing_type = self.parse_enum_backing_type()?; + let interfaces = self.parse_class_interface_clause()?; + self.expect(TokenKind::LBrace)?; + let mut cases = Vec::new(); + let mut constants = Vec::new(); + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_enum_member(&mut cases, &mut constants, &mut methods)?; + } + self.consume_semicolon(); + Ok(vec![EvalStmt::EnumDecl(EvalEnum::with_members( + name, + backing_type, + interfaces, + cases, + constants, + methods, + ))]) + } + + /// Parses an optional backed-enum scalar type after the enum name. + pub(super) fn parse_enum_backing_type( + &mut self, + ) -> Result, EvalParseError> { + if !self.consume(TokenKind::Colon) { + return Ok(None); + } + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let backing_type = if ident_eq(name, "int") { + EvalEnumBackingType::Int + } else if ident_eq(name, "string") { + EvalEnumBackingType::String + } else { + return Err(EvalParseError::UnsupportedConstruct); + }; + self.advance(); + Ok(Some(backing_type)) + } + + /// Parses one enum case, constant, or method declaration. + pub(super) fn parse_enum_member( + &mut self, + cases: &mut Vec, + constants: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) { + cases.push(self.parse_enum_case_decl()?); + return Ok(()); + } + let (visibility, is_static, is_abstract, is_final) = self.parse_class_member_modifiers()?; + if is_abstract { + return Err(EvalParseError::UnsupportedConstruct); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + constants + .push(self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))?); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + methods.push(self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + false, + is_final, + )?); + return Ok(()); + } + Err(EvalParseError::UnsupportedConstruct) + } + + /// Parses `case Name;` or `case Name = expr;` inside an eval enum body. + pub(super) fn parse_enum_case_decl(&mut self) -> Result { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let value = if self.consume(TokenKind::Equal) { + Some(self.parse_expr()?) + } else { + None + }; + self.expect_semicolon()?; + Ok(EvalEnumCase::new(name, value)) + } + /// Parses `interface Name [extends Parent, ...] { function name(...); }`. pub(super) fn parse_interface_decl_stmt(&mut self) -> Result, EvalParseError> { self.advance(); diff --git a/crates/elephc-eval/src/parser/tests/enums.rs b/crates/elephc-eval/src/parser/tests/enums.rs new file mode 100644 index 0000000000..21ce114879 --- /dev/null +++ b/crates/elephc-eval/src/parser/tests/enums.rs @@ -0,0 +1,76 @@ +//! Purpose: +//! Parser tests for eval-declared pure and backed enum declarations. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover enum cases, backing types, interfaces, constants, and methods. + +use super::support::*; + +/// Verifies pure enum cases lower to dynamic enum metadata. +#[test] +fn parse_fragment_accepts_pure_enum_declaration_source() { + let program = + parse_fragment(b"enum EvalSuit { case Hearts; case Clubs; }").expect("parse eval enum"); + assert_eq!( + program.statements(), + &[EvalStmt::EnumDecl(EvalEnum::new( + "EvalSuit", + None, + vec![ + EvalEnumCase::new("Hearts", None), + EvalEnumCase::new("Clubs", None), + ], + ))] + ); +} + +/// Verifies backed enum metadata preserves interfaces, case values, constants, and methods. +#[test] +fn parse_fragment_accepts_backed_enum_members() { + let program = parse_fragment( + br#"enum EvalColor: string implements EvalLabel { + case Red = "r"; + public const PREFIX = "color"; + public function label() { return self::PREFIX . ":" . $this->name; } +}"#, + ) + .expect("parse backed eval enum"); + assert_eq!( + program.statements(), + &[EvalStmt::EnumDecl(EvalEnum::with_members( + "EvalColor", + Some(EvalEnumBackingType::String), + vec!["EvalLabel".to_string()], + vec![EvalEnumCase::new( + "Red", + Some(EvalExpr::Const(EvalConst::String("r".to_string()))), + )], + vec![EvalClassConstant::new( + "PREFIX", + EvalExpr::Const(EvalConst::String("color".to_string())), + )], + vec![EvalClassMethod::new( + "label", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::ClassConstantFetch { + class_name: "self".to_string(), + constant: "PREFIX".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::String(":".to_string()))), + }), + right: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "name".to_string(), + }), + }))], + )], + ))] + ); +} diff --git a/crates/elephc-eval/src/parser/tests/mod.rs b/crates/elephc-eval/src/parser/tests/mod.rs index 7a69de2972..476774d0c7 100644 --- a/crates/elephc-eval/src/parser/tests/mod.rs +++ b/crates/elephc-eval/src/parser/tests/mod.rs @@ -16,6 +16,7 @@ mod calls; mod class_constants; mod classes_errors; mod control_statements; +mod enums; mod exceptions_control; mod magic_comments; mod namespaces; diff --git a/docs/php/eval.md b/docs/php/eval.md index cb4046f7a4..9854aa6650 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -50,6 +50,7 @@ such a local alias removes the alias without unsetting the global value. | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | | Classes | Eval fragments can declare classes with properties, methods, `__construct()`, inheritance, visibility, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | +| Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -148,6 +149,16 @@ generated AOT class/interface metadata and eval-created object metadata. `interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated AOT metadata. Eval-declared classes, interfaces, traits, and class aliases are visible through the corresponding eval and post-barrier native metadata probes. +Eval-declared enums are visible inside eval through `enum_exists()` and through +class-like probes such as `class_exists()`. + +Eval-declared enums share the dynamic class-like metadata path used by +eval-declared classes. Pure and backed enum cases are singleton objects, +`EnumName::cases()` returns those singletons in declaration order, and backed +`EnumName::from()` / `EnumName::tryFrom()` compare against the declared scalar +values. Enums can implement eval-declared or generated interfaces and can use +their own instance/static methods and class constants. Direct `new EnumName()` +and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native methods are bridged to eval. Public zero-, one-, or two-scalar-argument method @@ -272,9 +283,10 @@ arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are eval-declared enums, property hooks, -attributes/reflection metadata, readonly semantics, and generated/AOT dynamic -static-method call forms. +remaining class-system gaps are enum trait-use declarations, enum `from()` +misses as catchable `ValueError` objects, property hooks, attributes/reflection +metadata, readonly semantics, and generated/AOT dynamic static-method call +forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index de94399dca..5ea2d47019 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4518,6 +4518,33 @@ echo function_exists("trait_exists"); echo function_exists("enum_exists");'); assert_eq!(out, "TTtEEeTEte11"); } +/// Verifies eval fragments can declare and use backed enums through the bridge. +#[test] +fn test_eval_fragment_declares_enum_cases_and_methods() { + let out = compile_and_run( + r#"name . ":" . $this->value; } + public static function fallback() { return self::Red; } +} +$cases = EvalDynColor::cases(); +echo enum_exists("evaldyncolor") ? "E" : "e"; +echo class_exists("EvalDynColor") ? "C" : "c"; +echo count($cases); +echo $cases[1] === EvalDynColor::Green ? "G" : "g"; +echo EvalDynColor::Green->label(); +echo EvalDynColor::from("r") === EvalDynColor::Red ? "F" : "f"; +echo is_null(EvalDynColor::tryFrom("missing")) ? "N" : "n"; +echo is_a(EvalDynColor::Red, "EvalDynLabel") ? "I" : "i";'); +"#, + ); + assert_eq!(out, "EC2Gcolor:Green:gFNI"); +} + /// Verifies eval `is_a()` and `is_subclass_of()` use generated AOT relation metadata. #[test] fn test_eval_fragment_is_a_relation_probes_aot_metadata() { From 1441ae031335ab9480c1c6435af69174b9a25847 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 18:10:11 +0200 Subject: [PATCH 0337/1208] Support eval enum ValueError misses --- .../elephc-eval/src/interpreter/statements.rs | 48 ++- .../src/interpreter/tests/enums.rs | 29 ++ .../src/interpreter/tests/support/mod.rs | 1 + .../interpreter/tests/support/object_ops.rs | 82 +++++- docs/php/eval.md | 15 +- src/codegen/eval_method_helpers.rs | 276 ++++++++++++++++-- tests/codegen/eval.rs | 22 ++ 7 files changed, 434 insertions(+), 39 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 8bf8b6fbbf..85f6fe69c2 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1411,15 +1411,14 @@ fn eval_enum_from_result( enum_name: &str, evaluated_args: Vec, nullable_miss: bool, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let enum_decl = context .enum_decl(enum_name) .ok_or(EvalStatus::RuntimeFatal)?; - if enum_decl.backing_type().is_none() { - return Err(EvalStatus::RuntimeFatal); - } + let backing_type = enum_decl.backing_type().ok_or(EvalStatus::RuntimeFatal)?; + let enum_display_name = enum_decl.name().trim_start_matches('\\').to_string(); let case_names = enum_decl .cases() .iter() @@ -1441,10 +1440,49 @@ fn eval_enum_from_result( if nullable_miss { values.null() } else { - Err(EvalStatus::RuntimeFatal) + let message = eval_enum_invalid_backing_value_message( + &enum_display_name, + backing_type, + value, + values, + )?; + eval_throw_value_error(&message, context, values) } } +/// Builds PHP's backed-enum `ValueError` message for an unmatched enum value. +fn eval_enum_invalid_backing_value_message( + enum_name: &str, + backing_type: EvalEnumBackingType, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let value = String::from_utf8_lossy(&bytes); + let value = match backing_type { + EvalEnumBackingType::Int => value.into_owned(), + EvalEnumBackingType::String => format!("\"{}\"", value), + }; + Ok(format!( + "{} is not a valid backing value for enum {}", + value, enum_name + )) +} + +/// Creates and schedules a `ValueError` through eval's normal Throwable channel. +fn eval_throw_value_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("ValueError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + /// Resolves a static method using private-method scope rules. fn eval_dynamic_static_method_for_call( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/enums.rs b/crates/elephc-eval/src/interpreter/tests/enums.rs index a6fd30cfb7..a4dc4045d3 100644 --- a/crates/elephc-eval/src/interpreter/tests/enums.rs +++ b/crates/elephc-eval/src/interpreter/tests/enums.rs @@ -62,6 +62,35 @@ return EvalColor::tryFrom(99);"#, assert_eq!(values.get(result), FakeValue::Null); } +/// Verifies eval enum `from()` misses throw catchable `ValueError` objects. +#[test] +fn execute_program_enum_from_miss_throws_value_error() { + let program = parse_fragment( + br#"enum EvalColor: int { + case Red = 1; +} +try { + EvalColor::from(99); + echo "bad"; +} catch (ValueError $e) { + echo get_class($e) . ":" . $e->getMessage(); + return true; +} +return false;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ValueError:99 is not a valid backing value for enum EvalColor" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval enum methods, constants, and interface implementation dispatch. #[test] fn execute_program_dispatches_eval_enum_methods_and_interfaces() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/mod.rs b/crates/elephc-eval/src/interpreter/tests/support/mod.rs index ae6195e370..b0b59d733f 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/mod.rs @@ -53,6 +53,7 @@ pub(super) enum FakeValue { pub(super) struct FakeOps { pub(super) next_id: usize, pub(super) values: HashMap, + pub(super) object_classes: HashMap, pub(super) output: String, pub(super) releases: Vec, pub(super) warnings: Vec, diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 8f69b489a0..c16928166f 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -77,7 +77,8 @@ impl FakeOps { method: &str, args: Vec, ) -> Result { - match (self.get(object), method) { + let method = method.to_ascii_lowercase(); + match (self.get(object), method.as_str()) { (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { let id = object.as_ptr() as usize; let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { @@ -98,6 +99,12 @@ impl FakeOps { self.null() } (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), + (FakeValue::Object(properties), "getmessage") if args.is_empty() => { + Self::object_property(&properties, "message").map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "getcode") if args.is_empty() => { + Self::object_property(&properties, "code").map_or_else(|| self.int(0), Ok) + } (FakeValue::Object(properties), "read_x") => { if !args.is_empty() { return Err(EvalStatus::UnsupportedConstruct); @@ -139,9 +146,12 @@ impl FakeOps { /// Creates one fake object for eval `new` unit tests. pub(super) fn runtime_new_object( &mut self, - _class_name: &str, + class_name: &str, ) -> Result { - Ok(self.alloc(FakeValue::Object(Vec::new()))) + let object = self.alloc(FakeValue::Object(Vec::new())); + self.object_classes + .insert(object.as_ptr() as usize, class_name.to_string()); + Ok(object) } /// Applies fake constructor side effects for eval `new` unit tests. pub(super) fn runtime_construct_object( @@ -150,9 +160,31 @@ impl FakeOps { args: Vec, ) -> Result<(), EvalStatus> { let id = object.as_ptr() as usize; + let class_name = self.object_classes.get(&id).cloned(); let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { return Err(EvalStatus::UnsupportedConstruct); }; + if class_name + .as_deref() + .is_some_and(fake_runtime_exception_like_class) + { + if let Some(message) = args.first().copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "message") + { + *value = message; + } else { + properties.push(("message".to_string(), message)); + } + } + if let Some(code) = args.get(1).copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "code") { + *value = code; + } else { + properties.push(("code".to_string(), code)); + } + } + return Ok(()); + } if let Some(first) = args.first().copied() { if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { *value = first; @@ -185,7 +217,14 @@ impl FakeOps { target_class: &str, exclude_self: bool, ) -> Result { + let object_id = object_or_class.as_ptr() as usize; match self.get(object_or_class) { + FakeValue::Object(_) if self.object_classes.contains_key(&object_id) => Ok(self + .object_classes + .get(&object_id) + .is_some_and(|class_name| { + fake_runtime_object_is_a(class_name, target_class, exclude_self) + })), FakeValue::Object(_) if target_class.eq_ignore_ascii_case("Exception") || target_class.eq_ignore_ascii_case("Throwable") => @@ -204,6 +243,10 @@ impl FakeOps { &mut self, object: RuntimeCellHandle, ) -> Result { + let object_id = object.as_ptr() as usize; + if let Some(class_name) = self.object_classes.get(&object_id).cloned() { + return self.string(&class_name); + } match self.get(object) { FakeValue::Object(_) => self.string("stdClass"), FakeValue::Iterator { .. } => self.string("Iterator"), @@ -234,3 +277,36 @@ impl FakeOps { } } } + +/// Returns whether a fake runtime class stores PHP Throwable constructor state. +fn fake_runtime_exception_like_class(class_name: &str) -> bool { + ["Exception", "JsonException", "Error", "ValueError"] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)) +} + +/// Checks the small fake Throwable inheritance graph used by eval interpreter tests. +fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: bool) -> bool { + if class_name.eq_ignore_ascii_case(target_class) { + return !exclude_self; + } + if class_name.eq_ignore_ascii_case("KnownClass") + && target_class.eq_ignore_ascii_case("ParentClass") + { + return true; + } + if target_class.eq_ignore_ascii_case("Throwable") { + return fake_runtime_exception_like_class(class_name); + } + if target_class.eq_ignore_ascii_case("Exception") { + return ["Exception", "JsonException"] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)); + } + if target_class.eq_ignore_ascii_case("Error") { + return ["Error", "ValueError"] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)); + } + false +} diff --git a/docs/php/eval.md b/docs/php/eval.md index 9854aa6650..db25b13e13 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -156,9 +156,11 @@ Eval-declared enums share the dynamic class-like metadata path used by eval-declared classes. Pure and backed enum cases are singleton objects, `EnumName::cases()` returns those singletons in declaration order, and backed `EnumName::from()` / `EnumName::tryFrom()` compare against the declared scalar -values. Enums can implement eval-declared or generated interfaces and can use -their own instance/static methods and class constants. Direct `new EnumName()` -and property writes to enum cases are rejected. +values. `EnumName::from()` misses throw a catchable `ValueError`, while +`EnumName::tryFrom()` misses return `null`. Enums can implement eval-declared +or generated interfaces and can use their own instance/static methods and class +constants. Direct `new EnumName()` and property writes to enum cases are +rejected. Public declared property reads/writes through `$this->property` from native methods are bridged to eval. Public zero-, one-, or two-scalar-argument method @@ -283,10 +285,9 @@ arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are enum trait-use declarations, enum `from()` -misses as catchable `ValueError` objects, property hooks, attributes/reflection -metadata, readonly semantics, and generated/AOT dynamic static-method call -forms. +remaining class-system gaps are enum trait-use declarations, property hooks, +attributes/reflection metadata, readonly semantics, and generated/AOT dynamic +static-method call forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index cfe9664a70..36812e8a4a 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -15,8 +15,8 @@ use std::collections::BTreeMap; use crate::codegen::abi; use crate::codegen::data_section::DataSection; -use crate::codegen::emit_box_current_value_as_mixed; use crate::codegen::emit::Emitter; +use crate::codegen::emit_box_current_value_as_mixed; use crate::codegen::platform::Arch; use crate::ir::{Function, LocalKind, Module}; use crate::names::method_symbol; @@ -35,6 +35,29 @@ struct EvalMethodSlot { } const MAX_EVAL_METHOD_ARGS: usize = 2; +const BUILTIN_THROWABLE_METHOD_CLASSES: &[&str] = &[ + "Error", + "TypeError", + "ValueError", + "Exception", + "LogicException", + "BadFunctionCallException", + "BadMethodCallException", + "DomainException", + "InvalidArgumentException", + "LengthException", + "OutOfRangeException", + "RuntimeException", + "OutOfBoundsException", + "OverflowException", + "RangeException", + "UnderflowException", + "UnexpectedValueException", + "JsonException", + "FiberError", +]; +const BUILTIN_THROWABLE_GET_MESSAGE_LABEL: &str = "__elephc_eval_builtin_throwable_getmessage"; +const BUILTIN_THROWABLE_GET_CODE_LABEL: &str = "__elephc_eval_builtin_throwable_getcode"; /// Emits eval method-call helpers when any lowered function owns an eval context. pub(super) fn emit_eval_method_helpers( @@ -46,7 +69,8 @@ pub(super) fn emit_eval_method_helpers( return; } let slots = collect_eval_method_slots(module); - emit_method_call_helper(module, emitter, data, &slots); + let builtin_throwable_class_ids = collect_builtin_throwable_method_class_ids(module); + emit_method_call_helper(module, emitter, data, &slots, &builtin_throwable_class_ids); } /// Returns true when the EIR module contains a function that can call eval. @@ -69,15 +93,12 @@ fn all_module_functions(module: &Module) -> impl Iterator { /// Returns true when a function has hidden eval state locals. fn function_uses_eval(function: &Function) -> bool { - function - .locals - .iter() - .any(|local| { - matches!( - local.kind, - LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope - ) - }) + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) } /// Collects public bridge-supported instance methods backed by emitted EIR symbols. @@ -92,6 +113,18 @@ fn collect_eval_method_slots(module: &Module) -> Vec { slots } +/// Collects compact builtin Throwable class ids that eval can inspect directly. +fn collect_builtin_throwable_method_class_ids(module: &Module) -> Vec { + let mut class_ids = BUILTIN_THROWABLE_METHOD_CLASSES + .iter() + .filter_map(|class_name| module.class_infos.get(*class_name)) + .map(|class_info| class_info.class_id) + .collect::>(); + class_ids.sort_unstable(); + class_ids.dedup(); + class_ids +} + /// Adds bridge-supported public methods for one class. fn collect_class_method_slots( class_name: &str, @@ -121,11 +154,7 @@ fn collect_class_method_slots( class_name: class_name.to_string(), method: method.clone(), impl_class: impl_class.to_string(), - params: sig - .params - .iter() - .map(|(_, ty)| ty.codegen_repr()) - .collect(), + params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), }); } @@ -144,10 +173,7 @@ fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_METHOD_ARGS && sig.variadic.is_none() && sig.ref_params.iter().all(|is_ref| !*is_ref) - && sig - .params - .iter() - .all(|(_, ty)| method_param_supported(ty)) + && sig.params.iter().all(|(_, ty)| method_param_supported(ty)) } /// Returns true for an eval-supplied method argument type supported by this bridge. @@ -181,13 +207,18 @@ fn emit_method_call_helper( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalMethodSlot], + builtin_throwable_class_ids: &[u64], ) { emitter.blank(); emitter.comment("--- eval bridge: user method call ---"); label_c_global(module, emitter, "__elephc_eval_value_method_call"); match module.target.arch { - Arch::AArch64 => emit_method_call_aarch64(module, emitter, data, slots), - Arch::X86_64 => emit_method_call_x86_64(module, emitter, data, slots), + Arch::AArch64 => { + emit_method_call_aarch64(module, emitter, data, slots, builtin_throwable_class_ids) + } + Arch::X86_64 => { + emit_method_call_x86_64(module, emitter, data, slots, builtin_throwable_class_ids) + } } } @@ -197,6 +228,7 @@ fn emit_method_call_aarch64( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalMethodSlot], + builtin_throwable_class_ids: &[u64], ) { let fail_label = "__elephc_eval_value_method_call_fail"; let done_label = "__elephc_eval_value_method_call_done"; @@ -211,8 +243,15 @@ fn emit_method_call_aarch64( emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers cannot dispatch instance methods emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for method calls + emit_aarch64_builtin_throwable_method_dispatch( + module, + emitter, + data, + builtin_throwable_class_ids, + ); emit_aarch64_method_dispatch(module, emitter, data, slots); emitter.instruction(&format!("b {}", fail_label)); // no supported public method matched the request + emit_aarch64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); emit_aarch64_method_bodies(module, emitter, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure @@ -228,6 +267,7 @@ fn emit_method_call_x86_64( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalMethodSlot], + builtin_throwable_class_ids: &[u64], ) { let fail_label = "__elephc_eval_value_method_call_fail_x"; let done_label = "__elephc_eval_value_method_call_done_x"; @@ -244,8 +284,15 @@ fn emit_method_call_x86_64( emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers cannot dispatch instance methods emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for method calls + emit_x86_64_builtin_throwable_method_dispatch( + module, + emitter, + data, + builtin_throwable_class_ids, + ); emit_x86_64_method_dispatch(module, emitter, data, slots); emitter.instruction(&format!("jmp {}", fail_label)); // no supported public method matched the request + emit_x86_64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); emit_x86_64_method_bodies(module, emitter, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure @@ -297,6 +344,105 @@ fn emit_x86_64_method_dispatch( } } +/// Emits ARM64 class-id and method-name dispatch for compact Throwable methods. +fn emit_aarch64_builtin_throwable_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_ids: &[u64], +) { + for class_id in class_ids { + let next_label = format!("__elephc_eval_builtin_throwable_method_next_{}", class_id); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer before this builtin Throwable test + emitter.instruction("ldr x9, [x9]"); // load the receiver class id for builtin Throwable dispatch + abi::emit_load_int_immediate(emitter, "x10", *class_id as i64); + emitter.instruction("cmp x9, x10"); // compare receiver class id against this builtin Throwable class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next builtin Throwable class when ids differ + emit_aarch64_builtin_throwable_method_name_branch( + module, + emitter, + data, + "getmessage", + BUILTIN_THROWABLE_GET_MESSAGE_LABEL, + ); + emit_aarch64_builtin_throwable_method_name_branch( + module, + emitter, + data, + "getcode", + BUILTIN_THROWABLE_GET_CODE_LABEL, + ); + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-id and method-name dispatch for compact Throwable methods. +fn emit_x86_64_builtin_throwable_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_ids: &[u64], +) { + for class_id in class_ids { + let next_label = format!("__elephc_eval_builtin_throwable_method_next_{}_x", class_id); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer before this builtin Throwable test + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the receiver class id for builtin Throwable dispatch + abi::emit_load_int_immediate(emitter, "r10", *class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this builtin Throwable class + emitter.instruction(&format!("jne {}", next_label)); // try the next builtin Throwable class when ids differ + emit_x86_64_builtin_throwable_method_name_branch( + module, + emitter, + data, + "getmessage", + BUILTIN_THROWABLE_GET_MESSAGE_LABEL, + ); + emit_x86_64_builtin_throwable_method_name_branch( + module, + emitter, + data, + "getcode", + BUILTIN_THROWABLE_GET_CODE_LABEL, + ); + emitter.label(&next_label); + } +} + +/// Emits one ARM64 method-name comparison for a compact Throwable method. +fn emit_aarch64_builtin_throwable_method_name_branch( + _module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + method_key: &str, + target_label: &str, +) { + let (label, len) = data.add_string(method_key.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested method-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested method-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare requested method name with this Throwable method + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the compact Throwable method when names match +} + +/// Emits one x86_64 method-name comparison for a compact Throwable method. +fn emit_x86_64_builtin_throwable_method_name_branch( + _module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + method_key: &str, + target_label: &str, +) { + let (label, len) = data.add_string(method_key.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested method-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare requested method name with this Throwable method + emitter.instruction("test rax, rax"); // check whether the method names matched + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the compact Throwable method when names match +} + /// Emits one ARM64 method-name comparison and branch to the matching body. fn emit_aarch64_method_name_compare( module: &Module, @@ -310,7 +456,8 @@ fn emit_aarch64_method_name_compare( abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); emitter.instruction("bl __rt_str_eq"); // compare requested method name with this public method - emitter.instruction(&format!("cbnz x0, {}", method_body_label(module, slot))); // dispatch to the method body when the names match + emitter.instruction(&format!("cbnz x0, {}", method_body_label(module, slot))); + // dispatch to the method body when the names match } /// Emits one x86_64 method-name comparison and branch to the matching body. @@ -330,6 +477,84 @@ fn emit_x86_64_method_name_compare( emitter.instruction(&format!("jne {}", method_body_label(module, slot))); // dispatch to the method body when the names match } +/// Emits ARM64 bodies for compact Throwable methods used by eval. +fn emit_aarch64_builtin_throwable_method_bodies( + module: &Module, + emitter: &mut Emitter, + done_label: &str, + fail_label: &str, +) { + emitter.label(BUILTIN_THROWABLE_GET_MESSAGE_LABEL); + emit_aarch64_validate_builtin_throwable_method_arg_count(module, emitter, fail_label); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for getMessage() + emitter.instruction("ldr x1, [x9, #8]"); // load Throwable message pointer + emitter.instruction("ldr x2, [x9, #16]"); // load Throwable message length + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // box the Throwable message as a Mixed string + emitter.instruction(&format!("b {}", done_label)); // return the boxed Throwable method result + + emitter.label(BUILTIN_THROWABLE_GET_CODE_LABEL); + emit_aarch64_validate_builtin_throwable_method_arg_count(module, emitter, fail_label); + emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for getCode() + emitter.instruction("ldr x1, [x9, #24]"); // load Throwable integer code + emitter.instruction("mov x2, xzr"); // integer payloads do not use a high word + emitter.instruction("mov x0, #0"); // runtime tag 0 = integer + emitter.instruction("bl __rt_mixed_from_value"); // box the Throwable code as a Mixed integer + emitter.instruction(&format!("b {}", done_label)); // return the boxed Throwable method result +} + +/// Emits x86_64 bodies for compact Throwable methods used by eval. +fn emit_x86_64_builtin_throwable_method_bodies( + module: &Module, + emitter: &mut Emitter, + done_label: &str, + fail_label: &str, +) { + emitter.label(BUILTIN_THROWABLE_GET_MESSAGE_LABEL); + emit_x86_64_validate_builtin_throwable_method_arg_count(module, emitter, fail_label); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for getMessage() + emitter.instruction("mov rdi, QWORD PTR [r10 + 8]"); // load Throwable message pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + 16]"); // load Throwable message length + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // box the Throwable message as a Mixed string + emitter.instruction(&format!("jmp {}", done_label)); // return the boxed Throwable method result + + emitter.label(BUILTIN_THROWABLE_GET_CODE_LABEL); + emit_x86_64_validate_builtin_throwable_method_arg_count(module, emitter, fail_label); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for getCode() + emitter.instruction("mov rdi, QWORD PTR [r10 + 24]"); // load Throwable integer code + emitter.instruction("xor esi, esi"); // integer payloads do not use a high word + emitter.instruction("xor eax, eax"); // runtime tag 0 = integer + emitter.instruction("call __rt_mixed_from_value"); // box the Throwable code as a Mixed integer + emitter.instruction(&format!("jmp {}", done_label)); // return the boxed Throwable method result +} + +/// Emits ARM64 zero-argument validation for compact Throwable eval methods. +fn emit_aarch64_validate_builtin_throwable_method_arg_count( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for Throwable method arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("cmp x0, #0"); // compact Throwable methods accept no eval arguments + emitter.instruction(&format!("b.ne {}", fail_label)); // reject unsupported Throwable method arguments from eval +} + +/// Emits x86_64 zero-argument validation for compact Throwable eval methods. +fn emit_x86_64_validate_builtin_throwable_method_arg_count( + module: &Module, + emitter: &mut Emitter, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for Throwable method arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("test rax, rax"); // compact Throwable methods accept no eval arguments + emitter.instruction(&format!("jne {}", fail_label)); // reject unsupported Throwable method arguments from eval +} + /// Emits ARM64 method-call bodies for every bridge-supported method. fn emit_aarch64_method_bodies( module: &Module, @@ -532,7 +757,10 @@ fn emit_box_method_result(module: &Module, emitter: &mut Emitter, slot: &EvalMet fn grouped_slots(slots: &[EvalMethodSlot]) -> BTreeMap> { let mut grouped = BTreeMap::new(); for slot in slots { - grouped.entry(slot.class_id).or_insert_with(Vec::new).push(slot); + grouped + .entry(slot.class_id) + .or_insert_with(Vec::new) + .push(slot); } grouped } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5ea2d47019..adedf82b9b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4545,6 +4545,28 @@ echo is_a(EvalDynColor::Red, "EvalDynLabel") ? "I" : "i";'); assert_eq!(out, "EC2Gcolor:Green:gFNI"); } +/// Verifies eval enum `from()` misses throw catchable `ValueError` objects. +#[test] +fn test_eval_fragment_enum_from_miss_throws_value_error() { + let out = compile_and_run( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "ValueError:\"live\" is not a valid backing value for enum EvalDynStatus" + ); +} + /// Verifies eval `is_a()` and `is_subclass_of()` use generated AOT relation metadata. #[test] fn test_eval_fragment_is_a_relation_probes_aot_metadata() { From faaa092497380d506e0dd9a5f13cfaf185dc5195 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 18:22:22 +0200 Subject: [PATCH 0338/1208] Support eval readonly properties --- crates/elephc-eval/src/eval_ir.rs | 18 ++++++ .../elephc-eval/src/interpreter/statements.rs | 35 ++++++++++++ .../src/interpreter/tests/classes.rs | 56 +++++++++++++++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + crates/elephc-eval/src/parser/statements.rs | 53 ++++++++++++++---- .../src/parser/tests/classes_errors.rs | 31 ++++++++++ docs/php/eval.md | 18 +++--- tests/codegen/eval.rs | 38 +++++++++++++ 8 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/tests/classes.rs diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 0bccbece0b..d7a9360537 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -724,6 +724,7 @@ pub struct EvalClassProperty { name: String, visibility: EvalVisibility, is_static: bool, + is_readonly: bool, default: Option, } @@ -748,11 +749,23 @@ impl EvalClassProperty { visibility: EvalVisibility, is_static: bool, default: Option, + ) -> Self { + Self::with_visibility_static_and_readonly(name, visibility, is_static, false, default) + } + + /// Creates an eval class property with explicit storage and readonly metadata. + pub fn with_visibility_static_and_readonly( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + is_readonly: bool, + default: Option, ) -> Self { Self { name: name.into(), visibility, is_static, + is_readonly, default, } } @@ -772,6 +785,11 @@ impl EvalClassProperty { self.is_static } + /// Returns whether this property was declared `readonly`. + pub const fn is_readonly(&self) -> bool { + self.is_readonly + } + /// Returns the property initializer expression, when one was declared. pub fn default(&self) -> Option<&EvalExpr> { self.default.as_ref() diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 85f6fe69c2..8abdba299b 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1217,10 +1217,42 @@ pub(in crate::interpreter) fn eval_property_set_result( eval_dynamic_property_for_access(class.name(), property_name, context) { validate_eval_member_access(&declaring_class, property.visibility(), context)?; + validate_eval_readonly_property_write(&declaring_class, &property, context)?; } values.property_set(object, property_name, value) } +/// Rejects writes to readonly eval-declared properties outside their declaring constructor. +fn validate_eval_readonly_property_write( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !property.is_readonly() { + return Ok(()); + } + current_eval_method_is_declaring_constructor(declaring_class, context) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns true while executing `__construct` for the property declaring class. +fn current_eval_method_is_declaring_constructor( + declaring_class: &str, + context: &ElephcEvalContext, +) -> bool { + let Some(current_class) = context.current_class_scope() else { + return false; + }; + if !same_eval_class_name(current_class, declaring_class) { + return false; + } + context + .current_function() + .and_then(|function| function.rsplit_once("::")) + .is_some_and(|(_, method)| method.eq_ignore_ascii_case("__construct")) +} + /// Resolves the property metadata visible from the current class scope, if any. fn eval_dynamic_property_for_access( object_class_name: &str, @@ -1307,6 +1339,7 @@ pub(in crate::interpreter) fn eval_static_property_set_result( return Err(EvalStatus::RuntimeFatal); } validate_eval_member_access(&declaring_class, property.visibility(), context)?; + validate_eval_readonly_property_write(&declaring_class, &property, context)?; if let Some(replaced) = context.set_static_property(&declaring_class, property.name(), value) { values.release(replaced)?; } @@ -1591,6 +1624,8 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( { let value = if let Some(default) = property.default() { eval_expr(default, context, caller_scope, values)? + } else if property.is_readonly() { + continue; } else { values.null()? }; diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs new file mode 100644 index 0000000000..bf35781213 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Interpreter tests for eval-declared class runtime behavior. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - These cases cover class property semantics that need eval runtime state. + +use super::super::*; +use super::support::*; + +/// Verifies readonly eval properties can be initialized inside their constructor. +#[test] +fn execute_program_initializes_readonly_property_in_constructor() { + let program = parse_fragment( + br#"class EvalReadonlyBox { + public readonly int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +$box = new EvalReadonlyBox(7); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies readonly eval properties reject writes outside the declaring constructor. +#[test] +fn execute_program_rejects_readonly_property_write_after_constructor() { + let program = parse_fragment( + br#"class EvalReadonlyBox { + public readonly int $id; + public function __construct($id) { $this->id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyBox(7); +$box->replace(8);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly property write should fail outside constructor"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 2d9c071ca9..0eb9d4642d 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -36,6 +36,7 @@ mod builtins_strings_text; mod builtins_symbols; mod builtins_system_network; mod class_constants; +mod classes; mod control_flow; mod core; mod dynamic_calls; diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 1351005c41..13ba5911a6 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -354,7 +354,8 @@ impl Parser { traits: &mut Vec, trait_adaptations: &mut Vec, ) -> Result<(), EvalParseError> { - let (visibility, is_static, is_abstract, is_final) = self.parse_class_member_modifiers()?; + let (visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); @@ -364,6 +365,7 @@ impl Parser { && !is_static && !is_abstract && !is_final + && !is_readonly && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) { self.parse_class_trait_use(traits, trait_adaptations)?; @@ -371,7 +373,7 @@ impl Parser { } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_abstract || is_final { + if is_static || is_abstract || is_final || is_readonly { return Err(EvalParseError::UnsupportedConstruct); } constants @@ -380,6 +382,9 @@ impl Parser { } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } methods.push(self.parse_class_method_decl( visibility.unwrap_or(EvalVisibility::Public), is_static, @@ -393,7 +398,7 @@ impl Parser { if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl(visibility, is_static)?); + properties.push(self.parse_class_property_decl(visibility, is_static, is_readonly)?); Ok(()) } @@ -538,11 +543,12 @@ impl Parser { /// Parses method modifiers supported by eval class declarations. pub(super) fn parse_class_member_modifiers( &mut self, - ) -> Result<(Option, bool, bool, bool), EvalParseError> { + ) -> Result<(Option, bool, bool, bool, bool), EvalParseError> { let mut visibility = None; let mut is_static = false; let mut is_abstract = false; let mut is_final = false; + let mut is_readonly = false; loop { match self.current() { TokenKind::Ident(name) if ident_eq(name, "public") => { @@ -587,10 +593,17 @@ impl Parser { is_final = true; self.advance(); } + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + is_readonly = true; + self.advance(); + } TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { return Err(EvalParseError::UnsupportedConstruct); } - _ => return Ok((visibility, is_static, is_abstract, is_final)), + _ => return Ok((visibility, is_static, is_abstract, is_final, is_readonly)), } } } @@ -633,7 +646,11 @@ impl Parser { &mut self, visibility: EvalVisibility, is_static: bool, + is_readonly: bool, ) -> Result { + if is_static && is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } self.skip_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -641,13 +658,20 @@ impl Parser { let name = name.clone(); self.advance(); let default = if self.consume(TokenKind::Equal) { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } Some(self.parse_expr()?) } else { None }; self.expect_semicolon()?; - Ok(EvalClassProperty::with_visibility_and_static( - name, visibility, is_static, default, + Ok(EvalClassProperty::with_visibility_static_and_readonly( + name, + visibility, + is_static, + is_readonly, + default, )) } @@ -682,12 +706,13 @@ impl Parser { properties: &mut Vec, methods: &mut Vec, ) -> Result<(), EvalParseError> { - let (visibility, is_static, is_abstract, is_final) = self.parse_class_member_modifiers()?; + let (visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_abstract || is_final { + if is_static || is_abstract || is_final || is_readonly { return Err(EvalParseError::UnsupportedConstruct); } constants @@ -695,6 +720,9 @@ impl Parser { return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } methods.push(self.parse_class_method_decl( visibility.unwrap_or(EvalVisibility::Public), is_static, @@ -707,7 +735,7 @@ impl Parser { if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl(visibility, is_static)?); + properties.push(self.parse_class_property_decl(visibility, is_static, is_readonly)?); Ok(()) } @@ -774,8 +802,9 @@ impl Parser { cases.push(self.parse_enum_case_decl()?); return Ok(()); } - let (visibility, is_static, is_abstract, is_final) = self.parse_class_member_modifiers()?; - if is_abstract { + let (visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; + if is_abstract || is_readonly { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index e8ff9868f5..5bacc32125 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -119,6 +119,37 @@ fn parse_fragment_accepts_private_and_protected_class_members() { ))] ); } + +/// Verifies readonly property modifiers lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_readonly_class_property() { + let program = parse_fragment(b"class DynEvalReadonly { public readonly int $id; }") + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalReadonly", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "id", + EvalVisibility::Public, + false, + true, + None + )], + Vec::new() + ))] + ); +} + +/// Verifies eval rejects readonly property forms that PHP does not allow. +#[test] +fn parse_fragment_rejects_invalid_readonly_class_properties() { + parse_fragment(b"class DynEvalReadonlyDefault { public readonly int $id = 1; }") + .expect_err("readonly properties cannot have defaults in eval"); + parse_fragment(b"class DynEvalReadonlyStatic { public static readonly int $id; }") + .expect_err("static properties cannot be readonly in eval"); +} + /// Verifies abstract and final class modifiers lower into dynamic class metadata. #[test] fn parse_fragment_accepts_abstract_and_final_class_members() { diff --git a/docs/php/eval.md b/docs/php/eval.md index db25b13e13..1481c3ade7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -130,12 +130,14 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties -and methods, `__construct()`, abstract classes and methods, final classes and -methods, trait composition with `insteadof` conflict resolution and `as` -aliases/visibility adaptations, interface implementation checks, static -properties, static methods, class constants, interface constants, trait -constants, and `ClassName::class` literals. Member visibility is checked at -runtime for eval-declared objects and static/class-constant accesses. `self::`, +and methods, property-level `readonly`, `__construct()`, abstract classes and +methods, final classes and methods, trait composition with `insteadof` conflict +resolution and `as` aliases/visibility adaptations, interface implementation +checks, static properties, static methods, class constants, interface +constants, trait constants, and `ClassName::class` literals. Member visibility +is checked at runtime for eval-declared objects and static/class-constant +accesses. `readonly` eval properties may be assigned from the constructor of +the declaring class and later writes fail as eval runtime fatals. `self::`, `parent::`, and late-bound `static::` work for supported static members, class constants, and class-name literals. @@ -286,8 +288,8 @@ native method bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are enum trait-use declarations, property hooks, -attributes/reflection metadata, readonly semantics, and generated/AOT dynamic -static-method call forms. +attributes/reflection metadata, `readonly class` declarations, and generated/AOT +dynamic static-method call forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index adedf82b9b..81b6ddd1ba 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4855,6 +4855,44 @@ echo $box->readProtected(2);'); assert_eq!(out.stdout, "7:7"); } +/// Verifies eval-declared readonly properties can be initialized only in constructors. +#[test] +fn test_eval_declared_readonly_property_rules() { + let out = compile_and_run_capture( + r#"id = $id; } + public function id() { return $this->id; } +} +$box = new EvalReadonlyBox(7); +echo $box->id();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7"); + + let err = compile_and_run_expect_failure( + r#"id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyFailBox(7); +$box->replace(8);'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies eval-declared static properties and static methods work through the bridge. #[test] fn test_eval_declared_static_members_and_late_static_binding() { From a89ba06a97e1245fb8e390745a545828906da52c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 18:32:49 +0200 Subject: [PATCH 0339/1208] Support eval readonly classes --- crates/elephc-eval/src/eval_ir.rs | 132 +++++++++++++++++- .../elephc-eval/src/interpreter/statements.rs | 52 +++++-- .../src/interpreter/tests/classes.rs | 108 ++++++++++++++ crates/elephc-eval/src/parser/statements.rs | 51 +++++-- .../src/parser/tests/classes_errors.rs | 39 ++++++ docs/php/eval.md | 28 ++-- tests/codegen/eval.rs | 53 +++++++ 7 files changed, 422 insertions(+), 41 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index d7a9360537..b14c0c4a3e 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -403,6 +403,7 @@ pub struct EvalClass { name: String, is_abstract: bool, is_final: bool, + is_readonly_class: bool, parent: Option, interfaces: Vec, traits: Vec, @@ -443,10 +444,34 @@ impl EvalClass { properties: Vec, methods: Vec, ) -> Self { - Self::with_modifiers_and_traits( + Self::with_class_modifiers( name, is_abstract, is_final, + false, + parent, + interfaces, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, optional parent, and interfaces. + pub fn with_class_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_and_traits( + name, + is_abstract, + is_final, + is_readonly_class, parent, interfaces, Vec::new(), @@ -466,10 +491,36 @@ impl EvalClass { properties: Vec, methods: Vec, ) -> Self { - Self::with_modifiers_traits_and_constants( + Self::with_class_modifiers_and_traits( name, is_abstract, is_final, + false, + parent, + interfaces, + traits, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, relations, and trait uses. + pub fn with_class_modifiers_and_traits( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, parent, interfaces, traits, @@ -491,10 +542,38 @@ impl EvalClass { properties: Vec, methods: Vec, ) -> Self { - Self::with_modifiers_traits_adaptations_and_constants( + Self::with_class_modifiers_traits_and_constants( name, is_abstract, is_final, + false, + parent, + interfaces, + traits, + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, relations, trait uses, constants, and members. + pub fn with_class_modifiers_traits_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, parent, interfaces, traits, @@ -517,11 +596,41 @@ impl EvalClass { constants: Vec, properties: Vec, methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + traits, + trait_adaptations, + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with all class modifiers, relations, adaptations, constants, and members. + pub fn with_class_modifiers_traits_adaptations_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), is_abstract, is_final, + is_readonly_class, parent, interfaces, traits, @@ -532,6 +641,18 @@ impl EvalClass { } } + /// Marks all instance properties readonly when this metadata represents a `readonly class`. + pub fn with_readonly_instance_properties(mut self) -> Self { + if self.is_readonly_class { + for property in &mut self.properties { + if !property.is_static { + property.is_readonly = true; + } + } + } + self + } + /// Returns the original source spelling of this eval-declared class name. pub fn name(&self) -> &str { &self.name @@ -547,6 +668,11 @@ impl EvalClass { self.is_final } + /// Returns whether this eval-declared class was declared `readonly`. + pub const fn is_readonly_class(&self) -> bool { + self.is_readonly_class + } + /// Returns the parent class name declared by this eval class, when present. pub fn parent(&self) -> Option<&str> { self.parent.as_deref() diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 8abdba299b..002dc2a71b 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -377,14 +377,17 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( { return Err(EvalStatus::RuntimeFatal); } - let class = expand_eval_class_traits(class, context)?; + let class = expand_eval_class_traits(class, context)?.with_readonly_instance_properties(); let class = &class; validate_eval_class_modifiers(class, context)?; if let Some(parent) = class.parent() { let Some(parent_class) = context.class(parent) else { return Err(EvalStatus::RuntimeFatal); }; - if parent_class.is_final() || context.class_is_a(parent, name, false) { + if parent_class.is_final() + || parent_class.is_readonly_class() != class.is_readonly_class() + || context.class_is_a(parent, name, false) + { return Err(EvalStatus::RuntimeFatal); } } @@ -732,18 +735,21 @@ fn expand_eval_class_traits( constants.extend(class.constants().iter().cloned()); properties.extend(class.properties().iter().cloned()); methods.extend(class.methods().iter().cloned()); - Ok(EvalClass::with_modifiers_traits_adaptations_and_constants( - class.name().to_string(), - class.is_abstract(), - class.is_final(), - class.parent().map(str::to_string), - class.interfaces().to_vec(), - class.traits().to_vec(), - class.trait_adaptations().to_vec(), - constants, - properties, - methods, - )) + Ok( + EvalClass::with_class_modifiers_traits_adaptations_and_constants( + class.name().to_string(), + class.is_abstract(), + class.is_final(), + class.is_readonly_class(), + class.parent().map(str::to_string), + class.interfaces().to_vec(), + class.traits().to_vec(), + class.trait_adaptations().to_vec(), + constants, + properties, + methods, + ), + ) } /// Returns case-insensitive method names declared directly by a pending class. @@ -969,6 +975,7 @@ fn validate_eval_class_modifiers( return Err(EvalStatus::RuntimeFatal); } validate_eval_declared_constants(class.constants())?; + validate_eval_declared_properties(class.properties())?; for method in class.methods() { if method.is_abstract() && method.is_final() { return Err(EvalStatus::RuntimeFatal); @@ -987,6 +994,23 @@ fn validate_eval_class_modifiers( Ok(()) } +/// Validates property declarations that can be checked before class registration. +fn validate_eval_declared_properties(properties: &[EvalClassProperty]) -> Result<(), EvalStatus> { + let mut names = std::collections::HashSet::new(); + for property in properties { + if !names.insert(property.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_static() && property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_readonly() && property.default().is_some() { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + /// Validates constant declarations that can be checked before registration. fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index bf35781213..a6c3f291de 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -54,3 +54,111 @@ $box->replace(8);"#, assert_eq!(err, EvalStatus::RuntimeFatal); } + +/// Verifies readonly classes make instance properties readonly implicitly. +#[test] +fn execute_program_initializes_readonly_class_property_in_constructor() { + let program = parse_fragment( + br#"readonly class EvalReadonlyClassBox { + public int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +$box = new EvalReadonlyClassBox(11); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "11:"); + assert_eq!(values.get(result), FakeValue::Int(11)); +} + +/// Verifies readonly class instance properties reject writes after construction. +#[test] +fn execute_program_rejects_readonly_class_property_write_after_constructor() { + let program = parse_fragment( + br#"readonly class EvalReadonlyClassFailBox { + public int $id; + public function __construct($id) { $this->id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyClassFailBox(11); +$box->replace(12);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly class property write should fail outside constructor"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly class static properties remain mutable. +#[test] +fn execute_program_allows_readonly_class_static_property_mutation() { + let program = parse_fragment( + br#"readonly class EvalReadonlyStaticBox { + public static int $count = 1; +} +EvalReadonlyStaticBox::$count = 5; +echo EvalReadonlyStaticBox::$count; echo ":"; +EvalReadonlyStaticBox::$count = EvalReadonlyStaticBox::$count + 1; +return EvalReadonlyStaticBox::$count;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:"); + assert_eq!(values.get(result), FakeValue::Int(6)); +} + +/// Verifies readonly classes may extend readonly parents and use inherited constructors. +#[test] +fn execute_program_allows_readonly_class_extending_readonly_parent() { + let program = parse_fragment( + br#"readonly class EvalReadonlyParentBase { + public int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +readonly class EvalReadonlyParentChild extends EvalReadonlyParentBase {} +$box = new EvalReadonlyParentChild(13); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "13:"); + assert_eq!(values.get(result), FakeValue::Int(13)); +} + +/// Verifies readonly class inheritance requires matching readonly status. +#[test] +fn execute_program_rejects_readonly_class_extending_non_readonly_parent() { + let program = parse_fragment( + br#"class EvalReadonlyParentMismatchBase {} +readonly class EvalReadonlyParentMismatchChild extends EvalReadonlyParentMismatchBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly class cannot extend non-readonly parent"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 13ba5911a6..8a9c0267e5 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -44,7 +44,11 @@ impl Parser { } TokenKind::Ident(name) if ident_eq(name, "for") => self.parse_for_stmt(), TokenKind::Ident(name) if ident_eq(name, "foreach") => self.parse_foreach_stmt(), - TokenKind::Ident(name) if ident_eq(name, "abstract") || ident_eq(name, "final") => { + TokenKind::Ident(name) + if ident_eq(name, "abstract") + || ident_eq(name, "final") + || ident_eq(name, "readonly") => + { self.parse_class_decl_stmt() } TokenKind::Ident(name) if ident_eq(name, "class") => self.parse_class_decl_stmt(), @@ -244,9 +248,9 @@ impl Parser { }]) } - /// Parses `[abstract|final] class Name [extends Parent] [implements Iface, ...] { ... }`. + /// Parses `[abstract|final|readonly] class Name [extends Parent] [implements Iface, ...] { ... }`. pub(super) fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { - let (is_abstract, is_final) = self.parse_class_decl_modifiers()?; + let (is_abstract, is_final, is_readonly_class) = self.parse_class_decl_modifiers()?; if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { return Err(EvalParseError::UnsupportedConstruct); } @@ -269,6 +273,7 @@ impl Parser { return Err(EvalParseError::UnexpectedEof); } self.parse_class_member( + is_readonly_class, &mut constants, &mut properties, &mut methods, @@ -278,10 +283,11 @@ impl Parser { } self.consume_semicolon(); Ok(vec![EvalStmt::ClassDecl( - EvalClass::with_modifiers_traits_adaptations_and_constants( + EvalClass::with_class_modifiers_traits_adaptations_and_constants( name, is_abstract, is_final, + is_readonly_class, parent, interfaces, traits, @@ -293,10 +299,13 @@ impl Parser { )]) } - /// Parses class-level `abstract` and `final` modifiers before `class`. - pub(super) fn parse_class_decl_modifiers(&mut self) -> Result<(bool, bool), EvalParseError> { + /// Parses class-level `abstract`, `final`, and `readonly` modifiers before `class`. + pub(super) fn parse_class_decl_modifiers( + &mut self, + ) -> Result<(bool, bool, bool), EvalParseError> { let mut is_abstract = false; let mut is_final = false; + let mut is_readonly_class = false; loop { match self.current() { TokenKind::Ident(name) if ident_eq(name, "abstract") => { @@ -313,7 +322,14 @@ impl Parser { is_final = true; self.advance(); } - _ => return Ok((is_abstract, is_final)), + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly_class { + return Err(EvalParseError::UnsupportedConstruct); + } + is_readonly_class = true; + self.advance(); + } + _ => return Ok((is_abstract, is_final, is_readonly_class)), } } } @@ -348,6 +364,7 @@ impl Parser { /// Parses one public property or method from an eval class body. pub(super) fn parse_class_member( &mut self, + is_readonly_class: bool, constants: &mut Vec, properties: &mut Vec, methods: &mut Vec, @@ -398,7 +415,12 @@ impl Parser { if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl(visibility, is_static, is_readonly)?); + properties.push(self.parse_class_property_decl( + visibility, + is_static, + is_readonly, + is_readonly_class, + )?); Ok(()) } @@ -647,10 +669,12 @@ impl Parser { visibility: EvalVisibility, is_static: bool, is_readonly: bool, + is_readonly_class: bool, ) -> Result { if is_static && is_readonly { return Err(EvalParseError::UnsupportedConstruct); } + let effective_readonly = is_readonly || (is_readonly_class && !is_static); self.skip_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -658,7 +682,7 @@ impl Parser { let name = name.clone(); self.advance(); let default = if self.consume(TokenKind::Equal) { - if is_readonly { + if effective_readonly { return Err(EvalParseError::UnsupportedConstruct); } Some(self.parse_expr()?) @@ -670,7 +694,7 @@ impl Parser { name, visibility, is_static, - is_readonly, + effective_readonly, default, )) } @@ -735,7 +759,12 @@ impl Parser { if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl(visibility, is_static, is_readonly)?); + properties.push(self.parse_class_property_decl( + visibility, + is_static, + is_readonly, + false, + )?); Ok(()) } diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 5bacc32125..8f9b809c88 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -141,6 +141,43 @@ fn parse_fragment_accepts_readonly_class_property() { ); } +/// Verifies readonly class modifiers lower into class and property metadata. +#[test] +fn parse_fragment_accepts_readonly_class_modifier() { + let program = parse_fragment( + b"final readonly class DynEvalReadonlyClass { public int $id; public static int $count = 0; }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_class_modifiers( + "DynEvalReadonlyClass", + false, + true, + true, + None, + Vec::new(), + vec![ + EvalClassProperty::with_visibility_static_and_readonly( + "id", + EvalVisibility::Public, + false, + true, + None + ), + EvalClassProperty::with_visibility_static_and_readonly( + "count", + EvalVisibility::Public, + true, + false, + Some(EvalExpr::Const(EvalConst::Int(0))) + ) + ], + Vec::new() + ))] + ); +} + /// Verifies eval rejects readonly property forms that PHP does not allow. #[test] fn parse_fragment_rejects_invalid_readonly_class_properties() { @@ -148,6 +185,8 @@ fn parse_fragment_rejects_invalid_readonly_class_properties() { .expect_err("readonly properties cannot have defaults in eval"); parse_fragment(b"class DynEvalReadonlyStatic { public static readonly int $id; }") .expect_err("static properties cannot be readonly in eval"); + parse_fragment(b"readonly class DynEvalReadonlyClassDefault { public int $id = 1; }") + .expect_err("readonly class instance properties cannot have defaults in eval"); } /// Verifies abstract and final class modifiers lower into dynamic class metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index 1481c3ade7..de99c404c7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, methods, `__construct()`, inheritance, visibility, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -130,16 +130,18 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties -and methods, property-level `readonly`, `__construct()`, abstract classes and -methods, final classes and methods, trait composition with `insteadof` conflict -resolution and `as` aliases/visibility adaptations, interface implementation -checks, static properties, static methods, class constants, interface -constants, trait constants, and `ClassName::class` literals. Member visibility -is checked at runtime for eval-declared objects and static/class-constant -accesses. `readonly` eval properties may be assigned from the constructor of -the declaring class and later writes fail as eval runtime fatals. `self::`, -`parent::`, and late-bound `static::` work for supported static members, class -constants, and class-name literals. +and methods, property-level `readonly`, `readonly class`, `__construct()`, +abstract classes and methods, final classes and methods, trait composition with +`insteadof` conflict resolution and `as` aliases/visibility adaptations, +interface implementation checks, static properties, static methods, class +constants, interface constants, trait constants, and `ClassName::class` +literals. Member visibility is checked at runtime for eval-declared objects and +static/class-constant accesses. `readonly` eval properties may be assigned from +the constructor of the declaring class and later writes fail as eval runtime +fatals. A `readonly class` makes instance properties readonly implicitly while +leaving static properties mutable. `self::`, `parent::`, and late-bound +`static::` work for supported static members, class constants, and class-name +literals. Eval object construction can allocate eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime class metadata. Missing class names @@ -288,8 +290,8 @@ native method bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are enum trait-use declarations, property hooks, -attributes/reflection metadata, `readonly class` declarations, and generated/AOT -dynamic static-method call forms. +attributes/reflection metadata, and generated/AOT dynamic static-method call +forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 81b6ddd1ba..7c59170e8a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4893,6 +4893,59 @@ $box->replace(8);'); ); } +/// Verifies eval-declared readonly classes mirror instance/static property rules. +#[test] +fn test_eval_declared_readonly_class_rules() { + let out = compile_and_run_capture( + r#"id = $id; } + public function id() { return $this->id; } +} +readonly class EvalReadonlyClassChild extends EvalReadonlyClassBox {} +$box = new EvalReadonlyClassBox(7); +$child = new EvalReadonlyClassChild(9); +EvalReadonlyClassBox::$count = 5; +echo $box->id() . ":" . EvalReadonlyClassBox::$count . ":" . $child->id();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:5:9"); + + let err = compile_and_run_expect_failure( + r#"id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyClassFailBox(7); +$box->replace(8);'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let parent_err = compile_and_run_expect_failure( + r#" Date: Thu, 18 Jun 2026 18:42:54 +0200 Subject: [PATCH 0340/1208] Support eval property hooks --- crates/elephc-eval/src/eval_ir.rs | 21 +++ .../elephc-eval/src/interpreter/statements.rs | 99 +++++++++++- .../src/interpreter/tests/classes.rs | 103 ++++++++++++ crates/elephc-eval/src/parser/statements.rs | 153 ++++++++++++++++-- .../src/parser/tests/classes_errors.rs | 51 ++++++ docs/php/eval.md | 34 ++-- tests/codegen/eval.rs | 43 +++++ 7 files changed, 470 insertions(+), 34 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index b14c0c4a3e..1b4c85415c 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -851,6 +851,8 @@ pub struct EvalClassProperty { visibility: EvalVisibility, is_static: bool, is_readonly: bool, + has_get_hook: bool, + has_set_hook: bool, default: Option, } @@ -892,10 +894,19 @@ impl EvalClassProperty { visibility, is_static, is_readonly, + has_get_hook: false, + has_set_hook: false, default, } } + /// Returns a copy of this property marked with concrete get/set hook metadata. + pub const fn with_hooks(mut self, has_get_hook: bool, has_set_hook: bool) -> Self { + self.has_get_hook = has_get_hook; + self.has_set_hook = has_set_hook; + self + } + /// Returns the PHP-visible property name without `$`. pub fn name(&self) -> &str { &self.name @@ -916,6 +927,16 @@ impl EvalClassProperty { self.is_readonly } + /// Returns whether this property has a concrete get hook accessor. + pub const fn has_get_hook(&self) -> bool { + self.has_get_hook + } + + /// Returns whether this property has a concrete set hook accessor. + pub const fn has_set_hook(&self) -> bool { + self.has_set_hook + } + /// Returns the property initializer expression, when one was declared. pub fn default(&self) -> Option<&EvalExpr> { self.default.as_ref() diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 002dc2a71b..8fe37e6ed3 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1007,6 +1007,11 @@ fn validate_eval_declared_properties(properties: &[EvalClassProperty]) -> Result if property.is_readonly() && property.default().is_some() { return Err(EvalStatus::RuntimeFatal); } + if (property.has_get_hook() || property.has_set_hook()) + && (property.is_static() || property.is_readonly() || property.default().is_some()) + { + return Err(EvalStatus::RuntimeFatal); + } } Ok(()) } @@ -1212,10 +1217,35 @@ pub(in crate::interpreter) fn eval_property_get_result( let Some(class) = context.dynamic_object_class(identity) else { return values.property_get(object, property_name); }; + let object_class_name = class.name().to_string(); if let Some((declaring_class, property)) = - eval_dynamic_property_for_access(class.name(), property_name, context) + eval_dynamic_property_for_access(&object_class_name, property_name, context) { validate_eval_member_access(&declaring_class, property.visibility(), context)?; + if property.has_get_hook() + && !current_eval_property_hook_is( + &declaring_class, + property.name(), + &property_hook_get_method(property.name()), + context, + ) + { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_get_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + return eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + Vec::new(), + context, + values, + ); + } } values.property_get(object, property_name) } @@ -1234,18 +1264,81 @@ pub(in crate::interpreter) fn eval_property_set_result( let Some(class) = context.dynamic_object_class(identity) else { return values.property_set(object, property_name, value); }; - if context.has_enum(class.name()) { + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { return Err(EvalStatus::RuntimeFatal); } if let Some((declaring_class, property)) = - eval_dynamic_property_for_access(class.name(), property_name, context) + eval_dynamic_property_for_access(&object_class_name, property_name, context) { validate_eval_member_access(&declaring_class, property.visibility(), context)?; validate_eval_readonly_property_write(&declaring_class, &property, context)?; + if property.has_set_hook() { + if !current_eval_property_hook_is( + &declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_set_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + let hook_result = eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + vec![EvaluatedCallArg { name: None, value }], + context, + values, + )?; + values.release(hook_result)?; + return Ok(()); + } + } else if property.has_get_hook() { + return Err(EvalStatus::RuntimeFatal); + } } values.property_set(object, property_name, value) } +/// Returns true while executing the named hook accessor for one property. +fn current_eval_property_hook_is( + declaring_class: &str, + property_name: &str, + hook_method: &str, + context: &ElephcEvalContext, +) -> bool { + let Some(current_class) = context.current_class_scope() else { + return false; + }; + if !same_eval_class_name(current_class, declaring_class) { + return false; + } + let Some((_, method)) = context + .current_function() + .and_then(|function| function.rsplit_once("::")) + else { + return false; + }; + method.eq_ignore_ascii_case(hook_method) + || method.eq_ignore_ascii_case(&property_hook_get_method(property_name)) + || method.eq_ignore_ascii_case(&property_hook_set_method(property_name)) +} + +/// Returns the synthetic get-hook method name for one property. +fn property_hook_get_method(property_name: &str) -> String { + format!("__propget_{property_name}") +} + +/// Returns the synthetic set-hook method name for one property. +fn property_hook_set_method(property_name: &str) -> String { + format!("__propset_{property_name}") +} + /// Rejects writes to readonly eval-declared properties outside their declaring constructor. fn validate_eval_readonly_property_write( declaring_class: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index a6c3f291de..4ce21188d8 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -162,3 +162,106 @@ readonly class EvalReadonlyParentMismatchChild extends EvalReadonlyParentMismatc assert_eq!(err, EvalStatus::RuntimeFatal); } + +/// Verifies a get-only property hook computes a virtual eval property. +#[test] +fn execute_program_reads_eval_property_get_hook() { + let program = parse_fragment( + br#"class EvalHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + get => $this->first . " " . $this->last; + } +} +$person = new EvalHookPerson(); +echo $person->full; +return $person->full;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace"); + assert_eq!( + values.get(result), + FakeValue::String("Ada Lovelace".to_string()) + ); +} + +/// Verifies get/set property hooks can use the raw backing slot from inside accessors. +#[test] +fn execute_program_routes_eval_property_get_and_set_hooks() { + let program = parse_fragment( + br#"class EvalHookName { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$name = new EvalHookName(); +$name->value = "Ada"; +echo $name->value; +return $name->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies get-only property hooks reject writes outside a set accessor. +#[test] +fn execute_program_rejects_write_to_get_only_eval_property_hook() { + let program = parse_fragment( + br#"class EvalHookReadOnly { + public int $answer { + get => 42; + } +} +$box = new EvalHookReadOnly(); +$box->answer = 7;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("get-only property hook write should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval subclasses inherit parent property hooks. +#[test] +fn execute_program_inherits_eval_property_hooks() { + let program = parse_fragment( + br#"class EvalHookBase { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalHookChild extends EvalHookBase { + public function shout() { return $this->value . "?"; } +} +$box = new EvalHookChild(); +$box->value = "Ada"; +echo $box->value; echo ":"; +return $box->shout();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!:"); + assert_eq!(values.get(result), FakeValue::String("Ada!?".to_string())); +} diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 8a9c0267e5..2d17dddca4 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -415,12 +415,10 @@ impl Parser { if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl( - visibility, - is_static, - is_readonly, - is_readonly_class, - )?); + let (property, mut hook_methods) = + self.parse_class_property_decl(visibility, is_static, is_readonly, is_readonly_class)?; + properties.push(property); + methods.append(&mut hook_methods); Ok(()) } @@ -670,7 +668,7 @@ impl Parser { is_static: bool, is_readonly: bool, is_readonly_class: bool, - ) -> Result { + ) -> Result<(EvalClassProperty, Vec), EvalParseError> { if is_static && is_readonly { return Err(EvalParseError::UnsupportedConstruct); } @@ -689,16 +687,133 @@ impl Parser { } else { None }; - self.expect_semicolon()?; - Ok(EvalClassProperty::with_visibility_static_and_readonly( + let default_is_some = default.is_some(); + let (has_get_hook, has_set_hook, hook_methods) = + self.parse_property_hook_tail(&name, is_static, effective_readonly, default_is_some)?; + let property = EvalClassProperty::with_visibility_static_and_readonly( name, visibility, is_static, effective_readonly, default, + ) + .with_hooks(has_get_hook, has_set_hook); + Ok((property, hook_methods)) + } + + /// Parses `;` or a concrete eval property hook block after one property declaration. + pub(super) fn parse_property_hook_tail( + &mut self, + property_name: &str, + is_static: bool, + is_readonly: bool, + has_default: bool, + ) -> Result<(bool, bool, Vec), EvalParseError> { + if self.consume(TokenKind::Semicolon) { + return Ok((false, false, Vec::new())); + } + if !matches!(self.current(), TokenKind::LBrace) { + return Err(EvalParseError::UnexpectedToken); + } + if is_static || is_readonly || has_default { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let mut has_get_hook = false; + let mut has_set_hook = false; + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + let (is_get, method) = self.parse_property_hook_decl(property_name)?; + if is_get { + if has_get_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + has_get_hook = true; + } else { + if has_set_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + has_set_hook = true; + } + methods.push(method); + } + if !has_get_hook && !has_set_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok((has_get_hook, has_set_hook, methods)) + } + + /// Parses one concrete `get` or `set` property hook declaration. + pub(super) fn parse_property_hook_decl( + &mut self, + property_name: &str, + ) -> Result<(bool, EvalClassMethod), EvalParseError> { + if self.consume(TokenKind::Ampersand) { + return Err(EvalParseError::UnsupportedConstruct); + } + let TokenKind::Ident(hook_name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let is_get = ident_eq(hook_name, "get"); + let is_set = ident_eq(hook_name, "set"); + if !is_get && !is_set { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let params = if is_set { + vec![self.parse_property_set_hook_param()?] + } else { + Vec::new() + }; + let body = match self.current() { + TokenKind::Semicolon => return Err(EvalParseError::UnsupportedConstruct), + TokenKind::FatArrow if is_get => { + self.advance(); + let expr = self.parse_expr()?; + self.expect_semicolon()?; + vec![EvalStmt::Return(Some(expr))] + } + TokenKind::FatArrow => return Err(EvalParseError::UnsupportedConstruct), + TokenKind::LBrace => self.parse_block()?, + _ => return Err(EvalParseError::UnexpectedToken), + }; + let method_name = if is_get { + property_hook_get_method(property_name) + } else { + property_hook_set_method(property_name) + }; + Ok(( + is_get, + EvalClassMethod::with_visibility_and_modifiers( + method_name, + EvalVisibility::Public, + false, + false, + false, + params, + body, + ), )) } + /// Parses an optional set-hook parameter list and returns the hook value variable. + pub(super) fn parse_property_set_hook_param(&mut self) -> Result { + if !self.consume(TokenKind::LParen) { + return Ok("value".to_string()); + } + self.skip_optional_property_type()?; + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::RParen)?; + Ok(name) + } + /// Parses `trait Name { ... }` declarations into dynamic trait metadata. pub(super) fn parse_trait_decl_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -759,12 +874,10 @@ impl Parser { if is_abstract || is_final { return Err(EvalParseError::UnsupportedConstruct); } - properties.push(self.parse_class_property_decl( - visibility, - is_static, - is_readonly, - false, - )?); + let (property, mut hook_methods) = + self.parse_class_property_decl(visibility, is_static, is_readonly, false)?; + properties.push(property); + methods.append(&mut hook_methods); Ok(()) } @@ -1653,3 +1766,13 @@ impl Parser { Ok(statements) } } + +/// Returns the synthetic get-hook method name for one property. +fn property_hook_get_method(property_name: &str) -> String { + format!("__propget_{property_name}") +} + +/// Returns the synthetic set-hook method name for one property. +fn property_hook_set_method(property_name: &str) -> String { + format!("__propset_{property_name}") +} diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 8f9b809c88..3f4a29b353 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -178,6 +178,46 @@ fn parse_fragment_accepts_readonly_class_modifier() { ); } +/// Verifies concrete property hooks lower to property metadata plus accessor methods. +#[test] +fn parse_fragment_accepts_concrete_class_property_hooks() { + let program = parse_fragment( + br#"class DynEvalHooked { + public int $value { + get => 7; + set { return; } + } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalHooked", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "value", + EvalVisibility::Public, + false, + false, + None + ) + .with_hooks(true, true)], + vec![ + EvalClassMethod::new( + "__propget_value", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(7))))] + ), + EvalClassMethod::new( + "__propset_value", + vec!["value".to_string()], + vec![EvalStmt::Return(None)] + ) + ] + ))] + ); +} + /// Verifies eval rejects readonly property forms that PHP does not allow. #[test] fn parse_fragment_rejects_invalid_readonly_class_properties() { @@ -189,6 +229,17 @@ fn parse_fragment_rejects_invalid_readonly_class_properties() { .expect_err("readonly class instance properties cannot have defaults in eval"); } +/// Verifies eval rejects property hook forms that need broader class contracts. +#[test] +fn parse_fragment_rejects_invalid_property_hooks() { + parse_fragment(b"class DynEvalHookDefault { public int $id = 1 { get => $this->id; } }") + .expect_err("hooked properties cannot have defaults in eval"); + parse_fragment(b"class DynEvalHookStatic { public static int $id { get => 1; } }") + .expect_err("static properties cannot have hooks in eval"); + parse_fragment(b"class DynEvalHookAbstract { public int $id { get; } }") + .expect_err("abstract property hooks are not supported in eval classes"); +} + /// Verifies abstract and final class modifiers lower into dynamic class metadata. #[test] fn parse_fragment_accepts_abstract_and_final_class_members() { diff --git a/docs/php/eval.md b/docs/php/eval.md index de99c404c7..6a2bc92e9a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -130,18 +130,20 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties -and methods, property-level `readonly`, `readonly class`, `__construct()`, -abstract classes and methods, final classes and methods, trait composition with -`insteadof` conflict resolution and `as` aliases/visibility adaptations, -interface implementation checks, static properties, static methods, class -constants, interface constants, trait constants, and `ClassName::class` -literals. Member visibility is checked at runtime for eval-declared objects and -static/class-constant accesses. `readonly` eval properties may be assigned from -the constructor of the declaring class and later writes fail as eval runtime -fatals. A `readonly class` makes instance properties readonly implicitly while -leaving static properties mutable. `self::`, `parent::`, and late-bound -`static::` work for supported static members, class constants, and class-name -literals. +and methods, concrete property `get` / `set` hooks, property-level `readonly`, +`readonly class`, `__construct()`, abstract classes and methods, final classes +and methods, trait composition with `insteadof` conflict resolution and `as` +aliases/visibility adaptations, interface implementation checks, static +properties, static methods, class constants, interface constants, trait +constants, and `ClassName::class` literals. Member visibility is checked at +runtime for eval-declared objects and static/class-constant accesses. +Concrete property hooks are lowered to eval accessor methods; reads and writes +route through inherited hooks, while access from the accessor itself uses the +raw backing slot. `readonly` eval properties may be assigned from the +constructor of the declaring class and later writes fail as eval runtime fatals. +A `readonly class` makes instance properties readonly implicitly while leaving +static properties mutable. `self::`, `parent::`, and late-bound `static::` work +for supported static members, class constants, and class-name literals. Eval object construction can allocate eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime class metadata. Missing class names @@ -289,9 +291,9 @@ arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are enum trait-use declarations, property hooks, -attributes/reflection metadata, and generated/AOT dynamic static-method call -forms. +remaining class-system gaps are enum trait-use declarations, abstract/interface +property-hook contracts, attributes/reflection metadata, and generated/AOT +dynamic static-method call forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7c59170e8a..b7eeef42e1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4946,6 +4946,49 @@ readonly class EvalReadonlyClassChild extends EvalReadonlyClassBase {}'); ); } +/// Verifies eval-declared property hooks route get/set access through accessors. +#[test] +fn test_eval_declared_property_hooks() { + let out = compile_and_run_capture( + r#" $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalHookChild extends EvalHookName { + public function shout() { return $this->value . "?"; } +} +$box = new EvalHookChild(); +$box->value = "Ada"; +echo $box->value . ":" . $box->shout();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada!:Ada!?"); + + let err = compile_and_run_expect_failure( + r#" 42; + } +} +$box = new EvalHookReadOnly(); +$box->answer = 7;'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies eval-declared static properties and static methods work through the bridge. #[test] fn test_eval_declared_static_members_and_late_static_binding() { From 24eb5cf26180453f030e986ba2bb4b93d64717b1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 18:52:52 +0200 Subject: [PATCH 0341/1208] Support eval interface property hooks --- crates/elephc-eval/src/context.rs | 46 ++++++- crates/elephc-eval/src/eval_ir.rs | 61 +++++++++ crates/elephc-eval/src/interpreter/mod.rs | 6 +- .../elephc-eval/src/interpreter/statements.rs | 42 ++++++ .../src/interpreter/tests/classes.rs | 103 +++++++++++++++ crates/elephc-eval/src/parser/statements.rs | 124 +++++++++++++----- .../src/parser/tests/classes_errors.rs | 41 ++++++ docs/php/eval.md | 21 +-- tests/codegen/eval.rs | 52 ++++++++ 9 files changed, 452 insertions(+), 44 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 29e979b343..689ef94b9b 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -17,7 +17,7 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::{ EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, EvalFunction, - EvalInterface, EvalInterfaceMethod, EvalTrait, + EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalTrait, }; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; @@ -765,6 +765,50 @@ impl ElephcEvalContext { } } + /// Returns direct and inherited property contracts for an eval interface. + pub fn interface_property_requirements( + &self, + interface_name: &str, + ) -> Vec { + let mut properties = Vec::new(); + let mut seen_interfaces = HashSet::new(); + self.collect_interface_property_requirements( + interface_name, + &mut properties, + &mut seen_interfaces, + ); + properties + } + + /// Collects eval interface property contracts, merging duplicate inherited names. + fn collect_interface_property_requirements( + &self, + interface_name: &str, + properties: &mut Vec, + seen_interfaces: &mut HashSet, + ) { + let key = normalize_class_name(interface_name); + if !seen_interfaces.insert(key) { + return; + } + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + self.collect_interface_property_requirements(parent, properties, seen_interfaces); + } + for property in interface.properties() { + if let Some(existing) = properties + .iter_mut() + .find(|existing| existing.name() == property.name()) + { + *existing = existing.merged_with(property); + } else { + properties.push(property.clone()); + } + } + } + /// Returns whether an eval-declared class satisfies one class/interface target. pub fn class_is_a(&self, class_name: &str, target: &str, exclude_self: bool) -> bool { let Some(class) = self.class(class_name) else { diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 1b4c85415c..45695d175e 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -314,6 +314,7 @@ pub struct EvalInterface { name: String, parents: Vec, constants: Vec, + properties: Vec, methods: Vec, } @@ -333,11 +334,23 @@ impl EvalInterface { parents: Vec, constants: Vec, methods: Vec, + ) -> Self { + Self::with_constants_and_properties(name, parents, constants, Vec::new(), methods) + } + + /// Creates a dynamic eval interface with constants, property contracts, and methods. + pub fn with_constants_and_properties( + name: impl Into, + parents: Vec, + constants: Vec, + properties: Vec, + methods: Vec, ) -> Self { Self { name: name.into(), parents, constants, + properties, methods, } } @@ -364,12 +377,60 @@ impl EvalInterface { .find(|constant| constant.name() == name) } + /// Returns property hook contracts declared directly by this eval interface. + pub fn properties(&self) -> &[EvalInterfaceProperty] { + &self.properties + } + /// Returns method signatures declared directly by this eval interface. pub fn methods(&self) -> &[EvalInterfaceMethod] { &self.methods } } +/// Property hook contract metadata for a runtime eval interface. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalInterfaceProperty { + name: String, + requires_get: bool, + requires_set: bool, +} + +impl EvalInterfaceProperty { + /// Creates one eval interface property contract. + pub fn new(name: impl Into, requires_get: bool, requires_set: bool) -> Self { + Self { + name: name.into(), + requires_get, + requires_set, + } + } + + /// Returns the PHP-visible property name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns whether the interface requires the property to be readable. + pub const fn requires_get(&self) -> bool { + self.requires_get + } + + /// Returns whether the interface requires the property to be writable. + pub const fn requires_set(&self) -> bool { + self.requires_set + } + + /// Returns a merged contract containing either side's get/set requirements. + pub fn merged_with(&self, other: &Self) -> Self { + Self { + name: self.name.clone(), + requires_get: self.requires_get || other.requires_get, + requires_set: self.requires_set || other.requires_set, + } + } +} + /// Method signature metadata for a runtime eval interface. #[derive(Debug, Clone, PartialEq)] pub struct EvalInterfaceMethod { diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index f14a7295cf..ea42968a58 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -32,9 +32,9 @@ use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, - EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalMagicConst, EvalMatchArm, - EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, - EvalVisibility, + EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, + EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, + EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 8fe37e6ed3..0ce242d528 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1178,6 +1178,11 @@ fn validate_class_implements_eval_interface( return Err(EvalStatus::RuntimeFatal); } } + for requirement in context.interface_property_requirements(interface_name) { + if !class_has_interface_property(class, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } Ok(()) } @@ -1204,6 +1209,43 @@ fn class_has_interface_method( }) } +/// Returns whether a class or its eval parents satisfy one interface property contract. +fn class_has_interface_property( + class: &EvalClass, + requirement: &EvalInterfaceProperty, + context: &ElephcEvalContext, +) -> bool { + pending_class_property(class, requirement.name(), context).is_some_and(|property| { + if property.visibility() != EvalVisibility::Public || property.is_static() { + return false; + } + if requirement.requires_set() { + return property.has_set_hook() + || (!property.has_get_hook() && !property.is_readonly()); + } + requirement.requires_get() + }) +} + +/// Returns a property from a pending class or one of its already registered parents. +fn pending_class_property( + class: &EvalClass, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(property) = class + .properties() + .iter() + .find(|property| property.name() == property_name) + { + return Some(property.clone()); + } + class + .parent() + .and_then(|parent| context.class_property(parent, property_name)) + .map(|(_, property)| property) +} + /// Reads one object property while enforcing eval-declared member visibility. pub(in crate::interpreter) fn eval_property_get_result( object: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 4ce21188d8..0232054123 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -265,3 +265,106 @@ return $box->shout();"#, assert_eq!(values.output, "Ada!:"); assert_eq!(values.get(result), FakeValue::String("Ada!?".to_string())); } + +/// Verifies eval interface property hook contracts are enforced through inheritance. +#[test] +fn execute_program_accepts_interface_property_hook_contracts() { + let program = parse_fragment( + br#"interface EvalHookContract { + public string $value { get; set; } +} +interface EvalNamedHookContract extends EvalHookContract { + public string $name { get; } +} +class EvalHookContractBox implements EvalNamedHookContract { + public string $name = "box"; + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$box = new EvalHookContractBox(); +$box->value = "Ada"; +echo $box->name; echo ":"; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "box:Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies a normal public mutable property satisfies an eval interface get/set contract. +#[test] +fn execute_program_accepts_plain_property_for_interface_hook_contracts() { + let program = parse_fragment( + br#"interface EvalPlainHookContract { + public string $value { get; set; } +} +class EvalPlainHookContractBox implements EvalPlainHookContract { + public string $value = "Ada"; +} +$box = new EvalPlainHookContractBox(); +echo $box->value; echo ":"; +$box->value = "Grace"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada:"); + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} + +/// Verifies a get-only hook cannot satisfy a writable eval interface contract. +#[test] +fn execute_program_rejects_get_only_hook_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalHookSetContract { + public int $answer { get; set; } +} +class EvalHookGetOnlyContractBox implements EvalHookSetContract { + public int $answer { + get => 42; + } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("get-only hook should fail writable interface contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly properties cannot satisfy writable eval interface contracts. +#[test] +fn execute_program_rejects_readonly_property_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalReadonlyHookContract { + public int $id { get; set; } +} +class EvalReadonlyHookContractBox implements EvalReadonlyHookContract { + public readonly int $id; + public function __construct($id) { $this->id = $id; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly property should fail writable interface contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 2d17dddca4..4ddcf6a8c6 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -13,8 +13,9 @@ use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, - EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInterface, EvalInterfaceMethod, EvalStmt, - EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalVisibility, + EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInterface, EvalInterfaceMethod, + EvalInterfaceProperty, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, + EvalVisibility, }; use crate::lexer::TokenKind; @@ -997,20 +998,19 @@ impl Parser { let parents = self.parse_interface_parent_clause()?; self.expect(TokenKind::LBrace)?; let mut constants = Vec::new(); + let mut properties = Vec::new(); let mut methods = Vec::new(); while !self.consume(TokenKind::RBrace) { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - if let Some(constant) = self.parse_optional_interface_constant_decl()? { - constants.push(constant); - } else { - methods.push(self.parse_interface_method_decl()?); - } + self.parse_interface_member(&mut constants, &mut properties, &mut methods)?; } self.consume_semicolon(); Ok(vec![EvalStmt::InterfaceDecl( - EvalInterface::with_constants(name, parents, constants, methods), + EvalInterface::with_constants_and_properties( + name, parents, constants, properties, methods, + ), )]) } @@ -1031,36 +1031,35 @@ impl Parser { Ok(parents) } - /// Parses an interface constant declaration when the current member starts with `const`. - pub(super) fn parse_optional_interface_constant_decl( + /// Parses one eval interface constant, property contract, or method signature. + pub(super) fn parse_interface_member( &mut self, - ) -> Result, EvalParseError> { - let visibility = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) - { - self.advance(); - EvalVisibility::Public - } else { - EvalVisibility::Public - }; - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - return Ok(None); - } - Ok(Some(self.parse_class_const_decl(visibility)?)) - } - - /// Parses one eval interface method signature. - pub(super) fn parse_interface_method_decl( - &mut self, - ) -> Result { + constants: &mut Vec, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) { self.advance(); } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) { return Err(EvalParseError::UnsupportedConstruct); } - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - return Err(EvalParseError::UnsupportedConstruct); + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + constants.push(self.parse_class_const_decl(EvalVisibility::Public)?); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + methods.push(self.parse_interface_method_decl_after_function_keyword()?); + return Ok(()); } + properties.push(self.parse_interface_property_decl()?); + Ok(()) + } + + /// Parses one eval interface method signature after `function` has been selected. + pub(super) fn parse_interface_method_decl_after_function_keyword( + &mut self, + ) -> Result { self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1073,6 +1072,71 @@ impl Parser { Ok(EvalInterfaceMethod::new(name, params)) } + /// Parses one interface property hook contract. + pub(super) fn parse_interface_property_decl( + &mut self, + ) -> Result { + self.skip_optional_property_type()?; + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + if matches!(self.current(), TokenKind::Equal) { + return Err(EvalParseError::UnsupportedConstruct); + } + let (requires_get, requires_set) = self.parse_interface_property_hook_contracts()?; + Ok(EvalInterfaceProperty::new(name, requires_get, requires_set)) + } + + /// Parses `{ get; set; }` hook contracts for an interface property. + pub(super) fn parse_interface_property_hook_contracts( + &mut self, + ) -> Result<(bool, bool), EvalParseError> { + self.expect(TokenKind::LBrace)?; + let mut requires_get = false; + let mut requires_set = false; + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + if self.consume(TokenKind::Ampersand) { + return Err(EvalParseError::UnsupportedConstruct); + } + let TokenKind::Ident(hook_name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let is_get = ident_eq(hook_name, "get"); + let is_set = ident_eq(hook_name, "set"); + if !is_get && !is_set { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + if matches!( + self.current(), + TokenKind::LParen | TokenKind::FatArrow | TokenKind::LBrace + ) { + return Err(EvalParseError::UnsupportedConstruct); + } + self.expect_semicolon()?; + if is_get { + if requires_get { + return Err(EvalParseError::UnsupportedConstruct); + } + requires_get = true; + } else { + if requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + requires_set = true; + } + } + if !requires_get && !requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok((requires_get, requires_set)) + } + /// Consumes a simple declared property type before the `$property` token. pub(super) fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { if matches!(self.current(), TokenKind::DollarIdent(_)) { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 3f4a29b353..34e2019171 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -62,6 +62,34 @@ fn parse_fragment_accepts_interface_declaration_source() { ))] ); } + +/// Verifies interface property hook contracts lower to eval interface metadata. +#[test] +fn parse_fragment_accepts_interface_property_hook_contracts() { + let program = parse_fragment( + br#"interface DynEvalHookIface { + public string $value { get; set; } + public int $id { get; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl( + EvalInterface::with_constants_and_properties( + "DynEvalHookIface", + Vec::new(), + Vec::new(), + vec![ + EvalInterfaceProperty::new("value", true, true), + EvalInterfaceProperty::new("id", true, false), + ], + Vec::new(), + ) + )] + ); +} + /// Verifies public property and method class members lower into dynamic class metadata. #[test] fn parse_fragment_accepts_public_class_members() { @@ -240,6 +268,19 @@ fn parse_fragment_rejects_invalid_property_hooks() { .expect_err("abstract property hooks are not supported in eval classes"); } +/// Verifies eval rejects concrete hook bodies in interface property contracts. +#[test] +fn parse_fragment_rejects_invalid_interface_property_hooks() { + parse_fragment(b"interface DynEvalIfaceHookBody { public int $id { get => 1; } }") + .expect_err("interface property hooks cannot have concrete bodies"); + parse_fragment(b"interface DynEvalIfaceHookDuplicate { public int $id { get; get; } }") + .expect_err("interface property hooks cannot repeat contracts"); + parse_fragment(b"interface DynEvalIfaceHookEmpty { public int $id { } }") + .expect_err("interface property hooks require at least one contract"); + parse_fragment(b"interface DynEvalIfaceHookDefault { public int $id = 1 { get; } }") + .expect_err("interface property hook contracts cannot have defaults"); +} + /// Verifies abstract and final class modifiers lower into dynamic class metadata. #[test] fn parse_fragment_accepts_abstract_and_final_class_members() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 6a2bc92e9a..162f977848 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -130,13 +130,14 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties -and methods, concrete property `get` / `set` hooks, property-level `readonly`, -`readonly class`, `__construct()`, abstract classes and methods, final classes -and methods, trait composition with `insteadof` conflict resolution and `as` -aliases/visibility adaptations, interface implementation checks, static -properties, static methods, class constants, interface constants, trait -constants, and `ClassName::class` literals. Member visibility is checked at -runtime for eval-declared objects and static/class-constant accesses. +and methods, concrete property `get` / `set` hooks, interface property hook +contract checks, property-level `readonly`, `readonly class`, `__construct()`, +abstract classes and methods, final classes and methods, trait composition with +`insteadof` conflict resolution and `as` aliases/visibility adaptations, +interface implementation checks, static properties, static methods, class +constants, interface constants, trait constants, and `ClassName::class` +literals. Member visibility is checked at runtime for eval-declared objects and +static/class-constant accesses. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the @@ -291,9 +292,9 @@ arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are enum trait-use declarations, abstract/interface -property-hook contracts, attributes/reflection metadata, and generated/AOT -dynamic static-method call forms. +remaining class-system gaps are enum trait-use declarations, abstract property +hook contracts, attributes/reflection metadata, and generated/AOT dynamic +static-method call forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b7eeef42e1..95121379d1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4989,6 +4989,58 @@ $box->answer = 7;'); ); } +/// Verifies eval-declared interface property hook contracts validate class properties. +#[test] +fn test_eval_declared_interface_property_hook_contracts() { + let out = compile_and_run_capture( + r#" $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalIfacePlainBox implements EvalIfaceHookContract { + public string $value = "Grace"; +} +$box = new EvalIfaceHookBox(); +$box->value = "Ada"; +$plain = new EvalIfacePlainBox(); +echo $box->name . ":" . $box->value . ":" . $plain->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "box:Ada!:Grace"); + + let err = compile_and_run_expect_failure( + r#" 42; + } +}'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies eval-declared static properties and static methods work through the bridge. #[test] fn test_eval_declared_static_members_and_late_static_binding() { From 2ddb84c05a03df45c1920f0da5472aacff6540b6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 19:00:04 +0200 Subject: [PATCH 0342/1208] Support eval abstract property hooks --- crates/elephc-eval/src/eval_ir.rs | 33 +++++ .../elephc-eval/src/interpreter/statements.rs | 125 +++++++++++++++++- .../src/interpreter/tests/classes.rs | 120 +++++++++++++++++ crates/elephc-eval/src/parser/statements.rs | 46 +++++-- .../src/parser/tests/classes_errors.rs | 66 ++++++++- docs/php/eval.md | 20 +-- tests/codegen/eval.rs | 44 ++++++ 7 files changed, 427 insertions(+), 27 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 45695d175e..c207f69908 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -912,8 +912,11 @@ pub struct EvalClassProperty { visibility: EvalVisibility, is_static: bool, is_readonly: bool, + is_abstract: bool, has_get_hook: bool, has_set_hook: bool, + requires_get_hook: bool, + requires_set_hook: bool, default: Option, } @@ -955,8 +958,11 @@ impl EvalClassProperty { visibility, is_static, is_readonly, + is_abstract: false, has_get_hook: false, has_set_hook: false, + requires_get_hook: false, + requires_set_hook: false, default, } } @@ -968,6 +974,18 @@ impl EvalClassProperty { self } + /// Returns a copy of this property marked as an abstract hook contract. + pub const fn with_abstract_hook_contract( + mut self, + requires_get_hook: bool, + requires_set_hook: bool, + ) -> Self { + self.is_abstract = true; + self.requires_get_hook = requires_get_hook; + self.requires_set_hook = requires_set_hook; + self + } + /// Returns the PHP-visible property name without `$`. pub fn name(&self) -> &str { &self.name @@ -988,6 +1006,11 @@ impl EvalClassProperty { self.is_readonly } + /// Returns whether this property is an abstract property hook contract. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + /// Returns whether this property has a concrete get hook accessor. pub const fn has_get_hook(&self) -> bool { self.has_get_hook @@ -998,6 +1021,16 @@ impl EvalClassProperty { self.has_set_hook } + /// Returns whether this abstract property contract requires read access. + pub const fn requires_get_hook(&self) -> bool { + self.requires_get_hook + } + + /// Returns whether this abstract property contract requires write access. + pub const fn requires_set_hook(&self) -> bool { + self.requires_set_hook + } + /// Returns the property initializer expression, when one was declared. pub fn default(&self) -> Option<&EvalExpr> { self.default.as_ref() diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 0ce242d528..3d738cc708 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -975,7 +975,7 @@ fn validate_eval_class_modifiers( return Err(EvalStatus::RuntimeFatal); } validate_eval_declared_constants(class.constants())?; - validate_eval_declared_properties(class.properties())?; + validate_eval_declared_properties(class)?; for method in class.methods() { if method.is_abstract() && method.is_final() { return Err(EvalStatus::RuntimeFatal); @@ -995,12 +995,21 @@ fn validate_eval_class_modifiers( } /// Validates property declarations that can be checked before class registration. -fn validate_eval_declared_properties(properties: &[EvalClassProperty]) -> Result<(), EvalStatus> { +fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); - for property in properties { + for property in class.properties() { if !names.insert(property.name().to_string()) { return Err(EvalStatus::RuntimeFatal); } + if property.is_abstract() + && (!class.is_abstract() + || property.is_static() + || property.is_readonly() + || property.default().is_some() + || (!property.requires_get_hook() && !property.requires_set_hook())) + { + return Err(EvalStatus::RuntimeFatal); + } if property.is_static() && property.is_readonly() { return Err(EvalStatus::RuntimeFatal); } @@ -1076,6 +1085,9 @@ fn validate_concrete_class_requirements( if !pending_class_abstract_method_requirements(class, context).is_empty() { return Err(EvalStatus::RuntimeFatal); } + if !pending_class_abstract_property_requirements(class, context)?.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } for interface in pending_class_interface_names(class, context) { if context.has_interface(&interface) { validate_class_implements_eval_interface(class, &interface, context)?; @@ -1097,6 +1109,19 @@ fn pending_class_abstract_method_requirements( requirements.into_values().collect() } +/// Returns inherited abstract properties that the pending class has not concretized. +fn pending_class_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_class_abstract_property_requirements(parent, context, &mut requirements)?; + } + apply_class_abstract_property_requirements(class, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + /// Collects abstract method requirements from one declared eval class ancestry chain. fn collect_class_abstract_method_requirements( class_name: &str, @@ -1112,6 +1137,21 @@ fn collect_class_abstract_method_requirements( apply_class_abstract_method_requirements(class, requirements); } +/// Collects abstract property requirements from one declared eval class ancestry chain. +fn collect_class_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let Some(class) = context.class(class_name) else { + return Ok(()); + }; + if let Some(parent) = class.parent() { + collect_class_abstract_property_requirements(parent, context, requirements)?; + } + apply_class_abstract_property_requirements(class, requirements) +} + /// Applies one class's methods to the open abstract-method requirement set. fn apply_class_abstract_method_requirements( class: &EvalClass, @@ -1127,6 +1167,78 @@ fn apply_class_abstract_method_requirements( } } +/// Applies one class's properties to the open abstract-property requirement set. +fn apply_class_abstract_property_requirements( + class: &EvalClass, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for property in class.properties() { + let key = property.name().to_string(); + if property.is_abstract() { + if let Some(existing) = requirements.get(&key) { + property_contract_visibility_allows(existing, property) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal)?; + requirements.insert(key, merge_abstract_property_contracts(existing, property)); + } else { + requirements.insert(key, property.clone()); + } + } else if let Some(requirement) = requirements.get(&key) { + class_property_satisfies_abstract_contract(property, requirement) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal)?; + requirements.remove(&key); + } + } + Ok(()) +} + +/// Merges inherited and redeclared abstract property hook requirements. +fn merge_abstract_property_contracts( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> EvalClassProperty { + redeclared.clone().with_abstract_hook_contract( + inherited.requires_get_hook() || redeclared.requires_get_hook(), + inherited.requires_set_hook() || redeclared.requires_set_hook(), + ) +} + +/// Returns whether a redeclared property keeps compatible visibility. +fn property_contract_visibility_allows( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> bool { + property_visibility_rank(redeclared.visibility()) + >= property_visibility_rank(inherited.visibility()) +} + +/// Returns whether a concrete property satisfies an abstract hook contract. +fn class_property_satisfies_abstract_contract( + property: &EvalClassProperty, + requirement: &EvalClassProperty, +) -> bool { + if property.is_abstract() + || property.is_static() + || !property_contract_visibility_allows(requirement, property) + { + return false; + } + if requirement.requires_set_hook() { + return property.has_set_hook() || (!property.has_get_hook() && !property.is_readonly()); + } + requirement.requires_get_hook() +} + +/// Returns a comparable rank where larger means less restrictive property visibility. +fn property_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} + /// Returns interface names inherited or directly declared by a pending eval class. fn pending_class_interface_names(class: &EvalClass, context: &ElephcEvalContext) -> Vec { let mut interfaces = Vec::new(); @@ -1216,7 +1328,10 @@ fn class_has_interface_property( context: &ElephcEvalContext, ) -> bool { pending_class_property(class, requirement.name(), context).is_some_and(|property| { - if property.visibility() != EvalVisibility::Public || property.is_static() { + if property.is_abstract() + || property.visibility() != EvalVisibility::Public + || property.is_static() + { return false; } if requirement.requires_set() { @@ -1779,7 +1894,7 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( for property in class .properties() .iter() - .filter(|property| !property.is_static()) + .filter(|property| !property.is_static() && !property.is_abstract()) { let value = if let Some(default) = property.default() { eval_expr(default, context, caller_scope, values)? diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 0232054123..10d7ac8e4d 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -368,3 +368,123 @@ class EvalReadonlyHookContractBox implements EvalReadonlyHookContract { assert_eq!(err, EvalStatus::RuntimeFatal); } + +/// Verifies concrete eval subclasses satisfy abstract property hook contracts. +#[test] +fn execute_program_accepts_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"abstract class EvalAbstractHookBase { + abstract public string $value { get; set; } +} +class EvalAbstractHookBox extends EvalAbstractHookBase { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$box = new EvalAbstractHookBox(); +$box->value = "Ada"; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies normal mutable properties satisfy abstract get/set hook contracts. +#[test] +fn execute_program_accepts_plain_property_for_abstract_hook_contracts() { + let program = parse_fragment( + br#"abstract class EvalPlainAbstractHookBase { + abstract public string $value { get; set; } +} +class EvalPlainAbstractHookBox extends EvalPlainAbstractHookBase { + public string $value = "Ada"; +} +$box = new EvalPlainAbstractHookBox(); +echo $box->value; echo ":"; +$box->value = "Grace"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada:"); + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} + +/// Verifies concrete eval subclasses must declare inherited abstract properties. +#[test] +fn execute_program_rejects_missing_abstract_property_hook_contract() { + let program = parse_fragment( + br#"abstract class EvalMissingAbstractHookBase { + abstract public string $value { get; } +} +class EvalMissingAbstractHookBox extends EvalMissingAbstractHookBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing abstract property should fail concrete subclass"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly properties cannot satisfy abstract writable hook contracts. +#[test] +fn execute_program_rejects_readonly_property_for_abstract_set_contract() { + let program = parse_fragment( + br#"abstract class EvalReadonlyAbstractHookBase { + abstract public int $id { get; set; } +} +class EvalReadonlyAbstractHookBox extends EvalReadonlyAbstractHookBase { + public readonly int $id; + public function __construct($id) { $this->id = $id; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly property should fail abstract writable contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract trait property hook contracts are enforced after trait expansion. +#[test] +fn execute_program_enforces_trait_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"trait EvalTraitNeedsName { + abstract protected string $name { get; } + public function label() { return $this->name; } +} +class EvalTraitNameBox { + use EvalTraitNeedsName; + protected string $name = "Ada"; +} +$box = new EvalTraitNameBox(); +echo $box->label(); +return $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada"); + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); +} diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 4ddcf6a8c6..b0fa73cbd2 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -413,11 +413,16 @@ impl Parser { } let visibility = visibility.unwrap_or(EvalVisibility::Public); - if is_abstract || is_final { + if is_final { return Err(EvalParseError::UnsupportedConstruct); } - let (property, mut hook_methods) = - self.parse_class_property_decl(visibility, is_static, is_readonly, is_readonly_class)?; + let (property, mut hook_methods) = self.parse_class_property_decl( + visibility, + is_static, + is_readonly, + is_readonly_class, + is_abstract, + )?; properties.push(property); methods.append(&mut hook_methods); Ok(()) @@ -669,6 +674,7 @@ impl Parser { is_static: bool, is_readonly: bool, is_readonly_class: bool, + is_abstract: bool, ) -> Result<(EvalClassProperty, Vec), EvalParseError> { if is_static && is_readonly { return Err(EvalParseError::UnsupportedConstruct); @@ -681,13 +687,28 @@ impl Parser { let name = name.clone(); self.advance(); let default = if self.consume(TokenKind::Equal) { - if effective_readonly { + if is_abstract || effective_readonly { return Err(EvalParseError::UnsupportedConstruct); } Some(self.parse_expr()?) } else { None }; + if is_abstract { + if is_static || effective_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + let (requires_get_hook, requires_set_hook) = self.parse_property_hook_contracts()?; + let property = EvalClassProperty::with_visibility_static_and_readonly( + name, + visibility, + is_static, + effective_readonly, + None, + ) + .with_abstract_hook_contract(requires_get_hook, requires_set_hook); + return Ok((property, Vec::new())); + } let default_is_some = default.is_some(); let (has_get_hook, has_set_hook, hook_methods) = self.parse_property_hook_tail(&name, is_static, effective_readonly, default_is_some)?; @@ -872,11 +893,11 @@ impl Parser { return Ok(()); } let visibility = visibility.unwrap_or(EvalVisibility::Public); - if is_abstract || is_final { + if is_final { return Err(EvalParseError::UnsupportedConstruct); } let (property, mut hook_methods) = - self.parse_class_property_decl(visibility, is_static, is_readonly, false)?; + self.parse_class_property_decl(visibility, is_static, is_readonly, false, is_abstract)?; properties.push(property); methods.append(&mut hook_methods); Ok(()) @@ -1089,10 +1110,8 @@ impl Parser { Ok(EvalInterfaceProperty::new(name, requires_get, requires_set)) } - /// Parses `{ get; set; }` hook contracts for an interface property. - pub(super) fn parse_interface_property_hook_contracts( - &mut self, - ) -> Result<(bool, bool), EvalParseError> { + /// Parses `{ get; set; }` hook contracts for an abstract or interface property. + pub(super) fn parse_property_hook_contracts(&mut self) -> Result<(bool, bool), EvalParseError> { self.expect(TokenKind::LBrace)?; let mut requires_get = false; let mut requires_set = false; @@ -1137,6 +1156,13 @@ impl Parser { Ok((requires_get, requires_set)) } + /// Parses `{ get; set; }` hook contracts for an interface property. + pub(super) fn parse_interface_property_hook_contracts( + &mut self, + ) -> Result<(bool, bool), EvalParseError> { + self.parse_property_hook_contracts() + } + /// Consumes a simple declared property type before the `$property` token. pub(super) fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { if matches!(self.current(), TokenKind::DollarIdent(_)) { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 34e2019171..2a9cc4f6a0 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -246,6 +246,62 @@ fn parse_fragment_accepts_concrete_class_property_hooks() { ); } +/// Verifies abstract property hook contracts lower without concrete accessors. +#[test] +fn parse_fragment_accepts_abstract_class_property_hook_contracts() { + let program = parse_fragment( + br#"abstract class DynEvalAbstractHooked { + abstract public int $value { get; set; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_modifiers( + "DynEvalAbstractHooked", + true, + false, + None, + Vec::new(), + vec![EvalClassProperty::with_visibility_static_and_readonly( + "value", + EvalVisibility::Public, + false, + false, + None + ) + .with_abstract_hook_contract(true, true)], + Vec::new(), + ))] + ); +} + +/// Verifies trait abstract property hook contracts lower without concrete accessors. +#[test] +fn parse_fragment_accepts_trait_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"trait DynEvalAbstractHookTrait { + abstract protected string $name { get; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalAbstractHookTrait", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "name", + EvalVisibility::Protected, + false, + false, + None + ) + .with_abstract_hook_contract(true, false)], + Vec::new(), + ))] + ); +} + /// Verifies eval rejects readonly property forms that PHP does not allow. #[test] fn parse_fragment_rejects_invalid_readonly_class_properties() { @@ -264,8 +320,14 @@ fn parse_fragment_rejects_invalid_property_hooks() { .expect_err("hooked properties cannot have defaults in eval"); parse_fragment(b"class DynEvalHookStatic { public static int $id { get => 1; } }") .expect_err("static properties cannot have hooks in eval"); - parse_fragment(b"class DynEvalHookAbstract { public int $id { get; } }") - .expect_err("abstract property hooks are not supported in eval classes"); + parse_fragment( + b"abstract class DynEvalHookAbstractDefault { abstract public int $id = 1 { get; } }", + ) + .expect_err("abstract properties cannot have defaults in eval"); + parse_fragment( + b"abstract class DynEvalHookAbstractStatic { abstract public static int $id { get; } }", + ) + .expect_err("static properties cannot have abstract hooks in eval"); } /// Verifies eval rejects concrete hook bodies in interface property contracts. diff --git a/docs/php/eval.md b/docs/php/eval.md index 162f977848..89656bbd61 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -131,13 +131,13 @@ containers to eval-declared functions. Eval-declared classes support inheritance, public/protected/private properties and methods, concrete property `get` / `set` hooks, interface property hook -contract checks, property-level `readonly`, `readonly class`, `__construct()`, -abstract classes and methods, final classes and methods, trait composition with -`insteadof` conflict resolution and `as` aliases/visibility adaptations, -interface implementation checks, static properties, static methods, class -constants, interface constants, trait constants, and `ClassName::class` -literals. Member visibility is checked at runtime for eval-declared objects and -static/class-constant accesses. +contract checks, abstract property hook contracts, property-level `readonly`, +`readonly class`, `__construct()`, abstract classes and methods, final classes +and methods, trait composition with `insteadof` conflict resolution and `as` +aliases/visibility adaptations, interface implementation checks, static +properties, static methods, class constants, interface constants, trait +constants, and `ClassName::class` literals. Member visibility is checked at +runtime for eval-declared objects and static/class-constant accesses. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the @@ -292,9 +292,9 @@ arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are enum trait-use declarations, abstract property -hook contracts, attributes/reflection metadata, and generated/AOT dynamic -static-method call forms. +remaining class-system gaps are enum trait-use declarations, +attributes/reflection metadata, and generated/AOT dynamic static-method call +forms. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 95121379d1..0704951285 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5041,6 +5041,50 @@ class EvalIfaceHookReadOnlyBox implements EvalIfaceHookSetContract { ); } +/// Verifies eval-declared abstract property hook contracts validate concrete subclasses. +#[test] +fn test_eval_declared_abstract_property_hook_contracts() { + let out = compile_and_run_capture( + r#" $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalPlainAbstractHookBox extends EvalAbstractHookBase { + public string $value = "Grace"; +} +$box = new EvalAbstractHookBox(); +$box->value = "Ada"; +$plain = new EvalPlainAbstractHookBox(); +echo $box->value . ":" . $plain->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada!:Grace"); + + let err = compile_and_run_expect_failure( + r#" Date: Thu, 18 Jun 2026 19:16:22 +0200 Subject: [PATCH 0343/1208] Support eval class attribute metadata --- crates/elephc-eval/src/eval_ir.rs | 89 ++++++++++ .../interpreter/builtins/class_metadata.rs | 99 ++++++++++- .../builtins/registry/dispatch/symbols.rs | 2 +- crates/elephc-eval/src/interpreter/mod.rs | 10 +- .../elephc-eval/src/interpreter/statements.rs | 3 +- .../tests/builtins_class_metadata.rs | 55 ++++-- crates/elephc-eval/src/lexer/scan.rs | 6 + crates/elephc-eval/src/lexer/token.rs | 1 + crates/elephc-eval/src/parser/statements.rs | 160 ++++++++++++++++-- .../src/parser/tests/classes_errors.rs | 61 +++++++ docs/php/eval.md | 21 ++- tests/codegen/eval.rs | 27 +++ 12 files changed, 484 insertions(+), 50 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index c207f69908..ac68686726 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -176,12 +176,49 @@ impl EvalFunction { } } +/// Literal attribute argument metadata retained by eval declarations. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalAttributeArg { + String(String), + Int(i64), + Bool(bool), + Null, +} + +/// Attribute metadata retained for eval class-like declarations. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalAttribute { + name: String, + args: Option>, +} + +impl EvalAttribute { + /// Creates one eval attribute metadata entry. + pub fn new(name: impl Into, args: Option>) -> Self { + Self { + name: name.into(), + args, + } + } + + /// Returns the resolved PHP-visible attribute class name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns supported literal positional args, or `None` for unsupported metadata. + pub fn args(&self) -> Option<&[EvalAttributeArg]> { + self.args.as_deref() + } +} + /// Runtime enum declared by an eval fragment. #[derive(Debug, Clone, PartialEq)] pub struct EvalEnum { name: String, backing_type: Option, interfaces: Vec, + attributes: Vec, cases: Vec, constants: Vec, methods: Vec, @@ -217,12 +254,19 @@ impl EvalEnum { name: name.into(), backing_type, interfaces, + attributes: Vec::new(), cases, constants, methods, } } + /// Returns a copy of this enum with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Returns the original source spelling of this eval-declared enum name. pub fn name(&self) -> &str { &self.name @@ -238,6 +282,11 @@ impl EvalEnum { &self.interfaces } + /// Returns attributes declared directly on this eval enum. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns cases declared directly by this eval enum. pub fn cases(&self) -> &[EvalEnumCase] { &self.cases @@ -271,6 +320,7 @@ impl EvalEnum { Vec::new(), self.methods.clone(), ) + .with_attributes(self.attributes.clone()) } } @@ -313,6 +363,7 @@ impl EvalEnumCase { pub struct EvalInterface { name: String, parents: Vec, + attributes: Vec, constants: Vec, properties: Vec, methods: Vec, @@ -349,12 +400,19 @@ impl EvalInterface { Self { name: name.into(), parents, + attributes: Vec::new(), constants, properties, methods, } } + /// Returns a copy of this interface with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Returns the original source spelling of this eval-declared interface name. pub fn name(&self) -> &str { &self.name @@ -365,6 +423,11 @@ impl EvalInterface { &self.parents } + /// Returns attributes declared directly on this eval interface. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns constants declared directly by this eval interface. pub fn constants(&self) -> &[EvalClassConstant] { &self.constants @@ -467,6 +530,7 @@ pub struct EvalClass { is_readonly_class: bool, parent: Option, interfaces: Vec, + attributes: Vec, traits: Vec, trait_adaptations: Vec, constants: Vec, @@ -694,6 +758,7 @@ impl EvalClass { is_readonly_class, parent, interfaces, + attributes: Vec::new(), traits, trait_adaptations, constants, @@ -702,6 +767,12 @@ impl EvalClass { } } + /// Returns a copy of this class with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Marks all instance properties readonly when this metadata represents a `readonly class`. pub fn with_readonly_instance_properties(mut self) -> Self { if self.is_readonly_class { @@ -744,6 +815,11 @@ impl EvalClass { &self.interfaces } + /// Returns attributes declared directly on this eval class. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns trait names used directly by this eval class. pub fn traits(&self) -> &[String] { &self.traits @@ -847,6 +923,7 @@ impl EvalClassConstant { #[derive(Debug, Clone, PartialEq)] pub struct EvalTrait { name: String, + attributes: Vec, constants: Vec, properties: Vec, methods: Vec, @@ -871,17 +948,29 @@ impl EvalTrait { ) -> Self { Self { name: name.into(), + attributes: Vec::new(), constants, properties, methods, } } + /// Returns a copy of this trait with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Returns the original source spelling of this eval-declared trait name. pub fn name(&self) -> &str { &self.name } + /// Returns attributes declared directly on this eval trait. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns constants declared directly by this eval trait. pub fn constants(&self) -> &[EvalClassConstant] { &self.constants diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 6bc8aaf864..1c9544456b 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -6,8 +6,8 @@ //! - Dynamic callable dispatch under `builtins::registry::dispatch`. //! //! Key details: -//! - Eval-declared classes carry parent, interface, and direct trait-use metadata; -//! attribute metadata remains empty. +//! - Eval-declared class-like symbols carry parent, interface, direct trait-use, +//! and class-level attribute metadata for literal positional args. //! - Missing class-like relation targets return `false`, matching the main //! backend's unknown-target fallback. @@ -104,29 +104,114 @@ pub(in crate::interpreter) fn eval_builtin_class_attribute_metadata( ], _ => return Err(EvalStatus::RuntimeFatal), }; - eval_class_attribute_metadata_result(name, &evaluated_args, values) + eval_class_attribute_metadata_result(name, &evaluated_args, context, values) } /// Evaluates materialized class attribute metadata arguments. pub(in crate::interpreter) fn eval_class_attribute_metadata_result( name: &str, evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match (name, evaluated_args) { - ("class_attribute_names" | "class_get_attributes", [class_name]) => { + ("class_attribute_names", [class_name]) => { + let class_name = eval_class_metadata_name(*class_name, values)?; + let Some(attributes) = eval_class_like_attributes(context, &class_name) else { + return values.array_new(0); + }; + eval_class_attribute_names_result(attributes, values) + } + ("class_get_attributes", [class_name]) => { let _ = eval_class_metadata_name(*class_name, values)?; values.array_new(0) } ("class_attribute_args", [class_name, attribute_name]) => { - let _ = eval_class_metadata_name(*class_name, values)?; - let _ = eval_class_metadata_name(*attribute_name, values)?; - values.array_new(0) + let class_name = eval_class_metadata_name(*class_name, values)?; + let attribute_name = eval_class_metadata_name(*attribute_name, values)?; + let Some(attributes) = eval_class_like_attributes(context, &class_name) else { + return values.array_new(0); + }; + let Some(attribute) = attributes + .iter() + .find(|attribute| eval_attribute_name_matches(attribute.name(), &attribute_name)) + else { + return values.array_new(0); + }; + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_class_attribute_args_result(args, values) } _ => Err(EvalStatus::RuntimeFatal), } } +/// Returns class-like attributes for a dynamic eval class, interface, trait, or enum. +fn eval_class_like_attributes<'a>( + context: &'a ElephcEvalContext, + name: &str, +) -> Option<&'a [EvalAttribute]> { + if let Some(class) = context.class(name) { + return Some(class.attributes()); + } + if let Some(interface) = context.interface(name) { + return Some(interface.attributes()); + } + if let Some(trait_decl) = context.trait_decl(name) { + return Some(trait_decl.attributes()); + } + context.enum_decl(name).map(EvalEnum::attributes) +} + +/// Builds the indexed string array returned by `class_attribute_names()`. +fn eval_class_attribute_names_result( + attributes: &[EvalAttribute], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(attributes.len())?; + for (index, attribute) in attributes.iter().enumerate() { + let key = values.int(index as i64)?; + let value = values.string(attribute.name())?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds the indexed mixed array returned by `class_attribute_args()`. +fn eval_class_attribute_args_result( + args: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(args.len())?; + for (index, arg) in args.iter().enumerate() { + let key = values.int(index as i64)?; + let value = eval_class_attribute_arg_value(arg, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Materializes one retained eval attribute argument as a runtime cell. +fn eval_class_attribute_arg_value( + arg: &EvalAttributeArg, + values: &mut impl RuntimeValueOps, +) -> Result { + match arg { + EvalAttributeArg::String(value) => values.string(value), + EvalAttributeArg::Int(value) => values.int(*value), + EvalAttributeArg::Bool(value) => values.bool_value(*value), + EvalAttributeArg::Null => values.null(), + } +} + +/// Returns whether a query names the same PHP attribute class case-insensitively. +fn eval_attribute_name_matches(attribute_name: &str, query: &str) -> bool { + attribute_name + .trim_start_matches('\\') + .eq_ignore_ascii_case(query.trim_start_matches('\\')) +} + /// Returns whether a class-relation target refers to a known class-like symbol. fn eval_class_relation_target_name( target: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index c732a76d37..068d3d5e52 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -56,7 +56,7 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, "class_alias" => eval_class_alias_result(evaluated_args, context, values)?, "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { - eval_class_attribute_metadata_result(name, evaluated_args, values)? + eval_class_attribute_metadata_result(name, evaluated_args, context, values)? } "class_implements" | "class_parents" | "class_uses" => { eval_class_relation_result(name, evaluated_args, context, values)? diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index ea42968a58..ba765e1a52 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -30,11 +30,11 @@ mod statements; use crate::context::{ElephcEvalContext, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassConstant, - EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, - EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, - EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, - EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, + EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, + EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, + EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, + EvalInterfaceProperty, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, + EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 3d738cc708..97b1111bbf 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -748,7 +748,8 @@ fn expand_eval_class_traits( constants, properties, methods, - ), + ) + .with_attributes(class.attributes().to_vec()), ) } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 7be3767b9f..bdeefeb972 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -5,8 +5,8 @@ //! - `cargo test -p elephc-eval` through Rust's test harness. //! //! Key details: -//! - Eval class declarations expose parent/interface metadata while trait and -//! attribute metadata remains empty. +//! - Eval class declarations expose parent/interface metadata plus class-level +//! attribute names and supported literal positional args. //! - Tests verify direct calls, dynamic calls, named arguments, and builtin probes. use super::super::*; @@ -74,24 +74,31 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies class attribute helpers expose empty metadata arrays in eval. +/// Verifies class attribute helpers expose eval class-level metadata. #[test] fn execute_program_dispatches_class_attribute_metadata_builtins() { let program = parse_fragment( - br#"class EvalAttrMeta {} + br#"#[Route("/home", -1, true, null)] +#[Tag("first"), Tag("second")] +class EvalAttrMeta {} $names = class_attribute_names("EvalAttrMeta"); -echo is_array($names) && count($names) === 0 ? "names" : "bad"; echo ":"; +echo count($names); echo ":"; echo $names[0]; echo ":"; echo $names[1]; echo ":"; echo $names[2]; echo ":"; +$args = class_attribute_args("EvalAttrMeta", "route"); +echo count($args); echo ":"; echo $args[0]; echo ":"; echo $args[1]; echo ":"; +echo $args[2] ? "T" : "F"; echo ":"; echo is_null($args[3]) ? "N" : "bad"; echo ":"; +$tag = class_attribute_args("evalattrmeta", "Tag"); +echo $tag[0]; echo ":"; +$missing = class_attribute_args("EvalAttrMeta", "Missing"); +echo count($missing); echo ":"; $attrs = class_get_attributes("EvalAttrMeta"); -echo is_array($attrs) && count($attrs) === 0 ? "attrs" : "bad"; echo ":"; -$args = class_attribute_args("EvalAttrMeta", "DemoAttr"); -echo is_array($args) && count($args) === 0 ? "args" : "bad"; echo ":"; +echo count($attrs); echo ":"; $call_names = call_user_func("class_attribute_names", "EvalAttrMeta"); -echo is_array($call_names) && count($call_names) === 0 ? "callnames" : "bad"; echo ":"; +echo $call_names[0]; echo ":"; $call_args = call_user_func_array( "class_attribute_args", - ["class_name" => "EvalAttrMeta", "attribute_name" => "DemoAttr"] + ["class_name" => "EvalAttrMeta", "attribute_name" => "Route"] ); -echo is_array($call_args) && count($call_args) === 0 ? "callargs" : "bad"; echo ":"; +echo $call_args[0]; echo ":"; echo function_exists("class_attribute_names"); echo function_exists("class_get_attributes"); echo function_exists("class_attribute_args"); return true;"#, @@ -102,6 +109,30 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "names:attrs:args:callnames:callargs:111"); + assert_eq!( + values.output, + "3:Route:Tag:Tag:4:/home:-1:T:N:first:0:0:Route:/home:111" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies unsupported attribute argument metadata remains name-visible but not materializable. +#[test] +fn execute_program_rejects_unsupported_class_attribute_args_metadata() { + let program = parse_fragment( + br#"#[Tag($dynamic)] +class EvalUnsupportedAttr {} +$names = class_attribute_names("EvalUnsupportedAttr"); +echo count($names); echo ":"; echo $names[0]; echo ":"; +class_attribute_args("EvalUnsupportedAttr", "Tag");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unsupported attribute metadata should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.output, "1:Tag:"); +} diff --git a/crates/elephc-eval/src/lexer/scan.rs b/crates/elephc-eval/src/lexer/scan.rs index a5cca800ef..59ca47147d 100644 --- a/crates/elephc-eval/src/lexer/scan.rs +++ b/crates/elephc-eval/src/lexer/scan.rs @@ -297,6 +297,11 @@ impl<'a> Lexer<'a> { self.bump_char(); Ok(TokenKind::Backslash) } + '#' if self.peek_next_char() == Some('[') => { + self.bump_char(); + self.bump_char(); + Ok(TokenKind::AttributeStart) + } _ if is_ident_start(ch) => { let ident = self.lex_ident(); Ok(magic_const_token(&ident, line).unwrap_or(TokenKind::Ident(ident))) @@ -409,6 +414,7 @@ impl<'a> Lexer<'a> { } match (self.peek_char(), self.peek_next_char()) { (Some('/'), Some('/')) => self.skip_line_comment(), + (Some('#'), Some('[')) => return Ok(()), (Some('#'), _) => self.skip_line_comment(), (Some('/'), Some('*')) => self.skip_block_comment()?, _ => return Ok(()), diff --git a/crates/elephc-eval/src/lexer/token.rs b/crates/elephc-eval/src/lexer/token.rs index 6c3c25ce81..6902dbcd3b 100644 --- a/crates/elephc-eval/src/lexer/token.rs +++ b/crates/elephc-eval/src/lexer/token.rs @@ -77,5 +77,6 @@ pub(crate) enum TokenKind { Colon, DoubleColon, Backslash, + AttributeStart, Eof, } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index b0fa73cbd2..472d11d84d 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -12,16 +12,19 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, - EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInterface, EvalInterfaceMethod, - EvalInterfaceProperty, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, - EvalVisibility, + EvalAttribute, EvalAttributeArg, EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, + EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, + EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalStmt, EvalSwitchCase, EvalTrait, + EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::lexer::TokenKind; impl Parser { /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. pub(super) fn parse_stmt(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::AttributeStart) { + return self.parse_attributed_stmt(); + } match self.current() { TokenKind::Ident(name) if ident_eq(name, "break") => { self.advance(); @@ -125,6 +128,78 @@ impl Parser { } } + /// Parses one declaration preceded by PHP attribute groups. + pub(super) fn parse_attributed_stmt(&mut self) -> Result, EvalParseError> { + let attributes = self.parse_attribute_groups()?; + match self.current() { + TokenKind::Ident(name) + if ident_eq(name, "abstract") + || ident_eq(name, "final") + || ident_eq(name, "readonly") + || ident_eq(name, "class") => + { + self.parse_class_decl_stmt_with_attributes(attributes) + } + TokenKind::Ident(name) if ident_eq(name, "enum") => { + self.parse_enum_decl_stmt_with_attributes(attributes) + } + TokenKind::Ident(name) if ident_eq(name, "interface") => { + self.parse_interface_decl_stmt_with_attributes(attributes) + } + TokenKind::Ident(name) if ident_eq(name, "trait") => { + self.parse_trait_decl_stmt_with_attributes(attributes) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses one or more PHP `#[...]` attribute groups. + pub(super) fn parse_attribute_groups(&mut self) -> Result, EvalParseError> { + let mut attributes = Vec::new(); + while self.consume(TokenKind::AttributeStart) { + loop { + attributes.push(self.parse_attribute()?); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect(TokenKind::RBracket)?; + } + Ok(attributes) + } + + /// Parses one attribute name and optional literal positional arguments. + pub(super) fn parse_attribute(&mut self) -> Result { + let name = self.parse_qualified_name()?; + let name = self.resolve_class_name(name); + let args = if self.consume(TokenKind::LParen) { + let mut args = Vec::new(); + let mut supported = true; + if !self.consume(TokenKind::RParen) { + loop { + let arg = self.parse_call_arg()?; + if supported { + if arg.name().is_some() || arg.is_spread() { + supported = false; + } else if let Some(arg) = eval_attribute_arg_from_expr(arg.value()) { + args.push(arg); + } else { + supported = false; + } + } + if self.consume(TokenKind::RParen) { + break; + } + self.expect(TokenKind::Comma)?; + } + } + supported.then_some(args) + } else { + Some(Vec::new()) + }; + Ok(EvalAttribute::new(name, args)) + } + /// Returns true when the current tokens form `Class::$property `. pub(super) fn current_starts_static_property_assignment(&self) -> bool { let mut pos = self.pos; @@ -251,6 +326,14 @@ impl Parser { /// Parses `[abstract|final|readonly] class Name [extends Parent] [implements Iface, ...] { ... }`. pub(super) fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_class_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses a class declaration and attaches already parsed class attributes. + pub(super) fn parse_class_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { let (is_abstract, is_final, is_readonly_class) = self.parse_class_decl_modifiers()?; if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { return Err(EvalParseError::UnsupportedConstruct); @@ -296,7 +379,8 @@ impl Parser { constants, properties, methods, - ), + ) + .with_attributes(attributes), )]) } @@ -838,6 +922,14 @@ impl Parser { /// Parses `trait Name { ... }` declarations into dynamic trait metadata. pub(super) fn parse_trait_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_trait_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses a trait declaration and attaches already parsed class-like attributes. + pub(super) fn parse_trait_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -855,9 +947,10 @@ impl Parser { self.parse_trait_member(&mut constants, &mut properties, &mut methods)?; } self.consume_semicolon(); - Ok(vec![EvalStmt::TraitDecl(EvalTrait::with_constants( - name, constants, properties, methods, - ))]) + Ok(vec![EvalStmt::TraitDecl( + EvalTrait::with_constants(name, constants, properties, methods) + .with_attributes(attributes), + )]) } /// Parses one property or method from an eval trait body. @@ -905,6 +998,14 @@ impl Parser { /// Parses `enum Name [: int|string] [implements Iface, ...] { ... }`. pub(super) fn parse_enum_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_enum_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses an enum declaration and attaches already parsed class-like attributes. + pub(super) fn parse_enum_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -924,14 +1025,10 @@ impl Parser { self.parse_enum_member(&mut cases, &mut constants, &mut methods)?; } self.consume_semicolon(); - Ok(vec![EvalStmt::EnumDecl(EvalEnum::with_members( - name, - backing_type, - interfaces, - cases, - constants, - methods, - ))]) + Ok(vec![EvalStmt::EnumDecl( + EvalEnum::with_members(name, backing_type, interfaces, cases, constants, methods) + .with_attributes(attributes), + )]) } /// Parses an optional backed-enum scalar type after the enum name. @@ -1010,6 +1107,14 @@ impl Parser { /// Parses `interface Name [extends Parent, ...] { function name(...); }`. pub(super) fn parse_interface_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_interface_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses an interface declaration and attaches already parsed class-like attributes. + pub(super) fn parse_interface_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1031,7 +1136,8 @@ impl Parser { Ok(vec![EvalStmt::InterfaceDecl( EvalInterface::with_constants_and_properties( name, parents, constants, properties, methods, - ), + ) + .with_attributes(attributes), )]) } @@ -1857,6 +1963,26 @@ impl Parser { } } +/// Converts a parsed attribute argument expression into retained literal metadata. +fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { + match expr { + EvalExpr::Const(EvalConst::String(value)) => Some(EvalAttributeArg::String(value.clone())), + EvalExpr::Const(EvalConst::Int(value)) => Some(EvalAttributeArg::Int(*value)), + EvalExpr::Const(EvalConst::Bool(value)) => Some(EvalAttributeArg::Bool(*value)), + EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Null), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => match expr.as_ref() { + EvalExpr::Const(EvalConst::Int(value)) => { + Some(EvalAttributeArg::Int(value.wrapping_neg())) + } + _ => None, + }, + _ => None, + } +} + /// Returns the synthetic get-hook method name for one property. fn property_hook_get_method(property_name: &str) -> String { format!("__propget_{property_name}") diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 2a9cc4f6a0..894ecfa40b 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -40,6 +40,67 @@ fn parse_fragment_accepts_class_extends_and_implements_source() { ))] ); } + +/// Verifies class attributes lower to eval class metadata with supported literal args. +#[test] +fn parse_fragment_accepts_class_attribute_metadata() { + let program = parse_fragment( + br#"#[Route("/home", -1, true, null)] +#[Tag(name: "named")] +class DynEvalAttributed {}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::new("DynEvalAttributed", Vec::new(), Vec::new()).with_attributes(vec![ + EvalAttribute::new( + "Route", + Some(vec![ + EvalAttributeArg::String("/home".to_string()), + EvalAttributeArg::Int(-1), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + ]), + ), + EvalAttribute::new("Tag", None), + ]) + )] + ); +} + +/// Verifies class-like declaration attributes attach to interfaces, traits, and enums. +#[test] +fn parse_fragment_accepts_class_like_attribute_metadata() { + let program = parse_fragment( + br#"#[IfaceMark] interface DynEvalAttrIface {} +#[TraitMark] trait DynEvalAttrTrait {} +#[EnumMark] enum DynEvalAttrEnum { case Ready; }"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::InterfaceDecl( + EvalInterface::new("DynEvalAttrIface", Vec::new(), Vec::new()) + .with_attributes(vec![EvalAttribute::new("IfaceMark", Some(Vec::new()))]) + ), + EvalStmt::TraitDecl( + EvalTrait::new("DynEvalAttrTrait", Vec::new(), Vec::new()) + .with_attributes(vec![EvalAttribute::new("TraitMark", Some(Vec::new()))]) + ), + EvalStmt::EnumDecl( + EvalEnum::new( + "DynEvalAttrEnum", + None, + vec![EvalEnumCase::new("Ready", None)] + ) + .with_attributes(vec![EvalAttribute::new("EnumMark", Some(Vec::new()))]) + ), + ] + ); +} + /// Verifies eval interface declarations lower to dynamic interface metadata. #[test] fn parse_fragment_accepts_interface_declaration_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 89656bbd61..bc8e53dd9c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, and class constants. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -136,8 +136,14 @@ contract checks, abstract property hook contracts, property-level `readonly`, and methods, trait composition with `insteadof` conflict resolution and `as` aliases/visibility adaptations, interface implementation checks, static properties, static methods, class constants, interface constants, trait -constants, and `ClassName::class` literals. Member visibility is checked at -runtime for eval-declared objects and static/class-constant accesses. +constants, class-level attributes, and `ClassName::class` literals. Member +visibility is checked at runtime for eval-declared objects and +static/class-constant accesses. Class-level attributes declared on eval classes, +interfaces, traits, and enums are visible through `class_attribute_names()` and +`class_attribute_args()` when their arguments are supported literal positional +values (`string`, `int`, `bool`, `null`, or negated integer literals). +Attribute names remain visible when an attribute uses unsupported argument +syntax, but requesting those arguments is a runtime fatal. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the @@ -219,7 +225,7 @@ where listed below unless a note says otherwise. | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | | Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | -| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `is_a()`, `is_subclass_of()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()` | | Debug output | `print_r()`, `var_dump()` | | Constants | `define()`, `defined()` | @@ -292,9 +298,10 @@ arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are enum trait-use declarations, -attributes/reflection metadata, and generated/AOT dynamic static-method call -forms. +remaining class-system gaps are ReflectionAttribute object materialization, +member-level attribute metadata, and generated/AOT dynamic static-method call +forms. `class_get_attributes()` is recognized in eval but does not yet +materialize `ReflectionAttribute` objects for eval-declared attributes. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0704951285..81a3dcc6bc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5249,6 +5249,33 @@ echo EvalClassNameChild::staticName();'); ); } +/// Verifies eval-declared class attributes expose names and supported literal args. +#[test] +fn test_eval_declared_class_attribute_metadata() { + let out = compile_and_run_capture( + r#" Date: Thu, 18 Jun 2026 19:21:53 +0200 Subject: [PATCH 0344/1208] Retain eval member attribute metadata --- crates/elephc-eval/src/eval_ir.rs | 79 ++++++++++++ crates/elephc-eval/src/parser/statements.rs | 102 +++++++++++----- .../src/parser/tests/classes_errors.rs | 115 ++++++++++++++++++ docs/php/eval.md | 12 +- 4 files changed, 274 insertions(+), 34 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index ac68686726..1f7647fa51 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -335,6 +335,7 @@ pub enum EvalEnumBackingType { #[derive(Debug, Clone, PartialEq)] pub struct EvalEnumCase { name: String, + attributes: Vec, value: Option, } @@ -343,15 +344,27 @@ impl EvalEnumCase { pub fn new(name: impl Into, value: Option) -> Self { Self { name: name.into(), + attributes: Vec::new(), value, } } + /// Returns a copy of this enum case with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Returns the PHP-visible enum case name. pub fn name(&self) -> &str { &self.name } + /// Returns attributes declared directly on this enum case. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns the optional backing value expression. pub fn value(&self) -> Option<&EvalExpr> { self.value.as_ref() @@ -455,6 +468,7 @@ impl EvalInterface { #[derive(Debug, Clone, PartialEq)] pub struct EvalInterfaceProperty { name: String, + attributes: Vec, requires_get: bool, requires_set: bool, } @@ -464,16 +478,28 @@ impl EvalInterfaceProperty { pub fn new(name: impl Into, requires_get: bool, requires_set: bool) -> Self { Self { name: name.into(), + attributes: Vec::new(), requires_get, requires_set, } } + /// Returns a copy of this interface property with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Returns the PHP-visible property name. pub fn name(&self) -> &str { &self.name } + /// Returns attributes declared directly on this interface property. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns whether the interface requires the property to be readable. pub const fn requires_get(&self) -> bool { self.requires_get @@ -488,6 +514,7 @@ impl EvalInterfaceProperty { pub fn merged_with(&self, other: &Self) -> Self { Self { name: self.name.clone(), + attributes: self.attributes.clone(), requires_get: self.requires_get || other.requires_get, requires_set: self.requires_set || other.requires_set, } @@ -498,6 +525,7 @@ impl EvalInterfaceProperty { #[derive(Debug, Clone, PartialEq)] pub struct EvalInterfaceMethod { name: String, + attributes: Vec, params: Vec, } @@ -506,15 +534,27 @@ impl EvalInterfaceMethod { pub fn new(name: impl Into, params: Vec) -> Self { Self { name: name.into(), + attributes: Vec::new(), params, } } + /// Returns a copy of this interface method with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Returns the PHP-visible method name. pub fn name(&self) -> &str { &self.name } + /// Returns attributes declared directly on this interface method. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns source-order parameter names without leading `$`. pub fn params(&self) -> &[String] { &self.params @@ -880,6 +920,7 @@ pub enum EvalTraitAdaptation { #[derive(Debug, Clone, PartialEq)] pub struct EvalClassConstant { name: String, + attributes: Vec, visibility: EvalVisibility, value: EvalExpr, } @@ -898,16 +939,28 @@ impl EvalClassConstant { ) -> Self { Self { name: name.into(), + attributes: Vec::new(), visibility, value, } } + /// Returns a copy of this class constant with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Returns the PHP-visible class constant name. pub fn name(&self) -> &str { &self.name } + /// Returns attributes declared directly on this class constant. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns the PHP visibility declared for this constant. pub const fn visibility(&self) -> EvalVisibility { self.visibility @@ -998,6 +1051,7 @@ impl EvalTrait { #[derive(Debug, Clone, PartialEq)] pub struct EvalClassProperty { name: String, + attributes: Vec, visibility: EvalVisibility, is_static: bool, is_readonly: bool, @@ -1044,6 +1098,7 @@ impl EvalClassProperty { ) -> Self { Self { name: name.into(), + attributes: Vec::new(), visibility, is_static, is_readonly, @@ -1075,11 +1130,22 @@ impl EvalClassProperty { self } + /// Returns a copy of this property with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + /// Returns the PHP-visible property name without `$`. pub fn name(&self) -> &str { &self.name } + /// Returns attributes declared directly on this class property. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns the PHP visibility declared for this property. pub const fn visibility(&self) -> EvalVisibility { self.visibility @@ -1138,6 +1204,7 @@ pub enum EvalVisibility { #[derive(Debug, Clone, PartialEq)] pub struct EvalClassMethod { name: String, + attributes: Vec, visibility: EvalVisibility, is_static: bool, is_abstract: bool, @@ -1183,6 +1250,7 @@ impl EvalClassMethod { ) -> Self { Self { name: name.into(), + attributes: Vec::new(), visibility, is_static, is_abstract, @@ -1197,6 +1265,17 @@ impl EvalClassMethod { &self.name } + /// Returns a copy of this method with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns attributes declared directly on this class method. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns a copy of this method with a different PHP-visible name. pub fn renamed(&self, name: impl Into) -> Self { let mut method = self.clone(); diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 472d11d84d..3173ce5df8 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -456,6 +456,7 @@ impl Parser { traits: &mut Vec, trait_adaptations: &mut Vec, ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; let (visibility, is_static, is_abstract, is_final, is_readonly) = self.parse_class_member_modifiers()?; @@ -470,6 +471,9 @@ impl Parser { && !is_readonly && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } self.parse_class_trait_use(traits, trait_adaptations)?; return Ok(()); } @@ -478,8 +482,10 @@ impl Parser { if is_static || is_abstract || is_final || is_readonly { return Err(EvalParseError::UnsupportedConstruct); } - constants - .push(self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))?); + constants.push( + self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))? + .with_attributes(attributes), + ); return Ok(()); } @@ -487,12 +493,15 @@ impl Parser { if is_readonly { return Err(EvalParseError::UnsupportedConstruct); } - methods.push(self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - is_abstract, - is_final, - )?); + methods.push( + self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + is_abstract, + is_final, + )? + .with_attributes(attributes), + ); return Ok(()); } @@ -507,11 +516,22 @@ impl Parser { is_readonly_class, is_abstract, )?; - properties.push(property); + properties.push(property.with_attributes(attributes)); methods.append(&mut hook_methods); Ok(()) } + /// Parses optional attributes that decorate one class-like member. + pub(super) fn parse_optional_member_attributes( + &mut self, + ) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::AttributeStart) { + self.parse_attribute_groups() + } else { + Ok(Vec::new()) + } + } + /// Parses one eval class constant declaration. pub(super) fn parse_class_const_decl( &mut self, @@ -960,6 +980,7 @@ impl Parser { properties: &mut Vec, methods: &mut Vec, ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; let (visibility, is_static, is_abstract, is_final, is_readonly) = self.parse_class_member_modifiers()?; if is_abstract && is_final { @@ -969,20 +990,25 @@ impl Parser { if is_static || is_abstract || is_final || is_readonly { return Err(EvalParseError::UnsupportedConstruct); } - constants - .push(self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))?); + constants.push( + self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))? + .with_attributes(attributes), + ); return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { if is_readonly { return Err(EvalParseError::UnsupportedConstruct); } - methods.push(self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - is_abstract, - is_final, - )?); + methods.push( + self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + is_abstract, + is_final, + )? + .with_attributes(attributes), + ); return Ok(()); } let visibility = visibility.unwrap_or(EvalVisibility::Public); @@ -991,7 +1017,7 @@ impl Parser { } let (property, mut hook_methods) = self.parse_class_property_decl(visibility, is_static, is_readonly, false, is_abstract)?; - properties.push(property); + properties.push(property.with_attributes(attributes)); methods.append(&mut hook_methods); Ok(()) } @@ -1059,8 +1085,9 @@ impl Parser { constants: &mut Vec, methods: &mut Vec, ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) { - cases.push(self.parse_enum_case_decl()?); + cases.push(self.parse_enum_case_decl()?.with_attributes(attributes)); return Ok(()); } let (visibility, is_static, is_abstract, is_final, is_readonly) = @@ -1072,17 +1099,22 @@ impl Parser { if is_static || is_final { return Err(EvalParseError::UnsupportedConstruct); } - constants - .push(self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))?); + constants.push( + self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))? + .with_attributes(attributes), + ); return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - methods.push(self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - false, - is_final, - )?); + methods.push( + self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + false, + is_final, + )? + .with_attributes(attributes), + ); return Ok(()); } Err(EvalParseError::UnsupportedConstruct) @@ -1165,6 +1197,7 @@ impl Parser { properties: &mut Vec, methods: &mut Vec, ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) { self.advance(); } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) @@ -1172,14 +1205,23 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - constants.push(self.parse_class_const_decl(EvalVisibility::Public)?); + constants.push( + self.parse_class_const_decl(EvalVisibility::Public)? + .with_attributes(attributes), + ); return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - methods.push(self.parse_interface_method_decl_after_function_keyword()?); + methods.push( + self.parse_interface_method_decl_after_function_keyword()? + .with_attributes(attributes), + ); return Ok(()); } - properties.push(self.parse_interface_property_decl()?); + properties.push( + self.parse_interface_property_decl()? + .with_attributes(attributes), + ); Ok(()) } diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 894ecfa40b..40b9d689a8 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -101,6 +101,121 @@ fn parse_fragment_accepts_class_like_attribute_metadata() { ); } +/// Verifies attributes on class constants, properties, and methods are retained. +#[test] +fn parse_fragment_accepts_class_member_attribute_metadata() { + let program = parse_fragment( + br#"class DynEvalMemberAttrs { + #[ConstMark] + public const SEED = 1; + #[PropMark("p")] + public int $value; + #[MethodMark(true)] + public function read() { return 1; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::with_modifiers_traits_and_constants( + "DynEvalMemberAttrs", + false, + false, + None, + Vec::new(), + Vec::new(), + vec![ + EvalClassConstant::new("SEED", EvalExpr::Const(EvalConst::Int(1))) + .with_attributes(vec![EvalAttribute::new("ConstMark", Some(Vec::new()))]) + ], + vec![EvalClassProperty::new("value", None).with_attributes(vec![ + EvalAttribute::new( + "PropMark", + Some(vec![EvalAttributeArg::String("p".to_string())]) + ) + ])], + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))] + ) + .with_attributes(vec![EvalAttribute::new( + "MethodMark", + Some(vec![EvalAttributeArg::Bool(true)]) + )])], + ) + )] + ); +} + +/// Verifies interface, trait, and enum member attributes are retained. +#[test] +fn parse_fragment_accepts_class_like_member_attribute_metadata() { + let program = parse_fragment( + br#"interface DynEvalMemberIface { + #[IfaceProp] + public string $value { get; } + #[IfaceMethod] + function read(); +} +trait DynEvalMemberTrait { + #[TraitProp] + public int $seed; + #[TraitMethod] + public function add() { return 2; } +} +enum DynEvalMemberEnum { + #[CaseMark] + case Ready; + #[EnumMethod] + public function label() { return "ready"; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::InterfaceDecl(EvalInterface::with_constants_and_properties( + "DynEvalMemberIface", + Vec::new(), + Vec::new(), + vec![EvalInterfaceProperty::new("value", true, false) + .with_attributes(vec![EvalAttribute::new("IfaceProp", Some(Vec::new()))])], + vec![EvalInterfaceMethod::new("read", Vec::new()) + .with_attributes(vec![EvalAttribute::new("IfaceMethod", Some(Vec::new()))])], + )), + EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalMemberTrait", + vec![EvalClassProperty::new("seed", None) + .with_attributes(vec![EvalAttribute::new("TraitProp", Some(Vec::new()))])], + vec![EvalClassMethod::new( + "add", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(2))))] + ) + .with_attributes(vec![EvalAttribute::new("TraitMethod", Some(Vec::new()))])], + )), + EvalStmt::EnumDecl(EvalEnum::with_members( + "DynEvalMemberEnum", + None, + Vec::new(), + vec![EvalEnumCase::new("Ready", None) + .with_attributes(vec![EvalAttribute::new("CaseMark", Some(Vec::new()))])], + Vec::new(), + vec![EvalClassMethod::new( + "label", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( + "ready".to_string() + ))))] + ) + .with_attributes(vec![EvalAttribute::new("EnumMethod", Some(Vec::new()))])], + )), + ] + ); +} + /// Verifies eval interface declarations lower to dynamic interface metadata. #[test] fn parse_fragment_accepts_interface_declaration_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index bc8e53dd9c..e0f7e68fcd 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -143,7 +143,10 @@ interfaces, traits, and enums are visible through `class_attribute_names()` and `class_attribute_args()` when their arguments are supported literal positional values (`string`, `int`, `bool`, `null`, or negated integer literals). Attribute names remain visible when an attribute uses unsupported argument -syntax, but requesting those arguments is a runtime fatal. +syntax, but requesting those arguments is a runtime fatal. Attributes on +methods, properties, constants, interface members, trait members, and enum cases +are parsed and retained for future reflection support, but eval does not expose +them through Reflection APIs yet. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the @@ -299,9 +302,10 @@ native method bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are ReflectionAttribute object materialization, -member-level attribute metadata, and generated/AOT dynamic static-method call -forms. `class_get_attributes()` is recognized in eval but does not yet -materialize `ReflectionAttribute` objects for eval-declared attributes. +Reflection exposure for member-level attributes, and generated/AOT dynamic +static-method call forms. `class_get_attributes()` is recognized in eval but +does not yet materialize `ReflectionAttribute` objects for eval-declared +attributes. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` From f522b8d069bfd1fabc3b2cf9f9b03b305ab25c96 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 19:39:25 +0200 Subject: [PATCH 0345/1208] Support eval AOT static method calls --- .../src/interpreter/runtime_ops.rs | 8 + .../elephc-eval/src/interpreter/statements.rs | 52 ++- .../src/interpreter/tests/dynamic_calls.rs | 37 ++ .../interpreter/tests/support/object_ops.rs | 39 ++ .../interpreter/tests/support/runtime_ops.rs | 9 + .../elephc-eval/src/runtime_hooks/externs.rs | 7 + crates/elephc-eval/src/runtime_hooks/ops.rs | 23 + docs/php/eval.md | 28 +- src/codegen/eval_method_helpers.rs | 413 +++++++++++++++++- tests/codegen/eval.rs | 31 ++ 10 files changed, 611 insertions(+), 36 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 758bae48d7..acc01cebec 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -87,6 +87,14 @@ pub trait RuntimeValueOps { args: Vec, ) -> Result; + /// Calls a named public static method through the generated AOT bridge. + fn static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result; + /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 97b1111bbf..ba0c83f3f0 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1629,7 +1629,7 @@ pub(in crate::interpreter) fn eval_static_method_call_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let class_name = resolve_eval_static_class_name(class_name, context)?; + let class_name = resolve_eval_static_method_class_name(class_name, context)?; if context.has_enum(&class_name) && eval_enum_static_builtin_name(method_name).is_some() { return eval_enum_builtin_static_method_result( &class_name, @@ -1639,21 +1639,34 @@ pub(in crate::interpreter) fn eval_static_method_call_result( values, ); } - let (declaring_class, method) = + if let Some((declaring_class, method)) = eval_dynamic_static_method_for_call(&class_name, method_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; - if !method.is_static() || method.is_abstract() { + { + if !method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, method.visibility(), context)?; + return eval_dynamic_static_method_with_values( + &declaring_class, + &class_name, + &method, + evaluated_args, + context, + values, + ); + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, method.visibility(), context)?; - eval_dynamic_static_method_with_values( - &declaring_class, - &class_name, - &method, - evaluated_args, - context, - values, - ) + if evaluated_args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + let args = evaluated_args.into_iter().map(|arg| arg.value).collect(); + values.static_method_call(&class_name, method_name, args) } /// Returns a recognized enum-provided static method name. @@ -1847,6 +1860,19 @@ fn resolve_eval_static_class_name( } } +/// Resolves static method receivers while allowing non-eval class names to reach AOT lookup. +fn resolve_eval_static_method_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + /// Resolves `self`, `parent`, `static`, and named class-like receivers for constant access. fn resolve_eval_static_class_like_name( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs index 8d6f8c6730..fb44cab439 100644 --- a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs @@ -201,6 +201,43 @@ return $named(right: "H", left: "G");"#, assert_eq!(values.get(result), FakeValue::String("GH".to_string())); } +/// Verifies static calls fall back to runtime AOT hooks when no eval class matches. +#[test] +fn execute_program_static_call_dispatches_runtime_method_hook() { + let program = parse_fragment( + br#"echo KnownClass::join("A", "B"); echo ":"; +$cb = ["KnownClass", "join"]; +echo call_user_func($cb, "C", "D"); echo ":"; +$named = "KnownClass::join"; +echo $named("E", "F"); echo ":"; +return call_user_func_array(["KnownClass", "sum"], [2, 5]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:CD:EF:"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies runtime AOT static method fallback rejects named arguments. +#[test] +fn execute_program_static_runtime_method_hook_rejects_named_args() { + let program = parse_fragment( + br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let error = + execute_program(&program, &mut scope, &mut values).expect_err("named AOT call should fail"); + + assert_eq!(error, EvalStatus::RuntimeFatal); +} + /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. #[test] fn execute_program_call_user_func_array_dispatches_declared_function() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index c16928166f..43e6599846 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -143,6 +143,45 @@ impl FakeOps { _ => Err(EvalStatus::UnsupportedConstruct), } } + /// Calls one fake public static AOT method by class and method name. + pub(super) fn runtime_static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + let method = method.to_ascii_lowercase(); + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Err(EvalStatus::UnsupportedConstruct); + } + match method.as_str() { + "join" => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.string(&format!("{}{}", left, right)) + } + "sum" => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(left + right) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } /// Creates one fake object for eval `new` unit tests. pub(super) fn runtime_new_object( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 5ad8ad0f5b..98cd28fdd5 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -92,6 +92,15 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_method_call(object, method, args) } + /// Calls one fake static runtime method by class and method name. + fn static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + self.runtime_static_method_call(class_name, method, args) + } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { self.runtime_new_object(_class_name) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index b7c2f5794a..9023a71243 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -56,6 +56,13 @@ unsafe extern "C" { name_len: u64, args: *mut RuntimeCell, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_static_method_call( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + args: *mut RuntimeCell, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, name_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 4b359c4309..5282e73c10 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -144,6 +144,29 @@ impl RuntimeValueOps for ElephcRuntimeOps { result } + /// Calls a public AOT static method through the generated user helper. + fn static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + let arg_array = Self::arg_array(args)?; + let result = Self::handle(unsafe { + __elephc_eval_value_static_method_call( + class_name.as_ptr(), + class_name.len() as u64, + method.as_ptr(), + method.len() as u64, + arg_array.as_ptr(), + ) + }); + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + result + } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { let object = Self::handle(unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index e0f7e68fcd..d77edcab73 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata use positional constructor arguments. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method fallback remains positional. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method and static-method fallback remains positional. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -117,10 +117,11 @@ eval-declared functions, and registered AOT functions. Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, ...)`, `call_user_func_array($cb, [...])`, and `iterator_apply()` with -positional arguments. Static method callables for eval-declared static methods -can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, -`call_user_func()`, and `call_user_func_array()`; `call_user_func_array()` can -also bind string-keyed named arguments for these eval-declared static methods. +positional arguments. Static method callables can use `["ClassName", "method"]` +or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and +`call_user_func_array()`. Eval-declared static methods also support string-keyed +named arguments through `call_user_func_array()`; generated/AOT static method +fallback remains positional. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -294,18 +295,17 @@ Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In -particular, advanced native callable descriptors, closure callback values, and -generated/AOT static-method callable arrays are still outside eval fragments. -Runtime/AOT object-method fallback from eval remains positional, so named method -arguments are supported for eval-declared methods but not for every generated -native method bridge. +particular, advanced native callable descriptors and closure callback values are +still outside eval fragments. Runtime/AOT object-method and static-method +fallback from eval remains positional, so named method arguments are supported +for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are ReflectionAttribute object materialization, -Reflection exposure for member-level attributes, and generated/AOT dynamic -static-method call forms. `class_get_attributes()` is recognized in eval but -does not yet materialize `ReflectionAttribute` objects for eval-declared -attributes. +Reflection exposure for member-level attributes, and broader generated/AOT +method bridge signatures beyond the current public scalar zero-to-two-argument +slice. `class_get_attributes()` is recognized in eval but does not yet +materialize `ReflectionAttribute` objects for eval-declared attributes. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 36812e8a4a..6cac491783 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -1,6 +1,6 @@ //! Purpose: //! Emits user-assembly helpers that let libelephc-eval call public native -//! instance methods on runtime objects known to the current module. +//! instance and static methods known to the current module. //! //! Called from: //! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. @@ -19,7 +19,7 @@ use crate::codegen::emit::Emitter; use crate::codegen::emit_box_current_value_as_mixed; use crate::codegen::platform::Arch; use crate::ir::{Function, LocalKind, Module}; -use crate::names::method_symbol; +use crate::names::{method_symbol, static_method_symbol}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -34,6 +34,17 @@ struct EvalMethodSlot { return_ty: PhpType, } +/// Static method metadata needed by eval static method-call bridge dispatch. +#[derive(Clone)] +struct EvalStaticMethodSlot { + class_id: u64, + class_name: String, + method: String, + impl_class: String, + params: Vec, + return_ty: PhpType, +} + const MAX_EVAL_METHOD_ARGS: usize = 2; const BUILTIN_THROWABLE_METHOD_CLASSES: &[&str] = &[ "Error", @@ -69,8 +80,10 @@ pub(super) fn emit_eval_method_helpers( return; } let slots = collect_eval_method_slots(module); + let static_slots = collect_eval_static_method_slots(module); let builtin_throwable_class_ids = collect_builtin_throwable_method_class_ids(module); emit_method_call_helper(module, emitter, data, &slots, &builtin_throwable_class_ids); + emit_static_method_call_helper(module, emitter, data, &static_slots); } /// Returns true when the EIR module contains a function that can call eval. @@ -113,6 +126,18 @@ fn collect_eval_method_slots(module: &Module) -> Vec { slots } +/// Collects public bridge-supported static methods backed by emitted EIR symbols. +fn collect_eval_static_method_slots(module: &Module) -> Vec { + let emitted_methods = super::eir_class_method_keys(module); + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_static_method_slots(class_name, class_info, &emitted_methods, &mut slots); + } + slots +} + /// Collects compact builtin Throwable class ids that eval can inspect directly. fn collect_builtin_throwable_method_class_ids(module: &Module) -> Vec { let mut class_ids = BUILTIN_THROWABLE_METHOD_CLASSES @@ -160,6 +185,41 @@ fn collect_class_method_slots( } } +/// Adds bridge-supported public static methods for one class. +fn collect_class_static_method_slots( + class_name: &str, + class_info: &ClassInfo, + emitted_methods: &std::collections::HashSet<(String, String, bool)>, + slots: &mut Vec, +) { + let mut methods = class_info.static_methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method, sig) in methods { + if !static_method_is_public(class_info, method) + || !method_signature_supported(sig) + || !method_return_supported(&sig.return_type) + { + continue; + } + let impl_class = class_info + .static_method_impl_classes + .get(method) + .map(String::as_str) + .unwrap_or(class_name); + if !emitted_methods.contains(&(impl_class.to_string(), method.clone(), true)) { + continue; + } + slots.push(EvalStaticMethodSlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + method: method.clone(), + impl_class: impl_class.to_string(), + params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), + return_ty: sig.return_type.codegen_repr(), + }); + } +} + /// Returns true when a method is publicly visible to runtime eval. fn method_is_public(class_info: &ClassInfo, method: &str) -> bool { class_info @@ -168,6 +228,14 @@ fn method_is_public(class_info: &ClassInfo, method: &str) -> bool { .is_none_or(|visibility| matches!(visibility, Visibility::Public)) } +/// Returns true when a static method is publicly visible to runtime eval. +fn static_method_is_public(class_info: &ClassInfo, method: &str) -> bool { + class_info + .static_method_visibilities + .get(method) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + /// Returns true for method signatures supported by this first eval bridge slice. fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_METHOD_ARGS @@ -222,6 +290,78 @@ fn emit_method_call_helper( } } +/// Emits `__elephc_eval_value_static_method_call(class, method, MixedArray*) -> Mixed*`. +fn emit_static_method_call_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user static method call ---"); + label_c_global(module, emitter, "__elephc_eval_value_static_method_call"); + match module.target.arch { + Arch::AArch64 => emit_static_method_call_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_static_method_call_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 static method-call helper body. +fn emit_static_method_call_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], +) { + let fail_label = "__elephc_eval_value_static_method_call_fail"; + let done_label = "__elephc_eval_value_static_method_call_done"; + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class, method, args, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested method-name pointer + emitter.instruction("str x4, [sp, #24]"); // save the boxed eval argument array + emitter.instruction("str x3, [sp, #40]"); // save the requested method-name length + emit_aarch64_static_method_dispatch(module, emitter, data, slots); + emitter.instruction(&format!("b {}", fail_label)); // no supported public static method matched the request + emit_aarch64_static_method_bodies(module, emitter, slots, done_label, fail_label); + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed static method result to Rust +} + +/// Emits the x86_64 static method-call helper body. +fn emit_static_method_call_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], +) { + let fail_label = "__elephc_eval_value_static_method_call_fail_x"; + let done_label = "__elephc_eval_value_static_method_call_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, method, args, and one argument + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested method-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], r8"); // save the boxed eval argument array + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save the requested method-name length + emit_x86_64_static_method_dispatch(module, emitter, data, slots); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported public static method matched the request + emit_x86_64_static_method_bodies(module, emitter, slots, done_label, fail_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed static method result to Rust +} + /// Emits the ARM64 method-call helper body. fn emit_method_call_aarch64( module: &Module, @@ -344,6 +484,79 @@ fn emit_x86_64_method_dispatch( } } +/// Emits ARM64 class-name and method-name dispatch for static method helper bodies. +fn emit_aarch64_static_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], +) { + for (class_name, class_slots) in grouped_static_slots(slots) { + let next_label = format!( + "__elephc_eval_static_method_next_{}", + label_fragment(class_name) + ); + emit_aarch64_static_class_name_compare(emitter, data, class_name, &next_label); + for slot in class_slots { + emit_aarch64_static_method_name_compare(module, emitter, data, slot); + } + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-name and method-name dispatch for static method helper bodies. +fn emit_x86_64_static_method_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticMethodSlot], +) { + for (class_name, class_slots) in grouped_static_slots(slots) { + let next_label = format!( + "__elephc_eval_static_method_next_{}_x", + label_fragment(class_name) + ); + emit_x86_64_static_class_name_compare(emitter, data, class_name, &next_label); + for slot in class_slots { + emit_x86_64_static_method_name_compare(module, emitter, data, slot); + } + emitter.label(&next_label); + } +} + +/// Emits one ARM64 case-insensitive class-name comparison for a static method group. +fn emit_aarch64_static_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction(&format!("cbnz x0, {}", next_label)); // try the next class when names differ +} + +/// Emits one x86_64 case-insensitive class-name comparison for a static method group. +fn emit_x86_64_static_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the class names matched + emitter.instruction(&format!("jne {}", next_label)); // try the next class when names differ +} + /// Emits ARM64 class-id and method-name dispatch for compact Throwable methods. fn emit_aarch64_builtin_throwable_method_dispatch( module: &Module, @@ -456,8 +669,8 @@ fn emit_aarch64_method_name_compare( abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); emitter.instruction("bl __rt_str_eq"); // compare requested method name with this public method - emitter.instruction(&format!("cbnz x0, {}", method_body_label(module, slot))); - // dispatch to the method body when the names match + let target_label = method_body_label(module, slot); + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the method body when the names match } /// Emits one x86_64 method-name comparison and branch to the matching body. @@ -477,6 +690,41 @@ fn emit_x86_64_method_name_compare( emitter.instruction(&format!("jne {}", method_body_label(module, slot))); // dispatch to the method body when the names match } +/// Emits one ARM64 static method-name comparison and branch to the matching body. +fn emit_aarch64_static_method_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticMethodSlot, +) { + let (label, len) = data.add_string(slot.method.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested static method-name pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload requested static method-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare static method names with PHP case-insensitive rules + let target_label = static_method_body_label(module, slot); + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the static method body when the names match +} + +/// Emits one x86_64 static method-name comparison and branch to the matching body. +fn emit_x86_64_static_method_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticMethodSlot, +) { + let (label, len) = data.add_string(slot.method.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested static method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload requested static method-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare static method names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the static method names matched + let target_label = static_method_body_label(module, slot); + emitter.instruction(&format!("je {}", target_label)); // dispatch to the static method body when the names match +} + /// Emits ARM64 bodies for compact Throwable methods used by eval. fn emit_aarch64_builtin_throwable_method_bodies( module: &Module, @@ -569,7 +817,7 @@ fn emit_aarch64_method_bodies( let overflow_bytes = emit_aarch64_prepare_method_args(module, emitter, slot); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); abi::emit_release_temporary_stack(emitter, overflow_bytes); - emit_box_method_result(module, emitter, slot); + emit_box_method_result(module, emitter, &slot.return_ty); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result } } @@ -588,11 +836,55 @@ fn emit_x86_64_method_bodies( let overflow_bytes = emit_x86_64_prepare_method_args(module, emitter, slot); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); abi::emit_release_temporary_stack(emitter, overflow_bytes); - emit_box_method_result(module, emitter, slot); + emit_box_method_result(module, emitter, &slot.return_ty); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result } } +/// Emits ARM64 static method-call bodies for every bridge-supported static method. +fn emit_aarch64_static_method_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticMethodSlot], + done_label: &str, + fail_label: &str, +) { + for slot in slots { + emitter.label(&static_method_body_label(module, slot)); + emit_aarch64_validate_static_method_arg_count(module, emitter, slot, fail_label); + let overflow_bytes = emit_aarch64_prepare_static_method_args(module, emitter, slot); + abi::emit_call_label( + emitter, + &static_method_symbol(&slot.impl_class, &slot.method), + ); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_box_method_result(module, emitter, &slot.return_ty); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the native static method result + } +} + +/// Emits x86_64 static method-call bodies for every bridge-supported static method. +fn emit_x86_64_static_method_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticMethodSlot], + done_label: &str, + fail_label: &str, +) { + for slot in slots { + emitter.label(&static_method_body_label(module, slot)); + emit_x86_64_validate_static_method_arg_count(module, emitter, slot, fail_label); + let overflow_bytes = emit_x86_64_prepare_static_method_args(module, emitter, slot); + abi::emit_call_label( + emitter, + &static_method_symbol(&slot.impl_class, &slot.method), + ); + abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_box_method_result(module, emitter, &slot.return_ty); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native static method result + } +} + /// Emits ARM64 arity validation for one method body. fn emit_aarch64_validate_method_arg_count( module: &Module, @@ -623,6 +915,36 @@ fn emit_x86_64_validate_method_arg_count( emitter.instruction(&format!("jne {}", fail_label)); // reject method dispatch when arity differs } +/// Emits ARM64 arity validation for one static method body. +fn emit_aarch64_validate_static_method_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalStaticMethodSlot, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for static arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); + emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the static method signature + emitter.instruction(&format!("b.ne {}", fail_label)); // reject static method dispatch when arity differs +} + +/// Emits x86_64 arity validation for one static method body. +fn emit_x86_64_validate_static_method_arg_count( + module: &Module, + emitter: &mut Emitter, + slot: &EvalStaticMethodSlot, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for static arity validation + let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); + abi::emit_call_label(emitter, &array_len_symbol); + abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); + emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the static method signature + emitter.instruction(&format!("jne {}", fail_label)); // reject static method dispatch when arity differs +} + /// Prepares ARM64 method ABI registers for the supported argument shapes. fn emit_aarch64_prepare_method_args( module: &Module, @@ -640,6 +962,22 @@ fn emit_aarch64_prepare_method_args( materialize_method_args(module, emitter, &receiver_ty, &slot.params) } +/// Prepares ARM64 static method ABI registers for the supported argument shapes. +fn emit_aarch64_prepare_static_method_args( + module: &Module, + emitter: &mut Emitter, + slot: &EvalStaticMethodSlot, +) -> usize { + abi::emit_load_int_immediate(emitter, "x0", slot.class_id as i64); + abi::emit_push_result_value(emitter, &PhpType::Int); + for (index, param_ty) in slot.params.iter().enumerate() { + emit_aarch64_load_eval_arg(module, emitter, index); + emit_aarch64_cast_eval_arg(emitter, param_ty); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + materialize_static_method_args(module, emitter, slot) +} + /// Prepares x86_64 method ABI registers for the supported argument shapes. fn emit_x86_64_prepare_method_args( module: &Module, @@ -657,6 +995,22 @@ fn emit_x86_64_prepare_method_args( materialize_method_args(module, emitter, &receiver_ty, &slot.params) } +/// Prepares x86_64 static method ABI registers for the supported argument shapes. +fn emit_x86_64_prepare_static_method_args( + module: &Module, + emitter: &mut Emitter, + slot: &EvalStaticMethodSlot, +) -> usize { + abi::emit_load_int_immediate(emitter, "rax", slot.class_id as i64); + abi::emit_push_result_value(emitter, &PhpType::Int); + for (index, param_ty) in slot.params.iter().enumerate() { + emit_x86_64_load_eval_arg(module, emitter, index); + emit_x86_64_cast_eval_arg(emitter, param_ty); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + materialize_static_method_args(module, emitter, slot) +} + /// Materializes the pushed receiver and eval arguments into the target method ABI. fn materialize_method_args( module: &Module, @@ -671,6 +1025,19 @@ fn materialize_method_args( abi::materialize_outgoing_args(emitter, &assignments) } +/// Materializes pushed eval arguments into the target static method ABI. +fn materialize_static_method_args( + module: &Module, + emitter: &mut Emitter, + slot: &EvalStaticMethodSlot, +) -> usize { + let mut arg_types = Vec::with_capacity(slot.params.len() + 1); + arg_types.push(PhpType::Int); + arg_types.extend(slot.params.iter().map(|param| param.codegen_repr())); + let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); + abi::materialize_outgoing_args(emitter, &assignments) +} + /// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. fn emit_aarch64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); @@ -744,12 +1111,12 @@ fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { } /// Boxes the current native method result as the Mixed cell expected by eval. -fn emit_box_method_result(module: &Module, emitter: &mut Emitter, slot: &EvalMethodSlot) { - if slot.return_ty.codegen_repr() == PhpType::Void { +fn emit_box_method_result(module: &Module, emitter: &mut Emitter, return_ty: &PhpType) { + if return_ty.codegen_repr() == PhpType::Void { let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); abi::emit_call_label(emitter, &null_symbol); } else { - emit_box_current_value_as_mixed(emitter, &slot.return_ty); + emit_box_current_value_as_mixed(emitter, return_ty); } } @@ -765,6 +1132,20 @@ fn grouped_slots(slots: &[EvalMethodSlot]) -> BTreeMap grouped } +/// Groups static method slots by class name while preserving sorted class order. +fn grouped_static_slots( + slots: &[EvalStaticMethodSlot], +) -> BTreeMap<&str, Vec<&EvalStaticMethodSlot>> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped + .entry(slot.class_name.as_str()) + .or_insert_with(Vec::new) + .push(slot); + } + grouped +} + /// Returns a platform-safe body label for a method slot. fn method_body_label(module: &Module, slot: &EvalMethodSlot) -> String { let suffix = match module.target.arch { @@ -779,6 +1160,20 @@ fn method_body_label(module: &Module, slot: &EvalMethodSlot) -> String { ) } +/// Returns a platform-safe body label for a static method slot. +fn static_method_body_label(module: &Module, slot: &EvalStaticMethodSlot) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!( + "__elephc_eval_static_method_{}_{}{}", + label_fragment(&slot.class_name), + label_fragment(&slot.method), + suffix + ) +} + /// Converts arbitrary PHP metadata names into assembly-label-safe fragments. fn label_fragment(value: &str) -> String { value diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 81a3dcc6bc..3922d71aee 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4427,6 +4427,37 @@ $box->run(); assert_eq!(out, "41a:42b:43c"); } +/// Verifies eval static calls and static callables dispatch public AOT static methods. +#[test] +fn test_eval_fragment_dispatches_aot_static_methods() { + let out = compile_and_run_capture( + r#" Date: Thu, 18 Jun 2026 19:52:33 +0200 Subject: [PATCH 0346/1208] Support eval ReflectionAttribute metadata --- .../interpreter/builtins/class_metadata.rs | 26 +- .../src/interpreter/runtime_ops.rs | 7 + .../tests/builtins_class_metadata.rs | 10 +- .../interpreter/tests/support/object_ops.rs | 25 ++ .../interpreter/tests/support/runtime_ops.rs | 8 + .../elephc-eval/src/runtime_hooks/externs.rs | 5 + crates/elephc-eval/src/runtime_hooks/ops.rs | 11 + docs/php/eval.md | 9 +- src/codegen/eval_reflection_helpers.rs | 298 ++++++++++++++++++ src/codegen/mod.rs | 2 + tests/codegen/eval.rs | 13 +- 11 files changed, 403 insertions(+), 11 deletions(-) create mode 100644 src/codegen/eval_reflection_helpers.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 1c9544456b..654bb7c868 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -123,8 +123,11 @@ pub(in crate::interpreter) fn eval_class_attribute_metadata_result( eval_class_attribute_names_result(attributes, values) } ("class_get_attributes", [class_name]) => { - let _ = eval_class_metadata_name(*class_name, values)?; - values.array_new(0) + let class_name = eval_class_metadata_name(*class_name, values)?; + let Some(attributes) = eval_class_like_attributes(context, &class_name) else { + return values.array_new(0); + }; + eval_class_get_attributes_result(attributes, values) } ("class_attribute_args", [class_name, attribute_name]) => { let class_name = eval_class_metadata_name(*class_name, values)?; @@ -178,6 +181,25 @@ fn eval_class_attribute_names_result( Ok(result) } +/// Builds the indexed `ReflectionAttribute` array returned by `class_get_attributes()`. +fn eval_class_get_attributes_result( + attributes: &[EvalAttribute], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(attributes.len())?; + for (index, attribute) in attributes.iter().enumerate() { + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + let key = values.int(index as i64)?; + let args = eval_class_attribute_args_result(args, values)?; + let attribute = values.reflection_attribute_new(attribute.name(), args)?; + values.release(args)?; + result = values.array_set(result, key, attribute)?; + } + Ok(result) +} + /// Builds the indexed mixed array returned by `class_attribute_args()`. fn eval_class_attribute_args_result( args: &[EvalAttributeArg], diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index acc01cebec..4839a860c3 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -95,6 +95,13 @@ pub trait RuntimeValueOps { args: Vec, ) -> Result; + /// Materializes a synthetic `ReflectionAttribute` object through generated private-layout code. + fn reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + ) -> Result; + /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index bdeefeb972..3c4f626f60 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -91,7 +91,13 @@ echo $tag[0]; echo ":"; $missing = class_attribute_args("EvalAttrMeta", "Missing"); echo count($missing); echo ":"; $attrs = class_get_attributes("EvalAttrMeta"); -echo count($attrs); echo ":"; +echo count($attrs); echo ":"; echo $attrs[0]->getName(); echo ":"; +$attr_args = $attrs[0]->getArguments(); +echo count($attr_args); echo ":"; echo $attr_args[0]; echo ":"; echo $attr_args[1]; echo ":"; +echo $attr_args[2] ? "T" : "F"; echo ":"; echo is_null($attr_args[3]) ? "N" : "bad"; echo ":"; +$tag_attr_args = $attrs[1]->getArguments(); +echo $attrs[1]->getName(); echo ":"; echo $tag_attr_args[0]; echo ":"; +echo is_null($attrs[0]->newInstance()) ? "N" : "bad"; echo ":"; $call_names = call_user_func("class_attribute_names", "EvalAttrMeta"); echo $call_names[0]; echo ":"; $call_args = call_user_func_array( @@ -111,7 +117,7 @@ return true;"#, assert_eq!( values.output, - "3:Route:Tag:Tag:4:/home:-1:T:N:first:0:0:Route:/home:111" + "3:Route:Tag:Tag:4:/home:-1:T:N:first:0:3:Route:4:/home:-1:T:N:Tag:first:N:Route:/home:111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 43e6599846..b63dffa8f6 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -99,6 +99,14 @@ impl FakeOps { self.null() } (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), + (FakeValue::Object(properties), "getname") if args.is_empty() => { + Self::object_property(&properties, "__name").map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "getarguments") if args.is_empty() => { + Self::object_property(&properties, "__args") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(_), "newinstance") if args.is_empty() => self.null(), (FakeValue::Object(properties), "getmessage") if args.is_empty() => { Self::object_property(&properties, "message").map_or_else(|| self.string(""), Ok) } @@ -182,6 +190,23 @@ impl FakeOps { _ => Err(EvalStatus::UnsupportedConstruct), } } + /// Materializes one fake populated `ReflectionAttribute` object. + pub(super) fn runtime_reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + ) -> Result { + let name = self.string(name)?; + let factory = self.int(0)?; + let object = self.alloc(FakeValue::Object(vec![ + ("__name".to_string(), name), + ("__args".to_string(), args), + ("__factory".to_string(), factory), + ])); + self.object_classes + .insert(object.as_ptr() as usize, "ReflectionAttribute".to_string()); + Ok(object) + } /// Creates one fake object for eval `new` unit tests. pub(super) fn runtime_new_object( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 98cd28fdd5..0d2857cb2e 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -101,6 +101,14 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_static_method_call(class_name, method, args) } + /// Materializes one fake `ReflectionAttribute` object for eval metadata tests. + fn reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + ) -> Result { + self.runtime_reflection_attribute_new(name, args) + } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { self.runtime_new_object(_class_name) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 9023a71243..2627a17727 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -63,6 +63,11 @@ unsafe extern "C" { name_len: u64, args: *mut RuntimeCell, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_attribute_new( + name_ptr: *const u8, + name_len: u64, + args: *mut RuntimeCell, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, name_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 5282e73c10..8e9f087f09 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -167,6 +167,17 @@ impl RuntimeValueOps for ElephcRuntimeOps { result } + /// Materializes a populated synthetic `ReflectionAttribute` object for eval metadata. + fn reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_attribute_new(name.as_ptr(), name.len() as u64, args.as_ptr()) + }) + } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { let object = Self::handle(unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index d77edcab73..c9770d7b48 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -301,11 +301,10 @@ fallback from eval remains positional, so named method arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are ReflectionAttribute object materialization, -Reflection exposure for member-level attributes, and broader generated/AOT -method bridge signatures beyond the current public scalar zero-to-two-argument -slice. `class_get_attributes()` is recognized in eval but does not yet -materialize `ReflectionAttribute` objects for eval-declared attributes. +remaining class-system gaps are ReflectionAttribute `newInstance()` factories +for eval-declared attribute classes, Reflection exposure for member-level +attributes, and broader generated/AOT method bridge signatures beyond the +current public scalar zero-to-two-argument slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_reflection_helpers.rs b/src/codegen/eval_reflection_helpers.rs new file mode 100644 index 0000000000..cd34fbd1bc --- /dev/null +++ b/src/codegen/eval_reflection_helpers.rs @@ -0,0 +1,298 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-eval materialize selected +//! synthetic reflection objects using the current module's private layouts. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - `ReflectionAttribute` stores private implementation slots that eval cannot +//! populate through public property writes. +//! - Eval-declared attributes do not have compile-time factories, so the helper +//! writes factory id 0; `ReflectionAttribute::newInstance()` then returns null. + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::ir::{Function, LocalKind, Module}; +use crate::types::ClassInfo; + +const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; + +/// Fixed object slot layout for the synthetic `ReflectionAttribute` class. +struct ReflectionAttributeLayout { + class_id: u64, + property_count: usize, + name_lo: usize, + name_hi: usize, + args_lo: usize, + args_hi: usize, + factory_lo: usize, + factory_hi: usize, +} + +/// Emits eval reflection helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_reflection_helpers(module: &Module, emitter: &mut Emitter) { + if !module_uses_eval(module) { + return; + } + emitter.blank(); + emitter.comment("--- eval bridge: reflection helpers ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_attribute_new"); + let Some(layout) = reflection_attribute_layout(module) else { + emit_reflection_attribute_new_stub(emitter); + return; + }; + match module.target.arch { + Arch::AArch64 => emit_reflection_attribute_new_aarch64(emitter, &layout), + Arch::X86_64 => emit_reflection_attribute_new_x86_64(emitter, &layout), + } +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Returns the synthetic `ReflectionAttribute` object layout from class metadata. +fn reflection_attribute_layout(module: &Module) -> Option { + let info = module.class_infos.get("ReflectionAttribute")?; + let name_lo = reflection_property_offset(info, "__name")?; + let args_lo = reflection_property_offset(info, "__args")?; + let factory_lo = reflection_property_offset(info, "__factory")?; + Some(ReflectionAttributeLayout { + class_id: info.class_id, + property_count: info.properties.len(), + name_lo, + name_hi: name_lo + 8, + args_lo, + args_hi: args_lo + 8, + factory_lo, + factory_hi: factory_lo + 8, + }) +} + +/// Returns one declared property offset from the synthetic reflection class layout. +fn reflection_property_offset(info: &ClassInfo, property: &str) -> Option { + info.property_offsets.get(property).copied() +} + +/// Emits a fail-closed helper when reflection metadata is unavailable. +fn emit_reflection_attribute_new_stub(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("mov x0, xzr"); // report helper failure when ReflectionAttribute metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust + } + Arch::X86_64 => { + emitter.instruction("xor eax, eax"); // report helper failure when ReflectionAttribute metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust + } + } +} + +/// Emits the ARM64 `ReflectionAttribute` materializer helper body. +fn emit_reflection_attribute_new_aarch64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, +) { + let fail_label = "__elephc_eval_reflection_attribute_new_fail"; + let done_label = "__elephc_eval_reflection_attribute_new_done"; + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for inputs, object, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the attribute-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the attribute-name length + emitter.instruction("str x2, [sp, #16]"); // save the boxed eval argument array + emit_alloc_reflection_attribute_object_aarch64(emitter, layout); + emitter.instruction("str x0, [sp, #24]"); // save the unboxed ReflectionAttribute object pointer + emit_set_name_property_aarch64(emitter, layout); + emit_set_args_property_aarch64(emitter, layout, fail_label); + emit_set_factory_property_aarch64(emitter, layout); + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("ldr x1, [sp, #24]"); // move the ReflectionAttribute object pointer into the Mixed payload + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the ReflectionAttribute object for eval + emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the boxed reflection attribute to Rust +} + +/// Emits the x86_64 `ReflectionAttribute` materializer helper body. +fn emit_reflection_attribute_new_x86_64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, +) { + let fail_label = "__elephc_eval_reflection_attribute_new_fail_x"; + let done_label = "__elephc_eval_reflection_attribute_new_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve slots for inputs, object, and unboxed args + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the attribute-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the attribute-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the boxed eval argument array + emit_alloc_reflection_attribute_object_x86_64(emitter, layout); + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the unboxed ReflectionAttribute object pointer + emit_set_name_property_x86_64(emitter, layout); + emit_set_args_property_x86_64(emitter, layout, fail_label); + emit_set_factory_property_x86_64(emitter, layout); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // move the ReflectionAttribute object pointer into the Mixed payload + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("call __rt_mixed_from_value"); // box the ReflectionAttribute object for eval + emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reflection attribute to Rust +} + +/// Allocates a zero-initialized ARM64 `ReflectionAttribute` object payload. +fn emit_alloc_reflection_attribute_object_aarch64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, +) { + let payload_size = 8 + layout.property_count * 16; + emitter.instruction(&format!("mov x0, #{}", payload_size)); // request ReflectionAttribute object payload storage + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction("mov x9, #4"); // heap kind 4 marks ReflectionAttribute as an object + emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the ReflectionAttribute class id + emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + for index in 0..layout.property_count { + let offset = 8 + index * 16; + abi::emit_store_zero_to_address(emitter, "x0", offset); + abi::emit_store_zero_to_address(emitter, "x0", offset + 8); + } +} + +/// Allocates a zero-initialized x86_64 `ReflectionAttribute` object payload. +fn emit_alloc_reflection_attribute_object_x86_64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, +) { + let payload_size = 8 + layout.property_count * 16; + emitter.instruction(&format!("mov rax, {}", payload_size)); // request ReflectionAttribute object payload storage + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the ReflectionAttribute class id + emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + for index in 0..layout.property_count { + let offset = 8 + index * 16; + abi::emit_store_zero_to_address(emitter, "rax", offset); + abi::emit_store_zero_to_address(emitter, "rax", offset + 8); + } +} + +/// Stores the incoming ARM64 attribute name into the object private slot. +fn emit_set_name_property_aarch64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("ldr x1, [sp, #0]"); // reload the attribute-name pointer for persistence + emitter.instruction("ldr x2, [sp, #8]"); // reload the attribute-name length for persistence + emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer + abi::emit_store_to_address(emitter, "x1", "x9", layout.name_lo); + abi::emit_store_to_address(emitter, "x2", "x9", layout.name_hi); +} + +/// Stores the incoming x86_64 attribute name into the object private slot. +fn emit_set_name_property_x86_64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the attribute-name pointer for persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload the attribute-name length for persistence + emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the ReflectionAttribute object pointer + abi::emit_store_to_address(emitter, "rax", "r10", layout.name_lo); + abi::emit_store_to_address(emitter, "rdx", "r10", layout.name_hi); +} + +/// Stores a retained ARM64 argument-array payload into the object private slot. +fn emit_set_args_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed eval attribute-argument array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null argument arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the argument array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array argument metadata + emitter.instruction("str x1, [sp, #32]"); // save the unboxed argument array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the argument array for ReflectionAttribute ownership + emitter.instruction("ldr x1, [sp, #32]"); // reload the retained argument array payload + emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer + abi::emit_store_to_address(emitter, "x1", "x9", layout.args_lo); + abi::emit_load_int_immediate(emitter, "x10", 4); + abi::emit_store_to_address(emitter, "x10", "x9", layout.args_hi); +} + +/// Stores a retained x86_64 argument-array payload into the object private slot. +fn emit_set_args_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionAttributeLayout, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the boxed eval attribute-argument array + emitter.instruction("test rax, rax"); // check whether the boxed argument array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null argument arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the argument array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array argument metadata + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the unboxed argument array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the argument array for ReflectionAttribute ownership + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the retained argument array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the ReflectionAttribute object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", layout.args_lo); + abi::emit_load_int_immediate(emitter, "r11", 4); + abi::emit_store_to_address(emitter, "r11", "r10", layout.args_hi); +} + +/// Stores factory id 0 on the ARM64 reflection object. +fn emit_set_factory_property_aarch64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer + abi::emit_store_zero_to_address(emitter, "x9", layout.factory_lo); + abi::emit_store_zero_to_address(emitter, "x9", layout.factory_hi); +} + +/// Stores factory id 0 on the x86_64 reflection object. +fn emit_set_factory_property_x86_64(emitter: &mut Emitter, layout: &ReflectionAttributeLayout) { + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the ReflectionAttribute object pointer + abi::emit_store_zero_to_address(emitter, "r10", layout.factory_lo); + abi::emit_store_zero_to_address(emitter, "r10", layout.factory_hi); +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index d74ebc8845..afde7a84d6 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -14,6 +14,7 @@ pub(crate) mod context; mod eval_constructor_helpers; mod eval_method_helpers; mod eval_property_helpers; +mod eval_reflection_helpers; mod fibers; mod frame; mod function_variants; @@ -197,6 +198,7 @@ fn finalize_user_asm( eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); eval_constructor_helpers::emit_eval_constructor_helpers(module, &mut emitter); eval_method_helpers::emit_eval_method_helpers(module, &mut emitter, &mut data); + eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); let data_output = data.emit(); let empty_globals = HashSet::::new(); let empty_static_vars = HashMap::<(String, String), PhpType>::new(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3922d71aee..5fcb71d72f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5296,7 +5296,13 @@ echo ($args[2] ? "T" : "F") . ":" . (is_null($args[3]) ? "N" : "bad") . ":"; $tag = class_attribute_args("evalattrmeta", "Tag"); echo $tag[0] . ":"; $attrs = class_get_attributes("EvalAttrMeta"); -echo count($attrs);'); +echo count($attrs) . ":" . $attrs[0]->getName() . ":"; +$attrArgs = $attrs[0]->getArguments(); +echo count($attrArgs) . ":" . $attrArgs[0] . ":" . $attrArgs[1] . ":"; +echo ($attrArgs[2] ? "T" : "F") . ":" . (is_null($attrArgs[3]) ? "N" : "bad") . ":"; +$tagArgs = $attrs[1]->getArguments(); +echo $attrs[1]->getName() . ":" . $tagArgs[0] . ":"; +echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); "#, ); assert!( @@ -5304,7 +5310,10 @@ echo count($attrs);'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "3:Route:Tag:Tag:4:/home:-1:T:N:first:0"); + assert_eq!( + out.stdout, + "3:Route:Tag:Tag:4:/home:-1:T:N:first:3:Route:4:/home:-1:T:N:Tag:first:N" + ); } /// Verifies eval interface and trait constants work through the bridge. From bd21273d2ec7dacf68541818aaea7c8aadc7a4df Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 19:58:55 +0200 Subject: [PATCH 0347/1208] Instantiate eval reflection attributes --- crates/elephc-eval/src/context.rs | 17 ++++++- .../interpreter/builtins/class_metadata.rs | 12 +++-- .../elephc-eval/src/interpreter/statements.rs | 48 +++++++++++++++++++ .../tests/builtins_class_metadata.rs | 34 +++++++++++++ docs/php/eval.md | 15 +++--- tests/codegen/eval.rs | 33 +++++++++++++ 6 files changed, 146 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 689ef94b9b..67468d38f0 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -16,8 +16,8 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::{ - EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, EvalFunction, - EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalTrait, + EvalAttribute, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, + EvalFunction, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalTrait, }; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; @@ -109,6 +109,7 @@ pub struct ElephcEvalContext { class_constants: HashMap<(String, String), RuntimeCellHandle>, included_files: HashSet, dynamic_objects: HashMap, + eval_reflection_attributes: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, class_stack: Vec, @@ -148,6 +149,7 @@ impl ElephcEvalContext { class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), + eval_reflection_attributes: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -188,6 +190,7 @@ impl ElephcEvalContext { class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), + eval_reflection_attributes: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -471,6 +474,16 @@ impl ElephcEvalContext { .and_then(|class_key| self.classes.get(class_key)) } + /// Records eval-declared attribute metadata for one synthetic ReflectionAttribute object. + pub fn register_eval_reflection_attribute(&mut self, identity: u64, attribute: EvalAttribute) { + self.eval_reflection_attributes.insert(identity, attribute); + } + + /// Returns eval-declared attribute metadata attached to a synthetic ReflectionAttribute. + pub fn eval_reflection_attribute(&self, identity: u64) -> Option<&EvalAttribute> { + self.eval_reflection_attributes.get(&identity) + } + /// Returns eval-declared class metadata from parent to child for construction. pub fn class_chain(&self, name: &str) -> Vec { let mut chain = Vec::new(); diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 654bb7c868..3183c59f54 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -111,7 +111,7 @@ pub(in crate::interpreter) fn eval_builtin_class_attribute_metadata( pub(in crate::interpreter) fn eval_class_attribute_metadata_result( name: &str, evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match (name, evaluated_args) { @@ -127,7 +127,8 @@ pub(in crate::interpreter) fn eval_class_attribute_metadata_result( let Some(attributes) = eval_class_like_attributes(context, &class_name) else { return values.array_new(0); }; - eval_class_get_attributes_result(attributes, values) + let attributes = attributes.to_vec(); + eval_class_get_attributes_result(&attributes, context, values) } ("class_attribute_args", [class_name, attribute_name]) => { let class_name = eval_class_metadata_name(*class_name, values)?; @@ -184,6 +185,7 @@ fn eval_class_attribute_names_result( /// Builds the indexed `ReflectionAttribute` array returned by `class_get_attributes()`. fn eval_class_get_attributes_result( attributes: &[EvalAttribute], + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let mut result = values.array_new(attributes.len())?; @@ -193,9 +195,11 @@ fn eval_class_get_attributes_result( }; let key = values.int(index as i64)?; let args = eval_class_attribute_args_result(args, values)?; - let attribute = values.reflection_attribute_new(attribute.name(), args)?; + let reflection_attribute = values.reflection_attribute_new(attribute.name(), args)?; + let identity = values.object_identity(reflection_attribute)?; + context.register_eval_reflection_attribute(identity, attribute.clone()); values.release(args)?; - result = values.array_set(result, key, attribute)?; + result = values.array_set(result, key, reflection_attribute)?; } Ok(result) } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index ba0c83f3f0..0f900a8de2 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1981,6 +1981,14 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; return values.method_call(object, method_name, evaluated_args); }; + if let Some(attribute) = context.eval_reflection_attribute(identity).cloned() { + if method_name.eq_ignore_ascii_case("newInstance") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return eval_reflection_attribute_new_instance_result(&attribute, context, values); + } + } let Some(class) = context.dynamic_object_class(identity) else { let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; return values.method_call(object, method_name, evaluated_args); @@ -2004,6 +2012,46 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( ) } +/// Instantiates an eval-declared attribute class for `ReflectionAttribute::newInstance()`. +fn eval_reflection_attribute_new_instance_result( + attribute: &EvalAttribute, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class) = context.class(attribute.name()).cloned() else { + return values.null(); + }; + let args = eval_reflection_attribute_arg_values(attribute, values)?; + let mut scope = ElephcEvalScope::new(); + eval_dynamic_class_new_object(&class, positional_args(args), context, &mut scope, values) +} + +/// Materializes eval attribute literal arguments as constructor argument cells. +fn eval_reflection_attribute_arg_values( + attribute: &EvalAttribute, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + args.iter() + .map(|arg| eval_reflection_attribute_arg_value(arg, values)) + .collect() +} + +/// Materializes one eval attribute literal as a constructor argument cell. +fn eval_reflection_attribute_arg_value( + arg: &EvalAttributeArg, + values: &mut impl RuntimeValueOps, +) -> Result { + match arg { + EvalAttributeArg::String(value) => values.string(value), + EvalAttributeArg::Int(value) => values.int(*value), + EvalAttributeArg::Bool(value) => values.bool_value(*value), + EvalAttributeArg::Null => values.null(), + } +} + /// Resolves the method metadata visible from the current class scope. fn eval_dynamic_method_for_call( object_class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 3c4f626f60..73a5718847 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -122,6 +122,40 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionAttribute::newInstance instantiates eval-declared attribute classes. +#[test] +fn execute_program_instantiates_eval_declared_reflection_attribute() { + let program = parse_fragment( + br#"class EvalRoute { + public $path; + public $code; + public $enabled; + public function __construct($path, $code, $enabled) { + $this->path = $path; + $this->code = $code; + $this->enabled = $enabled; + } + public function summary() { + return $this->path . ":" . $this->code . ":" . ($this->enabled ? "T" : "F"); + } +} +#[EvalRoute("/home", -7, true)] +class EvalRouteTarget {} +$attrs = class_get_attributes("EvalRouteTarget"); +$instance = $attrs[0]->newInstance(); +echo get_class($instance); echo ":"; echo $instance->summary(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalRoute:/home:-7:T"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index c9770d7b48..1964cf0a09 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -140,9 +140,11 @@ properties, static methods, class constants, interface constants, trait constants, class-level attributes, and `ClassName::class` literals. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, -interfaces, traits, and enums are visible through `class_attribute_names()` and -`class_attribute_args()` when their arguments are supported literal positional -values (`string`, `int`, `bool`, `null`, or negated integer literals). +interfaces, traits, and enums are visible through `class_attribute_names()`, +`class_attribute_args()`, and `class_get_attributes()` when their arguments are +supported literal positional values (`string`, `int`, `bool`, `null`, or negated +integer literals). `ReflectionAttribute::newInstance()` instantiates +eval-declared attribute classes from those materialized class-level attributes. Attribute names remain visible when an attribute uses unsupported argument syntax, but requesting those arguments is a runtime fatal. Attributes on methods, properties, constants, interface members, trait members, and enum cases @@ -301,10 +303,9 @@ fallback from eval remains positional, so named method arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are ReflectionAttribute `newInstance()` factories -for eval-declared attribute classes, Reflection exposure for member-level -attributes, and broader generated/AOT method bridge signatures beyond the -current public scalar zero-to-two-argument slice. +remaining class-system gaps are Reflection exposure for member-level attributes +and broader generated/AOT method bridge signatures beyond the current public +scalar zero-to-two-argument slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5fcb71d72f..75998a0905 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5316,6 +5316,39 @@ echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); ); } +/// Verifies eval ReflectionAttribute::newInstance builds eval-declared attribute objects. +#[test] +fn test_eval_reflection_attribute_new_instance_for_eval_class() { + let out = compile_and_run_capture( + r#"path = $path; + $this->code = $code; + $this->enabled = $enabled; + } + public function summary() { + return $this->path . ":" . $this->code . ":" . ($this->enabled ? "T" : "F"); + } +} +#[EvalRoute("/home", -7, true)] +class EvalRouteTarget {} +$attrs = class_get_attributes("EvalRouteTarget"); +$instance = $attrs[0]->newInstance(); +echo get_class($instance) . ":" . $instance->summary();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalRoute:/home:-7:T"); +} + /// Verifies eval interface and trait constants work through the bridge. #[test] fn test_eval_declared_interface_and_trait_constants() { From 885c8e83126502cfb1832e69ea134b0454a02b3c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 20:10:28 +0200 Subject: [PATCH 0348/1208] Expose eval member attributes through reflection --- .../interpreter/builtins/class_metadata.rs | 6 +- .../src/interpreter/expressions.rs | 5 + crates/elephc-eval/src/interpreter/mod.rs | 2 + .../elephc-eval/src/interpreter/reflection.rs | 242 ++++++++++++ .../src/interpreter/runtime_ops.rs | 12 + .../tests/builtins_class_metadata.rs | 44 +++ .../interpreter/tests/support/object_ops.rs | 26 ++ .../interpreter/tests/support/runtime_ops.rs | 9 + .../elephc-eval/src/runtime_hooks/externs.rs | 6 + crates/elephc-eval/src/runtime_hooks/ops.rs | 17 + docs/php/eval.md | 20 +- src/codegen/eval_reflection_owner_helpers.rs | 363 ++++++++++++++++++ src/codegen/mod.rs | 2 + tests/codegen/eval.rs | 43 +++ 14 files changed, 786 insertions(+), 11 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/reflection.rs create mode 100644 src/codegen/eval_reflection_owner_helpers.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 3183c59f54..903832a03c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -128,7 +128,7 @@ pub(in crate::interpreter) fn eval_class_attribute_metadata_result( return values.array_new(0); }; let attributes = attributes.to_vec(); - eval_class_get_attributes_result(&attributes, context, values) + eval_reflection_attribute_array_result(&attributes, context, values) } ("class_attribute_args", [class_name, attribute_name]) => { let class_name = eval_class_metadata_name(*class_name, values)?; @@ -182,8 +182,8 @@ fn eval_class_attribute_names_result( Ok(result) } -/// Builds the indexed `ReflectionAttribute` array returned by `class_get_attributes()`. -fn eval_class_get_attributes_result( +/// Builds an indexed `ReflectionAttribute` array from eval-retained attribute metadata. +pub(in crate::interpreter) fn eval_reflection_attribute_array_result( attributes: &[EvalAttribute], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 9d0363bada..904044a818 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -65,6 +65,11 @@ pub(in crate::interpreter) fn eval_expr( } => eval_namespaced_const_fetch(name, fallback_name, context, values), EvalExpr::NewObject { class_name, args } => { let args = eval_method_call_arg_values(args, context, scope, values)?; + if let Some(object) = + eval_reflection_owner_new_object(class_name, args.clone(), context, values)? + { + return Ok(object); + } if let Some(class) = context.class(class_name).cloned() { eval_dynamic_class_new_object(&class, args, context, scope, values) } else { diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index ba765e1a52..31f402fbb8 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -23,6 +23,7 @@ mod expressions; mod include_exec; mod json; mod libc_shims; +mod reflection; mod runtime_ops; mod scope_cells; mod statements; @@ -56,6 +57,7 @@ use expressions::*; use include_exec::*; use json::*; use libc_shims::*; +use reflection::*; use regex::bytes::{Captures, Regex, RegexBuilder}; pub use runtime_ops::RuntimeValueOps; use runtime_ops::*; diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs new file mode 100644 index 0000000000..a208184511 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -0,0 +1,242 @@ +//! Purpose: +//! Handles eval-aware construction of builtin reflection owner objects. +//! These objects need private metadata slots populated from eval-declared class +//! metadata, which ordinary public property writes cannot express. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_expr()` for `new Reflection*`. +//! +//! Key details: +//! - Only eval-declared classes/interfaces/traits/enums are handled here. +//! - Non-eval targets fall back to the generated AOT runtime bridge. + +use super::*; + +/// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. +pub(in crate::interpreter) fn eval_reflection_owner_new_object( + class_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match reflection_owner_kind(class_name) { + Some(EVAL_REFLECTION_OWNER_CLASS) => { + eval_reflection_class_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_METHOD) => { + eval_reflection_method_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_PROPERTY) => { + eval_reflection_property_new(evaluated_args, context, values) + } + Some(_) => Err(EvalStatus::RuntimeFatal), + None => Ok(None), + } +} + +/// Builds an eval-backed `ReflectionClass` object when the reflected class-like exists in eval. +fn eval_reflection_class_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; + let class_name = eval_reflection_string_arg(args[0], values)?; + let Some((resolved_name, attributes)) = + eval_reflection_class_like_attributes(&class_name, context) + else { + return Ok(None); + }; + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS, + &resolved_name, + &attributes, + context, + values, + ) + .map(Some) +} + +/// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. +fn eval_reflection_method_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("method_name")], + evaluated_args, + )?; + let class_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_class_like_exists(&class_name, context) { + return Ok(None); + } + let method_name = eval_reflection_string_arg(args[1], values)?; + let attributes = eval_reflection_method_attributes(&class_name, &method_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_METHOD, + "", + &attributes, + context, + values, + ) + .map(Some) +} + +/// Builds an eval-backed `ReflectionProperty` object when the reflected property exists in eval. +fn eval_reflection_property_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("property_name")], + evaluated_args, + )?; + let class_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_class_like_exists(&class_name, context) { + return Ok(None); + } + let property_name = eval_reflection_string_arg(args[1], values)?; + let attributes = eval_reflection_property_attributes(&class_name, &property_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_PROPERTY, + "", + &attributes, + context, + values, + ) + .map(Some) +} + +/// Materializes one Reflection owner object and transfers the temporary attribute array. +fn eval_reflection_owner_object( + owner_kind: u64, + reflected_name: &str, + attributes: &[EvalAttribute], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = eval_reflection_attribute_array_result(attributes, context, values)?; + let object = values.reflection_owner_new(owner_kind, reflected_name, attrs)?; + values.release(attrs)?; + Ok(object) +} + +/// Returns the eval-retained class-like attributes plus canonical reflected name. +fn eval_reflection_class_like_attributes( + name: &str, + context: &ElephcEvalContext, +) -> Option<(String, Vec)> { + if let Some(class) = context.class(name) { + return Some(( + class.name().trim_start_matches('\\').to_string(), + class.attributes().to_vec(), + )); + } + if let Some(interface) = context.interface(name) { + return Some(( + interface.name().trim_start_matches('\\').to_string(), + interface.attributes().to_vec(), + )); + } + if let Some(trait_decl) = context.trait_decl(name) { + return Some(( + trait_decl.name().trim_start_matches('\\').to_string(), + trait_decl.attributes().to_vec(), + )); + } + context.enum_decl(name).map(|enum_decl| { + ( + enum_decl.name().trim_start_matches('\\').to_string(), + enum_decl.attributes().to_vec(), + ) + }) +} + +/// Returns true when a name resolves to an eval-declared class-like symbol. +fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> bool { + context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || context.has_enum(name) +} + +/// Returns attributes attached to a method-like member on an eval class-like symbol. +fn eval_reflection_method_attributes( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option> { + if context.has_class(class_name) || context.has_enum(class_name) { + return context + .class_method(class_name, method_name) + .map(|(_, method)| method.attributes().to_vec()); + } + if context.has_interface(class_name) { + return context + .interface_method_requirements(class_name) + .into_iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| method.attributes().to_vec()); + } + context.trait_decl(class_name).and_then(|trait_decl| { + trait_decl + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| method.attributes().to_vec()) + }) +} + +/// Returns attributes attached to a property-like member on an eval class-like symbol. +fn eval_reflection_property_attributes( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option> { + if context.has_class(class_name) || context.has_enum(class_name) { + return context + .class_property(class_name, property_name) + .map(|(_, property)| property.attributes().to_vec()); + } + if context.has_interface(class_name) { + return context + .interface_property_requirements(class_name) + .into_iter() + .find(|property| property.name() == property_name) + .map(|property| property.attributes().to_vec()); + } + context.trait_decl(class_name).and_then(|trait_decl| { + trait_decl + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| property.attributes().to_vec()) + }) +} + +/// Converts one reflection constructor argument to a Rust UTF-8 string. +fn eval_reflection_string_arg( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Maps a PHP reflection owner class name to the helper owner kind. +fn reflection_owner_kind(class_name: &str) -> Option { + match class_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str() + { + "reflectionclass" => Some(EVAL_REFLECTION_OWNER_CLASS), + "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), + "reflectionproperty" => Some(EVAL_REFLECTION_OWNER_PROPERTY), + _ => None, + } +} diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 4839a860c3..da14de58b3 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -102,6 +102,14 @@ pub trait RuntimeValueOps { args: RuntimeCellHandle, ) -> Result; + /// Materializes a synthetic ReflectionClass/Method/Property object through generated private-layout code. + fn reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + ) -> Result; + /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; @@ -331,3 +339,7 @@ pub(super) const EVAL_TAG_ASSOC: u64 = 5; pub(super) const EVAL_TAG_OBJECT: u64 = 6; pub(super) const EVAL_TAG_NULL: u64 = 8; pub(super) const EVAL_TAG_RESOURCE: u64 = 9; + +pub(super) const EVAL_REFLECTION_OWNER_CLASS: u64 = 0; +pub(super) const EVAL_REFLECTION_OWNER_METHOD: u64 = 1; +pub(super) const EVAL_REFLECTION_OWNER_PROPERTY: u64 = 2; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 73a5718847..c7e54eee78 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -156,6 +156,50 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass/Method/Property expose eval-declared attribute metadata. +#[test] +fn execute_program_reflects_eval_member_attributes() { + let program = parse_fragment( + br#"class EvalMarker { + public $name; + public function __construct($name) { + $this->name = $name; + } + public function label() { + return $this->name; + } +} +#[EvalMarker("class")] +class EvalReflectTarget { + #[EvalMarker("method")] + public function handle() {} + #[EvalMarker("property")] + public $id; +} +$class_attrs = (new ReflectionClass("EvalReflectTarget"))->getAttributes(); +echo count($class_attrs); echo ":"; echo (new ReflectionClass("EvalReflectTarget"))->getName(); echo ":"; +echo $class_attrs[0]->getName(); echo ":"; echo $class_attrs[0]->newInstance()->label(); echo ":"; +$method_attrs = (new ReflectionMethod("EvalReflectTarget", "handle"))->getAttributes(); +echo count($method_attrs); echo ":"; echo $method_attrs[0]->getName(); echo ":"; +echo $method_attrs[0]->getArguments()[0]; echo ":"; echo $method_attrs[0]->newInstance()->label(); echo ":"; +$property_attrs = (new ReflectionProperty("EvalReflectTarget", "id"))->getAttributes(); +echo count($property_attrs); echo ":"; echo $property_attrs[0]->getName(); echo ":"; +echo $property_attrs[0]->getArguments()[0]; echo ":"; echo $property_attrs[0]->newInstance()->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:EvalReflectTarget:EvalMarker:class:1:EvalMarker:method:method:1:EvalMarker:property:property" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index b63dffa8f6..dc6d5229df 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -107,6 +107,10 @@ impl FakeOps { .map_or_else(|| self.runtime_array_new(0), Ok) } (FakeValue::Object(_), "newinstance") if args.is_empty() => self.null(), + (FakeValue::Object(properties), "getattributes") if args.is_empty() => { + Self::object_property(&properties, "__attrs") + .map_or_else(|| self.runtime_array_new(0), Ok) + } (FakeValue::Object(properties), "getmessage") if args.is_empty() => { Self::object_property(&properties, "message").map_or_else(|| self.string(""), Ok) } @@ -207,6 +211,28 @@ impl FakeOps { .insert(object.as_ptr() as usize, "ReflectionAttribute".to_string()); Ok(object) } + /// Materializes one fake populated Reflection owner object. + pub(super) fn runtime_reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + ) -> Result { + let class_name = match owner_kind { + EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", + EVAL_REFLECTION_OWNER_METHOD => "ReflectionMethod", + EVAL_REFLECTION_OWNER_PROPERTY => "ReflectionProperty", + _ => return Err(EvalStatus::RuntimeFatal), + }; + let name = self.string(reflected_name)?; + let object = self.alloc(FakeValue::Object(vec![ + ("__name".to_string(), name), + ("__attrs".to_string(), attrs), + ])); + self.object_classes + .insert(object.as_ptr() as usize, class_name.to_string()); + Ok(object) + } /// Creates one fake object for eval `new` unit tests. pub(super) fn runtime_new_object( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 0d2857cb2e..69469f41a5 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -109,6 +109,15 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_reflection_attribute_new(name, args) } + /// Materializes one fake Reflection owner object for eval metadata tests. + fn reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + ) -> Result { + self.runtime_reflection_owner_new(owner_kind, reflected_name, attrs) + } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { self.runtime_new_object(_class_name) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 2627a17727..6e6ba07f45 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -68,6 +68,12 @@ unsafe extern "C" { name_len: u64, args: *mut RuntimeCell, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_owner_new( + owner_kind: u64, + name_ptr: *const u8, + name_len: u64, + attrs: *mut RuntimeCell, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, name_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 8e9f087f09..8bf4dd4f00 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -178,6 +178,23 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Materializes a populated synthetic Reflection owner object for eval metadata. + fn reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_owner_new( + owner_kind, + reflected_name.as_ptr(), + reflected_name.len() as u64, + attrs.as_ptr(), + ) + }) + } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { let object = Self::handle(unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index 1964cf0a09..865010ca72 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -144,12 +144,15 @@ interfaces, traits, and enums are visible through `class_attribute_names()`, `class_attribute_args()`, and `class_get_attributes()` when their arguments are supported literal positional values (`string`, `int`, `bool`, `null`, or negated integer literals). `ReflectionAttribute::newInstance()` instantiates -eval-declared attribute classes from those materialized class-level attributes. +eval-declared attribute classes from those materialized attributes. Attribute names remain visible when an attribute uses unsupported argument -syntax, but requesting those arguments is a runtime fatal. Attributes on -methods, properties, constants, interface members, trait members, and enum cases -are parsed and retained for future reflection support, but eval does not expose -them through Reflection APIs yet. +syntax, but requesting those arguments is a runtime fatal. +`ReflectionClass::getAttributes()`, `ReflectionMethod::getAttributes()`, and +`ReflectionProperty::getAttributes()` expose eval-retained class, method, and +property attributes for eval-declared class-like symbols when their arguments +fit the same literal subset. Attributes on constants and enum cases are parsed +and retained for future reflection support, but eval does not expose them +through Reflection APIs yet. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the @@ -303,9 +306,10 @@ fallback from eval remains positional, so named method arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are Reflection exposure for member-level attributes -and broader generated/AOT method bridge signatures beyond the current public -scalar zero-to-two-argument slice. +remaining class-system gaps are Reflection exposure for constant and enum-case +attributes, broader reflection APIs beyond the supported attribute/getName +slice, and broader generated/AOT method bridge signatures beyond the current +public scalar zero-to-two-argument slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs new file mode 100644 index 0000000000..06c4f7d3ff --- /dev/null +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -0,0 +1,363 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-eval materialize +//! ReflectionClass, ReflectionMethod, and ReflectionProperty objects with +//! private metadata slots populated from runtime eval declarations. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - Reflection owner objects store `__attrs`, and ReflectionClass also stores +//! `__name`; both slots are private implementation details. +//! - The helper retains the supplied attribute array payload for object ownership. + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::ir::{Function, LocalKind, Module}; +use crate::types::ClassInfo; + +const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; + +/// Fixed object layout for one synthetic Reflection owner class. +struct ReflectionOwnerLayout { + class_id: u64, + property_count: usize, + name_lo: Option, + name_hi: Option, + attrs_lo: usize, + attrs_hi: usize, +} + +/// Layouts for the three Reflection owner classes eval can materialize. +struct ReflectionOwnerLayouts { + class: ReflectionOwnerLayout, + method: ReflectionOwnerLayout, + property: ReflectionOwnerLayout, +} + +/// Emits eval Reflection owner helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_reflection_owner_helpers(module: &Module, emitter: &mut Emitter) { + if !module_uses_eval(module) { + return; + } + emitter.blank(); + emitter.comment("--- eval bridge: reflection owner helpers ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_owner_new"); + let Some(layouts) = reflection_owner_layouts(module) else { + emit_reflection_owner_new_stub(emitter); + return; + }; + match module.target.arch { + Arch::AArch64 => emit_reflection_owner_new_aarch64(emitter, &layouts), + Arch::X86_64 => emit_reflection_owner_new_x86_64(emitter, &layouts), + } +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Returns the Reflection owner object layouts from class metadata. +fn reflection_owner_layouts(module: &Module) -> Option { + Some(ReflectionOwnerLayouts { + class: reflection_owner_layout(module.class_infos.get("ReflectionClass")?, true)?, + method: reflection_owner_layout(module.class_infos.get("ReflectionMethod")?, false)?, + property: reflection_owner_layout(module.class_infos.get("ReflectionProperty")?, false)?, + }) +} + +/// Returns one Reflection owner layout from class metadata. +fn reflection_owner_layout( + info: &ClassInfo, + has_name: bool, +) -> Option { + let attrs_lo = reflection_property_offset(info, "__attrs")?; + let name_lo = has_name.then(|| reflection_property_offset(info, "__name")).flatten(); + Some(ReflectionOwnerLayout { + class_id: info.class_id, + property_count: info.properties.len(), + name_lo, + name_hi: name_lo.map(|offset| offset + 8), + attrs_lo, + attrs_hi: attrs_lo + 8, + }) +} + +/// Returns one declared property offset from the synthetic reflection class layout. +fn reflection_property_offset(info: &ClassInfo, property: &str) -> Option { + info.property_offsets.get(property).copied() +} + +/// Emits a fail-closed helper when Reflection owner metadata is unavailable. +fn emit_reflection_owner_new_stub(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("mov x0, xzr"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust + } + Arch::X86_64 => { + emitter.instruction("xor eax, eax"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust + } + } +} + +/// Emits the ARM64 Reflection owner materializer helper body. +fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &ReflectionOwnerLayouts) { + let fail_label = "__elephc_eval_reflection_owner_new_fail"; + let done_label = "__elephc_eval_reflection_owner_new_done"; + let box_label = "__elephc_eval_reflection_owner_new_box"; + let class_label = "__elephc_eval_reflection_owner_new_class"; + let method_label = "__elephc_eval_reflection_owner_new_method"; + let property_label = "__elephc_eval_reflection_owner_new_property"; + emitter.instruction("sub sp, sp, #96"); // reserve helper frame for inputs, object, scratch, and fp/lr + emitter.instruction("stp x29, x30, [sp, #80]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #80"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the Reflection owner kind + emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer + emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array + emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds + emit_aarch64_owner_kind_body(emitter, class_label, &layouts.class, true, fail_label, box_label); + emit_aarch64_owner_kind_body(emitter, method_label, &layouts.method, false, fail_label, box_label); + emit_aarch64_owner_kind_body(emitter, property_label, &layouts.property, false, fail_label, box_label); + emitter.label(box_label); + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #96"); // release the helper frame + emitter.instruction("ret"); // return the boxed reflection owner to Rust +} + +/// Emits the x86_64 Reflection owner materializer helper body. +fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionOwnerLayouts) { + let fail_label = "__elephc_eval_reflection_owner_new_fail_x"; + let done_label = "__elephc_eval_reflection_owner_new_done_x"; + let box_label = "__elephc_eval_reflection_owner_new_box_x"; + let class_label = "__elephc_eval_reflection_owner_new_class_x"; + let method_label = "__elephc_eval_reflection_owner_new_method_x"; + let property_label = "__elephc_eval_reflection_owner_new_property_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 64"); // reserve slots for inputs, object, and unboxed attrs + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the Reflection owner kind + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array + emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds + emit_x86_64_owner_kind_body(emitter, class_label, &layouts.class, true, fail_label, box_label); + emit_x86_64_owner_kind_body(emitter, method_label, &layouts.method, false, fail_label, box_label); + emit_x86_64_owner_kind_body(emitter, property_label, &layouts.property, false, fail_label, box_label); + emitter.label(box_label); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("call __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reflection owner to Rust +} + +/// Emits one ARM64 owner-kind allocation and slot-population body. +fn emit_aarch64_owner_kind_body( + emitter: &mut Emitter, + label: &str, + layout: &ReflectionOwnerLayout, + set_name: bool, + fail_label: &str, + box_label: &str, +) { + emitter.label(label); + emit_alloc_reflection_owner_object_aarch64(emitter, layout); + emitter.instruction("str x0, [sp, #32]"); // save the unboxed Reflection owner object pointer + if set_name { + emit_set_owner_name_property_aarch64(emitter, layout); + } + emit_set_owner_attrs_property_aarch64(emitter, layout, fail_label); + emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object +} + +/// Emits one x86_64 owner-kind allocation and slot-population body. +fn emit_x86_64_owner_kind_body( + emitter: &mut Emitter, + label: &str, + layout: &ReflectionOwnerLayout, + set_name: bool, + fail_label: &str, + box_label: &str, +) { + emitter.label(label); + emit_alloc_reflection_owner_object_x86_64(emitter, layout); + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the unboxed Reflection owner object pointer + if set_name { + emit_set_owner_name_property_x86_64(emitter, layout); + } + emit_set_owner_attrs_property_x86_64(emitter, layout, fail_label); + emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object +} + +/// Allocates a zero-initialized ARM64 Reflection owner object payload. +fn emit_alloc_reflection_owner_object_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let payload_size = 8 + layout.property_count * 16; + emitter.instruction(&format!("mov x0, #{}", payload_size)); // request Reflection owner object payload storage + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction("mov x9, #4"); // heap kind 4 marks the payload as an object + emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + for index in 0..layout.property_count { + let offset = 8 + index * 16; + abi::emit_store_zero_to_address(emitter, "x0", offset); + abi::emit_store_zero_to_address(emitter, "x0", offset + 8); + } +} + +/// Allocates a zero-initialized x86_64 Reflection owner object payload. +fn emit_alloc_reflection_owner_object_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let payload_size = 8 + layout.property_count * 16; + emitter.instruction(&format!("mov rax, {}", payload_size)); // request Reflection owner object payload storage + abi::emit_call_label(emitter, "__rt_heap_alloc"); + emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + for index in 0..layout.property_count { + let offset = 8 + index * 16; + abi::emit_store_zero_to_address(emitter, "rax", offset); + abi::emit_store_zero_to_address(emitter, "rax", offset + 8); + } +} + +/// Stores the incoming ARM64 reflected class name into ReflectionClass. +fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &ReflectionOwnerLayout) { + let Some(name_lo) = layout.name_lo else { + return; + }; + let Some(name_hi) = layout.name_hi else { + return; + }; + emitter.instruction("ldr x1, [sp, #8]"); // reload the reflected-name pointer for persistence + emitter.instruction("ldr x2, [sp, #16]"); // reload the reflected-name length for persistence + emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", name_lo); + abi::emit_store_to_address(emitter, "x2", "x9", name_hi); +} + +/// Stores the incoming x86_64 reflected class name into ReflectionClass. +fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &ReflectionOwnerLayout) { + let Some(name_lo) = layout.name_lo else { + return; + }; + let Some(name_hi) = layout.name_hi else { + return; + }; + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the reflected-name pointer for persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the reflected-name length for persistence + emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rax", "r10", name_lo); + abi::emit_store_to_address(emitter, "rdx", "r10", name_hi); +} + +/// Stores a retained ARM64 attribute-array payload into the owner private slot. +fn emit_set_owner_attrs_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed ReflectionAttribute array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed attribute array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained attribute array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "x1", "x9", layout.attrs_lo); + abi::emit_load_int_immediate(emitter, "x10", 4); + abi::emit_store_to_address(emitter, "x10", "x9", layout.attrs_hi); +} + +/// Stores a retained x86_64 attribute-array payload into the owner private slot. +fn emit_set_owner_attrs_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed ReflectionAttribute array + emitter.instruction("test rax, rax"); // check whether the boxed attribute array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed attribute array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained attribute array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + abi::emit_store_to_address(emitter, "rdi", "r10", layout.attrs_lo); + abi::emit_load_int_immediate(emitter, "r11", 4); + abi::emit_store_to_address(emitter, "r11", "r10", layout.attrs_hi); +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index afde7a84d6..2452072c35 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -15,6 +15,7 @@ mod eval_constructor_helpers; mod eval_method_helpers; mod eval_property_helpers; mod eval_reflection_helpers; +mod eval_reflection_owner_helpers; mod fibers; mod frame; mod function_variants; @@ -199,6 +200,7 @@ fn finalize_user_asm( eval_constructor_helpers::emit_eval_constructor_helpers(module, &mut emitter); eval_method_helpers::emit_eval_method_helpers(module, &mut emitter, &mut data); eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); + eval_reflection_owner_helpers::emit_eval_reflection_owner_helpers(module, &mut emitter); let data_output = data.emit(); let empty_globals = HashSet::::new(); let empty_static_vars = HashMap::<(String, String), PhpType>::new(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 75998a0905..74b1a4b6ab 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5349,6 +5349,49 @@ echo get_class($instance) . ":" . $instance->summary();'); assert_eq!(out.stdout, "EvalRoute:/home:-7:T"); } +/// Verifies eval ReflectionClass/Method/Property expose eval-declared attributes. +#[test] +fn test_eval_reflection_member_attributes() { + let out = compile_and_run_capture( + r#"name = $name; + } + public function label() { + return $this->name; + } +} +#[EvalMarker("class")] +class EvalReflectTarget { + #[EvalMarker("method")] + public function handle() {} + #[EvalMarker("property")] + public $id; +} +$classAttrs = (new ReflectionClass("EvalReflectTarget"))->getAttributes(); +echo count($classAttrs) . ":" . (new ReflectionClass("EvalReflectTarget"))->getName() . ":"; +echo $classAttrs[0]->getName() . ":" . $classAttrs[0]->newInstance()->label() . ":"; +$methodAttrs = (new ReflectionMethod("EvalReflectTarget", "handle"))->getAttributes(); +echo count($methodAttrs) . ":" . $methodAttrs[0]->getName() . ":"; +echo $methodAttrs[0]->getArguments()[0] . ":" . $methodAttrs[0]->newInstance()->label() . ":"; +$propertyAttrs = (new ReflectionProperty("EvalReflectTarget", "id"))->getAttributes(); +echo count($propertyAttrs) . ":" . $propertyAttrs[0]->getName() . ":"; +echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance()->label();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalReflectTarget:EvalMarker:class:1:EvalMarker:method:method:1:EvalMarker:property:property" + ); +} + /// Verifies eval interface and trait constants work through the bridge. #[test] fn test_eval_declared_interface_and_trait_constants() { From 5a6059f3aa57d3cd738b0cdda50332f166cd1a7c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 20:27:39 +0200 Subject: [PATCH 0349/1208] Reflect eval constant and enum case attributes --- .../elephc-eval/src/interpreter/reflection.rs | 92 ++++++ .../src/interpreter/runtime_ops.rs | 3 + .../tests/builtins_class_metadata.rs | 49 +++ .../interpreter/tests/support/object_ops.rs | 3 + docs/php/eval.md | 15 +- src/autoload/mod.rs | 3 + src/codegen/eval_reflection_owner_helpers.rs | 50 ++- src/codegen/lower_inst/objects.rs | 6 + src/codegen/lower_inst/objects/reflection.rs | 121 +++++++- src/codegen/mod.rs | 3 + src/codegen_support/reflection.rs | 10 +- src/codegen_support/runtime/data/user.rs | 2 + src/ir_lower/program.rs | 3 + src/ir_lower/tests/exhaustive.rs | 2 + src/types/checker/builtin_enums.rs | 4 + src/types/checker/builtin_types/reflection.rs | 117 +++++-- .../checker/inference/objects/constructors.rs | 223 +++++--------- .../objects/constructors/reflection.rs | 286 ++++++++++++++++++ src/types/checker/schema/classes/mod.rs | 10 +- src/types/checker/schema/classes/state.rs | 27 +- src/types/checker/schema/enums.rs | 19 ++ src/types/schema.rs | 7 + tests/codegen/eval.rs | 48 +++ tests/codegen/oop/attributes.rs | 58 +++- 24 files changed, 958 insertions(+), 203 deletions(-) create mode 100644 src/types/checker/inference/objects/constructors/reflection.rs diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index a208184511..b45bb88eeb 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -29,6 +29,21 @@ pub(in crate::interpreter) fn eval_reflection_owner_new_object( Some(EVAL_REFLECTION_OWNER_PROPERTY) => { eval_reflection_property_new(evaluated_args, context, values) } + Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT) => { + eval_reflection_class_constant_new(evaluated_args, context, values) + } + Some(EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE) => eval_reflection_enum_case_new( + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE, + evaluated_args, + context, + values, + ), + Some(EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE) => eval_reflection_enum_case_new( + EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE, + evaluated_args, + context, + values, + ), Some(_) => Err(EvalStatus::RuntimeFatal), None => Ok(None), } @@ -111,6 +126,64 @@ fn eval_reflection_property_new( .map(Some) } +/// Builds an eval-backed `ReflectionClassConstant` object for a class constant or enum case. +fn eval_reflection_class_constant_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("constant_name")], + evaluated_args, + )?; + let class_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_class_like_exists(&class_name, context) { + return Ok(None); + } + let constant_name = eval_reflection_string_arg(args[1], values)?; + let attributes = + eval_reflection_class_constant_attributes(&class_name, &constant_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + &constant_name, + &attributes, + context, + values, + ) + .map(Some) +} + +/// Builds an eval-backed ReflectionEnumUnitCase/BackedCase object for an enum case. +fn eval_reflection_enum_case_new( + owner_kind: u64, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("constant_name")], + evaluated_args, + )?; + let enum_name = eval_reflection_string_arg(args[0], values)?; + let Some(enum_decl) = context.enum_decl(&enum_name) else { + return if eval_reflection_class_like_exists(&enum_name, context) { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(None) + }; + }; + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && enum_decl.backing_type().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + let case_name = eval_reflection_string_arg(args[1], values)?; + let attributes = enum_decl + .case(&case_name) + .map(|case| case.attributes().to_vec()) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_owner_object(owner_kind, &case_name, &attributes, context, values).map(Some) +} + /// Materializes one Reflection owner object and transfers the temporary attribute array. fn eval_reflection_owner_object( owner_kind: u64, @@ -156,6 +229,22 @@ fn eval_reflection_class_like_attributes( }) } +/// Returns attributes attached to an eval class constant or enum case. +fn eval_reflection_class_constant_attributes( + class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, +) -> Option> { + if let Some(enum_decl) = context.enum_decl(class_name) { + if let Some(case) = enum_decl.case(constant_name) { + return Some(case.attributes().to_vec()); + } + } + context + .class_constant(class_name, constant_name) + .map(|(_, constant)| constant.attributes().to_vec()) +} + /// Returns true when a name resolves to an eval-declared class-like symbol. fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> bool { context.has_class(name) @@ -237,6 +326,9 @@ fn reflection_owner_kind(class_name: &str) -> Option { "reflectionclass" => Some(EVAL_REFLECTION_OWNER_CLASS), "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), "reflectionproperty" => Some(EVAL_REFLECTION_OWNER_PROPERTY), + "reflectionclassconstant" => Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT), + "reflectionenumunitcase" => Some(EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE), + "reflectionenumbackedcase" => Some(EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE), _ => None, } } diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index da14de58b3..923222ebed 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -343,3 +343,6 @@ pub(super) const EVAL_TAG_RESOURCE: u64 = 9; pub(super) const EVAL_REFLECTION_OWNER_CLASS: u64 = 0; pub(super) const EVAL_REFLECTION_OWNER_METHOD: u64 = 1; pub(super) const EVAL_REFLECTION_OWNER_PROPERTY: u64 = 2; +pub(super) const EVAL_REFLECTION_OWNER_CLASS_CONSTANT: u64 = 3; +pub(super) const EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE: u64 = 4; +pub(super) const EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE: u64 = 5; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c7e54eee78..be13b4be00 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -200,6 +200,55 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. +#[test] +fn execute_program_reflects_eval_constant_and_enum_case_attributes() { + let program = parse_fragment( + br#"class EvalConstMarker { + public $name; + public function __construct($name) { + $this->name = $name; + } + public function label() { + return $this->name; + } +} +class EvalConstReflectTarget { + #[EvalConstMarker("const")] + public const ANSWER = 42; +} +enum EvalCaseReflectTarget: string { + #[EvalConstMarker("case")] + case Ready = "ready"; +} +$const_attrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); +echo count($const_attrs); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName(); echo ":"; +echo $const_attrs[0]->getName(); echo ":"; echo $const_attrs[0]->getArguments()[0]; echo ":"; +echo $const_attrs[0]->newInstance()->label(); echo ":"; +$case_attrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo count($case_attrs); echo ":"; echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo $case_attrs[0]->getName(); echo ":"; echo $case_attrs[0]->getArguments()[0]; echo ":"; +$unit_attrs = (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo $unit_attrs[0]->newInstance()->label(); echo ":"; +$backed_attrs = (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo $backed_attrs[0]->newInstance()->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:ANSWER:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:case:Ready:case" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index dc6d5229df..48f8ea0831 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -222,6 +222,9 @@ impl FakeOps { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", EVAL_REFLECTION_OWNER_METHOD => "ReflectionMethod", EVAL_REFLECTION_OWNER_PROPERTY => "ReflectionProperty", + EVAL_REFLECTION_OWNER_CLASS_CONSTANT => "ReflectionClassConstant", + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE => "ReflectionEnumUnitCase", + EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => "ReflectionEnumBackedCase", _ => return Err(EvalStatus::RuntimeFatal), }; let name = self.string(reflected_name)?; diff --git a/docs/php/eval.md b/docs/php/eval.md index 865010ca72..77c0f1df21 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -150,9 +150,11 @@ syntax, but requesting those arguments is a runtime fatal. `ReflectionClass::getAttributes()`, `ReflectionMethod::getAttributes()`, and `ReflectionProperty::getAttributes()` expose eval-retained class, method, and property attributes for eval-declared class-like symbols when their arguments -fit the same literal subset. Attributes on constants and enum cases are parsed -and retained for future reflection support, but eval does not expose them -through Reflection APIs yet. +fit the same literal subset. `ReflectionClassConstant::getAttributes()`, +`ReflectionEnumUnitCase::getAttributes()`, and +`ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant +and enum-case attributes through the same materialized `ReflectionAttribute` +shape. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the @@ -306,10 +308,9 @@ fallback from eval remains positional, so named method arguments are supported for eval-declared methods but not for every generated native method bridge. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are Reflection exposure for constant and enum-case -attributes, broader reflection APIs beyond the supported attribute/getName -slice, and broader generated/AOT method bridge signatures beyond the current -public scalar zero-to-two-argument slice. +remaining class-system gaps are broader reflection APIs beyond the supported +attribute/getName slice and broader generated/AOT method bridge signatures +beyond the current public scalar zero-to-two-argument slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index 151f3928e7..4436425fd5 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -74,6 +74,9 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "RecursiveIteratorIterator", "ReflectionAttribute", "ReflectionClass", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", "ReflectionMethod", "ReflectionProperty", "RuntimeException", diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 06c4f7d3ff..03c9cdb088 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1,14 +1,15 @@ //! Purpose: //! Emits user-assembly helpers that let libelephc-eval materialize -//! ReflectionClass, ReflectionMethod, and ReflectionProperty objects with -//! private metadata slots populated from runtime eval declarations. +//! ReflectionClass, ReflectionMethod, ReflectionProperty, ReflectionClassConstant, +//! and ReflectionEnum* objects with private metadata slots populated from +//! runtime eval declarations. //! //! Called from: //! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. //! //! Key details: -//! - Reflection owner objects store `__attrs`, and ReflectionClass also stores -//! `__name`; both slots are private implementation details. +//! - Reflection owner objects store `__attrs`; owners with public `getName()` +//! also store `__name`; both slots are private implementation details. //! - The helper retains the supplied attribute array payload for object ownership. use crate::codegen::abi; @@ -29,11 +30,14 @@ struct ReflectionOwnerLayout { attrs_hi: usize, } -/// Layouts for the three Reflection owner classes eval can materialize. +/// Layouts for the Reflection owner classes eval can materialize. struct ReflectionOwnerLayouts { class: ReflectionOwnerLayout, method: ReflectionOwnerLayout, property: ReflectionOwnerLayout, + class_constant: ReflectionOwnerLayout, + enum_unit_case: ReflectionOwnerLayout, + enum_backed_case: ReflectionOwnerLayout, } /// Emits eval Reflection owner helpers when any lowered function owns an eval context. @@ -88,6 +92,18 @@ fn reflection_owner_layouts(module: &Module) -> Option { class: reflection_owner_layout(module.class_infos.get("ReflectionClass")?, true)?, method: reflection_owner_layout(module.class_infos.get("ReflectionMethod")?, false)?, property: reflection_owner_layout(module.class_infos.get("ReflectionProperty")?, false)?, + class_constant: reflection_owner_layout( + module.class_infos.get("ReflectionClassConstant")?, + true, + )?, + enum_unit_case: reflection_owner_layout( + module.class_infos.get("ReflectionEnumUnitCase")?, + true, + )?, + enum_backed_case: reflection_owner_layout( + module.class_infos.get("ReflectionEnumBackedCase")?, + true, + )?, }) } @@ -135,6 +151,9 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection let class_label = "__elephc_eval_reflection_owner_new_class"; let method_label = "__elephc_eval_reflection_owner_new_method"; let property_label = "__elephc_eval_reflection_owner_new_property"; + let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant"; + let enum_unit_case_label = "__elephc_eval_reflection_owner_new_enum_unit_case"; + let enum_backed_case_label = "__elephc_eval_reflection_owner_new_enum_backed_case"; emitter.instruction("sub sp, sp, #96"); // reserve helper frame for inputs, object, scratch, and fp/lr emitter.instruction("stp x29, x30, [sp, #80]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #80"); // establish a stable helper frame pointer @@ -148,10 +167,19 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp x0, #3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("b.eq {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp x0, #4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("b.eq {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body(emitter, class_label, &layouts.class, true, fail_label, box_label); emit_aarch64_owner_kind_body(emitter, method_label, &layouts.method, false, fail_label, box_label); emit_aarch64_owner_kind_body(emitter, property_label, &layouts.property, false, fail_label, box_label); + emit_aarch64_owner_kind_body(emitter, class_constant_label, &layouts.class_constant, true, fail_label, box_label); + emit_aarch64_owner_kind_body(emitter, enum_unit_case_label, &layouts.enum_unit_case, true, fail_label, box_label); + emit_aarch64_owner_kind_body(emitter, enum_backed_case_label, &layouts.enum_backed_case, true, fail_label, box_label); emitter.label(box_label); emitter.instruction("mov x0, #6"); // runtime tag 6 = object emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload @@ -174,6 +202,9 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let class_label = "__elephc_eval_reflection_owner_new_class_x"; let method_label = "__elephc_eval_reflection_owner_new_method_x"; let property_label = "__elephc_eval_reflection_owner_new_property_x"; + let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant_x"; + let enum_unit_case_label = "__elephc_eval_reflection_owner_new_enum_unit_case_x"; + let enum_backed_case_label = "__elephc_eval_reflection_owner_new_enum_backed_case_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer emitter.instruction("sub rsp, 64"); // reserve slots for inputs, object, and unboxed attrs @@ -187,10 +218,19 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp rdi, 3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("je {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp rdi, 4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("je {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body(emitter, class_label, &layouts.class, true, fail_label, box_label); emit_x86_64_owner_kind_body(emitter, method_label, &layouts.method, false, fail_label, box_label); emit_x86_64_owner_kind_body(emitter, property_label, &layouts.property, false, fail_label, box_label); + emit_x86_64_owner_kind_body(emitter, class_constant_label, &layouts.class_constant, true, fail_label, box_label); + emit_x86_64_owner_kind_body(emitter, enum_unit_case_label, &layouts.enum_unit_case, true, fail_label, box_label); + emit_x86_64_owner_kind_body(emitter, enum_backed_case_label, &layouts.enum_backed_case, true, fail_label, box_label); emitter.label(box_label); emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload emitter.instruction("xor esi, esi"); // object payloads do not use a high word diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 1fc999ca5c..5a5df5c485 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1253,6 +1253,9 @@ fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { "RangeException", "RecursiveCallbackFilterIterator", "ReflectionClass", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", "ReflectionMethod", "ReflectionProperty", "RuntimeException", @@ -1316,6 +1319,9 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "RecursiveRegexIterator", "ReflectionAttribute", "ReflectionClass", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", "ReflectionMethod", "ReflectionProperty", "RegexIterator", diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 080c1ae940..1667601eb1 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -6,7 +6,8 @@ //! - `crate::codegen::lower_inst::objects::lower_object_new()`. //! //! Key details: -//! - `ReflectionClass`, `ReflectionMethod`, and `ReflectionProperty` +//! - `ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`, +//! `ReflectionClassConstant`, and `ReflectionEnum*` //! constructors are compile-time metadata lookups that populate private //! `__name`/`__attrs` slots instead of running their public empty bodies. @@ -30,7 +31,12 @@ struct ReflectionOwnerMetadata { pub(super) fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, - "ReflectionClass" | "ReflectionMethod" | "ReflectionProperty" + "ReflectionClass" + | "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" ) } @@ -450,6 +456,10 @@ fn reflection_owner_metadata( "ReflectionClass" => reflection_class_metadata(ctx, inst), "ReflectionMethod" => reflection_method_metadata(ctx, inst), "ReflectionProperty" => reflection_property_metadata(ctx, inst), + "ReflectionClassConstant" => reflection_class_constant_metadata(ctx, inst), + "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" => { + reflection_enum_case_metadata(ctx, class_name, inst) + } _ => Ok(empty_reflection_metadata()), } } @@ -521,6 +531,72 @@ fn reflection_property_metadata( .unwrap_or_else(empty_reflection_metadata)) } +/// Resolves `ReflectionClassConstant(class, constant)` metadata. +fn reflection_class_constant_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(class_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(constant_operand) = inst.operands.get(1).copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_class = + const_string_or_class_operand(ctx, class_operand, "ReflectionClassConstant")?; + let constant_name = + const_required_string_operand(ctx, constant_operand, "ReflectionClassConstant")?; + if let Some(case) = resolve_reflection_enum_case(ctx, &reflected_class, &constant_name) { + return Ok(ReflectionOwnerMetadata { + reflected_name: Some(constant_name), + attr_names: case.attribute_names.clone(), + attr_args: case.attribute_args.clone(), + }); + } + Ok(resolve_reflection_class_constant(ctx, &reflected_class, &constant_name) + .map(|(_, info)| { + let attr_names = info + .constant_attribute_names + .get(&constant_name) + .cloned() + .unwrap_or_default(); + let attr_args = info + .constant_attribute_args + .get(&constant_name) + .cloned() + .unwrap_or_default(); + ReflectionOwnerMetadata { + reflected_name: Some(constant_name), + attr_names, + attr_args, + } + }) + .unwrap_or_else(empty_reflection_metadata)) +} + +/// Resolves `ReflectionEnumUnitCase/BackedCase(enum, case)` metadata. +fn reflection_enum_case_metadata( + ctx: &FunctionContext<'_>, + class_name: &str, + inst: &Instruction, +) -> Result { + let Some(enum_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(case_operand) = inst.operands.get(1).copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_enum = const_string_or_class_operand(ctx, enum_operand, class_name)?; + let case_name = const_required_string_operand(ctx, case_operand, class_name)?; + Ok(resolve_reflection_enum_case(ctx, &reflected_enum, &case_name) + .map(|case| ReflectionOwnerMetadata { + reflected_name: Some(case_name), + attr_names: case.attribute_names.clone(), + attr_args: case.attribute_args.clone(), + }) + .unwrap_or_else(empty_reflection_metadata)) +} + /// Looks up class metadata by PHP-style case-insensitive name. fn resolve_reflection_class<'a>( ctx: &'a FunctionContext<'_>, @@ -534,6 +610,34 @@ fn resolve_reflection_class<'a>( .map(|(name, info)| (name.as_str(), info)) } +/// Looks up class-constant metadata by PHP-style class name and case-sensitive constant name. +fn resolve_reflection_class_constant<'a>( + ctx: &'a FunctionContext<'_>, + class_name: &str, + constant_name: &str, +) -> Option<(&'a str, &'a crate::types::ClassInfo)> { + let (resolved_name, info) = resolve_reflection_class(ctx, class_name)?; + if info.constants.contains_key(constant_name) { + return Some((resolved_name, info)); + } + let parent = info.parent.as_deref()?; + resolve_reflection_class_constant(ctx, parent, constant_name) +} + +/// Looks up enum-case metadata by PHP-style enum name and case-sensitive case name. +fn resolve_reflection_enum_case<'a>( + ctx: &'a FunctionContext<'_>, + enum_name: &str, + case_name: &str, +) -> Option<&'a crate::types::EnumCaseInfo> { + let enum_key = php_symbol_key(enum_name.trim_start_matches('\\')); + ctx.module + .enum_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == enum_key) + .and_then(|(_, info)| info.cases.iter().find(|case| case.name == case_name)) +} + /// Returns empty Reflection metadata for unsupported dynamic constructor operands. fn empty_reflection_metadata() -> ReflectionOwnerMetadata { ReflectionOwnerMetadata { @@ -676,9 +780,20 @@ fn emit_reflection_attrs_property( /// Returns the low/high object offsets for the private `__attrs` slot. fn reflection_attrs_offsets(class_name: &str) -> (usize, usize) { - if class_name == "ReflectionClass" { + if reflection_owner_has_name(class_name) { (24, 32) } else { (8, 16) } } + +/// Returns true when the synthetic Reflection owner stores a private `__name` slot. +fn reflection_owner_has_name(class_name: &str) -> bool { + matches!( + class_name, + "ReflectionClass" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 2452072c35..710f649e63 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -565,6 +565,9 @@ fn seed_builtin_reflection_class_names(module: &Module, names: &mut HashSet( } /// Scans every class in `classes` and collects all distinct class-level, -/// method-level, and property-level attribute name/argument pairs into a -/// sorted vector of `ReflectionAttributeFactory` records with sequential ids. +/// method-level, property-level, and constant-level attribute name/argument +/// pairs into a sorted vector of `ReflectionAttributeFactory` records with +/// sequential ids. pub(crate) fn collect_attribute_factories( classes: &HashMap, ) -> Vec { @@ -69,6 +70,11 @@ pub(crate) fn collect_attribute_factories( collect_from_attribute_lists(classes, names, args, &mut unique); } } + for (member, names) in &class_info.constant_attribute_names { + if let Some(args) = class_info.constant_attribute_args.get(member) { + collect_from_attribute_lists(classes, names, args, &mut unique); + } + } } unique diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 81c7a30694..2e403cd2f5 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -1395,6 +1395,8 @@ mod tests { method_attribute_args: HashMap::new(), property_attribute_names: HashMap::new(), property_attribute_args: HashMap::new(), + constant_attribute_names: HashMap::new(), + constant_attribute_args: HashMap::new(), used_traits: Vec::new(), properties: Vec::new(), property_offsets: HashMap::new(), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 6dde52f03c..9e70311069 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -687,6 +687,9 @@ fn lower_builtin_reflection_methods( for class_name in [ "ReflectionAttribute", "ReflectionClass", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", "ReflectionMethod", "ReflectionProperty", "ReflectionFunction", diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 1a61484fa2..912d665ba8 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -167,6 +167,8 @@ fn class_info(_class_name: &str) -> ClassInfo { method_attribute_args: HashMap::new(), property_attribute_names: HashMap::new(), property_attribute_args: HashMap::new(), + constant_attribute_names: HashMap::new(), + constant_attribute_args: HashMap::new(), used_traits: Vec::new(), properties: Vec::new(), property_offsets: HashMap::new(), diff --git a/src/types/checker/builtin_enums.rs b/src/types/checker/builtin_enums.rs index 8103e24c01..b71f36a7b8 100644 --- a/src/types/checker/builtin_enums.rs +++ b/src/types/checker/builtin_enums.rs @@ -35,10 +35,14 @@ pub(crate) fn inject_builtin_enums( EnumCaseInfo { name: "Ascending".to_string(), value: None, + attribute_names: Vec::new(), + attribute_args: Vec::new(), }, EnumCaseInfo { name: "Descending".to_string(), value: None, + attribute_names: Vec::new(), + attribute_args: Vec::new(), }, ], &[], diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 1eddec371b..82f7ab3576 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -22,7 +22,7 @@ use crate::types::PhpType; use super::super::Checker; -/// Injects the four built-in reflection types into `class_map` after verifying +/// Injects the built-in reflection types into `class_map` after verifying /// none are already declared. Each type is a dummy shell; runtime population /// happens in codegen. Returns an error if any reflection name is already in use. pub(crate) fn inject_builtin_reflection( @@ -38,6 +38,9 @@ pub(crate) fn inject_builtin_reflection( "ReflectionFunction", "ReflectionParameter", "ReflectionNamedType", + "ReflectionClassConstant", + "ReflectionEnumUnitCase", + "ReflectionEnumBackedCase", ] { let builtin_key = php_symbol_key(builtin_name); if interface_map @@ -48,7 +51,10 @@ pub(crate) fn inject_builtin_reflection( { return Err(CompileError::new( crate::span::Span::dummy(), - &format!("Cannot redeclare built-in reflection type: {}", builtin_name), + &format!( + "Cannot redeclare built-in reflection type: {}", + builtin_name + ), )); } } @@ -63,9 +69,24 @@ pub(crate) fn inject_builtin_reflection( is_final: true, is_readonly_class: false, properties: vec![ - builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), - builtin_property("__args", Visibility::Private, Some(array_type()), empty_array()), - builtin_property("__factory", Visibility::Private, Some(TypeExpr::Int), int_lit(0)), + builtin_property( + "__name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__args", + Visibility::Private, + Some(array_type()), + empty_array(), + ), + builtin_property( + "__factory", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + ), ], methods: vec![ builtin_reflection_attribute_constructor_method(), @@ -78,14 +99,12 @@ pub(crate) fn inject_builtin_reflection( used_traits: Vec::new(), }, ); - class_map.insert( - "ReflectionClass".to_string(), - builtin_reflection_class(), - ); + class_map.insert("ReflectionClass".to_string(), builtin_reflection_class()); class_map.insert( "ReflectionMethod".to_string(), builtin_reflection_owner_class( "ReflectionMethod", + false, vec![ ("class_name", Some(TypeExpr::Str), None, false), ("method_name", Some(TypeExpr::Str), None, false), @@ -96,6 +115,7 @@ pub(crate) fn inject_builtin_reflection( "ReflectionProperty".to_string(), builtin_reflection_owner_class( "ReflectionProperty", + false, vec![ ("class_name", Some(TypeExpr::Str), None, false), ("property_name", Some(TypeExpr::Str), None, false), @@ -111,6 +131,23 @@ pub(crate) fn inject_builtin_reflection( "ReflectionNamedType".to_string(), builtin_reflection_named_type(), ); + for class_name in [ + "ReflectionClassConstant", + "ReflectionEnumUnitCase", + "ReflectionEnumBackedCase", + ] { + class_map.insert( + class_name.to_string(), + builtin_reflection_owner_class( + class_name, + true, + vec![ + ("class_name", Some(TypeExpr::Str), None, false), + ("constant_name", Some(TypeExpr::Str), None, false), + ], + ), + ); + } Ok(()) } @@ -487,7 +524,12 @@ fn builtin_reflection_class() -> FlattenedClass { is_final: true, is_readonly_class: false, properties: vec![ - builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), + builtin_property( + "__name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), builtin_property( "__attrs", Visibility::Private, @@ -548,8 +590,29 @@ fn builtin_reflection_class_get_name_method() -> ClassMethod { /// returning the `__attrs` array). fn builtin_reflection_owner_class( name: &str, + has_name: bool, constructor_params: Vec<(&str, Option, Option, bool)>, ) -> FlattenedClass { + let mut properties = Vec::new(); + let mut methods = vec![builtin_reflection_owner_constructor_method( + constructor_params, + )]; + if has_name { + properties.push(builtin_property( + "__name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + methods.push(builtin_reflection_class_get_name_method()); + } + properties.push(builtin_property( + "__attrs", + Visibility::Private, + Some(array_type()), + empty_array(), + )); + methods.push(builtin_reflection_owner_get_attributes_method()); FlattenedClass { name: name.to_string(), extends: None, @@ -557,16 +620,8 @@ fn builtin_reflection_owner_class( is_abstract: false, is_final: true, is_readonly_class: false, - properties: vec![builtin_property( - "__attrs", - Visibility::Private, - Some(array_type()), - empty_array(), - )], - methods: vec![ - builtin_reflection_owner_constructor_method(constructor_params), - builtin_reflection_owner_get_attributes_method(), - ], + properties, + methods, attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), @@ -657,20 +712,32 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Mixed; } } - for class_name in ["ReflectionClass", "ReflectionMethod", "ReflectionProperty"] { + for class_name in [ + "ReflectionClass", + "ReflectionMethod", + "ReflectionProperty", + "ReflectionClassConstant", + "ReflectionEnumUnitCase", + "ReflectionEnumBackedCase", + ] { if let Some(class_info) = checker.classes.get_mut(class_name) { if let Some(sig) = class_info.methods.get_mut("__construct") { sig.return_type = PhpType::Void; } - if class_name == "ReflectionClass" { + if matches!( + class_name, + "ReflectionClass" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getName")) { sig.return_type = PhpType::Str; } } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { - sig.return_type = PhpType::Array(Box::new(PhpType::Object( - "ReflectionAttribute".to_string(), - ))); + sig.return_type = + PhpType::Array(Box::new(PhpType::Object("ReflectionAttribute".to_string()))); } } } diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 3be8b0ef5e..acf58a811a 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -15,6 +15,8 @@ use crate::types::{fibers, FunctionSig, PhpType, TypeEnv}; use super::super::super::Checker; +mod reflection; + impl Checker { /// Infers the type of a `new Class(...)` expression. /// @@ -81,7 +83,8 @@ impl Checker { .map(String::as_str) .unwrap_or(class_name.as_str()); if !self.can_access_member(declaring_class, visibility) - && !self.can_construct_internal_iterator_from_builtin_get_iterator(&class_name) + && !self + .can_construct_internal_iterator_from_builtin_get_iterator(&class_name) { return Err(CompileError::new( expr.span, @@ -243,6 +246,37 @@ impl Checker { )?; self.validate_reflection_property_attrs(&reflected_class, &property_name, expr) } + "ReflectionClassConstant" => { + let constant_name = self.reflection_string_literal_arg( + class_name, + "constant name", + normalized_args.get(1), + env, + )?; + self.validate_reflection_class_constant_attrs( + &reflected_class, + &constant_name, + expr, + ) + } + "ReflectionEnumUnitCase" => { + let case_name = self.reflection_string_literal_arg( + class_name, + "case name", + normalized_args.get(1), + env, + )?; + self.validate_reflection_enum_case_attrs(&reflected_class, &case_name, false, expr) + } + "ReflectionEnumBackedCase" => { + let case_name = self.reflection_string_literal_arg( + class_name, + "case name", + normalized_args.get(1), + env, + )?; + self.validate_reflection_enum_case_attrs(&reflected_class, &case_name, true, expr) + } _ => Ok(()), } } @@ -357,128 +391,6 @@ impl Checker { )) } } - - /// Validates that a class's attributes do not have unsupported argument metadata. - /// - /// Returns `Ok` if the class has no attribute args or if all args are - /// supported. Used by `ReflectionClass` constructor validation. - fn validate_reflection_class_attrs( - &self, - class_name: &str, - expr: &Expr, - ) -> Result<(), CompileError> { - let Some(class_info) = self.classes.get(class_name) else { - return Err(CompileError::new( - expr.span, - &format!("ReflectionClass::__construct(): undefined class '{}'", class_name), - )); - }; - if attributes_have_unsupported_args(&class_info.attribute_names, &class_info.attribute_args) - { - return Err(CompileError::new( - expr.span, - "ReflectionClass::getAttributes(): class has attribute argument metadata that is not supported yet", - )); - } - Ok(()) - } - - /// Validates that a method's attributes do not have unsupported argument metadata. - /// - /// Also checks that the method exists on the class. Used by - /// `ReflectionMethod` constructor validation. - fn validate_reflection_method_attrs( - &self, - class_name: &str, - method_name: &str, - expr: &Expr, - ) -> Result<(), CompileError> { - let Some(class_info) = self.classes.get(class_name) else { - return Err(CompileError::new( - expr.span, - &format!("ReflectionMethod::__construct(): undefined class '{}'", class_name), - )); - }; - let method_key = php_symbol_key(method_name); - if !class_info.methods.contains_key(&method_key) - && !class_info.static_methods.contains_key(&method_key) - { - return Err(CompileError::new( - expr.span, - &format!( - "ReflectionMethod::__construct(): undefined method '{}::{}'", - class_name, method_name - ), - )); - } - let empty_names = Vec::new(); - let empty_args = Vec::new(); - let names = class_info - .method_attribute_names - .get(&method_key) - .unwrap_or(&empty_names); - let args = class_info - .method_attribute_args - .get(&method_key) - .unwrap_or(&empty_args); - if attributes_have_unsupported_args(names, args) { - return Err(CompileError::new( - expr.span, - "ReflectionMethod::getAttributes(): method has attribute argument metadata that is not supported yet", - )); - } - Ok(()) - } - - /// Validates that a property's attributes do not have unsupported argument metadata. - /// - /// Also checks that the property exists on the class (instance or static). - /// Used by `ReflectionProperty` constructor validation. - fn validate_reflection_property_attrs( - &self, - class_name: &str, - property_name: &str, - expr: &Expr, - ) -> Result<(), CompileError> { - let Some(class_info) = self.classes.get(class_name) else { - return Err(CompileError::new( - expr.span, - &format!("ReflectionProperty::__construct(): undefined class '{}'", class_name), - )); - }; - if !class_info.properties.iter().any(|(name, _)| name == property_name) - && !class_info - .static_properties - .iter() - .any(|(name, _)| name == property_name) - { - return Err(CompileError::new( - expr.span, - &format!( - "ReflectionProperty::__construct(): undefined property '{}::${}'", - class_name, property_name - ), - )); - } - let empty_names = Vec::new(); - let empty_args = Vec::new(); - let names = class_info - .property_attribute_names - .get(property_name) - .unwrap_or(&empty_names); - let args = class_info - .property_attribute_args - .get(property_name) - .unwrap_or(&empty_args); - if attributes_have_unsupported_args(names, args) { - return Err(CompileError::new( - expr.span, - "ReflectionProperty::getAttributes(): property has attribute argument metadata that is not supported yet", - )); - } - Ok(()) - } - /// Resolves a static receiver to a class name for reflection class constant. /// /// `Named` returns the canonical name. `Self_`/`Static` require a class @@ -490,10 +402,11 @@ impl Checker { ) -> Result { match receiver { StaticReceiver::Named(name) => Ok(name.as_canonical()), - StaticReceiver::Self_ | StaticReceiver::Static => self - .current_class - .clone() - .ok_or_else(|| CompileError::new(span, "Cannot use self::class outside a class context")), + StaticReceiver::Self_ | StaticReceiver::Static => { + self.current_class.clone().ok_or_else(|| { + CompileError::new(span, "Cannot use self::class outside a class context") + }) + } StaticReceiver::Parent => { let current = self.current_class.as_ref().ok_or_else(|| { CompileError::new(span, "Cannot use parent::class outside a class context") @@ -502,10 +415,7 @@ impl Checker { .get(current) .and_then(|info| info.parent.clone()) .ok_or_else(|| { - CompileError::new( - span, - &format!("Class '{}' has no parent class", current), - ) + CompileError::new(span, &format!("Class '{}' has no parent class", current)) }) } } @@ -546,7 +456,10 @@ impl Checker { { return Ok(()); } - return Err(CompileError::new(callback.span, "Fiber callback must be callable")); + return Err(CompileError::new( + callback.span, + "Fiber callback must be callable", + )); }; let visible_param_count = match &callback.kind { @@ -572,11 +485,19 @@ impl Checker { } match &callback.kind { - ExprKind::StringLiteral(name) => self.resolve_fiber_string_callable_sig(name, callback.span, env), - ExprKind::ArrayLiteral(_) => self.resolve_fiber_callable_array_literal_sig(callback, env), + ExprKind::StringLiteral(name) => { + self.resolve_fiber_string_callable_sig(name, callback.span, env) + } + ExprKind::ArrayLiteral(_) => { + self.resolve_fiber_callable_array_literal_sig(callback, env) + } ExprKind::Variable(name) => { if let Some(target) = self.callable_array_targets.get(name).cloned() { - return self.resolve_fiber_callable_array_variable_sig(&target, callback.span, env); + return self.resolve_fiber_callable_array_variable_sig( + &target, + callback.span, + env, + ); } self.resolve_fiber_invokable_object_sig(callback, env) } @@ -600,7 +521,9 @@ impl Checker { receiver: StaticReceiver::Named(Name::from(class_name.to_string())), method: method_name.to_string(), }; - return self.resolve_first_class_callable_sig(&target, span, env).map(Some); + return self + .resolve_first_class_callable_sig(&target, span, env) + .map(Some); } let function_name = self @@ -608,7 +531,8 @@ impl Checker { .or_else(|| crate::name_resolver::canonical_builtin_function_name(name)) .unwrap_or_else(|| name.trim_start_matches('\\').to_string()); let target = CallableTarget::Function(Name::from(function_name)); - self.resolve_first_class_callable_sig(&target, span, env).map(Some) + self.resolve_first_class_callable_sig(&target, span, env) + .map(Some) } /// Resolves a literal callable array to a static-method or receiver-bound method signature. @@ -631,7 +555,8 @@ impl Checker { span: crate::span::Span, env: &TypeEnv, ) -> Result, CompileError> { - self.resolve_first_class_callable_sig(target, span, env).map(Some) + self.resolve_first_class_callable_sig(target, span, env) + .map(Some) } /// Resolves an invokable object callback signature for `$object` or `$this`. @@ -694,9 +619,9 @@ impl Checker { ExprKind::StringLiteral(class_name) => self .resolve_fiber_callable_class_name(class_name) .map(str::to_string), - ExprKind::ClassConstant { receiver } => Some( - self.resolve_fiber_callable_static_receiver_class(receiver, span)?, - ), + ExprKind::ClassConstant { receiver } => { + Some(self.resolve_fiber_callable_static_receiver_class(receiver, span)?) + } _ => None, }; Ok(class_name.map(|class_name| StaticReceiver::Named(Name::from(class_name)))) @@ -728,10 +653,7 @@ impl Checker { .get(current) .and_then(|class_info| class_info.parent.clone()) .ok_or_else(|| { - CompileError::new( - span, - &format!("Class '{}' has no parent class", current), - ) + CompileError::new(span, &format!("Class '{}' has no parent class", current)) }) } } @@ -851,7 +773,10 @@ impl Checker { let Some(sig) = self.functions.get_mut(name.as_str()) else { return Err(CompileError::new( span, - &format!("Undefined function for CallbackFilterIterator callback: {}", name), + &format!( + "Undefined function for CallbackFilterIterator callback: {}", + name + ), )); }; let callback_arg_types = [ @@ -911,7 +836,8 @@ fn fiber_callable_array_parts(expr: &Expr) -> Option<(&Expr, &str)> { } /// Returns `true` if `class_name` is a reflection owner class -/// (`ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`). +/// (`ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`, +/// `ReflectionClassConstant`, `ReflectionEnumUnitCase`, `ReflectionEnumBackedCase`). fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, @@ -919,6 +845,9 @@ fn is_reflection_owner_class(class_name: &str) -> bool { | "ReflectionMethod" | "ReflectionProperty" | "ReflectionFunction" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" ) } diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs new file mode 100644 index 0000000000..63a6b6bd25 --- /dev/null +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -0,0 +1,286 @@ +//! Purpose: +//! Validates builtin Reflection owner constructor metadata for object inference. +//! Keeps ReflectionClass/Method/Property/Constant/EnumCase attribute checks out +//! of the general constructor inference driver. +//! +//! Called from: +//! - `crate::types::checker::inference::objects::constructors::Checker::validate_reflection_constructor_args()`. +//! +//! Key details: +//! - Constructor validation checks that reflected members exist and that captured +//! attribute arguments are materializable by the current ReflectionAttribute model. + +use crate::errors::CompileError; +use crate::names::php_symbol_key; +use crate::parser::ast::Expr; +use crate::types::checker::Checker; + +type ReflectionAttributeArgs = Vec>>; + +impl Checker { + /// Validates class-level attributes for `ReflectionClass`. + /// + /// Returns `Ok` when the class exists and its captured attribute argument + /// metadata can be materialized by `ReflectionAttribute::getArguments()`. + pub(super) fn validate_reflection_class_attrs( + &self, + class_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + let Some(class_info) = self.classes.get(class_name) else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionClass::__construct(): undefined class '{}'", + class_name + ), + )); + }; + self.validate_reflection_attribute_metadata( + &class_info.attribute_names, + &class_info.attribute_args, + expr, + "ReflectionClass::getAttributes(): class has attribute argument metadata that is not supported yet", + ) + } + + /// Validates method-level attributes for `ReflectionMethod`. + /// + /// Checks that the method exists on the reflected class before validating + /// its captured attribute argument metadata. + pub(super) fn validate_reflection_method_attrs( + &self, + class_name: &str, + method_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + let Some(class_info) = self.classes.get(class_name) else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionMethod::__construct(): undefined class '{}'", + class_name + ), + )); + }; + let method_key = php_symbol_key(method_name); + if !class_info.methods.contains_key(&method_key) + && !class_info.static_methods.contains_key(&method_key) + { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionMethod::__construct(): undefined method '{}::{}'", + class_name, method_name + ), + )); + } + let empty_names = Vec::new(); + let empty_args = Vec::new(); + let names = class_info + .method_attribute_names + .get(&method_key) + .unwrap_or(&empty_names); + let args = class_info + .method_attribute_args + .get(&method_key) + .unwrap_or(&empty_args); + self.validate_reflection_attribute_metadata( + names, + args, + expr, + "ReflectionMethod::getAttributes(): method has attribute argument metadata that is not supported yet", + ) + } + + /// Validates property-level attributes for `ReflectionProperty`. + /// + /// Checks both instance and static properties because PHP reflection accepts + /// either surface through the same constructor. + pub(super) fn validate_reflection_property_attrs( + &self, + class_name: &str, + property_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + let Some(class_info) = self.classes.get(class_name) else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionProperty::__construct(): undefined class '{}'", + class_name + ), + )); + }; + if !class_info + .properties + .iter() + .any(|(name, _)| name == property_name) + && !class_info + .static_properties + .iter() + .any(|(name, _)| name == property_name) + { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionProperty::__construct(): undefined property '{}::${}'", + class_name, property_name + ), + )); + } + let empty_names = Vec::new(); + let empty_args = Vec::new(); + let names = class_info + .property_attribute_names + .get(property_name) + .unwrap_or(&empty_names); + let args = class_info + .property_attribute_args + .get(property_name) + .unwrap_or(&empty_args); + self.validate_reflection_attribute_metadata( + names, + args, + expr, + "ReflectionProperty::getAttributes(): property has attribute argument metadata that is not supported yet", + ) + } + + /// Validates class-constant or enum-case attributes for `ReflectionClassConstant`. + /// + /// Enum cases are checked first because PHP exposes them through + /// `ReflectionClassConstant` as well as the enum-case-specific reflectors. + pub(super) fn validate_reflection_class_constant_attrs( + &self, + class_name: &str, + constant_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + if let Some((names, args)) = + self.reflection_enum_case_attribute_metadata(class_name, constant_name) + { + return self.validate_reflection_attribute_metadata( + &names, + &args, + expr, + "ReflectionClassConstant::getAttributes(): enum case has attribute argument metadata that is not supported yet", + ); + } + let Some((names, args)) = + self.reflection_class_constant_attribute_metadata(class_name, constant_name) + else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionClassConstant::__construct(): undefined class constant '{}::{}'", + class_name, constant_name + ), + )); + }; + self.validate_reflection_attribute_metadata( + &names, + &args, + expr, + "ReflectionClassConstant::getAttributes(): class constant has attribute argument metadata that is not supported yet", + ) + } + + /// Validates enum-case attributes for `ReflectionEnumUnitCase` or `ReflectionEnumBackedCase`. + /// + /// `require_backed` is true for `ReflectionEnumBackedCase`; unit-case + /// reflection accepts backed cases too, matching PHP. + pub(super) fn validate_reflection_enum_case_attrs( + &self, + enum_name: &str, + case_name: &str, + require_backed: bool, + expr: &Expr, + ) -> Result<(), CompileError> { + let Some(enum_info) = self.enums.get(enum_name) else { + return Err(CompileError::new( + expr.span, + &format!("{} is not an enum", enum_name), + )); + }; + if require_backed && enum_info.backing_type.is_none() { + return Err(CompileError::new( + expr.span, + &format!( + "Enum case {}::{} is not a backed case", + enum_name, case_name + ), + )); + } + let Some((names, args)) = + self.reflection_enum_case_attribute_metadata(enum_name, case_name) + else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionEnumUnitCase::__construct(): undefined enum case '{}::{}'", + enum_name, case_name + ), + )); + }; + self.validate_reflection_attribute_metadata( + &names, + &args, + expr, + "ReflectionEnumUnitCase::getAttributes(): enum case has attribute argument metadata that is not supported yet", + ) + } + + /// Returns cloned class-constant attribute metadata, walking parent classes. + fn reflection_class_constant_attribute_metadata( + &self, + class_name: &str, + constant_name: &str, + ) -> Option<(Vec, ReflectionAttributeArgs)> { + let class_info = self.classes.get(class_name)?; + if class_info.constants.contains_key(constant_name) { + return Some(( + class_info + .constant_attribute_names + .get(constant_name) + .cloned() + .unwrap_or_default(), + class_info + .constant_attribute_args + .get(constant_name) + .cloned() + .unwrap_or_default(), + )); + } + let parent = class_info.parent.as_deref()?; + self.reflection_class_constant_attribute_metadata(parent, constant_name) + } + + /// Returns cloned enum-case attribute metadata for one case. + fn reflection_enum_case_attribute_metadata( + &self, + enum_name: &str, + case_name: &str, + ) -> Option<(Vec, ReflectionAttributeArgs)> { + self.enums + .get(enum_name)? + .cases + .iter() + .find(|case| case.name == case_name) + .map(|case| (case.attribute_names.clone(), case.attribute_args.clone())) + } + + /// Validates one pair of reflection attribute metadata slices. + fn validate_reflection_attribute_metadata( + &self, + names: &[String], + args: &[Option>], + expr: &Expr, + message: &str, + ) -> Result<(), CompileError> { + if super::attributes_have_unsupported_args(names, args) { + return Err(CompileError::new(expr.span, message)); + } + Ok(()) + } +} diff --git a/src/types/checker/schema/classes/mod.rs b/src/types/checker/schema/classes/mod.rs index ce4f6eae69..e7ccc428b3 100644 --- a/src/types/checker/schema/classes/mod.rs +++ b/src/types/checker/schema/classes/mod.rs @@ -25,6 +25,8 @@ use super::super::Checker; use super::validation::build_constructor_param_map; use state::ClassBuildState; +pub(super) use state::{collect_attribute_args, collect_attribute_names}; + /// Recursively builds and registers `ClassInfo` for `class_name` and its inheritance chain. /// /// Uses `building` set to detect circular inheritance. Validates modifiers, resolves the parent @@ -77,8 +79,7 @@ pub(crate) fn build_class_info_recursive( )?; interfaces::ensure_concrete_class_implements_abstracts(&state, &class)?; - let constructor_param_to_prop = - constructor_param_to_prop_for(&class, parent_info.as_ref()); + let constructor_param_to_prop = constructor_param_to_prop_for(&class, parent_info.as_ref()); let class_info = state.into_class_info(*next_class_id, &class, constructor_param_to_prop)?; checker.classes.insert(class.name.clone(), class_info); *next_class_id += 1; @@ -166,7 +167,10 @@ fn validate_parent_constraints( if parent.is_final { return Err(CompileError::new( crate::span::Span::dummy(), - &format!("Class {} cannot extend final class {}", class.name, parent_name), + &format!( + "Class {} cannot extend final class {}", + class.name, parent_name + ), )); } if class.is_readonly_class != parent.is_readonly_class { diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index d54674e11c..4f77896cbc 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -100,6 +100,26 @@ impl ClassBuildState { constructor_param_to_prop: Vec>, ) -> Result { let attribute_args = collect_attribute_args(&class.attributes); + let constant_attribute_names = class + .constants + .iter() + .map(|constant| { + ( + constant.name.clone(), + collect_attribute_names(&constant.attributes), + ) + }) + .collect(); + let constant_attribute_args = class + .constants + .iter() + .map(|constant| { + ( + constant.name.clone(), + collect_attribute_args(&constant.attributes), + ) + }) + .collect(); Ok(ClassInfo { class_id, parent: class.extends.clone(), @@ -124,6 +144,8 @@ impl ClassBuildState { method_attribute_args: self.method_attribute_args, property_attribute_names: self.property_attribute_names, property_attribute_args: self.property_attribute_args, + constant_attribute_names, + constant_attribute_args, used_traits: class.used_traits.clone(), properties: self.prop_types, property_offsets: self.property_offsets, @@ -165,14 +187,13 @@ impl ClassBuildState { constructor_param_to_prop, }) } - } /// Collect attribute names from a class's attribute groups, preserving source /// order. Name resolution has already canonicalised fully-qualified names by /// the time this runs, so names are emitted in ReflectionAttribute::getName() /// shape without a synthetic leading backslash. -pub(super) fn collect_attribute_names( +pub(in crate::types::checker::schema) fn collect_attribute_names( groups: &[crate::parser::ast::AttributeGroup], ) -> Vec { let mut out = Vec::new(); @@ -195,7 +216,7 @@ pub(super) fn collect_attribute_names( /// `#[Status(-1)]` survives parsing. Unsupported metadata is marked as /// `None` so legal PHP attribute syntax can still compile until a runtime /// reflection helper needs the missing argument payload. -pub(super) fn collect_attribute_args( +pub(in crate::types::checker::schema) fn collect_attribute_args( groups: &[crate::parser::ast::AttributeGroup], ) -> Vec>> { use crate::parser::ast::ExprKind; diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 143edc4638..2dd01ee06f 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -16,6 +16,7 @@ use crate::parser::ast::{ClassMethod, ExprKind, Visibility}; use crate::types::{ClassInfo, EnumCaseInfo, EnumCaseValue, EnumInfo, FunctionSig, PhpType}; use super::super::Checker; +use super::classes::{collect_attribute_args, collect_attribute_names}; use super::validation::build_method_sig; /// Propagates concrete return types from overrides to their abstract parent declarations. @@ -224,6 +225,8 @@ pub(crate) fn build_enum_info( enum_cases.push(EnumCaseInfo { name: case.name.clone(), value, + attribute_names: collect_attribute_names(&case.attributes), + attribute_args: collect_attribute_args(&case.attributes), }); } @@ -374,8 +377,22 @@ pub(crate) fn insert_enum_metadata( // User-declared enum constants. Values are kept as their parsed expressions, matching the // class-constant representation. let mut constants = HashMap::new(); + let mut constant_attribute_names = HashMap::new(); + let mut constant_attribute_args = HashMap::new(); for constant in user_constants { constants.insert(constant.name.clone(), constant.value.clone()); + constant_attribute_names.insert( + constant.name.clone(), + collect_attribute_names(&constant.attributes), + ); + constant_attribute_args.insert( + constant.name.clone(), + collect_attribute_args(&constant.attributes), + ); + } + for case in &enum_cases { + constant_attribute_names.insert(case.name.clone(), case.attribute_names.clone()); + constant_attribute_args.insert(case.name.clone(), case.attribute_args.clone()); } let interfaces: Vec = implements @@ -399,6 +416,8 @@ pub(crate) fn insert_enum_metadata( method_attribute_args: HashMap::new(), property_attribute_names: HashMap::new(), property_attribute_args: HashMap::new(), + constant_attribute_names, + constant_attribute_args, used_traits: Vec::new(), properties, property_offsets, diff --git a/src/types/schema.rs b/src/types/schema.rs index a4b6c8d38f..5d9a025d31 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -148,6 +148,11 @@ pub struct ClassInfo { pub property_attribute_names: HashMap>, /// Literal property-attribute args aligned with `property_attribute_names`. pub property_attribute_args: HashMap>>>, + /// Attribute names attached to class constants visible on this class. + /// Constant names are case-sensitive, so the source constant name is the key. + pub constant_attribute_names: HashMap>, + /// Literal class-constant-attribute args aligned with `constant_attribute_names`. + pub constant_attribute_args: HashMap>>>, /// Trait names used directly by this class declaration, preserving source order. pub used_traits: Vec, pub properties: Vec<(String, PhpType)>, @@ -217,6 +222,8 @@ pub enum EnumCaseValue { pub struct EnumCaseInfo { pub name: String, pub value: Option, + pub attribute_names: Vec, + pub attribute_args: Vec>>, } /// Enum metadata for a resolved backed enum declaration (PHP 8.1+). diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 74b1a4b6ab..0a98899d11 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5392,6 +5392,54 @@ echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance ); } +/// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. +#[test] +fn test_eval_reflection_constant_and_enum_case_attributes() { + let out = compile_and_run_capture( + r#"name = $name; + } + public function label() { + return $this->name; + } +} +class EvalConstReflectTarget { + #[EvalConstMarker("const")] + public const ANSWER = 42; +} +enum EvalCaseReflectTarget: string { + #[EvalConstMarker("case")] + case Ready = "ready"; +} +$constAttrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); +echo count($constAttrs) . ":" . (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName() . ":"; +echo $constAttrs[0]->getName() . ":" . $constAttrs[0]->getArguments()[0] . ":"; +echo $constAttrs[0]->newInstance()->label() . ":"; +$caseAttrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo count($caseAttrs) . ":" . (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo $caseAttrs[0]->getName() . ":" . $caseAttrs[0]->getArguments()[0] . ":"; +$unitAttrs = (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo $unitAttrs[0]->newInstance()->label() . ":"; +$backedAttrs = (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo $backedAttrs[0]->newInstance()->label();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:ANSWER:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:case:Ready:case" + ); +} + /// Verifies eval interface and trait constants work through the bridge. #[test] fn test_eval_declared_interface_and_trait_constants() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 2a8c495dbc..204df09e03 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -719,10 +719,7 @@ foreach ($attrs as $attr) { } "#, ); - assert_eq!( - out, - "count=2\nAuthor:[Ada][1815]\nVersion:[1.0][1]\n" - ); + assert_eq!(out, "count=2\nAuthor:[Ada][1815]\nVersion:[1.0][1]\n"); } /// Verifies that `class_get_attributes()` returns an empty array for a @@ -1126,10 +1123,7 @@ echo $ref->getName(); fn test_reflection_class_get_name_for_autoloaded_class() { let out = compile_and_run_files( &[ - ( - "DemoThing.php", - "getArguments()[0]; assert_eq!(out, "1/Column/id"); } +/// Verifies that `ReflectionClassConstant` and enum-case reflectors expose +/// attribute name, arguments, `getName()`, and `newInstance()` data. +#[test] +fn test_reflection_constant_and_enum_case_get_attributes() { + let out = compile_and_run( + r#"label; } +} +class ConstTarget { + #[Marker("const")] + public const ANSWER = 42; +} +enum CaseTarget: string { + #[Marker("case")] + case Ready = "ready"; +} +$const = new ReflectionClassConstant(ConstTarget::class, "ANSWER"); +$constAttrs = $const->getAttributes(); +echo $const->getName() . "/"; +echo count($constAttrs) . "/"; +echo $constAttrs[0]->getName() . "/"; +echo $constAttrs[0]->getArguments()[0] . "/"; +echo $constAttrs[0]->newInstance()->label() . "\n"; +$case = new ReflectionClassConstant(CaseTarget::class, "Ready"); +$caseAttrs = $case->getAttributes(); +echo $case->getName() . "/"; +echo count($caseAttrs) . "/"; +echo $caseAttrs[0]->getName() . "/"; +echo $caseAttrs[0]->getArguments()[0] . "/"; +echo $caseAttrs[0]->newInstance()->label() . "\n"; +$unit = new ReflectionEnumUnitCase(CaseTarget::class, "Ready"); +$unitAttrs = $unit->getAttributes(); +echo $unit->getName() . "/"; +echo $unitAttrs[0]->newInstance()->label() . "\n"; +$backed = new ReflectionEnumBackedCase(CaseTarget::class, "Ready"); +$backedAttrs = $backed->getAttributes(); +echo $backed->getName() . "/"; +echo $backedAttrs[0]->newInstance()->label(); +"#, + ); + assert_eq!( + out, + "ANSWER/1/Marker/const/const\nReady/1/Marker/case/case\nReady/case\nReady/case" + ); +} + /// Verifies that `ReflectionClass` accepts `user::class` (lowercase class /// constant) for case-insensitive class resolution and `getAttributes()` /// returns the correct attribute data. From eaf840c8fa9c8be62e3068665710ea57d1dc2e2a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 20:31:18 +0200 Subject: [PATCH 0350/1208] Expose reflection member names --- crates/elephc-eval/src/interpreter/reflection.rs | 4 ++-- .../src/interpreter/tests/builtins_class_metadata.rs | 8 +++++--- docs/php/eval.md | 5 +++-- src/codegen/eval_reflection_owner_helpers.rs | 12 ++++++------ src/codegen/lower_inst/objects/reflection.rs | 6 ++++-- src/types/checker/builtin_types/reflection.rs | 6 ++++-- tests/codegen/eval.rs | 8 +++++--- tests/codegen/oop/attributes.rs | 12 ++++++++---- 8 files changed, 37 insertions(+), 24 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index b45bb88eeb..a8fe734f8c 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -91,7 +91,7 @@ fn eval_reflection_method_new( .ok_or(EvalStatus::RuntimeFatal)?; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_METHOD, - "", + &method_name, &attributes, context, values, @@ -118,7 +118,7 @@ fn eval_reflection_property_new( .ok_or(EvalStatus::RuntimeFatal)?; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_PROPERTY, - "", + &property_name, &attributes, context, values, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index be13b4be00..d49a2c7ef7 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -180,10 +180,12 @@ $class_attrs = (new ReflectionClass("EvalReflectTarget"))->getAttributes(); echo count($class_attrs); echo ":"; echo (new ReflectionClass("EvalReflectTarget"))->getName(); echo ":"; echo $class_attrs[0]->getName(); echo ":"; echo $class_attrs[0]->newInstance()->label(); echo ":"; $method_attrs = (new ReflectionMethod("EvalReflectTarget", "handle"))->getAttributes(); -echo count($method_attrs); echo ":"; echo $method_attrs[0]->getName(); echo ":"; +echo count($method_attrs); echo ":"; echo (new ReflectionMethod("EvalReflectTarget", "handle"))->getName(); echo ":"; +echo $method_attrs[0]->getName(); echo ":"; echo $method_attrs[0]->getArguments()[0]; echo ":"; echo $method_attrs[0]->newInstance()->label(); echo ":"; $property_attrs = (new ReflectionProperty("EvalReflectTarget", "id"))->getAttributes(); -echo count($property_attrs); echo ":"; echo $property_attrs[0]->getName(); echo ":"; +echo count($property_attrs); echo ":"; echo (new ReflectionProperty("EvalReflectTarget", "id"))->getName(); echo ":"; +echo $property_attrs[0]->getName(); echo ":"; echo $property_attrs[0]->getArguments()[0]; echo ":"; echo $property_attrs[0]->newInstance()->label(); return true;"#, ) @@ -195,7 +197,7 @@ return true;"#, assert_eq!( values.output, - "1:EvalReflectTarget:EvalMarker:class:1:EvalMarker:method:method:1:EvalMarker:property:property" + "1:EvalReflectTarget:EvalMarker:class:1:handle:EvalMarker:method:method:1:id:EvalMarker:property:property" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 77c0f1df21..f541130f39 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -150,11 +150,12 @@ syntax, but requesting those arguments is a runtime fatal. `ReflectionClass::getAttributes()`, `ReflectionMethod::getAttributes()`, and `ReflectionProperty::getAttributes()` expose eval-retained class, method, and property attributes for eval-declared class-like symbols when their arguments -fit the same literal subset. `ReflectionClassConstant::getAttributes()`, +fit the same literal subset, and `getName()` returns the reflected class or +member name for those owners. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant and enum-case attributes through the same materialized `ReflectionAttribute` -shape. +shape; their `getName()` methods return the reflected constant or case name. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 03c9cdb088..edc87626bb 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -90,8 +90,8 @@ fn function_uses_eval(function: &Function) -> bool { fn reflection_owner_layouts(module: &Module) -> Option { Some(ReflectionOwnerLayouts { class: reflection_owner_layout(module.class_infos.get("ReflectionClass")?, true)?, - method: reflection_owner_layout(module.class_infos.get("ReflectionMethod")?, false)?, - property: reflection_owner_layout(module.class_infos.get("ReflectionProperty")?, false)?, + method: reflection_owner_layout(module.class_infos.get("ReflectionMethod")?, true)?, + property: reflection_owner_layout(module.class_infos.get("ReflectionProperty")?, true)?, class_constant: reflection_owner_layout( module.class_infos.get("ReflectionClassConstant")?, true, @@ -175,8 +175,8 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body(emitter, class_label, &layouts.class, true, fail_label, box_label); - emit_aarch64_owner_kind_body(emitter, method_label, &layouts.method, false, fail_label, box_label); - emit_aarch64_owner_kind_body(emitter, property_label, &layouts.property, false, fail_label, box_label); + emit_aarch64_owner_kind_body(emitter, method_label, &layouts.method, true, fail_label, box_label); + emit_aarch64_owner_kind_body(emitter, property_label, &layouts.property, true, fail_label, box_label); emit_aarch64_owner_kind_body(emitter, class_constant_label, &layouts.class_constant, true, fail_label, box_label); emit_aarch64_owner_kind_body(emitter, enum_unit_case_label, &layouts.enum_unit_case, true, fail_label, box_label); emit_aarch64_owner_kind_body(emitter, enum_backed_case_label, &layouts.enum_backed_case, true, fail_label, box_label); @@ -226,8 +226,8 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body(emitter, class_label, &layouts.class, true, fail_label, box_label); - emit_x86_64_owner_kind_body(emitter, method_label, &layouts.method, false, fail_label, box_label); - emit_x86_64_owner_kind_body(emitter, property_label, &layouts.property, false, fail_label, box_label); + emit_x86_64_owner_kind_body(emitter, method_label, &layouts.method, true, fail_label, box_label); + emit_x86_64_owner_kind_body(emitter, property_label, &layouts.property, true, fail_label, box_label); emit_x86_64_owner_kind_body(emitter, class_constant_label, &layouts.class_constant, true, fail_label, box_label); emit_x86_64_owner_kind_body(emitter, enum_unit_case_label, &layouts.enum_unit_case, true, fail_label, box_label); emit_x86_64_owner_kind_body(emitter, enum_backed_case_label, &layouts.enum_backed_case, true, fail_label, box_label); diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 1667601eb1..6e2d99395c 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -499,7 +499,7 @@ fn reflection_method_metadata( Ok(resolve_reflection_class(ctx, &reflected_class) .and_then(|(_, info)| { Some(ReflectionOwnerMetadata { - reflected_name: None, + reflected_name: Some(method_name.clone()), attr_names: info.method_attribute_names.get(&method_key)?.clone(), attr_args: info.method_attribute_args.get(&method_key)?.clone(), }) @@ -523,7 +523,7 @@ fn reflection_property_metadata( Ok(resolve_reflection_class(ctx, &reflected_class) .and_then(|(_, info)| { Some(ReflectionOwnerMetadata { - reflected_name: None, + reflected_name: Some(property_name.clone()), attr_names: info.property_attribute_names.get(&property_name)?.clone(), attr_args: info.property_attribute_args.get(&property_name)?.clone(), }) @@ -792,6 +792,8 @@ fn reflection_owner_has_name(class_name: &str) -> bool { matches!( class_name, "ReflectionClass" + | "ReflectionMethod" + | "ReflectionProperty" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 82f7ab3576..2b65cb1888 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -104,7 +104,7 @@ pub(crate) fn inject_builtin_reflection( "ReflectionMethod".to_string(), builtin_reflection_owner_class( "ReflectionMethod", - false, + true, vec![ ("class_name", Some(TypeExpr::Str), None, false), ("method_name", Some(TypeExpr::Str), None, false), @@ -115,7 +115,7 @@ pub(crate) fn inject_builtin_reflection( "ReflectionProperty".to_string(), builtin_reflection_owner_class( "ReflectionProperty", - false, + true, vec![ ("class_name", Some(TypeExpr::Str), None, false), ("property_name", Some(TypeExpr::Str), None, false), @@ -727,6 +727,8 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if matches!( class_name, "ReflectionClass" + | "ReflectionMethod" + | "ReflectionProperty" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0a98899d11..1047f375e0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5374,10 +5374,12 @@ $classAttrs = (new ReflectionClass("EvalReflectTarget"))->getAttributes(); echo count($classAttrs) . ":" . (new ReflectionClass("EvalReflectTarget"))->getName() . ":"; echo $classAttrs[0]->getName() . ":" . $classAttrs[0]->newInstance()->label() . ":"; $methodAttrs = (new ReflectionMethod("EvalReflectTarget", "handle"))->getAttributes(); -echo count($methodAttrs) . ":" . $methodAttrs[0]->getName() . ":"; +echo count($methodAttrs) . ":" . (new ReflectionMethod("EvalReflectTarget", "handle"))->getName() . ":"; +echo $methodAttrs[0]->getName() . ":"; echo $methodAttrs[0]->getArguments()[0] . ":" . $methodAttrs[0]->newInstance()->label() . ":"; $propertyAttrs = (new ReflectionProperty("EvalReflectTarget", "id"))->getAttributes(); -echo count($propertyAttrs) . ":" . $propertyAttrs[0]->getName() . ":"; +echo count($propertyAttrs) . ":" . (new ReflectionProperty("EvalReflectTarget", "id"))->getName() . ":"; +echo $propertyAttrs[0]->getName() . ":"; echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance()->label();'); "#, ); @@ -5388,7 +5390,7 @@ echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance ); assert_eq!( out.stdout, - "1:EvalReflectTarget:EvalMarker:class:1:EvalMarker:method:method:1:EvalMarker:property:property" + "1:EvalReflectTarget:EvalMarker:class:1:handle:EvalMarker:method:method:1:id:EvalMarker:property:property" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 204df09e03..ee4b80c9c3 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1210,13 +1210,14 @@ class Controller { } $ref = new ReflectionMethod('Controller', 'index'); $attrs = $ref->getAttributes(); +echo $ref->getName() . "/"; echo count($attrs) . "/"; echo $attrs[0]->getName() . "/"; echo $attrs[0]->getArguments()[0] . "/"; echo $attrs[0]->getArguments()[1]; "#, ); - assert_eq!(out, "1/Route//home/GET"); + assert_eq!(out, "index/1/Route//home/GET"); } /// Verifies that `ReflectionMethod`'s constructor accepts named arguments @@ -1232,12 +1233,13 @@ class Controller { } $ref = new ReflectionMethod(method_name: 'index', class_name: 'Controller'); $attrs = $ref->getAttributes(); +echo $ref->getName() . "/"; echo count($attrs) . "/"; echo $attrs[0]->getName() . "/"; echo $attrs[0]->getArguments()[0]; "#, ); - assert_eq!(out, "1/Route//home"); + assert_eq!(out, "index/1/Route//home"); } /// Verifies that `ReflectionProperty::getAttributes()` works when the class @@ -1253,12 +1255,13 @@ class User { } $ref = new ReflectionProperty(User::class, 'id'); $attrs = $ref->getAttributes(); +echo $ref->getName() . "/"; echo count($attrs) . "/"; echo $attrs[0]->getName() . "/"; echo $attrs[0]->getArguments()[0]; "#, ); - assert_eq!(out, "1/Column/id"); + assert_eq!(out, "id/1/Column/id"); } /// Verifies that `ReflectionProperty`'s constructor accepts static associative @@ -1274,12 +1277,13 @@ class User { } $ref = new ReflectionProperty(...["property_name" => "id", "class_name" => "User"]); $attrs = $ref->getAttributes(); +echo $ref->getName() . "/"; echo count($attrs) . "/"; echo $attrs[0]->getName() . "/"; echo $attrs[0]->getArguments()[0]; "#, ); - assert_eq!(out, "1/Column/id"); + assert_eq!(out, "id/1/Column/id"); } /// Verifies that `ReflectionClassConstant` and enum-case reflectors expose From 229fa2ebc09fb8a4b6b7638167128aad5248680b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 20:44:44 +0200 Subject: [PATCH 0351/1208] Expose ReflectionClass modifier flags --- .../elephc-eval/src/interpreter/reflection.rs | 27 +++++-- .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 31 ++++++++ .../interpreter/tests/support/object_ops.rs | 21 ++++-- .../interpreter/tests/support/runtime_ops.rs | 3 +- .../elephc-eval/src/runtime_hooks/externs.rs | 1 + crates/elephc-eval/src/runtime_hooks/ops.rs | 2 + docs/php/eval.md | 5 +- src/codegen/eval_reflection_owner_helpers.rs | 72 +++++++++++++++++++ src/codegen/lower_inst/objects/reflection.rs | 46 ++++++++++++ src/types/checker/builtin_types/reflection.rs | 63 ++++++++++++++++ tests/codegen/eval.rs | 30 ++++++++ tests/codegen/oop/attributes.rs | 21 ++++++ 13 files changed, 313 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index a8fe734f8c..a3c572a1db 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -12,6 +12,9 @@ use super::*; +const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; +const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; + /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. pub(in crate::interpreter) fn eval_reflection_owner_new_object( class_name: &str, @@ -57,7 +60,7 @@ fn eval_reflection_class_new( ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; let class_name = eval_reflection_string_arg(args[0], values)?; - let Some((resolved_name, attributes)) = + let Some((resolved_name, attributes, flags)) = eval_reflection_class_like_attributes(&class_name, context) else { return Ok(None); @@ -66,6 +69,7 @@ fn eval_reflection_class_new( EVAL_REFLECTION_OWNER_CLASS, &resolved_name, &attributes, + flags, context, values, ) @@ -93,6 +97,7 @@ fn eval_reflection_method_new( EVAL_REFLECTION_OWNER_METHOD, &method_name, &attributes, + 0, context, values, ) @@ -120,6 +125,7 @@ fn eval_reflection_property_new( EVAL_REFLECTION_OWNER_PROPERTY, &property_name, &attributes, + 0, context, values, ) @@ -148,6 +154,7 @@ fn eval_reflection_class_constant_new( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, &constant_name, &attributes, + 0, context, values, ) @@ -181,7 +188,7 @@ fn eval_reflection_enum_case_new( .case(&case_name) .map(|case| case.attributes().to_vec()) .ok_or(EvalStatus::RuntimeFatal)?; - eval_reflection_owner_object(owner_kind, &case_name, &attributes, context, values).map(Some) + eval_reflection_owner_object(owner_kind, &case_name, &attributes, 0, context, values).map(Some) } /// Materializes one Reflection owner object and transfers the temporary attribute array. @@ -189,11 +196,12 @@ fn eval_reflection_owner_object( owner_kind: u64, reflected_name: &str, attributes: &[EvalAttribute], + flags: u64, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let attrs = eval_reflection_attribute_array_result(attributes, context, values)?; - let object = values.reflection_owner_new(owner_kind, reflected_name, attrs)?; + let object = values.reflection_owner_new(owner_kind, reflected_name, attrs, flags)?; values.release(attrs)?; Ok(object) } @@ -202,29 +210,40 @@ fn eval_reflection_owner_object( fn eval_reflection_class_like_attributes( name: &str, context: &ElephcEvalContext, -) -> Option<(String, Vec)> { +) -> Option<(String, Vec, u64)> { if let Some(class) = context.class(name) { + let mut flags = 0; + if class.is_final() { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL; + } + if class.is_abstract() { + flags |= EVAL_REFLECTION_CLASS_FLAG_ABSTRACT; + } return Some(( class.name().trim_start_matches('\\').to_string(), class.attributes().to_vec(), + flags, )); } if let Some(interface) = context.interface(name) { return Some(( interface.name().trim_start_matches('\\').to_string(), interface.attributes().to_vec(), + 0, )); } if let Some(trait_decl) = context.trait_decl(name) { return Some(( trait_decl.name().trim_start_matches('\\').to_string(), trait_decl.attributes().to_vec(), + 0, )); } context.enum_decl(name).map(|enum_decl| { ( enum_decl.name().trim_start_matches('\\').to_string(), enum_decl.attributes().to_vec(), + EVAL_REFLECTION_CLASS_FLAG_FINAL, ) }) } diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 923222ebed..e0e253d4f4 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -108,6 +108,7 @@ pub trait RuntimeValueOps { owner_kind: u64, reflected_name: &str, attrs: RuntimeCellHandle, + flags: u64, ) -> Result; /// Creates a named runtime object without constructor arguments. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index d49a2c7ef7..2cbbbe7119 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -202,6 +202,37 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass exposes eval class-like final and abstract flags. +#[test] +fn execute_program_reflects_eval_class_modifier_flags() { + let program = parse_fragment( + br#"abstract class EvalAbstractReflect {} +final class EvalFinalReflect {} +interface EvalIfaceReflect {} +trait EvalTraitReflect {} +enum EvalEnumReflect { case Ready; } +echo (new ReflectionClass("EvalAbstractReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalFinalReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalEnumReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalIfaceReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalTraitReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Af:aF:aF:af:af"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. #[test] fn execute_program_reflects_eval_constant_and_enum_case_attributes() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 48f8ea0831..9a30b30263 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -102,6 +102,14 @@ impl FakeOps { (FakeValue::Object(properties), "getname") if args.is_empty() => { Self::object_property(&properties, "__name").map_or_else(|| self.string(""), Ok) } + (FakeValue::Object(properties), "isfinal") if args.is_empty() => { + Self::object_property(&properties, "__is_final") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isabstract") if args.is_empty() => { + Self::object_property(&properties, "__is_abstract") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getarguments") if args.is_empty() => { Self::object_property(&properties, "__args") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -217,6 +225,7 @@ impl FakeOps { owner_kind: u64, reflected_name: &str, attrs: RuntimeCellHandle, + flags: u64, ) -> Result { let class_name = match owner_kind { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", @@ -228,10 +237,14 @@ impl FakeOps { _ => return Err(EvalStatus::RuntimeFatal), }; let name = self.string(reflected_name)?; - let object = self.alloc(FakeValue::Object(vec![ - ("__name".to_string(), name), - ("__attrs".to_string(), attrs), - ])); + let is_final = self.bool_value((flags & 1) != 0)?; + let is_abstract = self.bool_value((flags & 2) != 0)?; + let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; + if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); + } + let object = self.alloc(FakeValue::Object(properties)); self.object_classes .insert(object.as_ptr() as usize, class_name.to_string()); Ok(object) diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 69469f41a5..1995acb4f9 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -115,8 +115,9 @@ impl RuntimeValueOps for FakeOps { owner_kind: u64, reflected_name: &str, attrs: RuntimeCellHandle, + flags: u64, ) -> Result { - self.runtime_reflection_owner_new(owner_kind, reflected_name, attrs) + self.runtime_reflection_owner_new(owner_kind, reflected_name, attrs, flags) } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 6e6ba07f45..8a1502553e 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -73,6 +73,7 @@ unsafe extern "C" { name_ptr: *const u8, name_len: u64, attrs: *mut RuntimeCell, + flags: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 8bf4dd4f00..436a1cedda 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -184,6 +184,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { owner_kind: u64, reflected_name: &str, attrs: RuntimeCellHandle, + flags: u64, ) -> Result { Self::handle(unsafe { __elephc_eval_reflection_owner_new( @@ -191,6 +192,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { reflected_name.as_ptr(), reflected_name.len() as u64, attrs.as_ptr(), + flags, ) }) } diff --git a/docs/php/eval.md b/docs/php/eval.md index f541130f39..aae261852d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -151,7 +151,10 @@ syntax, but requesting those arguments is a runtime fatal. `ReflectionProperty::getAttributes()` expose eval-retained class, method, and property attributes for eval-declared class-like symbols when their arguments fit the same literal subset, and `getName()` returns the reflected class or -member name for those owners. `ReflectionClassConstant::getAttributes()`, +member name for those owners. `ReflectionClass::isFinal()` and +`ReflectionClass::isAbstract()` report eval class-like modifier metadata, +including PHP-compatible enum finality and false values for eval interfaces and +traits. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant and enum-case attributes through the same materialized `ReflectionAttribute` diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index edc87626bb..aac0ace8ea 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -28,6 +28,10 @@ struct ReflectionOwnerLayout { name_hi: Option, attrs_lo: usize, attrs_hi: usize, + is_final_lo: Option, + is_final_hi: Option, + is_abstract_lo: Option, + is_abstract_hi: Option, } /// Layouts for the Reflection owner classes eval can materialize. @@ -114,6 +118,8 @@ fn reflection_owner_layout( ) -> Option { let attrs_lo = reflection_property_offset(info, "__attrs")?; let name_lo = has_name.then(|| reflection_property_offset(info, "__name")).flatten(); + let is_final_lo = reflection_property_offset(info, "__is_final"); + let is_abstract_lo = reflection_property_offset(info, "__is_abstract"); Some(ReflectionOwnerLayout { class_id: info.class_id, property_count: info.properties.len(), @@ -121,6 +127,10 @@ fn reflection_owner_layout( name_hi: name_lo.map(|offset| offset + 8), attrs_lo, attrs_hi: attrs_lo + 8, + is_final_lo, + is_final_hi: is_final_lo.map(|offset| offset + 8), + is_abstract_lo, + is_abstract_hi: is_abstract_lo.map(|offset| offset + 8), }) } @@ -161,6 +171,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array + emitter.instruction("str x4, [sp, #48]"); // save ReflectionClass modifier flags emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod @@ -212,6 +223,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array + emitter.instruction("mov QWORD PTR [rbp - 56], r8"); // save ReflectionClass modifier flags emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod @@ -260,6 +272,7 @@ fn emit_aarch64_owner_kind_body( if set_name { emit_set_owner_name_property_aarch64(emitter, layout); } + emit_set_owner_class_flags_property_aarch64(emitter, layout); emit_set_owner_attrs_property_aarch64(emitter, layout, fail_label); emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object } @@ -279,6 +292,7 @@ fn emit_x86_64_owner_kind_body( if set_name { emit_set_owner_name_property_x86_64(emitter, layout); } + emit_set_owner_class_flags_property_x86_64(emitter, layout); emit_set_owner_attrs_property_x86_64(emitter, layout, fail_label); emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object } @@ -353,6 +367,64 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio abi::emit_store_to_address(emitter, "rdx", "r10", name_hi); } +/// Stores incoming ARM64 ReflectionClass boolean modifier flags. +fn emit_set_owner_class_flags_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let Some(is_final_lo) = layout.is_final_lo else { + return; + }; + let Some(is_final_hi) = layout.is_final_hi else { + return; + }; + let Some(is_abstract_lo) = layout.is_abstract_lo else { + return; + }; + let Some(is_abstract_hi) = layout.is_abstract_hi else { + return; + }; + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); + emitter.instruction("lsr x10, x11, #1"); // move the abstract-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); +} + +/// Stores incoming x86_64 ReflectionClass boolean modifier flags. +fn emit_set_owner_class_flags_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let Some(is_final_lo) = layout.is_final_lo else { + return; + }; + let Some(is_final_hi) = layout.is_final_hi else { + return; + }; + let Some(is_abstract_lo) = layout.is_abstract_lo else { + return; + }; + let Some(is_abstract_hi) = layout.is_abstract_hi else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit + emitter.instruction("and rax, 1"); // extract the final-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit + emitter.instruction("shr rax, 1"); // move the abstract-class bit into position + emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); +} + /// Stores a retained ARM64 attribute-array payload into the owner private slot. fn emit_set_owner_attrs_property_aarch64( emitter: &mut Emitter, diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 6e2d99395c..bf57a27d61 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -25,6 +25,8 @@ struct ReflectionOwnerMetadata { reflected_name: Option, attr_names: Vec, attr_args: Vec>>, + is_final: bool, + is_abstract: bool, } /// Returns true for reflection owner classes that need metadata-aware construction. @@ -76,6 +78,10 @@ pub(super) fn lower_reflection_owner_new( &metadata.attr_names, &metadata.attr_args, )?; + if class_name == "ReflectionClass" { + emit_reflection_bool_property(ctx, "__is_final", metadata.is_final)?; + emit_reflection_bool_property(ctx, "__is_abstract", metadata.is_abstract)?; + } let result = inst .result .ok_or_else(|| CodegenIrError::invalid_module("reflection object_new missing result"))?; @@ -478,6 +484,8 @@ fn reflection_class_metadata( reflected_name: Some(class_name.to_string()), attr_names: info.attribute_names.clone(), attr_args: info.attribute_args.clone(), + is_final: info.is_final, + is_abstract: info.is_abstract, }) .unwrap_or_else(empty_reflection_metadata)) } @@ -502,6 +510,8 @@ fn reflection_method_metadata( reflected_name: Some(method_name.clone()), attr_names: info.method_attribute_names.get(&method_key)?.clone(), attr_args: info.method_attribute_args.get(&method_key)?.clone(), + is_final: false, + is_abstract: false, }) }) .unwrap_or_else(empty_reflection_metadata)) @@ -526,6 +536,8 @@ fn reflection_property_metadata( reflected_name: Some(property_name.clone()), attr_names: info.property_attribute_names.get(&property_name)?.clone(), attr_args: info.property_attribute_args.get(&property_name)?.clone(), + is_final: false, + is_abstract: false, }) }) .unwrap_or_else(empty_reflection_metadata)) @@ -551,6 +563,8 @@ fn reflection_class_constant_metadata( reflected_name: Some(constant_name), attr_names: case.attribute_names.clone(), attr_args: case.attribute_args.clone(), + is_final: false, + is_abstract: false, }); } Ok(resolve_reflection_class_constant(ctx, &reflected_class, &constant_name) @@ -569,6 +583,8 @@ fn reflection_class_constant_metadata( reflected_name: Some(constant_name), attr_names, attr_args, + is_final: false, + is_abstract: false, } }) .unwrap_or_else(empty_reflection_metadata)) @@ -593,6 +609,8 @@ fn reflection_enum_case_metadata( reflected_name: Some(case_name), attr_names: case.attribute_names.clone(), attr_args: case.attribute_args.clone(), + is_final: false, + is_abstract: false, }) .unwrap_or_else(empty_reflection_metadata)) } @@ -644,6 +662,8 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { reflected_name: None, attr_names: Vec::new(), attr_args: Vec::new(), + is_final: false, + is_abstract: false, } } @@ -778,6 +798,32 @@ fn emit_reflection_attrs_property( Ok(()) } +/// Stores one boolean property on the current ReflectionClass object result. +fn emit_reflection_bool_property( + ctx: &mut FunctionContext<'_>, + property_name: &str, + value: bool, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + emit_reflection_int_property(ctx, i64::from(value), low_offset, low_offset + 8); + Ok(()) +} + +/// Returns one declared property offset from a synthetic Reflection class layout. +fn reflection_property_offset(info: &crate::types::ClassInfo, property: &str) -> Result { + info.property_offsets.get(property).copied().ok_or_else(|| { + CodegenIrError::invalid_module(format!( + "Reflection owner missing property offset for ${}", + property + )) + }) +} + /// Returns the low/high object offsets for the private `__attrs` slot. fn reflection_attrs_offsets(class_name: &str) -> (usize, usize) { if reflection_owner_has_name(class_name) { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 2b65cb1888..21f592b239 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -193,6 +193,14 @@ fn empty_array() -> Option { )) } +/// Returns a `BoolLiteral(false)` expression. +fn false_bool() -> Option { + Some(Expr::new( + ExprKind::BoolLiteral(false), + crate::span::Span::dummy(), + )) +} + /// Returns an `IntLiteral` expression with the given value. fn int_lit(value: i64) -> Option { Some(Expr::new( @@ -224,6 +232,11 @@ fn mixed_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("mixed")) } +/// Returns a `TypeExpr` for PHP's builtin boolean type. +fn bool_type() -> TypeExpr { + TypeExpr::Bool +} + /// Returns a private parameterless `__construct` method for `ReflectionAttribute`. fn builtin_reflection_attribute_constructor_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -536,6 +549,18 @@ fn builtin_reflection_class() -> FlattenedClass { Some(array_type()), empty_array(), ), + builtin_property( + "__is_final", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_abstract", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![( @@ -545,6 +570,8 @@ fn builtin_reflection_class() -> FlattenedClass { false, )]), builtin_reflection_class_get_name_method(), + builtin_reflection_class_bool_method("isFinal", "__is_final"), + builtin_reflection_class_bool_method("isAbstract", "__is_abstract"), builtin_reflection_owner_get_attributes_method(), ], attributes: Vec::new(), @@ -584,6 +611,35 @@ fn builtin_reflection_class_get_name_method() -> ClassMethod { } } +/// Returns a public `ReflectionClass` boolean method backed by one private slot. +fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds a `FlattenedClass` for `ReflectionMethod` or `ReflectionProperty` /// with a private `__attrs` array property and two methods: `__construct` /// (public, accepting the supplied params) and `getAttributes` (public, @@ -737,6 +793,13 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Str; } } + if class_name == "ReflectionClass" { + for method_name in ["isfinal", "isabstract"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionAttribute".to_string()))); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1047f375e0..d878d13cf0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5394,6 +5394,36 @@ echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance ); } +/// Verifies eval ReflectionClass reports class-like final and abstract flags. +#[test] +fn test_eval_reflection_class_modifier_flags() { + let out = compile_and_run_capture( + r#"isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalFinalReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalEnumReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalIfaceReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalTraitReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Af:aF:aF:af:af"); +} + /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. #[test] fn test_eval_reflection_constant_and_enum_case_attributes() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index ee4b80c9c3..cfda9315ed 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1102,6 +1102,27 @@ echo $ref->getName(); assert_eq!(out, "Plain"); } +/// Verifies that `ReflectionClass` reports final and abstract flags for static metadata. +#[test] +fn test_reflection_class_reports_modifier_flags() { + let out = compile_and_run( + r#"isAbstract() ? "A" : "a"; +echo (new ReflectionClass(StaticAbstractReflect::class))->isFinal() ? "F" : "f"; +echo ":"; +echo (new ReflectionClass(StaticFinalReflect::class))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass(StaticFinalReflect::class))->isFinal() ? "F" : "f"; +echo ":"; +echo (new ReflectionClass(StaticEnumReflect::class))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass(StaticEnumReflect::class))->isFinal() ? "F" : "f"; +"#, + ); + assert_eq!(out, "Af:aF:aF"); +} + /// Verifies that `ReflectionClass::getName()` returns the canonical declared /// name after case-insensitive class-string construction. #[test] From 660393fc756c5720f8eb7b71a5c928ed391cedce Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 20:51:44 +0200 Subject: [PATCH 0352/1208] Resolve ReflectionClass interface and trait names --- docs/internals/the-runtime.md | 2 +- docs/php/classes.md | 8 +-- src/codegen/lower_inst/objects/reflection.rs | 50 +++++++++++++++++-- src/types/checker/driver/init.rs | 3 +- src/types/checker/driver/mod.rs | 43 +++++++++++----- .../checker/inference/objects/constructors.rs | 4 +- .../objects/constructors/reflection.rs | 33 ++++++------ src/types/checker/mod.rs | 14 ++++-- tests/codegen/oop/attributes.rs | 14 +++++- 9 files changed, 128 insertions(+), 43 deletions(-) diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index d71f7f91b7..2ecbd43398 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -955,7 +955,7 @@ Additionally, the runtime emits static data tables: - `_instanceof_target_count`, `_instanceof_target_entries`, `_instanceof_name_*` — case-insensitive class/interface name metadata used by dynamic `instanceof` string targets, including leading-backslash aliases - `_class_gc_desc_count`, `_class_gc_desc_ptrs`, `_class_gc_desc_` — per-class property traversal metadata used by object deep-free and cycle collection - `_class_json_desc_ptrs`, `_class_json_desc_`, `_class_json_pname__`, `_json_exception_class_id`, `_stdclass_class_id` — JSON object encoding descriptors, JsonException construction metadata, and stdClass runtime class id -- `_class_attribute_count`, `_class_attribute_ptrs`, `_class_attributes_` — emitted class-level PHP attribute metadata. The current PHP-facing helpers and Reflection owner constructors materialize their results from the same `ClassInfo` metadata during codegen, rather than doing dynamic runtime class/member lookup. +- `_class_attribute_count`, `_class_attribute_ptrs`, `_class_attributes_` — emitted class-level PHP attribute metadata. The current PHP-facing helpers and class/enum Reflection attribute constructors materialize their results from the same `ClassInfo` metadata during codegen, rather than doing dynamic runtime class/member lookup. - `_class_vtable_ptrs`, `_class_vtable_` — per-class virtual-method tables used by inheritance dispatch through `class_id` - `_class_static_vtable_ptrs`, `_class_static_vtable_` — per-class static-method tables used by late static binding - `_class_destruct_ptrs` — class_id-indexed `__destruct` method pointers (or `0`) consulted by `__rt_call_object_destructor` during object deep-free diff --git a/docs/php/classes.md b/docs/php/classes.md index 167732dae9..171710d848 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1080,7 +1080,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | Reflection method | Supported constructor | Description | |---|---|---| -| `ReflectionClass::getName()` | `new ReflectionClass($class_name)` | Return the resolved class name | +| `ReflectionClass::getName()` | `new ReflectionClass($class_name)` | Return the resolved class-like name | +| `ReflectionClass::isFinal()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is final | +| `ReflectionClass::isAbstract()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is abstract | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | @@ -1137,12 +1139,12 @@ name with `isBuiltin()` false. | `ReflectionNamedType::allowsNull()` | `bool` | True when the declared type is nullable | Limitations today: -- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Method/Property(...)` must be compile-time class/member strings. `ClassName::class` is accepted for the class-name argument of `new ReflectionClass/Method/Property(...)`, and normal named-argument / static associative-spread normalization runs before the literal-string check. Dynamic class, method, property, or attribute names require a runtime name→id lookup table that is not yet implemented. +- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Method/Property(...)` must be compile-time class/member strings. `ClassName::class` is accepted for the class-name argument of `new ReflectionClass/Method/Property(...)`, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, or attribute names require a runtime name→id lookup table that is not yet implemented. - Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** — a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` → `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()` and `getAttributes()`. `ReflectionMethod` and `ReflectionProperty` currently support `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getAttributes()`, `isFinal()`, and `isAbstract()`. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()`; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index bf57a27d61..e25120bb3c 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -479,15 +479,22 @@ fn reflection_class_metadata( return Ok(empty_reflection_metadata()); }; let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionClass")?; - Ok(resolve_reflection_class(ctx, &reflected_class) - .map(|(class_name, info)| ReflectionOwnerMetadata { + if let Some((class_name, info)) = resolve_reflection_class(ctx, &reflected_class) { + return Ok(ReflectionOwnerMetadata { reflected_name: Some(class_name.to_string()), attr_names: info.attribute_names.clone(), attr_args: info.attribute_args.clone(), is_final: info.is_final, is_abstract: info.is_abstract, - }) - .unwrap_or_else(empty_reflection_metadata)) + }); + } + if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { + return Ok(class_like_reflection_metadata(interface_name)); + } + if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { + return Ok(class_like_reflection_metadata(trait_name)); + } + Ok(empty_reflection_metadata()) } /// Resolves `ReflectionMethod(class, method)` metadata. @@ -628,6 +635,41 @@ fn resolve_reflection_class<'a>( .map(|(name, info)| (name.as_str(), info)) } +/// Looks up interface metadata by PHP-style case-insensitive name. +fn resolve_reflection_interface<'a>( + ctx: &'a FunctionContext<'_>, + interface_name: &str, +) -> Option<&'a str> { + let interface_key = php_symbol_key(interface_name.trim_start_matches('\\')); + ctx.module + .interface_infos + .keys() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == interface_key) + .map(String::as_str) +} + +/// Looks up a declared trait by PHP-style case-insensitive name. +fn resolve_reflection_trait<'a>(ctx: &'a FunctionContext<'_>, trait_name: &str) -> Option<&'a str> { + let trait_key = php_symbol_key(trait_name.trim_start_matches('\\')); + ctx.module + .trait_table + .names + .iter() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == trait_key) + .map(String::as_str) +} + +/// Builds empty ReflectionClass metadata for class-like symbols without stored attributes. +fn class_like_reflection_metadata(class_like_name: &str) -> ReflectionOwnerMetadata { + ReflectionOwnerMetadata { + reflected_name: Some(class_like_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + is_final: false, + is_abstract: false, + } +} + /// Looks up class-constant metadata by PHP-style class name and case-sensitive constant name. fn resolve_reflection_class_constant<'a>( ctx: &'a FunctionContext<'_>, diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index d78d61d9f4..9b4beb648a 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -15,8 +15,8 @@ use crate::types::array_constants::ARRAY_INT_CONSTANTS; use crate::types::date_constants::DATE_INT_CONSTANTS; use crate::types::ent_constants::ENT_INT_CONSTANTS; use crate::types::json_constants::JSON_INT_CONSTANTS; -use crate::types::stream_constants::STREAM_INT_CONSTANTS; use crate::types::preg_constants::PREG_INT_CONSTANTS; +use crate::types::stream_constants::STREAM_INT_CONSTANTS; use crate::types::PhpType; use super::super::Checker; @@ -96,6 +96,7 @@ impl Checker { declared_classes: HashSet::new(), enums: HashMap::new(), declared_interfaces: HashSet::new(), + declared_traits: HashSet::new(), current_class: None, current_method: None, current_method_is_static: false, diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 194084619b..d89766599c 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -27,9 +27,7 @@ use super::builtin_types::{ patch_magic_method_signatures, InterfaceDeclInfo, }; use super::builtin_enums::inject_builtin_enums; -use super::builtin_interfaces::{ - apply_implicit_stringable_interfaces, inject_builtin_interfaces, -}; +use super::builtin_interfaces::{apply_implicit_stringable_interfaces, inject_builtin_interfaces}; use super::builtin_iterators::{inject_builtin_iterators, patch_builtin_generator_signatures}; use super::builtin_json::{inject_builtin_json_interfaces, patch_builtin_json_signatures}; use super::builtin_spl_classes::{ @@ -84,13 +82,7 @@ pub(super) fn check_types_impl( // single pass feeds the schema signatures, the body-check pass, and codegen (which all read // the flattened method/property declarations), so no later stage sees a symbolic `self`. substitute_relative_class_types_in_flattened(&mut flattened_classes); - let declared_traits: HashSet = program - .iter() - .filter_map(|stmt| match &stmt.kind { - StmtKind::TraitDecl { name, .. } => Some(name.clone()), - _ => None, - }) - .collect(); + let declared_traits = collect_declared_trait_names(program); let mut seen_classes = HashSet::new(); let mut class_map = HashMap::new(); for class in &flattened_classes { @@ -181,13 +173,13 @@ pub(super) fn check_types_impl( if let Err(error) = inject_builtin_user_filter(&mut class_map) { errors.extend(error.flatten()); } - if let Err(error) = - inject_builtin_reflection(&interface_map, &mut class_map, &declared_traits) + if let Err(error) = inject_builtin_reflection(&interface_map, &mut class_map, &declared_traits) { errors.extend(error.flatten()); } checker.declared_classes = class_map.keys().cloned().collect(); checker.declared_interfaces = interface_map.keys().cloned().collect(); + checker.declared_traits = declared_traits.clone(); // Enum names must resolve as types in member positions (property and // promoted-constructor-param types), which are checked during the class // schema pass — before the enum-processing phase populates `enums`. Pre- @@ -310,6 +302,28 @@ pub(super) fn check_types_impl( Ok((checker, final_global_env)) } +/// Collects source-declared trait names recursively, including namespace blocks. +fn collect_declared_trait_names(program: &Program) -> HashSet { + let mut names = HashSet::new(); + collect_declared_trait_names_into(program, &mut names); + names +} + +/// Pushes recursive source-declared trait names into `names`. +fn collect_declared_trait_names_into(program: &Program, names: &mut HashSet) { + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { name, .. } => { + names.insert(name.clone()); + } + StmtKind::NamespaceBlock { body, .. } => { + collect_declared_trait_names_into(body, names); + } + _ => {} + } + } +} + /// Resolves the relative class types `self`/`static`/`parent` to concrete class names across /// every flattened class's method parameter, method return, and property type annotations. /// @@ -336,7 +350,10 @@ fn flatten_enum_methods(program: &[Stmt]) -> Vec { let mut flattened = FlattenedClass { name: name.clone(), extends: None, - implements: implements.iter().map(|name| name.as_str().to_string()).collect(), + implements: implements + .iter() + .map(|name| name.as_str().to_string()) + .collect(), is_abstract: false, is_final: true, is_readonly_class: false, diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index acf58a811a..20ab8e3bb6 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -424,11 +424,13 @@ impl Checker { /// Looks up a class name by PHP case-insensitive symbol key. /// /// Strips leading backslashes and uses `php_symbol_key` for comparison. - /// Returns the canonical class name string if found. + /// Returns the canonical class-like name string if found. fn resolve_reflection_class_name<'a>(&'a self, class_name: &str) -> Option<&'a str> { let class_key = php_symbol_key(class_name.trim_start_matches('\\')); self.classes .keys() + .chain(self.interfaces.keys()) + .chain(self.declared_traits.iter()) .find(|existing| php_symbol_key(existing) == class_key) .map(String::as_str) } diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index 63a6b6bd25..6d09b4ae28 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -27,21 +27,24 @@ impl Checker { class_name: &str, expr: &Expr, ) -> Result<(), CompileError> { - let Some(class_info) = self.classes.get(class_name) else { - return Err(CompileError::new( - expr.span, - &format!( - "ReflectionClass::__construct(): undefined class '{}'", - class_name - ), - )); - }; - self.validate_reflection_attribute_metadata( - &class_info.attribute_names, - &class_info.attribute_args, - expr, - "ReflectionClass::getAttributes(): class has attribute argument metadata that is not supported yet", - ) + if let Some(class_info) = self.classes.get(class_name) { + return self.validate_reflection_attribute_metadata( + &class_info.attribute_names, + &class_info.attribute_args, + expr, + "ReflectionClass::getAttributes(): class has attribute argument metadata that is not supported yet", + ); + } + if self.interfaces.contains_key(class_name) || self.declared_traits.contains(class_name) { + return Ok(()); + } + Err(CompileError::new( + expr.span, + &format!( + "ReflectionClass::__construct(): undefined class '{}'", + class_name + ), + )) } /// Validates method-level attributes for `ReflectionMethod`. diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index a8c98ef019..6b81abc2fa 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -8,7 +8,6 @@ //! Key details: //! - Checker state is populated in ordered phases; later passes assume schemas, builtins, and signatures are complete. -pub(crate) mod builtins; mod builtin_enums; mod builtin_interfaces; mod builtin_iterators; @@ -19,9 +18,8 @@ mod builtin_spl_exceptions; pub(crate) mod builtin_stdclass; mod builtin_types; mod builtin_user_filter; +pub(crate) mod builtins; mod callables; -/// yield_validation -pub(crate) mod yield_validation; mod driver; mod extern_decl; mod functions; @@ -30,6 +28,8 @@ mod method_pass; mod schema; mod stmt_check; mod type_compat; +/// yield_validation +pub(crate) mod yield_validation; use std::collections::{HashMap, HashSet}; @@ -109,6 +109,9 @@ pub(crate) struct Checker { /// Canonical interface names declared in the program, available for forward references /// before the full interface definitions are available. pub declared_interfaces: HashSet, + /// Canonical trait names declared in the program, available for reflection + /// and class-like metadata probes that accept traits. + pub declared_traits: HashSet, /// Name of the class currently being type-checked (used for `$this` resolution). pub current_class: Option, /// Name of the current method being type-checked, when inside a class body. @@ -200,7 +203,10 @@ pub(crate) struct FnDecl { /// a `CheckResult` on success or a `CompileError` on failure. The checker validates /// types, resolves declarations, infers return types, and collects warnings. Abstract /// return types are propagated from concrete implementations before returning. -pub fn check_types(program: &Program, target_platform: Platform) -> Result { +pub fn check_types( + program: &Program, + target_platform: Platform, +) -> Result { let (mut checker, global_env) = driver::check_types_impl(program, target_platform)?; propagate_abstract_return_types(&mut checker); diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index cfda9315ed..8279e05cf3 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1109,6 +1109,8 @@ fn test_reflection_class_reports_modifier_flags() { r#"isAbstract() ? "A" : "a"; echo (new ReflectionClass(StaticAbstractReflect::class))->isFinal() ? "F" : "f"; @@ -1118,9 +1120,19 @@ echo (new ReflectionClass(StaticFinalReflect::class))->isFinal() ? "F" : "f"; echo ":"; echo (new ReflectionClass(StaticEnumReflect::class))->isAbstract() ? "A" : "a"; echo (new ReflectionClass(StaticEnumReflect::class))->isFinal() ? "F" : "f"; +echo ":"; +$iface = new ReflectionClass("staticifacereflect"); +echo $iface->getName() . "/"; +echo $iface->isAbstract() ? "A" : "a"; +echo $iface->isFinal() ? "F" : "f"; +echo ":"; +$trait = new ReflectionClass("STATICTRAITREFLECT"); +echo $trait->getName() . "/"; +echo $trait->isAbstract() ? "A" : "a"; +echo $trait->isFinal() ? "F" : "f"; "#, ); - assert_eq!(out, "Af:aF:aF"); + assert_eq!(out, "Af:aF:aF:StaticIfaceReflect/af:StaticTraitReflect/af"); } /// Verifies that `ReflectionClass::getName()` returns the canonical declared From bddd567ff674c9ee856c1bbfb5874bbcccb131f4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 21:05:17 +0200 Subject: [PATCH 0353/1208] Expand eval AOT method bridge --- docs/php/eval.md | 14 ++--- src/codegen/eval_constructor_helpers.rs | 59 +++++++++++++------- src/codegen/eval_method_helpers.rs | 62 +++++++++++++++++---- tests/codegen/eval.rs | 72 ++++++++++++++++++++++++- 4 files changed, 169 insertions(+), 38 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index aae261852d..eb7933f131 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -73,8 +73,8 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata use positional constructor arguments. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method and static-method fallback remains positional. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata use fixed positional constructor arguments for scalar/Mixed signatures that fit the target ABI register bridge. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method and static-method fallback remains fixed-arity and positional for public scalar/Mixed signatures that fit the target ABI register bridge. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -121,7 +121,8 @@ positional arguments. Static method callables can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method -fallback remains positional. +fallback remains fixed-arity and positional for public scalar/Mixed signatures +that fit the target ABI register bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -308,13 +309,14 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are still outside eval fragments. Runtime/AOT object-method and static-method -fallback from eval remains positional, so named method arguments are supported -for eval-declared methods but not for every generated native method bridge. +fallback from eval remains fixed-arity and positional, so named method arguments +are supported for eval-declared methods but not for every generated native method +bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported attribute/getName slice and broader generated/AOT method bridge signatures -beyond the current public scalar zero-to-two-argument slice. +beyond the current public scalar/Mixed fixed-arity register slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 65ca4c98ed..abbb2432b9 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -9,18 +9,20 @@ //! - The cacheable runtime object can allocate by name, but only user assembly //! knows constructor symbols and parameter ABI shapes. //! - Classes without constructors are treated as successful no-ops, matching PHP. +//! - Constructors are bridged only when their fixed scalar/Mixed arguments fit +//! the target ABI register bridge. use std::collections::BTreeMap; use crate::codegen::abi; use crate::codegen::emit::Emitter; -use crate::codegen::platform::Arch; +use crate::codegen::platform::{Arch, Target}; use crate::ir::{Function, LocalKind, Module}; use crate::names::{method_symbol, php_symbol_key}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, FunctionSig, PhpType}; -const MAX_EVAL_CONSTRUCTOR_ARGS: usize = 2; +const MAX_EVAL_CONSTRUCTOR_ARGS: usize = 8; const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ "Error", "TypeError", @@ -83,15 +85,12 @@ fn all_module_functions(module: &Module) -> impl Iterator { /// Returns true when a function has hidden eval state locals. fn function_uses_eval(function: &Function) -> bool { - function - .locals - .iter() - .any(|local| { - matches!( - local.kind, - LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope - ) - }) + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) } /// Collects AOT constructors backed by emitted EIR symbols in stable class-id order. @@ -101,7 +100,13 @@ fn collect_eval_constructor_slots(module: &Module) -> Vec { let mut classes = module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_constructor_slot(class_name, class_info, &emitted_methods, &mut slots); + collect_class_constructor_slot( + module.target, + class_name, + class_info, + &emitted_methods, + &mut slots, + ); } slots } @@ -120,6 +125,7 @@ fn collect_builtin_throwable_constructor_class_ids(module: &Module) -> Vec /// Adds one constructor slot for a class when the constructor has emitted code. fn collect_class_constructor_slot( + target: Target, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -138,12 +144,9 @@ fn collect_class_constructor_slot( return; } let supported = constructor_is_public(class_info, &method_key) - && constructor_signature_supported(sig); + && constructor_signature_supported(target, class_name, sig); let params = if supported { - sig.params - .iter() - .map(|(_, ty)| ty.codegen_repr()) - .collect() + sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect() } else { Vec::new() }; @@ -165,7 +168,7 @@ fn constructor_is_public(class_info: &ClassInfo, method_key: &str) -> bool { } /// Returns true for constructor signatures supported by this eval bridge slice. -fn constructor_signature_supported(sig: &FunctionSig) -> bool { +fn constructor_signature_supported(target: Target, class_name: &str, sig: &FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_CONSTRUCTOR_ARGS && sig.variadic.is_none() && sig.ref_params.iter().all(|is_ref| !*is_ref) @@ -173,6 +176,7 @@ fn constructor_signature_supported(sig: &FunctionSig) -> bool { .params .iter() .all(|(_, ty)| constructor_param_supported(ty)) + && eval_bridge_constructor_signature_fits_registers(target, class_name, sig) } /// Returns true for one constructor argument type supported by the bridge. @@ -183,6 +187,20 @@ fn constructor_param_supported(ty: &PhpType) -> bool { ) } +/// Returns true when this helper can materialize the complete constructor call in registers. +fn eval_bridge_constructor_signature_fits_registers( + target: Target, + class_name: &str, + sig: &FunctionSig, +) -> bool { + let mut arg_types = Vec::with_capacity(sig.params.len() + 1); + arg_types.push(PhpType::Object(class_name.to_string())); + arg_types.extend(sig.params.iter().map(|(_, ty)| ty.codegen_repr())); + abi::build_outgoing_arg_assignments_for_target(target, &arg_types, 0) + .iter() + .all(|assignment| assignment.in_register()) +} + /// Emits `__elephc_eval_value_construct_object(Mixed*, MixedArray*) -> bool`. fn emit_constructor_helper( module: &Module, @@ -661,7 +679,10 @@ fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { fn grouped_slots(slots: &[EvalConstructorSlot]) -> BTreeMap> { let mut grouped = BTreeMap::new(); for slot in slots { - grouped.entry(slot.class_id).or_insert_with(Vec::new).push(slot); + grouped + .entry(slot.class_id) + .or_insert_with(Vec::new) + .push(slot); } grouped } diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 6cac491783..fc5e6f2a82 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -8,8 +8,9 @@ //! Key details: //! - The cacheable runtime object cannot know user class ids, method symbols, //! or return types, so this bridge is emitted into the user assembly. -//! - This method-call slice supports public AOT methods with zero, one, or two -//! non-by-ref scalar arguments and reports unsupported calls as runtime failure. +//! - This method-call slice supports public AOT methods with fixed non-by-ref +//! scalar/Mixed argument lists that fit the target ABI register bridge, and +//! reports unsupported calls as runtime failure. use std::collections::BTreeMap; @@ -17,7 +18,7 @@ use crate::codegen::abi; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::emit_box_current_value_as_mixed; -use crate::codegen::platform::Arch; +use crate::codegen::platform::{Arch, Target}; use crate::ir::{Function, LocalKind, Module}; use crate::names::{method_symbol, static_method_symbol}; use crate::parser::ast::Visibility; @@ -45,7 +46,7 @@ struct EvalStaticMethodSlot { return_ty: PhpType, } -const MAX_EVAL_METHOD_ARGS: usize = 2; +const MAX_EVAL_METHOD_ARGS: usize = 8; const BUILTIN_THROWABLE_METHOD_CLASSES: &[&str] = &[ "Error", "TypeError", @@ -121,7 +122,13 @@ fn collect_eval_method_slots(module: &Module) -> Vec { let mut classes = module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_method_slots(class_name, class_info, &emitted_methods, &mut slots); + collect_class_method_slots( + module.target, + class_name, + class_info, + &emitted_methods, + &mut slots, + ); } slots } @@ -133,7 +140,13 @@ fn collect_eval_static_method_slots(module: &Module) -> Vec>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_static_method_slots(class_name, class_info, &emitted_methods, &mut slots); + collect_class_static_method_slots( + module.target, + class_name, + class_info, + &emitted_methods, + &mut slots, + ); } slots } @@ -152,6 +165,7 @@ fn collect_builtin_throwable_method_class_ids(module: &Module) -> Vec { /// Adds bridge-supported public methods for one class. fn collect_class_method_slots( + target: Target, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -161,7 +175,7 @@ fn collect_class_method_slots( methods.sort_by_key(|(method, _)| method.as_str()); for (method, sig) in methods { if !method_is_public(class_info, method) - || !method_signature_supported(sig) + || !method_signature_supported(sig, target, PhpType::Object(class_name.to_string())) || !method_return_supported(&sig.return_type) { continue; @@ -187,6 +201,7 @@ fn collect_class_method_slots( /// Adds bridge-supported public static methods for one class. fn collect_class_static_method_slots( + target: Target, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -196,7 +211,7 @@ fn collect_class_static_method_slots( methods.sort_by_key(|(method, _)| method.as_str()); for (method, sig) in methods { if !static_method_is_public(class_info, method) - || !method_signature_supported(sig) + || !method_signature_supported(sig, target, PhpType::Int) || !method_return_supported(&sig.return_type) { continue; @@ -236,22 +251,41 @@ fn static_method_is_public(class_info: &ClassInfo, method: &str) -> bool { .is_none_or(|visibility| matches!(visibility, Visibility::Public)) } -/// Returns true for method signatures supported by this first eval bridge slice. -fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { +/// Returns true for method signatures supported by the eval bridge. +fn method_signature_supported( + sig: &crate::types::FunctionSig, + target: Target, + hidden_arg_ty: PhpType, +) -> bool { sig.params.len() <= MAX_EVAL_METHOD_ARGS && sig.variadic.is_none() && sig.ref_params.iter().all(|is_ref| !*is_ref) && sig.params.iter().all(|(_, ty)| method_param_supported(ty)) + && eval_bridge_signature_fits_registers(target, hidden_arg_ty, &sig.params) } /// Returns true for an eval-supplied method argument type supported by this bridge. fn method_param_supported(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), - PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str | PhpType::Mixed ) } +/// Returns true when this helper can materialize the complete call without stack args. +fn eval_bridge_signature_fits_registers( + target: Target, + hidden_arg_ty: PhpType, + params: &[(String, PhpType)], +) -> bool { + let mut arg_types = Vec::with_capacity(params.len() + 1); + arg_types.push(hidden_arg_ty.codegen_repr()); + arg_types.extend(params.iter().map(|(_, ty)| ty.codegen_repr())); + abi::build_outgoing_arg_assignments_for_target(target, &arg_types, 0) + .iter() + .all(|assignment| assignment.in_register()) +} + /// Returns true for return storage shapes the bridge can box for eval. fn method_return_supported(ty: &PhpType) -> bool { matches!( @@ -1083,6 +1117,9 @@ fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 } + PhpType::Mixed => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for a Mixed method parameter + } _ => {} } } @@ -1106,6 +1143,9 @@ fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair } + PhpType::Mixed => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for a Mixed method parameter + } _ => {} } } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d878d13cf0..5cbe09b255 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4374,6 +4374,52 @@ $box->run(); assert_eq!(out, "50!"); } +/// Verifies eval fragments pass more than two fixed scalar arguments to public AOT methods. +#[test] +fn test_eval_fragment_can_call_this_public_many_arg_method() { + let out = compile_and_run( + r#"x + $a + $b + $c) . $suffix; + } + + public function run(): void { + echo eval('return $this->label(1, 2, 3, "!");'); + } +} + +$box = new EvalMethodManyArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "16!"); +} + +/// Verifies eval fragments pass boxed Mixed values to public AOT methods. +#[test] +fn test_eval_fragment_can_call_this_public_mixed_arg_method() { + let out = compile_and_run( + r#"identity("mixed-ok");'); + } +} + +$box = new EvalMethodMixedArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "mixed-ok"); +} + /// Verifies eval fragments can unpack numeric arrays into public AOT method calls. #[test] fn test_eval_fragment_can_call_this_public_method_with_spread_args() { @@ -4437,6 +4483,10 @@ class EvalAotStaticBox { return $left . $right; } + public static function sum4(int $a, int $b, int $c, int $d): int { + return $a + $b + $c + $d; + } + public static function sum(int $left, int $right): int { return $left + $right; } @@ -4447,7 +4497,8 @@ $cb = ["EvalAotStaticBox", "join"]; echo call_user_func($cb, "C", "D"); echo ":"; $named = "EvalAotStaticBox::join"; echo $named("E", "F"); echo ":"; -echo call_user_func_array(["EvalAotStaticBox", "sum"], [2, 5]);'); +echo call_user_func_array(["EvalAotStaticBox", "sum"], [2, 5]); echo ":"; +echo EvalAotStaticBox::sum4(1, 2, 3, 4);'); "#, ); assert!( @@ -4455,7 +4506,7 @@ echo call_user_func_array(["EvalAotStaticBox", "sum"], [2, 5]);'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "AB:CD:EF:7"); + assert_eq!(out.stdout, "AB:CD:EF:7:10"); } /// Verifies native callable probes can see functions declared by eval after the barrier. @@ -5715,6 +5766,23 @@ echo eval('$box = new EvalDynamicNewOneArgCtor(11); return $box->x;'); assert_eq!(out, "11"); } +/// Verifies eval object construction passes more than two arguments to an AOT constructor. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_many_args() { + let out = compile_and_run( + r#"label = ($a + $b + $c) . $suffix; + } +} +echo eval('$box = new EvalDynamicNewManyArgCtor(1, 2, 3, "!"); return $box->label;'); +"#, + ); + assert_eq!(out, "6!"); +} + /// Verifies eval follows PHP by accepting constructor arguments when no constructor exists. #[test] fn test_eval_dynamic_new_accepts_args_without_constructor() { From 254912b689ac6f491c6344233a73b3e0887f234a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 21:19:39 +0200 Subject: [PATCH 0354/1208] Expand eval AOT stack arg bridge --- docs/php/eval.md | 8 ++-- src/codegen/eval_constructor_helpers.rs | 32 ++++--------- src/codegen/eval_method_helpers.rs | 50 +++++++++----------- src/codegen/lower_inst.rs | 5 +- src/codegen_support/abi/calls/mod.rs | 5 +- src/codegen_support/abi/calls/outgoing.rs | 13 ++++++ src/codegen_support/abi/mod.rs | 1 + tests/codegen/eval.rs | 56 +++++++++++++++++++++++ 8 files changed, 110 insertions(+), 60 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index eb7933f131..76917a717f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -73,8 +73,8 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata use fixed positional constructor arguments for scalar/Mixed signatures that fit the target ABI register bridge. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method and static-method fallback remains fixed-arity and positional for public scalar/Mixed signatures that fit the target ABI register bridge. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata use fixed positional constructor arguments for supported public scalar/Mixed signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method and static-method fallback remains fixed-arity and positional for supported public scalar/Mixed signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -122,7 +122,7 @@ or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method fallback remains fixed-arity and positional for public scalar/Mixed signatures -that fit the target ABI register bridge. +supported by the generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -316,7 +316,7 @@ bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported attribute/getName slice and broader generated/AOT method bridge signatures -beyond the current public scalar/Mixed fixed-arity register slice. +beyond the current public scalar/Mixed fixed-arity positional slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index abbb2432b9..d92de6b7eb 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -9,14 +9,13 @@ //! - The cacheable runtime object can allocate by name, but only user assembly //! knows constructor symbols and parameter ABI shapes. //! - Classes without constructors are treated as successful no-ops, matching PHP. -//! - Constructors are bridged only when their fixed scalar/Mixed arguments fit -//! the target ABI register bridge. +//! - Constructors are bridged for fixed non-by-ref scalar/Mixed arguments. use std::collections::BTreeMap; use crate::codegen::abi; use crate::codegen::emit::Emitter; -use crate::codegen::platform::{Arch, Target}; +use crate::codegen::platform::Arch; use crate::ir::{Function, LocalKind, Module}; use crate::names::{method_symbol, php_symbol_key}; use crate::parser::ast::Visibility; @@ -101,7 +100,6 @@ fn collect_eval_constructor_slots(module: &Module) -> Vec { classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { collect_class_constructor_slot( - module.target, class_name, class_info, &emitted_methods, @@ -125,7 +123,6 @@ fn collect_builtin_throwable_constructor_class_ids(module: &Module) -> Vec /// Adds one constructor slot for a class when the constructor has emitted code. fn collect_class_constructor_slot( - target: Target, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -144,7 +141,7 @@ fn collect_class_constructor_slot( return; } let supported = constructor_is_public(class_info, &method_key) - && constructor_signature_supported(target, class_name, sig); + && constructor_signature_supported(sig); let params = if supported { sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect() } else { @@ -168,7 +165,7 @@ fn constructor_is_public(class_info: &ClassInfo, method_key: &str) -> bool { } /// Returns true for constructor signatures supported by this eval bridge slice. -fn constructor_signature_supported(target: Target, class_name: &str, sig: &FunctionSig) -> bool { +fn constructor_signature_supported(sig: &FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_CONSTRUCTOR_ARGS && sig.variadic.is_none() && sig.ref_params.iter().all(|is_ref| !*is_ref) @@ -176,7 +173,6 @@ fn constructor_signature_supported(target: Target, class_name: &str, sig: &Funct .params .iter() .all(|(_, ty)| constructor_param_supported(ty)) - && eval_bridge_constructor_signature_fits_registers(target, class_name, sig) } /// Returns true for one constructor argument type supported by the bridge. @@ -187,20 +183,6 @@ fn constructor_param_supported(ty: &PhpType) -> bool { ) } -/// Returns true when this helper can materialize the complete constructor call in registers. -fn eval_bridge_constructor_signature_fits_registers( - target: Target, - class_name: &str, - sig: &FunctionSig, -) -> bool { - let mut arg_types = Vec::with_capacity(sig.params.len() + 1); - arg_types.push(PhpType::Object(class_name.to_string())); - arg_types.extend(sig.params.iter().map(|(_, ty)| ty.codegen_repr())); - abi::build_outgoing_arg_assignments_for_target(target, &arg_types, 0) - .iter() - .all(|assignment| assignment.in_register()) -} - /// Emits `__elephc_eval_value_construct_object(Mixed*, MixedArray*) -> bool`. fn emit_constructor_helper( module: &Module, @@ -495,7 +477,10 @@ fn emit_aarch64_constructor_body( } emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); let overflow_bytes = emit_aarch64_prepare_constructor_args(module, emitter, slot); + let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emitter.instruction(&format!("b {}", success_label)); // constructor returned normally } @@ -514,7 +499,10 @@ fn emit_x86_64_constructor_body( } emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); let overflow_bytes = emit_x86_64_prepare_constructor_args(module, emitter, slot); + let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emitter.instruction(&format!("jmp {}", success_label)); // constructor returned normally } diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index fc5e6f2a82..2eb127eda8 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -9,8 +9,7 @@ //! - The cacheable runtime object cannot know user class ids, method symbols, //! or return types, so this bridge is emitted into the user assembly. //! - This method-call slice supports public AOT methods with fixed non-by-ref -//! scalar/Mixed argument lists that fit the target ABI register bridge, and -//! reports unsupported calls as runtime failure. +//! scalar/Mixed argument lists and reports unsupported calls as runtime failure. use std::collections::BTreeMap; @@ -18,7 +17,7 @@ use crate::codegen::abi; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::emit_box_current_value_as_mixed; -use crate::codegen::platform::{Arch, Target}; +use crate::codegen::platform::Arch; use crate::ir::{Function, LocalKind, Module}; use crate::names::{method_symbol, static_method_symbol}; use crate::parser::ast::Visibility; @@ -123,7 +122,6 @@ fn collect_eval_method_slots(module: &Module) -> Vec { classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { collect_class_method_slots( - module.target, class_name, class_info, &emitted_methods, @@ -141,7 +139,6 @@ fn collect_eval_static_method_slots(module: &Module) -> Vec Vec { /// Adds bridge-supported public methods for one class. fn collect_class_method_slots( - target: Target, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -175,7 +171,7 @@ fn collect_class_method_slots( methods.sort_by_key(|(method, _)| method.as_str()); for (method, sig) in methods { if !method_is_public(class_info, method) - || !method_signature_supported(sig, target, PhpType::Object(class_name.to_string())) + || !method_signature_supported(sig) || !method_return_supported(&sig.return_type) { continue; @@ -201,7 +197,6 @@ fn collect_class_method_slots( /// Adds bridge-supported public static methods for one class. fn collect_class_static_method_slots( - target: Target, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -211,7 +206,7 @@ fn collect_class_static_method_slots( methods.sort_by_key(|(method, _)| method.as_str()); for (method, sig) in methods { if !static_method_is_public(class_info, method) - || !method_signature_supported(sig, target, PhpType::Int) + || !method_signature_supported(sig) || !method_return_supported(&sig.return_type) { continue; @@ -252,16 +247,11 @@ fn static_method_is_public(class_info: &ClassInfo, method: &str) -> bool { } /// Returns true for method signatures supported by the eval bridge. -fn method_signature_supported( - sig: &crate::types::FunctionSig, - target: Target, - hidden_arg_ty: PhpType, -) -> bool { +fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_METHOD_ARGS && sig.variadic.is_none() && sig.ref_params.iter().all(|is_ref| !*is_ref) && sig.params.iter().all(|(_, ty)| method_param_supported(ty)) - && eval_bridge_signature_fits_registers(target, hidden_arg_ty, &sig.params) } /// Returns true for an eval-supplied method argument type supported by this bridge. @@ -272,20 +262,6 @@ fn method_param_supported(ty: &PhpType) -> bool { ) } -/// Returns true when this helper can materialize the complete call without stack args. -fn eval_bridge_signature_fits_registers( - target: Target, - hidden_arg_ty: PhpType, - params: &[(String, PhpType)], -) -> bool { - let mut arg_types = Vec::with_capacity(params.len() + 1); - arg_types.push(hidden_arg_ty.codegen_repr()); - arg_types.extend(params.iter().map(|(_, ty)| ty.codegen_repr())); - abi::build_outgoing_arg_assignments_for_target(target, &arg_types, 0) - .iter() - .all(|assignment| assignment.in_register()) -} - /// Returns true for return storage shapes the bridge can box for eval. fn method_return_supported(ty: &PhpType) -> bool { matches!( @@ -849,7 +825,11 @@ fn emit_aarch64_method_bodies( emitter.label(&method_body_label(module, slot)); emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); let overflow_bytes = emit_aarch64_prepare_method_args(module, emitter, slot); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result @@ -868,7 +848,11 @@ fn emit_x86_64_method_bodies( emitter.label(&method_body_label(module, slot)); emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); let overflow_bytes = emit_x86_64_prepare_method_args(module, emitter, slot); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result @@ -887,10 +871,14 @@ fn emit_aarch64_static_method_bodies( emitter.label(&static_method_body_label(module, slot)); emit_aarch64_validate_static_method_arg_count(module, emitter, slot, fail_label); let overflow_bytes = emit_aarch64_prepare_static_method_args(module, emitter, slot); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label( emitter, &static_method_symbol(&slot.impl_class, &slot.method), ); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native static method result @@ -909,10 +897,14 @@ fn emit_x86_64_static_method_bodies( emitter.label(&static_method_body_label(module, slot)); emit_x86_64_validate_static_method_arg_count(module, emitter, slot, fail_label); let overflow_bytes = emit_x86_64_prepare_static_method_args(module, emitter, slot); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); + abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label( emitter, &static_method_symbol(&slot.impl_class, &slot.method), ); + abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native static method result diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 78ad5c42e9..2cc58e3424 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -6022,10 +6022,7 @@ pub(super) fn direct_call_stack_pad_bytes( ctx: &FunctionContext<'_>, overflow_bytes: usize, ) -> usize { - match ctx.emitter.target.arch { - Arch::AArch64 if overflow_bytes > 0 => 16, - _ => 0, - } + abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, overflow_bytes) } /// Lowers a signed integer comparison into a boolean result value. diff --git a/src/codegen_support/abi/calls/mod.rs b/src/codegen_support/abi/calls/mod.rs index 6d2cd8b299..60e968a233 100644 --- a/src/codegen_support/abi/calls/mod.rs +++ b/src/codegen_support/abi/calls/mod.rs @@ -15,7 +15,10 @@ mod stack; pub use incoming::emit_store_incoming_param; pub use invoke::{emit_call_label, emit_call_reg}; -pub use outgoing::{build_outgoing_arg_assignments_for_target, materialize_outgoing_args}; +pub use outgoing::{ + build_outgoing_arg_assignments_for_target, materialize_outgoing_args, + outgoing_call_stack_pad_bytes, +}; pub use stack::{ emit_load_temporary_stack_slot, emit_pop_float_reg, emit_pop_reg, emit_pop_reg_pair, emit_push_float_reg, emit_push_reg, emit_push_reg_pair, emit_push_result_value, diff --git a/src/codegen_support/abi/calls/outgoing.rs b/src/codegen_support/abi/calls/outgoing.rs index a693daa8b5..0b406022aa 100644 --- a/src/codegen_support/abi/calls/outgoing.rs +++ b/src/codegen_support/abi/calls/outgoing.rs @@ -255,3 +255,16 @@ pub fn materialize_outgoing_args( overflow_bytes } + +/// Returns the temporary caller-stack padding needed after outgoing stack args are materialized. +/// +/// AArch64 callees read the first spilled incoming argument at `x29+32`, which leaves a +/// 16-byte gap above the outgoing stack-argument area after the callee saves `x29/x30`. +/// x86_64 already gets the corresponding gap from `call` pushing the return address and the +/// callee saving `rbp`. +pub fn outgoing_call_stack_pad_bytes(target: Target, overflow_bytes: usize) -> usize { + match target.arch { + Arch::AArch64 if overflow_bytes > 0 => 16, + _ => 0, + } +} diff --git a/src/codegen_support/abi/mod.rs b/src/codegen_support/abi/mod.rs index 0d425ed45b..33e3a4851e 100644 --- a/src/codegen_support/abi/mod.rs +++ b/src/codegen_support/abi/mod.rs @@ -29,6 +29,7 @@ pub use calls::{ emit_push_float_reg, emit_push_reg, emit_push_reg_pair, emit_push_result_value, emit_release_temporary_stack, emit_reserve_temporary_stack, emit_store_incoming_param, emit_store_to_sp, emit_temporary_stack_address, materialize_outgoing_args, + outgoing_call_stack_pad_bytes, }; pub use frame::{ emit_frame_prologue, emit_frame_restore, emit_frame_slot_address, emit_load_from_address, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5cbe09b255..5940d86884 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4398,6 +4398,28 @@ $box->run(); assert_eq!(out, "16!"); } +/// Verifies eval fragments pass AOT method arguments that overflow onto the caller stack. +#[test] +fn test_eval_fragment_can_call_this_public_method_with_stack_string_arg() { + let out = compile_and_run( + r#"join4("A", "B", "C", "D");'); + } +} + +$box = new EvalMethodStackStringArgBox(); +$box->run(); +"#, + ); + assert_eq!(out, "ABCD"); +} + /// Verifies eval fragments pass boxed Mixed values to public AOT methods. #[test] fn test_eval_fragment_can_call_this_public_mixed_arg_method() { @@ -4509,6 +4531,23 @@ echo EvalAotStaticBox::sum4(1, 2, 3, 4);'); assert_eq!(out.stdout, "AB:CD:EF:7:10"); } +/// Verifies eval static dispatch passes AOT static method arguments on the caller stack. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_stack_string_arg() { + let out = compile_and_run( + r#"labe assert_eq!(out, "6!"); } +/// Verifies eval object construction passes AOT constructor arguments on the caller stack. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_stack_string_arg() { + let out = compile_and_run( + r#"label = $a . $b . $c . $d; + } +} +echo eval('$box = new EvalDynamicNewStackStringCtor("Q", "R", "S", "T"); return $box->label;'); +"#, + ); + assert_eq!(out, "QRST"); +} + /// Verifies eval follows PHP by accepting constructor arguments when no constructor exists. #[test] fn test_eval_dynamic_new_accepts_args_without_constructor() { From 6a08247eba282ee7a0e0a78c7ce171372ea1f55c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 21:37:33 +0200 Subject: [PATCH 0355/1208] Support eval AOT method named args --- crates/elephc-eval/src/context.rs | 166 +++++++++ crates/elephc-eval/src/ffi/mod.rs | 2 + crates/elephc-eval/src/ffi/native_methods.rs | 306 +++++++++++++++++ crates/elephc-eval/src/ffi/tests.rs | 83 +++++ .../src/interpreter/expressions.rs | 3 +- crates/elephc-eval/src/interpreter/mod.rs | 2 +- .../elephc-eval/src/interpreter/statements.rs | 40 ++- .../src/interpreter/tests/dynamic_calls.rs | 25 +- .../src/interpreter/tests/expressions.rs | 19 ++ .../src/interpreter/tests/method_arguments.rs | 28 +- docs/php/eval.md | 23 +- src/codegen/lower_inst/builtins/eval.rs | 319 +++++++++++++++++- tests/codegen/eval.rs | 56 +++ 13 files changed, 1047 insertions(+), 25 deletions(-) create mode 100644 crates/elephc-eval/src/ffi/native_methods.rs diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 67468d38f0..74f6198723 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -83,6 +83,45 @@ impl NativeFunction { } } +/// Native AOT method or constructor signature metadata visible to eval fragments. +#[derive(Clone)] +pub struct NativeCallableSignature { + param_count: usize, + param_names: Vec, +} + +impl NativeCallableSignature { + /// Creates signature metadata with the visible positional parameter count. + pub const fn new(param_count: usize) -> Self { + Self { + param_count, + param_names: Vec::new(), + } + } + + /// Returns the visible positional parameter count accepted by this callable. + pub const fn param_count(&self) -> usize { + self.param_count + } + + /// Records the PHP parameter name for one positional callable slot. + pub fn set_param_name(&mut self, index: usize, name: impl Into) -> bool { + if index >= self.param_count { + return false; + } + if self.param_names.len() < self.param_count { + self.param_names.resize(self.param_count, String::new()); + } + self.param_names[index] = name.into(); + true + } + + /// Returns the PHP-visible parameter names registered for this callable. + pub fn param_names(&self) -> &[String] { + &self.param_names + } +} + /// Process-level eval context passed opaquely across the C ABI. /// /// Generated code never inspects this layout directly; it only passes pointers @@ -104,6 +143,9 @@ pub struct ElephcEvalContext { constants: HashMap, functions: HashMap, native_functions: HashMap, + native_methods: HashMap<(String, String), NativeCallableSignature>, + native_static_methods: HashMap<(String, String), NativeCallableSignature>, + native_constructors: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, static_properties: HashMap<(String, String), RuntimeCellHandle>, class_constants: HashMap<(String, String), RuntimeCellHandle>, @@ -144,6 +186,9 @@ impl ElephcEvalContext { constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), + native_methods: HashMap::new(), + native_static_methods: HashMap::new(), + native_constructors: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -185,6 +230,9 @@ impl ElephcEvalContext { constants: HashMap::new(), functions: HashMap::new(), native_functions: HashMap::new(), + native_methods: HashMap::new(), + native_static_methods: HashMap::new(), + native_constructors: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -914,6 +962,111 @@ impl ElephcEvalContext { .is_some_and(|function| function.set_param_name(index, param_name)) } + /// Defines native AOT instance-method signature metadata for eval named-argument binding. + pub fn define_native_method_signature( + &mut self, + class_name: &str, + method_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_methods + .insert(native_method_key(class_name, method_name), signature) + .is_none() + } + + /// Defines native AOT static-method signature metadata for eval named-argument binding. + pub fn define_native_static_method_signature( + &mut self, + class_name: &str, + method_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_static_methods + .insert(native_method_key(class_name, method_name), signature) + .is_none() + } + + /// Defines native AOT constructor signature metadata for eval named-argument binding. + pub fn define_native_constructor_signature( + &mut self, + class_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_constructors + .insert(normalize_class_name(class_name), signature) + .is_none() + } + + /// Records one parameter name for registered native AOT instance-method metadata. + pub fn define_native_method_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Records one parameter name for registered native AOT static-method metadata. + pub fn define_native_static_method_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Records one parameter name for registered native AOT constructor metadata. + pub fn define_native_constructor_param( + &mut self, + class_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Returns native AOT instance-method signature metadata by PHP class and method name. + pub fn native_method_signature( + &self, + class_name: &str, + method_name: &str, + ) -> Option { + self.native_methods + .get(&native_method_key(class_name, method_name)) + .cloned() + } + + /// Returns native AOT static-method signature metadata by PHP class and method name. + pub fn native_static_method_signature( + &self, + class_name: &str, + method_name: &str, + ) -> Option { + self.native_static_methods + .get(&native_method_key(class_name, method_name)) + .cloned() + } + + /// Returns native AOT constructor signature metadata by PHP class name. + pub fn native_constructor_signature( + &self, + class_name: &str, + ) -> Option { + self.native_constructors + .get(&normalize_class_name(class_name)) + .cloned() + } + /// Returns true when the context has a dynamic or native function with this lowercase PHP name. pub fn has_function(&self, name: &str) -> bool { self.functions.contains_key(name) || self.native_functions.contains_key(name) @@ -1160,6 +1313,19 @@ fn normalize_enum_case_name(name: &str) -> String { name.to_string() } +/// Normalizes PHP method names for case-insensitive native metadata lookup. +fn normalize_method_name(name: &str) -> String { + name.to_ascii_lowercase() +} + +/// Builds the folded native method metadata key used for eval argument binding. +fn native_method_key(class_name: &str, method_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + normalize_method_name(method_name), + ) +} + /// Pushes a PHP class-like name once, preserving the first visible spelling. fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSet) { let key = normalize_class_name(name); diff --git a/crates/elephc-eval/src/ffi/mod.rs b/crates/elephc-eval/src/ffi/mod.rs index 55ea34c319..4c87feb3f5 100644 --- a/crates/elephc-eval/src/ffi/mod.rs +++ b/crates/elephc-eval/src/ffi/mod.rs @@ -14,6 +14,7 @@ pub mod execute; #[cfg(not(test))] pub mod function_calls; pub mod native_functions; +pub mod native_methods; pub mod scope; pub mod symbols; pub(crate) mod util; @@ -23,6 +24,7 @@ pub use execute::*; #[cfg(not(test))] pub use function_calls::*; pub use native_functions::*; +pub use native_methods::*; pub use scope::*; pub use symbols::*; diff --git a/crates/elephc-eval/src/ffi/native_methods.rs b/crates/elephc-eval/src/ffi/native_methods.rs new file mode 100644 index 0000000000..5ddfa8ed2c --- /dev/null +++ b/crates/elephc-eval/src/ffi/native_methods.rs @@ -0,0 +1,306 @@ +//! Purpose: +//! Exports registration of generated native PHP method signatures into an eval +//! context so runtime fragments can bind AOT method named arguments. +//! +//! Called from: +//! - Generated EIR backend assembly before fragments can call AOT methods. +//! +//! Key details: +//! - Invalid names, handles, or indexes fail closed as `false`. +//! - The metadata records parameter names only; generated user helpers still +//! perform the actual method, static method, and constructor calls. + +use super::util::abi_name_to_string; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; +use crate::context::NativeCallableSignature; + +/// Registers a generated native PHP method signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `method_key_ptr` must point to a +/// readable `ClassName::methodName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_inner(ctx, method_key_ptr, method_key_len, false, param_count) + }) + .unwrap_or(0) +} + +/// Registers a generated native PHP static-method signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `method_key_ptr` must point to a +/// readable `ClassName::methodName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_inner(ctx, method_key_ptr, method_key_len, true, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers a generated native PHP constructor signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `class_name_ptr` must be readable +/// for `class_name_len` bytes. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_inner(ctx, class_name_ptr, class_name_len, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and parameter name pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Runs native method registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method`; invalid handles, names, or +/// counts fail closed as `false`. +unsafe fn register_native_method_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + let signature = NativeCallableSignature::new(param_count); + if is_static { + i32::from(context.define_native_static_method_signature( + class_name, + method_name, + signature, + )) + } else { + i32::from(context.define_native_method_signature(class_name, method_name, signature)) + } +} + +/// Runs native method parameter registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param`; invalid handles, names, +/// or indexes fail closed as `false`. +unsafe fn register_native_method_param_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param( + class_name, + method_name, + param_index, + param_name, + )) + } else { + i32::from(context.define_native_method_param( + class_name, + method_name, + param_index, + param_name, + )) + } +} + +/// Runs native constructor registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor`; invalid handles, names, +/// or counts fail closed as `false`. +unsafe fn register_native_constructor_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + i32::from(context.define_native_constructor_signature( + &class_name, + NativeCallableSignature::new(param_count), + )) +} + +/// Runs native constructor parameter registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param`; invalid handles, +/// names, or indexes fail closed as `false`. +unsafe fn register_native_constructor_param_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_constructor_param(&class_name, param_index, param_name)) +} + +/// Splits one generated `ClassName::methodName` metadata key into class and method pieces. +fn split_method_key(method_key: &str) -> Option<(&str, &str)> { + let (class_name, method_name) = method_key.rsplit_once("::")?; + (!class_name.is_empty() && !method_name.is_empty()).then_some((class_name, method_name)) +} diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs index 4408a0eea0..196b308871 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -13,6 +13,7 @@ use super::context::*; use super::execute::*; use super::native_functions::*; +use super::native_methods::*; use super::scope::*; use super::symbols::*; use crate::abi::{ @@ -230,6 +231,88 @@ fn register_native_function_reports_function_exists() { assert_eq!(native.param_names(), &["value".to_string()]); } +/// Verifies native AOT method registration records instance/static/constructor parameters. +#[test] +fn register_native_methods_record_signature_metadata() { + let mut ctx = ElephcEvalContext::new(); + let method = b"KnownClass::join"; + let static_method = b"KnownClass::sum"; + let class = b"KnownClass"; + let left = b"left"; + let right = b"right"; + let value = b"value"; + + let method_registered = unsafe { + __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 2) + }; + let method_param_registered = unsafe { + __elephc_eval_register_native_method_param( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + right.as_ptr(), + right.len() as u64, + ) + }; + let static_registered = unsafe { + __elephc_eval_register_native_static_method( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 2, + ) + }; + let static_param_registered = unsafe { + __elephc_eval_register_native_static_method_param( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + left.as_ptr(), + left.len() as u64, + ) + }; + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let constructor_param_registered = unsafe { + __elephc_eval_register_native_constructor_param( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + value.as_ptr(), + value.len() as u64, + ) + }; + + assert_eq!(method_registered, 1); + assert_eq!(method_param_registered, 1); + assert_eq!(static_registered, 1); + assert_eq!(static_param_registered, 1); + assert_eq!(constructor_registered, 1); + assert_eq!(constructor_param_registered, 1); + assert_eq!( + ctx.native_method_signature("knownclass", "JOIN") + .expect("method metadata") + .param_names(), + &["".to_string(), "right".to_string()] + ); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_names(), + &["left".to_string(), "".to_string()] + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_names(), + &["value".to_string()] + ); +} + /// Verifies scope allocation returns an empty opaque activation scope handle. #[test] fn scope_new_returns_empty_handle() { diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 904044a818..35defe0d54 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -76,7 +76,8 @@ pub(in crate::interpreter) fn eval_expr( let class_name = context .resolve_class_name(class_name) .unwrap_or_else(|| class_name.clone()); - let args = positional_evaluated_arg_values(args)?; + let args = + bind_native_callable_args(context.native_constructor_signature(&class_name), args)?; values .new_object(&class_name) .and_then(|object| values.construct_object(object, args).map(|()| object)) diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index 31f402fbb8..0551c5018a 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -28,7 +28,7 @@ mod runtime_ops; mod scope_cells; mod statements; -use crate::context::{ElephcEvalContext, NativeFunction}; +use crate::context::{ElephcEvalContext, NativeCallableSignature, NativeFunction}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 0f900a8de2..41d73ea567 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1662,10 +1662,10 @@ pub(in crate::interpreter) fn eval_static_method_call_result( { return Err(EvalStatus::RuntimeFatal); } - if evaluated_args.iter().any(|arg| arg.name.is_some()) { - return Err(EvalStatus::RuntimeFatal); - } - let args = evaluated_args.into_iter().map(|arg| arg.value).collect(); + let args = bind_native_callable_args( + context.native_static_method_signature(&class_name, method_name), + evaluated_args, + )?; values.static_method_call(&class_name, method_name, args) } @@ -1990,7 +1990,11 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( } } let Some(class) = context.dynamic_object_class(identity) else { - let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; + let class_name = runtime_object_class_name(object, values)?; + let evaluated_args = bind_native_callable_args( + context.native_method_signature(&class_name, method_name), + evaluated_args, + )?; return values.method_call(object, method_name, evaluated_args); }; let called_class_name = class.name().to_string(); @@ -2012,6 +2016,17 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( ) } +/// Returns the runtime-visible class name for a non-eval object receiver. +fn runtime_object_class_name( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) +} + /// Instantiates an eval-declared attribute class for `ReflectionAttribute::newInstance()`. fn eval_reflection_attribute_new_instance_result( attribute: &EvalAttribute, @@ -2217,6 +2232,21 @@ pub(in crate::interpreter) fn positional_evaluated_arg_values( Ok(args.into_iter().map(|arg| arg.value).collect()) } +/// Binds native AOT callable args by registered names, or falls back to positional-only args. +pub(in crate::interpreter) fn bind_native_callable_args( + signature: Option, + args: Vec, +) -> Result, EvalStatus> { + let Some(signature) = signature else { + return positional_evaluated_arg_values(args); + }; + if signature.param_names().len() == signature.param_count() { + bind_evaluated_function_args(signature.param_names(), args) + } else { + positional_evaluated_arg_values(args) + } +} + /// Executes a PHP `static $name = expr;` declaration in the current eval scope. pub(in crate::interpreter) fn execute_static_var_stmt( name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs index fb44cab439..7a89d56aa1 100644 --- a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs @@ -222,9 +222,30 @@ return call_user_func_array(["KnownClass", "sum"], [2, 5]);"#, assert_eq!(values.get(result), FakeValue::Int(7)); } -/// Verifies runtime AOT static method fallback rejects named arguments. +/// Verifies runtime AOT static method fallback binds registered named arguments. #[test] -fn execute_program_static_runtime_method_hook_rejects_named_args() { +fn execute_program_static_runtime_method_hook_binds_named_args() { + let program = parse_fragment( + br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature("KnownClass", "join", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered named AOT call should bind"); + + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} + +/// Verifies runtime AOT static method fallback rejects named arguments without metadata. +#[test] +fn execute_program_static_runtime_method_hook_rejects_unregistered_named_args() { let program = parse_fragment( br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, ) diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index da8daa662a..bda451d9f8 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -307,6 +307,25 @@ fn execute_program_constructs_named_object_with_args() { assert_eq!(values.get(x), FakeValue::Int(1)); } + +/// Verifies eval object construction binds registered AOT constructor named arguments. +#[test] +fn execute_program_constructs_named_object_with_registered_named_args() { + let program = parse_fragment(br#"$box = new KnownClass(value: 9); return $box->read_x();"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered constructor named args should bind"); + + assert_eq!(values.get(result), FakeValue::Int(9)); +} + /// Verifies eval-declared classes create objects with properties and methods. #[test] fn execute_program_constructs_eval_declared_class_with_method() { diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index 9181ac42c4..f641710f63 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -64,9 +64,31 @@ return $box->read(missing: "bad");"#, assert_eq!(err, EvalStatus::RuntimeFatal); } -/// Verifies runtime/AOT method fallback still rejects named arguments. +/// Verifies runtime/AOT method fallback binds registered native method named arguments. #[test] -fn execute_program_rejects_named_args_for_runtime_method_fallback() { +fn execute_program_binds_registered_runtime_method_named_args() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +return $box->add2_x(right: 2, left: 3);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered runtime method named args should bind"); + + assert_eq!(values.get(result), FakeValue::Int(15)); +} + +/// Verifies runtime/AOT method fallback rejects named arguments without metadata. +#[test] +fn execute_program_rejects_unregistered_named_args_for_runtime_method_fallback() { let program = parse_fragment(br#"return $this->answer(value: 1);"#).expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -75,7 +97,7 @@ fn execute_program_rejects_named_args_for_runtime_method_fallback() { scope.set("this", object, ScopeCellOwnership::Borrowed); let err = execute_program(&program, &mut scope, &mut values) - .expect_err("runtime method fallback named args should fail"); + .expect_err("unregistered runtime method fallback named args should fail"); assert_eq!(err, EvalStatus::RuntimeFatal); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 76917a717f..d0a95be8ad 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -73,8 +73,8 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata use fixed positional constructor arguments for supported public scalar/Mixed signatures. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method and static-method fallback remains fixed-arity and positional for supported public scalar/Mixed signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -121,8 +121,8 @@ positional arguments. Static method callables can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method -fallback remains fixed-arity and positional for public scalar/Mixed signatures -supported by the generated bridge. +fallback supports the same named-argument binding for public scalar/Mixed +signatures supported by the generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -192,8 +192,9 @@ constants. Direct `new EnumName()` and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native -methods are bridged to eval. Public zero-, one-, or two-scalar-argument method -calls through `$this->method(...)` are supported by the native method bridge. +methods are bridged to eval. Public fixed scalar/Mixed method calls through +`$this->method(...)` are supported by the native method bridge, including +registered named arguments and string-keyed unpacking. ## Namespaces and constants @@ -308,15 +309,15 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are -still outside eval fragments. Runtime/AOT object-method and static-method -fallback from eval remains fixed-arity and positional, so named method arguments -are supported for eval-declared methods but not for every generated native method -bridge. +still outside eval fragments. Runtime/AOT object-method, static-method, and +constructor fallback from eval remains limited to the generated public +non-by-reference fixed scalar/Mixed bridge slice, so variadic, by-reference, and +broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported attribute/getName slice and broader generated/AOT method bridge signatures -beyond the current public scalar/Mixed fixed-arity positional slice. +beyond the current public non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 59434c7339..3fd0acaadf 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -19,8 +19,9 @@ use crate::codegen::{ abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, }; use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op}; -use crate::names::{function_symbol, ir_global_symbol}; -use crate::types::{FunctionSig, PhpType}; +use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; +use crate::parser::ast::Visibility; +use crate::types::{ClassInfo, FunctionSig, PhpType}; use super::super::super::context::FunctionContext; use super::super::{ @@ -46,6 +47,7 @@ const EVAL_CODE_LEN_OFFSET: usize = 56; const EVAL_GLOBAL_SCOPE_HANDLE_OFFSET: usize = 64; const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; +const MAX_EVAL_NATIVE_METHOD_PARAMS: usize = 8; /// Local slot metadata needed for conservative eval scope synchronization. #[derive(Clone)] @@ -75,6 +77,20 @@ struct EvalNativeFunctionRegistration { signature: FunctionSig, } +/// A module-local method signature that can be registered with the eval context. +struct EvalNativeMethodRegistration { + class_name: String, + method_name: String, + is_static: bool, + signature: FunctionSig, +} + +/// A module-local constructor signature that can be registered with the eval context. +struct EvalNativeConstructorRegistration { + class_name: String, + signature: FunctionSig, +} + /// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { super::ensure_arg_count(inst, "eval", 1)?; @@ -400,6 +416,7 @@ fn ensure_eval_context(ctx: &mut FunctionContext<'_>) -> Result<()> { abi::emit_call_label(ctx.emitter, &symbol); abi::store_at_offset(ctx.emitter, result_reg, offset); register_eval_native_functions(ctx, offset)?; + register_eval_native_method_signatures(ctx, offset); ctx.emitter.label(&ready); abi::load_at_offset(ctx.emitter, result_reg, offset); abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_CONTEXT_HANDLE_OFFSET); @@ -428,6 +445,16 @@ fn register_eval_native_functions( Ok(()) } +/// Registers eligible AOT method and constructor signatures with a newly allocated eval context. +fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context_offset: usize) { + for registration in eval_native_method_registrations(ctx) { + register_eval_native_method(ctx, context_offset, ®istration); + } + for registration in eval_native_constructor_registrations(ctx) { + register_eval_native_constructor(ctx, context_offset, ®istration); + } +} + /// Collects global PHP functions that can use the descriptor-invoker bridge. fn eval_native_function_registrations( ctx: &FunctionContext<'_>, @@ -443,6 +470,92 @@ fn eval_native_function_registrations( .collect() } +/// Collects public AOT methods whose parameter names can be exposed to eval binding. +fn eval_native_method_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_eval_native_instance_methods(class_name, class_info, &mut registrations); + collect_eval_native_static_methods(class_name, class_info, &mut registrations); + } + registrations +} + +/// Collects public AOT constructors whose parameter names can be exposed to eval binding. +fn eval_native_constructor_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let method_key = php_symbol_key("__construct"); + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + let Some(signature) = class_info.methods.get(&method_key) else { + continue; + }; + if !class_method_is_public(class_info, &method_key) + || !method_signature_can_register_with_eval(signature) + { + continue; + } + registrations.push(EvalNativeConstructorRegistration { + class_name: class_name.clone(), + signature: signature.clone(), + }); + } + registrations +} + +/// Adds eligible public instance methods for one class to eval signature registration. +fn collect_eval_native_instance_methods( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut methods = class_info.methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method_name, signature) in methods { + if method_name == "__construct" + || !class_method_is_public(class_info, method_name) + || !method_signature_can_register_with_eval(signature) + { + continue; + } + registrations.push(EvalNativeMethodRegistration { + class_name: class_name.to_string(), + method_name: method_name.clone(), + is_static: false, + signature: signature.clone(), + }); + } +} + +/// Adds eligible public static methods for one class to eval signature registration. +fn collect_eval_native_static_methods( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut methods = class_info.static_methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method_name, signature) in methods { + if !class_static_method_is_public(class_info, method_name) + || !method_signature_can_register_with_eval(signature) + { + continue; + } + registrations.push(EvalNativeMethodRegistration { + class_name: class_name.to_string(), + method_name: method_name.clone(), + is_static: true, + signature: signature.clone(), + }); + } +} + /// Returns true when a module function is a PHP-visible AOT function supported by this bridge. fn function_can_register_with_eval(function: &Function) -> bool { !function.flags.is_main @@ -453,6 +566,29 @@ fn function_can_register_with_eval(function: &Function) -> bool { .all(|param| !param.by_ref && !param.variadic) } +/// Returns true when eval can bind native method arguments by fixed parameter names. +fn method_signature_can_register_with_eval(signature: &FunctionSig) -> bool { + signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS + && signature.variadic.is_none() + && signature.ref_params.iter().all(|is_ref| !*is_ref) +} + +/// Returns true when an instance method is public in the class metadata. +fn class_method_is_public(class_info: &ClassInfo, method_name: &str) -> bool { + class_info + .method_visibilities + .get(method_name) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + +/// Returns true when a static method is public in the class metadata. +fn class_static_method_is_public(class_info: &ClassInfo, method_name: &str) -> bool { + class_info + .static_method_visibilities + .get(method_name) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + /// Emits one native-function registration call into the just-created eval context. fn register_eval_native_function( ctx: &mut FunctionContext<'_>, @@ -519,6 +655,185 @@ fn register_eval_native_function( Ok(()) } +/// Emits one native method signature registration call into the eval context. +fn register_eval_native_method( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeMethodRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let method_key = format!("{}::{}", registration.class_name, registration.method_name); + let (method_key_label, method_key_len) = ctx.data.add_string(method_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + registration.signature.params.len() as i64, + ); + let symbol = if registration.is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method") + }; + abi::emit_call_label(ctx.emitter, &symbol); + for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { + register_eval_native_method_param( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + index, + param_name, + ); + } +} + +/// Emits one native method parameter-name registration call. +fn register_eval_native_method_param( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + param_index: usize, + param_name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + ¶m_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + param_name_len as i64, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_param") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native constructor signature registration call into the eval context. +fn register_eval_native_constructor( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeConstructorRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let (class_name_label, class_name_len) = ctx.data.add_string(registration.class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + registration.signature.params.len() as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor"); + abi::emit_call_label(ctx.emitter, &symbol); + for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { + register_eval_native_constructor_param( + ctx, + context_offset, + &class_name_label, + class_name_len, + index, + param_name, + ); + } +} + +/// Emits one native constructor parameter-name registration call. +fn register_eval_native_constructor_param( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + param_index: usize, + param_name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (param_name_label, param_name_len) = ctx.data.add_string(param_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + ¶m_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + param_name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native-function parameter-name registration call. fn register_eval_native_function_param( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5940d86884..50258c310b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4548,6 +4548,62 @@ eval('echo EvalAotStaticStackStringBox::join4("G", "H", "I", "J");'); assert_eq!(out, "GHIJ"); } +/// Verifies eval binds named arguments before dispatching an AOT instance method. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_named_args() { + let out = compile_and_run( + r#"join(right: "B", left: "A");'); + } + + public function join(string $left, string $right): string { + return $left . $right; + } +} + +echo (new EvalAotNamedMethodBox())->run(); +"#, + ); + assert_eq!(out, "AB"); +} + +/// Verifies eval binds named arguments before dispatching an AOT static method. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_named_args() { + let out = compile_and_run( + r#"label = $left . $right; + } +} + +echo eval('$box = new EvalDynamicNewNamedCtor(right: "F", left: "E"); return $box->label;'); +"#, + ); + assert_eq!(out, "EF"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From 709cc7d1003babbaadd755832fcd4afae6ebf892 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 21:47:31 +0200 Subject: [PATCH 0356/1208] Support ReflectionClass kind predicates --- .../elephc-eval/src/interpreter/reflection.rs | 12 +- .../tests/builtins_class_metadata.rs | 25 ++- .../interpreter/tests/support/object_ops.rs | 18 ++ docs/php/classes.md | 7 +- docs/php/eval.md | 9 +- src/codegen/eval_reflection_owner_helpers.rs | 195 ++++++++++++++++-- src/codegen/lower_inst/objects/reflection.rs | 121 ++++++++--- src/types/checker/builtin_types/reflection.rs | 21 ++ tests/codegen/eval.rs | 27 ++- tests/codegen/oop/attributes.rs | 20 +- 10 files changed, 385 insertions(+), 70 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index a3c572a1db..00395c4ed8 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -14,6 +14,9 @@ use super::*; const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; +const EVAL_REFLECTION_CLASS_FLAG_INTERFACE: u64 = 4; +const EVAL_REFLECTION_CLASS_FLAG_TRAIT: u64 = 8; +const EVAL_REFLECTION_CLASS_FLAG_ENUM: u64 = 16; /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. pub(in crate::interpreter) fn eval_reflection_owner_new_object( @@ -219,6 +222,9 @@ fn eval_reflection_class_like_attributes( if class.is_abstract() { flags |= EVAL_REFLECTION_CLASS_FLAG_ABSTRACT; } + if context.has_enum(class.name()) { + flags |= EVAL_REFLECTION_CLASS_FLAG_ENUM; + } return Some(( class.name().trim_start_matches('\\').to_string(), class.attributes().to_vec(), @@ -229,21 +235,21 @@ fn eval_reflection_class_like_attributes( return Some(( interface.name().trim_start_matches('\\').to_string(), interface.attributes().to_vec(), - 0, + EVAL_REFLECTION_CLASS_FLAG_INTERFACE, )); } if let Some(trait_decl) = context.trait_decl(name) { return Some(( trait_decl.name().trim_start_matches('\\').to_string(), trait_decl.attributes().to_vec(), - 0, + EVAL_REFLECTION_CLASS_FLAG_TRAIT, )); } context.enum_decl(name).map(|enum_decl| { ( enum_decl.name().trim_start_matches('\\').to_string(), enum_decl.attributes().to_vec(), - EVAL_REFLECTION_CLASS_FLAG_FINAL, + EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM, ) }) } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 2cbbbe7119..08f8eba6fe 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -212,15 +212,30 @@ interface EvalIfaceReflect {} trait EvalTraitReflect {} enum EvalEnumReflect { case Ready; } echo (new ReflectionClass("EvalAbstractReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalAbstractReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalAbstractReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalAbstractReflect"))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass("EvalFinalReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalFinalReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalFinalReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalFinalReflect"))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass("EvalEnumReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalEnumReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalEnumReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalEnumReflect"))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass("EvalIfaceReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalIfaceReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalIfaceReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalIfaceReflect"))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass("EvalTraitReflect"))->isAbstract() ? "A" : "a"; echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalTraitReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalTraitReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalTraitReflect"))->isEnum() ? "E" : "e"; return true;"#, ) .expect("parse eval fragment"); @@ -229,7 +244,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Af:aF:aF:af:af"); + assert_eq!(values.output, "Afite:aFite:aFitE:afIte:afiTe"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 9a30b30263..ea78444fa1 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -110,6 +110,18 @@ impl FakeOps { Self::object_property(&properties, "__is_abstract") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isinterface") if args.is_empty() => { + Self::object_property(&properties, "__is_interface") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "istrait") if args.is_empty() => { + Self::object_property(&properties, "__is_trait") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isenum") if args.is_empty() => { + Self::object_property(&properties, "__is_enum") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getarguments") if args.is_empty() => { Self::object_property(&properties, "__args") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -239,10 +251,16 @@ impl FakeOps { let name = self.string(reflected_name)?; let is_final = self.bool_value((flags & 1) != 0)?; let is_abstract = self.bool_value((flags & 2) != 0)?; + let is_interface = self.bool_value((flags & 4) != 0)?; + let is_trait = self.bool_value((flags & 8) != 0)?; + let is_enum = self.bool_value((flags & 16) != 0)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); + properties.push(("__is_interface".to_string(), is_interface)); + properties.push(("__is_trait".to_string(), is_trait)); + properties.push(("__is_enum".to_string(), is_enum)); } let object = self.alloc(FakeValue::Object(properties)); self.object_classes diff --git a/docs/php/classes.md b/docs/php/classes.md index 171710d848..d6aa92a85c 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1083,8 +1083,13 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getName()` | `new ReflectionClass($class_name)` | Return the resolved class-like name | | `ReflectionClass::isFinal()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is final | | `ReflectionClass::isAbstract()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is abstract | +| `ReflectionClass::isInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an interface | +| `ReflectionClass::isTrait()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is a trait | +| `ReflectionClass::isEnum()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an enum | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | +| `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | +| `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionAttribute::newInstance()` | Internal only | Instantiate the attribute class from captured literal args | @@ -1144,7 +1149,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getAttributes()`, `isFinal()`, and `isAbstract()`. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()`; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, and `isEnum()`. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index d0a95be8ad..b1281dd454 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -152,10 +152,11 @@ syntax, but requesting those arguments is a runtime fatal. `ReflectionProperty::getAttributes()` expose eval-retained class, method, and property attributes for eval-declared class-like symbols when their arguments fit the same literal subset, and `getName()` returns the reflected class or -member name for those owners. `ReflectionClass::isFinal()` and -`ReflectionClass::isAbstract()` report eval class-like modifier metadata, -including PHP-compatible enum finality and false values for eval interfaces and -traits. `ReflectionClassConstant::getAttributes()`, +member name for those owners. `ReflectionClass::isFinal()`, +`ReflectionClass::isAbstract()`, `ReflectionClass::isInterface()`, +`ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval +class-like metadata, including PHP-compatible enum finality and class-like kind +checks for eval interfaces, traits, and enums. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant and enum-case attributes through the same materialized `ReflectionAttribute` diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index aac0ace8ea..9b0d574191 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -32,6 +32,12 @@ struct ReflectionOwnerLayout { is_final_hi: Option, is_abstract_lo: Option, is_abstract_hi: Option, + is_interface_lo: Option, + is_interface_hi: Option, + is_trait_lo: Option, + is_trait_hi: Option, + is_enum_lo: Option, + is_enum_hi: Option, } /// Layouts for the Reflection owner classes eval can materialize. @@ -112,14 +118,16 @@ fn reflection_owner_layouts(module: &Module) -> Option { } /// Returns one Reflection owner layout from class metadata. -fn reflection_owner_layout( - info: &ClassInfo, - has_name: bool, -) -> Option { +fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option { let attrs_lo = reflection_property_offset(info, "__attrs")?; - let name_lo = has_name.then(|| reflection_property_offset(info, "__name")).flatten(); + let name_lo = has_name + .then(|| reflection_property_offset(info, "__name")) + .flatten(); let is_final_lo = reflection_property_offset(info, "__is_final"); let is_abstract_lo = reflection_property_offset(info, "__is_abstract"); + let is_interface_lo = reflection_property_offset(info, "__is_interface"); + let is_trait_lo = reflection_property_offset(info, "__is_trait"); + let is_enum_lo = reflection_property_offset(info, "__is_enum"); Some(ReflectionOwnerLayout { class_id: info.class_id, property_count: info.properties.len(), @@ -131,6 +139,12 @@ fn reflection_owner_layout( is_final_hi: is_final_lo.map(|offset| offset + 8), is_abstract_lo, is_abstract_hi: is_abstract_lo.map(|offset| offset + 8), + is_interface_lo, + is_interface_hi: is_interface_lo.map(|offset| offset + 8), + is_trait_lo, + is_trait_hi: is_trait_lo.map(|offset| offset + 8), + is_enum_lo, + is_enum_hi: is_enum_lo.map(|offset| offset + 8), }) } @@ -185,12 +199,54 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds - emit_aarch64_owner_kind_body(emitter, class_label, &layouts.class, true, fail_label, box_label); - emit_aarch64_owner_kind_body(emitter, method_label, &layouts.method, true, fail_label, box_label); - emit_aarch64_owner_kind_body(emitter, property_label, &layouts.property, true, fail_label, box_label); - emit_aarch64_owner_kind_body(emitter, class_constant_label, &layouts.class_constant, true, fail_label, box_label); - emit_aarch64_owner_kind_body(emitter, enum_unit_case_label, &layouts.enum_unit_case, true, fail_label, box_label); - emit_aarch64_owner_kind_body(emitter, enum_backed_case_label, &layouts.enum_backed_case, true, fail_label, box_label); + emit_aarch64_owner_kind_body( + emitter, + class_label, + &layouts.class, + true, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + method_label, + &layouts.method, + true, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + property_label, + &layouts.property, + true, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + class_constant_label, + &layouts.class_constant, + true, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + enum_unit_case_label, + &layouts.enum_unit_case, + true, + fail_label, + box_label, + ); + emit_aarch64_owner_kind_body( + emitter, + enum_backed_case_label, + &layouts.enum_backed_case, + true, + fail_label, + box_label, + ); emitter.label(box_label); emitter.instruction("mov x0, #6"); // runtime tag 6 = object emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload @@ -237,12 +293,54 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds - emit_x86_64_owner_kind_body(emitter, class_label, &layouts.class, true, fail_label, box_label); - emit_x86_64_owner_kind_body(emitter, method_label, &layouts.method, true, fail_label, box_label); - emit_x86_64_owner_kind_body(emitter, property_label, &layouts.property, true, fail_label, box_label); - emit_x86_64_owner_kind_body(emitter, class_constant_label, &layouts.class_constant, true, fail_label, box_label); - emit_x86_64_owner_kind_body(emitter, enum_unit_case_label, &layouts.enum_unit_case, true, fail_label, box_label); - emit_x86_64_owner_kind_body(emitter, enum_backed_case_label, &layouts.enum_backed_case, true, fail_label, box_label); + emit_x86_64_owner_kind_body( + emitter, + class_label, + &layouts.class, + true, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + method_label, + &layouts.method, + true, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + property_label, + &layouts.property, + true, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + class_constant_label, + &layouts.class_constant, + true, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + enum_unit_case_label, + &layouts.enum_unit_case, + true, + fail_label, + box_label, + ); + emit_x86_64_owner_kind_body( + emitter, + enum_backed_case_label, + &layouts.enum_backed_case, + true, + fail_label, + box_label, + ); emitter.label(box_label); emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload emitter.instruction("xor esi, esi"); // object payloads do not use a high word @@ -384,6 +482,24 @@ fn emit_set_owner_class_flags_property_aarch64( let Some(is_abstract_hi) = layout.is_abstract_hi else { return; }; + let Some(is_interface_lo) = layout.is_interface_lo else { + return; + }; + let Some(is_interface_hi) = layout.is_interface_hi else { + return; + }; + let Some(is_trait_lo) = layout.is_trait_lo else { + return; + }; + let Some(is_trait_hi) = layout.is_trait_hi else { + return; + }; + let Some(is_enum_lo) = layout.is_enum_lo else { + return; + }; + let Some(is_enum_hi) = layout.is_enum_hi else { + return; + }; emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean @@ -393,6 +509,18 @@ fn emit_set_owner_class_flags_property_aarch64( emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); + emitter.instruction("lsr x10, x11, #2"); // move the interface bit into position + emitter.instruction("and x10, x10, #1"); // extract the interface flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_interface_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_interface_hi); + emitter.instruction("lsr x10, x11, #3"); // move the trait bit into position + emitter.instruction("and x10, x10, #1"); // extract the trait flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_trait_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_trait_hi); + emitter.instruction("lsr x10, x11, #4"); // move the enum bit into position + emitter.instruction("and x10, x10, #1"); // extract the enum flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_enum_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_enum_hi); } /// Stores incoming x86_64 ReflectionClass boolean modifier flags. @@ -412,6 +540,24 @@ fn emit_set_owner_class_flags_property_x86_64( let Some(is_abstract_hi) = layout.is_abstract_hi else { return; }; + let Some(is_interface_lo) = layout.is_interface_lo else { + return; + }; + let Some(is_interface_hi) = layout.is_interface_hi else { + return; + }; + let Some(is_trait_lo) = layout.is_trait_lo else { + return; + }; + let Some(is_trait_hi) = layout.is_trait_hi else { + return; + }; + let Some(is_enum_lo) = layout.is_enum_lo else { + return; + }; + let Some(is_enum_hi) = layout.is_enum_hi else { + return; + }; emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit @@ -423,6 +569,21 @@ fn emit_set_owner_class_flags_property_x86_64( emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the interface bit + emitter.instruction("shr rax, 2"); // move the interface bit into position + emitter.instruction("and rax, 1"); // extract the interface flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_interface_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_interface_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the trait bit + emitter.instruction("shr rax, 3"); // move the trait bit into position + emitter.instruction("and rax, 1"); // extract the trait flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_trait_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_trait_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the enum bit + emitter.instruction("shr rax, 4"); // move the enum bit into position + emitter.instruction("and rax, 1"); // extract the enum flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_enum_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_enum_hi); } /// Stores a retained ARM64 attribute-array payload into the owner private slot. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index e25120bb3c..629551967d 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -27,6 +27,9 @@ struct ReflectionOwnerMetadata { attr_args: Vec>>, is_final: bool, is_abstract: bool, + is_interface: bool, + is_trait: bool, + is_enum: bool, } /// Returns true for reflection owner classes that need metadata-aware construction. @@ -81,6 +84,9 @@ pub(super) fn lower_reflection_owner_new( if class_name == "ReflectionClass" { emit_reflection_bool_property(ctx, "__is_final", metadata.is_final)?; emit_reflection_bool_property(ctx, "__is_abstract", metadata.is_abstract)?; + emit_reflection_bool_property(ctx, "__is_interface", metadata.is_interface)?; + emit_reflection_bool_property(ctx, "__is_trait", metadata.is_trait)?; + emit_reflection_bool_property(ctx, "__is_enum", metadata.is_enum)?; } let result = inst .result @@ -486,13 +492,23 @@ fn reflection_class_metadata( attr_args: info.attribute_args.clone(), is_final: info.is_final, is_abstract: info.is_abstract, + is_interface: false, + is_trait: false, + is_enum: is_reflection_enum(ctx, class_name), }); } if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { - return Ok(class_like_reflection_metadata(interface_name)); + return Ok(class_like_reflection_metadata( + interface_name, + true, + false, + false, + )); } if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { - return Ok(class_like_reflection_metadata(trait_name)); + return Ok(class_like_reflection_metadata( + trait_name, false, true, false, + )); } Ok(empty_reflection_metadata()) } @@ -519,6 +535,9 @@ fn reflection_method_metadata( attr_args: info.method_attribute_args.get(&method_key)?.clone(), is_final: false, is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, }) }) .unwrap_or_else(empty_reflection_metadata)) @@ -545,6 +564,9 @@ fn reflection_property_metadata( attr_args: info.property_attribute_args.get(&property_name)?.clone(), is_final: false, is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, }) }) .unwrap_or_else(empty_reflection_metadata)) @@ -572,29 +594,37 @@ fn reflection_class_constant_metadata( attr_args: case.attribute_args.clone(), is_final: false, is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, }); } - Ok(resolve_reflection_class_constant(ctx, &reflected_class, &constant_name) - .map(|(_, info)| { - let attr_names = info - .constant_attribute_names - .get(&constant_name) - .cloned() - .unwrap_or_default(); - let attr_args = info - .constant_attribute_args - .get(&constant_name) - .cloned() - .unwrap_or_default(); - ReflectionOwnerMetadata { - reflected_name: Some(constant_name), - attr_names, - attr_args, - is_final: false, - is_abstract: false, - } - }) - .unwrap_or_else(empty_reflection_metadata)) + Ok( + resolve_reflection_class_constant(ctx, &reflected_class, &constant_name) + .map(|(_, info)| { + let attr_names = info + .constant_attribute_names + .get(&constant_name) + .cloned() + .unwrap_or_default(); + let attr_args = info + .constant_attribute_args + .get(&constant_name) + .cloned() + .unwrap_or_default(); + ReflectionOwnerMetadata { + reflected_name: Some(constant_name), + attr_names, + attr_args, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + } + }) + .unwrap_or_else(empty_reflection_metadata), + ) } /// Resolves `ReflectionEnumUnitCase/BackedCase(enum, case)` metadata. @@ -611,15 +641,20 @@ fn reflection_enum_case_metadata( }; let reflected_enum = const_string_or_class_operand(ctx, enum_operand, class_name)?; let case_name = const_required_string_operand(ctx, case_operand, class_name)?; - Ok(resolve_reflection_enum_case(ctx, &reflected_enum, &case_name) - .map(|case| ReflectionOwnerMetadata { - reflected_name: Some(case_name), - attr_names: case.attribute_names.clone(), - attr_args: case.attribute_args.clone(), - is_final: false, - is_abstract: false, - }) - .unwrap_or_else(empty_reflection_metadata)) + Ok( + resolve_reflection_enum_case(ctx, &reflected_enum, &case_name) + .map(|case| ReflectionOwnerMetadata { + reflected_name: Some(case_name.clone()), + attr_names: case.attribute_names.clone(), + attr_args: case.attribute_args.clone(), + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + }) + .unwrap_or_else(empty_reflection_metadata), + ) } /// Looks up class metadata by PHP-style case-insensitive name. @@ -659,14 +694,31 @@ fn resolve_reflection_trait<'a>(ctx: &'a FunctionContext<'_>, trait_name: &str) .map(String::as_str) } +/// Looks up enum metadata by PHP-style case-insensitive name. +fn is_reflection_enum(ctx: &FunctionContext<'_>, enum_name: &str) -> bool { + let enum_key = php_symbol_key(enum_name.trim_start_matches('\\')); + ctx.module + .enum_infos + .keys() + .any(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == enum_key) +} + /// Builds empty ReflectionClass metadata for class-like symbols without stored attributes. -fn class_like_reflection_metadata(class_like_name: &str) -> ReflectionOwnerMetadata { +fn class_like_reflection_metadata( + class_like_name: &str, + is_interface: bool, + is_trait: bool, + is_enum: bool, +) -> ReflectionOwnerMetadata { ReflectionOwnerMetadata { reflected_name: Some(class_like_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), is_final: false, is_abstract: false, + is_interface, + is_trait, + is_enum, } } @@ -706,6 +758,9 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { attr_args: Vec::new(), is_final: false, is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, } } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 21f592b239..7715038bb6 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -561,6 +561,24 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__is_interface", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_trait", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_enum", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![( @@ -572,6 +590,9 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_get_name_method(), builtin_reflection_class_bool_method("isFinal", "__is_final"), builtin_reflection_class_bool_method("isAbstract", "__is_abstract"), + builtin_reflection_class_bool_method("isInterface", "__is_interface"), + builtin_reflection_class_bool_method("isTrait", "__is_trait"), + builtin_reflection_class_bool_method("isEnum", "__is_enum"), builtin_reflection_owner_get_attributes_method(), ], attributes: Vec::new(), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 50258c310b..6e3bfe46d1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5551,15 +5551,30 @@ interface EvalIfaceReflect {} trait EvalTraitReflect {} enum EvalEnumReflect { case Ready; } echo (new ReflectionClass("EvalAbstractReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalAbstractReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalAbstractReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalAbstractReflect"))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass("EvalFinalReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalFinalReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalFinalReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalFinalReflect"))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass("EvalEnumReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalEnumReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalEnumReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalEnumReflect"))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass("EvalIfaceReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalIfaceReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalIfaceReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalIfaceReflect"))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass("EvalTraitReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f";'); +echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalTraitReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalTraitReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalTraitReflect"))->isEnum() ? "E" : "e";'); "#, ); assert!( @@ -5567,7 +5582,7 @@ echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f";'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "Af:aF:aF:af:af"); + assert_eq!(out.stdout, "Afite:aFite:aFitE:afIte:afiTe"); } /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 8279e05cf3..f7ab5eb878 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1114,25 +1114,43 @@ trait StaticTraitReflect {} enum StaticEnumReflect { case Ready; } echo (new ReflectionClass(StaticAbstractReflect::class))->isAbstract() ? "A" : "a"; echo (new ReflectionClass(StaticAbstractReflect::class))->isFinal() ? "F" : "f"; +echo (new ReflectionClass(StaticAbstractReflect::class))->isInterface() ? "I" : "i"; +echo (new ReflectionClass(StaticAbstractReflect::class))->isTrait() ? "T" : "t"; +echo (new ReflectionClass(StaticAbstractReflect::class))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass(StaticFinalReflect::class))->isAbstract() ? "A" : "a"; echo (new ReflectionClass(StaticFinalReflect::class))->isFinal() ? "F" : "f"; +echo (new ReflectionClass(StaticFinalReflect::class))->isInterface() ? "I" : "i"; +echo (new ReflectionClass(StaticFinalReflect::class))->isTrait() ? "T" : "t"; +echo (new ReflectionClass(StaticFinalReflect::class))->isEnum() ? "E" : "e"; echo ":"; echo (new ReflectionClass(StaticEnumReflect::class))->isAbstract() ? "A" : "a"; echo (new ReflectionClass(StaticEnumReflect::class))->isFinal() ? "F" : "f"; +echo (new ReflectionClass(StaticEnumReflect::class))->isInterface() ? "I" : "i"; +echo (new ReflectionClass(StaticEnumReflect::class))->isTrait() ? "T" : "t"; +echo (new ReflectionClass(StaticEnumReflect::class))->isEnum() ? "E" : "e"; echo ":"; $iface = new ReflectionClass("staticifacereflect"); echo $iface->getName() . "/"; echo $iface->isAbstract() ? "A" : "a"; echo $iface->isFinal() ? "F" : "f"; +echo $iface->isInterface() ? "I" : "i"; +echo $iface->isTrait() ? "T" : "t"; +echo $iface->isEnum() ? "E" : "e"; echo ":"; $trait = new ReflectionClass("STATICTRAITREFLECT"); echo $trait->getName() . "/"; echo $trait->isAbstract() ? "A" : "a"; echo $trait->isFinal() ? "F" : "f"; +echo $trait->isInterface() ? "I" : "i"; +echo $trait->isTrait() ? "T" : "t"; +echo $trait->isEnum() ? "E" : "e"; "#, ); - assert_eq!(out, "Af:aF:aF:StaticIfaceReflect/af:StaticTraitReflect/af"); + assert_eq!( + out, + "Afite:aFite:aFitE:StaticIfaceReflect/afIte:StaticTraitReflect/afiTe" + ); } /// Verifies that `ReflectionClass::getName()` returns the canonical declared From 155467873b5ab08051f7c8c29f69b5ad1278adf7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 21:59:08 +0200 Subject: [PATCH 0357/1208] Support ReflectionClass name part APIs --- .../tests/builtins_class_metadata.rs | 23 +++ .../interpreter/tests/support/object_ops.rs | 31 ++++ docs/php/classes.md | 5 +- docs/php/eval.md | 13 +- src/codegen/eval_reflection_owner_helpers.rs | 143 +++++++++++++++++- src/codegen/lower_inst/objects/reflection.rs | 42 +++++ src/types/checker/builtin_types/reflection.rs | 34 ++++- tests/codegen/eval.rs | 22 +++ tests/codegen/oop/attributes.rs | 14 +- 9 files changed, 309 insertions(+), 18 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 08f8eba6fe..8aab5237e0 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -202,6 +202,29 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass exposes eval class namespace-derived name parts. +#[test] +fn execute_program_reflects_eval_class_name_parts() { + let program = parse_fragment( + br#"namespace Eval\Ns; +class Thing {} +$ref = new \ReflectionClass("Eval\\Ns\\Thing"); +echo $ref->getName(); echo ":"; +echo $ref->getShortName(); echo ":"; +echo $ref->getNamespaceName(); echo ":"; +echo $ref->inNamespace() ? "Y" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Eval\\Ns\\Thing:Thing:Eval\\Ns:Y"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class-like final and abstract flags. #[test] fn execute_program_reflects_eval_class_modifier_flags() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index ea78444fa1..69f4f31960 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -102,6 +102,18 @@ impl FakeOps { (FakeValue::Object(properties), "getname") if args.is_empty() => { Self::object_property(&properties, "__name").map_or_else(|| self.string(""), Ok) } + (FakeValue::Object(properties), "getshortname") if args.is_empty() => { + Self::object_property(&properties, "__short_name") + .map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "getnamespacename") if args.is_empty() => { + Self::object_property(&properties, "__namespace_name") + .map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "innamespace") if args.is_empty() => { + Self::object_property(&properties, "__in_namespace") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "isfinal") if args.is_empty() => { Self::object_property(&properties, "__is_final") .map_or_else(|| self.bool_value(false), Ok) @@ -256,11 +268,19 @@ impl FakeOps { let is_enum = self.bool_value((flags & 16) != 0)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + let (namespace_name, short_name) = reflection_name_parts(reflected_name); + let has_namespace = !namespace_name.is_empty(); + let namespace_name = self.string(namespace_name)?; + let short_name = self.string(short_name)?; + let in_namespace = self.bool_value(has_namespace)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_interface".to_string(), is_interface)); properties.push(("__is_trait".to_string(), is_trait)); properties.push(("__is_enum".to_string(), is_enum)); + properties.push(("__short_name".to_string(), short_name)); + properties.push(("__namespace_name".to_string(), namespace_name)); + properties.push(("__in_namespace".to_string(), in_namespace)); } let object = self.alloc(FakeValue::Object(properties)); self.object_classes @@ -409,6 +429,17 @@ fn fake_runtime_exception_like_class(class_name: &str) -> bool { .any(|known| class_name.eq_ignore_ascii_case(known)) } +/// Splits one PHP class-like name into namespace and short-name parts. +fn reflection_name_parts(reflected_name: &str) -> (&str, &str) { + match reflected_name.rfind('\\') { + Some(separator) => ( + &reflected_name[..separator], + &reflected_name[separator + 1..], + ), + None => ("", reflected_name), + } +} + /// Checks the small fake Throwable inheritance graph used by eval interpreter tests. fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: bool) -> bool { if class_name.eq_ignore_ascii_case(target_class) { diff --git a/docs/php/classes.md b/docs/php/classes.md index d6aa92a85c..0c3be98f5e 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1081,6 +1081,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | Reflection method | Supported constructor | Description | |---|---|---| | `ReflectionClass::getName()` | `new ReflectionClass($class_name)` | Return the resolved class-like name | +| `ReflectionClass::getShortName()` | `new ReflectionClass($class_name)` | Return the class-like name after the final namespace separator | +| `ReflectionClass::getNamespaceName()` | `new ReflectionClass($class_name)` | Return the namespace portion of the reflected class-like name | +| `ReflectionClass::inNamespace()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is namespaced | | `ReflectionClass::isFinal()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is final | | `ReflectionClass::isAbstract()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is abstract | | `ReflectionClass::isInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an interface | @@ -1149,7 +1152,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, and `isEnum()`. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, and `isEnum()`. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index b1281dd454..1b102a6d2f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -152,11 +152,14 @@ syntax, but requesting those arguments is a runtime fatal. `ReflectionProperty::getAttributes()` expose eval-retained class, method, and property attributes for eval-declared class-like symbols when their arguments fit the same literal subset, and `getName()` returns the reflected class or -member name for those owners. `ReflectionClass::isFinal()`, -`ReflectionClass::isAbstract()`, `ReflectionClass::isInterface()`, -`ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval -class-like metadata, including PHP-compatible enum finality and class-like kind -checks for eval interfaces, traits, and enums. `ReflectionClassConstant::getAttributes()`, +member name for those owners. `ReflectionClass::getShortName()`, +`ReflectionClass::getNamespaceName()`, and `ReflectionClass::inNamespace()` +derive namespace-aware parts from the resolved eval class-like name. +`ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, +`ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and +`ReflectionClass::isEnum()` report eval class-like metadata, including +PHP-compatible enum finality and class-like kind checks for eval interfaces, +traits, and enums. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant and enum-case attributes through the same materialized `ReflectionAttribute` diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 9b0d574191..734392cad5 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -26,6 +26,10 @@ struct ReflectionOwnerLayout { property_count: usize, name_lo: Option, name_hi: Option, + short_name_lo: Option, + short_name_hi: Option, + namespace_name_lo: Option, + namespace_name_hi: Option, attrs_lo: usize, attrs_hi: usize, is_final_lo: Option, @@ -38,6 +42,8 @@ struct ReflectionOwnerLayout { is_trait_hi: Option, is_enum_lo: Option, is_enum_hi: Option, + in_namespace_lo: Option, + in_namespace_hi: Option, } /// Layouts for the Reflection owner classes eval can materialize. @@ -123,16 +129,23 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, + reflected_name: &str, +) -> Result<()> { + let (namespace_name, short_name) = reflection_name_parts(reflected_name); + emit_reflection_string_property_by_name(ctx, "__short_name", short_name)?; + emit_reflection_string_property_by_name(ctx, "__namespace_name", namespace_name)?; + emit_reflection_bool_property(ctx, "__in_namespace", !namespace_name.is_empty())?; + Ok(()) +} + +/// Splits a canonical PHP class-like name into namespace and short-name parts. +fn reflection_name_parts(reflected_name: &str) -> (&str, &str) { + match reflected_name.rfind('\\') { + Some(separator) => ( + &reflected_name[..separator], + &reflected_name[separator + 1..], + ), + None => ("", reflected_name), + } +} + /// Resolves Reflection constructor operands to captured class/member metadata. fn reflection_owner_metadata( ctx: &FunctionContext<'_>, @@ -864,6 +890,22 @@ fn emit_reflection_string_property( abi::emit_pop_reg(ctx.emitter, result_reg); } +/// Writes a heap-persisted string into a named ReflectionClass property slot. +fn emit_reflection_string_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + value: &str, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + emit_reflection_string_property(ctx, value, low_offset, low_offset + 8); + Ok(()) +} + /// Replaces the Reflection object's default `__attrs` array with populated metadata. fn emit_reflection_attrs_property( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 7715038bb6..cbe3ed517f 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -579,6 +579,24 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__short_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__namespace_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__in_namespace", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![( @@ -587,7 +605,10 @@ fn builtin_reflection_class() -> FlattenedClass { None, false, )]), - builtin_reflection_class_get_name_method(), + builtin_reflection_class_string_method("getName", "__name"), + builtin_reflection_class_string_method("getShortName", "__short_name"), + builtin_reflection_class_string_method("getNamespaceName", "__namespace_name"), + builtin_reflection_class_bool_method("inNamespace", "__in_namespace"), builtin_reflection_class_bool_method("isFinal", "__is_final"), builtin_reflection_class_bool_method("isAbstract", "__is_abstract"), builtin_reflection_class_bool_method("isInterface", "__is_interface"), @@ -601,12 +622,11 @@ fn builtin_reflection_class() -> FlattenedClass { } } -/// Returns a public `ReflectionClass::getName()` method that returns the -/// resolved reflected class name from the private `__name` slot. -fn builtin_reflection_class_get_name_method() -> ClassMethod { +/// Returns a public `ReflectionClass` string method backed by one private slot. +fn builtin_reflection_class_string_method(method_name: &str, property: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); ClassMethod { - name: "getName".to_string(), + name: method_name.to_string(), visibility: Visibility::Public, is_static: false, is_abstract: false, @@ -621,7 +641,7 @@ fn builtin_reflection_class_get_name_method() -> ClassMethod { StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { object: Box::new(Expr::new(ExprKind::This, dummy_span)), - property: "__name".to_string(), + property: property.to_string(), }, dummy_span, ))), @@ -681,7 +701,7 @@ fn builtin_reflection_owner_class( Some(TypeExpr::Str), empty_string(), )); - methods.push(builtin_reflection_class_get_name_method()); + methods.push(builtin_reflection_class_string_method("getName", "__name")); } properties.push(builtin_property( "__attrs", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6e3bfe46d1..b6d8fbc105 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5540,6 +5540,28 @@ echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance ); } +/// Verifies eval ReflectionClass exposes namespace-derived class-name parts. +#[test] +fn test_eval_reflection_class_name_parts() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $ref->getShortName() . ":"; +echo $ref->getNamespaceName() . ":"; +echo $ref->inNamespace() ? "Y" : "N";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Eval\\Ns\\Thing:Thing:Eval\\Ns:Y"); +} + /// Verifies eval ReflectionClass reports class-like final and abstract flags. #[test] fn test_eval_reflection_class_modifier_flags() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index f7ab5eb878..57b4969c34 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1096,10 +1096,13 @@ fn test_reflection_class_get_name_returns_class_name() { r#"getName(); +echo $ref->getName() . ":"; +echo $ref->getShortName() . ":"; +echo $ref->getNamespaceName() . ":"; +echo $ref->inNamespace() ? "Y" : "N"; "#, ); - assert_eq!(out, "Plain"); + assert_eq!(out, "Plain:Plain::N"); } /// Verifies that `ReflectionClass` reports final and abstract flags for static metadata. @@ -1162,10 +1165,13 @@ fn test_reflection_class_get_name_uses_resolved_class_case() { namespace Demo; class Thing {} $ref = new \ReflectionClass('demo\thing'); -echo $ref->getName(); +echo $ref->getName() . ":"; +echo $ref->getShortName() . ":"; +echo $ref->getNamespaceName() . ":"; +echo $ref->inNamespace() ? "Y" : "N"; "#, ); - assert_eq!(out, "Demo\\Thing"); + assert_eq!(out, "Demo\\Thing:Thing:Demo:Y"); } /// Verifies that `ReflectionClass::getName()` works for a class discovered From 58fca1a2ff6c62d32c1238512a5404dbd3d39a9a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 22:19:19 +0200 Subject: [PATCH 0358/1208] Support ReflectionClass relation name APIs --- .../elephc-eval/src/interpreter/reflection.rs | 121 +++++++++---- .../src/interpreter/runtime_ops.rs | 2 + .../tests/builtins_class_metadata.rs | 33 ++++ .../interpreter/tests/support/object_ops.rs | 12 ++ .../interpreter/tests/support/runtime_ops.rs | 11 +- .../elephc-eval/src/runtime_hooks/externs.rs | 2 + crates/elephc-eval/src/runtime_hooks/ops.rs | 4 + docs/php/classes.md | 4 +- docs/php/eval.md | 5 +- src/codegen/eval_reflection_owner_helpers.rs | 163 +++++++++++++++-- src/codegen/lower_inst/objects/reflection.rs | 165 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 53 ++++++ tests/codegen/eval.rs | 32 ++++ tests/codegen/oop/attributes.rs | 27 +++ 14 files changed, 589 insertions(+), 45 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 00395c4ed8..7c64310d09 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -18,6 +18,15 @@ const EVAL_REFLECTION_CLASS_FLAG_INTERFACE: u64 = 4; const EVAL_REFLECTION_CLASS_FLAG_TRAIT: u64 = 8; const EVAL_REFLECTION_CLASS_FLAG_ENUM: u64 = 16; +/// Eval metadata needed to materialize one `ReflectionClass` owner object. +struct EvalReflectionClassMetadata { + resolved_name: String, + attributes: Vec, + flags: u64, + interface_names: Vec, + trait_names: Vec, +} + /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. pub(in crate::interpreter) fn eval_reflection_owner_new_object( class_name: &str, @@ -63,16 +72,16 @@ fn eval_reflection_class_new( ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; let class_name = eval_reflection_string_arg(args[0], values)?; - let Some((resolved_name, attributes, flags)) = - eval_reflection_class_like_attributes(&class_name, context) - else { + let Some(metadata) = eval_reflection_class_like_attributes(&class_name, context) else { return Ok(None); }; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, - &resolved_name, - &attributes, - flags, + &metadata.resolved_name, + &metadata.attributes, + &metadata.interface_names, + &metadata.trait_names, + metadata.flags, context, values, ) @@ -100,6 +109,8 @@ fn eval_reflection_method_new( EVAL_REFLECTION_OWNER_METHOD, &method_name, &attributes, + &[], + &[], 0, context, values, @@ -128,6 +139,8 @@ fn eval_reflection_property_new( EVAL_REFLECTION_OWNER_PROPERTY, &property_name, &attributes, + &[], + &[], 0, context, values, @@ -157,6 +170,8 @@ fn eval_reflection_class_constant_new( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, &constant_name, &attributes, + &[], + &[], 0, context, values, @@ -191,7 +206,17 @@ fn eval_reflection_enum_case_new( .case(&case_name) .map(|case| case.attributes().to_vec()) .ok_or(EvalStatus::RuntimeFatal)?; - eval_reflection_owner_object(owner_kind, &case_name, &attributes, 0, context, values).map(Some) + eval_reflection_owner_object( + owner_kind, + &case_name, + &attributes, + &[], + &[], + 0, + context, + values, + ) + .map(Some) } /// Materializes one Reflection owner object and transfers the temporary attribute array. @@ -199,21 +224,49 @@ fn eval_reflection_owner_object( owner_kind: u64, reflected_name: &str, attributes: &[EvalAttribute], + interface_names: &[String], + trait_names: &[String], flags: u64, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let attrs = eval_reflection_attribute_array_result(attributes, context, values)?; - let object = values.reflection_owner_new(owner_kind, reflected_name, attrs, flags)?; + let interface_names = eval_reflection_string_array_result(interface_names, values)?; + let trait_names = eval_reflection_string_array_result(trait_names, values)?; + let object = values.reflection_owner_new( + owner_kind, + reflected_name, + attrs, + interface_names, + trait_names, + flags, + )?; values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; Ok(object) } +/// Builds an indexed PHP string array for ReflectionClass relation metadata. +fn eval_reflection_string_array_result( + names: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Returns the eval-retained class-like attributes plus canonical reflected name. fn eval_reflection_class_like_attributes( name: &str, context: &ElephcEvalContext, -) -> Option<(String, Vec, u64)> { +) -> Option { if let Some(class) = context.class(name) { let mut flags = 0; if class.is_final() { @@ -225,33 +278,41 @@ fn eval_reflection_class_like_attributes( if context.has_enum(class.name()) { flags |= EVAL_REFLECTION_CLASS_FLAG_ENUM; } - return Some(( - class.name().trim_start_matches('\\').to_string(), - class.attributes().to_vec(), + return Some(EvalReflectionClassMetadata { + resolved_name: class.name().trim_start_matches('\\').to_string(), + attributes: class.attributes().to_vec(), + interface_names: context.class_interface_names(class.name()), + trait_names: context.class_trait_names(class.name()), flags, - )); + }); } if let Some(interface) = context.interface(name) { - return Some(( - interface.name().trim_start_matches('\\').to_string(), - interface.attributes().to_vec(), - EVAL_REFLECTION_CLASS_FLAG_INTERFACE, - )); + return Some(EvalReflectionClassMetadata { + resolved_name: interface.name().trim_start_matches('\\').to_string(), + attributes: interface.attributes().to_vec(), + interface_names: context.interface_parent_names(interface.name()), + trait_names: Vec::new(), + flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE, + }); } if let Some(trait_decl) = context.trait_decl(name) { - return Some(( - trait_decl.name().trim_start_matches('\\').to_string(), - trait_decl.attributes().to_vec(), - EVAL_REFLECTION_CLASS_FLAG_TRAIT, - )); + return Some(EvalReflectionClassMetadata { + resolved_name: trait_decl.name().trim_start_matches('\\').to_string(), + attributes: trait_decl.attributes().to_vec(), + interface_names: Vec::new(), + trait_names: Vec::new(), + flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT, + }); } - context.enum_decl(name).map(|enum_decl| { - ( - enum_decl.name().trim_start_matches('\\').to_string(), - enum_decl.attributes().to_vec(), - EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM, - ) - }) + context + .enum_decl(name) + .map(|enum_decl| EvalReflectionClassMetadata { + resolved_name: enum_decl.name().trim_start_matches('\\').to_string(), + attributes: enum_decl.attributes().to_vec(), + interface_names: context.class_interface_names(enum_decl.name()), + trait_names: Vec::new(), + flags: EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM, + }) } /// Returns attributes attached to an eval class constant or enum case. diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index e0e253d4f4..98077816be 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -108,6 +108,8 @@ pub trait RuntimeValueOps { owner_kind: u64, reflected_name: &str, attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, flags: u64, ) -> Result; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 8aab5237e0..0f76f2d116 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -225,6 +225,39 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass exposes eval interface and trait relation names. +#[test] +fn execute_program_reflects_eval_class_relation_names() { + let program = parse_fragment( + br#"interface EvalRelationIface {} +trait EvalRelationTrait {} +class EvalRelationTarget implements EvalRelationIface { + use EvalRelationTrait; +} +interface EvalRelationParent {} +interface EvalRelationChild extends EvalRelationParent {} +$ref = new ReflectionClass("EvalRelationTarget"); +$interfaces = $ref->getInterfaceNames(); +$traits = $ref->getTraitNames(); +echo count($interfaces); echo ":"; echo $interfaces[0]; echo ":"; +echo count($traits); echo ":"; echo $traits[0]; echo ":"; +$parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); +echo count($parentInterfaces); echo ":"; echo $parentInterfaces[0]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class-like final and abstract flags. #[test] fn execute_program_reflects_eval_class_modifier_flags() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 69f4f31960..5a798df831 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -134,6 +134,14 @@ impl FakeOps { Self::object_property(&properties, "__is_enum") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "getinterfacenames") if args.is_empty() => { + Self::object_property(&properties, "__interface_names") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "gettraitnames") if args.is_empty() => { + Self::object_property(&properties, "__trait_names") + .map_or_else(|| self.runtime_array_new(0), Ok) + } (FakeValue::Object(properties), "getarguments") if args.is_empty() => { Self::object_property(&properties, "__args") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -249,6 +257,8 @@ impl FakeOps { owner_kind: u64, reflected_name: &str, attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, flags: u64, ) -> Result { let class_name = match owner_kind { @@ -281,6 +291,8 @@ impl FakeOps { properties.push(("__short_name".to_string(), short_name)); properties.push(("__namespace_name".to_string(), namespace_name)); properties.push(("__in_namespace".to_string(), in_namespace)); + properties.push(("__interface_names".to_string(), interface_names)); + properties.push(("__trait_names".to_string(), trait_names)); } let object = self.alloc(FakeValue::Object(properties)); self.object_classes diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 1995acb4f9..6eade4e4db 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -115,9 +115,18 @@ impl RuntimeValueOps for FakeOps { owner_kind: u64, reflected_name: &str, attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, flags: u64, ) -> Result { - self.runtime_reflection_owner_new(owner_kind, reflected_name, attrs, flags) + self.runtime_reflection_owner_new( + owner_kind, + reflected_name, + attrs, + interface_names, + trait_names, + flags, + ) } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 8a1502553e..27a2fab8c7 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -73,6 +73,8 @@ unsafe extern "C" { name_ptr: *const u8, name_len: u64, attrs: *mut RuntimeCell, + interface_names: *mut RuntimeCell, + trait_names: *mut RuntimeCell, flags: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 436a1cedda..4b43fd193c 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -184,6 +184,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { owner_kind: u64, reflected_name: &str, attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, flags: u64, ) -> Result { Self::handle(unsafe { @@ -192,6 +194,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { reflected_name.as_ptr(), reflected_name.len() as u64, attrs.as_ptr(), + interface_names.as_ptr(), + trait_names.as_ptr(), flags, ) }) diff --git a/docs/php/classes.md b/docs/php/classes.md index 0c3be98f5e..718f6d15ee 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1089,6 +1089,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an interface | | `ReflectionClass::isTrait()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is a trait | | `ReflectionClass::isEnum()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an enum | +| `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | +| `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | @@ -1152,7 +1154,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, and `isEnum()`. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `getInterfaceNames()`, and `getTraitNames()`. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 1b102a6d2f..21749451a6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -159,7 +159,10 @@ derive namespace-aware parts from the resolved eval class-like name. `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval class-like metadata, including PHP-compatible enum finality and class-like kind checks for eval interfaces, -traits, and enums. `ReflectionClassConstant::getAttributes()`, +traits, and enums. `ReflectionClass::getInterfaceNames()` returns implemented +interfaces for eval classes and parent interfaces for eval interfaces, while +`ReflectionClass::getTraitNames()` returns traits used directly by eval classes. +`ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant and enum-case attributes through the same materialized `ReflectionAttribute` diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 734392cad5..80e263c900 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -8,9 +8,9 @@ //! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. //! //! Key details: -//! - Reflection owner objects store `__attrs`; owners with public `getName()` -//! also store `__name`; both slots are private implementation details. -//! - The helper retains the supplied attribute array payload for object ownership. +//! - Reflection owner objects store private metadata slots such as `__attrs`, +//! `__name`, and the ReflectionClass relation-name arrays. +//! - The helper retains supplied array payloads for object ownership. use crate::codegen::abi; use crate::codegen::emit::Emitter; @@ -30,6 +30,10 @@ struct ReflectionOwnerLayout { short_name_hi: Option, namespace_name_lo: Option, namespace_name_hi: Option, + interface_names_lo: Option, + interface_names_hi: Option, + trait_names_lo: Option, + trait_names_hi: Option, attrs_lo: usize, attrs_hi: usize, is_final_lo: Option, @@ -131,6 +135,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, attr_names: Vec, attr_args: Vec>>, + interface_names: Vec, + trait_names: Vec, is_final: bool, is_abstract: bool, is_interface: bool, @@ -76,6 +78,16 @@ pub(super) fn lower_reflection_owner_new( emit_reflection_string_property(ctx, reflected_name, 8, 16); if class_name == "ReflectionClass" { emit_reflection_class_name_parts(ctx, reflected_name)?; + emit_reflection_string_array_property_by_name( + ctx, + "__interface_names", + &metadata.interface_names, + )?; + emit_reflection_string_array_property_by_name( + ctx, + "__trait_names", + &metadata.trait_names, + )?; } } emit_reflection_attrs_property( @@ -516,6 +528,8 @@ fn reflection_class_metadata( reflected_name: Some(class_name.to_string()), attr_names: info.attribute_names.clone(), attr_args: info.attribute_args.clone(), + interface_names: info.interfaces.clone(), + trait_names: info.used_traits.clone(), is_final: info.is_final, is_abstract: info.is_abstract, is_interface: false, @@ -526,14 +540,27 @@ fn reflection_class_metadata( if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { return Ok(class_like_reflection_metadata( interface_name, + reflection_interface_parent_names(ctx, interface_name), + Vec::new(), true, false, false, )); } if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { + let trait_names = ctx + .module + .declared_trait_uses + .get(trait_name) + .cloned() + .unwrap_or_default(); return Ok(class_like_reflection_metadata( - trait_name, false, true, false, + trait_name, + Vec::new(), + trait_names, + false, + true, + false, )); } Ok(empty_reflection_metadata()) @@ -559,6 +586,8 @@ fn reflection_method_metadata( reflected_name: Some(method_name.clone()), attr_names: info.method_attribute_names.get(&method_key)?.clone(), attr_args: info.method_attribute_args.get(&method_key)?.clone(), + interface_names: Vec::new(), + trait_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -588,6 +617,8 @@ fn reflection_property_metadata( reflected_name: Some(property_name.clone()), attr_names: info.property_attribute_names.get(&property_name)?.clone(), attr_args: info.property_attribute_args.get(&property_name)?.clone(), + interface_names: Vec::new(), + trait_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -618,6 +649,8 @@ fn reflection_class_constant_metadata( reflected_name: Some(constant_name), attr_names: case.attribute_names.clone(), attr_args: case.attribute_args.clone(), + interface_names: Vec::new(), + trait_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -642,6 +675,8 @@ fn reflection_class_constant_metadata( reflected_name: Some(constant_name), attr_names, attr_args, + interface_names: Vec::new(), + trait_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -673,6 +708,8 @@ fn reflection_enum_case_metadata( reflected_name: Some(case_name.clone()), attr_names: case.attribute_names.clone(), attr_args: case.attribute_args.clone(), + interface_names: Vec::new(), + trait_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -729,9 +766,44 @@ fn is_reflection_enum(ctx: &FunctionContext<'_>, enum_name: &str) -> bool { .any(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == enum_key) } +/// Collects direct and inherited parent interfaces for a reflected interface. +fn reflection_interface_parent_names( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let mut names = Vec::new(); + collect_reflection_interface_parent_names(ctx, interface_name, &mut names); + names +} + +/// Recursively collects interface parents without duplicating case-insensitive names. +fn collect_reflection_interface_parent_names( + ctx: &FunctionContext<'_>, + interface_name: &str, + names: &mut Vec, +) { + let Some(interface) = ctx.module.interface_infos.get(interface_name) else { + return; + }; + for parent in &interface.parents { + let parent_name = resolve_reflection_interface(ctx, parent) + .map(str::to_string) + .unwrap_or_else(|| parent.clone()); + if !names + .iter() + .any(|name| php_symbol_key(name) == php_symbol_key(&parent_name)) + { + names.push(parent_name.clone()); + collect_reflection_interface_parent_names(ctx, &parent_name, names); + } + } +} + /// Builds empty ReflectionClass metadata for class-like symbols without stored attributes. fn class_like_reflection_metadata( class_like_name: &str, + interface_names: Vec, + trait_names: Vec, is_interface: bool, is_trait: bool, is_enum: bool, @@ -740,6 +812,8 @@ fn class_like_reflection_metadata( reflected_name: Some(class_like_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), + interface_names, + trait_names, is_final: false, is_abstract: false, is_interface, @@ -782,6 +856,8 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { reflected_name: None, attr_names: Vec::new(), attr_args: Vec::new(), + interface_names: Vec::new(), + trait_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -937,6 +1013,93 @@ fn emit_reflection_attrs_property( Ok(()) } +/// Replaces a ReflectionClass private array slot with an indexed string array. +fn emit_reflection_string_array_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + names: &[String], +) -> Result<()> { + if names.is_empty() { + return Ok(()); + } + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_string_array(ctx, names)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Allocates an indexed string array containing ReflectionClass relation names. +fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) -> Result<()> { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_int_immediate(ctx.emitter, "x0", names.len() as i64); + abi::emit_load_int_immediate(ctx.emitter, "x1", 16); + } + Arch::X86_64 => { + abi::emit_load_int_immediate(ctx.emitter, "rdi", names.len() as i64); + abi::emit_load_int_immediate(ctx.emitter, "rsi", 16); + } + } + abi::emit_call_label(ctx.emitter, "__rt_array_new"); + match ctx.emitter.target.arch { + Arch::AArch64 => emit_reflection_string_array_fill_aarch64(ctx, names), + Arch::X86_64 => emit_reflection_string_array_fill_x86_64(ctx, names), + } + Ok(()) +} + +/// Appends ReflectionClass relation names to the current ARM64 result array. +fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the relation-name array while appending strings + for name in names { + let (label, len) = ctx.data.add_string(name.as_bytes()); + ctx.emitter.instruction("ldr x0, [sp]"); // reload the relation-name array for this append + abi::emit_symbol_address(ctx.emitter, "x1", &label); + abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); + abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown relation-name array + } + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final relation-name array as the result +} + +/// Appends ReflectionClass relation names to the current x86_64 result array. +fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { + ctx.emitter.instruction("push rax"); // park the relation-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + for name in names { + let (label, len) = ctx.data.add_string(name.as_bytes()); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the relation-name array for this append + abi::emit_symbol_address(ctx.emitter, "rsi", &label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); + abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown relation-name array + } + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final relation-name array as the result +} + /// Stores one boolean property on the current ReflectionClass object result. fn emit_reflection_bool_property( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index cbe3ed517f..5f35ca33be 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -227,6 +227,11 @@ fn array_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("array")) } +/// Returns a `TypeExpr` for an indexed array of strings. +fn string_array_type() -> TypeExpr { + TypeExpr::Array(Box::new(TypeExpr::Str)) +} + /// Returns a `TypeExpr` for the unqualified name `mixed`. fn mixed_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("mixed")) @@ -597,6 +602,18 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__interface_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__trait_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![( @@ -609,6 +626,8 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_string_method("getShortName", "__short_name"), builtin_reflection_class_string_method("getNamespaceName", "__namespace_name"), builtin_reflection_class_bool_method("inNamespace", "__in_namespace"), + builtin_reflection_class_array_method("getInterfaceNames", "__interface_names"), + builtin_reflection_class_array_method("getTraitNames", "__trait_names"), builtin_reflection_class_bool_method("isFinal", "__is_final"), builtin_reflection_class_bool_method("isAbstract", "__is_abstract"), builtin_reflection_class_bool_method("isInterface", "__is_interface"), @@ -652,6 +671,35 @@ fn builtin_reflection_class_string_method(method_name: &str, property: &str) -> } } +/// Returns a public `ReflectionClass` array method backed by one private slot. +fn builtin_reflection_class_array_method(method_name: &str, property: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(string_array_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass` boolean method backed by one private slot. fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -840,6 +888,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Bool; } } + for method_name in ["getinterfacenames", "gettraitnames"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Array(Box::new(PhpType::Str)); + } + } } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b6d8fbc105..8be872ef6a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5562,6 +5562,38 @@ echo $ref->inNamespace() ? "Y" : "N";'); assert_eq!(out.stdout, "Eval\\Ns\\Thing:Thing:Eval\\Ns:Y"); } +/// Verifies eval ReflectionClass exposes implemented interface and used trait names. +#[test] +fn test_eval_reflection_class_relation_names() { + let out = compile_and_run_capture( + r#"getInterfaceNames(); +$traits = $ref->getTraitNames(); +echo count($interfaces) . ":" . $interfaces[0] . ":"; +echo count($traits) . ":" . $traits[0] . ":"; +$parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); +echo count($parentInterfaces) . ":" . $parentInterfaces[0];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent" + ); +} + /// Verifies eval ReflectionClass reports class-like final and abstract flags. #[test] fn test_eval_reflection_class_modifier_flags() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 57b4969c34..4480bc2f1e 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1156,6 +1156,33 @@ echo $trait->isEnum() ? "E" : "e"; ); } +/// Verifies that `ReflectionClass` reports implemented interface and used trait names. +#[test] +fn test_reflection_class_reports_relation_names() { + let out = compile_and_run( + r#"getInterfaceNames(); +$traits = $ref->getTraitNames(); +echo count($interfaces) . ":" . $interfaces[0] . ":"; +echo count($traits) . ":" . $traits[0] . ":"; +$parentInterfaces = (new ReflectionClass(StaticRelationChild::class))->getInterfaceNames(); +echo count($parentInterfaces) . ":" . $parentInterfaces[0]; +"#, + ); + assert_eq!( + out, + "1:StaticRelationIface:1:StaticRelationTrait:1:StaticRelationParent" + ); +} + /// Verifies that `ReflectionClass::getName()` returns the canonical declared /// name after case-insensitive class-string construction. #[test] From 743397fe04f210170c43ec473407fee9f0b93ac6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 22:31:53 +0200 Subject: [PATCH 0359/1208] Support ReflectionClass modifier bitmask API --- .../elephc-eval/src/interpreter/reflection.rs | 41 +++++++++++- .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 30 +++++++++ .../interpreter/tests/support/object_ops.rs | 6 ++ .../interpreter/tests/support/runtime_ops.rs | 2 + .../elephc-eval/src/runtime_hooks/externs.rs | 1 + crates/elephc-eval/src/runtime_hooks/ops.rs | 2 + docs/php/classes.md | 3 +- docs/php/eval.md | 6 +- src/codegen/eval_reflection_owner_helpers.rs | 26 +++++++ src/codegen/lower_inst/objects/reflection.rs | 67 +++++++++++++++++-- src/types/checker/builtin_types/reflection.rs | 65 +++++++++++++++++- tests/codegen/eval.rs | 29 ++++++++ tests/codegen/oop/attributes.rs | 28 ++++++++ 14 files changed, 296 insertions(+), 11 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 7c64310d09..830838ee92 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -23,6 +23,7 @@ struct EvalReflectionClassMetadata { resolved_name: String, attributes: Vec, flags: u64, + modifiers: u64, interface_names: Vec, trait_names: Vec, } @@ -82,6 +83,7 @@ fn eval_reflection_class_new( &metadata.interface_names, &metadata.trait_names, metadata.flags, + metadata.modifiers, context, values, ) @@ -112,6 +114,7 @@ fn eval_reflection_method_new( &[], &[], 0, + 0, context, values, ) @@ -142,6 +145,7 @@ fn eval_reflection_property_new( &[], &[], 0, + 0, context, values, ) @@ -173,6 +177,7 @@ fn eval_reflection_class_constant_new( &[], &[], 0, + 0, context, values, ) @@ -213,6 +218,7 @@ fn eval_reflection_enum_case_new( &[], &[], 0, + 0, context, values, ) @@ -227,6 +233,7 @@ fn eval_reflection_owner_object( interface_names: &[String], trait_names: &[String], flags: u64, + modifiers: u64, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -240,6 +247,7 @@ fn eval_reflection_owner_object( interface_names, trait_names, flags, + modifiers, )?; values.release(attrs)?; values.release(interface_names)?; @@ -268,6 +276,7 @@ fn eval_reflection_class_like_attributes( context: &ElephcEvalContext, ) -> Option { if let Some(class) = context.class(name) { + let is_enum = context.has_enum(class.name()); let mut flags = 0; if class.is_final() { flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL; @@ -275,15 +284,22 @@ fn eval_reflection_class_like_attributes( if class.is_abstract() { flags |= EVAL_REFLECTION_CLASS_FLAG_ABSTRACT; } - if context.has_enum(class.name()) { + if is_enum { flags |= EVAL_REFLECTION_CLASS_FLAG_ENUM; } + let modifiers = eval_reflection_class_modifiers( + class.is_final(), + class.is_abstract(), + class.is_readonly_class(), + is_enum, + ); return Some(EvalReflectionClassMetadata { resolved_name: class.name().trim_start_matches('\\').to_string(), attributes: class.attributes().to_vec(), interface_names: context.class_interface_names(class.name()), trait_names: context.class_trait_names(class.name()), flags, + modifiers, }); } if let Some(interface) = context.interface(name) { @@ -293,6 +309,7 @@ fn eval_reflection_class_like_attributes( interface_names: context.interface_parent_names(interface.name()), trait_names: Vec::new(), flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE, + modifiers: 0, }); } if let Some(trait_decl) = context.trait_decl(name) { @@ -302,6 +319,7 @@ fn eval_reflection_class_like_attributes( interface_names: Vec::new(), trait_names: Vec::new(), flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT, + modifiers: 0, }); } context @@ -312,9 +330,30 @@ fn eval_reflection_class_like_attributes( interface_names: context.class_interface_names(enum_decl.name()), trait_names: Vec::new(), flags: EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM, + modifiers: 32, }) } +/// Computes PHP's `ReflectionClass::getModifiers()` bitmask for eval metadata. +fn eval_reflection_class_modifiers( + is_final: bool, + is_abstract: bool, + is_readonly_class: bool, + is_enum: bool, +) -> u64 { + let mut modifiers = 0; + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly_class && !is_enum { + modifiers |= 65_536; + } + modifiers +} + /// Returns attributes attached to an eval class constant or enum case. fn eval_reflection_class_constant_attributes( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 98077816be..c99899188b 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -111,6 +111,7 @@ pub trait RuntimeValueOps { interface_names: RuntimeCellHandle, trait_names: RuntimeCellHandle, flags: u64, + modifiers: u64, ) -> Result; /// Creates a named runtime object without constructor arguments. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 0f76f2d116..d8046d5826 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -304,6 +304,36 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass exposes PHP modifier bitmasks for eval class-like metadata. +#[test] +fn execute_program_reflects_eval_class_modifier_bitmask() { + let program = parse_fragment( + br#"abstract class EvalModifierAbstract {} +final class EvalModifierFinal {} +readonly class EvalModifierReadonly {} +final readonly class EvalModifierFinalReadonly {} +enum EvalModifierEnum { case Ready; } +interface EvalModifierIface {} +trait EvalModifierTrait {} +echo (new ReflectionClass("EvalModifierAbstract"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierFinal"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierReadonly"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierFinalReadonly"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierEnum"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierIface"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierTrait"))->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "64:32:65536:65568:32:0:0"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. #[test] fn execute_program_reflects_eval_constant_and_enum_case_attributes() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 5a798df831..cf085175b0 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -134,6 +134,9 @@ impl FakeOps { Self::object_property(&properties, "__is_enum") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { + Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) + } (FakeValue::Object(properties), "getinterfacenames") if args.is_empty() => { Self::object_property(&properties, "__interface_names") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -260,6 +263,7 @@ impl FakeOps { interface_names: RuntimeCellHandle, trait_names: RuntimeCellHandle, flags: u64, + modifiers: u64, ) -> Result { let class_name = match owner_kind { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", @@ -276,6 +280,7 @@ impl FakeOps { let is_interface = self.bool_value((flags & 4) != 0)?; let is_trait = self.bool_value((flags & 8) != 0)?; let is_enum = self.bool_value((flags & 16) != 0)?; + let modifiers = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { let (namespace_name, short_name) = reflection_name_parts(reflected_name); @@ -288,6 +293,7 @@ impl FakeOps { properties.push(("__is_interface".to_string(), is_interface)); properties.push(("__is_trait".to_string(), is_trait)); properties.push(("__is_enum".to_string(), is_enum)); + properties.push(("__modifiers".to_string(), modifiers)); properties.push(("__short_name".to_string(), short_name)); properties.push(("__namespace_name".to_string(), namespace_name)); properties.push(("__in_namespace".to_string(), in_namespace)); diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 6eade4e4db..8f9b55005a 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -118,6 +118,7 @@ impl RuntimeValueOps for FakeOps { interface_names: RuntimeCellHandle, trait_names: RuntimeCellHandle, flags: u64, + modifiers: u64, ) -> Result { self.runtime_reflection_owner_new( owner_kind, @@ -126,6 +127,7 @@ impl RuntimeValueOps for FakeOps { interface_names, trait_names, flags, + modifiers, ) } /// Creates one fake object for eval `new` unit tests. diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 27a2fab8c7..742f92d3a7 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -76,6 +76,7 @@ unsafe extern "C" { interface_names: *mut RuntimeCell, trait_names: *mut RuntimeCell, flags: u64, + modifiers: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 4b43fd193c..6081c1b0e5 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -187,6 +187,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { interface_names: RuntimeCellHandle, trait_names: RuntimeCellHandle, flags: u64, + modifiers: u64, ) -> Result { Self::handle(unsafe { __elephc_eval_reflection_owner_new( @@ -197,6 +198,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { interface_names.as_ptr(), trait_names.as_ptr(), flags, + modifiers, ) }) } diff --git a/docs/php/classes.md b/docs/php/classes.md index 718f6d15ee..c3465e59d6 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1089,6 +1089,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an interface | | `ReflectionClass::isTrait()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is a trait | | `ReflectionClass::isEnum()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an enum | +| `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | @@ -1154,7 +1155,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `getInterfaceNames()`, and `getTraitNames()`. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `getModifiers()`, `getInterfaceNames()`, and `getTraitNames()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 21749451a6..fb13f9418e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -159,8 +159,10 @@ derive namespace-aware parts from the resolved eval class-like name. `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval class-like metadata, including PHP-compatible enum finality and class-like kind checks for eval interfaces, -traits, and enums. `ReflectionClass::getInterfaceNames()` returns implemented -interfaces for eval classes and parent interfaces for eval interfaces, while +traits, and enums. `ReflectionClass::getModifiers()` returns PHP's +`ReflectionClass::IS_*` modifier bitmask for eval class-like metadata. +`ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval +classes and parent interfaces for eval interfaces, while `ReflectionClass::getTraitNames()` returns traits used directly by eval classes. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 80e263c900..00d74666cb 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -46,6 +46,8 @@ struct ReflectionOwnerLayout { is_trait_hi: Option, is_enum_lo: Option, is_enum_hi: Option, + modifiers_lo: Option, + modifiers_hi: Option, in_namespace_lo: Option, in_namespace_hi: Option, } @@ -142,6 +144,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option ReflectionOwnerMetadata { is_interface: false, is_trait: false, is_enum: false, + modifiers: 0, } } @@ -1116,6 +1132,47 @@ fn emit_reflection_bool_property( Ok(()) } +/// Stores one integer property on the current ReflectionClass object result. +fn emit_reflection_int_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + value: i64, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let value_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, value_reg, value); + abi::emit_store_to_address(ctx.emitter, value_reg, result_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, result_reg, high_offset); + Ok(()) +} + +/// Computes PHP's `ReflectionClass::getModifiers()` bitmask for class metadata. +fn reflection_class_modifiers( + is_final: bool, + is_abstract: bool, + is_readonly_class: bool, + is_enum: bool, +) -> i64 { + let mut modifiers = 0; + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly_class && !is_enum { + modifiers |= 65_536; + } + modifiers +} + /// Returns one declared property offset from a synthetic Reflection class layout. fn reflection_property_offset(info: &crate::types::ClassInfo, property: &str) -> Result { info.property_offsets.get(property).copied().ok_or_else(|| { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 5f35ca33be..e68f5e0515 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -15,7 +15,7 @@ use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::{ - ClassMethod, ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, Visibility, + ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, Visibility, }; use crate::types::traits::FlattenedClass; use crate::types::PhpType; @@ -584,6 +584,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__modifiers", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + ), builtin_property( "__short_name", Visibility::Private, @@ -633,14 +639,37 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_bool_method("isInterface", "__is_interface"), builtin_reflection_class_bool_method("isTrait", "__is_trait"), builtin_reflection_class_bool_method("isEnum", "__is_enum"), + builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_owner_get_attributes_method(), ], attributes: Vec::new(), - constants: Vec::new(), + constants: reflection_class_constants(), used_traits: Vec::new(), } } +/// Returns the public modifier constants exposed by PHP's `ReflectionClass`. +fn reflection_class_constants() -> Vec { + vec![ + builtin_class_const("IS_IMPLICIT_ABSTRACT", 16), + builtin_class_const("IS_FINAL", 32), + builtin_class_const("IS_EXPLICIT_ABSTRACT", 64), + builtin_class_const("IS_READONLY", 65_536), + ] +} + +/// Builds a public integer class constant for a synthetic reflection type. +fn builtin_class_const(name: &str, value: i64) -> ClassConst { + ClassConst { + name: name.to_string(), + visibility: Visibility::Public, + is_final: false, + value: Expr::new(ExprKind::IntLiteral(value), crate::span::Span::dummy()), + span: crate::span::Span::dummy(), + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass` string method backed by one private slot. fn builtin_reflection_class_string_method(method_name: &str, property: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -671,6 +700,35 @@ fn builtin_reflection_class_string_method(method_name: &str, property: &str) -> } } +/// Returns a public `ReflectionClass` integer method backed by one private slot. +fn builtin_reflection_class_int_method(method_name: &str, property: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Int), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass` array method backed by one private slot. fn builtin_reflection_class_array_method(method_name: &str, property: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -893,6 +951,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Array(Box::new(PhpType::Str)); } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { + sig.return_type = PhpType::Int; + } } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8be872ef6a..3b7944bc40 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5639,6 +5639,35 @@ echo (new ReflectionClass("EvalTraitReflect"))->isEnum() ? "E" : "e";'); assert_eq!(out.stdout, "Afite:aFite:aFitE:afIte:afiTe"); } +/// Verifies eval ReflectionClass reports PHP modifier bitmasks through the bridge. +#[test] +fn test_eval_reflection_class_modifier_bitmask() { + let out = compile_and_run_capture( + r#"getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierFinal"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierReadonly"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierFinalReadonly"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierEnum"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierIface"))->getModifiers() . ":"; +echo (new ReflectionClass("EvalModifierTrait"))->getModifiers();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "64:32:65536:65568:32:0:0"); +} + /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. #[test] fn test_eval_reflection_constant_and_enum_case_attributes() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 4480bc2f1e..f9eb564104 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1156,6 +1156,34 @@ echo $trait->isEnum() ? "E" : "e"; ); } +/// Verifies that `ReflectionClass::getModifiers()` reports PHP modifier bitmasks. +#[test] +fn test_reflection_class_get_modifiers_reports_php_bitmask() { + let out = compile_and_run( + r#"getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierFinal::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierReadonly::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierFinalReadonly::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierEnum::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierIface::class))->getModifiers() . ":"; +echo (new ReflectionClass(StaticModifierTrait::class))->getModifiers() . ":"; +echo ReflectionClass::IS_IMPLICIT_ABSTRACT . ":"; +echo ReflectionClass::IS_FINAL . ":"; +echo ReflectionClass::IS_EXPLICIT_ABSTRACT . ":"; +echo ReflectionClass::IS_READONLY; +"#, + ); + assert_eq!(out, "64:32:65536:65568:32:0:0:16:32:64:65536"); +} + /// Verifies that `ReflectionClass` reports implemented interface and used trait names. #[test] fn test_reflection_class_reports_relation_names() { From fae10c0f77e966a81bd19ed0e040f525b633d4f9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 23:05:29 +0200 Subject: [PATCH 0360/1208] Support ReflectionClass member probes --- crates/elephc-eval/src/context.rs | 111 +++++++++ .../elephc-eval/src/interpreter/reflection.rs | 39 +++- .../src/interpreter/runtime_ops.rs | 12 + .../tests/builtins_class_metadata.rs | 73 ++++++ .../interpreter/tests/support/array_ops.rs | 21 ++ .../interpreter/tests/support/object_ops.rs | 39 ++++ .../interpreter/tests/support/runtime_ops.rs | 16 ++ .../elephc-eval/src/runtime_hooks/externs.rs | 8 + crates/elephc-eval/src/runtime_hooks/ops.rs | 24 ++ docs/php/classes.md | 4 +- docs/php/eval.md | 7 +- src/codegen/eval_reflection_owner_helpers.rs | 142 ++++++++---- src/codegen/lower_inst/objects/reflection.rs | 214 ++++++++++++++++-- src/codegen_support/runtime/eval_bridge.rs | 96 ++++++++ src/ir/module.rs | 4 + src/ir_lower/program.rs | 56 +++++ src/types/checker/builtin_types/reflection.rs | 75 +++++- tests/codegen/eval.rs | 72 ++++++ tests/codegen/oop/attributes.rs | 68 ++++++ 19 files changed, 1014 insertions(+), 67 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 74f6198723..deb952cdf7 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -18,6 +18,7 @@ use crate::abi::ABI_VERSION; use crate::eval_ir::{ EvalAttribute, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalTrait, + EvalVisibility, }; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; @@ -757,6 +758,96 @@ impl ElephcEvalContext { }) } + /// Returns PHP case-insensitive method names visible to `ReflectionClass::hasMethod()`. + pub fn class_method_names(&self, class_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for class in self.class_chain(class_name).into_iter().rev() { + for method in class.methods() { + push_unique_method_name(method.name(), &mut names, &mut seen); + } + if let Some(enum_decl) = self.enum_decl(class.name()) { + push_unique_method_name("cases", &mut names, &mut seen); + if enum_decl.backing_type().is_some() { + push_unique_method_name("from", &mut names, &mut seen); + push_unique_method_name("tryFrom", &mut names, &mut seen); + } + } + } + names + } + + /// Returns PHP case-sensitive property names visible to `ReflectionClass::hasProperty()`. + pub fn class_property_names(&self, class_name: &str) -> Vec { + let reflected_name = self + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let mut names = Vec::new(); + let mut seen = HashSet::new(); + if let Some(enum_decl) = self.enum_decl(&reflected_name) { + push_unique_property_name("name", &mut names, &mut seen); + if enum_decl.backing_type().is_some() { + push_unique_property_name("value", &mut names, &mut seen); + } + } + for class in self.class_chain(&reflected_name) { + let declaring_is_reflected = same_class_name(class.name(), &reflected_name); + for property in class.properties() { + if property.visibility() == EvalVisibility::Private && !declaring_is_reflected { + continue; + } + push_unique_property_name(property.name(), &mut names, &mut seen); + } + } + names + } + + /// Returns PHP case-insensitive method names declared by an eval interface hierarchy. + pub fn interface_method_names(&self, interface_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for method in self.interface_method_requirements(interface_name) { + push_unique_method_name(method.name(), &mut names, &mut seen); + } + names + } + + /// Returns PHP case-sensitive property names declared by an eval interface hierarchy. + pub fn interface_property_names(&self, interface_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for property in self.interface_property_requirements(interface_name) { + push_unique_property_name(property.name(), &mut names, &mut seen); + } + names + } + + /// Returns PHP case-insensitive direct method names declared by an eval trait. + pub fn trait_method_names(&self, trait_name: &str) -> Vec { + let Some(trait_decl) = self.trait_decl(trait_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for method in trait_decl.methods() { + push_unique_method_name(method.name(), &mut names, &mut seen); + } + names + } + + /// Returns PHP case-sensitive direct property names declared by an eval trait. + pub fn trait_property_names(&self, trait_name: &str) -> Vec { + let Some(trait_decl) = self.trait_decl(trait_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for property in trait_decl.properties() { + push_unique_property_name(property.name(), &mut names, &mut seen); + } + names + } + /// Returns parent interface names for an eval-declared interface. pub fn interface_parent_names(&self, interface_name: &str) -> Vec { let mut parents = Vec::new(); @@ -1334,6 +1425,26 @@ fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSe } } +/// Returns whether two PHP class-like names resolve to the same normalized spelling. +fn same_class_name(left: &str, right: &str) -> bool { + normalize_class_name(left) == normalize_class_name(right) +} + +/// Pushes a case-insensitive PHP method name once for ReflectionClass metadata. +fn push_unique_method_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + let key = normalize_method_name(name); + if seen.insert(key.clone()) { + names.push(key); + } +} + +/// Pushes a case-sensitive PHP property name once for ReflectionClass metadata. +fn push_unique_property_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + if seen.insert(name.to_string()) { + names.push(name.to_string()); + } +} + /// Normalizes PHP constant names for case-sensitive eval dynamic probes. fn normalize_constant_name(name: &str) -> String { name.trim_start_matches('\\').to_string() diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 830838ee92..1b5af854b9 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -26,6 +26,8 @@ struct EvalReflectionClassMetadata { modifiers: u64, interface_names: Vec, trait_names: Vec, + method_names: Vec, + property_names: Vec, } /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. @@ -82,6 +84,8 @@ fn eval_reflection_class_new( &metadata.attributes, &metadata.interface_names, &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, metadata.flags, metadata.modifiers, context, @@ -113,6 +117,8 @@ fn eval_reflection_method_new( &attributes, &[], &[], + &[], + &[], 0, 0, context, @@ -144,6 +150,8 @@ fn eval_reflection_property_new( &attributes, &[], &[], + &[], + &[], 0, 0, context, @@ -176,6 +184,8 @@ fn eval_reflection_class_constant_new( &attributes, &[], &[], + &[], + &[], 0, 0, context, @@ -217,6 +227,8 @@ fn eval_reflection_enum_case_new( &attributes, &[], &[], + &[], + &[], 0, 0, context, @@ -232,6 +244,8 @@ fn eval_reflection_owner_object( attributes: &[EvalAttribute], interface_names: &[String], trait_names: &[String], + method_names: &[String], + property_names: &[String], flags: u64, modifiers: u64, context: &mut ElephcEvalContext, @@ -240,32 +254,35 @@ fn eval_reflection_owner_object( let attrs = eval_reflection_attribute_array_result(attributes, context, values)?; let interface_names = eval_reflection_string_array_result(interface_names, values)?; let trait_names = eval_reflection_string_array_result(trait_names, values)?; + let method_names = eval_reflection_string_array_result(method_names, values)?; + let property_names = eval_reflection_string_array_result(property_names, values)?; let object = values.reflection_owner_new( owner_kind, reflected_name, attrs, interface_names, trait_names, + method_names, + property_names, flags, modifiers, )?; values.release(attrs)?; values.release(interface_names)?; values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; Ok(object) } -/// Builds an indexed PHP string array for ReflectionClass relation metadata. +/// Builds an indexed PHP string array for ReflectionClass metadata names. fn eval_reflection_string_array_result( names: &[String], values: &mut impl RuntimeValueOps, ) -> Result { - let mut result = values.array_new(names.len())?; - for (index, name) in names.iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string(name)?; - result = values.array_set(result, key, value)?; + let mut result = values.string_array_new(names.len())?; + for name in names { + result = values.string_array_push(result, name)?; } Ok(result) } @@ -298,6 +315,8 @@ fn eval_reflection_class_like_attributes( attributes: class.attributes().to_vec(), interface_names: context.class_interface_names(class.name()), trait_names: context.class_trait_names(class.name()), + method_names: context.class_method_names(class.name()), + property_names: context.class_property_names(class.name()), flags, modifiers, }); @@ -308,6 +327,8 @@ fn eval_reflection_class_like_attributes( attributes: interface.attributes().to_vec(), interface_names: context.interface_parent_names(interface.name()), trait_names: Vec::new(), + method_names: context.interface_method_names(interface.name()), + property_names: context.interface_property_names(interface.name()), flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE, modifiers: 0, }); @@ -318,6 +339,8 @@ fn eval_reflection_class_like_attributes( attributes: trait_decl.attributes().to_vec(), interface_names: Vec::new(), trait_names: Vec::new(), + method_names: context.trait_method_names(trait_decl.name()), + property_names: context.trait_property_names(trait_decl.name()), flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT, modifiers: 0, }); @@ -329,6 +352,8 @@ fn eval_reflection_class_like_attributes( attributes: enum_decl.attributes().to_vec(), interface_names: context.class_interface_names(enum_decl.name()), trait_names: Vec::new(), + method_names: context.class_method_names(enum_decl.name()), + property_names: context.class_property_names(enum_decl.name()), flags: EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM, modifiers: 32, }) diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index c99899188b..979f5855a4 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -19,6 +19,16 @@ pub trait RuntimeValueOps { /// Creates a runtime indexed-array cell with room for at least `capacity` elements. fn array_new(&mut self, capacity: usize) -> Result; + /// Creates a runtime indexed-array cell specialized for direct string payload slots. + fn string_array_new(&mut self, capacity: usize) -> Result; + + /// Appends one string payload to a runtime string-array cell. + fn string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result; + /// Creates a runtime associative-array cell with room for at least `capacity` elements. fn assoc_new(&mut self, capacity: usize) -> Result; @@ -110,6 +120,8 @@ pub trait RuntimeValueOps { attrs: RuntimeCellHandle, interface_names: RuntimeCellHandle, trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index d8046d5826..90ea182517 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -334,6 +334,79 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass reports eval class-like method and property membership. +#[test] +fn execute_program_reflects_eval_class_member_existence() { + let program = parse_fragment( + br#"class EvalMemberParent { + private function hiddenParent() {} + protected static function parentStatic() {} + private $hiddenProp; + protected static $parentStaticProp; +} +class EvalMemberChild extends EvalMemberParent { + public function ChildMethod() {} + public $childProp; +} +interface EvalMemberIfaceParent { + public function parentRequirement(); +} +interface EvalMemberIface extends EvalMemberIfaceParent { + public function childRequirement(); + public string $hook { get; } +} +trait EvalMemberTrait { + private function traitHidden() {} + public $traitProp; +} +enum EvalMemberPureEnum { + case Ready; + public function label() { return "ok"; } +} +enum EvalMemberBackedEnum: string { + case Ready = "ready"; +} +$child = new ReflectionClass("EvalMemberChild"); +echo $child->hasMethod("childmethod") ? "M" : "m"; +echo $child->hasMethod("HIDDENPARENT") ? "P" : "p"; +echo $child->hasMethod("parentStatic") ? "S" : "s"; +echo $child->hasMethod("missing") ? "X" : "x"; +echo ":"; +echo $child->hasProperty("childProp") ? "C" : "c"; +echo $child->hasProperty("hiddenProp") ? "H" : "h"; +echo $child->hasProperty("parentStaticProp") ? "T" : "t"; +echo $child->hasProperty("childprop") ? "W" : "w"; +echo ":"; +$iface = new ReflectionClass("EvalMemberIface"); +echo $iface->hasMethod("parentrequirement") ? "I" : "i"; +echo $iface->hasMethod("childRequirement") ? "J" : "j"; +echo $iface->hasProperty("hook") ? "K" : "k"; +echo ":"; +$trait = new ReflectionClass("EvalMemberTrait"); +echo $trait->hasMethod("traithidden") ? "R" : "r"; +echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo ":"; +$pure = new ReflectionClass("EvalMemberPureEnum"); +echo $pure->hasMethod("cases") ? "E" : "e"; +echo $pure->hasMethod("label") ? "L" : "l"; +echo $pure->hasProperty("name") ? "N" : "n"; +echo $pure->hasProperty("value") ? "V" : "v"; +echo ":"; +$backed = new ReflectionClass("EvalMemberBackedEnum"); +echo $backed->hasMethod("tryfrom") ? "B" : "b"; +echo $backed->hasProperty("value") ? "Y" : "y"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "MPSx:ChTw:IJK:RU:ELNv:BY"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. #[test] fn execute_program_reflects_eval_constant_and_enum_case_attributes() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/array_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/array_ops.rs index f815d8de8a..8970629b9b 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/array_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/array_ops.rs @@ -17,6 +17,27 @@ impl FakeOps { ) -> Result { Ok(self.alloc(FakeValue::Array(Vec::with_capacity(capacity)))) } + /// Creates a fake direct-string indexed array cell. + pub(super) fn runtime_string_array_new( + &mut self, + capacity: usize, + ) -> Result { + self.runtime_array_new(capacity) + } + /// Appends one string to a fake direct-string indexed array. + pub(super) fn runtime_string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result { + let value = self.runtime_string(value)?; + let id = array.as_ptr() as usize; + let Some(FakeValue::Array(elements)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + elements.push(value); + Ok(array) + } /// Creates a fake associative array cell. pub(super) fn runtime_assoc_new( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index cf085175b0..8bffe2d03c 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -137,6 +137,12 @@ impl FakeOps { (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) } + (FakeValue::Object(properties), "hasmethod") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__method_names", args[0], true) + } + (FakeValue::Object(properties), "hasproperty") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__property_names", args[0], false) + } (FakeValue::Object(properties), "getinterfacenames") if args.is_empty() => { Self::object_property(&properties, "__interface_names") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -262,6 +268,8 @@ impl FakeOps { attrs: RuntimeCellHandle, interface_names: RuntimeCellHandle, trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -299,12 +307,43 @@ impl FakeOps { properties.push(("__in_namespace".to_string(), in_namespace)); properties.push(("__interface_names".to_string(), interface_names)); properties.push(("__trait_names".to_string(), trait_names)); + properties.push(("__method_names".to_string(), method_names)); + properties.push(("__property_names".to_string(), property_names)); } let object = self.alloc(FakeValue::Object(properties)); self.object_classes .insert(object.as_ptr() as usize, class_name.to_string()); Ok(object) } + /// Checks whether a private fake object array property contains one string. + fn object_string_array_contains( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + let FakeValue::String(mut needle) = self.get(needle) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if case_insensitive { + needle = needle.to_ascii_lowercase(); + } + let Some(array) = Self::object_property(properties, property) else { + return self.bool_value(false); + }; + let contains = match self.get(array) { + FakeValue::Array(elements) => elements.iter().any(|element| match self.get(*element) { + FakeValue::String(value) if case_insensitive => { + value.to_ascii_lowercase() == needle + } + FakeValue::String(value) => value == needle, + _ => false, + }), + _ => false, + }; + self.bool_value(contains) + } /// Creates one fake object for eval `new` unit tests. pub(super) fn runtime_new_object( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 8f9b55005a..c6d20856fa 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -17,6 +17,18 @@ impl RuntimeValueOps for FakeOps { fn array_new(&mut self, capacity: usize) -> Result { self.runtime_array_new(capacity) } + /// Creates a fake direct-string indexed array cell. + fn string_array_new(&mut self, capacity: usize) -> Result { + self.runtime_string_array_new(capacity) + } + /// Appends one string to a fake direct-string indexed array cell. + fn string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result { + self.runtime_string_array_push(array, value) + } /// Creates a fake associative array cell. fn assoc_new(&mut self, _capacity: usize) -> Result { self.runtime_assoc_new(_capacity) @@ -117,6 +129,8 @@ impl RuntimeValueOps for FakeOps { attrs: RuntimeCellHandle, interface_names: RuntimeCellHandle, trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -126,6 +140,8 @@ impl RuntimeValueOps for FakeOps { attrs, interface_names, trait_names, + method_names, + property_names, flags, modifiers, ) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 742f92d3a7..647aadf174 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -16,6 +16,12 @@ use crate::value::RuntimeCell; #[cfg(not(test))] unsafe extern "C" { pub(super) fn __elephc_eval_value_array_new(capacity: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_string_array_new(capacity: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_string_array_push( + array: *mut RuntimeCell, + value_ptr: *const u8, + value_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_assoc_new(capacity: u64) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_array_get( array: *mut RuntimeCell, @@ -75,6 +81,8 @@ unsafe extern "C" { attrs: *mut RuntimeCell, interface_names: *mut RuntimeCell, trait_names: *mut RuntimeCell, + method_names: *mut RuntimeCell, + property_names: *mut RuntimeCell, flags: u64, modifiers: u64, ) -> *mut RuntimeCell; diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 6081c1b0e5..91e91e8d87 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -24,6 +24,26 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_array_new(capacity as u64) }) } + /// Creates a boxed Mixed indexed array whose payload uses direct string slots. + fn string_array_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_string_array_new(capacity as u64) }) + } + + /// Appends one string to a boxed direct-string indexed array. + fn string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_string_array_push( + array.as_ptr(), + value.as_ptr(), + value.len() as u64, + ) + }) + } + /// Creates a boxed Mixed associative array through the generated runtime wrapper. fn assoc_new(&mut self, capacity: usize) -> Result { Self::handle(unsafe { __elephc_eval_value_assoc_new(capacity as u64) }) @@ -186,6 +206,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { attrs: RuntimeCellHandle, interface_names: RuntimeCellHandle, trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -197,6 +219,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { attrs.as_ptr(), interface_names.as_ptr(), trait_names.as_ptr(), + method_names.as_ptr(), + property_names.as_ptr(), flags, modifiers, ) diff --git a/docs/php/classes.md b/docs/php/classes.md index c3465e59d6..9a4b34068b 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1090,6 +1090,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isTrait()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is a trait | | `ReflectionClass::isEnum()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an enum | | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | +| `ReflectionClass::hasMethod()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named method; method lookup is case-insensitive | +| `ReflectionClass::hasProperty()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named property; property lookup is case-sensitive | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | @@ -1155,7 +1157,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `getModifiers()`, `getInterfaceNames()`, and `getTraitNames()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, and `getTraitNames()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index fb13f9418e..1c0c12e722 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -164,6 +164,9 @@ traits, and enums. `ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval classes and parent interfaces for eval interfaces, while `ReflectionClass::getTraitNames()` returns traits used directly by eval classes. +`ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report +method and property membership for eval classes, interfaces, traits, and enums; +method lookup is case-insensitive, while property lookup is case-sensitive. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant @@ -325,8 +328,8 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported -attribute/getName slice and broader generated/AOT method bridge signatures -beyond the current public non-by-reference fixed scalar/Mixed slice. +ReflectionClass/attribute slice and broader generated/AOT method bridge +signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 00d74666cb..388eaecc72 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -9,7 +9,7 @@ //! //! Key details: //! - Reflection owner objects store private metadata slots such as `__attrs`, -//! `__name`, and the ReflectionClass relation-name arrays. +//! `__name`, and the ReflectionClass metadata-name arrays. //! - The helper retains supplied array payloads for object ownership. use crate::codegen::abi; @@ -34,6 +34,10 @@ struct ReflectionOwnerLayout { interface_names_hi: Option, trait_names_lo: Option, trait_names_hi: Option, + method_names_lo: Option, + method_names_hi: Option, + property_names_lo: Option, + property_names_hi: Option, attrs_lo: usize, attrs_hi: usize, is_final_lo: Option, @@ -139,6 +143,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option>>, interface_names: Vec, trait_names: Vec, + method_names: Vec, + property_names: Vec, is_final: bool, is_abstract: bool, is_interface: bool, @@ -89,6 +92,16 @@ pub(super) fn lower_reflection_owner_new( "__trait_names", &metadata.trait_names, )?; + emit_reflection_string_array_property_by_name( + ctx, + "__method_names", + &metadata.method_names, + )?; + emit_reflection_string_array_property_by_name( + ctx, + "__property_names", + &metadata.property_names, + )?; } } emit_reflection_attrs_property( @@ -533,6 +546,8 @@ fn reflection_class_metadata( attr_args: info.attribute_args.clone(), interface_names: info.interfaces.clone(), trait_names: info.used_traits.clone(), + method_names: reflection_class_method_names(ctx, class_name), + property_names: reflection_class_property_names(ctx, class_name, info), is_final: info.is_final, is_abstract: info.is_abstract, is_interface: false, @@ -551,6 +566,8 @@ fn reflection_class_metadata( interface_name, reflection_interface_parent_names(ctx, interface_name), Vec::new(), + reflection_interface_method_names(ctx, interface_name), + reflection_interface_property_names(ctx, interface_name), true, false, false, @@ -567,6 +584,8 @@ fn reflection_class_metadata( trait_name, Vec::new(), trait_names, + reflection_trait_method_names(ctx, trait_name), + reflection_trait_property_names(ctx, trait_name), false, true, false, @@ -597,6 +616,8 @@ fn reflection_method_metadata( attr_args: info.method_attribute_args.get(&method_key)?.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -629,6 +650,8 @@ fn reflection_property_metadata( attr_args: info.property_attribute_args.get(&property_name)?.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -662,13 +685,15 @@ fn reflection_class_constant_metadata( attr_args: case.attribute_args.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), is_final: false, is_abstract: false, - is_interface: false, - is_trait: false, - is_enum: false, - modifiers: 0, - }); + is_interface: false, + is_trait: false, + is_enum: false, + modifiers: 0, + }); } Ok( resolve_reflection_class_constant(ctx, &reflected_class, &constant_name) @@ -689,6 +714,8 @@ fn reflection_class_constant_metadata( attr_args, interface_names: Vec::new(), trait_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -723,6 +750,8 @@ fn reflection_enum_case_metadata( attr_args: case.attribute_args.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -813,11 +842,160 @@ fn collect_reflection_interface_parent_names( } } +/// Returns PHP case-insensitive method names visible to `ReflectionClass::hasMethod()`. +fn reflection_class_method_names(ctx: &FunctionContext<'_>, class_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + let mut current = Some(class_name.to_string()); + while let Some(current_name) = current { + let Some((resolved_name, info)) = resolve_reflection_class(ctx, ¤t_name) else { + break; + }; + push_unique_method_names(info.methods.keys(), &mut names, &mut seen); + push_unique_method_names(info.static_methods.keys(), &mut names, &mut seen); + current = info.parent.clone(); + if current.as_deref() == Some(resolved_name) { + break; + } + } + names +} + +/// Returns PHP case-sensitive property names visible to `ReflectionClass::hasProperty()`. +fn reflection_class_property_names( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, +) -> Vec { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if is_reflection_enum(ctx, class_name) { + push_unique_property_name("name", &mut names, &mut seen); + } + for (name, _) in &info.properties { + if reflection_property_visible_from_class(info, class_name, name, false) { + push_unique_property_name(name, &mut names, &mut seen); + } + } + for (name, _) in &info.static_properties { + if reflection_property_visible_from_class(info, class_name, name, true) { + push_unique_property_name(name, &mut names, &mut seen); + } + } + names +} + +/// Returns true when a property should be visible for `ReflectionClass::hasProperty()`. +fn reflection_property_visible_from_class( + info: &crate::types::ClassInfo, + reflected_class: &str, + property_name: &str, + is_static: bool, +) -> bool { + let visibility = if is_static { + info.static_property_visibilities.get(property_name) + } else { + info.property_visibilities.get(property_name) + }; + if visibility != Some(&Visibility::Private) { + return true; + } + let declaring_class = if is_static { + info.static_property_declaring_classes.get(property_name) + } else { + info.property_declaring_classes.get(property_name) + }; + declaring_class + .map(|declaring_class| php_symbol_key(declaring_class) == php_symbol_key(reflected_class)) + .unwrap_or(false) +} + +/// Returns PHP case-insensitive method names declared by an interface and its parents. +fn reflection_interface_method_names( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return Vec::new(); + }; + let Some(info) = ctx.module.interface_infos.get(interface_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + push_unique_method_names(info.methods.keys(), &mut names, &mut seen); + names +} + +/// Returns PHP case-sensitive property names declared by an interface and its parents. +fn reflection_interface_property_names( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return Vec::new(); + }; + let Some(info) = ctx.module.interface_infos.get(interface_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for property in info.properties.keys() { + push_unique_property_name(property, &mut names, &mut seen); + } + names +} + +/// Returns PHP case-insensitive direct method names declared by a trait. +fn reflection_trait_method_names(ctx: &FunctionContext<'_>, trait_name: &str) -> Vec { + ctx.module + .declared_trait_method_names + .get(trait_name) + .cloned() + .unwrap_or_default() +} + +/// Returns PHP case-sensitive direct property names declared by a trait. +fn reflection_trait_property_names(ctx: &FunctionContext<'_>, trait_name: &str) -> Vec { + ctx.module + .declared_trait_property_names + .get(trait_name) + .cloned() + .unwrap_or_default() +} + +/// Appends lower-case method names while preserving first-seen order. +fn push_unique_method_names<'a>( + method_names: impl Iterator, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + for method_name in method_names { + let key = php_symbol_key(method_name); + if seen.insert(key.clone()) { + names.push(key); + } + } +} + +/// Appends one case-sensitive property name while preserving first-seen order. +fn push_unique_property_name( + property_name: &str, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(property_name.to_string()) { + names.push(property_name.to_string()); + } +} + /// Builds empty ReflectionClass metadata for class-like symbols without stored attributes. fn class_like_reflection_metadata( class_like_name: &str, interface_names: Vec, trait_names: Vec, + method_names: Vec, + property_names: Vec, is_interface: bool, is_trait: bool, is_enum: bool, @@ -828,6 +1006,8 @@ fn class_like_reflection_metadata( attr_args: Vec::new(), interface_names, trait_names, + method_names, + property_names, is_final: false, is_abstract: false, is_interface, @@ -873,6 +1053,8 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { attr_args: Vec::new(), interface_names: Vec::new(), trait_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -1066,7 +1248,7 @@ fn emit_reflection_string_array_property_by_name( Ok(()) } -/// Allocates an indexed string array containing ReflectionClass relation names. +/// Allocates an indexed string array containing ReflectionClass metadata names. fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) -> Result<()> { match ctx.emitter.target.arch { Arch::AArch64 => { @@ -1086,34 +1268,34 @@ fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) Ok(()) } -/// Appends ReflectionClass relation names to the current ARM64 result array. +/// Appends ReflectionClass metadata names to the current ARM64 result array. fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the relation-name array while appending strings + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("ldr x0, [sp]"); // reload the relation-name array for this append + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown relation-name array + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final relation-name array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result } -/// Appends ReflectionClass relation names to the current x86_64 result array. +/// Appends ReflectionClass metadata names to the current x86_64 result array. fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("push rax"); // park the relation-name array while appending strings + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the relation-name array for this append + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown relation-name array + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array } ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final relation-name array as the result + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result } /// Stores one boolean property on the current ReflectionClass object result. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 58fd690ee3..992778a749 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -344,6 +344,53 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #48"); // release the array-new wrapper frame emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_value_string_array_new"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for string-array allocation and boxing + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("mov x9, #4"); // minimum indexed-array capacity for eval metadata lists + emitter.instruction("cmp x0, x9"); // compare requested capacity with the minimum capacity + emitter.instruction("csel x0, x0, x9, hs"); // use max(requested, 4) as the runtime allocation capacity + emitter.instruction("mov x1, #16"); // direct string arrays store pointer/length pairs + emitter.instruction("bl __rt_array_new"); // allocate indexed-array storage for direct string slots + emitter.instruction("str x0, [sp, #0]"); // save the owned string-array pointer while boxing it + emitter.instruction("mov x0, #24"); // Mixed cells store tag plus two payload words + emitter.instruction("bl __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array + emitter.instruction("mov x9, #5"); // low byte 5 = mixed cell heap kind + emitter.instruction("str x9, [x0, #-8]"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov x10, #4"); // runtime tag 4 = indexed array + emitter.instruction("str x10, [x0]"); // store the indexed-array tag in the Mixed cell + emitter.instruction("ldr x11, [sp, #0]"); // reload the owned direct-string array pointer + emitter.instruction("str x11, [x0, #8]"); // store the string-array pointer as the Mixed low payload word + emitter.instruction("str xzr, [x0, #16]"); // indexed arrays do not use the high payload word + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the string-array-new wrapper frame + emitter.instruction("ret"); // return the boxed direct-string array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_string_array_push"); + emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame while appending one metadata string + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable wrapper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the boxed string-array owner + emitter.instruction("stp x1, x2, [sp, #8]"); // save the incoming string pointer and length + emitter.instruction("cbz x0, __elephc_eval_value_string_array_push_fail"); // reject malformed null string-array handles + emitter.instruction("bl __rt_mixed_unbox"); // expose the indexed-array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction("b.ne __elephc_eval_value_string_array_push_fail"); // reject non-array metadata containers + emitter.instruction("mov x0, x1"); // pass the unboxed array payload to the string append helper + emitter.instruction("ldp x1, x2, [sp, #8]"); // reload the string payload to append + emitter.instruction("bl __rt_array_push_str"); // persist and append the string, returning the updated array payload + emitter.instruction("ldr x9, [sp, #0]"); // reload the boxed string-array owner + emitter.instruction("str x0, [x9, #8]"); // update the boxed payload in case the array grew + emitter.instruction("mov x0, x9"); // return the boxed string-array owner to Rust + emitter.instruction("b __elephc_eval_value_string_array_push_done"); // skip the malformed-input null result + emitter.label("__elephc_eval_value_string_array_push_fail"); + emitter.instruction("mov x0, xzr"); // report a null pointer so Rust converts it to RuntimeFatal + emitter.label("__elephc_eval_value_string_array_push_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the string-array-push wrapper frame + emitter.instruction("ret"); // return the updated boxed string-array handle to Rust + label_c_global(emitter, "__elephc_eval_value_assoc_new"); emitter.instruction("sub sp, sp, #48"); // allocate a wrapper frame for hash allocation and boxing emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across runtime calls @@ -1688,6 +1735,55 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed array Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_value_string_array_new"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve local slots for the string-array pointer + emitter.instruction("cmp rdi, 4"); // compare requested capacity with the minimum capacity + emitter.instruction("mov r10, 4"); // minimum indexed-array capacity for eval metadata lists + emitter.instruction("cmovb rdi, r10"); // use max(requested, 4) as the runtime allocation capacity + emitter.instruction("mov rsi, 16"); // direct string arrays store pointer/length pairs + emitter.instruction("call __rt_array_new"); // allocate indexed-array storage for direct string slots + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the owned direct-string array pointer while boxing it + emitter.instruction("mov rax, 24"); // Mixed cells store tag plus two payload words + emitter.instruction("call __rt_heap_alloc"); // allocate a boxed Mixed cell without retaining the new array + emitter.instruction(&x86_64_mixed_heap_kind_instruction()); // materialize the mixed-cell heap kind with the x86_64 heap marker + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // install the mixed-cell heap kind in the uniform header + emitter.instruction("mov QWORD PTR [rax], 4"); // runtime tag 4 = indexed array + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the owned direct-string array pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the string-array pointer as the Mixed low payload word + emitter.instruction("mov QWORD PTR [rax + 16], 0"); // indexed arrays do not use the high payload word + emitter.instruction("add rsp, 16"); // release the string-array-new wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed direct-string array Mixed cell to Rust + + label_c_global(emitter, "__elephc_eval_value_string_array_push"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve local slots for boxed owner and incoming string payload + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the boxed string-array owner + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the incoming string pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the incoming string length + emitter.instruction("test rdi, rdi"); // check whether the boxed string-array handle is null + emitter.instruction("jz __elephc_eval_value_string_array_push_fail_x86"); // reject malformed null string-array handles + emitter.instruction("mov rax, rdi"); // move the boxed owner into mixed_unbox's input register + emitter.instruction("call __rt_mixed_unbox"); // expose the indexed-array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction("jne __elephc_eval_value_string_array_push_fail_x86"); // reject non-array metadata containers + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload the string pointer to append + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the string length to append + emitter.instruction("call __rt_array_push_str"); // persist and append the string, returning the updated array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the boxed string-array owner + emitter.instruction("mov QWORD PTR [r10 + 8], rax"); // update the boxed payload in case the array grew + emitter.instruction("mov rax, r10"); // return the boxed string-array owner to Rust + emitter.instruction("jmp __elephc_eval_value_string_array_push_done_x86"); // skip the malformed-input null result + emitter.label("__elephc_eval_value_string_array_push_fail_x86"); + emitter.instruction("xor eax, eax"); // report a null pointer so Rust converts it to RuntimeFatal + emitter.label("__elephc_eval_value_string_array_push_done_x86"); + emitter.instruction("add rsp, 32"); // release the string-array-push wrapper slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the updated boxed string-array handle to Rust + label_c_global(emitter, "__elephc_eval_value_assoc_new"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/src/ir/module.rs b/src/ir/module.rs index 62f9f34db3..cbf2eeb1a4 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -58,6 +58,8 @@ pub struct Module { pub declared_interface_names: Vec, pub declared_trait_names: Vec, pub declared_trait_uses: HashMap>, + pub declared_trait_method_names: HashMap>, + pub declared_trait_property_names: HashMap>, pub class_infos: HashMap, pub interface_infos: HashMap, pub enum_infos: HashMap, @@ -92,6 +94,8 @@ impl Module { declared_interface_names: Vec::new(), declared_trait_names: Vec::new(), declared_trait_uses: HashMap::new(), + declared_trait_method_names: HashMap::new(), + declared_trait_property_names: HashMap::new(), class_infos: HashMap::new(), interface_infos: HashMap::new(), enum_infos: HashMap::new(), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 9e70311069..0c3b743a75 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -72,6 +72,8 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec collect_declared_interface_names(program, &check_result.interfaces); module.declared_trait_names = collect_declared_trait_names(program); module.declared_trait_uses = collect_declared_trait_uses(program); + module.declared_trait_method_names = collect_declared_trait_method_names(program); + module.declared_trait_property_names = collect_declared_trait_property_names(program); module.class_infos = check_result.classes.clone(); module.interface_infos = check_result.interfaces.clone(); module.enum_infos = check_result.enums.clone(); @@ -401,6 +403,60 @@ fn collect_declared_trait_uses(program: &Program) -> HashMap uses } +/// Collects direct PHP method names declared by each trait in source order. +fn collect_declared_trait_method_names(program: &Program) -> HashMap> { + let mut methods = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + methods: trait_methods, + .. + } => { + methods.insert( + name.clone(), + trait_methods + .iter() + .map(|method| php_symbol_key(&method.name)) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + methods.extend(collect_declared_trait_method_names(body)); + } + _ => {} + } + } + methods +} + +/// Collects direct PHP property names declared by each trait in source order. +fn collect_declared_trait_property_names(program: &Program) -> HashMap> { + let mut properties = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + properties: trait_properties, + .. + } => { + properties.insert( + name.clone(), + trait_properties + .iter() + .map(|property| property.name.clone()) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + properties.extend(collect_declared_trait_property_names(body)); + } + _ => {} + } + } + properties +} + /// Recursively collects source-declared names that are present in checked metadata. fn collect_program_declared_names( program: &Program, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index e68f5e0515..dcab147478 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -14,6 +14,7 @@ use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; +use crate::names::Name; use crate::parser::ast::{ ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, Visibility, }; @@ -620,6 +621,18 @@ fn builtin_reflection_class() -> FlattenedClass { Some(string_array_type()), empty_array(), ), + builtin_property( + "__method_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__property_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![( @@ -640,6 +653,8 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_bool_method("isTrait", "__is_trait"), builtin_reflection_class_bool_method("isEnum", "__is_enum"), builtin_reflection_class_int_method("getModifiers", "__modifiers"), + builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), + builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), builtin_reflection_owner_get_attributes_method(), ], attributes: Vec::new(), @@ -729,6 +744,56 @@ fn builtin_reflection_class_int_method(method_name: &str, property: &str) -> Cla } } +/// Returns a public `ReflectionClass` membership probe backed by a private string array. +fn builtin_reflection_class_has_name_method( + method_name: &str, + property: &str, + case_insensitive: bool, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name_arg = Expr::new(ExprKind::Variable("name".to_string()), dummy_span); + let needle = if case_insensitive { + Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("strtolower"), + args: vec![name_arg], + }, + dummy_span, + ) + } else { + name_arg + }; + let haystack = Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ); + let contains = Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("in_array"), + args: vec![needle, haystack], + }, + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Int), + body: vec![Stmt::new(StmtKind::Return(Some(contains)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass` array method backed by one private slot. fn builtin_reflection_class_array_method(method_name: &str, property: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -941,7 +1006,15 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionClass" { - for method_name in ["isfinal", "isabstract"] { + for method_name in [ + "isfinal", + "isabstract", + "isinterface", + "istrait", + "isenum", + "hasmethod", + "hasproperty", + ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3b7944bc40..1276c62ae2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5668,6 +5668,78 @@ echo (new ReflectionClass("EvalModifierTrait"))->getModifiers();'); assert_eq!(out.stdout, "64:32:65536:65568:32:0:0"); } +/// Verifies eval ReflectionClass reports method and property membership through the bridge. +#[test] +fn test_eval_reflection_class_member_existence() { + let out = compile_and_run_capture( + r#"hasMethod("childmethod") ? "M" : "m"; +echo $child->hasMethod("HIDDENPARENT") ? "P" : "p"; +echo $child->hasMethod("parentStatic") ? "S" : "s"; +echo $child->hasMethod("missing") ? "X" : "x"; +echo ":"; +echo $child->hasProperty("childProp") ? "C" : "c"; +echo $child->hasProperty("hiddenProp") ? "H" : "h"; +echo $child->hasProperty("parentStaticProp") ? "T" : "t"; +echo $child->hasProperty("childprop") ? "W" : "w"; +echo ":"; +$iface = new ReflectionClass("EvalMemberIface"); +echo $iface->hasMethod("parentrequirement") ? "I" : "i"; +echo $iface->hasMethod("childRequirement") ? "J" : "j"; +echo $iface->hasProperty("hook") ? "K" : "k"; +echo ":"; +$trait = new ReflectionClass("EvalMemberTrait"); +echo $trait->hasMethod("traithidden") ? "R" : "r"; +echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo ":"; +$pure = new ReflectionClass("EvalMemberPureEnum"); +echo $pure->hasMethod("cases") ? "E" : "e"; +echo $pure->hasMethod("label") ? "L" : "l"; +echo $pure->hasProperty("name") ? "N" : "n"; +echo $pure->hasProperty("value") ? "V" : "v"; +echo ":"; +$backed = new ReflectionClass("EvalMemberBackedEnum"); +echo $backed->hasMethod("tryfrom") ? "B" : "b"; +echo $backed->hasProperty("value") ? "Y" : "y";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "MPSx:ChTw:IJK:RU:ELNv:BY"); +} + /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. #[test] fn test_eval_reflection_constant_and_enum_case_attributes() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index f9eb564104..e5921aabb0 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1184,6 +1184,74 @@ echo ReflectionClass::IS_READONLY; assert_eq!(out, "64:32:65536:65568:32:0:0:16:32:64:65536"); } +/// Verifies that `ReflectionClass::hasMethod()` and `hasProperty()` report +/// PHP-visible members for static class-like metadata. +#[test] +fn test_reflection_class_reports_member_existence() { + let out = compile_and_run( + r#"hasMethod("childmethod") ? "M" : "m"; +echo $child->hasMethod("HIDDENPARENT") ? "P" : "p"; +echo $child->hasMethod("parentStatic") ? "S" : "s"; +echo $child->hasMethod("missing") ? "X" : "x"; +echo ":"; +echo $child->hasProperty("childProp") ? "C" : "c"; +echo $child->hasProperty("hiddenProp") ? "H" : "h"; +echo $child->hasProperty("parentStaticProp") ? "T" : "t"; +echo $child->hasProperty("childprop") ? "W" : "w"; +echo ":"; +$iface = new ReflectionClass(StaticMemberIface::class); +echo $iface->hasMethod("parentrequirement") ? "I" : "i"; +echo $iface->hasMethod("childRequirement") ? "J" : "j"; +echo $iface->hasProperty("hook") ? "K" : "k"; +echo ":"; +$trait = new ReflectionClass(StaticMemberTrait::class); +echo $trait->hasMethod("traithidden") ? "R" : "r"; +echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo ":"; +$pure = new ReflectionClass(StaticMemberPureEnum::class); +echo $pure->hasMethod("cases") ? "E" : "e"; +echo $pure->hasMethod("label") ? "L" : "l"; +echo $pure->hasProperty("name") ? "N" : "n"; +echo $pure->hasProperty("value") ? "V" : "v"; +echo ":"; +$backed = new ReflectionClass(StaticMemberBackedEnum::class); +echo $backed->hasMethod("tryfrom") ? "B" : "b"; +echo $backed->hasProperty("value") ? "Y" : "y"; +"#, + ); + assert_eq!(out, "MPSx:ChTw:IJK:RU:ELNv:BY"); +} + /// Verifies that `ReflectionClass` reports implemented interface and used trait names. #[test] fn test_reflection_class_reports_relation_names() { From 3ac52f966b478546ca46e90c9b39cc884232a6c4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 23:30:00 +0200 Subject: [PATCH 0361/1208] Support Reflection member predicates --- .../elephc-eval/src/interpreter/reflection.rs | 119 ++- .../tests/builtins_class_metadata.rs | 53 ++ .../interpreter/tests/support/object_ops.rs | 43 + docs/php/classes.md | 12 +- docs/php/eval.md | 9 +- src/codegen/eval_reflection_owner_helpers.rs | 745 +++++++++++------- src/codegen/lower_inst/objects/reflection.rs | 177 ++++- src/types/checker/builtin_types/reflection.rs | 51 ++ tests/codegen/eval.rs | 52 ++ tests/codegen/oop/attributes.rs | 48 ++ 10 files changed, 974 insertions(+), 335 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 1b5af854b9..0280b4c698 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -17,6 +17,12 @@ const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; const EVAL_REFLECTION_CLASS_FLAG_INTERFACE: u64 = 4; const EVAL_REFLECTION_CLASS_FLAG_TRAIT: u64 = 8; const EVAL_REFLECTION_CLASS_FLAG_ENUM: u64 = 16; +const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; +const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; /// Eval metadata needed to materialize one `ReflectionClass` owner object. struct EvalReflectionClassMetadata { @@ -30,6 +36,15 @@ struct EvalReflectionClassMetadata { property_names: Vec, } +/// Eval metadata needed to materialize one `ReflectionMethod` or `ReflectionProperty` owner object. +struct EvalReflectionMemberMetadata { + attributes: Vec, + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, +} + /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. pub(in crate::interpreter) fn eval_reflection_owner_new_object( class_name: &str, @@ -109,17 +124,23 @@ fn eval_reflection_method_new( return Ok(None); } let method_name = eval_reflection_string_arg(args[1], values)?; - let attributes = eval_reflection_method_attributes(&class_name, &method_name, context) + let method = eval_reflection_method_metadata(&class_name, &method_name, context) .ok_or(EvalStatus::RuntimeFatal)?; + let flags = eval_reflection_member_flags( + method.visibility, + method.is_static, + method.is_final, + method.is_abstract, + ); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_METHOD, &method_name, - &attributes, + &method.attributes, &[], &[], &[], &[], - 0, + flags, 0, context, values, @@ -142,17 +163,18 @@ fn eval_reflection_property_new( return Ok(None); } let property_name = eval_reflection_string_arg(args[1], values)?; - let attributes = eval_reflection_property_attributes(&class_name, &property_name, context) + let property = eval_reflection_property_metadata(&class_name, &property_name, context) .ok_or(EvalStatus::RuntimeFatal)?; + let flags = eval_reflection_member_flags(property.visibility, property.is_static, false, false); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_PROPERTY, &property_name, - &attributes, + &property.attributes, &[], &[], &[], &[], - 0, + flags, 0, context, values, @@ -403,60 +425,121 @@ fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> || context.has_enum(name) } -/// Returns attributes attached to a method-like member on an eval class-like symbol. -fn eval_reflection_method_attributes( +/// Returns method metadata for a method-like member on an eval class-like symbol. +fn eval_reflection_method_metadata( class_name: &str, method_name: &str, context: &ElephcEvalContext, -) -> Option> { +) -> Option { if context.has_class(class_name) || context.has_enum(class_name) { return context .class_method(class_name, method_name) - .map(|(_, method)| method.attributes().to_vec()); + .map(|(_, method)| EvalReflectionMemberMetadata { + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + }); } if context.has_interface(class_name) { return context .interface_method_requirements(class_name) .into_iter() .find(|method| method.name().eq_ignore_ascii_case(method_name)) - .map(|method| method.attributes().to_vec()); + .map(|method| EvalReflectionMemberMetadata { + attributes: method.attributes().to_vec(), + visibility: EvalVisibility::Public, + is_static: false, + is_final: false, + is_abstract: true, + }); } context.trait_decl(class_name).and_then(|trait_decl| { trait_decl .methods() .iter() .find(|method| method.name().eq_ignore_ascii_case(method_name)) - .map(|method| method.attributes().to_vec()) + .map(|method| EvalReflectionMemberMetadata { + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + }) }) } -/// Returns attributes attached to a property-like member on an eval class-like symbol. -fn eval_reflection_property_attributes( +/// Returns property metadata for a property-like member on an eval class-like symbol. +fn eval_reflection_property_metadata( class_name: &str, property_name: &str, context: &ElephcEvalContext, -) -> Option> { +) -> Option { if context.has_class(class_name) || context.has_enum(class_name) { return context .class_property(class_name, property_name) - .map(|(_, property)| property.attributes().to_vec()); + .map(|(_, property)| EvalReflectionMemberMetadata { + attributes: property.attributes().to_vec(), + visibility: property.visibility(), + is_static: property.is_static(), + is_final: false, + is_abstract: property.is_abstract(), + }); } if context.has_interface(class_name) { return context .interface_property_requirements(class_name) .into_iter() .find(|property| property.name() == property_name) - .map(|property| property.attributes().to_vec()); + .map(|property| EvalReflectionMemberMetadata { + attributes: property.attributes().to_vec(), + visibility: EvalVisibility::Public, + is_static: false, + is_final: false, + is_abstract: true, + }); } context.trait_decl(class_name).and_then(|trait_decl| { trait_decl .properties() .iter() .find(|property| property.name() == property_name) - .map(|property| property.attributes().to_vec()) + .map(|property| EvalReflectionMemberMetadata { + attributes: property.attributes().to_vec(), + visibility: property.visibility(), + is_static: property.is_static(), + is_final: false, + is_abstract: property.is_abstract(), + }) }) } +/// Packs ReflectionMethod/ReflectionProperty predicate flags for the runtime owner factory. +fn eval_reflection_member_flags( + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, +) -> u64 { + let mut flags = 0; + if is_static { + flags |= EVAL_REFLECTION_MEMBER_FLAG_STATIC; + } + match visibility { + EvalVisibility::Public => flags |= EVAL_REFLECTION_MEMBER_FLAG_PUBLIC, + EvalVisibility::Protected => flags |= EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, + EvalVisibility::Private => flags |= EVAL_REFLECTION_MEMBER_FLAG_PRIVATE, + } + if is_final { + flags |= EVAL_REFLECTION_MEMBER_FLAG_FINAL; + } + if is_abstract { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT; + } + flags +} + /// Converts one reflection constructor argument to a Rust UTF-8 string. fn eval_reflection_string_arg( value: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 90ea182517..c517bcc797 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -407,6 +407,59 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod and ReflectionProperty expose eval member predicate metadata. +#[test] +fn execute_program_reflects_eval_member_predicates() { + let program = parse_fragment( + br#"abstract class EvalReflectMemberBase { + protected static function baseStatic() {} + abstract protected function mustImplement(); + final public function locked() {} +} +class EvalReflectMemberChild extends EvalReflectMemberBase { + public function mustImplement() {} + private static $token; + protected $visible; +} +$baseStatic = new ReflectionMethod("EvalReflectMemberChild", "baseStatic"); +echo $baseStatic->isStatic() ? "S" : "s"; +echo $baseStatic->isProtected() ? "P" : "p"; +echo $baseStatic->isPublic() ? "U" : "u"; +echo $baseStatic->isPrivate() ? "R" : "r"; +echo $baseStatic->isFinal() ? "F" : "f"; +echo $baseStatic->isAbstract() ? "A" : "a"; +echo ":"; +$abstractMethod = new ReflectionMethod("EvalReflectMemberBase", "mustImplement"); +echo $abstractMethod->isAbstract() ? "A" : "a"; +echo $abstractMethod->isProtected() ? "P" : "p"; +echo $abstractMethod->isStatic() ? "S" : "s"; +echo ":"; +$finalMethod = new ReflectionMethod("EvalReflectMemberChild", "locked"); +echo $finalMethod->isFinal() ? "F" : "f"; +echo $finalMethod->isPublic() ? "U" : "u"; +echo $finalMethod->isStatic() ? "S" : "s"; +echo ":"; +$staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); +echo $staticProp->isStatic() ? "S" : "s"; +echo $staticProp->isPrivate() ? "R" : "r"; +echo $staticProp->isProtected() ? "P" : "p"; +echo ":"; +$visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); +echo $visibleProp->isStatic() ? "S" : "s"; +echo $visibleProp->isProtected() ? "P" : "p"; +echo $visibleProp->isPublic() ? "U" : "u"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "SPurfa:APs:FUs:SRp:sPu"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. #[test] fn execute_program_reflects_eval_constant_and_enum_case_attributes() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 8bffe2d03c..8065a45e83 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -9,6 +9,13 @@ use super::*; +const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; +const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; + impl FakeOps { /// Reads one fake object property by name. pub(super) fn runtime_property_get( @@ -137,6 +144,22 @@ impl FakeOps { (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) } + (FakeValue::Object(properties), "isstatic") if args.is_empty() => { + Self::object_property(&properties, "__is_static") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "ispublic") if args.is_empty() => { + Self::object_property(&properties, "__is_public") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isprotected") if args.is_empty() => { + Self::object_property(&properties, "__is_protected") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isprivate") if args.is_empty() => { + Self::object_property(&properties, "__is_private") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "hasmethod") if args.len() == 1 => { self.object_string_array_contains(&properties, "__method_names", args[0], true) } @@ -310,6 +333,26 @@ impl FakeOps { properties.push(("__method_names".to_string(), method_names)); properties.push(("__property_names".to_string(), property_names)); } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD + || owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + { + let is_static = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC) != 0)?; + let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; + let is_protected = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; + let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; + properties.push(("__is_static".to_string(), is_static)); + properties.push(("__is_public".to_string(), is_public)); + properties.push(("__is_protected".to_string(), is_protected)); + properties.push(("__is_private".to_string(), is_private)); + } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_abstract = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); + } let object = self.alloc(FakeValue::Object(properties)); self.object_classes .insert(object.as_ptr() as usize, class_name.to_string()); diff --git a/docs/php/classes.md b/docs/php/classes.md index 9a4b34068b..577b06e753 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1097,8 +1097,18 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | +| `ReflectionMethod::isStatic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is static | +| `ReflectionMethod::isPublic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is public | +| `ReflectionMethod::isProtected()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is protected | +| `ReflectionMethod::isPrivate()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is private | +| `ReflectionMethod::isFinal()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is final | +| `ReflectionMethod::isAbstract()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is abstract | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | +| `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | +| `ReflectionProperty::isPublic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is public | +| `ReflectionProperty::isProtected()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is protected | +| `ReflectionProperty::isPrivate()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is private | | `ReflectionAttribute::newInstance()` | Internal only | Instantiate the attribute class from captured literal args | Functions and their parameters can also be reflected. `ReflectionFunction` reads @@ -1157,7 +1167,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, and `getTraitNames()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` and `ReflectionProperty` currently support `getName()` and `getAttributes()` only; broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, and `getTraitNames()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 1c0c12e722..c155c6518e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -167,6 +167,10 @@ classes and parent interfaces for eval interfaces, while `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. +`ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, +`isFinal()`, and `isAbstract()` report eval method metadata. +`ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and +`isPrivate()` report eval property metadata. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant @@ -328,8 +332,9 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported -ReflectionClass/attribute slice and broader generated/AOT method bridge -signatures beyond the current public non-by-reference fixed scalar/Mixed slice. +ReflectionClass/Method/Property/attribute slice and broader generated/AOT method +bridge signatures beyond the current public non-by-reference fixed scalar/Mixed +slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 388eaecc72..9585ec34d7 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -54,6 +54,14 @@ struct ReflectionOwnerLayout { modifiers_hi: Option, in_namespace_lo: Option, in_namespace_hi: Option, + is_static_lo: Option, + is_static_hi: Option, + is_public_lo: Option, + is_public_hi: Option, + is_protected_lo: Option, + is_protected_hi: Option, + is_private_lo: Option, + is_private_hi: Option, } /// Layouts for the Reflection owner classes eval can materialize. @@ -152,6 +160,10 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option Option fn emit_reflection_owner_new_stub(emitter: &mut Emitter) { match emitter.target.arch { Arch::AArch64 => { - emitter.instruction("mov x0, xzr"); // report helper failure when Reflection owner metadata is missing - emitter.instruction("ret"); // return the null pointer to Rust + emitter.instruction("mov x0, xzr"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust } Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // report helper failure when Reflection owner metadata is missing - emitter.instruction("ret"); // return the null pointer to Rust + emitter.instruction("xor eax, eax"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust } } } @@ -218,34 +238,34 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant"; let enum_unit_case_label = "__elephc_eval_reflection_owner_new_enum_unit_case"; let enum_backed_case_label = "__elephc_eval_reflection_owner_new_enum_backed_case"; - emitter.instruction("sub sp, sp, #160"); // reserve helper frame for inputs, object, arrays, scratch, and fp/lr - emitter.instruction("stp x29, x30, [sp, #144]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #144"); // establish a stable helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the Reflection owner kind - emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer - emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length - emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array - emitter.instruction("str x4, [sp, #80]"); // save the boxed ReflectionClass interface-name array - emitter.instruction("str x5, [sp, #88]"); // save the boxed ReflectionClass trait-name array - emitter.instruction("str x6, [sp, #104]"); // save the boxed ReflectionClass method-name array - emitter.instruction("str x7, [sp, #112]"); // save the boxed ReflectionClass property-name array - emitter.instruction("ldr x8, [sp, #160]"); // load ReflectionClass modifier flags from the first stack argument - emitter.instruction("str x8, [sp, #48]"); // save ReflectionClass modifier flags - emitter.instruction("ldr x8, [sp, #168]"); // load ReflectionClass getModifiers bitmask from the second stack argument - emitter.instruction("str x8, [sp, #96]"); // save ReflectionClass getModifiers bitmask - emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass - emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner - emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod - emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner - emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty - emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner - emitter.instruction("cmp x0, #3"); // owner kind 3 means ReflectionClassConstant - emitter.instruction(&format!("b.eq {}", class_constant_label)); // allocate a ReflectionClassConstant owner - emitter.instruction("cmp x0, #4"); // owner kind 4 means ReflectionEnumUnitCase - emitter.instruction(&format!("b.eq {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner - emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase - emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner - emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds + emitter.instruction("sub sp, sp, #160"); // reserve helper frame for inputs, object, arrays, scratch, and fp/lr + emitter.instruction("stp x29, x30, [sp, #144]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #144"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the Reflection owner kind + emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer + emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array + emitter.instruction("str x4, [sp, #80]"); // save the boxed ReflectionClass interface-name array + emitter.instruction("str x5, [sp, #88]"); // save the boxed ReflectionClass trait-name array + emitter.instruction("str x6, [sp, #104]"); // save the boxed ReflectionClass method-name array + emitter.instruction("str x7, [sp, #112]"); // save the boxed ReflectionClass property-name array + emitter.instruction("ldr x8, [sp, #160]"); // load ReflectionClass modifier flags from the first stack argument + emitter.instruction("str x8, [sp, #48]"); // save ReflectionClass modifier flags + emitter.instruction("ldr x8, [sp, #168]"); // load ReflectionClass getModifiers bitmask from the second stack argument + emitter.instruction("str x8, [sp, #96]"); // save ReflectionClass getModifiers bitmask + emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp x0, #3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("b.eq {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp x0, #4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("b.eq {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner + emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body( emitter, class_label, @@ -295,17 +315,17 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection box_label, ); emitter.label(box_label); - emitter.instruction("mov x0, #6"); // runtime tag 6 = object - emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload - emitter.instruction("mov x2, xzr"); // object payloads do not use a high word - emitter.instruction("bl __rt_mixed_from_value"); // box the Reflection owner object for eval - emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing emitter.label(fail_label); - emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #144]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #160"); // release the helper frame - emitter.instruction("ret"); // return the boxed reflection owner to Rust + emitter.instruction("ldp x29, x30, [sp, #144]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #160"); // release the helper frame + emitter.instruction("ret"); // return the boxed reflection owner to Rust } /// Emits the x86_64 Reflection owner materializer helper body. @@ -319,36 +339,36 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant_x"; let enum_unit_case_label = "__elephc_eval_reflection_owner_new_enum_unit_case_x"; let enum_backed_case_label = "__elephc_eval_reflection_owner_new_enum_backed_case_x"; - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 128"); // reserve slots for inputs, object, metadata arrays, and name parts - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the Reflection owner kind - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer - emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length - emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array - emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // save the boxed ReflectionClass interface-name array - emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // save the boxed ReflectionClass trait-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the boxed ReflectionClass method-name array from the first stack argument - emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the boxed ReflectionClass method-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the boxed ReflectionClass property-name array from the second stack argument - emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // save the boxed ReflectionClass property-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 32]"); // load ReflectionClass modifier flags from the third stack argument - emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags - emitter.instruction("mov rax, QWORD PTR [rbp + 40]"); // load ReflectionClass getModifiers bitmask from the fourth stack argument - emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save ReflectionClass getModifiers bitmask - emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass - emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner - emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod - emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner - emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty - emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner - emitter.instruction("cmp rdi, 3"); // owner kind 3 means ReflectionClassConstant - emitter.instruction(&format!("je {}", class_constant_label)); // allocate a ReflectionClassConstant owner - emitter.instruction("cmp rdi, 4"); // owner kind 4 means ReflectionEnumUnitCase - emitter.instruction(&format!("je {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner - emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase - emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner - emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 128"); // reserve slots for inputs, object, metadata arrays, and name parts + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the Reflection owner kind + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array + emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // save the boxed ReflectionClass interface-name array + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // save the boxed ReflectionClass trait-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the boxed ReflectionClass method-name array from the first stack argument + emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the boxed ReflectionClass method-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the boxed ReflectionClass property-name array from the second stack argument + emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // save the boxed ReflectionClass property-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 32]"); // load ReflectionClass modifier flags from the third stack argument + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags + emitter.instruction("mov rax, QWORD PTR [rbp + 40]"); // load ReflectionClass getModifiers bitmask from the fourth stack argument + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save ReflectionClass getModifiers bitmask + emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp rdi, 3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("je {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp rdi, 4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("je {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner + emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body( emitter, class_label, @@ -398,17 +418,17 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO box_label, ); emitter.label(box_label); - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload - emitter.instruction("xor esi, esi"); // object payloads do not use a high word - emitter.instruction("mov eax, 6"); // runtime tag 6 = object - emitter.instruction("call __rt_mixed_from_value"); // box the Reflection owner object for eval - emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("call __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing emitter.label(fail_label); - emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the boxed reflection owner to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reflection owner to Rust } /// Emits one ARM64 owner-kind allocation and slot-population body. @@ -422,14 +442,15 @@ fn emit_aarch64_owner_kind_body( ) { emitter.label(label); emit_alloc_reflection_owner_object_aarch64(emitter, layout); - emitter.instruction("str x0, [sp, #32]"); // save the unboxed Reflection owner object pointer + emitter.instruction("str x0, [sp, #32]"); // save the unboxed Reflection owner object pointer if set_name { emit_set_owner_name_property_aarch64(emitter, layout); } emit_set_owner_class_flags_property_aarch64(emitter, layout); + emit_set_owner_member_flags_property_aarch64(emitter, layout); emit_set_owner_metadata_arrays_property_aarch64(emitter, layout, fail_label); emit_set_owner_attrs_property_aarch64(emitter, layout, fail_label); - emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object + emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object } /// Emits one x86_64 owner-kind allocation and slot-population body. @@ -443,14 +464,15 @@ fn emit_x86_64_owner_kind_body( ) { emitter.label(label); emit_alloc_reflection_owner_object_x86_64(emitter, layout); - emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the unboxed Reflection owner object pointer + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the unboxed Reflection owner object pointer if set_name { emit_set_owner_name_property_x86_64(emitter, layout); } emit_set_owner_class_flags_property_x86_64(emitter, layout); + emit_set_owner_member_flags_property_x86_64(emitter, layout); emit_set_owner_metadata_arrays_property_x86_64(emitter, layout, fail_label); emit_set_owner_attrs_property_x86_64(emitter, layout, fail_label); - emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object + emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object } /// Allocates a zero-initialized ARM64 Reflection owner object payload. @@ -459,12 +481,12 @@ fn emit_alloc_reflection_owner_object_aarch64( layout: &ReflectionOwnerLayout, ) { let payload_size = 8 + layout.property_count * 16; - emitter.instruction(&format!("mov x0, #{}", payload_size)); // request Reflection owner object payload storage + emitter.instruction(&format!("mov x0, #{}", payload_size)); // request Reflection owner object payload storage abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #4"); // heap kind 4 marks the payload as an object - emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload - emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the Reflection owner class id - emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + emitter.instruction("mov x9, #4"); // heap kind 4 marks the payload as an object + emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero for index in 0..layout.property_count { let offset = 8 + index * 16; abi::emit_store_zero_to_address(emitter, "x0", offset); @@ -478,12 +500,15 @@ fn emit_alloc_reflection_owner_object_x86_64( layout: &ReflectionOwnerLayout, ) { let payload_size = 8 + layout.property_count * 16; - emitter.instruction(&format!("mov rax, {}", payload_size)); // request Reflection owner object payload storage + emitter.instruction(&format!("mov rax, {}", payload_size)); // request Reflection owner object payload storage abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload - emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id - emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 4 + )); // materialize the x86_64 object heap kind word + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero for index in 0..layout.property_count { let offset = 8 + index * 16; abi::emit_store_zero_to_address(emitter, "rax", offset); @@ -499,10 +524,10 @@ fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &Reflecti let Some(name_hi) = layout.name_hi else { return; }; - emitter.instruction("ldr x1, [sp, #8]"); // reload the reflected-name pointer for persistence - emitter.instruction("ldr x2, [sp, #16]"); // reload the reflected-name length for persistence - emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #8]"); // reload the reflected-name pointer for persistence + emitter.instruction("ldr x2, [sp, #16]"); // reload the reflected-name length for persistence + emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", name_lo); abi::emit_store_to_address(emitter, "x2", "x9", name_hi); let ( @@ -527,43 +552,43 @@ fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &Reflecti let found_label = "__elephc_eval_reflection_owner_name_scan_found"; let no_namespace_label = "__elephc_eval_reflection_owner_name_scan_none"; let store_parts_label = "__elephc_eval_reflection_owner_name_store_parts"; - emitter.instruction("ldr x3, [sp, #8]"); // reload the original reflected-name pointer for splitting - emitter.instruction("ldr x4, [sp, #16]"); // reload the original reflected-name length for splitting - emitter.instruction("mov x5, x4"); // start scanning from one byte past the final name byte - emitter.instruction(&format!("cbz x5, {}", no_namespace_label)); // empty names have no namespace component + emitter.instruction("ldr x3, [sp, #8]"); // reload the original reflected-name pointer for splitting + emitter.instruction("ldr x4, [sp, #16]"); // reload the original reflected-name length for splitting + emitter.instruction("mov x5, x4"); // start scanning from one byte past the final name byte + emitter.instruction(&format!("cbz x5, {}", no_namespace_label)); // empty names have no namespace component emitter.label(scan_loop_label); - emitter.instruction("sub x5, x5, #1"); // move the scan cursor to the previous byte - emitter.instruction("ldrb w6, [x3, x5]"); // read one reflected-name byte from the scan cursor - emitter.instruction("cmp w6, #92"); // compare against PHP namespace separator '\\' - emitter.instruction(&format!("b.eq {}", found_label)); // split at the final namespace separator - emitter.instruction(&format!("cbnz x5, {}", scan_loop_label)); // keep scanning until the first byte has been checked + emitter.instruction("sub x5, x5, #1"); // move the scan cursor to the previous byte + emitter.instruction("ldrb w6, [x3, x5]"); // read one reflected-name byte from the scan cursor + emitter.instruction("cmp w6, #92"); // compare against PHP namespace separator '\\' + emitter.instruction(&format!("b.eq {}", found_label)); // split at the final namespace separator + emitter.instruction(&format!("cbnz x5, {}", scan_loop_label)); // keep scanning until the first byte has been checked emitter.label(no_namespace_label); - emitter.instruction("str x3, [sp, #56]"); // short-name pointer is the original name pointer - emitter.instruction("str x4, [sp, #64]"); // short-name length is the full name length - emitter.instruction("str xzr, [sp, #72]"); // namespace length is zero for global names - emitter.instruction(&format!("b {}", store_parts_label)); // skip the namespaced split path + emitter.instruction("str x3, [sp, #56]"); // short-name pointer is the original name pointer + emitter.instruction("str x4, [sp, #64]"); // short-name length is the full name length + emitter.instruction("str xzr, [sp, #72]"); // namespace length is zero for global names + emitter.instruction(&format!("b {}", store_parts_label)); // skip the namespaced split path emitter.label(found_label); - emitter.instruction("add x6, x5, #1"); // compute the short-name byte offset after the separator - emitter.instruction("add x7, x3, x6"); // compute the short-name pointer - emitter.instruction("sub x8, x4, x6"); // compute the short-name length - emitter.instruction("str x7, [sp, #56]"); // save the short-name pointer across persistence calls - emitter.instruction("str x8, [sp, #64]"); // save the short-name length across persistence calls - emitter.instruction("str x5, [sp, #72]"); // namespace length is the separator offset + emitter.instruction("add x6, x5, #1"); // compute the short-name byte offset after the separator + emitter.instruction("add x7, x3, x6"); // compute the short-name pointer + emitter.instruction("sub x8, x4, x6"); // compute the short-name length + emitter.instruction("str x7, [sp, #56]"); // save the short-name pointer across persistence calls + emitter.instruction("str x8, [sp, #64]"); // save the short-name length across persistence calls + emitter.instruction("str x5, [sp, #72]"); // namespace length is the separator offset emitter.label(store_parts_label); - emitter.instruction("ldr x1, [sp, #8]"); // use the original name pointer for namespace persistence - emitter.instruction("ldr x2, [sp, #72]"); // reload the namespace byte length - emitter.instruction("bl __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #8]"); // use the original name pointer for namespace persistence + emitter.instruction("ldr x2, [sp, #72]"); // reload the namespace byte length + emitter.instruction("bl __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", namespace_name_lo); abi::emit_store_to_address(emitter, "x2", "x9", namespace_name_hi); - emitter.instruction("cmp x2, #0"); // detect whether a namespace component was present - emitter.instruction("cset x10, ne"); // materialize ReflectionClass::inNamespace() + emitter.instruction("cmp x2, #0"); // detect whether a namespace component was present + emitter.instruction("cset x10, ne"); // materialize ReflectionClass::inNamespace() abi::emit_store_to_address(emitter, "x10", "x9", in_namespace_lo); abi::emit_store_zero_to_address(emitter, "x9", in_namespace_hi); - emitter.instruction("ldr x1, [sp, #56]"); // reload the short-name pointer - emitter.instruction("ldr x2, [sp, #64]"); // reload the short-name byte length - emitter.instruction("bl __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #56]"); // reload the short-name pointer + emitter.instruction("ldr x2, [sp, #64]"); // reload the short-name byte length + emitter.instruction("bl __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", short_name_lo); abi::emit_store_to_address(emitter, "x2", "x9", short_name_hi); } @@ -576,10 +601,10 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio let Some(name_hi) = layout.name_hi else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the reflected-name pointer for persistence - emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the reflected-name length for persistence - emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the reflected-name pointer for persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the reflected-name length for persistence + emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", name_hi); let ( @@ -604,47 +629,47 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio let found_label = "__elephc_eval_reflection_owner_name_scan_found_x"; let no_namespace_label = "__elephc_eval_reflection_owner_name_scan_none_x"; let store_parts_label = "__elephc_eval_reflection_owner_name_store_parts_x"; - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the original reflected-name pointer for splitting - emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the original reflected-name length for splitting - emitter.instruction("mov r11, r9"); // start scanning from one byte past the final name byte - emitter.instruction("test r11, r11"); // check whether the reflected name is empty - emitter.instruction(&format!("jz {}", no_namespace_label)); // empty names have no namespace component + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the original reflected-name pointer for splitting + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the original reflected-name length for splitting + emitter.instruction("mov r11, r9"); // start scanning from one byte past the final name byte + emitter.instruction("test r11, r11"); // check whether the reflected name is empty + emitter.instruction(&format!("jz {}", no_namespace_label)); // empty names have no namespace component emitter.label(scan_loop_label); - emitter.instruction("sub r11, 1"); // move the scan cursor to the previous byte - emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // read one reflected-name byte from the scan cursor - emitter.instruction("cmp eax, 92"); // compare against PHP namespace separator '\\' - emitter.instruction(&format!("je {}", found_label)); // split at the final namespace separator - emitter.instruction("test r11, r11"); // check whether the first byte has been examined - emitter.instruction(&format!("jnz {}", scan_loop_label)); // keep scanning until the first byte has been checked + emitter.instruction("sub r11, 1"); // move the scan cursor to the previous byte + emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // read one reflected-name byte from the scan cursor + emitter.instruction("cmp eax, 92"); // compare against PHP namespace separator '\\' + emitter.instruction(&format!("je {}", found_label)); // split at the final namespace separator + emitter.instruction("test r11, r11"); // check whether the first byte has been examined + emitter.instruction(&format!("jnz {}", scan_loop_label)); // keep scanning until the first byte has been checked emitter.label(no_namespace_label); - emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // short-name pointer is the original name pointer - emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // short-name length is the full name length - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // namespace length is zero for global names - emitter.instruction(&format!("jmp {}", store_parts_label)); // skip the namespaced split path + emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // short-name pointer is the original name pointer + emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // short-name length is the full name length + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // namespace length is zero for global names + emitter.instruction(&format!("jmp {}", store_parts_label)); // skip the namespaced split path emitter.label(found_label); - emitter.instruction("lea rax, [r11 + 1]"); // compute the short-name byte offset after the separator - emitter.instruction("lea r10, [r8 + rax]"); // compute the short-name pointer - emitter.instruction("mov rcx, r9"); // copy the full name length before subtracting the prefix - emitter.instruction("sub rcx, rax"); // compute the short-name length - emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the short-name pointer across persistence calls - emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // save the short-name length across persistence calls - emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // namespace length is the separator offset + emitter.instruction("lea rax, [r11 + 1]"); // compute the short-name byte offset after the separator + emitter.instruction("lea r10, [r8 + rax]"); // compute the short-name pointer + emitter.instruction("mov rcx, r9"); // copy the full name length before subtracting the prefix + emitter.instruction("sub rcx, rax"); // compute the short-name length + emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the short-name pointer across persistence calls + emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // save the short-name length across persistence calls + emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // namespace length is the separator offset emitter.label(store_parts_label); - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // use the original name pointer for namespace persistence - emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the namespace byte length - emitter.instruction("call __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // use the original name pointer for namespace persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the namespace byte length + emitter.instruction("call __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", namespace_name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", namespace_name_hi); - emitter.instruction("test rdx, rdx"); // detect whether a namespace component was present - emitter.instruction("setne al"); // materialize ReflectionClass::inNamespace() - emitter.instruction("movzx eax, al"); // widen the namespace boolean to a full word + emitter.instruction("test rdx, rdx"); // detect whether a namespace component was present + emitter.instruction("setne al"); // materialize ReflectionClass::inNamespace() + emitter.instruction("movzx eax, al"); // widen the namespace boolean to a full word abi::emit_store_to_address(emitter, "rax", "r10", in_namespace_lo); abi::emit_store_zero_to_address(emitter, "r10", in_namespace_hi); - emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the short-name pointer - emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // reload the short-name byte length - emitter.instruction("call __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the short-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // reload the short-name byte length + emitter.instruction("call __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", short_name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", short_name_hi); } @@ -654,64 +679,58 @@ fn emit_set_owner_class_flags_property_aarch64( emitter: &mut Emitter, layout: &ReflectionOwnerLayout, ) { - let Some(is_final_lo) = layout.is_final_lo else { - return; - }; - let Some(is_final_hi) = layout.is_final_hi else { - return; - }; - let Some(is_abstract_lo) = layout.is_abstract_lo else { - return; - }; - let Some(is_abstract_hi) = layout.is_abstract_hi else { - return; - }; - let Some(is_interface_lo) = layout.is_interface_lo else { - return; - }; - let Some(is_interface_hi) = layout.is_interface_hi else { - return; - }; - let Some(is_trait_lo) = layout.is_trait_lo else { - return; - }; - let Some(is_trait_hi) = layout.is_trait_hi else { - return; - }; - let Some(is_enum_lo) = layout.is_enum_lo else { - return; - }; - let Some(is_enum_hi) = layout.is_enum_hi else { - return; - }; - let Some(modifiers_lo) = layout.modifiers_lo else { - return; - }; - let Some(modifiers_hi) = layout.modifiers_hi else { + let ( + Some(is_final_lo), + Some(is_final_hi), + Some(is_abstract_lo), + Some(is_abstract_hi), + Some(is_interface_lo), + Some(is_interface_hi), + Some(is_trait_lo), + Some(is_trait_hi), + Some(is_enum_lo), + Some(is_enum_hi), + Some(modifiers_lo), + Some(modifiers_hi), + ) = ( + layout.is_final_lo, + layout.is_final_hi, + layout.is_abstract_lo, + layout.is_abstract_hi, + layout.is_interface_lo, + layout.is_interface_hi, + layout.is_trait_lo, + layout.is_trait_hi, + layout.is_enum_lo, + layout.is_enum_hi, + layout.modifiers_lo, + layout.modifiers_hi, + ) + else { return; }; - emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer - emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); - emitter.instruction("lsr x10, x11, #1"); // move the abstract-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean + emitter.instruction("lsr x10, x11, #1"); // move the abstract-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); - emitter.instruction("lsr x10, x11, #2"); // move the interface bit into position - emitter.instruction("and x10, x10, #1"); // extract the interface flag as a boolean + emitter.instruction("lsr x10, x11, #2"); // move the interface bit into position + emitter.instruction("and x10, x10, #1"); // extract the interface flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_interface_lo); abi::emit_store_zero_to_address(emitter, "x9", is_interface_hi); - emitter.instruction("lsr x10, x11, #3"); // move the trait bit into position - emitter.instruction("and x10, x10, #1"); // extract the trait flag as a boolean + emitter.instruction("lsr x10, x11, #3"); // move the trait bit into position + emitter.instruction("and x10, x10, #1"); // extract the trait flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_trait_lo); abi::emit_store_zero_to_address(emitter, "x9", is_trait_hi); - emitter.instruction("lsr x10, x11, #4"); // move the enum bit into position - emitter.instruction("and x10, x10, #1"); // extract the enum flag as a boolean + emitter.instruction("lsr x10, x11, #4"); // move the enum bit into position + emitter.instruction("and x10, x10, #1"); // extract the enum flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_enum_lo); abi::emit_store_zero_to_address(emitter, "x9", is_enum_hi); - emitter.instruction("ldr x10, [sp, #96]"); // reload PHP ReflectionClass::getModifiers() bitmask + emitter.instruction("ldr x10, [sp, #96]"); // reload PHP ReflectionClass::getModifiers() bitmask abi::emit_store_to_address(emitter, "x10", "x9", modifiers_lo); abi::emit_store_zero_to_address(emitter, "x9", modifiers_hi); } @@ -721,71 +740,195 @@ fn emit_set_owner_class_flags_property_x86_64( emitter: &mut Emitter, layout: &ReflectionOwnerLayout, ) { - let Some(is_final_lo) = layout.is_final_lo else { - return; - }; - let Some(is_final_hi) = layout.is_final_hi else { - return; - }; - let Some(is_abstract_lo) = layout.is_abstract_lo else { - return; - }; - let Some(is_abstract_hi) = layout.is_abstract_hi else { - return; - }; - let Some(is_interface_lo) = layout.is_interface_lo else { - return; - }; - let Some(is_interface_hi) = layout.is_interface_hi else { - return; - }; - let Some(is_trait_lo) = layout.is_trait_lo else { - return; - }; - let Some(is_trait_hi) = layout.is_trait_hi else { + let ( + Some(is_final_lo), + Some(is_final_hi), + Some(is_abstract_lo), + Some(is_abstract_hi), + Some(is_interface_lo), + Some(is_interface_hi), + Some(is_trait_lo), + Some(is_trait_hi), + Some(is_enum_lo), + Some(is_enum_hi), + Some(modifiers_lo), + Some(modifiers_hi), + ) = ( + layout.is_final_lo, + layout.is_final_hi, + layout.is_abstract_lo, + layout.is_abstract_hi, + layout.is_interface_lo, + layout.is_interface_hi, + layout.is_trait_lo, + layout.is_trait_hi, + layout.is_enum_lo, + layout.is_enum_hi, + layout.modifiers_lo, + layout.modifiers_hi, + ) + else { return; }; - let Some(is_enum_lo) = layout.is_enum_lo else { + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit + emitter.instruction("and rax, 1"); // extract the final-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit + emitter.instruction("shr rax, 1"); // move the abstract-class bit into position + emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the interface bit + emitter.instruction("shr rax, 2"); // move the interface bit into position + emitter.instruction("and rax, 1"); // extract the interface flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_interface_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_interface_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the trait bit + emitter.instruction("shr rax, 3"); // move the trait bit into position + emitter.instruction("and rax, 1"); // extract the trait flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_trait_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_trait_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the enum bit + emitter.instruction("shr rax, 4"); // move the enum bit into position + emitter.instruction("and rax, 1"); // extract the enum flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_enum_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_enum_hi); + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP ReflectionClass::getModifiers() bitmask + abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); + abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); +} + +/// Stores incoming ARM64 ReflectionMethod/ReflectionProperty boolean flags. +fn emit_set_owner_member_flags_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let ( + Some(is_static_lo), + Some(is_static_hi), + Some(is_public_lo), + Some(is_public_hi), + Some(is_protected_lo), + Some(is_protected_hi), + Some(is_private_lo), + Some(is_private_hi), + ) = ( + layout.is_static_lo, + layout.is_static_hi, + layout.is_public_lo, + layout.is_public_hi, + layout.is_protected_lo, + layout.is_protected_hi, + layout.is_private_lo, + layout.is_private_hi, + ) + else { return; }; - let Some(is_enum_hi) = layout.is_enum_hi else { + emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection member predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract the static-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_static_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_static_hi); + emitter.instruction("lsr x10, x11, #1"); // move the public-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the public-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_public_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_public_hi); + emitter.instruction("lsr x10, x11, #2"); // move the protected-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the protected-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_protected_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_protected_hi); + emitter.instruction("lsr x10, x11, #3"); // move the private-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the private-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_private_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_private_hi); + let (Some(is_final_lo), Some(is_final_hi), Some(is_abstract_lo), Some(is_abstract_hi)) = ( + layout.is_final_lo, + layout.is_final_hi, + layout.is_abstract_lo, + layout.is_abstract_hi, + ) else { return; }; - let Some(modifiers_lo) = layout.modifiers_lo else { + emitter.instruction("lsr x10, x11, #4"); // move the final-method bit into position + emitter.instruction("and x10, x10, #1"); // extract the final-method flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); + emitter.instruction("lsr x10, x11, #5"); // move the abstract-method bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-method flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); +} + +/// Stores incoming x86_64 ReflectionMethod/ReflectionProperty boolean flags. +fn emit_set_owner_member_flags_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let ( + Some(is_static_lo), + Some(is_static_hi), + Some(is_public_lo), + Some(is_public_hi), + Some(is_protected_lo), + Some(is_protected_hi), + Some(is_private_lo), + Some(is_private_hi), + ) = ( + layout.is_static_lo, + layout.is_static_hi, + layout.is_public_lo, + layout.is_public_hi, + layout.is_protected_lo, + layout.is_protected_hi, + layout.is_private_lo, + layout.is_private_hi, + ) + else { return; }; - let Some(modifiers_hi) = layout.modifiers_hi else { + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection member predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting the static bit + emitter.instruction("and rax, 1"); // extract the static-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_static_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_static_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the public bit + emitter.instruction("shr rax, 1"); // move the public-member bit into position + emitter.instruction("and rax, 1"); // extract the public-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_public_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_public_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the protected bit + emitter.instruction("shr rax, 2"); // move the protected-member bit into position + emitter.instruction("and rax, 1"); // extract the protected-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_protected_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_protected_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the private bit + emitter.instruction("shr rax, 3"); // move the private-member bit into position + emitter.instruction("and rax, 1"); // extract the private-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_private_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_private_hi); + let (Some(is_final_lo), Some(is_final_hi), Some(is_abstract_lo), Some(is_abstract_hi)) = ( + layout.is_final_lo, + layout.is_final_hi, + layout.is_abstract_lo, + layout.is_abstract_hi, + ) else { return; }; - emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit - emitter.instruction("and rax, 1"); // extract the final-class flag as a boolean + emitter.instruction("shr rax, 4"); // move the final-method bit into position + emitter.instruction("and rax, 1"); // extract the final-method flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit - emitter.instruction("shr rax, 1"); // move the abstract-class bit into position - emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean + emitter.instruction("shr rax, 5"); // move the abstract-method bit into position + emitter.instruction("and rax, 1"); // extract the abstract-method flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the interface bit - emitter.instruction("shr rax, 2"); // move the interface bit into position - emitter.instruction("and rax, 1"); // extract the interface flag as a boolean - abi::emit_store_to_address(emitter, "rax", "r10", is_interface_lo); - abi::emit_store_zero_to_address(emitter, "r10", is_interface_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the trait bit - emitter.instruction("shr rax, 3"); // move the trait bit into position - emitter.instruction("and rax, 1"); // extract the trait flag as a boolean - abi::emit_store_to_address(emitter, "rax", "r10", is_trait_lo); - abi::emit_store_zero_to_address(emitter, "r10", is_trait_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the enum bit - emitter.instruction("shr rax, 4"); // move the enum bit into position - emitter.instruction("and rax, 1"); // extract the enum flag as a boolean - abi::emit_store_to_address(emitter, "rax", "r10", is_enum_lo); - abi::emit_store_zero_to_address(emitter, "r10", is_enum_hi); - emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP ReflectionClass::getModifiers() bitmask - abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); - abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); } /// Stores incoming ARM64 ReflectionClass metadata name arrays. @@ -854,16 +997,16 @@ fn emit_set_owner_metadata_array_slot_aarch64( high_offset: usize, fail_label: &str, ) { - emitter.instruction(&format!("ldr x0, [sp, #{}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null metadata-name arrays - emitter.instruction("bl __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer - emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array metadata-name metadata - emitter.instruction("str x1, [sp, #40]"); // save the unboxed metadata-name array across incref - emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register - emitter.instruction("bl __rt_incref"); // retain the metadata-name array for ReflectionClass storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained metadata-name array payload - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction(&format!("ldr x0, [sp, #{}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null metadata-name arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array metadata-name metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed metadata-name array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the metadata-name array for ReflectionClass storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained metadata-name array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", low_offset); abi::emit_load_int_immediate(emitter, "x10", 4); abi::emit_store_to_address(emitter, "x10", "x9", high_offset); @@ -940,17 +1083,17 @@ fn emit_set_owner_metadata_array_slot_x86_64( } else { format!("+ {}", boxed_slot) }; - emitter.instruction(&format!("mov rax, QWORD PTR [rbp {}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array - emitter.instruction("test rax, rax"); // check whether the boxed metadata-name array is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null metadata-name arrays - emitter.instruction("call __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer - emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("jne {}", fail_label)); // reject non-array metadata-name metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed metadata-name array across incref - emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register - emitter.instruction("call __rt_incref"); // retain the metadata-name array for ReflectionClass storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained metadata-name array payload - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction(&format!("mov rax, QWORD PTR [rbp {}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array + emitter.instruction("test rax, rax"); // check whether the boxed metadata-name array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null metadata-name arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array metadata-name metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed metadata-name array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the metadata-name array for ReflectionClass storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained metadata-name array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", low_offset); abi::emit_load_int_immediate(emitter, "r11", 4); abi::emit_store_to_address(emitter, "r11", "r10", high_offset); @@ -962,16 +1105,16 @@ fn emit_set_owner_attrs_property_aarch64( layout: &ReflectionOwnerLayout, fail_label: &str, ) { - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed ReflectionAttribute array - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null attribute arrays - emitter.instruction("bl __rt_mixed_unbox"); // expose the attribute array tag and payload pointer - emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array attribute metadata - emitter.instruction("str x1, [sp, #40]"); // save the unboxed attribute array across incref - emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register - emitter.instruction("bl __rt_incref"); // retain the attribute array for Reflection owner storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained attribute array payload - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed ReflectionAttribute array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed attribute array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained attribute array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", layout.attrs_lo); abi::emit_load_int_immediate(emitter, "x10", 4); abi::emit_store_to_address(emitter, "x10", "x9", layout.attrs_hi); @@ -983,17 +1126,17 @@ fn emit_set_owner_attrs_property_x86_64( layout: &ReflectionOwnerLayout, fail_label: &str, ) { - emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed ReflectionAttribute array - emitter.instruction("test rax, rax"); // check whether the boxed attribute array is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null attribute arrays - emitter.instruction("call __rt_mixed_unbox"); // expose the attribute array tag and payload pointer - emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("jne {}", fail_label)); // reject non-array attribute metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed attribute array across incref - emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register - emitter.instruction("call __rt_incref"); // retain the attribute array for Reflection owner storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained attribute array payload - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed ReflectionAttribute array + emitter.instruction("test rax, rax"); // check whether the boxed attribute array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed attribute array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained attribute array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", layout.attrs_lo); abi::emit_load_int_immediate(emitter, "r11", 4); abi::emit_store_to_address(emitter, "r11", "r10", layout.attrs_hi); diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 0aa9af0d59..9e42da0300 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -36,6 +36,18 @@ struct ReflectionOwnerMetadata { is_trait: bool, is_enum: bool, modifiers: i64, + member_flags: ReflectionMemberFlags, +} + +/// Boolean metadata exposed by ReflectionMethod and ReflectionProperty predicates. +#[derive(Clone, Copy, Default)] +struct ReflectionMemberFlags { + is_static: bool, + is_public: bool, + is_protected: bool, + is_private: bool, + is_final: bool, + is_abstract: bool, } /// Returns true for reflection owner classes that need metadata-aware construction. @@ -118,6 +130,7 @@ pub(super) fn lower_reflection_owner_new( emit_reflection_bool_property(ctx, "__is_enum", metadata.is_enum)?; emit_reflection_int_property_by_name(ctx, "__modifiers", metadata.modifiers)?; } + emit_reflection_member_flag_properties(ctx, class_name, metadata.member_flags)?; let result = inst .result .ok_or_else(|| CodegenIrError::invalid_module("reflection object_new missing result"))?; @@ -559,6 +572,7 @@ fn reflection_class_metadata( info.is_readonly_class, is_enum, ), + member_flags: ReflectionMemberFlags::default(), }); } if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { @@ -624,6 +638,7 @@ fn reflection_method_metadata( is_trait: false, is_enum: false, modifiers: 0, + member_flags: reflection_method_member_flags(info, &method_key)?, }) }) .unwrap_or_else(empty_reflection_metadata)) @@ -658,6 +673,7 @@ fn reflection_property_metadata( is_trait: false, is_enum: false, modifiers: 0, + member_flags: reflection_property_member_flags(info, &property_name)?, }) }) .unwrap_or_else(empty_reflection_metadata)) @@ -693,6 +709,7 @@ fn reflection_class_constant_metadata( is_trait: false, is_enum: false, modifiers: 0, + member_flags: ReflectionMemberFlags::default(), }); } Ok( @@ -722,6 +739,7 @@ fn reflection_class_constant_metadata( is_trait: false, is_enum: false, modifiers: 0, + member_flags: ReflectionMemberFlags::default(), } }) .unwrap_or_else(empty_reflection_metadata), @@ -758,6 +776,7 @@ fn reflection_enum_case_metadata( is_trait: false, is_enum: false, modifiers: 0, + member_flags: ReflectionMemberFlags::default(), }) .unwrap_or_else(empty_reflection_metadata), ) @@ -910,6 +929,85 @@ fn reflection_property_visible_from_class( .unwrap_or(false) } +/// Returns ReflectionMethod predicate flags for a method visible on one class. +fn reflection_method_member_flags( + info: &crate::types::ClassInfo, + method_key: &str, +) -> Option { + if info.methods.contains_key(method_key) { + let visibility = info + .method_visibilities + .get(method_key) + .unwrap_or(&Visibility::Public); + return Some(reflection_member_flags( + false, + visibility, + info.final_methods.contains(method_key), + !info.method_impl_classes.contains_key(method_key), + )); + } + if info.static_methods.contains_key(method_key) { + let visibility = info + .static_method_visibilities + .get(method_key) + .unwrap_or(&Visibility::Public); + return Some(reflection_member_flags( + true, + visibility, + info.final_static_methods.contains(method_key), + !info.static_method_impl_classes.contains_key(method_key), + )); + } + None +} + +/// Returns ReflectionProperty predicate flags for a property visible on one class. +fn reflection_property_member_flags( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + if info + .properties + .iter() + .any(|(name, _)| name == property_name) + { + let visibility = info + .property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + return Some(reflection_member_flags(false, visibility, false, false)); + } + if info + .static_properties + .iter() + .any(|(name, _)| name == property_name) + { + let visibility = info + .static_property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + return Some(reflection_member_flags(true, visibility, false, false)); + } + None +} + +/// Builds common ReflectionMethod/ReflectionProperty predicate flags. +fn reflection_member_flags( + is_static: bool, + visibility: &Visibility, + is_final: bool, + is_abstract: bool, +) -> ReflectionMemberFlags { + ReflectionMemberFlags { + is_static, + is_public: visibility == &Visibility::Public, + is_protected: visibility == &Visibility::Protected, + is_private: visibility == &Visibility::Private, + is_final, + is_abstract, + } +} + /// Returns PHP case-insensitive method names declared by an interface and its parents. fn reflection_interface_method_names( ctx: &FunctionContext<'_>, @@ -1014,6 +1112,7 @@ fn class_like_reflection_metadata( is_trait, is_enum, modifiers: if is_enum { 32 } else { 0 }, + member_flags: ReflectionMemberFlags::default(), } } @@ -1061,6 +1160,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { is_trait: false, is_enum: false, modifiers: 0, + member_flags: ReflectionMemberFlags::default(), } } @@ -1270,50 +1370,101 @@ fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) /// Appends ReflectionClass metadata names to the current ARM64 result array. fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result } /// Appends ReflectionClass metadata names to the current x86_64 result array. fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings - ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result } -/// Stores one boolean property on the current ReflectionClass object result. -fn emit_reflection_bool_property( +/// Stores ReflectionMethod/ReflectionProperty boolean predicate slots when supported. +fn emit_reflection_member_flag_properties( + ctx: &mut FunctionContext<'_>, + class_name: &str, + flags: ReflectionMemberFlags, +) -> Result<()> { + match class_name { + "ReflectionMethod" => { + emit_reflection_owner_bool_property(ctx, class_name, "__is_static", flags.is_static)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_protected", + flags.is_protected, + )?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_private", flags.is_private)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_final", flags.is_final)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_abstract", + flags.is_abstract, + )?; + } + "ReflectionProperty" => { + emit_reflection_owner_bool_property(ctx, class_name, "__is_static", flags.is_static)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_protected", + flags.is_protected, + )?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_private", flags.is_private)?; + } + _ => {} + } + Ok(()) +} + +/// Stores one boolean property on the current Reflection owner object result. +fn emit_reflection_owner_bool_property( ctx: &mut FunctionContext<'_>, + class_name: &str, property_name: &str, value: bool, ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; emit_reflection_int_property(ctx, i64::from(value), low_offset, low_offset + 8); Ok(()) } +/// Stores one boolean property on the current ReflectionClass object result. +fn emit_reflection_bool_property( + ctx: &mut FunctionContext<'_>, + property_name: &str, + value: bool, +) -> Result<()> { + emit_reflection_owner_bool_property(ctx, "ReflectionClass", property_name, value) +} + /// Stores one integer property on the current ReflectionClass object result. fn emit_reflection_int_property_by_name( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index dcab147478..a5473d832b 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -874,6 +874,7 @@ fn builtin_reflection_owner_class( )); methods.push(builtin_reflection_class_string_method("getName", "__name")); } + add_reflection_member_flag_methods(name, &mut properties, &mut methods); properties.push(builtin_property( "__attrs", Visibility::Private, @@ -896,6 +897,42 @@ fn builtin_reflection_owner_class( } } +/// Adds member visibility/staticity predicates for method and property reflection owners. +fn add_reflection_member_flag_methods( + class_name: &str, + properties: &mut Vec, + methods: &mut Vec, +) { + let common_flags = [ + ("__is_static", "isStatic"), + ("__is_public", "isPublic"), + ("__is_protected", "isProtected"), + ("__is_private", "isPrivate"), + ]; + if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { + for (property, method) in common_flags { + properties.push(builtin_property( + property, + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method(method, property)); + } + } + if class_name == "ReflectionMethod" { + for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { + properties.push(builtin_property( + property, + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method(method, property)); + } + } +} + /// Builds a public `__construct` method for a reflection owner class using the /// provided parameter list: each tuple is (name, type_expr, default, by_ref). fn builtin_reflection_owner_constructor_method( @@ -1028,6 +1065,20 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Int; } } + if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { + for method_name in ["isstatic", "ispublic", "isprotected", "isprivate"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + } + if class_name == "ReflectionMethod" { + for method_name in ["isfinal", "isabstract"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionAttribute".to_string()))); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1276c62ae2..d5c9e5afe8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5740,6 +5740,58 @@ echo $backed->hasProperty("value") ? "Y" : "y";'); assert_eq!(out.stdout, "MPSx:ChTw:IJK:RU:ELNv:BY"); } +/// Verifies eval ReflectionMethod and ReflectionProperty expose member predicates through the bridge. +#[test] +fn test_eval_reflection_member_predicates() { + let out = compile_and_run_capture( + r#"isStatic() ? "S" : "s"; +echo $baseStatic->isProtected() ? "P" : "p"; +echo $baseStatic->isPublic() ? "U" : "u"; +echo $baseStatic->isPrivate() ? "R" : "r"; +echo $baseStatic->isFinal() ? "F" : "f"; +echo $baseStatic->isAbstract() ? "A" : "a"; +echo ":"; +$abstractMethod = new ReflectionMethod("EvalReflectMemberBase", "mustImplement"); +echo $abstractMethod->isAbstract() ? "A" : "a"; +echo $abstractMethod->isProtected() ? "P" : "p"; +echo $abstractMethod->isStatic() ? "S" : "s"; +echo ":"; +$finalMethod = new ReflectionMethod("EvalReflectMemberChild", "locked"); +echo $finalMethod->isFinal() ? "F" : "f"; +echo $finalMethod->isPublic() ? "U" : "u"; +echo $finalMethod->isStatic() ? "S" : "s"; +echo ":"; +$staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); +echo $staticProp->isStatic() ? "S" : "s"; +echo $staticProp->isPrivate() ? "R" : "r"; +echo $staticProp->isProtected() ? "P" : "p"; +echo ":"; +$visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); +echo $visibleProp->isStatic() ? "S" : "s"; +echo $visibleProp->isProtected() ? "P" : "p"; +echo $visibleProp->isPublic() ? "U" : "u";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "SPurfa:APs:FUs:SRp:sPu"); +} + /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. #[test] fn test_eval_reflection_constant_and_enum_case_attributes() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index e5921aabb0..34dc6e1628 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1466,6 +1466,54 @@ echo $attrs[0]->getArguments()[0]; assert_eq!(out, "id/1/Column/id"); } +/// Verifies that `ReflectionMethod` and `ReflectionProperty` expose member +/// visibility, staticity, finality, and abstractness predicates. +#[test] +fn test_reflection_member_predicates_report_method_and_property_flags() { + let out = compile_and_run( + r#"isStatic() ? "S" : "s"; +echo $baseStatic->isProtected() ? "P" : "p"; +echo $baseStatic->isPublic() ? "U" : "u"; +echo $baseStatic->isPrivate() ? "R" : "r"; +echo $baseStatic->isFinal() ? "F" : "f"; +echo $baseStatic->isAbstract() ? "A" : "a"; +echo ":"; +$abstractMethod = new ReflectionMethod(ReflectMemberBase::class, "mustImplement"); +echo $abstractMethod->isAbstract() ? "A" : "a"; +echo $abstractMethod->isProtected() ? "P" : "p"; +echo $abstractMethod->isStatic() ? "S" : "s"; +echo ":"; +$finalMethod = new ReflectionMethod(ReflectMemberChild::class, "locked"); +echo $finalMethod->isFinal() ? "F" : "f"; +echo $finalMethod->isPublic() ? "U" : "u"; +echo $finalMethod->isStatic() ? "S" : "s"; +echo ":"; +$staticProp = new ReflectionProperty(ReflectMemberChild::class, "token"); +echo $staticProp->isStatic() ? "S" : "s"; +echo $staticProp->isPrivate() ? "R" : "r"; +echo $staticProp->isProtected() ? "P" : "p"; +echo ":"; +$visibleProp = new ReflectionProperty(ReflectMemberChild::class, "visible"); +echo $visibleProp->isStatic() ? "S" : "s"; +echo $visibleProp->isProtected() ? "P" : "p"; +echo $visibleProp->isPublic() ? "U" : "u"; +"#, + ); + assert_eq!(out, "SPurfa:APs:FUs:SRp:sPu"); +} + /// Verifies that `ReflectionClassConstant` and enum-case reflectors expose /// attribute name, arguments, `getName()`, and `newInstance()` data. #[test] From aefc48f5d299d0834fe3fe5e507ae3588a814f6e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 18 Jun 2026 23:42:18 +0200 Subject: [PATCH 0362/1208] Support ReflectionClass readonly predicate --- .../elephc-eval/src/interpreter/reflection.rs | 4 +++ .../tests/builtins_class_metadata.rs | 28 +++++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 6 ++++ docs/internals/the-runtime.md | 2 +- docs/php/classes.md | 3 +- docs/php/eval.md | 3 +- src/codegen/eval_reflection_owner_helpers.rs | 22 +++++++++++++++ src/codegen/lower_inst/objects/reflection.rs | 10 +++++++ src/types/checker/builtin_types/reflection.rs | 8 ++++++ tests/codegen/eval.rs | 27 ++++++++++++++++++ tests/codegen/oop/attributes.rs | 22 +++++++++++++++ 11 files changed, 132 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 0280b4c698..fa8020e0dc 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -17,6 +17,7 @@ const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; const EVAL_REFLECTION_CLASS_FLAG_INTERFACE: u64 = 4; const EVAL_REFLECTION_CLASS_FLAG_TRAIT: u64 = 8; const EVAL_REFLECTION_CLASS_FLAG_ENUM: u64 = 16; +const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; @@ -326,6 +327,9 @@ fn eval_reflection_class_like_attributes( if is_enum { flags |= EVAL_REFLECTION_CLASS_FLAG_ENUM; } + if class.is_readonly_class() && !is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; + } let modifiers = eval_reflection_class_modifiers( class.is_final(), class.is_abstract(), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c517bcc797..98414b75de 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -334,6 +334,34 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass exposes eval readonly class metadata. +#[test] +fn execute_program_reflects_eval_class_readonly_predicate() { + let program = parse_fragment( + br#"class EvalReadonlyPlain {} +readonly class EvalReadonlyReflect {} +final readonly class EvalReadonlyFinalReflect {} +enum EvalReadonlyEnumReflect { case Ready; } +interface EvalReadonlyIface {} +trait EvalReadonlyTrait {} +echo (new ReflectionClass("EvalReadonlyPlain"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyFinalReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyEnumReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyIface"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyTrait"))->isReadOnly() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "rRRrrr"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass reports eval class-like method and property membership. #[test] fn execute_program_reflects_eval_class_member_existence() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 8065a45e83..06cd3aa91f 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -141,6 +141,10 @@ impl FakeOps { Self::object_property(&properties, "__is_enum") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isreadonly") if args.is_empty() => { + Self::object_property(&properties, "__is_readonly") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) } @@ -311,6 +315,7 @@ impl FakeOps { let is_interface = self.bool_value((flags & 4) != 0)?; let is_trait = self.bool_value((flags & 8) != 0)?; let is_enum = self.bool_value((flags & 16) != 0)?; + let is_readonly = self.bool_value((flags & 32) != 0)?; let modifiers = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { @@ -324,6 +329,7 @@ impl FakeOps { properties.push(("__is_interface".to_string(), is_interface)); properties.push(("__is_trait".to_string(), is_trait)); properties.push(("__is_enum".to_string(), is_enum)); + properties.push(("__is_readonly".to_string(), is_readonly)); properties.push(("__modifiers".to_string(), modifiers)); properties.push(("__short_name".to_string(), short_name)); properties.push(("__namespace_name".to_string(), namespace_name)); diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 2ecbd43398..a7e2b15c27 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -19,7 +19,7 @@ The generated EIR backend treats `eval()` as a strong dynamic barrier. At the ca Closure bodies that can execute `eval()` participate in the same dynamic barrier rules. By-value captures flush and reload only the closure-local captured copy. By-reference captures store a ref-cell pointer shared with the source variable; when such a capture may cross an eval barrier, EIR lowering widens the shared cell to boxed `Mixed` before closure creation so the caller and closure agree on the representation after eval writes through the cell. -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)` and class-specific and union `catch` clauses, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with public properties, public methods, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch` handling with `Throwable` and class-specific and union clauses, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. +The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)` and class-specific and union `catch` clauses, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with inheritance, visibility, readonly/final/abstract modifiers, traits, interfaces, static members, constants, attributes, enums, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch` handling with `Throwable` and class-specific and union clauses, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. diff --git a/docs/php/classes.md b/docs/php/classes.md index 577b06e753..f94170b35d 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1089,6 +1089,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an interface | | `ReflectionClass::isTrait()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is a trait | | `ReflectionClass::isEnum()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an enum | +| `ReflectionClass::isReadOnly()` | `new ReflectionClass($class_name)` | Return whether the reflected class is a `readonly class` | | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | | `ReflectionClass::hasMethod()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named method; method lookup is case-insensitive | | `ReflectionClass::hasProperty()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named property; property lookup is case-sensitive | @@ -1167,7 +1168,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, and `getTraitNames()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, and `getTraitNames()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index c155c6518e..542911c550 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -159,7 +159,8 @@ derive namespace-aware parts from the resolved eval class-like name. `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval class-like metadata, including PHP-compatible enum finality and class-like kind checks for eval interfaces, -traits, and enums. `ReflectionClass::getModifiers()` returns PHP's +traits, and enums. `ReflectionClass::isReadOnly()` reports eval `readonly class` +metadata. `ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::IS_*` modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval classes and parent interfaces for eval interfaces, while diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 9585ec34d7..fea8778690 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -50,6 +50,8 @@ struct ReflectionOwnerLayout { is_trait_hi: Option, is_enum_lo: Option, is_enum_hi: Option, + is_readonly_lo: Option, + is_readonly_hi: Option, modifiers_lo: Option, modifiers_hi: Option, in_namespace_lo: Option, @@ -158,6 +160,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option ReflectionOwnerMetadata { is_interface: false, is_trait: false, is_enum: false, + is_readonly: false, modifiers: 0, member_flags: ReflectionMemberFlags::default(), } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index a5473d832b..737e2792d1 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -585,6 +585,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__is_readonly", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), builtin_property( "__modifiers", Visibility::Private, @@ -652,6 +658,7 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_bool_method("isInterface", "__is_interface"), builtin_reflection_class_bool_method("isTrait", "__is_trait"), builtin_reflection_class_bool_method("isEnum", "__is_enum"), + builtin_reflection_class_bool_method("isReadOnly", "__is_readonly"), builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), @@ -1049,6 +1056,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isinterface", "istrait", "isenum", + "isreadonly", "hasmethod", "hasproperty", ] { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d5c9e5afe8..628ccc7b13 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5668,6 +5668,33 @@ echo (new ReflectionClass("EvalModifierTrait"))->getModifiers();'); assert_eq!(out.stdout, "64:32:65536:65568:32:0:0"); } +/// Verifies eval ReflectionClass reports readonly class status through the bridge. +#[test] +fn test_eval_reflection_class_readonly_predicate() { + let out = compile_and_run_capture( + r#"isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyFinalReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyEnumReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyIface"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyTrait"))->isReadOnly() ? "R" : "r";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "rRRrrr"); +} + /// Verifies eval ReflectionClass reports method and property membership through the bridge. #[test] fn test_eval_reflection_class_member_existence() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 34dc6e1628..981c8706c2 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1184,6 +1184,28 @@ echo ReflectionClass::IS_READONLY; assert_eq!(out, "64:32:65536:65568:32:0:0:16:32:64:65536"); } +/// Verifies that `ReflectionClass::isReadOnly()` reports readonly class metadata. +#[test] +fn test_reflection_class_is_readonly_reports_class_metadata() { + let out = compile_and_run( + r#"isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyReflect::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyFinalReflect::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyEnumReflect::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyIface::class))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass(StaticReadonlyTrait::class))->isReadOnly() ? "R" : "r"; +"#, + ); + assert_eq!(out, "rRRrrr"); +} + /// Verifies that `ReflectionClass::hasMethod()` and `hasProperty()` report /// PHP-visible members for static class-like metadata. #[test] From 5aa3ab7352d7f5c682ddb5c8bd39507e377ff2ba Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 00:06:03 +0200 Subject: [PATCH 0363/1208] Support ReflectionClass member arrays --- .../elephc-eval/src/interpreter/reflection.rs | 107 +++++- .../src/interpreter/runtime_ops.rs | 2 + .../tests/builtins_class_metadata.rs | 50 +++ .../interpreter/tests/support/object_ops.rs | 12 + .../interpreter/tests/support/runtime_ops.rs | 4 + .../elephc-eval/src/runtime_hooks/externs.rs | 2 + crates/elephc-eval/src/runtime_hooks/ops.rs | 4 + docs/php/classes.md | 4 +- docs/php/eval.md | 4 + src/codegen/eval_reflection_owner_helpers.rs | 72 +++- src/codegen/lower_inst/objects/reflection.rs | 334 ++++++++++++++++-- src/types/checker/builtin_types/reflection.rs | 56 ++- tests/codegen/eval.rs | 49 +++ tests/codegen/oop/attributes.rs | 50 +++ 14 files changed, 705 insertions(+), 45 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index fa8020e0dc..8c9472970e 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -275,26 +275,52 @@ fn eval_reflection_owner_object( values: &mut impl RuntimeValueOps, ) -> Result { let attrs = eval_reflection_attribute_array_result(attributes, context, values)?; - let interface_names = eval_reflection_string_array_result(interface_names, values)?; - let trait_names = eval_reflection_string_array_result(trait_names, values)?; - let method_names = eval_reflection_string_array_result(method_names, values)?; - let property_names = eval_reflection_string_array_result(property_names, values)?; + let interface_names_array = eval_reflection_string_array_result(interface_names, values)?; + let trait_names_array = eval_reflection_string_array_result(trait_names, values)?; + let method_names_array = eval_reflection_string_array_result(method_names, values)?; + let property_names_array = eval_reflection_string_array_result(property_names, values)?; + let method_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + eval_reflection_member_object_array_result( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + &method_names, + context, + values, + )? + } else { + values.array_new(0)? + }; + let property_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + eval_reflection_member_object_array_result( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + &property_names, + context, + values, + )? + } else { + values.array_new(0)? + }; let object = values.reflection_owner_new( owner_kind, reflected_name, attrs, - interface_names, - trait_names, - method_names, - property_names, + interface_names_array, + trait_names_array, + method_names_array, + property_names_array, + method_objects, + property_objects, flags, modifiers, )?; values.release(attrs)?; - values.release(interface_names)?; - values.release(trait_names)?; - values.release(method_names)?; - values.release(property_names)?; + values.release(interface_names_array)?; + values.release(trait_names_array)?; + values.release(method_names_array)?; + values.release(property_names_array)?; + values.release(method_objects)?; + values.release(property_objects)?; Ok(object) } @@ -310,6 +336,63 @@ fn eval_reflection_string_array_result( Ok(result) } +/// Builds an indexed array of ReflectionMethod or ReflectionProperty objects for a ReflectionClass. +fn eval_reflection_member_object_array_result( + owner_kind: u64, + class_name: &str, + names: &[String], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + let mut index = 0; + for name in names { + let Some(member) = eval_reflection_member_metadata(owner_kind, class_name, name, context) + else { + continue; + }; + let flags = eval_reflection_member_flags( + member.visibility, + member.is_static, + member.is_final, + member.is_abstract, + ); + let member_object = eval_reflection_owner_object( + owner_kind, + name, + &member.attributes, + &[], + &[], + &[], + &[], + flags, + 0, + context, + values, + )?; + let key = values.int(index)?; + result = values.array_set(result, key, member_object)?; + index += 1; + } + Ok(result) +} + +/// Returns member metadata for one ReflectionClass member-array entry. +fn eval_reflection_member_metadata( + owner_kind: u64, + class_name: &str, + name: &str, + context: &ElephcEvalContext, +) -> Option { + match owner_kind { + EVAL_REFLECTION_OWNER_METHOD => eval_reflection_method_metadata(class_name, name, context), + EVAL_REFLECTION_OWNER_PROPERTY => { + eval_reflection_property_metadata(class_name, name, context) + } + _ => None, + } +} + /// Returns the eval-retained class-like attributes plus canonical reflected name. fn eval_reflection_class_like_attributes( name: &str, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 979f5855a4..47a67feda4 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -122,6 +122,8 @@ pub trait RuntimeValueOps { trait_names: RuntimeCellHandle, method_names: RuntimeCellHandle, property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 98414b75de..6a0c1d4401 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -488,6 +488,56 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass getMethods/getProperties return eval member objects. +#[test] +fn execute_program_reflection_class_lists_eval_member_objects() { + let program = parse_fragment( + br#"#[Attribute] +class EvalListMarker {} +class EvalReflectListTarget { + #[EvalListMarker] + public function first() {} + private static function helper() {} + #[EvalListMarker] + protected $visible; + private static $token; +} +$ref = new ReflectionClass("EvalReflectListTarget"); +$methods = $ref->getMethods(); +$properties = $ref->getProperties(); +echo count($methods); echo ":"; echo count($properties); echo ":"; +foreach ($methods as $method) { + if ($method->getName() === "first") { + echo "F"; echo count($method->getAttributes()); + } + if ($method->getName() === "helper") { + echo $method->isStatic() ? "S" : "s"; + echo $method->isPrivate() ? "R" : "r"; + } +} +echo ":"; +foreach ($properties as $property) { + if ($property->getName() === "visible") { + echo "V"; echo count($property->getAttributes()); + echo $property->isProtected() ? "P" : "p"; + } + if ($property->getName() === "token") { + echo $property->isStatic() ? "T" : "t"; + echo $property->isPrivate() ? "R" : "r"; + } +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:2:F1SR:V1PTR"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. #[test] fn execute_program_reflects_eval_constant_and_enum_case_attributes() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 06cd3aa91f..433d232ff1 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -178,6 +178,14 @@ impl FakeOps { Self::object_property(&properties, "__trait_names") .map_or_else(|| self.runtime_array_new(0), Ok) } + (FakeValue::Object(properties), "getmethods") if args.is_empty() => { + Self::object_property(&properties, "__methods") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getproperties") if args.is_empty() => { + Self::object_property(&properties, "__properties") + .map_or_else(|| self.runtime_array_new(0), Ok) + } (FakeValue::Object(properties), "getarguments") if args.is_empty() => { Self::object_property(&properties, "__args") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -297,6 +305,8 @@ impl FakeOps { trait_names: RuntimeCellHandle, method_names: RuntimeCellHandle, property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -338,6 +348,8 @@ impl FakeOps { properties.push(("__trait_names".to_string(), trait_names)); properties.push(("__method_names".to_string(), method_names)); properties.push(("__property_names".to_string(), property_names)); + properties.push(("__methods".to_string(), method_objects)); + properties.push(("__properties".to_string(), property_objects)); } if owner_kind == EVAL_REFLECTION_OWNER_METHOD || owner_kind == EVAL_REFLECTION_OWNER_PROPERTY diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index c6d20856fa..0e05ca2193 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -131,6 +131,8 @@ impl RuntimeValueOps for FakeOps { trait_names: RuntimeCellHandle, method_names: RuntimeCellHandle, property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -142,6 +144,8 @@ impl RuntimeValueOps for FakeOps { trait_names, method_names, property_names, + method_objects, + property_objects, flags, modifiers, ) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 647aadf174..5c49e1a70c 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -83,6 +83,8 @@ unsafe extern "C" { trait_names: *mut RuntimeCell, method_names: *mut RuntimeCell, property_names: *mut RuntimeCell, + method_objects: *mut RuntimeCell, + property_objects: *mut RuntimeCell, flags: u64, modifiers: u64, ) -> *mut RuntimeCell; diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 91e91e8d87..b31fcdaeb3 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -208,6 +208,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { trait_names: RuntimeCellHandle, method_names: RuntimeCellHandle, property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -221,6 +223,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { trait_names.as_ptr(), method_names.as_ptr(), property_names.as_ptr(), + method_objects.as_ptr(), + property_objects.as_ptr(), flags, modifiers, ) diff --git a/docs/php/classes.md b/docs/php/classes.md index f94170b35d..72964a585e 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1095,6 +1095,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::hasProperty()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named property; property lookup is case-sensitive | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | +| `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | +| `ReflectionClass::getProperties()` | `new ReflectionClass($class_name)` | Return `ReflectionProperty` objects for properties visible through the reflected class-like metadata | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | @@ -1168,7 +1170,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, and `getTraitNames()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as `getProperties()`, `getMethods()`, and object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, and `getProperties()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as object construction through `ReflectionClass::newInstance()` are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 542911c550..bbaf8b5a95 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -168,6 +168,10 @@ classes and parent interfaces for eval interfaces, while `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. +`ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return +materialized `ReflectionMethod` and `ReflectionProperty` objects for the same +visible member metadata, including supported member attributes and predicate +flags. `ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()` report eval method metadata. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index fea8778690..cac81de879 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -38,6 +38,10 @@ struct ReflectionOwnerLayout { method_names_hi: Option, property_names_lo: Option, property_names_hi: Option, + method_objects_lo: Option, + method_objects_hi: Option, + property_objects_lo: Option, + property_objects_hi: Option, attrs_lo: usize, attrs_hi: usize, is_final_lo: Option, @@ -155,6 +159,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, method_names: Vec, property_names: Vec, + method_members: Vec, + property_members: Vec, is_final: bool, is_abstract: bool, is_interface: bool, @@ -40,6 +42,14 @@ struct ReflectionOwnerMetadata { member_flags: ReflectionMemberFlags, } +/// Metadata for one member object returned by `ReflectionClass::getMethods()` or `getProperties()`. +struct ReflectionListedMember { + name: String, + attr_names: Vec, + attr_args: Vec>>, + flags: ReflectionMemberFlags, +} + /// Boolean metadata exposed by ReflectionMethod and ReflectionProperty predicates. #[derive(Clone, Copy, Default)] struct ReflectionMemberFlags { @@ -115,6 +125,18 @@ pub(super) fn lower_reflection_owner_new( "__property_names", &metadata.property_names, )?; + emit_reflection_member_array_property_by_name( + ctx, + "__methods", + "ReflectionMethod", + &metadata.method_members, + )?; + emit_reflection_member_array_property_by_name( + ctx, + "__properties", + "ReflectionProperty", + &metadata.property_members, + )?; } } emit_reflection_attrs_property( @@ -555,14 +577,21 @@ fn reflection_class_metadata( let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionClass")?; if let Some((class_name, info)) = resolve_reflection_class(ctx, &reflected_class) { let is_enum = is_reflection_enum(ctx, class_name); + let method_names = reflection_class_method_names(ctx, class_name); + let property_names = reflection_class_property_names(ctx, class_name, info); + let method_members = reflection_class_method_members(info, &method_names); + let property_members = + reflection_class_property_members(ctx, class_name, info, &property_names); return Ok(ReflectionOwnerMetadata { reflected_name: Some(class_name.to_string()), attr_names: info.attribute_names.clone(), attr_args: info.attribute_args.clone(), interface_names: info.interfaces.clone(), trait_names: info.used_traits.clone(), - method_names: reflection_class_method_names(ctx, class_name), - property_names: reflection_class_property_names(ctx, class_name, info), + method_names, + property_names, + method_members, + property_members, is_final: info.is_final, is_abstract: info.is_abstract, is_interface: false, @@ -635,6 +664,8 @@ fn reflection_method_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -671,6 +702,8 @@ fn reflection_property_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -708,6 +741,8 @@ fn reflection_class_constant_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -739,6 +774,8 @@ fn reflection_class_constant_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -777,6 +814,8 @@ fn reflection_enum_case_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -999,6 +1038,123 @@ fn reflection_property_member_flags( None } +/// Builds ReflectionMethod array entries for the methods visible on one class. +fn reflection_class_method_members( + info: &crate::types::ClassInfo, + method_names: &[String], +) -> Vec { + method_names + .iter() + .filter_map(|method_name| reflection_class_method_member(info, method_name)) + .collect() +} + +/// Builds one ReflectionMethod array entry from class metadata. +fn reflection_class_method_member( + info: &crate::types::ClassInfo, + method_name: &str, +) -> Option { + let method_key = php_symbol_key(method_name); + Some(ReflectionListedMember { + name: method_key.clone(), + attr_names: info + .method_attribute_names + .get(&method_key) + .cloned() + .unwrap_or_default(), + attr_args: info + .method_attribute_args + .get(&method_key) + .cloned() + .unwrap_or_default(), + flags: reflection_method_member_flags(info, &method_key)?, + }) +} + +/// Builds ReflectionProperty array entries for the properties visible on one class. +fn reflection_class_property_members( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, + property_names: &[String], +) -> Vec { + property_names + .iter() + .filter_map(|property_name| reflection_class_property_member(ctx, class_name, info, property_name)) + .collect() +} + +/// Builds one ReflectionProperty array entry from class or enum metadata. +fn reflection_class_property_member( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + let flags = reflection_property_member_flags(info, property_name).or_else(|| { + (is_reflection_enum(ctx, class_name) && property_name == "name").then_some( + reflection_member_flags(false, &Visibility::Public, false, false), + ) + })?; + Some(ReflectionListedMember { + name: property_name.to_string(), + attr_names: info + .property_attribute_names + .get(property_name) + .cloned() + .unwrap_or_default(), + attr_args: info + .property_attribute_args + .get(property_name) + .cloned() + .unwrap_or_default(), + flags, + }) +} + +/// Builds placeholder ReflectionMethod entries for class-like metadata without full method schemas. +fn default_method_members( + method_names: &[String], + is_interface: bool, + _is_trait: bool, +) -> Vec { + method_names + .iter() + .map(|name| ReflectionListedMember { + name: name.clone(), + attr_names: Vec::new(), + attr_args: Vec::new(), + flags: reflection_member_flags( + false, + &Visibility::Public, + false, + is_interface, + ), + }) + .collect() +} + +/// Builds placeholder ReflectionProperty entries for class-like metadata without full property schemas. +fn default_property_members( + property_names: &[String], + is_interface: bool, +) -> Vec { + property_names + .iter() + .map(|name| ReflectionListedMember { + name: name.clone(), + attr_names: Vec::new(), + attr_args: Vec::new(), + flags: reflection_member_flags( + false, + &Visibility::Public, + false, + is_interface, + ), + }) + .collect() +} + /// Builds common ReflectionMethod/ReflectionProperty predicate flags. fn reflection_member_flags( is_static: bool, @@ -1106,6 +1262,8 @@ fn class_like_reflection_metadata( is_trait: bool, is_enum: bool, ) -> ReflectionOwnerMetadata { + let method_members = default_method_members(method_names.as_slice(), is_interface, is_trait); + let property_members = default_property_members(property_names.as_slice(), is_interface); ReflectionOwnerMetadata { reflected_name: Some(class_like_name.to_string()), attr_names: Vec::new(), @@ -1114,6 +1272,8 @@ fn class_like_reflection_metadata( trait_names, method_names, property_names, + method_members, + property_members, is_final: false, is_abstract: false, is_interface, @@ -1163,6 +1323,8 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -1297,7 +1459,7 @@ fn emit_reflection_attrs_property( attr_names: &[String], attr_args: &[Option>], ) -> Result<()> { - let (attrs_low_offset, attrs_high_offset) = reflection_attrs_offsets(class_name); + let (attrs_low_offset, attrs_high_offset) = reflection_attrs_offsets(ctx, class_name)?; let result_reg = abi::int_result_reg(ctx.emitter); let object_reg = abi::symbol_scratch_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); @@ -1358,6 +1520,143 @@ fn emit_reflection_string_array_property_by_name( Ok(()) } +/// Replaces a ReflectionClass private array slot with ReflectionMethod/Property objects. +fn emit_reflection_member_array_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + member_class_name: &str, + members: &[ReflectionListedMember], +) -> Result<()> { + if members.is_empty() { + return Ok(()); + } + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_member_array(ctx, member_class_name, members)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Allocates an indexed array of populated ReflectionMethod/ReflectionProperty objects. +fn emit_reflection_member_array( + ctx: &mut FunctionContext<'_>, + member_class_name: &str, + members: &[ReflectionListedMember], +) -> Result<()> { + emit_reflection_indexed_array(ctx, members.len().max(1), 8); + crate::codegen::emit_array_value_type_stamp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &PhpType::Object(member_class_name.to_string()), + ); + + for member in members { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_member_object(ctx, member_class_name, member)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_append_reflection_member_object(ctx); + } + + Ok(()) +} + +/// Allocates and populates one ReflectionMethod/ReflectionProperty object. +fn emit_reflection_member_object( + ctx: &mut FunctionContext<'_>, + member_class_name: &str, + member: &ReflectionListedMember, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = + ctx.module.class_infos.get(member_class_name).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", member_class_name)) + })?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + )?; + let class_info = ctx + .module + .class_infos + .get(member_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let name_offset = reflection_property_offset(class_info, "__name")?; + emit_reflection_string_property(ctx, &member.name, name_offset, name_offset + 8); + emit_reflection_attrs_property( + ctx, + member_class_name, + &member.attr_names, + &member.attr_args, + )?; + emit_reflection_member_flag_properties(ctx, member_class_name, member.flags)?; + Ok(()) +} + +/// Allocates an indexed array for static reflection metadata. +fn emit_reflection_indexed_array( + ctx: &mut FunctionContext<'_>, + capacity: usize, + stride: i64, +) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_int_immediate(ctx.emitter, "x0", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "x1", stride); + } + Arch::X86_64 => { + abi::emit_load_int_immediate(ctx.emitter, "rdi", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "rsi", stride); + } + } + abi::emit_call_label(ctx.emitter, "__rt_array_new"); +} + +/// Appends the stacked member object to the stacked member array and leaves the array in result. +fn emit_append_reflection_member_object(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_pop_reg(ctx.emitter, "x1"); + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); + } + Arch::X86_64 => { + abi::emit_pop_reg(ctx.emitter, "rsi"); + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); + } + } +} + /// Allocates an indexed string array containing ReflectionClass metadata names. fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) -> Result<()> { match ctx.emitter.target.arch { @@ -1527,23 +1826,12 @@ fn reflection_property_offset(info: &crate::types::ClassInfo, property: &str) -> } /// Returns the low/high object offsets for the private `__attrs` slot. -fn reflection_attrs_offsets(class_name: &str) -> (usize, usize) { - if reflection_owner_has_name(class_name) { - (24, 32) - } else { - (8, 16) - } -} - -/// Returns true when the synthetic Reflection owner stores a private `__name` slot. -fn reflection_owner_has_name(class_name: &str) -> bool { - matches!( - class_name, - "ReflectionClass" - | "ReflectionMethod" - | "ReflectionProperty" - | "ReflectionClassConstant" - | "ReflectionEnumUnitCase" - | "ReflectionEnumBackedCase" - ) +fn reflection_attrs_offsets(ctx: &FunctionContext<'_>, class_name: &str) -> Result<(usize, usize)> { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let attrs_low_offset = reflection_property_offset(class_info, "__attrs")?; + Ok((attrs_low_offset, attrs_low_offset + 8)) } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 737e2792d1..3fff21e052 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -233,6 +233,11 @@ fn string_array_type() -> TypeExpr { TypeExpr::Array(Box::new(TypeExpr::Str)) } +/// Returns a `TypeExpr` for an indexed array of objects with the given class name. +fn object_array_type(class_name: &str) -> TypeExpr { + TypeExpr::Array(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) +} + /// Returns a `TypeExpr` for the unqualified name `mixed`. fn mixed_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("mixed")) @@ -639,6 +644,18 @@ fn builtin_reflection_class() -> FlattenedClass { Some(string_array_type()), empty_array(), ), + builtin_property( + "__methods", + Visibility::Private, + Some(object_array_type("ReflectionMethod")), + empty_array(), + ), + builtin_property( + "__properties", + Visibility::Private, + Some(object_array_type("ReflectionProperty")), + empty_array(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![( @@ -651,8 +668,16 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_string_method("getShortName", "__short_name"), builtin_reflection_class_string_method("getNamespaceName", "__namespace_name"), builtin_reflection_class_bool_method("inNamespace", "__in_namespace"), - builtin_reflection_class_array_method("getInterfaceNames", "__interface_names"), - builtin_reflection_class_array_method("getTraitNames", "__trait_names"), + builtin_reflection_class_array_method( + "getInterfaceNames", + "__interface_names", + string_array_type(), + ), + builtin_reflection_class_array_method( + "getTraitNames", + "__trait_names", + string_array_type(), + ), builtin_reflection_class_bool_method("isFinal", "__is_final"), builtin_reflection_class_bool_method("isAbstract", "__is_abstract"), builtin_reflection_class_bool_method("isInterface", "__is_interface"), @@ -662,6 +687,16 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), + builtin_reflection_class_array_method( + "getMethods", + "__methods", + object_array_type("ReflectionMethod"), + ), + builtin_reflection_class_array_method( + "getProperties", + "__properties", + object_array_type("ReflectionProperty"), + ), builtin_reflection_owner_get_attributes_method(), ], attributes: Vec::new(), @@ -802,7 +837,11 @@ fn builtin_reflection_class_has_name_method( } /// Returns a public `ReflectionClass` array method backed by one private slot. -fn builtin_reflection_class_array_method(method_name: &str, property: &str) -> ClassMethod { +fn builtin_reflection_class_array_method( + method_name: &str, + property: &str, + return_type: TypeExpr, +) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); ClassMethod { name: method_name.to_string(), @@ -814,7 +853,7 @@ fn builtin_reflection_class_array_method(method_name: &str, property: &str) -> C params: Vec::new(), variadic: None, variadic_type: None, - return_type: Some(string_array_type()), + return_type: Some(return_type), body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { @@ -1069,6 +1108,15 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Array(Box::new(PhpType::Str)); } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getMethods")) { + sig.return_type = + PhpType::Array(Box::new(PhpType::Object("ReflectionMethod".to_string()))); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getProperties")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionProperty".to_string(), + ))); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 628ccc7b13..f6ca825839 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5819,6 +5819,55 @@ echo $visibleProp->isPublic() ? "U" : "u";'); assert_eq!(out.stdout, "SPurfa:APs:FUs:SRp:sPu"); } +/// Verifies eval ReflectionClass getMethods/getProperties return member objects through the bridge. +#[test] +fn test_eval_reflection_class_lists_member_objects() { + let out = compile_and_run_capture( + r#"getMethods(); +$properties = $ref->getProperties(); +echo count($methods) . ":" . count($properties) . ":"; +foreach ($methods as $method) { + if ($method->getName() === "first") { + echo "F" . count($method->getAttributes()); + } + if ($method->getName() === "helper") { + echo $method->isStatic() ? "S" : "s"; + echo $method->isPrivate() ? "R" : "r"; + } +} +echo ":"; +foreach ($properties as $property) { + if ($property->getName() === "visible") { + echo "V" . count($property->getAttributes()); + echo $property->isProtected() ? "P" : "p"; + } + if ($property->getName() === "token") { + echo $property->isStatic() ? "T" : "t"; + echo $property->isPrivate() ? "R" : "r"; + } +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); +} + /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. #[test] fn test_eval_reflection_constant_and_enum_case_attributes() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 981c8706c2..5314e3d2fd 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1536,6 +1536,56 @@ echo $visibleProp->isPublic() ? "U" : "u"; assert_eq!(out, "SPurfa:APs:FUs:SRp:sPu"); } +/// Verifies that `ReflectionClass::getMethods()` and `getProperties()` return +/// populated ReflectionMethod/ReflectionProperty objects with member metadata. +#[test] +fn test_reflection_class_get_methods_and_properties_return_member_objects() { + let out = compile_and_run_capture( + r#"getMethods(); +$properties = $ref->getProperties(); +echo count($methods) . ":" . count($properties) . ":"; +foreach ($methods as $method) { + if ($method->getName() === "first") { + echo "F" . count($method->getAttributes()); + } + if ($method->getName() === "helper") { + echo $method->isStatic() ? "S" : "s"; + echo $method->isPrivate() ? "R" : "r"; + } +} +echo ":"; +foreach ($properties as $property) { + if ($property->getName() === "visible") { + echo "V" . count($property->getAttributes()); + echo $property->isProtected() ? "P" : "p"; + } + if ($property->getName() === "token") { + echo $property->isStatic() ? "T" : "t"; + echo $property->isPrivate() ? "R" : "r"; + } +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); +} + /// Verifies that `ReflectionClassConstant` and enum-case reflectors expose /// attribute name, arguments, `getName()`, and `newInstance()` data. #[test] From ffd9972c1e8455ed481e9bc91187a6b9663b352f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 00:26:29 +0200 Subject: [PATCH 0364/1208] Support ReflectionClass newInstance --- crates/elephc-eval/src/context.rs | 16 +++++ .../elephc-eval/src/interpreter/reflection.rs | 4 ++ .../elephc-eval/src/interpreter/statements.rs | 43 ++++++++++++ .../tests/builtins_class_metadata.rs | 30 +++++++++ docs/php/classes.md | 3 +- docs/php/eval.md | 4 +- src/ir_lower/expr/mod.rs | 67 +++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 57 +++++++++++++++- tests/codegen/eval.rs | 24 +++++++ tests/codegen/oop/attributes.rs | 25 +++++++ 10 files changed, 269 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index deb952cdf7..dabfc3f0c2 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -153,6 +153,7 @@ pub struct ElephcEvalContext { included_files: HashSet, dynamic_objects: HashMap, eval_reflection_attributes: HashMap, + eval_reflection_classes: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, class_stack: Vec, @@ -196,6 +197,7 @@ impl ElephcEvalContext { included_files: HashSet::new(), dynamic_objects: HashMap::new(), eval_reflection_attributes: HashMap::new(), + eval_reflection_classes: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -240,6 +242,7 @@ impl ElephcEvalContext { included_files: HashSet::new(), dynamic_objects: HashMap::new(), eval_reflection_attributes: HashMap::new(), + eval_reflection_classes: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -533,6 +536,19 @@ impl ElephcEvalContext { self.eval_reflection_attributes.get(&identity) } + /// Records eval-declared class metadata for one synthetic ReflectionClass object. + pub fn register_eval_reflection_class(&mut self, identity: u64, class_name: &str) { + self.eval_reflection_classes + .insert(identity, normalize_class_name(class_name)); + } + + /// Returns the reflected eval class name attached to a synthetic ReflectionClass. + pub fn eval_reflection_class_name(&self, identity: u64) -> Option<&str> { + self.eval_reflection_classes + .get(&identity) + .map(String::as_str) + } + /// Returns eval-declared class metadata from parent to child for construction. pub fn class_chain(&self, name: &str) -> Vec { let mut chain = Vec::new(); diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 8c9472970e..3465bd0d78 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -314,6 +314,10 @@ fn eval_reflection_owner_object( flags, modifiers, )?; + if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + let identity = values.object_identity(object)?; + context.register_eval_reflection_class(identity, reflected_name); + } values.release(attrs)?; values.release(interface_names_array)?; values.release(trait_names_array)?; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 41d73ea567..bcdc65623d 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1989,6 +1989,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( return eval_reflection_attribute_new_instance_result(&attribute, context, values); } } + if let Some(instance) = eval_reflection_class_new_instance_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(instance); + } let Some(class) = context.dynamic_object_class(identity) else { let class_name = runtime_object_class_name(object, values)?; let evaluated_args = bind_native_callable_args( @@ -2027,6 +2036,40 @@ fn runtime_object_class_name( String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) } +/// Instantiates the class named by a materialized eval `ReflectionClass` object. +fn eval_reflection_class_new_instance_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("newInstance") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(class) = context.class(&reflected_name).cloned() { + let mut scope = ElephcEvalScope::new(); + return eval_dynamic_class_new_object(&class, evaluated_args, context, &mut scope, values) + .map(Some); + } + let class_name = context + .resolve_class_name(&reflected_name) + .unwrap_or(reflected_name); + let args = bind_native_callable_args( + context.native_constructor_signature(&class_name), + evaluated_args, + )?; + let instance = values.new_object(&class_name)?; + values.construct_object(instance, args)?; + Ok(Some(instance)) +} + /// Instantiates an eval-declared attribute class for `ReflectionAttribute::newInstance()`. fn eval_reflection_attribute_new_instance_result( attribute: &EvalAttribute, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 6a0c1d4401..71da697f62 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -538,6 +538,36 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::newInstance constructs eval-declared classes. +#[test] +fn execute_program_reflection_class_new_instance_constructs_eval_class() { + let program = parse_fragment( + br#"class EvalReflectNewTarget { + public $label; + public function __construct($left, $right) { + $this->label = $left . $right; + } + public function label() { + return $this->label; + } +} +$ref = new ReflectionClass("EvalReflectNewTarget"); +$first = $ref->newInstance("I", "J"); +echo $first->label(); echo ":"; +$second = $ref->newInstance(...["K", "L"]); +echo $second->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "IJ:KL"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. #[test] fn execute_program_reflects_eval_constant_and_enum_case_attributes() { diff --git a/docs/php/classes.md b/docs/php/classes.md index 72964a585e..41a6f7fc60 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1097,6 +1097,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | | `ReflectionClass::getProperties()` | `new ReflectionClass($class_name)` | Return `ReflectionProperty` objects for properties visible through the reflected class-like metadata | +| `ReflectionClass::newInstance()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class with forwarded constructor arguments | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | @@ -1170,7 +1171,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, and `getProperties()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as object construction through `ReflectionClass::newInstance()` are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index bbaf8b5a95..c943b8438c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -171,7 +171,9 @@ method lookup is case-insensitive, while property lookup is case-sensitive. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate -flags. +flags. `ReflectionClass::newInstance()` constructs eval-declared reflected +classes and forwards constructor arguments through eval's positional, named, and +unpacking-aware call binding. `ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()` report eval method metadata. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index e1fbab332d..209547a0df 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9345,6 +9345,9 @@ fn lower_method_call( if op == Op::MethodCall && value_is_nullable(ctx, object.value) { return lower_nullable_regular_method_call(ctx, object, method, args, expr); } + if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { + return lower_reflection_class_new_instance(ctx, object, args, expr); + } if matches!( ctx.builder.value_php_type(object.value).codegen_repr(), PhpType::Callable @@ -9537,6 +9540,67 @@ fn lower_nullable_regular_method_call( take_owned_temp(ctx, &temp_name, expr.span) } +/// Lowers `ReflectionClass::newInstance()` by constructing the reflected class name. +fn lower_reflection_class_new_instance( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) || crate::types::call_args::has_named_args(&args) { + return lower_reflection_class_new_instance_unsupported(ctx, expr); + } + let class_name = lower_property_get_from_value(ctx, object, "__name", Op::PropGet, expr); + let mut operands = vec![class_name.value]; + operands.extend(lower_args(ctx, &args)); + ctx.emit_value( + Op::DynamicObjectNewMixed, + operands, + None, + PhpType::Mixed, + Op::DynamicObjectNewMixed.default_effects(), + Some(expr.span), + ) +} + +/// Returns the source arguments that can be forwarded to `new $class(...)`. +fn reflection_class_new_instance_args(args: &[Expr]) -> Vec { + if has_static_call_spread_args(args) { + return expand_static_call_spread_args(args); + } + args.to_vec() +} + +/// Emits a runtime fatal for ReflectionClass newInstance argument forms not yet lowered. +fn lower_reflection_class_new_instance_unsupported( + ctx: &mut LoweringContext<'_, '_>, + expr: &Expr, +) -> LoweredValue { + let result = lower_boxed_null(ctx, expr); + let message = ctx.intern_string( + "Fatal error: unsupported ReflectionClass::newInstance() argument forwarding\n", + ); + ctx.builder.terminate(Terminator::Fatal { message }); + result +} + +/// Returns true when a method call targets the built-in `ReflectionClass::newInstance()`. +fn is_reflection_class_new_instance_call( + ctx: &LoweringContext<'_, '_>, + object: ValueId, + method: &str, +) -> bool { + if php_symbol_key(method) != "newinstance" { + return false; + } + let object_ty = ctx.builder.value_php_type(object); + let Some((class_name, false)) = singular_object_class(&object_ty) else { + return false; + }; + php_symbol_key(class_name.trim_start_matches('\\')) == "reflectionclass" +} + /// Emits the PHP fatal terminator for an ordinary method call on null. fn terminate_method_call_on_null(ctx: &mut LoweringContext<'_, '_>, method: &str) { let message = format!("Fatal error: Call to a member function {}() on null\n", method); @@ -9636,6 +9700,9 @@ fn lower_method_call_with_receiver( op: Op, expr: &Expr, ) -> LoweredValue { + if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { + return lower_reflection_class_new_instance(ctx, object, args, expr); + } let magic_args; let (dispatch_method, args) = if let Some(args) = magic_call_dispatch_args(ctx, object.value, method, args, expr.span) { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 3fff21e052..54ef0b6dc8 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -7,8 +7,8 @@ //! - `crate::types::checker::driver::init` (alongside `inject_builtin_throwables`). //! //! Key details: -//! - Property and method bodies are dummies or simple private-slot accessors; -//! runtime population is handled by codegen-only reflection constructors. +//! - Property and method bodies are dummies, private-slot accessors, or small +//! fallbacks; runtime population is handled by codegen-only reflection constructors. use std::collections::{HashMap, HashSet}; @@ -356,6 +356,47 @@ fn builtin_reflection_attribute_new_instance_method() -> ClassMethod { } } +/// Returns a public variadic `ReflectionClass::newInstance()` method. +/// +/// Direct calls are lowered specially so their source arguments become +/// constructor arguments for the reflected class. The no-argument body keeps +/// indirect calls and metadata emission coherent when no argument forwarding is +/// required. +fn builtin_reflection_class_new_instance_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "newInstance".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + variadic: Some("args".to_string()), + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::NewDynamic { + name_expr: Box::new(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: "__name".to_string(), + }, + dummy_span, + )), + args: Vec::new(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public no-op method that returns the private `property` slot typed /// `return_type`. Reflection getters are populated at codegen; their bodies just /// surface the corresponding private slot. @@ -697,6 +738,7 @@ fn builtin_reflection_class() -> FlattenedClass { "__properties", object_array_type("ReflectionProperty"), ), + builtin_reflection_class_new_instance_method(), builtin_reflection_owner_get_attributes_method(), ], attributes: Vec::new(), @@ -1120,6 +1162,17 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("newInstance")) { + sig.return_type = PhpType::Mixed; + sig.variadic = Some("args".to_string()); + if !sig.params.iter().any(|(name, _)| name == "args") { + sig.params + .push(("args".to_string(), PhpType::Array(Box::new(PhpType::Mixed)))); + sig.defaults.push(None); + sig.ref_params.push(false); + sig.declared_params.push(false); + } + } } if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { for method_name in ["isstatic", "ispublic", "isprotected", "isprivate"] { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f6ca825839..670187636c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5868,6 +5868,30 @@ foreach ($properties as $property) { assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); } +/// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. +#[test] +fn test_eval_reflection_class_new_instance_constructs_eval_class() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label() { + return $this->label; + } +} +$ref = new ReflectionClass("EvalReflectNewTarget"); +$first = $ref->newInstance("E", "F"); +echo $first->label() . ":"; +$second = $ref->newInstance(...["G", "H"]); +echo $second->label();'); +"#, + ); + assert_eq!(out, "EF:GH"); +} + /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. #[test] fn test_eval_reflection_constant_and_enum_case_attributes() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 5314e3d2fd..99e12050a6 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1586,6 +1586,31 @@ foreach ($properties as $property) { assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); } +/// Verifies that `ReflectionClass::newInstance()` constructs reflected classes +/// and forwards direct and statically-unpacked constructor arguments. +#[test] +fn test_reflection_class_new_instance_constructs_reflected_class() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} +$ref = new ReflectionClass(ReflectNewTarget::class); +$first = $ref->newInstance("A", "B"); +echo $first->label() . ":"; +$second = $ref->newInstance(...["C", "D"]); +echo $second->label(); +"#, + ); + assert_eq!(out, "AB:CD"); +} + /// Verifies that `ReflectionClassConstant` and enum-case reflectors expose /// attribute name, arguments, `getName()`, and `newInstance()` data. #[test] From 50b3e25ad45ee3e45ca0eec84b27cc2b3510a5db Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 00:33:33 +0200 Subject: [PATCH 0365/1208] Support enum case name property --- src/types/checker/schema/enums.rs | 69 ++++++++++++++++++++++--------- tests/codegen/oop/attributes.rs | 3 +- tests/codegen/types/enums.rs | 26 ++++++++++-- 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 2dd01ee06f..177617f664 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -269,26 +269,33 @@ pub(crate) fn insert_enum_metadata( let mut readonly_properties = HashSet::new(); let reference_properties = HashSet::new(); if let Some(backing_ty) = &backing_type { - properties.push(("value".to_string(), backing_ty.clone())); - property_offsets.insert("value".to_string(), 8); - property_declaring_classes.insert("value".to_string(), name.to_string()); - defaults.push(None); - property_visibilities.insert("value".to_string(), Visibility::Public); - declared_properties.insert("value".to_string()); - readonly_properties.insert("value".to_string()); + push_enum_readonly_property( + "value", + backing_ty.clone(), + name, + &mut properties, + &mut property_offsets, + &mut property_declaring_classes, + &mut defaults, + &mut property_visibilities, + &mut declared_properties, + &mut readonly_properties, + ); } - // Every enum case (pure or backed) exposes a readonly public `name` string holding the case - // identifier, mirroring PHP's `UnitEnum::$name`. Append it after any backing `value` so backed - // enums keep `value` at offset 8; the offset matches the singleton property slot layout used by - // EIR codegen (`8 + index * 16`). - let name_offset = 8 + properties.len() * 16; - properties.push(("name".to_string(), PhpType::Str)); - property_offsets.insert("name".to_string(), name_offset); - property_declaring_classes.insert("name".to_string(), name.to_string()); - defaults.push(None); - property_visibilities.insert("name".to_string(), Visibility::Public); - declared_properties.insert("name".to_string()); - readonly_properties.insert("name".to_string()); + // Append `name` after any backing `value` so backed enums keep `value` at + // offset 8, matching singleton initialization in codegen. + push_enum_readonly_property( + "name", + PhpType::Str, + name, + &mut properties, + &mut property_offsets, + &mut property_declaring_classes, + &mut defaults, + &mut property_visibilities, + &mut declared_properties, + &mut readonly_properties, + ); let mut static_methods = HashMap::new(); let mut static_method_visibilities = HashMap::new(); @@ -469,3 +476,27 @@ pub(crate) fn insert_enum_metadata( *next_class_id += 1; Ok(()) } + +/// Appends one synthetic public readonly enum case property to class metadata. +fn push_enum_readonly_property( + property: &str, + php_type: PhpType, + enum_name: &str, + properties: &mut Vec<(String, PhpType)>, + property_offsets: &mut HashMap, + property_declaring_classes: &mut HashMap, + defaults: &mut Vec>, + property_visibilities: &mut HashMap, + declared_properties: &mut HashSet, + readonly_properties: &mut HashSet, +) { + let offset = 8 + properties.len() * 16; + let property = property.to_string(); + properties.push((property.clone(), php_type)); + property_offsets.insert(property.clone(), offset); + property_declaring_classes.insert(property.clone(), enum_name.to_string()); + defaults.push(None); + property_visibilities.insert(property.clone(), Visibility::Public); + declared_properties.insert(property.clone()); + readonly_properties.insert(property); +} diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 99e12050a6..b8039823d5 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1268,10 +1268,11 @@ echo $pure->hasProperty("value") ? "V" : "v"; echo ":"; $backed = new ReflectionClass(StaticMemberBackedEnum::class); echo $backed->hasMethod("tryfrom") ? "B" : "b"; +echo $backed->hasProperty("name") ? "N" : "n"; echo $backed->hasProperty("value") ? "Y" : "y"; "#, ); - assert_eq!(out, "MPSx:ChTw:IJK:RU:ELNv:BY"); + assert_eq!(out, "MPSx:ChTw:IJK:RU:ELNv:BNY"); } /// Verifies that `ReflectionClass` reports implemented interface and used trait names. diff --git a/tests/codegen/types/enums.rs b/tests/codegen/types/enums.rs index 0a5b9ff6bf..8f59a0d2cb 100644 --- a/tests/codegen/types/enums.rs +++ b/tests/codegen/types/enums.rs @@ -110,6 +110,26 @@ fn test_pure_enum_cases_identity() { assert_eq!(out, "2\n1"); } +/// Verifies enum case objects expose PHP's readonly `name` property directly and inside methods. +#[test] +fn test_enum_case_name_property_and_method() { + let out = compile_and_run( + "name; + } + } + echo Suit::Hearts->name; + echo '|'; + echo Suit::Clubs->label(); + ", + ); + assert_eq!(out, "Hearts|Clubs"); +} + /// Verifies that `Color::from(99)` throws a catchable `ValueError` with PHP's /// invalid backing-value message. #[test] @@ -391,12 +411,12 @@ fn test_enum_method_reads_backing_value() { enum Power: int { case Low = 1; case High = 10; - public function doubled(): int { return $this->value * 2; } + public function label(): string { return $this->name . ':' . ($this->value * 2); } } - echo Power::High->doubled(); + echo Power::High->label(); ", ); - assert_eq!(out, "20"); + assert_eq!(out, "High:20"); } /// Verifies that a static enum method (a factory) dispatches and returns a case. From 261f8105d46ad90035572f5387d1919c4de038a4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 00:47:55 +0200 Subject: [PATCH 0366/1208] Support trait use in enums --- docs/php/classes.md | 5 +- src/conditional/stmts.rs | 2 + src/ir_lower/program.rs | 5 +- src/ir_lower/tests/exhaustive.rs | 2 +- src/magic_constants/walker/stmts.rs | 2 + src/name_resolver/declarations.rs | 6 + src/optimize/control/dce.rs | 2 + src/optimize/control/fold.rs | 2 + src/optimize/control/prune/statements.rs | 2 + src/optimize/propagate/stmt.rs | 2 + src/parser/ast/stmt.rs | 2 + src/parser/stmt/oop/declarations.rs | 7 +- src/resolver/stmt_exprs.rs | 2 + src/types/checker/driver/mod.rs | 53 +++-- src/types/traits.rs | 249 +++++++++++++------- tests/codegen/types/enums.rs | 53 +++++ tests/error_tests/exceptions_enums_magic.rs | 8 +- tests/parser_tests/classes/traits.rs | 37 +++ 18 files changed, 331 insertions(+), 110 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 41a6f7fc60..9efe93f516 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -763,12 +763,13 @@ Rules: - Instance methods may use `$this` (the case), `match ($this)`, the case `$this->name`, the backing `$this->value`, and `self::CONST`. - Static methods dispatch like class static methods and can act as factories. - An enum can `implements` one or more interfaces and be used through them. +- Enums can `use` traits that provide methods, including `insteadof` and `as` + adaptations. Traits with properties are rejected because PHP enums cannot have + properties. - `self`/`static` type hints in enum methods resolve to the enum. Enum constants are readable both inside the enum (`self::CONST`) and from outside it (`EnumName::CONST`). Enum method bodies are type-checked like class method bodies, so a mismatched return type or an undefined variable inside an enum method is reported. -Current limitations: using a trait inside an enum is not supported. - ### Built-in `SortDirection` PHP 8.6's global unit enum is available without a user declaration: diff --git a/src/conditional/stmts.rs b/src/conditional/stmts.rs index b62690f491..cec6a35000 100644 --- a/src/conditional/stmts.rs +++ b/src/conditional/stmts.rs @@ -315,12 +315,14 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { backing_type, cases, implements, + trait_uses, methods, constants, } => StmtKind::EnumDecl { name, backing_type, implements, + trait_uses, methods, constants, cases: cases diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 0c3b743a75..2a5201018f 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -377,13 +377,16 @@ fn collect_declared_trait_names(program: &Program) -> Vec { names } -/// Collects direct trait-use declarations keyed by the declaring trait name. +/// Collects direct trait-use declarations keyed by the declaring trait or enum name. fn collect_declared_trait_uses(program: &Program) -> HashMap> { let mut uses = HashMap::new(); for stmt in program { match &stmt.kind { StmtKind::TraitDecl { name, trait_uses, .. + } + | StmtKind::EnumDecl { + name, trait_uses, .. } => { uses.insert( name.clone(), diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 912d665ba8..6adf061c7e 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -453,7 +453,7 @@ fn lowers_every_stmt_variant_smoke() { methods: vec![method.clone(), static_method.clone()], constants: vec![class_const.clone()], }), - stmt(StmtKind::EnumDecl { name: "E".to_string(), backing_type: Some(TypeExpr::Int), cases: vec![EnumCaseDecl { name: "A".to_string(), value: Some(int(1)), span: sp(), attributes: Vec::new() }], implements: Vec::new(), methods: Vec::new(), constants: Vec::new() }), + stmt(StmtKind::EnumDecl { name: "E".to_string(), backing_type: Some(TypeExpr::Int), cases: vec![EnumCaseDecl { name: "A".to_string(), value: Some(int(1)), span: sp(), attributes: Vec::new() }], implements: Vec::new(), trait_uses: Vec::new(), methods: Vec::new(), constants: Vec::new() }), stmt(StmtKind::PackedClassDecl { name: "P".to_string(), fields: vec![PackedField { name: "x".to_string(), type_expr: TypeExpr::Int, span: sp() }] }), stmt(StmtKind::InterfaceDecl { name: "I".to_string(), extends: Vec::new(), properties: Vec::new(), methods: Vec::new(), constants: Vec::new() }), stmt(StmtKind::TraitDecl { name: "T".to_string(), trait_uses: Vec::new(), properties: Vec::new(), methods: vec![method], constants: vec![class_const] }), diff --git a/src/magic_constants/walker/stmts.rs b/src/magic_constants/walker/stmts.rs index ef600df401..82ae4ef377 100644 --- a/src/magic_constants/walker/stmts.rs +++ b/src/magic_constants/walker/stmts.rs @@ -349,6 +349,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { backing_type, cases, implements, + trait_uses, methods, constants, } => { @@ -372,6 +373,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { backing_type, cases, implements, + trait_uses, methods, constants, } diff --git a/src/name_resolver/declarations.rs b/src/name_resolver/declarations.rs index a8b2d881aa..5e432ff025 100644 --- a/src/name_resolver/declarations.rs +++ b/src/name_resolver/declarations.rs @@ -108,9 +108,14 @@ pub(super) fn resolve_decl_stmt( backing_type, cases, implements, + trait_uses, methods, constants, } => { + let trait_uses = trait_uses + .iter() + .map(|trait_use| resolve_trait_use(trait_use, namespace, imports, symbols)) + .collect::, CompileError>>()?; let resolved_cases = cases .iter() .map(|case| crate::parser::ast::EnumCaseDecl { @@ -140,6 +145,7 @@ pub(super) fn resolve_decl_stmt( resolved_name(resolved_class_name(name, namespace, imports, symbols)) }) .collect(), + trait_uses, methods: resolved_methods, constants: resolve_class_consts(constants, namespace, imports, symbols), }, diff --git a/src/optimize/control/dce.rs b/src/optimize/control/dce.rs index 5db00318ea..8558911652 100644 --- a/src/optimize/control/dce.rs +++ b/src/optimize/control/dce.rs @@ -574,6 +574,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { backing_type, cases, implements, + trait_uses, methods, constants, } => vec![Stmt { @@ -582,6 +583,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { backing_type, cases, implements, + trait_uses, methods, constants, }, diff --git a/src/optimize/control/fold.rs b/src/optimize/control/fold.rs index ef7c9c8924..2db6332fa9 100644 --- a/src/optimize/control/fold.rs +++ b/src/optimize/control/fold.rs @@ -228,12 +228,14 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { backing_type, cases, implements, + trait_uses, methods, constants, } => StmtKind::EnumDecl { name, backing_type, implements, + trait_uses, methods: methods.into_iter().map(fold_method).collect(), constants, cases: cases.into_iter().map(fold_enum_case).collect(), diff --git a/src/optimize/control/prune/statements.rs b/src/optimize/control/prune/statements.rs index 64a108cced..59943a3b1b 100644 --- a/src/optimize/control/prune/statements.rs +++ b/src/optimize/control/prune/statements.rs @@ -317,6 +317,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { backing_type, cases, implements, + trait_uses, methods, constants, } => vec![Stmt { @@ -325,6 +326,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { backing_type, cases, implements, + trait_uses, methods, constants, }, diff --git a/src/optimize/propagate/stmt.rs b/src/optimize/propagate/stmt.rs index 72ce565da3..bb646d61b4 100644 --- a/src/optimize/propagate/stmt.rs +++ b/src/optimize/propagate/stmt.rs @@ -378,6 +378,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv backing_type, cases, implements, + trait_uses, methods, constants, } => ( @@ -386,6 +387,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv name, backing_type, implements, + trait_uses, methods: methods.into_iter().map(propagate_method).collect(), constants, cases: cases.into_iter().map(propagate_enum_case).collect(), diff --git a/src/parser/ast/stmt.rs b/src/parser/ast/stmt.rs index debcf12e45..c080997c16 100644 --- a/src/parser/ast/stmt.rs +++ b/src/parser/ast/stmt.rs @@ -227,6 +227,8 @@ pub enum StmtKind { cases: Vec, /// Interfaces the enum implements (`enum E implements Foo, Bar`). implements: Vec, + /// Trait-use declarations flattened into enum method metadata by the checker. + trait_uses: Vec, /// User-declared enum methods (instance and static). Enums dispatch instance methods on /// the case singleton, like a class. methods: Vec, diff --git a/src/parser/stmt/oop/declarations.rs b/src/parser/stmt/oop/declarations.rs index bb1be87f66..b43a176d61 100644 --- a/src/parser/stmt/oop/declarations.rs +++ b/src/parser/stmt/oop/declarations.rs @@ -225,12 +225,6 @@ pub(in crate::parser::stmt) fn parse_enum_decl( if !properties.is_empty() { return Err(CompileError::new(span, "Enums cannot declare properties")); } - if !trait_uses.is_empty() { - return Err(CompileError::new( - span, - "Enums using traits are not supported yet", - )); - } Ok(Stmt::new( StmtKind::EnumDecl { @@ -238,6 +232,7 @@ pub(in crate::parser::stmt) fn parse_enum_decl( backing_type, cases, implements, + trait_uses, methods, constants, }, diff --git a/src/resolver/stmt_exprs.rs b/src/resolver/stmt_exprs.rs index 6630028c9c..4d9cc5f405 100644 --- a/src/resolver/stmt_exprs.rs +++ b/src/resolver/stmt_exprs.rs @@ -488,12 +488,14 @@ pub(super) fn resolve_stmt_exprs( backing_type, cases, implements, + trait_uses, methods, constants, } => StmtKind::EnumDecl { name, backing_type, implements, + trait_uses, methods: resolve_method_exprs( methods, base_dir, diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index d89766599c..ef5ea4830c 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -75,13 +75,14 @@ pub(super) fn check_types_impl( checker.collect_function_decls(program, &mut errors); - let (mut flattened_classes, flatten_errors) = flatten_classes(program); + let (mut flattened_classes, mut flattened_enums, flatten_errors) = flatten_classes(program); errors.extend(flatten_errors); // Resolve the relative class types `self`/`static`/`parent` in every member type annotation // now that inheritance and trait flattening have settled the concrete enclosing class. This // single pass feeds the schema signatures, the body-check pass, and codegen (which all read // the flattened method/property declarations), so no later stage sees a symbolic `self`. substitute_relative_class_types_in_flattened(&mut flattened_classes); + substitute_relative_class_types_in_flattened_enums(&mut flattened_enums); let declared_traits = collect_declared_trait_names(program); let mut seen_classes = HashSet::new(); let mut class_map = HashMap::new(); @@ -231,14 +232,19 @@ pub(super) fn check_types_impl( implements, methods, constants, + .. } = &stmt.kind { + let enum_methods = flattened_enums + .get(name) + .map(|flattened| flattened.methods.as_slice()) + .unwrap_or(methods.as_slice()); if let Err(error) = build_enum_info( name, backing_type.as_ref(), cases, implements, - methods, + enum_methods, constants, stmt.span, &mut checker, @@ -276,7 +282,7 @@ pub(super) fn check_types_impl( // the enum schema pass), so they would otherwise skip body checking entirely. Flatten them // into method-checkable units here — their signatures already live in `checker.classes`. let mut methods_to_check = flattened_classes.clone(); - methods_to_check.extend(flatten_enum_methods(program)); + methods_to_check.extend(flatten_enum_methods(program, &flattened_enums)); checker.type_check_methods_until_stable(&methods_to_check, &global_env, &mut errors)?; patch_builtin_spl_storage_signatures(&mut checker); apply_implicit_stringable_interfaces(&mut checker.classes); @@ -324,19 +330,15 @@ fn collect_declared_trait_names_into(program: &Program, names: &mut HashSet Vec { +fn flatten_enum_methods( + program: &[Stmt], + flattened_enums: &HashMap, +) -> Vec { let mut units = Vec::new(); for stmt in program { if let StmtKind::EnumDecl { @@ -347,6 +349,10 @@ fn flatten_enum_methods(program: &[Stmt]) -> Vec { .. } = &stmt.kind { + if let Some(flattened) = flattened_enums.get(name) { + units.push(flattened.clone()); + continue; + } let mut flattened = FlattenedClass { name: name.clone(), extends: None, @@ -370,7 +376,13 @@ fn flatten_enum_methods(program: &[Stmt]) -> Vec { units } -/// Rewrites `self`/`static`/`parent` annotations across flattened class metadata. +/// Resolves the relative class types `self`/`static`/`parent` to concrete class names across +/// every flattened class's method parameter, method return, and property type annotations. +/// +/// `self`/`static` resolve to the flattened class itself and `parent` to its `extends` target. +/// Because trait methods are already merged into the using class at this point, a trait method's +/// `self` correctly resolves to the using class rather than the trait. Annotations with no +/// relative type are left untouched. fn substitute_relative_class_types_in_flattened(classes: &mut [FlattenedClass]) { for class in classes.iter_mut() { let self_class = class.name.clone(); @@ -385,11 +397,20 @@ fn substitute_relative_class_types_in_flattened(classes: &mut [FlattenedClass]) } } +/// Resolves relative class types inside flattened enum methods. +fn substitute_relative_class_types_in_flattened_enums( + enums: &mut HashMap, +) { + for enum_unit in enums.values_mut() { + let self_class = enum_unit.name.clone(); + substitute_relative_class_types_in_methods(&mut enum_unit.methods, &self_class, None); + } +} + /// Rewrites `self`/`static`/`parent` type annotations on a slice of methods by delegating to -/// `ClassMethod::substitute_relative_class_types`. Used for: -/// - user classes (after trait/inheritance flattening) -/// - interfaces -/// - enums (to prepare method bodies for checking) +/// `ClassMethod::substitute_relative_class_types`. +/// +/// Used for user classes after trait/inheritance flattening, interfaces, and enums. fn substitute_relative_class_types_in_methods( methods: &mut [ClassMethod], self_class: &str, diff --git a/src/types/traits.rs b/src/types/traits.rs index c9d9a5dd59..af76c50e93 100644 --- a/src/types/traits.rs +++ b/src/types/traits.rs @@ -66,16 +66,22 @@ struct ImportedMethod { decl: ClassMethod, } -/// Scans `program` for all traits and classes, validates direct member uniqueness, -/// expands trait uses for each class, merges imported vs. local members, and returns -/// a vector of `FlattenedClass` with any composition errors collected. +/// Scans `program` for all traits, classes, and enums, validates direct member uniqueness, +/// expands trait uses for each class-like declaration, and returns flattened metadata with +/// any composition errors collected. /// /// Trait declarations are stored in `trait_map` for later expansion. -/// Classes with traits are processed in program order; each class's trait uses are -/// resolved recursively, then merged with the class's own members. +/// Class-like declarations with traits are processed in program order; each declaration's trait +/// uses are resolved recursively, then merged with the declaration's own members. /// Circular trait composition and duplicate declarations are reported as errors. -/// Returns `([FlattenedClass], Vec)`. -pub fn flatten_classes(program: &Program) -> (Vec, Vec) { +/// Returns `(flattened_classes, flattened_enums, errors)`. +pub fn flatten_classes( + program: &Program, +) -> ( + Vec, + HashMap, + Vec, +) { let mut trait_map = HashMap::new(); let mut trait_keys = HashSet::new(); let mut class_like_keys = HashSet::new(); @@ -128,92 +134,175 @@ pub fn flatten_classes(program: &Program) -> (Vec, Vec result, - Err(error) => { - errors.extend(error.flatten()); - continue; - } - }; - let merged_props = match merge::merge_properties( - &imported_props, properties, - stmt.span, - &format!("class {}", name), - true, - ) { - Ok(props) => props, - Err(error) => { + methods, + constants, + } => { + if let Err(error) = + validation::validate_direct_members(properties, methods, stmt.span, name) + { errors.extend(error.flatten()); continue; } - }; - let merged_methods = match merge::merge_methods( - imported_methods, + let (imported_props, imported_methods) = match expand::resolve_trait_uses( + trait_uses, + &trait_map, + &mut cache, + &mut stack, + &format!("class {}", name), + stmt.span, + ) { + Ok(result) => result, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + let merged_props = match merge::merge_properties( + &imported_props, + properties, + stmt.span, + &format!("class {}", name), + true, + ) { + Ok(props) => props, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + let merged_methods = match merge::merge_methods( + imported_methods, + methods, + stmt.span, + &format!("class {}", name), + ) { + Ok(methods) => methods, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + let (merged_props, merged_methods) = + crate::magic_constants::bind_trait_class_constants( + merged_props, + merged_methods, + name, + ); + flattened.push(FlattenedClass { + name: name.clone(), + extends: extends.as_ref().map(|name| name.as_str().to_string()), + implements: implements.iter().map(|name| name.as_str().to_string()).collect(), + is_abstract: *is_abstract, + is_final: *is_final, + is_readonly_class: *is_readonly_class, + properties: merged_props, + methods: merged_methods, + attributes: stmt.attributes.clone(), + constants: constants.clone(), + used_traits: used_trait_names(trait_uses), + }); + } + StmtKind::EnumDecl { + name, + implements, + trait_uses, methods, - stmt.span, - &format!("class {}", name), - ) { - Ok(methods) => methods, - Err(error) => { + constants, + .. + } => { + if let Err(error) = + validation::validate_direct_members(&[], methods, stmt.span, name) + { errors.extend(error.flatten()); continue; } - }; - let (merged_props, merged_methods) = - crate::magic_constants::bind_trait_class_constants( - merged_props, - merged_methods, - name, - ); - flattened.push(FlattenedClass { - name: name.clone(), - extends: extends.as_ref().map(|name| name.as_str().to_string()), - implements: implements.iter().map(|name| name.as_str().to_string()).collect(), - is_abstract: *is_abstract, - is_final: *is_final, - is_readonly_class: *is_readonly_class, - properties: merged_props, - methods: merged_methods, - attributes: stmt.attributes.clone(), - constants: constants.clone(), - used_traits: trait_uses - .iter() - .flat_map(|use_decl| { - use_decl - .trait_names + let (imported_props, imported_methods) = match expand::resolve_trait_uses( + trait_uses, + &trait_map, + &mut cache, + &mut stack, + &format!("enum {}", name), + stmt.span, + ) { + Ok(result) => result, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + if let Some(property) = imported_props.first() { + errors.push(CompileError::new( + property.span, + "Enums cannot use traits with properties", + )); + continue; + } + let merged_methods = match merge::merge_methods( + imported_methods, + methods, + stmt.span, + &format!("enum {}", name), + ) { + Ok(methods) => methods, + Err(error) => { + errors.extend(error.flatten()); + continue; + } + }; + let (_merged_props, merged_methods) = + crate::magic_constants::bind_trait_class_constants( + Vec::new(), + merged_methods, + name, + ); + flattened_enums.insert( + name.clone(), + FlattenedClass { + name: name.clone(), + extends: None, + implements: implements .iter() .map(|name| name.as_str().to_string()) - }) - .collect(), - }); + .collect(), + is_abstract: false, + is_final: true, + is_readonly_class: false, + properties: Vec::new(), + methods: merged_methods, + attributes: stmt.attributes.clone(), + constants: constants.clone(), + used_traits: used_trait_names(trait_uses), + }, + ); + } + _ => {} } } - (flattened, errors) + (flattened, flattened_enums, errors) +} + +/// Returns the direct trait names from a group of trait-use declarations. +fn used_trait_names(trait_uses: &[TraitUse]) -> Vec { + trait_uses + .iter() + .flat_map(|use_decl| { + use_decl + .trait_names + .iter() + .map(|name| name.as_str().to_string()) + }) + .collect() } diff --git a/tests/codegen/types/enums.rs b/tests/codegen/types/enums.rs index 8f59a0d2cb..6bb0aeafa8 100644 --- a/tests/codegen/types/enums.rs +++ b/tests/codegen/types/enums.rs @@ -130,6 +130,59 @@ fn test_enum_case_name_property_and_method() { assert_eq!(out, "Hearts|Clubs"); } +/// Verifies enum methods can be imported from traits and run with `$this` bound to the case. +#[test] +fn test_enum_uses_trait_method() { + let out = compile_and_run( + "name; + } + } + enum Suit { + use HasEnumLabel; + case Hearts; + case Clubs; + } + echo Suit::Hearts->label(); + echo '|'; + echo Suit::Clubs->label(); + ", + ); + assert_eq!(out, "Hearts|Clubs"); +} + +/// Verifies enum trait adaptations support `insteadof` conflict resolution and aliases. +#[test] +fn test_enum_trait_insteadof_and_alias() { + let out = compile_and_run( + "name; + } + } + trait SecondaryEnumLabel { + public function label(): string { + return 'S:' . $this->name; + } + } + enum Mode { + use PrimaryEnumLabel, SecondaryEnumLabel { + PrimaryEnumLabel::label insteadof SecondaryEnumLabel; + SecondaryEnumLabel::label as secondaryLabel; + } + case Active; + } + echo Mode::Active->label(); + echo '|'; + echo Mode::Active->secondaryLabel(); + ", + ); + assert_eq!(out, "P:Active|S:Active"); +} + /// Verifies that `Color::from(99)` throws a catchable `ValueError` with PHP's /// invalid backing-value message. #[test] diff --git a/tests/error_tests/exceptions_enums_magic.rs b/tests/error_tests/exceptions_enums_magic.rs index 58cf289799..bc22f1d8ad 100644 --- a/tests/error_tests/exceptions_enums_magic.rs +++ b/tests/error_tests/exceptions_enums_magic.rs @@ -388,12 +388,12 @@ fn test_error_case_outside_enum() { ); } -/// Verifies that using a trait inside an enum is rejected (not yet supported). +/// Verifies that enum trait use rejects imported properties because PHP enums cannot have them. #[test] -fn test_error_enum_using_trait() { +fn test_error_enum_trait_with_property() { expect_error( - "name; } } enum Suit { use HasLabel { HasLabel::label as text; } case Hearts; }", + ); + match &stmts[1].kind { + StmtKind::EnumDecl { + trait_uses, + cases, + methods, + .. + } => { + assert_eq!(trait_uses.len(), 1); + assert_eq!(trait_uses[0].trait_names, vec!["HasLabel".to_string()]); + assert_eq!(trait_uses[0].adaptations.len(), 1); + assert_eq!(cases.len(), 1); + assert!(methods.is_empty()); + match &trait_uses[0].adaptations[0] { + TraitAdaptation::Alias { + trait_name, + method, + alias, + visibility, + } => { + assert_eq!(trait_name.as_deref(), Some("HasLabel")); + assert_eq!(method, "label"); + assert_eq!(alias.as_deref(), Some("text")); + assert_eq!(*visibility, None); + } + _ => panic!("Expected trait alias adaptation"), + } + } + _ => panic!("Expected EnumDecl"), + } +} + /// Parses `echo __TRAIT__;` and asserts the expression is `ExprKind::MagicConstant(MagicConstant::Trait)`. #[test] fn test_parse_dunder_trait_magic_constant() { From 06a17f8ba473ab6e8055ff9b78878a7a4a985824 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 01:05:51 +0200 Subject: [PATCH 0367/1208] Support nullsafe dynamic method calls --- docs/php/classes.md | 2 +- src/autoload/walk.rs | 11 ++++ .../program_usage/required_classes/collect.rs | 11 ++++ .../required_classes/dynamic_instanceof.rs | 9 +++ src/codegen_support/runtime_features.rs | 18 +++++ src/conditional/exprs.rs | 12 ++++ src/ir_lower/expr/mod.rs | 66 +++++++++++++++++-- src/ir_lower/expr/nullsafe_chain.rs | 35 ++++++++++ src/magic_constants/walker/exprs.rs | 9 +++ src/name_resolver/expressions.rs | 12 ++++ src/optimize/control/prune/expr.rs | 9 +++ src/optimize/effects.rs | 9 +++ src/optimize/fold/expr.rs | 9 +++ src/optimize/fold/inline_closure.rs | 1 + src/optimize/propagate/expr.rs | 13 ++++ src/optimize/propagate/invalidation.rs | 14 ++++ src/parser/ast/expr.rs | 5 ++ src/parser/expr/assignment_targets.rs | 32 +++++++++ src/parser/expr/pratt.rs | 21 +++--- src/parser/expr/prefix_complex.rs | 11 ++++ src/pdo_prelude/detect.rs | 5 ++ src/resolver/contains.rs | 9 +++ src/resolver/discovery/exprs.rs | 9 +++ src/resolver/exprs.rs | 23 +++++++ src/resolver/include_path.rs | 3 + src/types/checker/builtins/callables.rs | 1 + src/types/checker/inference/expr/effects.rs | 13 ++++ src/types/checker/inference/expr/mod.rs | 5 ++ .../checker/inference/expr/static_closure.rs | 12 ++++ .../checker/inference/objects/methods.rs | 27 ++++++++ src/types/checker/inference/ops.rs | 3 +- src/types/checker/schema/classes/constants.rs | 9 +++ src/types/checker/yield_validation/detect.rs | 9 +++ .../checker/yield_validation/validate.rs | 11 ++++ src/types/warnings/expr_reads.rs | 11 ++++ tests/codegen/oop/dynamic_dispatch.rs | 42 ++++++++++++ tests/error_tests/classes_traits.rs | 8 +-- tests/parser_tests/classes/access/nullsafe.rs | 22 +++++++ 38 files changed, 511 insertions(+), 20 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 9efe93f516..f717a18ff3 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -642,7 +642,7 @@ $static = "version"; echo $class::$static(); // 1.0 — both class and method dynamic ``` -`$obj->$name(...)` and `$class::$name(...)` are equivalent to `call_user_func([$obj, $name], ...)` / `call_user_func([$class, $name], ...)`. A dynamic method name on a literal class also works (`ClassName::$name(...)`). Arguments are forwarded positionally. A nullsafe dynamic method call (`$obj?->$name()`) is not yet supported, and **named arguments are rejected** in dynamic calls because the target method — and therefore its parameter names — is not known at compile time. +`$obj->$name(...)` and `$class::$name(...)` are equivalent to `call_user_func([$obj, $name], ...)` / `call_user_func([$class, $name], ...)`. A dynamic method name on a literal class also works (`ClassName::$name(...)`). Arguments are forwarded positionally. Nullsafe dynamic method calls (`$obj?->$name(...)` and `$obj?->{$expr}(...)`) short-circuit like other `?->` chains: if the receiver is `null`, the method-name expression and arguments are not evaluated and the result is `null`. **Named arguments are rejected** in dynamic calls because the target method — and therefore its parameter names — is not known at compile time. ## Anonymous classes (`new class {}`) diff --git a/src/autoload/walk.rs b/src/autoload/walk.rs index a16b661cb9..f8a8dcd84b 100644 --- a/src/autoload/walk.rs +++ b/src/autoload/walk.rs @@ -558,6 +558,17 @@ fn collect_refs_expr(expr: &Expr, out: &mut HashSet) { collect_refs_expr(a, out); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_refs_expr(object, out); + collect_refs_expr(method, out); + for a in args { + collect_refs_expr(a, out); + } + } ExprKind::NewScopedObject { receiver, args } => { collect_static_receiver(receiver, out); for a in args { diff --git a/src/codegen_support/program_usage/required_classes/collect.rs b/src/codegen_support/program_usage/required_classes/collect.rs index f42f6317bd..230ce4ff54 100644 --- a/src/codegen_support/program_usage/required_classes/collect.rs +++ b/src/codegen_support/program_usage/required_classes/collect.rs @@ -425,6 +425,17 @@ fn collect_required_class_names_in_expr(expr: &Expr, names: &mut HashSet collect_required_class_names_in_expr(arg, names); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_required_class_names_in_expr(object, names); + collect_required_class_names_in_expr(method, names); + for arg in args { + collect_required_class_names_in_expr(arg, names); + } + } ExprKind::StaticMethodCall { receiver, args, .. } => { if let crate::parser::ast::StaticReceiver::Named(name) = receiver { names.insert(name.as_str().to_string()); diff --git a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs index 1a5498843b..0e41602f26 100644 --- a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs +++ b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs @@ -241,6 +241,15 @@ fn expr_has_dynamic_instanceof(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_has_dynamic_instanceof(object) || args.iter().any(expr_has_dynamic_instanceof) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_has_dynamic_instanceof(object) + || expr_has_dynamic_instanceof(method) + || args.iter().any(expr_has_dynamic_instanceof) + } ExprKind::FirstClassCallable(crate::parser::ast::CallableTarget::Method { object, .. diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index d9472f03b2..3877cfa7b0 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -427,6 +427,15 @@ fn expr_has_regex_call(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_has_regex_call(object) || args.iter().any(expr_has_regex_call) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_has_regex_call(object) + || expr_has_regex_call(method) + || args.iter().any(expr_has_regex_call) + } ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { expr_has_regex_call(object) } @@ -732,6 +741,15 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_needs_descriptor_invoker(object) || args.iter().any(expr_needs_descriptor_invoker) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_needs_descriptor_invoker(object) + || expr_needs_descriptor_invoker(method) + || args.iter().any(expr_needs_descriptor_invoker) + } ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { expr_needs_descriptor_invoker(object) } diff --git a/src/conditional/exprs.rs b/src/conditional/exprs.rs index 1f63385282..6de1e15c9c 100644 --- a/src/conditional/exprs.rs +++ b/src/conditional/exprs.rs @@ -215,6 +215,18 @@ pub(super) fn rewrite_expr(expr: Expr, defines: &HashSet) -> Expr { .map(|arg| rewrite_expr(arg, defines)) .collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(rewrite_expr(*object, defines)), + method: Box::new(rewrite_expr(*method, defines)), + args: args + .into_iter() + .map(|arg| rewrite_expr(arg, defines)) + .collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 209547a0df..bf981ff0f2 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -140,13 +140,24 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ExprKind::StaticPropertyAccess { receiver, property } => { lower_static_property_get(ctx, receiver, property, expr) } - ExprKind::MethodCall { object, method, args } => lower_method_call(ctx, object, method, args, Op::MethodCall, expr), - ExprKind::NullsafeMethodCall { object, method, args } => { - lower_nullsafe_method_call(ctx, object, method, args, expr) - } - ExprKind::StaticMethodCall { receiver, method, args } => { - lower_static_method_call(ctx, receiver, method, args, expr) + ExprKind::MethodCall { + object, + method, + args, + } => lower_method_call(ctx, object, method, args, Op::MethodCall, expr), + ExprKind::NullsafeMethodCall { + object, + method, + args, + } => lower_nullsafe_method_call(ctx, object, method, args, expr), + ExprKind::NullsafeDynamicMethodCall { .. } => { + unreachable!("nullsafe dynamic method calls are lowered as a nullsafe postfix chain") } + ExprKind::StaticMethodCall { + receiver, + method, + args, + } => lower_static_method_call(ctx, receiver, method, args, expr), ExprKind::FirstClassCallable(target) => lower_first_class_callable(ctx, target, expr), ExprKind::This => ctx.load_local("this", Some(expr.span)), ExprKind::PtrCast { target_type, expr: inner } => lower_ptr_cast(ctx, target_type, inner, expr), @@ -679,6 +690,7 @@ fn expr_can_reset_concat_storage(expr: &Expr) -> bool { | ExprKind::ExprCall { .. } | ExprKind::MethodCall { .. } | ExprKind::NullsafeMethodCall { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } | ExprKind::StaticMethodCall { .. } | ExprKind::NewObject { .. } | ExprKind::NewDynamic { .. } @@ -8252,6 +8264,15 @@ fn expr_contains_eval_call(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_contains_eval_call(object) || args.iter().any(expr_contains_eval_call) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_contains_eval_call(object) + || expr_contains_eval_call(method) + || args.iter().any(expr_contains_eval_call) + } ExprKind::FirstClassCallable(target) => callable_target_contains_eval_call(target), ExprKind::Yield { key, value } => { key.as_ref().is_some_and(|key| expr_contains_eval_call(key)) @@ -9730,6 +9751,39 @@ fn lower_method_call_with_receiver( call } +/// Lowers a nullsafe dynamic instance method call after the receiver was evaluated and guarded. +/// +/// The non-null receiver is stored in a hidden temp so the existing +/// `call_user_func([$obj, $method], ...)` lowering can be reused without +/// evaluating the original receiver expression again. +pub(super) fn lower_dynamic_method_call_with_receiver( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + method: &Expr, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let receiver_type = strip_void_from_union(ctx.builder.value_php_type(object.value)); + let receiver_name = ctx.declare_hidden_temp(receiver_type.clone()); + ctx.store_local(&receiver_name, object, receiver_type, Some(expr.span)); + let receiver = Expr::new(ExprKind::Variable(receiver_name), expr.span); + let callback = Expr::new( + ExprKind::ArrayLiteral(vec![receiver, method.clone()]), + expr.span, + ); + let mut call_args = Vec::with_capacity(args.len() + 1); + call_args.push(callback); + call_args.extend(args.iter().cloned()); + let call = Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("call_user_func"), + args: call_args, + }, + expr.span, + ); + lower_expr(ctx, &call) +} + /// Releases normalized call arguments that cannot be returned by this call. fn release_owned_call_arg_temporaries( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/ir_lower/expr/nullsafe_chain.rs b/src/ir_lower/expr/nullsafe_chain.rs index 274c17b677..35f009ff61 100644 --- a/src/ir_lower/expr/nullsafe_chain.rs +++ b/src/ir_lower/expr/nullsafe_chain.rs @@ -68,6 +68,12 @@ enum PostfixSegment<'a> { args: &'a [Expr], nullsafe: bool, }, + DynamicMethod { + expr: &'a Expr, + method: &'a Expr, + args: &'a [Expr], + nullsafe: bool, + }, Array { expr: &'a Expr, index: &'a Expr, @@ -92,6 +98,9 @@ impl PostfixSegment<'_> { } | PostfixSegment::Method { nullsafe: true, .. + } | PostfixSegment::DynamicMethod { + nullsafe: true, + .. } ) } @@ -162,6 +171,19 @@ fn flatten_nullsafe_postfix_chain(expr: &Expr) -> Option> { }); base = object; } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + segments.push(PostfixSegment::DynamicMethod { + expr: base, + method, + args, + nullsafe: true, + }); + base = object; + } ExprKind::ArrayAccess { array, index } => { segments.push(PostfixSegment::Array { expr: base, index }); base = array; @@ -284,6 +306,19 @@ fn lower_nullsafe_postfix_segment( expr, )) } + PostfixSegment::DynamicMethod { + expr, + method, + args, + nullsafe, + } => { + if nullsafe && !guard_nullsafe_chain_receiver(ctx, current, null_block, expr) { + return None; + } + Some(super::lower_dynamic_method_call_with_receiver( + ctx, current, method, args, expr, + )) + } PostfixSegment::Array { expr, index } => { Some(lower_array_access_from_value( ctx, diff --git a/src/magic_constants/walker/exprs.rs b/src/magic_constants/walker/exprs.rs index 57b3596849..120c82aff4 100644 --- a/src/magic_constants/walker/exprs.rs +++ b/src/magic_constants/walker/exprs.rs @@ -241,6 +241,15 @@ pub(super) fn walk_expr(expr: Expr, pass: &mut P) -> Expr { method, args: args.into_iter().map(|a| walk_expr(a, pass)).collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(walk_expr(*object, pass)), + method: Box::new(walk_expr(*method, pass)), + args: args.into_iter().map(|a| walk_expr(a, pass)).collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/name_resolver/expressions.rs b/src/name_resolver/expressions.rs index 98729e303f..ef137555be 100644 --- a/src/name_resolver/expressions.rs +++ b/src/name_resolver/expressions.rs @@ -299,6 +299,18 @@ pub(super) fn resolve_expr( .map(|arg| resolve_expr(arg, current_namespace, imports, symbols)) .collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(resolve_expr(object, current_namespace, imports, symbols)), + method: Box::new(resolve_expr(method, current_namespace, imports, symbols)), + args: args + .iter() + .map(|arg| resolve_expr(arg, current_namespace, imports, symbols)) + .collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/optimize/control/prune/expr.rs b/src/optimize/control/prune/expr.rs index 6bb9af9289..a4de94b09f 100644 --- a/src/optimize/control/prune/expr.rs +++ b/src/optimize/control/prune/expr.rs @@ -219,6 +219,15 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { method, args: args.into_iter().map(prune_expr).collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(prune_expr(*object)), + method: Box::new(prune_expr(*method)), + args: args.into_iter().map(prune_expr).collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/optimize/effects.rs b/src/optimize/effects.rs index 9fc6574987..9f6e5262d8 100644 --- a/src/optimize/effects.rs +++ b/src/optimize/effects.rs @@ -263,6 +263,15 @@ pub(super) fn expr_effect(expr: &Expr) -> Effect { ExprKind::ExprCall { callee, args } => expr_effect(callee) .combine(combine_effects(args.iter().map(expr_effect))) .combine(expr_call_effect(callee)), + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_effect(object) + .combine(expr_effect(method)) + .combine(combine_effects(args.iter().map(expr_effect))) + .with_side_effects() + .with_may_throw(), ExprKind::NewObject { args, .. } => combine_effects(args.iter().map(expr_effect)) .with_side_effects() .with_may_throw(), diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index 99b17399c6..d45db5ed98 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -339,6 +339,15 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { method, args: args.into_iter().map(fold_expr).collect(), }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(fold_expr(*object)), + method: Box::new(fold_expr(*method)), + args: args.into_iter().map(fold_expr).collect(), + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/optimize/fold/inline_closure.rs b/src/optimize/fold/inline_closure.rs index 1ca249870a..bf0d7d03a9 100644 --- a/src/optimize/fold/inline_closure.rs +++ b/src/optimize/fold/inline_closure.rs @@ -149,6 +149,7 @@ fn expr_contains_call(expr: &Expr) -> bool { | ExprKind::ExprCall { .. } | ExprKind::MethodCall { .. } | ExprKind::NullsafeMethodCall { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } | ExprKind::StaticMethodCall { .. } | ExprKind::NewObject { .. } | ExprKind::NewScopedObject { .. } => true, diff --git a/src/optimize/propagate/expr.rs b/src/optimize/propagate/expr.rs index fd3c1e287b..e984dd6e5f 100644 --- a/src/optimize/propagate/expr.rs +++ b/src/optimize/propagate/expr.rs @@ -315,6 +315,19 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { args: propagate_args(args, None, None), } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + let object = propagate_expr(*object, env); + let method = propagate_expr(*method, env); + ExprKind::NullsafeDynamicMethodCall { + object: Box::new(object), + method: Box::new(method), + args: propagate_args(args, None), + } + } ExprKind::StaticMethodCall { receiver, method, diff --git a/src/optimize/propagate/invalidation.rs b/src/optimize/propagate/invalidation.rs index 7d3732f866..b1a36bca62 100644 --- a/src/optimize/propagate/invalidation.rs +++ b/src/optimize/propagate/invalidation.rs @@ -255,6 +255,20 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation { object, method, ))) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_invalidation(object) + .union(expr_invalidation(method)) + .union(args_invalidation(args)) + .union(call_args_invalidation(None, args, true)) + .union(top_level_globals_guard( + Effect::PURE + .with_side_effects() + .with_may_throw() + .with_writes_globals(), + )), ExprKind::StaticMethodCall { receiver, method, diff --git a/src/parser/ast/expr.rs b/src/parser/ast/expr.rs index d8237a80aa..53d2b77705 100644 --- a/src/parser/ast/expr.rs +++ b/src/parser/ast/expr.rs @@ -186,6 +186,11 @@ pub enum ExprKind { method: String, args: Vec, }, + NullsafeDynamicMethodCall { + object: Box, + method: Box, + args: Vec, + }, StaticMethodCall { receiver: StaticReceiver, method: String, diff --git a/src/parser/expr/assignment_targets.rs b/src/parser/expr/assignment_targets.rs index b067301187..88f37b47b4 100644 --- a/src/parser/expr/assignment_targets.rs +++ b/src/parser/expr/assignment_targets.rs @@ -316,6 +316,17 @@ fn collect_assignment_target_dependencies(expr: &Expr, dependencies: &mut HashSe | ExprKind::NullsafeMethodCall { object, .. } => { collect_assignment_target_dependencies(object, dependencies); } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_assignment_target_dependencies(object, dependencies); + collect_assignment_target_dependencies(method, dependencies); + for arg in args { + collect_assignment_target_dependencies(arg, dependencies); + } + } ExprKind::DynamicPropertyAccess { object, property } | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { collect_assignment_target_dependencies(object, dependencies); @@ -532,6 +543,18 @@ fn expr_may_write_dependency(expr: &Expr, dependencies: &HashSet) -> boo || expr_may_write_dependency(arg, dependencies) }) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_may_write_dependency(object, dependencies) + || expr_may_write_dependency(method, dependencies) + || args.iter().any(|arg| { + expr_contains_dependency(arg, dependencies) + || expr_may_write_dependency(arg, dependencies) + }) + } ExprKind::NewObject { args, .. } | ExprKind::NewScopedObject { args, .. } => { args.iter().any(|arg| { expr_contains_dependency(arg, dependencies) @@ -756,6 +779,15 @@ fn expr_contains_equivalent(expr: &Expr, needle: &Expr) -> bool { expr_contains_equivalent(object, needle) || args.iter().any(|arg| expr_contains_equivalent(arg, needle)) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_contains_equivalent(object, needle) + || expr_contains_equivalent(method, needle) + || args.iter().any(|arg| expr_contains_equivalent(arg, needle)) + } ExprKind::BufferNew { len, .. } => expr_contains_equivalent(len, needle), ExprKind::Yield { key, value } => { key.as_deref() diff --git a/src/parser/expr/pratt.rs b/src/parser/expr/pratt.rs index 447720940f..da7b143a3d 100644 --- a/src/parser/expr/pratt.rs +++ b/src/parser/expr/pratt.rs @@ -151,19 +151,24 @@ fn parse_expr_bp_inner( ObjectMember::Named(member_name) => member_name, ObjectMember::Dynamic(property) => { if *pos < tokens.len() && tokens[*pos].0 == Token::LParen { - if nullsafe { - return Err(CompileError::new( - arrow_span, - "Nullsafe dynamic method calls are not supported yet", - )); - } - // `$obj->$method(args)` reuses the runtime dynamic-dispatch path by - // desugaring to `call_user_func([$obj, $method], ...args)`. *pos += 1; // consume '(' let dynamic_args = crate::parser::expr::parse_args(tokens, pos, arrow_span)?; let arrow_span = crate::parser::expr::span_through_prev_token(tokens, *pos, arrow_span); reject_named_args_in_dynamic_call(&dynamic_args, arrow_span)?; + if nullsafe { + lhs = Expr::new( + ExprKind::NullsafeDynamicMethodCall { + object: Box::new(lhs), + method: Box::new(property), + args: dynamic_args, + }, + arrow_span, + ); + continue; + } + // `$obj->$method(args)` reuses the runtime dynamic-dispatch path by + // desugaring to `call_user_func([$obj, $method], ...args)`. let mut call_args = vec![Expr::new( ExprKind::ArrayLiteral(vec![lhs, property]), arrow_span, diff --git a/src/parser/expr/prefix_complex.rs b/src/parser/expr/prefix_complex.rs index d79c3db462..263e37b1de 100644 --- a/src/parser/expr/prefix_complex.rs +++ b/src/parser/expr/prefix_complex.rs @@ -459,6 +459,17 @@ fn collect_arrow_expr_captures( collect_arrow_expr_captures(arg, bound, seen, captures); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_arrow_expr_captures(object, bound, seen, captures); + collect_arrow_expr_captures(method, bound, seen, captures); + for arg in args { + collect_arrow_expr_captures(arg, bound, seen, captures); + } + } ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { collect_arrow_expr_captures(object, bound, seen, captures); } diff --git a/src/pdo_prelude/detect.rs b/src/pdo_prelude/detect.rs index d4a141043b..fa79cc2922 100644 --- a/src/pdo_prelude/detect.rs +++ b/src/pdo_prelude/detect.rs @@ -272,6 +272,11 @@ fn expr_refs_pdo(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_refs_pdo(object) || args.iter().any(expr_refs_pdo) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_refs_pdo(object) || expr_refs_pdo(method) || args.iter().any(expr_refs_pdo), ExprKind::StaticMethodCall { receiver, args, .. } => { receiver_refs_pdo(receiver) || args.iter().any(expr_refs_pdo) } diff --git a/src/resolver/contains.rs b/src/resolver/contains.rs index 64888047aa..abc8c02472 100644 --- a/src/resolver/contains.rs +++ b/src/resolver/contains.rs @@ -224,6 +224,15 @@ fn expr_has_includes(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_has_includes(object) || args.iter().any(expr_has_includes) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_has_includes(object) + || expr_has_includes(method) + || args.iter().any(expr_has_includes) + } ExprKind::FirstClassCallable(CallableTarget::Method { object, .. }) => { expr_has_includes(object) } diff --git a/src/resolver/discovery/exprs.rs b/src/resolver/discovery/exprs.rs index 29f5895a8a..2a8ba1b9e9 100644 --- a/src/resolver/discovery/exprs.rs +++ b/src/resolver/discovery/exprs.rs @@ -188,6 +188,15 @@ pub(super) fn discover_expr( discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?; discover_exprs(args, base_dir, loaded_paths, include_chain, state, output)?; } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?; + discover_expr(method, base_dir, loaded_paths, include_chain, state, output)?; + discover_exprs(args, base_dir, loaded_paths, include_chain, state, output)?; + } ExprKind::FirstClassCallable(crate::parser::ast::CallableTarget::Method { object, .. }) => { discover_expr(object, base_dir, loaded_paths, include_chain, state, output)?; } diff --git a/src/resolver/exprs.rs b/src/resolver/exprs.rs index 44eb7595b5..dc02d2287c 100644 --- a/src/resolver/exprs.rs +++ b/src/resolver/exprs.rs @@ -373,6 +373,29 @@ pub(super) fn resolve_expr( method, args: resolve_exprs(args, base_dir, declared_once, include_chain, state, function_variants)?, }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(resolve_expr( + *object, + base_dir, + declared_once, + include_chain, + state, + function_variants, + )?), + method: Box::new(resolve_expr( + *method, + base_dir, + declared_once, + include_chain, + state, + function_variants, + )?), + args: resolve_exprs(args, base_dir, declared_once, include_chain, state, function_variants)?, + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/resolver/include_path.rs b/src/resolver/include_path.rs index bab4f557f4..04a1bc393a 100644 --- a/src/resolver/include_path.rs +++ b/src/resolver/include_path.rs @@ -83,6 +83,9 @@ fn runtime_dynamic_include_path_detail(expr: &Expr) -> Option { ExprKind::MethodCall { method, .. } | ExprKind::NullsafeMethodCall { method, .. } => { Some(format!("method call `->{}` is resolved at runtime", method)) } + ExprKind::NullsafeDynamicMethodCall { .. } => { + Some("dynamic method call is resolved at runtime".to_string()) + } ExprKind::StaticMethodCall { method, .. } => { Some(format!("static method call `::{}` is resolved at runtime", method)) } diff --git a/src/types/checker/builtins/callables.rs b/src/types/checker/builtins/callables.rs index 0805faa9f4..37b45854e9 100644 --- a/src/types/checker/builtins/callables.rs +++ b/src/types/checker/builtins/callables.rs @@ -647,6 +647,7 @@ fn callback_descriptor_env_ownership(callback: &Expr) -> CallbackDescriptorEnvOw | ExprKind::ExprCall { .. } | ExprKind::MethodCall { .. } | ExprKind::NullsafeMethodCall { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } | ExprKind::StaticMethodCall { .. } => CallbackDescriptorEnvOwnership::Owned, ExprKind::Ternary { then_expr, diff --git a/src/types/checker/inference/expr/effects.rs b/src/types/checker/inference/expr/effects.rs index 7c94e33050..9b3c842c66 100644 --- a/src/types/checker/inference/expr/effects.rs +++ b/src/types/checker/inference/expr/effects.rs @@ -329,6 +329,19 @@ impl Checker { Self::purge_property_narrowings(env); Ok(ty) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + self.infer_type_with_assignment_effects(object, env)?; + self.infer_type_with_assignment_effects(method, env)?; + let expanded_args = crate::types::call_args::expand_static_assoc_spread_args(args); + for arg in &expanded_args { + self.infer_type_with_assignment_effects(arg, env)?; + } + self.infer_type(expr, env) + } ExprKind::BufferNew { len, .. } => { self.infer_type_with_assignment_effects(len, env)?; self.infer_type(expr, env) diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index 0663037265..e55737234e 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -582,6 +582,11 @@ impl Checker { method, args, } => self.infer_nullsafe_method_call_type(object, method, args, expr, env), + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => self.infer_nullsafe_dynamic_method_call_type(object, method, args, expr, env), ExprKind::StaticMethodCall { receiver, method, diff --git a/src/types/checker/inference/expr/static_closure.rs b/src/types/checker/inference/expr/static_closure.rs index ab3a423d8c..f39029987b 100644 --- a/src/types/checker/inference/expr/static_closure.rs +++ b/src/types/checker/inference/expr/static_closure.rs @@ -417,6 +417,18 @@ fn expr_must_not_use_this(expr: &Expr, span: Span) -> Result<(), CompileError> { } Ok(()) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_must_not_use_this(object, span)?; + expr_must_not_use_this(method, span)?; + for arg in args { + expr_must_not_use_this(arg, span)?; + } + Ok(()) + } ExprKind::ArrayLiteral(items) => { for item in items { expr_must_not_use_this(item, span)?; diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index 2c18096673..a14567aa77 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -196,6 +196,33 @@ impl Checker { } } + /// Infers `$obj?->$method(...)` when the method name is known only at runtime. + /// + /// The receiver must still be an object-or-null like other nullsafe member + /// accesses. The dynamic target prevents method signature validation, so the + /// return type is `Mixed`; method-name and argument expressions are inferred + /// only when the receiver is not statically null. + pub(crate) fn infer_nullsafe_dynamic_method_call_type( + &mut self, + object: &Expr, + method: &Expr, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result { + let obj_ty = self.infer_type(object, env)?; + let Some((_class_name, _nullable)) = + self.nullsafe_object_receiver(&obj_ty, expr, "method call")? + else { + return Ok(PhpType::Void); + }; + self.infer_type(method, env)?; + for arg in args { + self.infer_type(arg, env)?; + } + Ok(PhpType::Mixed) + } + /// Infers the type of a method call on an interface type. /// /// Looks up the method in the interface schema, validates arguments via diff --git a/src/types/checker/inference/ops.rs b/src/types/checker/inference/ops.rs index 07f5e22457..4563dedcfb 100644 --- a/src/types/checker/inference/ops.rs +++ b/src/types/checker/inference/ops.rs @@ -1181,7 +1181,8 @@ fn expr_contains_nullsafe_member(expr: &Expr) -> bool { match &expr.kind { ExprKind::NullsafePropertyAccess { .. } | ExprKind::NullsafeDynamicPropertyAccess { .. } - | ExprKind::NullsafeMethodCall { .. } => true, + | ExprKind::NullsafeMethodCall { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } => true, ExprKind::PropertyAccess { object, .. } | ExprKind::DynamicPropertyAccess { object, .. } | ExprKind::MethodCall { object, .. } => expr_contains_nullsafe_member(object), diff --git a/src/types/checker/schema/classes/constants.rs b/src/types/checker/schema/classes/constants.rs index 9487774100..d64eb95cd2 100644 --- a/src/types/checker/schema/classes/constants.rs +++ b/src/types/checker/schema/classes/constants.rs @@ -266,6 +266,15 @@ fn rewrite_expr( method: method.clone(), args: rewrite_expr_list(args, class_name, parent_name)?, }, + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => ExprKind::NullsafeDynamicMethodCall { + object: Box::new(rewrite_expr(object, class_name, parent_name)?), + method: Box::new(rewrite_expr(method, class_name, parent_name)?), + args: rewrite_expr_list(args, class_name, parent_name)?, + }, ExprKind::StaticMethodCall { receiver, method, diff --git a/src/types/checker/yield_validation/detect.rs b/src/types/checker/yield_validation/detect.rs index 5ce8923bb0..52cca7c211 100644 --- a/src/types/checker/yield_validation/detect.rs +++ b/src/types/checker/yield_validation/detect.rs @@ -182,6 +182,15 @@ fn expr_contains_yield(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_contains_yield(object) || args.iter().any(expr_contains_yield) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_contains_yield(object) + || expr_contains_yield(method) + || args.iter().any(expr_contains_yield) + } ExprKind::ArrayLiteral(items) => items.iter().any(expr_contains_yield), ExprKind::ArrayLiteralAssoc(pairs) => pairs .iter() diff --git a/src/types/checker/yield_validation/validate.rs b/src/types/checker/yield_validation/validate.rs index d25a9f9e07..5050e9567a 100644 --- a/src/types/checker/yield_validation/validate.rs +++ b/src/types/checker/yield_validation/validate.rs @@ -332,6 +332,17 @@ fn visit_expr(expr: &Expr, st: &mut State) { visit_expr(a, st); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + visit_expr(object, st); + visit_expr(method, st); + for a in args { + visit_expr(a, st); + } + } ExprKind::ArrayLiteral(items) => { for it in items { visit_expr(it, st); diff --git a/src/types/warnings/expr_reads.rs b/src/types/warnings/expr_reads.rs index d6def10c74..c1804728da 100644 --- a/src/types/warnings/expr_reads.rs +++ b/src/types/warnings/expr_reads.rs @@ -103,6 +103,17 @@ pub(super) fn collect_expr_reads( collect_expr_reads(arg, scope, warnings); } } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_expr_reads(object, scope, warnings); + collect_expr_reads(method, scope, warnings); + for arg in args { + collect_expr_reads(arg, scope, warnings); + } + } ExprKind::NewDynamic { name_expr, args } => { collect_expr_reads(name_expr, scope, warnings); for arg in args { diff --git a/tests/codegen/oop/dynamic_dispatch.rs b/tests/codegen/oop/dynamic_dispatch.rs index 41dcbc55ec..6022598bdc 100644 --- a/tests/codegen/oop/dynamic_dispatch.rs +++ b/tests/codegen/oop/dynamic_dispatch.rs @@ -57,6 +57,48 @@ fn test_dynamic_instance_method_brace_form() { assert_eq!(out, "r"); } +/// Verifies that `$obj?->$method()` dispatches dynamically when non-null and returns null when +/// the receiver is null without evaluating arguments. +#[test] +fn test_nullsafe_dynamic_instance_method_call() { + let out = compile_and_run( + "$method(skipped_arg()) ?? \"none\"; + echo \"|\"; + $c = new C(); + echo $c?->$method(\"ok\"); + ", + ); + assert_eq!(out, "none|run:ok"); +} + +/// Verifies that the brace form `$obj?->{$expr}()` skips the method-name expression and +/// arguments on the null branch, then dispatches dynamically on the non-null branch. +#[test] +fn test_nullsafe_dynamic_instance_method_brace_form_is_lazy() { + let out = compile_and_run( + "{method_name()}(skipped_arg()) ?? \"none\"; + echo \"|\"; + $g = new Greeter(); + echo $g?->{method_name()}(\"ok\"); + ", + ); + assert_eq!(out, "none|name:method:ok"); +} + /// Verifies that `$cls::method()` dispatches to a static method on the named class. #[test] fn test_dynamic_static_call_literal_method() { diff --git a/tests/error_tests/classes_traits.rs b/tests/error_tests/classes_traits.rs index 8fa19f5497..29ee5a8079 100644 --- a/tests/error_tests/classes_traits.rs +++ b/tests/error_tests/classes_traits.rs @@ -626,11 +626,11 @@ fn test_error_anonymous_class_missing_body() { ); } -/// Verifies that a nullsafe dynamic method call (`$obj?->$m()`) is rejected (not yet supported). +/// Verifies that nullsafe dynamic method calls still reject named arguments. #[test] -fn test_error_nullsafe_dynamic_method_call() { +fn test_error_nullsafe_dynamic_method_call_named_arguments() { expect_error( - "$m();", - "Nullsafe dynamic method calls are not supported yet", + "$m(value: 1);", + "Named arguments are not supported in dynamic calls", ); } diff --git a/tests/parser_tests/classes/access/nullsafe.rs b/tests/parser_tests/classes/access/nullsafe.rs index 0ff4103cbd..feee03761d 100644 --- a/tests/parser_tests/classes/access/nullsafe.rs +++ b/tests/parser_tests/classes/access/nullsafe.rs @@ -65,6 +65,28 @@ fn test_parse_nullsafe_method_call() { } } +/// Parses `$method(1);` and verifies that the AST preserves +/// the dynamic method expression as a nullsafe dynamic method call. +#[test] +fn test_parse_nullsafe_dynamic_method_call() { + let stmts = parse_source("$method(1);"); + match &stmts[0].kind { + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + assert!(matches!(object.kind, ExprKind::Variable(ref name) if name == "obj")); + assert!(matches!(method.kind, ExprKind::Variable(ref name) if name == "method")); + assert_eq!(args.len(), 1); + } + other => panic!("Expected NullsafeDynamicMethodCall, got {:?}", other), + }, + other => panic!("Expected ExprStmt, got {:?}", other), + } +} + /// Parses `profile?->name;` and verifies that chained nullsafe /// access produces a nested `NullsafePropertyAccess` AST structure: outer accesses /// "name", inner accesses "profile" on variable "$user". From 52d2aafcd91188c03befcb19f0820621f488d58a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 01:18:20 +0200 Subject: [PATCH 0368/1208] Make dynamic instantiation fatal on invalid class names --- docs/php/classes.md | 5 +- src/codegen/literal_defaults.rs | 6 ++- src/codegen/lower_inst/objects.rs | 88 +++++++++++++++++++++++++------ tests/codegen/objects/classes.rs | 40 +++++++------- 4 files changed, 99 insertions(+), 40 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index f717a18ff3..a392692553 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -614,11 +614,10 @@ $obj = new $cls(); // Foo instance echo gettype($obj); // "object" $missing = "NoSuchClass"; -$bad = new $missing(); // PHP null -echo gettype($bad); // "NULL" +$bad = new $missing(); // fatal: Class "NoSuchClass" not found ``` -elephc resolves the class name case-insensitively against compile-time class metadata, matching PHP class lookup. A match dispatches through the same allocation path as `new ClassName()`, including constructor calls, declared property defaults, and supported built-in/SPL runtime storage initialization. An unknown name currently yields PHP `null`; the missing-class fatal path is not yet tightened. +elephc resolves the class name case-insensitively against compile-time class metadata, matching PHP class lookup. A match dispatches through the same allocation path as `new ClassName()`, including constructor calls, declared property defaults, and supported built-in/SPL runtime storage initialization. Unknown class strings are fatal, and non-string class expressions are rejected with PHP's invalid class-name fatal. ## Dynamic method and static calls diff --git a/src/codegen/literal_defaults.rs b/src/codegen/literal_defaults.rs index 1b5d7333cf..cbcfe18de4 100644 --- a/src/codegen/literal_defaults.rs +++ b/src/codegen/literal_defaults.rs @@ -9,8 +9,9 @@ //! Key details: //! - This is intentionally narrower than full PHP expression lowering: only //! scalar, string, null, indexed-array literals with scalar/string/null -//! elements, and associative-array literals (empty, positional, or with -//! constant integer/string keys and scalar/string/null values) land here. +//! elements, empty object-typed indexed arrays, and associative-array literals +//! (empty, positional, or with constant integer/string keys and scalar/string/null +//! values) land here. use crate::codegen::platform::Arch; use crate::codegen::{ @@ -800,6 +801,7 @@ fn array_element_size(elem_type: &PhpType) -> Result { | PhpType::Bool | PhpType::Float | PhpType::Mixed + | PhpType::Object(_) | PhpType::Union(_) | PhpType::Iterable | PhpType::Void => Ok(8), diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 5a5df5c485..04eb2d4454 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1125,8 +1125,8 @@ pub(super) fn lower_dynamic_object_new_mixed( let done_label = ctx.next_label("dynamic_new_mixed_done"); let non_string_label = ctx.next_label("dynamic_new_mixed_non_string"); if !emit_generic_dynamic_new_class_string(ctx, class_name_value, &non_string_label)? { - emit_boxed_null(ctx); - return store_if_result(ctx, inst); + emit_dynamic_new_invalid_class_name_fatal(ctx); + return Ok(()); } abi::emit_push_result_value(ctx.emitter, &PhpType::Str); @@ -1155,8 +1155,7 @@ pub(super) fn lower_dynamic_object_new_mixed( abi::emit_jump(ctx.emitter, &done_label); ctx.emitter.label(&non_string_label); - emit_boxed_null(ctx); - ctx.store_result_value(result)?; + emit_dynamic_new_invalid_class_name_fatal(ctx); ctx.emitter.label(&done_label); Ok(()) @@ -1518,30 +1517,34 @@ fn emit_dynamic_new_mixed_constructor_call( emit_ref_arg_writebacks(ctx, &call_args.ref_writebacks) } -/// Invokes the runtime class-name registry fallback and boxes object/null as Mixed. +/// Invokes the runtime class-name registry fallback and boxes a matched object as Mixed. fn emit_dynamic_new_mixed_fallback(ctx: &mut FunctionContext<'_>) { - let null_label = ctx.next_label("dynamic_new_mixed_null"); + let miss_label = ctx.next_label("dynamic_new_mixed_missing_class"); let done_label = ctx.next_label("dynamic_new_mixed_fallback_done"); match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_pop_reg_pair(ctx.emitter, "x1", "x2"); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); abi::emit_call_label(ctx.emitter, "__rt_new_by_name"); - ctx.emitter.instruction(&format!("cbz x0, {}", null_label)); // registry miss returns PHP null for dynamic construction + ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // registry miss is PHP's class-not-found fatal for source-level dynamic construction + abi::emit_release_temporary_stack(ctx.emitter, 16); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(String::new())); - ctx.emitter.instruction(&format!("b {}", done_label)); // skip null boxing after a registry allocation - ctx.emitter.label(&null_label); - emit_boxed_null(ctx); + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the fatal path after a registry allocation + ctx.emitter.label(&miss_label); + emit_dynamic_new_class_not_found_fatal(ctx); ctx.emitter.label(&done_label); } Arch::X86_64 => { - abi::emit_pop_reg_pair(ctx.emitter, "rax", "rdx"); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rax", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", 8); abi::emit_call_label(ctx.emitter, "__rt_new_by_name"); - ctx.emitter.instruction("test rax, rax"); // registry miss returns PHP null for dynamic construction - ctx.emitter.instruction(&format!("jz {}", null_label)); // box PHP null when no runtime class table entry matched + ctx.emitter.instruction("test rax, rax"); // did the runtime class registry produce an object? + ctx.emitter.instruction(&format!("jz {}", miss_label)); // registry miss is PHP's class-not-found fatal + abi::emit_release_temporary_stack(ctx.emitter, 16); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(String::new())); - ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip null boxing after a registry allocation - ctx.emitter.label(&null_label); - emit_boxed_null(ctx); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the fatal path after a registry allocation + ctx.emitter.label(&miss_label); + emit_dynamic_new_class_not_found_fatal(ctx); ctx.emitter.label(&done_label); } } @@ -1840,6 +1843,57 @@ fn emit_dynamic_new_fatal(ctx: &mut FunctionContext<'_>, required_parent: &str) emit_fatal_message(ctx, message.as_bytes()); } +/// Emits PHP's fatal diagnostic for source-level `new $name` with a missing class. +fn emit_dynamic_new_class_not_found_fatal(ctx: &mut FunctionContext<'_>) { + let (prefix_label, prefix_len) = + ctx.data.add_string(b"Fatal error: Uncaught Error: Class \""); + let (suffix_label, suffix_len) = ctx.data.add_string(b"\" not found\n"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #2"); // select stderr for the class-not-found prefix + ctx.emitter.adrp("x1", &prefix_label); + ctx.emitter.add_lo12("x1", "x1", &prefix_label); + ctx.emitter.instruction(&format!("mov x2, #{}", prefix_len)); // pass the class-not-found prefix byte length + ctx.emitter.syscall(4); + ctx.emitter.instruction("mov x0, #2"); // select stderr for the missing class name + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); + ctx.emitter.syscall(4); + ctx.emitter.instruction("mov x0, #2"); // select stderr for the class-not-found suffix + ctx.emitter.adrp("x1", &suffix_label); + ctx.emitter.add_lo12("x1", "x1", &suffix_label); + ctx.emitter.instruction(&format!("mov x2, #{}", suffix_len)); // pass the class-not-found suffix byte length + ctx.emitter.syscall(4); + } + Arch::X86_64 => { + abi::emit_symbol_address(ctx.emitter, "rsi", &prefix_label); + ctx.emitter.instruction(&format!("mov edx, {}", prefix_len)); // pass the class-not-found prefix byte length + ctx.emitter.instruction("mov edi, 2"); // select stderr for the class-not-found prefix + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the prefix + ctx.emitter.instruction("syscall"); // write the class-not-found prefix + abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", 8); + ctx.emitter.instruction("mov edi, 2"); // select stderr for the missing class name + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the class name + ctx.emitter.instruction("syscall"); // write the missing class name + abi::emit_symbol_address(ctx.emitter, "rsi", &suffix_label); + ctx.emitter.instruction(&format!("mov edx, {}", suffix_len)); // pass the class-not-found suffix byte length + ctx.emitter.instruction("mov edi, 2"); // select stderr for the class-not-found suffix + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the suffix + ctx.emitter.instruction("syscall"); // write the class-not-found suffix + } + } + abi::emit_exit(ctx.emitter, 1); +} + +/// Emits PHP's fatal diagnostic for `new $name` when the class expression is not a string. +fn emit_dynamic_new_invalid_class_name_fatal(ctx: &mut FunctionContext<'_>) { + emit_fatal_message( + ctx, + b"Fatal error: Uncaught Error: Class name must be a valid object or a string\n", + ); +} + /// Writes a fatal diagnostic to stderr and exits. fn emit_fatal_message(ctx: &mut FunctionContext<'_>, message: &[u8]) { let (message_label, message_len) = ctx.data.add_string(message); diff --git a/tests/codegen/objects/classes.rs b/tests/codegen/objects/classes.rs index ecae889c3e..64973a8621 100644 --- a/tests/codegen/objects/classes.rs +++ b/tests/codegen/objects/classes.rs @@ -69,9 +69,7 @@ echo $b->tag; #[test] fn test_class_dynamic_instantiation() { // `new $variable()` dispatches known class names through the same AOT - // allocation path as `new ClassName()`. A known name yields an object - // Mixed cell; an unknown name currently preserves the legacy PHP null - // fallback until the missing-class fatal path is tightened. + // allocation path as `new ClassName()`. Known names yield object Mixed cells. let out = compile_and_run( r#"x; assert_eq!(out, "object:12"); } -/// Verifies compiled PHP output for dynamic instantiation missing class skips propinit. +/// Verifies dynamic instantiation of an unknown class exits with PHP's class-not-found fatal. #[test] -fn test_dynamic_instantiation_missing_class_skips_propinit() { - // An unknown class name must still return null and must NOT dispatch a - // property-init thunk for a missing class_id (regression for the - // _class_propinit_ptrs miss path). - let out = compile_and_run( +fn test_dynamic_instantiation_missing_class_is_fatal() { + let err = compile_and_run_expect_failure( r#"x; "#, ); - assert_eq!(out, "NULL|9"); + assert!(err.contains("Fatal error: Uncaught Error: Class \"Nope\" not found"), "{err}"); +} + +/// Verifies dynamic instantiation rejects non-string class expressions instead of returning null. +#[test] +fn test_dynamic_instantiation_non_string_class_name_is_fatal() { + let err = compile_and_run_expect_failure( + r#" Date: Fri, 19 Jun 2026 01:22:17 +0200 Subject: [PATCH 0369/1208] Allow new expressions as statements --- docs/php/classes.md | 2 +- src/parser/stmt/mod.rs | 1 + tests/codegen/objects/classes.rs | 43 ++++++++++++++++++++++ tests/parser_tests/classes/declarations.rs | 32 ++++++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index a392692553..7e6ec9202d 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -614,7 +614,7 @@ $obj = new $cls(); // Foo instance echo gettype($obj); // "object" $missing = "NoSuchClass"; -$bad = new $missing(); // fatal: Class "NoSuchClass" not found +new $missing(); // fatal: Class "NoSuchClass" not found ``` elephc resolves the class name case-insensitively against compile-time class metadata, matching PHP class lookup. A match dispatches through the same allocation path as `new ClassName()`, including constructor calls, declared property defaults, and supported built-in/SPL runtime storage initialization. Unknown class strings are fatal, and non-string class expressions are rejected with PHP's invalid class-name fatal. diff --git a/src/parser/stmt/mod.rs b/src/parser/stmt/mod.rs index 6befd9cda1..952f8ccca8 100644 --- a/src/parser/stmt/mod.rs +++ b/src/parser/stmt/mod.rs @@ -152,6 +152,7 @@ fn parse_stmt_dispatch( | Token::Parent | Token::Backslash | Token::Question + | Token::New | Token::LParen | Token::Match => { if matches!(&tokens[*pos].0, Token::Identifier(name) if name.eq_ignore_ascii_case("list")) diff --git a/tests/codegen/objects/classes.rs b/tests/codegen/objects/classes.rs index 64973a8621..8fc6d5d203 100644 --- a/tests/codegen/objects/classes.rs +++ b/tests/codegen/objects/classes.rs @@ -174,6 +174,37 @@ echo gettype($o) . ":" . $o->x; assert_eq!(out, "object:12"); } +/// Verifies `new ClassName();` is valid as a standalone expression statement and preserves +/// constructor side effects. +#[test] +fn test_new_object_expression_statement_runs_constructor() { + let out = compile_and_run( + r#" match &expr.kind { + ExprKind::NewObject { class_name, args } => { + assert_eq!(class_name, "Point"); + assert_eq!(args.len(), 2); + } + other => panic!("Expected NewObject, got {:?}", other), + }, + other => panic!("Expected ExprStmt, got {:?}", other), + } +} + +/// Verifies that ` match &expr.kind { + ExprKind::NewDynamic { name_expr, args } => { + assert!(matches!(&name_expr.kind, ExprKind::Variable(name) if name == "className")); + assert!(args.is_empty()); + } + other => panic!("Expected NewDynamic, got {:?}", other), + }, + other => panic!("Expected ExprStmt, got {:?}", other), + } +} + /// Verifies that ` Date: Fri, 19 Jun 2026 01:29:08 +0200 Subject: [PATCH 0370/1208] Support short property set hooks --- docs/php/classes.md | 16 ++++++++-- src/parser/stmt/oop/body.rs | 14 +++++---- tests/codegen/oop/property_hooks.rs | 39 +++++++++++++++++++++++++ tests/error_tests/misc/classes.rs | 10 +++---- tests/parser_tests/classes/modifiers.rs | 33 +++++++++++++++++++++ 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 7e6ec9202d..78ba473a42 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -371,6 +371,18 @@ $t->fahrenheit = 212.0; echo $t->celsius; // 100 ``` +The short `set => expr;` form stores `expr` into the property's own backing slot: + +```php + $this->value; + set => trim($value); + } +} +``` + Inside a property's own hook, `$this->prop` accesses the raw stored value rather than re-running the hook, so a hook may read and write the property it belongs to (a *backed* property): ```php @@ -392,7 +404,7 @@ Rules: - A property with a `get` hook but no `set` hook is read-only. Writing it is a compile-time error. - Hooked properties cannot have a default value and cannot be `static`, `final`, or `readonly`. - Each hook may be declared at most once per property. -- The short `set => expr;` form is not supported; use a block `set { ... }`. +- `set => expr;` is equivalent to assigning `expr` to the property's own backing slot. - Hooks are inherited by subclasses along with the property. Abstract classes, interfaces, and traits may declare hook *contracts* (`{ get; }`, `{ set; }`, `{ get; set; }`) with no body; see [Abstract properties](#abstract-properties) and [Interfaces](#interfaces). @@ -1198,7 +1210,7 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa ## Limitations - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. -- Backed property hooks may read and write their own backing slot, but the short `set => expr;` form is not supported; use a block `set { ... }`. +- Backed property hooks may read and write their own backing slot. - Shadowing a private parent property with a same-named child property is not yet supported (PHP gives them separate slots; elephc uses one slot per name) - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. - Class attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()`); per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). diff --git a/src/parser/stmt/oop/body.rs b/src/parser/stmt/oop/body.rs index caa280b3da..05a6008cbb 100644 --- a/src/parser/stmt/oop/body.rs +++ b/src/parser/stmt/oop/body.rs @@ -12,8 +12,8 @@ use crate::errors::CompileError; use crate::lexer::Token; use crate::names::{property_hook_get_method, property_hook_set_method}; use crate::parser::ast::{ - ClassConst, ClassMethod, ClassProperty, EnumCaseDecl, PropertyHooks, Stmt, StmtKind, TraitUse, - TypeExpr, Visibility, + ClassConst, ClassMethod, ClassProperty, EnumCaseDecl, Expr, ExprKind, PropertyHooks, Stmt, + StmtKind, TraitUse, TypeExpr, Visibility, }; use crate::parser::expr::parse_expr; use crate::span::Span; @@ -913,10 +913,14 @@ fn parse_property_hooks( if is_get { Some(vec![Stmt::new(StmtKind::Return(Some(expr)), hook_span)]) } else { - return Err(CompileError::new( + Some(vec![Stmt::new( + StmtKind::PropertyAssign { + object: Box::new(Expr::new(ExprKind::This, hook_span)), + property: prop_name.to_string(), + value: expr, + }, hook_span, - "Short `set => expr` hooks require a backed property; use a block `set { ... }`", - )); + )]) } } Some(Token::LBrace) => Some(parse_block(tokens, pos)?), diff --git a/tests/codegen/oop/property_hooks.rs b/tests/codegen/oop/property_hooks.rs index fc969dae5d..0ff3da76b6 100644 --- a/tests/codegen/oop/property_hooks.rs +++ b/tests/codegen/oop/property_hooks.rs @@ -115,6 +115,45 @@ fn test_backed_property_set_normalizes() { assert_eq!(out, "[Ada]"); } +/// Verifies a short `set => expr;` hook stores the expression result into the property's raw +/// backing slot. +#[test] +fn test_short_set_hook_normalizes_backing_slot() { + let out = compile_and_run( + " $this->value; + set => trim($value); + } + } + $n = new Name(); + $n->value = \" Ada \"; + echo \"[\", $n->value, \"]\"; + ", + ); + assert_eq!(out, "[Ada]"); +} + +/// Verifies a short set hook honors a custom parameter name. +#[test] +fn test_short_set_hook_custom_parameter_name() { + let out = compile_and_run( + " $this->text; + set(string $raw) => strtoupper($raw); + } + } + $l = new Label(); + $l->text = \"hi\"; + echo $l->text; + ", + ); + assert_eq!(out, "HI"); +} + /// Verifies a custom set-hook parameter name (`set(string $v)`) is honored in the body. #[test] fn test_set_hook_custom_parameter_name() { diff --git a/tests/error_tests/misc/classes.rs b/tests/error_tests/misc/classes.rs index 3561064468..b47aa9af73 100644 --- a/tests/error_tests/misc/classes.rs +++ b/tests/error_tests/misc/classes.rs @@ -792,13 +792,13 @@ fn test_error_write_to_get_only_hooked_property() { ); } -/// Verifies that the short `set => expr` hook form (which needs a backed property) is rejected. +/// Verifies that a set hook cannot be declared by reference. #[test] -fn test_error_short_set_hook_rejected() { - // short `set => expr` requires a backed property; only the block form is supported. +fn test_error_set_hook_by_ref_rejected() { + // only get hooks may be declared by reference; set hooks receive the assigned value. expect_error( - " $this->n; set => $this->n; } }", - "Short `set => expr` hooks require a backed property", + " $this->x; &set { $this->x = $value; } } }", + "Set property hook cannot return by reference", ); } diff --git a/tests/parser_tests/classes/modifiers.rs b/tests/parser_tests/classes/modifiers.rs index d83c61c393..ec00c17551 100644 --- a/tests/parser_tests/classes/modifiers.rs +++ b/tests/parser_tests/classes/modifiers.rs @@ -353,3 +353,36 @@ fn test_parse_get_set_hooks_generate_both_accessors() { other => panic!("Expected ClassDecl, got {:?}", other), } } + +/// Verifies a short `set => expr;` hook generates a setter accessor whose body writes to +/// the hooked property's own raw backing slot. +#[test] +fn test_parse_short_set_hook_generates_backing_assignment() { + let stmts = parse_source( + " $this->v; set => trim($value); } }", + ); + match &stmts[0].kind { + StmtKind::ClassDecl { + properties, methods, .. + } => { + let v = properties.iter().find(|p| p.name == "v").unwrap(); + assert!(v.hooks.get); + assert!(v.hooks.set); + let setter = methods.iter().find(|m| m.name == "__propset_v").unwrap(); + assert_eq!(setter.params.len(), 1); + match &setter.body[0].kind { + StmtKind::PropertyAssign { + object, + property, + value, + } => { + assert!(matches!(&object.kind, ExprKind::This)); + assert_eq!(property, "v"); + assert!(matches!(&value.kind, ExprKind::FunctionCall { .. })); + } + other => panic!("Expected synthetic PropertyAssign, got {:?}", other), + } + } + other => panic!("Expected ClassDecl, got {:?}", other), + } +} From 4a36aa728c5d697ab58243deb0a35fa0e11beae1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 01:44:35 +0200 Subject: [PATCH 0371/1208] Support ReflectionClass getConstructor --- docs/php/classes.md | 3 +- src/codegen/lower_inst/objects/reflection.rs | 56 +++++++++++++++- src/types/checker/builtin_types/reflection.rs | 60 +++++++++++++++++ tests/codegen/oop/attributes.rs | 64 +++++++++++++++++++ 4 files changed, 181 insertions(+), 2 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 78ba473a42..252cc4bd50 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1108,6 +1108,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | +| `ReflectionClass::getConstructor()` | `new ReflectionClass($class_name)` | Return the reflected constructor as a `ReflectionMethod`, or `null` when no constructor is visible | | `ReflectionClass::getProperties()` | `new ReflectionClass($class_name)` | Return `ReflectionProperty` objects for properties visible through the reflected class-like metadata | | `ReflectionClass::newInstance()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class with forwarded constructor arguments | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | @@ -1183,7 +1184,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index f7c18fc452..13a4efd788 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -11,7 +11,7 @@ //! constructors are compile-time metadata lookups that populate private //! `__name`/`__attrs` slots instead of running their public empty bodies. -use crate::codegen::abi; +use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::codegen::platform::Arch; use crate::codegen::{CodegenIrError, Result}; use crate::ir::{Immediate, Instruction, Op, ValueDef, ValueId}; @@ -32,6 +32,7 @@ struct ReflectionOwnerMetadata { property_names: Vec, method_members: Vec, property_members: Vec, + constructor_member: Option, is_final: bool, is_abstract: bool, is_interface: bool, @@ -43,6 +44,7 @@ struct ReflectionOwnerMetadata { } /// Metadata for one member object returned by `ReflectionClass::getMethods()` or `getProperties()`. +#[derive(Clone)] struct ReflectionListedMember { name: String, attr_names: Vec, @@ -131,6 +133,7 @@ pub(super) fn lower_reflection_owner_new( "ReflectionMethod", &metadata.method_members, )?; + emit_reflection_constructor_property(ctx, metadata.constructor_member.as_ref())?; emit_reflection_member_array_property_by_name( ctx, "__properties", @@ -582,6 +585,7 @@ fn reflection_class_metadata( let method_members = reflection_class_method_members(info, &method_names); let property_members = reflection_class_property_members(ctx, class_name, info, &property_names); + let constructor_member = reflection_constructor_member(&method_members); return Ok(ReflectionOwnerMetadata { reflected_name: Some(class_name.to_string()), attr_names: info.attribute_names.clone(), @@ -592,6 +596,7 @@ fn reflection_class_metadata( property_names, method_members, property_members, + constructor_member, is_final: info.is_final, is_abstract: info.is_abstract, is_interface: false, @@ -666,6 +671,7 @@ fn reflection_method_metadata( property_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + constructor_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -704,6 +710,7 @@ fn reflection_property_metadata( property_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + constructor_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -743,6 +750,7 @@ fn reflection_class_constant_metadata( property_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + constructor_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -776,6 +784,7 @@ fn reflection_class_constant_metadata( property_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + constructor_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -816,6 +825,7 @@ fn reflection_enum_case_metadata( property_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + constructor_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -1155,6 +1165,16 @@ fn default_property_members( .collect() } +/// Returns the `__construct` member object metadata when the reflected class-like symbol has one. +fn reflection_constructor_member( + method_members: &[ReflectionListedMember], +) -> Option { + method_members + .iter() + .find(|member| php_symbol_key(&member.name) == "__construct") + .cloned() +} + /// Builds common ReflectionMethod/ReflectionProperty predicate flags. fn reflection_member_flags( is_static: bool, @@ -1264,6 +1284,7 @@ fn class_like_reflection_metadata( ) -> ReflectionOwnerMetadata { let method_members = default_method_members(method_names.as_slice(), is_interface, is_trait); let property_members = default_property_members(property_names.as_slice(), is_interface); + let constructor_member = reflection_constructor_member(&method_members); ReflectionOwnerMetadata { reflected_name: Some(class_like_name.to_string()), attr_names: Vec::new(), @@ -1274,6 +1295,7 @@ fn class_like_reflection_metadata( property_names, method_members, property_members, + constructor_member, is_final: false, is_abstract: false, is_interface, @@ -1325,6 +1347,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { property_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + constructor_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -1558,6 +1581,37 @@ fn emit_reflection_member_array_property_by_name( Ok(()) } +/// Replaces the ReflectionClass private constructor slot with `ReflectionMethod|null`. +fn emit_reflection_constructor_property( + ctx: &mut FunctionContext<'_>, + member: Option<&ReflectionListedMember>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, "__constructor")?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(member) = member { + emit_reflection_member_object(ctx, "ReflectionMethod", member)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionMethod".to_string()), + ); + } else { + super::emit_boxed_null(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + /// Allocates an indexed array of populated ReflectionMethod/ReflectionProperty objects. fn emit_reflection_member_array( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 54ef0b6dc8..aa88528f61 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -223,6 +223,11 @@ fn bool_lit(value: bool) -> Option { )) } +/// Returns a `null` expression for nullable synthetic property defaults. +fn null_expr() -> Option { + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())) +} + /// Returns a `TypeExpr` for the unqualified name `array`. fn array_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("array")) @@ -238,6 +243,11 @@ fn object_array_type(class_name: &str) -> TypeExpr { TypeExpr::Array(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) } +/// Returns a nullable object type expression for one synthetic reflection class. +fn nullable_object_type(class_name: &str) -> TypeExpr { + TypeExpr::Nullable(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) +} + /// Returns a `TypeExpr` for the unqualified name `mixed`. fn mixed_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("mixed")) @@ -691,6 +701,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(object_array_type("ReflectionMethod")), empty_array(), ), + builtin_property( + "__constructor", + Visibility::Private, + Some(nullable_object_type("ReflectionMethod")), + null_expr(), + ), builtin_property( "__properties", Visibility::Private, @@ -733,6 +749,11 @@ fn builtin_reflection_class() -> FlattenedClass { "__methods", object_array_type("ReflectionMethod"), ), + builtin_reflection_class_nullable_object_method( + "getConstructor", + "__constructor", + "ReflectionMethod", + ), builtin_reflection_class_array_method( "getProperties", "__properties", @@ -911,6 +932,39 @@ fn builtin_reflection_class_array_method( } } +/// Returns a public nullable object `ReflectionClass` method backed by one private slot. +fn builtin_reflection_class_nullable_object_method( + method_name: &str, + property: &str, + class_name: &str, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(nullable_object_type(class_name)), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass` boolean method backed by one private slot. fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -1159,6 +1213,12 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionProperty".to_string(), ))); } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getConstructor")) { + sig.return_type = PhpType::Union(vec![ + PhpType::Object("ReflectionMethod".to_string()), + PhpType::Void, + ]); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index b8039823d5..3b08a866a5 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1587,6 +1587,70 @@ foreach ($properties as $property) { assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); } +/// Verifies that `ReflectionClass::getConstructor()` returns a ReflectionMethod +/// for direct, inherited, interface, and trait constructors, and null otherwise. +#[test] +fn test_reflection_class_get_constructor_returns_method_or_null() { + let out = compile_and_run( + r#"getConstructor(); +if ($base instanceof ReflectionMethod) { + echo $base->getName(); +} else { + echo "null"; +} +echo ":"; + +$child = (new ReflectionClass(ReflectCtorChild::class))->getConstructor(); +if ($child instanceof ReflectionMethod) { + echo $child->getName(); +} else { + echo "null"; +} +echo ":"; + +$plain = (new ReflectionClass(ReflectCtorPlain::class))->getConstructor(); +if ($plain instanceof ReflectionMethod) { + echo $plain->getName(); +} else { + echo "null"; +} +echo ":"; + +$interface = (new ReflectionClass(ReflectCtorInterface::class))->getConstructor(); +if ($interface instanceof ReflectionMethod) { + echo $interface->getName(); +} else { + echo "null"; +} +echo ":"; + +$trait = (new ReflectionClass(ReflectCtorTrait::class))->getConstructor(); +if ($trait instanceof ReflectionMethod) { + echo $trait->getName(); +} else { + echo "null"; +} +"#, + ); + assert_eq!( + out, + "__construct:__construct:null:__construct:__construct" + ); +} + /// Verifies that `ReflectionClass::newInstance()` constructs reflected classes /// and forwards direct and statically-unpacked constructor arguments. #[test] From f30c9036b57fe8625bbf77f8d19b9596e57b76cd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 02:17:18 +0200 Subject: [PATCH 0372/1208] Support reflection parameter metadata --- .../elephc-eval/src/interpreter/reflection.rs | 116 +++ .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 58 ++ .../interpreter/tests/support/object_ops.rs | 51 +- docs/internals/the-parser.md | 2 +- docs/php/classes.md | 11 +- docs/php/eval.md | 12 +- src/codegen/eval_reflection_owner_helpers.rs | 830 ++++++++++-------- src/codegen/lower_inst/objects.rs | 1 + src/codegen/lower_inst/objects/reflection.rs | 213 ++++- src/ir_lower/program.rs | 2 + src/types/checker/builtin_types/reflection.rs | 51 ++ tests/codegen/eval.rs | 28 + tests/codegen/oop/attributes.rs | 58 ++ tests/error_tests/classes_traits.rs | 10 + 15 files changed, 1043 insertions(+), 401 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 3465bd0d78..e05a79ad1d 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -24,6 +24,10 @@ const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; +const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; +const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; +const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; /// Eval metadata needed to materialize one `ReflectionClass` owner object. struct EvalReflectionClassMetadata { @@ -44,6 +48,17 @@ struct EvalReflectionMemberMetadata { is_static: bool, is_final: bool, is_abstract: bool, + parameters: Vec, +} + +/// Eval metadata needed to materialize one `ReflectionParameter` object. +struct EvalReflectionParameterMetadata { + name: String, + position: usize, + is_optional: bool, + is_variadic: bool, + is_passed_by_reference: bool, + has_type: bool, } /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. @@ -102,6 +117,7 @@ fn eval_reflection_class_new( &metadata.trait_names, &metadata.method_names, &metadata.property_names, + &[], metadata.flags, metadata.modifiers, context, @@ -141,6 +157,7 @@ fn eval_reflection_method_new( &[], &[], &[], + &method.parameters, flags, 0, context, @@ -175,6 +192,7 @@ fn eval_reflection_property_new( &[], &[], &[], + &[], flags, 0, context, @@ -209,6 +227,7 @@ fn eval_reflection_class_constant_new( &[], &[], &[], + &[], 0, 0, context, @@ -252,6 +271,7 @@ fn eval_reflection_enum_case_new( &[], &[], &[], + &[], 0, 0, context, @@ -269,6 +289,7 @@ fn eval_reflection_owner_object( trait_names: &[String], method_names: &[String], property_names: &[String], + parameter_metadata: &[EvalReflectionParameterMetadata], flags: u64, modifiers: u64, context: &mut ElephcEvalContext, @@ -287,6 +308,8 @@ fn eval_reflection_owner_object( context, values, )? + } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + eval_reflection_parameter_object_array_result(parameter_metadata, values)? } else { values.array_new(0)? }; @@ -340,6 +363,56 @@ fn eval_reflection_string_array_result( Ok(result) } +/// Builds an indexed array of populated ReflectionParameter objects. +fn eval_reflection_parameter_object_array_result( + parameters: &[EvalReflectionParameterMetadata], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(parameters.len())?; + for parameter in parameters { + let parameter_object = eval_reflection_parameter_object_result(parameter, values)?; + let key = values.int(parameter.position as i64)?; + result = values.array_set(result, key, parameter_object)?; + } + Ok(result) +} + +/// Materializes one ReflectionParameter object through the shared reflection helper. +fn eval_reflection_parameter_object_result( + parameter: &EvalReflectionParameterMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let method_objects = values.array_new(0)?; + let property_objects = values.array_new(0)?; + let flags = eval_reflection_parameter_flags(parameter); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_PARAMETER, + ¶meter.name, + attrs, + interface_names, + trait_names, + method_names, + property_names, + method_objects, + property_objects, + flags, + parameter.position as u64, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(method_objects)?; + values.release(property_objects)?; + Ok(object) +} + /// Builds an indexed array of ReflectionMethod or ReflectionProperty objects for a ReflectionClass. fn eval_reflection_member_object_array_result( owner_kind: u64, @@ -369,6 +442,7 @@ fn eval_reflection_member_object_array_result( &[], &[], &[], + &member.parameters, flags, 0, context, @@ -531,6 +605,7 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), + parameters: eval_reflection_parameters_from_names(method.params()), }); } if context.has_interface(class_name) { @@ -544,6 +619,7 @@ fn eval_reflection_method_metadata( is_static: false, is_final: false, is_abstract: true, + parameters: eval_reflection_parameters_from_names(method.params()), }); } context.trait_decl(class_name).and_then(|trait_decl| { @@ -557,6 +633,7 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), + parameters: eval_reflection_parameters_from_names(method.params()), }) }) } @@ -576,6 +653,7 @@ fn eval_reflection_property_metadata( is_static: property.is_static(), is_final: false, is_abstract: property.is_abstract(), + parameters: Vec::new(), }); } if context.has_interface(class_name) { @@ -589,6 +667,7 @@ fn eval_reflection_property_metadata( is_static: false, is_final: false, is_abstract: true, + parameters: Vec::new(), }); } context.trait_decl(class_name).and_then(|trait_decl| { @@ -602,10 +681,29 @@ fn eval_reflection_property_metadata( is_static: property.is_static(), is_final: false, is_abstract: property.is_abstract(), + parameters: Vec::new(), }) }) } +/// Builds parameter reflection metadata from the currently supported eval parameter syntax. +fn eval_reflection_parameters_from_names( + names: &[String], +) -> Vec { + names + .iter() + .enumerate() + .map(|(position, name)| EvalReflectionParameterMetadata { + name: name.clone(), + position, + is_optional: false, + is_variadic: false, + is_passed_by_reference: false, + has_type: false, + }) + .collect() +} + /// Packs ReflectionMethod/ReflectionProperty predicate flags for the runtime owner factory. fn eval_reflection_member_flags( visibility: EvalVisibility, @@ -631,6 +729,24 @@ fn eval_reflection_member_flags( flags } +/// Packs ReflectionParameter predicate flags for the runtime parameter factory. +fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) -> u64 { + let mut flags = 0; + if parameter.is_optional { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL; + } + if parameter.is_variadic { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC; + } + if parameter.is_passed_by_reference { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_BY_REF; + } + if parameter.has_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE; + } + flags +} + /// Converts one reflection constructor argument to a Rust UTF-8 string. fn eval_reflection_string_arg( value: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 47a67feda4..4c85e8a6e7 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -364,3 +364,4 @@ pub(super) const EVAL_REFLECTION_OWNER_PROPERTY: u64 = 2; pub(super) const EVAL_REFLECTION_OWNER_CLASS_CONSTANT: u64 = 3; pub(super) const EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE: u64 = 4; pub(super) const EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE: u64 = 5; +pub(super) const EVAL_REFLECTION_OWNER_PARAMETER: u64 = 6; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 71da697f62..2b0f0d1079 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -488,6 +488,64 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod exposes eval method parameter objects with names and positions. +#[test] +fn execute_program_reflects_eval_method_parameters() { + let program = parse_fragment( + br##"class EvalReflectParamTarget { + public function run($first, $second) {} +} +$params = (new ReflectionMethod("EvalReflectParamTarget", "run"))->getParameters(); +echo count($params); echo ":"; +foreach ($params as $param) { + echo $param->getName(); echo "#"; echo $param->getPosition(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isVariadic() ? "V" : "v"; + echo $param->isPassedByReference() ? "R" : "b"; + echo $param->hasType() ? "T" : "t"; + echo "|"; +} +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:first#0rvbt|second#1rvbt|"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::getMethods preserves eval method parameter metadata. +#[test] +fn execute_program_reflection_class_lists_eval_method_parameters() { + let program = parse_fragment( + br#"class EvalReflectListedParamTarget { + public function first($left) {} + public function second($right, $tail) {} +} +$methods = (new ReflectionClass("EvalReflectListedParamTarget"))->getMethods(); +foreach ($methods as $method) { + $params = $method->getParameters(); + echo $method->getName(); echo ":"; echo count($params); + if (count($params) > 0) { + echo ":"; echo $params[0]->getName(); echo ":"; echo $params[0]->getPosition(); + } + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "first:1:left:0|second:2:right:0|"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass getMethods/getProperties return eval member objects. #[test] fn execute_program_reflection_class_lists_eval_member_objects() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 433d232ff1..d72c6b1c6d 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -15,6 +15,10 @@ const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; +const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; +const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; +const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; impl FakeOps { /// Reads one fake object property by name. @@ -190,6 +194,29 @@ impl FakeOps { Self::object_property(&properties, "__args") .map_or_else(|| self.runtime_array_new(0), Ok) } + (FakeValue::Object(properties), "getparameters") if args.is_empty() => { + Self::object_property(&properties, "__parameters") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getposition") if args.is_empty() => { + Self::object_property(&properties, "__position").map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "isoptional") if args.is_empty() => { + Self::object_property(&properties, "__is_optional") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isvariadic") if args.is_empty() => { + Self::object_property(&properties, "__is_variadic") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "ispassedbyreference") if args.is_empty() => { + Self::object_property(&properties, "__is_passed_by_reference") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "hastype") if args.is_empty() => { + Self::object_property(&properties, "__has_type") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(_), "newinstance") if args.is_empty() => self.null(), (FakeValue::Object(properties), "getattributes") if args.is_empty() => { Self::object_property(&properties, "__attrs") @@ -317,6 +344,7 @@ impl FakeOps { EVAL_REFLECTION_OWNER_CLASS_CONSTANT => "ReflectionClassConstant", EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE => "ReflectionEnumUnitCase", EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => "ReflectionEnumBackedCase", + EVAL_REFLECTION_OWNER_PARAMETER => "ReflectionParameter", _ => return Err(EvalStatus::RuntimeFatal), }; let name = self.string(reflected_name)?; @@ -326,7 +354,7 @@ impl FakeOps { let is_trait = self.bool_value((flags & 8) != 0)?; let is_enum = self.bool_value((flags & 16) != 0)?; let is_readonly = self.bool_value((flags & 32) != 0)?; - let modifiers = self.int(modifiers as i64)?; + let modifiers_cell = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { let (namespace_name, short_name) = reflection_name_parts(reflected_name); @@ -340,7 +368,7 @@ impl FakeOps { properties.push(("__is_trait".to_string(), is_trait)); properties.push(("__is_enum".to_string(), is_enum)); properties.push(("__is_readonly".to_string(), is_readonly)); - properties.push(("__modifiers".to_string(), modifiers)); + properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__short_name".to_string(), short_name)); properties.push(("__namespace_name".to_string(), namespace_name)); properties.push(("__in_namespace".to_string(), in_namespace)); @@ -370,6 +398,25 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); + properties.push(("__parameters".to_string(), method_objects)); + } + if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { + let position = self.int(modifiers as i64)?; + let is_optional = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL) != 0)?; + let is_variadic = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC) != 0)?; + let is_passed_by_reference = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_BY_REF) != 0)?; + let has_type = self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE) != 0)?; + properties.push(("__position".to_string(), position)); + properties.push(("__is_optional".to_string(), is_optional)); + properties.push(("__is_variadic".to_string(), is_variadic)); + properties.push(( + "__is_passed_by_reference".to_string(), + is_passed_by_reference, + )); + properties.push(("__has_type".to_string(), has_type)); } let object = self.alloc(FakeValue::Object(properties)); self.object_classes diff --git a/docs/internals/the-parser.md b/docs/internals/the-parser.md index eb5da2f4c0..6db5d2f4ba 100644 --- a/docs/internals/the-parser.md +++ b/docs/internals/the-parser.md @@ -230,7 +230,7 @@ forms such as `?T|U` and normalize accepted declarations. | Type | Fields | Description | |---|---|---| | `Visibility` | `Public`, `Protected`, `Private` | Enum for property/method visibility | -| `Attribute` | `name`, `args`, `span` | A PHP 8 attribute entry from a `#[...]` group. The parser validates names and optional argument expressions. Class, method, and property names plus supported literal args feed `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported Reflection `getAttributes()` APIs; parameter reflection is not implemented yet. | +| `Attribute` | `name`, `args`, `span` | A PHP 8 attribute entry from a `#[...]` group. The parser validates names and optional argument expressions. Class, method, and property names plus supported literal args feed `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported Reflection `getAttributes()` APIs. Method parameter names, positions, optional/variadic/by-reference flags, and declared-type presence feed the supported `ReflectionMethod::getParameters()` / `ReflectionParameter` slice. | | `AttributeGroup` | `attributes`, `span` | One bracketed attribute group. Declaration sites can carry one or more groups. | | `EnumCaseDecl` | `name`, `value`, `span`, `attributes` | A backed or unit enum case declaration, with declaration-level attributes preserved in the AST. | | `ClassConst` | `name`, `visibility`, `is_final`, `value`, `span`, `attributes` | A class, interface, or trait constant declaration. | diff --git a/docs/php/classes.md b/docs/php/classes.md index 252cc4bd50..f21266403e 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1120,6 +1120,13 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isPrivate()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is private | | `ReflectionMethod::isFinal()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is final | | `ReflectionMethod::isAbstract()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is abstract | +| `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | +| `ReflectionParameter::getName()` | Internal only | Return the parameter name without `$` | +| `ReflectionParameter::getPosition()` | Internal only | Return the zero-based parameter position | +| `ReflectionParameter::isOptional()` | Internal only | Return whether the parameter has a default value or is variadic | +| `ReflectionParameter::isVariadic()` | Internal only | Return whether the parameter is variadic | +| `ReflectionParameter::isPassedByReference()` | Internal only | Return whether the parameter is passed by reference | +| `ReflectionParameter::hasType()` | Internal only | Return whether the parameter has a declared type | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | @@ -1184,7 +1191,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1214,4 +1221,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - Backed property hooks may read and write their own backing slot. - Shadowing a private parent property with a same-named child property is not yet supported (PHP gives them separate slots; elephc uses one slot per name) - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()`); per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()` for supported named types); method parameter names, positions, optional/variadic/by-reference flags, and declared-type presence are exposed through `ReflectionMethod::getParameters()`. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). diff --git a/docs/php/eval.md b/docs/php/eval.md index c943b8438c..4b273ce0d3 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -176,6 +176,11 @@ classes and forwards constructor arguments through eval's positional, named, and unpacking-aware call binding. `ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()` report eval method metadata. +`ReflectionMethod::getParameters()` returns `ReflectionParameter` objects for +eval-declared method parameters. Eval currently exposes parameter names and +zero-based positions there; optional, variadic, by-reference, and declared-type +flags remain false until eval's method-parameter parser carries those richer +parameter forms. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()` report eval property metadata. `ReflectionClassConstant::getAttributes()`, @@ -339,9 +344,10 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported -ReflectionClass/Method/Property/attribute slice and broader generated/AOT method -bridge signatures beyond the current public non-by-reference fixed scalar/Mixed -slice. +ReflectionClass/Method/Parameter/Property/attribute slice, richer +ReflectionParameter metadata inside eval-declared methods, and broader +generated/AOT method bridge signatures beyond the current public +non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index cac81de879..c8760867b5 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1,15 +1,15 @@ //! Purpose: //! Emits user-assembly helpers that let libelephc-eval materialize -//! ReflectionClass, ReflectionMethod, ReflectionProperty, ReflectionClassConstant, -//! and ReflectionEnum* objects with private metadata slots populated from -//! runtime eval declarations. +//! ReflectionClass, ReflectionMethod, ReflectionParameter, ReflectionProperty, +//! ReflectionClassConstant, and ReflectionEnum* objects with private metadata +//! slots populated from runtime eval declarations. //! //! Called from: //! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. //! //! Key details: //! - Reflection owner objects store private metadata slots such as `__attrs`, -//! `__name`, and the ReflectionClass metadata-name arrays. +//! `__name`, `__parameters`, and the ReflectionClass metadata-name arrays. //! - The helper retains supplied array payloads for object ownership. use crate::codegen::abi; @@ -68,6 +68,16 @@ struct ReflectionOwnerLayout { is_protected_hi: Option, is_private_lo: Option, is_private_hi: Option, + position_lo: Option, + position_hi: Option, + is_optional_lo: Option, + is_optional_hi: Option, + is_variadic_lo: Option, + is_variadic_hi: Option, + is_passed_by_reference_lo: Option, + is_passed_by_reference_hi: Option, + has_type_lo: Option, + has_type_hi: Option, } /// Layouts for the Reflection owner classes eval can materialize. @@ -78,6 +88,7 @@ struct ReflectionOwnerLayouts { class_constant: ReflectionOwnerLayout, enum_unit_case: ReflectionOwnerLayout, enum_backed_case: ReflectionOwnerLayout, + parameter: ReflectionOwnerLayout, } /// Emits eval Reflection owner helpers when any lowered function owns an eval context. @@ -144,6 +155,7 @@ fn reflection_owner_layouts(module: &Module) -> Option { module.class_infos.get("ReflectionEnumBackedCase")?, true, )?, + parameter: reflection_owner_layout(module.class_infos.get("ReflectionParameter")?, true)?, }) } @@ -159,7 +171,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option Option Option fn emit_reflection_owner_new_stub(emitter: &mut Emitter) { match emitter.target.arch { Arch::AArch64 => { - emitter.instruction("mov x0, xzr"); // report helper failure when Reflection owner metadata is missing - emitter.instruction("ret"); // return the null pointer to Rust + emitter.instruction("mov x0, xzr"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust } Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // report helper failure when Reflection owner metadata is missing - emitter.instruction("ret"); // return the null pointer to Rust + emitter.instruction("xor eax, eax"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust } } } @@ -253,38 +281,41 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant"; let enum_unit_case_label = "__elephc_eval_reflection_owner_new_enum_unit_case"; let enum_backed_case_label = "__elephc_eval_reflection_owner_new_enum_backed_case"; - emitter.instruction("sub sp, sp, #160"); // reserve helper frame for inputs, object, arrays, scratch, and fp/lr - emitter.instruction("stp x29, x30, [sp, #144]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #144"); // establish a stable helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the Reflection owner kind - emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer - emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length - emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array - emitter.instruction("str x4, [sp, #80]"); // save the boxed ReflectionClass interface-name array - emitter.instruction("str x5, [sp, #88]"); // save the boxed ReflectionClass trait-name array - emitter.instruction("str x6, [sp, #104]"); // save the boxed ReflectionClass method-name array - emitter.instruction("str x7, [sp, #112]"); // save the boxed ReflectionClass property-name array - emitter.instruction("ldr x8, [sp, #160]"); // load the boxed ReflectionClass method objects array from the first stack argument - emitter.instruction("str x8, [sp, #120]"); // save the boxed ReflectionClass method objects array - emitter.instruction("ldr x8, [sp, #168]"); // load the boxed ReflectionClass property objects array from the second stack argument - emitter.instruction("str x8, [sp, #128]"); // save the boxed ReflectionClass property objects array - emitter.instruction("ldr x8, [sp, #176]"); // load ReflectionClass modifier flags from the third stack argument - emitter.instruction("str x8, [sp, #48]"); // save ReflectionClass modifier flags - emitter.instruction("ldr x8, [sp, #184]"); // load ReflectionClass getModifiers bitmask from the fourth stack argument - emitter.instruction("str x8, [sp, #96]"); // save ReflectionClass getModifiers bitmask - emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass - emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner - emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod - emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner - emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty - emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner - emitter.instruction("cmp x0, #3"); // owner kind 3 means ReflectionClassConstant - emitter.instruction(&format!("b.eq {}", class_constant_label)); // allocate a ReflectionClassConstant owner - emitter.instruction("cmp x0, #4"); // owner kind 4 means ReflectionEnumUnitCase - emitter.instruction(&format!("b.eq {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner - emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase - emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner - emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds + let parameter_label = "__elephc_eval_reflection_owner_new_parameter"; + emitter.instruction("sub sp, sp, #160"); // reserve helper frame for inputs, object, arrays, scratch, and fp/lr + emitter.instruction("stp x29, x30, [sp, #144]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #144"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the Reflection owner kind + emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer + emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array + emitter.instruction("str x4, [sp, #80]"); // save the boxed ReflectionClass interface-name array + emitter.instruction("str x5, [sp, #88]"); // save the boxed ReflectionClass trait-name array + emitter.instruction("str x6, [sp, #104]"); // save the boxed ReflectionClass method-name array + emitter.instruction("str x7, [sp, #112]"); // save the boxed ReflectionClass property-name array + emitter.instruction("ldr x8, [sp, #160]"); // load the boxed ReflectionClass method objects array from the first stack argument + emitter.instruction("str x8, [sp, #120]"); // save the boxed ReflectionClass method objects array + emitter.instruction("ldr x8, [sp, #168]"); // load the boxed ReflectionClass property objects array from the second stack argument + emitter.instruction("str x8, [sp, #128]"); // save the boxed ReflectionClass property objects array + emitter.instruction("ldr x8, [sp, #176]"); // load ReflectionClass modifier flags from the third stack argument + emitter.instruction("str x8, [sp, #48]"); // save ReflectionClass modifier flags + emitter.instruction("ldr x8, [sp, #184]"); // load ReflectionClass getModifiers bitmask from the fourth stack argument + emitter.instruction("str x8, [sp, #96]"); // save ReflectionClass getModifiers bitmask + emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp x0, #3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("b.eq {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp x0, #4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("b.eq {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner + emitter.instruction("cmp x0, #6"); // owner kind 6 means ReflectionParameter + emitter.instruction(&format!("b.eq {}", parameter_label)); // allocate a ReflectionParameter owner + emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body( emitter, class_label, @@ -333,18 +364,26 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection fail_label, box_label, ); + emit_aarch64_owner_kind_body( + emitter, + parameter_label, + &layouts.parameter, + true, + fail_label, + box_label, + ); emitter.label(box_label); - emitter.instruction("mov x0, #6"); // runtime tag 6 = object - emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload - emitter.instruction("mov x2, xzr"); // object payloads do not use a high word - emitter.instruction("bl __rt_mixed_from_value"); // box the Reflection owner object for eval - emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing emitter.label(fail_label); - emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #144]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #160"); // release the helper frame - emitter.instruction("ret"); // return the boxed reflection owner to Rust + emitter.instruction("ldp x29, x30, [sp, #144]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #160"); // release the helper frame + emitter.instruction("ret"); // return the boxed reflection owner to Rust } /// Emits the x86_64 Reflection owner materializer helper body. @@ -358,40 +397,43 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant_x"; let enum_unit_case_label = "__elephc_eval_reflection_owner_new_enum_unit_case_x"; let enum_backed_case_label = "__elephc_eval_reflection_owner_new_enum_backed_case_x"; - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 144"); // reserve slots for inputs, object, metadata arrays, and name parts - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the Reflection owner kind - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer - emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length - emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array - emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // save the boxed ReflectionClass interface-name array - emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // save the boxed ReflectionClass trait-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the boxed ReflectionClass method-name array from the first stack argument - emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the boxed ReflectionClass method-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the boxed ReflectionClass property-name array from the second stack argument - emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // save the boxed ReflectionClass property-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 32]"); // load the boxed ReflectionClass method objects array from the third stack argument - emitter.instruction("mov QWORD PTR [rbp - 128], rax"); // save the boxed ReflectionClass method objects array - emitter.instruction("mov rax, QWORD PTR [rbp + 40]"); // load the boxed ReflectionClass property objects array from the fourth stack argument - emitter.instruction("mov QWORD PTR [rbp - 136], rax"); // save the boxed ReflectionClass property objects array - emitter.instruction("mov rax, QWORD PTR [rbp + 48]"); // load ReflectionClass modifier flags from the fifth stack argument - emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags - emitter.instruction("mov rax, QWORD PTR [rbp + 56]"); // load ReflectionClass getModifiers bitmask from the sixth stack argument - emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save ReflectionClass getModifiers bitmask - emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass - emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner - emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod - emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner - emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty - emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner - emitter.instruction("cmp rdi, 3"); // owner kind 3 means ReflectionClassConstant - emitter.instruction(&format!("je {}", class_constant_label)); // allocate a ReflectionClassConstant owner - emitter.instruction("cmp rdi, 4"); // owner kind 4 means ReflectionEnumUnitCase - emitter.instruction(&format!("je {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner - emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase - emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner - emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds + let parameter_label = "__elephc_eval_reflection_owner_new_parameter_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 144"); // reserve slots for inputs, object, metadata arrays, and name parts + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the Reflection owner kind + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array + emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // save the boxed ReflectionClass interface-name array + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // save the boxed ReflectionClass trait-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the boxed ReflectionClass method-name array from the first stack argument + emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the boxed ReflectionClass method-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the boxed ReflectionClass property-name array from the second stack argument + emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // save the boxed ReflectionClass property-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 32]"); // load the boxed ReflectionClass method objects array from the third stack argument + emitter.instruction("mov QWORD PTR [rbp - 128], rax"); // save the boxed ReflectionClass method objects array + emitter.instruction("mov rax, QWORD PTR [rbp + 40]"); // load the boxed ReflectionClass property objects array from the fourth stack argument + emitter.instruction("mov QWORD PTR [rbp - 136], rax"); // save the boxed ReflectionClass property objects array + emitter.instruction("mov rax, QWORD PTR [rbp + 48]"); // load ReflectionClass modifier flags from the fifth stack argument + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags + emitter.instruction("mov rax, QWORD PTR [rbp + 56]"); // load ReflectionClass getModifiers bitmask from the sixth stack argument + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save ReflectionClass getModifiers bitmask + emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp rdi, 3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("je {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp rdi, 4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("je {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner + emitter.instruction("cmp rdi, 6"); // owner kind 6 means ReflectionParameter + emitter.instruction(&format!("je {}", parameter_label)); // allocate a ReflectionParameter owner + emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body( emitter, class_label, @@ -440,18 +482,26 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO fail_label, box_label, ); + emit_x86_64_owner_kind_body( + emitter, + parameter_label, + &layouts.parameter, + true, + fail_label, + box_label, + ); emitter.label(box_label); - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload - emitter.instruction("xor esi, esi"); // object payloads do not use a high word - emitter.instruction("mov eax, 6"); // runtime tag 6 = object - emitter.instruction("call __rt_mixed_from_value"); // box the Reflection owner object for eval - emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("call __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing emitter.label(fail_label); - emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the boxed reflection owner to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reflection owner to Rust } /// Emits one ARM64 owner-kind allocation and slot-population body. @@ -465,15 +515,16 @@ fn emit_aarch64_owner_kind_body( ) { emitter.label(label); emit_alloc_reflection_owner_object_aarch64(emitter, layout); - emitter.instruction("str x0, [sp, #32]"); // save the unboxed Reflection owner object pointer + emitter.instruction("str x0, [sp, #32]"); // save the unboxed Reflection owner object pointer if set_name { emit_set_owner_name_property_aarch64(emitter, layout); } emit_set_owner_class_flags_property_aarch64(emitter, layout); emit_set_owner_member_flags_property_aarch64(emitter, layout); + emit_set_owner_parameter_property_aarch64(emitter, layout); emit_set_owner_metadata_arrays_property_aarch64(emitter, layout, fail_label); emit_set_owner_attrs_property_aarch64(emitter, layout, fail_label); - emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object + emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object } /// Emits one x86_64 owner-kind allocation and slot-population body. @@ -487,15 +538,16 @@ fn emit_x86_64_owner_kind_body( ) { emitter.label(label); emit_alloc_reflection_owner_object_x86_64(emitter, layout); - emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the unboxed Reflection owner object pointer + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the unboxed Reflection owner object pointer if set_name { emit_set_owner_name_property_x86_64(emitter, layout); } emit_set_owner_class_flags_property_x86_64(emitter, layout); emit_set_owner_member_flags_property_x86_64(emitter, layout); + emit_set_owner_parameter_property_x86_64(emitter, layout); emit_set_owner_metadata_arrays_property_x86_64(emitter, layout, fail_label); emit_set_owner_attrs_property_x86_64(emitter, layout, fail_label); - emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object + emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object } /// Allocates a zero-initialized ARM64 Reflection owner object payload. @@ -504,12 +556,12 @@ fn emit_alloc_reflection_owner_object_aarch64( layout: &ReflectionOwnerLayout, ) { let payload_size = 8 + layout.property_count * 16; - emitter.instruction(&format!("mov x0, #{}", payload_size)); // request Reflection owner object payload storage + emitter.instruction(&format!("mov x0, #{}", payload_size)); // request Reflection owner object payload storage abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #4"); // heap kind 4 marks the payload as an object - emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload - emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the Reflection owner class id - emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + emitter.instruction("mov x9, #4"); // heap kind 4 marks the payload as an object + emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero for index in 0..layout.property_count { let offset = 8 + index * 16; abi::emit_store_zero_to_address(emitter, "x0", offset); @@ -523,15 +575,15 @@ fn emit_alloc_reflection_owner_object_x86_64( layout: &ReflectionOwnerLayout, ) { let payload_size = 8 + layout.property_count * 16; - emitter.instruction(&format!("mov rax, {}", payload_size)); // request Reflection owner object payload storage + emitter.instruction(&format!("mov rax, {}", payload_size)); // request Reflection owner object payload storage abi::emit_call_label(emitter, "__rt_heap_alloc"); emitter.instruction(&format!( "mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4 )); // materialize the x86_64 object heap kind word - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload - emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id - emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero for index in 0..layout.property_count { let offset = 8 + index * 16; abi::emit_store_zero_to_address(emitter, "rax", offset); @@ -547,10 +599,10 @@ fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &Reflecti let Some(name_hi) = layout.name_hi else { return; }; - emitter.instruction("ldr x1, [sp, #8]"); // reload the reflected-name pointer for persistence - emitter.instruction("ldr x2, [sp, #16]"); // reload the reflected-name length for persistence - emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #8]"); // reload the reflected-name pointer for persistence + emitter.instruction("ldr x2, [sp, #16]"); // reload the reflected-name length for persistence + emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", name_lo); abi::emit_store_to_address(emitter, "x2", "x9", name_hi); let ( @@ -575,43 +627,43 @@ fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &Reflecti let found_label = "__elephc_eval_reflection_owner_name_scan_found"; let no_namespace_label = "__elephc_eval_reflection_owner_name_scan_none"; let store_parts_label = "__elephc_eval_reflection_owner_name_store_parts"; - emitter.instruction("ldr x3, [sp, #8]"); // reload the original reflected-name pointer for splitting - emitter.instruction("ldr x4, [sp, #16]"); // reload the original reflected-name length for splitting - emitter.instruction("mov x5, x4"); // start scanning from one byte past the final name byte - emitter.instruction(&format!("cbz x5, {}", no_namespace_label)); // empty names have no namespace component + emitter.instruction("ldr x3, [sp, #8]"); // reload the original reflected-name pointer for splitting + emitter.instruction("ldr x4, [sp, #16]"); // reload the original reflected-name length for splitting + emitter.instruction("mov x5, x4"); // start scanning from one byte past the final name byte + emitter.instruction(&format!("cbz x5, {}", no_namespace_label)); // empty names have no namespace component emitter.label(scan_loop_label); - emitter.instruction("sub x5, x5, #1"); // move the scan cursor to the previous byte - emitter.instruction("ldrb w6, [x3, x5]"); // read one reflected-name byte from the scan cursor - emitter.instruction("cmp w6, #92"); // compare against PHP namespace separator '\\' - emitter.instruction(&format!("b.eq {}", found_label)); // split at the final namespace separator - emitter.instruction(&format!("cbnz x5, {}", scan_loop_label)); // keep scanning until the first byte has been checked + emitter.instruction("sub x5, x5, #1"); // move the scan cursor to the previous byte + emitter.instruction("ldrb w6, [x3, x5]"); // read one reflected-name byte from the scan cursor + emitter.instruction("cmp w6, #92"); // compare against PHP namespace separator '\\' + emitter.instruction(&format!("b.eq {}", found_label)); // split at the final namespace separator + emitter.instruction(&format!("cbnz x5, {}", scan_loop_label)); // keep scanning until the first byte has been checked emitter.label(no_namespace_label); - emitter.instruction("str x3, [sp, #56]"); // short-name pointer is the original name pointer - emitter.instruction("str x4, [sp, #64]"); // short-name length is the full name length - emitter.instruction("str xzr, [sp, #72]"); // namespace length is zero for global names - emitter.instruction(&format!("b {}", store_parts_label)); // skip the namespaced split path + emitter.instruction("str x3, [sp, #56]"); // short-name pointer is the original name pointer + emitter.instruction("str x4, [sp, #64]"); // short-name length is the full name length + emitter.instruction("str xzr, [sp, #72]"); // namespace length is zero for global names + emitter.instruction(&format!("b {}", store_parts_label)); // skip the namespaced split path emitter.label(found_label); - emitter.instruction("add x6, x5, #1"); // compute the short-name byte offset after the separator - emitter.instruction("add x7, x3, x6"); // compute the short-name pointer - emitter.instruction("sub x8, x4, x6"); // compute the short-name length - emitter.instruction("str x7, [sp, #56]"); // save the short-name pointer across persistence calls - emitter.instruction("str x8, [sp, #64]"); // save the short-name length across persistence calls - emitter.instruction("str x5, [sp, #72]"); // namespace length is the separator offset + emitter.instruction("add x6, x5, #1"); // compute the short-name byte offset after the separator + emitter.instruction("add x7, x3, x6"); // compute the short-name pointer + emitter.instruction("sub x8, x4, x6"); // compute the short-name length + emitter.instruction("str x7, [sp, #56]"); // save the short-name pointer across persistence calls + emitter.instruction("str x8, [sp, #64]"); // save the short-name length across persistence calls + emitter.instruction("str x5, [sp, #72]"); // namespace length is the separator offset emitter.label(store_parts_label); - emitter.instruction("ldr x1, [sp, #8]"); // use the original name pointer for namespace persistence - emitter.instruction("ldr x2, [sp, #72]"); // reload the namespace byte length - emitter.instruction("bl __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #8]"); // use the original name pointer for namespace persistence + emitter.instruction("ldr x2, [sp, #72]"); // reload the namespace byte length + emitter.instruction("bl __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", namespace_name_lo); abi::emit_store_to_address(emitter, "x2", "x9", namespace_name_hi); - emitter.instruction("cmp x2, #0"); // detect whether a namespace component was present - emitter.instruction("cset x10, ne"); // materialize ReflectionClass::inNamespace() + emitter.instruction("cmp x2, #0"); // detect whether a namespace component was present + emitter.instruction("cset x10, ne"); // materialize ReflectionClass::inNamespace() abi::emit_store_to_address(emitter, "x10", "x9", in_namespace_lo); abi::emit_store_zero_to_address(emitter, "x9", in_namespace_hi); - emitter.instruction("ldr x1, [sp, #56]"); // reload the short-name pointer - emitter.instruction("ldr x2, [sp, #64]"); // reload the short-name byte length - emitter.instruction("bl __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #56]"); // reload the short-name pointer + emitter.instruction("ldr x2, [sp, #64]"); // reload the short-name byte length + emitter.instruction("bl __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", short_name_lo); abi::emit_store_to_address(emitter, "x2", "x9", short_name_hi); } @@ -624,10 +676,10 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio let Some(name_hi) = layout.name_hi else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the reflected-name pointer for persistence - emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the reflected-name length for persistence - emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the reflected-name pointer for persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the reflected-name length for persistence + emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", name_hi); let ( @@ -652,47 +704,47 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio let found_label = "__elephc_eval_reflection_owner_name_scan_found_x"; let no_namespace_label = "__elephc_eval_reflection_owner_name_scan_none_x"; let store_parts_label = "__elephc_eval_reflection_owner_name_store_parts_x"; - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the original reflected-name pointer for splitting - emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the original reflected-name length for splitting - emitter.instruction("mov r11, r9"); // start scanning from one byte past the final name byte - emitter.instruction("test r11, r11"); // check whether the reflected name is empty - emitter.instruction(&format!("jz {}", no_namespace_label)); // empty names have no namespace component + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the original reflected-name pointer for splitting + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the original reflected-name length for splitting + emitter.instruction("mov r11, r9"); // start scanning from one byte past the final name byte + emitter.instruction("test r11, r11"); // check whether the reflected name is empty + emitter.instruction(&format!("jz {}", no_namespace_label)); // empty names have no namespace component emitter.label(scan_loop_label); - emitter.instruction("sub r11, 1"); // move the scan cursor to the previous byte - emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // read one reflected-name byte from the scan cursor - emitter.instruction("cmp eax, 92"); // compare against PHP namespace separator '\\' - emitter.instruction(&format!("je {}", found_label)); // split at the final namespace separator - emitter.instruction("test r11, r11"); // check whether the first byte has been examined - emitter.instruction(&format!("jnz {}", scan_loop_label)); // keep scanning until the first byte has been checked + emitter.instruction("sub r11, 1"); // move the scan cursor to the previous byte + emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // read one reflected-name byte from the scan cursor + emitter.instruction("cmp eax, 92"); // compare against PHP namespace separator '\\' + emitter.instruction(&format!("je {}", found_label)); // split at the final namespace separator + emitter.instruction("test r11, r11"); // check whether the first byte has been examined + emitter.instruction(&format!("jnz {}", scan_loop_label)); // keep scanning until the first byte has been checked emitter.label(no_namespace_label); - emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // short-name pointer is the original name pointer - emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // short-name length is the full name length - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // namespace length is zero for global names - emitter.instruction(&format!("jmp {}", store_parts_label)); // skip the namespaced split path + emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // short-name pointer is the original name pointer + emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // short-name length is the full name length + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // namespace length is zero for global names + emitter.instruction(&format!("jmp {}", store_parts_label)); // skip the namespaced split path emitter.label(found_label); - emitter.instruction("lea rax, [r11 + 1]"); // compute the short-name byte offset after the separator - emitter.instruction("lea r10, [r8 + rax]"); // compute the short-name pointer - emitter.instruction("mov rcx, r9"); // copy the full name length before subtracting the prefix - emitter.instruction("sub rcx, rax"); // compute the short-name length - emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the short-name pointer across persistence calls - emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // save the short-name length across persistence calls - emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // namespace length is the separator offset + emitter.instruction("lea rax, [r11 + 1]"); // compute the short-name byte offset after the separator + emitter.instruction("lea r10, [r8 + rax]"); // compute the short-name pointer + emitter.instruction("mov rcx, r9"); // copy the full name length before subtracting the prefix + emitter.instruction("sub rcx, rax"); // compute the short-name length + emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the short-name pointer across persistence calls + emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // save the short-name length across persistence calls + emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // namespace length is the separator offset emitter.label(store_parts_label); - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // use the original name pointer for namespace persistence - emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the namespace byte length - emitter.instruction("call __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // use the original name pointer for namespace persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the namespace byte length + emitter.instruction("call __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", namespace_name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", namespace_name_hi); - emitter.instruction("test rdx, rdx"); // detect whether a namespace component was present - emitter.instruction("setne al"); // materialize ReflectionClass::inNamespace() - emitter.instruction("movzx eax, al"); // widen the namespace boolean to a full word + emitter.instruction("test rdx, rdx"); // detect whether a namespace component was present + emitter.instruction("setne al"); // materialize ReflectionClass::inNamespace() + emitter.instruction("movzx eax, al"); // widen the namespace boolean to a full word abi::emit_store_to_address(emitter, "rax", "r10", in_namespace_lo); abi::emit_store_zero_to_address(emitter, "r10", in_namespace_hi); - emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the short-name pointer - emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // reload the short-name byte length - emitter.instruction("call __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the short-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // reload the short-name byte length + emitter.instruction("call __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", short_name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", short_name_hi); } @@ -736,32 +788,32 @@ fn emit_set_owner_class_flags_property_aarch64( else { return; }; - emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer - emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); - emitter.instruction("lsr x10, x11, #1"); // move the abstract-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean + emitter.instruction("lsr x10, x11, #1"); // move the abstract-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); - emitter.instruction("lsr x10, x11, #2"); // move the interface bit into position - emitter.instruction("and x10, x10, #1"); // extract the interface flag as a boolean + emitter.instruction("lsr x10, x11, #2"); // move the interface bit into position + emitter.instruction("and x10, x10, #1"); // extract the interface flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_interface_lo); abi::emit_store_zero_to_address(emitter, "x9", is_interface_hi); - emitter.instruction("lsr x10, x11, #3"); // move the trait bit into position - emitter.instruction("and x10, x10, #1"); // extract the trait flag as a boolean + emitter.instruction("lsr x10, x11, #3"); // move the trait bit into position + emitter.instruction("and x10, x10, #1"); // extract the trait flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_trait_lo); abi::emit_store_zero_to_address(emitter, "x9", is_trait_hi); - emitter.instruction("lsr x10, x11, #4"); // move the enum bit into position - emitter.instruction("and x10, x10, #1"); // extract the enum flag as a boolean + emitter.instruction("lsr x10, x11, #4"); // move the enum bit into position + emitter.instruction("and x10, x10, #1"); // extract the enum flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_enum_lo); abi::emit_store_zero_to_address(emitter, "x9", is_enum_hi); emitter.instruction("lsr x10, x11, #5"); // move the readonly-class bit into position emitter.instruction("and x10, x10, #1"); // extract the readonly-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); - emitter.instruction("ldr x10, [sp, #96]"); // reload PHP ReflectionClass::getModifiers() bitmask + emitter.instruction("ldr x10, [sp, #96]"); // reload PHP ReflectionClass::getModifiers() bitmask abi::emit_store_to_address(emitter, "x10", "x9", modifiers_lo); abi::emit_store_zero_to_address(emitter, "x9", modifiers_hi); } @@ -805,30 +857,30 @@ fn emit_set_owner_class_flags_property_x86_64( else { return; }; - emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer - emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit - emitter.instruction("and rax, 1"); // extract the final-class flag as a boolean + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit + emitter.instruction("and rax, 1"); // extract the final-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit - emitter.instruction("shr rax, 1"); // move the abstract-class bit into position - emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit + emitter.instruction("shr rax, 1"); // move the abstract-class bit into position + emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the interface bit - emitter.instruction("shr rax, 2"); // move the interface bit into position - emitter.instruction("and rax, 1"); // extract the interface flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the interface bit + emitter.instruction("shr rax, 2"); // move the interface bit into position + emitter.instruction("and rax, 1"); // extract the interface flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_interface_lo); abi::emit_store_zero_to_address(emitter, "r10", is_interface_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the trait bit - emitter.instruction("shr rax, 3"); // move the trait bit into position - emitter.instruction("and rax, 1"); // extract the trait flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the trait bit + emitter.instruction("shr rax, 3"); // move the trait bit into position + emitter.instruction("and rax, 1"); // extract the trait flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_trait_lo); abi::emit_store_zero_to_address(emitter, "r10", is_trait_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the enum bit - emitter.instruction("shr rax, 4"); // move the enum bit into position - emitter.instruction("and rax, 1"); // extract the enum flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the enum bit + emitter.instruction("shr rax, 4"); // move the enum bit into position + emitter.instruction("and rax, 1"); // extract the enum flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_enum_lo); abi::emit_store_zero_to_address(emitter, "r10", is_enum_hi); emitter.instruction("mov rax, r11"); // copy flags before extracting the readonly-class bit @@ -836,7 +888,7 @@ fn emit_set_owner_class_flags_property_x86_64( emitter.instruction("and rax, 1"); // extract the readonly-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); - emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP ReflectionClass::getModifiers() bitmask + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP ReflectionClass::getModifiers() bitmask abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); } @@ -971,84 +1023,140 @@ fn emit_set_owner_member_flags_property_x86_64( abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); } -/// Stores incoming ARM64 ReflectionClass metadata name arrays. -fn emit_set_owner_metadata_arrays_property_aarch64( +/// Stores incoming ARM64 ReflectionParameter position and predicate flags. +fn emit_set_owner_parameter_property_aarch64( emitter: &mut Emitter, layout: &ReflectionOwnerLayout, - fail_label: &str, ) { let ( - Some(interface_names_lo), - Some(interface_names_hi), - Some(trait_names_lo), - Some(trait_names_hi), - Some(method_names_lo), - Some(method_names_hi), - Some(property_names_lo), - Some(property_names_hi), - Some(method_objects_lo), - Some(method_objects_hi), - Some(property_objects_lo), - Some(property_objects_hi), + Some(position_lo), + Some(position_hi), + Some(is_optional_lo), + Some(is_optional_hi), + Some(is_variadic_lo), + Some(is_variadic_hi), + Some(is_passed_by_reference_lo), + Some(is_passed_by_reference_hi), + Some(has_type_lo), + Some(has_type_hi), ) = ( - layout.interface_names_lo, - layout.interface_names_hi, - layout.trait_names_lo, - layout.trait_names_hi, - layout.method_names_lo, - layout.method_names_hi, - layout.property_names_lo, - layout.property_names_hi, - layout.method_objects_lo, - layout.method_objects_hi, - layout.property_objects_lo, - layout.property_objects_hi, + layout.position_lo, + layout.position_hi, + layout.is_optional_lo, + layout.is_optional_hi, + layout.is_variadic_lo, + layout.is_variadic_hi, + layout.is_passed_by_reference_lo, + layout.is_passed_by_reference_hi, + layout.has_type_lo, + layout.has_type_hi, ) else { return; }; - emit_set_owner_metadata_array_slot_aarch64( - emitter, - 80, - interface_names_lo, - interface_names_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_aarch64( - emitter, - 88, - trait_names_lo, - trait_names_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_aarch64( - emitter, - 104, - method_names_lo, - method_names_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_aarch64( - emitter, - 112, - property_names_lo, - property_names_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_aarch64( - emitter, - 120, - method_objects_lo, - method_objects_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_aarch64( - emitter, - 128, - property_objects_lo, - property_objects_hi, - fail_label, - ); + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionParameter predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + emitter.instruction("ldr x10, [sp, #96]"); // reload the zero-based parameter position + abi::emit_store_to_address(emitter, "x10", "x9", position_lo); + abi::emit_store_zero_to_address(emitter, "x9", position_hi); + emitter.instruction("and x10, x11, #1"); // extract the optional-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_optional_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_optional_hi); + emitter.instruction("lsr x10, x11, #1"); // move the variadic-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the variadic-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_variadic_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_variadic_hi); + emitter.instruction("lsr x10, x11, #2"); // move the by-reference-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the by-reference-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_passed_by_reference_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_passed_by_reference_hi); + emitter.instruction("lsr x10, x11, #3"); // move the typed-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the typed-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", has_type_lo); + abi::emit_store_zero_to_address(emitter, "x9", has_type_hi); +} + +/// Stores incoming x86_64 ReflectionParameter position and predicate flags. +fn emit_set_owner_parameter_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let ( + Some(position_lo), + Some(position_hi), + Some(is_optional_lo), + Some(is_optional_hi), + Some(is_variadic_lo), + Some(is_variadic_hi), + Some(is_passed_by_reference_lo), + Some(is_passed_by_reference_hi), + Some(has_type_lo), + Some(has_type_hi), + ) = ( + layout.position_lo, + layout.position_hi, + layout.is_optional_lo, + layout.is_optional_hi, + layout.is_variadic_lo, + layout.is_variadic_hi, + layout.is_passed_by_reference_lo, + layout.is_passed_by_reference_hi, + layout.has_type_lo, + layout.has_type_hi, + ) + else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionParameter predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload the zero-based parameter position + abi::emit_store_to_address(emitter, "rax", "r10", position_lo); + abi::emit_store_zero_to_address(emitter, "r10", position_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the optional bit + emitter.instruction("and rax, 1"); // extract the optional-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_optional_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_optional_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the variadic bit + emitter.instruction("shr rax, 1"); // move the variadic-parameter bit into position + emitter.instruction("and rax, 1"); // extract the variadic-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_variadic_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_variadic_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the by-reference bit + emitter.instruction("shr rax, 2"); // move the by-reference-parameter bit into position + emitter.instruction("and rax, 1"); // extract the by-reference-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_passed_by_reference_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_passed_by_reference_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the typed bit + emitter.instruction("shr rax, 3"); // move the typed-parameter bit into position + emitter.instruction("and rax, 1"); // extract the typed-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", has_type_lo); + abi::emit_store_zero_to_address(emitter, "r10", has_type_hi); +} + +/// Stores incoming ARM64 ReflectionClass metadata name arrays. +fn emit_set_owner_metadata_arrays_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + if let (Some(low), Some(high)) = (layout.interface_names_lo, layout.interface_names_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 80, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.trait_names_lo, layout.trait_names_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 88, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.method_names_lo, layout.method_names_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 104, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.property_names_lo, layout.property_names_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 112, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.method_objects_lo, layout.method_objects_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 120, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.property_objects_lo, layout.property_objects_hi) { + emit_set_owner_metadata_array_slot_aarch64(emitter, 128, low, high, fail_label); + } } /// Stores one retained ARM64 boxed metadata-name array into a ReflectionClass slot. @@ -1059,16 +1167,16 @@ fn emit_set_owner_metadata_array_slot_aarch64( high_offset: usize, fail_label: &str, ) { - emitter.instruction(&format!("ldr x0, [sp, #{}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null metadata-name arrays - emitter.instruction("bl __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer - emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array metadata-name metadata - emitter.instruction("str x1, [sp, #40]"); // save the unboxed metadata-name array across incref - emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register - emitter.instruction("bl __rt_incref"); // retain the metadata-name array for ReflectionClass storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained metadata-name array payload - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction(&format!("ldr x0, [sp, #{}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null metadata-name arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array metadata-name metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed metadata-name array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the metadata-name array for ReflectionClass storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained metadata-name array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", low_offset); abi::emit_load_int_immediate(emitter, "x10", 4); abi::emit_store_to_address(emitter, "x10", "x9", high_offset); @@ -1080,78 +1188,24 @@ fn emit_set_owner_metadata_arrays_property_x86_64( layout: &ReflectionOwnerLayout, fail_label: &str, ) { - let ( - Some(interface_names_lo), - Some(interface_names_hi), - Some(trait_names_lo), - Some(trait_names_hi), - Some(method_names_lo), - Some(method_names_hi), - Some(property_names_lo), - Some(property_names_hi), - Some(method_objects_lo), - Some(method_objects_hi), - Some(property_objects_lo), - Some(property_objects_hi), - ) = ( - layout.interface_names_lo, - layout.interface_names_hi, - layout.trait_names_lo, - layout.trait_names_hi, - layout.method_names_lo, - layout.method_names_hi, - layout.property_names_lo, - layout.property_names_hi, - layout.method_objects_lo, - layout.method_objects_hi, - layout.property_objects_lo, - layout.property_objects_hi, - ) - else { - return; - }; - emit_set_owner_metadata_array_slot_x86_64( - emitter, - -88, - interface_names_lo, - interface_names_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_x86_64( - emitter, - -96, - trait_names_lo, - trait_names_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_x86_64( - emitter, - -112, - method_names_lo, - method_names_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_x86_64( - emitter, - -120, - property_names_lo, - property_names_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_x86_64( - emitter, - -128, - method_objects_lo, - method_objects_hi, - fail_label, - ); - emit_set_owner_metadata_array_slot_x86_64( - emitter, - -136, - property_objects_lo, - property_objects_hi, - fail_label, - ); + if let (Some(low), Some(high)) = (layout.interface_names_lo, layout.interface_names_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -88, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.trait_names_lo, layout.trait_names_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -96, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.method_names_lo, layout.method_names_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -112, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.property_names_lo, layout.property_names_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -120, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.method_objects_lo, layout.method_objects_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -128, low, high, fail_label); + } + if let (Some(low), Some(high)) = (layout.property_objects_lo, layout.property_objects_hi) { + emit_set_owner_metadata_array_slot_x86_64(emitter, -136, low, high, fail_label); + } } /// Stores one retained x86_64 boxed metadata-name array into a ReflectionClass slot. @@ -1167,17 +1221,17 @@ fn emit_set_owner_metadata_array_slot_x86_64( } else { format!("+ {}", boxed_slot) }; - emitter.instruction(&format!("mov rax, QWORD PTR [rbp {}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array - emitter.instruction("test rax, rax"); // check whether the boxed metadata-name array is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null metadata-name arrays - emitter.instruction("call __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer - emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("jne {}", fail_label)); // reject non-array metadata-name metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed metadata-name array across incref - emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register - emitter.instruction("call __rt_incref"); // retain the metadata-name array for ReflectionClass storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained metadata-name array payload - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction(&format!("mov rax, QWORD PTR [rbp {}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array + emitter.instruction("test rax, rax"); // check whether the boxed metadata-name array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null metadata-name arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array metadata-name metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed metadata-name array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the metadata-name array for ReflectionClass storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained metadata-name array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", low_offset); abi::emit_load_int_immediate(emitter, "r11", 4); abi::emit_store_to_address(emitter, "r11", "r10", high_offset); @@ -1189,16 +1243,16 @@ fn emit_set_owner_attrs_property_aarch64( layout: &ReflectionOwnerLayout, fail_label: &str, ) { - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed ReflectionAttribute array - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null attribute arrays - emitter.instruction("bl __rt_mixed_unbox"); // expose the attribute array tag and payload pointer - emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array attribute metadata - emitter.instruction("str x1, [sp, #40]"); // save the unboxed attribute array across incref - emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register - emitter.instruction("bl __rt_incref"); // retain the attribute array for Reflection owner storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained attribute array payload - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed ReflectionAttribute array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed attribute array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained attribute array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", layout.attrs_lo); abi::emit_load_int_immediate(emitter, "x10", 4); abi::emit_store_to_address(emitter, "x10", "x9", layout.attrs_hi); @@ -1210,17 +1264,17 @@ fn emit_set_owner_attrs_property_x86_64( layout: &ReflectionOwnerLayout, fail_label: &str, ) { - emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed ReflectionAttribute array - emitter.instruction("test rax, rax"); // check whether the boxed attribute array is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null attribute arrays - emitter.instruction("call __rt_mixed_unbox"); // expose the attribute array tag and payload pointer - emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("jne {}", fail_label)); // reject non-array attribute metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed attribute array across incref - emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register - emitter.instruction("call __rt_incref"); // retain the attribute array for Reflection owner storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained attribute array payload - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed ReflectionAttribute array + emitter.instruction("test rax, rax"); // check whether the boxed attribute array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed attribute array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained attribute array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", layout.attrs_lo); abi::emit_load_int_immediate(emitter, "r11", 4); abi::emit_store_to_address(emitter, "r11", "r10", layout.attrs_hi); diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 04eb2d4454..d9f9e1e742 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1322,6 +1322,7 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", "ReflectionMethod", + "ReflectionParameter", "ReflectionProperty", "RegexIterator", "RuntimeException", diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 13a4efd788..c75f2d40a5 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -17,7 +17,7 @@ use crate::codegen::{CodegenIrError, Result}; use crate::ir::{Immediate, Instruction, Op, ValueDef, ValueId}; use crate::names::php_symbol_key; use crate::parser::ast::Visibility; -use crate::types::{AttrArgEntry, PhpType}; +use crate::types::{AttrArgEntry, FunctionSig, PhpType}; use super::super::super::context::FunctionContext; @@ -33,6 +33,7 @@ struct ReflectionOwnerMetadata { method_members: Vec, property_members: Vec, constructor_member: Option, + parameter_members: Vec, is_final: bool, is_abstract: bool, is_interface: bool, @@ -50,6 +51,18 @@ struct ReflectionListedMember { attr_names: Vec, attr_args: Vec>>, flags: ReflectionMemberFlags, + parameters: Vec, +} + +/// Metadata for one object returned by `ReflectionMethod::getParameters()`. +#[derive(Clone)] +struct ReflectionParameterMember { + name: String, + position: i64, + is_optional: bool, + is_variadic: bool, + is_passed_by_reference: bool, + has_type: bool, } /// Boolean metadata exposed by ReflectionMethod and ReflectionProperty predicates. @@ -157,6 +170,14 @@ pub(super) fn lower_reflection_owner_new( emit_reflection_bool_property(ctx, "__is_readonly", metadata.is_readonly)?; emit_reflection_int_property_by_name(ctx, "__modifiers", metadata.modifiers)?; } + if class_name == "ReflectionMethod" { + emit_reflection_parameter_array_property_by_name( + ctx, + class_name, + "__parameters", + &metadata.parameter_members, + )?; + } emit_reflection_member_flag_properties(ctx, class_name, metadata.member_flags)?; let result = inst .result @@ -597,6 +618,7 @@ fn reflection_class_metadata( method_members, property_members, constructor_member, + parameter_members: Vec::new(), is_final: info.is_final, is_abstract: info.is_abstract, is_interface: false, @@ -661,10 +683,11 @@ fn reflection_method_metadata( let method_key = php_symbol_key(&method_name); Ok(resolve_reflection_class(ctx, &reflected_class) .and_then(|(_, info)| { + let member = reflection_class_method_member(info, &method_key)?; Some(ReflectionOwnerMetadata { reflected_name: Some(method_name.clone()), - attr_names: info.method_attribute_names.get(&method_key)?.clone(), - attr_args: info.method_attribute_args.get(&method_key)?.clone(), + attr_names: member.attr_names, + attr_args: member.attr_args, interface_names: Vec::new(), trait_names: Vec::new(), method_names: Vec::new(), @@ -672,6 +695,7 @@ fn reflection_method_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parameter_members: member.parameters, is_final: false, is_abstract: false, is_interface: false, @@ -679,7 +703,7 @@ fn reflection_method_metadata( is_enum: false, is_readonly: false, modifiers: 0, - member_flags: reflection_method_member_flags(info, &method_key)?, + member_flags: member.flags, }) }) .unwrap_or_else(empty_reflection_metadata)) @@ -711,6 +735,7 @@ fn reflection_property_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parameter_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -751,6 +776,7 @@ fn reflection_class_constant_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parameter_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -785,6 +811,7 @@ fn reflection_class_constant_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parameter_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -826,6 +853,7 @@ fn reflection_enum_case_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parameter_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -1065,6 +1093,10 @@ fn reflection_class_method_member( method_name: &str, ) -> Option { let method_key = php_symbol_key(method_name); + let sig = info + .methods + .get(&method_key) + .or_else(|| info.static_methods.get(&method_key))?; Some(ReflectionListedMember { name: method_key.clone(), attr_names: info @@ -1078,6 +1110,7 @@ fn reflection_class_method_member( .cloned() .unwrap_or_default(), flags: reflection_method_member_flags(info, &method_key)?, + parameters: reflection_parameter_members(sig), }) } @@ -1119,6 +1152,7 @@ fn reflection_class_property_member( .cloned() .unwrap_or_default(), flags, + parameters: Vec::new(), }) } @@ -1140,6 +1174,7 @@ fn default_method_members( false, is_interface, ), + parameters: Vec::new(), }) .collect() } @@ -1161,6 +1196,31 @@ fn default_property_members( false, is_interface, ), + parameters: Vec::new(), + }) + .collect() +} + +/// Builds reflected parameter metadata from a method/function signature. +fn reflection_parameter_members(sig: &FunctionSig) -> Vec { + sig.params + .iter() + .enumerate() + .map(|(index, (name, _))| { + let is_variadic = sig.variadic.as_deref() == Some(name.as_str()); + ReflectionParameterMember { + name: name.clone(), + position: index as i64, + is_optional: is_variadic + || sig + .defaults + .get(index) + .map(|default| default.is_some()) + .unwrap_or(false), + is_variadic, + is_passed_by_reference: sig.ref_params.get(index).copied().unwrap_or(false), + has_type: sig.declared_params.get(index).copied().unwrap_or(false), + } }) .collect() } @@ -1296,6 +1356,7 @@ fn class_like_reflection_metadata( method_members, property_members, constructor_member, + parameter_members: Vec::new(), is_final: false, is_abstract: false, is_interface, @@ -1348,6 +1409,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parameter_members: Vec::new(), is_final: false, is_abstract: false, is_interface: false, @@ -1612,6 +1674,44 @@ fn emit_reflection_constructor_property( Ok(()) } +/// Replaces a ReflectionMethod private array slot with ReflectionParameter objects. +fn emit_reflection_parameter_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + parameters: &[ReflectionParameterMember], +) -> Result<()> { + if parameters.is_empty() { + return Ok(()); + } + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_parameter_array(ctx, parameters)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + /// Allocates an indexed array of populated ReflectionMethod/ReflectionProperty objects. fn emit_reflection_member_array( ctx: &mut FunctionContext<'_>, @@ -1635,6 +1735,28 @@ fn emit_reflection_member_array( Ok(()) } +/// Allocates an indexed array of populated ReflectionParameter objects. +fn emit_reflection_parameter_array( + ctx: &mut FunctionContext<'_>, + parameters: &[ReflectionParameterMember], +) -> Result<()> { + emit_reflection_indexed_array(ctx, parameters.len().max(1), 8); + crate::codegen::emit_array_value_type_stamp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &PhpType::Object("ReflectionParameter".to_string()), + ); + + for parameter in parameters { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_parameter_object(ctx, parameter)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_append_reflection_member_object(ctx); + } + + Ok(()) +} + /// Allocates and populates one ReflectionMethod/ReflectionProperty object. fn emit_reflection_member_object( ctx: &mut FunctionContext<'_>, @@ -1672,10 +1794,81 @@ fn emit_reflection_member_object( &member.attr_names, &member.attr_args, )?; + if member_class_name == "ReflectionMethod" { + emit_reflection_parameter_array_property_by_name( + ctx, + member_class_name, + "__parameters", + &member.parameters, + )?; + } emit_reflection_member_flag_properties(ctx, member_class_name, member.flags)?; Ok(()) } +/// Allocates and populates one ReflectionParameter object. +fn emit_reflection_parameter_object( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = + ctx.module.class_infos.get("ReflectionParameter").ok_or_else(|| { + CodegenIrError::unsupported("unknown class ReflectionParameter") + })?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + )?; + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let name_offset = reflection_property_offset(class_info, "__name")?; + emit_reflection_string_property(ctx, ¶meter.name, name_offset, name_offset + 8); + emit_reflection_owner_int_property( + ctx, + "ReflectionParameter", + "__position", + parameter.position, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__optional", + parameter.is_optional, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__variadic", + parameter.is_variadic, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_passed_by_reference", + parameter.is_passed_by_reference, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__has_type", + parameter.has_type, + )?; + Ok(()) +} + /// Allocates an indexed array for static reflection metadata. fn emit_reflection_indexed_array( ctx: &mut FunctionContext<'_>, @@ -1833,11 +2026,21 @@ fn emit_reflection_int_property_by_name( ctx: &mut FunctionContext<'_>, property_name: &str, value: i64, +) -> Result<()> { + emit_reflection_owner_int_property(ctx, "ReflectionClass", property_name, value) +} + +/// Stores one integer property on the current Reflection owner object result. +fn emit_reflection_owner_int_property( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + value: i64, ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; let high_offset = low_offset + 8; diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 2a5201018f..38fc813db9 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -750,6 +750,7 @@ fn lower_builtin_reflection_methods( "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", "ReflectionMethod", + "ReflectionParameter", "ReflectionProperty", "ReflectionFunction", "ReflectionParameter", @@ -1051,6 +1052,7 @@ fn known_dynamic_new_builtin_class_name(class_name: &str) -> bool { | "reflectionattribute" | "reflectionclass" | "reflectionmethod" + | "reflectionparameter" | "reflectionproperty" | "regexiterator" | "runtimeexception" diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index aa88528f61..7af077cdb1 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -260,6 +260,11 @@ fn bool_type() -> TypeExpr { /// Returns a private parameterless `__construct` method for `ReflectionAttribute`. fn builtin_reflection_attribute_constructor_method() -> ClassMethod { + builtin_reflection_private_constructor_method() +} + +/// Returns a private parameterless `__construct` for internally materialized reflection objects. +fn builtin_reflection_private_constructor_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); ClassMethod { name: "__construct".to_string(), @@ -533,6 +538,12 @@ fn builtin_reflection_parameter() -> FlattenedClass { Some(TypeExpr::Bool), bool_lit(false), ), + builtin_property( + "__is_passed_by_reference", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), builtin_property("__has_type", Visibility::Private, Some(TypeExpr::Bool), bool_lit(false)), builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), ], @@ -541,6 +552,11 @@ fn builtin_reflection_parameter() -> FlattenedClass { builtin_reflection_slot_getter("getPosition", "__position", TypeExpr::Int), builtin_reflection_slot_getter("isOptional", "__optional", TypeExpr::Bool), builtin_reflection_slot_getter("isVariadic", "__variadic", TypeExpr::Bool), + builtin_reflection_slot_getter( + "isPassedByReference", + "__is_passed_by_reference", + TypeExpr::Bool, + ), builtin_reflection_slot_getter("hasType", "__has_type", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), ], @@ -1017,6 +1033,19 @@ fn builtin_reflection_owner_class( methods.push(builtin_reflection_class_string_method("getName", "__name")); } add_reflection_member_flag_methods(name, &mut properties, &mut methods); + if name == "ReflectionMethod" { + properties.push(builtin_property( + "__parameters", + Visibility::Private, + Some(object_array_type("ReflectionParameter")), + empty_array(), + )); + methods.push(builtin_reflection_class_array_method( + "getParameters", + "__parameters", + object_array_type("ReflectionParameter"), + )); + } properties.push(builtin_property( "__attrs", Visibility::Private, @@ -1163,6 +1192,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionClass", "ReflectionMethod", "ReflectionProperty", + "ReflectionParameter", "ReflectionClassConstant", "ReflectionEnumUnitCase", "ReflectionEnumBackedCase", @@ -1176,6 +1206,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionClass" | "ReflectionMethod" | "ReflectionProperty" + | "ReflectionParameter" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" @@ -1247,6 +1278,26 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Bool; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionParameter".to_string(), + ))); + } + } + if class_name == "ReflectionParameter" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPosition")) { + sig.return_type = PhpType::Int; + } + for method_name in [ + "isoptional", + "isvariadic", + "ispassedbyreference", + "hastype", + ] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 670187636c..f4c081b4a1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5868,6 +5868,34 @@ foreach ($properties as $property) { assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); } +/// Verifies eval ReflectionMethod materializes ReflectionParameter objects through the bridge. +#[test] +fn test_eval_reflection_method_lists_parameters() { + let out = compile_and_run_capture( + r#"getParameters(); +echo count($params) . ":"; +foreach ($params as $param) { + echo $param->getName() . "@" . $param->getPosition(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isVariadic() ? "V" : "v"; + echo $param->isPassedByReference() ? "R" : "b"; + echo $param->hasType() ? "T" : "t"; + echo "|"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:first@0rvbt|second@1rvbt|"); +} + /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. #[test] fn test_eval_reflection_class_new_instance_constructs_eval_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 3b08a866a5..9405b16232 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1587,6 +1587,64 @@ foreach ($properties as $property) { assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); } +/// Verifies that `ReflectionMethod::getParameters()` returns populated +/// ReflectionParameter objects for typed, by-reference, defaulted, and variadic parameters. +#[test] +fn test_reflection_method_get_parameters_returns_parameter_metadata() { + let out = compile_and_run_capture( + r##"getParameters(); +echo count($params) . ":"; +foreach ($params as $param) { + echo $param->getName() . "#" . $param->getPosition(); + echo ($param->hasType() ? "T" : "t"); + echo ($param->isOptional() ? "O" : "R"); + echo ($param->isPassedByReference() ? "B" : "b"); + echo ($param->isVariadic() ? "V" : "v"); + echo "|"; +} +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "4:id#0TRbv|name#1tRBv|mode#2TObv|rest#3tObV|"); +} + +/// Verifies that ReflectionMethod objects returned from `ReflectionClass::getMethods()` +/// carry the same parameter metadata as directly constructed method reflectors. +#[test] +fn test_reflection_class_get_methods_preserves_parameter_metadata() { + let out = compile_and_run_capture( + r##"getMethods(); +foreach ($methods as $method) { + if ($method->getName() === "listed") { + $params = $method->getParameters(); + echo count($params) . ":"; + echo $params[0]->getName() . ($params[0]->hasType() ? "T" : "t"); + echo ":"; + echo $params[1]->getName() . ($params[1]->isOptional() ? "O" : "R"); + } +} +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:firstT:secondO"); +} + /// Verifies that `ReflectionClass::getConstructor()` returns a ReflectionMethod /// for direct, inherited, interface, and trait constructors, and null otherwise. #[test] diff --git a/tests/error_tests/classes_traits.rs b/tests/error_tests/classes_traits.rs index 29ee5a8079..e26ff161ed 100644 --- a/tests/error_tests/classes_traits.rs +++ b/tests/error_tests/classes_traits.rs @@ -547,6 +547,16 @@ fn test_error_reflection_attribute_constructor_is_private() { ); } +/// Verifies that `new ReflectionParameter()` reports "Cannot access private +/// constructor: ReflectionParameter::__construct". +#[test] +fn test_error_reflection_parameter_constructor_is_private() { + expect_error( + " Date: Fri, 19 Jun 2026 02:23:04 +0200 Subject: [PATCH 0373/1208] Track eval method parameter types for reflection --- crates/elephc-eval/src/eval_ir.rs | 28 ++++++++ .../elephc-eval/src/interpreter/reflection.rs | 22 ++++-- .../tests/builtins_class_metadata.rs | 4 +- crates/elephc-eval/src/parser/statements.rs | 67 +++++++++++++++++-- .../src/parser/tests/classes_errors.rs | 23 +++++++ docs/php/eval.md | 13 ++-- tests/codegen/eval.rs | 4 +- 7 files changed, 141 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 1f7647fa51..034f722254 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -527,15 +527,18 @@ pub struct EvalInterfaceMethod { name: String, attributes: Vec, params: Vec, + parameter_has_types: Vec, } impl EvalInterfaceMethod { /// Creates one dynamic eval interface method signature. pub fn new(name: impl Into, params: Vec) -> Self { + let parameter_has_types = vec![false; params.len()]; Self { name: name.into(), attributes: Vec::new(), params, + parameter_has_types, } } @@ -545,6 +548,12 @@ impl EvalInterfaceMethod { self } + /// Returns a copy of this interface method with parameter type-presence flags. + pub fn with_parameter_type_flags(mut self, parameter_has_types: Vec) -> Self { + self.parameter_has_types = parameter_has_types; + self + } + /// Returns the PHP-visible method name. pub fn name(&self) -> &str { &self.name @@ -559,6 +568,11 @@ impl EvalInterfaceMethod { pub fn params(&self) -> &[String] { &self.params } + + /// Returns source-order flags for whether each parameter declared a type. + pub fn parameter_has_types(&self) -> &[bool] { + &self.parameter_has_types + } } /// Runtime class declared by an eval fragment. @@ -1210,6 +1224,7 @@ pub struct EvalClassMethod { is_abstract: bool, is_final: bool, params: Vec, + parameter_has_types: Vec, body: Vec, } @@ -1248,6 +1263,7 @@ impl EvalClassMethod { params: Vec, body: Vec, ) -> Self { + let parameter_has_types = vec![false; params.len()]; Self { name: name.into(), attributes: Vec::new(), @@ -1256,6 +1272,7 @@ impl EvalClassMethod { is_abstract, is_final, params, + parameter_has_types, body, } } @@ -1271,6 +1288,12 @@ impl EvalClassMethod { self } + /// Returns a copy of this method with source-order parameter type-presence flags. + pub fn with_parameter_type_flags(mut self, parameter_has_types: Vec) -> Self { + self.parameter_has_types = parameter_has_types; + self + } + /// Returns attributes declared directly on this class method. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes @@ -1315,6 +1338,11 @@ impl EvalClassMethod { &self.params } + /// Returns source-order flags for whether each parameter declared a type. + pub fn parameter_has_types(&self) -> &[bool] { + &self.parameter_has_types + } + /// Returns the dynamic EvalIR statements that form the method body. pub fn body(&self) -> &[EvalStmt] { &self.body diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index e05a79ad1d..7d17880681 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -605,7 +605,10 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), - parameters: eval_reflection_parameters_from_names(method.params()), + parameters: eval_reflection_parameters_from_names_and_type_flags( + method.params(), + method.parameter_has_types(), + ), }); } if context.has_interface(class_name) { @@ -619,7 +622,10 @@ fn eval_reflection_method_metadata( is_static: false, is_final: false, is_abstract: true, - parameters: eval_reflection_parameters_from_names(method.params()), + parameters: eval_reflection_parameters_from_names_and_type_flags( + method.params(), + method.parameter_has_types(), + ), }); } context.trait_decl(class_name).and_then(|trait_decl| { @@ -633,7 +639,10 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), - parameters: eval_reflection_parameters_from_names(method.params()), + parameters: eval_reflection_parameters_from_names_and_type_flags( + method.params(), + method.parameter_has_types(), + ), }) }) } @@ -686,9 +695,10 @@ fn eval_reflection_property_metadata( }) } -/// Builds parameter reflection metadata from the currently supported eval parameter syntax. -fn eval_reflection_parameters_from_names( +/// Builds parameter reflection metadata from eval parameter names and type flags. +fn eval_reflection_parameters_from_names_and_type_flags( names: &[String], + has_type_flags: &[bool], ) -> Vec { names .iter() @@ -699,7 +709,7 @@ fn eval_reflection_parameters_from_names( is_optional: false, is_variadic: false, is_passed_by_reference: false, - has_type: false, + has_type: has_type_flags.get(position).copied().unwrap_or(false), }) .collect() } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 2b0f0d1079..38d5a9d71b 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -493,7 +493,7 @@ return true;"#, fn execute_program_reflects_eval_method_parameters() { let program = parse_fragment( br##"class EvalReflectParamTarget { - public function run($first, $second) {} + public function run(int $first, \App\Name|null $second) {} } $params = (new ReflectionMethod("EvalReflectParamTarget", "run"))->getParameters(); echo count($params); echo ":"; @@ -513,7 +513,7 @@ return true;"##, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:first#0rvbt|second#1rvbt|"); + assert_eq!(values.output, "2:first#0rvbT|second#1rvbT|"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 3173ce5df8..f91540c88e 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -753,7 +753,7 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let params = self.parse_function_params()?; + let (params, parameter_has_types) = self.parse_method_params()?; let body = if is_abstract { self.expect_semicolon()?; Vec::new() @@ -768,7 +768,8 @@ impl Parser { is_final, params, body, - )) + ) + .with_parameter_type_flags(parameter_has_types)) } /// Parses one public property declaration with an optional initializer. @@ -1236,9 +1237,9 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let params = self.parse_function_params()?; + let (params, parameter_has_types) = self.parse_method_params()?; self.expect_semicolon()?; - Ok(EvalInterfaceMethod::new(name, params)) + Ok(EvalInterfaceMethod::new(name, params).with_parameter_type_flags(parameter_has_types)) } /// Parses one interface property hook contract. @@ -1649,6 +1650,64 @@ impl Parser { Ok(params) } + /// Parses a method parameter list and records whether each parameter declared a type. + pub(super) fn parse_method_params( + &mut self, + ) -> Result<(Vec, Vec), EvalParseError> { + let mut params = Vec::new(); + let mut parameter_has_types = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok((params, parameter_has_types)); + } + loop { + let has_type = self.parse_optional_parameter_type()?; + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + params.push(name.clone()); + parameter_has_types.push(has_type); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok((params, parameter_has_types)) + } + + /// Consumes a supported method parameter type and reports whether one existed. + fn parse_optional_parameter_type(&mut self) -> Result { + if matches!(self.current(), TokenKind::DollarIdent(_)) { + return Ok(false); + } + let nullable_shorthand = self.consume(TokenKind::Question); + if nullable_shorthand && matches!(self.current(), TokenKind::DollarIdent(_)) { + return Err(EvalParseError::UnexpectedToken); + } + self.parse_parameter_type_name()?; + if nullable_shorthand && matches!(self.current(), TokenKind::Pipe) { + return Err(EvalParseError::UnsupportedConstruct); + } + while self.consume(TokenKind::Pipe) { + self.parse_parameter_type_name()?; + } + Ok(true) + } + + /// Consumes one simple qualified method parameter type name. + fn parse_parameter_type_name(&mut self) -> Result<(), EvalParseError> { + match self.current() { + TokenKind::Ident(_) | TokenKind::Backslash => { + let _ = self.parse_qualified_name()?; + Ok(()) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + /// Parses the optional first clause of a `for` loop. pub(super) fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { if matches!(self.current(), TokenKind::Semicolon) { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 40b9d689a8..3622fc390f 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -559,6 +559,29 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { ))] ); } + +/// Verifies eval method parameters retain declared-type presence for reflection metadata. +#[test] +fn parse_fragment_accepts_typed_method_parameter_metadata() { + let program = parse_fragment( + br#"class DynEvalTypedParams { + public function run(int $id, ?\App\Name $name, string|null $label) {} +}"#, + ) + .expect("fragment should parse"); + let [EvalStmt::ClassDecl(class)] = program.statements() else { + panic!("expected one class declaration"); + }; + let [method] = class.methods() else { + panic!("expected one class method"); + }; + assert_eq!( + method.params(), + &["id".to_string(), "name".to_string(), "label".to_string()] + ); + assert_eq!(method.parameter_has_types(), &[true, true, true]); +} + /// Verifies trait declarations and class trait uses lower into dynamic metadata. #[test] fn parse_fragment_accepts_trait_declaration_and_class_use() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 4b273ce0d3..0d776a11ee 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -178,9 +178,9 @@ unpacking-aware call binding. `isFinal()`, and `isAbstract()` report eval method metadata. `ReflectionMethod::getParameters()` returns `ReflectionParameter` objects for eval-declared method parameters. Eval currently exposes parameter names and -zero-based positions there; optional, variadic, by-reference, and declared-type -flags remain false until eval's method-parameter parser carries those richer -parameter forms. +zero-based positions there, plus declared-type presence for method parameter +type hints. Optional, variadic, and by-reference flags remain false until eval's +method-parameter parser and call binder carry those richer parameter forms. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()` report eval property metadata. `ReflectionClassConstant::getAttributes()`, @@ -345,9 +345,10 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter metadata inside eval-declared methods, and broader -generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed slice. +ReflectionParameter metadata for optional, variadic, and by-reference +eval-declared parameters, enforcement of eval-declared method parameter type +hints, and broader generated/AOT method bridge signatures beyond the current +public non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f4c081b4a1..5b1c63ac69 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5874,7 +5874,7 @@ fn test_eval_reflection_method_lists_parameters() { let out = compile_and_run_capture( r#"getParameters(); echo count($params) . ":"; @@ -5893,7 +5893,7 @@ foreach ($params as $param) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:first@0rvbt|second@1rvbt|"); + assert_eq!(out.stdout, "2:first@0rvbT|second@1rvbT|"); } /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. From 94f011315ba79e99c32ef671b0faed9e28707c31 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 02:30:09 +0200 Subject: [PATCH 0374/1208] Support eval method parameter defaults --- crates/elephc-eval/src/eval_ir.rs | 28 +++++++++ .../src/interpreter/dynamic_functions.rs | 60 +++++++++++++++++++ .../elephc-eval/src/interpreter/reflection.rs | 6 +- .../elephc-eval/src/interpreter/statements.rs | 14 ++++- .../tests/builtins_class_metadata.rs | 6 +- .../src/interpreter/tests/method_arguments.rs | 30 ++++++++++ crates/elephc-eval/src/parser/statements.rs | 45 +++++++++++--- .../src/parser/tests/classes_errors.rs | 10 +++- docs/php/eval.md | 15 +++-- tests/codegen/eval.rs | 4 +- 10 files changed, 195 insertions(+), 23 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 034f722254..b943c567c3 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -528,17 +528,20 @@ pub struct EvalInterfaceMethod { attributes: Vec, params: Vec, parameter_has_types: Vec, + parameter_defaults: Vec>, } impl EvalInterfaceMethod { /// Creates one dynamic eval interface method signature. pub fn new(name: impl Into, params: Vec) -> Self { let parameter_has_types = vec![false; params.len()]; + let parameter_defaults = vec![None; params.len()]; Self { name: name.into(), attributes: Vec::new(), params, parameter_has_types, + parameter_defaults, } } @@ -554,6 +557,12 @@ impl EvalInterfaceMethod { self } + /// Returns a copy of this interface method with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + /// Returns the PHP-visible method name. pub fn name(&self) -> &str { &self.name @@ -573,6 +582,11 @@ impl EvalInterfaceMethod { pub fn parameter_has_types(&self) -> &[bool] { &self.parameter_has_types } + + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } } /// Runtime class declared by an eval fragment. @@ -1225,6 +1239,7 @@ pub struct EvalClassMethod { is_final: bool, params: Vec, parameter_has_types: Vec, + parameter_defaults: Vec>, body: Vec, } @@ -1264,6 +1279,7 @@ impl EvalClassMethod { body: Vec, ) -> Self { let parameter_has_types = vec![false; params.len()]; + let parameter_defaults = vec![None; params.len()]; Self { name: name.into(), attributes: Vec::new(), @@ -1273,6 +1289,7 @@ impl EvalClassMethod { is_final, params, parameter_has_types, + parameter_defaults, body, } } @@ -1294,6 +1311,12 @@ impl EvalClassMethod { self } + /// Returns a copy of this method with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + /// Returns attributes declared directly on this class method. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes @@ -1343,6 +1366,11 @@ impl EvalClassMethod { &self.parameter_has_types } + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } + /// Returns the dynamic EvalIR statements that form the method body. pub fn body(&self) -> &[EvalStmt] { &self.body diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index bdebc18617..d0ef1b34e7 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -145,6 +145,66 @@ pub(in crate::interpreter) fn bind_evaluated_function_args( .ok_or(EvalStatus::RuntimeFatal) } +/// Binds evaluated method arguments and fills omitted parameters from defaults. +pub(in crate::interpreter) fn bind_evaluated_method_args( + params: &[String], + parameter_defaults: &[Option], + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_dynamic_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + for (position, value) in bound_args.iter_mut().enumerate() { + if value.is_none() { + let Some(Some(default)) = parameter_defaults.get(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *value = Some(eval_method_parameter_default(default, values)?); + } + } + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Materializes a supported eval method parameter default expression. +fn eval_method_parameter_default( + default: &EvalExpr, + values: &mut impl RuntimeValueOps, +) -> Result { + match default { + EvalExpr::Const(value) => eval_const(value, values), + EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr, + } => match expr.as_ref() { + EvalExpr::Const(EvalConst::Int(value)) => values.int(*value), + EvalExpr::Const(EvalConst::Float(value)) => values.float(*value), + _ => Err(EvalStatus::UnsupportedConstruct), + }, + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => match expr.as_ref() { + EvalExpr::Const(EvalConst::Int(value)) => values.int(value.wrapping_neg()), + EvalExpr::Const(EvalConst::Float(value)) => values.float(-*value), + _ => Err(EvalStatus::UnsupportedConstruct), + }, + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Binds one positional dynamic-call value to the next declared parameter slot. pub(in crate::interpreter) fn bind_dynamic_positional_arg( bound_args: &mut [Option], diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 7d17880681..a6866d71d9 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -608,6 +608,7 @@ fn eval_reflection_method_metadata( parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), + method.parameter_defaults(), ), }); } @@ -625,6 +626,7 @@ fn eval_reflection_method_metadata( parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), + method.parameter_defaults(), ), }); } @@ -642,6 +644,7 @@ fn eval_reflection_method_metadata( parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), + method.parameter_defaults(), ), }) }) @@ -699,6 +702,7 @@ fn eval_reflection_property_metadata( fn eval_reflection_parameters_from_names_and_type_flags( names: &[String], has_type_flags: &[bool], + defaults: &[Option], ) -> Vec { names .iter() @@ -706,7 +710,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( .map(|(position, name)| EvalReflectionParameterMetadata { name: name.clone(), position, - is_optional: false, + is_optional: defaults.get(position).is_some_and(Option::is_some), is_variadic: false, is_passed_by_reference: false, has_type: has_type_flags.get(position).copied().unwrap_or(false), diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index bcdc65623d..504b083131 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2178,7 +2178,12 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = bind_evaluated_function_args(method.params(), evaluated_args)?; + let evaluated_args = bind_evaluated_method_args( + method.params(), + method.parameter_defaults(), + evaluated_args, + values, + )?; let mut method_scope = ElephcEvalScope::new(); method_scope.set("this", object, ScopeCellOwnership::Borrowed); for (name, value) in method.params().iter().zip(evaluated_args) { @@ -2222,7 +2227,12 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = bind_evaluated_function_args(method.params(), evaluated_args)?; + let evaluated_args = bind_evaluated_method_args( + method.params(), + method.parameter_defaults(), + evaluated_args, + values, + )?; let mut method_scope = ElephcEvalScope::new(); for (name, value) in method.params().iter().zip(evaluated_args) { method_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 38d5a9d71b..03664927d3 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -492,8 +492,8 @@ return true;"#, #[test] fn execute_program_reflects_eval_method_parameters() { let program = parse_fragment( - br##"class EvalReflectParamTarget { - public function run(int $first, \App\Name|null $second) {} +br##"class EvalReflectParamTarget { + public function run(int $first, \App\Name|null $second = null) {} } $params = (new ReflectionMethod("EvalReflectParamTarget", "run"))->getParameters(); echo count($params); echo ":"; @@ -513,7 +513,7 @@ return true;"##, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:first#0rvbT|second#1rvbT|"); + assert_eq!(values.output, "2:first#0rvbT|second#1OvbT|"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index f641710f63..53c4fc4af8 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -42,6 +42,36 @@ return EvalNamedMethodBox::join(right: "H", left: "G");"#, assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); } +/// Verifies eval-declared methods use default values for omitted arguments. +#[test] +fn execute_program_binds_eval_method_default_args() { + let program = parse_fragment( + br#"class EvalDefaultMethodBox { + public function __construct($left = "A", $right = "B") { + $this->label = $left . $right; + } + public function read($left, $right = "D") { + return $this->label . ":" . $left . ":" . $right; + } + public static function join($left = "G", $right = "H") { + return $left . "-" . $right; + } +} +$box = new EvalDefaultMethodBox(); +echo $box->read("C"); echo ":"; +echo $box->read(right: "F", left: "E"); echo ":"; +return EvalDefaultMethodBox::join();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:C:D:AB:E:F:"); + assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); +} + /// Verifies eval-declared methods reject unknown named arguments. #[test] fn execute_program_rejects_unknown_eval_method_named_arg() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index f91540c88e..399e20e839 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -753,7 +753,7 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let (params, parameter_has_types) = self.parse_method_params()?; + let (params, parameter_has_types, parameter_defaults) = self.parse_method_params()?; let body = if is_abstract { self.expect_semicolon()?; Vec::new() @@ -769,7 +769,8 @@ impl Parser { params, body, ) - .with_parameter_type_flags(parameter_has_types)) + .with_parameter_type_flags(parameter_has_types) + .with_parameter_defaults(parameter_defaults)) } /// Parses one public property declaration with an optional initializer. @@ -1237,9 +1238,11 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let (params, parameter_has_types) = self.parse_method_params()?; + let (params, parameter_has_types, parameter_defaults) = self.parse_method_params()?; self.expect_semicolon()?; - Ok(EvalInterfaceMethod::new(name, params).with_parameter_type_flags(parameter_has_types)) + Ok(EvalInterfaceMethod::new(name, params) + .with_parameter_type_flags(parameter_has_types) + .with_parameter_defaults(parameter_defaults)) } /// Parses one interface property hook contract. @@ -1650,14 +1653,15 @@ impl Parser { Ok(params) } - /// Parses a method parameter list and records whether each parameter declared a type. + /// Parses a method parameter list and records type/default metadata. pub(super) fn parse_method_params( &mut self, - ) -> Result<(Vec, Vec), EvalParseError> { + ) -> Result<(Vec, Vec, Vec>), EvalParseError> { let mut params = Vec::new(); let mut parameter_has_types = Vec::new(); + let mut parameter_defaults = Vec::new(); if self.consume(TokenKind::RParen) { - return Ok((params, parameter_has_types)); + return Ok((params, parameter_has_types, parameter_defaults)); } loop { let has_type = self.parse_optional_parameter_type()?; @@ -1667,6 +1671,16 @@ impl Parser { params.push(name.clone()); parameter_has_types.push(has_type); self.advance(); + let default = if self.consume(TokenKind::Equal) { + let default = self.parse_expr()?; + if !method_parameter_default_is_supported(&default) { + return Err(EvalParseError::UnsupportedConstruct); + } + Some(default) + } else { + None + }; + parameter_defaults.push(default); if !self.consume(TokenKind::Comma) { break; } @@ -1675,7 +1689,7 @@ impl Parser { } } self.expect(TokenKind::RParen)?; - Ok((params, parameter_has_types)) + Ok((params, parameter_has_types, parameter_defaults)) } /// Consumes a supported method parameter type and reports whether one existed. @@ -2064,6 +2078,21 @@ impl Parser { } } +/// Returns whether an eval method parameter default can be materialized safely. +fn method_parameter_default_is_supported(default: &EvalExpr) -> bool { + match default { + EvalExpr::Const(_) => true, + EvalExpr::Unary { + op: EvalUnaryOp::Plus | EvalUnaryOp::Negate, + expr, + } => matches!( + expr.as_ref(), + EvalExpr::Const(EvalConst::Int(_) | EvalConst::Float(_)) + ), + _ => false, + } +} + /// Converts a parsed attribute argument expression into retained literal metadata. fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { match expr { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 3622fc390f..889386995f 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -565,7 +565,7 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { fn parse_fragment_accepts_typed_method_parameter_metadata() { let program = parse_fragment( br#"class DynEvalTypedParams { - public function run(int $id, ?\App\Name $name, string|null $label) {} + public function run(int $id = 7, ?\App\Name $name = null, string|null $label = "x") {} }"#, ) .expect("fragment should parse"); @@ -580,6 +580,14 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { &["id".to_string(), "name".to_string(), "label".to_string()] ); assert_eq!(method.parameter_has_types(), &[true, true, true]); + assert!(matches!( + method.parameter_defaults(), + [ + Some(EvalExpr::Const(EvalConst::Int(7))), + Some(EvalExpr::Const(EvalConst::Null)), + Some(EvalExpr::Const(EvalConst::String(label))) + ] if label == "x" + )); } /// Verifies trait declarations and class trait uses lower into dynamic metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index 0d776a11ee..fc85214f76 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -179,8 +179,10 @@ unpacking-aware call binding. `ReflectionMethod::getParameters()` returns `ReflectionParameter` objects for eval-declared method parameters. Eval currently exposes parameter names and zero-based positions there, plus declared-type presence for method parameter -type hints. Optional, variadic, and by-reference flags remain false until eval's -method-parameter parser and call binder carry those richer parameter forms. +type hints. Defaulted eval method parameters are bound when omitted and reported +through `ReflectionParameter::isOptional()`. Variadic and by-reference flags +remain false until eval's method-parameter parser and call binder carry those +richer parameter forms. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()` report eval property metadata. `ReflectionClassConstant::getAttributes()`, @@ -345,10 +347,11 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter metadata for optional, variadic, and by-reference -eval-declared parameters, enforcement of eval-declared method parameter type -hints, and broader generated/AOT method bridge signatures beyond the current -public non-by-reference fixed scalar/Mixed slice. +ReflectionParameter metadata for variadic and by-reference eval-declared +parameters, enforcement of eval-declared method parameter type hints, broader +default-value expression support beyond scalar literals, and broader +generated/AOT method bridge signatures beyond the current public +non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5b1c63ac69..984a3e2598 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5874,7 +5874,7 @@ fn test_eval_reflection_method_lists_parameters() { let out = compile_and_run_capture( r#"getParameters(); echo count($params) . ":"; @@ -5893,7 +5893,7 @@ foreach ($params as $param) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:first@0rvbT|second@1rvbT|"); + assert_eq!(out.stdout, "2:first@0rvbT|second@1OvbT|"); } /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. From 721cb000f58bc62c4b49619d8cd7815bf4a459a9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 02:41:55 +0200 Subject: [PATCH 0375/1208] Support eval variadic method parameters --- crates/elephc-eval/src/eval_ir.rs | 28 ++++ .../src/interpreter/dynamic_functions.rs | 144 +++++++++++++++++- .../elephc-eval/src/interpreter/reflection.rs | 9 +- .../elephc-eval/src/interpreter/statements.rs | 90 ++++++++++- .../tests/builtins_class_metadata.rs | 4 +- .../src/interpreter/tests/expressions.rs | 69 +++++++++ .../src/interpreter/tests/method_arguments.rs | 74 +++++++++ crates/elephc-eval/src/parser/statements.rs | 42 ++++- .../src/parser/tests/classes_errors.rs | 24 ++- docs/php/eval.md | 19 ++- tests/codegen/eval.rs | 34 ++++- 11 files changed, 507 insertions(+), 30 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index b943c567c3..9077a11f63 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -529,6 +529,7 @@ pub struct EvalInterfaceMethod { params: Vec, parameter_has_types: Vec, parameter_defaults: Vec>, + parameter_is_variadic: Vec, } impl EvalInterfaceMethod { @@ -536,12 +537,14 @@ impl EvalInterfaceMethod { pub fn new(name: impl Into, params: Vec) -> Self { let parameter_has_types = vec![false; params.len()]; let parameter_defaults = vec![None; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), attributes: Vec::new(), params, parameter_has_types, parameter_defaults, + parameter_is_variadic, } } @@ -563,6 +566,12 @@ impl EvalInterfaceMethod { self } + /// Returns a copy of this interface method with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + /// Returns the PHP-visible method name. pub fn name(&self) -> &str { &self.name @@ -587,6 +596,11 @@ impl EvalInterfaceMethod { pub fn parameter_defaults(&self) -> &[Option] { &self.parameter_defaults } + + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } } /// Runtime class declared by an eval fragment. @@ -1240,6 +1254,7 @@ pub struct EvalClassMethod { params: Vec, parameter_has_types: Vec, parameter_defaults: Vec>, + parameter_is_variadic: Vec, body: Vec, } @@ -1280,6 +1295,7 @@ impl EvalClassMethod { ) -> Self { let parameter_has_types = vec![false; params.len()]; let parameter_defaults = vec![None; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), attributes: Vec::new(), @@ -1290,6 +1306,7 @@ impl EvalClassMethod { params, parameter_has_types, parameter_defaults, + parameter_is_variadic, body, } } @@ -1317,6 +1334,12 @@ impl EvalClassMethod { self } + /// Returns a copy of this method with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + /// Returns attributes declared directly on this class method. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes @@ -1371,6 +1394,11 @@ impl EvalClassMethod { &self.parameter_defaults } + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } + /// Returns the dynamic EvalIR statements that form the method body. pub fn body(&self) -> &[EvalStmt] { &self.body diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index d0ef1b34e7..c100da57fd 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -149,22 +149,62 @@ pub(in crate::interpreter) fn bind_evaluated_function_args( pub(in crate::interpreter) fn bind_evaluated_method_args( params: &[String], parameter_defaults: &[Option], + parameter_is_variadic: &[bool], evaluated_args: Vec, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let mut bound_args = vec![None; params.len()]; + let variadic_index = parameter_is_variadic.iter().position(|is_variadic| *is_variadic); + let required_count = + method_required_param_count(params.len(), parameter_defaults, parameter_is_variadic); let mut next_positional = 0; + let mut next_variadic_index = 0_i64; + let mut variadic_named_args = std::collections::HashSet::new(); + + if let Some(index) = variadic_index { + let array = if evaluated_args_contain_named_variadic_values( + params, + variadic_index, + &evaluated_args, + ) { + values.assoc_new(evaluated_args.len())? + } else { + values.array_new(evaluated_args.len())? + }; + bound_args[index] = Some(array); + } for arg in evaluated_args { if let Some(name) = arg.name { - bind_dynamic_named_arg(params, &mut bound_args, &name, arg.value)?; + bind_dynamic_named_method_arg( + params, + variadic_index, + &mut bound_args, + &name, + arg.value, + &mut variadic_named_args, + values, + )?; } else { - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + bind_dynamic_positional_method_arg( + &mut bound_args, + variadic_index, + &mut next_positional, + &mut next_variadic_index, + arg.value, + values, + )?; } } for (position, value) in bound_args.iter_mut().enumerate() { + if Some(position) == variadic_index { + continue; + } if value.is_none() { + if position < required_count { + return Err(EvalStatus::RuntimeFatal); + } let Some(Some(default)) = parameter_defaults.get(position) else { return Err(EvalStatus::RuntimeFatal); }; @@ -178,6 +218,106 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( .ok_or(EvalStatus::RuntimeFatal) } +/// Returns the minimum argument count for a PHP method signature. +fn method_required_param_count( + param_count: usize, + defaults: &[Option], + variadics: &[bool], +) -> usize { + let fixed_count = variadics + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(param_count); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + +/// Returns true when evaluated args contain named values captured by a variadic parameter. +fn evaluated_args_contain_named_variadic_values( + params: &[String], + variadic_index: Option, + evaluated_args: &[EvaluatedCallArg], +) -> bool { + let Some(variadic_index) = variadic_index else { + return false; + }; + evaluated_args.iter().any(|arg| { + arg.name.as_ref().is_some_and(|name| { + regular_method_param_index(params, Some(variadic_index), name).is_none() + }) + }) +} + +/// Binds one positional method argument to a fixed parameter or variadic array. +fn bind_dynamic_positional_method_arg( + bound_args: &mut [Option], + variadic_index: Option, + next_positional: &mut usize, + next_variadic_index: &mut i64, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if variadic_index.is_some_and(|index| *next_positional >= index) { + let key = values.int(*next_variadic_index)?; + *next_variadic_index = next_variadic_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + return bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, values); + } + bind_dynamic_positional_arg(bound_args, next_positional, value) +} + +/// Binds one named method argument to a fixed parameter or variadic array. +fn bind_dynamic_named_method_arg( + params: &[String], + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, + variadic_named_args: &mut std::collections::HashSet, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(param_index) = regular_method_param_index(params, variadic_index, name) { + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + return Ok(()); + } + if variadic_index.is_none() || !variadic_named_args.insert(name.to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + let key = values.string(name)?; + bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, values) +} + +/// Returns the matching non-variadic parameter index for one PHP named argument. +fn regular_method_param_index( + params: &[String], + variadic_index: Option, + name: &str, +) -> Option { + params + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + +/// Appends one value into the method variadic array. +fn bind_dynamic_variadic_arg( + bound_args: &mut [Option], + variadic_index: Option, + key: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; + let array = bound_args[index].ok_or(EvalStatus::RuntimeFatal)?; + bound_args[index] = Some(values.array_set(array, key, value)?); + Ok(()) +} + /// Materializes a supported eval method parameter default expression. fn eval_method_parameter_default( default: &EvalExpr, diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index a6866d71d9..c561cc16eb 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -609,6 +609,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_defaults(), + method.parameter_is_variadic(), ), }); } @@ -627,6 +628,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_defaults(), + method.parameter_is_variadic(), ), }); } @@ -645,6 +647,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_defaults(), + method.parameter_is_variadic(), ), }) }) @@ -703,6 +706,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( names: &[String], has_type_flags: &[bool], defaults: &[Option], + variadic_flags: &[bool], ) -> Vec { names .iter() @@ -710,8 +714,9 @@ fn eval_reflection_parameters_from_names_and_type_flags( .map(|(position, name)| EvalReflectionParameterMetadata { name: name.clone(), position, - is_optional: defaults.get(position).is_some_and(Option::is_some), - is_variadic: false, + is_optional: defaults.get(position).is_some_and(Option::is_some) + || variadic_flags.get(position).copied().unwrap_or(false), + is_variadic: variadic_flags.get(position).copied().unwrap_or(false), is_passed_by_reference: false, has_type: has_type_flags.get(position).copied().unwrap_or(false), }) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 504b083131..471cb1fbb0 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1066,9 +1066,24 @@ fn validate_method_parent_override( if method.is_abstract() && !parent_method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } + if !class_method_signature_accepts(method, &parent_method) { + return Err(EvalStatus::RuntimeFatal); + } Ok(()) } +/// Returns whether one eval class method can accept every call accepted by its parent method. +fn class_method_signature_accepts(method: &EvalClassMethod, required: &EvalClassMethod) -> bool { + method_signature_accepts( + method.params().len(), + method.parameter_defaults(), + method.parameter_is_variadic(), + required.params().len(), + required.parameter_defaults(), + required.parameter_is_variadic(), + ) +} + /// Returns a comparable rank where larger means less restrictive visibility. fn method_visibility_rank(visibility: EvalVisibility) -> u8 { match visibility { @@ -1309,7 +1324,7 @@ fn class_has_interface_method( return method.visibility() == EvalVisibility::Public && !method.is_static() && !method.is_abstract() - && method.params().len() == requirement.params().len(); + && class_method_satisfies_interface_signature(method, requirement); } class .parent() @@ -1318,10 +1333,79 @@ fn class_has_interface_method( method.visibility() == EvalVisibility::Public && !method.is_static() && !method.is_abstract() - && method.params().len() == requirement.params().len() + && class_method_satisfies_interface_signature(&method, requirement) }) } +/// Returns whether one class method can accept every call required by an interface method. +fn class_method_satisfies_interface_signature( + method: &EvalClassMethod, + requirement: &EvalInterfaceMethod, +) -> bool { + method_signature_accepts( + method.params().len(), + method.parameter_defaults(), + method.parameter_is_variadic(), + requirement.params().len(), + requirement.parameter_defaults(), + requirement.parameter_is_variadic(), + ) +} + +/// Returns whether an implementing method accepts the full required arity range. +fn method_signature_accepts( + implementation_param_count: usize, + implementation_defaults: &[Option], + implementation_variadics: &[bool], + required_param_count: usize, + required_defaults: &[Option], + required_variadics: &[bool], +) -> bool { + let implementation_min = method_signature_min_arity( + implementation_param_count, + implementation_defaults, + implementation_variadics, + ); + let required_min = + method_signature_min_arity(required_param_count, required_defaults, required_variadics); + if implementation_min > required_min { + return false; + } + + let implementation_max = + method_signature_max_arity(implementation_param_count, implementation_variadics); + let required_max = method_signature_max_arity(required_param_count, required_variadics); + match (implementation_max, required_max) { + (None, _) => true, + (Some(_), None) => false, + (Some(implementation_max), Some(required_max)) => implementation_max >= required_max, + } +} + +/// Returns the minimum argument count accepted by one eval method signature. +fn method_signature_min_arity( + param_count: usize, + defaults: &[Option], + variadics: &[bool], +) -> usize { + let fixed_count = variadics + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(param_count); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + +/// Returns the maximum argument count accepted by one eval method signature. +fn method_signature_max_arity(param_count: usize, variadics: &[bool]) -> Option { + if variadics.iter().any(|is_variadic| *is_variadic) { + None + } else { + Some(param_count) + } +} + /// Returns whether a class or its eval parents satisfy one interface property contract. fn class_has_interface_property( class: &EvalClass, @@ -2181,6 +2265,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( let evaluated_args = bind_evaluated_method_args( method.params(), method.parameter_defaults(), + method.parameter_is_variadic(), evaluated_args, values, )?; @@ -2230,6 +2315,7 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( let evaluated_args = bind_evaluated_method_args( method.params(), method.parameter_defaults(), + method.parameter_is_variadic(), evaluated_args, values, )?; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 03664927d3..b8952cdf4e 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -493,7 +493,7 @@ return true;"#, fn execute_program_reflects_eval_method_parameters() { let program = parse_fragment( br##"class EvalReflectParamTarget { - public function run(int $first, \App\Name|null $second = null) {} + public function run(int $first, \App\Name|null $second = null, ...$rest) {} } $params = (new ReflectionMethod("EvalReflectParamTarget", "run"))->getParameters(); echo count($params); echo ":"; @@ -513,7 +513,7 @@ return true;"##, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:first#0rvbT|second#1OvbT|"); + assert_eq!(values.output, "3:first#0rvbT|second#1OvbT|rest#2OVbt|"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index bda451d9f8..c5a4ca39bc 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -727,6 +727,28 @@ class EvalVisibleChild extends EvalVisibleBase { assert_eq!(err, EvalStatus::RuntimeFatal); } + +/// Verifies eval rejects parent method overrides that require more arguments. +#[test] +fn execute_program_rejects_method_override_with_narrower_arity() { + let program = parse_fragment( + br#"class EvalArityBase { + public function read($value = "base") { return $value; } +} +class EvalArityChild extends EvalArityBase { + public function read($value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("narrower method override arity should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval rejects classes missing methods required by eval interfaces. #[test] fn execute_program_rejects_missing_dynamic_interface_method() { @@ -745,3 +767,50 @@ class EvalMissingRead implements EvalNeedsRead {}"#, assert_eq!(err, EvalStatus::RuntimeFatal); } + +/// Verifies variadic eval methods can satisfy fixed-arity interface contracts. +#[test] +fn execute_program_accepts_variadic_method_for_fixed_interface_contract() { + let program = parse_fragment( + br#"interface EvalFixedReadable { + function read($left, $right); +} +class EvalVariadicReadable implements EvalFixedReadable { + public function read($left, ...$tail) { + return $left . $tail[0]; + } +} +$box = new EvalVariadicReadable(); +return $box->read("A", "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} + +/// Verifies non-variadic eval methods cannot satisfy variadic interface contracts. +#[test] +fn execute_program_rejects_non_variadic_method_for_variadic_interface_contract() { + let program = parse_fragment( + br#"interface EvalVariadicReadable { + function read($left, ...$tail); +} +class EvalFixedReadable implements EvalVariadicReadable { + public function read($left, $tail = null) { + return $left; + } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("non-variadic implementation should not satisfy variadic contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index 53c4fc4af8..9aecd29975 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -72,6 +72,80 @@ return EvalDefaultMethodBox::join();"#, assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); } +/// Verifies eval-declared methods bind positional and named values into variadic arrays. +#[test] +fn execute_program_binds_eval_method_variadic_args() { + let program = parse_fragment( + br#"class EvalVariadicMethodBox { + public function __construct(...$parts) { + $this->label = $parts[0] . $parts["right"]; + } + public function read($head, ...$tail) { + echo count($tail); echo ":"; + return $this->label . ":" . $head . ":" . $tail[0] . ":" . $tail["named"] . ":" . $tail["tail"]; + } + public static function join(...$items) { + return $items[0] . $items[1]; + } +} +$box = new EvalVariadicMethodBox("A", right: "B"); +echo $box->read("C", "D", named: "E", tail: "F"); echo ":"; +return EvalVariadicMethodBox::join("G", "H");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:AB:C:D:E:F:"); + assert_eq!(values.get(result), FakeValue::String("GH".to_string())); +} + +/// Verifies eval-declared variadic methods reject duplicate named variadic keys. +#[test] +fn execute_program_rejects_duplicate_eval_method_variadic_named_arg() { + let program = parse_fragment( + br#"class EvalDuplicateVariadicBox { + public function read(...$tail) { + return count($tail); + } +} +$box = new EvalDuplicateVariadicBox(); +return $box->read(name: "A", name: "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("duplicate named variadic argument should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies defaults before required eval method parameters do not make earlier slots optional. +#[test] +fn execute_program_rejects_eval_method_default_before_required_omission() { + let program = parse_fragment( + br#"class EvalRequiredAfterDefaultBox { + public function read($left = "A", $right) { + return $left . $right; + } +} +$box = new EvalRequiredAfterDefaultBox(); +return $box->read(right: "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("default before required parameter should remain required"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval-declared methods reject unknown named arguments. #[test] fn execute_program_rejects_unknown_eval_method_named_arg() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 399e20e839..73efc7f931 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -753,7 +753,8 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let (params, parameter_has_types, parameter_defaults) = self.parse_method_params()?; + let (params, parameter_has_types, parameter_defaults, parameter_is_variadic) = + self.parse_method_params()?; let body = if is_abstract { self.expect_semicolon()?; Vec::new() @@ -770,7 +771,8 @@ impl Parser { body, ) .with_parameter_type_flags(parameter_has_types) - .with_parameter_defaults(parameter_defaults)) + .with_parameter_defaults(parameter_defaults) + .with_parameter_variadic_flags(parameter_is_variadic)) } /// Parses one public property declaration with an optional initializer. @@ -1238,11 +1240,13 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let (params, parameter_has_types, parameter_defaults) = self.parse_method_params()?; + let (params, parameter_has_types, parameter_defaults, parameter_is_variadic) = + self.parse_method_params()?; self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) .with_parameter_type_flags(parameter_has_types) - .with_parameter_defaults(parameter_defaults)) + .with_parameter_defaults(parameter_defaults) + .with_parameter_variadic_flags(parameter_is_variadic)) } /// Parses one interface property hook contract. @@ -1656,22 +1660,33 @@ impl Parser { /// Parses a method parameter list and records type/default metadata. pub(super) fn parse_method_params( &mut self, - ) -> Result<(Vec, Vec, Vec>), EvalParseError> { + ) -> Result<(Vec, Vec, Vec>, Vec), EvalParseError> { let mut params = Vec::new(); let mut parameter_has_types = Vec::new(); let mut parameter_defaults = Vec::new(); + let mut parameter_is_variadic = Vec::new(); if self.consume(TokenKind::RParen) { - return Ok((params, parameter_has_types, parameter_defaults)); + return Ok(( + params, + parameter_has_types, + parameter_defaults, + parameter_is_variadic, + )); } loop { let has_type = self.parse_optional_parameter_type()?; + let is_variadic = self.consume(TokenKind::Ellipsis); let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; params.push(name.clone()); parameter_has_types.push(has_type); + parameter_is_variadic.push(is_variadic); self.advance(); let default = if self.consume(TokenKind::Equal) { + if is_variadic { + return Err(EvalParseError::UnsupportedConstruct); + } let default = self.parse_expr()?; if !method_parameter_default_is_supported(&default) { return Err(EvalParseError::UnsupportedConstruct); @@ -1684,17 +1699,28 @@ impl Parser { if !self.consume(TokenKind::Comma) { break; } + if is_variadic { + return Err(EvalParseError::UnsupportedConstruct); + } if matches!(self.current(), TokenKind::RParen) { return Err(EvalParseError::ExpectedVariable); } } self.expect(TokenKind::RParen)?; - Ok((params, parameter_has_types, parameter_defaults)) + Ok(( + params, + parameter_has_types, + parameter_defaults, + parameter_is_variadic, + )) } /// Consumes a supported method parameter type and reports whether one existed. fn parse_optional_parameter_type(&mut self) -> Result { - if matches!(self.current(), TokenKind::DollarIdent(_)) { + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ellipsis + ) { return Ok(false); } let nullable_shorthand = self.consume(TokenKind::Question); diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 889386995f..242178afa4 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -565,7 +565,7 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { fn parse_fragment_accepts_typed_method_parameter_metadata() { let program = parse_fragment( br#"class DynEvalTypedParams { - public function run(int $id = 7, ?\App\Name $name = null, string|null $label = "x") {} + public function run(int $id = 7, ?\App\Name $name = null, string|null $label = "x", mixed ...$tail) {} }"#, ) .expect("fragment should parse"); @@ -577,17 +577,33 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { }; assert_eq!( method.params(), - &["id".to_string(), "name".to_string(), "label".to_string()] + &[ + "id".to_string(), + "name".to_string(), + "label".to_string(), + "tail".to_string() + ] ); - assert_eq!(method.parameter_has_types(), &[true, true, true]); + assert_eq!(method.parameter_has_types(), &[true, true, true, true]); assert!(matches!( method.parameter_defaults(), [ Some(EvalExpr::Const(EvalConst::Int(7))), Some(EvalExpr::Const(EvalConst::Null)), - Some(EvalExpr::Const(EvalConst::String(label))) + Some(EvalExpr::Const(EvalConst::String(label))), + None ] if label == "x" )); + assert_eq!(method.parameter_is_variadic(), &[false, false, false, true]); +} + +/// Verifies eval rejects invalid variadic method parameter forms. +#[test] +fn parse_fragment_rejects_invalid_variadic_method_parameters() { + parse_fragment(b"class DynEvalVariadicDefault { public function run(...$tail = null) {} }") + .expect_err("variadic method parameters cannot have defaults"); + parse_fragment(b"class DynEvalVariadicNotLast { public function run(...$tail, $next) {} }") + .expect_err("variadic method parameters must be last"); } /// Verifies trait declarations and class trait uses lower into dynamic metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index fc85214f76..8a71a6dd24 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -180,9 +180,12 @@ unpacking-aware call binding. eval-declared method parameters. Eval currently exposes parameter names and zero-based positions there, plus declared-type presence for method parameter type hints. Defaulted eval method parameters are bound when omitted and reported -through `ReflectionParameter::isOptional()`. Variadic and by-reference flags -remain false until eval's method-parameter parser and call binder carry those -richer parameter forms. +through `ReflectionParameter::isOptional()`. Variadic eval method parameters +bind extra positional and unknown named arguments into a PHP array and are +reported through `ReflectionParameter::isVariadic()` and +`ReflectionParameter::isOptional()`. By-reference flags remain false until +eval's method-parameter parser and call binder carry by-reference parameter +forms. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()` report eval property metadata. `ReflectionClassConstant::getAttributes()`, @@ -347,11 +350,11 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter metadata for variadic and by-reference eval-declared -parameters, enforcement of eval-declared method parameter type hints, broader -default-value expression support beyond scalar literals, and broader -generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed slice. +ReflectionParameter metadata for by-reference eval-declared parameters, +enforcement of eval-declared method parameter type hints, broader default-value +expression support beyond scalar literals, and broader generated/AOT method +bridge signatures beyond the current public non-by-reference fixed scalar/Mixed +slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 984a3e2598..39d629883b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5334,6 +5334,36 @@ echo EvalNamedMethodBox::join(right: "H", left: "G");'); assert_eq!(out.stdout, "AB:C:D:AB:E:F:G-H"); } +/// Verifies eval-declared constructors and methods bind variadic arguments. +#[test] +fn test_eval_declared_method_variadic_arguments() { + let out = compile_and_run_capture( + r#"label = $parts[0] . $parts["right"]; + } + public function read($head, ...$tail) { + echo count($tail) . ":"; + return $this->label . ":" . $head . ":" . $tail[0] . ":" . $tail["named"] . ":" . $tail["tail"]; + } + public static function join(...$items) { + return $items[0] . $items[1]; + } +} +$box = new EvalVariadicMethodBox("A", right: "B"); +echo $box->read("C", "D", named: "E", tail: "F") . ":"; +echo EvalVariadicMethodBox::join("G", "H");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "3:AB:C:D:E:F:GH"); +} + /// Verifies eval dynamic static callables dispatch eval-declared static methods. #[test] fn test_eval_declared_static_method_dynamic_callables() { @@ -5874,7 +5904,7 @@ fn test_eval_reflection_method_lists_parameters() { let out = compile_and_run_capture( r#"getParameters(); echo count($params) . ":"; @@ -5893,7 +5923,7 @@ foreach ($params as $param) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:first@0rvbT|second@1OvbT|"); + assert_eq!(out.stdout, "3:first@0rvbT|second@1OvbT|rest@2OVbt|"); } /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. From 4f06858949121513f6a1d22b98db8dabfe2d5fc7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 02:50:34 +0200 Subject: [PATCH 0376/1208] Enforce eval method parameter type hints --- crates/elephc-eval/src/eval_ir.rs | 72 ++++++ .../src/interpreter/dynamic_functions.rs | 206 ++++++++++++++++++ crates/elephc-eval/src/interpreter/mod.rs | 5 +- .../elephc-eval/src/interpreter/statements.rs | 4 + .../src/interpreter/tests/method_arguments.rs | 93 ++++++++ crates/elephc-eval/src/parser/statements.rs | 96 ++++++-- .../src/parser/tests/classes_errors.rs | 16 ++ docs/php/eval.md | 11 +- tests/codegen/eval.rs | 26 +++ 9 files changed, 502 insertions(+), 27 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 9077a11f63..134bbef841 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -176,6 +176,48 @@ impl EvalFunction { } } +/// One supported eval method parameter type atom. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EvalParameterTypeVariant { + Array, + Bool, + Callable, + Class(String), + Float, + Int, + Iterable, + Mixed, + Object, + String, +} + +/// Type metadata retained for one eval method parameter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EvalParameterType { + variants: Vec, + allows_null: bool, +} + +impl EvalParameterType { + /// Creates one eval method parameter type from union variants and nullability. + pub fn new(variants: Vec, allows_null: bool) -> Self { + Self { + variants, + allows_null, + } + } + + /// Returns the non-null type atoms in source order. + pub fn variants(&self) -> &[EvalParameterTypeVariant] { + &self.variants + } + + /// Returns whether the type explicitly accepts PHP null. + pub const fn allows_null(&self) -> bool { + self.allows_null + } +} + /// Literal attribute argument metadata retained by eval declarations. #[derive(Debug, Clone, PartialEq)] pub enum EvalAttributeArg { @@ -528,6 +570,7 @@ pub struct EvalInterfaceMethod { attributes: Vec, params: Vec, parameter_has_types: Vec, + parameter_types: Vec>, parameter_defaults: Vec>, parameter_is_variadic: Vec, } @@ -536,6 +579,7 @@ impl EvalInterfaceMethod { /// Creates one dynamic eval interface method signature. pub fn new(name: impl Into, params: Vec) -> Self { let parameter_has_types = vec![false; params.len()]; + let parameter_types = vec![None; params.len()]; let parameter_defaults = vec![None; params.len()]; let parameter_is_variadic = vec![false; params.len()]; Self { @@ -543,6 +587,7 @@ impl EvalInterfaceMethod { attributes: Vec::new(), params, parameter_has_types, + parameter_types, parameter_defaults, parameter_is_variadic, } @@ -560,6 +605,13 @@ impl EvalInterfaceMethod { self } + /// Returns a copy of this interface method with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); + self.parameter_types = parameter_types; + self + } + /// Returns a copy of this interface method with source-order default expressions. pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { self.parameter_defaults = parameter_defaults; @@ -592,6 +644,11 @@ impl EvalInterfaceMethod { &self.parameter_has_types } + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + /// Returns default expressions declared for each source-order parameter. pub fn parameter_defaults(&self) -> &[Option] { &self.parameter_defaults @@ -1253,6 +1310,7 @@ pub struct EvalClassMethod { is_final: bool, params: Vec, parameter_has_types: Vec, + parameter_types: Vec>, parameter_defaults: Vec>, parameter_is_variadic: Vec, body: Vec, @@ -1294,6 +1352,7 @@ impl EvalClassMethod { body: Vec, ) -> Self { let parameter_has_types = vec![false; params.len()]; + let parameter_types = vec![None; params.len()]; let parameter_defaults = vec![None; params.len()]; let parameter_is_variadic = vec![false; params.len()]; Self { @@ -1305,6 +1364,7 @@ impl EvalClassMethod { is_final, params, parameter_has_types, + parameter_types, parameter_defaults, parameter_is_variadic, body, @@ -1328,6 +1388,13 @@ impl EvalClassMethod { self } + /// Returns a copy of this method with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); + self.parameter_types = parameter_types; + self + } + /// Returns a copy of this method with source-order default expressions. pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { self.parameter_defaults = parameter_defaults; @@ -1389,6 +1456,11 @@ impl EvalClassMethod { &self.parameter_has_types } + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + /// Returns default expressions declared for each source-order parameter. pub fn parameter_defaults(&self) -> &[Option] { &self.parameter_defaults diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index c100da57fd..cb89c88f9c 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -148,9 +148,11 @@ pub(in crate::interpreter) fn bind_evaluated_function_args( /// Binds evaluated method arguments and fills omitted parameters from defaults. pub(in crate::interpreter) fn bind_evaluated_method_args( params: &[String], + parameter_types: &[Option], parameter_defaults: &[Option], parameter_is_variadic: &[bool], evaluated_args: Vec, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let mut bound_args = vec![None; params.len()]; @@ -178,20 +180,24 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( if let Some(name) = arg.name { bind_dynamic_named_method_arg( params, + parameter_types, variadic_index, &mut bound_args, &name, arg.value, &mut variadic_named_args, + context, values, )?; } else { bind_dynamic_positional_method_arg( &mut bound_args, + parameter_types, variadic_index, &mut next_positional, &mut next_variadic_index, arg.value, + context, values, )?; } @@ -210,6 +216,10 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( }; *value = Some(eval_method_parameter_default(default, values)?); } + if let Some(param_type) = parameter_types.get(position).and_then(Option::as_ref) { + let typed = eval_method_parameter_value(param_type, value.unwrap(), context, values)?; + *value = Some(typed); + } } bound_args @@ -252,10 +262,12 @@ fn evaluated_args_contain_named_variadic_values( /// Binds one positional method argument to a fixed parameter or variadic array. fn bind_dynamic_positional_method_arg( bound_args: &mut [Option], + parameter_types: &[Option], variadic_index: Option, next_positional: &mut usize, next_variadic_index: &mut i64, value: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if variadic_index.is_some_and(|index| *next_positional >= index) { @@ -263,6 +275,13 @@ fn bind_dynamic_positional_method_arg( *next_variadic_index = next_variadic_index .checked_add(1) .ok_or(EvalStatus::RuntimeFatal)?; + let value = eval_variadic_method_parameter_value( + parameter_types, + variadic_index, + value, + context, + values, + )?; return bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, values); } bind_dynamic_positional_arg(bound_args, next_positional, value) @@ -271,11 +290,13 @@ fn bind_dynamic_positional_method_arg( /// Binds one named method argument to a fixed parameter or variadic array. fn bind_dynamic_named_method_arg( params: &[String], + parameter_types: &[Option], variadic_index: Option, bound_args: &mut [Option], name: &str, value: RuntimeCellHandle, variadic_named_args: &mut std::collections::HashSet, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if let Some(param_index) = regular_method_param_index(params, variadic_index, name) { @@ -289,9 +310,32 @@ fn bind_dynamic_named_method_arg( return Err(EvalStatus::RuntimeFatal); } let key = values.string(name)?; + let value = eval_variadic_method_parameter_value( + parameter_types, + variadic_index, + value, + context, + values, + )?; bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, values) } +/// Applies a variadic parameter type to one captured argument value. +fn eval_variadic_method_parameter_value( + parameter_types: &[Option], + variadic_index: Option, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(param_type) = + variadic_index.and_then(|index| parameter_types.get(index).and_then(Option::as_ref)) + else { + return Ok(value); + }; + eval_method_parameter_value(param_type, value, context, values) +} + /// Returns the matching non-variadic parameter index for one PHP named argument. fn regular_method_param_index( params: &[String], @@ -318,6 +362,168 @@ fn bind_dynamic_variadic_arg( Ok(()) } +/// Applies one eval method parameter type to a bound runtime value. +fn eval_method_parameter_value( + param_type: &EvalParameterType, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_method_parameter_type_accepts_exact(param_type, value, context, values)? { + return Ok(value); + } + for variant in param_type.variants() { + if let Some(coerced) = eval_method_parameter_scalar_coercion(variant, value, values)? { + return Ok(coerced); + } + } + Err(EvalStatus::RuntimeFatal) +} + +/// Returns whether a value satisfies one eval parameter type without scalar coercion. +fn eval_method_parameter_type_accepts_exact( + param_type: &EvalParameterType, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + if tag == EVAL_TAG_NULL && param_type.allows_null() { + return Ok(true); + } + for variant in param_type.variants() { + if eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { + return Ok(true); + } + } + Ok(false) +} + +/// Returns whether a value exactly satisfies one non-null eval parameter type atom. +fn eval_method_parameter_variant_accepts_exact( + variant: &EvalParameterTypeVariant, + value: RuntimeCellHandle, + tag: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match variant { + EvalParameterTypeVariant::Array => Ok(matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC)), + EvalParameterTypeVariant::Bool => Ok(tag == EVAL_TAG_BOOL), + EvalParameterTypeVariant::Callable => Ok(matches!( + tag, + EVAL_TAG_STRING | EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT + )), + EvalParameterTypeVariant::Class(class_name) => { + eval_method_parameter_class_accepts(value, tag, class_name, context, values) + } + EvalParameterTypeVariant::Float => Ok(tag == EVAL_TAG_FLOAT), + EvalParameterTypeVariant::Int => Ok(tag == EVAL_TAG_INT), + EvalParameterTypeVariant::Iterable => { + if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Ok(true); + } + if eval_method_parameter_class_accepts(value, tag, "Traversable", context, values)? { + return Ok(true); + } + eval_method_parameter_class_accepts(value, tag, "Iterator", context, values) + } + EvalParameterTypeVariant::Mixed => Ok(true), + EvalParameterTypeVariant::Object => Ok(tag == EVAL_TAG_OBJECT), + EvalParameterTypeVariant::String => Ok(tag == EVAL_TAG_STRING), + } +} + +/// Returns whether an object value satisfies one class/interface parameter target. +fn eval_method_parameter_class_accepts( + value: RuntimeCellHandle, + tag: u64, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if tag != EVAL_TAG_OBJECT { + return Ok(false); + } + let target = eval_method_parameter_runtime_class_name(class_name, context)?; + let identity = values.object_identity(value)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(context.class_is_a(class.name(), &target, false)); + } + values.object_is_a(value, &target, false) +} + +/// Resolves late-bound class keywords inside eval method parameter type checks. +fn eval_method_parameter_runtime_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "static" => context + .current_class_scope() + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "parent" => { + let current = context + .current_class_scope() + .ok_or(EvalStatus::RuntimeFatal)?; + context + .class(current) + .and_then(EvalClass::parent) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal) + } + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Applies PHP weak-mode scalar coercion for supported scalar parameter types. +fn eval_method_parameter_scalar_coercion( + variant: &EvalParameterTypeVariant, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let tag = values.type_tag(value)?; + match variant { + EvalParameterTypeVariant::Bool if eval_method_scalar_coercible_tag(tag) => { + values.cast_bool(value).map(Some) + } + EvalParameterTypeVariant::Float if eval_method_numeric_coercible_value(value, tag, values)? => { + values.cast_float(value).map(Some) + } + EvalParameterTypeVariant::Int if eval_method_numeric_coercible_value(value, tag, values)? => { + values.cast_int(value).map(Some) + } + EvalParameterTypeVariant::String if eval_method_scalar_coercible_tag(tag) => { + values.cast_string(value).map(Some) + } + _ => Ok(None), + } +} + +/// Returns whether a runtime tag can be weakly coerced to string/bool parameters. +fn eval_method_scalar_coercible_tag(tag: u64) -> bool { + matches!( + tag, + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_STRING | EVAL_TAG_BOOL + ) +} + +/// Returns whether a runtime value can be weakly coerced to a numeric parameter. +fn eval_method_numeric_coercible_value( + value: RuntimeCellHandle, + tag: u64, + values: &mut impl RuntimeValueOps, +) -> Result { + match tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL => Ok(true), + EVAL_TAG_STRING => Ok(eval_is_numeric_string(&values.string_bytes(value)?)), + _ => Ok(false), + } +} + /// Materializes a supported eval method parameter default expression. fn eval_method_parameter_default( default: &EvalExpr, diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index 0551c5018a..5c5c34bd9b 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -34,8 +34,9 @@ use crate::eval_ir::{ EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, - EvalInterfaceProperty, EvalMagicConst, EvalMatchArm, EvalProgram, EvalStmt, EvalSwitchCase, - EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, + EvalInterfaceProperty, EvalMagicConst, EvalMatchArm, EvalParameterType, + EvalParameterTypeVariant, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, + EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; use crate::parser::parse_fragment; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 471cb1fbb0..cb84f28f51 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2264,9 +2264,11 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( ) -> Result { let evaluated_args = bind_evaluated_method_args( method.params(), + method.parameter_types(), method.parameter_defaults(), method.parameter_is_variadic(), evaluated_args, + context, values, )?; let mut method_scope = ElephcEvalScope::new(); @@ -2314,9 +2316,11 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( ) -> Result { let evaluated_args = bind_evaluated_method_args( method.params(), + method.parameter_types(), method.parameter_defaults(), method.parameter_is_variadic(), evaluated_args, + context, values, )?; let mut method_scope = ElephcEvalScope::new(); diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index 9aecd29975..c7da8bf566 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -146,6 +146,99 @@ return $box->read(right: "B");"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval-declared method scalar type hints coerce weak scalar arguments. +#[test] +fn execute_program_enforces_eval_method_scalar_type_hints() { + let program = parse_fragment( + br#"class EvalTypedScalarBox { + public function read(int $id, string $label, bool $flag) { + echo $id + 1; echo ":"; + echo $label; echo ":"; + return $flag ? "T" : "F"; + } +} +$box = new EvalTypedScalarBox(); +return $box->read("7", 8, 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:8:"); + assert_eq!(values.get(result), FakeValue::String("T".to_string())); +} + +/// Verifies eval-declared method scalar type hints reject non-coercible values. +#[test] +fn execute_program_rejects_eval_method_scalar_type_mismatch() { + let program = parse_fragment( + br#"class EvalTypedScalarFailBox { + public function read(int $id) { + return $id; + } +} +$box = new EvalTypedScalarFailBox(); +return $box->read("not numeric");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("non-numeric string should fail int parameter type"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared method class/interface type hints accept matching eval objects. +#[test] +fn execute_program_enforces_eval_method_object_type_hints() { + let program = parse_fragment( + br#"interface EvalTypedReadable {} +class EvalTypedDep implements EvalTypedReadable {} +class EvalTypedObjectBox { + public function read(EvalTypedReadable $dep, ?EvalTypedDep $nullable) { + echo get_class($dep); echo ":"; + return $nullable === null ? "N" : "bad"; + } +} +$dep = new EvalTypedDep(); +$box = new EvalTypedObjectBox(); +return $box->read($dep, null);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalTypedDep:"); + assert_eq!(values.get(result), FakeValue::String("N".to_string())); +} + +/// Verifies eval-declared variadic method type hints apply to each captured argument. +#[test] +fn execute_program_enforces_eval_method_variadic_type_hints() { + let program = parse_fragment( + br#"class EvalTypedVariadicBox { + public function sum(int ...$items) { + return $items[0] + $items[1]; + } +} +$box = new EvalTypedVariadicBox(); +return $box->sum("3", 4);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + /// Verifies eval-declared methods reject unknown named arguments. #[test] fn execute_program_rejects_unknown_eval_method_named_arg() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 73efc7f931..020de19602 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -14,8 +14,9 @@ use crate::errors::EvalParseError; use crate::eval_ir::{ EvalAttribute, EvalAttributeArg, EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, - EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalStmt, EvalSwitchCase, EvalTrait, - EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, + EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, + EvalParameterTypeVariant, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, + EvalUnaryOp, EvalVisibility, }; use crate::lexer::TokenKind; @@ -753,7 +754,7 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let (params, parameter_has_types, parameter_defaults, parameter_is_variadic) = + let (params, parameter_types, parameter_defaults, parameter_is_variadic) = self.parse_method_params()?; let body = if is_abstract { self.expect_semicolon()?; @@ -770,7 +771,7 @@ impl Parser { params, body, ) - .with_parameter_type_flags(parameter_has_types) + .with_parameter_types(parameter_types) .with_parameter_defaults(parameter_defaults) .with_parameter_variadic_flags(parameter_is_variadic)) } @@ -1240,11 +1241,11 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let (params, parameter_has_types, parameter_defaults, parameter_is_variadic) = + let (params, parameter_types, parameter_defaults, parameter_is_variadic) = self.parse_method_params()?; self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) - .with_parameter_type_flags(parameter_has_types) + .with_parameter_types(parameter_types) .with_parameter_defaults(parameter_defaults) .with_parameter_variadic_flags(parameter_is_variadic)) } @@ -1660,27 +1661,35 @@ impl Parser { /// Parses a method parameter list and records type/default metadata. pub(super) fn parse_method_params( &mut self, - ) -> Result<(Vec, Vec, Vec>, Vec), EvalParseError> { + ) -> Result< + ( + Vec, + Vec>, + Vec>, + Vec, + ), + EvalParseError, + > { let mut params = Vec::new(); - let mut parameter_has_types = Vec::new(); + let mut parameter_types = Vec::new(); let mut parameter_defaults = Vec::new(); let mut parameter_is_variadic = Vec::new(); if self.consume(TokenKind::RParen) { return Ok(( params, - parameter_has_types, + parameter_types, parameter_defaults, parameter_is_variadic, )); } loop { - let has_type = self.parse_optional_parameter_type()?; + let param_type = self.parse_optional_parameter_type()?; let is_variadic = self.consume(TokenKind::Ellipsis); let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; params.push(name.clone()); - parameter_has_types.push(has_type); + parameter_types.push(param_type); parameter_is_variadic.push(is_variadic); self.advance(); let default = if self.consume(TokenKind::Equal) { @@ -1709,45 +1718,90 @@ impl Parser { self.expect(TokenKind::RParen)?; Ok(( params, - parameter_has_types, + parameter_types, parameter_defaults, parameter_is_variadic, )) } - /// Consumes a supported method parameter type and reports whether one existed. - fn parse_optional_parameter_type(&mut self) -> Result { + /// Consumes a supported method parameter type and returns retained metadata. + fn parse_optional_parameter_type( + &mut self, + ) -> Result, EvalParseError> { if matches!( self.current(), TokenKind::DollarIdent(_) | TokenKind::Ellipsis ) { - return Ok(false); + return Ok(None); } let nullable_shorthand = self.consume(TokenKind::Question); if nullable_shorthand && matches!(self.current(), TokenKind::DollarIdent(_)) { return Err(EvalParseError::UnexpectedToken); } - self.parse_parameter_type_name()?; + let first = self.parse_parameter_type_name()?; + let mut variants = Vec::new(); + let mut allows_null = nullable_shorthand || matches!(first, None); + if let Some(first) = first { + variants.push(first); + } if nullable_shorthand && matches!(self.current(), TokenKind::Pipe) { return Err(EvalParseError::UnsupportedConstruct); } while self.consume(TokenKind::Pipe) { - self.parse_parameter_type_name()?; + match self.parse_parameter_type_name()? { + Some(variant) => variants.push(variant), + None => allows_null = true, + } } - Ok(true) + Ok(Some(EvalParameterType::new(variants, allows_null))) } /// Consumes one simple qualified method parameter type name. - fn parse_parameter_type_name(&mut self) -> Result<(), EvalParseError> { + fn parse_parameter_type_name( + &mut self, + ) -> Result, EvalParseError> { match self.current() { TokenKind::Ident(_) | TokenKind::Backslash => { - let _ = self.parse_qualified_name()?; - Ok(()) + let name = self.parse_qualified_name()?; + self.parameter_type_from_name(name) } _ => Err(EvalParseError::UnexpectedToken), } } + /// Converts one parsed PHP parameter type name to retained eval metadata. + fn parameter_type_from_name( + &self, + name: ParsedQualifiedName, + ) -> Result, EvalParseError> { + if !name.absolute { + let lower = name.name.to_ascii_lowercase(); + let builtin = match lower.as_str() { + "array" => Some(EvalParameterTypeVariant::Array), + "bool" => Some(EvalParameterTypeVariant::Bool), + "callable" => Some(EvalParameterTypeVariant::Callable), + "float" => Some(EvalParameterTypeVariant::Float), + "int" => Some(EvalParameterTypeVariant::Int), + "iterable" => Some(EvalParameterTypeVariant::Iterable), + "mixed" => Some(EvalParameterTypeVariant::Mixed), + "null" => return Ok(None), + "object" => Some(EvalParameterTypeVariant::Object), + "string" => Some(EvalParameterTypeVariant::String), + "void" | "never" => return Err(EvalParseError::UnsupportedConstruct), + "self" | "parent" | "static" => { + Some(EvalParameterTypeVariant::Class(lower.to_string())) + } + _ => None, + }; + if let Some(builtin) = builtin { + return Ok(Some(builtin)); + } + } + Ok(Some(EvalParameterTypeVariant::Class( + self.resolve_class_name(name), + ))) + } + /// Parses the optional first clause of a `for` loop. pub(super) fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { if matches!(self.current(), TokenKind::Semicolon) { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 242178afa4..3b109b4322 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -585,6 +585,22 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { ] ); assert_eq!(method.parameter_has_types(), &[true, true, true, true]); + assert!(method.parameter_types().iter().all(Option::is_some)); + let id_type = method.parameter_types()[0].as_ref().expect("id type"); + assert_eq!(id_type.variants(), &[EvalParameterTypeVariant::Int]); + assert!(!id_type.allows_null()); + let name_type = method.parameter_types()[1].as_ref().expect("name type"); + assert_eq!( + name_type.variants(), + &[EvalParameterTypeVariant::Class("App\\Name".to_string())] + ); + assert!(name_type.allows_null()); + let label_type = method.parameter_types()[2].as_ref().expect("label type"); + assert_eq!( + label_type.variants(), + &[EvalParameterTypeVariant::String] + ); + assert!(label_type.allows_null()); assert!(matches!( method.parameter_defaults(), [ diff --git a/docs/php/eval.md b/docs/php/eval.md index 8a71a6dd24..0b3c009808 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -174,6 +174,10 @@ visible member metadata, including supported member attributes and predicate flags. `ReflectionClass::newInstance()` constructs eval-declared reflected classes and forwards constructor arguments through eval's positional, named, and unpacking-aware call binding. +Eval-declared method parameter type hints are checked when the method is +entered. Supported checks include scalar hints with PHP-style weak scalar +coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and +eval/runtime class or interface names. `ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()` report eval method metadata. `ReflectionMethod::getParameters()` returns `ReflectionParameter` objects for @@ -351,10 +355,9 @@ Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer ReflectionParameter metadata for by-reference eval-declared parameters, -enforcement of eval-declared method parameter type hints, broader default-value -expression support beyond scalar literals, and broader generated/AOT method -bridge signatures beyond the current public non-by-reference fixed scalar/Mixed -slice. +broader default-value expression support beyond scalar literals, and broader +generated/AOT method bridge signatures beyond the current public +non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 39d629883b..a07b976aa4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5364,6 +5364,32 @@ echo EvalVariadicMethodBox::join("G", "H");'); assert_eq!(out.stdout, "3:AB:C:D:E:F:GH"); } +/// Verifies eval-declared method parameter type hints are enforced through the bridge. +#[test] +fn test_eval_declared_method_parameter_type_hints() { + let out = compile_and_run_capture( + r#"read($dep, "3", 4);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalTypedDep:7"); +} + /// Verifies eval dynamic static callables dispatch eval-declared static methods. #[test] fn test_eval_declared_static_method_dynamic_callables() { From 0f00cb00d1be2c6429648e7add1632b94c65cb01 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 03:07:54 +0200 Subject: [PATCH 0377/1208] Support eval method by-reference parameters --- crates/elephc-eval/src/eval_ir.rs | 28 ++ crates/elephc-eval/src/interpreter/control.rs | 19 ++ .../src/interpreter/dynamic_functions.rs | 130 +++++++-- crates/elephc-eval/src/interpreter/mod.rs | 4 +- .../elephc-eval/src/interpreter/reflection.rs | 6 +- .../elephc-eval/src/interpreter/statements.rs | 249 +++++++++++++++++- .../tests/builtins_class_metadata.rs | 4 +- .../src/interpreter/tests/expressions.rs | 56 ++++ .../src/interpreter/tests/method_arguments.rs | 108 ++++++++ crates/elephc-eval/src/parser/statements.rs | 26 +- .../src/parser/tests/classes_errors.rs | 10 +- docs/php/eval.md | 18 +- tests/codegen/eval.rs | 34 +++ 13 files changed, 649 insertions(+), 43 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 134bbef841..cdc7564be9 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -572,6 +572,7 @@ pub struct EvalInterfaceMethod { parameter_has_types: Vec, parameter_types: Vec>, parameter_defaults: Vec>, + parameter_is_by_ref: Vec, parameter_is_variadic: Vec, } @@ -581,6 +582,7 @@ impl EvalInterfaceMethod { let parameter_has_types = vec![false; params.len()]; let parameter_types = vec![None; params.len()]; let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), @@ -589,6 +591,7 @@ impl EvalInterfaceMethod { parameter_has_types, parameter_types, parameter_defaults, + parameter_is_by_ref, parameter_is_variadic, } } @@ -618,6 +621,12 @@ impl EvalInterfaceMethod { self } + /// Returns a copy of this interface method with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + /// Returns a copy of this interface method with source-order variadic flags. pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { self.parameter_is_variadic = parameter_is_variadic; @@ -654,6 +663,11 @@ impl EvalInterfaceMethod { &self.parameter_defaults } + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + /// Returns source-order flags for whether each parameter was declared variadic. pub fn parameter_is_variadic(&self) -> &[bool] { &self.parameter_is_variadic @@ -1312,6 +1326,7 @@ pub struct EvalClassMethod { parameter_has_types: Vec, parameter_types: Vec>, parameter_defaults: Vec>, + parameter_is_by_ref: Vec, parameter_is_variadic: Vec, body: Vec, } @@ -1354,6 +1369,7 @@ impl EvalClassMethod { let parameter_has_types = vec![false; params.len()]; let parameter_types = vec![None; params.len()]; let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), @@ -1366,6 +1382,7 @@ impl EvalClassMethod { parameter_has_types, parameter_types, parameter_defaults, + parameter_is_by_ref, parameter_is_variadic, body, } @@ -1401,6 +1418,12 @@ impl EvalClassMethod { self } + /// Returns a copy of this method with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + /// Returns a copy of this method with source-order variadic flags. pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { self.parameter_is_variadic = parameter_is_variadic; @@ -1466,6 +1489,11 @@ impl EvalClassMethod { &self.parameter_defaults } + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + /// Returns source-order flags for whether each parameter was declared variadic. pub fn parameter_is_variadic(&self) -> &[bool] { &self.parameter_is_variadic diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-eval/src/interpreter/control.rs index 752597a0b0..e2b2f5d2c6 100644 --- a/crates/elephc-eval/src/interpreter/control.rs +++ b/crates/elephc-eval/src/interpreter/control.rs @@ -8,6 +8,7 @@ //! Key details: //! - Runtime cells are opaque handles; these types do not own or release values by themselves. +use crate::scope::ElephcEvalScope; use crate::value::RuntimeCellHandle; /// Internal statement-control result used to propagate eval returns and loops. @@ -30,6 +31,24 @@ pub enum EvalOutcome { pub(super) struct EvaluatedCallArg { pub(super) name: Option, pub(super) value: RuntimeCellHandle, + pub(super) ref_target: Option, +} + +/// Caller-side storage target for an argument that can satisfy a by-reference parameter. +#[derive(Clone)] +pub(super) enum EvaluatedCallRefTarget { + Variable { + scope: *mut ElephcEvalScope, + name: String, + }, +} + +/// One method argument after PHP parameter-order binding and default materialization. +#[derive(Clone)] +pub(super) struct BoundMethodArg { + pub(super) value: RuntimeCellHandle, + pub(super) ref_target: Option, + pub(super) variadic_ref_targets: Vec<(RuntimeCellHandle, EvaluatedCallRefTarget)>, } /// One already evaluated PHP callback supported by the eval dispatcher. diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index cb89c88f9c..eccbcf205c 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -60,10 +60,12 @@ pub(in crate::interpreter) fn eval_call_arg_values( if let Some(name) = arg.name() { saw_named = true; + let ref_target = eval_call_ref_target(arg.value(), caller_scope); let value = eval_expr(arg.value(), context, caller_scope, values)?; evaluated_args.push(EvaluatedCallArg { name: Some(name.to_string()), value, + ref_target, }); continue; } @@ -71,13 +73,32 @@ pub(in crate::interpreter) fn eval_call_arg_values( if saw_named { return Err(EvalStatus::RuntimeFatal); } + let ref_target = eval_call_ref_target(arg.value(), caller_scope); let value = eval_expr(arg.value(), context, caller_scope, values)?; - evaluated_args.push(EvaluatedCallArg { name: None, value }); + evaluated_args.push(EvaluatedCallArg { + name: None, + value, + ref_target, + }); } Ok(evaluated_args) } +/// Returns a caller-side variable target that can satisfy pass-by-reference method parameters. +fn eval_call_ref_target( + expr: &EvalExpr, + caller_scope: &mut ElephcEvalScope, +) -> Option { + let EvalExpr::LoadVar(name) = expr else { + return None; + }; + Some(EvaluatedCallRefTarget::Variable { + scope: caller_scope as *mut ElephcEvalScope, + name: name.clone(), + }) +} + /// Converts a `call_user_func_array` argument array into ordered call arguments. pub(in crate::interpreter) fn eval_array_call_arg_values( arg_array: RuntimeCellHandle, @@ -106,7 +127,11 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( if *saw_named { return Err(EvalStatus::RuntimeFatal); } - evaluated_args.push(EvaluatedCallArg { name: None, value }); + evaluated_args.push(EvaluatedCallArg { + name: None, + value, + ref_target: None, + }); } EVAL_TAG_STRING => { *saw_named = true; @@ -115,6 +140,7 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( evaluated_args.push(EvaluatedCallArg { name: Some(name), value, + ref_target: None, }); } _ => return Err(EvalStatus::RuntimeFatal), @@ -150,11 +176,12 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( params: &[String], parameter_types: &[Option], parameter_defaults: &[Option], + parameter_is_by_ref: &[bool], parameter_is_variadic: &[bool], evaluated_args: Vec, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let mut bound_args = vec![None; params.len()]; let variadic_index = parameter_is_variadic.iter().position(|is_variadic| *is_variadic); let required_count = @@ -173,7 +200,11 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( } else { values.array_new(evaluated_args.len())? }; - bound_args[index] = Some(array); + bound_args[index] = Some(BoundMethodArg { + value: array, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); } for arg in evaluated_args { @@ -181,10 +212,12 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( bind_dynamic_named_method_arg( params, parameter_types, + parameter_is_by_ref, variadic_index, &mut bound_args, &name, arg.value, + arg.ref_target, &mut variadic_named_args, context, values, @@ -193,10 +226,12 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( bind_dynamic_positional_method_arg( &mut bound_args, parameter_types, + parameter_is_by_ref, variadic_index, &mut next_positional, &mut next_variadic_index, arg.value, + arg.ref_target, context, values, )?; @@ -214,11 +249,15 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( let Some(Some(default)) = parameter_defaults.get(position) else { return Err(EvalStatus::RuntimeFatal); }; - *value = Some(eval_method_parameter_default(default, values)?); + *value = Some(BoundMethodArg { + value: eval_method_parameter_default(default, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); } if let Some(param_type) = parameter_types.get(position).and_then(Option::as_ref) { - let typed = eval_method_parameter_value(param_type, value.unwrap(), context, values)?; - *value = Some(typed); + let bound = value.as_mut().ok_or(EvalStatus::RuntimeFatal)?; + bound.value = eval_method_parameter_value(param_type, bound.value, context, values)?; } } @@ -261,12 +300,14 @@ fn evaluated_args_contain_named_variadic_values( /// Binds one positional method argument to a fixed parameter or variadic array. fn bind_dynamic_positional_method_arg( - bound_args: &mut [Option], + bound_args: &mut [Option], parameter_types: &[Option], + parameter_is_by_ref: &[bool], variadic_index: Option, next_positional: &mut usize, next_variadic_index: &mut i64, value: RuntimeCellHandle, + ref_target: Option, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { @@ -282,19 +323,42 @@ fn bind_dynamic_positional_method_arg( context, values, )?; - return bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, values); + let ref_target = + method_parameter_ref_target(parameter_is_by_ref, variadic_index, ref_target)?; + return bind_dynamic_variadic_arg( + bound_args, + variadic_index, + key, + value, + ref_target, + values, + ); } - bind_dynamic_positional_arg(bound_args, next_positional, value) + let param_index = *next_positional; + if param_index >= bound_args.len() || bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = + method_parameter_ref_target(parameter_is_by_ref, Some(param_index), ref_target)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) } /// Binds one named method argument to a fixed parameter or variadic array. fn bind_dynamic_named_method_arg( params: &[String], parameter_types: &[Option], + parameter_is_by_ref: &[bool], variadic_index: Option, - bound_args: &mut [Option], + bound_args: &mut [Option], name: &str, value: RuntimeCellHandle, + ref_target: Option, variadic_named_args: &mut std::collections::HashSet, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -303,7 +367,13 @@ fn bind_dynamic_named_method_arg( if bound_args[param_index].is_some() { return Err(EvalStatus::RuntimeFatal); } - bound_args[param_index] = Some(value); + let ref_target = + method_parameter_ref_target(parameter_is_by_ref, Some(param_index), ref_target)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); return Ok(()); } if variadic_index.is_none() || !variadic_named_args.insert(name.to_string()) { @@ -317,7 +387,31 @@ fn bind_dynamic_named_method_arg( context, values, )?; - bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, values) + let ref_target = method_parameter_ref_target( + parameter_is_by_ref, + variadic_index, + ref_target, + )?; + bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, ref_target, values) +} + +/// Returns the caller writeback target required by a by-reference method parameter. +fn method_parameter_ref_target( + parameter_is_by_ref: &[bool], + param_index: Option, + ref_target: Option, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !parameter_is_by_ref + .get(param_index) + .copied() + .unwrap_or(false) + { + return Ok(None); + } + ref_target.map(Some).ok_or(EvalStatus::RuntimeFatal) } /// Applies a variadic parameter type to one captured argument value. @@ -350,15 +444,19 @@ fn regular_method_param_index( /// Appends one value into the method variadic array. fn bind_dynamic_variadic_arg( - bound_args: &mut [Option], + bound_args: &mut [Option], variadic_index: Option, key: RuntimeCellHandle, value: RuntimeCellHandle, + ref_target: Option, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; - let array = bound_args[index].ok_or(EvalStatus::RuntimeFatal)?; - bound_args[index] = Some(values.array_set(array, key, value)?); + let bound = bound_args[index].as_mut().ok_or(EvalStatus::RuntimeFatal)?; + bound.value = values.array_set(bound.value, key, value)?; + if let Some(ref_target) = ref_target { + bound.variadic_ref_targets.push((key, ref_target)); + } Ok(()) } diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index 5c5c34bd9b..57ac26eff2 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -48,8 +48,8 @@ use constant_eval::*; use constants::*; pub use control::EvalOutcome; use control::{ - EvalArraySpliceDirectArgs, EvalControl, EvalPredefinedConstant, EvalSprintfSpec, - EvaluatedCallArg, EvaluatedCallable, + BoundMethodArg, EvalArraySpliceDirectArgs, EvalControl, EvalPredefinedConstant, + EvalSprintfSpec, EvaluatedCallArg, EvaluatedCallRefTarget, EvaluatedCallable, }; use core_builtins::*; use debug_output::*; diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index c561cc16eb..3feecc236f 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -609,6 +609,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_defaults(), + method.parameter_is_by_ref(), method.parameter_is_variadic(), ), }); @@ -628,6 +629,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_defaults(), + method.parameter_is_by_ref(), method.parameter_is_variadic(), ), }); @@ -647,6 +649,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_defaults(), + method.parameter_is_by_ref(), method.parameter_is_variadic(), ), }) @@ -706,6 +709,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( names: &[String], has_type_flags: &[bool], defaults: &[Option], + by_ref_flags: &[bool], variadic_flags: &[bool], ) -> Vec { names @@ -717,7 +721,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( is_optional: defaults.get(position).is_some_and(Option::is_some) || variadic_flags.get(position).copied().unwrap_or(false), is_variadic: variadic_flags.get(position).copied().unwrap_or(false), - is_passed_by_reference: false, + is_passed_by_reference: by_ref_flags.get(position).copied().unwrap_or(false), has_type: has_type_flags.get(position).copied().unwrap_or(false), }) .collect() diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index cb84f28f51..d6e767869c 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1077,9 +1077,11 @@ fn class_method_signature_accepts(method: &EvalClassMethod, required: &EvalClass method_signature_accepts( method.params().len(), method.parameter_defaults(), + method.parameter_is_by_ref(), method.parameter_is_variadic(), required.params().len(), required.parameter_defaults(), + required.parameter_is_by_ref(), required.parameter_is_variadic(), ) } @@ -1345,9 +1347,11 @@ fn class_method_satisfies_interface_signature( method_signature_accepts( method.params().len(), method.parameter_defaults(), + method.parameter_is_by_ref(), method.parameter_is_variadic(), requirement.params().len(), requirement.parameter_defaults(), + requirement.parameter_is_by_ref(), requirement.parameter_is_variadic(), ) } @@ -1356,9 +1360,11 @@ fn class_method_satisfies_interface_signature( fn method_signature_accepts( implementation_param_count: usize, implementation_defaults: &[Option], + implementation_by_refs: &[bool], implementation_variadics: &[bool], required_param_count: usize, required_defaults: &[Option], + required_by_refs: &[bool], required_variadics: &[bool], ) -> bool { let implementation_min = method_signature_min_arity( @@ -1375,11 +1381,51 @@ fn method_signature_accepts( let implementation_max = method_signature_max_arity(implementation_param_count, implementation_variadics); let required_max = method_signature_max_arity(required_param_count, required_variadics); - match (implementation_max, required_max) { + let arity_accepted = match (implementation_max, required_max) { (None, _) => true, (Some(_), None) => false, (Some(implementation_max), Some(required_max)) => implementation_max >= required_max, + }; + arity_accepted + && method_signature_by_refs_accept( + implementation_by_refs, + implementation_variadics, + required_param_count, + required_by_refs, + required_variadics, + ) +} + +/// Returns whether pass-by-reference requirements are compatible across accepted args. +fn method_signature_by_refs_accept( + implementation_by_refs: &[bool], + implementation_variadics: &[bool], + required_param_count: usize, + required_by_refs: &[bool], + required_variadics: &[bool], +) -> bool { + (0..required_param_count).all(|position| { + method_signature_effective_by_ref( + implementation_by_refs, + implementation_variadics, + position, + ) + == method_signature_effective_by_ref(required_by_refs, required_variadics, position) + }) +} + +/// Returns the by-reference mode that one signature applies at an argument position. +fn method_signature_effective_by_ref( + by_refs: &[bool], + variadics: &[bool], + position: usize, +) -> bool { + if let Some(variadic_index) = variadics.iter().position(|is_variadic| *is_variadic) { + if position >= variadic_index { + return by_refs.get(variadic_index).copied().unwrap_or(false); + } } + by_refs.get(position).copied().unwrap_or(false) } /// Returns the minimum argument count accepted by one eval method signature. @@ -1533,7 +1579,11 @@ pub(in crate::interpreter) fn eval_property_set_result( &object_class_name, &hook_method, object, - vec![EvaluatedCallArg { name: None, value }], + vec![EvaluatedCallArg { + name: None, + value, + ref_target: None, + }], context, values, )?; @@ -2252,6 +2302,159 @@ fn eval_classes_are_related(left: &str, right: &str, context: &ElephcEvalContext || context.class_is_a(right, left, false) } +/// Binds method parameters into a fresh method scope and marks by-reference params as aliases. +fn bind_method_scope_args( + method_scope: &mut ElephcEvalScope, + params: &[String], + parameter_is_by_ref: &[bool], + bound_args: &[BoundMethodArg], +) { + for (position, (name, bound_arg)) in params.iter().zip(bound_args.iter()).enumerate() { + if parameter_is_by_ref.get(position).copied().unwrap_or(false) { + method_scope.set_reference( + name.clone(), + name.clone(), + bound_arg.value, + ScopeCellOwnership::Borrowed, + ); + } else { + method_scope.set(name.clone(), bound_arg.value, ScopeCellOwnership::Borrowed); + } + } + alias_duplicate_method_ref_args(method_scope, params, bound_args); +} + +/// Creates local aliases when two by-reference method parameters point at the same caller variable. +fn alias_duplicate_method_ref_args( + method_scope: &mut ElephcEvalScope, + params: &[String], + bound_args: &[BoundMethodArg], +) { + for (position, bound_arg) in bound_args.iter().enumerate() { + let Some(target) = bound_arg.ref_target.as_ref() else { + continue; + }; + let Some(param) = params.get(position) else { + continue; + }; + for previous_position in 0..position { + let Some(previous_target) = bound_args[previous_position].ref_target.as_ref() else { + continue; + }; + if !same_method_ref_target(target, previous_target) { + continue; + } + if let Some(previous_param) = params.get(previous_position) { + method_scope.set_reference( + param.clone(), + previous_param.clone(), + bound_args[previous_position].value, + ScopeCellOwnership::Borrowed, + ); + } + break; + } + } +} + +/// Returns true when two evaluated arguments target the same caller-side variable. +fn same_method_ref_target( + left: &EvaluatedCallRefTarget, + right: &EvaluatedCallRefTarget, +) -> bool { + match (left, right) { + ( + EvaluatedCallRefTarget::Variable { + scope: left_scope, + name: left_name, + }, + EvaluatedCallRefTarget::Variable { + scope: right_scope, + name: right_name, + }, + ) => left_scope == right_scope && left_name == right_name, + } +} + +/// Writes completed by-reference method parameter values back to their caller-side variables. +fn write_back_method_ref_args( + params: &[String], + bound_args: &[BoundMethodArg], + method_scope: &ElephcEvalScope, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, bound_arg) in bound_args.iter().enumerate() { + let Some(param) = params.get(position) else { + continue; + }; + if let Some(target) = bound_arg.ref_target.as_ref() { + let Some(entry) = method_scope + .entry(param) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + continue; + }; + write_back_method_ref_target(target, entry.cell(), context, values)?; + } + write_back_method_variadic_ref_args(param, bound_arg, method_scope, context, values)?; + } + Ok(()) +} + +/// Writes element-level changes from a by-reference variadic method parameter back to callers. +fn write_back_method_variadic_ref_args( + param: &str, + bound_arg: &BoundMethodArg, + method_scope: &ElephcEvalScope, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if bound_arg.variadic_ref_targets.is_empty() { + return Ok(()); + } + let Some(entry) = method_scope + .entry(param) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + return Ok(()); + }; + if entry.cell() != bound_arg.value { + return Ok(()); + } + for (key, target) in &bound_arg.variadic_ref_targets { + let value = values.array_get(entry.cell(), *key)?; + write_back_method_ref_target(target, value, context, values)?; + } + Ok(()) +} + +/// Stores one by-reference method result in the original caller-side variable. +fn write_back_method_ref_target( + target: &EvaluatedCallRefTarget, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match target { + EvaluatedCallRefTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + for replaced in set_scope_cell( + context, + scope, + name.clone(), + value, + ScopeCellOwnership::Borrowed, + )? { + values.release(replaced)?; + } + Ok(()) + } + } +} + /// Executes one eval-declared class method with `$this` bound in method scope. pub(in crate::interpreter) fn eval_dynamic_method_with_values( class_name: &str, @@ -2266,6 +2469,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( method.params(), method.parameter_types(), method.parameter_defaults(), + method.parameter_is_by_ref(), method.parameter_is_variadic(), evaluated_args, context, @@ -2273,9 +2477,12 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( )?; let mut method_scope = ElephcEvalScope::new(); method_scope.set("this", object, ScopeCellOwnership::Borrowed); - for (name, value) in method.params().iter().zip(evaluated_args) { - method_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); - } + bind_method_scope_args( + &mut method_scope, + method.params(), + method.parameter_is_by_ref(), + &evaluated_args, + ); let qualified_method_name = format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); let static_names = static_var_names(method.body()); @@ -2290,10 +2497,18 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( &method_scope, values, ); + let writeback_result = write_back_method_ref_args( + method.params(), + &evaluated_args, + &method_scope, + context, + values, + ); context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); persist_result?; + writeback_result?; match result? { EvalControl::None => values.null(), EvalControl::Return(result) => Ok(result), @@ -2318,15 +2533,19 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( method.params(), method.parameter_types(), method.parameter_defaults(), + method.parameter_is_by_ref(), method.parameter_is_variadic(), evaluated_args, context, values, )?; let mut method_scope = ElephcEvalScope::new(); - for (name, value) in method.params().iter().zip(evaluated_args) { - method_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); - } + bind_method_scope_args( + &mut method_scope, + method.params(), + method.parameter_is_by_ref(), + &evaluated_args, + ); let qualified_method_name = format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); let static_names = static_var_names(method.body()); @@ -2341,10 +2560,18 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( &method_scope, values, ); + let writeback_result = write_back_method_ref_args( + method.params(), + &evaluated_args, + &method_scope, + context, + values, + ); context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); persist_result?; + writeback_result?; match result? { EvalControl::None => values.null(), EvalControl::Return(result) => Ok(result), @@ -2361,7 +2588,11 @@ pub(in crate::interpreter) fn positional_args( args: Vec, ) -> Vec { args.into_iter() - .map(|value| EvaluatedCallArg { name: None, value }) + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) .collect() } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index b8952cdf4e..faabdb9a98 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -493,7 +493,7 @@ return true;"#, fn execute_program_reflects_eval_method_parameters() { let program = parse_fragment( br##"class EvalReflectParamTarget { - public function run(int $first, \App\Name|null $second = null, ...$rest) {} + public function run(int &$first, \App\Name|null $second = null, &...$rest) {} } $params = (new ReflectionMethod("EvalReflectParamTarget", "run"))->getParameters(); echo count($params); echo ":"; @@ -513,7 +513,7 @@ return true;"##, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:first#0rvbT|second#1OvbT|rest#2OVbt|"); + assert_eq!(values.output, "3:first#0rvRT|second#1OvbT|rest#2OVRt|"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index c5a4ca39bc..826257cf77 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -768,6 +768,62 @@ class EvalMissingRead implements EvalNeedsRead {}"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval interface method contracts require matching by-reference parameters. +#[test] +fn execute_program_validates_interface_method_by_ref_parameters() { + let program = parse_fragment( + br#"interface EvalRefReadable { + function read(&$value); +} +class EvalRefReader implements EvalRefReadable { + public function read(&$value) { + $value = "ok"; + } +} +$value = "bad"; +$reader = new EvalRefReader(); +$reader->read($value); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok".to_string())); + + let bad_value_impl = parse_fragment( + br#"interface EvalNeedsByRef { + function read(&$value); +} +class EvalByValueReader implements EvalNeedsByRef { + public function read($value) {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_value_impl, &mut scope, &mut values) + .expect_err("by-value implementation must not satisfy by-reference contract"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_ref_impl = parse_fragment( + br#"interface EvalNeedsByValue { + function read($value); +} +class EvalByRefReader implements EvalNeedsByValue { + public function read(&$value) {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_ref_impl, &mut scope, &mut values) + .expect_err("by-reference implementation must not satisfy by-value contract"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies variadic eval methods can satisfy fixed-arity interface contracts. #[test] fn execute_program_accepts_variadic_method_for_fixed_interface_contract() { diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index c7da8bf566..7141205d38 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -102,6 +102,114 @@ return EvalVariadicMethodBox::join("G", "H");"#, assert_eq!(values.get(result), FakeValue::String("GH".to_string())); } +/// Verifies eval-declared instance, static, and constructor methods write back by-reference args. +#[test] +fn execute_program_writes_back_eval_method_by_ref_args() { + let program = parse_fragment( + br#"class EvalByRefMethodBox { + public function __construct(&$value) { + $value = $value . "-ctor"; + } + public function change(&$value) { + $value = $value . "-method"; + } + public static function changeStatic(&$value) { + $value = $value . "-static"; + } +} +$ctor = "A"; +$box = new EvalByRefMethodBox($ctor); +$box->change($ctor); +EvalByRefMethodBox::changeStatic($ctor); +$named = "B"; +$box->change(value: $named); +echo $ctor; echo ":"; +return $named;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A-ctor-method-static:"); + assert_eq!(values.get(result), FakeValue::String("B-method".to_string())); +} + +/// Verifies eval-declared by-reference method parameters reject temporary values. +#[test] +fn execute_program_rejects_eval_method_by_ref_temporary_arg() { + let program = parse_fragment( + br#"class EvalByRefMethodBox { + public function change(&$value) { + $value = "changed"; + } +} +$box = new EvalByRefMethodBox(); +$box->change("literal");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a by-reference parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies typed eval-declared by-reference method params write back entry coercions. +#[test] +fn execute_program_writes_back_eval_method_by_ref_type_coercion() { + let program = parse_fragment( + br#"class EvalByRefTypedMethodBox { + public function coerce(int &$value) {} +} +$value = "3"; +$box = new EvalByRefTypedMethodBox(); +$box->coerce($value); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies eval-declared by-reference variadics write back mutated captured elements. +#[test] +fn execute_program_writes_back_eval_method_by_ref_variadic_elements() { + let program = parse_fragment( + br#"class EvalByRefVariadicMethodBox { + public function change(&...$items) { + $items[0] = $items[0] . "-first"; + $items["named"] = $items["named"] . "-named"; + } + public function rebind(&...$items) { + $items = []; + } +} +$box = new EvalByRefVariadicMethodBox(); +$first = "A"; +$named = "B"; +$box->change($first, named: $named); +$box->rebind($first); +echo $first; echo ":"; +return $named;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A-first:"); + assert_eq!(values.get(result), FakeValue::String("B-named".to_string())); +} + /// Verifies eval-declared variadic methods reject duplicate named variadic keys. #[test] fn execute_program_rejects_duplicate_eval_method_variadic_named_arg() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 020de19602..2b26291a5a 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -754,7 +754,13 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let (params, parameter_types, parameter_defaults, parameter_is_variadic) = + let ( + params, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + ) = self.parse_method_params()?; let body = if is_abstract { self.expect_semicolon()?; @@ -773,6 +779,7 @@ impl Parser { ) .with_parameter_types(parameter_types) .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) .with_parameter_variadic_flags(parameter_is_variadic)) } @@ -1241,12 +1248,19 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let (params, parameter_types, parameter_defaults, parameter_is_variadic) = + let ( + params, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + ) = self.parse_method_params()?; self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) .with_parameter_types(parameter_types) .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) .with_parameter_variadic_flags(parameter_is_variadic)) } @@ -1667,29 +1681,34 @@ impl Parser { Vec>, Vec>, Vec, + Vec, ), EvalParseError, > { let mut params = Vec::new(); let mut parameter_types = Vec::new(); let mut parameter_defaults = Vec::new(); + let mut parameter_is_by_ref = Vec::new(); let mut parameter_is_variadic = Vec::new(); if self.consume(TokenKind::RParen) { return Ok(( params, parameter_types, parameter_defaults, + parameter_is_by_ref, parameter_is_variadic, )); } loop { let param_type = self.parse_optional_parameter_type()?; + let is_by_ref = self.consume(TokenKind::Ampersand); let is_variadic = self.consume(TokenKind::Ellipsis); let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; params.push(name.clone()); parameter_types.push(param_type); + parameter_is_by_ref.push(is_by_ref); parameter_is_variadic.push(is_variadic); self.advance(); let default = if self.consume(TokenKind::Equal) { @@ -1720,6 +1739,7 @@ impl Parser { params, parameter_types, parameter_defaults, + parameter_is_by_ref, parameter_is_variadic, )) } @@ -1730,7 +1750,7 @@ impl Parser { ) -> Result, EvalParseError> { if matches!( self.current(), - TokenKind::DollarIdent(_) | TokenKind::Ellipsis + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis ) { return Ok(None); } diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 3b109b4322..11a3670468 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -560,12 +560,12 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { ); } -/// Verifies eval method parameters retain declared-type presence for reflection metadata. +/// Verifies eval method parameters retain type, default, by-reference, and variadic metadata. #[test] fn parse_fragment_accepts_typed_method_parameter_metadata() { let program = parse_fragment( br#"class DynEvalTypedParams { - public function run(int $id = 7, ?\App\Name $name = null, string|null $label = "x", mixed ...$tail) {} + public function run(int &$id = 7, ?\App\Name &$name = null, string|null $label = "x", mixed &...$tail) {} }"#, ) .expect("fragment should parse"); @@ -610,6 +610,10 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { None ] if label == "x" )); + assert_eq!( + method.parameter_is_by_ref(), + &[true, true, false, true] + ); assert_eq!(method.parameter_is_variadic(), &[false, false, false, true]); } @@ -620,6 +624,8 @@ fn parse_fragment_rejects_invalid_variadic_method_parameters() { .expect_err("variadic method parameters cannot have defaults"); parse_fragment(b"class DynEvalVariadicNotLast { public function run(...$tail, $next) {} }") .expect_err("variadic method parameters must be last"); + parse_fragment(b"class DynEvalVariadicRefOrder { public function run(...&$tail) {} }") + .expect_err("by-reference marker must precede variadic marker"); } /// Verifies trait declarations and class trait uses lower into dynamic metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index 0b3c009808..551a3a5afa 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -187,9 +187,11 @@ type hints. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`. Variadic eval method parameters bind extra positional and unknown named arguments into a PHP array and are reported through `ReflectionParameter::isVariadic()` and -`ReflectionParameter::isOptional()`. By-reference flags remain false until -eval's method-parameter parser and call binder carry by-reference parameter -forms. +`ReflectionParameter::isOptional()`. By-reference eval method parameters accept +direct variable arguments, write back fixed parameters after method execution, +write back mutated `&...$items` elements when the variadic container itself is +not rebound, and are reported through +`ReflectionParameter::isPassedByReference()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()` report eval property metadata. `ReflectionClassConstant::getAttributes()`, @@ -354,10 +356,10 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter metadata for by-reference eval-declared parameters, -broader default-value expression support beyond scalar literals, and broader -generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed slice. +ReflectionParameter type metadata beyond presence flags, by-reference method +arguments for property/array-element lvalues, broader default-value expression +support beyond scalar literals, and broader generated/AOT method bridge +signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a07b976aa4..c71476d0cb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5390,6 +5390,40 @@ echo $box->read($dep, "3", 4);'); assert_eq!(out.stdout, "EvalTypedDep:7"); } +/// Verifies eval-declared methods write back by-reference arguments through compiled eval calls. +#[test] +fn test_eval_declared_method_by_ref_arguments() { + let out = compile_and_run_capture( + r#"change($value); +EvalByRefMethodBox::changeStatic($value); +$named = "B"; +$box->changeVariadic($value, named: $named); +echo $value . ":" . $named;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "A-method-static-variadic:B-named"); +} + /// Verifies eval dynamic static callables dispatch eval-declared static methods. #[test] fn test_eval_declared_static_method_dynamic_callables() { From 0393986123b457c13829393470ca64127af87ced Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 03:11:58 +0200 Subject: [PATCH 0378/1208] Support eval array element ref method args --- crates/elephc-eval/src/interpreter/control.rs | 5 ++ .../src/interpreter/dynamic_functions.rs | 53 +++++++++---- .../elephc-eval/src/interpreter/statements.rs | 79 +++++++++++++++++++ .../src/interpreter/tests/method_arguments.rs | 30 +++++++ docs/php/eval.md | 14 ++-- tests/codegen/eval.rs | 6 +- 6 files changed, 164 insertions(+), 23 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-eval/src/interpreter/control.rs index e2b2f5d2c6..52327ced9f 100644 --- a/crates/elephc-eval/src/interpreter/control.rs +++ b/crates/elephc-eval/src/interpreter/control.rs @@ -41,6 +41,11 @@ pub(super) enum EvaluatedCallRefTarget { scope: *mut ElephcEvalScope, name: String, }, + ArrayElement { + scope: *mut ElephcEvalScope, + array_name: String, + index: RuntimeCellHandle, + }, } /// One method argument after PHP parameter-order binding and default materialization. diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index eccbcf205c..c03c501de1 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -60,8 +60,8 @@ pub(in crate::interpreter) fn eval_call_arg_values( if let Some(name) = arg.name() { saw_named = true; - let ref_target = eval_call_ref_target(arg.value(), caller_scope); - let value = eval_expr(arg.value(), context, caller_scope, values)?; + let (value, ref_target) = + eval_call_arg_value(arg.value(), context, caller_scope, values)?; evaluated_args.push(EvaluatedCallArg { name: Some(name.to_string()), value, @@ -73,8 +73,7 @@ pub(in crate::interpreter) fn eval_call_arg_values( if saw_named { return Err(EvalStatus::RuntimeFatal); } - let ref_target = eval_call_ref_target(arg.value(), caller_scope); - let value = eval_expr(arg.value(), context, caller_scope, values)?; + let (value, ref_target) = eval_call_arg_value(arg.value(), context, caller_scope, values)?; evaluated_args.push(EvaluatedCallArg { name: None, value, @@ -85,18 +84,44 @@ pub(in crate::interpreter) fn eval_call_arg_values( Ok(evaluated_args) } -/// Returns a caller-side variable target that can satisfy pass-by-reference method parameters. -fn eval_call_ref_target( +/// Evaluates one call arg and captures caller-side storage for by-reference parameters. +fn eval_call_arg_value( expr: &EvalExpr, + context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, -) -> Option { - let EvalExpr::LoadVar(name) = expr else { - return None; - }; - Some(EvaluatedCallRefTarget::Variable { - scope: caller_scope as *mut ElephcEvalScope, - name: name.clone(), - }) + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + match expr { + EvalExpr::LoadVar(name) => { + let value = visible_scope_cell(context, caller_scope, name) + .map_or_else(|| values.null(), Ok)?; + Ok(( + value, + Some(EvaluatedCallRefTarget::Variable { + scope: caller_scope as *mut ElephcEvalScope, + name: name.clone(), + }), + )) + } + EvalExpr::ArrayGet { array, index } => { + let EvalExpr::LoadVar(array_name) = array.as_ref() else { + return eval_expr(expr, context, caller_scope, values).map(|value| (value, None)); + }; + let array = visible_scope_cell(context, caller_scope, array_name) + .map_or_else(|| values.null(), Ok)?; + let index = eval_expr(index, context, caller_scope, values)?; + let value = values.array_get(array, index)?; + Ok(( + value, + Some(EvaluatedCallRefTarget::ArrayElement { + scope: caller_scope as *mut ElephcEvalScope, + array_name: array_name.clone(), + index, + }), + )) + } + _ => eval_expr(expr, context, caller_scope, values).map(|value| (value, None)), + } } /// Converts a `call_user_func_array` argument array into ordered call arguments. diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index d6e767869c..c6a9186feb 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2373,6 +2373,21 @@ fn same_method_ref_target( name: right_name, }, ) => left_scope == right_scope && left_name == right_name, + ( + EvaluatedCallRefTarget::ArrayElement { + scope: left_scope, + array_name: left_name, + index: left_index, + }, + EvaluatedCallRefTarget::ArrayElement { + scope: right_scope, + array_name: right_name, + index: right_index, + }, + ) => { + left_scope == right_scope && left_name == right_name && left_index == right_index + } + _ => false, } } @@ -2452,6 +2467,70 @@ fn write_back_method_ref_target( } Ok(()) } + EvaluatedCallRefTarget::ArrayElement { + scope, + array_name, + index, + } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + write_back_method_array_element_ref_target( + scope, + array_name, + *index, + value, + context, + values, + ) + } + } +} + +/// Stores one by-reference method result in a caller-side array element. +fn write_back_method_array_element_ref_target( + scope: &mut ElephcEvalScope, + array_name: &str, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some(existing) = + scope_entry(context, scope, array_name).filter(|entry| entry.flags().is_visible()) + { + if values.is_array_like(existing.cell())? { + ownership = existing.flags().ownership; + existing.cell() + } else { + eval_new_array_for_index(index, values)? + } + } else { + eval_new_array_for_index(index, values)? + }; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell( + context, + scope, + array_name.to_string(), + array, + ownership, + )? { + values.release(replaced)?; + } + Ok(()) +} + +/// Creates an indexed or associative array according to the first write key. +fn eval_new_array_for_index( + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(index)? == EVAL_TAG_STRING { + values.assoc_new(1) + } else { + values.array_new(1) } } diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index 7141205d38..9ba9693f08 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -210,6 +210,36 @@ return $named;"#, assert_eq!(values.get(result), FakeValue::String("B-named".to_string())); } +/// Verifies eval-declared by-reference method params write back array-element lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_array_elements() { + let program = parse_fragment( + br#"class EvalByRefArrayElementMethodBox { + public function set(&$value, $next) { + $value = $next; + } + public function variadic(&...$items) { + $items[0] = "variadic"; + } +} +$box = new EvalByRefArrayElementMethodBox(); +$items = ["k" => "old"]; +$box->set($items["k"], "changed"); +$box->set($missing["new"], "created"); +$box->variadic($items["k"]); +echo $items["k"]; echo ":"; +return $missing["new"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "variadic:"); + assert_eq!(values.get(result), FakeValue::String("created".to_string())); +} + /// Verifies eval-declared variadic methods reject duplicate named variadic keys. #[test] fn execute_program_rejects_duplicate_eval_method_variadic_named_arg() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 551a3a5afa..6b5e697945 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable and array-element arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -188,9 +188,9 @@ through `ReflectionParameter::isOptional()`. Variadic eval method parameters bind extra positional and unknown named arguments into a PHP array and are reported through `ReflectionParameter::isVariadic()` and `ReflectionParameter::isOptional()`. By-reference eval method parameters accept -direct variable arguments, write back fixed parameters after method execution, -write back mutated `&...$items` elements when the variadic container itself is -not rebound, and are reported through +direct variable and array-element arguments, write back fixed parameters after +method execution, write back mutated `&...$items` elements when the variadic +container itself is not rebound, and are reported through `ReflectionParameter::isPassedByReference()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()` report eval property metadata. @@ -357,9 +357,9 @@ Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer ReflectionParameter type metadata beyond presence flags, by-reference method -arguments for property/array-element lvalues, broader default-value expression -support beyond scalar literals, and broader generated/AOT method bridge -signatures beyond the current public non-by-reference fixed scalar/Mixed slice. +arguments for object-property lvalues, broader default-value expression support +beyond scalar literals, and broader generated/AOT method bridge signatures +beyond the current public non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c71476d0cb..ec65fdd779 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5413,7 +5413,9 @@ $box->change($value); EvalByRefMethodBox::changeStatic($value); $named = "B"; $box->changeVariadic($value, named: $named); -echo $value . ":" . $named;'); +$items = ["k" => "C"]; +$box->change($items["k"]); +echo $value . ":" . $named . ":" . $items["k"];'); "#, ); assert!( @@ -5421,7 +5423,7 @@ echo $value . ":" . $named;'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "A-method-static-variadic:B-named"); + assert_eq!(out.stdout, "A-method-static-variadic:B-named:C-method"); } /// Verifies eval dynamic static callables dispatch eval-declared static methods. From 6ec2cca7b2408171995585c9268fe9e614fc60c8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 03:17:17 +0200 Subject: [PATCH 0379/1208] Support eval property ref method args --- crates/elephc-eval/src/context.rs | 32 +++++++ crates/elephc-eval/src/interpreter/control.rs | 6 ++ .../src/interpreter/dynamic_functions.rs | 14 +++ crates/elephc-eval/src/interpreter/mod.rs | 4 +- .../elephc-eval/src/interpreter/statements.rs | 61 ++++++++++++- .../src/interpreter/tests/method_arguments.rs | 86 +++++++++++++++++++ docs/php/eval.md | 16 ++-- tests/codegen/eval.rs | 9 +- 8 files changed, 214 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index dabfc3f0c2..5b77d9260c 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -28,6 +28,14 @@ use crate::value::{RuntimeCell, RuntimeCellHandle}; pub type NativeFunctionInvoker = unsafe extern "C" fn(*mut c_void, *mut RuntimeCell) -> *mut RuntimeCell; +/// Snapshot of eval execution stacks used to restore caller-sensitive access checks. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ElephcEvalExecutionScope { + function_stack: Vec, + class_stack: Vec, + called_class_stack: Vec, +} + /// Native AOT function callback metadata visible to runtime eval fragments. #[derive(Clone)] pub struct NativeFunction { @@ -1310,6 +1318,30 @@ impl ElephcEvalContext { self.called_class_stack.last().map(String::as_str) } + /// Captures the current eval execution stacks for later caller-context-sensitive work. + pub fn execution_scope(&self) -> ElephcEvalExecutionScope { + ElephcEvalExecutionScope { + function_stack: self.function_stack.clone(), + class_stack: self.class_stack.clone(), + called_class_stack: self.called_class_stack.clone(), + } + } + + /// Replaces eval execution stacks and returns the previous stacks for restoration. + pub fn replace_execution_scope( + &mut self, + scope: ElephcEvalExecutionScope, + ) -> ElephcEvalExecutionScope { + ElephcEvalExecutionScope { + function_stack: std::mem::replace(&mut self.function_stack, scope.function_stack), + class_stack: std::mem::replace(&mut self.class_stack, scope.class_stack), + called_class_stack: std::mem::replace( + &mut self.called_class_stack, + scope.called_class_stack, + ), + } + } + /// Records a Throwable cell that escaped from an eval-executed function call. pub fn set_pending_throw(&mut self, value: RuntimeCellHandle) { self.pending_throw = Some(value); diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-eval/src/interpreter/control.rs index 52327ced9f..4daf5b0ad0 100644 --- a/crates/elephc-eval/src/interpreter/control.rs +++ b/crates/elephc-eval/src/interpreter/control.rs @@ -8,6 +8,7 @@ //! Key details: //! - Runtime cells are opaque handles; these types do not own or release values by themselves. +use crate::context::ElephcEvalExecutionScope; use crate::scope::ElephcEvalScope; use crate::value::RuntimeCellHandle; @@ -46,6 +47,11 @@ pub(super) enum EvaluatedCallRefTarget { array_name: String, index: RuntimeCellHandle, }, + ObjectProperty { + object: RuntimeCellHandle, + property: String, + access_scope: ElephcEvalExecutionScope, + }, } /// One method argument after PHP parameter-order binding and default materialization. diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index c03c501de1..c8d43d5635 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -120,6 +120,20 @@ fn eval_call_arg_value( }), )) } + EvalExpr::PropertyGet { object, property } => { + let access_scope = context.execution_scope(); + let object = eval_expr(object, context, caller_scope, values)?; + let value = eval_property_get_result(object, property, context, values)?; + validate_property_ref_target(object, property, context, values)?; + Ok(( + value, + Some(EvaluatedCallRefTarget::ObjectProperty { + object, + property: property.clone(), + access_scope, + }), + )) + } _ => eval_expr(expr, context, caller_scope, values).map(|value| (value, None)), } } diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index 57ac26eff2..f797eed058 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -28,7 +28,9 @@ mod runtime_ops; mod scope_cells; mod statements; -use crate::context::{ElephcEvalContext, NativeCallableSignature, NativeFunction}; +use crate::context::{ + ElephcEvalContext, ElephcEvalExecutionScope, NativeCallableSignature, NativeFunction, +}; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index c6a9186feb..c72dd07abf 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1597,6 +1597,34 @@ pub(in crate::interpreter) fn eval_property_set_result( values.property_set(object, property_name, value) } +/// Validates that an object property may be used as a by-reference method argument. +pub(in crate::interpreter) fn validate_property_ref_target( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(()); + }; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + if property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + /// Returns true while executing the named hook accessor for one property. fn current_eval_property_hook_is( declaring_class: &str, @@ -2396,7 +2424,7 @@ fn write_back_method_ref_args( params: &[String], bound_args: &[BoundMethodArg], method_scope: &ElephcEvalScope, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { for (position, bound_arg) in bound_args.iter().enumerate() { @@ -2422,7 +2450,7 @@ fn write_back_method_variadic_ref_args( param: &str, bound_arg: &BoundMethodArg, method_scope: &ElephcEvalScope, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if bound_arg.variadic_ref_targets.is_empty() { @@ -2448,7 +2476,7 @@ fn write_back_method_variadic_ref_args( fn write_back_method_ref_target( target: &EvaluatedCallRefTarget, value: RuntimeCellHandle, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { match target { @@ -2484,6 +2512,18 @@ fn write_back_method_ref_target( values, ) } + EvaluatedCallRefTarget::ObjectProperty { + object, + property, + access_scope, + } => write_back_method_object_property_ref_target( + *object, + property, + access_scope.clone(), + value, + context, + values, + ), } } @@ -2522,6 +2562,21 @@ fn write_back_method_array_element_ref_target( Ok(()) } +/// Stores one by-reference method result in a caller-side object property. +fn write_back_method_object_property_ref_target( + object: RuntimeCellHandle, + property: &str, + access_scope: ElephcEvalExecutionScope, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let previous_scope = context.replace_execution_scope(access_scope); + let result = eval_property_set_result(object, property, value, context, values); + context.replace_execution_scope(previous_scope); + result +} + /// Creates an indexed or associative array according to the first write key. fn eval_new_array_for_index( index: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index 9ba9693f08..d82df2d04d 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -240,6 +240,92 @@ return $missing["new"];"#, assert_eq!(values.get(result), FakeValue::String("created".to_string())); } +/// Verifies eval-declared by-reference method params write back object-property lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_object_properties() { + let program = parse_fragment( + br#"class EvalByRefPropertyChanger { + public function set(&$value, $next) { + $value = $next; + } + public function variadic(&...$items) { + $items[0] = "variadic"; + } +} +class EvalByRefPublicPropertyBox { + public string $value = "old"; +} +class EvalByRefPrivatePropertyBox { + private string $value = "private"; + public function update($changer) { + $changer->set($this->value, "secret"); + return $this->value; + } +} +$changer = new EvalByRefPropertyChanger(); +$public = new EvalByRefPublicPropertyBox(); +$changer->set($public->value, "changed"); +$changer->variadic($public->value); +echo $public->value; echo ":"; +$private = new EvalByRefPrivatePropertyBox(); +return $private->update($changer);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "variadic:"); + assert_eq!(values.get(result), FakeValue::String("secret".to_string())); +} + +/// Verifies eval-declared by-reference method params keep property access restrictions. +#[test] +fn execute_program_rejects_invalid_eval_method_by_ref_object_property_targets() { + let private_program = parse_fragment( + br#"class EvalByRefPrivatePropertyFailChanger { + public function set(&$value) { + $value = "bad"; + } +} +class EvalByRefPrivatePropertyFailBox { + private string $value = "private"; +} +$changer = new EvalByRefPrivatePropertyFailChanger(); +$box = new EvalByRefPrivatePropertyFailBox(); +$changer->set($box->value);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&private_program, &mut scope, &mut values) + .expect_err("private property by-ref target should fail from global scope"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let readonly_program = parse_fragment( + br#"class EvalByRefReadonlyPropertyFailChanger { + public function set(&$value) { + $value = "bad"; + } +} +class EvalByRefReadonlyPropertyFailBox { + public readonly string $value; + public function __construct($changer) { + $this->value = "old"; + $changer->set($this->value); + } +} +new EvalByRefReadonlyPropertyFailBox(new EvalByRefReadonlyPropertyFailChanger());"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&readonly_program, &mut scope, &mut values) + .expect_err("readonly property by-ref target should fail as an indirect modification"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval-declared variadic methods reject duplicate named variadic keys. #[test] fn execute_program_rejects_duplicate_eval_method_variadic_named_arg() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 6b5e697945..dcd1054965 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable and array-element arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -188,9 +188,9 @@ through `ReflectionParameter::isOptional()`. Variadic eval method parameters bind extra positional and unknown named arguments into a PHP array and are reported through `ReflectionParameter::isVariadic()` and `ReflectionParameter::isOptional()`. By-reference eval method parameters accept -direct variable and array-element arguments, write back fixed parameters after -method execution, write back mutated `&...$items` elements when the variadic -container itself is not rebound, and are reported through +direct variable, array-element, and object-property arguments, write back fixed +parameters after method execution, write back mutated `&...$items` elements +when the variadic container itself is not rebound, and are reported through `ReflectionParameter::isPassedByReference()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()` report eval property metadata. @@ -356,10 +356,10 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter type metadata beyond presence flags, by-reference method -arguments for object-property lvalues, broader default-value expression support -beyond scalar literals, and broader generated/AOT method bridge signatures -beyond the current public non-by-reference fixed scalar/Mixed slice. +ReflectionParameter type metadata beyond presence flags, broader default-value +expression support beyond scalar literals, and broader generated/AOT method +bridge signatures beyond the current public non-by-reference fixed scalar/Mixed +slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ec65fdd779..d0c173a04a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5407,6 +5407,9 @@ eval('class EvalByRefMethodBox { $items["named"] = $items["named"] . "-named"; } } +class EvalByRefPropertyBox { + public string $value = "D"; +} $box = new EvalByRefMethodBox(); $value = "A"; $box->change($value); @@ -5415,7 +5418,9 @@ $named = "B"; $box->changeVariadic($value, named: $named); $items = ["k" => "C"]; $box->change($items["k"]); -echo $value . ":" . $named . ":" . $items["k"];'); +$prop = new EvalByRefPropertyBox(); +$box->change($prop->value); +echo $value . ":" . $named . ":" . $items["k"] . ":" . $prop->value;'); "#, ); assert!( @@ -5423,7 +5428,7 @@ echo $value . ":" . $named . ":" . $items["k"];'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "A-method-static-variadic:B-named:C-method"); + assert_eq!(out.stdout, "A-method-static-variadic:B-named:C-method:D-method"); } /// Verifies eval dynamic static callables dispatch eval-declared static methods. From 8a3a36e567d10f7da9f09a33c905694e59853076 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 03:26:07 +0200 Subject: [PATCH 0380/1208] Support eval method constant defaults --- .../src/interpreter/dynamic_functions.rs | 17 ++++++- .../elephc-eval/src/interpreter/statements.rs | 48 ++++++++++++------- .../src/interpreter/tests/method_arguments.rs | 40 ++++++++++++++++ crates/elephc-eval/src/parser/statements.rs | 5 ++ .../src/parser/tests/classes_errors.rs | 46 ++++++++++++++++++ docs/php/eval.md | 17 ++++--- tests/codegen/eval.rs | 40 ++++++++++++++++ 7 files changed, 189 insertions(+), 24 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index c8d43d5635..ad1b115943 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -218,7 +218,7 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( parameter_is_by_ref: &[bool], parameter_is_variadic: &[bool], evaluated_args: Vec, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let mut bound_args = vec![None; params.len()]; @@ -289,7 +289,7 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( return Err(EvalStatus::RuntimeFatal); }; *value = Some(BoundMethodArg { - value: eval_method_parameter_default(default, values)?, + value: eval_method_parameter_default(default, context, values)?, ref_target: None, variadic_ref_targets: Vec::new(), }); @@ -664,10 +664,23 @@ fn eval_method_numeric_coercible_value( /// Materializes a supported eval method parameter default expression. fn eval_method_parameter_default( default: &EvalExpr, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match default { EvalExpr::Const(value) => eval_const(value, values), + EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), + EvalExpr::NamespacedConstFetch { + name, + fallback_name, + } => eval_namespaced_const_fetch(name, fallback_name, context, values), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => eval_class_constant_fetch_result(class_name, constant, context, values), + EvalExpr::ClassNameFetch { class_name } => { + eval_class_name_fetch_result(class_name, context, values) + } EvalExpr::Unary { op: EvalUnaryOp::Plus, expr, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index c72dd07abf..c14401da1b 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2599,7 +2599,13 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = bind_evaluated_method_args( + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(called_class_name.to_string()); + let evaluated_args = match bind_evaluated_method_args( method.params(), method.parameter_types(), method.parameter_defaults(), @@ -2608,7 +2614,15 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( evaluated_args, context, values, - )?; + ) { + Ok(args) => args, + Err(status) => { + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return Err(status); + } + }; let mut method_scope = ElephcEvalScope::new(); method_scope.set("this", object, ScopeCellOwnership::Borrowed); bind_method_scope_args( @@ -2617,12 +2631,6 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( method.parameter_is_by_ref(), &evaluated_args, ); - let qualified_method_name = - format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); - let static_names = static_var_names(method.body()); - context.push_function(qualified_method_name.clone()); - context.push_class_scope(class_name.to_string()); - context.push_called_class_scope(called_class_name.to_string()); let result = execute_statements(method.body(), context, &mut method_scope, values); let persist_result = persist_static_locals( context, @@ -2663,7 +2671,13 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = bind_evaluated_method_args( + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(called_class_name.to_string()); + let evaluated_args = match bind_evaluated_method_args( method.params(), method.parameter_types(), method.parameter_defaults(), @@ -2672,7 +2686,15 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( evaluated_args, context, values, - )?; + ) { + Ok(args) => args, + Err(status) => { + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return Err(status); + } + }; let mut method_scope = ElephcEvalScope::new(); bind_method_scope_args( &mut method_scope, @@ -2680,12 +2702,6 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( method.parameter_is_by_ref(), &evaluated_args, ); - let qualified_method_name = - format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); - let static_names = static_var_names(method.body()); - context.push_function(qualified_method_name.clone()); - context.push_class_scope(class_name.to_string()); - context.push_called_class_scope(called_class_name.to_string()); let result = execute_statements(method.body(), context, &mut method_scope, values); let persist_result = persist_static_locals( context, diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index d82df2d04d..fae2c93596 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -72,6 +72,46 @@ return EvalDefaultMethodBox::join();"#, assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); } +/// Verifies eval-declared methods materialize constant-expression parameter defaults. +#[test] +fn execute_program_binds_eval_method_constant_default_args() { + let program = parse_fragment( + br#"define("EVAL_METHOD_DEFAULT_GLOBAL", "G"); +class EvalDefaultConstBase { + const LABEL = "base"; +} +interface EvalDefaultConstIface { + const WORD = "iface"; +} +class EvalDefaultConstBox extends EvalDefaultConstBase { + const LABEL = "box"; + public function __construct($label = self::LABEL) { + $this->label = $label; + } + public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class) { + return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass; + } + public static function join($label = self::LABEL, $parent = parent::LABEL) { + return $label . "-" . $parent; + } +} +$box = new EvalDefaultConstBox(); +echo $box->read(); echo ":"; +return EvalDefaultConstBox::join();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:" + ); + assert_eq!(values.get(result), FakeValue::String("box-base".to_string())); +} + /// Verifies eval-declared methods bind positional and named values into variadic arrays. #[test] fn execute_program_binds_eval_method_variadic_args() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 2b26291a5a..ead670c4fa 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -2182,6 +2182,11 @@ impl Parser { fn method_parameter_default_is_supported(default: &EvalExpr) -> bool { match default { EvalExpr::Const(_) => true, + EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, + EvalExpr::ClassConstantFetch { class_name, .. } + | EvalExpr::ClassNameFetch { class_name } => { + !class_name.eq_ignore_ascii_case("static") + } EvalExpr::Unary { op: EvalUnaryOp::Plus | EvalUnaryOp::Negate, expr, diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 11a3670468..26be57fb29 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -617,6 +617,52 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { assert_eq!(method.parameter_is_variadic(), &[false, false, false, true]); } +/// Verifies eval method parameter defaults retain supported constant-expression metadata. +#[test] +fn parse_fragment_accepts_method_parameter_constant_defaults() { + let program = parse_fragment( + br#"class DynEvalDefaultConstants { + const LABEL = "box"; + public function read($global = DYN_EVAL_DEFAULT_GLOBAL, $label = self::LABEL, $parent = parent::LABEL, $class = self::class) {} +}"#, + ) + .expect("fragment should parse"); + let [EvalStmt::ClassDecl(class)] = program.statements() else { + panic!("expected one class declaration"); + }; + let [method] = class.methods() else { + panic!("expected one class method"); + }; + + assert!(matches!( + method.parameter_defaults(), + [ + Some(EvalExpr::ConstFetch(global)), + Some(EvalExpr::ClassConstantFetch { class_name: self_name, constant: self_constant }), + Some(EvalExpr::ClassConstantFetch { class_name: parent_name, constant: parent_constant }), + Some(EvalExpr::ClassNameFetch { class_name }) + ] if global == "DYN_EVAL_DEFAULT_GLOBAL" + && self_name == "self" + && self_constant == "LABEL" + && parent_name == "parent" + && parent_constant == "LABEL" + && class_name == "self" + )); +} + +/// Verifies eval rejects late-bound `static::` defaults like PHP compile-time constants do. +#[test] +fn parse_fragment_rejects_late_bound_static_method_parameter_defaults() { + parse_fragment( + b"class DynEvalStaticDefault { public function read($label = static::LABEL) {} }", + ) + .expect_err("static class constant defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalStaticClassDefault { public function read($class = static::class) {} }", + ) + .expect_err("static class-name defaults are not PHP compile-time constants"); +} + /// Verifies eval rejects invalid variadic method parameter forms. #[test] fn parse_fragment_rejects_invalid_variadic_method_parameters() { diff --git a/docs/php/eval.md b/docs/php/eval.md index dcd1054965..7acf2d00da 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -184,9 +184,14 @@ eval/runtime class or interface names. eval-declared method parameters. Eval currently exposes parameter names and zero-based positions there, plus declared-type presence for method parameter type hints. Defaulted eval method parameters are bound when omitted and reported -through `ReflectionParameter::isOptional()`. Variadic eval method parameters -bind extra positional and unknown named arguments into a PHP array and are -reported through `ReflectionParameter::isVariadic()` and +through `ReflectionParameter::isOptional()`. Supported default expressions +include scalar literals, signed numeric literals, predefined or eval-defined +constant fetches, namespaced constant fallback, class/interface/trait/enum +constant fetches, and `self::class` / `parent::class` / named class-like +`::class` literals. Late-bound `static::` defaults are rejected like PHP +compile-time constants. Variadic eval method parameters bind extra positional +and unknown named arguments into a PHP array and are reported through +`ReflectionParameter::isVariadic()` and `ReflectionParameter::isOptional()`. By-reference eval method parameters accept direct variable, array-element, and object-property arguments, write back fixed parameters after method execution, write back mutated `&...$items` elements @@ -357,9 +362,9 @@ Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer ReflectionParameter type metadata beyond presence flags, broader default-value -expression support beyond scalar literals, and broader generated/AOT method -bridge signatures beyond the current public non-by-reference fixed scalar/Mixed -slice. +expression support beyond the supported literal and constant-expression forms, +and broader generated/AOT method bridge signatures beyond the current public +non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d0c173a04a..b2de1522ba 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5334,6 +5334,46 @@ echo EvalNamedMethodBox::join(right: "H", left: "G");'); assert_eq!(out.stdout, "AB:C:D:AB:E:F:G-H"); } +/// Verifies eval-declared constructors and methods bind constant-expression defaults. +#[test] +fn test_eval_declared_method_constant_default_arguments() { + let out = compile_and_run_capture( + r#"label = $label; + } + public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class) { + return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass; + } + public static function join($label = self::LABEL, $parent = parent::LABEL) { + return $label . "-" . $parent; + } +} +$box = new EvalDefaultConstBox(); +echo $box->read() . ":"; +echo EvalDefaultConstBox::join();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:box-base" + ); +} + /// Verifies eval-declared constructors and methods bind variadic arguments. #[test] fn test_eval_declared_method_variadic_arguments() { From 2967c4daf531556f54b9b5143aabc70aebec54eb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 03:33:56 +0200 Subject: [PATCH 0381/1208] Broaden eval method default expressions --- .../src/interpreter/dynamic_functions.rs | 96 +++++++++++++------ .../src/interpreter/expressions.rs | 21 +++- .../elephc-eval/src/interpreter/statements.rs | 2 +- .../src/interpreter/tests/method_arguments.rs | 14 ++- crates/elephc-eval/src/parser/statements.rs | 77 ++++++++++++--- .../src/parser/tests/classes_errors.rs | 18 +++- docs/php/eval.md | 21 ++-- tests/codegen/eval.rs | 14 ++- 8 files changed, 196 insertions(+), 67 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index ad1b115943..a7b0ae19c0 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -667,40 +667,76 @@ fn eval_method_parameter_default( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match default { - EvalExpr::Const(value) => eval_const(value, values), - EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), - EvalExpr::NamespacedConstFetch { - name, - fallback_name, - } => eval_namespaced_const_fetch(name, fallback_name, context, values), - EvalExpr::ClassConstantFetch { - class_name, - constant, - } => eval_class_constant_fetch_result(class_name, constant, context, values), - EvalExpr::ClassNameFetch { class_name } => { - eval_class_name_fetch_result(class_name, context, values) + if !eval_method_default_expr_is_supported(default) { + return Err(EvalStatus::UnsupportedConstruct); + } + let mut default_scope = ElephcEvalScope::new(); + eval_expr(default, context, &mut default_scope, values) +} + +/// Returns whether an EvalIR expression can be safely evaluated as a method default. +fn eval_method_default_expr_is_supported(expr: &EvalExpr) -> bool { + match expr { + EvalExpr::Array(elements) => elements + .iter() + .all(eval_method_default_array_element_is_supported), + EvalExpr::Const(_) | EvalExpr::Magic(_) => true, + EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, + EvalExpr::ClassConstantFetch { class_name, .. } + | EvalExpr::ClassNameFetch { class_name } => { + eval_method_default_class_receiver_is_supported(class_name) + } + EvalExpr::NewObject { class_name, args } => { + eval_method_default_class_receiver_is_supported(class_name) + && args.iter().all(eval_method_default_call_arg_is_supported) + } + EvalExpr::NullCoalesce { value, default } => { + eval_method_default_expr_is_supported(value) + && eval_method_default_expr_is_supported(default) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_method_default_expr_is_supported(condition) + && then_branch + .as_deref() + .is_none_or(eval_method_default_expr_is_supported) + && eval_method_default_expr_is_supported(else_branch) + } + EvalExpr::Unary { expr, .. } => eval_method_default_expr_is_supported(expr), + EvalExpr::Binary { left, right, .. } => { + eval_method_default_expr_is_supported(left) + && eval_method_default_expr_is_supported(right) } - EvalExpr::Unary { - op: EvalUnaryOp::Plus, - expr, - } => match expr.as_ref() { - EvalExpr::Const(EvalConst::Int(value)) => values.int(*value), - EvalExpr::Const(EvalConst::Float(value)) => values.float(*value), - _ => Err(EvalStatus::UnsupportedConstruct), - }, - EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr, - } => match expr.as_ref() { - EvalExpr::Const(EvalConst::Int(value)) => values.int(value.wrapping_neg()), - EvalExpr::Const(EvalConst::Float(value)) => values.float(-*value), - _ => Err(EvalStatus::UnsupportedConstruct), - }, - _ => Err(EvalStatus::UnsupportedConstruct), + _ => false, } } +/// Returns whether one object-construction argument is safe inside a method default. +fn eval_method_default_call_arg_is_supported(arg: &EvalCallArg) -> bool { + !arg.is_spread() && eval_method_default_expr_is_supported(arg.value()) +} + +/// Returns whether one array default element contains only supported constant expressions. +fn eval_method_default_array_element_is_supported(element: &EvalArrayElement) -> bool { + match element { + EvalArrayElement::Value(value) => eval_method_default_expr_is_supported(value), + EvalArrayElement::KeyValue { key, value } => { + eval_method_default_expr_is_supported(key) + && eval_method_default_expr_is_supported(value) + } + } +} + +/// Returns whether a class-like receiver is legal in a compile-time method default. +fn eval_method_default_class_receiver_is_supported(class_name: &str) -> bool { + !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("static") +} + /// Binds one positional dynamic-call value to the next declared parameter slot. pub(in crate::interpreter) fn bind_dynamic_positional_arg( bound_args: &mut [Option], diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 35defe0d54..bba4512f1a 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -65,17 +65,15 @@ pub(in crate::interpreter) fn eval_expr( } => eval_namespaced_const_fetch(name, fallback_name, context, values), EvalExpr::NewObject { class_name, args } => { let args = eval_method_call_arg_values(args, context, scope, values)?; + let class_name = eval_new_object_class_name(class_name, context)?; if let Some(object) = - eval_reflection_owner_new_object(class_name, args.clone(), context, values)? + eval_reflection_owner_new_object(&class_name, args.clone(), context, values)? { return Ok(object); } - if let Some(class) = context.class(class_name).cloned() { + if let Some(class) = context.class(&class_name).cloned() { eval_dynamic_class_new_object(&class, args, context, scope, values) } else { - let class_name = context - .resolve_class_name(class_name) - .unwrap_or_else(|| class_name.clone()); let args = bind_native_callable_args(context.native_constructor_signature(&class_name), args)?; values @@ -224,6 +222,19 @@ pub(in crate::interpreter) fn eval_expr( } } +/// Resolves special class names used by `new` while preserving AOT fallback names. +fn eval_new_object_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + /// Evaluates a PHP `match` expression with strict comparison and lazy arm values. pub(in crate::interpreter) fn eval_match_expr( subject: &EvalExpr, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index c14401da1b..8b138e2b71 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1987,7 +1987,7 @@ fn eval_dynamic_static_method_for_call( } /// Resolves `self`, `parent`, and `static` for eval static member access. -fn resolve_eval_static_class_name( +pub(in crate::interpreter) fn resolve_eval_static_class_name( class_name: &str, context: &ElephcEvalContext, ) -> Result { diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs index fae2c93596..5d9940fdfe 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-eval/src/interpreter/tests/method_arguments.rs @@ -83,13 +83,21 @@ class EvalDefaultConstBase { interface EvalDefaultConstIface { const WORD = "iface"; } +class EvalDefaultConstDep { + public function __construct($label = "dep") { + $this->label = $label; + } + public function read() { + return $this->label; + } +} class EvalDefaultConstBox extends EvalDefaultConstBase { const LABEL = "box"; public function __construct($label = self::LABEL) { $this->label = $label; } - public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class) { - return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass; + public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "fallback"], $method = __METHOD__, $dep = new EvalDefaultConstDep(label: "dep"), $clone = new self("inner")) { + return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass . ":" . $items[self::LABEL] . ":" . $items["fallback"] . ":" . $method . ":" . $dep->read() . ":" . $clone->label; } public static function join($label = self::LABEL, $parent = parent::LABEL) { return $label . "-" . $parent; @@ -107,7 +115,7 @@ return EvalDefaultConstBox::join();"#, assert_eq!( values.output, - "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:" + "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:3:fallback:EvalDefaultConstBox::read:dep:inner:" ); assert_eq!(values.get(result), FakeValue::String("box-base".to_string())); } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index ead670c4fa..695cf31a88 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -12,11 +12,11 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalAttribute, EvalAttributeArg, EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, - EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, - EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, - EvalParameterTypeVariant, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, - EvalUnaryOp, EvalVisibility, + EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalCallArg, EvalCatch, EvalClass, + EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, + EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInterface, EvalInterfaceMethod, + EvalInterfaceProperty, EvalParameterType, EvalParameterTypeVariant, EvalStmt, EvalSwitchCase, + EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::lexer::TokenKind; @@ -2180,24 +2180,73 @@ impl Parser { /// Returns whether an eval method parameter default can be materialized safely. fn method_parameter_default_is_supported(default: &EvalExpr) -> bool { - match default { + eval_constant_expression_default_is_supported(default) +} + +/// Returns whether an EvalIR expression is safe to retain as a method default. +fn eval_constant_expression_default_is_supported(expr: &EvalExpr) -> bool { + match expr { + EvalExpr::Array(elements) => elements + .iter() + .all(eval_array_element_default_is_supported), EvalExpr::Const(_) => true, + EvalExpr::Magic(_) => true, EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, EvalExpr::ClassConstantFetch { class_name, .. } | EvalExpr::ClassNameFetch { class_name } => { - !class_name.eq_ignore_ascii_case("static") + eval_default_class_receiver_is_supported(class_name) + } + EvalExpr::NewObject { class_name, args } => { + eval_default_class_receiver_is_supported(class_name) + && args.iter().all(eval_call_arg_default_is_supported) + } + EvalExpr::NullCoalesce { value, default } => { + eval_constant_expression_default_is_supported(value) + && eval_constant_expression_default_is_supported(default) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_constant_expression_default_is_supported(condition) + && then_branch + .as_deref() + .is_none_or(eval_constant_expression_default_is_supported) + && eval_constant_expression_default_is_supported(else_branch) + } + EvalExpr::Unary { expr, .. } => eval_constant_expression_default_is_supported(expr), + EvalExpr::Binary { left, right, .. } => { + eval_constant_expression_default_is_supported(left) + && eval_constant_expression_default_is_supported(right) } - EvalExpr::Unary { - op: EvalUnaryOp::Plus | EvalUnaryOp::Negate, - expr, - } => matches!( - expr.as_ref(), - EvalExpr::Const(EvalConst::Int(_) | EvalConst::Float(_)) - ), _ => false, } } +/// Returns whether one object-construction argument is safe inside a method default. +fn eval_call_arg_default_is_supported(arg: &EvalCallArg) -> bool { + !arg.is_spread() && eval_constant_expression_default_is_supported(arg.value()) +} + +/// Returns whether one array default element contains only supported constant expressions. +fn eval_array_element_default_is_supported(element: &EvalArrayElement) -> bool { + match element { + EvalArrayElement::Value(value) => eval_constant_expression_default_is_supported(value), + EvalArrayElement::KeyValue { key, value } => { + eval_constant_expression_default_is_supported(key) + && eval_constant_expression_default_is_supported(value) + } + } +} + +/// Returns whether a class-like receiver is legal in a compile-time method default. +fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { + !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("static") +} + /// Converts a parsed attribute argument expression into retained literal metadata. fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { match expr { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 26be57fb29..83176b0f86 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -623,7 +623,7 @@ fn parse_fragment_accepts_method_parameter_constant_defaults() { let program = parse_fragment( br#"class DynEvalDefaultConstants { const LABEL = "box"; - public function read($global = DYN_EVAL_DEFAULT_GLOBAL, $label = self::LABEL, $parent = parent::LABEL, $class = self::class) {} + public function read($global = DYN_EVAL_DEFAULT_GLOBAL, $label = self::LABEL, $parent = parent::LABEL, $class = self::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "x"], $method = __METHOD__, $dep = new DynEvalDefaultDep(label: "dep")) {} }"#, ) .expect("fragment should parse"); @@ -640,13 +640,19 @@ fn parse_fragment_accepts_method_parameter_constant_defaults() { Some(EvalExpr::ConstFetch(global)), Some(EvalExpr::ClassConstantFetch { class_name: self_name, constant: self_constant }), Some(EvalExpr::ClassConstantFetch { class_name: parent_name, constant: parent_constant }), - Some(EvalExpr::ClassNameFetch { class_name }) + Some(EvalExpr::ClassNameFetch { class_name }), + Some(EvalExpr::Array(_)), + Some(EvalExpr::Magic(EvalMagicConst::Method)), + Some(EvalExpr::NewObject { class_name: dep_name, args }) ] if global == "DYN_EVAL_DEFAULT_GLOBAL" && self_name == "self" && self_constant == "LABEL" && parent_name == "parent" && parent_constant == "LABEL" && class_name == "self" + && dep_name == "DynEvalDefaultDep" + && args.len() == 1 + && args[0].name() == Some("label") )); } @@ -661,6 +667,14 @@ fn parse_fragment_rejects_late_bound_static_method_parameter_defaults() { b"class DynEvalStaticClassDefault { public function read($class = static::class) {} }", ) .expect_err("static class-name defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalStaticNewDefault { public function read($dep = new static()) {} }", + ) + .expect_err("static object defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalSpreadNewDefault { public function read($dep = new DynEvalDep(...[\"x\"])) {} }", + ) + .expect_err("argument unpacking is not supported in PHP constant expressions"); } /// Verifies eval rejects invalid variadic method parameter forms. diff --git a/docs/php/eval.md b/docs/php/eval.md index 7acf2d00da..690f247d4a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -185,12 +185,16 @@ eval-declared method parameters. Eval currently exposes parameter names and zero-based positions there, plus declared-type presence for method parameter type hints. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`. Supported default expressions -include scalar literals, signed numeric literals, predefined or eval-defined -constant fetches, namespaced constant fallback, class/interface/trait/enum -constant fetches, and `self::class` / `parent::class` / named class-like -`::class` literals. Late-bound `static::` defaults are rejected like PHP -compile-time constants. Variadic eval method parameters bind extra positional -and unknown named arguments into a PHP array and are reported through +include scalar literals, arrays whose keys and values are supported default +expressions, magic constants, unary and binary operators supported by eval, +ternary and null-coalescing expressions, predefined or eval-defined constant +fetches, namespaced constant fallback, class/interface/trait/enum constant +fetches, `self::class` / `parent::class` / named class-like `::class` literals, +and `new ClassName(...)` / `new self(...)` / `new parent(...)` with supported +non-spread constructor arguments. Late-bound `static::` defaults and unpacked +constructor arguments in defaults are rejected like PHP constant expressions. +Variadic eval method parameters bind extra positional and unknown named +arguments into a PHP array and are reported through `ReflectionParameter::isVariadic()` and `ReflectionParameter::isOptional()`. By-reference eval method parameters accept direct variable, array-element, and object-property arguments, write back fixed @@ -361,9 +365,8 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter type metadata beyond presence flags, broader default-value -expression support beyond the supported literal and constant-expression forms, -and broader generated/AOT method bridge signatures beyond the current public +ReflectionParameter type metadata beyond presence flags, and broader +generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b2de1522ba..41dd2b75e1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5346,13 +5346,21 @@ class EvalDefaultConstBase { interface EvalDefaultConstIface { const WORD = "iface"; } +class EvalDefaultConstDep { + public function __construct($label = "dep") { + $this->label = $label; + } + public function read() { + return $this->label; + } +} class EvalDefaultConstBox extends EvalDefaultConstBase { const LABEL = "box"; public function __construct($label = self::LABEL) { $this->label = $label; } - public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class) { - return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass; + public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "fallback"], $method = __METHOD__, $dep = new EvalDefaultConstDep(label: "dep"), $clone = new self("inner")) { + return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass . ":" . $items[self::LABEL] . ":" . $items["fallback"] . ":" . $method . ":" . $dep->read() . ":" . $clone->label; } public static function join($label = self::LABEL, $parent = parent::LABEL) { return $label . "-" . $parent; @@ -5370,7 +5378,7 @@ echo EvalDefaultConstBox::join();'); ); assert_eq!( out.stdout, - "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:box-base" + "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:3:fallback:EvalDefaultConstBox::read:dep:inner:box-base" ); } From 8e8eb7ffb1fd6252ca2756ed8dcdda567bdf56e4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 03:51:39 +0200 Subject: [PATCH 0382/1208] Support reflection method parameter counts --- .../elephc-eval/src/interpreter/reflection.rs | 34 ++++- .../tests/builtins_class_metadata.rs | 17 ++- .../interpreter/tests/support/object_ops.rs | 14 +++ docs/php/classes.md | 7 +- docs/php/eval.md | 13 +- src/codegen/eval_reflection_owner_helpers.rs | 41 ++++++ src/codegen/lower_inst/objects/reflection.rs | 117 +++++++++++++++--- src/types/checker/builtin_types/reflection.rs | 51 ++++++++ tests/codegen/eval.rs | 8 +- tests/codegen/oop/attributes.rs | 32 +++-- 10 files changed, 293 insertions(+), 41 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 3feecc236f..21bd924139 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -48,6 +48,7 @@ struct EvalReflectionMemberMetadata { is_static: bool, is_final: bool, is_abstract: bool, + required_parameter_count: usize, parameters: Vec, } @@ -159,7 +160,7 @@ fn eval_reflection_method_new( &[], &method.parameters, flags, - 0, + method.required_parameter_count as u64, context, values, ) @@ -444,7 +445,7 @@ fn eval_reflection_member_object_array_result( &[], &member.parameters, flags, - 0, + member.required_parameter_count as u64, context, values, )?; @@ -605,6 +606,10 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), + required_parameter_count: eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ), parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), @@ -625,6 +630,10 @@ fn eval_reflection_method_metadata( is_static: false, is_final: false, is_abstract: true, + required_parameter_count: eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ), parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), @@ -645,6 +654,10 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), + required_parameter_count: eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ), parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), @@ -671,6 +684,7 @@ fn eval_reflection_property_metadata( is_static: property.is_static(), is_final: false, is_abstract: property.is_abstract(), + required_parameter_count: 0, parameters: Vec::new(), }); } @@ -685,6 +699,7 @@ fn eval_reflection_property_metadata( is_static: false, is_final: false, is_abstract: true, + required_parameter_count: 0, parameters: Vec::new(), }); } @@ -699,11 +714,26 @@ fn eval_reflection_property_metadata( is_static: property.is_static(), is_final: false, is_abstract: property.is_abstract(), + required_parameter_count: 0, parameters: Vec::new(), }) }) } +/// Returns PHP's required parameter count for a reflected method signature. +fn eval_reflection_required_parameter_count( + defaults: &[Option], + variadic_flags: &[bool], +) -> usize { + let fixed_count = variadic_flags + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(defaults.len()); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + /// Builds parameter reflection metadata from eval parameter names and type flags. fn eval_reflection_parameters_from_names_and_type_flags( names: &[String], diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index faabdb9a98..2eabb4732b 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -495,8 +495,10 @@ fn execute_program_reflects_eval_method_parameters() { br##"class EvalReflectParamTarget { public function run(int &$first, \App\Name|null $second = null, &...$rest) {} } -$params = (new ReflectionMethod("EvalReflectParamTarget", "run"))->getParameters(); -echo count($params); echo ":"; +$method = new ReflectionMethod("EvalReflectParamTarget", "run"); +echo $method->getNumberOfParameters(); echo "/"; +echo $method->getNumberOfRequiredParameters(); echo ":"; +$params = $method->getParameters(); foreach ($params as $param) { echo $param->getName(); echo "#"; echo $param->getPosition(); echo $param->isOptional() ? "O" : "r"; @@ -513,7 +515,10 @@ return true;"##, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:first#0rvRT|second#1OvbT|rest#2OVRt|"); + assert_eq!( + values.output, + "3/1:first#0rvRT|second#1OvbT|rest#2OVRt|" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -528,7 +533,9 @@ fn execute_program_reflection_class_lists_eval_method_parameters() { $methods = (new ReflectionClass("EvalReflectListedParamTarget"))->getMethods(); foreach ($methods as $method) { $params = $method->getParameters(); - echo $method->getName(); echo ":"; echo count($params); + echo $method->getName(); echo ":"; + echo $method->getNumberOfParameters(); echo "/"; + echo $method->getNumberOfRequiredParameters(); if (count($params) > 0) { echo ":"; echo $params[0]->getName(); echo ":"; echo $params[0]->getPosition(); } @@ -542,7 +549,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "first:1:left:0|second:2:right:0|"); + assert_eq!(values.output, "first:1/1:left:0|second:2/2:right:0|"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index d72c6b1c6d..c5773b6692 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -198,6 +198,19 @@ impl FakeOps { Self::object_property(&properties, "__parameters") .map_or_else(|| self.runtime_array_new(0), Ok) } + (FakeValue::Object(properties), "getnumberofparameters") if args.is_empty() => { + match Self::object_property(&properties, "__parameters") { + Some(parameters) => { + let len = self.array_len(parameters)?; + self.int(len as i64) + } + None => self.int(0), + } + } + (FakeValue::Object(properties), "getnumberofrequiredparameters") if args.is_empty() => { + Self::object_property(&properties, "__required_parameter_count") + .map_or_else(|| self.int(0), Ok) + } (FakeValue::Object(properties), "getposition") if args.is_empty() => { Self::object_property(&properties, "__position").map_or_else(|| self.int(0), Ok) } @@ -399,6 +412,7 @@ impl FakeOps { properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__parameters".to_string(), method_objects)); + properties.push(("__required_parameter_count".to_string(), modifiers_cell)); } if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { let position = self.int(modifiers as i64)?; diff --git a/docs/php/classes.md b/docs/php/classes.md index f21266403e..f6937bc944 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1121,6 +1121,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isFinal()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is final | | `ReflectionMethod::isAbstract()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is abstract | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | +| `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | +| `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | | `ReflectionParameter::getName()` | Internal only | Return the parameter name without `$` | | `ReflectionParameter::getPosition()` | Internal only | Return the zero-based parameter position | | `ReflectionParameter::isOptional()` | Internal only | Return whether the parameter has a default value or is variadic | @@ -1191,8 +1193,9 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. +- Standalone native trait reflection currently retains trait method names but not full trait method signatures, so `ReflectionMethod` objects returned from `new ReflectionClass(TraitName::class)` expose empty parameter metadata until trait signatures are retained in EIR metadata. ### Class constants @@ -1221,4 +1224,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - Backed property hooks may read and write their own backing slot. - Shadowing a private parent property with a same-named child property is not yet supported (PHP gives them separate slots; elephc uses one slot per name) - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()` for supported named types); method parameter names, positions, optional/variadic/by-reference flags, and declared-type presence are exposed through `ReflectionMethod::getParameters()`. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()` for supported named types); method parameter names, counts, positions, optional/variadic/by-reference flags, and declared-type presence are exposed through the supported `ReflectionMethod`/`ReflectionParameter` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). diff --git a/docs/php/eval.md b/docs/php/eval.md index 690f247d4a..56dca2d384 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -180,12 +180,13 @@ coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and eval/runtime class or interface names. `ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()` report eval method metadata. -`ReflectionMethod::getParameters()` returns `ReflectionParameter` objects for -eval-declared method parameters. Eval currently exposes parameter names and -zero-based positions there, plus declared-type presence for method parameter -type hints. Defaulted eval method parameters are bound when omitted and reported -through `ReflectionParameter::isOptional()`. Supported default expressions -include scalar literals, arrays whose keys and values are supported default +`ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and +`getNumberOfRequiredParameters()` report eval-declared method parameter +metadata. Eval currently exposes parameter names and zero-based positions there, +plus declared-type presence for method parameter type hints. Defaulted eval +method parameters are bound when omitted and reported through +`ReflectionParameter::isOptional()`. Supported default expressions include +scalar literals, arrays whose keys and values are supported default expressions, magic constants, unary and binary operators supported by eval, ternary and null-coalescing expressions, predefined or eval-defined constant fetches, namespaced constant fallback, class/interface/trait/enum constant diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index c8760867b5..ccc38cdb1e 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -68,6 +68,8 @@ struct ReflectionOwnerLayout { is_protected_hi: Option, is_private_lo: Option, is_private_hi: Option, + required_parameter_count_lo: Option, + required_parameter_count_hi: Option, position_lo: Option, position_hi: Option, is_optional_lo: Option, @@ -186,6 +188,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, constructor_member: Option, parameter_members: Vec, + required_parameter_count: i64, is_final: bool, is_abstract: bool, is_interface: bool, @@ -51,6 +52,7 @@ struct ReflectionListedMember { attr_names: Vec, attr_args: Vec>>, flags: ReflectionMemberFlags, + required_parameter_count: i64, parameters: Vec, } @@ -177,6 +179,12 @@ pub(super) fn lower_reflection_owner_new( "__parameters", &metadata.parameter_members, )?; + emit_reflection_owner_int_property( + ctx, + class_name, + "__required_parameter_count", + metadata.required_parameter_count, + )?; } emit_reflection_member_flag_properties(ctx, class_name, metadata.member_flags)?; let result = inst @@ -619,6 +627,7 @@ fn reflection_class_metadata( property_members, constructor_member, parameter_members: Vec::new(), + required_parameter_count: 0, is_final: info.is_final, is_abstract: info.is_abstract, is_interface: false, @@ -635,16 +644,38 @@ fn reflection_class_metadata( }); } if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { - return Ok(class_like_reflection_metadata( - interface_name, - reflection_interface_parent_names(ctx, interface_name), - Vec::new(), - reflection_interface_method_names(ctx, interface_name), - reflection_interface_property_names(ctx, interface_name), - true, - false, - false, - )); + let method_names = reflection_interface_method_names(ctx, interface_name); + let property_names = reflection_interface_property_names(ctx, interface_name); + let method_members = ctx + .module + .interface_infos + .get(interface_name) + .map(|info| reflection_interface_method_members(info, &method_names)) + .unwrap_or_else(|| default_method_members(&method_names, true, false)); + let property_members = default_property_members(&property_names, true); + let constructor_member = reflection_constructor_member(&method_members); + return Ok(ReflectionOwnerMetadata { + reflected_name: Some(interface_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + interface_names: reflection_interface_parent_names(ctx, interface_name), + trait_names: Vec::new(), + method_names, + property_names, + method_members, + property_members, + constructor_member, + parameter_members: Vec::new(), + required_parameter_count: 0, + is_final: false, + is_abstract: false, + is_interface: true, + is_trait: false, + is_enum: false, + is_readonly: false, + modifiers: 0, + member_flags: ReflectionMemberFlags::default(), + }); } if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { let trait_names = ctx @@ -696,6 +727,7 @@ fn reflection_method_metadata( property_members: Vec::new(), constructor_member: None, parameter_members: member.parameters, + required_parameter_count: member.required_parameter_count, is_final: false, is_abstract: false, is_interface: false, @@ -736,6 +768,7 @@ fn reflection_property_metadata( property_members: Vec::new(), constructor_member: None, parameter_members: Vec::new(), + required_parameter_count: 0, is_final: false, is_abstract: false, is_interface: false, @@ -777,6 +810,7 @@ fn reflection_class_constant_metadata( property_members: Vec::new(), constructor_member: None, parameter_members: Vec::new(), + required_parameter_count: 0, is_final: false, is_abstract: false, is_interface: false, @@ -812,6 +846,7 @@ fn reflection_class_constant_metadata( property_members: Vec::new(), constructor_member: None, parameter_members: Vec::new(), + required_parameter_count: 0, is_final: false, is_abstract: false, is_interface: false, @@ -854,6 +889,7 @@ fn reflection_enum_case_metadata( property_members: Vec::new(), constructor_member: None, parameter_members: Vec::new(), + required_parameter_count: 0, is_final: false, is_abstract: false, is_interface: false, @@ -1110,6 +1146,35 @@ fn reflection_class_method_member( .cloned() .unwrap_or_default(), flags: reflection_method_member_flags(info, &method_key)?, + required_parameter_count: reflection_required_parameter_count(sig), + parameters: reflection_parameter_members(sig), + }) +} + +/// Builds ReflectionMethod array entries for methods declared by an interface. +fn reflection_interface_method_members( + info: &InterfaceInfo, + method_names: &[String], +) -> Vec { + method_names + .iter() + .filter_map(|method_name| reflection_interface_method_member(info, method_name)) + .collect() +} + +/// Builds one ReflectionMethod array entry from interface metadata. +fn reflection_interface_method_member( + info: &InterfaceInfo, + method_name: &str, +) -> Option { + let method_key = php_symbol_key(method_name); + let sig = info.methods.get(&method_key)?; + Some(ReflectionListedMember { + name: method_key, + attr_names: Vec::new(), + attr_args: Vec::new(), + flags: reflection_member_flags(false, &Visibility::Public, false, true), + required_parameter_count: reflection_required_parameter_count(sig), parameters: reflection_parameter_members(sig), }) } @@ -1152,6 +1217,7 @@ fn reflection_class_property_member( .cloned() .unwrap_or_default(), flags, + required_parameter_count: 0, parameters: Vec::new(), }) } @@ -1174,6 +1240,7 @@ fn default_method_members( false, is_interface, ), + required_parameter_count: 0, parameters: Vec::new(), }) .collect() @@ -1196,11 +1263,28 @@ fn default_property_members( false, is_interface, ), + required_parameter_count: 0, parameters: Vec::new(), }) .collect() } +/// Returns PHP's required parameter count for a reflected native signature. +fn reflection_required_parameter_count(sig: &FunctionSig) -> i64 { + let fixed_count = sig + .variadic + .as_deref() + .and_then(|variadic| { + sig.params + .iter() + .position(|(name, _)| name.as_str() == variadic) + }) + .unwrap_or(sig.params.len()); + (0..fixed_count) + .rfind(|index| !sig.defaults.get(*index).is_some_and(Option::is_some)) + .map_or(0, |index| index as i64 + 1) +} + /// Builds reflected parameter metadata from a method/function signature. fn reflection_parameter_members(sig: &FunctionSig) -> Vec { sig.params @@ -1357,6 +1441,7 @@ fn class_like_reflection_metadata( property_members, constructor_member, parameter_members: Vec::new(), + required_parameter_count: 0, is_final: false, is_abstract: false, is_interface, @@ -1410,6 +1495,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { property_members: Vec::new(), constructor_member: None, parameter_members: Vec::new(), + required_parameter_count: 0, is_final: false, is_abstract: false, is_interface: false, @@ -1681,9 +1767,6 @@ fn emit_reflection_parameter_array_property_by_name( property_name: &str, parameters: &[ReflectionParameterMember], ) -> Result<()> { - if parameters.is_empty() { - return Ok(()); - } let class_info = ctx .module .class_infos @@ -1801,6 +1884,12 @@ fn emit_reflection_member_object( "__parameters", &member.parameters, )?; + emit_reflection_owner_int_property( + ctx, + member_class_name, + "__required_parameter_count", + member.required_parameter_count, + )?; } emit_reflection_member_flag_properties(ctx, member_class_name, member.flags)?; Ok(()) diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 7af077cdb1..8e58e39542 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1040,11 +1040,22 @@ fn builtin_reflection_owner_class( Some(object_array_type("ReflectionParameter")), empty_array(), )); + properties.push(builtin_property( + "__required_parameter_count", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + )); methods.push(builtin_reflection_class_array_method( "getParameters", "__parameters", object_array_type("ReflectionParameter"), )); + methods.push(builtin_reflection_method_parameter_count_method()); + methods.push(builtin_reflection_class_int_method( + "getNumberOfRequiredParameters", + "__required_parameter_count", + )); } properties.push(builtin_property( "__attrs", @@ -1068,6 +1079,41 @@ fn builtin_reflection_owner_class( } } +/// Builds `ReflectionMethod::getNumberOfParameters()` over the retained parameter array. +fn builtin_reflection_method_parameter_count_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "getNumberOfParameters".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Int), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("count"), + args: vec![Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: "__parameters".to_string(), + }, + dummy_span, + )], + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Adds member visibility/staticity predicates for method and property reflection owners. fn add_reflection_member_flag_methods( class_name: &str, @@ -1283,6 +1329,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionParameter".to_string(), ))); } + for method_name in ["getNumberOfParameters", "getNumberOfRequiredParameters"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Int; + } + } } if class_name == "ReflectionParameter" { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPosition")) { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 41dd2b75e1..ec429894fb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6021,8 +6021,10 @@ fn test_eval_reflection_method_lists_parameters() { eval('class EvalReflectParamTarget { public function run(int $first, \App\Name|null $second = null, ...$rest) {} } -$params = (new ReflectionMethod("EvalReflectParamTarget", "run"))->getParameters(); -echo count($params) . ":"; +$method = new ReflectionMethod("EvalReflectParamTarget", "run"); +echo $method->getNumberOfParameters() . "/"; +echo $method->getNumberOfRequiredParameters() . ":"; +$params = $method->getParameters(); foreach ($params as $param) { echo $param->getName() . "@" . $param->getPosition(); echo $param->isOptional() ? "O" : "r"; @@ -6038,7 +6040,7 @@ foreach ($params as $param) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "3:first@0rvbT|second@1OvbT|rest@2OVbt|"); + assert_eq!(out.stdout, "3/1:first@0rvbT|second@1OvbT|rest@2OVbt|"); } /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 9405b16232..99d345b7b7 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1596,8 +1596,10 @@ fn test_reflection_method_get_parameters_returns_parameter_metadata() { class ReflectParamTarget { public function run(int $id, &$name, string $mode = "x", ...$rest) {} } -$params = (new ReflectionMethod(ReflectParamTarget::class, "run"))->getParameters(); -echo count($params) . ":"; +$method = new ReflectionMethod(ReflectParamTarget::class, "run"); +echo $method->getNumberOfParameters() . "/"; +echo $method->getNumberOfRequiredParameters() . ":"; +$params = $method->getParameters(); foreach ($params as $param) { echo $param->getName() . "#" . $param->getPosition(); echo ($param->hasType() ? "T" : "t"); @@ -1613,7 +1615,10 @@ foreach ($params as $param) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "4:id#0TRbv|name#1tRBv|mode#2TObv|rest#3tObV|"); + assert_eq!( + out.stdout, + "4/2:id#0TRbv|name#1tRBv|mode#2TObv|rest#3tObV|" + ); } /// Verifies that ReflectionMethod objects returned from `ReflectionClass::getMethods()` @@ -1629,7 +1634,8 @@ $methods = (new ReflectionClass(ReflectListedParamTarget::class))->getMethods(); foreach ($methods as $method) { if ($method->getName() === "listed") { $params = $method->getParameters(); - echo count($params) . ":"; + echo $method->getNumberOfParameters() . "/"; + echo $method->getNumberOfRequiredParameters() . ":"; echo $params[0]->getName() . ($params[0]->hasType() ? "T" : "t"); echo ":"; echo $params[1]->getName() . ($params[1]->isOptional() ? "O" : "R"); @@ -1642,7 +1648,7 @@ foreach ($methods as $method) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:firstT:secondO"); + assert_eq!(out.stdout, "2/1:firstT:secondO"); } /// Verifies that `ReflectionClass::getConstructor()` returns a ReflectionMethod @@ -1652,20 +1658,22 @@ fn test_reflection_class_get_constructor_returns_method_or_null() { let out = compile_and_run( r#"getConstructor(); if ($base instanceof ReflectionMethod) { echo $base->getName(); + echo "/" . $base->getNumberOfParameters(); + echo "/" . $base->getNumberOfRequiredParameters(); } else { echo "null"; } @@ -1674,6 +1682,8 @@ echo ":"; $child = (new ReflectionClass(ReflectCtorChild::class))->getConstructor(); if ($child instanceof ReflectionMethod) { echo $child->getName(); + echo "/" . $child->getNumberOfParameters(); + echo "/" . $child->getNumberOfRequiredParameters(); } else { echo "null"; } @@ -1690,6 +1700,8 @@ echo ":"; $interface = (new ReflectionClass(ReflectCtorInterface::class))->getConstructor(); if ($interface instanceof ReflectionMethod) { echo $interface->getName(); + echo "/" . $interface->getNumberOfParameters(); + echo "/" . $interface->getNumberOfRequiredParameters(); } else { echo "null"; } @@ -1698,6 +1710,8 @@ echo ":"; $trait = (new ReflectionClass(ReflectCtorTrait::class))->getConstructor(); if ($trait instanceof ReflectionMethod) { echo $trait->getName(); + echo "/" . $trait->getNumberOfParameters(); + echo "/" . $trait->getNumberOfRequiredParameters(); } else { echo "null"; } @@ -1705,7 +1719,7 @@ if ($trait instanceof ReflectionMethod) { ); assert_eq!( out, - "__construct:__construct:null:__construct:__construct" + "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/0/0" ); } From 6e1aba7842c678e0ed7ac775d764bf68b937824b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 03:59:55 +0200 Subject: [PATCH 0383/1208] Retain trait method reflection signatures --- docs/php/classes.md | 1 - src/codegen/lower_inst/objects/reflection.rs | 189 +++++++++++------- src/ir/mod.rs | 2 +- src/ir/module.rs | 13 ++ src/ir_lower/function.rs | 20 +- src/ir_lower/program.rs | 41 ++++ src/types/checker/driver/init.rs | 1 + src/types/checker/driver/mod.rs | 29 +++ .../objects/constructors/reflection.rs | 72 ++++--- src/types/checker/mod.rs | 2 + tests/codegen/oop/attributes.rs | 52 ++++- 11 files changed, 317 insertions(+), 105 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index f6937bc944..4b1287a770 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1195,7 +1195,6 @@ Limitations today: - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. -- Standalone native trait reflection currently retains trait method names but not full trait method signatures, so `ReflectionMethod` objects returned from `new ReflectionClass(TraitName::class)` expose empty parameter metadata until trait signatures are retained in EIR metadata. ### Class constants diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 8b879f5ff5..26ebbf1e91 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -14,7 +14,7 @@ use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::codegen::platform::Arch; use crate::codegen::{CodegenIrError, Result}; -use crate::ir::{Immediate, Instruction, Op, ValueDef, ValueId}; +use crate::ir::{Immediate, Instruction, Op, TraitMethodInfo, ValueDef, ValueId}; use crate::names::php_symbol_key; use crate::parser::ast::Visibility; use crate::types::{AttrArgEntry, FunctionSig, InterfaceInfo, PhpType}; @@ -684,16 +684,38 @@ fn reflection_class_metadata( .get(trait_name) .cloned() .unwrap_or_default(); - return Ok(class_like_reflection_metadata( - trait_name, - Vec::new(), + let method_names = reflection_trait_method_names(ctx, trait_name); + let property_names = reflection_trait_property_names(ctx, trait_name); + let method_members = ctx + .module + .declared_trait_methods + .get(trait_name) + .map(|methods| reflection_trait_method_members(methods, &method_names)) + .unwrap_or_else(|| default_method_members(&method_names, false, true)); + let property_members = default_property_members(&property_names, false); + let constructor_member = reflection_constructor_member(&method_members); + return Ok(ReflectionOwnerMetadata { + reflected_name: Some(trait_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + interface_names: Vec::new(), trait_names, - reflection_trait_method_names(ctx, trait_name), - reflection_trait_property_names(ctx, trait_name), - false, - true, - false, - )); + method_names, + property_names, + method_members, + property_members, + constructor_member, + parameter_members: Vec::new(), + required_parameter_count: 0, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: true, + is_enum: false, + is_readonly: false, + modifiers: 0, + member_flags: ReflectionMemberFlags::default(), + }); } Ok(empty_reflection_metadata()) } @@ -712,33 +734,55 @@ fn reflection_method_metadata( let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionMethod")?; let method_name = const_required_string_operand(ctx, method_operand, "ReflectionMethod")?; let method_key = php_symbol_key(&method_name); - Ok(resolve_reflection_class(ctx, &reflected_class) - .and_then(|(_, info)| { - let member = reflection_class_method_member(info, &method_key)?; - Some(ReflectionOwnerMetadata { - reflected_name: Some(method_name.clone()), - attr_names: member.attr_names, - attr_args: member.attr_args, - interface_names: Vec::new(), - trait_names: Vec::new(), - method_names: Vec::new(), - property_names: Vec::new(), - method_members: Vec::new(), - property_members: Vec::new(), - constructor_member: None, - parameter_members: member.parameters, - required_parameter_count: member.required_parameter_count, - is_final: false, - is_abstract: false, - is_interface: false, - is_trait: false, - is_enum: false, - is_readonly: false, - modifiers: 0, - member_flags: member.flags, - }) - }) - .unwrap_or_else(empty_reflection_metadata)) + if let Some((_, info)) = resolve_reflection_class(ctx, &reflected_class) { + if let Some(member) = reflection_class_method_member(info, &method_key) { + return Ok(reflection_method_owner_metadata(&method_name, member)); + } + } + if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { + if let Some(info) = ctx.module.interface_infos.get(interface_name) { + if let Some(member) = reflection_interface_method_member(info, &method_key) { + return Ok(reflection_method_owner_metadata(&method_name, member)); + } + } + } + if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { + if let Some(methods) = ctx.module.declared_trait_methods.get(trait_name) { + if let Some(member) = reflection_trait_method_member(methods, &method_key) { + return Ok(reflection_method_owner_metadata(&method_name, member)); + } + } + } + Ok(empty_reflection_metadata()) +} + +/// Builds direct ReflectionMethod constructor metadata from one reflected method member. +fn reflection_method_owner_metadata( + method_name: &str, + member: ReflectionListedMember, +) -> ReflectionOwnerMetadata { + ReflectionOwnerMetadata { + reflected_name: Some(method_name.to_string()), + attr_names: member.attr_names, + attr_args: member.attr_args, + interface_names: Vec::new(), + trait_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), + constructor_member: None, + parameter_members: member.parameters, + required_parameter_count: member.required_parameter_count, + is_final: false, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + is_readonly: false, + modifiers: 0, + member_flags: member.flags, + } } /// Resolves `ReflectionProperty(class, property)` metadata. @@ -1179,6 +1223,39 @@ fn reflection_interface_method_member( }) } +/// Builds ReflectionMethod array entries for methods declared by a trait. +fn reflection_trait_method_members( + methods: &std::collections::HashMap, + method_names: &[String], +) -> Vec { + method_names + .iter() + .filter_map(|method_name| reflection_trait_method_member(methods, method_name)) + .collect() +} + +/// Builds one ReflectionMethod array entry from retained trait metadata. +fn reflection_trait_method_member( + methods: &std::collections::HashMap, + method_name: &str, +) -> Option { + let method_key = php_symbol_key(method_name); + let info = methods.get(&method_key)?; + Some(ReflectionListedMember { + name: method_key, + attr_names: Vec::new(), + attr_args: Vec::new(), + flags: reflection_member_flags( + info.is_static, + &info.visibility, + info.is_final, + info.is_abstract, + ), + required_parameter_count: reflection_required_parameter_count(&info.signature), + parameters: reflection_parameter_members(&info.signature), + }) +} + /// Builds ReflectionProperty array entries for the properties visible on one class. fn reflection_class_property_members( ctx: &FunctionContext<'_>, @@ -1415,44 +1492,6 @@ fn push_unique_property_name( } } -/// Builds empty ReflectionClass metadata for class-like symbols without stored attributes. -fn class_like_reflection_metadata( - class_like_name: &str, - interface_names: Vec, - trait_names: Vec, - method_names: Vec, - property_names: Vec, - is_interface: bool, - is_trait: bool, - is_enum: bool, -) -> ReflectionOwnerMetadata { - let method_members = default_method_members(method_names.as_slice(), is_interface, is_trait); - let property_members = default_property_members(property_names.as_slice(), is_interface); - let constructor_member = reflection_constructor_member(&method_members); - ReflectionOwnerMetadata { - reflected_name: Some(class_like_name.to_string()), - attr_names: Vec::new(), - attr_args: Vec::new(), - interface_names, - trait_names, - method_names, - property_names, - method_members, - property_members, - constructor_member, - parameter_members: Vec::new(), - required_parameter_count: 0, - is_final: false, - is_abstract: false, - is_interface, - is_trait, - is_enum, - is_readonly: false, - modifiers: if is_enum { 32 } else { 0 }, - member_flags: ReflectionMemberFlags::default(), - } -} - /// Looks up class-constant metadata by PHP-style class name and case-sensitive constant name. fn resolve_reflection_class_constant<'a>( ctx: &'a FunctionContext<'_>, diff --git a/src/ir/mod.rs b/src/ir/mod.rs index d4a525f648..4ea6b714ca 100644 --- a/src/ir/mod.rs +++ b/src/ir/mod.rs @@ -36,7 +36,7 @@ pub use instr::{ PassOrigin,}; pub use module::{ ClassTable, DataId, DataPool, EnumTable, ExternDecl, ExternParamDecl, InterfaceTable, - Module, PackedLayoutTable, + Module, PackedLayoutTable, TraitMethodInfo, }; pub use print::{print_function, print_module}; pub use types::{IrHeapKind, IrType}; diff --git a/src/ir/module.rs b/src/ir/module.rs index cbf2eeb1a4..9dd9368eee 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -15,6 +15,7 @@ use crate::codegen::platform::Target; use crate::codegen::RuntimeFeatures; use crate::ir::function::{Function, FunctionId}; use crate::ir::types::IrType; +use crate::parser::ast::Visibility; use crate::types::{ ClassInfo, EnumInfo, ExternClassInfo, FunctionSig, InterfaceInfo, PackedClassInfo, PhpType, }; @@ -35,6 +36,16 @@ impl DataId { } } +/// Method metadata retained for standalone trait reflection. +#[derive(Debug, Clone)] +pub struct TraitMethodInfo { + pub signature: FunctionSig, + pub visibility: Visibility, + pub is_static: bool, + pub is_final: bool, + pub is_abstract: bool, +} + /// Complete EIR module for one compile target. #[derive(Debug, Clone)] pub struct Module { @@ -59,6 +70,7 @@ pub struct Module { pub declared_trait_names: Vec, pub declared_trait_uses: HashMap>, pub declared_trait_method_names: HashMap>, + pub declared_trait_methods: HashMap>, pub declared_trait_property_names: HashMap>, pub class_infos: HashMap, pub interface_infos: HashMap, @@ -95,6 +107,7 @@ impl Module { declared_trait_names: Vec::new(), declared_trait_uses: HashMap::new(), declared_trait_method_names: HashMap::new(), + declared_trait_methods: HashMap::new(), declared_trait_property_names: HashMap::new(), class_infos: HashMap::new(), interface_infos: HashMap::new(), diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 588df57ead..3d4a1eb5c9 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -18,7 +18,7 @@ use crate::ir_lower::context::{ StaticCallableBinding, }; use crate::ir_lower::effects_lookup; -use crate::parser::ast::{Expr, ExprKind, Program, Stmt, StmtKind, TypeExpr}; +use crate::parser::ast::{ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind, TypeExpr}; use crate::span::Span; use crate::types::{CheckResult, ClassInfo, FunctionSig, PackedClassInfo, PhpType, TypeEnv}; @@ -325,6 +325,24 @@ pub(crate) fn lower_class_method( module.class_methods.push(function); } +/// Builds fallback method signature metadata from parsed class-like method syntax. +pub(crate) fn method_signature_from_ast(method: &ClassMethod) -> FunctionSig { + let mut signature = signature_from_ast_with_variadic( + &method.params, + method.return_type.as_ref(), + method.variadic.as_deref(), + ); + if let Some(variadic_type) = &method.variadic_type { + if let Some((_, php_type)) = signature.params.last_mut() { + *php_type = type_expr_to_php_type(variadic_type); + } + if let Some(declared) = signature.declared_params.last_mut() { + *declared = true; + } + } + signature +} + /// Lowers a synthetic `_class_propinit_` function for dynamic by-name allocation. pub(crate) fn lower_property_init_thunk( class_name: &str, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 38fc813db9..8b037fce17 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -17,6 +17,7 @@ use crate::codegen::RuntimeFeatures; use crate::intrinsics::IntrinsicCall; use crate::ir::{ validate_module, ExternDecl, ExternParamDecl, Function, Immediate, IrType, Module, Op, + TraitMethodInfo, }; use crate::ir_lower::{builtin_datetime, function, LoweringError}; use crate::names::php_symbol_key; @@ -73,6 +74,7 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec module.declared_trait_names = collect_declared_trait_names(program); module.declared_trait_uses = collect_declared_trait_uses(program); module.declared_trait_method_names = collect_declared_trait_method_names(program); + module.declared_trait_methods = collect_declared_trait_methods(program); module.declared_trait_property_names = collect_declared_trait_property_names(program); module.class_infos = check_result.classes.clone(); module.interface_infos = check_result.interfaces.clone(); @@ -433,6 +435,45 @@ fn collect_declared_trait_method_names(program: &Program) -> HashMap HashMap> { + let mut methods = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + methods: trait_methods, + .. + } => { + methods.insert( + name.clone(), + trait_methods + .iter() + .map(|method| { + let method_key = php_symbol_key(&method.name); + let info = TraitMethodInfo { + signature: function::method_signature_from_ast(method), + visibility: method.visibility.clone(), + is_static: method.is_static, + is_final: method.is_final, + is_abstract: method.is_abstract, + }; + (method_key, info) + }) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + methods.extend(collect_declared_trait_methods(body)); + } + _ => {} + } + } + methods +} + /// Collects direct PHP property names declared by each trait in source order. fn collect_declared_trait_property_names(program: &Program) -> HashMap> { let mut properties = HashMap::new(); diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index 9b4beb648a..48484670bd 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -97,6 +97,7 @@ impl Checker { enums: HashMap::new(), declared_interfaces: HashSet::new(), declared_traits: HashSet::new(), + declared_trait_methods: HashMap::new(), current_class: None, current_method: None, current_method_is_static: false, diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index ef5ea4830c..8cc81c0254 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -84,6 +84,7 @@ pub(super) fn check_types_impl( substitute_relative_class_types_in_flattened(&mut flattened_classes); substitute_relative_class_types_in_flattened_enums(&mut flattened_enums); let declared_traits = collect_declared_trait_names(program); + let declared_trait_methods = collect_declared_trait_methods(program); let mut seen_classes = HashSet::new(); let mut class_map = HashMap::new(); for class in &flattened_classes { @@ -181,6 +182,7 @@ pub(super) fn check_types_impl( checker.declared_classes = class_map.keys().cloned().collect(); checker.declared_interfaces = interface_map.keys().cloned().collect(); checker.declared_traits = declared_traits.clone(); + checker.declared_trait_methods = declared_trait_methods; // Enum names must resolve as types in member positions (property and // promoted-constructor-param types), which are checked during the class // schema pass — before the enum-processing phase populates `enums`. Pre- @@ -330,6 +332,33 @@ fn collect_declared_trait_names_into(program: &Program, names: &mut HashSet HashMap> { + let mut methods = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + methods: trait_methods, + .. + } => { + methods.insert( + name.clone(), + trait_methods + .iter() + .map(|method| php_symbol_key(&method.name)) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + methods.extend(collect_declared_trait_methods(body)); + } + _ => {} + } + } + methods +} + /// Builds method-checkable `FlattenedClass` units for every `enum` in the program so their method /// bodies go through the same validation as class methods. Enum signatures are already registered /// in `checker.classes` by the enum schema pass; these units only carry the names and method diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index 6d09b4ae28..caa91d76ab 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -57,19 +57,52 @@ impl Checker { method_name: &str, expr: &Expr, ) -> Result<(), CompileError> { - let Some(class_info) = self.classes.get(class_name) else { + let method_key = php_symbol_key(method_name); + if let Some(class_info) = self.classes.get(class_name) { + if !class_info.methods.contains_key(&method_key) + && !class_info.static_methods.contains_key(&method_key) + { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionMethod::__construct(): undefined method '{}::{}'", + class_name, method_name + ), + )); + } + let empty_names = Vec::new(); + let empty_args = Vec::new(); + let names = class_info + .method_attribute_names + .get(&method_key) + .unwrap_or(&empty_names); + let args = class_info + .method_attribute_args + .get(&method_key) + .unwrap_or(&empty_args); + return self.validate_reflection_attribute_metadata( + names, + args, + expr, + "ReflectionMethod::getAttributes(): method has attribute argument metadata that is not supported yet", + ); + } + if let Some(interface_info) = self.interfaces.get(class_name) { + if interface_info.methods.contains_key(&method_key) { + return Ok(()); + } return Err(CompileError::new( expr.span, &format!( - "ReflectionMethod::__construct(): undefined class '{}'", - class_name + "ReflectionMethod::__construct(): undefined method '{}::{}'", + class_name, method_name ), )); - }; - let method_key = php_symbol_key(method_name); - if !class_info.methods.contains_key(&method_key) - && !class_info.static_methods.contains_key(&method_key) - { + } + if let Some(trait_methods) = self.declared_trait_methods.get(class_name) { + if trait_methods.contains(&method_key) { + return Ok(()); + } return Err(CompileError::new( expr.span, &format!( @@ -78,22 +111,13 @@ impl Checker { ), )); } - let empty_names = Vec::new(); - let empty_args = Vec::new(); - let names = class_info - .method_attribute_names - .get(&method_key) - .unwrap_or(&empty_names); - let args = class_info - .method_attribute_args - .get(&method_key) - .unwrap_or(&empty_args); - self.validate_reflection_attribute_metadata( - names, - args, - expr, - "ReflectionMethod::getAttributes(): method has attribute argument metadata that is not supported yet", - ) + Err(CompileError::new( + expr.span, + &format!( + "ReflectionMethod::__construct(): undefined class '{}'", + class_name + ), + )) } /// Validates property-level attributes for `ReflectionProperty`. diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index 6b81abc2fa..d5d65df35c 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -112,6 +112,8 @@ pub(crate) struct Checker { /// Canonical trait names declared in the program, available for reflection /// and class-like metadata probes that accept traits. pub declared_traits: HashSet, + /// Case-insensitive method keys declared directly on each trait. + pub declared_trait_methods: HashMap>, /// Name of the class currently being type-checked (used for `$this` resolution). pub current_class: Option, /// Name of the current method being type-checked, when inside a class body. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 99d345b7b7..881da255a6 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1596,6 +1596,12 @@ fn test_reflection_method_get_parameters_returns_parameter_metadata() { class ReflectParamTarget { public function run(int $id, &$name, string $mode = "x", ...$rest) {} } +interface ReflectParamInterface { + public function iface(int $id, $name = "x"); +} +trait ReflectParamTrait { + private static function traitRun(int $id, $name = "x", string ...$rest) {} +} $method = new ReflectionMethod(ReflectParamTarget::class, "run"); echo $method->getNumberOfParameters() . "/"; echo $method->getNumberOfRequiredParameters() . ":"; @@ -1608,6 +1614,22 @@ foreach ($params as $param) { echo ($param->isVariadic() ? "V" : "v"); echo "|"; } +echo "\n"; +$iface = new ReflectionMethod(ReflectParamInterface::class, "iface"); +echo $iface->getNumberOfParameters() . "/"; +echo $iface->getNumberOfRequiredParameters() . ":"; +$ifaceParams = $iface->getParameters(); +echo $ifaceParams[0]->getName() . ($ifaceParams[0]->hasType() ? "T" : "t"); +echo ":" . $ifaceParams[1]->getName() . ($ifaceParams[1]->isOptional() ? "O" : "R"); +echo "\n"; +$trait = new ReflectionMethod(ReflectParamTrait::class, "traitRun"); +echo $trait->getNumberOfParameters() . "/"; +echo $trait->getNumberOfRequiredParameters() . ":"; +echo ($trait->isStatic() ? "S" : "s"); +echo ($trait->isPrivate() ? "R" : "r"); +$traitParams = $trait->getParameters(); +echo ":" . $traitParams[2]->getName() . ($traitParams[2]->isVariadic() ? "V" : "v"); +echo ($traitParams[2]->hasType() ? "T" : "t"); "##, ); assert!( @@ -1617,7 +1639,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "4/2:id#0TRbv|name#1tRBv|mode#2TObv|rest#3tObV|" + "4/2:id#0TRbv|name#1tRBv|mode#2TObv|rest#3tObV|\n2/1:idT:nameO\n3/1:SR:restVT" ); } @@ -1630,6 +1652,9 @@ fn test_reflection_class_get_methods_preserves_parameter_metadata() { class ReflectListedParamTarget { public function listed(int $first, $second = 2) {} } +trait ReflectListedParamTrait { + protected static function traitListed(int $first, $second = 2, string ...$rest) {} +} $methods = (new ReflectionClass(ReflectListedParamTarget::class))->getMethods(); foreach ($methods as $method) { if ($method->getName() === "listed") { @@ -1641,6 +1666,24 @@ foreach ($methods as $method) { echo $params[1]->getName() . ($params[1]->isOptional() ? "O" : "R"); } } +echo "|"; +$traitMethods = (new ReflectionClass(ReflectListedParamTrait::class))->getMethods(); +foreach ($traitMethods as $method) { + if ($method->getName() === "traitlisted") { + $params = $method->getParameters(); + echo $method->getNumberOfParameters() . "/"; + echo $method->getNumberOfRequiredParameters() . ":"; + echo ($method->isStatic() ? "S" : "s"); + echo ($method->isProtected() ? "P" : "p"); + echo ":"; + echo $params[0]->getName() . ($params[0]->hasType() ? "T" : "t"); + echo ":"; + echo $params[1]->getName() . ($params[1]->isOptional() ? "O" : "R"); + echo ":"; + echo $params[2]->getName() . ($params[2]->isVariadic() ? "V" : "v"); + echo ($params[2]->hasType() ? "T" : "t"); + } +} "##, ); assert!( @@ -1648,7 +1691,10 @@ foreach ($methods as $method) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2/1:firstT:secondO"); + assert_eq!( + out.stdout, + "2/1:firstT:secondO|3/1:SP:firstT:secondO:restVT" + ); } /// Verifies that `ReflectionClass::getConstructor()` returns a ReflectionMethod @@ -1719,7 +1765,7 @@ if ($trait instanceof ReflectionMethod) { ); assert_eq!( out, - "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/0/0" + "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/3/1" ); } From 2c13e7aeafaa13774c5cb6463354e8e4ebbb46e1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 04:24:58 +0200 Subject: [PATCH 0384/1208] Support ReflectionClass parent metadata --- .../elephc-eval/src/interpreter/reflection.rs | 64 +++++++++++ .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 27 +++++ .../interpreter/tests/support/object_ops.rs | 6 ++ .../interpreter/tests/support/runtime_ops.rs | 2 + .../elephc-eval/src/runtime_hooks/externs.rs | 1 + crates/elephc-eval/src/runtime_hooks/ops.rs | 2 + docs/php/classes.md | 3 +- docs/php/eval.md | 4 +- src/codegen/eval_reflection_owner_helpers.rs | 58 +++++++++- src/codegen/lower_inst/objects/reflection.rs | 101 +++++++++++++++--- src/types/checker/builtin_types/reflection.rs | 39 +++++++ tests/codegen/eval.rs | 25 +++++ tests/codegen/oop/attributes.rs | 18 ++++ 14 files changed, 331 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 21bd924139..0d8dbf7ef9 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -39,6 +39,7 @@ struct EvalReflectionClassMetadata { trait_names: Vec, method_names: Vec, property_names: Vec, + parent_class_name: Option, } /// Eval metadata needed to materialize one `ReflectionMethod` or `ReflectionProperty` owner object. @@ -118,6 +119,7 @@ fn eval_reflection_class_new( &metadata.trait_names, &metadata.method_names, &metadata.property_names, + metadata.parent_class_name.as_deref(), &[], metadata.flags, metadata.modifiers, @@ -158,6 +160,7 @@ fn eval_reflection_method_new( &[], &[], &[], + None, &method.parameters, flags, method.required_parameter_count as u64, @@ -193,6 +196,7 @@ fn eval_reflection_property_new( &[], &[], &[], + None, &[], flags, 0, @@ -228,6 +232,7 @@ fn eval_reflection_class_constant_new( &[], &[], &[], + None, &[], 0, 0, @@ -272,6 +277,7 @@ fn eval_reflection_enum_case_new( &[], &[], &[], + None, &[], 0, 0, @@ -290,6 +296,7 @@ fn eval_reflection_owner_object( trait_names: &[String], method_names: &[String], property_names: &[String], + parent_class_name: Option<&str>, parameter_metadata: &[EvalReflectionParameterMetadata], flags: u64, modifiers: u64, @@ -325,6 +332,8 @@ fn eval_reflection_owner_object( } else { values.array_new(0)? }; + let parent_class = + eval_reflection_parent_class_result(owner_kind, parent_class_name, context, values)?; let object = values.reflection_owner_new( owner_kind, reflected_name, @@ -335,6 +344,7 @@ fn eval_reflection_owner_object( property_names_array, method_objects, property_objects, + parent_class, flags, modifiers, )?; @@ -349,9 +359,43 @@ fn eval_reflection_owner_object( values.release(property_names_array)?; values.release(method_objects)?; values.release(property_objects)?; + values.release(parent_class)?; Ok(object) } +/// Builds the `ReflectionClass|false` value stored in one ReflectionClass parent slot. +fn eval_reflection_parent_class_result( + owner_kind: u64, + parent_class_name: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if owner_kind != EVAL_REFLECTION_OWNER_CLASS { + return values.bool_value(false); + } + let Some(parent_class_name) = parent_class_name else { + return values.bool_value(false); + }; + let Some(metadata) = eval_reflection_class_like_attributes(parent_class_name, context) else { + return values.bool_value(false); + }; + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + &metadata.attributes, + &metadata.interface_names, + &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, + metadata.parent_class_name.as_deref(), + &[], + metadata.flags, + metadata.modifiers, + context, + values, + ) +} + /// Builds an indexed PHP string array for ReflectionClass metadata names. fn eval_reflection_string_array_result( names: &[String], @@ -390,6 +434,7 @@ fn eval_reflection_parameter_object_result( let property_names = values.array_new(0)?; let method_objects = values.array_new(0)?; let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; let flags = eval_reflection_parameter_flags(parameter); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_PARAMETER, @@ -401,6 +446,7 @@ fn eval_reflection_parameter_object_result( property_names, method_objects, property_objects, + parent_class, flags, parameter.position as u64, )?; @@ -411,6 +457,7 @@ fn eval_reflection_parameter_object_result( values.release(property_names)?; values.release(method_objects)?; values.release(property_objects)?; + values.release(parent_class)?; Ok(object) } @@ -443,6 +490,7 @@ fn eval_reflection_member_object_array_result( &[], &[], &[], + None, &member.parameters, flags, member.required_parameter_count as u64, @@ -505,6 +553,7 @@ fn eval_reflection_class_like_attributes( trait_names: context.class_trait_names(class.name()), method_names: context.class_method_names(class.name()), property_names: context.class_property_names(class.name()), + parent_class_name: eval_reflection_parent_class_name(class, context), flags, modifiers, }); @@ -517,6 +566,7 @@ fn eval_reflection_class_like_attributes( trait_names: Vec::new(), method_names: context.interface_method_names(interface.name()), property_names: context.interface_property_names(interface.name()), + parent_class_name: None, flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE, modifiers: 0, }); @@ -529,6 +579,7 @@ fn eval_reflection_class_like_attributes( trait_names: Vec::new(), method_names: context.trait_method_names(trait_decl.name()), property_names: context.trait_property_names(trait_decl.name()), + parent_class_name: None, flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT, modifiers: 0, }); @@ -542,11 +593,24 @@ fn eval_reflection_class_like_attributes( trait_names: Vec::new(), method_names: context.class_method_names(enum_decl.name()), property_names: context.class_property_names(enum_decl.name()), + parent_class_name: None, flags: EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM, modifiers: 32, }) } +/// Returns the canonical eval parent class name for ReflectionClass metadata. +fn eval_reflection_parent_class_name( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Option { + let parent = class.parent()?; + context + .class(parent) + .map(|parent_class| parent_class.name().trim_start_matches('\\').to_string()) + .or_else(|| Some(parent.trim_start_matches('\\').to_string())) +} + /// Computes PHP's `ReflectionClass::getModifiers()` bitmask for eval metadata. fn eval_reflection_class_modifiers( is_final: bool, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 4c85e8a6e7..e29a185f4e 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -124,6 +124,7 @@ pub trait RuntimeValueOps { property_names: RuntimeCellHandle, method_objects: RuntimeCellHandle, property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 2eabb4732b..c7be480932 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -603,6 +603,33 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::getParentClass returns eval parent metadata or false. +#[test] +fn execute_program_reflection_class_get_parent_class() { + let program = parse_fragment( + br#"class EvalReflectParentBase {} +class EvalReflectParentChild extends EvalReflectParentBase {} +$parent = (new ReflectionClass("EvalReflectParentChild"))->getParentClass(); +echo $parent->getName(); +echo ":"; +$root = (new ReflectionClass("EvalReflectParentBase"))->getParentClass(); +if ($root === false) { + echo "false"; +} else { + echo "bad"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectParentBase:false"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::newInstance constructs eval-declared classes. #[test] fn execute_program_reflection_class_new_instance_constructs_eval_class() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index c5773b6692..af60f40972 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -149,6 +149,10 @@ impl FakeOps { Self::object_property(&properties, "__is_readonly") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "getparentclass") if args.is_empty() => { + Self::object_property(&properties, "__parent_class") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) } @@ -347,6 +351,7 @@ impl FakeOps { property_names: RuntimeCellHandle, method_objects: RuntimeCellHandle, property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -390,6 +395,7 @@ impl FakeOps { properties.push(("__method_names".to_string(), method_names)); properties.push(("__property_names".to_string(), property_names)); properties.push(("__methods".to_string(), method_objects)); + properties.push(("__parent_class".to_string(), parent_class)); properties.push(("__properties".to_string(), property_objects)); } if owner_kind == EVAL_REFLECTION_OWNER_METHOD diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 0e05ca2193..9076291c46 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -133,6 +133,7 @@ impl RuntimeValueOps for FakeOps { property_names: RuntimeCellHandle, method_objects: RuntimeCellHandle, property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -146,6 +147,7 @@ impl RuntimeValueOps for FakeOps { property_names, method_objects, property_objects, + parent_class, flags, modifiers, ) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 5c49e1a70c..8dc9290360 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -85,6 +85,7 @@ unsafe extern "C" { property_names: *mut RuntimeCell, method_objects: *mut RuntimeCell, property_objects: *mut RuntimeCell, + parent_class: *mut RuntimeCell, flags: u64, modifiers: u64, ) -> *mut RuntimeCell; diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index b31fcdaeb3..c41443cdd6 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -210,6 +210,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { property_names: RuntimeCellHandle, method_objects: RuntimeCellHandle, property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, ) -> Result { @@ -225,6 +226,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { property_names.as_ptr(), method_objects.as_ptr(), property_objects.as_ptr(), + parent_class.as_ptr(), flags, modifiers, ) diff --git a/docs/php/classes.md b/docs/php/classes.md index 4b1287a770..2122918a45 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1109,6 +1109,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | | `ReflectionClass::getConstructor()` | `new ReflectionClass($class_name)` | Return the reflected constructor as a `ReflectionMethod`, or `null` when no constructor is visible | +| `ReflectionClass::getParentClass()` | `new ReflectionClass($class_name)` | Return the reflected parent class as a `ReflectionClass`, or `false` when the reflected class-like symbol has no parent class | | `ReflectionClass::getProperties()` | `new ReflectionClass($class_name)` | Return `ReflectionProperty` objects for properties visible through the reflected class-like metadata | | `ReflectionClass::newInstance()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class with forwarded constructor arguments | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | @@ -1193,7 +1194,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 56dca2d384..d1be0bc2ff 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -171,7 +171,9 @@ method lookup is case-insensitive, while property lookup is case-sensitive. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate -flags. `ReflectionClass::newInstance()` constructs eval-declared reflected +flags. `ReflectionClass::getParentClass()` returns a materialized +`ReflectionClass` for eval-declared parent classes or `false` when no parent +class exists. `ReflectionClass::newInstance()` constructs eval-declared reflected classes and forwards constructor arguments through eval's positional, named, and unpacking-aware call binding. Eval-declared method parameter type hints are checked when the method is diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index ccc38cdb1e..9eaf9b0abe 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -42,6 +42,8 @@ struct ReflectionOwnerLayout { method_objects_hi: Option, property_objects_lo: Option, property_objects_hi: Option, + parent_class_lo: Option, + parent_class_hi: Option, attrs_lo: usize, attrs_hi: usize, is_final_lo: Option, @@ -176,6 +178,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, property_members: Vec, constructor_member: Option, + parent_class_name: Option, parameter_members: Vec, required_parameter_count: i64, is_final: bool, @@ -98,6 +99,19 @@ pub(super) fn lower_reflection_owner_new( class_name: &str, ) -> Result<()> { let metadata = reflection_owner_metadata(ctx, class_name, inst)?; + emit_reflection_owner_object(ctx, class_name, &metadata)?; + let result = inst + .result + .ok_or_else(|| CodegenIrError::invalid_module("reflection object_new missing result"))?; + ctx.store_result_value(result) +} + +/// Allocates and populates one builtin Reflection owner object from metadata. +fn emit_reflection_owner_object( + ctx: &mut FunctionContext<'_>, + class_name: &str, + metadata: &ReflectionOwnerMetadata, +) -> Result<()> { let (class_id, property_count, uninitialized_marker_offsets) = { let class_info = ctx .module @@ -149,6 +163,7 @@ pub(super) fn lower_reflection_owner_new( &metadata.method_members, )?; emit_reflection_constructor_property(ctx, metadata.constructor_member.as_ref())?; + emit_reflection_parent_class_property(ctx, metadata.parent_class_name.as_deref())?; emit_reflection_member_array_property_by_name( ctx, "__properties", @@ -187,10 +202,7 @@ pub(super) fn lower_reflection_owner_new( )?; } emit_reflection_member_flag_properties(ctx, class_name, metadata.member_flags)?; - let result = inst - .result - .ok_or_else(|| CodegenIrError::invalid_module("reflection object_new missing result"))?; - ctx.store_result_value(result) + Ok(()) } /// Lowers `new ReflectionFunction("name")` by populating its name and @@ -607,6 +619,14 @@ fn reflection_class_metadata( return Ok(empty_reflection_metadata()); }; let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionClass")?; + reflection_class_metadata_for_name(ctx, &reflected_class) +} + +/// Resolves `ReflectionClass(name)` metadata for a known class-like name. +fn reflection_class_metadata_for_name( + ctx: &FunctionContext<'_>, + reflected_class: &str, +) -> Result { if let Some((class_name, info)) = resolve_reflection_class(ctx, &reflected_class) { let is_enum = is_reflection_enum(ctx, class_name); let method_names = reflection_class_method_names(ctx, class_name); @@ -626,6 +646,7 @@ fn reflection_class_metadata( method_members, property_members, constructor_member, + parent_class_name: reflection_parent_class_name(ctx, info), parameter_members: Vec::new(), required_parameter_count: 0, is_final: info.is_final, @@ -665,6 +686,7 @@ fn reflection_class_metadata( method_members, property_members, constructor_member, + parent_class_name: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -705,6 +727,7 @@ fn reflection_class_metadata( method_members, property_members, constructor_member, + parent_class_name: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -772,6 +795,7 @@ fn reflection_method_owner_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parent_class_name: None, parameter_members: member.parameters, required_parameter_count: member.required_parameter_count, is_final: false, @@ -811,6 +835,7 @@ fn reflection_property_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parent_class_name: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -853,6 +878,7 @@ fn reflection_class_constant_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parent_class_name: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -889,6 +915,7 @@ fn reflection_class_constant_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parent_class_name: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -932,6 +959,7 @@ fn reflection_enum_case_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parent_class_name: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -993,6 +1021,17 @@ fn is_reflection_enum(ctx: &FunctionContext<'_>, enum_name: &str) -> bool { .any(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == enum_key) } +/// Returns the canonical parent class name for a reflected class, if any. +fn reflection_parent_class_name( + ctx: &FunctionContext<'_>, + info: &crate::types::ClassInfo, +) -> Option { + let parent = info.parent.as_ref()?; + resolve_reflection_class(ctx, parent) + .map(|(parent_name, _)| parent_name.to_string()) + .or_else(|| Some(parent.trim_start_matches('\\').to_string())) +} + /// Collects direct and inherited parent interfaces for a reflected interface. fn reflection_interface_parent_names( ctx: &FunctionContext<'_>, @@ -1533,6 +1572,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, + parent_class_name: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -1799,6 +1839,39 @@ fn emit_reflection_constructor_property( Ok(()) } +/// Replaces the ReflectionClass private parent slot with `ReflectionClass|false`. +fn emit_reflection_parent_class_property( + ctx: &mut FunctionContext<'_>, + parent_class_name: Option<&str>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, "__parent_class")?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(parent_class_name) = parent_class_name { + let parent_metadata = reflection_class_metadata_for_name(ctx, parent_class_name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &parent_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionClass".to_string()), + ); + } else { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + /// Replaces a ReflectionMethod private array slot with ReflectionParameter objects. fn emit_reflection_parameter_array_property_by_name( ctx: &mut FunctionContext<'_>, @@ -2054,32 +2127,32 @@ fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) /// Appends ReflectionClass metadata names to the current ARM64 result array. fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result } /// Appends ReflectionClass metadata names to the current x86_64 result array. fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings - ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result } /// Stores ReflectionMethod/ReflectionProperty boolean predicate slots when supported. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 8e58e39542..70e1fd8ca7 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -723,6 +723,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(nullable_object_type("ReflectionMethod")), null_expr(), ), + builtin_property( + "__parent_class", + Visibility::Private, + Some(mixed_type()), + false_bool(), + ), builtin_property( "__properties", Visibility::Private, @@ -770,6 +776,7 @@ fn builtin_reflection_class() -> FlattenedClass { "__constructor", "ReflectionMethod", ), + builtin_reflection_class_mixed_method("getParentClass", "__parent_class"), builtin_reflection_class_array_method( "getProperties", "__properties", @@ -981,6 +988,35 @@ fn builtin_reflection_class_nullable_object_method( } } +/// Returns a public mixed `ReflectionClass` method backed by one private slot. +fn builtin_reflection_class_mixed_method(method_name: &str, property: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass` boolean method backed by one private slot. fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -1296,6 +1332,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { PhpType::Void, ]); } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParentClass")) { + sig.return_type = PhpType::Mixed; + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ec429894fb..f7f1d6cdd0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5739,6 +5739,31 @@ echo count($parentInterfaces) . ":" . $parentInterfaces[0];'); ); } +/// Verifies eval ReflectionClass::getParentClass crosses the generated runtime bridge. +#[test] +fn test_eval_reflection_class_get_parent_class() { + let out = compile_and_run_capture( + r#"getParentClass(); +echo $parent->getName() . ":"; +$root = (new ReflectionClass("EvalBridgeParent"))->getParentClass(); +if ($root === false) { + echo "false"; +} else { + echo "bad"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalBridgeParent:false"); +} + /// Verifies eval ReflectionClass reports class-like final and abstract flags. #[test] fn test_eval_reflection_class_modifier_flags() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 881da255a6..efcca7ecd7 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1302,6 +1302,24 @@ echo count($parentInterfaces) . ":" . $parentInterfaces[0]; ); } +/// Verifies that `ReflectionClass::getParentClass()` returns a ReflectionClass +/// object for subclasses and `false` for parentless classes. +#[test] +fn test_reflection_class_get_parent_class() { + let out = compile_and_run( + r#"getParentClass(); +echo $parent instanceof ReflectionClass ? $parent->getName() : "missing"; +echo ":"; +$root = (new ReflectionClass(StaticParentBase::class))->getParentClass(); +echo $root === false ? "false" : "bad"; +"#, + ); + assert_eq!(out, "StaticParentBase:false"); +} + /// Verifies that `ReflectionClass::getName()` returns the canonical declared /// name after case-insensitive class-string construction. #[test] From 861dd7dabb3f97ca0a639052db80947c240bdea0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 04:44:31 +0200 Subject: [PATCH 0385/1208] Support ReflectionClass instantiability metadata --- .../elephc-eval/src/interpreter/reflection.rs | 19 +++++++++++ .../tests/builtins_class_metadata.rs | 32 +++++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 6 ++++ docs/php/classes.md | 3 +- docs/php/eval.md | 6 ++-- src/codegen/eval_reflection_owner_helpers.rs | 22 +++++++++++++ src/codegen/lower_inst/objects/reflection.rs | 27 ++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 8 +++++ tests/codegen/eval.rs | 31 ++++++++++++++++++ tests/codegen/oop/attributes.rs | 27 ++++++++++++++++ 10 files changed, 178 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 0d8dbf7ef9..f18ad119ee 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -18,6 +18,7 @@ const EVAL_REFLECTION_CLASS_FLAG_INTERFACE: u64 = 4; const EVAL_REFLECTION_CLASS_FLAG_TRAIT: u64 = 8; const EVAL_REFLECTION_CLASS_FLAG_ENUM: u64 = 16; const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; +const EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE: u64 = 64; const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; @@ -540,6 +541,9 @@ fn eval_reflection_class_like_attributes( if class.is_readonly_class() && !is_enum { flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; } + if eval_reflection_class_is_instantiable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; + } let modifiers = eval_reflection_class_modifiers( class.is_final(), class.is_abstract(), @@ -611,6 +615,21 @@ fn eval_reflection_parent_class_name( .or_else(|| Some(parent.trim_start_matches('\\').to_string())) } +/// Returns PHP's `ReflectionClass::isInstantiable()` value for eval class metadata. +fn eval_reflection_class_is_instantiable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_method(class.name(), "__construct") + .map(|(_, method)| method.visibility() == EvalVisibility::Public) + .unwrap_or(true) +} + /// Computes PHP's `ReflectionClass::getModifiers()` bitmask for eval metadata. fn eval_reflection_class_modifiers( is_final: bool, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c7be480932..a1ad63a081 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -362,6 +362,38 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass exposes eval class instantiability metadata. +#[test] +fn execute_program_reflects_eval_class_instantiable_predicate() { + let program = parse_fragment( + br#"abstract class EvalInstAbstract {} +class EvalInstPublic {} +final class EvalInstFinal {} +class EvalInstPrivate { private function __construct() {} } +class EvalInstProtected { protected function __construct() {} } +interface EvalInstIface {} +trait EvalInstTrait {} +enum EvalInstEnum { case Ready; } +echo (new ReflectionClass("EvalInstAbstract"))->isInstantiable() ? "A" : "a"; +echo (new ReflectionClass("EvalInstPublic"))->isInstantiable() ? "B" : "b"; +echo (new ReflectionClass("EvalInstFinal"))->isInstantiable() ? "C" : "c"; +echo (new ReflectionClass("EvalInstPrivate"))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass("EvalInstProtected"))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass("EvalInstIface"))->isInstantiable() ? "I" : "i"; +echo (new ReflectionClass("EvalInstTrait"))->isInstantiable() ? "T" : "t"; +echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "aBCprite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass reports eval class-like method and property membership. #[test] fn execute_program_reflects_eval_class_member_existence() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index af60f40972..62f3330d19 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -149,6 +149,10 @@ impl FakeOps { Self::object_property(&properties, "__is_readonly") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isinstantiable") if args.is_empty() => { + Self::object_property(&properties, "__is_instantiable") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getparentclass") if args.is_empty() => { Self::object_property(&properties, "__parent_class") .map_or_else(|| self.bool_value(false), Ok) @@ -372,6 +376,7 @@ impl FakeOps { let is_trait = self.bool_value((flags & 8) != 0)?; let is_enum = self.bool_value((flags & 16) != 0)?; let is_readonly = self.bool_value((flags & 32) != 0)?; + let is_instantiable = self.bool_value((flags & 64) != 0)?; let modifiers_cell = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { @@ -386,6 +391,7 @@ impl FakeOps { properties.push(("__is_trait".to_string(), is_trait)); properties.push(("__is_enum".to_string(), is_enum)); properties.push(("__is_readonly".to_string(), is_readonly)); + properties.push(("__is_instantiable".to_string(), is_instantiable)); properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__short_name".to_string(), short_name)); properties.push(("__namespace_name".to_string(), namespace_name)); diff --git a/docs/php/classes.md b/docs/php/classes.md index 2122918a45..eefcc52c69 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1102,6 +1102,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isTrait()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is a trait | | `ReflectionClass::isEnum()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an enum | | `ReflectionClass::isReadOnly()` | `new ReflectionClass($class_name)` | Return whether the reflected class is a `readonly class` | +| `ReflectionClass::isInstantiable()` | `new ReflectionClass($class_name)` | Return whether the reflected symbol is a concrete class with no constructor or a public constructor | | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | | `ReflectionClass::hasMethod()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named method; method lookup is case-insensitive | | `ReflectionClass::hasProperty()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named property; property lookup is case-sensitive | @@ -1194,7 +1195,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index d1be0bc2ff..5435b5faee 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -160,8 +160,10 @@ derive namespace-aware parts from the resolved eval class-like name. `ReflectionClass::isEnum()` report eval class-like metadata, including PHP-compatible enum finality and class-like kind checks for eval interfaces, traits, and enums. `ReflectionClass::isReadOnly()` reports eval `readonly class` -metadata. `ReflectionClass::getModifiers()` returns PHP's -`ReflectionClass::IS_*` modifier bitmask for eval class-like metadata. +metadata. `ReflectionClass::isInstantiable()` reports whether eval class-like +metadata describes a concrete class with no constructor or a public constructor. +`ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::IS_*` +modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval classes and parent interfaces for eval interfaces, while `ReflectionClass::getTraitNames()` returns traits used directly by eval classes. diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 9eaf9b0abe..32d64052e9 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -58,6 +58,8 @@ struct ReflectionOwnerLayout { is_enum_hi: Option, is_readonly_lo: Option, is_readonly_hi: Option, + is_instantiable_lo: Option, + is_instantiable_hi: Option, modifiers_lo: Option, modifiers_hi: Option, in_namespace_lo: Option, @@ -185,6 +187,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, +) -> bool { + if info.is_abstract || is_enum { + return false; + } + constructor_member + .map(|member| member.flags.is_public) + .unwrap_or(true) +} + /// Collects direct and inherited parent interfaces for a reflected interface. fn reflection_interface_parent_names( ctx: &FunctionContext<'_>, @@ -1581,6 +1607,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { is_trait: false, is_enum: false, is_readonly: false, + is_instantiable: false, modifiers: 0, member_flags: ReflectionMemberFlags::default(), } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 70e1fd8ca7..657b9df2ef 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -663,6 +663,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__is_instantiable", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), builtin_property( "__modifiers", Visibility::Private, @@ -763,6 +769,7 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_bool_method("isTrait", "__is_trait"), builtin_reflection_class_bool_method("isEnum", "__is_enum"), builtin_reflection_class_bool_method("isReadOnly", "__is_readonly"), + builtin_reflection_class_bool_method("isInstantiable", "__is_instantiable"), builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), @@ -1305,6 +1312,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "istrait", "isenum", "isreadonly", + "isinstantiable", "hasmethod", "hasproperty", ] { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f7f1d6cdd0..0ab32e0aca 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5865,6 +5865,37 @@ echo (new ReflectionClass("EvalReadonlyTrait"))->isReadOnly() ? "R" : "r";'); assert_eq!(out.stdout, "rRRrrr"); } +/// Verifies eval ReflectionClass reports instantiability through the bridge. +#[test] +fn test_eval_reflection_class_instantiable_predicate() { + let out = compile_and_run_capture( + r#"isInstantiable() ? "A" : "a"; +echo (new ReflectionClass("EvalInstPublic"))->isInstantiable() ? "B" : "b"; +echo (new ReflectionClass("EvalInstFinal"))->isInstantiable() ? "C" : "c"; +echo (new ReflectionClass("EvalInstPrivate"))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass("EvalInstProtected"))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass("EvalInstIface"))->isInstantiable() ? "I" : "i"; +echo (new ReflectionClass("EvalInstTrait"))->isInstantiable() ? "T" : "t"; +echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "aBCprite"); +} + /// Verifies eval ReflectionClass reports method and property membership through the bridge. #[test] fn test_eval_reflection_class_member_existence() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index efcca7ecd7..1b2b794556 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1206,6 +1206,33 @@ echo (new ReflectionClass(StaticReadonlyTrait::class))->isReadOnly() ? "R" : "r" assert_eq!(out, "rRRrrr"); } +/// Verifies that `ReflectionClass::isInstantiable()` reports PHP constructor +/// visibility and class-kind rules for static metadata. +#[test] +fn test_reflection_class_is_instantiable() { + let out = compile_and_run( + r#"isInstantiable() ? "A" : "a"; +echo (new ReflectionClass(ReflectInstPublic::class))->isInstantiable() ? "B" : "b"; +echo (new ReflectionClass(ReflectInstFinal::class))->isInstantiable() ? "C" : "c"; +echo (new ReflectionClass(ReflectInstPrivate::class))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass(ReflectInstProtected::class))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass(ReflectInstIface::class))->isInstantiable() ? "I" : "i"; +echo (new ReflectionClass(ReflectInstTrait::class))->isInstantiable() ? "T" : "t"; +echo (new ReflectionClass(ReflectInstEnum::class))->isInstantiable() ? "E" : "e"; +"#, + ); + assert_eq!(out, "aBCprite"); +} + /// Verifies that `ReflectionClass::hasMethod()` and `hasProperty()` report /// PHP-visible members for static class-like metadata. #[test] From 1b8fef9a8971c44dbe24bda29b3ffb3bf5d9e9fc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 05:01:04 +0200 Subject: [PATCH 0386/1208] Support private parent property shadowing --- .../elephc-eval/src/interpreter/statements.rs | 23 ++- .../src/interpreter/tests/expressions.rs | 68 ++++++++ docs/php/classes.md | 7 +- docs/php/eval.md | 3 + src/codegen/eval_property_helpers.rs | 10 +- src/codegen/lower_inst/objects.rs | 49 ++---- src/codegen_support/runtime/data/user.rs | 11 +- src/ir_lower/expr/mod.rs | 3 +- src/ir_lower/stmt/mod.rs | 6 +- src/ir_lower/tests/exhaustive.rs | 2 + src/types/checker/inference/objects/access.rs | 2 +- src/types/checker/method_pass.rs | 4 +- src/types/checker/mod.rs | 5 + .../checker/schema/classes/properties.rs | 148 +++++++++++++++--- src/types/checker/schema/classes/state.rs | 18 +++ src/types/checker/schema/enums.rs | 12 ++ .../stmt_check/assignments/properties.rs | 29 ++-- src/types/checker/type_compat/object_types.rs | 12 +- src/types/schema.rs | 76 +++++++++ tests/codegen/eval.rs | 30 ++++ tests/codegen/oop/inheritance.rs | 75 +++++++++ tests/error_tests/misc/classes.rs | 10 -- 22 files changed, 493 insertions(+), 110 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 8b138e2b71..0eb90fa1f9 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1506,10 +1506,12 @@ pub(in crate::interpreter) fn eval_property_get_result( return values.property_get(object, property_name); }; let object_class_name = class.name().to_string(); + let mut storage_property_name = property_name.to_string(); if let Some((declaring_class, property)) = eval_dynamic_property_for_access(&object_class_name, property_name, context) { validate_eval_member_access(&declaring_class, property.visibility(), context)?; + storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); if property.has_get_hook() && !current_eval_property_hook_is( &declaring_class, @@ -1535,7 +1537,7 @@ pub(in crate::interpreter) fn eval_property_get_result( ); } } - values.property_get(object, property_name) + values.property_get(object, &storage_property_name) } /// Writes one object property while enforcing eval-declared member visibility. @@ -1556,11 +1558,13 @@ pub(in crate::interpreter) fn eval_property_set_result( if context.has_enum(&object_class_name) { return Err(EvalStatus::RuntimeFatal); } + let mut storage_property_name = property_name.to_string(); if let Some((declaring_class, property)) = eval_dynamic_property_for_access(&object_class_name, property_name, context) { validate_eval_member_access(&declaring_class, property.visibility(), context)?; validate_eval_readonly_property_write(&declaring_class, &property, context)?; + storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); if property.has_set_hook() { if !current_eval_property_hook_is( &declaring_class, @@ -1594,7 +1598,7 @@ pub(in crate::interpreter) fn eval_property_set_result( return Err(EvalStatus::RuntimeFatal); } } - values.property_set(object, property_name, value) + values.property_set(object, &storage_property_name, value) } /// Validates that an object property may be used as a by-reference method argument. @@ -1710,6 +1714,18 @@ fn eval_dynamic_property_for_access( context.class_property(object_class_name, property_name) } +/// Returns the physical storage name for an eval object property slot. +fn eval_instance_property_storage_name( + declaring_class: &str, + property: &EvalClassProperty, +) -> String { + if property.visibility() == EvalVisibility::Private { + format!("\0{}\0{}", declaring_class.trim_start_matches('\\'), property.name()) + } else { + property.name().to_string() + } +} + /// Reads one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_get_result( class_name: &str, @@ -2092,7 +2108,8 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( } else { values.null()? }; - values.property_set(object, property.name(), value)?; + let storage_name = eval_instance_property_storage_name(class.name(), property); + values.property_set(object, &storage_name, value)?; } } if let Some((constructor_class, constructor)) = diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 826257cf77..07db502777 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -668,6 +668,74 @@ return $box->read(2);"#, assert_eq!(values.get(result), FakeValue::Int(7)); } + +/// Verifies eval child properties shadow private parent properties with a separate storage slot. +#[test] +fn execute_program_shadows_private_eval_parent_property_with_separate_slot() { + let program = parse_fragment( + br#"class EvalPrivateShadowBase { + private $value = 1; + + public function parentValue() { + return $this->value; + } +} +class EvalPrivateShadowChild extends EvalPrivateShadowBase { + public $value = "child"; + + public function childValue() { + return $this->value; + } +} +$box = new EvalPrivateShadowChild(); +echo $box->parentValue(); echo ":"; +echo $box->childValue(); echo ":"; +echo $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:child:child"); +} + +/// Verifies eval later redeclarations update the visible slot while preserving a private grandparent slot. +#[test] +fn execute_program_keeps_eval_private_grandparent_slot_after_later_redeclaration() { + let program = parse_fragment( + br#"class EvalPrivateGrandBase { + private $value = 1; + + public function grandValue() { + return $this->value; + } +} +class EvalPrivateGrandParent extends EvalPrivateGrandBase { + public $value = 2; + + public function parentValue() { + return $this->value; + } +} +class EvalPrivateGrandChild extends EvalPrivateGrandParent { + public $value = 3; +} +$box = new EvalPrivateGrandChild(); +echo $box->grandValue(); echo ":"; +echo $box->parentValue(); echo ":"; +echo $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:3"); +} + /// Verifies eval rejects private member access from global scope. #[test] fn execute_program_rejects_private_eval_member_access_from_global_scope() { diff --git a/docs/php/classes.md b/docs/php/classes.md index eefcc52c69..91590952b6 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -313,6 +313,12 @@ A child class may redeclare a property inherited from a non-private parent. The - `final` parent properties cannot be redeclared. - The child shares the parent's slot, so reads of the property from inherited methods see the child's value. +Private parent properties are different: they are not overridden. A child may +declare a same-named property, and elephc keeps a separate storage slot for each +declaring class. Methods declared on the parent keep reading/writing the private +parent slot, while child methods and external public access use the child +property. + ```php , ) { - for (property, ty) in &class_info.properties { - if !property_is_public(class_info, property) || !property_type_supported(ty) { + for (index, (property, ty)) in class_info.properties.iter().enumerate() { + if class_info.visible_property_index(property) != Some(index) { continue; } - let Some(offset) = class_info.property_offsets.get(property).copied() else { + if !property_is_public(class_info, property) || !property_type_supported(ty) { continue; - }; + } slots.push(EvalPropertySlot { class_id: class_info.class_id, class_name: class_name.to_string(), property: property.clone(), - offset, + offset: 8 + index * 16, ty: ty.codegen_repr(), }); } diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index d9f9e1e742..ab60c91642 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1927,11 +1927,7 @@ fn collect_property_defaults( let Some(default_expr) = class_info.defaults.get(index).and_then(Option::as_ref) else { continue; }; - let offset = class_info - .property_offsets - .get(property) - .copied() - .unwrap_or(8 + index * 16); + let offset = 8 + index * 16; defaults.push(PropertyDefault { offset, value: literal_default_value( @@ -3898,18 +3894,13 @@ fn uninitialized_property_marker_offsets(class_info: &ClassInfo) -> Vec { .iter() .enumerate() .filter_map(|(index, (property, _))| { - let starts_uninitialized = class_info.declared_properties.contains(property) + let is_owned_reference = class_info.owned_reference_properties.contains(property) + && class_info.property_slot_is_reference(index, property); + let starts_uninitialized = class_info.property_slot_is_declared(index, property) && class_info.defaults.get(index).is_some_and(|default| default.is_none()) - && !class_info.owned_reference_properties.contains(property); + && !is_owned_reference; if starts_uninitialized { - Some( - class_info - .property_offsets - .get(property) - .copied() - .unwrap_or(8 + index * 16) - + 8, - ) + Some(8 + index * 16 + 8) } else { None } @@ -3925,14 +3916,10 @@ fn owned_reference_property_offsets(class_info: &ClassInfo) -> Vec { .iter() .enumerate() .filter_map(|(index, (property, _))| { - if class_info.owned_reference_properties.contains(property) { - Some( - class_info - .property_offsets - .get(property) - .copied() - .unwrap_or(8 + index * 16), - ) + if class_info.owned_reference_properties.contains(property) + && class_info.property_slot_is_reference(index, property) + { + Some(8 + index * 16) } else { None } @@ -4024,12 +4011,7 @@ fn resolve_property_slot_for_class( .class_infos .get(normalized) .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", normalized)))?; - let is_reference = class_info.reference_properties.contains(property); - let Some((index, (_, php_type))) = class_info - .properties - .iter() - .enumerate() - .find(|(_, (name, _))| name == property) + let Some((index, (_, php_type))) = class_info.visible_property(property) else { return Err(CodegenIrError::unsupported(format!( "{} for dynamic or missing property {}::${}", @@ -4038,20 +4020,17 @@ fn resolve_property_slot_for_class( property ))); }; + let is_reference = class_info.property_slot_is_reference(index, property); let php_type = runtime_property_type_override(ctx, normalized, property) .unwrap_or_else(|| php_type.clone()); ensure_property_type_supported(&php_type, inst)?; - let offset = class_info - .property_offsets - .get(property) - .copied() - .unwrap_or(8 + index * 16); + let offset = 8 + index * 16; Ok(PropertySlot { class_name: normalized.to_string(), property: property.to_string(), php_type, offset, - is_declared: class_info.declared_properties.contains(property), + is_declared: class_info.property_slot_is_declared(index, property), is_packed: false, is_reference, }) diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 2e403cd2f5..332e0ae865 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -659,6 +659,11 @@ pub(crate) fn emit_runtime_data_user( .properties .iter() .enumerate() + .filter(|(prop_index, (name, _))| { + class_info + .visible_property_index(name) + .is_some_and(|visible_index| visible_index == *prop_index) + }) .filter(|(_, (name, _))| { class_info .property_visibilities @@ -698,7 +703,7 @@ pub(crate) fn emit_runtime_data_user( } out.push_str(&format!(" .quad {}\n", public_props.len())); for (prop_index, (prop_name, prop_ty)) in &public_props { - let tag = if class_info.reference_properties.contains(prop_name) { + let tag = if class_info.property_slot_is_reference(*prop_index, prop_name) { 0 } else { match prop_ty { @@ -742,7 +747,7 @@ pub(crate) fn emit_runtime_data_user( out.push_str(", "); } let prop_name = &class_info.properties[i].0; - let tag = if class_info.reference_properties.contains(prop_name) { + let tag = if class_info.property_slot_is_reference(i, prop_name) { 0 } else { match prop_ty { @@ -1405,10 +1410,12 @@ mod tests { property_visibilities: HashMap::new(), property_set_visibilities: HashMap::new(), declared_properties: HashSet::new(), + property_declared_slots: Vec::new(), final_properties: HashSet::new(), readonly_properties: HashSet::new(), reference_properties: HashSet::new(), owned_reference_properties: HashSet::new(), + property_reference_slots: Vec::new(), abstract_properties: HashSet::new(), abstract_property_hooks: HashMap::new(), static_properties: Vec::new(), diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index bf981ff0f2..25fb127259 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9059,7 +9059,8 @@ fn property_get_result_type( property_ty }; } - let Some((_, property_ty)) = class_info.properties.iter().find(|(name, _)| name == property) else { + let Some((_, (_, property_ty))) = class_info.visible_property(property) + else { if let Some(magic_ty) = magic_get_result_type(ctx, normalized) { return if nullable { nullable_result_type(magic_ty) diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index a5cdb497b7..a9b98d2f04 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -3202,10 +3202,8 @@ fn object_property_type( }; ctx.classes .get(class_name.trim_start_matches('\\'))? - .properties - .iter() - .find(|(name, _)| name == property) - .map(|(_, property_ty)| normalize_value_php_type(property_ty.codegen_repr())) + .visible_property(property) + .map(|(_, (_, property_ty))| normalize_value_php_type(property_ty.codegen_repr())) } /// Returns true when a property type uses concrete indexed-array storage. diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 6adf061c7e..fa1bb7fd4c 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -177,10 +177,12 @@ fn class_info(_class_name: &str) -> ClassInfo { property_visibilities: HashMap::new(), property_set_visibilities: HashMap::new(), declared_properties: Default::default(), + property_declared_slots: Vec::new(), final_properties: Default::default(), readonly_properties: Default::default(), reference_properties: Default::default(), owned_reference_properties: Default::default(), + property_reference_slots: Vec::new(), abstract_properties: Default::default(), abstract_property_hooks: HashMap::new(), static_properties: Vec::new(), diff --git a/src/types/checker/inference/objects/access.rs b/src/types/checker/inference/objects/access.rs index ad88535ea6..4c5996a9e1 100644 --- a/src/types/checker/inference/objects/access.rs +++ b/src/types/checker/inference/objects/access.rs @@ -258,7 +258,7 @@ impl Checker { { return Ok(ty); } - if let Some((_, ty)) = class_info.properties.iter().find(|(n, _)| n == property) { + if let Some((_, (_, ty))) = class_info.visible_property(property) { return Ok(ty.clone()); } if let Some(sig) = class_info.methods.get("__get") { diff --git a/src/types/checker/method_pass.rs b/src/types/checker/method_pass.rs index c677819b95..d4076e45e2 100644 --- a/src/types/checker/method_pass.rs +++ b/src/types/checker/method_pass.rs @@ -190,10 +190,10 @@ impl Checker { continue; } if let Some(Some(prop_name)) = ci.constructor_param_to_prop.get(i) { - if ci.declared_properties.contains(prop_name) { + if ci.visible_property_is_declared(prop_name) { continue; } - if let Some((_, ty)) = ci.properties.iter().find(|(n, _)| n == prop_name) { + if let Some((_, (_, ty))) = ci.visible_property(prop_name) { method_env.insert(pname.clone(), ty.clone()); if let Some(ci_mut) = self.classes.get_mut(&class.name) { if let Some(sig) = ci_mut.methods.get_mut("__construct") { diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index d5d65df35c..4f92417446 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -314,6 +314,11 @@ fn apply_reference_property_promotions(checker: &mut Checker) { } info.reference_properties.insert(prop.clone()); info.owned_reference_properties.insert(prop.clone()); + if let Some(slot) = info.visible_property_index(&prop) { + if let Some(slot_reference) = info.property_reference_slots.get_mut(slot) { + *slot_reference = true; + } + } } } } diff --git a/src/types/checker/schema/classes/properties.rs b/src/types/checker/schema/classes/properties.rs index b9019beeba..54a3122aaf 100644 --- a/src/types/checker/schema/classes/properties.rs +++ b/src/types/checker/schema/classes/properties.rs @@ -247,6 +247,7 @@ fn apply_instance_property( ); } + let mut is_declared_slot = false; let ty = if let Some(declared_ty) = resolve_property_declared_type(checker, &class.name, prop)? { checker.validate_schema_declared_default_type( &declared_ty, @@ -255,6 +256,7 @@ fn apply_instance_property( &format!("Property {}::${} default", class.name, prop.name), )?; state.declared_properties.insert(prop.name.clone()); + is_declared_slot = true; refine_declared_array_type_from_default(declared_ty, prop.default.as_ref()) } else if let Some(default) = &prop.default { infer_untyped_property_default_type(default) @@ -270,6 +272,8 @@ fn apply_instance_property( state .property_declaring_classes .insert(prop.name.clone(), class.name.clone()); + state.property_declared_slots.push(is_declared_slot); + state.property_reference_slots.push(prop.by_ref); state .property_attribute_names .insert(prop.name.clone(), collect_attribute_names(&prop.attributes)); @@ -321,6 +325,14 @@ fn apply_instance_property_redeclaration( prop: &ClassProperty, parent_declaring_class: &str, ) -> Result<(), CompileError> { + let inherited_visibility = state + .property_visibilities + .get(&prop.name) + .cloned() + .unwrap_or(Visibility::Public); + if inherited_visibility == Visibility::Private { + return apply_private_parent_property_shadowing(state, class, checker, prop); + } if state.final_properties.contains(&prop.name) { return Err(CompileError::new( prop.span, @@ -331,15 +343,16 @@ fn apply_instance_property_redeclaration( )); } let declared_ty = resolve_property_declared_type(checker, &class.name, prop)?; - validate_instance_property_override( - state, - class, - checker, - prop, - declared_ty.as_ref(), - parent_declaring_class, + validate_instance_property_override( + state, + class, + checker, + prop, + declared_ty.as_ref(), + parent_declaring_class, )?; + let is_declared_slot = declared_ty.is_some(); let ty = if let Some(declared_ty) = declared_ty { checker.validate_schema_declared_default_type( &declared_ty, @@ -358,6 +371,12 @@ fn apply_instance_property_redeclaration( let slot = find_instance_property_slot(state, &prop.name); state.prop_types[slot] = (prop.name.clone(), ty); state.defaults[slot] = prop.default.clone(); + if let Some(slot_declared) = state.property_declared_slots.get_mut(slot) { + *slot_declared = is_declared_slot; + } + if let Some(slot_reference) = state.property_reference_slots.get_mut(slot) { + *slot_reference = prop.by_ref; + } state .property_declaring_classes .insert(prop.name.clone(), class.name.clone()); @@ -398,6 +417,94 @@ fn apply_instance_property_redeclaration( Ok(()) } +/// Records a child property with the same name as a private parent property as a +/// fresh physical slot, matching PHP's private-property shadowing semantics. +fn apply_private_parent_property_shadowing( + state: &mut ClassBuildState, + class: &FlattenedClass, + checker: &Checker, + prop: &ClassProperty, +) -> Result<(), CompileError> { + let declared_ty = resolve_property_declared_type(checker, &class.name, prop)?; + let is_declared_slot = declared_ty.is_some(); + let ty = if let Some(declared_ty) = declared_ty { + checker.validate_declared_default_type( + &declared_ty, + prop.default.as_ref(), + prop.span, + &format!("Property {}::${} default", class.name, prop.name), + )?; + state.declared_properties.insert(prop.name.clone()); + refine_declared_array_type_from_default(declared_ty, prop.default.as_ref()) + } else if let Some(default) = &prop.default { + state.declared_properties.remove(&prop.name); + infer_expr_type_syntactic(default) + } else { + state.declared_properties.remove(&prop.name); + PhpType::Int + }; + + let slot_index = state.prop_types.len(); + state.prop_types.push((prop.name.clone(), ty)); + state + .property_offsets + .insert(prop.name.clone(), 8 + slot_index * 16); + state.defaults.push(prop.default.clone()); + state.property_declared_slots.push(is_declared_slot); + state.property_reference_slots.push(prop.by_ref); + state + .property_declaring_classes + .insert(prop.name.clone(), class.name.clone()); + state + .property_attribute_names + .insert(prop.name.clone(), collect_attribute_names(&prop.attributes)); + state + .property_attribute_args + .insert(prop.name.clone(), collect_attribute_args(&prop.attributes)); + state + .property_visibilities + .insert(prop.name.clone(), prop.visibility.clone()); + apply_set_visibility(state, prop); + replace_active_property_flags(state, class, checker, prop)?; + Ok(()) +} + +/// Replaces the name-keyed flags for the property currently visible from this +/// class after private-parent shadowing creates a fresh child slot. +fn replace_active_property_flags( + state: &mut ClassBuildState, + class: &FlattenedClass, + checker: &Checker, + prop: &ClassProperty, +) -> Result<(), CompileError> { + if prop.is_final { + state.final_properties.insert(prop.name.clone()); + } else { + state.final_properties.remove(&prop.name); + } + if class.is_readonly_class || prop.readonly { + state.readonly_properties.insert(prop.name.clone()); + } else { + state.readonly_properties.remove(&prop.name); + } + if prop.by_ref { + state.reference_properties.insert(prop.name.clone()); + } else { + state.reference_properties.remove(&prop.name); + } + if prop.is_abstract { + state.abstract_properties.insert(prop.name.clone()); + let contract = build_property_contract(checker, &class.name, prop)?; + state + .abstract_property_hooks + .insert(prop.name.clone(), contract); + } else { + state.abstract_properties.remove(&prop.name); + state.abstract_property_hooks.remove(&prop.name); + } + Ok(()) +} + /// Validates an instance property override against PHP inheritance rules: /// visibility reduction, final override attempts, readonly removal, by-reference /// toggling, and making a concrete property abstract. For abstract parent @@ -417,17 +524,6 @@ fn validate_instance_property_override( .get(&prop.name) .cloned() .unwrap_or(Visibility::Public); - if inherited_visibility == Visibility::Private { - // PHP allows shadowing a private parent property with a fresh slot in the child, - // but our property layout uses one slot per name. Reject until proper scoping is added. - return Err(CompileError::new( - prop.span, - &format!( - "Cannot redeclare property {}::${}: parent class {} has a private property with the same name (shadowing private parent properties is not yet supported)", - class.name, prop.name, parent_declaring_class - ), - )); - } if visibility_rank(&prop.visibility) < visibility_rank(&inherited_visibility) { return Err(CompileError::new( prop.span, @@ -579,10 +675,10 @@ fn inherited_static_property_type(state: &ClassBuildState, property: &str) -> Ph /// parent's resolved types in `state.prop_types`. Returns `PhpType::Int` /// if the property is not found, matching the undeclared-property default. fn inherited_instance_property_type(state: &ClassBuildState, property: &str) -> PhpType { + let slot = find_instance_property_slot(state, property); state .prop_types - .iter() - .find(|(name, _)| name == property) + .get(slot) .map(|(_, ty)| ty.clone()) .unwrap_or(PhpType::Int) } @@ -591,10 +687,20 @@ fn inherited_instance_property_type(state: &ClassBuildState, property: &str) -> /// Panics if the property is not found; the caller is responsible for ensuring /// the property exists via prior checks on `state.property_declaring_classes`. fn find_instance_property_slot(state: &ClassBuildState, name: &str) -> usize { + if let Some(index) = state + .property_offsets + .get(name) + .and_then(|offset| offset.checked_sub(8)) + .filter(|payload_offset| payload_offset % 16 == 0) + .map(|payload_offset| payload_offset / 16) + .filter(|index| *index < state.prop_types.len()) + { + return index; + } state .prop_types .iter() - .position(|(prop_name, _)| prop_name == name) + .rposition(|(prop_name, _)| prop_name == name) .expect("redeclaration path: property must exist in prop_types when declaring_classes has it") } diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index 4f77896cbc..4923d957f4 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -34,9 +34,11 @@ pub(super) struct ClassBuildState { pub(super) property_visibilities: HashMap, pub(super) property_set_visibilities: HashMap, pub(super) declared_properties: HashSet, + pub(super) property_declared_slots: Vec, pub(super) final_properties: HashSet, pub(super) readonly_properties: HashSet, pub(super) reference_properties: HashSet, + pub(super) property_reference_slots: Vec, pub(super) abstract_properties: HashSet, pub(super) abstract_property_hooks: HashMap, pub(super) static_prop_types: Vec<(String, PhpType)>, @@ -154,10 +156,12 @@ impl ClassBuildState { property_visibilities: self.property_visibilities, property_set_visibilities: self.property_set_visibilities, declared_properties: self.declared_properties, + property_declared_slots: self.property_declared_slots, final_properties: self.final_properties, readonly_properties: self.readonly_properties, reference_properties: self.reference_properties, owned_reference_properties: HashSet::new(), + property_reference_slots: self.property_reference_slots, abstract_properties: self.abstract_properties, abstract_property_hooks: self.abstract_property_hooks, static_properties: self.static_prop_types, @@ -360,6 +364,20 @@ impl ClassBuildState { self.prop_types.push((name.clone(), ty.clone())); self.property_offsets.insert(name.clone(), 8 + index * 16); self.defaults.push(parent.defaults[index].clone()); + self.property_declared_slots.push( + parent + .property_declared_slots + .get(index) + .copied() + .unwrap_or_else(|| parent.declared_properties.contains(name)), + ); + self.property_reference_slots.push( + parent + .property_reference_slots + .get(index) + .copied() + .unwrap_or_else(|| parent.reference_properties.contains(name)), + ); if let Some(visibility) = parent.property_visibilities.get(name) { self.property_visibilities .insert(name.clone(), visibility.clone()); diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 177617f664..a3d8bd8fea 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -265,9 +265,11 @@ pub(crate) fn insert_enum_metadata( let mut defaults = Vec::new(); let mut property_visibilities = HashMap::new(); let mut declared_properties = HashSet::new(); + let mut property_declared_slots = Vec::new(); let final_properties = HashSet::new(); let mut readonly_properties = HashSet::new(); let reference_properties = HashSet::new(); + let mut property_reference_slots = Vec::new(); if let Some(backing_ty) = &backing_type { push_enum_readonly_property( "value", @@ -279,7 +281,9 @@ pub(crate) fn insert_enum_metadata( &mut defaults, &mut property_visibilities, &mut declared_properties, + &mut property_declared_slots, &mut readonly_properties, + &mut property_reference_slots, ); } // Append `name` after any backing `value` so backed enums keep `value` at @@ -294,7 +298,9 @@ pub(crate) fn insert_enum_metadata( &mut defaults, &mut property_visibilities, &mut declared_properties, + &mut property_declared_slots, &mut readonly_properties, + &mut property_reference_slots, ); let mut static_methods = HashMap::new(); @@ -433,10 +439,12 @@ pub(crate) fn insert_enum_metadata( property_visibilities, property_set_visibilities: HashMap::new(), declared_properties, + property_declared_slots, final_properties, readonly_properties, reference_properties, owned_reference_properties: HashSet::new(), + property_reference_slots, abstract_properties: HashSet::new(), abstract_property_hooks: HashMap::new(), static_properties: Vec::new(), @@ -488,7 +496,9 @@ fn push_enum_readonly_property( defaults: &mut Vec>, property_visibilities: &mut HashMap, declared_properties: &mut HashSet, + property_declared_slots: &mut Vec, readonly_properties: &mut HashSet, + property_reference_slots: &mut Vec, ) { let offset = 8 + properties.len() * 16; let property = property.to_string(); @@ -498,5 +508,7 @@ fn push_enum_readonly_property( defaults.push(None); property_visibilities.insert(property.clone(), Visibility::Public); declared_properties.insert(property.clone()); + property_declared_slots.push(true); readonly_properties.insert(property); + property_reference_slots.push(false); } diff --git a/src/types/checker/stmt_check/assignments/properties.rs b/src/types/checker/stmt_check/assignments/properties.rs index 150638c964..eb252b6b9c 100644 --- a/src/types/checker/stmt_check/assignments/properties.rs +++ b/src/types/checker/stmt_check/assignments/properties.rs @@ -210,7 +210,7 @@ fn check_object_property_write( return Ok(()); } if let Some(class_info) = checker.classes.get(class_name) { - if !class_info.properties.iter().any(|(n, _)| n == property) { + if class_info.visible_property(property).is_none() { if class_info.methods.contains_key("__set") { return Ok(()); } @@ -227,10 +227,8 @@ fn check_object_property_write( } validate_object_property_access(checker, class_name, property, true, span)?; let expected_ty = class_info - .properties - .iter() - .find(|(n, _)| n == property) - .map(|(_, ty)| ty.clone()) + .visible_property(property) + .map(|(_, (_, ty))| ty.clone()) .unwrap_or(PhpType::Int); let readonly_non_null_coalesce_keep = null_coalesce_property_keeps_non_null(object, property, value, &expected_ty); @@ -284,7 +282,7 @@ fn check_object_property_write( ), )); } - if class_info.declared_properties.contains(property) { + if class_info.visible_property_is_declared(property) { checker.require_compatible_arg_type( &expected_ty, val_ty, @@ -361,12 +359,9 @@ fn refine_object_property_type( val_ty: &PhpType, ) { if let Some(class_info) = checker.classes.get_mut(class_name) { - let property_has_declared_type = class_info.declared_properties.contains(property); - if let Some(prop) = class_info - .properties - .iter_mut() - .find(|(n, _)| n == property) - { + let property_has_declared_type = class_info.visible_property_is_declared(property); + if let Some(slot) = class_info.visible_property_index(property) { + let prop = &mut class_info.properties[slot]; if !property_has_declared_type { if matches!(prop.1, PhpType::Int | PhpType::Void) && prop.1 != *val_ty { prop.1 = val_ty.clone(); @@ -451,7 +446,7 @@ fn resolve_object_array_property( .classes .get(class_name) .ok_or_else(|| CompileError::new(span, &format!("Undefined class: {}", class_name)))?; - if !class_info.properties.iter().any(|(n, _)| n == property) { + if class_info.visible_property(property).is_none() { return Err(CompileError::new( span, &format!("Undefined property: {}::{}", class_name, property), @@ -460,12 +455,10 @@ fn resolve_object_array_property( // Indirect array modification (`$obj->prop[] = x` / `$obj->prop[$k] = x`) is a write, so it // must honor PHP 8.4 asymmetric `set` visibility — not the read visibility. validate_object_property_access(checker, class_name, property, true, span)?; - let property_has_declared_type = class_info.declared_properties.contains(property); + let property_has_declared_type = class_info.visible_property_is_declared(property); let prop_ty = class_info - .properties - .iter() - .find(|(name, _)| name == property) - .map(|(_, ty)| ty.clone()) + .visible_property(property) + .map(|(_, (_, ty))| ty.clone()) .unwrap_or(PhpType::Int); Ok((prop_ty, property_has_declared_type)) } diff --git a/src/types/checker/type_compat/object_types.rs b/src/types/checker/type_compat/object_types.rs index 0226b6656e..696161a75e 100644 --- a/src/types/checker/type_compat/object_types.rs +++ b/src/types/checker/type_compat/object_types.rs @@ -374,14 +374,12 @@ impl Checker { continue; } - let property_has_declared_type = class_info.declared_properties.contains(&prop_name); + let property_has_declared_type = class_info.visible_property_is_declared(&prop_name); if !property_has_declared_type { - if let Some(prop) = class_info - .properties - .iter_mut() - .find(|(name, _)| name == &prop_name) - { - prop.1 = arg_ty.clone(); + if let Some(slot) = class_info.visible_property_index(&prop_name) { + if let Some(prop) = class_info.properties.get_mut(slot) { + prop.1 = arg_ty.clone(); + } } } diff --git a/src/types/schema.rs b/src/types/schema.rs index 5d9a025d31..ec4e52d9ed 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -165,6 +165,13 @@ pub struct ClassInfo { /// use their `property_visibilities` entry for writes too. pub property_set_visibilities: HashMap, pub declared_properties: HashSet, + /// Per-layout-slot typed-declaration flags for instance properties. + /// + /// The name-keyed `declared_properties` map describes the property currently + /// visible by name in this class. This vector follows `properties` by index + /// so hidden private parent slots keep their typed-property initialization + /// metadata when a child declares a same-named property. + pub property_declared_slots: Vec, pub final_properties: HashSet, pub readonly_properties: HashSet, pub reference_properties: HashSet, @@ -175,6 +182,12 @@ pub struct ClassInfo { /// caller). The object allocates a cell per such property at construction and releases /// it on destruction. pub owned_reference_properties: HashSet, + /// Per-layout-slot by-reference flags for instance properties. + /// + /// The name-keyed `reference_properties` map describes the currently + /// visible property by name. Runtime GC descriptors need the original slot + /// flag even when a private parent slot is shadowed by a child property. + pub property_reference_slots: Vec, pub abstract_properties: HashSet, pub abstract_property_hooks: HashMap, pub static_properties: Vec<(String, PhpType)>, @@ -209,6 +222,69 @@ pub struct ClassInfo { pub constructor_param_to_prop: Vec>, } +impl ClassInfo { + /// Resolves the layout index of the property visible by name on this class. + /// + /// The result follows `property_offsets` when present so private parent + /// slots shadowed by child declarations do not win merely because they occur + /// earlier in the physical object layout. + pub fn visible_property_index(&self, property: &str) -> Option { + self.property_offsets + .get(property) + .and_then(|offset| property_index_from_offset(*offset, self.properties.len())) + .or_else(|| { + self.properties + .iter() + .rposition(|(name, _)| name == property) + }) + } + + /// Returns the property tuple visible by name on this class. + pub fn visible_property(&self, property: &str) -> Option<(usize, &(String, PhpType))> { + let index = self.visible_property_index(property)?; + self.properties.get(index).map(|entry| (index, entry)) + } + + /// Returns whether one physical property slot has a declared PHP type. + pub fn property_slot_is_declared(&self, index: usize, property: &str) -> bool { + self.property_declared_slots + .get(index) + .copied() + .unwrap_or_else(|| self.declared_properties.contains(property)) + } + + /// Returns whether the property visible by name has a declared PHP type. + pub fn visible_property_is_declared(&self, property: &str) -> bool { + self.visible_property(property) + .is_some_and(|(index, (name, _))| self.property_slot_is_declared(index, name)) + } + + /// Returns whether one physical property slot stores a by-reference cell. + pub fn property_slot_is_reference(&self, index: usize, property: &str) -> bool { + self.property_reference_slots + .get(index) + .copied() + .unwrap_or_else(|| self.reference_properties.contains(property)) + } + + /// Returns whether the property visible by name stores a by-reference cell. + pub fn visible_property_is_reference(&self, property: &str) -> bool { + self.visible_property(property) + .is_some_and(|(index, (name, _))| self.property_slot_is_reference(index, name)) + } +} + +/// Converts a property offset into a `properties` vector index when it points +/// at a normal object-property slot. +fn property_index_from_offset(offset: usize, property_count: usize) -> Option { + let payload_offset = offset.checked_sub(8)?; + if payload_offset % 16 != 0 { + return None; + } + let index = payload_offset / 16; + (index < property_count).then_some(index) +} + /// Enum case value, either an integer or a string (PHP 8.1+ backed enums). #[derive(Debug, Clone, PartialEq)] pub enum EnumCaseValue { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0ab32e0aca..9b05eed2c5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5032,6 +5032,36 @@ echo $box->readProtected(2);'); assert_eq!(out.stdout, "7:7"); } +/// Verifies eval-declared private parent properties keep separate storage when a child shadows them. +#[test] +fn test_eval_declared_private_parent_property_shadowing() { + let out = compile_and_run_capture( + r#"value; } +} +class EvalShadowParent extends EvalShadowGrand { + public $value = 2; + public function parentValue() { return $this->value; } +} +class EvalShadowChild extends EvalShadowParent { + public $value = 3; +} +$box = new EvalShadowChild(); +echo $box->grandValue() . ":"; +echo $box->parentValue() . ":"; +echo $box->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1:3:3"); +} + /// Verifies eval-declared readonly properties can be initialized only in constructors. #[test] fn test_eval_declared_readonly_property_rules() { diff --git a/tests/codegen/oop/inheritance.rs b/tests/codegen/oop/inheritance.rs index 15bb307508..a0d82c776e 100644 --- a/tests/codegen/oop/inheritance.rs +++ b/tests/codegen/oop/inheritance.rs @@ -635,3 +635,78 @@ echo $c->value; ); assert_eq!(out, "42:42"); } + +/// Verifies a child property can shadow a private parent property with a fresh +/// slot: parent methods keep reading the private slot while child/global reads +/// see the child property. +#[test] +fn test_private_parent_property_shadowing_uses_separate_slots() { + let out = compile_and_run( + r#"value = 2; + } + + public function parentValue() { + return $this->value; + } +} + +class Child extends Base { + public $value = "child"; + + public function childValue() { + return $this->value; + } +} + +$c = new Child(); +echo $c->parentValue(); +echo ":"; +echo $c->childValue(); +echo ":"; +echo $c->value; +"#, + ); + assert_eq!(out, "2:child:child"); +} + +/// Verifies a later non-private redeclaration updates the visible parent slot, +/// while an older private grandparent slot stays separate for grandparent methods. +#[test] +fn test_private_grandparent_property_shadowing_survives_later_redeclaration() { + let out = compile_and_run( + r#"value; + } +} + +class ParentBox extends GrandParentBox { + public int $value = 2; + + public function parentValue() { + return $this->value; + } +} + +class ChildBox extends ParentBox { + public int $value = 3; +} + +$c = new ChildBox(); +echo $c->grandParentValue(); +echo ":"; +echo $c->parentValue(); +echo ":"; +echo $c->value; +"#, + ); + assert_eq!(out, "1:3:3"); +} diff --git a/tests/error_tests/misc/classes.rs b/tests/error_tests/misc/classes.rs index b47aa9af73..0c4a92468a 100644 --- a/tests/error_tests/misc/classes.rs +++ b/tests/error_tests/misc/classes.rs @@ -712,16 +712,6 @@ fn test_error_property_redeclaration_adds_type_to_untyped_parent() { ); } -/// Verifies the error diagnostic for property redeclaration shadows private parent property. -#[test] -fn test_error_property_redeclaration_shadows_private_parent_property() { - // private parent properties cannot be shadowed by a child; the feature is not yet supported. - expect_error( - " Date: Fri, 19 Jun 2026 05:16:20 +0200 Subject: [PATCH 0387/1208] Support ReflectionClass interface predicate --- .../tests/builtins_class_metadata.rs | 28 +++++ .../interpreter/tests/support/object_ops.rs | 28 +++++ docs/php/classes.md | 3 +- docs/php/eval.md | 14 ++- src/codegen/lower_inst/objects/reflection.rs | 10 +- src/types/checker/builtin_types/reflection.rs | 115 +++++++++++++++++- tests/codegen/eval.rs | 27 ++++ tests/codegen/oop/attributes.rs | 27 ++++ 8 files changed, 237 insertions(+), 15 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index a1ad63a081..2b92089954 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -258,6 +258,34 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::implementsInterface reports eval class, enum, and +/// interface metadata using case-insensitive interface names. +#[test] +fn execute_program_reflects_eval_class_implements_interface_predicate() { + let program = parse_fragment( + br#"interface EvalImplBase {} +interface EvalImplChild extends EvalImplBase {} +class EvalImplTarget implements EvalImplChild {} +enum EvalImplEnum implements EvalImplBase { case Ready; } +trait EvalImplTrait {} +echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("EvalImplChild") ? "C" : "c"; +echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("evalimplbase") ? "B" : "b"; +echo (new ReflectionClass("EvalImplEnum"))->implementsInterface("EvalImplBase") ? "E" : "e"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplChild") ? "I" : "i"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplBase") ? "P" : "p"; +echo (new ReflectionClass("EvalImplTrait"))->implementsInterface("EvalImplBase") ? "T" : "t"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "CBEIPt"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class-like final and abstract flags. #[test] fn execute_program_reflects_eval_class_modifier_flags() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 62f3330d19..582f1f9e89 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -182,6 +182,34 @@ impl FakeOps { (FakeValue::Object(properties), "hasproperty") if args.len() == 1 => { self.object_string_array_contains(&properties, "__property_names", args[0], false) } + (FakeValue::Object(properties), "implementsinterface") if args.len() == 1 => { + let direct = self.object_string_array_contains( + &properties, + "__interface_names", + args[0], + true, + )?; + if matches!(self.get(direct), FakeValue::Bool(true)) { + return Ok(direct); + } + let Some(is_interface) = Self::object_property(&properties, "__is_interface") + else { + return Ok(direct); + }; + if !matches!(self.get(is_interface), FakeValue::Bool(true)) { + return Ok(direct); + } + let Some(reflected_name) = Self::object_property(&properties, "__name") else { + return Ok(direct); + }; + let FakeValue::String(reflected_name) = self.get(reflected_name) else { + return Ok(direct); + }; + let FakeValue::String(interface_name) = self.get(args[0]) else { + return Ok(direct); + }; + self.bool_value(reflected_name.eq_ignore_ascii_case(&interface_name)) + } (FakeValue::Object(properties), "getinterfacenames") if args.is_empty() => { Self::object_property(&properties, "__interface_names") .map_or_else(|| self.runtime_array_new(0), Ok) diff --git a/docs/php/classes.md b/docs/php/classes.md index 91590952b6..511f6f90f1 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1112,6 +1112,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | | `ReflectionClass::hasMethod()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named method; method lookup is case-insensitive | | `ReflectionClass::hasProperty()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named property; property lookup is case-sensitive | +| `ReflectionClass::implementsInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol implements, extends, or is the requested interface; interface lookup is case-insensitive | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | @@ -1201,7 +1202,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` currently returns the metadata predicate for known interface names but does not yet throw PHP's `ReflectionException` when the argument is missing or names a class. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 7d4fe440ae..3d610c3a4f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -168,8 +168,11 @@ metadata describes a concrete class with no constructor or a public constructor. `ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::IS_*` modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval -classes and parent interfaces for eval interfaces, while -`ReflectionClass::getTraitNames()` returns traits used directly by eval classes. +classes and parent interfaces for eval interfaces. +`ReflectionClass::implementsInterface()` checks those relations +case-insensitively and returns true when reflecting the requested interface +itself, while `ReflectionClass::getTraitNames()` returns traits used directly by +eval classes. `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. @@ -373,9 +376,10 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter type metadata beyond presence flags, and broader -generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed slice. +ReflectionParameter type metadata beyond presence flags, +`ReflectionClass::implementsInterface()` `ReflectionException` validation for +missing or non-interface argument names, and broader generated/AOT method bridge +signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index d1e9cca04a..8268272445 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -1766,9 +1766,6 @@ fn emit_reflection_string_array_property_by_name( property_name: &str, names: &[String], ) -> Result<()> { - if names.is_empty() { - return Ok(()); - } let class_info = ctx .module .class_infos @@ -1804,9 +1801,6 @@ fn emit_reflection_member_array_property_by_name( member_class_name: &str, members: &[ReflectionListedMember], ) -> Result<()> { - if members.is_empty() { - return Ok(()); - } let class_info = ctx .module .class_infos @@ -2136,11 +2130,11 @@ fn emit_append_reflection_member_object(ctx: &mut FunctionContext<'_>) { fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) -> Result<()> { match ctx.emitter.target.arch { Arch::AArch64 => { - abi::emit_load_int_immediate(ctx.emitter, "x0", names.len() as i64); + abi::emit_load_int_immediate(ctx.emitter, "x0", names.len().max(1) as i64); abi::emit_load_int_immediate(ctx.emitter, "x1", 16); } Arch::X86_64 => { - abi::emit_load_int_immediate(ctx.emitter, "rdi", names.len() as i64); + abi::emit_load_int_immediate(ctx.emitter, "rdi", names.len().max(1) as i64); abi::emit_load_int_immediate(ctx.emitter, "rsi", 16); } } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 657b9df2ef..6dc148cc41 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -16,7 +16,8 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::names::Name; use crate::parser::ast::{ - ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, Visibility, + BinOp, ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, + Visibility, }; use crate::types::traits::FlattenedClass; use crate::types::PhpType; @@ -202,6 +203,14 @@ fn false_bool() -> Option { )) } +/// Returns a `BoolLiteral(true)` expression. +fn true_bool() -> Option { + Some(Expr::new( + ExprKind::BoolLiteral(true), + crate::span::Span::dummy(), + )) +} + /// Returns an `IntLiteral` expression with the given value. fn int_lit(value: i64) -> Option { Some(Expr::new( @@ -773,6 +782,7 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), + builtin_reflection_class_implements_interface_method(), builtin_reflection_class_array_method( "getMethods", "__methods", @@ -929,6 +939,108 @@ fn builtin_reflection_class_has_name_method( } } +/// Returns `ReflectionClass::implementsInterface()` backed by interface-name metadata. +fn builtin_reflection_class_implements_interface_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let interface_var = Expr::new(ExprKind::Variable("interface".to_string()), dummy_span); + let candidate_var = Expr::new(ExprKind::Variable("interfaceName".to_string()), dummy_span); + let lowered_interface = strtolower_call(interface_var.clone(), dummy_span); + let lowered_candidate = strtolower_call(candidate_var, dummy_span); + let candidate_matches = Expr::new( + ExprKind::BinaryOp { + left: Box::new(lowered_candidate), + op: BinOp::Eq, + right: Box::new(lowered_interface.clone()), + }, + dummy_span, + ); + let reflected_name_matches = Expr::new( + ExprKind::BinaryOp { + left: Box::new(strtolower_call( + reflection_this_property("__name", dummy_span), + dummy_span, + )), + op: BinOp::Eq, + right: Box::new(lowered_interface), + }, + dummy_span, + ); + let interface_self_matches = Expr::new( + ExprKind::BinaryOp { + left: Box::new(reflection_this_property("__is_interface", dummy_span)), + op: BinOp::And, + right: Box::new(reflected_name_matches), + }, + dummy_span, + ); + ClassMethod { + name: "implementsInterface".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("interface".to_string(), Some(TypeExpr::Str), None, false)], + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![ + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__interface_names", dummy_span), + key_var: None, + value_var: "interfaceName".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: candidate_matches, + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: interface_self_matches, + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new(StmtKind::Return(false_bool()), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `$this->{$property}` for synthetic ReflectionClass method bodies. +fn reflection_this_property(property: &str, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, span)), + property: property.to_string(), + }, + span, + ) +} + +/// Builds a `strtolower()` call around an expression for case-insensitive class names. +fn strtolower_call(expr: Expr, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("strtolower"), + args: vec![expr], + }, + span, + ) +} + /// Returns a public `ReflectionClass` array method backed by one private slot. fn builtin_reflection_class_array_method( method_name: &str, @@ -1315,6 +1427,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isinstantiable", "hasmethod", "hasproperty", + "implementsinterface", ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9b05eed2c5..af618404c6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5769,6 +5769,33 @@ echo count($parentInterfaces) . ":" . $parentInterfaces[0];'); ); } +/// Verifies eval ReflectionClass::implementsInterface reports class, enum, and +/// interface metadata through the bridge. +#[test] +fn test_eval_reflection_class_implements_interface_predicate() { + let out = compile_and_run_capture( + r#"implementsInterface("EvalImplChild") ? "C" : "c"; +echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("evalimplbase") ? "B" : "b"; +echo (new ReflectionClass("EvalImplEnum"))->implementsInterface("EvalImplBase") ? "E" : "e"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplChild") ? "I" : "i"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplBase") ? "P" : "p"; +echo (new ReflectionClass("EvalImplTrait"))->implementsInterface("EvalImplBase") ? "T" : "t";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "CBEIPt"); +} + /// Verifies eval ReflectionClass::getParentClass crosses the generated runtime bridge. #[test] fn test_eval_reflection_class_get_parent_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 1b2b794556..008ca2227a 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1329,6 +1329,33 @@ echo count($parentInterfaces) . ":" . $parentInterfaces[0]; ); } +/// Verifies that `ReflectionClass::implementsInterface()` reports class, enum, +/// and interface metadata using case-insensitive interface names. +#[test] +fn test_reflection_class_implements_interface() { + let out = compile_and_run_capture( + r#"implementsInterface(StaticImplChild::class) ? "C" : "c"; +echo (new ReflectionClass(StaticImplTarget::class))->implementsInterface("staticimplbase") ? "B" : "b"; +echo (new ReflectionClass(StaticImplEnum::class))->implementsInterface(StaticImplBase::class) ? "E" : "e"; +echo (new ReflectionClass(StaticImplChild::class))->implementsInterface(StaticImplChild::class) ? "I" : "i"; +echo (new ReflectionClass(StaticImplChild::class))->implementsInterface(StaticImplBase::class) ? "P" : "p"; +echo (new ReflectionClass(StaticImplTrait::class))->implementsInterface(StaticImplBase::class) ? "T" : "t"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "CBEIPt"); +} + /// Verifies that `ReflectionClass::getParentClass()` returns a ReflectionClass /// object for subclasses and `false` for parentless classes. #[test] From d6336520cb0c137ff67a56e1ba2f464af1d1610f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 05:56:01 +0200 Subject: [PATCH 0388/1208] Support ReflectionException interface validation --- .../elephc-eval/src/interpreter/reflection.rs | 110 ++++ .../elephc-eval/src/interpreter/statements.rs | 42 +- .../tests/builtins_class_metadata.rs | 55 +- .../interpreter/tests/support/object_ops.rs | 13 +- docs/php/classes.md | 4 +- docs/php/eval.md | 12 +- src/codegen/eval_constructor_helpers.rs | 16 +- src/codegen/eval_method_helpers.rs | 15 +- src/codegen/lower_inst/builtins.rs | 151 +++++- src/codegen/lower_inst/objects.rs | 492 ++++++++++++------ src/codegen/mod.rs | 1 + .../checker/builtin_types/declarations.rs | 31 +- src/types/checker/builtin_types/exception.rs | 34 +- src/types/checker/builtin_types/reflection.rs | 155 +++++- tests/codegen/eval.rs | 56 +- tests/codegen/exceptions.rs | 19 +- tests/codegen/oop/attributes.rs | 51 ++ 17 files changed, 969 insertions(+), 288 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index f18ad119ee..d733088736 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -101,6 +101,48 @@ pub(in crate::interpreter) fn eval_reflection_owner_new_object( } } +/// Handles eval-backed `ReflectionClass::implementsInterface()` calls. +pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("implementsInterface") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("interface")], evaluated_args)?; + let interface_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_interface_exists(&interface_name, context, values)? { + if eval_reflection_non_interface_exists(&interface_name, context, values)? { + return eval_throw_reflection_exception( + &format!("{} is not an interface", interface_name), + context, + values, + ); + } + return eval_throw_reflection_exception( + &format!("Interface \"{}\" does not exist", interface_name), + context, + values, + ); + } + values + .bool_value(eval_reflection_class_implements_interface_name( + &reflected_name, + &interface_name, + context, + )) + .map(Some) +} + /// Builds an eval-backed `ReflectionClass` object when the reflected class-like exists in eval. fn eval_reflection_class_new( evaluated_args: Vec, @@ -674,6 +716,74 @@ fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> || context.has_enum(name) } +/// Returns true when one name exists as an eval or runtime interface. +fn eval_reflection_interface_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(context.has_interface(name) || values.interface_exists(name)?) +} + +/// Returns true when one name exists as a non-interface class-like symbol. +fn eval_reflection_non_interface_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(name) + || context.has_trait(name) + || context.has_enum(name) + || values.class_exists(name)? + || values.trait_exists(name)? + { + return Ok(true); + } + values.enum_exists(name) +} + +/// Returns true when reflected eval metadata implements or extends an interface name. +fn eval_reflection_class_implements_interface_name( + reflected_name: &str, + interface_name: &str, + context: &ElephcEvalContext, +) -> bool { + if context.has_interface(reflected_name) { + return eval_reflection_same_class_like_name(reflected_name, interface_name) + || context + .interface_parent_names(reflected_name) + .iter() + .any(|parent| eval_reflection_same_class_like_name(parent, interface_name)); + } + if context.has_class(reflected_name) || context.has_enum(reflected_name) { + return context + .class_interface_names(reflected_name) + .iter() + .any(|interface| eval_reflection_same_class_like_name(interface, interface_name)); + } + false +} + +/// Returns true when two PHP class-like names match case-insensitively. +fn eval_reflection_same_class_like_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Creates a catchable `ReflectionException` and propagates it through eval throw state. +fn eval_throw_reflection_exception( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ReflectionException")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + /// Returns method metadata for a method-like member on an eval class-like symbol. fn eval_reflection_method_metadata( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 0eb90fa1f9..4a642aa2eb 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1409,8 +1409,7 @@ fn method_signature_by_refs_accept( implementation_by_refs, implementation_variadics, position, - ) - == method_signature_effective_by_ref(required_by_refs, required_variadics, position) + ) == method_signature_effective_by_ref(required_by_refs, required_variadics, position) }) } @@ -1720,7 +1719,11 @@ fn eval_instance_property_storage_name( property: &EvalClassProperty, ) -> String { if property.visibility() == EvalVisibility::Private { - format!("\0{}\0{}", declaring_class.trim_start_matches('\\'), property.name()) + format!( + "\0{}\0{}", + declaring_class.trim_start_matches('\\'), + property.name() + ) } else { property.name().to_string() } @@ -2168,6 +2171,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( return eval_reflection_attribute_new_instance_result(&attribute, context, values); } } + if let Some(result) = eval_reflection_class_implements_interface_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(instance) = eval_reflection_class_new_instance_result( identity, method_name, @@ -2403,10 +2415,7 @@ fn alias_duplicate_method_ref_args( } /// Returns true when two evaluated arguments target the same caller-side variable. -fn same_method_ref_target( - left: &EvaluatedCallRefTarget, - right: &EvaluatedCallRefTarget, -) -> bool { +fn same_method_ref_target(left: &EvaluatedCallRefTarget, right: &EvaluatedCallRefTarget) -> bool { match (left, right) { ( EvaluatedCallRefTarget::Variable { @@ -2429,9 +2438,7 @@ fn same_method_ref_target( array_name: right_name, index: right_index, }, - ) => { - left_scope == right_scope && left_name == right_name && left_index == right_index - } + ) => left_scope == right_scope && left_name == right_name && left_index == right_index, _ => false, } } @@ -2521,12 +2528,7 @@ fn write_back_method_ref_target( return Err(EvalStatus::RuntimeFatal); }; write_back_method_array_element_ref_target( - scope, - array_name, - *index, - value, - context, - values, + scope, array_name, *index, value, context, values, ) } EvaluatedCallRefTarget::ObjectProperty { @@ -2567,13 +2569,7 @@ fn write_back_method_array_element_ref_target( eval_new_array_for_index(index, values)? }; let array = values.array_set(array, index, value)?; - for replaced in set_scope_cell( - context, - scope, - array_name.to_string(), - array, - ownership, - )? { + for replaced in set_scope_cell(context, scope, array_name.to_string(), array, ownership)? { values.release(replaced)?; } Ok(()) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 2b92089954..b2e477b81f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -286,6 +286,56 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::implementsInterface rejects non-interface names with catchable errors. +#[test] +fn execute_program_reflection_class_implements_interface_rejects_non_interfaces() { + let program = parse_fragment( + br#"interface EvalImplRejectIface {} +class EvalImplRejectTarget {} +class EvalImplRejectClass {} +trait EvalImplRejectTrait {} +enum EvalImplRejectEnum { case Ready; } +$ref = new ReflectionClass("EvalImplRejectTarget"); +echo $ref->implementsInterface("EvalImplRejectIface") ? "T" : "F"; +try { + $ref->implementsInterface("EvalImplRejectClass"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectTrait"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectEnum"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "F:ReflectionException:EvalImplRejectClass is not an interface:ReflectionException:EvalImplRejectTrait is not an interface:ReflectionException:EvalImplRejectEnum is not an interface:ReflectionException:Interface \"EvalImplRejectMissing\" does not exist" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class-like final and abstract flags. #[test] fn execute_program_reflects_eval_class_modifier_flags() { @@ -575,10 +625,7 @@ return true;"##, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "3/1:first#0rvRT|second#1OvbT|rest#2OVRt|" - ); + assert_eq!(values.output, "3/1:first#0rvRT|second#1OvbT|rest#2OVRt|"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 582f1f9e89..68305cb300 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -462,7 +462,8 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC) != 0)?; let is_passed_by_reference = self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_BY_REF) != 0)?; - let has_type = self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE) != 0)?; + let has_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE) != 0)?; properties.push(("__position".to_string(), position)); properties.push(("__is_optional".to_string(), is_optional)); properties.push(("__is_variadic".to_string(), is_variadic)); @@ -643,7 +644,13 @@ impl FakeOps { /// Returns whether a fake runtime class stores PHP Throwable constructor state. fn fake_runtime_exception_like_class(class_name: &str) -> bool { - ["Exception", "JsonException", "Error", "ValueError"] + [ + "Exception", + "JsonException", + "ReflectionException", + "Error", + "ValueError", + ] .iter() .any(|known| class_name.eq_ignore_ascii_case(known)) } @@ -673,7 +680,7 @@ fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: return fake_runtime_exception_like_class(class_name); } if target_class.eq_ignore_ascii_case("Exception") { - return ["Exception", "JsonException"] + return ["Exception", "JsonException", "ReflectionException"] .iter() .any(|known| class_name.eq_ignore_ascii_case(known)); } diff --git a/docs/php/classes.md b/docs/php/classes.md index 511f6f90f1..7e42dac7e6 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1112,7 +1112,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | | `ReflectionClass::hasMethod()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named method; method lookup is case-insensitive | | `ReflectionClass::hasProperty()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named property; property lookup is case-sensitive | -| `ReflectionClass::implementsInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol implements, extends, or is the requested interface; interface lookup is case-insensitive | +| `ReflectionClass::implementsInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol implements, extends, or is the requested interface; interface lookup is case-insensitive and invalid names throw `ReflectionException` | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | @@ -1202,7 +1202,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` currently returns the metadata predicate for known interface names but does not yet throw PHP's `ReflectionException` when the argument is missing or names a class. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 3d610c3a4f..36a509ddcb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -171,8 +171,9 @@ modifier bitmask for eval class-like metadata. classes and parent interfaces for eval interfaces. `ReflectionClass::implementsInterface()` checks those relations case-insensitively and returns true when reflecting the requested interface -itself, while `ReflectionClass::getTraitNames()` returns traits used directly by -eval classes. +itself. It throws catchable `ReflectionException` values when the argument names +a class, trait, enum, or missing interface, while `ReflectionClass::getTraitNames()` +returns traits used directly by eval classes. `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. @@ -376,10 +377,9 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter type metadata beyond presence flags, -`ReflectionClass::implementsInterface()` `ReflectionException` validation for -missing or non-interface argument names, and broader generated/AOT method bridge -signatures beyond the current public non-by-reference fixed scalar/Mixed slice. +ReflectionParameter type metadata beyond presence flags, and broader +generated/AOT method bridge signatures beyond the current public +non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index d92de6b7eb..68a3294192 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -40,6 +40,7 @@ const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ "RangeException", "UnderflowException", "UnexpectedValueException", + "ReflectionException", "JsonException", "FiberError", ]; @@ -99,12 +100,7 @@ fn collect_eval_constructor_slots(module: &Module) -> Vec { let mut classes = module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_constructor_slot( - class_name, - class_info, - &emitted_methods, - &mut slots, - ); + collect_class_constructor_slot(class_name, class_info, &emitted_methods, &mut slots); } slots } @@ -140,8 +136,8 @@ fn collect_class_constructor_slot( if !emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), false)) { return; } - let supported = constructor_is_public(class_info, &method_key) - && constructor_signature_supported(sig); + let supported = + constructor_is_public(class_info, &method_key) && constructor_signature_supported(sig); let params = if supported { sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect() } else { @@ -197,7 +193,9 @@ fn emit_constructor_helper( Arch::AArch64 => { emit_constructor_aarch64(module, emitter, slots, builtin_throwable_class_ids) } - Arch::X86_64 => emit_constructor_x86_64(module, emitter, slots, builtin_throwable_class_ids), + Arch::X86_64 => { + emit_constructor_x86_64(module, emitter, slots, builtin_throwable_class_ids) + } } } diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 2eb127eda8..0fd05cd06c 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -64,6 +64,7 @@ const BUILTIN_THROWABLE_METHOD_CLASSES: &[&str] = &[ "RangeException", "UnderflowException", "UnexpectedValueException", + "ReflectionException", "JsonException", "FiberError", ]; @@ -121,12 +122,7 @@ fn collect_eval_method_slots(module: &Module) -> Vec { let mut classes = module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_method_slots( - class_name, - class_info, - &emitted_methods, - &mut slots, - ); + collect_class_method_slots(class_name, class_info, &emitted_methods, &mut slots); } slots } @@ -138,12 +134,7 @@ fn collect_eval_static_method_slots(module: &Module) -> Vec>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_static_method_slots( - class_name, - class_info, - &emitted_methods, - &mut slots, - ); + collect_class_static_method_slots(class_name, class_info, &emitted_methods, &mut slots); } slots } diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 2ffa10c44f..fe8fee8950 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -348,7 +348,7 @@ pub(crate) fn lower_function_exists(ctx: &mut FunctionContext<'_>, inst: &Instru store_if_result(ctx, inst) } -/// Lowers AOT class/interface/enum existence checks for literal names. +/// Lowers AOT class/interface/enum existence checks for literal or dynamic string names. pub(crate) fn lower_class_like_exists( ctx: &mut FunctionContext<'_>, inst: &Instruction, @@ -356,24 +356,127 @@ pub(crate) fn lower_class_like_exists( ) -> Result<()> { ensure_arg_count_between(inst, name, 1, 2)?; let value = expect_operand(inst, 0)?; - let symbol_name = const_string_operand(ctx, value)?; - let exists = match name { - "class_exists" => contains_folded( - ctx.module - .class_infos - .keys() - .filter(|class_name| !is_internal_synthetic_class_name(class_name)), - &symbol_name, - ), - "interface_exists" => contains_folded(ctx.module.interface_infos.keys(), &symbol_name), - "trait_exists" => contains_folded(ctx.module.trait_table.names.iter(), &symbol_name), - "enum_exists" => contains_folded(ctx.module.enum_infos.keys(), &symbol_name), - _ => false, - }; - emit_static_bool(ctx, exists); + if let Some(symbol_name) = maybe_const_string_operand(ctx, value)? { + let exists = match name { + "class_exists" => contains_folded( + ctx.module + .class_infos + .keys() + .filter(|class_name| !is_internal_synthetic_class_name(class_name)), + &symbol_name, + ), + "interface_exists" => contains_folded(ctx.module.interface_infos.keys(), &symbol_name), + "trait_exists" => contains_folded(ctx.module.trait_table.names.iter(), &symbol_name), + "enum_exists" => contains_folded(ctx.module.enum_infos.keys(), &symbol_name), + _ => false, + }; + emit_static_bool(ctx, exists); + } else { + lower_dynamic_class_like_exists(ctx, name, value)?; + } store_if_result(ctx, inst) } +/// Lowers a dynamic string `class_exists()`-family lookup against known AOT metadata. +fn lower_dynamic_class_like_exists( + ctx: &mut FunctionContext<'_>, + name: &str, + value: ValueId, +) -> Result<()> { + if ctx.value_php_type(value)?.codegen_repr() != PhpType::Str { + return Err(CodegenIrError::unsupported(format!( + "{} with non-string dynamic name", + name + ))); + } + let candidates = dynamic_class_like_exists_candidates(ctx, name); + if candidates.is_empty() { + emit_static_bool(ctx, false); + return Ok(()); + } + + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + ctx.load_string_value_to_regs(value, ptr_reg, len_reg)?; + abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); + + let matched_label = ctx.next_label(&format!("{}_dynamic_match", name)); + let done_label = ctx.next_label(&format!("{}_dynamic_done", name)); + for candidate in candidates { + emit_branch_if_dynamic_class_like_exists_candidate(ctx, &candidate, &matched_label); + } + emit_static_bool(ctx, false); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&matched_label); + emit_static_bool(ctx, true); + + ctx.emitter.label(&done_label); + abi::emit_release_temporary_stack(ctx.emitter, 16); + Ok(()) +} + +/// Collects deterministic class-like name candidates for a dynamic existence lookup. +fn dynamic_class_like_exists_candidates(ctx: &FunctionContext<'_>, name: &str) -> Vec { + let mut candidates = BTreeSet::new(); + match name { + "class_exists" => { + candidates.extend( + ctx.module + .class_infos + .keys() + .filter(|class_name| !is_internal_synthetic_class_name(class_name)) + .cloned(), + ); + } + "interface_exists" => candidates.extend(ctx.module.interface_infos.keys().cloned()), + "trait_exists" => candidates.extend(ctx.module.trait_table.names.iter().cloned()), + "enum_exists" => candidates.extend(ctx.module.enum_infos.keys().cloned()), + _ => {} + } + candidates.into_iter().collect() +} + +/// Branches when the saved dynamic class-like string matches a metadata candidate. +fn emit_branch_if_dynamic_class_like_exists_candidate( + ctx: &mut FunctionContext<'_>, + candidate: &str, + matched_label: &str, +) { + let bare_candidate = candidate.trim_start_matches('\\'); + emit_dynamic_class_like_exists_compare(ctx, bare_candidate.as_bytes(), matched_label); + let qualified_candidate = format!("\\{}", bare_candidate); + emit_dynamic_class_like_exists_compare(ctx, qualified_candidate.as_bytes(), matched_label); +} + +/// Emits one case-insensitive comparison for the saved dynamic class-like lookup. +fn emit_dynamic_class_like_exists_compare( + ctx: &mut FunctionContext<'_>, + candidate: &[u8], + matched_label: &str, +) { + let (candidate_label, candidate_len) = ctx.data.add_string(candidate); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); + abi::emit_symbol_address(ctx.emitter, "x3", &candidate_label); + abi::emit_load_int_immediate(ctx.emitter, "x4", candidate_len as i64); + abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); + ctx.emitter.instruction("cmp x0, #0"); // did the dynamic class-like name match this metadata entry? + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // report existence when the runtime name matches case-insensitively + } + Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", 8); + abi::emit_symbol_address(ctx.emitter, "rdx", &candidate_label); + abi::emit_load_int_immediate(ctx.emitter, "rcx", candidate_len as i64); + abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); + ctx.emitter.instruction("test rax, rax"); // did the dynamic class-like name match this metadata entry? + ctx.emitter.instruction(&format!("je {}", matched_label)); // report existence when the runtime name matches case-insensitively + } + } +} + /// Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers. pub(crate) fn lower_is_callable(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "is_callable", 1)?; @@ -1157,23 +1260,26 @@ fn is_internal_synthetic_class_name(name: &str) -> bool { /// Returns a string literal value defined by a `ConstStr` instruction. fn const_string_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result { + maybe_const_string_operand(ctx, value)?.ok_or_else(|| { + CodegenIrError::unsupported("function_exists with non-literal function name") + }) +} + +/// Returns a string literal operand when a value is produced by `ConstStr`. +fn maybe_const_string_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result> { let value_ref = ctx .function .value(value) .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; let ValueDef::Instruction { inst, .. } = value_ref.def else { - return Err(CodegenIrError::unsupported( - "function_exists with non-literal function name", - )); + return Ok(None); }; let inst_ref = ctx .function .instruction(inst) .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; if inst_ref.op != Op::ConstStr { - return Err(CodegenIrError::unsupported( - "function_exists with non-literal function name", - )); + return Ok(None); } let Some(Immediate::Data(data)) = inst_ref.immediate else { return Err(CodegenIrError::invalid_module( @@ -1185,6 +1291,7 @@ fn const_string_operand(ctx: &FunctionContext<'_>, value: ValueId) -> Result, inst: &Instruction property_defaults, constructor_impl, ) = { - let class_info = ctx - .module - .class_infos - .get(&class_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", class_name)))?; + let class_info = + ctx.module.class_infos.get(&class_name).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", class_name)) + })?; if class_interfaces_require_missing_method_symbols(ctx, &class_name, class_info) { return Err(CodegenIrError::unsupported(format!( "object allocation requiring interface method symbols not emitted by EIR for {}", @@ -322,12 +323,17 @@ fn lower_callback_filter_iterator_new( } let source = expect_operand(inst, 0)?; let callback = expect_operand(inst, 1)?; - let (class_id, property_count, uninitialized_marker_offsets, property_defaults, callback_env_offset) = { - let class_info = ctx - .module - .class_infos - .get(class_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", class_name)))?; + let ( + class_id, + property_count, + uninitialized_marker_offsets, + property_defaults, + callback_env_offset, + ) = { + let class_info = + ctx.module.class_infos.get(class_name).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", class_name)) + })?; if class_info.allow_dynamic_properties { return Err(CodegenIrError::unsupported(format!( "object allocation requiring dynamic properties for {}", @@ -507,7 +513,13 @@ fn lower_iterator_iterator_new(ctx: &mut FunctionContext<'_>, inst: &Instruction .result .ok_or_else(|| CodegenIrError::invalid_module("object_new missing result value"))?; ctx.store_result_value(result)?; - emit_iterator_iterator_inner_from_traversable(ctx, source, inst.operands.get(1).copied(), result, &slot) + emit_iterator_iterator_inner_from_traversable( + ctx, + source, + inst.operands.get(1).copied(), + result, + &slot, + ) } /// Stores IteratorIterator::$inner after converting IteratorAggregate inputs through getIterator(). @@ -695,7 +707,10 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte ctx.emitter.instruction("str x9, [x0]"); // store the class id at object header abi::emit_symbol_address(ctx.emitter, "x9", "_iterator_iterator_downcast_msg"); ctx.emitter.instruction("str x9, [x0, #8]"); // store static exception message pointer - ctx.emitter.instruction(&format!("mov x9, #{}", ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len())); // load static exception message length + ctx.emitter.instruction(&format!( + "mov x9, #{}", + ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len() + )); // load static exception message length ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero abi::emit_symbol_address(ctx.emitter, "x9", "_exc_value"); @@ -710,13 +725,19 @@ fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionConte abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap kind word ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object - ctx.emitter.instruction("mov r10, QWORD PTR [rip + _spl_logic_exception_class_id]"); // load LogicException's runtime class id + ctx.emitter + .instruction("mov r10, QWORD PTR [rip + _spl_logic_exception_class_id]"); // load LogicException's runtime class id ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object header - ctx.emitter.instruction("lea r10, [rip + _iterator_iterator_downcast_msg]"); // materialize static exception message pointer + ctx.emitter + .instruction("lea r10, [rip + _iterator_iterator_downcast_msg]"); // materialize static exception message pointer ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static exception message pointer - ctx.emitter.instruction(&format!("mov QWORD PTR [rax + 16], {}", ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len())); // store static exception message length + ctx.emitter.instruction(&format!( + "mov QWORD PTR [rax + 16], {}", + ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len() + )); // store static exception message length ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero - ctx.emitter.instruction("mov QWORD PTR [rip + _exc_value], rax"); // publish the active exception object + ctx.emitter + .instruction("mov QWORD PTR [rip + _exc_value], rax"); // publish the active exception object ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing ctx.emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder @@ -731,7 +752,9 @@ fn emit_branch_if_saved_traversable_implements( target_label: &str, ) -> Result<()> { let interface_id = interface_info_by_name(ctx, interface_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("missing interface {}", interface_name)))? + .ok_or_else(|| { + CodegenIrError::unsupported(format!("missing interface {}", interface_name)) + })? .interface_id as i64; match ctx.emitter.target.arch { Arch::AArch64 => { @@ -770,7 +793,12 @@ fn emit_iterator_inner_property_from_result( let base_reg = abi::symbol_scratch_reg(ctx.emitter); let tag_reg = abi::secondary_scratch_reg(ctx.emitter); ctx.load_value_to_reg(target, base_reg)?; - abi::emit_store_to_address(ctx.emitter, abi::int_result_reg(ctx.emitter), base_reg, inner_offset); + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + base_reg, + inner_offset, + ); abi::emit_load_int_immediate(ctx.emitter, tag_reg, 6); abi::emit_store_to_address(ctx.emitter, tag_reg, base_reg, inner_offset + 8); Ok(()) @@ -808,6 +836,7 @@ fn is_builtin_throwable_payload_class(class_name: &str) -> bool { | "ArithmeticError" | "Exception" | "RuntimeException" + | "ReflectionException" | "JsonException" | "FiberError" | "LogicException" @@ -879,7 +908,10 @@ fn emit_throwable_allocation(ctx: &mut FunctionContext<'_>, class_id: u64) { Arch::X86_64 => { ctx.emitter.instruction("mov rax, 32"); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 6)); // materialize the x86_64 Throwable heap kind word + ctx.emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 6 + )); // materialize the x86_64 Throwable heap kind word ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the Throwable payload ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the Throwable runtime class id ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at payload offset zero @@ -915,6 +947,7 @@ fn emit_throwable_message_fields_aarch64( ) -> Result<()> { if let Some(message) = message { ctx.load_string_value_to_regs(message, "x1", "x2")?; + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); } else { emit_empty_string_to_regs(ctx, "x1", "x2"); } @@ -931,6 +964,7 @@ fn emit_throwable_message_fields_x86_64( ) -> Result<()> { if let Some(message) = message { ctx.load_string_value_to_regs(message, "rax", "rdx")?; + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); } else { emit_empty_string_to_regs(ctx, "rax", "rdx"); } @@ -948,10 +982,7 @@ fn emit_empty_string_to_regs(ctx: &mut FunctionContext<'_>, ptr_reg: &str, len_r } /// Writes the integer exception code into the compact Throwable payload. -fn emit_throwable_code_field( - ctx: &mut FunctionContext<'_>, - code: Option, -) -> Result<()> { +fn emit_throwable_code_field(ctx: &mut FunctionContext<'_>, code: Option) -> Result<()> { match ctx.emitter.target.arch { Arch::AArch64 => emit_throwable_code_field_aarch64(ctx, code), Arch::X86_64 => emit_throwable_code_field_x86_64(ctx, code), @@ -1051,7 +1082,8 @@ fn move_fiber_callable_result_to_arg(ctx: &mut FunctionContext<'_>, callable_arg if result_reg == callable_arg { return; } - ctx.emitter.instruction(&format!("mov {}, {}", callable_arg, result_reg)); // pass selected callable descriptor to Fiber constructor + ctx.emitter + .instruction(&format!("mov {}, {}", callable_arg, result_reg)); // pass selected callable descriptor to Fiber constructor } /// Lowers constrained runtime class-string object construction. @@ -1061,10 +1093,9 @@ pub(super) fn lower_dynamic_object_new( ) -> Result<()> { let (_fallback_class, required_parent) = dynamic_object_new_metadata(ctx, inst)?; let class_name_value = expect_operand(inst, 0)?; - let constructor_args = inst - .operands - .get(1..) - .ok_or_else(|| CodegenIrError::invalid_module("dynamic_object_new missing class operand"))?; + let constructor_args = inst.operands.get(1..).ok_or_else(|| { + CodegenIrError::invalid_module("dynamic_object_new missing class operand") + })?; let candidates = dynamic_new_candidates(ctx, &required_parent, constructor_args.len(), inst)?; if candidates.is_empty() { return Err(CodegenIrError::unsupported(format!( @@ -1115,13 +1146,12 @@ pub(super) fn lower_dynamic_object_new_mixed( inst: &Instruction, ) -> Result<()> { let class_name_value = expect_operand(inst, 0)?; - let constructor_args = inst - .operands - .get(1..) - .ok_or_else(|| CodegenIrError::invalid_module("dynamic_object_new_mixed missing class operand"))?; - let result = inst - .result - .ok_or_else(|| CodegenIrError::invalid_module("dynamic_object_new_mixed missing result value"))?; + let constructor_args = inst.operands.get(1..).ok_or_else(|| { + CodegenIrError::invalid_module("dynamic_object_new_mixed missing class operand") + })?; + let result = inst.result.ok_or_else(|| { + CodegenIrError::invalid_module("dynamic_object_new_mixed missing result value") + })?; let done_label = ctx.next_label("dynamic_new_mixed_done"); let non_string_label = ctx.next_label("dynamic_new_mixed_non_string"); if !emit_generic_dynamic_new_class_string(ctx, class_name_value, &non_string_label)? { @@ -1145,7 +1175,13 @@ pub(super) fn lower_dynamic_object_new_mixed( for (candidate, label) in candidates.iter().zip(case_labels.iter()) { ctx.emitter.label(label); abi::emit_release_temporary_stack(ctx.emitter, 16); - emit_dynamic_new_mixed_candidate(ctx, candidate, constructor_args, class_name_value, result)?; + emit_dynamic_new_mixed_candidate( + ctx, + candidate, + constructor_args, + class_name_value, + result, + )?; abi::emit_jump(ctx.emitter, &done_label); } @@ -1180,11 +1216,13 @@ fn emit_generic_dynamic_new_class_string( match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter.instruction("cmp x0, #1"); // require a boxed string class name for dynamic construction - ctx.emitter.instruction(&format!("b.ne {}", non_string_label)); // non-string class names produce the runtime null fallback + ctx.emitter + .instruction(&format!("b.ne {}", non_string_label)); // non-string class names produce the runtime null fallback } Arch::X86_64 => { ctx.emitter.instruction("cmp rax, 1"); // require a boxed string class name for dynamic construction - ctx.emitter.instruction(&format!("jne {}", non_string_label)); // non-string class names produce the runtime null fallback + ctx.emitter + .instruction(&format!("jne {}", non_string_label)); // non-string class names produce the runtime null fallback ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the string result register } } @@ -1251,6 +1289,7 @@ fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { "OverflowException", "RangeException", "RecursiveCallbackFilterIterator", + "ReflectionException", "ReflectionClass", "ReflectionClassConstant", "ReflectionEnumBackedCase", @@ -1321,6 +1360,7 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionClassConstant", "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", + "ReflectionException", "ReflectionMethod", "ReflectionParameter", "ReflectionProperty", @@ -1421,10 +1461,7 @@ fn emit_dynamic_new_mixed_candidate( } abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); abi::emit_release_temporary_stack(ctx.emitter, 16); - emit_box_current_value_as_mixed( - ctx.emitter, - &PhpType::Object(candidate.class_name.clone()), - ); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(candidate.class_name.clone())); ctx.store_result_value(result) } @@ -1731,10 +1768,7 @@ fn emit_dynamic_new_class_lookup( } /// Unboxes a mixed class-string or emits the dynamic-factory fatal. -fn emit_dynamic_new_mixed_class_string( - ctx: &mut FunctionContext<'_>, - required_parent: &str, -) { +fn emit_dynamic_new_mixed_class_string(ctx: &mut FunctionContext<'_>, required_parent: &str) { let string_label = ctx.next_label("dynamic_new_class_string"); abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { @@ -1755,10 +1789,7 @@ fn emit_dynamic_new_mixed_class_string( } /// Branches when the dynamic factory lookup failed or named an interface. -fn emit_branch_if_dynamic_new_lookup_invalid( - ctx: &mut FunctionContext<'_>, - invalid_label: &str, -) { +fn emit_branch_if_dynamic_new_lookup_invalid(ctx: &mut FunctionContext<'_>, invalid_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter.instruction("cmp x0, #0"); // did the dynamic factory class-string resolve to metadata? @@ -1793,11 +1824,13 @@ fn emit_compare_dynamic_new_class_id( abi::emit_load_temporary_stack_slot(ctx.emitter, scratch, 0); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, #{}", scratch, class_id)); // compare the requested factory class with this candidate class id + ctx.emitter + .instruction(&format!("cmp {}, #{}", scratch, class_id)); // compare the requested factory class with this candidate class id ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // branch when the runtime class-string selected this constructor } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", scratch, class_id)); // compare the requested factory class with this candidate class id + ctx.emitter + .instruction(&format!("cmp {}, {}", scratch, class_id)); // compare the requested factory class with this candidate class id ctx.emitter.instruction(&format!("je {}", matched_label)); // branch when the runtime class-string selected this constructor } } @@ -1846,8 +1879,9 @@ fn emit_dynamic_new_fatal(ctx: &mut FunctionContext<'_>, required_parent: &str) /// Emits PHP's fatal diagnostic for source-level `new $name` with a missing class. fn emit_dynamic_new_class_not_found_fatal(ctx: &mut FunctionContext<'_>) { - let (prefix_label, prefix_len) = - ctx.data.add_string(b"Fatal error: Uncaught Error: Class \""); + let (prefix_label, prefix_len) = ctx + .data + .add_string(b"Fatal error: Uncaught Error: Class \""); let (suffix_label, suffix_len) = ctx.data.add_string(b"\" not found\n"); match ctx.emitter.target.arch { Arch::AArch64 => { @@ -1903,12 +1937,14 @@ fn emit_fatal_message(ctx: &mut FunctionContext<'_>, message: &[u8]) { ctx.emitter.instruction("mov x0, #2"); // select stderr for the fatal diagnostic ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); - ctx.emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the fatal diagnostic byte length to write() + ctx.emitter + .instruction(&format!("mov x2, #{}", message_len)); // pass the fatal diagnostic byte length to write() ctx.emitter.syscall(4); } Arch::X86_64 => { abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); - ctx.emitter.instruction(&format!("mov edx, {}", message_len)); // pass the fatal diagnostic byte length to write() + ctx.emitter + .instruction(&format!("mov edx, {}", message_len)); // pass the fatal diagnostic byte length to write() ctx.emitter.instruction("mov edi, 2"); // select stderr for the fatal diagnostic ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall ctx.emitter.instruction("syscall"); // write the fatal diagnostic bytes @@ -1994,10 +2030,14 @@ fn emit_property_default( abi::emit_symbol_address(ctx.emitter, scratch, &label); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("ldr {}, [{}]", float_reg, scratch)); // load the property default float literal through the symbol scratch register + ctx.emitter + .instruction(&format!("ldr {}, [{}]", float_reg, scratch)); + // load the property default float literal through the symbol scratch register } Arch::X86_64 => { - ctx.emitter.instruction(&format!("movsd {}, QWORD PTR [{}]", float_reg, scratch)); // load the property default float literal through the symbol scratch register + ctx.emitter + .instruction(&format!("movsd {}, QWORD PTR [{}]", float_reg, scratch)); + // load the property default float literal through the symbol scratch register } } abi::emit_store_to_address(ctx.emitter, float_reg, object_reg, default.offset); @@ -2147,7 +2187,10 @@ pub(super) fn lower_prop_get(ctx: &mut FunctionContext<'_>, inst: &Instruction) if let Some(class_name) = union_object_member_class(ctx, object)? { return lower_union_object_prop_get(ctx, inst, object, &class_name, &property); } - if matches!(ctx.value_php_type(object)?.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + if matches!( + ctx.value_php_type(object)?.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { return lower_mixed_prop_get(ctx, inst, object, &property); } if object_is_builtin_stdclass(ctx, object)? { @@ -2276,7 +2319,11 @@ fn magic_get_receiver_class( let Some(class_info) = ctx.module.class_infos.get(normalized) else { return Ok(None); }; - if class_info.properties.iter().any(|(name, _)| name == property) { + if class_info + .properties + .iter() + .any(|(name, _)| name == property) + { return Ok(None); } if class_info.methods.contains_key(&php_symbol_key("__get")) { @@ -2304,7 +2351,10 @@ fn lower_magic_get_prop( if let Some(slot) = target.dynamic_slot { super::emit_dynamic_instance_method_call(ctx, slot); } else { - abi::emit_call_label(ctx.emitter, &method_symbol(&target.impl_class, &target.method_key)); + abi::emit_call_label( + ctx.emitter, + &method_symbol(&target.impl_class, &target.method_key), + ); } store_method_call_result(ctx, inst, &target) } @@ -2381,7 +2431,8 @@ fn lower_allow_dynamic_prop_get( ctx.load_value_to_reg(object, object_reg)?; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + ctx.emitter + .instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); abi::emit_call_label(ctx.emitter, "__rt_hash_get"); @@ -2390,7 +2441,10 @@ fn lower_allow_dynamic_prop_get( ctx.emitter.instruction(&format!("b {}", done_label)); // skip the null fallback after a successful dynamic-property hit } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov rdi, QWORD PTR [{} + {}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + ctx.emitter.instruction(&format!( + "mov rdi, QWORD PTR [{} + {}]", + object_reg, hash_offset + )); // load the dynamic-property hash pointer from the receiver abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); abi::emit_call_label(ctx.emitter, "__rt_hash_get"); @@ -2610,11 +2664,13 @@ fn emit_branch_to_stdclass_candidate( abi::emit_load_int_immediate(ctx.emitter, scratch_reg, stdclass_id as i64); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage + ctx.emitter + .instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage ctx.emitter.instruction(&format!("b.eq {}", stdclass_label)); // route stdClass reads through the hash-backed helper } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage + ctx.emitter + .instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage ctx.emitter.instruction(&format!("je {}", stdclass_label)); // route stdClass reads through the hash-backed helper } } @@ -2713,17 +2769,22 @@ fn lower_nullable_prop_get_with_warning( /// Emits PHP's warning for reading a property from null. fn emit_property_on_null_warning(ctx: &mut FunctionContext<'_>, property: &str) { - let message = format!("Warning: Attempt to read property \"{}\" on null\n", property); + let message = format!( + "Warning: Attempt to read property \"{}\" on null\n", + property + ); let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); - ctx.emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the property-on-null warning byte length + ctx.emitter + .instruction(&format!("mov x2, #{}", message_len)); // pass the property-on-null warning byte length } Arch::X86_64 => { abi::emit_symbol_address(ctx.emitter, "rdi", &message_label); - ctx.emitter.instruction(&format!("mov esi, {}", message_len)); // pass the property-on-null warning byte length + ctx.emitter + .instruction(&format!("mov esi, {}", message_len)); // pass the property-on-null warning byte length } } abi::emit_call_label(ctx.emitter, "__rt_diag_warning"); @@ -2773,7 +2834,10 @@ pub(super) fn lower_dynamic_prop_get( if let Some(property) = const_string_operand(ctx, property_value)? { return lower_const_dynamic_prop_get(ctx, object, property, inst); } - if matches!(ctx.value_php_type(object)?.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + if matches!( + ctx.value_php_type(object)?.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { return lower_runtime_dynamic_mixed_prop_get(ctx, inst, object, property_value); } if object_is_builtin_stdclass(ctx, object)? { @@ -2789,7 +2853,10 @@ fn lower_const_dynamic_prop_get( property: &str, inst: &Instruction, ) -> Result<()> { - if matches!(ctx.value_php_type(object)?.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + if matches!( + ctx.value_php_type(object)?.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { return lower_mixed_prop_get(ctx, inst, object, property); } if object_is_builtin_stdclass(ctx, object)? { @@ -2951,11 +3018,10 @@ fn declared_dynamic_property_slots( ) -> Result> { let normalized = class_name.trim_start_matches('\\'); let property_names = { - let class_info = ctx - .module - .class_infos - .get(normalized) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", normalized)))?; + let class_info = + ctx.module.class_infos.get(normalized).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", normalized)) + })?; class_info .properties .iter() @@ -3055,7 +3121,8 @@ fn emit_branch_if_dynamic_name_matches( abi::emit_symbol_address(ctx.emitter, "x3", &label); abi::emit_load_int_immediate(ctx.emitter, "x4", len as i64); ctx.emitter.instruction("bl __rt_str_eq"); // compare the runtime property name against this declared property - ctx.emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the declared property slot when the names match + ctx.emitter + .instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the declared property slot when the names match } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); @@ -3541,13 +3608,18 @@ fn lower_allow_dynamic_prop_set( Arch::AArch64 => { ctx.emitter.instruction(&format!("mov {}, x0", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore abi::emit_pop_reg(ctx.emitter, object_reg); - ctx.emitter.instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + ctx.emitter + .instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver abi::emit_push_reg(ctx.emitter, object_reg); abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); ctx.emitter.instruction(&format!("mov x3, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use the high payload word - abi::emit_load_int_immediate(ctx.emitter, "x5", runtime_value_tag(&PhpType::Mixed) as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Mixed) as i64, + ); abi::emit_call_label(ctx.emitter, "__rt_hash_set"); abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, "x0", object_reg, hash_offset); @@ -3555,13 +3627,20 @@ fn lower_allow_dynamic_prop_set( Arch::X86_64 => { ctx.emitter.instruction(&format!("mov {}, rax", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore abi::emit_pop_reg(ctx.emitter, object_reg); - ctx.emitter.instruction(&format!("mov rdi, QWORD PTR [{} + {}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver + ctx.emitter.instruction(&format!( + "mov rdi, QWORD PTR [{} + {}]", + object_reg, hash_offset + )); // load the dynamic-property hash pointer from the receiver abi::emit_push_reg(ctx.emitter, object_reg); abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); ctx.emitter.instruction(&format!("mov rcx, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash entries do not use the high payload word - abi::emit_load_int_immediate(ctx.emitter, "r9", runtime_value_tag(&PhpType::Mixed) as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Mixed) as i64, + ); abi::emit_call_label(ctx.emitter, "__rt_hash_set"); abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, "rax", object_reg, hash_offset); @@ -3625,14 +3704,16 @@ fn emit_property_assign_on_null_fatal(ctx: &mut FunctionContext<'_>, property: & ctx.emitter.instruction("mov x0, #2"); // write the property-assign-on-null fatal to stderr ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); - ctx.emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the property-assign-on-null fatal byte length + ctx.emitter + .instruction(&format!("mov x2, #{}", message_len)); // pass the property-assign-on-null fatal byte length ctx.emitter.syscall(4); abi::emit_exit(ctx.emitter, 1); } Arch::X86_64 => { ctx.emitter.instruction("mov edi, 2"); // write the property-assign-on-null fatal to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); - ctx.emitter.instruction(&format!("mov edx, {}", message_len)); // pass the property-assign-on-null fatal byte length + ctx.emitter + .instruction(&format!("mov edx, {}", message_len)); // pass the property-assign-on-null fatal byte length ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write ctx.emitter.instruction("syscall"); // emit the property-assign-on-null fatal before exiting abi::emit_exit(ctx.emitter, 1); @@ -3644,7 +3725,10 @@ fn emit_property_assign_on_null_fatal(ctx: &mut FunctionContext<'_>, property: & pub(super) fn lower_instanceof(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let value = expect_operand(inst, 0)?; let value_ty = ctx.value_php_type(value)?; - if !matches!(value_ty, PhpType::Object(_) | PhpType::Mixed | PhpType::Union(_)) { + if !matches!( + value_ty, + PhpType::Object(_) | PhpType::Mixed | PhpType::Union(_) + ) { emit_false(ctx); return store_if_result(ctx, inst); } @@ -3668,7 +3752,10 @@ pub(super) fn lower_instanceof(ctx: &mut FunctionContext<'_>, inst: &Instruction } /// Lowers dynamic `instanceof` where the target is resolved from a runtime string or object. -pub(super) fn lower_instanceof_dynamic(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_instanceof_dynamic( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { let value = expect_operand(inst, 0)?; let target = expect_operand(inst, 1)?; let value_ty = ctx.value_php_type(value)?; @@ -3701,7 +3788,8 @@ fn emit_object_allocation( let payload_size = dynamic_properties_offset + dynamic_properties_bytes; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov x0, #{}", payload_size)); // request object payload storage for the class id and property slots + ctx.emitter + .instruction(&format!("mov x0, #{}", payload_size)); // request object payload storage for the class id and property slots abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the object payload @@ -3709,9 +3797,13 @@ fn emit_object_allocation( ctx.emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov rax, {}", payload_size)); // request object payload storage for the class id and property slots + ctx.emitter + .instruction(&format!("mov rax, {}", payload_size)); // request object payload storage for the class id and property slots abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word + ctx.emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 4 + )); // materialize the x86_64 object heap kind word ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the object payload ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the compile-time class id ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero @@ -3725,7 +3817,11 @@ fn emit_object_allocation( } if !uninitialized_marker_offsets.is_empty() { let marker_reg = abi::secondary_scratch_reg(ctx.emitter); - abi::emit_load_int_immediate(ctx.emitter, marker_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + abi::emit_load_int_immediate( + ctx.emitter, + marker_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); for offset in uninitialized_marker_offsets { abi::emit_store_to_address(ctx.emitter, marker_reg, object_reg, *offset); } @@ -3766,23 +3862,27 @@ fn dynamic_property_hash_offset(property_count: usize) -> usize { } /// Allocates the per-object dynamic-property hash and stores it in the object payload. -fn emit_dynamic_property_hash_init( - ctx: &mut FunctionContext<'_>, - object_reg: &str, - offset: usize, -) { +fn emit_dynamic_property_hash_init(ctx: &mut FunctionContext<'_>, object_reg: &str, offset: usize) { let hash_reg = abi::secondary_scratch_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, object_reg); match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_load_int_immediate(ctx.emitter, "x0", 4); - abi::emit_load_int_immediate(ctx.emitter, "x1", runtime_value_tag(&PhpType::Mixed) as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x1", + runtime_value_tag(&PhpType::Mixed) as i64, + ); abi::emit_call_label(ctx.emitter, "__rt_hash_new"); ctx.emitter.instruction(&format!("mov {}, x0", hash_reg)); // preserve the dynamic-property hash across object restore } Arch::X86_64 => { abi::emit_load_int_immediate(ctx.emitter, "rdi", 4); - abi::emit_load_int_immediate(ctx.emitter, "rsi", runtime_value_tag(&PhpType::Mixed) as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "rsi", + runtime_value_tag(&PhpType::Mixed) as i64, + ); abi::emit_call_label(ctx.emitter, "__rt_hash_new"); ctx.emitter.instruction(&format!("mov {}, rax", hash_reg)); // preserve the dynamic-property hash across object restore } @@ -3799,7 +3899,11 @@ fn class_interfaces_require_missing_method_symbols( ) -> bool { let emitted_methods = emitted_instance_method_keys(ctx); let mut seen = HashSet::new(); - let mut stack = class_info.interfaces.iter().map(String::as_str).collect::>(); + let mut stack = class_info + .interfaces + .iter() + .map(String::as_str) + .collect::>(); while let Some(interface_name) = stack.pop() { if !seen.insert(interface_name.to_string()) { continue; @@ -3881,8 +3985,7 @@ fn class_method_already_emitted( .name .rsplit_once("::") .is_some_and(|(candidate_class, candidate_method)| { - candidate_class == class_name - && php_symbol_key(candidate_method) == method_key + candidate_class == class_name && php_symbol_key(candidate_method) == method_key }) }) } @@ -3976,11 +4079,17 @@ fn dynamic_property_hash_offset_for_class( .class_infos .get(normalized) .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", normalized)))?; - if class_info.properties.iter().any(|(name, _)| name == property) { + if class_info + .properties + .iter() + .any(|(name, _)| name == property) + { return Ok(None); } if class_info.allow_dynamic_properties { - return Ok(Some(dynamic_property_hash_offset(class_info.properties.len()))); + return Ok(Some(dynamic_property_hash_offset( + class_info.properties.len(), + ))); } Ok(None) } @@ -4011,8 +4120,7 @@ fn resolve_property_slot_for_class( .class_infos .get(normalized) .ok_or_else(|| CodegenIrError::unsupported(format!("unknown class {}", normalized)))?; - let Some((index, (_, php_type))) = class_info.visible_property(property) - else { + let Some((index, (_, php_type))) = class_info.visible_property(property) else { return Err(CodegenIrError::unsupported(format!( "{} for dynamic or missing property {}::${}", inst.op.name(), @@ -4077,7 +4185,9 @@ fn const_string_operand<'a>(ctx: &FunctionContext<'a>, value: ValueId) -> Result return Ok(None); } let Some(Immediate::Data(data)) = instruction.immediate else { - return Err(CodegenIrError::invalid_module("const_str missing data immediate")); + return Err(CodegenIrError::invalid_module( + "const_str missing data immediate", + )); }; ctx.module .data @@ -4120,10 +4230,7 @@ pub(super) fn nullable_object_receiver_class( } /// Returns the unique object class carried by a boxed union, ignoring null and scalar arms. -fn union_object_member_class( - ctx: &FunctionContext<'_>, - object: ValueId, -) -> Result> { +fn union_object_member_class(ctx: &FunctionContext<'_>, object: ValueId) -> Result> { let PhpType::Union(members) = raw_value_php_type(ctx, object)? else { return Ok(None); }; @@ -4175,7 +4282,11 @@ pub(super) fn emit_nullable_receiver_object_payload( /// Boxes a PHP null sentinel as a runtime Mixed cell. pub(super) fn emit_boxed_null(ctx: &mut FunctionContext<'_>) { - abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), RUNTIME_NULL_SENTINEL); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + RUNTIME_NULL_SENTINEL, + ); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Void); } @@ -4191,8 +4302,14 @@ fn resolve_packed_field_slot( .module .packed_class_infos .get(normalized) - .ok_or_else(|| CodegenIrError::unsupported(format!("unknown packed class {}", normalized)))?; - let Some(field) = class_info.fields.iter().find(|field| field.name == property) else { + .ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown packed class {}", normalized)) + })?; + let Some(field) = class_info + .fields + .iter() + .find(|field| field.name == property) + else { return Err(CodegenIrError::unsupported(format!( "{} for missing packed field {}::${}", inst.op.name(), @@ -4320,11 +4437,9 @@ fn object_type_implements_interface( let Some(class_info) = class_info_by_name(ctx, &class_name) else { return false; }; - if class_info - .interfaces - .iter() - .any(|interface_name| interface_extends_interface(ctx, interface_name, target_interface)) - { + if class_info.interfaces.iter().any(|interface_name| { + interface_extends_interface(ctx, interface_name, target_interface) + }) { return true; } current = class_info.parent.clone(); @@ -4351,11 +4466,7 @@ fn interface_extends_interface( } /// Returns true when a class is or extends the target class. -fn class_extends_class( - ctx: &FunctionContext<'_>, - class_name: &str, - target_class: &str, -) -> bool { +fn class_extends_class(ctx: &FunctionContext<'_>, class_name: &str, target_class: &str) -> bool { let mut current = Some(class_name.to_string()); while let Some(name) = current { if same_php_type_name(&name, target_class) { @@ -4367,10 +4478,7 @@ fn class_extends_class( } /// Finds class metadata by PHP-case-insensitive name. -fn class_info_by_name<'a>( - ctx: &'a FunctionContext<'_>, - class_name: &str, -) -> Option<&'a ClassInfo> { +fn class_info_by_name<'a>(ctx: &'a FunctionContext<'_>, class_name: &str) -> Option<&'a ClassInfo> { let wanted = php_symbol_key(class_name.trim_start_matches('\\')); ctx.module .class_infos @@ -4453,8 +4561,7 @@ fn can_convert_indexed_array_to_mixed_property(value_ty: &PhpType, slot_ty: &Php else { return false; }; - slot_elem.codegen_repr() == PhpType::Mixed - && value_elem.codegen_repr() != PhpType::Mixed + slot_elem.codegen_repr() == PhpType::Mixed && value_elem.codegen_repr() != PhpType::Mixed } /// Returns true when a value can initialize a pointer-sized slot as null. @@ -4518,10 +4625,12 @@ fn emit_property_load( let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); } - _ => return Err(CodegenIrError::unsupported(format!( + _ => { + return Err(CodegenIrError::unsupported(format!( "property load for PHP type {:?}", slot.php_type - ))), + ))) + } } Ok(()) } @@ -4545,15 +4654,20 @@ fn emit_reference_property_load( abi::emit_load_from_address(ctx.emitter, float_reg, pointer_reg, 0); } ty if is_pointer_sized_property_type(&ty) - || matches!(ty, PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never) => + || matches!( + ty, + PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never + ) => { let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, int_reg, pointer_reg, 0); } - ty => return Err(CodegenIrError::unsupported(format!( + ty => { + return Err(CodegenIrError::unsupported(format!( "reference property load for PHP type {:?}", ty - ))), + ))) + } } Ok(()) } @@ -4580,22 +4694,31 @@ fn emit_packed_field_load( PhpType::Packed(_) => { let int_reg = abi::int_result_reg(ctx.emitter); if slot.offset == 0 { - ctx.emitter.instruction(&format!("mov {}, {}", int_reg, base_reg)); // return the nested packed field address directly + ctx.emitter + .instruction(&format!("mov {}, {}", int_reg, base_reg)); // return the nested packed field address directly } else { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("add {}, {}, #{}", int_reg, base_reg, slot.offset)); // compute the nested packed field address + ctx.emitter.instruction(&format!( + "add {}, {}, #{}", + int_reg, base_reg, slot.offset + )); // compute the nested packed field address } Arch::X86_64 => { - ctx.emitter.instruction(&format!("lea {}, [{} + {}]", int_reg, base_reg, slot.offset)); // compute the nested packed field address + ctx.emitter.instruction(&format!( + "lea {}, [{} + {}]", + int_reg, base_reg, slot.offset + )); // compute the nested packed field address } } } } - _ => return Err(CodegenIrError::unsupported(format!( + _ => { + return Err(CodegenIrError::unsupported(format!( "packed field load for PHP type {:?}", slot.php_type - ))), + ))) + } } Ok(()) } @@ -4669,10 +4792,12 @@ fn emit_property_store( abi::emit_store_to_address(ctx.emitter, int_reg, base_reg, slot.offset); abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset + 8); } - _ => return Err(CodegenIrError::unsupported(format!( + _ => { + return Err(CodegenIrError::unsupported(format!( "property store for PHP type {:?}", slot.php_type - ))), + ))) + } } Ok(()) } @@ -4685,7 +4810,12 @@ fn emit_reference_property_bind( base_reg: &str, ) -> Result<()> { super::materialize_local_ref_arg_address(ctx, value)?; - abi::emit_store_to_address(ctx.emitter, abi::int_result_reg(ctx.emitter), base_reg, slot.offset); + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + base_reg, + slot.offset, + ); abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset + 8); Ok(()) } @@ -4799,7 +4929,12 @@ fn release_previous_referenced_value( abi::emit_push_result_value(ctx.emitter, &result_ty.codegen_repr()); } abi::emit_push_reg(ctx.emitter, pointer_reg); - abi::emit_load_from_address(ctx.emitter, abi::int_result_reg(ctx.emitter), pointer_reg, 0); + abi::emit_load_from_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + pointer_reg, + 0, + ); match prop_ty { PhpType::Str => abi::emit_call_label(ctx.emitter, "__rt_heap_free_safe"), PhpType::Callable => callable_descriptor::emit_release_current_descriptor(ctx.emitter), @@ -4824,17 +4959,32 @@ fn store_current_result_to_reference_cell( abi::emit_store_to_address(ctx.emitter, len_reg, pointer_reg, 8); } PhpType::Float => { - abi::emit_store_to_address(ctx.emitter, abi::float_result_reg(ctx.emitter), pointer_reg, 0); + abi::emit_store_to_address( + ctx.emitter, + abi::float_result_reg(ctx.emitter), + pointer_reg, + 0, + ); } ty if is_pointer_sized_property_type(&ty) - || matches!(ty, PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never) => + || matches!( + ty, + PhpType::Bool | PhpType::Int | PhpType::Void | PhpType::Never + ) => { - abi::emit_store_to_address(ctx.emitter, abi::int_result_reg(ctx.emitter), pointer_reg, 0); + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + pointer_reg, + 0, + ); } - ty => return Err(CodegenIrError::unsupported(format!( + ty => { + return Err(CodegenIrError::unsupported(format!( "reference property store for PHP type {:?}", ty - ))), + ))) + } } Ok(()) } @@ -4867,7 +5017,12 @@ fn release_previous_property_value( abi::emit_push_result_value(ctx.emitter, &result_ty.codegen_repr()); } abi::emit_push_reg(ctx.emitter, base_reg); - abi::emit_load_from_address(ctx.emitter, abi::int_result_reg(ctx.emitter), base_reg, offset); + abi::emit_load_from_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + base_reg, + offset, + ); match prop_ty { PhpType::Str => abi::emit_call_label(ctx.emitter, "__rt_heap_free_safe"), PhpType::Callable => callable_descriptor::emit_release_current_descriptor(ctx.emitter), @@ -4983,10 +5138,12 @@ fn emit_packed_field_store( abi::emit_pop_reg(ctx.emitter, base_reg); abi::emit_store_to_address(ctx.emitter, int_reg, base_reg, slot.offset); } - _ => return Err(CodegenIrError::unsupported(format!( + _ => { + return Err(CodegenIrError::unsupported(format!( "packed field store for PHP type {:?}", slot.php_type - ))), + ))) + } } Ok(()) } @@ -5019,15 +5176,23 @@ fn emit_uninitialized_typed_property_guard( let marker_reg = abi::secondary_scratch_reg(ctx.emitter); let sentinel_reg = abi::tertiary_scratch_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, marker_reg, object_reg, slot.offset + 8); - abi::emit_load_int_immediate(ctx.emitter, sentinel_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + abi::emit_load_int_immediate( + ctx.emitter, + sentinel_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel - ctx.emitter.instruction(&format!("b.ne {}", initialized_label)); // continue the property read once the slot has been initialized + ctx.emitter + .instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel + ctx.emitter + .instruction(&format!("b.ne {}", initialized_label)); // continue the property read once the slot has been initialized } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel - ctx.emitter.instruction(&format!("jne {}", initialized_label)); // continue the property read once the slot has been initialized + ctx.emitter + .instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel + ctx.emitter + .instruction(&format!("jne {}", initialized_label)); // continue the property read once the slot has been initialized } } emit_uninitialized_typed_property_fatal(ctx, slot); @@ -5303,12 +5468,7 @@ fn emit_invalid_dynamic_target_fatal(ctx: &mut FunctionContext<'_>) { } /// Emits the metadata matcher call with object-or-mixed input already in argument 0. -fn emit_match_call( - ctx: &mut FunctionContext<'_>, - target_id: u64, - target_kind: i64, - helper: &str, -) { +fn emit_match_call(ctx: &mut FunctionContext<'_>, target_id: u64, target_kind: i64, helper: &str) { abi::emit_load_int_immediate( ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1), @@ -5323,10 +5483,7 @@ fn emit_match_call( } /// Classifies a named target as a class `(kind 0)` or interface `(kind 1)`. -fn classify_named_target( - ctx: &FunctionContext<'_>, - class_name: &str, -) -> Option<(u64, i64)> { +fn classify_named_target(ctx: &FunctionContext<'_>, class_name: &str) -> Option<(u64, i64)> { let normalized = class_name.trim_start_matches('\\'); if let Some(class_info) = ctx.module.class_infos.get(normalized) { return Some((class_info.class_id, 0)); @@ -5357,10 +5514,7 @@ fn property_name_immediate<'a>( } /// Resolves an instruction class-name immediate into the module data pool. -fn class_name_immediate<'a>( - ctx: &'a FunctionContext<'_>, - inst: &Instruction, -) -> Result<&'a str> { +fn class_name_immediate<'a>(ctx: &'a FunctionContext<'_>, inst: &Instruction) -> Result<&'a str> { let data = expect_data(inst)?; ctx.module .data diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 710f649e63..ef8030b882 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -542,6 +542,7 @@ fn seed_runtime_throwable_class_names(module: &Module, names: &mut HashSet, class_map: &mut HashMap, @@ -74,6 +74,7 @@ pub(crate) fn inject_builtin_throwables( "ArithmeticError", "Exception", "RuntimeException", + "ReflectionException", "JsonException", "Fiber", "FiberError", @@ -162,9 +163,9 @@ pub(crate) fn inject_builtin_throwables( used_traits: Vec::new(), }, ); - // RuntimeException and JsonException inherit the Throwable API from - // Exception via the standard inheritance machinery; they don't need to - // redeclare anything locally. + // RuntimeException, ReflectionException, and JsonException inherit the + // Throwable API from Exception via the standard inheritance machinery; they + // don't need to redeclare anything locally. class_map.insert( "RuntimeException".to_string(), FlattenedClass { @@ -181,6 +182,22 @@ pub(crate) fn inject_builtin_throwables( used_traits: Vec::new(), }, ); + class_map.insert( + "ReflectionException".to_string(), + FlattenedClass { + name: "ReflectionException".to_string(), + extends: Some("Exception".to_string()), + implements: Vec::new(), + is_abstract: false, + is_final: false, + is_readonly_class: false, + properties: Vec::new(), + methods: Vec::new(), + attributes: Vec::new(), + constants: Vec::new(), + used_traits: Vec::new(), + }, + ); class_map.insert( "JsonException".to_string(), FlattenedClass { diff --git a/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index 33523e1c0b..f4f06425b4 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -9,10 +9,9 @@ //! Key details: //! - Dummy AST members carry type contracts only; runtime behavior is implemented elsewhere. -use crate::names::{Name, php_symbol_key}; +use crate::names::{php_symbol_key, Name}; use crate::parser::ast::{ - ClassMethod, ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, - Visibility, + ClassMethod, ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, Visibility, }; use crate::types::PhpType; @@ -188,7 +187,10 @@ pub(super) fn builtin_exception_get_trace_method() -> ClassMethod { concrete_throwable_method( "getTrace", array_type(), - Expr::new(ExprKind::ArrayLiteral(Vec::new()), crate::span::Span::dummy()), + Expr::new( + ExprKind::ArrayLiteral(Vec::new()), + crate::span::Span::dummy(), + ), ) } @@ -301,12 +303,17 @@ fn nullable_throwable_type() -> TypeExpr { /// Patches the checker metadata for the Throwable interface and all builtin exception classes. /// Updates return types for getter methods and the `__construct` parameter types for Error, TypeError, -/// ValueError, Exception, RuntimeException, JsonException, and FiberError. +/// ValueError, Exception, RuntimeException, ReflectionException, JsonException, and FiberError. pub(crate) fn patch_builtin_exception_signatures(checker: &mut Checker) { - let nullable_throwable = - checker.normalize_union_type(vec![PhpType::Object("Throwable".to_string()), PhpType::Void]); + let nullable_throwable = checker.normalize_union_type(vec![ + PhpType::Object("Throwable".to_string()), + PhpType::Void, + ]); if let Some(interface_info) = checker.interfaces.get_mut("Throwable") { - if let Some(sig) = interface_info.methods.get_mut(&php_symbol_key("getMessage")) { + if let Some(sig) = interface_info + .methods + .get_mut(&php_symbol_key("getMessage")) + { sig.return_type = PhpType::Str; } if let Some(sig) = interface_info.methods.get_mut(&php_symbol_key("getCode")) { @@ -327,10 +334,16 @@ pub(crate) fn patch_builtin_exception_signatures(checker: &mut Checker) { { sig.return_type = PhpType::Str; } - if let Some(sig) = interface_info.methods.get_mut(&php_symbol_key("getPrevious")) { + if let Some(sig) = interface_info + .methods + .get_mut(&php_symbol_key("getPrevious")) + { sig.return_type = nullable_throwable.clone(); } - if let Some(sig) = interface_info.methods.get_mut(&php_symbol_key("__toString")) { + if let Some(sig) = interface_info + .methods + .get_mut(&php_symbol_key("__toString")) + { sig.return_type = PhpType::Str; } } @@ -341,6 +354,7 @@ pub(crate) fn patch_builtin_exception_signatures(checker: &mut Checker) { "ArithmeticError", "Exception", "RuntimeException", + "ReflectionException", "JsonException", "FiberError", ] { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 6dc148cc41..196e2e96d1 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -944,6 +944,65 @@ fn builtin_reflection_class_implements_interface_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); let interface_var = Expr::new(ExprKind::Variable("interface".to_string()), dummy_span); let candidate_var = Expr::new(ExprKind::Variable("interfaceName".to_string()), dummy_span); + let missing_interface_check = Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(function_call( + "interface_exists", + vec![interface_var.clone()], + dummy_span, + ))), + dummy_span, + ), + then_body: vec![ + throw_if_class_like_exists( + "class_exists", + interface_var.clone(), + concat_expr( + interface_var.clone(), + string_lit(" is not an interface", dummy_span), + dummy_span, + ), + dummy_span, + ), + throw_if_class_like_exists( + "trait_exists", + interface_var.clone(), + concat_expr( + interface_var.clone(), + string_lit(" is not an interface", dummy_span), + dummy_span, + ), + dummy_span, + ), + throw_if_class_like_exists( + "enum_exists", + interface_var.clone(), + concat_expr( + interface_var.clone(), + string_lit(" is not an interface", dummy_span), + dummy_span, + ), + dummy_span, + ), + throw_new_reflection_exception( + concat_expr( + concat_expr( + string_lit("Interface \"", dummy_span), + interface_var.clone(), + dummy_span, + ), + string_lit("\" does not exist", dummy_span), + dummy_span, + ), + dummy_span, + ), + ], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ); let lowered_interface = strtolower_call(interface_var.clone(), dummy_span); let lowered_candidate = strtolower_call(candidate_var, dummy_span); let candidate_matches = Expr::new( @@ -985,6 +1044,7 @@ fn builtin_reflection_class_implements_interface_method() -> ClassMethod { variadic_type: None, return_type: Some(bool_type()), body: vec![ + missing_interface_check, Stmt::new( StmtKind::Foreach { array: reflection_this_property("__interface_names", dummy_span), @@ -1019,6 +1079,71 @@ fn builtin_reflection_class_implements_interface_method() -> ClassMethod { } } +/// Builds `if (($interface)) throw new ReflectionException($message);`. +fn throw_if_class_like_exists( + predicate_name: &str, + interface_var: Expr, + message: Expr, + span: crate::span::Span, +) -> Stmt { + Stmt::new( + StmtKind::If { + condition: function_call(predicate_name, vec![interface_var], span), + then_body: vec![throw_new_reflection_exception(message, span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds a normal function call expression for synthetic Reflection method bodies. +fn function_call(name: &str, args: Vec, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified(name), + args, + }, + span, + ) +} + +/// Builds a binary expression with the given operator and operands. +fn binary_expr(left: Expr, op: BinOp, right: Expr, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::BinaryOp { + left: Box::new(left), + op, + right: Box::new(right), + }, + span, + ) +} + +/// Builds a PHP string literal expression for synthetic method bodies. +fn string_lit(value: &str, span: crate::span::Span) -> Expr { + Expr::new(ExprKind::StringLiteral(value.to_string()), span) +} + +/// Builds a PHP string concatenation expression. +fn concat_expr(left: Expr, right: Expr, span: crate::span::Span) -> Expr { + binary_expr(left, BinOp::Concat, right, span) +} + +/// Builds `throw new ReflectionException($message)`. +fn throw_new_reflection_exception(message: Expr, span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::Throw(Expr::new( + ExprKind::NewObject { + class_name: Name::unqualified("ReflectionException"), + args: vec![message], + }, + span, + )), + span, + ) +} + /// Builds `$this->{$property}` for synthetic ReflectionClass method bodies. fn reflection_this_property(property: &str, span: crate::span::Span) -> Expr { Expr::new( @@ -1032,13 +1157,7 @@ fn reflection_this_property(property: &str, span: crate::span::Span) -> Expr { /// Builds a `strtolower()` call around an expression for case-insensitive class names. fn strtolower_call(expr: Expr, span: crate::span::Span) -> Expr { - Expr::new( - ExprKind::FunctionCall { - name: Name::unqualified("strtolower"), - args: vec![expr], - }, - span, - ) + function_call("strtolower", vec![expr], span) } /// Returns a public `ReflectionClass` array method backed by one private slot. @@ -1443,17 +1562,22 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { PhpType::Array(Box::new(PhpType::Object("ReflectionMethod".to_string()))); } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getProperties")) { - sig.return_type = PhpType::Array(Box::new(PhpType::Object( - "ReflectionProperty".to_string(), - ))); + sig.return_type = + PhpType::Array(Box::new(PhpType::Object("ReflectionProperty".to_string()))); } - if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getConstructor")) { + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getConstructor")) + { sig.return_type = PhpType::Union(vec![ PhpType::Object("ReflectionMethod".to_string()), PhpType::Void, ]); } - if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParentClass")) { + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getParentClass")) + { sig.return_type = PhpType::Mixed; } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { @@ -1499,12 +1623,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPosition")) { sig.return_type = PhpType::Int; } - for method_name in [ - "isoptional", - "isvariadic", - "ispassedbyreference", - "hastype", - ] { + for method_name in ["isoptional", "isvariadic", "ispassedbyreference", "hastype"] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index af618404c6..7542093377 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5506,7 +5506,10 @@ echo $value . ":" . $named . ":" . $items["k"] . ":" . $prop->value;'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "A-method-static-variadic:B-named:C-method:D-method"); + assert_eq!( + out.stdout, + "A-method-static-variadic:B-named:C-method:D-method" + ); } /// Verifies eval dynamic static callables dispatch eval-declared static methods. @@ -5796,6 +5799,57 @@ echo (new ReflectionClass("EvalImplTrait"))->implementsInterface("EvalImplBase") assert_eq!(out.stdout, "CBEIPt"); } +/// Verifies eval `ReflectionClass::implementsInterface()` throws ReflectionException +/// for missing or non-interface argument names. +#[test] +fn test_eval_reflection_class_implements_interface_rejects_non_interfaces() { + let out = compile_and_run_capture( + r#"implementsInterface("EvalImplRejectOther") ? "T" : "F"; +try { + $ref->implementsInterface("EvalImplRejectClass"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectTrait"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectEnum"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectMissing"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "F:ReflectionException:EvalImplRejectClass is not an interface:ReflectionException:EvalImplRejectTrait is not an interface:ReflectionException:EvalImplRejectEnum is not an interface:ReflectionException:Interface \"EvalImplRejectMissing\" does not exist" + ); +} + /// Verifies eval ReflectionClass::getParentClass crosses the generated runtime bridge. #[test] fn test_eval_reflection_class_get_parent_class() { diff --git a/tests/codegen/exceptions.rs b/tests/codegen/exceptions.rs index 58e17e9f99..12e23d6242 100644 --- a/tests/codegen/exceptions.rs +++ b/tests/codegen/exceptions.rs @@ -62,6 +62,22 @@ fn test_builtin_exception_message_api() { assert_eq!(out, "boom:boom"); } +/// Checks that Exception messages built from temporary string results survive the throw. +#[test] +fn test_builtin_exception_message_persists_concatenated_temporary() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!(out, "dynamic boom"); +} + /// Verifies builtin throwable catches exception. #[test] fn test_builtin_throwable_catches_exception() { @@ -75,8 +91,7 @@ fn test_builtin_throwable_catches_exception() { #[test] fn test_builtin_throwable_catches_error() { // Throwable (the root interface) catches a builtin Error. - let out = - compile_and_run("implementsInterface(StaticIm assert_eq!(out.stdout, "CBEIPt"); } +/// Verifies that `ReflectionClass::implementsInterface()` throws PHP-compatible +/// ReflectionException objects for missing or non-interface argument names. +#[test] +fn test_reflection_class_implements_interface_rejects_non_interfaces() { + let out = compile_and_run_capture( + r#"implementsInterface(StaticImplRejectOther::class) ? "T" : "F"; +try { + $ref->implementsInterface("StaticImplRejectClass"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("StaticImplRejectTrait"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("StaticImplRejectEnum"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +try { + $ref->implementsInterface("StaticImplRejectMissing"); + echo ":ok"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "F:ReflectionException:StaticImplRejectClass is not an interface:ReflectionException:StaticImplRejectTrait is not an interface:ReflectionException:StaticImplRejectEnum is not an interface:ReflectionException:Interface \"StaticImplRejectMissing\" does not exist" + ); +} + /// Verifies that `ReflectionClass::getParentClass()` returns a ReflectionClass /// object for subclasses and `false` for parentless classes. #[test] From a597dccfdb2272e16f5a770b978dbed22610e6dc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 06:06:49 +0200 Subject: [PATCH 0389/1208] Support static interface methods --- crates/elephc-eval/src/eval_ir.rs | 13 ++ .../elephc-eval/src/interpreter/reflection.rs | 2 +- .../elephc-eval/src/interpreter/statements.rs | 4 +- .../src/interpreter/tests/expressions.rs | 44 +++++ crates/elephc-eval/src/parser/statements.rs | 38 +++- docs/php/classes.md | 3 +- docs/php/eval.md | 2 +- src/codegen/lower_inst/objects/reflection.rs | 9 +- .../objects/constructors/reflection.rs | 4 +- .../checker/schema/classes/interfaces.rs | 177 ++++++++++++++---- src/types/checker/schema/classes/methods.rs | 25 ++- src/types/checker/schema/interfaces.rs | 70 ++++++- src/types/schema.rs | 15 +- tests/codegen/eval.rs | 45 +++++ tests/codegen/oop/interfaces.rs | 69 +++++++ tests/error_tests/misc/classes.rs | 38 ++-- 16 files changed, 469 insertions(+), 89 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index cdc7564be9..a600a2b09d 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -568,6 +568,7 @@ impl EvalInterfaceProperty { pub struct EvalInterfaceMethod { name: String, attributes: Vec, + is_static: bool, params: Vec, parameter_has_types: Vec, parameter_types: Vec>, @@ -587,6 +588,7 @@ impl EvalInterfaceMethod { Self { name: name.into(), attributes: Vec::new(), + is_static: false, params, parameter_has_types, parameter_types, @@ -596,6 +598,12 @@ impl EvalInterfaceMethod { } } + /// Returns a copy of this interface method with its static modifier flag set. + pub fn with_static(mut self, is_static: bool) -> Self { + self.is_static = is_static; + self + } + /// Returns a copy of this interface method with declaration attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; @@ -643,6 +651,11 @@ impl EvalInterfaceMethod { &self.attributes } + /// Returns whether this interface method was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + /// Returns source-order parameter names without leading `$`. pub fn params(&self) -> &[String] { &self.params diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index d733088736..77d9fcadbd 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -820,7 +820,7 @@ fn eval_reflection_method_metadata( .map(|method| EvalReflectionMemberMetadata { attributes: method.attributes().to_vec(), visibility: EvalVisibility::Public, - is_static: false, + is_static: method.is_static(), is_final: false, is_abstract: true, required_parameter_count: eval_reflection_required_parameter_count( diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 4a642aa2eb..888b93eddb 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1324,7 +1324,7 @@ fn class_has_interface_method( ) -> bool { if let Some(method) = class.method(requirement.name()) { return method.visibility() == EvalVisibility::Public - && !method.is_static() + && method.is_static() == requirement.is_static() && !method.is_abstract() && class_method_satisfies_interface_signature(method, requirement); } @@ -1333,7 +1333,7 @@ fn class_has_interface_method( .and_then(|parent| context.class_method(parent, requirement.name())) .is_some_and(|(_, method)| { method.visibility() == EvalVisibility::Public - && !method.is_static() + && method.is_static() == requirement.is_static() && !method.is_abstract() && class_method_satisfies_interface_signature(&method, requirement) }) diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 07db502777..d0459fff83 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -836,6 +836,50 @@ class EvalMissingRead implements EvalNeedsRead {}"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval static interface method contracts are satisfied by public static methods. +#[test] +fn execute_program_accepts_static_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsStaticRead { + public static function read($n); +} +class EvalStaticReader implements EvalNeedsStaticRead { + public static function read($n) { + return $n . "!"; + } +} +return EvalStaticReader::read("ok");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok!".to_string())); +} + +/// Verifies eval rejects instance methods for static interface method contracts. +#[test] +fn execute_program_rejects_instance_method_for_static_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsStaticRead { + public static function read(); +} +class EvalInstanceReader implements EvalNeedsStaticRead { + public function read() {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("instance method should not satisfy static interface method"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval interface method contracts require matching by-reference parameters. #[test] fn execute_program_validates_interface_method_by_ref_parameters() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 695cf31a88..597efae55e 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -1210,13 +1210,34 @@ impl Parser { methods: &mut Vec, ) -> Result<(), EvalParseError> { let attributes = self.parse_optional_member_attributes()?; - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "public")) { - self.advance(); - } else if matches!(self.current(), TokenKind::Ident(name) if is_unsupported_class_member_modifier(name)) - { - return Err(EvalParseError::UnsupportedConstruct); + let mut is_static = false; + let mut saw_public = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + if saw_public { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_public = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "static") => { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + is_static = true; + self.advance(); + } + TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { + return Err(EvalParseError::UnsupportedConstruct); + } + _ => break, + } } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } constants.push( self.parse_class_const_decl(EvalVisibility::Public)? .with_attributes(attributes), @@ -1225,11 +1246,14 @@ impl Parser { } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { methods.push( - self.parse_interface_method_decl_after_function_keyword()? + self.parse_interface_method_decl_after_function_keyword(is_static)? .with_attributes(attributes), ); return Ok(()); } + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } properties.push( self.parse_interface_property_decl()? .with_attributes(attributes), @@ -1240,6 +1264,7 @@ impl Parser { /// Parses one eval interface method signature after `function` has been selected. pub(super) fn parse_interface_method_decl_after_function_keyword( &mut self, + is_static: bool, ) -> Result { self.advance(); let TokenKind::Ident(name) = self.current() else { @@ -1258,6 +1283,7 @@ impl Parser { self.parse_method_params()?; self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) + .with_static(is_static) .with_parameter_types(parameter_types) .with_parameter_defaults(parameter_defaults) .with_parameter_by_ref_flags(parameter_is_by_ref) diff --git a/docs/php/classes.md b/docs/php/classes.md index 7e42dac7e6..fa9ce7f24f 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -44,8 +44,9 @@ class Product implements Named { public function label() { return strtoupper($this->name()); } } ``` -- signature-only methods and PHP 8.4 property hook contracts; method and hook bodies are not allowed in interfaces +- signature-only instance/static methods and PHP 8.4 property hook contracts; method and hook bodies are not allowed in interfaces - interface inheritance flattened transitively with cycle detection +- static interface methods must be implemented by public static methods; instance and static methods cannot satisfy each other's contracts Interfaces may also declare `static` methods (PHP 8.3+). A concrete implementing class must provide a compatible public static method (an instance method does diff --git a/docs/php/eval.md b/docs/php/eval.md index 36a509ddcb..d38e4c1412 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -137,7 +137,7 @@ contract checks, abstract property hook contracts, property-level `readonly`, `readonly class`, `__construct()`, abstract classes and methods, final classes and methods, trait composition with `insteadof` conflict resolution and `as` aliases/visibility adaptations, interface implementation checks, static -properties, static methods, class constants, interface constants, trait +properties, static methods, static interface method contracts, class constants, interface constants, trait constants, class-level attributes, and `ClassName::class` literals. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 8268272445..3a03a94dac 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -1277,12 +1277,16 @@ fn reflection_interface_method_member( method_name: &str, ) -> Option { let method_key = php_symbol_key(method_name); - let sig = info.methods.get(&method_key)?; + let (sig, is_static) = info + .methods + .get(&method_key) + .map(|sig| (sig, false)) + .or_else(|| info.static_methods.get(&method_key).map(|sig| (sig, true)))?; Some(ReflectionListedMember { name: method_key, attr_names: Vec::new(), attr_args: Vec::new(), - flags: reflection_member_flags(false, &Visibility::Public, false, true), + flags: reflection_member_flags(is_static, &Visibility::Public, false, true), required_parameter_count: reflection_required_parameter_count(sig), parameters: reflection_parameter_members(sig), }) @@ -1492,6 +1496,7 @@ fn reflection_interface_method_names( let mut names = Vec::new(); let mut seen = std::collections::HashSet::new(); push_unique_method_names(info.methods.keys(), &mut names, &mut seen); + push_unique_method_names(info.static_methods.keys(), &mut names, &mut seen); names } diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index caa91d76ab..745493d717 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -88,7 +88,9 @@ impl Checker { ); } if let Some(interface_info) = self.interfaces.get(class_name) { - if interface_info.methods.contains_key(&method_key) { + if interface_info.methods.contains_key(&method_key) + || interface_info.static_methods.contains_key(&method_key) + { return Ok(()); } return Err(CompileError::new( diff --git a/src/types/checker/schema/classes/interfaces.rs b/src/types/checker/schema/classes/interfaces.rs index dcd5d55af3..e4affaebbc 100644 --- a/src/types/checker/schema/classes/interfaces.rs +++ b/src/types/checker/schema/classes/interfaces.rs @@ -15,7 +15,7 @@ use crate::names::php_symbol_key; use crate::parser::ast::Visibility; use crate::span::Span; use crate::types::traits::FlattenedClass; -use crate::types::{FunctionSig, PhpType, PropertyHookContract}; +use crate::types::{PhpType, PropertyHookContract}; use super::super::super::Checker; use super::super::validation::{ @@ -136,16 +136,15 @@ pub(super) fn validate_interface_contracts( )?; } for method_name in &interface_info.static_method_order { - let required_sig = interface_info - .static_methods - .get(method_name) - .expect("type checker bug: missing interface static method signature"); - validate_interface_static_method( + validate_static_interface_method( state, class, &interface_name, method_name, - required_sig, + class_map, + checker, + next_class_id, + building, )?; } for property_name in &interface_info.property_order { @@ -169,6 +168,139 @@ pub(super) fn validate_interface_contracts( Ok(()) } +/// Validates that `class` implements the static interface method `method_name`. +/// +/// Checks static/instance kind, signature compatibility, return type declarations, +/// public visibility, and object return metadata. Abstract classes may defer the +/// required static method into their own abstract method set. +#[allow(clippy::too_many_arguments)] +fn validate_static_interface_method( + state: &mut ClassBuildState, + class: &FlattenedClass, + interface_name: &str, + method_name: &str, + class_map: &HashMap, + checker: &mut Checker, + next_class_id: &mut u64, + building: &mut HashSet, +) -> Result<(), CompileError> { + if state.method_sigs.contains_key(method_name) { + return Err(CompileError::new( + crate::span::Span::dummy(), + &format!( + "Cannot use instance method to satisfy static interface contract: {}::{}", + class.name, method_name + ), + )); + } + let interface_info = checker + .interfaces + .get(interface_name) + .expect("type checker bug: interface exists") + .clone(); + let required_sig = interface_info + .static_methods + .get(method_name) + .expect("type checker bug: missing static interface method signature"); + let actual_sig = match state.static_sigs.get(method_name) { + Some(sig) => sig, + None if class.is_abstract => { + state + .static_sigs + .insert(method_name.to_string(), required_sig.clone()); + state + .static_method_visibilities + .insert(method_name.to_string(), Visibility::Public); + state + .static_method_declaring_classes + .insert(method_name.to_string(), class.name.clone()); + state.static_method_impl_classes.remove(method_name); + if !state.static_vtable_slots.contains_key(method_name) { + let slot = state.static_vtable_methods.len(); + state + .static_vtable_slots + .insert(method_name.to_string(), slot); + state.static_vtable_methods.push(method_name.to_string()); + } + return Ok(()); + } + None => { + return Err(CompileError::new( + crate::span::Span::dummy(), + &format!( + "Class {} must implement interface static method {}::{}", + class.name, interface_name, method_name + ), + )) + } + }; + validate_signature_compatibility( + crate::span::Span::dummy(), + &class.name, + method_name, + actual_sig, + required_sig, + "static method", + "implementing interface", + )?; + let actual_method = class + .methods + .iter() + .find(|m| m.is_static && php_symbol_key(&m.name) == method_name); + if required_sig.declared_return && !actual_sig.declared_return { + return Err(CompileError::new( + actual_method + .map(|m| m.span) + .unwrap_or_else(crate::span::Span::dummy), + &format!( + "Cannot implement interface static method {}::{} without declaring a compatible return type (interface returns {})", + class.name, method_name, required_sig.return_type + ), + )); + } + if let PhpType::Object(actual_name) = &actual_sig.return_type { + if actual_name != &class.name + && class_map.contains_key(actual_name) + && !checker.classes.contains_key(actual_name) + { + super::build_class_info_recursive( + actual_name, + class_map, + checker, + next_class_id, + building, + )?; + } + } + if required_sig.declared_return + && !declared_return_type_compatible( + checker, + &required_sig.return_type, + &actual_sig.return_type, + ) + { + return Err(CompileError::new( + actual_method + .map(|m| m.span) + .unwrap_or_else(crate::span::Span::dummy), + &format!( + "Cannot implement interface static method {}::{} with incompatible return type {} (interface returns {})", + class.name, method_name, actual_sig.return_type, required_sig.return_type + ), + )); + } + if state.static_method_visibilities.get(method_name) != Some(&Visibility::Public) { + return Err(CompileError::new( + crate::span::Span::dummy(), + &format!( + "Interface static method implementation must be public: {}::{}", + class.name, method_name + ), + )); + } + Ok(()) +} + /// Validates that a concrete (non-abstract) `class` has implementations for all deferred abstract /// methods and properties accumulated in `state`. /// @@ -234,37 +366,6 @@ pub(super) fn ensure_concrete_class_implements_abstracts( /// Checks signature compatibility, return type declarations, visibility (must be public), and /// that non-public static methods cannot satisfy interface contracts. For abstract classes, /// missing methods are inserted into the class state as deferred contracts. -/// Validates that `class` satisfies the static interface method `method_name` from -/// `interface_name` — a concrete class must declare a compatible `static` method; an abstract -/// class may defer. PHP 8.3+ static interface methods. -fn validate_interface_static_method( - state: &ClassBuildState, - class: &FlattenedClass, - interface_name: &str, - method_name: &str, - required_sig: &FunctionSig, -) -> Result<(), CompileError> { - match state.static_sigs.get(method_name) { - Some(actual_sig) => validate_signature_compatibility( - Span::dummy(), - &class.name, - method_name, - actual_sig, - required_sig, - "method", - "implementing interface", - ), - None if class.is_abstract => Ok(()), - None => Err(CompileError::new( - Span::dummy(), - &format!( - "Class {} must implement static interface method {}::{}", - class.name, interface_name, method_name - ), - )), - } -} - #[allow(clippy::too_many_arguments)] fn validate_interface_method( state: &mut ClassBuildState, diff --git a/src/types/checker/schema/classes/methods.rs b/src/types/checker/schema/classes/methods.rs index 0d14c4df8b..d152860095 100644 --- a/src/types/checker/schema/classes/methods.rs +++ b/src/types/checker/schema/classes/methods.rs @@ -134,7 +134,7 @@ fn apply_static_method( if let Some(parent_sig) = state.static_sigs.get(&method_key) { validate_override_signature(checker, &class.name, method, parent_sig, true)?; } else if has_override_attribute(method) - && !interface_declares_method(checker, state, class, &method_key) + && !interface_declares_method(checker, state, class, &method_key, true) { return Err(missing_override_target(class, method)); } @@ -231,7 +231,7 @@ fn apply_instance_method( if let Some(parent_sig) = state.method_sigs.get(&method_key) { validate_override_signature(checker, &class.name, method, parent_sig, false)?; } else if has_override_attribute(method) - && !interface_declares_method(checker, state, class, &method_key) + && !interface_declares_method(checker, state, class, &method_key, false) { return Err(missing_override_target(class, method)); } @@ -313,17 +313,19 @@ fn has_override_attribute(method: &ClassMethod) -> bool { } /// Returns `true` if any interface implemented by the class (directly or -/// transitively via parent interfaces or parent classes) declares the method — -/// instance OR PHP 8.3+ static (a `#[\Override]` on a static interface-method -/// implementation is valid). Seeds from `class.implements` because `apply_methods` -/// runs before `collect_interfaces` has added the class's own clause to -/// `state.interfaces`, plus `state.interfaces`, which at this point carries the -/// interfaces inherited from the parent class chain. +/// transitively via parent interfaces or inherited parent-class contracts) +/// declares the method with the requested static/instance kind. +/// +/// Seeds from `class.implements` because `apply_methods` runs before +/// `collect_interfaces` has added the class's own clause to `state.interfaces`; +/// also scans `state.interfaces`, which already carries interfaces inherited +/// from the parent class chain. fn interface_declares_method( checker: &Checker, state: &ClassBuildState, class: &FlattenedClass, method_key: &str, + is_static: bool, ) -> bool { let mut visited = std::collections::HashSet::new(); let mut queue: Vec = class.implements.clone(); @@ -335,7 +337,12 @@ fn interface_declares_method( let Some(info) = checker.interfaces.get(&name) else { continue; }; - if info.methods.contains_key(method_key) || info.static_methods.contains_key(method_key) { + let declares_method = if is_static { + info.static_methods.contains_key(method_key) + } else { + info.methods.contains_key(method_key) + }; + if declares_method { return true; } queue.extend(info.parents.iter().cloned()); diff --git a/src/types/checker/schema/interfaces.rs b/src/types/checker/schema/interfaces.rs index 7fa53e88ef..f86b3b2f6e 100644 --- a/src/types/checker/schema/interfaces.rs +++ b/src/types/checker/schema/interfaces.rs @@ -70,7 +70,8 @@ pub(crate) fn build_interface_info_recursive( let mut method_order = Vec::new(); let mut method_slots = HashMap::new(); let mut static_methods = HashMap::new(); - let mut static_method_order: Vec = Vec::new(); + let mut static_method_declaring_interfaces = HashMap::new(); + let mut static_method_order = Vec::new(); let mut properties = HashMap::new(); let mut property_order = Vec::new(); @@ -107,6 +108,13 @@ pub(crate) fn build_interface_info_recursive( .methods .get(method_name) .expect("type checker bug: missing interface parent method signature"); + if static_methods.contains_key(method_name) { + return Err(interface_method_kind_conflict( + interface.span, + &interface.name, + method_name, + )); + } if let Some(existing_sig) = methods.get(method_name) { validate_signature_compatibility( interface.span, @@ -134,7 +142,14 @@ pub(crate) fn build_interface_info_recursive( let parent_sig = parent_info .static_methods .get(method_name) - .expect("type checker bug: missing interface parent static method signature"); + .expect("type checker bug: missing parent static interface method signature"); + if methods.contains_key(method_name) { + return Err(interface_method_kind_conflict( + interface.span, + &interface.name, + method_name, + )); + } if let Some(existing_sig) = static_methods.get(method_name) { validate_signature_compatibility( interface.span, @@ -142,12 +157,18 @@ pub(crate) fn build_interface_info_recursive( method_name, existing_sig, parent_sig, - "method", + "static method", "combining interface parent", )?; continue; } static_methods.insert(method_name.clone(), parent_sig.clone()); + let declaring = parent_info + .static_method_declaring_interfaces + .get(method_name) + .cloned() + .unwrap_or_else(|| parent_name.clone()); + static_method_declaring_interfaces.insert(method_name.clone(), declaring); static_method_order.push(method_name.clone()); } for property_name in &parent_info.property_order { @@ -243,26 +264,39 @@ pub(crate) fn build_interface_info_recursive( let sig = build_method_sig(checker, method)?; if method.is_static { - // PHP 8.3+ static interface method: a bodyless static contract. Recorded apart - // from instance methods (no vtable slot — dispatch is by class). Implementing - // classes satisfy it with a static method (see validate_interface_contracts). - if let Some(existing) = static_methods.get(&method_key) { + if methods.contains_key(&method_key) { + return Err(interface_method_kind_conflict( + method.span, + &interface.name, + &method.name, + )); + } + if let Some(parent_sig) = static_methods.get(&method_key) { validate_signature_compatibility( method.span, &interface.name, &method.name, &sig, - existing, - "method", + parent_sig, + "static method", "redeclaring interface", )?; } static_methods.insert(method_key.clone(), sig); + static_method_declaring_interfaces + .insert(method_key.clone(), interface.name.clone()); if !static_method_order.contains(&method_key) { - static_method_order.push(method_key.clone()); + static_method_order.push(method_key); } continue; } + if static_methods.contains_key(&method_key) { + return Err(interface_method_kind_conflict( + method.span, + &interface.name, + &method.name, + )); + } if let Some(parent_sig) = methods.get(&method_key) { validate_signature_compatibility( method.span, @@ -308,6 +342,7 @@ pub(crate) fn build_interface_info_recursive( method_order, method_slots, static_methods, + static_method_declaring_interfaces, static_method_order, constants: iface_constants, }, @@ -317,6 +352,21 @@ pub(crate) fn build_interface_info_recursive( Ok(()) } +/// Constructs a diagnostic for conflicting static/non-static interface methods. +fn interface_method_kind_conflict( + span: crate::span::Span, + interface_name: &str, + method_name: &str, +) -> CompileError { + CompileError::new( + span, + &format!( + "Cannot combine static and non-static interface method: {}::{}", + interface_name, method_name + ), + ) +} + /// Validates a single interface property declaration for syntactic correctness. /// Checks that the property is public, non-static, non-readonly, and has at least one hook. /// diff --git a/src/types/schema.rs b/src/types/schema.rs index ec4e52d9ed..3b262bfbf8 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -89,21 +89,28 @@ impl PartialEq for PropertyHookContract { } /// Interface metadata for resolved declarations. Tracks parents, properties, -/// methods, constants, and vtable layout after name resolution and inheritance flattening. +/// instance/static methods, constants, and instance vtable layout after name +/// resolution and inheritance flattening. #[derive(Debug, Clone)] pub struct InterfaceInfo { pub interface_id: u64, pub parents: Vec, pub properties: HashMap, pub property_order: Vec, + /// Instance method contracts, keyed by PHP's case-insensitive method key. + /// + /// These entries are the only methods that participate in interface + /// dispatch tables and `method_slots`. pub methods: HashMap, pub method_declaring_interfaces: HashMap, pub method_order: Vec, pub method_slots: HashMap, - /// Static interface methods (PHP 8.3+), keyed by method key. Recorded separately from - /// instance `methods` — static dispatch is by class, so they take no vtable slot. + /// Static method contracts, keyed by PHP's case-insensitive method key. + /// + /// PHP requires implementors to provide matching public static methods, but + /// these entries never participate in instance interface dispatch tables. pub static_methods: HashMap, - /// Declaration order of `static_methods`, for deterministic conformance checking. + pub static_method_declaring_interfaces: HashMap, pub static_method_order: Vec, /// Interface constants (PHP 5.0+). Inherited from parent interfaces. pub constants: HashMap, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7542093377..a93a42cab0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5333,6 +5333,51 @@ echo EvalStaticBase::baseRead();'); assert_eq!(out.stdout, "1:3:3:14:5:5"); } +/// Verifies eval-declared static interface methods are validated and reflected. +#[test] +fn test_eval_declared_static_interface_methods() { + let out = compile_and_run_capture( + r#"getMethods()[0]; +echo $listed->getName() . ":"; +echo $listed->isStatic() ? "static" : "instance"; +echo ":"; +$method = new ReflectionMethod(EvalStaticContract::class, "make"); +echo $method->isStatic() ? "S" : "s";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "S:box:make:static:S"); + + let err = compile_and_run_expect_failure( + r#"name() . ":" . $item->tag(); assert_eq!(out, "box:BX"); } +/// Verifies a class can satisfy a static interface method contract. +/// +/// Fixture: interface `StaticMaker` declares `public static make(...)`; +/// `StaticWidget` implements it. The test also checks ReflectionClass and +/// ReflectionMethod expose the method as static. +#[test] +fn test_static_interface_method_contract_is_supported() { + let out = compile_and_run( + r#"hasMethod("make") ? "H" : "h"; +echo ":"; +$listed = $interface->getMethods()[0]; +echo $listed->getName(); +echo ":"; +echo $listed->isStatic() ? "S" : "s"; +echo ":"; +echo $listed->getNumberOfParameters(); +echo ":"; +$method = new ReflectionMethod(StaticMaker::class, "make"); +echo $method->isStatic() ? "S" : "s"; +echo ":"; +echo $method->getName(); +echo ":"; +echo (new ReflectionClass(StaticWidget::class))->implementsInterface(StaticMaker::class) ? "Y" : "N"; +"#, + ); + assert_eq!(out, "W:box:H:make:S:1:S:make:Y"); +} + +/// Verifies an abstract class may defer a static interface method to a concrete child. +/// +/// Fixture: `AbstractStaticLabel` implements `StaticLabel` but leaves the +/// static contract abstract; `ConcreteStaticLabel` provides it and is callable. +#[test] +fn test_abstract_class_can_defer_static_interface_method_to_child() { + let out = compile_and_run( + r#"name())`. /// Asserts the method call correctly resolves through the transitive interface hierarchy. diff --git a/tests/error_tests/misc/classes.rs b/tests/error_tests/misc/classes.rs index 0c4a92468a..2bbc2ed493 100644 --- a/tests/error_tests/misc/classes.rs +++ b/tests/error_tests/misc/classes.rs @@ -435,33 +435,43 @@ fn test_error_missing_interface_method() { ); } -/// Verifies the error diagnostic for a missing static interface method (PHP 8.3+). +/// Verifies the error diagnostic for a missing static interface method. #[test] fn test_error_missing_static_interface_method() { - // a concrete class implementing an interface must provide all its static methods. + // a concrete class must implement static methods required by interfaces. expect_error( - " Date: Fri, 19 Jun 2026 06:18:45 +0200 Subject: [PATCH 0390/1208] Support named ReflectionClass newInstance args --- docs/php/classes.md | 13 ++-- src/ir_lower/expr/mod.rs | 113 ++++++++++++++++++++++++++++++-- tests/codegen/oop/attributes.rs | 46 +++++++++++++ 3 files changed, 161 insertions(+), 11 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index fa9ce7f24f..c0fd0be109 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -323,18 +323,21 @@ property. ```php value; + } } class Child extends Base { public int $value = 5; } -echo (new Child())->value; // 5 +$child = new Child(); +echo $child->value . ":" . $child->parentValue(); // 5:1 ``` -Private parent properties are still considered separate slots in PHP, but elephc rejects same-named redeclarations through them; declare a different name in the child for now. - ### Property hooks (`get` / `set`) A property can define **hooks** that run when it is read or written, replacing hand-written getter and setter methods. The hooks live in a `{ ... }` block after the property name. Reading the property runs its `get` hook; assigning to it runs its `set` hook. @@ -1203,7 +1206,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. Dynamic unpacking and named-argument forwarding through the static pipeline are still outside this slice. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 25fb127259..42dd574b5c 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9059,8 +9059,7 @@ fn property_get_result_type( property_ty }; } - let Some((_, (_, property_ty))) = class_info.visible_property(property) - else { + let Some((_, (_, property_ty))) = class_info.visible_property(property) else { if let Some(magic_ty) = magic_get_result_type(ctx, normalized) { return if nullable { nullable_result_type(magic_ty) @@ -9368,7 +9367,7 @@ fn lower_method_call( return lower_nullable_regular_method_call(ctx, object, method, args, expr); } if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { - return lower_reflection_class_new_instance(ctx, object, args, expr); + return lower_reflection_class_new_instance(ctx, Some(object_expr), object, args, expr); } if matches!( ctx.builder.value_php_type(object.value).codegen_repr(), @@ -9565,17 +9564,26 @@ fn lower_nullable_regular_method_call( /// Lowers `ReflectionClass::newInstance()` by constructing the reflected class name. fn lower_reflection_class_new_instance( ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, object: LoweredValue, args: &[Expr], expr: &Expr, ) -> LoweredValue { let args = reflection_class_new_instance_args(args); - if args.iter().any(is_spread_arg) || crate::types::call_args::has_named_args(&args) { + let constructor_sig = + reflection_class_new_instance_constructor_signature(ctx, object_expr, &args).cloned(); + if args.iter().any(is_spread_arg) + || (crate::types::call_args::has_named_args(&args) && constructor_sig.is_none()) + { return lower_reflection_class_new_instance_unsupported(ctx, expr); } let class_name = lower_property_get_from_value(ctx, object, "__name", Op::PropGet, expr); let mut operands = vec![class_name.value]; - operands.extend(lower_args(ctx, &args)); + operands.extend(lower_args_with_signature( + ctx, + constructor_sig.as_ref(), + &args, + )); ctx.emit_value( Op::DynamicObjectNewMixed, operands, @@ -9594,6 +9602,99 @@ fn reflection_class_new_instance_args(args: &[Expr]) -> Vec { args.to_vec() } +/// Returns the reflected constructor signature when the ReflectionClass receiver +/// is an inline `new ReflectionClass(Known::class)` expression. +fn reflection_class_new_instance_constructor_signature<'a>( + ctx: &'a LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + forwarded_args: &[Expr], +) -> Option<&'a FunctionSig> { + let class_name = reflection_class_new_instance_reflected_class(ctx, object_expr?)?; + if forwarded_args.is_empty() && constructor_signature_for_class_name(ctx, &class_name).is_none() + { + return None; + } + constructor_signature_for_class_name(ctx, &class_name) +} + +/// Resolves the target class from an inline `ReflectionClass` construction when +/// its constructor argument is a literal class string or `ClassName::class`. +fn reflection_class_new_instance_reflected_class( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option { + let ExprKind::NewObject { class_name, args } = &object_expr.kind else { + return None; + }; + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionclass" { + return None; + } + let reflected_arg = reflection_class_constructor_class_arg(ctx, args)?; + let raw_class_name = match &reflected_arg.kind { + ExprKind::StringLiteral(value) => value.clone(), + ExprKind::ClassConstant { receiver } => static_receiver_class_name(ctx, receiver)?, + _ => return None, + }; + resolve_known_class_name(ctx, &raw_class_name) +} + +/// Returns the `ReflectionClass::__construct()` class-name argument after static +/// spread and named-argument normalization. +fn reflection_class_constructor_class_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return args.first().cloned(); + } + let sig = ctx + .classes + .get("ReflectionClass") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + planned_regular_arg_expr(plan.regular_args.first()?).cloned() +} + +/// Resolves a PHP class name case-insensitively against known class metadata. +fn resolve_known_class_name(ctx: &LoweringContext<'_, '_>, class_name: &str) -> Option { + let key = php_symbol_key(class_name.trim_start_matches('\\')); + ctx.classes + .keys() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == key) + .cloned() +} + +/// Returns constructor signature metadata for a known class name. +fn constructor_signature_for_class_name<'a>( + ctx: &'a LoweringContext<'_, '_>, + class_name: &str, +) -> Option<&'a FunctionSig> { + let key = php_symbol_key("__construct"); + ctx.classes + .get(class_name.trim_start_matches('\\')) + .and_then(|class_info| class_info.methods.get(&key)) +} + /// Emits a runtime fatal for ReflectionClass newInstance argument forms not yet lowered. fn lower_reflection_class_new_instance_unsupported( ctx: &mut LoweringContext<'_, '_>, @@ -9723,7 +9824,7 @@ fn lower_method_call_with_receiver( expr: &Expr, ) -> LoweredValue { if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { - return lower_reflection_class_new_instance(ctx, object, args, expr); + return lower_reflection_class_new_instance(ctx, None, object, args, expr); } let magic_args; let (dispatch_method, args) = diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index dd85066474..bdab94c8ba 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1917,6 +1917,52 @@ echo $second->label(); assert_eq!(out, "AB:CD"); } +/// Verifies inline `ReflectionClass::newInstance()` forwards named constructor +/// arguments through the reflected constructor signature. +#[test] +fn test_reflection_class_new_instance_forwards_named_constructor_args() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} +$first = (new ReflectionClass(ReflectNamedNewTarget::class))->newInstance(right: "B", left: "A"); +echo $first->label() . ":"; +$second = (new ReflectionClass(class_name: "reflectnamednewtarget"))->newInstance(...["right" => "D", "left" => "C"]); +echo $second->label(); +"#, + ); + assert_eq!(out, "AB:CD"); +} + +/// Verifies inline `ReflectionClass::newInstance()` uses constructor defaults +/// when named arguments leave optional parameters unspecified. +#[test] +fn test_reflection_class_new_instance_named_args_use_constructor_defaults() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} +$value = (new ReflectionClass(ReflectDefaultNewTarget::class))->newInstance(right: "B"); +echo $value->label(); +"#, + ); + assert_eq!(out, "LB"); +} + /// Verifies that `ReflectionClassConstant` and enum-case reflectors expose /// attribute name, arguments, `getName()`, and `newInstance()` data. #[test] From 755f102ce006a89aa0ac4fafee71d178de7f7b21 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 06:32:51 +0200 Subject: [PATCH 0391/1208] Support direct ReflectionParameter construction --- docs/php/classes.md | 18 +- src/codegen/lower_inst/objects/reflection.rs | 208 +++++++++++++++--- src/ir_lower/expr/mod.rs | 85 ++++++- src/types/checker/builtin_types/reflection.rs | 4 + .../checker/inference/objects/constructors.rs | 156 ++++++++++++- tests/codegen/oop/attributes.rs | 42 ++++ tests/error_tests/classes_traits.rs | 46 ++-- 7 files changed, 482 insertions(+), 77 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index c0fd0be109..34f4da2ed6 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1136,12 +1136,12 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | -| `ReflectionParameter::getName()` | Internal only | Return the parameter name without `$` | -| `ReflectionParameter::getPosition()` | Internal only | Return the zero-based parameter position | -| `ReflectionParameter::isOptional()` | Internal only | Return whether the parameter has a default value or is variadic | -| `ReflectionParameter::isVariadic()` | Internal only | Return whether the parameter is variadic | -| `ReflectionParameter::isPassedByReference()` | Internal only | Return whether the parameter is passed by reference | -| `ReflectionParameter::hasType()` | Internal only | Return whether the parameter has a declared type | +| `ReflectionParameter::getName()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | +| `ReflectionParameter::getPosition()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | +| `ReflectionParameter::isOptional()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | +| `ReflectionParameter::isVariadic()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | +| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | +| `ReflectionParameter::hasType()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | @@ -1201,13 +1201,13 @@ name with `isBuiltin()` false. | `ReflectionNamedType::allowsNull()` | `bool` | True when the declared type is nullable | Limitations today: -- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Method/Property(...)` must be compile-time class/member strings. `ClassName::class` is accepted for the class-name argument of `new ReflectionClass/Method/Property(...)`, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, or attribute names require a runtime name→id lookup table that is not yet implemented. +- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Method/Property/Parameter(...)` must be compile-time class/member strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, parameter, or attribute names require a runtime name→id lookup table that is not yet implemented. - Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** — a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` → `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. -- `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), and per-parameter attribute reflection are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known class/interface method arrays (`[ClassName::class, "method"]`) and `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, and function/object/trait-target direct `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 3a03a94dac..db60f3a9ef 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -11,9 +11,8 @@ //! constructors are compile-time metadata lookups that populate private //! `__name`/`__attrs` slots instead of running their public empty bodies. -use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::codegen::platform::Arch; -use crate::codegen::{CodegenIrError, Result}; +use crate::codegen::{abi, emit_box_current_value_as_mixed, CodegenIrError, Result}; use crate::ir::{Immediate, Instruction, Op, TraitMethodInfo, ValueDef, ValueId}; use crate::names::php_symbol_key; use crate::parser::ast::Visibility; @@ -69,6 +68,12 @@ struct ReflectionParameterMember { has_type: bool, } +/// Compile-time parameter selector from `ReflectionParameter::__construct()`. +enum ReflectionParameterSelector { + Name(String), + Position(i64), +} + /// Boolean metadata exposed by ReflectionMethod and ReflectionProperty predicates. #[derive(Clone, Copy, Default)] struct ReflectionMemberFlags { @@ -87,6 +92,7 @@ pub(super) fn is_reflection_owner_class(class_name: &str) -> bool { "ReflectionClass" | "ReflectionMethod" | "ReflectionProperty" + | "ReflectionParameter" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" @@ -203,6 +209,11 @@ fn emit_reflection_owner_object( metadata.required_parameter_count, )?; } + if class_name == "ReflectionParameter" { + if let Some(parameter) = metadata.parameter_members.first() { + emit_reflection_parameter_properties(ctx, parameter)?; + } + } emit_reflection_member_flag_properties(ctx, class_name, metadata.member_flags)?; Ok(()) } @@ -604,6 +615,7 @@ fn reflection_owner_metadata( "ReflectionClass" => reflection_class_metadata(ctx, inst), "ReflectionMethod" => reflection_method_metadata(ctx, inst), "ReflectionProperty" => reflection_property_metadata(ctx, inst), + "ReflectionParameter" => reflection_parameter_metadata(ctx, inst), "ReflectionClassConstant" => reflection_class_constant_metadata(ctx, inst), "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" => { reflection_enum_case_metadata(ctx, class_name, inst) @@ -860,6 +872,85 @@ fn reflection_property_metadata( .unwrap_or_else(empty_reflection_metadata)) } +/// Resolves `ReflectionParameter([class, method], parameter)` metadata. +fn reflection_parameter_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(class_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(method_operand) = inst.operands.get(1).copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(parameter_operand) = inst.operands.get(2).copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_class = const_string_or_class_operand(ctx, class_operand, "ReflectionParameter")?; + let method_name = const_required_string_operand(ctx, method_operand, "ReflectionParameter")?; + let selector = const_parameter_selector_operand(ctx, parameter_operand)?; + let method_key = php_symbol_key(&method_name); + let method = reflection_method_member_for_class_like(ctx, &reflected_class, &method_key); + let Some(parameter) = method + .as_ref() + .and_then(|method| reflection_parameter_member_for_selector(&method.parameters, selector)) + else { + return Ok(empty_reflection_metadata()); + }; + Ok(reflection_parameter_owner_metadata(parameter)) +} + +/// Builds direct ReflectionParameter constructor metadata from one parameter member. +fn reflection_parameter_owner_metadata( + parameter: ReflectionParameterMember, +) -> ReflectionOwnerMetadata { + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(parameter.name.clone()); + metadata.parameter_members.push(parameter); + metadata +} + +/// Resolves a reflected method member on a class, interface, or trait. +fn reflection_method_member_for_class_like( + ctx: &FunctionContext<'_>, + reflected_class: &str, + method_key: &str, +) -> Option { + if let Some((_, info)) = resolve_reflection_class(ctx, reflected_class) { + return reflection_class_method_member(info, method_key); + } + if let Some(interface_name) = resolve_reflection_interface(ctx, reflected_class) { + return ctx + .module + .interface_infos + .get(interface_name) + .and_then(|info| reflection_interface_method_member(info, method_key)); + } + resolve_reflection_trait(ctx, reflected_class).and_then(|trait_name| { + ctx.module + .declared_trait_methods + .get(trait_name) + .and_then(|methods| reflection_trait_method_member(methods, method_key)) + }) +} + +/// Returns the selected parameter member by PHP name or zero-based position. +fn reflection_parameter_member_for_selector( + parameters: &[ReflectionParameterMember], + selector: ReflectionParameterSelector, +) -> Option { + match selector { + ReflectionParameterSelector::Name(name) => parameters + .iter() + .find(|parameter| parameter.name == name) + .cloned(), + ReflectionParameterSelector::Position(position) if position >= 0 => { + parameters.get(position as usize).cloned() + } + ReflectionParameterSelector::Position(_) => None, + } +} + /// Resolves `ReflectionClassConstant(class, constant)` metadata. fn reflection_class_constant_metadata( ctx: &FunctionContext<'_>, @@ -1334,7 +1425,9 @@ fn reflection_class_property_members( ) -> Vec { property_names .iter() - .filter_map(|property_name| reflection_class_property_member(ctx, class_name, info, property_name)) + .filter_map(|property_name| { + reflection_class_property_member(ctx, class_name, info, property_name) + }) .collect() } @@ -1380,12 +1473,7 @@ fn default_method_members( name: name.clone(), attr_names: Vec::new(), attr_args: Vec::new(), - flags: reflection_member_flags( - false, - &Visibility::Public, - false, - is_interface, - ), + flags: reflection_member_flags(false, &Visibility::Public, false, is_interface), required_parameter_count: 0, parameters: Vec::new(), }) @@ -1403,12 +1491,7 @@ fn default_property_members( name: name.clone(), attr_names: Vec::new(), attr_args: Vec::new(), - flags: reflection_member_flags( - false, - &Visibility::Public, - false, - is_interface, - ), + flags: reflection_member_flags(false, &Visibility::Public, false, is_interface), required_parameter_count: 0, parameters: Vec::new(), }) @@ -1636,6 +1719,51 @@ fn const_required_string_operand( const_data_operand(ctx, value, owner, false) } +/// Extracts a constant ReflectionParameter name or offset selector from EIR. +fn const_parameter_selector_operand( + ctx: &FunctionContext<'_>, + value: ValueId, +) -> Result { + let value_ref = ctx + .function + .value(value) + .ok_or_else(|| CodegenIrError::missing_entry("value", value.as_raw()))?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return Err(CodegenIrError::unsupported( + "ReflectionParameter constructor with non-literal parameter selector", + )); + }; + let inst_ref = ctx + .function + .instruction(inst) + .ok_or_else(|| CodegenIrError::missing_entry("instruction", inst.as_raw()))?; + match inst_ref.op { + Op::ConstI64 => match inst_ref.immediate { + Some(Immediate::I64(value)) => Ok(ReflectionParameterSelector::Position(value)), + _ => Err(CodegenIrError::invalid_module( + "ReflectionParameter position selector missing i64 immediate", + )), + }, + Op::ConstStr => { + let Some(Immediate::Data(data)) = inst_ref.immediate else { + return Err(CodegenIrError::invalid_module( + "ReflectionParameter name selector missing data id", + )); + }; + ctx.module + .data + .strings + .get(data.as_raw() as usize) + .cloned() + .map(ReflectionParameterSelector::Name) + .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw())) + } + _ => Err(CodegenIrError::unsupported( + "ReflectionParameter constructor with non-literal parameter selector", + )), + } +} + /// Reads a `ConstStr` or optional `ConstClassName` value from the module data pool. fn const_data_operand( ctx: &FunctionContext<'_>, @@ -1985,8 +2113,11 @@ fn emit_reflection_member_object( member: &ReflectionListedMember, ) -> Result<()> { let (class_id, property_count, uninitialized_marker_offsets) = { - let class_info = - ctx.module.class_infos.get(member_class_name).ok_or_else(|| { + let class_info = ctx + .module + .class_infos + .get(member_class_name) + .ok_or_else(|| { CodegenIrError::unsupported(format!("unknown class {}", member_class_name)) })?; ( @@ -2039,10 +2170,11 @@ fn emit_reflection_parameter_object( parameter: &ReflectionParameterMember, ) -> Result<()> { let (class_id, property_count, uninitialized_marker_offsets) = { - let class_info = - ctx.module.class_infos.get("ReflectionParameter").ok_or_else(|| { - CodegenIrError::unsupported("unknown class ReflectionParameter") - })?; + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionParameter"))?; ( class_info.class_id, class_info.properties.len(), @@ -2056,6 +2188,14 @@ fn emit_reflection_parameter_object( false, &uninitialized_marker_offsets, )?; + emit_reflection_parameter_properties(ctx, parameter) +} + +/// Writes one ReflectionParameter object's private metadata properties. +fn emit_reflection_parameter_properties( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { let class_info = ctx .module .class_infos @@ -2097,11 +2237,7 @@ fn emit_reflection_parameter_object( } /// Allocates an indexed array for static reflection metadata. -fn emit_reflection_indexed_array( - ctx: &mut FunctionContext<'_>, - capacity: usize, - stride: i64, -) { +fn emit_reflection_indexed_array(ctx: &mut FunctionContext<'_>, capacity: usize, stride: i64) { match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_load_int_immediate(ctx.emitter, "x0", capacity as i64); @@ -2153,32 +2289,32 @@ fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) /// Appends ReflectionClass metadata names to the current ARM64 result array. fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result } /// Appends ReflectionClass metadata names to the current x86_64 result array. fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings - ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result } /// Stores ReflectionMethod/ReflectionProperty boolean predicate slots when supported. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 42dd574b5c..abac1c0c89 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -8803,7 +8803,26 @@ fn lower_first_class_callable_expr_call( } /// Lowers fixed-class object construction. -fn lower_new_object(ctx: &mut LoweringContext<'_, '_>, class_name: &Name, args: &[Expr], expr: &Expr) -> LoweredValue { +fn lower_new_object( + ctx: &mut LoweringContext<'_, '_>, + class_name: &Name, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) == "reflectionparameter" { + if let Some(operands) = lower_reflection_parameter_constructor_operands(ctx, args) { + let php_type = PhpType::Object(class_name.as_str().to_string()); + let data = ctx.intern_class_name(class_name.as_str()); + return ctx.emit_value( + Op::ObjectNew, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ObjectNew.default_effects(), + Some(expr.span), + ); + } + } let sig = constructor_signature(ctx, class_name).cloned(); let operands = lower_args_with_signature(ctx, sig.as_ref(), args); let php_type = PhpType::Object(class_name.as_str().to_string()); @@ -8818,6 +8837,70 @@ fn lower_new_object(ctx: &mut LoweringContext<'_, '_>, class_name: &Name, args: ) } +/// Lowers validated `ReflectionParameter` constructor arguments into metadata +/// operands: reflected class, reflected method, and selected parameter. +fn lower_reflection_parameter_constructor_operands( + ctx: &mut LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let (class_arg, method_arg, parameter_arg) = + reflection_parameter_constructor_arg_exprs(ctx, args)?; + Some(vec![ + lower_expr(ctx, &class_arg).value, + lower_expr(ctx, &method_arg).value, + lower_expr(ctx, ¶meter_arg).value, + ]) +} + +/// Returns class/method/parameter expressions from a normalized static +/// `ReflectionParameter` constructor call. +fn reflection_parameter_constructor_arg_exprs( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option<(Expr, Expr, Expr)> { + let args = expand_static_call_spread_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (target, parameter) = if crate::types::call_args::has_named_args(&args) { + let sig = ctx + .classes + .get("ReflectionParameter") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = + crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + ( + planned_regular_arg_expr(plan.regular_args.first()?)?.clone(), + planned_regular_arg_expr(plan.regular_args.get(1)?)?.clone(), + ) + } else { + (args.first()?.clone(), args.get(1)?.clone()) + }; + let ExprKind::ArrayLiteral(items) = &target.kind else { + return None; + }; + if items.len() != 2 { + return None; + } + Some((items[0].clone(), items[1].clone(), parameter)) +} + /// Lowers PHP `new $class(...)` into the generic dynamic-new EIR opcode. fn lower_new_dynamic( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 196e2e96d1..f9fe845d90 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -557,6 +557,10 @@ fn builtin_reflection_parameter() -> FlattenedClass { builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), ], methods: vec![ + builtin_reflection_owner_constructor_method(vec![ + ("function", Some(mixed_type()), None, false), + ("param", Some(mixed_type()), None, false), + ]), builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), builtin_reflection_slot_getter("getPosition", "__position", TypeExpr::Int), builtin_reflection_slot_getter("isOptional", "__optional", TypeExpr::Bool), diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 20ab8e3bb6..7704c43be1 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -17,6 +17,12 @@ use super::super::super::Checker; mod reflection; +/// Compile-time selector accepted by `ReflectionParameter::__construct()`. +enum ReflectionParameterSelector { + Name(String), + Position(i64), +} + impl Checker { /// Infers the type of a `new Class(...)` expression. /// @@ -180,12 +186,11 @@ impl Checker { && self.current_method.as_deref() == Some(get_iterator_key.as_str()) } - /// Validates constructor arguments for reflection owner classes - /// (`ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`). + /// Validates constructor arguments for reflection owner classes. /// - /// Extracts the reflected class/method/property from string literal args, - /// then delegates to `validate_reflection_class_attrs`, - /// `validate_reflection_method_attrs`, or `validate_reflection_property_attrs`. + /// Extracts the reflected class/member metadata from literal arguments and + /// delegates to the focused ReflectionClass/Method/Property/Parameter + /// validation helpers. fn validate_reflection_owner_constructor( &mut self, class_name: &str, @@ -223,6 +228,9 @@ impl Checker { )?; return self.validate_reflection_function_target(&function_name, expr); } + if class_name == "ReflectionParameter" { + return self.validate_reflection_parameter_constructor(&normalized_args, expr, env); + } let reflected_class = self.reflection_class_literal_arg(class_name, &normalized_args[0], env)?; @@ -281,6 +289,140 @@ impl Checker { } } + /// Validates `new ReflectionParameter([ClassName::class, "method"], param)`. + /// + /// The currently supported constructor target is a statically known + /// class/interface/trait method array. The parameter selector must be an + /// integer position or string name known at compile time. + fn validate_reflection_parameter_constructor( + &mut self, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result<(), CompileError> { + let (class_name, method_name) = + self.reflection_parameter_method_target(args.first(), env)?; + let sig = self + .reflection_method_signature(&class_name, &method_name) + .ok_or_else(|| { + CompileError::new( + expr.span, + &format!( + "ReflectionParameter::__construct(): undefined method '{}::{}'", + class_name, method_name + ), + ) + })?; + let selector = self.reflection_parameter_selector_arg(args.get(1), env)?; + match selector { + ReflectionParameterSelector::Name(name) => { + if sig.params.iter().any(|(param_name, _)| param_name == &name) { + Ok(()) + } else { + Err(CompileError::new( + expr.span, + "ReflectionParameter::__construct(): parameter specified by name could not be found", + )) + } + } + ReflectionParameterSelector::Position(position) => { + if position >= 0 && (position as usize) < sig.params.len() { + Ok(()) + } else { + Err(CompileError::new( + expr.span, + "ReflectionParameter::__construct(): parameter specified by offset could not be found", + )) + } + } + } + } + + /// Extracts the class and method names from the ReflectionParameter target array. + fn reflection_parameter_method_target( + &mut self, + arg: Option<&Expr>, + env: &TypeEnv, + ) -> Result<(String, String), CompileError> { + let arg = arg.expect("reflection parameter constructor arity was validated"); + let arg_ty = self.infer_type(arg, env)?; + if !matches!(arg_ty.codegen_repr(), PhpType::Array(_) | PhpType::Mixed) { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() first argument must be a class-method array", + )); + } + let ExprKind::ArrayLiteral(items) = &arg.kind else { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() requires a literal class-method array target", + )); + }; + if items.len() != 2 { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() expects array(class, method) as first argument", + )); + } + let class_name = + self.reflection_class_literal_arg("ReflectionParameter", &items[0], env)?; + let method_name = self.reflection_string_literal_arg( + "ReflectionParameter", + "method name", + items.get(1), + env, + )?; + Ok((class_name, method_name)) + } + + /// Extracts the name or position selector for `ReflectionParameter`. + fn reflection_parameter_selector_arg( + &mut self, + arg: Option<&Expr>, + env: &TypeEnv, + ) -> Result { + let arg = arg.expect("reflection parameter constructor arity was validated"); + let arg_ty = self.infer_type(arg, env)?; + if !matches!(arg_ty, PhpType::Str | PhpType::Int) { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() second argument must be a string or int", + )); + } + match &arg.kind { + ExprKind::StringLiteral(name) => Ok(ReflectionParameterSelector::Name(name.clone())), + ExprKind::IntLiteral(position) => Ok(ReflectionParameterSelector::Position(*position)), + _ => Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() requires a literal parameter name or position", + )), + } + } + + /// Returns the reflected method signature for class/interface metadata. + fn reflection_method_signature( + &self, + class_name: &str, + method_name: &str, + ) -> Option { + let method_key = php_symbol_key(method_name); + if let Some(class_info) = self.classes.get(class_name) { + return class_info + .methods + .get(&method_key) + .or_else(|| class_info.static_methods.get(&method_key)) + .cloned(); + } + if let Some(interface_info) = self.interfaces.get(class_name) { + return interface_info + .methods + .get(&method_key) + .or_else(|| interface_info.static_methods.get(&method_key)) + .cloned(); + } + None + } + /// Extracts the class name argument from a reflection constructor call. /// /// Accepts a string literal or `ClassName::class` constant; returns the @@ -839,7 +981,8 @@ fn fiber_callable_array_parts(expr: &Expr) -> Option<(&Expr, &str)> { /// Returns `true` if `class_name` is a reflection owner class /// (`ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`, -/// `ReflectionClassConstant`, `ReflectionEnumUnitCase`, `ReflectionEnumBackedCase`). +/// `ReflectionParameter`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, +/// `ReflectionEnumBackedCase`). fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, @@ -847,6 +990,7 @@ fn is_reflection_owner_class(class_name: &str) -> bool { | "ReflectionMethod" | "ReflectionProperty" | "ReflectionFunction" + | "ReflectionParameter" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index bdab94c8ba..3a79074cf3 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1766,6 +1766,48 @@ echo ($traitParams[2]->hasType() ? "T" : "t"); ); } +/// Verifies direct `new ReflectionParameter()` construction for statically known +/// class and interface method targets. +#[test] +fn test_reflection_parameter_constructor_reflects_method_parameters() { + let out = compile_and_run_capture( + r##"getName() . "#" . $byName->getPosition(); +echo ($byName->hasType() ? "T" : "t"); +echo ($byName->isOptional() ? "O" : "R"); +echo ($byName->isPassedByReference() ? "B" : "b"); +echo ($byName->isVariadic() ? "V" : "v"); +echo "|"; +$byPosition = new ReflectionParameter(["reflectdirectparamtarget", "run"], 3); +echo $byPosition->getName() . "#" . $byPosition->getPosition(); +echo ($byPosition->hasType() ? "T" : "t"); +echo ($byPosition->isOptional() ? "O" : "R"); +echo ($byPosition->isPassedByReference() ? "B" : "b"); +echo ($byPosition->isVariadic() ? "V" : "v"); +echo "|"; +$iface = new ReflectionParameter([ReflectDirectParamInterface::class, "build"], 1); +echo $iface->getName() . "#" . $iface->getPosition(); +echo ($iface->isOptional() ? "O" : "R"); +echo "|"; +$named = new ReflectionParameter(param: "id", function: [ReflectDirectParamTarget::class, "run"]); +echo $named->getName() . "#" . $named->getPosition(); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "name#1tRBv|rest#3tObV|name#1O|id#0"); +} + /// Verifies that ReflectionMethod objects returned from `ReflectionClass::getMethods()` /// carry the same parameter metadata as directly constructed method reflectors. #[test] diff --git a/tests/error_tests/classes_traits.rs b/tests/error_tests/classes_traits.rs index e26ff161ed..e79d10571e 100644 --- a/tests/error_tests/classes_traits.rs +++ b/tests/error_tests/classes_traits.rs @@ -290,10 +290,8 @@ fn test_error_override_attribute_import_alias_is_recognized() { /// parent-method matching. #[test] fn test_override_attribute_qualified_lookalike_is_not_builtin() { - check_source( - " Date: Fri, 19 Jun 2026 06:37:40 +0200 Subject: [PATCH 0392/1208] Support ReflectionParameter on trait methods --- docs/php/classes.md | 16 ++--- src/types/checker/driver/mod.rs | 72 +++++++++++++++++-- .../checker/inference/objects/constructors.rs | 5 +- .../objects/constructors/reflection.rs | 2 +- src/types/checker/mod.rs | 4 +- tests/codegen/oop/attributes.rs | 11 ++- 6 files changed, 90 insertions(+), 20 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 34f4da2ed6..b00a48a2bc 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1136,12 +1136,12 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | -| `ReflectionParameter::getName()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | -| `ReflectionParameter::getPosition()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | -| `ReflectionParameter::isOptional()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | -| `ReflectionParameter::isVariadic()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | -| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | -| `ReflectionParameter::hasType()` | `new ReflectionParameter([ClassName::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | +| `ReflectionParameter::getName()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | +| `ReflectionParameter::getPosition()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | +| `ReflectionParameter::isOptional()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | +| `ReflectionParameter::isVariadic()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | +| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | +| `ReflectionParameter::hasType()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | @@ -1206,8 +1206,8 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known class/interface method arrays (`[ClassName::class, "method"]`) and `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. -- `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, and function/object/trait-target direct `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known class/interface/trait method arrays (`[ClassLike::class, "method"]`) and `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, and function/object-target direct `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 8cc81c0254..cd960667b1 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -15,8 +15,9 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::{ClassMethod, Program, Stmt, StmtKind}; use crate::types::{ + callable_wrapper_sig, traits::{flatten_classes, FlattenedClass}, - TypeEnv, + FunctionSig, PhpType, TypeEnv, }; use super::builtin_types::{ @@ -332,8 +333,10 @@ fn collect_declared_trait_names_into(program: &Program, names: &mut HashSet HashMap> { +/// Collects source-declared trait method signatures recursively, including namespace blocks. +fn collect_declared_trait_methods( + program: &Program, +) -> HashMap> { let mut methods = HashMap::new(); for stmt in program { match &stmt.kind { @@ -346,7 +349,12 @@ fn collect_declared_trait_methods(program: &Program) -> HashMap HashMap FunctionSig { + let params = method + .params + .iter() + .map(|(name, type_ann, _, _)| { + ( + name.clone(), + if type_ann.is_some() { + PhpType::Mixed + } else { + PhpType::Int + }, + ) + }) + .collect(); + let defaults = method + .params + .iter() + .map(|(_, _, default, _)| default.clone()) + .collect(); + let ref_params = method + .params + .iter() + .map(|(_, _, _, by_ref)| *by_ref) + .collect(); + callable_wrapper_sig(&FunctionSig { + params, + defaults, + return_type: PhpType::Mixed, + declared_return: method.return_type.is_some(), + ref_params, + declared_params: method + .params + .iter() + .map(|(_, type_ann, _, _)| type_ann.is_some()) + .chain( + method + .variadic + .iter() + .map(|_| method.variadic_type.is_some()), + ) + .collect(), + variadic: method.variadic.clone(), + deprecation: None, + }) +} + /// Builds method-checkable `FlattenedClass` units for every `enum` in the program so their method /// bodies go through the same validation as class methods. Enum signatures are already registered /// in `checker.classes` by the enum schema pass; these units only carry the names and method @@ -427,9 +487,7 @@ fn substitute_relative_class_types_in_flattened(classes: &mut [FlattenedClass]) } /// Resolves relative class types inside flattened enum methods. -fn substitute_relative_class_types_in_flattened_enums( - enums: &mut HashMap, -) { +fn substitute_relative_class_types_in_flattened_enums(enums: &mut HashMap) { for enum_unit in enums.values_mut() { let self_class = enum_unit.name.clone(); substitute_relative_class_types_in_methods(&mut enum_unit.methods, &self_class, None); diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 7704c43be1..429368c792 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -399,7 +399,7 @@ impl Checker { } } - /// Returns the reflected method signature for class/interface metadata. + /// Returns the reflected method signature for class/interface/trait metadata. fn reflection_method_signature( &self, class_name: &str, @@ -420,6 +420,9 @@ impl Checker { .or_else(|| interface_info.static_methods.get(&method_key)) .cloned(); } + if let Some(trait_methods) = self.declared_trait_methods.get(class_name) { + return trait_methods.get(&method_key).cloned(); + } None } diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index 745493d717..3d0325216f 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -102,7 +102,7 @@ impl Checker { )); } if let Some(trait_methods) = self.declared_trait_methods.get(class_name) { - if trait_methods.contains(&method_key) { + if trait_methods.contains_key(&method_key) { return Ok(()); } return Err(CompileError::new( diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index 4f92417446..0077ae3713 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -112,8 +112,8 @@ pub(crate) struct Checker { /// Canonical trait names declared in the program, available for reflection /// and class-like metadata probes that accept traits. pub declared_traits: HashSet, - /// Case-insensitive method keys declared directly on each trait. - pub declared_trait_methods: HashMap>, + /// Reflection-visible method signatures declared directly on each trait. + pub declared_trait_methods: HashMap>, /// Name of the class currently being type-checked (used for `$this` resolution). pub current_class: Option, /// Name of the current method being type-checked, when inside a class body. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 3a79074cf3..3af82bf459 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1778,6 +1778,9 @@ class ReflectDirectParamTarget { interface ReflectDirectParamInterface { public static function build(int $id, $name = "x"): void; } +trait ReflectDirectParamTrait { + protected static function traitRun(int $first, $second = 2, string ...$rest) {} +} $byName = new ReflectionParameter([ReflectDirectParamTarget::class, "run"], "name"); echo $byName->getName() . "#" . $byName->getPosition(); echo ($byName->hasType() ? "T" : "t"); @@ -1796,6 +1799,12 @@ $iface = new ReflectionParameter([ReflectDirectParamInterface::class, "build"], echo $iface->getName() . "#" . $iface->getPosition(); echo ($iface->isOptional() ? "O" : "R"); echo "|"; +$trait = new ReflectionParameter([ReflectDirectParamTrait::class, "traitRun"], "rest"); +echo $trait->getName() . "#" . $trait->getPosition(); +echo ($trait->hasType() ? "T" : "t"); +echo ($trait->isOptional() ? "O" : "R"); +echo ($trait->isVariadic() ? "V" : "v"); +echo "|"; $named = new ReflectionParameter(param: "id", function: [ReflectDirectParamTarget::class, "run"]); echo $named->getName() . "#" . $named->getPosition(); "##, @@ -1805,7 +1814,7 @@ echo $named->getName() . "#" . $named->getPosition(); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "name#1tRBv|rest#3tObV|name#1O|id#0"); + assert_eq!(out.stdout, "name#1tRBv|rest#3tObV|name#1O|rest#2TOV|id#0"); } /// Verifies that ReflectionMethod objects returned from `ReflectionClass::getMethods()` From 4feff9d494eb4fa390541de5d01632ebf2c52af7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 06:50:57 +0200 Subject: [PATCH 0393/1208] Support ReflectionParameter on functions --- docs/php/classes.md | 18 +-- src/codegen/lower_inst/objects/reflection.rs | 32 ++++- src/ir_lower/expr/mod.rs | 37 +++--- src/types/checker/driver/functions.rs | 2 +- src/types/checker/functions/resolution/mod.rs | 7 +- .../checker/functions/resolution/signature.rs | 4 +- .../checker/inference/objects/constructors.rs | 118 +++++++++++++++--- src/types/checker/type_compat/declarations.rs | 18 ++- tests/codegen/oop/attributes.rs | 34 +++++ tests/error_tests/classes_traits.rs | 19 +++ 10 files changed, 231 insertions(+), 58 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index b00a48a2bc..8f2540c3a3 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1136,12 +1136,12 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | -| `ReflectionParameter::getName()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | -| `ReflectionParameter::getPosition()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | -| `ReflectionParameter::isOptional()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | -| `ReflectionParameter::isVariadic()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | -| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | -| `ReflectionParameter::hasType()` | `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)` or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | +| `ReflectionParameter::getName()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | +| `ReflectionParameter::getPosition()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | +| `ReflectionParameter::isOptional()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | +| `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | +| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | +| `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | @@ -1201,13 +1201,13 @@ name with `isBuiltin()` false. | `ReflectionNamedType::allowsNull()` | `bool` | True when the declared type is nullable | Limitations today: -- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Method/Property/Parameter(...)` must be compile-time class/member strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, parameter, or attribute names require a runtime name→id lookup table that is not yet implemented. +- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name→id lookup table that is not yet implemented. - Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** — a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` → `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known class/interface/trait method arrays (`[ClassLike::class, "method"]`) and `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. -- `ReflectionFunction`/`ReflectionParameter` reflect named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, and function/object-target direct `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`) and class/interface/trait method arrays (`[ClassLike::class, "method"]`) and `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionFunction` reflects named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, internal-function parameter metadata, and object-target direct `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index db60f3a9ef..292090b725 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -872,11 +872,14 @@ fn reflection_property_metadata( .unwrap_or_else(empty_reflection_metadata)) } -/// Resolves `ReflectionParameter([class, method], parameter)` metadata. +/// Resolves `ReflectionParameter(target, parameter)` metadata. fn reflection_parameter_metadata( ctx: &FunctionContext<'_>, inst: &Instruction, ) -> Result { + if inst.operands.len() == 2 { + return reflection_function_parameter_metadata(ctx, inst); + } let Some(class_operand) = inst.operands.first().copied() else { return Ok(empty_reflection_metadata()); }; @@ -900,6 +903,33 @@ fn reflection_parameter_metadata( Ok(reflection_parameter_owner_metadata(parameter)) } +/// Resolves `ReflectionParameter(function, parameter)` metadata. +fn reflection_function_parameter_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(function_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let Some(parameter_operand) = inst.operands.get(1).copied() else { + return Ok(empty_reflection_metadata()); + }; + let function_name = + const_required_string_operand(ctx, function_operand, "ReflectionParameter")?; + let selector = const_parameter_selector_operand(ctx, parameter_operand)?; + let Some(function) = ctx.function_by_name(&function_name) else { + return Ok(empty_reflection_metadata()); + }; + let Some(signature) = function.signature.as_ref() else { + return Ok(empty_reflection_metadata()); + }; + let parameters = reflection_parameter_members(signature); + let Some(parameter) = reflection_parameter_member_for_selector(¶meters, selector) else { + return Ok(empty_reflection_metadata()); + }; + Ok(reflection_parameter_owner_metadata(parameter)) +} + /// Builds direct ReflectionParameter constructor metadata from one parameter member. fn reflection_parameter_owner_metadata( parameter: ReflectionParameterMember, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index abac1c0c89..36434abf2d 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -8837,27 +8837,28 @@ fn lower_new_object( ) } -/// Lowers validated `ReflectionParameter` constructor arguments into metadata -/// operands: reflected class, reflected method, and selected parameter. +/// Lowers validated `ReflectionParameter` constructor arguments into metadata operands. +/// +/// Method targets lower as `[class, method, parameter]`; function targets lower +/// as `[function, parameter]`. fn lower_reflection_parameter_constructor_operands( ctx: &mut LoweringContext<'_, '_>, args: &[Expr], ) -> Option> { - let (class_arg, method_arg, parameter_arg) = - reflection_parameter_constructor_arg_exprs(ctx, args)?; - Some(vec![ - lower_expr(ctx, &class_arg).value, - lower_expr(ctx, &method_arg).value, - lower_expr(ctx, ¶meter_arg).value, - ]) + let arg_exprs = reflection_parameter_constructor_arg_exprs(ctx, args)?; + Some( + arg_exprs + .iter() + .map(|arg| lower_expr(ctx, arg).value) + .collect(), + ) } -/// Returns class/method/parameter expressions from a normalized static -/// `ReflectionParameter` constructor call. +/// Returns metadata operand expressions from a normalized static `ReflectionParameter` call. fn reflection_parameter_constructor_arg_exprs( ctx: &LoweringContext<'_, '_>, args: &[Expr], -) -> Option<(Expr, Expr, Expr)> { +) -> Option> { let args = expand_static_call_spread_args(args); if args.iter().any(is_spread_arg) { return None; @@ -8892,13 +8893,13 @@ fn reflection_parameter_constructor_arg_exprs( } else { (args.first()?.clone(), args.get(1)?.clone()) }; - let ExprKind::ArrayLiteral(items) = &target.kind else { - return None; - }; - if items.len() != 2 { - return None; + match &target.kind { + ExprKind::ArrayLiteral(items) if items.len() == 2 => { + Some(vec![items[0].clone(), items[1].clone(), parameter]) + } + ExprKind::StringLiteral(_) => Some(vec![target, parameter]), + _ => None, } - Some((items[0].clone(), items[1].clone(), parameter)) } /// Lowers PHP `new $class(...)` into the generic dynamic-new EIR opcode. diff --git a/src/types/checker/driver/functions.rs b/src/types/checker/driver/functions.rs index 954dd699c7..620686c7ce 100644 --- a/src/types/checker/driver/functions.rs +++ b/src/types/checker/driver/functions.rs @@ -279,7 +279,7 @@ impl Checker { .param_types .iter() .map(|type_ann| type_ann.is_some()) - .chain(decl.variadic.iter().map(|_| false)) + .chain(decl.variadic.iter().map(|_| decl.variadic_type.is_some())) .collect(), variadic: decl.variadic, deprecation: None, diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index c248f1054f..9f4f1a8998 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -196,7 +196,12 @@ impl Checker { declared_return: decl.return_type.is_some(), by_ref_return: false, ref_params: decl.ref_params.clone(), - declared_params: decl.param_types.iter().map(|type_ann| type_ann.is_some()).collect(), + declared_params: decl + .param_types + .iter() + .map(|type_ann| type_ann.is_some()) + .chain(decl.variadic.iter().map(|_| decl.variadic_type.is_some())) + .collect(), variadic: decl.variadic.clone(), deprecation: None, }; diff --git a/src/types/checker/functions/resolution/signature.rs b/src/types/checker/functions/resolution/signature.rs index 09f7bbf12a..69b5dcc15d 100644 --- a/src/types/checker/functions/resolution/signature.rs +++ b/src/types/checker/functions/resolution/signature.rs @@ -102,7 +102,7 @@ impl Checker { .param_types .iter() .map(|type_ann| type_ann.is_some()) - .chain(decl.variadic.iter().map(|_| false)) + .chain(decl.variadic.iter().map(|_| decl.variadic_type.is_some())) .collect(), variadic: decl.variadic.clone(), }; @@ -238,7 +238,7 @@ impl Checker { .param_types .iter() .map(|type_ann| type_ann.is_some()) - .chain(decl.variadic.iter().map(|_| false)) + .chain(decl.variadic.iter().map(|_| decl.variadic_type.is_some())) .collect(), variadic: decl.variadic.clone(), deprecation: crate::types::checker::schema::validation::extract_deprecation( diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 429368c792..1738675925 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -23,6 +23,15 @@ enum ReflectionParameterSelector { Position(i64), } +/// Compile-time function or method target accepted by `ReflectionParameter::__construct()`. +enum ReflectionParameterTarget { + Function(String), + Method { + class_name: String, + method_name: String, + }, +} + impl Checker { /// Infers the type of a `new Class(...)` expression. /// @@ -289,10 +298,10 @@ impl Checker { } } - /// Validates `new ReflectionParameter([ClassName::class, "method"], param)`. + /// Validates `new ReflectionParameter(target, param)`. /// - /// The currently supported constructor target is a statically known - /// class/interface/trait method array. The parameter selector must be an + /// Supported targets are statically known user function names and + /// class/interface/trait method arrays. The parameter selector must be an /// integer position or string name known at compile time. fn validate_reflection_parameter_constructor( &mut self, @@ -300,19 +309,34 @@ impl Checker { expr: &Expr, env: &TypeEnv, ) -> Result<(), CompileError> { - let (class_name, method_name) = - self.reflection_parameter_method_target(args.first(), env)?; - let sig = self - .reflection_method_signature(&class_name, &method_name) - .ok_or_else(|| { - CompileError::new( - expr.span, - &format!( - "ReflectionParameter::__construct(): undefined method '{}::{}'", - class_name, method_name - ), - ) - })?; + let target = self.reflection_parameter_target(args.first(), env)?; + let sig = match target { + ReflectionParameterTarget::Function(function_name) => self + .reflection_function_signature(&function_name)? + .ok_or_else(|| { + CompileError::new( + expr.span, + &format!( + "ReflectionParameter::__construct(): Function {}() does not exist", + function_name + ), + ) + })?, + ReflectionParameterTarget::Method { + class_name, + method_name, + } => self + .reflection_method_signature(&class_name, &method_name) + .ok_or_else(|| { + CompileError::new( + expr.span, + &format!( + "ReflectionParameter::__construct(): undefined method '{}::{}'", + class_name, method_name + ), + ) + })?, + }; let selector = self.reflection_parameter_selector_arg(args.get(1), env)?; match selector { ReflectionParameterSelector::Name(name) => { @@ -338,13 +362,39 @@ impl Checker { } } - /// Extracts the class and method names from the ReflectionParameter target array. - fn reflection_parameter_method_target( + /// Extracts the function or class-method target from a ReflectionParameter call. + fn reflection_parameter_target( &mut self, arg: Option<&Expr>, env: &TypeEnv, - ) -> Result<(String, String), CompileError> { + ) -> Result { let arg = arg.expect("reflection parameter constructor arity was validated"); + let arg_ty = self.infer_type(arg, env)?; + match arg_ty.codegen_repr() { + PhpType::Str => self + .reflection_string_literal_arg( + "ReflectionParameter", + "function name", + Some(arg), + env, + ) + .map(ReflectionParameterTarget::Function), + PhpType::Array(_) | PhpType::Mixed => { + self.reflection_parameter_method_target(arg, env) + } + _ => Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() first argument must be a function name string or class-method array", + )), + } + } + + /// Extracts the class and method names from the ReflectionParameter target array. + fn reflection_parameter_method_target( + &mut self, + arg: &Expr, + env: &TypeEnv, + ) -> Result { let arg_ty = self.infer_type(arg, env)?; if !matches!(arg_ty.codegen_repr(), PhpType::Array(_) | PhpType::Mixed) { return Err(CompileError::new( @@ -372,7 +422,35 @@ impl Checker { items.get(1), env, )?; - Ok((class_name, method_name)) + Ok(ReflectionParameterTarget::Method { + class_name, + method_name, + }) + } + + /// Returns the reflected signature for a statically declared user function. + fn reflection_function_signature( + &mut self, + function_name: &str, + ) -> Result, CompileError> { + let canonical = + match self.canonical_function_name_folded(function_name.trim_start_matches('\\')) { + Some(canonical) => canonical, + None => return Ok(None), + }; + if let Some(sig) = self.functions.get(&canonical).cloned() { + return Ok(Some(sig)); + } + if self.function_variant_groups.contains_key(&canonical) { + self.ensure_function_variant_group_signature(&canonical, crate::span::Span::dummy())?; + return Ok(self.functions.get(&canonical).cloned()); + } + if let Some(decl) = self.fn_decls.get(&canonical).cloned() { + let param_types = self.initial_function_param_types(&canonical, &decl)?; + self.resolve_function_signature(&canonical, &decl, param_types)?; + return Ok(self.functions.get(&canonical).cloned()); + } + Ok(None) } /// Extracts the name or position selector for `ReflectionParameter`. diff --git a/src/types/checker/type_compat/declarations.rs b/src/types/checker/type_compat/declarations.rs index cf1845fcfb..d15b961404 100644 --- a/src/types/checker/type_compat/declarations.rs +++ b/src/types/checker/type_compat/declarations.rs @@ -211,8 +211,8 @@ impl Checker { } /// Builds the initial parameter type list for a function declaration, resolving type hints, - /// validating defaults, and inferring types for untyped parameters. Adds variadic parameter - /// type as `PhpType::Array(Int)` if the function is variadic. + /// validating defaults, and inferring types for untyped parameters. Adds a variadic parameter + /// array type, using the declared element type for typed variadics. pub(crate) fn initial_function_param_types( &self, name: &str, @@ -240,10 +240,16 @@ impl Checker { } } if let Some(variadic_name) = decl.variadic.as_ref() { - param_types.push(( - variadic_name.clone(), - PhpType::Array(Box::new(PhpType::Int)), - )); + let elem_ty = if let Some(type_ann) = decl.variadic_type.as_ref() { + self.resolve_declared_param_type_hint( + type_ann, + decl.span, + &format!("Function '{}' variadic parameter ${}", name, variadic_name), + )? + } else { + PhpType::Int + }; + param_types.push((variadic_name.clone(), PhpType::Array(Box::new(elem_ty)))); } Ok(param_types) } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 3af82bf459..7de991b013 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1817,6 +1817,40 @@ echo $named->getName() . "#" . $named->getPosition(); assert_eq!(out.stdout, "name#1tRBv|rest#3tObV|name#1O|rest#2TOV|id#0"); } +/// Verifies direct `new ReflectionParameter()` construction for statically known +/// user function targets. +#[test] +fn test_reflection_parameter_constructor_reflects_function_parameters() { + let out = compile_and_run_capture( + r##"getName() . "#" . $byName->getPosition(); +echo ($byName->hasType() ? "T" : "t"); +echo ($byName->isOptional() ? "O" : "R"); +echo ($byName->isPassedByReference() ? "B" : "b"); +echo ($byName->isVariadic() ? "V" : "v"); +echo "|"; +$byPosition = new ReflectionParameter("REFLECT_DIRECT_FUNCTION", 3); +echo $byPosition->getName() . "#" . $byPosition->getPosition(); +echo ($byPosition->hasType() ? "T" : "t"); +echo ($byPosition->isOptional() ? "O" : "R"); +echo ($byPosition->isPassedByReference() ? "B" : "b"); +echo ($byPosition->isVariadic() ? "V" : "v"); +echo "|"; +$named = new ReflectionParameter(param: "id", function: "\\reflect_direct_function"); +echo $named->getName() . "#" . $named->getPosition(); +echo ($named->hasType() ? "T" : "t"); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "name#1tRBv|rest#3TObV|id#0T"); +} + /// Verifies that ReflectionMethod objects returned from `ReflectionClass::getMethods()` /// carry the same parameter metadata as directly constructed method reflectors. #[test] diff --git a/tests/error_tests/classes_traits.rs b/tests/error_tests/classes_traits.rs index e79d10571e..c328d2944c 100644 --- a/tests/error_tests/classes_traits.rs +++ b/tests/error_tests/classes_traits.rs @@ -543,6 +543,25 @@ fn test_error_reflection_parameter_constructor_unknown_name() { ); } +/// Verifies that `new ReflectionParameter()` rejects unknown function targets. +#[test] +fn test_error_reflection_parameter_constructor_unknown_function() { + expect_error( + " Date: Fri, 19 Jun 2026 06:56:00 +0200 Subject: [PATCH 0394/1208] Support ReflectionParameter on object method arrays --- docs/php/classes.md | 16 ++-- src/ir_lower/expr/mod.rs | 75 +++++++++++++++++-- .../checker/inference/objects/constructors.rs | 36 ++++++++- tests/codegen/oop/attributes.rs | 13 +++- 4 files changed, 124 insertions(+), 16 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 8f2540c3a3..34112389b9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1136,12 +1136,12 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | -| `ReflectionParameter::getName()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | -| `ReflectionParameter::getPosition()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | -| `ReflectionParameter::isOptional()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | -| `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | -| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | -| `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | +| `ReflectionParameter::getName()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | +| `ReflectionParameter::getPosition()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | +| `ReflectionParameter::isOptional()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | +| `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | +| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | +| `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | @@ -1206,8 +1206,8 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`) and class/interface/trait method arrays (`[ClassLike::class, "method"]`) and `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. -- `ReflectionFunction` reflects named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, internal-function parameter metadata, and object-target direct `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionFunction` reflects named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 36434abf2d..b1541d3a36 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -8837,6 +8837,12 @@ fn lower_new_object( ) } +/// Metadata operand source for direct `ReflectionParameter` constructor lowering. +enum ReflectionParameterConstructorOperand { + Expr(Expr), + ClassName { name: String, span: Span }, +} + /// Lowers validated `ReflectionParameter` constructor arguments into metadata operands. /// /// Method targets lower as `[class, method, parameter]`; function targets lower @@ -8849,16 +8855,38 @@ fn lower_reflection_parameter_constructor_operands( Some( arg_exprs .iter() - .map(|arg| lower_expr(ctx, arg).value) + .map(|arg| lower_reflection_parameter_constructor_operand(ctx, arg)) .collect(), ) } +/// Lowers one direct `ReflectionParameter` metadata operand. +fn lower_reflection_parameter_constructor_operand( + ctx: &mut LoweringContext<'_, '_>, + operand: &ReflectionParameterConstructorOperand, +) -> ValueId { + match operand { + ReflectionParameterConstructorOperand::Expr(expr) => lower_expr(ctx, expr).value, + ReflectionParameterConstructorOperand::ClassName { name, span } => { + let data = ctx.intern_class_name(name); + ctx.emit_value( + Op::ConstClassName, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Str, + Op::ConstClassName.default_effects(), + Some(*span), + ) + .value + } + } +} + /// Returns metadata operand expressions from a normalized static `ReflectionParameter` call. fn reflection_parameter_constructor_arg_exprs( ctx: &LoweringContext<'_, '_>, args: &[Expr], -) -> Option> { +) -> Option> { let args = expand_static_call_spread_args(args); if args.iter().any(is_spread_arg) { return None; @@ -8895,13 +8923,50 @@ fn reflection_parameter_constructor_arg_exprs( }; match &target.kind { ExprKind::ArrayLiteral(items) if items.len() == 2 => { - Some(vec![items[0].clone(), items[1].clone(), parameter]) - } - ExprKind::StringLiteral(_) => Some(vec![target, parameter]), + let owner = reflection_parameter_method_owner_operand(ctx, &items[0])?; + Some(vec![ + owner, + ReflectionParameterConstructorOperand::Expr(items[1].clone()), + ReflectionParameterConstructorOperand::Expr(parameter), + ]) + } + ExprKind::StringLiteral(_) => Some(vec![ + ReflectionParameterConstructorOperand::Expr(target), + ReflectionParameterConstructorOperand::Expr(parameter), + ]), _ => None, } } +/// Returns the static class-name operand for a ReflectionParameter method target. +fn reflection_parameter_method_owner_operand( + ctx: &LoweringContext<'_, '_>, + owner: &Expr, +) -> Option { + match &owner.kind { + ExprKind::Variable(name) => { + let PhpType::Object(class_name) = ctx.local_type(name).codegen_repr() else { + return None; + }; + if class_name.is_empty() { + return None; + } + Some(ReflectionParameterConstructorOperand::ClassName { + name: class_name, + span: owner.span, + }) + } + ExprKind::This => ctx + .current_class + .clone() + .map(|name| ReflectionParameterConstructorOperand::ClassName { + name, + span: owner.span, + }), + _ => Some(ReflectionParameterConstructorOperand::Expr(owner.clone())), + } +} + /// Lowers PHP `new $class(...)` into the generic dynamic-new EIR opcode. fn lower_new_dynamic( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 1738675925..0766ce3aa3 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -414,8 +414,7 @@ impl Checker { "ReflectionParameter::__construct() expects array(class, method) as first argument", )); } - let class_name = - self.reflection_class_literal_arg("ReflectionParameter", &items[0], env)?; + let class_name = self.reflection_parameter_owner_class_name(&items[0], env)?; let method_name = self.reflection_string_literal_arg( "ReflectionParameter", "method name", @@ -428,6 +427,39 @@ impl Checker { }) } + /// Resolves the class-like name from a class literal or statically known object target. + fn reflection_parameter_owner_class_name( + &mut self, + arg: &Expr, + env: &TypeEnv, + ) -> Result { + match &arg.kind { + ExprKind::StringLiteral(_) | ExprKind::ClassConstant { .. } => { + self.reflection_class_literal_arg("ReflectionParameter", arg, env) + } + ExprKind::Variable(_) | ExprKind::This => { + let target_ty = self.infer_type(arg, env)?.codegen_repr(); + let PhpType::Object(class_name) = target_ty else { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() object target must have a concrete class type", + )); + }; + if class_name.is_empty() || !self.classes.contains_key(class_name.as_str()) { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() object target must have a concrete class type", + )); + } + Ok(class_name) + } + _ => Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() requires a literal class name or statically typed object target", + )), + } + } + /// Returns the reflected signature for a statically declared user function. fn reflection_function_signature( &mut self, diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 7de991b013..61bcc3b3e1 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1795,6 +1795,14 @@ echo ($byPosition->isOptional() ? "O" : "R"); echo ($byPosition->isPassedByReference() ? "B" : "b"); echo ($byPosition->isVariadic() ? "V" : "v"); echo "|"; +$object = new ReflectDirectParamTarget(); +$byObject = new ReflectionParameter([$object, "run"], "mode"); +echo $byObject->getName() . "#" . $byObject->getPosition(); +echo ($byObject->hasType() ? "T" : "t"); +echo ($byObject->isOptional() ? "O" : "R"); +echo ($byObject->isPassedByReference() ? "B" : "b"); +echo ($byObject->isVariadic() ? "V" : "v"); +echo "|"; $iface = new ReflectionParameter([ReflectDirectParamInterface::class, "build"], 1); echo $iface->getName() . "#" . $iface->getPosition(); echo ($iface->isOptional() ? "O" : "R"); @@ -1814,7 +1822,10 @@ echo $named->getName() . "#" . $named->getPosition(); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "name#1tRBv|rest#3tObV|name#1O|rest#2TOV|id#0"); + assert_eq!( + out.stdout, + "name#1tRBv|rest#3tObV|mode#2TObv|name#1O|rest#2TOV|id#0" + ); } /// Verifies direct `new ReflectionParameter()` construction for statically known From 0944f72750894d97a9806e4e4eeb922e4b14a50f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 07:06:33 +0200 Subject: [PATCH 0395/1208] Support ReflectionFunction parameter metadata --- docs/php/classes.md | 3 +- src/autoload/mod.rs | 2 + src/codegen/lower_inst/objects.rs | 3 ++ src/codegen/lower_inst/objects/reflection.rs | 41 ++++++++++++---- src/ir_lower/program.rs | 1 + src/types/checker/builtin_types/reflection.rs | 49 ++++++++++++++----- .../checker/inference/objects/constructors.rs | 32 ++++++++++++ tests/codegen/oop/attributes.rs | 34 +++++++++++++ tests/error_tests/classes_traits.rs | 19 +++++++ 9 files changed, 161 insertions(+), 23 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 34112389b9..b0b55a3aa7 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1201,12 +1201,13 @@ name with `isBuiltin()` false. | `ReflectionNamedType::allowsNull()` | `bool` | True when the declared type is nullable | Limitations today: -- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name→id lookup table that is not yet implemented. +- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Function/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name→id lookup table that is not yet implemented. - Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** — a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` → `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionFunction` supports `getName()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. - `ReflectionFunction` reflects named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index 4436425fd5..609fd33fa9 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -77,7 +77,9 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "ReflectionClassConstant", "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", + "ReflectionFunction", "ReflectionMethod", + "ReflectionParameter", "ReflectionProperty", "RuntimeException", "SeekableIterator", diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index d59c2fb021..5d96e7feb5 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1294,7 +1294,9 @@ fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionClassConstant", "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", + "ReflectionFunction", "ReflectionMethod", + "ReflectionParameter", "ReflectionProperty", "RuntimeException", "SplDoublyLinkedList", @@ -1361,6 +1363,7 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", "ReflectionException", + "ReflectionFunction", "ReflectionMethod", "ReflectionParameter", "ReflectionProperty", diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 292090b725..c761316a67 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -6,10 +6,10 @@ //! - `crate::codegen::lower_inst::objects::lower_object_new()`. //! //! Key details: -//! - `ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`, -//! `ReflectionClassConstant`, and `ReflectionEnum*` +//! - `ReflectionClass`, `ReflectionFunction`, `ReflectionMethod`, +//! `ReflectionProperty`, `ReflectionClassConstant`, and `ReflectionEnum*` //! constructors are compile-time metadata lookups that populate private -//! `__name`/`__attrs` slots instead of running their public empty bodies. +//! metadata slots instead of running their public empty bodies. use crate::codegen::platform::Arch; use crate::codegen::{abi, emit_box_current_value_as_mixed, CodegenIrError, Result}; @@ -90,6 +90,7 @@ pub(super) fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, "ReflectionClass" + | "ReflectionFunction" | "ReflectionMethod" | "ReflectionProperty" | "ReflectionParameter" @@ -179,12 +180,9 @@ fn emit_reflection_owner_object( )?; } } - emit_reflection_attrs_property( - ctx, - class_name, - &metadata.attr_names, - &metadata.attr_args, - )?; + if class_name != "ReflectionFunction" { + emit_reflection_attrs_property(ctx, class_name, &metadata.attr_names, &metadata.attr_args)?; + } if class_name == "ReflectionClass" { emit_reflection_bool_property(ctx, "__is_final", metadata.is_final)?; emit_reflection_bool_property(ctx, "__is_abstract", metadata.is_abstract)?; @@ -195,7 +193,7 @@ fn emit_reflection_owner_object( emit_reflection_bool_property(ctx, "__is_instantiable", metadata.is_instantiable)?; emit_reflection_int_property_by_name(ctx, "__modifiers", metadata.modifiers)?; } - if class_name == "ReflectionMethod" { + if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { emit_reflection_parameter_array_property_by_name( ctx, class_name, @@ -613,6 +611,7 @@ fn reflection_owner_metadata( ) -> Result { match class_name { "ReflectionClass" => reflection_class_metadata(ctx, inst), + "ReflectionFunction" => reflection_function_metadata(ctx, inst), "ReflectionMethod" => reflection_method_metadata(ctx, inst), "ReflectionProperty" => reflection_property_metadata(ctx, inst), "ReflectionParameter" => reflection_parameter_metadata(ctx, inst), @@ -762,6 +761,28 @@ fn reflection_class_metadata_for_name( Ok(empty_reflection_metadata()) } +/// Resolves `ReflectionFunction(function)` metadata. +fn reflection_function_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(function_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let function_name = const_required_string_operand(ctx, function_operand, "ReflectionFunction")?; + let Some(function) = ctx.function_by_name(&function_name) else { + return Ok(empty_reflection_metadata()); + }; + let Some(signature) = function.signature.as_ref() else { + return Ok(empty_reflection_metadata()); + }; + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(function.name.trim_start_matches('\\').to_string()); + metadata.parameter_members = reflection_parameter_members(signature); + metadata.required_parameter_count = reflection_required_parameter_count(signature); + Ok(metadata) +} + /// Resolves `ReflectionMethod(class, method)` metadata. fn reflection_method_metadata( ctx: &FunctionContext<'_>, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 8b037fce17..3720b92466 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -790,6 +790,7 @@ fn lower_builtin_reflection_methods( "ReflectionClassConstant", "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", + "ReflectionFunction", "ReflectionMethod", "ReflectionParameter", "ReflectionProperty", diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index f9fe845d90..254814ed51 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -35,6 +35,7 @@ pub(crate) fn inject_builtin_reflection( for builtin_name in [ "ReflectionAttribute", "ReflectionClass", + "ReflectionFunction", "ReflectionMethod", "ReflectionProperty", "ReflectionFunction", @@ -102,6 +103,14 @@ pub(crate) fn inject_builtin_reflection( }, ); class_map.insert("ReflectionClass".to_string(), builtin_reflection_class()); + class_map.insert( + "ReflectionFunction".to_string(), + builtin_reflection_owner_class( + "ReflectionFunction", + true, + vec![("function", Some(TypeExpr::Str), None, false)], + ), + ); class_map.insert( "ReflectionMethod".to_string(), builtin_reflection_owner_class( @@ -1288,7 +1297,7 @@ fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> Cl } } -/// Builds a `FlattenedClass` for `ReflectionMethod` or `ReflectionProperty` +/// Builds a `FlattenedClass` for simple reflection owner classes /// with a private `__attrs` array property and two methods: `__construct` /// (public, accepting the supplied params) and `getAttributes` (public, /// returning the `__attrs` array). @@ -1311,7 +1320,7 @@ fn builtin_reflection_owner_class( methods.push(builtin_reflection_class_string_method("getName", "__name")); } add_reflection_member_flag_methods(name, &mut properties, &mut methods); - if name == "ReflectionMethod" { + if matches!(name, "ReflectionFunction" | "ReflectionMethod") { properties.push(builtin_property( "__parameters", Visibility::Private, @@ -1329,19 +1338,21 @@ fn builtin_reflection_owner_class( "__parameters", object_array_type("ReflectionParameter"), )); - methods.push(builtin_reflection_method_parameter_count_method()); + methods.push(builtin_reflection_parameter_count_method()); methods.push(builtin_reflection_class_int_method( "getNumberOfRequiredParameters", "__required_parameter_count", )); } - properties.push(builtin_property( - "__attrs", - Visibility::Private, - Some(array_type()), - empty_array(), - )); - methods.push(builtin_reflection_owner_get_attributes_method()); + if name != "ReflectionFunction" { + properties.push(builtin_property( + "__attrs", + Visibility::Private, + Some(array_type()), + empty_array(), + )); + methods.push(builtin_reflection_owner_get_attributes_method()); + } FlattenedClass { name: name.to_string(), extends: None, @@ -1357,8 +1368,8 @@ fn builtin_reflection_owner_class( } } -/// Builds `ReflectionMethod::getNumberOfParameters()` over the retained parameter array. -fn builtin_reflection_method_parameter_count_method() -> ClassMethod { +/// Builds `getNumberOfParameters()` over the retained parameter array. +fn builtin_reflection_parameter_count_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); ClassMethod { name: "getNumberOfParameters".to_string(), @@ -1514,6 +1525,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } for class_name in [ "ReflectionClass", + "ReflectionFunction", "ReflectionMethod", "ReflectionProperty", "ReflectionParameter", @@ -1528,6 +1540,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if matches!( class_name, "ReflectionClass" + | "ReflectionFunction" | "ReflectionMethod" | "ReflectionProperty" | "ReflectionParameter" @@ -1623,6 +1636,18 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } } + if class_name == "ReflectionFunction" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionParameter".to_string(), + ))); + } + for method_name in ["getNumberOfParameters", "getNumberOfRequiredParameters"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Int; + } + } + } if class_name == "ReflectionParameter" { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPosition")) { sig.return_type = PhpType::Int; diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 0766ce3aa3..613b4e3cb3 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -240,6 +240,9 @@ impl Checker { if class_name == "ReflectionParameter" { return self.validate_reflection_parameter_constructor(&normalized_args, expr, env); } + if class_name == "ReflectionFunction" { + return self.validate_reflection_function_constructor(&normalized_args, expr, env); + } let reflected_class = self.reflection_class_literal_arg(class_name, &normalized_args[0], env)?; @@ -298,6 +301,34 @@ impl Checker { } } + /// Validates `new ReflectionFunction(function)` for a statically known user function. + fn validate_reflection_function_constructor( + &mut self, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result<(), CompileError> { + let function_name = self.reflection_string_literal_arg( + "ReflectionFunction", + "function name", + args.first(), + env, + )?; + if self + .reflection_function_signature(&function_name)? + .is_some() + { + return Ok(()); + } + Err(CompileError::new( + expr.span, + &format!( + "ReflectionFunction::__construct(): Function {}() does not exist", + function_name + ), + )) + } + /// Validates `new ReflectionParameter(target, param)`. /// /// Supported targets are statically known user function names and @@ -1100,6 +1131,7 @@ fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, "ReflectionClass" + | "ReflectionFunction" | "ReflectionMethod" | "ReflectionProperty" | "ReflectionFunction" diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 61bcc3b3e1..35299eeef9 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1862,6 +1862,40 @@ echo ($named->hasType() ? "T" : "t"); assert_eq!(out.stdout, "name#1tRBv|rest#3TObV|id#0T"); } +/// Verifies `ReflectionFunction` exposes user-function name and parameter metadata. +#[test] +fn test_reflection_function_reflects_user_function_parameters() { + let out = compile_and_run_capture( + r##"getName() . "#"; +echo $ref->getNumberOfParameters() . "#"; +echo $ref->getNumberOfRequiredParameters() . ":"; +$params = $ref->getParameters(); +foreach ($params as $param) { + echo $param->getName() . "#" . $param->getPosition(); + echo ($param->hasType() ? "T" : "t"); + echo ($param->isOptional() ? "O" : "R"); + echo ($param->isPassedByReference() ? "B" : "b"); + echo ($param->isVariadic() ? "V" : "v"); + echo "|"; +} +$named = new ReflectionFunction(function: "\\reflect_function_target"); +echo ":" . $named->getName(); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "reflect_function_target#4#2:id#0TRbv|name#1tRBv|mode#2TObv|rest#3TObV|:reflect_function_target" + ); +} + /// Verifies that ReflectionMethod objects returned from `ReflectionClass::getMethods()` /// carry the same parameter metadata as directly constructed method reflectors. #[test] diff --git a/tests/error_tests/classes_traits.rs b/tests/error_tests/classes_traits.rs index c328d2944c..9235678e61 100644 --- a/tests/error_tests/classes_traits.rs +++ b/tests/error_tests/classes_traits.rs @@ -572,6 +572,25 @@ fn test_error_reflection_parameter_constructor_dynamic_method_name() { ); } +/// Verifies that `new ReflectionFunction()` rejects unknown function targets. +#[test] +fn test_error_reflection_function_constructor_unknown_function() { + expect_error( + " Date: Fri, 19 Jun 2026 07:17:58 +0200 Subject: [PATCH 0396/1208] Support ReflectionFunction attributes --- docs/php/classes.md | 9 +- src/codegen/lower_inst/builtins/attributes.rs | 33 ++++- src/codegen/lower_inst/objects/reflection.rs | 6 +- src/codegen_support/reflection.rs | 51 +++++++- src/ir/function.rs | 6 +- src/ir_lower/function.rs | 20 ++- src/ir_lower/program.rs | 32 ++++- src/ir_lower/tests/exhaustive.rs | 2 + src/types/checker/builtin_types/reflection.rs | 16 ++- .../checker/inference/objects/constructors.rs | 2 +- .../objects/constructors/reflection.rs | 37 +++++- src/types/checker/mod.rs | 17 ++- src/types/checker/schema/classes/methods.rs | 3 +- src/types/checker/schema/classes/mod.rs | 2 +- .../checker/schema/classes/properties.rs | 3 +- src/types/mod.rs | 1 + src/types/result.rs | 6 +- src/types/schema.rs | 116 +++++++++++++++++- tests/codegen/oop/attributes.rs | 29 +++++ tests/error_tests/classes_traits.rs | 10 ++ 20 files changed, 361 insertions(+), 40 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index b0b55a3aa7..7af81d1ad3 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1125,6 +1125,11 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getProperties()` | `new ReflectionClass($class_name)` | Return `ReflectionProperty` objects for properties visible through the reflected class-like metadata | | `ReflectionClass::newInstance()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class with forwarded constructor arguments | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | +| `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user function name | +| `ReflectionFunction::getAttributes()` | `new ReflectionFunction($function_name)` | Return `ReflectionAttribute` objects for function attributes | +| `ReflectionFunction::getParameters()` | `new ReflectionFunction($function_name)` | Return `ReflectionParameter` objects for the reflected function parameters | +| `ReflectionFunction::getNumberOfParameters()` | `new ReflectionFunction($function_name)` | Return the total number of reflected function parameters | +| `ReflectionFunction::getNumberOfRequiredParameters()` | `new ReflectionFunction($function_name)` | Return the number of required reflected function parameters | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | | `ReflectionMethod::isStatic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is static | @@ -1207,7 +1212,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. -- `ReflectionFunction` supports `getName()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. +- `ReflectionFunction` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. - `ReflectionFunction` reflects named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1236,4 +1241,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()` for supported named types); method parameter names, counts, positions, optional/variadic/by-reference flags, and declared-type presence are exposed through the supported `ReflectionMethod`/`ReflectionParameter` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()` for supported named types); method parameter names, counts, positions, optional/variadic/by-reference flags, and declared-type presence are exposed through the supported `ReflectionMethod`/`ReflectionParameter` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). diff --git a/src/codegen/lower_inst/builtins/attributes.rs b/src/codegen/lower_inst/builtins/attributes.rs index aee9f45461..ca414d897c 100644 --- a/src/codegen/lower_inst/builtins/attributes.rs +++ b/src/codegen/lower_inst/builtins/attributes.rs @@ -12,7 +12,7 @@ use crate::codegen::abi; use crate::codegen::platform::Arch; use crate::codegen::{CodegenIrError, Result}; -use crate::ir::{Immediate, Instruction, Op, ValueDef, ValueId}; +use crate::ir::{Immediate, Instruction, Module, Op, ValueDef, ValueId}; use crate::names::php_symbol_key; use crate::types::{AttrArgEntry, AttrArgValue, ClassInfo, PhpType}; @@ -131,11 +131,15 @@ pub(in crate::codegen::lower_inst) fn emit_reflection_attribute_array( .get(idx) .and_then(|args| args.as_deref()) .unwrap_or(&[]); - let factory_id = crate::codegen::reflection::attribute_factory_id( - &ctx.module.class_infos, - attr_name, - attr_arg_list, - ); + let factory_id = { + let function_attrs = function_attribute_sources(ctx.module); + crate::codegen::reflection::attribute_factory_id_with_extra( + &ctx.module.class_infos, + &function_attrs, + attr_name, + attr_arg_list, + ) + }; abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); emit_reflection_attribute_object(ctx, &layout); @@ -149,6 +153,23 @@ pub(in crate::codegen::lower_inst) fn emit_reflection_attribute_array( Ok(()) } +/// Returns reflection-visible top-level function attribute metadata sources. +fn function_attribute_sources( + module: &Module, +) -> Vec> { + module + .functions + .iter() + .filter(|function| !function.attribute_names.is_empty()) + .map(|function| { + ( + function.attribute_names.as_slice(), + function.attribute_args.as_slice(), + ) + }) + .collect() +} + /// Returns the synthetic `ReflectionAttribute` class layout from EIR metadata. fn reflection_attribute_layout(ctx: &FunctionContext<'_>) -> Result { let info = ctx diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index c761316a67..081829c9a8 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -180,9 +180,7 @@ fn emit_reflection_owner_object( )?; } } - if class_name != "ReflectionFunction" { - emit_reflection_attrs_property(ctx, class_name, &metadata.attr_names, &metadata.attr_args)?; - } + emit_reflection_attrs_property(ctx, class_name, &metadata.attr_names, &metadata.attr_args)?; if class_name == "ReflectionClass" { emit_reflection_bool_property(ctx, "__is_final", metadata.is_final)?; emit_reflection_bool_property(ctx, "__is_abstract", metadata.is_abstract)?; @@ -778,6 +776,8 @@ fn reflection_function_metadata( }; let mut metadata = empty_reflection_metadata(); metadata.reflected_name = Some(function.name.trim_start_matches('\\').to_string()); + metadata.attr_names = function.attribute_names.clone(); + metadata.attr_args = function.attribute_args.clone(); metadata.parameter_members = reflection_parameter_members(signature); metadata.required_parameter_count = reflection_required_parameter_count(signature); Ok(metadata) diff --git a/src/codegen_support/reflection.rs b/src/codegen_support/reflection.rs index e9b3bdcbe3..3551e882d0 100644 --- a/src/codegen_support/reflection.rs +++ b/src/codegen_support/reflection.rs @@ -16,6 +16,10 @@ use crate::names::{php_symbol_key, Name}; use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, Stmt, StmtKind}; use crate::types::{AttrArgEntry, AttrArgValue, AttrKey, ClassInfo}; +/// Borrowed attribute-name/argument metadata from a reflection-visible source. +pub(crate) type AttributeMetadataSource<'a> = + (&'a [String], &'a [Option>]); + #[derive(Clone)] /// Factory record for compile-time reflection attribute metadata. /// `id` is assigned sequentially and must match across all compilation units @@ -51,6 +55,15 @@ pub(crate) fn resolve_class_name<'a>( /// sequential ids. pub(crate) fn collect_attribute_factories( classes: &HashMap, +) -> Vec { + collect_attribute_factories_with_extra(classes, &[]) +} + +/// Scans class metadata plus additional attribute metadata sources and collects +/// all distinct attribute name/argument pairs into deterministic factory records. +pub(crate) fn collect_attribute_factories_with_extra( + classes: &HashMap, + extra_attrs: &[AttributeMetadataSource<'_>], ) -> Vec { let mut unique = BTreeMap::new(); for class_info in classes.values() { @@ -76,6 +89,9 @@ pub(crate) fn collect_attribute_factories( } } } + for (names, args) in extra_attrs { + collect_from_attribute_lists(classes, names, args, &mut unique); + } unique .into_iter() @@ -98,6 +114,17 @@ pub(crate) fn attribute_factory_id( classes: &HashMap, attr_name: &str, attr_args: &[AttrArgEntry], +) -> i64 { + attribute_factory_id_with_extra(classes, &[], attr_name, attr_args) +} + +/// Returns the factory id for an attribute, considering classes plus extra +/// metadata sources such as top-level function attributes retained by EIR. +pub(crate) fn attribute_factory_id_with_extra( + classes: &HashMap, + extra_attrs: &[AttributeMetadataSource<'_>], + attr_name: &str, + attr_args: &[AttrArgEntry], ) -> i64 { // Non-class attributes are registered under their raw name (see // `collect_from_attribute_lists`), so fall back to it when the name does @@ -105,7 +132,7 @@ pub(crate) fn attribute_factory_id( let lookup_name = resolve_class_name(classes, attr_name) .map(|resolved| resolved.to_string()) .unwrap_or_else(|| attr_name.to_string()); - collect_attribute_factories(classes) + collect_attribute_factories_with_extra(classes, extra_attrs) .into_iter() .find(|factory| factory.class_name == lookup_name && factory.args == attr_args) .map(|factory| factory.id) @@ -114,8 +141,17 @@ pub(crate) fn attribute_factory_id( /// Builds the synthetic dispatch body for `ReflectionAttribute::newInstance()`. pub(crate) fn build_attribute_new_instance_body(classes: &HashMap) -> Vec { + build_attribute_new_instance_body_with_extra(classes, &[]) +} + +/// Builds the synthetic `ReflectionAttribute::newInstance()` body using class +/// metadata plus additional attribute metadata sources. +pub(crate) fn build_attribute_new_instance_body_with_extra( + classes: &HashMap, + extra_attrs: &[AttributeMetadataSource<'_>], +) -> Vec { let span = crate::span::Span::dummy(); - let factories = collect_attribute_factories(classes); + let factories = collect_attribute_factories_with_extra(classes, extra_attrs); let mut body = Vec::new(); for factory in factories { // Only resolvable attribute classes can be instantiated. Non-class @@ -264,9 +300,18 @@ fn attr_key_expr(key: &AttrKey) -> Expr { /// property populated at construction. pub(crate) fn build_attribute_get_arguments_body( classes: &HashMap, +) -> Vec { + build_attribute_get_arguments_body_with_extra(classes, &[]) +} + +/// Builds the synthetic `ReflectionAttribute::getArguments()` body using class +/// metadata plus additional attribute metadata sources. +pub(crate) fn build_attribute_get_arguments_body_with_extra( + classes: &HashMap, + extra_attrs: &[AttributeMetadataSource<'_>], ) -> Vec { let span = crate::span::Span::dummy(); - let factories = collect_attribute_factories(classes); + let factories = collect_attribute_factories_with_extra(classes, extra_attrs); let mut body = Vec::new(); for factory in factories { let condition = factory_condition(factory.id); diff --git a/src/ir/function.rs b/src/ir/function.rs index 4af360e57c..6ca48f1c99 100644 --- a/src/ir/function.rs +++ b/src/ir/function.rs @@ -14,7 +14,7 @@ use crate::ir::instr::{InstId, Instruction}; use crate::ir::types::IrType; use crate::ir::value::{Value, ValueId}; use crate::parser::ast::Stmt; -use crate::types::{FunctionSig, PhpType}; +use crate::types::{AttrArgEntry, FunctionSig, PhpType}; /// Module-local identifier for an EIR function. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] @@ -63,6 +63,8 @@ pub struct Function { pub entry: BlockId, pub source_signature: Option, pub signature: Option, + pub attribute_names: Vec, + pub attribute_args: Vec>>, pub generator_source: Option, pub flags: FunctionFlags, } @@ -83,6 +85,8 @@ impl Function { entry: BlockId::from_raw(0), source_signature: None, signature: None, + attribute_names: Vec::new(), + attribute_args: Vec::new(), generator_source: None, flags: FunctionFlags::default(), } diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 3d4a1eb5c9..7609f82061 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -18,9 +18,14 @@ use crate::ir_lower::context::{ StaticCallableBinding, }; use crate::ir_lower::effects_lookup; -use crate::parser::ast::{ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind, TypeExpr}; +use crate::parser::ast::{ + AttributeGroup, ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind, TypeExpr, +}; use crate::span::Span; -use crate::types::{CheckResult, ClassInfo, FunctionSig, PackedClassInfo, PhpType, TypeEnv}; +use crate::types::{ + collect_attribute_args, collect_attribute_names, CheckResult, ClassInfo, FunctionSig, + PackedClassInfo, PhpType, TypeEnv, +}; /// AST parameter tuple shape used by function, method, and closure declarations. type AstParams = [(String, Option, Option, bool)]; @@ -176,6 +181,7 @@ pub(crate) fn lower_user_function( name: &str, params: &AstParams, return_type: Option<&TypeExpr>, + attributes: &[AttributeGroup], body: &[Stmt], module: &mut Module, check_result: &CheckResult, @@ -205,6 +211,16 @@ pub(crate) fn lower_user_function( function.flags.by_ref_return = signature.by_ref_return; function.source_signature = Some(source_signature(name, &eir_signature)); function.signature = Some(eir_runtime_metadata_signature(&eir_signature)); + function.attribute_names = check_result + .function_attribute_names + .get(name) + .cloned() + .unwrap_or_else(|| collect_attribute_names(attributes)); + function.attribute_args = check_result + .function_attribute_args + .get(name) + .cloned() + .unwrap_or_else(|| collect_attribute_args(attributes)); attach_generator_source_if_needed(&mut function, body, eir_signature.params.len()); let closures = lower_body_into_function( &mut function, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 3720b92466..3318de9b63 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -585,6 +585,7 @@ fn lower_function_declarations( name, params, return_type.as_ref(), + &stmt.attributes, body, module, check_result, @@ -821,15 +822,21 @@ fn lower_builtin_reflection_class_methods( let generated_body; let method_key = crate::names::php_symbol_key(&method.name); let body = if class_name == "ReflectionAttribute" && method_key == "newinstance" { - generated_body = - crate::codegen::reflection::build_attribute_new_instance_body(&check_result.classes); + let function_attrs = function_attribute_sources(module); + generated_body = crate::codegen::reflection::build_attribute_new_instance_body_with_extra( + &check_result.classes, + &function_attrs, + ); generated_body.as_slice() } else if class_name == "ReflectionAttribute" && method_key == "getarguments" { // Materialize captured attribute arguments through the normal array // lowering (named arguments and associative arrays included) rather // than a bespoke codegen path. - generated_body = - crate::codegen::reflection::build_attribute_get_arguments_body(&check_result.classes); + let function_attrs = function_attribute_sources(module); + generated_body = crate::codegen::reflection::build_attribute_get_arguments_body_with_extra( + &check_result.classes, + &function_attrs, + ); generated_body.as_slice() } else { &method.body @@ -852,6 +859,23 @@ fn lower_builtin_reflection_class_methods( } } +/// Returns reflection-visible top-level function attribute metadata sources. +fn function_attribute_sources( + module: &Module, +) -> Vec> { + module + .functions + .iter() + .filter(|function| !function.attribute_names.is_empty()) + .map(|function| { + ( + function.attribute_names.as_slice(), + function.attribute_args.as_slice(), + ) + }) + .collect() +} + /// Lowers the small builtin SPL method slice currently consumed by the EIR backend. fn lower_referenced_builtin_spl_methods( module: &mut Module, diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index fa1bb7fd4c..f30a280ae4 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -120,6 +120,8 @@ fn dummy_check_result() -> CheckResult { CheckResult { global_env: HashMap::new(), functions, + function_attribute_names: HashMap::new(), + function_attribute_args: HashMap::new(), callable_param_sigs: HashMap::new(), callable_return_sigs: HashMap::new(), callable_array_return_sigs: HashMap::new(), diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 254814ed51..e2fe6b0430 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1344,15 +1344,13 @@ fn builtin_reflection_owner_class( "__required_parameter_count", )); } - if name != "ReflectionFunction" { - properties.push(builtin_property( - "__attrs", - Visibility::Private, - Some(array_type()), - empty_array(), - )); - methods.push(builtin_reflection_owner_get_attributes_method()); - } + properties.push(builtin_property( + "__attrs", + Visibility::Private, + Some(array_type()), + empty_array(), + )); + methods.push(builtin_reflection_owner_get_attributes_method()); FlattenedClass { name: name.to_string(), extends: None, diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 613b4e3cb3..5759d5ce79 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -318,7 +318,7 @@ impl Checker { .reflection_function_signature(&function_name)? .is_some() { - return Ok(()); + return self.validate_reflection_function_attrs(&function_name, expr); } Err(CompileError::new( expr.span, diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index 3d0325216f..cd8db6ab26 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -1,6 +1,6 @@ //! Purpose: //! Validates builtin Reflection owner constructor metadata for object inference. -//! Keeps ReflectionClass/Method/Property/Constant/EnumCase attribute checks out +//! Keeps ReflectionClass/Function/Method/Property/Constant/EnumCase attribute checks out //! of the general constructor inference driver. //! //! Called from: @@ -14,10 +14,45 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::Expr; use crate::types::checker::Checker; +use crate::types::{collect_attribute_args, collect_attribute_names}; type ReflectionAttributeArgs = Vec>>; impl Checker { + /// Validates function-level attributes for `ReflectionFunction`. + /// + /// The reflected function must be a statically declared user function; when + /// it has attributes, their captured argument metadata must be materializable + /// by `ReflectionAttribute::getArguments()`. + pub(super) fn validate_reflection_function_attrs( + &self, + function_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + let Some(canonical) = + self.canonical_function_name_folded(function_name.trim_start_matches('\\')) + else { + return Err(CompileError::new( + expr.span, + &format!( + "ReflectionFunction::__construct(): Function {}() does not exist", + function_name + ), + )); + }; + let Some(decl) = self.fn_decls.get(&canonical) else { + return Ok(()); + }; + let names = collect_attribute_names(&decl.attributes); + let args = collect_attribute_args(&decl.attributes); + self.validate_reflection_attribute_metadata( + &names, + &args, + expr, + "ReflectionFunction::getAttributes(): function has attribute argument metadata that is not supported yet", + ) + } + /// Validates class-level attributes for `ReflectionClass`. /// /// Returns `Ok` when the class exists and its captured attribute argument diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index 0077ae3713..55f80647cb 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -40,8 +40,9 @@ use crate::parser::ast::{ }; use crate::span::Span; use crate::types::{ - CheckResult, ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, - InterfaceInfo, PackedClassInfo, PhpType, ThrowAccessInfo, TypeEnv, + collect_attribute_args, collect_attribute_names, CheckResult, ClassInfo, EnumInfo, + ExternClassInfo, ExternFunctionSig, FunctionSig, InterfaceInfo, PackedClassInfo, PhpType, + ThrowAccessInfo, TypeEnv, }; pub use inference::{infer_expr_type_syntactic, infer_return_type_syntactic}; @@ -217,11 +218,23 @@ pub fn check_types( let mut warnings = crate::types::warnings::collect_warnings(program); warnings.extend(checker.warnings); + let function_attribute_names = checker + .fn_decls + .iter() + .map(|(name, decl)| (name.clone(), collect_attribute_names(&decl.attributes))) + .collect(); + let function_attribute_args = checker + .fn_decls + .iter() + .map(|(name, decl)| (name.clone(), collect_attribute_args(&decl.attributes))) + .collect(); dedupe_warnings(&mut warnings); Ok(CheckResult { global_env, functions: checker.functions, + function_attribute_names, + function_attribute_args, callable_param_sigs: checker.callable_param_sigs, callable_return_sigs: checker.callable_return_sigs, callable_array_return_sigs: checker.callable_array_return_sigs, diff --git a/src/types/checker/schema/classes/methods.rs b/src/types/checker/schema/classes/methods.rs index d152860095..a938c1d4df 100644 --- a/src/types/checker/schema/classes/methods.rs +++ b/src/types/checker/schema/classes/methods.rs @@ -18,7 +18,8 @@ use super::super::validation::{ build_method_sig, matches_global_builtin_attribute, validate_override_signature, visibility_rank, }; -use super::state::{collect_attribute_args, collect_attribute_names, ClassBuildState}; +use super::state::ClassBuildState; +use super::{collect_attribute_args, collect_attribute_names}; /// Validates and registers all methods of a flattened class into the build state. /// Enforces abstract/final modifiers, method body presence, and delegates to diff --git a/src/types/checker/schema/classes/mod.rs b/src/types/checker/schema/classes/mod.rs index e7ccc428b3..070aa730cd 100644 --- a/src/types/checker/schema/classes/mod.rs +++ b/src/types/checker/schema/classes/mod.rs @@ -25,7 +25,7 @@ use super::super::Checker; use super::validation::build_constructor_param_map; use state::ClassBuildState; -pub(super) use state::{collect_attribute_args, collect_attribute_names}; +pub(super) use crate::types::{collect_attribute_args, collect_attribute_names}; /// Recursively builds and registers `ClassInfo` for `class_name` and its inheritance chain. /// diff --git a/src/types/checker/schema/classes/properties.rs b/src/types/checker/schema/classes/properties.rs index 54a3122aaf..2c2cbe6cac 100644 --- a/src/types/checker/schema/classes/properties.rs +++ b/src/types/checker/schema/classes/properties.rs @@ -17,7 +17,8 @@ use crate::types::PhpType; use super::super::super::{infer_expr_type_syntactic, Checker}; use super::super::validation::visibility_rank; use super::super::interfaces::{build_property_contract, merge_property_contract}; -use super::state::{collect_attribute_args, collect_attribute_names, ClassBuildState}; +use super::state::ClassBuildState; +use super::{collect_attribute_args, collect_attribute_names}; /// Applies property schema validation and metadata for all static and instance /// properties declared in `class`. Static properties are validated for PHP diff --git a/src/types/mod.rs b/src/types/mod.rs index 2d34cda20e..fbb5baca80 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -55,6 +55,7 @@ pub use schema::{ ExternClassInfo, ExternFieldInfo, ExternFunctionSig, InterfaceInfo, PackedClassInfo, PackedFieldInfo, PropertyHookContract, }; +pub(crate) use schema::{collect_attribute_args, collect_attribute_names}; pub(crate) use signatures::{ builtin_call_sig, callable_wrapper_sig, first_class_callable_builtin_sig, }; diff --git a/src/types/result.rs b/src/types/result.rs index 97f548bfa6..9d6969e526 100644 --- a/src/types/result.rs +++ b/src/types/result.rs @@ -17,8 +17,8 @@ use crate::parser::ast::Program; use crate::span::Span; use super::{ - checker, ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, InterfaceInfo, - PackedClassInfo, PhpType, TypeEnv, + checker, AttrArgEntry, ClassInfo, EnumInfo, ExternClassInfo, ExternFunctionSig, FunctionSig, + InterfaceInfo, PackedClassInfo, PhpType, TypeEnv, }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -59,6 +59,8 @@ pub enum ThrowAccessKind { pub struct CheckResult { pub global_env: TypeEnv, pub functions: HashMap, + pub function_attribute_names: HashMap>, + pub function_attribute_args: HashMap>>>, pub callable_param_sigs: HashMap<(String, String), FunctionSig>, #[allow(dead_code)] pub callable_return_sigs: HashMap, diff --git a/src/types/schema.rs b/src/types/schema.rs index 3b262bfbf8..3c75022114 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -11,7 +11,7 @@ use std::collections::{HashMap, HashSet}; -use crate::parser::ast::{ClassMethod, Expr, Visibility}; +use crate::parser::ast::{AttributeGroup, ClassMethod, Expr, ExprKind, StaticReceiver, Visibility}; use crate::span::Span; use super::{FunctionSig, PhpType}; @@ -65,6 +65,120 @@ pub enum AttrKey { Str(String), } +/// Collects attribute names from attribute groups while preserving source order. +/// +/// Name resolution has already canonicalized fully-qualified names by the time +/// checker/codegen metadata uses this helper, so returned names match +/// `ReflectionAttribute::getName()` shape without synthetic leading slashes. +pub(crate) fn collect_attribute_names(groups: &[AttributeGroup]) -> Vec { + let mut out = Vec::new(); + for group in groups { + for attr in &group.attributes { + out.push(attr.name.as_str().to_string()); + } + } + out +} + +/// Collects materializable positional, named, and array attribute arguments in source order. +/// +/// Legal PHP attribute expressions outside the current literal subset are +/// represented as `None` so compilation can proceed until a reflection query +/// needs the missing payload and reports the unsupported metadata. +pub(crate) fn collect_attribute_args( + groups: &[AttributeGroup], +) -> Vec>> { + let mut out = Vec::new(); + for group in groups { + for attr in &group.attributes { + let mut entries = Vec::new(); + let mut supported = true; + for arg_expr in &attr.args { + let (key, value_expr) = match &arg_expr.kind { + ExprKind::NamedArg { name, value } => { + (Some(AttrKey::Str(name.clone())), value.as_ref()) + } + _ => (None, arg_expr), + }; + match fold_attr_value(value_expr) { + Some(value) => entries.push(AttrArgEntry { key, value }), + None => { + supported = false; + break; + } + } + } + out.push(if supported { Some(entries) } else { None }); + } + } + out +} + +/// Folds one attribute argument expression to retained reflection metadata. +fn fold_attr_value(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::StringLiteral(value) => Some(AttrArgValue::Str(value.clone())), + ExprKind::IntLiteral(value) => Some(AttrArgValue::Int(*value)), + ExprKind::FloatLiteral(value) => Some(AttrArgValue::Float(value.to_bits())), + ExprKind::BoolLiteral(value) => Some(AttrArgValue::Bool(*value)), + ExprKind::Null => Some(AttrArgValue::Null), + ExprKind::ConstRef(name) => Some(AttrArgValue::ConstRef(name.as_str().to_string())), + ExprKind::ScopedConstantAccess { receiver, name } => scoped_receiver_type_name(receiver) + .map(|type_name| AttrArgValue::ScopedConst(type_name, name.clone())), + ExprKind::ClassConstant { + receiver: StaticReceiver::Named(name), + } => Some(AttrArgValue::Str(name.as_str().to_string())), + ExprKind::ClassConstant { .. } => None, + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(n) => Some(AttrArgValue::Int(n.wrapping_neg())), + ExprKind::FloatLiteral(n) => Some(AttrArgValue::Float((-*n).to_bits())), + _ => None, + }, + ExprKind::ArrayLiteral(elements) => { + let mut entries = Vec::with_capacity(elements.len()); + for element in elements { + entries.push(AttrArgEntry { + key: None, + value: fold_attr_value(element)?, + }); + } + Some(AttrArgValue::Array(entries)) + } + ExprKind::ArrayLiteralAssoc(pairs) => { + let mut entries = Vec::with_capacity(pairs.len()); + for (key_expr, value_expr) in pairs { + entries.push(AttrArgEntry { + key: Some(fold_attr_key(key_expr)?), + value: fold_attr_value(value_expr)?, + }); + } + Some(AttrArgValue::Array(entries)) + } + _ => None, + } +} + +/// Folds one supported associative attribute array key. +fn fold_attr_key(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => Some(AttrKey::Int(*value)), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(n) => Some(AttrKey::Int(n.wrapping_neg())), + _ => None, + }, + ExprKind::StringLiteral(value) => Some(AttrKey::Str(value.clone())), + _ => None, + } +} + +/// Returns the canonical named receiver for class-constant attribute arguments. +fn scoped_receiver_type_name(receiver: &StaticReceiver) -> Option { + match receiver { + StaticReceiver::Named(name) => Some(name.as_str().to_string()), + StaticReceiver::Self_ | StaticReceiver::Static | StaticReceiver::Parent => None, + } +} + /// Property hook contract for `get`/`set` hook declarations in classes and interfaces. #[derive(Debug, Clone)] pub struct PropertyHookContract { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 35299eeef9..405e3212f2 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1896,6 +1896,35 @@ echo ":" . $named->getName(); ); } +/// Verifies `ReflectionFunction::getAttributes()` exposes function attributes +/// and assigns factory ids that make `ReflectionAttribute::newInstance()` work. +#[test] +fn test_reflection_function_get_attributes_returns_function_attributes() { + let out = compile_and_run( + r##"name . "#" . $this->rank; } +} +class FlagMarker {} + +#[FunctionMarker("target", 7), FlagMarker] +function reflected_function_attrs() {} + +$ref = new ReflectionFunction("REFLECTED_FUNCTION_ATTRS"); +$attrs = $ref->getAttributes(); +echo count($attrs) . "/"; +echo $attrs[0]->getName() . "/"; +echo $attrs[0]->getArguments()[0] . "/"; +echo $attrs[0]->getArguments()[1] . "/"; +echo $attrs[0]->newInstance()->label() . "/"; +echo $attrs[1]->getName() . "/"; +echo count($attrs[1]->getArguments()); +"##, + ); + assert_eq!(out, "2/FunctionMarker/target/7/target#7/FlagMarker/0"); +} + /// Verifies that ReflectionMethod objects returned from `ReflectionClass::getMethods()` /// carry the same parameter metadata as directly constructed method reflectors. #[test] diff --git a/tests/error_tests/classes_traits.rs b/tests/error_tests/classes_traits.rs index 9235678e61..80b3ac83e1 100644 --- a/tests/error_tests/classes_traits.rs +++ b/tests/error_tests/classes_traits.rs @@ -591,6 +591,16 @@ fn test_error_reflection_function_constructor_dynamic_function_name() { ); } +/// Verifies `ReflectionFunction` rejects attributes whose arguments cannot yet +/// be materialized into `ReflectionAttribute` metadata. +#[test] +fn test_error_reflection_function_get_attributes_unsupported_arg_metadata() { + expect_error( + " Date: Fri, 19 Jun 2026 07:28:33 +0200 Subject: [PATCH 0397/1208] Support __callStatic magic dispatch --- src/ir_lower/expr/mod.rs | 88 +++++------- .../checker/inference/objects/methods.rs | 125 ++++++++++-------- tests/codegen/objects/magic_methods.rs | 22 +++ tests/error_tests/exceptions_enums_magic.rs | 34 ++++- 4 files changed, 160 insertions(+), 109 deletions(-) diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index b1541d3a36..b5a4cb720a 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10245,19 +10245,23 @@ fn lower_static_method_call( return emit_closure_bind(ctx, closure.value, new_this.value, expr); } } - let sig = static_method_implementation_signature(ctx, receiver, method) - .or_else(|| lexical_instance_static_call_signature(ctx, receiver, method)) + + let magic_args; + let (dispatch_method, call_args) = if let Some(args) = + magic_static_call_dispatch_args(ctx, receiver, method, args, expr.span) + { + magic_args = args; + ("__callStatic", magic_args.as_slice()) + } else { + (method, args) + }; + let sig = static_method_implementation_signature(ctx, receiver, dispatch_method) + .or_else(|| lexical_instance_static_call_signature(ctx, receiver, dispatch_method)) .cloned(); - // PHP `__callStatic`: an undefined static method forwards to the class's - // `__callStatic($name, $args)` when the class declares one. - if sig.is_none() { - if let Some(class_name) = magic_callstatic_receiver_class(ctx, receiver, method) { - return lower_magic_callstatic(ctx, &class_name, method, args, expr); - } - } - let operands = lower_args_with_signature(ctx, sig.as_ref(), args); - let operands = coerce_int_backed_enum_string_argument(ctx, receiver, method, operands, expr); - let name = format!("{}::{}", receiver_name(receiver), method); + let operands = lower_args_with_signature(ctx, sig.as_ref(), call_args); + let operands = + coerce_int_backed_enum_string_argument(ctx, receiver, dispatch_method, operands, expr); + let name = format!("{}::{}", receiver_name(receiver), dispatch_method); let data = ctx.intern_string(&name); let result_type = sig .as_ref() @@ -10337,57 +10341,29 @@ fn coerce_int_backed_enum_string_argument( operands } -/// Resolves the class whose `__callStatic` should handle an otherwise-undefined -/// static call `Receiver::method(...)`. Returns `None` when the receiver already -/// has a real static method of that name or declares no `__callStatic`. -fn magic_callstatic_receiver_class( +/// Builds synthetic `__callStatic` arguments when a class lacks the requested static method. +fn magic_static_call_dispatch_args( ctx: &LoweringContext<'_, '_>, receiver: &StaticReceiver, method: &str, -) -> Option { + args: &[Expr], + span: Span, +) -> Option> { + if static_method_implementation_signature(ctx, receiver, method).is_some() + || lexical_instance_static_call_signature(ctx, receiver, method).is_some() + { + return None; + } let class_name = static_receiver_class_name(ctx, receiver)?; let class_info = ctx.classes.get(class_name.as_str())?; - if class_info.static_methods.contains_key(&php_symbol_key(method)) { + if class_info.methods.contains_key(&php_symbol_key(method)) { return None; } - class_info - .static_methods - .contains_key("__callstatic") - .then_some(class_name) -} - -/// Lowers `Class::method(args)` as a forward to `Class::__callStatic("method", [args])`. -fn lower_magic_callstatic( - ctx: &mut LoweringContext<'_, '_>, - class_name: &str, - method: &str, - args: &[Expr], - expr: &Expr, -) -> LoweredValue { - let sig = ctx - .classes - .get(class_name) - .and_then(|info| info.static_methods.get("__callstatic")) - .cloned(); - let magic_args = vec![ - Expr::new(ExprKind::StringLiteral(method.to_string()), expr.span), - Expr::new(ExprKind::ArrayLiteral(args.to_vec()), expr.span), - ]; - let operands = lower_args_with_signature(ctx, sig.as_ref(), &magic_args); - let name = format!("{}::__callStatic", class_name); - let data = ctx.intern_string(&name); - let result_type = sig - .as_ref() - .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) - .unwrap_or_else(|| fallback_expr_type(expr)); - ctx.emit_value( - Op::StaticMethodCall, - operands, - Some(Immediate::Data(data)), - result_type, - Op::StaticMethodCall.default_effects(), - Some(expr.span), - ) + static_method_implementation_signature(ctx, receiver, "__callStatic")?; + Some(vec![ + Expr::new(ExprKind::StringLiteral(method.to_string()), span), + Expr::new(ExprKind::ArrayLiteral(args.to_vec()), span), + ]) } /// Lowers a static-method callable-array call through a descriptor invoker. diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index a14567aa77..175082626f 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -37,9 +37,8 @@ impl Checker { let obj_ty = self.infer_type(object, env)?; if let PhpType::Object(class_name) = &obj_ty { if self.interfaces.contains_key(class_name) { - return self.infer_method_call_on_interface_type( - class_name, method, args, expr, env, - ); + return self + .infer_method_call_on_interface_type(class_name, method, args, expr, env); } return self.infer_method_call_on_class_type(class_name, method, args, expr, env); } @@ -281,12 +280,7 @@ impl Checker { env: &TypeEnv, ) -> Result { self.infer_method_call_on_class_type_with_options( - class_name, - method, - args, - expr, - env, - false, + class_name, method, args, expr, env, false, ) } @@ -300,14 +294,7 @@ impl Checker { expr: &Expr, env: &TypeEnv, ) -> Result { - self.infer_method_call_on_class_type_with_options( - class_name, - method, - args, - expr, - env, - true, - ) + self.infer_method_call_on_class_type_with_options(class_name, method, args, expr, env, true) } /// Shared implementation for class method call inference. @@ -404,8 +391,7 @@ impl Checker { } } else if let Some(sig) = class_info.methods.get("__call") { let magic_args = Self::magic_call_args(method, args, expr.span); - let declared_flags = - Self::declared_method_param_flags(class_info, "__call", false); + let declared_flags = Self::declared_method_param_flags(class_info, "__call", false); let mut effective_sig = Self::callable_sig_for_declared_params(sig, &declared_flags); Self::relax_magic_call_validation_sig(&mut effective_sig); @@ -551,7 +537,7 @@ impl Checker { /// Refines a `__callStatic($name, $args)` signature's array parameter from /// the actual static-call arguments, the static counterpart of /// `specialize_magic_call_signature`. - fn specialize_magic_callstatic_signature( + fn specialize_magic_static_call_signature( &mut self, class_name: &str, args: &[Expr], @@ -740,12 +726,18 @@ impl Checker { return self .check_enum_static_call(&enum_info, class_name, method, args, env, expr.span); } + let method_key = php_symbol_key(method); let normalized_args: Vec; + let mut magic_return_ty = None; + let mut magic_original_args = None; if let Some(class_info) = self.classes.get(class_name) { - if let Some(sig) = class_info.static_methods.get(method) { + if let Some(sig) = class_info.static_methods.get(&method_key) { if let Some(reason) = sig.deprecation.clone() { let message = if reason.is_empty() { - format!("Call to deprecated static method: {}::{}()", class_name, method) + format!( + "Call to deprecated static method: {}::{}()", + class_name, method + ) } else { format!( "Call to deprecated static method: {}::{}() — {}", @@ -755,10 +747,10 @@ impl Checker { self.warnings .push(crate::errors::CompileWarning::new(expr.span, &message)); } - if let Some(visibility) = class_info.static_method_visibilities.get(method) { + if let Some(visibility) = class_info.static_method_visibilities.get(&method_key) { let declaring_class = class_info .static_method_declaring_classes - .get(method) + .get(&method_key) .map(String::as_str) .unwrap_or(class_name); if !self.can_access_member(declaring_class, visibility) { @@ -773,8 +765,13 @@ impl Checker { )); } } - let declared_flags = Self::declared_method_param_flags(class_info, method, true); - let effective_sig = Self::callable_sig_for_declared_params(sig, &declared_flags); + let declared_flags = + Self::declared_method_param_flags(class_info, &method_key, true); + let mut effective_sig = + Self::callable_sig_for_declared_params(sig, &declared_flags); + if method_key == "__callstatic" { + Self::relax_magic_call_validation_sig(&mut effective_sig); + } normalized_args = self.normalize_named_call_args( &effective_sig, args, @@ -810,16 +807,16 @@ impl Checker { }, )); } - let sig = class_info.methods.get(method).ok_or_else(|| { + let sig = class_info.methods.get(&method_key).ok_or_else(|| { CompileError::new( expr.span, &format!("Undefined method: {}::{}", class_name, method), ) })?; - if let Some(visibility) = class_info.method_visibilities.get(method) { + if let Some(visibility) = class_info.method_visibilities.get(&method_key) { let declaring_class = class_info .method_declaring_classes - .get(method) + .get(&method_key) .map(String::as_str) .unwrap_or(class_name); if !self.can_access_member(declaring_class, visibility) { @@ -834,7 +831,8 @@ impl Checker { )); } } - let declared_flags = Self::declared_method_param_flags(class_info, method, false); + let declared_flags = + Self::declared_method_param_flags(class_info, &method_key, false); let effective_sig = Self::callable_sig_for_declared_params(sig, &declared_flags); normalized_args = self.normalize_named_call_args( &effective_sig, @@ -875,23 +873,7 @@ impl Checker { ), )?; } - } else if let Some(callstatic_sig) = - class_info.static_methods.get("__callstatic").cloned() - { - // Forward `Foo::missing(...)` to `Foo::__callStatic("missing", [...])`. - let magic_args = Self::magic_call_args(method, args, expr.span); - let mut validation_sig = callstatic_sig.clone(); - Self::relax_magic_call_validation_sig(&mut validation_sig); - self.check_known_callable_call( - &validation_sig, - &magic_args, - expr.span, - env, - &format!("Static method {}::__callStatic", class_name), - )?; - self.specialize_magic_callstatic_signature(class_name, args, env)?; - return Ok(callstatic_sig.return_type.clone()); - } else if class_info.methods.contains_key(method) { + } else if class_info.methods.contains_key(&method_key) { return Err(CompileError::new( expr.span, &format!( @@ -899,6 +881,39 @@ impl Checker { class_name, method ), )); + } else if let Some(sig) = class_info.static_methods.get("__callstatic") { + let magic_args = Self::magic_call_args(method, args, expr.span); + let declared_flags = + Self::declared_method_param_flags(class_info, "__callstatic", true); + let mut effective_sig = + Self::callable_sig_for_declared_params(sig, &declared_flags); + Self::relax_magic_call_validation_sig(&mut effective_sig); + normalized_args = self.normalize_named_call_args( + &effective_sig, + &magic_args, + expr.span, + &format!("Static method {}::__callStatic", class_name), + env, + )?; + if allow_by_ref_spread { + self.check_known_callable_call_allowing_by_ref_spread( + &effective_sig, + &normalized_args, + expr.span, + env, + &format!("Static method {}::__callStatic", class_name), + )?; + } else { + self.check_known_callable_call( + &effective_sig, + &normalized_args, + expr.span, + env, + &format!("Static method {}::__callStatic", class_name), + )?; + } + magic_return_ty = Some(effective_sig.return_type.clone()); + magic_original_args = Some(args.to_vec()); } else { return Err(CompileError::new( expr.span, @@ -911,6 +926,12 @@ impl Checker { &format!("Undefined class: {}", class_name), )); } + if let Some(return_ty) = magic_return_ty { + if let Some(args) = magic_original_args { + self.specialize_magic_static_call_signature(class_name, &args, env)?; + } + return Ok(return_ty); + } let mut arg_types = Vec::new(); for arg in &normalized_args { arg_types.push(self.infer_type(arg, env)?); @@ -919,7 +940,7 @@ impl Checker { let direct_impl_class_name = if parent_call || self_call { self.classes .get(class_name) - .and_then(|class_info| class_info.method_impl_classes.get(method)) + .and_then(|class_info| class_info.method_impl_classes.get(&method_key)) .cloned() .unwrap_or_else(|| class_name.to_string()) } else { @@ -928,10 +949,10 @@ impl Checker { let static_declared_flags = self .classes .get(class_name) - .map(|class_info| Self::declared_method_param_flags(class_info, method, true)) + .map(|class_info| Self::declared_method_param_flags(class_info, &method_key, true)) .unwrap_or_default(); if let Some(class_info) = self.classes.get_mut(class_name) { - if let Some(sig) = class_info.static_methods.get_mut(method) { + if let Some(sig) = class_info.static_methods.get_mut(&method_key) { let regular_param_count = if sig.variadic.is_some() { sig.params.len().saturating_sub(1) } else { @@ -988,12 +1009,12 @@ impl Checker { let instance_declared_flags = self .classes .get(&direct_impl_class_name) - .map(|class_info| Self::declared_method_param_flags(class_info, method, false)) + .map(|class_info| Self::declared_method_param_flags(class_info, &method_key, false)) .unwrap_or_default(); if let Some(sig) = self .classes .get_mut(&direct_impl_class_name) - .and_then(|class_info| class_info.methods.get_mut(method)) + .and_then(|class_info| class_info.methods.get_mut(&method_key)) { let regular_param_count = if sig.variadic.is_some() { sig.params.len().saturating_sub(1) diff --git a/tests/codegen/objects/magic_methods.rs b/tests/codegen/objects/magic_methods.rs index 118883edba..1409695d29 100644 --- a/tests/codegen/objects/magic_methods.rs +++ b/tests/codegen/objects/magic_methods.rs @@ -200,6 +200,28 @@ echo User::where("active", 1), "|", User::first(); assert_eq!(out, "where(2)|first(0)"); } +/// Verifies `__callStatic` does not intercept an existing static method. +#[test] +fn test_magic_callstatic_leaves_existing_static_method_direct() { + let out = compile_and_run( + r#"prop)` on an undeclared property dispatches to `__isset` /// and uses its boolean result for both present and absent names. #[test] diff --git a/tests/error_tests/exceptions_enums_magic.rs b/tests/error_tests/exceptions_enums_magic.rs index bc22f1d8ad..74aa7d003a 100644 --- a/tests/error_tests/exceptions_enums_magic.rs +++ b/tests/error_tests/exceptions_enums_magic.rs @@ -22,7 +22,9 @@ fn test_error_magic_method_contracts_collect_multiple_errors() { assert!( all.len() >= 2, "expected multiple magic method contract errors, got {:?}", - all.iter().map(|error| error.message.clone()).collect::>(), + all.iter() + .map(|error| error.message.clone()) + .collect::>(), ); } @@ -218,6 +220,36 @@ fn test_error_magic_call_must_be_public() { ); } +/// Verifies that non-static `__callStatic` reports +/// "Magic method must be static: Proxy::__callStatic". +#[test] +fn test_error_magic_call_static_must_be_static() { + expect_error( + " Date: Fri, 19 Jun 2026 07:51:15 +0200 Subject: [PATCH 0398/1208] Support __isset magic property probes --- docs/php/classes.md | 2 +- src/ir_lower/expr/mod.rs | 241 +++++++++++++++--- .../checker/builtin_types/magic_methods.rs | 50 +++- src/types/checker/builtins/arrays.rs | 38 ++- src/types/checker/inference/expr/effects.rs | 41 ++- src/types/checker/inference/objects/access.rs | 17 +- tests/codegen/objects/magic_methods.rs | 60 +++++ tests/error_tests/exceptions_enums_magic.rs | 60 +++-- 8 files changed, 428 insertions(+), 81 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 7af81d1ad3..51fcc4f228 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -815,7 +815,7 @@ echo sqlSortKeyword(SortDirection::Descending); // DESC - `__toString()` — string coercion - `__get($name)` — reading an undeclared property - `__set($name, $value)` — writing an undeclared property -- `__isset($name)` — `isset()`/`empty()` on an undeclared property +- `__isset($name)` — `isset()`/`empty()` on an undeclared or inaccessible property - `__unset($name)` — `unset()` of an undeclared property - `__invoke(...$args)` — calling an object directly - `__call($name, $args)` — intercepting missing instance methods diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index b5a4cb720a..bedcc72b5c 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -2058,30 +2058,9 @@ fn lower_lazy_isset_operand( } Some(lower_native_isset_offset_probe(ctx, array, index, arg)) } - // `isset($obj->prop)` on an undeclared property dispatches to `__isset`. - ExprKind::PropertyAccess { object, property } => { - property_existence_magic_class(ctx, object, property, "__isset")?; - let synthetic = Expr::new( - ExprKind::MethodCall { - object: object.clone(), - method: "__isset".to_string(), - args: vec![Expr::new(ExprKind::StringLiteral(property.clone()), arg.span)], - }, - arg.span, - ); - Some(lower_expr(ctx, &synthetic)) - } - ExprKind::NullsafePropertyAccess { object, property } => { - property_existence_magic_class(ctx, object, property, "__isset")?; - let synthetic = Expr::new( - ExprKind::NullsafeMethodCall { - object: object.clone(), - method: "__isset".to_string(), - args: vec![Expr::new(ExprKind::StringLiteral(property.clone()), arg.span)], - }, - arg.span, - ); - Some(lower_expr(ctx, &synthetic)) + ExprKind::PropertyAccess { object, property } + | ExprKind::NullsafePropertyAccess { object, property } => { + lower_lazy_property_isset_operand(ctx, object, property, arg) } // `isset($this)` inside a static closure always evaluates to `false` // because static closures have no `$this` binding. PHP allows this @@ -2211,9 +2190,8 @@ fn lazy_empty_magic_property_calls( } /// Returns the class whose `magic` method (`__isset`/`__unset`) should handle -/// `isset($obj->prop)` / `unset($obj->prop)`: an undeclared property on an -/// object whose class declares the magic method. Returns `None` (normal -/// handling) for declared properties or classes without the hook. +/// property existence/removal: a property that cannot be accessed normally on an +/// object whose class declares the magic method. fn property_existence_magic_class( ctx: &LoweringContext<'_, '_>, object: &Expr, @@ -2222,10 +2200,10 @@ fn property_existence_magic_class( ) -> Option { let class_name = instance_callable_object_class(ctx, object)?; let class_info = ctx.classes.get(&class_name)?; - if class_info.properties.iter().any(|(name, _)| name == property) { + if property_is_accessible_for_ir(ctx, &class_name, class_info, property) { return None; } - class_info.methods.contains_key(magic).then_some(class_name) + class_method_signature(ctx, &class_name, &php_symbol_key(magic)).map(|_| class_name) } /// Lowers native array/hash `isset($array[$key])` without reading the element value. @@ -2382,6 +2360,196 @@ fn array_access_expr_supports_native_isset_probe( matches!(ty, PhpType::Array(_) | PhpType::AssocArray { .. }) } +/// Lowers `isset($object->property)` without performing a normal property read first. +fn lower_lazy_property_isset_operand( + ctx: &mut LoweringContext<'_, '_>, + object: &Expr, + property: &str, + arg: &Expr, +) -> Option { + match property_isset_action(ctx, object, property)? { + IssetPropertyAction::Fallback => None, + IssetPropertyAction::Magic => { + let object = lower_expr(ctx, object); + Some(lower_magic_property_isset(ctx, object, property, arg)) + } + IssetPropertyAction::AlwaysFalse => { + lower_expr(ctx, object); + Some(emit_bool_literal(ctx, false, Some(arg.span))) + } + } +} + +/// Describes how `isset($object->property)` should be lowered for a known receiver class. +enum IssetPropertyAction { + Fallback, + Magic, + AlwaysFalse, +} + +/// Selects the PHP-visible `isset()` behavior for a statically known object property operand. +fn property_isset_action( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + property: &str, +) -> Option { + let (class_name, _) = isset_object_expr_class(ctx, object)?; + if is_builtin_stdclass_name(&class_name) { + return Some(IssetPropertyAction::Fallback); + } + let class_info = ctx.classes.get(class_name.as_str())?; + if class_info.allow_dynamic_properties { + return Some(IssetPropertyAction::Fallback); + } + if property_is_accessible_for_ir(ctx, &class_name, class_info, property) { + return Some(IssetPropertyAction::Fallback); + } + if class_method_signature(ctx, &class_name, &php_symbol_key("__isset")).is_some() { + Some(IssetPropertyAction::Magic) + } else { + Some(IssetPropertyAction::AlwaysFalse) + } +} + +/// Returns the single receiver class and whether that receiver may be null. +fn isset_object_expr_class(ctx: &LoweringContext<'_, '_>, object: &Expr) -> Option<(String, bool)> { + let ty = match &object.kind { + ExprKind::Variable(name) => ctx.local_type(name), + ExprKind::This => PhpType::Object(ctx.current_class.clone()?), + ExprKind::NewObject { class_name, .. } => PhpType::Object(class_name.to_string()), + ExprKind::NewDynamicObject { fallback_class, .. } => { + PhpType::Object(fallback_class.to_string()) + } + ExprKind::FunctionCall { name, .. } => ctx + .functions + .get(name.as_str()) + .map(|sig| sig.return_type.clone()) + .unwrap_or_else(|| infer_expr_type_syntactic(object)), + _ => infer_expr_type_syntactic(object), + }; + let (class_name, nullable) = singular_object_class(&ty)?; + normalized_class_name(class_name).map(|name| (name, nullable)) +} + +/// Returns whether a named property can use normal `isset()` value probing. +fn property_is_accessible_for_ir( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + class_info: &crate::types::ClassInfo, + property: &str, +) -> bool { + if class_info.visible_property(property).is_none() { + return false; + } + class_info + .property_visibilities + .get(property) + .is_none_or(|visibility| { + let declaring_class = class_info + .property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name); + ir_can_access_member(ctx, declaring_class, visibility) + }) +} + +/// Checks PHP member visibility from the current lowering class scope. +fn ir_can_access_member( + ctx: &LoweringContext<'_, '_>, + declaring_class: &str, + visibility: &Visibility, +) -> bool { + match visibility { + Visibility::Public => true, + Visibility::Private => ctx + .current_class + .as_deref() + .is_some_and(|current| same_php_class_name(current, declaring_class)), + Visibility::Protected => ctx.current_class.as_deref().is_some_and(|current| { + same_php_class_name(current, declaring_class) + || class_extends_class(ctx, current, declaring_class) + }), + } +} + +/// Returns true when two class metadata names match PHP's case-insensitive class lookup. +fn same_php_class_name(left: &str, right: &str) -> bool { + php_symbol_key(left.trim_start_matches('\\')) == php_symbol_key(right.trim_start_matches('\\')) +} + +/// Lowers a magic `__isset($name)` call and coerces the result to PHP boolean semantics. +fn lower_magic_property_isset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + arg: &Expr, +) -> LoweredValue { + if value_is_nullable(ctx, object.value) { + return lower_nullable_magic_property_isset(ctx, object, property, arg); + } + let args = vec![Expr::new( + ExprKind::StringLiteral(property.to_string()), + arg.span, + )]; + let result = + lower_method_call_with_receiver(ctx, object, "__isset", &args, Op::MethodCall, arg); + ctx.truthy(result, Some(arg.span)) +} + +/// Lowers `__isset` for nullable receivers, returning false instead of calling on null. +fn lower_nullable_magic_property_isset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + arg: &Expr, +) -> LoweredValue { + let temp_name = ctx.declare_hidden_temp(PhpType::Bool); + let null_block = ctx + .builder + .create_named_block("isset.property.null", Vec::new()); + let call_block = ctx + .builder + .create_named_block("isset.property.call", Vec::new()); + let merge = ctx + .builder + .create_named_block("isset.property.merge", Vec::new()); + let is_null = ctx.emit_value( + Op::IsNull, + vec![object.value], + None, + PhpType::Bool, + Op::IsNull.default_effects(), + Some(arg.span), + ); + ctx.builder.terminate(Terminator::CondBr { + cond: is_null.value, + then_target: null_block, + then_args: Vec::new(), + else_target: call_block, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(null_block); + let false_value = emit_bool_literal(ctx, false, Some(arg.span)); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, false_value, arg.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(call_block); + let args = vec![Expr::new( + ExprKind::StringLiteral(property.to_string()), + arg.span, + )]; + let result = + lower_method_call_with_receiver(ctx, object, "__isset", &args, Op::MethodCall, arg); + let result = ctx.truthy(result, Some(arg.span)); + store_value_into_temp(ctx, &temp_name, PhpType::Bool, result, arg.span); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); + ctx.load_local(&temp_name, Some(arg.span)) +} + /// Lowers direct function/static-method first-class callable probes for `is_callable()`. fn lower_static_is_callable( ctx: &mut LoweringContext<'_, '_>, @@ -8956,13 +9124,14 @@ fn reflection_parameter_method_owner_operand( span: owner.span, }) } - ExprKind::This => ctx - .current_class - .clone() - .map(|name| ReflectionParameterConstructorOperand::ClassName { - name, - span: owner.span, - }), + ExprKind::This => { + ctx.current_class + .clone() + .map(|name| ReflectionParameterConstructorOperand::ClassName { + name, + span: owner.span, + }) + } _ => Some(ReflectionParameterConstructorOperand::Expr(owner.clone())), } } diff --git a/src/types/checker/builtin_types/magic_methods.rs b/src/types/checker/builtin_types/magic_methods.rs index b0b2d1667f..aec3f210b6 100644 --- a/src/types/checker/builtin_types/magic_methods.rs +++ b/src/types/checker/builtin_types/magic_methods.rs @@ -11,7 +11,7 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; -use crate::parser::ast::Visibility; +use crate::parser::ast::{TypeExpr, Visibility}; use crate::types::PhpType; use super::super::Checker; @@ -23,6 +23,7 @@ use super::super::Checker; /// For `__set`: parameter 0 is `PhpType::Str`, parameter 1 is `PhpType::Mixed`. /// For `__call`/`__callStatic`: parameter 0 is `PhpType::Str`, parameter 1 is /// `PhpType::Array` of `PhpType::Never` (the forwarded argument list). +/// Declared `__isset` return types are validated separately. /// Does nothing for classes that do not declare these methods. pub(crate) fn patch_magic_method_signatures(checker: &mut Checker) { for class_info in checker.classes.values_mut() { @@ -41,6 +42,11 @@ pub(crate) fn patch_magic_method_signatures(checker: &mut Checker) { param.1 = PhpType::Mixed; } } + if let Some(sig) = class_info.methods.get_mut("__isset") { + if let Some(param) = sig.params.get_mut(0) { + param.1 = PhpType::Str; + } + } if let Some(sig) = class_info.methods.get_mut("__call") { if let Some(param) = sig.params.get_mut(0) { param.1 = PhpType::Str; @@ -159,6 +165,39 @@ pub(crate) fn validate_magic_method_contracts(checker: &Checker) -> Result<(), C )); } } + "__isset" => { + if method.is_static { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be non-static: {}::__isset", class_name), + )); + continue; + } + if method.visibility != Visibility::Public { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be public: {}::__isset", class_name), + )); + continue; + } + if method.params.len() != 1 || method.variadic.is_some() { + errors.push(CompileError::new( + method.span, + &format!("Magic method must take 1 argument: {}::__isset", class_name), + )); + continue; + } + if method + .return_type + .as_ref() + .is_some_and(|return_type| !matches!(return_type, TypeExpr::Bool)) + { + errors.push(CompileError::new( + method.span, + &format!("Magic method must return bool: {}::__isset", class_name), + )); + } + } "__call" => { if method.is_static { errors.push(CompileError::new( @@ -220,28 +259,27 @@ pub(crate) fn validate_magic_method_contracts(checker: &Checker) -> Result<(), C )); } } - magic @ ("__isset" | "__unset") => { + "__unset" => { // `isset($obj->prop)`/`unset($obj->prop)` on an undeclared // property dispatch here; both take the property name only. - let pretty = if magic == "__isset" { "__isset" } else { "__unset" }; if method.is_static { errors.push(CompileError::new( method.span, - &format!("Magic method must be non-static: {}::{}", class_name, pretty), + &format!("Magic method must be non-static: {}::__unset", class_name), )); continue; } if method.visibility != Visibility::Public { errors.push(CompileError::new( method.span, - &format!("Magic method must be public: {}::{}", class_name, pretty), + &format!("Magic method must be public: {}::__unset", class_name), )); continue; } if method.params.len() != 1 || method.variadic.is_some() { errors.push(CompileError::new( method.span, - &format!("Magic method must take 1 argument: {}::{}", class_name, pretty), + &format!("Magic method must take 1 argument: {}::__unset", class_name), )); } } diff --git a/src/types/checker/builtins/arrays.rs b/src/types/checker/builtins/arrays.rs index 0667b98407..2ca794e653 100644 --- a/src/types/checker/builtins/arrays.rs +++ b/src/types/checker/builtins/arrays.rs @@ -9,7 +9,7 @@ //! - Signatures, callable aliases, optimizer effects, and codegen builtin dispatch must remain in lockstep. use crate::errors::CompileError; -use crate::parser::ast::Expr; +use crate::parser::ast::{Expr, ExprKind}; use crate::types::{PhpType, TypeEnv}; use super::super::Checker; @@ -46,16 +46,7 @@ pub(super) fn check_builtin( return Err(CompileError::new(span, "isset() takes at least 1 argument")); } for arg in args { - // `isset($obj->prop)` on an undeclared property dispatches to - // `__isset`; the helper infers the receiver but skips the bare - // property access that would otherwise reject the property. - if checker - .isset_unset_property_magic_class(arg, "__isset", env)? - .is_some() - { - continue; - } - checker.infer_type(arg, env)?; + check_isset_arg(checker, arg, env)?; } Ok(Some(PhpType::Bool)) } @@ -63,6 +54,31 @@ pub(super) fn check_builtin( } } +/// Type-checks one `isset()` operand while preserving PHP's non-reading property semantics. +fn check_isset_arg(checker: &mut Checker, arg: &Expr, env: &TypeEnv) -> Result<(), CompileError> { + if let ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } = &arg.kind + { + let object_ty = checker.infer_type(object, env)?; + if isset_object_receiver_type(checker, &object_ty) { + return Ok(()); + } + } + checker.infer_type(arg, env).map(|_| ()) +} + +/// Returns true when `isset($object->property)` can be checked without reading the property. +fn isset_object_receiver_type(checker: &Checker, ty: &PhpType) -> bool { + match ty { + PhpType::Object(_) | PhpType::Mixed => true, + PhpType::Union(members) => { + checker.union_single_object_class(ty).is_some() + || members.iter().any(|member| matches!(member, PhpType::Mixed)) + } + _ => false, + } +} + /// Returns `true` if a `PhpType` is a countable array type for Union membership checks. /// /// Used by `crate::builtins::array::count` to test whether every branch of a Union type diff --git a/src/types/checker/inference/expr/effects.rs b/src/types/checker/inference/expr/effects.rs index 9b3c842c66..191d6e2c1a 100644 --- a/src/types/checker/inference/expr/effects.rs +++ b/src/types/checker/inference/expr/effects.rs @@ -219,9 +219,11 @@ impl Checker { // an undeclared property routed to `__isset`/`__unset`, which must // not be inferred as a bare property access here. The call's own // inference handles the operands (with magic routing). - let is_lazy_construct = builtin_name.eq_ignore_ascii_case("isset") - || builtin_name.eq_ignore_ascii_case("unset"); - if !is_lazy_construct { + if builtin_name.eq_ignore_ascii_case("isset") { + for arg in &expanded_args { + self.infer_isset_arg_assignment_effects(arg, env)?; + } + } else if !builtin_name.eq_ignore_ascii_case("unset") { for (idx, arg) in expanded_args.iter().enumerate() { if builtin_name.eq_ignore_ascii_case("preg_replace_callback") && idx == 1 { continue; @@ -359,6 +361,39 @@ impl Checker { } } + /// Infers effects for one `isset()` operand without treating object properties as reads. + fn infer_isset_arg_assignment_effects( + &mut self, + arg: &Expr, + env: &mut TypeEnv, + ) -> Result<(), CompileError> { + match &arg.kind { + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => { + self.infer_type_with_assignment_effects(object, env)?; + Ok(()) + } + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + self.infer_type_with_assignment_effects(object, env)?; + self.infer_type_with_assignment_effects(property, env)?; + Ok(()) + } + ExprKind::ArrayAccess { array, index } => { + self.infer_type_with_assignment_effects(array, env)?; + self.infer_type_with_assignment_effects(index, env)?; + Ok(()) + } + ExprKind::NamedArg { value, .. } => { + self.infer_isset_arg_assignment_effects(value, env) + } + _ => { + self.infer_type_with_assignment_effects(arg, env)?; + Ok(()) + } + } + } + /// Returns true when an expression call target is first-class `preg_replace_callback`. fn expr_targets_preg_replace_callback(&self, callee: &Expr) -> bool { match &callee.kind { diff --git a/src/types/checker/inference/objects/access.rs b/src/types/checker/inference/objects/access.rs index 4c5996a9e1..51367821f4 100644 --- a/src/types/checker/inference/objects/access.rs +++ b/src/types/checker/inference/objects/access.rs @@ -114,10 +114,10 @@ impl Checker { /// Returns the class whose `magic` method (`__isset` or `__unset`) should /// handle `isset($obj->prop)` / `unset($obj->prop)`: that is, when `$obj` is - /// an object whose class declares `magic` and `prop` is not a declared - /// property. Infers (and type-checks) the receiver object as a side effect, + /// an object whose class declares `magic` and `prop` is not normally + /// accessible. Infers (and type-checks) the receiver object as a side effect, /// so callers can skip inferring the bare property access — which would - /// otherwise reject the undeclared property before the magic call is reached. + /// otherwise reject the property before the magic call is reached. pub(crate) fn isset_unset_property_magic_class( &mut self, arg: &Expr, @@ -136,7 +136,16 @@ impl Checker { let Some(class_info) = self.classes.get(&normalized) else { return Ok(None); }; - if class_info.properties.iter().any(|(name, _)| name == property) { + if let Some(visibility) = class_info.property_visibilities.get(property) { + let declaring_class = class_info + .property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(normalized.as_str()); + if self.can_access_member(declaring_class, visibility) { + return Ok(None); + } + } else if class_info.visible_property(property).is_some() { return Ok(None); } Ok(class_info.methods.contains_key(magic).then_some(normalized)) diff --git a/tests/codegen/objects/magic_methods.rs b/tests/codegen/objects/magic_methods.rs index 1409695d29..1e5310868b 100644 --- a/tests/codegen/objects/magic_methods.rs +++ b/tests/codegen/objects/magic_methods.rs @@ -119,6 +119,66 @@ echo $m->answer; assert_eq!(out, "answer:99|answer"); } +/// Verifies `__isset` is invoked for undefined property probes and its result is coerced to bool. +#[test] +fn test_magic_isset_handles_missing_property_probes() { + let out = compile_and_run( + r#"enabled) ? "Y" : "N"; +echo ":"; +echo isset($p->disabled) ? "Y" : "N"; +"#, + ); + assert_eq!(out, "check:enabled;Y:check:disabled;N"); +} + +/// Verifies `isset()` does not read an undefined property through `__get`. +#[test] +fn test_magic_isset_missing_property_does_not_call_magic_get() { + let out = compile_and_run( + r#"missing) ? "Y" : "N"; +"#, + ); + assert_eq!(out, "N"); +} + +/// Verifies `__isset` is invoked for inaccessible property probes from outside the class. +#[test] +fn test_magic_isset_handles_inaccessible_property_probes() { + let out = compile_and_run( + r#"token) ? "Y" : "N"; +"#, + ); + assert_eq!(out, "magic:token;Y"); +} + /// Verifies `__invoke` is called when an object is invoked as a function via a variable holding the object. #[test] fn test_magic_invoke_handles_variable_object_call() { diff --git a/tests/error_tests/exceptions_enums_magic.rs b/tests/error_tests/exceptions_enums_magic.rs index 74aa7d003a..305b05b11d 100644 --- a/tests/error_tests/exceptions_enums_magic.rs +++ b/tests/error_tests/exceptions_enums_magic.rs @@ -200,6 +200,46 @@ fn test_error_magic_set_must_take_two_arguments() { ); } +/// Verifies that a `static` `__isset` reports +/// "Magic method must be non-static: Bag::__isset". +#[test] +fn test_error_magic_isset_must_be_non_static() { + expect_error( + " Date: Fri, 19 Jun 2026 08:00:57 +0200 Subject: [PATCH 0399/1208] Support __unset magic property targets --- docs/php/classes.md | 2 +- src/ir_lower/expr/mod.rs | 153 +++++++++++++++--- .../checker/builtin_types/magic_methods.rs | 64 +++++--- src/types/checker/builtins/mod.rs | 3 +- src/types/checker/builtins/numeric.rs | 100 ++++++++++-- src/types/checker/inference/expr/effects.rs | 13 +- tests/codegen/objects/magic_methods.rs | 70 ++++++++ tests/error_tests/exceptions_enums_magic.rs | 61 +++++-- 8 files changed, 390 insertions(+), 76 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 51fcc4f228..b5cce833d9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -816,7 +816,7 @@ echo sqlSortKeyword(SortDirection::Descending); // DESC - `__get($name)` — reading an undeclared property - `__set($name, $value)` — writing an undeclared property - `__isset($name)` — `isset()`/`empty()` on an undeclared or inaccessible property -- `__unset($name)` — `unset()` of an undeclared property +- `__unset($name)` — `unset()` of an undeclared or inaccessible property - `__invoke(...$args)` — calling an object directly - `__call($name, $args)` — intercepting missing instance methods - `__callStatic($name, $args)` — intercepting missing static methods diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index bedcc72b5c..a39269ad96 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -4181,28 +4181,9 @@ fn lower_unset_locals( ExprKind::ArrayAccess { array, index } => { lower_unset_array_access(ctx, array, index, arg); } - // `unset($obj->prop)` on an undeclared property dispatches to `__unset`. - ExprKind::PropertyAccess { object, property } => { - let synthetic = Expr::new( - ExprKind::MethodCall { - object: object.clone(), - method: "__unset".to_string(), - args: vec![Expr::new(ExprKind::StringLiteral(property.clone()), arg.span)], - }, - arg.span, - ); - lower_expr(ctx, &synthetic); - } - ExprKind::NullsafePropertyAccess { object, property } => { - let synthetic = Expr::new( - ExprKind::NullsafeMethodCall { - object: object.clone(), - method: "__unset".to_string(), - args: vec![Expr::new(ExprKind::StringLiteral(property.clone()), arg.span)], - }, - arg.span, - ); - lower_expr(ctx, &synthetic); + ExprKind::PropertyAccess { object, property } + | ExprKind::NullsafePropertyAccess { object, property } => { + lower_unset_property_access(ctx, object, property, arg); } _ => {} } @@ -4221,7 +4202,7 @@ fn unset_target_supported(ctx: &LoweringContext<'_, '_>, arg: &Expr) -> bool { } ExprKind::PropertyAccess { object, property } | ExprKind::NullsafePropertyAccess { object, property } => { - property_existence_magic_class(ctx, object, property, "__unset").is_some() + unset_property_access_has_direct_lowering(ctx, object, property) } _ => false, } @@ -4363,6 +4344,132 @@ fn lower_unset_indexed_element( lower_unset_hash_element(ctx, name, array_span, index, expr); } +/// Returns true when a property unset target can be lowered without normal property storage support. +fn unset_property_access_has_direct_lowering( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + property: &str, +) -> bool { + matches!( + property_unset_action(ctx, object, property), + Some(UnsetPropertyAction::Magic | UnsetPropertyAction::Noop) + ) +} + +/// Lowers `unset($object->property)` for magic and no-op property targets. +fn lower_unset_property_access( + ctx: &mut LoweringContext<'_, '_>, + object: &Expr, + property: &str, + expr: &Expr, +) { + match property_unset_action(ctx, object, property) { + Some(UnsetPropertyAction::Magic) => { + let object = lower_expr(ctx, object); + lower_magic_property_unset(ctx, object, property, expr); + } + Some(UnsetPropertyAction::Noop) => { + lower_expr(ctx, object); + } + Some(UnsetPropertyAction::Fallback) | None => {} + } +} + +/// Describes how `unset($object->property)` should be lowered for a known receiver class. +enum UnsetPropertyAction { + Fallback, + Magic, + Noop, +} + +/// Selects the PHP-visible `unset()` behavior for a statically known object property operand. +fn property_unset_action( + ctx: &LoweringContext<'_, '_>, + object: &Expr, + property: &str, +) -> Option { + let (class_name, _) = isset_object_expr_class(ctx, object)?; + if is_builtin_stdclass_name(&class_name) { + return Some(UnsetPropertyAction::Fallback); + } + let class_info = ctx.classes.get(class_name.as_str())?; + if class_info.allow_dynamic_properties { + return Some(UnsetPropertyAction::Fallback); + } + if property_is_accessible_for_ir(ctx, &class_name, class_info, property) { + return Some(UnsetPropertyAction::Fallback); + } + if class_method_signature(ctx, &class_name, &php_symbol_key("__unset")).is_some() { + Some(UnsetPropertyAction::Magic) + } else { + Some(UnsetPropertyAction::Noop) + } +} + +/// Lowers a magic `__unset($name)` call, guarding nullable receivers as a no-op. +fn lower_magic_property_unset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + expr: &Expr, +) { + if value_is_nullable(ctx, object.value) { + lower_nullable_magic_property_unset(ctx, object, property, expr); + return; + } + let args = vec![Expr::new( + ExprKind::StringLiteral(property.to_string()), + expr.span, + )]; + lower_method_call_with_receiver(ctx, object, "__unset", &args, Op::MethodCall, expr); +} + +/// Lowers `__unset` for nullable receivers, doing nothing when the receiver is null. +fn lower_nullable_magic_property_unset( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + property: &str, + expr: &Expr, +) { + let null_block = ctx + .builder + .create_named_block("unset.property.null", Vec::new()); + let call_block = ctx + .builder + .create_named_block("unset.property.call", Vec::new()); + let merge = ctx + .builder + .create_named_block("unset.property.merge", Vec::new()); + let is_null = ctx.emit_value( + Op::IsNull, + vec![object.value], + None, + PhpType::Bool, + Op::IsNull.default_effects(), + Some(expr.span), + ); + ctx.builder.terminate(Terminator::CondBr { + cond: is_null.value, + then_target: null_block, + then_args: Vec::new(), + else_target: call_block, + else_args: Vec::new(), + }); + + ctx.builder.position_at_end(null_block); + branch_to(ctx, merge); + + ctx.builder.position_at_end(call_block); + let args = vec![Expr::new( + ExprKind::StringLiteral(property.to_string()), + expr.span, + )]; + lower_method_call_with_receiver(ctx, object, "__unset", &args, Op::MethodCall, expr); + branch_to(ctx, merge); + + ctx.builder.position_at_end(merge); +} + /// Lowers `array_push($local, $value)` as a direct indexed-array mutation. fn lower_static_array_push( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/types/checker/builtin_types/magic_methods.rs b/src/types/checker/builtin_types/magic_methods.rs index aec3f210b6..1fb720fe4f 100644 --- a/src/types/checker/builtin_types/magic_methods.rs +++ b/src/types/checker/builtin_types/magic_methods.rs @@ -23,7 +23,7 @@ use super::super::Checker; /// For `__set`: parameter 0 is `PhpType::Str`, parameter 1 is `PhpType::Mixed`. /// For `__call`/`__callStatic`: parameter 0 is `PhpType::Str`, parameter 1 is /// `PhpType::Array` of `PhpType::Never` (the forwarded argument list). -/// Declared `__isset` return types are validated separately. +/// Declared `__isset`/`__unset` return types are validated separately. /// Does nothing for classes that do not declare these methods. pub(crate) fn patch_magic_method_signatures(checker: &mut Checker) { for class_info in checker.classes.values_mut() { @@ -47,6 +47,11 @@ pub(crate) fn patch_magic_method_signatures(checker: &mut Checker) { param.1 = PhpType::Str; } } + if let Some(sig) = class_info.methods.get_mut("__unset") { + if let Some(param) = sig.params.get_mut(0) { + param.1 = PhpType::Str; + } + } if let Some(sig) = class_info.methods.get_mut("__call") { if let Some(param) = sig.params.get_mut(0) { param.1 = PhpType::Str; @@ -198,6 +203,39 @@ pub(crate) fn validate_magic_method_contracts(checker: &Checker) -> Result<(), C )); } } + "__unset" => { + if method.is_static { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be non-static: {}::__unset", class_name), + )); + continue; + } + if method.visibility != Visibility::Public { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be public: {}::__unset", class_name), + )); + continue; + } + if method.params.len() != 1 || method.variadic.is_some() { + errors.push(CompileError::new( + method.span, + &format!("Magic method must take 1 argument: {}::__unset", class_name), + )); + continue; + } + if method + .return_type + .as_ref() + .is_some_and(|return_type| !matches!(return_type, TypeExpr::Void)) + { + errors.push(CompileError::new( + method.span, + &format!("Magic method must return void: {}::__unset", class_name), + )); + } + } "__call" => { if method.is_static { errors.push(CompileError::new( @@ -259,30 +297,6 @@ pub(crate) fn validate_magic_method_contracts(checker: &Checker) -> Result<(), C )); } } - "__unset" => { - // `isset($obj->prop)`/`unset($obj->prop)` on an undeclared - // property dispatch here; both take the property name only. - if method.is_static { - errors.push(CompileError::new( - method.span, - &format!("Magic method must be non-static: {}::__unset", class_name), - )); - continue; - } - if method.visibility != Visibility::Public { - errors.push(CompileError::new( - method.span, - &format!("Magic method must be public: {}::__unset", class_name), - )); - continue; - } - if method.params.len() != 1 || method.variadic.is_some() { - errors.push(CompileError::new( - method.span, - &format!("Magic method must take 1 argument: {}::__unset", class_name), - )); - } - } "__callstatic" => { // Unlike the other interception hooks, `__callStatic` must be // declared `public static` (PHP invokes it in a static context). diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index cd89cd1fd0..6b3e15ff68 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -65,7 +65,8 @@ impl Checker { // undeclared property routed to `__isset`/`__unset`, which must not be // eagerly inferred by argument normalization. Their handlers inspect the // raw operands directly. - let is_lazy_construct = matches!(name, "isset" | "unset"); + let builtin_key = crate::names::php_symbol_key(name.trim_start_matches('\\')); + let is_lazy_construct = matches!(builtin_key.as_str(), "isset" | "unset"); let normalized_args; let args = if let Some(sig) = (!is_lazy_construct).then(|| crate::types::builtin_call_sig(name)).flatten() diff --git a/src/types/checker/builtins/numeric.rs b/src/types/checker/builtins/numeric.rs index 264e037242..42dcc33cd1 100644 --- a/src/types/checker/builtins/numeric.rs +++ b/src/types/checker/builtins/numeric.rs @@ -89,16 +89,7 @@ pub(super) fn check_builtin( return Err(CompileError::new(span, "unset() takes at least 1 argument")); } for arg in args { - // `unset($obj->prop)` on an undeclared property dispatches to - // `__unset`; the helper infers the receiver but skips the bare - // property access that would otherwise reject the property. - if checker - .isset_unset_property_magic_class(arg, "__unset", env)? - .is_some() - { - continue; - } - checker.infer_type(arg, env)?; + check_unset_arg(checker, arg, env)?; } Ok(Some(PhpType::Void)) } @@ -160,3 +151,92 @@ pub(super) fn check_builtin( _ => Ok(None), } } + +/// Type-checks one `unset()` operand while preserving PHP's non-reading property semantics. +fn check_unset_arg(checker: &mut Checker, arg: &Expr, env: &TypeEnv) -> Result<(), CompileError> { + if let ExprKind::PropertyAccess { object, property } + | ExprKind::NullsafePropertyAccess { object, property } = &arg.kind + { + let object_ty = checker.infer_type(object, env)?; + if unset_object_property_probe_is_valid(checker, &object_ty, property, arg)? { + return Ok(()); + } + } + checker.infer_type(arg, env).map(|_| ()) +} + +/// Returns true when `unset($object->property)` can be checked without reading the property. +fn unset_object_property_probe_is_valid( + checker: &Checker, + object_ty: &PhpType, + property: &str, + arg: &Expr, +) -> Result { + match object_ty { + PhpType::Object(class_name) => { + unset_property_probe_is_valid_on_class(checker, class_name, property, arg) + } + PhpType::Mixed => Ok(true), + PhpType::Union(members) => { + if let Some(class_name) = checker.union_single_object_class(object_ty) { + unset_property_probe_is_valid_on_class(checker, &class_name, property, arg) + } else { + Ok(members.iter().any(|member| matches!(member, PhpType::Mixed))) + } + } + _ => Ok(false), + } +} + +/// Checks one known receiver class for PHP `unset($object->property)` magic/no-op legality. +fn unset_property_probe_is_valid_on_class( + checker: &Checker, + class_name: &str, + property: &str, + arg: &Expr, +) -> Result { + if crate::types::checker::builtin_stdclass::is_stdclass(class_name) { + return Ok(true); + } + let Some(class_info) = checker.classes.get(class_name) else { + return Ok(false); + }; + if let Some(visibility) = class_info.property_visibilities.get(property) { + let declaring_class = class_info + .property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name); + if !checker.can_access_member(declaring_class, visibility) { + if class_info.methods.contains_key("__unset") { + return Ok(true); + } + return Err(CompileError::new( + arg.span, + &format!( + "Cannot access {} property: {}::{}", + Checker::visibility_label(visibility), + class_name, + property + ), + )); + } + return Ok(false); + } + Ok(true) +} + +/// Returns the most precise supported result type for `abs($value)`. +fn abs_result_type(ty: &PhpType) -> PhpType { + match ty { + PhpType::Float => PhpType::Float, + PhpType::Mixed => PhpType::Mixed, + PhpType::Union(members) if members.iter().any(|member| *member == PhpType::Float) => { + PhpType::Mixed + } + PhpType::Union(members) if members.iter().any(|member| *member == PhpType::Mixed) => { + PhpType::Mixed + } + _ => PhpType::Int, + } +} diff --git a/src/types/checker/inference/expr/effects.rs b/src/types/checker/inference/expr/effects.rs index 191d6e2c1a..171bd6280b 100644 --- a/src/types/checker/inference/expr/effects.rs +++ b/src/types/checker/inference/expr/effects.rs @@ -219,9 +219,12 @@ impl Checker { // an undeclared property routed to `__isset`/`__unset`, which must // not be inferred as a bare property access here. The call's own // inference handles the operands (with magic routing). - if builtin_name.eq_ignore_ascii_case("isset") { + if matches!( + php_symbol_key(builtin_name).as_str(), + "isset" | "unset" + ) { for arg in &expanded_args { - self.infer_isset_arg_assignment_effects(arg, env)?; + self.infer_non_reading_arg_assignment_effects(arg, env)?; } } else if !builtin_name.eq_ignore_ascii_case("unset") { for (idx, arg) in expanded_args.iter().enumerate() { @@ -361,8 +364,8 @@ impl Checker { } } - /// Infers effects for one `isset()` operand without treating object properties as reads. - fn infer_isset_arg_assignment_effects( + /// Infers effects for a language-construct operand without treating properties as reads. + fn infer_non_reading_arg_assignment_effects( &mut self, arg: &Expr, env: &mut TypeEnv, @@ -385,7 +388,7 @@ impl Checker { Ok(()) } ExprKind::NamedArg { value, .. } => { - self.infer_isset_arg_assignment_effects(value, env) + self.infer_non_reading_arg_assignment_effects(value, env) } _ => { self.infer_type_with_assignment_effects(arg, env)?; diff --git a/tests/codegen/objects/magic_methods.rs b/tests/codegen/objects/magic_methods.rs index 1e5310868b..147f2671f4 100644 --- a/tests/codegen/objects/magic_methods.rs +++ b/tests/codegen/objects/magic_methods.rs @@ -179,6 +179,76 @@ echo isset($s->token) ? "Y" : "N"; assert_eq!(out, "magic:token;Y"); } +/// Verifies `__unset` is invoked for undefined property unsets. +#[test] +fn test_magic_unset_handles_missing_property_targets() { + let out = compile_and_run( + r#"missing); +echo "done"; +"#, + ); + assert_eq!(out, "unset:missing;done"); +} + +/// Verifies `unset()` does not read an undefined property through `__get`. +#[test] +fn test_magic_unset_missing_property_does_not_call_magic_get() { + let out = compile_and_run( + r#"missing); +echo "done"; +"#, + ); + assert_eq!(out, "done"); +} + +/// Verifies `__unset` is invoked for inaccessible property unsets from outside the class. +#[test] +fn test_magic_unset_handles_inaccessible_property_targets() { + let out = compile_and_run( + r#"token); +echo "done"; +"#, + ); + assert_eq!(out, "magic:token;done"); +} + +/// Verifies unsetting an undefined property without `__unset` is a no-op. +#[test] +fn test_magic_unset_missing_property_without_magic_is_noop() { + let out = compile_and_run( + r#"missing); +echo "done"; +"#, + ); + assert_eq!(out, "done"); +} + /// Verifies `__invoke` is called when an object is invoked as a function via a variable holding the object. #[test] fn test_magic_invoke_handles_variable_object_call() { diff --git a/tests/error_tests/exceptions_enums_magic.rs b/tests/error_tests/exceptions_enums_magic.rs index 305b05b11d..23873306a5 100644 --- a/tests/error_tests/exceptions_enums_magic.rs +++ b/tests/error_tests/exceptions_enums_magic.rs @@ -240,6 +240,55 @@ fn test_error_magic_isset_declared_return_type_must_be_bool() { ); } +/// Verifies that a `static` `__unset` reports +/// "Magic method must be non-static: Bag::__unset". +#[test] +fn test_error_magic_unset_must_be_non_static() { + expect_error( + "token);", + "Cannot access private property: Bag::token", + ); +} + /// Verifies that `__call` with only one parameter reports /// "Magic method must take 2 arguments: Proxy::__call". #[test] @@ -330,20 +379,10 @@ fn test_error_magic_callstatic_must_be_public() { ); } -/// Verifies that a `static` `__unset` reports -/// "Magic method must be non-static: Bag::__unset". -#[test] -fn test_error_magic_unset_must_be_non_static() { - expect_error( - " Date: Fri, 19 Jun 2026 08:02:17 +0200 Subject: [PATCH 0400/1208] Remove unsupported clone from class docs --- docs/php/classes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index b5cce833d9..fa1f183924 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -608,8 +608,8 @@ class Node { trait Fluent { // In a trait, `static` resolves to the class that uses the trait. - public function copy(): static { - return clone $this; + public function self(): static { + return $this; } } ``` From 5d0536c00f01bee9731dc5c4fe2526d47b49d39a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 08:49:28 +0200 Subject: [PATCH 0401/1208] Add object clone support --- docs/php/classes.md | 43 ++++ src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/objects.rs | 206 ++++++++++++++++++ .../program_usage/required_classes/collect.rs | 1 + .../required_classes/dynamic_instanceof.rs | 1 + src/codegen_support/runtime_features.rs | 2 + src/ir/instr.rs | 6 +- src/ir_lower/context.rs | 1 + src/ir_lower/expr/mod.rs | 31 +++ src/lexer/literals/identifiers.rs | 1 + src/lexer/token.rs | 1 + src/magic_constants/walker/exprs.rs | 1 + src/optimize/control/prune/expr.rs | 1 + src/optimize/effects.rs | 3 + src/optimize/fold/expr.rs | 1 + src/optimize/propagate/expr.rs | 4 + src/optimize/propagate/invalidation.rs | 6 + src/parser/ast/expr.rs | 1 + src/parser/expr/assignment_targets.rs | 3 + src/parser/expr/prefix.rs | 1 + src/parser/expr/prefix_complex.rs | 1 + src/pdo_prelude/detect.rs | 1 + src/resolver/contains.rs | 1 + src/resolver/discovery/exprs.rs | 1 + .../checker/builtin_types/magic_methods.rs | 29 ++- src/types/checker/inference/expr/mod.rs | 44 ++++ src/types/checker/schema/classes/constants.rs | 3 + .../checker/yield_validation/validate.rs | 1 + src/types/warnings/expr_reads.rs | 1 + tests/codegen/objects.rs | 2 + tests/codegen/objects/cloning.rs | 89 ++++++++ tests/error_tests/exceptions_enums_magic.rs | 45 ++++ tests/parser_tests/expressions/basics.rs | 19 ++ 33 files changed, 550 insertions(+), 2 deletions(-) create mode 100644 tests/codegen/objects/cloning.rs diff --git a/docs/php/classes.md b/docs/php/classes.md index fa1f183924..ade7a1b87f 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -812,6 +812,7 @@ echo sqlSortKeyword(SortDirection::Descending); // DESC ## Magic methods - `__construct(...)` — runs at instantiation - `__destruct()` — runs when the object is released (see below) +- `__clone()` — runs after `clone $object` creates the shallow copy - `__toString()` — string coercion - `__get($name)` — reading an undeclared property - `__set($name, $value)` — writing an undeclared property @@ -889,6 +890,48 @@ echo User::orderBy("name"); // orderBy(name) → __callStatic Contract: `__callStatic` must be declared `public static` and takes exactly two arguments — the method name (`string`) and the argument list (`array`). +## Object cloning (`clone`) + +`clone $object` creates a shallow copy of a user object or `stdClass` instance: +declared property slots are copied into a new object, dynamic properties are +copied into a separate hash table, and object-valued properties still point at +the same nested object just like PHP. + +```php +child = new Child(); + } + public function __clone(): void { + // Runs on the copy after the shallow copy has been created. + $this->child->x = $this->child->x + 1; + } +} + +$a = new Boxed(); +$b = clone $a; +``` + +`__clone` must be non-static, take no arguments, and if it declares a return +type the return type must be `void`. PHP permits any visibility for `__clone`; +elephc checks that the `clone` expression is allowed to access the hook from the +current scope. A private `__clone` can therefore be used from inside the +declaring class, while cloning that object from unrelated global code is +rejected. + +Runtime-managed built-in objects whose storage is not represented as ordinary +declared properties are not cloneable yet. This includes `Fiber`, `Generator`, +Reflection objects, and SPL containers/iterators with native storage such as +`SplFixedArray`, `SplDoublyLinkedList`, `SplStack`, `SplQueue`, +`IteratorIterator`, `CallbackFilterIterator`, and +`RecursiveCallbackFilterIterator`. + ## Destructors (`__destruct`) A class may declare `public function __destruct(): void` to run cleanup when an diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 2cc58e3424..656b85bd6e 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -167,6 +167,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::BufferGet => buffers::lower_buffer_get(ctx, &inst), Op::BufferSet => buffers::lower_buffer_set(ctx, &inst), Op::ObjectNew => objects::lower_object_new(ctx, &inst), + Op::ObjectCloneShallow => objects::lower_object_clone_shallow(ctx, &inst), Op::DynamicObjectNew => objects::lower_dynamic_object_new(ctx, &inst), Op::DynamicObjectNewMixed => objects::lower_dynamic_object_new_mixed(ctx, &inst), Op::PropGet => objects::lower_prop_get(ctx, &inst), diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 5d96e7feb5..98dab741b8 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -228,6 +228,81 @@ pub(super) fn lower_object_new(ctx: &mut FunctionContext<'_>, inst: &Instruction Ok(()) } +/// Lowers PHP object cloning for fixed-class receivers. +pub(super) fn lower_object_clone_shallow( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let source = expect_operand(inst, 0)?; + let class_name = class_name_immediate(ctx, inst)?.to_string(); + if is_builtin_stdclass(&class_name) { + return lower_stdclass_clone(ctx, inst, source); + } + if is_runtime_managed_object_clone_class(&class_name) { + return Err(CodegenIrError::unsupported(format!( + "clone for runtime-managed class {}", + class_name + ))); + } + let (class_id, property_count, allow_dynamic_properties, retained_offsets) = { + let class_info = + ctx.module.class_infos.get(&class_name).ok_or_else(|| { + CodegenIrError::unsupported(format!("unknown class {}", class_name)) + })?; + let retained_offsets = cloned_property_retain_offsets(class_info); + ( + class_info.class_id, + class_info.properties.len(), + class_info.allow_dynamic_properties, + retained_offsets, + ) + }; + let result = inst + .result + .ok_or_else(|| CodegenIrError::invalid_module("object_clone_shallow missing result value"))?; + let result_reg = abi::int_result_reg(ctx.emitter); + ctx.load_value_to_reg(source, result_reg)?; + abi::emit_push_reg(ctx.emitter, result_reg); + emit_object_allocation(ctx, class_id, property_count, allow_dynamic_properties, &[])?; + ctx.store_result_value(result)?; + let source_reg = abi::secondary_scratch_reg(ctx.emitter); + let dest_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_pop_reg(ctx.emitter, source_reg); + ctx.load_value_to_reg(result, dest_reg)?; + emit_clone_declared_property_slots(ctx, source_reg, dest_reg, property_count, &retained_offsets); + if allow_dynamic_properties { + emit_clone_dynamic_property_hash( + ctx, + source_reg, + dest_reg, + dynamic_property_hash_offset(property_count), + ); + } + Ok(()) +} + +/// Lowers `clone` for `stdClass`, whose payload is just class id plus dynamic-property hash. +fn lower_stdclass_clone( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + source: ValueId, +) -> Result<()> { + let result = inst + .result + .ok_or_else(|| CodegenIrError::invalid_module("stdClass clone missing result value"))?; + let result_reg = abi::int_result_reg(ctx.emitter); + ctx.load_value_to_reg(source, result_reg)?; + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_call_label(ctx.emitter, "__rt_stdclass_new"); + ctx.store_result_value(result)?; + let source_reg = abi::secondary_scratch_reg(ctx.emitter); + let dest_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_pop_reg(ctx.emitter, source_reg); + ctx.load_value_to_reg(result, dest_reg)?; + emit_clone_dynamic_property_hash(ctx, source_reg, dest_reg, 8); + Ok(()) +} + /// Lowers `new stdClass()` through the runtime helper that seeds its dynamic-property hash. fn lower_stdclass_new(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { if !inst.operands.is_empty() { @@ -245,6 +320,19 @@ fn is_spl_doubly_linked_list_family(class_name: &str) -> bool { matches!(class_name, "SplDoublyLinkedList" | "SplStack" | "SplQueue") } +/// Returns true for object classes whose payload is not the generic declared-property layout. +fn is_runtime_managed_object_clone_class(class_name: &str) -> bool { + let class_name = class_name.trim_start_matches('\\'); + is_fiber_class(class_name) + || class_name == "Generator" + || reflection::is_reflection_owner_class(class_name) + || class_name == "CallbackFilterIterator" + || class_name == "RecursiveCallbackFilterIterator" + || class_name == "IteratorIterator" + || is_spl_doubly_linked_list_family(class_name) + || class_name == "SplFixedArray" +} + /// Lowers `new SplDoublyLinkedList`, `new SplStack`, and `new SplQueue`. fn lower_spl_doubly_linked_list_new( ctx: &mut FunctionContext<'_>, @@ -3864,6 +3952,124 @@ fn dynamic_property_hash_offset(property_count: usize) -> usize { 8 + property_count * 16 } +/// Returns property slot offsets whose copied low word must be retained for the cloned owner. +fn cloned_property_retain_offsets(class_info: &ClassInfo) -> Vec { + class_info + .properties + .iter() + .enumerate() + .filter_map(|(index, (property, php_type))| { + if class_info.property_slot_is_reference(index, property) { + return None; + } + property_clone_needs_retain(php_type).then_some(8 + index * 16) + }) + .collect() +} + +/// Returns true when a property slot's low word owns heap storage after a shallow copy. +fn property_clone_needs_retain(php_type: &PhpType) -> bool { + let php_type = php_type.codegen_repr(); + matches!(php_type, PhpType::Str) || php_type.is_refcounted() +} + +/// Copies declared 16-byte property slots and retains heap-backed child payloads. +fn emit_clone_declared_property_slots( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, + property_count: usize, + retained_offsets: &[usize], +) { + for index in 0..property_count { + let offset = 8 + index * 16; + emit_copy_property_slot(ctx, source_reg, dest_reg, offset); + if retained_offsets.contains(&offset) { + emit_retain_cloned_property_pointer(ctx, source_reg, dest_reg, offset); + } + } +} + +/// Copies one 16-byte declared-property slot from the source object to the clone. +fn emit_copy_property_slot( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, + offset: usize, +) { + let low_reg = abi::int_result_reg(ctx.emitter); + let high_reg = abi::tertiary_scratch_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, low_reg, source_reg, offset); + abi::emit_load_from_address(ctx.emitter, high_reg, source_reg, offset + 8); + abi::emit_store_to_address(ctx.emitter, low_reg, dest_reg, offset); + abi::emit_store_to_address(ctx.emitter, high_reg, dest_reg, offset + 8); +} + +/// Retains the copied low-word pointer for string, array, hash, object, or Mixed slots. +fn emit_retain_cloned_property_pointer( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, + offset: usize, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, source_reg); + abi::emit_push_reg(ctx.emitter, dest_reg); + abi::emit_load_from_address(ctx.emitter, result_reg, dest_reg, offset); + abi::emit_call_label(ctx.emitter, "__rt_incref"); + abi::emit_pop_reg(ctx.emitter, dest_reg); + abi::emit_pop_reg(ctx.emitter, source_reg); +} + +/// Replaces the constructor-seeded dynamic-property hash with a shallow clone of the source hash. +fn emit_clone_dynamic_property_hash( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, + offset: usize, +) { + emit_release_existing_dynamic_property_hash(ctx, source_reg, dest_reg, offset); + let null_label = ctx.next_label("object_clone_dyn_props_null"); + let done_label = ctx.next_label("object_clone_dyn_props_done"); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, result_reg, source_reg, offset); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbz {}, {}", result_reg, null_label)); // missing dynamic-property hash clones as a null hash pointer + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // check whether the source dynamic-property hash exists + ctx.emitter.instruction(&format!("jz {}", null_label)); // missing dynamic-property hash clones as a null hash pointer + } + } + abi::emit_push_reg(ctx.emitter, source_reg); + abi::emit_push_reg(ctx.emitter, dest_reg); + abi::emit_call_label(ctx.emitter, "__rt_hash_clone_shallow"); + abi::emit_pop_reg(ctx.emitter, dest_reg); + abi::emit_pop_reg(ctx.emitter, source_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, dest_reg, offset); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&null_label); + abi::emit_store_zero_to_address(ctx.emitter, dest_reg, offset); + ctx.emitter.label(&done_label); +} + +/// Releases the empty hash allocated while constructing the clone shell. +fn emit_release_existing_dynamic_property_hash( + ctx: &mut FunctionContext<'_>, + source_reg: &str, + dest_reg: &str, + offset: usize, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, source_reg); + abi::emit_push_reg(ctx.emitter, dest_reg); + abi::emit_load_from_address(ctx.emitter, result_reg, dest_reg, offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_any"); + abi::emit_pop_reg(ctx.emitter, dest_reg); + abi::emit_pop_reg(ctx.emitter, source_reg); +} + /// Allocates the per-object dynamic-property hash and stores it in the object payload. fn emit_dynamic_property_hash_init(ctx: &mut FunctionContext<'_>, object_reg: &str, offset: usize) { let hash_reg = abi::secondary_scratch_reg(ctx.emitter); diff --git a/src/codegen_support/program_usage/required_classes/collect.rs b/src/codegen_support/program_usage/required_classes/collect.rs index 230ce4ff54..1b79caef13 100644 --- a/src/codegen_support/program_usage/required_classes/collect.rs +++ b/src/codegen_support/program_usage/required_classes/collect.rs @@ -287,6 +287,7 @@ fn collect_required_class_names_in_expr(expr: &Expr, names: &mut HashSet | ExprKind::Throw(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) + | ExprKind::Clone(expr) | ExprKind::Spread(expr) | ExprKind::Cast { expr, .. } | ExprKind::PtrCast { expr, .. } => collect_required_class_names_in_expr(expr, names), diff --git a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs index 0e41602f26..c2896b238f 100644 --- a/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs +++ b/src/codegen_support/program_usage/required_classes/dynamic_instanceof.rs @@ -158,6 +158,7 @@ fn expr_has_dynamic_instanceof(expr: &Expr) -> bool { | ExprKind::Throw(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) + | ExprKind::Clone(expr) | ExprKind::Spread(expr) | ExprKind::Cast { expr, .. } | ExprKind::PtrCast { expr, .. } diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index 3877cfa7b0..6e6bd1b16e 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -346,6 +346,7 @@ fn expr_has_regex_call(expr: &Expr) -> bool { | ExprKind::Not(expr) | ExprKind::BitNot(expr) | ExprKind::Throw(expr) + | ExprKind::Clone(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) | ExprKind::Spread(expr) @@ -666,6 +667,7 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { | ExprKind::Not(expr) | ExprKind::BitNot(expr) | ExprKind::Throw(expr) + | ExprKind::Clone(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) | ExprKind::Spread(expr) diff --git a/src/ir/instr.rs b/src/ir/instr.rs index dba6e78eb6..04eda26da0 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -310,6 +310,7 @@ pub enum Op { IteratorMethodCall, SplRuntimeCall, ObjectNew, + ObjectCloneShallow, DynamicObjectNew, DynamicObjectNewMixed, PropGet, @@ -465,7 +466,9 @@ impl Op { } ArrayGet => E::READS_HEAP | E::MAY_FATAL | E::MAY_WARN, StrPersist | ArrayEnsureUnique | HashEnsureUnique | ArrayCloneShallow - | HashCloneShallow => E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP, + | HashCloneShallow | ObjectCloneShallow => { + E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP + } ArrayLen | HashLen | ArrayKeyExists | OffsetExists | PropGet | LoadPropRefCell => { E::READS_HEAP } @@ -676,6 +679,7 @@ impl Op { IteratorMethodCall => "iterator_method_call", SplRuntimeCall => "spl_runtime_call", ObjectNew => "object_new", + ObjectCloneShallow => "object_clone_shallow", DynamicObjectNew => "dynamic_object_new", DynamicObjectNewMixed => "dynamic_object_new_mixed", PropGet => "prop_get", diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index a4483efa52..846b3a9bb7 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1161,6 +1161,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::HashArrayUnion | Op::ArrayToHash | Op::ObjectNew + | Op::ObjectCloneShallow | Op::DynamicObjectNew | Op::DynamicObjectNewMixed | Op::ClosureNew diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index a39269ad96..889e06ee92 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -123,6 +123,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ExprKind::ExprCall { callee, args } => lower_expr_call(ctx, callee, args, expr), ExprKind::ConstRef(name) => constants::lower_const_ref(ctx, name, expr), ExprKind::NewObject { class_name, args } => lower_new_object(ctx, class_name, args, expr), + ExprKind::Clone(inner) => lower_clone(ctx, inner, expr), ExprKind::NewDynamic { name_expr, args } => { lower_new_dynamic(ctx, name_expr, args, expr) } @@ -696,6 +697,7 @@ fn expr_can_reset_concat_storage(expr: &Expr) -> bool { | ExprKind::NewDynamic { .. } | ExprKind::NewDynamicObject { .. } | ExprKind::NewScopedObject { .. } + | ExprKind::Clone(_) | ExprKind::Pipe { .. } | ExprKind::Yield { .. } | ExprKind::YieldFrom(_) => true, @@ -8474,6 +8476,7 @@ fn expr_contains_eval_call(expr: &Expr) -> bool { | ExprKind::Not(expr) | ExprKind::BitNot(expr) | ExprKind::Throw(expr) + | ExprKind::Clone(expr) | ExprKind::ErrorSuppress(expr) | ExprKind::Print(expr) | ExprKind::Spread(expr) @@ -9112,6 +9115,34 @@ fn lower_new_object( ) } +/// Lowers PHP `clone $object` to a shallow object-copy opcode and optional `__clone()` hook. +fn lower_clone( + ctx: &mut LoweringContext<'_, '_>, + inner: &Expr, + expr: &Expr, +) -> LoweredValue { + let object = lower_expr(ctx, inner); + let object_ty = ctx.builder.value_php_type(object.value); + let Some((class_name, false)) = singular_object_class(&object_ty) else { + unreachable!("clone expressions must be type-checked as non-null objects before lowering"); + }; + let class_name = class_name.to_string(); + let data = ctx.intern_class_name(&class_name); + let result_ty = PhpType::Object(class_name.clone()); + let cloned = ctx.emit_value( + Op::ObjectCloneShallow, + vec![object.value], + Some(Immediate::Data(data)), + result_ty, + Op::ObjectCloneShallow.default_effects(), + Some(expr.span), + ); + if class_method_signature(ctx, &class_name, &php_symbol_key("__clone")).is_some() { + lower_method_call_with_receiver(ctx, cloned, "__clone", &[], Op::MethodCall, expr); + } + cloned +} + /// Metadata operand source for direct `ReflectionParameter` constructor lowering. enum ReflectionParameterConstructorOperand { Expr(Expr), diff --git a/src/lexer/literals/identifiers.rs b/src/lexer/literals/identifiers.rs index f08fefe891..0d522a6006 100644 --- a/src/lexer/literals/identifiers.rs +++ b/src/lexer/literals/identifiers.rs @@ -184,6 +184,7 @@ pub(in crate::lexer) fn scan_keyword(cursor: &mut Cursor) -> Result Ok(Token::Class), "enum" => Ok(Token::Enum), "new" => Ok(Token::New), + "clone" => Ok(Token::Clone), "public" => Ok(Token::Public), "protected" => Ok(Token::Protected), "private" => Ok(Token::Private), diff --git a/src/lexer/token.rs b/src/lexer/token.rs index 6b651724b9..7c5b3e248b 100644 --- a/src/lexer/token.rs +++ b/src/lexer/token.rs @@ -108,6 +108,7 @@ pub enum Token { Class, // class Enum, // enum New, // new + Clone, // clone Public, // public Protected, // protected Private, // private diff --git a/src/magic_constants/walker/exprs.rs b/src/magic_constants/walker/exprs.rs index 120c82aff4..f4717af9e5 100644 --- a/src/magic_constants/walker/exprs.rs +++ b/src/magic_constants/walker/exprs.rs @@ -52,6 +52,7 @@ pub(super) fn walk_expr(expr: Expr, pass: &mut P) -> Expr { ExprKind::Not(inner) => ExprKind::Not(Box::new(walk_expr(*inner, pass))), ExprKind::BitNot(inner) => ExprKind::BitNot(Box::new(walk_expr(*inner, pass))), ExprKind::Throw(inner) => ExprKind::Throw(Box::new(walk_expr(*inner, pass))), + ExprKind::Clone(inner) => ExprKind::Clone(Box::new(walk_expr(*inner, pass))), ExprKind::ErrorSuppress(inner) => ExprKind::ErrorSuppress(Box::new(walk_expr(*inner, pass))), ExprKind::Print(inner) => ExprKind::Print(Box::new(walk_expr(*inner, pass))), ExprKind::NullCoalesce { value, default } => ExprKind::NullCoalesce { diff --git a/src/optimize/control/prune/expr.rs b/src/optimize/control/prune/expr.rs index a4de94b09f..1a04955c0e 100644 --- a/src/optimize/control/prune/expr.rs +++ b/src/optimize/control/prune/expr.rs @@ -42,6 +42,7 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { ExprKind::Not(inner) => ExprKind::Not(Box::new(prune_expr(*inner))), ExprKind::BitNot(inner) => ExprKind::BitNot(Box::new(prune_expr(*inner))), ExprKind::Throw(inner) => ExprKind::Throw(Box::new(prune_expr(*inner))), + ExprKind::Clone(inner) => ExprKind::Clone(Box::new(prune_expr(*inner))), ExprKind::ErrorSuppress(inner) => ExprKind::ErrorSuppress(Box::new(prune_expr(*inner))), ExprKind::Print(inner) => ExprKind::Print(Box::new(prune_expr(*inner))), ExprKind::NullCoalesce { value, default } => ExprKind::NullCoalesce { diff --git a/src/optimize/effects.rs b/src/optimize/effects.rs index 9f6e5262d8..327baa599c 100644 --- a/src/optimize/effects.rs +++ b/src/optimize/effects.rs @@ -227,6 +227,9 @@ pub(super) fn expr_effect(expr: &Expr) -> Effect { | ExprKind::PtrCast { expr: inner, .. } | ExprKind::Spread(inner) => expr_effect(inner), ExprKind::Print(inner) => expr_effect(inner).with_side_effects(), + ExprKind::Clone(inner) => expr_effect(inner) + .with_side_effects() + .with_may_throw(), ExprKind::BinaryOp { left, right, .. } => expr_effect(left).combine(expr_effect(right)), ExprKind::InstanceOf { value, target } => { expr_effect(value).combine(instanceof_target_effect(target)) diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index d45db5ed98..e6ef5279eb 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -126,6 +126,7 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { try_fold_bit_not(&inner).unwrap_or_else(|| ExprKind::BitNot(Box::new(inner))) } ExprKind::Throw(inner) => ExprKind::Throw(Box::new(fold_expr(*inner))), + ExprKind::Clone(inner) => ExprKind::Clone(Box::new(fold_expr(*inner))), ExprKind::ErrorSuppress(inner) => ExprKind::ErrorSuppress(Box::new(fold_expr(*inner))), ExprKind::Print(inner) => ExprKind::Print(Box::new(fold_expr(*inner))), ExprKind::NullCoalesce { value, default } => { diff --git a/src/optimize/propagate/expr.rs b/src/optimize/propagate/expr.rs index e984dd6e5f..d47ad49efc 100644 --- a/src/optimize/propagate/expr.rs +++ b/src/optimize/propagate/expr.rs @@ -88,6 +88,10 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { ExprKind::Not(inner) => ExprKind::Not(Box::new(propagate_expr(*inner, env))), ExprKind::BitNot(inner) => ExprKind::BitNot(Box::new(propagate_expr(*inner, env))), ExprKind::Throw(inner) => ExprKind::Throw(Box::new(propagate_expr(*inner, env))), + ExprKind::Clone(inner) => { + let empty_env = HashMap::new(); + ExprKind::Clone(Box::new(propagate_expr(*inner, &empty_env))) + } ExprKind::ErrorSuppress(inner) => { ExprKind::ErrorSuppress(Box::new(propagate_expr(*inner, env))) } diff --git a/src/optimize/propagate/invalidation.rs b/src/optimize/propagate/invalidation.rs index b1a36bca62..1d40eaa54e 100644 --- a/src/optimize/propagate/invalidation.rs +++ b/src/optimize/propagate/invalidation.rs @@ -116,6 +116,12 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation { | ExprKind::Cast { expr: inner, .. } | ExprKind::BufferNew { len: inner, .. } | ExprKind::NamedArg { value: inner, .. } => expr_invalidation(inner), + ExprKind::Clone(inner) => expr_invalidation(inner).union(top_level_globals_guard( + Effect::PURE + .with_side_effects() + .with_may_throw() + .with_writes_globals(), + )), ExprKind::BinaryOp { left, right, .. } => { expr_invalidation(left).union(expr_invalidation(right)) } diff --git a/src/parser/ast/expr.rs b/src/parser/ast/expr.rs index 53d2b77705..8812d05682 100644 --- a/src/parser/ast/expr.rs +++ b/src/parser/ast/expr.rs @@ -156,6 +156,7 @@ pub enum ExprKind { required_parent: Name, args: Vec, }, + Clone(Box), PropertyAccess { object: Box, property: String, diff --git a/src/parser/expr/assignment_targets.rs b/src/parser/expr/assignment_targets.rs index 88f37b47b4..5dbd1c6521 100644 --- a/src/parser/expr/assignment_targets.rs +++ b/src/parser/expr/assignment_targets.rs @@ -344,6 +344,7 @@ fn collect_assignment_target_dependencies(expr: &Expr, dependencies: &mut HashSe | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Cast { expr: value, .. } @@ -476,6 +477,7 @@ fn expr_may_write_dependency(expr: &Expr, dependencies: &HashSet) -> boo | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Cast { expr: value, .. } @@ -686,6 +688,7 @@ fn expr_contains_equivalent(expr: &Expr, needle: &Expr) -> bool { | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Cast { expr: value, .. } diff --git a/src/parser/expr/prefix.rs b/src/parser/expr/prefix.rs index 311d764012..50bdca3eb2 100644 --- a/src/parser/expr/prefix.rs +++ b/src/parser/expr/prefix.rs @@ -44,6 +44,7 @@ pub(super) fn parse_prefix( Token::At => parse_unary(tokens, pos, span, ExprKind::ErrorSuppress, 35), Token::Print => parse_unary(tokens, pos, span, ExprKind::Print, 7), Token::Throw => parse_unary(tokens, pos, span, ExprKind::Throw, 0), + Token::Clone => parse_unary(tokens, pos, span, ExprKind::Clone, 35), Token::True => parse_simple(tokens, pos, span, ExprKind::BoolLiteral(true)), Token::False => parse_simple(tokens, pos, span, ExprKind::BoolLiteral(false)), Token::Null => parse_simple(tokens, pos, span, ExprKind::Null), diff --git a/src/parser/expr/prefix_complex.rs b/src/parser/expr/prefix_complex.rs index 263e37b1de..dd0eb0c799 100644 --- a/src/parser/expr/prefix_complex.rs +++ b/src/parser/expr/prefix_complex.rs @@ -341,6 +341,7 @@ fn collect_arrow_expr_captures( | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) diff --git a/src/pdo_prelude/detect.rs b/src/pdo_prelude/detect.rs index fa79cc2922..3505ce0406 100644 --- a/src/pdo_prelude/detect.rs +++ b/src/pdo_prelude/detect.rs @@ -185,6 +185,7 @@ fn expr_refs_pdo(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) diff --git a/src/resolver/contains.rs b/src/resolver/contains.rs index abc8c02472..fd1d3d8ce0 100644 --- a/src/resolver/contains.rs +++ b/src/resolver/contains.rs @@ -144,6 +144,7 @@ fn expr_has_includes(expr: &Expr) -> bool { | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Spread(value) diff --git a/src/resolver/discovery/exprs.rs b/src/resolver/discovery/exprs.rs index 2a8ba1b9e9..47d0c0916a 100644 --- a/src/resolver/discovery/exprs.rs +++ b/src/resolver/discovery/exprs.rs @@ -69,6 +69,7 @@ pub(super) fn discover_expr( | ExprKind::Not(value) | ExprKind::BitNot(value) | ExprKind::Throw(value) + | ExprKind::Clone(value) | ExprKind::ErrorSuppress(value) | ExprKind::Print(value) | ExprKind::Spread(value) diff --git a/src/types/checker/builtin_types/magic_methods.rs b/src/types/checker/builtin_types/magic_methods.rs index 1fb720fe4f..6495bd89d1 100644 --- a/src/types/checker/builtin_types/magic_methods.rs +++ b/src/types/checker/builtin_types/magic_methods.rs @@ -73,7 +73,8 @@ pub(crate) fn patch_magic_method_signatures(checker: &mut Checker) { } /// Validates that user-declared magic methods (`__toString`, `__get`, `__set`, -/// `__isset`, `__unset`, `__call`, `__callStatic`, `__invoke`, `__destruct`) +/// `__isset`, `__unset`, `__call`, `__callStatic`, `__invoke`, `__clone`, +/// `__destruct`) /// conform to PHP's static/non-static, visibility, arity, and return-type rules. /// /// Returns `Ok(())` if all declared magic methods are contract-compliant. @@ -273,6 +274,32 @@ pub(crate) fn validate_magic_method_contracts(checker: &Checker) -> Result<(), C )); } } + "__clone" => { + if method.is_static { + errors.push(CompileError::new( + method.span, + &format!("Magic method must be non-static: {}::__clone", class_name), + )); + continue; + } + if !method.params.is_empty() || method.variadic.is_some() { + errors.push(CompileError::new( + method.span, + &format!("Magic method must take 0 arguments: {}::__clone", class_name), + )); + continue; + } + if method + .return_type + .as_ref() + .is_some_and(|return_type| !matches!(return_type, TypeExpr::Void)) + { + errors.push(CompileError::new( + method.span, + &format!("Magic method must return void: {}::__clone", class_name), + )); + } + } "__destruct" => { // PHP permits any visibility for __destruct (the engine calls // it regardless), so only the non-static and zero-argument diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index e55737234e..abec45e2d3 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -9,6 +9,7 @@ //! - Inference must preserve PHP evaluation errors and avoid treating effectful expressions as pure type facts. use crate::errors::CompileError; +use crate::names::php_symbol_key; use crate::parser::ast::{Expr, ExprKind}; use crate::span::Span; use crate::types::{ @@ -528,6 +529,16 @@ impl Checker { ExprKind::NewObject { class_name, args } => { self.infer_new_object_type(class_name.as_str(), args, expr, env) } + ExprKind::Clone(inner) => { + let ty = self.infer_type(inner, env)?; + match ty { + PhpType::Object(class_name) => { + self.check_clone_visibility(&class_name, expr.span)?; + Ok(PhpType::Object(class_name)) + } + _ => Err(CompileError::new(expr.span, "clone requires an object value")), + } + } ExprKind::NewDynamic { name_expr, args } => { // The class is named at runtime; without a literal class // we can't typecheck constructor args or the resulting @@ -745,6 +756,39 @@ impl Checker { } +impl Checker { + /// Checks whether the current scope may invoke a class's `__clone` hook. + /// + /// PHP permits `__clone` to be non-public, but the actual `clone $object` + /// expression must obey the hook's visibility when a hook exists. + fn check_clone_visibility(&self, class_name: &str, span: Span) -> Result<(), CompileError> { + let normalized = class_name.trim_start_matches('\\'); + let Some(class_info) = self.classes.get(normalized) else { + return Ok(()); + }; + let key = php_symbol_key("__clone"); + let Some(visibility) = class_info.method_visibilities.get(&key) else { + return Ok(()); + }; + let declaring_class = class_info + .method_declaring_classes + .get(&key) + .map(String::as_str) + .unwrap_or(normalized); + if self.can_access_member(declaring_class, visibility) { + return Ok(()); + } + Err(CompileError::new( + span, + &format!( + "Cannot access {} method: {}::__clone", + Self::visibility_label(visibility), + normalized + ), + )) + } +} + /// Returns `true` if `index` is a valid string offset index for a string receiver. /// /// A valid index is an integer type, or a string literal whose value can be diff --git a/src/types/checker/schema/classes/constants.rs b/src/types/checker/schema/classes/constants.rs index d64eb95cd2..ab75d897cf 100644 --- a/src/types/checker/schema/classes/constants.rs +++ b/src/types/checker/schema/classes/constants.rs @@ -65,6 +65,9 @@ fn rewrite_expr( ExprKind::Throw(inner) => { ExprKind::Throw(Box::new(rewrite_expr(inner, class_name, parent_name)?)) } + ExprKind::Clone(inner) => { + ExprKind::Clone(Box::new(rewrite_expr(inner, class_name, parent_name)?)) + } ExprKind::ErrorSuppress(inner) => ExprKind::ErrorSuppress(Box::new(rewrite_expr( inner, class_name, diff --git a/src/types/checker/yield_validation/validate.rs b/src/types/checker/yield_validation/validate.rs index 5050e9567a..22e52e8d1a 100644 --- a/src/types/checker/yield_validation/validate.rs +++ b/src/types/checker/yield_validation/validate.rs @@ -284,6 +284,7 @@ fn visit_expr(expr: &Expr, st: &mut State) { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Spread(inner) | ExprKind::Cast { expr: inner, .. } diff --git a/src/types/warnings/expr_reads.rs b/src/types/warnings/expr_reads.rs index c1804728da..96b1ebe85d 100644 --- a/src/types/warnings/expr_reads.rs +++ b/src/types/warnings/expr_reads.rs @@ -44,6 +44,7 @@ pub(super) fn collect_expr_reads( | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) diff --git a/tests/codegen/objects.rs b/tests/codegen/objects.rs index 0e8dd6b74e..960fe08210 100644 --- a/tests/codegen/objects.rs +++ b/tests/codegen/objects.rs @@ -15,6 +15,8 @@ mod classes; mod gc_aliasing; #[path = "objects/magic_methods.rs"] mod magic_methods; +#[path = "objects/cloning.rs"] +mod cloning; #[path = "objects/property_access/mod.rs"] mod property_access; #[path = "objects/constructor_promotion.rs"] diff --git a/tests/codegen/objects/cloning.rs b/tests/codegen/objects/cloning.rs new file mode 100644 index 0000000000..411ad1c015 --- /dev/null +++ b/tests/codegen/objects/cloning.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Integration or regression tests for PHP object cloning codegen. +//! Covers shallow object copies, declared property slots, stdClass dynamic properties, and `__clone` hooks. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Inline PHP fixtures compile to native binaries and compare stdout against PHP clone semantics. + +use super::*; + +/// Verifies cloning declared scalar/string properties creates an independent object slot copy. +#[test] +fn test_clone_copies_declared_properties_independently() { + let out = compile_and_run( + r#"n = 2; +$b->label = "two"; +echo $a->n . ":" . $a->label . "|" . $b->n . ":" . $b->label; +"#, + ); + assert_eq!(out, "1:one|2:two"); +} + +/// Verifies `__clone()` is invoked after the shallow copy and mutates the clone, not the source. +#[test] +fn test_clone_invokes_magic_clone_on_the_copy() { + let out = compile_and_run( + r#"n = $this->n + 10; + } +} +$a = new Counter(); +$b = clone $a; +echo $a->n . "|" . $b->n; +"#, + ); + assert_eq!(out, "hook;1|11"); +} + +/// Verifies object-valued properties are shallow-copied, so nested object mutations remain shared. +#[test] +fn test_clone_keeps_nested_objects_shared() { + let out = compile_and_run( + r#"child = new Child(); + } +} +$a = new Boxed(); +$b = clone $a; +$b->child->x = 7; +echo $a->child->x . "|" . $b->child->x; +"#, + ); + assert_eq!(out, "7|7"); +} + +/// Verifies stdClass dynamic properties are copied into a separate hash table during cloning. +#[test] +fn test_clone_copies_stdclass_dynamic_properties_independently() { + let out = compile_and_run( + r#"name = "source"; +$b = clone $a; +$b->name = "copy"; +$b->extra = "new"; +echo $a->name . "|" . $b->name . "|" . (isset($a->extra) ? "Y" : "N"); +"#, + ); + assert_eq!(out, "source|copy|N"); +} diff --git a/tests/error_tests/exceptions_enums_magic.rs b/tests/error_tests/exceptions_enums_magic.rs index 23873306a5..c3a9d638b5 100644 --- a/tests/error_tests/exceptions_enums_magic.rs +++ b/tests/error_tests/exceptions_enums_magic.rs @@ -120,6 +120,51 @@ fn test_error_throw_expression_requires_object() { ); } +/// Verifies that `clone` rejects scalar operands during type checking. +#[test] +fn test_error_clone_requires_object() { + expect_error(" { + assert_eq!(name, "copy"); + match &value.kind { + ExprKind::Clone(inner) => { + assert_eq!(inner.kind, ExprKind::Variable("obj".into())); + } + other => panic!("expected clone expression, got {:?}", other), + } + } + other => panic!("expected assignment, got {:?}", other), + } +} + /// Verifies that ` Date: Fri, 19 Jun 2026 08:54:16 +0200 Subject: [PATCH 0402/1208] Preserve inherited iterator storage type --- .../checker/builtin_spl_classes/forwarding.rs | 7 ++++- .../checker/builtin_spl_classes/patch.rs | 4 +-- .../checker/builtin_spl_classes/recursive.rs | 27 ++++++++++++++++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/types/checker/builtin_spl_classes/forwarding.rs b/src/types/checker/builtin_spl_classes/forwarding.rs index 36013e4036..4c8def47a6 100644 --- a/src/types/checker/builtin_spl_classes/forwarding.rs +++ b/src/types/checker/builtin_spl_classes/forwarding.rs @@ -15,6 +15,7 @@ use crate::parser::ast::{BinOp, ClassMethod, ClassProperty, Expr, Stmt, TypeExpr use crate::types::traits::FlattenedClass; use super::common::*; +use super::recursive_array::assume_recursive_iterator_expr; /// Inserts classes into the supplied builtin metadata registry. pub(super) fn insert_classes(class_map: &mut HashMap) { @@ -210,7 +211,11 @@ pub(super) fn inner_void_body(method: &str) -> Vec { /// Builds the synthetic method body for recursive inner return. pub(super) fn recursive_inner_return_body(method: &str) -> Vec { - return_body(method_call(inner_expr(), method, Vec::new())) + return_body(method_call( + assume_recursive_iterator_expr(inner_expr()), + method, + Vec::new(), + )) } /// Builds the AST expression for limit position. diff --git a/src/types/checker/builtin_spl_classes/patch.rs b/src/types/checker/builtin_spl_classes/patch.rs index 08c86ae2a4..a98369524e 100644 --- a/src/types/checker/builtin_spl_classes/patch.rs +++ b/src/types/checker/builtin_spl_classes/patch.rs @@ -97,9 +97,7 @@ pub(super) fn patch_builtin_spl_storage_signatures(checker: &mut Checker) { ] { if let Some(class_info) = checker.classes.get_mut(class_name) { for (name, ty) in &mut class_info.properties { - if name == "inner" { - *ty = PhpType::Object("RecursiveIterator".to_string()); - } else if name == "callback" { + if name == "callback" { *ty = PhpType::Callable; } else if name == "callbackEnv" { *ty = PhpType::Pointer(None); diff --git a/src/types/checker/builtin_spl_classes/recursive.rs b/src/types/checker/builtin_spl_classes/recursive.rs index 06735b1837..55fe7cb721 100644 --- a/src/types/checker/builtin_spl_classes/recursive.rs +++ b/src/types/checker/builtin_spl_classes/recursive.rs @@ -154,7 +154,14 @@ fn spl_parent_iterator_methods() -> Vec { /// Builds the synthetic method body for recursive filter get children. fn recursive_filter_get_children_body() -> Vec { vec![ - assign_stmt("child", method_call(inner_expr(), "getChildren", Vec::new())), + assign_stmt( + "child", + method_call( + assume_recursive_iterator_expr(inner_expr()), + "getChildren", + Vec::new(), + ), + ), if_stmt( function_call("is_null", vec![var_expr("child")]), return_body(null_expr()), @@ -167,7 +174,14 @@ fn recursive_filter_get_children_body() -> Vec { /// Builds the synthetic method body for recursive callback filter get children. fn recursive_callback_filter_get_children_body() -> Vec { vec![ - assign_stmt("child", method_call(inner_expr(), "getChildren", Vec::new())), + assign_stmt( + "child", + method_call( + assume_recursive_iterator_expr(inner_expr()), + "getChildren", + Vec::new(), + ), + ), if_stmt( function_call("is_null", vec![var_expr("child")]), return_body(null_expr()), @@ -195,7 +209,14 @@ fn recursive_callback_filter_get_children_body() -> Vec { /// Builds the synthetic method body for parent iterator get children. fn parent_iterator_get_children_body() -> Vec { vec![ - assign_stmt("child", method_call(inner_expr(), "getChildren", Vec::new())), + assign_stmt( + "child", + method_call( + assume_recursive_iterator_expr(inner_expr()), + "getChildren", + Vec::new(), + ), + ), if_stmt( function_call("is_null", vec![var_expr("child")]), return_body(null_expr()), From f0d1f4c0ccecfd7c3e1c98736954570871b2ae8a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 09:03:32 +0200 Subject: [PATCH 0403/1208] Fix Throwable message ownership --- src/codegen/lower_inst.rs | 3 ++- tests/codegen/exceptions.rs | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 656b85bd6e..b7be4b56dc 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -3719,11 +3719,12 @@ fn lower_throwable_standard_method( store_if_result(ctx, inst) } -/// Loads `Throwable::getMessage()` from payload offsets 8/16 into string result registers. +/// Loads `Throwable::getMessage()` from payload offsets 8/16 and returns a caller-owned string copy. fn lower_throwable_get_message(ctx: &mut FunctionContext<'_>, object_reg: &str) -> Result { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); abi::emit_load_from_address(ctx.emitter, ptr_reg, object_reg, 8); abi::emit_load_from_address(ctx.emitter, len_reg, object_reg, 16); + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); Ok(PhpType::Str) } diff --git a/tests/codegen/exceptions.rs b/tests/codegen/exceptions.rs index 12e23d6242..714e5fdadc 100644 --- a/tests/codegen/exceptions.rs +++ b/tests/codegen/exceptions.rs @@ -62,6 +62,16 @@ fn test_builtin_exception_message_api() { assert_eq!(out, "boom:boom"); } +/// Verifies `getMessage()` returns a caller-owned string without consuming the +/// builtin Throwable payload used by later reads and `__toString()`. +#[test] +fn test_builtin_exception_get_message_does_not_consume_payload() { + let out = compile_and_run( + "getMessage(); echo \":\"; echo $e->getMessage(); echo \":\"; echo $e->__toString();", + ); + assert_eq!(out, "boom:boom:boom"); +} + /// Checks that Exception messages built from temporary string results survive the throw. #[test] fn test_builtin_exception_message_persists_concatenated_temporary() { From 1d138c33a222549073a07dff849c7f3c694e72e9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 09:30:38 +0200 Subject: [PATCH 0404/1208] Fix dynamic builtin callable wrappers --- src/codegen/lower_inst.rs | 22 +++++++------- src/ir_lower/expr/mod.rs | 5 ++- tests/codegen/spl/iterator_helpers.rs | 44 +++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index b7be4b56dc..76899e2cb4 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -1772,17 +1772,6 @@ fn first_class_callable_descriptor( method_name, )); } - if let Some(callee) = ctx.callable_function_by_name(target) { - return Ok(Some(FirstClassCallableDescriptor { - entry_label: function_symbol(&callee.name), - kind: callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, - sig: Some(function_signature_from_eir(callee)), - invocation: callable_descriptor::CallableDescriptorInvocation::named( - callable_descriptor::CallableDescriptorShape::Function, - callee.name.clone(), - ), - })); - } if ctx.has_extern_function(target) { return Ok(Some(FirstClassCallableDescriptor { entry_label: ctx.emitter.target.extern_symbol(target), @@ -1797,6 +1786,17 @@ fn first_class_callable_descriptor( if let Some(descriptor) = first_class_builtin_descriptor(ctx, target)? { return Ok(Some(descriptor)); } + if let Some(callee) = ctx.callable_function_by_name(target) { + return Ok(Some(FirstClassCallableDescriptor { + entry_label: function_symbol(&callee.name), + kind: callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, + sig: Some(function_signature_from_eir(callee)), + invocation: callable_descriptor::CallableDescriptorInvocation::named( + callable_descriptor::CallableDescriptorShape::Function, + callee.name.clone(), + ), + })); + } Ok(None) } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 889e06ee92..03edef2988 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -3879,10 +3879,13 @@ fn resolve_static_string_callable( if let Some(function_name) = lookup_folded_name(ctx.extern_functions.keys(), callback) { return Some(StaticCallableBinding::ExternFunction(function_name)); } + if let Some(function_name) = canonical_builtin_function_name(callback) { + return Some(StaticCallableBinding::Builtin(function_name)); + } if let Some(function_name) = lookup_folded_name(ctx.functions.keys(), callback) { return Some(StaticCallableBinding::UserFunction(function_name)); } - canonical_builtin_function_name(callback).map(StaticCallableBinding::Builtin) + None } /// Appends captured closure values after caller-visible operands for hidden ABI params. diff --git a/tests/codegen/spl/iterator_helpers.rs b/tests/codegen/spl/iterator_helpers.rs index 32fd8bf1d9..238f4525da 100644 --- a/tests/codegen/spl/iterator_helpers.rs +++ b/tests/codegen/spl/iterator_helpers.rs @@ -929,6 +929,50 @@ echo iterator_apply(new Range(), $callback, $args); assert_eq!(out, "2"); } +/// Verifies that iterator apply can dispatch dynamic string callbacks to `is_array`. +#[test] +fn test_iterator_apply_dynamic_string_is_array_callback_assoc_args() { + let out = compile_and_run( + r#"i = 0; } + public function rewind(): void { $this->i = 0; } + public function valid(): bool { return $this->i < 2; } + public function current(): int { return $this->i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } +} +$callback = "is_array"; +$args = ["value" => [1]]; +echo iterator_apply(new Range(), $callback, $args); +"#, + ); + assert_eq!(out, "2"); +} + +/// Verifies that iterator apply can dispatch dynamic string callbacks to type predicate aliases. +#[test] +fn test_iterator_apply_dynamic_string_is_integer_callback_assoc_args() { + let out = compile_and_run( + r#"i = 0; } + public function rewind(): void { $this->i = 0; } + public function valid(): bool { return $this->i < 2; } + public function current(): int { return $this->i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } +} +$callback = "is_integer"; +$args = ["value" => 1]; +echo iterator_apply(new Range(), $callback, $args); +"#, + ); + assert_eq!(out, "2"); +} + /// Verifies that iterator apply dynamic args for by ref callback use temp cells. #[test] fn test_iterator_apply_dynamic_args_for_by_ref_callback_use_temp_cells() { From ebaf011e4b0ea68628ba34706f696d912f42048c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 09:47:21 +0200 Subject: [PATCH 0405/1208] Add ReflectionClass hasConstant metadata --- crates/elephc-eval/src/context.rs | 71 ++++++++++++++ .../elephc-eval/src/interpreter/reflection.rs | 31 +++++++ .../elephc-eval/src/interpreter/statements.rs | 9 ++ .../tests/builtins_class_metadata.rs | 26 +++++- .../interpreter/tests/support/object_ops.rs | 3 + src/codegen/lower_inst/objects/reflection.rs | 92 +++++++++++++++++++ src/ir/module.rs | 2 + src/ir_lower/program.rs | 28 ++++++ src/types/checker/builtin_types/reflection.rs | 7 ++ tests/codegen/eval.rs | 28 +++++- tests/codegen/oop/attributes.rs | 28 +++++- 11 files changed, 314 insertions(+), 11 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 5b77d9260c..6a16f7bc2f 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -826,6 +826,31 @@ impl ElephcEvalContext { names } + /// Returns PHP case-sensitive constant names visible to `ReflectionClass::hasConstant()`. + pub fn class_constant_names(&self, class_name: &str) -> Vec { + let reflected_name = self + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let mut names = Vec::new(); + let mut seen = HashSet::new(); + if let Some(enum_decl) = self.enum_decl(&reflected_name) { + for case in enum_decl.cases() { + push_unique_constant_name(case.name(), &mut names, &mut seen); + } + } + for class in self.class_chain(&reflected_name).into_iter().rev() { + for constant in class.constants() { + push_unique_constant_name(constant.name(), &mut names, &mut seen); + } + for interface_name in class.interfaces() { + for constant in self.interface_constant_names(interface_name) { + push_unique_constant_name(&constant, &mut names, &mut seen); + } + } + } + names + } + /// Returns PHP case-insensitive method names declared by an eval interface hierarchy. pub fn interface_method_names(&self, interface_name: &str) -> Vec { let mut names = Vec::new(); @@ -846,6 +871,32 @@ impl ElephcEvalContext { names } + /// Returns PHP case-sensitive constant names declared by an eval interface hierarchy. + pub fn interface_constant_names(&self, interface_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + self.collect_interface_constant_names(interface_name, &mut names, &mut seen); + names + } + + /// Collects eval interface constants without duplicating inherited names. + fn collect_interface_constant_names( + &self, + interface_name: &str, + names: &mut Vec, + seen: &mut HashSet, + ) { + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + self.collect_interface_constant_names(parent, names, seen); + } + for constant in interface.constants() { + push_unique_constant_name(constant.name(), names, seen); + } + } + /// Returns PHP case-insensitive direct method names declared by an eval trait. pub fn trait_method_names(&self, trait_name: &str) -> Vec { let Some(trait_decl) = self.trait_decl(trait_name) else { @@ -872,6 +923,19 @@ impl ElephcEvalContext { names } + /// Returns PHP case-sensitive direct constant names declared by an eval trait. + pub fn trait_constant_names(&self, trait_name: &str) -> Vec { + let Some(trait_decl) = self.trait_decl(trait_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for constant in trait_decl.constants() { + push_unique_constant_name(constant.name(), &mut names, &mut seen); + } + names + } + /// Returns parent interface names for an eval-declared interface. pub fn interface_parent_names(&self, interface_name: &str) -> Vec { let mut parents = Vec::new(); @@ -1493,6 +1557,13 @@ fn push_unique_property_name(name: &str, names: &mut Vec, seen: &mut Has } } +/// Pushes a case-sensitive PHP class constant name once for ReflectionClass metadata. +fn push_unique_constant_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + if seen.insert(name.to_string()) { + names.push(name.to_string()); + } +} + /// Normalizes PHP constant names for case-sensitive eval dynamic probes. fn normalize_constant_name(name: &str) -> String { name.trim_start_matches('\\').to_string() diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 77d9fcadbd..2134811098 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -143,6 +143,37 @@ pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( .map(Some) } +/// Handles eval-backed `ReflectionClass::hasConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let constant_name = eval_reflection_string_arg(args[0], values)?; + let constant_names = if context.has_interface(&reflected_name) { + context.interface_constant_names(&reflected_name) + } else if context.has_trait(&reflected_name) { + context.trait_constant_names(&reflected_name) + } else { + context.class_constant_names(&reflected_name) + }; + values + .bool_value(constant_names.iter().any(|name| name == &constant_name)) + .map(Some) +} + /// Builds an eval-backed `ReflectionClass` object when the reflected class-like exists in eval. fn eval_reflection_class_new( evaluated_args: Vec, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 888b93eddb..95777ac4c6 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2180,6 +2180,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_has_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(instance) = eval_reflection_class_new_instance_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index b2e477b81f..2f856f247e 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -472,33 +472,42 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies ReflectionClass reports eval class-like method and property membership. +/// Verifies ReflectionClass reports eval class-like method, property, and constant membership. #[test] fn execute_program_reflects_eval_class_member_existence() { let program = parse_fragment( br#"class EvalMemberParent { + const PARENT_CONST = 1; private function hiddenParent() {} protected static function parentStatic() {} private $hiddenProp; protected static $parentStaticProp; } -class EvalMemberChild extends EvalMemberParent { +interface EvalMemberClassIface { + const CLASS_LIMIT = 10; +} +class EvalMemberChild extends EvalMemberParent implements EvalMemberClassIface { + const CHILD_CONST = 2; public function ChildMethod() {} public $childProp; } interface EvalMemberIfaceParent { + const PARENT_LIMIT = 10; public function parentRequirement(); } interface EvalMemberIface extends EvalMemberIfaceParent { + const CHILD_LIMIT = 20; public function childRequirement(); public string $hook { get; } } trait EvalMemberTrait { + const TRAIT_CONST = 30; private function traitHidden() {} public $traitProp; } enum EvalMemberPureEnum { case Ready; + const LEVEL = 40; public function label() { return "ok"; } } enum EvalMemberBackedEnum: string { @@ -514,25 +523,36 @@ echo $child->hasProperty("childProp") ? "C" : "c"; echo $child->hasProperty("hiddenProp") ? "H" : "h"; echo $child->hasProperty("parentStaticProp") ? "T" : "t"; echo $child->hasProperty("childprop") ? "W" : "w"; +echo $child->hasConstant("CHILD_CONST") ? "D" : "d"; +echo $child->hasConstant("PARENT_CONST") ? "P" : "p"; +echo $child->hasConstant("CLASS_LIMIT") ? "A" : "a"; +echo $child->hasConstant("child_const") ? "Z" : "z"; echo ":"; $iface = new ReflectionClass("EvalMemberIface"); echo $iface->hasMethod("parentrequirement") ? "I" : "i"; echo $iface->hasMethod("childRequirement") ? "J" : "j"; echo $iface->hasProperty("hook") ? "K" : "k"; +echo $iface->hasConstant("PARENT_LIMIT") ? "L" : "l"; +echo $iface->hasConstant("CHILD_LIMIT") ? "C" : "c"; echo ":"; $trait = new ReflectionClass("EvalMemberTrait"); echo $trait->hasMethod("traithidden") ? "R" : "r"; echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo $trait->hasConstant("TRAIT_CONST") ? "K" : "k"; echo ":"; $pure = new ReflectionClass("EvalMemberPureEnum"); echo $pure->hasMethod("cases") ? "E" : "e"; echo $pure->hasMethod("label") ? "L" : "l"; echo $pure->hasProperty("name") ? "N" : "n"; echo $pure->hasProperty("value") ? "V" : "v"; +echo $pure->hasConstant("Ready") ? "G" : "g"; +echo $pure->hasConstant("LEVEL") ? "F" : "f"; +echo $pure->hasConstant("ready") ? "R" : "r"; echo ":"; $backed = new ReflectionClass("EvalMemberBackedEnum"); echo $backed->hasMethod("tryfrom") ? "B" : "b"; echo $backed->hasProperty("value") ? "Y" : "y"; +echo $backed->hasConstant("Ready") ? "Q" : "q"; return true;"#, ) .expect("parse eval fragment"); @@ -541,7 +561,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "MPSx:ChTw:IJK:RU:ELNv:BY"); + assert_eq!(values.output, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BYQ"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 68305cb300..05caffa79f 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -182,6 +182,9 @@ impl FakeOps { (FakeValue::Object(properties), "hasproperty") if args.len() == 1 => { self.object_string_array_contains(&properties, "__property_names", args[0], false) } + (FakeValue::Object(properties), "hasconstant") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__constant_names", args[0], false) + } (FakeValue::Object(properties), "implementsinterface") if args.len() == 1 => { let direct = self.object_string_array_contains( &properties, diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 081829c9a8..52e6873c9c 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -29,6 +29,7 @@ struct ReflectionOwnerMetadata { trait_names: Vec, method_names: Vec, property_names: Vec, + constant_names: Vec, method_members: Vec, property_members: Vec, constructor_member: Option, @@ -164,6 +165,11 @@ fn emit_reflection_owner_object( "__property_names", &metadata.property_names, )?; + emit_reflection_string_array_property_by_name( + ctx, + "__constant_names", + &metadata.constant_names, + )?; emit_reflection_member_array_property_by_name( ctx, "__methods", @@ -642,6 +648,7 @@ fn reflection_class_metadata_for_name( let is_enum = is_reflection_enum(ctx, class_name); let method_names = reflection_class_method_names(ctx, class_name); let property_names = reflection_class_property_names(ctx, class_name, info); + let constant_names = reflection_class_constant_names(ctx, class_name, info); let method_members = reflection_class_method_members(info, &method_names); let property_members = reflection_class_property_members(ctx, class_name, info, &property_names); @@ -656,6 +663,7 @@ fn reflection_class_metadata_for_name( trait_names: info.used_traits.clone(), method_names, property_names, + constant_names, method_members, property_members, constructor_member, @@ -681,6 +689,7 @@ fn reflection_class_metadata_for_name( if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { let method_names = reflection_interface_method_names(ctx, interface_name); let property_names = reflection_interface_property_names(ctx, interface_name); + let constant_names = reflection_interface_constant_names(ctx, interface_name); let method_members = ctx .module .interface_infos @@ -697,6 +706,7 @@ fn reflection_class_metadata_for_name( trait_names: Vec::new(), method_names, property_names, + constant_names, method_members, property_members, constructor_member, @@ -723,6 +733,7 @@ fn reflection_class_metadata_for_name( .unwrap_or_default(); let method_names = reflection_trait_method_names(ctx, trait_name); let property_names = reflection_trait_property_names(ctx, trait_name); + let constant_names = reflection_trait_constant_names(ctx, trait_name); let method_members = ctx .module .declared_trait_methods @@ -739,6 +750,7 @@ fn reflection_class_metadata_for_name( trait_names, method_names, property_names, + constant_names, method_members, property_members, constructor_member, @@ -832,6 +844,7 @@ fn reflection_method_owner_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + constant_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -873,6 +886,7 @@ fn reflection_property_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + constant_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1026,6 +1040,7 @@ fn reflection_class_constant_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + constant_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1064,6 +1079,7 @@ fn reflection_class_constant_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + constant_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1109,6 +1125,7 @@ fn reflection_enum_case_metadata( trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + constant_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1276,6 +1293,41 @@ fn reflection_class_property_names( names } +/// Returns PHP case-sensitive class constant names visible to `ReflectionClass::hasConstant()`. +fn reflection_class_constant_names( + ctx: &FunctionContext<'_>, + class_name: &str, + _info: &crate::types::ClassInfo, +) -> Vec { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(enum_info) = ctx.module.enum_infos.get(class_name) { + for case in &enum_info.cases { + push_unique_constant_name(&case.name, &mut names, &mut seen); + } + } + let mut current = Some(class_name.to_string()); + while let Some(current_name) = current { + let Some((resolved_name, current_info)) = resolve_reflection_class(ctx, ¤t_name) + else { + break; + }; + for constant in current_info.constants.keys() { + push_unique_constant_name(constant, &mut names, &mut seen); + } + for interface_name in ¤t_info.interfaces { + for constant in reflection_interface_constant_names(ctx, interface_name) { + push_unique_constant_name(&constant, &mut names, &mut seen); + } + } + current = current_info.parent.clone(); + if current.as_deref() == Some(resolved_name) { + break; + } + } + names +} + /// Returns true when a property should be visible for `ReflectionClass::hasProperty()`. fn reflection_property_visible_from_class( info: &crate::types::ClassInfo, @@ -1653,6 +1705,25 @@ fn reflection_interface_property_names( names } +/// Returns PHP case-sensitive constant names declared by an interface and its parents. +fn reflection_interface_constant_names( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return Vec::new(); + }; + let Some(info) = ctx.module.interface_infos.get(interface_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for constant in info.constants.keys() { + push_unique_constant_name(constant, &mut names, &mut seen); + } + names +} + /// Returns PHP case-insensitive direct method names declared by a trait. fn reflection_trait_method_names(ctx: &FunctionContext<'_>, trait_name: &str) -> Vec { ctx.module @@ -1671,6 +1742,15 @@ fn reflection_trait_property_names(ctx: &FunctionContext<'_>, trait_name: &str) .unwrap_or_default() } +/// Returns PHP case-sensitive direct constant names declared by a trait. +fn reflection_trait_constant_names(ctx: &FunctionContext<'_>, trait_name: &str) -> Vec { + ctx.module + .declared_trait_constant_names + .get(trait_name) + .cloned() + .unwrap_or_default() +} + /// Appends lower-case method names while preserving first-seen order. fn push_unique_method_names<'a>( method_names: impl Iterator, @@ -1696,6 +1776,17 @@ fn push_unique_property_name( } } +/// Appends one case-sensitive class constant name while preserving first-seen order. +fn push_unique_constant_name( + constant_name: &str, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(constant_name.to_string()) { + names.push(constant_name.to_string()); + } +} + /// Looks up class-constant metadata by PHP-style class name and case-sensitive constant name. fn resolve_reflection_class_constant<'a>( ctx: &'a FunctionContext<'_>, @@ -1734,6 +1825,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { trait_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), + constant_names: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, diff --git a/src/ir/module.rs b/src/ir/module.rs index 9dd9368eee..de59704e26 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -72,6 +72,7 @@ pub struct Module { pub declared_trait_method_names: HashMap>, pub declared_trait_methods: HashMap>, pub declared_trait_property_names: HashMap>, + pub declared_trait_constant_names: HashMap>, pub class_infos: HashMap, pub interface_infos: HashMap, pub enum_infos: HashMap, @@ -109,6 +110,7 @@ impl Module { declared_trait_method_names: HashMap::new(), declared_trait_methods: HashMap::new(), declared_trait_property_names: HashMap::new(), + declared_trait_constant_names: HashMap::new(), class_infos: HashMap::new(), interface_infos: HashMap::new(), enum_infos: HashMap::new(), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 3318de9b63..528ca0572b 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -76,6 +76,7 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec module.declared_trait_method_names = collect_declared_trait_method_names(program); module.declared_trait_methods = collect_declared_trait_methods(program); module.declared_trait_property_names = collect_declared_trait_property_names(program); + module.declared_trait_constant_names = collect_declared_trait_constant_names(program); module.class_infos = check_result.classes.clone(); module.interface_infos = check_result.interfaces.clone(); module.enum_infos = check_result.enums.clone(); @@ -501,6 +502,33 @@ fn collect_declared_trait_property_names(program: &Program) -> HashMap HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .map(|constant| constant.name.clone()) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_constant_names(body)); + } + _ => {} + } + } + constants +} + /// Recursively collects source-declared names that are present in checked metadata. fn collect_program_declared_names( program: &Program, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index e2fe6b0430..7ae3b89180 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -739,6 +739,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(string_array_type()), empty_array(), ), + builtin_property( + "__constant_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), builtin_property( "__methods", Visibility::Private, @@ -795,6 +801,7 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), + builtin_reflection_class_has_name_method("hasConstant", "__constant_names", false), builtin_reflection_class_implements_interface_method(), builtin_reflection_class_array_method( "getMethods", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a93a42cab0..10224b1f9b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6052,34 +6052,43 @@ echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e";'); assert_eq!(out.stdout, "aBCprite"); } -/// Verifies eval ReflectionClass reports method and property membership through the bridge. +/// Verifies eval ReflectionClass reports method, property, and constant membership through the bridge. #[test] fn test_eval_reflection_class_member_existence() { let out = compile_and_run_capture( r#"hasProperty("childProp") ? "C" : "c"; echo $child->hasProperty("hiddenProp") ? "H" : "h"; echo $child->hasProperty("parentStaticProp") ? "T" : "t"; echo $child->hasProperty("childprop") ? "W" : "w"; +echo $child->hasConstant("CHILD_CONST") ? "D" : "d"; +echo $child->hasConstant("PARENT_CONST") ? "P" : "p"; +echo $child->hasConstant("CLASS_LIMIT") ? "A" : "a"; +echo $child->hasConstant("child_const") ? "Z" : "z"; echo ":"; $iface = new ReflectionClass("EvalMemberIface"); echo $iface->hasMethod("parentrequirement") ? "I" : "i"; echo $iface->hasMethod("childRequirement") ? "J" : "j"; echo $iface->hasProperty("hook") ? "K" : "k"; +echo $iface->hasConstant("PARENT_LIMIT") ? "L" : "l"; +echo $iface->hasConstant("CHILD_LIMIT") ? "C" : "c"; echo ":"; $trait = new ReflectionClass("EvalMemberTrait"); echo $trait->hasMethod("traithidden") ? "R" : "r"; echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo $trait->hasConstant("TRAIT_CONST") ? "K" : "k"; echo ":"; $pure = new ReflectionClass("EvalMemberPureEnum"); echo $pure->hasMethod("cases") ? "E" : "e"; echo $pure->hasMethod("label") ? "L" : "l"; echo $pure->hasProperty("name") ? "N" : "n"; echo $pure->hasProperty("value") ? "V" : "v"; +echo $pure->hasConstant("Ready") ? "G" : "g"; +echo $pure->hasConstant("LEVEL") ? "F" : "f"; +echo $pure->hasConstant("ready") ? "R" : "r"; echo ":"; $backed = new ReflectionClass("EvalMemberBackedEnum"); echo $backed->hasMethod("tryfrom") ? "B" : "b"; -echo $backed->hasProperty("value") ? "Y" : "y";'); +echo $backed->hasProperty("value") ? "Y" : "y"; +echo $backed->hasConstant("Ready") ? "Q" : "q";'); "#, ); assert!( @@ -6121,7 +6141,7 @@ echo $backed->hasProperty("value") ? "Y" : "y";'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "MPSx:ChTw:IJK:RU:ELNv:BY"); + assert_eq!(out.stdout, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BYQ"); } /// Verifies eval ReflectionMethod and ReflectionProperty expose member predicates through the bridge. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 405e3212f2..4baddca69f 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1233,35 +1233,44 @@ echo (new ReflectionClass(ReflectInstEnum::class))->isInstantiable() ? "E" : "e" assert_eq!(out, "aBCprite"); } -/// Verifies that `ReflectionClass::hasMethod()` and `hasProperty()` report -/// PHP-visible members for static class-like metadata. +/// Verifies that `ReflectionClass::hasMethod()`, `hasProperty()`, and +/// `hasConstant()` report PHP-visible members for static class-like metadata. #[test] fn test_reflection_class_reports_member_existence() { let out = compile_and_run( r#"hasProperty("childProp") ? "C" : "c"; echo $child->hasProperty("hiddenProp") ? "H" : "h"; echo $child->hasProperty("parentStaticProp") ? "T" : "t"; echo $child->hasProperty("childprop") ? "W" : "w"; +echo $child->hasConstant("CHILD_CONST") ? "D" : "d"; +echo $child->hasConstant("PARENT_CONST") ? "P" : "p"; +echo $child->hasConstant("CLASS_LIMIT") ? "A" : "a"; +echo $child->hasConstant("child_const") ? "Z" : "z"; echo ":"; $iface = new ReflectionClass(StaticMemberIface::class); echo $iface->hasMethod("parentrequirement") ? "I" : "i"; echo $iface->hasMethod("childRequirement") ? "J" : "j"; echo $iface->hasProperty("hook") ? "K" : "k"; +echo $iface->hasConstant("PARENT_LIMIT") ? "L" : "l"; +echo $iface->hasConstant("CHILD_LIMIT") ? "C" : "c"; echo ":"; $trait = new ReflectionClass(StaticMemberTrait::class); echo $trait->hasMethod("traithidden") ? "R" : "r"; echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo $trait->hasConstant("TRAIT_CONST") ? "K" : "k"; echo ":"; $pure = new ReflectionClass(StaticMemberPureEnum::class); echo $pure->hasMethod("cases") ? "E" : "e"; echo $pure->hasMethod("label") ? "L" : "l"; echo $pure->hasProperty("name") ? "N" : "n"; echo $pure->hasProperty("value") ? "V" : "v"; +echo $pure->hasConstant("Ready") ? "G" : "g"; +echo $pure->hasConstant("LEVEL") ? "F" : "f"; +echo $pure->hasConstant("ready") ? "R" : "r"; echo ":"; $backed = new ReflectionClass(StaticMemberBackedEnum::class); echo $backed->hasMethod("tryfrom") ? "B" : "b"; echo $backed->hasProperty("name") ? "N" : "n"; echo $backed->hasProperty("value") ? "Y" : "y"; +echo $backed->hasConstant("Ready") ? "Q" : "q"; "#, ); - assert_eq!(out, "MPSx:ChTw:IJK:RU:ELNv:BNY"); + assert_eq!(out, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BNYQ"); } /// Verifies that `ReflectionClass` reports implemented interface and used trait names. From 9c315e670141f2d7019c5a171e0527ed465c89d2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 10:23:05 +0200 Subject: [PATCH 0406/1208] Add ReflectionClass constant value metadata --- .../elephc-eval/src/interpreter/reflection.rs | 84 +++ .../elephc-eval/src/interpreter/statements.rs | 18 + .../tests/builtins_class_metadata.rs | 56 +- .../interpreter/tests/support/object_ops.rs | 23 +- src/codegen/lower_inst/objects/reflection.rs | 513 +++++++++++++++++- src/ir/module.rs | 2 + src/ir_lower/program.rs | 30 +- src/types/checker/builtin_types/reflection.rs | 66 +++ tests/codegen/eval.rs | 53 ++ tests/codegen/oop/attributes.rs | 51 ++ 10 files changed, 879 insertions(+), 17 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 2134811098..637f156ef9 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -174,6 +174,90 @@ pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( .map(Some) } +/// Handles eval-backed `ReflectionClass::getConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let constant_name = eval_reflection_string_arg(args[0], values)?; + if let Some(value) = eval_reflection_constant_value(&reflected_name, &constant_name, context) { + return Ok(Some(value)); + } + values.bool_value(false).map(Some) +} + +/// Handles eval-backed `ReflectionClass::getConstants()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getConstants") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = eval_reflection_constant_names(&reflected_name, context); + let mut result = values.assoc_new(names.len())?; + for name in names { + let Some(value) = eval_reflection_constant_value(&reflected_name, &name, context) else { + continue; + }; + let key = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + +/// Returns the constant names visible through eval-backed `ReflectionClass`. +fn eval_reflection_constant_names( + reflected_name: &str, + context: &ElephcEvalContext, +) -> Vec { + if context.has_interface(reflected_name) { + context.interface_constant_names(reflected_name) + } else if context.has_trait(reflected_name) { + context.trait_constant_names(reflected_name) + } else { + context.class_constant_names(reflected_name) + } +} + +/// Returns a materialized eval constant value for Reflection without visibility checks. +fn eval_reflection_constant_value( + reflected_name: &str, + constant_name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(case) = context.enum_case(reflected_name, constant_name) { + return Some(case); + } + let (declaring_class, constant) = context.class_constant(reflected_name, constant_name)?; + context.class_constant_cell(&declaring_class, constant.name()) +} + /// Builds an eval-backed `ReflectionClass` object when the reflected class-like exists in eval. fn eval_reflection_class_new( evaluated_args: Vec, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 95777ac4c6..66527f6fd2 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2189,6 +2189,24 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_get_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_constants_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(instance) = eval_reflection_class_new_instance_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 2f856f247e..487bd17754 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -565,6 +565,60 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass returns eval class-like constant values and enum cases. +#[test] +fn execute_program_reflects_eval_class_constant_values() { + let program = parse_fragment( + br#"class EvalReflectConstBase { + public const BASE = 1; +} +interface EvalReflectConstIface { + public const LIMIT = 2; +} +trait EvalReflectConstTrait { + public const TRAIT_VALUE = 3; +} +class EvalReflectConstChild extends EvalReflectConstBase implements EvalReflectConstIface { + private const SECRET = 9; + public const OWN = "own"; + public const SUM = 5; +} +enum EvalReflectConstEnum { + case Ready; + public const LEVEL = 40; +} +$ref = new ReflectionClass("EvalReflectConstChild"); +$all = $ref->getConstants(); +echo $ref->getConstant("OWN"); echo ":"; +echo $ref->getConstant("BASE"); echo ":"; +echo $ref->getConstant("LIMIT"); echo ":"; +echo $ref->getConstant("SECRET"); echo ":"; +echo $ref->getConstant("SUM"); echo ":"; +echo $ref->getConstant("own") ? "bad" : "missing"; +echo ":"; echo count($all); echo ":"; echo $all["OWN"]; echo ":"; echo $all["BASE"]; echo ":"; echo $all["LIMIT"]; +$trait = new ReflectionClass("EvalReflectConstTrait"); +$traitAll = $trait->getConstants(); +echo ":"; echo $trait->getConstant("TRAIT_VALUE"); echo ":"; echo count($traitAll); echo ":"; echo $traitAll["TRAIT_VALUE"]; +$enum = new ReflectionClass("EvalReflectConstEnum"); +$case = $enum->getConstant("Ready"); +$enumAll = $enum->getConstants(); +echo ":"; echo $case->name; +echo ":"; echo $enum->getConstant("LEVEL"); echo ":"; echo $enumAll["LEVEL"]; echo ":"; echo count($enumAll); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "own:1:2:9:5:missing:5:own:1:2:3:1:3:Ready:40:40:2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod and ReflectionProperty expose eval member predicate metadata. #[test] fn execute_program_reflects_eval_member_predicates() { @@ -622,7 +676,7 @@ return true;"#, #[test] fn execute_program_reflects_eval_method_parameters() { let program = parse_fragment( -br##"class EvalReflectParamTarget { + br##"class EvalReflectParamTarget { public function run(int &$first, \App\Name|null $second = null, &...$rest) {} } $method = new ReflectionMethod("EvalReflectParamTarget", "run"); diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 05caffa79f..7c7b976c89 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -185,6 +185,17 @@ impl FakeOps { (FakeValue::Object(properties), "hasconstant") if args.len() == 1 => { self.object_string_array_contains(&properties, "__constant_names", args[0], false) } + (FakeValue::Object(properties), "getconstant") if args.len() == 1 => { + let Some(constants) = Self::object_property(&properties, "__constants") else { + return self.bool_value(false); + }; + let exists = self.runtime_array_key_exists(args[0], constants)?; + if matches!(self.get(exists), FakeValue::Bool(true)) { + self.runtime_array_get(constants, args[0]) + } else { + self.bool_value(false) + } + } (FakeValue::Object(properties), "implementsinterface") if args.len() == 1 => { let direct = self.object_string_array_contains( &properties, @@ -229,6 +240,10 @@ impl FakeOps { Self::object_property(&properties, "__properties") .map_or_else(|| self.runtime_array_new(0), Ok) } + (FakeValue::Object(properties), "getconstants") if args.is_empty() => { + Self::object_property(&properties, "__constants") + .map_or_else(|| self.runtime_assoc_new(0), Ok) + } (FakeValue::Object(properties), "getarguments") if args.is_empty() => { Self::object_property(&properties, "__args") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -416,6 +431,8 @@ impl FakeOps { let namespace_name = self.string(namespace_name)?; let short_name = self.string(short_name)?; let in_namespace = self.bool_value(has_namespace)?; + let constant_names = self.runtime_array_new(0)?; + let constants = self.runtime_assoc_new(0)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_interface".to_string(), is_interface)); @@ -431,6 +448,8 @@ impl FakeOps { properties.push(("__trait_names".to_string(), trait_names)); properties.push(("__method_names".to_string(), method_names)); properties.push(("__property_names".to_string(), property_names)); + properties.push(("__constant_names".to_string(), constant_names)); + properties.push(("__constants".to_string(), constants)); properties.push(("__methods".to_string(), method_objects)); properties.push(("__parent_class".to_string(), parent_class)); properties.push(("__properties".to_string(), property_objects)); @@ -654,8 +673,8 @@ fn fake_runtime_exception_like_class(class_name: &str) -> bool { "Error", "ValueError", ] - .iter() - .any(|known| class_name.eq_ignore_ascii_case(known)) + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)) } /// Splits one PHP class-like name into namespace and short-name parts. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 52e6873c9c..abbfa2419c 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -12,10 +12,17 @@ //! metadata slots instead of running their public empty bodies. use crate::codegen::platform::Arch; -use crate::codegen::{abi, emit_box_current_value_as_mixed, CodegenIrError, Result}; +use crate::codegen::literal_defaults::{ + emit_boxed_bool_literal_to_result, emit_boxed_float_literal_to_result, + emit_boxed_int_literal_to_result, emit_boxed_null_literal_to_result, + emit_boxed_string_literal_default_to_result, emit_empty_assoc_array_literal_to_result, +}; +use crate::codegen::{ + abi, emit_box_current_value_as_mixed, runtime_value_tag, CodegenIrError, Result, +}; use crate::ir::{Immediate, Instruction, Op, TraitMethodInfo, ValueDef, ValueId}; -use crate::names::php_symbol_key; -use crate::parser::ast::Visibility; +use crate::names::{enum_case_symbol, php_symbol_key}; +use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, Visibility}; use crate::types::{AttrArgEntry, FunctionSig, InterfaceInfo, PhpType}; use super::super::super::context::FunctionContext; @@ -30,6 +37,7 @@ struct ReflectionOwnerMetadata { method_names: Vec, property_names: Vec, constant_names: Vec, + constant_members: Vec, method_members: Vec, property_members: Vec, constructor_member: Option, @@ -69,6 +77,27 @@ struct ReflectionParameterMember { has_type: bool, } +/// Metadata for one constant entry returned by `ReflectionClass::getConstants()`. +#[derive(Clone)] +struct ReflectionConstantMember { + name: String, + value: ReflectionConstantValue, +} + +/// Compile-time value forms supported by Reflection constant metadata emission. +#[derive(Clone)] +enum ReflectionConstantValue { + Int(i64), + Bool(bool), + Float(f64), + Str(String), + Null, + EnumCase { + enum_name: String, + case_name: String, + }, +} + /// Compile-time parameter selector from `ReflectionParameter::__construct()`. enum ReflectionParameterSelector { Name(String), @@ -170,6 +199,11 @@ fn emit_reflection_owner_object( "__constant_names", &metadata.constant_names, )?; + emit_reflection_constant_array_property_by_name( + ctx, + "__constants", + &metadata.constant_members, + )?; emit_reflection_member_array_property_by_name( ctx, "__methods", @@ -649,6 +683,7 @@ fn reflection_class_metadata_for_name( let method_names = reflection_class_method_names(ctx, class_name); let property_names = reflection_class_property_names(ctx, class_name, info); let constant_names = reflection_class_constant_names(ctx, class_name, info); + let constant_members = reflection_class_constant_members(ctx, class_name, info)?; let method_members = reflection_class_method_members(info, &method_names); let property_members = reflection_class_property_members(ctx, class_name, info, &property_names); @@ -664,6 +699,7 @@ fn reflection_class_metadata_for_name( method_names, property_names, constant_names, + constant_members, method_members, property_members, constructor_member, @@ -690,6 +726,7 @@ fn reflection_class_metadata_for_name( let method_names = reflection_interface_method_names(ctx, interface_name); let property_names = reflection_interface_property_names(ctx, interface_name); let constant_names = reflection_interface_constant_names(ctx, interface_name); + let constant_members = reflection_interface_constant_members(ctx, interface_name)?; let method_members = ctx .module .interface_infos @@ -707,6 +744,7 @@ fn reflection_class_metadata_for_name( method_names, property_names, constant_names, + constant_members, method_members, property_members, constructor_member, @@ -734,6 +772,7 @@ fn reflection_class_metadata_for_name( let method_names = reflection_trait_method_names(ctx, trait_name); let property_names = reflection_trait_property_names(ctx, trait_name); let constant_names = reflection_trait_constant_names(ctx, trait_name); + let constant_members = reflection_trait_constant_members(ctx, trait_name)?; let method_members = ctx .module .declared_trait_methods @@ -751,6 +790,7 @@ fn reflection_class_metadata_for_name( method_names, property_names, constant_names, + constant_members, method_members, property_members, constructor_member, @@ -845,6 +885,7 @@ fn reflection_method_owner_metadata( method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), + constant_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -887,6 +928,7 @@ fn reflection_property_metadata( method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), + constant_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1041,6 +1083,7 @@ fn reflection_class_constant_metadata( method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), + constant_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1080,6 +1123,7 @@ fn reflection_class_constant_metadata( method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), + constant_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1126,6 +1170,7 @@ fn reflection_enum_case_metadata( method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), + constant_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1328,6 +1373,107 @@ fn reflection_class_constant_names( names } +/// Returns materializable class constant values for `ReflectionClass::getConstants()`. +fn reflection_class_constant_members( + ctx: &FunctionContext<'_>, + class_name: &str, + _info: &crate::types::ClassInfo, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(enum_info) = ctx.module.enum_infos.get(class_name) { + for case in &enum_info.cases { + push_unique_constant_member( + &case.name, + ReflectionConstantValue::EnumCase { + enum_name: class_name.to_string(), + case_name: case.name.clone(), + }, + &mut members, + &mut seen, + ); + } + } + let mut current = Some(class_name.to_string()); + while let Some(current_name) = current { + let Some((resolved_name, current_info)) = resolve_reflection_class(ctx, ¤t_name) + else { + break; + }; + for (constant_name, value_expr) in ¤t_info.constants { + if seen.contains(constant_name) { + continue; + } + let value = + reflection_constant_value(ctx, resolved_name, Some(current_info), value_expr, 0)?; + push_unique_constant_member(constant_name, value, &mut members, &mut seen); + } + for interface_name in ¤t_info.interfaces { + for member in reflection_interface_constant_members(ctx, interface_name)? { + push_unique_constant_member(&member.name, member.value, &mut members, &mut seen); + } + } + current = current_info.parent.clone(); + if current.as_deref() == Some(resolved_name) { + break; + } + } + Ok(members) +} + +/// Returns materializable interface constant values for ReflectionClass metadata. +fn reflection_interface_constant_members( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + collect_interface_constant_members(ctx, interface_name, &mut members, &mut seen)?; + Ok(members) +} + +/// Recursively appends interface constants, preserving inherited-interface precedence. +fn collect_interface_constant_members( + ctx: &FunctionContext<'_>, + interface_name: &str, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) -> Result<()> { + let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { + return Ok(()); + }; + for parent in &interface_info.parents { + collect_interface_constant_members(ctx, parent, members, seen)?; + } + for (constant_name, value_expr) in &interface_info.constants { + if seen.contains(constant_name) { + continue; + } + let value = reflection_constant_value(ctx, interface_name, None, value_expr, 0)?; + push_unique_constant_member(constant_name, value, members, seen); + } + Ok(()) +} + +/// Returns materializable direct trait constant values for ReflectionClass metadata. +fn reflection_trait_constant_members( + ctx: &FunctionContext<'_>, + trait_name: &str, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(constants) = ctx.module.declared_trait_constants.get(trait_name) { + for (constant_name, value_expr) in constants { + if seen.contains(constant_name) { + continue; + } + let value = reflection_constant_value(ctx, trait_name, None, value_expr, 0)?; + push_unique_constant_member(constant_name, value, &mut members, &mut seen); + } + } + Ok(members) +} + /// Returns true when a property should be visible for `ReflectionClass::hasProperty()`. fn reflection_property_visible_from_class( info: &crate::types::ClassInfo, @@ -1787,6 +1933,237 @@ fn push_unique_constant_name( } } +/// Appends one constant metadata member while preserving first-seen order. +fn push_unique_constant_member( + constant_name: &str, + value: ReflectionConstantValue, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(constant_name.to_string()) { + members.push(ReflectionConstantMember { + name: constant_name.to_string(), + value, + }); + } +} + +/// Evaluates one class/interface/trait constant expression for static Reflection metadata. +fn reflection_constant_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + expr: &Expr, + depth: usize, +) -> Result { + if depth > 16 { + return Err(CodegenIrError::unsupported( + "deep recursive ReflectionClass constant metadata", + )); + } + match &expr.kind { + ExprKind::IntLiteral(value) => Ok(ReflectionConstantValue::Int(*value)), + ExprKind::BoolLiteral(value) => Ok(ReflectionConstantValue::Bool(*value)), + ExprKind::FloatLiteral(value) => Ok(ReflectionConstantValue::Float(*value)), + ExprKind::StringLiteral(value) => Ok(ReflectionConstantValue::Str(value.clone())), + ExprKind::Null => Ok(ReflectionConstantValue::Null), + ExprKind::Negate(inner) => { + match reflection_constant_value(ctx, current_class, current_info, inner, depth + 1)? { + ReflectionConstantValue::Int(value) => Ok(ReflectionConstantValue::Int(-value)), + ReflectionConstantValue::Float(value) => Ok(ReflectionConstantValue::Float(-value)), + other => Err(unsupported_reflection_constant_value(other)), + } + } + ExprKind::BinaryOp { left, op, right } => reflection_binary_constant_value( + ctx, + current_class, + current_info, + left, + op, + right, + depth + 1, + ), + ExprKind::ClassConstant { receiver } => { + let class_name = + reflection_static_receiver_name(current_class, current_info, receiver)?; + Ok(ReflectionConstantValue::Str(class_name)) + } + ExprKind::ScopedConstantAccess { receiver, name } => reflection_scoped_constant_value( + ctx, + current_class, + current_info, + receiver, + name, + depth + 1, + ), + other => Err(CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata expression {:?}", + other + ))), + } +} + +/// Evaluates one supported binary operator in a static Reflection constant expression. +fn reflection_binary_constant_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + left: &Expr, + op: &BinOp, + right: &Expr, + depth: usize, +) -> Result { + let left = reflection_constant_value(ctx, current_class, current_info, left, depth)?; + let right = reflection_constant_value(ctx, current_class, current_info, right, depth)?; + match (left, op, right) { + (ReflectionConstantValue::Int(left), BinOp::Add, ReflectionConstantValue::Int(right)) => { + Ok(ReflectionConstantValue::Int(left + right)) + } + (ReflectionConstantValue::Int(left), BinOp::Sub, ReflectionConstantValue::Int(right)) => { + Ok(ReflectionConstantValue::Int(left - right)) + } + (ReflectionConstantValue::Int(left), BinOp::Mul, ReflectionConstantValue::Int(right)) => { + Ok(ReflectionConstantValue::Int(left * right)) + } + (ReflectionConstantValue::Int(left), BinOp::Mod, ReflectionConstantValue::Int(right)) => { + Ok(ReflectionConstantValue::Int(left % right)) + } + (ReflectionConstantValue::Int(left), BinOp::Pow, ReflectionConstantValue::Int(right)) + if right >= 0 => + { + Ok(ReflectionConstantValue::Int(left.pow(right as u32))) + } + ( + ReflectionConstantValue::Str(left), + BinOp::Concat, + ReflectionConstantValue::Str(right), + ) => Ok(ReflectionConstantValue::Str(format!("{}{}", left, right))), + (left, _, right) => Err(CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata binary value {:?} {:?}", + reflection_constant_value_kind(&left), + reflection_constant_value_kind(&right) + ))), + } +} + +/// Resolves and evaluates one scoped class/interface/trait constant value. +fn reflection_scoped_constant_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + receiver: &StaticReceiver, + constant_name: &str, + depth: usize, +) -> Result { + let class_name = reflection_static_receiver_name(current_class, current_info, receiver)?; + if let Some((resolved_name, info)) = resolve_reflection_class(ctx, &class_name) { + if let Some(value_expr) = info.constants.get(constant_name) { + return reflection_constant_value(ctx, resolved_name, Some(info), value_expr, depth); + } + for interface_name in &info.interfaces { + if let Some(value_expr) = + reflection_interface_constant_expr(ctx, interface_name, constant_name) + { + return reflection_constant_value(ctx, interface_name, None, &value_expr, depth); + } + } + } + if let Some(interface_name) = resolve_reflection_interface(ctx, &class_name) { + if let Some(value_expr) = + reflection_interface_constant_expr(ctx, interface_name, constant_name) + { + return reflection_constant_value(ctx, interface_name, None, &value_expr, depth); + } + } + if let Some(trait_name) = resolve_reflection_trait(ctx, &class_name) { + if let Some(value_expr) = ctx + .module + .declared_trait_constants + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + { + return reflection_constant_value(ctx, trait_name, None, value_expr, depth); + } + } + if ctx + .module + .enum_infos + .get(&class_name) + .is_some_and(|info| info.cases.iter().any(|case| case.name == constant_name)) + { + return Ok(ReflectionConstantValue::EnumCase { + enum_name: class_name, + case_name: constant_name.to_string(), + }); + } + Err(CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata for {}::{}", + current_class, constant_name + ))) +} + +/// Returns an interface constant expression, including inherited parent interfaces. +fn reflection_interface_constant_expr( + ctx: &FunctionContext<'_>, + interface_name: &str, + constant_name: &str, +) -> Option { + let mut visited = std::collections::HashSet::new(); + let mut queue = vec![interface_name.to_string()]; + while let Some(name) = queue.pop() { + if !visited.insert(name.clone()) { + continue; + } + if let Some(info) = ctx.module.interface_infos.get(&name) { + if let Some(value) = info.constants.get(constant_name) { + return Some(value.clone()); + } + queue.extend(info.parents.iter().cloned()); + } + } + None +} + +/// Resolves a static receiver against the current reflected declaration. +fn reflection_static_receiver_name( + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + receiver: &StaticReceiver, +) -> Result { + match receiver { + StaticReceiver::Named(name) => Ok(name.as_str().trim_start_matches('\\').to_string()), + StaticReceiver::Self_ | StaticReceiver::Static => Ok(current_class.to_string()), + StaticReceiver::Parent => current_info + .and_then(|info| info.parent.clone()) + .ok_or_else(|| { + CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata parent receiver in {}", + current_class + )) + }), + } +} + +/// Returns a small label for unsupported constant-value diagnostics. +fn reflection_constant_value_kind(value: &ReflectionConstantValue) -> &'static str { + match value { + ReflectionConstantValue::Int(_) => "int", + ReflectionConstantValue::Bool(_) => "bool", + ReflectionConstantValue::Float(_) => "float", + ReflectionConstantValue::Str(_) => "string", + ReflectionConstantValue::Null => "null", + ReflectionConstantValue::EnumCase { .. } => "enum-case", + } +} + +/// Reports an unsupported unary constant value while avoiding large debug output. +fn unsupported_reflection_constant_value(value: ReflectionConstantValue) -> CodegenIrError { + CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata unary value {}", + reflection_constant_value_kind(&value) + )) +} + /// Looks up class-constant metadata by PHP-style class name and case-sensitive constant name. fn resolve_reflection_class_constant<'a>( ctx: &'a FunctionContext<'_>, @@ -1826,6 +2203,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), + constant_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -2070,6 +2448,39 @@ fn emit_reflection_string_array_property_by_name( Ok(()) } +/// Replaces a ReflectionClass private slot with an associative constant-value array. +fn emit_reflection_constant_array_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + members: &[ReflectionConstantMember], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_constant_array(ctx, members)?; + let assoc_type = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }; + emit_box_current_value_as_mixed(ctx.emitter, &assoc_type); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + /// Replaces a ReflectionClass private array slot with ReflectionMethod/Property objects. fn emit_reflection_member_array_property_by_name( ctx: &mut FunctionContext<'_>, @@ -2249,6 +2660,82 @@ fn emit_reflection_parameter_array( Ok(()) } +/// Allocates and populates the associative ReflectionClass constant map. +fn emit_reflection_constant_array( + ctx: &mut FunctionContext<'_>, + members: &[ReflectionConstantMember], +) -> Result<()> { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Mixed); + for member in members { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, &member.value); + emit_reflection_constant_hash_insert(ctx, &member.name); + } + Ok(()) +} + +/// Materializes one Reflection constant value as a boxed Mixed cell. +fn emit_reflection_constant_value_as_mixed( + ctx: &mut FunctionContext<'_>, + value: &ReflectionConstantValue, +) { + match value { + ReflectionConstantValue::Int(value) => emit_boxed_int_literal_to_result(ctx, *value), + ReflectionConstantValue::Bool(value) => emit_boxed_bool_literal_to_result(ctx, *value), + ReflectionConstantValue::Float(value) => emit_boxed_float_literal_to_result(ctx, *value), + ReflectionConstantValue::Str(value) => { + emit_boxed_string_literal_default_to_result(ctx, value) + } + ReflectionConstantValue::Null => emit_boxed_null_literal_to_result(ctx), + ReflectionConstantValue::EnumCase { + enum_name, + case_name, + } => { + let case_label = enum_case_symbol(enum_name, case_name); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &case_label, + 0, + ); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(enum_name.clone())); + } + } +} + +/// Inserts the current boxed Mixed constant value into the stacked associative array. +fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + /// Allocates and populates one ReflectionMethod/ReflectionProperty object. fn emit_reflection_member_object( ctx: &mut FunctionContext<'_>, @@ -2432,32 +2919,32 @@ fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) /// Appends ReflectionClass metadata names to the current ARM64 result array. fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result } /// Appends ReflectionClass metadata names to the current x86_64 result array. fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings - ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result } /// Stores ReflectionMethod/ReflectionProperty boolean predicate slots when supported. diff --git a/src/ir/module.rs b/src/ir/module.rs index de59704e26..c663fbfa01 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -73,6 +73,7 @@ pub struct Module { pub declared_trait_methods: HashMap>, pub declared_trait_property_names: HashMap>, pub declared_trait_constant_names: HashMap>, + pub declared_trait_constants: HashMap>, pub class_infos: HashMap, pub interface_infos: HashMap, pub enum_infos: HashMap, @@ -111,6 +112,7 @@ impl Module { declared_trait_methods: HashMap::new(), declared_trait_property_names: HashMap::new(), declared_trait_constant_names: HashMap::new(), + declared_trait_constants: HashMap::new(), class_infos: HashMap::new(), interface_infos: HashMap::new(), enum_infos: HashMap::new(), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 528ca0572b..4f96fc2d34 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -21,7 +21,7 @@ use crate::ir::{ }; use crate::ir_lower::{builtin_datetime, function, LoweringError}; use crate::names::php_symbol_key; -use crate::parser::ast::{ClassMethod, ExprKind, Program, Stmt, StmtKind}; +use crate::parser::ast::{ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind}; use crate::types::{CheckResult, ClassInfo, InterfaceInfo, PhpType}; /// Lowers an optimized typed AST program into a validated EIR module. @@ -77,6 +77,7 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec module.declared_trait_methods = collect_declared_trait_methods(program); module.declared_trait_property_names = collect_declared_trait_property_names(program); module.declared_trait_constant_names = collect_declared_trait_constant_names(program); + module.declared_trait_constants = collect_declared_trait_constants(program); module.class_infos = check_result.classes.clone(); module.interface_infos = check_result.interfaces.clone(); module.enum_infos = check_result.enums.clone(); @@ -529,6 +530,33 @@ fn collect_declared_trait_constant_names(program: &Program) -> HashMap HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .map(|constant| (constant.name.clone(), constant.value.clone())) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_constants(body)); + } + _ => {} + } + } + constants +} + /// Recursively collects source-declared names that are present in checked metadata. fn collect_program_declared_names( program: &Program, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 7ae3b89180..c7b435c362 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -745,6 +745,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(string_array_type()), empty_array(), ), + builtin_property( + "__constants", + Visibility::Private, + Some(mixed_type()), + empty_array(), + ), builtin_property( "__methods", Visibility::Private, @@ -802,6 +808,8 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), builtin_reflection_class_has_name_method("hasConstant", "__constant_names", false), + builtin_reflection_class_get_constant_method(), + builtin_reflection_class_mixed_method("getConstants", "__constants"), builtin_reflection_class_implements_interface_method(), builtin_reflection_class_array_method( "getMethods", @@ -959,6 +967,64 @@ fn builtin_reflection_class_has_name_method( } } +/// Returns `ReflectionClass::getConstant()` backed by the private constant-value map. +fn builtin_reflection_class_get_constant_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name_arg = Expr::new(ExprKind::Variable("name".to_string()), dummy_span); + let value = Expr::new(ExprKind::Variable("value".to_string()), dummy_span); + let value_read = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(reflection_this_property("__constants", dummy_span)), + index: Box::new(name_arg), + }, + dummy_span, + ); + let value_is_present = Expr::new( + ExprKind::BinaryOp { + left: Box::new(value.clone()), + op: BinOp::StrictNotEq, + right: Box::new(Expr::new(ExprKind::Null, dummy_span)), + }, + dummy_span, + ); + ClassMethod { + name: "getConstant".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![ + Stmt::new( + StmtKind::Assign { + name: "value".to_string(), + value: value_read, + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: value_is_present, + then_body: vec![Stmt::new(StmtKind::Return(Some(value)), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::BoolLiteral(false), dummy_span))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns `ReflectionClass::implementsInterface()` backed by interface-name metadata. fn builtin_reflection_class_implements_interface_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 10224b1f9b..7b4fd0efd6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6144,6 +6144,59 @@ echo $backed->hasConstant("Ready") ? "Q" : "q";'); assert_eq!(out.stdout, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BYQ"); } +/// Verifies eval ReflectionClass returns constant values and enum cases through the bridge. +#[test] +fn test_eval_reflection_class_constant_values() { + let out = compile_and_run_capture( + r#"getConstants(); +echo $ref->getConstant("OWN") . ":"; +echo $ref->getConstant("BASE") . ":"; +echo $ref->getConstant("LIMIT") . ":"; +echo $ref->getConstant("SECRET") . ":"; +echo $ref->getConstant("SUM") . ":"; +echo $ref->getConstant("own") ? "bad" : "missing"; +echo ":" . count($all) . ":" . $all["OWN"] . ":" . $all["BASE"] . ":" . $all["LIMIT"]; +$trait = new ReflectionClass("EvalReflectConstTrait"); +$traitAll = $trait->getConstants(); +echo ":" . $trait->getConstant("TRAIT_VALUE") . ":" . count($traitAll) . ":" . $traitAll["TRAIT_VALUE"]; +$enum = new ReflectionClass("EvalReflectConstEnum"); +$case = $enum->getConstant("Ready"); +$enumAll = $enum->getConstants(); +echo ":" . $case->name; +echo ":" . $enum->getConstant("LEVEL") . ":" . $enumAll["LEVEL"] . ":" . count($enumAll);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "own:1:2:9:5:missing:5:own:1:2:3:1:3:Ready:40:40:2" + ); +} + /// Verifies eval ReflectionMethod and ReflectionProperty expose member predicates through the bridge. #[test] fn test_eval_reflection_member_predicates() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 4baddca69f..a3c0698550 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1322,6 +1322,57 @@ echo $backed->hasConstant("Ready") ? "Q" : "q"; assert_eq!(out, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BNYQ"); } +/// Verifies that `ReflectionClass::getConstant()` and `getConstants()` expose +/// class, parent, interface, trait, private, and enum-case constants. +#[test] +fn test_reflection_class_returns_constant_values() { + let out = compile_and_run( + r#"getConstants(); +echo $ref->getConstant("OWN") . ":"; +echo $ref->getConstant("BASE") . ":"; +echo $ref->getConstant("LIMIT") . ":"; +echo $ref->getConstant("SECRET") . ":"; +echo $ref->getConstant("SUM") . ":"; +echo $ref->getConstant("NAME") . ":"; +echo $ref->getConstant("own") ? "bad" : "missing"; +echo ":" . count($all) . ":" . $all["OWN"] . ":" . $all["BASE"] . ":" . $all["LIMIT"]; +$trait = new ReflectionClass(ReflectConstTrait::class); +$traitAll = $trait->getConstants(); +echo ":" . $trait->getConstant("TRAIT_VALUE") . ":" . count($traitAll) . ":" . $traitAll["TRAIT_VALUE"]; +$enum = new ReflectionClass(ReflectConstEnum::class); +$case = $enum->getConstant("Ready"); +$enumAll = $enum->getConstants(); +echo ":" . $case->name; +echo ":" . $enum->getConstant("LEVEL") . ":" . $enumAll["LEVEL"] . ":" . count($enumAll); +"#, + ); + assert_eq!( + out, + "own:1:2:9:5:ReflectConstChild:missing:6:own:1:2:3:1:3:Ready:40:40:2" + ); +} + /// Verifies that `ReflectionClass` reports implemented interface and used trait names. #[test] fn test_reflection_class_reports_relation_names() { From 6309534a7204c1f5cb75f63a6b478ba6df1b7bb2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 10:33:07 +0200 Subject: [PATCH 0407/1208] Add ReflectionClass member lookup methods --- .../elephc-eval/src/interpreter/reflection.rs | 97 ++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 9 ++ .../tests/builtins_class_metadata.rs | 32 ++++ .../interpreter/tests/support/object_ops.rs | 47 ++++++ src/types/checker/builtin_types/reflection.rs | 139 ++++++++++++++++++ tests/codegen/eval.rs | 50 +++++++ tests/codegen/oop/attributes.rs | 51 +++++++ 7 files changed, 425 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 637f156ef9..77ef709d00 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -231,6 +231,68 @@ pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( Ok(Some(result)) } +/// Handles eval-backed `ReflectionClass::getMethod()` and `getProperty()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_member_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let owner_kind = if method_name.eq_ignore_ascii_case("getMethod") { + EVAL_REFLECTION_OWNER_METHOD + } else if method_name.eq_ignore_ascii_case("getProperty") { + EVAL_REFLECTION_OWNER_PROPERTY + } else { + return Ok(None); + }; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let Some(member_name) = + eval_reflection_member_name(owner_kind, &reflected_name, &requested_name, context) + else { + let message_name = eval_reflection_class_like_attributes(&reflected_name, context) + .map(|metadata| metadata.resolved_name) + .unwrap_or_else(|| reflected_name.clone()); + let message = eval_reflection_missing_member_message( + owner_kind, + &message_name, + &requested_name, + ); + return eval_throw_reflection_exception(&message, context, values); + }; + let member = eval_reflection_member_metadata(owner_kind, &reflected_name, &member_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + let flags = eval_reflection_member_flags( + member.visibility, + member.is_static, + member.is_final, + member.is_abstract, + ); + eval_reflection_owner_object( + owner_kind, + &member_name, + &member.attributes, + &[], + &[], + &[], + &[], + None, + &member.parameters, + flags, + member.required_parameter_count as u64, + context, + values, + ) + .map(Some) +} + /// Returns the constant names visible through eval-backed `ReflectionClass`. fn eval_reflection_constant_names( reflected_name: &str, @@ -258,6 +320,41 @@ fn eval_reflection_constant_value( context.class_constant_cell(&declaring_class, constant.name()) } +/// Resolves the declared member spelling for eval `ReflectionClass` single-member lookups. +fn eval_reflection_member_name( + owner_kind: u64, + reflected_name: &str, + requested_name: &str, + context: &ElephcEvalContext, +) -> Option { + let metadata = eval_reflection_class_like_attributes(reflected_name, context)?; + let names = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + metadata.method_names + } else { + metadata.property_names + }; + names + .into_iter() + .find(|name| match owner_kind { + EVAL_REFLECTION_OWNER_METHOD => name.eq_ignore_ascii_case(requested_name), + EVAL_REFLECTION_OWNER_PROPERTY => name == requested_name, + _ => false, + }) +} + +/// Builds PHP-compatible missing-member messages for eval ReflectionClass lookups. +fn eval_reflection_missing_member_message( + owner_kind: u64, + reflected_name: &str, + requested_name: &str, +) -> String { + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + format!("Method {}::{}() does not exist", reflected_name, requested_name) + } else { + format!("Property {}::${} does not exist", reflected_name, requested_name) + } +} + /// Builds an eval-backed `ReflectionClass` object when the reflected class-like exists in eval. fn eval_reflection_class_new( evaluated_args: Vec, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 66527f6fd2..2e39cec39a 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2207,6 +2207,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_get_member_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(instance) = eval_reflection_class_new_instance_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 487bd17754..29a080cf5b 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -784,6 +784,38 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass getMethod/getProperty return eval member objects. +#[test] +fn execute_program_reflection_class_gets_eval_member_objects() { + let program = parse_fragment( + br#"class EvalReflectLookupTarget { + public function first() {} + private static function helper() {} + protected $visible; + private static $token; +} +$ref = new ReflectionClass("EvalReflectLookupTarget"); +$method = $ref->getMethod("FIRST"); +echo $method->getName(); echo ":"; +echo $method->isPublic() ? "U" : "u"; echo ":"; +$helper = $ref->getMethod("helper"); +echo $helper->isPrivate() ? "P" : "p"; +echo $helper->isStatic() ? "S" : "s"; echo ":"; +$property = $ref->getProperty("visible"); +echo $property->getName(); echo ":"; +echo $property->isProtected() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "first:U:PS:visible:R"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::getParentClass returns eval parent metadata or false. #[test] fn execute_program_reflection_class_get_parent_class() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 7c7b976c89..399942076c 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -236,10 +236,16 @@ impl FakeOps { Self::object_property(&properties, "__methods") .map_or_else(|| self.runtime_array_new(0), Ok) } + (FakeValue::Object(properties), "getmethod") if args.len() == 1 => { + self.object_named_member(&properties, "__methods", args[0], true) + } (FakeValue::Object(properties), "getproperties") if args.is_empty() => { Self::object_property(&properties, "__properties") .map_or_else(|| self.runtime_array_new(0), Ok) } + (FakeValue::Object(properties), "getproperty") if args.len() == 1 => { + self.object_named_member(&properties, "__properties", args[0], false) + } (FakeValue::Object(properties), "getconstants") if args.is_empty() => { Self::object_property(&properties, "__constants") .map_or_else(|| self.runtime_assoc_new(0), Ok) @@ -529,6 +535,47 @@ impl FakeOps { }; self.bool_value(contains) } + + /// Finds one fake ReflectionMethod/ReflectionProperty object by its private name slot. + fn object_named_member( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + let FakeValue::String(mut needle) = self.get(needle) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if case_insensitive { + needle = needle.to_ascii_lowercase(); + } + let Some(array) = Self::object_property(properties, property) else { + return Err(EvalStatus::RuntimeFatal); + }; + let FakeValue::Array(elements) = self.get(array) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::Object(member_properties) = self.get(element) else { + continue; + }; + let Some(name) = Self::object_property(&member_properties, "__name") else { + continue; + }; + let FakeValue::String(mut name) = self.get(name) else { + continue; + }; + if case_insensitive { + name = name.to_ascii_lowercase(); + } + if name == needle { + return Ok(element); + } + } + Err(EvalStatus::RuntimeFatal) + } + /// Creates one fake object for eval `new` unit tests. pub(super) fn runtime_new_object( &mut self, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index c7b435c362..f0b5e9bdcb 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -816,6 +816,12 @@ fn builtin_reflection_class() -> FlattenedClass { "__methods", object_array_type("ReflectionMethod"), ), + builtin_reflection_class_get_member_method( + "getMethod", + "__methods", + "ReflectionMethod", + true, + ), builtin_reflection_class_nullable_object_method( "getConstructor", "__constructor", @@ -827,6 +833,12 @@ fn builtin_reflection_class() -> FlattenedClass { "__properties", object_array_type("ReflectionProperty"), ), + builtin_reflection_class_get_member_method( + "getProperty", + "__properties", + "ReflectionProperty", + false, + ), builtin_reflection_class_new_instance_method(), builtin_reflection_owner_get_attributes_method(), ], @@ -1246,6 +1258,23 @@ fn strtolower_call(expr: Expr, span: crate::span::Span) -> Expr { function_call("strtolower", vec![expr], span) } +/// Builds a variable expression for synthetic Reflection method bodies. +fn variable_expr(name: &str, span: crate::span::Span) -> Expr { + Expr::new(ExprKind::Variable(name.to_string()), span) +} + +/// Builds a method call expression for synthetic Reflection method bodies. +fn method_call_expr(object: Expr, method: &str, args: Vec, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::MethodCall { + object: Box::new(object), + method: method.to_string(), + args, + }, + span, + ) +} + /// Returns a public `ReflectionClass` array method backed by one private slot. fn builtin_reflection_class_array_method( method_name: &str, @@ -1279,6 +1308,110 @@ fn builtin_reflection_class_array_method( } } +/// Returns a public `ReflectionClass::getMethod()` or `getProperty()` lookup method. +fn builtin_reflection_class_get_member_method( + method_name: &str, + property: &str, + return_class: &str, + case_insensitive: bool, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name = variable_expr("name", dummy_span); + let member = variable_expr("member", dummy_span); + let member_name = method_call_expr(member.clone(), "getName", Vec::new(), dummy_span); + let left = if case_insensitive { + strtolower_call(member_name, dummy_span) + } else { + member_name + }; + let right = if case_insensitive { + strtolower_call(name.clone(), dummy_span) + } else { + name.clone() + }; + let exists = Expr::new( + ExprKind::BinaryOp { + left: Box::new(left), + op: if case_insensitive { + BinOp::Eq + } else { + BinOp::StrictEq + }, + right: Box::new(right), + }, + dummy_span, + ); + let message = if return_class == "ReflectionMethod" { + concat_expr( + concat_expr( + concat_expr( + reflection_this_property("__name", dummy_span), + string_lit("::", dummy_span), + dummy_span, + ), + name.clone(), + dummy_span, + ), + string_lit("() does not exist", dummy_span), + dummy_span, + ) + } else { + concat_expr( + concat_expr( + concat_expr( + reflection_this_property("__name", dummy_span), + string_lit("::$", dummy_span), + dummy_span, + ), + name.clone(), + dummy_span, + ), + string_lit(" does not exist", dummy_span), + dummy_span, + ) + }; + let message = concat_expr( + string_lit(if return_class == "ReflectionMethod" { "Method " } else { "Property " }, dummy_span), + message, + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Named(Name::unqualified(return_class))), + body: vec![ + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property(property, dummy_span), + key_var: None, + value_var: "member".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: exists, + then_body: vec![Stmt::new(StmtKind::Return(Some(member)), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + throw_new_reflection_exception(message, dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public nullable object `ReflectionClass` method backed by one private slot. fn builtin_reflection_class_nullable_object_method( method_name: &str, @@ -1649,10 +1782,16 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionMethod".to_string()))); } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getMethod")) { + sig.return_type = PhpType::Object("ReflectionMethod".to_string()); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getProperties")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionProperty".to_string()))); } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getProperty")) { + sig.return_type = PhpType::Object("ReflectionProperty".to_string()); + } if let Some(sig) = class_info .methods .get_mut(&php_symbol_key("getConstructor")) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7b4fd0efd6..32b1edb07c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6298,6 +6298,56 @@ foreach ($properties as $property) { assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); } +/// Verifies eval ReflectionClass getMethod/getProperty return single member objects. +#[test] +fn test_eval_reflection_class_get_method_and_property_lookup_members() { + let out = compile_and_run_capture( + r#"getMethod("FIRST"); +echo $method->getName() . ":"; +echo $method->isPublic() ? "U" : "u"; +echo ":"; +$helper = $ref->getMethod("helper"); +echo $helper->isPrivate() ? "P" : "p"; +echo $helper->isStatic() ? "S" : "s"; +echo ":"; +$property = $ref->getProperty("visible"); +echo $property->getName() . ":"; +echo $property->isProtected() ? "R" : "r"; +echo ":"; +try { + $ref->getProperty("Visible"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo ":"; +try { + $ref->getMethod("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "first:U:PS:visible:R:Property EvalReflectLookupTarget::$Visible does not exist:Method EvalReflectLookupTarget::missing() does not exist" + ); +} + /// Verifies eval ReflectionMethod materializes ReflectionParameter objects through the bridge. #[test] fn test_eval_reflection_method_lists_parameters() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index a3c0698550..4eb2674cf2 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1781,6 +1781,57 @@ foreach ($properties as $property) { assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); } +/// Verifies that `ReflectionClass::getMethod()` and `getProperty()` return +/// single member objects and throw ReflectionException for missing members. +#[test] +fn test_reflection_class_get_method_and_property_lookup_members() { + let out = compile_and_run_capture( + r#"getMethod("FIRST"); +echo $method->getName() . ":"; +echo $method->isPublic() ? "U" : "u"; +echo ":"; +$helper = $ref->getMethod("helper"); +echo $helper->isPrivate() ? "P" : "p"; +echo $helper->isStatic() ? "S" : "s"; +echo ":"; +$property = $ref->getProperty("visible"); +echo $property->getName() . ":"; +echo $property->isProtected() ? "R" : "r"; +echo ":"; +try { + $ref->getProperty("Visible"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo ":"; +try { + $ref->getMethod("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "first:U:PS:visible:R:Property ReflectLookupTarget::$Visible does not exist:Method ReflectLookupTarget::missing() does not exist" + ); +} + /// Verifies that `ReflectionMethod::getParameters()` returns populated /// ReflectionParameter objects for typed, by-reference, defaulted, and variadic parameters. #[test] From 4e30c39fa1b515342012fcbdde501caad0d1a1ab Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 10:51:59 +0200 Subject: [PATCH 0408/1208] Add ReflectionClass constant reflector methods --- .../elephc-eval/src/interpreter/reflection.rs | 121 ++++++++++-- .../elephc-eval/src/interpreter/statements.rs | 18 ++ .../tests/builtins_class_metadata.rs | 51 ++++++ .../interpreter/tests/support/object_ops.rs | 28 +++ docs/php/classes.md | 7 +- docs/php/eval.md | 5 + src/codegen/lower_inst/objects/reflection.rs | 172 ++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 90 ++++++++- tests/codegen/eval.rs | 50 +++++ tests/codegen/oop/attributes.rs | 41 +++++ 10 files changed, 565 insertions(+), 18 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 77ef709d00..a464c45c38 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -231,6 +231,66 @@ pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( Ok(Some(result)) } +/// Handles eval-backed `ReflectionClass::getReflectionConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getReflectionConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_constant_names(&reflected_name, context) + .iter() + .any(|name| name == &requested_name) + { + return values.bool_value(false).map(Some); + } + eval_reflection_class_constant_object_result(&reflected_name, &requested_name, context, values) + .map(Some) +} + +/// Handles eval-backed `ReflectionClass::getReflectionConstants()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getReflectionConstants") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = eval_reflection_constant_names(&reflected_name, context); + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + let object = + eval_reflection_class_constant_object_result(&reflected_name, name, context, values)?; + let key = values.int(index as i64)?; + result = values.array_set(result, key, object)?; + } + Ok(Some(result)) +} + /// Handles eval-backed `ReflectionClass::getMethod()` and `getProperty()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_member_result( identity: u64, @@ -260,15 +320,13 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( let message_name = eval_reflection_class_like_attributes(&reflected_name, context) .map(|metadata| metadata.resolved_name) .unwrap_or_else(|| reflected_name.clone()); - let message = eval_reflection_missing_member_message( - owner_kind, - &message_name, - &requested_name, - ); + let message = + eval_reflection_missing_member_message(owner_kind, &message_name, &requested_name); return eval_throw_reflection_exception(&message, context, values); }; - let member = eval_reflection_member_metadata(owner_kind, &reflected_name, &member_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; + let member = + eval_reflection_member_metadata(owner_kind, &reflected_name, &member_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; let flags = eval_reflection_member_flags( member.visibility, member.is_static, @@ -320,6 +378,33 @@ fn eval_reflection_constant_value( context.class_constant_cell(&declaring_class, constant.name()) } +/// Builds one eval-backed `ReflectionClassConstant` object for a visible constant name. +fn eval_reflection_class_constant_object_result( + reflected_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let attributes = + eval_reflection_class_constant_attributes(reflected_name, constant_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + constant_name, + &attributes, + &[], + &[], + &[], + &[], + None, + &[], + 0, + 0, + context, + values, + ) +} + /// Resolves the declared member spelling for eval `ReflectionClass` single-member lookups. fn eval_reflection_member_name( owner_kind: u64, @@ -333,13 +418,11 @@ fn eval_reflection_member_name( } else { metadata.property_names }; - names - .into_iter() - .find(|name| match owner_kind { - EVAL_REFLECTION_OWNER_METHOD => name.eq_ignore_ascii_case(requested_name), - EVAL_REFLECTION_OWNER_PROPERTY => name == requested_name, - _ => false, - }) + names.into_iter().find(|name| match owner_kind { + EVAL_REFLECTION_OWNER_METHOD => name.eq_ignore_ascii_case(requested_name), + EVAL_REFLECTION_OWNER_PROPERTY => name == requested_name, + _ => false, + }) } /// Builds PHP-compatible missing-member messages for eval ReflectionClass lookups. @@ -349,9 +432,15 @@ fn eval_reflection_missing_member_message( requested_name: &str, ) -> String { if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - format!("Method {}::{}() does not exist", reflected_name, requested_name) + format!( + "Method {}::{}() does not exist", + reflected_name, requested_name + ) } else { - format!("Property {}::${} does not exist", reflected_name, requested_name) + format!( + "Property {}::${} does not exist", + reflected_name, requested_name + ) } } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 2e39cec39a..dfd97f837f 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2207,6 +2207,24 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_get_reflection_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_reflection_constants_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_get_member_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 29a080cf5b..ac72031217 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -619,6 +619,57 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass returns eval class-constant reflector objects. +#[test] +fn execute_program_reflects_eval_class_constant_reflector_objects() { + let program = parse_fragment( + br#"class EvalReflectConstMarker { + public $label; + public function __construct($label) { + $this->label = $label; + } + public function label() { + return $this->label; + } +} +class EvalReflectConstObjectTarget { + #[EvalReflectConstMarker("const")] + public const ANSWER = 42; +} +enum EvalReflectConstObjectEnum { + #[EvalReflectConstMarker("case")] + case Ready; + public const LEVEL = 7; +} +$ref = new ReflectionClass("EvalReflectConstObjectTarget"); +$single = $ref->getReflectionConstant("ANSWER"); +$all = $ref->getReflectionConstants(); +echo $single->getName(); echo ":"; +echo count($all); echo ":"; echo $all[0]->getName(); echo ":"; +echo $single->getAttributes()[0]->newInstance()->label(); echo ":"; +echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +$enum = new ReflectionClass("EvalReflectConstObjectEnum"); +$enumAll = $enum->getReflectionConstants(); +$case = $enum->getReflectionConstant("Ready"); +$level = $enum->getReflectionConstant("LEVEL"); +echo ":"; echo count($enumAll); echo ":"; echo $enumAll[0]->getName(); echo ":"; echo $enumAll[1]->getName(); +echo ":"; echo $case->getAttributes()[0]->newInstance()->label(); echo ":"; +echo count($level->getAttributes()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ANSWER:1:ANSWER:const:missing:2:Ready:LEVEL:case:0" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod and ReflectionProperty expose eval member predicate metadata. #[test] fn execute_program_reflects_eval_member_predicates() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 399942076c..a5a5189060 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -250,6 +250,17 @@ impl FakeOps { Self::object_property(&properties, "__constants") .map_or_else(|| self.runtime_assoc_new(0), Ok) } + (FakeValue::Object(properties), "getreflectionconstants") if args.is_empty() => { + Self::object_property(&properties, "__reflection_constants") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getreflectionconstant") if args.len() == 1 => self + .object_named_member_or_false( + &properties, + "__reflection_constants", + args[0], + false, + ), (FakeValue::Object(properties), "getarguments") if args.is_empty() => { Self::object_property(&properties, "__args") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -439,6 +450,7 @@ impl FakeOps { let in_namespace = self.bool_value(has_namespace)?; let constant_names = self.runtime_array_new(0)?; let constants = self.runtime_assoc_new(0)?; + let reflection_constants = self.runtime_array_new(0)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_interface".to_string(), is_interface)); @@ -456,6 +468,7 @@ impl FakeOps { properties.push(("__property_names".to_string(), property_names)); properties.push(("__constant_names".to_string(), constant_names)); properties.push(("__constants".to_string(), constants)); + properties.push(("__reflection_constants".to_string(), reflection_constants)); properties.push(("__methods".to_string(), method_objects)); properties.push(("__parent_class".to_string(), parent_class)); properties.push(("__properties".to_string(), property_objects)); @@ -576,6 +589,21 @@ impl FakeOps { Err(EvalStatus::RuntimeFatal) } + /// Finds one fake reflection member by name, returning PHP `false` when absent. + fn object_named_member_or_false( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + match self.object_named_member(properties, property, needle, case_insensitive) { + Ok(member) => Ok(member), + Err(EvalStatus::RuntimeFatal) => self.bool_value(false), + Err(status) => Err(status), + } + } + /// Creates one fake object for eval `new` unit tests. pub(super) fn runtime_new_object( &mut self, diff --git a/docs/php/classes.md b/docs/php/classes.md index ade7a1b87f..6b3f3503b9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1159,6 +1159,11 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | | `ReflectionClass::hasMethod()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named method; method lookup is case-insensitive | | `ReflectionClass::hasProperty()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named property; property lookup is case-sensitive | +| `ReflectionClass::hasConstant()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named class constant or enum case; lookup is case-sensitive | +| `ReflectionClass::getConstant()` | `new ReflectionClass($class_name)` | Return the reflected constant value, enum case object, or `false` when no such constant is visible | +| `ReflectionClass::getConstants()` | `new ReflectionClass($class_name)` | Return an associative array of visible constant values keyed by constant or enum-case name | +| `ReflectionClass::getReflectionConstant()` | `new ReflectionClass($class_name)` | Return a `ReflectionClassConstant` object for the named visible constant or enum case, or `false` when no such constant is visible | +| `ReflectionClass::getReflectionConstants()` | `new ReflectionClass($class_name)` | Return indexed `ReflectionClassConstant` objects for visible class constants and enum cases | | `ReflectionClass::implementsInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol implements, extends, or is the requested interface; interface lookup is case-insensitive and invalid names throw `ReflectionException` | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | @@ -1254,7 +1259,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. - `ReflectionFunction` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. - `ReflectionFunction` reflects named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. diff --git a/docs/php/eval.md b/docs/php/eval.md index d38e4c1412..574f563348 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -177,6 +177,11 @@ returns traits used directly by eval classes. `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. +`ReflectionClass::hasConstant()`, `getConstant()`, `getConstants()`, +`getReflectionConstant()`, and `getReflectionConstants()` expose eval-visible +class constants, interface constants, trait constants, and enum cases. +Constant lookup is case-sensitive; single-value lookups return `false` when no +constant or case is visible. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index abbfa2419c..4e91d4f429 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -38,6 +38,7 @@ struct ReflectionOwnerMetadata { property_names: Vec, constant_names: Vec, constant_members: Vec, + constant_reflection_members: Vec, method_members: Vec, property_members: Vec, constructor_member: Option, @@ -204,6 +205,12 @@ fn emit_reflection_owner_object( "__constants", &metadata.constant_members, )?; + emit_reflection_member_array_property_by_name( + ctx, + "__reflection_constants", + "ReflectionClassConstant", + &metadata.constant_reflection_members, + )?; emit_reflection_member_array_property_by_name( ctx, "__methods", @@ -684,6 +691,8 @@ fn reflection_class_metadata_for_name( let property_names = reflection_class_property_names(ctx, class_name, info); let constant_names = reflection_class_constant_names(ctx, class_name, info); let constant_members = reflection_class_constant_members(ctx, class_name, info)?; + let constant_reflection_members = + reflection_class_constant_reflection_members(ctx, class_name, info)?; let method_members = reflection_class_method_members(info, &method_names); let property_members = reflection_class_property_members(ctx, class_name, info, &property_names); @@ -700,6 +709,7 @@ fn reflection_class_metadata_for_name( property_names, constant_names, constant_members, + constant_reflection_members, method_members, property_members, constructor_member, @@ -727,6 +737,8 @@ fn reflection_class_metadata_for_name( let property_names = reflection_interface_property_names(ctx, interface_name); let constant_names = reflection_interface_constant_names(ctx, interface_name); let constant_members = reflection_interface_constant_members(ctx, interface_name)?; + let constant_reflection_members = + reflection_interface_constant_reflection_members(ctx, interface_name); let method_members = ctx .module .interface_infos @@ -745,6 +757,7 @@ fn reflection_class_metadata_for_name( property_names, constant_names, constant_members, + constant_reflection_members, method_members, property_members, constructor_member, @@ -773,6 +786,8 @@ fn reflection_class_metadata_for_name( let property_names = reflection_trait_property_names(ctx, trait_name); let constant_names = reflection_trait_constant_names(ctx, trait_name); let constant_members = reflection_trait_constant_members(ctx, trait_name)?; + let constant_reflection_members = + reflection_trait_constant_reflection_members(ctx, trait_name); let method_members = ctx .module .declared_trait_methods @@ -791,6 +806,7 @@ fn reflection_class_metadata_for_name( property_names, constant_names, constant_members, + constant_reflection_members, method_members, property_members, constructor_member, @@ -886,6 +902,7 @@ fn reflection_method_owner_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -929,6 +946,7 @@ fn reflection_property_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1084,6 +1102,7 @@ fn reflection_class_constant_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1124,6 +1143,7 @@ fn reflection_class_constant_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1171,6 +1191,7 @@ fn reflection_enum_case_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, @@ -1474,6 +1495,156 @@ fn reflection_trait_constant_members( Ok(members) } +/// Returns materializable constant-reflector objects for `ReflectionClass::getReflectionConstants()`. +fn reflection_class_constant_reflection_members( + ctx: &FunctionContext<'_>, + class_name: &str, + _info: &crate::types::ClassInfo, +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(enum_info) = ctx.module.enum_infos.get(class_name) { + for case in &enum_info.cases { + push_unique_constant_reflection_member( + &case.name, + case.attribute_names.clone(), + case.attribute_args.clone(), + &mut members, + &mut seen, + ); + } + } + let mut current = Some(class_name.to_string()); + while let Some(current_name) = current { + let Some((resolved_name, current_info)) = resolve_reflection_class(ctx, ¤t_name) + else { + break; + }; + for constant_name in current_info.constants.keys() { + if seen.contains(constant_name) { + continue; + } + push_unique_constant_reflection_member( + constant_name, + current_info + .constant_attribute_names + .get(constant_name) + .cloned() + .unwrap_or_default(), + current_info + .constant_attribute_args + .get(constant_name) + .cloned() + .unwrap_or_default(), + &mut members, + &mut seen, + ); + } + for interface_name in ¤t_info.interfaces { + for member in reflection_interface_constant_reflection_members(ctx, interface_name) { + push_unique_listed_constant_member(member, &mut members, &mut seen); + } + } + current = current_info.parent.clone(); + if current.as_deref() == Some(resolved_name) { + break; + } + } + Ok(members) +} + +/// Returns constant-reflector objects for interface constants. +fn reflection_interface_constant_reflection_members( + ctx: &FunctionContext<'_>, + interface_name: &str, +) -> Vec { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + collect_interface_constant_reflection_members(ctx, interface_name, &mut members, &mut seen); + members +} + +/// Recursively appends interface constant-reflector objects. +fn collect_interface_constant_reflection_members( + ctx: &FunctionContext<'_>, + interface_name: &str, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) { + let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { + return; + }; + for parent in &interface_info.parents { + collect_interface_constant_reflection_members(ctx, parent, members, seen); + } + for constant_name in interface_info.constants.keys() { + push_unique_constant_reflection_member( + constant_name, + Vec::new(), + Vec::new(), + members, + seen, + ); + } +} + +/// Returns constant-reflector objects for direct trait constants. +fn reflection_trait_constant_reflection_members( + ctx: &FunctionContext<'_>, + trait_name: &str, +) -> Vec { + ctx.module + .declared_trait_constants + .get(trait_name) + .map(|constants| { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for constant_name in constants.keys() { + push_unique_constant_reflection_member( + constant_name, + Vec::new(), + Vec::new(), + &mut members, + &mut seen, + ); + } + members + }) + .unwrap_or_default() +} + +/// Appends one constant-reflector member if a constant with this name was not already visible. +fn push_unique_constant_reflection_member( + name: &str, + attr_names: Vec, + attr_args: Vec>>, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if !seen.insert(name.to_string()) { + return; + } + members.push(ReflectionListedMember { + name: name.to_string(), + attr_names, + attr_args, + flags: ReflectionMemberFlags::default(), + required_parameter_count: 0, + parameters: Vec::new(), + }); +} + +/// Appends a prebuilt constant-reflector member if its name was not already visible. +fn push_unique_listed_constant_member( + member: ReflectionListedMember, + members: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(member.name.clone()) { + members.push(member); + } +} + /// Returns true when a property should be visible for `ReflectionClass::hasProperty()`. fn reflection_property_visible_from_class( info: &crate::types::ClassInfo, @@ -2204,6 +2375,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index f0b5e9bdcb..a43fc135d8 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -751,6 +751,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(mixed_type()), empty_array(), ), + builtin_property( + "__reflection_constants", + Visibility::Private, + Some(object_array_type("ReflectionClassConstant")), + empty_array(), + ), builtin_property( "__methods", Visibility::Private, @@ -810,6 +816,12 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_has_name_method("hasConstant", "__constant_names", false), builtin_reflection_class_get_constant_method(), builtin_reflection_class_mixed_method("getConstants", "__constants"), + builtin_reflection_class_array_method( + "getReflectionConstants", + "__reflection_constants", + object_array_type("ReflectionClassConstant"), + ), + builtin_reflection_class_get_reflection_constant_method(), builtin_reflection_class_implements_interface_method(), builtin_reflection_class_array_method( "getMethods", @@ -1037,6 +1049,61 @@ fn builtin_reflection_class_get_constant_method() -> ClassMethod { } } +/// Returns `ReflectionClass::getReflectionConstant()` backed by reflected constant objects. +fn builtin_reflection_class_get_reflection_constant_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name = variable_expr("name", dummy_span); + let member = variable_expr("member", dummy_span); + let exists = Expr::new( + ExprKind::BinaryOp { + left: Box::new(method_call_expr( + member.clone(), + "getName", + Vec::new(), + dummy_span, + )), + op: BinOp::StrictEq, + right: Box::new(name.clone()), + }, + dummy_span, + ); + ClassMethod { + name: "getReflectionConstant".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![ + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__reflection_constants", dummy_span), + key_var: None, + value_var: "member".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: exists, + then_body: vec![Stmt::new(StmtKind::Return(Some(member)), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new(StmtKind::Return(false_bool()), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns `ReflectionClass::implementsInterface()` backed by interface-name metadata. fn builtin_reflection_class_implements_interface_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -1371,7 +1438,14 @@ fn builtin_reflection_class_get_member_method( ) }; let message = concat_expr( - string_lit(if return_class == "ReflectionMethod" { "Method " } else { "Property " }, dummy_span), + string_lit( + if return_class == "ReflectionMethod" { + "Method " + } else { + "Property " + }, + dummy_span, + ), message, dummy_span, ); @@ -1785,6 +1859,20 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getMethod")) { sig.return_type = PhpType::Object("ReflectionMethod".to_string()); } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getReflectionConstants")) + { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionClassConstant".to_string(), + ))); + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getReflectionConstant")) + { + sig.return_type = PhpType::Mixed; + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getProperties")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionProperty".to_string()))); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 32b1edb07c..af1585a437 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6197,6 +6197,56 @@ echo ":" . $enum->getConstant("LEVEL") . ":" . $enumAll["LEVEL"] . ":" . count($ ); } +/// Verifies eval ReflectionClass returns class-constant reflector objects through the bridge. +#[test] +fn test_eval_reflection_class_constant_reflector_objects() { + let out = compile_and_run_capture( + r#"label = $label; + } + public function label() { + return $this->label; + } +} +class EvalReflectConstObjectTarget { + #[EvalReflectConstMarker("const")] + public const ANSWER = 42; +} +enum EvalReflectConstObjectEnum { + #[EvalReflectConstMarker("case")] + case Ready; + public const LEVEL = 7; +} +$ref = new ReflectionClass("EvalReflectConstObjectTarget"); +$single = $ref->getReflectionConstant("ANSWER"); +$all = $ref->getReflectionConstants(); +echo $single->getName() . ":"; +echo count($all) . ":" . $all[0]->getName() . ":"; +echo $single->getAttributes()[0]->newInstance()->label() . ":"; +echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +$enum = new ReflectionClass("EvalReflectConstObjectEnum"); +$enumAll = $enum->getReflectionConstants(); +$case = $enum->getReflectionConstant("Ready"); +$level = $enum->getReflectionConstant("LEVEL"); +echo ":" . count($enumAll) . ":" . $enumAll[0]->getName() . ":" . $enumAll[1]->getName(); +echo ":" . $case->getAttributes()[0]->newInstance()->label() . ":"; +echo count($level->getAttributes());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ANSWER:1:ANSWER:const:missing:2:Ready:LEVEL:case:0" + ); +} + /// Verifies eval ReflectionMethod and ReflectionProperty expose member predicates through the bridge. #[test] fn test_eval_reflection_member_predicates() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 4eb2674cf2..f482048970 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1373,6 +1373,47 @@ echo ":" . $enum->getConstant("LEVEL") . ":" . $enumAll["LEVEL"] . ":" . count($ ); } +/// Verifies that `ReflectionClass::getReflectionConstant()` and +/// `getReflectionConstants()` expose constant and enum-case reflector objects. +#[test] +fn test_reflection_class_returns_constant_reflector_objects() { + let out = compile_and_run( + r#"label; } +} +class ReflectConstObjectTarget { + #[ReflectConstMarker("const")] + public const ANSWER = 42; +} +enum ReflectConstObjectEnum { + #[ReflectConstMarker("case")] + case Ready; + public const LEVEL = 7; +} +$ref = new ReflectionClass(ReflectConstObjectTarget::class); +$single = $ref->getReflectionConstant("ANSWER"); +$all = $ref->getReflectionConstants(); +echo $single->getName() . ":"; +echo count($all) . ":" . $all[0]->getName() . ":"; +$singleAttrs = $all[0]->getAttributes(); +echo $singleAttrs[0]->newInstance()->label() . ":"; +echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +$enum = new ReflectionClass(ReflectConstObjectEnum::class); +$enumAll = $enum->getReflectionConstants(); +$case = $enum->getReflectionConstant("Ready"); +$level = $enum->getReflectionConstant("LEVEL"); +echo ":" . count($enumAll) . ":" . $enumAll[0]->getName() . ":" . $enumAll[1]->getName(); +$caseAttrs = $enumAll[0]->getAttributes(); +$levelAttrs = $enumAll[1]->getAttributes(); +echo ":" . $caseAttrs[0]->newInstance()->label() . ":"; +echo count($levelAttrs); +"#, + ); + assert_eq!(out, "ANSWER:1:ANSWER:const:missing:2:Ready:LEVEL:case:0"); +} + /// Verifies that `ReflectionClass` reports implemented interface and used trait names. #[test] fn test_reflection_class_reports_relation_names() { From 802ccfb533901bd7ae1e238f4d1fe60138152fca Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 11:15:40 +0200 Subject: [PATCH 0409/1208] Add ReflectionParameter named type metadata --- .../elephc-eval/src/interpreter/reflection.rs | 131 ++++- .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 13 +- .../interpreter/tests/support/object_ops.rs | 19 + docs/php/classes.md | 8 +- docs/php/eval.md | 14 +- src/autoload/mod.rs | 1 + src/codegen/eval_reflection_owner_helpers.rs | 157 +++++- src/codegen/lower_inst/objects.rs | 5 +- src/codegen/lower_inst/objects/reflection.rs | 487 +++++------------- src/ir_lower/program.rs | 1 + src/types/checker/builtin_types/reflection.rs | 77 +-- tests/codegen/eval.rs | 13 +- tests/codegen/oop/attributes.rs | 41 ++ 14 files changed, 567 insertions(+), 401 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index a464c45c38..6fff194ed0 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -29,6 +29,8 @@ const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; +const EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL: u64 = 1; +const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; /// Eval metadata needed to materialize one `ReflectionClass` owner object. struct EvalReflectionClassMetadata { @@ -62,6 +64,14 @@ struct EvalReflectionParameterMetadata { is_variadic: bool, is_passed_by_reference: bool, has_type: bool, + type_metadata: Option, +} + +/// Eval metadata needed to materialize one `ReflectionNamedType` object. +struct EvalReflectionNamedTypeMetadata { + name: String, + allows_null: bool, + is_builtin: bool, } /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. @@ -779,6 +789,10 @@ fn eval_reflection_parameter_object_result( let method_objects = values.array_new(0)?; let property_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; + let type_value = match parameter.type_metadata.as_ref() { + Some(type_metadata) => eval_reflection_named_type_object_result(type_metadata, values)?, + None => values.null()?, + }; let flags = eval_reflection_parameter_flags(parameter); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_PARAMETER, @@ -788,7 +802,7 @@ fn eval_reflection_parameter_object_result( trait_names, method_names, property_names, - method_objects, + type_value, property_objects, parent_class, flags, @@ -800,6 +814,46 @@ fn eval_reflection_parameter_object_result( values.release(method_names)?; values.release(property_names)?; values.release(method_objects)?; + values.release(type_value)?; + values.release(property_objects)?; + values.release(parent_class)?; + Ok(object) +} + +/// Materializes one ReflectionNamedType object through the shared reflection helper. +fn eval_reflection_named_type_object_result( + type_metadata: &EvalReflectionNamedTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let method_objects = values.array_new(0)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let flags = eval_reflection_named_type_flags(type_metadata); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_NAMED_TYPE, + &type_metadata.name, + attrs, + interface_names, + trait_names, + method_names, + property_names, + method_objects, + property_objects, + parent_class, + flags, + 0, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(method_objects)?; values.release(property_objects)?; values.release(parent_class)?; Ok(object) @@ -1107,6 +1161,7 @@ fn eval_reflection_method_metadata( parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), + method.parameter_types(), method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), @@ -1131,6 +1186,7 @@ fn eval_reflection_method_metadata( parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), + method.parameter_types(), method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), @@ -1155,6 +1211,7 @@ fn eval_reflection_method_metadata( parameters: eval_reflection_parameters_from_names_and_type_flags( method.params(), method.parameter_has_types(), + method.parameter_types(), method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), @@ -1232,6 +1289,7 @@ fn eval_reflection_required_parameter_count( fn eval_reflection_parameters_from_names_and_type_flags( names: &[String], has_type_flags: &[bool], + parameter_types: &[Option], defaults: &[Option], by_ref_flags: &[bool], variadic_flags: &[bool], @@ -1247,10 +1305,69 @@ fn eval_reflection_parameters_from_names_and_type_flags( is_variadic: variadic_flags.get(position).copied().unwrap_or(false), is_passed_by_reference: by_ref_flags.get(position).copied().unwrap_or(false), has_type: has_type_flags.get(position).copied().unwrap_or(false), + type_metadata: parameter_types + .get(position) + .and_then(Option::as_ref) + .and_then(eval_reflection_named_type_metadata) + .filter(|_| has_type_flags.get(position).copied().unwrap_or(false)), }) .collect() } +/// Converts eval parameter type metadata into the simple ReflectionNamedType subset. +fn eval_reflection_named_type_metadata( + parameter_type: &EvalParameterType, +) -> Option { + let [variant] = parameter_type.variants() else { + return None; + }; + let allows_null = parameter_type.allows_null(); + match variant { + EvalParameterTypeVariant::Array => { + Some(eval_reflection_builtin_named_type("array", allows_null)) + } + EvalParameterTypeVariant::Bool => { + Some(eval_reflection_builtin_named_type("bool", allows_null)) + } + EvalParameterTypeVariant::Callable => { + Some(eval_reflection_builtin_named_type("callable", allows_null)) + } + EvalParameterTypeVariant::Class(name) => Some(EvalReflectionNamedTypeMetadata { + name: name.clone(), + allows_null, + is_builtin: false, + }), + EvalParameterTypeVariant::Float => { + Some(eval_reflection_builtin_named_type("float", allows_null)) + } + EvalParameterTypeVariant::Int => { + Some(eval_reflection_builtin_named_type("int", allows_null)) + } + EvalParameterTypeVariant::Iterable => { + Some(eval_reflection_builtin_named_type("iterable", allows_null)) + } + EvalParameterTypeVariant::Mixed => Some(eval_reflection_builtin_named_type("mixed", true)), + EvalParameterTypeVariant::Object => { + Some(eval_reflection_builtin_named_type("object", allows_null)) + } + EvalParameterTypeVariant::String => { + Some(eval_reflection_builtin_named_type("string", allows_null)) + } + } +} + +/// Builds metadata for one builtin eval `ReflectionNamedType`. +fn eval_reflection_builtin_named_type( + name: &str, + allows_null: bool, +) -> EvalReflectionNamedTypeMetadata { + EvalReflectionNamedTypeMetadata { + name: name.to_string(), + allows_null, + is_builtin: true, + } +} + /// Packs ReflectionMethod/ReflectionProperty predicate flags for the runtime owner factory. fn eval_reflection_member_flags( visibility: EvalVisibility, @@ -1294,6 +1411,18 @@ fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) flags } +/// Packs ReflectionNamedType predicate flags for the runtime type factory. +fn eval_reflection_named_type_flags(type_metadata: &EvalReflectionNamedTypeMetadata) -> u64 { + let mut flags = 0; + if type_metadata.allows_null { + flags |= EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL; + } + if type_metadata.is_builtin { + flags |= EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN; + } + flags +} + /// Converts one reflection constructor argument to a Rust UTF-8 string. fn eval_reflection_string_arg( value: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index e29a185f4e..82007d1fd5 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -366,3 +366,4 @@ pub(super) const EVAL_REFLECTION_OWNER_CLASS_CONSTANT: u64 = 3; pub(super) const EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE: u64 = 4; pub(super) const EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE: u64 = 5; pub(super) const EVAL_REFLECTION_OWNER_PARAMETER: u64 = 6; +pub(super) const EVAL_REFLECTION_OWNER_NAMED_TYPE: u64 = 7; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index ac72031217..3408b633d5 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -740,6 +740,14 @@ foreach ($params as $param) { echo $param->isVariadic() ? "V" : "v"; echo $param->isPassedByReference() ? "R" : "b"; echo $param->hasType() ? "T" : "t"; + $type = $param->getType(); + if ($type) { + echo ":"; echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo ":null"; + } echo "|"; } return true;"##, @@ -750,7 +758,10 @@ return true;"##, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3/1:first#0rvRT|second#1OvbT|rest#2OVRt|"); + assert_eq!( + values.output, + "3/1:first#0rvRT:int!B|second#1OvbT:App\\Name?C|rest#2OVRt:null|" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index a5a5189060..bce1bbc363 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -285,6 +285,9 @@ impl FakeOps { (FakeValue::Object(properties), "getposition") if args.is_empty() => { Self::object_property(&properties, "__position").map_or_else(|| self.int(0), Ok) } + (FakeValue::Object(properties), "gettype") if args.is_empty() => { + Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) + } (FakeValue::Object(properties), "isoptional") if args.is_empty() => { Self::object_property(&properties, "__is_optional") .map_or_else(|| self.bool_value(false), Ok) @@ -301,6 +304,14 @@ impl FakeOps { Self::object_property(&properties, "__has_type") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "allowsnull") if args.is_empty() => { + Self::object_property(&properties, "__allows_null") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isbuiltin") if args.is_empty() => { + Self::object_property(&properties, "__is_builtin") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(_), "newinstance") if args.is_empty() => self.null(), (FakeValue::Object(properties), "getattributes") if args.is_empty() => { Self::object_property(&properties, "__attrs") @@ -430,6 +441,7 @@ impl FakeOps { EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE => "ReflectionEnumUnitCase", EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => "ReflectionEnumBackedCase", EVAL_REFLECTION_OWNER_PARAMETER => "ReflectionParameter", + EVAL_REFLECTION_OWNER_NAMED_TYPE => "ReflectionNamedType", _ => return Err(EvalStatus::RuntimeFatal), }; let name = self.string(reflected_name)?; @@ -513,6 +525,13 @@ impl FakeOps { is_passed_by_reference, )); properties.push(("__has_type".to_string(), has_type)); + properties.push(("__type".to_string(), method_objects)); + } + if owner_kind == EVAL_REFLECTION_OWNER_NAMED_TYPE { + let allows_null = self.bool_value((flags & 1) != 0)?; + let is_builtin = self.bool_value((flags & 2) != 0)?; + properties.push(("__allows_null".to_string(), allows_null)); + properties.push(("__is_builtin".to_string(), is_builtin)); } let object = self.alloc(FakeValue::Object(properties)); self.object_classes diff --git a/docs/php/classes.md b/docs/php/classes.md index 6b3f3503b9..077d7995b8 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1195,6 +1195,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | | `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | | `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | +| `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types or `null` when no type is declared or the declared type needs a broader Reflection type object | +| `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | @@ -1259,9 +1261,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` where type metadata is available. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs beyond the listed reflection surface are not yet available. -- `ReflectionFunction` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. -- `ReflectionFunction` reflects named functions only (the constructor argument must be a compile-time function-name string). `ReflectionParameter::getType()` resolves a single named type (including a nullable `?T`); union and intersection parameter types, default-value reflection (`getDefaultValue()`), per-parameter attribute reflection, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` for simple named types. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as union/intersection parameter type objects, parameter default-value objects, parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1289,4 +1289,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and parameter signatures are exposed through `ReflectionFunction` and `ReflectionParameter` (including `getType()` for supported named types); method parameter names, counts, positions, optional/variadic/by-reference flags, and declared-type presence are exposed through the supported `ReflectionMethod`/`ReflectionParameter` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, and simple named type metadata are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). diff --git a/docs/php/eval.md b/docs/php/eval.md index 574f563348..2773688adb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -199,8 +199,10 @@ eval/runtime class or interface names. `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report eval-declared method parameter metadata. Eval currently exposes parameter names and zero-based positions there, -plus declared-type presence for method parameter type hints. Defaulted eval -method parameters are bound when omitted and reported through +declared-type presence, and simple named type metadata through +`ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, +`allowsNull()`, and `isBuiltin()`. Union/intersection parameter type objects are +not yet materialized. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`. Supported default expressions include scalar literals, arrays whose keys and values are supported default expressions, magic constants, unary and binary operators supported by eval, @@ -381,10 +383,10 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported -ReflectionClass/Method/Parameter/Property/attribute slice, richer -ReflectionParameter type metadata beyond presence flags, and broader -generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed slice. +ReflectionClass/Method/Parameter/Property/NamedType/attribute slice, +union/intersection Reflection type objects, and broader generated/AOT method +bridge signatures beyond the current public non-by-reference fixed scalar/Mixed +slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index 609fd33fa9..7799470eb7 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -79,6 +79,7 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "ReflectionEnumUnitCase", "ReflectionFunction", "ReflectionMethod", + "ReflectionNamedType", "ReflectionParameter", "ReflectionProperty", "RuntimeException", diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 32d64052e9..61042b8006 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1,8 +1,8 @@ //! Purpose: //! Emits user-assembly helpers that let libelephc-eval materialize //! ReflectionClass, ReflectionMethod, ReflectionParameter, ReflectionProperty, -//! ReflectionClassConstant, and ReflectionEnum* objects with private metadata -//! slots populated from runtime eval declarations. +//! ReflectionClassConstant, ReflectionEnum*, and ReflectionNamedType objects +//! with private metadata slots populated from runtime eval declarations. //! //! Called from: //! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. @@ -84,6 +84,12 @@ struct ReflectionOwnerLayout { is_passed_by_reference_hi: Option, has_type_lo: Option, has_type_hi: Option, + parameter_type_lo: Option, + parameter_type_hi: Option, + allows_null_lo: Option, + allows_null_hi: Option, + is_builtin_lo: Option, + is_builtin_hi: Option, } /// Layouts for the Reflection owner classes eval can materialize. @@ -95,6 +101,7 @@ struct ReflectionOwnerLayouts { enum_unit_case: ReflectionOwnerLayout, enum_backed_case: ReflectionOwnerLayout, parameter: ReflectionOwnerLayout, + named_type: ReflectionOwnerLayout, } /// Emits eval Reflection owner helpers when any lowered function owns an eval context. @@ -162,6 +169,7 @@ fn reflection_owner_layouts(module: &Module) -> Option { true, )?, parameter: reflection_owner_layout(module.class_infos.get("ReflectionParameter")?, true)?, + named_type: reflection_owner_layout(module.class_infos.get("ReflectionNamedType")?, true)?, }) } @@ -196,10 +204,16 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, inst: &Instruction if is_fiber_class(&class_name) { return lower_fiber_new(ctx, inst); } - if class_name == "ReflectionFunction" { - return reflection::lower_reflection_function_new(ctx, inst); - } if reflection::is_reflection_owner_class(&class_name) { return reflection::lower_reflection_owner_new(ctx, inst, &class_name); } @@ -1384,6 +1381,7 @@ fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionEnumUnitCase", "ReflectionFunction", "ReflectionMethod", + "ReflectionNamedType", "ReflectionParameter", "ReflectionProperty", "RuntimeException", @@ -1453,6 +1451,7 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionException", "ReflectionFunction", "ReflectionMethod", + "ReflectionNamedType", "ReflectionParameter", "ReflectionProperty", "RegexIterator", diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 4e91d4f429..29d98ed71c 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -76,6 +76,15 @@ struct ReflectionParameterMember { is_variadic: bool, is_passed_by_reference: bool, has_type: bool, + type_metadata: Option, +} + +/// Metadata for one `ReflectionNamedType` returned by `ReflectionParameter::getType()`. +#[derive(Clone)] +struct ReflectionNamedTypeMetadata { + name: String, + allows_null: bool, + is_builtin: bool, } /// Metadata for one constant entry returned by `ReflectionClass::getConstants()`. @@ -225,6 +234,9 @@ fn emit_reflection_owner_object( "ReflectionProperty", &metadata.property_members, )?; + } else if class_name == "ReflectionFunction" { + let (_, short_name) = reflection_name_parts(reflected_name); + emit_reflection_string_property_by_name(ctx, "__short_name", short_name)?; } } emit_reflection_attrs_property(ctx, class_name, &metadata.attr_names, &metadata.attr_args)?; @@ -261,160 +273,6 @@ fn emit_reflection_owner_object( Ok(()) } -/// Lowers `new ReflectionFunction("name")` by populating its name and -/// parameter-count slots from the reflected function's signature. The slot -/// layout is `__name` (8/16), `__short` (24/32), `__num_params` (40/48), -/// `__num_required` (56/64). -pub(super) fn lower_reflection_function_new( - ctx: &mut FunctionContext<'_>, - inst: &Instruction, -) -> Result<()> { - let (full_name, short_name, num_params, num_required) = reflection_function_metadata(ctx, inst)?; - let (class_id, property_count, uninitialized_marker_offsets, name_off, short_off, np_off, nr_off) = { - let class_info = ctx - .module - .class_infos - .get("ReflectionFunction") - .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionFunction"))?; - let slot = |name: &str| -> Result { - class_info - .property_offsets - .get(name) - .copied() - .ok_or_else(|| CodegenIrError::missing_entry("property offset", 0)) - }; - ( - class_info.class_id, - class_info.properties.len(), - super::uninitialized_property_marker_offsets(class_info), - slot("__name")?, - slot("__short")?, - slot("__num_params")?, - slot("__num_required")?, - ) - }; - super::emit_object_allocation( - ctx, - class_id, - property_count, - false, - &uninitialized_marker_offsets, - &[], - )?; - emit_reflection_string_property(ctx, &full_name, name_off, name_off + 8); - emit_reflection_string_property(ctx, &short_name, short_off, short_off + 8); - emit_reflection_int_property(ctx, num_params, np_off, np_off + 8); - emit_reflection_int_property(ctx, num_required, nr_off, nr_off + 8); - - // Build the `ReflectionParameter[]` array and store it into `__params`. - let params_off = ctx - .module - .class_infos - .get("ReflectionFunction") - .and_then(|ci| ci.property_offsets.get("__params").copied()) - .ok_or_else(|| CodegenIrError::missing_entry("property offset", 0))?; - let param_infos = reflection_function_param_infos(ctx, &full_name); - let (rp_class_id, rp_prop_count, rp_markers, rp_name, rp_pos, rp_opt, rp_var, rp_type, rp_has_type) = { - let ci = ctx - .module - .class_infos - .get("ReflectionParameter") - .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionParameter"))?; - let slot = |n: &str| -> Result { - ci.property_offsets - .get(n) - .copied() - .ok_or_else(|| CodegenIrError::missing_entry("property offset", 0)) - }; - ( - ci.class_id, - ci.properties.len(), - super::uninitialized_property_marker_offsets(ci), - slot("__name")?, - slot("__position")?, - slot("__optional")?, - slot("__variadic")?, - slot("__type")?, - slot("__has_type")?, - ) - }; - let result_reg = abi::int_result_reg(ctx.emitter); - let object_reg = abi::symbol_scratch_reg(ctx.emitter); - abi::emit_push_reg(ctx.emitter, result_reg); - abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); - abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, params_off); - abi::emit_call_label(ctx.emitter, "__rt_decref_array"); - emit_reflection_parameter_array( - ctx, - ¶m_infos, - rp_class_id, - rp_prop_count, - &rp_markers, - rp_name, - rp_pos, - rp_opt, - rp_var, - rp_type, - rp_has_type, - )?; - abi::emit_pop_reg(ctx.emitter, object_reg); - abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, params_off); - abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); - abi::emit_store_to_address( - ctx.emitter, - abi::secondary_scratch_reg(ctx.emitter), - object_reg, - params_off + 8, - ); - abi::emit_push_reg(ctx.emitter, object_reg); - abi::emit_pop_reg(ctx.emitter, result_reg); - - let result = inst - .result - .ok_or_else(|| CodegenIrError::invalid_module("reflection object_new missing result"))?; - ctx.store_result_value(result) -} - -/// Resolves `ReflectionFunction(name)` to its full name, short name, and -/// parameter counts from the reflected function's lowered signature. -fn reflection_function_metadata( - ctx: &FunctionContext<'_>, - inst: &Instruction, -) -> Result<(String, String, i64, i64)> { - let Some(name_operand) = inst.operands.first().copied() else { - return Ok((String::new(), String::new(), 0, 0)); - }; - let function_name = const_required_string_operand(ctx, name_operand, "ReflectionFunction")?; - let key = php_symbol_key(function_name.trim_start_matches('\\')); - let signature = ctx - .module - .functions - .iter() - .find(|function| php_symbol_key(function.name.trim_start_matches('\\')) == key) - .and_then(|function| function.signature.as_ref()); - let (num_params, num_required) = signature - .map(|sig| { - let total = sig.params.len() as i64; - let required = sig - .params - .iter() - .zip(sig.defaults.iter().chain(std::iter::repeat(&None))) - .filter(|((name, _), default)| { - default.is_none() && sig.variadic.as_deref() != Some(name.as_str()) - }) - .count() as i64; - (total, required) - }) - .unwrap_or((0, 0)); - let short_name = function_name - .trim_start_matches('\\') - .rsplit('\\') - .next() - .unwrap_or(&function_name) - .to_string(); - Ok((function_name.clone(), short_name, num_params, num_required)) -} - /// Stores an integer immediate into a Reflection object's property slot. fn emit_reflection_int_property( ctx: &mut FunctionContext<'_>, @@ -430,201 +288,6 @@ fn emit_reflection_int_property( abi::emit_store_to_address(ctx.emitter, scratch, object_reg, high_offset); } -/// Per-parameter reflection metadata for one function parameter. -struct ReflectionParamInfo { - name: String, - optional: bool, - variadic: bool, - /// `Some((type_name, is_builtin, allows_null))` when the parameter declares a - /// single named type; `None` for an untyped parameter (`getType()` is null). - type_info: Option<(String, bool, bool)>, -} - -/// Maps a declared parameter type to `ReflectionNamedType` metadata -/// `(name, is_builtin, allows_null)`, or `None` for an unsupported/union shape. -fn reflection_named_type_info(ty: &crate::types::PhpType) -> Option<(String, bool, bool)> { - use crate::types::PhpType; - match ty { - PhpType::Int => Some(("int".to_string(), true, false)), - PhpType::Str => Some(("string".to_string(), true, false)), - PhpType::Float => Some(("float".to_string(), true, false)), - PhpType::Bool => Some(("bool".to_string(), true, false)), - PhpType::Array(_) | PhpType::AssocArray { .. } => Some(("array".to_string(), true, false)), - PhpType::Callable => Some(("callable".to_string(), true, false)), - PhpType::Iterable => Some(("iterable".to_string(), true, false)), - // Bare `Mixed` is how an *untyped* parameter is represented in the EIR - // signature (and `declared_params` is unreliable here — it is also set - // for boxed-ABI params). PHP reports untyped parameters as having no - // type, so map `Mixed` to no named type. An explicit `mixed` hint is - // the only case this under-reports, which is an accepted edge case. - PhpType::Object(class) => Some((class.trim_start_matches('\\').to_string(), false, false)), - PhpType::Union(members) => { - let has_null = members.iter().any(|m| matches!(m, PhpType::Void)); - let mut non_null = members.iter().filter(|m| !matches!(m, PhpType::Void)); - let single = non_null.next(); - // Only `T|null` (a single non-null member) maps to a named type. - match (single, non_null.next()) { - (Some(member), None) => reflection_named_type_info(member) - .map(|(name, builtin, _)| (name, builtin, has_null)), - _ => None, - } - } - _ => None, - } -} - -/// Extracts per-parameter reflection metadata from a function's lowered -/// signature. A parameter is optional once a default or the variadic is seen -/// (matching PHP's `isOptional`). -fn reflection_function_param_infos( - ctx: &FunctionContext<'_>, - function_name: &str, -) -> Vec { - let key = php_symbol_key(function_name.trim_start_matches('\\')); - let Some(signature) = ctx - .module - .functions - .iter() - .find(|function| php_symbol_key(function.name.trim_start_matches('\\')) == key) - .and_then(|function| function.signature.as_ref()) - else { - return Vec::new(); - }; - let mut seen_optional = false; - signature - .params - .iter() - .enumerate() - .map(|(idx, (name, ty))| { - let variadic = signature.variadic.as_deref() == Some(name.as_str()); - let has_default = signature.defaults.get(idx).map_or(false, Option::is_some); - if has_default || variadic { - seen_optional = true; - } - let declared = signature.declared_params.get(idx).copied().unwrap_or(false); - let type_info = if declared { - reflection_named_type_info(ty) - } else { - None - }; - ReflectionParamInfo { - name: name.clone(), - optional: seen_optional, - variadic, - type_info, - } - }) - .collect() -} - -/// Allocates a fresh indexed array sized for `count` object handles (8-byte stride). -fn emit_alloc_object_array(ctx: &mut FunctionContext<'_>, count: usize) { - match ctx.emitter.target.arch { - Arch::AArch64 => { - abi::emit_load_int_immediate(ctx.emitter, "x0", count.max(1) as i64); - abi::emit_load_int_immediate(ctx.emitter, "x1", 8); - } - Arch::X86_64 => { - abi::emit_load_int_immediate(ctx.emitter, "rdi", count.max(1) as i64); - abi::emit_load_int_immediate(ctx.emitter, "rsi", 8); - } - } - abi::emit_call_label(ctx.emitter, "__rt_array_new"); -} - -/// Pops a freshly built object and the result array off the stack and appends -/// the object handle to the array (leaving the array pointer in the result reg). -fn emit_append_object_to_array(ctx: &mut FunctionContext<'_>) { - match ctx.emitter.target.arch { - Arch::AArch64 => { - abi::emit_pop_reg(ctx.emitter, "x1"); - abi::emit_pop_reg(ctx.emitter, "x0"); - } - Arch::X86_64 => { - abi::emit_pop_reg(ctx.emitter, "rsi"); - abi::emit_pop_reg(ctx.emitter, "rdi"); - } - } - abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); -} - -/// Builds an indexed array of `ReflectionParameter` objects (one per function -/// parameter), leaving the array pointer in the result register. Stack-balanced. -#[allow(clippy::too_many_arguments)] -fn emit_reflection_parameter_array( - ctx: &mut FunctionContext<'_>, - params: &[ReflectionParamInfo], - class_id: u64, - property_count: usize, - markers: &[usize], - name_off: usize, - pos_off: usize, - opt_off: usize, - var_off: usize, - type_off: usize, - has_type_off: usize, -) -> Result<()> { - // ReflectionNamedType layout for building per-parameter type objects. - let named_type = ctx.module.class_infos.get("ReflectionNamedType").map(|ci| { - let off = |n: &str| ci.property_offsets.get(n).copied().unwrap_or(0); - ( - ci.class_id, - ci.properties.len(), - super::uninitialized_property_marker_offsets(ci), - off("__name"), - off("__allows_null"), - off("__builtin"), - ) - }); - emit_alloc_object_array(ctx, params.len()); - crate::codegen::emit_array_value_type_stamp( - ctx.emitter, - abi::int_result_reg(ctx.emitter), - &crate::types::PhpType::Object("ReflectionParameter".to_string()), - ); - for (position, param) in params.iter().enumerate() { - abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); - super::emit_object_allocation(ctx, class_id, property_count, false, markers, &[])?; - abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); - emit_reflection_string_property(ctx, ¶m.name, name_off, name_off + 8); - emit_reflection_int_property(ctx, position as i64, pos_off, pos_off + 8); - emit_reflection_int_property(ctx, param.optional as i64, opt_off, opt_off + 8); - emit_reflection_int_property(ctx, param.variadic as i64, var_off, var_off + 8); - if let (Some((type_name, builtin, allows_null)), Some((nt_id, nt_count, nt_markers, nt_name, nt_anull, nt_builtin))) = - (¶m.type_info, &named_type) - { - // Build a ReflectionNamedType (result reg); the parameter object is - // safe on the stack at slot 0 across this balanced construction. - super::emit_object_allocation(ctx, *nt_id, *nt_count, false, nt_markers, &[])?; - emit_reflection_string_property(ctx, type_name, *nt_name, *nt_name + 8); - emit_reflection_int_property(ctx, *builtin as i64, *nt_builtin, *nt_builtin + 8); - emit_reflection_int_property(ctx, *allows_null as i64, *nt_anull, *nt_anull + 8); - // `__type` is a `mixed` property, so its value must be a *boxed* - // Mixed cell (the receiver later dispatches `getType()->...` through - // the Mixed unbox path). Box the freshly built object pointer (still - // in the result reg) into a cell, then store it as a Mixed slot: - // boxed-cell pointer in the low word, 0 in the high word. The slot - // was zero-initialized at allocation, so no decref of an old value - // is required. - crate::codegen::emit_box_current_value_as_mixed( - ctx.emitter, - &crate::types::PhpType::Object("ReflectionNamedType".to_string()), - ); - let cell_reg = abi::int_result_reg(ctx.emitter); - let param_reg = abi::symbol_scratch_reg(ctx.emitter); - let flag_reg = abi::secondary_scratch_reg(ctx.emitter); - abi::emit_load_temporary_stack_slot(ctx.emitter, param_reg, 0); - abi::emit_store_to_address(ctx.emitter, cell_reg, param_reg, type_off); - abi::emit_store_zero_to_address(ctx.emitter, param_reg, type_off + 8); - abi::emit_load_int_immediate(ctx.emitter, flag_reg, 1); - abi::emit_store_to_address(ctx.emitter, flag_reg, param_reg, has_type_off); - abi::emit_store_zero_to_address(ctx.emitter, param_reg, has_type_off + 8); - } - emit_append_object_to_array(ctx); - } - Ok(()) -} - /// Stores namespace-aware name parts for a statically materialized ReflectionClass. fn emit_reflection_class_name_parts( ctx: &mut FunctionContext<'_>, @@ -1939,8 +1602,9 @@ fn reflection_parameter_members(sig: &FunctionSig) -> Vec Vec Option { + match ty { + PhpType::Int => Some(reflection_builtin_named_type("int", false)), + PhpType::Float => Some(reflection_builtin_named_type("float", false)), + PhpType::Str => Some(reflection_builtin_named_type("string", false)), + PhpType::Bool => Some(reflection_builtin_named_type("bool", false)), + PhpType::Iterable => Some(reflection_builtin_named_type("iterable", false)), + PhpType::Mixed => Some(reflection_builtin_named_type("mixed", true)), + PhpType::Array(_) | PhpType::AssocArray { .. } => { + Some(reflection_builtin_named_type("array", false)) + } + PhpType::Callable => Some(reflection_builtin_named_type("callable", false)), + PhpType::Object(name) => Some(ReflectionNamedTypeMetadata { + name: name.clone(), + allows_null: false, + is_builtin: false, + }), + PhpType::Union(members) => reflection_nullable_named_type_metadata(members), + _ => None, + } +} + +/// Builds metadata for one builtin named type. +fn reflection_builtin_named_type(name: &str, allows_null: bool) -> ReflectionNamedTypeMetadata { + ReflectionNamedTypeMetadata { + name: name.to_string(), + allows_null, + is_builtin: true, + } +} + +/// Handles `T|null` unions while leaving broader union types for future `ReflectionUnionType`. +fn reflection_nullable_named_type_metadata( + members: &[PhpType], +) -> Option { + let mut non_null_members = members.iter().filter(|member| !matches!(member, PhpType::Void)); + let first = non_null_members.next()?; + if non_null_members.next().is_some() { + return None; + } + let mut metadata = reflection_named_type_metadata(first)?; + metadata.allows_null = true; + Some(metadata) +} + /// Returns the `__construct` member object metadata when the reflected class-like symbol has one. fn reflection_constructor_member( method_members: &[ReflectionListedMember], @@ -3035,6 +2746,80 @@ fn emit_reflection_parameter_properties( "__has_type", parameter.has_type, )?; + emit_reflection_parameter_type_property(ctx, parameter)?; + Ok(()) +} + +/// Writes one ReflectionParameter object's nullable `ReflectionNamedType` slot. +fn emit_reflection_parameter_type_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let type_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__type")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(type_metadata) = parameter.type_metadata.as_ref() { + emit_reflection_named_type_object(ctx, type_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionNamedType".to_string()), + ); + } else { + emit_boxed_null_literal_to_result(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, type_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, type_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Allocates and populates one `ReflectionNamedType` object. +fn emit_reflection_named_type_object( + ctx: &mut FunctionContext<'_>, + type_metadata: &ReflectionNamedTypeMetadata, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets, name_offset) = { + let class_info = ctx + .module + .class_infos + .get("ReflectionNamedType") + .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionNamedType"))?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + reflection_property_offset(class_info, "__name")?, + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + )?; + emit_reflection_string_property(ctx, &type_metadata.name, name_offset, name_offset + 8); + emit_reflection_owner_bool_property( + ctx, + "ReflectionNamedType", + "__allows_null", + type_metadata.allows_null, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionNamedType", + "__is_builtin", + type_metadata.is_builtin, + )?; Ok(()) } diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 4f96fc2d34..9ffa3ad78c 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -849,6 +849,7 @@ fn lower_builtin_reflection_methods( "ReflectionEnumUnitCase", "ReflectionFunction", "ReflectionMethod", + "ReflectionNamedType", "ReflectionParameter", "ReflectionProperty", "ReflectionFunction", diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index a43fc135d8..d0ef9dc56c 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -38,7 +38,6 @@ pub(crate) fn inject_builtin_reflection( "ReflectionFunction", "ReflectionMethod", "ReflectionProperty", - "ReflectionFunction", "ReflectionParameter", "ReflectionNamedType", "ReflectionClassConstant", @@ -103,14 +102,7 @@ pub(crate) fn inject_builtin_reflection( }, ); class_map.insert("ReflectionClass".to_string(), builtin_reflection_class()); - class_map.insert( - "ReflectionFunction".to_string(), - builtin_reflection_owner_class( - "ReflectionFunction", - true, - vec![("function", Some(TypeExpr::Str), None, false)], - ), - ); + class_map.insert("ReflectionFunction".to_string(), builtin_reflection_function()); class_map.insert( "ReflectionMethod".to_string(), builtin_reflection_owner_class( @@ -133,15 +125,11 @@ pub(crate) fn inject_builtin_reflection( ], ), ); - class_map.insert("ReflectionFunction".to_string(), builtin_reflection_function()); class_map.insert( "ReflectionParameter".to_string(), builtin_reflection_parameter(), ); - class_map.insert( - "ReflectionNamedType".to_string(), - builtin_reflection_named_type(), - ); + class_map.insert("ReflectionNamedType".to_string(), builtin_reflection_named_type()); for class_name in [ "ReflectionClassConstant", "ReflectionEnumUnitCase", @@ -478,7 +466,7 @@ fn builtin_reflection_function_constructor_method() -> ClassMethod { is_abstract: false, is_final: false, has_body: true, - params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + params: vec![("function".to_string(), Some(TypeExpr::Str), None, false)], variadic: None, variadic_type: None, return_type: None, @@ -489,9 +477,9 @@ fn builtin_reflection_function_constructor_method() -> ClassMethod { } } -/// Builds the `ReflectionFunction` shell with private name/short-name and -/// parameter-count slots plus public accessors. The slots are populated at -/// codegen from the reflected function's signature. +/// Builds the `ReflectionFunction` shell with private name/short-name, +/// attribute, and parameter slots. Codegen populates these from the reflected +/// function's signature and attribute metadata. fn builtin_reflection_function() -> FlattenedClass { FlattenedClass { name: "ReflectionFunction".to_string(), @@ -502,27 +490,41 @@ fn builtin_reflection_function() -> FlattenedClass { is_readonly_class: false, properties: vec![ builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), - builtin_property("__short", Visibility::Private, Some(TypeExpr::Str), empty_string()), - builtin_property("__num_params", Visibility::Private, Some(TypeExpr::Int), int_lit(0)), builtin_property( - "__num_required", + "__short_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), + builtin_property( + "__parameters", + Visibility::Private, + Some(object_array_type("ReflectionParameter")), + empty_array(), + ), + builtin_property( + "__required_parameter_count", Visibility::Private, Some(TypeExpr::Int), int_lit(0), ), - builtin_property("__params", Visibility::Private, Some(array_type()), empty_array()), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), ], methods: vec![ builtin_reflection_function_constructor_method(), builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), - builtin_reflection_slot_getter("getShortName", "__short", TypeExpr::Str), - builtin_reflection_slot_getter("getNumberOfParameters", "__num_params", TypeExpr::Int), - builtin_reflection_slot_getter( + builtin_reflection_slot_getter("getShortName", "__short_name", TypeExpr::Str), + builtin_reflection_parameter_count_method(), + builtin_reflection_class_int_method( "getNumberOfRequiredParameters", - "__num_required", - TypeExpr::Int, + "__required_parameter_count", + ), + builtin_reflection_class_array_method( + "getParameters", + "__parameters", + object_array_type("ReflectionParameter"), ), - builtin_reflection_slot_getter("getParameters", "__params", array_type()), + builtin_reflection_owner_get_attributes_method(), ], attributes: Vec::new(), constants: Vec::new(), @@ -543,6 +545,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { is_readonly_class: false, properties: vec![ builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), builtin_property("__position", Visibility::Private, Some(TypeExpr::Int), int_lit(0)), builtin_property( "__optional", @@ -601,6 +604,7 @@ fn builtin_reflection_named_type() -> FlattenedClass { is_readonly_class: false, properties: vec![ builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), builtin_property( "__allows_null", Visibility::Private, @@ -608,16 +612,17 @@ fn builtin_reflection_named_type() -> FlattenedClass { bool_lit(false), ), builtin_property( - "__builtin", + "__is_builtin", Visibility::Private, Some(TypeExpr::Bool), bool_lit(false), ), ], methods: vec![ + builtin_reflection_private_constructor_method(), builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), builtin_reflection_slot_getter("allowsNull", "__allows_null", TypeExpr::Bool), - builtin_reflection_slot_getter("isBuiltin", "__builtin", TypeExpr::Bool), + builtin_reflection_slot_getter("isBuiltin", "__is_builtin", TypeExpr::Bool), ], attributes: Vec::new(), constants: Vec::new(), @@ -1807,6 +1812,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionMethod", "ReflectionProperty", "ReflectionParameter", + "ReflectionNamedType", "ReflectionClassConstant", "ReflectionEnumUnitCase", "ReflectionEnumBackedCase", @@ -1822,6 +1828,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { | "ReflectionMethod" | "ReflectionProperty" | "ReflectionParameter" + | "ReflectionNamedType" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" @@ -1955,6 +1962,16 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Bool; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getType")) { + sig.return_type = PhpType::Mixed; + } + } + if class_name == "ReflectionNamedType" { + for method_name in ["allowsnull", "isbuiltin"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index af1585a437..e3955239b1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6416,6 +6416,14 @@ foreach ($params as $param) { echo $param->isVariadic() ? "V" : "v"; echo $param->isPassedByReference() ? "R" : "b"; echo $param->hasType() ? "T" : "t"; + $type = $param->getType(); + if ($type) { + echo ":" . $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo ":null"; + } echo "|"; }'); "#, @@ -6425,7 +6433,10 @@ foreach ($params as $param) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "3/1:first@0rvbT|second@1OvbT|rest@2OVbt|"); + assert_eq!( + out.stdout, + "3/1:first@0rvbT:int!B|second@1OvbT:App\\Name?C|rest@2OVbt:null|" + ); } /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index f482048970..741672188a 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1929,6 +1929,47 @@ echo ($traitParams[2]->hasType() ? "T" : "t"); ); } +/// Verifies that `ReflectionParameter::getType()` returns simple named type metadata. +#[test] +fn test_reflection_parameter_get_type_returns_named_type_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + echo $param->getName() . ":"; + echo $param->hasType() ? "T:" : "t:"; + $type = $param->getType(); + if ($type instanceof ReflectionNamedType) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionParameter([ReflectParamTypeTarget::class, "run"], "dep"); +$directType = $direct->getType(); +if ($directType instanceof ReflectionNamedType) { + echo "direct:" . $directType->getName(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:T:int!B|name:T:string?B|dep:T:ReflectParamTypeDep!C|plain:t:null|union:T:null|direct:ReflectParamTypeDep" + ); +} + /// Verifies direct `new ReflectionParameter()` construction for statically known /// class and interface method targets. #[test] From 503fff6d1e0edaa6f39d12f13d70abad98aca604 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 11:29:00 +0200 Subject: [PATCH 0410/1208] Add ReflectionParameter default value metadata --- .../src/interpreter/dynamic_functions.rs | 2 +- .../elephc-eval/src/interpreter/reflection.rs | 21 +++-- .../tests/builtins_class_metadata.rs | 7 +- .../interpreter/tests/support/object_ops.rs | 13 +++ docs/php/classes.md | 8 +- docs/php/eval.md | 6 +- src/codegen/eval_reflection_owner_helpers.rs | 70 ++++++++++++++++ src/codegen/lower_inst/objects/reflection.rs | 80 ++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 82 ++++++++++++++++++- tests/codegen/eval.rs | 7 +- tests/codegen/oop/attributes.rs | 33 ++++++++ 11 files changed, 314 insertions(+), 15 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index a7b0ae19c0..5e0eca5a73 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -662,7 +662,7 @@ fn eval_method_numeric_coercible_value( } /// Materializes a supported eval method parameter default expression. -fn eval_method_parameter_default( +pub(in crate::interpreter) fn eval_method_parameter_default( default: &EvalExpr, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 6fff194ed0..44cc0cdd9b 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -29,6 +29,7 @@ const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; +const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; const EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL: u64 = 1; const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; @@ -65,6 +66,7 @@ struct EvalReflectionParameterMetadata { is_passed_by_reference: bool, has_type: bool, type_metadata: Option, + default_value: Option, } /// Eval metadata needed to materialize one `ReflectionNamedType` object. @@ -671,7 +673,7 @@ fn eval_reflection_owner_object( values, )? } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - eval_reflection_parameter_object_array_result(parameter_metadata, values)? + eval_reflection_parameter_object_array_result(parameter_metadata, context, values)? } else { values.array_new(0)? }; @@ -765,11 +767,12 @@ fn eval_reflection_string_array_result( /// Builds an indexed array of populated ReflectionParameter objects. fn eval_reflection_parameter_object_array_result( parameters: &[EvalReflectionParameterMetadata], + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let mut result = values.array_new(parameters.len())?; for parameter in parameters { - let parameter_object = eval_reflection_parameter_object_result(parameter, values)?; + let parameter_object = eval_reflection_parameter_object_result(parameter, context, values)?; let key = values.int(parameter.position as i64)?; result = values.array_set(result, key, parameter_object)?; } @@ -779,6 +782,7 @@ fn eval_reflection_parameter_object_array_result( /// Materializes one ReflectionParameter object through the shared reflection helper. fn eval_reflection_parameter_object_result( parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let attrs = values.array_new(0)?; @@ -787,12 +791,15 @@ fn eval_reflection_parameter_object_result( let method_names = values.array_new(0)?; let property_names = values.array_new(0)?; let method_objects = values.array_new(0)?; - let property_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; let type_value = match parameter.type_metadata.as_ref() { Some(type_metadata) => eval_reflection_named_type_object_result(type_metadata, values)?, None => values.null()?, }; + let default_value = match parameter.default_value.as_ref() { + Some(default) => eval_method_parameter_default(default, context, values)?, + None => values.null()?, + }; let flags = eval_reflection_parameter_flags(parameter); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_PARAMETER, @@ -803,7 +810,7 @@ fn eval_reflection_parameter_object_result( method_names, property_names, type_value, - property_objects, + default_value, parent_class, flags, parameter.position as u64, @@ -815,7 +822,7 @@ fn eval_reflection_parameter_object_result( values.release(property_names)?; values.release(method_objects)?; values.release(type_value)?; - values.release(property_objects)?; + values.release(default_value)?; values.release(parent_class)?; Ok(object) } @@ -1310,6 +1317,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( .and_then(Option::as_ref) .and_then(eval_reflection_named_type_metadata) .filter(|_| has_type_flags.get(position).copied().unwrap_or(false)), + default_value: defaults.get(position).and_then(Clone::clone), }) .collect() } @@ -1408,6 +1416,9 @@ fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) if parameter.has_type { flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE; } + if parameter.default_value.is_some() { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE; + } flags } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 3408b633d5..8bb5856952 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -748,6 +748,11 @@ foreach ($params as $param) { } else { echo ":null"; } + echo $param->isDefaultValueAvailable() ? ":D" : ":d"; + if ($param->isDefaultValueAvailable()) { + echo "="; + echo $param->getDefaultValue() === null ? "null" : $param->getDefaultValue(); + } echo "|"; } return true;"##, @@ -760,7 +765,7 @@ return true;"##, assert_eq!( values.output, - "3/1:first#0rvRT:int!B|second#1OvbT:App\\Name?C|rest#2OVRt:null|" + "3/1:first#0rvRT:int!B:d|second#1OvbT:App\\Name?C:D=null|rest#2OVRt:null:d|" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index bce1bbc363..74715b49cf 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -19,6 +19,7 @@ const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; +const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; impl FakeOps { /// Reads one fake object property by name. @@ -288,6 +289,10 @@ impl FakeOps { (FakeValue::Object(properties), "gettype") if args.is_empty() => { Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "getdefaultvalue") if args.is_empty() => { + Self::object_property(&properties, "__default_value") + .map_or_else(|| self.null(), Ok) + } (FakeValue::Object(properties), "isoptional") if args.is_empty() => { Self::object_property(&properties, "__is_optional") .map_or_else(|| self.bool_value(false), Ok) @@ -304,6 +309,10 @@ impl FakeOps { Self::object_property(&properties, "__has_type") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isdefaultvalueavailable") if args.is_empty() => { + Self::object_property(&properties, "__has_default_value") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "allowsnull") if args.is_empty() => { Self::object_property(&properties, "__allows_null") .map_or_else(|| self.bool_value(false), Ok) @@ -517,6 +526,8 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_BY_REF) != 0)?; let has_type = self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE) != 0)?; + let has_default_value = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE) != 0)?; properties.push(("__position".to_string(), position)); properties.push(("__is_optional".to_string(), is_optional)); properties.push(("__is_variadic".to_string(), is_variadic)); @@ -526,6 +537,8 @@ impl FakeOps { )); properties.push(("__has_type".to_string(), has_type)); properties.push(("__type".to_string(), method_objects)); + properties.push(("__has_default_value".to_string(), has_default_value)); + properties.push(("__default_value".to_string(), property_objects)); } if owner_kind == EVAL_REFLECTION_OWNER_NAMED_TYPE { let allows_null = self.bool_value((flags & 1) != 0)?; diff --git a/docs/php/classes.md b/docs/php/classes.md index 077d7995b8..02c79f4d48 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1196,6 +1196,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | | `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types or `null` when no type is declared or the declared type needs a broader Reflection type object | +| `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | +| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null default values, or throw `ReflectionException` when no default is available | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | @@ -1251,6 +1253,8 @@ name with `isBuiltin()` false. | `ReflectionParameter::isVariadic()` | `bool` | True for the `...$rest` parameter | | `ReflectionParameter::hasType()` | `bool` | True when the parameter declares a type | | `ReflectionParameter::getType()` | `?ReflectionNamedType` | The declared type, or `null` when untyped | +| `ReflectionParameter::isDefaultValueAvailable()` | `bool` | True when a supported default value is available | +| `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar or null default value, or throws when unavailable | | `ReflectionNamedType::getName()` | `string` | The type name (`int`, `string`, a class name, …) | | `ReflectionNamedType::isBuiltin()` | `bool` | True for builtin types, false for class types | | `ReflectionNamedType::allowsNull()` | `bool` | True when the declared type is nullable | @@ -1261,7 +1265,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, and `getType()` for simple named types. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as union/intersection parameter type objects, parameter default-value objects, parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named types, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as union/intersection parameter type objects, array/object/class-constant parameter default values, parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1289,4 +1293,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, and simple named type metadata are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named type metadata, and scalar/null parameter defaults are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). diff --git a/docs/php/eval.md b/docs/php/eval.md index 2773688adb..c514ee833a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -203,7 +203,8 @@ declared-type presence, and simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, `allowsNull()`, and `isBuiltin()`. Union/intersection parameter type objects are not yet materialized. Defaulted eval method parameters are bound when omitted and reported through -`ReflectionParameter::isOptional()`. Supported default expressions include +`ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and +`getDefaultValue()`. Supported default expressions include scalar literals, arrays whose keys and values are supported default expressions, magic constants, unary and binary operators supported by eval, ternary and null-coalescing expressions, predefined or eval-defined constant @@ -384,7 +385,8 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/NamedType/attribute slice, -union/intersection Reflection type objects, and broader generated/AOT method +union/intersection Reflection type objects, broader parameter default-value +objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 61042b8006..c351455dbe 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -86,6 +86,10 @@ struct ReflectionOwnerLayout { has_type_hi: Option, parameter_type_lo: Option, parameter_type_hi: Option, + has_default_value_lo: Option, + has_default_value_hi: Option, + default_value_lo: Option, + default_value_hi: Option, allows_null_lo: Option, allows_null_hi: Option, is_builtin_lo: Option, @@ -211,6 +215,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, + default_value: Option, } /// Metadata for one `ReflectionNamedType` returned by `ReflectionParameter::getType()`. @@ -87,6 +88,16 @@ struct ReflectionNamedTypeMetadata { is_builtin: bool, } +/// Compile-time default forms returned by `ReflectionParameter::getDefaultValue()`. +#[derive(Clone)] +enum ReflectionParameterDefaultValue { + Int(i64), + Bool(bool), + Float(f64), + Str(String), + Null, +} + /// Metadata for one constant entry returned by `ReflectionClass::getConstants()`. #[derive(Clone)] struct ReflectionConstantMember { @@ -1618,11 +1629,35 @@ fn reflection_parameter_members(sig: &FunctionSig) -> Vec Option { + match &default.kind { + ExprKind::IntLiteral(value) => Some(ReflectionParameterDefaultValue::Int(*value)), + ExprKind::BoolLiteral(value) => Some(ReflectionParameterDefaultValue::Bool(*value)), + ExprKind::FloatLiteral(value) => Some(ReflectionParameterDefaultValue::Float(*value)), + ExprKind::StringLiteral(value) => Some(ReflectionParameterDefaultValue::Str(value.clone())), + ExprKind::Null => Some(ReflectionParameterDefaultValue::Null), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value + .checked_neg() + .map(ReflectionParameterDefaultValue::Int), + ExprKind::FloatLiteral(value) => Some(ReflectionParameterDefaultValue::Float(-value)), + _ => None, + }, + _ => None, + } +} + /// Converts a normalized parameter type into the simple `ReflectionNamedType` subset. fn reflection_named_type_metadata(ty: &PhpType) -> Option { match ty { @@ -2747,6 +2782,13 @@ fn emit_reflection_parameter_properties( parameter.has_type, )?; emit_reflection_parameter_type_property(ctx, parameter)?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__has_default_value", + parameter.default_value.is_some(), + )?; + emit_reflection_parameter_default_property(ctx, parameter)?; Ok(()) } @@ -2782,6 +2824,44 @@ fn emit_reflection_parameter_type_property( Ok(()) } +/// Writes one ReflectionParameter object's boxed default-value slot. +fn emit_reflection_parameter_default_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let default_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__default_value")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + match parameter.default_value.as_ref() { + Some(ReflectionParameterDefaultValue::Int(value)) => { + emit_boxed_int_literal_to_result(ctx, *value) + } + Some(ReflectionParameterDefaultValue::Bool(value)) => { + emit_boxed_bool_literal_to_result(ctx, *value) + } + Some(ReflectionParameterDefaultValue::Float(value)) => { + emit_boxed_float_literal_to_result(ctx, *value) + } + Some(ReflectionParameterDefaultValue::Str(value)) => { + emit_boxed_string_literal_default_to_result(ctx, value) + } + Some(ReflectionParameterDefaultValue::Null) | None => emit_boxed_null_literal_to_result(ctx), + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, default_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, default_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + /// Allocates and populates one `ReflectionNamedType` object. fn emit_reflection_named_type_object( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index d0ef9dc56c..456a60e68b 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -567,6 +567,18 @@ fn builtin_reflection_parameter() -> FlattenedClass { ), builtin_property("__has_type", Visibility::Private, Some(TypeExpr::Bool), bool_lit(false)), builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), + builtin_property( + "__has_default_value", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__default_value", + Visibility::Private, + Some(mixed_type()), + null_lit(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![ @@ -584,6 +596,12 @@ fn builtin_reflection_parameter() -> FlattenedClass { ), builtin_reflection_slot_getter("hasType", "__has_type", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), + builtin_reflection_slot_getter( + "isDefaultValueAvailable", + "__has_default_value", + TypeExpr::Bool, + ), + builtin_reflection_parameter_get_default_value_method(), ], attributes: Vec::new(), constants: Vec::new(), @@ -591,6 +609,56 @@ fn builtin_reflection_parameter() -> FlattenedClass { } } +/// Builds `ReflectionParameter::getDefaultValue()` over the retained default slot. +fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "getDefaultValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__has_default_value", + dummy_span, + ))), + dummy_span, + ), + then_body: vec![throw_new_reflection_exception( + string_lit( + "Internal error: Failed to retrieve the default value", + dummy_span, + ), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(reflection_this_property( + "__default_value", + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds the `ReflectionNamedType` shell: a parameter/return type rendered as a /// runtime object with a name, nullability flag, and builtin flag. Populated at /// codegen from the declared type. @@ -1957,13 +2025,21 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPosition")) { sig.return_type = PhpType::Int; } - for method_name in ["isoptional", "isvariadic", "ispassedbyreference", "hastype"] { + for method_name in [ + "isoptional", + "isvariadic", + "ispassedbyreference", + "hastype", + "isdefaultvalueavailable", + ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; } } - if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getType")) { - sig.return_type = PhpType::Mixed; + for method_name in ["getType", "getDefaultValue"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Mixed; + } } } if class_name == "ReflectionNamedType" { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e3955239b1..cee97f861f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6424,6 +6424,11 @@ foreach ($params as $param) { } else { echo ":null"; } + echo $param->isDefaultValueAvailable() ? ":D" : ":d"; + if ($param->isDefaultValueAvailable()) { + echo "="; + echo $param->getDefaultValue() === null ? "null" : $param->getDefaultValue(); + } echo "|"; }'); "#, @@ -6435,7 +6440,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "3/1:first@0rvbT:int!B|second@1OvbT:App\\Name?C|rest@2OVbt:null|" + "3/1:first@0rvbT:int!B:d|second@1OvbT:App\\Name?C:D=null|rest@2OVbt:null:d|" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 741672188a..85108b69c7 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1970,6 +1970,39 @@ if ($directType instanceof ReflectionNamedType) { ); } +/// Verifies that `ReflectionParameter` exposes supported scalar/null defaults. +#[test] +fn test_reflection_parameter_exposes_default_values() { + let out = compile_and_run_capture( + r##"getParameters(); +echo $params[0]->isDefaultValueAvailable() ? "D" : "d"; +try { + $params[0]->getDefaultValue(); +} catch (ReflectionException $e) { + echo ":E"; +} +echo "|"; +echo $params[1]->isDefaultValueAvailable() ? "D:" : "d:"; +echo $params[1]->getDefaultValue(); +echo "|"; +echo $params[2]->isDefaultValueAvailable() ? "D:" : "d:"; +echo $params[2]->getDefaultValue() === null ? "null" : "value"; +echo "|"; +$direct = new ReflectionParameter("reflect_default_function", "label"); +echo $direct->isDefaultValueAvailable() ? "D:" : "d:"; +echo $direct->getDefaultValue(); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "d:E|D:7|D:null|D:ok"); +} + /// Verifies direct `new ReflectionParameter()` construction for statically known /// class and interface method targets. #[test] From ba8af9791f45b4a9dd95b00e35486a2d74c69ef3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 11:48:56 +0200 Subject: [PATCH 0411/1208] Add ReflectionUnionType parameter metadata --- .../elephc-eval/src/interpreter/reflection.rs | 135 +++++++++++++- .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 15 +- .../interpreter/tests/support/object_ops.rs | 10 + docs/php/classes.md | 11 +- docs/php/eval.md | 18 +- src/autoload/mod.rs | 1 + src/codegen/eval_reflection_owner_helpers.rs | 63 ++++--- src/codegen/lower_inst/objects.rs | 2 + src/codegen/lower_inst/objects/reflection.rs | 172 +++++++++++++++--- src/ir_lower/program.rs | 4 +- src/types/checker/builtin_types/reflection.rs | 59 +++++- tests/codegen/eval.rs | 13 +- tests/codegen/oop/attributes.rs | 13 +- 14 files changed, 435 insertions(+), 82 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 44cc0cdd9b..f95ff2d09b 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -65,10 +65,21 @@ struct EvalReflectionParameterMetadata { is_variadic: bool, is_passed_by_reference: bool, has_type: bool, - type_metadata: Option, + type_metadata: Option, default_value: Option, } +/// Eval metadata needed to materialize one parameter `ReflectionType` object. +struct EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind, +} + +/// Eval reflection parameter type object variants. +enum EvalReflectionParameterTypeKind { + Named(EvalReflectionNamedTypeMetadata), + Union(EvalReflectionUnionTypeMetadata), +} + /// Eval metadata needed to materialize one `ReflectionNamedType` object. struct EvalReflectionNamedTypeMetadata { name: String, @@ -76,6 +87,12 @@ struct EvalReflectionNamedTypeMetadata { is_builtin: bool, } +/// Eval metadata needed to materialize one `ReflectionUnionType` object. +struct EvalReflectionUnionTypeMetadata { + types: Vec, + allows_null: bool, +} + /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. pub(in crate::interpreter) fn eval_reflection_owner_new_object( class_name: &str, @@ -793,7 +810,7 @@ fn eval_reflection_parameter_object_result( let method_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; let type_value = match parameter.type_metadata.as_ref() { - Some(type_metadata) => eval_reflection_named_type_object_result(type_metadata, values)?, + Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, None => values.null()?, }; let default_value = match parameter.default_value.as_ref() { @@ -827,6 +844,21 @@ fn eval_reflection_parameter_object_result( Ok(object) } +/// Materializes one parameter ReflectionType object through the shared reflection helper. +fn eval_reflection_type_object_result( + type_metadata: &EvalReflectionParameterTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named_type) => { + eval_reflection_named_type_object_result(named_type, values) + } + EvalReflectionParameterTypeKind::Union(union_type) => { + eval_reflection_union_type_object_result(union_type, values) + } + } +} + /// Materializes one ReflectionNamedType object through the shared reflection helper. fn eval_reflection_named_type_object_result( type_metadata: &EvalReflectionNamedTypeMetadata, @@ -866,6 +898,59 @@ fn eval_reflection_named_type_object_result( Ok(object) } +/// Materializes one ReflectionUnionType object through the shared reflection helper. +fn eval_reflection_union_type_object_result( + type_metadata: &EvalReflectionUnionTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let flags = eval_reflection_union_type_flags(type_metadata); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_UNION_TYPE, + "", + attrs, + interface_names, + trait_names, + method_names, + property_names, + types, + property_objects, + parent_class, + flags, + 0, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(types)?; + values.release(property_objects)?; + values.release(parent_class)?; + Ok(object) +} + +/// Builds an indexed array of populated ReflectionNamedType objects. +fn eval_reflection_named_type_object_array_result( + types: &[EvalReflectionNamedTypeMetadata], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(types.len())?; + for (position, type_metadata) in types.iter().enumerate() { + let type_object = eval_reflection_named_type_object_result(type_metadata, values)?; + let key = values.int(position as i64)?; + result = values.array_set(result, key, type_object)?; + } + Ok(result) +} + /// Builds an indexed array of ReflectionMethod or ReflectionProperty objects for a ReflectionClass. fn eval_reflection_member_object_array_result( owner_kind: u64, @@ -1315,21 +1400,46 @@ fn eval_reflection_parameters_from_names_and_type_flags( type_metadata: parameter_types .get(position) .and_then(Option::as_ref) - .and_then(eval_reflection_named_type_metadata) + .and_then(eval_reflection_parameter_type_metadata) .filter(|_| has_type_flags.get(position).copied().unwrap_or(false)), default_value: defaults.get(position).and_then(Clone::clone), }) .collect() } -/// Converts eval parameter type metadata into the simple ReflectionNamedType subset. -fn eval_reflection_named_type_metadata( +/// Converts eval parameter type metadata into the supported ReflectionType subset. +fn eval_reflection_parameter_type_metadata( parameter_type: &EvalParameterType, -) -> Option { - let [variant] = parameter_type.variants() else { +) -> Option { + let variants = parameter_type.variants(); + if variants.is_empty() { return None; - }; + } let allows_null = parameter_type.allows_null(); + let mut types = variants + .iter() + .map(|variant| eval_reflection_named_type_variant_metadata(variant, false)) + .collect::>>()?; + if types.len() == 1 { + let mut named = types.pop()?; + named.allows_null = allows_null; + return Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(named), + }); + } + Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Union(EvalReflectionUnionTypeMetadata { + types, + allows_null, + }), + }) +} + +/// Converts one eval parameter type variant into `ReflectionNamedType` metadata. +fn eval_reflection_named_type_variant_metadata( + variant: &EvalParameterTypeVariant, + allows_null: bool, +) -> Option { match variant { EvalParameterTypeVariant::Array => { Some(eval_reflection_builtin_named_type("array", allows_null)) @@ -1434,6 +1544,15 @@ fn eval_reflection_named_type_flags(type_metadata: &EvalReflectionNamedTypeMetad flags } +/// Packs ReflectionUnionType predicate flags for the runtime type factory. +fn eval_reflection_union_type_flags(type_metadata: &EvalReflectionUnionTypeMetadata) -> u64 { + if type_metadata.allows_null { + EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL + } else { + 0 + } +} + /// Converts one reflection constructor argument to a Rust UTF-8 string. fn eval_reflection_string_arg( value: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 82007d1fd5..040fe87b94 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -367,3 +367,4 @@ pub(super) const EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE: u64 = 4; pub(super) const EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE: u64 = 5; pub(super) const EVAL_REFLECTION_OWNER_PARAMETER: u64 = 6; pub(super) const EVAL_REFLECTION_OWNER_NAMED_TYPE: u64 = 7; +pub(super) const EVAL_REFLECTION_OWNER_UNION_TYPE: u64 = 8; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 8bb5856952..c9c20bb5f9 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -727,8 +727,8 @@ return true;"#, #[test] fn execute_program_reflects_eval_method_parameters() { let program = parse_fragment( - br##"class EvalReflectParamTarget { - public function run(int &$first, \App\Name|null $second = null, &...$rest) {} +br##"class EvalReflectParamTarget { + public function run(int &$first, int|string $union, \App\Name|null $second = null, &...$rest) {} } $method = new ReflectionMethod("EvalReflectParamTarget", "run"); echo $method->getNumberOfParameters(); echo "/"; @@ -741,7 +741,14 @@ foreach ($params as $param) { echo $param->isPassedByReference() ? "R" : "b"; echo $param->hasType() ? "T" : "t"; $type = $param->getType(); - if ($type) { + if ($param->getName() == "union") { + echo ":union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { echo ":"; echo $type->getName(); echo $type->allowsNull() ? "?" : "!"; echo $type->isBuiltin() ? "B" : "C"; @@ -765,7 +772,7 @@ return true;"##, assert_eq!( values.output, - "3/1:first#0rvRT:int!B:d|second#1OvbT:App\\Name?C:D=null|rest#2OVRt:null:d|" + "4/2:first#0rvRT:int!B:d|union#1rvbT:union!:intB:stringB:d|second#2OvbT:App\\Name?C:D=null|rest#3OVRt:null:d|" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 74715b49cf..a0799d1742 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -289,6 +289,10 @@ impl FakeOps { (FakeValue::Object(properties), "gettype") if args.is_empty() => { Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "gettypes") if args.is_empty() => { + Self::object_property(&properties, "__types") + .map_or_else(|| self.runtime_array_new(0), Ok) + } (FakeValue::Object(properties), "getdefaultvalue") if args.is_empty() => { Self::object_property(&properties, "__default_value") .map_or_else(|| self.null(), Ok) @@ -451,6 +455,7 @@ impl FakeOps { EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => "ReflectionEnumBackedCase", EVAL_REFLECTION_OWNER_PARAMETER => "ReflectionParameter", EVAL_REFLECTION_OWNER_NAMED_TYPE => "ReflectionNamedType", + EVAL_REFLECTION_OWNER_UNION_TYPE => "ReflectionUnionType", _ => return Err(EvalStatus::RuntimeFatal), }; let name = self.string(reflected_name)?; @@ -546,6 +551,11 @@ impl FakeOps { properties.push(("__allows_null".to_string(), allows_null)); properties.push(("__is_builtin".to_string(), is_builtin)); } + if owner_kind == EVAL_REFLECTION_OWNER_UNION_TYPE { + let allows_null = self.bool_value((flags & 1) != 0)?; + properties.push(("__types".to_string(), method_objects)); + properties.push(("__allows_null".to_string(), allows_null)); + } let object = self.alloc(FakeValue::Object(properties)); self.object_classes .insert(object.as_ptr() as usize, class_name.to_string()); diff --git a/docs/php/classes.md b/docs/php/classes.md index 02c79f4d48..ddfe0c4e7d 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1195,10 +1195,11 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | | `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | | `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | -| `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types or `null` when no type is declared or the declared type needs a broader Reflection type object | +| `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | | `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null default values, or throw `ReflectionException` when no default is available | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | +| `ReflectionUnionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return non-null union members as `ReflectionNamedType` objects and report whether `null` was part of the union | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | @@ -1252,12 +1253,14 @@ name with `isBuiltin()` false. | `ReflectionParameter::isOptional()` | `bool` | True for a parameter with a default or variadic, and any after it | | `ReflectionParameter::isVariadic()` | `bool` | True for the `...$rest` parameter | | `ReflectionParameter::hasType()` | `bool` | True when the parameter declares a type | -| `ReflectionParameter::getType()` | `?ReflectionNamedType` | The declared type, or `null` when untyped | +| `ReflectionParameter::getType()` | `?ReflectionNamedType\|ReflectionUnionType` | The declared type, or `null` when untyped | | `ReflectionParameter::isDefaultValueAvailable()` | `bool` | True when a supported default value is available | | `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar or null default value, or throws when unavailable | | `ReflectionNamedType::getName()` | `string` | The type name (`int`, `string`, a class name, …) | | `ReflectionNamedType::isBuiltin()` | `bool` | True for builtin types, false for class types | | `ReflectionNamedType::allowsNull()` | `bool` | True when the declared type is nullable | +| `ReflectionUnionType::getTypes()` | `ReflectionNamedType[]` | The non-null named members of a supported union | +| `ReflectionUnionType::allowsNull()` | `bool` | True when the union includes `null` | Limitations today: - All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Function/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name→id lookup table that is not yet implemented. @@ -1265,7 +1268,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named types, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as union/intersection parameter type objects, array/object/class-constant parameter default values, parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named and union parameter types, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` supports `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as intersection parameter type objects, array/object/class-constant parameter default values, parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1293,4 +1296,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named type metadata, and scalar/null parameter defaults are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named and union type metadata, and scalar/null parameter defaults are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType`/`ReflectionUnionType` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). diff --git a/docs/php/eval.md b/docs/php/eval.md index c514ee833a..d5d6079810 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -199,10 +199,12 @@ eval/runtime class or interface names. `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report eval-declared method parameter metadata. Eval currently exposes parameter names and zero-based positions there, -declared-type presence, and simple named type metadata through +declared-type presence, simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, -`allowsNull()`, and `isBuiltin()`. Union/intersection parameter type objects are -not yet materialized. Defaulted eval method parameters are bound when omitted and reported through +`allowsNull()`, and `isBuiltin()`, and multi-member union metadata through +`ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter +type objects are not yet materialized. Defaulted eval method parameters are +bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and `getDefaultValue()`. Supported default expressions include scalar literals, arrays whose keys and values are supported default @@ -384,11 +386,11 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported -ReflectionClass/Method/Parameter/Property/NamedType/attribute slice, -union/intersection Reflection type objects, broader parameter default-value -objects beyond the eval-supported constant-expression subset, and broader generated/AOT method -bridge signatures beyond the current public non-by-reference fixed scalar/Mixed -slice. +ReflectionClass/Method/Parameter/Property/NamedType/UnionType/attribute slice, +ReflectionUnionType APIs beyond parameter metadata, intersection Reflection type +objects, broader parameter default-value objects beyond the eval-supported +constant-expression subset, and broader generated/AOT method bridge signatures +beyond the current public non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index 7799470eb7..9269fe9437 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -82,6 +82,7 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "ReflectionNamedType", "ReflectionParameter", "ReflectionProperty", + "ReflectionUnionType", "RuntimeException", "SeekableIterator", "SortDirection", diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index c351455dbe..9650ecf6ac 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -106,6 +106,7 @@ struct ReflectionOwnerLayouts { enum_backed_case: ReflectionOwnerLayout, parameter: ReflectionOwnerLayout, named_type: ReflectionOwnerLayout, + union_type: ReflectionOwnerLayout, } /// Emits eval Reflection owner helpers when any lowered function owns an eval context. @@ -174,6 +175,7 @@ fn reflection_owner_layouts(module: &Module) -> Option { )?, parameter: reflection_owner_layout(module.class_infos.get("ReflectionParameter")?, true)?, named_type: reflection_owner_layout(module.class_infos.get("ReflectionNamedType")?, true)?, + union_type: reflection_owner_layout(module.class_infos.get("ReflectionUnionType")?, false)?, }) } @@ -190,7 +192,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option &'static [&'static str] { "ReflectionNamedType", "ReflectionParameter", "ReflectionProperty", + "ReflectionUnionType", "RuntimeException", "SplDoublyLinkedList", "SplFixedArray", @@ -1454,6 +1455,7 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionNamedType", "ReflectionParameter", "ReflectionProperty", + "ReflectionUnionType", "RegexIterator", "RuntimeException", "SplDoublyLinkedList", diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 8e66673ab9..656f296ccf 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -76,10 +76,17 @@ struct ReflectionParameterMember { is_variadic: bool, is_passed_by_reference: bool, has_type: bool, - type_metadata: Option, + type_metadata: Option, default_value: Option, } +/// Metadata for one `ReflectionType` object returned by `ReflectionParameter::getType()`. +#[derive(Clone)] +enum ReflectionParameterTypeMetadata { + Named(ReflectionNamedTypeMetadata), + Union(ReflectionUnionTypeMetadata), +} + /// Metadata for one `ReflectionNamedType` returned by `ReflectionParameter::getType()`. #[derive(Clone)] struct ReflectionNamedTypeMetadata { @@ -88,6 +95,13 @@ struct ReflectionNamedTypeMetadata { is_builtin: bool, } +/// Metadata for one `ReflectionUnionType` returned by `ReflectionParameter::getType()`. +#[derive(Clone)] +struct ReflectionUnionTypeMetadata { + types: Vec, + allows_null: bool, +} + /// Compile-time default forms returned by `ReflectionParameter::getDefaultValue()`. #[derive(Clone)] enum ReflectionParameterDefaultValue { @@ -1628,7 +1642,7 @@ fn reflection_parameter_members(sig: &FunctionSig) -> Vec Option Option { + match ty { + PhpType::Union(members) => reflection_union_or_nullable_type_metadata(members), + _ => reflection_named_type_metadata(ty).map(ReflectionParameterTypeMetadata::Named), + } +} + +/// Converts a normalized non-union parameter type into a simple `ReflectionNamedType`. fn reflection_named_type_metadata(ty: &PhpType) -> Option { match ty { PhpType::Int => Some(reflection_builtin_named_type("int", false)), @@ -1676,7 +1698,6 @@ fn reflection_named_type_metadata(ty: &PhpType) -> Option reflection_nullable_named_type_metadata(members), _ => None, } } @@ -1690,18 +1711,27 @@ fn reflection_builtin_named_type(name: &str, allows_null: bool) -> ReflectionNam } } -/// Handles `T|null` unions while leaving broader union types for future `ReflectionUnionType`. -fn reflection_nullable_named_type_metadata( +/// Handles `T|null` as a nullable named type and wider unions as `ReflectionUnionType`. +fn reflection_union_or_nullable_type_metadata( members: &[PhpType], -) -> Option { - let mut non_null_members = members.iter().filter(|member| !matches!(member, PhpType::Void)); - let first = non_null_members.next()?; - if non_null_members.next().is_some() { - return None; - } - let mut metadata = reflection_named_type_metadata(first)?; - metadata.allows_null = true; - Some(metadata) +) -> Option { + let allows_null = members.iter().any(|member| matches!(member, PhpType::Void)); + let non_null_members = members + .iter() + .filter(|member| !matches!(member, PhpType::Void)) + .collect::>(); + if non_null_members.len() == 1 { + let mut metadata = reflection_named_type_metadata(non_null_members[0])?; + metadata.allows_null = allows_null; + return Some(ReflectionParameterTypeMetadata::Named(metadata)); + } + let types = non_null_members + .into_iter() + .map(reflection_named_type_metadata) + .collect::>>()?; + (!types.is_empty()).then_some(ReflectionParameterTypeMetadata::Union( + ReflectionUnionTypeMetadata { types, allows_null }, + )) } /// Returns the `__construct` member object metadata when the reflected class-like symbol has one. @@ -2806,16 +2836,24 @@ fn emit_reflection_parameter_type_property( reflection_property_offset(class_info, "__type")? }; let result_reg = abi::int_result_reg(ctx.emitter); - let object_reg = abi::secondary_scratch_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); - if let Some(type_metadata) = parameter.type_metadata.as_ref() { - emit_reflection_named_type_object(ctx, type_metadata)?; - emit_box_current_value_as_mixed( - ctx.emitter, - &PhpType::Object("ReflectionNamedType".to_string()), - ); - } else { - emit_boxed_null_literal_to_result(ctx); + match parameter.type_metadata.as_ref() { + Some(ReflectionParameterTypeMetadata::Named(type_metadata)) => { + emit_reflection_named_type_object(ctx, type_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionNamedType".to_string()), + ); + } + Some(ReflectionParameterTypeMetadata::Union(type_metadata)) => { + emit_reflection_union_type_object(ctx, type_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionUnionType".to_string()), + ); + } + None => emit_boxed_null_literal_to_result(ctx), } abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, type_offset); @@ -2838,7 +2876,7 @@ fn emit_reflection_parameter_default_property( reflection_property_offset(class_info, "__default_value")? }; let result_reg = abi::int_result_reg(ctx.emitter); - let object_reg = abi::secondary_scratch_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); match parameter.default_value.as_ref() { Some(ReflectionParameterDefaultValue::Int(value)) => { @@ -2862,6 +2900,90 @@ fn emit_reflection_parameter_default_property( Ok(()) } +/// Allocates and populates one `ReflectionUnionType` object. +fn emit_reflection_union_type_object( + ctx: &mut FunctionContext<'_>, + type_metadata: &ReflectionUnionTypeMetadata, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = ctx + .module + .class_infos + .get("ReflectionUnionType") + .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionUnionType"))?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + )?; + emit_reflection_union_type_types_property(ctx, &type_metadata.types)?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionUnionType", + "__allows_null", + type_metadata.allows_null, + )?; + Ok(()) +} + +/// Writes the `ReflectionUnionType::__types` array of `ReflectionNamedType` objects. +fn emit_reflection_union_type_types_property( + ctx: &mut FunctionContext<'_>, + types: &[ReflectionNamedTypeMetadata], +) -> Result<()> { + let types_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionUnionType") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__types")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + emit_reflection_named_type_array(ctx, types)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, types_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + types_offset + 8, + ); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Allocates an indexed array of populated `ReflectionNamedType` objects. +fn emit_reflection_named_type_array( + ctx: &mut FunctionContext<'_>, + types: &[ReflectionNamedTypeMetadata], +) -> Result<()> { + emit_reflection_indexed_array(ctx, types.len().max(1), 8); + crate::codegen::emit_array_value_type_stamp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &PhpType::Object("ReflectionNamedType".to_string()), + ); + for type_metadata in types { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_named_type_object(ctx, type_metadata)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_append_reflection_member_object(ctx); + } + Ok(()) +} + /// Allocates and populates one `ReflectionNamedType` object. fn emit_reflection_named_type_object( ctx: &mut FunctionContext<'_>, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 9ffa3ad78c..4e4fb1ce1f 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -852,9 +852,7 @@ fn lower_builtin_reflection_methods( "ReflectionNamedType", "ReflectionParameter", "ReflectionProperty", - "ReflectionFunction", - "ReflectionParameter", - "ReflectionNamedType", + "ReflectionUnionType", ] { lower_builtin_reflection_class_methods(class_name, module, check_result, constants, fiber_return_sigs); } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 456a60e68b..39eb80f31d 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -40,6 +40,7 @@ pub(crate) fn inject_builtin_reflection( "ReflectionProperty", "ReflectionParameter", "ReflectionNamedType", + "ReflectionUnionType", "ReflectionClassConstant", "ReflectionEnumUnitCase", "ReflectionEnumBackedCase", @@ -130,6 +131,10 @@ pub(crate) fn inject_builtin_reflection( builtin_reflection_parameter(), ); class_map.insert("ReflectionNamedType".to_string(), builtin_reflection_named_type()); + class_map.insert( + "ReflectionUnionType".to_string(), + builtin_reflection_union_type(), + ); for class_name in [ "ReflectionClassConstant", "ReflectionEnumUnitCase", @@ -698,6 +703,45 @@ fn builtin_reflection_named_type() -> FlattenedClass { } } +/// Builds the `ReflectionUnionType` shell returned for supported union hints. +fn builtin_reflection_union_type() -> FlattenedClass { + FlattenedClass { + name: "ReflectionUnionType".to_string(), + extends: None, + implements: Vec::new(), + is_abstract: false, + is_final: true, + is_readonly_class: false, + properties: vec![ + builtin_property( + "__types", + Visibility::Private, + Some(object_array_type("ReflectionNamedType")), + empty_array(), + ), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), + builtin_property( + "__allows_null", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + ], + methods: vec![ + builtin_reflection_private_constructor_method(), + builtin_reflection_class_array_method( + "getTypes", + "__types", + object_array_type("ReflectionNamedType"), + ), + builtin_reflection_class_bool_method("allowsNull", "__allows_null"), + ], + attributes: Vec::new(), + constants: Vec::new(), + used_traits: Vec::new(), + } +} + /// Builds the `ReflectionClass` shell with a private resolved-name slot, /// private attribute array slot, public constructor, `getName()`, and /// `getAttributes()`. @@ -1881,6 +1925,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionProperty", "ReflectionParameter", "ReflectionNamedType", + "ReflectionUnionType", "ReflectionClassConstant", "ReflectionEnumUnitCase", "ReflectionEnumBackedCase", @@ -1897,6 +1942,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { | "ReflectionProperty" | "ReflectionParameter" | "ReflectionNamedType" + | "ReflectionUnionType" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" @@ -2049,6 +2095,16 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } } + if class_name == "ReflectionUnionType" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTypes")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionNamedType".to_string(), + ))); + } + if let Some(sig) = class_info.methods.get_mut("allowsnull") { + sig.return_type = PhpType::Bool; + } + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionAttribute".to_string()))); @@ -2067,9 +2123,10 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } if let Some(class_info) = checker.classes.get_mut("ReflectionParameter") { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getType")) { - // ?ReflectionNamedType — null for untyped parameters. + // ReflectionNamedType|ReflectionUnionType|null for supported parameter types. sig.return_type = PhpType::Union(vec![ PhpType::Object("ReflectionNamedType".to_string()), + PhpType::Object("ReflectionUnionType".to_string()), PhpType::Void, ]); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cee97f861f..9d839d37f0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6404,7 +6404,7 @@ fn test_eval_reflection_method_lists_parameters() { let out = compile_and_run_capture( r#"getNumberOfParameters() . "/"; @@ -6417,7 +6417,14 @@ foreach ($params as $param) { echo $param->isPassedByReference() ? "R" : "b"; echo $param->hasType() ? "T" : "t"; $type = $param->getType(); - if ($type) { + if ($param->getName() == "union") { + echo ":union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { echo ":" . $type->getName(); echo $type->allowsNull() ? "?" : "!"; echo $type->isBuiltin() ? "B" : "C"; @@ -6440,7 +6447,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "3/1:first@0rvbT:int!B:d|second@1OvbT:App\\Name?C:D=null|rest@2OVbt:null:d|" + "4/2:first@0rvbT:int!B:d|union@1rvbT:union!:intB:stringB:d|second@2OvbT:App\\Name?C:D=null|rest@3OVbt:null:d|" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 85108b69c7..9d39acc95a 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1929,14 +1929,14 @@ echo ($traitParams[2]->hasType() ? "T" : "t"); ); } -/// Verifies that `ReflectionParameter::getType()` returns simple named type metadata. +/// Verifies that `ReflectionParameter::getType()` returns named and union type metadata. #[test] fn test_reflection_parameter_get_type_returns_named_type_metadata() { let out = compile_and_run_capture( r#"getParameters(); foreach ($params as $param) { @@ -1947,6 +1947,13 @@ foreach ($params as $param) { echo $type->getName(); echo $type->allowsNull() ? "?" : "!"; echo $type->isBuiltin() ? "B" : "C"; + } elseif ($type instanceof ReflectionUnionType) { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } } else { echo "null"; } @@ -1966,7 +1973,7 @@ if ($directType instanceof ReflectionNamedType) { ); assert_eq!( out.stdout, - "id:T:int!B|name:T:string?B|dep:T:ReflectParamTypeDep!C|plain:t:null|union:T:null|direct:ReflectParamTypeDep" + "id:T:int!B|name:T:string?B|dep:T:ReflectParamTypeDep!C|plain:t:null|union:T:union!:intB:stringB|nullableUnion:T:union?:intB:stringB|direct:ReflectParamTypeDep" ); } From 695c6d50db2490741c5390e94dfe9e9161325116 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 12:16:05 +0200 Subject: [PATCH 0412/1208] Add ReflectionIntersectionType parameter metadata --- crates/elephc-eval/src/eval_ir.rs | 23 +++ .../src/interpreter/dynamic_functions.rs | 11 ++ .../elephc-eval/src/interpreter/reflection.rs | 54 +++++++ .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 15 +- .../interpreter/tests/support/object_ops.rs | 6 + crates/elephc-eval/src/parser/statements.rs | 25 ++++ .../src/parser/tests/classes_errors.rs | 32 +++- docs/php/classes.md | 11 +- docs/php/eval.md | 9 +- src/autoload/mod.rs | 1 + src/codegen/eval_reflection_owner_helpers.rs | 29 +++- src/codegen/fibers.rs | 3 + src/codegen/lower_inst.rs | 2 + src/codegen/lower_inst/objects.rs | 2 + src/codegen/lower_inst/objects/reflection.rs | 137 +++++++++++++++++- src/codegen_support/callable_descriptor.rs | 2 + src/ir_lower/expr/mod.rs | 1 + src/ir_lower/fibers.rs | 7 + src/ir_lower/function.rs | 6 + src/ir_lower/program.rs | 1 + src/ir_lower/tests/exhaustive.rs | 2 + src/types/call_args/plan.rs | 1 + .../checker/builtin_spl_classes/patch.rs | 1 + src/types/checker/builtin_types/reflection.rs | 60 +++++++- .../callables/preg_replace_callback.rs | 4 + src/types/checker/callables/closures.rs | 6 + src/types/checker/callables/first_class.rs | 1 + src/types/checker/driver/externs.rs | 1 + src/types/checker/driver/functions.rs | 6 + src/types/checker/driver/mod.rs | 6 + src/types/checker/functions/resolution/mod.rs | 8 +- .../checker/functions/resolution/signature.rs | 12 ++ src/types/checker/schema/enums.rs | 2 + src/types/checker/schema/validation.rs | 6 + src/types/signatures.rs | 9 +- tests/codegen/eval.rs | 15 +- tests/codegen/oop/attributes.rs | 15 +- 38 files changed, 501 insertions(+), 32 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index a600a2b09d..b717b5513d 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -191,11 +191,19 @@ pub enum EvalParameterTypeVariant { String, } +/// How multiple eval parameter type atoms combine. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalParameterTypeKind { + Union, + Intersection, +} + /// Type metadata retained for one eval method parameter. #[derive(Debug, Clone, PartialEq, Eq)] pub struct EvalParameterType { variants: Vec, allows_null: bool, + kind: EvalParameterTypeKind, } impl EvalParameterType { @@ -204,6 +212,16 @@ impl EvalParameterType { Self { variants, allows_null, + kind: EvalParameterTypeKind::Union, + } + } + + /// Creates one eval method parameter type from intersection variants. + pub fn intersection(variants: Vec) -> Self { + Self { + variants, + allows_null: false, + kind: EvalParameterTypeKind::Intersection, } } @@ -216,6 +234,11 @@ impl EvalParameterType { pub const fn allows_null(&self) -> bool { self.allows_null } + + /// Returns whether all variants must match the value. + pub const fn is_intersection(&self) -> bool { + matches!(self.kind, EvalParameterTypeKind::Intersection) + } } /// Literal attribute argument metadata retained by eval declarations. diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 5e0eca5a73..804e51de3f 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -509,6 +509,9 @@ fn eval_method_parameter_value( if eval_method_parameter_type_accepts_exact(param_type, value, context, values)? { return Ok(value); } + if param_type.is_intersection() { + return Err(EvalStatus::RuntimeFatal); + } for variant in param_type.variants() { if let Some(coerced) = eval_method_parameter_scalar_coercion(variant, value, values)? { return Ok(coerced); @@ -528,6 +531,14 @@ fn eval_method_parameter_type_accepts_exact( if tag == EVAL_TAG_NULL && param_type.allows_null() { return Ok(true); } + if param_type.is_intersection() { + for variant in param_type.variants() { + if !eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { + return Ok(false); + } + } + return Ok(true); + } for variant in param_type.variants() { if eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { return Ok(true); diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index f95ff2d09b..467ba2c61f 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -78,6 +78,7 @@ struct EvalReflectionParameterTypeMetadata { enum EvalReflectionParameterTypeKind { Named(EvalReflectionNamedTypeMetadata), Union(EvalReflectionUnionTypeMetadata), + Intersection(EvalReflectionIntersectionTypeMetadata), } /// Eval metadata needed to materialize one `ReflectionNamedType` object. @@ -93,6 +94,11 @@ struct EvalReflectionUnionTypeMetadata { allows_null: bool, } +/// Eval metadata needed to materialize one `ReflectionIntersectionType` object. +struct EvalReflectionIntersectionTypeMetadata { + types: Vec, +} + /// Attempts to construct a ReflectionClass/Method/Property object for eval metadata. pub(in crate::interpreter) fn eval_reflection_owner_new_object( class_name: &str, @@ -856,6 +862,9 @@ fn eval_reflection_type_object_result( EvalReflectionParameterTypeKind::Union(union_type) => { eval_reflection_union_type_object_result(union_type, values) } + EvalReflectionParameterTypeKind::Intersection(intersection_type) => { + eval_reflection_intersection_type_object_result(intersection_type, values) + } } } @@ -937,6 +946,44 @@ fn eval_reflection_union_type_object_result( Ok(object) } +/// Materializes one ReflectionIntersectionType object through the shared reflection helper. +fn eval_reflection_intersection_type_object_result( + type_metadata: &EvalReflectionIntersectionTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_INTERSECTION_TYPE, + "", + attrs, + interface_names, + trait_names, + method_names, + property_names, + types, + property_objects, + parent_class, + 0, + 0, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(types)?; + values.release(property_objects)?; + values.release(parent_class)?; + Ok(object) +} + /// Builds an indexed array of populated ReflectionNamedType objects. fn eval_reflection_named_type_object_array_result( types: &[EvalReflectionNamedTypeMetadata], @@ -1427,6 +1474,13 @@ fn eval_reflection_parameter_type_metadata( kind: EvalReflectionParameterTypeKind::Named(named), }); } + if parameter_type.is_intersection() { + return Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Intersection( + EvalReflectionIntersectionTypeMetadata { types }, + ), + }); + } Some(EvalReflectionParameterTypeMetadata { kind: EvalReflectionParameterTypeKind::Union(EvalReflectionUnionTypeMetadata { types, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 040fe87b94..b260732240 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -368,3 +368,4 @@ pub(super) const EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE: u64 = 5; pub(super) const EVAL_REFLECTION_OWNER_PARAMETER: u64 = 6; pub(super) const EVAL_REFLECTION_OWNER_NAMED_TYPE: u64 = 7; pub(super) const EVAL_REFLECTION_OWNER_UNION_TYPE: u64 = 8; +pub(super) const EVAL_REFLECTION_OWNER_INTERSECTION_TYPE: u64 = 9; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c9c20bb5f9..488b6508ab 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -727,8 +727,10 @@ return true;"#, #[test] fn execute_program_reflects_eval_method_parameters() { let program = parse_fragment( -br##"class EvalReflectParamTarget { - public function run(int &$first, int|string $union, \App\Name|null $second = null, &...$rest) {} +br##"interface EvalReflectLeft {} +interface EvalReflectRight {} +class EvalReflectParamTarget { + public function run(int &$first, int|string $union, EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, &...$rest) {} } $method = new ReflectionMethod("EvalReflectParamTarget", "run"); echo $method->getNumberOfParameters(); echo "/"; @@ -748,6 +750,13 @@ foreach ($params as $param) { echo ":"; echo $memberType->getName(); echo $memberType->isBuiltin() ? "B" : "C"; } + } elseif ($param->getName() == "both") { + echo ":intersection"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } } elseif ($type) { echo ":"; echo $type->getName(); echo $type->allowsNull() ? "?" : "!"; @@ -772,7 +781,7 @@ return true;"##, assert_eq!( values.output, - "4/2:first#0rvRT:int!B:d|union#1rvbT:union!:intB:stringB:d|second#2OvbT:App\\Name?C:D=null|rest#3OVRt:null:d|" + "5/3:first#0rvRT:int!B:d|union#1rvbT:union!:intB:stringB:d|both#2rvbT:intersection!:EvalReflectLeftC:EvalReflectRightC:d|second#3OvbT:App\\Name?C:D=null|rest#4OVRt:null:d|" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index a0799d1742..4d90432ffa 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -456,6 +456,7 @@ impl FakeOps { EVAL_REFLECTION_OWNER_PARAMETER => "ReflectionParameter", EVAL_REFLECTION_OWNER_NAMED_TYPE => "ReflectionNamedType", EVAL_REFLECTION_OWNER_UNION_TYPE => "ReflectionUnionType", + EVAL_REFLECTION_OWNER_INTERSECTION_TYPE => "ReflectionIntersectionType", _ => return Err(EvalStatus::RuntimeFatal), }; let name = self.string(reflected_name)?; @@ -556,6 +557,11 @@ impl FakeOps { properties.push(("__types".to_string(), method_objects)); properties.push(("__allows_null".to_string(), allows_null)); } + if owner_kind == EVAL_REFLECTION_OWNER_INTERSECTION_TYPE { + let allows_null = self.bool_value(false)?; + properties.push(("__types".to_string(), method_objects)); + properties.push(("__allows_null".to_string(), allows_null)); + } let object = self.alloc(FakeValue::Object(properties)); self.object_classes .insert(object.as_ptr() as usize, class_name.to_string()); diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 597efae55e..5766a3d3ba 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -1793,6 +1793,23 @@ impl Parser { if nullable_shorthand && matches!(self.current(), TokenKind::Pipe) { return Err(EvalParseError::UnsupportedConstruct); } + if nullable_shorthand + && matches!(self.current(), TokenKind::Ampersand) + && !self.next_token_starts_parameter_storage() + { + return Err(EvalParseError::UnsupportedConstruct); + } + if matches!(self.current(), TokenKind::Ampersand) + && !self.next_token_starts_parameter_storage() + { + while self.consume(TokenKind::Ampersand) { + let Some(variant) = self.parse_parameter_type_name()? else { + return Err(EvalParseError::UnsupportedConstruct); + }; + variants.push(variant); + } + return Ok(Some(EvalParameterType::intersection(variants))); + } while self.consume(TokenKind::Pipe) { match self.parse_parameter_type_name()? { Some(variant) => variants.push(variant), @@ -1802,6 +1819,14 @@ impl Parser { Ok(Some(EvalParameterType::new(variants, allows_null))) } + /// Returns whether `&` belongs to by-reference parameter storage. + fn next_token_starts_parameter_storage(&self) -> bool { + matches!( + self.peek(), + TokenKind::DollarIdent(_) | TokenKind::Ellipsis + ) + } + /// Consumes one simple qualified method parameter type name. fn parse_parameter_type_name( &mut self, diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 83176b0f86..25447e25ef 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -565,7 +565,7 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { fn parse_fragment_accepts_typed_method_parameter_metadata() { let program = parse_fragment( br#"class DynEvalTypedParams { - public function run(int &$id = 7, ?\App\Name &$name = null, string|null $label = "x", mixed &...$tail) {} + public function run(Left&Right $both, int &$id = 7, ?\App\Name &$name = null, string|null $label = "x", mixed &...$tail) {} }"#, ) .expect("fragment should parse"); @@ -578,32 +578,47 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { assert_eq!( method.params(), &[ + "both".to_string(), "id".to_string(), "name".to_string(), "label".to_string(), "tail".to_string() ] ); - assert_eq!(method.parameter_has_types(), &[true, true, true, true]); + assert_eq!(method.parameter_has_types(), &[true, true, true, true, true]); assert!(method.parameter_types().iter().all(Option::is_some)); - let id_type = method.parameter_types()[0].as_ref().expect("id type"); + let both_type = method.parameter_types()[0].as_ref().expect("both type"); + assert_eq!( + both_type.variants(), + &[ + EvalParameterTypeVariant::Class("Left".to_string()), + EvalParameterTypeVariant::Class("Right".to_string()) + ] + ); + assert!(!both_type.allows_null()); + assert!(both_type.is_intersection()); + let id_type = method.parameter_types()[1].as_ref().expect("id type"); assert_eq!(id_type.variants(), &[EvalParameterTypeVariant::Int]); assert!(!id_type.allows_null()); - let name_type = method.parameter_types()[1].as_ref().expect("name type"); + assert!(!id_type.is_intersection()); + let name_type = method.parameter_types()[2].as_ref().expect("name type"); assert_eq!( name_type.variants(), &[EvalParameterTypeVariant::Class("App\\Name".to_string())] ); assert!(name_type.allows_null()); - let label_type = method.parameter_types()[2].as_ref().expect("label type"); + assert!(!name_type.is_intersection()); + let label_type = method.parameter_types()[3].as_ref().expect("label type"); assert_eq!( label_type.variants(), &[EvalParameterTypeVariant::String] ); assert!(label_type.allows_null()); + assert!(!label_type.is_intersection()); assert!(matches!( method.parameter_defaults(), [ + None, Some(EvalExpr::Const(EvalConst::Int(7))), Some(EvalExpr::Const(EvalConst::Null)), Some(EvalExpr::Const(EvalConst::String(label))), @@ -612,9 +627,12 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { )); assert_eq!( method.parameter_is_by_ref(), - &[true, true, false, true] + &[false, true, true, false, true] + ); + assert_eq!( + method.parameter_is_variadic(), + &[false, false, false, false, true] ); - assert_eq!(method.parameter_is_variadic(), &[false, false, false, true]); } /// Verifies eval method parameter defaults retain supported constant-expression metadata. diff --git a/docs/php/classes.md b/docs/php/classes.md index ddfe0c4e7d..b17a24b3c4 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1195,11 +1195,12 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | | `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | | `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | -| `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | +| `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, a `ReflectionIntersectionType` for intersection parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | | `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null default values, or throw `ReflectionException` when no default is available | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | | `ReflectionUnionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return non-null union members as `ReflectionNamedType` objects and report whether `null` was part of the union | +| `ReflectionIntersectionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return intersection members as `ReflectionNamedType` objects and report `false` for nullability | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | @@ -1253,7 +1254,7 @@ name with `isBuiltin()` false. | `ReflectionParameter::isOptional()` | `bool` | True for a parameter with a default or variadic, and any after it | | `ReflectionParameter::isVariadic()` | `bool` | True for the `...$rest` parameter | | `ReflectionParameter::hasType()` | `bool` | True when the parameter declares a type | -| `ReflectionParameter::getType()` | `?ReflectionNamedType\|ReflectionUnionType` | The declared type, or `null` when untyped | +| `ReflectionParameter::getType()` | `?ReflectionNamedType\|ReflectionUnionType\|ReflectionIntersectionType` | The declared type, or `null` when untyped | | `ReflectionParameter::isDefaultValueAvailable()` | `bool` | True when a supported default value is available | | `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar or null default value, or throws when unavailable | | `ReflectionNamedType::getName()` | `string` | The type name (`int`, `string`, a class name, …) | @@ -1261,6 +1262,8 @@ name with `isBuiltin()` false. | `ReflectionNamedType::allowsNull()` | `bool` | True when the declared type is nullable | | `ReflectionUnionType::getTypes()` | `ReflectionNamedType[]` | The non-null named members of a supported union | | `ReflectionUnionType::allowsNull()` | `bool` | True when the union includes `null` | +| `ReflectionIntersectionType::getTypes()` | `ReflectionNamedType[]` | The named members of a supported intersection | +| `ReflectionIntersectionType::allowsNull()` | `bool` | Always false for supported intersections | Limitations today: - All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Function/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name→id lookup table that is not yet implemented. @@ -1268,7 +1271,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named and union parameter types, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` supports `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as intersection parameter type objects, array/object/class-constant parameter default values, parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as array/object/class-constant parameter default values, parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1296,4 +1299,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named and union type metadata, and scalar/null parameter defaults are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType`/`ReflectionUnionType` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, and scalar/null parameter defaults are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType`/`ReflectionUnionType`/`ReflectionIntersectionType` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). diff --git a/docs/php/eval.md b/docs/php/eval.md index d5d6079810..7ede12d84d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -203,7 +203,8 @@ declared-type presence, simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, `allowsNull()`, and `isBuiltin()`, and multi-member union metadata through `ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter -type objects are not yet materialized. Defaulted eval method parameters are +metadata is exposed through `ReflectionIntersectionType::getTypes()` and +`allowsNull()`. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and `getDefaultValue()`. Supported default expressions include @@ -386,9 +387,9 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported -ReflectionClass/Method/Parameter/Property/NamedType/UnionType/attribute slice, -ReflectionUnionType APIs beyond parameter metadata, intersection Reflection type -objects, broader parameter default-value objects beyond the eval-supported +ReflectionClass/Method/Parameter/Property/NamedType/UnionType/IntersectionType +and attribute slice, Reflection type APIs beyond parameter metadata, broader +parameter default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index 9269fe9437..7f0b001970 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -83,6 +83,7 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "ReflectionParameter", "ReflectionProperty", "ReflectionUnionType", + "ReflectionIntersectionType", "RuntimeException", "SeekableIterator", "SortDirection", diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 9650ecf6ac..d545b6bcfe 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1,7 +1,7 @@ //! Purpose: //! Emits user-assembly helpers that let libelephc-eval materialize //! ReflectionClass, ReflectionMethod, ReflectionParameter, ReflectionProperty, -//! ReflectionClassConstant, ReflectionEnum*, and ReflectionNamedType objects +//! ReflectionClassConstant, ReflectionEnum*, and ReflectionType objects //! with private metadata slots populated from runtime eval declarations. //! //! Called from: @@ -107,6 +107,7 @@ struct ReflectionOwnerLayouts { parameter: ReflectionOwnerLayout, named_type: ReflectionOwnerLayout, union_type: ReflectionOwnerLayout, + intersection_type: ReflectionOwnerLayout, } /// Emits eval Reflection owner helpers when any lowered function owns an eval context. @@ -176,6 +177,10 @@ fn reflection_owner_layouts(module: &Module) -> Option { parameter: reflection_owner_layout(module.class_infos.get("ReflectionParameter")?, true)?, named_type: reflection_owner_layout(module.class_infos.get("ReflectionNamedType")?, true)?, union_type: reflection_owner_layout(module.class_infos.get("ReflectionUnionType")?, false)?, + intersection_type: reflection_owner_layout( + module.class_infos.get("ReflectionIntersectionType")?, + false, + )?, }) } @@ -332,6 +337,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection let parameter_label = "__elephc_eval_reflection_owner_new_parameter"; let named_type_label = "__elephc_eval_reflection_owner_new_named_type"; let union_type_label = "__elephc_eval_reflection_owner_new_union_type"; + let intersection_type_label = "__elephc_eval_reflection_owner_new_intersection_type"; emitter.instruction("sub sp, sp, #160"); // reserve helper frame for inputs, object, arrays, scratch, and fp/lr emitter.instruction("stp x29, x30, [sp, #144]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #144"); // establish a stable helper frame pointer @@ -371,6 +377,8 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction(&format!("b.eq {}", named_type_label)); // allocate a ReflectionNamedType owner emitter.instruction("cmp x0, #8"); // owner kind 8 means ReflectionUnionType emitter.instruction(&format!("b.eq {}", union_type_label)); // allocate a ReflectionUnionType owner + emitter.instruction("cmp x0, #9"); // owner kind 9 means ReflectionIntersectionType + emitter.instruction(&format!("b.eq {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body( emitter, @@ -444,6 +452,14 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection fail_label, box_label, ); + emit_aarch64_owner_kind_body( + emitter, + intersection_type_label, + &layouts.intersection_type, + false, + fail_label, + box_label, + ); emitter.label(box_label); emitter.instruction("mov x0, #6"); // runtime tag 6 = object emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload @@ -472,6 +488,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let parameter_label = "__elephc_eval_reflection_owner_new_parameter_x"; let named_type_label = "__elephc_eval_reflection_owner_new_named_type_x"; let union_type_label = "__elephc_eval_reflection_owner_new_union_type_x"; + let intersection_type_label = "__elephc_eval_reflection_owner_new_intersection_type_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer emitter.instruction("sub rsp, 144"); // reserve slots for inputs, object, metadata arrays, and name parts @@ -513,6 +530,8 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction(&format!("je {}", named_type_label)); // allocate a ReflectionNamedType owner emitter.instruction("cmp rdi, 8"); // owner kind 8 means ReflectionUnionType emitter.instruction(&format!("je {}", union_type_label)); // allocate a ReflectionUnionType owner + emitter.instruction("cmp rdi, 9"); // owner kind 9 means ReflectionIntersectionType + emitter.instruction(&format!("je {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body( emitter, @@ -586,6 +605,14 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO fail_label, box_label, ); + emit_x86_64_owner_kind_body( + emitter, + intersection_type_label, + &layouts.intersection_type, + false, + fail_label, + box_label, + ); emitter.label(box_label); emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload emitter.instruction("xor esi, esi"); // object payloads do not use a high word diff --git a/src/codegen/fibers.rs b/src/codegen/fibers.rs index 6e5741c398..6ed0c5927f 100644 --- a/src/codegen/fibers.rs +++ b/src/codegen/fibers.rs @@ -156,6 +156,7 @@ fn fiber_wrapper_label(closure_name: &str) -> String { fn descriptor_invoker_placeholder_sig() -> FunctionSig { FunctionSig { params: Vec::new(), + param_type_exprs: Vec::new(), defaults: Vec::new(), return_type: PhpType::Mixed, declared_return: false, @@ -185,6 +186,7 @@ fn signature_from_closure(closure: &Function, visible_abi_param_count: usize) -> .take(visible_abi_param_count) .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), + param_type_exprs: vec![None; visible_abi_param_count], defaults: closure .params .iter() @@ -230,6 +232,7 @@ fn ensure_variadic_param_slot(signature: &mut FunctionSig) { signature.defaults.push(None); signature.ref_params.push(false); signature.declared_params.push(false); + signature.param_type_exprs.push(None); } /// Returns hidden argument types for closure captures stored in descriptor capture slots. diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 76899e2cb4..54f2f54f16 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -613,6 +613,7 @@ fn function_signature_from_eir_with_param_count( .take(param_count) .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), + param_type_exprs: vec![None; param_count], defaults: function .params .iter() @@ -658,6 +659,7 @@ fn ensure_variadic_param_slot(signature: &mut FunctionSig) { signature.defaults.push(None); signature.ref_params.push(false); signature.declared_params.push(false); + signature.param_type_exprs.push(None); } /// Lowers a concrete include-loaded function variant activation marker. diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 8253ea8285..416ea6961e 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1385,6 +1385,7 @@ fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionParameter", "ReflectionProperty", "ReflectionUnionType", + "ReflectionIntersectionType", "RuntimeException", "SplDoublyLinkedList", "SplFixedArray", @@ -1456,6 +1457,7 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "ReflectionParameter", "ReflectionProperty", "ReflectionUnionType", + "ReflectionIntersectionType", "RegexIterator", "RuntimeException", "SplDoublyLinkedList", diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 656f296ccf..bbc3040809 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -22,7 +22,7 @@ use crate::codegen::{ }; use crate::ir::{Immediate, Instruction, Op, TraitMethodInfo, ValueDef, ValueId}; use crate::names::{enum_case_symbol, php_symbol_key}; -use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, Visibility}; +use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, TypeExpr, Visibility}; use crate::types::{AttrArgEntry, FunctionSig, InterfaceInfo, PhpType}; use super::super::super::context::FunctionContext; @@ -85,6 +85,7 @@ struct ReflectionParameterMember { enum ReflectionParameterTypeMetadata { Named(ReflectionNamedTypeMetadata), Union(ReflectionUnionTypeMetadata), + Intersection(ReflectionIntersectionTypeMetadata), } /// Metadata for one `ReflectionNamedType` returned by `ReflectionParameter::getType()`. @@ -102,6 +103,12 @@ struct ReflectionUnionTypeMetadata { allows_null: bool, } +/// Metadata for one `ReflectionIntersectionType` returned by `ReflectionParameter::getType()`. +#[derive(Clone)] +struct ReflectionIntersectionTypeMetadata { + types: Vec, +} + /// Compile-time default forms returned by `ReflectionParameter::getDefaultValue()`. #[derive(Clone)] enum ReflectionParameterDefaultValue { @@ -1305,7 +1312,7 @@ fn reflection_trait_constant_reflection_members( fn push_unique_constant_reflection_member( name: &str, attr_names: Vec, - attr_args: Vec>>, + attr_args: Vec>>, members: &mut Vec, seen: &mut std::collections::HashSet, ) { @@ -1642,7 +1649,11 @@ fn reflection_parameter_members(sig: &FunctionSig) -> Vec Option Option { +fn reflection_parameter_type_metadata( + type_expr: Option<&TypeExpr>, + ty: &PhpType, +) -> Option { + if let Some(TypeExpr::Intersection(members)) = type_expr { + return reflection_intersection_type_metadata(members); + } match ty { PhpType::Union(members) => reflection_union_or_nullable_type_metadata(members), _ => reflection_named_type_metadata(ty).map(ReflectionParameterTypeMetadata::Named), @@ -1734,6 +1751,47 @@ fn reflection_union_or_nullable_type_metadata( )) } +/// Converts a declared `A&B` type into `ReflectionIntersectionType` metadata. +fn reflection_intersection_type_metadata( + members: &[TypeExpr], +) -> Option { + let types = members + .iter() + .map(reflection_named_type_metadata_from_type_expr) + .collect::>>()?; + (!types.is_empty()).then_some(ReflectionParameterTypeMetadata::Intersection( + ReflectionIntersectionTypeMetadata { types }, + )) +} + +/// Converts one declared type atom into `ReflectionNamedType` metadata. +fn reflection_named_type_metadata_from_type_expr( + type_expr: &TypeExpr, +) -> Option { + match type_expr { + TypeExpr::Int => Some(reflection_builtin_named_type("int", false)), + TypeExpr::Float => Some(reflection_builtin_named_type("float", false)), + TypeExpr::Bool => Some(reflection_builtin_named_type("bool", false)), + TypeExpr::Str => Some(reflection_builtin_named_type("string", false)), + TypeExpr::Iterable => Some(reflection_builtin_named_type("iterable", false)), + TypeExpr::Array(_) => Some(reflection_builtin_named_type("array", false)), + TypeExpr::Named(name) => { + let raw_name = name.as_str().trim_start_matches('\\'); + match raw_name.to_ascii_lowercase().as_str() { + "array" | "callable" | "mixed" | "object" => { + Some(reflection_builtin_named_type(raw_name, false)) + } + _ => Some(ReflectionNamedTypeMetadata { + name: raw_name.to_string(), + allows_null: false, + is_builtin: false, + }), + } + } + _ => None, + } +} + /// Returns the `__construct` member object metadata when the reflected class-like symbol has one. fn reflection_constructor_member( method_members: &[ReflectionListedMember], @@ -2853,6 +2911,13 @@ fn emit_reflection_parameter_type_property( &PhpType::Object("ReflectionUnionType".to_string()), ); } + Some(ReflectionParameterTypeMetadata::Intersection(type_metadata)) => { + emit_reflection_intersection_type_object(ctx, type_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionIntersectionType".to_string()), + ); + } None => emit_boxed_null_literal_to_result(ctx), } abi::emit_pop_reg(ctx.emitter, object_reg); @@ -2964,6 +3029,70 @@ fn emit_reflection_union_type_types_property( Ok(()) } +/// Allocates and populates one `ReflectionIntersectionType` object. +fn emit_reflection_intersection_type_object( + ctx: &mut FunctionContext<'_>, + type_metadata: &ReflectionIntersectionTypeMetadata, +) -> Result<()> { + let (class_id, property_count, uninitialized_marker_offsets) = { + let class_info = ctx + .module + .class_infos + .get("ReflectionIntersectionType") + .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionIntersectionType"))?; + ( + class_info.class_id, + class_info.properties.len(), + super::uninitialized_property_marker_offsets(class_info), + ) + }; + super::emit_object_allocation( + ctx, + class_id, + property_count, + false, + &uninitialized_marker_offsets, + )?; + emit_reflection_intersection_type_types_property(ctx, &type_metadata.types)?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionIntersectionType", + "__allows_null", + false, + )?; + Ok(()) +} + +/// Writes the `ReflectionIntersectionType::__types` array of `ReflectionNamedType` objects. +fn emit_reflection_intersection_type_types_property( + ctx: &mut FunctionContext<'_>, + types: &[ReflectionNamedTypeMetadata], +) -> Result<()> { + let types_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionIntersectionType") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__types")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + emit_reflection_named_type_array(ctx, types)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, types_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + types_offset + 8, + ); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + /// Allocates an indexed array of populated `ReflectionNamedType` objects. fn emit_reflection_named_type_array( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen_support/callable_descriptor.rs b/src/codegen_support/callable_descriptor.rs index fd1725c4ff..5b311d8ae5 100644 --- a/src/codegen_support/callable_descriptor.rs +++ b/src/codegen_support/callable_descriptor.rs @@ -586,6 +586,7 @@ mod tests { ("label".to_string(), PhpType::Str), ("rest".to_string(), PhpType::Array(Box::new(PhpType::Mixed))), ], + param_type_exprs: vec![None, None, None], defaults: vec![ None, Some(Expr::new(ExprKind::StringLiteral("fallback".to_string()), Span::dummy())), @@ -634,6 +635,7 @@ mod tests { let mut data = DataSection::new(); let sig = FunctionSig { params: vec![("value".to_string(), PhpType::Int)], + param_type_exprs: vec![None], defaults: vec![None], return_type: PhpType::Int, declared_return: false, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 03edef2988..1dd9452802 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -2916,6 +2916,7 @@ fn signature_for_static_callable_binding( fn function_sig_from_extern_for_descriptor(sig: &ExternFunctionSig) -> FunctionSig { FunctionSig { params: sig.params.clone(), + param_type_exprs: vec![None; sig.params.len()], defaults: vec![None; sig.params.len()], return_type: sig.return_type.clone(), declared_return: true, diff --git a/src/ir_lower/fibers.rs b/src/ir_lower/fibers.rs index 4f5f2ce7df..6f0ee7390c 100644 --- a/src/ir_lower/fibers.rs +++ b/src/ir_lower/fibers.rs @@ -194,6 +194,7 @@ fn callback_sig_for_binding( StaticCallableBinding::ExternFunction(name) => { ctx.extern_functions.get(name.as_str()).map(|sig| FunctionSig { params: sig.params.clone(), + param_type_exprs: vec![None; sig.params.len()], defaults: vec![None; sig.params.len()], return_type: sig.return_type.clone(), declared_return: true, @@ -307,6 +308,10 @@ fn callback_sig_from_closure_params( ) }) .collect(), + param_type_exprs: params + .iter() + .map(|(_, type_ann, _, _)| type_ann.clone()) + .collect(), defaults: params .iter() .map(|(_, _, default, _)| default.clone()) @@ -323,6 +328,7 @@ fn callback_sig_from_closure_params( if !sig.params.iter().any(|(name, _)| name == variadic_name) { sig.params .push((variadic_name.to_string(), PhpType::Array(Box::new(PhpType::Mixed)))); + sig.param_type_exprs.push(None); sig.defaults.push(None); sig.ref_params.push(false); sig.declared_params.push(false); @@ -342,6 +348,7 @@ fn start_sig_from_callback_sig(sig: &FunctionSig) -> Option { .iter() .map(|(name, _)| (name.clone(), PhpType::Mixed)) .collect(), + param_type_exprs: sig.param_type_exprs.clone(), defaults: sig.defaults.clone(), return_type: PhpType::Mixed, declared_return: false, diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 7609f82061..d99b6f274e 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -385,6 +385,7 @@ pub(crate) fn lower_property_init_thunk( }); let sig = FunctionSig { params: vec![("this".to_string(), this_type.clone())], + param_type_exprs: vec![None], defaults: vec![None], return_type: PhpType::Void, declared_return: false, @@ -1178,6 +1179,10 @@ fn signature_from_ast_with_variadic( ) }) .collect(), + param_type_exprs: params + .iter() + .map(|(_, type_ann, _, _)| type_ann.clone()) + .collect(), defaults: params.iter().map(|(_, _, default, _)| default.clone()).collect(), return_type: return_type .map(type_expr_to_php_type) @@ -1204,6 +1209,7 @@ fn append_variadic_param_slot(signature: &mut FunctionSig) { signature .params .push((variadic, PhpType::Array(Box::new(PhpType::Mixed)))); + signature.param_type_exprs.push(None); signature.defaults.push(None); signature.ref_params.push(false); signature.declared_params.push(false); diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 4e4fb1ce1f..1bda882ba2 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -853,6 +853,7 @@ fn lower_builtin_reflection_methods( "ReflectionParameter", "ReflectionProperty", "ReflectionUnionType", + "ReflectionIntersectionType", ] { lower_builtin_reflection_class_methods(class_name, module, check_result, constants, fiber_return_sigs); } diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index f30a280ae4..8b5abe603d 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -73,6 +73,7 @@ fn dummy_check_result() -> CheckResult { "f".to_string(), FunctionSig { params: vec![("x".to_string(), PhpType::Int)], + param_type_exprs: vec![None], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -142,6 +143,7 @@ fn dummy_check_result() -> CheckResult { fn class_info(_class_name: &str) -> ClassInfo { let method_sig = FunctionSig { params: Vec::new(), + param_type_exprs: Vec::new(), defaults: Vec::new(), return_type: PhpType::Int, declared_return: true, diff --git a/src/types/call_args/plan.rs b/src/types/call_args/plan.rs index d068f7e7b9..8ec4a436c7 100644 --- a/src/types/call_args/plan.rs +++ b/src/types/call_args/plan.rs @@ -352,6 +352,7 @@ mod tests { .collect(); FunctionSig { params, + param_type_exprs: vec![None; 3], defaults, return_type: PhpType::Int, declared_return: true, diff --git a/src/types/checker/builtin_spl_classes/patch.rs b/src/types/checker/builtin_spl_classes/patch.rs index a98369524e..a10c4c82de 100644 --- a/src/types/checker/builtin_spl_classes/patch.rs +++ b/src/types/checker/builtin_spl_classes/patch.rs @@ -47,6 +47,7 @@ pub(super) fn patch_builtin_spl_storage_signatures(checker: &mut Checker) { "class".to_string(), PhpType::Union(vec![PhpType::Str, PhpType::Void]), )); + sig.param_type_exprs.push(None); sig.defaults.push(Some(null_expr())); sig.ref_params.push(false); sig.declared_params.push(true); diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 39eb80f31d..56fd6fe55e 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -41,6 +41,7 @@ pub(crate) fn inject_builtin_reflection( "ReflectionParameter", "ReflectionNamedType", "ReflectionUnionType", + "ReflectionIntersectionType", "ReflectionClassConstant", "ReflectionEnumUnitCase", "ReflectionEnumBackedCase", @@ -135,6 +136,10 @@ pub(crate) fn inject_builtin_reflection( "ReflectionUnionType".to_string(), builtin_reflection_union_type(), ); + class_map.insert( + "ReflectionIntersectionType".to_string(), + builtin_reflection_intersection_type(), + ); for class_name in [ "ReflectionClassConstant", "ReflectionEnumUnitCase", @@ -742,6 +747,45 @@ fn builtin_reflection_union_type() -> FlattenedClass { } } +/// Builds the `ReflectionIntersectionType` shell returned for supported intersection hints. +fn builtin_reflection_intersection_type() -> FlattenedClass { + FlattenedClass { + name: "ReflectionIntersectionType".to_string(), + extends: None, + implements: Vec::new(), + is_abstract: false, + is_final: true, + is_readonly_class: false, + properties: vec![ + builtin_property( + "__types", + Visibility::Private, + Some(object_array_type("ReflectionNamedType")), + empty_array(), + ), + builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), + builtin_property( + "__allows_null", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + ], + methods: vec![ + builtin_reflection_private_constructor_method(), + builtin_reflection_class_array_method( + "getTypes", + "__types", + object_array_type("ReflectionNamedType"), + ), + builtin_reflection_class_bool_method("allowsNull", "__allows_null"), + ], + attributes: Vec::new(), + constants: Vec::new(), + used_traits: Vec::new(), + } +} + /// Builds the `ReflectionClass` shell with a private resolved-name slot, /// private attribute array slot, public constructor, `getName()`, and /// `getAttributes()`. @@ -1926,6 +1970,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionParameter", "ReflectionNamedType", "ReflectionUnionType", + "ReflectionIntersectionType", "ReflectionClassConstant", "ReflectionEnumUnitCase", "ReflectionEnumBackedCase", @@ -1943,6 +1988,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { | "ReflectionParameter" | "ReflectionNamedType" | "ReflectionUnionType" + | "ReflectionIntersectionType" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" @@ -2025,6 +2071,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if !sig.params.iter().any(|(name, _)| name == "args") { sig.params .push(("args".to_string(), PhpType::Array(Box::new(PhpType::Mixed)))); + sig.param_type_exprs.push(None); sig.defaults.push(None); sig.ref_params.push(false); sig.declared_params.push(false); @@ -2105,6 +2152,16 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Bool; } } + if class_name == "ReflectionIntersectionType" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTypes")) { + sig.return_type = PhpType::Array(Box::new(PhpType::Object( + "ReflectionNamedType".to_string(), + ))); + } + if let Some(sig) = class_info.methods.get_mut("allowsnull") { + sig.return_type = PhpType::Bool; + } + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionAttribute".to_string()))); @@ -2123,10 +2180,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } if let Some(class_info) = checker.classes.get_mut("ReflectionParameter") { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getType")) { - // ReflectionNamedType|ReflectionUnionType|null for supported parameter types. + // ReflectionNamedType|ReflectionUnionType|ReflectionIntersectionType|null. sig.return_type = PhpType::Union(vec![ PhpType::Object("ReflectionNamedType".to_string()), PhpType::Object("ReflectionUnionType".to_string()), + PhpType::Object("ReflectionIntersectionType".to_string()), PhpType::Void, ]); } diff --git a/src/types/checker/builtins/callables/preg_replace_callback.rs b/src/types/checker/builtins/callables/preg_replace_callback.rs index 400909efcd..e9de04cefe 100644 --- a/src/types/checker/builtins/callables/preg_replace_callback.rs +++ b/src/types/checker/builtins/callables/preg_replace_callback.rs @@ -78,6 +78,7 @@ fn contextual_closure_sig( let mut closure_env = env.clone(); let mut param_types = Vec::new(); + let mut param_type_exprs = Vec::new(); let mut defaults = Vec::new(); let mut ref_params = Vec::new(); let mut declared_params = Vec::new(); @@ -119,6 +120,7 @@ fn contextual_closure_sig( closure_env.insert(name.clone(), env_ty); param_types.push((name.clone(), sig_ty)); + param_type_exprs.push(type_ann.clone()); defaults.push(default.clone()); ref_params.push(*is_ref); declared_params.push(declared); @@ -127,6 +129,7 @@ fn contextual_closure_sig( if let Some(name) = variadic { closure_env.insert(name.clone(), PhpType::Array(Box::new(PhpType::Int))); param_types.push((name.clone(), PhpType::Array(Box::new(PhpType::Mixed)))); + param_type_exprs.push(None); defaults.push(None); ref_params.push(false); declared_params.push(false); @@ -136,6 +139,7 @@ fn contextual_closure_sig( checker.resolve_closure_return_type(body, return_type, callback.span, &closure_env)?; Ok(Some(FunctionSig { params: param_types, + param_type_exprs, defaults, return_type, declared_return, diff --git a/src/types/checker/callables/closures.rs b/src/types/checker/callables/closures.rs index 6ddd323a24..eceb5cd222 100644 --- a/src/types/checker/callables/closures.rs +++ b/src/types/checker/callables/closures.rs @@ -22,6 +22,7 @@ use super::super::Checker; /// environment bindings, default value expressions, by-reference flags, and declared-param flags. pub(crate) struct ClosureSignatureContext { pub params: Vec<(String, PhpType)>, + pub param_type_exprs: Vec>, pub env: TypeEnv, pub defaults: Vec>, pub ref_params: Vec, @@ -76,6 +77,7 @@ impl Checker { let mut closure_env = env.clone(); let mut param_types = Vec::new(); + let mut param_type_exprs = Vec::new(); let mut defaults = Vec::new(); let mut ref_params = Vec::new(); let mut declared_params = Vec::new(); @@ -104,6 +106,7 @@ impl Checker { closure_env.insert(name.clone(), env_ty); param_types.push((name.clone(), sig_ty)); + param_type_exprs.push(type_ann.clone()); defaults.push(default.clone()); ref_params.push(*is_ref); declared_params.push(type_ann.is_some()); @@ -112,6 +115,7 @@ impl Checker { if let Some(name) = variadic { closure_env.insert(name.clone(), PhpType::Array(Box::new(PhpType::Int))); param_types.push((name.clone(), PhpType::Array(Box::new(PhpType::Mixed)))); + param_type_exprs.push(None); defaults.push(None); ref_params.push(false); declared_params.push(false); @@ -119,6 +123,7 @@ impl Checker { Ok(ClosureSignatureContext { params: param_types, + param_type_exprs, env: closure_env, defaults, ref_params, @@ -239,6 +244,7 @@ impl Checker { )?; Ok(Some(FunctionSig { params: closure_sig.params, + param_type_exprs: closure_sig.param_type_exprs, defaults: closure_sig.defaults, return_type, declared_return, diff --git a/src/types/checker/callables/first_class.rs b/src/types/checker/callables/first_class.rs index 1e7a8b4513..b6506f2bf8 100644 --- a/src/types/checker/callables/first_class.rs +++ b/src/types/checker/callables/first_class.rs @@ -53,6 +53,7 @@ impl Checker { if let Some(sig) = self.extern_functions.get(function_name) { return Ok(FunctionSig { params: sig.params.clone(), + param_type_exprs: vec![None; sig.params.len()], defaults: vec![None; sig.params.len()], return_type: sig.return_type.clone(), declared_return: true, diff --git a/src/types/checker/driver/externs.rs b/src/types/checker/driver/externs.rs index 070a957cde..e7c344874a 100644 --- a/src/types/checker/driver/externs.rs +++ b/src/types/checker/driver/externs.rs @@ -112,6 +112,7 @@ impl Checker { } let sig = FunctionSig { params: php_params.clone(), + param_type_exprs: vec![None; php_params.len()], defaults: params.iter().map(|_| None).collect(), return_type: php_ret.clone(), declared_return: true, diff --git a/src/types/checker/driver/functions.rs b/src/types/checker/driver/functions.rs index 620686c7ce..755c580788 100644 --- a/src/types/checker/driver/functions.rs +++ b/src/types/checker/driver/functions.rs @@ -270,6 +270,12 @@ impl Checker { let param_types = self.initial_function_param_types(first_variant, &decl)?; Ok(Some(FunctionSig { params: param_types, + param_type_exprs: decl + .param_types + .iter() + .cloned() + .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) + .collect(), defaults: decl.defaults, return_type: crate::types::PhpType::Int, declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index cd960667b1..b54431f6f6 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -399,6 +399,12 @@ fn trait_method_reflection_sig(method: &ClassMethod) -> FunctionSig { .collect(); callable_wrapper_sig(&FunctionSig { params, + param_type_exprs: method + .params + .iter() + .map(|(_, type_ann, _, _)| type_ann.clone()) + .chain(method.variadic.iter().map(|_| method.variadic_type.clone())) + .collect(), defaults, return_type: PhpType::Mixed, declared_return: method.return_type.is_some(), diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index 9f4f1a8998..c80b9818c1 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -190,7 +190,13 @@ impl Checker { .cloned() .map(|name| (name, PhpType::Array(Box::new(PhpType::Int)))), ) - .collect(), + .collect(), + param_type_exprs: decl + .param_types + .iter() + .cloned() + .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) + .collect(), defaults: decl.defaults.clone(), return_type: PhpType::Int, declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/functions/resolution/signature.rs b/src/types/checker/functions/resolution/signature.rs index 69b5dcc15d..0400d56e57 100644 --- a/src/types/checker/functions/resolution/signature.rs +++ b/src/types/checker/functions/resolution/signature.rs @@ -92,6 +92,12 @@ impl Checker { let provisional_sig = FunctionSig { params: param_types.clone(), + param_type_exprs: decl + .param_types + .iter() + .cloned() + .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) + .collect(), defaults: decl.defaults.clone(), return_type: PhpType::Int, declared_return: decl.return_type.is_some(), @@ -229,6 +235,12 @@ impl Checker { let sig = FunctionSig { params: param_types, + param_type_exprs: decl + .param_types + .iter() + .cloned() + .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) + .collect(), defaults: decl.defaults.clone(), return_type: return_type.clone(), declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index a3d8bd8fea..fea65bae7a 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -311,6 +311,7 @@ pub(crate) fn insert_enum_metadata( "cases".to_string(), FunctionSig { params: Vec::new(), + param_type_exprs: Vec::new(), defaults: Vec::new(), return_type: PhpType::Array(Box::new(PhpType::Object(name.to_string()))), declared_return: true, @@ -330,6 +331,7 @@ pub(crate) fn insert_enum_metadata( method_name.to_string(), FunctionSig { params: vec![("value".to_string(), backing_ty.clone())], + param_type_exprs: vec![None], defaults: vec![None], return_type: if method_name == "from" { PhpType::Object(name.to_string()) diff --git a/src/types/checker/schema/validation.rs b/src/types/checker/schema/validation.rs index bb459cd709..47e38200cb 100644 --- a/src/types/checker/schema/validation.rs +++ b/src/types/checker/schema/validation.rs @@ -80,6 +80,12 @@ pub(crate) fn build_method_sig( }; let mut sig = Checker::callable_wrapper_sig(&FunctionSig { params, + param_type_exprs: method + .params + .iter() + .map(|(_, type_ann, _, _)| type_ann.clone()) + .chain(method.variadic.iter().map(|_| method.variadic_type.clone())) + .collect(), defaults, return_type, declared_return: method.return_type.is_some(), diff --git a/src/types/signatures.rs b/src/types/signatures.rs index 6b64f9853f..b5ea00e10a 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -9,7 +9,7 @@ //! Key details: //! - Builtin signatures must match PHP so named arguments, first-class callables, and mutation semantics stay coherent. -use crate::parser::ast::{Expr, ExprKind}; +use crate::parser::ast::{Expr, ExprKind, TypeExpr}; use crate::span::Span; use super::PhpType; @@ -22,6 +22,7 @@ use super::PhpType; /// named arguments, callable aliases, and mutation semantics. pub struct FunctionSig { pub params: Vec<(String, PhpType)>, + pub param_type_exprs: Vec>, pub defaults: Vec>, pub return_type: PhpType, pub declared_return: bool, @@ -66,6 +67,7 @@ pub(crate) fn callable_wrapper_sig(sig: &FunctionSig) -> FunctionSig { wrapper_sig.defaults.push(None); wrapper_sig.ref_params.push(false); wrapper_sig.declared_params.push(false); + wrapper_sig.param_type_exprs.push(None); wrapper_sig } @@ -705,6 +707,7 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { match name { "strlen" => Some(FunctionSig { params: vec![("string".to_string(), PhpType::Str)], + param_type_exprs: vec![None], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -722,6 +725,7 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { value: Box::new(PhpType::Mixed), }, )], + param_type_exprs: vec![None], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -733,6 +737,7 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { }), "buffer_len" => Some(FunctionSig { params: vec![("buffer".to_string(), PhpType::Buffer(Box::new(PhpType::Int)))], + param_type_exprs: vec![None], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -1139,6 +1144,7 @@ fn make_sig(params: &[&str], defaults: Vec>, variadic: Option<&str> .iter() .map(|name| ((*name).to_string(), PhpType::Mixed)) .collect(), + param_type_exprs: vec![None; params.len()], defaults, return_type: PhpType::Mixed, declared_return: false, @@ -1178,6 +1184,7 @@ mod tests { fn variadic_sig(params: Vec<(String, PhpType)>) -> FunctionSig { FunctionSig { defaults: vec![None; params.len()], + param_type_exprs: vec![None; params.len()], return_type: PhpType::Mixed, declared_return: false, by_ref_return: false, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9d839d37f0..9f89b96c2a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6403,8 +6403,10 @@ try { fn test_eval_reflection_method_lists_parameters() { let out = compile_and_run_capture( r#"getNumberOfParameters() . "/"; @@ -6424,6 +6426,13 @@ foreach ($params as $param) { echo ":" . $memberType->getName(); echo $memberType->isBuiltin() ? "B" : "C"; } + } elseif ($param->getName() == "both") { + echo ":intersection"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } } elseif ($type) { echo ":" . $type->getName(); echo $type->allowsNull() ? "?" : "!"; @@ -6447,7 +6456,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "4/2:first@0rvbT:int!B:d|union@1rvbT:union!:intB:stringB:d|second@2OvbT:App\\Name?C:D=null|rest@3OVbt:null:d|" + "5/3:first@0rvbT:int!B:d|union@1rvbT:union!:intB:stringB:d|both@2rvbT:intersection!:EvalReflectLeftC:EvalReflectRightC:d|second@3OvbT:App\\Name?C:D=null|rest@4OVbt:null:d|" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 9d39acc95a..dc9ec40774 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1929,14 +1929,16 @@ echo ($traitParams[2]->hasType() ? "T" : "t"); ); } -/// Verifies that `ReflectionParameter::getType()` returns named and union type metadata. +/// Verifies that `ReflectionParameter::getType()` returns named, union, and intersection metadata. #[test] fn test_reflection_parameter_get_type_returns_named_type_metadata() { let out = compile_and_run_capture( r#"getParameters(); foreach ($params as $param) { @@ -1954,6 +1956,13 @@ foreach ($params as $param) { echo ":" . $memberType->getName(); echo $memberType->isBuiltin() ? "B" : "C"; } + } elseif ($type instanceof ReflectionIntersectionType) { + echo "intersection"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } } else { echo "null"; } @@ -1973,7 +1982,7 @@ if ($directType instanceof ReflectionNamedType) { ); assert_eq!( out.stdout, - "id:T:int!B|name:T:string?B|dep:T:ReflectParamTypeDep!C|plain:t:null|union:T:union!:intB:stringB|nullableUnion:T:union?:intB:stringB|direct:ReflectParamTypeDep" + "id:T:int!B|name:T:string?B|dep:T:ReflectParamTypeDep!C|plain:t:null|union:T:union!:intB:stringB|nullableUnion:T:union?:intB:stringB|intersection:T:intersection!:ReflectParamTypeAC:ReflectParamTypeBC|direct:ReflectParamTypeDep" ); } From ca6ac078f8a85aa0d007a551a5de5c92ddb6c915 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 12:50:00 +0200 Subject: [PATCH 0413/1208] Add ReflectionParameter attribute metadata --- crates/elephc-eval/src/eval_ir.rs | 34 +++++ .../elephc-eval/src/interpreter/reflection.rs | 11 +- .../tests/builtins_class_metadata.rs | 10 +- crates/elephc-eval/src/parser/statements.rs | 25 ++-- .../src/parser/tests/classes_errors.rs | 14 +- docs/internals/the-parser.md | 4 +- docs/php/classes.md | 8 +- docs/php/eval.md | 16 ++- src/codegen/fibers.rs | 2 + src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/objects/reflection.rs | 33 +++-- src/codegen_support/callable_descriptor.rs | 2 + src/ir_lower/expr/mod.rs | 1 + src/ir_lower/fibers.rs | 3 + src/ir_lower/function.rs | 2 + src/ir_lower/tests/exhaustive.rs | 3 + src/name_resolver/declarations.rs | 60 ++++----- src/optimize/fold/expr.rs | 51 ++++---- src/optimize/tests/effects/methods.rs | 6 + src/parser/ast/oop.rs | 12 +- src/parser/stmt/oop/body.rs | 83 +++++++----- src/parser/stmt/oop/method_params.rs | 37 ++++-- src/types/call_args/plan.rs | 1 + src/types/checker/builtin_interfaces.rs | 2 + src/types/checker/builtin_iterators.rs | 4 + src/types/checker/builtin_json.rs | 1 + .../checker/builtin_spl_classes/common.rs | 1 + src/types/checker/builtin_types/exception.rs | 3 + src/types/checker/builtin_types/fiber.rs | 8 ++ src/types/checker/builtin_types/reflection.rs | 23 ++++ .../callables/preg_replace_callback.rs | 1 + src/types/checker/callables/closures.rs | 1 + src/types/checker/callables/first_class.rs | 1 + src/types/checker/driver/externs.rs | 1 + src/types/checker/driver/functions.rs | 1 + src/types/checker/driver/mod.rs | 1 + src/types/checker/functions/resolution/mod.rs | 1 + .../checker/functions/resolution/signature.rs | 2 + src/types/checker/schema/enums.rs | 2 + src/types/checker/schema/validation.rs | 18 ++- src/types/signatures.rs | 15 ++- tests/codegen/eval.rs | 10 +- tests/codegen/oop/attributes.rs | 38 ++++++ tests/parser_tests/attributes.rs | 121 +++++++++--------- 44 files changed, 460 insertions(+), 214 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index b717b5513d..6fbb9dbfa0 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -593,6 +593,7 @@ pub struct EvalInterfaceMethod { attributes: Vec, is_static: bool, params: Vec, + parameter_attributes: Vec>, parameter_has_types: Vec, parameter_types: Vec>, parameter_defaults: Vec>, @@ -604,6 +605,7 @@ impl EvalInterfaceMethod { /// Creates one dynamic eval interface method signature. pub fn new(name: impl Into, params: Vec) -> Self { let parameter_has_types = vec![false; params.len()]; + let parameter_attributes = vec![Vec::new(); params.len()]; let parameter_types = vec![None; params.len()]; let parameter_defaults = vec![None; params.len()]; let parameter_is_by_ref = vec![false; params.len()]; @@ -613,6 +615,7 @@ impl EvalInterfaceMethod { attributes: Vec::new(), is_static: false, params, + parameter_attributes, parameter_has_types, parameter_types, parameter_defaults, @@ -639,6 +642,15 @@ impl EvalInterfaceMethod { self } + /// Returns a copy of this interface method with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + /// Returns a copy of this interface method with source-order parameter type metadata. pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); @@ -684,6 +696,11 @@ impl EvalInterfaceMethod { &self.params } + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + /// Returns source-order flags for whether each parameter declared a type. pub fn parameter_has_types(&self) -> &[bool] { &self.parameter_has_types @@ -1359,6 +1376,7 @@ pub struct EvalClassMethod { is_abstract: bool, is_final: bool, params: Vec, + parameter_attributes: Vec>, parameter_has_types: Vec, parameter_types: Vec>, parameter_defaults: Vec>, @@ -1403,6 +1421,7 @@ impl EvalClassMethod { body: Vec, ) -> Self { let parameter_has_types = vec![false; params.len()]; + let parameter_attributes = vec![Vec::new(); params.len()]; let parameter_types = vec![None; params.len()]; let parameter_defaults = vec![None; params.len()]; let parameter_is_by_ref = vec![false; params.len()]; @@ -1415,6 +1434,7 @@ impl EvalClassMethod { is_abstract, is_final, params, + parameter_attributes, parameter_has_types, parameter_types, parameter_defaults, @@ -1441,6 +1461,15 @@ impl EvalClassMethod { self } + /// Returns a copy of this method with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + /// Returns a copy of this method with source-order parameter type metadata. pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); @@ -1510,6 +1539,11 @@ impl EvalClassMethod { &self.params } + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + /// Returns source-order flags for whether each parameter declared a type. pub fn parameter_has_types(&self) -> &[bool] { &self.parameter_has_types diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 467ba2c61f..93fbecb720 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -60,6 +60,7 @@ struct EvalReflectionMemberMetadata { /// Eval metadata needed to materialize one `ReflectionParameter` object. struct EvalReflectionParameterMetadata { name: String, + attributes: Vec, position: usize, is_optional: bool, is_variadic: bool, @@ -808,7 +809,7 @@ fn eval_reflection_parameter_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let attrs = values.array_new(0)?; + let attrs = eval_reflection_attribute_array_result(¶meter.attributes, context, values)?; let interface_names = values.array_new(0)?; let trait_names = values.array_new(0)?; let method_names = values.array_new(0)?; @@ -1301,6 +1302,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_types(), + method.parameter_attributes(), method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), @@ -1326,6 +1328,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_types(), + method.parameter_attributes(), method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), @@ -1351,6 +1354,7 @@ fn eval_reflection_method_metadata( method.params(), method.parameter_has_types(), method.parameter_types(), + method.parameter_attributes(), method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), @@ -1429,6 +1433,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( names: &[String], has_type_flags: &[bool], parameter_types: &[Option], + parameter_attributes: &[Vec], defaults: &[Option], by_ref_flags: &[bool], variadic_flags: &[bool], @@ -1438,6 +1443,10 @@ fn eval_reflection_parameters_from_names_and_type_flags( .enumerate() .map(|(position, name)| EvalReflectionParameterMetadata { name: name.clone(), + attributes: parameter_attributes + .get(position) + .cloned() + .unwrap_or_default(), position, is_optional: defaults.get(position).is_some_and(Option::is_some) || variadic_flags.get(position).copied().unwrap_or(false), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 488b6508ab..0ffb9e2622 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -730,7 +730,7 @@ fn execute_program_reflects_eval_method_parameters() { br##"interface EvalReflectLeft {} interface EvalReflectRight {} class EvalReflectParamTarget { - public function run(int &$first, int|string $union, EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, &...$rest) {} + public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, &...$rest) {} } $method = new ReflectionMethod("EvalReflectParamTarget", "run"); echo $method->getNumberOfParameters(); echo "/"; @@ -764,6 +764,12 @@ foreach ($params as $param) { } else { echo ":null"; } + $attrs = $param->getAttributes(); + echo ":A"; echo count($attrs); + if (count($attrs) > 0) { + echo ":"; echo $attrs[0]->getName(); + echo ":"; echo $attrs[0]->getArguments()[0]; + } echo $param->isDefaultValueAvailable() ? ":D" : ":d"; if ($param->isDefaultValueAvailable()) { echo "="; @@ -781,7 +787,7 @@ return true;"##, assert_eq!( values.output, - "5/3:first#0rvRT:int!B:d|union#1rvbT:union!:intB:stringB:d|both#2rvbT:intersection!:EvalReflectLeftC:EvalReflectRightC:d|second#3OvbT:App\\Name?C:D=null|rest#4OVRt:null:d|" + "5/3:first#0rvRT:int!B:A1:EvalParamTag:first:d|union#1rvbT:union!:intB:stringB:A0:d|both#2rvbT:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second#3OvbT:App\\Name?C:A0:D=null|rest#4OVRt:null:A0:d|" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 5766a3d3ba..314ea00197 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -756,12 +756,12 @@ impl Parser { self.expect(TokenKind::LParen)?; let ( params, + parameter_attributes, parameter_types, parameter_defaults, parameter_is_by_ref, parameter_is_variadic, - ) = - self.parse_method_params()?; + ) = self.parse_method_params()?; let body = if is_abstract { self.expect_semicolon()?; Vec::new() @@ -778,6 +778,7 @@ impl Parser { body, ) .with_parameter_types(parameter_types) + .with_parameter_attributes(parameter_attributes) .with_parameter_defaults(parameter_defaults) .with_parameter_by_ref_flags(parameter_is_by_ref) .with_parameter_variadic_flags(parameter_is_variadic)) @@ -1275,16 +1276,17 @@ impl Parser { self.expect(TokenKind::LParen)?; let ( params, + parameter_attributes, parameter_types, parameter_defaults, parameter_is_by_ref, parameter_is_variadic, - ) = - self.parse_method_params()?; + ) = self.parse_method_params()?; self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) .with_static(is_static) .with_parameter_types(parameter_types) + .with_parameter_attributes(parameter_attributes) .with_parameter_defaults(parameter_defaults) .with_parameter_by_ref_flags(parameter_is_by_ref) .with_parameter_variadic_flags(parameter_is_variadic)) @@ -1704,6 +1706,7 @@ impl Parser { ) -> Result< ( Vec, + Vec>, Vec>, Vec>, Vec, @@ -1712,6 +1715,7 @@ impl Parser { EvalParseError, > { let mut params = Vec::new(); + let mut parameter_attributes = Vec::new(); let mut parameter_types = Vec::new(); let mut parameter_defaults = Vec::new(); let mut parameter_is_by_ref = Vec::new(); @@ -1719,6 +1723,7 @@ impl Parser { if self.consume(TokenKind::RParen) { return Ok(( params, + parameter_attributes, parameter_types, parameter_defaults, parameter_is_by_ref, @@ -1726,6 +1731,7 @@ impl Parser { )); } loop { + let attributes = self.parse_attribute_groups()?; let param_type = self.parse_optional_parameter_type()?; let is_by_ref = self.consume(TokenKind::Ampersand); let is_variadic = self.consume(TokenKind::Ellipsis); @@ -1733,6 +1739,7 @@ impl Parser { return Err(EvalParseError::ExpectedVariable); }; params.push(name.clone()); + parameter_attributes.push(attributes); parameter_types.push(param_type); parameter_is_by_ref.push(is_by_ref); parameter_is_variadic.push(is_variadic); @@ -1763,6 +1770,7 @@ impl Parser { self.expect(TokenKind::RParen)?; Ok(( params, + parameter_attributes, parameter_types, parameter_defaults, parameter_is_by_ref, @@ -1821,10 +1829,7 @@ impl Parser { /// Returns whether `&` belongs to by-reference parameter storage. fn next_token_starts_parameter_storage(&self) -> bool { - matches!( - self.peek(), - TokenKind::DollarIdent(_) | TokenKind::Ellipsis - ) + matches!(self.peek(), TokenKind::DollarIdent(_) | TokenKind::Ellipsis) } /// Consumes one simple qualified method parameter type name. @@ -2237,9 +2242,7 @@ fn method_parameter_default_is_supported(default: &EvalExpr) -> bool { /// Returns whether an EvalIR expression is safe to retain as a method default. fn eval_constant_expression_default_is_supported(expr: &EvalExpr) -> bool { match expr { - EvalExpr::Array(elements) => elements - .iter() - .all(eval_array_element_default_is_supported), + EvalExpr::Array(elements) => elements.iter().all(eval_array_element_default_is_supported), EvalExpr::Const(_) => true, EvalExpr::Magic(_) => true, EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 25447e25ef..58710ff815 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -565,7 +565,7 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { fn parse_fragment_accepts_typed_method_parameter_metadata() { let program = parse_fragment( br#"class DynEvalTypedParams { - public function run(Left&Right $both, int &$id = 7, ?\App\Name &$name = null, string|null $label = "x", mixed &...$tail) {} + public function run(#[Both("pair")] Left&Right $both, int &$id = 7, ?\App\Name &$name = null, string|null $label = "x", mixed &...$tail) {} }"#, ) .expect("fragment should parse"); @@ -585,8 +585,13 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { "tail".to_string() ] ); - assert_eq!(method.parameter_has_types(), &[true, true, true, true, true]); + assert_eq!( + method.parameter_has_types(), + &[true, true, true, true, true] + ); assert!(method.parameter_types().iter().all(Option::is_some)); + assert_eq!(method.parameter_attributes()[0][0].name(), "Both"); + assert!(method.parameter_attributes()[1].is_empty()); let both_type = method.parameter_types()[0].as_ref().expect("both type"); assert_eq!( both_type.variants(), @@ -609,10 +614,7 @@ fn parse_fragment_accepts_typed_method_parameter_metadata() { assert!(name_type.allows_null()); assert!(!name_type.is_intersection()); let label_type = method.parameter_types()[3].as_ref().expect("label type"); - assert_eq!( - label_type.variants(), - &[EvalParameterTypeVariant::String] - ); + assert_eq!(label_type.variants(), &[EvalParameterTypeVariant::String]); assert!(label_type.allows_null()); assert!(!label_type.is_intersection()); assert!(matches!( diff --git a/docs/internals/the-parser.md b/docs/internals/the-parser.md index 6db5d2f4ba..efa9d3d3bc 100644 --- a/docs/internals/the-parser.md +++ b/docs/internals/the-parser.md @@ -230,12 +230,12 @@ forms such as `?T|U` and normalize accepted declarations. | Type | Fields | Description | |---|---|---| | `Visibility` | `Public`, `Protected`, `Private` | Enum for property/method visibility | -| `Attribute` | `name`, `args`, `span` | A PHP 8 attribute entry from a `#[...]` group. The parser validates names and optional argument expressions. Class, method, and property names plus supported literal args feed `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported Reflection `getAttributes()` APIs. Method parameter names, positions, optional/variadic/by-reference flags, and declared-type presence feed the supported `ReflectionMethod::getParameters()` / `ReflectionParameter` slice. | +| `Attribute` | `name`, `args`, `span` | A PHP 8 attribute entry from a `#[...]` group. The parser validates names and optional argument expressions. Class, method, property, and method-parameter names plus supported literal args feed `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported Reflection `getAttributes()` APIs. Method parameter names, positions, optional/variadic/by-reference flags, declared-type presence, and method-parameter attributes feed the supported `ReflectionMethod::getParameters()` / `ReflectionParameter` slice. | | `AttributeGroup` | `attributes`, `span` | One bracketed attribute group. Declaration sites can carry one or more groups. | | `EnumCaseDecl` | `name`, `value`, `span`, `attributes` | A backed or unit enum case declaration, with declaration-level attributes preserved in the AST. | | `ClassConst` | `name`, `visibility`, `is_final`, `value`, `span`, `attributes` | A class, interface, or trait constant declaration. | | `ClassProperty` | `name`, `visibility`, `type_expr`, `hooks`, `readonly`, `is_final`, `is_static`, `is_abstract`, `by_ref`, `default`, `span`, `attributes` | A property declaration inside a class, trait, or interface, optionally carrying a parsed property type declaration, hook contract, static-property marker, by-reference promotion marker, or declaration-level attributes | -| `ClassMethod` | `name`, `visibility`, `is_static`, `is_abstract`, `is_final`, `has_body`, `params`, `variadic`, `return_type`, `body`, `span`, `attributes` | A method declaration inside a class, trait, or interface | +| `ClassMethod` | `name`, `visibility`, `is_static`, `is_abstract`, `is_final`, `has_body`, `params`, `param_attributes`, `variadic`, `return_type`, `body`, `span`, `attributes` | A method declaration inside a class, trait, or interface, including source-order parameter attribute groups | | `CatchClause` | `exception_types`, `variable`, `body` | A catch arm. `exception_types` supports both single-type and PHP-style multi-catch (`TypeA | TypeB`), and `variable` is optional for PHP 8-style `catch (Exception)` | | `StaticReceiver` | `Named(Name)`, `Self_`, `Static`, `Parent` | Left-hand side of `ClassName::method()`, `self::method()`, `static::method()`, and `parent::method()` | | `TraitUse` | `trait_names`, `adaptations`, `span` | A `use TraitA, TraitB { ... }` clause inside a class or trait body | diff --git a/docs/php/classes.md b/docs/php/classes.md index b17a24b3c4..87373ad09a 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -987,7 +987,7 @@ Rules and notes: ## Attributes -PHP 8.0 attributes (`#[Name]`) decorate declarations. elephc parses attributes at every site PHP allows: classes, interfaces, traits, enums, enum cases, top-level functions, methods, properties, function/method/closure parameters (incl. promoted constructor params), closures, and arrow functions. Class, method, and property attributes have limited runtime reflection through the helpers below; attributes on other declaration sites are currently validated for syntax and kept only in the AST. +PHP 8.0 attributes (`#[Name]`) decorate declarations. elephc parses attributes at every site PHP allows: classes, interfaces, traits, enums, enum cases, top-level functions, methods, properties, function/method/closure parameters (incl. promoted constructor params), closures, and arrow functions. Class, function, method, property, and method-parameter attributes have limited runtime reflection through the helpers below; attributes on other declaration sites are currently validated for syntax and kept only in the AST. ```php getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as array/object/class-constant parameter default values, parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1299,4 +1301,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, and scalar/null parameter defaults are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType`/`ReflectionUnionType`/`ReflectionIntersectionType` APIs. Per-parameter attribute reflection is not yet available. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to parameters (refactor of param representation and stack-trace infrastructure pending). +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, and scalar/null parameter defaults are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType`/`ReflectionUnionType`/`ReflectionIntersectionType` APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/docs/php/eval.md b/docs/php/eval.md index 7ede12d84d..ec257d3d88 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -151,11 +151,12 @@ syntax, but requesting those arguments is a runtime fatal. Private parent properties shadowed by same-named child properties use separate runtime storage, so parent methods keep seeing the private parent value while child methods and public access see the child property. -`ReflectionClass::getAttributes()`, `ReflectionMethod::getAttributes()`, and -`ReflectionProperty::getAttributes()` expose eval-retained class, method, and -property attributes for eval-declared class-like symbols when their arguments -fit the same literal subset, and `getName()` returns the reflected class or -member name for those owners. `ReflectionClass::getShortName()`, +`ReflectionClass::getAttributes()`, `ReflectionMethod::getAttributes()`, +`ReflectionProperty::getAttributes()`, and `ReflectionParameter::getAttributes()` +expose eval-retained class, method, property, and method-parameter attributes +for eval-declared class-like symbols when their arguments fit the same literal +subset, and `getName()` returns the reflected class, member, or parameter name +for those owners. `ReflectionClass::getShortName()`, `ReflectionClass::getNamespaceName()`, and `ReflectionClass::inNamespace()` derive namespace-aware parts from the resolved eval class-like name. `ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, @@ -204,8 +205,9 @@ declared-type presence, simple named type metadata through `allowsNull()`, and `isBuiltin()`, and multi-member union metadata through `ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter metadata is exposed through `ReflectionIntersectionType::getTypes()` and -`allowsNull()`. Defaulted eval method parameters are -bound when omitted and reported through +`allowsNull()`. Eval method parameter attributes are exposed through +`ReflectionParameter::getAttributes()` using materialized `ReflectionAttribute` +objects. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and `getDefaultValue()`. Supported default expressions include scalar literals, arrays whose keys and values are supported default diff --git a/src/codegen/fibers.rs b/src/codegen/fibers.rs index 6ed0c5927f..f6cb7c76d2 100644 --- a/src/codegen/fibers.rs +++ b/src/codegen/fibers.rs @@ -157,6 +157,7 @@ fn descriptor_invoker_placeholder_sig() -> FunctionSig { FunctionSig { params: Vec::new(), param_type_exprs: Vec::new(), + param_attributes: Vec::new(), defaults: Vec::new(), return_type: PhpType::Mixed, declared_return: false, @@ -187,6 +188,7 @@ fn signature_from_closure(closure: &Function, visible_abi_param_count: usize) -> .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), param_type_exprs: vec![None; visible_abi_param_count], + param_attributes: Vec::new(), defaults: closure .params .iter() diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 54f2f54f16..9fde7805c8 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -614,6 +614,7 @@ fn function_signature_from_eir_with_param_count( .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), param_type_exprs: vec![None; param_count], + param_attributes: Vec::new(), defaults: function .params .iter() diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index bbc3040809..47bd277f2e 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -71,6 +71,8 @@ struct ReflectionListedMember { #[derive(Clone)] struct ReflectionParameterMember { name: String, + attr_names: Vec, + attr_args: Vec>>, position: i64, is_optional: bool, is_variadic: bool, @@ -1639,6 +1641,16 @@ fn reflection_parameter_members(sig: &FunctionSig) -> Vec { emit_boxed_string_literal_default_to_result(ctx, value) } - Some(ReflectionParameterDefaultValue::Null) | None => emit_boxed_null_literal_to_result(ctx), + Some(ReflectionParameterDefaultValue::Null) | None => { + emit_boxed_null_literal_to_result(ctx) + } } abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, default_offset); @@ -3039,7 +3059,9 @@ fn emit_reflection_intersection_type_object( .module .class_infos .get("ReflectionIntersectionType") - .ok_or_else(|| CodegenIrError::unsupported("unknown class ReflectionIntersectionType"))?; + .ok_or_else(|| { + CodegenIrError::unsupported("unknown class ReflectionIntersectionType") + })?; ( class_info.class_id, class_info.properties.len(), @@ -3054,12 +3076,7 @@ fn emit_reflection_intersection_type_object( &uninitialized_marker_offsets, )?; emit_reflection_intersection_type_types_property(ctx, &type_metadata.types)?; - emit_reflection_owner_bool_property( - ctx, - "ReflectionIntersectionType", - "__allows_null", - false, - )?; + emit_reflection_owner_bool_property(ctx, "ReflectionIntersectionType", "__allows_null", false)?; Ok(()) } diff --git a/src/codegen_support/callable_descriptor.rs b/src/codegen_support/callable_descriptor.rs index 5b311d8ae5..2918002878 100644 --- a/src/codegen_support/callable_descriptor.rs +++ b/src/codegen_support/callable_descriptor.rs @@ -587,6 +587,7 @@ mod tests { ("rest".to_string(), PhpType::Array(Box::new(PhpType::Mixed))), ], param_type_exprs: vec![None, None, None], + param_attributes: Vec::new(), defaults: vec![ None, Some(Expr::new(ExprKind::StringLiteral("fallback".to_string()), Span::dummy())), @@ -636,6 +637,7 @@ mod tests { let sig = FunctionSig { params: vec![("value".to_string(), PhpType::Int)], param_type_exprs: vec![None], + param_attributes: Vec::new(), defaults: vec![None], return_type: PhpType::Int, declared_return: false, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 1dd9452802..3a49520e84 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -2917,6 +2917,7 @@ fn function_sig_from_extern_for_descriptor(sig: &ExternFunctionSig) -> FunctionS FunctionSig { params: sig.params.clone(), param_type_exprs: vec![None; sig.params.len()], + param_attributes: Vec::new(), defaults: vec![None; sig.params.len()], return_type: sig.return_type.clone(), declared_return: true, diff --git a/src/ir_lower/fibers.rs b/src/ir_lower/fibers.rs index 6f0ee7390c..7d27d28e32 100644 --- a/src/ir_lower/fibers.rs +++ b/src/ir_lower/fibers.rs @@ -195,6 +195,7 @@ fn callback_sig_for_binding( ctx.extern_functions.get(name.as_str()).map(|sig| FunctionSig { params: sig.params.clone(), param_type_exprs: vec![None; sig.params.len()], + param_attributes: Vec::new(), defaults: vec![None; sig.params.len()], return_type: sig.return_type.clone(), declared_return: true, @@ -312,6 +313,7 @@ fn callback_sig_from_closure_params( .iter() .map(|(_, type_ann, _, _)| type_ann.clone()) .collect(), + param_attributes: Vec::new(), defaults: params .iter() .map(|(_, _, default, _)| default.clone()) @@ -349,6 +351,7 @@ fn start_sig_from_callback_sig(sig: &FunctionSig) -> Option { .map(|(name, _)| (name.clone(), PhpType::Mixed)) .collect(), param_type_exprs: sig.param_type_exprs.clone(), + param_attributes: Vec::new(), defaults: sig.defaults.clone(), return_type: PhpType::Mixed, declared_return: false, diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index d99b6f274e..3a6af39173 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -386,6 +386,7 @@ pub(crate) fn lower_property_init_thunk( let sig = FunctionSig { params: vec![("this".to_string(), this_type.clone())], param_type_exprs: vec![None], + param_attributes: Vec::new(), defaults: vec![None], return_type: PhpType::Void, declared_return: false, @@ -1183,6 +1184,7 @@ fn signature_from_ast_with_variadic( .iter() .map(|(_, type_ann, _, _)| type_ann.clone()) .collect(), + param_attributes: Vec::new(), defaults: params.iter().map(|(_, _, default, _)| default.clone()).collect(), return_type: return_type .map(type_expr_to_php_type) diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 8b5abe603d..e52f8824d5 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -74,6 +74,7 @@ fn dummy_check_result() -> CheckResult { FunctionSig { params: vec![("x".to_string(), PhpType::Int)], param_type_exprs: vec![None], + param_attributes: Vec::new(), defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -144,6 +145,7 @@ fn class_info(_class_name: &str) -> ClassInfo { let method_sig = FunctionSig { params: Vec::new(), param_type_exprs: Vec::new(), + param_attributes: Vec::new(), defaults: Vec::new(), return_type: PhpType::Int, declared_return: true, @@ -490,6 +492,7 @@ fn class_method(name: &str, is_static: bool) -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Int), diff --git a/src/name_resolver/declarations.rs b/src/name_resolver/declarations.rs index 5e432ff025..fa14c9e949 100644 --- a/src/name_resolver/declarations.rs +++ b/src/name_resolver/declarations.rs @@ -282,10 +282,7 @@ fn resolve_attribute_groups( .iter() .map(|attr| Attribute { name: resolved_name(resolved_class_name( - &attr.name, - namespace, - imports, - symbols, + &attr.name, namespace, imports, symbols, )), args: attr .args @@ -314,6 +311,11 @@ fn resolve_methods( let body = resolve_stmt_list(&method.body, namespace, imports, symbols)?; Ok(ClassMethod { params: resolve_params(&method.params, namespace, imports, symbols), + param_attributes: method + .param_attributes + .iter() + .map(|groups| resolve_attribute_groups(groups, namespace, imports, symbols)) + .collect(), return_type: method .return_type .as_ref() @@ -343,12 +345,7 @@ fn resolve_class_consts( .iter() .map(|constant| ClassConst { value: resolve_expr(&constant.value, namespace, imports, symbols), - attributes: resolve_attribute_groups( - &constant.attributes, - namespace, - imports, - symbols, - ), + attributes: resolve_attribute_groups(&constant.attributes, namespace, imports, symbols), ..constant.clone() }) .collect() @@ -373,12 +370,7 @@ fn resolve_properties( .default .as_ref() .map(|expr| resolve_expr(expr, namespace, imports, symbols)), - attributes: resolve_attribute_groups( - &property.attributes, - namespace, - imports, - symbols, - ), + attributes: resolve_attribute_groups(&property.attributes, namespace, imports, symbols), ..property.clone() }) .collect() @@ -415,16 +407,14 @@ pub(super) fn resolve_trait_use( alias, visibility, } => Ok(TraitAdaptation::Alias { - trait_name: trait_name - .as_ref() - .map(|name| { - resolved_name(resolved_class_name( - name, - current_namespace, - imports, - symbols, - )) - }), + trait_name: trait_name.as_ref().map(|name| { + resolved_name(resolved_class_name( + name, + current_namespace, + imports, + symbols, + )) + }), method: php_symbol_key(method), alias: alias.as_ref().map(|alias| php_symbol_key(alias)), visibility: visibility.clone(), @@ -434,16 +424,14 @@ pub(super) fn resolve_trait_use( method, instead_of, } => Ok(TraitAdaptation::InsteadOf { - trait_name: trait_name - .as_ref() - .map(|name| { - resolved_name(resolved_class_name( - name, - current_namespace, - imports, - symbols, - )) - }), + trait_name: trait_name.as_ref().map(|name| { + resolved_name(resolved_class_name( + name, + current_namespace, + imports, + symbols, + )) + }), method: php_symbol_key(method), instead_of: instead_of .iter() diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index e6ef5279eb..fd59f3115b 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -8,24 +8,32 @@ //! Key details: //! - Folding must respect PHP coercions, truthiness, numeric edge cases, and runtime error boundaries. -use super::super::{fold_block, try_prune_match_expr}; use super::super::*; +use super::super::{fold_block, try_prune_match_expr}; use super::casts::try_fold_cast; use super::ops::{ - try_fold_array_access, try_fold_binary_op, try_fold_bit_not, try_fold_negate, - try_fold_not, try_fold_null_coalesce, try_fold_short_ternary, try_fold_ternary, + try_fold_array_access, try_fold_binary_op, try_fold_bit_not, try_fold_negate, try_fold_not, + try_fold_null_coalesce, try_fold_short_ternary, try_fold_ternary, }; /// Folds default expressions in function parameters. /// Returns a new parameter list with each parameter's default expression folded. pub(in crate::optimize) fn fold_params( - params: Vec<(String, Option, Option, bool)>, -) -> Vec<(String, Option, Option, bool)> { + params: Vec<( + String, + Option, + Option, + bool, + )>, +) -> Vec<( + String, + Option, + Option, + bool, +)> { params .into_iter() - .map(|(name, type_expr, default, is_ref)| { - (name, type_expr, default.map(fold_expr), is_ref) - }) + .map(|(name, type_expr, default, is_ref)| (name, type_expr, default.map(fold_expr), is_ref)) .collect() } @@ -58,6 +66,7 @@ pub(in crate::optimize) fn fold_method(method: ClassMethod) -> ClassMethod { is_final: method.is_final, has_body: method.has_body, params: fold_params(method.params), + param_attributes: method.param_attributes, variadic: method.variadic, variadic_type: method.variadic_type, return_type: method.return_type, @@ -91,9 +100,9 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { let kind = match expr.kind { // `IncludeValue` is a transient parser node fully expanded by the resolver; // it can never reach this pass. - ExprKind::IncludeValue { .. } => unreachable!( - "ExprKind::IncludeValue must be expanded by the resolver" - ), + ExprKind::IncludeValue { .. } => { + unreachable!("ExprKind::IncludeValue must be expanded by the resolver") + } ExprKind::StringLiteral(value) => ExprKind::StringLiteral(value), ExprKind::IntLiteral(value) => ExprKind::IntLiteral(value), ExprKind::FloatLiteral(value) => ExprKind::FloatLiteral(value), @@ -301,18 +310,14 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { object: Box::new(fold_expr(*object)), property, }, - ExprKind::DynamicPropertyAccess { object, property } => { - ExprKind::DynamicPropertyAccess { - object: Box::new(fold_expr(*object)), - property: Box::new(fold_expr(*property)), - } - } - ExprKind::NullsafePropertyAccess { object, property } => { - ExprKind::NullsafePropertyAccess { - object: Box::new(fold_expr(*object)), - property, - } - } + ExprKind::DynamicPropertyAccess { object, property } => ExprKind::DynamicPropertyAccess { + object: Box::new(fold_expr(*object)), + property: Box::new(fold_expr(*property)), + }, + ExprKind::NullsafePropertyAccess { object, property } => ExprKind::NullsafePropertyAccess { + object: Box::new(fold_expr(*object)), + property, + }, ExprKind::NullsafeDynamicPropertyAccess { object, property } => { ExprKind::NullsafeDynamicPropertyAccess { object: Box::new(fold_expr(*object)), diff --git a/src/optimize/tests/effects/methods.rs b/src/optimize/tests/effects/methods.rs index 54e872155b..a1abf389a0 100644 --- a/src/optimize/tests/effects/methods.rs +++ b/src/optimize/tests/effects/methods.rs @@ -32,6 +32,7 @@ fn test_program_static_method_effects_recognize_pure_static_methods() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -85,6 +86,7 @@ fn test_program_static_method_effects_resolve_self_receiver() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -110,6 +112,7 @@ fn test_program_static_method_effects_resolve_self_receiver() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -166,6 +169,7 @@ fn test_program_static_method_effects_resolve_parent_receiver() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -205,6 +209,7 @@ fn test_program_static_method_effects_resolve_parent_receiver() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -259,6 +264,7 @@ fn test_program_private_instance_method_effects_recognize_private_methods() { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/parser/ast/oop.rs b/src/parser/ast/oop.rs index 548c44a95e..51c359914a 100644 --- a/src/parser/ast/oop.rs +++ b/src/parser/ast/oop.rs @@ -62,8 +62,7 @@ pub struct EnumCaseDecl { impl PartialEq for EnumCaseDecl { /// Compares two enum cases by name, value, and attributes; span is not compared. fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.value == other.value - && self.attributes == other.attributes + self.name == other.name && self.value == other.value && self.attributes == other.attributes } } @@ -159,7 +158,8 @@ impl PartialEq for ClassProperty { /// Compares class properties by name, visibility, type, hooks, modifiers, /// by-ref flag, default value, and attributes; span is not compared. fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.visibility == other.visibility + self.name == other.name + && self.visibility == other.visibility && self.set_visibility == other.set_visibility && self.type_expr == other.type_expr && self.hooks == other.hooks @@ -211,6 +211,9 @@ pub struct ClassMethod { pub is_final: bool, pub has_body: bool, pub params: Vec<(String, Option, Option, bool)>, + /// Attribute groups declared on each source-order parameter. + /// This vector is parallel to `params`, plus one trailing entry when `variadic` is present. + pub param_attributes: Vec>, pub variadic: Option, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. Each argument /// collected into the variadic is checked against this type. @@ -250,7 +253,8 @@ impl PartialEq for ClassMethod { /// Compares class methods by name, visibility, static/abstract/final flags, /// has_body, and attributes; span, params, return_type, and body are not compared. fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.visibility == other.visibility + self.name == other.name + && self.visibility == other.visibility && self.is_static == other.is_static && self.is_abstract == other.is_abstract && self.is_final == other.is_final diff --git a/src/parser/stmt/oop/body.rs b/src/parser/stmt/oop/body.rs index 05a6008cbb..ce0c3cbb62 100644 --- a/src/parser/stmt/oop/body.rs +++ b/src/parser/stmt/oop/body.rs @@ -228,8 +228,8 @@ pub(in crate::parser::stmt) fn parse_class_like_body( )); } *pos += 1; // consume `const` - // PHP 8 allows semi-reserved keywords as class-constant names, except `class`, - // which is reserved for the `Foo::class` name fetch. + // PHP 8 allows semi-reserved keywords as class-constant names, except `class`, + // which is reserved for the `Foo::class` name fetch. let const_name = match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Class) => { return Err(CompileError::new( @@ -324,7 +324,10 @@ pub(in crate::parser::stmt) fn parse_class_like_body( if modifiers.is_abstract && default.is_some() { return Err(CompileError::new( member_span, - &format!("Abstract property ${} cannot have a default value", prop_name), + &format!( + "Abstract property ${} cannot have a default value", + prop_name + ), )); } if modifiers.is_abstract && !hooks.any() { @@ -421,7 +424,10 @@ fn append_promoted_properties( promoted_properties: Vec, ) -> Result<(), CompileError> { for promoted in promoted_properties { - if properties.iter().any(|property| property.name == promoted.name) { + if properties + .iter() + .any(|property| property.name == promoted.name) + { return Err(CompileError::new( promoted.span, &format!("Cannot redeclare promoted property ${}", promoted.name), @@ -583,8 +589,14 @@ fn parse_class_like_method( &Token::LParen, "Expected '(' after method name", )?; - let (params, variadic, variadic_type, promoted_properties, promoted_assignments) = - parse_method_params(tokens, pos, span, &method_name)?; + let ( + params, + param_attributes, + variadic, + variadic_type, + promoted_properties, + promoted_assignments, + ) = parse_method_params(tokens, pos, span, &method_name)?; expect_token(tokens, pos, &Token::RParen, "Expected ')'")?; // Parse optional return type: `: TypeExpr` let return_type = if *pos < tokens.len() && tokens[*pos].0 == Token::Colon { @@ -618,22 +630,26 @@ fn parse_class_like_method( } else { promoted_assignments.into_iter().chain(body).collect() }; - Ok((ClassMethod { - name: method_name, - visibility, - is_static, - is_abstract, - is_final, - has_body, - params, - variadic, - variadic_type, - return_type, - by_ref_return, - body, - span, - attributes: Vec::new(), - }, promoted_properties)) + Ok(( + ClassMethod { + name: method_name, + visibility, + is_static, + is_abstract, + is_final, + has_body, + params, + param_attributes, + variadic, + variadic_type, + return_type, + by_ref_return, + body, + span, + attributes: Vec::new(), + }, + promoted_properties, + )) } /// Parses the body of an `interface` declaration. @@ -663,8 +679,8 @@ fn parse_interface_body( } if tokens[*pos].0 == Token::Const { *pos += 1; // consume `const` - // PHP 8 allows semi-reserved keywords as class-constant names, except `class`, - // which is reserved for the `Foo::class` name fetch. + // PHP 8 allows semi-reserved keywords as class-constant names, except `class`, + // which is reserved for the `Foo::class` name fetch. let const_name = match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Class) => { return Err(CompileError::new( @@ -730,7 +746,10 @@ fn parse_interface_body( } let prop_name = prop_name.clone(); *pos += 1; - if properties.iter().any(|property: &ClassProperty| property.name == prop_name) { + if properties + .iter() + .any(|property: &ClassProperty| property.name == prop_name) + { return Err(CompileError::new( member_span, &format!("Cannot redeclare interface property ${}", prop_name), @@ -855,12 +874,7 @@ fn parse_property_hooks( }; let hook_name = match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Identifier(name)) => name.clone(), - _ => { - return Err(CompileError::new( - hook_span, - "Expected property hook name", - )) - } + _ => return Err(CompileError::new(hook_span, "Expected property hook name")), }; *pos += 1; let is_get = hook_name.eq_ignore_ascii_case("get"); @@ -947,6 +961,7 @@ fn parse_property_hooks( is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: prop_type.cloned(), @@ -976,6 +991,7 @@ fn parse_property_hooks( is_final: false, has_body: true, params: vec![(set_param, prop_type.cloned(), None, false)], + param_attributes: vec![Vec::new()], variadic: None, variadic_type: None, return_type: Some(TypeExpr::Void), @@ -995,7 +1011,10 @@ fn parse_property_hooks( "Expected '}' at end of property hook block", )?; if !hooks.any() { - return Err(CompileError::new(span, "Expected property hook declaration")); + return Err(CompileError::new( + span, + "Expected property hook declaration", + )); } Ok((hooks, accessors)) } diff --git a/src/parser/stmt/oop/method_params.rs b/src/parser/stmt/oop/method_params.rs index 475c859121..367c34dbb7 100644 --- a/src/parser/stmt/oop/method_params.rs +++ b/src/parser/stmt/oop/method_params.rs @@ -11,17 +11,19 @@ use crate::errors::CompileError; use crate::lexer::Token; use crate::parser::ast::{ - ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, Visibility, + AttributeGroup, ClassProperty, Expr, ExprKind, PropertyHooks, Stmt, StmtKind, TypeExpr, + Visibility, }; use crate::parser::expr::parse_expr; use crate::span::Span; -use super::super::params::{looks_like_typed_param, parse_type_expr}; use super::super::expect_token; +use super::super::params::{looks_like_typed_param, parse_type_expr}; type MethodParam = (String, Option, Option, bool); type ParsedMethodParams = ( Vec, + Vec>, Option, Option, Vec, @@ -44,6 +46,7 @@ pub(super) fn parse_method_params( method_name: &str, ) -> Result { let mut params = Vec::new(); + let mut param_attributes = Vec::new(); let mut variadic = None; let mut variadic_type = None; let mut promoted_properties = Vec::new(); @@ -64,7 +67,7 @@ pub(super) fn parse_method_params( } // PHP 8.0 parameter attributes — also covers attributes preceding a // promoted-property modifier such as `#[Inject] public Foo $f`. - crate::parser::consume_attribute_lists(tokens, pos)?; + let attributes = crate::parser::parse_attribute_lists(tokens, pos)?; if variadic.is_some() { return Err(CompileError::new( span, @@ -106,6 +109,7 @@ pub(super) fn parse_method_params( Some(Token::Variable(n)) => { variadic = Some(n.clone()); variadic_type = type_ann; + param_attributes.push(attributes); *pos += 1; } _ => return Err(CompileError::new(span, "Expected variable after '...'")), @@ -147,17 +151,25 @@ pub(super) fn parse_method_params( // not on the promoted property's default metadata. default: None, span: property_span, - attributes: Vec::new(), + attributes: attributes.clone(), }); promoted_assignments.push(promoted_property_assignment(&n, param_span)); } + param_attributes.push(attributes); params.push((n, type_ann, default, is_ref)); } _ => return Err(CompileError::new(span, "Expected parameter variable")), } } - Ok((params, variadic, variadic_type, promoted_properties, promoted_assignments)) + Ok(( + params, + param_attributes, + variadic, + variadic_type, + promoted_properties, + promoted_assignments, + )) } /// Scans the token stream for visibility modifiers (`public`/`protected`/`private`) @@ -176,7 +188,10 @@ fn parse_promoted_param_modifiers( match tokens.get(*pos).map(|(t, s)| (t, *s)) { Some((Token::Public, token_span)) => { if visibility.is_some() { - return Err(CompileError::new(token_span, "Duplicate parameter visibility")); + return Err(CompileError::new( + token_span, + "Duplicate parameter visibility", + )); } first_span.get_or_insert(token_span); visibility = Some(Visibility::Public); @@ -184,7 +199,10 @@ fn parse_promoted_param_modifiers( } Some((Token::Protected, token_span)) => { if visibility.is_some() { - return Err(CompileError::new(token_span, "Duplicate parameter visibility")); + return Err(CompileError::new( + token_span, + "Duplicate parameter visibility", + )); } first_span.get_or_insert(token_span); visibility = Some(Visibility::Protected); @@ -192,7 +210,10 @@ fn parse_promoted_param_modifiers( } Some((Token::Private, token_span)) => { if visibility.is_some() { - return Err(CompileError::new(token_span, "Duplicate parameter visibility")); + return Err(CompileError::new( + token_span, + "Duplicate parameter visibility", + )); } first_span.get_or_insert(token_span); visibility = Some(Visibility::Private); diff --git a/src/types/call_args/plan.rs b/src/types/call_args/plan.rs index 8ec4a436c7..99cb91f111 100644 --- a/src/types/call_args/plan.rs +++ b/src/types/call_args/plan.rs @@ -353,6 +353,7 @@ mod tests { FunctionSig { params, param_type_exprs: vec![None; 3], + param_attributes: Vec::new(), defaults, return_type: PhpType::Int, declared_return: true, diff --git a/src/types/checker/builtin_interfaces.rs b/src/types/checker/builtin_interfaces.rs index 4c38bfa961..2ef293d2e3 100644 --- a/src/types/checker/builtin_interfaces.rs +++ b/src/types/checker/builtin_interfaces.rs @@ -330,6 +330,7 @@ fn builtin_interface_method(name: &str, return_type: TypeExpr) -> ClassMethod { is_final: false, has_body: false, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(return_type), @@ -361,6 +362,7 @@ fn builtin_interface_method_with_params( is_final: false, has_body: false, params, + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(return_type), diff --git a/src/types/checker/builtin_iterators.rs b/src/types/checker/builtin_iterators.rs index 7c39046636..75b3fec39b 100644 --- a/src/types/checker/builtin_iterators.rs +++ b/src/types/checker/builtin_iterators.rs @@ -91,6 +91,7 @@ fn stub_method_returning_null(name: &str) -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), @@ -117,6 +118,7 @@ fn stub_method_returning_null_with_param(name: &str, param: &str) -> ClassMethod is_final: false, has_body: true, params: vec![(param.to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), @@ -141,6 +143,7 @@ fn stub_method_returning_false(name: &str) -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Bool), @@ -168,6 +171,7 @@ fn stub_void_method(name: &str) -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Void), diff --git a/src/types/checker/builtin_json.rs b/src/types/checker/builtin_json.rs index 9bdda15567..2f52632ce5 100644 --- a/src/types/checker/builtin_json.rs +++ b/src/types/checker/builtin_json.rs @@ -71,6 +71,7 @@ fn json_serialize_method() -> ClassMethod { is_final: false, has_body: false, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), diff --git a/src/types/checker/builtin_spl_classes/common.rs b/src/types/checker/builtin_spl_classes/common.rs index c0c16f06aa..807a96733c 100644 --- a/src/types/checker/builtin_spl_classes/common.rs +++ b/src/types/checker/builtin_spl_classes/common.rs @@ -78,6 +78,7 @@ pub(super) fn class_method_with_body( is_final: false, has_body: true, params, + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type, diff --git a/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index f4f06425b4..b5c16e22c1 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -70,6 +70,7 @@ pub(super) fn builtin_exception_constructor_method() -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -258,6 +259,7 @@ fn concrete_throwable_method(name: &str, return_type: TypeExpr, value: Expr) -> is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(return_type), @@ -281,6 +283,7 @@ fn abstract_throwable_method(name: &str, return_type: TypeExpr) -> ClassMethod { is_final: false, has_body: false, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(return_type), diff --git a/src/types/checker/builtin_types/fiber.rs b/src/types/checker/builtin_types/fiber.rs index 280c343b3d..52f3a9416b 100644 --- a/src/types/checker/builtin_types/fiber.rs +++ b/src/types/checker/builtin_types/fiber.rs @@ -63,6 +63,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -82,6 +83,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: vec![("callback".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -102,6 +104,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -119,6 +122,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: vec![("value".to_string(), None, null_default(), false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -136,6 +140,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: vec![("exception".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -153,6 +158,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -175,6 +181,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: vec![("value".to_string(), None, null_default(), false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -192,6 +199,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { is_final: true, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 56fd6fe55e..26db52c5a0 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -290,6 +290,7 @@ fn builtin_reflection_private_constructor_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -312,6 +313,7 @@ fn builtin_reflection_attribute_get_name_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Str), @@ -343,6 +345,7 @@ fn builtin_reflection_attribute_get_arguments_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Named(crate::names::Name::unqualified("array"))), @@ -374,6 +377,7 @@ fn builtin_reflection_attribute_new_instance_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(mixed_type()), @@ -403,6 +407,7 @@ fn builtin_reflection_class_new_instance_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: Some("args".to_string()), variadic_type: None, return_type: Some(mixed_type()), @@ -445,6 +450,7 @@ fn builtin_reflection_slot_getter( is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(return_type), @@ -477,6 +483,7 @@ fn builtin_reflection_function_constructor_method() -> ClassMethod { is_final: false, has_body: true, params: vec![("function".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -606,6 +613,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { ), builtin_reflection_slot_getter("hasType", "__has_type", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), + builtin_reflection_owner_get_attributes_method(), builtin_reflection_slot_getter( "isDefaultValueAvailable", "__has_default_value", @@ -630,6 +638,7 @@ fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(mixed_type()), @@ -1054,6 +1063,7 @@ fn builtin_reflection_class_string_method(method_name: &str, property: &str) -> is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Str), @@ -1084,6 +1094,7 @@ fn builtin_reflection_class_int_method(method_name: &str, property: &str) -> Cla is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Int), @@ -1143,6 +1154,7 @@ fn builtin_reflection_class_has_name_method( is_final: false, has_body: true, params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Int), @@ -1180,6 +1192,7 @@ fn builtin_reflection_class_get_constant_method() -> ClassMethod { is_final: false, has_body: true, params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(mixed_type()), @@ -1236,6 +1249,7 @@ fn builtin_reflection_class_get_reflection_constant_method() -> ClassMethod { is_final: false, has_body: true, params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(mixed_type()), @@ -1366,6 +1380,7 @@ fn builtin_reflection_class_implements_interface_method() -> ClassMethod { is_final: false, has_body: true, params: vec![("interface".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(bool_type()), @@ -1518,6 +1533,7 @@ fn builtin_reflection_class_array_method( is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(return_type), @@ -1618,6 +1634,7 @@ fn builtin_reflection_class_get_member_method( is_final: false, has_body: true, params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(return_class))), @@ -1662,6 +1679,7 @@ fn builtin_reflection_class_nullable_object_method( is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(nullable_object_type(class_name)), @@ -1691,6 +1709,7 @@ fn builtin_reflection_class_mixed_method(method_name: &str, property: &str) -> C is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(mixed_type()), @@ -1720,6 +1739,7 @@ fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> Cl is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(bool_type()), @@ -1818,6 +1838,7 @@ fn builtin_reflection_parameter_count_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Int), @@ -1895,6 +1916,7 @@ fn builtin_reflection_owner_constructor_method( .into_iter() .map(|(name, ty, default, by_ref)| (name.to_string(), ty, default, by_ref)) .collect(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -1917,6 +1939,7 @@ fn builtin_reflection_owner_get_attributes_method() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(array_type()), diff --git a/src/types/checker/builtins/callables/preg_replace_callback.rs b/src/types/checker/builtins/callables/preg_replace_callback.rs index e9de04cefe..38cf72bf51 100644 --- a/src/types/checker/builtins/callables/preg_replace_callback.rs +++ b/src/types/checker/builtins/callables/preg_replace_callback.rs @@ -140,6 +140,7 @@ fn contextual_closure_sig( Ok(Some(FunctionSig { params: param_types, param_type_exprs, + param_attributes: Vec::new(), defaults, return_type, declared_return, diff --git a/src/types/checker/callables/closures.rs b/src/types/checker/callables/closures.rs index eceb5cd222..037ea28a14 100644 --- a/src/types/checker/callables/closures.rs +++ b/src/types/checker/callables/closures.rs @@ -245,6 +245,7 @@ impl Checker { Ok(Some(FunctionSig { params: closure_sig.params, param_type_exprs: closure_sig.param_type_exprs, + param_attributes: Vec::new(), defaults: closure_sig.defaults, return_type, declared_return, diff --git a/src/types/checker/callables/first_class.rs b/src/types/checker/callables/first_class.rs index b6506f2bf8..ef7cb828d9 100644 --- a/src/types/checker/callables/first_class.rs +++ b/src/types/checker/callables/first_class.rs @@ -54,6 +54,7 @@ impl Checker { return Ok(FunctionSig { params: sig.params.clone(), param_type_exprs: vec![None; sig.params.len()], + param_attributes: Vec::new(), defaults: vec![None; sig.params.len()], return_type: sig.return_type.clone(), declared_return: true, diff --git a/src/types/checker/driver/externs.rs b/src/types/checker/driver/externs.rs index e7c344874a..50005b710f 100644 --- a/src/types/checker/driver/externs.rs +++ b/src/types/checker/driver/externs.rs @@ -113,6 +113,7 @@ impl Checker { let sig = FunctionSig { params: php_params.clone(), param_type_exprs: vec![None; php_params.len()], + param_attributes: Vec::new(), defaults: params.iter().map(|_| None).collect(), return_type: php_ret.clone(), declared_return: true, diff --git a/src/types/checker/driver/functions.rs b/src/types/checker/driver/functions.rs index 755c580788..0da9193c0f 100644 --- a/src/types/checker/driver/functions.rs +++ b/src/types/checker/driver/functions.rs @@ -276,6 +276,7 @@ impl Checker { .cloned() .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) .collect(), + param_attributes: Vec::new(), defaults: decl.defaults, return_type: crate::types::PhpType::Int, declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index b54431f6f6..572f31a007 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -405,6 +405,7 @@ fn trait_method_reflection_sig(method: &ClassMethod) -> FunctionSig { .map(|(_, type_ann, _, _)| type_ann.clone()) .chain(method.variadic.iter().map(|_| method.variadic_type.clone())) .collect(), + param_attributes: method.param_attributes.clone(), defaults, return_type: PhpType::Mixed, declared_return: method.return_type.is_some(), diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index c80b9818c1..d44acb2be3 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -197,6 +197,7 @@ impl Checker { .cloned() .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) .collect(), + param_attributes: Vec::new(), defaults: decl.defaults.clone(), return_type: PhpType::Int, declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/functions/resolution/signature.rs b/src/types/checker/functions/resolution/signature.rs index 0400d56e57..555b4472fb 100644 --- a/src/types/checker/functions/resolution/signature.rs +++ b/src/types/checker/functions/resolution/signature.rs @@ -98,6 +98,7 @@ impl Checker { .cloned() .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) .collect(), + param_attributes: Vec::new(), defaults: decl.defaults.clone(), return_type: PhpType::Int, declared_return: decl.return_type.is_some(), @@ -241,6 +242,7 @@ impl Checker { .cloned() .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) .collect(), + param_attributes: Vec::new(), defaults: decl.defaults.clone(), return_type: return_type.clone(), declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index fea65bae7a..8cc88d7338 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -312,6 +312,7 @@ pub(crate) fn insert_enum_metadata( FunctionSig { params: Vec::new(), param_type_exprs: Vec::new(), + param_attributes: Vec::new(), defaults: Vec::new(), return_type: PhpType::Array(Box::new(PhpType::Object(name.to_string()))), declared_return: true, @@ -332,6 +333,7 @@ pub(crate) fn insert_enum_metadata( FunctionSig { params: vec![("value".to_string(), backing_ty.clone())], param_type_exprs: vec![None], + param_attributes: Vec::new(), defaults: vec![None], return_type: if method_name == "from" { PhpType::Object(name.to_string()) diff --git a/src/types/checker/schema/validation.rs b/src/types/checker/schema/validation.rs index 47e38200cb..d58c7ca45d 100644 --- a/src/types/checker/schema/validation.rs +++ b/src/types/checker/schema/validation.rs @@ -86,6 +86,7 @@ pub(crate) fn build_method_sig( .map(|(_, type_ann, _, _)| type_ann.clone()) .chain(method.variadic.iter().map(|_| method.variadic_type.clone())) .collect(), + param_attributes: method.param_attributes.clone(), defaults, return_type, declared_return: method.return_type.is_some(), @@ -95,7 +96,12 @@ pub(crate) fn build_method_sig( .params .iter() .map(|(_, type_ann, _, _)| type_ann.is_some()) - .chain(method.variadic.iter().map(|_| method.variadic_type.is_some())) + .chain( + method + .variadic + .iter() + .map(|_| method.variadic_type.is_some()), + ) .collect(), variadic: method.variadic.clone(), deprecation: extract_deprecation(&method.attributes), @@ -123,9 +129,7 @@ pub(crate) fn build_method_sig( /// marker, with `reason` set to the attribute's first string argument (or an /// empty string if absent). Match is case-insensitive on the last segment of /// the attribute name. -pub(crate) fn extract_deprecation( - groups: &[crate::parser::ast::AttributeGroup], -) -> Option { +pub(crate) fn extract_deprecation(groups: &[crate::parser::ast::AttributeGroup]) -> Option { for group in groups { for attr in &group.attributes { if !matches_global_builtin_attribute(attr, "Deprecated") { @@ -331,7 +335,11 @@ pub(crate) fn validate_override_signature( )); } if parent_sig.declared_return - && !declared_return_type_compatible(checker, &parent_sig.return_type, &child_sig.return_type) + && !declared_return_type_compatible( + checker, + &parent_sig.return_type, + &child_sig.return_type, + ) { return Err(CompileError::new( method.span, diff --git a/src/types/signatures.rs b/src/types/signatures.rs index b5ea00e10a..b56813a21f 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -9,7 +9,7 @@ //! Key details: //! - Builtin signatures must match PHP so named arguments, first-class callables, and mutation semantics stay coherent. -use crate::parser::ast::{Expr, ExprKind, TypeExpr}; +use crate::parser::ast::{AttributeGroup, Expr, ExprKind, TypeExpr}; use crate::span::Span; use super::PhpType; @@ -23,6 +23,7 @@ use super::PhpType; pub struct FunctionSig { pub params: Vec<(String, PhpType)>, pub param_type_exprs: Vec>, + pub param_attributes: Vec>, pub defaults: Vec>, pub return_type: PhpType, pub declared_return: bool, @@ -60,6 +61,12 @@ pub(crate) fn callable_wrapper_sig(sig: &FunctionSig) -> FunctionSig { } } + let variadic_attributes = if wrapper_sig.param_attributes.len() > wrapper_sig.params.len() { + wrapper_sig.param_attributes.remove(wrapper_sig.params.len()) + } else { + Vec::new() + }; + wrapper_sig.params.push(( variadic_name.clone(), PhpType::Array(Box::new(PhpType::Mixed)), @@ -68,6 +75,7 @@ pub(crate) fn callable_wrapper_sig(sig: &FunctionSig) -> FunctionSig { wrapper_sig.ref_params.push(false); wrapper_sig.declared_params.push(false); wrapper_sig.param_type_exprs.push(None); + wrapper_sig.param_attributes.push(variadic_attributes); wrapper_sig } @@ -708,6 +716,7 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { "strlen" => Some(FunctionSig { params: vec![("string".to_string(), PhpType::Str)], param_type_exprs: vec![None], + param_attributes: vec![Vec::new()], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -726,6 +735,7 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { }, )], param_type_exprs: vec![None], + param_attributes: vec![Vec::new()], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -738,6 +748,7 @@ fn legacy_first_class_callable_builtin_sig(name: &str) -> Option { "buffer_len" => Some(FunctionSig { params: vec![("buffer".to_string(), PhpType::Buffer(Box::new(PhpType::Int)))], param_type_exprs: vec![None], + param_attributes: vec![Vec::new()], defaults: vec![None], return_type: PhpType::Int, declared_return: true, @@ -1145,6 +1156,7 @@ fn make_sig(params: &[&str], defaults: Vec>, variadic: Option<&str> .map(|name| ((*name).to_string(), PhpType::Mixed)) .collect(), param_type_exprs: vec![None; params.len()], + param_attributes: vec![Vec::new(); params.len()], defaults, return_type: PhpType::Mixed, declared_return: false, @@ -1185,6 +1197,7 @@ mod tests { FunctionSig { defaults: vec![None; params.len()], param_type_exprs: vec![None; params.len()], + param_attributes: vec![Vec::new(); params.len()], return_type: PhpType::Mixed, declared_return: false, by_ref_return: false, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9f89b96c2a..bec668281e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6406,7 +6406,7 @@ fn test_eval_reflection_method_lists_parameters() { eval('interface EvalReflectLeft {} interface EvalReflectRight {} class EvalReflectParamTarget { - public function run(int $first, int|string $union, EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, ...$rest) {} + public function run(#[EvalParamTag("first")] int $first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, ...$rest) {} } $method = new ReflectionMethod("EvalReflectParamTarget", "run"); echo $method->getNumberOfParameters() . "/"; @@ -6440,6 +6440,12 @@ foreach ($params as $param) { } else { echo ":null"; } + $attrs = $param->getAttributes(); + echo ":A" . count($attrs); + if (count($attrs) > 0) { + echo ":" . $attrs[0]->getName(); + echo ":" . $attrs[0]->getArguments()[0]; + } echo $param->isDefaultValueAvailable() ? ":D" : ":d"; if ($param->isDefaultValueAvailable()) { echo "="; @@ -6456,7 +6462,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "5/3:first@0rvbT:int!B:d|union@1rvbT:union!:intB:stringB:d|both@2rvbT:intersection!:EvalReflectLeftC:EvalReflectRightC:d|second@3OvbT:App\\Name?C:D=null|rest@4OVbt:null:d|" + "5/3:first@0rvbT:int!B:A1:EvalParamTag:first:d|union@1rvbT:union!:intB:stringB:A0:d|both@2rvbT:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second@3OvbT:App\\Name?C:A0:D=null|rest@4OVbt:null:A0:d|" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index dc9ec40774..137d7594b2 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1986,6 +1986,44 @@ if ($directType instanceof ReflectionNamedType) { ); } +/// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. +#[test] +fn test_reflection_parameter_get_attributes_returns_parameter_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $attrs = $param->getAttributes(); + echo $param->getName() . ":" . count($attrs); + if (count($attrs) > 0) { + echo ":" . $attrs[0]->getName(); + echo ":" . $attrs[0]->getArguments()[0]; + } + echo "|"; +} +$direct = new ReflectionParameter([ReflectParamAttrTarget::class, "run"], "name"); +$directAttrs = $direct->getAttributes(); +echo "direct:" . count($directAttrs) . ":" . $directAttrs[0]->getName() . ":" . $directAttrs[0]->getArguments()[0]; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:1:ReflectParamTag:id|name:1:ReflectParamTag:name|plain:0|direct:1:ReflectParamTag:name" + ); +} + /// Verifies that `ReflectionParameter` exposes supported scalar/null defaults. #[test] fn test_reflection_parameter_exposes_default_values() { diff --git a/tests/parser_tests/attributes.rs b/tests/parser_tests/attributes.rs index 642fe06395..749cf27683 100644 --- a/tests/parser_tests/attributes.rs +++ b/tests/parser_tests/attributes.rs @@ -24,9 +24,20 @@ fn first_class_decl_name(stmts: &[Stmt]) -> &str { /// Extracts attribute groups, properties, and methods from the first ClassDecl in a parsed program. /// Panics if no ClassDecl is found. -fn class_decl<'a>(stmts: &'a [Stmt]) -> (&'a Vec, &'a Vec, &'a Vec) { +fn class_decl<'a>( + stmts: &'a [Stmt], +) -> ( + &'a Vec, + &'a Vec, + &'a Vec, +) { for stmt in stmts { - if let StmtKind::ClassDecl { properties, methods, .. } = &stmt.kind { + if let StmtKind::ClassDecl { + properties, + methods, + .. + } = &stmt.kind + { return (&stmt.attributes, properties, methods); } } @@ -48,9 +59,8 @@ fn test_class_attribute_is_accepted_and_does_not_alter_decl() { fn test_method_attribute_is_accepted() { // `#[Required]` on a class method parses without error. // Persistence is verified by test_method_attribute_is_persisted. - let _ = parse_source( - " cases, other => panic!("expected EnumDecl, got {:?}", other), From 0231b5ca8ab0b1c28c7322c76ed6d863b0c92933 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 13:11:52 +0200 Subject: [PATCH 0414/1208] Add reflection declaring class metadata --- .../elephc-eval/src/interpreter/reflection.rs | 155 ++++++++++++--- .../tests/builtins_class_metadata.rs | 41 ++++ .../interpreter/tests/support/object_ops.rs | 14 ++ docs/php/classes.md | 12 +- docs/php/eval.md | 9 +- src/codegen/eval_reflection_owner_helpers.rs | 3 +- src/codegen/lower_inst/objects/reflection.rs | 187 +++++++++++++++--- src/types/checker/builtin_types/reflection.rs | 19 ++ tests/codegen/eval.rs | 40 ++++ tests/codegen/oop/attributes.rs | 35 ++++ 10 files changed, 457 insertions(+), 58 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 93fbecb720..31fda063a5 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -48,6 +48,7 @@ struct EvalReflectionClassMetadata { /// Eval metadata needed to materialize one `ReflectionMethod` or `ReflectionProperty` owner object. struct EvalReflectionMemberMetadata { + declaring_class_name: Option, attributes: Vec, visibility: EvalVisibility, is_static: bool, @@ -377,7 +378,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( &[], &[], &[], - None, + member.declaring_class_name.as_deref(), &member.parameters, flags, member.required_parameter_count as u64, @@ -421,8 +422,8 @@ fn eval_reflection_class_constant_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let attributes = - eval_reflection_class_constant_attributes(reflected_name, constant_name, context) + let (declaring_class_name, attributes) = + eval_reflection_class_constant_metadata(reflected_name, constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, @@ -432,7 +433,7 @@ fn eval_reflection_class_constant_object_result( &[], &[], &[], - None, + Some(&declaring_class_name), &[], 0, 0, @@ -540,7 +541,7 @@ fn eval_reflection_method_new( &[], &[], &[], - None, + method.declaring_class_name.as_deref(), &method.parameters, flags, method.required_parameter_count as u64, @@ -576,7 +577,7 @@ fn eval_reflection_property_new( &[], &[], &[], - None, + property.declaring_class_name.as_deref(), &[], flags, 0, @@ -601,8 +602,8 @@ fn eval_reflection_class_constant_new( return Ok(None); } let constant_name = eval_reflection_string_arg(args[1], values)?; - let attributes = - eval_reflection_class_constant_attributes(&class_name, &constant_name, context) + let (declaring_class_name, attributes) = + eval_reflection_class_constant_metadata(&class_name, &constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, @@ -612,7 +613,7 @@ fn eval_reflection_class_constant_new( &[], &[], &[], - None, + Some(&declaring_class_name), &[], 0, 0, @@ -645,6 +646,7 @@ fn eval_reflection_enum_case_new( return Err(EvalStatus::RuntimeFatal); } let case_name = eval_reflection_string_arg(args[1], values)?; + let declaring_class_name = enum_decl.name().to_string(); let attributes = enum_decl .case(&case_name) .map(|case| case.attributes().to_vec()) @@ -657,7 +659,7 @@ fn eval_reflection_enum_case_new( &[], &[], &[], - None, + Some(&declaring_class_name), &[], 0, 0, @@ -682,13 +684,48 @@ fn eval_reflection_owner_object( modifiers: u64, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_owner_object_with_members( + owner_kind, + reflected_name, + attributes, + interface_names, + trait_names, + method_names, + property_names, + parent_class_name, + parameter_metadata, + flags, + modifiers, + true, + context, + values, + ) +} + +/// Materializes one Reflection owner object with optional nested class member objects. +fn eval_reflection_owner_object_with_members( + owner_kind: u64, + reflected_name: &str, + attributes: &[EvalAttribute], + interface_names: &[String], + trait_names: &[String], + method_names: &[String], + property_names: &[String], + parent_class_name: Option<&str>, + parameter_metadata: &[EvalReflectionParameterMetadata], + flags: u64, + modifiers: u64, + include_class_members: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let attrs = eval_reflection_attribute_array_result(attributes, context, values)?; let interface_names_array = eval_reflection_string_array_result(interface_names, values)?; let trait_names_array = eval_reflection_string_array_result(trait_names, values)?; let method_names_array = eval_reflection_string_array_result(method_names, values)?; let property_names_array = eval_reflection_string_array_result(property_names, values)?; - let method_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + let method_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS && include_class_members { eval_reflection_member_object_array_result( EVAL_REFLECTION_OWNER_METHOD, reflected_name, @@ -701,7 +738,7 @@ fn eval_reflection_owner_object( } else { values.array_new(0)? }; - let property_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + let property_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS && include_class_members { eval_reflection_member_object_array_result( EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, @@ -712,8 +749,13 @@ fn eval_reflection_owner_object( } else { values.array_new(0)? }; - let parent_class = - eval_reflection_parent_class_result(owner_kind, parent_class_name, context, values)?; + let parent_class = eval_reflection_related_class_result( + owner_kind, + parent_class_name, + include_class_members, + context, + values, + )?; let object = values.reflection_owner_new( owner_kind, reflected_name, @@ -743,20 +785,40 @@ fn eval_reflection_owner_object( Ok(object) } -/// Builds the `ReflectionClass|false` value stored in one ReflectionClass parent slot. -fn eval_reflection_parent_class_result( +/// Builds the `ReflectionClass|false` value stored in parent or declaring-class slots. +fn eval_reflection_related_class_result( owner_kind: u64, - parent_class_name: Option<&str>, + related_class_name: Option<&str>, + include_class_members: bool, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if owner_kind != EVAL_REFLECTION_OWNER_CLASS { - return values.bool_value(false); - } - let Some(parent_class_name) = parent_class_name else { + let Some(related_class_name) = related_class_name else { return values.bool_value(false); }; - let Some(metadata) = eval_reflection_class_like_attributes(parent_class_name, context) else { + if owner_kind == EVAL_REFLECTION_OWNER_CLASS && include_class_members { + return eval_reflection_full_class_object_result(related_class_name, context, values); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD + | EVAL_REFLECTION_OWNER_PROPERTY + | EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + return eval_reflection_shallow_class_object_result(related_class_name, context, values); + } + values.bool_value(false) +} + +/// Builds a full `ReflectionClass` object for parent-class metadata. +fn eval_reflection_full_class_object_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { return values.bool_value(false); }; eval_reflection_owner_object( @@ -776,6 +838,33 @@ fn eval_reflection_parent_class_result( ) } +/// Builds a shallow `ReflectionClass` object for member declaring-class metadata. +fn eval_reflection_shallow_class_object_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { + return values.bool_value(false); + }; + eval_reflection_owner_object_with_members( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + &metadata.attributes, + &metadata.interface_names, + &metadata.trait_names, + &[], + &[], + None, + &[], + metadata.flags, + metadata.modifiers, + false, + context, + values, + ) +} + /// Builds an indexed PHP string array for ReflectionClass metadata names. fn eval_reflection_string_array_result( names: &[String], @@ -1028,7 +1117,7 @@ fn eval_reflection_member_object_array_result( &[], &[], &[], - None, + member.declaring_class_name.as_deref(), &member.parameters, flags, member.required_parameter_count as u64, @@ -1187,20 +1276,20 @@ fn eval_reflection_class_modifiers( modifiers } -/// Returns attributes attached to an eval class constant or enum case. -fn eval_reflection_class_constant_attributes( +/// Returns declaring class and attributes attached to an eval class constant or enum case. +fn eval_reflection_class_constant_metadata( class_name: &str, constant_name: &str, context: &ElephcEvalContext, -) -> Option> { +) -> Option<(String, Vec)> { if let Some(enum_decl) = context.enum_decl(class_name) { if let Some(case) = enum_decl.case(constant_name) { - return Some(case.attributes().to_vec()); + return Some((enum_decl.name().to_string(), case.attributes().to_vec())); } } context .class_constant(class_name, constant_name) - .map(|(_, constant)| constant.attributes().to_vec()) + .map(|(declaring_class, constant)| (declaring_class, constant.attributes().to_vec())) } /// Returns true when a name resolves to an eval-declared class-like symbol. @@ -1288,7 +1377,8 @@ fn eval_reflection_method_metadata( if context.has_class(class_name) || context.has_enum(class_name) { return context .class_method(class_name, method_name) - .map(|(_, method)| EvalReflectionMemberMetadata { + .map(|(declaring_class, method)| EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), attributes: method.attributes().to_vec(), visibility: method.visibility(), is_static: method.is_static(), @@ -1315,6 +1405,7 @@ fn eval_reflection_method_metadata( .into_iter() .find(|method| method.name().eq_ignore_ascii_case(method_name)) .map(|method| EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.to_string()), attributes: method.attributes().to_vec(), visibility: EvalVisibility::Public, is_static: method.is_static(), @@ -1341,6 +1432,7 @@ fn eval_reflection_method_metadata( .iter() .find(|method| method.name().eq_ignore_ascii_case(method_name)) .map(|method| EvalReflectionMemberMetadata { + declaring_class_name: Some(trait_decl.name().to_string()), attributes: method.attributes().to_vec(), visibility: method.visibility(), is_static: method.is_static(), @@ -1372,7 +1464,8 @@ fn eval_reflection_property_metadata( if context.has_class(class_name) || context.has_enum(class_name) { return context .class_property(class_name, property_name) - .map(|(_, property)| EvalReflectionMemberMetadata { + .map(|(declaring_class, property)| EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), attributes: property.attributes().to_vec(), visibility: property.visibility(), is_static: property.is_static(), @@ -1388,6 +1481,7 @@ fn eval_reflection_property_metadata( .into_iter() .find(|property| property.name() == property_name) .map(|property| EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.to_string()), attributes: property.attributes().to_vec(), visibility: EvalVisibility::Public, is_static: false, @@ -1403,6 +1497,7 @@ fn eval_reflection_property_metadata( .iter() .find(|property| property.name() == property_name) .map(|property| EvalReflectionMemberMetadata { + declaring_class_name: Some(trait_decl.name().to_string()), attributes: property.attributes().to_vec(), visibility: property.visibility(), is_static: property.is_static(), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 0ffb9e2622..77b7d8bf3a 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -723,6 +723,47 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval member and enum-case reflectors expose their declaring class. +#[test] +fn execute_program_reflects_eval_declaring_class_metadata() { + let program = parse_fragment( + br#"class EvalDeclaringBase { + public $baseProp = 1; + public function inherited() { return "base"; } + public const BASE_CONST = 10; +} +class EvalDeclaringChild extends EvalDeclaringBase { + public $childProp = 2; + public function own() { return "child"; } + public const CHILD_CONST = 20; +} +enum EvalDeclaringEnum: string { + case Ready = "ready"; + public const LEVEL = 3; +} +echo (new ReflectionMethod("EvalDeclaringChild", "inherited"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getMethod("own")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionProperty("EvalDeclaringChild", "baseProp"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getProperty("childProp")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getReflectionConstant("BASE_CONST")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClassConstant("EvalDeclaringChild", "BASE_CONST"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringEnum"))->getReflectionConstant("Ready")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionEnumBackedCase("EvalDeclaringEnum", "Ready"))->getDeclaringClass()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringBase:EvalDeclaringEnum:EvalDeclaringEnum" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod exposes eval method parameter objects with names and positions. #[test] fn execute_program_reflects_eval_method_parameters() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 4d90432ffa..1e41b14a6f 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -158,6 +158,10 @@ impl FakeOps { Self::object_property(&properties, "__parent_class") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "getdeclaringclass") if args.is_empty() => { + Self::object_property(&properties, "__declaring_class") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) } @@ -513,6 +517,16 @@ impl FakeOps { properties.push(("__is_protected".to_string(), is_protected)); properties.push(("__is_private".to_string(), is_private)); } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD + | EVAL_REFLECTION_OWNER_PROPERTY + | EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + properties.push(("__declaring_class".to_string(), parent_class)); + } if owner_kind == EVAL_REFLECTION_OWNER_METHOD { let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; let is_abstract = diff --git a/docs/php/classes.md b/docs/php/classes.md index 87373ad09a..9b82e388bd 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1179,6 +1179,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::getNumberOfParameters()` | `new ReflectionFunction($function_name)` | Return the total number of reflected function parameters | | `ReflectionFunction::getNumberOfRequiredParameters()` | `new ReflectionFunction($function_name)` | Return the number of required reflected function parameters | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | +| `ReflectionMethod::getDeclaringClass()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected method | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | | `ReflectionMethod::isStatic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is static | | `ReflectionMethod::isPublic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is public | @@ -1203,11 +1204,18 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionUnionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return non-null union members as `ReflectionNamedType` objects and report whether `null` was part of the union | | `ReflectionIntersectionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return intersection members as `ReflectionNamedType` objects and report `false` for nullability | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | +| `ReflectionProperty::getDeclaringClass()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected property | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | | `ReflectionProperty::isStatic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is static | | `ReflectionProperty::isPublic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is public | | `ReflectionProperty::isProtected()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is protected | | `ReflectionProperty::isPrivate()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is private | +| `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | +| `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | +| `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | +| `ReflectionEnumUnitCase::getName()` / `ReflectionEnumBackedCase::getName()` | `new ReflectionEnumUnitCase($enum_name, $case_name)` or `new ReflectionEnumBackedCase($enum_name, $case_name)` | Return the reflected enum-case name | +| `ReflectionEnumUnitCase::getAttributes()` / `ReflectionEnumBackedCase::getAttributes()` | Same as enum-case `getName()` | Return `ReflectionAttribute` objects for enum-case attributes | +| `ReflectionEnumUnitCase::getDeclaringClass()` / `ReflectionEnumBackedCase::getDeclaringClass()` | Same as enum-case `getName()` | Return a `ReflectionClass` object for the enum that declares the reflected case | | `ReflectionAttribute::newInstance()` | Internal only | Instantiate the attribute class from captured literal args | Functions and their parameters can also be reflected. `ReflectionFunction` reads @@ -1273,7 +1281,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, declaring function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, declaring function metadata, parameter declaring-function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1301,4 +1309,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` APIs. Function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, and scalar/null parameter defaults are exposed through the supported `ReflectionFunction`/`ReflectionMethod`/`ReflectionParameter`/`ReflectionNamedType`/`ReflectionUnionType`/`ReflectionIntersectionType` APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, scalar/null parameter defaults, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/docs/php/eval.md b/docs/php/eval.md index ec257d3d88..c624434606 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -186,7 +186,10 @@ constant or case is visible. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate -flags. `ReflectionClass::getParentClass()` returns a materialized +flags. `ReflectionMethod::getDeclaringClass()` and +`ReflectionProperty::getDeclaringClass()` return a materialized +`ReflectionClass` for the eval class-like symbol that declares the reflected +member. `ReflectionClass::getParentClass()` returns a materialized `ReflectionClass` for eval-declared parent classes or `false` when no parent class exists. `ReflectionClass::newInstance()` constructs eval-declared reflected classes and forwards constructor arguments through eval's positional, named, and @@ -232,7 +235,9 @@ when the variadic container itself is not rebound, and are reported through `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant and enum-case attributes through the same materialized `ReflectionAttribute` -shape; their `getName()` methods return the reflected constant or case name. +shape; their `getName()` methods return the reflected constant or case name, +and `getDeclaringClass()` returns the declaring class or enum as a +`ReflectionClass`. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index d545b6bcfe..6779050e36 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -200,7 +200,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option, attr_names: Vec, attr_args: Vec>>, flags: ReflectionMemberFlags, @@ -284,6 +285,20 @@ fn emit_reflection_owner_object( emit_reflection_bool_property(ctx, "__is_instantiable", metadata.is_instantiable)?; emit_reflection_int_property_by_name(ctx, "__modifiers", metadata.modifiers)?; } + if matches!( + class_name, + "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { + emit_reflection_declaring_class_property( + ctx, + class_name, + metadata.parent_class_name.as_deref(), + )?; + } if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { emit_reflection_parameter_array_property_by_name( ctx, @@ -440,9 +455,9 @@ fn reflection_class_metadata_for_name( .module .interface_infos .get(interface_name) - .map(|info| reflection_interface_method_members(info, &method_names)) - .unwrap_or_else(|| default_method_members(&method_names, true, false)); - let property_members = default_property_members(&property_names, true); + .map(|info| reflection_interface_method_members(info, interface_name, &method_names)) + .unwrap_or_else(|| default_method_members(&method_names, true, interface_name)); + let property_members = default_property_members(&property_names, true, interface_name); let constructor_member = reflection_constructor_member(&method_members); return Ok(ReflectionOwnerMetadata { reflected_name: Some(interface_name.to_string()), @@ -489,9 +504,9 @@ fn reflection_class_metadata_for_name( .module .declared_trait_methods .get(trait_name) - .map(|methods| reflection_trait_method_members(methods, &method_names)) - .unwrap_or_else(|| default_method_members(&method_names, false, true)); - let property_members = default_property_members(&property_names, false); + .map(|methods| reflection_trait_method_members(methods, trait_name, &method_names)) + .unwrap_or_else(|| default_method_members(&method_names, false, trait_name)); + let property_members = default_property_members(&property_names, false, trait_name); let constructor_member = reflection_constructor_member(&method_members); return Ok(ReflectionOwnerMetadata { reflected_name: Some(trait_name.to_string()), @@ -524,6 +539,24 @@ fn reflection_class_metadata_for_name( Ok(empty_reflection_metadata()) } +/// Resolves class metadata for nested declaring-class slots without recursive member objects. +fn reflection_shallow_class_metadata_for_name( + ctx: &FunctionContext<'_>, + reflected_class: &str, +) -> Result { + let mut metadata = reflection_class_metadata_for_name(ctx, reflected_class)?; + metadata.method_names.clear(); + metadata.property_names.clear(); + metadata.constant_names.clear(); + metadata.constant_members.clear(); + metadata.constant_reflection_members.clear(); + metadata.method_members.clear(); + metadata.property_members.clear(); + metadata.constructor_member = None; + metadata.parent_class_name = None; + Ok(metadata) +} + /// Resolves `ReflectionFunction(function)` metadata. fn reflection_function_metadata( ctx: &FunctionContext<'_>, @@ -569,14 +602,17 @@ fn reflection_method_metadata( } if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { if let Some(info) = ctx.module.interface_infos.get(interface_name) { - if let Some(member) = reflection_interface_method_member(info, &method_key) { + if let Some(member) = + reflection_interface_method_member(info, interface_name, &method_key) + { return Ok(reflection_method_owner_metadata(&method_name, member)); } } } if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { if let Some(methods) = ctx.module.declared_trait_methods.get(trait_name) { - if let Some(member) = reflection_trait_method_member(methods, &method_key) { + if let Some(member) = reflection_trait_method_member(methods, trait_name, &method_key) + { return Ok(reflection_method_owner_metadata(&method_name, member)); } } @@ -603,7 +639,7 @@ fn reflection_method_owner_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, - parent_class_name: None, + parent_class_name: member.declaring_class_name, parameter_members: member.parameters, required_parameter_count: member.required_parameter_count, is_final: false, @@ -633,6 +669,10 @@ fn reflection_property_metadata( let property_name = const_required_string_operand(ctx, property_operand, "ReflectionProperty")?; Ok(resolve_reflection_class(ctx, &reflected_class) .and_then(|(_, info)| { + let declaring_class_name = reflection_property_declaring_class_name( + info, + &property_name, + ); Some(ReflectionOwnerMetadata { reflected_name: Some(property_name.clone()), attr_names: info.property_attribute_names.get(&property_name)?.clone(), @@ -647,7 +687,7 @@ fn reflection_property_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, - parent_class_name: None, + parent_class_name: declaring_class_name, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -746,13 +786,13 @@ fn reflection_method_member_for_class_like( .module .interface_infos .get(interface_name) - .and_then(|info| reflection_interface_method_member(info, method_key)); + .and_then(|info| reflection_interface_method_member(info, interface_name, method_key)); } resolve_reflection_trait(ctx, reflected_class).and_then(|trait_name| { ctx.module .declared_trait_methods .get(trait_name) - .and_then(|methods| reflection_trait_method_member(methods, method_key)) + .and_then(|methods| reflection_trait_method_member(methods, trait_name, method_key)) }) } @@ -788,7 +828,9 @@ fn reflection_class_constant_metadata( const_string_or_class_operand(ctx, class_operand, "ReflectionClassConstant")?; let constant_name = const_required_string_operand(ctx, constant_operand, "ReflectionClassConstant")?; - if let Some(case) = resolve_reflection_enum_case(ctx, &reflected_class, &constant_name) { + if let Some((enum_name, case)) = + resolve_reflection_enum_case(ctx, &reflected_class, &constant_name) + { return Ok(ReflectionOwnerMetadata { reflected_name: Some(constant_name), attr_names: case.attribute_names.clone(), @@ -803,7 +845,7 @@ fn reflection_class_constant_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, - parent_class_name: None, + parent_class_name: Some(enum_name.to_string()), parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -819,7 +861,7 @@ fn reflection_class_constant_metadata( } Ok( resolve_reflection_class_constant(ctx, &reflected_class, &constant_name) - .map(|(_, info)| { + .map(|(declaring_class_name, info)| { let attr_names = info .constant_attribute_names .get(&constant_name) @@ -844,7 +886,7 @@ fn reflection_class_constant_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, - parent_class_name: None, + parent_class_name: Some(declaring_class_name.to_string()), parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -878,7 +920,7 @@ fn reflection_enum_case_metadata( let case_name = const_required_string_operand(ctx, case_operand, class_name)?; Ok( resolve_reflection_enum_case(ctx, &reflected_enum, &case_name) - .map(|case| ReflectionOwnerMetadata { + .map(|(enum_name, case)| ReflectionOwnerMetadata { reflected_name: Some(case_name.clone()), attr_names: case.attribute_names.clone(), attr_args: case.attribute_args.clone(), @@ -892,7 +934,7 @@ fn reflection_enum_case_metadata( method_members: Vec::new(), property_members: Vec::new(), constructor_member: None, - parent_class_name: None, + parent_class_name: Some(enum_name.to_string()), parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -1204,6 +1246,7 @@ fn reflection_class_constant_reflection_members( for case in &enum_info.cases { push_unique_constant_reflection_member( &case.name, + class_name, case.attribute_names.clone(), case.attribute_args.clone(), &mut members, @@ -1223,6 +1266,7 @@ fn reflection_class_constant_reflection_members( } push_unique_constant_reflection_member( constant_name, + resolved_name, current_info .constant_attribute_names .get(constant_name) @@ -1277,6 +1321,7 @@ fn collect_interface_constant_reflection_members( for constant_name in interface_info.constants.keys() { push_unique_constant_reflection_member( constant_name, + interface_name, Vec::new(), Vec::new(), members, @@ -1299,6 +1344,7 @@ fn reflection_trait_constant_reflection_members( for constant_name in constants.keys() { push_unique_constant_reflection_member( constant_name, + trait_name, Vec::new(), Vec::new(), &mut members, @@ -1313,6 +1359,7 @@ fn reflection_trait_constant_reflection_members( /// Appends one constant-reflector member if a constant with this name was not already visible. fn push_unique_constant_reflection_member( name: &str, + declaring_class_name: &str, attr_names: Vec, attr_args: Vec>>, members: &mut Vec, @@ -1323,6 +1370,7 @@ fn push_unique_constant_reflection_member( } members.push(ReflectionListedMember { name: name.to_string(), + declaring_class_name: Some(declaring_class_name.to_string()), attr_names, attr_args, flags: ReflectionMemberFlags::default(), @@ -1367,6 +1415,28 @@ fn reflection_property_visible_from_class( .unwrap_or(false) } +/// Returns the class that declares one reflected instance or static method. +fn reflection_method_declaring_class_name( + info: &crate::types::ClassInfo, + method_key: &str, +) -> Option { + info.method_impl_classes + .get(method_key) + .or_else(|| info.static_method_impl_classes.get(method_key)) + .cloned() +} + +/// Returns the class that declares one reflected instance or static property. +fn reflection_property_declaring_class_name( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + info.property_declaring_classes + .get(property_name) + .or_else(|| info.static_property_declaring_classes.get(property_name)) + .cloned() +} + /// Returns ReflectionMethod predicate flags for a method visible on one class. fn reflection_method_member_flags( info: &crate::types::ClassInfo, @@ -1450,8 +1520,10 @@ fn reflection_class_method_member( .methods .get(&method_key) .or_else(|| info.static_methods.get(&method_key))?; + let declaring_class_name = reflection_method_declaring_class_name(info, &method_key); Some(ReflectionListedMember { name: method_key.clone(), + declaring_class_name, attr_names: info .method_attribute_names .get(&method_key) @@ -1471,17 +1543,21 @@ fn reflection_class_method_member( /// Builds ReflectionMethod array entries for methods declared by an interface. fn reflection_interface_method_members( info: &InterfaceInfo, + interface_name: &str, method_names: &[String], ) -> Vec { method_names .iter() - .filter_map(|method_name| reflection_interface_method_member(info, method_name)) + .filter_map(|method_name| { + reflection_interface_method_member(info, interface_name, method_name) + }) .collect() } /// Builds one ReflectionMethod array entry from interface metadata. fn reflection_interface_method_member( info: &InterfaceInfo, + interface_name: &str, method_name: &str, ) -> Option { let method_key = php_symbol_key(method_name); @@ -1490,8 +1566,15 @@ fn reflection_interface_method_member( .get(&method_key) .map(|sig| (sig, false)) .or_else(|| info.static_methods.get(&method_key).map(|sig| (sig, true)))?; + let declaring_class_name = info + .method_declaring_interfaces + .get(&method_key) + .or_else(|| info.static_method_declaring_interfaces.get(&method_key)) + .cloned() + .unwrap_or_else(|| interface_name.to_string()); Some(ReflectionListedMember { name: method_key, + declaring_class_name: Some(declaring_class_name), attr_names: Vec::new(), attr_args: Vec::new(), flags: reflection_member_flags(is_static, &Visibility::Public, false, true), @@ -1503,23 +1586,26 @@ fn reflection_interface_method_member( /// Builds ReflectionMethod array entries for methods declared by a trait. fn reflection_trait_method_members( methods: &std::collections::HashMap, + trait_name: &str, method_names: &[String], ) -> Vec { method_names .iter() - .filter_map(|method_name| reflection_trait_method_member(methods, method_name)) + .filter_map(|method_name| reflection_trait_method_member(methods, trait_name, method_name)) .collect() } /// Builds one ReflectionMethod array entry from retained trait metadata. fn reflection_trait_method_member( methods: &std::collections::HashMap, + trait_name: &str, method_name: &str, ) -> Option { let method_key = php_symbol_key(method_name); let info = methods.get(&method_key)?; Some(ReflectionListedMember { name: method_key, + declaring_class_name: Some(trait_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), flags: reflection_member_flags( @@ -1562,6 +1648,11 @@ fn reflection_class_property_member( })?; Some(ReflectionListedMember { name: property_name.to_string(), + declaring_class_name: reflection_property_declaring_class_name(info, property_name) + .or_else(|| { + (is_reflection_enum(ctx, class_name) && property_name == "name") + .then(|| class_name.to_string()) + }), attr_names: info .property_attribute_names .get(property_name) @@ -1582,12 +1673,13 @@ fn reflection_class_property_member( fn default_method_members( method_names: &[String], is_interface: bool, - _is_trait: bool, + declaring_class_name: &str, ) -> Vec { method_names .iter() .map(|name| ReflectionListedMember { name: name.clone(), + declaring_class_name: Some(declaring_class_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), flags: reflection_member_flags(false, &Visibility::Public, false, is_interface), @@ -1601,11 +1693,13 @@ fn default_method_members( fn default_property_members( property_names: &[String], is_interface: bool, + declaring_class_name: &str, ) -> Vec { property_names .iter() .map(|name| ReflectionListedMember { name: name.clone(), + declaring_class_name: Some(declaring_class_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), flags: reflection_member_flags(false, &Visibility::Public, false, is_interface), @@ -2200,13 +2294,18 @@ fn resolve_reflection_enum_case<'a>( ctx: &'a FunctionContext<'_>, enum_name: &str, case_name: &str, -) -> Option<&'a crate::types::EnumCaseInfo> { +) -> Option<(&'a str, &'a crate::types::EnumCaseInfo)> { let enum_key = php_symbol_key(enum_name.trim_start_matches('\\')); ctx.module .enum_infos .iter() .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == enum_key) - .and_then(|(_, info)| info.cases.iter().find(|case| case.name == case_name)) + .and_then(|(name, info)| { + info.cases + .iter() + .find(|case| case.name == case_name) + .map(|case| (name.as_str(), case)) + }) } /// Returns empty Reflection metadata for unsupported dynamic constructor operands. @@ -2598,6 +2697,43 @@ fn emit_reflection_parent_class_property( Ok(()) } +/// Replaces a member reflector's private declaring-class slot with `ReflectionClass|false`. +fn emit_reflection_declaring_class_property( + ctx: &mut FunctionContext<'_>, + member_class_name: &str, + declaring_class_name: Option<&str>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(member_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let Some(low_offset) = class_info.property_offsets.get("__declaring_class").copied() else { + return Ok(()); + }; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(declaring_class_name) = declaring_class_name { + let declaring_metadata = + reflection_shallow_class_metadata_for_name(ctx, declaring_class_name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &declaring_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionClass".to_string()), + ); + } else { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + /// Replaces a ReflectionMethod private array slot with ReflectionParameter objects. fn emit_reflection_parameter_array_property_by_name( ctx: &mut FunctionContext<'_>, @@ -2794,6 +2930,11 @@ fn emit_reflection_member_object( &member.attr_names, &member.attr_args, )?; + emit_reflection_declaring_class_property( + ctx, + member_class_name, + member.declaring_class_name.as_deref(), + )?; if member_class_name == "ReflectionMethod" { emit_reflection_parameter_array_property_by_name( ctx, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 26db52c5a0..8abf409d30 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1781,6 +1781,25 @@ fn builtin_reflection_owner_class( methods.push(builtin_reflection_class_string_method("getName", "__name")); } add_reflection_member_flag_methods(name, &mut properties, &mut methods); + if matches!( + name, + "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { + properties.push(builtin_property( + "__declaring_class", + Visibility::Private, + Some(mixed_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_mixed_method( + "getDeclaringClass", + "__declaring_class", + )); + } if matches!(name, "ReflectionFunction" | "ReflectionMethod") { properties.push(builtin_property( "__parameters", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bec668281e..dead016217 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6299,6 +6299,46 @@ echo $visibleProp->isPublic() ? "U" : "u";'); assert_eq!(out.stdout, "SPurfa:APs:FUs:SRp:sPu"); } +/// Verifies eval reflectors expose their declaring class through the bridge. +#[test] +fn test_eval_reflection_members_report_declaring_class() { + let out = compile_and_run_capture( + r#"getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getMethod("own")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionProperty("EvalDeclaringChild", "baseProp"))->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getProperty("childProp")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getReflectionConstant("BASE_CONST")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClassConstant("EvalDeclaringChild", "BASE_CONST"))->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass("EvalDeclaringEnum"))->getReflectionConstant("Ready")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionEnumBackedCase("EvalDeclaringEnum", "Ready"))->getDeclaringClass()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringBase:EvalDeclaringEnum:EvalDeclaringEnum" + ); +} + /// Verifies eval ReflectionClass getMethods/getProperties return member objects through the bridge. #[test] fn test_eval_reflection_class_lists_member_objects() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 137d7594b2..d095d56353 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1772,6 +1772,41 @@ echo $visibleProp->isPublic() ? "U" : "u"; assert_eq!(out, "SPurfa:APs:FUs:SRp:sPu"); } +/// Verifies member and enum-case reflectors expose their declaring class object. +#[test] +fn test_reflection_members_report_declaring_class() { + let out = compile_and_run( + r#"getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass(ReflectDeclaringChild::class))->getMethod("own")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionProperty(ReflectDeclaringChild::class, "baseProp"))->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass(ReflectDeclaringChild::class))->getProperty("childProp")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass(ReflectDeclaringChild::class))->getReflectionConstant("BASE_CONST")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClassConstant(ReflectDeclaringChild::class, "BASE_CONST"))->getDeclaringClass()->getName() . ":"; +echo (new ReflectionClass(ReflectDeclaringEnum::class))->getReflectionConstant("Ready")->getDeclaringClass()->getName() . ":"; +echo (new ReflectionEnumBackedCase(ReflectDeclaringEnum::class, "Ready"))->getDeclaringClass()->getName(); +"#, + ); + assert_eq!( + out, + "ReflectDeclaringBase:ReflectDeclaringChild:ReflectDeclaringBase:ReflectDeclaringChild:ReflectDeclaringBase:ReflectDeclaringBase:ReflectDeclaringEnum:ReflectDeclaringEnum" + ); +} + /// Verifies that `ReflectionClass::getMethods()` and `getProperties()` return /// populated ReflectionMethod/ReflectionProperty objects with member metadata. #[test] From aaf9c9dc215f69a4c7a83309fbd1500d900b9ed1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 13:19:03 +0200 Subject: [PATCH 0415/1208] Add ReflectionParameter declaring class metadata --- .../elephc-eval/src/interpreter/reflection.rs | 42 ++++++++----- .../tests/builtins_class_metadata.rs | 29 +++++++++ .../interpreter/tests/support/object_ops.rs | 1 + docs/php/classes.md | 5 +- docs/php/eval.md | 4 +- src/codegen/lower_inst/objects/reflection.rs | 60 ++++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 7 +++ tests/codegen/eval.rs | 25 ++++++++ tests/codegen/oop/attributes.rs | 31 ++++++++++ 9 files changed, 184 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 31fda063a5..3c5e1b5261 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -61,6 +61,7 @@ struct EvalReflectionMemberMetadata { /// Eval metadata needed to materialize one `ReflectionParameter` object. struct EvalReflectionParameterMetadata { name: String, + declaring_class_name: Option, attributes: Vec, position: usize, is_optional: bool, @@ -904,7 +905,12 @@ fn eval_reflection_parameter_object_result( let method_names = values.array_new(0)?; let property_names = values.array_new(0)?; let method_objects = values.array_new(0)?; - let parent_class = values.bool_value(false)?; + let parent_class = match parameter.declaring_class_name.as_deref() { + Some(declaring_class_name) => { + eval_reflection_shallow_class_object_result(declaring_class_name, context, values)? + } + None => values.null()?, + }; let type_value = match parameter.type_metadata.as_ref() { Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, None => values.null()?, @@ -1377,18 +1383,9 @@ fn eval_reflection_method_metadata( if context.has_class(class_name) || context.has_enum(class_name) { return context .class_method(class_name, method_name) - .map(|(declaring_class, method)| EvalReflectionMemberMetadata { - declaring_class_name: Some(declaring_class), - attributes: method.attributes().to_vec(), - visibility: method.visibility(), - is_static: method.is_static(), - is_final: method.is_final(), - is_abstract: method.is_abstract(), - required_parameter_count: eval_reflection_required_parameter_count( - method.parameter_defaults(), - method.parameter_is_variadic(), - ), - parameters: eval_reflection_parameters_from_names_and_type_flags( + .map(|(declaring_class, method)| { + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(declaring_class.as_str()), method.params(), method.parameter_has_types(), method.parameter_types(), @@ -1396,7 +1393,20 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), - ), + ); + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + required_parameter_count: eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ), + parameters, + } }); } if context.has_interface(class_name) { @@ -1416,6 +1426,7 @@ fn eval_reflection_method_metadata( method.parameter_is_variadic(), ), parameters: eval_reflection_parameters_from_names_and_type_flags( + Some(class_name), method.params(), method.parameter_has_types(), method.parameter_types(), @@ -1443,6 +1454,7 @@ fn eval_reflection_method_metadata( method.parameter_is_variadic(), ), parameters: eval_reflection_parameters_from_names_and_type_flags( + Some(trait_decl.name()), method.params(), method.parameter_has_types(), method.parameter_types(), @@ -1525,6 +1537,7 @@ fn eval_reflection_required_parameter_count( /// Builds parameter reflection metadata from eval parameter names and type flags. fn eval_reflection_parameters_from_names_and_type_flags( + declaring_class_name: Option<&str>, names: &[String], has_type_flags: &[bool], parameter_types: &[Option], @@ -1538,6 +1551,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( .enumerate() .map(|(position, name)| EvalReflectionParameterMetadata { name: name.clone(), + declaring_class_name: declaring_class_name.map(str::to_string), attributes: parameter_attributes .get(position) .cloned() diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 77b7d8bf3a..57405ced65 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -833,6 +833,35 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval ReflectionParameter exposes declaring class metadata. +#[test] +fn execute_program_reflects_eval_parameter_declaring_class() { + let program = parse_fragment( + br#"class EvalDeclaringParamBase { + public function inherited($base) {} +} +class EvalDeclaringParamChild extends EvalDeclaringParamBase { + public function own($child) {} +} +$inherited = (new ReflectionMethod("EvalDeclaringParamChild", "inherited"))->getParameters()[0]; +echo $inherited->getDeclaringClass()->getName(); echo ":"; +$listed = (new ReflectionMethod("EvalDeclaringParamChild", "own"))->getParameters()[0]; +echo $listed->getDeclaringClass()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalDeclaringParamBase:EvalDeclaringParamChild" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::getMethods preserves eval method parameter metadata. #[test] fn execute_program_reflection_class_lists_eval_method_parameters() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 1e41b14a6f..4b52840e07 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -524,6 +524,7 @@ impl FakeOps { | EVAL_REFLECTION_OWNER_CLASS_CONSTANT | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + | EVAL_REFLECTION_OWNER_PARAMETER ) { properties.push(("__declaring_class".to_string(), parent_class)); } diff --git a/docs/php/classes.md b/docs/php/classes.md index 9b82e388bd..2c8d2d6fc4 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1198,6 +1198,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, a `ReflectionIntersectionType` for intersection parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | | `ReflectionParameter::getAttributes()` | Same construction forms as `ReflectionParameter::hasType()` | Return `ReflectionAttribute` objects for method parameter attributes | +| `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | | `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null default values, or throw `ReflectionException` when no default is available | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | @@ -1281,7 +1282,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, declaring function metadata, parameter declaring-function/class metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, declaring function metadata, parameter declaring-function metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1309,4 +1310,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, scalar/null parameter defaults, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, scalar/null parameter defaults, parameter declaring-class metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/docs/php/eval.md b/docs/php/eval.md index c624434606..46aea2c7cf 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -210,7 +210,9 @@ declared-type presence, simple named type metadata through metadata is exposed through `ReflectionIntersectionType::getTypes()` and `allowsNull()`. Eval method parameter attributes are exposed through `ReflectionParameter::getAttributes()` using materialized `ReflectionAttribute` -objects. Defaulted eval method parameters are bound when omitted and reported through +objects. `ReflectionParameter::getDeclaringClass()` returns the declaring +class-like symbol for eval method parameters. Defaulted eval method parameters +are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and `getDefaultValue()`. Supported default expressions include scalar literals, arrays whose keys and values are supported default diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 3eaa5267d9..9d1c843c90 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -72,6 +72,7 @@ struct ReflectionListedMember { #[derive(Clone)] struct ReflectionParameterMember { name: String, + declaring_class_name: Option, attr_names: Vec, attr_args: Vec>>, position: i64, @@ -1521,6 +1522,10 @@ fn reflection_class_method_member( .get(&method_key) .or_else(|| info.static_methods.get(&method_key))?; let declaring_class_name = reflection_method_declaring_class_name(info, &method_key); + let parameters = reflection_parameter_members_with_declaring_class( + sig, + declaring_class_name.as_deref(), + ); Some(ReflectionListedMember { name: method_key.clone(), declaring_class_name, @@ -1536,7 +1541,7 @@ fn reflection_class_method_member( .unwrap_or_default(), flags: reflection_method_member_flags(info, &method_key)?, required_parameter_count: reflection_required_parameter_count(sig), - parameters: reflection_parameter_members(sig), + parameters, }) } @@ -1572,6 +1577,8 @@ fn reflection_interface_method_member( .or_else(|| info.static_method_declaring_interfaces.get(&method_key)) .cloned() .unwrap_or_else(|| interface_name.to_string()); + let parameters = + reflection_parameter_members_with_declaring_class(sig, Some(declaring_class_name.as_str())); Some(ReflectionListedMember { name: method_key, declaring_class_name: Some(declaring_class_name), @@ -1579,7 +1586,7 @@ fn reflection_interface_method_member( attr_args: Vec::new(), flags: reflection_member_flags(is_static, &Visibility::Public, false, true), required_parameter_count: reflection_required_parameter_count(sig), - parameters: reflection_parameter_members(sig), + parameters, }) } @@ -1615,7 +1622,10 @@ fn reflection_trait_method_member( info.is_abstract, ), required_parameter_count: reflection_required_parameter_count(&info.signature), - parameters: reflection_parameter_members(&info.signature), + parameters: reflection_parameter_members_with_declaring_class( + &info.signature, + Some(trait_name), + ), }) } @@ -1727,6 +1737,14 @@ fn reflection_required_parameter_count(sig: &FunctionSig) -> i64 { /// Builds reflected parameter metadata from a method/function signature. fn reflection_parameter_members(sig: &FunctionSig) -> Vec { + reflection_parameter_members_with_declaring_class(sig, None) +} + +/// Builds reflected parameter metadata and attaches declaring class metadata when present. +fn reflection_parameter_members_with_declaring_class( + sig: &FunctionSig, + declaring_class_name: Option<&str>, +) -> Vec { sig.params .iter() .enumerate() @@ -1735,6 +1753,7 @@ fn reflection_parameter_members(sig: &FunctionSig) -> Vec, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let declaring_class_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__declaring_class")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(declaring_class_name) = parameter.declaring_class_name.as_deref() { + let declaring_metadata = + reflection_shallow_class_metadata_for_name(ctx, declaring_class_name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &declaring_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionClass".to_string()), + ); + } else { + emit_boxed_null_literal_to_result(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, declaring_class_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, declaring_class_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); Ok(()) } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 8abf409d30..16726df3b1 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -596,6 +596,12 @@ fn builtin_reflection_parameter() -> FlattenedClass { Some(mixed_type()), null_lit(), ), + builtin_property( + "__declaring_class", + Visibility::Private, + Some(mixed_type()), + null_lit(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![ @@ -620,6 +626,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { TypeExpr::Bool, ), builtin_reflection_parameter_get_default_value_method(), + builtin_reflection_slot_getter("getDeclaringClass", "__declaring_class", mixed_type()), ], attributes: Vec::new(), constants: Vec::new(), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index dead016217..c05c1aa583 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6506,6 +6506,31 @@ foreach ($params as $param) { ); } +/// Verifies eval ReflectionParameter exposes the declaring class for method parameters. +#[test] +fn test_eval_reflection_parameter_reports_declaring_class() { + let out = compile_and_run_capture( + r#"getParameters()[0]; +echo $inherited->getDeclaringClass()->getName() . ":"; +$listed = (new ReflectionMethod("EvalDeclaringParamChild", "own"))->getParameters()[0]; +echo $listed->getDeclaringClass()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalDeclaringParamBase:EvalDeclaringParamChild"); +} + /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. #[test] fn test_eval_reflection_class_new_instance_constructs_eval_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index d095d56353..650f50007c 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1964,6 +1964,37 @@ echo ($traitParams[2]->hasType() ? "T" : "t"); ); } +/// Verifies `ReflectionParameter::getDeclaringClass()` reports method owners and null for functions. +#[test] +fn test_reflection_parameter_get_declaring_class_reports_method_owner() { + let out = compile_and_run_capture( + r#"getDeclaringClass()->getName() . ":"; +$listed = (new ReflectionMethod(ReflectDeclaringParamChild::class, "own"))->getParameters()[0]; +echo $listed->getDeclaringClass()->getName() . ":"; +$function = new ReflectionParameter("reflect_declaring_function", "value"); +echo is_null($function->getDeclaringClass()) ? "null" : "bad"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectDeclaringParamBase:ReflectDeclaringParamChild:null" + ); +} + /// Verifies that `ReflectionParameter::getType()` returns named, union, and intersection metadata. #[test] fn test_reflection_parameter_get_type_returns_named_type_metadata() { From dafd64db23a1c6e522bda7168c2177aa8320deaf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 13:29:14 +0200 Subject: [PATCH 0416/1208] Add ReflectionParameter declaring function metadata --- .../elephc-eval/src/interpreter/reflection.rs | 150 +++++++++--- .../tests/builtins_class_metadata.rs | 7 +- .../interpreter/tests/support/object_ops.rs | 5 + docs/php/classes.md | 5 +- docs/php/eval.md | 6 +- src/codegen/eval_reflection_owner_helpers.rs | 52 +++++ src/codegen/lower_inst/objects/reflection.rs | 213 +++++++++++++++--- src/types/checker/builtin_types/reflection.rs | 11 + tests/codegen/eval.rs | 10 +- tests/codegen/oop/attributes.rs | 6 +- 10 files changed, 395 insertions(+), 70 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 3c5e1b5261..40a9d70a35 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -62,6 +62,7 @@ struct EvalReflectionMemberMetadata { struct EvalReflectionParameterMetadata { name: String, declaring_class_name: Option, + declaring_function: Option, attributes: Vec, position: usize, is_optional: bool, @@ -72,6 +73,16 @@ struct EvalReflectionParameterMetadata { default_value: Option, } +/// Eval metadata needed for `ReflectionParameter::getDeclaringFunction()`. +#[derive(Clone)] +struct EvalReflectionDeclaringFunctionMetadata { + name: String, + declaring_class_name: Option, + attributes: Vec, + flags: u64, + required_parameter_count: usize, +} + /// Eval metadata needed to materialize one parameter `ReflectionType` object. struct EvalReflectionParameterTypeMetadata { kind: EvalReflectionParameterTypeKind, @@ -900,7 +911,12 @@ fn eval_reflection_parameter_object_result( values: &mut impl RuntimeValueOps, ) -> Result { let attrs = eval_reflection_attribute_array_result(¶meter.attributes, context, values)?; - let interface_names = values.array_new(0)?; + let declaring_function = match parameter.declaring_function.as_ref() { + Some(metadata) => { + eval_reflection_declaring_function_object_result(metadata, context, values)? + } + None => values.null()?, + }; let trait_names = values.array_new(0)?; let method_names = values.array_new(0)?; let property_names = values.array_new(0)?; @@ -924,7 +940,7 @@ fn eval_reflection_parameter_object_result( EVAL_REFLECTION_OWNER_PARAMETER, ¶meter.name, attrs, - interface_names, + declaring_function, trait_names, method_names, property_names, @@ -935,7 +951,7 @@ fn eval_reflection_parameter_object_result( parameter.position as u64, )?; values.release(attrs)?; - values.release(interface_names)?; + values.release(declaring_function)?; values.release(trait_names)?; values.release(method_names)?; values.release(property_names)?; @@ -946,6 +962,29 @@ fn eval_reflection_parameter_object_result( Ok(object) } +/// Builds a shallow ReflectionMethod object for a parameter's declaring function metadata. +fn eval_reflection_declaring_function_object_result( + metadata: &EvalReflectionDeclaringFunctionMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_METHOD, + &metadata.name, + &metadata.attributes, + &[], + &[], + &[], + &[], + metadata.declaring_class_name.as_deref(), + &[], + metadata.flags, + metadata.required_parameter_count as u64, + context, + values, + ) +} + /// Materializes one parameter ReflectionType object through the shared reflection helper. fn eval_reflection_type_object_result( type_metadata: &EvalReflectionParameterTypeMetadata, @@ -1384,8 +1423,26 @@ fn eval_reflection_method_metadata( return context .class_method(class_name, method_name) .map(|(declaring_class, method)| { + let required_parameter_count = eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ); + let flags = eval_reflection_member_flags( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + ); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(declaring_class.clone()), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; let parameters = eval_reflection_parameters_from_names_and_type_flags( Some(declaring_class.as_str()), + Some(&declaring_function), method.params(), method.parameter_has_types(), method.parameter_types(), @@ -1401,10 +1458,7 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), - required_parameter_count: eval_reflection_required_parameter_count( - method.parameter_defaults(), - method.parameter_is_variadic(), - ), + required_parameter_count, parameters, } }); @@ -1414,19 +1468,27 @@ fn eval_reflection_method_metadata( .interface_method_requirements(class_name) .into_iter() .find(|method| method.name().eq_ignore_ascii_case(method_name)) - .map(|method| EvalReflectionMemberMetadata { - declaring_class_name: Some(class_name.to_string()), - attributes: method.attributes().to_vec(), - visibility: EvalVisibility::Public, - is_static: method.is_static(), - is_final: false, - is_abstract: true, - required_parameter_count: eval_reflection_required_parameter_count( + .map(|method| { + let required_parameter_count = eval_reflection_required_parameter_count( method.parameter_defaults(), method.parameter_is_variadic(), - ), - parameters: eval_reflection_parameters_from_names_and_type_flags( + ); + let flags = eval_reflection_member_flags( + EvalVisibility::Public, + method.is_static(), + false, + true, + ); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(class_name.to_string()), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let parameters = eval_reflection_parameters_from_names_and_type_flags( Some(class_name), + Some(&declaring_function), method.params(), method.parameter_has_types(), method.parameter_types(), @@ -1434,7 +1496,17 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), - ), + ); + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.to_string()), + attributes: method.attributes().to_vec(), + visibility: EvalVisibility::Public, + is_static: method.is_static(), + is_final: false, + is_abstract: true, + required_parameter_count, + parameters, + } }); } context.trait_decl(class_name).and_then(|trait_decl| { @@ -1442,19 +1514,27 @@ fn eval_reflection_method_metadata( .methods() .iter() .find(|method| method.name().eq_ignore_ascii_case(method_name)) - .map(|method| EvalReflectionMemberMetadata { - declaring_class_name: Some(trait_decl.name().to_string()), - attributes: method.attributes().to_vec(), - visibility: method.visibility(), - is_static: method.is_static(), - is_final: method.is_final(), - is_abstract: method.is_abstract(), - required_parameter_count: eval_reflection_required_parameter_count( + .map(|method| { + let required_parameter_count = eval_reflection_required_parameter_count( method.parameter_defaults(), method.parameter_is_variadic(), - ), - parameters: eval_reflection_parameters_from_names_and_type_flags( + ); + let flags = eval_reflection_member_flags( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + ); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(trait_decl.name().to_string()), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let parameters = eval_reflection_parameters_from_names_and_type_flags( Some(trait_decl.name()), + Some(&declaring_function), method.params(), method.parameter_has_types(), method.parameter_types(), @@ -1462,7 +1542,17 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), - ), + ); + EvalReflectionMemberMetadata { + declaring_class_name: Some(trait_decl.name().to_string()), + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + required_parameter_count, + parameters, + } }) }) } @@ -1538,6 +1628,7 @@ fn eval_reflection_required_parameter_count( /// Builds parameter reflection metadata from eval parameter names and type flags. fn eval_reflection_parameters_from_names_and_type_flags( declaring_class_name: Option<&str>, + declaring_function: Option<&EvalReflectionDeclaringFunctionMetadata>, names: &[String], has_type_flags: &[bool], parameter_types: &[Option], @@ -1552,6 +1643,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( .map(|(position, name)| EvalReflectionParameterMetadata { name: name.clone(), declaring_class_name: declaring_class_name.map(str::to_string), + declaring_function: declaring_function.cloned(), attributes: parameter_attributes .get(position) .cloned() diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 57405ced65..3ee2303c0f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -845,8 +845,11 @@ class EvalDeclaringParamChild extends EvalDeclaringParamBase { } $inherited = (new ReflectionMethod("EvalDeclaringParamChild", "inherited"))->getParameters()[0]; echo $inherited->getDeclaringClass()->getName(); echo ":"; +echo $inherited->getDeclaringFunction()->getName(); echo ":"; +echo $inherited->getDeclaringFunction()->getDeclaringClass()->getName(); echo ":"; $listed = (new ReflectionMethod("EvalDeclaringParamChild", "own"))->getParameters()[0]; -echo $listed->getDeclaringClass()->getName(); +echo $listed->getDeclaringClass()->getName(); echo ":"; +echo $listed->getDeclaringFunction()->getName(); return true;"#, ) .expect("parse eval fragment"); @@ -857,7 +860,7 @@ return true;"#, assert_eq!( values.output, - "EvalDeclaringParamBase:EvalDeclaringParamChild" + "EvalDeclaringParamBase:inherited:EvalDeclaringParamBase:EvalDeclaringParamChild:own" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 4b52840e07..2e60e9cea2 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -162,6 +162,10 @@ impl FakeOps { Self::object_property(&properties, "__declaring_class") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "getdeclaringfunction") if args.is_empty() => { + Self::object_property(&properties, "__declaring_function") + .map_or_else(|| self.null(), Ok) + } (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) } @@ -560,6 +564,7 @@ impl FakeOps { properties.push(("__type".to_string(), method_objects)); properties.push(("__has_default_value".to_string(), has_default_value)); properties.push(("__default_value".to_string(), property_objects)); + properties.push(("__declaring_function".to_string(), interface_names)); } if owner_kind == EVAL_REFLECTION_OWNER_NAMED_TYPE { let allows_null = self.bool_value((flags & 1) != 0)?; diff --git a/docs/php/classes.md b/docs/php/classes.md index 2c8d2d6fc4..edc1b74827 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1199,6 +1199,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, a `ReflectionIntersectionType` for intersection parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | | `ReflectionParameter::getAttributes()` | Same construction forms as `ReflectionParameter::hasType()` | Return `ReflectionAttribute` objects for method parameter attributes | | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | +| `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | | `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null default values, or throw `ReflectionException` when no default is available | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | @@ -1282,7 +1283,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, declaring function metadata, parameter declaring-function metadata, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants @@ -1310,4 +1311,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, scalar/null parameter defaults, parameter declaring-class metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, scalar/null parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/docs/php/eval.md b/docs/php/eval.md index 46aea2c7cf..fecf4ab9f7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -211,8 +211,10 @@ metadata is exposed through `ReflectionIntersectionType::getTypes()` and `allowsNull()`. Eval method parameter attributes are exposed through `ReflectionParameter::getAttributes()` using materialized `ReflectionAttribute` objects. `ReflectionParameter::getDeclaringClass()` returns the declaring -class-like symbol for eval method parameters. Defaulted eval method parameters -are bound when omitted and reported through +class-like symbol for eval method parameters, and +`ReflectionParameter::getDeclaringFunction()` returns a `ReflectionMethod` +object for the declaring eval method. Defaulted eval method parameters are bound +when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and `getDefaultValue()`. Supported default expressions include scalar literals, arrays whose keys and values are supported default diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 6779050e36..61e1399086 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -90,6 +90,8 @@ struct ReflectionOwnerLayout { has_default_value_hi: Option, default_value_lo: Option, default_value_hi: Option, + declaring_function_lo: Option, + declaring_function_hi: Option, allows_null_lo: Option, allows_null_hi: Option, is_builtin_lo: Option, @@ -226,6 +228,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, + declaring_function: Option, attr_names: Vec, attr_args: Vec>>, position: i64, @@ -84,6 +85,25 @@ struct ReflectionParameterMember { default_value: Option, } +/// Metadata needed for `ReflectionParameter::getDeclaringFunction()`. +#[derive(Clone)] +enum ReflectionDeclaringFunctionMember { + Function { + name: String, + attr_names: Vec, + attr_args: Vec>>, + required_parameter_count: i64, + }, + Method { + name: String, + declaring_class_name: Option, + attr_names: Vec, + attr_args: Vec>>, + flags: ReflectionMemberFlags, + required_parameter_count: i64, + }, +} + /// Metadata for one `ReflectionType` object returned by `ReflectionParameter::getType()`. #[derive(Clone)] enum ReflectionParameterTypeMetadata { @@ -573,12 +593,21 @@ fn reflection_function_metadata( let Some(signature) = function.signature.as_ref() else { return Ok(empty_reflection_metadata()); }; + let reflected_name = function.name.trim_start_matches('\\').to_string(); + let required_parameter_count = reflection_required_parameter_count(signature); + let declaring_function = ReflectionDeclaringFunctionMember::Function { + name: reflected_name.clone(), + attr_names: function.attribute_names.clone(), + attr_args: function.attribute_args.clone(), + required_parameter_count, + }; let mut metadata = empty_reflection_metadata(); - metadata.reflected_name = Some(function.name.trim_start_matches('\\').to_string()); + metadata.reflected_name = Some(reflected_name); metadata.attr_names = function.attribute_names.clone(); metadata.attr_args = function.attribute_args.clone(); - metadata.parameter_members = reflection_parameter_members(signature); - metadata.required_parameter_count = reflection_required_parameter_count(signature); + metadata.parameter_members = + reflection_parameter_members_with_declaring_function(signature, None, Some(declaring_function)); + metadata.required_parameter_count = required_parameter_count; Ok(metadata) } @@ -756,7 +785,15 @@ fn reflection_function_parameter_metadata( let Some(signature) = function.signature.as_ref() else { return Ok(empty_reflection_metadata()); }; - let parameters = reflection_parameter_members(signature); + let reflected_name = function.name.trim_start_matches('\\').to_string(); + let declaring_function = ReflectionDeclaringFunctionMember::Function { + name: reflected_name, + attr_names: function.attribute_names.clone(), + attr_args: function.attribute_args.clone(), + required_parameter_count: reflection_required_parameter_count(signature), + }; + let parameters = + reflection_parameter_members_with_declaring_function(signature, None, Some(declaring_function)); let Some(parameter) = reflection_parameter_member_for_selector(¶meters, selector) else { return Ok(empty_reflection_metadata()); }; @@ -1522,25 +1559,38 @@ fn reflection_class_method_member( .get(&method_key) .or_else(|| info.static_methods.get(&method_key))?; let declaring_class_name = reflection_method_declaring_class_name(info, &method_key); + let attr_names = info + .method_attribute_names + .get(&method_key) + .cloned() + .unwrap_or_default(); + let attr_args = info + .method_attribute_args + .get(&method_key) + .cloned() + .unwrap_or_default(); + let flags = reflection_method_member_flags(info, &method_key)?; + let required_parameter_count = reflection_required_parameter_count(sig); + let declaring_function = ReflectionDeclaringFunctionMember::Method { + name: method_key.clone(), + declaring_class_name: declaring_class_name.clone(), + attr_names: attr_names.clone(), + attr_args: attr_args.clone(), + flags, + required_parameter_count, + }; let parameters = reflection_parameter_members_with_declaring_class( sig, declaring_class_name.as_deref(), + Some(declaring_function), ); Some(ReflectionListedMember { name: method_key.clone(), declaring_class_name, - attr_names: info - .method_attribute_names - .get(&method_key) - .cloned() - .unwrap_or_default(), - attr_args: info - .method_attribute_args - .get(&method_key) - .cloned() - .unwrap_or_default(), - flags: reflection_method_member_flags(info, &method_key)?, - required_parameter_count: reflection_required_parameter_count(sig), + attr_names, + attr_args, + flags, + required_parameter_count, parameters, }) } @@ -1577,15 +1627,28 @@ fn reflection_interface_method_member( .or_else(|| info.static_method_declaring_interfaces.get(&method_key)) .cloned() .unwrap_or_else(|| interface_name.to_string()); - let parameters = - reflection_parameter_members_with_declaring_class(sig, Some(declaring_class_name.as_str())); + let required_parameter_count = reflection_required_parameter_count(sig); + let flags = reflection_member_flags(is_static, &Visibility::Public, false, true); + let declaring_function = ReflectionDeclaringFunctionMember::Method { + name: method_key.clone(), + declaring_class_name: Some(declaring_class_name.clone()), + attr_names: Vec::new(), + attr_args: Vec::new(), + flags, + required_parameter_count, + }; + let parameters = reflection_parameter_members_with_declaring_class( + sig, + Some(declaring_class_name.as_str()), + Some(declaring_function), + ); Some(ReflectionListedMember { name: method_key, declaring_class_name: Some(declaring_class_name), attr_names: Vec::new(), attr_args: Vec::new(), - flags: reflection_member_flags(is_static, &Visibility::Public, false, true), - required_parameter_count: reflection_required_parameter_count(sig), + flags, + required_parameter_count, parameters, }) } @@ -1610,21 +1673,32 @@ fn reflection_trait_method_member( ) -> Option { let method_key = php_symbol_key(method_name); let info = methods.get(&method_key)?; + let flags = reflection_member_flags( + info.is_static, + &info.visibility, + info.is_final, + info.is_abstract, + ); + let required_parameter_count = reflection_required_parameter_count(&info.signature); + let declaring_function = ReflectionDeclaringFunctionMember::Method { + name: method_key.clone(), + declaring_class_name: Some(trait_name.to_string()), + attr_names: Vec::new(), + attr_args: Vec::new(), + flags, + required_parameter_count, + }; Some(ReflectionListedMember { name: method_key, declaring_class_name: Some(trait_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), - flags: reflection_member_flags( - info.is_static, - &info.visibility, - info.is_final, - info.is_abstract, - ), - required_parameter_count: reflection_required_parameter_count(&info.signature), + flags, + required_parameter_count, parameters: reflection_parameter_members_with_declaring_class( &info.signature, Some(trait_name), + Some(declaring_function), ), }) } @@ -1735,15 +1809,24 @@ fn reflection_required_parameter_count(sig: &FunctionSig) -> i64 { .map_or(0, |index| index as i64 + 1) } -/// Builds reflected parameter metadata from a method/function signature. -fn reflection_parameter_members(sig: &FunctionSig) -> Vec { - reflection_parameter_members_with_declaring_class(sig, None) -} - /// Builds reflected parameter metadata and attaches declaring class metadata when present. fn reflection_parameter_members_with_declaring_class( sig: &FunctionSig, declaring_class_name: Option<&str>, + declaring_function: Option, +) -> Vec { + reflection_parameter_members_with_declaring_function( + sig, + declaring_class_name, + declaring_function, + ) +} + +/// Builds reflected parameter metadata with optional declaring owner metadata. +fn reflection_parameter_members_with_declaring_function( + sig: &FunctionSig, + declaring_class_name: Option<&str>, + declaring_function: Option, ) -> Vec { sig.params .iter() @@ -1754,6 +1837,7 @@ fn reflection_parameter_members_with_declaring_class( ReflectionParameterMember { name: name.clone(), declaring_class_name: declaring_class_name.map(str::to_string), + declaring_function: declaring_function.clone(), attr_names: sig .param_attributes .get(index) @@ -3056,6 +3140,71 @@ fn emit_reflection_parameter_properties( )?; emit_reflection_parameter_default_property(ctx, parameter)?; emit_reflection_parameter_declaring_class_property(ctx, parameter)?; + emit_reflection_parameter_declaring_function_property(ctx, parameter)?; + Ok(()) +} + +/// Writes one ReflectionParameter object's declaring-function slot. +fn emit_reflection_parameter_declaring_function_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let declaring_function_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__declaring_function")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + match parameter.declaring_function.as_ref() { + Some(ReflectionDeclaringFunctionMember::Function { + name, + attr_names, + attr_args, + required_parameter_count, + }) => { + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(name.clone()); + metadata.attr_names = attr_names.clone(); + metadata.attr_args = attr_args.clone(); + metadata.required_parameter_count = *required_parameter_count; + emit_reflection_owner_object(ctx, "ReflectionFunction", &metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionFunction".to_string()), + ); + } + Some(ReflectionDeclaringFunctionMember::Method { + name, + declaring_class_name, + attr_names, + attr_args, + flags, + required_parameter_count, + }) => { + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(name.clone()); + metadata.parent_class_name = declaring_class_name.clone(); + metadata.attr_names = attr_names.clone(); + metadata.attr_args = attr_args.clone(); + metadata.member_flags = *flags; + metadata.required_parameter_count = *required_parameter_count; + emit_reflection_owner_object(ctx, "ReflectionMethod", &metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionMethod".to_string()), + ); + } + None => emit_boxed_null_literal_to_result(ctx), + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, declaring_function_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, declaring_function_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); Ok(()) } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 16726df3b1..136a3bb8b8 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -602,6 +602,12 @@ fn builtin_reflection_parameter() -> FlattenedClass { Some(mixed_type()), null_lit(), ), + builtin_property( + "__declaring_function", + Visibility::Private, + Some(mixed_type()), + null_lit(), + ), ], methods: vec![ builtin_reflection_owner_constructor_method(vec![ @@ -627,6 +633,11 @@ fn builtin_reflection_parameter() -> FlattenedClass { ), builtin_reflection_parameter_get_default_value_method(), builtin_reflection_slot_getter("getDeclaringClass", "__declaring_class", mixed_type()), + builtin_reflection_slot_getter( + "getDeclaringFunction", + "__declaring_function", + mixed_type(), + ), ], attributes: Vec::new(), constants: Vec::new(), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c05c1aa583..a5353374f6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6519,8 +6519,11 @@ class EvalDeclaringParamChild extends EvalDeclaringParamBase { } $inherited = (new ReflectionMethod("EvalDeclaringParamChild", "inherited"))->getParameters()[0]; echo $inherited->getDeclaringClass()->getName() . ":"; +echo $inherited->getDeclaringFunction()->getName() . ":"; +echo $inherited->getDeclaringFunction()->getDeclaringClass()->getName() . ":"; $listed = (new ReflectionMethod("EvalDeclaringParamChild", "own"))->getParameters()[0]; -echo $listed->getDeclaringClass()->getName();'); +echo $listed->getDeclaringClass()->getName() . ":"; +echo $listed->getDeclaringFunction()->getName();'); "#, ); assert!( @@ -6528,7 +6531,10 @@ echo $listed->getDeclaringClass()->getName();'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "EvalDeclaringParamBase:EvalDeclaringParamChild"); + assert_eq!( + out.stdout, + "EvalDeclaringParamBase:inherited:EvalDeclaringParamBase:EvalDeclaringParamChild:own" + ); } /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 650f50007c..777b832578 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1978,9 +1978,13 @@ class ReflectDeclaringParamChild extends ReflectDeclaringParamBase { } $inherited = new ReflectionParameter([ReflectDeclaringParamChild::class, "inherited"], "base"); echo $inherited->getDeclaringClass()->getName() . ":"; +echo $inherited->getDeclaringFunction()->getName() . ":"; +echo $inherited->getDeclaringFunction()->getDeclaringClass()->getName() . ":"; $listed = (new ReflectionMethod(ReflectDeclaringParamChild::class, "own"))->getParameters()[0]; echo $listed->getDeclaringClass()->getName() . ":"; +echo $listed->getDeclaringFunction()->getName() . ":"; $function = new ReflectionParameter("reflect_declaring_function", "value"); +echo $function->getDeclaringFunction()->getName() . ":"; echo is_null($function->getDeclaringClass()) ? "null" : "bad"; "#, ); @@ -1991,7 +1995,7 @@ echo is_null($function->getDeclaringClass()) ? "null" : "bad"; ); assert_eq!( out.stdout, - "ReflectDeclaringParamBase:ReflectDeclaringParamChild:null" + "ReflectDeclaringParamBase:inherited:ReflectDeclaringParamBase:ReflectDeclaringParamChild:own:reflect_declaring_function:null" ); } From 5bf4f449dff44130bbf3271c60cd89a549276835 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 13:44:37 +0200 Subject: [PATCH 0417/1208] Add ReflectionProperty readonly metadata --- .../elephc-eval/src/interpreter/reflection.rs | 34 ++++++++++++++++--- .../tests/builtins_class_metadata.rs | 15 +++++++- .../interpreter/tests/support/object_ops.rs | 6 ++++ docs/php/classes.md | 3 +- docs/php/eval.md | 4 +-- src/codegen/eval_reflection_owner_helpers.rs | 17 ++++++++++ src/codegen/lower_inst/objects/reflection.rs | 30 ++++++++++++---- src/types/checker/builtin_types/reflection.rs | 12 +++++++ tests/codegen/eval.rs | 17 ++++++++-- tests/codegen/oop/attributes.rs | 15 +++++++- 10 files changed, 135 insertions(+), 18 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 40a9d70a35..25b3c829c8 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -25,6 +25,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -54,6 +55,7 @@ struct EvalReflectionMemberMetadata { is_static: bool, is_final: bool, is_abstract: bool, + is_readonly: bool, required_parameter_count: usize, parameters: Vec, } @@ -381,6 +383,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( member.is_static, member.is_final, member.is_abstract, + member.is_readonly, ); eval_reflection_owner_object( owner_kind, @@ -544,6 +547,7 @@ fn eval_reflection_method_new( method.is_static, method.is_final, method.is_abstract, + false, ); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_METHOD, @@ -580,7 +584,13 @@ fn eval_reflection_property_new( let property_name = eval_reflection_string_arg(args[1], values)?; let property = eval_reflection_property_metadata(&class_name, &property_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - let flags = eval_reflection_member_flags(property.visibility, property.is_static, false, false); + let flags = eval_reflection_member_flags( + property.visibility, + property.is_static, + false, + false, + property.is_readonly, + ); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_PROPERTY, &property_name, @@ -1153,6 +1163,7 @@ fn eval_reflection_member_object_array_result( member.is_static, member.is_final, member.is_abstract, + member.is_readonly, ); let member_object = eval_reflection_owner_object( owner_kind, @@ -1432,6 +1443,7 @@ fn eval_reflection_method_metadata( method.is_static(), method.is_final(), method.is_abstract(), + false, ); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), @@ -1458,6 +1470,7 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), + is_readonly: false, required_parameter_count, parameters, } @@ -1478,6 +1491,7 @@ fn eval_reflection_method_metadata( method.is_static(), false, true, + false, ); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), @@ -1504,6 +1518,7 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: false, is_abstract: true, + is_readonly: false, required_parameter_count, parameters, } @@ -1524,6 +1539,7 @@ fn eval_reflection_method_metadata( method.is_static(), method.is_final(), method.is_abstract(), + false, ); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), @@ -1550,6 +1566,7 @@ fn eval_reflection_method_metadata( is_static: method.is_static(), is_final: method.is_final(), is_abstract: method.is_abstract(), + is_readonly: false, required_parameter_count, parameters, } @@ -1564,18 +1581,19 @@ fn eval_reflection_property_metadata( context: &ElephcEvalContext, ) -> Option { if context.has_class(class_name) || context.has_enum(class_name) { - return context - .class_property(class_name, property_name) - .map(|(declaring_class, property)| EvalReflectionMemberMetadata { + return context.class_property(class_name, property_name).map( + |(declaring_class, property)| EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class), attributes: property.attributes().to_vec(), visibility: property.visibility(), is_static: property.is_static(), is_final: false, is_abstract: property.is_abstract(), + is_readonly: property.is_readonly(), required_parameter_count: 0, parameters: Vec::new(), - }); + }, + ); } if context.has_interface(class_name) { return context @@ -1589,6 +1607,7 @@ fn eval_reflection_property_metadata( is_static: false, is_final: false, is_abstract: true, + is_readonly: false, required_parameter_count: 0, parameters: Vec::new(), }); @@ -1605,6 +1624,7 @@ fn eval_reflection_property_metadata( is_static: property.is_static(), is_final: false, is_abstract: property.is_abstract(), + is_readonly: property.is_readonly(), required_parameter_count: 0, parameters: Vec::new(), }) @@ -1756,6 +1776,7 @@ fn eval_reflection_member_flags( is_static: bool, is_final: bool, is_abstract: bool, + is_readonly: bool, ) -> u64 { let mut flags = 0; if is_static { @@ -1772,6 +1793,9 @@ fn eval_reflection_member_flags( if is_abstract { flags |= EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT; } + if is_readonly { + flags |= EVAL_REFLECTION_MEMBER_FLAG_READONLY; + } flags } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 3ee2303c0f..3e055e5c77 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -679,10 +679,14 @@ fn execute_program_reflects_eval_member_predicates() { abstract protected function mustImplement(); final public function locked() {} } +readonly class EvalReflectReadonlyClass { + public int $classReadonly; +} class EvalReflectMemberChild extends EvalReflectMemberBase { public function mustImplement() {} private static $token; protected $visible; + public readonly int $locked; } $baseStatic = new ReflectionMethod("EvalReflectMemberChild", "baseStatic"); echo $baseStatic->isStatic() ? "S" : "s"; @@ -706,11 +710,20 @@ $staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); echo $staticProp->isStatic() ? "S" : "s"; echo $staticProp->isPrivate() ? "R" : "r"; echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isReadOnly() ? "R" : "r"; echo ":"; $visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); echo $visibleProp->isStatic() ? "S" : "s"; echo $visibleProp->isProtected() ? "P" : "p"; echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isReadOnly() ? "R" : "r"; +echo ":"; +$readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); +echo $readonlyProp->isReadOnly() ? "R" : "r"; +echo $readonlyProp->isPublic() ? "U" : "u"; +echo ":"; +$classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); +echo $classReadonlyProp->isReadOnly() ? "C" : "c"; return true;"#, ) .expect("parse eval fragment"); @@ -719,7 +732,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "SPurfa:APs:FUs:SRp:sPu"); + assert_eq!(values.output, "SPurfa:APs:FUs:SRpr:sPur:RU:C"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 2e60e9cea2..1c2201108a 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -15,6 +15,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -520,6 +521,11 @@ impl FakeOps { properties.push(("__is_public".to_string(), is_public)); properties.push(("__is_protected".to_string(), is_protected)); properties.push(("__is_private".to_string(), is_private)); + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + let is_readonly = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY) != 0)?; + properties.push(("__is_readonly".to_string(), is_readonly)); + } } if matches!( owner_kind, diff --git a/docs/php/classes.md b/docs/php/classes.md index edc1b74827..1c9691dc19 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1212,6 +1212,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isPublic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is public | | `ReflectionProperty::isProtected()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is protected | | `ReflectionProperty::isPrivate()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is private | +| `ReflectionProperty::isReadOnly()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is readonly | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | @@ -1283,7 +1284,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, and `isPrivate()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, and `isReadOnly()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index fecf4ab9f7..efbb35d010 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -233,8 +233,8 @@ direct variable, array-element, and object-property arguments, write back fixed parameters after method execution, write back mutated `&...$items` elements when the variadic container itself is not rebound, and are reported through `ReflectionParameter::isPassedByReference()`. -`ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, and -`isPrivate()` report eval property metadata. +`ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, +and `isReadOnly()` report eval property metadata. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 61e1399086..9e21d50139 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1095,6 +1095,14 @@ fn emit_set_owner_member_flags_property_aarch64( emitter.instruction("and x10, x10, #1"); // extract the private-member flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_private_lo); abi::emit_store_zero_to_address(emitter, "x9", is_private_hi); + if let (Some(is_readonly_lo), Some(is_readonly_hi)) = + (layout.is_readonly_lo, layout.is_readonly_hi) + { + emitter.instruction("lsr x10, x11, #6"); // move the readonly-property bit into position + emitter.instruction("and x10, x10, #1"); // extract the readonly-property flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_readonly_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); + } let (Some(is_final_lo), Some(is_final_hi), Some(is_abstract_lo), Some(is_abstract_hi)) = ( layout.is_final_lo, layout.is_final_hi, @@ -1161,6 +1169,15 @@ fn emit_set_owner_member_flags_property_x86_64( emitter.instruction("and rax, 1"); // extract the private-member flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_private_lo); abi::emit_store_zero_to_address(emitter, "r10", is_private_hi); + if let (Some(is_readonly_lo), Some(is_readonly_hi)) = + (layout.is_readonly_lo, layout.is_readonly_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the readonly bit + emitter.instruction("shr rax, 6"); // move the readonly-property bit into position + emitter.instruction("and rax, 1"); // extract the readonly-property flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_readonly_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); + } let (Some(is_final_lo), Some(is_final_hi), Some(is_abstract_lo), Some(is_abstract_hi)) = ( layout.is_final_lo, layout.is_final_hi, diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index b1523f82e5..42d34cecd3 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -179,6 +179,7 @@ struct ReflectionMemberFlags { is_private: bool, is_final: bool, is_abstract: bool, + is_readonly: bool, } /// Returns true for reflection owner classes that need metadata-aware construction. @@ -1490,6 +1491,7 @@ fn reflection_method_member_flags( visibility, info.final_methods.contains(method_key), !info.method_impl_classes.contains_key(method_key), + false, )); } if info.static_methods.contains_key(method_key) { @@ -1502,6 +1504,7 @@ fn reflection_method_member_flags( visibility, info.final_static_methods.contains(method_key), !info.static_method_impl_classes.contains_key(method_key), + false, )); } None @@ -1521,7 +1524,13 @@ fn reflection_property_member_flags( .property_visibilities .get(property_name) .unwrap_or(&Visibility::Public); - return Some(reflection_member_flags(false, visibility, false, false)); + return Some(reflection_member_flags( + false, + visibility, + false, + false, + info.readonly_properties.contains(property_name), + )); } if info .static_properties @@ -1532,7 +1541,7 @@ fn reflection_property_member_flags( .static_property_visibilities .get(property_name) .unwrap_or(&Visibility::Public); - return Some(reflection_member_flags(true, visibility, false, false)); + return Some(reflection_member_flags(true, visibility, false, false, false)); } None } @@ -1628,7 +1637,7 @@ fn reflection_interface_method_member( .cloned() .unwrap_or_else(|| interface_name.to_string()); let required_parameter_count = reflection_required_parameter_count(sig); - let flags = reflection_member_flags(is_static, &Visibility::Public, false, true); + let flags = reflection_member_flags(is_static, &Visibility::Public, false, true, false); let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), declaring_class_name: Some(declaring_class_name.clone()), @@ -1678,6 +1687,7 @@ fn reflection_trait_method_member( &info.visibility, info.is_final, info.is_abstract, + false, ); let required_parameter_count = reflection_required_parameter_count(&info.signature); let declaring_function = ReflectionDeclaringFunctionMember::Method { @@ -1727,7 +1737,7 @@ fn reflection_class_property_member( ) -> Option { let flags = reflection_property_member_flags(info, property_name).or_else(|| { (is_reflection_enum(ctx, class_name) && property_name == "name").then_some( - reflection_member_flags(false, &Visibility::Public, false, false), + reflection_member_flags(false, &Visibility::Public, false, false, true), ) })?; Some(ReflectionListedMember { @@ -1766,7 +1776,7 @@ fn default_method_members( declaring_class_name: Some(declaring_class_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), - flags: reflection_member_flags(false, &Visibility::Public, false, is_interface), + flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), required_parameter_count: 0, parameters: Vec::new(), }) @@ -1786,7 +1796,7 @@ fn default_property_members( declaring_class_name: Some(declaring_class_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), - flags: reflection_member_flags(false, &Visibility::Public, false, is_interface), + flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), required_parameter_count: 0, parameters: Vec::new(), }) @@ -2017,6 +2027,7 @@ fn reflection_member_flags( visibility: &Visibility, is_final: bool, is_abstract: bool, + is_readonly: bool, ) -> ReflectionMemberFlags { ReflectionMemberFlags { is_static, @@ -2025,6 +2036,7 @@ fn reflection_member_flags( is_private: visibility == &Visibility::Private, is_final, is_abstract, + is_readonly, } } @@ -3631,6 +3643,12 @@ fn emit_reflection_member_flag_properties( flags.is_protected, )?; emit_reflection_owner_bool_property(ctx, class_name, "__is_private", flags.is_private)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_readonly", + flags.is_readonly, + )?; } _ => {} } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 136a3bb8b8..4fa09c3983 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1923,6 +1923,18 @@ fn add_reflection_member_flag_methods( methods.push(builtin_reflection_class_bool_method(method, property)); } } + if class_name == "ReflectionProperty" { + properties.push(builtin_property( + "__is_readonly", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method( + "isReadOnly", + "__is_readonly", + )); + } if class_name == "ReflectionMethod" { for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { properties.push(builtin_property( diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a5353374f6..d51709513a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6257,10 +6257,14 @@ eval('abstract class EvalReflectMemberBase { abstract protected function mustImplement(); final public function locked() {} } +readonly class EvalReflectReadonlyClass { + public int $classReadonly; +} class EvalReflectMemberChild extends EvalReflectMemberBase { public function mustImplement() {} private static $token; protected $visible; + public readonly int $locked; } $baseStatic = new ReflectionMethod("EvalReflectMemberChild", "baseStatic"); echo $baseStatic->isStatic() ? "S" : "s"; @@ -6284,11 +6288,20 @@ $staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); echo $staticProp->isStatic() ? "S" : "s"; echo $staticProp->isPrivate() ? "R" : "r"; echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isReadOnly() ? "R" : "r"; echo ":"; $visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); echo $visibleProp->isStatic() ? "S" : "s"; echo $visibleProp->isProtected() ? "P" : "p"; -echo $visibleProp->isPublic() ? "U" : "u";'); +echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isReadOnly() ? "R" : "r"; +echo ":"; +$readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); +echo $readonlyProp->isReadOnly() ? "R" : "r"; +echo $readonlyProp->isPublic() ? "U" : "u"; +echo ":"; +$classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); +echo $classReadonlyProp->isReadOnly() ? "C" : "c";'); "#, ); assert!( @@ -6296,7 +6309,7 @@ echo $visibleProp->isPublic() ? "U" : "u";'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "SPurfa:APs:FUs:SRp:sPu"); + assert_eq!(out.stdout, "SPurfa:APs:FUs:SRpr:sPur:RU:C"); } /// Verifies eval reflectors expose their declaring class through the bridge. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 777b832578..d4120aca29 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1735,10 +1735,14 @@ abstract class ReflectMemberBase { abstract protected function mustImplement(); final public function locked() {} } +readonly class ReflectReadonlyClass { + public int $classReadonly; +} class ReflectMemberChild extends ReflectMemberBase { public function mustImplement() {} private static string $token = "x"; protected int $visible = 2; + public readonly int $locked; } $baseStatic = new ReflectionMethod(ReflectMemberChild::class, "baseStatic"); echo $baseStatic->isStatic() ? "S" : "s"; @@ -1762,14 +1766,23 @@ $staticProp = new ReflectionProperty(ReflectMemberChild::class, "token"); echo $staticProp->isStatic() ? "S" : "s"; echo $staticProp->isPrivate() ? "R" : "r"; echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isReadOnly() ? "R" : "r"; echo ":"; $visibleProp = new ReflectionProperty(ReflectMemberChild::class, "visible"); echo $visibleProp->isStatic() ? "S" : "s"; echo $visibleProp->isProtected() ? "P" : "p"; echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isReadOnly() ? "R" : "r"; +echo ":"; +$readonlyProp = new ReflectionProperty(ReflectMemberChild::class, "locked"); +echo $readonlyProp->isReadOnly() ? "R" : "r"; +echo $readonlyProp->isPublic() ? "U" : "u"; +echo ":"; +$classReadonlyProp = new ReflectionProperty(ReflectReadonlyClass::class, "classReadonly"); +echo $classReadonlyProp->isReadOnly() ? "C" : "c"; "#, ); - assert_eq!(out, "SPurfa:APs:FUs:SRp:sPu"); + assert_eq!(out, "SPurfa:APs:FUs:SRpr:sPur:RU:C"); } /// Verifies member and enum-case reflectors expose their declaring class object. From f6d963d8a23f3d07e2dc4711f6199fa01ffa8c83 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 14:00:15 +0200 Subject: [PATCH 0418/1208] Add ReflectionProperty final and abstract metadata --- crates/elephc-eval/src/eval_ir.rs | 26 ++++++++++++ .../elephc-eval/src/interpreter/reflection.rs | 8 ++-- .../elephc-eval/src/interpreter/statements.rs | 28 +++++++++++++ .../tests/builtins_class_metadata.rs | 23 +++++++++- .../src/interpreter/tests/classes.rs | 13 ++++++ .../src/interpreter/tests/expressions.rs | 22 ++++++++++ .../interpreter/tests/support/object_ops.rs | 5 +++ crates/elephc-eval/src/parser/statements.rs | 24 ++++++----- .../src/parser/tests/classes_errors.rs | 10 ++++- docs/php/classes.md | 4 +- docs/php/eval.md | 6 +-- src/codegen/lower_inst/objects/reflection.rs | 19 +++++++-- src/types/checker/builtin_types/reflection.rs | 9 ++++ tests/codegen/eval.rs | 42 ++++++++++++++++++- tests/codegen/oop/attributes.rs | 23 +++++++++- 15 files changed, 237 insertions(+), 25 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 6fbb9dbfa0..226c1370f1 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1220,6 +1220,7 @@ pub struct EvalClassProperty { attributes: Vec, visibility: EvalVisibility, is_static: bool, + is_final: bool, is_readonly: bool, is_abstract: bool, has_get_hook: bool, @@ -1261,12 +1262,32 @@ impl EvalClassProperty { is_static: bool, is_readonly: bool, default: Option, + ) -> Self { + Self::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + false, + is_readonly, + default, + ) + } + + /// Creates an eval class property with explicit storage and modifier metadata. + pub fn with_visibility_static_final_and_readonly( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_readonly: bool, + default: Option, ) -> Self { Self { name: name.into(), attributes: Vec::new(), visibility, is_static, + is_final, is_readonly, is_abstract: false, has_get_hook: false, @@ -1322,6 +1343,11 @@ impl EvalClassProperty { self.is_static } + /// Returns whether this property was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + /// Returns whether this property was declared `readonly`. pub const fn is_readonly(&self) -> bool { self.is_readonly diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 25b3c829c8..d160969c92 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -587,8 +587,8 @@ fn eval_reflection_property_new( let flags = eval_reflection_member_flags( property.visibility, property.is_static, - false, - false, + property.is_final, + property.is_abstract, property.is_readonly, ); eval_reflection_owner_object( @@ -1587,7 +1587,7 @@ fn eval_reflection_property_metadata( attributes: property.attributes().to_vec(), visibility: property.visibility(), is_static: property.is_static(), - is_final: false, + is_final: property.is_final(), is_abstract: property.is_abstract(), is_readonly: property.is_readonly(), required_parameter_count: 0, @@ -1622,7 +1622,7 @@ fn eval_reflection_property_metadata( attributes: property.attributes().to_vec(), visibility: property.visibility(), is_static: property.is_static(), - is_final: false, + is_final: property.is_final(), is_abstract: property.is_abstract(), is_readonly: property.is_readonly(), required_parameter_count: 0, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index dfd97f837f..10f77902d8 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -977,6 +977,9 @@ fn validate_eval_class_modifiers( } validate_eval_declared_constants(class.constants())?; validate_eval_declared_properties(class)?; + for property in class.properties() { + validate_property_parent_redeclaration(class, property, context)?; + } for method in class.methods() { if method.is_abstract() && method.is_final() { return Err(EvalStatus::RuntimeFatal); @@ -1005,6 +1008,7 @@ fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus if property.is_abstract() && (!class.is_abstract() || property.is_static() + || property.is_final() || property.is_readonly() || property.default().is_some() || (!property.requires_get_hook() && !property.requires_set_hook())) @@ -1014,6 +1018,9 @@ fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus if property.is_static() && property.is_readonly() { return Err(EvalStatus::RuntimeFatal); } + if property.is_final() && property.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } if property.is_readonly() && property.default().is_some() { return Err(EvalStatus::RuntimeFatal); } @@ -1026,6 +1033,27 @@ fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus Ok(()) } +/// Validates one property declaration against inherited eval property metadata. +fn validate_property_parent_redeclaration( + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(()); + }; + let Some((_, parent_property)) = context.class_property(parent, property.name()) else { + return Ok(()); + }; + if parent_property.visibility() == EvalVisibility::Private { + return Ok(()); + } + if parent_property.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + /// Validates constant declarations that can be checked before registration. fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 3e055e5c77..f7093852df 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -682,11 +682,16 @@ fn execute_program_reflects_eval_member_predicates() { readonly class EvalReflectReadonlyClass { public int $classReadonly; } +abstract class EvalReflectAbstractProperty { + abstract public int $mustRead { get; } +} class EvalReflectMemberChild extends EvalReflectMemberBase { public function mustImplement() {} private static $token; + final public static $staticSeal; protected $visible; public readonly int $locked; + final public int $sealed; } $baseStatic = new ReflectionMethod("EvalReflectMemberChild", "baseStatic"); echo $baseStatic->isStatic() ? "S" : "s"; @@ -710,18 +715,34 @@ $staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); echo $staticProp->isStatic() ? "S" : "s"; echo $staticProp->isPrivate() ? "R" : "r"; echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isFinal() ? "F" : "f"; +echo $staticProp->isAbstract() ? "A" : "a"; echo $staticProp->isReadOnly() ? "R" : "r"; echo ":"; $visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); echo $visibleProp->isStatic() ? "S" : "s"; echo $visibleProp->isProtected() ? "P" : "p"; echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isFinal() ? "F" : "f"; +echo $visibleProp->isAbstract() ? "A" : "a"; echo $visibleProp->isReadOnly() ? "R" : "r"; echo ":"; $readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); echo $readonlyProp->isReadOnly() ? "R" : "r"; echo $readonlyProp->isPublic() ? "U" : "u"; echo ":"; +$sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); +echo $sealedProp->isFinal() ? "F" : "f"; +echo $sealedProp->isPublic() ? "U" : "u"; +echo ":"; +$staticFinalProp = new ReflectionProperty("EvalReflectMemberChild", "staticSeal"); +echo $staticFinalProp->isFinal() ? "F" : "f"; +echo $staticFinalProp->isStatic() ? "S" : "s"; +echo ":"; +$abstractProp = new ReflectionProperty("EvalReflectAbstractProperty", "mustRead"); +echo $abstractProp->isAbstract() ? "A" : "a"; +echo $abstractProp->isFinal() ? "F" : "f"; +echo ":"; $classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); echo $classReadonlyProp->isReadOnly() ? "C" : "c"; return true;"#, @@ -732,7 +753,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "SPurfa:APs:FUs:SRpr:sPur:RU:C"); + assert_eq!(values.output, "SPurfa:APs:FUs:SRpfar:sPufar:RU:FU:FS:Af:C"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 10d7ac8e4d..83dc073400 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -441,6 +441,19 @@ class EvalMissingAbstractHookBox extends EvalMissingAbstractHookBase {}"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies abstract final eval properties are rejected while parsing. +#[test] +fn parse_fragment_rejects_final_abstract_property_hook_contract() { + let err = parse_fragment( + br#"abstract class EvalFinalAbstractHookBase { + abstract final public string $value { get; } +}"#, + ) + .expect_err("final abstract property should fail"); + + assert_eq!(err, EvalParseError::UnsupportedConstruct); +} + /// Verifies readonly properties cannot satisfy abstract writable hook contracts. #[test] fn execute_program_rejects_readonly_property_for_abstract_set_contract() { diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index d0459fff83..33be6714b3 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -533,6 +533,28 @@ class EvalFinalMethodChild extends EvalFinalMethodBase { assert_eq!(err, EvalStatus::RuntimeFatal); } + +/// Verifies eval rejects overriding a final eval-declared property. +#[test] +fn execute_program_rejects_overriding_final_eval_property() { + let program = parse_fragment( + br#"class EvalFinalPropertyBase { + final public $value = 1; +} +class EvalFinalPropertyChild extends EvalFinalPropertyBase { + public $value = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final property should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval-declared traits contribute methods, properties, and metadata. #[test] fn execute_program_constructs_class_using_eval_declared_trait() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 1c2201108a..ab37d390da 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -522,8 +522,13 @@ impl FakeOps { properties.push(("__is_protected".to_string(), is_protected)); properties.push(("__is_private".to_string(), is_private)); if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_abstract = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; let is_readonly = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY) != 0)?; + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_readonly".to_string(), is_readonly)); } } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 314ea00197..d4791a6b2a 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -507,12 +507,10 @@ impl Parser { } let visibility = visibility.unwrap_or(EvalVisibility::Public); - if is_final { - return Err(EvalParseError::UnsupportedConstruct); - } let (property, mut hook_methods) = self.parse_class_property_decl( visibility, is_static, + is_final, is_readonly, is_readonly_class, is_abstract, @@ -789,6 +787,7 @@ impl Parser { &mut self, visibility: EvalVisibility, is_static: bool, + is_final: bool, is_readonly: bool, is_readonly_class: bool, is_abstract: bool, @@ -816,10 +815,11 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } let (requires_get_hook, requires_set_hook) = self.parse_property_hook_contracts()?; - let property = EvalClassProperty::with_visibility_static_and_readonly( + let property = EvalClassProperty::with_visibility_static_final_and_readonly( name, visibility, is_static, + is_final, effective_readonly, None, ) @@ -829,10 +829,11 @@ impl Parser { let default_is_some = default.is_some(); let (has_get_hook, has_set_hook, hook_methods) = self.parse_property_hook_tail(&name, is_static, effective_readonly, default_is_some)?; - let property = EvalClassProperty::with_visibility_static_and_readonly( + let property = EvalClassProperty::with_visibility_static_final_and_readonly( name, visibility, is_static, + is_final, effective_readonly, default, ) @@ -1025,11 +1026,14 @@ impl Parser { return Ok(()); } let visibility = visibility.unwrap_or(EvalVisibility::Public); - if is_final { - return Err(EvalParseError::UnsupportedConstruct); - } - let (property, mut hook_methods) = - self.parse_class_property_decl(visibility, is_static, is_readonly, false, is_abstract)?; + let (property, mut hook_methods) = self.parse_class_property_decl( + visibility, + is_static, + is_final, + is_readonly, + false, + is_abstract, + )?; properties.push(property.with_attributes(attributes)); methods.append(&mut hook_methods); Ok(()) diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 58710ff815..65cc855abe 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -525,6 +525,7 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { let program = parse_fragment( br#"abstract class DynEvalAbstract { abstract public function read($value); + final public $value = 42; final public function label() { return "base"; } }"#, ) @@ -537,7 +538,14 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { false, None, Vec::new(), - Vec::new(), + vec![EvalClassProperty::with_visibility_static_final_and_readonly( + "value", + EvalVisibility::Public, + false, + true, + false, + Some(EvalExpr::Const(EvalConst::Int(42))), + )], vec![ EvalClassMethod::with_modifiers( "read", diff --git a/docs/php/classes.md b/docs/php/classes.md index 1c9691dc19..e63d30faef 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1212,6 +1212,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isPublic()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is public | | `ReflectionProperty::isProtected()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is protected | | `ReflectionProperty::isPrivate()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is private | +| `ReflectionProperty::isFinal()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is final | +| `ReflectionProperty::isAbstract()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is abstract | | `ReflectionProperty::isReadOnly()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is readonly | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | @@ -1284,7 +1286,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, and `isReadOnly()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index efbb35d010..79b7fb610e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -134,8 +134,8 @@ containers to eval-declared functions. Eval-declared classes support inheritance, public/protected/private properties and methods, concrete property `get` / `set` hooks, interface property hook contract checks, abstract property hook contracts, property-level `readonly`, -`readonly class`, `__construct()`, abstract classes and methods, final classes -and methods, trait composition with `insteadof` conflict resolution and `as` +`readonly class`, `__construct()`, abstract classes and methods, final classes, +methods, and properties, trait composition with `insteadof` conflict resolution and `as` aliases/visibility adaptations, interface implementation checks, static properties, static methods, static interface method contracts, class constants, interface constants, trait constants, class-level attributes, and `ClassName::class` literals. Member @@ -234,7 +234,7 @@ parameters after method execution, write back mutated `&...$items` elements when the variadic container itself is not rebound, and are reported through `ReflectionParameter::isPassedByReference()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, -and `isReadOnly()` report eval property metadata. +`isFinal()`, `isAbstract()`, and `isReadOnly()` report eval property metadata. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 42d34cecd3..b3d193c7c8 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -1527,8 +1527,8 @@ fn reflection_property_member_flags( return Some(reflection_member_flags( false, visibility, - false, - false, + info.final_properties.contains(property_name), + info.abstract_properties.contains(property_name), info.readonly_properties.contains(property_name), )); } @@ -1541,7 +1541,13 @@ fn reflection_property_member_flags( .static_property_visibilities .get(property_name) .unwrap_or(&Visibility::Public); - return Some(reflection_member_flags(true, visibility, false, false, false)); + return Some(reflection_member_flags( + true, + visibility, + info.final_static_properties.contains(property_name), + false, + false, + )); } None } @@ -3643,6 +3649,13 @@ fn emit_reflection_member_flag_properties( flags.is_protected, )?; emit_reflection_owner_bool_property(ctx, class_name, "__is_private", flags.is_private)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_final", flags.is_final)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_abstract", + flags.is_abstract, + )?; emit_reflection_owner_bool_property( ctx, class_name, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 4fa09c3983..cdf6895a73 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1924,6 +1924,15 @@ fn add_reflection_member_flag_methods( } } if class_name == "ReflectionProperty" { + for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { + properties.push(builtin_property( + property, + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method(method, property)); + } properties.push(builtin_property( "__is_readonly", Visibility::Private, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d51709513a..e658d7f9ba 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6260,11 +6260,16 @@ eval('abstract class EvalReflectMemberBase { readonly class EvalReflectReadonlyClass { public int $classReadonly; } +abstract class EvalReflectAbstractProperty { + abstract public int $mustRead { get; } +} class EvalReflectMemberChild extends EvalReflectMemberBase { public function mustImplement() {} private static $token; + final public static $staticSeal; protected $visible; public readonly int $locked; + final public int $sealed; } $baseStatic = new ReflectionMethod("EvalReflectMemberChild", "baseStatic"); echo $baseStatic->isStatic() ? "S" : "s"; @@ -6288,18 +6293,34 @@ $staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); echo $staticProp->isStatic() ? "S" : "s"; echo $staticProp->isPrivate() ? "R" : "r"; echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isFinal() ? "F" : "f"; +echo $staticProp->isAbstract() ? "A" : "a"; echo $staticProp->isReadOnly() ? "R" : "r"; echo ":"; $visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); echo $visibleProp->isStatic() ? "S" : "s"; echo $visibleProp->isProtected() ? "P" : "p"; echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isFinal() ? "F" : "f"; +echo $visibleProp->isAbstract() ? "A" : "a"; echo $visibleProp->isReadOnly() ? "R" : "r"; echo ":"; $readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); echo $readonlyProp->isReadOnly() ? "R" : "r"; echo $readonlyProp->isPublic() ? "U" : "u"; echo ":"; +$sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); +echo $sealedProp->isFinal() ? "F" : "f"; +echo $sealedProp->isPublic() ? "U" : "u"; +echo ":"; +$staticFinalProp = new ReflectionProperty("EvalReflectMemberChild", "staticSeal"); +echo $staticFinalProp->isFinal() ? "F" : "f"; +echo $staticFinalProp->isStatic() ? "S" : "s"; +echo ":"; +$abstractProp = new ReflectionProperty("EvalReflectAbstractProperty", "mustRead"); +echo $abstractProp->isAbstract() ? "A" : "a"; +echo $abstractProp->isFinal() ? "F" : "f"; +echo ":"; $classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); echo $classReadonlyProp->isReadOnly() ? "C" : "c";'); "#, @@ -6309,7 +6330,26 @@ echo $classReadonlyProp->isReadOnly() ? "C" : "c";'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "SPurfa:APs:FUs:SRpr:sPur:RU:C"); + assert_eq!(out.stdout, "SPurfa:APs:FUs:SRpfar:sPufar:RU:FU:FS:Af:C"); +} + +/// Verifies eval-declared final properties cannot be redeclared by subclasses. +#[test] +fn test_eval_declared_final_property_override_fails() { + let err = compile_and_run_expect_failure( + r#"isStatic() ? "S" : "s"; @@ -1766,23 +1771,39 @@ $staticProp = new ReflectionProperty(ReflectMemberChild::class, "token"); echo $staticProp->isStatic() ? "S" : "s"; echo $staticProp->isPrivate() ? "R" : "r"; echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isFinal() ? "F" : "f"; +echo $staticProp->isAbstract() ? "A" : "a"; echo $staticProp->isReadOnly() ? "R" : "r"; echo ":"; $visibleProp = new ReflectionProperty(ReflectMemberChild::class, "visible"); echo $visibleProp->isStatic() ? "S" : "s"; echo $visibleProp->isProtected() ? "P" : "p"; echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isFinal() ? "F" : "f"; +echo $visibleProp->isAbstract() ? "A" : "a"; echo $visibleProp->isReadOnly() ? "R" : "r"; echo ":"; $readonlyProp = new ReflectionProperty(ReflectMemberChild::class, "locked"); echo $readonlyProp->isReadOnly() ? "R" : "r"; echo $readonlyProp->isPublic() ? "U" : "u"; echo ":"; +$sealedProp = new ReflectionProperty(ReflectMemberChild::class, "sealed"); +echo $sealedProp->isFinal() ? "F" : "f"; +echo $sealedProp->isPublic() ? "U" : "u"; +echo ":"; +$staticFinalProp = new ReflectionProperty(ReflectMemberChild::class, "staticSeal"); +echo $staticFinalProp->isFinal() ? "F" : "f"; +echo $staticFinalProp->isStatic() ? "S" : "s"; +echo ":"; +$abstractProp = new ReflectionProperty(ReflectAbstractProperty::class, "mustRead"); +echo $abstractProp->isAbstract() ? "A" : "a"; +echo $abstractProp->isFinal() ? "F" : "f"; +echo ":"; $classReadonlyProp = new ReflectionProperty(ReflectReadonlyClass::class, "classReadonly"); echo $classReadonlyProp->isReadOnly() ? "C" : "c"; "#, ); - assert_eq!(out, "SPurfa:APs:FUs:SRpr:sPur:RU:C"); + assert_eq!(out, "SPurfa:APs:FUs:SRpfar:sPufar:RU:FU:FS:Af:C"); } /// Verifies member and enum-case reflectors expose their declaring class object. From 18f9b2139030b7e83409a98710c363697ec17d1e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 14:32:47 +0200 Subject: [PATCH 0419/1208] Add final class constant metadata --- crates/elephc-eval/src/eval_ir.rs | 17 ++++ .../elephc-eval/src/interpreter/reflection.rs | 36 ++++++--- .../elephc-eval/src/interpreter/statements.rs | 52 +++++++++++++ .../tests/builtins_class_metadata.rs | 5 +- .../src/interpreter/tests/class_constants.rs | 77 +++++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 3 + crates/elephc-eval/src/parser/statements.rs | 41 +++++++--- .../src/parser/tests/class_constants.rs | 21 +++-- crates/elephc-eval/src/parser/tests/enums.rs | 6 +- docs/php/classes.md | 3 +- docs/php/eval.md | 22 +++--- src/codegen/eval_reflection_owner_helpers.rs | 33 ++++++++ src/codegen/lower_inst/objects/reflection.rs | 21 ++++- src/codegen_support/runtime/data/user.rs | 1 + src/ir_lower/tests/exhaustive.rs | 1 + src/types/checker/builtin_types/reflection.rs | 12 +++ src/types/checker/schema/classes/mod.rs | 45 +++++++++++ src/types/checker/schema/classes/state.rs | 6 ++ src/types/checker/schema/enums.rs | 5 ++ src/types/checker/schema/interfaces.rs | 22 ++++++ src/types/schema.rs | 4 + tests/codegen/eval.rs | 45 ++++++++++- tests/codegen/oop/attributes.rs | 11 ++- tests/codegen/oop/constants.rs | 70 +++++++++++++++++ tests/codegen/support/projects.rs | 32 ++++++++ 25 files changed, 542 insertions(+), 49 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 226c1370f1..8302c6960a 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1088,6 +1088,7 @@ pub struct EvalClassConstant { name: String, attributes: Vec, visibility: EvalVisibility, + is_final: bool, value: EvalExpr, } @@ -1102,11 +1103,22 @@ impl EvalClassConstant { name: impl Into, visibility: EvalVisibility, value: EvalExpr, + ) -> Self { + Self::with_visibility_and_final(name, visibility, false, value) + } + + /// Creates an eval class constant with explicit PHP visibility and finality. + pub fn with_visibility_and_final( + name: impl Into, + visibility: EvalVisibility, + is_final: bool, + value: EvalExpr, ) -> Self { Self { name: name.into(), attributes: Vec::new(), visibility, + is_final, value, } } @@ -1132,6 +1144,11 @@ impl EvalClassConstant { self.visibility } + /// Returns whether this class constant was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + /// Returns the constant initializer expression. pub fn value(&self) -> &EvalExpr { &self.value diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index d160969c92..53811acec3 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -437,9 +437,14 @@ fn eval_reflection_class_constant_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let (declaring_class_name, attributes) = + let (declaring_class_name, attributes, is_final) = eval_reflection_class_constant_metadata(reflected_name, constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; + let flags = if is_final { + EVAL_REFLECTION_CLASS_FLAG_FINAL + } else { + 0 + }; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, constant_name, @@ -450,7 +455,7 @@ fn eval_reflection_class_constant_object_result( &[], Some(&declaring_class_name), &[], - 0, + flags, 0, context, values, @@ -624,9 +629,14 @@ fn eval_reflection_class_constant_new( return Ok(None); } let constant_name = eval_reflection_string_arg(args[1], values)?; - let (declaring_class_name, attributes) = + let (declaring_class_name, attributes, is_final) = eval_reflection_class_constant_metadata(&class_name, &constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; + let flags = if is_final { + EVAL_REFLECTION_CLASS_FLAG_FINAL + } else { + 0 + }; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, &constant_name, @@ -637,7 +647,7 @@ fn eval_reflection_class_constant_new( &[], Some(&declaring_class_name), &[], - 0, + flags, 0, context, values, @@ -1332,20 +1342,26 @@ fn eval_reflection_class_modifiers( modifiers } -/// Returns declaring class and attributes attached to an eval class constant or enum case. +/// Returns declaring class, attributes, and finality for an eval class constant or enum case. fn eval_reflection_class_constant_metadata( class_name: &str, constant_name: &str, context: &ElephcEvalContext, -) -> Option<(String, Vec)> { +) -> Option<(String, Vec, bool)> { if let Some(enum_decl) = context.enum_decl(class_name) { if let Some(case) = enum_decl.case(constant_name) { - return Some((enum_decl.name().to_string(), case.attributes().to_vec())); + return Some((enum_decl.name().to_string(), case.attributes().to_vec(), false)); } } - context - .class_constant(class_name, constant_name) - .map(|(declaring_class, constant)| (declaring_class, constant.attributes().to_vec())) + context.class_constant(class_name, constant_name).map( + |(declaring_class, constant)| { + ( + declaring_class, + constant.attributes().to_vec(), + constant.is_final(), + ) + }, + ) } /// Returns true when a name resolves to an eval-declared class-like symbol. diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 10f77902d8..62469c85b0 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -645,6 +645,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( } } validate_eval_declared_constants(interface.constants())?; + validate_interface_constant_parent_redeclarations(interface, context)?; if context.define_interface(interface.clone()) { initialize_eval_declared_constants( interface.name(), @@ -789,6 +790,9 @@ fn append_eval_trait_constants( ) -> Result<(), EvalStatus> { for constant in trait_decl.constants() { if class_constant_names.contains(constant.name()) { + if constant.is_final() { + return Err(EvalStatus::RuntimeFatal); + } continue; } if !trait_constant_names.insert(constant.name().to_string()) { @@ -976,6 +980,9 @@ fn validate_eval_class_modifiers( return Err(EvalStatus::RuntimeFatal); } validate_eval_declared_constants(class.constants())?; + for constant in class.constants() { + validate_constant_parent_redeclaration(class, constant, context)?; + } validate_eval_declared_properties(class)?; for property in class.properties() { validate_property_parent_redeclaration(class, property, context)?; @@ -1061,6 +1068,51 @@ fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<( if !names.insert(constant.name().to_string()) { return Err(EvalStatus::RuntimeFatal); } + if constant.is_final() && constant.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates interface constants against inherited parent-interface constants. +fn validate_interface_constant_parent_redeclarations( + interface: &EvalInterface, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for constant in interface.constants() { + for parent in interface.parents() { + if let Some((_, parent_constant)) = context.interface_constant(parent, constant.name()) { + if parent_constant.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + } + } + } + Ok(()) +} + +/// Validates one constant declaration against inherited eval constant metadata. +fn validate_constant_parent_redeclaration( + class: &EvalClass, + constant: &EvalClassConstant, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if let Some(parent) = class.parent() { + if let Some((_, parent_constant)) = context.class_constant(parent, constant.name()) { + if parent_constant.visibility() != EvalVisibility::Private && parent_constant.is_final() + { + return Err(EvalStatus::RuntimeFatal); + } + } + } + for interface in class.interfaces() { + if let Some((_, interface_constant)) = context.interface_constant(interface, constant.name()) + { + if interface_constant.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + } } Ok(()) } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index f7093852df..0b2c8435d9 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1084,7 +1084,7 @@ fn execute_program_reflects_eval_constant_and_enum_case_attributes() { } class EvalConstReflectTarget { #[EvalConstMarker("const")] - public const ANSWER = 42; + final public const ANSWER = 42; } enum EvalCaseReflectTarget: string { #[EvalConstMarker("case")] @@ -1092,6 +1092,7 @@ enum EvalCaseReflectTarget: string { } $const_attrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); echo count($const_attrs); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName(); echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isFinal() ? "F" : "f"; echo ":"; echo $const_attrs[0]->getName(); echo ":"; echo $const_attrs[0]->getArguments()[0]; echo ":"; echo $const_attrs[0]->newInstance()->label(); echo ":"; $case_attrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); @@ -1113,7 +1114,7 @@ return true;"#, assert_eq!( values.output, - "1:ANSWER:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:case:Ready:case" + "1:ANSWER:F:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:case:Ready:case" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/class_constants.rs b/crates/elephc-eval/src/interpreter/tests/class_constants.rs index 5b73d363ed..3d19b8cbcb 100644 --- a/crates/elephc-eval/src/interpreter/tests/class_constants.rs +++ b/crates/elephc-eval/src/interpreter/tests/class_constants.rs @@ -80,6 +80,45 @@ fn execute_program_rejects_duplicate_eval_class_constant() { .expect_err("duplicate class constant should fail"); } +/// Verifies final eval class constants are readable and reject child redeclarations. +#[test] +fn execute_program_rejects_overriding_final_eval_class_constant() { + let program = parse_fragment( + br#"class EvalFinalConstBase { + final public const SEED = 1; +} +class EvalFinalConstChild extends EvalFinalConstBase { + public const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final class constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies private eval constants cannot be declared final. +#[test] +fn execute_program_rejects_final_private_eval_class_constant() { + let program = parse_fragment( + br#"class EvalFinalPrivateConst { + final private const SEED = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("final private class constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies class-name literals resolve class-like receiver spelling. #[test] fn execute_program_reads_eval_class_name_literals() { @@ -139,6 +178,44 @@ return EvalConstIfaceImpl::BASE + EvalConstIfaceImpl::LOCAL;"#, assert_eq!(values.get(result), FakeValue::Int(5)); } +/// Verifies final eval interface constants cannot be redeclared by children or implementors. +#[test] +fn execute_program_rejects_overriding_final_eval_interface_constant() { + let program = parse_fragment( + br#"interface EvalFinalConstIface { + final public const SEED = 1; +} +interface EvalFinalConstChildIface extends EvalFinalConstIface { + public const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final interface constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let program = parse_fragment( + br#"interface EvalFinalImplConstIface { + final public const SEED = 1; +} +class EvalFinalImplConstBox implements EvalFinalImplConstIface { + public const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("class overriding final interface constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies trait constants are readable directly and from classes using the trait. #[test] fn execute_program_reads_eval_trait_constants() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index ab37d390da..0fa9e7dbac 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -552,6 +552,9 @@ impl FakeOps { properties.push(("__parameters".to_string(), method_objects)); properties.push(("__required_parameter_count".to_string(), modifiers_cell)); } + if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { + properties.push(("__is_final".to_string(), is_final)); + } if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { let position = self.int(modifiers as i64)?; let is_optional = diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index d4791a6b2a..28f09c1b3e 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -480,11 +480,14 @@ impl Parser { } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_abstract || is_final || is_readonly { + if is_static || is_abstract || is_readonly { return Err(EvalParseError::UnsupportedConstruct); } constants.push( - self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))? + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + )? .with_attributes(attributes), ); return Ok(()); @@ -535,6 +538,7 @@ impl Parser { pub(super) fn parse_class_const_decl( &mut self, visibility: EvalVisibility, + is_final: bool, ) -> Result { self.advance(); let TokenKind::Ident(name) = self.current() else { @@ -545,7 +549,9 @@ impl Parser { self.expect(TokenKind::Equal)?; let value = self.parse_expr()?; self.expect_semicolon()?; - Ok(EvalClassConstant::with_visibility(name, visibility, value)) + Ok(EvalClassConstant::with_visibility_and_final( + name, visibility, is_final, value, + )) } /// Parses `use TraitName, OtherTrait;` or an adaptation block inside an eval class body. @@ -1001,11 +1007,14 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_abstract || is_final || is_readonly { + if is_static || is_abstract || is_readonly { return Err(EvalParseError::UnsupportedConstruct); } constants.push( - self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))? + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + )? .with_attributes(attributes), ); return Ok(()); @@ -1113,11 +1122,14 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_final { + if is_static { return Err(EvalParseError::UnsupportedConstruct); } constants.push( - self.parse_class_const_decl(visibility.unwrap_or(EvalVisibility::Public))? + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + )? .with_attributes(attributes), ); return Ok(()); @@ -1216,6 +1228,7 @@ impl Parser { ) -> Result<(), EvalParseError> { let attributes = self.parse_optional_member_attributes()?; let mut is_static = false; + let mut is_final = false; let mut saw_public = false; loop { match self.current() { @@ -1233,6 +1246,13 @@ impl Parser { is_static = true; self.advance(); } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { return Err(EvalParseError::UnsupportedConstruct); } @@ -1244,19 +1264,22 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } constants.push( - self.parse_class_const_decl(EvalVisibility::Public)? + self.parse_class_const_decl(EvalVisibility::Public, is_final)? .with_attributes(attributes), ); return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } methods.push( self.parse_interface_method_decl_after_function_keyword(is_static)? .with_attributes(attributes), ); return Ok(()); } - if is_static { + if is_static || is_final { return Err(EvalParseError::UnsupportedConstruct); } properties.push( diff --git a/crates/elephc-eval/src/parser/tests/class_constants.rs b/crates/elephc-eval/src/parser/tests/class_constants.rs index 0f5186d381..804b718f3c 100644 --- a/crates/elephc-eval/src/parser/tests/class_constants.rs +++ b/crates/elephc-eval/src/parser/tests/class_constants.rs @@ -14,7 +14,7 @@ use super::support::*; fn parse_fragment_accepts_class_constant_declarations() { let program = parse_fragment( br#"class EvalConstBox { - public const SEED = 2; + final public const SEED = 2; protected const LABEL = "box"; }"#, ) @@ -30,7 +30,12 @@ fn parse_fragment_accepts_class_constant_declarations() { Vec::new(), Vec::new(), vec![ - EvalClassConstant::new("SEED", EvalExpr::Const(EvalConst::Int(2))), + EvalClassConstant::with_visibility_and_final( + "SEED", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(2)), + ), EvalClassConstant::with_visibility( "LABEL", EvalVisibility::Protected, @@ -102,7 +107,7 @@ fn parse_fragment_accepts_class_name_fetches() { fn parse_fragment_accepts_interface_constant_declarations() { let program = parse_fragment( br#"interface EvalConstIface { - public const SEED = 4; + final public const SEED = 4; function read(); }"#, ) @@ -112,8 +117,10 @@ fn parse_fragment_accepts_interface_constant_declarations() { &[EvalStmt::InterfaceDecl(EvalInterface::with_constants( "EvalConstIface", Vec::new(), - vec![EvalClassConstant::new( + vec![EvalClassConstant::with_visibility_and_final( "SEED", + EvalVisibility::Public, + true, EvalExpr::Const(EvalConst::Int(4)), )], vec![EvalInterfaceMethod::new("read", Vec::new())], @@ -126,7 +133,7 @@ fn parse_fragment_accepts_interface_constant_declarations() { fn parse_fragment_accepts_trait_constant_declarations() { let program = parse_fragment( br#"trait EvalConstTrait { - public const SEED = 6; + final public const SEED = 6; public function read() { return self::SEED; } }"#, ) @@ -135,8 +142,10 @@ fn parse_fragment_accepts_trait_constant_declarations() { program.statements(), &[EvalStmt::TraitDecl(EvalTrait::with_constants( "EvalConstTrait", - vec![EvalClassConstant::new( + vec![EvalClassConstant::with_visibility_and_final( "SEED", + EvalVisibility::Public, + true, EvalExpr::Const(EvalConst::Int(6)), )], Vec::new(), diff --git a/crates/elephc-eval/src/parser/tests/enums.rs b/crates/elephc-eval/src/parser/tests/enums.rs index 21ce114879..3e350d0cc0 100644 --- a/crates/elephc-eval/src/parser/tests/enums.rs +++ b/crates/elephc-eval/src/parser/tests/enums.rs @@ -33,7 +33,7 @@ fn parse_fragment_accepts_backed_enum_members() { let program = parse_fragment( br#"enum EvalColor: string implements EvalLabel { case Red = "r"; - public const PREFIX = "color"; + final public const PREFIX = "color"; public function label() { return self::PREFIX . ":" . $this->name; } }"#, ) @@ -48,8 +48,10 @@ fn parse_fragment_accepts_backed_enum_members() { "Red", Some(EvalExpr::Const(EvalConst::String("r".to_string()))), )], - vec![EvalClassConstant::new( + vec![EvalClassConstant::with_visibility_and_final( "PREFIX", + EvalVisibility::Public, + true, EvalExpr::Const(EvalConst::String("color".to_string())), )], vec![EvalClassMethod::new( diff --git a/docs/php/classes.md b/docs/php/classes.md index e63d30faef..2fb7814d54 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1218,6 +1218,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | +| `ReflectionClassConstant::isFinal()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class/interface/trait/enum constant is final; enum cases report `false` | | `ReflectionEnumUnitCase::getName()` / `ReflectionEnumBackedCase::getName()` | `new ReflectionEnumUnitCase($enum_name, $case_name)` or `new ReflectionEnumBackedCase($enum_name, $case_name)` | Return the reflected enum-case name | | `ReflectionEnumUnitCase::getAttributes()` / `ReflectionEnumBackedCase::getAttributes()` | Same as enum-case `getName()` | Return `ReflectionAttribute` objects for enum-case attributes | | `ReflectionEnumUnitCase::getDeclaringClass()` / `ReflectionEnumBackedCase::getDeclaringClass()` | Same as enum-case `getName()` | Return a `ReflectionClass` object for the enum that declares the reflected case | @@ -1286,7 +1287,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getAttributes()`, `getDeclaringClass()`, and `isFinal()`; `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 79b7fb610e..2c0fcc924f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,8 +49,8 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class constants, and class-level attributes. Duplicate eval class-like names are rejected. | -| Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | +| Classes | Eval fragments can declare classes with properties, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -135,10 +135,11 @@ Eval-declared classes support inheritance, public/protected/private properties and methods, concrete property `get` / `set` hooks, interface property hook contract checks, abstract property hook contracts, property-level `readonly`, `readonly class`, `__construct()`, abstract classes and methods, final classes, -methods, and properties, trait composition with `insteadof` conflict resolution and `as` -aliases/visibility adaptations, interface implementation checks, static -properties, static methods, static interface method contracts, class constants, interface constants, trait -constants, class-level attributes, and `ClassName::class` literals. Member +methods, and properties, trait composition with `insteadof` conflict resolution +and `as` aliases/visibility adaptations, interface implementation checks, +static properties, static methods, static interface method contracts, class, +interface, trait, and enum constants including `final` constants, class-level +attributes, and `ClassName::class` literals. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, interfaces, traits, and enums are visible through `class_attribute_names()`, @@ -180,9 +181,9 @@ method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. `ReflectionClass::hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, and `getReflectionConstants()` expose eval-visible -class constants, interface constants, trait constants, and enum cases. -Constant lookup is case-sensitive; single-value lookups return `false` when no -constant or case is visible. +class constants, interface constants, trait constants, enum constants, and enum +cases. Constant lookup is case-sensitive; single-value lookups return `false` +when no constant or case is visible. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate @@ -241,7 +242,8 @@ when the variadic container itself is not rebound, and are reported through and enum-case attributes through the same materialized `ReflectionAttribute` shape; their `getName()` methods return the reflected constant or case name, and `getDeclaringClass()` returns the declaring class or enum as a -`ReflectionClass`. +`ReflectionClass`. `ReflectionClassConstant::isFinal()` reports final +class/interface/trait/enum constants and returns `false` for enum cases. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 9e21d50139..9952ba09a5 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -762,6 +762,7 @@ fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &Reflecti layout.in_namespace_hi, ) else { + emit_set_owner_low_bit_final_property_aarch64(emitter, layout); return; }; let scan_loop_label = "__elephc_eval_reflection_owner_name_scan_loop"; @@ -1121,6 +1122,21 @@ fn emit_set_owner_member_flags_property_aarch64( abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); } +/// Stores incoming bit-zero finality for ARM64 owners without full member flags. +fn emit_set_owner_low_bit_final_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) else { + return; + }; + emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection owner predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract bit-zero finality as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); +} + /// Stores incoming x86_64 ReflectionMethod/ReflectionProperty boolean flags. fn emit_set_owner_member_flags_property_x86_64( emitter: &mut Emitter, @@ -1146,6 +1162,7 @@ fn emit_set_owner_member_flags_property_x86_64( layout.is_private_hi, ) else { + emit_set_owner_low_bit_final_property_x86_64(emitter, layout); return; }; emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection member predicate flags @@ -1198,6 +1215,22 @@ fn emit_set_owner_member_flags_property_x86_64( abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); } +/// Stores incoming bit-zero finality for x86_64 owners without full member flags. +fn emit_set_owner_low_bit_final_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, +) { + let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection owner predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting bit-zero finality + emitter.instruction("and rax, 1"); // extract bit-zero finality as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); +} + /// Stores incoming ARM64 ReflectionMethod required-parameter count. fn emit_set_owner_required_parameter_count_property_aarch64( emitter: &mut Emitter, diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index b3d193c7c8..887a52b3a6 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -901,6 +901,7 @@ fn reflection_class_constant_metadata( Ok( resolve_reflection_class_constant(ctx, &reflected_class, &constant_name) .map(|(declaring_class_name, info)| { + let is_final = info.final_constants.contains(&constant_name); let attr_names = info .constant_attribute_names .get(&constant_name) @@ -928,7 +929,7 @@ fn reflection_class_constant_metadata( parent_class_name: Some(declaring_class_name.to_string()), parameter_members: Vec::new(), required_parameter_count: 0, - is_final: false, + is_final, is_abstract: false, is_interface: false, is_trait: false, @@ -936,7 +937,10 @@ fn reflection_class_constant_metadata( is_readonly: false, is_instantiable: false, modifiers: 0, - member_flags: ReflectionMemberFlags::default(), + member_flags: ReflectionMemberFlags { + is_final, + ..ReflectionMemberFlags::default() + }, } }) .unwrap_or_else(empty_reflection_metadata), @@ -1288,6 +1292,7 @@ fn reflection_class_constant_reflection_members( class_name, case.attribute_names.clone(), case.attribute_args.clone(), + false, &mut members, &mut seen, ); @@ -1316,6 +1321,7 @@ fn reflection_class_constant_reflection_members( .get(constant_name) .cloned() .unwrap_or_default(), + current_info.final_constants.contains(constant_name), &mut members, &mut seen, ); @@ -1363,6 +1369,7 @@ fn collect_interface_constant_reflection_members( interface_name, Vec::new(), Vec::new(), + interface_info.final_constants.contains(constant_name), members, seen, ); @@ -1386,6 +1393,7 @@ fn reflection_trait_constant_reflection_members( trait_name, Vec::new(), Vec::new(), + false, &mut members, &mut seen, ); @@ -1401,6 +1409,7 @@ fn push_unique_constant_reflection_member( declaring_class_name: &str, attr_names: Vec, attr_args: Vec>>, + is_final: bool, members: &mut Vec, seen: &mut std::collections::HashSet, ) { @@ -1412,7 +1421,10 @@ fn push_unique_constant_reflection_member( declaring_class_name: Some(declaring_class_name.to_string()), attr_names, attr_args, - flags: ReflectionMemberFlags::default(), + flags: ReflectionMemberFlags { + is_final, + ..ReflectionMemberFlags::default() + }, required_parameter_count: 0, parameters: Vec::new(), }); @@ -3663,6 +3675,9 @@ fn emit_reflection_member_flag_properties( flags.is_readonly, )?; } + "ReflectionClassConstant" => { + emit_reflection_owner_bool_property(ctx, class_name, "__is_final", flags.is_final)?; + } _ => {} } Ok(()) diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 332e0ae865..1a86acc9cf 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -1394,6 +1394,7 @@ mod tests { is_readonly_class: false, allow_dynamic_properties: false, constants: HashMap::new(), + final_constants: HashSet::new(), attribute_names: Vec::new(), attribute_args: Vec::new(), method_attribute_names: HashMap::new(), diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index e52f8824d5..af7339b442 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -167,6 +167,7 @@ fn class_info(_class_name: &str) -> ClassInfo { is_readonly_class: false, allow_dynamic_properties: true, constants: HashMap::new(), + final_constants: Default::default(), attribute_names: Vec::new(), attribute_args: Vec::new(), method_attribute_names: HashMap::new(), diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index cdf6895a73..a36ff7d649 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1944,6 +1944,18 @@ fn add_reflection_member_flag_methods( "__is_readonly", )); } + if class_name == "ReflectionClassConstant" { + properties.push(builtin_property( + "__is_final", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method( + "isFinal", + "__is_final", + )); + } if class_name == "ReflectionMethod" { for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { properties.push(builtin_property( diff --git a/src/types/checker/schema/classes/mod.rs b/src/types/checker/schema/classes/mod.rs index 070aa730cd..55ab3d1943 100644 --- a/src/types/checker/schema/classes/mod.rs +++ b/src/types/checker/schema/classes/mod.rs @@ -69,6 +69,7 @@ pub(crate) fn build_class_info_recursive( properties::apply_properties(&mut state, &class, checker)?; methods::apply_methods(&mut state, &class, checker)?; interfaces::collect_interfaces(&mut state, &class, class_map, checker)?; + validate_final_constant_constraints(&class, parent_info.as_ref(), &state, checker)?; interfaces::validate_interface_contracts( &mut state, &class, @@ -187,6 +188,50 @@ fn validate_parent_constraints( Ok(()) } +/// Validates PHP final class-constant inheritance constraints for one class. +fn validate_final_constant_constraints( + class: &FlattenedClass, + parent_info: Option<&ClassInfo>, + state: &ClassBuildState, + checker: &Checker, +) -> Result<(), CompileError> { + for constant in &class.constants { + if constant.is_final && constant.visibility == crate::parser::ast::Visibility::Private { + return Err(CompileError::new( + constant.span, + &format!( + "Private constant {}::{} cannot be final", + class.name, constant.name + ), + )); + } + if parent_info.is_some_and(|parent| parent.final_constants.contains(&constant.name)) { + return Err(CompileError::new( + constant.span, + &format!( + "{}::{} cannot override final constant", + class.name, constant.name + ), + )); + } + for interface_name in &state.interfaces { + let Some(interface_info) = checker.interfaces.get(interface_name) else { + continue; + }; + if interface_info.final_constants.contains(&constant.name) { + return Err(CompileError::new( + constant.span, + &format!( + "{}::{} cannot override final interface constant", + class.name, constant.name + ), + )); + } + } + } + Ok(()) +} + /// Builds a mapping from constructor parameter position to promoted property name for `class`. /// /// If `class` defines `__construct`, maps each parameter (by order) to the property it promotes, diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index 4923d957f4..b3d8b6d58d 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -140,6 +140,12 @@ impl ClassBuildState { )) }) .collect::, CompileError>>()?, + final_constants: class + .constants + .iter() + .filter(|constant| constant.is_final) + .map(|constant| constant.name.clone()) + .collect(), attribute_names: collect_attribute_names(&class.attributes), attribute_args, method_attribute_names: self.method_attribute_names, diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 8cc88d7338..263e4a0365 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -394,10 +394,14 @@ pub(crate) fn insert_enum_metadata( // User-declared enum constants. Values are kept as their parsed expressions, matching the // class-constant representation. let mut constants = HashMap::new(); + let mut final_constants = HashSet::new(); let mut constant_attribute_names = HashMap::new(); let mut constant_attribute_args = HashMap::new(); for constant in user_constants { constants.insert(constant.name.clone(), constant.value.clone()); + if constant.is_final { + final_constants.insert(constant.name.clone()); + } constant_attribute_names.insert( constant.name.clone(), collect_attribute_names(&constant.attributes), @@ -427,6 +431,7 @@ pub(crate) fn insert_enum_metadata( is_readonly_class: true, allow_dynamic_properties: false, constants, + final_constants, attribute_names: Vec::new(), attribute_args: Vec::new(), method_attribute_names: HashMap::new(), diff --git a/src/types/checker/schema/interfaces.rs b/src/types/checker/schema/interfaces.rs index f86b3b2f6e..56336481a2 100644 --- a/src/types/checker/schema/interfaces.rs +++ b/src/types/checker/schema/interfaces.rs @@ -318,6 +318,7 @@ pub(crate) fn build_interface_info_recursive( } let mut iface_constants: HashMap = HashMap::new(); + let mut final_constants = HashSet::new(); for parent_name in &interface.extends { if let Some(parent_info) = checker.interfaces.get(parent_name) { for (k, v) in &parent_info.constants { @@ -325,10 +326,30 @@ pub(crate) fn build_interface_info_recursive( .entry(k.clone()) .or_insert_with(|| v.clone()); } + final_constants.extend(parent_info.final_constants.iter().cloned()); } } for c in &interface.constants { + for parent_name in &interface.extends { + let Some(parent_info) = checker.interfaces.get(parent_name) else { + continue; + }; + if parent_info.final_constants.contains(&c.name) { + return Err(CompileError::new( + c.span, + &format!( + "{}::{} cannot override final interface constant", + interface.name, c.name + ), + )); + } + } iface_constants.insert(c.name.clone(), c.value.clone()); + if c.is_final { + final_constants.insert(c.name.clone()); + } else { + final_constants.remove(&c.name); + } } checker.interfaces.insert( interface.name.clone(), @@ -345,6 +366,7 @@ pub(crate) fn build_interface_info_recursive( static_method_declaring_interfaces, static_method_order, constants: iface_constants, + final_constants, }, ); *next_interface_id += 1; diff --git a/src/types/schema.rs b/src/types/schema.rs index 3c75022114..ce2b0940a9 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -228,6 +228,8 @@ pub struct InterfaceInfo { pub static_method_order: Vec, /// Interface constants (PHP 5.0+). Inherited from parent interfaces. pub constants: HashMap, + /// Interface constants declared with PHP 8.1+ `final`, including inherited parents. + pub final_constants: HashSet, } /// Class metadata for resolved declarations. Tracks inheritance, properties, @@ -247,6 +249,8 @@ pub struct ClassInfo { /// User-declared class constants (PHP 7.1+). Maps the constant name to /// its value expression — codegen inlines the literal at access time. pub constants: HashMap, + /// Class constants declared with PHP 8.1+ `final`, keyed by constant name. + pub final_constants: HashSet, /// Names of PHP 8 attributes attached to this class declaration, in /// source order. Name resolution stores canonical class-like text without /// a synthetic leading backslash, matching `ReflectionAttribute::getName()`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e658d7f9ba..c52c6f86a9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5619,6 +5619,41 @@ echo EvalConstChild::hidden();'); assert_eq!(out.stdout, "2:7:9:2:5"); } +/// Verifies eval-declared final class constants cannot be redeclared. +#[test] +fn test_eval_declared_final_class_constant_override_fails() { + let err = compile_and_run_expect_failure( + r#"getReflectionConstant("ANSWER"); $all = $ref->getReflectionConstants(); echo $single->getName() . ":"; +echo ($single->isFinal() ? "F" : "f") . ":"; echo count($all) . ":" . $all[0]->getName() . ":"; echo $single->getAttributes()[0]->newInstance()->label() . ":"; echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; @@ -6233,7 +6269,8 @@ $case = $enum->getReflectionConstant("Ready"); $level = $enum->getReflectionConstant("LEVEL"); echo ":" . count($enumAll) . ":" . $enumAll[0]->getName() . ":" . $enumAll[1]->getName(); echo ":" . $case->getAttributes()[0]->newInstance()->label() . ":"; -echo count($level->getAttributes());'); +echo count($level->getAttributes()) . ":"; +echo $level->isFinal() ? "F" : "f";'); "#, ); assert!( @@ -6243,7 +6280,7 @@ echo count($level->getAttributes());'); ); assert_eq!( out.stdout, - "ANSWER:1:ANSWER:const:missing:2:Ready:LEVEL:case:0" + "ANSWER:F:1:ANSWER:const:missing:2:Ready:LEVEL:case:0:F" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index e82ff5e0bd..71a79fde3c 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2529,26 +2529,33 @@ class Marker { } class ConstTarget { #[Marker("const")] - public const ANSWER = 42; + final public const ANSWER = 42; } enum CaseTarget: string { #[Marker("case")] case Ready = "ready"; + final public const LEVEL = 7; } $const = new ReflectionClassConstant(ConstTarget::class, "ANSWER"); $constAttrs = $const->getAttributes(); echo $const->getName() . "/"; +echo ($const->isFinal() ? "final" : "open") . "/"; echo count($constAttrs) . "/"; echo $constAttrs[0]->getName() . "/"; echo $constAttrs[0]->getArguments()[0] . "/"; echo $constAttrs[0]->newInstance()->label() . "\n"; +$listed = (new ReflectionClass(ConstTarget::class))->getReflectionConstants()[0]; +echo ($listed->isFinal() ? "listed-final" : "listed-open") . "\n"; $case = new ReflectionClassConstant(CaseTarget::class, "Ready"); $caseAttrs = $case->getAttributes(); echo $case->getName() . "/"; +echo ($case->isFinal() ? "final" : "open") . "/"; echo count($caseAttrs) . "/"; echo $caseAttrs[0]->getName() . "/"; echo $caseAttrs[0]->getArguments()[0] . "/"; echo $caseAttrs[0]->newInstance()->label() . "\n"; +$level = new ReflectionClassConstant(CaseTarget::class, "LEVEL"); +echo ($level->isFinal() ? "level-final" : "level-open") . "\n"; $unit = new ReflectionEnumUnitCase(CaseTarget::class, "Ready"); $unitAttrs = $unit->getAttributes(); echo $unit->getName() . "/"; @@ -2561,7 +2568,7 @@ echo $backedAttrs[0]->newInstance()->label(); ); assert_eq!( out, - "ANSWER/1/Marker/const/const\nReady/1/Marker/case/case\nReady/case\nReady/case" + "ANSWER/final/1/Marker/const/const\nlisted-final\nReady/open/1/Marker/case/case\nlevel-final\nReady/case\nReady/case" ); } diff --git a/tests/codegen/oop/constants.rs b/tests/codegen/oop/constants.rs index 3a046b7597..1d446b06d4 100644 --- a/tests/codegen/oop/constants.rs +++ b/tests/codegen/oop/constants.rs @@ -161,6 +161,76 @@ echo $b->get(); assert_eq!(out, "100"); } +/// Verifies final class constants cannot be redeclared by subclasses. +#[test] +fn test_final_class_constant_override_fails() { + let err = compile_expect_type_error( + r#" String { output } +/// Compiles a PHP source string through type checking and returns the expected diagnostic. +pub(crate) fn compile_expect_type_error(source: &str) -> String { + let id = TEST_ID.fetch_add(1, Ordering::SeqCst); + let tid = std::thread::current().id(); + let pid = std::process::id(); + let dir = std::env::temp_dir().join(format!("elephc_type_test_{}_{:?}_{}", pid, tid, id)); + fs::create_dir_all(&dir).unwrap(); + + let tokens = elephc::lexer::tokenize(source).expect("tokenize failed"); + let ast = elephc::parser::parse(&tokens).expect("parse failed"); + let synthetic_main = dir.join("test.php"); + let ast = elephc::magic_constants::substitute_file_and_scope_constants(ast, &synthetic_main); + let define_set = HashSet::new(); + let ast = elephc::conditional::apply(ast, &define_set); + let (autoload_registry, ast) = elephc::autoload::Registry::build(&dir, ast); + elephc::codegen::set_autoload_rule_count(autoload_registry.rule_count()); + let resolved = elephc::resolver::resolve(ast, &dir).expect("resolve failed"); + let resolved = elephc::autoload::collect_aliases(resolved); + let resolved = elephc::pdo_prelude::inject_if_used(resolved); + let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); + let resolved = + elephc::autoload::run(resolved, &dir, &autoload_registry).expect("autoload failed"); + let resolved = elephc::optimize::fold_constants(resolved); + let error = match elephc::types::check_with_target(&resolved, target()) { + Ok(_) => panic!("source unexpectedly passed type checking"), + Err(error) => error.to_string(), + }; + + let _ = fs::remove_dir_all(&dir); + error +} + // Compiles a multi-file PHP project (using library directly, not CLI) where the // main entry point is `main_file`. Writes all files to an isolated temp directory, // runs the full pipeline, links, and asserts the binary exits successfully. From 0fdb822b109dada00e5ec06636978cd1961e8f48 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 14:41:50 +0200 Subject: [PATCH 0420/1208] Reflect final trait constants --- src/codegen/lower_inst/objects/reflection.rs | 158 ++++++++++++------ src/ir/module.rs | 4 +- src/ir_lower/program.rs | 29 ++++ src/types/checker/driver/init.rs | 1 + src/types/checker/driver/mod.rs | 29 ++++ .../objects/constructors/reflection.rs | 30 +++- src/types/checker/mod.rs | 2 + tests/codegen/oop/attributes.rs | 46 +++++ 8 files changed, 248 insertions(+), 51 deletions(-) diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 887a52b3a6..9549effb8b 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -56,6 +56,14 @@ struct ReflectionOwnerMetadata { member_flags: ReflectionMemberFlags, } +/// Compile-time metadata for one class/interface/trait/enum constant reflector. +struct ReflectionClassConstantMetadata { + declaring_class_name: String, + attr_names: Vec, + attr_args: Vec>>, + is_final: bool, +} + /// Metadata for one member object returned by `ReflectionClass::getMethods()` or `getProperties()`. #[derive(Clone)] struct ReflectionListedMember { @@ -898,53 +906,9 @@ fn reflection_class_constant_metadata( member_flags: ReflectionMemberFlags::default(), }); } - Ok( - resolve_reflection_class_constant(ctx, &reflected_class, &constant_name) - .map(|(declaring_class_name, info)| { - let is_final = info.final_constants.contains(&constant_name); - let attr_names = info - .constant_attribute_names - .get(&constant_name) - .cloned() - .unwrap_or_default(); - let attr_args = info - .constant_attribute_args - .get(&constant_name) - .cloned() - .unwrap_or_default(); - ReflectionOwnerMetadata { - reflected_name: Some(constant_name), - attr_names, - attr_args, - interface_names: Vec::new(), - trait_names: Vec::new(), - method_names: Vec::new(), - property_names: Vec::new(), - constant_names: Vec::new(), - constant_members: Vec::new(), - constant_reflection_members: Vec::new(), - method_members: Vec::new(), - property_members: Vec::new(), - constructor_member: None, - parent_class_name: Some(declaring_class_name.to_string()), - parameter_members: Vec::new(), - required_parameter_count: 0, - is_final, - is_abstract: false, - is_interface: false, - is_trait: false, - is_enum: false, - is_readonly: false, - is_instantiable: false, - modifiers: 0, - member_flags: ReflectionMemberFlags { - is_final, - ..ReflectionMemberFlags::default() - }, - } - }) - .unwrap_or_else(empty_reflection_metadata), - ) + Ok(reflection_class_constant_lookup(ctx, &reflected_class, &constant_name) + .map(|metadata| reflection_class_constant_owner_metadata(constant_name, metadata)) + .unwrap_or_else(empty_reflection_metadata)) } /// Resolves `ReflectionEnumUnitCase/BackedCase(enum, case)` metadata. @@ -994,6 +958,103 @@ fn reflection_enum_case_metadata( ) } +/// Builds owner metadata for one resolved class/interface/trait/enum constant reflector. +fn reflection_class_constant_owner_metadata( + reflected_name: String, + metadata: ReflectionClassConstantMetadata, +) -> ReflectionOwnerMetadata { + let is_final = metadata.is_final; + ReflectionOwnerMetadata { + reflected_name: Some(reflected_name), + attr_names: metadata.attr_names, + attr_args: metadata.attr_args, + interface_names: Vec::new(), + trait_names: Vec::new(), + method_names: Vec::new(), + property_names: Vec::new(), + constant_names: Vec::new(), + constant_members: Vec::new(), + constant_reflection_members: Vec::new(), + method_members: Vec::new(), + property_members: Vec::new(), + constructor_member: None, + parent_class_name: Some(metadata.declaring_class_name), + parameter_members: Vec::new(), + required_parameter_count: 0, + is_final, + is_abstract: false, + is_interface: false, + is_trait: false, + is_enum: false, + is_readonly: false, + is_instantiable: false, + modifiers: 0, + member_flags: ReflectionMemberFlags { + is_final, + ..ReflectionMemberFlags::default() + }, + } +} + +/// Resolves static metadata for a direct `ReflectionClassConstant` constructor call. +fn reflection_class_constant_lookup( + ctx: &FunctionContext<'_>, + class_name: &str, + constant_name: &str, +) -> Option { + if let Some((declaring_class_name, info)) = + resolve_reflection_class_constant(ctx, class_name, constant_name) + { + return Some(ReflectionClassConstantMetadata { + declaring_class_name: declaring_class_name.to_string(), + attr_names: info + .constant_attribute_names + .get(constant_name) + .cloned() + .unwrap_or_default(), + attr_args: info + .constant_attribute_args + .get(constant_name) + .cloned() + .unwrap_or_default(), + is_final: info.final_constants.contains(constant_name), + }); + } + if let Some(interface_name) = resolve_reflection_interface(ctx, class_name) { + if let Some(info) = ctx.module.interface_infos.get(interface_name) { + if info.constants.contains_key(constant_name) { + return Some(ReflectionClassConstantMetadata { + declaring_class_name: interface_name.to_string(), + attr_names: Vec::new(), + attr_args: Vec::new(), + is_final: info.final_constants.contains(constant_name), + }); + } + } + } + if let Some(trait_name) = resolve_reflection_trait(ctx, class_name) { + if ctx + .module + .declared_trait_constants + .get(trait_name) + .is_some_and(|constants| constants.contains_key(constant_name)) + { + let is_final = ctx + .module + .declared_trait_final_constants + .get(trait_name) + .is_some_and(|constants| constants.contains(constant_name)); + return Some(ReflectionClassConstantMetadata { + declaring_class_name: trait_name.to_string(), + attr_names: Vec::new(), + attr_args: Vec::new(), + is_final, + }); + } + } + None +} + /// Looks up class metadata by PHP-style case-insensitive name. fn resolve_reflection_class<'a>( ctx: &'a FunctionContext<'_>, @@ -1387,13 +1448,14 @@ fn reflection_trait_constant_reflection_members( .map(|constants| { let mut members = Vec::new(); let mut seen = std::collections::HashSet::new(); + let final_constants = ctx.module.declared_trait_final_constants.get(trait_name); for constant_name in constants.keys() { push_unique_constant_reflection_member( constant_name, trait_name, Vec::new(), Vec::new(), - false, + final_constants.is_some_and(|constants| constants.contains(constant_name)), &mut members, &mut seen, ); diff --git a/src/ir/module.rs b/src/ir/module.rs index c663fbfa01..b73a742a2c 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -9,7 +9,7 @@ //! - Runtime helper bodies remain outside EIR; modules reference runtime //! features and metadata needed to select/link helpers. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::codegen::platform::Target; use crate::codegen::RuntimeFeatures; @@ -74,6 +74,7 @@ pub struct Module { pub declared_trait_property_names: HashMap>, pub declared_trait_constant_names: HashMap>, pub declared_trait_constants: HashMap>, + pub declared_trait_final_constants: HashMap>, pub class_infos: HashMap, pub interface_infos: HashMap, pub enum_infos: HashMap, @@ -113,6 +114,7 @@ impl Module { declared_trait_property_names: HashMap::new(), declared_trait_constant_names: HashMap::new(), declared_trait_constants: HashMap::new(), + declared_trait_final_constants: HashMap::new(), class_infos: HashMap::new(), interface_infos: HashMap::new(), enum_infos: HashMap::new(), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 1bda882ba2..168628df59 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -78,6 +78,7 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec module.declared_trait_property_names = collect_declared_trait_property_names(program); module.declared_trait_constant_names = collect_declared_trait_constant_names(program); module.declared_trait_constants = collect_declared_trait_constants(program); + module.declared_trait_final_constants = collect_declared_trait_final_constants(program); module.class_infos = check_result.classes.clone(); module.interface_infos = check_result.interfaces.clone(); module.enum_infos = check_result.enums.clone(); @@ -557,6 +558,34 @@ fn collect_declared_trait_constants(program: &Program) -> HashMap HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .filter(|constant| constant.is_final) + .map(|constant| constant.name.clone()) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_final_constants(body)); + } + _ => {} + } + } + constants +} + /// Recursively collects source-declared names that are present in checked metadata. fn collect_program_declared_names( program: &Program, diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index 48484670bd..eacfde53a3 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -98,6 +98,7 @@ impl Checker { declared_interfaces: HashSet::new(), declared_traits: HashSet::new(), declared_trait_methods: HashMap::new(), + declared_trait_constants: HashMap::new(), current_class: None, current_method: None, current_method_is_static: false, diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 572f31a007..0e8ad3717c 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -86,6 +86,7 @@ pub(super) fn check_types_impl( substitute_relative_class_types_in_flattened_enums(&mut flattened_enums); let declared_traits = collect_declared_trait_names(program); let declared_trait_methods = collect_declared_trait_methods(program); + let declared_trait_constants = collect_declared_trait_constants(program); let mut seen_classes = HashSet::new(); let mut class_map = HashMap::new(); for class in &flattened_classes { @@ -184,6 +185,7 @@ pub(super) fn check_types_impl( checker.declared_interfaces = interface_map.keys().cloned().collect(); checker.declared_traits = declared_traits.clone(); checker.declared_trait_methods = declared_trait_methods; + checker.declared_trait_constants = declared_trait_constants; // Enum names must resolve as types in member positions (property and // promoted-constructor-param types), which are checked during the class // schema pass — before the enum-processing phase populates `enums`. Pre- @@ -367,6 +369,33 @@ fn collect_declared_trait_methods( methods } +/// Collects source-declared trait constant names recursively, including namespace blocks. +fn collect_declared_trait_constants(program: &Program) -> HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .map(|constant| constant.name.clone()) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_constants(body)); + } + _ => {} + } + } + constants +} + /// Builds the reflection-visible signature for a direct trait method. /// /// Trait direct reflection only needs parameter names, defaults, by-reference diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index cd8db6ab26..66559ffeee 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -231,8 +231,10 @@ impl Checker { "ReflectionClassConstant::getAttributes(): enum case has attribute argument metadata that is not supported yet", ); } - let Some((names, args)) = - self.reflection_class_constant_attribute_metadata(class_name, constant_name) + let Some((names, args)) = self + .reflection_class_constant_attribute_metadata(class_name, constant_name) + .or_else(|| self.reflection_interface_constant_metadata(class_name, constant_name)) + .or_else(|| self.reflection_trait_constant_metadata(class_name, constant_name)) else { return Err(CompileError::new( expr.span, @@ -320,6 +322,30 @@ impl Checker { self.reflection_class_constant_attribute_metadata(parent, constant_name) } + /// Returns empty attribute metadata when an interface constant exists. + fn reflection_interface_constant_metadata( + &self, + interface_name: &str, + constant_name: &str, + ) -> Option<(Vec, ReflectionAttributeArgs)> { + self.interfaces + .get(interface_name) + .filter(|info| info.constants.contains_key(constant_name)) + .map(|_| (Vec::new(), Vec::new())) + } + + /// Returns empty attribute metadata when a trait constant exists. + fn reflection_trait_constant_metadata( + &self, + trait_name: &str, + constant_name: &str, + ) -> Option<(Vec, ReflectionAttributeArgs)> { + self.declared_trait_constants + .get(trait_name) + .filter(|constants| constants.contains(constant_name)) + .map(|_| (Vec::new(), Vec::new())) + } + /// Returns cloned enum-case attribute metadata for one case. fn reflection_enum_case_attribute_metadata( &self, diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index 55f80647cb..db50e94126 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -115,6 +115,8 @@ pub(crate) struct Checker { pub declared_traits: HashSet, /// Reflection-visible method signatures declared directly on each trait. pub declared_trait_methods: HashMap>, + /// Reflection-visible class constant names declared directly on each trait. + pub declared_trait_constants: HashMap>, /// Name of the class currently being type-checked (used for `$this` resolution). pub current_class: Option, /// Name of the current method being type-checked, when inside a class body. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 71a79fde3c..e0479654c3 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2572,6 +2572,52 @@ echo $backedAttrs[0]->newInstance()->label(); ); } +/// Verifies trait constants expose final metadata through direct and listed reflection. +#[test] +fn test_reflection_trait_constant_final_metadata() { + let out = compile_and_run( + r#"getDeclaringClass()->getName() . ":"; +echo $direct->isFinal() ? "F" : "f"; +$flag = "?"; +$open = "?"; +foreach ((new ReflectionClass(TraitConstTarget::class))->getReflectionConstants() as $constant) { + if ($constant->getName() === "FLAG") { + $flag = $constant->isFinal() ? "F" : "f"; + } + if ($constant->getName() === "OPEN") { + $open = $constant->isFinal() ? "O" : "o"; + } +} +echo ":" . $flag . $open; +$ifaceDirect = new ReflectionClassConstant(InterfaceConstTarget::class, "LIMIT"); +echo ":" . $ifaceDirect->getDeclaringClass()->getName() . ":"; +echo $ifaceDirect->isFinal() ? "I" : "i"; +$limit = "?"; +$ifaceOpen = "?"; +foreach ((new ReflectionClass(InterfaceConstTarget::class))->getReflectionConstants() as $constant) { + if ($constant->getName() === "LIMIT") { + $limit = $constant->isFinal() ? "I" : "i"; + } + if ($constant->getName() === "OPEN") { + $ifaceOpen = $constant->isFinal() ? "P" : "p"; + } +} +echo ":" . $limit . $ifaceOpen; +"#, + ); + assert_eq!(out, "TraitConstTarget:F:Fo:InterfaceConstTarget:I:Ip"); +} + /// Verifies that `ReflectionClass` accepts `user::class` (lowercase class /// constant) for case-insensitive class resolution and `getAttributes()` /// returns the correct attribute data. From a2f0c827f4761cbd0177d81c38d9dade8a661bab Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 14:47:51 +0200 Subject: [PATCH 0421/1208] Preserve interface constant declaring metadata --- src/codegen/lower_inst/objects/reflection.rs | 85 ++++++++++++++----- .../objects/constructors/reflection.rs | 16 +++- src/types/checker/schema/interfaces.rs | 17 +++- src/types/schema.rs | 2 + tests/codegen/oop/attributes.rs | 44 ++++++++++ 5 files changed, 137 insertions(+), 27 deletions(-) diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 9549effb8b..a8acf29176 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -1020,18 +1020,22 @@ fn reflection_class_constant_lookup( is_final: info.final_constants.contains(constant_name), }); } - if let Some(interface_name) = resolve_reflection_interface(ctx, class_name) { - if let Some(info) = ctx.module.interface_infos.get(interface_name) { - if info.constants.contains_key(constant_name) { - return Some(ReflectionClassConstantMetadata { - declaring_class_name: interface_name.to_string(), - attr_names: Vec::new(), - attr_args: Vec::new(), - is_final: info.final_constants.contains(constant_name), - }); + if let Some((_, class_info)) = resolve_reflection_class(ctx, class_name) { + for interface_name in &class_info.interfaces { + if let Some(metadata) = + reflection_interface_class_constant_lookup(ctx, interface_name, constant_name) + { + return Some(metadata); } } } + if let Some(interface_name) = resolve_reflection_interface(ctx, class_name) { + if let Some(metadata) = + reflection_interface_class_constant_lookup(ctx, interface_name, constant_name) + { + return Some(metadata); + } + } if let Some(trait_name) = resolve_reflection_trait(ctx, class_name) { if ctx .module @@ -1055,6 +1059,44 @@ fn reflection_class_constant_lookup( None } +/// Resolves interface constant metadata with the original declaring interface preserved. +fn reflection_interface_class_constant_lookup( + ctx: &FunctionContext<'_>, + interface_name: &str, + constant_name: &str, +) -> Option { + let interface_name = resolve_reflection_interface(ctx, interface_name)?; + let info = ctx.module.interface_infos.get(interface_name)?; + if !info.constants.contains_key(constant_name) { + return None; + } + let declaring_interface = + interface_constant_declaring_interface(info, interface_name, constant_name); + let is_final = ctx + .module + .interface_infos + .get(declaring_interface) + .is_some_and(|info| info.final_constants.contains(constant_name)); + Some(ReflectionClassConstantMetadata { + declaring_class_name: declaring_interface.to_string(), + attr_names: Vec::new(), + attr_args: Vec::new(), + is_final, + }) +} + +/// Returns the interface that originally declared a visible interface constant. +fn interface_constant_declaring_interface<'a>( + info: &'a InterfaceInfo, + fallback_interface: &'a str, + constant_name: &str, +) -> &'a str { + info.constant_declaring_interfaces + .get(constant_name) + .map(String::as_str) + .unwrap_or(fallback_interface) +} + /// Looks up class metadata by PHP-style case-insensitive name. fn resolve_reflection_class<'a>( ctx: &'a FunctionContext<'_>, @@ -1296,7 +1338,7 @@ fn reflection_interface_constant_members( Ok(members) } -/// Recursively appends interface constants, preserving inherited-interface precedence. +/// Appends flattened interface constants while preserving their declaring interface. fn collect_interface_constant_members( ctx: &FunctionContext<'_>, interface_name: &str, @@ -1306,14 +1348,13 @@ fn collect_interface_constant_members( let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { return Ok(()); }; - for parent in &interface_info.parents { - collect_interface_constant_members(ctx, parent, members, seen)?; - } for (constant_name, value_expr) in &interface_info.constants { if seen.contains(constant_name) { continue; } - let value = reflection_constant_value(ctx, interface_name, None, value_expr, 0)?; + let declaring_interface = + interface_constant_declaring_interface(interface_info, interface_name, constant_name); + let value = reflection_constant_value(ctx, declaring_interface, None, value_expr, 0)?; push_unique_constant_member(constant_name, value, members, seen); } Ok(()) @@ -1411,7 +1452,7 @@ fn reflection_interface_constant_reflection_members( members } -/// Recursively appends interface constant-reflector objects. +/// Appends flattened interface constant-reflector objects with declaring-interface metadata. fn collect_interface_constant_reflection_members( ctx: &FunctionContext<'_>, interface_name: &str, @@ -1421,16 +1462,20 @@ fn collect_interface_constant_reflection_members( let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { return; }; - for parent in &interface_info.parents { - collect_interface_constant_reflection_members(ctx, parent, members, seen); - } for constant_name in interface_info.constants.keys() { + let declaring_interface = + interface_constant_declaring_interface(interface_info, interface_name, constant_name); + let is_final = ctx + .module + .interface_infos + .get(declaring_interface) + .is_some_and(|info| info.final_constants.contains(constant_name)); push_unique_constant_reflection_member( constant_name, - interface_name, + declaring_interface, Vec::new(), Vec::new(), - interface_info.final_constants.contains(constant_name), + is_final, members, seen, ); diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index 66559ffeee..f9b1765228 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -328,10 +328,18 @@ impl Checker { interface_name: &str, constant_name: &str, ) -> Option<(Vec, ReflectionAttributeArgs)> { - self.interfaces - .get(interface_name) - .filter(|info| info.constants.contains_key(constant_name)) - .map(|_| (Vec::new(), Vec::new())) + if let Some(info) = self.interfaces.get(interface_name) { + if info.constants.contains_key(constant_name) { + return Some((Vec::new(), Vec::new())); + } + } + let class_info = self.classes.get(interface_name)?; + class_info.interfaces.iter().find_map(|implemented| { + self.interfaces + .get(implemented) + .filter(|info| info.constants.contains_key(constant_name)) + .map(|_| (Vec::new(), Vec::new())) + }) } /// Returns empty attribute metadata when a trait constant exists. diff --git a/src/types/checker/schema/interfaces.rs b/src/types/checker/schema/interfaces.rs index 56336481a2..5f92bcb8ba 100644 --- a/src/types/checker/schema/interfaces.rs +++ b/src/types/checker/schema/interfaces.rs @@ -318,13 +318,22 @@ pub(crate) fn build_interface_info_recursive( } let mut iface_constants: HashMap = HashMap::new(); + let mut constant_declaring_interfaces = HashMap::new(); let mut final_constants = HashSet::new(); for parent_name in &interface.extends { if let Some(parent_info) = checker.interfaces.get(parent_name) { for (k, v) in &parent_info.constants { - iface_constants - .entry(k.clone()) - .or_insert_with(|| v.clone()); + if !iface_constants.contains_key(k) { + iface_constants.insert(k.clone(), v.clone()); + constant_declaring_interfaces.insert( + k.clone(), + parent_info + .constant_declaring_interfaces + .get(k) + .cloned() + .unwrap_or_else(|| parent_name.clone()), + ); + } } final_constants.extend(parent_info.final_constants.iter().cloned()); } @@ -345,6 +354,7 @@ pub(crate) fn build_interface_info_recursive( } } iface_constants.insert(c.name.clone(), c.value.clone()); + constant_declaring_interfaces.insert(c.name.clone(), interface.name.clone()); if c.is_final { final_constants.insert(c.name.clone()); } else { @@ -366,6 +376,7 @@ pub(crate) fn build_interface_info_recursive( static_method_declaring_interfaces, static_method_order, constants: iface_constants, + constant_declaring_interfaces, final_constants, }, ); diff --git a/src/types/schema.rs b/src/types/schema.rs index ce2b0940a9..52c5ff1837 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -228,6 +228,8 @@ pub struct InterfaceInfo { pub static_method_order: Vec, /// Interface constants (PHP 5.0+). Inherited from parent interfaces. pub constants: HashMap, + /// Declaring interface for each visible constant, keyed by case-sensitive constant name. + pub constant_declaring_interfaces: HashMap, /// Interface constants declared with PHP 8.1+ `final`, including inherited parents. pub final_constants: HashSet, } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index e0479654c3..f001bdbb35 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2618,6 +2618,50 @@ echo ":" . $limit . $ifaceOpen; assert_eq!(out, "TraitConstTarget:F:Fo:InterfaceConstTarget:I:Ip"); } +/// Verifies interface constant reflection keeps the interface that declared each constant. +#[test] +fn test_reflection_interface_constant_declaring_metadata() { + let out = compile_and_run( + r#"getDeclaringClass()->getName() . ":"; +echo $shared->getDeclaringClass()->getName() . ":"; +echo $implRoot->getDeclaringClass()->getName() . ":"; +echo $implShared->getDeclaringClass()->getName() . ":"; +echo $implLock->getDeclaringClass()->getName() . ":"; +echo $implLock->isFinal() ? "F" : "f"; +$all = (new ReflectionClass(InterfaceConstImpl::class))->getConstants(); +echo ":" . $all["ROOT"] . ":" . $all["SHARED"] . ":" . $all["LOCK"]; +$decls = ["ROOT" => "?", "SHARED" => "?", "LOCK" => "?"]; +$finals = ["ROOT" => "?", "SHARED" => "?", "LOCK" => "?"]; +foreach ((new ReflectionClass(InterfaceConstImpl::class))->getReflectionConstants() as $constant) { + $name = $constant->getName(); + $decls[$name] = $constant->getDeclaringClass()->getName(); + $finals[$name] = $constant->isFinal() ? "F" : "f"; +} +echo ":" . $decls["ROOT"] . ":" . $decls["SHARED"] . ":" . $decls["LOCK"]; +echo ":" . $finals["ROOT"] . ":" . $finals["SHARED"] . ":" . $finals["LOCK"]; +"#, + ); + assert_eq!( + out, + "InterfaceConstBase:InterfaceConstChild:InterfaceConstBase:InterfaceConstChild:InterfaceConstBase:F:1:3:5:InterfaceConstBase:InterfaceConstChild:InterfaceConstBase:f:f:F" + ); +} + /// Verifies that `ReflectionClass` accepts `user::class` (lowercase class /// constant) for case-insensitive class resolution and `getAttributes()` /// returns the correct attribute data. From e01283c93b6d234fca289bcf2db85305dd5d1581 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 15:05:05 +0200 Subject: [PATCH 0422/1208] Expose ReflectionClassConstant modifiers --- .../elephc-eval/src/interpreter/reflection.rs | 47 +++++--- .../interpreter/tests/support/object_ops.rs | 9 ++ docs/php/classes.md | 6 +- docs/php/eval.md | 6 +- src/codegen/eval_reflection_owner_helpers.rs | 100 +++++++++--------- src/codegen/lower_inst/objects/reflection.rs | 98 ++++++++++++++--- src/codegen_support/runtime/data/user.rs | 1 + src/ir/module.rs | 2 + src/ir_lower/program.rs | 33 +++++- src/ir_lower/tests/exhaustive.rs | 1 + src/types/checker/builtin_types/reflection.rs | 47 +++++++- src/types/checker/schema/classes/state.rs | 5 + src/types/checker/schema/enums.rs | 3 + src/types/schema.rs | 2 + tests/codegen/eval.rs | 58 ++++++++++ tests/codegen/oop/attributes.rs | 53 ++++++++++ 16 files changed, 381 insertions(+), 90 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 53811acec3..90e3272bbc 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -437,14 +437,11 @@ fn eval_reflection_class_constant_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let (declaring_class_name, attributes, is_final) = + let (declaring_class_name, attributes, visibility, is_final) = eval_reflection_class_constant_metadata(reflected_name, constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - let flags = if is_final { - EVAL_REFLECTION_CLASS_FLAG_FINAL - } else { - 0 - }; + let flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, constant_name, @@ -456,7 +453,7 @@ fn eval_reflection_class_constant_object_result( Some(&declaring_class_name), &[], flags, - 0, + modifiers, context, values, ) @@ -629,14 +626,11 @@ fn eval_reflection_class_constant_new( return Ok(None); } let constant_name = eval_reflection_string_arg(args[1], values)?; - let (declaring_class_name, attributes, is_final) = + let (declaring_class_name, attributes, visibility, is_final) = eval_reflection_class_constant_metadata(&class_name, &constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - let flags = if is_final { - EVAL_REFLECTION_CLASS_FLAG_FINAL - } else { - 0 - }; + let flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, &constant_name, @@ -648,7 +642,7 @@ fn eval_reflection_class_constant_new( Some(&declaring_class_name), &[], flags, - 0, + modifiers, context, values, ) @@ -1342,15 +1336,33 @@ fn eval_reflection_class_modifiers( modifiers } -/// Returns declaring class, attributes, and finality for an eval class constant or enum case. +/// Computes PHP's `ReflectionClassConstant::getModifiers()` bitmask for eval metadata. +fn eval_reflection_class_constant_modifiers(visibility: EvalVisibility, is_final: bool) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_final { + modifiers |= 32; + } + modifiers +} + +/// Returns declaring class, attributes, visibility, and finality for an eval class constant or enum case. fn eval_reflection_class_constant_metadata( class_name: &str, constant_name: &str, context: &ElephcEvalContext, -) -> Option<(String, Vec, bool)> { +) -> Option<(String, Vec, EvalVisibility, bool)> { if let Some(enum_decl) = context.enum_decl(class_name) { if let Some(case) = enum_decl.case(constant_name) { - return Some((enum_decl.name().to_string(), case.attributes().to_vec(), false)); + return Some(( + enum_decl.name().to_string(), + case.attributes().to_vec(), + EvalVisibility::Public, + false, + )); } } context.class_constant(class_name, constant_name).map( @@ -1358,6 +1370,7 @@ fn eval_reflection_class_constant_metadata( ( declaring_class, constant.attributes().to_vec(), + constant.visibility(), constant.is_final(), ) }, diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 0fa9e7dbac..d538eeeda3 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -553,7 +553,16 @@ impl FakeOps { properties.push(("__required_parameter_count".to_string(), modifiers_cell)); } if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { + let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; + let is_protected = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; + let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + properties.push(("__is_public".to_string(), is_public)); + properties.push(("__is_protected".to_string(), is_protected)); + properties.push(("__is_private".to_string(), is_private)); properties.push(("__is_final".to_string(), is_final)); + properties.push(("__modifiers".to_string(), modifiers_cell)); } if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { let position = self.int(modifiers as i64)?; diff --git a/docs/php/classes.md b/docs/php/classes.md index 2fb7814d54..9ecac3ed2a 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1218,7 +1218,11 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | +| `ReflectionClassConstant::isPublic()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant or enum case is public | +| `ReflectionClassConstant::isProtected()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant is protected; enum cases report `false` | +| `ReflectionClassConstant::isPrivate()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant is private; enum cases report `false` | | `ReflectionClassConstant::isFinal()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class/interface/trait/enum constant is final; enum cases report `false` | +| `ReflectionClassConstant::getModifiers()` | Same as `ReflectionClassConstant::getName()` | Return PHP's `ReflectionClassConstant::IS_*` visibility/finality bitmask | | `ReflectionEnumUnitCase::getName()` / `ReflectionEnumBackedCase::getName()` | `new ReflectionEnumUnitCase($enum_name, $case_name)` or `new ReflectionEnumBackedCase($enum_name, $case_name)` | Return the reflected enum-case name | | `ReflectionEnumUnitCase::getAttributes()` / `ReflectionEnumBackedCase::getAttributes()` | Same as enum-case `getName()` | Return `ReflectionAttribute` objects for enum-case attributes | | `ReflectionEnumUnitCase::getDeclaringClass()` / `ReflectionEnumBackedCase::getDeclaringClass()` | Same as enum-case `getName()` | Return a `ReflectionClass` object for the enum that declares the reflected case | @@ -1287,7 +1291,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getAttributes()`, `getDeclaringClass()`, and `isFinal()`; `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getAttributes()`, `getDeclaringClass()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 2c0fcc924f..3e6fdf5623 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -242,8 +242,10 @@ when the variadic container itself is not rebound, and are reported through and enum-case attributes through the same materialized `ReflectionAttribute` shape; their `getName()` methods return the reflected constant or case name, and `getDeclaringClass()` returns the declaring class or enum as a -`ReflectionClass`. `ReflectionClassConstant::isFinal()` reports final -class/interface/trait/enum constants and returns `false` for enum cases. +`ReflectionClass`. `ReflectionClassConstant::isPublic()`, `isProtected()`, +`isPrivate()`, `isFinal()`, and `getModifiers()` report visibility/finality +metadata with PHP's `ReflectionClassConstant::IS_*` bitmasks; enum cases report +public, non-final constants. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 9952ba09a5..6b26ca7b67 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1058,8 +1058,6 @@ fn emit_set_owner_member_flags_property_aarch64( layout: &ReflectionOwnerLayout, ) { let ( - Some(is_static_lo), - Some(is_static_hi), Some(is_public_lo), Some(is_public_hi), Some(is_protected_lo), @@ -1067,8 +1065,6 @@ fn emit_set_owner_member_flags_property_aarch64( Some(is_private_lo), Some(is_private_hi), ) = ( - layout.is_static_lo, - layout.is_static_hi, layout.is_public_lo, layout.is_public_hi, layout.is_protected_lo, @@ -1081,9 +1077,11 @@ fn emit_set_owner_member_flags_property_aarch64( }; emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection member predicate flags emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer - emitter.instruction("and x10, x11, #1"); // extract the static-member flag as a boolean - abi::emit_store_to_address(emitter, "x10", "x9", is_static_lo); - abi::emit_store_zero_to_address(emitter, "x9", is_static_hi); + if let (Some(is_static_lo), Some(is_static_hi)) = (layout.is_static_lo, layout.is_static_hi) { + emitter.instruction("and x10, x11, #1"); // extract the static-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_static_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_static_hi); + } emitter.instruction("lsr x10, x11, #1"); // move the public-member bit into position emitter.instruction("and x10, x10, #1"); // extract the public-member flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_public_lo); @@ -1104,22 +1102,25 @@ fn emit_set_owner_member_flags_property_aarch64( abi::emit_store_to_address(emitter, "x10", "x9", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); } - let (Some(is_final_lo), Some(is_final_hi), Some(is_abstract_lo), Some(is_abstract_hi)) = ( - layout.is_final_lo, - layout.is_final_hi, - layout.is_abstract_lo, - layout.is_abstract_hi, - ) else { - return; - }; - emitter.instruction("lsr x10, x11, #4"); // move the final-method bit into position - emitter.instruction("and x10, x10, #1"); // extract the final-method flag as a boolean - abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); - abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); - emitter.instruction("lsr x10, x11, #5"); // move the abstract-method bit into position - emitter.instruction("and x10, x10, #1"); // extract the abstract-method flag as a boolean - abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); - abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); + if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { + emitter.instruction("ldr x10, [sp, #96]"); // reload PHP Reflection member getModifiers() bitmask + abi::emit_store_to_address(emitter, "x10", "x9", modifiers_lo); + abi::emit_store_zero_to_address(emitter, "x9", modifiers_hi); + } + if let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) { + emitter.instruction("lsr x10, x11, #4"); // move the final-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the final-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); + } + if let (Some(is_abstract_lo), Some(is_abstract_hi)) = + (layout.is_abstract_lo, layout.is_abstract_hi) + { + emitter.instruction("lsr x10, x11, #5"); // move the abstract-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-member flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); + } } /// Stores incoming bit-zero finality for ARM64 owners without full member flags. @@ -1143,8 +1144,6 @@ fn emit_set_owner_member_flags_property_x86_64( layout: &ReflectionOwnerLayout, ) { let ( - Some(is_static_lo), - Some(is_static_hi), Some(is_public_lo), Some(is_public_hi), Some(is_protected_lo), @@ -1152,8 +1151,6 @@ fn emit_set_owner_member_flags_property_x86_64( Some(is_private_lo), Some(is_private_hi), ) = ( - layout.is_static_lo, - layout.is_static_hi, layout.is_public_lo, layout.is_public_hi, layout.is_protected_lo, @@ -1167,10 +1164,12 @@ fn emit_set_owner_member_flags_property_x86_64( }; emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection member predicate flags emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer - emitter.instruction("mov rax, r11"); // copy flags before extracting the static bit - emitter.instruction("and rax, 1"); // extract the static-member flag as a boolean - abi::emit_store_to_address(emitter, "rax", "r10", is_static_lo); - abi::emit_store_zero_to_address(emitter, "r10", is_static_hi); + if let (Some(is_static_lo), Some(is_static_hi)) = (layout.is_static_lo, layout.is_static_hi) { + emitter.instruction("mov rax, r11"); // copy flags before extracting the static bit + emitter.instruction("and rax, 1"); // extract the static-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_static_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_static_hi); + } emitter.instruction("mov rax, r11"); // copy flags before extracting the public bit emitter.instruction("shr rax, 1"); // move the public-member bit into position emitter.instruction("and rax, 1"); // extract the public-member flag as a boolean @@ -1195,24 +1194,27 @@ fn emit_set_owner_member_flags_property_x86_64( abi::emit_store_to_address(emitter, "rax", "r10", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); } - let (Some(is_final_lo), Some(is_final_hi), Some(is_abstract_lo), Some(is_abstract_hi)) = ( - layout.is_final_lo, - layout.is_final_hi, - layout.is_abstract_lo, - layout.is_abstract_hi, - ) else { - return; - }; - emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit - emitter.instruction("shr rax, 4"); // move the final-method bit into position - emitter.instruction("and rax, 1"); // extract the final-method flag as a boolean - abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); - abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit - emitter.instruction("shr rax, 5"); // move the abstract-method bit into position - emitter.instruction("and rax, 1"); // extract the abstract-method flag as a boolean - abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); - abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); + if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP Reflection member getModifiers() bitmask + abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); + abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); + } + if let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) { + emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit + emitter.instruction("shr rax, 4"); // move the final-member bit into position + emitter.instruction("and rax, 1"); // extract the final-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); + } + if let (Some(is_abstract_lo), Some(is_abstract_hi)) = + (layout.is_abstract_lo, layout.is_abstract_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit + emitter.instruction("shr rax, 5"); // move the abstract-member bit into position + emitter.instruction("and rax, 1"); // extract the abstract-member flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); + } } /// Stores incoming bit-zero finality for x86_64 owners without full member flags. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index a8acf29176..4312c3c00f 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -61,6 +61,7 @@ struct ReflectionClassConstantMetadata { declaring_class_name: String, attr_names: Vec, attr_args: Vec>>, + visibility: Visibility, is_final: bool, } @@ -343,6 +344,9 @@ fn emit_reflection_owner_object( metadata.required_parameter_count, )?; } + if class_name == "ReflectionClassConstant" { + emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + } if class_name == "ReflectionParameter" { if let Some(parameter) = metadata.parameter_members.first() { emit_reflection_parameter_properties(ctx, parameter)?; @@ -513,8 +517,8 @@ fn reflection_class_metadata_for_name( is_enum: false, is_readonly: false, is_instantiable: false, - modifiers: 0, - member_flags: ReflectionMemberFlags::default(), + modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), }); } if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { @@ -562,8 +566,8 @@ fn reflection_class_metadata_for_name( is_enum: false, is_readonly: false, is_instantiable: false, - modifiers: 0, - member_flags: ReflectionMemberFlags::default(), + modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), }); } Ok(empty_reflection_metadata()) @@ -902,8 +906,8 @@ fn reflection_class_constant_metadata( is_enum: false, is_readonly: false, is_instantiable: false, - modifiers: 0, - member_flags: ReflectionMemberFlags::default(), + modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), }); } Ok(reflection_class_constant_lookup(ctx, &reflected_class, &constant_name) @@ -964,6 +968,8 @@ fn reflection_class_constant_owner_metadata( metadata: ReflectionClassConstantMetadata, ) -> ReflectionOwnerMetadata { let is_final = metadata.is_final; + let modifiers = reflection_class_constant_modifiers(&metadata.visibility, is_final); + let member_flags = reflection_member_flags(false, &metadata.visibility, is_final, false, false); ReflectionOwnerMetadata { reflected_name: Some(reflected_name), attr_names: metadata.attr_names, @@ -988,11 +994,8 @@ fn reflection_class_constant_owner_metadata( is_enum: false, is_readonly: false, is_instantiable: false, - modifiers: 0, - member_flags: ReflectionMemberFlags { - is_final, - ..ReflectionMemberFlags::default() - }, + modifiers, + member_flags, } } @@ -1017,6 +1020,11 @@ fn reflection_class_constant_lookup( .get(constant_name) .cloned() .unwrap_or_default(), + visibility: info + .constant_visibilities + .get(constant_name) + .cloned() + .unwrap_or(Visibility::Public), is_final: info.final_constants.contains(constant_name), }); } @@ -1052,6 +1060,13 @@ fn reflection_class_constant_lookup( declaring_class_name: trait_name.to_string(), attr_names: Vec::new(), attr_args: Vec::new(), + visibility: ctx + .module + .declared_trait_constant_visibilities + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + .cloned() + .unwrap_or(Visibility::Public), is_final, }); } @@ -1081,6 +1096,7 @@ fn reflection_interface_class_constant_lookup( declaring_class_name: declaring_interface.to_string(), attr_names: Vec::new(), attr_args: Vec::new(), + visibility: Visibility::Public, is_final, }) } @@ -1394,6 +1410,7 @@ fn reflection_class_constant_reflection_members( class_name, case.attribute_names.clone(), case.attribute_args.clone(), + Visibility::Public, false, &mut members, &mut seen, @@ -1423,6 +1440,11 @@ fn reflection_class_constant_reflection_members( .get(constant_name) .cloned() .unwrap_or_default(), + current_info + .constant_visibilities + .get(constant_name) + .cloned() + .unwrap_or(Visibility::Public), current_info.final_constants.contains(constant_name), &mut members, &mut seen, @@ -1475,6 +1497,7 @@ fn collect_interface_constant_reflection_members( declaring_interface, Vec::new(), Vec::new(), + Visibility::Public, is_final, members, seen, @@ -1500,6 +1523,12 @@ fn reflection_trait_constant_reflection_members( trait_name, Vec::new(), Vec::new(), + ctx.module + .declared_trait_constant_visibilities + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + .cloned() + .unwrap_or(Visibility::Public), final_constants.is_some_and(|constants| constants.contains(constant_name)), &mut members, &mut seen, @@ -1516,6 +1545,7 @@ fn push_unique_constant_reflection_member( declaring_class_name: &str, attr_names: Vec, attr_args: Vec>>, + visibility: Visibility, is_final: bool, members: &mut Vec, seen: &mut std::collections::HashSet, @@ -1528,10 +1558,7 @@ fn push_unique_constant_reflection_member( declaring_class_name: Some(declaring_class_name.to_string()), attr_names, attr_args, - flags: ReflectionMemberFlags { - is_final, - ..ReflectionMemberFlags::default() - }, + flags: reflection_member_flags(false, &visibility, is_final, false, false), required_parameter_count: 0, parameters: Vec::new(), }); @@ -3189,6 +3216,14 @@ fn emit_reflection_member_object( member.required_parameter_count, )?; } + if member_class_name == "ReflectionClassConstant" { + emit_reflection_owner_int_property( + ctx, + member_class_name, + "__modifiers", + reflection_class_constant_modifiers_from_flags(member.flags), + )?; + } emit_reflection_member_flag_properties(ctx, member_class_name, member.flags)?; Ok(()) } @@ -3783,6 +3818,14 @@ fn emit_reflection_member_flag_properties( )?; } "ReflectionClassConstant" => { + emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_protected", + flags.is_protected, + )?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_private", flags.is_private)?; emit_reflection_owner_bool_property(ctx, class_name, "__is_final", flags.is_final)?; } _ => {} @@ -3867,6 +3910,31 @@ fn reflection_class_modifiers( modifiers } +/// Computes PHP's `ReflectionClassConstant::getModifiers()` bitmask. +fn reflection_class_constant_modifiers(visibility: &Visibility, is_final: bool) -> i64 { + let mut modifiers = match visibility { + Visibility::Public => 1, + Visibility::Protected => 2, + Visibility::Private => 4, + }; + if is_final { + modifiers |= 32; + } + modifiers +} + +/// Computes the class-constant modifier bitmask from populated Reflection member flags. +fn reflection_class_constant_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 { + let visibility = if flags.is_private { + Visibility::Private + } else if flags.is_protected { + Visibility::Protected + } else { + Visibility::Public + }; + reflection_class_constant_modifiers(&visibility, flags.is_final) +} + /// Returns one declared property offset from a synthetic Reflection class layout. fn reflection_property_offset(info: &crate::types::ClassInfo, property: &str) -> Result { info.property_offsets.get(property).copied().ok_or_else(|| { diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 1a86acc9cf..90c20fbecc 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -1394,6 +1394,7 @@ mod tests { is_readonly_class: false, allow_dynamic_properties: false, constants: HashMap::new(), + constant_visibilities: HashMap::new(), final_constants: HashSet::new(), attribute_names: Vec::new(), attribute_args: Vec::new(), diff --git a/src/ir/module.rs b/src/ir/module.rs index b73a742a2c..0dcad19674 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -74,6 +74,7 @@ pub struct Module { pub declared_trait_property_names: HashMap>, pub declared_trait_constant_names: HashMap>, pub declared_trait_constants: HashMap>, + pub declared_trait_constant_visibilities: HashMap>, pub declared_trait_final_constants: HashMap>, pub class_infos: HashMap, pub interface_infos: HashMap, @@ -114,6 +115,7 @@ impl Module { declared_trait_property_names: HashMap::new(), declared_trait_constant_names: HashMap::new(), declared_trait_constants: HashMap::new(), + declared_trait_constant_visibilities: HashMap::new(), declared_trait_final_constants: HashMap::new(), class_infos: HashMap::new(), interface_infos: HashMap::new(), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 168628df59..a4dd1f71e4 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -21,7 +21,7 @@ use crate::ir::{ }; use crate::ir_lower::{builtin_datetime, function, LoweringError}; use crate::names::php_symbol_key; -use crate::parser::ast::{ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind}; +use crate::parser::ast::{ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind, Visibility}; use crate::types::{CheckResult, ClassInfo, InterfaceInfo, PhpType}; /// Lowers an optimized typed AST program into a validated EIR module. @@ -78,6 +78,8 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec module.declared_trait_property_names = collect_declared_trait_property_names(program); module.declared_trait_constant_names = collect_declared_trait_constant_names(program); module.declared_trait_constants = collect_declared_trait_constants(program); + module.declared_trait_constant_visibilities = + collect_declared_trait_constant_visibilities(program); module.declared_trait_final_constants = collect_declared_trait_final_constants(program); module.class_infos = check_result.classes.clone(); module.interface_infos = check_result.interfaces.clone(); @@ -558,6 +560,35 @@ fn collect_declared_trait_constants(program: &Program) -> HashMap HashMap> { + let mut constants = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { + name, + constants: trait_constants, + .. + } => { + constants.insert( + name.clone(), + trait_constants + .iter() + .map(|constant| (constant.name.clone(), constant.visibility.clone())) + .collect(), + ); + } + StmtKind::NamespaceBlock { body, .. } => { + constants.extend(collect_declared_trait_constant_visibilities(body)); + } + _ => {} + } + } + constants +} + /// Collects direct PHP final constant names declared by each trait. fn collect_declared_trait_final_constants(program: &Program) -> HashMap> { let mut constants = HashMap::new(); diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index af7339b442..df7cf7b0e7 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -167,6 +167,7 @@ fn class_info(_class_name: &str) -> ClassInfo { is_readonly_class: false, allow_dynamic_properties: true, constants: HashMap::new(), + constant_visibilities: Default::default(), final_constants: Default::default(), attribute_names: Vec::new(), attribute_args: Vec::new(), diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index a36ff7d649..bc562ebea5 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1859,11 +1859,24 @@ fn builtin_reflection_owner_class( properties, methods, attributes: Vec::new(), - constants: Vec::new(), + constants: reflection_owner_constants(name), used_traits: Vec::new(), } } +/// Returns public class constants exposed by a synthetic reflection owner. +fn reflection_owner_constants(class_name: &str) -> Vec { + if class_name == "ReflectionClassConstant" { + return vec![ + builtin_class_const("IS_PUBLIC", 1), + builtin_class_const("IS_PROTECTED", 2), + builtin_class_const("IS_PRIVATE", 4), + builtin_class_const("IS_FINAL", 32), + ]; + } + Vec::new() +} + /// Builds `getNumberOfParameters()` over the retained parameter array. fn builtin_reflection_parameter_count_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -1906,14 +1919,16 @@ fn add_reflection_member_flag_methods( properties: &mut Vec, methods: &mut Vec, ) { - let common_flags = [ - ("__is_static", "isStatic"), + let visibility_flags = [ ("__is_public", "isPublic"), ("__is_protected", "isProtected"), ("__is_private", "isPrivate"), ]; - if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { - for (property, method) in common_flags { + if matches!( + class_name, + "ReflectionMethod" | "ReflectionProperty" | "ReflectionClassConstant" + ) { + for (property, method) in visibility_flags { properties.push(builtin_property( property, Visibility::Private, @@ -1923,6 +1938,18 @@ fn add_reflection_member_flag_methods( methods.push(builtin_reflection_class_bool_method(method, property)); } } + if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { + properties.push(builtin_property( + "__is_static", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method( + "isStatic", + "__is_static", + )); + } if class_name == "ReflectionProperty" { for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { properties.push(builtin_property( @@ -1955,6 +1982,16 @@ fn add_reflection_member_flag_methods( "isFinal", "__is_final", )); + properties.push(builtin_property( + "__modifiers", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + )); + methods.push(builtin_reflection_class_int_method( + "getModifiers", + "__modifiers", + )); } if class_name == "ReflectionMethod" { for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index b3d8b6d58d..b2c48164c8 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -140,6 +140,11 @@ impl ClassBuildState { )) }) .collect::, CompileError>>()?, + constant_visibilities: class + .constants + .iter() + .map(|constant| (constant.name.clone(), constant.visibility.clone())) + .collect(), final_constants: class .constants .iter() diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 263e4a0365..0a4ea2701f 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -394,11 +394,13 @@ pub(crate) fn insert_enum_metadata( // User-declared enum constants. Values are kept as their parsed expressions, matching the // class-constant representation. let mut constants = HashMap::new(); + let mut constant_visibilities = HashMap::new(); let mut final_constants = HashSet::new(); let mut constant_attribute_names = HashMap::new(); let mut constant_attribute_args = HashMap::new(); for constant in user_constants { constants.insert(constant.name.clone(), constant.value.clone()); + constant_visibilities.insert(constant.name.clone(), constant.visibility.clone()); if constant.is_final { final_constants.insert(constant.name.clone()); } @@ -431,6 +433,7 @@ pub(crate) fn insert_enum_metadata( is_readonly_class: true, allow_dynamic_properties: false, constants, + constant_visibilities, final_constants, attribute_names: Vec::new(), attribute_args: Vec::new(), diff --git a/src/types/schema.rs b/src/types/schema.rs index 52c5ff1837..bdd60d11c8 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -251,6 +251,8 @@ pub struct ClassInfo { /// User-declared class constants (PHP 7.1+). Maps the constant name to /// its value expression — codegen inlines the literal at access time. pub constants: HashMap, + /// Class constant visibilities keyed by case-sensitive constant name. + pub constant_visibilities: HashMap, /// Class constants declared with PHP 8.1+ `final`, keyed by constant name. pub final_constants: HashSet, /// Names of PHP 8 attributes attached to this class declaration, in diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c52c6f86a9..d063f43f7c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6699,6 +6699,64 @@ echo $backedAttrs[0]->newInstance()->label();'); ); } +/// Verifies eval ReflectionClassConstant exposes visibility predicates and modifiers. +#[test] +fn test_eval_reflection_class_constant_visibility_and_modifiers() { + let out = compile_and_run_capture( + r#"isPrivate() ? "R" : "r"; +echo $secret->isProtected() ? "P" : "p"; +echo $secret->isPublic() ? "U" : "u"; +echo $secret->isFinal() ? "F" : "f"; +echo ":" . $secret->getModifiers() . "\n"; +$limit = new ReflectionClassConstant("EvalConstVisibilityTarget", "LIMIT"); +echo "LIMIT:"; +echo $limit->isPrivate() ? "R" : "r"; +echo $limit->isProtected() ? "P" : "p"; +echo $limit->isPublic() ? "U" : "u"; +echo $limit->isFinal() ? "F" : "f"; +echo ":" . $limit->getModifiers() . "\n"; +$answer = new ReflectionClassConstant("EvalConstVisibilityTarget", "ANSWER"); +echo "ANSWER:"; +echo $answer->isPrivate() ? "R" : "r"; +echo $answer->isProtected() ? "P" : "p"; +echo $answer->isPublic() ? "U" : "u"; +echo $answer->isFinal() ? "F" : "f"; +echo ":" . $answer->getModifiers() . "\n"; +$case = new ReflectionClassConstant("EvalConstVisibilityEnum", "Ready"); +echo "Ready:"; +echo $case->isPrivate() ? "R" : "r"; +echo $case->isProtected() ? "P" : "p"; +echo $case->isPublic() ? "U" : "u"; +echo $case->isFinal() ? "F" : "f"; +echo ":" . $case->getModifiers() . "\n";'); +echo ReflectionClassConstant::IS_PUBLIC . ":"; +echo ReflectionClassConstant::IS_PROTECTED . ":"; +echo ReflectionClassConstant::IS_PRIVATE . ":"; +echo ReflectionClassConstant::IS_FINAL; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "SECRET:Rpuf:4\nLIMIT:rPuf:2\nANSWER:rpUF:33\nReady:rpUf:1\n1:2:4:32" + ); +} + /// Verifies eval interface and trait constants work through the bridge. #[test] fn test_eval_declared_interface_and_trait_constants() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index f001bdbb35..a1b3fb2774 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2572,6 +2572,59 @@ echo $backedAttrs[0]->newInstance()->label(); ); } +/// Verifies `ReflectionClassConstant` exposes visibility predicates and modifiers. +#[test] +fn test_reflection_class_constant_visibility_and_modifiers() { + let out = compile_and_run( + r#"isPrivate() ? "R" : "r"; +echo $secret->isProtected() ? "P" : "p"; +echo $secret->isPublic() ? "U" : "u"; +echo $secret->isFinal() ? "F" : "f"; +echo ":" . $secret->getModifiers() . "\n"; +$limit = new ReflectionClassConstant(ConstVisibilityTarget::class, "LIMIT"); +echo "LIMIT:"; +echo $limit->isPrivate() ? "R" : "r"; +echo $limit->isProtected() ? "P" : "p"; +echo $limit->isPublic() ? "U" : "u"; +echo $limit->isFinal() ? "F" : "f"; +echo ":" . $limit->getModifiers() . "\n"; +$answer = new ReflectionClassConstant(ConstVisibilityTarget::class, "ANSWER"); +echo "ANSWER:"; +echo $answer->isPrivate() ? "R" : "r"; +echo $answer->isProtected() ? "P" : "p"; +echo $answer->isPublic() ? "U" : "u"; +echo $answer->isFinal() ? "F" : "f"; +echo ":" . $answer->getModifiers() . "\n"; +$case = new ReflectionClassConstant(ConstVisibilityEnum::class, "Ready"); +echo "Ready:"; +echo $case->isPrivate() ? "R" : "r"; +echo $case->isProtected() ? "P" : "p"; +echo $case->isPublic() ? "U" : "u"; +echo $case->isFinal() ? "F" : "f"; +echo ":" . $case->getModifiers() . "\n"; +echo ReflectionClassConstant::IS_PUBLIC . ":"; +echo ReflectionClassConstant::IS_PROTECTED . ":"; +echo ReflectionClassConstant::IS_PRIVATE . ":"; +echo ReflectionClassConstant::IS_FINAL; +"#, + ); + assert_eq!( + out, + "SECRET:Rpuf:4\nLIMIT:rPuf:2\nANSWER:rpUF:33\nReady:rpUf:1\n1:2:4:32" + ); +} + /// Verifies trait constants expose final metadata through direct and listed reflection. #[test] fn test_reflection_trait_constant_final_metadata() { From 14b702b4ca8e5e96501432037539d2fbbc34e6d6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 15:23:30 +0200 Subject: [PATCH 0423/1208] Add ReflectionClassConstant getValue --- .../elephc-eval/src/interpreter/reflection.rs | 38 ++++ .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 4 +- .../interpreter/tests/support/object_ops.rs | 5 + .../interpreter/tests/support/runtime_ops.rs | 2 + .../elephc-eval/src/runtime_hooks/externs.rs | 1 + crates/elephc-eval/src/runtime_hooks/ops.rs | 2 + docs/php/classes.md | 3 +- docs/php/eval.md | 3 +- src/codegen/eval_reflection_owner_helpers.rs | 50 +++++ src/codegen/lower_inst/objects/reflection.rs | 203 ++++++++++++------ src/types/checker/builtin_types/reflection.rs | 10 + tests/codegen/eval.rs | 12 +- tests/codegen/oop/attributes.rs | 12 +- 14 files changed, 276 insertions(+), 70 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 90e3272bbc..5b55d7b230 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -397,6 +397,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( &member.parameters, flags, member.required_parameter_count as u64, + None, context, values, ) @@ -440,6 +441,8 @@ fn eval_reflection_class_constant_object_result( let (declaring_class_name, attributes, visibility, is_final) = eval_reflection_class_constant_metadata(reflected_name, constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; + let constant_value = eval_reflection_constant_value(reflected_name, constant_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; let flags = eval_reflection_member_flags(visibility, false, is_final, false, false); let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); eval_reflection_owner_object( @@ -454,6 +457,7 @@ fn eval_reflection_class_constant_object_result( &[], flags, modifiers, + Some(constant_value), context, values, ) @@ -521,6 +525,7 @@ fn eval_reflection_class_new( &[], metadata.flags, metadata.modifiers, + None, context, values, ) @@ -563,6 +568,7 @@ fn eval_reflection_method_new( &method.parameters, flags, method.required_parameter_count as u64, + None, context, values, ) @@ -605,6 +611,7 @@ fn eval_reflection_property_new( &[], flags, 0, + None, context, values, ) @@ -629,6 +636,8 @@ fn eval_reflection_class_constant_new( let (declaring_class_name, attributes, visibility, is_final) = eval_reflection_class_constant_metadata(&class_name, &constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; + let constant_value = eval_reflection_constant_value(&class_name, &constant_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; let flags = eval_reflection_member_flags(visibility, false, is_final, false, false); let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); eval_reflection_owner_object( @@ -643,6 +652,7 @@ fn eval_reflection_class_constant_new( &[], flags, modifiers, + Some(constant_value), context, values, ) @@ -689,6 +699,7 @@ fn eval_reflection_enum_case_new( &[], 0, 0, + None, context, values, ) @@ -708,6 +719,7 @@ fn eval_reflection_owner_object( parameter_metadata: &[EvalReflectionParameterMetadata], flags: u64, modifiers: u64, + constant_value: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -723,6 +735,7 @@ fn eval_reflection_owner_object( parameter_metadata, flags, modifiers, + constant_value, true, context, values, @@ -742,6 +755,7 @@ fn eval_reflection_owner_object_with_members( parameter_metadata: &[EvalReflectionParameterMetadata], flags: u64, modifiers: u64, + constant_value: Option, include_class_members: bool, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -782,6 +796,10 @@ fn eval_reflection_owner_object_with_members( context, values, )?; + let (constant_value_cell, release_constant_value) = match constant_value { + Some(value) => (value, false), + None => (values.null()?, true), + }; let object = values.reflection_owner_new( owner_kind, reflected_name, @@ -795,6 +813,7 @@ fn eval_reflection_owner_object_with_members( parent_class, flags, modifiers, + constant_value_cell, )?; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { let identity = values.object_identity(object)?; @@ -808,6 +827,9 @@ fn eval_reflection_owner_object_with_members( values.release(method_objects)?; values.release(property_objects)?; values.release(parent_class)?; + if release_constant_value { + values.release(constant_value_cell)?; + } Ok(object) } @@ -859,6 +881,7 @@ fn eval_reflection_full_class_object_result( &[], metadata.flags, metadata.modifiers, + None, context, values, ) @@ -885,6 +908,7 @@ fn eval_reflection_shallow_class_object_result( &[], metadata.flags, metadata.modifiers, + None, false, context, values, @@ -949,6 +973,7 @@ fn eval_reflection_parameter_object_result( Some(default) => eval_method_parameter_default(default, context, values)?, None => values.null()?, }; + let constant_value = values.null()?; let flags = eval_reflection_parameter_flags(parameter); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_PARAMETER, @@ -963,6 +988,7 @@ fn eval_reflection_parameter_object_result( parent_class, flags, parameter.position as u64, + constant_value, )?; values.release(attrs)?; values.release(declaring_function)?; @@ -973,6 +999,7 @@ fn eval_reflection_parameter_object_result( values.release(type_value)?; values.release(default_value)?; values.release(parent_class)?; + values.release(constant_value)?; Ok(object) } @@ -994,6 +1021,7 @@ fn eval_reflection_declaring_function_object_result( &[], metadata.flags, metadata.required_parameter_count as u64, + None, context, values, ) @@ -1030,6 +1058,7 @@ fn eval_reflection_named_type_object_result( let method_objects = values.array_new(0)?; let property_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; let flags = eval_reflection_named_type_flags(type_metadata); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_NAMED_TYPE, @@ -1044,6 +1073,7 @@ fn eval_reflection_named_type_object_result( parent_class, flags, 0, + constant_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1053,6 +1083,7 @@ fn eval_reflection_named_type_object_result( values.release(method_objects)?; values.release(property_objects)?; values.release(parent_class)?; + values.release(constant_value)?; Ok(object) } @@ -1069,6 +1100,7 @@ fn eval_reflection_union_type_object_result( let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; let property_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; let flags = eval_reflection_union_type_flags(type_metadata); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_UNION_TYPE, @@ -1083,6 +1115,7 @@ fn eval_reflection_union_type_object_result( parent_class, flags, 0, + constant_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1092,6 +1125,7 @@ fn eval_reflection_union_type_object_result( values.release(types)?; values.release(property_objects)?; values.release(parent_class)?; + values.release(constant_value)?; Ok(object) } @@ -1108,6 +1142,7 @@ fn eval_reflection_intersection_type_object_result( let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; let property_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_INTERSECTION_TYPE, "", @@ -1121,6 +1156,7 @@ fn eval_reflection_intersection_type_object_result( parent_class, 0, 0, + constant_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1130,6 +1166,7 @@ fn eval_reflection_intersection_type_object_result( values.release(types)?; values.release(property_objects)?; values.release(parent_class)?; + values.release(constant_value)?; Ok(object) } @@ -1181,6 +1218,7 @@ fn eval_reflection_member_object_array_result( &member.parameters, flags, member.required_parameter_count as u64, + None, context, values, )?; diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index b260732240..6add1d172c 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -127,6 +127,7 @@ pub trait RuntimeValueOps { parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, + constant_value: RuntimeCellHandle, ) -> Result; /// Creates a named runtime object without constructor arguments. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 0b2c8435d9..d6bf493335 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1093,6 +1093,8 @@ enum EvalCaseReflectTarget: string { $const_attrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); echo count($const_attrs); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName(); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getValue(); echo ":"; +echo ((new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "E" : "e"; echo ":"; echo $const_attrs[0]->getName(); echo ":"; echo $const_attrs[0]->getArguments()[0]; echo ":"; echo $const_attrs[0]->newInstance()->label(); echo ":"; $case_attrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); @@ -1114,7 +1116,7 @@ return true;"#, assert_eq!( values.output, - "1:ANSWER:F:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:case:Ready:case" + "1:ANSWER:F:42:E:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:case:Ready:case" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index d538eeeda3..2bf12807ac 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -170,6 +170,9 @@ impl FakeOps { (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) } + (FakeValue::Object(properties), "getvalue") if args.is_empty() => { + Self::object_property(&properties, "__value").map_or_else(|| self.null(), Ok) + } (FakeValue::Object(properties), "isstatic") if args.is_empty() => { Self::object_property(&properties, "__is_static") .map_or_else(|| self.bool_value(false), Ok) @@ -454,6 +457,7 @@ impl FakeOps { parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, + constant_value: RuntimeCellHandle, ) -> Result { let class_name = match owner_kind { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", @@ -563,6 +567,7 @@ impl FakeOps { properties.push(("__is_private".to_string(), is_private)); properties.push(("__is_final".to_string(), is_final)); properties.push(("__modifiers".to_string(), modifiers_cell)); + properties.push(("__value".to_string(), constant_value)); } if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { let position = self.int(modifiers as i64)?; diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 9076291c46..9d107ea086 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -136,6 +136,7 @@ impl RuntimeValueOps for FakeOps { parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, + constant_value: RuntimeCellHandle, ) -> Result { self.runtime_reflection_owner_new( owner_kind, @@ -150,6 +151,7 @@ impl RuntimeValueOps for FakeOps { parent_class, flags, modifiers, + constant_value, ) } /// Creates one fake object for eval `new` unit tests. diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 8dc9290360..e3d327c88d 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -88,6 +88,7 @@ unsafe extern "C" { parent_class: *mut RuntimeCell, flags: u64, modifiers: u64, + constant_value: *mut RuntimeCell, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index c41443cdd6..32b6d1efe8 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -213,6 +213,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, + constant_value: RuntimeCellHandle, ) -> Result { Self::handle(unsafe { __elephc_eval_reflection_owner_new( @@ -229,6 +230,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { parent_class.as_ptr(), flags, modifiers, + constant_value.as_ptr(), ) }) } diff --git a/docs/php/classes.md b/docs/php/classes.md index 9ecac3ed2a..7d45b98d07 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1218,6 +1218,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | +| `ReflectionClassConstant::getValue()` | Same as `ReflectionClassConstant::getName()` | Return the reflected class-constant value, or the enum-case object for reflected enum cases | | `ReflectionClassConstant::isPublic()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant or enum case is public | | `ReflectionClassConstant::isProtected()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant is protected; enum cases report `false` | | `ReflectionClassConstant::isPrivate()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant is private; enum cases report `false` | @@ -1291,7 +1292,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getAttributes()`, `getDeclaringClass()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 3e6fdf5623..981a371304 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -241,7 +241,8 @@ when the variadic container itself is not rebound, and are reported through `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant and enum-case attributes through the same materialized `ReflectionAttribute` shape; their `getName()` methods return the reflected constant or case name, -and `getDeclaringClass()` returns the declaring class or enum as a +`ReflectionClassConstant::getValue()` returns the class-constant value or enum +case object, and `getDeclaringClass()` returns the declaring class or enum as a `ReflectionClass`. `ReflectionClassConstant::isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()` report visibility/finality metadata with PHP's `ReflectionClassConstant::IS_*` bitmasks; enum cases report diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 6b26ca7b67..147922675f 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -44,6 +44,8 @@ struct ReflectionOwnerLayout { property_objects_hi: Option, parent_class_lo: Option, parent_class_hi: Option, + value_lo: Option, + value_hi: Option, attrs_lo: usize, attrs_hi: usize, is_final_lo: Option, @@ -204,6 +206,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, constructor_member: Option, parent_class_name: Option, + constant_value: Option, parameter_members: Vec, required_parameter_count: i64, is_final: bool, @@ -60,7 +61,8 @@ struct ReflectionOwnerMetadata { struct ReflectionClassConstantMetadata { declaring_class_name: String, attr_names: Vec, - attr_args: Vec>>, + attr_args: Vec>>, + value: ReflectionConstantValue, visibility: Visibility, is_final: bool, } @@ -72,6 +74,7 @@ struct ReflectionListedMember { declaring_class_name: Option, attr_names: Vec, attr_args: Vec>>, + constant_value: Option, flags: ReflectionMemberFlags, required_parameter_count: i64, parameters: Vec, @@ -84,7 +87,7 @@ struct ReflectionParameterMember { declaring_class_name: Option, declaring_function: Option, attr_names: Vec, - attr_args: Vec>>, + attr_args: Vec>>, position: i64, is_optional: bool, is_variadic: bool, @@ -100,14 +103,14 @@ enum ReflectionDeclaringFunctionMember { Function { name: String, attr_names: Vec, - attr_args: Vec>>, + attr_args: Vec>>, required_parameter_count: i64, }, Method { name: String, declaring_class_name: Option, attr_names: Vec, - attr_args: Vec>>, + attr_args: Vec>>, flags: ReflectionMemberFlags, required_parameter_count: i64, }, @@ -345,6 +348,11 @@ fn emit_reflection_owner_object( )?; } if class_name == "ReflectionClassConstant" { + if let Some(value) = &metadata.constant_value { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, value); + emit_reflection_owner_mixed_property_from_result(ctx, class_name, "__value")?; + } emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; } if class_name == "ReflectionParameter" { @@ -460,6 +468,7 @@ fn reflection_class_metadata_for_name( property_members, constructor_member, parent_class_name: reflection_parent_class_name(ctx, info), + constant_value: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: info.is_final, @@ -484,7 +493,7 @@ fn reflection_class_metadata_for_name( let constant_names = reflection_interface_constant_names(ctx, interface_name); let constant_members = reflection_interface_constant_members(ctx, interface_name)?; let constant_reflection_members = - reflection_interface_constant_reflection_members(ctx, interface_name); + reflection_interface_constant_reflection_members(ctx, interface_name)?; let method_members = ctx .module .interface_infos @@ -508,6 +517,7 @@ fn reflection_class_metadata_for_name( property_members, constructor_member, parent_class_name: None, + constant_value: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -533,7 +543,7 @@ fn reflection_class_metadata_for_name( let constant_names = reflection_trait_constant_names(ctx, trait_name); let constant_members = reflection_trait_constant_members(ctx, trait_name)?; let constant_reflection_members = - reflection_trait_constant_reflection_members(ctx, trait_name); + reflection_trait_constant_reflection_members(ctx, trait_name)?; let method_members = ctx .module .declared_trait_methods @@ -557,6 +567,7 @@ fn reflection_class_metadata_for_name( property_members, constructor_member, parent_class_name: None, + constant_value: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -683,6 +694,7 @@ fn reflection_method_owner_metadata( property_members: Vec::new(), constructor_member: None, parent_class_name: member.declaring_class_name, + constant_value: member.constant_value, parameter_members: member.parameters, required_parameter_count: member.required_parameter_count, is_final: false, @@ -731,6 +743,7 @@ fn reflection_property_metadata( property_members: Vec::new(), constructor_member: None, parent_class_name: declaring_class_name, + constant_value: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -897,6 +910,10 @@ fn reflection_class_constant_metadata( property_members: Vec::new(), constructor_member: None, parent_class_name: Some(enum_name.to_string()), + constant_value: Some(ReflectionConstantValue::EnumCase { + enum_name: enum_name.to_string(), + case_name: constant_name.clone(), + }), parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -910,7 +927,7 @@ fn reflection_class_constant_metadata( member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), }); } - Ok(reflection_class_constant_lookup(ctx, &reflected_class, &constant_name) + Ok(reflection_class_constant_lookup(ctx, &reflected_class, &constant_name)? .map(|metadata| reflection_class_constant_owner_metadata(constant_name, metadata)) .unwrap_or_else(empty_reflection_metadata)) } @@ -946,6 +963,7 @@ fn reflection_enum_case_metadata( property_members: Vec::new(), constructor_member: None, parent_class_name: Some(enum_name.to_string()), + constant_value: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -985,6 +1003,7 @@ fn reflection_class_constant_owner_metadata( property_members: Vec::new(), constructor_member: None, parent_class_name: Some(metadata.declaring_class_name), + constant_value: Some(metadata.value), parameter_members: Vec::new(), required_parameter_count: 0, is_final, @@ -1004,11 +1023,15 @@ fn reflection_class_constant_lookup( ctx: &FunctionContext<'_>, class_name: &str, constant_name: &str, -) -> Option { +) -> Result> { if let Some((declaring_class_name, info)) = resolve_reflection_class_constant(ctx, class_name, constant_name) { - return Some(ReflectionClassConstantMetadata { + let Some(value_expr) = info.constants.get(constant_name) else { + return Ok(None); + }; + let value = reflection_constant_value(ctx, declaring_class_name, Some(info), value_expr, 0)?; + return Ok(Some(ReflectionClassConstantMetadata { declaring_class_name: declaring_class_name.to_string(), attr_names: info .constant_attribute_names @@ -1020,46 +1043,49 @@ fn reflection_class_constant_lookup( .get(constant_name) .cloned() .unwrap_or_default(), + value, visibility: info .constant_visibilities .get(constant_name) .cloned() .unwrap_or(Visibility::Public), is_final: info.final_constants.contains(constant_name), - }); + })); } if let Some((_, class_info)) = resolve_reflection_class(ctx, class_name) { for interface_name in &class_info.interfaces { if let Some(metadata) = - reflection_interface_class_constant_lookup(ctx, interface_name, constant_name) + reflection_interface_class_constant_lookup(ctx, interface_name, constant_name)? { - return Some(metadata); + return Ok(Some(metadata)); } } } if let Some(interface_name) = resolve_reflection_interface(ctx, class_name) { if let Some(metadata) = - reflection_interface_class_constant_lookup(ctx, interface_name, constant_name) + reflection_interface_class_constant_lookup(ctx, interface_name, constant_name)? { - return Some(metadata); + return Ok(Some(metadata)); } } if let Some(trait_name) = resolve_reflection_trait(ctx, class_name) { - if ctx + if let Some(value_expr) = ctx .module .declared_trait_constants .get(trait_name) - .is_some_and(|constants| constants.contains_key(constant_name)) + .and_then(|constants| constants.get(constant_name)) { let is_final = ctx .module .declared_trait_final_constants .get(trait_name) .is_some_and(|constants| constants.contains(constant_name)); - return Some(ReflectionClassConstantMetadata { + let value = reflection_constant_value(ctx, trait_name, None, value_expr, 0)?; + return Ok(Some(ReflectionClassConstantMetadata { declaring_class_name: trait_name.to_string(), attr_names: Vec::new(), attr_args: Vec::new(), + value, visibility: ctx .module .declared_trait_constant_visibilities @@ -1068,10 +1094,10 @@ fn reflection_class_constant_lookup( .cloned() .unwrap_or(Visibility::Public), is_final, - }); + })); } } - None + Ok(None) } /// Resolves interface constant metadata with the original declaring interface preserved. @@ -1079,12 +1105,16 @@ fn reflection_interface_class_constant_lookup( ctx: &FunctionContext<'_>, interface_name: &str, constant_name: &str, -) -> Option { - let interface_name = resolve_reflection_interface(ctx, interface_name)?; - let info = ctx.module.interface_infos.get(interface_name)?; - if !info.constants.contains_key(constant_name) { - return None; - } +) -> Result> { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return Ok(None); + }; + let Some(info) = ctx.module.interface_infos.get(interface_name) else { + return Ok(None); + }; + let Some(value_expr) = info.constants.get(constant_name) else { + return Ok(None); + }; let declaring_interface = interface_constant_declaring_interface(info, interface_name, constant_name); let is_final = ctx @@ -1092,13 +1122,15 @@ fn reflection_interface_class_constant_lookup( .interface_infos .get(declaring_interface) .is_some_and(|info| info.final_constants.contains(constant_name)); - Some(ReflectionClassConstantMetadata { + let value = reflection_constant_value(ctx, declaring_interface, None, value_expr, 0)?; + Ok(Some(ReflectionClassConstantMetadata { declaring_class_name: declaring_interface.to_string(), attr_names: Vec::new(), attr_args: Vec::new(), + value, visibility: Visibility::Public, is_final, - }) + })) } /// Returns the interface that originally declared a visible interface constant. @@ -1410,6 +1442,10 @@ fn reflection_class_constant_reflection_members( class_name, case.attribute_names.clone(), case.attribute_args.clone(), + ReflectionConstantValue::EnumCase { + enum_name: class_name.to_string(), + case_name: case.name.clone(), + }, Visibility::Public, false, &mut members, @@ -1423,10 +1459,12 @@ fn reflection_class_constant_reflection_members( else { break; }; - for constant_name in current_info.constants.keys() { + for (constant_name, value_expr) in ¤t_info.constants { if seen.contains(constant_name) { continue; } + let value = + reflection_constant_value(ctx, resolved_name, Some(current_info), value_expr, 0)?; push_unique_constant_reflection_member( constant_name, resolved_name, @@ -1440,6 +1478,7 @@ fn reflection_class_constant_reflection_members( .get(constant_name) .cloned() .unwrap_or_default(), + value, current_info .constant_visibilities .get(constant_name) @@ -1451,7 +1490,7 @@ fn reflection_class_constant_reflection_members( ); } for interface_name in ¤t_info.interfaces { - for member in reflection_interface_constant_reflection_members(ctx, interface_name) { + for member in reflection_interface_constant_reflection_members(ctx, interface_name)? { push_unique_listed_constant_member(member, &mut members, &mut seen); } } @@ -1467,11 +1506,11 @@ fn reflection_class_constant_reflection_members( fn reflection_interface_constant_reflection_members( ctx: &FunctionContext<'_>, interface_name: &str, -) -> Vec { +) -> Result> { let mut members = Vec::new(); let mut seen = std::collections::HashSet::new(); - collect_interface_constant_reflection_members(ctx, interface_name, &mut members, &mut seen); - members + collect_interface_constant_reflection_members(ctx, interface_name, &mut members, &mut seen)?; + Ok(members) } /// Appends flattened interface constant-reflector objects with declaring-interface metadata. @@ -1480,11 +1519,11 @@ fn collect_interface_constant_reflection_members( interface_name: &str, members: &mut Vec, seen: &mut std::collections::HashSet, -) { +) -> Result<()> { let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { - return; + return Ok(()); }; - for constant_name in interface_info.constants.keys() { + for (constant_name, value_expr) in &interface_info.constants { let declaring_interface = interface_constant_declaring_interface(interface_info, interface_name, constant_name); let is_final = ctx @@ -1492,51 +1531,53 @@ fn collect_interface_constant_reflection_members( .interface_infos .get(declaring_interface) .is_some_and(|info| info.final_constants.contains(constant_name)); + let value = reflection_constant_value(ctx, declaring_interface, None, value_expr, 0)?; push_unique_constant_reflection_member( constant_name, declaring_interface, Vec::new(), Vec::new(), + value, Visibility::Public, is_final, members, seen, ); } + Ok(()) } /// Returns constant-reflector objects for direct trait constants. fn reflection_trait_constant_reflection_members( ctx: &FunctionContext<'_>, trait_name: &str, -) -> Vec { - ctx.module - .declared_trait_constants - .get(trait_name) - .map(|constants| { - let mut members = Vec::new(); - let mut seen = std::collections::HashSet::new(); - let final_constants = ctx.module.declared_trait_final_constants.get(trait_name); - for constant_name in constants.keys() { - push_unique_constant_reflection_member( - constant_name, - trait_name, - Vec::new(), - Vec::new(), - ctx.module - .declared_trait_constant_visibilities - .get(trait_name) - .and_then(|constants| constants.get(constant_name)) - .cloned() - .unwrap_or(Visibility::Public), - final_constants.is_some_and(|constants| constants.contains(constant_name)), - &mut members, - &mut seen, - ); - } - members - }) - .unwrap_or_default() +) -> Result> { + let mut members = Vec::new(); + let mut seen = std::collections::HashSet::new(); + let Some(constants) = ctx.module.declared_trait_constants.get(trait_name) else { + return Ok(members); + }; + let final_constants = ctx.module.declared_trait_final_constants.get(trait_name); + for (constant_name, value_expr) in constants { + let value = reflection_constant_value(ctx, trait_name, None, value_expr, 0)?; + push_unique_constant_reflection_member( + constant_name, + trait_name, + Vec::new(), + Vec::new(), + value, + ctx.module + .declared_trait_constant_visibilities + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + .cloned() + .unwrap_or(Visibility::Public), + final_constants.is_some_and(|constants| constants.contains(constant_name)), + &mut members, + &mut seen, + ); + } + Ok(members) } /// Appends one constant-reflector member if a constant with this name was not already visible. @@ -1545,6 +1586,7 @@ fn push_unique_constant_reflection_member( declaring_class_name: &str, attr_names: Vec, attr_args: Vec>>, + value: ReflectionConstantValue, visibility: Visibility, is_final: bool, members: &mut Vec, @@ -1558,6 +1600,7 @@ fn push_unique_constant_reflection_member( declaring_class_name: Some(declaring_class_name.to_string()), attr_names, attr_args, + constant_value: Some(value), flags: reflection_member_flags(false, &visibility, is_final, false, false), required_parameter_count: 0, parameters: Vec::new(), @@ -1750,6 +1793,7 @@ fn reflection_class_method_member( declaring_class_name, attr_names, attr_args, + constant_value: None, flags, required_parameter_count, parameters, @@ -1808,6 +1852,7 @@ fn reflection_interface_method_member( declaring_class_name: Some(declaring_class_name), attr_names: Vec::new(), attr_args: Vec::new(), + constant_value: None, flags, required_parameter_count, parameters, @@ -1855,6 +1900,7 @@ fn reflection_trait_method_member( declaring_class_name: Some(trait_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), + constant_value: None, flags, required_parameter_count, parameters: reflection_parameter_members_with_declaring_class( @@ -1909,6 +1955,7 @@ fn reflection_class_property_member( .get(property_name) .cloned() .unwrap_or_default(), + constant_value: None, flags, required_parameter_count: 0, parameters: Vec::new(), @@ -1928,6 +1975,7 @@ fn default_method_members( declaring_class_name: Some(declaring_class_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), + constant_value: None, flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), required_parameter_count: 0, parameters: Vec::new(), @@ -1948,6 +1996,7 @@ fn default_property_members( declaring_class_name: Some(declaring_class_name.to_string()), attr_names: Vec::new(), attr_args: Vec::new(), + constant_value: None, flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), required_parameter_count: 0, parameters: Vec::new(), @@ -2592,6 +2641,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { property_members: Vec::new(), constructor_member: None, parent_class_name: None, + constant_value: None, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -3217,6 +3267,11 @@ fn emit_reflection_member_object( )?; } if member_class_name == "ReflectionClassConstant" { + if let Some(value) = &member.constant_value { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, value); + emit_reflection_owner_mixed_property_from_result(ctx, member_class_name, "__value")?; + } emit_reflection_owner_int_property( ctx, member_class_name, @@ -3890,6 +3945,28 @@ fn emit_reflection_owner_int_property( Ok(()) } +/// Stores the current boxed Mixed result into one Reflection owner property. +fn emit_reflection_owner_mixed_property_from_result( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let value_reg = abi::int_result_reg(ctx.emitter); + let owner_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_pop_reg(ctx.emitter, owner_reg); + abi::emit_store_to_address(ctx.emitter, value_reg, owner_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, owner_reg, high_offset); + abi::emit_reg_move(ctx.emitter, value_reg, owner_reg); + Ok(()) +} + /// Computes PHP's `ReflectionClass::getModifiers()` bitmask for class metadata. fn reflection_class_modifiers( is_final: bool, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index bc562ebea5..01a1365202 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1972,6 +1972,16 @@ fn add_reflection_member_flag_methods( )); } if class_name == "ReflectionClassConstant" { + properties.push(builtin_property( + "__value", + Visibility::Private, + Some(mixed_type()), + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), + )); + methods.push(builtin_reflection_class_mixed_method( + "getValue", + "__value", + )); properties.push(builtin_property( "__is_final", Visibility::Private, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d063f43f7c..1480537779 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6739,7 +6739,15 @@ echo $case->isPrivate() ? "R" : "r"; echo $case->isProtected() ? "P" : "p"; echo $case->isPublic() ? "U" : "u"; echo $case->isFinal() ? "F" : "f"; -echo ":" . $case->getModifiers() . "\n";'); +echo ":" . $case->getModifiers() . "\n"; +echo "VALUES:" . $secret->getValue() . ":" . $limit->getValue() . ":" . $answer->getValue() . ":"; +echo $case->getValue() === EvalConstVisibilityEnum::Ready ? "E" : "e"; +echo "\n"; +foreach ((new ReflectionClass("EvalConstVisibilityTarget"))->getReflectionConstants() as $constant) { + if ($constant->getName() === "ANSWER") { + echo "LIST:" . $constant->getValue() . "\n"; + } +}'); echo ReflectionClassConstant::IS_PUBLIC . ":"; echo ReflectionClassConstant::IS_PROTECTED . ":"; echo ReflectionClassConstant::IS_PRIVATE . ":"; @@ -6753,7 +6761,7 @@ echo ReflectionClassConstant::IS_FINAL; ); assert_eq!( out.stdout, - "SECRET:Rpuf:4\nLIMIT:rPuf:2\nANSWER:rpUF:33\nReady:rpUf:1\n1:2:4:32" + "SECRET:Rpuf:4\nLIMIT:rPuf:2\nANSWER:rpUF:33\nReady:rpUf:1\nVALUES:1:2:3:E\nLIST:3\n1:2:4:32" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index a1b3fb2774..26c4e2bfcf 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2616,12 +2616,20 @@ echo ":" . $case->getModifiers() . "\n"; echo ReflectionClassConstant::IS_PUBLIC . ":"; echo ReflectionClassConstant::IS_PROTECTED . ":"; echo ReflectionClassConstant::IS_PRIVATE . ":"; -echo ReflectionClassConstant::IS_FINAL; +echo ReflectionClassConstant::IS_FINAL . "\n"; +echo "VALUES:" . $secret->getValue() . ":" . $limit->getValue() . ":" . $answer->getValue() . ":"; +echo $case->getValue() === ConstVisibilityEnum::Ready ? "E" : "e"; +echo "\n"; +foreach ((new ReflectionClass(ConstVisibilityTarget::class))->getReflectionConstants() as $constant) { + if ($constant->getName() === "ANSWER") { + echo "LIST:" . $constant->getValue(); + } +} "#, ); assert_eq!( out, - "SECRET:Rpuf:4\nLIMIT:rPuf:2\nANSWER:rpUF:33\nReady:rpUf:1\n1:2:4:32" + "SECRET:Rpuf:4\nLIMIT:rPuf:2\nANSWER:rpUF:33\nReady:rpUf:1\n1:2:4:32\nVALUES:1:2:3:E\nLIST:3" ); } From b4d08493fdcaaeb945b5b812a660cf0fb127a762 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 15:42:36 +0200 Subject: [PATCH 0424/1208] Expand enum case Reflection APIs --- .../elephc-eval/src/interpreter/reflection.rs | 69 +++++++++++++++-- .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 7 +- .../interpreter/tests/support/object_ops.rs | 22 ++++++ .../interpreter/tests/support/runtime_ops.rs | 2 + .../elephc-eval/src/runtime_hooks/externs.rs | 1 + crates/elephc-eval/src/runtime_hooks/ops.rs | 2 + docs/php/classes.md | 5 +- docs/php/eval.md | 14 ++-- src/codegen/eval_reflection_owner_helpers.rs | 72 ++++++++++++++++++ src/codegen/lower_inst/objects/reflection.rs | 74 ++++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 37 ++++++++++ tests/codegen/eval.rs | 7 +- tests/codegen/oop/attributes.rs | 15 +++- 14 files changed, 308 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 5b55d7b230..affae7f910 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -26,6 +26,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; +const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -398,6 +399,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( flags, member.required_parameter_count as u64, None, + None, context, values, ) @@ -438,12 +440,15 @@ fn eval_reflection_class_constant_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let (declaring_class_name, attributes, visibility, is_final) = + let (declaring_class_name, attributes, visibility, is_final, is_enum_case) = eval_reflection_class_constant_metadata(reflected_name, constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; let constant_value = eval_reflection_constant_value(reflected_name, constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - let flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + if is_enum_case { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + } let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, @@ -458,6 +463,7 @@ fn eval_reflection_class_constant_object_result( flags, modifiers, Some(constant_value), + None, context, values, ) @@ -526,6 +532,7 @@ fn eval_reflection_class_new( metadata.flags, metadata.modifiers, None, + None, context, values, ) @@ -569,6 +576,7 @@ fn eval_reflection_method_new( flags, method.required_parameter_count as u64, None, + None, context, values, ) @@ -612,6 +620,7 @@ fn eval_reflection_property_new( flags, 0, None, + None, context, values, ) @@ -633,12 +642,15 @@ fn eval_reflection_class_constant_new( return Ok(None); } let constant_name = eval_reflection_string_arg(args[1], values)?; - let (declaring_class_name, attributes, visibility, is_final) = + let (declaring_class_name, attributes, visibility, is_final, is_enum_case) = eval_reflection_class_constant_metadata(&class_name, &constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; let constant_value = eval_reflection_constant_value(&class_name, &constant_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - let flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + if is_enum_case { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + } let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, @@ -653,6 +665,7 @@ fn eval_reflection_class_constant_new( flags, modifiers, Some(constant_value), + None, context, values, ) @@ -683,6 +696,18 @@ fn eval_reflection_enum_case_new( } let case_name = eval_reflection_string_arg(args[1], values)?; let declaring_class_name = enum_decl.name().to_string(); + let case_value = context + .enum_case(&declaring_class_name, &case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let backing_value = if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { + Some( + context + .enum_case_value(&declaring_class_name, &case_name) + .ok_or(EvalStatus::RuntimeFatal)?, + ) + } else { + None + }; let attributes = enum_decl .case(&case_name) .map(|case| case.attributes().to_vec()) @@ -699,7 +724,8 @@ fn eval_reflection_enum_case_new( &[], 0, 0, - None, + Some(case_value), + backing_value, context, values, ) @@ -720,6 +746,7 @@ fn eval_reflection_owner_object( flags: u64, modifiers: u64, constant_value: Option, + backing_value: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -736,6 +763,7 @@ fn eval_reflection_owner_object( flags, modifiers, constant_value, + backing_value, true, context, values, @@ -756,6 +784,7 @@ fn eval_reflection_owner_object_with_members( flags: u64, modifiers: u64, constant_value: Option, + backing_value: Option, include_class_members: bool, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -800,6 +829,10 @@ fn eval_reflection_owner_object_with_members( Some(value) => (value, false), None => (values.null()?, true), }; + let (backing_value_cell, release_backing_value) = match backing_value { + Some(value) => (value, false), + None => (values.null()?, true), + }; let object = values.reflection_owner_new( owner_kind, reflected_name, @@ -814,6 +847,7 @@ fn eval_reflection_owner_object_with_members( flags, modifiers, constant_value_cell, + backing_value_cell, )?; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { let identity = values.object_identity(object)?; @@ -830,6 +864,9 @@ fn eval_reflection_owner_object_with_members( if release_constant_value { values.release(constant_value_cell)?; } + if release_backing_value { + values.release(backing_value_cell)?; + } Ok(object) } @@ -882,6 +919,7 @@ fn eval_reflection_full_class_object_result( metadata.flags, metadata.modifiers, None, + None, context, values, ) @@ -909,6 +947,7 @@ fn eval_reflection_shallow_class_object_result( metadata.flags, metadata.modifiers, None, + None, false, context, values, @@ -974,6 +1013,7 @@ fn eval_reflection_parameter_object_result( None => values.null()?, }; let constant_value = values.null()?; + let backing_value = values.null()?; let flags = eval_reflection_parameter_flags(parameter); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_PARAMETER, @@ -989,6 +1029,7 @@ fn eval_reflection_parameter_object_result( flags, parameter.position as u64, constant_value, + backing_value, )?; values.release(attrs)?; values.release(declaring_function)?; @@ -1000,6 +1041,7 @@ fn eval_reflection_parameter_object_result( values.release(default_value)?; values.release(parent_class)?; values.release(constant_value)?; + values.release(backing_value)?; Ok(object) } @@ -1022,6 +1064,7 @@ fn eval_reflection_declaring_function_object_result( metadata.flags, metadata.required_parameter_count as u64, None, + None, context, values, ) @@ -1059,6 +1102,7 @@ fn eval_reflection_named_type_object_result( let property_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; let constant_value = values.null()?; + let backing_value = values.null()?; let flags = eval_reflection_named_type_flags(type_metadata); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_NAMED_TYPE, @@ -1074,6 +1118,7 @@ fn eval_reflection_named_type_object_result( flags, 0, constant_value, + backing_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1084,6 +1129,7 @@ fn eval_reflection_named_type_object_result( values.release(property_objects)?; values.release(parent_class)?; values.release(constant_value)?; + values.release(backing_value)?; Ok(object) } @@ -1101,6 +1147,7 @@ fn eval_reflection_union_type_object_result( let property_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; let constant_value = values.null()?; + let backing_value = values.null()?; let flags = eval_reflection_union_type_flags(type_metadata); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_UNION_TYPE, @@ -1116,6 +1163,7 @@ fn eval_reflection_union_type_object_result( flags, 0, constant_value, + backing_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1126,6 +1174,7 @@ fn eval_reflection_union_type_object_result( values.release(property_objects)?; values.release(parent_class)?; values.release(constant_value)?; + values.release(backing_value)?; Ok(object) } @@ -1143,6 +1192,7 @@ fn eval_reflection_intersection_type_object_result( let property_objects = values.array_new(0)?; let parent_class = values.bool_value(false)?; let constant_value = values.null()?; + let backing_value = values.null()?; let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_INTERSECTION_TYPE, "", @@ -1157,6 +1207,7 @@ fn eval_reflection_intersection_type_object_result( 0, 0, constant_value, + backing_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1167,6 +1218,7 @@ fn eval_reflection_intersection_type_object_result( values.release(property_objects)?; values.release(parent_class)?; values.release(constant_value)?; + values.release(backing_value)?; Ok(object) } @@ -1219,6 +1271,7 @@ fn eval_reflection_member_object_array_result( flags, member.required_parameter_count as u64, None, + None, context, values, )?; @@ -1387,12 +1440,12 @@ fn eval_reflection_class_constant_modifiers(visibility: EvalVisibility, is_final modifiers } -/// Returns declaring class, attributes, visibility, and finality for an eval class constant or enum case. +/// Returns declaring class, attributes, visibility, finality, and enum-case kind. fn eval_reflection_class_constant_metadata( class_name: &str, constant_name: &str, context: &ElephcEvalContext, -) -> Option<(String, Vec, EvalVisibility, bool)> { +) -> Option<(String, Vec, EvalVisibility, bool, bool)> { if let Some(enum_decl) = context.enum_decl(class_name) { if let Some(case) = enum_decl.case(constant_name) { return Some(( @@ -1400,6 +1453,7 @@ fn eval_reflection_class_constant_metadata( case.attributes().to_vec(), EvalVisibility::Public, false, + true, )); } } @@ -1410,6 +1464,7 @@ fn eval_reflection_class_constant_metadata( constant.attributes().to_vec(), constant.visibility(), constant.is_final(), + false, ) }, ) diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 6add1d172c..955a98565d 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -128,6 +128,7 @@ pub trait RuntimeValueOps { flags: u64, modifiers: u64, constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, ) -> Result; /// Creates a named runtime object without constructor arguments. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index d6bf493335..d282838d0d 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1093,8 +1093,10 @@ enum EvalCaseReflectTarget: string { $const_attrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); echo count($const_attrs); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName(); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isEnumCase() ? "enum" : "plain"; echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getValue(); echo ":"; echo ((new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "E" : "e"; echo ":"; +echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->isEnumCase() ? "enum" : "plain"; echo ":"; echo $const_attrs[0]->getName(); echo ":"; echo $const_attrs[0]->getArguments()[0]; echo ":"; echo $const_attrs[0]->newInstance()->label(); echo ":"; $case_attrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); @@ -1102,9 +1104,12 @@ echo count($case_attrs); echo ":"; echo (new ReflectionClassConstant("EvalCaseRe echo $case_attrs[0]->getName(); echo ":"; echo $case_attrs[0]->getArguments()[0]; echo ":"; $unit_attrs = (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); echo (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo ((new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "unit" : "bad"; echo ":"; echo $unit_attrs[0]->newInstance()->label(); echo ":"; $backed_attrs = (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo ((new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "backed" : "bad"; echo ":"; +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getBackingValue(); echo ":"; echo $backed_attrs[0]->newInstance()->label(); return true;"#, ) @@ -1116,7 +1121,7 @@ return true;"#, assert_eq!( values.output, - "1:ANSWER:F:42:E:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:case:Ready:case" + "1:ANSWER:F:plain:42:E:enum:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:unit:case:Ready:backed:ready:case" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 2bf12807ac..f63961f6b4 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -16,6 +16,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; +const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -173,6 +174,14 @@ impl FakeOps { (FakeValue::Object(properties), "getvalue") if args.is_empty() => { Self::object_property(&properties, "__value").map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "getbackingvalue") if args.is_empty() => { + Self::object_property(&properties, "__backing_value") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "isenumcase") if args.is_empty() => { + Self::object_property(&properties, "__is_enum_case") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "isstatic") if args.is_empty() => { Self::object_property(&properties, "__is_static") .map_or_else(|| self.bool_value(false), Ok) @@ -458,6 +467,7 @@ impl FakeOps { flags: u64, modifiers: u64, constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, ) -> Result { let class_name = match owner_kind { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", @@ -562,13 +572,25 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_enum_case = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE) != 0)?; properties.push(("__is_public".to_string(), is_public)); properties.push(("__is_protected".to_string(), is_protected)); properties.push(("__is_private".to_string(), is_private)); properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_enum_case".to_string(), is_enum_case)); properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__value".to_string(), constant_value)); } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + properties.push(("__value".to_string(), constant_value)); + } + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { + properties.push(("__backing_value".to_string(), backing_value)); + } if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { let position = self.int(modifiers as i64)?; let is_optional = diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 9d107ea086..3418d4553f 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -137,6 +137,7 @@ impl RuntimeValueOps for FakeOps { flags: u64, modifiers: u64, constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, ) -> Result { self.runtime_reflection_owner_new( owner_kind, @@ -152,6 +153,7 @@ impl RuntimeValueOps for FakeOps { flags, modifiers, constant_value, + backing_value, ) } /// Creates one fake object for eval `new` unit tests. diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index e3d327c88d..8301168099 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -89,6 +89,7 @@ unsafe extern "C" { flags: u64, modifiers: u64, constant_value: *mut RuntimeCell, + backing_value: *mut RuntimeCell, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 32b6d1efe8..6ac2f0bde2 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -214,6 +214,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { flags: u64, modifiers: u64, constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, ) -> Result { Self::handle(unsafe { __elephc_eval_reflection_owner_new( @@ -231,6 +232,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { flags, modifiers, constant_value.as_ptr(), + backing_value.as_ptr(), ) }) } diff --git a/docs/php/classes.md b/docs/php/classes.md index 7d45b98d07..860ad4b176 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1219,12 +1219,15 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | | `ReflectionClassConstant::getValue()` | Same as `ReflectionClassConstant::getName()` | Return the reflected class-constant value, or the enum-case object for reflected enum cases | +| `ReflectionClassConstant::isEnumCase()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected constant entry is an enum case | | `ReflectionClassConstant::isPublic()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant or enum case is public | | `ReflectionClassConstant::isProtected()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant is protected; enum cases report `false` | | `ReflectionClassConstant::isPrivate()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant is private; enum cases report `false` | | `ReflectionClassConstant::isFinal()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class/interface/trait/enum constant is final; enum cases report `false` | | `ReflectionClassConstant::getModifiers()` | Same as `ReflectionClassConstant::getName()` | Return PHP's `ReflectionClassConstant::IS_*` visibility/finality bitmask | | `ReflectionEnumUnitCase::getName()` / `ReflectionEnumBackedCase::getName()` | `new ReflectionEnumUnitCase($enum_name, $case_name)` or `new ReflectionEnumBackedCase($enum_name, $case_name)` | Return the reflected enum-case name | +| `ReflectionEnumUnitCase::getValue()` / `ReflectionEnumBackedCase::getValue()` | Same as enum-case `getName()` | Return the reflected enum-case object | +| `ReflectionEnumBackedCase::getBackingValue()` | `new ReflectionEnumBackedCase($enum_name, $case_name)` | Return the scalar backing value for the reflected backed enum case | | `ReflectionEnumUnitCase::getAttributes()` / `ReflectionEnumBackedCase::getAttributes()` | Same as enum-case `getName()` | Return `ReflectionAttribute` objects for enum-case attributes | | `ReflectionEnumUnitCase::getDeclaringClass()` / `ReflectionEnumBackedCase::getDeclaringClass()` | Same as enum-case `getName()` | Return a `ReflectionClass` object for the enum that declares the reflected case | | `ReflectionAttribute::newInstance()` | Internal only | Instantiate the attribute class from captured literal args | @@ -1292,7 +1295,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getAttributes()`, and `getDeclaringClass()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 981a371304..94bf73f09f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -242,11 +242,15 @@ when the variadic container itself is not rebound, and are reported through and enum-case attributes through the same materialized `ReflectionAttribute` shape; their `getName()` methods return the reflected constant or case name, `ReflectionClassConstant::getValue()` returns the class-constant value or enum -case object, and `getDeclaringClass()` returns the declaring class or enum as a -`ReflectionClass`. `ReflectionClassConstant::isPublic()`, `isProtected()`, -`isPrivate()`, `isFinal()`, and `getModifiers()` report visibility/finality -metadata with PHP's `ReflectionClassConstant::IS_*` bitmasks; enum cases report -public, non-final constants. +case object, while `ReflectionEnumUnitCase::getValue()` and +`ReflectionEnumBackedCase::getValue()` return the reflected enum-case object. +`ReflectionEnumBackedCase::getBackingValue()` returns the scalar backing value, +and `getDeclaringClass()` returns the declaring class or enum as a +`ReflectionClass`. `ReflectionClassConstant::isEnumCase()` reports enum cases. +`ReflectionClassConstant::isPublic()`, `isProtected()`, `isPrivate()`, +`isFinal()`, and `getModifiers()` report visibility/finality metadata with +PHP's `ReflectionClassConstant::IS_*` bitmasks; enum cases report public, +non-final constants. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 147922675f..daa2b9537e 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -46,6 +46,8 @@ struct ReflectionOwnerLayout { parent_class_hi: Option, value_lo: Option, value_hi: Option, + backing_value_lo: Option, + backing_value_hi: Option, attrs_lo: usize, attrs_hi: usize, is_final_lo: Option, @@ -74,6 +76,8 @@ struct ReflectionOwnerLayout { is_protected_hi: Option, is_private_lo: Option, is_private_hi: Option, + is_enum_case_lo: Option, + is_enum_case_hi: Option, required_parameter_count_lo: Option, required_parameter_count_hi: Option, position_lo: Option, @@ -207,6 +211,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option Option Option, parent_class_name: Option, constant_value: Option, + backing_value: Option, + is_enum_case: bool, parameter_members: Vec, required_parameter_count: i64, is_final: bool, @@ -75,6 +77,7 @@ struct ReflectionListedMember { attr_names: Vec, attr_args: Vec>>, constant_value: Option, + is_enum_case: bool, flags: ReflectionMemberFlags, required_parameter_count: i64, parameters: Vec, @@ -347,12 +350,30 @@ fn emit_reflection_owner_object( metadata.required_parameter_count, )?; } - if class_name == "ReflectionClassConstant" { + if matches!( + class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { if let Some(value) = &metadata.constant_value { abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); emit_reflection_constant_value_as_mixed(ctx, value); emit_reflection_owner_mixed_property_from_result(ctx, class_name, "__value")?; } + } + if class_name == "ReflectionEnumBackedCase" { + if let Some(value) = &metadata.backing_value { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, value); + emit_reflection_owner_mixed_property_from_result(ctx, class_name, "__backing_value")?; + } + } + if class_name == "ReflectionClassConstant" { + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_enum_case", + metadata.is_enum_case, + )?; emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; } if class_name == "ReflectionParameter" { @@ -469,6 +490,8 @@ fn reflection_class_metadata_for_name( constructor_member, parent_class_name: reflection_parent_class_name(ctx, info), constant_value: None, + backing_value: None, + is_enum_case: false, parameter_members: Vec::new(), required_parameter_count: 0, is_final: info.is_final, @@ -518,6 +541,8 @@ fn reflection_class_metadata_for_name( constructor_member, parent_class_name: None, constant_value: None, + backing_value: None, + is_enum_case: false, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -568,6 +593,8 @@ fn reflection_class_metadata_for_name( constructor_member, parent_class_name: None, constant_value: None, + backing_value: None, + is_enum_case: false, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -695,6 +722,8 @@ fn reflection_method_owner_metadata( constructor_member: None, parent_class_name: member.declaring_class_name, constant_value: member.constant_value, + backing_value: None, + is_enum_case: member.is_enum_case, parameter_members: member.parameters, required_parameter_count: member.required_parameter_count, is_final: false, @@ -744,6 +773,8 @@ fn reflection_property_metadata( constructor_member: None, parent_class_name: declaring_class_name, constant_value: None, + backing_value: None, + is_enum_case: false, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -914,6 +945,8 @@ fn reflection_class_constant_metadata( enum_name: enum_name.to_string(), case_name: constant_name.clone(), }), + backing_value: None, + is_enum_case: true, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -963,7 +996,12 @@ fn reflection_enum_case_metadata( property_members: Vec::new(), constructor_member: None, parent_class_name: Some(enum_name.to_string()), - constant_value: None, + constant_value: Some(ReflectionConstantValue::EnumCase { + enum_name: enum_name.to_string(), + case_name: case_name.clone(), + }), + backing_value: reflection_enum_case_backing_value(case), + is_enum_case: true, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -1004,6 +1042,8 @@ fn reflection_class_constant_owner_metadata( constructor_member: None, parent_class_name: Some(metadata.declaring_class_name), constant_value: Some(metadata.value), + backing_value: None, + is_enum_case: false, parameter_members: Vec::new(), required_parameter_count: 0, is_final, @@ -1448,6 +1488,7 @@ fn reflection_class_constant_reflection_members( }, Visibility::Public, false, + true, &mut members, &mut seen, ); @@ -1485,6 +1526,7 @@ fn reflection_class_constant_reflection_members( .cloned() .unwrap_or(Visibility::Public), current_info.final_constants.contains(constant_name), + false, &mut members, &mut seen, ); @@ -1540,6 +1582,7 @@ fn collect_interface_constant_reflection_members( value, Visibility::Public, is_final, + false, members, seen, ); @@ -1573,6 +1616,7 @@ fn reflection_trait_constant_reflection_members( .cloned() .unwrap_or(Visibility::Public), final_constants.is_some_and(|constants| constants.contains(constant_name)), + false, &mut members, &mut seen, ); @@ -1589,6 +1633,7 @@ fn push_unique_constant_reflection_member( value: ReflectionConstantValue, visibility: Visibility, is_final: bool, + is_enum_case: bool, members: &mut Vec, seen: &mut std::collections::HashSet, ) { @@ -1601,6 +1646,7 @@ fn push_unique_constant_reflection_member( attr_names, attr_args, constant_value: Some(value), + is_enum_case, flags: reflection_member_flags(false, &visibility, is_final, false, false), required_parameter_count: 0, parameters: Vec::new(), @@ -1794,6 +1840,7 @@ fn reflection_class_method_member( attr_names, attr_args, constant_value: None, + is_enum_case: false, flags, required_parameter_count, parameters, @@ -1853,6 +1900,7 @@ fn reflection_interface_method_member( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + is_enum_case: false, flags, required_parameter_count, parameters, @@ -1901,6 +1949,7 @@ fn reflection_trait_method_member( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + is_enum_case: false, flags, required_parameter_count, parameters: reflection_parameter_members_with_declaring_class( @@ -1956,6 +2005,7 @@ fn reflection_class_property_member( .cloned() .unwrap_or_default(), constant_value: None, + is_enum_case: false, flags, required_parameter_count: 0, parameters: Vec::new(), @@ -1976,6 +2026,7 @@ fn default_method_members( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + is_enum_case: false, flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), required_parameter_count: 0, parameters: Vec::new(), @@ -1997,6 +2048,7 @@ fn default_property_members( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + is_enum_case: false, flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), required_parameter_count: 0, parameters: Vec::new(), @@ -2624,6 +2676,14 @@ fn resolve_reflection_enum_case<'a>( }) } +/// Returns a static Reflection value for a backed enum case, when present. +fn reflection_enum_case_backing_value(case: &EnumCaseInfo) -> Option { + match case.value.as_ref()? { + EnumCaseValue::Int(value) => Some(ReflectionConstantValue::Int(*value)), + EnumCaseValue::Str(value) => Some(ReflectionConstantValue::Str(value.clone())), + } +} + /// Returns empty Reflection metadata for unsupported dynamic constructor operands. fn empty_reflection_metadata() -> ReflectionOwnerMetadata { ReflectionOwnerMetadata { @@ -2642,6 +2702,8 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { constructor_member: None, parent_class_name: None, constant_value: None, + backing_value: None, + is_enum_case: false, parameter_members: Vec::new(), required_parameter_count: 0, is_final: false, @@ -3272,6 +3334,12 @@ fn emit_reflection_member_object( emit_reflection_constant_value_as_mixed(ctx, value); emit_reflection_owner_mixed_property_from_result(ctx, member_class_name, "__value")?; } + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__is_enum_case", + member.is_enum_case, + )?; emit_reflection_owner_int_property( ctx, member_class_name, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 01a1365202..ca7cb6b851 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1982,6 +1982,16 @@ fn add_reflection_member_flag_methods( "getValue", "__value", )); + properties.push(builtin_property( + "__is_enum_case", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_bool_method( + "isEnumCase", + "__is_enum_case", + )); properties.push(builtin_property( "__is_final", Visibility::Private, @@ -2003,6 +2013,33 @@ fn add_reflection_member_flag_methods( "__modifiers", )); } + if matches!( + class_name, + "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + properties.push(builtin_property( + "__value", + Visibility::Private, + Some(mixed_type()), + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), + )); + methods.push(builtin_reflection_class_mixed_method( + "getValue", + "__value", + )); + } + if class_name == "ReflectionEnumBackedCase" { + properties.push(builtin_property( + "__backing_value", + Visibility::Private, + Some(mixed_type()), + Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), + )); + methods.push(builtin_reflection_class_mixed_method( + "getBackingValue", + "__backing_value", + )); + } if class_name == "ReflectionMethod" { for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { properties.push(builtin_property( diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1480537779..88fc8b3e11 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6675,16 +6675,21 @@ enum EvalCaseReflectTarget: string { } $constAttrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); echo count($constAttrs) . ":" . (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName() . ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isEnumCase() ? "enum" : "plain"; echo ":"; echo $constAttrs[0]->getName() . ":" . $constAttrs[0]->getArguments()[0] . ":"; echo $constAttrs[0]->newInstance()->label() . ":"; $caseAttrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); echo count($caseAttrs) . ":" . (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->isEnumCase() ? "enum" : "plain"; echo ":"; echo $caseAttrs[0]->getName() . ":" . $caseAttrs[0]->getArguments()[0] . ":"; $unitAttrs = (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); echo (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo ((new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "unit" : "bad"; echo ":"; echo $unitAttrs[0]->newInstance()->label() . ":"; $backedAttrs = (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getName() . ":"; +echo ((new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "backed" : "bad"; echo ":"; +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getBackingValue() . ":"; echo $backedAttrs[0]->newInstance()->label();'); "#, ); @@ -6695,7 +6700,7 @@ echo $backedAttrs[0]->newInstance()->label();'); ); assert_eq!( out.stdout, - "1:ANSWER:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:case:Ready:case" + "1:ANSWER:plain:EvalConstMarker:const:const:1:Ready:enum:EvalConstMarker:case:Ready:unit:case:Ready:backed:ready:case" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 26c4e2bfcf..e1d993f452 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2540,6 +2540,7 @@ $const = new ReflectionClassConstant(ConstTarget::class, "ANSWER"); $constAttrs = $const->getAttributes(); echo $const->getName() . "/"; echo ($const->isFinal() ? "final" : "open") . "/"; +echo ($const->isEnumCase() ? "enum" : "plain") . "/"; echo count($constAttrs) . "/"; echo $constAttrs[0]->getName() . "/"; echo $constAttrs[0]->getArguments()[0] . "/"; @@ -2550,25 +2551,35 @@ $case = new ReflectionClassConstant(CaseTarget::class, "Ready"); $caseAttrs = $case->getAttributes(); echo $case->getName() . "/"; echo ($case->isFinal() ? "final" : "open") . "/"; +echo ($case->isEnumCase() ? "enum" : "plain") . "/"; echo count($caseAttrs) . "/"; echo $caseAttrs[0]->getName() . "/"; echo $caseAttrs[0]->getArguments()[0] . "/"; echo $caseAttrs[0]->newInstance()->label() . "\n"; +foreach ((new ReflectionClass(CaseTarget::class))->getReflectionConstants() as $constant) { + if ($constant->getName() === "Ready") { + echo ($constant->isEnumCase() ? "listed-enum" : "listed-plain") . "\n"; + } +} $level = new ReflectionClassConstant(CaseTarget::class, "LEVEL"); -echo ($level->isFinal() ? "level-final" : "level-open") . "\n"; +echo ($level->isFinal() ? "level-final" : "level-open") . "/"; +echo ($level->isEnumCase() ? "level-enum" : "level-plain") . "\n"; $unit = new ReflectionEnumUnitCase(CaseTarget::class, "Ready"); $unitAttrs = $unit->getAttributes(); echo $unit->getName() . "/"; +echo ($unit->getValue() === CaseTarget::Ready ? "unit-value" : "unit-bad") . "/"; echo $unitAttrs[0]->newInstance()->label() . "\n"; $backed = new ReflectionEnumBackedCase(CaseTarget::class, "Ready"); $backedAttrs = $backed->getAttributes(); echo $backed->getName() . "/"; +echo ($backed->getValue() === CaseTarget::Ready ? "backed-value" : "backed-bad") . "/"; +echo $backed->getBackingValue() . "/"; echo $backedAttrs[0]->newInstance()->label(); "#, ); assert_eq!( out, - "ANSWER/final/1/Marker/const/const\nlisted-final\nReady/open/1/Marker/case/case\nlevel-final\nReady/case\nReady/case" + "ANSWER/final/plain/1/Marker/const/const\nlisted-final\nReady/open/enum/1/Marker/case/case\nlisted-enum\nlevel-final/level-plain\nReady/unit-value/case\nReady/backed-value/ready/case" ); } From a5237c7f1c6ae58f25032cae5d7e47ad1716dab7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 16:05:35 +0200 Subject: [PATCH 0425/1208] Expose ReflectionMethod modifiers --- .../elephc-eval/src/interpreter/reflection.rs | 92 +++++++++++++++++++ .../src/interpreter/runtime_ops.rs | 1 + .../elephc-eval/src/interpreter/statements.rs | 45 ++++++++- .../tests/builtins_class_metadata.rs | 33 ++++++- .../interpreter/tests/support/object_ops.rs | 3 + .../interpreter/tests/support/runtime_ops.rs | 2 + .../elephc-eval/src/runtime_hooks/externs.rs | 1 + crates/elephc-eval/src/runtime_hooks/ops.rs | 2 + docs/php/classes.md | 3 +- docs/php/eval.md | 3 +- src/codegen/eval_reflection_owner_helpers.rs | 32 +++++-- src/codegen/lower_inst/objects/reflection.rs | 35 ++++++- src/types/checker/builtin_types/reflection.rs | 25 +++++ tests/codegen/eval.rs | 7 +- tests/codegen/oop/attributes.rs | 7 +- 15 files changed, 274 insertions(+), 17 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index affae7f910..e998201782 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -386,6 +386,16 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( member.is_abstract, member.is_readonly, ); + let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + eval_reflection_method_modifiers( + member.visibility, + member.is_static, + member.is_final, + member.is_abstract, + ) + } else { + 0 + }; eval_reflection_owner_object( owner_kind, &member_name, @@ -398,6 +408,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( &member.parameters, flags, member.required_parameter_count as u64, + method_modifiers, None, None, context, @@ -462,6 +473,7 @@ fn eval_reflection_class_constant_object_result( &[], flags, modifiers, + 0, Some(constant_value), None, context, @@ -531,6 +543,7 @@ fn eval_reflection_class_new( &[], metadata.flags, metadata.modifiers, + 0, None, None, context, @@ -575,6 +588,12 @@ fn eval_reflection_method_new( &method.parameters, flags, method.required_parameter_count as u64, + eval_reflection_method_modifiers( + method.visibility, + method.is_static, + method.is_final, + method.is_abstract, + ), None, None, context, @@ -619,6 +638,7 @@ fn eval_reflection_property_new( &[], flags, 0, + 0, None, None, context, @@ -664,6 +684,7 @@ fn eval_reflection_class_constant_new( &[], flags, modifiers, + 0, Some(constant_value), None, context, @@ -724,6 +745,7 @@ fn eval_reflection_enum_case_new( &[], 0, 0, + 0, Some(case_value), backing_value, context, @@ -745,6 +767,7 @@ fn eval_reflection_owner_object( parameter_metadata: &[EvalReflectionParameterMetadata], flags: u64, modifiers: u64, + method_modifiers: u64, constant_value: Option, backing_value: Option, context: &mut ElephcEvalContext, @@ -762,6 +785,7 @@ fn eval_reflection_owner_object( parameter_metadata, flags, modifiers, + method_modifiers, constant_value, backing_value, true, @@ -783,6 +807,7 @@ fn eval_reflection_owner_object_with_members( parameter_metadata: &[EvalReflectionParameterMetadata], flags: u64, modifiers: u64, + method_modifiers: u64, constant_value: Option, backing_value: Option, include_class_members: bool, @@ -846,6 +871,7 @@ fn eval_reflection_owner_object_with_members( parent_class, flags, modifiers, + method_modifiers, constant_value_cell, backing_value_cell, )?; @@ -918,6 +944,7 @@ fn eval_reflection_full_class_object_result( &[], metadata.flags, metadata.modifiers, + 0, None, None, context, @@ -946,6 +973,7 @@ fn eval_reflection_shallow_class_object_result( &[], metadata.flags, metadata.modifiers, + 0, None, None, false, @@ -1028,6 +1056,7 @@ fn eval_reflection_parameter_object_result( parent_class, flags, parameter.position as u64, + 0, constant_value, backing_value, )?; @@ -1063,6 +1092,7 @@ fn eval_reflection_declaring_function_object_result( &[], metadata.flags, metadata.required_parameter_count as u64, + eval_reflection_method_modifiers_from_flags(metadata.flags), None, None, context, @@ -1117,6 +1147,7 @@ fn eval_reflection_named_type_object_result( parent_class, flags, 0, + 0, constant_value, backing_value, )?; @@ -1162,6 +1193,7 @@ fn eval_reflection_union_type_object_result( parent_class, flags, 0, + 0, constant_value, backing_value, )?; @@ -1206,6 +1238,7 @@ fn eval_reflection_intersection_type_object_result( parent_class, 0, 0, + 0, constant_value, backing_value, )?; @@ -1258,6 +1291,16 @@ fn eval_reflection_member_object_array_result( member.is_abstract, member.is_readonly, ); + let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + eval_reflection_method_modifiers( + member.visibility, + member.is_static, + member.is_final, + member.is_abstract, + ) + } else { + 0 + }; let member_object = eval_reflection_owner_object( owner_kind, name, @@ -1270,6 +1313,7 @@ fn eval_reflection_member_object_array_result( &member.parameters, flags, member.required_parameter_count as u64, + method_modifiers, None, None, context, @@ -1440,6 +1484,54 @@ fn eval_reflection_class_constant_modifiers(visibility: EvalVisibility, is_final modifiers } +/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask for eval metadata. +fn eval_reflection_method_modifiers( + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, +) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + modifiers +} + +/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from eval member flags. +fn eval_reflection_method_modifiers_from_flags(flags: u64) -> u64 { + let mut modifiers = 0; + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0 { + modifiers |= 1; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0 { + modifiers |= 2; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0 { + modifiers |= 4; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC) != 0 { + modifiers |= 16; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0 { + modifiers |= 32; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0 { + modifiers |= 64; + } + modifiers +} + /// Returns declaring class, attributes, visibility, finality, and enum-case kind. fn eval_reflection_class_constant_metadata( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 955a98565d..40432253cb 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -127,6 +127,7 @@ pub trait RuntimeValueOps { parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, + method_modifiers: u64, constant_value: RuntimeCellHandle, backing_value: RuntimeCellHandle, ) -> Result; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 62469c85b0..a606c8be98 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1834,8 +1834,12 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( class_name: &str, constant_name: &str, context: &mut ElephcEvalContext, - _values: &mut impl RuntimeValueOps, + values: &mut impl RuntimeValueOps, ) -> Result { + if let Some(value) = eval_builtin_reflection_class_constant(class_name, constant_name, values)? + { + return Ok(value); + } let class_name = resolve_eval_static_class_like_name(class_name, context)?; if let Some(case) = context.enum_case(&class_name, constant_name) { return Ok(case); @@ -1849,6 +1853,45 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( .ok_or(EvalStatus::RuntimeFatal) } +/// Resolves eval-visible built-in Reflection class constants. +fn eval_builtin_reflection_class_constant( + class_name: &str, + constant_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + let value = if class_name.eq_ignore_ascii_case("ReflectionClass") { + match constant_name { + "IS_IMPLICIT_ABSTRACT" => Some(16), + "IS_FINAL" => Some(32), + "IS_EXPLICIT_ABSTRACT" => Some(64), + "IS_READONLY" => Some(65_536), + _ => None, + } + } else if class_name.eq_ignore_ascii_case("ReflectionMethod") { + match constant_name { + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_STATIC" => Some(16), + "IS_FINAL" => Some(32), + "IS_ABSTRACT" => Some(64), + _ => None, + } + } else if class_name.eq_ignore_ascii_case("ReflectionClassConstant") { + match constant_name { + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_FINAL" => Some(32), + _ => None, + } + } else { + None + }; + value.map(|value| values.int(value)).transpose() +} + /// Returns the PHP class-name literal for `ClassName::class`-style eval expressions. pub(in crate::interpreter) fn eval_class_name_fetch_result( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index d282838d0d..5e05405799 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -412,6 +412,32 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval can read built-in Reflection `IS_*` class constants. +#[test] +fn execute_program_reads_builtin_reflection_modifier_constants() { + let program = parse_fragment( + br#"echo ReflectionClass::IS_FINAL; echo ":"; +echo ReflectionClass::IS_EXPLICIT_ABSTRACT; echo ":"; +echo ReflectionClass::IS_READONLY; echo ":"; +echo ReflectionMethod::IS_STATIC; echo ":"; +echo ReflectionMethod::IS_PRIVATE; echo ":"; +echo ReflectionMethod::IS_ABSTRACT; echo ":"; +echo ReflectionClassConstant::IS_PUBLIC; echo ":"; +echo ReflectionClassConstant::IS_PROTECTED; echo ":"; +echo ReflectionClassConstant::IS_PRIVATE; echo ":"; +echo ReflectionClassConstant::IS_FINAL; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "32:64:65536:16:4:64:1:2:4:32"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval readonly class metadata. #[test] fn execute_program_reflects_eval_class_readonly_predicate() { @@ -948,13 +974,18 @@ $ref = new ReflectionClass("EvalReflectListTarget"); $methods = $ref->getMethods(); $properties = $ref->getProperties(); echo count($methods); echo ":"; echo count($properties); echo ":"; +echo ReflectionMethod::IS_STATIC; echo ":"; echo ReflectionMethod::IS_PRIVATE; echo ":"; +$direct = new ReflectionMethod("EvalReflectListTarget", "helper"); +echo "D"; echo $direct->getModifiers(); echo ":"; foreach ($methods as $method) { if ($method->getName() === "first") { echo "F"; echo count($method->getAttributes()); + echo "M"; echo $method->getModifiers(); } if ($method->getName() === "helper") { echo $method->isStatic() ? "S" : "s"; echo $method->isPrivate() ? "R" : "r"; + echo "M"; echo $method->getModifiers(); } } echo ":"; @@ -976,7 +1007,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:2:F1SR:V1PTR"); + assert_eq!(values.output, "2:2:16:4:D20:F1M1SRM20:V1PTR"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index f63961f6b4..64089a93dc 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -466,6 +466,7 @@ impl FakeOps { parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, + method_modifiers: u64, constant_value: RuntimeCellHandle, backing_value: RuntimeCellHandle, ) -> Result { @@ -561,10 +562,12 @@ impl FakeOps { let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; let is_abstract = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; + let method_modifiers = self.int(method_modifiers as i64)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__parameters".to_string(), method_objects)); properties.push(("__required_parameter_count".to_string(), modifiers_cell)); + properties.push(("__modifiers".to_string(), method_modifiers)); } if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 3418d4553f..f08a638704 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -136,6 +136,7 @@ impl RuntimeValueOps for FakeOps { parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, + method_modifiers: u64, constant_value: RuntimeCellHandle, backing_value: RuntimeCellHandle, ) -> Result { @@ -152,6 +153,7 @@ impl RuntimeValueOps for FakeOps { parent_class, flags, modifiers, + method_modifiers, constant_value, backing_value, ) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 8301168099..13e545afc5 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -88,6 +88,7 @@ unsafe extern "C" { parent_class: *mut RuntimeCell, flags: u64, modifiers: u64, + method_modifiers: u64, constant_value: *mut RuntimeCell, backing_value: *mut RuntimeCell, ) -> *mut RuntimeCell; diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 6ac2f0bde2..853ea948f4 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -213,6 +213,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { parent_class: RuntimeCellHandle, flags: u64, modifiers: u64, + method_modifiers: u64, constant_value: RuntimeCellHandle, backing_value: RuntimeCellHandle, ) -> Result { @@ -231,6 +232,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { parent_class.as_ptr(), flags, modifiers, + method_modifiers, constant_value.as_ptr(), backing_value.as_ptr(), ) diff --git a/docs/php/classes.md b/docs/php/classes.md index 860ad4b176..6d5d2f5039 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1187,6 +1187,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isPrivate()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is private | | `ReflectionMethod::isFinal()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is final | | `ReflectionMethod::isAbstract()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is abstract | +| `ReflectionMethod::getModifiers()` | `new ReflectionMethod($class_name, $method_name)` | Return PHP's `ReflectionMethod::IS_*` visibility/static/finality/abstract bitmask | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | @@ -1295,7 +1296,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `isAbstract()`. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 94bf73f09f..2b73ce8232 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -200,7 +200,8 @@ entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and eval/runtime class or interface names. `ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, -`isFinal()`, and `isAbstract()` report eval method metadata. +`isFinal()`, `isAbstract()`, and `getModifiers()` report eval method metadata, +with PHP-compatible `ReflectionMethod::IS_*` constants for the bitmask. `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report eval-declared method parameter metadata. Eval currently exposes parameter names and zero-based positions there, diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index daa2b9537e..6d43516e67 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -378,11 +378,13 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction("str x8, [sp, #136]"); // save the boxed ReflectionClass parent value emitter.instruction("ldr x8, [sp, #184]"); // load ReflectionClass modifier flags from the fourth stack argument emitter.instruction("str x8, [sp, #48]"); // save ReflectionClass modifier flags - emitter.instruction("ldr x8, [sp, #192]"); // load ReflectionClass getModifiers bitmask from the fifth stack argument - emitter.instruction("str x8, [sp, #96]"); // save ReflectionClass getModifiers bitmask - emitter.instruction("ldr x8, [sp, #200]"); // load boxed ReflectionClassConstant value from the sixth stack argument + emitter.instruction("ldr x8, [sp, #192]"); // load owner modifier/count metadata from the fifth stack argument + emitter.instruction("str x8, [sp, #96]"); // save owner modifier/count metadata + emitter.instruction("ldr x8, [sp, #200]"); // load ReflectionMethod getModifiers bitmask from the sixth stack argument + emitter.instruction("str x8, [sp, #72]"); // save ReflectionMethod getModifiers bitmask + emitter.instruction("ldr x8, [sp, #208]"); // load boxed ReflectionClassConstant value from the seventh stack argument emitter.instruction("str x8, [sp, #56]"); // save boxed ReflectionClassConstant value - emitter.instruction("ldr x8, [sp, #208]"); // load boxed ReflectionEnumBackedCase backing value from the seventh stack argument + emitter.instruction("ldr x8, [sp, #216]"); // load boxed ReflectionEnumBackedCase backing value from the eighth stack argument emitter.instruction("str x8, [sp, #64]"); // save boxed ReflectionEnumBackedCase backing value emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner @@ -535,11 +537,13 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction("mov QWORD PTR [rbp - 144], rax"); // save the boxed ReflectionClass parent value emitter.instruction("mov rax, QWORD PTR [rbp + 56]"); // load ReflectionClass modifier flags from the sixth stack argument emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags - emitter.instruction("mov rax, QWORD PTR [rbp + 64]"); // load ReflectionClass getModifiers bitmask from the seventh stack argument - emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save ReflectionClass getModifiers bitmask - emitter.instruction("mov rax, QWORD PTR [rbp + 72]"); // load boxed ReflectionClassConstant value from the eighth stack argument + emitter.instruction("mov rax, QWORD PTR [rbp + 64]"); // load owner modifier/count metadata from the seventh stack argument + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save owner modifier/count metadata + emitter.instruction("mov rax, QWORD PTR [rbp + 72]"); // load ReflectionMethod getModifiers bitmask from the eighth stack argument + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save ReflectionMethod getModifiers bitmask + emitter.instruction("mov rax, QWORD PTR [rbp + 80]"); // load boxed ReflectionClassConstant value from the ninth stack argument emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save boxed ReflectionClassConstant value - emitter.instruction("mov rax, QWORD PTR [rbp + 80]"); // load boxed ReflectionEnumBackedCase backing value from the ninth stack argument + emitter.instruction("mov rax, QWORD PTR [rbp + 88]"); // load boxed ReflectionEnumBackedCase backing value from the tenth stack argument emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save boxed ReflectionEnumBackedCase backing value emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner @@ -1138,7 +1142,11 @@ fn emit_set_owner_member_flags_property_aarch64( abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); } if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { - emitter.instruction("ldr x10, [sp, #96]"); // reload PHP Reflection member getModifiers() bitmask + if layout.required_parameter_count_lo.is_some() { + emitter.instruction("ldr x10, [sp, #72]"); // reload PHP ReflectionMethod::getModifiers() bitmask + } else { + emitter.instruction("ldr x10, [sp, #96]"); // reload PHP Reflection member getModifiers() bitmask + } abi::emit_store_to_address(emitter, "x10", "x9", modifiers_lo); abi::emit_store_zero_to_address(emitter, "x9", modifiers_hi); } @@ -1239,7 +1247,11 @@ fn emit_set_owner_member_flags_property_x86_64( abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); } if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { - emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP Reflection member getModifiers() bitmask + if layout.required_parameter_count_lo.is_some() { + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload PHP ReflectionMethod::getModifiers() bitmask + } else { + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP Reflection member getModifiers() bitmask + } abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); } diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 0127cf2093..cbf4f1c117 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -376,6 +376,9 @@ fn emit_reflection_owner_object( )?; emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; } + if class_name == "ReflectionMethod" { + emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + } if class_name == "ReflectionParameter" { if let Some(parameter) = metadata.parameter_members.first() { emit_reflection_parameter_properties(ctx, parameter)?; @@ -733,7 +736,7 @@ fn reflection_method_owner_metadata( is_enum: false, is_readonly: false, is_instantiable: false, - modifiers: 0, + modifiers: reflection_method_modifiers_from_flags(member.flags), member_flags: member.flags, } } @@ -3327,6 +3330,12 @@ fn emit_reflection_member_object( "__required_parameter_count", member.required_parameter_count, )?; + emit_reflection_owner_int_property( + ctx, + member_class_name, + "__modifiers", + reflection_method_modifiers_from_flags(member.flags), + )?; } if member_class_name == "ReflectionClassConstant" { if let Some(value) = &member.constant_value { @@ -4080,6 +4089,30 @@ fn reflection_class_constant_modifiers_from_flags(flags: ReflectionMemberFlags) reflection_class_constant_modifiers(&visibility, flags.is_final) } +/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from method flags. +fn reflection_method_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 { + let mut modifiers = 0; + if flags.is_public { + modifiers |= 1; + } + if flags.is_protected { + modifiers |= 2; + } + if flags.is_private { + modifiers |= 4; + } + if flags.is_static { + modifiers |= 16; + } + if flags.is_final { + modifiers |= 32; + } + if flags.is_abstract { + modifiers |= 64; + } + modifiers +} + /// Returns one declared property offset from a synthetic Reflection class layout. fn reflection_property_offset(info: &crate::types::ClassInfo, property: &str) -> Result { info.property_offsets.get(property).copied().ok_or_else(|| { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index ca7cb6b851..86c4e29e11 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1866,6 +1866,16 @@ fn builtin_reflection_owner_class( /// Returns public class constants exposed by a synthetic reflection owner. fn reflection_owner_constants(class_name: &str) -> Vec { + if class_name == "ReflectionMethod" { + return vec![ + builtin_class_const("IS_PUBLIC", 1), + builtin_class_const("IS_PROTECTED", 2), + builtin_class_const("IS_PRIVATE", 4), + builtin_class_const("IS_STATIC", 16), + builtin_class_const("IS_FINAL", 32), + builtin_class_const("IS_ABSTRACT", 64), + ]; + } if class_name == "ReflectionClassConstant" { return vec![ builtin_class_const("IS_PUBLIC", 1), @@ -1950,6 +1960,18 @@ fn add_reflection_member_flag_methods( "__is_static", )); } + if class_name == "ReflectionMethod" { + properties.push(builtin_property( + "__modifiers", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + )); + methods.push(builtin_reflection_class_int_method( + "getModifiers", + "__modifiers", + )); + } if class_name == "ReflectionProperty" { for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { properties.push(builtin_property( @@ -2268,6 +2290,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Bool; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { + sig.return_type = PhpType::Int; + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object( "ReflectionParameter".to_string(), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 88fc8b3e11..adbf02d1d5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6448,13 +6448,18 @@ $ref = new ReflectionClass("EvalReflectListTarget"); $methods = $ref->getMethods(); $properties = $ref->getProperties(); echo count($methods) . ":" . count($properties) . ":"; +echo ReflectionMethod::IS_STATIC . ":" . ReflectionMethod::IS_PRIVATE . ":"; +$direct = new ReflectionMethod("EvalReflectListTarget", "helper"); +echo "D" . $direct->getModifiers() . ":"; foreach ($methods as $method) { if ($method->getName() === "first") { echo "F" . count($method->getAttributes()); + echo "M" . $method->getModifiers(); } if ($method->getName() === "helper") { echo $method->isStatic() ? "S" : "s"; echo $method->isPrivate() ? "R" : "r"; + echo "M" . $method->getModifiers(); } } echo ":"; @@ -6475,7 +6480,7 @@ foreach ($properties as $property) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); + assert_eq!(out.stdout, "2:2:16:4:D20:F1M1SRM20:V1PTR"); } /// Verifies eval ReflectionClass getMethod/getProperty return single member objects. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index e1d993f452..1cda950c28 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1861,13 +1861,18 @@ $ref = new ReflectionClass(ReflectListTarget::class); $methods = $ref->getMethods(); $properties = $ref->getProperties(); echo count($methods) . ":" . count($properties) . ":"; +echo ReflectionMethod::IS_STATIC . ":" . ReflectionMethod::IS_PRIVATE . ":"; +$direct = new ReflectionMethod(ReflectListTarget::class, "helper"); +echo "D" . $direct->getModifiers() . ":"; foreach ($methods as $method) { if ($method->getName() === "first") { echo "F" . count($method->getAttributes()); + echo "M" . $method->getModifiers(); } if ($method->getName() === "helper") { echo $method->isStatic() ? "S" : "s"; echo $method->isPrivate() ? "R" : "r"; + echo "M" . $method->getModifiers(); } } echo ":"; @@ -1888,7 +1893,7 @@ foreach ($properties as $property) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:2:F1SR:V1PTR"); + assert_eq!(out.stdout, "2:2:16:4:D20:F1M1SRM20:V1PTR"); } /// Verifies that `ReflectionClass::getMethod()` and `getProperty()` return From 648a3e223a24b7cc5f710a01a6b8e7725a066084 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 16:30:16 +0200 Subject: [PATCH 0426/1208] Expose ReflectionProperty modifiers --- .../elephc-eval/src/interpreter/reflection.rs | 102 ++++++++++- .../elephc-eval/src/interpreter/statements.rs | 14 ++ .../tests/builtins_class_metadata.rs | 31 +++- .../interpreter/tests/support/object_ops.rs | 1 + docs/php/classes.md | 3 +- docs/php/eval.md | 4 +- src/codegen/lower_inst/objects/reflection.rs | 159 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 34 ++++ tests/codegen/eval.rs | 18 +- tests/codegen/oop/attributes.rs | 52 +++++- 10 files changed, 391 insertions(+), 27 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index e998201782..c7c147e893 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -57,6 +57,7 @@ struct EvalReflectionMemberMetadata { is_final: bool, is_abstract: bool, is_readonly: bool, + modifiers: u64, required_parameter_count: usize, parameters: Vec, } @@ -637,7 +638,7 @@ fn eval_reflection_property_new( property.declaring_class_name.as_deref(), &[], flags, - 0, + property.modifiers, 0, None, None, @@ -1291,13 +1292,13 @@ fn eval_reflection_member_object_array_result( member.is_abstract, member.is_readonly, ); + let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + member.required_parameter_count as u64 + } else { + member.modifiers + }; let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - eval_reflection_method_modifiers( - member.visibility, - member.is_static, - member.is_final, - member.is_abstract, - ) + member.modifiers } else { 0 }; @@ -1312,7 +1313,7 @@ fn eval_reflection_member_object_array_result( member.declaring_class_name.as_deref(), &member.parameters, flags, - member.required_parameter_count as u64, + owner_modifiers, method_modifiers, None, None, @@ -1508,6 +1509,49 @@ fn eval_reflection_method_modifiers( modifiers } +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask for eval metadata. +fn eval_reflection_property_modifiers( + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_virtual: bool, +) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly { + modifiers |= 128; + } + if is_virtual { + modifiers |= 512; + } + if is_readonly && visibility == EvalVisibility::Public { + modifiers |= 2048; + } + modifiers +} + +/// Returns whether an eval property is virtual because it has or requires hooks. +fn eval_reflection_property_is_virtual(property: &EvalClassProperty) -> bool { + property.has_get_hook() + || property.has_set_hook() + || property.requires_get_hook() + || property.requires_set_hook() +} + /// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from eval member flags. fn eval_reflection_method_modifiers_from_flags(flags: u64) -> u64 { let mut modifiers = 0; @@ -1685,6 +1729,12 @@ fn eval_reflection_method_metadata( is_final: method.is_final(), is_abstract: method.is_abstract(), is_readonly: false, + modifiers: eval_reflection_method_modifiers( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + ), required_parameter_count, parameters, } @@ -1733,6 +1783,12 @@ fn eval_reflection_method_metadata( is_final: false, is_abstract: true, is_readonly: false, + modifiers: eval_reflection_method_modifiers( + EvalVisibility::Public, + method.is_static(), + false, + true, + ), required_parameter_count, parameters, } @@ -1781,6 +1837,12 @@ fn eval_reflection_method_metadata( is_final: method.is_final(), is_abstract: method.is_abstract(), is_readonly: false, + modifiers: eval_reflection_method_modifiers( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + ), required_parameter_count, parameters, } @@ -1804,6 +1866,14 @@ fn eval_reflection_property_metadata( is_final: property.is_final(), is_abstract: property.is_abstract(), is_readonly: property.is_readonly(), + modifiers: eval_reflection_property_modifiers( + property.visibility(), + property.is_static(), + property.is_final(), + property.is_abstract(), + property.is_readonly(), + eval_reflection_property_is_virtual(&property), + ), required_parameter_count: 0, parameters: Vec::new(), }, @@ -1822,6 +1892,14 @@ fn eval_reflection_property_metadata( is_final: false, is_abstract: true, is_readonly: false, + modifiers: eval_reflection_property_modifiers( + EvalVisibility::Public, + false, + false, + true, + false, + true, + ), required_parameter_count: 0, parameters: Vec::new(), }); @@ -1839,6 +1917,14 @@ fn eval_reflection_property_metadata( is_final: property.is_final(), is_abstract: property.is_abstract(), is_readonly: property.is_readonly(), + modifiers: eval_reflection_property_modifiers( + property.visibility(), + property.is_static(), + property.is_final(), + property.is_abstract(), + property.is_readonly(), + eval_reflection_property_is_virtual(property), + ), required_parameter_count: 0, parameters: Vec::new(), }) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index a606c8be98..65dc7d1d05 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1878,6 +1878,20 @@ fn eval_builtin_reflection_class_constant( "IS_ABSTRACT" => Some(64), _ => None, } + } else if class_name.eq_ignore_ascii_case("ReflectionProperty") { + match constant_name { + "IS_STATIC" => Some(16), + "IS_READONLY" => Some(128), + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_ABSTRACT" => Some(64), + "IS_PROTECTED_SET" => Some(2048), + "IS_PRIVATE_SET" => Some(4096), + "IS_VIRTUAL" => Some(512), + "IS_FINAL" => Some(32), + _ => None, + } } else if class_name.eq_ignore_ascii_case("ReflectionClassConstant") { match constant_name { "IS_PUBLIC" => Some(1), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 5e05405799..c8e0038e54 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -422,6 +422,16 @@ echo ReflectionClass::IS_READONLY; echo ":"; echo ReflectionMethod::IS_STATIC; echo ":"; echo ReflectionMethod::IS_PRIVATE; echo ":"; echo ReflectionMethod::IS_ABSTRACT; echo ":"; +echo ReflectionProperty::IS_STATIC; echo ":"; +echo ReflectionProperty::IS_READONLY; echo ":"; +echo ReflectionProperty::IS_PUBLIC; echo ":"; +echo ReflectionProperty::IS_PROTECTED; echo ":"; +echo ReflectionProperty::IS_PRIVATE; echo ":"; +echo ReflectionProperty::IS_ABSTRACT; echo ":"; +echo ReflectionProperty::IS_PROTECTED_SET; echo ":"; +echo ReflectionProperty::IS_PRIVATE_SET; echo ":"; +echo ReflectionProperty::IS_VIRTUAL; echo ":"; +echo ReflectionProperty::IS_FINAL; echo ":"; echo ReflectionClassConstant::IS_PUBLIC; echo ":"; echo ReflectionClassConstant::IS_PROTECTED; echo ":"; echo ReflectionClassConstant::IS_PRIVATE; echo ":"; @@ -434,7 +444,10 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "32:64:65536:16:4:64:1:2:4:32"); + assert_eq!( + values.output, + "32:64:65536:16:4:64:16:128:1:2:4:64:2048:4096:512:32:1:2:4:32" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -744,6 +757,7 @@ echo $staticProp->isProtected() ? "P" : "p"; echo $staticProp->isFinal() ? "F" : "f"; echo $staticProp->isAbstract() ? "A" : "a"; echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->getModifiers(); echo ":"; $visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); echo $visibleProp->isStatic() ? "S" : "s"; @@ -752,25 +766,31 @@ echo $visibleProp->isPublic() ? "U" : "u"; echo $visibleProp->isFinal() ? "F" : "f"; echo $visibleProp->isAbstract() ? "A" : "a"; echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->getModifiers(); echo ":"; $readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); echo $readonlyProp->isReadOnly() ? "R" : "r"; echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->getModifiers(); echo ":"; $sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); echo $sealedProp->isFinal() ? "F" : "f"; echo $sealedProp->isPublic() ? "U" : "u"; +echo $sealedProp->getModifiers(); echo ":"; $staticFinalProp = new ReflectionProperty("EvalReflectMemberChild", "staticSeal"); echo $staticFinalProp->isFinal() ? "F" : "f"; echo $staticFinalProp->isStatic() ? "S" : "s"; +echo $staticFinalProp->getModifiers(); echo ":"; $abstractProp = new ReflectionProperty("EvalReflectAbstractProperty", "mustRead"); echo $abstractProp->isAbstract() ? "A" : "a"; echo $abstractProp->isFinal() ? "F" : "f"; +echo $abstractProp->getModifiers(); echo ":"; $classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->getModifiers(); return true;"#, ) .expect("parse eval fragment"); @@ -779,7 +799,10 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "SPurfa:APs:FUs:SRpfar:sPufar:RU:FU:FS:Af:C"); + assert_eq!( + values.output, + "SPurfa:APs:FUs:SRpfar20:sPufar2:RU2177:FU33:FS49:Af577:C2177" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -993,10 +1016,12 @@ foreach ($properties as $property) { if ($property->getName() === "visible") { echo "V"; echo count($property->getAttributes()); echo $property->isProtected() ? "P" : "p"; + echo "M"; echo $property->getModifiers(); } if ($property->getName() === "token") { echo $property->isStatic() ? "T" : "t"; echo $property->isPrivate() ? "R" : "r"; + echo "M"; echo $property->getModifiers(); } } return true;"#, @@ -1007,7 +1032,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:2:16:4:D20:F1M1SRM20:V1PTR"); + assert_eq!(values.output, "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 64089a93dc..d283183cd5 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -545,6 +545,7 @@ impl FakeOps { properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_readonly".to_string(), is_readonly)); + properties.push(("__modifiers".to_string(), modifiers_cell)); } } if matches!( diff --git a/docs/php/classes.md b/docs/php/classes.md index 6d5d2f5039..fba8520197 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1216,6 +1216,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isFinal()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is final | | `ReflectionProperty::isAbstract()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is abstract | | `ReflectionProperty::isReadOnly()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is readonly | +| `ReflectionProperty::getModifiers()` | `new ReflectionProperty($class_name, $property_name)` | Return PHP's `ReflectionProperty::IS_*` visibility/static/finality/abstract/readonly/virtual/set-visibility bitmask | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | @@ -1296,7 +1297,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `isReadOnly()`. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 2b73ce8232..64a0b43e12 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -236,7 +236,9 @@ parameters after method execution, write back mutated `&...$items` elements when the variadic container itself is not rebound, and are reported through `ReflectionParameter::isPassedByReference()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, -`isFinal()`, `isAbstract()`, and `isReadOnly()` report eval property metadata. +`isFinal()`, `isAbstract()`, `isReadOnly()`, and `getModifiers()` report eval +property metadata with PHP-compatible `ReflectionProperty::IS_*` constants for +the bitmask. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index cbf4f1c117..49dacbb3f7 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -21,7 +21,9 @@ use crate::codegen::{ abi, emit_box_current_value_as_mixed, runtime_value_tag, CodegenIrError, Result, }; use crate::ir::{Immediate, Instruction, Op, TraitMethodInfo, ValueDef, ValueId}; -use crate::names::{enum_case_symbol, php_symbol_key}; +use crate::names::{ + enum_case_symbol, php_symbol_key, property_hook_get_method, property_hook_set_method, +}; use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, TypeExpr, Visibility}; use crate::types::{AttrArgEntry, EnumCaseInfo, EnumCaseValue, FunctionSig, InterfaceInfo, PhpType}; @@ -79,6 +81,7 @@ struct ReflectionListedMember { constant_value: Option, is_enum_case: bool, flags: ReflectionMemberFlags, + modifiers: i64, required_parameter_count: i64, parameters: Vec, } @@ -379,6 +382,9 @@ fn emit_reflection_owner_object( if class_name == "ReflectionMethod" { emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; } + if class_name == "ReflectionProperty" { + emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + } if class_name == "ReflectionParameter" { if let Some(parameter) = metadata.parameter_members.first() { emit_reflection_parameter_properties(ctx, parameter)?; @@ -555,7 +561,7 @@ fn reflection_class_metadata_for_name( is_enum: false, is_readonly: false, is_instantiable: false, - modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + modifiers: 0, member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), }); } @@ -607,7 +613,7 @@ fn reflection_class_metadata_for_name( is_enum: false, is_readonly: false, is_instantiable: false, - modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + modifiers: 0, member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), }); } @@ -787,7 +793,7 @@ fn reflection_property_metadata( is_enum: false, is_readonly: false, is_instantiable: false, - modifiers: 0, + modifiers: reflection_property_modifiers_for_info(info, &property_name)?, member_flags: reflection_property_member_flags(info, &property_name)?, }) }) @@ -1651,6 +1657,7 @@ fn push_unique_constant_reflection_member( constant_value: Some(value), is_enum_case, flags: reflection_member_flags(false, &visibility, is_final, false, false), + modifiers: reflection_class_constant_modifiers(&visibility, is_final), required_parameter_count: 0, parameters: Vec::new(), }); @@ -1845,6 +1852,7 @@ fn reflection_class_method_member( constant_value: None, is_enum_case: false, flags, + modifiers: reflection_method_modifiers_from_flags(flags), required_parameter_count, parameters, }) @@ -1905,6 +1913,7 @@ fn reflection_interface_method_member( constant_value: None, is_enum_case: false, flags, + modifiers: reflection_method_modifiers_from_flags(flags), required_parameter_count, parameters, }) @@ -1954,6 +1963,7 @@ fn reflection_trait_method_member( constant_value: None, is_enum_case: false, flags, + modifiers: reflection_method_modifiers_from_flags(flags), required_parameter_count, parameters: reflection_parameter_members_with_declaring_class( &info.signature, @@ -2010,6 +2020,8 @@ fn reflection_class_property_member( constant_value: None, is_enum_case: false, flags, + modifiers: reflection_property_modifiers_for_info(info, property_name) + .unwrap_or_else(|| reflection_property_modifiers_from_flags(flags)), required_parameter_count: 0, parameters: Vec::new(), }) @@ -2031,6 +2043,13 @@ fn default_method_members( constant_value: None, is_enum_case: false, flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), + modifiers: reflection_method_modifiers_from_flags(reflection_member_flags( + false, + &Visibility::Public, + false, + is_interface, + false, + )), required_parameter_count: 0, parameters: Vec::new(), }) @@ -2053,6 +2072,15 @@ fn default_property_members( constant_value: None, is_enum_case: false, flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), + modifiers: reflection_property_modifiers( + &Visibility::Public, + false, + false, + is_interface, + false, + is_interface, + None, + ), required_parameter_count: 0, parameters: Vec::new(), }) @@ -3334,7 +3362,15 @@ fn emit_reflection_member_object( ctx, member_class_name, "__modifiers", - reflection_method_modifiers_from_flags(member.flags), + member.modifiers, + )?; + } + if member_class_name == "ReflectionProperty" { + emit_reflection_owner_int_property( + ctx, + member_class_name, + "__modifiers", + member.modifiers, )?; } if member_class_name == "ReflectionClassConstant" { @@ -3353,7 +3389,7 @@ fn emit_reflection_member_object( ctx, member_class_name, "__modifiers", - reflection_class_constant_modifiers_from_flags(member.flags), + member.modifiers, )?; } emit_reflection_member_flag_properties(ctx, member_class_name, member.flags)?; @@ -4077,8 +4113,105 @@ fn reflection_class_constant_modifiers(visibility: &Visibility, is_final: bool) modifiers } -/// Computes the class-constant modifier bitmask from populated Reflection member flags. -fn reflection_class_constant_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 { +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask from class metadata. +fn reflection_property_modifiers_for_info( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + if info + .properties + .iter() + .any(|(name, _)| name == property_name) + { + let visibility = info + .property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + return Some(reflection_property_modifiers( + visibility, + false, + info.final_properties.contains(property_name), + info.abstract_properties.contains(property_name), + info.readonly_properties.contains(property_name), + reflection_property_is_virtual(info, property_name), + info.property_set_visibilities.get(property_name), + )); + } + if info + .static_properties + .iter() + .any(|(name, _)| name == property_name) + { + let visibility = info + .static_property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + return Some(reflection_property_modifiers( + visibility, + true, + info.final_static_properties.contains(property_name), + false, + false, + false, + None, + )); + } + None +} + +/// Returns whether a property is virtual because it has or requires hooks. +fn reflection_property_is_virtual(info: &crate::types::ClassInfo, property_name: &str) -> bool { + let get_method = php_symbol_key(&property_hook_get_method(property_name)); + let set_method = php_symbol_key(&property_hook_set_method(property_name)); + info.abstract_property_hooks.contains_key(property_name) + || info.methods.contains_key(&get_method) + || info.methods.contains_key(&set_method) +} + +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask. +fn reflection_property_modifiers( + visibility: &Visibility, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_virtual: bool, + set_visibility: Option<&Visibility>, +) -> i64 { + let mut modifiers = match visibility { + Visibility::Public => 1, + Visibility::Protected => 2, + Visibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly { + modifiers |= 128; + } + if is_virtual { + modifiers |= 512; + } + match set_visibility { + Some(Visibility::Private) => modifiers |= 32 | 4096, + Some(Visibility::Protected) => modifiers |= 2048, + Some(Visibility::Public) | None => { + if is_readonly && visibility == &Visibility::Public { + modifiers |= 2048; + } + } + } + modifiers +} + +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask from predicate flags. +fn reflection_property_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 { let visibility = if flags.is_private { Visibility::Private } else if flags.is_protected { @@ -4086,7 +4219,15 @@ fn reflection_class_constant_modifiers_from_flags(flags: ReflectionMemberFlags) } else { Visibility::Public }; - reflection_class_constant_modifiers(&visibility, flags.is_final) + reflection_property_modifiers( + &visibility, + flags.is_static, + flags.is_final, + flags.is_abstract, + flags.is_readonly, + false, + None, + ) } /// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from method flags. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 86c4e29e11..3e4bb762b0 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1876,6 +1876,20 @@ fn reflection_owner_constants(class_name: &str) -> Vec { builtin_class_const("IS_ABSTRACT", 64), ]; } + if class_name == "ReflectionProperty" { + return vec![ + builtin_class_const("IS_STATIC", 16), + builtin_class_const("IS_READONLY", 128), + builtin_class_const("IS_PUBLIC", 1), + builtin_class_const("IS_PROTECTED", 2), + builtin_class_const("IS_PRIVATE", 4), + builtin_class_const("IS_ABSTRACT", 64), + builtin_class_const("IS_PROTECTED_SET", 2048), + builtin_class_const("IS_PRIVATE_SET", 4096), + builtin_class_const("IS_VIRTUAL", 512), + builtin_class_const("IS_FINAL", 32), + ]; + } if class_name == "ReflectionClassConstant" { return vec![ builtin_class_const("IS_PUBLIC", 1), @@ -1973,6 +1987,16 @@ fn add_reflection_member_flag_methods( )); } if class_name == "ReflectionProperty" { + properties.push(builtin_property( + "__modifiers", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + )); + methods.push(builtin_reflection_class_int_method( + "getModifiers", + "__modifiers", + )); for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { properties.push(builtin_property( property, @@ -2284,6 +2308,16 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } } + if class_name == "ReflectionProperty" { + for method_name in ["isfinal", "isabstract", "isreadonly"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { + sig.return_type = PhpType::Int; + } + } if class_name == "ReflectionMethod" { for method_name in ["isfinal", "isabstract"] { if let Some(sig) = class_info.methods.get_mut(method_name) { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index adbf02d1d5..f5a21e7c4e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6333,6 +6333,7 @@ echo $staticProp->isProtected() ? "P" : "p"; echo $staticProp->isFinal() ? "F" : "f"; echo $staticProp->isAbstract() ? "A" : "a"; echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->getModifiers(); echo ":"; $visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); echo $visibleProp->isStatic() ? "S" : "s"; @@ -6341,25 +6342,31 @@ echo $visibleProp->isPublic() ? "U" : "u"; echo $visibleProp->isFinal() ? "F" : "f"; echo $visibleProp->isAbstract() ? "A" : "a"; echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->getModifiers(); echo ":"; $readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); echo $readonlyProp->isReadOnly() ? "R" : "r"; echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->getModifiers(); echo ":"; $sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); echo $sealedProp->isFinal() ? "F" : "f"; echo $sealedProp->isPublic() ? "U" : "u"; +echo $sealedProp->getModifiers(); echo ":"; $staticFinalProp = new ReflectionProperty("EvalReflectMemberChild", "staticSeal"); echo $staticFinalProp->isFinal() ? "F" : "f"; echo $staticFinalProp->isStatic() ? "S" : "s"; +echo $staticFinalProp->getModifiers(); echo ":"; $abstractProp = new ReflectionProperty("EvalReflectAbstractProperty", "mustRead"); echo $abstractProp->isAbstract() ? "A" : "a"; echo $abstractProp->isFinal() ? "F" : "f"; +echo $abstractProp->getModifiers(); echo ":"; $classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); -echo $classReadonlyProp->isReadOnly() ? "C" : "c";'); +echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->getModifiers();'); "#, ); assert!( @@ -6367,7 +6374,10 @@ echo $classReadonlyProp->isReadOnly() ? "C" : "c";'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "SPurfa:APs:FUs:SRpfar:sPufar:RU:FU:FS:Af:C"); + assert_eq!( + out.stdout, + "SPurfa:APs:FUs:SRpfar20:sPufar2:RU2177:FU33:FS49:Af577:C2177" + ); } /// Verifies eval-declared final properties cannot be redeclared by subclasses. @@ -6467,10 +6477,12 @@ foreach ($properties as $property) { if ($property->getName() === "visible") { echo "V" . count($property->getAttributes()); echo $property->isProtected() ? "P" : "p"; + echo "M" . $property->getModifiers(); } if ($property->getName() === "token") { echo $property->isStatic() ? "T" : "t"; echo $property->isPrivate() ? "R" : "r"; + echo "M" . $property->getModifiers(); } }'); "#, @@ -6480,7 +6492,7 @@ foreach ($properties as $property) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:2:16:4:D20:F1M1SRM20:V1PTR"); + assert_eq!(out.stdout, "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20"); } /// Verifies eval ReflectionClass getMethod/getProperty return single member objects. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 1cda950c28..2ca87b5883 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1184,6 +1184,42 @@ echo ReflectionClass::IS_READONLY; assert_eq!(out, "64:32:65536:65568:32:0:0:16:32:64:65536"); } +/// Verifies that `ReflectionProperty::IS_*` constants use PHP modifier values. +#[test] +fn test_reflection_property_modifier_constants_report_php_values() { + let out = compile_and_run( + r#"getModifiers() . ":"; +echo (new ReflectionProperty(ReflectSetVisibility::class, "protectedSet"))->getModifiers(); +"#, + ); + assert_eq!(out, "4129:2049"); +} + /// Verifies that `ReflectionClass::isReadOnly()` reports readonly class metadata. #[test] fn test_reflection_class_is_readonly_reports_class_metadata() { @@ -1774,6 +1810,7 @@ echo $staticProp->isProtected() ? "P" : "p"; echo $staticProp->isFinal() ? "F" : "f"; echo $staticProp->isAbstract() ? "A" : "a"; echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->getModifiers(); echo ":"; $visibleProp = new ReflectionProperty(ReflectMemberChild::class, "visible"); echo $visibleProp->isStatic() ? "S" : "s"; @@ -1782,28 +1819,37 @@ echo $visibleProp->isPublic() ? "U" : "u"; echo $visibleProp->isFinal() ? "F" : "f"; echo $visibleProp->isAbstract() ? "A" : "a"; echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->getModifiers(); echo ":"; $readonlyProp = new ReflectionProperty(ReflectMemberChild::class, "locked"); echo $readonlyProp->isReadOnly() ? "R" : "r"; echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->getModifiers(); echo ":"; $sealedProp = new ReflectionProperty(ReflectMemberChild::class, "sealed"); echo $sealedProp->isFinal() ? "F" : "f"; echo $sealedProp->isPublic() ? "U" : "u"; +echo $sealedProp->getModifiers(); echo ":"; $staticFinalProp = new ReflectionProperty(ReflectMemberChild::class, "staticSeal"); echo $staticFinalProp->isFinal() ? "F" : "f"; echo $staticFinalProp->isStatic() ? "S" : "s"; +echo $staticFinalProp->getModifiers(); echo ":"; $abstractProp = new ReflectionProperty(ReflectAbstractProperty::class, "mustRead"); echo $abstractProp->isAbstract() ? "A" : "a"; echo $abstractProp->isFinal() ? "F" : "f"; +echo $abstractProp->getModifiers(); echo ":"; $classReadonlyProp = new ReflectionProperty(ReflectReadonlyClass::class, "classReadonly"); echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->getModifiers(); "#, ); - assert_eq!(out, "SPurfa:APs:FUs:SRpfar:sPufar:RU:FU:FS:Af:C"); + assert_eq!( + out, + "SPurfa:APs:FUs:SRpfar20:sPufar2:RU2177:FU33:FS49:Af577:C2177" + ); } /// Verifies member and enum-case reflectors expose their declaring class object. @@ -1880,10 +1926,12 @@ foreach ($properties as $property) { if ($property->getName() === "visible") { echo "V" . count($property->getAttributes()); echo $property->isProtected() ? "P" : "p"; + echo "M" . $property->getModifiers(); } if ($property->getName() === "token") { echo $property->isStatic() ? "T" : "t"; echo $property->isPrivate() ? "R" : "r"; + echo "M" . $property->getModifiers(); } } "#, @@ -1893,7 +1941,7 @@ foreach ($properties as $property) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:2:16:4:D20:F1M1SRM20:V1PTR"); + assert_eq!(out.stdout, "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20"); } /// Verifies that `ReflectionClass::getMethod()` and `getProperty()` return From 538e43ecb5aa9c0fd4ceec1d02ced5e01ffa6728 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 16:43:44 +0200 Subject: [PATCH 0427/1208] Expose eval ReflectionClass constructors --- .../elephc-eval/src/interpreter/reflection.rs | 205 ++++++++---------- .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 48 ++++ .../interpreter/tests/support/object_ops.rs | 6 + .../interpreter/tests/support/runtime_ops.rs | 2 + .../elephc-eval/src/runtime_hooks/externs.rs | 1 + crates/elephc-eval/src/runtime_hooks/ops.rs | 2 + docs/php/eval.md | 12 +- src/codegen/eval_reflection_owner_helpers.rs | 46 ++++ tests/codegen/eval.rs | 45 ++++ 10 files changed, 247 insertions(+), 121 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index c7c147e893..8a38879d60 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -380,42 +380,8 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( let member = eval_reflection_member_metadata(owner_kind, &reflected_name, &member_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - let flags = eval_reflection_member_flags( - member.visibility, - member.is_static, - member.is_final, - member.is_abstract, - member.is_readonly, - ); - let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - eval_reflection_method_modifiers( - member.visibility, - member.is_static, - member.is_final, - member.is_abstract, - ) - } else { - 0 - }; - eval_reflection_owner_object( - owner_kind, - &member_name, - &member.attributes, - &[], - &[], - &[], - &[], - member.declaring_class_name.as_deref(), - &member.parameters, - flags, - member.required_parameter_count as u64, - method_modifiers, - None, - None, - context, - values, - ) - .map(Some) + eval_reflection_member_object_result(owner_kind, &member_name, &member, context, values) + .map(Some) } /// Returns the constant names visible through eval-backed `ReflectionClass`. @@ -570,33 +536,10 @@ fn eval_reflection_method_new( let method_name = eval_reflection_string_arg(args[1], values)?; let method = eval_reflection_method_metadata(&class_name, &method_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - let flags = eval_reflection_member_flags( - method.visibility, - method.is_static, - method.is_final, - method.is_abstract, - false, - ); - eval_reflection_owner_object( + eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_METHOD, &method_name, - &method.attributes, - &[], - &[], - &[], - &[], - method.declaring_class_name.as_deref(), - &method.parameters, - flags, - method.required_parameter_count as u64, - eval_reflection_method_modifiers( - method.visibility, - method.is_static, - method.is_final, - method.is_abstract, - ), - None, - None, + &method, context, values, ) @@ -620,28 +563,10 @@ fn eval_reflection_property_new( let property_name = eval_reflection_string_arg(args[1], values)?; let property = eval_reflection_property_metadata(&class_name, &property_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - let flags = eval_reflection_member_flags( - property.visibility, - property.is_static, - property.is_final, - property.is_abstract, - property.is_readonly, - ); - eval_reflection_owner_object( + eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_PROPERTY, &property_name, - &property.attributes, - &[], - &[], - &[], - &[], - property.declaring_class_name.as_deref(), - &[], - flags, - property.modifiers, - 0, - None, - None, + &property, context, values, ) @@ -851,6 +776,13 @@ fn eval_reflection_owner_object_with_members( context, values, )?; + let constructor = eval_reflection_constructor_object_result( + owner_kind, + reflected_name, + include_class_members, + context, + values, + )?; let (constant_value_cell, release_constant_value) = match constant_value { Some(value) => (value, false), None => (values.null()?, true), @@ -875,6 +807,7 @@ fn eval_reflection_owner_object_with_members( method_modifiers, constant_value_cell, backing_value_cell, + constructor, )?; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { let identity = values.object_identity(object)?; @@ -888,6 +821,7 @@ fn eval_reflection_owner_object_with_members( values.release(method_objects)?; values.release(property_objects)?; values.release(parent_class)?; + values.release(constructor)?; if release_constant_value { values.release(constant_value_cell)?; } @@ -1060,6 +994,7 @@ fn eval_reflection_parameter_object_result( 0, constant_value, backing_value, + constant_value, )?; values.release(attrs)?; values.release(declaring_function)?; @@ -1151,6 +1086,7 @@ fn eval_reflection_named_type_object_result( 0, constant_value, backing_value, + constant_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1197,6 +1133,7 @@ fn eval_reflection_union_type_object_result( 0, constant_value, backing_value, + constant_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1242,6 +1179,7 @@ fn eval_reflection_intersection_type_object_result( 0, constant_value, backing_value, + constant_value, )?; values.release(attrs)?; values.release(interface_names)?; @@ -1270,6 +1208,74 @@ fn eval_reflection_named_type_object_array_result( Ok(result) } +/// Builds the `ReflectionMethod|null` value stored in ReflectionClass::__constructor. +fn eval_reflection_constructor_object_result( + owner_kind: u64, + class_name: &str, + include_class_members: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if owner_kind != EVAL_REFLECTION_OWNER_CLASS || !include_class_members { + return values.null(); + } + let Some(member) = eval_reflection_method_metadata(class_name, "__construct", context) else { + return values.null(); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + "__construct", + &member, + context, + values, + ) +} + +/// Materializes one eval-backed ReflectionMethod or ReflectionProperty object. +fn eval_reflection_member_object_result( + owner_kind: u64, + reflected_name: &str, + member: &EvalReflectionMemberMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = eval_reflection_member_flags( + member.visibility, + member.is_static, + member.is_final, + member.is_abstract, + member.is_readonly, + ); + let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + member.required_parameter_count as u64 + } else { + member.modifiers + }; + let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + member.modifiers + } else { + 0 + }; + eval_reflection_owner_object( + owner_kind, + reflected_name, + &member.attributes, + &[], + &[], + &[], + &[], + member.declaring_class_name.as_deref(), + &member.parameters, + flags, + owner_modifiers, + method_modifiers, + None, + None, + context, + values, + ) +} + /// Builds an indexed array of ReflectionMethod or ReflectionProperty objects for a ReflectionClass. fn eval_reflection_member_object_array_result( owner_kind: u64, @@ -1285,41 +1291,8 @@ fn eval_reflection_member_object_array_result( else { continue; }; - let flags = eval_reflection_member_flags( - member.visibility, - member.is_static, - member.is_final, - member.is_abstract, - member.is_readonly, - ); - let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - member.required_parameter_count as u64 - } else { - member.modifiers - }; - let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - member.modifiers - } else { - 0 - }; - let member_object = eval_reflection_owner_object( - owner_kind, - name, - &member.attributes, - &[], - &[], - &[], - &[], - member.declaring_class_name.as_deref(), - &member.parameters, - flags, - owner_modifiers, - method_modifiers, - None, - None, - context, - values, - )?; + let member_object = + eval_reflection_member_object_result(owner_kind, name, &member, context, values)?; let key = values.int(index)?; result = values.array_set(result, key, member_object)?; index += 1; diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 40432253cb..2bc773776d 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -130,6 +130,7 @@ pub trait RuntimeValueOps { method_modifiers: u64, constant_value: RuntimeCellHandle, backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, ) -> Result; /// Creates a named runtime object without constructor arguments. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c8e0038e54..e344a3205b 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -511,6 +511,54 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::getConstructor exposes eval constructor metadata. +#[test] +fn execute_program_reflection_class_get_constructor() { + let program = parse_fragment( + br#"class EvalCtorBase { + public function __construct($required, $optional = 2) {} +} +class EvalCtorChild extends EvalCtorBase {} +class EvalCtorPlain {} +interface EvalCtorInterface { + public function __construct($required); +} +trait EvalCtorTrait { + public function __construct($required, $optional = null, ...$rest) {} +} +$base = (new ReflectionClass("EvalCtorBase"))->getConstructor(); +echo $base->getName(); echo "/"; +echo $base->getNumberOfParameters(); echo "/"; +echo $base->getNumberOfRequiredParameters(); echo ":"; +$child = (new ReflectionClass("EvalCtorChild"))->getConstructor(); +echo $child->getName(); echo "/"; +echo $child->getNumberOfParameters(); echo "/"; +echo $child->getNumberOfRequiredParameters(); echo ":"; +$plain = (new ReflectionClass("EvalCtorPlain"))->getConstructor(); +echo $plain === null ? "null" : "bad"; echo ":"; +$interface = (new ReflectionClass("EvalCtorInterface"))->getConstructor(); +echo $interface->getName(); echo "/"; +echo $interface->getNumberOfParameters(); echo "/"; +echo $interface->getNumberOfRequiredParameters(); echo ":"; +$trait = (new ReflectionClass("EvalCtorTrait"))->getConstructor(); +echo $trait->getName(); echo "/"; +echo $trait->getNumberOfParameters(); echo "/"; +echo $trait->getNumberOfRequiredParameters(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/3/1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass reports eval class-like method, property, and constant membership. #[test] fn execute_program_reflects_eval_class_member_existence() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index d283183cd5..d03ac80a0a 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -160,6 +160,10 @@ impl FakeOps { Self::object_property(&properties, "__parent_class") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "getconstructor") if args.is_empty() => { + Self::object_property(&properties, "__constructor") + .map_or_else(|| self.null(), Ok) + } (FakeValue::Object(properties), "getdeclaringclass") if args.is_empty() => { Self::object_property(&properties, "__declaring_class") .map_or_else(|| self.bool_value(false), Ok) @@ -469,6 +473,7 @@ impl FakeOps { method_modifiers: u64, constant_value: RuntimeCellHandle, backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, ) -> Result { let class_name = match owner_kind { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", @@ -521,6 +526,7 @@ impl FakeOps { properties.push(("__constants".to_string(), constants)); properties.push(("__reflection_constants".to_string(), reflection_constants)); properties.push(("__methods".to_string(), method_objects)); + properties.push(("__constructor".to_string(), constructor)); properties.push(("__parent_class".to_string(), parent_class)); properties.push(("__properties".to_string(), property_objects)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index f08a638704..902ef85770 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -139,6 +139,7 @@ impl RuntimeValueOps for FakeOps { method_modifiers: u64, constant_value: RuntimeCellHandle, backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, ) -> Result { self.runtime_reflection_owner_new( owner_kind, @@ -156,6 +157,7 @@ impl RuntimeValueOps for FakeOps { method_modifiers, constant_value, backing_value, + constructor, ) } /// Creates one fake object for eval `new` unit tests. diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 13e545afc5..3553f02e52 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -91,6 +91,7 @@ unsafe extern "C" { method_modifiers: u64, constant_value: *mut RuntimeCell, backing_value: *mut RuntimeCell, + constructor: *mut RuntimeCell, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 853ea948f4..9ddd13abc3 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -216,6 +216,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { method_modifiers: u64, constant_value: RuntimeCellHandle, backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, ) -> Result { Self::handle(unsafe { __elephc_eval_reflection_owner_new( @@ -235,6 +236,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { method_modifiers, constant_value.as_ptr(), backing_value.as_ptr(), + constructor.as_ptr(), ) }) } diff --git a/docs/php/eval.md b/docs/php/eval.md index 64a0b43e12..5d242adde9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -190,11 +190,13 @@ visible member metadata, including supported member attributes and predicate flags. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the eval class-like symbol that declares the reflected -member. `ReflectionClass::getParentClass()` returns a materialized -`ReflectionClass` for eval-declared parent classes or `false` when no parent -class exists. `ReflectionClass::newInstance()` constructs eval-declared reflected -classes and forwards constructor arguments through eval's positional, named, and -unpacking-aware call binding. +member. `ReflectionClass::getConstructor()` returns a materialized +`ReflectionMethod` for direct, inherited, interface, and trait constructors, or +`null` when no constructor is visible. `ReflectionClass::getParentClass()` +returns a materialized `ReflectionClass` for eval-declared parent classes or +`false` when no parent class exists. `ReflectionClass::newInstance()` constructs +eval-declared reflected classes and forwards constructor arguments through +eval's positional, named, and unpacking-aware call binding. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 6d43516e67..6a9e179119 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -42,6 +42,8 @@ struct ReflectionOwnerLayout { method_objects_hi: Option, property_objects_lo: Option, property_objects_hi: Option, + constructor_lo: Option, + constructor_hi: Option, parent_class_lo: Option, parent_class_hi: Option, value_lo: Option, @@ -208,6 +210,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option OptiongetConstructor(); +echo $base->getName() . "/" . $base->getNumberOfParameters(); +echo "/" . $base->getNumberOfRequiredParameters() . ":"; +$child = (new ReflectionClass("EvalBridgeCtorChild"))->getConstructor(); +echo $child->getName() . "/" . $child->getNumberOfParameters(); +echo "/" . $child->getNumberOfRequiredParameters() . ":"; +$plain = (new ReflectionClass("EvalBridgeCtorPlain"))->getConstructor(); +echo $plain === null ? "null" : "bad"; +echo ":"; +$interface = (new ReflectionClass("EvalBridgeCtorInterface"))->getConstructor(); +echo $interface->getName() . "/" . $interface->getNumberOfParameters(); +echo "/" . $interface->getNumberOfRequiredParameters() . ":"; +$trait = (new ReflectionClass("EvalBridgeCtorTrait"))->getConstructor(); +echo $trait->getName() . "/" . $trait->getNumberOfParameters(); +echo "/" . $trait->getNumberOfRequiredParameters(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/3/1" + ); +} + /// Verifies eval ReflectionClass reports class-like final and abstract flags. #[test] fn test_eval_reflection_class_modifier_flags() { From 55c1ec4c2423a4cb3b0013c52483fa5a3dac9960 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 17:00:30 +0200 Subject: [PATCH 0428/1208] Expose ReflectionProperty type metadata --- crates/elephc-eval/src/eval_ir.rs | 30 +++++++++++ .../elephc-eval/src/interpreter/reflection.rs | 29 +++++++++++ .../tests/builtins_class_metadata.rs | 52 +++++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 10 +++- crates/elephc-eval/src/parser/statements.rs | 31 ++++------- docs/php/eval.md | 6 ++- src/codegen/lower_inst/objects/reflection.rs | 50 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 45 ++++++++++++++++ tests/codegen/eval.rs | 52 +++++++++++++++++++ tests/codegen/oop/attributes.rs | 52 +++++++++++++++++++ 10 files changed, 331 insertions(+), 26 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 8302c6960a..7a4c957d8b 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -534,6 +534,7 @@ impl EvalInterface { pub struct EvalInterfaceProperty { name: String, attributes: Vec, + property_type: Option, requires_get: bool, requires_set: bool, } @@ -544,11 +545,18 @@ impl EvalInterfaceProperty { Self { name: name.into(), attributes: Vec::new(), + property_type: None, requires_get, requires_set, } } + /// Returns a copy of this interface property with retained type metadata. + pub fn with_type(mut self, property_type: Option) -> Self { + self.property_type = property_type; + self + } + /// Returns a copy of this interface property with declaration attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; @@ -565,6 +573,11 @@ impl EvalInterfaceProperty { &self.attributes } + /// Returns retained PHP type metadata for this interface property contract. + pub fn property_type(&self) -> Option<&EvalParameterType> { + self.property_type.as_ref() + } + /// Returns whether the interface requires the property to be readable. pub const fn requires_get(&self) -> bool { self.requires_get @@ -580,6 +593,10 @@ impl EvalInterfaceProperty { Self { name: self.name.clone(), attributes: self.attributes.clone(), + property_type: self + .property_type + .clone() + .or_else(|| other.property_type.clone()), requires_get: self.requires_get || other.requires_get, requires_set: self.requires_set || other.requires_set, } @@ -1235,6 +1252,7 @@ impl EvalTrait { pub struct EvalClassProperty { name: String, attributes: Vec, + property_type: Option, visibility: EvalVisibility, is_static: bool, is_final: bool, @@ -1302,6 +1320,7 @@ impl EvalClassProperty { Self { name: name.into(), attributes: Vec::new(), + property_type: None, visibility, is_static, is_final, @@ -1340,6 +1359,12 @@ impl EvalClassProperty { self } + /// Returns a copy of this property with retained type metadata. + pub fn with_type(mut self, property_type: Option) -> Self { + self.property_type = property_type; + self + } + /// Returns the PHP-visible property name without `$`. pub fn name(&self) -> &str { &self.name @@ -1350,6 +1375,11 @@ impl EvalClassProperty { &self.attributes } + /// Returns retained PHP type metadata for this property. + pub fn property_type(&self) -> Option<&EvalParameterType> { + self.property_type.as_ref() + } + /// Returns the PHP visibility declared for this property. pub const fn visibility(&self) -> EvalVisibility { self.visibility diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 8a38879d60..75cf013df4 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -58,6 +58,7 @@ struct EvalReflectionMemberMetadata { is_abstract: bool, is_readonly: bool, modifiers: u64, + type_metadata: Option, required_parameter_count: usize, parameters: Vec, } @@ -438,6 +439,7 @@ fn eval_reflection_class_constant_object_result( &[], Some(&declaring_class_name), &[], + None, flags, modifiers, 0, @@ -508,6 +510,7 @@ fn eval_reflection_class_new( &metadata.property_names, metadata.parent_class_name.as_deref(), &[], + None, metadata.flags, metadata.modifiers, 0, @@ -608,6 +611,7 @@ fn eval_reflection_class_constant_new( &[], Some(&declaring_class_name), &[], + None, flags, modifiers, 0, @@ -669,6 +673,7 @@ fn eval_reflection_enum_case_new( &[], Some(&declaring_class_name), &[], + None, 0, 0, 0, @@ -691,6 +696,7 @@ fn eval_reflection_owner_object( property_names: &[String], parent_class_name: Option<&str>, parameter_metadata: &[EvalReflectionParameterMetadata], + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, flags: u64, modifiers: u64, method_modifiers: u64, @@ -709,6 +715,7 @@ fn eval_reflection_owner_object( property_names, parent_class_name, parameter_metadata, + type_metadata, flags, modifiers, method_modifiers, @@ -731,6 +738,7 @@ fn eval_reflection_owner_object_with_members( property_names: &[String], parent_class_name: Option<&str>, parameter_metadata: &[EvalReflectionParameterMetadata], + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, flags: u64, modifiers: u64, method_modifiers: u64, @@ -755,6 +763,11 @@ fn eval_reflection_owner_object_with_members( )? } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { eval_reflection_parameter_object_array_result(parameter_metadata, context, values)? + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + match type_metadata { + Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, + None => values.null()?, + } } else { values.array_new(0)? }; @@ -877,6 +890,7 @@ fn eval_reflection_full_class_object_result( &metadata.property_names, metadata.parent_class_name.as_deref(), &[], + None, metadata.flags, metadata.modifiers, 0, @@ -906,6 +920,7 @@ fn eval_reflection_shallow_class_object_result( &[], None, &[], + None, metadata.flags, metadata.modifiers, 0, @@ -1026,6 +1041,7 @@ fn eval_reflection_declaring_function_object_result( &[], metadata.declaring_class_name.as_deref(), &[], + None, metadata.flags, metadata.required_parameter_count as u64, eval_reflection_method_modifiers_from_flags(metadata.flags), @@ -1266,6 +1282,7 @@ fn eval_reflection_member_object_result( &[], member.declaring_class_name.as_deref(), &member.parameters, + member.type_metadata.as_ref(), flags, owner_modifiers, method_modifiers, @@ -1708,6 +1725,7 @@ fn eval_reflection_method_metadata( method.is_final(), method.is_abstract(), ), + type_metadata: None, required_parameter_count, parameters, } @@ -1762,6 +1780,7 @@ fn eval_reflection_method_metadata( false, true, ), + type_metadata: None, required_parameter_count, parameters, } @@ -1816,6 +1835,7 @@ fn eval_reflection_method_metadata( method.is_final(), method.is_abstract(), ), + type_metadata: None, required_parameter_count, parameters, } @@ -1847,6 +1867,9 @@ fn eval_reflection_property_metadata( property.is_readonly(), eval_reflection_property_is_virtual(&property), ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), required_parameter_count: 0, parameters: Vec::new(), }, @@ -1873,6 +1896,9 @@ fn eval_reflection_property_metadata( false, true, ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), required_parameter_count: 0, parameters: Vec::new(), }); @@ -1898,6 +1924,9 @@ fn eval_reflection_property_metadata( property.is_readonly(), eval_reflection_property_is_virtual(property), ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), required_parameter_count: 0, parameters: Vec::new(), }) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index e344a3205b..24260e1bcb 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -964,6 +964,58 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty exposes eval property type metadata. +#[test] +fn execute_program_reflection_property_get_type_metadata() { + let program = parse_fragment( +br##"class EvalReflectPropertyTypeDep {} +class EvalReflectPropertyTypeTarget { + public int $id; + public ?string $name; + public EvalReflectPropertyTypeDep $dep; + public $plain; + public int|string $union; +} +$properties = (new ReflectionClass("EvalReflectPropertyTypeTarget"))->getProperties(); +foreach ($properties as $property) { + echo $property->getName(); echo ":"; + echo $property->hasType() ? "T:" : "t:"; + $type = $property->getType(); + if ($property->getName() == "union") { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionProperty("EvalReflectPropertyTypeTarget", "dep"); +$directType = $direct->getType(); +echo "direct:"; echo $direct->hasType() ? "T:" : "t:"; +echo $directType->getName(); +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval ReflectionParameter exposes declaring class metadata. #[test] fn execute_program_reflects_eval_parameter_declaring_class() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index d03ac80a0a..2464f6ee9b 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -335,8 +335,13 @@ impl FakeOps { .map_or_else(|| self.bool_value(false), Ok) } (FakeValue::Object(properties), "hastype") if args.is_empty() => { - Self::object_property(&properties, "__has_type") - .map_or_else(|| self.bool_value(false), Ok) + if let Some(has_type) = Self::object_property(&properties, "__has_type") { + return Ok(has_type); + } + match Self::object_property(&properties, "__type") { + Some(value) => self.bool_value(!matches!(self.get(value), FakeValue::Null)), + None => self.bool_value(false), + } } (FakeValue::Object(properties), "isdefaultvalueavailable") if args.is_empty() => { Self::object_property(&properties, "__has_default_value") @@ -552,6 +557,7 @@ impl FakeOps { properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_readonly".to_string(), is_readonly)); properties.push(("__modifiers".to_string(), modifiers_cell)); + properties.push(("__type".to_string(), method_objects)); } } if matches!( diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 28f09c1b3e..0d8a8e40af 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -802,7 +802,7 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } let effective_readonly = is_readonly || (is_readonly_class && !is_static); - self.skip_optional_property_type()?; + let property_type = self.parse_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; @@ -829,6 +829,7 @@ impl Parser { effective_readonly, None, ) + .with_type(property_type) .with_abstract_hook_contract(requires_get_hook, requires_set_hook); return Ok((property, Vec::new())); } @@ -843,6 +844,7 @@ impl Parser { effective_readonly, default, ) + .with_type(property_type) .with_hooks(has_get_hook, has_set_hook); Ok((property, hook_methods)) } @@ -950,7 +952,7 @@ impl Parser { if !self.consume(TokenKind::LParen) { return Ok("value".to_string()); } - self.skip_optional_property_type()?; + let _ = self.parse_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; @@ -1323,7 +1325,7 @@ impl Parser { pub(super) fn parse_interface_property_decl( &mut self, ) -> Result { - self.skip_optional_property_type()?; + let property_type = self.parse_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; @@ -1333,7 +1335,7 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } let (requires_get, requires_set) = self.parse_interface_property_hook_contracts()?; - Ok(EvalInterfaceProperty::new(name, requires_get, requires_set)) + Ok(EvalInterfaceProperty::new(name, requires_get, requires_set).with_type(property_type)) } /// Parses `{ get; set; }` hook contracts for an abstract or interface property. @@ -1389,22 +1391,11 @@ impl Parser { self.parse_property_hook_contracts() } - /// Consumes a simple declared property type before the `$property` token. - pub(super) fn skip_optional_property_type(&mut self) -> Result<(), EvalParseError> { - if matches!(self.current(), TokenKind::DollarIdent(_)) { - return Ok(()); - } - if self.consume(TokenKind::Question) && matches!(self.current(), TokenKind::DollarIdent(_)) - { - return Err(EvalParseError::UnexpectedToken); - } - match self.current() { - TokenKind::Ident(_) | TokenKind::Backslash => { - let _ = self.parse_qualified_name()?; - Ok(()) - } - _ => Err(EvalParseError::UnexpectedToken), - } + /// Parses retained property type metadata before the `$property` token. + pub(super) fn parse_optional_property_type( + &mut self, + ) -> Result, EvalParseError> { + self.parse_optional_parameter_type() } /// Parses `function name($param, ...) { ... }` declarations. diff --git a/docs/php/eval.md b/docs/php/eval.md index 5d242adde9..f0a77026e4 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -240,7 +240,9 @@ when the variadic container itself is not rebound, and are reported through `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, and `getModifiers()` report eval property metadata with PHP-compatible `ReflectionProperty::IS_*` constants for -the bitmask. +the bitmask. `ReflectionProperty::hasType()` and `getType()` expose retained +property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and +`ReflectionIntersectionType` where eval has retained a supported declared type. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant @@ -413,7 +415,7 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/NamedType/UnionType/IntersectionType -and attribute slice, Reflection type APIs beyond parameter metadata, broader +and attribute slice, Reflection type APIs beyond parameter/property metadata, broader parameter default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 49dacbb3f7..f683c1bdac 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -49,6 +49,7 @@ struct ReflectionOwnerMetadata { backing_value: Option, is_enum_case: bool, parameter_members: Vec, + type_metadata: Option, required_parameter_count: i64, is_final: bool, is_abstract: bool, @@ -82,6 +83,7 @@ struct ReflectionListedMember { is_enum_case: bool, flags: ReflectionMemberFlags, modifiers: i64, + type_metadata: Option, required_parameter_count: i64, parameters: Vec, } @@ -384,6 +386,7 @@ fn emit_reflection_owner_object( } if class_name == "ReflectionProperty" { emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + emit_reflection_owner_type_property(ctx, class_name, metadata.type_metadata.as_ref())?; } if class_name == "ReflectionParameter" { if let Some(parameter) = metadata.parameter_members.first() { @@ -502,6 +505,7 @@ fn reflection_class_metadata_for_name( backing_value: None, is_enum_case: false, parameter_members: Vec::new(), + type_metadata: None, required_parameter_count: 0, is_final: info.is_final, is_abstract: info.is_abstract, @@ -553,6 +557,7 @@ fn reflection_class_metadata_for_name( backing_value: None, is_enum_case: false, parameter_members: Vec::new(), + type_metadata: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -605,6 +610,7 @@ fn reflection_class_metadata_for_name( backing_value: None, is_enum_case: false, parameter_members: Vec::new(), + type_metadata: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -734,6 +740,7 @@ fn reflection_method_owner_metadata( backing_value: None, is_enum_case: member.is_enum_case, parameter_members: member.parameters, + type_metadata: None, required_parameter_count: member.required_parameter_count, is_final: false, is_abstract: false, @@ -785,6 +792,7 @@ fn reflection_property_metadata( backing_value: None, is_enum_case: false, parameter_members: Vec::new(), + type_metadata: reflection_property_type_metadata(info, &property_name), required_parameter_count: 0, is_final: false, is_abstract: false, @@ -957,6 +965,7 @@ fn reflection_class_constant_metadata( backing_value: None, is_enum_case: true, parameter_members: Vec::new(), + type_metadata: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -1012,6 +1021,7 @@ fn reflection_enum_case_metadata( backing_value: reflection_enum_case_backing_value(case), is_enum_case: true, parameter_members: Vec::new(), + type_metadata: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -1054,6 +1064,7 @@ fn reflection_class_constant_owner_metadata( backing_value: None, is_enum_case: false, parameter_members: Vec::new(), + type_metadata: None, required_parameter_count: 0, is_final, is_abstract: false, @@ -1658,6 +1669,7 @@ fn push_unique_constant_reflection_member( is_enum_case, flags: reflection_member_flags(false, &visibility, is_final, false, false), modifiers: reflection_class_constant_modifiers(&visibility, is_final), + type_metadata: None, required_parameter_count: 0, parameters: Vec::new(), }); @@ -1853,6 +1865,7 @@ fn reflection_class_method_member( is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), + type_metadata: None, required_parameter_count, parameters, }) @@ -1914,6 +1927,7 @@ fn reflection_interface_method_member( is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), + type_metadata: None, required_parameter_count, parameters, }) @@ -1964,6 +1978,7 @@ fn reflection_trait_method_member( is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), + type_metadata: None, required_parameter_count, parameters: reflection_parameter_members_with_declaring_class( &info.signature, @@ -2000,6 +2015,7 @@ fn reflection_class_property_member( reflection_member_flags(false, &Visibility::Public, false, false, true), ) })?; + let type_metadata = reflection_property_type_metadata(info, property_name); Some(ReflectionListedMember { name: property_name.to_string(), declaring_class_name: reflection_property_declaring_class_name(info, property_name) @@ -2022,11 +2038,24 @@ fn reflection_class_property_member( flags, modifiers: reflection_property_modifiers_for_info(info, property_name) .unwrap_or_else(|| reflection_property_modifiers_from_flags(flags)), + type_metadata, required_parameter_count: 0, parameters: Vec::new(), }) } +/// Returns reflection type metadata for one typed property visible on a class. +fn reflection_property_type_metadata( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + if !info.visible_property_is_declared(property_name) { + return None; + } + let (_, (_, property_type)) = info.visible_property(property_name)?; + reflection_parameter_type_metadata(None, property_type) +} + /// Builds placeholder ReflectionMethod entries for class-like metadata without full method schemas. fn default_method_members( method_names: &[String], @@ -2050,6 +2079,7 @@ fn default_method_members( is_interface, false, )), + type_metadata: None, required_parameter_count: 0, parameters: Vec::new(), }) @@ -2081,6 +2111,7 @@ fn default_property_members( is_interface, None, ), + type_metadata: None, required_parameter_count: 0, parameters: Vec::new(), }) @@ -2736,6 +2767,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { backing_value: None, is_enum_case: false, parameter_members: Vec::new(), + type_metadata: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -3372,6 +3404,11 @@ fn emit_reflection_member_object( "__modifiers", member.modifiers, )?; + emit_reflection_owner_type_property( + ctx, + member_class_name, + member.type_metadata.as_ref(), + )?; } if member_class_name == "ReflectionClassConstant" { if let Some(value) = &member.constant_value { @@ -3586,19 +3623,28 @@ fn emit_reflection_parameter_declaring_class_property( fn emit_reflection_parameter_type_property( ctx: &mut FunctionContext<'_>, parameter: &ReflectionParameterMember, +) -> Result<()> { + emit_reflection_owner_type_property(ctx, "ReflectionParameter", parameter.type_metadata.as_ref()) +} + +/// Writes one reflection owner's nullable type slot. +fn emit_reflection_owner_type_property( + ctx: &mut FunctionContext<'_>, + class_name: &str, + type_metadata: Option<&ReflectionParameterTypeMetadata>, ) -> Result<()> { let type_offset = { let class_info = ctx .module .class_infos - .get("ReflectionParameter") + .get(class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; reflection_property_offset(class_info, "__type")? }; let result_reg = abi::int_result_reg(ctx.emitter); let object_reg = abi::symbol_scratch_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); - match parameter.type_metadata.as_ref() { + match type_metadata { Some(ReflectionParameterTypeMetadata::Named(type_metadata)) => { emit_reflection_named_type_object(ctx, type_metadata)?; emit_box_current_value_as_mixed( diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 3e4bb762b0..2da929a8bd 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1776,6 +1776,43 @@ fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> Cl } } +/// Returns `ReflectionProperty::hasType()` backed by a nullable private `__type` slot. +fn builtin_reflection_property_has_type_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "hasType".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::BinaryOp { + left: Box::new(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: "__type".to_string(), + }, + dummy_span, + )), + op: BinOp::StrictNotEq, + right: Box::new(Expr::new(ExprKind::Null, dummy_span)), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds a `FlattenedClass` for simple reflection owner classes /// with a private `__attrs` array property and two methods: `__construct` /// (public, accepting the supplied params) and `getAttributes` (public, @@ -1987,6 +2024,12 @@ fn add_reflection_member_flag_methods( )); } if class_name == "ReflectionProperty" { + properties.push(builtin_property( + "__type", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); properties.push(builtin_property( "__modifiers", Visibility::Private, @@ -1997,6 +2040,8 @@ fn add_reflection_member_flag_methods( "getModifiers", "__modifiers", )); + methods.push(builtin_reflection_property_has_type_method()); + methods.push(builtin_reflection_class_mixed_method("getType", "__type")); for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { properties.push(builtin_property( property, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 41f6648646..0fbd5d6e20 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6658,6 +6658,58 @@ foreach ($params as $param) { ); } +/// Verifies eval ReflectionProperty materializes property type metadata through the bridge. +#[test] +fn test_eval_reflection_property_get_type_metadata() { + let out = compile_and_run_capture( + r#"getProperties(); +foreach ($properties as $property) { + echo $property->getName() . ":"; + echo $property->hasType() ? "T:" : "t:"; + $type = $property->getType(); + if ($property->getName() == "union") { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionProperty("EvalReflectPropertyTypeTarget", "dep"); +$directType = $direct->getType(); +echo "direct:"; +echo $direct->hasType() ? "T:" : "t:"; +echo $directType->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep" + ); +} + /// Verifies eval ReflectionParameter exposes the declaring class for method parameters. #[test] fn test_eval_reflection_parameter_reports_declaring_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 2ca87b5883..852f6a0c2b 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2143,6 +2143,58 @@ if ($directType instanceof ReflectionNamedType) { ); } +/// Verifies that `ReflectionProperty::getType()` returns named and union metadata. +#[test] +fn test_reflection_property_get_type_returns_type_metadata() { + let out = compile_and_run_capture( + r#"getProperties(); +foreach ($properties as $property) { + echo $property->getName() . ":"; + echo $property->hasType() ? "T:" : "t:"; + $type = $property->getType(); + if ($type instanceof ReflectionNamedType) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } elseif ($type instanceof ReflectionUnionType) { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionProperty(ReflectPropertyTypeTarget::class, "dep"); +$directType = $direct->getType(); +if ($directType instanceof ReflectionNamedType) { + echo "direct:" . $directType->getName(); +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:T:int!B|name:T:string?B|dep:T:ReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:ReflectPropertyTypeDep" + ); +} + /// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. #[test] fn test_reflection_parameter_get_attributes_returns_parameter_metadata() { From 94dbeead1d859f03e5222a87a324bae61fcf46d5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 17:20:37 +0200 Subject: [PATCH 0429/1208] Expose ReflectionProperty default metadata --- .../elephc-eval/src/interpreter/reflection.rs | 138 +++++++++----- .../tests/builtins_class_metadata.rs | 41 ++++- .../interpreter/tests/support/object_ops.rs | 12 +- docs/php/classes.md | 6 +- docs/php/eval.md | 3 + src/codegen/eval_reflection_owner_helpers.rs | 17 ++ src/codegen/lower_inst/objects/reflection.rs | 173 ++++++++++++++---- src/types/checker/builtin_types/reflection.rs | 42 ++++- tests/codegen/eval.rs | 38 ++++ tests/codegen/oop/attributes.rs | 60 ++++++ 10 files changed, 435 insertions(+), 95 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 75cf013df4..1dfa48749e 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -27,6 +27,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; +const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -59,6 +60,7 @@ struct EvalReflectionMemberMetadata { is_readonly: bool, modifiers: u64, type_metadata: Option, + default_value: Option, required_parameter_count: usize, parameters: Vec, } @@ -440,6 +442,7 @@ fn eval_reflection_class_constant_object_result( Some(&declaring_class_name), &[], None, + None, flags, modifiers, 0, @@ -511,6 +514,7 @@ fn eval_reflection_class_new( metadata.parent_class_name.as_deref(), &[], None, + None, metadata.flags, metadata.modifiers, 0, @@ -612,6 +616,7 @@ fn eval_reflection_class_constant_new( Some(&declaring_class_name), &[], None, + None, flags, modifiers, 0, @@ -674,6 +679,7 @@ fn eval_reflection_enum_case_new( Some(&declaring_class_name), &[], None, + None, 0, 0, 0, @@ -697,6 +703,7 @@ fn eval_reflection_owner_object( parent_class_name: Option<&str>, parameter_metadata: &[EvalReflectionParameterMetadata], type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, flags: u64, modifiers: u64, method_modifiers: u64, @@ -716,6 +723,7 @@ fn eval_reflection_owner_object( parent_class_name, parameter_metadata, type_metadata, + default_value, flags, modifiers, method_modifiers, @@ -739,6 +747,7 @@ fn eval_reflection_owner_object_with_members( parent_class_name: Option<&str>, parameter_metadata: &[EvalReflectionParameterMetadata], type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, flags: u64, modifiers: u64, method_modifiers: u64, @@ -779,6 +788,11 @@ fn eval_reflection_owner_object_with_members( context, values, )? + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + match default_value { + Some(default) => eval_method_parameter_default(default, context, values)?, + None => values.null()?, + } } else { values.array_new(0)? }; @@ -891,6 +905,7 @@ fn eval_reflection_full_class_object_result( metadata.parent_class_name.as_deref(), &[], None, + None, metadata.flags, metadata.modifiers, 0, @@ -921,6 +936,7 @@ fn eval_reflection_shallow_class_object_result( None, &[], None, + None, metadata.flags, metadata.modifiers, 0, @@ -1042,6 +1058,7 @@ fn eval_reflection_declaring_function_object_result( metadata.declaring_class_name.as_deref(), &[], None, + None, metadata.flags, metadata.required_parameter_count as u64, eval_reflection_method_modifiers_from_flags(metadata.flags), @@ -1255,13 +1272,16 @@ fn eval_reflection_member_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let flags = eval_reflection_member_flags( + let mut flags = eval_reflection_member_flags( member.visibility, member.is_static, member.is_final, member.is_abstract, member.is_readonly, ); + if member.default_value.is_some() { + flags |= EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE; + } let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { member.required_parameter_count as u64 } else { @@ -1283,6 +1303,7 @@ fn eval_reflection_member_object_result( member.declaring_class_name.as_deref(), &member.parameters, member.type_metadata.as_ref(), + member.default_value.as_ref(), flags, owner_modifiers, method_modifiers, @@ -1583,8 +1604,9 @@ fn eval_reflection_class_constant_metadata( )); } } - context.class_constant(class_name, constant_name).map( - |(declaring_class, constant)| { + context + .class_constant(class_name, constant_name) + .map(|(declaring_class, constant)| { ( declaring_class, constant.attributes().to_vec(), @@ -1592,8 +1614,7 @@ fn eval_reflection_class_constant_metadata( constant.is_final(), false, ) - }, - ) + }) } /// Returns true when a name resolves to an eval-declared class-like symbol. @@ -1726,6 +1747,7 @@ fn eval_reflection_method_metadata( method.is_abstract(), ), type_metadata: None, + default_value: None, required_parameter_count, parameters, } @@ -1781,6 +1803,7 @@ fn eval_reflection_method_metadata( true, ), type_metadata: None, + default_value: None, required_parameter_count, parameters, } @@ -1836,6 +1859,7 @@ fn eval_reflection_method_metadata( method.is_abstract(), ), type_metadata: None, + default_value: None, required_parameter_count, parameters, } @@ -1851,27 +1875,31 @@ fn eval_reflection_property_metadata( ) -> Option { if context.has_class(class_name) || context.has_enum(class_name) { return context.class_property(class_name, property_name).map( - |(declaring_class, property)| EvalReflectionMemberMetadata { - declaring_class_name: Some(declaring_class), - attributes: property.attributes().to_vec(), - visibility: property.visibility(), - is_static: property.is_static(), - is_final: property.is_final(), - is_abstract: property.is_abstract(), - is_readonly: property.is_readonly(), - modifiers: eval_reflection_property_modifiers( - property.visibility(), - property.is_static(), - property.is_final(), - property.is_abstract(), - property.is_readonly(), - eval_reflection_property_is_virtual(&property), - ), - type_metadata: property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - required_parameter_count: 0, - parameters: Vec::new(), + |(declaring_class, property)| { + let default_value = eval_reflection_property_default_value(&property); + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + attributes: property.attributes().to_vec(), + visibility: property.visibility(), + is_static: property.is_static(), + is_final: property.is_final(), + is_abstract: property.is_abstract(), + is_readonly: property.is_readonly(), + modifiers: eval_reflection_property_modifiers( + property.visibility(), + property.is_static(), + property.is_final(), + property.is_abstract(), + property.is_readonly(), + eval_reflection_property_is_virtual(&property), + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + default_value, + required_parameter_count: 0, + parameters: Vec::new(), + } }, ); } @@ -1899,6 +1927,7 @@ fn eval_reflection_property_metadata( type_metadata: property .property_type() .and_then(eval_reflection_parameter_type_metadata), + default_value: None, required_parameter_count: 0, parameters: Vec::new(), }); @@ -1908,31 +1937,46 @@ fn eval_reflection_property_metadata( .properties() .iter() .find(|property| property.name() == property_name) - .map(|property| EvalReflectionMemberMetadata { - declaring_class_name: Some(trait_decl.name().to_string()), - attributes: property.attributes().to_vec(), - visibility: property.visibility(), - is_static: property.is_static(), - is_final: property.is_final(), - is_abstract: property.is_abstract(), - is_readonly: property.is_readonly(), - modifiers: eval_reflection_property_modifiers( - property.visibility(), - property.is_static(), - property.is_final(), - property.is_abstract(), - property.is_readonly(), - eval_reflection_property_is_virtual(property), - ), - type_metadata: property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - required_parameter_count: 0, - parameters: Vec::new(), + .map(|property| { + let default_value = eval_reflection_property_default_value(property); + EvalReflectionMemberMetadata { + declaring_class_name: Some(trait_decl.name().to_string()), + attributes: property.attributes().to_vec(), + visibility: property.visibility(), + is_static: property.is_static(), + is_final: property.is_final(), + is_abstract: property.is_abstract(), + is_readonly: property.is_readonly(), + modifiers: eval_reflection_property_modifiers( + property.visibility(), + property.is_static(), + property.is_final(), + property.is_abstract(), + property.is_readonly(), + eval_reflection_property_is_virtual(property), + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + default_value, + required_parameter_count: 0, + parameters: Vec::new(), + } }) }) } +/// Returns ReflectionProperty default metadata for concrete eval properties. +fn eval_reflection_property_default_value(property: &EvalClassProperty) -> Option { + if let Some(default) = property.default() { + return Some(default.clone()); + } + if property.is_abstract() || property.property_type().is_some() { + return None; + } + Some(EvalExpr::Const(EvalConst::Null)) +} + /// Returns PHP's required parameter count for a reflected method signature. fn eval_reflection_required_parameter_count( defaults: &[Option], diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 24260e1bcb..4035c22701 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -968,7 +968,7 @@ return true;"##, #[test] fn execute_program_reflection_property_get_type_metadata() { let program = parse_fragment( -br##"class EvalReflectPropertyTypeDep {} + br##"class EvalReflectPropertyTypeDep {} class EvalReflectPropertyTypeTarget { public int $id; public ?string $name; @@ -1016,6 +1016,45 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty exposes eval property default metadata. +#[test] +fn execute_program_reflection_property_get_default_value_metadata() { + let program = parse_fragment( + br#"class EvalReflectPropertyDefaultTarget { + public $implicit; + public int $typed; + public ?string $nullableTyped; + public $explicitNull = null; + public int $count = 7; + public static string $label = "ok"; +} +foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label"] as $name) { + $property = new ReflectionProperty("EvalReflectPropertyDefaultTarget", $name); + echo $property->getName(); echo ":"; + echo $property->hasDefaultValue() ? "D:" : "d:"; + $value = $property->getDefaultValue(); + echo $value === null ? "null" : $value; + echo "|"; +} +$listed = (new ReflectionClass("EvalReflectPropertyDefaultTarget"))->getProperty("implicit"); +echo "listed:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue() === null ? "null" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "implicit:D:null|typed:d:null|nullableTyped:d:null|explicitNull:D:null|count:D:7|label:D:ok|listed:D:null" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval ReflectionParameter exposes declaring class metadata. #[test] fn execute_program_reflects_eval_parameter_declaring_class() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 2464f6ee9b..f7f898ca82 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -17,6 +17,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; +const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -161,8 +162,7 @@ impl FakeOps { .map_or_else(|| self.bool_value(false), Ok) } (FakeValue::Object(properties), "getconstructor") if args.is_empty() => { - Self::object_property(&properties, "__constructor") - .map_or_else(|| self.null(), Ok) + Self::object_property(&properties, "__constructor").map_or_else(|| self.null(), Ok) } (FakeValue::Object(properties), "getdeclaringclass") if args.is_empty() => { Self::object_property(&properties, "__declaring_class") @@ -343,7 +343,9 @@ impl FakeOps { None => self.bool_value(false), } } - (FakeValue::Object(properties), "isdefaultvalueavailable") if args.is_empty() => { + (FakeValue::Object(properties), "isdefaultvalueavailable" | "hasdefaultvalue") + if args.is_empty() => + { Self::object_property(&properties, "__has_default_value") .map_or_else(|| self.bool_value(false), Ok) } @@ -553,11 +555,15 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; let is_readonly = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY) != 0)?; + let has_default_value = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE) != 0)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_readonly".to_string(), is_readonly)); properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__type".to_string(), method_objects)); + properties.push(("__has_default_value".to_string(), has_default_value)); + properties.push(("__default_value".to_string(), property_objects)); } } if matches!( diff --git a/docs/php/classes.md b/docs/php/classes.md index fba8520197..eb0dcdb628 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1217,6 +1217,10 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isAbstract()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is abstract | | `ReflectionProperty::isReadOnly()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is readonly | | `ReflectionProperty::getModifiers()` | `new ReflectionProperty($class_name, $property_name)` | Return PHP's `ReflectionProperty::IS_*` visibility/static/finality/abstract/readonly/virtual/set-visibility bitmask | +| `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | +| `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | +| `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | +| `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | @@ -1297,7 +1301,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index f0a77026e4..8332e1acbe 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -243,6 +243,9 @@ property metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the bitmask. `ReflectionProperty::hasType()` and `getType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained a supported declared type. +`ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose +materialized property default metadata, including PHP's implicit `null` default +for untyped concrete properties without an explicit initializer. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 6a9e179119..b3322f36c3 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1148,6 +1148,14 @@ fn emit_set_owner_member_flags_property_aarch64( abi::emit_store_to_address(emitter, "x10", "x9", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); } + if let (Some(has_default_value_lo), Some(has_default_value_hi)) = + (layout.has_default_value_lo, layout.has_default_value_hi) + { + emitter.instruction("lsr x10, x11, #8"); // move the default-value bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-value flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", has_default_value_lo); + abi::emit_store_zero_to_address(emitter, "x9", has_default_value_hi); + } if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { if layout.required_parameter_count_lo.is_some() { emitter.instruction("ldr x10, [sp, #72]"); // reload PHP ReflectionMethod::getModifiers() bitmask @@ -1253,6 +1261,15 @@ fn emit_set_owner_member_flags_property_x86_64( abi::emit_store_to_address(emitter, "rax", "r10", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); } + if let (Some(has_default_value_lo), Some(has_default_value_hi)) = + (layout.has_default_value_lo, layout.has_default_value_hi) + { + emitter.instruction("mov rax, r11"); // copy flags before extracting the default-value bit + emitter.instruction("shr rax, 8"); // move the default-value bit into position + emitter.instruction("and rax, 1"); // extract the default-value flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", has_default_value_lo); + abi::emit_store_zero_to_address(emitter, "r10", has_default_value_hi); + } if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { if layout.required_parameter_count_lo.is_some() { emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload PHP ReflectionMethod::getModifiers() bitmask diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index f683c1bdac..6f3a5103a6 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -50,6 +50,7 @@ struct ReflectionOwnerMetadata { is_enum_case: bool, parameter_members: Vec, type_metadata: Option, + property_default_value: Option, required_parameter_count: i64, is_final: bool, is_abstract: bool, @@ -84,6 +85,7 @@ struct ReflectionListedMember { flags: ReflectionMemberFlags, modifiers: i64, type_metadata: Option, + default_value: Option, required_parameter_count: i64, parameters: Vec, } @@ -387,6 +389,17 @@ fn emit_reflection_owner_object( if class_name == "ReflectionProperty" { emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; emit_reflection_owner_type_property(ctx, class_name, metadata.type_metadata.as_ref())?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__has_default_value", + metadata.property_default_value.is_some(), + )?; + emit_reflection_owner_default_value_property( + ctx, + class_name, + metadata.property_default_value.as_ref(), + )?; } if class_name == "ReflectionParameter" { if let Some(parameter) = metadata.parameter_members.first() { @@ -506,6 +519,7 @@ fn reflection_class_metadata_for_name( is_enum_case: false, parameter_members: Vec::new(), type_metadata: None, + property_default_value: None, required_parameter_count: 0, is_final: info.is_final, is_abstract: info.is_abstract, @@ -558,6 +572,7 @@ fn reflection_class_metadata_for_name( is_enum_case: false, parameter_members: Vec::new(), type_metadata: None, + property_default_value: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -611,6 +626,7 @@ fn reflection_class_metadata_for_name( is_enum_case: false, parameter_members: Vec::new(), type_metadata: None, + property_default_value: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -671,8 +687,11 @@ fn reflection_function_metadata( metadata.reflected_name = Some(reflected_name); metadata.attr_names = function.attribute_names.clone(); metadata.attr_args = function.attribute_args.clone(); - metadata.parameter_members = - reflection_parameter_members_with_declaring_function(signature, None, Some(declaring_function)); + metadata.parameter_members = reflection_parameter_members_with_declaring_function( + signature, + None, + Some(declaring_function), + ); metadata.required_parameter_count = required_parameter_count; Ok(metadata) } @@ -707,8 +726,7 @@ fn reflection_method_metadata( } if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { if let Some(methods) = ctx.module.declared_trait_methods.get(trait_name) { - if let Some(member) = reflection_trait_method_member(methods, trait_name, &method_key) - { + if let Some(member) = reflection_trait_method_member(methods, trait_name, &method_key) { return Ok(reflection_method_owner_metadata(&method_name, member)); } } @@ -741,6 +759,7 @@ fn reflection_method_owner_metadata( is_enum_case: member.is_enum_case, parameter_members: member.parameters, type_metadata: None, + property_default_value: None, required_parameter_count: member.required_parameter_count, is_final: false, is_abstract: false, @@ -769,10 +788,8 @@ fn reflection_property_metadata( let property_name = const_required_string_operand(ctx, property_operand, "ReflectionProperty")?; Ok(resolve_reflection_class(ctx, &reflected_class) .and_then(|(_, info)| { - let declaring_class_name = reflection_property_declaring_class_name( - info, - &property_name, - ); + let declaring_class_name = + reflection_property_declaring_class_name(info, &property_name); Some(ReflectionOwnerMetadata { reflected_name: Some(property_name.clone()), attr_names: info.property_attribute_names.get(&property_name)?.clone(), @@ -793,6 +810,7 @@ fn reflection_property_metadata( is_enum_case: false, parameter_members: Vec::new(), type_metadata: reflection_property_type_metadata(info, &property_name), + property_default_value: reflection_property_default_value(info, &property_name), required_parameter_count: 0, is_final: false, is_abstract: false, @@ -866,8 +884,11 @@ fn reflection_function_parameter_metadata( attr_args: function.attribute_args.clone(), required_parameter_count: reflection_required_parameter_count(signature), }; - let parameters = - reflection_parameter_members_with_declaring_function(signature, None, Some(declaring_function)); + let parameters = reflection_parameter_members_with_declaring_function( + signature, + None, + Some(declaring_function), + ); let Some(parameter) = reflection_parameter_member_for_selector(¶meters, selector) else { return Ok(empty_reflection_metadata()); }; @@ -966,6 +987,7 @@ fn reflection_class_constant_metadata( is_enum_case: true, parameter_members: Vec::new(), type_metadata: None, + property_default_value: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -978,9 +1000,11 @@ fn reflection_class_constant_metadata( member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), }); } - Ok(reflection_class_constant_lookup(ctx, &reflected_class, &constant_name)? - .map(|metadata| reflection_class_constant_owner_metadata(constant_name, metadata)) - .unwrap_or_else(empty_reflection_metadata)) + Ok( + reflection_class_constant_lookup(ctx, &reflected_class, &constant_name)? + .map(|metadata| reflection_class_constant_owner_metadata(constant_name, metadata)) + .unwrap_or_else(empty_reflection_metadata), + ) } /// Resolves `ReflectionEnumUnitCase/BackedCase(enum, case)` metadata. @@ -1022,6 +1046,7 @@ fn reflection_enum_case_metadata( is_enum_case: true, parameter_members: Vec::new(), type_metadata: None, + property_default_value: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -1065,6 +1090,7 @@ fn reflection_class_constant_owner_metadata( is_enum_case: false, parameter_members: Vec::new(), type_metadata: None, + property_default_value: None, required_parameter_count: 0, is_final, is_abstract: false, @@ -1090,7 +1116,8 @@ fn reflection_class_constant_lookup( let Some(value_expr) = info.constants.get(constant_name) else { return Ok(None); }; - let value = reflection_constant_value(ctx, declaring_class_name, Some(info), value_expr, 0)?; + let value = + reflection_constant_value(ctx, declaring_class_name, Some(info), value_expr, 0)?; return Ok(Some(ReflectionClassConstantMetadata { declaring_class_name: declaring_class_name.to_string(), attr_names: info @@ -1670,6 +1697,7 @@ fn push_unique_constant_reflection_member( flags: reflection_member_flags(false, &visibility, is_final, false, false), modifiers: reflection_class_constant_modifiers(&visibility, is_final), type_metadata: None, + default_value: None, required_parameter_count: 0, parameters: Vec::new(), }); @@ -1866,6 +1894,7 @@ fn reflection_class_method_member( flags, modifiers: reflection_method_modifiers_from_flags(flags), type_metadata: None, + default_value: None, required_parameter_count, parameters, }) @@ -1928,6 +1957,7 @@ fn reflection_interface_method_member( flags, modifiers: reflection_method_modifiers_from_flags(flags), type_metadata: None, + default_value: None, required_parameter_count, parameters, }) @@ -1979,6 +2009,7 @@ fn reflection_trait_method_member( flags, modifiers: reflection_method_modifiers_from_flags(flags), type_metadata: None, + default_value: None, required_parameter_count, parameters: reflection_parameter_members_with_declaring_class( &info.signature, @@ -2016,6 +2047,7 @@ fn reflection_class_property_member( ) })?; let type_metadata = reflection_property_type_metadata(info, property_name); + let default_value = reflection_property_default_value(info, property_name); Some(ReflectionListedMember { name: property_name.to_string(), declaring_class_name: reflection_property_declaring_class_name(info, property_name) @@ -2039,6 +2071,7 @@ fn reflection_class_property_member( modifiers: reflection_property_modifiers_for_info(info, property_name) .unwrap_or_else(|| reflection_property_modifiers_from_flags(flags)), type_metadata, + default_value, required_parameter_count: 0, parameters: Vec::new(), }) @@ -2056,6 +2089,40 @@ fn reflection_property_type_metadata( reflection_parameter_type_metadata(None, property_type) } +/// Returns supported default metadata for one reflected property. +fn reflection_property_default_value( + info: &crate::types::ClassInfo, + property_name: &str, +) -> Option { + if let Some((index, (name, _))) = info.visible_property(property_name) { + return reflection_property_slot_default_value( + info.property_slot_is_declared(index, name), + info.defaults.get(index).and_then(Option::as_ref), + ); + } + info.static_properties + .iter() + .position(|(name, _)| name == property_name) + .and_then(|index| { + reflection_property_slot_default_value( + info.declared_static_properties.contains(property_name), + info.static_defaults.get(index).and_then(Option::as_ref), + ) + }) +} + +/// Converts one physical property slot default into PHP Reflection metadata. +fn reflection_property_slot_default_value( + is_declared: bool, + default: Option<&Expr>, +) -> Option { + match default { + Some(default) => reflection_parameter_default_value(default), + None if !is_declared => Some(ReflectionParameterDefaultValue::Null), + None => None, + } +} + /// Builds placeholder ReflectionMethod entries for class-like metadata without full method schemas. fn default_method_members( method_names: &[String], @@ -2080,6 +2147,7 @@ fn default_method_members( false, )), type_metadata: None, + default_value: None, required_parameter_count: 0, parameters: Vec::new(), }) @@ -2112,6 +2180,7 @@ fn default_property_members( None, ), type_metadata: None, + default_value: None, required_parameter_count: 0, parameters: Vec::new(), }) @@ -2768,6 +2837,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { is_enum_case: false, parameter_members: Vec::new(), type_metadata: None, + property_default_value: None, required_parameter_count: 0, is_final: false, is_abstract: false, @@ -3150,7 +3220,11 @@ fn emit_reflection_declaring_class_property( .class_infos .get(member_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; - let Some(low_offset) = class_info.property_offsets.get("__declaring_class").copied() else { + let Some(low_offset) = class_info + .property_offsets + .get("__declaring_class") + .copied() + else { return Ok(()); }; let high_offset = low_offset + 8; @@ -3304,8 +3378,8 @@ fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection constant value as the hash payload - ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word abi::emit_pop_reg(ctx.emitter, "x0"); abi::emit_symbol_address(ctx.emitter, "x1", &key_label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); @@ -3317,8 +3391,8 @@ fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str abi::emit_call_label(ctx.emitter, "__rt_hash_set"); } Arch::X86_64 => { - ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection constant value as the hash payload - ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word abi::emit_pop_reg(ctx.emitter, "rdi"); abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); @@ -3404,10 +3478,17 @@ fn emit_reflection_member_object( "__modifiers", member.modifiers, )?; - emit_reflection_owner_type_property( + emit_reflection_owner_type_property(ctx, member_class_name, member.type_metadata.as_ref())?; + emit_reflection_owner_bool_property( ctx, member_class_name, - member.type_metadata.as_ref(), + "__has_default_value", + member.default_value.is_some(), + )?; + emit_reflection_owner_default_value_property( + ctx, + member_class_name, + member.default_value.as_ref(), )?; } if member_class_name == "ReflectionClassConstant" { @@ -3579,7 +3660,12 @@ fn emit_reflection_parameter_declaring_function_property( None => emit_boxed_null_literal_to_result(ctx), } abi::emit_pop_reg(ctx.emitter, object_reg); - abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, declaring_function_offset); + abi::emit_store_to_address( + ctx.emitter, + result_reg, + object_reg, + declaring_function_offset, + ); abi::emit_store_zero_to_address(ctx.emitter, object_reg, declaring_function_offset + 8); abi::emit_reg_move(ctx.emitter, result_reg, object_reg); Ok(()) @@ -3624,7 +3710,11 @@ fn emit_reflection_parameter_type_property( ctx: &mut FunctionContext<'_>, parameter: &ReflectionParameterMember, ) -> Result<()> { - emit_reflection_owner_type_property(ctx, "ReflectionParameter", parameter.type_metadata.as_ref()) + emit_reflection_owner_type_property( + ctx, + "ReflectionParameter", + parameter.type_metadata.as_ref(), + ) } /// Writes one reflection owner's nullable type slot. @@ -3679,19 +3769,32 @@ fn emit_reflection_owner_type_property( fn emit_reflection_parameter_default_property( ctx: &mut FunctionContext<'_>, parameter: &ReflectionParameterMember, +) -> Result<()> { + emit_reflection_owner_default_value_property( + ctx, + "ReflectionParameter", + parameter.default_value.as_ref(), + ) +} + +/// Writes one reflection owner's boxed default-value slot. +fn emit_reflection_owner_default_value_property( + ctx: &mut FunctionContext<'_>, + class_name: &str, + default_value: Option<&ReflectionParameterDefaultValue>, ) -> Result<()> { let default_offset = { let class_info = ctx .module .class_infos - .get("ReflectionParameter") + .get(class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; reflection_property_offset(class_info, "__default_value")? }; let result_reg = abi::int_result_reg(ctx.emitter); let object_reg = abi::symbol_scratch_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); - match parameter.default_value.as_ref() { + match default_value { Some(ReflectionParameterDefaultValue::Int(value)) => { emit_boxed_int_literal_to_result(ctx, *value) } @@ -3954,32 +4057,32 @@ fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) /// Appends ReflectionClass metadata names to the current ARM64 result array. fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result } /// Appends ReflectionClass metadata names to the current x86_64 result array. fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings - ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result } /// Stores ReflectionMethod/ReflectionProperty boolean predicate slots when supported. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 2da929a8bd..3f47857278 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2030,6 +2030,18 @@ fn add_reflection_member_flag_methods( Some(mixed_type()), null_expr(), )); + properties.push(builtin_property( + "__has_default_value", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__default_value", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); properties.push(builtin_property( "__modifiers", Visibility::Private, @@ -2042,6 +2054,14 @@ fn add_reflection_member_flag_methods( )); methods.push(builtin_reflection_property_has_type_method()); methods.push(builtin_reflection_class_mixed_method("getType", "__type")); + methods.push(builtin_reflection_class_bool_method( + "hasDefaultValue", + "__has_default_value", + )); + methods.push(builtin_reflection_class_mixed_method( + "getDefaultValue", + "__default_value", + )); for (property, method) in [("__is_final", "isFinal"), ("__is_abstract", "isAbstract")] { properties.push(builtin_property( property, @@ -2069,10 +2089,7 @@ fn add_reflection_member_flag_methods( Some(mixed_type()), Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), )); - methods.push(builtin_reflection_class_mixed_method( - "getValue", - "__value", - )); + methods.push(builtin_reflection_class_mixed_method("getValue", "__value")); properties.push(builtin_property( "__is_enum_case", Visibility::Private, @@ -2114,10 +2131,7 @@ fn add_reflection_member_flag_methods( Some(mixed_type()), Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), )); - methods.push(builtin_reflection_class_mixed_method( - "getValue", - "__value", - )); + methods.push(builtin_reflection_class_mixed_method("getValue", "__value")); } if class_name == "ReflectionEnumBackedCase" { properties.push(builtin_property( @@ -2359,6 +2373,18 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Bool; } } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("hasDefaultValue")) + { + sig.return_type = PhpType::Bool; + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getDefaultValue")) + { + sig.return_type = PhpType::Mixed; + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0fbd5d6e20..d028a638b7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6710,6 +6710,44 @@ echo $directType->getName();'); ); } +/// Verifies eval ReflectionProperty materializes property default metadata through the bridge. +#[test] +fn test_eval_reflection_property_get_default_value_metadata() { + let out = compile_and_run_capture( + r#"getName() . ":"; + echo $property->hasDefaultValue() ? "D:" : "d:"; + $value = $property->getDefaultValue(); + echo $value === null ? "null" : $value; + echo "|"; +} +$listed = (new ReflectionClass("EvalReflectPropertyDefaultTarget"))->getProperty("implicit"); +echo "listed:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue() === null ? "null" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "implicit:D:null|typed:d:null|nullableTyped:d:null|explicitNull:D:null|count:D:7|label:D:ok|listed:D:null" + ); +} + /// Verifies eval ReflectionParameter exposes the declaring class for method parameters. #[test] fn test_eval_reflection_parameter_reports_declaring_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 852f6a0c2b..a879b5e056 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2195,6 +2195,66 @@ if ($directType instanceof ReflectionNamedType) { ); } +/// Verifies that `ReflectionProperty` exposes supported property defaults. +#[test] +fn test_reflection_property_get_default_value_returns_property_metadata() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $implicit->hasDefaultValue() ? "D:" : "d:"; +echo $implicit->getDefaultValue() === null ? "null" : $implicit->getDefaultValue(); +echo "|"; +$typed = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "typed"); +echo $typed->getName() . ":"; +echo $typed->hasDefaultValue() ? "D:" : "d:"; +echo $typed->getDefaultValue() === null ? "null" : $typed->getDefaultValue(); +echo "|"; +$nullableTyped = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "nullableTyped"); +echo $nullableTyped->getName() . ":"; +echo $nullableTyped->hasDefaultValue() ? "D:" : "d:"; +echo $nullableTyped->getDefaultValue() === null ? "null" : $nullableTyped->getDefaultValue(); +echo "|"; +$explicitNull = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "explicitNull"); +echo $explicitNull->getName() . ":"; +echo $explicitNull->hasDefaultValue() ? "D:" : "d:"; +echo $explicitNull->getDefaultValue() === null ? "null" : $explicitNull->getDefaultValue(); +echo "|"; +$count = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "count"); +echo $count->getName() . ":"; +echo $count->hasDefaultValue() ? "D:" : "d:"; +echo $count->getDefaultValue() === null ? "null" : $count->getDefaultValue(); +echo "|"; +$label = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "label"); +echo $label->getName() . ":"; +echo $label->hasDefaultValue() ? "D:" : "d:"; +echo $label->getDefaultValue() === null ? "null" : $label->getDefaultValue(); +echo "|"; +$listed = (new ReflectionClass(ReflectPropertyDefaultTarget::class))->getProperty("implicit"); +echo "listed:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue() === null ? "null" : "bad"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "implicit:D:null|typed:d:null|nullableTyped:d:null|explicitNull:D:null|count:D:7|label:D:ok|listed:D:null" + ); +} + /// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. #[test] fn test_reflection_parameter_get_attributes_returns_parameter_metadata() { From d322899c6403efa3b9db3237ad138f5881967561 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 17:28:39 +0200 Subject: [PATCH 0430/1208] Expose ReflectionMethod lifecycle predicates --- .../tests/builtins_class_metadata.rs | 36 ++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 18 ++++++++ docs/php/classes.md | 4 +- docs/php/eval.md | 2 + src/types/checker/builtin_types/reflection.rs | 41 ++++++++++++++++++- tests/codegen/eval.rs | 35 ++++++++++++++++ tests/codegen/oop/attributes.rs | 31 ++++++++++++++ 7 files changed, 165 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 4035c22701..d14f6f0e90 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -854,6 +854,42 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod constructor/destructor predicates for eval methods. +#[test] +fn execute_program_reflection_method_reports_constructor_and_destructor() { + let program = parse_fragment( + br#"class EvalReflectLifecycle { + public function __construct() {} + public function __destruct() {} + public function run() {} +} +$ctor = new ReflectionMethod("EvalReflectLifecycle", "__CONSTRUCT"); +echo $ctor->isConstructor() ? "C" : "c"; +echo $ctor->isDestructor() ? "D" : "d"; +echo ":"; +$dtor = new ReflectionMethod("EvalReflectLifecycle", "__destruct"); +echo $dtor->isConstructor() ? "C" : "c"; +echo $dtor->isDestructor() ? "D" : "d"; +echo ":"; +$run = new ReflectionMethod("EvalReflectLifecycle", "run"); +echo $run->isConstructor() ? "C" : "c"; +echo $run->isDestructor() ? "D" : "d"; +echo ":"; +$listed = (new ReflectionClass("EvalReflectLifecycle"))->getConstructor(); +echo $listed->isConstructor() ? "C" : "c"; +echo $listed->isDestructor() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Cd:cD:cd:Cd"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval member and enum-case reflectors expose their declaring class. #[test] fn execute_program_reflects_eval_declaring_class_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index f7f898ca82..f89e59818c 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -322,6 +322,24 @@ impl FakeOps { Self::object_property(&properties, "__default_value") .map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "isconstructor") if args.is_empty() => { + let Some(name) = Self::object_property(&properties, "__name") else { + return self.bool_value(false); + }; + let FakeValue::String(name) = self.get(name) else { + return self.bool_value(false); + }; + self.bool_value(name.eq_ignore_ascii_case("__construct")) + } + (FakeValue::Object(properties), "isdestructor") if args.is_empty() => { + let Some(name) = Self::object_property(&properties, "__name") else { + return self.bool_value(false); + }; + let FakeValue::String(name) = self.get(name) else { + return self.bool_value(false); + }; + self.bool_value(name.eq_ignore_ascii_case("__destruct")) + } (FakeValue::Object(properties), "isoptional") if args.is_empty() => { Self::object_property(&properties, "__is_optional") .map_or_else(|| self.bool_value(false), Ok) diff --git a/docs/php/classes.md b/docs/php/classes.md index eb0dcdb628..6e6afa0a22 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1187,6 +1187,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isPrivate()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is private | | `ReflectionMethod::isFinal()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is final | | `ReflectionMethod::isAbstract()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is abstract | +| `ReflectionMethod::isConstructor()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getConstructor()` | Return whether the reflected method is the constructor | +| `ReflectionMethod::isDestructor()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is the destructor | | `ReflectionMethod::getModifiers()` | `new ReflectionMethod($class_name, $method_name)` | Return PHP's `ReflectionMethod::IS_*` visibility/static/finality/abstract bitmask | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | @@ -1301,7 +1303,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. ### Class constants diff --git a/docs/php/eval.md b/docs/php/eval.md index 8332e1acbe..612e38e3aa 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -204,6 +204,8 @@ eval/runtime class or interface names. `ReflectionMethod::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, and `getModifiers()` report eval method metadata, with PHP-compatible `ReflectionMethod::IS_*` constants for the bitmask. +`ReflectionMethod::isConstructor()` and `isDestructor()` report whether the +reflected method is `__construct` or `__destruct`. `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report eval-declared method parameter metadata. Eval currently exposes parameter names and zero-based positions there, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 3f47857278..98328aec14 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1776,6 +1776,37 @@ fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> Cl } } +/// Returns a `ReflectionMethod` predicate derived from its case-insensitive method name. +fn builtin_reflection_method_name_predicate_method( + method_name: &str, + expected_name: &str, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let lower_name = strtolower_call(reflection_this_property("__name", dummy_span), dummy_span); + let comparison = binary_expr( + lower_name, + BinOp::StrictEq, + string_lit(expected_name, dummy_span), + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new(StmtKind::Return(Some(comparison)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns `ReflectionProperty::hasType()` backed by a nullable private `__type` slot. fn builtin_reflection_property_has_type_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -2022,6 +2053,14 @@ fn add_reflection_member_flag_methods( "getModifiers", "__modifiers", )); + methods.push(builtin_reflection_method_name_predicate_method( + "isConstructor", + "__construct", + )); + methods.push(builtin_reflection_method_name_predicate_method( + "isDestructor", + "__destruct", + )); } if class_name == "ReflectionProperty" { properties.push(builtin_property( @@ -2390,7 +2429,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionMethod" { - for method_name in ["isfinal", "isabstract"] { + for method_name in ["isfinal", "isabstract", "isconstructor", "isdestructor"] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d028a638b7..fc815bcdc9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6425,6 +6425,41 @@ echo $classReadonlyProp->getModifiers();'); ); } +/// Verifies eval ReflectionMethod constructor/destructor predicates through the bridge. +#[test] +fn test_eval_reflection_method_reports_constructor_and_destructor() { + let out = compile_and_run_capture( + r#"isConstructor() ? "C" : "c"; +echo $ctor->isDestructor() ? "D" : "d"; +echo ":"; +$dtor = new ReflectionMethod("EvalReflectLifecycle", "__destruct"); +echo $dtor->isConstructor() ? "C" : "c"; +echo $dtor->isDestructor() ? "D" : "d"; +echo ":"; +$run = new ReflectionMethod("EvalReflectLifecycle", "run"); +echo $run->isConstructor() ? "C" : "c"; +echo $run->isDestructor() ? "D" : "d"; +echo ":"; +$listed = (new ReflectionClass("EvalReflectLifecycle"))->getConstructor(); +echo $listed->isConstructor() ? "C" : "c"; +echo $listed->isDestructor() ? "D" : "d";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Cd:cD:cd:Cd"); +} + /// Verifies eval-declared final properties cannot be redeclared by subclasses. #[test] fn test_eval_declared_final_property_override_fails() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index a879b5e056..99589b8e0f 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1852,6 +1852,37 @@ echo $classReadonlyProp->getModifiers(); ); } +/// Verifies that `ReflectionMethod::isConstructor()` and `isDestructor()` derive +/// their result from the reflected method name. +#[test] +fn test_reflection_method_reports_constructor_and_destructor() { + let out = compile_and_run( + r#"isConstructor() ? "C" : "c"; +echo $ctor->isDestructor() ? "D" : "d"; +echo ":"; +$dtor = new ReflectionMethod(ReflectLifecycle::class, "__destruct"); +echo $dtor->isConstructor() ? "C" : "c"; +echo $dtor->isDestructor() ? "D" : "d"; +echo ":"; +$run = new ReflectionMethod(ReflectLifecycle::class, "run"); +echo $run->isConstructor() ? "C" : "c"; +echo $run->isDestructor() ? "D" : "d"; +echo ":"; +$listed = (new ReflectionClass(ReflectLifecycle::class))->getConstructor(); +echo $listed->isConstructor() ? "C" : "c"; +echo $listed->isDestructor() ? "D" : "d"; +"#, + ); + assert_eq!(out, "Cd:cD:cd:Cd"); +} + /// Verifies member and enum-case reflectors expose their declaring class object. #[test] fn test_reflection_members_report_declaring_class() { From fd3a9ae943c3e378b65fa47c64de6b9c12629528 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 17:42:21 +0200 Subject: [PATCH 0431/1208] Expose ReflectionProperty isDefault --- .../tests/builtins_class_metadata.rs | 4 ++- .../interpreter/tests/support/object_ops.rs | 3 +++ docs/php/classes.md | 6 +++++ docs/php/eval.md | 9 ++++--- src/types/checker/builtin_types/reflection.rs | 27 ++++++++++++++++++- tests/codegen/eval.rs | 4 ++- tests/codegen/oop/attributes.rs | 9 ++++++- 7 files changed, 54 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index d14f6f0e90..853c6dab26 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1067,6 +1067,7 @@ fn execute_program_reflection_property_get_default_value_metadata() { foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label"] as $name) { $property = new ReflectionProperty("EvalReflectPropertyDefaultTarget", $name); echo $property->getName(); echo ":"; + echo $property->isDefault() ? "Y:" : "N:"; echo $property->hasDefaultValue() ? "D:" : "d:"; $value = $property->getDefaultValue(); echo $value === null ? "null" : $value; @@ -1074,6 +1075,7 @@ foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label" } $listed = (new ReflectionClass("EvalReflectPropertyDefaultTarget"))->getProperty("implicit"); echo "listed:"; +echo $listed->isDefault() ? "Y:" : "N:"; echo $listed->hasDefaultValue() ? "D:" : "d:"; echo $listed->getDefaultValue() === null ? "null" : "bad"; return true;"#, @@ -1086,7 +1088,7 @@ return true;"#, assert_eq!( values.output, - "implicit:D:null|typed:d:null|nullableTyped:d:null|explicitNull:D:null|count:D:7|label:D:ok|listed:D:null" + "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|listed:Y:D:null" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index f89e59818c..7d51edac32 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -367,6 +367,9 @@ impl FakeOps { Self::object_property(&properties, "__has_default_value") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isdefault") if args.is_empty() => { + self.bool_value(Self::object_property(&properties, "__has_default_value").is_some()) + } (FakeValue::Object(properties), "allowsnull") if args.is_empty() => { Self::object_property(&properties, "__allows_null") .map_or_else(|| self.bool_value(false), Ok) diff --git a/docs/php/classes.md b/docs/php/classes.md index 6e6afa0a22..a4fdf4288d 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1219,6 +1219,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isAbstract()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is abstract | | `ReflectionProperty::isReadOnly()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is readonly | | `ReflectionProperty::getModifiers()` | `new ReflectionProperty($class_name, $property_name)` | Return PHP's `ReflectionProperty::IS_*` visibility/static/finality/abstract/readonly/virtual/set-visibility bitmask | +| `ReflectionProperty::isDefault()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property is declared/default rather than dynamic | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | @@ -1305,6 +1306,11 @@ Limitations today: - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +`ReflectionProperty::isDefault()` is also supported for materialized declared +properties. elephc does not currently materialize dynamic object properties as +`ReflectionProperty` instances, so supported reflected properties report +`true`. + ### Class constants ```php diff --git a/docs/php/eval.md b/docs/php/eval.md index 612e38e3aa..d4a8cbe6e7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -240,10 +240,11 @@ parameters after method execution, write back mutated `&...$items` elements when the variadic container itself is not rebound, and are reported through `ReflectionParameter::isPassedByReference()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, -`isFinal()`, `isAbstract()`, `isReadOnly()`, and `getModifiers()` report eval -property metadata with PHP-compatible `ReflectionProperty::IS_*` constants for -the bitmask. `ReflectionProperty::hasType()` and `getType()` expose retained -property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and +`isFinal()`, `isAbstract()`, `isReadOnly()`, `isDefault()`, and +`getModifiers()` report eval property metadata with PHP-compatible +`ReflectionProperty::IS_*` constants for the bitmask. +`ReflectionProperty::hasType()` and `getType()` expose retained property type +metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained a supported declared type. `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose materialized property default metadata, including PHP's implicit `null` default diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 98328aec14..d31d3bb13e 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1776,6 +1776,30 @@ fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> Cl } } +/// Returns a public Reflection boolean method that always reports one literal value. +fn builtin_reflection_constant_bool_method(method_name: &str, value: bool) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new( + StmtKind::Return(if value { true_bool() } else { false_bool() }), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a `ReflectionMethod` predicate derived from its case-insensitive method name. fn builtin_reflection_method_name_predicate_method( method_name: &str, @@ -2097,6 +2121,7 @@ fn add_reflection_member_flag_methods( "hasDefaultValue", "__has_default_value", )); + methods.push(builtin_reflection_constant_bool_method("isDefault", true)); methods.push(builtin_reflection_class_mixed_method( "getDefaultValue", "__default_value", @@ -2407,7 +2432,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionProperty" { - for method_name in ["isfinal", "isabstract", "isreadonly"] { + for method_name in ["isfinal", "isabstract", "isreadonly", "isdefault"] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fc815bcdc9..3610d8aaf5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6761,6 +6761,7 @@ eval('class EvalReflectPropertyDefaultTarget { foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label"] as $name) { $property = new ReflectionProperty("EvalReflectPropertyDefaultTarget", $name); echo $property->getName() . ":"; + echo $property->isDefault() ? "Y:" : "N:"; echo $property->hasDefaultValue() ? "D:" : "d:"; $value = $property->getDefaultValue(); echo $value === null ? "null" : $value; @@ -6768,6 +6769,7 @@ foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label" } $listed = (new ReflectionClass("EvalReflectPropertyDefaultTarget"))->getProperty("implicit"); echo "listed:"; +echo $listed->isDefault() ? "Y:" : "N:"; echo $listed->hasDefaultValue() ? "D:" : "d:"; echo $listed->getDefaultValue() === null ? "null" : "bad";'); "#, @@ -6779,7 +6781,7 @@ echo $listed->getDefaultValue() === null ? "null" : "bad";'); ); assert_eq!( out.stdout, - "implicit:D:null|typed:d:null|nullableTyped:d:null|explicitNull:D:null|count:D:7|label:D:ok|listed:D:null" + "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|listed:Y:D:null" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 99589b8e0f..2ea97d73b2 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2241,36 +2241,43 @@ class ReflectPropertyDefaultTarget { } $implicit = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "implicit"); echo $implicit->getName() . ":"; +echo $implicit->isDefault() ? "Y:" : "N:"; echo $implicit->hasDefaultValue() ? "D:" : "d:"; echo $implicit->getDefaultValue() === null ? "null" : $implicit->getDefaultValue(); echo "|"; $typed = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "typed"); echo $typed->getName() . ":"; +echo $typed->isDefault() ? "Y:" : "N:"; echo $typed->hasDefaultValue() ? "D:" : "d:"; echo $typed->getDefaultValue() === null ? "null" : $typed->getDefaultValue(); echo "|"; $nullableTyped = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "nullableTyped"); echo $nullableTyped->getName() . ":"; +echo $nullableTyped->isDefault() ? "Y:" : "N:"; echo $nullableTyped->hasDefaultValue() ? "D:" : "d:"; echo $nullableTyped->getDefaultValue() === null ? "null" : $nullableTyped->getDefaultValue(); echo "|"; $explicitNull = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "explicitNull"); echo $explicitNull->getName() . ":"; +echo $explicitNull->isDefault() ? "Y:" : "N:"; echo $explicitNull->hasDefaultValue() ? "D:" : "d:"; echo $explicitNull->getDefaultValue() === null ? "null" : $explicitNull->getDefaultValue(); echo "|"; $count = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "count"); echo $count->getName() . ":"; +echo $count->isDefault() ? "Y:" : "N:"; echo $count->hasDefaultValue() ? "D:" : "d:"; echo $count->getDefaultValue() === null ? "null" : $count->getDefaultValue(); echo "|"; $label = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "label"); echo $label->getName() . ":"; +echo $label->isDefault() ? "Y:" : "N:"; echo $label->hasDefaultValue() ? "D:" : "d:"; echo $label->getDefaultValue() === null ? "null" : $label->getDefaultValue(); echo "|"; $listed = (new ReflectionClass(ReflectPropertyDefaultTarget::class))->getProperty("implicit"); echo "listed:"; +echo $listed->isDefault() ? "Y:" : "N:"; echo $listed->hasDefaultValue() ? "D:" : "d:"; echo $listed->getDefaultValue() === null ? "null" : "bad"; "#, @@ -2282,7 +2289,7 @@ echo $listed->getDefaultValue() === null ? "null" : "bad"; ); assert_eq!( out.stdout, - "implicit:D:null|typed:d:null|nullableTyped:d:null|explicitNull:D:null|count:D:7|label:D:ok|listed:D:null" + "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|listed:Y:D:null" ); } From 9ad3d87e765affbef8868b090d2a115071e0ecdb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 18:06:06 +0200 Subject: [PATCH 0432/1208] Expose ReflectionClass isSubclassOf --- .../elephc-eval/src/interpreter/reflection.rs | 99 ++++++++++- .../elephc-eval/src/interpreter/statements.rs | 9 + .../tests/builtins_class_metadata.rs | 39 +++++ .../interpreter/tests/support/object_ops.rs | 3 + docs/php/classes.md | 3 +- docs/php/eval.md | 4 + src/codegen/lower_inst/objects/reflection.rs | 36 ++++ src/codegen_support/runtime/eval_bridge.rs | 52 +++++- src/types/checker/builtin_types/reflection.rs | 155 ++++++++++++++++++ tests/codegen/eval.rs | 64 ++++++++ tests/codegen/oop/attributes.rs | 39 +++++ 11 files changed, 499 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 1dfa48749e..3d4da65b96 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -199,6 +199,48 @@ pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( .map(Some) } +/// Handles eval-backed `ReflectionClass::isSubclassOf()` calls. +pub(in crate::interpreter) fn eval_reflection_class_is_subclass_of_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isSubclassOf") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("class")], evaluated_args)?; + let target_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_class_like_exists(&target_name, context) + && !values.class_exists(&target_name)? + && !values.interface_exists(&target_name)? + && !values.trait_exists(&target_name)? + && !values.enum_exists(&target_name)? + { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", target_name), + context, + values, + ); + } + let result = if eval_reflection_class_like_exists(&reflected_name, context) { + eval_reflection_class_is_subclass_of_name(&reflected_name, &target_name, context) + } else { + let reflected_class = values.string(&reflected_name)?; + let result = values.object_is_a(reflected_class, &target_name, true)?; + values.release(reflected_class)?; + result + }; + values.bool_value(result).map(Some) +} + /// Handles eval-backed `ReflectionClass::hasConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( identity: u64, @@ -501,7 +543,44 @@ fn eval_reflection_class_new( let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; let class_name = eval_reflection_string_arg(args[0], values)?; let Some(metadata) = eval_reflection_class_like_attributes(&class_name, context) else { - return Ok(None); + let is_class = values.class_exists(&class_name)?; + let is_interface = values.interface_exists(&class_name)?; + let is_trait = values.trait_exists(&class_name)?; + let is_enum = values.enum_exists(&class_name)?; + if !(is_class || is_interface || is_trait || is_enum) { + return Ok(None); + } + let mut flags = 0; + if is_interface { + flags |= EVAL_REFLECTION_CLASS_FLAG_INTERFACE; + } + if is_trait { + flags |= EVAL_REFLECTION_CLASS_FLAG_TRAIT; + } + if is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM; + } + return eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS, + class_name.trim_start_matches('\\'), + &[], + &[], + &[], + &[], + &[], + None, + &[], + None, + None, + flags, + if is_enum { 32 } else { 0 }, + 0, + None, + None, + context, + values, + ) + .map(Some); }; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, @@ -1673,6 +1752,24 @@ fn eval_reflection_class_implements_interface_name( false } +/// Returns true when reflected eval metadata is a subclass or subinterface of a target. +fn eval_reflection_class_is_subclass_of_name( + reflected_name: &str, + target_name: &str, + context: &ElephcEvalContext, +) -> bool { + if context.has_interface(reflected_name) { + return context + .interface_parent_names(reflected_name) + .iter() + .any(|parent| eval_reflection_same_class_like_name(parent, target_name)); + } + if context.has_class(reflected_name) || context.has_enum(reflected_name) { + return context.class_is_a(reflected_name, target_name, true); + } + false +} + /// Returns true when two PHP class-like names match case-insensitively. fn eval_reflection_same_class_like_name(left: &str, right: &str) -> bool { left.trim_start_matches('\\') diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 65dc7d1d05..1b9ca839f3 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2317,6 +2317,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_is_subclass_of_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_has_constant_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 853c6dab26..ca7088f7b8 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -336,6 +336,45 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::isSubclassOf reports eval parent/interface metadata. +#[test] +fn execute_program_reflection_class_is_subclass_of_predicate() { + let program = parse_fragment( + br#"interface EvalSubclassIface {} +interface EvalSubclassChildIface extends EvalSubclassIface {} +class EvalSubclassBase {} +class EvalSubclassParent extends EvalSubclassBase {} +class EvalSubclassChild extends EvalSubclassParent implements EvalSubclassChildIface {} +trait EvalSubclassTrait {} +enum EvalSubclassEnum implements EvalSubclassIface { case Ready; } +$ref = new ReflectionClass("EvalSubclassChild"); +echo $ref->isSubclassOf("EvalSubclassParent") ? "P" : "p"; +echo $ref->isSubclassOf("evalsubclassbase") ? "B" : "b"; +echo $ref->isSubclassOf("EvalSubclassIface") ? "I" : "i"; +echo $ref->isSubclassOf("EvalSubclassChild") ? "S" : "s"; +echo (new ReflectionClass("EvalSubclassChildIface"))->isSubclassOf("EvalSubclassIface") ? "J" : "j"; +echo (new ReflectionClass("EvalSubclassIface"))->isSubclassOf("EvalSubclassIface") ? "X" : "x"; +echo $ref->isSubclassOf("EvalSubclassTrait") ? "T" : "t"; +echo $ref->isSubclassOf("EvalSubclassEnum") ? "Q" : "q"; +echo (new ReflectionClass("EvalSubclassEnum"))->isSubclassOf("EvalSubclassIface") ? "E" : "e"; +try { + $ref->isSubclassOf("EvalSubclassMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":missing"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "PBIsJxtqE:missing"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class-like final and abstract flags. #[test] fn execute_program_reflects_eval_class_modifier_flags() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 7d51edac32..833b7d7445 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -857,6 +857,9 @@ impl FakeOps { Ok(!exclude_self) } FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), + FakeValue::String(name) => { + Ok(fake_runtime_object_is_a(&name, target_class, exclude_self)) + } _ => Ok(false), } } diff --git a/docs/php/classes.md b/docs/php/classes.md index a4fdf4288d..878f0a4393 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1165,6 +1165,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getReflectionConstant()` | `new ReflectionClass($class_name)` | Return a `ReflectionClassConstant` object for the named visible constant or enum case, or `false` when no such constant is visible | | `ReflectionClass::getReflectionConstants()` | `new ReflectionClass($class_name)` | Return indexed `ReflectionClassConstant` objects for visible class constants and enum cases | | `ReflectionClass::implementsInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol implements, extends, or is the requested interface; interface lookup is case-insensitive and invalid names throw `ReflectionException` | +| `ReflectionClass::isSubclassOf()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol extends the requested class or implements/extends the requested interface; self is excluded, trait/enum targets return `false`, and missing names throw `ReflectionException` | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | @@ -1304,7 +1305,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index d4a8cbe6e7..93f54559d4 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -176,6 +176,10 @@ case-insensitively and returns true when reflecting the requested interface itself. It throws catchable `ReflectionException` values when the argument names a class, trait, enum, or missing interface, while `ReflectionClass::getTraitNames()` returns traits used directly by eval classes. +`ReflectionClass::isSubclassOf()` checks eval parent-class chains and +implemented or extended interfaces case-insensitively. It excludes the reflected +symbol itself, returns `false` for trait and enum targets, and throws a +catchable `ReflectionException` when the target name is missing. `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 6f3a5103a6..19b902b1b2 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -36,6 +36,7 @@ struct ReflectionOwnerMetadata { attr_args: Vec>>, interface_names: Vec, trait_names: Vec, + parent_names: Vec, method_names: Vec, property_names: Vec, constant_names: Vec, @@ -273,6 +274,11 @@ fn emit_reflection_owner_object( "__trait_names", &metadata.trait_names, )?; + emit_reflection_string_array_property_by_name( + ctx, + "__parent_names", + &metadata.parent_names, + )?; emit_reflection_string_array_property_by_name( ctx, "__method_names", @@ -505,6 +511,7 @@ fn reflection_class_metadata_for_name( attr_args: info.attribute_args.clone(), interface_names: info.interfaces.clone(), trait_names: info.used_traits.clone(), + parent_names: reflection_parent_class_names(ctx, info), method_names, property_names, constant_names, @@ -558,6 +565,7 @@ fn reflection_class_metadata_for_name( attr_args: Vec::new(), interface_names: reflection_interface_parent_names(ctx, interface_name), trait_names: Vec::new(), + parent_names: Vec::new(), method_names, property_names, constant_names, @@ -612,6 +620,7 @@ fn reflection_class_metadata_for_name( attr_args: Vec::new(), interface_names: Vec::new(), trait_names, + parent_names: Vec::new(), method_names, property_names, constant_names, @@ -745,6 +754,7 @@ fn reflection_method_owner_metadata( attr_args: member.attr_args, interface_names: Vec::new(), trait_names: Vec::new(), + parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), @@ -796,6 +806,7 @@ fn reflection_property_metadata( attr_args: info.property_attribute_args.get(&property_name)?.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), @@ -970,6 +981,7 @@ fn reflection_class_constant_metadata( attr_args: case.attribute_args.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), @@ -1029,6 +1041,7 @@ fn reflection_enum_case_metadata( attr_args: case.attribute_args.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), @@ -1076,6 +1089,7 @@ fn reflection_class_constant_owner_metadata( attr_args: metadata.attr_args, interface_names: Vec::new(), trait_names: Vec::new(), + parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), @@ -1289,6 +1303,27 @@ fn reflection_parent_class_name( .or_else(|| Some(parent.trim_start_matches('\\').to_string())) } +/// Returns direct and inherited parent class names for `ReflectionClass::isSubclassOf()`. +fn reflection_parent_class_names( + ctx: &FunctionContext<'_>, + info: &crate::types::ClassInfo, +) -> Vec { + let mut names: Vec = Vec::new(); + let mut current = reflection_parent_class_name(ctx, info); + while let Some(parent_name) = current { + if names + .iter() + .any(|name| php_symbol_key(name) == php_symbol_key(&parent_name)) + { + break; + } + current = resolve_reflection_class(ctx, &parent_name) + .and_then(|(_, parent_info)| reflection_parent_class_name(ctx, parent_info)); + names.push(parent_name); + } + names +} + /// Returns PHP's `ReflectionClass::isInstantiable()` value for static class metadata. fn reflection_class_is_instantiable( info: &crate::types::ClassInfo, @@ -2823,6 +2858,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { attr_args: Vec::new(), interface_names: Vec::new(), trait_names: Vec::new(), + parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), constant_names: Vec::new(), diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 992778a749..c965d2937f 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -183,7 +183,31 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("ldr x0, [sp, #0]"); // reload the boxed eval value for unboxing emitter.instruction("bl __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and payload words emitter.instruction("cmp x0, #6"); // runtime tag 6 means the eval value is an object - emitter.instruction("b.ne __elephc_eval_value_is_a_false"); // non-object values do not satisfy class relations + emitter.instruction("b.eq __elephc_eval_value_is_a_object"); // object values can use their concrete runtime class id + emitter.instruction("cmp x0, #1"); // runtime tag 1 means the eval value is a class string + emitter.instruction("b.eq __elephc_eval_value_is_a_string"); // class-string values need source metadata lookup + emitter.instruction("b __elephc_eval_value_is_a_false"); // other runtime tags cannot satisfy class relations + emitter.label("__elephc_eval_value_is_a_string"); + emitter.instruction("bl __rt_instanceof_lookup"); // resolve the source class string to matcher metadata + emitter.instruction("cmp x0, #0"); // did the source string resolve to emitted metadata? + emitter.instruction("b.eq __elephc_eval_value_is_a_false"); // unresolved source strings cannot match relation metadata + emitter.instruction("cmp x2, #0"); // source strings must resolve to concrete classes for this matcher + emitter.instruction("b.ne __elephc_eval_value_is_a_false"); // interface-source strings need a dedicated interface-parent matcher + emitter.instruction("str x1, [sp, #32]"); // build a fake object header containing the source class id + emitter.instruction("ldr x10, [sp, #8]"); // reload the exact-self exclusion flag + emitter.instruction("cbz x10, __elephc_eval_value_is_a_string_match"); // is_a() allows exact class-string matches + emitter.instruction("ldr x11, [sp, #24]"); // reload target kind before exact-class filtering + emitter.instruction("cbnz x11, __elephc_eval_value_is_a_string_match"); // interface targets cannot be exact concrete-class self matches + emitter.instruction("ldr x13, [sp, #16]"); // reload the target concrete class id + emitter.instruction("cmp x1, x13"); // compare source and target class ids for subclass self exclusion + emitter.instruction("b.eq __elephc_eval_value_is_a_false"); // is_subclass_of() excludes the exact class string + emitter.label("__elephc_eval_value_is_a_string_match"); + emitter.instruction("add x0, sp, #32"); // pass the fake object header to the metadata matcher + emitter.instruction("ldr x1, [sp, #16]"); // pass the target class/interface id + emitter.instruction("ldr x2, [sp, #24]"); // pass the target kind: 0 class, 1 interface + emitter.instruction("bl __rt_exception_matches"); // test class-string inheritance or implemented interfaces + emitter.instruction("b __elephc_eval_value_is_a_done"); // keep the matcher result and restore the wrapper frame + emitter.label("__elephc_eval_value_is_a_object"); emitter.instruction("mov x9, x1"); // keep the unboxed object pointer for matcher input emitter.instruction("cbz x9, __elephc_eval_value_is_a_false"); // malformed object payloads cannot match class metadata emitter.instruction("ldr x10, [sp, #8]"); // reload the exact-self exclusion flag @@ -1573,7 +1597,31 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the boxed eval value for unboxing emitter.instruction("call __rt_mixed_unbox"); // unwrap nested Mixed cells to tag and payload words emitter.instruction("cmp rax, 6"); // runtime tag 6 means the eval value is an object - emitter.instruction("jne __elephc_eval_value_is_a_false_x86"); // non-object values do not satisfy class relations + emitter.instruction("je __elephc_eval_value_is_a_object_x86"); // object values can use their concrete runtime class id + emitter.instruction("cmp rax, 1"); // runtime tag 1 means the eval value is a class string + emitter.instruction("je __elephc_eval_value_is_a_string_x86"); // class-string values need source metadata lookup + emitter.instruction("jmp __elephc_eval_value_is_a_false_x86"); // other runtime tags cannot satisfy class relations + emitter.label("__elephc_eval_value_is_a_string_x86"); + emitter.instruction("mov rax, rdi"); // pass the source class-string pointer to the metadata lookup + emitter.instruction("call __rt_instanceof_lookup"); // resolve the source class string to matcher metadata + emitter.instruction("test rax, rax"); // did the source string resolve to emitted metadata? + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // unresolved source strings cannot match relation metadata + emitter.instruction("test rdx, rdx"); // source strings must resolve to concrete classes for this matcher + emitter.instruction("jne __elephc_eval_value_is_a_false_x86"); // interface-source strings need a dedicated interface-parent matcher + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // build a fake object header containing the source class id + emitter.instruction("cmp QWORD PTR [rbp - 16], 0"); // does this call reject exact concrete-class matches? + emitter.instruction("je __elephc_eval_value_is_a_string_match_x86"); // is_a() allows exact class-string matches + emitter.instruction("cmp QWORD PTR [rbp - 32], 0"); // is the target a concrete class rather than an interface? + emitter.instruction("jne __elephc_eval_value_is_a_string_match_x86"); // interface targets cannot be exact concrete-class self matches + emitter.instruction("cmp rdi, QWORD PTR [rbp - 24]"); // compare source and target class ids for subclass self exclusion + emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // is_subclass_of() excludes the exact class string + emitter.label("__elephc_eval_value_is_a_string_match_x86"); + emitter.instruction("lea rdi, [rbp - 40]"); // pass the fake object header to the metadata matcher + emitter.instruction("mov rsi, QWORD PTR [rbp - 24]"); // pass the target class/interface id + emitter.instruction("mov rdx, QWORD PTR [rbp - 32]"); // pass the target kind: 0 class, 1 interface + emitter.instruction("call __rt_exception_matches"); // test class-string inheritance or implemented interfaces + emitter.instruction("jmp __elephc_eval_value_is_a_done_x86"); // keep the matcher result and restore the wrapper frame + emitter.label("__elephc_eval_value_is_a_object_x86"); emitter.instruction("test rdi, rdi"); // check the unboxed object pointer before reading its header emitter.instruction("je __elephc_eval_value_is_a_false_x86"); // malformed object payloads cannot match class metadata emitter.instruction("mov r8, rdi"); // keep the unboxed object pointer for matcher input diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index d31d3bb13e..5ab65a4036 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -915,6 +915,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(string_array_type()), empty_array(), ), + builtin_property( + "__parent_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), builtin_property( "__method_names", Visibility::Private, @@ -1011,6 +1017,7 @@ fn builtin_reflection_class() -> FlattenedClass { ), builtin_reflection_class_get_reflection_constant_method(), builtin_reflection_class_implements_interface_method(), + builtin_reflection_class_is_subclass_of_method(), builtin_reflection_class_array_method( "getMethods", "__methods", @@ -1438,6 +1445,153 @@ fn builtin_reflection_class_implements_interface_method() -> ClassMethod { } } +/// Returns `ReflectionClass::isSubclassOf()` backed by parent and interface metadata. +fn builtin_reflection_class_is_subclass_of_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let class_var = variable_expr("class", dummy_span); + let target_var = variable_expr("target", dummy_span); + let parent_name_var = variable_expr("parentName", dummy_span); + let interface_name_var = variable_expr("interfaceName", dummy_span); + let target_missing = binary_expr( + binary_expr( + Expr::new( + ExprKind::Not(Box::new(function_call( + "class_exists", + vec![class_var.clone()], + dummy_span, + ))), + dummy_span, + ), + BinOp::And, + Expr::new( + ExprKind::Not(Box::new(function_call( + "interface_exists", + vec![class_var.clone()], + dummy_span, + ))), + dummy_span, + ), + dummy_span, + ), + BinOp::And, + binary_expr( + Expr::new( + ExprKind::Not(Box::new(function_call( + "trait_exists", + vec![class_var.clone()], + dummy_span, + ))), + dummy_span, + ), + BinOp::And, + Expr::new( + ExprKind::Not(Box::new(function_call( + "enum_exists", + vec![class_var.clone()], + dummy_span, + ))), + dummy_span, + ), + dummy_span, + ), + dummy_span, + ); + let missing_target_check = Stmt::new( + StmtKind::If { + condition: target_missing, + then_body: vec![throw_new_reflection_exception( + concat_expr( + concat_expr( + string_lit("Class \"", dummy_span), + class_var.clone(), + dummy_span, + ), + string_lit("\" does not exist", dummy_span), + dummy_span, + ), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ); + let parent_matches = binary_expr( + strtolower_call(parent_name_var, dummy_span), + BinOp::Eq, + target_var.clone(), + dummy_span, + ); + let interface_matches = binary_expr( + strtolower_call(interface_name_var, dummy_span), + BinOp::Eq, + target_var.clone(), + dummy_span, + ); + ClassMethod { + name: "isSubclassOf".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("class".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![ + missing_target_check, + Stmt::new( + StmtKind::Assign { + name: "target".to_string(), + value: strtolower_call(class_var, dummy_span), + }, + dummy_span, + ), + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__parent_names", dummy_span), + key_var: None, + value_var: "parentName".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: parent_matches, + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__interface_names", dummy_span), + key_var: None, + value_var: "interfaceName".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: interface_matches, + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new(StmtKind::Return(false_bool()), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds `if (($interface)) throw new ReflectionException($message);`. fn throw_if_class_like_exists( predicate_name: &str, @@ -2355,6 +2509,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "hasmethod", "hasproperty", "implementsinterface", + "issubclassof", ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3610d8aaf5..768e4e0448 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5930,6 +5930,70 @@ try { ); } +/// Verifies eval ReflectionClass::isSubclassOf reports parent and interface +/// metadata through the linked eval bridge. +#[test] +fn test_eval_reflection_class_is_subclass_of_predicate() { + let out = compile_and_run_capture( + r#"isSubclassOf("EvalSubclassParent") ? "P" : "p"; +echo $ref->isSubclassOf("evalsubclassbase") ? "B" : "b"; +echo $ref->isSubclassOf("EvalSubclassIface") ? "I" : "i"; +echo $ref->isSubclassOf("EvalSubclassChild") ? "S" : "s"; +echo (new ReflectionClass("EvalSubclassChildIface"))->isSubclassOf("EvalSubclassIface") ? "J" : "j"; +echo (new ReflectionClass("EvalSubclassIface"))->isSubclassOf("EvalSubclassIface") ? "X" : "x"; +echo $ref->isSubclassOf("EvalSubclassTrait") ? "T" : "t"; +echo $ref->isSubclassOf("EvalSubclassEnum") ? "Q" : "q"; +echo (new ReflectionClass("EvalSubclassEnum"))->isSubclassOf("EvalSubclassIface") ? "E" : "e"; +try { + $ref->isSubclassOf("EvalSubclassMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":missing"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PBIsJxtqE:missing"); +} + +/// Verifies eval ReflectionClass::isSubclassOf can query generated AOT class +/// relations when the reflected class was declared outside the eval fragment. +#[test] +fn test_eval_reflection_class_is_subclass_of_aot_class() { + let out = compile_and_run_capture( + r#"isSubclassOf("EvalAotSubclassParent") ? "P" : "p"; +echo $child->isSubclassOf("EvalAotSubclassChild") ? "S" : "s"; +$impl = new ReflectionClass("EvalAotSubclassImpl"); +echo $impl->isSubclassOf("EvalAotSubclassIface") ? "I" : "i";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PsI"); +} + /// Verifies eval ReflectionClass::getParentClass crosses the generated runtime bridge. #[test] fn test_eval_reflection_class_get_parent_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 2ea97d73b2..6d75c25430 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1555,6 +1555,45 @@ try { ); } +/// Verifies that `ReflectionClass::isSubclassOf()` reports parent classes and +/// inherited interfaces while excluding self and accepting trait/enum targets as false. +#[test] +fn test_reflection_class_is_subclass_of() { + let out = compile_and_run_capture( + r#"isSubclassOf(StaticSubclassParent::class) ? "P" : "p"; +echo $ref->isSubclassOf("staticsubclassbase") ? "B" : "b"; +echo $ref->isSubclassOf(StaticSubclassIface::class) ? "I" : "i"; +echo $ref->isSubclassOf(StaticSubclassChild::class) ? "S" : "s"; +echo (new ReflectionClass(StaticSubclassChildIface::class))->isSubclassOf(StaticSubclassIface::class) ? "J" : "j"; +echo (new ReflectionClass(StaticSubclassIface::class))->isSubclassOf(StaticSubclassIface::class) ? "X" : "x"; +echo $ref->isSubclassOf(StaticSubclassTrait::class) ? "T" : "t"; +echo $ref->isSubclassOf(StaticSubclassEnum::class) ? "Q" : "q"; +echo (new ReflectionClass(StaticSubclassEnum::class))->isSubclassOf(StaticSubclassIface::class) ? "E" : "e"; +try { + $ref->isSubclassOf("StaticSubclassMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":missing"; +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PBIsJxtqE:missing"); +} + /// Verifies that `ReflectionClass::getParentClass()` returns a ReflectionClass /// object for subclasses and `false` for parentless classes. #[test] From 7fd17eefaa7d637d987b29facdfd8acb5e1a2b7d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 18:20:31 +0200 Subject: [PATCH 0433/1208] Expose ReflectionClass isInstance --- .../elephc-eval/src/interpreter/reflection.rs | 27 +++++++++ .../elephc-eval/src/interpreter/statements.rs | 9 +++ .../tests/builtins_class_metadata.rs | 33 +++++++++++ docs/php/classes.md | 3 +- docs/php/eval.md | 3 + docs/php/types.md | 2 +- src/ir_lower/context.rs | 1 + src/types/checker/builtin_types/reflection.rs | 43 +++++++++++++- src/types/checker/type_compat/pointers.rs | 1 + src/types/checker/type_compat/unions.rs | 3 +- tests/codegen/eval.rs | 59 +++++++++++++++++++ tests/codegen/oop/attributes.rs | 33 +++++++++++ tests/codegen/oop/misc.rs | 16 +++++ 13 files changed, 228 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 3d4da65b96..b3e2cd68bb 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -241,6 +241,33 @@ pub(in crate::interpreter) fn eval_reflection_class_is_subclass_of_result( values.bool_value(result).map(Some) } +/// Handles eval-backed `ReflectionClass::isInstance()` calls. +pub(in crate::interpreter) fn eval_reflection_class_is_instance_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isInstance") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + let object = args[0]; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let result = dynamic_object_is_a(object, &reflected_name, false, context, values)? + .map_or_else(|| values.object_is_a(object, &reflected_name, false), Ok)?; + values.bool_value(result).map(Some) +} + /// Handles eval-backed `ReflectionClass::hasConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( identity: u64, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 1b9ca839f3..33b0a3a480 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2326,6 +2326,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_is_instance_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_has_constant_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index ca7088f7b8..6305446ee2 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -375,6 +375,39 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::isInstance reports eval object class/interface metadata. +#[test] +fn execute_program_reflection_class_is_instance_predicate() { + let program = parse_fragment( + br#"interface EvalInstanceIface {} +class EvalInstanceBase {} +class EvalInstanceChild extends EvalInstanceBase implements EvalInstanceIface {} +trait EvalInstanceTrait {} +enum EvalInstanceEnum implements EvalInstanceIface { case Ready; } +$base = new ReflectionClass("EvalInstanceBase"); +$child = new ReflectionClass("EvalInstanceChild"); +$iface = new ReflectionClass("EvalInstanceIface"); +$trait = new ReflectionClass("EvalInstanceTrait"); +$enum = new ReflectionClass("EvalInstanceEnum"); +$childObj = new EvalInstanceChild(); +echo $base->isInstance($childObj) ? "B" : "b"; +echo $child->isInstance(new EvalInstanceBase()) ? "C" : "c"; +echo $iface->isInstance($childObj) ? "I" : "i"; +echo $trait->isInstance($childObj) ? "T" : "t"; +echo $enum->isInstance(EvalInstanceEnum::Ready) ? "E" : "e"; +echo $iface->isInstance(EvalInstanceEnum::Ready) ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "BcItEN"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class-like final and abstract flags. #[test] fn execute_program_reflects_eval_class_modifier_flags() { diff --git a/docs/php/classes.md b/docs/php/classes.md index 878f0a4393..bf1572d960 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1166,6 +1166,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getReflectionConstants()` | `new ReflectionClass($class_name)` | Return indexed `ReflectionClassConstant` objects for visible class constants and enum cases | | `ReflectionClass::implementsInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol implements, extends, or is the requested interface; interface lookup is case-insensitive and invalid names throw `ReflectionException` | | `ReflectionClass::isSubclassOf()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol extends the requested class or implements/extends the requested interface; self is excluded, trait/enum targets return `false`, and missing names throw `ReflectionException` | +| `ReflectionClass::isInstance()` | `new ReflectionClass($class_name)` | Return whether an object is an instance of the reflected class-like symbol, including parent-class, interface, and enum-case relations; trait targets return `false` | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | | `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | @@ -1305,7 +1306,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index 93f54559d4..b91c64f9d7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -180,6 +180,9 @@ returns traits used directly by eval classes. implemented or extended interfaces case-insensitively. It excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws a catchable `ReflectionException` when the target name is missing. +`ReflectionClass::isInstance()` checks eval-created or generated/AOT objects +against the reflected class-like metadata, including parent, interface, and enum +relations; trait targets return `false`. `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. diff --git a/docs/php/types.md b/docs/php/types.md index 5d8074a72f..a9d186486d 100644 --- a/docs/php/types.md +++ b/docs/php/types.md @@ -20,7 +20,7 @@ sidebar: | `iterable` | Yes | PHP pseudo-type for `array \| Traversable`. Supports indexed arrays, associative arrays, `Iterator`, and `IteratorAggregate`; runtime operations (`foreach`, `echo`, `gettype()`, `var_dump()`, `===`, casts, `is_iterable()`) dispatch on heap-kind, value-type, or interface metadata as needed. | | `resource` | Inferred only | File handles and standard streams are modeled separately from integers. `fopen()` returns `resource\|false`, and `STDIN`, `STDOUT`, and `STDERR` are stream resources. PHP does not allow `resource` as a type declaration, so elephc does not accept `resource` annotations. | | `callable` | Yes | Closures, arrow functions, first-class callables, and FFI callback parameters. | -| `object` | Yes | Class instances. Heap-allocated, fixed-layout. `new ClassName(...)` | +| `object` | Yes | Class instances. Heap-allocated, fixed-layout. `new ClassName(...)`; the generic `object` type hint accepts any class, interface, or enum object. | | `enum` | Yes | Pure and backed enums. Cases are singletons. Backed enums support `->value`, `::from()`, `::tryFrom()`, `::cases()`. | | `int|string` | Yes | Union type — variable accepts any of the listed types. Lowered to Mixed at runtime. | | `?int` | Yes | Nullable shorthand — sugar for `int|null`. The explicit `T|null` form (e.g. `A|null`) is also accepted. | diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 846b3a9bb7..fa66eb4f1f 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1581,6 +1581,7 @@ fn named_type_expr_to_php_type(name: &str) -> PhpType { "array" => PhpType::Array(Box::new(PhpType::Mixed)), "callable" => PhpType::Callable, "mixed" => PhpType::Mixed, + "object" => PhpType::Object(String::new()), _ => PhpType::Object(name.to_string()), } } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 5ab65a4036..39c69add2a 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -16,8 +16,8 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::names::Name; use crate::parser::ast::{ - BinOp, ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, Stmt, StmtKind, TypeExpr, - Visibility, + BinOp, ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, InstanceOfTarget, Stmt, StmtKind, + TypeExpr, Visibility, }; use crate::types::traits::FlattenedClass; use crate::types::PhpType; @@ -264,6 +264,11 @@ fn nullable_object_type(class_name: &str) -> TypeExpr { TypeExpr::Nullable(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) } +/// Returns a `TypeExpr` for PHP's generic `object` type. +fn object_type() -> TypeExpr { + TypeExpr::Named(Name::unqualified("object")) +} + /// Returns a `TypeExpr` for the unqualified name `mixed`. fn mixed_type() -> TypeExpr { TypeExpr::Named(crate::names::Name::unqualified("mixed")) @@ -1018,6 +1023,7 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_get_reflection_constant_method(), builtin_reflection_class_implements_interface_method(), builtin_reflection_class_is_subclass_of_method(), + builtin_reflection_class_is_instance_method(), builtin_reflection_class_array_method( "getMethods", "__methods", @@ -1592,6 +1598,38 @@ fn builtin_reflection_class_is_subclass_of_method() -> ClassMethod { } } +/// Returns `ReflectionClass::isInstance()` backed by PHP's class relation predicate. +fn builtin_reflection_class_is_instance_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "isInstance".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(object_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::InstanceOf { + value: Box::new(variable_expr("object", dummy_span)), + target: InstanceOfTarget::Expr(Box::new(reflection_this_property( + "__name", dummy_span, + ))), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds `if (($interface)) throw new ReflectionException($message);`. fn throw_if_class_like_exists( predicate_name: &str, @@ -2510,6 +2548,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "hasproperty", "implementsinterface", "issubclassof", + "isinstance", ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; diff --git a/src/types/checker/type_compat/pointers.rs b/src/types/checker/type_compat/pointers.rs index be6af3547c..afdd815fdb 100644 --- a/src/types/checker/type_compat/pointers.rs +++ b/src/types/checker/type_compat/pointers.rs @@ -117,6 +117,7 @@ impl Checker { "string" => Ok(PhpType::Str), "mixed" => Ok(PhpType::Mixed), "callable" => Ok(PhpType::Callable), + "object" => Ok(PhpType::Object(String::new())), "void" => Ok(PhpType::Void), "array" => Ok(PhpType::Array(Box::new(PhpType::Mixed))), // Relative class types only survive to this point when used outside a class diff --git a/src/types/checker/type_compat/unions.rs b/src/types/checker/type_compat/unions.rs index f7f0c66c62..5261a6cabc 100644 --- a/src/types/checker/type_compat/unions.rs +++ b/src/types/checker/type_compat/unions.rs @@ -105,7 +105,8 @@ impl Checker { }, PhpType::Object(expected_name) => match actual { PhpType::Object(actual_name) => { - expected_name == actual_name + expected_name.is_empty() + || expected_name == actual_name || self.is_subclass_of(actual_name, expected_name) || self.class_implements_interface(actual_name, expected_name) || self.interface_extends_interface(actual_name, expected_name) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 768e4e0448..bf2c426b85 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5994,6 +5994,65 @@ echo $impl->isSubclassOf("EvalAotSubclassIface") ? "I" : "i";'); assert_eq!(out.stdout, "PsI"); } +/// Verifies eval ReflectionClass::isInstance reports eval-declared object +/// relations through the linked eval bridge. +#[test] +fn test_eval_reflection_class_is_instance_predicate() { + let out = compile_and_run_capture( + r#"isInstance($childObj) ? "B" : "b"; +echo $child->isInstance(new EvalInstanceBase()) ? "C" : "c"; +echo $iface->isInstance($childObj) ? "I" : "i"; +echo $trait->isInstance($childObj) ? "T" : "t"; +echo $enum->isInstance(EvalInstanceEnum::Ready) ? "E" : "e"; +echo $iface->isInstance(EvalInstanceEnum::Ready) ? "N" : "n";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "BcItEN"); +} + +/// Verifies eval ReflectionClass::isInstance can query generated AOT object +/// relations when the reflected class was declared outside the eval fragment. +#[test] +fn test_eval_reflection_class_is_instance_aot_class() { + let out = compile_and_run_capture( + r#"isInstance(new EvalAotInstanceChild()) ? "P" : "p"; +$child = new ReflectionClass("EvalAotInstanceChild"); +echo $child->isInstance(new EvalAotInstanceParent()) ? "C" : "c"; +$iface = new ReflectionClass("EvalAotInstanceIface"); +echo $iface->isInstance(new EvalAotInstanceImpl()) ? "I" : "i";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PcI"); +} + /// Verifies eval ReflectionClass::getParentClass crosses the generated runtime bridge. #[test] fn test_eval_reflection_class_get_parent_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 6d75c25430..7d35a8b1c1 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1594,6 +1594,39 @@ try { assert_eq!(out.stdout, "PBIsJxtqE:missing"); } +/// Verifies that `ReflectionClass::isInstance()` reports class, interface, +/// trait, and enum instance relations using runtime object metadata. +#[test] +fn test_reflection_class_is_instance() { + let out = compile_and_run_capture( + r#"isInstance($childObj) ? "B" : "b"; +echo $child->isInstance(new StaticInstanceBase()) ? "C" : "c"; +echo $iface->isInstance($childObj) ? "I" : "i"; +echo $trait->isInstance($childObj) ? "T" : "t"; +echo $enum->isInstance(StaticInstanceEnum::Ready) ? "E" : "e"; +echo $iface->isInstance(StaticInstanceEnum::Ready) ? "N" : "n"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "BcItEN"); +} + /// Verifies that `ReflectionClass::getParentClass()` returns a ReflectionClass /// object for subclasses and `false` for parentless classes. #[test] diff --git a/tests/codegen/oop/misc.rs b/tests/codegen/oop/misc.rs index 32b46055ca..57efc30708 100644 --- a/tests/codegen/oop/misc.rs +++ b/tests/codegen/oop/misc.rs @@ -9,6 +9,22 @@ use super::*; +/// Verifies PHP's generic `object` parameter type accepts concrete objects and +/// preserves object-shaped ABI lowering. +#[test] +fn test_generic_object_parameter_type_accepts_concrete_object() { + let out = compile_and_run( + r#" Date: Fri, 19 Jun 2026 18:35:17 +0200 Subject: [PATCH 0434/1208] Expose ReflectionClass default properties --- .../elephc-eval/src/interpreter/reflection.rs | 51 ++++++++ .../elephc-eval/src/interpreter/statements.rs | 15 ++- .../tests/builtins_class_metadata.rs | 40 +++++++ docs/php/classes.md | 3 +- docs/php/eval.md | 9 +- src/codegen/lower_inst/objects/reflection.rs | 110 ++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 11 +- tests/codegen/eval.rs | 39 +++++++ tests/codegen/oop/attributes.rs | 51 ++++++++ 9 files changed, 320 insertions(+), 9 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index b3e2cd68bb..013dedf54f 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -356,6 +356,43 @@ pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( Ok(Some(result)) } +/// Handles eval-backed `ReflectionClass::getDefaultProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_default_properties_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getDefaultProperties") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let property_names = eval_reflection_default_property_names(&reflected_name, context); + let mut result = values.assoc_new(property_names.len())?; + for name in property_names { + let Some(member) = eval_reflection_property_metadata(&reflected_name, &name, context) + else { + continue; + }; + let Some(default) = member.default_value.as_ref() else { + continue; + }; + let key = values.string(&name)?; + let value = eval_method_parameter_default(default, context, values)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + /// Handles eval-backed `ReflectionClass::getReflectionConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_result( identity: u64, @@ -2090,6 +2127,20 @@ fn eval_reflection_property_metadata( }) } +/// Returns property names that can contribute to `ReflectionClass::getDefaultProperties()`. +fn eval_reflection_default_property_names( + reflected_name: &str, + context: &ElephcEvalContext, +) -> Vec { + if context.has_trait(reflected_name) { + return context.trait_property_names(reflected_name); + } + if context.has_interface(reflected_name) { + return context.interface_property_names(reflected_name); + } + context.class_property_names(reflected_name) +} + /// Returns ReflectionProperty default metadata for concrete eval properties. fn eval_reflection_property_default_value(property: &EvalClassProperty) -> Option { if let Some(default) = property.default() { diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 33b0a3a480..6dd52167b9 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1082,7 +1082,8 @@ fn validate_interface_constant_parent_redeclarations( ) -> Result<(), EvalStatus> { for constant in interface.constants() { for parent in interface.parents() { - if let Some((_, parent_constant)) = context.interface_constant(parent, constant.name()) { + if let Some((_, parent_constant)) = context.interface_constant(parent, constant.name()) + { if parent_constant.is_final() { return Err(EvalStatus::RuntimeFatal); } @@ -1107,7 +1108,8 @@ fn validate_constant_parent_redeclaration( } } for interface in class.interfaces() { - if let Some((_, interface_constant)) = context.interface_constant(interface, constant.name()) + if let Some((_, interface_constant)) = + context.interface_constant(interface, constant.name()) { if interface_constant.is_final() { return Err(EvalStatus::RuntimeFatal); @@ -2362,6 +2364,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_get_default_properties_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_get_reflection_constant_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 6305446ee2..753268b504 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1165,6 +1165,46 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass exposes eval property default metadata as an associative map. +#[test] +fn execute_program_reflection_class_get_default_properties_metadata() { + let program = parse_fragment( + br#"class EvalReflectDefaultBase { + public int $base = 1; + protected string $prot = "p"; + private int $shadow = 3; + public $implicit; + public int $typed; + public static string $baseStatic = "bs"; +} +class EvalReflectDefaultChild extends EvalReflectDefaultBase { + public int $child = 5; + private int $shadow = 9; + public static int $childStatic = 7; + public ?int $nullable = null; +} +$defaults = (new ReflectionClass("EvalReflectDefaultChild"))->getDefaultProperties(); +echo $defaults["childStatic"]; echo ":"; +echo $defaults["baseStatic"]; echo ":"; +echo $defaults["child"]; echo ":"; +echo $defaults["shadow"]; echo ":"; +echo $defaults["base"]; echo ":"; +echo $defaults["prot"]; echo ":"; +echo array_key_exists("implicit", $defaults) && $defaults["implicit"] === null ? "I:" : "i:"; +echo array_key_exists("nullable", $defaults) && $defaults["nullable"] === null ? "N:" : "n:"; +echo array_key_exists("typed", $defaults) ? "T" : "t"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:bs:5:9:1:p:I:N:t"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval ReflectionParameter exposes declaring class metadata. #[test] fn execute_program_reflects_eval_parameter_declaring_class() { diff --git a/docs/php/classes.md b/docs/php/classes.md index bf1572d960..2cc827eeeb 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1162,6 +1162,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::hasConstant()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named class constant or enum case; lookup is case-sensitive | | `ReflectionClass::getConstant()` | `new ReflectionClass($class_name)` | Return the reflected constant value, enum case object, or `false` when no such constant is visible | | `ReflectionClass::getConstants()` | `new ReflectionClass($class_name)` | Return an associative array of visible constant values keyed by constant or enum-case name | +| `ReflectionClass::getDefaultProperties()` | `new ReflectionClass($class_name)` | Return an associative array of supported materialized instance/static property defaults keyed by property name | | `ReflectionClass::getReflectionConstant()` | `new ReflectionClass($class_name)` | Return a `ReflectionClassConstant` object for the named visible constant or enum case, or `false` when no such constant is visible | | `ReflectionClass::getReflectionConstants()` | `new ReflectionClass($class_name)` | Return indexed `ReflectionClassConstant` objects for visible class constants and enum cases | | `ReflectionClass::implementsInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol implements, extends, or is the requested interface; interface lookup is case-insensitive and invalid names throw `ReflectionException` | @@ -1306,7 +1307,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index b91c64f9d7..9a9c2a51df 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -187,10 +187,11 @@ relations; trait targets return `false`. method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. `ReflectionClass::hasConstant()`, `getConstant()`, `getConstants()`, -`getReflectionConstant()`, and `getReflectionConstants()` expose eval-visible -class constants, interface constants, trait constants, enum constants, and enum -cases. Constant lookup is case-sensitive; single-value lookups return `false` -when no constant or case is visible. +`getDefaultProperties()`, `getReflectionConstant()`, and +`getReflectionConstants()` expose eval-visible class constants, interface +constants, trait constants, enum constants, enum cases, and supported +materialized property defaults. Constant lookup is case-sensitive; single-value +lookups return `false` when no constant or case is visible. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 19b902b1b2..5fcd14cca9 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -41,6 +41,7 @@ struct ReflectionOwnerMetadata { property_names: Vec, constant_names: Vec, constant_members: Vec, + default_property_members: Vec, constant_reflection_members: Vec, method_members: Vec, property_members: Vec, @@ -173,6 +174,13 @@ struct ReflectionConstantMember { value: ReflectionConstantValue, } +/// Metadata for one property entry returned by `ReflectionClass::getDefaultProperties()`. +#[derive(Clone)] +struct ReflectionDefaultPropertyMember { + name: String, + value: ReflectionParameterDefaultValue, +} + /// Compile-time value forms supported by Reflection constant metadata emission. #[derive(Clone)] enum ReflectionConstantValue { @@ -299,6 +307,11 @@ fn emit_reflection_owner_object( "__constants", &metadata.constant_members, )?; + emit_reflection_default_property_array_property_by_name( + ctx, + "__default_properties", + &metadata.default_property_members, + )?; emit_reflection_member_array_property_by_name( ctx, "__reflection_constants", @@ -497,6 +510,8 @@ fn reflection_class_metadata_for_name( let property_names = reflection_class_property_names(ctx, class_name, info); let constant_names = reflection_class_constant_names(ctx, class_name, info); let constant_members = reflection_class_constant_members(ctx, class_name, info)?; + let default_property_members = + reflection_class_default_property_members(info, &property_names); let constant_reflection_members = reflection_class_constant_reflection_members(ctx, class_name, info)?; let method_members = reflection_class_method_members(info, &method_names); @@ -516,6 +531,7 @@ fn reflection_class_metadata_for_name( property_names, constant_names, constant_members, + default_property_members, constant_reflection_members, method_members, property_members, @@ -570,6 +586,7 @@ fn reflection_class_metadata_for_name( property_names, constant_names, constant_members, + default_property_members: Vec::new(), constant_reflection_members, method_members, property_members, @@ -625,6 +642,7 @@ fn reflection_class_metadata_for_name( property_names, constant_names, constant_members, + default_property_members: Vec::new(), constant_reflection_members, method_members, property_members, @@ -759,6 +777,7 @@ fn reflection_method_owner_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + default_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -811,6 +830,7 @@ fn reflection_property_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + default_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -986,6 +1006,7 @@ fn reflection_class_constant_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + default_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -1046,6 +1067,7 @@ fn reflection_enum_case_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + default_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -1094,6 +1116,7 @@ fn reflection_class_constant_owner_metadata( property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + default_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -1497,6 +1520,24 @@ fn reflection_class_constant_members( Ok(members) } +/// Returns materializable property defaults for `ReflectionClass::getDefaultProperties()`. +fn reflection_class_default_property_members( + info: &crate::types::ClassInfo, + property_names: &[String], +) -> Vec { + property_names + .iter() + .filter_map(|property_name| { + reflection_property_default_value(info, property_name).map(|value| { + ReflectionDefaultPropertyMember { + name: property_name.clone(), + value, + } + }) + }) + .collect() +} + /// Returns materializable interface constant values for ReflectionClass metadata. fn reflection_interface_constant_members( ctx: &FunctionContext<'_>, @@ -2863,6 +2904,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { property_names: Vec::new(), constant_names: Vec::new(), constant_members: Vec::new(), + default_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -3146,6 +3188,39 @@ fn emit_reflection_constant_array_property_by_name( Ok(()) } +/// Replaces a ReflectionClass private slot with an associative default-property array. +fn emit_reflection_default_property_array_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + members: &[ReflectionDefaultPropertyMember], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_default_property_array(ctx, members); + let assoc_type = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }; + emit_box_current_value_as_mixed(ctx.emitter, &assoc_type); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + /// Replaces a ReflectionClass private array slot with ReflectionMethod/Property objects. fn emit_reflection_member_array_property_by_name( ctx: &mut FunctionContext<'_>, @@ -3380,6 +3455,19 @@ fn emit_reflection_constant_array( Ok(()) } +/// Allocates and populates the associative ReflectionClass default-property map. +fn emit_reflection_default_property_array( + ctx: &mut FunctionContext<'_>, + members: &[ReflectionDefaultPropertyMember], +) { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Mixed); + for member in members { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_default_value_as_mixed(ctx, &member.value); + emit_reflection_constant_hash_insert(ctx, &member.name); + } +} + /// Materializes one Reflection constant value as a boxed Mixed cell. fn emit_reflection_constant_value_as_mixed( ctx: &mut FunctionContext<'_>, @@ -3409,6 +3497,28 @@ fn emit_reflection_constant_value_as_mixed( } } +/// Materializes one Reflection default-property value as a boxed Mixed cell. +fn emit_reflection_default_value_as_mixed( + ctx: &mut FunctionContext<'_>, + value: &ReflectionParameterDefaultValue, +) { + match value { + ReflectionParameterDefaultValue::Int(value) => { + emit_boxed_int_literal_to_result(ctx, *value) + } + ReflectionParameterDefaultValue::Bool(value) => { + emit_boxed_bool_literal_to_result(ctx, *value) + } + ReflectionParameterDefaultValue::Float(value) => { + emit_boxed_float_literal_to_result(ctx, *value) + } + ReflectionParameterDefaultValue::Str(value) => { + emit_boxed_string_literal_default_to_result(ctx, value) + } + ReflectionParameterDefaultValue::Null => emit_boxed_null_literal_to_result(ctx), + } +} + /// Inserts the current boxed Mixed constant value into the stacked associative array. fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 39c69add2a..b5e32dc317 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -16,8 +16,8 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::names::Name; use crate::parser::ast::{ - BinOp, ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, InstanceOfTarget, Stmt, StmtKind, - TypeExpr, Visibility, + BinOp, ClassConst, ClassMethod, ClassProperty, Expr, ExprKind, InstanceOfTarget, Stmt, + StmtKind, TypeExpr, Visibility, }; use crate::types::traits::FlattenedClass; use crate::types::PhpType; @@ -950,6 +950,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(mixed_type()), empty_array(), ), + builtin_property( + "__default_properties", + Visibility::Private, + Some(mixed_type()), + empty_array(), + ), builtin_property( "__reflection_constants", Visibility::Private, @@ -1015,6 +1021,7 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_has_name_method("hasConstant", "__constant_names", false), builtin_reflection_class_get_constant_method(), builtin_reflection_class_mixed_method("getConstants", "__constants"), + builtin_reflection_class_mixed_method("getDefaultProperties", "__default_properties"), builtin_reflection_class_array_method( "getReflectionConstants", "__reflection_constants", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bf2c426b85..a66e9d94cb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6908,6 +6908,45 @@ echo $listed->getDefaultValue() === null ? "null" : "bad";'); ); } +/// Verifies eval ReflectionClass materializes property default metadata through the bridge. +#[test] +fn test_eval_reflection_class_get_default_properties_metadata() { + let out = compile_and_run_capture( + r#"getDefaultProperties(); +echo $defaults["childStatic"] . ":"; +echo $defaults["baseStatic"] . ":"; +echo $defaults["child"] . ":"; +echo $defaults["shadow"] . ":"; +echo $defaults["base"] . ":"; +echo $defaults["prot"] . ":"; +echo array_key_exists("implicit", $defaults) && $defaults["implicit"] === null ? "I:" : "i:"; +echo array_key_exists("nullable", $defaults) && $defaults["nullable"] === null ? "N:" : "n:"; +echo array_key_exists("typed", $defaults) ? "T" : "t";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:bs:5:9:1:p:I:N:t"); +} + /// Verifies eval ReflectionParameter exposes the declaring class for method parameters. #[test] fn test_eval_reflection_parameter_reports_declaring_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 7d35a8b1c1..6bf48098c2 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2365,6 +2365,57 @@ echo $listed->getDefaultValue() === null ? "null" : "bad"; ); } +/// Verifies that `ReflectionClass::getDefaultProperties()` exposes supported property defaults. +#[test] +fn test_reflection_class_get_default_properties_returns_property_metadata() { + let out = compile_and_run_capture( + r#"getDefaultProperties(); +echo $defaults["childStatic"] . ":"; +echo $defaults["baseStatic"] . ":"; +echo $defaults["child"] . ":"; +echo $defaults["shadow"] . ":"; +echo $defaults["base"] . ":"; +echo $defaults["prot"] . ":"; +$implicit = "i"; +$explicitNull = "e"; +$typed = "t"; +foreach ($defaults as $key => $value) { + if ($key === "implicit" && $value === null) { + $implicit = "I"; + } + if ($key === "explicitNull" && $value === null) { + $explicitNull = "E"; + } + if ($key === "typed") { + $typed = "T"; + } +} +echo $implicit . ":" . $explicitNull . ":" . $typed . ":" . count($defaults); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:bs:5:9:1:p:I:E:t:8"); +} + /// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. #[test] fn test_reflection_parameter_get_attributes_returns_parameter_metadata() { From cff6ff8416966d5e240863de85a79df5d41262d9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 18:52:40 +0200 Subject: [PATCH 0435/1208] Expose ReflectionClass anonymous predicate --- .../tests/builtins_class_metadata.rs | 24 +++++++++++ .../interpreter/tests/support/object_ops.rs | 6 +++ docs/php/classes.md | 9 ++-- docs/php/eval.md | 5 ++- src/codegen/lower_inst/objects/reflection.rs | 18 ++++++++ src/ir_lower/expr/mod.rs | 43 ++++++++++++++++--- src/types/checker/builtin_types/reflection.rs | 10 ++++- .../checker/inference/objects/constructors.rs | 17 +++++++- tests/codegen/eval.rs | 24 +++++++++++ tests/codegen/oop/anonymous_classes.rs | 23 ++++++++++ 10 files changed, 166 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 753268b504..c8c235d2a7 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -583,6 +583,30 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::isAnonymous reports false for eval-declared named class-like symbols. +#[test] +fn execute_program_reflection_class_reports_named_classes_not_anonymous() { + let program = parse_fragment( + br#"class EvalNamedAnonymousReflect {} +interface EvalNamedAnonymousIface {} +trait EvalNamedAnonymousTrait {} +enum EvalNamedAnonymousEnum { case Ready; } +echo (new ReflectionClass("EvalNamedAnonymousReflect"))->isAnonymous() ? "C" : "c"; +echo (new ReflectionClass("EvalNamedAnonymousIface"))->isAnonymous() ? "I" : "i"; +echo (new ReflectionClass("EvalNamedAnonymousTrait"))->isAnonymous() ? "T" : "t"; +echo (new ReflectionClass("EvalNamedAnonymousEnum"))->isAnonymous() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "cite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::getConstructor exposes eval constructor metadata. #[test] fn execute_program_reflection_class_get_constructor() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 833b7d7445..6db71a7e10 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -153,6 +153,10 @@ impl FakeOps { Self::object_property(&properties, "__is_readonly") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isanonymous") if args.is_empty() => { + Self::object_property(&properties, "__is_anonymous") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "isinstantiable") if args.is_empty() => { Self::object_property(&properties, "__is_instantiable") .map_or_else(|| self.bool_value(false), Ok) @@ -524,6 +528,7 @@ impl FakeOps { let is_enum = self.bool_value((flags & 16) != 0)?; let is_readonly = self.bool_value((flags & 32) != 0)?; let is_instantiable = self.bool_value((flags & 64) != 0)?; + let is_anonymous = self.bool_value(false)?; let modifiers_cell = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { @@ -541,6 +546,7 @@ impl FakeOps { properties.push(("__is_trait".to_string(), is_trait)); properties.push(("__is_enum".to_string(), is_enum)); properties.push(("__is_readonly".to_string(), is_readonly)); + properties.push(("__is_anonymous".to_string(), is_anonymous)); properties.push(("__is_instantiable".to_string(), is_instantiable)); properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__short_name".to_string(), short_name)); diff --git a/docs/php/classes.md b/docs/php/classes.md index 2cc827eeeb..ee90671e49 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1155,6 +1155,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isTrait()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is a trait | | `ReflectionClass::isEnum()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is an enum | | `ReflectionClass::isReadOnly()` | `new ReflectionClass($class_name)` | Return whether the reflected class is a `readonly class` | +| `ReflectionClass::isAnonymous()` | `new ReflectionClass($class_name_or_object)` | Return whether the reflected class is an anonymous class | | `ReflectionClass::isInstantiable()` | `new ReflectionClass($class_name)` | Return whether the reflected symbol is a concrete class with no constructor or a public constructor | | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | | `ReflectionClass::hasMethod()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named method; method lookup is case-insensitive | @@ -1302,12 +1303,12 @@ name with `isBuiltin()` false. | `ReflectionIntersectionType::allowsNull()` | `bool` | Always false for supported intersections | Limitations today: -- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Function/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, and traits; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name→id lookup table that is not yet implemented. -- Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** — a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` → `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. -- A symbolic reference that elephc cannot resolve — for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered — is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. +- All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Function/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, traits, and statically typed object expressions, including anonymous-class objects; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name->id lookup table that is not yet implemented. +- Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** - a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` -> `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. +- A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index 9a9c2a51df..80eed7a0b2 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -165,8 +165,9 @@ derive namespace-aware parts from the resolved eval class-like name. `ReflectionClass::isEnum()` report eval class-like metadata, including PHP-compatible enum finality and class-like kind checks for eval interfaces, traits, and enums. `ReflectionClass::isReadOnly()` reports eval `readonly class` -metadata. `ReflectionClass::isInstantiable()` reports whether eval class-like -metadata describes a concrete class with no constructor or a public constructor. +metadata. `ReflectionClass::isAnonymous()` reports `false` for eval-declared +named class-like symbols. `ReflectionClass::isInstantiable()` reports whether +eval class-like metadata describes a concrete class with no constructor or a public constructor. `ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::IS_*` modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 5fcd14cca9..bf5d68e5b0 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -60,6 +60,7 @@ struct ReflectionOwnerMetadata { is_trait: bool, is_enum: bool, is_readonly: bool, + is_anonymous: bool, is_instantiable: bool, modifiers: i64, member_flags: ReflectionMemberFlags, @@ -345,6 +346,7 @@ fn emit_reflection_owner_object( emit_reflection_bool_property(ctx, "__is_trait", metadata.is_trait)?; emit_reflection_bool_property(ctx, "__is_enum", metadata.is_enum)?; emit_reflection_bool_property(ctx, "__is_readonly", metadata.is_readonly)?; + emit_reflection_bool_property(ctx, "__is_anonymous", metadata.is_anonymous)?; emit_reflection_bool_property(ctx, "__is_instantiable", metadata.is_instantiable)?; emit_reflection_int_property_by_name(ctx, "__modifiers", metadata.modifiers)?; } @@ -550,6 +552,7 @@ fn reflection_class_metadata_for_name( is_trait: false, is_enum, is_readonly: info.is_readonly_class && !is_enum, + is_anonymous: is_reflection_anonymous_class_name(class_name), is_instantiable, modifiers: reflection_class_modifiers( info.is_final, @@ -605,6 +608,7 @@ fn reflection_class_metadata_for_name( is_trait: false, is_enum: false, is_readonly: false, + is_anonymous: false, is_instantiable: false, modifiers: 0, member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), @@ -661,6 +665,7 @@ fn reflection_class_metadata_for_name( is_trait: true, is_enum: false, is_readonly: false, + is_anonymous: false, is_instantiable: false, modifiers: 0, member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), @@ -796,6 +801,7 @@ fn reflection_method_owner_metadata( is_trait: false, is_enum: false, is_readonly: false, + is_anonymous: false, is_instantiable: false, modifiers: reflection_method_modifiers_from_flags(member.flags), member_flags: member.flags, @@ -849,6 +855,7 @@ fn reflection_property_metadata( is_trait: false, is_enum: false, is_readonly: false, + is_anonymous: false, is_instantiable: false, modifiers: reflection_property_modifiers_for_info(info, &property_name)?, member_flags: reflection_property_member_flags(info, &property_name)?, @@ -1028,6 +1035,7 @@ fn reflection_class_constant_metadata( is_trait: false, is_enum: false, is_readonly: false, + is_anonymous: false, is_instantiable: false, modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false), @@ -1089,6 +1097,7 @@ fn reflection_enum_case_metadata( is_trait: false, is_enum: false, is_readonly: false, + is_anonymous: false, is_instantiable: false, modifiers: 0, member_flags: ReflectionMemberFlags::default(), @@ -1135,6 +1144,7 @@ fn reflection_class_constant_owner_metadata( is_trait: false, is_enum: false, is_readonly: false, + is_anonymous: false, is_instantiable: false, modifiers, member_flags, @@ -1282,6 +1292,13 @@ fn resolve_reflection_class<'a>( .map(|(name, info)| (name.as_str(), info)) } +/// Returns true when a class name uses the parser's anonymous-class synthetic prefix. +fn is_reflection_anonymous_class_name(class_name: &str) -> bool { + class_name + .trim_start_matches('\\') + .starts_with("class@anonymous#") +} + /// Looks up interface metadata by PHP-style case-insensitive name. fn resolve_reflection_interface<'a>( ctx: &'a FunctionContext<'_>, @@ -2923,6 +2940,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { is_trait: false, is_enum: false, is_readonly: false, + is_anonymous: false, is_instantiable: false, modifiers: 0, member_flags: ReflectionMemberFlags::default(), diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 3a49520e84..8b2dbfbe81 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9092,6 +9092,20 @@ fn lower_new_object( args: &[Expr], expr: &Expr, ) -> LoweredValue { + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) == "reflectionclass" { + if let Some(operands) = lower_reflection_class_constructor_operands(ctx, args) { + let php_type = PhpType::Object(class_name.as_str().to_string()); + let data = ctx.intern_class_name(class_name.as_str()); + return ctx.emit_value( + Op::ObjectNew, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ObjectNew.default_effects(), + Some(expr.span), + ); + } + } if php_symbol_key(class_name.as_str().trim_start_matches('\\')) == "reflectionparameter" { if let Some(operands) = lower_reflection_parameter_constructor_operands(ctx, args) { let php_type = PhpType::Object(class_name.as_str().to_string()); @@ -9120,12 +9134,31 @@ fn lower_new_object( ) } -/// Lowers PHP `clone $object` to a shallow object-copy opcode and optional `__clone()` hook. -fn lower_clone( +/// Lowers `ReflectionClass(object)` to the object's statically-known class name. +fn lower_reflection_class_constructor_operands( ctx: &mut LoweringContext<'_, '_>, - inner: &Expr, - expr: &Expr, -) -> LoweredValue { + args: &[Expr], +) -> Option> { + let reflected_arg = reflection_class_constructor_class_arg(ctx, args)?; + let class_name = instance_callable_object_class(ctx, &reflected_arg)?; + let lowered = lower_expr(ctx, &reflected_arg); + if ctx.value_is_owning_temporary(lowered) { + crate::ir_lower::ownership::release_if_owned(ctx, lowered, Some(reflected_arg.span)); + } + let data = ctx.intern_class_name(&class_name); + let value = ctx.emit_value( + Op::ConstClassName, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Str, + Op::ConstClassName.default_effects(), + Some(reflected_arg.span), + ); + Some(vec![value.value]) +} + +/// Lowers PHP `clone $object` to a shallow object-copy opcode and optional `__clone()` hook. +fn lower_clone(ctx: &mut LoweringContext<'_, '_>, inner: &Expr, expr: &Expr) -> LoweredValue { let object = lower_expr(ctx, inner); let object_ty = ctx.builder.value_php_type(object.value); let Some((class_name, false)) = singular_object_class(&object_ty) else { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index b5e32dc317..142a84003b 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -878,6 +878,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__is_anonymous", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), builtin_property( "__is_instantiable", Visibility::Private, @@ -990,7 +996,7 @@ fn builtin_reflection_class() -> FlattenedClass { methods: vec![ builtin_reflection_owner_constructor_method(vec![( "class_name", - Some(TypeExpr::Str), + Some(mixed_type()), None, false, )]), @@ -1014,6 +1020,7 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_bool_method("isTrait", "__is_trait"), builtin_reflection_class_bool_method("isEnum", "__is_enum"), builtin_reflection_class_bool_method("isReadOnly", "__is_readonly"), + builtin_reflection_class_bool_method("isAnonymous", "__is_anonymous"), builtin_reflection_class_bool_method("isInstantiable", "__is_instantiable"), builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), @@ -2550,6 +2557,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "istrait", "isenum", "isreadonly", + "isanonymous", "isinstantiable", "hasmethod", "hasproperty", diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 5759d5ce79..9e73457837 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -579,7 +579,22 @@ impl Checker { env: &TypeEnv, ) -> Result { let arg_ty = self.infer_type(arg, env)?; - if !matches!(arg_ty, PhpType::Str) { + if let PhpType::Object(class_name) = arg_ty.codegen_repr() { + if reflection_type == "ReflectionClass" + && !class_name.is_empty() + && self.classes.contains_key(class_name.as_str()) + { + return Ok(class_name); + } + return Err(CompileError::new( + arg.span, + &format!( + "{}::__construct() first argument must be a string class name", + reflection_type + ), + )); + } + if !matches!(arg_ty.codegen_repr(), PhpType::Str) { return Err(CompileError::new( arg.span, &format!( diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a66e9d94cb..f3e754fd20 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6255,6 +6255,30 @@ echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e";'); assert_eq!(out.stdout, "aBCprite"); } +/// Verifies eval ReflectionClass reports named eval class-like symbols as non-anonymous through +/// the generated reflection-owner bridge. +#[test] +fn test_eval_reflection_class_anonymous_predicate() { + let out = compile_and_run_capture( + r#"isAnonymous() ? "C" : "c"; +echo (new ReflectionClass("EvalAnonIface"))->isAnonymous() ? "I" : "i"; +echo (new ReflectionClass("EvalAnonTrait"))->isAnonymous() ? "T" : "t"; +echo (new ReflectionClass("EvalAnonEnum"))->isAnonymous() ? "E" : "e";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "cite"); +} + /// Verifies eval ReflectionClass reports method, property, and constant membership through the bridge. #[test] fn test_eval_reflection_class_member_existence() { diff --git a/tests/codegen/oop/anonymous_classes.rs b/tests/codegen/oop/anonymous_classes.rs index 715d2bec63..209126fa68 100644 --- a/tests/codegen/oop/anonymous_classes.rs +++ b/tests/codegen/oop/anonymous_classes.rs @@ -119,6 +119,29 @@ fn test_readonly_anonymous_class() { assert_eq!(out, "frozen"); } +/// Verifies `ReflectionClass::isAnonymous()` reports anonymous-class metadata without relying on +/// the parser-global synthetic class counter spelling. +#[test] +fn test_reflection_class_reports_anonymous_class() { + let out = compile_and_run( + "isAnonymous() ? \"A\" : \"a\"; + echo $directRef->isAnonymous() ? \"D\" : \"d\"; + echo $namedRef->isAnonymous() ? \"N\" : \"n\"; + ", + ); + assert_eq!(out, "ADn"); +} + /// Compiles and runs the checked-in `examples/anonymous-classes/main.php` fixture, covering /// interface-implementing anonymous classes (with and without constructor args) and one that /// extends an abstract base. From 0c805a8f039ee8d8e5c924843689bf4c907b6971 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 19:06:46 +0200 Subject: [PATCH 0436/1208] Expose ReflectionClass cloneable predicate --- .../elephc-eval/src/interpreter/reflection.rs | 19 ++++++ .../tests/builtins_class_metadata.rs | 34 ++++++++++ .../interpreter/tests/support/object_ops.rs | 6 ++ docs/php/classes.md | 2 +- docs/php/eval.md | 2 + src/codegen/eval_reflection_owner_helpers.rs | 48 ++++++++----- src/codegen/lower_inst/objects/reflection.rs | 67 +++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 8 +++ tests/codegen/eval.rs | 28 ++++++++ tests/codegen/oop/attributes.rs | 31 +++++++++ 10 files changed, 227 insertions(+), 18 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 013dedf54f..b82900d11d 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -19,6 +19,7 @@ const EVAL_REFLECTION_CLASS_FLAG_TRAIT: u64 = 8; const EVAL_REFLECTION_CLASS_FLAG_ENUM: u64 = 16; const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; const EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE: u64 = 64; +const EVAL_REFLECTION_CLASS_FLAG_CLONEABLE: u64 = 128; const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; @@ -1520,6 +1521,9 @@ fn eval_reflection_class_like_attributes( if eval_reflection_class_is_instantiable(class, is_enum, context) { flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; } + if eval_reflection_class_is_cloneable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; + } let modifiers = eval_reflection_class_modifiers( class.is_final(), class.is_abstract(), @@ -1606,6 +1610,21 @@ fn eval_reflection_class_is_instantiable( .unwrap_or(true) } +/// Returns PHP's `ReflectionClass::isCloneable()` value for eval class metadata. +fn eval_reflection_class_is_cloneable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_method(class.name(), "__clone") + .map(|(_, method)| method.visibility() == EvalVisibility::Public) + .unwrap_or(true) +} + /// Computes PHP's `ReflectionClass::getModifiers()` bitmask for eval metadata. fn eval_reflection_class_modifiers( is_final: bool, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c8c235d2a7..68722e0626 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -607,6 +607,40 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::isCloneable reports eval class clone metadata. +#[test] +fn execute_program_reflects_eval_class_cloneable_predicate() { + let program = parse_fragment( + br#"abstract class EvalCloneAbstract {} +class EvalClonePlain {} +final class EvalCloneFinal {} +class EvalClonePrivate { private function __clone() {} } +class EvalCloneProtected { protected function __clone() {} } +class EvalClonePublic { public function __clone() {} } +interface EvalCloneIface {} +trait EvalCloneTrait {} +enum EvalCloneEnum { case Ready; } +echo (new ReflectionClass("EvalCloneAbstract"))->isCloneable() ? "A" : "a"; +echo (new ReflectionClass("EvalClonePlain"))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass("EvalCloneFinal"))->isCloneable() ? "F" : "f"; +echo (new ReflectionClass("EvalClonePrivate"))->isCloneable() ? "V" : "v"; +echo (new ReflectionClass("EvalCloneProtected"))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass("EvalClonePublic"))->isCloneable() ? "U" : "u"; +echo (new ReflectionClass("EvalCloneIface"))->isCloneable() ? "I" : "i"; +echo (new ReflectionClass("EvalCloneTrait"))->isCloneable() ? "T" : "t"; +echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "aPFvrUite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::getConstructor exposes eval constructor metadata. #[test] fn execute_program_reflection_class_get_constructor() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 6db71a7e10..12b39f75dc 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -161,6 +161,10 @@ impl FakeOps { Self::object_property(&properties, "__is_instantiable") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "iscloneable") if args.is_empty() => { + Self::object_property(&properties, "__is_cloneable") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getparentclass") if args.is_empty() => { Self::object_property(&properties, "__parent_class") .map_or_else(|| self.bool_value(false), Ok) @@ -528,6 +532,7 @@ impl FakeOps { let is_enum = self.bool_value((flags & 16) != 0)?; let is_readonly = self.bool_value((flags & 32) != 0)?; let is_instantiable = self.bool_value((flags & 64) != 0)?; + let is_cloneable = self.bool_value((flags & 128) != 0)?; let is_anonymous = self.bool_value(false)?; let modifiers_cell = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; @@ -548,6 +553,7 @@ impl FakeOps { properties.push(("__is_readonly".to_string(), is_readonly)); properties.push(("__is_anonymous".to_string(), is_anonymous)); properties.push(("__is_instantiable".to_string(), is_instantiable)); + properties.push(("__is_cloneable".to_string(), is_cloneable)); properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__short_name".to_string(), short_name)); properties.push(("__namespace_name".to_string(), namespace_name)); diff --git a/docs/php/classes.md b/docs/php/classes.md index ee90671e49..058e91ea2d 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1308,7 +1308,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index 80eed7a0b2..bbcc283b04 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -168,6 +168,8 @@ traits, and enums. `ReflectionClass::isReadOnly()` reports eval `readonly class` metadata. `ReflectionClass::isAnonymous()` reports `false` for eval-declared named class-like symbols. `ReflectionClass::isInstantiable()` reports whether eval class-like metadata describes a concrete class with no constructor or a public constructor. +`ReflectionClass::isCloneable()` reports whether eval class metadata describes +a concrete class with no `__clone()` or a public `__clone()`. `ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::IS_*` modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index b3322f36c3..d7091d29a6 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -66,6 +66,8 @@ struct ReflectionOwnerLayout { is_readonly_hi: Option, is_instantiable_lo: Option, is_instantiable_hi: Option, + is_cloneable_lo: Option, + is_cloneable_hi: Option, modifiers_lo: Option, modifiers_hi: Option, in_namespace_lo: Option, @@ -222,6 +224,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option Option bool { + if info.is_abstract || is_enum || reflection_class_has_runtime_managed_storage(class_name) { + return false; + } + let clone_key = php_symbol_key("__clone"); + info.method_visibilities + .get(&clone_key) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + +/// Returns whether a builtin's object layout is outside ordinary declared slots. +fn reflection_class_has_runtime_managed_storage(class_name: &str) -> bool { + let key = php_symbol_key(class_name); + matches!( + key.as_str(), + "throwable" + | "error" + | "exception" + | "valueerror" + | "runtimeexception" + | "reflectionexception" + | "jsonexception" + | "fiber" + | "fibererror" + | "generator" + | "reflectionattribute" + | "reflectionclass" + | "reflectionfunction" + | "reflectionmethod" + | "reflectionproperty" + | "reflectionparameter" + | "reflectionnamedtype" + | "reflectionuniontype" + | "reflectionintersectiontype" + | "reflectionclassconstant" + | "reflectionenumunitcase" + | "reflectionenumbackedcase" + | "splfixedarray" + | "spldoublylinkedlist" + | "splstack" + | "splqueue" + | "iteratoriterator" + | "filteriterator" + | "callbackfilteriterator" + | "recursivefilteriterator" + | "recursivecallbackfilteriterator" + | "recursiveiteratoriterator" + ) +} + /// Collects direct and inherited parent interfaces for a reflected interface. fn reflection_interface_parent_names( ctx: &FunctionContext<'_>, @@ -2942,6 +3008,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { is_readonly: false, is_anonymous: false, is_instantiable: false, + is_cloneable: false, modifiers: 0, member_flags: ReflectionMemberFlags::default(), } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 142a84003b..ead2bfcfd1 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -890,6 +890,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__is_cloneable", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), builtin_property( "__modifiers", Visibility::Private, @@ -1022,6 +1028,7 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_bool_method("isReadOnly", "__is_readonly"), builtin_reflection_class_bool_method("isAnonymous", "__is_anonymous"), builtin_reflection_class_bool_method("isInstantiable", "__is_instantiable"), + builtin_reflection_class_bool_method("isCloneable", "__is_cloneable"), builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), @@ -2559,6 +2566,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isreadonly", "isanonymous", "isinstantiable", + "iscloneable", "hasmethod", "hasproperty", "implementsinterface", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f3e754fd20..7cea35fffe 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7002,6 +7002,34 @@ echo $listed->getDeclaringFunction()->getName();'); ); } +/// Verifies eval ReflectionClass::isCloneable uses eval class metadata through the bridge. +#[test] +fn test_eval_reflection_class_cloneable_predicate() { + let out = compile_and_run( + r#"isCloneable() ? "A" : "a"; +echo (new ReflectionClass("EvalClonePlain"))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass("EvalCloneFinal"))->isCloneable() ? "F" : "f"; +echo (new ReflectionClass("EvalClonePrivate"))->isCloneable() ? "V" : "v"; +echo (new ReflectionClass("EvalCloneProtected"))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass("EvalClonePublic"))->isCloneable() ? "U" : "u"; +echo (new ReflectionClass("EvalCloneIface"))->isCloneable() ? "I" : "i"; +echo (new ReflectionClass("EvalCloneTrait"))->isCloneable() ? "T" : "t"; +echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e";'); +"#, + ); + assert_eq!(out, "aPFvrUite"); +} + /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. #[test] fn test_eval_reflection_class_new_instance_constructs_eval_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 6bf48098c2..274d0243d6 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1269,6 +1269,37 @@ echo (new ReflectionClass(ReflectInstEnum::class))->isInstantiable() ? "E" : "e" assert_eq!(out, "aBCprite"); } +/// Verifies that `ReflectionClass::isCloneable()` reports PHP clone visibility, +/// class-kind, and elephc runtime-storage rules for static metadata. +#[test] +fn test_reflection_class_is_cloneable() { + let out = compile_and_run( + r#"isCloneable() ? "A" : "a"; +echo (new ReflectionClass(ReflectClonePlain::class))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass(ReflectCloneFinal::class))->isCloneable() ? "F" : "f"; +echo (new ReflectionClass(ReflectClonePrivate::class))->isCloneable() ? "V" : "v"; +echo (new ReflectionClass(ReflectCloneProtected::class))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass(ReflectClonePublic::class))->isCloneable() ? "U" : "u"; +echo (new ReflectionClass(ReflectCloneIface::class))->isCloneable() ? "I" : "i"; +echo (new ReflectionClass(ReflectCloneTrait::class))->isCloneable() ? "T" : "t"; +echo (new ReflectionClass(ReflectCloneEnum::class))->isCloneable() ? "E" : "e"; +echo (new ReflectionClass(stdClass::class))->isCloneable() ? "S" : "s"; +echo (new ReflectionClass(ReflectionClass::class))->isCloneable() ? "C" : "c"; +"#, + ); + assert_eq!(out, "aPFvrUiteSc"); +} + /// Verifies that `ReflectionClass::hasMethod()`, `hasProperty()`, and /// `hasConstant()` report PHP-visible members for static class-like metadata. #[test] From 75d43c3913857b25430fd639bb47f2dae607f673 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 19:23:09 +0200 Subject: [PATCH 0437/1208] Expose ReflectionClass origin predicates --- .../elephc-eval/src/interpreter/reflection.rs | 61 +++++++++- .../tests/builtins_class_metadata.rs | 30 +++++ .../interpreter/tests/support/object_ops.rs | 12 ++ docs/php/classes.md | 5 +- docs/php/eval.md | 3 + src/codegen/eval_reflection_owner_helpers.rs | 44 +++++++ src/codegen/lower_inst/objects/reflection.rs | 109 ++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 16 +++ tests/codegen/eval.rs | 27 +++++ tests/codegen/oop/attributes.rs | 29 +++++ 10 files changed, 331 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index b82900d11d..b18f9b5089 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -20,6 +20,8 @@ const EVAL_REFLECTION_CLASS_FLAG_ENUM: u64 = 16; const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; const EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE: u64 = 64; const EVAL_REFLECTION_CLASS_FLAG_CLONEABLE: u64 = 128; +const EVAL_REFLECTION_CLASS_FLAG_INTERNAL: u64 = 256; +const EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED: u64 = 512; const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; @@ -616,6 +618,11 @@ fn eval_reflection_class_new( return Ok(None); } let mut flags = 0; + if eval_reflection_class_like_is_internal(&class_name) { + flags |= EVAL_REFLECTION_CLASS_FLAG_INTERNAL; + } else { + flags |= EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; + } if is_interface { flags |= EVAL_REFLECTION_CLASS_FLAG_INTERFACE; } @@ -1505,7 +1512,7 @@ fn eval_reflection_class_like_attributes( ) -> Option { if let Some(class) = context.class(name) { let is_enum = context.has_enum(class.name()); - let mut flags = 0; + let mut flags = EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; if class.is_final() { flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL; } @@ -1551,7 +1558,7 @@ fn eval_reflection_class_like_attributes( method_names: context.interface_method_names(interface.name()), property_names: context.interface_property_names(interface.name()), parent_class_name: None, - flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE, + flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, modifiers: 0, }); } @@ -1564,7 +1571,7 @@ fn eval_reflection_class_like_attributes( method_names: context.trait_method_names(trait_decl.name()), property_names: context.trait_property_names(trait_decl.name()), parent_class_name: None, - flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT, + flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, modifiers: 0, }); } @@ -1578,7 +1585,9 @@ fn eval_reflection_class_like_attributes( method_names: context.class_method_names(enum_decl.name()), property_names: context.class_property_names(enum_decl.name()), parent_class_name: None, - flags: EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM, + flags: EVAL_REFLECTION_CLASS_FLAG_FINAL + | EVAL_REFLECTION_CLASS_FLAG_ENUM + | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, modifiers: 32, }) } @@ -1625,6 +1634,50 @@ fn eval_reflection_class_is_cloneable( .unwrap_or(true) } +/// Returns whether one reflected class-like name belongs to compiler-injected metadata. +fn eval_reflection_class_like_is_internal(class_name: &str) -> bool { + let trimmed = class_name.trim_start_matches('\\'); + if EVAL_SPL_CLASS_NAMES + .iter() + .any(|candidate| candidate.eq_ignore_ascii_case(trimmed)) + { + return true; + } + matches!( + trimmed.to_ascii_lowercase().as_str(), + "__elephcappenditeratorarrayiterator" + | "fiber" + | "fibererror" + | "generator" + | "internaliterator" + | "jsonexception" + | "phar" + | "phardata" + | "pharfileinfo" + | "php_user_filter" + | "reflectionattribute" + | "reflectionclass" + | "reflectionclassconstant" + | "reflectionenumbackedcase" + | "reflectionenumunitcase" + | "reflectionexception" + | "reflectionfunction" + | "reflectionintersectiontype" + | "reflectionmethod" + | "reflectionnamedtype" + | "reflectionparameter" + | "reflectionproperty" + | "reflectionuniontype" + | "sortdirection" + | "splheap" + | "splmaxheap" + | "splminheap" + | "splobjectstorage" + | "splpriorityqueue" + | "stdclass" + ) +} + /// Computes PHP's `ReflectionClass::getModifiers()` bitmask for eval metadata. fn eval_reflection_class_modifiers( is_final: bool, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 68722e0626..94a6deac90 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -641,6 +641,36 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass origin predicates report eval class-like symbols as user-defined. +#[test] +fn execute_program_reflects_eval_class_origin_predicates() { + let program = parse_fragment( + br#"class EvalOriginClass {} +interface EvalOriginIface {} +trait EvalOriginTrait {} +enum EvalOriginEnum { case Ready; } +function eval_reflect_origin($name) { + $r = new ReflectionClass($name); + echo $r->isInternal() ? "I" : "i"; + echo $r->isUserDefined() ? "U" : "u"; + echo ":"; +} +eval_reflect_origin("EvalOriginClass"); +eval_reflect_origin("EvalOriginIface"); +eval_reflect_origin("EvalOriginTrait"); +eval_reflect_origin("EvalOriginEnum"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "iU:iU:iU:iU:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::getConstructor exposes eval constructor metadata. #[test] fn execute_program_reflection_class_get_constructor() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 12b39f75dc..217d529deb 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -165,6 +165,14 @@ impl FakeOps { Self::object_property(&properties, "__is_cloneable") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isinternal") if args.is_empty() => { + Self::object_property(&properties, "__is_internal") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isuserdefined") if args.is_empty() => { + Self::object_property(&properties, "__is_user_defined") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getparentclass") if args.is_empty() => { Self::object_property(&properties, "__parent_class") .map_or_else(|| self.bool_value(false), Ok) @@ -533,6 +541,8 @@ impl FakeOps { let is_readonly = self.bool_value((flags & 32) != 0)?; let is_instantiable = self.bool_value((flags & 64) != 0)?; let is_cloneable = self.bool_value((flags & 128) != 0)?; + let is_internal = self.bool_value((flags & 256) != 0)?; + let is_user_defined = self.bool_value((flags & 512) != 0)?; let is_anonymous = self.bool_value(false)?; let modifiers_cell = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; @@ -554,6 +564,8 @@ impl FakeOps { properties.push(("__is_anonymous".to_string(), is_anonymous)); properties.push(("__is_instantiable".to_string(), is_instantiable)); properties.push(("__is_cloneable".to_string(), is_cloneable)); + properties.push(("__is_internal".to_string(), is_internal)); + properties.push(("__is_user_defined".to_string(), is_user_defined)); properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__short_name".to_string(), short_name)); properties.push(("__namespace_name".to_string(), namespace_name)); diff --git a/docs/php/classes.md b/docs/php/classes.md index 058e91ea2d..582a586922 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1157,6 +1157,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isReadOnly()` | `new ReflectionClass($class_name)` | Return whether the reflected class is a `readonly class` | | `ReflectionClass::isAnonymous()` | `new ReflectionClass($class_name_or_object)` | Return whether the reflected class is an anonymous class | | `ReflectionClass::isInstantiable()` | `new ReflectionClass($class_name)` | Return whether the reflected symbol is a concrete class with no constructor or a public constructor | +| `ReflectionClass::isCloneable()` | `new ReflectionClass($class_name)` | Return whether the reflected symbol is a concrete cloneable class with no `__clone()` or a public `__clone()` | +| `ReflectionClass::isInternal()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is compiler-injected builtin metadata | +| `ReflectionClass::isUserDefined()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol comes from user source | | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | | `ReflectionClass::hasMethod()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named method; method lookup is case-insensitive | | `ReflectionClass::hasProperty()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol has the named property; property lookup is case-sensitive | @@ -1308,7 +1311,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index bbcc283b04..839485d31d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -170,6 +170,9 @@ named class-like symbols. `ReflectionClass::isInstantiable()` reports whether eval class-like metadata describes a concrete class with no constructor or a public constructor. `ReflectionClass::isCloneable()` reports whether eval class metadata describes a concrete class with no `__clone()` or a public `__clone()`. +`ReflectionClass::isInternal()` and `isUserDefined()` distinguish +compiler-injected class-like metadata from eval-declared or generated +user-defined class-like symbols. `ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::IS_*` modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index d7091d29a6..bd2dcd80c8 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -68,6 +68,10 @@ struct ReflectionOwnerLayout { is_instantiable_hi: Option, is_cloneable_lo: Option, is_cloneable_hi: Option, + is_internal_lo: Option, + is_internal_hi: Option, + is_user_defined_lo: Option, + is_user_defined_hi: Option, modifiers_lo: Option, modifiers_hi: Option, in_namespace_lo: Option, @@ -225,6 +229,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option bool { ) } +/// Returns whether the reflected class-like name belongs to compiler-injected PHP metadata. +fn reflection_class_like_is_internal(class_name: &str) -> bool { + let key = php_symbol_key(class_name.trim_start_matches('\\')); + matches!( + key.as_str(), + "__elephcappenditeratorarrayiterator" + | "appenditerator" + | "arrayaccess" + | "arrayiterator" + | "arrayobject" + | "badfunctioncallexception" + | "badmethodcallexception" + | "cachingiterator" + | "callbackfilteriterator" + | "countable" + | "directoryiterator" + | "domainexception" + | "emptyiterator" + | "error" + | "exception" + | "fiber" + | "fibererror" + | "filteriterator" + | "filesystemiterator" + | "generator" + | "globiterator" + | "infiniteiterator" + | "internaliterator" + | "invalidargumentexception" + | "iterator" + | "iteratoraggregate" + | "iteratoriterator" + | "jsonexception" + | "jsonserializable" + | "lengthexception" + | "limititerator" + | "logicexception" + | "multipleiterator" + | "norewinditerator" + | "outeriterator" + | "outofboundsexception" + | "outofrangeexception" + | "overflowexception" + | "parentiterator" + | "phar" + | "phardata" + | "pharfileinfo" + | "php_user_filter" + | "rangeexception" + | "recursivearrayiterator" + | "recursivecachingiterator" + | "recursivecallbackfilteriterator" + | "recursivedirectoryiterator" + | "recursivefilteriterator" + | "recursiveiterator" + | "recursiveiteratoriterator" + | "recursiveregexiterator" + | "reflectionattribute" + | "reflectionclass" + | "reflectionclassconstant" + | "reflectionenumbackedcase" + | "reflectionenumunitcase" + | "reflectionexception" + | "reflectionfunction" + | "reflectionintersectiontype" + | "reflectionmethod" + | "reflectionnamedtype" + | "reflectionparameter" + | "reflectionproperty" + | "reflectionuniontype" + | "regexiterator" + | "runtimeexception" + | "seekableiterator" + | "sortdirection" + | "spldoublylinkedlist" + | "splfixedarray" + | "splfileinfo" + | "splfileobject" + | "splheap" + | "splmaxheap" + | "splminheap" + | "splobjectstorage" + | "splobserver" + | "splpriorityqueue" + | "splqueue" + | "splstack" + | "splsubject" + | "spltempfileobject" + | "stdclass" + | "stringable" + | "throwable" + | "traversable" + | "typeerror" + | "underflowexception" + | "unexpectedvalueexception" + | "valueerror" + ) +} + /// Collects direct and inherited parent interfaces for a reflected interface. fn reflection_interface_parent_names( ctx: &FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index ead2bfcfd1..3a8d7346b8 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -896,6 +896,18 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__is_internal", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__is_user_defined", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), builtin_property( "__modifiers", Visibility::Private, @@ -1029,6 +1041,8 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_bool_method("isAnonymous", "__is_anonymous"), builtin_reflection_class_bool_method("isInstantiable", "__is_instantiable"), builtin_reflection_class_bool_method("isCloneable", "__is_cloneable"), + builtin_reflection_class_bool_method("isInternal", "__is_internal"), + builtin_reflection_class_bool_method("isUserDefined", "__is_user_defined"), builtin_reflection_class_int_method("getModifiers", "__modifiers"), builtin_reflection_class_has_name_method("hasMethod", "__method_names", true), builtin_reflection_class_has_name_method("hasProperty", "__property_names", false), @@ -2567,6 +2581,8 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isanonymous", "isinstantiable", "iscloneable", + "isinternal", + "isuserdefined", "hasmethod", "hasproperty", "implementsinterface", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7cea35fffe..3f124b1c93 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7030,6 +7030,33 @@ echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e";'); assert_eq!(out, "aPFvrUite"); } +/// Verifies eval ReflectionClass origin predicates distinguish eval symbols from built-ins. +#[test] +fn test_eval_reflection_class_internal_user_defined_predicates() { + let out = compile_and_run( + r#"isInternal() ? "I" : "i"; + echo $r->isUserDefined() ? "U" : "u"; + echo ":"; +} +eval_reflect_origin("EvalOriginClass"); +eval_reflect_origin("EvalOriginIface"); +eval_reflect_origin("EvalOriginTrait"); +eval_reflect_origin("EvalOriginEnum"); +eval_reflect_origin("stdClass"); +eval_reflect_origin("ReflectionClass"); +eval_reflect_origin("Iterator");'); +"#, + ); + assert_eq!(out, "iU:iU:iU:iU:Iu:Iu:Iu:"); +} + /// Verifies eval ReflectionClass::newInstance constructs eval-declared classes. #[test] fn test_eval_reflection_class_new_instance_constructs_eval_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 274d0243d6..4fbb77a54a 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1300,6 +1300,35 @@ echo (new ReflectionClass(ReflectionClass::class))->isCloneable() ? "C" : "c"; assert_eq!(out, "aPFvrUiteSc"); } +/// Verifies that `ReflectionClass::isInternal()` and `isUserDefined()` report +/// whether class-like metadata came from compiler built-ins or user code. +#[test] +fn test_reflection_class_internal_user_defined_predicates() { + let out = compile_and_run( + r#"isInternal() ? "I" : "i"; echo $class->isUserDefined() ? "U" : "u"; echo ":"; +$iface = new ReflectionClass(ReflectOriginIface::class); +echo $iface->isInternal() ? "I" : "i"; echo $iface->isUserDefined() ? "U" : "u"; echo ":"; +$trait = new ReflectionClass(ReflectOriginTrait::class); +echo $trait->isInternal() ? "I" : "i"; echo $trait->isUserDefined() ? "U" : "u"; echo ":"; +$enum = new ReflectionClass(ReflectOriginEnum::class); +echo $enum->isInternal() ? "I" : "i"; echo $enum->isUserDefined() ? "U" : "u"; echo ":"; +$std = new ReflectionClass(stdClass::class); +echo $std->isInternal() ? "I" : "i"; echo $std->isUserDefined() ? "U" : "u"; echo ":"; +$reflection = new ReflectionClass(ReflectionClass::class); +echo $reflection->isInternal() ? "I" : "i"; echo $reflection->isUserDefined() ? "U" : "u"; echo ":"; +$iterator = new ReflectionClass(Iterator::class); +echo $iterator->isInternal() ? "I" : "i"; echo $iterator->isUserDefined() ? "U" : "u"; echo ":"; +"#, + ); + assert_eq!(out, "iU:iU:iU:iU:Iu:Iu:Iu:"); +} + /// Verifies that `ReflectionClass::hasMethod()`, `hasProperty()`, and /// `hasConstant()` report PHP-visible members for static class-like metadata. #[test] From 0e14cda2ff9edddc1fd86429e38e610f03fc72d0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 19:45:18 +0200 Subject: [PATCH 0438/1208] Expose ReflectionClass iterable predicate --- .../elephc-eval/src/interpreter/reflection.rs | 69 +++++++++++++++++++ .../tests/builtins_class_metadata.rs | 40 +++++++++++ .../interpreter/tests/support/object_ops.rs | 15 +++- docs/php/classes.md | 3 +- docs/php/eval.md | 3 + src/codegen/eval_reflection_owner_helpers.rs | 22 ++++++ src/codegen/lower_inst/objects/reflection.rs | 22 ++++++ src/types/checker/builtin_types/reflection.rs | 10 +++ tests/codegen/eval.rs | 37 ++++++++++ tests/codegen/oop/attributes.rs | 38 ++++++++++ 10 files changed, 257 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index b18f9b5089..bd351e843c 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -22,6 +22,7 @@ const EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE: u64 = 64; const EVAL_REFLECTION_CLASS_FLAG_CLONEABLE: u64 = 128; const EVAL_REFLECTION_CLASS_FLAG_INTERNAL: u64 = 256; const EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED: u64 = 512; +const EVAL_REFLECTION_CLASS_FLAG_ITERABLE: u64 = 1024; const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; @@ -632,6 +633,9 @@ fn eval_reflection_class_new( if is_enum { flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM; } + if eval_reflection_builtin_class_is_iterable(&class_name) { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, class_name.trim_start_matches('\\'), @@ -1531,6 +1535,9 @@ fn eval_reflection_class_like_attributes( if eval_reflection_class_is_cloneable(class, is_enum, context) { flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; } + if eval_reflection_class_is_iterable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } let modifiers = eval_reflection_class_modifiers( class.is_final(), class.is_abstract(), @@ -1634,6 +1641,68 @@ fn eval_reflection_class_is_cloneable( .unwrap_or(true) } +/// Returns PHP's `ReflectionClass::isIterable()` value for eval class metadata. +fn eval_reflection_class_is_iterable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_interface_names(class.name()) + .iter() + .any(|name| { + name.eq_ignore_ascii_case("Iterator") || name.eq_ignore_ascii_case("IteratorAggregate") + }) +} + +/// Returns PHP's `ReflectionClass::isIterable()` value for compiler-injected class names. +fn eval_reflection_builtin_class_is_iterable(class_name: &str) -> bool { + matches!( + class_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str(), + "__elephcappenditeratorarrayiterator" + | "appenditerator" + | "arrayiterator" + | "arrayobject" + | "cachingiterator" + | "callbackfilteriterator" + | "directoryiterator" + | "emptyiterator" + | "filesystemiterator" + | "generator" + | "globiterator" + | "infiniteiterator" + | "internaliterator" + | "iteratoriterator" + | "limititerator" + | "multipleiterator" + | "norewinditerator" + | "parentiterator" + | "recursivearrayiterator" + | "recursivecachingiterator" + | "recursivecallbackfilteriterator" + | "recursivedirectoryiterator" + | "recursiveiteratoriterator" + | "recursiveregexiterator" + | "regexiterator" + | "spldoublylinkedlist" + | "splfixedarray" + | "splfileobject" + | "splmaxheap" + | "splminheap" + | "splobjectstorage" + | "splpriorityqueue" + | "splqueue" + | "splstack" + | "spltempfileobject" + ) +} + /// Returns whether one reflected class-like name belongs to compiler-injected metadata. fn eval_reflection_class_like_is_internal(class_name: &str) -> bool { let trimmed = class_name.trim_start_matches('\\'); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 94a6deac90..4f51c8d621 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -641,6 +641,46 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::isIterable reports eval Traversable-compatible class metadata. +#[test] +fn execute_program_reflects_eval_class_iterable_predicate() { + let program = parse_fragment( + br#"class EvalIterablePlain {} +abstract class EvalIterableAbstract implements Iterator {} +interface EvalIterableIface extends Iterator {} +trait EvalIterableTrait {} +enum EvalIterableEnum { case Ready; } +class EvalIterableIterator implements Iterator { + public function current() { return null; } + public function key() { return null; } + public function next() {} + public function valid() { return false; } + public function rewind() {} +} +class EvalIterableAggregate implements IteratorAggregate { + public function getIterator() { return $this; } +} +echo (new ReflectionClass("EvalIterablePlain"))->isIterable() ? "P" : "p"; +$iter = new ReflectionClass("EvalIterableIterator"); +echo $iter->isIterable() ? "I" : "i"; +echo $iter->isIterateable() ? "A" : "a"; +echo (new ReflectionClass("EvalIterableAggregate"))->isIterable() ? "G" : "g"; +echo (new ReflectionClass("EvalIterableAbstract"))->isIterable() ? "B" : "b"; +echo (new ReflectionClass("EvalIterableIface"))->isIterable() ? "F" : "f"; +echo (new ReflectionClass("EvalIterableEnum"))->isIterable() ? "E" : "e"; +echo (new ReflectionClass("EvalIterableTrait"))->isIterable() ? "H" : "h"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "pIAGbfeh"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass origin predicates report eval class-like symbols as user-defined. #[test] fn execute_program_reflects_eval_class_origin_predicates() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 217d529deb..5d369a5c76 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -165,6 +165,10 @@ impl FakeOps { Self::object_property(&properties, "__is_cloneable") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isiterable" | "isiterateable") if args.is_empty() => { + Self::object_property(&properties, "__is_iterable") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "isinternal") if args.is_empty() => { Self::object_property(&properties, "__is_internal") .map_or_else(|| self.bool_value(false), Ok) @@ -543,6 +547,7 @@ impl FakeOps { let is_cloneable = self.bool_value((flags & 128) != 0)?; let is_internal = self.bool_value((flags & 256) != 0)?; let is_user_defined = self.bool_value((flags & 512) != 0)?; + let is_iterable = self.bool_value((flags & 1024) != 0)?; let is_anonymous = self.bool_value(false)?; let modifiers_cell = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; @@ -564,6 +569,7 @@ impl FakeOps { properties.push(("__is_anonymous".to_string(), is_anonymous)); properties.push(("__is_instantiable".to_string(), is_instantiable)); properties.push(("__is_cloneable".to_string(), is_cloneable)); + properties.push(("__is_iterable".to_string(), is_iterable)); properties.push(("__is_internal".to_string(), is_internal)); properties.push(("__is_user_defined".to_string(), is_user_defined)); properties.push(("__modifiers".to_string(), modifiers_cell)); @@ -846,7 +852,14 @@ impl FakeOps { } /// Reports one fake AOT interface for eval `interface_exists` unit tests. pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownInterface")) + Ok([ + "KnownInterface", + "Iterator", + "IteratorAggregate", + "Traversable", + ] + .iter() + .any(|known| name.eq_ignore_ascii_case(known))) } /// Reports one fake AOT trait for eval `trait_exists` unit tests. pub(super) fn runtime_trait_exists(&mut self, name: &str) -> Result { diff --git a/docs/php/classes.md b/docs/php/classes.md index 582a586922..c5f12af7ef 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1158,6 +1158,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isAnonymous()` | `new ReflectionClass($class_name_or_object)` | Return whether the reflected class is an anonymous class | | `ReflectionClass::isInstantiable()` | `new ReflectionClass($class_name)` | Return whether the reflected symbol is a concrete class with no constructor or a public constructor | | `ReflectionClass::isCloneable()` | `new ReflectionClass($class_name)` | Return whether the reflected symbol is a concrete cloneable class with no `__clone()` or a public `__clone()` | +| `ReflectionClass::isIterable()` / `isIterateable()` | `new ReflectionClass($class_name)` | Return whether the reflected symbol is a concrete `Iterator` or `IteratorAggregate` class | | `ReflectionClass::isInternal()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol is compiler-injected builtin metadata | | `ReflectionClass::isUserDefined()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol comes from user source | | `ReflectionClass::getModifiers()` | `new ReflectionClass($class_name)` | Return PHP's `ReflectionClass::IS_*` modifier bitmask for final, explicit abstract, and readonly class-like metadata | @@ -1311,7 +1312,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index 839485d31d..07e8ac6b4d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -170,6 +170,9 @@ named class-like symbols. `ReflectionClass::isInstantiable()` reports whether eval class-like metadata describes a concrete class with no constructor or a public constructor. `ReflectionClass::isCloneable()` reports whether eval class metadata describes a concrete class with no `__clone()` or a public `__clone()`. +`ReflectionClass::isIterable()` and `isIterateable()` report whether eval or +generated class metadata describes a concrete `Iterator` or `IteratorAggregate` +class. `ReflectionClass::isInternal()` and `isUserDefined()` distinguish compiler-injected class-like metadata from eval-declared or generated user-defined class-like symbols. diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index bd2dcd80c8..12599cb46e 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -68,6 +68,8 @@ struct ReflectionOwnerLayout { is_instantiable_hi: Option, is_cloneable_lo: Option, is_cloneable_hi: Option, + is_iterable_lo: Option, + is_iterable_hi: Option, is_internal_lo: Option, is_internal_hi: Option, is_user_defined_lo: Option, @@ -229,6 +231,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option bool { + if info.is_abstract || is_enum { + return false; + } + info.interfaces + .iter() + .any(|name| name == "Iterator" || name == "IteratorAggregate") +} + /// Returns whether a builtin's object layout is outside ordinary declared slots. fn reflection_class_has_runtime_managed_storage(class_name: &str) -> bool { let key = php_symbol_key(class_name); @@ -3118,6 +3139,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { is_anonymous: false, is_instantiable: false, is_cloneable: false, + is_iterable: false, modifiers: 0, member_flags: ReflectionMemberFlags::default(), } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 3a8d7346b8..2db3136368 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -896,6 +896,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(bool_type()), false_bool(), ), + builtin_property( + "__is_iterable", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), builtin_property( "__is_internal", Visibility::Private, @@ -1041,6 +1047,8 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_bool_method("isAnonymous", "__is_anonymous"), builtin_reflection_class_bool_method("isInstantiable", "__is_instantiable"), builtin_reflection_class_bool_method("isCloneable", "__is_cloneable"), + builtin_reflection_class_bool_method("isIterable", "__is_iterable"), + builtin_reflection_class_bool_method("isIterateable", "__is_iterable"), builtin_reflection_class_bool_method("isInternal", "__is_internal"), builtin_reflection_class_bool_method("isUserDefined", "__is_user_defined"), builtin_reflection_class_int_method("getModifiers", "__modifiers"), @@ -2581,6 +2589,8 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isanonymous", "isinstantiable", "iscloneable", + "isiterable", + "isiterateable", "isinternal", "isuserdefined", "hasmethod", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3f124b1c93..705658ec90 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7030,6 +7030,43 @@ echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e";'); assert_eq!(out, "aPFvrUite"); } +/// Verifies eval ReflectionClass::isIterable reports eval and builtin class metadata. +#[test] +fn test_eval_reflection_class_iterable_predicate() { + let out = compile_and_run( + r#"isIterable() ? "P" : "p"; +$iter = new ReflectionClass("EvalIterableIterator"); +echo $iter->isIterable() ? "I" : "i"; +echo $iter->isIterateable() ? "A" : "a"; +echo (new ReflectionClass("EvalIterableAggregate"))->isIterable() ? "G" : "g"; +echo (new ReflectionClass("EvalIterableAbstract"))->isIterable() ? "B" : "b"; +echo (new ReflectionClass("EvalIterableIface"))->isIterable() ? "F" : "f"; +echo (new ReflectionClass("Iterator"))->isIterable() ? "T" : "t"; +echo (new ReflectionClass("ArrayIterator"))->isIterable() ? "R" : "r"; +echo (new ReflectionClass("stdClass"))->isIterable() ? "S" : "s"; +echo (new ReflectionClass("EvalIterableEnum"))->isIterable() ? "E" : "e"; +echo (new ReflectionClass("EvalIterableTrait"))->isIterable() ? "H" : "h";'); +"#, + ); + assert_eq!(out, "pIAGbftRseh"); +} + /// Verifies eval ReflectionClass origin predicates distinguish eval symbols from built-ins. #[test] fn test_eval_reflection_class_internal_user_defined_predicates() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 4fbb77a54a..8c1a4b32c3 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1300,6 +1300,44 @@ echo (new ReflectionClass(ReflectionClass::class))->isCloneable() ? "C" : "c"; assert_eq!(out, "aPFvrUiteSc"); } +/// Verifies that `ReflectionClass::isIterable()` and its historical alias +/// report PHP Traversable-compatible class metadata. +#[test] +fn test_reflection_class_is_iterable() { + let out = compile_and_run( + r#"isIterable() ? "P" : "p"; +$iter = new ReflectionClass(ReflectIterableIterator::class); +echo $iter->isIterable() ? "I" : "i"; +echo $iter->isIterateable() ? "A" : "a"; +echo (new ReflectionClass(ReflectIterableAggregate::class))->isIterable() ? "G" : "g"; +echo (new ReflectionClass(ReflectIterableAbstract::class))->isIterable() ? "B" : "b"; +echo (new ReflectionClass(ReflectIterableIface::class))->isIterable() ? "F" : "f"; +echo (new ReflectionClass(Iterator::class))->isIterable() ? "T" : "t"; +echo (new ReflectionClass(ArrayIterator::class))->isIterable() ? "R" : "r"; +echo (new ReflectionClass(stdClass::class))->isIterable() ? "S" : "s"; +echo (new ReflectionClass(ReflectIterableEnum::class))->isIterable() ? "E" : "e"; +echo (new ReflectionClass(ReflectIterableTrait::class))->isIterable() ? "H" : "h"; +"#, + ); + assert_eq!(out, "pIAGbftRseh"); +} + /// Verifies that `ReflectionClass::isInternal()` and `isUserDefined()` report /// whether class-like metadata came from compiler built-ins or user code. #[test] From 28de1dc5b865f7b02403af5884c4446ba96aac45 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 20:11:17 +0200 Subject: [PATCH 0439/1208] Expose ReflectionProperty promoted predicate --- crates/elephc-eval/src/eval_ir.rs | 7 + .../elephc-eval/src/interpreter/reflection.rs | 66 ++++++++ .../src/interpreter/runtime_ops.rs | 7 + .../interpreter/tests/support/object_ops.rs | 21 +++ .../interpreter/tests/support/runtime_ops.rs | 8 + .../elephc-eval/src/runtime_hooks/externs.rs | 6 + crates/elephc-eval/src/runtime_hooks/ops.rs | 17 ++ docs/php/classes.md | 3 +- docs/php/eval.md | 7 +- src/codegen/eval_reflection_owner_helpers.rs | 22 +++ src/codegen/lower_inst/objects/reflection.rs | 48 ++++-- src/codegen_support/runtime/data/user.rs | 145 ++++++++++++++++++ src/codegen_support/runtime/eval_bridge.rs | 135 +++++++++++++++- src/ir_lower/tests/exhaustive.rs | 2 + src/optimize/fold/expr.rs | 1 + src/optimize/propagate/stmt/declarations.rs | 1 + src/optimize/tests/fold.rs | 1 + src/parser/ast/oop.rs | 2 + src/parser/stmt/oop/body.rs | 2 + src/parser/stmt/oop/method_params.rs | 1 + .../checker/builtin_spl_classes/common.rs | 1 + src/types/checker/builtin_types/exception.rs | 2 + src/types/checker/builtin_types/reflection.rs | 19 ++- src/types/checker/builtin_user_filter.rs | 1 + .../checker/schema/classes/properties.rs | 15 ++ src/types/checker/schema/classes/state.rs | 5 + src/types/checker/schema/enums.rs | 1 + src/types/schema.rs | 1 + tests/codegen/eval.rs | 36 +++++ tests/codegen/oop/attributes.rs | 38 +++++ 30 files changed, 607 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 7a4c957d8b..24a35de7e8 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1257,6 +1257,7 @@ pub struct EvalClassProperty { is_static: bool, is_final: bool, is_readonly: bool, + is_promoted: bool, is_abstract: bool, has_get_hook: bool, has_set_hook: bool, @@ -1325,6 +1326,7 @@ impl EvalClassProperty { is_static, is_final, is_readonly, + is_promoted: false, is_abstract: false, has_get_hook: false, has_set_hook: false, @@ -1400,6 +1402,11 @@ impl EvalClassProperty { self.is_readonly } + /// Returns whether this property came from constructor property promotion. + pub const fn is_promoted(&self) -> bool { + self.is_promoted + } + /// Returns whether this property is an abstract property hook contract. pub const fn is_abstract(&self) -> bool { self.is_abstract diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index bd351e843c..792dab80a2 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -32,6 +32,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; +const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -62,6 +63,7 @@ struct EvalReflectionMemberMetadata { is_final: bool, is_abstract: bool, is_readonly: bool, + is_promoted: bool, modifiers: u64, type_metadata: Option, default_value: Option, @@ -720,6 +722,20 @@ fn eval_reflection_property_new( )?; let class_name = eval_reflection_string_arg(args[0], values)?; if !eval_reflection_class_like_exists(&class_name, context) { + let property_name = eval_reflection_string_arg(args[1], values)?; + let runtime_class_name = class_name.trim_start_matches('\\'); + if let Some(flags) = values.reflection_property_flags(runtime_class_name, &property_name)? { + let property = + eval_reflection_aot_property_metadata(runtime_class_name, flags, &property_name); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &property_name, + &property, + context, + values, + ) + .map(Some); + } return Ok(None); } let property_name = eval_reflection_string_arg(args[1], values)?; @@ -735,6 +751,47 @@ fn eval_reflection_property_new( .map(Some) } +/// Converts AOT property flag metadata into the eval ReflectionProperty shape. +fn eval_reflection_aot_property_metadata( + class_name: &str, + flags: u64, + _property_name: &str, +) -> EvalReflectionMemberMetadata { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let is_final = flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0; + let is_abstract = flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0; + let is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + attributes: Vec::new(), + visibility, + is_static, + is_final, + is_abstract, + is_readonly, + is_promoted: flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED != 0, + modifiers: eval_reflection_property_modifiers( + visibility, + is_static, + is_final, + is_abstract, + is_readonly, + false, + ), + type_metadata: None, + default_value: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} + /// Builds an eval-backed `ReflectionClassConstant` object for a class constant or enum case. fn eval_reflection_class_constant_new( evaluated_args: Vec, @@ -1437,6 +1494,9 @@ fn eval_reflection_member_object_result( if member.default_value.is_some() { flags |= EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE; } + if member.is_promoted { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PROMOTED; + } let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { member.required_parameter_count as u64 } else { @@ -2042,6 +2102,7 @@ fn eval_reflection_method_metadata( is_final: method.is_final(), is_abstract: method.is_abstract(), is_readonly: false, + is_promoted: false, modifiers: eval_reflection_method_modifiers( method.visibility(), method.is_static(), @@ -2098,6 +2159,7 @@ fn eval_reflection_method_metadata( is_final: false, is_abstract: true, is_readonly: false, + is_promoted: false, modifiers: eval_reflection_method_modifiers( EvalVisibility::Public, method.is_static(), @@ -2154,6 +2216,7 @@ fn eval_reflection_method_metadata( is_final: method.is_final(), is_abstract: method.is_abstract(), is_readonly: false, + is_promoted: false, modifiers: eval_reflection_method_modifiers( method.visibility(), method.is_static(), @@ -2187,6 +2250,7 @@ fn eval_reflection_property_metadata( is_final: property.is_final(), is_abstract: property.is_abstract(), is_readonly: property.is_readonly(), + is_promoted: property.is_promoted(), modifiers: eval_reflection_property_modifiers( property.visibility(), property.is_static(), @@ -2218,6 +2282,7 @@ fn eval_reflection_property_metadata( is_final: false, is_abstract: true, is_readonly: false, + is_promoted: false, modifiers: eval_reflection_property_modifiers( EvalVisibility::Public, false, @@ -2249,6 +2314,7 @@ fn eval_reflection_property_metadata( is_final: property.is_final(), is_abstract: property.is_abstract(), is_readonly: property.is_readonly(), + is_promoted: property.is_promoted(), modifiers: eval_reflection_property_modifiers( property.visibility(), property.is_static(), diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 2bc773776d..93d195e656 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -133,6 +133,13 @@ pub trait RuntimeValueOps { constructor: RuntimeCellHandle, ) -> Result; + /// Returns generated AOT ReflectionProperty flags for a class/property pair. + fn reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus>; + /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 5d369a5c76..f8c3363fd3 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -18,6 +18,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; +const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -153,6 +154,10 @@ impl FakeOps { Self::object_property(&properties, "__is_readonly") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "ispromoted") if args.is_empty() => { + Self::object_property(&properties, "__is_promoted") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "isanonymous") if args.is_empty() => { Self::object_property(&properties, "__is_anonymous") .map_or_else(|| self.bool_value(false), Ok) @@ -608,12 +613,15 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY) != 0)?; let has_default_value = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE) != 0)?; + let is_promoted = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED) != 0)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_readonly".to_string(), is_readonly)); properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__type".to_string(), method_objects)); properties.push(("__has_default_value".to_string(), has_default_value)); + properties.push(("__is_promoted".to_string(), is_promoted)); properties.push(("__default_value".to_string(), property_objects)); } } @@ -850,6 +858,19 @@ impl FakeOps { pub(super) fn runtime_class_exists(&mut self, name: &str) -> Result { Ok(name.eq_ignore_ascii_case("KnownClass")) } + /// Reports fake generated AOT ReflectionProperty flags for eval metadata unit tests. + pub(super) fn runtime_reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + if class_name.eq_ignore_ascii_case("KnownClass") && property_name == "promoted" { + return Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_PUBLIC | EVAL_REFLECTION_MEMBER_FLAG_PROMOTED, + )); + } + Ok(None) + } /// Reports one fake AOT interface for eval `interface_exists` unit tests. pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { Ok([ diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 902ef85770..50b8a0bafe 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -160,6 +160,14 @@ impl RuntimeValueOps for FakeOps { constructor, ) } + /// Reports fake generated AOT ReflectionProperty flags for metadata bridge tests. + fn reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_property_flags(class_name, property_name) + } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { self.runtime_new_object(_class_name) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 3553f02e52..61663827b4 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -93,6 +93,12 @@ unsafe extern "C" { backing_value: *mut RuntimeCell, constructor: *mut RuntimeCell, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_property_flags( + class_ptr: *const u8, + class_len: u64, + property_ptr: *const u8, + property_len: u64, + ) -> u64; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, name_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 9ddd13abc3..6f8c7bc591 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -241,6 +241,23 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns generated AOT ReflectionProperty flags, or `None` when no row matches. + fn reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_property_flags( + class_name.as_ptr(), + class_name.len() as u64, + property_name.as_ptr(), + property_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { let object = Self::handle(unsafe { diff --git a/docs/php/classes.md b/docs/php/classes.md index c5f12af7ef..7a5dc30544 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1226,6 +1226,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isFinal()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is final | | `ReflectionProperty::isAbstract()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is abstract | | `ReflectionProperty::isReadOnly()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is readonly | +| `ReflectionProperty::isPromoted()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property came from constructor property promotion | | `ReflectionProperty::getModifiers()` | `new ReflectionProperty($class_name, $property_name)` | Return PHP's `ReflectionProperty::IS_*` visibility/static/finality/abstract/readonly/virtual/set-visibility bitmask | | `ReflectionProperty::isDefault()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property is declared/default rather than dynamic | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | @@ -1312,7 +1313,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index 07e8ac6b4d..56aeebd17a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -257,9 +257,12 @@ parameters after method execution, write back mutated `&...$items` elements when the variadic container itself is not rebound, and are reported through `ReflectionParameter::isPassedByReference()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, -`isFinal()`, `isAbstract()`, `isReadOnly()`, `isDefault()`, and +`isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isDefault()`, and `getModifiers()` report eval property metadata with PHP-compatible -`ReflectionProperty::IS_*` constants for the bitmask. +`ReflectionProperty::IS_*` constants for the bitmask. `isPromoted()` reports +generated/AOT promoted-property metadata; eval-declared properties currently +report `false` because eval fragments do not yet parse constructor promotion +syntax. `ReflectionProperty::hasType()` and `getType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained a supported declared type. diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 12599cb46e..4d1649b190 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -104,6 +104,8 @@ struct ReflectionOwnerLayout { parameter_type_hi: Option, has_default_value_lo: Option, has_default_value_hi: Option, + is_promoted_lo: Option, + is_promoted_hi: Option, default_value_lo: Option, default_value_hi: Option, declaring_function_lo: Option, @@ -252,6 +254,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option ReflectionOwnerMetadata { let is_final = metadata.is_final; let modifiers = reflection_class_constant_modifiers(&metadata.visibility, is_final); - let member_flags = reflection_member_flags(false, &metadata.visibility, is_final, false, false); + let member_flags = + reflection_member_flags(false, &metadata.visibility, is_final, false, false, false); ReflectionOwnerMetadata { reflected_name: Some(reflected_name), attr_names: metadata.attr_names, @@ -1983,7 +1985,7 @@ fn push_unique_constant_reflection_member( attr_args, constant_value: Some(value), is_enum_case, - flags: reflection_member_flags(false, &visibility, is_final, false, false), + flags: reflection_member_flags(false, &visibility, is_final, false, false, false), modifiers: reflection_class_constant_modifiers(&visibility, is_final), type_metadata: None, default_value: None, @@ -2066,6 +2068,7 @@ fn reflection_method_member_flags( info.final_methods.contains(method_key), !info.method_impl_classes.contains_key(method_key), false, + false, )); } if info.static_methods.contains_key(method_key) { @@ -2079,6 +2082,7 @@ fn reflection_method_member_flags( info.final_static_methods.contains(method_key), !info.static_method_impl_classes.contains_key(method_key), false, + false, )); } None @@ -2104,6 +2108,7 @@ fn reflection_property_member_flags( info.final_properties.contains(property_name), info.abstract_properties.contains(property_name), info.readonly_properties.contains(property_name), + info.promoted_properties.contains(property_name), )); } if info @@ -2121,6 +2126,7 @@ fn reflection_property_member_flags( info.final_static_properties.contains(property_name), false, false, + false, )); } None @@ -2222,7 +2228,7 @@ fn reflection_interface_method_member( .cloned() .unwrap_or_else(|| interface_name.to_string()); let required_parameter_count = reflection_required_parameter_count(sig); - let flags = reflection_member_flags(is_static, &Visibility::Public, false, true, false); + let flags = reflection_member_flags(is_static, &Visibility::Public, false, true, false, false); let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), declaring_class_name: Some(declaring_class_name.clone()), @@ -2278,6 +2284,7 @@ fn reflection_trait_method_member( info.is_final, info.is_abstract, false, + false, ); let required_parameter_count = reflection_required_parameter_count(&info.signature); let declaring_function = ReflectionDeclaringFunctionMember::Method { @@ -2332,7 +2339,7 @@ fn reflection_class_property_member( ) -> Option { let flags = reflection_property_member_flags(info, property_name).or_else(|| { (is_reflection_enum(ctx, class_name) && property_name == "name").then_some( - reflection_member_flags(false, &Visibility::Public, false, false, true), + reflection_member_flags(false, &Visibility::Public, false, false, true, false), ) })?; let type_metadata = reflection_property_type_metadata(info, property_name); @@ -2427,13 +2434,21 @@ fn default_method_members( attr_args: Vec::new(), constant_value: None, is_enum_case: false, - flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), + flags: reflection_member_flags( + false, + &Visibility::Public, + false, + is_interface, + false, + false, + ), modifiers: reflection_method_modifiers_from_flags(reflection_member_flags( false, &Visibility::Public, false, is_interface, false, + false, )), type_metadata: None, default_value: None, @@ -2458,7 +2473,14 @@ fn default_property_members( attr_args: Vec::new(), constant_value: None, is_enum_case: false, - flags: reflection_member_flags(false, &Visibility::Public, false, is_interface, false), + flags: reflection_member_flags( + false, + &Visibility::Public, + false, + is_interface, + false, + false, + ), modifiers: reflection_property_modifiers( &Visibility::Public, false, @@ -2701,6 +2723,7 @@ fn reflection_member_flags( is_final: bool, is_abstract: bool, is_readonly: bool, + is_promoted: bool, ) -> ReflectionMemberFlags { ReflectionMemberFlags { is_static, @@ -2710,6 +2733,7 @@ fn reflection_member_flags( is_final, is_abstract, is_readonly, + is_promoted, } } @@ -4495,6 +4519,12 @@ fn emit_reflection_member_flag_properties( "__is_readonly", flags.is_readonly, )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_promoted", + flags.is_promoted, + )?; } "ReflectionClassConstant" => { emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 90c20fbecc..1c6507adbb 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -19,6 +19,16 @@ use crate::types::{ClassInfo, EnumInfo, FunctionSig, InterfaceInfo, PhpType}; use super::instanceof::{escaped_ascii, escaped_bytes}; +const EVAL_REFLECTION_PROPERTY_FLAG_STATIC: u64 = 1; +const EVAL_REFLECTION_PROPERTY_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_PROPERTY_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_PROPERTY_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_PROPERTY_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_PROPERTY_FLAG_READONLY: u64 = 64; +const EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE: u64 = 256; +const EVAL_REFLECTION_PROPERTY_FLAG_PROMOTED: u64 = 512; + /// Emit the user-dependent data section — globals, statics, class metadata. /// This changes per program and cannot be cached. pub(crate) fn emit_runtime_data_user( @@ -453,6 +463,8 @@ pub(crate) fn emit_runtime_data_user( emit_static_callable_method_data(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); emit_classes_by_name_table(&mut out, &sorted_classes); + out.push_str(".p2align 3\n"); + emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); // -- class-level PHP 8 attribute metadata table -- // Per-class layout: count followed by (name_ptr, name_len) pairs. @@ -937,6 +949,138 @@ fn emit_name_lookup_data( } } +/// Emits AOT property flag rows consumed by eval ReflectionProperty metadata probes. +fn emit_eval_reflection_property_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + for (slot, (property_name, _)) in class_info.properties.iter().enumerate() { + let flags = eval_reflection_instance_property_flags(class_info, slot, property_name); + let class_label = format!("_eval_reflection_property_class_{}", index); + let property_label = format!("_eval_reflection_property_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + property_label, + escaped_ascii(property_name) + )); + entries.push(( + class_label, + class_name.len(), + property_label, + property_name.len(), + flags, + )); + index += 1; + } + for (slot, (property_name, _)) in class_info.static_properties.iter().enumerate() { + let flags = eval_reflection_static_property_flags(class_info, slot, property_name); + let class_label = format!("_eval_reflection_property_class_{}", index); + let property_label = format!("_eval_reflection_property_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + property_label, + escaped_ascii(property_name) + )); + entries.push(( + class_label, + class_name.len(), + property_label, + property_name.len(), + flags, + )); + index += 1; + } + } + + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_property_count\n_eval_reflection_property_count:\n"); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_properties\n_eval_reflection_properties:\n"); + for (class_label, class_len, property_label, property_len, flags) in entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", property_label)); + out.push_str(&format!(" .quad {}\n", property_len)); + out.push_str(&format!(" .quad {}\n", flags)); + } +} + +/// Returns eval ReflectionProperty bitflags for one instance property slot. +fn eval_reflection_instance_property_flags( + class_info: &ClassInfo, + slot: usize, + property_name: &str, +) -> u64 { + let visibility = class_info + .property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + let mut flags = eval_reflection_visibility_flags(visibility); + if class_info.final_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_FINAL; + } + if class_info.abstract_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_ABSTRACT; + } + if class_info.readonly_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_READONLY; + } + if class_info.promoted_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_PROMOTED; + } + if class_info.defaults.get(slot).is_some_and(Option::is_some) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE; + } + flags +} + +/// Returns eval ReflectionProperty bitflags for one static property slot. +fn eval_reflection_static_property_flags( + class_info: &ClassInfo, + slot: usize, + property_name: &str, +) -> u64 { + let visibility = class_info + .static_property_visibilities + .get(property_name) + .unwrap_or(&Visibility::Public); + let mut flags = + EVAL_REFLECTION_PROPERTY_FLAG_STATIC | eval_reflection_visibility_flags(visibility); + if class_info.final_static_properties.contains(property_name) { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_FINAL; + } + if class_info + .static_defaults + .get(slot) + .is_some_and(Option::is_some) + { + flags |= EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE; + } + flags +} + +/// Converts a property visibility into eval ReflectionProperty bitflags. +fn eval_reflection_visibility_flags(visibility: &Visibility) -> u64 { + match visibility { + Visibility::Public => EVAL_REFLECTION_PROPERTY_FLAG_PUBLIC, + Visibility::Protected => EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED, + Visibility::Private => EVAL_REFLECTION_PROPERTY_FLAG_PRIVATE, + } +} + /// Emits the callable-function name table and pointer table for user-defined functions. /// Each function name is emitted as an ASCII label; the pointer table references /// either the active variant symbol for polymorphic functions or zero. @@ -1417,6 +1561,7 @@ mod tests { readonly_properties: HashSet::new(), reference_properties: HashSet::new(), owned_reference_properties: HashSet::new(), + promoted_properties: HashSet::new(), property_reference_slots: Vec::new(), abstract_properties: HashSet::new(), abstract_property_hooks: HashMap::new(), diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index c965d2937f..aa225dc940 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -169,6 +169,8 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { "__elephc_eval_enum_exists", ); + emit_aarch64_eval_reflection_property_flags(emitter); + label_c_global(emitter, "__elephc_eval_value_is_a"); emitter.instruction("sub sp, sp, #64"); // reserve relation lookup state and preserve the Rust return address emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across runtime match helpers @@ -1582,6 +1584,8 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { "__elephc_eval_enum_exists_x86", ); + emit_x86_64_eval_reflection_property_flags(emitter); + label_c_global(emitter, "__elephc_eval_value_is_a"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime match helpers emitter.instruction("mov rbp, rsp"); // establish a stable is-a relation frame pointer @@ -2959,7 +2963,136 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell } -/// Emits an AArch64 eval wrapper that scans one `(name_ptr, name_len)` metadata table. +/// Emits the ARM64 eval hook that returns AOT ReflectionProperty predicate flags. +fn emit_aarch64_eval_reflection_property_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_property_flags"); + emitter.instruction("sub sp, sp, #96"); // reserve scan state across runtime string comparisons + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_property_count"); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection-property row count + emitter.instruction("cbz x9, __elephc_eval_reflection_property_flags_miss"); // an empty table cannot contain the requested property + emitter.instruction("str x9, [sp, #32]"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_properties"); + emitter.instruction("str x10, [sp, #40]"); // save the current property metadata row + emitter.instruction("mov x11, #0"); // start scanning at property metadata row zero + emitter.label("__elephc_eval_reflection_property_flags_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the property metadata row count + emitter.instruction("cmp x11, x9"); // have all property metadata rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_property_flags_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_property_flags_skip"); // length mismatch means the class cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_property_flags_skip"); // class mismatch means the row cannot match + emitter.instruction("ldr x10, [sp, #40]"); // reload the current row for the property-name compare + emitter.instruction("ldr x12, [x10, #24]"); // load the stored property-name length + emitter.instruction("ldr x2, [sp, #24]"); // reload the requested property-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested property-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_property_flags_skip"); // length mismatch means the property cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the property-name compare + emitter.instruction("ldr x1, [sp, #16]"); // pass the requested property-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // pass the requested property-name length + emitter.instruction("ldr x3, [x10, #16]"); // pass the stored property-name pointer + emitter.instruction("mov x4, x12"); // pass the stored property-name length + emitter.instruction("bl __rt_str_eq"); // compare property names with PHP case-sensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the property-name compare + emitter.instruction("cmp x0, #0"); // did the requested property name match this row? + emitter.instruction("b.eq __elephc_eval_reflection_property_flags_skip"); // property mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #40]"); // reload the matched property metadata row + emitter.instruction("ldr x0, [x10, #32]"); // return the row's ReflectionProperty predicate flags + emitter.instruction("b __elephc_eval_reflection_property_flags_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_property_flags_skip"); + emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row + emitter.instruction("add x10, x10, #40"); // advance to the next 40-byte metadata row + emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_property_flags_loop"); // continue scanning property metadata rows + emitter.label("__elephc_eval_reflection_property_flags_miss"); + emitter.instruction("mov x0, #0"); // return zero when no AOT property metadata matched + emitter.label("__elephc_eval_reflection_property_flags_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the property metadata scan frame + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionProperty predicate flags. +fn emit_x86_64_eval_reflection_property_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_property_flags"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable scan frame pointer + emitter.instruction("sub rsp, 64"); // reserve scan state across runtime string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_property_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection-property row count + emitter.instruction("test r10, r10"); // is the property metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_property_flags_miss_x86"); // an empty table cannot contain the requested property + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_properties"); + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the current property metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at property metadata row zero + emitter.label("__elephc_eval_reflection_property_flags_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the property metadata row count + emitter.instruction("cmp r11, r10"); // have all property metadata rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_property_flags_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_property_flags_skip_x86"); // length mismatch means the class cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_property_flags_skip_x86"); // class mismatch means the row cannot match + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current row for the property-name compare + emitter.instruction("mov rcx, QWORD PTR [r10 + 24]"); // load the stored property-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // compare stored and requested property-name lengths + emitter.instruction("jne __elephc_eval_reflection_property_flags_skip_x86"); // length mismatch means the property cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the property-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // pass the requested property-name length + emitter.instruction("mov rdx, QWORD PTR [r10 + 16]"); // pass the stored property-name pointer + emitter.instruction("call __rt_str_eq"); // compare property names with PHP case-sensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the property-name compare + emitter.instruction("test rax, rax"); // did the requested property name match this row? + emitter.instruction("jz __elephc_eval_reflection_property_flags_skip_x86"); // property mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the matched property metadata row + emitter.instruction("mov rax, QWORD PTR [r10 + 32]"); // return the row's ReflectionProperty predicate flags + emitter.instruction("jmp __elephc_eval_reflection_property_flags_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_property_flags_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row + emitter.instruction("add r10, 40"); // advance to the next 40-byte metadata row + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_property_flags_loop_x86"); // continue scanning property metadata rows + emitter.label("__elephc_eval_reflection_property_flags_miss_x86"); + emitter.instruction("xor eax, eax"); // return zero when no AOT property metadata matched + emitter.label("__elephc_eval_reflection_property_flags_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits a class-like name membership scanner for ARM64 eval bridge hooks. fn emit_aarch64_eval_name_table_exists( emitter: &mut Emitter, exported_label: &str, diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index df7cf7b0e7..0366fa7a83 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -190,6 +190,7 @@ fn class_info(_class_name: &str) -> ClassInfo { readonly_properties: Default::default(), reference_properties: Default::default(), owned_reference_properties: Default::default(), + promoted_properties: Default::default(), property_reference_slots: Vec::new(), abstract_properties: Default::default(), abstract_property_hooks: HashMap::new(), @@ -368,6 +369,7 @@ fn lowers_every_stmt_variant_smoke() { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(int(0)), span: sp(), attributes: Vec::new(), diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index fd59f3115b..789a4c5a57 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -50,6 +50,7 @@ pub(in crate::optimize) fn fold_property(property: ClassProperty) -> ClassProper is_static: property.is_static, is_abstract: property.is_abstract, by_ref: property.by_ref, + is_promoted: property.is_promoted, default: property.default.map(fold_expr), span: property.span, attributes: property.attributes, diff --git a/src/optimize/propagate/stmt/declarations.rs b/src/optimize/propagate/stmt/declarations.rs index 1407c3a887..5c12e00dd4 100644 --- a/src/optimize/propagate/stmt/declarations.rs +++ b/src/optimize/propagate/stmt/declarations.rs @@ -48,6 +48,7 @@ pub(super) fn propagate_property(property: ClassProperty) -> ClassProperty { is_static: property.is_static, is_abstract: property.is_abstract, by_ref: property.by_ref, + is_promoted: property.is_promoted, default: property .default .map(|expr| propagate_expr(expr, &HashMap::new())), diff --git a/src/optimize/tests/fold.rs b/src/optimize/tests/fold.rs index 3ad4538d13..bdbdaf2e64 100644 --- a/src/optimize/tests/fold.rs +++ b/src/optimize/tests/fold.rs @@ -91,6 +91,7 @@ fn test_fold_string_concat_and_property_default() { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new( ExprKind::BinaryOp { left: Box::new(Expr::string_lit("hello ")), diff --git a/src/parser/ast/oop.rs b/src/parser/ast/oop.rs index 51c359914a..1f3134de7f 100644 --- a/src/parser/ast/oop.rs +++ b/src/parser/ast/oop.rs @@ -148,6 +148,7 @@ pub struct ClassProperty { pub is_static: bool, pub is_abstract: bool, pub by_ref: bool, + pub is_promoted: bool, pub default: Option, #[allow(dead_code)] // Used for error reporting in future phases pub span: Span, @@ -168,6 +169,7 @@ impl PartialEq for ClassProperty { && self.is_static == other.is_static && self.is_abstract == other.is_abstract && self.by_ref == other.by_ref + && self.is_promoted == other.is_promoted && self.default == other.default && self.attributes == other.attributes } diff --git a/src/parser/stmt/oop/body.rs b/src/parser/stmt/oop/body.rs index ce0c3cbb62..8edc73770b 100644 --- a/src/parser/stmt/oop/body.rs +++ b/src/parser/stmt/oop/body.rs @@ -397,6 +397,7 @@ pub(in crate::parser::stmt) fn parse_class_like_body( is_static: modifiers.is_static, is_abstract: modifiers.is_abstract, by_ref: false, + is_promoted: false, default, span: member_span, attributes: member_attributes, @@ -798,6 +799,7 @@ fn parse_interface_body( is_static: false, is_abstract: true, by_ref: false, + is_promoted: false, default: None, span: member_span, attributes: member_attributes, diff --git a/src/parser/stmt/oop/method_params.rs b/src/parser/stmt/oop/method_params.rs index 367c34dbb7..058a96d512 100644 --- a/src/parser/stmt/oop/method_params.rs +++ b/src/parser/stmt/oop/method_params.rs @@ -147,6 +147,7 @@ pub(super) fn parse_method_params( is_static: false, is_abstract: false, by_ref: is_ref, + is_promoted: true, // PHP keeps constructor-promotion defaults on the parameter, // not on the promoted property's default metadata. default: None, diff --git a/src/types/checker/builtin_spl_classes/common.rs b/src/types/checker/builtin_spl_classes/common.rs index 807a96733c..75fd60f84c 100644 --- a/src/types/checker/builtin_spl_classes/common.rs +++ b/src/types/checker/builtin_spl_classes/common.rs @@ -136,6 +136,7 @@ pub(super) fn storage_property_with_visibility( is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default, span: crate::span::Span::dummy(), attributes: Vec::new(), diff --git a/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index b5c16e22c1..eef802f3a5 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -31,6 +31,7 @@ pub(super) fn builtin_exception_message_property() -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new( ExprKind::StringLiteral(String::new()), crate::span::Span::dummy(), @@ -118,6 +119,7 @@ pub(super) fn builtin_exception_code_property() -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new( ExprKind::IntLiteral(0), crate::span::Span::dummy(), diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 2db3136368..93d12eba09 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -180,6 +180,7 @@ fn builtin_property( is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default, span: crate::span::Span::dummy(), attributes: Vec::new(), @@ -2334,6 +2335,12 @@ fn add_reflection_member_flag_methods( Some(bool_type()), false_bool(), )); + properties.push(builtin_property( + "__is_promoted", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); properties.push(builtin_property( "__default_value", Visibility::Private, @@ -2356,6 +2363,10 @@ fn add_reflection_member_flag_methods( "hasDefaultValue", "__has_default_value", )); + methods.push(builtin_reflection_class_bool_method( + "isPromoted", + "__is_promoted", + )); methods.push(builtin_reflection_constant_bool_method("isDefault", true)); methods.push(builtin_reflection_class_mixed_method( "getDefaultValue", @@ -2675,7 +2686,13 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionProperty" { - for method_name in ["isfinal", "isabstract", "isreadonly", "isdefault"] { + for method_name in [ + "isfinal", + "isabstract", + "isreadonly", + "isdefault", + "ispromoted", + ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; } diff --git a/src/types/checker/builtin_user_filter.rs b/src/types/checker/builtin_user_filter.rs index 748e7ca0ab..6bb12159b4 100644 --- a/src/types/checker/builtin_user_filter.rs +++ b/src/types/checker/builtin_user_filter.rs @@ -67,6 +67,7 @@ fn params_property() -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new(ExprKind::Null, Span::dummy())), span: Span::dummy(), attributes: Vec::new(), diff --git a/src/types/checker/schema/classes/properties.rs b/src/types/checker/schema/classes/properties.rs index 2c2cbe6cac..afd9ceb7f7 100644 --- a/src/types/checker/schema/classes/properties.rs +++ b/src/types/checker/schema/classes/properties.rs @@ -297,6 +297,11 @@ fn apply_instance_property( if prop.by_ref { state.reference_properties.insert(prop.name.clone()); } + if prop.is_promoted { + state.promoted_properties.insert(prop.name.clone()); + } else { + state.promoted_properties.remove(&prop.name); + } // Fresh declarations only ever add to `abstract_properties`. Concrete // declarations of a brand-new property never appear there in the first // place, so there is nothing to remove; the only path that clears a @@ -415,6 +420,11 @@ fn apply_instance_property_redeclaration( if prop.by_ref { state.reference_properties.insert(prop.name.clone()); } + if prop.is_promoted { + state.promoted_properties.insert(prop.name.clone()); + } else { + state.promoted_properties.remove(&prop.name); + } Ok(()) } @@ -493,6 +503,11 @@ fn replace_active_property_flags( } else { state.reference_properties.remove(&prop.name); } + if prop.is_promoted { + state.promoted_properties.insert(prop.name.clone()); + } else { + state.promoted_properties.remove(&prop.name); + } if prop.is_abstract { state.abstract_properties.insert(prop.name.clone()); let contract = build_property_contract(checker, &class.name, prop)?; diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index b2c48164c8..3295ba3e35 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -38,6 +38,7 @@ pub(super) struct ClassBuildState { pub(super) final_properties: HashSet, pub(super) readonly_properties: HashSet, pub(super) reference_properties: HashSet, + pub(super) promoted_properties: HashSet, pub(super) property_reference_slots: Vec, pub(super) abstract_properties: HashSet, pub(super) abstract_property_hooks: HashMap, @@ -172,6 +173,7 @@ impl ClassBuildState { readonly_properties: self.readonly_properties, reference_properties: self.reference_properties, owned_reference_properties: HashSet::new(), + promoted_properties: self.promoted_properties, property_reference_slots: self.property_reference_slots, abstract_properties: self.abstract_properties, abstract_property_hooks: self.abstract_property_hooks, @@ -421,6 +423,9 @@ impl ClassBuildState { if parent.reference_properties.contains(name) { self.reference_properties.insert(name.clone()); } + if parent.promoted_properties.contains(name) { + self.promoted_properties.insert(name.clone()); + } if parent.abstract_properties.contains(name) { self.abstract_properties.insert(name.clone()); } diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 0a4ea2701f..b35557cd73 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -456,6 +456,7 @@ pub(crate) fn insert_enum_metadata( readonly_properties, reference_properties, owned_reference_properties: HashSet::new(), + promoted_properties: HashSet::new(), property_reference_slots, abstract_properties: HashSet::new(), abstract_property_hooks: HashMap::new(), diff --git a/src/types/schema.rs b/src/types/schema.rs index bdd60d11c8..68eed670da 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -311,6 +311,7 @@ pub struct ClassInfo { /// caller). The object allocates a cell per such property at construction and releases /// it on destruction. pub owned_reference_properties: HashSet, + pub promoted_properties: HashSet, /// Per-layout-slot by-reference flags for instance properties. /// /// The name-keyed `reference_properties` map describes the currently diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 705658ec90..fab91820ff 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6572,6 +6572,42 @@ echo $classReadonlyProp->getModifiers();'); ); } +/// Verifies eval can observe AOT constructor-promotion metadata through +/// `ReflectionProperty::isPromoted()`. +#[test] +fn test_eval_reflection_property_is_promoted_for_aot_class() { + let out = compile_and_run_capture( + r#"isPromoted() ? "I" : "i"; +$root = new ReflectionProperty("\EvalAotPromotedBase", "id"); +echo $root->isPromoted() ? "I" : "i"; +$name = new ReflectionProperty("EvalAotPromotedBase", "name"); +echo $name->isPromoted() ? "N" : "n"; +$child = new ReflectionProperty("EvalAotPromotedChild", "id"); +echo $child->isPromoted() ? "C" : "c"; +$plain = new ReflectionProperty("EvalAotPromotedPlain", "id"); +echo $plain->isPromoted() ? "P" : "p"; +$static = new ReflectionProperty("EvalAotPromotedPlain", "count"); +echo $static->isPromoted() ? "S" : "s";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "IINCps"); +} + /// Verifies eval ReflectionMethod constructor/destructor predicates through the bridge. #[test] fn test_eval_reflection_method_reports_constructor_and_destructor() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 8c1a4b32c3..c234352cb6 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2022,6 +2022,44 @@ echo $classReadonlyProp->getModifiers(); ); } +/// Verifies that `ReflectionProperty::isPromoted()` reports constructor property +/// promotion metadata for direct, inherited, and listed reflected properties. +#[test] +fn test_reflection_property_is_promoted() { + let out = compile_and_run( + r#"isPromoted() ? "I" : "i"; +$name = new ReflectionProperty(ReflectPromotedBase::class, "name"); +echo $name->isPromoted() ? "N" : "n"; +$child = new ReflectionProperty(ReflectPromotedChild::class, "id"); +echo $child->isPromoted() ? "C" : "c"; +$plain = new ReflectionProperty(ReflectPromotedPlain::class, "id"); +echo $plain->isPromoted() ? "P" : "p"; +$static = new ReflectionProperty(ReflectPromotedPlain::class, "count"); +echo $static->isPromoted() ? "S" : "s"; +echo ":"; +foreach ((new ReflectionClass(ReflectPromotedBase::class))->getProperties() as $property) { + if ($property->getName() === "id") { + echo $property->isPromoted() ? "L" : "l"; + } + if ($property->getName() === "name") { + echo $property->isPromoted() ? "M" : "m"; + } +} +"#, + ); + assert_eq!(out, "INCps:LM"); +} + /// Verifies that `ReflectionMethod::isConstructor()` and `isDestructor()` derive /// their result from the reflected method name. #[test] From dd4edf4477a7d4e0553258b8237321dfae94ff63 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 20:22:43 +0200 Subject: [PATCH 0440/1208] Bridge AOT ReflectionClass property lookups --- crates/elephc-eval/src/context.rs | 6 +- .../elephc-eval/src/interpreter/reflection.rs | 165 ++++++++++++++---- .../elephc-eval/src/interpreter/statements.rs | 9 + docs/php/eval.md | 8 +- tests/codegen/eval.rs | 21 ++- 5 files changed, 170 insertions(+), 39 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 6a16f7bc2f..d11de4ffb0 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -544,13 +544,13 @@ impl ElephcEvalContext { self.eval_reflection_attributes.get(&identity) } - /// Records eval-declared class metadata for one synthetic ReflectionClass object. + /// Records reflected class metadata for one synthetic ReflectionClass object. pub fn register_eval_reflection_class(&mut self, identity: u64, class_name: &str) { self.eval_reflection_classes - .insert(identity, normalize_class_name(class_name)); + .insert(identity, class_name.trim_start_matches('\\').to_string()); } - /// Returns the reflected eval class name attached to a synthetic ReflectionClass. + /// Returns the reflected class name attached to a synthetic ReflectionClass. pub fn eval_reflection_class_name(&self, identity: u64) -> Option<&str> { self.eval_reflection_classes .get(&identity) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 792dab80a2..780088427f 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -7,8 +7,8 @@ //! - `crate::interpreter::expressions::eval_expr()` for `new Reflection*`. //! //! Key details: -//! - Only eval-declared classes/interfaces/traits/enums are handled here. -//! - Non-eval targets fall back to the generated AOT runtime bridge. +//! - Eval-declared classes/interfaces/traits/enums are materialized from dynamic metadata. +//! - Generated/AOT targets use focused runtime hooks for supported point lookups. use super::*; @@ -274,6 +274,36 @@ pub(in crate::interpreter) fn eval_reflection_class_is_instance_result( values.bool_value(result).map(Some) } +/// Handles eval-backed `ReflectionClass::hasProperty()` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_property_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasProperty") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let property_name = eval_reflection_string_arg(args[0], values)?; + let exists = if let Some(metadata) = + eval_reflection_class_like_attributes(&reflected_name, context) + { + metadata.property_names.iter().any(|name| name == &property_name) + } else { + eval_reflection_aot_property_metadata_if_exists(&reflected_name, &property_name, values)? + .is_some() + }; + values.bool_value(exists).map(Some) +} + /// Handles eval-backed `ReflectionClass::hasConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( identity: u64, @@ -485,6 +515,24 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( let Some(member_name) = eval_reflection_member_name(owner_kind, &reflected_name, &requested_name, context) else { + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + && !eval_reflection_class_like_exists(&reflected_name, context) + { + if let Some(member) = eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + &requested_name, + values, + )? { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } + } let message_name = eval_reflection_class_like_attributes(&reflected_name, context) .map(|metadata| metadata.resolved_name) .unwrap_or_else(|| reflected_name.clone()); @@ -613,31 +661,10 @@ fn eval_reflection_class_new( let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; let class_name = eval_reflection_string_arg(args[0], values)?; let Some(metadata) = eval_reflection_class_like_attributes(&class_name, context) else { - let is_class = values.class_exists(&class_name)?; - let is_interface = values.interface_exists(&class_name)?; - let is_trait = values.trait_exists(&class_name)?; - let is_enum = values.enum_exists(&class_name)?; - if !(is_class || is_interface || is_trait || is_enum) { + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(&class_name, values)? + else { return Ok(None); - } - let mut flags = 0; - if eval_reflection_class_like_is_internal(&class_name) { - flags |= EVAL_REFLECTION_CLASS_FLAG_INTERNAL; - } else { - flags |= EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; - } - if is_interface { - flags |= EVAL_REFLECTION_CLASS_FLAG_INTERFACE; - } - if is_trait { - flags |= EVAL_REFLECTION_CLASS_FLAG_TRAIT; - } - if is_enum { - flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM; - } - if eval_reflection_builtin_class_is_iterable(&class_name) { - flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; - } + }; return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, class_name.trim_start_matches('\\'), @@ -651,7 +678,7 @@ fn eval_reflection_class_new( None, None, flags, - if is_enum { 32 } else { 0 }, + modifiers, 0, None, None, @@ -683,6 +710,41 @@ fn eval_reflection_class_new( .map(Some) } +/// Returns generated/AOT class flags for synthetic ReflectionClass fallback objects. +fn eval_reflection_aot_class_flags( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let is_class = values.class_exists(runtime_class_name)?; + let is_interface = values.interface_exists(runtime_class_name)?; + let is_trait = values.trait_exists(runtime_class_name)?; + let is_enum = values.enum_exists(runtime_class_name)?; + if !(is_class || is_interface || is_trait || is_enum) { + return Ok(None); + } + let mut flags = 0; + if eval_reflection_class_like_is_internal(runtime_class_name) { + flags |= EVAL_REFLECTION_CLASS_FLAG_INTERNAL; + } else { + flags |= EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; + } + if is_interface { + flags |= EVAL_REFLECTION_CLASS_FLAG_INTERFACE; + } + if is_trait { + flags |= EVAL_REFLECTION_CLASS_FLAG_TRAIT; + } + if is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM; + } + if eval_reflection_builtin_class_is_iterable(runtime_class_name) { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } + let modifiers = if is_enum { 32 } else { 0 }; + Ok(Some((flags, modifiers))) +} + /// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. fn eval_reflection_method_new( evaluated_args: Vec, @@ -723,10 +785,9 @@ fn eval_reflection_property_new( let class_name = eval_reflection_string_arg(args[0], values)?; if !eval_reflection_class_like_exists(&class_name, context) { let property_name = eval_reflection_string_arg(args[1], values)?; - let runtime_class_name = class_name.trim_start_matches('\\'); - if let Some(flags) = values.reflection_property_flags(runtime_class_name, &property_name)? { - let property = - eval_reflection_aot_property_metadata(runtime_class_name, flags, &property_name); + if let Some(property) = + eval_reflection_aot_property_metadata_if_exists(&class_name, &property_name, values)? + { return eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_PROPERTY, &property_name, @@ -751,11 +812,26 @@ fn eval_reflection_property_new( .map(Some) } +/// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. +fn eval_reflection_aot_property_metadata_if_exists( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { + return Ok(None); + }; + Ok(Some(eval_reflection_aot_property_metadata( + runtime_class_name, + flags, + ))) +} + /// Converts AOT property flag metadata into the eval ReflectionProperty shape. fn eval_reflection_aot_property_metadata( class_name: &str, flags: u64, - _property_name: &str, ) -> EvalReflectionMemberMetadata { let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { EvalVisibility::Private @@ -1135,7 +1211,30 @@ fn eval_reflection_shallow_class_object_result( values: &mut impl RuntimeValueOps, ) -> Result { let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { - return values.bool_value(false); + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { + return values.bool_value(false); + }; + return eval_reflection_owner_object_with_members( + EVAL_REFLECTION_OWNER_CLASS, + class_name.trim_start_matches('\\'), + &[], + &[], + &[], + &[], + &[], + None, + &[], + None, + None, + flags, + modifiers, + 0, + None, + None, + false, + context, + values, + ); }; eval_reflection_owner_object_with_members( EVAL_REFLECTION_OWNER_CLASS, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 6dd52167b9..acef333236 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2337,6 +2337,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_has_property_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_has_constant_result( identity, method_name, diff --git a/docs/php/eval.md b/docs/php/eval.md index 56aeebd17a..cab149a529 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -195,6 +195,9 @@ relations; trait targets return `false`. `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. +For generated/AOT classes, `ReflectionClass::hasProperty()` can also probe +emitted property metadata without requiring the full property list to be +materialized on the `ReflectionClass` object. `ReflectionClass::hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, and `getReflectionConstants()` expose eval-visible class constants, interface @@ -204,7 +207,10 @@ lookups return `false` when no constant or case is visible. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate -flags. `ReflectionMethod::getDeclaringClass()` and +flags. For generated/AOT classes, `ReflectionClass::getProperty()` can +materialize a single `ReflectionProperty` from emitted predicate metadata, while +full AOT `getProperties()` enumeration still depends on the property arrays +materialized on the `ReflectionClass` object. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the eval class-like symbol that declares the reflected member. `ReflectionClass::getConstructor()` returns a materialized diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fab91820ff..a51062e943 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6597,7 +6597,21 @@ echo $child->isPromoted() ? "C" : "c"; $plain = new ReflectionProperty("EvalAotPromotedPlain", "id"); echo $plain->isPromoted() ? "P" : "p"; $static = new ReflectionProperty("EvalAotPromotedPlain", "count"); -echo $static->isPromoted() ? "S" : "s";'); +echo $static->isPromoted() ? "S" : "s"; +$class = new ReflectionClass("EvalAotPromotedBase"); +echo $class->hasProperty("id") ? "H" : "h"; +echo $class->hasProperty("missing") ? "M" : "m"; +$listed = $class->getProperty("id"); +echo $listed->isPromoted() ? "G" : "g"; +echo $listed->getDeclaringClass()->getName(); +$rootClass = new ReflectionClass("\EvalAotPromotedBase"); +echo $rootClass->hasProperty("name") ? "N" : "n"; +try { + $class->getProperty("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo ":" . $e->getMessage(); +}'); "#, ); assert!( @@ -6605,7 +6619,10 @@ echo $static->isPromoted() ? "S" : "s";'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "IINCps"); + assert_eq!( + out.stdout, + "IINCpsHmGEvalAotPromotedBaseN:Property EvalAotPromotedBase::$missing does not exist" + ); } /// Verifies eval ReflectionMethod constructor/destructor predicates through the bridge. From 8e8459371b145b6205fed270e05d030859516407 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 20:33:24 +0200 Subject: [PATCH 0441/1208] Bridge AOT ReflectionClass method lookups --- .../elephc-eval/src/interpreter/reflection.rs | 112 +++++++++++++++ .../src/interpreter/runtime_ops.rs | 7 + .../elephc-eval/src/interpreter/statements.rs | 9 ++ .../interpreter/tests/support/object_ops.rs | 20 +++ .../interpreter/tests/support/runtime_ops.rs | 8 ++ .../elephc-eval/src/runtime_hooks/externs.rs | 6 + crates/elephc-eval/src/runtime_hooks/ops.rs | 17 +++ docs/php/eval.md | 15 +- src/codegen_support/runtime/data/user.rs | 124 +++++++++++++++++ src/codegen_support/runtime/eval_bridge.rs | 131 ++++++++++++++++++ tests/codegen/eval.rs | 50 +++++++ 11 files changed, 492 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 780088427f..5d32901ce0 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -274,6 +274,39 @@ pub(in crate::interpreter) fn eval_reflection_class_is_instance_result( values.bool_value(result).map(Some) } +/// Handles eval-backed `ReflectionClass::hasMethod()` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_method_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasMethod") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let exists = if let Some(metadata) = + eval_reflection_class_like_attributes(&reflected_name, context) + { + metadata + .method_names + .iter() + .any(|name| name.eq_ignore_ascii_case(&requested_name)) + } else { + eval_reflection_aot_method_metadata_if_exists(&reflected_name, &requested_name, values)? + .is_some() + }; + values.bool_value(exists).map(Some) +} + /// Handles eval-backed `ReflectionClass::hasProperty()` calls. pub(in crate::interpreter) fn eval_reflection_class_has_property_result( identity: u64, @@ -515,6 +548,26 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( let Some(member_name) = eval_reflection_member_name(owner_kind, &reflected_name, &requested_name, context) else { + if owner_kind == EVAL_REFLECTION_OWNER_METHOD + && !eval_reflection_class_like_exists(&reflected_name, context) + { + if let Some(member) = eval_reflection_aot_method_metadata_if_exists( + &reflected_name, + &requested_name, + values, + )? + { + let member_name = requested_name.to_ascii_lowercase(); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &member_name, + &member, + context, + values, + ) + .map(Some); + } + } if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && !eval_reflection_class_like_exists(&reflected_name, context) { @@ -757,6 +810,20 @@ fn eval_reflection_method_new( )?; let class_name = eval_reflection_string_arg(args[0], values)?; if !eval_reflection_class_like_exists(&class_name, context) { + let method_name = eval_reflection_string_arg(args[1], values)?; + if let Some(method) = + eval_reflection_aot_method_metadata_if_exists(&class_name, &method_name, values)? + { + let method_name = method_name.to_ascii_lowercase(); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &method_name, + &method, + context, + values, + ) + .map(Some); + } return Ok(None); } let method_name = eval_reflection_string_arg(args[1], values)?; @@ -812,6 +879,51 @@ fn eval_reflection_property_new( .map(Some) } +/// Returns generated AOT ReflectionMethod metadata when the runtime table has a matching row. +fn eval_reflection_aot_method_metadata_if_exists( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { + return Ok(None); + }; + Ok(Some(eval_reflection_aot_method_metadata( + runtime_class_name, + flags, + ))) +} + +/// Converts AOT method flag metadata into the eval ReflectionMethod shape. +fn eval_reflection_aot_method_metadata( + class_name: &str, + flags: u64, +) -> EvalReflectionMemberMetadata { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + attributes: Vec::new(), + visibility, + is_static: flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, + is_final: flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, + is_abstract: flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0, + is_readonly: false, + is_promoted: false, + modifiers: eval_reflection_method_modifiers_from_flags(flags), + type_metadata: None, + default_value: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} + /// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. fn eval_reflection_aot_property_metadata_if_exists( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 93d195e656..7d77229ba5 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -133,6 +133,13 @@ pub trait RuntimeValueOps { constructor: RuntimeCellHandle, ) -> Result; + /// Returns generated AOT ReflectionMethod flags for a class/method pair. + fn reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus>; + /// Returns generated AOT ReflectionProperty flags for a class/property pair. fn reflection_property_flags( &mut self, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index acef333236..9855c44b8e 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2337,6 +2337,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_has_method_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_has_property_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index f8c3363fd3..fedc9e3b18 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -858,6 +858,26 @@ impl FakeOps { pub(super) fn runtime_class_exists(&mut self, name: &str) -> Result { Ok(name.eq_ignore_ascii_case("KnownClass")) } + /// Reports fake generated AOT ReflectionMethod flags for eval metadata unit tests. + pub(super) fn runtime_reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Ok(None); + } + match method_name.to_ascii_lowercase().as_str() { + "run" => Ok(Some(EVAL_REFLECTION_MEMBER_FLAG_PUBLIC)), + "helper" => Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_STATIC | EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, + )), + "locked" => Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_PUBLIC | EVAL_REFLECTION_MEMBER_FLAG_FINAL, + )), + _ => Ok(None), + } + } /// Reports fake generated AOT ReflectionProperty flags for eval metadata unit tests. pub(super) fn runtime_reflection_property_flags( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 50b8a0bafe..4f34a6262f 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -160,6 +160,14 @@ impl RuntimeValueOps for FakeOps { constructor, ) } + /// Reports fake generated AOT ReflectionMethod flags for metadata bridge tests. + fn reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_method_flags(class_name, method_name) + } /// Reports fake generated AOT ReflectionProperty flags for metadata bridge tests. fn reflection_property_flags( &mut self, diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 61663827b4..4e6ade6e71 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -93,6 +93,12 @@ unsafe extern "C" { backing_value: *mut RuntimeCell, constructor: *mut RuntimeCell, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_method_flags( + class_ptr: *const u8, + class_len: u64, + method_ptr: *const u8, + method_len: u64, + ) -> u64; pub(super) fn __elephc_eval_reflection_property_flags( class_ptr: *const u8, class_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 6f8c7bc591..29ff5d3a13 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -241,6 +241,23 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns generated AOT ReflectionMethod flags, or `None` when no row matches. + fn reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_method_flags( + class_name.as_ptr(), + class_name.len() as u64, + method_name.as_ptr(), + method_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + /// Returns generated AOT ReflectionProperty flags, or `None` when no row matches. fn reflection_property_flags( &mut self, diff --git a/docs/php/eval.md b/docs/php/eval.md index cab149a529..d64223f2b9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -195,9 +195,9 @@ relations; trait targets return `false`. `ReflectionClass::hasMethod()` and `ReflectionClass::hasProperty()` report method and property membership for eval classes, interfaces, traits, and enums; method lookup is case-insensitive, while property lookup is case-sensitive. -For generated/AOT classes, `ReflectionClass::hasProperty()` can also probe -emitted property metadata without requiring the full property list to be -materialized on the `ReflectionClass` object. +For generated/AOT classes, `ReflectionClass::hasMethod()` and `hasProperty()` +can also probe emitted method/property metadata without requiring the full +member lists to be materialized on the `ReflectionClass` object. `ReflectionClass::hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, and `getReflectionConstants()` expose eval-visible class constants, interface @@ -207,10 +207,11 @@ lookups return `false` when no constant or case is visible. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate -flags. For generated/AOT classes, `ReflectionClass::getProperty()` can -materialize a single `ReflectionProperty` from emitted predicate metadata, while -full AOT `getProperties()` enumeration still depends on the property arrays -materialized on the `ReflectionClass` object. `ReflectionMethod::getDeclaringClass()` and +flags. For generated/AOT classes, `ReflectionClass::getMethod()` and +`getProperty()` can materialize a single `ReflectionMethod` or +`ReflectionProperty` from emitted predicate metadata, while full AOT +`getMethods()` / `getProperties()` enumeration still depends on the member +arrays materialized on the `ReflectionClass` object. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the eval class-like symbol that declares the reflected member. `ReflectionClass::getConstructor()` returns a materialized diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 1c6507adbb..b51e40da72 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -28,6 +28,12 @@ const EVAL_REFLECTION_PROPERTY_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_PROPERTY_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE: u64 = 256; const EVAL_REFLECTION_PROPERTY_FLAG_PROMOTED: u64 = 512; +const EVAL_REFLECTION_METHOD_FLAG_STATIC: u64 = 1; +const EVAL_REFLECTION_METHOD_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_METHOD_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_METHOD_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_METHOD_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_METHOD_FLAG_ABSTRACT: u64 = 32; /// Emit the user-dependent data section — globals, statics, class metadata. /// This changes per program and cannot be cached. @@ -464,6 +470,8 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); emit_classes_by_name_table(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); + emit_eval_reflection_method_lookup_data(&mut out, &sorted_classes); + out.push_str(".p2align 3\n"); emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); // -- class-level PHP 8 attribute metadata table -- @@ -949,6 +957,122 @@ fn emit_name_lookup_data( } } +/// Emits AOT method flag rows consumed by eval ReflectionMethod metadata probes. +fn emit_eval_reflection_method_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + let mut methods = class_info.methods.keys().collect::>(); + methods.sort(); + for method_name in methods { + let flags = eval_reflection_instance_method_flags(class_info, method_name); + let class_label = format!("_eval_reflection_method_class_{}", index); + let method_label = format!("_eval_reflection_method_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + method_label, + escaped_ascii(method_name) + )); + entries.push(( + class_label, + class_name.len(), + method_label, + method_name.len(), + flags, + )); + index += 1; + } + + let mut static_methods = class_info.static_methods.keys().collect::>(); + static_methods.sort(); + for method_name in static_methods { + let flags = eval_reflection_static_method_flags(class_info, method_name); + let class_label = format!("_eval_reflection_method_class_{}", index); + let method_label = format!("_eval_reflection_method_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + method_label, + escaped_ascii(method_name) + )); + entries.push(( + class_label, + class_name.len(), + method_label, + method_name.len(), + flags, + )); + index += 1; + } + } + + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_method_count\n_eval_reflection_method_count:\n"); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_methods\n_eval_reflection_methods:\n"); + for (class_label, class_len, method_label, method_len, flags) in entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", method_label)); + out.push_str(&format!(" .quad {}\n", method_len)); + out.push_str(&format!(" .quad {}\n", flags)); + } +} + +/// Returns eval ReflectionMethod bitflags for one instance method entry. +fn eval_reflection_instance_method_flags(class_info: &ClassInfo, method_name: &str) -> u64 { + let visibility = class_info + .method_visibilities + .get(method_name) + .unwrap_or(&Visibility::Public); + let mut flags = eval_reflection_method_visibility_flags(visibility); + if class_info.final_methods.contains(method_name) { + flags |= EVAL_REFLECTION_METHOD_FLAG_FINAL; + } + if !class_info.method_impl_classes.contains_key(method_name) { + flags |= EVAL_REFLECTION_METHOD_FLAG_ABSTRACT; + } + flags +} + +/// Returns eval ReflectionMethod bitflags for one static method entry. +fn eval_reflection_static_method_flags(class_info: &ClassInfo, method_name: &str) -> u64 { + let visibility = class_info + .static_method_visibilities + .get(method_name) + .unwrap_or(&Visibility::Public); + let mut flags = + EVAL_REFLECTION_METHOD_FLAG_STATIC | eval_reflection_method_visibility_flags(visibility); + if class_info.final_static_methods.contains(method_name) { + flags |= EVAL_REFLECTION_METHOD_FLAG_FINAL; + } + if !class_info.static_method_impl_classes.contains_key(method_name) { + flags |= EVAL_REFLECTION_METHOD_FLAG_ABSTRACT; + } + flags +} + +/// Converts method visibility metadata into eval ReflectionMethod flag bits. +fn eval_reflection_method_visibility_flags(visibility: &Visibility) -> u64 { + match visibility { + Visibility::Public => EVAL_REFLECTION_METHOD_FLAG_PUBLIC, + Visibility::Protected => EVAL_REFLECTION_METHOD_FLAG_PROTECTED, + Visibility::Private => EVAL_REFLECTION_METHOD_FLAG_PRIVATE, + } +} + /// Emits AOT property flag rows consumed by eval ReflectionProperty metadata probes. fn emit_eval_reflection_property_lookup_data( out: &mut String, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index aa225dc940..bf56588532 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -169,6 +169,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { "__elephc_eval_enum_exists", ); + emit_aarch64_eval_reflection_method_flags(emitter); emit_aarch64_eval_reflection_property_flags(emitter); label_c_global(emitter, "__elephc_eval_value_is_a"); @@ -1584,6 +1585,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { "__elephc_eval_enum_exists_x86", ); + emit_x86_64_eval_reflection_method_flags(emitter); emit_x86_64_eval_reflection_property_flags(emitter); label_c_global(emitter, "__elephc_eval_value_is_a"); @@ -2963,6 +2965,72 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell } +/// Emits the ARM64 eval hook that returns AOT ReflectionMethod predicate flags. +fn emit_aarch64_eval_reflection_method_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_method_flags"); + emitter.instruction("sub sp, sp, #96"); // reserve scan state across runtime string comparisons + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested method-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested method-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_method_count"); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection-method row count + emitter.instruction("cbz x9, __elephc_eval_reflection_method_flags_miss"); // an empty table cannot contain the requested method + emitter.instruction("str x9, [sp, #32]"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_methods"); + emitter.instruction("str x10, [sp, #40]"); // save the current method metadata row + emitter.instruction("mov x11, #0"); // start scanning at method metadata row zero + emitter.label("__elephc_eval_reflection_method_flags_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the method metadata row count + emitter.instruction("cmp x11, x9"); // have all method metadata rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_method_flags_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_method_flags_skip"); // length mismatch means the class cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_method_flags_skip"); // class mismatch means the row cannot match + emitter.instruction("ldr x10, [sp, #40]"); // reload the current row for the method-name compare + emitter.instruction("ldr x12, [x10, #24]"); // load the stored method-name length + emitter.instruction("ldr x2, [sp, #24]"); // reload the requested method-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested method-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_method_flags_skip"); // length mismatch means the method cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the method-name compare + emitter.instruction("ldr x1, [sp, #16]"); // pass the requested method-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // pass the requested method-name length + emitter.instruction("ldr x3, [x10, #16]"); // pass the stored method-name pointer + emitter.instruction("mov x4, x12"); // pass the stored method-name length + emitter.instruction("bl __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the method-name compare + emitter.instruction("cmp x0, #0"); // did the requested method name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_method_flags_skip"); // method mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #40]"); // reload the matched method metadata row + emitter.instruction("ldr x0, [x10, #32]"); // return the row's ReflectionMethod predicate flags + emitter.instruction("b __elephc_eval_reflection_method_flags_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_method_flags_skip"); + emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row + emitter.instruction("add x10, x10, #40"); // advance to the next 40-byte metadata row + emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_method_flags_loop"); // continue scanning method metadata rows + emitter.label("__elephc_eval_reflection_method_flags_miss"); + emitter.instruction("mov x0, #0"); // return zero when no AOT method metadata matched + emitter.label("__elephc_eval_reflection_method_flags_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the method metadata scan frame + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + /// Emits the ARM64 eval hook that returns AOT ReflectionProperty predicate flags. fn emit_aarch64_eval_reflection_property_flags(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_reflection_property_flags"); @@ -3029,6 +3097,69 @@ fn emit_aarch64_eval_reflection_property_flags(emitter: &mut Emitter) { emitter.instruction("ret"); // return flags, or zero for a miss, to Rust } +/// Emits the x86_64 eval hook that returns AOT ReflectionMethod predicate flags. +fn emit_x86_64_eval_reflection_method_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_method_flags"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable scan frame pointer + emitter.instruction("sub rsp, 64"); // reserve scan state across runtime string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested method-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested method-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_method_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection-method row count + emitter.instruction("test r10, r10"); // is the method metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_method_flags_miss_x86"); // an empty table cannot contain the requested method + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_methods"); + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the current method metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at method metadata row zero + emitter.label("__elephc_eval_reflection_method_flags_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the method metadata row count + emitter.instruction("cmp r11, r10"); // have all method metadata rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_method_flags_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_method_flags_skip_x86"); // length mismatch means the class cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_method_flags_skip_x86"); // class mismatch means the row cannot match + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current row for the method-name compare + emitter.instruction("mov rcx, QWORD PTR [r10 + 24]"); // load the stored method-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // compare stored and requested method-name lengths + emitter.instruction("jne __elephc_eval_reflection_method_flags_skip_x86"); // length mismatch means the method cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the method-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the requested method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // pass the requested method-name length + emitter.instruction("mov rdx, QWORD PTR [r10 + 16]"); // pass the stored method-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the method-name compare + emitter.instruction("test rax, rax"); // did the requested method name match this row? + emitter.instruction("jne __elephc_eval_reflection_method_flags_skip_x86"); // method mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the matched method metadata row + emitter.instruction("mov rax, QWORD PTR [r10 + 32]"); // return the row's ReflectionMethod predicate flags + emitter.instruction("jmp __elephc_eval_reflection_method_flags_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_method_flags_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row + emitter.instruction("add r10, 40"); // advance to the next 40-byte metadata row + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_method_flags_loop_x86"); // continue scanning method metadata rows + emitter.label("__elephc_eval_reflection_method_flags_miss_x86"); + emitter.instruction("xor eax, eax"); // return zero when no AOT method metadata matched + emitter.label("__elephc_eval_reflection_method_flags_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + /// Emits the x86_64 eval hook that returns AOT ReflectionProperty predicate flags. fn emit_x86_64_eval_reflection_property_flags(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_reflection_property_flags"); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a51062e943..5297ab6326 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6625,6 +6625,56 @@ try { ); } +/// Verifies eval can probe generated/AOT method predicate metadata through +/// `ReflectionClass::hasMethod()`, `getMethod()`, and direct `ReflectionMethod`. +#[test] +fn test_eval_reflection_method_for_aot_class() { + let out = compile_and_run_capture( + r#"hasMethod("RUN") ? "R" : "r"; +echo $class->hasMethod("BASESTATIC") ? "B" : "b"; +echo $class->hasMethod("missing") ? "M" : "m"; +$run = $class->getMethod("RUN"); +echo ":" . $run->getName(); +echo $run->isPublic() ? "U" : "u"; +echo $run->isStatic() ? "S" : "s"; +echo $run->getDeclaringClass()->getName(); +$base = $class->getMethod("baseStatic"); +echo ":" . ($base->isStatic() ? "S" : "s"); +echo $base->isProtected() ? "P" : "p"; +$locked = new ReflectionMethod("EvalAotReflectMethodBase", "LOCKED"); +echo ":" . $locked->getName(); +echo $locked->isFinal() ? "F" : "f"; +echo $locked->isPublic() ? "U" : "u"; +echo $locked->getDeclaringClass()->getName(); +try { + $class->getMethod("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "RBm:runUsEvalAotReflectMethodChild:SP:lockedFUEvalAotReflectMethodBase:Method EvalAotReflectMethodChild::missing() does not exist" + ); +} + /// Verifies eval ReflectionMethod constructor/destructor predicates through the bridge. #[test] fn test_eval_reflection_method_reports_constructor_and_destructor() { From bf0b84581ddf34ce61a423fd119be7b3aa41b13f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 20:47:47 +0200 Subject: [PATCH 0442/1208] Enumerate AOT reflection members in eval --- .../elephc-eval/src/interpreter/reflection.rs | 120 ++++++++++ .../src/interpreter/runtime_ops.rs | 12 + .../elephc-eval/src/interpreter/statements.rs | 9 + .../interpreter/tests/support/object_ops.rs | 24 ++ .../interpreter/tests/support/runtime_ops.rs | 14 ++ .../elephc-eval/src/runtime_hooks/externs.rs | 8 + crates/elephc-eval/src/runtime_hooks/ops.rs | 20 ++ docs/php/eval.md | 13 +- src/codegen_support/runtime/eval_bridge.rs | 212 ++++++++++++++++++ tests/codegen/eval.rs | 37 ++- 10 files changed, 461 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 5d32901ce0..4a889660c6 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -522,6 +522,56 @@ pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_res Ok(Some(result)) } +/// Handles eval-backed `ReflectionClass::getMethods()` and `getProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_members_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let owner_kind = if method_name.eq_ignore_ascii_case("getMethods") { + EVAL_REFLECTION_OWNER_METHOD + } else if method_name.eq_ignore_ascii_case("getProperties") { + EVAL_REFLECTION_OWNER_PROPERTY + } else { + return Ok(None); + }; + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + let names = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + metadata.method_names + } else { + metadata.property_names + }; + return eval_reflection_member_object_array_result( + owner_kind, + &reflected_name, + &names, + context, + values, + ) + .map(Some); + } + let names = eval_reflection_aot_member_names(owner_kind, &reflected_name, values)?; + eval_reflection_aot_member_object_array_result( + owner_kind, + &reflected_name, + &names, + context, + values, + ) + .map(Some) +} + /// Handles eval-backed `ReflectionClass::getMethod()` and `getProperty()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_member_result( identity: u64, @@ -1764,6 +1814,76 @@ fn eval_reflection_member_object_array_result( Ok(result) } +/// Builds an indexed array of AOT ReflectionMethod or ReflectionProperty objects for a class. +fn eval_reflection_aot_member_object_array_result( + owner_kind: u64, + class_name: &str, + names: &[String], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + let mut index = 0; + for name in names { + let member = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + eval_reflection_aot_method_metadata_if_exists(class_name, name, values)? + } else { + eval_reflection_aot_property_metadata_if_exists(class_name, name, values)? + }; + let Some(member) = member else { + continue; + }; + let reflected_name = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + name.to_ascii_lowercase() + } else { + name.clone() + }; + let member_object = eval_reflection_member_object_result( + owner_kind, + &reflected_name, + &member, + context, + values, + )?; + let key = values.int(index)?; + result = values.array_set(result, key, member_object)?; + index += 1; + } + Ok(result) +} + +/// Returns generated AOT member names for one reflected class. +fn eval_reflection_aot_member_names( + owner_kind: u64, + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + values.reflection_method_names(runtime_class_name)? + } else { + values.reflection_property_names(runtime_class_name)? + }; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Copies a runtime string array into Rust-owned strings for reflection metadata assembly. +fn eval_reflection_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_reflection_string_arg(value, values)?); + } + Ok(result) +} + /// Returns member metadata for one ReflectionClass member-array entry. fn eval_reflection_member_metadata( owner_kind: u64, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 7d77229ba5..ffd0ba4e99 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -140,6 +140,12 @@ pub trait RuntimeValueOps { method_name: &str, ) -> Result, EvalStatus>; + /// Returns generated AOT ReflectionMethod names visible for one class. + fn reflection_method_names( + &mut self, + class_name: &str, + ) -> Result; + /// Returns generated AOT ReflectionProperty flags for a class/property pair. fn reflection_property_flags( &mut self, @@ -147,6 +153,12 @@ pub trait RuntimeValueOps { property_name: &str, ) -> Result, EvalStatus>; + /// Returns generated AOT ReflectionProperty names visible for one class. + fn reflection_property_names( + &mut self, + class_name: &str, + ) -> Result; + /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 9855c44b8e..2ea4d74356 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2409,6 +2409,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_get_members_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_get_member_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index fedc9e3b18..a56d52dbe8 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -878,6 +878,19 @@ impl FakeOps { _ => Ok(None), } } + /// Reports fake generated AOT ReflectionMethod names for eval metadata unit tests. + pub(super) fn runtime_reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(3)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "run")?; + array = self.runtime_string_array_push(array, "helper")?; + array = self.runtime_string_array_push(array, "locked")?; + } + Ok(array) + } /// Reports fake generated AOT ReflectionProperty flags for eval metadata unit tests. pub(super) fn runtime_reflection_property_flags( &mut self, @@ -891,6 +904,17 @@ impl FakeOps { } Ok(None) } + /// Reports fake generated AOT ReflectionProperty names for eval metadata unit tests. + pub(super) fn runtime_reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "promoted")?; + } + Ok(array) + } /// Reports one fake AOT interface for eval `interface_exists` unit tests. pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { Ok([ diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 4f34a6262f..c5244f9622 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -168,6 +168,13 @@ impl RuntimeValueOps for FakeOps { ) -> Result, EvalStatus> { self.runtime_reflection_method_flags(class_name, method_name) } + /// Reports fake generated AOT ReflectionMethod names for metadata bridge tests. + fn reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_method_names(class_name) + } /// Reports fake generated AOT ReflectionProperty flags for metadata bridge tests. fn reflection_property_flags( &mut self, @@ -176,6 +183,13 @@ impl RuntimeValueOps for FakeOps { ) -> Result, EvalStatus> { self.runtime_reflection_property_flags(class_name, property_name) } + /// Reports fake generated AOT ReflectionProperty names for metadata bridge tests. + fn reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_property_names(class_name) + } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { self.runtime_new_object(_class_name) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 4e6ade6e71..27a28c58da 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -99,12 +99,20 @@ unsafe extern "C" { method_ptr: *const u8, method_len: u64, ) -> u64; + pub(super) fn __elephc_eval_reflection_method_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_reflection_property_flags( class_ptr: *const u8, class_len: u64, property_ptr: *const u8, property_len: u64, ) -> u64; + pub(super) fn __elephc_eval_reflection_property_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, name_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 29ff5d3a13..473e50d690 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -258,6 +258,16 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok((flags != 0).then_some(flags)) } + /// Returns generated AOT ReflectionMethod names visible for one class. + fn reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_method_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + /// Returns generated AOT ReflectionProperty flags, or `None` when no row matches. fn reflection_property_flags( &mut self, @@ -275,6 +285,16 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok((flags != 0).then_some(flags)) } + /// Returns generated AOT ReflectionProperty names visible for one class. + fn reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_property_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { let object = Self::handle(unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index d64223f2b9..ab1d26ce73 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -207,13 +207,14 @@ lookups return `false` when no constant or case is visible. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate -flags. For generated/AOT classes, `ReflectionClass::getMethod()` and -`getProperty()` can materialize a single `ReflectionMethod` or -`ReflectionProperty` from emitted predicate metadata, while full AOT -`getMethods()` / `getProperties()` enumeration still depends on the member -arrays materialized on the `ReflectionClass` object. `ReflectionMethod::getDeclaringClass()` and +flags. For generated/AOT classes, `ReflectionClass::getMethod()` / +`getProperty()` and no-filter `getMethods()` / `getProperties()` materialize +reflection objects from emitted member-name and predicate metadata. AOT member +reflection currently exposes names, declaring classes, and predicate flags, but +does not expose full AOT parameter, attribute, property type, or property +default-value metadata. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized -`ReflectionClass` for the eval class-like symbol that declares the reflected +`ReflectionClass` for the symbol that declares the reflected member. `ReflectionClass::getConstructor()` returns a materialized `ReflectionMethod` for direct, inherited, interface, and trait constructors, or `null` when no constructor is visible. `ReflectionClass::getParentClass()` diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index bf56588532..5f2cbe78b8 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -169,6 +169,8 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { "__elephc_eval_enum_exists", ); + emit_aarch64_eval_reflection_method_names(emitter); + emit_aarch64_eval_reflection_property_names(emitter); emit_aarch64_eval_reflection_method_flags(emitter); emit_aarch64_eval_reflection_property_flags(emitter); @@ -1585,6 +1587,8 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { "__elephc_eval_enum_exists_x86", ); + emit_x86_64_eval_reflection_method_names(emitter); + emit_x86_64_eval_reflection_property_names(emitter); emit_x86_64_eval_reflection_method_flags(emitter); emit_x86_64_eval_reflection_property_flags(emitter); @@ -2965,6 +2969,110 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell } +/// Emits the ARM64 eval hook that returns AOT ReflectionMethod names. +fn emit_aarch64_eval_reflection_method_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_method_names", + "_eval_reflection_method_count", + "_eval_reflection_methods", + "__elephc_eval_reflection_method_names", + "method", + ); +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionProperty names. +fn emit_aarch64_eval_reflection_property_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_property_names", + "_eval_reflection_property_count", + "_eval_reflection_properties", + "__elephc_eval_reflection_property_names", + "property", + ); +} + +/// Emits an ARM64 class-filtered AOT reflection member-name scanner. +fn emit_aarch64_eval_reflection_member_names( + emitter: &mut Emitter, + symbol: &str, + count_symbol: &str, + table_symbol: &str, + label_prefix: &str, + member_kind: &str, +) { + let loop_label = format!("{label_prefix}_loop"); + let skip_label = format!("{label_prefix}_skip"); + let miss_label = format!("{label_prefix}_miss"); + let done_label = format!("{label_prefix}_done"); + let string_array_new_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_new"); + let string_array_push_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_push"); + label_c_global(emitter, symbol); + emitter.instruction("sub sp, sp, #112"); // reserve scan state across allocation and string comparisons + emitter.instruction("stp x29, x30, [sp, #96]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #96"); // establish a stable member-name scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + abi::emit_symbol_address(emitter, "x9", count_symbol); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection member row count + emitter.instruction("str x9, [sp, #16]"); // save the table count across helper calls + emitter.instruction("mov x0, x9"); // use the full table count as a safe result-array capacity + emitter.instruction(&format!("bl {string_array_new_symbol}")); // allocate the boxed result string array + emitter.instruction(&format!("cbz x0, {miss_label}")); // allocation failure reports a null pointer to Rust + emitter.instruction("str x0, [sp, #24]"); // save the boxed result string array + abi::emit_symbol_address(emitter, "x10", table_symbol); + emitter.instruction("str x10, [sp, #32]"); // save the current member metadata row + emitter.instruction("mov x11, #0"); // start scanning at member metadata row zero + emitter.label(&loop_label); + emitter.instruction("ldr x9, [sp, #16]"); // reload the member metadata row count + emitter.instruction("cmp x11, x9"); // have all member metadata rows been scanned? + emitter.instruction(&format!("b.ge {done_label}")); // return the accumulated names after the final row + emitter.instruction("ldr x10, [sp, #32]"); // reload the current member metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction(&format!("b.ne {skip_label}")); // length mismatch means this row belongs to another class + emitter.instruction("str x11, [sp, #40]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #40]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction(&format!("b.ne {skip_label}")); // class mismatch means scanning must continue + emitter.instruction("str x11, [sp, #40]"); // save the row index across appending the member name + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed result string array + emitter.instruction("ldr x10, [sp, #32]"); // reload the matched member metadata row + emitter.instruction("ldr x1, [x10, #16]"); // pass the stored member-name pointer + emitter.instruction("ldr x2, [x10, #24]"); // pass the stored member-name length + emitter.instruction(&format!("bl {string_array_push_symbol}")); // append the matched member name to the result array + emitter.instruction(&format!("cbz x0, {miss_label}")); // malformed append state reports a null pointer to Rust + emitter.instruction("str x0, [sp, #24]"); // save the updated boxed result string array + emitter.instruction("ldr x11, [sp, #40]"); // restore the row index after appending the member name + emitter.label(&skip_label); + emitter.instruction("ldr x10, [sp, #32]"); // reload the current member metadata row + emitter.instruction("add x10, x10, #40"); // advance to the next 40-byte metadata row + emitter.instruction("str x10, [sp, #32]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction(&format!("b {loop_label}")); // continue scanning member metadata rows + emitter.label(&done_label); + emitter.instruction("ldr x0, [sp, #24]"); // return the boxed result string array + emitter.instruction(&format!("b {label_prefix}_ret")); // share the frame teardown path + emitter.label(&miss_label); + emitter.instruction("mov x0, xzr"); // return a null pointer when allocation or append failed + emitter.label(&format!("{label_prefix}_ret")); + emitter.instruction("ldp x29, x30, [sp, #96]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #112"); // release the member-name scan frame + emitter.instruction("ret"); // return the boxed name array, or null on failure, to Rust + emitter.comment(&format!("--- end eval reflection {member_kind} names ---")); +} + /// Emits the ARM64 eval hook that returns AOT ReflectionMethod predicate flags. fn emit_aarch64_eval_reflection_method_flags(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_reflection_method_flags"); @@ -3097,6 +3205,110 @@ fn emit_aarch64_eval_reflection_property_flags(emitter: &mut Emitter) { emitter.instruction("ret"); // return flags, or zero for a miss, to Rust } +/// Emits the x86_64 eval hook that returns AOT ReflectionMethod names. +fn emit_x86_64_eval_reflection_method_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_method_names", + "_eval_reflection_method_count", + "_eval_reflection_methods", + "__elephc_eval_reflection_method_names_x86", + "method", + ); +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionProperty names. +fn emit_x86_64_eval_reflection_property_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_property_names", + "_eval_reflection_property_count", + "_eval_reflection_properties", + "__elephc_eval_reflection_property_names_x86", + "property", + ); +} + +/// Emits an x86_64 class-filtered AOT reflection member-name scanner. +fn emit_x86_64_eval_reflection_member_names( + emitter: &mut Emitter, + symbol: &str, + count_symbol: &str, + table_symbol: &str, + label_prefix: &str, + member_kind: &str, +) { + let loop_label = format!("{label_prefix}_loop"); + let skip_label = format!("{label_prefix}_skip"); + let miss_label = format!("{label_prefix}_miss"); + let done_label = format!("{label_prefix}_done"); + let string_array_new_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_new"); + let string_array_push_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_push"); + label_c_global(emitter, symbol); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable member-name scan frame pointer + emitter.instruction("sub rsp, 80"); // reserve scan state across allocation and string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + abi::emit_symbol_address(emitter, "r10", count_symbol); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection member row count + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the table count across helper calls + emitter.instruction("mov rdi, r10"); // use the full table count as a safe result-array capacity + emitter.instruction(&format!("call {string_array_new_symbol}")); // allocate the boxed result string array + emitter.instruction(&format!("test rax, rax")); // did allocation return a usable boxed array? + emitter.instruction(&format!("jz {miss_label}")); // allocation failure reports a null pointer to Rust + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the boxed result string array + abi::emit_symbol_address(emitter, "r11", table_symbol); + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the current member metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at member metadata row zero + emitter.label(&loop_label); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the member metadata row count + emitter.instruction("cmp r11, r10"); // have all member metadata rows been scanned? + emitter.instruction(&format!("jae {done_label}")); // return the accumulated names after the final row + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current member metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction(&format!("jne {skip_label}")); // length mismatch means this row belongs to another class + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction(&format!("jne {skip_label}")); // class mismatch means scanning must continue + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the row index across appending the member name + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the matched member metadata row + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed result string array + emitter.instruction("mov rsi, QWORD PTR [r10 + 16]"); // pass the stored member-name pointer + emitter.instruction("mov rdx, QWORD PTR [r10 + 24]"); // pass the stored member-name length + emitter.instruction(&format!("call {string_array_push_symbol}")); // append the matched member name to the result array + emitter.instruction("test rax, rax"); // did append return a usable boxed array? + emitter.instruction(&format!("jz {miss_label}")); // malformed append state reports a null pointer to Rust + emitter.instruction("mov QWORD PTR [rbp - 32], rax"); // save the updated boxed result string array + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // restore the row index after appending the member name + emitter.label(&skip_label); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current member metadata row + emitter.instruction("add r10, 40"); // advance to the next 40-byte metadata row + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction(&format!("jmp {loop_label}")); // continue scanning member metadata rows + emitter.label(&done_label); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // return the boxed result string array + emitter.instruction(&format!("jmp {label_prefix}_ret")); // share the frame teardown path + emitter.label(&miss_label); + emitter.instruction("xor eax, eax"); // return a null pointer when allocation or append failed + emitter.label(&format!("{label_prefix}_ret")); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed name array, or null on failure, to Rust + emitter.comment(&format!("--- end eval reflection {member_kind} names ---")); +} + /// Emits the x86_64 eval hook that returns AOT ReflectionMethod predicate flags. fn emit_x86_64_eval_reflection_method_flags(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_reflection_method_flags"); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5297ab6326..c60d804359 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6606,6 +6606,20 @@ echo $listed->isPromoted() ? "G" : "g"; echo $listed->getDeclaringClass()->getName(); $rootClass = new ReflectionClass("\EvalAotPromotedBase"); echo $rootClass->hasProperty("name") ? "N" : "n"; +$properties = $class->getProperties(); +echo ":" . count($properties); +$listedId = false; +$listedName = false; +foreach ($properties as $property) { + if ($property->getName() === "id") { + $listedId = $property->isPromoted(); + } + if ($property->getName() === "name") { + $listedName = $property->isPromoted(); + } +} +echo $listedId ? "I" : "i"; +echo $listedName ? "N" : "n"; try { $class->getProperty("missing"); echo "bad"; @@ -6621,7 +6635,7 @@ try { ); assert_eq!( out.stdout, - "IINCpsHmGEvalAotPromotedBaseN:Property EvalAotPromotedBase::$missing does not exist" + "IINCpsHmGEvalAotPromotedBaseN:2IN:Property EvalAotPromotedBase::$missing does not exist" ); } @@ -6656,6 +6670,25 @@ echo ":" . $locked->getName(); echo $locked->isFinal() ? "F" : "f"; echo $locked->isPublic() ? "U" : "u"; echo $locked->getDeclaringClass()->getName(); +$methods = $class->getMethods(); +$seenRun = false; +$seenBase = false; +$seenLocked = false; +foreach ($methods as $method) { + if (strtolower($method->getName()) === "run") { + $seenRun = $method->isPublic(); + } + if (strtolower($method->getName()) === "basestatic") { + $seenBase = $method->isStatic(); + } + if (strtolower($method->getName()) === "locked") { + $seenLocked = $method->isFinal(); + } +} +echo ":" . count($methods); +echo $seenRun ? "R" : "r"; +echo $seenBase ? "B" : "b"; +echo $seenLocked ? "L" : "l"; try { $class->getMethod("missing"); echo "bad"; @@ -6671,7 +6704,7 @@ try { ); assert_eq!( out.stdout, - "RBm:runUsEvalAotReflectMethodChild:SP:lockedFUEvalAotReflectMethodBase:Method EvalAotReflectMethodChild::missing() does not exist" + "RBm:runUsEvalAotReflectMethodChild:SP:lockedFUEvalAotReflectMethodBase:4RBL:Method EvalAotReflectMethodChild::missing() does not exist" ); } From 544ec0f155d6b72195cfd4b1ef61162dd53949a3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 20:57:40 +0200 Subject: [PATCH 0443/1208] Support eval reflection member filters --- .../elephc-eval/src/interpreter/reflection.rs | 59 ++++++++++++++++++- .../tests/builtins_class_metadata.rs | 19 +++++- docs/php/eval.md | 11 ++-- tests/codegen/eval.rs | 47 +++++++++++++-- 4 files changed, 123 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 4a889660c6..fc0b0225a0 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -537,9 +537,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_members_result( } else { return Ok(None); }; - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } + let filter = eval_reflection_member_filter(evaluated_args, values)?; let Some(reflected_name) = context .eval_reflection_class_name(identity) .map(str::to_string) @@ -556,6 +554,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_members_result( owner_kind, &reflected_name, &names, + filter, context, values, ) @@ -566,6 +565,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_members_result( owner_kind, &reflected_name, &names, + filter, context, values, ) @@ -1217,6 +1217,7 @@ fn eval_reflection_owner_object_with_members( EVAL_REFLECTION_OWNER_METHOD, reflected_name, &method_names, + None, context, values, )? @@ -1235,6 +1236,7 @@ fn eval_reflection_owner_object_with_members( EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, &property_names, + None, context, values, )? @@ -1795,6 +1797,7 @@ fn eval_reflection_member_object_array_result( owner_kind: u64, class_name: &str, names: &[String], + filter: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -1805,6 +1808,9 @@ fn eval_reflection_member_object_array_result( else { continue; }; + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } let member_object = eval_reflection_member_object_result(owner_kind, name, &member, context, values)?; let key = values.int(index)?; @@ -1819,6 +1825,7 @@ fn eval_reflection_aot_member_object_array_result( owner_kind: u64, class_name: &str, names: &[String], + filter: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -1833,6 +1840,9 @@ fn eval_reflection_aot_member_object_array_result( let Some(member) = member else { continue; }; + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } let reflected_name = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { name.to_ascii_lowercase() } else { @@ -1852,6 +1862,49 @@ fn eval_reflection_aot_member_object_array_result( Ok(result) } +/// Returns true when a ReflectionClass member passes an optional modifier filter. +fn eval_reflection_member_matches_filter( + member: &EvalReflectionMemberMetadata, + filter: Option, +) -> bool { + match filter { + Some(filter) => member.modifiers & filter != 0, + None => true, + } +} + +/// Parses the optional ReflectionClass member filter argument. +fn eval_reflection_member_filter( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut filter = None; + for arg in evaluated_args { + if let Some(name) = arg.name.as_deref() { + if name != "filter" { + return Err(EvalStatus::RuntimeFatal); + } + } + if filter.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + filter = Some(arg.value); + } + let Some(filter) = filter else { + return Ok(None); + }; + if values.is_null(filter)? { + return Ok(None); + } + let cast_filter = values.cast_int(filter)?; + let bytes = values.string_bytes(cast_filter)?; + values.release(cast_filter)?; + let text = std::str::from_utf8(&bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + text.parse::() + .map(|value| Some(value as u64)) + .map_err(|_| EvalStatus::RuntimeFatal) +} + /// Returns generated AOT member names for one reflected class. fn eval_reflection_aot_member_names( owner_kind: u64, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 4f51c8d621..66aacdcf39 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1413,6 +1413,13 @@ class EvalReflectListTarget { $ref = new ReflectionClass("EvalReflectListTarget"); $methods = $ref->getMethods(); $properties = $ref->getProperties(); +$staticMethods = $ref->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $ref->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$noMethods = $ref->getMethods(0); +$nullMethods = $ref->getMethods(null); +$staticProperties = $ref->getProperties(ReflectionProperty::IS_STATIC); +$protectedProperties = $ref->getProperties(filter: ReflectionProperty::IS_PROTECTED); +$noProperties = $ref->getProperties(0); echo count($methods); echo ":"; echo count($properties); echo ":"; echo ReflectionMethod::IS_STATIC; echo ":"; echo ReflectionMethod::IS_PRIVATE; echo ":"; $direct = new ReflectionMethod("EvalReflectListTarget", "helper"); @@ -1441,6 +1448,13 @@ foreach ($properties as $property) { echo "M"; echo $property->getModifiers(); } } +echo ":"; +echo count($staticMethods); echo $staticMethods[0]->getName(); echo ":"; +echo count($privateMethods); echo $privateMethods[0]->getName(); echo ":"; +echo count($noMethods); echo ":"; echo count($nullMethods); echo ":"; +echo count($staticProperties); echo $staticProperties[0]->getName(); echo ":"; +echo count($protectedProperties); echo $protectedProperties[0]->getName(); echo ":"; +echo count($noProperties); return true;"#, ) .expect("parse eval fragment"); @@ -1449,7 +1463,10 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20"); + assert_eq!( + values.output, + "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20:1helper:1helper:0:2:1token:1visible:0" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index ab1d26ce73..cfe438c9f2 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -208,11 +208,12 @@ lookups return `false` when no constant or case is visible. materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate flags. For generated/AOT classes, `ReflectionClass::getMethod()` / -`getProperty()` and no-filter `getMethods()` / `getProperties()` materialize -reflection objects from emitted member-name and predicate metadata. AOT member -reflection currently exposes names, declaring classes, and predicate flags, but -does not expose full AOT parameter, attribute, property type, or property -default-value metadata. `ReflectionMethod::getDeclaringClass()` and +`getProperty()` and `getMethods()` / `getProperties()` materialize reflection +objects from emitted member-name and predicate metadata, including optional +modifier filters. AOT member reflection currently exposes names, declaring +classes, and predicate flags, but does not expose full AOT parameter, +attribute, property type, or property default-value metadata. +`ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected member. `ReflectionClass::getConstructor()` returns a materialized diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c60d804359..7cd4f91813 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6620,6 +6620,11 @@ foreach ($properties as $property) { } echo $listedId ? "I" : "i"; echo $listedName ? "N" : "n"; +$publicProperties = $class->getProperties(ReflectionProperty::IS_PUBLIC); +$protectedProperties = $class->getProperties(filter: ReflectionProperty::IS_PROTECTED); +echo ":" . count($publicProperties) . $publicProperties[0]->getName(); +echo ":" . count($protectedProperties) . $protectedProperties[0]->getName(); +echo ":" . count($class->getProperties(0)); try { $class->getProperty("missing"); echo "bad"; @@ -6635,7 +6640,7 @@ try { ); assert_eq!( out.stdout, - "IINCpsHmGEvalAotPromotedBaseN:2IN:Property EvalAotPromotedBase::$missing does not exist" + "IINCpsHmGEvalAotPromotedBaseN:2IN:1id:1name:0:Property EvalAotPromotedBase::$missing does not exist" ); } @@ -6689,6 +6694,23 @@ echo ":" . count($methods); echo $seenRun ? "R" : "r"; echo $seenBase ? "B" : "b"; echo $seenLocked ? "L" : "l"; +$staticMethods = $class->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $class->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$seenStatic = false; +$seenHidden = false; +foreach ($staticMethods as $method) { + if (strtolower($method->getName()) === "basestatic") { + $seenStatic = $method->isProtected(); + } +} +foreach ($privateMethods as $method) { + if (strtolower($method->getName()) === "hidden") { + $seenHidden = $method->isPrivate(); + } +} +echo ":" . count($staticMethods) . ($seenStatic ? "S" : "s"); +echo ":" . count($privateMethods) . ($seenHidden ? "H" : "h"); +echo ":" . count($class->getMethods(0)); try { $class->getMethod("missing"); echo "bad"; @@ -6704,7 +6726,7 @@ try { ); assert_eq!( out.stdout, - "RBm:runUsEvalAotReflectMethodChild:SP:lockedFUEvalAotReflectMethodBase:4RBL:Method EvalAotReflectMethodChild::missing() does not exist" + "RBm:runUsEvalAotReflectMethodChild:SP:lockedFUEvalAotReflectMethodBase:4RBL:1S:1H:0:Method EvalAotReflectMethodChild::missing() does not exist" ); } @@ -6820,6 +6842,13 @@ class EvalReflectListTarget { $ref = new ReflectionClass("EvalReflectListTarget"); $methods = $ref->getMethods(); $properties = $ref->getProperties(); +$staticMethods = $ref->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $ref->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$noMethods = $ref->getMethods(0); +$nullMethods = $ref->getMethods(null); +$staticProperties = $ref->getProperties(ReflectionProperty::IS_STATIC); +$protectedProperties = $ref->getProperties(filter: ReflectionProperty::IS_PROTECTED); +$noProperties = $ref->getProperties(0); echo count($methods) . ":" . count($properties) . ":"; echo ReflectionMethod::IS_STATIC . ":" . ReflectionMethod::IS_PRIVATE . ":"; $direct = new ReflectionMethod("EvalReflectListTarget", "helper"); @@ -6847,7 +6876,14 @@ foreach ($properties as $property) { echo $property->isPrivate() ? "R" : "r"; echo "M" . $property->getModifiers(); } -}'); +} +echo ":"; +echo count($staticMethods) . $staticMethods[0]->getName() . ":"; +echo count($privateMethods) . $privateMethods[0]->getName() . ":"; +echo count($noMethods) . ":" . count($nullMethods) . ":"; +echo count($staticProperties) . $staticProperties[0]->getName() . ":"; +echo count($protectedProperties) . $protectedProperties[0]->getName() . ":"; +echo count($noProperties);'); "#, ); assert!( @@ -6855,7 +6891,10 @@ foreach ($properties as $property) { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20"); + assert_eq!( + out.stdout, + "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20:1helper:1helper:0:2:1token:1visible:0" + ); } /// Verifies eval ReflectionClass getMethod/getProperty return single member objects. From 94358db5f3e15571e74eefe3633a5e419b9ab655 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 21:13:53 +0200 Subject: [PATCH 0444/1208] Support eval reflection static properties --- .../elephc-eval/src/interpreter/reflection.rs | 211 ++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 27 +++ .../tests/builtins_class_metadata.rs | 58 +++++ docs/php/eval.md | 10 +- src/types/checker/builtin_types/reflection.rs | 65 ++++++ tests/codegen/eval.rs | 59 ++++- 6 files changed, 425 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index fc0b0225a0..f4b8bcdabf 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -462,6 +462,127 @@ pub(in crate::interpreter) fn eval_reflection_class_get_default_properties_resul Ok(Some(result)) } +/// Handles eval-backed `ReflectionClass::getStaticProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_static_properties_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getStaticProperties") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let property_names = eval_reflection_static_property_names(&reflected_name, context); + let mut result = values.assoc_new(property_names.len())?; + for name in property_names { + let Some(value) = + eval_reflection_static_property_value(&reflected_name, &name, context, values)? + else { + continue; + }; + let key = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getStaticPropertyValue()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_static_property_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getStaticPropertyValue") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let (property_name, default_value) = + eval_reflection_static_property_value_args(evaluated_args)?; + let property_name = eval_reflection_string_arg(property_name, values)?; + if let Some(value) = + eval_reflection_static_property_value(&reflected_name, &property_name, context, values)? + { + return Ok(Some(value)); + } + if let Some(default_value) = default_value { + return Ok(Some(default_value)); + } + eval_throw_reflection_exception( + &format!( + "Property {}::${} does not exist", + reflected_name, property_name + ), + context, + values, + ) +} + +/// Handles eval-backed `ReflectionClass::setStaticPropertyValue()` calls. +pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setStaticPropertyValue") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args( + &[String::from("name"), String::from("value")], + evaluated_args, + )?; + let property_name = eval_reflection_string_arg(args[0], values)?; + let Some(member) = eval_reflection_property_metadata(&reflected_name, &property_name, context) + else { + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); + }; + if !member.is_static { + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); + } + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(replaced) = context.set_static_property(declaring_class, &property_name, args[1]) { + values.release(replaced)?; + } + values.null().map(Some) +} + /// Handles eval-backed `ReflectionClass::getReflectionConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_result( identity: u64, @@ -2732,6 +2853,96 @@ fn eval_reflection_default_property_names( context.class_property_names(reflected_name) } +/// Returns eval property names that can contribute to `ReflectionClass::getStaticProperties()`. +fn eval_reflection_static_property_names( + reflected_name: &str, + context: &ElephcEvalContext, +) -> Vec { + eval_reflection_default_property_names(reflected_name, context) + .into_iter() + .filter(|name| { + eval_reflection_property_metadata(reflected_name, name, context) + .is_some_and(|property| property.is_static) + }) + .collect() +} + +/// Returns the current eval static property value or the trait metadata default. +fn eval_reflection_static_property_value( + reflected_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) + else { + return Ok(None); + }; + if !member.is_static { + return Ok(None); + } + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(value) = context.static_property(declaring_class, property_name) { + return Ok(Some(value)); + } + member + .default_value + .as_ref() + .map(|default| eval_method_parameter_default(default, context, values)) + .transpose() +} + +/// Binds `getStaticPropertyValue()` arguments while preserving whether a default was supplied. +fn eval_reflection_static_property_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let params = [String::from("name"), String::from("default")]; + let mut bound_args = [None, None]; + let mut next_positional = 0; + for arg in evaluated_args { + if let Some(name) = arg.name { + let Some(position) = params.iter().position(|param| param == &name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[position].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[position] = Some(arg.value); + } else { + while next_positional < bound_args.len() && bound_args[next_positional].is_some() { + next_positional += 1; + } + if next_positional >= bound_args.len() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg.value); + next_positional += 1; + } + } + let property_name = bound_args[0].ok_or(EvalStatus::RuntimeFatal)?; + Ok((property_name, bound_args[1])) +} + +/// Throws PHP's `ReflectionException` for invalid static-property writes. +fn eval_reflection_static_property_missing_for_set( + reflected_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_throw_reflection_exception( + &format!( + "Class {} does not have a property named {}", + reflected_name, property_name + ), + context, + values, + ) +} + /// Returns ReflectionProperty default metadata for concrete eval properties. fn eval_reflection_property_default_value(property: &EvalClassProperty) -> Option { if let Some(default) = property.default() { diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 2ea4d74356..34ac14e51d 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2391,6 +2391,33 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_get_static_properties_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_static_property_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_set_static_property_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_get_reflection_constant_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 66aacdcf39..7629913c16 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1333,6 +1333,64 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass exposes and mutates eval static property values. +#[test] +fn execute_program_reflection_class_static_property_values() { + let program = parse_fragment( + br#"class EvalReflectStaticBase { + public static $base = "b"; + protected static $prot = "p"; + private static $shadow = "base-hidden"; + public $instance = "i"; +} +class EvalReflectStaticChild extends EvalReflectStaticBase { + public static $child = "c"; + private static $shadow = "child-hidden"; + public static int $count = 1; +} +EvalReflectStaticChild::$child = "mut"; +$ref = new ReflectionClass("EvalReflectStaticChild"); +$statics = $ref->getStaticProperties(); +echo count($statics); echo ":"; +echo $statics["child"]; echo ":"; +echo $statics["base"]; echo ":"; +echo $statics["prot"]; echo ":"; +echo $statics["shadow"]; echo ":"; +echo $ref->getStaticPropertyValue("count"); echo ":"; +$ref->setStaticPropertyValue("shadow", "changed"); +echo $ref->getStaticPropertyValue("shadow"); echo ":"; +$ref->setStaticPropertyValue(name: "count", value: 5); +echo EvalReflectStaticChild::$count; echo ":"; +echo $ref->getStaticPropertyValue("instance", "fallback"); echo ":"; +echo $ref->getStaticPropertyValue("missing", "fallback"); echo ":"; +try { + $ref->getStaticPropertyValue("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +try { + $ref->setStaticPropertyValue("instance", "bad"); + echo "bad"; +} catch (ReflectionException $e) { + echo "S"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "5:mut:b:p:child-hidden:1:changed:5:fallback:fallback:E:S" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval ReflectionParameter exposes declaring class metadata. #[test] fn execute_program_reflects_eval_parameter_declaring_class() { diff --git a/docs/php/eval.md b/docs/php/eval.md index cfe438c9f2..df3d7fe0b0 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -199,10 +199,12 @@ For generated/AOT classes, `ReflectionClass::hasMethod()` and `hasProperty()` can also probe emitted method/property metadata without requiring the full member lists to be materialized on the `ReflectionClass` object. `ReflectionClass::hasConstant()`, `getConstant()`, `getConstants()`, -`getDefaultProperties()`, `getReflectionConstant()`, and -`getReflectionConstants()` expose eval-visible class constants, interface -constants, trait constants, enum constants, enum cases, and supported -materialized property defaults. Constant lookup is case-sensitive; single-value +`getDefaultProperties()`, `getStaticProperties()`, +`getStaticPropertyValue()`, `setStaticPropertyValue()`, +`getReflectionConstant()`, and `getReflectionConstants()` expose eval-visible +class constants, interface constants, trait constants, enum constants, enum +cases, supported materialized property defaults, and current eval-declared +static property values. Constant lookup is case-sensitive; single-value lookups return `false` when no constant or case is visible. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 93d12eba09..be104ca292 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -987,6 +987,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(mixed_type()), empty_array(), ), + builtin_property( + "__static_properties", + Visibility::Private, + Some(mixed_type()), + empty_array(), + ), builtin_property( "__reflection_constants", Visibility::Private, @@ -1059,6 +1065,9 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_get_constant_method(), builtin_reflection_class_mixed_method("getConstants", "__constants"), builtin_reflection_class_mixed_method("getDefaultProperties", "__default_properties"), + builtin_reflection_class_mixed_method("getStaticProperties", "__static_properties"), + builtin_reflection_class_get_static_property_value_method(), + builtin_reflection_class_set_static_property_value_method(), builtin_reflection_class_array_method( "getReflectionConstants", "__reflection_constants", @@ -1298,6 +1307,62 @@ fn builtin_reflection_class_get_constant_method() -> ClassMethod { } } +/// Returns `ReflectionClass::getStaticPropertyValue()` backed by the static-property map. +fn builtin_reflection_class_get_static_property_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name = variable_expr("name", dummy_span); + let value_read = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(reflection_this_property("__static_properties", dummy_span)), + index: Box::new(name), + }, + dummy_span, + ); + ClassMethod { + name: "getStaticPropertyValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![ + ("name".to_string(), Some(TypeExpr::Str), None, false), + ("default".to_string(), Some(mixed_type()), null_expr(), false), + ], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![Stmt::new(StmtKind::Return(Some(value_read)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns the public `ReflectionClass::setStaticPropertyValue()` signature. +fn builtin_reflection_class_set_static_property_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "setStaticPropertyValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![ + ("name".to_string(), Some(TypeExpr::Str), None, false), + ("value".to_string(), Some(mixed_type()), None, false), + ], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Void), + body: Vec::new(), + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns `ReflectionClass::getReflectionConstant()` backed by reflected constant objects. fn builtin_reflection_class_get_reflection_constant_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7cd4f91813..192dfb979a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7112,7 +7112,7 @@ echo $listed->getDefaultValue() === null ? "null" : "bad";'); fn test_eval_reflection_class_get_default_properties_metadata() { let out = compile_and_run_capture( r#"getStaticProperties(); +echo count($statics) . ":"; +echo $statics["child"] . ":"; +echo $statics["base"] . ":"; +echo $statics["prot"] . ":"; +echo $statics["shadow"] . ":"; +echo $ref->getStaticPropertyValue("count") . ":"; +$ref->setStaticPropertyValue("shadow", "changed"); +echo $ref->getStaticPropertyValue("shadow") . ":"; +$ref->setStaticPropertyValue(name: "count", value: 5); +echo EvalReflectStaticChild::$count . ":"; +echo $ref->getStaticPropertyValue("instance", "fallback") . ":"; +echo $ref->getStaticPropertyValue("missing", "fallback") . ":"; +try { + $ref->getStaticPropertyValue("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +try { + $ref->setStaticPropertyValue("instance", "bad"); + echo "bad"; +} catch (ReflectionException $e) { + echo "S"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "5:mut:b:p:child-hidden:1:changed:5:fallback:fallback:E:S" + ); +} + /// Verifies eval ReflectionParameter exposes the declaring class for method parameters. #[test] fn test_eval_reflection_parameter_reports_declaring_class() { From 96da190154fe9f4163f3e25364af9a1cb7b00740 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 21:23:58 +0200 Subject: [PATCH 0445/1208] Support eval reflection constructorless instantiation --- .../elephc-eval/src/interpreter/statements.rs | 88 +++++++++++++++---- .../tests/builtins_class_metadata.rs | 35 ++++++++ docs/php/eval.md | 3 + src/types/checker/builtin_types/reflection.rs | 36 ++++++++ tests/codegen/eval.rs | 29 ++++++ 5 files changed, 175 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 34ac14e51d..43f65f4bbb 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2226,6 +2226,33 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_dynamic_class_allocate_object(class, context, caller_scope, values)?; + if let Some((constructor_class, constructor)) = + context.class_method(class.name(), "__construct") + { + validate_eval_member_access(&constructor_class, constructor.visibility(), context)?; + eval_dynamic_method_with_values( + &constructor_class, + class.name(), + &constructor, + object, + evaluated_args, + context, + values, + )?; + } else if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(object) +} + +/// Creates a backing object for an eval-declared class without running its constructor. +fn eval_dynamic_class_allocate_object( + class: &EvalClass, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, ) -> Result { if class.is_abstract() || context.has_enum(class.name()) { return Err(EvalStatus::RuntimeFatal); @@ -2254,22 +2281,6 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( values.property_set(object, &storage_name, value)?; } } - if let Some((constructor_class, constructor)) = - context.class_method(class.name(), "__construct") - { - validate_eval_member_access(&constructor_class, constructor.visibility(), context)?; - eval_dynamic_method_with_values( - &constructor_class, - class.name(), - &constructor, - object, - evaluated_args, - context, - values, - )?; - } else if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } Ok(object) } @@ -2463,6 +2474,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(instance); } + if let Some(instance) = eval_reflection_class_new_instance_without_constructor_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(instance); + } let Some(class) = context.dynamic_object_class(identity) else { let class_name = runtime_object_class_name(object, values)?; let evaluated_args = bind_native_callable_args( @@ -2535,6 +2555,42 @@ fn eval_reflection_class_new_instance_result( Ok(Some(instance)) } +/// Allocates the class named by a materialized eval `ReflectionClass` without running `__construct()`. +fn eval_reflection_class_new_instance_without_constructor_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("newInstanceWithoutConstructor") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(class) = context.class(&reflected_name).cloned() { + let mut scope = ElephcEvalScope::new(); + return eval_dynamic_class_allocate_object(&class, context, &mut scope, values).map(Some); + } + if context.has_interface(&reflected_name) + || context.has_trait(&reflected_name) + || context.has_enum(&reflected_name) + { + return Err(EvalStatus::RuntimeFatal); + } + let class_name = context + .resolve_class_name(&reflected_name) + .unwrap_or(reflected_name); + values.new_object(&class_name).map(Some) +} + /// Instantiates an eval-declared attribute class for `ReflectionAttribute::newInstance()`. fn eval_reflection_attribute_new_instance_result( attribute: &EvalAttribute, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 7629913c16..7f8645a430 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1617,6 +1617,41 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::newInstanceWithoutConstructor skips eval constructors. +#[test] +fn execute_program_reflection_class_new_instance_without_constructor_allocates_eval_class() { + let program = parse_fragment( + br#"class EvalReflectNoCtorTarget { + public $label = "default"; + private $secret = "hidden"; + public function __construct() { + $this->label = "ctor"; + } + public function label() { + return $this->label; + } + public function secret() { + return $this->secret; + } +} +$ref = new ReflectionClass("EvalReflectNoCtorTarget"); +$without = $ref->newInstanceWithoutConstructor(); +echo $without->label(); echo ":"; +echo $without->secret(); echo ":"; +$with = $ref->newInstance(); +echo $with->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "default:hidden:ctor"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. #[test] fn execute_program_reflects_eval_constant_and_enum_case_attributes() { diff --git a/docs/php/eval.md b/docs/php/eval.md index df3d7fe0b0..0e7ad63b44 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -225,6 +225,9 @@ returns a materialized `ReflectionClass` for eval-declared parent classes or `false` when no parent class exists. `ReflectionClass::newInstance()` constructs eval-declared reflected classes and forwards constructor arguments through eval's positional, named, and unpacking-aware call binding. +`ReflectionClass::newInstanceWithoutConstructor()` allocates eval-declared +reflected classes, initializes supported property defaults, and skips +`__construct()`. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index be104ca292..5253e6b43a 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -702,6 +702,35 @@ fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { } } +/// Returns a public `ReflectionClass::newInstanceWithoutConstructor()` method. +/// +/// Eval dispatch supplies the real constructorless allocation. The body remains +/// a conservative placeholder so the built-in class metadata exposes the PHP +/// method without forcing ordinary method lowering to construct a class. +fn builtin_reflection_class_new_instance_without_constructor_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "newInstanceWithoutConstructor".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds the `ReflectionNamedType` shell: a parameter/return type rendered as a /// runtime object with a name, nullability flag, and builtin flag. Populated at /// codegen from the declared type. @@ -1106,6 +1135,7 @@ fn builtin_reflection_class() -> FlattenedClass { false, ), builtin_reflection_class_new_instance_method(), + builtin_reflection_class_new_instance_without_constructor_method(), builtin_reflection_owner_get_attributes_method(), ], attributes: Vec::new(), @@ -2742,6 +2772,12 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.declared_params.push(false); } } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("newInstanceWithoutConstructor")) + { + sig.return_type = PhpType::Mixed; + } } if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { for method_name in ["isstatic", "ispublic", "isprotected", "isprivate"] { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 192dfb979a..827c903aad 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7350,6 +7350,35 @@ echo $second->label();'); assert_eq!(out, "EF:GH"); } +/// Verifies eval ReflectionClass::newInstanceWithoutConstructor allocates without constructors. +#[test] +fn test_eval_reflection_class_new_instance_without_constructor_allocates_eval_class() { + let out = compile_and_run( + r#"label = "ctor"; + } + public function label() { + return $this->label; + } + public function secret() { + return $this->secret; + } +} +$ref = new ReflectionClass("EvalReflectNoCtorTarget"); +$without = $ref->newInstanceWithoutConstructor(); +echo $without->label() . ":"; +echo $without->secret() . ":"; +$with = $ref->newInstance(); +echo $with->label();'); +"#, + ); + assert_eq!(out, "default:hidden:ctor"); +} + /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. #[test] fn test_eval_reflection_constant_and_enum_case_attributes() { From 8362304977a10413a9b6863b0c1cdfdf3a212231 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 21:37:08 +0200 Subject: [PATCH 0446/1208] Support static reflection constructorless instantiation --- docs/php/classes.md | 3 +- src/codegen/lower_inst.rs | 3 + src/codegen/lower_inst/objects.rs | 131 ++++++++++++++++++++++++++++++ src/ir/instr.rs | 7 +- src/ir/validator.rs | 14 +++- src/ir_lower/context.rs | 1 + src/ir_lower/expr/mod.rs | 60 ++++++++++++++ src/ir_lower/program.rs | 8 ++ tests/codegen/oop/attributes.rs | 31 +++++++ 9 files changed, 254 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 7a5dc30544..7586845d57 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1180,6 +1180,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getParentClass()` | `new ReflectionClass($class_name)` | Return the reflected parent class as a `ReflectionClass`, or `false` when the reflected class-like symbol has no parent class | | `ReflectionClass::getProperties()` | `new ReflectionClass($class_name)` | Return `ReflectionProperty` objects for properties visible through the reflected class-like metadata | | `ReflectionClass::newInstance()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class with forwarded constructor arguments | +| `ReflectionClass::newInstanceWithoutConstructor()` | `new ReflectionClass($class_name)` | Allocate an instance of the reflected class without running `__construct()` | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | | `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user function name | | `ReflectionFunction::getAttributes()` | `new ReflectionFunction($function_name)` | Return `ReflectionAttribute` objects for function attributes | @@ -1313,7 +1314,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, and `newInstance()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 9fde7805c8..06c0e9faca 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -170,6 +170,9 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::ObjectCloneShallow => objects::lower_object_clone_shallow(ctx, &inst), Op::DynamicObjectNew => objects::lower_dynamic_object_new(ctx, &inst), Op::DynamicObjectNewMixed => objects::lower_dynamic_object_new_mixed(ctx, &inst), + Op::DynamicObjectNewWithoutConstructorMixed => { + objects::lower_dynamic_object_new_without_constructor_mixed(ctx, &inst) + } Op::PropGet => objects::lower_prop_get(ctx, &inst), Op::LoadPropRefCell => objects::lower_load_prop_ref_cell(ctx, &inst), Op::LoadArrayElemRefCell => arrays::lower_load_array_elem_ref_cell(ctx, &inst), diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 416ea6961e..d53b06cadd 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1282,6 +1282,59 @@ pub(super) fn lower_dynamic_object_new_mixed( Ok(()) } +/// Lowers dynamic allocation that intentionally skips PHP constructor dispatch. +pub(super) fn lower_dynamic_object_new_without_constructor_mixed( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let class_name_value = expect_operand(inst, 0)?; + if inst.operands.len() != 1 { + return Err(CodegenIrError::invalid_module( + "dynamic_object_new_without_constructor_mixed expects only a class operand", + )); + } + let result = inst.result.ok_or_else(|| { + CodegenIrError::invalid_module( + "dynamic_object_new_without_constructor_mixed missing result value", + ) + })?; + let done_label = ctx.next_label("dynamic_new_no_ctor_mixed_done"); + let non_string_label = ctx.next_label("dynamic_new_no_ctor_mixed_non_string"); + if !emit_generic_dynamic_new_class_string(ctx, class_name_value, &non_string_label)? { + emit_dynamic_new_invalid_class_name_fatal(ctx); + return Ok(()); + } + abi::emit_push_result_value(ctx.emitter, &PhpType::Str); + + let fallback_label = ctx.next_label("dynamic_new_no_ctor_mixed_fallback"); + let candidates = dynamic_new_without_constructor_mixed_candidates(ctx, inst)?; + let case_labels = candidates + .iter() + .map(|candidate| { + let label = ctx.next_label("dynamic_new_no_ctor_mixed_case"); + emit_branch_if_dynamic_new_mixed_class_name_matches(ctx, &candidate.class_name, &label); + label + }) + .collect::>(); + abi::emit_jump(ctx.emitter, &fallback_label); + + for (candidate, label) in candidates.iter().zip(case_labels.iter()) { + ctx.emitter.label(label); + abi::emit_release_temporary_stack(ctx.emitter, 16); + emit_dynamic_new_without_constructor_mixed_candidate(ctx, candidate, result)?; + abi::emit_jump(ctx.emitter, &done_label); + } + + ctx.emitter.label(&fallback_label); + emit_dynamic_new_class_not_found_fatal(ctx); + + ctx.emitter.label(&non_string_label); + emit_dynamic_new_invalid_class_name_fatal(ctx); + + ctx.emitter.label(&done_label); + Ok(()) +} + /// Materializes the dynamic class name as a string result pair, branching for non-string Mixed. fn emit_generic_dynamic_new_class_string( ctx: &mut FunctionContext<'_>, @@ -1339,6 +1392,27 @@ fn dynamic_new_mixed_candidates( Ok(candidates) } +/// Returns AOT candidates that can be allocated without constructor dispatch. +fn dynamic_new_without_constructor_mixed_candidates( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result> { + let mut candidates = Vec::new(); + let mut sorted_classes = ctx.module.class_infos.iter().collect::>(); + sorted_classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in sorted_classes { + if !is_dynamic_new_mixed_aot_candidate(class_name) { + continue; + } + if let Some(candidate) = + dynamic_new_without_constructor_candidate(ctx, class_name, class_info, inst)? + { + candidates.push(candidate); + } + } + Ok(candidates) +} + /// Returns true when a class can safely use the static allocation path for `new $name`. fn is_dynamic_new_mixed_aot_candidate(class_name: &str) -> bool { if class_name.starts_with("__Elephc") { @@ -1559,6 +1633,32 @@ fn emit_dynamic_new_mixed_candidate( ctx.store_result_value(result) } +/// Allocates one constructorless dynamic-new candidate and boxes it as Mixed. +fn emit_dynamic_new_without_constructor_mixed_candidate( + ctx: &mut FunctionContext<'_>, + candidate: &DynamicNewCandidate, + result: ValueId, +) -> Result<()> { + emit_object_allocation( + ctx, + candidate.class_id, + candidate.property_count, + candidate.allow_dynamic_properties, + &candidate.uninitialized_marker_offsets, + )?; + let object_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, object_reg); + let object_base_reg = abi::secondary_scratch_reg(ctx.emitter); + for default in &candidate.property_defaults { + abi::emit_load_temporary_stack_slot(ctx.emitter, object_base_reg, 0); + emit_property_default(ctx, object_base_reg, default)?; + } + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_release_temporary_stack(ctx.emitter, 16); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(candidate.class_name.clone())); + ctx.store_result_value(result) +} + /// Allocates a dynamic `SplFixedArray` candidate through its runtime storage constructor. fn emit_dynamic_new_mixed_spl_fixed_array_candidate( ctx: &mut FunctionContext<'_>, @@ -1808,6 +1908,37 @@ fn dynamic_new_candidate( })) } +/// Builds a dynamic-new candidate that deliberately omits constructor invocation. +fn dynamic_new_without_constructor_candidate( + ctx: &FunctionContext<'_>, + class_name: &str, + class_info: &ClassInfo, + inst: &Instruction, +) -> Result> { + if class_info.is_abstract || ctx.module.enum_infos.contains_key(class_name) { + return Ok(None); + } + if class_name != "stdClass" && known_dynamic_new_builtin_class_names().contains(&class_name) { + return Ok(None); + } + if class_name == "SplFixedArray" || is_spl_doubly_linked_list_family(class_name) { + return Ok(None); + } + if class_interfaces_require_missing_method_symbols(ctx, class_name, class_info) { + return Ok(None); + } + let property_defaults = collect_property_defaults(class_info, inst)?; + Ok(Some(DynamicNewCandidate { + class_name: class_name.to_string(), + class_id: class_info.class_id, + property_count: class_info.properties.len(), + allow_dynamic_properties: class_info.allow_dynamic_properties, + uninitialized_marker_offsets: uninitialized_property_marker_offsets(class_info), + property_defaults, + constructor_impl: None, + })) +} + /// Builds dynamic-new metadata for SPL classes whose storage is runtime-managed. fn spl_runtime_storage_dynamic_new_candidate( class_name: &str, diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 04eda26da0..c840516b28 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -313,6 +313,7 @@ pub enum Op { ObjectCloneShallow, DynamicObjectNew, DynamicObjectNewMixed, + DynamicObjectNewWithoutConstructorMixed, PropGet, PropSet, /// Loads the raw reference-cell pointer stored in a reference property's slot, @@ -488,7 +489,8 @@ impl Op { } HashSpread => E::READS_HEAP | E::WRITES_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP, IterStart | IterCurrentKey | IterCurrentValue | IteratorMethodCall - | SplRuntimeCall | DynamicObjectNew | DynamicObjectNewMixed | DynamicPropGet | NullsafePropGet + | SplRuntimeCall | DynamicObjectNew | DynamicObjectNewMixed + | DynamicObjectNewWithoutConstructorMixed | DynamicPropGet | NullsafePropGet | NullsafeMethodCall | MethodLookup | MethodCall | StaticMethodCall | InstanceOfDynamic | MixedNumericBinop | LooseEq | LooseNotEq | Spaceship => { E::READS_HEAP | E::MAY_DEOPT @@ -682,6 +684,9 @@ impl Op { ObjectCloneShallow => "object_clone_shallow", DynamicObjectNew => "dynamic_object_new", DynamicObjectNewMixed => "dynamic_object_new_mixed", + DynamicObjectNewWithoutConstructorMixed => { + "dynamic_object_new_without_constructor_mixed" + } PropGet => "prop_get", PropSet => "prop_set", LoadPropRefCell => "load_prop_ref_cell", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 8c64cb8c51..5f8031a2b2 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -435,8 +435,18 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio BufferLen | BufferGet | BufferSet | BufferFree => { check_first_heap(function, inst_id, inst, IrHeapKind::Buffer, "Heap(Buffer)") } - PropGet | PropSet | LoadPropRefCell | DynamicPropGet | DynamicPropSet | NullsafePropGet - | NullsafeMethodCall | MethodLookup | MethodCall | InstanceOf | InstanceOfDynamic => { + DynamicObjectNewWithoutConstructorMixed + | PropGet + | PropSet + | LoadPropRefCell + | DynamicPropGet + | DynamicPropSet + | NullsafePropGet + | NullsafeMethodCall + | MethodLookup + | MethodCall + | InstanceOf + | InstanceOfDynamic => { check_count_at_least(inst_id, inst, 1, "at least 1") } _ => Ok(()), diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index fa66eb4f1f..4cc60363d5 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1164,6 +1164,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::ObjectCloneShallow | Op::DynamicObjectNew | Op::DynamicObjectNewMixed + | Op::DynamicObjectNewWithoutConstructorMixed | Op::ClosureNew | Op::FirstClassCallableNew | Op::CallableArrayNew diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 8b2dbfbe81..0092618e96 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9863,6 +9863,11 @@ fn lower_method_call( if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { return lower_reflection_class_new_instance(ctx, Some(object_expr), object, args, expr); } + if op == Op::MethodCall + && is_reflection_class_new_instance_without_constructor_call(ctx, object.value, method) + { + return lower_reflection_class_new_instance_without_constructor(ctx, object, args, expr); + } if matches!( ctx.builder.value_php_type(object.value).codegen_repr(), PhpType::Callable @@ -10088,6 +10093,27 @@ fn lower_reflection_class_new_instance( ) } +/// Lowers `ReflectionClass::newInstanceWithoutConstructor()` to constructorless allocation. +fn lower_reflection_class_new_instance_without_constructor( + ctx: &mut LoweringContext<'_, '_>, + object: LoweredValue, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + if !args.is_empty() { + return lower_reflection_class_new_instance_without_constructor_unsupported(ctx, expr); + } + let class_name = lower_property_get_from_value(ctx, object, "__name", Op::PropGet, expr); + ctx.emit_value( + Op::DynamicObjectNewWithoutConstructorMixed, + vec![class_name.value], + None, + PhpType::Mixed, + Op::DynamicObjectNewWithoutConstructorMixed.default_effects(), + Some(expr.span), + ) +} + /// Returns the source arguments that can be forwarded to `new $class(...)`. fn reflection_class_new_instance_args(args: &[Expr]) -> Vec { if has_static_call_spread_args(args) { @@ -10202,6 +10228,19 @@ fn lower_reflection_class_new_instance_unsupported( result } +/// Emits a runtime fatal for unsupported `newInstanceWithoutConstructor()` argument forms. +fn lower_reflection_class_new_instance_without_constructor_unsupported( + ctx: &mut LoweringContext<'_, '_>, + expr: &Expr, +) -> LoweredValue { + let result = lower_boxed_null(ctx, expr); + let message = ctx.intern_string( + "Fatal error: unsupported ReflectionClass::newInstanceWithoutConstructor() arguments\n", + ); + ctx.builder.terminate(Terminator::Fatal { message }); + result +} + /// Returns true when a method call targets the built-in `ReflectionClass::newInstance()`. fn is_reflection_class_new_instance_call( ctx: &LoweringContext<'_, '_>, @@ -10218,6 +10257,22 @@ fn is_reflection_class_new_instance_call( php_symbol_key(class_name.trim_start_matches('\\')) == "reflectionclass" } +/// Returns true when a method call targets `ReflectionClass::newInstanceWithoutConstructor()`. +fn is_reflection_class_new_instance_without_constructor_call( + ctx: &LoweringContext<'_, '_>, + object: ValueId, + method: &str, +) -> bool { + if php_symbol_key(method) != "newinstancewithoutconstructor" { + return false; + } + let object_ty = ctx.builder.value_php_type(object); + let Some((class_name, false)) = singular_object_class(&object_ty) else { + return false; + }; + php_symbol_key(class_name.trim_start_matches('\\')) == "reflectionclass" +} + /// Emits the PHP fatal terminator for an ordinary method call on null. fn terminate_method_call_on_null(ctx: &mut LoweringContext<'_, '_>, method: &str) { let message = format!("Fatal error: Call to a member function {}() on null\n", method); @@ -10320,6 +10375,11 @@ fn lower_method_call_with_receiver( if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { return lower_reflection_class_new_instance(ctx, None, object, args, expr); } + if op == Op::MethodCall + && is_reflection_class_new_instance_without_constructor_call(ctx, object.value, method) + { + return lower_reflection_class_new_instance_without_constructor(ctx, object, args, expr); + } let magic_args; let (dispatch_method, args) = if let Some(args) = magic_call_dispatch_args(ctx, object.value, method, args, expr.span) { diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index a4dd1f71e4..07b42c279a 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -1081,6 +1081,14 @@ fn referenced_builtin_spl_methods(module: &Module) -> Vec<(String, String)> { push_builtin_spl_metadata_methods(&mut methods, module, class_name); } } + Op::DynamicObjectNewWithoutConstructorMixed => { + for class_name in module.class_infos.keys() { + if !is_dynamic_new_mixed_metadata_candidate(class_name) { + continue; + } + push_builtin_spl_metadata_methods(&mut methods, module, class_name); + } + } Op::MethodCall | Op::NullsafeMethodCall => { let Some(receiver) = inst.operands.first().copied() else { continue; diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index c234352cb6..bd8c15a0e3 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2933,6 +2933,37 @@ echo $second->label(); assert_eq!(out, "AB:CD"); } +/// Verifies that `ReflectionClass::newInstanceWithoutConstructor()` allocates +/// reflected classes while preserving property defaults and skipping `__construct()`. +#[test] +fn test_reflection_class_new_instance_without_constructor_skips_constructor() { + let out = compile_and_run( + r#"label = "ctor"; + } + public function label(): string { + return $this->label; + } + public function secret(): string { + return $this->secret; + } +} +$ref = new ReflectionClass(ReflectNoCtorTarget::class); +$without = $ref->newInstanceWithoutConstructor(); +echo $without->label() . ":" . $without->secret() . ":"; +$with = new ReflectNoCtorTarget(); +echo $with->label() . ":"; +$inline = (new ReflectionClass("reflectnoctortarget"))->newInstanceWithoutConstructor(); +echo $inline->label(); +"#, + ); + assert_eq!(out, "default:hidden:ctor:default"); +} + /// Verifies inline `ReflectionClass::newInstance()` forwards named constructor /// arguments through the reflected constructor signature. #[test] From a0720cb989cc62608162bf88bd7db139788e798e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 21:44:38 +0200 Subject: [PATCH 0447/1208] Allow zero-arg reflection instantiation --- src/types/checker/builtin_types/reflection.rs | 13 ++++++-- tests/codegen/oop/attributes.rs | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 5253e6b43a..c8fb3acc4f 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2763,11 +2763,20 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("newInstance")) { sig.return_type = PhpType::Mixed; sig.variadic = Some("args".to_string()); - if !sig.params.iter().any(|(name, _)| name == "args") { + let variadic_default = Some(Expr::new( + ExprKind::ArrayLiteral(Vec::new()), + crate::span::Span::dummy(), + )); + if let Some(index) = sig.params.iter().position(|(name, _)| name == "args") { + while sig.defaults.len() <= index { + sig.defaults.push(None); + } + sig.defaults[index] = variadic_default; + } else { sig.params .push(("args".to_string(), PhpType::Array(Box::new(PhpType::Mixed)))); sig.param_type_exprs.push(None); - sig.defaults.push(None); + sig.defaults.push(variadic_default); sig.ref_params.push(false); sig.declared_params.push(false); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index bd8c15a0e3..1947af7cee 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2933,6 +2933,37 @@ echo $second->label(); assert_eq!(out, "AB:CD"); } +/// Verifies that `ReflectionClass::newInstance()` accepts zero constructor +/// arguments for classes with no-argument or absent constructors. +#[test] +fn test_reflection_class_new_instance_allows_zero_constructor_args() { + let out = compile_and_run( + r#"label = "ctor"; + } + public function label(): string { + return $this->label; + } +} +class ReflectNoCtorNewTarget { + public string $label = "plain"; + public function label(): string { + return $this->label; + } +} +$first = (new ReflectionClass(ReflectNoArgNewTarget::class))->newInstance(); +echo $first->label() . ":"; +$ref = new ReflectionClass(ReflectNoCtorNewTarget::class); +$second = $ref->newInstance(); +echo $second->label(); +"#, + ); + assert_eq!(out, "ctor:plain"); +} + /// Verifies that `ReflectionClass::newInstanceWithoutConstructor()` allocates /// reflected classes while preserving property defaults and skipping `__construct()`. #[test] From 9f1acb1fc08581f327cc983afdbefb03e61698f9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 21:58:41 +0200 Subject: [PATCH 0448/1208] Support eval constructor property promotion --- crates/elephc-eval/src/eval_ir.rs | 6 + .../tests/builtins_class_metadata.rs | 26 ++ .../src/interpreter/tests/classes.rs | 46 ++++ crates/elephc-eval/src/parser/statements.rs | 246 +++++++++++++----- .../src/parser/tests/classes_errors.rs | 194 ++++++++++++-- .../src/parser/tests/static_members.rs | 6 +- docs/php/eval.md | 28 +- 7 files changed, 445 insertions(+), 107 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 24a35de7e8..4de55f2659 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1367,6 +1367,12 @@ impl EvalClassProperty { self } + /// Returns a copy of this property marked as coming from constructor promotion. + pub const fn with_promoted(mut self) -> Self { + self.is_promoted = true; + self + } + /// Returns the PHP-visible property name without `$`. pub fn name(&self) -> &str { &self.name diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 7f8645a430..7f529d6e1c 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1054,6 +1054,32 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty reports eval constructor-promotion metadata. +#[test] +fn execute_program_reflection_property_reports_eval_promoted_metadata() { + let program = parse_fragment( + br#"class EvalReflectPromotedTarget { + public function __construct(public int $id, private string $name = "Ada") {} + public string $plain = "x"; +} +$id = new ReflectionProperty("EvalReflectPromotedTarget", "id"); +$name = new ReflectionProperty("EvalReflectPromotedTarget", "name"); +$plain = new ReflectionProperty("EvalReflectPromotedTarget", "plain"); +echo $id->isPromoted() ? "I" : "i"; +echo $name->isPromoted() ? "N" : "n"; +echo $plain->isPromoted() ? "P" : "p"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "INp"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod constructor/destructor predicates for eval methods. #[test] fn execute_program_reflection_method_reports_constructor_and_destructor() { diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 83dc073400..5434eaae5b 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -10,6 +10,52 @@ use super::super::*; use super::support::*; +/// Verifies promoted constructor properties initialize before the constructor body runs. +#[test] +fn execute_program_initializes_constructor_promoted_properties() { + let program = parse_fragment( + br#"class EvalPromotedUser { + public function __construct(public int $id, private string $name = "Ada") { + $this->id = $this->id + 1; + } + public function label() { return $this->id . ":" . $this->name; } +} +$user = new EvalPromotedUser(6); +echo $user->id; echo ":"; +return $user->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:"); + assert_eq!(values.get(result), FakeValue::String("7:Ada".to_string())); +} + +/// Verifies promoted readonly properties keep the normal constructor-only write rule. +#[test] +fn execute_program_rejects_promoted_readonly_property_write_after_constructor() { + let program = parse_fragment( + br#"class EvalPromotedReadonlyBox { + public function __construct(public readonly int $id) {} + public function replace($id) { $this->id = $id; } +} +$box = new EvalPromotedReadonlyBox(7); +echo $box->id; +$box->replace(8);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("promoted readonly property write should fail outside constructor"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies readonly eval properties can be initialized inside their constructor. #[test] fn execute_program_initializes_readonly_property_in_constructor() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 0d8a8e40af..fe56e2d4a1 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -20,6 +20,18 @@ use crate::eval_ir::{ }; use crate::lexer::TokenKind; +/// Parsed method parameters plus constructor-promotion side products. +pub(super) struct ParsedMethodParams { + params: Vec, + parameter_attributes: Vec>, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + promoted_properties: Vec, + promoted_assignments: Vec, +} + impl Parser { /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. pub(super) fn parse_stmt(&mut self) -> Result, EvalParseError> { @@ -488,7 +500,7 @@ impl Parser { visibility.unwrap_or(EvalVisibility::Public), is_final, )? - .with_attributes(attributes), + .with_attributes(attributes), ); return Ok(()); } @@ -497,15 +509,14 @@ impl Parser { if is_readonly { return Err(EvalParseError::UnsupportedConstruct); } - methods.push( - self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - is_abstract, - is_final, - )? - .with_attributes(attributes), - ); + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + is_abstract, + is_final, + )?; + properties.extend(promoted_properties); + methods.push(method.with_attributes(attributes)); return Ok(()); } @@ -743,14 +754,14 @@ impl Parser { } } - /// Parses `function name($param, ...) { ... }` or an abstract method signature. + /// Parses one class/trait/enum method and returns constructor-promoted properties. pub(super) fn parse_class_method_decl( &mut self, visibility: EvalVisibility, is_static: bool, is_abstract: bool, is_final: bool, - ) -> Result { + ) -> Result<(EvalClassMethod, Vec), EvalParseError> { self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -758,34 +769,47 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let ( + let ParsedMethodParams { params, parameter_attributes, parameter_types, parameter_defaults, parameter_is_by_ref, parameter_is_variadic, - ) = self.parse_method_params()?; + promoted_properties, + promoted_assignments, + } = self.parse_method_params(&name)?; + if !promoted_properties.is_empty() && (is_abstract || is_static) { + return Err(EvalParseError::UnsupportedConstruct); + } let body = if is_abstract { self.expect_semicolon()?; Vec::new() } else { - self.parse_block()? + let body = self.parse_block()?; + if promoted_assignments.is_empty() { + body + } else { + promoted_assignments.into_iter().chain(body).collect() + } }; - Ok(EvalClassMethod::with_visibility_and_modifiers( - name, - visibility, - is_static, - is_abstract, - is_final, - params, - body, - ) - .with_parameter_types(parameter_types) - .with_parameter_attributes(parameter_attributes) - .with_parameter_defaults(parameter_defaults) - .with_parameter_by_ref_flags(parameter_is_by_ref) - .with_parameter_variadic_flags(parameter_is_variadic)) + Ok(( + EvalClassMethod::with_visibility_and_modifiers( + name, + visibility, + is_static, + is_abstract, + is_final, + params, + body, + ) + .with_parameter_types(parameter_types) + .with_parameter_attributes(parameter_attributes) + .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) + .with_parameter_variadic_flags(parameter_is_variadic), + promoted_properties, + )) } /// Parses one public property declaration with an optional initializer. @@ -1017,7 +1041,7 @@ impl Parser { visibility.unwrap_or(EvalVisibility::Public), is_final, )? - .with_attributes(attributes), + .with_attributes(attributes), ); return Ok(()); } @@ -1025,15 +1049,14 @@ impl Parser { if is_readonly { return Err(EvalParseError::UnsupportedConstruct); } - methods.push( - self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - is_abstract, - is_final, - )? - .with_attributes(attributes), - ); + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + is_abstract, + is_final, + )?; + properties.extend(promoted_properties); + methods.push(method.with_attributes(attributes)); return Ok(()); } let visibility = visibility.unwrap_or(EvalVisibility::Public); @@ -1132,20 +1155,21 @@ impl Parser { visibility.unwrap_or(EvalVisibility::Public), is_final, )? - .with_attributes(attributes), + .with_attributes(attributes), ); return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - methods.push( - self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - false, - is_final, - )? - .with_attributes(attributes), - ); + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + false, + is_final, + )?; + if !promoted_properties.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + methods.push(method.with_attributes(attributes)); return Ok(()); } Err(EvalParseError::UnsupportedConstruct) @@ -1303,14 +1327,19 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::LParen)?; - let ( + let ParsedMethodParams { params, parameter_attributes, parameter_types, parameter_defaults, parameter_is_by_ref, parameter_is_variadic, - ) = self.parse_method_params()?; + promoted_properties, + promoted_assignments, + } = self.parse_method_params(&name)?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) .with_static(is_static) @@ -1718,44 +1747,62 @@ impl Parser { Ok(params) } - /// Parses a method parameter list and records type/default metadata. + /// Parses a method parameter list and records metadata plus promotion side effects. pub(super) fn parse_method_params( &mut self, - ) -> Result< - ( - Vec, - Vec>, - Vec>, - Vec>, - Vec, - Vec, - ), - EvalParseError, - > { + method_name: &str, + ) -> Result { let mut params = Vec::new(); let mut parameter_attributes = Vec::new(); let mut parameter_types = Vec::new(); let mut parameter_defaults = Vec::new(); let mut parameter_is_by_ref = Vec::new(); let mut parameter_is_variadic = Vec::new(); + let mut promoted_properties = Vec::new(); + let mut promoted_assignments = Vec::new(); if self.consume(TokenKind::RParen) { - return Ok(( + return Ok(ParsedMethodParams { params, parameter_attributes, parameter_types, parameter_defaults, parameter_is_by_ref, parameter_is_variadic, - )); + promoted_properties, + promoted_assignments, + }); } loop { let attributes = self.parse_attribute_groups()?; + let promotion = self.parse_promoted_parameter_modifiers()?; + if promotion.is_some() && !method_name.eq_ignore_ascii_case("__construct") { + return Err(EvalParseError::UnsupportedConstruct); + } let param_type = self.parse_optional_parameter_type()?; let is_by_ref = self.consume(TokenKind::Ampersand); let is_variadic = self.consume(TokenKind::Ellipsis); let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; + if promotion.is_some() && (is_by_ref || is_variadic) { + return Err(EvalParseError::UnsupportedConstruct); + } + if let Some((visibility, is_readonly)) = promotion { + promoted_properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name.clone(), + visibility, + false, + false, + is_readonly, + None, + ) + .with_type(param_type.clone()) + .with_promoted() + .with_attributes(attributes.clone()), + ); + promoted_assignments.push(promoted_property_assignment(name)); + } params.push(name.clone()); parameter_attributes.push(attributes); parameter_types.push(param_type); @@ -1786,14 +1833,70 @@ impl Parser { } } self.expect(TokenKind::RParen)?; - Ok(( + Ok(ParsedMethodParams { params, parameter_attributes, parameter_types, parameter_defaults, parameter_is_by_ref, parameter_is_variadic, - )) + promoted_properties, + promoted_assignments, + }) + } + + /// Parses visibility and readonly modifiers on a promoted constructor parameter. + fn parse_promoted_parameter_modifiers( + &mut self, + ) -> Result, EvalParseError> { + let mut visibility = None; + let mut is_readonly = false; + let mut saw_modifier = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Public); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Protected); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Private); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + is_readonly = true; + self.advance(); + } + _ => break, + } + } + if saw_modifier { + Ok(Some(( + visibility.unwrap_or(EvalVisibility::Public), + is_readonly, + ))) + } else { + Ok(None) + } } /// Consumes a supported method parameter type and returns retained metadata. @@ -2348,3 +2451,12 @@ fn property_hook_get_method(property_name: &str) -> String { fn property_hook_set_method(property_name: &str) -> String { format!("__propset_{property_name}") } + +/// Builds the implicit `$this->name = $name` assignment for a promoted parameter. +fn promoted_property_assignment(name: &str) -> EvalStmt { + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: name.to_string(), + value: EvalExpr::LoadVar(name.to_string()), + } +} diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 65cc855abe..70cd956100 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -129,12 +129,15 @@ fn parse_fragment_accepts_class_member_attribute_metadata() { EvalClassConstant::new("SEED", EvalExpr::Const(EvalConst::Int(1))) .with_attributes(vec![EvalAttribute::new("ConstMark", Some(Vec::new()))]) ], - vec![EvalClassProperty::new("value", None).with_attributes(vec![ - EvalAttribute::new( + vec![EvalClassProperty::new("value", None) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_attributes(vec![EvalAttribute::new( "PropMark", Some(vec![EvalAttributeArg::String("p".to_string())]) - ) - ])], + )])], vec![EvalClassMethod::new( "read", Vec::new(), @@ -181,6 +184,10 @@ enum DynEvalMemberEnum { Vec::new(), Vec::new(), vec![EvalInterfaceProperty::new("value", true, false) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) .with_attributes(vec![EvalAttribute::new("IfaceProp", Some(Vec::new()))])], vec![EvalInterfaceMethod::new("read", Vec::new()) .with_attributes(vec![EvalAttribute::new("IfaceMethod", Some(Vec::new()))])], @@ -188,6 +195,10 @@ enum DynEvalMemberEnum { EvalStmt::TraitDecl(EvalTrait::new( "DynEvalMemberTrait", vec![EvalClassProperty::new("seed", None) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) .with_attributes(vec![EvalAttribute::new("TraitProp", Some(Vec::new()))])], vec![EvalClassMethod::new( "add", @@ -257,8 +268,12 @@ fn parse_fragment_accepts_interface_property_hook_contracts() { Vec::new(), Vec::new(), vec![ - EvalInterfaceProperty::new("value", true, true), - EvalInterfaceProperty::new("id", true, false), + EvalInterfaceProperty::new("value", true, true).with_type(Some( + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )), + EvalInterfaceProperty::new("id", true, false).with_type(Some( + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )), ], Vec::new(), ) @@ -277,10 +292,14 @@ fn parse_fragment_accepts_public_class_members() { program.statements(), &[EvalStmt::ClassDecl(EvalClass::new( "DynEvalSupported", - vec![EvalClassProperty::new( - "x", - Some(EvalExpr::Const(EvalConst::Int(1))) - )], + vec![ + EvalClassProperty::new("x", Some(EvalExpr::Const(EvalConst::Int(1)))).with_type( + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )) + ) + ], vec![EvalClassMethod::new( "read", Vec::new(), @@ -292,6 +311,98 @@ fn parse_fragment_accepts_public_class_members() { ))] ); } + +/// Verifies constructor-promoted properties lower to property metadata and assignments. +#[test] +fn parse_fragment_accepts_constructor_promoted_properties() { + let program = parse_fragment( + br#"class DynEvalPromoted { + public function __construct(public int $id, private readonly ?string $name = null) {} +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalPromoted", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_promoted(), + EvalClassProperty::with_visibility_static_final_and_readonly( + "name", + EvalVisibility::Private, + false, + false, + true, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + true + ))) + .with_promoted(), + ], + vec![EvalClassMethod::new( + "__construct", + vec!["id".to_string(), "name".to_string()], + vec![ + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "id".to_string(), + value: EvalExpr::LoadVar("id".to_string()), + }, + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "name".to_string(), + value: EvalExpr::LoadVar("name".to_string()), + }, + ], + ) + .with_parameter_types(vec![ + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )), + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + true + )), + ]) + .with_parameter_defaults(vec![None, Some(EvalExpr::Const(EvalConst::Null))])] + ))] + ); +} + +/// Verifies eval rejects promoted parameter forms that the eval runtime cannot model yet. +#[test] +fn parse_fragment_rejects_unsupported_constructor_promotion_forms() { + parse_fragment(b"class DynEvalPromotedMethod { public function run(public int $id) {} }") + .expect_err("promotion is only valid on constructors"); + parse_fragment(b"class DynEvalPromotedRef { public function __construct(public &$id) {} }") + .expect_err("promoted by-reference parameters need property alias storage"); + parse_fragment( + b"class DynEvalPromotedVariadic { public function __construct(public ...$ids) {} }", + ) + .expect_err("promoted variadic parameters need variadic property semantics"); + parse_fragment( + b"interface DynEvalPromotedIface { public function __construct(public int $id); }", + ) + .expect_err("interface signatures cannot promote properties"); + parse_fragment(b"enum DynEvalPromotedEnum { public function __construct(public int $id) {} }") + .expect_err("enum methods cannot promote properties"); +} + /// Verifies private and protected class members lower with explicit visibility metadata. #[test] fn parse_fragment_accepts_private_and_protected_class_members() { @@ -307,7 +418,11 @@ fn parse_fragment_accepts_private_and_protected_class_members() { "secret", EvalVisibility::Private, Some(EvalExpr::Const(EvalConst::Int(3))) - )], + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )))], vec![EvalClassMethod::with_visibility_and_modifiers( "reveal", EvalVisibility::Protected, @@ -339,7 +454,11 @@ fn parse_fragment_accepts_readonly_class_property() { false, true, None - )], + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )))], Vec::new() ))] ); @@ -368,7 +487,11 @@ fn parse_fragment_accepts_readonly_class_modifier() { false, true, None - ), + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))), EvalClassProperty::with_visibility_static_and_readonly( "count", EvalVisibility::Public, @@ -376,6 +499,10 @@ fn parse_fragment_accepts_readonly_class_modifier() { false, Some(EvalExpr::Const(EvalConst::Int(0))) ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) ], Vec::new() ))] @@ -405,6 +532,10 @@ fn parse_fragment_accepts_concrete_class_property_hooks() { false, None ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) .with_hooks(true, true)], vec![ EvalClassMethod::new( @@ -446,6 +577,10 @@ fn parse_fragment_accepts_abstract_class_property_hook_contracts() { false, None ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) .with_abstract_hook_contract(true, true)], Vec::new(), ))] @@ -472,6 +607,10 @@ fn parse_fragment_accepts_trait_abstract_property_hook_contracts() { false, None ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) .with_abstract_hook_contract(true, false)], Vec::new(), ))] @@ -538,14 +677,16 @@ fn parse_fragment_accepts_abstract_and_final_class_members() { false, None, Vec::new(), - vec![EvalClassProperty::with_visibility_static_final_and_readonly( - "value", - EvalVisibility::Public, - false, - true, - false, - Some(EvalExpr::Const(EvalConst::Int(42))), - )], + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "value", + EvalVisibility::Public, + false, + true, + false, + Some(EvalExpr::Const(EvalConst::Int(42))), + ) + ], vec![ EvalClassMethod::with_modifiers( "read", @@ -734,10 +875,13 @@ class DynEvalUsesTrait { &[ EvalStmt::TraitDecl(EvalTrait::new( "DynEvalTrait", - vec![EvalClassProperty::new( - "seed", - Some(EvalExpr::Const(EvalConst::Int(2))) - )], + vec![ + EvalClassProperty::new("seed", Some(EvalExpr::Const(EvalConst::Int(2)))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + ], vec![EvalClassMethod::new( "read", vec!["value".to_string()], diff --git a/crates/elephc-eval/src/parser/tests/static_members.rs b/crates/elephc-eval/src/parser/tests/static_members.rs index ef924beaff..2663993872 100644 --- a/crates/elephc-eval/src/parser/tests/static_members.rs +++ b/crates/elephc-eval/src/parser/tests/static_members.rs @@ -28,7 +28,11 @@ fn parse_fragment_accepts_static_class_members() { EvalVisibility::Public, true, Some(EvalExpr::Const(EvalConst::Int(1))), - )], + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false, + )))], vec![EvalClassMethod::with_visibility_and_modifiers( "read", EvalVisibility::Public, diff --git a/docs/php/eval.md b/docs/php/eval.md index 0e7ad63b44..fdac539889 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, constructor property promotion for non-by-reference parameters, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -132,14 +132,15 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties -and methods, concrete property `get` / `set` hooks, interface property hook -contract checks, abstract property hook contracts, property-level `readonly`, -`readonly class`, `__construct()`, abstract classes and methods, final classes, -methods, and properties, trait composition with `insteadof` conflict resolution -and `as` aliases/visibility adaptations, interface implementation checks, -static properties, static methods, static interface method contracts, class, -interface, trait, and enum constants including `final` constants, class-level -attributes, and `ClassName::class` literals. Member +and methods, constructor property promotion for non-by-reference parameters, +concrete property `get` / `set` hooks, interface property hook contract checks, +abstract property hook contracts, property-level `readonly`, `readonly class`, +`__construct()`, abstract classes and methods, final classes, methods, and +properties, trait composition with `insteadof` conflict resolution and `as` +aliases/visibility adaptations, interface implementation checks, static +properties, static methods, static interface method contracts, class, interface, +trait, and enum constants including `final` constants, class-level attributes, +and `ClassName::class` literals. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, interfaces, traits, and enums are visible through `class_attribute_names()`, @@ -274,9 +275,7 @@ when the variadic container itself is not rebound, and are reported through `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isDefault()`, and `getModifiers()` report eval property metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the bitmask. `isPromoted()` reports -generated/AOT promoted-property metadata; eval-declared properties currently -report `false` because eval fragments do not yet parse constructor promotion -syntax. +generated/AOT and eval-declared promoted-property metadata. `ReflectionProperty::hasType()` and `getType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained a supported declared type. @@ -457,8 +456,9 @@ remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond parameter/property metadata, broader parameter default-value objects beyond the eval-supported -constant-expression subset, and broader generated/AOT method bridge signatures -beyond the current public non-by-reference fixed scalar/Mixed slice. +constant-expression subset, by-reference promoted constructor parameters, and +broader generated/AOT method bridge signatures beyond the current public +non-by-reference fixed scalar/Mixed slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` From 10a4314d71c76015b4b91a1d1b5f756e84bb2497 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 22:17:18 +0200 Subject: [PATCH 0449/1208] Support ReflectionParameter promoted metadata --- .../elephc-eval/src/interpreter/reflection.rs | 94 +++++++++++++++---- .../tests/builtins_class_metadata.rs | 25 +++++ .../interpreter/tests/support/object_ops.rs | 4 + docs/php/classes.md | 3 +- docs/php/eval.md | 5 +- src/codegen/eval_reflection_owner_helpers.rs | 17 ++++ src/codegen/lower_inst/objects/reflection.rs | 30 ++++++ src/types/checker/builtin_types/reflection.rs | 7 ++ .../codegen/objects/constructor_promotion.rs | 22 +++++ 9 files changed, 188 insertions(+), 19 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index f4b8bcdabf..95cbe86ebb 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -38,6 +38,7 @@ const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; +const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; const EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL: u64 = 1; const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; @@ -81,6 +82,7 @@ struct EvalReflectionParameterMetadata { is_optional: bool, is_variadic: bool, is_passed_by_reference: bool, + is_promoted: bool, has_type: bool, type_metadata: Option, default_value: Option, @@ -293,17 +295,16 @@ pub(in crate::interpreter) fn eval_reflection_class_has_method_result( }; let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; let requested_name = eval_reflection_string_arg(args[0], values)?; - let exists = if let Some(metadata) = - eval_reflection_class_like_attributes(&reflected_name, context) - { - metadata - .method_names - .iter() - .any(|name| name.eq_ignore_ascii_case(&requested_name)) - } else { - eval_reflection_aot_method_metadata_if_exists(&reflected_name, &requested_name, values)? - .is_some() - }; + let exists = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + metadata + .method_names + .iter() + .any(|name| name.eq_ignore_ascii_case(&requested_name)) + } else { + eval_reflection_aot_method_metadata_if_exists(&reflected_name, &requested_name, values)? + .is_some() + }; values.bool_value(exists).map(Some) } @@ -329,7 +330,10 @@ pub(in crate::interpreter) fn eval_reflection_class_has_property_result( let exists = if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { - metadata.property_names.iter().any(|name| name == &property_name) + metadata + .property_names + .iter() + .any(|name| name == &property_name) } else { eval_reflection_aot_property_metadata_if_exists(&reflected_name, &property_name, values)? .is_some() @@ -726,8 +730,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( &reflected_name, &requested_name, values, - )? - { + )? { let member_name = requested_name.to_ascii_lowercase(); return eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_METHOD, @@ -885,8 +888,7 @@ fn eval_reflection_class_new( let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; let class_name = eval_reflection_string_arg(args[0], values)?; let Some(metadata) = eval_reflection_class_like_attributes(&class_name, context) else { - let Some((flags, modifiers)) = eval_reflection_aot_class_flags(&class_name, values)? - else { + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(&class_name, values)? else { return Ok(None); }; return eval_reflection_owner_object( @@ -2588,6 +2590,11 @@ fn eval_reflection_method_metadata( flags, required_parameter_count, }; + let promoted_parameter_names = eval_reflection_promoted_parameter_names( + &declaring_class, + method.name(), + context, + ); let parameters = eval_reflection_parameters_from_names_and_type_flags( Some(declaring_class.as_str()), Some(&declaring_function), @@ -2598,6 +2605,7 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), + &promoted_parameter_names, ); EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class), @@ -2655,6 +2663,7 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), + &[], ); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.to_string()), @@ -2702,6 +2711,8 @@ fn eval_reflection_method_metadata( flags, required_parameter_count, }; + let promoted_parameter_names = + eval_reflection_promoted_trait_parameter_names(trait_decl, method.name()); let parameters = eval_reflection_parameters_from_names_and_type_flags( Some(trait_decl.name()), Some(&declaring_function), @@ -2712,6 +2723,7 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_by_ref(), method.parameter_is_variadic(), + &promoted_parameter_names, ); EvalReflectionMemberMetadata { declaring_class_name: Some(trait_decl.name().to_string()), @@ -2979,6 +2991,7 @@ fn eval_reflection_parameters_from_names_and_type_flags( defaults: &[Option], by_ref_flags: &[bool], variadic_flags: &[bool], + promoted_parameter_names: &[String], ) -> Vec { names .iter() @@ -2996,6 +3009,9 @@ fn eval_reflection_parameters_from_names_and_type_flags( || variadic_flags.get(position).copied().unwrap_or(false), is_variadic: variadic_flags.get(position).copied().unwrap_or(false), is_passed_by_reference: by_ref_flags.get(position).copied().unwrap_or(false), + is_promoted: promoted_parameter_names + .iter() + .any(|promoted_name| promoted_name == name), has_type: has_type_flags.get(position).copied().unwrap_or(false), type_metadata: parameter_types .get(position) @@ -3007,6 +3023,49 @@ fn eval_reflection_parameters_from_names_and_type_flags( .collect() } +/// Returns promoted constructor parameter names for one eval class method. +fn eval_reflection_promoted_parameter_names( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Vec { + if !method_name.eq_ignore_ascii_case("__construct") { + return Vec::new(); + } + context + .class(class_name) + .map(eval_reflection_promoted_property_names) + .unwrap_or_default() +} + +/// Returns promoted constructor parameter names for one eval trait method. +fn eval_reflection_promoted_trait_parameter_names( + trait_decl: &EvalTrait, + method_name: &str, +) -> Vec { + if method_name.eq_ignore_ascii_case("__construct") { + eval_reflection_promoted_property_names_from_slice(trait_decl.properties()) + } else { + Vec::new() + } +} + +/// Returns property names marked as constructor-promoted in one eval class. +fn eval_reflection_promoted_property_names(class: &EvalClass) -> Vec { + eval_reflection_promoted_property_names_from_slice(class.properties()) +} + +/// Returns property names marked as constructor-promoted in one property list. +fn eval_reflection_promoted_property_names_from_slice( + properties: &[EvalClassProperty], +) -> Vec { + properties + .iter() + .filter(|property| property.is_promoted()) + .map(|property| property.name().to_string()) + .collect() +} + /// Converts eval parameter type metadata into the supported ReflectionType subset. fn eval_reflection_parameter_type_metadata( parameter_type: &EvalParameterType, @@ -3134,6 +3193,9 @@ fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) if parameter.is_passed_by_reference { flags |= EVAL_REFLECTION_PARAMETER_FLAG_BY_REF; } + if parameter.is_promoted { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED; + } if parameter.has_type { flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE; } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 7f529d6e1c..33616f5ae4 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1226,6 +1226,31 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionParameter reports eval constructor-promotion metadata. +#[test] +fn execute_program_reflection_parameter_reports_eval_promoted_metadata() { + let program = parse_fragment( + br#"class EvalReflectPromotedParamTarget { + public function __construct(public int $id, string $name = "Ada") {} + public function run(int $id) {} +} +$ctorParams = (new ReflectionMethod("EvalReflectPromotedParamTarget", "__construct"))->getParameters(); +$runParams = (new ReflectionMethod("EvalReflectPromotedParamTarget", "run"))->getParameters(); +echo $ctorParams[0]->isPromoted() ? "I" : "i"; +echo $ctorParams[1]->isPromoted() ? "N" : "n"; +echo $runParams[0]->isPromoted() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Inr"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionProperty exposes eval property type metadata. #[test] fn execute_program_reflection_property_get_type_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index a56d52dbe8..0e5bc8cd0d 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -24,6 +24,7 @@ const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; +const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; impl FakeOps { /// Reads one fake object property by name. @@ -684,6 +685,8 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE) != 0)?; let has_default_value = self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE) != 0)?; + let is_promoted = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED) != 0)?; properties.push(("__position".to_string(), position)); properties.push(("__is_optional".to_string(), is_optional)); properties.push(("__is_variadic".to_string(), is_variadic)); @@ -691,6 +694,7 @@ impl FakeOps { "__is_passed_by_reference".to_string(), is_passed_by_reference, )); + properties.push(("__is_promoted".to_string(), is_promoted)); properties.push(("__has_type".to_string(), has_type)); properties.push(("__type".to_string(), method_objects)); properties.push(("__has_default_value".to_string(), has_default_value)); diff --git a/docs/php/classes.md b/docs/php/classes.md index 7586845d57..0514e65e76 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1207,6 +1207,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::isOptional()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | | `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | | `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | +| `ReflectionParameter::isPromoted()` | Same construction forms as `ReflectionParameter::isPassedByReference()` | Return whether the parameter came from constructor property promotion | | `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, a `ReflectionIntersectionType` for intersection parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | | `ReflectionParameter::getAttributes()` | Same construction forms as `ReflectionParameter::hasType()` | Return `ReflectionAttribute` objects for method parameter attributes | @@ -1314,7 +1315,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index fdac539889..945e40fde3 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -265,8 +265,9 @@ non-spread constructor arguments. Late-bound `static::` defaults and unpacked constructor arguments in defaults are rejected like PHP constant expressions. Variadic eval method parameters bind extra positional and unknown named arguments into a PHP array and are reported through -`ReflectionParameter::isVariadic()` and -`ReflectionParameter::isOptional()`. By-reference eval method parameters accept +`ReflectionParameter::isVariadic()` and `ReflectionParameter::isOptional()`. +Constructor-promoted eval parameters are reported through +`ReflectionParameter::isPromoted()`. By-reference eval method parameters accept direct variable, array-element, and object-property arguments, write back fixed parameters after method execution, write back mutated `&...$items` elements when the variadic container itself is not rebound, and are reported through diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 4d1649b190..f9c48d5834 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1476,6 +1476,8 @@ fn emit_set_owner_parameter_property_aarch64( Some(has_type_hi), Some(has_default_value_lo), Some(has_default_value_hi), + Some(is_promoted_lo), + Some(is_promoted_hi), ) = ( layout.position_lo, layout.position_hi, @@ -1489,6 +1491,8 @@ fn emit_set_owner_parameter_property_aarch64( layout.has_type_hi, layout.has_default_value_lo, layout.has_default_value_hi, + layout.is_promoted_lo, + layout.is_promoted_hi, ) else { return; @@ -1517,6 +1521,10 @@ fn emit_set_owner_parameter_property_aarch64( emitter.instruction("and x10, x10, #1"); // extract the default-value flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", has_default_value_lo); abi::emit_store_zero_to_address(emitter, "x9", has_default_value_hi); + emitter.instruction("lsr x10, x11, #5"); // move the promoted-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the promoted-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_promoted_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_promoted_hi); } /// Stores incoming ARM64 ReflectionParameter type metadata. @@ -1599,6 +1607,8 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl Some(has_type_hi), Some(has_default_value_lo), Some(has_default_value_hi), + Some(is_promoted_lo), + Some(is_promoted_hi), ) = ( layout.position_lo, layout.position_hi, @@ -1612,6 +1622,8 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl layout.has_type_hi, layout.has_default_value_lo, layout.has_default_value_hi, + layout.is_promoted_lo, + layout.is_promoted_hi, ) else { return; @@ -1645,6 +1657,11 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl emitter.instruction("and rax, 1"); // extract the default-value flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", has_default_value_lo); abi::emit_store_zero_to_address(emitter, "r10", has_default_value_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the promoted bit + emitter.instruction("shr rax, 5"); // move the promoted-parameter bit into position + emitter.instruction("and rax, 1"); // extract the promoted-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_promoted_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_promoted_hi); } /// Stores incoming x86_64 ReflectionParameter type metadata. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 5133ba8010..71942c3eda 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -107,6 +107,7 @@ struct ReflectionParameterMember { is_optional: bool, is_variadic: bool, is_passed_by_reference: bool, + is_promoted: bool, has_type: bool, type_metadata: Option, default_value: Option, @@ -746,6 +747,7 @@ fn reflection_function_metadata( signature, None, Some(declaring_function), + &[], ); metadata.required_parameter_count = required_parameter_count; Ok(metadata) @@ -953,6 +955,7 @@ fn reflection_function_parameter_metadata( signature, None, Some(declaring_function), + &[], ); let Some(parameter) = reflection_parameter_member_for_selector(¶meters, selector) else { return Ok(empty_reflection_metadata()); @@ -2178,6 +2181,7 @@ fn reflection_class_method_member( sig, declaring_class_name.as_deref(), Some(declaring_function), + &reflection_promoted_constructor_parameter_names(info, &method_key), ); Some(ReflectionListedMember { name: method_key.clone(), @@ -2241,6 +2245,7 @@ fn reflection_interface_method_member( sig, Some(declaring_class_name.as_str()), Some(declaring_function), + &[], ); Some(ReflectionListedMember { name: method_key, @@ -2311,6 +2316,7 @@ fn reflection_trait_method_member( &info.signature, Some(trait_name), Some(declaring_function), + &[], ), }) } @@ -2514,16 +2520,30 @@ fn reflection_required_parameter_count(sig: &FunctionSig) -> i64 { .map_or(0, |index| index as i64 + 1) } +/// Returns promoted constructor property names for ReflectionParameter metadata. +fn reflection_promoted_constructor_parameter_names( + info: &crate::types::ClassInfo, + method_key: &str, +) -> Vec { + if method_key.eq_ignore_ascii_case("__construct") { + info.promoted_properties.iter().cloned().collect() + } else { + Vec::new() + } +} + /// Builds reflected parameter metadata and attaches declaring class metadata when present. fn reflection_parameter_members_with_declaring_class( sig: &FunctionSig, declaring_class_name: Option<&str>, declaring_function: Option, + promoted_parameter_names: &[String], ) -> Vec { reflection_parameter_members_with_declaring_function( sig, declaring_class_name, declaring_function, + promoted_parameter_names, ) } @@ -2532,6 +2552,7 @@ fn reflection_parameter_members_with_declaring_function( sig: &FunctionSig, declaring_class_name: Option<&str>, declaring_function: Option, + promoted_parameter_names: &[String], ) -> Vec { sig.params .iter() @@ -2562,6 +2583,9 @@ fn reflection_parameter_members_with_declaring_function( .unwrap_or(false), is_variadic, is_passed_by_reference: sig.ref_params.get(index).copied().unwrap_or(false), + is_promoted: promoted_parameter_names + .iter() + .any(|promoted_name| promoted_name == name), has_type, type_metadata: reflection_parameter_type_metadata( sig.param_type_exprs.get(index).and_then(Option::as_ref), @@ -3969,6 +3993,12 @@ fn emit_reflection_parameter_properties( "__is_passed_by_reference", parameter.is_passed_by_reference, )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_promoted", + parameter.is_promoted, + )?; emit_reflection_owner_bool_property( ctx, "ReflectionParameter", diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index c8fb3acc4f..975e5cbe9d 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -588,6 +588,12 @@ fn builtin_reflection_parameter() -> FlattenedClass { Some(TypeExpr::Bool), bool_lit(false), ), + builtin_property( + "__is_promoted", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), builtin_property("__has_type", Visibility::Private, Some(TypeExpr::Bool), bool_lit(false)), builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), builtin_property( @@ -629,6 +635,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { "__is_passed_by_reference", TypeExpr::Bool, ), + builtin_reflection_slot_getter("isPromoted", "__is_promoted", TypeExpr::Bool), builtin_reflection_slot_getter("hasType", "__has_type", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), builtin_reflection_owner_get_attributes_method(), diff --git a/tests/codegen/objects/constructor_promotion.rs b/tests/codegen/objects/constructor_promotion.rs index 336d31e6e0..c4e62b5302 100644 --- a/tests/codegen/objects/constructor_promotion.rs +++ b/tests/codegen/objects/constructor_promotion.rs @@ -185,3 +185,25 @@ echo read_value(); ); assert_eq!(out, "n:n:n"); } + +/// Verifies ReflectionParameter reports constructor-promoted parameters. +#[test] +fn test_reflection_parameter_is_promoted() { + let out = compile_and_run( + r#"getParameters(); +echo $params[0]->isPromoted() ? "I" : "i"; +echo $params[1]->isPromoted() ? "N" : "n"; +$direct = new ReflectionParameter([ReflectPromotedParamUser::class, "__construct"], "id"); +echo $direct->isPromoted() ? "D" : "d"; +$run = new ReflectionParameter([ReflectPromotedParamUser::class, "run"], "id"); +echo $run->isPromoted() ? "R" : "r"; +"#, + ); + assert_eq!(out, "InDr"); +} From c1088b25ed05a0d3e3471dca53fe37220868f93f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 22:36:20 +0200 Subject: [PATCH 0450/1208] Support eval ReflectionProperty values --- crates/elephc-eval/src/context.rs | 26 ++ .../elephc-eval/src/interpreter/reflection.rs | 298 ++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 26 +- .../tests/builtins_class_metadata.rs | 51 +++ docs/php/eval.md | 4 + tests/codegen/eval.rs | 50 +++ 6 files changed, 451 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index d11de4ffb0..25243d169d 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -162,6 +162,7 @@ pub struct ElephcEvalContext { dynamic_objects: HashMap, eval_reflection_attributes: HashMap, eval_reflection_classes: HashMap, + eval_reflection_properties: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, class_stack: Vec, @@ -206,6 +207,7 @@ impl ElephcEvalContext { dynamic_objects: HashMap::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), + eval_reflection_properties: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -251,6 +253,7 @@ impl ElephcEvalContext { dynamic_objects: HashMap::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), + eval_reflection_properties: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -557,6 +560,29 @@ impl ElephcEvalContext { .map(String::as_str) } + /// Records reflected property metadata for one synthetic ReflectionProperty object. + pub fn register_eval_reflection_property( + &mut self, + identity: u64, + declaring_class: &str, + property_name: &str, + ) { + self.eval_reflection_properties.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + property_name.to_string(), + ), + ); + } + + /// Returns the declaring class and property name attached to a synthetic ReflectionProperty. + pub fn eval_reflection_property(&self, identity: u64) -> Option<(&str, &str)> { + self.eval_reflection_properties + .get(&identity) + .map(|(class, property)| (class.as_str(), property.as_str())) + } + /// Returns eval-declared class metadata from parent to child for construction. pub fn class_chain(&self, name: &str) -> Vec { let mut chain = Vec::new(); diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 95cbe86ebb..6782f781ff 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -587,6 +587,101 @@ pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_re values.null().map(Some) } +/// Handles eval-backed `ReflectionProperty::getValue()` calls. +pub(in crate::interpreter) fn eval_reflection_property_get_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getValue") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let object = eval_reflection_property_get_value_arg(evaluated_args)?; + let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if member.is_static { + return eval_reflection_static_property_value( + &declaring_class, + &property_name, + context, + values, + )? + .map(Some) + .ok_or(EvalStatus::RuntimeFatal); + } + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + .map(Some) +} + +/// Handles eval-backed `ReflectionProperty::setValue()` calls. +pub(in crate::interpreter) fn eval_reflection_property_set_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setValue") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) + else { + return Err(EvalStatus::RuntimeFatal); + }; + let (object_or_value, value) = eval_reflection_property_set_value_args(evaluated_args)?; + if member.is_static { + let value = value.unwrap_or(object_or_value); + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(replaced) = context.set_static_property(declaring_class, &property_name, value) + { + values.release(replaced)?; + } + return values.null().map(Some); + } + let value = value.ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_instance_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + values.null().map(Some) +} + /// Handles eval-backed `ReflectionClass::getReflectionConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_result( identity: u64, @@ -1414,6 +1509,17 @@ fn eval_reflection_owner_object_with_members( if owner_kind == EVAL_REFLECTION_OWNER_CLASS { let identity = values.object_identity(object)?; context.register_eval_reflection_class(identity, reflected_name); + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + if let Some(declaring_class) = parent_class_name { + if context.has_class(declaring_class) { + let identity = values.object_identity(object)?; + context.register_eval_reflection_property( + identity, + declaring_class, + reflected_name, + ); + } + } } values.release(attrs)?; values.release(interface_names_array)?; @@ -2938,6 +3044,198 @@ fn eval_reflection_static_property_value_args( Ok((property_name, bound_args[1])) } +/// Binds the optional `ReflectionProperty::getValue()` object argument. +fn eval_reflection_property_get_value_arg( + evaluated_args: Vec, +) -> Result, EvalStatus> { + let params = [String::from("object")]; + let mut bound_arg = None; + for arg in evaluated_args { + if let Some(name) = arg.name { + if params.iter().all(|param| param != &name) || bound_arg.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_arg = Some(arg.value); + } else if bound_arg.is_none() { + bound_arg = Some(arg.value); + } else { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(bound_arg) +} + +/// Binds `ReflectionProperty::setValue()` arguments while allowing PHP's static shorthand. +fn eval_reflection_property_set_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let params = [String::from("objectOrValue"), String::from("value")]; + let mut bound_args = [None, None]; + let mut next_positional = 0; + for arg in evaluated_args { + if let Some(name) = arg.name { + let Some(position) = params.iter().position(|param| param == &name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[position].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[position] = Some(arg.value); + } else { + while next_positional < bound_args.len() && bound_args[next_positional].is_some() { + next_positional += 1; + } + if next_positional >= bound_args.len() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg.value); + next_positional += 1; + } + } + let object_or_value = bound_args[0].ok_or(EvalStatus::RuntimeFatal)?; + Ok((object_or_value, bound_args[1])) +} + +/// Reads one eval instance property through ReflectionProperty semantics. +fn eval_reflection_instance_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_class_name, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.has_get_hook() + && !current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_get_method(property.name()), + context, + ) + { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_get_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + return eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + Vec::new(), + context, + values, + ); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_get(object, &storage_property_name) +} + +/// Writes one eval instance property through ReflectionProperty semantics. +fn eval_reflection_instance_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let (object_class_name, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + validate_eval_reflection_property_write(declaring_class, &property, context)?; + if property.has_set_hook() { + if !current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_set_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + let hook_result = eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + vec![EvaluatedCallArg { + name: None, + value, + ref_target: None, + }], + context, + values, + )?; + values.release(hook_result)?; + return Ok(()); + } + } else if property.has_get_hook() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_set(object, &storage_property_name, value) +} + +/// Resolves and validates the object/property pair targeted by ReflectionProperty. +fn eval_reflection_instance_property_target( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(String, EvalClassProperty), EvalStatus> { + let identity = values.object_identity(object)?; + let object_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + .ok_or(EvalStatus::RuntimeFatal)?; + if !context.class_is_a(&object_class_name, declaring_class, false) { + return Err(EvalStatus::RuntimeFatal); + } + let (_, property) = context + .class_own_property(declaring_class, property_name) + .ok_or(EvalStatus::RuntimeFatal)?; + if property.is_static() || property.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + Ok((object_class_name, property)) +} + +/// Rejects writes to eval properties ReflectionProperty is not allowed to mutate. +fn validate_eval_reflection_property_write( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !property.is_readonly() { + return Ok(()); + } + current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + /// Throws PHP's `ReflectionException` for invalid static-property writes. fn eval_reflection_static_property_missing_for_set( reflected_name: &str, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 43f65f4bbb..45c838789f 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1711,7 +1711,7 @@ pub(in crate::interpreter) fn validate_property_ref_target( } /// Returns true while executing the named hook accessor for one property. -fn current_eval_property_hook_is( +pub(in crate::interpreter) fn current_eval_property_hook_is( declaring_class: &str, property_name: &str, hook_method: &str, @@ -1735,12 +1735,12 @@ fn current_eval_property_hook_is( } /// Returns the synthetic get-hook method name for one property. -fn property_hook_get_method(property_name: &str) -> String { +pub(in crate::interpreter) fn property_hook_get_method(property_name: &str) -> String { format!("__propget_{property_name}") } /// Returns the synthetic set-hook method name for one property. -fn property_hook_set_method(property_name: &str) -> String { +pub(in crate::interpreter) fn property_hook_set_method(property_name: &str) -> String { format!("__propset_{property_name}") } @@ -1796,7 +1796,7 @@ fn eval_dynamic_property_for_access( } /// Returns the physical storage name for an eval object property slot. -fn eval_instance_property_storage_name( +pub(in crate::interpreter) fn eval_instance_property_storage_name( declaring_class: &str, property: &EvalClassProperty, ) -> String { @@ -2429,6 +2429,24 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_property_get_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_set_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_get_reflection_constant_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 33616f5ae4..a4264b5e2e 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1384,6 +1384,57 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty can read and write eval instance and static property values. +#[test] +fn execute_program_reflection_property_gets_and_sets_eval_values() { + let program = parse_fragment( + br#"class EvalReflectValueBase { + private $secret = "base"; + public static $count = 1; +} +class EvalReflectValueChild extends EvalReflectValueBase { + protected $name = "Ada"; +} +class EvalReflectValueHook { + public $raw = 2; + public $doubled { + get => $this->raw * 2; + set { $this->raw = $value + 1; } + } +} +$child = new EvalReflectValueChild(); +$secret = new ReflectionProperty("EvalReflectValueBase", "secret"); +echo $secret->getValue($child); echo ":"; +$secret->setValue($child, "changed"); +echo $secret->getValue(object: $child); echo ":"; +$name = new ReflectionProperty("EvalReflectValueChild", "name"); +echo $name->getValue($child); echo ":"; +$name->setValue(objectOrValue: $child, value: "Grace"); +echo $name->getValue($child); echo ":"; +$count = new ReflectionProperty("EvalReflectValueBase", "count"); +echo $count->getValue(); echo ":"; +$count->setValue(5); +echo EvalReflectValueChild::$count; echo ":"; +$count->setValue(null, 6); +echo $count->getValue($child); echo ":"; +$hook = new EvalReflectValueHook(); +$doubled = new ReflectionProperty("EvalReflectValueHook", "doubled"); +echo $doubled->getValue($hook); echo ":"; +$doubled->setValue($hook, 4); +echo $hook->raw; echo ":"; +echo $doubled->getValue($hook); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "base:changed:Ada:Grace:1:5:6:4:5:10"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes and mutates eval static property values. #[test] fn execute_program_reflection_class_static_property_values() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 945e40fde3..f25c2e90f3 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -283,6 +283,10 @@ metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose materialized property default metadata, including PHP's implicit `null` default for untyped concrete properties without an explicit initializer. +`ReflectionProperty::getValue()` and `setValue()` read and write eval-declared +instance and static property values, bypass public/protected/private visibility +like PHP reflection, route concrete property hooks through their accessors, and +still reject readonly writes. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 827c903aad..fd0209c591 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7146,6 +7146,56 @@ echo array_key_exists("typed", $defaults) ? "T" : "t";'); assert_eq!(out.stdout, "7:bs:5:9:1:p:I:N:t"); } +/// Verifies eval ReflectionProperty value APIs use current runtime object values. +#[test] +fn test_eval_reflection_property_gets_and_sets_values() { + let out = compile_and_run_capture( + r#" $this->raw * 2; + set { $this->raw = $value + 1; } + } +} +$child = new EvalReflectValueChild(); +$secret = new ReflectionProperty("EvalReflectValueBase", "secret"); +echo $secret->getValue($child) . ":"; +$secret->setValue($child, "changed"); +echo $secret->getValue(object: $child) . ":"; +$name = new ReflectionProperty("EvalReflectValueChild", "name"); +echo $name->getValue($child) . ":"; +$name->setValue(objectOrValue: $child, value: "Grace"); +echo $name->getValue($child) . ":"; +$count = new ReflectionProperty("EvalReflectValueBase", "count"); +echo $count->getValue() . ":"; +$count->setValue(5); +echo EvalReflectValueChild::$count . ":"; +$count->setValue(null, 6); +echo $count->getValue($child) . ":"; +$hook = new EvalReflectValueHook(); +$doubled = new ReflectionProperty("EvalReflectValueHook", "doubled"); +echo $doubled->getValue($hook) . ":"; +$doubled->setValue($hook, 4); +echo $hook->raw . ":"; +echo $doubled->getValue($hook);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "base:changed:Ada:Grace:1:5:6:4:5:10"); +} + /// Verifies eval ReflectionClass static-property APIs use current runtime values. #[test] fn test_eval_reflection_class_static_property_values() { From 9063812c4211d4ad51b7c23dcb378e1339da15fb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 22:56:56 +0200 Subject: [PATCH 0451/1208] Support eval anonymous classes --- crates/elephc-eval/src/eval_ir.rs | 17 ++++ .../src/interpreter/dynamic_functions.rs | 19 ++-- .../src/interpreter/expressions.rs | 15 ++- .../elephc-eval/src/interpreter/reflection.rs | 4 + .../elephc-eval/src/interpreter/statements.rs | 52 +++++++--- .../src/interpreter/tests/classes.rs | 36 +++++++ .../interpreter/tests/support/object_ops.rs | 2 +- crates/elephc-eval/src/parser/expressions.rs | 10 +- crates/elephc-eval/src/parser/state.rs | 9 ++ crates/elephc-eval/src/parser/statements.rs | 94 +++++++++++++++---- .../src/parser/tests/arrays_objects.rs | 32 +++++++ docs/php/eval.md | 9 +- src/codegen/eval_reflection_owner_helpers.rs | 22 +++++ tests/codegen/eval.rs | 35 +++++++ 14 files changed, 308 insertions(+), 48 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 4de55f2659..ffde515e98 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -751,6 +751,7 @@ pub struct EvalClass { is_abstract: bool, is_final: bool, is_readonly_class: bool, + is_anonymous: bool, parent: Option, interfaces: Vec, attributes: Vec, @@ -979,6 +980,7 @@ impl EvalClass { is_abstract, is_final, is_readonly_class, + is_anonymous: false, parent, interfaces, attributes: Vec::new(), @@ -996,6 +998,12 @@ impl EvalClass { self } + /// Marks this eval class metadata as an anonymous class expression result. + pub fn with_anonymous(mut self) -> Self { + self.is_anonymous = true; + self + } + /// Marks all instance properties readonly when this metadata represents a `readonly class`. pub fn with_readonly_instance_properties(mut self) -> Self { if self.is_readonly_class { @@ -1028,6 +1036,11 @@ impl EvalClass { self.is_readonly_class } + /// Returns whether this eval class came from a `new class {}` expression. + pub const fn is_anonymous(&self) -> bool { + self.is_anonymous + } + /// Returns the parent class name declared by this eval class, when present. pub fn parent(&self) -> Option<&str> { self.parent.as_deref() @@ -1709,6 +1722,10 @@ pub enum EvalExpr { class_name: String, args: Vec, }, + NewAnonymousClass { + class: EvalClass, + args: Vec, + }, StaticMethodCall { class_name: String, method: String, diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 804e51de3f..fa908a4c9f 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -222,7 +222,9 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let mut bound_args = vec![None; params.len()]; - let variadic_index = parameter_is_variadic.iter().position(|is_variadic| *is_variadic); + let variadic_index = parameter_is_variadic + .iter() + .position(|is_variadic| *is_variadic); let required_count = method_required_param_count(params.len(), parameter_defaults, parameter_is_variadic); let mut next_positional = 0; @@ -426,11 +428,7 @@ fn bind_dynamic_named_method_arg( context, values, )?; - let ref_target = method_parameter_ref_target( - parameter_is_by_ref, - variadic_index, - ref_target, - )?; + let ref_target = method_parameter_ref_target(parameter_is_by_ref, variadic_index, ref_target)?; bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, ref_target, values) } @@ -638,10 +636,14 @@ fn eval_method_parameter_scalar_coercion( EvalParameterTypeVariant::Bool if eval_method_scalar_coercible_tag(tag) => { values.cast_bool(value).map(Some) } - EvalParameterTypeVariant::Float if eval_method_numeric_coercible_value(value, tag, values)? => { + EvalParameterTypeVariant::Float + if eval_method_numeric_coercible_value(value, tag, values)? => + { values.cast_float(value).map(Some) } - EvalParameterTypeVariant::Int if eval_method_numeric_coercible_value(value, tag, values)? => { + EvalParameterTypeVariant::Int + if eval_method_numeric_coercible_value(value, tag, values)? => + { values.cast_int(value).map(Some) } EvalParameterTypeVariant::String if eval_method_scalar_coercible_tag(tag) => { @@ -701,6 +703,7 @@ fn eval_method_default_expr_is_supported(expr: &EvalExpr) -> bool { eval_method_default_class_receiver_is_supported(class_name) && args.iter().all(eval_method_default_call_arg_is_supported) } + EvalExpr::NewAnonymousClass { .. } => false, EvalExpr::NullCoalesce { value, default } => { eval_method_default_expr_is_supported(value) && eval_method_default_expr_is_supported(default) diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index bba4512f1a..30fe1b766f 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -74,13 +74,24 @@ pub(in crate::interpreter) fn eval_expr( if let Some(class) = context.class(&class_name).cloned() { eval_dynamic_class_new_object(&class, args, context, scope, values) } else { - let args = - bind_native_callable_args(context.native_constructor_signature(&class_name), args)?; + let args = bind_native_callable_args( + context.native_constructor_signature(&class_name), + args, + )?; values .new_object(&class_name) .and_then(|object| values.construct_object(object, args).map(|()| object)) } } + EvalExpr::NewAnonymousClass { class, args } => { + ensure_eval_anonymous_class_decl(class, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + let class = context + .class(class.name()) + .cloned() + .ok_or(EvalStatus::RuntimeFatal)?; + eval_dynamic_class_new_object(&class, evaluated_args, context, scope, values) + } EvalExpr::StaticMethodCall { class_name, method, diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 6782f781ff..b8662e533d 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -23,6 +23,7 @@ const EVAL_REFLECTION_CLASS_FLAG_CLONEABLE: u64 = 128; const EVAL_REFLECTION_CLASS_FLAG_INTERNAL: u64 = 256; const EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED: u64 = 512; const EVAL_REFLECTION_CLASS_FLAG_ITERABLE: u64 = 1024; +const EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS: u64 = 2048; const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; @@ -2211,6 +2212,9 @@ fn eval_reflection_class_like_attributes( if eval_reflection_class_is_iterable(class, is_enum, context) { flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; } + if class.is_anonymous() { + flags |= EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS; + } let modifiers = eval_reflection_class_modifiers( class.is_final(), class.is_abstract(), diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 45c838789f..79b891c24b 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -413,6 +413,26 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( } } +/// Registers one eval anonymous class expression if this execution has not seen it yet. +pub(in crate::interpreter) fn ensure_eval_anonymous_class_decl( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !class.is_anonymous() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(existing) = context.class(class.name()) { + return if existing.is_anonymous() { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + }; + } + execute_class_decl_stmt(class, context, scope, values) +} + /// Registers an eval-declared enum and materializes its singleton cases. pub(in crate::interpreter) fn execute_enum_decl_stmt( enum_decl: &EvalEnum, @@ -736,22 +756,24 @@ fn expand_eval_class_traits( constants.extend(class.constants().iter().cloned()); properties.extend(class.properties().iter().cloned()); methods.extend(class.methods().iter().cloned()); - Ok( - EvalClass::with_class_modifiers_traits_adaptations_and_constants( - class.name().to_string(), - class.is_abstract(), - class.is_final(), - class.is_readonly_class(), - class.parent().map(str::to_string), - class.interfaces().to_vec(), - class.traits().to_vec(), - class.trait_adaptations().to_vec(), - constants, - properties, - methods, - ) - .with_attributes(class.attributes().to_vec()), + let mut expanded = EvalClass::with_class_modifiers_traits_adaptations_and_constants( + class.name().to_string(), + class.is_abstract(), + class.is_final(), + class.is_readonly_class(), + class.parent().map(str::to_string), + class.interfaces().to_vec(), + class.traits().to_vec(), + class.trait_adaptations().to_vec(), + constants, + properties, + methods, ) + .with_attributes(class.attributes().to_vec()); + if class.is_anonymous() { + expanded = expanded.with_anonymous(); + } + Ok(expanded) } /// Returns case-insensitive method names declared directly by a pending class. diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 5434eaae5b..825dc7cd46 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -209,6 +209,42 @@ readonly class EvalReadonlyParentMismatchChild extends EvalReadonlyParentMismatc assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies anonymous eval classes instantiate, reuse their synthetic class, and reflect as anonymous. +#[test] +fn execute_program_instantiates_anonymous_class_expressions() { + let program = parse_fragment( + br#"interface EvalAnonRuntimeLabel { + function label(); +} +class EvalAnonRuntimeBase { + protected string $prefix; + public function __construct($prefix) { $this->prefix = $prefix; } +} +function eval_anon_make($prefix) { + return new class($prefix) extends EvalAnonRuntimeBase implements EvalAnonRuntimeLabel { + public function label() { return $this->prefix . ":anon"; } + }; +} +$first = eval_anon_make("A"); +$second = eval_anon_make("B"); +echo $first->label(); echo ":"; +echo $second->label(); echo ":"; +echo get_class($first) === get_class($second) ? "same" : "different"; echo ":"; +$ref = new ReflectionClass(get_class($first)); +echo $ref->isAnonymous() ? "anonymous" : "named"; echo ":"; +echo $ref->implementsInterface("EvalAnonRuntimeLabel") ? "iface" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:anon:B:anon:same:anonymous:iface"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies a get-only property hook computes a virtual eval property. #[test] fn execute_program_reads_eval_property_get_hook() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 0e5bc8cd0d..fee682f4bb 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -554,7 +554,7 @@ impl FakeOps { let is_internal = self.bool_value((flags & 256) != 0)?; let is_user_defined = self.bool_value((flags & 512) != 0)?; let is_iterable = self.bool_value((flags & 1024) != 0)?; - let is_anonymous = self.bool_value(false)?; + let is_anonymous = self.bool_value((flags & 2048) != 0)?; let modifiers_cell = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if owner_kind == EVAL_REFLECTION_OWNER_CLASS { diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs index 890f5b52cf..b00c43046d 100644 --- a/crates/elephc-eval/src/parser/expressions.rs +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -626,9 +626,17 @@ impl Parser { } } - /// Parses `new ClassName(...)` expressions in eval fragments. + /// Parses `new ClassName(...)` and anonymous `new class {}` expressions in eval fragments. pub(super) fn parse_new_object_expr(&mut self) -> Result { self.advance(); + let is_readonly_anonymous = matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "readonly")) + && matches!(self.peek(), TokenKind::Ident(name) if ident_eq(name, "class")); + if is_readonly_anonymous { + self.advance(); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return self.parse_anonymous_class_expr(is_readonly_anonymous); + } let class_name = self.parse_qualified_name()?; let class_name = self.resolve_class_name(class_name); let args = self.parse_call_args()?; diff --git a/crates/elephc-eval/src/parser/state.rs b/crates/elephc-eval/src/parser/state.rs index dcb9fc5667..74953afa16 100644 --- a/crates/elephc-eval/src/parser/state.rs +++ b/crates/elephc-eval/src/parser/state.rs @@ -16,6 +16,9 @@ use crate::errors::EvalParseError; use crate::eval_ir::EvalProgram; use crate::lexer::TokenKind; use std::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static ANONYMOUS_CLASS_COUNTER: AtomicUsize = AtomicUsize::new(0); /// Parses tokenized eval fragments into EvalIR. pub(super) struct Parser { @@ -49,6 +52,12 @@ pub(super) enum UseImportKind { Const, } +/// Returns a parser-global synthetic class name for one eval anonymous class expression. +pub(super) fn next_anonymous_class_name() -> String { + let id = ANONYMOUS_CLASS_COUNTER.fetch_add(1, Ordering::Relaxed); + format!("class@anonymous#eval{id}") +} + impl NamespaceImports { /// Stores one class import under PHP's case-insensitive class alias key. pub(super) fn insert_class(&mut self, alias: String, name: String) { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index fe56e2d4a1..83224928af 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -32,6 +32,15 @@ pub(super) struct ParsedMethodParams { promoted_assignments: Vec, } +/// Class-body members collected while parsing a named or anonymous eval class. +struct ParsedClassBody { + constants: Vec, + properties: Vec, + methods: Vec, + traits: Vec, + trait_adaptations: Vec, +} + impl Parser { /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. pub(super) fn parse_stmt(&mut self) -> Result, EvalParseError> { @@ -359,6 +368,66 @@ impl Parser { self.advance(); let parent = self.parse_class_parent_clause()?; let interfaces = self.parse_class_interface_clause()?; + let body = self.parse_class_body_members(is_readonly_class)?; + self.consume_semicolon(); + Ok(vec![EvalStmt::ClassDecl( + EvalClass::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + body.traits, + body.trait_adaptations, + body.constants, + body.properties, + body.methods, + ) + .with_attributes(attributes), + )]) + } + + /// Parses `class [(args)] [extends Parent] [implements Iface, ...] { ... }` after `new`. + pub(super) fn parse_anonymous_class_expr( + &mut self, + is_readonly_class: bool, + ) -> Result { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let args = if matches!(self.current(), TokenKind::LParen) { + self.parse_call_args()? + } else { + Vec::new() + }; + let parent = self.parse_class_parent_clause()?; + let interfaces = self.parse_class_interface_clause()?; + let body = self.parse_class_body_members(is_readonly_class)?; + let name = next_anonymous_class_name(); + let class = EvalClass::with_class_modifiers_traits_adaptations_and_constants( + name, + false, + false, + is_readonly_class, + parent, + interfaces, + body.traits, + body.trait_adaptations, + body.constants, + body.properties, + body.methods, + ) + .with_anonymous(); + Ok(EvalExpr::NewAnonymousClass { class, args }) + } + + /// Parses members inside a class body after relation clauses. + fn parse_class_body_members( + &mut self, + is_readonly_class: bool, + ) -> Result { self.expect(TokenKind::LBrace)?; let mut constants = Vec::new(); let mut properties = Vec::new(); @@ -378,23 +447,13 @@ impl Parser { &mut trait_adaptations, )?; } - self.consume_semicolon(); - Ok(vec![EvalStmt::ClassDecl( - EvalClass::with_class_modifiers_traits_adaptations_and_constants( - name, - is_abstract, - is_final, - is_readonly_class, - parent, - interfaces, - traits, - trait_adaptations, - constants, - properties, - methods, - ) - .with_attributes(attributes), - )]) + Ok(ParsedClassBody { + constants, + properties, + methods, + traits, + trait_adaptations, + }) } /// Parses class-level `abstract`, `final`, and `readonly` modifiers before `class`. @@ -2375,6 +2434,7 @@ fn eval_constant_expression_default_is_supported(expr: &EvalExpr) -> bool { eval_default_class_receiver_is_supported(class_name) && args.iter().all(eval_call_arg_default_is_supported) } + EvalExpr::NewAnonymousClass { .. } => false, EvalExpr::NullCoalesce { value, default } => { eval_constant_expression_default_is_supported(value) && eval_constant_expression_default_is_supported(default) diff --git a/crates/elephc-eval/src/parser/tests/arrays_objects.rs b/crates/elephc-eval/src/parser/tests/arrays_objects.rs index 5ad39294ad..1e1fd31ec1 100644 --- a/crates/elephc-eval/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-eval/src/parser/tests/arrays_objects.rs @@ -159,6 +159,38 @@ fn parse_fragment_accepts_qualified_new_object_source() { }))] ); } + +/// Verifies anonymous class expressions parse as executable eval class metadata. +#[test] +fn parse_fragment_accepts_anonymous_class_source() { + let program = parse_fragment( + br#"return new readonly class("Ada") extends BaseBox implements Labelled { + public string $name; + public function label() { return $this->name; } +};"#, + ) + .expect("fragment should parse"); + let [EvalStmt::Return(Some(EvalExpr::NewAnonymousClass { class, args }))] = + program.statements() + else { + panic!("expected anonymous class return"); + }; + + assert!(class.name().starts_with("class@anonymous#eval")); + assert!(class.is_anonymous()); + assert!(class.is_readonly_class()); + assert_eq!(class.parent(), Some("BaseBox")); + assert_eq!(class.interfaces(), &["Labelled".to_string()]); + assert_eq!(class.properties().len(), 1); + assert_eq!(class.methods().len(), 1); + assert_eq!( + args, + &[EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string(), + )))] + ); +} + /// Verifies object method calls preserve source-order argument expressions. #[test] fn parse_fragment_accepts_method_call_args_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index f25c2e90f3..9ff370f05e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -73,7 +73,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | @@ -166,9 +166,10 @@ derive namespace-aware parts from the resolved eval class-like name. `ReflectionClass::isEnum()` report eval class-like metadata, including PHP-compatible enum finality and class-like kind checks for eval interfaces, traits, and enums. `ReflectionClass::isReadOnly()` reports eval `readonly class` -metadata. `ReflectionClass::isAnonymous()` reports `false` for eval-declared -named class-like symbols. `ReflectionClass::isInstantiable()` reports whether -eval class-like metadata describes a concrete class with no constructor or a public constructor. +metadata. `ReflectionClass::isAnonymous()` reports true for eval anonymous +classes and false for eval-declared named class-like symbols. +`ReflectionClass::isInstantiable()` reports whether eval class-like metadata +describes a concrete class with no constructor or a public constructor. `ReflectionClass::isCloneable()` reports whether eval class metadata describes a concrete class with no `__clone()` or a public `__clone()`. `ReflectionClass::isIterable()` and `isIterateable()` report whether eval or diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index f9c48d5834..280a490adc 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -64,6 +64,8 @@ struct ReflectionOwnerLayout { is_enum_hi: Option, is_readonly_lo: Option, is_readonly_hi: Option, + is_anonymous_lo: Option, + is_anonymous_hi: Option, is_instantiable_lo: Option, is_instantiable_hi: Option, is_cloneable_lo: Option, @@ -231,6 +233,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option OptionisAnonymous() ? "E" : "e";'); assert_eq!(out.stdout, "cite"); } +/// Verifies eval anonymous class expressions instantiate and reflect as anonymous through the bridge. +#[test] +fn test_eval_anonymous_class_expression_runtime_and_reflection() { + let out = compile_and_run_capture( + r#"prefix = $prefix; } +} +function eval_runtime_anon_make($prefix) { + return new class($prefix) extends EvalRuntimeAnonBase implements EvalRuntimeAnonLabel { + public function label() { return $this->prefix . ":anon"; } + }; +} +$first = eval_runtime_anon_make("A"); +$second = eval_runtime_anon_make("B"); +echo $first->label(); echo ":"; +echo $second->label(); echo ":"; +echo get_class($first) === get_class($second) ? "same" : "different"; echo ":"; +$ref = new ReflectionClass(get_class($first)); +echo $ref->isAnonymous() ? "anonymous" : "named"; echo ":"; +echo $ref->implementsInterface("EvalRuntimeAnonLabel") ? "iface" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "A:anon:B:anon:same:anonymous:iface"); +} + /// Verifies eval ReflectionClass reports method, property, and constant membership through the bridge. #[test] fn test_eval_reflection_class_member_existence() { From 202f3a53ae9149271fc35b5db87a1f77e0dc0b1f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 23:19:43 +0200 Subject: [PATCH 0452/1208] Support eval object cloning --- crates/elephc-eval/src/eval_ir.rs | 1 + .../src/interpreter/expressions.rs | 4 + .../src/interpreter/runtime_ops.rs | 6 ++ .../elephc-eval/src/interpreter/statements.rs | 36 ++++++++ .../src/interpreter/tests/classes.rs | 71 ++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 17 ++++ .../interpreter/tests/support/runtime_ops.rs | 7 ++ crates/elephc-eval/src/parser/cursor.rs | 2 +- crates/elephc-eval/src/parser/expressions.rs | 5 ++ .../src/parser/tests/arrays_objects.rs | 12 +++ .../src/parser/tests/classes_errors.rs | 5 +- .../elephc-eval/src/runtime_hooks/externs.rs | 4 + crates/elephc-eval/src/runtime_hooks/ops.rs | 8 ++ docs/php/eval.md | 8 +- src/codegen_support/runtime/eval_bridge.rs | 82 +++++++++++++++++++ tests/codegen/eval.rs | 22 +++++ 16 files changed, 284 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index ffde515e98..3d84f44c71 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1703,6 +1703,7 @@ pub enum EvalExpr { arms: Vec, default: Option>, }, + Clone(Box), NamespacedCall { name: String, fallback_name: String, diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 30fe1b766f..17e162111c 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -54,6 +54,10 @@ pub(in crate::interpreter) fn eval_expr( arms, default, } => eval_match_expr(subject, arms, default.as_deref(), context, scope, values), + EvalExpr::Clone(object) => { + let object = eval_expr(object, context, scope, values)?; + eval_object_clone_result(object, context, values) + } EvalExpr::NamespacedCall { name, fallback_name, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index ffd0ba4e99..7291c3fbed 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -79,6 +79,12 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result<(), EvalStatus>; + /// Creates a shallow clone of a runtime object held in a boxed Mixed cell. + fn object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result; + /// Returns the number of public JSON-visible properties on a runtime object. fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 79b891c24b..30365f772c 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2269,6 +2269,42 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( Ok(object) } +/// Creates a PHP shallow clone and invokes an eval-declared `__clone()` hook when present. +pub(in crate::interpreter) fn eval_object_clone_result( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + let dynamic_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()); + let clone_method = dynamic_class_name + .as_deref() + .and_then(|class_name| context.class_method(class_name, "__clone")); + if let Some((declaring_class, method)) = &clone_method { + validate_eval_member_access(declaring_class, method.visibility(), context)?; + } + + let clone = values.object_clone_shallow(object)?; + if let Some(class_name) = dynamic_class_name { + let clone_identity = values.object_identity(clone)?; + context.register_dynamic_object(clone_identity, &class_name); + if let Some((declaring_class, method)) = clone_method { + eval_dynamic_method_with_values( + &declaring_class, + &class_name, + &method, + clone, + Vec::new(), + context, + values, + )?; + } + } + Ok(clone) +} + /// Creates a backing object for an eval-declared class without running its constructor. fn eval_dynamic_class_allocate_object( class: &EvalClass, diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 825dc7cd46..2493b7510c 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -245,6 +245,77 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval object cloning copies properties before running `__clone()`. +#[test] +fn execute_program_clones_eval_object_and_runs_clone_hook() { + let program = parse_fragment( + br#"class EvalCloneRuntimeBox { + public string $name; + public function __construct($name) { $this->name = $name; } + public function __clone() { $this->name = $this->name . ":clone"; } +} +$first = new EvalCloneRuntimeBox("A"); +$second = clone $first; +echo $first->name; echo ":"; +echo $second->name; +$second->name = "B"; +return $first->name . ":" . $second->name;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:A:clone"); + assert_eq!(values.get(result), FakeValue::String("A:B".to_string())); +} + +/// Verifies private `__clone()` can be invoked from inside the declaring eval class. +#[test] +fn execute_program_allows_private_clone_hook_inside_declaring_class() { + let program = parse_fragment( + br#"class EvalCloneRuntimePrivateBox { + public string $name = "A"; + private function __clone() { $this->name = $this->name . ":copy"; } + public function copy() { return clone $this; } +} +$first = new EvalCloneRuntimePrivateBox(); +$second = $first->copy(); +echo $first->name; echo ":"; +echo $second->name; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:A:copy"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies private `__clone()` is not callable through a global clone expression. +#[test] +fn execute_program_rejects_private_clone_hook_outside_declaring_class() { + let program = parse_fragment( + br#"class EvalCloneRuntimePrivateFail { + private function __clone() {} +} +$box = new EvalCloneRuntimePrivateFail(); +clone $box;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private clone hook should be inaccessible outside the class"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies a get-only property hook computes a virtual eval property. #[test] fn execute_program_reads_eval_property_get_hook() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index fee682f4bb..770f4cb2c4 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -60,6 +60,23 @@ impl FakeOps { } Ok(()) } + /// Creates one shallow fake object clone, preserving stored property handles. + pub(super) fn runtime_object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + let id = object.as_ptr() as usize; + let properties = match self.get(object) { + FakeValue::Object(properties) => properties.clone(), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let clone = self.alloc(FakeValue::Object(properties)); + if let Some(class_name) = self.object_classes.get(&id).cloned() { + self.object_classes + .insert(clone.as_ptr() as usize, class_name); + } + Ok(clone) + } /// Returns the number of fake object properties in insertion order. pub(super) fn runtime_object_property_len( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index c5244f9622..76b9e59b98 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -83,6 +83,13 @@ impl RuntimeValueOps for FakeOps { ) -> Result<(), EvalStatus> { self.runtime_property_set(object, property, value) } + /// Creates one shallow fake object clone. + fn object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + self.runtime_object_clone_shallow(object) + } /// Returns the number of fake object properties in insertion order. fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { self.runtime_object_property_len(object) diff --git a/crates/elephc-eval/src/parser/cursor.rs b/crates/elephc-eval/src/parser/cursor.rs index 13e602380b..2ec77de93f 100644 --- a/crates/elephc-eval/src/parser/cursor.rs +++ b/crates/elephc-eval/src/parser/cursor.rs @@ -161,7 +161,7 @@ pub(super) fn join_grouped_use_name(prefix: &str, member: &str) -> String { /// Returns true for PHP expression forms that the eval subset intentionally does not parse yet. pub(super) fn is_unsupported_expression_keyword(name: &str) -> bool { - ["clone", "yield"] + ["yield"] .iter() .any(|keyword| ident_eq(name, keyword)) } diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs index b00c43046d..8dd277f0d5 100644 --- a/crates/elephc-eval/src/parser/expressions.rs +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -305,6 +305,11 @@ impl Parser { /// Parses right-associative unary prefix expressions. pub(super) fn parse_unary(&mut self) -> Result { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "clone")) { + self.advance(); + let expr = self.parse_unary()?; + return Ok(EvalExpr::Clone(Box::new(expr))); + } if self.consume(TokenKind::Plus) { let expr = self.parse_unary()?; return Ok(EvalExpr::Unary { diff --git a/crates/elephc-eval/src/parser/tests/arrays_objects.rs b/crates/elephc-eval/src/parser/tests/arrays_objects.rs index 1e1fd31ec1..9594d88b48 100644 --- a/crates/elephc-eval/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-eval/src/parser/tests/arrays_objects.rs @@ -160,6 +160,18 @@ fn parse_fragment_accepts_qualified_new_object_source() { ); } +/// Verifies clone expressions parse as unary object expressions. +#[test] +fn parse_fragment_accepts_clone_expression_source() { + let program = parse_fragment(br#"return clone $box;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Clone(Box::new( + EvalExpr::LoadVar("box".to_string()) + ))))] + ); +} + /// Verifies anonymous class expressions parse as executable eval class metadata. #[test] fn parse_fragment_accepts_anonymous_class_source() { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 70cd956100..847b45b2ef 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -919,10 +919,7 @@ fn parse_fragment_rejects_new_without_class_name() { /// Verifies unsupported expression keywords report the unsupported construct status. #[test] fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { - for source in [ - b"return clone $value;" as &[u8], - b"return yield 1;" as &[u8], - ] { + for source in [b"return yield 1;" as &[u8]] { assert_eq!( parse_fragment(source), Err(EvalParseError::UnsupportedConstruct) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 27a28c58da..4dd680790b 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -51,6 +51,10 @@ unsafe extern "C" { name_len: u64, value: *mut RuntimeCell, ) -> u64; + /// Returns a boxed shallow clone for stdClass/eval object storage. + pub(super) fn __elephc_eval_value_object_clone_shallow( + object: *mut RuntimeCell, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_object_property_len(object: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_value_object_property_iter_key( object: *mut RuntimeCell, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 473e50d690..ac73663f0c 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -125,6 +125,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } + /// Creates a shallow clone of a boxed Mixed stdClass/eval object through the generated wrapper. + fn object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_object_clone_shallow(object.as_ptr()) }) + } + /// Returns the JSON-visible public property count for a boxed Mixed object. fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { let len = unsafe { __elephc_eval_value_object_property_len(object.as_ptr()) }; diff --git a/docs/php/eval.md b/docs/php/eval.md index 9ff370f05e..c0716d6fd1 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,6 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | +| Object cloning | `clone $object` shallow-copies eval-declared objects and `stdClass` storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | @@ -314,6 +315,9 @@ for supported static members, class constants, and class-name literals. Eval object construction can allocate eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime class metadata. Missing class names during eval object construction fail with an eval runtime fatal diagnostic. +`clone $object` creates a shallow copy for eval-declared objects and `stdClass` +objects. Eval `__clone()` hooks are invoked on the cloned object after storage +copying and use the same runtime visibility checks as method calls. AOT and eval-declared class-name probes are visible through `class_exists()`. Eval object relation probes through `is_a()` and `is_subclass_of()` use @@ -464,7 +468,9 @@ and attribute slice, Reflection type APIs beyond parameter/property metadata, br parameter default-value objects beyond the eval-supported constant-expression subset, by-reference promoted constructor parameters, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed slice. +non-by-reference fixed scalar/Mixed slice. Eval object cloning currently covers +eval-declared and `stdClass` objects; cloning emitted AOT objects through the +eval bridge is still outside that bridge slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 5f2cbe78b8..01876f4e4d 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -99,6 +99,46 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the dynamic-object wrapper frame emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); + emitter.instruction("sub sp, sp, #48"); // reserve clone source, destination, and wrapper frame slots + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across clone helper calls + emitter.instruction("add x29, sp, #32"); // establish a stable clone wrapper frame pointer + emitter.instruction("cbz x0, __elephc_eval_value_object_clone_shallow_null"); // null handles cannot be cloned as objects + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.ne __elephc_eval_value_object_clone_shallow_null"); // non-object values cannot be cloned by this bridge + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_clone_shallow_null"); // malformed object payloads cannot be cloned + abi::emit_symbol_address(emitter, "x10", "_stdclass_class_id"); + emitter.instruction("ldr x10, [x10]"); // load the compile-time stdClass class id + emitter.instruction("ldr x11, [x9]"); // load the object's runtime class id + emitter.instruction("cmp x11, x10"); // check whether the object is stdClass-backed + emitter.instruction("b.ne __elephc_eval_value_object_clone_shallow_null"); // non-stdClass objects need a broader clone bridge + emitter.instruction("ldr x10, [x9, #8]"); // load the source dynamic-property hash pointer + emitter.instruction("str x10, [sp, #0]"); // save the source hash pointer across allocation + emitter.instruction("bl __rt_stdclass_new"); // allocate a fresh stdClass shell for the clone + emitter.instruction("str x0, [sp, #8]"); // save the clone object pointer before hash fixup + emitter.instruction("ldr x10, [sp, #0]"); // reload the source dynamic-property hash pointer + emitter.instruction("cbz x10, __elephc_eval_value_object_clone_shallow_box"); // empty source hashes can keep the fresh shell hash + emitter.instruction("ldr x0, [x0, #8]"); // load the fresh shell's empty hash pointer + emitter.instruction("bl __rt_decref_any"); // release the fresh empty hash before installing the clone + emitter.instruction("ldr x0, [sp, #0]"); // reload the source dynamic-property hash for cloning + emitter.instruction("bl __rt_hash_clone_shallow"); // clone dynamic properties and retain nested values + emitter.instruction("ldr x9, [sp, #8]"); // reload the clone object pointer + emitter.instruction("str x0, [x9, #8]"); // install the cloned dynamic-property hash + emitter.label("__elephc_eval_value_object_clone_shallow_box"); + emitter.instruction("ldr x1, [sp, #8]"); // move the cloned object pointer into the Mixed payload + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cloned object for Rust + emitter.instruction("b __elephc_eval_value_object_clone_shallow_done"); // skip the null sentinel after a successful clone + emitter.label("__elephc_eval_value_object_clone_shallow_null"); + emitter.instruction("mov x0, xzr"); // return a null C pointer for unsupported clone inputs + emitter.label("__elephc_eval_value_object_clone_shallow_done"); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the clone wrapper frame + emitter.instruction("ret"); // return the boxed clone or null failure sentinel + label_c_global(emitter, "__elephc_eval_class_exists"); emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class-name lookup state emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across string compares @@ -1518,6 +1558,48 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across clone calls + emitter.instruction("mov rbp, rsp"); // establish a stable clone wrapper frame pointer + emitter.instruction("sub rsp, 32"); // reserve source hash and clone object spill slots + emitter.instruction("test rdi, rdi"); // null handles cannot be cloned as objects + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_null_x86"); // branch to the null sentinel for null handles + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("jne __elephc_eval_value_object_clone_shallow_null_x86"); // non-object values cannot be cloned by this bridge + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // malformed object payloads cannot be cloned + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_null_x86"); // branch to the null sentinel for missing payloads + abi::emit_load_symbol_to_reg(emitter, "r11", "_stdclass_class_id", 0); + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the object's runtime class id + emitter.instruction("cmp rax, r11"); // check whether the object is stdClass-backed + emitter.instruction("jne __elephc_eval_value_object_clone_shallow_null_x86"); // non-stdClass objects need a broader clone bridge + emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // load the source dynamic-property hash pointer + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the source hash pointer across allocation + emitter.instruction("call __rt_stdclass_new"); // allocate a fresh stdClass shell for the clone + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the clone object pointer before hash fixup + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the source dynamic-property hash pointer + emitter.instruction("test r10, r10"); // empty source hashes can keep the fresh shell hash + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_box_x86"); // skip hash replacement when the source hash is absent + emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // load the fresh shell's empty hash pointer + emitter.instruction("call __rt_decref_any"); // release the fresh empty hash before installing the clone + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the source dynamic-property hash for cloning + emitter.instruction("call __rt_hash_clone_shallow"); // clone dynamic properties and retain nested values + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the clone object pointer + emitter.instruction("mov QWORD PTR [r10 + 8], rax"); // install the cloned dynamic-property hash + emitter.label("__elephc_eval_value_object_clone_shallow_box_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // move the cloned object pointer into the Mixed payload + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the cloned object for Rust + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_done_x86"); // skip the null sentinel after a successful clone + emitter.label("__elephc_eval_value_object_clone_shallow_null_x86"); + emitter.instruction("xor eax, eax"); // return a null C pointer for unsupported clone inputs + emitter.label("__elephc_eval_value_object_clone_shallow_done_x86"); + emitter.instruction("add rsp, 32"); // release clone wrapper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed clone or null failure sentinel + label_c_global(emitter, "__elephc_eval_class_exists"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable class-exists frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 944eabc7ef..5db6d0205e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7347,6 +7347,28 @@ echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e";'); assert_eq!(out, "aPFvrUite"); } +/// Verifies eval `clone` shallow-copies eval-declared objects and runs `__clone()`. +#[test] +fn test_eval_clone_object_expression_runtime_and_hook() { + let out = compile_and_run( + r#"name = $name; } + public function __clone() { $this->name = $this->name . ":clone"; } +} +$first = new EvalCloneRuntimeBox("A"); +$second = clone $first; +echo $first->name; echo ":"; +echo $second->name; echo ":"; +$second->name = "B"; +echo $first->name; echo ":"; +echo $second->name;'); +"#, + ); + assert_eq!(out, "A:A:clone:A:B"); +} + /// Verifies eval ReflectionClass::isIterable reports eval and builtin class metadata. #[test] fn test_eval_reflection_class_iterable_predicate() { From 76bb1842ddf64fa7088dd5dac8c390d1e66a2a94 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 23:42:24 +0200 Subject: [PATCH 0453/1208] Support eval ReflectionMethod invocation --- crates/elephc-eval/src/context.rs | 26 +++ .../elephc-eval/src/interpreter/reflection.rs | 210 ++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 61 ++++- .../tests/builtins_class_metadata.rs | 85 +++++++ docs/php/classes.md | 1 + docs/php/eval.md | 6 + tests/codegen/eval.rs | 73 ++++++ 7 files changed, 458 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 25243d169d..626f673c9d 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -162,6 +162,7 @@ pub struct ElephcEvalContext { dynamic_objects: HashMap, eval_reflection_attributes: HashMap, eval_reflection_classes: HashMap, + eval_reflection_methods: HashMap, eval_reflection_properties: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, @@ -207,6 +208,7 @@ impl ElephcEvalContext { dynamic_objects: HashMap::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), + eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), global_scope: None, function_stack: Vec::new(), @@ -253,6 +255,7 @@ impl ElephcEvalContext { dynamic_objects: HashMap::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), + eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), global_scope: None, function_stack: Vec::new(), @@ -560,6 +563,29 @@ impl ElephcEvalContext { .map(String::as_str) } + /// Records reflected method metadata for one synthetic ReflectionMethod object. + pub fn register_eval_reflection_method( + &mut self, + identity: u64, + declaring_class: &str, + method_name: &str, + ) { + self.eval_reflection_methods.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + method_name.to_string(), + ), + ); + } + + /// Returns the declaring class and method name attached to a synthetic ReflectionMethod. + pub fn eval_reflection_method(&self, identity: u64) -> Option<(&str, &str)> { + self.eval_reflection_methods + .get(&identity) + .map(|(class, method)| (class.as_str(), method.as_str())) + } + /// Records reflected property metadata for one synthetic ReflectionProperty object. pub fn register_eval_reflection_property( &mut self, diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index b8662e533d..e9674b355a 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -588,6 +588,44 @@ pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_re values.null().map(Some) } +/// Handles eval-backed `ReflectionMethod::invoke()` and `invokeArgs()` calls. +pub(in crate::interpreter) fn eval_reflection_method_invoke_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_invoke = method_name.eq_ignore_ascii_case("invoke"); + let is_invoke_args = method_name.eq_ignore_ascii_case("invokeArgs"); + if !is_invoke && !is_invoke_args { + return Ok(None); + } + let Some((declaring_class, reflected_method)) = + context + .eval_reflection_method(identity) + .map(|(declaring_class, method)| { + (declaring_class.to_string(), method.to_string()) + }) + else { + return Ok(None); + }; + let (object, method_args) = if is_invoke { + eval_reflection_method_invoke_args(evaluated_args)? + } else { + eval_reflection_method_invoke_args_array(evaluated_args, values)? + }; + eval_reflection_method_invoke_dispatch( + &declaring_class, + &reflected_method, + object, + method_args, + context, + values, + ) + .map(Some) +} + /// Handles eval-backed `ReflectionProperty::getValue()` calls. pub(in crate::interpreter) fn eval_reflection_property_get_value_result( identity: u64, @@ -1510,6 +1548,11 @@ fn eval_reflection_owner_object_with_members( if owner_kind == EVAL_REFLECTION_OWNER_CLASS { let identity = values.object_identity(object)?; context.register_eval_reflection_class(identity, reflected_name); + } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + if let Some(declaring_class) = parent_class_name { + let identity = values.object_identity(object)?; + context.register_eval_reflection_method(identity, declaring_class, reflected_name); + } } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { if let Some(declaring_class) = parent_class_name { if context.has_class(declaring_class) { @@ -3100,6 +3143,173 @@ fn eval_reflection_property_set_value_args( Ok((object_or_value, bound_args[1])) } +/// Binds `ReflectionMethod::invoke()` arguments and preserves forwarded named args. +fn eval_reflection_method_invoke_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let mut object = None; + let mut method_args = Vec::new(); + for arg in evaluated_args { + if matches!(arg.name.as_deref(), Some("object")) { + if object.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + object = Some(arg.value); + } else if object.is_none() && arg.name.is_none() { + object = Some(arg.value); + } else { + method_args.push(eval_reflection_method_forwarded_value_arg(arg)); + } + } + object + .map(|object| (object, method_args)) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts a variadic `invoke()` argument into a by-value forwarded method argument. +fn eval_reflection_method_forwarded_value_arg(arg: EvaluatedCallArg) -> EvaluatedCallArg { + EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + } +} + +/// Binds `ReflectionMethod::invokeArgs()` and expands its PHP argument array. +fn eval_reflection_method_invoke_args_array( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("object"), String::from("args")], + evaluated_args, + )?; + let method_args = eval_array_call_arg_values(args[1], values)?; + Ok((args[0], method_args)) +} + +/// Dispatches one reflected method invocation through eval or public AOT bridges. +fn eval_reflection_method_invoke_dispatch( + declaring_class: &str, + method_name: &str, + object: RuntimeCellHandle, + method_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some((method_class, method)) = context.class_method(declaring_class, method_name) { + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let by_value_parameters = vec![false; method.params().len()]; + if method.is_static() { + return eval_dynamic_static_method_with_values_and_ref_flags( + &method_class, + &method_class, + &method, + &by_value_parameters, + method_args, + context, + values, + ); + } + let called_class = eval_reflection_method_instance_called_class( + declaring_class, + object, + context, + values, + )?; + return eval_dynamic_method_with_values_and_ref_flags( + &method_class, + &called_class, + &method, + object, + &by_value_parameters, + method_args, + context, + values, + ); + } + eval_reflection_aot_method_invoke_dispatch( + declaring_class, + method_name, + object, + method_args, + context, + values, + ) +} + +/// Returns the runtime class name for an eval object used as a reflected receiver. +fn eval_reflection_method_instance_called_class( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)?; + let Some(object_class_name) = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if !context.class_is_a(&object_class_name, declaring_class, false) { + eval_throw_reflection_exception( + "Given object is not an instance of the class this method was declared in", + context, + values, + )?; + return Err(EvalStatus::UncaughtThrowable); + } + Ok(object_class_name) +} + +/// Invokes one reflected generated/AOT method when it fits the public bridge slice. +fn eval_reflection_aot_method_invoke_dispatch( + declaring_class: &str, + method_name: &str, + object: RuntimeCellHandle, + method_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let member = + eval_reflection_aot_method_metadata_if_exists(declaring_class, method_name, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + if member.visibility != EvalVisibility::Public || member.is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if member.is_static { + let args = bind_native_callable_args( + context.native_static_method_signature(declaring_class, method_name), + method_args, + )?; + return values.static_method_call(declaring_class, method_name, args); + } + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let is_instance = dynamic_object_is_a(object, declaring_class, false, context, values)? + .map_or_else(|| values.object_is_a(object, declaring_class, false), Ok)?; + if !is_instance { + eval_throw_reflection_exception( + "Given object is not an instance of the class this method was declared in", + context, + values, + )?; + return Err(EvalStatus::UncaughtThrowable); + } + let args = bind_native_callable_args( + context.native_method_signature(declaring_class, method_name), + method_args, + )?; + values.method_call(object, method_name, args) +} + /// Reads one eval instance property through ReflectionProperty semantics. fn eval_reflection_instance_property_get_value( declaring_class: &str, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 30365f772c..7e13933aab 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2487,6 +2487,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_method_invoke_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_get_value_result( identity, method_name, @@ -3017,6 +3026,29 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_method_with_values_and_ref_flags( + class_name, + called_class_name, + method, + object, + method.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Executes one eval-declared class method with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let qualified_method_name = format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); @@ -3028,7 +3060,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( method.params(), method.parameter_types(), method.parameter_defaults(), - method.parameter_is_by_ref(), + parameter_is_by_ref, method.parameter_is_variadic(), evaluated_args, context, @@ -3047,7 +3079,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values( bind_method_scope_args( &mut method_scope, method.params(), - method.parameter_is_by_ref(), + parameter_is_by_ref, &evaluated_args, ); let result = execute_statements(method.body(), context, &mut method_scope, values); @@ -3089,6 +3121,27 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_static_method_with_values_and_ref_flags( + class_name, + called_class_name, + method, + method.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Executes one eval-declared static method with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_flags( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let qualified_method_name = format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); @@ -3100,7 +3153,7 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( method.params(), method.parameter_types(), method.parameter_defaults(), - method.parameter_is_by_ref(), + parameter_is_by_ref, method.parameter_is_variadic(), evaluated_args, context, @@ -3118,7 +3171,7 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( bind_method_scope_args( &mut method_scope, method.params(), - method.parameter_is_by_ref(), + parameter_is_by_ref, &evaluated_args, ); let result = execute_statements(method.body(), context, &mut method_scope, values); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index a4264b5e2e..c0c3ec149d 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1719,6 +1719,91 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod::invoke dispatches eval-declared methods. +#[test] +fn execute_program_reflection_method_invoke_calls_eval_method() { + let program = parse_fragment( + br#"class EvalReflectInvokeBase { + private function hidden($label = "H") { + return "hidden:" . $label; + } + public function who() { + return static::class; + } + public static function make($left, $right = "S") { + return static::class . ":" . $left . $right; + } +} +class EvalReflectInvokeChild extends EvalReflectInvokeBase { + public function join($a, $b = "B") { + return $a . $b; + } + public function mutate(&$value) { + $value = $value . "!"; + return $value; + } +} +$object = new EvalReflectInvokeChild(); +$hidden = new ReflectionMethod("EvalReflectInvokeBase", "hidden"); +echo $hidden->invoke($object, "X"); echo ":"; +$who = (new ReflectionClass("EvalReflectInvokeChild"))->getMethod("who"); +echo $who->invoke($object); echo ":"; +$static = new ReflectionMethod("EvalReflectInvokeBase", "make"); +echo $static->invoke(null, right: "Y", left: "X"); echo ":"; +echo $static->invoke($object, "A"); echo ":"; +$join = null; +foreach ((new ReflectionClass("EvalReflectInvokeChild"))->getMethods() as $method) { + if ($method->getName() === "join") { + $join = $method; + } +} +$value = "Q"; +$mutate = new ReflectionMethod("EvalReflectInvokeChild", "mutate"); +echo $join->invokeArgs($object, ["b" => "2", "a" => "1"]); echo ":"; +echo $mutate->invoke($object, $value); echo ":"; echo $value; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "hidden:X:EvalReflectInvokeChild:EvalReflectInvokeBase:XY:EvalReflectInvokeBase:AS:12:Q!:Q" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod::invoke throws for incompatible eval receivers. +#[test] +fn execute_program_reflection_method_invoke_rejects_wrong_object() { + let program = parse_fragment( + br#"class EvalReflectInvokeOwner { + public function run() { + return "owner"; + } +} +class EvalReflectInvokeOther {} +try { + (new ReflectionMethod("EvalReflectInvokeOwner", "run"))->invoke(new EvalReflectInvokeOther()); + echo "bad"; +} catch (ReflectionException $e) { + echo "caught"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "caught"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::newInstanceWithoutConstructor skips eval constructors. #[test] fn execute_program_reflection_class_new_instance_without_constructor_allocates_eval_class() { diff --git a/docs/php/classes.md b/docs/php/classes.md index 0514e65e76..de2b7b0a91 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1199,6 +1199,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isConstructor()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getConstructor()` | Return whether the reflected method is the constructor | | `ReflectionMethod::isDestructor()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is the destructor | | `ReflectionMethod::getModifiers()` | `new ReflectionMethod($class_name, $method_name)` | Return PHP's `ReflectionMethod::IS_*` visibility/static/finality/abstract bitmask | +| `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | diff --git a/docs/php/eval.md b/docs/php/eval.md index c0716d6fd1..9eb544b6f5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -231,6 +231,12 @@ eval's positional, named, and unpacking-aware call binding. `ReflectionClass::newInstanceWithoutConstructor()` allocates eval-declared reflected classes, initializes supported property defaults, and skips `__construct()`. +`ReflectionMethod::invoke()` and `invokeArgs()` call eval-declared reflected +methods, bypass public/protected/private visibility like PHP reflection, +preserve named arguments for the invoked method, follow PHP's by-value +`invoke()` variadic forwarding, accept `null` or an object for static methods, +and throw catchable `ReflectionException` values when an instance receiver is +not compatible with the reflected declaring class. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5db6d0205e..069f9d3b94 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7457,6 +7457,79 @@ echo $second->label();'); assert_eq!(out, "EF:GH"); } +/// Verifies eval ReflectionMethod::invoke and invokeArgs call eval-declared methods. +#[test] +fn test_eval_reflection_method_invoke_calls_eval_method() { + let out = compile_and_run( + r#"invoke($object, "X") . ":"; +$who = (new ReflectionClass("EvalReflectInvokeChild"))->getMethod("who"); +echo $who->invoke($object) . ":"; +$static = new ReflectionMethod("EvalReflectInvokeBase", "make"); +echo $static->invoke(null, right: "Y", left: "X") . ":"; +echo $static->invoke($object, "A") . ":"; +$join = null; +foreach ((new ReflectionClass("EvalReflectInvokeChild"))->getMethods() as $method) { + if ($method->getName() === "join") { + $join = $method; + } +} +$value = "Q"; +$mutate = new ReflectionMethod("EvalReflectInvokeChild", "mutate"); +echo $join->invokeArgs($object, ["b" => "2", "a" => "1"]) . ":"; +echo $mutate->invoke($object, $value) . ":" . $value;'); +"#, + ); + assert_eq!( + out, + "hidden:X:EvalReflectInvokeChild:EvalReflectInvokeBase:XY:EvalReflectInvokeBase:AS:12:Q!:Q" + ); +} + +/// Verifies eval ReflectionMethod::invoke throws on incompatible receivers. +#[test] +fn test_eval_reflection_method_invoke_rejects_wrong_object() { + let out = compile_and_run( + r#"invoke(new EvalReflectInvokeOther()); + echo "bad"; +} catch (ReflectionException $e) { + echo "caught"; +}'); +"#, + ); + assert_eq!(out, "caught"); +} + /// Verifies eval ReflectionClass::newInstanceWithoutConstructor allocates without constructors. #[test] fn test_eval_reflection_class_new_instance_without_constructor_allocates_eval_class() { From cdae47930c67878f8f5d160726234bfe6081112a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 23:50:51 +0200 Subject: [PATCH 0454/1208] Support eval ReflectionClass newInstanceArgs --- .../elephc-eval/src/interpreter/statements.rs | 21 +++++++++++--- .../tests/builtins_class_metadata.rs | 8 +++-- docs/php/classes.md | 3 +- docs/php/eval.md | 2 ++ tests/codegen/eval.rs | 29 +++++++++++++++++-- 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 7e13933aab..410ad2ef59 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2614,9 +2614,13 @@ fn eval_reflection_class_new_instance_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("newInstance") { + let constructor_args = if method_name.eq_ignore_ascii_case("newInstance") { + evaluated_args + } else if method_name.eq_ignore_ascii_case("newInstanceArgs") { + eval_reflection_class_new_instance_args(evaluated_args, values)? + } else { return Ok(None); - } + }; let Some(reflected_name) = context .eval_reflection_class_name(identity) .map(str::to_string) @@ -2625,7 +2629,7 @@ fn eval_reflection_class_new_instance_result( }; if let Some(class) = context.class(&reflected_name).cloned() { let mut scope = ElephcEvalScope::new(); - return eval_dynamic_class_new_object(&class, evaluated_args, context, &mut scope, values) + return eval_dynamic_class_new_object(&class, constructor_args, context, &mut scope, values) .map(Some); } let class_name = context @@ -2633,13 +2637,22 @@ fn eval_reflection_class_new_instance_result( .unwrap_or(reflected_name); let args = bind_native_callable_args( context.native_constructor_signature(&class_name), - evaluated_args, + constructor_args, )?; let instance = values.new_object(&class_name)?; values.construct_object(instance, args)?; Ok(Some(instance)) } +/// Expands the single `ReflectionClass::newInstanceArgs()` array argument. +fn eval_reflection_class_new_instance_args( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; + eval_array_call_arg_values(args[0], values) +} + /// Allocates the class named by a materialized eval `ReflectionClass` without running `__construct()`. fn eval_reflection_class_new_instance_without_constructor_result( identity: u64, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c0c3ec149d..cbd9feb7a8 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1706,7 +1706,11 @@ $ref = new ReflectionClass("EvalReflectNewTarget"); $first = $ref->newInstance("I", "J"); echo $first->label(); echo ":"; $second = $ref->newInstance(...["K", "L"]); -echo $second->label(); +echo $second->label(); echo ":"; +$third = $ref->newInstanceArgs(["right" => "N", "left" => "M"]); +echo $third->label(); echo ":"; +$fourth = $ref->newInstanceArgs(["O", "P"]); +echo $fourth->label(); return true;"#, ) .expect("parse eval fragment"); @@ -1715,7 +1719,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "IJ:KL"); + assert_eq!(values.output, "IJ:KL:MN:OP"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/classes.md b/docs/php/classes.md index de2b7b0a91..344318d2c4 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1180,6 +1180,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getParentClass()` | `new ReflectionClass($class_name)` | Return the reflected parent class as a `ReflectionClass`, or `false` when the reflected class-like symbol has no parent class | | `ReflectionClass::getProperties()` | `new ReflectionClass($class_name)` | Return `ReflectionProperty` objects for properties visible through the reflected class-like metadata | | `ReflectionClass::newInstance()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class with forwarded constructor arguments | +| `ReflectionClass::newInstanceArgs()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class from an argument array | | `ReflectionClass::newInstanceWithoutConstructor()` | `new ReflectionClass($class_name)` | Allocate an instance of the reflected class without running `__construct()` | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | | `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user function name | @@ -1316,7 +1317,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index 9eb544b6f5..4eae042844 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -228,6 +228,8 @@ returns a materialized `ReflectionClass` for eval-declared parent classes or `false` when no parent class exists. `ReflectionClass::newInstance()` constructs eval-declared reflected classes and forwards constructor arguments through eval's positional, named, and unpacking-aware call binding. +`ReflectionClass::newInstanceArgs()` constructs eval-declared reflected classes +from an argument array, treating string keys as named constructor arguments. `ReflectionClass::newInstanceWithoutConstructor()` allocates eval-declared reflected classes, initializes supported property defaults, and skips `__construct()`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 069f9d3b94..c38ffd9b20 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7451,10 +7451,14 @@ $ref = new ReflectionClass("EvalReflectNewTarget"); $first = $ref->newInstance("E", "F"); echo $first->label() . ":"; $second = $ref->newInstance(...["G", "H"]); -echo $second->label();'); +echo $second->label() . ":"; +$third = $ref->newInstanceArgs(["right" => "J", "left" => "I"]); +echo $third->label() . ":"; +$fourth = $ref->newInstanceArgs(["K", "L"]); +echo $fourth->label();'); "#, ); - assert_eq!(out, "EF:GH"); + assert_eq!(out, "EF:GH:IJ:KL"); } /// Verifies eval ReflectionMethod::invoke and invokeArgs call eval-declared methods. @@ -7938,6 +7942,27 @@ echo eval('$box = new EvalDynamicNewManyArgCtor(1, 2, 3, "!"); return $box->labe assert_eq!(out, "6!"); } +/// Verifies eval ReflectionClass::newInstanceArgs forwards named args to AOT constructors. +#[test] +fn test_eval_reflection_class_new_instance_args_constructs_aot_class() { + let out = compile_and_run( + r#"label = $left . $right; + } +} +echo eval('$ref = new ReflectionClass("EvalReflectNewArgsAotTarget"); +$first = $ref->newInstanceArgs(["right" => "Y", "left" => "X"]); +echo $first->label . ":"; +$second = $ref->newInstanceArgs(["Q", "R"]); +return $second->label;'); +"#, + ); + assert_eq!(out, "XY:QR"); +} + /// Verifies eval object construction passes AOT constructor arguments on the caller stack. #[test] fn test_eval_dynamic_new_runs_constructor_with_stack_string_arg() { From 006572dcf9f8c6ef9f9c237fd7217ab28bb74684 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 19 Jun 2026 23:57:34 +0200 Subject: [PATCH 0455/1208] Support eval Reflection setAccessible noops --- .../elephc-eval/src/interpreter/reflection.rs | 20 +++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 9 ++++++ .../tests/builtins_class_metadata.rs | 29 +++++++++++++++++++ docs/php/classes.md | 2 ++ docs/php/eval.md | 2 ++ tests/codegen/eval.rs | 23 +++++++++++++++ 6 files changed, 85 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index e9674b355a..c426ab09e4 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -626,6 +626,26 @@ pub(in crate::interpreter) fn eval_reflection_method_invoke_result( .map(Some) } +/// Handles PHP's no-op `ReflectionMethod/Property::setAccessible()` calls. +pub(in crate::interpreter) fn eval_reflection_set_accessible_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setAccessible") { + return Ok(None); + } + if context.eval_reflection_method(identity).is_none() + && context.eval_reflection_property(identity).is_none() + { + return Ok(None); + } + let _ = bind_evaluated_function_args(&[String::from("accessible")], evaluated_args)?; + values.null().map(Some) +} + /// Handles eval-backed `ReflectionProperty::getValue()` calls. pub(in crate::interpreter) fn eval_reflection_property_get_value_result( identity: u64, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 410ad2ef59..9571dcc592 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2496,6 +2496,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_set_accessible_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_get_value_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index cbd9feb7a8..51b26752f5 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1808,6 +1808,35 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod/Property::setAccessible are PHP-compatible no-ops. +#[test] +fn execute_program_reflection_set_accessible_is_noop() { + let program = parse_fragment( + br#"class EvalReflectAccessTarget { + private $secret = "s"; + private function hidden() { + return $this->secret; + } +} +$object = new EvalReflectAccessTarget(); +$method = new ReflectionMethod("EvalReflectAccessTarget", "hidden"); +echo is_null($method->setAccessible(false)) ? "M" : "m"; echo ":"; +echo $method->invoke($object); echo ":"; +$property = new ReflectionProperty("EvalReflectAccessTarget", "secret"); +echo is_null($property->setAccessible(accessible: true)) ? "P" : "p"; echo ":"; +echo $property->getValue($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "M:s:P:s"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::newInstanceWithoutConstructor skips eval constructors. #[test] fn execute_program_reflection_class_new_instance_without_constructor_allocates_eval_class() { diff --git a/docs/php/classes.md b/docs/php/classes.md index 344318d2c4..eda36e9a11 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1200,6 +1200,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isConstructor()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getConstructor()` | Return whether the reflected method is the constructor | | `ReflectionMethod::isDestructor()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is the destructor | | `ReflectionMethod::getModifiers()` | `new ReflectionMethod($class_name, $method_name)` | Return PHP's `ReflectionMethod::IS_*` visibility/static/finality/abstract bitmask | +| `ReflectionMethod::setAccessible()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Accepted as a PHP-compatible no-op for eval-backed method reflection | | `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | @@ -1237,6 +1238,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | +| `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | diff --git a/docs/php/eval.md b/docs/php/eval.md index 4eae042844..be2b8ce130 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -248,6 +248,7 @@ eval/runtime class or interface names. with PHP-compatible `ReflectionMethod::IS_*` constants for the bitmask. `ReflectionMethod::isConstructor()` and `isDestructor()` report whether the reflected method is `__construct` or `__destruct`. +`ReflectionMethod::setAccessible()` is accepted as a PHP-compatible no-op. `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report eval-declared method parameter metadata. Eval currently exposes parameter names and zero-based positions there, @@ -293,6 +294,7 @@ metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose materialized property default metadata, including PHP's implicit `null` default for untyped concrete properties without an explicit initializer. +`ReflectionProperty::setAccessible()` is accepted as a PHP-compatible no-op. `ReflectionProperty::getValue()` and `setValue()` read and write eval-declared instance and static property values, bypass public/protected/private visibility like PHP reflection, route concrete property hooks through their accessors, and diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c38ffd9b20..d58cb0ebae 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7534,6 +7534,29 @@ try { assert_eq!(out, "caught"); } +/// Verifies eval ReflectionMethod/Property::setAccessible are PHP-compatible no-ops. +#[test] +fn test_eval_reflection_set_accessible_is_noop() { + let out = compile_and_run( + r#"secret; + } +} +$object = new EvalReflectAccessTarget(); +$method = new ReflectionMethod("EvalReflectAccessTarget", "hidden"); +echo is_null($method->setAccessible(false)) ? "M" : "m"; echo ":"; +echo $method->invoke($object); echo ":"; +$property = new ReflectionProperty("EvalReflectAccessTarget", "secret"); +echo is_null($property->setAccessible(accessible: true)) ? "P" : "p"; echo ":"; +echo $property->getValue($object);'); +"#, + ); + assert_eq!(out, "M:s:P:s"); +} + /// Verifies eval ReflectionClass::newInstanceWithoutConstructor allocates without constructors. #[test] fn test_eval_reflection_class_new_instance_without_constructor_allocates_eval_class() { From 9369ae66adb3ca836059d5eacf8b911e2973ca93 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 00:13:28 +0200 Subject: [PATCH 0456/1208] Support eval invokable object callables --- .../builtins/arrays/filters/iterator.rs | 4 +- .../interpreter/builtins/registry/callable.rs | 51 ++++++-- .../builtins/registry/dispatch/arrays.rs | 4 +- .../builtins/registry/dispatch/symbols.rs | 7 +- .../src/interpreter/builtins/symbols.rs | 109 +++++++++++++++++- crates/elephc-eval/src/interpreter/control.rs | 3 + .../src/interpreter/expressions.rs | 4 +- .../elephc-eval/src/interpreter/statements.rs | 14 ++- .../src/interpreter/tests/dynamic_calls.rs | 56 +++++++++ docs/php/eval.md | 12 +- tests/codegen/eval.rs | 56 +++++++++ 11 files changed, 288 insertions(+), 32 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs index 777d927fa4..b1bebd4e75 100644 --- a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs +++ b/crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs @@ -23,13 +23,13 @@ pub(in crate::interpreter) fn eval_builtin_iterator_apply( [iterator, callback] => { let iterator = eval_expr(iterator, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, values)?; + let callback = eval_callable(callback, context, values)?; eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) } [iterator, callback, callback_args] => { let iterator = eval_expr(iterator, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, values)?; + let callback = eval_callable(callback, context, values)?; let callback_args = eval_expr(callback_args, context, scope, values)?; let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; eval_iterator_apply_result(iterator, &callback, callback_args, context, values) diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs b/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs index a58615de5d..2fa6a6a1d8 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs @@ -50,7 +50,7 @@ pub(in crate::interpreter) fn eval_call_user_func_array_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable(callback, values)?; + let callback = eval_callable(callback, context, values)?; if !values.is_array_like(arg_array)? { return Err(EvalStatus::RuntimeFatal); } @@ -67,21 +67,44 @@ pub(in crate::interpreter) fn eval_call_user_func_with_values( let Some((callback, callback_args)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; - let callback = eval_callable(*callback, values)?; + let callback = eval_callable(*callback, context, values)?; eval_evaluated_callable_with_values(&callback, callback_args.to_vec(), context, values) } /// Normalizes one PHP callback value for eval dynamic callable dispatch. pub(in crate::interpreter) fn eval_callable( callback: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if values.type_tag(callback)? == EVAL_TAG_OBJECT { + return eval_object_callable(callback, context, values); + } if values.is_array_like(callback)? { return eval_array_callable(callback, values); } eval_string_callable(callback, values) } +/// Normalizes one invokable eval object for dynamic callable dispatch. +pub(in crate::interpreter) fn eval_object_callable( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(callback)?; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(EvaluatedCallable::InvokableObject { object: callback }); + }; + let Some((_, method)) = context.class_method(class.name(), "__invoke") else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::UnsupportedConstruct); + } + Ok(EvaluatedCallable::InvokableObject { object: callback }) +} + /// Normalizes one two-element object-method or static-method callable array. pub(in crate::interpreter) fn eval_array_callable( callback: RuntimeCellHandle, @@ -156,6 +179,9 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( EvaluatedCallable::Named(name) => { eval_callable_with_values(name, evaluated_args, context, values) } + EvaluatedCallable::InvokableObject { object } => { + eval_method_call_result(*object, "__invoke", evaluated_args, context, values) + } EvaluatedCallable::ObjectMethod { object, method } => { eval_method_call_result(*object, method, evaluated_args, context, values) } @@ -180,12 +206,23 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( EvaluatedCallable::Named(name) => { eval_callable_with_call_array_args(name, evaluated_args, context, values) } + EvaluatedCallable::InvokableObject { object } => { + eval_method_call_result_with_evaluated_args( + *object, + "__invoke", + evaluated_args, + context, + values, + ) + } EvaluatedCallable::ObjectMethod { object, method } => { - if evaluated_args.iter().any(|arg| arg.name.is_some()) { - return Err(EvalStatus::RuntimeFatal); - } - let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); - eval_method_call_result(*object, method, evaluated_args, context, values) + eval_method_call_result_with_evaluated_args( + *object, + method, + evaluated_args, + context, + values, + ) } EvaluatedCallable::StaticMethod { class_name, method } => { eval_static_method_call_result(class_name, method, evaluated_args, context, values) diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs index b187681991..a390fd6469 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs @@ -229,11 +229,11 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( }, "iterator_apply" => match evaluated_args { [iterator, callback] => { - let callback = eval_callable(*callback, values)?; + let callback = eval_callable(*callback, context, values)?; eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? } [iterator, callback, args] => { - let callback = eval_callable(*callback, values)?; + let callback = eval_callable(*callback, context, values)?; let callback_args = eval_iterator_apply_arg_values(*args, values)?; eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? } diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index 068d3d5e52..d92caf83ce 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -42,13 +42,10 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( eval_spl_object_identity_result(name, *object, values)? } "function_exists" | "is_callable" => { - let [name] = evaluated_args else { + let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - let name = values.string_bytes(*name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\').to_ascii_lowercase(); - values.bool_value(eval_function_probe_exists(context, &name))? + eval_function_probe_result(name, *value, context, values)? } "empty" => eval_empty_result(evaluated_args, values)?, "isset" => eval_isset_result(evaluated_args, values)?, diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index a5c5ced449..8318c93c1f 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -11,20 +11,37 @@ use super::super::*; use super::*; +/// Evaluates `function_exists()` and `is_callable()` inside an eval fragment. pub(in crate::interpreter) fn eval_builtin_function_probe( + name: &str, args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [name] = args else { + let [value] = args else { return Err(EvalStatus::RuntimeFatal); }; - let name = eval_expr(name, context, scope, values)?; - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\').to_ascii_lowercase(); - values.bool_value(eval_function_probe_exists(context, &name)) + let value = eval_expr(value, context, scope, values)?; + eval_function_probe_result(name, value, context, values) +} + +/// Evaluates `function_exists()` and `is_callable()` from materialized arguments. +pub(in crate::interpreter) fn eval_function_probe_result( + name: &str, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match name { + "function_exists" => { + let name = eval_function_probe_name(value, values)?; + eval_function_probe_exists(context, &name) + } + "is_callable" => eval_is_callable_value(value, context, values)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(exists) } /// Evaluates `define(name, value)` for eval dynamic constant-name registration. @@ -597,3 +614,83 @@ pub(in crate::interpreter) fn eval_function_probe_exists( ) -> bool { !name.contains("::") && (context.has_function(name) || eval_php_visible_builtin_exists(name)) } + +/// Reads and normalizes a function-probe string argument. +fn eval_function_probe_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_ascii_lowercase()) +} + +/// Returns whether one runtime value is callable from the current eval scope. +fn eval_is_callable_value( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(callback) = eval_callable(value, context, values) else { + return Ok(false); + }; + eval_callable_probe_exists(&callback, context, values) +} + +/// Returns whether a normalized eval callback has an invokable target. +fn eval_callable_probe_exists( + callback: &EvaluatedCallable, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named(name) => Ok(eval_function_probe_exists(context, name)), + EvaluatedCallable::InvokableObject { object } => { + eval_object_method_callable_probe(*object, "__invoke", context, values) + } + EvaluatedCallable::ObjectMethod { object, method } => { + eval_object_method_callable_probe(*object, method, context, values) + } + EvaluatedCallable::StaticMethod { class_name, method } => Ok( + eval_static_method_callable_probe(class_name, method, context), + ), + } +} + +/// Returns whether one object method can be called from the current eval scope. +fn eval_object_method_callable_probe( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return Ok(false); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(false); + }; + let Some((declaring_class, method)) = + eval_dynamic_method_for_call(class.name(), method_name, context) + else { + return Ok(false); + }; + if method.is_static() || method.is_abstract() { + return Ok(false); + } + Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok()) +} + +/// Returns whether one static method can be called from the current eval scope. +fn eval_static_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> bool { + let Some((declaring_class, method)) = context.class_method(class_name, method_name) else { + return false; + }; + method.is_static() + && !method.is_abstract() + && validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() +} diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-eval/src/interpreter/control.rs index 4daf5b0ad0..3764f55842 100644 --- a/crates/elephc-eval/src/interpreter/control.rs +++ b/crates/elephc-eval/src/interpreter/control.rs @@ -65,6 +65,9 @@ pub(super) struct BoundMethodArg { /// One already evaluated PHP callback supported by the eval dispatcher. pub(super) enum EvaluatedCallable { Named(String), + InvokableObject { + object: RuntimeCellHandle, + }, ObjectMethod { object: RuntimeCellHandle, method: String, diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 17e162111c..7324687fd0 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -399,7 +399,7 @@ pub(in crate::interpreter) fn eval_dynamic_call( values: &mut impl RuntimeValueOps, ) -> Result { let callback = eval_expr(callee, context, scope, values)?; - let callback = eval_callable(callback, values)?; + let callback = eval_callable(callback, context, values)?; let evaluated_args = eval_call_arg_values(args, context, scope, values)?; eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) } @@ -557,7 +557,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { - eval_builtin_function_probe(args, context, scope, values) + eval_builtin_function_probe(name, args, context, scope, values) } "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 9571dcc592..9e025104c3 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2638,8 +2638,14 @@ fn eval_reflection_class_new_instance_result( }; if let Some(class) = context.class(&reflected_name).cloned() { let mut scope = ElephcEvalScope::new(); - return eval_dynamic_class_new_object(&class, constructor_args, context, &mut scope, values) - .map(Some); + return eval_dynamic_class_new_object( + &class, + constructor_args, + context, + &mut scope, + values, + ) + .map(Some); } let class_name = context .resolve_class_name(&reflected_name) @@ -2739,7 +2745,7 @@ fn eval_reflection_attribute_arg_value( } /// Resolves the method metadata visible from the current class scope. -fn eval_dynamic_method_for_call( +pub(in crate::interpreter) fn eval_dynamic_method_for_call( object_class_name: &str, method_name: &str, context: &ElephcEvalContext, @@ -2759,7 +2765,7 @@ fn eval_dynamic_method_for_call( } /// Returns whether the current eval class scope can access one declared member. -fn validate_eval_member_access( +pub(in crate::interpreter) fn validate_eval_member_access( declaring_class: &str, visibility: EvalVisibility, context: &ElephcEvalContext, diff --git a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs index 7a89d56aa1..ee6d718746 100644 --- a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs @@ -201,6 +201,62 @@ return $named(right: "H", left: "G");"#, assert_eq!(values.get(result), FakeValue::String("GH".to_string())); } +/// Verifies invokable eval objects dispatch through variable and callback call paths. +#[test] +fn execute_program_invokes_eval_object_callables() { + let program = parse_fragment( + br#"class EvalInvokableBox { + public function __construct($label = "box") { + $this->label = $label; + } + public function __invoke($left = "A", $right = "B") { + return $this->label . ":" . $left . $right; + } +} +class EvalPlainCallableProbe {} +$box = new EvalInvokableBox("box"); +$plain = new EvalPlainCallableProbe(); +echo is_callable($box) ? "Y:" : "N:"; +echo is_callable($plain) ? "bad:" : "plain:"; +echo $box(right: "D", left: "C"); echo ":"; +echo (new EvalInvokableBox("new"))("E", "F"); echo ":"; +echo call_user_func($box, "G", "H"); echo ":"; +return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:plain:box:CD:new:EF:box:GH:"); + assert_eq!(values.get(result), FakeValue::String("box:IJ".to_string())); +} + +/// Verifies object-method callable arrays preserve eval named-argument binding. +#[test] +fn execute_program_object_method_callable_array_binds_eval_named_args() { + let program = parse_fragment( + br#"class EvalObjectCallableArrayBox { + public function join($left, $right) { + return $left . $right; + } +} +$box = new EvalObjectCallableArrayBox(); +$cb = [$box, "join"]; +echo is_callable($cb) ? "Y:" : "N:"; +return call_user_func_array($cb, ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:"); + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} + /// Verifies static calls fall back to runtime AOT hooks when no eval class matches. #[test] fn execute_program_static_call_dispatches_runtime_method_hook() { diff --git a/docs/php/eval.md b/docs/php/eval.md index be2b8ce130..f8395f6888 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -72,7 +72,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | -| Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, `call_user_func()`, and `call_user_func_array()` for supported call targets. | +| Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects and `stdClass` storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | @@ -117,9 +117,13 @@ eval-declared functions, and registered AOT functions. Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, -...)`, `call_user_func_array($cb, [...])`, and `iterator_apply()` with -positional arguments. Static method callables can use `["ClassName", "method"]` -or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and +...)`, `call_user_func_array($cb, [...])`, and `iterator_apply()`. Eval-declared +object methods support string-keyed named arguments through +`call_user_func_array()`. Eval-declared objects with `__invoke()` can be called +through `$object(...)`, `call_user_func($object, ...)`, and +`call_user_func_array($object, [...])`, and `is_callable($object)` reports them +as callable. Static method callables can use `["ClassName", "method"]` or +`"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method fallback supports the same named-argument binding for public scalar/Mixed diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d58cb0ebae..5f3a489ffe 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5583,6 +5583,62 @@ echo $named(right: "H", left: "G");'); assert_eq!(out.stdout, "AB:CD:EF:GH"); } +/// Verifies eval invokable objects dispatch through variable and callback call paths. +#[test] +fn test_eval_declared_invokable_object_dynamic_callables() { + let out = compile_and_run_capture( + r#"label = $label; + } + public function __invoke($left = "A", $right = "B") { + return $this->label . ":" . $left . $right; + } +} +class EvalPlainCallableProbe {} +$box = new EvalInvokableBox("box"); +$plain = new EvalPlainCallableProbe(); +echo is_callable($box) ? "Y:" : "N:"; +echo is_callable($plain) ? "bad:" : "plain:"; +echo $box(right: "D", left: "C") . ":"; +echo (new EvalInvokableBox("new"))("E", "F") . ":"; +echo call_user_func($box, "G", "H") . ":"; +echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Y:plain:box:CD:new:EF:box:GH:box:IJ"); +} + +/// Verifies eval object-method callable arrays bind named arguments. +#[test] +fn test_eval_declared_object_method_callable_array_named_args() { + let out = compile_and_run_capture( + r#" "B", "left" => "A"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Y:AB"); +} + /// Verifies eval-declared class constants work through the bridge. #[test] fn test_eval_declared_class_constants_and_scoped_fetches() { From 00c47b62262530c47f57b23c4a7fb8923763454b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 00:24:47 +0200 Subject: [PATCH 0457/1208] Support eval magic method fallback --- .../elephc-eval/src/interpreter/statements.rs | 153 +++++++++++++++++- .../src/interpreter/tests/static_members.rs | 62 ++++++- crates/elephc-eval/src/parser/expressions.rs | 4 +- .../src/parser/tests/arrays_objects.rs | 2 +- .../src/parser/tests/static_members.rs | 4 +- docs/php/eval.md | 5 +- tests/codegen/eval.rs | 47 ++++++ 7 files changed, 258 insertions(+), 19 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 9e025104c3..869eb816a1 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1987,7 +1987,18 @@ pub(in crate::interpreter) fn eval_static_method_call_result( if !method.is_static() || method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, method.visibility(), context)?; + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { + if let Some(result) = eval_magic_static_method_call( + &class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } + return Err(EvalStatus::RuntimeFatal); + } return eval_dynamic_static_method_with_values( &declaring_class, &class_name, @@ -2002,6 +2013,15 @@ pub(in crate::interpreter) fn eval_static_method_call_result( || context.has_trait(&class_name) || context.has_enum(&class_name) { + if let Some(result) = eval_magic_static_method_call( + &class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } return Err(EvalStatus::RuntimeFatal); } let args = bind_native_callable_args( @@ -2586,22 +2606,139 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( return values.method_call(object, method_name, evaluated_args); }; let called_class_name = class.name().to_string(); - let (class_name, method) = + if let Some((class_name, method)) = eval_dynamic_method_for_call(&called_class_name, method_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; - validate_eval_member_access(&class_name, method.visibility(), context)?; + { + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&class_name, method.visibility(), context).is_ok() { + if method.is_static() { + return eval_dynamic_static_method_with_values( + &class_name, + &called_class_name, + &method, + evaluated_args, + context, + values, + ); + } + return eval_dynamic_method_with_values( + &class_name, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ); + } + } + if let Some(result) = eval_magic_instance_method_call( + object, + &called_class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Dispatches a missing or inaccessible eval instance method through `__call()`. +fn eval_magic_instance_method_call( + object: RuntimeCellHandle, + called_class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(called_class_name, "__call") else { + return Ok(None); + }; if method.is_static() || method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); + return Ok(None); } + validate_eval_member_access(&declaring_class, method.visibility(), context)?; + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; eval_dynamic_method_with_values( - &class_name, - &called_class_name, + &declaring_class, + called_class_name, &method, object, - evaluated_args, + magic_args, context, values, ) + .map(Some) +} + +/// Dispatches a missing or inaccessible eval static method through `__callStatic()`. +fn eval_magic_static_method_call( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(class_name, "__callStatic") else { + return Ok(None); + }; + if !method.is_static() || method.is_abstract() { + return Ok(None); + } + validate_eval_member_access(&declaring_class, method.visibility(), context)?; + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_dynamic_static_method_with_values( + &declaring_class, + class_name, + &method, + magic_args, + context, + values, + ) + .map(Some) +} + +/// Builds the two synthetic arguments passed to `__call()` and `__callStatic()`. +fn eval_magic_call_args( + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method = values.string(method_name)?; + let args = eval_magic_call_arg_array(evaluated_args, values)?; + Ok(positional_args(vec![method, args])) +} + +/// Materializes PHP's `$args` array for a magic method fallback. +fn eval_magic_call_arg_array( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let contains_named = evaluated_args.iter().any(|arg| arg.name.is_some()); + let mut args = if contains_named { + values.assoc_new(evaluated_args.len())? + } else { + values.array_new(evaluated_args.len())? + }; + let mut next_positional = 0_i64; + for arg in evaluated_args { + let key = if let Some(name) = arg.name { + values.string(&name)? + } else { + let key = values.int(next_positional)?; + next_positional = next_positional + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + }; + args = values.array_set(args, key, arg.value)?; + } + Ok(args) } /// Returns the runtime-visible class name for a non-eval object receiver. diff --git a/crates/elephc-eval/src/interpreter/tests/static_members.rs b/crates/elephc-eval/src/interpreter/tests/static_members.rs index c4538d4cf4..d82c07a987 100644 --- a/crates/elephc-eval/src/interpreter/tests/static_members.rs +++ b/crates/elephc-eval/src/interpreter/tests/static_members.rs @@ -100,9 +100,9 @@ return EvalStaticCallRules::read();"#, .expect_err("static call to instance method should fail"); } -/// Verifies eval rejects object-style calls to static methods. +/// Verifies eval allows object-style calls to accessible static methods. #[test] -fn execute_program_rejects_instance_call_to_eval_static_method() { +fn execute_program_allows_instance_call_to_eval_static_method() { let program = parse_fragment( br#"class EvalStaticInstanceRules { public static function read() { return 1; } @@ -114,6 +114,60 @@ return $box->read();"#, let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - execute_program(&program, &mut scope, &mut values) - .expect_err("instance call to static method should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(1)); +} + +/// Verifies missing and inaccessible instance methods dispatch through `__call`. +#[test] +fn execute_program_dispatches_eval_magic_call() { + let program = parse_fragment( + br#"class EvalMagicCallBox { + private function hidden($value) { return "bad"; } + public function __call($method, $args) { + return $method . ":" . $args[0] . ":" . $args["name"]; + } +} +$box = new EvalMagicCallBox(); +echo $box->DoThing("A", name: "B"); echo ":"; +return $box->hidden("C", name: "D");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DoThing:A:B:"); + assert_eq!( + values.get(result), + FakeValue::String("hidden:C:D".to_string()) + ); +} + +/// Verifies missing and inaccessible static methods dispatch through `__callStatic`. +#[test] +fn execute_program_dispatches_eval_magic_call_static() { + let program = parse_fragment( + br#"class EvalMagicStaticBox { + private static function hidden($value) { return "bad"; } + public static function __callStatic($method, $args) { + return $method . ":" . $args[0] . ":" . $args["name"]; + } +} +echo EvalMagicStaticBox::DoStatic("A", name: "B"); echo ":"; +return EvalMagicStaticBox::Hidden("C", name: "D");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DoStatic:A:B:"); + assert_eq!( + values.get(result), + FakeValue::String("Hidden:C:D".to_string()) + ); } diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs index 8dd277f0d5..cc9ef70ef2 100644 --- a/crates/elephc-eval/src/parser/expressions.rs +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -386,7 +386,7 @@ impl Parser { let args = self.parse_call_args()?; expr = EvalExpr::MethodCall { object: Box::new(expr), - method: member.to_ascii_lowercase(), + method: member, args, }; } else { @@ -610,7 +610,7 @@ impl Parser { Ok(EvalExpr::ClassNameFetch { class_name }) } TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { - let method = method.to_ascii_lowercase(); + let method = method.clone(); self.advance(); let args = self.parse_call_args()?; Ok(EvalExpr::StaticMethodCall { diff --git a/crates/elephc-eval/src/parser/tests/arrays_objects.rs b/crates/elephc-eval/src/parser/tests/arrays_objects.rs index 9594d88b48..38d81d44ef 100644 --- a/crates/elephc-eval/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-eval/src/parser/tests/arrays_objects.rs @@ -130,7 +130,7 @@ fn parse_fragment_accepts_method_call_source() { program.statements(), &[EvalStmt::Return(Some(EvalExpr::MethodCall { object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "answer".to_string(), + method: "Answer".to_string(), args: Vec::new(), }))] ); diff --git a/crates/elephc-eval/src/parser/tests/static_members.rs b/crates/elephc-eval/src/parser/tests/static_members.rs index 2663993872..9cfb5de9ae 100644 --- a/crates/elephc-eval/src/parser/tests/static_members.rs +++ b/crates/elephc-eval/src/parser/tests/static_members.rs @@ -58,7 +58,7 @@ fn parse_fragment_accepts_static_method_call_expression() { program.statements(), &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { class_name: "EvalStaticBox".to_string(), - method: "read".to_string(), + method: "Read".to_string(), args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], }))] ); @@ -73,7 +73,7 @@ fn parse_fragment_accepts_named_static_method_call_expression() { program.statements(), &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { class_name: "EvalStaticBox".to_string(), - method: "read".to_string(), + method: "Read".to_string(), args: vec![EvalCallArg::named( "step", EvalExpr::Const(EvalConst::Int(2)), diff --git a/docs/php/eval.md b/docs/php/eval.md index f8395f6888..d91bc65ac7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -75,7 +75,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects and `stdClass` storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -145,7 +145,8 @@ properties, trait composition with `insteadof` conflict resolution and `as` aliases/visibility adaptations, interface implementation checks, static properties, static methods, static interface method contracts, class, interface, trait, and enum constants including `final` constants, class-level attributes, -and `ClassName::class` literals. Member +`ClassName::class` literals, and magic method fallback through `__call()` and +`__callStatic()`. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, interfaces, traits, and enums are visible through `class_attribute_names()`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5f3a489ffe..b7103a4065 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5615,6 +5615,53 @@ echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); assert_eq!(out.stdout, "Y:plain:box:CD:new:EF:box:GH:box:IJ"); } +/// Verifies eval object method fallback dispatches missing and inaccessible methods through `__call`. +#[test] +fn test_eval_declared_magic_call_method_fallback() { + let out = compile_and_run_capture( + r#"DoThing("A", name: "B") . ":"; +echo $box->hidden("C", name: "D");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "DoThing:A:B:hidden:C:D"); +} + +/// Verifies eval static method fallback dispatches missing and inaccessible methods through `__callStatic`. +#[test] +fn test_eval_declared_magic_call_static_method_fallback() { + let out = compile_and_run_capture( + r#" Date: Sat, 20 Jun 2026 00:36:13 +0200 Subject: [PATCH 0458/1208] Support eval magic property get set --- .../elephc-eval/src/interpreter/statements.rs | 127 +++++++++++++++++- .../src/interpreter/tests/classes.rs | 95 +++++++++++++ docs/php/eval.md | 11 +- tests/codegen/eval.rs | 62 +++++++++ 4 files changed, 289 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 869eb816a1..820ac2ce60 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1610,10 +1610,19 @@ pub(in crate::interpreter) fn eval_property_get_result( }; let object_class_name = class.name().to_string(); let mut storage_property_name = property_name.to_string(); + let mut declared_property_found = false; if let Some((declaring_class, property)) = eval_dynamic_property_for_access(&object_class_name, property_name, context) { - validate_eval_member_access(&declaring_class, property.visibility(), context)?; + declared_property_found = true; + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + if let Some(result) = + eval_magic_property_get(object, &object_class_name, property_name, context, values)? + { + return Ok(result); + } + return Err(EvalStatus::RuntimeFatal); + } storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); if property.has_get_hook() && !current_eval_property_hook_is( @@ -1640,6 +1649,18 @@ pub(in crate::interpreter) fn eval_property_get_result( ); } } + if !declared_property_found + && eval_object_public_property_exists(object, property_name, values)? + { + return values.property_get(object, property_name); + } + if !declared_property_found { + if let Some(result) = + eval_magic_property_get(object, &object_class_name, property_name, context, values)? + { + return Ok(result); + } + } values.property_get(object, &storage_property_name) } @@ -1662,10 +1683,24 @@ pub(in crate::interpreter) fn eval_property_set_result( return Err(EvalStatus::RuntimeFatal); } let mut storage_property_name = property_name.to_string(); + let mut declared_property_found = false; if let Some((declaring_class, property)) = eval_dynamic_property_for_access(&object_class_name, property_name, context) { - validate_eval_member_access(&declaring_class, property.visibility(), context)?; + declared_property_found = true; + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + if eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? { + return Ok(()); + } + return Err(EvalStatus::RuntimeFatal); + } validate_eval_readonly_property_write(&declaring_class, &property, context)?; storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); if property.has_set_hook() { @@ -1701,9 +1736,97 @@ pub(in crate::interpreter) fn eval_property_set_result( return Err(EvalStatus::RuntimeFatal); } } + if !declared_property_found + && eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? + { + return Ok(()); + } values.property_set(object, &storage_property_name, value) } +/// Dispatches an undefined or inaccessible eval property read through `__get()`. +fn eval_magic_property_get( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__get") else { + return Ok(None); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, method.visibility(), context)?; + let property = values.string(property_name)?; + eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + ) + .map(Some) +} + +/// Dispatches an undefined or inaccessible eval property write through `__set()`. +fn eval_magic_property_set( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__set") else { + return Ok(false); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, method.visibility(), context)?; + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property, value]), + context, + values, + )?; + values.release(result)?; + Ok(true) +} + +/// Returns whether the object already has a public dynamic property with this exact name. +fn eval_object_public_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} + /// Validates that an object property may be used as a by-reference method argument. pub(in crate::interpreter) fn validate_property_ref_target( object: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 2493b7510c..3481cdfca3 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -369,6 +369,101 @@ return $name->value;"#, assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); } +/// Verifies undefined eval property reads and writes dispatch through `__get` and `__set`. +#[test] +fn execute_program_dispatches_eval_magic_get_and_set() { + let program = parse_fragment( + br#"class EvalMagicPropertyBox { + public string $events = ""; + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return "value:" . $name; + } + public function __set($name, $value) { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } +} +$box = new EvalMagicPropertyBox(); +echo $box->missing; echo ":"; +$box->other = "B"; +$box->events = $box->events . "public;"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "value:missing:"); + assert_eq!( + values.get(result), + FakeValue::String("get:missing;set:other=B;public;".to_string()) + ); +} + +/// Verifies inaccessible eval properties dispatch through magic property methods. +#[test] +fn execute_program_dispatches_inaccessible_eval_properties_to_magic_methods() { + let program = parse_fragment( + br#"class EvalMagicPrivatePropertyBox { + private string $secret = "raw"; + public string $events = ""; + public function readOwn() { return $this->secret; } + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return "read:" . $name; + } + public function __set($name, $value) { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } +} +$box = new EvalMagicPrivatePropertyBox(); +echo $box->readOwn(); echo ":"; +echo $box->secret; echo ":"; +$box->secret = "new"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "raw:read:secret:"); + assert_eq!( + values.get(result), + FakeValue::String("get:secret;set:secret=new;".to_string()) + ); +} + +/// Verifies dynamic properties created without `__set` are read directly even when `__get` exists. +#[test] +fn execute_program_reads_existing_dynamic_property_before_magic_get() { + let program = parse_fragment( + br#"class EvalMagicExistingDynamicBox { + public function __get($name) { + return "magic:" . $name; + } +} +$box = new EvalMagicExistingDynamicBox(); +$box->known = "plain"; +echo $box->known; echo ":"; +return $box->missing;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "plain:"); + assert_eq!( + values.get(result), + FakeValue::String("magic:missing".to_string()) + ); +} + /// Verifies get-only property hooks reject writes outside a set accessor. #[test] fn execute_program_rejects_write_to_get_only_eval_property_hook() { diff --git a/docs/php/eval.md b/docs/php/eval.md index d91bc65ac7..db369cde83 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -70,7 +70,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access, static property access, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | @@ -145,8 +145,9 @@ properties, trait composition with `insteadof` conflict resolution and `as` aliases/visibility adaptations, interface implementation checks, static properties, static methods, static interface method contracts, class, interface, trait, and enum constants including `final` constants, class-level attributes, -`ClassName::class` literals, and magic method fallback through `__call()` and -`__callStatic()`. Member +`ClassName::class` literals, magic method fallback through `__call()` and +`__callStatic()`, and magic property fallback through `__get()` and `__set()`. +Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, interfaces, traits, and enums are visible through `class_attribute_names()`, @@ -485,7 +486,9 @@ constant-expression subset, by-reference promoted constructor parameters, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Eval object cloning currently covers eval-declared and `stdClass` objects; cloning emitted AOT objects through the -eval bridge is still outside that bridge slice. +eval bridge is still outside that bridge slice. Magic property `__isset()` and +`__unset()` dispatch still require the eval parser to accept property operands +for those language constructs. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b7103a4065..5c9f9b5d93 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5196,6 +5196,68 @@ $box->answer = 7;'); ); } +/// Verifies eval-declared magic property methods handle missing and inaccessible properties. +#[test] +fn test_eval_declared_magic_property_methods() { + let out = compile_and_run_capture( + r#"secret; } + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return "read:" . $name; + } + public function __set($name, $value) { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } +} +$box = new EvalMagicPropertyBox(); +echo $box->readOwn() . ":"; +echo $box->secret . ":"; +echo $box->missing . ":"; +$box->secret = "new"; +$box->other = "B"; +$box->events = $box->events . "public;"; +echo $box->events;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "raw:read:secret:read:missing:get:secret;get:missing;set:secret=new;set:other=B;public;" + ); +} + +/// Verifies eval reads existing dynamic properties before falling back to `__get`. +#[test] +fn test_eval_declared_magic_get_preserves_existing_dynamic_property() { + let out = compile_and_run_capture( + r#"known = "plain"; +echo $box->known . ":"; +echo $box->missing;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "plain:magic:missing"); +} + /// Verifies eval-declared interface property hook contracts validate class properties. #[test] fn test_eval_declared_interface_property_hook_contracts() { From b20aa2697b6fced372b7b83330c51ad7add1b33d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 00:46:28 +0200 Subject: [PATCH 0459/1208] Support eval magic property isset unset --- crates/elephc-eval/src/eval_ir.rs | 4 + .../src/interpreter/builtins/symbols.rs | 31 +++- .../src/interpreter/dynamic_functions.rs | 1 + .../src/interpreter/expressions.rs | 2 +- .../elephc-eval/src/interpreter/statements.rs | 144 ++++++++++++++++++ .../src/interpreter/tests/classes.rs | 50 ++++++ crates/elephc-eval/src/parser/statements.rs | 15 +- .../src/parser/tests/exceptions_control.rs | 8 +- docs/php/eval.md | 9 +- tests/codegen/eval.rs | 47 ++++++ 10 files changed, 293 insertions(+), 18 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 3d84f44c71..488f65e31b 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -124,6 +124,10 @@ pub enum EvalStmt { catches: Vec, finally_body: Vec, }, + UnsetProperty { + object: EvalExpr, + property: String, + }, UnsetVar { name: String, }, diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index 8318c93c1f..bf5512b778 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -514,9 +514,10 @@ pub(in crate::interpreter) fn eval_builtin_empty( values.bool_value(empty) } -/// Evaluates direct `unset(...)` calls over eval-visible variable names. +/// Evaluates direct `unset(...)` calls over eval-visible variables and object properties. pub(in crate::interpreter) fn eval_builtin_unset( args: &[EvalExpr], + context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { @@ -524,11 +525,17 @@ pub(in crate::interpreter) fn eval_builtin_unset( return Err(EvalStatus::RuntimeFatal); } for arg in args { - let EvalExpr::LoadVar(name) = arg else { - return Err(EvalStatus::RuntimeFatal); - }; - if let Some(replaced) = unset_scope_cell(scope, name.clone()) { - values.release(replaced)?; + match arg { + EvalExpr::LoadVar(name) => { + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { + values.release(replaced)?; + } + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_unset_result(object, property, context, values)?; + } + _ => return Err(EvalStatus::RuntimeFatal), } } values.null() @@ -586,6 +593,14 @@ pub(in crate::interpreter) fn eval_empty_arg( }; return Ok(!values.truthy(value)?); } + if let EvalExpr::PropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if !eval_property_isset_result(object, property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, property, context, values)?; + return Ok(!values.truthy(value)?); + } let value = eval_expr(arg, context, scope, values)?; Ok(!values.truthy(value)?) } @@ -603,6 +618,10 @@ pub(in crate::interpreter) fn eval_isset_arg( }; return Ok(!values.is_null(value)?); } + if let EvalExpr::PropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + return eval_property_isset_result(object, property, context, values); + } let value = eval_expr(arg, context, scope, values)?; Ok(!values.is_null(value)?) } diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index fa908a4c9f..8c868d32c3 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -896,6 +896,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::StoreVar { .. } | EvalStmt::Throw(_) | EvalStmt::TraitDecl(_) + | EvalStmt::UnsetProperty { .. } | EvalStmt::UnsetVar { .. } => {} } } diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 7324687fd0..9e92412207 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -777,7 +777,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "long2ip" => eval_builtin_long2ip(args, context, scope, values), "trim" => eval_builtin_trim_like(name, args, context, scope, values), "ucwords" => eval_builtin_ucwords(args, context, scope, values), - "unset" => eval_builtin_unset(args, scope, values), + "unset" => eval_builtin_unset(args, context, scope, values), "umask" => eval_builtin_umask(args, context, scope, values), "usleep" => eval_builtin_usleep(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 820ac2ce60..8496a6ef60 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -223,6 +223,11 @@ pub(in crate::interpreter) fn execute_stmt( catches, finally_body, } => execute_try_stmt(body, catches, finally_body, context, scope, values), + EvalStmt::UnsetProperty { object, property } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_unset_result(object, property, context, values)?; + Ok(EvalControl::None) + } EvalStmt::UnsetVar { name } => { if let Some(replaced) = unset_scope_cell(scope, name.clone()) { values.release(replaced)?; @@ -1751,6 +1756,86 @@ pub(in crate::interpreter) fn eval_property_set_result( values.property_set(object, &storage_property_name, value) } +/// Evaluates PHP `isset($object->property)` without forcing `__get()` first. +pub(in crate::interpreter) fn eval_property_isset_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + }; + let object_class_name = class.name().to_string(); + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { + let value = eval_property_get_result(object, property_name, context, values)?; + return Ok(!values.is_null(value)?); + } + return eval_magic_property_isset( + object, + &object_class_name, + property_name, + context, + values, + ) + .map(|result| result.unwrap_or(false)); + } + if eval_object_public_property_exists(object, property_name, values)? { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + } + eval_magic_property_isset(object, &object_class_name, property_name, context, values) + .map(|result| result.unwrap_or(false)) +} + +/// Evaluates PHP `unset($object->property)` for eval-declared object receivers. +pub(in crate::interpreter) fn eval_property_unset_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(()); + }; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { + validate_eval_readonly_property_write(&declaring_class, &property, context)?; + let storage_property_name = + eval_instance_property_storage_name(&declaring_class, &property); + let null = values.null()?; + return values.property_set(object, &storage_property_name, null); + } + if eval_magic_property_unset(object, &object_class_name, property_name, context, values)? { + return Ok(()); + } + return Ok(()); + } + if eval_object_public_property_exists(object, property_name, values)? { + let null = values.null()?; + return values.property_set(object, property_name, null); + } + let _ = eval_magic_property_unset(object, &object_class_name, property_name, context, values)?; + Ok(()) +} + /// Dispatches an undefined or inaccessible eval property read through `__get()`. fn eval_magic_property_get( object: RuntimeCellHandle, @@ -1809,6 +1894,65 @@ fn eval_magic_property_set( Ok(true) } +/// Dispatches an undefined or inaccessible eval property probe through `__isset()`. +fn eval_magic_property_isset( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__isset") else { + return Ok(None); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, method.visibility(), context)?; + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + )?; + let truthy = values.truthy(result)?; + values.release(result)?; + Ok(Some(truthy)) +} + +/// Dispatches an undefined or inaccessible eval property unset through `__unset()`. +fn eval_magic_property_unset( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__unset") else { + return Ok(false); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, method.visibility(), context)?; + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + )?; + values.release(result)?; + Ok(true) +} + /// Returns whether the object already has a public dynamic property with this exact name. fn eval_object_public_property_exists( object: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 3481cdfca3..4f3a7fe527 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -464,6 +464,56 @@ return $box->missing;"#, ); } +/// Verifies eval property probes and unsets dispatch through `__isset` and `__unset`. +#[test] +fn execute_program_dispatches_eval_magic_isset_empty_and_unset() { + let program = parse_fragment( + br#"class EvalMagicPropertyProbeBox { + public string $events = ""; + public string $present = "ready"; + public $nullish = null; + private string $secret = "raw"; + public function __isset($name) { + $this->events = $this->events . "isset:" . $name . ";"; + return $name !== "no"; + } + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return $name === "empty" ? "" : "value:" . $name; + } + public function __unset($name) { + $this->events = $this->events . "unset:" . $name . ";"; + } +} +$box = new EvalMagicPropertyProbeBox(); +echo isset($box->present) ? "P" : "p"; echo ":"; +echo isset($box->nullish) ? "N" : "n"; echo ":"; +echo isset($box->secret) ? "S" : "s"; echo ":"; +echo isset($box->no) ? "bad" : "no"; echo ":"; +echo empty($box->secret) ? "bad" : "filled"; echo ":"; +echo empty($box->empty) ? "empty" : "bad"; echo ":"; +unset($box->present); +unset($box->secret); +unset($box->missing); +echo isset($box->present) ? "bad" : "unset"; echo ":"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "P:n:S:no:filled:empty:unset:"); + assert_eq!( + values.get(result), + FakeValue::String( + "isset:secret;isset:no;isset:secret;get:secret;isset:empty;get:empty;unset:secret;unset:missing;" + .to_string() + ) + ); +} + /// Verifies get-only property hooks reject writes outside a set accessor. #[test] fn execute_program_rejects_write_to_get_only_eval_property_hook() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 83224928af..27819366d7 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -2339,17 +2339,22 @@ impl Parser { Ok(body) } - /// Parses `unset($name[, ...]);`. + /// Parses `unset($name[, ...]);` with variable and object-property operands. pub(super) fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { self.advance(); self.expect(TokenKind::LParen)?; let mut statements = Vec::new(); loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); + let target = self.parse_expr()?; + let stmt = match target { + EvalExpr::LoadVar(name) => EvalStmt::UnsetVar { name }, + EvalExpr::PropertyGet { object, property } => EvalStmt::UnsetProperty { + object: *object, + property, + }, + _ => return Err(EvalParseError::ExpectedVariable), }; - statements.push(EvalStmt::UnsetVar { name: name.clone() }); - self.advance(); + statements.push(stmt); if !self.consume(TokenKind::Comma) { break; } diff --git a/crates/elephc-eval/src/parser/tests/exceptions_control.rs b/crates/elephc-eval/src/parser/tests/exceptions_control.rs index ad9e364220..bf96242ff6 100644 --- a/crates/elephc-eval/src/parser/tests/exceptions_control.rs +++ b/crates/elephc-eval/src/parser/tests/exceptions_control.rs @@ -251,16 +251,20 @@ fn parse_fragment_accepts_eval_finally_source() { }] ); } -/// Verifies unset fragments expand to one by-name unset statement per variable. +/// Verifies unset fragments expand variable and object-property operands. #[test] fn parse_fragment_accepts_unset_source() { - let program = parse_fragment(b"unset($x, $y);").expect("fragment should parse"); + let program = parse_fragment(b"unset($x, $this->name, $y);").expect("fragment should parse"); assert_eq!( program.statements(), &[ EvalStmt::UnsetVar { name: "x".to_string() }, + EvalStmt::UnsetProperty { + object: EvalExpr::LoadVar("this".to_string()), + property: "name".to_string(), + }, EvalStmt::UnsetVar { name: "y".to_string() }, diff --git a/docs/php/eval.md b/docs/php/eval.md index db369cde83..a463db77dc 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -70,7 +70,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, static property access, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | @@ -147,6 +147,9 @@ properties, static methods, static interface method contracts, class, interface, trait, and enum constants including `final` constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and `__callStatic()`, and magic property fallback through `__get()` and `__set()`. +`isset()`, `empty()`, and `unset()` on missing or inaccessible eval properties +dispatch through `__isset()` and `__unset()` using PHP's `empty()` gate +ordering. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, @@ -486,9 +489,7 @@ constant-expression subset, by-reference promoted constructor parameters, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Eval object cloning currently covers eval-declared and `stdClass` objects; cloning emitted AOT objects through the -eval bridge is still outside that bridge slice. Magic property `__isset()` and -`__unset()` dispatch still require the eval parser to accept property operands -for those language constructs. +eval bridge is still outside that bridge slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5c9f9b5d93..0dc87c8087 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5258,6 +5258,53 @@ echo $box->missing;'); assert_eq!(out.stdout, "plain:magic:missing"); } +/// Verifies eval property probes and unsets dispatch through `__isset` and `__unset`. +#[test] +fn test_eval_declared_magic_isset_empty_and_unset_property_methods() { + let out = compile_and_run_capture( + r#"events = $this->events . "isset:" . $name . ";"; + return $name !== "no"; + } + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return $name === "empty" ? "" : "value:" . $name; + } + public function __unset($name) { + $this->events = $this->events . "unset:" . $name . ";"; + } +} +$box = new EvalMagicPropertyProbeBox(); +echo isset($box->present) ? "P" : "p"; echo ":"; +echo isset($box->nullish) ? "N" : "n"; echo ":"; +echo isset($box->secret) ? "S" : "s"; echo ":"; +echo isset($box->no) ? "bad" : "no"; echo ":"; +echo empty($box->secret) ? "bad" : "filled"; echo ":"; +echo empty($box->empty) ? "empty" : "bad"; echo ":"; +unset($box->present); +unset($box->secret); +unset($box->missing); +echo isset($box->present) ? "bad" : "unset"; echo ":"; +echo $box->events;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "P:n:S:no:filled:empty:unset:isset:secret;isset:no;isset:secret;get:secret;isset:empty;get:empty;unset:secret;unset:missing;" + ); +} + /// Verifies eval-declared interface property hook contracts validate class properties. #[test] fn test_eval_declared_interface_property_hook_contracts() { From a5ba1bbaef04915882189155c35e3b182ae35d69 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 01:00:31 +0200 Subject: [PATCH 0460/1208] Support eval instanceof operator --- crates/elephc-eval/src/eval_ir.rs | 11 ++ .../src/interpreter/expressions.rs | 105 ++++++++++++++++++ crates/elephc-eval/src/interpreter/mod.rs | 4 +- .../src/interpreter/tests/expressions.rs | 54 +++++++++ crates/elephc-eval/src/parser/expressions.rs | 88 ++++++++++++++- .../elephc-eval/src/parser/tests/operators.rs | 51 +++++++++ docs/php/eval.md | 8 +- tests/codegen/eval.rs | 40 +++++++ 8 files changed, 353 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 488f65e31b..f10caa8e98 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1701,6 +1701,10 @@ pub enum EvalExpr { required: bool, once: bool, }, + InstanceOf { + value: Box, + target: EvalInstanceOfTarget, + }, LoadVar(String), Match { subject: Box, @@ -1772,6 +1776,13 @@ pub enum EvalExpr { }, } +/// The right-hand side accepted by PHP's `instanceof` operator. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalInstanceOfTarget { + ClassName(String), + Expr(Box), +} + /// One source-order function or method call argument parsed from eval code. #[derive(Debug, Clone, PartialEq)] pub struct EvalCallArg { diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 9e92412207..ee793379d3 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -45,6 +45,9 @@ pub(in crate::interpreter) fn eval_expr( required, once, } => eval_include_expr(path, *required, *once, context, scope, values), + EvalExpr::InstanceOf { value, target } => { + eval_instanceof_expr(value, target, context, scope, values) + } EvalExpr::LoadVar(name) => { visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) } @@ -250,6 +253,108 @@ fn eval_new_object_class_name( } } +/// Evaluates PHP's `instanceof` operator over static and dynamic class targets. +fn eval_instanceof_expr( + value: &EvalExpr, + target: &EvalInstanceOfTarget, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(value, context, scope, values)?; + let result = match target { + EvalInstanceOfTarget::ClassName(class_name) => { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return values.bool_value(false); + } + let target_class = eval_instanceof_static_target_name(class_name, context)?; + eval_instanceof_object_result(value, &target_class, context, values)? + } + EvalInstanceOfTarget::Expr(target) => { + let target = eval_expr(target, context, scope, values)?; + let target_class = eval_instanceof_dynamic_target_name(target, context, values)?; + if values.type_tag(value)? == EVAL_TAG_OBJECT { + eval_instanceof_object_result(value, &target_class, context, values)? + } else { + false + } + } + }; + values.bool_value(result) +} + +/// Resolves a static `instanceof` target according to eval class aliases and scope keywords. +fn eval_instanceof_static_target_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(eval_instanceof_resolved_target_name(class_name, context)), + } +} + +/// Resolves a dynamic `instanceof` target cell to the PHP class name it represents. +fn eval_instanceof_dynamic_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_STRING => { + let target = eval_instanceof_string_target_name(target, values)?; + Ok(eval_instanceof_resolved_target_name(&target, context)) + } + EVAL_TAG_OBJECT => eval_instanceof_object_target_name(target, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads and normalizes one string-valued dynamic `instanceof` target. +fn eval_instanceof_string_target_name( + target: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(target)?; + let target = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(target.trim_start_matches('\\').to_string()) +} + +/// Reads the runtime class of an object-valued dynamic `instanceof` target. +fn eval_instanceof_object_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(target)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + let class_name = values.object_class_name(target)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Applies eval alias resolution to a target class name without requiring it to exist. +fn eval_instanceof_resolved_target_name(target: &str, context: &ElephcEvalContext) -> String { + context + .resolve_class_name(target) + .unwrap_or_else(|| target.trim_start_matches('\\').to_string()) +} + +/// Tests one object cell against a resolved `instanceof` target class/interface name. +fn eval_instanceof_object_result( + value: RuntimeCellHandle, + target_class: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, target_class, false, context, values)? + .map_or_else(|| values.object_is_a(value, target_class, false), Ok) +} + /// Evaluates a PHP `match` expression with strict comparison and lazy arm values. pub(in crate::interpreter) fn eval_match_expr( subject: &EvalExpr, diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index f797eed058..f8fb766ed0 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -35,8 +35,8 @@ use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, - EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalFunction, EvalInterface, EvalInterfaceMethod, - EvalInterfaceProperty, EvalMagicConst, EvalMatchArm, EvalParameterType, + EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalFunction, EvalInstanceOfTarget, EvalInterface, + EvalInterfaceMethod, EvalInterfaceProperty, EvalMagicConst, EvalMatchArm, EvalParameterType, EvalParameterTypeVariant, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 33be6714b3..bb34bb4445 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -394,6 +394,60 @@ return $box->base;"#, ); assert_eq!(values.get(result), FakeValue::Int(3)); } + +/// Verifies eval `instanceof` uses eval class, interface, and dynamic-target metadata. +#[test] +fn execute_program_evaluates_eval_instanceof_targets() { + let program = parse_fragment( + br#"interface EvalInstanceIface {} +class EvalInstanceBase {} +class EvalInstanceChild extends EvalInstanceBase implements EvalInstanceIface {} +class EvalInstanceOther {} +$box = new EvalInstanceChild(); +$class = "EvalInstanceChild"; +$target = ["EvalInstanceIface"]; +$prefix = "EvalInstance"; +$suffix = "Base"; +$targetObject = new EvalInstanceChild(); +echo $box instanceof EvalInstanceChild ? "C" : "c"; +echo $box instanceof EvalInstanceBase ? "B" : "b"; +echo $box instanceof EvalInstanceIface ? "I" : "i"; +echo $box instanceof $class ? "D" : "d"; +echo $box instanceof $target[0] ? "A" : "a"; +echo $box instanceof ($prefix . $suffix) ? "P" : "p"; +echo $box instanceof $targetObject ? "O" : "o"; +echo 7 instanceof MissingEvalClass ? "bad" : "S"; +return $box instanceof EvalInstanceOther;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "CBIDAPOS"); + assert_eq!(values.get(result), FakeValue::Bool(false)); +} + +/// Verifies dynamic `instanceof` rejects targets that are not strings or objects. +#[test] +fn execute_program_rejects_invalid_dynamic_instanceof_target() { + let program = parse_fragment( + br#"class EvalInvalidInstanceTarget {} +$box = new EvalInvalidInstanceTarget(); +$target = 42; +return $box instanceof $target;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("invalid instanceof target should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval-declared classes can implement eval-declared interfaces. #[test] fn execute_program_constructs_eval_declared_class_with_dynamic_interface() { diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs index cc9ef70ef2..064c09280e 100644 --- a/crates/elephc-eval/src/parser/expressions.rs +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -12,8 +12,8 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalMagicConst, EvalMatchArm, - EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalInstanceOfTarget, + EvalMagicConst, EvalMatchArm, EvalUnaryOp, }; use crate::lexer::TokenKind; @@ -338,7 +338,89 @@ impl Parser { expr: Box::new(expr), }); } - self.parse_power() + self.parse_instanceof() + } + + /// Parses left-associative `instanceof` with PHP's high operator precedence. + pub(super) fn parse_instanceof(&mut self) -> Result { + let mut expr = self.parse_power()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "instanceof")) { + self.advance(); + let target = self.parse_instanceof_target()?; + expr = EvalExpr::InstanceOf { + value: Box::new(expr), + target, + }; + } + Ok(expr) + } + + /// Parses a static or dynamic target after PHP's `instanceof` operator. + pub(super) fn parse_instanceof_target( + &mut self, + ) -> Result { + if self.consume(TokenKind::LParen) { + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + return Ok(EvalInstanceOfTarget::Expr(Box::new(expr))); + } + if matches!(self.current(), TokenKind::DollarIdent(_)) { + let target = self.parse_instanceof_variable_target()?; + return Ok(EvalInstanceOfTarget::Expr(Box::new(target))); + } + let name = self.parse_qualified_name()?; + let class_name = self.resolve_static_class_name(name); + if self.consume(TokenKind::DoubleColon) { + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let property = property.clone(); + self.advance(); + return Ok(EvalInstanceOfTarget::Expr(Box::new( + EvalExpr::StaticPropertyGet { + class_name, + property, + }, + ))); + } + Ok(EvalInstanceOfTarget::ClassName(class_name)) + } + + /// Parses PHP's unparenthesized dynamic `instanceof` variable/property/array target. + pub(super) fn parse_instanceof_variable_target(&mut self) -> Result { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut expr = EvalExpr::LoadVar(name.clone()); + self.advance(); + loop { + if self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + continue; + } + if self.consume(TokenKind::Arrow) { + let TokenKind::Ident(member) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + return Err(EvalParseError::UnexpectedToken); + } + expr = EvalExpr::PropertyGet { + object: Box::new(expr), + property: member, + }; + continue; + } + break; + } + Ok(expr) } /// Parses right-associative exponentiation with higher precedence than unary prefix operators. diff --git a/crates/elephc-eval/src/parser/tests/operators.rs b/crates/elephc-eval/src/parser/tests/operators.rs index 91acf392cf..42d9d4e589 100644 --- a/crates/elephc-eval/src/parser/tests/operators.rs +++ b/crates/elephc-eval/src/parser/tests/operators.rs @@ -78,6 +78,57 @@ fn parse_fragment_accepts_strict_equality_source() { }))] ); } +/// Verifies static `instanceof` parses as a high-precedence EvalIR expression. +#[test] +fn parse_fragment_accepts_static_instanceof_source() { + let program = + parse_fragment(br#"return !$object instanceof App\Box;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(EvalExpr::InstanceOf { + value: Box::new(EvalExpr::LoadVar("object".to_string())), + target: EvalInstanceOfTarget::ClassName("App\\Box".to_string()), + }), + }))] + ); +} + +/// Verifies dynamic `instanceof` targets parse from variables, properties, arrays, and parens. +#[test] +fn parse_fragment_accepts_dynamic_instanceof_targets() { + let program = parse_fragment( + br#"return $object instanceof $names[0] . ":" . ($object instanceof ($prefix . $suffix));"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::InstanceOf { + value: Box::new(EvalExpr::LoadVar("object".to_string())), + target: EvalInstanceOfTarget::Expr(Box::new(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::LoadVar("names".to_string())), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + })), + }), + right: Box::new(EvalExpr::Const(EvalConst::String(":".to_string()))), + }), + right: Box::new(EvalExpr::InstanceOf { + value: Box::new(EvalExpr::LoadVar("object".to_string())), + target: EvalInstanceOfTarget::Expr(Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("prefix".to_string())), + right: Box::new(EvalExpr::LoadVar("suffix".to_string())), + })), + }), + }))] + ); +} + /// Verifies logical operators parse with `&&` binding tighter than `||`. #[test] fn parse_fragment_accepts_short_circuit_logical_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index a463db77dc..1023e3dc77 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -70,7 +70,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, static property access, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | @@ -149,7 +149,9 @@ trait, and enum constants including `final` constants, class-level attributes, `__callStatic()`, and magic property fallback through `__get()` and `__set()`. `isset()`, `empty()`, and `unset()` on missing or inaccessible eval properties dispatch through `__isset()` and `__unset()` using PHP's `empty()` gate -ordering. +ordering. `instanceof` works with eval-declared classes and interfaces, +generated/AOT runtime objects, dynamic string targets, dynamic object targets, +and parenthesized target expressions. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, @@ -339,7 +341,7 @@ objects. Eval `__clone()` hooks are invoked on the cloned object after storage copying and use the same runtime visibility checks as method calls. AOT and eval-declared class-name probes are visible through `class_exists()`. -Eval object relation probes through `is_a()` and `is_subclass_of()` use +Eval object relation probes through `instanceof`, `is_a()`, and `is_subclass_of()` use generated AOT class/interface metadata and eval-created object metadata. `interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated AOT metadata. Eval-declared classes, interfaces, traits, and class aliases are diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0dc87c8087..66bb63279b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4769,6 +4769,46 @@ echo function_exists("is_a"); echo function_exists("is_subclass_of");'); assert_eq!(out, "YYYNYYYYN11"); } +/// Verifies eval `instanceof` probes AOT and eval-declared class metadata. +#[test] +fn test_eval_fragment_instanceof_probes_class_metadata() { + let out = compile_and_run_capture( + r#" Date: Sat, 20 Jun 2026 01:14:44 +0200 Subject: [PATCH 0461/1208] Support eval magic tostring --- crates/elephc-eval/src/context.rs | 16 +++++ .../builtins/registry/dispatch/scalars.rs | 4 +- .../src/interpreter/builtins/scalars/types.rs | 8 ++- .../src/interpreter/dynamic_functions.rs | 72 +++++++++++++++++-- .../src/interpreter/expressions.rs | 7 +- .../elephc-eval/src/interpreter/statements.rs | 1 + .../src/interpreter/tests/classes.rs | 54 ++++++++++++++ docs/php/eval.md | 4 ++ tests/codegen/eval.rs | 31 ++++++++ 9 files changed, 187 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 626f673c9d..8fe5d16670 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -1114,6 +1114,11 @@ impl ElephcEvalContext { if !exclude_self && normalize_class_name(class.name()) == target { return true; } + if target == normalize_class_name("Stringable") + && self.class_has_valid_tostring(class.name()) + { + return true; + } self.class_parent_names(class.name()) .iter() .any(|parent| normalize_class_name(parent) == target) @@ -1123,6 +1128,17 @@ impl ElephcEvalContext { .any(|interface| normalize_class_name(interface) == target) } + /// Returns whether one eval class exposes a PHP-compatible `__toString()` method. + fn class_has_valid_tostring(&self, class_name: &str) -> bool { + self.class_method(class_name, "__toString") + .is_some_and(|(_, method)| { + method.visibility() == EvalVisibility::Public + && !method.is_static() + && !method.is_abstract() + && method.params().is_empty() + }) + } + /// Defines an eval dynamic constant value, failing if the name is invalid or already present. pub fn define_constant(&mut self, name: &str, value: RuntimeCellHandle) -> bool { let key = normalize_constant_name(name); diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs index 62bb82e61b..accc9df1e7 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs @@ -15,7 +15,7 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_scalars_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { @@ -82,7 +82,7 @@ pub(in crate::interpreter) fn eval_scalars_builtin_with_values( let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_cast_result(name, *value, values)? + eval_cast_result(name, *value, context, values)? } "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, "gettype" => { diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs index 141380c7b3..7b99efedc1 100644 --- a/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs @@ -22,19 +22,23 @@ pub(in crate::interpreter) fn eval_builtin_cast( return Err(EvalStatus::RuntimeFatal); }; let value = eval_expr(value, context, scope, values)?; - eval_cast_result(name, value, values) + eval_cast_result(name, value, context, values) } /// Dispatches an already evaluated value through the matching PHP cast hook. pub(in crate::interpreter) fn eval_cast_result( name: &str, value: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match name { "intval" => values.cast_int(value), "floatval" => values.cast_float(value), - "strval" => values.cast_string(value), + "strval" => { + let value = eval_string_context_value(value, context, values)?; + values.cast_string(value) + } "boolval" => values.cast_bool(value), _ => Err(EvalStatus::UnsupportedConstruct), } diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 8c868d32c3..99362d237f 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -349,7 +349,7 @@ fn bind_dynamic_positional_method_arg( next_variadic_index: &mut i64, value: RuntimeCellHandle, ref_target: Option, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if variadic_index.is_some_and(|index| *next_positional >= index) { @@ -401,7 +401,7 @@ fn bind_dynamic_named_method_arg( value: RuntimeCellHandle, ref_target: Option, variadic_named_args: &mut std::collections::HashSet, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if let Some(param_index) = regular_method_param_index(params, variadic_index, name) { @@ -456,7 +456,7 @@ fn eval_variadic_method_parameter_value( parameter_types: &[Option], variadic_index: Option, value: RuntimeCellHandle, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let Some(param_type) = @@ -501,7 +501,7 @@ fn bind_dynamic_variadic_arg( fn eval_method_parameter_value( param_type: &EvalParameterType, value: RuntimeCellHandle, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if eval_method_parameter_type_accepts_exact(param_type, value, context, values)? { @@ -511,7 +511,9 @@ fn eval_method_parameter_value( return Err(EvalStatus::RuntimeFatal); } for variant in param_type.variants() { - if let Some(coerced) = eval_method_parameter_scalar_coercion(variant, value, values)? { + if let Some(coerced) = + eval_method_parameter_scalar_coercion(variant, value, context, values)? + { return Ok(coerced); } } @@ -629,6 +631,7 @@ fn eval_method_parameter_runtime_class_name( fn eval_method_parameter_scalar_coercion( variant: &EvalParameterTypeVariant, value: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let tag = values.type_tag(value)?; @@ -649,10 +652,69 @@ fn eval_method_parameter_scalar_coercion( EvalParameterTypeVariant::String if eval_method_scalar_coercible_tag(tag) => { values.cast_string(value).map(Some) } + EvalParameterTypeVariant::String if tag == EVAL_TAG_OBJECT => { + let coerced = eval_dynamic_object_string_context_value(value, context, values)?; + if values.type_tag(coerced)? == EVAL_TAG_STRING { + Ok(Some(coerced)) + } else { + Ok(None) + } + } _ => Ok(None), } } +/// Converts eval-declared objects in string contexts through `__toString()`. +pub(in crate::interpreter) fn eval_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Ok(value); + } + eval_dynamic_object_string_context_value(value, context, values) +} + +/// Invokes `__toString()` for eval-created objects and rejects missing invalid hooks. +fn eval_dynamic_object_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(value)?; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(value); + }; + let called_class_name = class.name().to_string(); + let Some((declaring_class, method)) = context.class_method(&called_class_name, "__toString") + else { + return Err(EvalStatus::RuntimeFatal); + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() + || method.is_abstract() + || !method.params().is_empty() + { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + value, + Vec::new(), + context, + values, + )?; + if values.type_tag(result)? == EVAL_TAG_STRING { + return Ok(result); + } + let coerced = values.cast_string(result)?; + values.release(result)?; + Ok(coerced) +} + /// Returns whether a runtime tag can be weakly coerced to string/bool parameters. fn eval_method_scalar_coercible_tag(tag: u64) -> bool { matches!( diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index ee793379d3..8fb414786d 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -147,6 +147,7 @@ pub(in crate::interpreter) fn eval_expr( } EvalExpr::Print(inner) => { let value = eval_expr(inner, context, scope, values)?; + let value = eval_string_context_value(value, context, values)?; values.echo(value)?; values.int(1) } @@ -217,7 +218,11 @@ pub(in crate::interpreter) fn eval_expr( | EvalBinOp::BitXor | EvalBinOp::ShiftLeft | EvalBinOp::ShiftRight => values.bitwise(*op, left, right), - EvalBinOp::Concat => values.concat(left, right), + EvalBinOp::Concat => { + let left = eval_string_context_value(left, context, values)?; + let right = eval_string_context_value(right, context, values)?; + values.concat(left, right) + } EvalBinOp::LogicalXor => { let left_truthy = values.truthy(left)?; let right_truthy = values.truthy(right)?; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 8496a6ef60..be2e8eadf2 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -89,6 +89,7 @@ pub(in crate::interpreter) fn execute_stmt( } EvalStmt::Echo(expr) => { let value = eval_expr(expr, context, scope, values)?; + let value = eval_string_context_value(value, context, values)?; values.echo(value)?; Ok(EvalControl::None) } diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 4f3a7fe527..f73c2f9274 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -514,6 +514,60 @@ return $box->events;"#, ); } +/// Verifies eval objects stringify through public `__toString()` in PHP string contexts. +#[test] +fn execute_program_dispatches_eval_magic_tostring_for_string_contexts() { + let program = parse_fragment( + br#"class EvalStringableBox { + public string $name = "Ada"; + public function __toString() { + return "box:" . $this->name; + } + public function accepts(string $value) { + return "typed:" . $value; + } +} +$box = new EvalStringableBox(); +echo $box; echo ":"; +print $box; echo ":"; +echo "pre" . $box; echo ":"; +echo strval($box); echo ":"; +echo call_user_func("strval", $box); echo ":"; +echo call_user_func_array("strval", [$box]); echo ":"; +echo $box instanceof Stringable ? "S" : "s"; echo ":"; +return $box->accepts($box);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "box:Ada:box:Ada:prebox:Ada:box:Ada:box:Ada:box:Ada:S:" + ); + assert_eq!( + values.get(result), + FakeValue::String("typed:box:Ada".to_string()) + ); +} + +/// Verifies eval objects without `__toString()` fail in PHP string contexts. +#[test] +fn execute_program_rejects_eval_object_string_context_without_tostring() { + let program = parse_fragment( + br#"class EvalPlainStringContext {} +$box = new EvalPlainStringContext(); +echo $box;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect_err("missing __toString should fail"); +} + /// Verifies get-only property hooks reject writes outside a set accessor. #[test] fn execute_program_rejects_write_to_get_only_eval_property_hook() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 1023e3dc77..382939332c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -152,6 +152,10 @@ dispatch through `__isset()` and `__unset()` using PHP's `empty()` gate ordering. `instanceof` works with eval-declared classes and interfaces, generated/AOT runtime objects, dynamic string targets, dynamic object targets, and parenthesized target expressions. +Eval-declared objects in string contexts dispatch through public +parameterless `__toString()` for `echo`, `print`, concatenation, `strval()`, +callable `strval()` dispatch, and weak `string` parameter coercion. Classes +with a compatible `__toString()` satisfy `Stringable` implicitly. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 66bb63279b..c96e437838 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3176,6 +3176,37 @@ echo ":"; echo function_exists("boolval");'); assert_eq!(out, "42:3.5:12:false:7:9:1"); } +/// Verifies eval-declared `__toString()` runs in string contexts through the bridge. +#[test] +fn test_eval_declared_tostring_string_contexts() { + let out = compile_and_run( + r#"name; + } + public function accepts(string $value) { + return "typed:" . $value; + } +} +$box = new EvalStringableBox(); +echo $box; echo ":"; +print $box; echo ":"; +echo "pre" . $box; echo ":"; +echo strval($box); echo ":"; +echo call_user_func("strval", $box); echo ":"; +echo call_user_func_array("strval", [$box]); echo ":"; +echo $box instanceof Stringable ? "S" : "s"; echo ":"; +echo $box->accepts($box);'); +"#, + ); + assert_eq!( + out, + "box:Ada:box:Ada:prebox:Ada:box:Ada:box:Ada:box:Ada:S:typed:box:Ada" + ); +} + /// Verifies eval `settype()` mutates direct variables and supports named arguments. #[test] fn test_eval_dispatches_settype_builtin_call() { From 6958b07ae4cb0c1432fa37e8d7ddd536e954739d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 01:22:29 +0200 Subject: [PATCH 0462/1208] Validate eval magic method contracts --- .../elephc-eval/src/interpreter/statements.rs | 91 +++++++++++++++++++ .../src/interpreter/tests/classes.rs | 47 ++++++++++ docs/php/eval.md | 4 + tests/codegen/eval.rs | 18 ++++ 4 files changed, 160 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index be2e8eadf2..b544ca2ca7 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -705,6 +705,7 @@ pub(in crate::interpreter) fn execute_trait_decl_stmt( return Err(EvalStatus::RuntimeFatal); } validate_eval_declared_constants(trait_decl.constants())?; + validate_eval_magic_methods(trait_decl.methods())?; if context.define_trait(trait_decl.clone()) { initialize_eval_declared_constants( trait_decl.name(), @@ -1016,6 +1017,7 @@ fn validate_eval_class_modifiers( validate_property_parent_redeclaration(class, property, context)?; } for method in class.methods() { + validate_eval_magic_method(method)?; if method.is_abstract() && method.is_final() { return Err(EvalStatus::RuntimeFatal); } @@ -1033,6 +1035,95 @@ fn validate_eval_class_modifiers( Ok(()) } +/// Validates PHP magic-method contracts for one eval class-like method list. +fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalStatus> { + for method in methods { + validate_eval_magic_method(method)?; + } + Ok(()) +} + +/// Validates staticness, visibility, and arity for one eval magic method. +fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus> { + match method.name().to_ascii_lowercase().as_str() { + "__tostring" => { + validate_magic_non_static(method)?; + validate_magic_public(method)?; + validate_magic_arity(method, 0)?; + } + "__get" | "__isset" | "__unset" => { + validate_magic_non_static(method)?; + validate_magic_public(method)?; + validate_magic_arity(method, 1)?; + } + "__set" | "__call" => { + validate_magic_non_static(method)?; + validate_magic_public(method)?; + validate_magic_arity(method, 2)?; + } + "__callstatic" => { + validate_magic_static(method)?; + validate_magic_public(method)?; + validate_magic_arity(method, 2)?; + } + "__invoke" => { + validate_magic_non_static(method)?; + validate_magic_public(method)?; + } + "__clone" | "__destruct" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + } + "__construct" => { + if method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + } + _ => {} + } + Ok(()) +} + +/// Rejects static declarations for magic methods that must be instance methods. +fn validate_magic_non_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.is_static() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects instance declarations for magic methods that must be static methods. +fn validate_magic_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.is_static() { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Rejects non-public declarations for public-only PHP magic methods. +fn validate_magic_public(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.visibility() == EvalVisibility::Public { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Rejects magic methods whose arity differs from PHP's required shape. +fn validate_magic_arity(method: &EvalClassMethod, expected: usize) -> Result<(), EvalStatus> { + let has_variadic = method + .parameter_is_variadic() + .iter() + .any(|is_variadic| *is_variadic); + if method.params().len() == expected && !has_variadic { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + /// Validates property declarations that can be checked before class registration. fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index f73c2f9274..de18362a7d 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -568,6 +568,53 @@ echo $box;"#, execute_program(&program, &mut scope, &mut values).expect_err("missing __toString should fail"); } +/// Verifies eval rejects magic methods whose staticness, visibility, or arity is invalid. +#[test] +fn execute_program_rejects_invalid_eval_magic_method_contracts() { + let cases: Vec<(&[u8], &str)> = vec![ + ( + br#"class EvalBadToString { private function __toString() { return "x"; } }"#.as_slice(), + "private __toString", + ), + ( + br#"class EvalBadGet { protected function __get($name) { return "x"; } }"#.as_slice(), + "protected __get", + ), + ( + br#"class EvalBadCall { public function __call($name, ...$args) { return "x"; } }"#.as_slice(), + "variadic __call", + ), + ( + br#"class EvalBadCallStatic { public function __callStatic($name, $args) { return "x"; } }"#.as_slice(), + "instance __callStatic", + ), + ( + br#"class EvalBadInvoke { private function __invoke() { return 1; } }"#.as_slice(), + "private __invoke", + ), + ( + br#"class EvalBadClone { public static function __clone() {} }"#.as_slice(), + "static __clone", + ), + ( + br#"class EvalBadDestruct { public static function __destruct() {} }"#.as_slice(), + "static __destruct", + ), + ( + br#"trait EvalBadMagicTrait { public static function __isset($name) { return true; } }"#.as_slice(), + "trait static __isset", + ), + ]; + + for (source, label) in cases { + let program = parse_fragment(source).expect(label); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect_err(label); + } +} + /// Verifies get-only property hooks reject writes outside a set accessor. #[test] fn execute_program_rejects_write_to_get_only_eval_property_hook() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 382939332c..edaeff7c1f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -156,6 +156,10 @@ Eval-declared objects in string contexts dispatch through public parameterless `__toString()` for `echo`, `print`, concatenation, `strval()`, callable `strval()` dispatch, and weak `string` parameter coercion. Classes with a compatible `__toString()` satisfy `Stringable` implicitly. +Eval validates magic method staticness, visibility, and arity contracts for +`__toString()`, `__get()`, `__set()`, `__isset()`, `__unset()`, `__call()`, +`__callStatic()`, `__invoke()`, `__clone()`, `__destruct()`, and `__construct()` +when dynamic classes or traits are declared. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c96e437838..17415d084c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5842,6 +5842,24 @@ echo EvalMagicStaticBox::Hidden("C", name: "D");'); assert_eq!(out.stdout, "DoStatic:A:B:Hidden:C:D"); } +/// Verifies eval rejects invalid magic method contracts during dynamic class declaration. +#[test] +fn test_eval_rejects_invalid_magic_method_contracts() { + let err = compile_and_run_expect_failure( + r#" Date: Sat, 20 Jun 2026 01:49:41 +0200 Subject: [PATCH 0463/1208] Support eval ReflectionFunction metadata --- crates/elephc-eval/src/context.rs | 16 +++ .../elephc-eval/src/interpreter/reflection.rs | 132 +++++++++++++++++- .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_reflection_functions.rs | 43 ++++++ .../elephc-eval/src/interpreter/tests/mod.rs | 1 + .../interpreter/tests/support/object_ops.rs | 10 +- docs/php/eval.md | 15 +- src/codegen/eval_method_helpers.rs | 16 +-- src/codegen/eval_reflection_owner_helpers.rs | 28 +++- tests/codegen/eval.rs | 32 +++++ 10 files changed, 273 insertions(+), 21 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 8fe5d16670..9ccb4b894c 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -162,6 +162,7 @@ pub struct ElephcEvalContext { dynamic_objects: HashMap, eval_reflection_attributes: HashMap, eval_reflection_classes: HashMap, + eval_reflection_functions: HashMap, eval_reflection_methods: HashMap, eval_reflection_properties: HashMap, global_scope: Option<*mut ElephcEvalScope>, @@ -208,6 +209,7 @@ impl ElephcEvalContext { dynamic_objects: HashMap::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), + eval_reflection_functions: HashMap::new(), eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), global_scope: None, @@ -255,6 +257,7 @@ impl ElephcEvalContext { dynamic_objects: HashMap::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), + eval_reflection_functions: HashMap::new(), eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), global_scope: None, @@ -563,6 +566,19 @@ impl ElephcEvalContext { .map(String::as_str) } + /// Records reflected function metadata for one synthetic ReflectionFunction object. + pub fn register_eval_reflection_function(&mut self, identity: u64, function_name: &str) { + self.eval_reflection_functions + .insert(identity, function_name.trim_start_matches('\\').to_string()); + } + + /// Returns the reflected function name attached to a synthetic ReflectionFunction. + pub fn eval_reflection_function_name(&self, identity: u64) -> Option<&str> { + self.eval_reflection_functions + .get(&identity) + .map(String::as_str) + } + /// Records reflected method metadata for one synthetic ReflectionMethod object. pub fn register_eval_reflection_method( &mut self, diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index c426ab09e4..4c934f860e 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -140,6 +140,9 @@ pub(in crate::interpreter) fn eval_reflection_owner_new_object( Some(EVAL_REFLECTION_OWNER_CLASS) => { eval_reflection_class_new(evaluated_args, context, values) } + Some(EVAL_REFLECTION_OWNER_FUNCTION) => { + eval_reflection_function_new(evaluated_args, context, values) + } Some(EVAL_REFLECTION_OWNER_METHOD) => { eval_reflection_method_new(evaluated_args, context, values) } @@ -1125,6 +1128,85 @@ fn eval_reflection_aot_class_flags( Ok(Some((flags, modifiers))) } +/// Builds an eval-backed `ReflectionFunction` object for eval or registered native functions. +fn eval_reflection_function_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("function")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if let Some(function) = context.function(&lookup_name).cloned() { + let parameters = + eval_reflection_function_parameters(function.name(), function.params(), Vec::new()); + return eval_reflection_function_object_result( + function.name(), + ¶meters, + context, + values, + ) + .map(Some); + } + if let Some(function) = context.native_function(&lookup_name) { + let reflected_name = requested_name.trim_start_matches('\\'); + let parameter_names = eval_reflection_native_function_parameter_names(&function); + let parameters = + eval_reflection_function_parameters(reflected_name, ¶meter_names, Vec::new()); + return eval_reflection_function_object_result( + reflected_name, + ¶meters, + context, + values, + ) + .map(Some); + } + Ok(None) +} + +/// Returns parameter names for a registered native function, filling missing bridge names. +fn eval_reflection_native_function_parameter_names(function: &NativeFunction) -> Vec { + (0..function.param_count()) + .map(|index| { + function + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", index)) + }) + .collect() +} + +/// Builds one `ReflectionFunction` object from retained eval function metadata. +fn eval_reflection_function_object_result( + function_name: &str, + parameters: &[EvalReflectionParameterMetadata], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_FUNCTION, + function_name, + &[], + &[], + &[], + &[], + &[], + None, + parameters, + None, + None, + 0, + parameters.len() as u64, + 0, + None, + None, + context, + values, + ) +} + /// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. fn eval_reflection_method_new( evaluated_args: Vec, @@ -1498,7 +1580,10 @@ fn eval_reflection_owner_object_with_members( context, values, )? - } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + } else if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION + ) { eval_reflection_parameter_object_array_result(parameter_metadata, context, values)? } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { match type_metadata { @@ -1573,6 +1658,9 @@ fn eval_reflection_owner_object_with_members( let identity = values.object_identity(object)?; context.register_eval_reflection_method(identity, declaring_class, reflected_name); } + } else if owner_kind == EVAL_REFLECTION_OWNER_FUNCTION { + let identity = values.object_identity(object)?; + context.register_eval_reflection_function(identity, reflected_name); } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { if let Some(declaring_class) = parent_class_name { if context.has_class(declaring_class) { @@ -1815,8 +1903,18 @@ fn eval_reflection_declaring_function_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + let owner_kind = if metadata.declaring_class_name.is_some() { + EVAL_REFLECTION_OWNER_METHOD + } else { + EVAL_REFLECTION_OWNER_FUNCTION + }; + let method_modifiers = if metadata.declaring_class_name.is_some() { + eval_reflection_method_modifiers_from_flags(metadata.flags) + } else { + 0 + }; eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_METHOD, + owner_kind, &metadata.name, &metadata.attributes, &[], @@ -1829,7 +1927,7 @@ fn eval_reflection_declaring_function_object_result( None, metadata.flags, metadata.required_parameter_count as u64, - eval_reflection_method_modifiers_from_flags(metadata.flags), + method_modifiers, None, None, context, @@ -3555,6 +3653,33 @@ fn eval_reflection_parameters_from_names_and_type_flags( .collect() } +/// Builds ReflectionParameter metadata for eval-declared or native free functions. +fn eval_reflection_function_parameters( + function_name: &str, + names: &[String], + attributes: Vec, +) -> Vec { + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: function_name.to_string(), + declaring_class_name: None, + attributes, + flags: 0, + required_parameter_count: names.len(), + }; + eval_reflection_parameters_from_names_and_type_flags( + None, + Some(&declaring_function), + names, + &[], + &[], + &[], + &[], + &[], + &[], + &[], + ) +} + /// Returns promoted constructor parameter names for one eval class method. fn eval_reflection_promoted_parameter_names( class_name: &str, @@ -3775,6 +3900,7 @@ fn reflection_owner_kind(class_name: &str) -> Option { .as_str() { "reflectionclass" => Some(EVAL_REFLECTION_OWNER_CLASS), + "reflectionfunction" => Some(EVAL_REFLECTION_OWNER_FUNCTION), "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), "reflectionproperty" => Some(EVAL_REFLECTION_OWNER_PROPERTY), "reflectionclassconstant" => Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT), diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 7291c3fbed..8681478c6f 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -405,3 +405,4 @@ pub(super) const EVAL_REFLECTION_OWNER_PARAMETER: u64 = 6; pub(super) const EVAL_REFLECTION_OWNER_NAMED_TYPE: u64 = 7; pub(super) const EVAL_REFLECTION_OWNER_UNION_TYPE: u64 = 8; pub(super) const EVAL_REFLECTION_OWNER_INTERSECTION_TYPE: u64 = 9; +pub(super) const EVAL_REFLECTION_OWNER_FUNCTION: u64 = 10; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs new file mode 100644 index 0000000000..c241294d02 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Interpreter tests for eval-backed ReflectionFunction objects. +//! +//! Called from: +//! - `cargo test -p elephc-eval` through Rust's test harness. +//! +//! Key details: +//! - Free eval functions retain only parameter names today, so required counts +//! match the visible parameter list until richer function metadata exists. + +use super::super::*; +use super::support::*; + +/// Verifies eval-declared functions materialize ReflectionFunction parameter metadata. +#[test] +fn execute_program_reflects_eval_declared_function_parameters() { + let program = parse_fragment( + br#"function eval_reflect_free($left, $right) { return $left; } +$ref = new ReflectionFunction("eval_reflect_free"); +$params = $ref->getParameters(); +echo $ref->getName(); echo ":"; +echo $ref->getNumberOfParameters(); echo ":"; +echo $ref->getNumberOfRequiredParameters(); echo ":"; +echo count($params); echo ":"; +echo $params[0]->getName(); echo ":"; +echo $params[1]->getPosition(); echo ":"; +$declaring = $params[0]->getDeclaringFunction(); +echo get_class($declaring); echo ":"; +echo $declaring->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "eval_reflect_free:2:2:2:left:1:ReflectionFunction:eval_reflect_free" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-eval/src/interpreter/tests/mod.rs index 0eb9d4642d..acb80fd20c 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-eval/src/interpreter/tests/mod.rs @@ -24,6 +24,7 @@ mod builtins_language_constructs; mod builtins_math_formatting; mod builtins_process_pipes; mod builtins_readline; +mod builtins_reflection_functions; mod builtins_scalars; mod builtins_spl_autoload; mod builtins_stream_contexts; diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 770f4cb2c4..a340eae99a 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -548,6 +548,7 @@ impl FakeOps { ) -> Result { let class_name = match owner_kind { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", + EVAL_REFLECTION_OWNER_FUNCTION => "ReflectionFunction", EVAL_REFLECTION_OWNER_METHOD => "ReflectionMethod", EVAL_REFLECTION_OWNER_PROPERTY => "ReflectionProperty", EVAL_REFLECTION_OWNER_CLASS_CONSTANT => "ReflectionClassConstant", @@ -654,6 +655,13 @@ impl FakeOps { ) { properties.push(("__declaring_class".to_string(), parent_class)); } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION + ) { + properties.push(("__parameters".to_string(), method_objects)); + properties.push(("__required_parameter_count".to_string(), modifiers_cell)); + } if owner_kind == EVAL_REFLECTION_OWNER_METHOD { let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; let is_abstract = @@ -661,8 +669,6 @@ impl FakeOps { let method_modifiers = self.int(method_modifiers as i64)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); - properties.push(("__parameters".to_string(), method_objects)); - properties.push(("__required_parameter_count".to_string(), modifiers_cell)); properties.push(("__modifiers".to_string(), method_modifiers)); } if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { diff --git a/docs/php/eval.md b/docs/php/eval.md index edaeff7c1f..ce0b4712bb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -268,10 +268,12 @@ with PHP-compatible `ReflectionMethod::IS_*` constants for the bitmask. `ReflectionMethod::isConstructor()` and `isDestructor()` report whether the reflected method is `__construct` or `__destruct`. `ReflectionMethod::setAccessible()` is accepted as a PHP-compatible no-op. +`ReflectionFunction::getName()`, `ReflectionFunction::getParameters()`, `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and -`getNumberOfRequiredParameters()` report eval-declared method parameter -metadata. Eval currently exposes parameter names and zero-based positions there, -declared-type presence, simple named type metadata through +`getNumberOfRequiredParameters()` report retained eval-declared function and +method metadata. Free eval functions expose the reflected function name, +parameter names, and positions from their currently retained function signature; +eval methods also expose declared-type presence, simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, `allowsNull()`, and `isBuiltin()`, and multi-member union metadata through `ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter @@ -280,8 +282,9 @@ metadata is exposed through `ReflectionIntersectionType::getTypes()` and `ReflectionParameter::getAttributes()` using materialized `ReflectionAttribute` objects. `ReflectionParameter::getDeclaringClass()` returns the declaring class-like symbol for eval method parameters, and -`ReflectionParameter::getDeclaringFunction()` returns a `ReflectionMethod` -object for the declaring eval method. Defaulted eval method parameters are bound +`ReflectionParameter::getDeclaringFunction()` returns a `ReflectionFunction` +object for eval free-function parameters or a `ReflectionMethod` object for the +declaring eval method. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and `getDefaultValue()`. Supported default expressions include @@ -492,7 +495,7 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported -ReflectionClass/Method/Parameter/Property/NamedType/UnionType/IntersectionType +ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond parameter/property metadata, broader parameter default-value objects beyond the eval-supported constant-expression subset, by-reference promoted constructor parameters, and diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 0fd05cd06c..c4c04c5fb1 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -635,8 +635,8 @@ fn emit_aarch64_builtin_throwable_method_name_branch( emitter.instruction("ldr x2, [sp, #8]"); // reload requested method-name length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_str_eq"); // compare requested method name with this Throwable method - emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the compact Throwable method when names match + emitter.instruction("bl __rt_strcasecmp"); // compare Throwable method names with PHP case-insensitive rules + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the compact Throwable method when names match } /// Emits one x86_64 method-name comparison for a compact Throwable method. @@ -652,9 +652,9 @@ fn emit_x86_64_builtin_throwable_method_name_branch( emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested method-name length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_str_eq"); // compare requested method name with this Throwable method + emitter.instruction("call __rt_strcasecmp"); // compare Throwable method names with PHP case-insensitive rules emitter.instruction("test rax, rax"); // check whether the method names matched - emitter.instruction(&format!("jne {}", target_label)); // dispatch to the compact Throwable method when names match + emitter.instruction(&format!("je {}", target_label)); // dispatch to the compact Throwable method when names match } /// Emits one ARM64 method-name comparison and branch to the matching body. @@ -669,9 +669,9 @@ fn emit_aarch64_method_name_compare( emitter.instruction("ldr x2, [sp, #8]"); // reload requested method-name length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_str_eq"); // compare requested method name with this public method + emitter.instruction("bl __rt_strcasecmp"); // compare public method names with PHP case-insensitive rules let target_label = method_body_label(module, slot); - emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the method body when the names match + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the method body when the names match } /// Emits one x86_64 method-name comparison and branch to the matching body. @@ -686,9 +686,9 @@ fn emit_x86_64_method_name_compare( emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested method-name length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_str_eq"); // compare requested method name with this public method + emitter.instruction("call __rt_strcasecmp"); // compare public method names with PHP case-insensitive rules emitter.instruction("test rax, rax"); // check whether the method names matched - emitter.instruction(&format!("jne {}", method_body_label(module, slot))); // dispatch to the method body when the names match + emitter.instruction(&format!("je {}", method_body_label(module, slot))); // dispatch to the method body when the names match } /// Emits one ARM64 static method-name comparison and branch to the matching body. diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 280a490adc..3ac1a441d4 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1,7 +1,7 @@ //! Purpose: //! Emits user-assembly helpers that let libelephc-eval materialize -//! ReflectionClass, ReflectionMethod, ReflectionParameter, ReflectionProperty, -//! ReflectionClassConstant, ReflectionEnum*, and ReflectionType objects +//! ReflectionClass, ReflectionFunction, ReflectionMethod, ReflectionParameter, +//! ReflectionProperty, ReflectionClassConstant, ReflectionEnum*, and ReflectionType objects //! with private metadata slots populated from runtime eval declarations. //! //! Called from: @@ -121,6 +121,7 @@ struct ReflectionOwnerLayout { /// Layouts for the Reflection owner classes eval can materialize. struct ReflectionOwnerLayouts { class: ReflectionOwnerLayout, + function: ReflectionOwnerLayout, method: ReflectionOwnerLayout, property: ReflectionOwnerLayout, class_constant: ReflectionOwnerLayout, @@ -182,6 +183,7 @@ fn function_uses_eval(function: &Function) -> bool { fn reflection_owner_layouts(module: &Module) -> Option { Some(ReflectionOwnerLayouts { class: reflection_owner_layout(module.class_infos.get("ReflectionClass")?, true)?, + function: reflection_owner_layout(module.class_infos.get("ReflectionFunction")?, true)?, method: reflection_owner_layout(module.class_infos.get("ReflectionMethod")?, true)?, property: reflection_owner_layout(module.class_infos.get("ReflectionProperty")?, true)?, class_constant: reflection_owner_layout( @@ -386,6 +388,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection let done_label = "__elephc_eval_reflection_owner_new_done"; let box_label = "__elephc_eval_reflection_owner_new_box"; let class_label = "__elephc_eval_reflection_owner_new_class"; + let function_label = "__elephc_eval_reflection_owner_new_function"; let method_label = "__elephc_eval_reflection_owner_new_method"; let property_label = "__elephc_eval_reflection_owner_new_property"; let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant"; @@ -442,6 +445,8 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction(&format!("b.eq {}", union_type_label)); // allocate a ReflectionUnionType owner emitter.instruction("cmp x0, #9"); // owner kind 9 means ReflectionIntersectionType emitter.instruction(&format!("b.eq {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner + emitter.instruction("cmp x0, #10"); // owner kind 10 means ReflectionFunction + emitter.instruction(&format!("b.eq {}", function_label)); // allocate a ReflectionFunction owner emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body( emitter, @@ -451,6 +456,14 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection fail_label, box_label, ); + emit_aarch64_owner_kind_body( + emitter, + function_label, + &layouts.function, + true, + fail_label, + box_label, + ); emit_aarch64_owner_kind_body( emitter, method_label, @@ -543,6 +556,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let done_label = "__elephc_eval_reflection_owner_new_done_x"; let box_label = "__elephc_eval_reflection_owner_new_box_x"; let class_label = "__elephc_eval_reflection_owner_new_class_x"; + let function_label = "__elephc_eval_reflection_owner_new_function_x"; let method_label = "__elephc_eval_reflection_owner_new_method_x"; let property_label = "__elephc_eval_reflection_owner_new_property_x"; let class_constant_label = "__elephc_eval_reflection_owner_new_class_constant_x"; @@ -601,6 +615,8 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction(&format!("je {}", union_type_label)); // allocate a ReflectionUnionType owner emitter.instruction("cmp rdi, 9"); // owner kind 9 means ReflectionIntersectionType emitter.instruction(&format!("je {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner + emitter.instruction("cmp rdi, 10"); // owner kind 10 means ReflectionFunction + emitter.instruction(&format!("je {}", function_label)); // allocate a ReflectionFunction owner emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body( emitter, @@ -610,6 +626,14 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO fail_label, box_label, ); + emit_x86_64_owner_kind_body( + emitter, + function_label, + &layouts.function, + true, + fail_label, + box_label, + ); emit_x86_64_owner_kind_body( emitter, method_label, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 17415d084c..5ac392e256 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7620,6 +7620,38 @@ echo $listed->getDeclaringFunction()->getName();'); ); } +/// Verifies eval ReflectionFunction materializes eval-declared function parameters. +#[test] +fn test_eval_reflection_function_reports_eval_function_parameters() { + let out = compile_and_run_capture( + r#"getParameters(); +echo $ref->getName() . ":"; +echo $ref->getNumberOfParameters() . ":"; +echo $ref->getNumberOfRequiredParameters() . ":"; +echo count($params) . ":"; +echo $params[0]->getName() . ":"; +echo $params[1]->getPosition() . ":"; +$declaring = $params[0]->getDeclaringFunction(); +echo get_class($declaring) . ":"; +echo $declaring->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "eval_reflect_free:2:2:2:left:1:ReflectionFunction:eval_reflect_free" + ); +} + /// Verifies eval ReflectionClass::isCloneable uses eval class metadata through the bridge. #[test] fn test_eval_reflection_class_cloneable_predicate() { From f678dc80a076cebff4d4afe06b40e0315bf68ca7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 02:03:09 +0200 Subject: [PATCH 0464/1208] Support rich eval function metadata --- crates/elephc-eval/src/eval_ir.rs | 92 +++++++++++++++++++ .../interpreter/builtins/registry/callable.rs | 8 +- .../src/interpreter/dynamic_functions.rs | 58 ++++++++++-- .../elephc-eval/src/interpreter/reflection.rs | 72 ++++++++++++--- .../elephc-eval/src/interpreter/statements.rs | 24 ++++- .../tests/builtins_reflection_functions.rs | 81 +++++++++++++++- crates/elephc-eval/src/parser/statements.rs | 60 +++++++----- .../src/parser/tests/namespaces.rs | 18 ++++ docs/php/eval.md | 14 +-- tests/codegen/eval.rs | 75 +++++++++++++++ 10 files changed, 440 insertions(+), 62 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index f10caa8e98..c366a6163b 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -80,7 +80,13 @@ pub enum EvalStmt { }, FunctionDecl { name: String, + attributes: Vec, params: Vec, + parameter_attributes: Vec>, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, body: Vec, }, Global { @@ -150,30 +156,116 @@ pub struct EvalCatch { #[derive(Debug, Clone, PartialEq)] pub struct EvalFunction { name: String, + attributes: Vec, params: Vec, + parameter_attributes: Vec>, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, body: Vec, } impl EvalFunction { /// Creates a dynamic eval function with source-order parameters and body. pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { + let parameter_attributes = vec![Vec::new(); params.len()]; + let parameter_types = vec![None; params.len()]; + let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), + attributes: Vec::new(), params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, body, } } + /// Returns a copy of this function with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this function with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + + /// Returns a copy of this function with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_types = parameter_types; + self + } + + /// Returns a copy of this function with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + + /// Returns a copy of this function with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + + /// Returns a copy of this function with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + /// Returns the original source spelling of this eval-declared function name. pub fn name(&self) -> &str { &self.name } + /// Returns attributes declared directly on this eval function. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + /// Returns source-order parameter names without leading `$`. pub fn params(&self) -> &[String] { &self.params } + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } + + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } + /// Returns the dynamic EvalIR statements that form the function body. pub fn body(&self) -> &[EvalStmt] { &self.body diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs b/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs index 2fa6a6a1d8..e2209f43bf 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs @@ -268,8 +268,12 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( return Ok(result); } if let Some(function) = context.function(name).cloned() { - let evaluated_args = bind_evaluated_function_args(function.params(), evaluated_args)?; - return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + return eval_dynamic_function_with_evaluated_args( + &function, + evaluated_args, + context, + values, + ); } if let Some(function) = context.native_function(name) { if function.param_names().len() != function.param_count() { diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 99362d237f..a2d6604b57 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -18,9 +18,8 @@ pub(in crate::interpreter) fn eval_dynamic_function( caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = - eval_function_call_args(function.params(), args, context, caller_scope, values)?; - eval_dynamic_function_with_values(function, evaluated_args, context, values) + let evaluated_args = eval_call_arg_values(args, context, caller_scope, values)?; + eval_dynamic_function_with_evaluated_args(function, evaluated_args, context, values) } /// Evaluates and binds function-like arguments to parameter order. @@ -851,12 +850,49 @@ pub(super) fn eval_dynamic_function_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let mut function_scope = ElephcEvalScope::new(); - for (name, value) in function.params().iter().zip(evaluated_args) { - function_scope.set(name.clone(), value, ScopeCellOwnership::Borrowed); - } + let evaluated_args = evaluated_args + .into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + eval_dynamic_function_with_evaluated_args(function, evaluated_args, context, values) +} + +/// Evaluates an eval-declared function after call arguments preserve names and ref targets. +pub(super) fn eval_dynamic_function_with_evaluated_args( + function: &EvalFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { let static_names = static_var_names(function.body()); context.push_function(function.name()); + let evaluated_args = match bind_evaluated_method_args( + function.params(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + evaluated_args, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + context.pop_function(); + return Err(status); + } + }; + let mut function_scope = ElephcEvalScope::new(); + bind_method_scope_args( + &mut function_scope, + function.params(), + function.parameter_is_by_ref(), + &evaluated_args, + ); let result = execute_statements(function.body(), context, &mut function_scope, values); let persist_result = persist_static_locals( context, @@ -865,8 +901,16 @@ pub(super) fn eval_dynamic_function_with_values( &function_scope, values, ); + let writeback_result = write_back_method_ref_args( + function.params(), + &evaluated_args, + &function_scope, + context, + values, + ); context.pop_function(); persist_result?; + writeback_result?; match result? { EvalControl::None => values.null(), EvalControl::Return(result) => Ok(result), diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 4c934f860e..5363362033 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1138,11 +1138,25 @@ fn eval_reflection_function_new( let requested_name = eval_reflection_string_arg(args[0], values)?; let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); if let Some(function) = context.function(&lookup_name).cloned() { - let parameters = - eval_reflection_function_parameters(function.name(), function.params(), Vec::new()); + let required_parameter_count = eval_reflection_required_parameter_count( + function.parameter_defaults(), + function.parameter_is_variadic(), + ); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); return eval_reflection_function_object_result( function.name(), + function.attributes(), ¶meters, + required_parameter_count, context, values, ) @@ -1151,11 +1165,28 @@ fn eval_reflection_function_new( if let Some(function) = context.native_function(&lookup_name) { let reflected_name = requested_name.trim_start_matches('\\'); let parameter_names = eval_reflection_native_function_parameter_names(&function); - let parameters = - eval_reflection_function_parameters(reflected_name, ¶meter_names, Vec::new()); + let parameter_attributes = vec![Vec::new(); parameter_names.len()]; + let parameter_types: Vec> = vec![None; parameter_names.len()]; + let parameter_defaults = vec![None; parameter_names.len()]; + let parameter_is_by_ref = vec![false; parameter_names.len()]; + let parameter_is_variadic = vec![false; parameter_names.len()]; + let required_parameter_count = + eval_reflection_required_parameter_count(¶meter_defaults, ¶meter_is_variadic); + let parameters = eval_reflection_function_parameters( + reflected_name, + ¶meter_names, + Vec::new(), + ¶meter_attributes, + ¶meter_types, + ¶meter_defaults, + ¶meter_is_by_ref, + ¶meter_is_variadic, + ); return eval_reflection_function_object_result( reflected_name, + &[], ¶meters, + required_parameter_count, context, values, ) @@ -1181,14 +1212,16 @@ fn eval_reflection_native_function_parameter_names(function: &NativeFunction) -> /// Builds one `ReflectionFunction` object from retained eval function metadata. fn eval_reflection_function_object_result( function_name: &str, + attributes: &[EvalAttribute], parameters: &[EvalReflectionParameterMetadata], + required_parameter_count: usize, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { eval_reflection_owner_object( EVAL_REFLECTION_OWNER_FUNCTION, function_name, - &[], + attributes, &[], &[], &[], @@ -1198,7 +1231,7 @@ fn eval_reflection_function_object_result( None, None, 0, - parameters.len() as u64, + required_parameter_count as u64, 0, None, None, @@ -3657,25 +3690,34 @@ fn eval_reflection_parameters_from_names_and_type_flags( fn eval_reflection_function_parameters( function_name: &str, names: &[String], - attributes: Vec, + function_attributes: Vec, + parameter_attributes: &[Vec], + parameter_types: &[Option], + defaults: &[Option], + by_ref_flags: &[bool], + variadic_flags: &[bool], ) -> Vec { + let has_type_flags = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: function_name.to_string(), declaring_class_name: None, - attributes, + attributes: function_attributes, flags: 0, - required_parameter_count: names.len(), + required_parameter_count: eval_reflection_required_parameter_count(defaults, variadic_flags), }; eval_reflection_parameters_from_names_and_type_flags( None, Some(&declaring_function), names, - &[], - &[], - &[], - &[], - &[], - &[], + &has_type_flags, + parameter_types, + parameter_attributes, + defaults, + by_ref_flags, + variadic_flags, &[], ) } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index b544ca2ca7..32967afd89 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -137,12 +137,28 @@ pub(in crate::interpreter) fn execute_stmt( scope, values, ), - EvalStmt::FunctionDecl { name, params, body } => { + EvalStmt::FunctionDecl { + name, + attributes, + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + body, + } => { let key = name.to_ascii_lowercase(); context .define_function( key, - EvalFunction::new(name.clone(), params.clone(), body.clone()), + EvalFunction::new(name.clone(), params.clone(), body.clone()) + .with_attributes(attributes.clone()) + .with_parameter_attributes(parameter_attributes.clone()) + .with_parameter_types(parameter_types.clone()) + .with_parameter_defaults(parameter_defaults.clone()) + .with_parameter_by_ref_flags(parameter_is_by_ref.clone()) + .with_parameter_variadic_flags(parameter_is_variadic.clone()), ) .map_err(|_| EvalStatus::RuntimeFatal)?; Ok(EvalControl::None) @@ -3299,7 +3315,7 @@ fn eval_classes_are_related(left: &str, right: &str, context: &ElephcEvalContext } /// Binds method parameters into a fresh method scope and marks by-reference params as aliases. -fn bind_method_scope_args( +pub(in crate::interpreter) fn bind_method_scope_args( method_scope: &mut ElephcEvalScope, params: &[String], parameter_is_by_ref: &[bool], @@ -3383,7 +3399,7 @@ fn same_method_ref_target(left: &EvaluatedCallRefTarget, right: &EvaluatedCallRe } /// Writes completed by-reference method parameter values back to their caller-side variables. -fn write_back_method_ref_args( +pub(in crate::interpreter) fn write_back_method_ref_args( params: &[String], bound_args: &[BoundMethodArg], method_scope: &ElephcEvalScope, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs index c241294d02..f15e06e17f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs @@ -5,8 +5,8 @@ //! - `cargo test -p elephc-eval` through Rust's test harness. //! //! Key details: -//! - Free eval functions retain only parameter names today, so required counts -//! match the visible parameter list until richer function metadata exists. +//! - Free eval functions retain function and parameter metadata used by +//! ReflectionFunction and ReflectionParameter. use super::super::*; use super::support::*; @@ -41,3 +41,80 @@ return true;"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval-declared function metadata includes attributes, types, defaults, and flags. +#[test] +fn execute_program_reflects_eval_function_signature_metadata() { + let program = parse_fragment( + br#"class EvalFuncAttr { + public $label; + public function __construct($label) { $this->label = $label; } + public function label() { return $this->label; } +} +#[EvalFuncAttr("free")] +function eval_reflect_rich(#[EvalFuncAttr("first")] string $name, int $count = 3, &...$items) { + return $count; +} +$ref = new ReflectionFunction("eval_reflect_rich"); +$attrs = $ref->getAttributes(); +$params = $ref->getParameters(); +echo count($attrs); echo ":"; +echo $attrs[0]->getName(); echo ":"; +echo $attrs[0]->newInstance()->label(); echo ":"; +echo $ref->getNumberOfParameters(); echo ":"; +echo $ref->getNumberOfRequiredParameters(); echo ":"; +echo $params[0]->hasType() ? "T" : "t"; echo ":"; +echo $params[0]->getType()->getName(); echo ":"; +$paramAttrs = $params[0]->getAttributes(); +echo count($paramAttrs); echo ":"; +echo $paramAttrs[0]->newInstance()->label(); echo ":"; +echo $params[1]->isOptional() ? "O" : "o"; echo ":"; +echo $params[1]->getDefaultValue(); echo ":"; +echo $params[2]->isVariadic() ? "V" : "v"; echo ":"; +echo $params[2]->isPassedByReference() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:EvalFuncAttr:free:3:1:T:string:1:first:O:3:V:R" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared functions bind named, default, by-reference, and variadic arguments. +#[test] +fn execute_program_calls_eval_function_with_rich_argument_binding() { + let program = parse_fragment( + br#"function eval_signature_call(string $name, &$value, int $count = 2, ...$rest) { + $value = $value + $count; + echo $name; echo ":"; + echo $count; echo ":"; + echo count($rest); echo ":"; +} +function eval_signature_array(string $name, int $count = 2, ...$rest) { + echo $name; echo ":"; + echo $count; echo ":"; + echo count($rest); echo ":"; + echo $rest["extra"]; +} +$seed = 4; +eval_signature_call(name: "ok", value: $seed, extra: "z"); +echo $seed; echo ":"; +call_user_func_array("eval_signature_array", ["extra" => "z", "name" => "cb"]); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ok:2:1:6:cb:2:1:z"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 27819366d7..7470cc7c0e 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -171,6 +171,9 @@ impl Parser { TokenKind::Ident(name) if ident_eq(name, "trait") => { self.parse_trait_decl_stmt_with_attributes(attributes) } + TokenKind::Ident(name) if ident_eq(name, "function") => { + self.parse_function_decl_stmt_with_attributes(attributes) + } _ => Err(EvalParseError::UnsupportedConstruct), } } @@ -1488,6 +1491,14 @@ impl Parser { /// Parses `function name($param, ...) { ... }` declarations. pub(super) fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_function_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses `function name($param, ...) { ... }` declarations with attributes. + pub(super) fn parse_function_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1495,9 +1506,31 @@ impl Parser { let name = self.qualify_name_in_current_namespace(name); self.advance(); self.expect(TokenKind::LParen)?; - let params = self.parse_function_params()?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params("")?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } let body = self.parse_block()?; - Ok(vec![EvalStmt::FunctionDecl { name, params, body }]) + Ok(vec![EvalStmt::FunctionDecl { + name, + attributes, + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + body, + }]) } /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. @@ -1783,29 +1816,6 @@ impl Parser { Ok(class_names) } - /// Parses a dynamic function declaration parameter list after `(`. - pub(super) fn parse_function_params(&mut self) -> Result, EvalParseError> { - let mut params = Vec::new(); - if self.consume(TokenKind::RParen) { - return Ok(params); - } - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - params.push(name.clone()); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - if matches!(self.current(), TokenKind::RParen) { - return Err(EvalParseError::ExpectedVariable); - } - } - self.expect(TokenKind::RParen)?; - Ok(params) - } - /// Parses a method parameter list and records metadata plus promotion side effects. pub(super) fn parse_method_params( &mut self, diff --git a/crates/elephc-eval/src/parser/tests/namespaces.rs b/crates/elephc-eval/src/parser/tests/namespaces.rs index 89857209c0..955ae8354b 100644 --- a/crates/elephc-eval/src/parser/tests/namespaces.rs +++ b/crates/elephc-eval/src/parser/tests/namespaces.rs @@ -18,7 +18,13 @@ fn parse_fragment_accepts_function_declaration_source() { program.statements(), &[EvalStmt::FunctionDecl { name: "dyn".to_string(), + attributes: Vec::new(), params: vec!["x".to_string()], + parameter_attributes: vec![Vec::new()], + parameter_types: vec![None], + parameter_defaults: vec![None], + parameter_is_by_ref: vec![false], + parameter_is_variadic: vec![false], body: vec![EvalStmt::Return(Some(EvalExpr::Binary { op: EvalBinOp::Add, left: Box::new(EvalExpr::LoadVar("x".to_string())), @@ -41,7 +47,13 @@ return dyn();"#, &[ EvalStmt::FunctionDecl { name: "Eval\\Ns\\dyn".to_string(), + attributes: Vec::new(), params: Vec::new(), + parameter_attributes: Vec::new(), + parameter_types: Vec::new(), + parameter_defaults: Vec::new(), + parameter_is_by_ref: Vec::new(), + parameter_is_variadic: Vec::new(), body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( "Eval\\Ns".to_string() ))))], @@ -226,7 +238,13 @@ function dyn() { return alias(); }"#, program.statements(), &[EvalStmt::FunctionDecl { name: "Eval\\UseNs\\dyn".to_string(), + attributes: Vec::new(), params: Vec::new(), + parameter_attributes: Vec::new(), + parameter_types: Vec::new(), + parameter_defaults: Vec::new(), + parameter_is_by_ref: Vec::new(), + parameter_is_variadic: Vec::new(), body: vec![EvalStmt::Return(Some(EvalExpr::Call { name: "lib\\target".to_string(), args: Vec::new(), diff --git a/docs/php/eval.md b/docs/php/eval.md index ce0b4712bb..068782f546 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -271,17 +271,17 @@ reflected method is `__construct` or `__destruct`. `ReflectionFunction::getName()`, `ReflectionFunction::getParameters()`, `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report retained eval-declared function and -method metadata. Free eval functions expose the reflected function name, -parameter names, and positions from their currently retained function signature; -eval methods also expose declared-type presence, simple named type metadata through +method metadata. Eval-declared functions and methods expose declared-type +presence, simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, `allowsNull()`, and `isBuiltin()`, and multi-member union metadata through `ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter metadata is exposed through `ReflectionIntersectionType::getTypes()` and -`allowsNull()`. Eval method parameter attributes are exposed through -`ReflectionParameter::getAttributes()` using materialized `ReflectionAttribute` -objects. `ReflectionParameter::getDeclaringClass()` returns the declaring -class-like symbol for eval method parameters, and +`allowsNull()`. Function, method, and parameter attributes are exposed through +`getAttributes()` using materialized `ReflectionAttribute` objects. Parameter +default values, optionality, variadic flags, and by-reference flags are retained +for eval-declared functions and methods. `ReflectionParameter::getDeclaringClass()` +returns the declaring class-like symbol for eval method parameters, and `ReflectionParameter::getDeclaringFunction()` returns a `ReflectionFunction` object for eval free-function parameters or a `ReflectionMethod` object for the declaring eval method. Defaulted eval method parameters are bound diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5ac392e256..7bb6d98c92 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7652,6 +7652,81 @@ echo $declaring->getName();'); ); } +/// Verifies eval ReflectionFunction preserves rich eval-declared function signatures. +#[test] +fn test_eval_reflection_function_reports_signature_metadata() { + let out = compile_and_run_capture( + r#"label = $label; } + public function label() { return $this->label; } +} +#[EvalFuncAttr("free")] +function eval_reflect_rich(#[EvalFuncAttr("first")] string $name, int $count = 3, &...$items) { + return $count; +} +$ref = new ReflectionFunction("eval_reflect_rich"); +$attrs = $ref->getAttributes(); +$params = $ref->getParameters(); +echo count($attrs) . ":"; +echo $attrs[0]->getName() . ":"; +echo $attrs[0]->newInstance()->label() . ":"; +echo $ref->getNumberOfParameters() . ":"; +echo $ref->getNumberOfRequiredParameters() . ":"; +echo ($params[0]->hasType() ? "T" : "t") . ":"; +echo $params[0]->getType()->getName() . ":"; +$paramAttrs = $params[0]->getAttributes(); +echo count($paramAttrs) . ":"; +echo $paramAttrs[0]->newInstance()->label() . ":"; +echo ($params[1]->isOptional() ? "O" : "o") . ":"; +echo $params[1]->getDefaultValue() . ":"; +echo ($params[2]->isVariadic() ? "V" : "v") . ":"; +echo $params[2]->isPassedByReference() ? "R" : "r";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalFuncAttr:free:3:1:T:string:1:first:O:3:V:R" + ); +} + +/// Verifies eval-declared functions share method-style named/default/ref/variadic binding. +#[test] +fn test_eval_declared_function_rich_argument_binding() { + let out = compile_and_run_capture( + r#" "z", "name" => "cb"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "ok:2:1:6:cb:2:1:z"); +} + /// Verifies eval ReflectionClass::isCloneable uses eval class metadata through the bridge. #[test] fn test_eval_reflection_class_cloneable_predicate() { From 273bbf7299ef7e8aab23f3f0d98fef76dce4c833 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 02:12:08 +0200 Subject: [PATCH 0465/1208] Support eval ReflectionFunction invocation --- .../src/interpreter/dynamic_functions.rs | 21 ++++++- .../elephc-eval/src/interpreter/reflection.rs | 61 +++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 9 +++ .../tests/builtins_reflection_functions.rs | 29 +++++++++ docs/php/eval.md | 4 +- tests/codegen/eval.rs | 28 +++++++++ 6 files changed, 149 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index a2d6604b57..e7c240dcff 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -867,6 +867,23 @@ pub(super) fn eval_dynamic_function_with_evaluated_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_function_with_evaluated_args_and_ref_flags( + function, + function.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Evaluates an eval-declared function with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_flags( + function: &EvalFunction, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let static_names = static_var_names(function.body()); context.push_function(function.name()); @@ -874,7 +891,7 @@ pub(super) fn eval_dynamic_function_with_evaluated_args( function.params(), function.parameter_types(), function.parameter_defaults(), - function.parameter_is_by_ref(), + parameter_is_by_ref, function.parameter_is_variadic(), evaluated_args, context, @@ -890,7 +907,7 @@ pub(super) fn eval_dynamic_function_with_evaluated_args( bind_method_scope_args( &mut function_scope, function.params(), - function.parameter_is_by_ref(), + parameter_is_by_ref, &evaluated_args, ); let result = execute_statements(function.body(), context, &mut function_scope, values); diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 5363362033..226c02ed87 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -629,6 +629,37 @@ pub(in crate::interpreter) fn eval_reflection_method_invoke_result( .map(Some) } +/// Handles eval-backed `ReflectionFunction::invoke()` and `invokeArgs()` calls. +pub(in crate::interpreter) fn eval_reflection_function_invoke_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_invoke = method_name.eq_ignore_ascii_case("invoke"); + let is_invoke_args = method_name.eq_ignore_ascii_case("invokeArgs"); + if !is_invoke && !is_invoke_args { + return Ok(None); + } + let Some(function_name) = context + .eval_reflection_function_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let function_args = if is_invoke { + evaluated_args + .into_iter() + .map(eval_reflection_method_forwarded_value_arg) + .collect() + } else { + eval_reflection_function_invoke_args_array(evaluated_args, values)? + }; + eval_reflection_function_invoke_dispatch(&function_name, function_args, context, values) + .map(Some) +} + /// Handles PHP's no-op `ReflectionMethod/Property::setAccessible()` calls. pub(in crate::interpreter) fn eval_reflection_set_accessible_result( identity: u64, @@ -3339,6 +3370,36 @@ fn eval_reflection_method_invoke_args_array( Ok((args[0], method_args)) } +/// Binds `ReflectionFunction::invokeArgs()` and expands its PHP argument array. +fn eval_reflection_function_invoke_args_array( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; + eval_array_call_arg_values(args[0], values) +} + +/// Dispatches one reflected function invocation through eval or registered native functions. +fn eval_reflection_function_invoke_dispatch( + function_name: &str, + function_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let function_key = function_name.to_ascii_lowercase(); + if let Some(function) = context.function(&function_key).cloned() { + let by_value_parameters = vec![false; function.params().len()]; + return eval_dynamic_function_with_evaluated_args_and_ref_flags( + &function, + &by_value_parameters, + function_args, + context, + values, + ); + } + eval_callable_with_call_array_args(&function_key, function_args, context, values) +} + /// Dispatches one reflected method invocation through eval or public AOT bridges. fn eval_reflection_method_invoke_dispatch( declaring_class: &str, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 32967afd89..8296670451 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2882,6 +2882,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_function_invoke_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_method_invoke_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs index f15e06e17f..d64dd889bc 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs @@ -118,3 +118,32 @@ return true;"#, assert_eq!(values.output, "ok:2:1:6:cb:2:1:z"); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies ReflectionFunction invocation dispatches eval functions with forwarded arguments. +#[test] +fn execute_program_reflection_function_invokes_eval_function() { + let program = parse_fragment( + br#"function eval_reflect_invoke($left = "A", $right = "B", ...$rest) { + return $left . $right . count($rest) . $rest["extra"]; +} +function eval_reflect_no_writeback(&$value) { + $value = $value . "!"; + return $value; +} +$ref = new ReflectionFunction("eval_reflect_invoke"); +echo $ref->invoke(right: "2", left: "1", extra: "X"); echo ":"; +echo $ref->invokeArgs(["extra" => "Y", "left" => "3", "right" => "4"]); echo ":"; +$value = "Q"; +$mutate = new ReflectionFunction("eval_reflect_no_writeback"); +echo $mutate->invoke($value); echo ":"; echo $value; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "121X:341Y:Q!:Q"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/docs/php/eval.md b/docs/php/eval.md index 068782f546..3ee47a8dc5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -284,7 +284,9 @@ for eval-declared functions and methods. `ReflectionParameter::getDeclaringClass returns the declaring class-like symbol for eval method parameters, and `ReflectionParameter::getDeclaringFunction()` returns a `ReflectionFunction` object for eval free-function parameters or a `ReflectionMethod` object for the -declaring eval method. Defaulted eval method parameters are bound +declaring eval method. `ReflectionFunction::invoke()` and `invokeArgs()` +dispatch eval-declared functions with the same named/default/variadic argument +binding used by direct eval function calls. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and `getDefaultValue()`. Supported default expressions include diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7bb6d98c92..9f4fe4c13a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7727,6 +7727,34 @@ call_user_func_array("eval_signature_array", ["extra" => "z", "name" => "cb"]);' assert_eq!(out.stdout, "ok:2:1:6:cb:2:1:z"); } +/// Verifies eval ReflectionFunction::invoke and invokeArgs call eval-declared functions. +#[test] +fn test_eval_reflection_function_invoke_calls_eval_function() { + let out = compile_and_run_capture( + r#"invoke(right: "2", left: "1", extra: "X") . ":"; +echo $ref->invokeArgs(["extra" => "Y", "left" => "3", "right" => "4"]) . ":"; +$value = "Q"; +$mutate = new ReflectionFunction("eval_reflect_no_writeback"); +echo $mutate->invoke($value) . ":" . $value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "121X:341Y:Q!:Q"); +} + /// Verifies eval ReflectionClass::isCloneable uses eval class metadata through the bridge. #[test] fn test_eval_reflection_class_cloneable_predicate() { From 5dea8e84c653ee56e84383a0b9a58b23fd682374 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 02:22:54 +0200 Subject: [PATCH 0466/1208] Support eval reflection origin metadata APIs --- .../tests/builtins_class_metadata.rs | 40 +++++++ .../tests/builtins_reflection_functions.rs | 21 ++++ .../interpreter/tests/support/object_ops.rs | 4 + docs/php/eval.md | 8 +- src/types/checker/builtin_types/reflection.rs | 100 +++++++++++++++++- tests/codegen/eval.rs | 44 ++++++++ 6 files changed, 213 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 51b26752f5..ea3b800c6c 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -202,6 +202,46 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies reflection owner origin metadata APIs report eval user-defined defaults. +#[test] +fn execute_program_reflection_owners_report_origin_metadata_defaults() { + let program = parse_fragment( + br#"class EvalReflectOriginTarget { + public $id; + public const ANSWER = 42; + public function run() {} +} +enum EvalReflectOriginCase: string { + case Ready = "ready"; +} +$class = new ReflectionClass("EvalReflectOriginTarget"); +$method = new ReflectionMethod("EvalReflectOriginTarget", "run"); +$property = new ReflectionProperty("EvalReflectOriginTarget", "id"); +$constant = new ReflectionClassConstant("EvalReflectOriginTarget", "ANSWER"); +$unit = new ReflectionEnumUnitCase("EvalReflectOriginCase", "Ready"); +$backed = new ReflectionEnumBackedCase("EvalReflectOriginCase", "Ready"); +echo ($class->getDocComment() === false) ? "C" : "c"; echo ":"; +echo ($method->getDocComment() === false) ? "M" : "m"; echo ":"; +echo ($property->getDocComment() === false) ? "P" : "p"; echo ":"; +echo ($constant->getDocComment() === false) ? "K" : "k"; echo ":"; +echo ($unit->getDocComment() === false) ? "U" : "u"; echo ":"; +echo ($backed->getDocComment() === false) ? "B" : "b"; echo ":"; +echo ($class->getExtensionName() === false) ? "E" : "e"; echo ":"; +echo ($method->getExtensionName() === false) ? "N" : "n"; echo ":"; +echo ($class->getExtension() === null) ? "X" : "x"; echo ":"; +echo ($method->getExtension() === null) ? "Y" : "y"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "C:M:P:K:U:B:E:N:X:Y"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class namespace-derived name parts. #[test] fn execute_program_reflects_eval_class_name_parts() { diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs index d64dd889bc..c73e229301 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs @@ -87,6 +87,27 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionFunction origin metadata APIs report eval user-defined defaults. +#[test] +fn execute_program_reflection_function_reports_origin_metadata_defaults() { + let program = parse_fragment( + br#"function eval_reflect_origin_defaults() {} +$ref = new ReflectionFunction("eval_reflect_origin_defaults"); +echo ($ref->getDocComment() === false) ? "D" : "d"; echo ":"; +echo ($ref->getExtensionName() === false) ? "E" : "e"; echo ":"; +echo ($ref->getExtension() === null) ? "X" : "x"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "D:E:X"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval-declared functions bind named, default, by-reference, and variadic arguments. #[test] fn execute_program_calls_eval_function_with_rich_argument_binding() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index a340eae99a..5106f7974d 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -136,6 +136,10 @@ impl FakeOps { (FakeValue::Object(properties), "getname") if args.is_empty() => { Self::object_property(&properties, "__name").map_or_else(|| self.string(""), Ok) } + (FakeValue::Object(_), "getdoccomment" | "getextensionname") if args.is_empty() => { + self.bool_value(false) + } + (FakeValue::Object(_), "getextension") if args.is_empty() => self.null(), (FakeValue::Object(properties), "getshortname") if args.is_empty() => { Self::object_property(&properties, "__short_name") .map_or_else(|| self.string(""), Ok) diff --git a/docs/php/eval.md b/docs/php/eval.md index 3ee47a8dc5..51be03dc5f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -178,7 +178,13 @@ child methods and public access see the child property. expose eval-retained class, method, property, and method-parameter attributes for eval-declared class-like symbols when their arguments fit the same literal subset, and `getName()` returns the reflected class, member, or parameter name -for those owners. `ReflectionClass::getShortName()`, +for those owners. `ReflectionClass`, `ReflectionFunction`, `ReflectionMethod`, +`ReflectionProperty`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and +`ReflectionEnumBackedCase` expose `getDocComment()` and report `false` because +eval does not retain docblock text. `ReflectionClass`, `ReflectionFunction`, +and `ReflectionMethod` expose `getExtensionName()` and `getExtension()` and +report `false` / `null` for eval-declared user symbols. +`ReflectionClass::getShortName()`, `ReflectionClass::getNamespaceName()`, and `ReflectionClass::inNamespace()` derive namespace-aware parts from the resolved eval class-like name. `ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 975e5cbe9d..cbe80436a3 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -280,6 +280,11 @@ fn bool_type() -> TypeExpr { TypeExpr::Bool } +/// Returns a `TypeExpr` for Reflection APIs whose PHP return is `string|false`. +fn string_or_bool_type() -> TypeExpr { + TypeExpr::Union(vec![TypeExpr::Str, TypeExpr::Bool]) +} + /// Returns a private parameterless `__construct` method for `ReflectionAttribute`. fn builtin_reflection_attribute_constructor_method() -> ClassMethod { builtin_reflection_private_constructor_method() @@ -855,9 +860,7 @@ fn builtin_reflection_intersection_type() -> FlattenedClass { } } -/// Builds the `ReflectionClass` shell with a private resolved-name slot, -/// private attribute array slot, public constructor, `getName()`, and -/// `getAttributes()`. +/// Builds the `ReflectionClass` shell with retained eval metadata accessors. fn builtin_reflection_class() -> FlattenedClass { FlattenedClass { name: "ReflectionClass".to_string(), @@ -1068,6 +1071,9 @@ fn builtin_reflection_class() -> FlattenedClass { false, )]), builtin_reflection_class_string_method("getName", "__name"), + builtin_reflection_constant_false_union_method("getDocComment"), + builtin_reflection_constant_false_union_method("getExtensionName"), + builtin_reflection_constant_null_mixed_method("getExtension"), builtin_reflection_class_string_method("getShortName", "__short_name"), builtin_reflection_class_string_method("getNamespaceName", "__namespace_name"), builtin_reflection_class_bool_method("inNamespace", "__in_namespace"), @@ -2138,6 +2144,48 @@ fn builtin_reflection_constant_bool_method(method_name: &str, value: bool) -> Cl } } +/// Returns a public Reflection method that always reports PHP `false` as `string|false`. +fn builtin_reflection_constant_false_union_method(method_name: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(string_or_bool_type()), + body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public Reflection method that always reports PHP `null` as mixed. +fn builtin_reflection_constant_null_mixed_method(method_name: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![Stmt::new(StmtKind::Return(null_expr()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a `ReflectionMethod` predicate derived from its case-insensitive method name. fn builtin_reflection_method_name_predicate_method( method_name: &str, @@ -2228,6 +2276,19 @@ fn builtin_reflection_owner_class( )); methods.push(builtin_reflection_class_string_method("getName", "__name")); } + if reflection_owner_has_doc_comment_method(name) { + methods.push(builtin_reflection_constant_false_union_method( + "getDocComment", + )); + } + if reflection_owner_has_extension_methods(name) { + methods.push(builtin_reflection_constant_false_union_method( + "getExtensionName", + )); + methods.push(builtin_reflection_constant_null_mixed_method( + "getExtension", + )); + } add_reflection_member_flag_methods(name, &mut properties, &mut methods); if matches!( name, @@ -2294,6 +2355,24 @@ fn builtin_reflection_owner_class( } } +/// Returns true when PHP exposes `getDocComment()` on this synthetic reflection owner. +fn reflection_owner_has_doc_comment_method(class_name: &str) -> bool { + matches!( + class_name, + "ReflectionFunction" + | "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) +} + +/// Returns true when PHP exposes extension-origin APIs on this reflection owner. +fn reflection_owner_has_extension_methods(class_name: &str) -> bool { + matches!(class_name, "ReflectionFunction" | "ReflectionMethod") +} + /// Returns public class constants exposed by a synthetic reflection owner. fn reflection_owner_constants(class_name: &str) -> Vec { if class_name == "ReflectionMethod" { @@ -2691,6 +2770,21 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Str; } } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getDocComment")) + { + sig.return_type = PhpType::Union(vec![PhpType::Str, PhpType::Bool]); + } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getExtensionName")) + { + sig.return_type = PhpType::Union(vec![PhpType::Str, PhpType::Bool]); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getExtension")) { + sig.return_type = PhpType::Mixed; + } if class_name == "ReflectionClass" { for method_name in [ "isfinal", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9f4fe4c13a..3f6b5386a5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7696,6 +7696,50 @@ echo $params[2]->isPassedByReference() ? "R" : "r";'); ); } +/// Verifies eval Reflection origin metadata APIs are present on supported owners. +#[test] +fn test_eval_reflection_origin_metadata_defaults() { + let out = compile_and_run_capture( + r#"getDocComment() === false) ? "C" : "c"; echo ":"; +echo ($function->getDocComment() === false) ? "F" : "f"; echo ":"; +echo ($method->getDocComment() === false) ? "M" : "m"; echo ":"; +echo ($property->getDocComment() === false) ? "P" : "p"; echo ":"; +echo ($constant->getDocComment() === false) ? "K" : "k"; echo ":"; +echo ($unit->getDocComment() === false) ? "U" : "u"; echo ":"; +echo ($backed->getDocComment() === false) ? "B" : "b"; echo ":"; +echo ($class->getExtensionName() === false) ? "E" : "e"; echo ":"; +echo ($function->getExtensionName() === false) ? "N" : "n"; echo ":"; +echo ($method->getExtensionName() === false) ? "O" : "o"; echo ":"; +echo ($class->getExtension() === null) ? "X" : "x"; echo ":"; +echo ($function->getExtension() === null) ? "Y" : "y"; echo ":"; +echo ($method->getExtension() === null) ? "Z" : "z";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "C:F:M:P:K:U:B:E:N:O:X:Y:Z"); +} + /// Verifies eval-declared functions share method-style named/default/ref/variadic binding. #[test] fn test_eval_declared_function_rich_argument_binding() { From 2be22b16c65ac3ee6c819d20105d3bbae0c85b43 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 02:40:29 +0200 Subject: [PATCH 0467/1208] Support eval by-ref promoted properties --- crates/elephc-eval/src/context.rs | 70 ++++++++ crates/elephc-eval/src/eval_ir.rs | 5 + crates/elephc-eval/src/interpreter/control.rs | 28 +-- .../src/interpreter/dynamic_functions.rs | 19 ++- crates/elephc-eval/src/interpreter/mod.rs | 5 +- .../elephc-eval/src/interpreter/statements.rs | 161 +++++++++++++++++- .../src/interpreter/tests/classes.rs | 120 +++++++++++++ crates/elephc-eval/src/parser/statements.rs | 24 ++- .../src/parser/tests/classes_errors.rs | 48 +++++- crates/elephc-eval/src/scope.rs | 19 ++- docs/php/eval.md | 5 +- tests/codegen/eval.rs | 19 +++ 12 files changed, 465 insertions(+), 58 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 9ccb4b894c..bd471b4b0f 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -36,6 +36,28 @@ pub struct ElephcEvalExecutionScope { called_class_stack: Vec, } +/// Caller-side storage target that can remain linked to an eval object property. +#[derive(Clone)] +pub enum EvalReferenceTarget { + Variable { + scope: *mut ElephcEvalScope, + name: String, + }, + ArrayElement { + scope: *mut ElephcEvalScope, + array_name: String, + index: RuntimeCellHandle, + }, + ObjectProperty { + object: RuntimeCellHandle, + property: String, + access_scope: ElephcEvalExecutionScope, + }, + Cell { + cell: RuntimeCellHandle, + }, +} + /// Native AOT function callback metadata visible to runtime eval fragments. #[derive(Clone)] pub struct NativeFunction { @@ -160,6 +182,7 @@ pub struct ElephcEvalContext { class_constants: HashMap<(String, String), RuntimeCellHandle>, included_files: HashSet, dynamic_objects: HashMap, + dynamic_property_aliases: HashMap<(u64, String), EvalReferenceTarget>, eval_reflection_attributes: HashMap, eval_reflection_classes: HashMap, eval_reflection_functions: HashMap, @@ -207,6 +230,7 @@ impl ElephcEvalContext { class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), + dynamic_property_aliases: HashMap::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), eval_reflection_functions: HashMap::new(), @@ -255,6 +279,7 @@ impl ElephcEvalContext { class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), + dynamic_property_aliases: HashMap::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), eval_reflection_functions: HashMap::new(), @@ -543,6 +568,51 @@ impl ElephcEvalContext { .and_then(|class_key| self.classes.get(class_key)) } + /// Binds one eval object property slot to a persistent PHP reference target. + pub fn bind_dynamic_property_alias( + &mut self, + identity: u64, + storage_property_name: &str, + target: EvalReferenceTarget, + ) -> Option { + self.dynamic_property_aliases + .insert((identity, storage_property_name.to_string()), target) + } + + /// Returns the persistent reference target bound to one eval object property slot. + pub fn dynamic_property_alias( + &self, + identity: u64, + storage_property_name: &str, + ) -> Option<&EvalReferenceTarget> { + self.dynamic_property_aliases + .get(&(identity, storage_property_name.to_string())) + } + + /// Removes the persistent reference target for one eval object property slot. + pub fn remove_dynamic_property_alias( + &mut self, + identity: u64, + storage_property_name: &str, + ) -> Option { + self.dynamic_property_aliases + .remove(&(identity, storage_property_name.to_string())) + } + + /// Copies persistent property aliases from a source object identity to a clone identity. + pub fn clone_dynamic_property_aliases(&mut self, source_identity: u64, clone_identity: u64) { + let aliases = self + .dynamic_property_aliases + .iter() + .filter_map(|((identity, property), target)| { + (*identity == source_identity).then(|| (property.clone(), target.clone())) + }) + .collect::>(); + for (property, target) in aliases { + self.bind_dynamic_property_alias(clone_identity, &property, target); + } + } + /// Records eval-declared attribute metadata for one synthetic ReflectionAttribute object. pub fn register_eval_reflection_attribute(&mut self, identity: u64, attribute: EvalAttribute) { self.eval_reflection_attributes.insert(identity, attribute); diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index c366a6163b..9da8f07def 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -102,6 +102,11 @@ pub enum EvalStmt { target: String, source: String, }, + PropertyReferenceBind { + object: EvalExpr, + property: String, + source: String, + }, PropertySet { object: EvalExpr, property: String, diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-eval/src/interpreter/control.rs index 3764f55842..b14e1eedcd 100644 --- a/crates/elephc-eval/src/interpreter/control.rs +++ b/crates/elephc-eval/src/interpreter/control.rs @@ -8,8 +8,7 @@ //! Key details: //! - Runtime cells are opaque handles; these types do not own or release values by themselves. -use crate::context::ElephcEvalExecutionScope; -use crate::scope::ElephcEvalScope; +use crate::context::EvalReferenceTarget; use crate::value::RuntimeCellHandle; /// Internal statement-control result used to propagate eval returns and loops. @@ -32,34 +31,15 @@ pub enum EvalOutcome { pub(super) struct EvaluatedCallArg { pub(super) name: Option, pub(super) value: RuntimeCellHandle, - pub(super) ref_target: Option, -} - -/// Caller-side storage target for an argument that can satisfy a by-reference parameter. -#[derive(Clone)] -pub(super) enum EvaluatedCallRefTarget { - Variable { - scope: *mut ElephcEvalScope, - name: String, - }, - ArrayElement { - scope: *mut ElephcEvalScope, - array_name: String, - index: RuntimeCellHandle, - }, - ObjectProperty { - object: RuntimeCellHandle, - property: String, - access_scope: ElephcEvalExecutionScope, - }, + pub(super) ref_target: Option, } /// One method argument after PHP parameter-order binding and default materialization. #[derive(Clone)] pub(super) struct BoundMethodArg { pub(super) value: RuntimeCellHandle, - pub(super) ref_target: Option, - pub(super) variadic_ref_targets: Vec<(RuntimeCellHandle, EvaluatedCallRefTarget)>, + pub(super) ref_target: Option, + pub(super) variadic_ref_targets: Vec<(RuntimeCellHandle, EvalReferenceTarget)>, } /// One already evaluated PHP callback supported by the eval dispatcher. diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index e7c240dcff..a6d52f4a82 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -89,14 +89,14 @@ fn eval_call_arg_value( context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, Option), EvalStatus> { +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { match expr { EvalExpr::LoadVar(name) => { let value = visible_scope_cell(context, caller_scope, name) .map_or_else(|| values.null(), Ok)?; Ok(( value, - Some(EvaluatedCallRefTarget::Variable { + Some(EvalReferenceTarget::Variable { scope: caller_scope as *mut ElephcEvalScope, name: name.clone(), }), @@ -112,7 +112,7 @@ fn eval_call_arg_value( let value = values.array_get(array, index)?; Ok(( value, - Some(EvaluatedCallRefTarget::ArrayElement { + Some(EvalReferenceTarget::ArrayElement { scope: caller_scope as *mut ElephcEvalScope, array_name: array_name.clone(), index, @@ -126,7 +126,7 @@ fn eval_call_arg_value( validate_property_ref_target(object, property, context, values)?; Ok(( value, - Some(EvaluatedCallRefTarget::ObjectProperty { + Some(EvalReferenceTarget::ObjectProperty { object, property: property.clone(), access_scope, @@ -347,7 +347,7 @@ fn bind_dynamic_positional_method_arg( next_positional: &mut usize, next_variadic_index: &mut i64, value: RuntimeCellHandle, - ref_target: Option, + ref_target: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { @@ -398,7 +398,7 @@ fn bind_dynamic_named_method_arg( bound_args: &mut [Option], name: &str, value: RuntimeCellHandle, - ref_target: Option, + ref_target: Option, variadic_named_args: &mut std::collections::HashSet, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -435,8 +435,8 @@ fn bind_dynamic_named_method_arg( fn method_parameter_ref_target( parameter_is_by_ref: &[bool], param_index: Option, - ref_target: Option, -) -> Result, EvalStatus> { + ref_target: Option, +) -> Result, EvalStatus> { let Some(param_index) = param_index else { return Ok(None); }; @@ -484,7 +484,7 @@ fn bind_dynamic_variadic_arg( variadic_index: Option, key: RuntimeCellHandle, value: RuntimeCellHandle, - ref_target: Option, + ref_target: Option, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; @@ -1012,6 +1012,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::Expr(_) | EvalStmt::Global { .. } | EvalStmt::InterfaceDecl(_) + | EvalStmt::PropertyReferenceBind { .. } | EvalStmt::PropertySet { .. } | EvalStmt::ReferenceAssign { .. } | EvalStmt::Return(_) diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index f8fb766ed0..bf00c19743 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -29,7 +29,8 @@ mod scope_cells; mod statements; use crate::context::{ - ElephcEvalContext, ElephcEvalExecutionScope, NativeCallableSignature, NativeFunction, + ElephcEvalContext, ElephcEvalExecutionScope, EvalReferenceTarget, NativeCallableSignature, + NativeFunction, }; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ @@ -51,7 +52,7 @@ use constants::*; pub use control::EvalOutcome; use control::{ BoundMethodArg, EvalArraySpliceDirectArgs, EvalControl, EvalPredefinedConstant, - EvalSprintfSpec, EvaluatedCallArg, EvaluatedCallRefTarget, EvaluatedCallable, + EvalSprintfSpec, EvaluatedCallArg, EvaluatedCallable, }; use core_builtins::*; use debug_output::*; diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 8296670451..514ad59e42 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -189,6 +189,15 @@ pub(in crate::interpreter) fn execute_stmt( } Ok(EvalControl::None) } + EvalStmt::PropertyReferenceBind { + object, + property, + source, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_reference_bind_result(object, property, source, context, scope, values)?; + Ok(EvalControl::None) + } EvalStmt::StaticVar { name, init } => { execute_static_var_stmt(name, init, context, scope, values)?; Ok(EvalControl::None) @@ -1774,6 +1783,12 @@ pub(in crate::interpreter) fn eval_property_get_result( return Ok(result); } } + if let Some(target) = context + .dynamic_property_alias(identity, &storage_property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } values.property_get(object, &storage_property_name) } @@ -1861,9 +1876,127 @@ pub(in crate::interpreter) fn eval_property_set_result( { return Ok(()); } + if let Some(target) = context + .dynamic_property_alias(identity, &storage_property_name) + .cloned() + { + eval_reference_target_write( + identity, + &storage_property_name, + target, + value, + context, + values, + )?; + return values.property_set(object, &storage_property_name, value); + } + values.property_set(object, &storage_property_name, value) +} + +/// Binds one eval object property to a by-reference source parameter. +fn eval_property_reference_bind_result( + object: RuntimeCellHandle, + property_name: &str, + source_name: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let identity = values.object_identity(object)?; + let class = context + .dynamic_object_class(identity) + .ok_or(EvalStatus::RuntimeFatal)?; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + let (declaring_class, property) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + if property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); + let target = eval_property_reference_target(source_name, context, scope, values)?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_dynamic_property_alias(identity, &storage_property_name, target); values.property_set(object, &storage_property_name, value) } +/// Resolves a local by-reference source into a persistent property alias target. +fn eval_property_reference_target( + source_name: &str, + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(target) = scope.reference_target(source_name).cloned() { + return Ok(target); + } + let cell = visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; + Ok(EvalReferenceTarget::Cell { cell }) +} + +/// Reads the current value from a persistent reference target. +fn eval_reference_target_value( + target: &EvalReferenceTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match target { + EvalReferenceTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) + } + EvalReferenceTarget::ArrayElement { + scope, + array_name, + index, + } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = + visible_scope_cell(context, scope, array_name).map_or_else(|| values.null(), Ok)?; + values.array_get(array, *index) + } + EvalReferenceTarget::ObjectProperty { + object, + property, + access_scope, + } => { + let previous_scope = context.replace_execution_scope(access_scope.clone()); + let result = eval_property_get_result(*object, property, context, values); + context.replace_execution_scope(previous_scope); + result + } + EvalReferenceTarget::Cell { cell } => Ok(*cell), + } +} + +/// Writes a new value to a persistent reference target. +fn eval_reference_target_write( + object_identity: u64, + storage_property_name: &str, + target: EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if matches!(target, EvalReferenceTarget::Cell { .. }) { + context.bind_dynamic_property_alias( + object_identity, + storage_property_name, + EvalReferenceTarget::Cell { cell: value }, + ); + return Ok(()); + } + write_back_method_ref_target(&target, value, context, values) +} + /// Evaluates PHP `isset($object->property)` without forcing `__get()` first. pub(in crate::interpreter) fn eval_property_isset_result( object: RuntimeCellHandle, @@ -1928,6 +2061,7 @@ pub(in crate::interpreter) fn eval_property_unset_result( validate_eval_readonly_property_write(&declaring_class, &property, context)?; let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); + context.remove_dynamic_property_alias(identity, &storage_property_name); let null = values.null()?; return values.property_set(object, &storage_property_name, null); } @@ -2685,6 +2819,7 @@ pub(in crate::interpreter) fn eval_object_clone_result( if let Some(class_name) = dynamic_class_name { let clone_identity = values.object_identity(clone)?; context.register_dynamic_object(clone_identity, &class_name); + context.clone_dynamic_property_aliases(identity, clone_identity); if let Some((declaring_class, method)) = clone_method { eval_dynamic_method_with_values( &declaring_class, @@ -3338,6 +3473,9 @@ pub(in crate::interpreter) fn bind_method_scope_args( bound_arg.value, ScopeCellOwnership::Borrowed, ); + if let Some(target) = bound_arg.ref_target.clone() { + method_scope.set_reference_target(name.clone(), target); + } } else { method_scope.set(name.clone(), bound_arg.value, ScopeCellOwnership::Borrowed); } @@ -3379,30 +3517,34 @@ fn alias_duplicate_method_ref_args( } /// Returns true when two evaluated arguments target the same caller-side variable. -fn same_method_ref_target(left: &EvaluatedCallRefTarget, right: &EvaluatedCallRefTarget) -> bool { +fn same_method_ref_target(left: &EvalReferenceTarget, right: &EvalReferenceTarget) -> bool { match (left, right) { ( - EvaluatedCallRefTarget::Variable { + EvalReferenceTarget::Variable { scope: left_scope, name: left_name, }, - EvaluatedCallRefTarget::Variable { + EvalReferenceTarget::Variable { scope: right_scope, name: right_name, }, ) => left_scope == right_scope && left_name == right_name, ( - EvaluatedCallRefTarget::ArrayElement { + EvalReferenceTarget::ArrayElement { scope: left_scope, array_name: left_name, index: left_index, }, - EvaluatedCallRefTarget::ArrayElement { + EvalReferenceTarget::ArrayElement { scope: right_scope, array_name: right_name, index: right_index, }, ) => left_scope == right_scope && left_name == right_name && left_index == right_index, + ( + EvalReferenceTarget::Cell { cell: left_cell }, + EvalReferenceTarget::Cell { cell: right_cell }, + ) => left_cell == right_cell, _ => false, } } @@ -3462,13 +3604,13 @@ fn write_back_method_variadic_ref_args( /// Stores one by-reference method result in the original caller-side variable. fn write_back_method_ref_target( - target: &EvaluatedCallRefTarget, + target: &EvalReferenceTarget, value: RuntimeCellHandle, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { match target { - EvaluatedCallRefTarget::Variable { scope, name } => { + EvalReferenceTarget::Variable { scope, name } => { let Some(scope) = (unsafe { scope.as_mut() }) else { return Err(EvalStatus::RuntimeFatal); }; @@ -3483,7 +3625,7 @@ fn write_back_method_ref_target( } Ok(()) } - EvaluatedCallRefTarget::ArrayElement { + EvalReferenceTarget::ArrayElement { scope, array_name, index, @@ -3495,7 +3637,7 @@ fn write_back_method_ref_target( scope, array_name, *index, value, context, values, ) } - EvaluatedCallRefTarget::ObjectProperty { + EvalReferenceTarget::ObjectProperty { object, property, access_scope, @@ -3507,6 +3649,7 @@ fn write_back_method_ref_target( context, values, ), + EvalReferenceTarget::Cell { .. } => Ok(()), } } diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index de18362a7d..1e1d0208fc 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -34,6 +34,126 @@ return $user->label();"#, assert_eq!(values.get(result), FakeValue::String("7:Ada".to_string())); } +/// Verifies by-reference promoted properties stay aliased to caller variables. +#[test] +fn execute_program_aliases_by_reference_promoted_variable_properties() { + let program = parse_fragment( + br#"class EvalPromotedRefBox { + public function __construct(public &$value) {} +} +$value = 1; +$box = new EvalPromotedRefBox($value); +$box->value = 5; +echo $value; echo ":"; +$value = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted properties can alias caller array elements. +#[test] +fn execute_program_aliases_by_reference_promoted_array_element_properties() { + let program = parse_fragment( + br#"class EvalPromotedArrayRefBox { + public function __construct(public &$value) {} +} +$items = [1]; +$box = new EvalPromotedArrayRefBox($items[0]); +$box->value = 5; +echo $items[0]; echo ":"; +$items[0] = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted properties can alias caller object properties. +#[test] +fn execute_program_aliases_by_reference_promoted_object_property_properties() { + let program = parse_fragment( + br#"class EvalPromotedObjectRefHolder { + public $value = 1; +} +class EvalPromotedObjectRefBox { + public function __construct(public &$value) {} +} +$holder = new EvalPromotedObjectRefHolder(); +$box = new EvalPromotedObjectRefBox($holder->value); +$box->value = 5; +echo $holder->value; echo ":"; +$holder->value = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted defaults use internal property alias storage. +#[test] +fn execute_program_aliases_by_reference_promoted_default_properties() { + let program = parse_fragment( + br#"class EvalPromotedDefaultRefBox { + public function __construct(public &$value = null) {} +} +$box = new EvalPromotedDefaultRefBox(); +$box->value = 5; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies readonly by-reference promotion fails when the constructor creates the alias. +#[test] +fn execute_program_rejects_readonly_by_reference_promoted_properties() { + let program = parse_fragment( + br#"class EvalPromotedReadonlyRefBox { + public function __construct(public readonly int &$value) {} +} +$value = 1; +new EvalPromotedReadonlyRefBox($value);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly by-reference promoted property should fail at construction"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies promoted readonly properties keep the normal constructor-only write rule. #[test] fn execute_program_rejects_promoted_readonly_property_write_after_constructor() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 7470cc7c0e..2a71b057fe 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -1853,7 +1853,7 @@ impl Parser { let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; - if promotion.is_some() && (is_by_ref || is_variadic) { + if promotion.is_some() && is_variadic { return Err(EvalParseError::UnsupportedConstruct); } if let Some((visibility, is_readonly)) = promotion { @@ -1870,7 +1870,7 @@ impl Parser { .with_promoted() .with_attributes(attributes.clone()), ); - promoted_assignments.push(promoted_property_assignment(name)); + promoted_assignments.push(promoted_property_assignment(name, is_by_ref)); } params.push(name.clone()); parameter_attributes.push(attributes); @@ -2527,11 +2527,19 @@ fn property_hook_set_method(property_name: &str) -> String { format!("__propset_{property_name}") } -/// Builds the implicit `$this->name = $name` assignment for a promoted parameter. -fn promoted_property_assignment(name: &str) -> EvalStmt { - EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: name.to_string(), - value: EvalExpr::LoadVar(name.to_string()), +/// Builds the implicit constructor assignment or alias for a promoted parameter. +fn promoted_property_assignment(name: &str, is_by_ref: bool) -> EvalStmt { + if is_by_ref { + EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: name.to_string(), + source: name.to_string(), + } + } else { + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: name.to_string(), + value: EvalExpr::LoadVar(name.to_string()), + } } } diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 847b45b2ef..1d5d1a32a9 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -384,13 +384,57 @@ fn parse_fragment_accepts_constructor_promoted_properties() { ); } +/// Verifies by-reference promoted constructor parameters lower to property aliases. +#[test] +fn parse_fragment_accepts_by_reference_constructor_promoted_properties() { + let program = parse_fragment( + br#"class DynEvalPromotedRef { + public function __construct(public int &$id) {} +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalPromotedRef", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_promoted() + ], + vec![EvalClassMethod::new( + "__construct", + vec!["id".to_string()], + vec![EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: "id".to_string(), + source: "id".to_string(), + }], + ) + .with_parameter_types(vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))]) + .with_parameter_by_ref_flags(vec![true])] + ))] + ); +} + /// Verifies eval rejects promoted parameter forms that the eval runtime cannot model yet. #[test] fn parse_fragment_rejects_unsupported_constructor_promotion_forms() { parse_fragment(b"class DynEvalPromotedMethod { public function run(public int $id) {} }") .expect_err("promotion is only valid on constructors"); - parse_fragment(b"class DynEvalPromotedRef { public function __construct(public &$id) {} }") - .expect_err("promoted by-reference parameters need property alias storage"); parse_fragment( b"class DynEvalPromotedVariadic { public function __construct(public ...$ids) {} }", ) diff --git a/crates/elephc-eval/src/scope.rs b/crates/elephc-eval/src/scope.rs index 3527a89334..15f23177b7 100644 --- a/crates/elephc-eval/src/scope.rs +++ b/crates/elephc-eval/src/scope.rs @@ -13,6 +13,7 @@ use std::collections::HashMap; +use crate::context::EvalReferenceTarget; use crate::value::RuntimeCellHandle; /// Records whether a scope entry owns or borrows its runtime cell. @@ -141,6 +142,7 @@ impl ScopeEntry { pub struct ElephcEvalScope { entries: HashMap, global_aliases: HashMap, + reference_targets: HashMap, generation: u64, } @@ -150,6 +152,7 @@ impl ElephcEvalScope { Self { entries: HashMap::new(), global_aliases: HashMap::new(), + reference_targets: HashMap::new(), generation: 0, } } @@ -252,12 +255,24 @@ impl ElephcEvalScope { owned_cells_except([previous_source, previous_target], cell) } + /// Records the caller-side storage target for one by-reference local variable. + pub fn set_reference_target(&mut self, name: impl Into, target: EvalReferenceTarget) { + self.reference_targets.insert(name.into(), target); + } + + /// Returns the caller-side storage target associated with one by-reference local. + pub fn reference_target(&self, name: &str) -> Option<&EvalReferenceTarget> { + self.reference_targets.get(name) + } + /// Marks a named variable as unset while preserving the fact that eval touched it. pub fn unset(&mut self, name: impl Into) -> Option { self.bump_generation(); + let name = name.into(); + self.reference_targets.remove(&name); let previous = self .entries - .insert(name.into(), ScopeEntry::unset(self.generation)); + .insert(name, ScopeEntry::unset(self.generation)); owned_cell(previous) } @@ -293,6 +308,7 @@ impl ElephcEvalScope { break; } } + self.reference_targets.remove(&name); let previous = self .entries .insert(name, ScopeEntry::unset(self.generation)); @@ -367,6 +383,7 @@ impl ElephcEvalScope { /// Removes every entry and returns runtime cells owned by the scope. pub fn drain_owned_cells(&mut self) -> Vec { + self.reference_targets.clear(); self.entries .drain() .filter_map(|(_, entry)| owned_cell(Some(entry))) diff --git a/docs/php/eval.md b/docs/php/eval.md index 51be03dc5f..1fbcb029a8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, constructor property promotion for non-by-reference parameters, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -506,8 +506,7 @@ remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond parameter/property metadata, broader parameter default-value objects beyond the eval-supported -constant-expression subset, by-reference promoted constructor parameters, and -broader generated/AOT method bridge signatures beyond the current public +constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Eval object cloning currently covers eval-declared and `stdClass` objects; cloning emitted AOT objects through the eval bridge is still outside that bridge slice. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3f6b5386a5..81929afae4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8384,6 +8384,25 @@ return $box->x;'); assert_eq!(out, "DynEvalSupported:9:Y10:12:12"); } +/// Verifies eval-declared by-reference promoted properties remain aliased after construction. +#[test] +fn test_eval_declared_class_aliases_by_reference_promoted_property() { + let out = compile_and_run( + r#"value = 5; +echo $value . ":"; +$value = 7; +return $box->value;'); +"#, + ); + assert_eq!(out, "5:7"); +} + /// Verifies eval can construct an AOT class with no declared constructor. #[test] fn test_eval_dynamic_new_constructs_aot_class() { From 2a0dc1dd396a032546a261b35295cc030e4e0aa3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 02:56:16 +0200 Subject: [PATCH 0468/1208] Support eval ReflectionFunctionAbstract metadata --- .../elephc-eval/src/interpreter/reflection.rs | 185 ++++++++++++++++-- .../elephc-eval/src/interpreter/statements.rs | 9 + .../tests/builtins_class_metadata.rs | 35 ++++ .../tests/builtins_reflection_functions.rs | 37 ++++ docs/php/eval.md | 13 +- tests/codegen/eval.rs | 54 +++++ 6 files changed, 319 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 226c02ed87..9fb7338183 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -118,6 +118,12 @@ struct EvalReflectionNamedTypeMetadata { is_builtin: bool, } +/// Registered ReflectionFunctionAbstract target metadata for simple method dispatch. +enum EvalReflectionFunctionMethodTarget { + Function { name: String, is_variadic: bool }, + Method { name: String, is_variadic: bool }, +} + /// Eval metadata needed to materialize one `ReflectionUnionType` object. struct EvalReflectionUnionTypeMetadata { types: Vec, @@ -604,12 +610,9 @@ pub(in crate::interpreter) fn eval_reflection_method_invoke_result( if !is_invoke && !is_invoke_args { return Ok(None); } - let Some((declaring_class, reflected_method)) = - context - .eval_reflection_method(identity) - .map(|(declaring_class, method)| { - (declaring_class.to_string(), method.to_string()) - }) + let Some((declaring_class, reflected_method)) = context + .eval_reflection_method(identity) + .map(|(declaring_class, method)| (declaring_class.to_string(), method.to_string())) else { return Ok(None); }; @@ -660,6 +663,65 @@ pub(in crate::interpreter) fn eval_reflection_function_invoke_result( .map(Some) } +/// Handles eval-backed ReflectionFunctionAbstract name/origin metadata calls. +pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_method_target(identity, context) else { + return Ok(None); + }; + let method_key = method_name.to_ascii_lowercase(); + match method_key.as_str() { + "getshortname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_function_method_short_name(&target)) + .map(Some) + } + "getnamespacename" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_function_method_namespace_name(&target)) + .map(Some) + } + "innamespace" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(!eval_reflection_function_method_namespace_name(&target).is_empty()) + .map(Some) + } + "isinternal" | "isclosure" | "isdeprecated" | "returnsreference" | "hasreturntype" + | "isgenerator" | "hastentativereturntype" => { + eval_reflection_false_metadata_result(evaluated_args, values) + } + "isuserdefined" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(true).map(Some) + } + "isvariadic" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_variadic(&target)) + .map(Some) + } + "isdisabled" => match target { + EvalReflectionFunctionMethodTarget::Function { .. } => { + eval_reflection_false_metadata_result(evaluated_args, values) + } + EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), + }, + "getreturntype" | "gettentativereturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.null().map(Some) + } + _ => Ok(None), + } +} + /// Handles PHP's no-op `ReflectionMethod/Property::setAccessible()` calls. pub(in crate::interpreter) fn eval_reflection_set_accessible_result( identity: u64, @@ -3425,12 +3487,8 @@ fn eval_reflection_method_invoke_dispatch( values, ); } - let called_class = eval_reflection_method_instance_called_class( - declaring_class, - object, - context, - values, - )?; + let called_class = + eval_reflection_method_instance_called_class(declaring_class, object, context, values)?; return eval_dynamic_method_with_values_and_ref_flags( &method_class, &called_class, @@ -3767,7 +3825,10 @@ fn eval_reflection_function_parameters( declaring_class_name: None, attributes: function_attributes, flags: 0, - required_parameter_count: eval_reflection_required_parameter_count(defaults, variadic_flags), + required_parameter_count: eval_reflection_required_parameter_count( + defaults, + variadic_flags, + ), }; eval_reflection_parameters_from_names_and_type_flags( None, @@ -3912,6 +3973,104 @@ fn eval_reflection_builtin_named_type( } } +/// Returns function or method metadata registered for a synthetic reflection owner object. +fn eval_reflection_function_method_target( + identity: u64, + context: &ElephcEvalContext, +) -> Option { + if let Some(name) = context.eval_reflection_function_name(identity) { + let is_variadic = context + .function(&name.to_ascii_lowercase()) + .is_some_and(|function| function.parameter_is_variadic().iter().any(|flag| *flag)); + return Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + is_variadic, + }); + } + context + .eval_reflection_method(identity) + .map(|(declaring_class, method_name)| { + let is_variadic = eval_reflection_method_metadata(declaring_class, method_name, context) + .is_some_and(|method| { + method + .parameters + .iter() + .any(|parameter| parameter.is_variadic) + }); + EvalReflectionFunctionMethodTarget::Method { + name: method_name.to_string(), + is_variadic, + } + }) +} + +/// Validates that a synthetic reflection metadata call received no arguments. +fn eval_reflection_bind_no_args(evaluated_args: Vec) -> Result<(), EvalStatus> { + let _ = bind_evaluated_function_args(&[], evaluated_args)?; + Ok(()) +} + +/// Returns a no-argument reflection metadata predicate result that is always false. +fn eval_reflection_false_metadata_result( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(false).map(Some) +} + +/// Returns PHP's short name for a ReflectionFunction or ReflectionMethod target. +fn eval_reflection_function_method_short_name( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + eval_reflection_short_name(name) + } + EvalReflectionFunctionMethodTarget::Method { name, .. } => name.clone(), + } +} + +/// Returns PHP's namespace name for a ReflectionFunction or ReflectionMethod target. +fn eval_reflection_function_method_namespace_name( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + eval_reflection_namespace_name(name) + } + EvalReflectionFunctionMethodTarget::Method { .. } => String::new(), + } +} + +/// Returns whether the reflected function or method has a variadic parameter. +fn eval_reflection_function_method_is_variadic( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_variadic, .. } + | EvalReflectionFunctionMethodTarget::Method { is_variadic, .. } => *is_variadic, + } +} + +/// Returns the final namespace segment-free name component from a PHP symbol name. +fn eval_reflection_short_name(name: &str) -> String { + let name = name.trim_start_matches('\\'); + name.rsplit_once('\\').map_or_else( + || name.to_string(), + |(_, short_name)| short_name.to_string(), + ) +} + +/// Returns the namespace prefix from a PHP function name, or an empty string. +fn eval_reflection_namespace_name(name: &str) -> String { + name.trim_start_matches('\\') + .rsplit_once('\\') + .map_or_else(String::new, |(namespace_name, _)| { + namespace_name.to_string() + }) +} + /// Packs ReflectionMethod/ReflectionProperty predicate flags for the runtime owner factory. fn eval_reflection_member_flags( visibility: EvalVisibility, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 514ad59e42..71479a6c7d 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3035,6 +3035,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_function_method_metadata_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_set_accessible_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index ea3b800c6c..e146247a13 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -242,6 +242,41 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod exposes PHP-compatible name and origin predicate metadata. +#[test] +fn execute_program_reflection_method_reports_name_and_origin_predicates() { + let program = parse_fragment( + br#"namespace EvalReflectMethodNs; +class Target { + public function run(...$items) {} +} +$ref = new \ReflectionMethod(Target::class, "run"); +echo $ref->getShortName(); echo ":"; +echo $ref->getNamespaceName(); echo ":"; +echo $ref->inNamespace() ? "Y" : "N"; echo ":"; +echo $ref->isInternal() ? "I" : "i"; +echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->isClosure() ? "C" : "c"; echo ":"; +echo $ref->isDeprecated() ? "D" : "d"; echo ":"; +echo $ref->returnsReference() ? "R" : "r"; echo ":"; +echo $ref->hasReturnType() ? "T" : "t"; echo ":"; +echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; +echo $ref->isGenerator() ? "G" : "g"; echo ":"; +echo $ref->isVariadic() ? "V" : "v"; echo ":"; +echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; +echo $ref->getTentativeReturnType() === null ? "Q" : "q"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "run::N:iU:c:d:r:t:N:g:V:h:Q"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class namespace-derived name parts. #[test] fn execute_program_reflects_eval_class_name_parts() { diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs index c73e229301..6e7438d993 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs @@ -108,6 +108,43 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionFunction exposes PHP-compatible name and origin predicate metadata. +#[test] +fn execute_program_reflection_function_reports_name_and_origin_predicates() { + let program = parse_fragment( + br#"namespace EvalReflectFnNs; +function sample(...$items) {} +$ref = new \ReflectionFunction('EvalReflectFnNs\\sample'); +echo $ref->getShortName(); echo ":"; +echo $ref->getNamespaceName(); echo ":"; +echo $ref->inNamespace() ? "Y" : "N"; echo ":"; +echo $ref->isInternal() ? "I" : "i"; +echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->isClosure() ? "C" : "c"; echo ":"; +echo $ref->isDeprecated() ? "D" : "d"; echo ":"; +echo $ref->returnsReference() ? "R" : "r"; echo ":"; +echo $ref->hasReturnType() ? "T" : "t"; echo ":"; +echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; +echo $ref->isGenerator() ? "G" : "g"; echo ":"; +echo $ref->isVariadic() ? "V" : "v"; echo ":"; +echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; +echo $ref->getTentativeReturnType() === null ? "Q" : "q"; echo ":"; +echo $ref->isDisabled() ? "X" : "x"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "sample:EvalReflectFnNs:Y:iU:c:d:r:t:N:g:V:h:Q:x" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval-declared functions bind named, default, by-reference, and variadic arguments. #[test] fn execute_program_calls_eval_function_with_rich_argument_binding() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 1fbcb029a8..59c048e56f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -137,7 +137,8 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties -and methods, constructor property promotion for non-by-reference parameters, +and methods, constructor property promotion including by-reference promotion +for variable, array-element, object-property, and default-value targets, concrete property `get` / `set` hooks, interface property hook contract checks, abstract property hook contracts, property-level `readonly`, `readonly class`, `__construct()`, abstract classes and methods, final classes, methods, and @@ -187,6 +188,16 @@ report `false` / `null` for eval-declared user symbols. `ReflectionClass::getShortName()`, `ReflectionClass::getNamespaceName()`, and `ReflectionClass::inNamespace()` derive namespace-aware parts from the resolved eval class-like name. +`ReflectionFunction::getShortName()`, `getNamespaceName()`, and +`inNamespace()` derive namespace-aware parts from the reflected eval function +name. `ReflectionMethod::getShortName()` reports the reflected method name, +while `ReflectionMethod::getNamespaceName()` reports an empty string and +`inNamespace()` reports `false`, matching PHP's method reflection behavior. +`ReflectionFunction` and `ReflectionMethod` report eval user-symbol defaults +through `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, +`returnsReference()`, `hasReturnType()`, `getReturnType()`, `isGenerator()`, +`isVariadic()`, `hasTentativeReturnType()`, and `getTentativeReturnType()`. +`ReflectionFunction::isDisabled()` reports `false` for eval-visible functions. `ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval class-like metadata, including diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 81929afae4..a55c492645 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7740,6 +7740,60 @@ echo ($method->getExtension() === null) ? "Z" : "z";'); assert_eq!(out.stdout, "C:F:M:P:K:U:B:E:N:O:X:Y:Z"); } +/// Verifies eval ReflectionFunction/Method expose name and origin predicate metadata. +#[test] +fn test_eval_reflection_function_and_method_name_origin_predicates() { + let out = compile_and_run_capture( + r#"getShortName() . ":"; +echo $fn->getNamespaceName() . ":"; +echo ($fn->inNamespace() ? "Y" : "N") . ":"; +echo ($fn->isInternal() ? "I" : "i"); +echo ($fn->isUserDefined() ? "U" : "u") . ":"; +echo ($fn->isClosure() ? "C" : "c") . ":"; +echo ($fn->isDeprecated() ? "D" : "d") . ":"; +echo ($fn->returnsReference() ? "R" : "r") . ":"; +echo ($fn->hasReturnType() ? "T" : "t") . ":"; +echo ($fn->getReturnType() === null ? "N" : "n") . ":"; +echo ($fn->isGenerator() ? "G" : "g") . ":"; +echo ($fn->isVariadic() ? "V" : "v") . ":"; +echo ($fn->hasTentativeReturnType() ? "H" : "h") . ":"; +echo ($fn->getTentativeReturnType() === null ? "Q" : "q") . ":"; +echo ($fn->isDisabled() ? "X" : "x") . "|"; +echo $method->getShortName() . ":"; +echo $method->getNamespaceName() . ":"; +echo ($method->inNamespace() ? "Y" : "N") . ":"; +echo ($method->isInternal() ? "I" : "i"); +echo ($method->isUserDefined() ? "U" : "u") . ":"; +echo ($method->isClosure() ? "C" : "c") . ":"; +echo ($method->isDeprecated() ? "D" : "d") . ":"; +echo ($method->returnsReference() ? "R" : "r") . ":"; +echo ($method->hasReturnType() ? "T" : "t") . ":"; +echo ($method->getReturnType() === null ? "N" : "n") . ":"; +echo ($method->isGenerator() ? "G" : "g") . ":"; +echo ($method->isVariadic() ? "V" : "v") . ":"; +echo ($method->hasTentativeReturnType() ? "H" : "h") . ":"; +echo $method->getTentativeReturnType() === null ? "Q" : "q";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "sample:EvalReflectNameNs:Y:iU:c:d:r:t:N:g:V:h:Q:x|run::N:iU:c:d:r:t:N:g:V:h:Q" + ); +} + /// Verifies eval-declared functions share method-style named/default/ref/variadic binding. #[test] fn test_eval_declared_function_rich_argument_binding() { From 1f0646ff47b256e2014da6c57309c6101bc414f2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 03:02:39 +0200 Subject: [PATCH 0469/1208] Support eval ReflectionMethod prototypes --- .../elephc-eval/src/interpreter/reflection.rs | 136 ++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 9 ++ .../tests/builtins_class_metadata.rs | 60 ++++++++ docs/php/eval.md | 5 +- tests/codegen/eval.rs | 59 ++++++++ 5 files changed, 268 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 9fb7338183..4117d1d6f6 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -722,6 +722,59 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( } } +/// Handles eval-backed `ReflectionMethod::hasPrototype()` and `getPrototype()` calls. +pub(in crate::interpreter) fn eval_reflection_method_prototype_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_has_prototype = method_name.eq_ignore_ascii_case("hasPrototype"); + let is_get_prototype = method_name.eq_ignore_ascii_case("getPrototype"); + if !is_has_prototype && !is_get_prototype { + return Ok(None); + } + let Some((declaring_class, reflected_method)) = context + .eval_reflection_method(identity) + .map(|(declaring_class, method_name)| (declaring_class.to_string(), method_name.to_string())) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let Some((prototype_class, prototype_method)) = + eval_reflection_method_prototype_target(&declaring_class, &reflected_method, context) + else { + if is_has_prototype { + return values.bool_value(false).map(Some); + } + return eval_throw_reflection_exception( + &format!( + "Method {}::{} does not have a prototype", + declaring_class, reflected_method + ), + context, + values, + ); + }; + if is_has_prototype { + return values.bool_value(true).map(Some); + } + let Some(metadata) = + eval_reflection_method_metadata(&prototype_class, &prototype_method, context) + else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &prototype_method, + &metadata, + context, + values, + ) + .map(Some) +} + /// Handles PHP's no-op `ReflectionMethod/Property::setAccessible()` calls. pub(in crate::interpreter) fn eval_reflection_set_accessible_result( identity: u64, @@ -4071,6 +4124,89 @@ fn eval_reflection_namespace_name(name: &str) -> String { }) } +/// Finds the PHP ReflectionMethod prototype target for an eval-declared method. +fn eval_reflection_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, String)> { + if !(context.has_class(declaring_class) || context.has_enum(declaring_class)) { + return None; + } + eval_reflection_parent_method_prototype_target(declaring_class, method_name, context) + .or_else(|| { + eval_reflection_interface_method_prototype_target( + declaring_class, + method_name, + context, + ) + }) +} + +/// Finds the nearest parent-class method prototype for an eval-declared override. +fn eval_reflection_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, String)> { + for parent_class in context.class_parent_names(declaring_class) { + if let Some((prototype_class, prototype_method)) = + context.class_own_method(&parent_class, method_name) + { + return Some((prototype_class, prototype_method.name().to_string())); + } + } + None +} + +/// Finds the interface method prototype for an eval-declared class method. +fn eval_reflection_interface_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, String)> { + let mut seen = std::collections::HashSet::new(); + for interface_name in context.class_interface_names(declaring_class) { + if let Some(prototype) = eval_reflection_interface_declared_method_target( + &interface_name, + method_name, + context, + &mut seen, + ) { + return Some(prototype); + } + } + None +} + +/// Finds the interface that actually declares a method in an interface hierarchy. +fn eval_reflection_interface_declared_method_target( + interface_name: &str, + method_name: &str, + context: &ElephcEvalContext, + seen: &mut std::collections::HashSet, +) -> Option<(String, String)> { + let interface = context.interface(interface_name)?; + if !seen.insert(interface.name().to_ascii_lowercase()) { + return None; + } + if let Some(method) = interface + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + { + return Some((interface.name().to_string(), method.name().to_string())); + } + for parent in interface.parents() { + if let Some(prototype) = + eval_reflection_interface_declared_method_target(parent, method_name, context, seen) + { + return Some(prototype); + } + } + None +} + /// Packs ReflectionMethod/ReflectionProperty predicate flags for the runtime owner factory. fn eval_reflection_member_flags( visibility: EvalVisibility, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 71479a6c7d..15610f3d82 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3044,6 +3044,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_method_prototype_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_set_accessible_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index e146247a13..aa04e8934a 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -277,6 +277,66 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod exposes eval parent and interface prototypes. +#[test] +fn execute_program_reflection_method_reports_eval_prototypes() { + let program = parse_fragment( + br#"interface EvalProtoParentIface { + public function parented(); +} +interface EvalProtoChildIface extends EvalProtoParentIface {} +interface EvalProtoIface { + public function iface(); +} +class EvalProtoBase { + public function run() {} + public function inherited() {} +} +class EvalProtoChild extends EvalProtoBase implements EvalProtoIface, EvalProtoChildIface { + public function run() {} + public function iface() {} + public function parented() {} + public function own() {} +} +$override = new ReflectionMethod("EvalProtoChild", "run"); +$overrideProto = $override->getPrototype(); +echo $override->hasPrototype() ? "Y" : "N"; echo ":"; +echo $overrideProto->getDeclaringClass()->getName(); echo "::"; +echo $overrideProto->getName(); echo ":"; +$iface = new ReflectionMethod("EvalProtoChild", "iface"); +$ifaceProto = $iface->getPrototype(); +echo $iface->hasPrototype() ? "Y" : "N"; echo ":"; +echo $ifaceProto->getDeclaringClass()->getName(); echo "::"; +echo $ifaceProto->getName(); echo ":"; +$parentIface = new ReflectionMethod("EvalProtoChild", "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName(); echo "::"; +echo $parentIfaceProto->getName(); echo ":"; +$own = new ReflectionMethod("EvalProtoChild", "own"); +echo $own->hasPrototype() ? "Y" : "N"; echo ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod("EvalProtoChild", "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Y:EvalProtoBase::run:Y:EvalProtoIface::iface:EvalProtoParentIface::parented:N:E:N" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval class namespace-derived name parts. #[test] fn execute_program_reflects_eval_class_name_parts() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 59c048e56f..8b319c6e21 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -257,7 +257,10 @@ attribute, property type, or property default-value metadata. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected -member. `ReflectionClass::getConstructor()` returns a materialized +member. `ReflectionMethod::hasPrototype()` and `getPrototype()` expose +eval parent-class overrides and interface implementation prototypes; inherited +methods that are not overridden report no prototype, matching PHP reflection. +`ReflectionClass::getConstructor()` returns a materialized `ReflectionMethod` for direct, inherited, interface, and trait constructors, or `null` when no constructor is visible. `ReflectionClass::getParentClass()` returns a materialized `ReflectionClass` for eval-declared parent classes or diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a55c492645..dd8bb196de 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7794,6 +7794,65 @@ echo $method->getTentativeReturnType() === null ? "Q" : "q";'); ); } +/// Verifies eval ReflectionMethod hasPrototype/getPrototype follow PHP inheritance rules. +#[test] +fn test_eval_reflection_method_reports_eval_prototypes() { + let out = compile_and_run_capture( + r#"getPrototype(); +echo ($override->hasPrototype() ? "Y" : "N") . ":"; +echo $overrideProto->getDeclaringClass()->getName() . "::"; +echo $overrideProto->getName() . ":"; +$iface = new ReflectionMethod("EvalProtoChild", "iface"); +$ifaceProto = $iface->getPrototype(); +echo ($iface->hasPrototype() ? "Y" : "N") . ":"; +echo $ifaceProto->getDeclaringClass()->getName() . "::"; +echo $ifaceProto->getName() . ":"; +$parentIface = new ReflectionMethod("EvalProtoChild", "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName() . "::"; +echo $parentIfaceProto->getName() . ":"; +$own = new ReflectionMethod("EvalProtoChild", "own"); +echo ($own->hasPrototype() ? "Y" : "N") . ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod("EvalProtoChild", "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Y:EvalProtoBase::run:Y:EvalProtoIface::iface:EvalProtoParentIface::parented:N:E:N" + ); +} + /// Verifies eval-declared functions share method-style named/default/ref/variadic binding. #[test] fn test_eval_declared_function_rich_argument_binding() { From 6947f7d2e70af4e778dddaee0ab46cfc739dd53a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 03:31:02 +0200 Subject: [PATCH 0470/1208] Support eval OOP introspection builtins --- .../interpreter/builtins/class_metadata.rs | 4 + .../class_metadata/oop_introspection.rs | 544 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 4 + .../builtins/registry/dispatch/symbols.rs | 5 + .../interpreter/builtins/registry/names.rs | 4 + .../src/interpreter/expressions.rs | 5 + .../tests/builtins_class_metadata.rs | 68 +++ docs/php/eval.md | 8 +- tests/codegen/eval.rs | 67 +++ 9 files changed, 708 insertions(+), 1 deletion(-) create mode 100644 crates/elephc-eval/src/interpreter/builtins/class_metadata/oop_introspection.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 903832a03c..37d5910a43 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -14,6 +14,10 @@ use super::super::*; use super::*; +mod oop_introspection; + +pub(in crate::interpreter) use oop_introspection::*; + /// Evaluates `class_implements()`, `class_parents()`, or `class_uses()`. pub(in crate::interpreter) fn eval_builtin_class_relation( name: &str, diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata/oop_introspection.rs new file mode 100644 index 0000000000..00fb265e07 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -0,0 +1,544 @@ +//! Purpose: +//! Implements eval OOP introspection builtins for class/object members and +//! visible object variables. +//! +//! Called from: +//! - `crate::interpreter::builtins::class_metadata` re-exports. +//! +//! Key details: +//! - `method_exists()` distinguishes object targets from class-string targets +//! because PHP exposes inherited private methods only on object targets. +//! - `get_object_vars()` filters declared storage slots so inaccessible +//! protected/private eval properties do not leak as dynamic properties. + +use super::super::super::*; +use super::{eval_class_metadata_name, eval_class_relation_name_exists}; +use std::collections::HashSet; + +const EVAL_CLASS_METADATA_FLAG_PUBLIC: u64 = 2; + +/// Evaluates `method_exists()` or `property_exists()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_member_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target, member] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + let member = eval_expr(member, context, scope, values)?; + eval_member_exists_result(name, &[target, member], context, values) +} + +/// Evaluates materialized `method_exists()` or `property_exists()` arguments. +pub(in crate::interpreter) fn eval_member_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target, member] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let member = eval_class_metadata_name(*member, values)?; + let exists = match name { + "method_exists" => eval_method_exists_target(*target, &member, context, values)?, + "property_exists" => eval_property_exists_target(*target, &member, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Evaluates `get_class_methods()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_class_methods( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + eval_get_class_methods_result(&[target], context, values) +} + +/// Evaluates materialized `get_class_methods()` arguments. +pub(in crate::interpreter) fn eval_get_class_methods_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let (class_name, target_is_object) = eval_class_metadata_target_name(*target, context, values)?; + if !target_is_object && !eval_class_relation_name_exists(&class_name, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let names = eval_class_method_names_for_scope(&class_name, context, values)?; + eval_indexed_string_array_result(&names, values) +} + +/// Evaluates `get_object_vars()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_object_vars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_get_object_vars_result(&[object], context, values) +} + +/// Evaluates materialized `get_object_vars()` arguments. +pub(in crate::interpreter) fn eval_get_object_vars_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + if values.type_tag(*object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let Ok(identity) = values.object_identity(*object) else { + return eval_public_object_vars_result(*object, values); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return eval_public_object_vars_result(*object, values); + }; + let class_name = class.name().to_string(); + eval_dynamic_object_vars_result(*object, &class_name, context, values) +} + +/// Resolves a `method_exists()` target and applies PHP object-vs-string lookup rules. +fn eval_method_exists_target( + target: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => { + let class_name = eval_object_class_metadata_name(target, context, values)?; + eval_method_exists_on_class(&class_name, method_name, true, context, values) + } + EVAL_TAG_STRING => { + let class_name = eval_resolved_class_metadata_name(target, context, values)?; + eval_method_exists_on_class(&class_name, method_name, false, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves a `property_exists()` target and applies declared and dynamic-property lookup rules. +fn eval_property_exists_target( + target: RuntimeCellHandle, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => { + let class_name = eval_object_class_metadata_name(target, context, values)?; + if eval_property_exists_on_class(&class_name, property_name, context, values)? { + return Ok(true); + } + eval_object_public_property_exists(target, property_name, values) + } + EVAL_TAG_STRING => { + let class_name = eval_resolved_class_metadata_name(target, context, values)?; + eval_property_exists_on_class(&class_name, property_name, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Checks method metadata for one resolved class-like name. +fn eval_method_exists_on_class( + class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(class_name) || context.has_enum(class_name) { + if target_is_object { + return Ok(context + .class_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name))); + } + if context + .class_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name)) + { + let Some((declaring_class, method)) = context.class_method(class_name, method_name) + else { + return Ok(true); + }; + return Ok(method.visibility() != EvalVisibility::Private + || declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(class_name.trim_start_matches('\\'))); + } + return Ok(false); + } + if context.has_interface(class_name) { + return Ok(context + .interface_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name))); + } + if context.has_trait(class_name) { + return Ok(context + .trait_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name))); + } + values + .reflection_method_flags(class_name, method_name) + .map(|flags| flags.is_some()) +} + +/// Checks property metadata for one resolved class-like name. +fn eval_property_exists_on_class( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(class_name) || context.has_enum(class_name) { + return Ok(context + .class_property_names(class_name) + .iter() + .any(|name| name == property_name)); + } + if context.has_interface(class_name) { + return Ok(context + .interface_property_names(class_name) + .iter() + .any(|name| name == property_name)); + } + if context.has_trait(class_name) { + return Ok(context + .trait_property_names(class_name) + .iter() + .any(|name| name == property_name)); + } + values + .reflection_property_flags(class_name, property_name) + .map(|flags| flags.is_some()) +} + +/// Resolves an object-or-class argument to a PHP class name and records whether it was an object. +fn eval_class_metadata_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(String, bool), EvalStatus> { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => Ok(( + eval_object_class_metadata_name(target, context, values)?, + true, + )), + EVAL_TAG_STRING => Ok(( + eval_resolved_class_metadata_name(target, context, values)?, + false, + )), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves an object cell to its eval or runtime class name. +fn eval_object_class_metadata_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().trim_start_matches('\\').to_string()); + } + let class_name = values.object_class_name(object)?; + let class_name_bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(class_name_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Reads a class-name cell and applies eval alias resolution. +fn eval_resolved_class_metadata_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_class_metadata_name(name, values)?; + Ok(context.resolve_class_name(&name).unwrap_or(name)) +} + +/// Collects PHP-visible methods for `get_class_methods()` in the current eval scope. +fn eval_class_method_names_for_scope( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(class_name) || context.has_enum(class_name) { + let mut names = Vec::new(); + for name in context.class_method_names(class_name) { + let Some((declaring_class, method)) = context.class_method(class_name, &name) else { + names.push(name); + continue; + }; + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() { + names.push(name); + } + } + return Ok(names); + } + if context.has_interface(class_name) { + return Ok(context.interface_method_names(class_name)); + } + if let Some(trait_decl) = context.trait_decl(class_name) { + return Ok(trait_decl + .methods() + .iter() + .filter(|method| method.visibility() == EvalVisibility::Public) + .map(|method| method.name().to_string()) + .collect()); + } + let method_names = values.reflection_method_names(class_name)?; + let names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + eval_public_runtime_method_names(class_name, names, values) +} + +/// Filters generated runtime methods to the public surface visible to eval. +fn eval_public_runtime_method_names( + class_name: &str, + names: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut result = Vec::new(); + for name in names { + if values + .reflection_method_flags(class_name, &name)? + .is_some_and(|flags| flags & EVAL_CLASS_METADATA_FLAG_PUBLIC != 0) + { + result.push(name); + } + } + Ok(result) +} + +/// Builds `get_object_vars()` for an eval-declared object. +fn eval_dynamic_object_vars_result( + object: RuntimeCellHandle, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(property_count)?; + let mut emitted_keys = HashSet::new(); + let storage_keys = eval_declared_object_storage_names(class_name, context); + result = eval_add_enum_object_vars( + result, + object, + class_name, + &mut emitted_keys, + context, + values, + )?; + result = eval_add_declared_object_vars( + result, + object, + class_name, + &mut emitted_keys, + context, + values, + )?; + eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &storage_keys, values) +} + +/// Adds synthetic enum properties exposed by PHP enum case objects. +fn eval_add_enum_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(enum_decl) = context.enum_decl(class_name) else { + return Ok(result); + }; + let is_backed = enum_decl.backing_type().is_some(); + result = eval_add_object_var(result, object, "name", emitted_keys, context, values)?; + if is_backed { + result = eval_add_object_var(result, object, "value", emitted_keys, context, values)?; + } + Ok(result) +} + +/// Adds declared instance properties visible from the current eval scope. +fn eval_add_declared_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + for class in context.class_chain(class_name) { + for property in class.properties() { + if property.is_static() + || validate_eval_member_access(class.name(), property.visibility(), context) + .is_err() + || emitted_keys.contains(property.name()) + { + continue; + } + result = eval_add_object_var( + result, + object, + property.name(), + emitted_keys, + context, + values, + )?; + } + } + Ok(result) +} + +/// Adds one visible object variable to an associative result array. +fn eval_add_object_var( + result: RuntimeCellHandle, + object: RuntimeCellHandle, + property_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + emitted_keys.insert(property_name.to_string()); + let key = values.string(property_name)?; + let value = eval_property_get_result(object, property_name, context, values)?; + values.array_set(result, key, value) +} + +/// Adds public dynamic properties that are not declared storage slots. +fn eval_add_dynamic_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + emitted_keys: &mut HashSet, + storage_keys: &HashSet, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + if key_name.contains('\0') + || storage_keys.contains(&key_name) + || !emitted_keys.insert(key_name.clone()) + { + continue; + } + let key = values.string(&key_name)?; + let value = values.property_get(object, &key_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns physical storage names used by declared eval object properties. +fn eval_declared_object_storage_names( + class_name: &str, + context: &ElephcEvalContext, +) -> HashSet { + let mut names = HashSet::new(); + for class in context.class_chain(class_name) { + for property in class.properties() { + names.insert(eval_instance_property_storage_name(class.name(), property)); + } + } + names +} + +/// Builds `get_object_vars()` for runtime objects with public bridge-visible properties. +fn eval_public_object_vars_result( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(property_count)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.string(&key_name)?; + let value = values.property_get(object, &key_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns whether an object has a public bridge-visible property by exact name. +fn eval_object_public_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} + +/// Builds an indexed PHP array from owned Rust strings. +fn eval_indexed_string_array_result( + names: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + let key = values.int(index as i64)?; + let value = values.string(name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Copies a runtime string array into Rust-owned strings for class metadata helpers. +fn eval_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_class_metadata_name(value, values)?); + } + Ok(result) +} diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs index 27bdd8f20a..219ce74d0d 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs @@ -138,6 +138,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), "settype" => Some(&["var", "type"]), "get_class" => Some(&["object"]), + "get_class_methods" => Some(&["object_or_class"]), + "get_object_vars" => Some(&["object"]), "get_parent_class" => Some(&["object_or_class"]), "call_user_func" => Some(&["callback"]), "call_user_func_array" => Some(&["callback", "args"]), @@ -148,6 +150,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "class_implements" | "class_parents" | "class_uses" => { Some(&["object_or_class", "autoload"]) } + "method_exists" => Some(&["object_or_class", "method"]), + "property_exists" => Some(&["object_or_class", "property"]), "enum_exists" => Some(&["enum", "autoload"]), "interface_exists" => Some(&["interface", "autoload"]), "trait_exists" => Some(&["trait", "autoload"]), diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs index d92caf83ce..8ea0f7d57b 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -58,6 +58,11 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( "class_implements" | "class_parents" | "class_uses" => { eval_class_relation_result(name, evaluated_args, context, values)? } + "method_exists" | "property_exists" => { + eval_member_exists_result(name, evaluated_args, context, values)? + } + "get_class_methods" => eval_get_class_methods_result(evaluated_args, context, values)?, + "get_object_vars" => eval_get_object_vars_result(evaluated_args, context, values)?, "enum_exists" | "trait_exists" => { eval_class_like_exists_result(name, evaluated_args, context, values)? } diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs index a76d6235d9..7ea77ea865 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/names.rs @@ -71,6 +71,8 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "class_implements" | "class_parents" | "class_uses" + | "method_exists" + | "property_exists" | "enum_exists" | "interface_exists" | "is_a" @@ -150,9 +152,11 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "getservbyname" | "getservbyport" | "get_class" + | "get_class_methods" | "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" + | "get_object_vars" | "get_parent_class" | "get_resource_id" | "get_resource_type" diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 8fb414786d..18e2f91f2e 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -598,6 +598,11 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "class_implements" | "class_parents" | "class_uses" => { eval_builtin_class_relation(name, args, context, scope, values) } + "method_exists" | "property_exists" => { + eval_builtin_member_exists(name, args, context, scope, values) + } + "get_class_methods" => eval_builtin_get_class_methods(args, context, scope, values), + "get_object_vars" => eval_builtin_get_object_vars(args, context, scope, values), "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), "trait_exists" | "enum_exists" => { eval_builtin_class_like_exists(name, args, context, scope, values) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index aa04e8934a..42bf847ce1 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -74,6 +74,74 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies PHP OOP introspection builtins follow eval visibility and scope rules. +#[test] +fn execute_program_dispatches_oop_introspection_builtins() { + let program = parse_fragment( + br#"class EvalOopIntrospectBase { + private $baseSecret = "bp"; + protected $baseProtected = "bq"; + public $basePublic = "br"; + private function basePrivate() {} + protected function baseProtectedMethod() {} + public function basePublicMethod() {} + public function parentView() { + $vars = get_object_vars($this); + ksort($vars); + echo implode(",", array_keys($vars)); + } +} +class EvalOopIntrospectChild extends EvalOopIntrospectBase { + private $childSecret = "cp"; + protected $childProtected = "cq"; + public $childPublic = "cr"; + private function childPrivate() {} + protected function childProtectedMethod() {} + public function childPublicMethod() {} + public function childView() { + $methods = get_class_methods($this); + sort($methods); + echo implode(",", $methods); echo "|"; + $vars = get_object_vars($this); + ksort($vars); + echo implode(",", array_keys($vars)); + } +} +$object = new EvalOopIntrospectChild(); +$object->dynamic = "dyn"; +echo method_exists("EvalOopIntrospectChild", "basePrivate") ? "bad" : "noParentPrivateMethod"; echo ":"; +echo method_exists($object, "basePrivate") ? "objectParentPrivateMethod" : "bad"; echo ":"; +echo method_exists("EvalOopIntrospectChild", "baseProtectedMethod") ? "classProtectedMethod" : "bad"; echo ":"; +echo property_exists("EvalOopIntrospectChild", "baseSecret") ? "bad" : "noParentPrivateProperty"; echo ":"; +echo property_exists($object, "baseSecret") ? "bad" : "noObjectParentPrivateProperty"; echo ":"; +echo property_exists($object, "dynamic") ? "dynamicProperty" : "bad"; echo ":"; +$methods = get_class_methods("EvalOopIntrospectChild"); +sort($methods); +echo implode(",", $methods); echo ":"; +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars)); echo ":"; +$object->childView(); echo ":"; +$object->parentView(); echo ":"; +echo call_user_func("method_exists", $object, "childPrivate") ? "callMethod" : "bad"; echo ":"; +echo call_user_func_array("property_exists", ["property" => "dynamic", "object_or_class" => $object]) ? "namedProperty" : "bad"; echo ":"; +echo function_exists("method_exists"); echo function_exists("property_exists"); +echo function_exists("get_class_methods"); echo function_exists("get_object_vars"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basepublicmethod,childpublicmethod,childview,parentview:basePublic,childPublic,dynamic:baseprotectedmethod,basepublicmethod,childprivate,childprotectedmethod,childpublicmethod,childview,parentview|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies class attribute helpers expose eval class-level metadata. #[test] fn execute_program_dispatches_class_attribute_metadata_builtins() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 8b319c6e21..14170bedb5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -381,6 +381,12 @@ AOT metadata. Eval-declared classes, interfaces, traits, and class aliases are visible through the corresponding eval and post-barrier native metadata probes. Eval-declared enums are visible inside eval through `enum_exists()` and through class-like probes such as `class_exists()`. +`method_exists()` and `property_exists()` inspect eval-declared class/interface/ +trait/enum metadata and generated runtime metadata. Object targets also see +dynamic public properties. `get_class_methods()` and `get_object_vars()` follow +PHP visibility from the current eval class scope for eval-declared objects; +generated/AOT objects expose the public bridge-visible metadata and object +properties available through the runtime hook slice. Eval-declared enums share the dynamic class-like metadata path used by eval-declared classes. Pure and backed enum cases are singleton objects, @@ -443,7 +449,7 @@ where listed below unless a note says otherwise. | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | | Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | -| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()` | | Debug output | `print_r()`, `var_dump()` | | Constants | `define()`, `defined()` | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index dd8bb196de..375b4382f2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5103,6 +5103,73 @@ echo $box->readProtected(2);'); assert_eq!(out.stdout, "7:7"); } +/// Verifies eval OOP introspection builtins preserve PHP visibility and scope rules. +#[test] +fn test_eval_declared_oop_introspection_builtins() { + let out = compile_and_run_capture( + r#"dynamic = "dyn"; +echo method_exists("EvalOopIntrospectChild", "basePrivate") ? "bad" : "noParentPrivateMethod"; echo ":"; +echo method_exists($object, "basePrivate") ? "objectParentPrivateMethod" : "bad"; echo ":"; +echo method_exists("EvalOopIntrospectChild", "baseProtectedMethod") ? "classProtectedMethod" : "bad"; echo ":"; +echo property_exists("EvalOopIntrospectChild", "baseSecret") ? "bad" : "noParentPrivateProperty"; echo ":"; +echo property_exists($object, "baseSecret") ? "bad" : "noObjectParentPrivateProperty"; echo ":"; +echo property_exists($object, "dynamic") ? "dynamicProperty" : "bad"; echo ":"; +$methods = get_class_methods("EvalOopIntrospectChild"); +sort($methods); +echo implode(",", $methods); echo ":"; +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars)); echo ":"; +$object->childView(); echo ":"; +$object->parentView(); echo ":"; +echo call_user_func("method_exists", $object, "childPrivate") ? "callMethod" : "bad"; echo ":"; +echo call_user_func_array("property_exists", ["property" => "dynamic", "object_or_class" => $object]) ? "namedProperty" : "bad"; echo ":"; +echo function_exists("method_exists"); echo function_exists("property_exists"); +echo function_exists("get_class_methods"); echo function_exists("get_object_vars");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basepublicmethod,childpublicmethod,childview,parentview:basePublic,childPublic,dynamic:baseprotectedmethod,basepublicmethod,childprivate,childprotectedmethod,childpublicmethod,childview,parentview|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" + ); +} + /// Verifies eval-declared private parent properties keep separate storage when a child shadows them. #[test] fn test_eval_declared_private_parent_property_shadowing() { From cf7894c8676b891e8d398a4253a700e00094fdb5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 03:36:15 +0200 Subject: [PATCH 0471/1208] Preserve eval method name casing --- crates/elephc-eval/src/context.rs | 4 ++-- .../src/interpreter/tests/builtins_class_metadata.rs | 2 +- tests/codegen/eval.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index bd471b4b0f..fea8fa28d2 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -1699,8 +1699,8 @@ fn same_class_name(left: &str, right: &str) -> bool { /// Pushes a case-insensitive PHP method name once for ReflectionClass metadata. fn push_unique_method_name(name: &str, names: &mut Vec, seen: &mut HashSet) { let key = normalize_method_name(name); - if seen.insert(key.clone()) { - names.push(key); + if seen.insert(key) { + names.push(name.trim_start_matches('\\').to_string()); } } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 42bf847ce1..5aace9f09f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -137,7 +137,7 @@ return true;"#, assert_eq!( values.output, - "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basepublicmethod,childpublicmethod,childview,parentview:basePublic,childPublic,dynamic:baseprotectedmethod,basepublicmethod,childprivate,childprotectedmethod,childpublicmethod,childview,parentview|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" + "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basePublicMethod,childPublicMethod,childView,parentView:basePublic,childPublic,dynamic:baseProtectedMethod,basePublicMethod,childPrivate,childProtectedMethod,childPublicMethod,childView,parentView|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 375b4382f2..ad80a4c577 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5166,7 +5166,7 @@ echo function_exists("get_class_methods"); echo function_exists("get_object_vars ); assert_eq!( out.stdout, - "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basepublicmethod,childpublicmethod,childview,parentview:basePublic,childPublic,dynamic:baseprotectedmethod,basepublicmethod,childprivate,childprotectedmethod,childpublicmethod,childview,parentview|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" + "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basePublicMethod,childPublicMethod,childView,parentView:basePublic,childPublic,dynamic:baseProtectedMethod,basePublicMethod,childPrivate,childProtectedMethod,childPublicMethod,childView,parentView|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" ); } From a601de17115bb96557df179417aedea8facd8119 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 03:41:22 +0200 Subject: [PATCH 0472/1208] Preserve eval ReflectionMethod name casing --- .../elephc-eval/src/interpreter/reflection.rs | 9 +++++- .../tests/builtins_class_metadata.rs | 30 +++++++++++++++++++ tests/codegen/eval.rs | 29 ++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 4117d1d6f6..6a98175aea 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1414,7 +1414,14 @@ fn eval_reflection_method_new( } return Ok(None); } - let method_name = eval_reflection_string_arg(args[1], values)?; + let requested_method_name = eval_reflection_string_arg(args[1], values)?; + let method_name = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &class_name, + &requested_method_name, + context, + ) + .ok_or(EvalStatus::RuntimeFatal)?; let method = eval_reflection_method_metadata(&class_name, &method_name, context) .ok_or(EvalStatus::RuntimeFatal)?; eval_reflection_member_object_result( diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 5aace9f09f..0647dcf77d 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1319,6 +1319,36 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod preserves declared method case after case-insensitive lookup. +#[test] +fn execute_program_reflection_method_preserves_declared_name_case() { + let program = parse_fragment( + br#"class EvalReflectMethodCaseBase { + public function MiXeDCase() { return "base"; } +} +class EvalReflectMethodCaseChild extends EvalReflectMethodCaseBase { + public function childCase() { return "child"; } +} +$object = new EvalReflectMethodCaseChild(); +$direct = new ReflectionMethod("EvalReflectMethodCaseChild", "mixedcase"); +echo $direct->getName(); echo ":"; +echo $direct->getShortName(); echo ":"; +echo $direct->invoke($object); echo ":"; +$listed = (new ReflectionClass("EvalReflectMethodCaseChild"))->getMethod("CHILDCASE"); +echo $listed->getName(); echo ":"; +echo $listed->invoke($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "MiXeDCase:MiXeDCase:base:childCase:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval member and enum-case reflectors expose their declaring class. #[test] fn execute_program_reflects_eval_declaring_class_metadata() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ad80a4c577..7a8525e9e8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7168,6 +7168,35 @@ echo $listed->isDestructor() ? "D" : "d";'); assert_eq!(out.stdout, "Cd:cD:cd:Cd"); } +/// Verifies eval ReflectionMethod keeps declared name case after case-insensitive lookup. +#[test] +fn test_eval_reflection_method_preserves_declared_name_case() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $direct->getShortName() . ":"; +echo $direct->invoke($object) . ":"; +$listed = (new ReflectionClass("EvalReflectMethodCaseChild"))->getMethod("CHILDCASE"); +echo $listed->getName() . ":"; +echo $listed->invoke($object);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "MiXeDCase:MiXeDCase:base:childCase:child"); +} + /// Verifies eval-declared final properties cannot be redeclared by subclasses. #[test] fn test_eval_declared_final_property_override_fails() { From 19bdc2bc67d38cfc1f48fa8f3262b92fddb64a93 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 03:55:18 +0200 Subject: [PATCH 0473/1208] Support eval class-like aliases --- crates/elephc-eval/src/context.rs | 183 +++++++++++++++--- .../src/interpreter/builtins/symbols.rs | 57 +++++- .../elephc-eval/src/interpreter/reflection.rs | 10 +- .../src/interpreter/tests/builtins_symbols.rs | 28 ++- docs/php/eval.md | 13 +- tests/codegen/eval.rs | 36 ++++ 6 files changed, 285 insertions(+), 42 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index fea8fa28d2..472dabf07e 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -153,6 +153,22 @@ impl NativeCallableSignature { } } +/// PHP class-like declaration kind targeted by a dynamic `class_alias()`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EvalClassAliasKind { + Class, + Interface, + Trait, + Enum, +} + +/// Dynamic alias target and kind recorded for eval-visible class-like symbols. +#[derive(Debug, Clone, PartialEq, Eq)] +struct EvalClassAlias { + target: String, + kind: EvalClassAliasKind, +} + /// Process-level eval context passed opaquely across the C ABI. /// /// Generated code never inspects this layout directly; it only passes pointers @@ -161,7 +177,7 @@ impl NativeCallableSignature { pub struct ElephcEvalContext { abi_version: u32, classes: HashMap, - class_aliases: HashMap, + class_aliases: HashMap, declared_class_names: Vec, interfaces: HashMap, declared_interface_names: Vec, @@ -325,7 +341,13 @@ impl ElephcEvalContext { /// Returns true when this eval context has a dynamic class or alias with the requested name. pub fn has_class(&self, name: &str) -> bool { let key = normalize_class_name(name); - self.classes.contains_key(&key) || self.class_aliases.contains_key(&key) + self.classes.contains_key(&key) + || self.class_aliases.get(&key).is_some_and(|alias| { + matches!( + alias.kind, + EvalClassAliasKind::Class | EvalClassAliasKind::Enum + ) + }) } /// Returns a dynamic eval class by PHP case-insensitive class name or alias. @@ -334,9 +356,14 @@ impl ElephcEvalContext { if let Some(class) = self.classes.get(&key) { return Some(class); } - self.class_aliases - .get(&key) - .and_then(|target| self.classes.get(&normalize_class_name(target))) + let alias = self.class_aliases.get(&key)?; + if !matches!( + alias.kind, + EvalClassAliasKind::Class | EvalClassAliasKind::Enum + ) { + return None; + } + self.classes.get(&normalize_class_name(&alias.target)) } /// Resolves a PHP class name or alias to the canonical target spelling stored by eval. @@ -345,7 +372,13 @@ impl ElephcEvalContext { if let Some(class) = self.classes.get(&key) { return Some(class.name().to_string()); } - self.class_aliases.get(&key).cloned() + self.class_aliases.get(&key).and_then(|alias| { + matches!( + alias.kind, + EvalClassAliasKind::Class | EvalClassAliasKind::Enum + ) + .then(|| alias.target.clone()) + }) } /// Resolves a PHP class-like name to eval class, interface, trait, or alias spelling. @@ -363,19 +396,69 @@ impl ElephcEvalContext { if let Some(enum_decl) = self.enums.get(&key) { return Some(enum_decl.name().to_string()); } - self.class_aliases.get(&key).cloned() + self.class_aliases + .get(&key) + .map(|alias| alias.target.clone()) } /// Defines an alias for an eval-declared class or an already known alias. pub fn define_class_alias(&mut self, original: &str, alias: &str) -> bool { - let Some(target) = self.resolve_class_name(original) else { + let Some((target, kind)) = self.resolve_class_like_alias_target(original) else { return false; }; - self.define_external_class_alias(&target, alias) + self.define_class_alias_with_kind(&target, alias, kind) } /// Defines an alias for a runtime-visible class whose metadata lives outside eval. pub fn define_external_class_alias(&mut self, original: &str, alias: &str) -> bool { + self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Class) + } + + /// Defines an alias for a runtime-visible interface whose metadata lives outside eval. + pub fn define_external_interface_alias(&mut self, original: &str, alias: &str) -> bool { + self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Interface) + } + + /// Defines an alias for a runtime-visible trait whose metadata lives outside eval. + pub fn define_external_trait_alias(&mut self, original: &str, alias: &str) -> bool { + self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Trait) + } + + /// Defines an alias for a runtime-visible enum whose metadata lives outside eval. + pub fn define_external_enum_alias(&mut self, original: &str, alias: &str) -> bool { + self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Enum) + } + + /// Resolves the canonical target and declaration kind for a class-like alias source. + fn resolve_class_like_alias_target( + &self, + original: &str, + ) -> Option<(String, EvalClassAliasKind)> { + let key = normalize_class_name(original); + if let Some(enum_decl) = self.enums.get(&key) { + return Some((enum_decl.name().to_string(), EvalClassAliasKind::Enum)); + } + if let Some(class) = self.classes.get(&key) { + return Some((class.name().to_string(), EvalClassAliasKind::Class)); + } + if let Some(interface) = self.interfaces.get(&key) { + return Some((interface.name().to_string(), EvalClassAliasKind::Interface)); + } + if let Some(trait_decl) = self.traits.get(&key) { + return Some((trait_decl.name().to_string(), EvalClassAliasKind::Trait)); + } + self.class_aliases + .get(&key) + .map(|alias| (alias.target.clone(), alias.kind)) + } + + /// Defines one class-like alias after the caller has resolved the target kind. + fn define_class_alias_with_kind( + &mut self, + original: &str, + alias: &str, + kind: EvalClassAliasKind, + ) -> bool { let alias_key = normalize_class_name(alias); if alias_key.is_empty() || self.classes.contains_key(&alias_key) @@ -386,10 +469,13 @@ impl ElephcEvalContext { { return false; } - self.class_aliases - .insert(alias_key, original.trim_start_matches('\\').to_string()); - self.declared_class_names - .push(alias.trim_start_matches('\\').to_string()); + self.class_aliases.insert( + alias_key, + EvalClassAlias { + target: original.trim_start_matches('\\').to_string(), + kind, + }, + ); true } @@ -417,12 +503,24 @@ impl ElephcEvalContext { /// Returns true when this eval context has a dynamic interface with the requested name. pub fn has_interface(&self, name: &str) -> bool { - self.interfaces.contains_key(&normalize_class_name(name)) + let key = normalize_class_name(name); + self.interfaces.contains_key(&key) + || self + .class_aliases + .get(&key) + .is_some_and(|alias| alias.kind == EvalClassAliasKind::Interface) } /// Returns a dynamic eval interface by PHP case-insensitive interface name. pub fn interface(&self, name: &str) -> Option<&EvalInterface> { - self.interfaces.get(&normalize_class_name(name)) + let key = normalize_class_name(name); + if let Some(interface) = self.interfaces.get(&key) { + return Some(interface); + } + let alias = self.class_aliases.get(&key)?; + (alias.kind == EvalClassAliasKind::Interface) + .then(|| self.interfaces.get(&normalize_class_name(&alias.target))) + .flatten() } /// Returns interface names declared through eval in PHP-visible order. @@ -449,12 +547,24 @@ impl ElephcEvalContext { /// Returns true when this eval context has a dynamic trait with the requested name. pub fn has_trait(&self, name: &str) -> bool { - self.traits.contains_key(&normalize_class_name(name)) + let key = normalize_class_name(name); + self.traits.contains_key(&key) + || self + .class_aliases + .get(&key) + .is_some_and(|alias| alias.kind == EvalClassAliasKind::Trait) } /// Returns a dynamic eval trait by PHP case-insensitive trait name. pub fn trait_decl(&self, name: &str) -> Option<&EvalTrait> { - self.traits.get(&normalize_class_name(name)) + let key = normalize_class_name(name); + if let Some(trait_decl) = self.traits.get(&key) { + return Some(trait_decl); + } + let alias = self.class_aliases.get(&key)?; + (alias.kind == EvalClassAliasKind::Trait) + .then(|| self.traits.get(&normalize_class_name(&alias.target))) + .flatten() } /// Returns trait names declared through eval in PHP-visible order. @@ -485,12 +595,35 @@ impl ElephcEvalContext { /// Returns true when this eval context has a dynamic enum with the requested name. pub fn has_enum(&self, name: &str) -> bool { - self.enums.contains_key(&normalize_class_name(name)) + let key = normalize_class_name(name); + self.enums.contains_key(&key) + || self + .class_aliases + .get(&key) + .is_some_and(|alias| alias.kind == EvalClassAliasKind::Enum) } /// Returns a dynamic eval enum by PHP case-insensitive enum name. pub fn enum_decl(&self, name: &str) -> Option<&EvalEnum> { - self.enums.get(&normalize_class_name(name)) + let key = normalize_class_name(name); + if let Some(enum_decl) = self.enums.get(&key) { + return Some(enum_decl); + } + let alias = self.class_aliases.get(&key)?; + (alias.kind == EvalClassAliasKind::Enum) + .then(|| self.enums.get(&normalize_class_name(&alias.target))) + .flatten() + } + + /// Resolves an enum name or enum alias to the canonical eval enum spelling. + pub fn resolve_enum_name(&self, name: &str) -> Option { + let key = normalize_class_name(name); + if let Some(enum_decl) = self.enums.get(&key) { + return Some(enum_decl.name().to_string()); + } + self.class_aliases.get(&key).and_then(|alias| { + (alias.kind == EvalClassAliasKind::Enum).then(|| alias.target.clone()) + }) } /// Returns enum names declared through eval in PHP-visible order. @@ -500,9 +633,12 @@ impl ElephcEvalContext { /// Returns a materialized singleton case object for one eval enum case. pub fn enum_case(&self, enum_name: &str, case_name: &str) -> Option { + let enum_name = self + .resolve_enum_name(enum_name) + .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); self.enum_cases .get(&( - normalize_class_name(enum_name), + normalize_class_name(&enum_name), normalize_enum_case_name(case_name), )) .copied() @@ -527,9 +663,12 @@ impl ElephcEvalContext { /// Returns a materialized backing value for one eval backed-enum case. pub fn enum_case_value(&self, enum_name: &str, case_name: &str) -> Option { + let enum_name = self + .resolve_enum_name(enum_name) + .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); self.enum_case_values .get(&( - normalize_class_name(enum_name), + normalize_class_name(&enum_name), normalize_enum_case_name(case_name), )) .copied() @@ -1194,7 +1333,7 @@ impl ElephcEvalContext { }; let target = normalize_class_name( &self - .resolve_class_name(target) + .resolve_class_like_name(target) .unwrap_or_else(|| target.trim_start_matches('\\').to_string()), ); if !exclude_self && normalize_class_name(class.name()) == target { diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index bf5512b778..f0fe95e51e 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -229,13 +229,25 @@ pub(in crate::interpreter) fn eval_class_alias_result( }; let class = eval_class_alias_name(class, values)?; let alias = eval_class_alias_name(alias, values)?; - if alias.is_empty() || context.has_class(&alias) || values.class_exists(&alias)? { + if alias.is_empty() + || context.resolve_class_like_name(&alias).is_some() + || values.class_exists(&alias)? + || values.interface_exists(&alias)? + || values.trait_exists(&alias)? + || values.enum_exists(&alias)? + { return values.bool_value(false); } - let aliased = if context.has_class(&class) { + let aliased = if context.resolve_class_like_name(&class).is_some() { context.define_class_alias(&class, &alias) + } else if values.enum_exists(&class)? { + context.define_external_enum_alias(&class, &alias) } else if values.class_exists(&class)? { context.define_external_class_alias(&class, &alias) + } else if values.interface_exists(&class)? { + context.define_external_interface_alias(&class, &alias) + } else if values.trait_exists(&class)? { + context.define_external_trait_alias(&class, &alias) } else { false }; @@ -436,7 +448,7 @@ pub(in crate::interpreter) fn eval_is_a_relation_result( let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; let target_class = target_class.trim_start_matches('\\'); let resolved_target_class = context - .resolve_class_name(target_class) + .resolve_class_like_name(target_class) .unwrap_or_else(|| target_class.to_string()); let is_object = values.type_tag(object_or_class)? == 6; let exclude_self = name == "is_subclass_of"; @@ -455,8 +467,21 @@ pub(in crate::interpreter) fn eval_is_a_relation_result( } else if allow_string && values.type_tag(object_or_class)? == EVAL_TAG_STRING { let source_class = values.string_bytes(object_or_class)?; let source_class = String::from_utf8(source_class).map_err(|_| EvalStatus::RuntimeFatal)?; - if context.class(&source_class).is_some() { - context.class_is_a(&source_class, &resolved_target_class, exclude_self) + let resolved_source_class = context + .resolve_class_like_name(&source_class) + .unwrap_or_else(|| source_class.trim_start_matches('\\').to_string()); + if context.class(&resolved_source_class).is_some() { + context.class_is_a(&resolved_source_class, &resolved_target_class, exclude_self) + } else if context.interface(&resolved_source_class).is_some() { + eval_interface_string_is_a( + &resolved_source_class, + &resolved_target_class, + exclude_self, + context, + ) + } else if context.trait_decl(&resolved_source_class).is_some() { + !exclude_self + && eval_class_like_name_matches(&resolved_source_class, &resolved_target_class) } else { values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? } @@ -468,6 +493,28 @@ pub(in crate::interpreter) fn eval_is_a_relation_result( values.bool_value(result) } +/// Returns whether an interface string source satisfies a class-like target. +fn eval_interface_string_is_a( + source_class: &str, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, +) -> bool { + if !exclude_self && eval_class_like_name_matches(source_class, target_class) { + return true; + } + context + .interface_parent_names(source_class) + .iter() + .any(|parent| eval_class_like_name_matches(parent, target_class)) +} + +/// Returns whether two class-like names match PHP's case-insensitive class-name rules. +fn eval_class_like_name_matches(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + /// Returns whether an eval-created object matches a dynamic class/interface target. pub(in crate::interpreter) fn dynamic_object_is_a( object: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 6a98175aea..99f58bbce7 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1190,13 +1190,17 @@ fn eval_reflection_class_new( ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; let class_name = eval_reflection_string_arg(args[0], values)?; - let Some(metadata) = eval_reflection_class_like_attributes(&class_name, context) else { - let Some((flags, modifiers)) = eval_reflection_aot_class_flags(&class_name, values)? else { + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(&reflected_name, values)? + else { return Ok(None); }; return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, - class_name.trim_start_matches('\\'), + &reflected_name, &[], &[], &[], diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs index fbab44c22c..167ad2c7db 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs @@ -306,12 +306,31 @@ fn execute_program_class_alias_registers_aliases() { public function __construct($x) { $this->x = $x; } public function bump($n) { $this->x = $this->x + $n; return $this->x; } } +interface DynAliasIface {} +trait DynAliasTrait {} +enum DynAliasEnum: string { + case Ready = "ready"; +} echo class_alias("DynAliasBox", "DynAliasCopy") ? "alias" : "bad"; echo ":"; echo class_exists("DynAliasCopy") ? "exists" : "bad"; echo ":"; $box = new DynAliasCopy(5); echo get_class($box); echo ":"; echo $box->bump(2); echo ":"; echo is_a($box, "DynAliasCopy") ? "isa" : "bad"; echo ":"; +echo class_alias("DynAliasIface", "DynAliasIfaceCopy") ? "iface-alias" : "bad"; echo ":"; +echo interface_exists("DynAliasIfaceCopy") ? "iface-exists" : "bad"; echo ":"; +echo class_exists("DynAliasIfaceCopy") ? "bad" : "iface-not-class"; echo ":"; +echo is_a("DynAliasIfaceCopy", "DynAliasIface", true) ? "iface-isa" : "bad"; echo ":"; +echo (new ReflectionClass("DynAliasIfaceCopy"))->isInterface() ? "iface-reflect" : "bad"; echo ":"; +echo class_alias("DynAliasTrait", "DynAliasTraitCopy") ? "trait-alias" : "bad"; echo ":"; +echo trait_exists("DynAliasTraitCopy") ? "trait-exists" : "bad"; echo ":"; +echo class_exists("DynAliasTraitCopy") ? "bad" : "trait-not-class"; echo ":"; +echo is_a("DynAliasTraitCopy", "DynAliasTrait", true) ? "trait-isa" : "bad"; echo ":"; +echo class_alias("DynAliasEnum", "DynAliasEnumCopy") ? "enum-alias" : "bad"; echo ":"; +echo enum_exists("DynAliasEnumCopy") ? "enum-exists" : "bad"; echo ":"; +echo class_exists("DynAliasEnumCopy") ? "enum-class" : "bad"; echo ":"; +echo (new ReflectionClass("DynAliasEnumCopy"))->getName(); echo ":"; +echo DynAliasEnumCopy::Ready->value; echo ":"; echo class_alias("DynAliasBox", "DynAliasCopy") ? "bad" : "duplicate"; echo ":"; echo class_alias("MissingAliasSource", "MissingAliasTarget") ? "bad" : "missing"; echo ":"; echo call_user_func("class_alias", "DynAliasBox", "DynAliasCall") ? "call" : "bad"; echo ":"; @@ -330,7 +349,7 @@ return is_callable("class_alias");"#, assert_eq!( values.output, - "alias:exists:DynAliasBox:7:isa:duplicate:missing:call:call-exists:aot:known:1" + "alias:exists:DynAliasBox:7:isa:iface-alias:iface-exists:iface-not-class:iface-isa:iface-reflect:trait-alias:trait-exists:trait-not-class:trait-isa:enum-alias:enum-exists:enum-class:DynAliasEnum:ready:duplicate:missing:call:call-exists:aot:known:1" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -345,10 +364,8 @@ $classes = get_declared_classes(); echo count($classes); echo ":"; echo $classes[0]; echo ":"; echo $classes[1]; echo ":"; -echo $classes[2]; echo ":"; $call = call_user_func("get_declared_classes"); echo count($call); echo ":"; -echo $call[2]; echo ":"; echo count(get_declared_interfaces()); echo ":"; echo count(call_user_func_array("get_declared_traits", [])); echo ":"; echo function_exists("get_declared_classes"); @@ -361,10 +378,7 @@ return is_callable("get_declared_traits");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( - values.output, - "3:DeclaredOne:DeclaredTwo:DeclaredAlias:3:DeclaredAlias:0:0:11" - ); + assert_eq!(values.output, "2:DeclaredOne:DeclaredTwo:2:0:0:11"); assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies duplicate eval-declared class names fail through runtime status. diff --git a/docs/php/eval.md b/docs/php/eval.md index 14170bedb5..bfaa207908 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -377,10 +377,13 @@ AOT and eval-declared class-name probes are visible through `class_exists()`. Eval object relation probes through `instanceof`, `is_a()`, and `is_subclass_of()` use generated AOT class/interface metadata and eval-created object metadata. `interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated -AOT metadata. Eval-declared classes, interfaces, traits, and class aliases are -visible through the corresponding eval and post-barrier native metadata probes. -Eval-declared enums are visible inside eval through `enum_exists()` and through -class-like probes such as `class_exists()`. +AOT metadata. `class_alias()` can alias eval-declared and generated/AOT +classes, interfaces, traits, and enums, preserving the target class-like kind +for the corresponding metadata probes. Aliases are usable for class-like +lookups but are not added to `get_declared_classes()`, +`get_declared_interfaces()`, or `get_declared_traits()`. Eval-declared enums are +visible inside eval through `enum_exists()` and through class-like probes such +as `class_exists()`. `method_exists()` and `property_exists()` inspect eval-declared class/interface/ trait/enum metadata and generated runtime metadata. Object targets also see dynamic public properties. `get_class_methods()` and `get_object_vars()` follow @@ -449,7 +452,7 @@ where listed below unless a note says otherwise. | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | | Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | -| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()` | | Debug output | `print_r()`, `var_dump()` | | Constants | `define()`, `defined()` | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7a8525e9e8..5080f7e728 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8612,6 +8612,42 @@ return $box->value;'); assert_eq!(out, "5:7"); } +/// Verifies eval `class_alias()` supports class-like interface, trait, enum, and class targets. +#[test] +fn test_eval_class_alias_supports_class_like_targets() { + let out = compile_and_run( + r#"isInterface() ? "IR" : "ir"; echo ":"; +echo class_alias("EvalAliasTrait", "EvalAliasTraitCopy") ? "T" : "t"; echo ":"; +echo trait_exists("EvalAliasTraitCopy") ? "TE" : "te"; echo ":"; +echo class_exists("EvalAliasTraitCopy") ? "bad" : "TC"; echo ":"; +echo is_a("EvalAliasTraitCopy", "EvalAliasTrait", true) ? "TI" : "ti"; echo ":"; +echo class_alias("EvalAliasEnum", "EvalAliasEnumCopy") ? "E" : "e"; echo ":"; +echo enum_exists("EvalAliasEnumCopy") ? "EE" : "ee"; echo ":"; +echo class_exists("EvalAliasEnumCopy") ? "EC" : "bad"; echo ":"; +echo (new ReflectionClass("EvalAliasEnumCopy"))->getName(); echo ":"; +echo EvalAliasEnumCopy::Ready->value; echo ":"; +echo class_alias("EvalAliasClass", "EvalAliasClassCopy") ? "C" : "c"; echo ":"; +echo class_exists("EvalAliasClassCopy") ? "CE" : "ce"; echo ":"; +echo count(get_declared_classes()); echo ":"; +echo count(get_declared_interfaces()); echo ":"; +return count(get_declared_traits());'); +"#, + ); + assert_eq!( + out, + "I:IE:IC:II:IR:T:TE:TC:TI:E:EE:EC:EvalAliasEnum:ready:C:CE:2:1:1" + ); +} + /// Verifies eval can construct an AOT class with no declared constructor. #[test] fn test_eval_dynamic_new_constructs_aot_class() { From df1ff9c5b7fdbbce0e2257b11bf0c62882276075 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 04:10:38 +0200 Subject: [PATCH 0474/1208] Expose eval return type reflection --- crates/elephc-eval/src/eval_ir.rs | 42 ++++++ .../src/interpreter/dynamic_functions.rs | 1 + .../elephc-eval/src/interpreter/reflection.rs | 124 ++++++++++++++---- .../elephc-eval/src/interpreter/statements.rs | 4 +- .../tests/builtins_class_metadata.rs | 36 +++++ .../tests/builtins_reflection_functions.rs | 40 ++++++ crates/elephc-eval/src/parser/statements.rs | 77 +++++++++-- .../src/parser/tests/classes_errors.rs | 75 +++++++++++ .../src/parser/tests/namespaces.rs | 3 + docs/php/eval.md | 12 +- tests/codegen/eval.rs | 74 +++++++++++ 11 files changed, 447 insertions(+), 41 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 9da8f07def..b94ae1c169 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -87,6 +87,7 @@ pub enum EvalStmt { parameter_defaults: Vec>, parameter_is_by_ref: Vec, parameter_is_variadic: Vec, + return_type: Option, body: Vec, }, Global { @@ -168,6 +169,7 @@ pub struct EvalFunction { parameter_defaults: Vec>, parameter_is_by_ref: Vec, parameter_is_variadic: Vec, + return_type: Option, body: Vec, } @@ -188,6 +190,7 @@ impl EvalFunction { parameter_defaults, parameter_is_by_ref, parameter_is_variadic, + return_type: None, body, } } @@ -231,6 +234,12 @@ impl EvalFunction { self } + /// Returns a copy of this function with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + /// Returns the original source spelling of this eval-declared function name. pub fn name(&self) -> &str { &self.name @@ -271,6 +280,11 @@ impl EvalFunction { &self.parameter_is_variadic } + /// Returns retained return type metadata, if the function declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + /// Returns the dynamic EvalIR statements that form the function body. pub fn body(&self) -> &[EvalStmt] { &self.body @@ -288,8 +302,10 @@ pub enum EvalParameterTypeVariant { Int, Iterable, Mixed, + Never, Object, String, + Void, } /// How multiple eval parameter type atoms combine. @@ -717,6 +733,7 @@ pub struct EvalInterfaceMethod { parameter_defaults: Vec>, parameter_is_by_ref: Vec, parameter_is_variadic: Vec, + return_type: Option, } impl EvalInterfaceMethod { @@ -739,6 +756,7 @@ impl EvalInterfaceMethod { parameter_defaults, parameter_is_by_ref, parameter_is_variadic, + return_type: None, } } @@ -794,6 +812,12 @@ impl EvalInterfaceMethod { self } + /// Returns a copy of this interface method with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + /// Returns the PHP-visible method name. pub fn name(&self) -> &str { &self.name @@ -843,6 +867,11 @@ impl EvalInterfaceMethod { pub fn parameter_is_variadic(&self) -> &[bool] { &self.parameter_is_variadic } + + /// Returns retained return type metadata, if the method declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } } /// Runtime class declared by an eval fragment. @@ -1582,6 +1611,7 @@ pub struct EvalClassMethod { parameter_defaults: Vec>, parameter_is_by_ref: Vec, parameter_is_variadic: Vec, + return_type: Option, body: Vec, } @@ -1640,6 +1670,7 @@ impl EvalClassMethod { parameter_defaults, parameter_is_by_ref, parameter_is_variadic, + return_type: None, body, } } @@ -1695,6 +1726,12 @@ impl EvalClassMethod { self } + /// Returns a copy of this method with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + /// Returns attributes declared directly on this class method. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes @@ -1769,6 +1806,11 @@ impl EvalClassMethod { &self.parameter_is_variadic } + /// Returns retained return type metadata, if the method declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + /// Returns the dynamic EvalIR statements that form the method body. pub fn body(&self) -> &[EvalStmt] { &self.body diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index a6d52f4a82..d9b9eae339 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -576,6 +576,7 @@ fn eval_method_parameter_variant_accepts_exact( eval_method_parameter_class_accepts(value, tag, "Iterator", context, values) } EvalParameterTypeVariant::Mixed => Ok(true), + EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void => Ok(false), EvalParameterTypeVariant::Object => Ok(tag == EVAL_TAG_OBJECT), EvalParameterTypeVariant::String => Ok(tag == EVAL_TAG_STRING), } diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 99f58bbce7..b0dabdff80 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -68,6 +68,7 @@ struct EvalReflectionMemberMetadata { is_promoted: bool, modifiers: u64, type_metadata: Option, + return_type_metadata: Option, default_value: Option, required_parameter_count: usize, parameters: Vec, @@ -100,11 +101,13 @@ struct EvalReflectionDeclaringFunctionMetadata { } /// Eval metadata needed to materialize one parameter `ReflectionType` object. +#[derive(Clone)] struct EvalReflectionParameterTypeMetadata { kind: EvalReflectionParameterTypeKind, } /// Eval reflection parameter type object variants. +#[derive(Clone)] enum EvalReflectionParameterTypeKind { Named(EvalReflectionNamedTypeMetadata), Union(EvalReflectionUnionTypeMetadata), @@ -112,6 +115,7 @@ enum EvalReflectionParameterTypeKind { } /// Eval metadata needed to materialize one `ReflectionNamedType` object. +#[derive(Clone)] struct EvalReflectionNamedTypeMetadata { name: String, allows_null: bool, @@ -120,17 +124,27 @@ struct EvalReflectionNamedTypeMetadata { /// Registered ReflectionFunctionAbstract target metadata for simple method dispatch. enum EvalReflectionFunctionMethodTarget { - Function { name: String, is_variadic: bool }, - Method { name: String, is_variadic: bool }, + Function { + name: String, + is_variadic: bool, + return_type_metadata: Option, + }, + Method { + name: String, + is_variadic: bool, + return_type_metadata: Option, + }, } /// Eval metadata needed to materialize one `ReflectionUnionType` object. +#[derive(Clone)] struct EvalReflectionUnionTypeMetadata { types: Vec, allows_null: bool, } /// Eval metadata needed to materialize one `ReflectionIntersectionType` object. +#[derive(Clone)] struct EvalReflectionIntersectionTypeMetadata { types: Vec, } @@ -694,9 +708,17 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( .bool_value(!eval_reflection_function_method_namespace_name(&target).is_empty()) .map(Some) } - "isinternal" | "isclosure" | "isdeprecated" | "returnsreference" | "hasreturntype" - | "isgenerator" | "hastentativereturntype" => { - eval_reflection_false_metadata_result(evaluated_args, values) + "isinternal" + | "isclosure" + | "isdeprecated" + | "returnsreference" + | "isgenerator" + | "hastentativereturntype" => eval_reflection_false_metadata_result(evaluated_args, values), + "hasreturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_return_type(&target).is_some()) + .map(Some) } "isuserdefined" => { eval_reflection_bind_no_args(evaluated_args)?; @@ -714,7 +736,16 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( } EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), }, - "getreturntype" | "gettentativereturntype" => { + "getreturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + match eval_reflection_function_method_return_type(&target) { + Some(type_metadata) => { + eval_reflection_type_object_result(type_metadata, values).map(Some) + } + None => values.null().map(Some), + } + } + "gettentativereturntype" => { eval_reflection_bind_no_args(evaluated_args)?; values.null().map(Some) } @@ -735,9 +766,12 @@ pub(in crate::interpreter) fn eval_reflection_method_prototype_result( if !is_has_prototype && !is_get_prototype { return Ok(None); } - let Some((declaring_class, reflected_method)) = context - .eval_reflection_method(identity) - .map(|(declaring_class, method_name)| (declaring_class.to_string(), method_name.to_string())) + let Some((declaring_class, reflected_method)) = + context + .eval_reflection_method(identity) + .map(|(declaring_class, method_name)| { + (declaring_class.to_string(), method_name.to_string()) + }) else { return Ok(None); }; @@ -1517,6 +1551,7 @@ fn eval_reflection_aot_method_metadata( is_promoted: false, modifiers: eval_reflection_method_modifiers_from_flags(flags), type_metadata: None, + return_type_metadata: None, default_value: None, required_parameter_count: 0, parameters: Vec::new(), @@ -1573,6 +1608,7 @@ fn eval_reflection_aot_property_metadata( false, ), type_metadata: None, + return_type_metadata: None, default_value: None, required_parameter_count: 0, parameters: Vec::new(), @@ -3044,6 +3080,9 @@ fn eval_reflection_method_metadata( method.is_abstract(), false, ); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), declaring_class_name: Some(declaring_class.clone()), @@ -3084,6 +3123,7 @@ fn eval_reflection_method_metadata( method.is_abstract(), ), type_metadata: None, + return_type_metadata, default_value: None, required_parameter_count, parameters, @@ -3107,6 +3147,9 @@ fn eval_reflection_method_metadata( true, false, ); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), declaring_class_name: Some(class_name.to_string()), @@ -3142,6 +3185,7 @@ fn eval_reflection_method_metadata( true, ), type_metadata: None, + return_type_metadata, default_value: None, required_parameter_count, parameters, @@ -3165,6 +3209,9 @@ fn eval_reflection_method_metadata( method.is_abstract(), false, ); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), declaring_class_name: Some(trait_decl.name().to_string()), @@ -3202,6 +3249,7 @@ fn eval_reflection_method_metadata( method.is_abstract(), ), type_metadata: None, + return_type_metadata, default_value: None, required_parameter_count, parameters, @@ -3240,6 +3288,7 @@ fn eval_reflection_property_metadata( type_metadata: property .property_type() .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, default_value, required_parameter_count: 0, parameters: Vec::new(), @@ -3272,6 +3321,7 @@ fn eval_reflection_property_metadata( type_metadata: property .property_type() .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, default_value: None, required_parameter_count: 0, parameters: Vec::new(), @@ -3304,6 +3354,7 @@ fn eval_reflection_property_metadata( type_metadata: property .property_type() .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, default_value, required_parameter_count: 0, parameters: Vec::new(), @@ -4016,12 +4067,14 @@ fn eval_reflection_named_type_variant_metadata( Some(eval_reflection_builtin_named_type("iterable", allows_null)) } EvalParameterTypeVariant::Mixed => Some(eval_reflection_builtin_named_type("mixed", true)), + EvalParameterTypeVariant::Never => Some(eval_reflection_builtin_named_type("never", false)), EvalParameterTypeVariant::Object => { Some(eval_reflection_builtin_named_type("object", allows_null)) } EvalParameterTypeVariant::String => { Some(eval_reflection_builtin_named_type("string", allows_null)) } + EvalParameterTypeVariant::Void => Some(eval_reflection_builtin_named_type("void", false)), } } @@ -4043,27 +4096,35 @@ fn eval_reflection_function_method_target( context: &ElephcEvalContext, ) -> Option { if let Some(name) = context.eval_reflection_function_name(identity) { - let is_variadic = context - .function(&name.to_ascii_lowercase()) + let function = context.function(&name.to_ascii_lowercase()); + let is_variadic = function .is_some_and(|function| function.parameter_is_variadic().iter().any(|flag| *flag)); + let return_type_metadata = function + .and_then(EvalFunction::return_type) + .and_then(eval_reflection_parameter_type_metadata); return Some(EvalReflectionFunctionMethodTarget::Function { name: name.to_string(), is_variadic, + return_type_metadata, }); } context .eval_reflection_method(identity) .map(|(declaring_class, method_name)| { - let is_variadic = eval_reflection_method_metadata(declaring_class, method_name, context) - .is_some_and(|method| { - method - .parameters - .iter() - .any(|parameter| parameter.is_variadic) - }); + let method_metadata = + eval_reflection_method_metadata(declaring_class, method_name, context); + let is_variadic = method_metadata.as_ref().is_some_and(|method| { + method + .parameters + .iter() + .any(|parameter| parameter.is_variadic) + }); + let return_type_metadata = + method_metadata.and_then(|method| method.return_type_metadata); EvalReflectionFunctionMethodTarget::Method { name: method_name.to_string(), is_variadic, + return_type_metadata, } }) } @@ -4117,6 +4178,22 @@ fn eval_reflection_function_method_is_variadic( } } +/// Returns the retained return type metadata for a reflected function or method. +fn eval_reflection_function_method_return_type( + target: &EvalReflectionFunctionMethodTarget, +) -> Option<&EvalReflectionParameterTypeMetadata> { + match target { + EvalReflectionFunctionMethodTarget::Function { + return_type_metadata, + .. + } + | EvalReflectionFunctionMethodTarget::Method { + return_type_metadata, + .. + } => return_type_metadata.as_ref(), + } +} + /// Returns the final namespace segment-free name component from a PHP symbol name. fn eval_reflection_short_name(name: &str) -> String { let name = name.trim_start_matches('\\'); @@ -4144,14 +4221,9 @@ fn eval_reflection_method_prototype_target( if !(context.has_class(declaring_class) || context.has_enum(declaring_class)) { return None; } - eval_reflection_parent_method_prototype_target(declaring_class, method_name, context) - .or_else(|| { - eval_reflection_interface_method_prototype_target( - declaring_class, - method_name, - context, - ) - }) + eval_reflection_parent_method_prototype_target(declaring_class, method_name, context).or_else( + || eval_reflection_interface_method_prototype_target(declaring_class, method_name, context), + ) } /// Finds the nearest parent-class method prototype for an eval-declared override. diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 15610f3d82..91330d1a53 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -146,6 +146,7 @@ pub(in crate::interpreter) fn execute_stmt( parameter_defaults, parameter_is_by_ref, parameter_is_variadic, + return_type, body, } => { let key = name.to_ascii_lowercase(); @@ -158,7 +159,8 @@ pub(in crate::interpreter) fn execute_stmt( .with_parameter_types(parameter_types.clone()) .with_parameter_defaults(parameter_defaults.clone()) .with_parameter_by_ref_flags(parameter_is_by_ref.clone()) - .with_parameter_variadic_flags(parameter_is_variadic.clone()), + .with_parameter_variadic_flags(parameter_is_variadic.clone()) + .with_return_type(return_type.clone()), ) .map_err(|_| EvalStatus::RuntimeFatal)?; Ok(EvalControl::None) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 0647dcf77d..4078005878 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1459,6 +1459,42 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod exposes eval-declared return type metadata. +#[test] +fn execute_program_reflection_method_reports_return_type_metadata() { + let program = parse_fragment( + br#"interface EvalReflectReturnIface { + public function read(): string; +} +class EvalReflectReturnTarget implements EvalReflectReturnIface { + public function read(): string { return "ok"; } + public function selfReturn(): static { return $this; } + public function done(): void {} +} +$iface = new ReflectionMethod("EvalReflectReturnIface", "read"); +$ifaceType = $iface->getReturnType(); +echo $iface->hasReturnType() ? "I" : "i"; echo ":"; +echo $ifaceType->getName(); echo ":"; +echo $ifaceType->isBuiltin() ? "B" : "b"; echo ":"; +$self = (new ReflectionMethod("EvalReflectReturnTarget", "selfReturn"))->getReturnType(); +echo $self->getName(); echo ":"; +echo $self->isBuiltin() ? "B" : "b"; echo ":"; +$void = (new ReflectionMethod("EvalReflectReturnTarget", "done"))->getReturnType(); +echo $void->getName(); echo ":"; +echo $void->allowsNull() ? "N" : "n"; echo ":"; +echo $void->isBuiltin() ? "B" : "b"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "I:string:B:static:b:void:n:B"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionParameter reports eval constructor-promotion metadata. #[test] fn execute_program_reflection_parameter_reports_eval_promoted_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs index 6e7438d993..0d9251906a 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs @@ -87,6 +87,46 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionFunction exposes eval-declared return type metadata. +#[test] +fn execute_program_reflection_function_reports_return_type_metadata() { + let program = parse_fragment( + br#"function eval_reflect_return_named(): ?int { return 1; } +function eval_reflect_return_union(): int|string { return 1; } +function eval_reflect_return_never(): never { throw new Exception("stop"); } +function eval_reflect_return_plain() {} +$namedRef = new ReflectionFunction("eval_reflect_return_named"); +$named = $namedRef->getReturnType(); +echo $namedRef->hasReturnType() ? "T" : "t"; echo ":"; +echo $named->getName(); echo ":"; +echo $named->allowsNull() ? "N" : "n"; echo ":"; +echo $named->isBuiltin() ? "B" : "b"; echo ":"; +$union = (new ReflectionFunction("eval_reflect_return_union"))->getReturnType(); +echo count($union->getTypes()); echo ":"; +foreach ($union->getTypes() as $type) { + echo $type->getName(); + echo $type->isBuiltin() ? "B" : "b"; +} +echo ":"; +$never = (new ReflectionFunction("eval_reflect_return_never"))->getReturnType(); +echo $never->getName(); echo ":"; +echo $never->allowsNull() ? "N" : "n"; echo ":"; +echo $never->isBuiltin() ? "B" : "b"; echo ":"; +$plain = new ReflectionFunction("eval_reflect_return_plain"); +echo $plain->hasReturnType() ? "P" : "p"; echo ":"; +echo $plain->getReturnType() === null ? "Q" : "q"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "T:int:N:B:2:intBstringB:never:n:B:p:Q"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionFunction origin metadata APIs report eval user-defined defaults. #[test] fn execute_program_reflection_function_reports_origin_metadata_defaults() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 2a71b057fe..2cc32c2d1e 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -41,6 +41,13 @@ struct ParsedClassBody { trait_adaptations: Vec, } +/// Type-declaration position controls PHP-only atoms such as `void` and `never`. +#[derive(Clone, Copy)] +enum EvalTypePosition { + Parameter, + Return, +} + impl Parser { /// Parses one source statement, expanding `unset($a, $b)` to one statement per variable. pub(super) fn parse_stmt(&mut self) -> Result, EvalParseError> { @@ -844,6 +851,7 @@ impl Parser { if !promoted_properties.is_empty() && (is_abstract || is_static) { return Err(EvalParseError::UnsupportedConstruct); } + let return_type = self.parse_optional_return_type()?; let body = if is_abstract { self.expect_semicolon()?; Vec::new() @@ -869,7 +877,8 @@ impl Parser { .with_parameter_attributes(parameter_attributes) .with_parameter_defaults(parameter_defaults) .with_parameter_by_ref_flags(parameter_is_by_ref) - .with_parameter_variadic_flags(parameter_is_variadic), + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type), promoted_properties, )) } @@ -1402,6 +1411,7 @@ impl Parser { if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { return Err(EvalParseError::UnsupportedConstruct); } + let return_type = self.parse_optional_return_type()?; self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) .with_static(is_static) @@ -1409,7 +1419,8 @@ impl Parser { .with_parameter_attributes(parameter_attributes) .with_parameter_defaults(parameter_defaults) .with_parameter_by_ref_flags(parameter_is_by_ref) - .with_parameter_variadic_flags(parameter_is_variadic)) + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type)) } /// Parses one interface property hook contract. @@ -1519,6 +1530,7 @@ impl Parser { if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { return Err(EvalParseError::UnsupportedConstruct); } + let return_type = self.parse_optional_return_type()?; let body = self.parse_block()?; Ok(vec![EvalStmt::FunctionDecl { name, @@ -1529,6 +1541,7 @@ impl Parser { parameter_defaults, parameter_is_by_ref, parameter_is_variadic, + return_type, body, }]) } @@ -1978,11 +1991,27 @@ impl Parser { ) { return Ok(None); } + self.parse_type_decl(EvalTypePosition::Parameter) + } + + /// Consumes a supported function or method return type after `:`. + fn parse_optional_return_type(&mut self) -> Result, EvalParseError> { + if !self.consume(TokenKind::Colon) { + return Ok(None); + } + self.parse_type_decl(EvalTypePosition::Return) + } + + /// Parses one PHP type declaration and returns retained eval metadata. + fn parse_type_decl( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { let nullable_shorthand = self.consume(TokenKind::Question); if nullable_shorthand && matches!(self.current(), TokenKind::DollarIdent(_)) { return Err(EvalParseError::UnexpectedToken); } - let first = self.parse_parameter_type_name()?; + let first = self.parse_type_name(position)?; let mut variants = Vec::new(); let mut allows_null = nullable_shorthand || matches!(first, None); if let Some(first) = first { @@ -2001,19 +2030,27 @@ impl Parser { && !self.next_token_starts_parameter_storage() { while self.consume(TokenKind::Ampersand) { - let Some(variant) = self.parse_parameter_type_name()? else { + let Some(variant) = self.parse_type_name(position)? else { return Err(EvalParseError::UnsupportedConstruct); }; variants.push(variant); } + if type_variants_contain_standalone_return_only_atoms(&variants) { + return Err(EvalParseError::UnsupportedConstruct); + } return Ok(Some(EvalParameterType::intersection(variants))); } while self.consume(TokenKind::Pipe) { - match self.parse_parameter_type_name()? { + match self.parse_type_name(position)? { Some(variant) => variants.push(variant), None => allows_null = true, } } + if type_variants_contain_standalone_return_only_atoms(&variants) + && (variants.len() != 1 || allows_null) + { + return Err(EvalParseError::UnsupportedConstruct); + } Ok(Some(EvalParameterType::new(variants, allows_null))) } @@ -2022,23 +2059,25 @@ impl Parser { matches!(self.peek(), TokenKind::DollarIdent(_) | TokenKind::Ellipsis) } - /// Consumes one simple qualified method parameter type name. - fn parse_parameter_type_name( + /// Consumes one simple qualified method type name. + fn parse_type_name( &mut self, + position: EvalTypePosition, ) -> Result, EvalParseError> { match self.current() { TokenKind::Ident(_) | TokenKind::Backslash => { let name = self.parse_qualified_name()?; - self.parameter_type_from_name(name) + self.type_variant_from_name(name, position) } _ => Err(EvalParseError::UnexpectedToken), } } - /// Converts one parsed PHP parameter type name to retained eval metadata. - fn parameter_type_from_name( + /// Converts one parsed PHP type name to retained eval metadata. + fn type_variant_from_name( &self, name: ParsedQualifiedName, + position: EvalTypePosition, ) -> Result, EvalParseError> { if !name.absolute { let lower = name.name.to_ascii_lowercase(); @@ -2050,9 +2089,15 @@ impl Parser { "int" => Some(EvalParameterTypeVariant::Int), "iterable" => Some(EvalParameterTypeVariant::Iterable), "mixed" => Some(EvalParameterTypeVariant::Mixed), + "never" if matches!(position, EvalTypePosition::Return) => { + Some(EvalParameterTypeVariant::Never) + } "null" => return Ok(None), "object" => Some(EvalParameterTypeVariant::Object), "string" => Some(EvalParameterTypeVariant::String), + "void" if matches!(position, EvalTypePosition::Return) => { + Some(EvalParameterTypeVariant::Void) + } "void" | "never" => return Err(EvalParseError::UnsupportedConstruct), "self" | "parent" | "static" => { Some(EvalParameterTypeVariant::Class(lower.to_string())) @@ -2490,6 +2535,18 @@ fn eval_array_element_default_is_supported(element: &EvalArrayElement) -> bool { } } +/// Returns whether a type list contains return-only standalone atoms. +fn type_variants_contain_standalone_return_only_atoms( + variants: &[EvalParameterTypeVariant], +) -> bool { + variants.iter().any(|variant| { + matches!( + variant, + EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void + ) + }) +} + /// Returns whether a class-like receiver is legal in a compile-time method default. fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { !class_name diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 1d5d1a32a9..52cef39474 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -41,6 +41,81 @@ fn parse_fragment_accepts_class_extends_and_implements_source() { ); } +/// Verifies function, interface, and class method return types are retained. +#[test] +fn parse_fragment_accepts_return_type_metadata() { + let program = parse_fragment( + br#"function DynEvalReturn(): ?int { return 1; } +interface DynEvalReturnIface { + public function read(): string; +} +class DynEvalReturnClass { + public function selfReturn(): static { return $this; } + public function done(): void {} +}"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::FunctionDecl { + return_type: Some(function_return_type), + .. + } = &statements[0] + else { + panic!("expected function declaration with return type"); + }; + assert_eq!( + function_return_type.variants(), + &[EvalParameterTypeVariant::Int] + ); + assert!(function_return_type.allows_null()); + + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + let interface_return_type = interface.methods()[0] + .return_type() + .expect("interface method return type"); + assert_eq!( + interface_return_type.variants(), + &[EvalParameterTypeVariant::String] + ); + + let EvalStmt::ClassDecl(class) = &statements[2] else { + panic!("expected class declaration"); + }; + let self_return_type = class.methods()[0] + .return_type() + .expect("class method return type"); + assert_eq!( + self_return_type.variants(), + &[EvalParameterTypeVariant::Class("static".to_string())] + ); + let void_return_type = class.methods()[1] + .return_type() + .expect("void method return type"); + assert_eq!( + void_return_type.variants(), + &[EvalParameterTypeVariant::Void] + ); +} + +/// Verifies return-only type atoms reject nullable, union, and intersection forms. +#[test] +fn parse_fragment_rejects_invalid_void_and_never_return_type_forms() { + for source in [ + b"function DynEvalBadVoid(): ?void {}" as &[u8], + b"function DynEvalBadVoidUnion(): void|null {}", + b"function DynEvalBadNeverUnion(): never|int {}", + b"function DynEvalBadNeverIntersection(): never&Countable {}", + b"function DynEvalBadVoidParam(void $value) {}", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + /// Verifies class attributes lower to eval class metadata with supported literal args. #[test] fn parse_fragment_accepts_class_attribute_metadata() { diff --git a/crates/elephc-eval/src/parser/tests/namespaces.rs b/crates/elephc-eval/src/parser/tests/namespaces.rs index 955ae8354b..e3d6a4e3d8 100644 --- a/crates/elephc-eval/src/parser/tests/namespaces.rs +++ b/crates/elephc-eval/src/parser/tests/namespaces.rs @@ -25,6 +25,7 @@ fn parse_fragment_accepts_function_declaration_source() { parameter_defaults: vec![None], parameter_is_by_ref: vec![false], parameter_is_variadic: vec![false], + return_type: None, body: vec![EvalStmt::Return(Some(EvalExpr::Binary { op: EvalBinOp::Add, left: Box::new(EvalExpr::LoadVar("x".to_string())), @@ -54,6 +55,7 @@ return dyn();"#, parameter_defaults: Vec::new(), parameter_is_by_ref: Vec::new(), parameter_is_variadic: Vec::new(), + return_type: None, body: vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( "Eval\\Ns".to_string() ))))], @@ -245,6 +247,7 @@ function dyn() { return alias(); }"#, parameter_defaults: Vec::new(), parameter_is_by_ref: Vec::new(), parameter_is_variadic: Vec::new(), + return_type: None, body: vec![EvalStmt::Return(Some(EvalExpr::Call { name: "lib\\target".to_string(), args: Vec::new(), diff --git a/docs/php/eval.md b/docs/php/eval.md index bfaa207908..a7f5044f34 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -195,8 +195,11 @@ while `ReflectionMethod::getNamespaceName()` reports an empty string and `inNamespace()` reports `false`, matching PHP's method reflection behavior. `ReflectionFunction` and `ReflectionMethod` report eval user-symbol defaults through `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, -`returnsReference()`, `hasReturnType()`, `getReturnType()`, `isGenerator()`, -`isVariadic()`, `hasTentativeReturnType()`, and `getTentativeReturnType()`. +`returnsReference()`, `isGenerator()`, `isVariadic()`, +`hasTentativeReturnType()`, and `getTentativeReturnType()`. `hasReturnType()` +and `getReturnType()` expose retained eval return type metadata for supported +named, nullable, union, and intersection declarations, including `void` and +`never` as builtin non-nullable named types. `ReflectionFunction::isDisabled()` reports `false` for eval-visible functions. `ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and @@ -292,7 +295,7 @@ reflected method is `__construct` or `__destruct`. `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report retained eval-declared function and method metadata. Eval-declared functions and methods expose declared-type -presence, simple named type metadata through +presence for parameters and return types, simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, `allowsNull()`, and `isBuiltin()`, and multi-member union metadata through `ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter @@ -527,7 +530,8 @@ broader parameter/return ABI shapes are still outside that bridge. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType -and attribute slice, Reflection type APIs beyond parameter/property metadata, broader +and attribute slice, Reflection type APIs beyond retained parameter, property, +and function/method return metadata, broader parameter default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Eval object cloning currently covers diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5080f7e728..d98b694f91 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7447,6 +7447,41 @@ foreach ($params as $param) { ); } +/// Verifies eval ReflectionMethod exposes eval-declared return type metadata. +#[test] +fn test_eval_reflection_method_reports_return_type_metadata() { + let out = compile_and_run_capture( + r#"getReturnType(); +echo ($iface->hasReturnType() ? "I" : "i") . ":"; +echo $ifaceType->getName() . ":"; +echo ($ifaceType->isBuiltin() ? "B" : "b") . ":"; +$self = (new ReflectionMethod("EvalReflectReturnTarget", "selfReturn"))->getReturnType(); +echo $self->getName() . ":"; +echo ($self->isBuiltin() ? "B" : "b") . ":"; +$void = (new ReflectionMethod("EvalReflectReturnTarget", "done"))->getReturnType(); +echo $void->getName() . ":"; +echo ($void->allowsNull() ? "N" : "n") . ":"; +echo $void->isBuiltin() ? "B" : "b";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "I:string:B:static:b:void:n:B"); +} + /// Verifies eval ReflectionProperty materializes property type metadata through the bridge. #[test] fn test_eval_reflection_property_get_type_metadata() { @@ -7792,6 +7827,45 @@ echo $params[2]->isPassedByReference() ? "R" : "r";'); ); } +/// Verifies eval ReflectionFunction exposes eval-declared return type metadata. +#[test] +fn test_eval_reflection_function_reports_return_type_metadata() { + let out = compile_and_run_capture( + r#"getReturnType(); +echo ($namedRef->hasReturnType() ? "T" : "t") . ":"; +echo $named->getName() . ":"; +echo ($named->allowsNull() ? "N" : "n") . ":"; +echo ($named->isBuiltin() ? "B" : "b") . ":"; +$union = (new ReflectionFunction("eval_reflect_return_union"))->getReturnType(); +echo count($union->getTypes()) . ":"; +foreach ($union->getTypes() as $type) { + echo $type->getName(); + echo $type->isBuiltin() ? "B" : "b"; +} +echo ":"; +$never = (new ReflectionFunction("eval_reflect_return_never"))->getReturnType(); +echo $never->getName() . ":"; +echo ($never->allowsNull() ? "N" : "n") . ":"; +echo ($never->isBuiltin() ? "B" : "b") . ":"; +$plain = new ReflectionFunction("eval_reflect_return_plain"); +echo ($plain->hasReturnType() ? "P" : "p") . ":"; +echo $plain->getReturnType() === null ? "Q" : "q";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "T:int:N:B:2:intBstringB:never:n:B:p:Q"); +} + /// Verifies eval Reflection origin metadata APIs are present on supported owners. #[test] fn test_eval_reflection_origin_metadata_defaults() { From 6f93cfb9c0eb2be7cbc68bc6f8eabb3a454e2c22 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 04:36:57 +0200 Subject: [PATCH 0475/1208] Validate eval method return type contracts --- crates/elephc-eval/src/interpreter/mod.rs | 2 + .../src/interpreter/return_type_compat.rs | 424 ++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 62 ++- .../src/interpreter/tests/expressions.rs | 161 +++++++ docs/php/eval.md | 3 + tests/codegen/eval.rs | 140 ++++++ 6 files changed, 785 insertions(+), 7 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/return_type_compat.rs diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index bf00c19743..54910e4d65 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -24,6 +24,7 @@ mod include_exec; mod json; mod libc_shims; mod reflection; +mod return_type_compat; mod runtime_ops; mod scope_cells; mod statements; @@ -63,6 +64,7 @@ use json::*; use libc_shims::*; use reflection::*; use regex::bytes::{Captures, Regex, RegexBuilder}; +use return_type_compat::*; pub use runtime_ops::RuntimeValueOps; use runtime_ops::*; use scope_cells::*; diff --git a/crates/elephc-eval/src/interpreter/return_type_compat.rs b/crates/elephc-eval/src/interpreter/return_type_compat.rs new file mode 100644 index 0000000000..012fb4ed7d --- /dev/null +++ b/crates/elephc-eval/src/interpreter/return_type_compat.rs @@ -0,0 +1,424 @@ +//! Purpose: +//! Validates covariant return type compatibility for eval-declared methods. +//! This keeps class/interface signature checks out of the statement dispatcher. +//! +//! Called from: +//! - `crate::interpreter::statements` while registering eval class-like declarations. +//! +//! Key details: +//! - `self`, `parent`, and `static` are resolved relative to the declaration owner. +//! - Pending class declarations are checked before they are registered in the eval context. + +use super::*; + +/// Returns whether a method preserves the required declared return type contract. +pub(super) fn method_return_type_signature_accepts( + implementation_type: Option<&EvalParameterType>, + implementation_owner: &str, + required_type: Option<&EvalParameterType>, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + let Some(required_type) = required_type else { + return true; + }; + implementation_type.is_some_and(|implementation_type| { + eval_return_type_accepts( + required_type, + required_owner, + implementation_type, + implementation_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether `actual_type` is a covariant subtype of `expected_type`. +fn eval_return_type_accepts( + expected_type: &EvalParameterType, + expected_owner: &str, + actual_type: &EvalParameterType, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if eval_return_type_is_never(actual_type) { + return true; + } + if eval_return_type_is_never(expected_type) { + return false; + } + if eval_return_type_is_void(expected_type) { + return eval_return_type_is_void(actual_type); + } + if eval_return_type_is_void(actual_type) { + return false; + } + if eval_return_type_allows_null(actual_type) && !eval_return_type_allows_null(expected_type) { + return false; + } + if actual_type.variants().is_empty() { + return eval_return_type_allows_null(expected_type); + } + if expected_type.variants().is_empty() { + return false; + } + if actual_type.is_intersection() { + return eval_return_type_accepts_actual_intersection( + expected_type, + expected_owner, + actual_type, + actual_owner, + pending_class, + context, + ); + } + actual_type.variants().iter().all(|actual_variant| { + eval_return_type_accepts_actual_variant( + expected_type, + expected_owner, + actual_variant, + actual_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether a return type can produce PHP null, including standalone `mixed`. +fn eval_return_type_allows_null(return_type: &EvalParameterType) -> bool { + return_type.allows_null() + || (!return_type.is_intersection() + && return_type + .variants() + .iter() + .any(|variant| matches!(variant, EvalParameterTypeVariant::Mixed))) +} + +/// Returns whether a return type is exactly PHP `never`. +fn eval_return_type_is_never(return_type: &EvalParameterType) -> bool { + !return_type.allows_null() + && !return_type.is_intersection() + && matches!(return_type.variants(), [EvalParameterTypeVariant::Never]) +} + +/// Returns whether a return type is exactly PHP `void`. +fn eval_return_type_is_void(return_type: &EvalParameterType) -> bool { + !return_type.allows_null() + && !return_type.is_intersection() + && matches!(return_type.variants(), [EvalParameterTypeVariant::Void]) +} + +/// Returns whether an expected type accepts an actual intersection return type. +fn eval_return_type_accepts_actual_intersection( + expected_type: &EvalParameterType, + expected_owner: &str, + actual_type: &EvalParameterType, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if expected_type.is_intersection() { + return expected_type.variants().iter().all(|expected_variant| { + eval_return_variant_accepts_actual_intersection( + expected_variant, + expected_owner, + actual_type, + actual_owner, + pending_class, + context, + ) + }); + } + expected_type.variants().iter().any(|expected_variant| { + eval_return_variant_accepts_actual_intersection( + expected_variant, + expected_owner, + actual_type, + actual_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether an expected type accepts one actual non-intersection atom. +fn eval_return_type_accepts_actual_variant( + expected_type: &EvalParameterType, + expected_owner: &str, + actual_variant: &EvalParameterTypeVariant, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if expected_type.is_intersection() { + return expected_type.variants().iter().all(|expected_variant| { + eval_return_variant_accepts_actual_variant( + expected_variant, + expected_owner, + actual_variant, + actual_owner, + pending_class, + context, + ) + }); + } + expected_type.variants().iter().any(|expected_variant| { + eval_return_variant_accepts_actual_variant( + expected_variant, + expected_owner, + actual_variant, + actual_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether one expected atom accepts an actual intersection return type. +fn eval_return_variant_accepts_actual_intersection( + expected_variant: &EvalParameterTypeVariant, + expected_owner: &str, + actual_type: &EvalParameterType, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + actual_type.variants().iter().any(|actual_variant| { + eval_return_variant_accepts_actual_variant( + expected_variant, + expected_owner, + actual_variant, + actual_owner, + pending_class, + context, + ) + }) +} + +/// Returns whether one expected non-null atom accepts one actual non-null atom. +fn eval_return_variant_accepts_actual_variant( + expected_variant: &EvalParameterTypeVariant, + expected_owner: &str, + actual_variant: &EvalParameterTypeVariant, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + match (expected_variant, actual_variant) { + (_, EvalParameterTypeVariant::Never) => true, + (EvalParameterTypeVariant::Never, _) => false, + (EvalParameterTypeVariant::Void, _) | (_, EvalParameterTypeVariant::Void) => false, + (EvalParameterTypeVariant::Mixed, _) => true, + (EvalParameterTypeVariant::Object, EvalParameterTypeVariant::Object) + | (EvalParameterTypeVariant::Array, EvalParameterTypeVariant::Array) + | (EvalParameterTypeVariant::Bool, EvalParameterTypeVariant::Bool) + | (EvalParameterTypeVariant::Callable, EvalParameterTypeVariant::Callable) + | (EvalParameterTypeVariant::Float, EvalParameterTypeVariant::Float) + | (EvalParameterTypeVariant::Int, EvalParameterTypeVariant::Int) + | (EvalParameterTypeVariant::Iterable, EvalParameterTypeVariant::Iterable) + | (EvalParameterTypeVariant::String, EvalParameterTypeVariant::String) => true, + (EvalParameterTypeVariant::Object, EvalParameterTypeVariant::Class(_)) => true, + (EvalParameterTypeVariant::Iterable, EvalParameterTypeVariant::Array) => true, + (EvalParameterTypeVariant::Iterable, EvalParameterTypeVariant::Class(actual_name)) => { + eval_return_class_type_is_a( + actual_name, + actual_owner, + "Traversable", + actual_owner, + pending_class, + context, + ) || eval_return_class_type_is_a( + actual_name, + actual_owner, + "Iterator", + actual_owner, + pending_class, + context, + ) + } + ( + EvalParameterTypeVariant::Class(expected_name), + EvalParameterTypeVariant::Class(actual_name), + ) => eval_return_class_type_accepts( + expected_name, + expected_owner, + actual_name, + actual_owner, + pending_class, + context, + ), + _ => false, + } +} + +/// Returns whether one declared class-like return atom is covariant with another. +fn eval_return_class_type_accepts( + expected_name: &str, + expected_owner: &str, + actual_name: &str, + actual_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if expected_name.eq_ignore_ascii_case("static") { + return actual_name.eq_ignore_ascii_case("static"); + } + if actual_name.eq_ignore_ascii_case("static") { + return eval_return_class_type_is_a( + actual_owner, + actual_owner, + expected_name, + expected_owner, + pending_class, + context, + ); + } + let Some(actual_resolved) = + eval_resolve_return_class_type_name(actual_name, actual_owner, pending_class, context) + else { + return false; + }; + eval_return_class_type_is_a( + &actual_resolved, + actual_owner, + expected_name, + expected_owner, + pending_class, + context, + ) || eval_resolve_return_class_type_name(expected_name, expected_owner, pending_class, context) + .is_some_and(|expected_resolved| actual_resolved.eq_ignore_ascii_case(&expected_resolved)) +} + +/// Returns whether an actual class-like return type satisfies an expected class-like target. +fn eval_return_class_type_is_a( + actual_name: &str, + actual_owner: &str, + expected_name: &str, + expected_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + let Some(actual_resolved) = + eval_resolve_return_class_type_name(actual_name, actual_owner, pending_class, context) + else { + return false; + }; + let Some(expected_resolved) = + eval_resolve_return_class_type_name(expected_name, expected_owner, pending_class, context) + else { + return false; + }; + if actual_resolved.eq_ignore_ascii_case(&expected_resolved) { + return true; + } + if pending_class.is_some_and(|class| class.name().eq_ignore_ascii_case(&actual_resolved)) { + return pending_class_return_type_is_a(pending_class, &expected_resolved, context); + } + if context.has_class(&actual_resolved) { + return context.class_is_a(&actual_resolved, &expected_resolved, false); + } + if context.has_interface(&actual_resolved) { + return context + .interface_parent_names(&actual_resolved) + .iter() + .any(|parent| parent.eq_ignore_ascii_case(&expected_resolved)); + } + false +} + +/// Returns whether the pending class declaration satisfies one expected class-like type. +fn pending_class_return_type_is_a( + pending_class: Option<&EvalClass>, + expected_name: &str, + context: &ElephcEvalContext, +) -> bool { + let Some(class) = pending_class else { + return false; + }; + if class.name().eq_ignore_ascii_case(expected_name) { + return true; + } + if class.parent().is_some_and(|parent| { + parent.eq_ignore_ascii_case(expected_name) + || context.class_is_a(parent, expected_name, false) + }) { + return true; + } + pending_class_return_interface_names(class, context) + .iter() + .any(|interface| interface.eq_ignore_ascii_case(expected_name)) +} + +/// Returns direct and inherited interface names for a pending eval class declaration. +fn pending_class_return_interface_names( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Vec { + let mut interfaces = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = class.parent() { + for interface in context.class_interface_names(parent) { + push_unique_return_class_name(&interface, &mut interfaces, &mut seen); + } + } + for interface in class.interfaces() { + push_unique_return_class_name(interface, &mut interfaces, &mut seen); + for parent in context.interface_parent_names(interface) { + push_unique_return_class_name(&parent, &mut interfaces, &mut seen); + } + } + interfaces +} + +/// Adds one class-like name once using PHP case-insensitive matching. +fn push_unique_return_class_name( + name: &str, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + let name = name.trim_start_matches('\\'); + if seen.insert(name.to_ascii_lowercase()) { + names.push(name.to_string()); + } +} + +/// Resolves `self`/`parent`/`static` and aliases in a declared return type atom. +fn eval_resolve_return_class_type_name( + type_name: &str, + owner_name: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> Option { + let owner_name = owner_name.trim_start_matches('\\'); + match type_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str() + { + "self" | "static" => Some(owner_name.to_string()), + "parent" => { + if let Some(class) = pending_class.filter(|class| { + class + .name() + .trim_start_matches('\\') + .eq_ignore_ascii_case(owner_name) + }) { + return class + .parent() + .map(|parent| parent.trim_start_matches('\\').to_string()); + } + context + .class(owner_name) + .and_then(EvalClass::parent) + .map(|parent| parent.trim_start_matches('\\').to_string()) + } + _ => Some( + context + .resolve_class_like_name(type_name) + .unwrap_or_else(|| type_name.trim_start_matches('\\').to_string()), + ), + } +} diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 91330d1a53..ad982b1e29 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1274,7 +1274,8 @@ fn validate_method_parent_override( let Some(parent) = class.parent() else { return Ok(()); }; - let Some((_, parent_method)) = context.class_method(parent, method.name()) else { + let Some((parent_declaring_class, parent_method)) = context.class_method(parent, method.name()) + else { return Ok(()); }; if parent_method.visibility() == EvalVisibility::Private { @@ -1294,14 +1295,28 @@ fn validate_method_parent_override( if method.is_abstract() && !parent_method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } - if !class_method_signature_accepts(method, &parent_method) { + if !class_method_signature_accepts( + method, + class.name(), + &parent_method, + &parent_declaring_class, + Some(class), + context, + ) { return Err(EvalStatus::RuntimeFatal); } Ok(()) } /// Returns whether one eval class method can accept every call accepted by its parent method. -fn class_method_signature_accepts(method: &EvalClassMethod, required: &EvalClassMethod) -> bool { +fn class_method_signature_accepts( + method: &EvalClassMethod, + method_owner: &str, + required: &EvalClassMethod, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { method_signature_accepts( method.params().len(), method.parameter_defaults(), @@ -1311,6 +1326,13 @@ fn class_method_signature_accepts(method: &EvalClassMethod, required: &EvalClass required.parameter_defaults(), required.parameter_is_by_ref(), required.parameter_is_variadic(), + ) && method_return_type_signature_accepts( + method.return_type(), + method_owner, + required.return_type(), + required_owner, + pending_class, + context, ) } @@ -1532,7 +1554,7 @@ fn validate_class_implements_eval_interface( context: &ElephcEvalContext, ) -> Result<(), EvalStatus> { for requirement in context.interface_method_requirements(interface_name) { - if !class_has_interface_method(class, &requirement, context) { + if !class_has_interface_method(class, interface_name, &requirement, context) { return Err(EvalStatus::RuntimeFatal); } } @@ -1547,6 +1569,7 @@ fn validate_class_implements_eval_interface( /// Returns whether a class or its eval parents satisfy one interface method signature. fn class_has_interface_method( class: &EvalClass, + interface_name: &str, requirement: &EvalInterfaceMethod, context: &ElephcEvalContext, ) -> bool { @@ -1554,23 +1577,41 @@ fn class_has_interface_method( return method.visibility() == EvalVisibility::Public && method.is_static() == requirement.is_static() && !method.is_abstract() - && class_method_satisfies_interface_signature(method, requirement); + && class_method_satisfies_interface_signature( + method, + class.name(), + requirement, + interface_name, + Some(class), + context, + ); } class .parent() .and_then(|parent| context.class_method(parent, requirement.name())) - .is_some_and(|(_, method)| { + .is_some_and(|(declaring_class, method)| { method.visibility() == EvalVisibility::Public && method.is_static() == requirement.is_static() && !method.is_abstract() - && class_method_satisfies_interface_signature(&method, requirement) + && class_method_satisfies_interface_signature( + &method, + &declaring_class, + requirement, + interface_name, + Some(class), + context, + ) }) } /// Returns whether one class method can accept every call required by an interface method. fn class_method_satisfies_interface_signature( method: &EvalClassMethod, + method_owner: &str, requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, ) -> bool { method_signature_accepts( method.params().len(), @@ -1581,6 +1622,13 @@ fn class_method_satisfies_interface_signature( requirement.parameter_defaults(), requirement.parameter_is_by_ref(), requirement.parameter_is_variadic(), + ) && method_return_type_signature_accepts( + method.return_type(), + method_owner, + requirement.return_type(), + requirement_owner, + pending_class, + context, ) } diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index bb34bb4445..699c55d9b2 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -893,6 +893,109 @@ class EvalArityChild extends EvalArityBase { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval accepts covariant method return type overrides. +#[test] +fn execute_program_accepts_covariant_method_return_type_overrides() { + let program = parse_fragment( + br#"class EvalReturnBase { + public function id(): ?int { return 1; } + public function make(): EvalReturnBase { return $this; } + public function selfType(): self { return $this; } +} +class EvalReturnChild extends EvalReturnBase { + public function id(): int { return 2; } + public function make(): EvalReturnChild { return $this; } + public function selfType(): static { return $this; } +} +class EvalReturnParentRoot {} +class EvalReturnParentBase extends EvalReturnParentRoot { + public function parentKeyword(): EvalReturnParentRoot { return new EvalReturnParentRoot(); } +} +class EvalReturnParentChild extends EvalReturnParentBase { + public function parentKeyword(): parent { return new EvalReturnParentBase(); } +} +class EvalReturnMixedBase { + public function maybe(): mixed { return null; } +} +class EvalReturnMixedChild extends EvalReturnMixedBase { + public function maybe(): ?int { return null; } +} +$child = new EvalReturnChild(); +return $child->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); +} + +/// Verifies eval rejects method overrides that widen declared return types. +#[test] +fn execute_program_rejects_incompatible_method_return_type_overrides() { + let wider_nullable = parse_fragment( + br#"class EvalReturnNarrowBase { + public function id(): int { return 1; } +} +class EvalReturnWiderNullable extends EvalReturnNarrowBase { + public function id(): ?int { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&wider_nullable, &mut scope, &mut values) + .expect_err("wider nullable return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let missing_return = parse_fragment( + br#"class EvalReturnRequiredBase { + public function label(): string { return "base"; } +} +class EvalReturnMissingChild extends EvalReturnRequiredBase { + public function label() { return "child"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&missing_return, &mut scope, &mut values) + .expect_err("missing return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let static_to_self = parse_fragment( + br#"class EvalReturnStaticBase { + public function make(): static { return $this; } +} +class EvalReturnSelfChild extends EvalReturnStaticBase { + public function make(): self { return $this; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&static_to_self, &mut scope, &mut values) + .expect_err("static return type should not widen to self"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let nullable_to_mixed = parse_fragment( + br#"class EvalReturnNullableBase { + public function maybe(): ?int { return null; } +} +class EvalReturnMixedChildBad extends EvalReturnNullableBase { + public function maybe(): mixed { return null; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&nullable_to_mixed, &mut scope, &mut values) + .expect_err("mixed return type should widen nullable int"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval rejects classes missing methods required by eval interfaces. #[test] fn execute_program_rejects_missing_dynamic_interface_method() { @@ -912,6 +1015,64 @@ class EvalMissingRead implements EvalNeedsRead {}"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval accepts covariant return types for interface method contracts. +#[test] +fn execute_program_accepts_covariant_interface_method_return_type() { + let program = parse_fragment( + br#"interface EvalReturnReadable { + function read(): int|string; +} +class EvalReturnReader implements EvalReturnReadable { + public function read(): int { + return 7; + } +} +$reader = new EvalReturnReader(); +return $reader->read();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies eval rejects missing or wider return types for interface method contracts. +#[test] +fn execute_program_rejects_incompatible_interface_method_return_type() { + let missing_return = parse_fragment( + br#"interface EvalNeedsReturn { + function read(): string; +} +class EvalMissingReturnImpl implements EvalNeedsReturn { + public function read() { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&missing_return, &mut scope, &mut values) + .expect_err("missing interface return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let wider_return = parse_fragment( + br#"interface EvalNeedsStringReturn { + function read(): string; +} +class EvalWiderReturnImpl implements EvalNeedsStringReturn { + public function read(): int|string { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&wider_return, &mut scope, &mut values) + .expect_err("wider interface return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval static interface method contracts are satisfied by public static methods. #[test] fn execute_program_accepts_static_dynamic_interface_method() { diff --git a/docs/php/eval.md b/docs/php/eval.md index a7f5044f34..951d0011a8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -148,6 +148,9 @@ properties, static methods, static interface method contracts, class, interface, trait, and enum constants including `final` constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and `__callStatic()`, and magic property fallback through `__get()` and `__set()`. +Eval validates method override and interface method return types with PHP-style +covariance for supported declared return type metadata, including nullable, +union, `mixed`, `self`, `parent`, `static`, class, and interface return types. `isset()`, `empty()`, and `unset()` on missing or inaccessible eval properties dispatch through `__isset()` and `__unset()` using PHP's `empty()` gate ordering. `instanceof` works with eval-declared classes and interfaces, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d98b694f91..d5fe022f85 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4918,6 +4918,146 @@ echo count($implements) . ":" . $implements["EvalDynNamedReader"] . ":" . $imple ); } +/// Verifies eval-declared method overrides enforce covariant return types. +#[test] +fn test_eval_declared_method_return_type_override_contracts() { + let out = compile_and_run_capture( + r#"id();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2"); + + let err = compile_and_run_expect_failure( + r#"read();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7"); + + let err = compile_and_run_expect_failure( + r#" Date: Sat, 20 Jun 2026 04:41:08 +0200 Subject: [PATCH 0476/1208] Preserve eval interface return type owners --- crates/elephc-eval/src/context.rs | 15 +++++++++++++-- crates/elephc-eval/src/interpreter/statements.rs | 12 +++++++----- .../src/interpreter/tests/expressions.rs | 9 +++++++++ tests/codegen/eval.rs | 9 +++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 472dabf07e..2671e2a723 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -1239,6 +1239,17 @@ impl ElephcEvalContext { /// Returns direct and inherited method requirements for an eval interface. pub fn interface_method_requirements(&self, interface_name: &str) -> Vec { + self.interface_method_requirements_with_owners(interface_name) + .into_iter() + .map(|(_, method)| method) + .collect() + } + + /// Returns direct and inherited method requirements with their declaring interface. + pub fn interface_method_requirements_with_owners( + &self, + interface_name: &str, + ) -> Vec<(String, EvalInterfaceMethod)> { let mut methods = Vec::new(); let mut seen_interfaces = HashSet::new(); let mut seen_methods = HashSet::new(); @@ -1255,7 +1266,7 @@ impl ElephcEvalContext { fn collect_interface_method_requirements( &self, interface_name: &str, - methods: &mut Vec, + methods: &mut Vec<(String, EvalInterfaceMethod)>, seen_interfaces: &mut HashSet, seen_methods: &mut HashSet, ) { @@ -1277,7 +1288,7 @@ impl ElephcEvalContext { for method in interface.methods() { let key = method.name().to_ascii_lowercase(); if seen_methods.insert(key) { - methods.push(method.clone()); + methods.push((interface.name().to_string(), method.clone())); } } } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index ad982b1e29..6a2fe702bf 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1553,8 +1553,10 @@ fn validate_class_implements_eval_interface( interface_name: &str, context: &ElephcEvalContext, ) -> Result<(), EvalStatus> { - for requirement in context.interface_method_requirements(interface_name) { - if !class_has_interface_method(class, interface_name, &requirement, context) { + for (requirement_owner, requirement) in + context.interface_method_requirements_with_owners(interface_name) + { + if !class_has_interface_method(class, &requirement_owner, &requirement, context) { return Err(EvalStatus::RuntimeFatal); } } @@ -1569,7 +1571,7 @@ fn validate_class_implements_eval_interface( /// Returns whether a class or its eval parents satisfy one interface method signature. fn class_has_interface_method( class: &EvalClass, - interface_name: &str, + requirement_owner: &str, requirement: &EvalInterfaceMethod, context: &ElephcEvalContext, ) -> bool { @@ -1581,7 +1583,7 @@ fn class_has_interface_method( method, class.name(), requirement, - interface_name, + requirement_owner, Some(class), context, ); @@ -1597,7 +1599,7 @@ fn class_has_interface_method( &method, &declaring_class, requirement, - interface_name, + requirement_owner, Some(class), context, ) diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 699c55d9b2..4dc6f0968e 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -1027,6 +1027,15 @@ class EvalReturnReader implements EvalReturnReadable { return 7; } } +interface EvalReturnRootSelf { + function linked(): self; +} +interface EvalReturnChildSelf extends EvalReturnRootSelf {} +class EvalReturnSelfImpl implements EvalReturnChildSelf { + public function linked(): EvalReturnRootSelf { + return $this; + } +} $reader = new EvalReturnReader(); return $reader->read();"#, ) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d5fe022f85..39bee4bc51 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5016,6 +5016,15 @@ class EvalReturnReader implements EvalReturnReadable { return 7; } } +interface EvalReturnRootSelf { + function linked(): self; +} +interface EvalReturnChildSelf extends EvalReturnRootSelf {} +class EvalReturnSelfImpl implements EvalReturnChildSelf { + public function linked(): EvalReturnRootSelf { + return $this; + } +} $reader = new EvalReturnReader(); echo $reader->read();'); "#, From 2415d428942f1a38b221d0607f82b60a91c268ee Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 04:59:33 +0200 Subject: [PATCH 0477/1208] Enforce eval method return values --- crates/elephc-eval/src/interpreter/control.rs | 1 + .../src/interpreter/dynamic_functions.rs | 25 +- .../src/interpreter/include_exec.rs | 1 + crates/elephc-eval/src/interpreter/mod.rs | 4 +- .../src/interpreter/return_values.rs | 316 ++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 60 ++-- .../src/interpreter/tests/expressions.rs | 87 +++++ docs/php/eval.md | 3 + tests/codegen/eval.rs | 82 +++++ 9 files changed, 542 insertions(+), 37 deletions(-) create mode 100644 crates/elephc-eval/src/interpreter/return_values.rs diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-eval/src/interpreter/control.rs index b14e1eedcd..bc4e2009a2 100644 --- a/crates/elephc-eval/src/interpreter/control.rs +++ b/crates/elephc-eval/src/interpreter/control.rs @@ -14,6 +14,7 @@ use crate::value::RuntimeCellHandle; /// Internal statement-control result used to propagate eval returns and loops. pub(super) enum EvalControl { None, + ReturnVoid, Return(RuntimeCellHandle), Throw(RuntimeCellHandle), Break, diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index d9b9eae339..bb990b5a49 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -628,7 +628,7 @@ fn eval_method_parameter_runtime_class_name( } /// Applies PHP weak-mode scalar coercion for supported scalar parameter types. -fn eval_method_parameter_scalar_coercion( +pub(in crate::interpreter) fn eval_method_parameter_scalar_coercion( variant: &EvalParameterTypeVariant, value: RuntimeCellHandle, context: &mut ElephcEvalContext, @@ -926,18 +926,19 @@ pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_ context, values, ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + function.return_type(), + None, + None, + control, + context, + values, + ), + }; context.pop_function(); - persist_result?; - writeback_result?; - match result? { - EvalControl::None => values.null(), - EvalControl::Return(result) => Ok(result), - EvalControl::Throw(result) => { - context.set_pending_throw(result); - Err(EvalStatus::UncaughtThrowable) - } - EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), - } + return_result } /// Persists static local variables from one eval-declared function activation. diff --git a/crates/elephc-eval/src/interpreter/include_exec.rs b/crates/elephc-eval/src/interpreter/include_exec.rs index 5730d81c6f..f2a0b9a25b 100644 --- a/crates/elephc-eval/src/interpreter/include_exec.rs +++ b/crates/elephc-eval/src/interpreter/include_exec.rs @@ -113,6 +113,7 @@ fn eval_execute_include_bytes( match eval_execute_include_code(&bytes[code_start..code_end], path, context, scope, values)? { EvalControl::None => {} + EvalControl::ReturnVoid => return values.null(), EvalControl::Return(value) => return Ok(value), EvalControl::Throw(value) => { context.set_pending_throw(value); diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index 54910e4d65..f70d756fd4 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -25,6 +25,7 @@ mod json; mod libc_shims; mod reflection; mod return_type_compat; +mod return_values; mod runtime_ops; mod scope_cells; mod statements; @@ -65,6 +66,7 @@ use libc_shims::*; use reflection::*; use regex::bytes::{Captures, Regex, RegexBuilder}; use return_type_compat::*; +use return_values::*; pub use runtime_ops::RuntimeValueOps; use runtime_ops::*; use scope_cells::*; @@ -110,7 +112,7 @@ pub fn execute_program_outcome_with_context( values: &mut impl RuntimeValueOps, ) -> Result { match execute_statements(program.statements(), context, scope, values) { - Ok(EvalControl::None) => values.null().map(EvalOutcome::Value), + Ok(EvalControl::None | EvalControl::ReturnVoid) => values.null().map(EvalOutcome::Value), Ok(EvalControl::Return(result)) => Ok(EvalOutcome::Value(result)), Ok(EvalControl::Throw(result)) => Ok(EvalOutcome::Throwable(result)), Ok(EvalControl::Break | EvalControl::Continue) => Err(EvalStatus::UnsupportedConstruct), diff --git a/crates/elephc-eval/src/interpreter/return_values.rs b/crates/elephc-eval/src/interpreter/return_values.rs new file mode 100644 index 0000000000..9a9d8fc1f3 --- /dev/null +++ b/crates/elephc-eval/src/interpreter/return_values.rs @@ -0,0 +1,316 @@ +//! Purpose: +//! Enforces declared eval function and method return values at runtime. +//! This keeps return-value checks separate from argument binding and statement dispatch. +//! +//! Called from: +//! - `crate::interpreter::dynamic_functions` +//! - `crate::interpreter::statements` +//! +//! Key details: +//! - `self` resolves to the declaring owner, while `static` resolves to the called class. +//! - Return values use weak scalar coercions like parameter binding, with dedicated handling for `void` and `never`. + +use super::*; + +/// Applies one declared function or method return type to a completed control result. +pub(in crate::interpreter) fn eval_declared_return_control_value( + return_type: Option<&EvalParameterType>, + return_owner: Option<&str>, + called_class_name: Option<&str>, + control: EvalControl, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match control { + EvalControl::None => eval_declared_implicit_return_value(return_type, values), + EvalControl::ReturnVoid => eval_declared_void_return_value(return_type, values), + EvalControl::Return(result) => eval_declared_explicit_return_value( + return_type, + return_owner, + called_class_name, + result, + context, + values, + ), + EvalControl::Throw(result) => { + context.set_pending_throw(result); + Err(EvalStatus::UncaughtThrowable) + } + EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Materializes an implicit return according to the declared return type. +fn eval_declared_implicit_return_value( + return_type: Option<&EvalParameterType>, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(return_type) = return_type else { + return values.null(); + }; + if eval_declared_return_type_is_void(return_type) { + return values.null(); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Materializes `return;` according to the declared return type. +fn eval_declared_void_return_value( + return_type: Option<&EvalParameterType>, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(return_type) = return_type else { + return values.null(); + }; + if eval_declared_return_type_is_void(return_type) { + return values.null(); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Validates or coerces an explicit returned value according to a declared return type. +fn eval_declared_explicit_return_value( + return_type: Option<&EvalParameterType>, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(return_type) = return_type else { + return Ok(value); + }; + if eval_declared_return_type_is_void(return_type) + || eval_declared_return_type_is_never(return_type) + { + return Err(EvalStatus::RuntimeFatal); + } + eval_declared_return_value( + return_type, + return_owner, + called_class_name, + value, + context, + values, + ) +} + +/// Applies a non-void declared return type to one returned runtime value. +fn eval_declared_return_value( + return_type: &EvalParameterType, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_declared_return_type_accepts_exact( + return_type, + return_owner, + called_class_name, + value, + context, + values, + )? { + return Ok(value); + } + if return_type.is_intersection() { + return Err(EvalStatus::RuntimeFatal); + } + for variant in return_type.variants() { + if let Some(coerced) = + eval_method_parameter_scalar_coercion(variant, value, context, values)? + { + return Ok(coerced); + } + } + Err(EvalStatus::RuntimeFatal) +} + +/// Returns whether a value already satisfies one declared return type. +fn eval_declared_return_type_accepts_exact( + return_type: &EvalParameterType, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + if tag == EVAL_TAG_NULL && eval_declared_return_type_allows_null(return_type) { + return Ok(true); + } + if return_type.is_intersection() { + for variant in return_type.variants() { + if !eval_declared_return_variant_accepts_exact( + variant, + return_owner, + called_class_name, + value, + tag, + context, + values, + )? { + return Ok(false); + } + } + return Ok(true); + } + for variant in return_type.variants() { + if eval_declared_return_variant_accepts_exact( + variant, + return_owner, + called_class_name, + value, + tag, + context, + values, + )? { + return Ok(true); + } + } + Ok(false) +} + +/// Returns whether one non-null return type atom accepts a runtime value exactly. +fn eval_declared_return_variant_accepts_exact( + variant: &EvalParameterTypeVariant, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + tag: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match variant { + EvalParameterTypeVariant::Array => Ok(matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC)), + EvalParameterTypeVariant::Bool => Ok(tag == EVAL_TAG_BOOL), + EvalParameterTypeVariant::Callable => Ok(matches!( + tag, + EVAL_TAG_STRING | EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT + )), + EvalParameterTypeVariant::Class(class_name) => eval_declared_return_class_accepts( + value, + tag, + class_name, + return_owner, + called_class_name, + context, + values, + ), + EvalParameterTypeVariant::Float => Ok(tag == EVAL_TAG_FLOAT), + EvalParameterTypeVariant::Int => Ok(tag == EVAL_TAG_INT), + EvalParameterTypeVariant::Iterable => { + if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Ok(true); + } + if eval_declared_return_class_accepts( + value, + tag, + "Traversable", + return_owner, + called_class_name, + context, + values, + )? { + return Ok(true); + } + eval_declared_return_class_accepts( + value, + tag, + "Iterator", + return_owner, + called_class_name, + context, + values, + ) + } + EvalParameterTypeVariant::Mixed => Ok(true), + EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void => Ok(false), + EvalParameterTypeVariant::Object => Ok(tag == EVAL_TAG_OBJECT), + EvalParameterTypeVariant::String => Ok(tag == EVAL_TAG_STRING), + } +} + +/// Returns whether an object value satisfies one class-like declared return target. +fn eval_declared_return_class_accepts( + value: RuntimeCellHandle, + tag: u64, + class_name: &str, + return_owner: Option<&str>, + called_class_name: Option<&str>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if tag != EVAL_TAG_OBJECT { + return Ok(false); + } + let target = eval_declared_return_runtime_class_name( + class_name, + return_owner, + called_class_name, + context, + )?; + let identity = values.object_identity(value)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(context.class_is_a(class.name(), &target, false)); + } + values.object_is_a(value, &target, false) +} + +/// Resolves class keywords and aliases in a declared return type atom. +fn eval_declared_return_runtime_class_name( + class_name: &str, + return_owner: Option<&str>, + called_class_name: Option<&str>, + context: &ElephcEvalContext, +) -> Result { + match class_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str() + { + "self" => return_owner + .map(|owner| owner.trim_start_matches('\\').to_string()) + .ok_or(EvalStatus::RuntimeFatal), + "static" => called_class_name + .or(return_owner) + .map(|owner| owner.trim_start_matches('\\').to_string()) + .ok_or(EvalStatus::RuntimeFatal), + "parent" => { + let owner = return_owner.ok_or(EvalStatus::RuntimeFatal)?; + context + .class(owner) + .and_then(EvalClass::parent) + .map(|parent| parent.trim_start_matches('\\').to_string()) + .ok_or(EvalStatus::RuntimeFatal) + } + _ => Ok(context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Returns whether a declared return type can accept PHP null. +fn eval_declared_return_type_allows_null(return_type: &EvalParameterType) -> bool { + return_type.allows_null() + || (!return_type.is_intersection() + && return_type + .variants() + .iter() + .any(|variant| matches!(variant, EvalParameterTypeVariant::Mixed))) +} + +/// Returns whether a declared return type is exactly PHP `never`. +fn eval_declared_return_type_is_never(return_type: &EvalParameterType) -> bool { + !return_type.allows_null() + && !return_type.is_intersection() + && matches!(return_type.variants(), [EvalParameterTypeVariant::Never]) +} + +/// Returns whether a declared return type is exactly PHP `void`. +fn eval_declared_return_type_is_void(return_type: &EvalParameterType) -> bool { + !return_type.allows_null() + && !return_type.is_intersection() + && matches!(return_type.variants(), [EvalParameterTypeVariant::Void]) +} diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 6a2fe702bf..e0dc2aa54b 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -184,7 +184,7 @@ pub(in crate::interpreter) fn execute_stmt( EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr( expr, context, scope, values, )?)), - EvalStmt::Return(None) => Ok(EvalControl::Return(values.null()?)), + EvalStmt::Return(None) => Ok(EvalControl::ReturnVoid), EvalStmt::ReferenceAssign { target, source } => { for replaced in set_reference_alias(context, scope, target, source, values)? { values.release(replaced)?; @@ -271,6 +271,7 @@ pub(in crate::interpreter) fn execute_stmt( EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } } @@ -328,7 +329,10 @@ pub(in crate::interpreter) fn release_overridden_control( ) -> Result<(), EvalStatus> { match control { EvalControl::Return(value) | EvalControl::Throw(value) => values.release(value), - EvalControl::None | EvalControl::Break | EvalControl::Continue => Ok(()), + EvalControl::None + | EvalControl::ReturnVoid + | EvalControl::Break + | EvalControl::Continue => Ok(()), } } @@ -3859,20 +3863,21 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( context, values, ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + method.return_type(), + Some(class_name), + Some(called_class_name), + control, + context, + values, + ), + }; context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); - persist_result?; - writeback_result?; - match result? { - EvalControl::None => values.null(), - EvalControl::Return(result) => Ok(result), - EvalControl::Throw(result) => { - context.set_pending_throw(result); - Err(EvalStatus::UncaughtThrowable) - } - EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), - } + return_result } /// Executes one eval-declared static class method without binding `$this`. @@ -3951,20 +3956,21 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_fla context, values, ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + method.return_type(), + Some(class_name), + Some(called_class_name), + control, + context, + values, + ), + }; context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); - persist_result?; - writeback_result?; - match result? { - EvalControl::None => values.null(), - EvalControl::Return(result) => Ok(result), - EvalControl::Throw(result) => { - context.set_pending_throw(result); - Err(EvalStatus::UncaughtThrowable) - } - EvalControl::Break | EvalControl::Continue => Err(EvalStatus::UnsupportedConstruct), - } + return_result } /// Wraps positional method arguments into the shared dynamic-call binding shape. @@ -4069,6 +4075,7 @@ pub(in crate::interpreter) fn execute_switch_stmt( EvalControl::None => {} EvalControl::Break | EvalControl::Continue => break, EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } } @@ -4088,6 +4095,7 @@ pub(in crate::interpreter) fn execute_do_while_stmt( EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } let condition = eval_expr(condition, context, scope, values)?; @@ -4112,6 +4120,7 @@ pub(in crate::interpreter) fn execute_for_stmt( EvalControl::None | EvalControl::Continue => {} EvalControl::Break => return Ok(EvalControl::None), EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } loop { @@ -4125,12 +4134,14 @@ pub(in crate::interpreter) fn execute_for_stmt( EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } match execute_statements(update, context, scope, values)? { EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } } @@ -4178,6 +4189,7 @@ pub(in crate::interpreter) fn execute_foreach_stmt( EvalControl::None | EvalControl::Continue => {} EvalControl::Break => break, EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), EvalControl::Return(result) => return Ok(EvalControl::Return(result)), } } diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-eval/src/interpreter/tests/expressions.rs index 4dc6f0968e..0f02001f40 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-eval/src/interpreter/tests/expressions.rs @@ -996,6 +996,93 @@ class EvalReturnMixedChildBad extends EvalReturnNullableBase { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval enforces declared method return values at runtime. +#[test] +fn execute_program_enforces_eval_method_return_type_values() { + let program = parse_fragment( + br#"class EvalReturnRuntimeBase { + public function id(): int { return "12"; } + public function makeSelf(): self { return new EvalReturnRuntimeBase(); } + public function done(): void { return; } +} +class EvalReturnRuntimeChild extends EvalReturnRuntimeBase {} +$child = new EvalReturnRuntimeChild(); +echo $child->id(); echo ":"; +echo get_class($child->makeSelf()); echo ":"; +$child->done(); +return 3;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12:EvalReturnRuntimeBase:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies eval rejects method return values that do not satisfy declarations. +#[test] +fn execute_program_rejects_invalid_eval_method_return_type_values() { + let bad_scalar = parse_fragment( + br#"class EvalReturnBadScalar { + public function id(): int { return "nope"; } +} +$box = new EvalReturnBadScalar(); +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_scalar, &mut scope, &mut values) + .expect_err("non-numeric string should fail int return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_void = parse_fragment( + br#"class EvalReturnBadVoid { + public function done(): void { return null; } +} +$box = new EvalReturnBadVoid(); +return $box->done();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_void, &mut scope, &mut values) + .expect_err("explicit value should fail void return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_static = parse_fragment( + br#"class EvalReturnStaticRuntimeBase { + public function make(): static { return new EvalReturnStaticRuntimeBase(); } +} +class EvalReturnStaticRuntimeChild extends EvalReturnStaticRuntimeBase {} +$child = new EvalReturnStaticRuntimeChild(); +return $child->make();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_static, &mut scope, &mut values) + .expect_err("base instance should fail inherited static return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let implicit_return = parse_fragment( + br#"class EvalReturnImplicitBad { + public function id(): ?int {} +} +$box = new EvalReturnImplicitBad(); +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&implicit_return, &mut scope, &mut values) + .expect_err("implicit return should fail non-void return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval rejects classes missing methods required by eval interfaces. #[test] fn execute_program_rejects_missing_dynamic_interface_method() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 951d0011a8..17edfebae0 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -151,6 +151,9 @@ trait, and enum constants including `final` constants, class-level attributes, Eval validates method override and interface method return types with PHP-style covariance for supported declared return type metadata, including nullable, union, `mixed`, `self`, `parent`, `static`, class, and interface return types. +Eval-declared method calls also enforce declared return values at runtime, with +weak scalar coercions and PHP-style handling for `void`, `never`, `self`, +`parent`, and late-bound `static`. `isset()`, `empty()`, and `unset()` on missing or inaccessible eval properties dispatch through `__isset()` and `__unset()` using PHP's `empty()` gate ordering. `instanceof` works with eval-declared classes and interfaces, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 39bee4bc51..ae6174db11 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5067,6 +5067,88 @@ class EvalWiderReturnImpl implements EvalNeedsStringReturn { ); } +/// Verifies eval-declared methods enforce declared return values at runtime. +#[test] +fn test_eval_declared_method_return_type_values() { + let out = compile_and_run_capture( + r#"id(); +echo ":" . get_class($child->makeSelf()) . ":"; +$child->done();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "12:EvalReturnRuntimeBase:"); + + let err = compile_and_run_expect_failure( + r#"id();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let err = compile_and_run_expect_failure( + r#"done();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let err = compile_and_run_expect_failure( + r#"make();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let err = compile_and_run_expect_failure( + r#"id();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies eval-declared abstract classes can defer interface methods to concrete children. #[test] fn test_eval_declared_abstract_class_and_final_method_contracts() { From 5685efac8934e3186213471bc610f2f3e02c9438 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 05:17:30 +0200 Subject: [PATCH 0478/1208] Clone AOT objects through eval bridge --- docs/php/eval.md | 15 +- src/codegen_support/runtime/eval_bridge.rs | 353 ++++++++++++++++----- tests/codegen/eval.rs | 37 +++ 3 files changed, 318 insertions(+), 87 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 17edfebae0..297ac5ff53 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | -| Object cloning | `clone $object` shallow-copies eval-declared objects and `stdClass` storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility. | +| Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | @@ -378,9 +378,10 @@ for supported static members, class constants, and class-name literals. Eval object construction can allocate eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime class metadata. Missing class names during eval object construction fail with an eval runtime fatal diagnostic. -`clone $object` creates a shallow copy for eval-declared objects and `stdClass` -objects. Eval `__clone()` hooks are invoked on the cloned object after storage -copying and use the same runtime visibility checks as method calls. +`clone $object` creates a shallow copy for eval-declared objects, `stdClass` +objects, and ordinary emitted/AOT objects. Eval `__clone()` hooks are invoked on +the cloned object after storage copying and use the same runtime visibility +checks as method calls. AOT and eval-declared class-name probes are visible through `class_exists()`. Eval object relation probes through `instanceof`, `is_a()`, and `is_subclass_of()` use @@ -540,9 +541,9 @@ and attribute slice, Reflection type APIs beyond retained parameter, property, and function/method return metadata, broader parameter default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed slice. Eval object cloning currently covers -eval-declared and `stdClass` objects; cloning emitted AOT objects through the -eval bridge is still outside that bridge slice. +non-by-reference fixed scalar/Mixed slice. Eval object cloning covers ordinary +emitted/AOT storage, but AOT `__clone()` hook dispatch through eval clone is +still outside that bridge slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 01876f4e4d..1dab3c3482 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -99,45 +99,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the dynamic-object wrapper frame emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust - label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); - emitter.instruction("sub sp, sp, #48"); // reserve clone source, destination, and wrapper frame slots - emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across clone helper calls - emitter.instruction("add x29, sp, #32"); // establish a stable clone wrapper frame pointer - emitter.instruction("cbz x0, __elephc_eval_value_object_clone_shallow_null"); // null handles cannot be cloned as objects - emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag - emitter.instruction("cmp x9, #6"); // tag 6 = object - emitter.instruction("b.ne __elephc_eval_value_object_clone_shallow_null"); // non-object values cannot be cloned by this bridge - emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer - emitter.instruction("cbz x9, __elephc_eval_value_object_clone_shallow_null"); // malformed object payloads cannot be cloned - abi::emit_symbol_address(emitter, "x10", "_stdclass_class_id"); - emitter.instruction("ldr x10, [x10]"); // load the compile-time stdClass class id - emitter.instruction("ldr x11, [x9]"); // load the object's runtime class id - emitter.instruction("cmp x11, x10"); // check whether the object is stdClass-backed - emitter.instruction("b.ne __elephc_eval_value_object_clone_shallow_null"); // non-stdClass objects need a broader clone bridge - emitter.instruction("ldr x10, [x9, #8]"); // load the source dynamic-property hash pointer - emitter.instruction("str x10, [sp, #0]"); // save the source hash pointer across allocation - emitter.instruction("bl __rt_stdclass_new"); // allocate a fresh stdClass shell for the clone - emitter.instruction("str x0, [sp, #8]"); // save the clone object pointer before hash fixup - emitter.instruction("ldr x10, [sp, #0]"); // reload the source dynamic-property hash pointer - emitter.instruction("cbz x10, __elephc_eval_value_object_clone_shallow_box"); // empty source hashes can keep the fresh shell hash - emitter.instruction("ldr x0, [x0, #8]"); // load the fresh shell's empty hash pointer - emitter.instruction("bl __rt_decref_any"); // release the fresh empty hash before installing the clone - emitter.instruction("ldr x0, [sp, #0]"); // reload the source dynamic-property hash for cloning - emitter.instruction("bl __rt_hash_clone_shallow"); // clone dynamic properties and retain nested values - emitter.instruction("ldr x9, [sp, #8]"); // reload the clone object pointer - emitter.instruction("str x0, [x9, #8]"); // install the cloned dynamic-property hash - emitter.label("__elephc_eval_value_object_clone_shallow_box"); - emitter.instruction("ldr x1, [sp, #8]"); // move the cloned object pointer into the Mixed payload - emitter.instruction("mov x0, #6"); // runtime tag 6 = object - emitter.instruction("mov x2, xzr"); // object payloads do not use a high word - emitter.instruction("bl __rt_mixed_from_value"); // box the cloned object for Rust - emitter.instruction("b __elephc_eval_value_object_clone_shallow_done"); // skip the null sentinel after a successful clone - emitter.label("__elephc_eval_value_object_clone_shallow_null"); - emitter.instruction("mov x0, xzr"); // return a null C pointer for unsupported clone inputs - emitter.label("__elephc_eval_value_object_clone_shallow_done"); - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #48"); // release the clone wrapper frame - emitter.instruction("ret"); // return the boxed clone or null failure sentinel + emit_aarch64_object_clone_shallow_wrapper(emitter); label_c_global(emitter, "__elephc_eval_class_exists"); emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class-name lookup state @@ -1558,47 +1520,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust - label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across clone calls - emitter.instruction("mov rbp, rsp"); // establish a stable clone wrapper frame pointer - emitter.instruction("sub rsp, 32"); // reserve source hash and clone object spill slots - emitter.instruction("test rdi, rdi"); // null handles cannot be cloned as objects - emitter.instruction("jz __elephc_eval_value_object_clone_shallow_null_x86"); // branch to the null sentinel for null handles - emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag - emitter.instruction("cmp r10, 6"); // tag 6 = object - emitter.instruction("jne __elephc_eval_value_object_clone_shallow_null_x86"); // non-object values cannot be cloned by this bridge - emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer - emitter.instruction("test r10, r10"); // malformed object payloads cannot be cloned - emitter.instruction("jz __elephc_eval_value_object_clone_shallow_null_x86"); // branch to the null sentinel for missing payloads - abi::emit_load_symbol_to_reg(emitter, "r11", "_stdclass_class_id", 0); - emitter.instruction("mov rax, QWORD PTR [r10]"); // load the object's runtime class id - emitter.instruction("cmp rax, r11"); // check whether the object is stdClass-backed - emitter.instruction("jne __elephc_eval_value_object_clone_shallow_null_x86"); // non-stdClass objects need a broader clone bridge - emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // load the source dynamic-property hash pointer - emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the source hash pointer across allocation - emitter.instruction("call __rt_stdclass_new"); // allocate a fresh stdClass shell for the clone - emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the clone object pointer before hash fixup - emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the source dynamic-property hash pointer - emitter.instruction("test r10, r10"); // empty source hashes can keep the fresh shell hash - emitter.instruction("jz __elephc_eval_value_object_clone_shallow_box_x86"); // skip hash replacement when the source hash is absent - emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // load the fresh shell's empty hash pointer - emitter.instruction("call __rt_decref_any"); // release the fresh empty hash before installing the clone - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload the source dynamic-property hash for cloning - emitter.instruction("call __rt_hash_clone_shallow"); // clone dynamic properties and retain nested values - emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the clone object pointer - emitter.instruction("mov QWORD PTR [r10 + 8], rax"); // install the cloned dynamic-property hash - emitter.label("__elephc_eval_value_object_clone_shallow_box_x86"); - emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // move the cloned object pointer into the Mixed payload - emitter.instruction("mov eax, 6"); // runtime tag 6 = object - emitter.instruction("xor esi, esi"); // object payloads do not use a high word - emitter.instruction("call __rt_mixed_from_value"); // box the cloned object for Rust - emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_done_x86"); // skip the null sentinel after a successful clone - emitter.label("__elephc_eval_value_object_clone_shallow_null_x86"); - emitter.instruction("xor eax, eax"); // return a null C pointer for unsupported clone inputs - emitter.label("__elephc_eval_value_object_clone_shallow_done_x86"); - emitter.instruction("add rsp, 32"); // release clone wrapper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the boxed clone or null failure sentinel + emit_x86_64_object_clone_shallow_wrapper(emitter); label_c_global(emitter, "__elephc_eval_class_exists"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer @@ -3628,6 +3550,277 @@ fn emit_x86_64_eval_name_table_exists( emitter.instruction("ret"); // return the metadata-name existence flag to Rust } +/// Emits the ARM64 eval bridge wrapper for cloning boxed object cells. +fn emit_aarch64_object_clone_shallow_wrapper(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); + emitter.instruction("sub sp, sp, #80"); // reserve source, clone, descriptor, counters, and wrapper frame slots + emitter.instruction("stp x29, x30, [sp, #64]"); // save frame pointer and return address across clone helper calls + emitter.instruction("add x29, sp, #64"); // establish a stable clone wrapper frame pointer + emitter.instruction("cbz x0, __elephc_eval_value_object_clone_shallow_null"); // null handles cannot be cloned as objects + emitter.instruction("ldr x9, [x0]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp x9, #6"); // tag 6 = object + emitter.instruction("b.ne __elephc_eval_value_object_clone_shallow_null"); // non-object values cannot be cloned by this bridge + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer + emitter.instruction("cbz x9, __elephc_eval_value_object_clone_shallow_null"); // malformed object payloads cannot be cloned + emitter.instruction("str x9, [sp, #0]"); // save the source object payload pointer + emitter.instruction("ldr x11, [x9]"); // load the object's runtime class id + emitter.instruction("str x11, [sp, #56]"); // save class id across allocation and ownership calls + emit_aarch64_reject_runtime_managed_clone_classes(emitter, "x11", "__elephc_eval_value_object_clone_shallow_null"); + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_count"); + emitter.instruction("ldr x10, [x10]"); // load the number of emitted class descriptors + emitter.instruction("cmp x11, x10"); // is this class id inside the descriptor table? + emitter.instruction("b.hs __elephc_eval_value_object_clone_shallow_null"); // unknown class layouts cannot be cloned by the eval bridge + abi::emit_symbol_address(emitter, "x10", "_class_gc_desc_ptrs"); + emitter.instruction("lsl x12, x11, #3"); // scale class id to an 8-byte descriptor pointer slot + emitter.instruction("ldr x10, [x10, x12]"); // load the class property-tag descriptor pointer + emitter.instruction("str x10, [sp, #16]"); // save descriptor pointer for the property-copy loop + emitter.instruction("ldr w12, [x9, #-16]"); // load the object payload size from the heap header + emitter.instruction("str x12, [sp, #48]"); // save payload size for allocation and dyn-prop detection + emitter.instruction("mov x0, x12"); // pass the source payload size to the heap allocator + emitter.instruction("bl __rt_heap_alloc"); // allocate a clone object payload with the same byte size + emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers + emitter.instruction("str x9, [x0, #-8]"); // stamp the uniform object heap header + emitter.instruction("ldr x11, [sp, #56]"); // reload the source class id + emitter.instruction("str x11, [x0]"); // store the class id at the clone payload head + emitter.instruction("str x0, [sp, #8]"); // save the clone object payload pointer + emitter.instruction("ldr x12, [sp, #48]"); // reload the payload size + emitter.instruction("sub x12, x12, #8"); // remove the leading class id field from the clone layout + emitter.instruction("lsr x12, x12, #4"); // derive declared-property slot count from the payload size + emitter.instruction("str x12, [sp, #24]"); // save property count for the copy loop + emitter.instruction("str xzr, [sp, #32]"); // initialize property-copy index to zero + + emitter.label("__elephc_eval_value_object_clone_shallow_prop_loop"); + emitter.instruction("ldr x12, [sp, #32]"); // reload the current property index + emitter.instruction("ldr x13, [sp, #24]"); // reload the declared-property slot count + emitter.instruction("cmp x12, x13"); // has every declared property slot been copied? + emitter.instruction("b.ge __elephc_eval_value_object_clone_shallow_dyn"); // move on to the optional dynamic-property hash + emitter.instruction("mov x10, #16"); // each declared-property slot is two 8-byte words + emitter.instruction("mul x10, x12, x10"); // compute the byte offset inside the property region + emitter.instruction("add x10, x10, #8"); // skip the leading class id to reach this slot + emitter.instruction("ldr x9, [sp, #0]"); // reload the source object pointer + emitter.instruction("ldr x11, [sp, #8]"); // reload the clone object pointer + emitter.instruction("ldr x15, [x9, x10]"); // copy the source property low word and keep it for retains + emitter.instruction("str x15, [x11, x10]"); // store the property low word on the clone + emitter.instruction("add x10, x10, #8"); // advance to the high word of the property slot + emitter.instruction("ldr x14, [x9, x10]"); // copy the source property high word + emitter.instruction("str x14, [x11, x10]"); // store the property high word on the clone + emitter.instruction("ldr x11, [sp, #16]"); // reload the property-tag descriptor pointer + emitter.instruction("ldrb w14, [x11, x12]"); // load the compile-time ownership tag for this slot + emitter.instruction("cmp x14, #1"); // does the slot hold a retained string payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained string slots need an extra owner reference + emitter.instruction("cmp x14, #4"); // does the slot hold a retained indexed-array payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained array slots need an extra owner reference + emitter.instruction("cmp x14, #5"); // does the slot hold a retained associative-array payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained hash slots need an extra owner reference + emitter.instruction("cmp x14, #6"); // does the slot hold a retained object payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained object slots need an extra owner reference + emitter.instruction("cmp x14, #7"); // does the slot hold a retained boxed Mixed payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained Mixed slots need an extra owner reference + emitter.instruction("b __elephc_eval_value_object_clone_shallow_next"); // scalar slots are copied without ownership changes + + emitter.label("__elephc_eval_value_object_clone_shallow_retain"); + emitter.instruction("str x12, [sp, #32]"); // preserve property index across the retain helper + emitter.instruction("mov x0, x15"); // pass the copied heap payload to the retain helper + emitter.instruction("bl __rt_incref"); // retain the shared property payload for the cloned object + emitter.instruction("ldr x12, [sp, #32]"); // restore property index after the retain helper + + emitter.label("__elephc_eval_value_object_clone_shallow_next"); + emitter.instruction("add x12, x12, #1"); // advance to the next declared-property slot + emitter.instruction("str x12, [sp, #32]"); // save the advanced property-copy index + emitter.instruction("b __elephc_eval_value_object_clone_shallow_prop_loop"); // continue copying declared properties + + emitter.label("__elephc_eval_value_object_clone_shallow_dyn"); + emitter.instruction("ldr x12, [sp, #48]"); // reload clone payload size for dynamic-property detection + emitter.instruction("sub x13, x12, #8"); // isolate bytes after the leading class id field + emitter.instruction("and x14, x13, #15"); // check whether an 8-byte dynamic-property tail exists + emitter.instruction("cmp x14, #8"); // remainder 8 means the layout has a dyn-props hash slot + emitter.instruction("b.ne __elephc_eval_value_object_clone_shallow_box"); // no dynamic hash slot: box the copied clone + emitter.instruction("sub x13, x12, #8"); // compute dyn-props slot offset as payload_size - 8 + emitter.instruction("str x13, [sp, #40]"); // save dyn-props slot offset across hash cloning + emitter.instruction("ldr x9, [sp, #0]"); // reload the source object pointer + emitter.instruction("ldr x10, [x9, x13]"); // load the source dynamic-property hash pointer + emitter.instruction("ldr x11, [sp, #8]"); // reload the clone object pointer + emitter.instruction("cbz x10, __elephc_eval_value_object_clone_shallow_dyn_null"); // null source hash stays null on the clone + emitter.instruction("mov x0, x10"); // pass the source dynamic hash to the clone helper + emitter.instruction("bl __rt_hash_clone_shallow"); // clone dynamic properties and retain nested values + emitter.instruction("ldr x13, [sp, #40]"); // restore the dynamic-property slot offset + emitter.instruction("ldr x11, [sp, #8]"); // reload the clone object pointer after hash cloning + emitter.instruction("str x0, [x11, x13]"); // install the cloned dynamic-property hash + emitter.instruction("b __elephc_eval_value_object_clone_shallow_box"); // box the clone after dynamic properties are installed + + emitter.label("__elephc_eval_value_object_clone_shallow_dyn_null"); + emitter.instruction("str xzr, [x11, x13]"); // clear the clone's dynamic-property hash slot + + emitter.label("__elephc_eval_value_object_clone_shallow_box"); + emitter.instruction("ldr x1, [sp, #8]"); // move the cloned object pointer into the Mixed payload + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the cloned object for Rust + emitter.instruction("b __elephc_eval_value_object_clone_shallow_done"); // skip the null sentinel after a successful clone + + emitter.label("__elephc_eval_value_object_clone_shallow_null"); + emitter.instruction("mov x0, xzr"); // return a null C pointer for unsupported clone inputs + emitter.label("__elephc_eval_value_object_clone_shallow_done"); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #80"); // release the clone wrapper frame + emitter.instruction("ret"); // return the boxed clone or null failure sentinel +} + +/// Emits the x86_64 eval bridge wrapper for cloning boxed object cells. +fn emit_x86_64_object_clone_shallow_wrapper(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across clone calls + emitter.instruction("mov rbp, rsp"); // establish a stable clone wrapper frame pointer + emitter.instruction("sub rsp, 64"); // reserve source, clone, descriptor, counters, and payload slots + emitter.instruction("test rdi, rdi"); // null handles cannot be cloned as objects + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_null_x86"); // branch to the null sentinel for null handles + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the boxed Mixed runtime tag + emitter.instruction("cmp r10, 6"); // tag 6 = object + emitter.instruction("jne __elephc_eval_value_object_clone_shallow_null_x86"); // non-object values cannot be cloned by this bridge + emitter.instruction("mov r10, QWORD PTR [rdi + 8]"); // load the object payload pointer + emitter.instruction("test r10, r10"); // malformed object payloads cannot be cloned + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_null_x86"); // branch to the null sentinel for missing payloads + emitter.instruction("mov QWORD PTR [rbp - 8], r10"); // save the source object payload pointer + emitter.instruction("mov rax, QWORD PTR [r10]"); // load the object's runtime class id + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save class id across allocation and ownership calls + emit_x86_64_reject_runtime_managed_clone_classes(emitter, "rax", "__elephc_eval_value_object_clone_shallow_null_x86"); + abi::emit_load_symbol_to_reg(emitter, "r11", "_class_gc_desc_count", 0); + emitter.instruction("cmp rax, r11"); // is this class id inside the descriptor table? + emitter.instruction("jae __elephc_eval_value_object_clone_shallow_null_x86"); // unknown class layouts cannot be cloned by the eval bridge + abi::emit_symbol_address(emitter, "r11", "_class_gc_desc_ptrs"); + emitter.instruction("mov r11, QWORD PTR [r11 + rax * 8]"); // load the class property-tag descriptor pointer + emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // save descriptor pointer for the property-copy loop + emitter.instruction("mov ecx, DWORD PTR [r10 - 16]"); // load the object payload size from the heap header + emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save payload size for allocation and dyn-prop detection + emitter.instruction("mov rax, rcx"); // pass the source payload size to the heap allocator + emitter.instruction("call __rt_heap_alloc"); // allocate a clone object payload with the same byte size + emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the uniform object heap header + emitter.instruction("mov rcx, QWORD PTR [rbp - 56]"); // reload the source class id + emitter.instruction("mov QWORD PTR [rax], rcx"); // store the class id at the clone payload head + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the clone object payload pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the payload size + emitter.instruction("sub r10, 8"); // remove the leading class id field from the clone layout + emitter.instruction("shr r10, 4"); // derive declared-property slot count from the payload size + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // save property count for the copy loop + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // initialize property-copy index to zero + + emitter.label("__elephc_eval_value_object_clone_shallow_prop_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current property index + emitter.instruction("cmp r10, QWORD PTR [rbp - 32]"); // has every declared property slot been copied? + emitter.instruction("jae __elephc_eval_value_object_clone_shallow_dyn_x86"); // move on to the optional dynamic-property hash + emitter.instruction("mov rcx, r10"); // copy property index before scaling it into a byte offset + emitter.instruction("shl rcx, 4"); // each declared-property slot is two 8-byte words + emitter.instruction("add rcx, 8"); // skip the leading class id to reach this slot + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the source object pointer + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the clone object pointer + emitter.instruction("mov rax, QWORD PTR [r11 + rcx]"); // copy the source property low word + emitter.instruction("mov rdx, QWORD PTR [r11 + rcx + 8]"); // copy the source property high word + emitter.instruction("mov QWORD PTR [r8 + rcx], rax"); // store the property low word on the clone + emitter.instruction("mov QWORD PTR [r8 + rcx + 8], rdx"); // store the property high word on the clone + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the property-tag descriptor pointer + emitter.instruction("movzx r11, BYTE PTR [r9 + r10]"); // load the compile-time ownership tag for this slot + emitter.instruction("cmp r11, 1"); // does the slot hold a retained string payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained string slots need an extra owner reference + emitter.instruction("cmp r11, 4"); // does the slot hold a retained indexed-array payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained array slots need an extra owner reference + emitter.instruction("cmp r11, 5"); // does the slot hold a retained associative-array payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained hash slots need an extra owner reference + emitter.instruction("cmp r11, 6"); // does the slot hold a retained object payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained object slots need an extra owner reference + emitter.instruction("cmp r11, 7"); // does the slot hold a retained boxed Mixed payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained Mixed slots need an extra owner reference + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_next_x86"); // scalar slots are copied without ownership changes + + emitter.label("__elephc_eval_value_object_clone_shallow_retain_x86"); + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // preserve property index across the retain helper + emitter.instruction("mov rdi, rax"); // pass the copied heap payload to the retain helper + emitter.instruction("call __rt_incref"); // retain the shared property payload for the cloned object + + emitter.label("__elephc_eval_value_object_clone_shallow_next_x86"); + emitter.instruction("add QWORD PTR [rbp - 40], 1"); // advance to the next declared-property slot + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_prop_loop_x86"); // continue copying declared properties + + emitter.label("__elephc_eval_value_object_clone_shallow_dyn_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload clone payload size for dynamic-property detection + emitter.instruction("mov r11, r10"); // copy payload size before deriving the property region size + emitter.instruction("sub r11, 8"); // isolate bytes after the leading class id field + emitter.instruction("and r11, 15"); // check whether an 8-byte dynamic-property tail exists + emitter.instruction("cmp r11, 8"); // remainder 8 means the layout has a dyn-props hash slot + emitter.instruction("jne __elephc_eval_value_object_clone_shallow_box_x86"); // no dynamic hash slot: box the copied clone + emitter.instruction("sub r10, 8"); // compute dyn-props slot offset as payload_size - 8 + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save dyn-props slot offset across hash cloning + emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the source object pointer + emitter.instruction("mov rax, QWORD PTR [r11 + r10]"); // load the source dynamic-property hash pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload the clone object pointer + emitter.instruction("test rax, rax"); // is the source dynamic-property hash present? + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_dyn_null_x86"); // null source hash stays null on the clone + emitter.instruction("mov rdi, rax"); // pass the source dynamic hash to the clone helper + emitter.instruction("call __rt_hash_clone_shallow"); // clone dynamic properties and retain nested values + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // restore the dynamic-property slot offset + emitter.instruction("mov r11, QWORD PTR [rbp - 16]"); // reload the clone object pointer after hash cloning + emitter.instruction("mov QWORD PTR [r11 + r10], rax"); // install the cloned dynamic-property hash + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_box_x86"); // box the clone after dynamic properties are installed + + emitter.label("__elephc_eval_value_object_clone_shallow_dyn_null_x86"); + emitter.instruction("mov QWORD PTR [r11 + r10], 0"); // clear the clone's dynamic-property hash slot + + emitter.label("__elephc_eval_value_object_clone_shallow_box_x86"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 16]"); // move the cloned object pointer into the Mixed payload + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("call __rt_mixed_from_value"); // box the cloned object for Rust + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_done_x86"); // skip the null sentinel after a successful clone + + emitter.label("__elephc_eval_value_object_clone_shallow_null_x86"); + emitter.instruction("xor eax, eax"); // return a null C pointer for unsupported clone inputs + emitter.label("__elephc_eval_value_object_clone_shallow_done_x86"); + emitter.instruction("add rsp, 64"); // release clone wrapper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed clone or null failure sentinel +} + +/// Emits ARM64 comparisons that keep runtime-managed object payloads out of the generic clone path. +fn emit_aarch64_reject_runtime_managed_clone_classes( + emitter: &mut Emitter, + class_id_reg: &str, + reject_label: &str, +) { + for symbol in [ + "_fiber_class_id", + "_generator_class_id", + "_spl_dll_class_id", + "_spl_stack_class_id", + "_spl_queue_class_id", + "_spl_fixed_array_class_id", + ] { + abi::emit_symbol_address(emitter, "x10", symbol); + emitter.instruction("ldr x10, [x10]"); // load one runtime-managed class id sentinel + emitter.instruction(&format!("cmp {}, x10", class_id_reg)); // compare source class id with the unsupported payload sentinel + emitter.instruction(&format!("b.eq {}", reject_label)); // reject custom runtime payload layouts + } +} + +/// Emits x86_64 comparisons that keep runtime-managed object payloads out of the generic clone path. +fn emit_x86_64_reject_runtime_managed_clone_classes( + emitter: &mut Emitter, + class_id_reg: &str, + reject_label: &str, +) { + for symbol in [ + "_fiber_class_id", + "_generator_class_id", + "_spl_dll_class_id", + "_spl_stack_class_id", + "_spl_queue_class_id", + "_spl_fixed_array_class_id", + ] { + abi::emit_load_symbol_to_reg(emitter, "r11", symbol, 0); + emitter.instruction(&format!("cmp {}, r11", class_id_reg)); // compare source class id with the unsupported payload sentinel + emitter.instruction(&format!("je {}", reject_label)); // reject custom runtime payload layouts + } +} + /// Emits a global label with platform C-symbol mangling. fn label_c_global(emitter: &mut Emitter, name: &str) { let symbol = emitter.target.extern_symbol(name); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ae6174db11..ff1938a272 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8363,6 +8363,43 @@ echo $second->name;'); assert_eq!(out, "A:A:clone:A:B"); } +/// Verifies eval `clone` shallow-copies ordinary emitted AOT objects. +#[test] +fn test_eval_clone_aot_object_expression() { + let out = compile_and_run( + r#"name = $name; + $this->count = $count; + } + + public function run(): void { + eval('$copy = clone $this; +$copy->name = $copy->name . ":copy"; +$copy->count = $copy->count + 10; +echo $this->name; echo ":"; +echo $this->count; echo ":"; +echo $copy->name; echo ":"; +echo $copy->count; echo ":"; +$plain = new stdClass(); +$plain->name = "S"; +$plainCopy = clone $plain; +$plainCopy->name = "S:copy"; +echo $plain->name; echo ":"; +echo $plainCopy->name;'); + } +} + +(new EvalCloneAotBox("A", 2))->run(); +"#, + ); + assert_eq!(out, "A:2:A:copy:12:S:S:copy"); +} + /// Verifies eval ReflectionClass::isIterable reports eval and builtin class metadata. #[test] fn test_eval_reflection_class_iterable_predicate() { From d3a69a03768cb35f709fbb233c5252a8bd887415 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 05:35:30 +0200 Subject: [PATCH 0479/1208] Invoke AOT clone hooks from eval --- .../elephc-eval/src/interpreter/reflection.rs | 12 +++++++ .../elephc-eval/src/interpreter/statements.rs | 36 +++++++++++++++++++ docs/php/eval.md | 10 +++--- src/codegen_support/runtime/eval_bridge.rs | 33 ++++++++++++++--- tests/codegen/eval.rs | 29 +++++++++++++++ tests/codegen/objects/cloning.rs | 19 ++++++++++ 6 files changed, 131 insertions(+), 8 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index b0dabdff80..3d89d404c8 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1528,6 +1528,18 @@ fn eval_reflection_aot_method_metadata_if_exists( ))) } +/// Returns generated/AOT method dispatch metadata for interpreter-only runtime decisions. +pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + Ok( + eval_reflection_aot_method_metadata_if_exists(class_name, method_name, values)? + .map(|member| (member.visibility, member.is_static, member.is_abstract)), + ) +} + /// Converts AOT method flag metadata into the eval ReflectionMethod shape. fn eval_reflection_aot_method_metadata( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index e0dc2aa54b..b927678f79 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2870,6 +2870,11 @@ pub(in crate::interpreter) fn eval_object_clone_result( if let Some((declaring_class, method)) = &clone_method { validate_eval_member_access(declaring_class, method.visibility(), context)?; } + let should_call_aot_clone_hook = if dynamic_class_name.is_none() { + eval_aot_clone_hook_is_callable(object, values)? + } else { + false + }; let clone = values.object_clone_shallow(object)?; if let Some(class_name) = dynamic_class_name { @@ -2887,10 +2892,41 @@ pub(in crate::interpreter) fn eval_object_clone_result( values, )?; } + } else if should_call_aot_clone_hook { + let result = values.method_call(clone, "__clone", Vec::new())?; + values.release(result)?; } Ok(clone) } +/// Returns whether a public instance AOT `__clone()` hook should run. +fn eval_aot_clone_hook_is_callable( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = eval_runtime_object_class_name(object, values)?; + let Some((visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&class_name, "__clone", values)? + else { + return Ok(false); + }; + if visibility != EvalVisibility::Public || is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + Ok(true) +} + +/// Reads the PHP-visible runtime class name for one AOT object handle. +fn eval_runtime_object_class_name( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name)?; + values.release(class_name)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + /// Creates a backing object for an eval-declared class without running its constructor. fn eval_dynamic_class_allocate_object( class: &EvalClass, diff --git a/docs/php/eval.md b/docs/php/eval.md index 297ac5ff53..f06335865d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | -| Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility. | +| Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility; public emitted/AOT `__clone()` hooks run through the method bridge. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | @@ -381,7 +381,8 @@ during eval object construction fail with an eval runtime fatal diagnostic. `clone $object` creates a shallow copy for eval-declared objects, `stdClass` objects, and ordinary emitted/AOT objects. Eval `__clone()` hooks are invoked on the cloned object after storage copying and use the same runtime visibility -checks as method calls. +checks as method calls. Public emitted/AOT `__clone()` hooks are invoked through +the generated method bridge after the clone storage has been copied. AOT and eval-declared class-name probes are visible through `class_exists()`. Eval object relation probes through `instanceof`, `is_a()`, and `is_subclass_of()` use @@ -542,8 +543,9 @@ and function/method return metadata, broader parameter default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed slice. Eval object cloning covers ordinary -emitted/AOT storage, but AOT `__clone()` hook dispatch through eval clone is -still outside that bridge slice. +emitted/AOT storage and public AOT `__clone()` hooks, but non-public AOT +`__clone()` scope checks and broader bridge signatures remain outside that +bridge slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 1dab3c3482..396e25e91d 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -3606,8 +3606,8 @@ fn emit_aarch64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("str x14, [x11, x10]"); // store the property high word on the clone emitter.instruction("ldr x11, [sp, #16]"); // reload the property-tag descriptor pointer emitter.instruction("ldrb w14, [x11, x12]"); // load the compile-time ownership tag for this slot - emitter.instruction("cmp x14, #1"); // does the slot hold a retained string payload? - emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained string slots need an extra owner reference + emitter.instruction("cmp x14, #1"); // does the slot hold an owned string payload? + emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_string"); // string slots need an independent payload copy emitter.instruction("cmp x14, #4"); // does the slot hold a retained indexed-array payload? emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained array slots need an extra owner reference emitter.instruction("cmp x14, #5"); // does the slot hold a retained associative-array payload? @@ -3618,6 +3618,21 @@ fn emit_aarch64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("b.eq __elephc_eval_value_object_clone_shallow_retain"); // retained Mixed slots need an extra owner reference emitter.instruction("b __elephc_eval_value_object_clone_shallow_next"); // scalar slots are copied without ownership changes + emitter.label("__elephc_eval_value_object_clone_shallow_string"); + emitter.instruction("str x12, [sp, #32]"); // preserve property index across string persistence + emitter.instruction("str x10, [sp, #40]"); // preserve the high-word slot offset across the helper + emitter.instruction("mov x1, x15"); // pass the source string pointer to the persistence helper + emitter.instruction("ldr x2, [x9, x10]"); // pass the source string length to the persistence helper + emitter.instruction("bl __rt_str_persist"); // duplicate the string payload for clone ownership + emitter.instruction("ldr x10, [sp, #40]"); // restore the high-word slot offset after persistence + emitter.instruction("ldr x11, [sp, #8]"); // reload the clone object pointer after persistence + emitter.instruction("sub x10, x10, #8"); // move back to the low-word string pointer slot + emitter.instruction("str x1, [x11, x10]"); // install the persisted string pointer on the clone + emitter.instruction("add x10, x10, #8"); // move to the high-word string length slot + emitter.instruction("str x2, [x11, x10]"); // install the persisted string length on the clone + emitter.instruction("ldr x12, [sp, #32]"); // restore property index after string persistence + emitter.instruction("b __elephc_eval_value_object_clone_shallow_next"); // continue with the next declared property + emitter.label("__elephc_eval_value_object_clone_shallow_retain"); emitter.instruction("str x12, [sp, #32]"); // preserve property index across the retain helper emitter.instruction("mov x0, x15"); // pass the copied heap payload to the retain helper @@ -3720,8 +3735,8 @@ fn emit_x86_64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [r8 + rcx + 8], rdx"); // store the property high word on the clone emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the property-tag descriptor pointer emitter.instruction("movzx r11, BYTE PTR [r9 + r10]"); // load the compile-time ownership tag for this slot - emitter.instruction("cmp r11, 1"); // does the slot hold a retained string payload? - emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained string slots need an extra owner reference + emitter.instruction("cmp r11, 1"); // does the slot hold an owned string payload? + emitter.instruction("je __elephc_eval_value_object_clone_shallow_string_x86"); // string slots need an independent payload copy emitter.instruction("cmp r11, 4"); // does the slot hold a retained indexed-array payload? emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained array slots need an extra owner reference emitter.instruction("cmp r11, 5"); // does the slot hold a retained associative-array payload? @@ -3732,6 +3747,16 @@ fn emit_x86_64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("je __elephc_eval_value_object_clone_shallow_retain_x86"); // retained Mixed slots need an extra owner reference emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_next_x86"); // scalar slots are copied without ownership changes + emitter.label("__elephc_eval_value_object_clone_shallow_string_x86"); + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // preserve property index across string persistence + emitter.instruction("mov QWORD PTR [rbp - 64], rcx"); // preserve the low-word slot offset across the helper + emitter.instruction("call __rt_str_persist"); // duplicate the string payload for clone ownership + emitter.instruction("mov rcx, QWORD PTR [rbp - 64]"); // restore the low-word slot offset after persistence + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the clone object pointer after persistence + emitter.instruction("mov QWORD PTR [r8 + rcx], rax"); // install the persisted string pointer on the clone + emitter.instruction("mov QWORD PTR [r8 + rcx + 8], rdx"); // install the persisted string length on the clone + emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_next_x86"); // continue with the next declared property + emitter.label("__elephc_eval_value_object_clone_shallow_retain_x86"); emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // preserve property index across the retain helper emitter.instruction("mov rdi, rax"); // pass the copied heap payload to the retain helper diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ff1938a272..7a276926c1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8400,6 +8400,35 @@ echo $plainCopy->name;'); assert_eq!(out, "A:2:A:copy:12:S:S:copy"); } +/// Verifies eval `clone` invokes public AOT `__clone()` hooks after storage copying. +#[test] +fn test_eval_clone_aot_object_runs_clone_hook() { + let out = compile_and_run( + r#"name = $name; + } + + public function __clone(): void { + $this->name = $this->name . ":hook"; + } + + public function run(): void { + eval('$copy = clone $this; +echo $this->name; echo ":"; +echo $copy->name;'); + } +} + +(new EvalCloneAotHookBox("A"))->run(); +"#, + ); + assert_eq!(out, "A:A:hook"); +} + /// Verifies eval ReflectionClass::isIterable reports eval and builtin class metadata. #[test] fn test_eval_reflection_class_iterable_predicate() { diff --git a/tests/codegen/objects/cloning.rs b/tests/codegen/objects/cloning.rs index 411ad1c015..bc4fba82f7 100644 --- a/tests/codegen/objects/cloning.rs +++ b/tests/codegen/objects/cloning.rs @@ -49,6 +49,25 @@ echo $a->n . "|" . $b->n; assert_eq!(out, "hook;1|11"); } +/// Verifies `__clone()` can replace a string property without corrupting the source object. +#[test] +fn test_clone_persists_string_property_before_magic_clone_mutation() { + let out = compile_and_run( + r#"label = $this->label . ":copy"; + } +} +$a = new LabelBox(); +$b = clone $a; +echo $a->label . "|" . $b->label; +"#, + ); + assert_eq!(out, "A|A:copy"); +} + /// Verifies object-valued properties are shallow-copied, so nested object mutations remain shared. #[test] fn test_clone_keeps_nested_objects_shared() { From 936badb180f006303164d5d0489dd6bd8806a92a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 05:50:37 +0200 Subject: [PATCH 0480/1208] Support object args in eval AOT method bridge --- docs/php/eval.md | 22 +++--- src/codegen/eval_method_helpers.rs | 119 +++++++++++++++++++++++++---- tests/codegen/eval.rs | 34 +++++++++ 3 files changed, 149 insertions(+), 26 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index f06335865d..66c689d004 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -75,7 +75,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility; public emitted/AOT `__clone()` hooks run through the method bridge. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -126,7 +126,7 @@ as callable. Static method callables can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method -fallback supports the same named-argument binding for public scalar/Mixed +fallback supports the same named-argument binding for public scalar/Mixed/object signatures supported by the generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -413,9 +413,9 @@ constants. Direct `new EnumName()` and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native -methods are bridged to eval. Public fixed scalar/Mixed method calls through -`$this->method(...)` are supported by the native method bridge, including -registered named arguments and string-keyed unpacking. +methods are bridged to eval. Public fixed scalar/Mixed/object method calls +through `$this->method(...)` are supported by the native method bridge, +including registered named arguments and string-keyed unpacking. ## Namespaces and constants @@ -530,10 +530,12 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are -still outside eval fragments. Runtime/AOT object-method, static-method, and -constructor fallback from eval remains limited to the generated public -non-by-reference fixed scalar/Mixed bridge slice, so variadic, by-reference, and -broader parameter/return ABI shapes are still outside that bridge. +still outside eval fragments. Runtime/AOT object-method and static-method +fallback from eval remain limited to the generated public non-by-reference fixed +scalar/Mixed/object bridge slice, while runtime/AOT constructor fallback remains +limited to public non-by-reference fixed scalar/Mixed signatures. Variadic, +by-reference, and broader parameter/return ABI shapes are still outside those +bridge paths. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported @@ -542,7 +544,7 @@ and attribute slice, Reflection type APIs beyond retained parameter, property, and function/method return metadata, broader parameter default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed slice. Eval object cloning covers ordinary +non-by-reference fixed scalar/Mixed/object slice. Eval object cloning covers ordinary emitted/AOT storage and public AOT `__clone()` hooks, but non-public AOT `__clone()` scope checks and broader bridge signatures remain outside that bridge slice. diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index c4c04c5fb1..5cf50898ff 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -9,7 +9,8 @@ //! - The cacheable runtime object cannot know user class ids, method symbols, //! or return types, so this bridge is emitted into the user assembly. //! - This method-call slice supports public AOT methods with fixed non-by-ref -//! scalar/Mixed argument lists and reports unsupported calls as runtime failure. +//! scalar/Mixed/object argument lists and reports unsupported calls as +//! runtime failure. use std::collections::BTreeMap; @@ -249,7 +250,12 @@ fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { fn method_param_supported(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), - PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str | PhpType::Mixed + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Object(_) ) } @@ -326,7 +332,7 @@ fn emit_static_method_call_aarch64( emitter.instruction("str x3, [sp, #40]"); // save the requested method-name length emit_aarch64_static_method_dispatch(module, emitter, data, slots); emitter.instruction(&format!("b {}", fail_label)); // no supported public static method matched the request - emit_aarch64_static_method_bodies(module, emitter, slots, done_label, fail_label); + emit_aarch64_static_method_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -354,7 +360,7 @@ fn emit_static_method_call_x86_64( emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save the requested method-name length emit_x86_64_static_method_dispatch(module, emitter, data, slots); emitter.instruction(&format!("jmp {}", fail_label)); // no supported public static method matched the request - emit_x86_64_static_method_bodies(module, emitter, slots, done_label, fail_label); + emit_x86_64_static_method_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -393,7 +399,7 @@ fn emit_method_call_aarch64( emit_aarch64_method_dispatch(module, emitter, data, slots); emitter.instruction(&format!("b {}", fail_label)); // no supported public method matched the request emit_aarch64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); - emit_aarch64_method_bodies(module, emitter, slots, done_label, fail_label); + emit_aarch64_method_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -434,7 +440,7 @@ fn emit_method_call_x86_64( emit_x86_64_method_dispatch(module, emitter, data, slots); emitter.instruction(&format!("jmp {}", fail_label)); // no supported public method matched the request emit_x86_64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); - emit_x86_64_method_bodies(module, emitter, slots, done_label, fail_label); + emit_x86_64_method_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -808,6 +814,7 @@ fn emit_x86_64_validate_builtin_throwable_method_arg_count( fn emit_aarch64_method_bodies( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalMethodSlot], done_label: &str, fail_label: &str, @@ -815,7 +822,8 @@ fn emit_aarch64_method_bodies( for slot in slots { emitter.label(&method_body_label(module, slot)); emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = emit_aarch64_prepare_method_args(module, emitter, slot); + let overflow_bytes = + emit_aarch64_prepare_method_args(module, emitter, data, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -831,6 +839,7 @@ fn emit_aarch64_method_bodies( fn emit_x86_64_method_bodies( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalMethodSlot], done_label: &str, fail_label: &str, @@ -838,7 +847,8 @@ fn emit_x86_64_method_bodies( for slot in slots { emitter.label(&method_body_label(module, slot)); emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = emit_x86_64_prepare_method_args(module, emitter, slot); + let overflow_bytes = + emit_x86_64_prepare_method_args(module, emitter, data, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -854,6 +864,7 @@ fn emit_x86_64_method_bodies( fn emit_aarch64_static_method_bodies( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalStaticMethodSlot], done_label: &str, fail_label: &str, @@ -861,7 +872,8 @@ fn emit_aarch64_static_method_bodies( for slot in slots { emitter.label(&static_method_body_label(module, slot)); emit_aarch64_validate_static_method_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = emit_aarch64_prepare_static_method_args(module, emitter, slot); + let overflow_bytes = + emit_aarch64_prepare_static_method_args(module, emitter, data, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -880,6 +892,7 @@ fn emit_aarch64_static_method_bodies( fn emit_x86_64_static_method_bodies( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalStaticMethodSlot], done_label: &str, fail_label: &str, @@ -887,7 +900,8 @@ fn emit_x86_64_static_method_bodies( for slot in slots { emitter.label(&static_method_body_label(module, slot)); emit_x86_64_validate_static_method_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = emit_x86_64_prepare_static_method_args(module, emitter, slot); + let overflow_bytes = + emit_x86_64_prepare_static_method_args(module, emitter, data, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -966,14 +980,16 @@ fn emit_x86_64_validate_static_method_arg_count( fn emit_aarch64_prepare_method_args( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slot: &EvalMethodSlot, + fail_label: &str, ) -> usize { let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first method argument abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { emit_aarch64_load_eval_arg(module, emitter, index); - emit_aarch64_cast_eval_arg(emitter, param_ty); + emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_method_args(module, emitter, &receiver_ty, &slot.params) @@ -983,13 +999,15 @@ fn emit_aarch64_prepare_method_args( fn emit_aarch64_prepare_static_method_args( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slot: &EvalStaticMethodSlot, + fail_label: &str, ) -> usize { abi::emit_load_int_immediate(emitter, "x0", slot.class_id as i64); abi::emit_push_result_value(emitter, &PhpType::Int); for (index, param_ty) in slot.params.iter().enumerate() { emit_aarch64_load_eval_arg(module, emitter, index); - emit_aarch64_cast_eval_arg(emitter, param_ty); + emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_static_method_args(module, emitter, slot) @@ -999,14 +1017,16 @@ fn emit_aarch64_prepare_static_method_args( fn emit_x86_64_prepare_method_args( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slot: &EvalMethodSlot, + fail_label: &str, ) -> usize { let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first method argument abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { emit_x86_64_load_eval_arg(module, emitter, index); - emit_x86_64_cast_eval_arg(emitter, param_ty); + emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_method_args(module, emitter, &receiver_ty, &slot.params) @@ -1016,13 +1036,15 @@ fn emit_x86_64_prepare_method_args( fn emit_x86_64_prepare_static_method_args( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slot: &EvalStaticMethodSlot, + fail_label: &str, ) -> usize { abi::emit_load_int_immediate(emitter, "rax", slot.class_id as i64); abi::emit_push_result_value(emitter, &PhpType::Int); for (index, param_ty) in slot.params.iter().enumerate() { emit_x86_64_load_eval_arg(module, emitter, index); - emit_x86_64_cast_eval_arg(emitter, param_ty); + emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_static_method_args(module, emitter, slot) @@ -1082,7 +1104,13 @@ fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usiz } /// Casts one boxed eval argument into ARM64 result registers for temporary staging. -fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { +fn emit_aarch64_cast_eval_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + param_ty: &PhpType, + fail_label: &str, +) { match param_ty.codegen_repr() { PhpType::Int => { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for integer coercion @@ -1103,12 +1131,44 @@ fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { PhpType::Mixed => { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for a Mixed method parameter } + PhpType::Object(class_name) => { + emit_aarch64_cast_eval_object_arg(module, emitter, data, &class_name, fail_label); + } _ => {} } } +/// Validates and unboxes one ARM64 object-typed eval argument for native method dispatch. +fn emit_aarch64_cast_eval_object_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + fail_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for object type validation + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emitter.instruction("mov x3, xzr"); // allow exact class matches for object type hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject values that fail the object type hint + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for object unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the object payload for the native method call + emitter.instruction("cmp x0, #6"); // object type hints require an object payload, not a class string + emitter.instruction(&format!("b.ne {}", fail_label)); // reject malformed non-object payloads + emitter.instruction("mov x0, x1"); // place the unboxed object pointer in the result register +} + /// Casts one boxed eval argument into x86_64 result registers for temporary staging. -fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { +fn emit_x86_64_cast_eval_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + param_ty: &PhpType, + fail_label: &str, +) { match param_ty.codegen_repr() { PhpType::Int => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion @@ -1129,10 +1189,37 @@ fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { PhpType::Mixed => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for a Mixed method parameter } + PhpType::Object(class_name) => { + emit_x86_64_cast_eval_object_arg(module, emitter, data, &class_name, fail_label); + } _ => {} } } +/// Validates and unboxes one x86_64 object-typed eval argument for native method dispatch. +fn emit_x86_64_cast_eval_object_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + fail_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for object type validation + abi::emit_symbol_address(emitter, "rsi", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emitter.instruction("xor ecx, ecx"); // allow exact class matches for object type hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction("test rax, rax"); // check whether the value satisfied the object type hint + emitter.instruction(&format!("je {}", fail_label)); // reject values that fail the object type hint + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for object unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the object payload for the native method call + emitter.instruction("cmp rax, 6"); // object type hints require an object payload, not a class string + emitter.instruction(&format!("jne {}", fail_label)); // reject malformed non-object payloads + emitter.instruction("mov rax, rdi"); // place the unboxed object pointer in the result register +} + /// Boxes the current native method result as the Mixed cell expected by eval. fn emit_box_method_result(module: &Module, emitter: &mut Emitter, return_ty: &PhpType) { if return_ty.codegen_repr() == PhpType::Void { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7a276926c1..97c2f99d41 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4473,6 +4473,40 @@ $box->run(); assert_eq!(out, "mixed-ok"); } +/// Verifies eval fragments can pass object-typed arguments to public AOT methods. +#[test] +fn test_eval_fragment_can_call_aot_method_with_object_arg() { + let out = compile_and_run( + r#"name = $name; + } +} + +class EvalMethodObjectArgBox { + public function describe(EvalMethodObjectArgItem $item): string { + return $item->name; + } + + public static function describeStatic(EvalMethodObjectArgItem $item): string { + return $item->name . "!"; + } + + public function run() { + $item = new EvalMethodObjectArgItem("Obj"); + return eval('return $this->describe($item) . ":" . EvalMethodObjectArgBox::describeStatic($item);'); + } +} + +echo (new EvalMethodObjectArgBox())->run(); +"#, + ); + assert_eq!(out, "Obj:Obj!"); +} + /// Verifies eval fragments can unpack numeric arrays into public AOT method calls. #[test] fn test_eval_fragment_can_call_this_public_method_with_spread_args() { From f85ce106d507b01b28b90e76bfce161c9a533e42 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 06:14:12 +0200 Subject: [PATCH 0481/1208] Propagate AOT class scope into eval --- crates/elephc-eval/src/context.rs | 22 ++ crates/elephc-eval/src/ffi/context.rs | 96 ++++++- crates/elephc-eval/src/ffi/native_methods.rs | 58 +++- crates/elephc-eval/src/ffi/tests.rs | 49 ++++ .../elephc-eval/src/interpreter/statements.rs | 1 + src/codegen/lower_inst/builtins/eval.rs | 254 +++++++++++++++++- tests/codegen/eval.rs | 71 +++++ 7 files changed, 538 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 2671e2a723..60707c8469 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -193,6 +193,7 @@ pub struct ElephcEvalContext { native_methods: HashMap<(String, String), NativeCallableSignature>, native_static_methods: HashMap<(String, String), NativeCallableSignature>, native_constructors: HashMap, + native_class_parents: HashMap, static_locals: HashMap<(String, String), RuntimeCellHandle>, static_properties: HashMap<(String, String), RuntimeCellHandle>, class_constants: HashMap<(String, String), RuntimeCellHandle>, @@ -241,6 +242,7 @@ impl ElephcEvalContext { native_methods: HashMap::new(), native_static_methods: HashMap::new(), native_constructors: HashMap::new(), + native_class_parents: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -290,6 +292,7 @@ impl ElephcEvalContext { native_methods: HashMap::new(), native_static_methods: HashMap::new(), native_constructors: HashMap::new(), + native_class_parents: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -1550,6 +1553,25 @@ impl ElephcEvalContext { .cloned() } + /// Defines generated AOT parent metadata for eval `parent::` resolution. + pub fn define_native_class_parent(&mut self, class_name: &str, parent_name: &str) -> bool { + let class_key = normalize_class_name(class_name); + let parent_name = parent_name.trim_start_matches('\\'); + if class_key.is_empty() || parent_name.is_empty() { + return false; + } + self.native_class_parents + .insert(class_key, parent_name.to_string()) + .is_none() + } + + /// Returns generated AOT parent metadata by PHP class name. + pub fn native_class_parent(&self, class_name: &str) -> Option<&str> { + self.native_class_parents + .get(&normalize_class_name(class_name)) + .map(String::as_str) + } + /// Returns true when the context has a dynamic or native function with this lowercase PHP name. pub fn has_function(&self, name: &str) -> bool { self.functions.contains_key(name) || self.native_functions.contains_key(name) diff --git a/crates/elephc-eval/src/ffi/context.rs b/crates/elephc-eval/src/ffi/context.rs index 690a8c7b61..8d4523a89f 100644 --- a/crates/elephc-eval/src/ffi/context.rs +++ b/crates/elephc-eval/src/ffi/context.rs @@ -1,7 +1,7 @@ //! Purpose: //! Exports eval context handle allocation and context metadata setters. -//! These functions manage process-level eval state and call-site/global-scope -//! metadata used while executing fragments. +//! These functions manage process-level eval state and call-site/global/class +//! scope metadata used while executing fragments. //! //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_context_*` symbols. @@ -73,6 +73,42 @@ pub unsafe extern "C" fn __elephc_eval_context_set_global_scope( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Enters a generated caller's class scope for the next eval fragment. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name pointers must be +/// readable UTF-8 slices for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_push_class_scope( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + called_class_ptr: *const u8, + called_class_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_context_push_class_scope_inner( + ctx, + class_ptr, + class_len, + called_class_ptr, + called_class_len, + ) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Leaves a generated caller class scope after an eval fragment returns. +/// +/// # Safety +/// `ctx` must be a valid eval context handle previously passed to +/// `__elephc_eval_context_push_class_scope`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_context_pop_class_scope(ctx: *mut ElephcEvalContext) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_context_pop_class_scope_inner(ctx) }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Runs the call-site metadata setter ABI body after installing a panic boundary. /// /// # Safety @@ -125,3 +161,59 @@ unsafe fn eval_context_set_global_scope_inner( } EvalStatus::Ok.code() } + +/// Runs the class-scope push ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_push_class_scope`; callers must pass a valid +/// context and readable UTF-8 class-name byte slices. +unsafe fn eval_context_push_class_scope_inner( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + called_class_ptr: *const u8, + called_class_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(class_name) = abi_name_to_string(class_ptr, class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(called_class_name) = abi_name_to_string(called_class_ptr, called_class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let class_name = class_name.trim_start_matches('\\').to_string(); + if class_name.is_empty() { + return EvalStatus::RuntimeFatal.code(); + } + let called_class_name = called_class_name.trim_start_matches('\\'); + let called_class_name = if called_class_name.is_empty() { + class_name.clone() + } else { + called_class_name.to_string() + }; + context.push_class_scope(class_name); + context.push_called_class_scope(called_class_name); + EvalStatus::Ok.code() +} + +/// Runs the class-scope pop ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_context_pop_class_scope`; callers must pass a valid +/// context handle created by the eval bridge. +unsafe fn eval_context_pop_class_scope_inner(ctx: *mut ElephcEvalContext) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + context.pop_called_class_scope(); + context.pop_class_scope(); + EvalStatus::Ok.code() +} diff --git a/crates/elephc-eval/src/ffi/native_methods.rs b/crates/elephc-eval/src/ffi/native_methods.rs index 5ddfa8ed2c..747d46df83 100644 --- a/crates/elephc-eval/src/ffi/native_methods.rs +++ b/crates/elephc-eval/src/ffi/native_methods.rs @@ -151,6 +151,31 @@ pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param( .unwrap_or(0) } +/// Registers generated native PHP parent-class metadata in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and parent name pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_class_parent( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + parent_name_ptr: *const u8, + parent_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_class_parent_inner( + ctx, + class_name_ptr, + class_name_len, + parent_name_ptr, + parent_name_len, + ) + }) + .unwrap_or(0) +} + /// Runs native method registration after installing a panic boundary. /// /// # Safety @@ -180,11 +205,7 @@ unsafe fn register_native_method_inner( }; let signature = NativeCallableSignature::new(param_count); if is_static { - i32::from(context.define_native_static_method_signature( - class_name, - method_name, - signature, - )) + i32::from(context.define_native_static_method_signature(class_name, method_name, signature)) } else { i32::from(context.define_native_method_signature(class_name, method_name, signature)) } @@ -299,6 +320,33 @@ unsafe fn register_native_constructor_param_inner( i32::from(context.define_native_constructor_param(&class_name, param_index, param_name)) } +/// Runs native parent-class registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_class_parent`; invalid handles or +/// names fail closed as `false`. +unsafe fn register_native_class_parent_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + parent_name_ptr: *const u8, + parent_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(parent_name) = abi_name_to_string(parent_name_ptr, parent_name_len) else { + return 0; + }; + i32::from(context.define_native_class_parent(&class_name, &parent_name)) +} + /// Splits one generated `ClassName::methodName` metadata key into class and method pieces. fn split_method_key(method_key: &str) -> Option<(&str, &str)> { let (class_name, method_name) = method_key.rsplit_once("::")?; diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs index 196b308871..fc5f836eb1 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -112,6 +112,34 @@ fn context_set_global_scope_records_handle() { ); } +/// Verifies generated class scopes are pushed and popped through the context ABI. +#[test] +fn context_push_class_scope_records_self_and_called_class() { + let mut ctx = ElephcEvalContext::new(); + let class_name = b"AotBase"; + let called_class_name = b"AotChild"; + + let push_status = unsafe { + __elephc_eval_context_push_class_scope( + &mut ctx, + class_name.as_ptr(), + class_name.len() as u64, + called_class_name.as_ptr(), + called_class_name.len() as u64, + ) + }; + + assert_eq!(push_status, EvalStatus::Ok.code()); + assert_eq!(ctx.current_class_scope(), Some("AotBase")); + assert_eq!(ctx.current_called_class_scope(), Some("AotChild")); + + let pop_status = unsafe { __elephc_eval_context_pop_class_scope(&mut ctx) }; + + assert_eq!(pop_status, EvalStatus::Ok.code()); + assert_eq!(ctx.current_class_scope(), None); + assert_eq!(ctx.current_called_class_scope(), None); +} + /// Verifies the function-exists ABI probes eval-declared functions by folded name. #[test] fn function_exists_reports_declared_eval_function() { @@ -313,6 +341,27 @@ fn register_native_methods_record_signature_metadata() { ); } +/// Verifies native AOT parent metadata is available for eval static-scope resolution. +#[test] +fn register_native_class_parent_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownChild"; + let parent = b"KnownParent"; + + let registered = unsafe { + __elephc_eval_register_native_class_parent( + &mut ctx, + class.as_ptr(), + class.len() as u64, + parent.as_ptr(), + parent.len() as u64, + ) + }; + + assert_eq!(registered, 1); + assert_eq!(ctx.native_class_parent("knownchild"), Some("KnownParent")); +} + /// Verifies scope allocation returns an empty opaque activation scope handle. #[test] fn scope_new_returns_empty_handle() { diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index b927678f79..f16afc4aeb 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2774,6 +2774,7 @@ pub(in crate::interpreter) fn resolve_eval_static_class_name( .class(current) .and_then(EvalClass::parent) .map(str::to_string) + .or_else(|| context.native_class_parent(current).map(str::to_string)) .ok_or(EvalStatus::RuntimeFatal) } _ => context diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 3fd0acaadf..6c90b3be0e 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -36,7 +36,7 @@ const EVAL_PARSE_ERROR_MESSAGE: &str = "Parse error: eval() fragment is invalid\ const EVAL_UNSUPPORTED_MESSAGE: &str = "Fatal error: eval() fragment uses an unsupported construct\n"; const EVAL_RUNTIME_FATAL_MESSAGE: &str = "Fatal error: eval() runtime failed\n"; -const EVAL_STACK_BYTES: usize = 80; +const EVAL_STACK_BYTES: usize = 96; const EVAL_RESULT_VALUE_CELL_OFFSET: usize = 8; const EVAL_RESULT_ERROR_OFFSET: usize = 16; const EVAL_CONTEXT_HANDLE_OFFSET: usize = 24; @@ -45,9 +45,12 @@ const EVAL_TEMP_CELL_OFFSET: usize = 40; const EVAL_CODE_PTR_OFFSET: usize = 48; const EVAL_CODE_LEN_OFFSET: usize = 56; const EVAL_GLOBAL_SCOPE_HANDLE_OFFSET: usize = 64; +const EVAL_CALLED_CLASS_PTR_OFFSET: usize = 72; +const EVAL_CALLED_CLASS_LEN_OFFSET: usize = 80; const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; const MAX_EVAL_NATIVE_METHOD_PARAMS: usize = 8; +const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; /// Local slot metadata needed for conservative eval scope synchronization. #[derive(Clone)] @@ -116,6 +119,7 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R flush_eval_global_scope(ctx, &sync_globals)?; mark_eval_scope_global_aliases(ctx, &global_aliases); set_eval_context_global_scope(ctx); + let pushed_class_scope = push_eval_context_class_scope(ctx)?; load_eval_context_to_arg(ctx, 0); load_eval_scope_to_arg(ctx, 1); move_saved_eval_code_to_eval_args(ctx); @@ -123,6 +127,7 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_execute"); abi::emit_call_label(ctx.emitter, &symbol); + pop_eval_context_class_scope(ctx, pushed_class_scope); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); @@ -453,6 +458,7 @@ fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context for registration in eval_native_constructor_registrations(ctx) { register_eval_native_constructor(ctx, context_offset, ®istration); } + register_eval_native_class_parents(ctx, context_offset); } /// Collects global PHP functions that can use the descriptor-invoker bridge. @@ -509,6 +515,27 @@ fn eval_native_constructor_registrations( registrations } +/// Registers generated AOT class parent metadata for eval `parent::` resolution. +fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_offset: usize) { + let mut parents = ctx + .module + .class_infos + .iter() + .filter_map(|(class_name, class_info)| { + let parent_name = class_info.parent.as_deref()?; + Some(( + class_info.class_id, + class_name.clone(), + parent_name.to_string(), + )) + }) + .collect::>(); + parents.sort_by_key(|(class_id, _, _)| *class_id); + for (_, class_name, parent_name) in parents { + register_eval_native_class_parent(ctx, context_offset, &class_name, &parent_name); + } +} + /// Adds eligible public instance methods for one class to eval signature registration. fn collect_eval_native_instance_methods( class_name: &str, @@ -758,7 +785,8 @@ fn register_eval_native_constructor( registration: &EvalNativeConstructorRegistration, ) { load_eval_context_local_to_arg(ctx, context_offset, 0); - let (class_name_label, class_name_len) = ctx.data.add_string(registration.class_name.as_bytes()); + let (class_name_label, class_name_len) = + ctx.data.add_string(registration.class_name.as_bytes()); abi::emit_symbol_address( ctx.emitter, abi::int_arg_reg_name(ctx.emitter.target, 1), @@ -791,6 +819,43 @@ fn register_eval_native_constructor( } } +/// Emits one native class-parent metadata registration call into the eval context. +fn register_eval_native_class_parent( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name: &str, + parent_name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let (class_name_label, class_name_len) = ctx.data.add_string(class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + let (parent_name_label, parent_name_len) = ctx.data.add_string(parent_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &parent_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + parent_name_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_class_parent"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native constructor parameter-name registration call. fn register_eval_native_constructor_param( ctx: &mut FunctionContext<'_>, @@ -979,6 +1044,183 @@ fn set_eval_context_global_scope(ctx: &mut FunctionContext<'_>) { emit_eval_status_check(ctx); } +/// Enters the current AOT method's class scope in the eval context, if any. +fn push_eval_context_class_scope(ctx: &mut FunctionContext<'_>) -> Result { + let Some(class_name) = current_eval_method_class(ctx).map(str::to_string) else { + return Ok(false); + }; + emit_eval_called_class_name_result(ctx, &class_name)?; + let (called_ptr_reg, called_len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, called_ptr_reg, EVAL_CALLED_CLASS_PTR_OFFSET); + abi::emit_store_to_sp(ctx.emitter, called_len_reg, EVAL_CALLED_CLASS_LEN_OFFSET); + load_eval_context_to_arg(ctx, 0); + let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_CALLED_CLASS_PTR_OFFSET, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + EVAL_CALLED_CLASS_LEN_OFFSET, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_push_class_scope"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + Ok(true) +} + +/// Leaves a pushed eval class scope while preserving the original eval status. +fn pop_eval_context_class_scope(ctx: &mut FunctionContext<'_>, pushed: bool) { + if !pushed { + return; + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + load_eval_context_to_arg(ctx, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_context_pop_class_scope"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); +} + +/// Returns the lexical class encoded in the current EIR method name. +fn current_eval_method_class<'a>(ctx: &'a FunctionContext<'_>) -> Option<&'a str> { + ctx.function + .flags + .is_method + .then(|| { + ctx.function + .name + .rsplit_once("::") + .map(|(class_name, _)| class_name) + }) + .flatten() +} + +/// Materializes the runtime called-class name for eval `static::` resolution. +fn emit_eval_called_class_name_result( + ctx: &mut FunctionContext<'_>, + fallback_class: &str, +) -> Result<()> { + if eval_late_static_class_id_available(ctx) { + match ctx.emitter.target.arch { + Arch::AArch64 => emit_eval_called_class_name_result_aarch64(ctx), + Arch::X86_64 => emit_eval_called_class_name_result_x86_64(ctx), + } + } else { + emit_eval_static_string_result(ctx, fallback_class.as_bytes()); + Ok(()) + } +} + +/// Emits the AArch64 class-id table lookup for eval's called class. +fn emit_eval_called_class_name_result_aarch64(ctx: &mut FunctionContext<'_>) -> Result<()> { + let missing = ctx.next_label("eval_called_class_missing"); + let done = ctx.next_label("eval_called_class_done"); + emit_eval_late_static_class_id_to_reg(ctx, "x12")?; + abi::emit_load_symbol_to_reg(ctx.emitter, "x10", "_class_name_count", 0); + ctx.emitter.instruction("cmp x12, x10"); // reject called-class ids outside the class-name table + ctx.emitter.instruction(&format!("b.hs {}", missing)); // fall back to the lexical eval class when metadata is missing + abi::emit_symbol_address(ctx.emitter, "x11", "_class_name_entries"); + ctx.emitter.instruction("lsl x12, x12, #4"); // convert class id to a 16-byte class-name table offset + ctx.emitter.instruction("add x11, x11, x12"); // select the called-class metadata row + ctx.emitter.instruction("ldr x1, [x11]"); // load the called-class name pointer + ctx.emitter.instruction("ldr x2, [x11, #8]"); // load the called-class name length + ctx.emitter.instruction(&format!("b {}", done)); // skip the missing-metadata fallback + ctx.emitter.label(&missing); + abi::emit_symbol_address(ctx.emitter, "x1", "_class_name_missing"); + ctx.emitter.instruction("mov x2, #0"); // empty called-class name triggers lexical fallback in eval + ctx.emitter.label(&done); + Ok(()) +} + +/// Emits the x86_64 class-id table lookup for eval's called class. +fn emit_eval_called_class_name_result_x86_64(ctx: &mut FunctionContext<'_>) -> Result<()> { + let missing = ctx.next_label("eval_called_class_missing"); + let done = ctx.next_label("eval_called_class_done"); + emit_eval_late_static_class_id_to_reg(ctx, "r8")?; + abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_class_name_count", 0); + ctx.emitter.instruction("cmp r8, r9"); // reject called-class ids outside the class-name table + ctx.emitter.instruction(&format!("jae {}", missing)); // fall back to the lexical eval class when metadata is missing + abi::emit_symbol_address(ctx.emitter, "r10", "_class_name_entries"); + ctx.emitter.instruction("shl r8, 4"); // convert class id to a 16-byte class-name table offset + ctx.emitter.instruction("add r10, r8"); // select the called-class metadata row + ctx.emitter.instruction("mov rax, QWORD PTR [r10]"); // load the called-class name pointer + ctx.emitter.instruction("mov rdx, QWORD PTR [r10 + 8]"); // load the called-class name length + ctx.emitter.instruction(&format!("jmp {}", done)); // skip the missing-metadata fallback + ctx.emitter.label(&missing); + abi::emit_symbol_address(ctx.emitter, "rax", "_class_name_missing"); + ctx.emitter.instruction("mov rdx, 0"); // empty called-class name triggers lexical fallback in eval + ctx.emitter.label(&done); + Ok(()) +} + +/// Returns true when the current method frame can provide a late-static class id. +fn eval_late_static_class_id_available(ctx: &FunctionContext<'_>) -> bool { + ctx.local_slot_by_name(CALLED_CLASS_ID_PARAM).is_some() + || ctx.local_slot_by_name("this").is_some() +} + +/// Loads the late-static class id from the hidden static slot or `$this`. +fn emit_eval_late_static_class_id_to_reg(ctx: &mut FunctionContext<'_>, reg: &str) -> Result<()> { + if let Some(slot) = ctx.local_slot_by_name(CALLED_CLASS_ID_PARAM) { + let offset = ctx.local_offset(slot)?; + abi::load_at_offset(ctx.emitter, reg, offset); + return Ok(()); + } + if let Some(slot) = ctx.local_slot_by_name("this") { + match ctx.local_php_type(slot)? { + PhpType::Mixed | PhpType::Union(_) => { + ctx.load_local_to_result(slot)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + let object_reg = eval_mixed_unbox_low_payload_reg(ctx); + abi::emit_load_from_address(ctx.emitter, reg, object_reg, 0); + } + PhpType::Object(_) => { + let offset = ctx.local_offset(slot)?; + abi::load_at_offset(ctx.emitter, reg, offset); + abi::emit_load_from_address(ctx.emitter, reg, reg, 0); + } + other => { + return Err(CodegenIrError::invalid_module(format!( + "eval class scope this local has PHP type {:?}", + other + ))) + } + } + return Ok(()); + } + Err(CodegenIrError::invalid_module(format!( + "eval class scope without called-class source in {}", + ctx.function.name + ))) +} + +/// Emits a static string result for eval class-scope setup fallback paths. +fn emit_eval_static_string_result(ctx: &mut FunctionContext<'_>, bytes: &[u8]) { + let (label, len) = ctx.data.add_string(bytes); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); +} + /// Collects PHP-visible locals that the current conservative scope sync can round-trip. fn eval_sync_locals(ctx: &FunctionContext<'_>) -> Vec { ctx.function @@ -1596,12 +1838,12 @@ fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter - .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code + .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler } Arch::X86_64 => { ctx.emitter - .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code + .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler } } @@ -1634,7 +1876,7 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); ctx.emitter - .instruction(&format!("mov x2, #{}", message_len)); // pass the eval runtime diagnostic byte length + .instruction(&format!("mov x2, #{}", message_len)); // pass the eval runtime diagnostic byte length ctx.emitter.syscall(4); abi::emit_exit(ctx.emitter, 1); } @@ -1642,7 +1884,7 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); ctx.emitter - .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length + .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting abi::emit_exit(ctx.emitter, 1); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 97c2f99d41..4dd660d6ea 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4507,6 +4507,77 @@ echo (new EvalMethodObjectArgBox())->run(); assert_eq!(out, "Obj:Obj!"); } +/// Verifies eval fragments inherit lexical `self::` from an AOT instance method. +#[test] +fn test_eval_fragment_in_aot_method_resolves_self_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "EvalAotScopeSelfBox:self"); +} + +/// Verifies eval fragments inherit late-static `static::` from an AOT instance method. +#[test] +fn test_eval_fragment_in_aot_method_resolves_late_static_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "EvalAotScopeStaticBase:EvalAotScopeStaticChild:child"); +} + +/// Verifies eval fragments resolve `parent::` through AOT parent metadata. +#[test] +fn test_eval_fragment_in_aot_method_resolves_parent_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "parent"); +} + /// Verifies eval fragments can unpack numeric arrays into public AOT method calls. #[test] fn test_eval_fragment_can_call_this_public_method_with_spread_args() { From d82220036ccbe1b13d5850c14853de8ac0373dde Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 20 Jun 2026 06:34:19 +0200 Subject: [PATCH 0482/1208] Fix eval return type inference for inherited methods --- src/types/checker/inference/syntactic.rs | 20 ++++++++++++++++++++ tests/codegen/eval.rs | 17 +++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/types/checker/inference/syntactic.rs b/src/types/checker/inference/syntactic.rs index e75a4c12ff..d0a8b08213 100644 --- a/src/types/checker/inference/syntactic.rs +++ b/src/types/checker/inference/syntactic.rs @@ -255,6 +255,7 @@ pub fn infer_expr_type_syntactic(expr: &Expr) -> PhpType { .. } => PhpType::Bool, ExprKind::FunctionCall { name, args } => match name.as_str() { + "eval" => PhpType::Mixed, "substr" | "strtolower" | "strtoupper" | "trim" | "ltrim" | "rtrim" | "str_repeat" | "strrev" | "chr" | "str_replace" | "str_ireplace" | "ucfirst" | "lcfirst" | "ucwords" | "str_pad" | "implode" | "sprintf" | "vsprintf" | "nl2br" | "wordwrap" | "md5" @@ -508,6 +509,25 @@ fn merge_array_literal_element_type_syntactic(existing: PhpType, next: PhpType) #[cfg(test)] mod tests { use super::*; + use crate::names::Name; + use crate::span::Span; + + /// Verifies syntactic type inference treats eval as a runtime Mixed value. + #[test] + fn test_syntactic_eval_return_type_is_mixed() { + let expr = Expr { + kind: ExprKind::FunctionCall { + name: Name::unqualified("eval"), + args: vec![Expr { + kind: ExprKind::StringLiteral("return 1;".to_string()), + span: Span::dummy(), + }], + }, + span: Span::dummy(), + }; + + assert_eq!(infer_expr_type_syntactic(&expr), PhpType::Mixed); + } /// Verifies syntactic indexed plus assoc array union type. #[test] diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4dd660d6ea..be8db83a66 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9185,6 +9185,23 @@ echo eval('$box = new EvalDynamicNewManyArgCtor(1, 2, 3, "!"); return $box->labe assert_eq!(out, "6!"); } +/// Verifies inherited AOT methods returning eval results keep the boxed Mixed return ABI. +#[test] +fn test_eval_fragment_in_inherited_aot_method_returns_late_static_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "EvalInheritedAotScopeReturnChild"); +} + /// Verifies eval ReflectionClass::newInstanceArgs forwards named args to AOT constructors. #[test] fn test_eval_reflection_class_new_instance_args_constructs_aot_class() { From def185c962ec53fda0bd63b8d39f1835614389bd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 19:24:20 +0200 Subject: [PATCH 0483/1208] Support eval AOT callable defaults --- crates/elephc-eval/src/context.rs | 79 ++++ crates/elephc-eval/src/ffi/native_methods.rs | 369 +++++++++++++++++- crates/elephc-eval/src/ffi/tests.rs | 52 +++ .../src/interpreter/expressions.rs | 1 + crates/elephc-eval/src/interpreter/mod.rs | 4 +- .../elephc-eval/src/interpreter/reflection.rs | 2 + .../elephc-eval/src/interpreter/statements.rs | 56 ++- .../src/interpreter/tests/dynamic_calls.rs | 24 ++ docs/php/eval.md | 4 +- src/codegen/lower_inst/builtins/eval.rs | 278 +++++++++++-- tests/codegen/eval.rs | 61 +++ 11 files changed, 891 insertions(+), 39 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 60707c8469..c4fc4738c6 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -114,11 +114,22 @@ impl NativeFunction { } } +/// Scalar default value for a native AOT callable parameter visible to eval fragments. +#[derive(Clone, Debug, PartialEq)] +pub enum NativeCallableDefault { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), +} + /// Native AOT method or constructor signature metadata visible to eval fragments. #[derive(Clone)] pub struct NativeCallableSignature { param_count: usize, param_names: Vec, + param_defaults: Vec>, } impl NativeCallableSignature { @@ -127,6 +138,7 @@ impl NativeCallableSignature { Self { param_count, param_names: Vec::new(), + param_defaults: Vec::new(), } } @@ -147,10 +159,39 @@ impl NativeCallableSignature { true } + /// Records a PHP scalar default value for one positional callable slot. + pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { + if index >= self.param_count { + return false; + } + if self.param_defaults.len() < self.param_count { + self.param_defaults.resize(self.param_count, None); + } + self.param_defaults[index] = Some(default); + true + } + /// Returns the PHP-visible parameter names registered for this callable. pub fn param_names(&self) -> &[String] { &self.param_names } + + /// Returns the PHP-visible scalar parameter defaults registered for this callable. + pub fn param_defaults(&self) -> &[Option] { + &self.param_defaults + } + + /// Returns the registered scalar default for one parameter slot, if any. + pub fn param_default(&self, index: usize) -> Option<&NativeCallableDefault> { + self.param_defaults.get(index).and_then(Option::as_ref) + } + + /// Returns the minimum number of arguments required by registered defaults. + pub fn required_param_count(&self) -> usize { + (0..self.param_count) + .rfind(|index| self.param_default(*index).is_none()) + .map_or(0, |index| index + 1) + } } /// PHP class-like declaration kind targeted by a dynamic `class_alias()`. @@ -1496,6 +1537,19 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_name(index, param_name)) } + /// Records one parameter default for registered native AOT instance-method metadata. + pub fn define_native_method_param_default( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + /// Records one parameter name for registered native AOT static-method metadata. pub fn define_native_static_method_param( &mut self, @@ -1509,6 +1563,19 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_name(index, param_name)) } + /// Records one parameter default for registered native AOT static-method metadata. + pub fn define_native_static_method_param_default( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + /// Records one parameter name for registered native AOT constructor metadata. pub fn define_native_constructor_param( &mut self, @@ -1521,6 +1588,18 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_name(index, param_name)) } + /// Records one parameter default for registered native AOT constructor metadata. + pub fn define_native_constructor_param_default( + &mut self, + class_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + /// Returns native AOT instance-method signature metadata by PHP class and method name. pub fn native_method_signature( &self, diff --git a/crates/elephc-eval/src/ffi/native_methods.rs b/crates/elephc-eval/src/ffi/native_methods.rs index 747d46df83..bdb6e0862e 100644 --- a/crates/elephc-eval/src/ffi/native_methods.rs +++ b/crates/elephc-eval/src/ffi/native_methods.rs @@ -7,12 +7,17 @@ //! //! Key details: //! - Invalid names, handles, or indexes fail closed as `false`. -//! - The metadata records parameter names only; generated user helpers still -//! perform the actual method, static method, and constructor calls. +//! - The metadata records parameter names and scalar defaults; generated user +//! helpers still perform the actual method, static method, and constructor calls. use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ABI_VERSION}; -use crate::context::NativeCallableSignature; +use crate::context::{NativeCallableDefault, NativeCallableSignature}; + +const NATIVE_DEFAULT_NULL: u64 = 0; +const NATIVE_DEFAULT_BOOL: u64 = 1; +const NATIVE_DEFAULT_INT: u64 = 2; +const NATIVE_DEFAULT_FLOAT: u64 = 3; /// Registers a generated native PHP method signature in an eval context. /// @@ -106,6 +111,118 @@ pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param( .unwrap_or(0) } +/// Registers one generated native PHP method scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_scalar( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_scalar_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_scalar( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_scalar_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_string( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_string_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_string( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_string_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + /// Registers a generated native PHP constructor signature in an eval context. /// /// # Safety @@ -151,6 +268,60 @@ pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param( .unwrap_or(0) } +/// Registers one generated native PHP constructor scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_scalar( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_scalar_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_string( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_string_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + /// Registers generated native PHP parent-class metadata in an eval context. /// /// # Safety @@ -260,6 +431,105 @@ unsafe fn register_native_method_param_inner( } } +/// Runs native method scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_scalar`; invalid +/// handles, names, indexes, or default kinds fail closed as `false`. +unsafe fn register_native_method_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + +/// Runs native method string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_string`; invalid +/// handles, names, or indexes fail closed as `false`. +unsafe fn register_native_method_param_default_string_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Records a native method parameter default in the selected instance/static table. +/// +/// # Safety +/// `ctx` and `method_key_ptr` must be valid for their declared use; callers are +/// the exported ABI wrappers above. +unsafe fn register_native_method_param_default_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param_default( + class_name, + method_name, + param_index, + default, + )) + } else { + i32::from(context.define_native_method_param_default( + class_name, + method_name, + param_index, + default, + )) + } +} + /// Runs native constructor registration after installing a panic boundary. /// /// # Safety @@ -320,6 +590,83 @@ unsafe fn register_native_constructor_param_inner( i32::from(context.define_native_constructor_param(&class_name, param_index, param_name)) } +/// Runs native constructor scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_scalar`; +/// invalid handles, names, indexes, or default kinds fail closed as `false`. +unsafe fn register_native_constructor_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + +/// Runs native constructor string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_string`; +/// invalid handles, names, or indexes fail closed as `false`. +unsafe fn register_native_constructor_param_default_string_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Records a native constructor parameter default in the constructor signature table. +/// +/// # Safety +/// `ctx` and `class_name_ptr` must be valid for their declared use; callers are +/// the exported ABI wrappers above. +unsafe fn register_native_constructor_param_default_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_constructor_param_default(&class_name, param_index, default)) +} + /// Runs native parent-class registration after installing a panic boundary. /// /// # Safety @@ -347,6 +694,22 @@ unsafe fn register_native_class_parent_inner( i32::from(context.define_native_class_parent(&class_name, &parent_name)) } +/// Decodes scalar default kind/payload ABI fields into native callable metadata. +fn native_callable_scalar_default( + default_kind: u64, + default_payload: u64, +) -> Option { + match default_kind { + NATIVE_DEFAULT_NULL => Some(NativeCallableDefault::Null), + NATIVE_DEFAULT_BOOL => Some(NativeCallableDefault::Bool(default_payload != 0)), + NATIVE_DEFAULT_INT => Some(NativeCallableDefault::Int(default_payload as i64)), + NATIVE_DEFAULT_FLOAT => Some(NativeCallableDefault::Float(f64::from_bits( + default_payload, + ))), + _ => None, + } +} + /// Splits one generated `ClassName::methodName` metadata key into class and method pieces. fn split_method_key(method_key: &str) -> Option<(&str, &str)> { let (class_name, method_name) = method_key.rsplit_once("::")?; diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs index fc5f836eb1..c7859864e3 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -20,6 +20,7 @@ use crate::abi::{ ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_DIRTY, SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, }; +use crate::context::NativeCallableDefault; use crate::errors::EvalStatus; use crate::value::{RuntimeCell, RuntimeCellHandle}; use std::ffi::c_void; @@ -314,6 +315,36 @@ fn register_native_methods_record_signature_metadata() { value.len() as u64, ) }; + let method_default_registered = unsafe { + __elephc_eval_register_native_method_param_default_string( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + right.as_ptr(), + right.len() as u64, + ) + }; + let static_default_registered = unsafe { + __elephc_eval_register_native_static_method_param_default_scalar( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + 2, + 42, + ) + }; + let constructor_default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_scalar( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + 1, + 1, + ) + }; assert_eq!(method_registered, 1); assert_eq!(method_param_registered, 1); @@ -321,6 +352,9 @@ fn register_native_methods_record_signature_metadata() { assert_eq!(static_param_registered, 1); assert_eq!(constructor_registered, 1); assert_eq!(constructor_param_registered, 1); + assert_eq!(method_default_registered, 1); + assert_eq!(static_default_registered, 1); + assert_eq!(constructor_default_registered, 1); assert_eq!( ctx.native_method_signature("knownclass", "JOIN") .expect("method metadata") @@ -339,6 +373,24 @@ fn register_native_methods_record_signature_metadata() { .param_names(), &["value".to_string()] ); + assert_eq!( + ctx.native_method_signature("knownclass", "JOIN") + .expect("method metadata") + .param_default(1), + Some(&NativeCallableDefault::String("right".to_string())) + ); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_default(0), + Some(&NativeCallableDefault::Int(42)) + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&NativeCallableDefault::Bool(true)) + ); } /// Verifies native AOT parent metadata is available for eval static-scope resolution. diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 18e2f91f2e..312f1d9a40 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -84,6 +84,7 @@ pub(in crate::interpreter) fn eval_expr( let args = bind_native_callable_args( context.native_constructor_signature(&class_name), args, + values, )?; values .new_object(&class_name) diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index f70d756fd4..adcbb824e6 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -31,8 +31,8 @@ mod scope_cells; mod statements; use crate::context::{ - ElephcEvalContext, ElephcEvalExecutionScope, EvalReferenceTarget, NativeCallableSignature, - NativeFunction, + ElephcEvalContext, ElephcEvalExecutionScope, EvalReferenceTarget, NativeCallableDefault, + NativeCallableSignature, NativeFunction, }; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 3d89d404c8..c47c6bc883 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -3684,6 +3684,7 @@ fn eval_reflection_aot_method_invoke_dispatch( let args = bind_native_callable_args( context.native_static_method_signature(declaring_class, method_name), method_args, + values, )?; return values.static_method_call(declaring_class, method_name, args); } @@ -3703,6 +3704,7 @@ fn eval_reflection_aot_method_invoke_dispatch( let args = bind_native_callable_args( context.native_method_signature(declaring_class, method_name), method_args, + values, )?; values.method_call(object, method_name, args) } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index f16afc4aeb..3cd400e680 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2592,6 +2592,7 @@ pub(in crate::interpreter) fn eval_static_method_call_result( let args = bind_native_callable_args( context.native_static_method_signature(&class_name, method_name), evaluated_args, + values, )?; values.static_method_call(&class_name, method_name, args) } @@ -3232,6 +3233,7 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( let evaluated_args = bind_native_callable_args( context.native_method_signature(&class_name, method_name), evaluated_args, + values, )?; return values.method_call(object, method_name, evaluated_args); }; @@ -3420,6 +3422,7 @@ fn eval_reflection_class_new_instance_result( let args = bind_native_callable_args( context.native_constructor_signature(&class_name), constructor_args, + values, )?; let instance = values.new_object(&class_name)?; values.construct_object(instance, args)?; @@ -4037,17 +4040,68 @@ pub(in crate::interpreter) fn positional_evaluated_arg_values( pub(in crate::interpreter) fn bind_native_callable_args( signature: Option, args: Vec, + values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(signature) = signature else { return positional_evaluated_arg_values(args); }; if signature.param_names().len() == signature.param_count() { - bind_evaluated_function_args(signature.param_names(), args) + bind_native_signature_args(&signature, args, values) } else { positional_evaluated_arg_values(args) } } +/// Binds native AOT callable args and fills omitted scalar defaults from metadata. +fn bind_native_signature_args( + signature: &NativeCallableSignature, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; signature.param_count()]; + let mut next_positional = 0; + + for arg in args { + if let Some(name) = arg.name { + bind_dynamic_named_arg(signature.param_names(), &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + for (position, value) in bound_args.iter_mut().enumerate() { + if value.is_some() { + continue; + } + if position < signature.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = signature.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *value = Some(materialize_native_callable_default(default, values)?); + } + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Allocates a fresh runtime cell for one registered native AOT scalar default. +fn materialize_native_callable_default( + default: &NativeCallableDefault, + values: &mut impl RuntimeValueOps, +) -> Result { + match default { + NativeCallableDefault::Null => values.null(), + NativeCallableDefault::Bool(value) => values.bool_value(*value), + NativeCallableDefault::Int(value) => values.int(*value), + NativeCallableDefault::Float(value) => values.float(*value), + NativeCallableDefault::String(value) => values.string(value), + } +} + /// Executes a PHP `static $name = expr;` declaration in the current eval scope. pub(in crate::interpreter) fn execute_static_var_stmt( name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs index ee6d718746..9de01147cc 100644 --- a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs @@ -299,6 +299,30 @@ fn execute_program_static_runtime_method_hook_binds_named_args() { assert_eq!(values.get(result), FakeValue::String("AB".to_string())); } +/// Verifies runtime AOT static method fallback fills registered scalar defaults. +#[test] +fn execute_program_static_runtime_method_hook_binds_default_args() { + let program = parse_fragment( + br#"echo KnownClass::join("A"); echo ":"; +return call_user_func_array(["KnownClass", "join"], ["left" => "C"]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_default(1, NativeCallableDefault::String("B".to_string()))); + assert!(context.define_native_static_method_signature("KnownClass", "join", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered AOT defaults should bind"); + + assert_eq!(values.output, "AB:"); + assert_eq!(values.get(result), FakeValue::String("CB".to_string())); +} + /// Verifies runtime AOT static method fallback rejects named arguments without metadata. #[test] fn execute_program_static_runtime_method_hook_rejects_unregistered_named_args() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 66c689d004..7eb9b8869c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -73,9 +73,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, and string-keyed named unpacking for supported public scalar/Mixed constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar or null default arguments for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility; public emitted/AOT `__clone()` hooks run through the method bridge. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding for supported public scalar/Mixed/object method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar or null default arguments for supported public scalar/Mixed/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 6c90b3be0e..89fa9dd880 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -20,7 +20,7 @@ use crate::codegen::{ }; use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op}; use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; -use crate::parser::ast::Visibility; +use crate::parser::ast::{Expr, ExprKind, Visibility}; use crate::types::{ClassInfo, FunctionSig, PhpType}; use super::super::super::context::FunctionContext; @@ -51,6 +51,10 @@ const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; const MAX_EVAL_NATIVE_METHOD_PARAMS: usize = 8; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; +const NATIVE_DEFAULT_NULL: i64 = 0; +const NATIVE_DEFAULT_BOOL: i64 = 1; +const NATIVE_DEFAULT_INT: i64 = 2; +const NATIVE_DEFAULT_FLOAT: i64 = 3; /// Local slot metadata needed for conservative eval scope synchronization. #[derive(Clone)] @@ -94,6 +98,12 @@ struct EvalNativeConstructorRegistration { signature: FunctionSig, } +/// Scalar native callable default that can be registered with libelephc-eval. +enum EvalNativeCallableDefault { + Scalar { kind: i64, payload: i64 }, + String(String), +} + /// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { super::ensure_arg_count(inst, "eval", 1)?; @@ -600,6 +610,50 @@ fn method_signature_can_register_with_eval(signature: &FunctionSig) -> bool { && signature.ref_params.iter().all(|is_ref| !*is_ref) } +/// Converts a PHP signature default into the compact eval bridge default ABI. +fn eval_native_callable_default(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::Null => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + payload: 0, + }), + ExprKind::BoolLiteral(value) => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_BOOL, + payload: i64::from(*value), + }), + ExprKind::IntLiteral(value) => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload: *value, + }), + ExprKind::FloatLiteral(value) => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload: value.to_bits() as i64, + }), + ExprKind::StringLiteral(value) => Some(EvalNativeCallableDefault::String(value.clone())), + ExprKind::Negate(inner) => eval_native_callable_negated_default(inner), + _ => None, + } +} + +/// Converts a negated literal default into the compact eval bridge default ABI. +fn eval_native_callable_negated_default(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => { + value + .checked_neg() + .map(|payload| EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + }) + } + ExprKind::FloatLiteral(value) => Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload: (-*value).to_bits() as i64, + }), + _ => None, + } +} + /// Returns true when an instance method is public in the class metadata. fn class_method_is_public(class_info: &ClassInfo, method_name: &str) -> bool { class_info @@ -727,6 +781,20 @@ fn register_eval_native_method( param_name, ); } + for (index, default) in registration.signature.defaults.iter().enumerate() { + let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { + continue; + }; + register_eval_native_method_param_default( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + index, + &default, + ); + } } /// Emits one native method parameter-name registration call. @@ -778,6 +846,80 @@ fn register_eval_native_method_param( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native method parameter-default registration call. +fn register_eval_native_method_param_default( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + param_index: usize, + default: &EvalNativeCallableDefault, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let symbol = match default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + *kind, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + *payload, + ); + if is_static { + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_static_method_param_default_scalar", + ) + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_default_scalar") + } + } + EvalNativeCallableDefault::String(value) => { + let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + if is_static { + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_static_method_param_default_string", + ) + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_default_string") + } + } + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native constructor signature registration call into the eval context. fn register_eval_native_constructor( ctx: &mut FunctionContext<'_>, @@ -817,6 +959,19 @@ fn register_eval_native_constructor( param_name, ); } + for (index, default) in registration.signature.defaults.iter().enumerate() { + let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { + continue; + }; + register_eval_native_constructor_param_default( + ctx, + context_offset, + &class_name_label, + class_name_len, + index, + &default, + ); + } } /// Emits one native class-parent metadata registration call into the eval context. @@ -899,6 +1054,67 @@ fn register_eval_native_constructor_param( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native constructor parameter-default registration call. +fn register_eval_native_constructor_param_default( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + param_index: usize, + default: &EvalNativeCallableDefault, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let symbol = match default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + *kind, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + *payload, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_default_scalar") + } + EvalNativeCallableDefault::String(value) => { + let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_default_string") + } + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native-function parameter-name registration call. fn register_eval_native_function_param( ctx: &mut FunctionContext<'_>, @@ -1136,17 +1352,17 @@ fn emit_eval_called_class_name_result_aarch64(ctx: &mut FunctionContext<'_>) -> let done = ctx.next_label("eval_called_class_done"); emit_eval_late_static_class_id_to_reg(ctx, "x12")?; abi::emit_load_symbol_to_reg(ctx.emitter, "x10", "_class_name_count", 0); - ctx.emitter.instruction("cmp x12, x10"); // reject called-class ids outside the class-name table - ctx.emitter.instruction(&format!("b.hs {}", missing)); // fall back to the lexical eval class when metadata is missing + ctx.emitter.instruction("cmp x12, x10"); // reject called-class ids outside the class-name table + ctx.emitter.instruction(&format!("b.hs {}", missing)); // fall back to the lexical eval class when metadata is missing abi::emit_symbol_address(ctx.emitter, "x11", "_class_name_entries"); - ctx.emitter.instruction("lsl x12, x12, #4"); // convert class id to a 16-byte class-name table offset - ctx.emitter.instruction("add x11, x11, x12"); // select the called-class metadata row - ctx.emitter.instruction("ldr x1, [x11]"); // load the called-class name pointer - ctx.emitter.instruction("ldr x2, [x11, #8]"); // load the called-class name length - ctx.emitter.instruction(&format!("b {}", done)); // skip the missing-metadata fallback + ctx.emitter.instruction("lsl x12, x12, #4"); // convert class id to a 16-byte class-name table offset + ctx.emitter.instruction("add x11, x11, x12"); // select the called-class metadata row + ctx.emitter.instruction("ldr x1, [x11]"); // load the called-class name pointer + ctx.emitter.instruction("ldr x2, [x11, #8]"); // load the called-class name length + ctx.emitter.instruction(&format!("b {}", done)); // skip the missing-metadata fallback ctx.emitter.label(&missing); abi::emit_symbol_address(ctx.emitter, "x1", "_class_name_missing"); - ctx.emitter.instruction("mov x2, #0"); // empty called-class name triggers lexical fallback in eval + ctx.emitter.instruction("mov x2, #0"); // empty called-class name triggers lexical fallback in eval ctx.emitter.label(&done); Ok(()) } @@ -1157,17 +1373,17 @@ fn emit_eval_called_class_name_result_x86_64(ctx: &mut FunctionContext<'_>) -> R let done = ctx.next_label("eval_called_class_done"); emit_eval_late_static_class_id_to_reg(ctx, "r8")?; abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_class_name_count", 0); - ctx.emitter.instruction("cmp r8, r9"); // reject called-class ids outside the class-name table - ctx.emitter.instruction(&format!("jae {}", missing)); // fall back to the lexical eval class when metadata is missing + ctx.emitter.instruction("cmp r8, r9"); // reject called-class ids outside the class-name table + ctx.emitter.instruction(&format!("jae {}", missing)); // fall back to the lexical eval class when metadata is missing abi::emit_symbol_address(ctx.emitter, "r10", "_class_name_entries"); - ctx.emitter.instruction("shl r8, 4"); // convert class id to a 16-byte class-name table offset - ctx.emitter.instruction("add r10, r8"); // select the called-class metadata row - ctx.emitter.instruction("mov rax, QWORD PTR [r10]"); // load the called-class name pointer - ctx.emitter.instruction("mov rdx, QWORD PTR [r10 + 8]"); // load the called-class name length - ctx.emitter.instruction(&format!("jmp {}", done)); // skip the missing-metadata fallback + ctx.emitter.instruction("shl r8, 4"); // convert class id to a 16-byte class-name table offset + ctx.emitter.instruction("add r10, r8"); // select the called-class metadata row + ctx.emitter.instruction("mov rax, QWORD PTR [r10]"); // load the called-class name pointer + ctx.emitter.instruction("mov rdx, QWORD PTR [r10 + 8]"); // load the called-class name length + ctx.emitter.instruction(&format!("jmp {}", done)); // skip the missing-metadata fallback ctx.emitter.label(&missing); abi::emit_symbol_address(ctx.emitter, "rax", "_class_name_missing"); - ctx.emitter.instruction("mov rdx, 0"); // empty called-class name triggers lexical fallback in eval + ctx.emitter.instruction("mov rdx, 0"); // empty called-class name triggers lexical fallback in eval ctx.emitter.label(&done); Ok(()) } @@ -1596,12 +1812,12 @@ fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("b.eq {}", label)); // skip reload when eval unset or omitted the local } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_PRESENT)); // check whether eval left the local visible - ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local + ctx.emitter.instruction(&format!("je {}", label)); // skip reload when eval unset or omitted the local } } } @@ -1713,12 +1929,12 @@ fn emit_retain_scope_cell_if_owned(ctx: &mut FunctionContext<'_>) { Arch::AArch64 => { ctx.emitter .instruction(&format!("tst {}, #{}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("b.eq {}", skip)); // borrowed scope entries can be copied back without retaining } Arch::X86_64 => { ctx.emitter .instruction(&format!("test {}, {}", flags_reg, EVAL_SCOPE_FLAG_OWNED)); // check whether the scope keeps its own Mixed-cell owner - ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining + ctx.emitter.instruction(&format!("je {}", skip)); // borrowed scope entries can be copied back without retaining } } abi::emit_call_label(ctx.emitter, "__rt_incref"); @@ -1838,13 +2054,13 @@ fn emit_branch_if_eval_status(ctx: &mut FunctionContext<'_>, status: i64, label: match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter - .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler + .instruction(&format!("cmp {}, #{}", result_reg, status)); // compare the eval bridge status against the handled code + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching eval status handler } Arch::X86_64 => { ctx.emitter - .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code - ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler + .instruction(&format!("cmp {}, {}", result_reg, status)); // compare the eval bridge status against the handled code + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching eval status handler } } } @@ -1872,21 +2088,21 @@ fn emit_eval_fatal_message(ctx: &mut FunctionContext<'_>, message: &str) { let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr + ctx.emitter.instruction("mov x0, #2"); // write the eval runtime diagnostic to stderr ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); ctx.emitter - .instruction(&format!("mov x2, #{}", message_len)); // pass the eval runtime diagnostic byte length + .instruction(&format!("mov x2, #{}", message_len)); // pass the eval runtime diagnostic byte length ctx.emitter.syscall(4); abi::emit_exit(ctx.emitter, 1); } Arch::X86_64 => { - ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr + ctx.emitter.instruction("mov edi, 2"); // write the eval runtime diagnostic to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); ctx.emitter - .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length - ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting + .instruction(&format!("mov edx, {}", message_len)); // pass the eval runtime diagnostic byte length + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the eval runtime diagnostic before exiting abi::emit_exit(ctx.emitter, 1); } } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index be8db83a66..fbe5e98635 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7469,6 +7469,47 @@ try { ); } +/// Verifies eval ReflectionMethod::invoke can dispatch public generated/AOT methods. +#[test] +fn test_eval_reflection_method_invoke_calls_aot_method() { + let out = compile_and_run_capture( + r#"getMethod("who"); +echo $who->invoke($object) . ":"; +$static = new ReflectionMethod("EvalAotReflectInvokeBase", "make"); +echo $static->invoke(null, right: "Y", left: "X") . ":"; +echo $static->invoke($object, "A") . ":"; +$join = new ReflectionMethod("EvalAotReflectInvokeChild", "join"); +echo $join->invoke($object, "Q") . ":"; +return $join->invokeArgs($object, ["b" => "2", "a" => "1"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotReflectInvokeChild:EvalAotReflectInvokeBase:XY:EvalAotReflectInvokeBase:AS:QB:12" + ); +} + /// Verifies eval ReflectionMethod constructor/destructor predicates through the bridge. #[test] fn test_eval_reflection_method_reports_constructor_and_destructor() { @@ -9168,6 +9209,26 @@ echo eval('$box = new EvalDynamicNewOneArgCtor(11); return $box->x;'); assert_eq!(out, "11"); } +/// Verifies eval object construction fills registered AOT constructor defaults. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_default_arg() { + let out = compile_and_run( + r#"label = $left . $right; + } +} +echo eval('$first = new EvalDynamicNewDefaultCtor("A"); +echo $first->label . ":"; +$second = new EvalDynamicNewDefaultCtor(right: "Y", left: "X"); +return $second->label;'); +"#, + ); + assert_eq!(out, "AB:XY"); +} + /// Verifies eval object construction passes more than two arguments to an AOT constructor. #[test] fn test_eval_dynamic_new_runs_constructor_with_many_args() { From 663a4be51a7474fb04e2970c7a9fc00916e4d115 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 20:32:09 +0200 Subject: [PATCH 0484/1208] Expose AOT method params in eval reflection --- .../elephc-eval/src/interpreter/reflection.rs | 143 +++++++++++++++++- docs/php/eval.md | 15 +- tests/codegen/eval.rs | 58 +++++++ 3 files changed, 204 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index c47c6bc883..cf61d8cf27 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1063,9 +1063,10 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( if owner_kind == EVAL_REFLECTION_OWNER_METHOD && !eval_reflection_class_like_exists(&reflected_name, context) { - if let Some(member) = eval_reflection_aot_method_metadata_if_exists( + if let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( &reflected_name, &requested_name, + context, values, )? { let member_name = requested_name.to_ascii_lowercase(); @@ -1437,9 +1438,12 @@ fn eval_reflection_method_new( let class_name = eval_reflection_string_arg(args[0], values)?; if !eval_reflection_class_like_exists(&class_name, context) { let method_name = eval_reflection_string_arg(args[1], values)?; - if let Some(method) = - eval_reflection_aot_method_metadata_if_exists(&class_name, &method_name, values)? - { + if let Some(method) = eval_reflection_aot_method_metadata_with_signature_if_exists( + &class_name, + &method_name, + context, + values, + )? { let method_name = method_name.to_ascii_lowercase(); return eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_METHOD, @@ -1524,7 +1528,30 @@ fn eval_reflection_aot_method_metadata_if_exists( }; Ok(Some(eval_reflection_aot_method_metadata( runtime_class_name, + method_name, flags, + None, + ))) +} + +/// Returns generated AOT ReflectionMethod metadata with registered signature details. +fn eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { + return Ok(None); + }; + let signature = + eval_reflection_aot_method_signature(runtime_class_name, method_name, flags, context); + Ok(Some(eval_reflection_aot_method_metadata( + runtime_class_name, + method_name, + flags, + signature.as_ref(), ))) } @@ -1543,7 +1570,9 @@ pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata( /// Converts AOT method flag metadata into the eval ReflectionMethod shape. fn eval_reflection_aot_method_metadata( class_name: &str, + method_name: &str, flags: u64, + signature: Option<&NativeCallableSignature>, ) -> EvalReflectionMemberMetadata { let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { EvalVisibility::Private @@ -1552,6 +1581,11 @@ fn eval_reflection_aot_method_metadata( } else { EvalVisibility::Public }; + let required_parameter_count = + signature.map_or(0, NativeCallableSignature::required_param_count); + let parameters = signature.map_or_else(Vec::new, |signature| { + eval_reflection_native_callable_parameters(class_name, method_name, flags, signature) + }); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), attributes: Vec::new(), @@ -1565,11 +1599,104 @@ fn eval_reflection_aot_method_metadata( type_metadata: None, return_type_metadata: None, default_value: None, - required_parameter_count: 0, - parameters: Vec::new(), + required_parameter_count, + parameters, + } +} + +/// Selects the registered native signature for an AOT method-like member. +fn eval_reflection_aot_method_signature( + class_name: &str, + method_name: &str, + flags: u64, + context: &ElephcEvalContext, +) -> Option { + if method_name.eq_ignore_ascii_case("__construct") { + return context.native_constructor_signature(class_name); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0 { + context.native_static_method_signature(class_name, method_name) + } else { + context.native_method_signature(class_name, method_name) } } +/// Builds ReflectionParameter metadata for one registered native AOT signature. +fn eval_reflection_native_callable_parameters( + declaring_class_name: &str, + method_name: &str, + flags: u64, + signature: &NativeCallableSignature, +) -> Vec { + let names = eval_reflection_native_callable_parameter_names(signature); + let parameter_count = names.len(); + let has_type_flags = vec![false; parameter_count]; + let parameter_types = vec![None; parameter_count]; + let parameter_attributes = vec![Vec::new(); parameter_count]; + let defaults = eval_reflection_native_callable_parameter_defaults(signature); + let by_ref_flags = vec![false; parameter_count]; + let variadic_flags = vec![false; parameter_count]; + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method_name.to_ascii_lowercase(), + declaring_class_name: Some(declaring_class_name.trim_start_matches('\\').to_string()), + attributes: Vec::new(), + flags, + required_parameter_count: signature.required_param_count(), + }; + eval_reflection_parameters_from_names_and_type_flags( + Some(declaring_class_name.trim_start_matches('\\')), + Some(&declaring_function), + &names, + &has_type_flags, + ¶meter_types, + ¶meter_attributes, + &defaults, + &by_ref_flags, + &variadic_flags, + &[], + ) +} + +/// Returns parameter names for a registered native callable, filling missing bridge names. +fn eval_reflection_native_callable_parameter_names( + signature: &NativeCallableSignature, +) -> Vec { + (0..signature.param_count()) + .map(|index| { + signature + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", index)) + }) + .collect() +} + +/// Converts registered scalar native defaults into eval constant expressions. +fn eval_reflection_native_callable_parameter_defaults( + signature: &NativeCallableSignature, +) -> Vec> { + (0..signature.param_count()) + .map(|index| { + signature + .param_default(index) + .map(eval_reflection_native_callable_default_expr) + }) + .collect() +} + +/// Converts one registered native default into an eval constant expression. +fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) -> EvalExpr { + EvalExpr::Const(match default { + NativeCallableDefault::Null => EvalConst::Null, + NativeCallableDefault::Bool(value) => EvalConst::Bool(*value), + NativeCallableDefault::Int(value) => EvalConst::Int(*value), + NativeCallableDefault::Float(value) => EvalConst::Float(*value), + NativeCallableDefault::String(value) => EvalConst::String(value.clone()), + }) +} + /// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. fn eval_reflection_aot_property_metadata_if_exists( class_name: &str, @@ -2462,7 +2589,9 @@ fn eval_reflection_aot_member_object_array_result( let mut index = 0; for name in names { let member = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - eval_reflection_aot_method_metadata_if_exists(class_name, name, values)? + eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name, name, context, values, + )? } else { eval_reflection_aot_property_metadata_if_exists(class_name, name, values)? }; diff --git a/docs/php/eval.md b/docs/php/eval.md index 7eb9b8869c..53d085ff9c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -260,9 +260,11 @@ visible member metadata, including supported member attributes and predicate flags. For generated/AOT classes, `ReflectionClass::getMethod()` / `getProperty()` and `getMethods()` / `getProperties()` materialize reflection objects from emitted member-name and predicate metadata, including optional -modifier filters. AOT member reflection currently exposes names, declaring -classes, and predicate flags, but does not expose full AOT parameter, -attribute, property type, or property default-value metadata. +modifier filters. AOT method reflection also exposes registered parameter +names, required/optional counts, and registered scalar or null default values +for generated method/static-method signatures. AOT member reflection does not +yet expose full AOT type metadata, attributes, property types, or property +default-value metadata. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected @@ -300,8 +302,11 @@ reflected method is `__construct` or `__destruct`. `ReflectionFunction::getName()`, `ReflectionFunction::getParameters()`, `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report retained eval-declared function and -method metadata. Eval-declared functions and methods expose declared-type -presence for parameters and return types, simple named type metadata through +method metadata, plus registered generated/AOT method parameter names, +required/optional counts, and scalar or null default values when native +method/static-method signatures are registered. Eval-declared functions and +methods expose declared-type presence for parameters and return types, simple +named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, `allowsNull()`, and `isBuiltin()`, and multi-member union metadata through `ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fbe5e98635..6eff5f2af8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7510,6 +7510,64 @@ return $join->invokeArgs($object, ["b" => "2", "a" => "1"]);'); ); } +/// Verifies eval ReflectionMethod exposes registered generated/AOT parameter metadata. +#[test] +fn test_eval_reflection_method_exposes_aot_parameter_metadata() { + let out = compile_and_run_capture( + r#"getNumberOfParameters() . "/" . $method->getNumberOfRequiredParameters() . ":"; +foreach ($method->getParameters() as $param) { + echo $param->getName(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isDefaultValueAvailable() ? "=" : "-"; + if ($param->isDefaultValueAvailable()) { + $default = $param->getDefaultValue(); + echo is_null($default) ? "null" : $default; + } + echo ";"; +} +$static = new ReflectionMethod("EvalAotReflectParamTarget", "sum"); +echo ":" . $static->getNumberOfParameters() . "/" . $static->getNumberOfRequiredParameters() . ":"; +foreach ($static->getParameters() as $param) { + echo $param->getName(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isDefaultValueAvailable() ? "=" : "-"; + if ($param->isDefaultValueAvailable()) { + echo $param->getDefaultValue(); + } + echo ";"; +} +$listed = null; +foreach ((new ReflectionClass("EvalAotReflectParamTarget"))->getMethods() as $candidate) { + if ($candidate->getName() === "join") { + $listed = $candidate; + } +} +echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[2]->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "3/1:leftr-;rightO=B;countO=null;:2/1:firstr-;secondO=2;:3/count" + ); +} + /// Verifies eval ReflectionMethod constructor/destructor predicates through the bridge. #[test] fn test_eval_reflection_method_reports_constructor_and_destructor() { From f5ae20fdbedc39fc576194669c7fefac01f7dcc6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 20:42:27 +0200 Subject: [PATCH 0485/1208] Expose AOT constructors in eval reflection --- .../elephc-eval/src/interpreter/reflection.rs | 86 +++++++++++++++---- docs/php/eval.md | 12 +-- tests/codegen/eval.rs | 50 +++++++++++ 3 files changed, 125 insertions(+), 23 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index cf61d8cf27..c67353540d 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1233,14 +1233,24 @@ fn eval_reflection_class_new( else { return Ok(None); }; + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + &reflected_name, + values, + )?; + let property_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + &reflected_name, + values, + )?; return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, &reflected_name, &[], &[], &[], - &[], - &[], + &method_names, + &property_names, None, &[], None, @@ -1936,15 +1946,28 @@ fn eval_reflection_owner_object_with_members( let trait_names_array = eval_reflection_string_array_result(trait_names, values)?; let method_names_array = eval_reflection_string_array_result(method_names, values)?; let property_names_array = eval_reflection_string_array_result(property_names, values)?; + let is_eval_class = owner_kind == EVAL_REFLECTION_OWNER_CLASS + && eval_reflection_class_like_exists(reflected_name, context); let method_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS && include_class_members { - eval_reflection_member_object_array_result( - EVAL_REFLECTION_OWNER_METHOD, - reflected_name, - &method_names, - None, - context, - values, - )? + if is_eval_class { + eval_reflection_member_object_array_result( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + method_names, + None, + context, + values, + )? + } else { + eval_reflection_aot_member_object_array_result( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + method_names, + None, + context, + values, + )? + } } else if matches!( owner_kind, EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION @@ -1959,14 +1982,25 @@ fn eval_reflection_owner_object_with_members( values.array_new(0)? }; let property_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS && include_class_members { - eval_reflection_member_object_array_result( - EVAL_REFLECTION_OWNER_PROPERTY, - reflected_name, - &property_names, - None, - context, - values, - )? + if is_eval_class { + eval_reflection_member_object_array_result( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_names, + None, + context, + values, + )? + } else { + eval_reflection_aot_member_object_array_result( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_names, + None, + context, + values, + )? + } } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { match default_value { Some(default) => eval_method_parameter_default(default, context, values)?, @@ -2484,6 +2518,22 @@ fn eval_reflection_constructor_object_result( return values.null(); } let Some(member) = eval_reflection_method_metadata(class_name, "__construct", context) else { + if !eval_reflection_class_like_exists(class_name, context) { + if let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name, + "__construct", + context, + values, + )? { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + "__construct", + &member, + context, + values, + ); + } + } return values.null(); }; eval_reflection_member_object_result( diff --git a/docs/php/eval.md b/docs/php/eval.md index 53d085ff9c..0315ec39aa 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -262,9 +262,9 @@ flags. For generated/AOT classes, `ReflectionClass::getMethod()` / objects from emitted member-name and predicate metadata, including optional modifier filters. AOT method reflection also exposes registered parameter names, required/optional counts, and registered scalar or null default values -for generated method/static-method signatures. AOT member reflection does not -yet expose full AOT type metadata, attributes, property types, or property -default-value metadata. +for generated constructor, instance-method, and static-method signatures. AOT +member reflection does not yet expose full AOT type metadata, attributes, +property types, or property default-value metadata. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected @@ -272,8 +272,10 @@ member. `ReflectionMethod::hasPrototype()` and `getPrototype()` expose eval parent-class overrides and interface implementation prototypes; inherited methods that are not overridden report no prototype, matching PHP reflection. `ReflectionClass::getConstructor()` returns a materialized -`ReflectionMethod` for direct, inherited, interface, and trait constructors, or -`null` when no constructor is visible. `ReflectionClass::getParentClass()` +`ReflectionMethod` for direct, inherited, interface, trait, and generated/AOT +constructors, including registered generated/AOT constructor parameter names, +counts, and scalar/null defaults where available; it returns `null` when no +constructor is visible. `ReflectionClass::getParentClass()` returns a materialized `ReflectionClass` for eval-declared parent classes or `false` when no parent class exists. `ReflectionClass::newInstance()` constructs eval-declared reflected classes and forwards constructor arguments through diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6eff5f2af8..87d8e8ac23 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7568,6 +7568,56 @@ echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[2]- ); } +/// Verifies eval ReflectionClass::getConstructor exposes generated/AOT constructor metadata. +#[test] +fn test_eval_reflection_class_get_constructor_for_aot_class() { + let out = compile_and_run_capture( + r#"label = $left . $right . ($count ?? 0); + } +} +class EvalAotReflectCtorPlain {} +echo eval('$ctor = (new ReflectionClass("EvalAotReflectCtorParamTarget"))->getConstructor(); +echo ($ctor instanceof ReflectionMethod) ? "M:" : "m:"; +echo $ctor->getName() . "/" . $ctor->getDeclaringClass()->getName() . ":"; +echo $ctor->getNumberOfParameters() . "/" . $ctor->getNumberOfRequiredParameters() . ":"; +foreach ($ctor->getParameters() as $param) { + echo $param->getName(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isDefaultValueAvailable() ? "=" : "-"; + if ($param->isDefaultValueAvailable()) { + $default = $param->getDefaultValue(); + echo is_null($default) ? "null" : $default; + } + echo ";"; +} +$listed = null; +foreach ((new ReflectionClass("EvalAotReflectCtorParamTarget"))->getMethods() as $candidate) { + if ($candidate->getName() === "__construct") { + $listed = $candidate; + } +} +echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[0]->getName(); +$plain = (new ReflectionClass("EvalAotReflectCtorPlain"))->getConstructor(); +echo ":" . ($plain === null ? "null" : "bad"); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "M:__construct/EvalAotReflectCtorParamTarget:3/1:leftr-;rightO=B;countO=null;:3/left:null" + ); +} + /// Verifies eval ReflectionMethod constructor/destructor predicates through the bridge. #[test] fn test_eval_reflection_method_reports_constructor_and_destructor() { From c2c44fbd5d681c4ccaf81e04a2b3c19052ceae8e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 20:59:35 +0200 Subject: [PATCH 0486/1208] Fix eval AOT method declaring reflection --- .../elephc-eval/src/interpreter/reflection.rs | 18 ++- .../src/interpreter/runtime_ops.rs | 7 + .../interpreter/tests/support/object_ops.rs | 14 ++ .../interpreter/tests/support/runtime_ops.rs | 8 + .../elephc-eval/src/runtime_hooks/externs.rs | 6 + crates/elephc-eval/src/runtime_hooks/ops.rs | 25 +++ src/codegen_support/runtime/data/user.rs | 57 ++++++- src/codegen_support/runtime/eval_bridge.rs | 151 +++++++++++++++++- tests/codegen/eval.rs | 51 ++++++ 9 files changed, 328 insertions(+), 9 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index c67353540d..5997db2c19 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1536,8 +1536,11 @@ fn eval_reflection_aot_method_metadata_if_exists( let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { return Ok(None); }; + let declaring_class_name = values + .reflection_method_declaring_class(runtime_class_name, method_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); Ok(Some(eval_reflection_aot_method_metadata( - runtime_class_name, + &declaring_class_name, method_name, flags, None, @@ -1555,10 +1558,17 @@ fn eval_reflection_aot_method_metadata_with_signature_if_exists( let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { return Ok(None); }; - let signature = - eval_reflection_aot_method_signature(runtime_class_name, method_name, flags, context); + let declaring_class_name = values + .reflection_method_declaring_class(runtime_class_name, method_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + let mut signature = + eval_reflection_aot_method_signature(&declaring_class_name, method_name, flags, context); + if signature.is_none() && declaring_class_name != runtime_class_name { + signature = + eval_reflection_aot_method_signature(runtime_class_name, method_name, flags, context); + } Ok(Some(eval_reflection_aot_method_metadata( - runtime_class_name, + &declaring_class_name, method_name, flags, signature.as_ref(), diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 8681478c6f..2e2cac45e9 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -146,6 +146,13 @@ pub trait RuntimeValueOps { method_name: &str, ) -> Result, EvalStatus>; + /// Returns the generated AOT declaring class for a class/method pair. + fn reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus>; + /// Returns generated AOT ReflectionMethod names visible for one class. fn reflection_method_names( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 5106f7974d..f6633d722c 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -909,6 +909,20 @@ impl FakeOps { _ => Ok(None), } } + /// Reports fake generated AOT ReflectionMethod declaring classes for metadata unit tests. + pub(super) fn runtime_reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Ok(None); + } + match method_name.to_ascii_lowercase().as_str() { + "run" | "helper" | "locked" => Ok(Some("KnownClass".to_string())), + _ => Ok(None), + } + } /// Reports fake generated AOT ReflectionMethod names for eval metadata unit tests. pub(super) fn runtime_reflection_method_names( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 76b9e59b98..379092399a 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -175,6 +175,14 @@ impl RuntimeValueOps for FakeOps { ) -> Result, EvalStatus> { self.runtime_reflection_method_flags(class_name, method_name) } + /// Reports fake generated AOT ReflectionMethod declaring classes for metadata bridge tests. + fn reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_method_declaring_class(class_name, method_name) + } /// Reports fake generated AOT ReflectionMethod names for metadata bridge tests. fn reflection_method_names( &mut self, diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 4dd680790b..b901968de8 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -103,6 +103,12 @@ unsafe extern "C" { method_ptr: *const u8, method_len: u64, ) -> u64; + pub(super) fn __elephc_eval_reflection_method_declaring_class( + class_ptr: *const u8, + class_len: u64, + method_ptr: *const u8, + method_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_reflection_method_names( class_ptr: *const u8, class_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index ac73663f0c..a094caba9b 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -266,6 +266,31 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok((flags != 0).then_some(flags)) } + /// Returns generated AOT ReflectionMethod declaring class metadata. + fn reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_method_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + method_name.as_ptr(), + method_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + /// Returns generated AOT ReflectionMethod names visible for one class. fn reflection_method_names( &mut self, diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index b51e40da72..0c878d641f 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -971,6 +971,12 @@ fn emit_eval_reflection_method_lookup_data( let flags = eval_reflection_instance_method_flags(class_info, method_name); let class_label = format!("_eval_reflection_method_class_{}", index); let method_label = format!("_eval_reflection_method_name_{}", index); + let declaring_class = eval_reflection_instance_method_declaring_class( + class_name, + class_info, + method_name, + ); + let declaring_label = format!("_eval_reflection_method_declaring_class_{}", index); out.push_str(&format!( ".globl {0}\n{0}:\n .ascii \"{1}\"\n", class_label, @@ -981,12 +987,19 @@ fn emit_eval_reflection_method_lookup_data( method_label, escaped_ascii(method_name) )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + declaring_label, + escaped_ascii(declaring_class) + )); entries.push(( class_label, class_name.len(), method_label, method_name.len(), flags, + declaring_label, + declaring_class.len(), )); index += 1; } @@ -997,6 +1010,9 @@ fn emit_eval_reflection_method_lookup_data( let flags = eval_reflection_static_method_flags(class_info, method_name); let class_label = format!("_eval_reflection_method_class_{}", index); let method_label = format!("_eval_reflection_method_name_{}", index); + let declaring_class = + eval_reflection_static_method_declaring_class(class_name, class_info, method_name); + let declaring_label = format!("_eval_reflection_method_declaring_class_{}", index); out.push_str(&format!( ".globl {0}\n{0}:\n .ascii \"{1}\"\n", class_label, @@ -1007,12 +1023,19 @@ fn emit_eval_reflection_method_lookup_data( method_label, escaped_ascii(method_name) )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + declaring_label, + escaped_ascii(declaring_class) + )); entries.push(( class_label, class_name.len(), method_label, method_name.len(), flags, + declaring_label, + declaring_class.len(), )); index += 1; } @@ -1022,15 +1045,47 @@ fn emit_eval_reflection_method_lookup_data( out.push_str(".globl _eval_reflection_method_count\n_eval_reflection_method_count:\n"); out.push_str(&format!(" .quad {}\n", entries.len())); out.push_str(".globl _eval_reflection_methods\n_eval_reflection_methods:\n"); - for (class_label, class_len, method_label, method_len, flags) in entries { + for (class_label, class_len, method_label, method_len, flags, declaring_label, declaring_len) in + entries + { out.push_str(&format!(" .quad {}\n", class_label)); out.push_str(&format!(" .quad {}\n", class_len)); out.push_str(&format!(" .quad {}\n", method_label)); out.push_str(&format!(" .quad {}\n", method_len)); out.push_str(&format!(" .quad {}\n", flags)); + out.push_str(&format!(" .quad {}\n", declaring_label)); + out.push_str(&format!(" .quad {}\n", declaring_len)); } } +/// Returns the class name that declares one visible instance method. +fn eval_reflection_instance_method_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + method_name: &str, +) -> &'a str { + class_info + .method_impl_classes + .get(method_name) + .or_else(|| class_info.method_declaring_classes.get(method_name)) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one visible static method. +fn eval_reflection_static_method_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + method_name: &str, +) -> &'a str { + class_info + .static_method_impl_classes + .get(method_name) + .or_else(|| class_info.static_method_declaring_classes.get(method_name)) + .map(String::as_str) + .unwrap_or(reflected_class) +} + /// Returns eval ReflectionMethod bitflags for one instance method entry. fn eval_reflection_instance_method_flags(class_info: &ClassInfo, method_name: &str) -> u64 { let visibility = class_info diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 396e25e91d..4b9df203fc 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -174,6 +174,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emit_aarch64_eval_reflection_method_names(emitter); emit_aarch64_eval_reflection_property_names(emitter); emit_aarch64_eval_reflection_method_flags(emitter); + emit_aarch64_eval_reflection_method_declaring_class(emitter); emit_aarch64_eval_reflection_property_flags(emitter); label_c_global(emitter, "__elephc_eval_value_is_a"); @@ -1594,6 +1595,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emit_x86_64_eval_reflection_method_names(emitter); emit_x86_64_eval_reflection_property_names(emitter); emit_x86_64_eval_reflection_method_flags(emitter); + emit_x86_64_eval_reflection_method_declaring_class(emitter); emit_x86_64_eval_reflection_property_flags(emitter); label_c_global(emitter, "__elephc_eval_value_is_a"); @@ -2982,6 +2984,7 @@ fn emit_aarch64_eval_reflection_method_names(emitter: &mut Emitter) { "_eval_reflection_methods", "__elephc_eval_reflection_method_names", "method", + 56, ); } @@ -2994,6 +2997,7 @@ fn emit_aarch64_eval_reflection_property_names(emitter: &mut Emitter) { "_eval_reflection_properties", "__elephc_eval_reflection_property_names", "property", + 40, ); } @@ -3005,6 +3009,7 @@ fn emit_aarch64_eval_reflection_member_names( table_symbol: &str, label_prefix: &str, member_kind: &str, + row_stride: u64, ) { let loop_label = format!("{label_prefix}_loop"); let skip_label = format!("{label_prefix}_skip"); @@ -3061,7 +3066,7 @@ fn emit_aarch64_eval_reflection_member_names( emitter.instruction("ldr x11, [sp, #40]"); // restore the row index after appending the member name emitter.label(&skip_label); emitter.instruction("ldr x10, [sp, #32]"); // reload the current member metadata row - emitter.instruction("add x10, x10, #40"); // advance to the next 40-byte metadata row + emitter.instruction(&format!("add x10, x10, #{row_stride}")); // advance to the next reflection metadata row emitter.instruction("str x10, [sp, #32]"); // persist the advanced row cursor emitter.instruction("add x11, x11, #1"); // advance the row index emitter.instruction(&format!("b {loop_label}")); // continue scanning member metadata rows @@ -3131,7 +3136,7 @@ fn emit_aarch64_eval_reflection_method_flags(emitter: &mut Emitter) { emitter.instruction("b __elephc_eval_reflection_method_flags_done"); // restore the wrapper frame after a match emitter.label("__elephc_eval_reflection_method_flags_skip"); emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row - emitter.instruction("add x10, x10, #40"); // advance to the next 40-byte metadata row + emitter.instruction("add x10, x10, #56"); // advance to the next 56-byte method metadata row emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor emitter.instruction("add x11, x11, #1"); // advance the row index emitter.instruction("b __elephc_eval_reflection_method_flags_loop"); // continue scanning method metadata rows @@ -3143,6 +3148,75 @@ fn emit_aarch64_eval_reflection_method_flags(emitter: &mut Emitter) { emitter.instruction("ret"); // return flags, or zero for a miss, to Rust } +/// Emits the ARM64 eval hook that returns a matched AOT ReflectionMethod declaring class. +fn emit_aarch64_eval_reflection_method_declaring_class(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_method_declaring_class"); + emitter.instruction("sub sp, sp, #96"); // reserve scan state across runtime string comparisons + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested method-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested method-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_method_count"); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection-method row count + emitter.instruction("cbz x9, __elephc_eval_reflection_method_declaring_class_miss"); // an empty table cannot contain the requested method + emitter.instruction("str x9, [sp, #32]"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_methods"); + emitter.instruction("str x10, [sp, #40]"); // save the current method metadata row + emitter.instruction("mov x11, #0"); // start scanning at method metadata row zero + emitter.label("__elephc_eval_reflection_method_declaring_class_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the method metadata row count + emitter.instruction("cmp x11, x9"); // have all method metadata rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_method_declaring_class_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_method_declaring_class_skip"); // length mismatch means the class cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_method_declaring_class_skip"); // class mismatch means the row cannot match + emitter.instruction("ldr x10, [sp, #40]"); // reload the current row for the method-name compare + emitter.instruction("ldr x12, [x10, #24]"); // load the stored method-name length + emitter.instruction("ldr x2, [sp, #24]"); // reload the requested method-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested method-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_method_declaring_class_skip"); // length mismatch means the method cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the method-name compare + emitter.instruction("ldr x1, [sp, #16]"); // pass the requested method-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // pass the requested method-name length + emitter.instruction("ldr x3, [x10, #16]"); // pass the stored method-name pointer + emitter.instruction("mov x4, x12"); // pass the stored method-name length + emitter.instruction("bl __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the method-name compare + emitter.instruction("cmp x0, #0"); // did the requested method name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_method_declaring_class_skip"); // method mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #40]"); // reload the matched method metadata row + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("ldr x1, [x10, #40]"); // load the declaring class-name pointer + emitter.instruction("ldr x2, [x10, #48]"); // load the declaring class-name length + emitter.instruction("bl __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction("b __elephc_eval_reflection_method_declaring_class_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_method_declaring_class_skip"); + emitter.instruction("ldr x10, [sp, #40]"); // reload the current method metadata row + emitter.instruction("add x10, x10, #56"); // advance to the next 56-byte method metadata row + emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_method_declaring_class_loop"); // continue scanning method metadata rows + emitter.label("__elephc_eval_reflection_method_declaring_class_miss"); + emitter.instruction("mov x0, xzr"); // return null when no AOT method metadata matched + emitter.label("__elephc_eval_reflection_method_declaring_class_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the method metadata scan frame + emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust +} + /// Emits the ARM64 eval hook that returns AOT ReflectionProperty predicate flags. fn emit_aarch64_eval_reflection_property_flags(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_reflection_property_flags"); @@ -3218,6 +3292,7 @@ fn emit_x86_64_eval_reflection_method_names(emitter: &mut Emitter) { "_eval_reflection_methods", "__elephc_eval_reflection_method_names_x86", "method", + 56, ); } @@ -3230,6 +3305,7 @@ fn emit_x86_64_eval_reflection_property_names(emitter: &mut Emitter) { "_eval_reflection_properties", "__elephc_eval_reflection_property_names_x86", "property", + 40, ); } @@ -3241,6 +3317,7 @@ fn emit_x86_64_eval_reflection_member_names( table_symbol: &str, label_prefix: &str, member_kind: &str, + row_stride: u64, ) { let loop_label = format!("{label_prefix}_loop"); let skip_label = format!("{label_prefix}_skip"); @@ -3297,7 +3374,7 @@ fn emit_x86_64_eval_reflection_member_names( emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // restore the row index after appending the member name emitter.label(&skip_label); emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the current member metadata row - emitter.instruction("add r10, 40"); // advance to the next 40-byte metadata row + emitter.instruction(&format!("add r10, {row_stride}")); // advance to the next reflection metadata row emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // persist the advanced row cursor emitter.instruction("inc r11"); // advance the row index emitter.instruction(&format!("jmp {loop_label}")); // continue scanning member metadata rows @@ -3364,7 +3441,7 @@ fn emit_x86_64_eval_reflection_method_flags(emitter: &mut Emitter) { emitter.instruction("jmp __elephc_eval_reflection_method_flags_done_x86"); // restore the wrapper frame after a match emitter.label("__elephc_eval_reflection_method_flags_skip_x86"); emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row - emitter.instruction("add r10, 40"); // advance to the next 40-byte metadata row + emitter.instruction("add r10, 56"); // advance to the next 56-byte method metadata row emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor emitter.instruction("inc r11"); // advance the row index emitter.instruction("jmp __elephc_eval_reflection_method_flags_loop_x86"); // continue scanning method metadata rows @@ -3376,6 +3453,72 @@ fn emit_x86_64_eval_reflection_method_flags(emitter: &mut Emitter) { emitter.instruction("ret"); // return flags, or zero for a miss, to Rust } +/// Emits the x86_64 eval hook that returns a matched AOT ReflectionMethod declaring class. +fn emit_x86_64_eval_reflection_method_declaring_class(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_method_declaring_class"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable scan frame pointer + emitter.instruction("sub rsp, 64"); // reserve scan state across runtime string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested method-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested method-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_method_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection-method row count + emitter.instruction("test r10, r10"); // is the method metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_method_declaring_class_miss_x86"); // an empty table cannot contain the requested method + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_methods"); + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the current method metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at method metadata row zero + emitter.label("__elephc_eval_reflection_method_declaring_class_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the method metadata row count + emitter.instruction("cmp r11, r10"); // have all method metadata rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_method_declaring_class_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_method_declaring_class_skip_x86"); // length mismatch means the class cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_method_declaring_class_skip_x86"); // class mismatch means the row cannot match + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current row for the method-name compare + emitter.instruction("mov rcx, QWORD PTR [r10 + 24]"); // load the stored method-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // compare stored and requested method-name lengths + emitter.instruction("jne __elephc_eval_reflection_method_declaring_class_skip_x86"); // length mismatch means the method cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the method-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the requested method-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // pass the requested method-name length + emitter.instruction("mov rdx, QWORD PTR [r10 + 16]"); // pass the stored method-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare method names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the method-name compare + emitter.instruction("test rax, rax"); // did the requested method name match this row? + emitter.instruction("jne __elephc_eval_reflection_method_declaring_class_skip_x86"); // method mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the matched method metadata row + emitter.instruction("mov rdi, QWORD PTR [r10 + 40]"); // load the declaring class-name pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + 48]"); // load the declaring class-name length + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction("jmp __elephc_eval_reflection_method_declaring_class_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_method_declaring_class_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current method metadata row + emitter.instruction("add r10, 56"); // advance to the next 56-byte method metadata row + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_method_declaring_class_loop_x86"); // continue scanning method metadata rows + emitter.label("__elephc_eval_reflection_method_declaring_class_miss_x86"); + emitter.instruction("xor eax, eax"); // return null when no AOT method metadata matched + emitter.label("__elephc_eval_reflection_method_declaring_class_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust +} + /// Emits the x86_64 eval hook that returns AOT ReflectionProperty predicate flags. fn emit_x86_64_eval_reflection_property_flags(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_reflection_property_flags"); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 87d8e8ac23..3128a05f7f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7469,6 +7469,57 @@ try { ); } +/// Verifies eval reports declaring classes for inherited generated/AOT methods and constructors. +#[test] +fn test_eval_reflection_method_declaring_class_for_inherited_aot_members() { + let out = compile_and_run_capture( + r#"getMethod("inherited"); +echo $inherited->getDeclaringClass()->getName() . ":"; +$static = $class->getMethod("baseStatic"); +echo $static->getDeclaringClass()->getName() . ":"; +$own = $class->getMethod("own"); +echo $own->getDeclaringClass()->getName() . ":"; +$ctor = $class->getConstructor(); +echo $ctor->getDeclaringClass()->getName() . "/" . $ctor->getNumberOfParameters() . ":"; +$listed = null; +foreach ($class->getMethods() as $method) { + if ($method->getName() === "inherited") { + $listed = $method; + } +} +echo $listed->getDeclaringClass()->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotReflectDeclaringBase:EvalAotReflectDeclaringBase:EvalAotReflectDeclaringChild:EvalAotReflectDeclaringBase/1:EvalAotReflectDeclaringBase" + ); +} + /// Verifies eval ReflectionMethod::invoke can dispatch public generated/AOT methods. #[test] fn test_eval_reflection_method_invoke_calls_aot_method() { From 0745bce943638e9f7418d5ffb551847d316740e2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 21:11:08 +0200 Subject: [PATCH 0487/1208] Fix eval AOT property declaring reflection --- .../elephc-eval/src/interpreter/reflection.rs | 5 +- .../src/interpreter/runtime_ops.rs | 7 + .../interpreter/tests/support/object_ops.rs | 12 ++ .../interpreter/tests/support/runtime_ops.rs | 8 + .../elephc-eval/src/runtime_hooks/externs.rs | 6 + crates/elephc-eval/src/runtime_hooks/ops.rs | 25 +++ src/codegen_support/runtime/data/user.rs | 58 ++++++- src/codegen_support/runtime/eval_bridge.rs | 145 +++++++++++++++++- tests/codegen/eval.rs | 40 +++++ 9 files changed, 300 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 5997db2c19..1b035efcf7 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1727,8 +1727,11 @@ fn eval_reflection_aot_property_metadata_if_exists( let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { return Ok(None); }; + let declaring_class_name = values + .reflection_property_declaring_class(runtime_class_name, property_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); Ok(Some(eval_reflection_aot_property_metadata( - runtime_class_name, + &declaring_class_name, flags, ))) } diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 2e2cac45e9..6504b85e2d 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -166,6 +166,13 @@ pub trait RuntimeValueOps { property_name: &str, ) -> Result, EvalStatus>; + /// Returns the generated AOT declaring class for a class/property pair. + fn reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus>; + /// Returns generated AOT ReflectionProperty names visible for one class. fn reflection_property_names( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index f6633d722c..73d85bb245 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -949,6 +949,18 @@ impl FakeOps { } Ok(None) } + /// Reports fake generated AOT ReflectionProperty declaring classes for metadata unit tests. + pub(super) fn runtime_reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + if class_name.eq_ignore_ascii_case("KnownClass") && property_name == "promoted" { + Ok(Some("KnownClass".to_string())) + } else { + Ok(None) + } + } /// Reports fake generated AOT ReflectionProperty names for eval metadata unit tests. pub(super) fn runtime_reflection_property_names( &mut self, diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index 379092399a..fbbb5e53fd 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -198,6 +198,14 @@ impl RuntimeValueOps for FakeOps { ) -> Result, EvalStatus> { self.runtime_reflection_property_flags(class_name, property_name) } + /// Reports fake generated AOT ReflectionProperty declaring classes for metadata bridge tests. + fn reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_property_declaring_class(class_name, property_name) + } /// Reports fake generated AOT ReflectionProperty names for metadata bridge tests. fn reflection_property_names( &mut self, diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index b901968de8..787fc50330 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -119,6 +119,12 @@ unsafe extern "C" { property_ptr: *const u8, property_len: u64, ) -> u64; + pub(super) fn __elephc_eval_reflection_property_declaring_class( + class_ptr: *const u8, + class_len: u64, + property_ptr: *const u8, + property_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_reflection_property_names( class_ptr: *const u8, class_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index a094caba9b..9c1e7672dd 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -318,6 +318,31 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok((flags != 0).then_some(flags)) } + /// Returns generated AOT ReflectionProperty declaring class metadata. + fn reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_property_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + property_name.as_ptr(), + property_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + /// Returns generated AOT ReflectionProperty names visible for one class. fn reflection_property_names( &mut self, diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 0c878d641f..4e8212941b 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -1140,6 +1140,12 @@ fn emit_eval_reflection_property_lookup_data( let flags = eval_reflection_instance_property_flags(class_info, slot, property_name); let class_label = format!("_eval_reflection_property_class_{}", index); let property_label = format!("_eval_reflection_property_name_{}", index); + let declaring_class = eval_reflection_instance_property_declaring_class( + class_name, + class_info, + property_name, + ); + let declaring_label = format!("_eval_reflection_property_declaring_class_{}", index); out.push_str(&format!( ".globl {0}\n{0}:\n .ascii \"{1}\"\n", class_label, @@ -1150,12 +1156,19 @@ fn emit_eval_reflection_property_lookup_data( property_label, escaped_ascii(property_name) )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + declaring_label, + escaped_ascii(declaring_class) + )); entries.push(( class_label, class_name.len(), property_label, property_name.len(), flags, + declaring_label, + declaring_class.len(), )); index += 1; } @@ -1163,6 +1176,12 @@ fn emit_eval_reflection_property_lookup_data( let flags = eval_reflection_static_property_flags(class_info, slot, property_name); let class_label = format!("_eval_reflection_property_class_{}", index); let property_label = format!("_eval_reflection_property_name_{}", index); + let declaring_class = eval_reflection_static_property_declaring_class( + class_name, + class_info, + property_name, + ); + let declaring_label = format!("_eval_reflection_property_declaring_class_{}", index); out.push_str(&format!( ".globl {0}\n{0}:\n .ascii \"{1}\"\n", class_label, @@ -1173,12 +1192,19 @@ fn emit_eval_reflection_property_lookup_data( property_label, escaped_ascii(property_name) )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + declaring_label, + escaped_ascii(declaring_class) + )); entries.push(( class_label, class_name.len(), property_label, property_name.len(), flags, + declaring_label, + declaring_class.len(), )); index += 1; } @@ -1188,15 +1214,45 @@ fn emit_eval_reflection_property_lookup_data( out.push_str(".globl _eval_reflection_property_count\n_eval_reflection_property_count:\n"); out.push_str(&format!(" .quad {}\n", entries.len())); out.push_str(".globl _eval_reflection_properties\n_eval_reflection_properties:\n"); - for (class_label, class_len, property_label, property_len, flags) in entries { + for (class_label, class_len, property_label, property_len, flags, declaring_label, declaring_len) in + entries + { out.push_str(&format!(" .quad {}\n", class_label)); out.push_str(&format!(" .quad {}\n", class_len)); out.push_str(&format!(" .quad {}\n", property_label)); out.push_str(&format!(" .quad {}\n", property_len)); out.push_str(&format!(" .quad {}\n", flags)); + out.push_str(&format!(" .quad {}\n", declaring_label)); + out.push_str(&format!(" .quad {}\n", declaring_len)); } } +/// Returns the class name that declares one visible instance property. +fn eval_reflection_instance_property_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .property_declaring_classes + .get(property_name) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one visible static property. +fn eval_reflection_static_property_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .static_property_declaring_classes + .get(property_name) + .map(String::as_str) + .unwrap_or(reflected_class) +} + /// Returns eval ReflectionProperty bitflags for one instance property slot. fn eval_reflection_instance_property_flags( class_info: &ClassInfo, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 4b9df203fc..707fc3f169 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -175,6 +175,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emit_aarch64_eval_reflection_property_names(emitter); emit_aarch64_eval_reflection_method_flags(emitter); emit_aarch64_eval_reflection_method_declaring_class(emitter); + emit_aarch64_eval_reflection_property_declaring_class(emitter); emit_aarch64_eval_reflection_property_flags(emitter); label_c_global(emitter, "__elephc_eval_value_is_a"); @@ -1596,6 +1597,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emit_x86_64_eval_reflection_property_names(emitter); emit_x86_64_eval_reflection_method_flags(emitter); emit_x86_64_eval_reflection_method_declaring_class(emitter); + emit_x86_64_eval_reflection_property_declaring_class(emitter); emit_x86_64_eval_reflection_property_flags(emitter); label_c_global(emitter, "__elephc_eval_value_is_a"); @@ -2997,7 +2999,7 @@ fn emit_aarch64_eval_reflection_property_names(emitter: &mut Emitter) { "_eval_reflection_properties", "__elephc_eval_reflection_property_names", "property", - 40, + 56, ); } @@ -3217,6 +3219,75 @@ fn emit_aarch64_eval_reflection_method_declaring_class(emitter: &mut Emitter) { emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust } +/// Emits the ARM64 eval hook that returns a matched AOT ReflectionProperty declaring class. +fn emit_aarch64_eval_reflection_property_declaring_class(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_property_declaring_class"); + emitter.instruction("sub sp, sp, #96"); // reserve scan state across runtime string comparisons + emitter.instruction("stp x29, x30, [sp, #80]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #80"); // establish a stable scan frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_property_count"); + emitter.instruction("ldr x9, [x9]"); // load the AOT reflection-property row count + emitter.instruction("cbz x9, __elephc_eval_reflection_property_declaring_class_miss"); // an empty table cannot contain the requested property + emitter.instruction("str x9, [sp, #32]"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_properties"); + emitter.instruction("str x10, [sp, #40]"); // save the current property metadata row + emitter.instruction("mov x11, #0"); // start scanning at property metadata row zero + emitter.label("__elephc_eval_reflection_property_declaring_class_loop"); + emitter.instruction("ldr x9, [sp, #32]"); // reload the property metadata row count + emitter.instruction("cmp x11, x9"); // have all property metadata rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_property_declaring_class_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_property_declaring_class_skip"); // length mismatch means the class cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_property_declaring_class_skip"); // class mismatch means the row cannot match + emitter.instruction("ldr x10, [sp, #40]"); // reload the current row for the property-name compare + emitter.instruction("ldr x12, [x10, #24]"); // load the stored property-name length + emitter.instruction("ldr x2, [sp, #24]"); // reload the requested property-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested property-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_property_declaring_class_skip"); // length mismatch means the property cannot match + emitter.instruction("str x11, [sp, #48]"); // save the row index across the property-name compare + emitter.instruction("ldr x1, [sp, #16]"); // pass the requested property-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // pass the requested property-name length + emitter.instruction("ldr x3, [x10, #16]"); // pass the stored property-name pointer + emitter.instruction("mov x4, x12"); // pass the stored property-name length + emitter.instruction("bl __rt_strcasecmp"); // compare property names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #48]"); // restore the row index after the property-name compare + emitter.instruction("cmp x0, #0"); // did the requested property name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_property_declaring_class_skip"); // property mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #40]"); // reload the matched property metadata row + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("ldr x1, [x10, #40]"); // load the declaring class-name pointer + emitter.instruction("ldr x2, [x10, #48]"); // load the declaring class-name length + emitter.instruction("bl __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction("b __elephc_eval_reflection_property_declaring_class_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_property_declaring_class_skip"); + emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row + emitter.instruction("add x10, x10, #56"); // advance to the next 56-byte property metadata row + emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_property_declaring_class_loop"); // continue scanning property metadata rows + emitter.label("__elephc_eval_reflection_property_declaring_class_miss"); + emitter.instruction("mov x0, xzr"); // return null when no AOT property metadata matched + emitter.label("__elephc_eval_reflection_property_declaring_class_done"); + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the property metadata scan frame + emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust +} + /// Emits the ARM64 eval hook that returns AOT ReflectionProperty predicate flags. fn emit_aarch64_eval_reflection_property_flags(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_reflection_property_flags"); @@ -3271,7 +3342,7 @@ fn emit_aarch64_eval_reflection_property_flags(emitter: &mut Emitter) { emitter.instruction("b __elephc_eval_reflection_property_flags_done"); // restore the wrapper frame after a match emitter.label("__elephc_eval_reflection_property_flags_skip"); emitter.instruction("ldr x10, [sp, #40]"); // reload the current property metadata row - emitter.instruction("add x10, x10, #40"); // advance to the next 40-byte metadata row + emitter.instruction("add x10, x10, #56"); // advance to the next 56-byte property metadata row emitter.instruction("str x10, [sp, #40]"); // persist the advanced row cursor emitter.instruction("add x11, x11, #1"); // advance the row index emitter.instruction("b __elephc_eval_reflection_property_flags_loop"); // continue scanning property metadata rows @@ -3305,7 +3376,7 @@ fn emit_x86_64_eval_reflection_property_names(emitter: &mut Emitter) { "_eval_reflection_properties", "__elephc_eval_reflection_property_names_x86", "property", - 40, + 56, ); } @@ -3519,6 +3590,72 @@ fn emit_x86_64_eval_reflection_method_declaring_class(emitter: &mut Emitter) { emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust } +/// Emits the x86_64 eval hook that returns a matched AOT ReflectionProperty declaring class. +fn emit_x86_64_eval_reflection_property_declaring_class(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_property_declaring_class"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable scan frame pointer + emitter.instruction("sub rsp, 64"); // reserve scan state across runtime string comparisons + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_property_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the AOT reflection-property row count + emitter.instruction("test r10, r10"); // is the property metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_property_declaring_class_miss_x86"); // an empty table cannot contain the requested property + emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save the table count across string comparisons + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_properties"); + emitter.instruction("mov QWORD PTR [rbp - 48], r11"); // save the current property metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at property metadata row zero + emitter.label("__elephc_eval_reflection_property_declaring_class_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the property metadata row count + emitter.instruction("cmp r11, r10"); // have all property metadata rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_property_declaring_class_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_property_declaring_class_skip_x86"); // length mismatch means the class cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_property_declaring_class_skip_x86"); // class mismatch means the row cannot match + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current row for the property-name compare + emitter.instruction("mov rcx, QWORD PTR [r10 + 24]"); // load the stored property-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 32]"); // compare stored and requested property-name lengths + emitter.instruction("jne __elephc_eval_reflection_property_declaring_class_skip_x86"); // length mismatch means the property cannot match + emitter.instruction("mov QWORD PTR [rbp - 56], r11"); // save the row index across the property-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // pass the requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // pass the requested property-name length + emitter.instruction("mov rdx, QWORD PTR [r10 + 16]"); // pass the stored property-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare property names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // restore the row index after the property-name compare + emitter.instruction("test rax, rax"); // did the requested property name match this row? + emitter.instruction("jne __elephc_eval_reflection_property_declaring_class_skip_x86"); // property mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the matched property metadata row + emitter.instruction("mov rdi, QWORD PTR [r10 + 40]"); // load the declaring class-name pointer + emitter.instruction("mov rsi, QWORD PTR [r10 + 48]"); // load the declaring class-name length + emitter.instruction("mov eax, 1"); // runtime tag 1 = string + emitter.instruction("call __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction("jmp __elephc_eval_reflection_property_declaring_class_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_property_declaring_class_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row + emitter.instruction("add r10, 56"); // advance to the next 56-byte property metadata row + emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_property_declaring_class_loop_x86"); // continue scanning property metadata rows + emitter.label("__elephc_eval_reflection_property_declaring_class_miss_x86"); + emitter.instruction("xor eax, eax"); // return null when no AOT property metadata matched + emitter.label("__elephc_eval_reflection_property_declaring_class_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the declaring class string, or null for a miss, to Rust +} + /// Emits the x86_64 eval hook that returns AOT ReflectionProperty predicate flags. fn emit_x86_64_eval_reflection_property_flags(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_reflection_property_flags"); @@ -3570,7 +3707,7 @@ fn emit_x86_64_eval_reflection_property_flags(emitter: &mut Emitter) { emitter.instruction("jmp __elephc_eval_reflection_property_flags_done_x86"); // restore the wrapper frame after a match emitter.label("__elephc_eval_reflection_property_flags_skip_x86"); emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the current property metadata row - emitter.instruction("add r10, 40"); // advance to the next 40-byte metadata row + emitter.instruction("add r10, 56"); // advance to the next 56-byte property metadata row emitter.instruction("mov QWORD PTR [rbp - 48], r10"); // persist the advanced row cursor emitter.instruction("inc r11"); // advance the row index emitter.instruction("jmp __elephc_eval_reflection_property_flags_loop_x86"); // continue scanning property metadata rows diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3128a05f7f..758174e1a9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7383,6 +7383,46 @@ try { ); } +/// Verifies eval reports declaring classes for inherited generated/AOT properties. +#[test] +fn test_eval_reflection_property_declaring_class_for_inherited_aot_members() { + let out = compile_and_run_capture( + r#"getProperty("base"); +echo $base->getDeclaringClass()->getName() . ":"; +$static = $class->getProperty("baseStatic"); +echo $static->getDeclaringClass()->getName() . ":"; +$own = $class->getProperty("own"); +echo $own->getDeclaringClass()->getName() . ":"; +$listed = null; +foreach ($class->getProperties() as $property) { + if ($property->getName() === "base") { + $listed = $property; + } +} +echo $listed->getDeclaringClass()->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotReflectPropertyDeclaringBase:EvalAotReflectPropertyDeclaringBase:EvalAotReflectPropertyDeclaringChild:EvalAotReflectPropertyDeclaringBase" + ); +} + /// Verifies eval can probe generated/AOT method predicate metadata through /// `ReflectionClass::hasMethod()`, `getMethod()`, and direct `ReflectionMethod`. #[test] From c9ae1da9e2e996214aa11d75bc307587d7b21d07 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 21:18:59 +0200 Subject: [PATCH 0488/1208] Support AOT parents in eval reflection --- .../elephc-eval/src/interpreter/reflection.rs | 70 ++++++++++++++++++- docs/php/eval.md | 5 +- tests/codegen/eval.rs | 26 +++++++ 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 1b035efcf7..e6bd1c62f0 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1243,6 +1243,7 @@ fn eval_reflection_class_new( &reflected_name, values, )?; + let parent_class_name = eval_reflection_aot_parent_class_name(&reflected_name, values)?; return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, &reflected_name, @@ -1251,7 +1252,7 @@ fn eval_reflection_class_new( &[], &method_names, &property_names, - None, + parent_class_name.as_deref(), &[], None, None, @@ -2137,7 +2138,41 @@ fn eval_reflection_full_class_object_result( values: &mut impl RuntimeValueOps, ) -> Result { let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { - return values.bool_value(false); + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { + return values.bool_value(false); + }; + let runtime_class_name = class_name.trim_start_matches('\\'); + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + runtime_class_name, + values, + )?; + let property_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + runtime_class_name, + values, + )?; + let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; + return eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS, + runtime_class_name, + &[], + &[], + &[], + &method_names, + &property_names, + parent_class_name.as_deref(), + &[], + None, + None, + flags, + modifiers, + 0, + None, + None, + context, + values, + ); }; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, @@ -2216,6 +2251,37 @@ fn eval_reflection_shallow_class_object_result( ) } +/// Returns the generated/AOT parent class name for a reflected class, if any. +fn eval_reflection_aot_parent_class_name( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let class_cell = values.string(runtime_class_name)?; + let parent_cell = match values.parent_class_name(class_cell) { + Ok(parent_cell) => parent_cell, + Err(err) => { + values.release(class_cell)?; + return Err(err); + } + }; + values.release(class_cell)?; + let parent_bytes = match values.string_bytes(parent_cell) { + Ok(parent_bytes) => parent_bytes, + Err(err) => { + values.release(parent_cell)?; + return Err(err); + } + }; + values.release(parent_cell)?; + let parent_name = String::from_utf8(parent_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + if parent_name.is_empty() { + Ok(None) + } else { + Ok(Some(parent_name)) + } +} + /// Builds an indexed PHP string array for ReflectionClass metadata names. fn eval_reflection_string_array_result( names: &[String], diff --git a/docs/php/eval.md b/docs/php/eval.md index 0315ec39aa..d8afdcf0f9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -276,8 +276,9 @@ methods that are not overridden report no prototype, matching PHP reflection. constructors, including registered generated/AOT constructor parameter names, counts, and scalar/null defaults where available; it returns `null` when no constructor is visible. `ReflectionClass::getParentClass()` -returns a materialized `ReflectionClass` for eval-declared parent classes or -`false` when no parent class exists. `ReflectionClass::newInstance()` constructs +returns a materialized `ReflectionClass` for eval-declared and generated/AOT +parent classes or `false` when no parent class exists. +`ReflectionClass::newInstance()` constructs eval-declared reflected classes and forwards constructor arguments through eval's positional, named, and unpacking-aware call binding. `ReflectionClass::newInstanceArgs()` constructs eval-declared reflected classes diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 758174e1a9..a84f3ba594 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6782,6 +6782,32 @@ if ($root === false) { assert_eq!(out.stdout, "EvalBridgeParent:false"); } +/// Verifies eval ReflectionClass::getParentClass materializes generated/AOT parents. +#[test] +fn test_eval_reflection_class_get_parent_class_for_aot_class() { + let out = compile_and_run_capture( + r#"getParentClass(); +if ($parent === false) { + echo "missing"; +} else { + echo $parent->getName(); +} +echo ":"; +$root = (new ReflectionClass("EvalAotReflectParentBase"))->getParentClass(); +echo $root === false ? "false" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "EvalAotReflectParentBase:false"); +} + /// Verifies eval ReflectionClass::getConstructor crosses the generated runtime bridge. #[test] fn test_eval_reflection_class_get_constructor() { From 536f45f264c03e3967f1a32c2a4ff832f847f756 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 21:26:08 +0200 Subject: [PATCH 0489/1208] Support AOT interface reflection predicates --- .../elephc-eval/src/interpreter/reflection.rs | 18 +++++++++------ .../tests/builtins_class_metadata.rs | 19 ++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 5 +++++ docs/php/eval.md | 9 ++++---- tests/codegen/eval.rs | 22 +++++++++++++++++++ 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index e6bd1c62f0..8bf9424449 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -222,13 +222,17 @@ pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( values, ); } - values - .bool_value(eval_reflection_class_implements_interface_name( - &reflected_name, - &interface_name, - context, - )) - .map(Some) + let result = if eval_reflection_class_like_exists(&reflected_name, context) { + eval_reflection_class_implements_interface_name(&reflected_name, &interface_name, context) + } else if values.interface_exists(&reflected_name)? { + eval_reflection_same_class_like_name(&reflected_name, &interface_name) + } else { + let reflected_class = values.string(&reflected_name)?; + let result = values.object_is_a(reflected_class, &interface_name, false); + values.release(reflected_class)?; + result? + }; + values.bool_value(result).map(Some) } /// Handles eval-backed `ReflectionClass::isSubclassOf()` calls. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 4078005878..2e9ef9ebe3 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -489,6 +489,25 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::implementsInterface checks fake generated/AOT relations. +#[test] +fn execute_program_reflects_aot_class_implements_interface_predicate() { + let program = parse_fragment( + br#"$ref = new ReflectionClass("KnownClass"); +echo $ref->implementsInterface("KnownInterface") ? "Y" : "N"; echo ":"; +echo $ref->implementsInterface("Iterator") ? "bad" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::implementsInterface rejects non-interface names with catchable errors. #[test] fn execute_program_reflection_class_implements_interface_rejects_non_interfaces() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 73d85bb245..581cbd7847 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -1096,6 +1096,11 @@ fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: { return true; } + if class_name.eq_ignore_ascii_case("KnownClass") + && target_class.eq_ignore_ascii_case("KnownInterface") + { + return true; + } if target_class.eq_ignore_ascii_case("Throwable") { return fake_runtime_exception_like_class(class_name); } diff --git a/docs/php/eval.md b/docs/php/eval.md index d8afdcf0f9..bdef4ec299 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -228,10 +228,11 @@ user-defined class-like symbols. modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval classes and parent interfaces for eval interfaces. -`ReflectionClass::implementsInterface()` checks those relations -case-insensitively and returns true when reflecting the requested interface -itself. It throws catchable `ReflectionException` values when the argument names -a class, trait, enum, or missing interface, while `ReflectionClass::getTraitNames()` +`ReflectionClass::implementsInterface()` checks those eval relations +case-insensitively, returns true when reflecting the requested interface itself, +and checks generated/AOT class-interface relations through runtime metadata. It +throws catchable `ReflectionException` values when the argument names a class, +trait, enum, or missing interface, while `ReflectionClass::getTraitNames()` returns traits used directly by eval classes. `ReflectionClass::isSubclassOf()` checks eval parent-class chains and implemented or extended interfaces case-insensitively. It excludes the reflected diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a84f3ba594..2e416bcc09 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6583,6 +6583,28 @@ echo (new ReflectionClass("EvalImplTrait"))->implementsInterface("EvalImplBase") assert_eq!(out.stdout, "CBEIPt"); } +/// Verifies eval ReflectionClass::implementsInterface uses generated/AOT relations. +#[test] +fn test_eval_reflection_class_implements_interface_for_aot_class() { + let out = compile_and_run_capture( + r#"implementsInterface("EvalAotReflectImplChild") ? "C" : "c"; +echo $ref->implementsInterface("evalaotreflectimplbase") ? "B" : "b"; +echo $ref->implementsInterface("Iterator") ? "I" : "i";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "CBi"); +} + /// Verifies eval `ReflectionClass::implementsInterface()` throws ReflectionException /// for missing or non-interface argument names. #[test] From 6935c65685871ac67fa05dc85d376c022c698dc5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 21:38:21 +0200 Subject: [PATCH 0490/1208] Expose AOT interface names in eval reflection --- .../elephc-eval/src/interpreter/reflection.rs | 21 ++- .../src/interpreter/runtime_ops.rs | 6 + .../tests/builtins_class_metadata.rs | 20 +++ .../interpreter/tests/support/object_ops.rs | 13 ++ .../interpreter/tests/support/runtime_ops.rs | 7 + .../elephc-eval/src/runtime_hooks/externs.rs | 4 + crates/elephc-eval/src/runtime_hooks/ops.rs | 13 ++ docs/php/eval.md | 3 +- src/codegen_support/runtime/data/user.rs | 128 ++++++++++++++++++ src/codegen_support/runtime/eval_bridge.rs | 28 ++++ tests/codegen/eval.rs | 25 ++++ 11 files changed, 264 insertions(+), 4 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 8bf9424449..524b173489 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1247,12 +1247,13 @@ fn eval_reflection_class_new( &reflected_name, values, )?; + let interface_names = eval_reflection_aot_class_interface_names(&reflected_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(&reflected_name, values)?; return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, &reflected_name, &[], - &[], + &interface_names, &[], &method_names, &property_names, @@ -2156,12 +2157,13 @@ fn eval_reflection_full_class_object_result( runtime_class_name, values, )?; + let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, runtime_class_name, &[], - &[], + &interface_names, &[], &method_names, &property_names, @@ -2210,11 +2212,12 @@ fn eval_reflection_shallow_class_object_result( let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { return values.bool_value(false); }; + let interface_names = eval_reflection_aot_class_interface_names(class_name, values)?; return eval_reflection_owner_object_with_members( EVAL_REFLECTION_OWNER_CLASS, class_name.trim_start_matches('\\'), &[], - &[], + &interface_names, &[], &[], &[], @@ -2813,6 +2816,18 @@ fn eval_reflection_aot_member_names( Ok(names) } +/// Returns generated AOT interface names for one reflected class-like symbol. +fn eval_reflection_aot_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = values.reflection_class_interface_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + /// Copies a runtime string array into Rust-owned strings for reflection metadata assembly. fn eval_reflection_string_array_to_vec( array: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 6504b85e2d..93cf0f3a9d 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -179,6 +179,12 @@ pub trait RuntimeValueOps { class_name: &str, ) -> Result; + /// Returns generated/AOT interface names visible for one reflected class-like symbol. + fn reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result; + /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 2e9ef9ebe3..85743932ee 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -461,6 +461,26 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::getInterfaceNames reads fake generated/AOT metadata. +#[test] +fn execute_program_reflects_aot_class_interface_names() { + let program = parse_fragment( + br#"$class_names = (new ReflectionClass("KnownClass"))->getInterfaceNames(); +echo count($class_names); echo ":"; echo $class_names[0]; echo ":"; +$interface_names = (new ReflectionClass("KnownInterface"))->getInterfaceNames(); +echo count($interface_names); echo ":"; echo $interface_names[0]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:KnownInterface:1:Traversable"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::implementsInterface reports eval class, enum, and /// interface metadata using case-insensitive interface names. #[test] diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 581cbd7847..c5073b6a5d 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -972,6 +972,19 @@ impl FakeOps { } Ok(array) } + /// Reports fake generated/AOT ReflectionClass interface names for metadata unit tests. + pub(super) fn runtime_reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownInterface")?; + } else if class_name.eq_ignore_ascii_case("KnownInterface") { + array = self.runtime_string_array_push(array, "Traversable")?; + } + Ok(array) + } /// Reports one fake AOT interface for eval `interface_exists` unit tests. pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { Ok([ diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index fbbb5e53fd..c182757426 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -213,6 +213,13 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_reflection_property_names(class_name) } + /// Reports fake generated/AOT ReflectionClass interface names for metadata bridge tests. + fn reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_interface_names(class_name) + } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { self.runtime_new_object(_class_name) diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 787fc50330..3728461241 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -129,6 +129,10 @@ unsafe extern "C" { class_ptr: *const u8, class_len: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_interface_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, name_len: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index 9c1e7672dd..da664837b4 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -353,6 +353,19 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns generated AOT interface names visible for one reflected class-like symbol. + fn reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_interface_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { let object = Self::handle(unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index bdef4ec299..8ff6a27aa8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -227,7 +227,8 @@ user-defined class-like symbols. `ReflectionClass::getModifiers()` returns PHP's `ReflectionClass::IS_*` modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval -classes and parent interfaces for eval interfaces. +and generated/AOT classes, plus parent interfaces for eval and generated/AOT +interfaces. `ReflectionClass::implementsInterface()` checks those eval relations case-insensitively, returns true when reflecting the requested interface itself, and checks generated/AOT class-interface relations through runtime metadata. It diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 4e8212941b..6ea0cb81de 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -473,6 +473,8 @@ pub(crate) fn emit_runtime_data_user( emit_eval_reflection_method_lookup_data(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_interface_lookup_data(&mut out, &sorted_classes, interfaces); // -- class-level PHP 8 attribute metadata table -- // Per-class layout: count followed by (name_ptr, name_len) pairs. @@ -1253,6 +1255,132 @@ fn eval_reflection_static_property_declaring_class<'a>( .unwrap_or(reflected_class) } +/// Emits class-like/interface-name rows consumed by eval ReflectionClass metadata probes. +fn emit_eval_reflection_class_interface_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], + interfaces: &HashMap, +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + for interface_name in &class_info.interfaces { + push_eval_reflection_class_interface_row( + out, + &mut entries, + &mut index, + class_name, + interface_name, + ); + } + } + + let mut sorted_interfaces: Vec<&String> = interfaces.keys().collect(); + sorted_interfaces.sort(); + for interface_name in sorted_interfaces { + for parent_name in eval_reflection_interface_parent_names(interface_name, interfaces) { + push_eval_reflection_class_interface_row( + out, + &mut entries, + &mut index, + interface_name, + &parent_name, + ); + } + } + + out.push_str(".p2align 3\n"); + out.push_str( + ".globl _eval_reflection_class_interface_count\n_eval_reflection_class_interface_count:\n", + ); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_class_interfaces\n_eval_reflection_class_interfaces:\n"); + for (class_label, class_len, interface_label, interface_len) in entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", interface_label)); + out.push_str(&format!(" .quad {}\n", interface_len)); + } +} + +/// Adds one class-like/interface-name row and its backing string labels. +fn push_eval_reflection_class_interface_row( + out: &mut String, + entries: &mut Vec<(String, usize, String, usize)>, + index: &mut usize, + class_name: &str, + interface_name: &str, +) { + let class_label = format!("_eval_reflection_class_interface_class_{}", *index); + let interface_label = format!("_eval_reflection_class_interface_name_{}", *index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + interface_label, + escaped_ascii(interface_name) + )); + entries.push(( + class_label, + class_name.len(), + interface_label, + interface_name.len(), + )); + *index += 1; +} + +/// Returns direct and inherited parent interface names for one generated interface. +fn eval_reflection_interface_parent_names( + interface_name: &str, + interfaces: &HashMap, +) -> Vec { + let mut names = Vec::new(); + collect_eval_reflection_interface_parent_names(interface_name, interfaces, &mut names); + names +} + +/// Recursively appends interface parents without duplicating case-insensitive names. +fn collect_eval_reflection_interface_parent_names( + interface_name: &str, + interfaces: &HashMap, + names: &mut Vec, +) { + let Some((_, interface_info)) = eval_reflection_interface_entry(interface_name, interfaces) + else { + return; + }; + for parent in &interface_info.parents { + let parent_name = eval_reflection_interface_entry(parent, interfaces) + .map(|(name, _)| name.to_string()) + .unwrap_or_else(|| parent.clone()); + if names + .iter() + .any(|name| php_symbol_key(name) == php_symbol_key(&parent_name)) + { + continue; + } + names.push(parent_name.clone()); + collect_eval_reflection_interface_parent_names(&parent_name, interfaces, names); + } +} + +/// Returns the canonical generated interface entry for a possibly case-varied name. +fn eval_reflection_interface_entry<'a>( + interface_name: &str, + interfaces: &'a HashMap, +) -> Option<(&'a str, &'a InterfaceInfo)> { + if let Some((name, info)) = interfaces.get_key_value(interface_name) { + return Some((name.as_str(), info)); + } + interfaces + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case(interface_name)) + .map(|(name, info)| (name.as_str(), info)) +} + /// Returns eval ReflectionProperty bitflags for one instance property slot. fn eval_reflection_instance_property_flags( class_info: &ClassInfo, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 707fc3f169..432314214f 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -173,6 +173,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emit_aarch64_eval_reflection_method_names(emitter); emit_aarch64_eval_reflection_property_names(emitter); + emit_aarch64_eval_reflection_class_interface_names(emitter); emit_aarch64_eval_reflection_method_flags(emitter); emit_aarch64_eval_reflection_method_declaring_class(emitter); emit_aarch64_eval_reflection_property_declaring_class(emitter); @@ -1595,6 +1596,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emit_x86_64_eval_reflection_method_names(emitter); emit_x86_64_eval_reflection_property_names(emitter); + emit_x86_64_eval_reflection_class_interface_names(emitter); emit_x86_64_eval_reflection_method_flags(emitter); emit_x86_64_eval_reflection_method_declaring_class(emitter); emit_x86_64_eval_reflection_property_declaring_class(emitter); @@ -3003,6 +3005,19 @@ fn emit_aarch64_eval_reflection_property_names(emitter: &mut Emitter) { ); } +/// Emits the ARM64 eval hook that returns AOT ReflectionClass interface names. +fn emit_aarch64_eval_reflection_class_interface_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_interface_names", + "_eval_reflection_class_interface_count", + "_eval_reflection_class_interfaces", + "__elephc_eval_reflection_class_interface_names", + "class interface", + 32, + ); +} + /// Emits an ARM64 class-filtered AOT reflection member-name scanner. fn emit_aarch64_eval_reflection_member_names( emitter: &mut Emitter, @@ -3380,6 +3395,19 @@ fn emit_x86_64_eval_reflection_property_names(emitter: &mut Emitter) { ); } +/// Emits the x86_64 eval hook that returns AOT ReflectionClass interface names. +fn emit_x86_64_eval_reflection_class_interface_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_interface_names", + "_eval_reflection_class_interface_count", + "_eval_reflection_class_interfaces", + "__elephc_eval_reflection_class_interface_names_x86", + "class interface", + 32, + ); +} + /// Emits an x86_64 class-filtered AOT reflection member-name scanner. fn emit_x86_64_eval_reflection_member_names( emitter: &mut Emitter, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2e416bcc09..69eea34775 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6556,6 +6556,31 @@ echo count($parentInterfaces) . ":" . $parentInterfaces[0];'); ); } +/// Verifies eval ReflectionClass exposes generated/AOT implemented interface names. +#[test] +fn test_eval_reflection_class_get_interface_names_for_aot_class() { + let out = compile_and_run_capture( + r#"getInterfaceNames(); +sort($interfaces); +echo count($interfaces) . ":"; +echo implode(",", $interfaces);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "2:EvalAotReflectIfaceBase,EvalAotReflectIfaceChild" + ); +} + /// Verifies eval ReflectionClass::implementsInterface reports class, enum, and /// interface metadata through the bridge. #[test] From d9fdd276e51abcbde8f092b048feab0895e3eec8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 22:00:25 +0200 Subject: [PATCH 0491/1208] Expose AOT method types in eval reflection --- crates/elephc-eval/src/context.rs | 108 +++++- crates/elephc-eval/src/ffi/native_methods.rs | 359 ++++++++++++++++++ crates/elephc-eval/src/ffi/tests.rs | 87 +++++ .../elephc-eval/src/interpreter/reflection.rs | 79 ++-- docs/php/eval.md | 21 +- src/codegen/lower_inst/builtins/eval.rs | 269 ++++++++++++- tests/codegen/eval.rs | 56 ++- 7 files changed, 939 insertions(+), 40 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index c4fc4738c6..523a346b94 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -17,8 +17,8 @@ use std::ffi::c_void; use crate::abi::ABI_VERSION; use crate::eval_ir::{ EvalAttribute, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, - EvalFunction, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalTrait, - EvalVisibility, + EvalFunction, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, + EvalTrait, EvalVisibility, }; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; @@ -129,7 +129,9 @@ pub enum NativeCallableDefault { pub struct NativeCallableSignature { param_count: usize, param_names: Vec, + param_types: Vec>, param_defaults: Vec>, + return_type: Option, } impl NativeCallableSignature { @@ -138,7 +140,9 @@ impl NativeCallableSignature { Self { param_count, param_names: Vec::new(), + param_types: Vec::new(), param_defaults: Vec::new(), + return_type: None, } } @@ -159,6 +163,18 @@ impl NativeCallableSignature { true } + /// Records the PHP declared type metadata for one positional callable slot. + pub fn set_param_type(&mut self, index: usize, param_type: EvalParameterType) -> bool { + if index >= self.param_count { + return false; + } + if self.param_types.len() < self.param_count { + self.param_types.resize(self.param_count, None); + } + self.param_types[index] = Some(param_type); + true + } + /// Records a PHP scalar default value for one positional callable slot. pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { if index >= self.param_count { @@ -171,11 +187,26 @@ impl NativeCallableSignature { true } + /// Records the PHP declared return type metadata for this callable. + pub fn set_return_type(&mut self, return_type: EvalParameterType) { + self.return_type = Some(return_type); + } + /// Returns the PHP-visible parameter names registered for this callable. pub fn param_names(&self) -> &[String] { &self.param_names } + /// Returns PHP declared parameter types registered for this callable. + pub fn param_types(&self) -> &[Option] { + &self.param_types + } + + /// Returns the registered declared type for one parameter slot, if any. + pub fn param_type(&self, index: usize) -> Option<&EvalParameterType> { + self.param_types.get(index).and_then(Option::as_ref) + } + /// Returns the PHP-visible scalar parameter defaults registered for this callable. pub fn param_defaults(&self) -> &[Option] { &self.param_defaults @@ -192,6 +223,11 @@ impl NativeCallableSignature { .rfind(|index| self.param_default(*index).is_none()) .map_or(0, |index| index + 1) } + + /// Returns the registered declared return type metadata, if any. + pub fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } } /// PHP class-like declaration kind targeted by a dynamic `class_alias()`. @@ -1537,6 +1573,19 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_name(index, param_name)) } + /// Records one parameter type for registered native AOT instance-method metadata. + pub fn define_native_method_param_type( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + /// Records one parameter default for registered native AOT instance-method metadata. pub fn define_native_method_param_default( &mut self, @@ -1550,6 +1599,21 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_default(index, default)) } + /// Records one return type for registered native AOT instance-method metadata. + pub fn define_native_method_return_type( + &mut self, + class_name: &str, + method_name: &str, + return_type: EvalParameterType, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_return_type(return_type); + true + }) + } + /// Records one parameter name for registered native AOT static-method metadata. pub fn define_native_static_method_param( &mut self, @@ -1563,6 +1627,19 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_name(index, param_name)) } + /// Records one parameter type for registered native AOT static-method metadata. + pub fn define_native_static_method_param_type( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + /// Records one parameter default for registered native AOT static-method metadata. pub fn define_native_static_method_param_default( &mut self, @@ -1576,6 +1653,21 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_default(index, default)) } + /// Records one return type for registered native AOT static-method metadata. + pub fn define_native_static_method_return_type( + &mut self, + class_name: &str, + method_name: &str, + return_type: EvalParameterType, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_return_type(return_type); + true + }) + } + /// Records one parameter name for registered native AOT constructor metadata. pub fn define_native_constructor_param( &mut self, @@ -1588,6 +1680,18 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_name(index, param_name)) } + /// Records one parameter type for registered native AOT constructor metadata. + pub fn define_native_constructor_param_type( + &mut self, + class_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + /// Records one parameter default for registered native AOT constructor metadata. pub fn define_native_constructor_param_default( &mut self, diff --git a/crates/elephc-eval/src/ffi/native_methods.rs b/crates/elephc-eval/src/ffi/native_methods.rs index bdb6e0862e..0174803c32 100644 --- a/crates/elephc-eval/src/ffi/native_methods.rs +++ b/crates/elephc-eval/src/ffi/native_methods.rs @@ -13,12 +13,19 @@ use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ABI_VERSION}; use crate::context::{NativeCallableDefault, NativeCallableSignature}; +use crate::eval_ir::{EvalParameterType, EvalParameterTypeVariant}; const NATIVE_DEFAULT_NULL: u64 = 0; const NATIVE_DEFAULT_BOOL: u64 = 1; const NATIVE_DEFAULT_INT: u64 = 2; const NATIVE_DEFAULT_FLOAT: u64 = 3; +#[derive(Clone, Copy)] +enum NativeCallableTypePosition { + Parameter, + Return, +} + /// Registers a generated native PHP method signature in an eval context. /// /// # Safety @@ -111,6 +118,114 @@ pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param( .unwrap_or(0) } +/// Registers one generated native PHP method parameter declared type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_type_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method parameter declared type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_type_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method declared return type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_return_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_return_type_inner( + ctx, + method_key_ptr, + method_key_len, + false, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method declared return type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_return_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_return_type_inner( + ctx, + method_key_ptr, + method_key_len, + true, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + /// Registers one generated native PHP method scalar parameter default in an eval context. /// /// # Safety @@ -268,6 +383,33 @@ pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param( .unwrap_or(0) } +/// Registers one generated native PHP constructor parameter declared type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and type-spec pointers must +/// be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_type( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_type_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + /// Registers one generated native PHP constructor scalar parameter default in an eval context. /// /// # Safety @@ -431,6 +573,102 @@ unsafe fn register_native_method_param_inner( } } +/// Runs native method parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_type`; invalid handles, +/// names, indexes, or type specs fail closed as `false`. +unsafe fn register_native_method_param_type_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param_type( + class_name, + method_name, + param_index, + param_type, + )) + } else { + i32::from(context.define_native_method_param_type( + class_name, + method_name, + param_index, + param_type, + )) + } +} + +/// Runs native method return-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_return_type`; invalid handles, +/// names, or type specs fail closed as `false`. +unsafe fn register_native_method_return_type_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Some(return_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Return, + ) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_return_type( + class_name, + method_name, + return_type, + )) + } else { + i32::from(context.define_native_method_return_type(class_name, method_name, return_type)) + } +} + /// Runs native method scalar-default registration after installing a panic boundary. /// /// # Safety @@ -590,6 +828,41 @@ unsafe fn register_native_constructor_param_inner( i32::from(context.define_native_constructor_param(&class_name, param_index, param_name)) } +/// Runs native constructor parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_type`; invalid +/// handles, names, indexes, or type specs fail closed as `false`. +unsafe fn register_native_constructor_param_type_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_constructor_param_type(&class_name, param_index, param_type)) +} + /// Runs native constructor scalar-default registration after installing a panic boundary. /// /// # Safety @@ -710,6 +983,92 @@ fn native_callable_scalar_default( } } +/// Decodes one generated type-spec string into eval Reflection type metadata. +fn native_callable_type_from_abi( + type_spec_ptr: *const u8, + type_spec_len: u64, + position: NativeCallableTypePosition, +) -> Option { + let type_spec = abi_name_to_string(type_spec_ptr, type_spec_len).ok()?; + native_callable_type_from_spec(&type_spec, position) +} + +/// Parses the compact generated type syntax used by native signature registration. +fn native_callable_type_from_spec( + type_spec: &str, + position: NativeCallableTypePosition, +) -> Option { + let type_spec = type_spec.trim(); + if type_spec.is_empty() { + return None; + } + let nullable_shorthand = type_spec.strip_prefix('?'); + let (type_spec, mut allows_null) = match nullable_shorthand { + Some(inner) => (inner, true), + None => (type_spec, false), + }; + if type_spec.contains('&') { + if allows_null || type_spec.contains('|') { + return None; + } + let variants = type_spec + .split('&') + .map(|member| native_callable_type_variant(member, position)) + .collect::>>()?; + if variants.iter().any(Option::is_none) { + return None; + } + return Some(EvalParameterType::intersection( + variants.into_iter().flatten().collect(), + )); + } + let mut variants = Vec::new(); + for member in type_spec.split('|') { + match native_callable_type_variant(member, position)? { + Some(variant) => variants.push(variant), + None => allows_null = true, + } + } + if variants.is_empty() { + return None; + } + Some(EvalParameterType::new(variants, allows_null)) +} + +/// Converts one generated type member name into eval type metadata. +fn native_callable_type_variant( + member: &str, + position: NativeCallableTypePosition, +) -> Option> { + let member = member.trim(); + if member.is_empty() { + return None; + } + let lower = member.trim_start_matches('\\').to_ascii_lowercase(); + let variant = match lower.as_str() { + "array" => EvalParameterTypeVariant::Array, + "bool" => EvalParameterTypeVariant::Bool, + "callable" => EvalParameterTypeVariant::Callable, + "float" => EvalParameterTypeVariant::Float, + "int" => EvalParameterTypeVariant::Int, + "iterable" => EvalParameterTypeVariant::Iterable, + "mixed" => EvalParameterTypeVariant::Mixed, + "never" if matches!(position, NativeCallableTypePosition::Return) => { + EvalParameterTypeVariant::Never + } + "null" => return Some(None), + "object" => EvalParameterTypeVariant::Object, + "string" => EvalParameterTypeVariant::String, + "void" if matches!(position, NativeCallableTypePosition::Return) => { + EvalParameterTypeVariant::Void + } + "void" | "never" => return None, + "self" | "parent" | "static" => EvalParameterTypeVariant::Class(lower), + _ => EvalParameterTypeVariant::Class(member.trim_start_matches('\\').to_string()), + }; + Some(Some(variant)) +} + /// Splits one generated `ClassName::methodName` metadata key into class and method pieces. fn split_method_key(method_key: &str) -> Option<(&str, &str)> { let (class_name, method_name) = method_key.rsplit_once("::")?; diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs index c7859864e3..a1fb21e94e 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -22,6 +22,7 @@ use crate::abi::{ }; use crate::context::NativeCallableDefault; use crate::errors::EvalStatus; +use crate::eval_ir::EvalParameterTypeVariant; use crate::value::{RuntimeCell, RuntimeCellHandle}; use std::ffi::c_void; @@ -270,6 +271,10 @@ fn register_native_methods_record_signature_metadata() { let left = b"left"; let right = b"right"; let value = b"value"; + let method_type = b"int|string|null"; + let static_type = b"?string"; + let constructor_type = b"KnownDep"; + let return_type = b"bool"; let method_registered = unsafe { __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 2) @@ -284,6 +289,16 @@ fn register_native_methods_record_signature_metadata() { right.len() as u64, ) }; + let method_param_type_registered = unsafe { + __elephc_eval_register_native_method_param_type( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 0, + method_type.as_ptr(), + method_type.len() as u64, + ) + }; let static_registered = unsafe { __elephc_eval_register_native_static_method( &mut ctx, @@ -302,6 +317,25 @@ fn register_native_methods_record_signature_metadata() { left.len() as u64, ) }; + let static_param_type_registered = unsafe { + __elephc_eval_register_native_static_method_param_type( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + static_type.as_ptr(), + static_type.len() as u64, + ) + }; + let static_return_type_registered = unsafe { + __elephc_eval_register_native_static_method_return_type( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + return_type.as_ptr(), + return_type.len() as u64, + ) + }; let constructor_registered = unsafe { __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) }; @@ -315,6 +349,16 @@ fn register_native_methods_record_signature_metadata() { value.len() as u64, ) }; + let constructor_param_type_registered = unsafe { + __elephc_eval_register_native_constructor_param_type( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + constructor_type.as_ptr(), + constructor_type.len() as u64, + ) + }; let method_default_registered = unsafe { __elephc_eval_register_native_method_param_default_string( &mut ctx, @@ -348,10 +392,14 @@ fn register_native_methods_record_signature_metadata() { assert_eq!(method_registered, 1); assert_eq!(method_param_registered, 1); + assert_eq!(method_param_type_registered, 1); assert_eq!(static_registered, 1); assert_eq!(static_param_registered, 1); + assert_eq!(static_param_type_registered, 1); + assert_eq!(static_return_type_registered, 1); assert_eq!(constructor_registered, 1); assert_eq!(constructor_param_registered, 1); + assert_eq!(constructor_param_type_registered, 1); assert_eq!(method_default_registered, 1); assert_eq!(static_default_registered, 1); assert_eq!(constructor_default_registered, 1); @@ -361,18 +409,57 @@ fn register_native_methods_record_signature_metadata() { .param_names(), &["".to_string(), "right".to_string()] ); + let method_signature = ctx + .native_method_signature("knownclass", "JOIN") + .expect("method metadata"); + let method_type = method_signature + .param_type(0) + .expect("method parameter type"); + assert!(method_type.allows_null()); + assert_eq!( + method_type.variants(), + &[ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ] + ); assert_eq!( ctx.native_static_method_signature("KnownClass", "SUM") .expect("static method metadata") .param_names(), &["left".to_string(), "".to_string()] ); + let static_signature = ctx + .native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata"); + let static_type = static_signature + .param_type(0) + .expect("static method parameter type"); + assert!(static_type.allows_null()); + assert_eq!(static_type.variants(), &[EvalParameterTypeVariant::String]); + assert_eq!( + static_signature + .return_type() + .expect("static return type") + .variants(), + &[EvalParameterTypeVariant::Bool] + ); assert_eq!( ctx.native_constructor_signature("knownclass") .expect("constructor metadata") .param_names(), &["value".to_string()] ); + let constructor_signature = ctx + .native_constructor_signature("knownclass") + .expect("constructor metadata"); + assert_eq!( + constructor_signature + .param_type(0) + .expect("constructor parameter type") + .variants(), + &[EvalParameterTypeVariant::Class("KnownDep".to_string())] + ); assert_eq!( ctx.native_method_signature("knownclass", "JOIN") .expect("method metadata") diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 524b173489..c1f2f1e6c9 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -689,7 +689,7 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let Some(target) = eval_reflection_function_method_target(identity, context) else { + let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { return Ok(None); }; let method_key = method_name.to_ascii_lowercase(); @@ -1612,6 +1612,9 @@ fn eval_reflection_aot_method_metadata( let parameters = signature.map_or_else(Vec::new, |signature| { eval_reflection_native_callable_parameters(class_name, method_name, flags, signature) }); + let return_type_metadata = signature + .and_then(NativeCallableSignature::return_type) + .and_then(eval_reflection_parameter_type_metadata); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), attributes: Vec::new(), @@ -1623,7 +1626,7 @@ fn eval_reflection_aot_method_metadata( is_promoted: false, modifiers: eval_reflection_method_modifiers_from_flags(flags), type_metadata: None, - return_type_metadata: None, + return_type_metadata, default_value: None, required_parameter_count, parameters, @@ -1656,8 +1659,11 @@ fn eval_reflection_native_callable_parameters( ) -> Vec { let names = eval_reflection_native_callable_parameter_names(signature); let parameter_count = names.len(); - let has_type_flags = vec![false; parameter_count]; - let parameter_types = vec![None; parameter_count]; + let parameter_types = eval_reflection_native_callable_parameter_types(signature); + let has_type_flags = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); let parameter_attributes = vec![Vec::new(); parameter_count]; let defaults = eval_reflection_native_callable_parameter_defaults(signature); let by_ref_flags = vec![false; parameter_count]; @@ -1683,6 +1689,15 @@ fn eval_reflection_native_callable_parameters( ) } +/// Returns declared parameter type metadata for a registered native callable. +fn eval_reflection_native_callable_parameter_types( + signature: &NativeCallableSignature, +) -> Vec> { + (0..signature.param_count()) + .map(|index| signature.param_type(index).cloned()) + .collect() +} + /// Returns parameter names for a registered native callable, filling missing bridge names. fn eval_reflection_native_callable_parameter_names( signature: &NativeCallableSignature, @@ -2157,7 +2172,8 @@ fn eval_reflection_full_class_object_result( runtime_class_name, values, )?; - let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; + let interface_names = + eval_reflection_aot_class_interface_names(runtime_class_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, @@ -4385,7 +4401,8 @@ fn eval_reflection_builtin_named_type( fn eval_reflection_function_method_target( identity: u64, context: &ElephcEvalContext, -) -> Option { + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { if let Some(name) = context.eval_reflection_function_name(identity) { let function = context.function(&name.to_ascii_lowercase()); let is_variadic = function @@ -4393,31 +4410,39 @@ fn eval_reflection_function_method_target( let return_type_metadata = function .and_then(EvalFunction::return_type) .and_then(eval_reflection_parameter_type_metadata); - return Some(EvalReflectionFunctionMethodTarget::Function { + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { name: name.to_string(), is_variadic, return_type_metadata, - }); + })); } - context - .eval_reflection_method(identity) - .map(|(declaring_class, method_name)| { - let method_metadata = - eval_reflection_method_metadata(declaring_class, method_name, context); - let is_variadic = method_metadata.as_ref().is_some_and(|method| { - method - .parameters - .iter() - .any(|parameter| parameter.is_variadic) - }); - let return_type_metadata = - method_metadata.and_then(|method| method.return_type_metadata); - EvalReflectionFunctionMethodTarget::Method { - name: method_name.to_string(), - is_variadic, - return_type_metadata, - } - }) + let Some((declaring_class, method_name)) = context.eval_reflection_method(identity) else { + return Ok(None); + }; + let method_metadata = if let Some(method_metadata) = + eval_reflection_method_metadata(declaring_class, method_name, context) + { + Some(method_metadata) + } else { + eval_reflection_aot_method_metadata_with_signature_if_exists( + declaring_class, + method_name, + context, + values, + )? + }; + let is_variadic = method_metadata.as_ref().is_some_and(|method| { + method + .parameters + .iter() + .any(|parameter| parameter.is_variadic) + }); + let return_type_metadata = method_metadata.and_then(|method| method.return_type_metadata); + Ok(Some(EvalReflectionFunctionMethodTarget::Method { + name: method_name.to_string(), + is_variadic, + return_type_metadata, + })) } /// Validates that a synthetic reflection metadata call received no arguments. diff --git a/docs/php/eval.md b/docs/php/eval.md index 8ff6a27aa8..4ee97a1b6b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -263,10 +263,11 @@ flags. For generated/AOT classes, `ReflectionClass::getMethod()` / `getProperty()` and `getMethods()` / `getProperties()` materialize reflection objects from emitted member-name and predicate metadata, including optional modifier filters. AOT method reflection also exposes registered parameter -names, required/optional counts, and registered scalar or null default values -for generated constructor, instance-method, and static-method signatures. AOT -member reflection does not yet expose full AOT type metadata, attributes, -property types, or property default-value metadata. +names, declared parameter types, declared return types, required/optional +counts, and registered scalar or null default values for generated +constructor, instance-method, and static-method signatures. AOT member +reflection does not yet expose AOT attributes, property types, or property +default-value metadata. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected @@ -307,10 +308,10 @@ reflected method is `__construct` or `__destruct`. `ReflectionFunction::getName()`, `ReflectionFunction::getParameters()`, `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report retained eval-declared function and -method metadata, plus registered generated/AOT method parameter names, -required/optional counts, and scalar or null default values when native -method/static-method signatures are registered. Eval-declared functions and -methods expose declared-type presence for parameters and return types, simple +method metadata, plus registered generated/AOT method parameter names, declared +parameter and return types, required/optional counts, and scalar or null default +values when native method/static-method signatures are registered. Eval-declared +functions and methods expose declared-type presence for parameters and return types, simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, `allowsNull()`, and `isBuiltin()`, and multi-member union metadata through @@ -554,7 +555,9 @@ and attribute slice, Reflection type APIs beyond retained parameter, property, and function/method return metadata, broader parameter default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed/object slice. Eval object cloning covers ordinary +non-by-reference fixed scalar/Mixed/object slice. Generated/AOT method type +metadata is exposed for registered public bridge signatures, while broader +non-public or unsupported bridge shapes remain outside that slice. Eval object cloning covers ordinary emitted/AOT storage and public AOT `__clone()` hooks, but non-public AOT `__clone()` scope checks and broader bridge signatures remain outside that bridge slice. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 89fa9dd880..a599323c66 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -20,7 +20,7 @@ use crate::codegen::{ }; use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op}; use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; -use crate::parser::ast::{Expr, ExprKind, Visibility}; +use crate::parser::ast::{Expr, ExprKind, TypeExpr, Visibility}; use crate::types::{ClassInfo, FunctionSig, PhpType}; use super::super::super::context::FunctionContext; @@ -610,6 +610,105 @@ fn method_signature_can_register_with_eval(signature: &FunctionSig) -> bool { && signature.ref_params.iter().all(|is_ref| !*is_ref) } +/// Returns generated type specs for declared native callable parameters. +fn eval_native_callable_param_type_specs(signature: &FunctionSig) -> Vec> { + signature + .params + .iter() + .enumerate() + .map(|(index, (_, php_type))| { + if !signature + .declared_params + .get(index) + .copied() + .unwrap_or(false) + { + return None; + } + signature + .param_type_exprs + .get(index) + .and_then(Option::as_ref) + .and_then(eval_native_type_expr_spec) + .or_else(|| eval_native_php_type_spec(php_type, false)) + }) + .collect() +} + +/// Returns a generated type spec for a declared native callable return type. +fn eval_native_callable_return_type_spec(signature: &FunctionSig) -> Option { + signature + .declared_return + .then(|| eval_native_php_type_spec(&signature.return_type, true)) + .flatten() +} + +/// Formats one parsed PHP type expression for eval native metadata registration. +fn eval_native_type_expr_spec(type_expr: &TypeExpr) -> Option { + match type_expr { + TypeExpr::Int => Some("int".to_string()), + TypeExpr::Float => Some("float".to_string()), + TypeExpr::Bool => Some("bool".to_string()), + TypeExpr::Str => Some("string".to_string()), + TypeExpr::Void => Some("null".to_string()), + TypeExpr::Never => None, + TypeExpr::Iterable => Some("iterable".to_string()), + TypeExpr::Array(_) => Some("array".to_string()), + TypeExpr::Ptr(_) | TypeExpr::Buffer(_) => None, + TypeExpr::Named(name) => Some(name.as_str().to_string()), + TypeExpr::Nullable(inner) => { + let inner = eval_native_type_expr_spec(inner)?; + Some(format!("?{}", inner)) + } + TypeExpr::Union(members) => eval_native_type_expr_member_specs(members, "|"), + TypeExpr::Intersection(members) => eval_native_type_expr_member_specs(members, "&"), + } +} + +/// Formats a compound parsed type expression with the requested separator. +fn eval_native_type_expr_member_specs(members: &[TypeExpr], separator: &str) -> Option { + members + .iter() + .map(eval_native_type_expr_spec) + .collect::>>() + .map(|members| members.join(separator)) +} + +/// Formats one checked PHP type for eval native metadata registration. +fn eval_native_php_type_spec(php_type: &PhpType, allow_return_atoms: bool) -> Option { + match php_type { + PhpType::Int => Some("int".to_string()), + PhpType::Float => Some("float".to_string()), + PhpType::Str => Some("string".to_string()), + PhpType::Bool => Some("bool".to_string()), + PhpType::Void if allow_return_atoms => Some("void".to_string()), + PhpType::Void => Some("null".to_string()), + PhpType::Never if allow_return_atoms => Some("never".to_string()), + PhpType::Never => None, + PhpType::Iterable => Some("iterable".to_string()), + PhpType::Mixed => Some("mixed".to_string()), + PhpType::Array(_) | PhpType::AssocArray { .. } => Some("array".to_string()), + PhpType::Callable => Some("callable".to_string()), + PhpType::Object(name) if name.is_empty() => Some("object".to_string()), + PhpType::Object(name) => Some(name.clone()), + PhpType::Union(members) => eval_native_php_type_member_specs(members), + PhpType::Buffer(_) + | PhpType::Packed(_) + | PhpType::Pointer(_) + | PhpType::Resource(_) + | PhpType::TaggedScalar => None, + } +} + +/// Formats union members from checked PHP types for eval native metadata registration. +fn eval_native_php_type_member_specs(members: &[PhpType]) -> Option { + members + .iter() + .map(|member| eval_native_php_type_spec(member, false)) + .collect::>>() + .map(|members| members.join("|")) +} + /// Converts a PHP signature default into the compact eval bridge default ABI. fn eval_native_callable_default(expr: &Expr) -> Option { match &expr.kind { @@ -770,6 +869,7 @@ fn register_eval_native_method( .extern_symbol("__elephc_eval_register_native_method") }; abi::emit_call_label(ctx.emitter, &symbol); + let param_type_specs = eval_native_callable_param_type_specs(®istration.signature); for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { register_eval_native_method_param( ctx, @@ -780,6 +880,17 @@ fn register_eval_native_method( index, param_name, ); + if let Some(type_spec) = param_type_specs.get(index).and_then(Option::as_deref) { + register_eval_native_method_param_type( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + index, + type_spec, + ); + } } for (index, default) in registration.signature.defaults.iter().enumerate() { let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { @@ -795,6 +906,16 @@ fn register_eval_native_method( &default, ); } + if let Some(type_spec) = eval_native_callable_return_type_spec(®istration.signature) { + register_eval_native_method_return_type( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + &type_spec, + ); + } } /// Emits one native method parameter-name registration call. @@ -846,6 +967,98 @@ fn register_eval_native_method_param( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native method parameter-type registration call. +fn register_eval_native_method_param_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + param_index: usize, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + type_len as i64, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_param_type") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_type") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native method return-type registration call. +fn register_eval_native_method_return_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_return_type") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_return_type") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native method parameter-default registration call. fn register_eval_native_method_param_default( ctx: &mut FunctionContext<'_>, @@ -949,6 +1162,7 @@ fn register_eval_native_constructor( .target .extern_symbol("__elephc_eval_register_native_constructor"); abi::emit_call_label(ctx.emitter, &symbol); + let param_type_specs = eval_native_callable_param_type_specs(®istration.signature); for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { register_eval_native_constructor_param( ctx, @@ -958,6 +1172,16 @@ fn register_eval_native_constructor( index, param_name, ); + if let Some(type_spec) = param_type_specs.get(index).and_then(Option::as_deref) { + register_eval_native_constructor_param_type( + ctx, + context_offset, + &class_name_label, + class_name_len, + index, + type_spec, + ); + } } for (index, default) in registration.signature.defaults.iter().enumerate() { let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { @@ -1054,6 +1278,49 @@ fn register_eval_native_constructor_param( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native constructor parameter-type registration call. +fn register_eval_native_constructor_param_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + param_index: usize, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + type_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_type"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native constructor parameter-default registration call. fn register_eval_native_constructor_param_default( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 69eea34775..1847a9d7b4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7732,6 +7732,57 @@ echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[2]- ); } +/// Verifies eval ReflectionMethod exposes generated/AOT declared type metadata. +#[test] +fn test_eval_reflection_method_exposes_aot_type_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +$union = $params[0]->getType(); +echo "U" . count($union->getTypes()); +foreach ($union->getTypes() as $type) { + echo ":" . $type->getName() . ($type->isBuiltin() ? "B" : "C"); +} +$dep = $params[1]->getType(); +echo ":D" . ($dep->allowsNull() ? "?" : "!") . ":" . $dep->getName() . ($dep->isBuiltin() ? "B" : "C"); +$return = $method->getReturnType(); +echo ":R" . ($return->allowsNull() ? "?" : "!") . ":" . $return->getName() . ($return->isBuiltin() ? "B" : "C"); +$static = (new ReflectionMethod("EvalAotReflectTypeTarget", "factory"))->getReturnType(); +echo ":S" . ($static->allowsNull() ? "?" : "!") . ":" . $static->getName() . ($static->isBuiltin() ? "B" : "C"); +$intersection = (new ReflectionMethod("EvalAotReflectTypeTarget", "both"))->getParameters()[0]->getType(); +echo ":I" . count($intersection->getTypes()); +foreach ($intersection->getTypes() as $type) { + echo ":" . $type->getName() . ($type->isBuiltin() ? "B" : "C"); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "U2:intB:stringB:D?:EvalAotReflectTypeDepC:R?:stringB:S!:EvalAotReflectTypeDepC:I2:EvalAotReflectTypeLeftC:EvalAotReflectTypeRightC" + ); +} + /// Verifies eval ReflectionClass::getConstructor exposes generated/AOT constructor metadata. #[test] fn test_eval_reflection_class_get_constructor_for_aot_class() { @@ -7757,6 +7808,9 @@ foreach ($ctor->getParameters() as $param) { $default = $param->getDefaultValue(); echo is_null($default) ? "null" : $default; } + $type = $param->getType(); + echo ":"; + echo $type === null ? "none" : $type->getName() . ($type->allowsNull() ? "?" : "!"); echo ";"; } $listed = null; @@ -7778,7 +7832,7 @@ echo ":" . ($plain === null ? "null" : "bad"); ); assert_eq!( out.stdout, - "M:__construct/EvalAotReflectCtorParamTarget:3/1:leftr-;rightO=B;countO=null;:3/left:null" + "M:__construct/EvalAotReflectCtorParamTarget:3/1:leftr-:string!;rightO=B:string!;countO=null:int?;:3/left:null" ); } From a8c40604abf9c1ce3f968cb11c2b76553e0db80c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 22:14:29 +0200 Subject: [PATCH 0492/1208] Expose AOT property types in eval reflection --- crates/elephc-eval/src/context.rs | 38 +++++ crates/elephc-eval/src/ffi/native_methods.rs | 65 ++++++++ crates/elephc-eval/src/ffi/tests.rs | 41 +++++ .../elephc-eval/src/interpreter/reflection.rs | 61 ++++++-- docs/php/eval.md | 11 +- src/codegen/lower_inst/builtins/eval.rs | 145 ++++++++++++++++++ tests/codegen/eval.rs | 55 +++++++ 7 files changed, 396 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 523a346b94..88a1b99a82 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -271,6 +271,7 @@ pub struct ElephcEvalContext { native_static_methods: HashMap<(String, String), NativeCallableSignature>, native_constructors: HashMap, native_class_parents: HashMap, + native_property_types: HashMap<(String, String), EvalParameterType>, static_locals: HashMap<(String, String), RuntimeCellHandle>, static_properties: HashMap<(String, String), RuntimeCellHandle>, class_constants: HashMap<(String, String), RuntimeCellHandle>, @@ -320,6 +321,7 @@ impl ElephcEvalContext { native_static_methods: HashMap::new(), native_constructors: HashMap::new(), native_class_parents: HashMap::new(), + native_property_types: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -370,6 +372,7 @@ impl ElephcEvalContext { native_static_methods: HashMap::new(), native_constructors: HashMap::new(), native_class_parents: HashMap::new(), + native_property_types: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -1755,6 +1758,33 @@ impl ElephcEvalContext { .map(String::as_str) } + /// Defines generated AOT property type metadata for eval reflection. + pub fn define_native_property_type( + &mut self, + class_name: &str, + property_name: &str, + property_type: EvalParameterType, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_types + .insert(key, property_type) + .is_none() + } + + /// Returns generated AOT property type metadata by PHP class and property name. + pub fn native_property_type( + &self, + class_name: &str, + property_name: &str, + ) -> Option { + self.native_property_types + .get(&native_property_key(class_name, property_name)) + .cloned() + } + /// Returns true when the context has a dynamic or native function with this lowercase PHP name. pub fn has_function(&self, name: &str) -> bool { self.functions.contains_key(name) || self.native_functions.contains_key(name) @@ -2038,6 +2068,14 @@ fn native_method_key(class_name: &str, method_name: &str) -> (String, String) { ) } +/// Builds the folded native property metadata key used for eval reflection. +fn native_property_key(class_name: &str, property_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + property_name.trim_start_matches('$').to_string(), + ) +} + /// Pushes a PHP class-like name once, preserving the first visible spelling. fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSet) { let key = normalize_class_name(name); diff --git a/crates/elephc-eval/src/ffi/native_methods.rs b/crates/elephc-eval/src/ffi/native_methods.rs index 0174803c32..fd9c5e1d84 100644 --- a/crates/elephc-eval/src/ffi/native_methods.rs +++ b/crates/elephc-eval/src/ffi/native_methods.rs @@ -489,6 +489,32 @@ pub unsafe extern "C" fn __elephc_eval_register_native_class_parent( .unwrap_or(0) } +/// Registers one generated native PHP property type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key must be a +/// readable `ClassName::propertyName` byte string, and the type spec must be a +/// readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_type( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_type_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + /// Runs native method registration after installing a panic boundary. /// /// # Safety @@ -967,6 +993,40 @@ unsafe fn register_native_class_parent_inner( i32::from(context.define_native_class_parent(&class_name, &parent_name)) } +/// Runs native property-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_type`; invalid handles, +/// names, or type specs fail closed as `false`. +unsafe fn register_native_property_type_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, property_name)) = split_property_key(&property_key) else { + return 0; + }; + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_property_type(class_name, property_name, property_type)) +} + /// Decodes scalar default kind/payload ABI fields into native callable metadata. fn native_callable_scalar_default( default_kind: u64, @@ -1074,3 +1134,8 @@ fn split_method_key(method_key: &str) -> Option<(&str, &str)> { let (class_name, method_name) = method_key.rsplit_once("::")?; (!class_name.is_empty() && !method_name.is_empty()).then_some((class_name, method_name)) } + +/// Splits one generated `ClassName::propertyName` metadata key into class and property pieces. +fn split_property_key(property_key: &str) -> Option<(&str, &str)> { + split_method_key(property_key) +} diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs index a1fb21e94e..e93356db14 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -501,6 +501,47 @@ fn register_native_class_parent_records_metadata() { assert_eq!(ctx.native_class_parent("knownchild"), Some("KnownParent")); } +/// Verifies native AOT property type metadata is available to eval reflection. +#[test] +fn register_native_property_type_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownClass::name"; + let property_type = b"?KnownDep"; + let invalid_property = b"KnownClass::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_property_type( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_property_type( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + ) + }; + + assert_eq!(registered, 1); + let property_type = ctx + .native_property_type("knownclass", "name") + .expect("property type metadata"); + assert!(property_type.allows_null()); + assert_eq!( + property_type.variants(), + &[EvalParameterTypeVariant::Class("KnownDep".to_string())] + ); + assert_eq!(invalid_registered, 0); + assert!(ctx.native_property_type("KnownClass", "bad").is_none()); +} + /// Verifies scope allocation returns an empty opaque activation scope handle. #[test] fn scope_new_returns_empty_handle() { diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index c1f2f1e6c9..d874741532 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -355,17 +355,21 @@ pub(in crate::interpreter) fn eval_reflection_class_has_property_result( }; let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; let property_name = eval_reflection_string_arg(args[0], values)?; - let exists = if let Some(metadata) = - eval_reflection_class_like_attributes(&reflected_name, context) - { - metadata - .property_names - .iter() - .any(|name| name == &property_name) - } else { - eval_reflection_aot_property_metadata_if_exists(&reflected_name, &property_name, values)? + let exists = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + metadata + .property_names + .iter() + .any(|name| name == &property_name) + } else { + eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + &property_name, + context, + values, + )? .is_some() - }; + }; values.bool_value(exists).map(Some) } @@ -1090,6 +1094,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( if let Some(member) = eval_reflection_aot_property_metadata_if_exists( &reflected_name, &requested_name, + context, values, )? { return eval_reflection_member_object_result( @@ -1505,9 +1510,12 @@ fn eval_reflection_property_new( let class_name = eval_reflection_string_arg(args[0], values)?; if !eval_reflection_class_like_exists(&class_name, context) { let property_name = eval_reflection_string_arg(args[1], values)?; - if let Some(property) = - eval_reflection_aot_property_metadata_if_exists(&class_name, &property_name, values)? - { + if let Some(property) = eval_reflection_aot_property_metadata_if_exists( + &class_name, + &property_name, + context, + values, + )? { return eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_PROPERTY, &property_name, @@ -1742,6 +1750,7 @@ fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) fn eval_reflection_aot_property_metadata_if_exists( class_name: &str, property_name: &str, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let runtime_class_name = class_name.trim_start_matches('\\'); @@ -1751,16 +1760,38 @@ fn eval_reflection_aot_property_metadata_if_exists( let declaring_class_name = values .reflection_property_declaring_class(runtime_class_name, property_name)? .unwrap_or_else(|| runtime_class_name.to_string()); + let type_metadata = eval_reflection_aot_property_type_metadata( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); Ok(Some(eval_reflection_aot_property_metadata( &declaring_class_name, flags, + type_metadata, ))) } +/// Returns registered generated/AOT property type metadata for one reflected property. +fn eval_reflection_aot_property_type_metadata( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + context + .native_property_type(declaring_class_name, property_name) + .or_else(|| context.native_property_type(runtime_class_name, property_name)) + .as_ref() + .and_then(eval_reflection_parameter_type_metadata) +} + /// Converts AOT property flag metadata into the eval ReflectionProperty shape. fn eval_reflection_aot_property_metadata( class_name: &str, flags: u64, + type_metadata: Option, ) -> EvalReflectionMemberMetadata { let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { EvalVisibility::Private @@ -1790,7 +1821,7 @@ fn eval_reflection_aot_property_metadata( is_readonly, false, ), - type_metadata: None, + type_metadata, return_type_metadata: None, default_value: None, required_parameter_count: 0, @@ -2745,7 +2776,7 @@ fn eval_reflection_aot_member_object_array_result( class_name, name, context, values, )? } else { - eval_reflection_aot_property_metadata_if_exists(class_name, name, values)? + eval_reflection_aot_property_metadata_if_exists(class_name, name, context, values)? }; let Some(member) = member else { continue; diff --git a/docs/php/eval.md b/docs/php/eval.md index 4ee97a1b6b..b17249fab5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -265,9 +265,10 @@ objects from emitted member-name and predicate metadata, including optional modifier filters. AOT method reflection also exposes registered parameter names, declared parameter types, declared return types, required/optional counts, and registered scalar or null default values for generated -constructor, instance-method, and static-method signatures. AOT member -reflection does not yet expose AOT attributes, property types, or property -default-value metadata. +constructor, instance-method, and static-method signatures. AOT property +reflection exposes registered declared property types for supported generated +property metadata. AOT member reflection does not yet expose AOT attributes or +property default-value metadata. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected @@ -551,8 +552,8 @@ bridge paths. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType -and attribute slice, Reflection type APIs beyond retained parameter, property, -and function/method return metadata, broader +and attribute slice, Reflection type APIs beyond retained parameter, generated +property, and function/method return metadata, broader parameter default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice. Generated/AOT method type diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index a599323c66..529259ebbb 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -98,6 +98,13 @@ struct EvalNativeConstructorRegistration { signature: FunctionSig, } +/// A module-local property type that can be registered with the eval context. +struct EvalNativePropertyTypeRegistration { + class_name: String, + property_name: String, + type_spec: String, +} + /// Scalar native callable default that can be registered with libelephc-eval. enum EvalNativeCallableDefault { Scalar { kind: i64, payload: i64 }, @@ -468,6 +475,9 @@ fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context for registration in eval_native_constructor_registrations(ctx) { register_eval_native_constructor(ctx, context_offset, ®istration); } + for registration in eval_native_property_type_registrations(ctx) { + register_eval_native_property_type(ctx, context_offset, ®istration); + } register_eval_native_class_parents(ctx, context_offset); } @@ -525,6 +535,20 @@ fn eval_native_constructor_registrations( registrations } +/// Collects AOT property types whose declared PHP type can be exposed to eval reflection. +fn eval_native_property_type_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_eval_native_instance_property_types(class_name, class_info, &mut registrations); + collect_eval_native_static_property_types(class_name, class_info, &mut registrations); + } + registrations +} + /// Registers generated AOT class parent metadata for eval `parent::` resolution. fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_offset: usize) { let mut parents = ctx @@ -546,6 +570,87 @@ fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_off } } +/// Adds declared instance-property type metadata for one class to eval registration. +fn collect_eval_native_instance_property_types( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + for (slot, (property_name, php_type)) in class_info.properties.iter().enumerate() { + if !class_info.property_slot_is_declared(slot, property_name) { + continue; + } + let Some(type_spec) = eval_native_php_type_spec(php_type, false) else { + continue; + }; + registrations.push(EvalNativePropertyTypeRegistration { + class_name: eval_native_instance_property_declaring_class( + class_name, + class_info, + property_name, + ) + .to_string(), + property_name: property_name.clone(), + type_spec, + }); + } +} + +/// Adds declared static-property type metadata for one class to eval registration. +fn collect_eval_native_static_property_types( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + for (property_name, php_type) in &class_info.static_properties { + if !class_info + .declared_static_properties + .contains(property_name) + { + continue; + } + let Some(type_spec) = eval_native_php_type_spec(php_type, false) else { + continue; + }; + registrations.push(EvalNativePropertyTypeRegistration { + class_name: eval_native_static_property_declaring_class( + class_name, + class_info, + property_name, + ) + .to_string(), + property_name: property_name.clone(), + type_spec, + }); + } +} + +/// Returns the class name that declares one AOT instance property row. +fn eval_native_instance_property_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .property_declaring_classes + .get(property_name) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one AOT static property row. +fn eval_native_static_property_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .static_property_declaring_classes + .get(property_name) + .map(String::as_str) + .unwrap_or(reflected_class) +} + /// Adds eligible public instance methods for one class to eval signature registration. fn collect_eval_native_instance_methods( class_name: &str, @@ -1235,6 +1340,46 @@ fn register_eval_native_class_parent( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native property-type metadata registration call into the eval context. +fn register_eval_native_property_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativePropertyTypeRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let property_key = format!( + "{}::{}", + registration.class_name, registration.property_name + ); + let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &property_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + property_key_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_property_type"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native constructor parameter-name registration call. fn register_eval_native_constructor_param( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1847a9d7b4..9c5a36774c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7496,6 +7496,61 @@ echo $listed->getDeclaringClass()->getName(); ); } +/// Verifies eval exposes declared generated/AOT property types through +/// `ReflectionProperty::hasType()` and `getType()`. +#[test] +fn test_eval_reflection_property_exposes_aot_type_metadata() { + let out = compile_and_run_capture( + r#"hasType() ? "H:" : "h:"; +$type = $id->getType(); +$parts = $type->getTypes(); +echo $parts[0]->getName() . ($parts[0]->isBuiltin() ? "B" : "C"); +echo "," . $parts[1]->getName() . ($parts[1]->isBuiltin() ? "B" : "C"); +echo $type->allowsNull() ? ":N" : ":n"; +$dep = new ReflectionProperty("EvalAotReflectPropertyTypeTarget", "dep"); +$depType = $dep->getType(); +echo ":" . ($dep->hasType() ? "D" : "d"); +echo $depType->allowsNull() ? "?" : "!"; +echo $depType->getName() . ($depType->isBuiltin() ? "B" : "C"); +$static = new ReflectionProperty("EvalAotReflectPropertyTypeTarget", "count"); +$staticType = $static->getType(); +echo ":" . ($static->hasType() ? "S" : "s"); +echo $staticType->allowsNull() ? "?" : "!"; +echo $staticType->getName() . ($staticType->isBuiltin() ? "B" : "C"); +$base = new ReflectionProperty("EvalAotReflectPropertyTypeTarget", "baseName"); +$baseType = $base->getType(); +echo ":" . ($base->hasType() ? "B" : "b"); +echo $baseType->allowsNull() ? "?" : "!"; +echo $baseType->getName() . ($baseType->isBuiltin() ? "B" : "C"); +$untyped = new ReflectionProperty("EvalAotReflectPropertyTypeTarget", "untyped"); +echo ":" . ($untyped->hasType() ? "U" : "u"); +echo $untyped->getType() === null ? "N" : "n"; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "H:intB,stringB:n:D?EvalAotReflectPropertyTypeDepC:S?intB:B?stringB:uN" + ); +} + /// Verifies eval can probe generated/AOT method predicate metadata through /// `ReflectionClass::hasMethod()`, `getMethod()`, and direct `ReflectionMethod`. #[test] From e5605c3fbb16f753caa223aa32215aadfe421a31 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 22:21:34 +0200 Subject: [PATCH 0493/1208] Expose AOT property defaults in eval reflection --- crates/elephc-eval/src/context.rs | 28 ++++ crates/elephc-eval/src/ffi/native_methods.rs | 117 ++++++++++++++ crates/elephc-eval/src/ffi/tests.rs | 53 ++++++ .../elephc-eval/src/interpreter/reflection.rs | 24 ++- docs/php/eval.md | 8 +- src/codegen/lower_inst/builtins/eval.rs | 153 ++++++++++++++++++ tests/codegen/eval.rs | 49 ++++++ 7 files changed, 427 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 88a1b99a82..4857f9256a 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -272,6 +272,7 @@ pub struct ElephcEvalContext { native_constructors: HashMap, native_class_parents: HashMap, native_property_types: HashMap<(String, String), EvalParameterType>, + native_property_defaults: HashMap<(String, String), NativeCallableDefault>, static_locals: HashMap<(String, String), RuntimeCellHandle>, static_properties: HashMap<(String, String), RuntimeCellHandle>, class_constants: HashMap<(String, String), RuntimeCellHandle>, @@ -322,6 +323,7 @@ impl ElephcEvalContext { native_constructors: HashMap::new(), native_class_parents: HashMap::new(), native_property_types: HashMap::new(), + native_property_defaults: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -373,6 +375,7 @@ impl ElephcEvalContext { native_constructors: HashMap::new(), native_class_parents: HashMap::new(), native_property_types: HashMap::new(), + native_property_defaults: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -1785,6 +1788,31 @@ impl ElephcEvalContext { .cloned() } + /// Defines generated AOT property default metadata for eval reflection. + pub fn define_native_property_default( + &mut self, + class_name: &str, + property_name: &str, + default: NativeCallableDefault, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_defaults.insert(key, default).is_none() + } + + /// Returns generated AOT property default metadata by PHP class and property name. + pub fn native_property_default( + &self, + class_name: &str, + property_name: &str, + ) -> Option { + self.native_property_defaults + .get(&native_property_key(class_name, property_name)) + .cloned() + } + /// Returns true when the context has a dynamic or native function with this lowercase PHP name. pub fn has_function(&self, name: &str) -> bool { self.functions.contains_key(name) || self.native_functions.contains_key(name) diff --git a/crates/elephc-eval/src/ffi/native_methods.rs b/crates/elephc-eval/src/ffi/native_methods.rs index fd9c5e1d84..082467def9 100644 --- a/crates/elephc-eval/src/ffi/native_methods.rs +++ b/crates/elephc-eval/src/ffi/native_methods.rs @@ -515,6 +515,56 @@ pub unsafe extern "C" fn __elephc_eval_register_native_property_type( .unwrap_or(0) } +/// Registers one generated native PHP property scalar default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key must be a +/// readable `ClassName::propertyName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_scalar( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_scalar_inner( + ctx, + property_key_ptr, + property_key_len, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property string default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key and default +/// string pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_string( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_string_inner( + ctx, + property_key_ptr, + property_key_len, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + /// Runs native method registration after installing a panic boundary. /// /// # Safety @@ -1027,6 +1077,73 @@ unsafe fn register_native_property_type_inner( i32::from(context.define_native_property_type(class_name, property_name, property_type)) } +/// Runs native property scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_scalar`; invalid +/// handles, names, or default kinds fail closed as `false`. +unsafe fn register_native_property_default_scalar_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_property_default_inner(ctx, property_key_ptr, property_key_len, default) +} + +/// Runs native property string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_string`; invalid +/// handles, names, or string buffers fail closed as `false`. +unsafe fn register_native_property_default_string_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_property_default_inner( + ctx, + property_key_ptr, + property_key_len, + NativeCallableDefault::String(default), + ) +} + +/// Records a native property default in the property metadata table. +/// +/// # Safety +/// `ctx` and `property_key_ptr` must be valid for their declared use; callers +/// are the exported ABI wrappers above. +unsafe fn register_native_property_default_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, property_name)) = split_property_key(&property_key) else { + return 0; + }; + i32::from(context.define_native_property_default(class_name, property_name, default)) +} + /// Decodes scalar default kind/payload ABI fields into native callable metadata. fn native_callable_scalar_default( default_kind: u64, diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs index e93356db14..496a1d7e58 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -542,6 +542,59 @@ fn register_native_property_type_records_metadata() { assert!(ctx.native_property_type("KnownClass", "bad").is_none()); } +/// Verifies native AOT property default metadata is available to eval reflection. +#[test] +fn register_native_property_default_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let count = b"KnownClass::count"; + let label = b"KnownClass::label"; + let invalid = b"KnownClass::invalid"; + let label_value = b"ok"; + + let scalar_registered = unsafe { + __elephc_eval_register_native_property_default_scalar( + &mut ctx, + count.as_ptr(), + count.len() as u64, + 2, + 42, + ) + }; + let string_registered = unsafe { + __elephc_eval_register_native_property_default_string( + &mut ctx, + label.as_ptr(), + label.len() as u64, + label_value.as_ptr(), + label_value.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_property_default_scalar( + &mut ctx, + invalid.as_ptr(), + invalid.len() as u64, + 99, + 0, + ) + }; + + assert_eq!(scalar_registered, 1); + assert_eq!( + ctx.native_property_default("knownclass", "count"), + Some(NativeCallableDefault::Int(42)) + ); + assert_eq!(string_registered, 1); + assert_eq!( + ctx.native_property_default("KnownClass", "label"), + Some(NativeCallableDefault::String("ok".to_string())) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_property_default("KnownClass", "invalid") + .is_none()); +} + /// Verifies scope allocation returns an empty opaque activation scope handle. #[test] fn scope_new_returns_empty_handle() { diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index d874741532..7b4a9847f3 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1766,10 +1766,17 @@ fn eval_reflection_aot_property_metadata_if_exists( property_name, context, ); + let default_value = eval_reflection_aot_property_default_value( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); Ok(Some(eval_reflection_aot_property_metadata( &declaring_class_name, flags, type_metadata, + default_value, ))) } @@ -1787,11 +1794,26 @@ fn eval_reflection_aot_property_type_metadata( .and_then(eval_reflection_parameter_type_metadata) } +/// Returns registered generated/AOT property default metadata for one reflected property. +fn eval_reflection_aot_property_default_value( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + context + .native_property_default(declaring_class_name, property_name) + .or_else(|| context.native_property_default(runtime_class_name, property_name)) + .as_ref() + .map(eval_reflection_native_callable_default_expr) +} + /// Converts AOT property flag metadata into the eval ReflectionProperty shape. fn eval_reflection_aot_property_metadata( class_name: &str, flags: u64, type_metadata: Option, + default_value: Option, ) -> EvalReflectionMemberMetadata { let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { EvalVisibility::Private @@ -1823,7 +1845,7 @@ fn eval_reflection_aot_property_metadata( ), type_metadata, return_type_metadata: None, - default_value: None, + default_value, required_parameter_count: 0, parameters: Vec::new(), } diff --git a/docs/php/eval.md b/docs/php/eval.md index b17249fab5..9a11a9e8c5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -266,9 +266,9 @@ modifier filters. AOT method reflection also exposes registered parameter names, declared parameter types, declared return types, required/optional counts, and registered scalar or null default values for generated constructor, instance-method, and static-method signatures. AOT property -reflection exposes registered declared property types for supported generated -property metadata. AOT member reflection does not yet expose AOT attributes or -property default-value metadata. +reflection exposes registered declared property types and supported scalar, +string, or null default values for generated property metadata. AOT member +reflection does not yet expose AOT attributes. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected @@ -554,7 +554,7 @@ remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader -parameter default-value objects beyond the eval-supported +parameter and generated property default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice. Generated/AOT method type metadata is exposed for registered public bridge signatures, while broader diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 529259ebbb..693a18be46 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -105,6 +105,13 @@ struct EvalNativePropertyTypeRegistration { type_spec: String, } +/// A module-local property default that can be registered with the eval context. +struct EvalNativePropertyDefaultRegistration { + class_name: String, + property_name: String, + default: EvalNativeCallableDefault, +} + /// Scalar native callable default that can be registered with libelephc-eval. enum EvalNativeCallableDefault { Scalar { kind: i64, payload: i64 }, @@ -478,6 +485,9 @@ fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context for registration in eval_native_property_type_registrations(ctx) { register_eval_native_property_type(ctx, context_offset, ®istration); } + for registration in eval_native_property_default_registrations(ctx) { + register_eval_native_property_default(ctx, context_offset, ®istration); + } register_eval_native_class_parents(ctx, context_offset); } @@ -549,6 +559,20 @@ fn eval_native_property_type_registrations( registrations } +/// Collects AOT property defaults whose value can be exposed to eval reflection. +fn eval_native_property_default_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_eval_native_instance_property_defaults(class_name, class_info, &mut registrations); + collect_eval_native_static_property_defaults(class_name, class_info, &mut registrations); + } + registrations +} + /// Registers generated AOT class parent metadata for eval `parent::` resolution. fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_offset: usize) { let mut parents = ctx @@ -570,6 +594,62 @@ fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_off } } +/// Adds supported instance-property default metadata for one class to eval registration. +fn collect_eval_native_instance_property_defaults( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + for (slot, (property_name, _)) in class_info.properties.iter().enumerate() { + let default = class_info.defaults.get(slot).and_then(Option::as_ref); + let is_declared = class_info.property_slot_is_declared(slot, property_name); + let is_abstract = class_info.abstract_properties.contains(property_name); + let Some(default) = eval_native_property_default(default, is_declared, is_abstract) else { + continue; + }; + registrations.push(EvalNativePropertyDefaultRegistration { + class_name: eval_native_instance_property_declaring_class( + class_name, + class_info, + property_name, + ) + .to_string(), + property_name: property_name.clone(), + default, + }); + } +} + +/// Adds supported static-property default metadata for one class to eval registration. +fn collect_eval_native_static_property_defaults( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + for (slot, (property_name, _)) in class_info.static_properties.iter().enumerate() { + let default = class_info + .static_defaults + .get(slot) + .and_then(Option::as_ref); + let is_declared = class_info + .declared_static_properties + .contains(property_name); + let Some(default) = eval_native_property_default(default, is_declared, false) else { + continue; + }; + registrations.push(EvalNativePropertyDefaultRegistration { + class_name: eval_native_static_property_declaring_class( + class_name, + class_info, + property_name, + ) + .to_string(), + property_name: property_name.clone(), + default, + }); + } +} + /// Adds declared instance-property type metadata for one class to eval registration. fn collect_eval_native_instance_property_types( class_name: &str, @@ -839,6 +919,21 @@ fn eval_native_callable_default(expr: &Expr) -> Option, + is_declared: bool, + is_abstract: bool, +) -> Option { + if let Some(default) = default { + return eval_native_callable_default(default); + } + (!is_declared && !is_abstract).then_some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + payload: 0, + }) +} + /// Converts a negated literal default into the compact eval bridge default ABI. fn eval_native_callable_negated_default(expr: &Expr) -> Option { match &expr.kind { @@ -1380,6 +1475,64 @@ fn register_eval_native_property_type( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native property-default metadata registration call into the eval context. +fn register_eval_native_property_default( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativePropertyDefaultRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let property_key = format!( + "{}::{}", + registration.class_name, registration.property_name + ); + let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &property_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + property_key_len as i64, + ); + let symbol = match ®istration.default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + *kind, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + *payload, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_scalar") + } + EvalNativeCallableDefault::String(value) => { + let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_string") + } + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native constructor parameter-name registration call. fn register_eval_native_constructor_param( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9c5a36774c..003a702966 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7551,6 +7551,55 @@ echo $untyped->getType() === null ? "N" : "n"; ); } +/// Verifies eval exposes supported generated/AOT property defaults through +/// `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()`. +#[test] +fn test_eval_reflection_property_exposes_aot_default_metadata() { + let out = compile_and_run_capture( + r#"hasDefaultValue() ? "D:" : "d:"; + $value = $property->getDefaultValue(); + echo $value === null ? "null" : $value; + echo "|"; +} +$listed = null; +foreach ((new ReflectionClass("EvalAotReflectPropertyDefaultTarget"))->getProperties() as $property) { + if ($property->getName() === "count") { + $listed = $property; + } +} +echo "listed:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "count:D:7|label:D:ok|nullable:D:null|implicit:D:null|typed:d:null|base:D:3|flag:D:1|neg:D:-1.5|listed:D:7" + ); +} + /// Verifies eval can probe generated/AOT method predicate metadata through /// `ReflectionClass::hasMethod()`, `getMethod()`, and direct `ReflectionMethod`. #[test] From 76c389492c9277939ede89417ea8dd46f711caaf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 22:38:21 +0200 Subject: [PATCH 0494/1208] Expose AOT member attributes in eval reflection --- crates/elephc-eval/src/context.rs | 66 +++++ crates/elephc-eval/src/ffi/native_methods.rs | 167 +++++++++++- crates/elephc-eval/src/ffi/tests.rs | 115 +++++++- .../elephc-eval/src/interpreter/reflection.rs | 49 +++- docs/php/eval.md | 10 +- src/codegen/lower_inst/builtins/eval.rs | 248 +++++++++++++++++- tests/codegen/eval.rs | 53 ++++ 7 files changed, 699 insertions(+), 9 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 4857f9256a..86d32ba51e 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -271,8 +271,10 @@ pub struct ElephcEvalContext { native_static_methods: HashMap<(String, String), NativeCallableSignature>, native_constructors: HashMap, native_class_parents: HashMap, + native_method_attributes: HashMap<(String, String), Vec>, native_property_types: HashMap<(String, String), EvalParameterType>, native_property_defaults: HashMap<(String, String), NativeCallableDefault>, + native_property_attributes: HashMap<(String, String), Vec>, static_locals: HashMap<(String, String), RuntimeCellHandle>, static_properties: HashMap<(String, String), RuntimeCellHandle>, class_constants: HashMap<(String, String), RuntimeCellHandle>, @@ -322,8 +324,10 @@ impl ElephcEvalContext { native_static_methods: HashMap::new(), native_constructors: HashMap::new(), native_class_parents: HashMap::new(), + native_method_attributes: HashMap::new(), native_property_types: HashMap::new(), native_property_defaults: HashMap::new(), + native_property_attributes: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -374,8 +378,10 @@ impl ElephcEvalContext { native_static_methods: HashMap::new(), native_constructors: HashMap::new(), native_class_parents: HashMap::new(), + native_method_attributes: HashMap::new(), native_property_types: HashMap::new(), native_property_defaults: HashMap::new(), + native_property_attributes: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), class_constants: HashMap::new(), @@ -1761,6 +1767,36 @@ impl ElephcEvalContext { .map(String::as_str) } + /// Appends generated AOT method attribute metadata for eval reflection. + pub fn define_native_method_attribute( + &mut self, + class_name: &str, + method_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_method_key(class_name, method_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_method_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT method attribute metadata by PHP class and method name. + pub fn native_method_attributes( + &self, + class_name: &str, + method_name: &str, + ) -> Vec { + self.native_method_attributes + .get(&native_method_key(class_name, method_name)) + .cloned() + .unwrap_or_default() + } + /// Defines generated AOT property type metadata for eval reflection. pub fn define_native_property_type( &mut self, @@ -1813,6 +1849,36 @@ impl ElephcEvalContext { .cloned() } + /// Appends generated AOT property attribute metadata for eval reflection. + pub fn define_native_property_attribute( + &mut self, + class_name: &str, + property_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT property attribute metadata by PHP class and property name. + pub fn native_property_attributes( + &self, + class_name: &str, + property_name: &str, + ) -> Vec { + self.native_property_attributes + .get(&native_property_key(class_name, property_name)) + .cloned() + .unwrap_or_default() + } + /// Returns true when the context has a dynamic or native function with this lowercase PHP name. pub fn has_function(&self, name: &str) -> bool { self.functions.contains_key(name) || self.native_functions.contains_key(name) diff --git a/crates/elephc-eval/src/ffi/native_methods.rs b/crates/elephc-eval/src/ffi/native_methods.rs index 082467def9..90c8e3a7c7 100644 --- a/crates/elephc-eval/src/ffi/native_methods.rs +++ b/crates/elephc-eval/src/ffi/native_methods.rs @@ -13,12 +13,22 @@ use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ABI_VERSION}; use crate::context::{NativeCallableDefault, NativeCallableSignature}; -use crate::eval_ir::{EvalParameterType, EvalParameterTypeVariant}; +use crate::eval_ir::{ + EvalAttribute, EvalAttributeArg, EvalParameterType, EvalParameterTypeVariant, +}; const NATIVE_DEFAULT_NULL: u64 = 0; const NATIVE_DEFAULT_BOOL: u64 = 1; const NATIVE_DEFAULT_INT: u64 = 2; const NATIVE_DEFAULT_FLOAT: u64 = 3; +const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; +const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; +const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; +const NATIVE_ATTRIBUTE_ARGS_SUPPORTED: u8 = 1; +const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; +const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; +const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; +const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; #[derive(Clone, Copy)] enum NativeCallableTypePosition { @@ -565,6 +575,23 @@ pub unsafe extern "C" fn __elephc_eval_register_native_property_default_string( .unwrap_or(0) } +/// Registers one generated native PHP method/property attribute in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `record_ptr` must point to one +/// readable binary member-attribute metadata record. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_member_attribute( + ctx: *mut ElephcEvalContext, + record_ptr: *const u8, + record_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_member_attribute_inner(ctx, record_ptr, record_len) + }) + .unwrap_or(0) +} + /// Runs native method registration after installing a panic boundary. /// /// # Safety @@ -1144,6 +1171,144 @@ unsafe fn register_native_property_default_inner( i32::from(context.define_native_property_default(class_name, property_name, default)) } +/// Runs native member-attribute registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_member_attribute`; invalid handles or +/// binary records fail closed as `false`. +unsafe fn register_native_member_attribute_inner( + ctx: *mut ElephcEvalContext, + record_ptr: *const u8, + record_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Some(record) = native_member_attribute_record_from_abi(record_ptr, record_len) else { + return 0; + }; + let Some((class_name, member_name)) = split_method_key(&record.member_key) else { + return 0; + }; + match record.owner_kind { + NATIVE_MEMBER_ATTRIBUTE_METHOD => i32::from(context.define_native_method_attribute( + class_name, + member_name, + record.attribute, + )), + NATIVE_MEMBER_ATTRIBUTE_PROPERTY => i32::from(context.define_native_property_attribute( + class_name, + member_name, + record.attribute, + )), + _ => 0, + } +} + +/// Decoded native member-attribute metadata record. +struct NativeMemberAttributeRecord { + owner_kind: u8, + member_key: String, + attribute: EvalAttribute, +} + +/// Decodes one generated native member-attribute metadata record. +fn native_member_attribute_record_from_abi( + record_ptr: *const u8, + record_len: u64, +) -> Option { + if record_ptr.is_null() || record_len == 0 { + return None; + } + let record_len = usize::try_from(record_len).ok()?; + let bytes = unsafe { std::slice::from_raw_parts(record_ptr, record_len) }; + let mut offset = 0usize; + let owner_kind = native_attribute_take_u8(bytes, &mut offset)?; + let member_key = native_attribute_take_string(bytes, &mut offset)?; + let attribute_name = native_attribute_take_string(bytes, &mut offset)?; + let args = native_attribute_take_args(bytes, &mut offset)?; + (offset == bytes.len()).then_some(NativeMemberAttributeRecord { + owner_kind, + member_key, + attribute: EvalAttribute::new(attribute_name, args), + }) +} + +/// Decodes the optional argument vector from a native attribute record. +fn native_attribute_take_args( + bytes: &[u8], + offset: &mut usize, +) -> Option>> { + match native_attribute_take_u8(bytes, offset)? { + NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED => Some(None), + NATIVE_ATTRIBUTE_ARGS_SUPPORTED => { + let count = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut args = Vec::with_capacity(count); + for _ in 0..count { + args.push(native_attribute_take_arg(bytes, offset)?); + } + Some(Some(args)) + } + _ => None, + } +} + +/// Decodes one literal argument from a native attribute record. +fn native_attribute_take_arg(bytes: &[u8], offset: &mut usize) -> Option { + match native_attribute_take_u8(bytes, offset)? { + NATIVE_ATTRIBUTE_ARG_NULL => Some(EvalAttributeArg::Null), + NATIVE_ATTRIBUTE_ARG_BOOL => Some(EvalAttributeArg::Bool( + native_attribute_take_u8(bytes, offset)? != 0, + )), + NATIVE_ATTRIBUTE_ARG_INT => Some(EvalAttributeArg::Int(native_attribute_take_i64( + bytes, offset, + )?)), + NATIVE_ATTRIBUTE_ARG_STRING => { + native_attribute_take_string(bytes, offset).map(EvalAttributeArg::String) + } + _ => None, + } +} + +/// Reads one UTF-8 string with a little-endian u32 byte length prefix. +fn native_attribute_take_string(bytes: &[u8], offset: &mut usize) -> Option { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let chunk = native_attribute_take_bytes(bytes, offset, len)?; + std::str::from_utf8(chunk).ok().map(str::to_string) +} + +/// Reads one little-endian i64 from a native attribute record. +fn native_attribute_take_i64(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(i64::from_le_bytes(chunk.try_into().ok()?)) +} + +/// Reads one little-endian u32 from a native attribute record. +fn native_attribute_take_u32(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(u32::from_le_bytes(chunk.try_into().ok()?)) +} + +/// Reads one byte from a native attribute record. +fn native_attribute_take_u8(bytes: &[u8], offset: &mut usize) -> Option { + native_attribute_take_bytes(bytes, offset, 1).map(|chunk| chunk[0]) +} + +/// Reads one bounded byte slice and advances the decode offset. +fn native_attribute_take_bytes<'a>( + bytes: &'a [u8], + offset: &mut usize, + len: usize, +) -> Option<&'a [u8]> { + let end = offset.checked_add(len)?; + let chunk = bytes.get(*offset..end)?; + *offset = end; + Some(chunk) +} + /// Decodes scalar default kind/payload ABI fields into native callable metadata. fn native_callable_scalar_default( default_kind: u64, diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-eval/src/ffi/tests.rs index 496a1d7e58..10f454f1bc 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-eval/src/ffi/tests.rs @@ -22,7 +22,7 @@ use crate::abi::{ }; use crate::context::NativeCallableDefault; use crate::errors::EvalStatus; -use crate::eval_ir::EvalParameterTypeVariant; +use crate::eval_ir::{EvalAttributeArg, EvalParameterTypeVariant}; use crate::value::{RuntimeCell, RuntimeCellHandle}; use std::ffi::c_void; @@ -34,6 +34,55 @@ unsafe extern "C" fn fake_native_invoker( std::ptr::null_mut() } +/// Builds one native member-attribute ABI record for registration tests. +fn native_member_attribute_record( + owner_kind: u8, + member_key: &str, + attribute_name: &str, + args: Option<&[EvalAttributeArg]>, +) -> Vec { + let mut record = Vec::new(); + record.push(owner_kind); + native_member_attribute_push_string(&mut record, member_key); + native_member_attribute_push_string(&mut record, attribute_name); + match args { + Some(args) => { + record.push(1); + record.extend_from_slice(&(args.len() as u32).to_le_bytes()); + for arg in args { + native_member_attribute_push_arg(&mut record, arg); + } + } + None => record.push(0), + } + record +} + +/// Appends one test attribute argument to a native member-attribute ABI record. +fn native_member_attribute_push_arg(record: &mut Vec, arg: &EvalAttributeArg) { + match arg { + EvalAttributeArg::Null => record.push(0), + EvalAttributeArg::Bool(value) => { + record.push(1); + record.push(u8::from(*value)); + } + EvalAttributeArg::Int(value) => { + record.push(2); + record.extend_from_slice(&value.to_le_bytes()); + } + EvalAttributeArg::String(value) => { + record.push(3); + native_member_attribute_push_string(record, value); + } + } +} + +/// Appends one length-prefixed string to a native member-attribute ABI record. +fn native_member_attribute_push_string(record: &mut Vec, value: &str) { + record.extend_from_slice(&(value.len() as u32).to_le_bytes()); + record.extend_from_slice(value.as_bytes()); +} + /// Verifies the exported version entry point reports the crate ABI constant. #[test] fn abi_version_matches_constant() { @@ -595,6 +644,70 @@ fn register_native_property_default_records_metadata() { .is_none()); } +/// Verifies native AOT member attributes are available to eval reflection. +#[test] +fn register_native_member_attribute_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let method_record = native_member_attribute_record( + 0, + "KnownClass::run", + "Route", + Some(&[ + EvalAttributeArg::String("api".to_string()), + EvalAttributeArg::Int(7), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + ]), + ); + let property_record = native_member_attribute_record(1, "KnownClass::id", "Column", None); + let invalid_record = [99, 0, 0, 0, 0]; + + let method_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + method_record.as_ptr(), + method_record.len() as u64, + ) + }; + let property_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + property_record.as_ptr(), + property_record.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + invalid_record.as_ptr(), + invalid_record.len() as u64, + ) + }; + + assert_eq!(method_registered, 1); + let method_attributes = ctx.native_method_attributes("knownclass", "RUN"); + assert_eq!(method_attributes.len(), 1); + assert_eq!(method_attributes[0].name(), "Route"); + assert_eq!( + method_attributes[0].args(), + Some( + [ + EvalAttributeArg::String("api".to_string()), + EvalAttributeArg::Int(7), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + ] + .as_slice() + ) + ); + assert_eq!(property_registered, 1); + let property_attributes = ctx.native_property_attributes("KnownClass", "id"); + assert_eq!(property_attributes.len(), 1); + assert_eq!(property_attributes[0].name(), "Column"); + assert!(property_attributes[0].args().is_none()); + assert_eq!(invalid_registered, 0); +} + /// Verifies scope allocation returns an empty opaque activation scope handle. #[test] fn scope_new_returns_empty_handle() { diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 7b4a9847f3..0cfd5c5a2e 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1557,6 +1557,7 @@ fn eval_reflection_aot_method_metadata_if_exists( &declaring_class_name, method_name, flags, + Vec::new(), None, ))) } @@ -1581,10 +1582,17 @@ fn eval_reflection_aot_method_metadata_with_signature_if_exists( signature = eval_reflection_aot_method_signature(runtime_class_name, method_name, flags, context); } + let attributes = eval_reflection_aot_method_attributes( + runtime_class_name, + &declaring_class_name, + method_name, + context, + ); Ok(Some(eval_reflection_aot_method_metadata( &declaring_class_name, method_name, flags, + attributes, signature.as_ref(), ))) } @@ -1606,6 +1614,7 @@ fn eval_reflection_aot_method_metadata( class_name: &str, method_name: &str, flags: u64, + attributes: Vec, signature: Option<&NativeCallableSignature>, ) -> EvalReflectionMemberMetadata { let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { @@ -1625,7 +1634,7 @@ fn eval_reflection_aot_method_metadata( .and_then(eval_reflection_parameter_type_metadata); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), - attributes: Vec::new(), + attributes, visibility, is_static: flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, is_final: flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, @@ -1641,6 +1650,20 @@ fn eval_reflection_aot_method_metadata( } } +/// Returns registered generated/AOT method attributes for one reflected method. +fn eval_reflection_aot_method_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_method_attributes(declaring_class_name, method_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_method_attributes(runtime_class_name, method_name) +} + /// Selects the registered native signature for an AOT method-like member. fn eval_reflection_aot_method_signature( class_name: &str, @@ -1772,9 +1795,16 @@ fn eval_reflection_aot_property_metadata_if_exists( property_name, context, ); + let attributes = eval_reflection_aot_property_attributes( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); Ok(Some(eval_reflection_aot_property_metadata( &declaring_class_name, flags, + attributes, type_metadata, default_value, ))) @@ -1808,10 +1838,25 @@ fn eval_reflection_aot_property_default_value( .map(eval_reflection_native_callable_default_expr) } +/// Returns registered generated/AOT property attributes for one reflected property. +fn eval_reflection_aot_property_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_property_attributes(declaring_class_name, property_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_property_attributes(runtime_class_name, property_name) +} + /// Converts AOT property flag metadata into the eval ReflectionProperty shape. fn eval_reflection_aot_property_metadata( class_name: &str, flags: u64, + attributes: Vec, type_metadata: Option, default_value: Option, ) -> EvalReflectionMemberMetadata { @@ -1828,7 +1873,7 @@ fn eval_reflection_aot_property_metadata( let is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), - attributes: Vec::new(), + attributes, visibility, is_static, is_final, diff --git a/docs/php/eval.md b/docs/php/eval.md index 9a11a9e8c5..30f5eb60a7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -267,8 +267,9 @@ names, declared parameter types, declared return types, required/optional counts, and registered scalar or null default values for generated constructor, instance-method, and static-method signatures. AOT property reflection exposes registered declared property types and supported scalar, -string, or null default values for generated property metadata. AOT member -reflection does not yet expose AOT attributes. +string, or null default values for generated property metadata. AOT method and +property reflection expose generated member attributes when their arguments fit +the materializable literal subset. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected @@ -557,8 +558,9 @@ property, and function/method return metadata, broader parameter and generated property default-value objects beyond the eval-supported constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice. Generated/AOT method type -metadata is exposed for registered public bridge signatures, while broader -non-public or unsupported bridge shapes remain outside that slice. Eval object cloning covers ordinary +metadata and generated/AOT method/property attributes are exposed for registered +metadata slices, while broader non-public or unsupported bridge shapes remain +outside that slice. Eval object cloning covers ordinary emitted/AOT storage and public AOT `__clone()` hooks, but non-public AOT `__clone()` scope checks and broader bridge signatures remain outside that bridge slice. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 693a18be46..8614f7c157 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -21,7 +21,7 @@ use crate::codegen::{ use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op}; use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; use crate::parser::ast::{Expr, ExprKind, TypeExpr, Visibility}; -use crate::types::{ClassInfo, FunctionSig, PhpType}; +use crate::types::{AttrArgValue, ClassInfo, FunctionSig, PhpType}; use super::super::super::context::FunctionContext; use super::super::{ @@ -55,6 +55,14 @@ const NATIVE_DEFAULT_NULL: i64 = 0; const NATIVE_DEFAULT_BOOL: i64 = 1; const NATIVE_DEFAULT_INT: i64 = 2; const NATIVE_DEFAULT_FLOAT: i64 = 3; +const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; +const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; +const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; +const NATIVE_ATTRIBUTE_ARGS_SUPPORTED: u8 = 1; +const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; +const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; +const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; +const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; /// Local slot metadata needed for conservative eval scope synchronization. #[derive(Clone)] @@ -112,6 +120,15 @@ struct EvalNativePropertyDefaultRegistration { default: EvalNativeCallableDefault, } +/// A module-local member attribute that can be registered with the eval context. +struct EvalNativeMemberAttributeRegistration { + owner_kind: u8, + class_name: String, + member_name: String, + attribute_name: String, + attribute_args: Option>, +} + /// Scalar native callable default that can be registered with libelephc-eval. enum EvalNativeCallableDefault { Scalar { kind: i64, payload: i64 }, @@ -488,6 +505,9 @@ fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context for registration in eval_native_property_default_registrations(ctx) { register_eval_native_property_default(ctx, context_offset, ®istration); } + for registration in eval_native_member_attribute_registrations(ctx) { + register_eval_native_member_attribute(ctx, context_offset, ®istration); + } register_eval_native_class_parents(ctx, context_offset); } @@ -573,6 +593,41 @@ fn eval_native_property_default_registrations( registrations } +/// Collects AOT member attributes whose metadata can be exposed to eval reflection. +fn eval_native_member_attribute_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_eval_native_method_attributes(class_name, class_info, &mut registrations); + collect_eval_native_property_attributes(class_name, class_info, &mut registrations); + } + dedupe_eval_native_member_attribute_registrations(registrations) +} + +/// Removes inherited duplicate member-attribute registrations by normalized metadata key. +fn dedupe_eval_native_member_attribute_registrations( + registrations: Vec, +) -> Vec { + let mut seen = std::collections::HashSet::new(); + let mut unique = Vec::with_capacity(registrations.len()); + for registration in registrations { + let key = ( + registration.owner_kind, + php_symbol_key(®istration.class_name), + registration.member_name.clone(), + registration.attribute_name.clone(), + registration.attribute_args.clone(), + ); + if seen.insert(key) { + unique.push(registration); + } + } + unique +} + /// Registers generated AOT class parent metadata for eval `parent::` resolution. fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_offset: usize) { let mut parents = ctx @@ -594,6 +649,82 @@ fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_off } } +/// Adds method attribute metadata for one class to eval registration. +fn collect_eval_native_method_attributes( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut methods = class_info.method_attribute_names.iter().collect::>(); + methods.sort_by_key(|(method_name, _)| method_name.as_str()); + for (method_name, attribute_names) in methods { + let attribute_args = class_info + .method_attribute_args + .get(method_name) + .cloned() + .unwrap_or_default(); + collect_eval_native_member_attributes( + NATIVE_MEMBER_ATTRIBUTE_METHOD, + eval_native_method_declaring_class(class_name, class_info, method_name), + method_name, + attribute_names, + &attribute_args, + registrations, + ); + } +} + +/// Adds property attribute metadata for one class to eval registration. +fn collect_eval_native_property_attributes( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut properties = class_info + .property_attribute_names + .iter() + .collect::>(); + properties.sort_by_key(|(property_name, _)| property_name.as_str()); + for (property_name, attribute_names) in properties { + let attribute_args = class_info + .property_attribute_args + .get(property_name) + .cloned() + .unwrap_or_default(); + collect_eval_native_member_attributes( + NATIVE_MEMBER_ATTRIBUTE_PROPERTY, + eval_native_property_attribute_declaring_class(class_name, class_info, property_name), + property_name, + attribute_names, + &attribute_args, + registrations, + ); + } +} + +/// Adds aligned attribute name/argument metadata for one AOT member. +fn collect_eval_native_member_attributes( + owner_kind: u8, + class_name: &str, + member_name: &str, + attribute_names: &[String], + attribute_args: &[Option>], + registrations: &mut Vec, +) { + for (index, attribute_name) in attribute_names.iter().enumerate() { + let Some(args) = attribute_args.get(index).cloned().flatten() else { + continue; + }; + registrations.push(EvalNativeMemberAttributeRegistration { + owner_kind, + class_name: class_name.to_string(), + member_name: member_name.to_string(), + attribute_name: attribute_name.clone(), + attribute_args: Some(args), + }); + } +} + /// Adds supported instance-property default metadata for one class to eval registration. fn collect_eval_native_instance_property_defaults( class_name: &str, @@ -731,6 +862,40 @@ fn eval_native_static_property_declaring_class<'a>( .unwrap_or(reflected_class) } +/// Returns the class name that declares one AOT method metadata row. +fn eval_native_method_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + method_name: &str, +) -> &'a str { + class_info + .method_impl_classes + .get(method_name) + .or_else(|| class_info.static_method_impl_classes.get(method_name)) + .or_else(|| class_info.method_declaring_classes.get(method_name)) + .or_else(|| class_info.static_method_declaring_classes.get(method_name)) + .map(String::as_str) + .unwrap_or(reflected_class) +} + +/// Returns the class name that declares one AOT property attribute row. +fn eval_native_property_attribute_declaring_class<'a>( + reflected_class: &'a str, + class_info: &'a ClassInfo, + property_name: &str, +) -> &'a str { + class_info + .property_declaring_classes + .get(property_name) + .or_else(|| { + class_info + .static_property_declaring_classes + .get(property_name) + }) + .map(String::as_str) + .unwrap_or(reflected_class) +} + /// Adds eligible public instance methods for one class to eval signature registration. fn collect_eval_native_instance_methods( class_name: &str, @@ -1533,6 +1698,87 @@ fn register_eval_native_property_default( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native member-attribute metadata registration call into the eval context. +fn register_eval_native_member_attribute( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeMemberAttributeRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let record = eval_native_member_attribute_record(registration); + let (record_label, record_len) = ctx.data.add_string(&record); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &record_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + record_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_member_attribute"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Encodes one member-attribute registration record for the eval bridge ABI. +fn eval_native_member_attribute_record( + registration: &EvalNativeMemberAttributeRegistration, +) -> Vec { + let mut record = Vec::new(); + record.push(registration.owner_kind); + eval_native_member_attribute_push_string( + &mut record, + &format!("{}::{}", registration.class_name, registration.member_name), + ); + eval_native_member_attribute_push_string(&mut record, ®istration.attribute_name); + match ®istration.attribute_args { + Some(args) => { + record.push(NATIVE_ATTRIBUTE_ARGS_SUPPORTED); + eval_native_member_attribute_push_u32(&mut record, args.len()); + for arg in args { + eval_native_member_attribute_push_arg(&mut record, arg); + } + } + None => record.push(NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED), + } + record +} + +/// Encodes one attribute argument into a member-attribute registration record. +fn eval_native_member_attribute_push_arg(record: &mut Vec, arg: &AttrArgValue) { + match arg { + AttrArgValue::Null => record.push(NATIVE_ATTRIBUTE_ARG_NULL), + AttrArgValue::Bool(value) => { + record.push(NATIVE_ATTRIBUTE_ARG_BOOL); + record.push(u8::from(*value)); + } + AttrArgValue::Int(value) => { + record.push(NATIVE_ATTRIBUTE_ARG_INT); + record.extend_from_slice(&value.to_le_bytes()); + } + AttrArgValue::Str(value) => { + record.push(NATIVE_ATTRIBUTE_ARG_STRING); + eval_native_member_attribute_push_string(record, value); + } + } +} + +/// Encodes one length-prefixed UTF-8 string into a member-attribute registration record. +fn eval_native_member_attribute_push_string(record: &mut Vec, value: &str) { + eval_native_member_attribute_push_u32(record, value.len()); + record.extend_from_slice(value.as_bytes()); +} + +/// Encodes one little-endian u32 length into a member-attribute registration record. +fn eval_native_member_attribute_push_u32(record: &mut Vec, value: usize) { + let value = u32::try_from(value).unwrap_or(u32::MAX); + record.extend_from_slice(&value.to_le_bytes()); +} + /// Emits one native constructor parameter-name registration call. fn register_eval_native_constructor_param( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 003a702966..07d2d81ca5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7600,6 +7600,59 @@ echo $listed->getDefaultValue(); ); } +/// Verifies eval exposes generated/AOT method and property attributes through +/// `ReflectionMethod::getAttributes()` and `ReflectionProperty::getAttributes()`. +#[test] +fn test_eval_reflection_member_exposes_aot_attributes() { + let out = compile_and_run_capture( + r#"getAttributes(); +echo "M" . count($methodAttrs) . ":"; +echo $methodAttrs[0]->getName() . ":" . $methodAttrs[0]->getArguments()[0] . ":"; +$propertyAttrs = (new ReflectionProperty("EvalAotReflectAttrTarget", "id"))->getAttributes(); +echo "P" . count($propertyAttrs) . ":"; +echo $propertyAttrs[0]->getName() . ":"; +$propertyArgs = $propertyAttrs[0]->getArguments(); +echo $propertyArgs[0] . ":" . $propertyArgs[1] . ":"; +$baseMethodAttrs = (new ReflectionMethod("EvalAotReflectAttrTarget", "baseRun"))->getAttributes(); +echo "BM" . count($baseMethodAttrs) . ":"; +$args = $baseMethodAttrs[0]->getArguments(); +echo $args[0] . ":" . $args[1] . ":" . ($args[2] ? "T" : "F") . ":" . ($args[3] === null ? "N" : "n") . ":"; +$basePropertyAttrs = (new ReflectionProperty("EvalAotReflectAttrTarget", "baseId"))->getAttributes(); +echo "BP" . count($basePropertyAttrs) . ":" . $basePropertyAttrs[0]->getArguments()[0] . ":"; +$listedMethod = (new ReflectionClass("EvalAotReflectAttrTarget"))->getMethod("run"); +echo count($listedMethod->getAttributes()) . ":"; +$listedProperty = (new ReflectionClass("EvalAotReflectAttrTarget"))->getProperty("id"); +echo count($listedProperty->getAttributes()); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "M1:EvalAotMemberAttr:method:P1:EvalAotMemberAttr:property:-3:BM1:base:7:T:N:BP1:baseProp:1:1" + ); +} + /// Verifies eval can probe generated/AOT method predicate metadata through /// `ReflectionClass::hasMethod()`, `getMethod()`, and direct `ReflectionMethod`. #[test] From 75af93e40f0be6c02cfec8e20c2778579322f4ab Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 23:03:19 +0200 Subject: [PATCH 0495/1208] Expose ReflectionAttribute target metadata --- crates/elephc-eval/src/context.rs | 54 ++++++++- .../interpreter/builtins/class_metadata.rs | 23 +++- .../elephc-eval/src/interpreter/reflection.rs | 34 +++++- .../src/interpreter/runtime_ops.rs | 2 + .../elephc-eval/src/interpreter/statements.rs | 20 +++- .../tests/builtins_class_metadata.rs | 52 +++++++++ .../interpreter/tests/support/object_ops.rs | 6 + .../interpreter/tests/support/runtime_ops.rs | 4 +- .../elephc-eval/src/runtime_hooks/externs.rs | 2 + crates/elephc-eval/src/runtime_hooks/ops.rs | 10 +- docs/php/eval.md | 4 +- src/codegen/eval_reflection_helpers.rs | 78 ++++++++++--- src/codegen/lower_inst/builtins/attributes.rs | 110 +++++++++++++++--- src/codegen/lower_inst/objects/reflection.rs | 55 ++++++++- src/types/checker/builtin_types/reflection.rs | 33 +++++- tests/codegen/eval.rs | 46 ++++++++ tests/codegen/oop/attributes.rs | 40 +++++++ 17 files changed, 521 insertions(+), 52 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 86d32ba51e..ee0a6aea8f 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -246,6 +246,40 @@ struct EvalClassAlias { kind: EvalClassAliasKind, } +/// Metadata attached to one synthetic eval `ReflectionAttribute` object. +#[derive(Clone)] +pub struct EvalReflectionAttributeMetadata { + attribute: EvalAttribute, + target: u64, + repeated: bool, +} + +impl EvalReflectionAttributeMetadata { + /// Creates metadata for a materialized `ReflectionAttribute` object. + pub fn new(attribute: EvalAttribute, target: u64, repeated: bool) -> Self { + Self { + attribute, + target, + repeated, + } + } + + /// Returns the underlying eval-retained attribute metadata. + pub const fn attribute(&self) -> &EvalAttribute { + &self.attribute + } + + /// Returns the PHP `Attribute::TARGET_*` bitmask for this reflected owner. + pub const fn target(&self) -> u64 { + self.target + } + + /// Returns whether this owner has multiple attributes with the same name. + pub const fn is_repeated(&self) -> bool { + self.repeated + } +} + /// Process-level eval context passed opaquely across the C ABI. /// /// Generated code never inspects this layout directly; it only passes pointers @@ -281,7 +315,7 @@ pub struct ElephcEvalContext { included_files: HashSet, dynamic_objects: HashMap, dynamic_property_aliases: HashMap<(u64, String), EvalReferenceTarget>, - eval_reflection_attributes: HashMap, + eval_reflection_attributes: HashMap, eval_reflection_classes: HashMap, eval_reflection_functions: HashMap, eval_reflection_methods: HashMap, @@ -845,12 +879,24 @@ impl ElephcEvalContext { } /// Records eval-declared attribute metadata for one synthetic ReflectionAttribute object. - pub fn register_eval_reflection_attribute(&mut self, identity: u64, attribute: EvalAttribute) { - self.eval_reflection_attributes.insert(identity, attribute); + pub fn register_eval_reflection_attribute( + &mut self, + identity: u64, + attribute: EvalAttribute, + target: u64, + repeated: bool, + ) { + self.eval_reflection_attributes.insert( + identity, + EvalReflectionAttributeMetadata::new(attribute, target, repeated), + ); } /// Returns eval-declared attribute metadata attached to a synthetic ReflectionAttribute. - pub fn eval_reflection_attribute(&self, identity: u64) -> Option<&EvalAttribute> { + pub fn eval_reflection_attribute( + &self, + identity: u64, + ) -> Option<&EvalReflectionAttributeMetadata> { self.eval_reflection_attributes.get(&identity) } diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs index 37d5910a43..c3b2b7b9c8 100644 --- a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs @@ -132,7 +132,12 @@ pub(in crate::interpreter) fn eval_class_attribute_metadata_result( return values.array_new(0); }; let attributes = attributes.to_vec(); - eval_reflection_attribute_array_result(&attributes, context, values) + eval_reflection_attribute_array_result( + &attributes, + EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS, + context, + values, + ) } ("class_attribute_args", [class_name, attribute_name]) => { let class_name = eval_class_metadata_name(*class_name, values)?; @@ -189,6 +194,7 @@ fn eval_class_attribute_names_result( /// Builds an indexed `ReflectionAttribute` array from eval-retained attribute metadata. pub(in crate::interpreter) fn eval_reflection_attribute_array_result( attributes: &[EvalAttribute], + target: u64, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -199,15 +205,26 @@ pub(in crate::interpreter) fn eval_reflection_attribute_array_result( }; let key = values.int(index as i64)?; let args = eval_class_attribute_args_result(args, values)?; - let reflection_attribute = values.reflection_attribute_new(attribute.name(), args)?; + let repeated = eval_attribute_is_repeated(attributes, attribute.name()); + let reflection_attribute = + values.reflection_attribute_new(attribute.name(), args, target, repeated)?; let identity = values.object_identity(reflection_attribute)?; - context.register_eval_reflection_attribute(identity, attribute.clone()); + context.register_eval_reflection_attribute(identity, attribute.clone(), target, repeated); values.release(args)?; result = values.array_set(result, key, reflection_attribute)?; } Ok(result) } +/// Returns true when an attribute name appears more than once on the same owner. +fn eval_attribute_is_repeated(attributes: &[EvalAttribute], name: &str) -> bool { + attributes + .iter() + .filter(|attribute| eval_attribute_name_matches(attribute.name(), name)) + .nth(1) + .is_some() +} + /// Builds the indexed mixed array returned by `class_attribute_args()`. fn eval_class_attribute_args_result( args: &[EvalAttributeArg], diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 0cfd5c5a2e..cec315881a 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -33,6 +33,12 @@ const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS: u64 = 1; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_FUNCTION: u64 = 2; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_METHOD: u64 = 4; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_PROPERTY: u64 = 8; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT: u64 = 16; +pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_PARAMETER: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; @@ -2073,7 +2079,12 @@ fn eval_reflection_owner_object_with_members( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let attrs = eval_reflection_attribute_array_result(attributes, context, values)?; + let attrs = eval_reflection_attribute_array_result( + attributes, + eval_reflection_attribute_target(owner_kind), + context, + values, + )?; let interface_names_array = eval_reflection_string_array_result(interface_names, values)?; let trait_names_array = eval_reflection_string_array_result(trait_names, values)?; let method_names_array = eval_reflection_string_array_result(method_names, values)?; @@ -2415,6 +2426,20 @@ fn eval_reflection_string_array_result( Ok(result) } +/// Maps a synthetic reflection owner kind to PHP's `Attribute::TARGET_*` bitmask. +fn eval_reflection_attribute_target(owner_kind: u64) -> u64 { + match owner_kind { + EVAL_REFLECTION_OWNER_CLASS => EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS, + EVAL_REFLECTION_OWNER_FUNCTION => EVAL_REFLECTION_ATTRIBUTE_TARGET_FUNCTION, + EVAL_REFLECTION_OWNER_METHOD => EVAL_REFLECTION_ATTRIBUTE_TARGET_METHOD, + EVAL_REFLECTION_OWNER_PROPERTY => EVAL_REFLECTION_ATTRIBUTE_TARGET_PROPERTY, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT, + _ => 0, + } +} + /// Builds an indexed array of populated ReflectionParameter objects. fn eval_reflection_parameter_object_array_result( parameters: &[EvalReflectionParameterMetadata], @@ -2436,7 +2461,12 @@ fn eval_reflection_parameter_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let attrs = eval_reflection_attribute_array_result(¶meter.attributes, context, values)?; + let attrs = eval_reflection_attribute_array_result( + ¶meter.attributes, + EVAL_REFLECTION_ATTRIBUTE_TARGET_PARAMETER, + context, + values, + )?; let declaring_function = match parameter.declaring_function.as_ref() { Some(metadata) => { eval_reflection_declaring_function_object_result(metadata, context, values)? diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-eval/src/interpreter/runtime_ops.rs index 93cf0f3a9d..d26659ec38 100644 --- a/crates/elephc-eval/src/interpreter/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/runtime_ops.rs @@ -116,6 +116,8 @@ pub trait RuntimeValueOps { &mut self, name: &str, args: RuntimeCellHandle, + target: u64, + repeated: bool, ) -> Result; /// Materializes a synthetic ReflectionClass/Method/Property object through generated private-layout code. diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 3cd400e680..6788fcf8fc 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2995,12 +2995,28 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; return values.method_call(object, method_name, evaluated_args); }; - if let Some(attribute) = context.eval_reflection_attribute(identity).cloned() { + if let Some(attribute_metadata) = context.eval_reflection_attribute(identity).cloned() { if method_name.eq_ignore_ascii_case("newInstance") { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - return eval_reflection_attribute_new_instance_result(&attribute, context, values); + return eval_reflection_attribute_new_instance_result( + attribute_metadata.attribute(), + context, + values, + ); + } + if method_name.eq_ignore_ascii_case("getTarget") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return values.int(attribute_metadata.target() as i64); + } + if method_name.eq_ignore_ascii_case("isRepeated") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return values.bool_value(attribute_metadata.is_repeated()); } } if let Some(result) = eval_reflection_class_implements_interface_result( diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 85743932ee..8f7cd2f65d 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -270,6 +270,58 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionAttribute reports target bitmasks and repeated-owner metadata. +#[test] +fn execute_program_reflection_attribute_reports_target_and_repetition() { + let program = parse_fragment( + br#"class EvalTargetMarker { + public function __construct($name = null) {} +} +#[EvalTargetMarker("class-a"), EvalTargetMarker("class-b")] +class EvalReflectAttributeTarget { + #[EvalTargetMarker("method")] + public function run(#[EvalTargetMarker("param")] $id) {} + #[EvalTargetMarker("property")] + public $id; + #[EvalTargetMarker("const")] + public const ANSWER = 42; +} +enum EvalReflectAttributeEnum { + #[EvalTargetMarker("case")] + case Ready; +} +$class_attrs = (new ReflectionClass("EvalReflectAttributeTarget"))->getAttributes(); +echo $class_attrs[0]->getTarget(); echo "/"; +echo $class_attrs[0]->isRepeated() ? "R" : "r"; echo ":"; +echo $class_attrs[1]->getTarget(); echo "/"; +echo $class_attrs[1]->isRepeated() ? "R" : "r"; echo ":"; +$method_attr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getAttributes()[0]; +echo $method_attr->getTarget(); echo "/"; +echo $method_attr->isRepeated() ? "R" : "r"; echo ":"; +$property_attr = (new ReflectionProperty("EvalReflectAttributeTarget", "id"))->getAttributes()[0]; +echo $property_attr->getTarget(); echo "/"; +echo $property_attr->isRepeated() ? "R" : "r"; echo ":"; +$param_attr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getParameters()[0]->getAttributes()[0]; +echo $param_attr->getTarget(); echo "/"; +echo $param_attr->isRepeated() ? "R" : "r"; echo ":"; +$const_attr = (new ReflectionClassConstant("EvalReflectAttributeTarget", "ANSWER"))->getAttributes()[0]; +echo $const_attr->getTarget(); echo "/"; +echo $const_attr->isRepeated() ? "R" : "r"; echo ":"; +$case_attr = (new ReflectionEnumUnitCase("EvalReflectAttributeEnum", "Ready"))->getAttributes()[0]; +echo $case_attr->getTarget(); echo "/"; +echo $case_attr->isRepeated() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1/R:1/R:4/r:8/r:32/r:16/r:16/r"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies reflection owner origin metadata APIs report eval user-defined defaults. #[test] fn execute_program_reflection_owners_report_origin_metadata_defaults() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index c5073b6a5d..f2c56352f7 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -518,13 +518,19 @@ impl FakeOps { &mut self, name: &str, args: RuntimeCellHandle, + target: u64, + repeated: bool, ) -> Result { let name = self.string(name)?; let factory = self.int(0)?; + let target = self.int(target as i64)?; + let repeated = self.bool_value(repeated)?; let object = self.alloc(FakeValue::Object(vec![ ("__name".to_string(), name), ("__args".to_string(), args), ("__factory".to_string(), factory), + ("__target".to_string(), target), + ("__is_repeated".to_string(), repeated), ])); self.object_classes .insert(object.as_ptr() as usize, "ReflectionAttribute".to_string()); diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs index c182757426..d15472fcc8 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs @@ -125,8 +125,10 @@ impl RuntimeValueOps for FakeOps { &mut self, name: &str, args: RuntimeCellHandle, + target: u64, + repeated: bool, ) -> Result { - self.runtime_reflection_attribute_new(name, args) + self.runtime_reflection_attribute_new(name, args, target, repeated) } /// Materializes one fake Reflection owner object for eval metadata tests. fn reflection_owner_new( diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-eval/src/runtime_hooks/externs.rs index 3728461241..73d85d3ecf 100644 --- a/crates/elephc-eval/src/runtime_hooks/externs.rs +++ b/crates/elephc-eval/src/runtime_hooks/externs.rs @@ -77,6 +77,8 @@ unsafe extern "C" { name_ptr: *const u8, name_len: u64, args: *mut RuntimeCell, + target: u64, + repeated: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_reflection_owner_new( owner_kind: u64, diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-eval/src/runtime_hooks/ops.rs index da664837b4..4ae55e7f64 100644 --- a/crates/elephc-eval/src/runtime_hooks/ops.rs +++ b/crates/elephc-eval/src/runtime_hooks/ops.rs @@ -200,9 +200,17 @@ impl RuntimeValueOps for ElephcRuntimeOps { &mut self, name: &str, args: RuntimeCellHandle, + target: u64, + repeated: bool, ) -> Result { Self::handle(unsafe { - __elephc_eval_reflection_attribute_new(name.as_ptr(), name.len() as u64, args.as_ptr()) + __elephc_eval_reflection_attribute_new( + name.as_ptr(), + name.len() as u64, + args.as_ptr(), + target, + if repeated { 1 } else { 0 }, + ) }) } diff --git a/docs/php/eval.md b/docs/php/eval.md index 30f5eb60a7..243946c9a7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -174,7 +174,9 @@ interfaces, traits, and enums are visible through `class_attribute_names()`, `class_attribute_args()`, and `class_get_attributes()` when their arguments are supported literal positional values (`string`, `int`, `bool`, `null`, or negated integer literals). `ReflectionAttribute::newInstance()` instantiates -eval-declared attribute classes from those materialized attributes. +eval-declared attribute classes from those materialized attributes, and +`ReflectionAttribute::getTarget()` / `isRepeated()` report the reflected owner +target and same-owner repetition metadata. Attribute names remain visible when an attribute uses unsupported argument syntax, but requesting those arguments is a runtime fatal. Private parent properties shadowed by same-named child properties use separate diff --git a/src/codegen/eval_reflection_helpers.rs b/src/codegen/eval_reflection_helpers.rs index cd34fbd1bc..0dedbf7947 100644 --- a/src/codegen/eval_reflection_helpers.rs +++ b/src/codegen/eval_reflection_helpers.rs @@ -10,6 +10,8 @@ //! populate through public property writes. //! - Eval-declared attributes do not have compile-time factories, so the helper //! writes factory id 0; `ReflectionAttribute::newInstance()` then returns null. +//! - Target and repetition metadata are passed by the Rust eval bridge and +//! stored in the same private layout used by generated reflection objects. use crate::codegen::abi; use crate::codegen::emit::Emitter; @@ -29,6 +31,10 @@ struct ReflectionAttributeLayout { args_hi: usize, factory_lo: usize, factory_hi: usize, + target_lo: usize, + target_hi: usize, + repeated_lo: usize, + repeated_hi: usize, } /// Emits eval reflection helpers when any lowered function owns an eval context. @@ -83,6 +89,8 @@ fn reflection_attribute_layout(module: &Module) -> Option Option, attr_names: &[String], attr_args: &[Option>], + target: i64, ) -> Result<()> { let layout = reflection_attribute_layout(ctx)?; allocate_indexed_array(ctx, attr_names.len().max(1), 8); @@ -147,6 +166,12 @@ pub(in crate::codegen::lower_inst) fn emit_reflection_attribute_array( emit_set_name_property(ctx, attr_name, &layout); emit_set_args_property(ctx, attr_arg_list, &layout)?; emit_set_factory_property(ctx, factory_id, &layout); + emit_set_target_property(ctx, target, &layout); + emit_set_repeated_property( + ctx, + reflection_attribute_name_is_repeated(attr_names, attr_name), + &layout, + ); emit_append_reflection_attribute_object(ctx); } @@ -180,6 +205,8 @@ fn reflection_attribute_layout(ctx: &FunctionContext<'_>) -> Result) -> Result { - ctx.emitter.instruction(&format!("mov x0, #{}", payload_size)); // request ReflectionAttribute object payload storage + ctx.emitter + .instruction(&format!("mov x0, #{}", payload_size)); // request ReflectionAttribute object payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks ReflectionAttribute as an object ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload - ctx.emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the ReflectionAttribute class id + ctx.emitter + .instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the ReflectionAttribute class id ctx.emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov rax, {}", payload_size)); // request ReflectionAttribute object payload storage + ctx.emitter + .instruction(&format!("mov rax, {}", payload_size)); // request ReflectionAttribute object payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction(&format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4)); // materialize the x86_64 object heap kind word + ctx.emitter.instruction(&format!( + "mov r10, 0x{:x}", + (X86_64_HEAP_MAGIC_HI32 << 32) | 4 + )); // materialize the x86_64 object heap kind word ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload - ctx.emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the ReflectionAttribute class id + ctx.emitter + .instruction(&format!("mov r10, {}", layout.class_id)); // materialize the ReflectionAttribute class id ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero } } @@ -297,6 +335,44 @@ fn emit_set_factory_property( abi::emit_store_zero_to_address(ctx.emitter, object_reg, layout.factory_hi); } +/// Stores the PHP `Attribute::TARGET_*` bitmask on the stacked reflection object. +fn emit_set_target_property( + ctx: &mut FunctionContext<'_>, + target: i64, + layout: &ReflectionAttributeLayout, +) { + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + let target_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, target_reg, target); + abi::emit_store_to_address(ctx.emitter, target_reg, object_reg, layout.target_lo); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, layout.target_hi); +} + +/// Stores whether this attribute name is repeated on the same owner. +fn emit_set_repeated_property( + ctx: &mut FunctionContext<'_>, + repeated: bool, + layout: &ReflectionAttributeLayout, +) { + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + let repeated_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, repeated_reg, if repeated { 1 } else { 0 }); + abi::emit_store_to_address(ctx.emitter, repeated_reg, object_reg, layout.repeated_lo); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, layout.repeated_hi); +} + +/// Returns true when an attribute name appears multiple times on one reflected owner. +fn reflection_attribute_name_is_repeated(attr_names: &[String], attr_name: &str) -> bool { + let needle = php_symbol_key(attr_name.trim_start_matches('\\')); + attr_names + .iter() + .filter(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == needle) + .nth(1) + .is_some() +} + /// Appends the stacked object to the stacked result array and leaves the array in result. fn emit_append_reflection_attribute_object(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { @@ -461,7 +537,8 @@ fn emit_box_scalar_arg_aarch64(ctx: &mut FunctionContext<'_>, arg: &AttrArgValue } AttrArgValue::Bool(value) => { ctx.emitter.instruction("mov x0, #3"); // runtime tag 3 = boolean payload - ctx.emitter.instruction(&format!("mov x1, #{}", *value as u64)); // pass the captured boolean as the mixed low word + ctx.emitter + .instruction(&format!("mov x1, #{}", *value as u64)); // pass the captured boolean as the mixed low word ctx.emitter.instruction("mov x2, xzr"); // boolean mixed payloads do not use the high word } AttrArgValue::Str(value) => { @@ -508,7 +585,8 @@ fn emit_box_scalar_arg_x86_64(ctx: &mut FunctionContext<'_>, arg: &AttrArgValue) } AttrArgValue::Bool(value) => { ctx.emitter.instruction("mov rax, 3"); // runtime tag 3 = boolean payload - ctx.emitter.instruction(&format!("mov rdi, {}", *value as u64)); // pass the captured boolean as the mixed low word + ctx.emitter + .instruction(&format!("mov rdi, {}", *value as u64)); // pass the captured boolean as the mixed low word ctx.emitter.instruction("xor rsi, rsi"); // boolean mixed payloads do not use the high word } AttrArgValue::Str(value) => { diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 71942c3eda..f3cca6f828 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -633,7 +633,14 @@ fn reflection_class_metadata_for_name( is_cloneable: false, is_iterable: false, modifiers: 0, - member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false, false), + member_flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), }); } if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { @@ -692,7 +699,14 @@ fn reflection_class_metadata_for_name( is_cloneable: false, is_iterable: false, modifiers: 0, - member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false, false), + member_flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), }); } Ok(empty_reflection_metadata()) @@ -1070,7 +1084,14 @@ fn reflection_class_constant_metadata( is_cloneable: false, is_iterable: false, modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), - member_flags: reflection_member_flags(false, &Visibility::Public, false, false, false, false), + member_flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), }); } Ok( @@ -3369,7 +3390,10 @@ fn emit_reflection_attrs_property( abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, attrs_low_offset); abi::emit_call_label(ctx.emitter, "__rt_decref_array"); super::super::builtins::attributes::emit_reflection_attribute_array( - ctx, attr_names, attr_args, + ctx, + attr_names, + attr_args, + reflection_attribute_target_for_owner(class_name), )?; abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, attrs_low_offset); @@ -3385,6 +3409,29 @@ fn emit_reflection_attrs_property( Ok(()) } +/// Returns PHP's `Attribute::TARGET_*` bitmask for attributes on one Reflection owner type. +fn reflection_attribute_target_for_owner(class_name: &str) -> i64 { + match class_name { + "ReflectionClass" => super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_CLASS, + "ReflectionFunction" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_FUNCTION + } + "ReflectionMethod" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_METHOD + } + "ReflectionProperty" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_PROPERTY + } + "ReflectionParameter" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_PARAMETER + } + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT + } + _ => 0, + } +} + /// Replaces a ReflectionClass private array slot with an indexed string array. fn emit_reflection_string_array_property_by_name( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index cbe80436a3..3f6db36ba1 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -91,12 +91,26 @@ pub(crate) fn inject_builtin_reflection( Some(TypeExpr::Int), int_lit(0), ), + builtin_property( + "__target", + Visibility::Private, + Some(TypeExpr::Int), + int_lit(0), + ), + builtin_property( + "__is_repeated", + Visibility::Private, + Some(TypeExpr::Bool), + false_bool(), + ), ], methods: vec![ builtin_reflection_attribute_constructor_method(), builtin_reflection_attribute_get_name_method(), builtin_reflection_attribute_get_arguments_method(), builtin_reflection_attribute_new_instance_method(), + builtin_reflection_class_int_method("getTarget", "__target"), + builtin_reflection_class_bool_method("isRepeated", "__is_repeated"), ], attributes: Vec::new(), constants: Vec::new(), @@ -1370,7 +1384,12 @@ fn builtin_reflection_class_get_static_property_value_method() -> ClassMethod { has_body: true, params: vec![ ("name".to_string(), Some(TypeExpr::Str), None, false), - ("default".to_string(), Some(mixed_type()), null_expr(), false), + ( + "default".to_string(), + Some(mixed_type()), + null_expr(), + false, + ), ], param_attributes: Vec::new(), variadic: None, @@ -2714,6 +2733,7 @@ fn builtin_reflection_owner_get_attributes_method() -> ClassMethod { /// - `__construct` → `void` /// - `getName` / `getArguments` → `string` / `array` /// - `newInstance` → `mixed` +/// - `getTarget` / `isRepeated` → `int` / `bool` /// - `getAttributes` → `array` pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(class_info) = checker.classes.get_mut("ReflectionAttribute") { @@ -2734,6 +2754,12 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("newInstance")) { sig.return_type = PhpType::Mixed; } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTarget")) { + sig.return_type = PhpType::Int; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("isRepeated")) { + sig.return_type = PhpType::Bool; + } } for class_name in [ "ReflectionClass", @@ -2770,10 +2796,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Str; } } - if let Some(sig) = class_info - .methods - .get_mut(&php_symbol_key("getDocComment")) - { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getDocComment")) { sig.return_type = PhpType::Union(vec![PhpType::Str, PhpType::Bool]); } if let Some(sig) = class_info diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 07d2d81ca5..ba3e10c706 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6502,6 +6502,52 @@ echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance ); } +/// Verifies eval ReflectionAttribute exposes owner target and repetition metadata. +#[test] +fn test_eval_reflection_attribute_target_and_repetition() { + let out = compile_and_run_capture( + r#"getAttributes(); +echo $classAttrs[0]->getTarget() . "/" . ($classAttrs[0]->isRepeated() ? "R" : "r") . ":"; +echo $classAttrs[1]->getTarget() . "/" . ($classAttrs[1]->isRepeated() ? "R" : "r") . ":"; +$methodAttr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getAttributes()[0]; +echo $methodAttr->getTarget() . "/" . ($methodAttr->isRepeated() ? "R" : "r") . ":"; +$propertyAttr = (new ReflectionProperty("EvalReflectAttributeTarget", "id"))->getAttributes()[0]; +echo $propertyAttr->getTarget() . "/" . ($propertyAttr->isRepeated() ? "R" : "r") . ":"; +$paramAttr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getParameters()[0]->getAttributes()[0]; +echo $paramAttr->getTarget() . "/" . ($paramAttr->isRepeated() ? "R" : "r") . ":"; +$constAttr = (new ReflectionClassConstant("EvalReflectAttributeTarget", "ANSWER"))->getAttributes()[0]; +echo $constAttr->getTarget() . "/" . ($constAttr->isRepeated() ? "R" : "r") . ":"; +$caseAttr = (new ReflectionEnumUnitCase("EvalReflectAttributeEnum", "Ready"))->getAttributes()[0]; +echo $caseAttr->getTarget() . "/" . ($caseAttr->isRepeated() ? "R" : "r") . ":"; +echo method_exists($classAttrs[0], "getTarget") ? "Y" : "n"; +echo method_exists($classAttrs[0], "isRepeated") ? "Y" : "n";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1/R:1/R:4/r:8/r:32/r:16/r:16/r:YY"); +} + /// Verifies eval ReflectionClass exposes namespace-derived class-name parts. #[test] fn test_eval_reflection_class_name_parts() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 1947af7cee..4070a1372d 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -722,6 +722,46 @@ foreach ($attrs as $attr) { assert_eq!(out, "count=2\nAuthor:[Ada][1815]\nVersion:[1.0][1]\n"); } +/// Verifies that `ReflectionAttribute::getTarget()` and `isRepeated()` report +/// PHP-compatible owner target bits and duplicate-owner metadata. +#[test] +fn test_reflection_attribute_target_and_repetition_metadata() { + let out = compile_and_run( + r#"getAttributes(); +echo $classAttrs[0]->getTarget() . "/" . ($classAttrs[0]->isRepeated() ? "R" : "r") . ":"; +echo $classAttrs[1]->getTarget() . "/" . ($classAttrs[1]->isRepeated() ? "R" : "r") . ":"; +$methodAttr = (new ReflectionMethod(ReflectAttributeTarget::class, "run"))->getAttributes()[0]; +echo $methodAttr->getTarget() . "/" . ($methodAttr->isRepeated() ? "R" : "r") . ":"; +$propertyAttr = (new ReflectionProperty(ReflectAttributeTarget::class, "id"))->getAttributes()[0]; +echo $propertyAttr->getTarget() . "/" . ($propertyAttr->isRepeated() ? "R" : "r") . ":"; +$paramAttr = (new ReflectionMethod(ReflectAttributeTarget::class, "run"))->getParameters()[0]->getAttributes()[0]; +echo $paramAttr->getTarget() . "/" . ($paramAttr->isRepeated() ? "R" : "r") . ":"; +$constAttr = (new ReflectionClassConstant(ReflectAttributeTarget::class, "ANSWER"))->getAttributes()[0]; +echo $constAttr->getTarget() . "/" . ($constAttr->isRepeated() ? "R" : "r") . ":"; +$caseAttr = (new ReflectionEnumUnitCase(ReflectAttributeEnum::class, "Ready"))->getAttributes()[0]; +echo $caseAttr->getTarget() . "/" . ($caseAttr->isRepeated() ? "R" : "r"); +"#, + ); + assert_eq!(out, "1/R:1/R:4/r:8/r:32/r:16/r:16/r"); +} + /// Verifies that `class_get_attributes()` returns an empty array for a /// class with no attributes. #[test] From 16e1dce993bb4090cb3a6f51cf91801961b6a912 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 23:15:05 +0200 Subject: [PATCH 0496/1208] Support ReflectionParameter nullability metadata --- .../elephc-eval/src/interpreter/reflection.rs | 75 ++++++++++++----- .../tests/builtins_class_metadata.rs | 3 +- .../interpreter/tests/support/object_ops.rs | 4 + docs/php/eval.md | 5 +- src/codegen/eval_reflection_owner_helpers.rs | 23 +++++- src/codegen/lower_inst/objects/reflection.rs | 82 +++++++++++++------ src/types/checker/builtin_types/reflection.rs | 9 ++ tests/codegen/eval.rs | 3 +- tests/codegen/oop/attributes.rs | 3 +- 9 files changed, 155 insertions(+), 52 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index cec315881a..3e82899bed 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -46,6 +46,7 @@ const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; +const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; const EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL: u64 = 1; const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; @@ -92,6 +93,7 @@ struct EvalReflectionParameterMetadata { is_passed_by_reference: bool, is_promoted: bool, has_type: bool, + allows_null: bool, type_metadata: Option, default_value: Option, } @@ -4328,29 +4330,39 @@ fn eval_reflection_parameters_from_names_and_type_flags( names .iter() .enumerate() - .map(|(position, name)| EvalReflectionParameterMetadata { - name: name.clone(), - declaring_class_name: declaring_class_name.map(str::to_string), - declaring_function: declaring_function.cloned(), - attributes: parameter_attributes - .get(position) - .cloned() - .unwrap_or_default(), - position, - is_optional: defaults.get(position).is_some_and(Option::is_some) - || variadic_flags.get(position).copied().unwrap_or(false), - is_variadic: variadic_flags.get(position).copied().unwrap_or(false), - is_passed_by_reference: by_ref_flags.get(position).copied().unwrap_or(false), - is_promoted: promoted_parameter_names - .iter() - .any(|promoted_name| promoted_name == name), - has_type: has_type_flags.get(position).copied().unwrap_or(false), - type_metadata: parameter_types + .map(|(position, name)| { + let has_type = has_type_flags.get(position).copied().unwrap_or(false); + let default_value = defaults.get(position).and_then(Clone::clone); + let type_metadata = parameter_types .get(position) .and_then(Option::as_ref) .and_then(eval_reflection_parameter_type_metadata) - .filter(|_| has_type_flags.get(position).copied().unwrap_or(false)), - default_value: defaults.get(position).and_then(Clone::clone), + .filter(|_| has_type); + EvalReflectionParameterMetadata { + name: name.clone(), + declaring_class_name: declaring_class_name.map(str::to_string), + declaring_function: declaring_function.cloned(), + attributes: parameter_attributes + .get(position) + .cloned() + .unwrap_or_default(), + position, + is_optional: defaults.get(position).is_some_and(Option::is_some) + || variadic_flags.get(position).copied().unwrap_or(false), + is_variadic: variadic_flags.get(position).copied().unwrap_or(false), + is_passed_by_reference: by_ref_flags.get(position).copied().unwrap_or(false), + is_promoted: promoted_parameter_names + .iter() + .any(|promoted_name| promoted_name == name), + has_type, + allows_null: eval_reflection_parameter_allows_null( + has_type, + type_metadata.as_ref(), + default_value.as_ref(), + ), + type_metadata, + default_value, + } }) .collect() } @@ -4472,6 +4484,26 @@ fn eval_reflection_parameter_type_metadata( }) } +/// Returns PHP's `ReflectionParameter::allowsNull()` value for retained metadata. +fn eval_reflection_parameter_allows_null( + has_type: bool, + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, +) -> bool { + !has_type + || default_value.is_some_and(|default| matches!(default, EvalExpr::Const(EvalConst::Null))) + || type_metadata.is_some_and(eval_reflection_type_allows_null) +} + +/// Returns whether one retained ReflectionType metadata value accepts null. +fn eval_reflection_type_allows_null(type_metadata: &EvalReflectionParameterTypeMetadata) -> bool { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named_type) => named_type.allows_null, + EvalReflectionParameterTypeKind::Union(union_type) => union_type.allows_null, + EvalReflectionParameterTypeKind::Intersection(_) => false, + } +} + /// Converts one eval parameter type variant into `ReflectionNamedType` metadata. fn eval_reflection_named_type_variant_metadata( variant: &EvalParameterTypeVariant, @@ -4784,6 +4816,9 @@ fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) if parameter.default_value.is_some() { flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE; } + if parameter.allows_null { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL; + } flags } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 8f7cd2f65d..43825d6184 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1500,6 +1500,7 @@ foreach ($params as $param) { echo $param->isVariadic() ? "V" : "v"; echo $param->isPassedByReference() ? "R" : "b"; echo $param->hasType() ? "T" : "t"; + echo $param->allowsNull() ? "N" : "n"; $type = $param->getType(); if ($param->getName() == "union") { echo ":union"; @@ -1545,7 +1546,7 @@ return true;"##, assert_eq!( values.output, - "5/3:first#0rvRT:int!B:A1:EvalParamTag:first:d|union#1rvbT:union!:intB:stringB:A0:d|both#2rvbT:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second#3OvbT:App\\Name?C:A0:D=null|rest#4OVRt:null:A0:d|" + "5/3:first#0rvRTn:int!B:A1:EvalParamTag:first:d|union#1rvbTn:union!:intB:stringB:A0:d|both#2rvbTn:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second#3OvbTN:App\\Name?C:A0:D=null|rest#4OVRtN:null:A0:d|" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index f2c56352f7..9117cc7e8e 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -25,6 +25,7 @@ const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; +const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; impl FakeOps { /// Reads one fake object property by name. @@ -720,6 +721,8 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE) != 0)?; let is_promoted = self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED) != 0)?; + let allows_null = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL) != 0)?; properties.push(("__position".to_string(), position)); properties.push(("__is_optional".to_string(), is_optional)); properties.push(("__is_variadic".to_string(), is_variadic)); @@ -729,6 +732,7 @@ impl FakeOps { )); properties.push(("__is_promoted".to_string(), is_promoted)); properties.push(("__has_type".to_string(), has_type)); + properties.push(("__allows_null".to_string(), allows_null)); properties.push(("__type".to_string(), method_objects)); properties.push(("__has_default_value".to_string(), has_default_value)); properties.push(("__default_value".to_string(), property_objects)); diff --git a/docs/php/eval.md b/docs/php/eval.md index 243946c9a7..cdd5c88c20 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -323,8 +323,9 @@ named type metadata through metadata is exposed through `ReflectionIntersectionType::getTypes()` and `allowsNull()`. Function, method, and parameter attributes are exposed through `getAttributes()` using materialized `ReflectionAttribute` objects. Parameter -default values, optionality, variadic flags, and by-reference flags are retained -for eval-declared functions and methods. `ReflectionParameter::getDeclaringClass()` +default values, optionality, nullability, variadic flags, and by-reference +flags are retained for eval-declared functions and methods, including +`ReflectionParameter::allowsNull()`. `ReflectionParameter::getDeclaringClass()` returns the declaring class-like symbol for eval method parameters, and `ReflectionParameter::getDeclaringFunction()` returns a `ReflectionFunction` object for eval free-function parameters or a `ReflectionMethod` object for the diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 3ac1a441d4..e8fd675abe 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -740,10 +740,10 @@ fn emit_aarch64_owner_kind_body( emit_set_owner_constant_value_property_aarch64(emitter, layout, fail_label); emit_set_owner_backing_value_property_aarch64(emitter, layout, fail_label); emit_set_owner_required_parameter_count_property_aarch64(emitter, layout); + emit_set_owner_named_type_flags_property_aarch64(emitter, layout); emit_set_owner_parameter_property_aarch64(emitter, layout); emit_set_owner_parameter_type_property_aarch64(emitter, layout, fail_label); emit_set_owner_parameter_default_property_aarch64(emitter, layout, fail_label); - emit_set_owner_named_type_flags_property_aarch64(emitter, layout); emit_set_owner_metadata_arrays_property_aarch64(emitter, layout, fail_label); emit_set_owner_constructor_property_aarch64(emitter, layout, fail_label); emit_set_owner_parent_class_property_aarch64(emitter, layout, fail_label); @@ -772,10 +772,10 @@ fn emit_x86_64_owner_kind_body( emit_set_owner_constant_value_property_x86_64(emitter, layout, fail_label); emit_set_owner_backing_value_property_x86_64(emitter, layout, fail_label); emit_set_owner_required_parameter_count_property_x86_64(emitter, layout); + emit_set_owner_named_type_flags_property_x86_64(emitter, layout); emit_set_owner_parameter_property_x86_64(emitter, layout); emit_set_owner_parameter_type_property_x86_64(emitter, layout, fail_label); emit_set_owner_parameter_default_property_x86_64(emitter, layout, fail_label); - emit_set_owner_named_type_flags_property_x86_64(emitter, layout); emit_set_owner_metadata_arrays_property_x86_64(emitter, layout, fail_label); emit_set_owner_constructor_property_x86_64(emitter, layout, fail_label); emit_set_owner_parent_class_property_x86_64(emitter, layout, fail_label); @@ -814,7 +814,7 @@ fn emit_alloc_reflection_owner_object_x86_64( emitter.instruction(&format!( "mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4 - )); // materialize the x86_64 object heap kind word + )); // materialize the x86_64 object heap kind word emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero @@ -1524,6 +1524,8 @@ fn emit_set_owner_parameter_property_aarch64( Some(has_default_value_hi), Some(is_promoted_lo), Some(is_promoted_hi), + Some(allows_null_lo), + Some(allows_null_hi), ) = ( layout.position_lo, layout.position_hi, @@ -1539,6 +1541,8 @@ fn emit_set_owner_parameter_property_aarch64( layout.has_default_value_hi, layout.is_promoted_lo, layout.is_promoted_hi, + layout.allows_null_lo, + layout.allows_null_hi, ) else { return; @@ -1571,6 +1575,10 @@ fn emit_set_owner_parameter_property_aarch64( emitter.instruction("and x10, x10, #1"); // extract the promoted-parameter flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_promoted_lo); abi::emit_store_zero_to_address(emitter, "x9", is_promoted_hi); + emitter.instruction("lsr x10, x11, #6"); // move the nullable-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the nullable-parameter flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", allows_null_lo); + abi::emit_store_zero_to_address(emitter, "x9", allows_null_hi); } /// Stores incoming ARM64 ReflectionParameter type metadata. @@ -1655,6 +1663,8 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl Some(has_default_value_hi), Some(is_promoted_lo), Some(is_promoted_hi), + Some(allows_null_lo), + Some(allows_null_hi), ) = ( layout.position_lo, layout.position_hi, @@ -1670,6 +1680,8 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl layout.has_default_value_hi, layout.is_promoted_lo, layout.is_promoted_hi, + layout.allows_null_lo, + layout.allows_null_hi, ) else { return; @@ -1708,6 +1720,11 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl emitter.instruction("and rax, 1"); // extract the promoted-parameter flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_promoted_lo); abi::emit_store_zero_to_address(emitter, "r10", is_promoted_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the nullable bit + emitter.instruction("shr rax, 6"); // move the nullable-parameter bit into position + emitter.instruction("and rax, 1"); // extract the nullable-parameter flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", allows_null_lo); + abi::emit_store_zero_to_address(emitter, "r10", allows_null_hi); } /// Stores incoming x86_64 ReflectionParameter type metadata. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index f3cca6f828..3f3bfeddd5 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -109,6 +109,7 @@ struct ReflectionParameterMember { is_passed_by_reference: bool, is_promoted: bool, has_type: bool, + allows_null: bool, type_metadata: Option, default_value: Option, } @@ -2581,6 +2582,16 @@ fn reflection_parameter_members_with_declaring_function( .map(|(index, (name, ty))| { let is_variadic = sig.variadic.as_deref() == Some(name.as_str()); let has_type = sig.declared_params.get(index).copied().unwrap_or(false); + let type_metadata = reflection_parameter_type_metadata( + sig.param_type_exprs.get(index).and_then(Option::as_ref), + ty, + ) + .filter(|_| has_type); + let default_value = sig + .defaults + .get(index) + .and_then(Option::as_ref) + .and_then(reflection_parameter_default_value); ReflectionParameterMember { name: name.clone(), declaring_class_name: declaring_class_name.map(str::to_string), @@ -2608,21 +2619,38 @@ fn reflection_parameter_members_with_declaring_function( .iter() .any(|promoted_name| promoted_name == name), has_type, - type_metadata: reflection_parameter_type_metadata( - sig.param_type_exprs.get(index).and_then(Option::as_ref), - ty, - ) - .filter(|_| has_type), - default_value: sig - .defaults - .get(index) - .and_then(Option::as_ref) - .and_then(reflection_parameter_default_value), + allows_null: reflection_parameter_allows_null( + has_type, + type_metadata.as_ref(), + default_value.as_ref(), + ), + type_metadata, + default_value, } }) .collect() } +/// Returns PHP's `ReflectionParameter::allowsNull()` value for static metadata. +fn reflection_parameter_allows_null( + has_type: bool, + type_metadata: Option<&ReflectionParameterTypeMetadata>, + default_value: Option<&ReflectionParameterDefaultValue>, +) -> bool { + !has_type + || matches!(default_value, Some(ReflectionParameterDefaultValue::Null)) + || type_metadata.is_some_and(reflection_type_allows_null) +} + +/// Returns whether one retained ReflectionType metadata value accepts null. +fn reflection_type_allows_null(type_metadata: &ReflectionParameterTypeMetadata) -> bool { + match type_metadata { + ReflectionParameterTypeMetadata::Named(named_type) => named_type.allows_null, + ReflectionParameterTypeMetadata::Union(union_type) => union_type.allows_null, + ReflectionParameterTypeMetadata::Intersection(_) => false, + } +} + /// Converts a supported parameter default expression into Reflection metadata. fn reflection_parameter_default_value(default: &Expr) -> Option { match &default.kind { @@ -3835,8 +3863,8 @@ fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection constant value as the hash payload - ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word abi::emit_pop_reg(ctx.emitter, "x0"); abi::emit_symbol_address(ctx.emitter, "x1", &key_label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); @@ -3848,8 +3876,8 @@ fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str abi::emit_call_label(ctx.emitter, "__rt_hash_set"); } Arch::X86_64 => { - ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection constant value as the hash payload - ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word abi::emit_pop_reg(ctx.emitter, "rdi"); abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); @@ -4052,6 +4080,12 @@ fn emit_reflection_parameter_properties( "__has_type", parameter.has_type, )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__allows_null", + parameter.allows_null, + )?; emit_reflection_parameter_type_property(ctx, parameter)?; emit_reflection_owner_bool_property( ctx, @@ -4520,32 +4554,32 @@ fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) /// Appends ReflectionClass metadata names to the current ARM64 result array. fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result } /// Appends ReflectionClass metadata names to the current x86_64 result array. fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings - ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result } /// Stores ReflectionMethod/ReflectionProperty boolean predicate slots when supported. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 3f6db36ba1..06185c54ec 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -614,6 +614,12 @@ fn builtin_reflection_parameter() -> FlattenedClass { bool_lit(false), ), builtin_property("__has_type", Visibility::Private, Some(TypeExpr::Bool), bool_lit(false)), + builtin_property( + "__allows_null", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(true), + ), builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), builtin_property( "__has_default_value", @@ -656,6 +662,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { ), builtin_reflection_slot_getter("isPromoted", "__is_promoted", TypeExpr::Bool), builtin_reflection_slot_getter("hasType", "__has_type", TypeExpr::Bool), + builtin_reflection_slot_getter("allowsNull", "__allows_null", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), builtin_reflection_owner_get_attributes_method(), builtin_reflection_slot_getter( @@ -2987,7 +2994,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isoptional", "isvariadic", "ispassedbyreference", + "ispromoted", "hastype", + "allowsnull", "isdefaultvalueavailable", ] { if let Some(sig) = class_info.methods.get_mut(method_name) { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ba3e10c706..a4209c9349 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8305,6 +8305,7 @@ foreach ($params as $param) { echo $param->isVariadic() ? "V" : "v"; echo $param->isPassedByReference() ? "R" : "b"; echo $param->hasType() ? "T" : "t"; + echo $param->allowsNull() ? "N" : "n"; $type = $param->getType(); if ($param->getName() == "union") { echo ":union"; @@ -8349,7 +8350,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "5/3:first@0rvbT:int!B:A1:EvalParamTag:first:d|union@1rvbT:union!:intB:stringB:A0:d|both@2rvbT:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second@3OvbT:App\\Name?C:A0:D=null|rest@4OVbt:null:A0:d|" + "5/3:first@0rvbTn:int!B:A1:EvalParamTag:first:d|union@1rvbTn:union!:intB:stringB:A0:d|both@2rvbTn:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second@3OvbTN:App\\Name?C:A0:D=null|rest@4OVbtN:null:A0:d|" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 4070a1372d..317799cb6c 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2380,6 +2380,7 @@ $params = (new ReflectionMethod(ReflectParamTypeTarget::class, "run"))->getParam foreach ($params as $param) { echo $param->getName() . ":"; echo $param->hasType() ? "T:" : "t:"; + echo $param->allowsNull() ? "N:" : "n:"; $type = $param->getType(); if ($type instanceof ReflectionNamedType) { echo $type->getName(); @@ -2418,7 +2419,7 @@ if ($directType instanceof ReflectionNamedType) { ); assert_eq!( out.stdout, - "id:T:int!B|name:T:string?B|dep:T:ReflectParamTypeDep!C|plain:t:null|union:T:union!:intB:stringB|nullableUnion:T:union?:intB:stringB|intersection:T:intersection!:ReflectParamTypeAC:ReflectParamTypeBC|direct:ReflectParamTypeDep" + "id:T:n:int!B|name:T:N:string?B|dep:T:n:ReflectParamTypeDep!C|plain:t:N:null|union:T:n:union!:intB:stringB|nullableUnion:T:N:union?:intB:stringB|intersection:T:n:intersection!:ReflectParamTypeAC:ReflectParamTypeBC|direct:ReflectParamTypeDep" ); } From 5923b56337ba34e90347a1217dee41e861368379 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 21 Jun 2026 23:20:28 +0200 Subject: [PATCH 0497/1208] Support ReflectionParameter pass-by-value metadata --- .../tests/builtins_class_metadata.rs | 3 +- .../interpreter/tests/support/object_ops.rs | 7 ++++ docs/php/eval.md | 3 +- src/types/checker/builtin_types/reflection.rs | 33 +++++++++++++++++++ tests/codegen/eval.rs | 5 +-- tests/codegen/oop/attributes.rs | 3 +- 6 files changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 43825d6184..c7a45fba5c 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1499,6 +1499,7 @@ foreach ($params as $param) { echo $param->isOptional() ? "O" : "r"; echo $param->isVariadic() ? "V" : "v"; echo $param->isPassedByReference() ? "R" : "b"; + echo $param->canBePassedByValue() ? "Y" : "N"; echo $param->hasType() ? "T" : "t"; echo $param->allowsNull() ? "N" : "n"; $type = $param->getType(); @@ -1546,7 +1547,7 @@ return true;"##, assert_eq!( values.output, - "5/3:first#0rvRTn:int!B:A1:EvalParamTag:first:d|union#1rvbTn:union!:intB:stringB:A0:d|both#2rvbTn:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second#3OvbTN:App\\Name?C:A0:D=null|rest#4OVRtN:null:A0:d|" + "5/3:first#0rvRNTn:int!B:A1:EvalParamTag:first:d|union#1rvbYTn:union!:intB:stringB:A0:d|both#2rvbYTn:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second#3OvbYTN:App\\Name?C:A0:D=null|rest#4OVRNtN:null:A0:d|" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 9117cc7e8e..1c44c77218 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -400,6 +400,13 @@ impl FakeOps { Self::object_property(&properties, "__is_passed_by_reference") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "canbepassedbyvalue") if args.is_empty() => { + let Some(by_ref) = Self::object_property(&properties, "__is_passed_by_reference") + else { + return self.bool_value(true); + }; + self.bool_value(!matches!(self.get(by_ref), FakeValue::Bool(true))) + } (FakeValue::Object(properties), "hastype") if args.is_empty() => { if let Some(has_type) = Self::object_property(&properties, "__has_type") { return Ok(has_type); diff --git a/docs/php/eval.md b/docs/php/eval.md index cdd5c88c20..cdfb533672 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -351,7 +351,8 @@ Constructor-promoted eval parameters are reported through direct variable, array-element, and object-property arguments, write back fixed parameters after method execution, write back mutated `&...$items` elements when the variadic container itself is not rebound, and are reported through -`ReflectionParameter::isPassedByReference()`. +`ReflectionParameter::isPassedByReference()` and +`ReflectionParameter::canBePassedByValue()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isDefault()`, and `getModifiers()` report eval property metadata with PHP-compatible diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 06185c54ec..d038171656 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -660,6 +660,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { "__is_passed_by_reference", TypeExpr::Bool, ), + builtin_reflection_parameter_can_be_passed_by_value_method(), builtin_reflection_slot_getter("isPromoted", "__is_promoted", TypeExpr::Bool), builtin_reflection_slot_getter("hasType", "__has_type", TypeExpr::Bool), builtin_reflection_slot_getter("allowsNull", "__allows_null", TypeExpr::Bool), @@ -764,6 +765,37 @@ fn builtin_reflection_class_new_instance_without_constructor_method() -> ClassMe } } +/// Builds `ReflectionParameter::canBePassedByValue()` from the retained by-ref flag. +fn builtin_reflection_parameter_can_be_passed_by_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "canBePassedByValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__is_passed_by_reference", + dummy_span, + ))), + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds the `ReflectionNamedType` shell: a parameter/return type rendered as a /// runtime object with a name, nullability flag, and builtin flag. Populated at /// codegen from the declared type. @@ -2994,6 +3026,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isoptional", "isvariadic", "ispassedbyreference", + "canbepassedbyvalue", "ispromoted", "hastype", "allowsnull", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a4209c9349..676e38bf13 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8293,7 +8293,7 @@ fn test_eval_reflection_method_lists_parameters() { eval('interface EvalReflectLeft {} interface EvalReflectRight {} class EvalReflectParamTarget { - public function run(#[EvalParamTag("first")] int $first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, ...$rest) {} + public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, &...$rest) {} } $method = new ReflectionMethod("EvalReflectParamTarget", "run"); echo $method->getNumberOfParameters() . "/"; @@ -8304,6 +8304,7 @@ foreach ($params as $param) { echo $param->isOptional() ? "O" : "r"; echo $param->isVariadic() ? "V" : "v"; echo $param->isPassedByReference() ? "R" : "b"; + echo $param->canBePassedByValue() ? "Y" : "N"; echo $param->hasType() ? "T" : "t"; echo $param->allowsNull() ? "N" : "n"; $type = $param->getType(); @@ -8350,7 +8351,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "5/3:first@0rvbTn:int!B:A1:EvalParamTag:first:d|union@1rvbTn:union!:intB:stringB:A0:d|both@2rvbTn:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second@3OvbTN:App\\Name?C:A0:D=null|rest@4OVbtN:null:A0:d|" + "5/3:first@0rvRNTn:int!B:A1:EvalParamTag:first:d|union@1rvbYTn:union!:intB:stringB:A0:d|both@2rvbYTn:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second@3OvbYTN:App\\Name?C:A0:D=null|rest@4OVRNtN:null:A0:d|" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 317799cb6c..8ebe7ebafe 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2298,6 +2298,7 @@ foreach ($params as $param) { echo ($param->hasType() ? "T" : "t"); echo ($param->isOptional() ? "O" : "R"); echo ($param->isPassedByReference() ? "B" : "b"); + echo ($param->canBePassedByValue() ? "P" : "p"); echo ($param->isVariadic() ? "V" : "v"); echo "|"; } @@ -2326,7 +2327,7 @@ echo ($traitParams[2]->hasType() ? "T" : "t"); ); assert_eq!( out.stdout, - "4/2:id#0TRbv|name#1tRBv|mode#2TObv|rest#3tObV|\n2/1:idT:nameO\n3/1:SR:restVT" + "4/2:id#0TRbPv|name#1tRBpv|mode#2TObPv|rest#3tObPV|\n2/1:idT:nameO\n3/1:SR:restVT" ); } From bf8c62287ef358d723bedc04b1f8f6b02acfacba Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 00:04:06 +0200 Subject: [PATCH 0498/1208] Support ReflectionParameter constant default metadata --- .../elephc-eval/src/interpreter/reflection.rs | 54 +- .../tests/builtins_class_metadata.rs | 44 + .../interpreter/tests/support/object_ops.rs | 16 + docs/php/eval.md | 17 +- src/codegen/eval_reflection_owner_helpers.rs | 1137 +++++++++-------- src/codegen/lower_inst/objects/reflection.rs | 399 ++++-- src/types/checker/builtin_types/reflection.rs | 119 +- tests/codegen/eval.rs | 42 + tests/codegen/oop/attributes.rs | 45 + 9 files changed, 1212 insertions(+), 661 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 3e82899bed..2eccbd4442 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -47,6 +47,7 @@ const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; +const EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT: u64 = 128; const EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL: u64 = 1; const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; @@ -96,6 +97,7 @@ struct EvalReflectionParameterMetadata { allows_null: bool, type_metadata: Option, default_value: Option, + default_value_constant_name: Option, } /// Eval metadata needed for `ReflectionParameter::getDeclaringFunction()`. @@ -2489,11 +2491,11 @@ fn eval_reflection_parameter_object_result( Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, None => values.null()?, }; - let default_value = match parameter.default_value.as_ref() { - Some(default) => eval_method_parameter_default(default, context, values)?, + let default_value = eval_reflection_parameter_default_value(parameter, context, values)?; + let default_value_constant_name = match parameter.default_value_constant_name.as_deref() { + Some(name) => values.string(name)?, None => values.null()?, }; - let constant_value = values.null()?; let backing_value = values.null()?; let flags = eval_reflection_parameter_flags(parameter); let object = values.reflection_owner_new( @@ -2510,9 +2512,9 @@ fn eval_reflection_parameter_object_result( flags, parameter.position as u64, 0, - constant_value, + default_value_constant_name, + backing_value, backing_value, - constant_value, )?; values.release(attrs)?; values.release(declaring_function)?; @@ -2523,11 +2525,31 @@ fn eval_reflection_parameter_object_result( values.release(type_value)?; values.release(default_value)?; values.release(parent_class)?; - values.release(constant_value)?; + values.release(default_value_constant_name)?; values.release(backing_value)?; Ok(object) } +/// Materializes one ReflectionParameter default using the declaring class scope when present. +fn eval_reflection_parameter_default_value( + parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(default) = parameter.default_value.as_ref() else { + return values.null(); + }; + let Some(class_name) = parameter.declaring_class_name.as_deref() else { + return eval_method_parameter_default(default, context, values); + }; + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(class_name.to_string()); + let result = eval_method_parameter_default(default, context, values); + context.pop_called_class_scope(); + context.pop_class_scope(); + result +} + /// Builds a shallow ReflectionMethod object for a parameter's declaring function metadata. fn eval_reflection_declaring_function_object_result( metadata: &EvalReflectionDeclaringFunctionMetadata, @@ -4333,6 +4355,9 @@ fn eval_reflection_parameters_from_names_and_type_flags( .map(|(position, name)| { let has_type = has_type_flags.get(position).copied().unwrap_or(false); let default_value = defaults.get(position).and_then(Clone::clone); + let default_value_constant_name = default_value + .as_ref() + .and_then(eval_reflection_default_constant_name); let type_metadata = parameter_types .get(position) .and_then(Option::as_ref) @@ -4362,11 +4387,25 @@ fn eval_reflection_parameters_from_names_and_type_flags( ), type_metadata, default_value, + default_value_constant_name, } }) .collect() } +/// Returns PHP's ReflectionParameter default-constant name for retained eval defaults. +fn eval_reflection_default_constant_name(default: &EvalExpr) -> Option { + match default { + EvalExpr::ConstFetch(name) => Some(name.clone()), + EvalExpr::NamespacedConstFetch { name, .. } => Some(name.clone()), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => Some(format!("{}::{}", class_name, constant)), + _ => None, + } +} + /// Builds ReflectionParameter metadata for eval-declared or native free functions. fn eval_reflection_function_parameters( function_name: &str, @@ -4819,6 +4858,9 @@ fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) if parameter.allows_null { flags |= EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL; } + if parameter.default_value_constant_name.is_some() { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT; + } flags } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c7a45fba5c..5040966e2b 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1677,6 +1677,7 @@ fn execute_program_reflection_property_get_default_value_metadata() { public int $count = 7; public static string $label = "ok"; } + foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label"] as $name) { $property = new ReflectionProperty("EvalReflectPropertyDefaultTarget", $name); echo $property->getName(); echo ":"; @@ -1706,6 +1707,49 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionParameter reports eval constant-default metadata. +#[test] +fn execute_program_reflection_parameter_reports_default_constant_metadata() { + let program = parse_fragment( + br##"define("EVAL_REFLECT_PARAM_DEFAULT_GLOBAL", "G"); +class EvalReflectParamDefaultBase { + const BASE = "B"; +} +class EvalReflectParamDefaultTarget extends EvalReflectParamDefaultBase { + const LABEL = "L"; + public function run($required, $global = EVAL_REFLECT_PARAM_DEFAULT_GLOBAL, $self = self::LABEL, $parent = parent::BASE, $literal = 7) {} +} +$params = (new ReflectionMethod("EvalReflectParamDefaultTarget", "run"))->getParameters(); +foreach ($params as $param) { + echo $param->getName(); echo ":"; + echo $param->isDefaultValueAvailable() ? "D:" : "d:"; + if ($param->isDefaultValueAvailable()) { + if ($param->isDefaultValueConstant()) { + echo "C:"; + echo $param->getDefaultValueConstantName(); + echo ":"; + } else { + echo "c:null:"; + } + echo $param->getDefaultValue(); + } + echo "|"; +} +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "required:d:|global:D:C:EVAL_REFLECT_PARAM_DEFAULT_GLOBAL:G|self:D:C:self::LABEL:L|parent:D:C:parent::BASE:B|literal:D:c:null:7|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval property default metadata as an associative map. #[test] fn execute_program_reflection_class_get_default_properties_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 1c44c77218..b35facc0a5 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -26,6 +26,7 @@ const EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE: u64 = 8; const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; +const EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT: u64 = 128; impl FakeOps { /// Reads one fake object property by name. @@ -370,6 +371,10 @@ impl FakeOps { Self::object_property(&properties, "__default_value") .map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "getdefaultvalueconstantname") if args.is_empty() => { + Self::object_property(&properties, "__default_value_constant_name") + .map_or_else(|| self.null(), Ok) + } (FakeValue::Object(properties), "isconstructor") if args.is_empty() => { let Some(name) = Self::object_property(&properties, "__name") else { return self.bool_value(false); @@ -422,6 +427,10 @@ impl FakeOps { Self::object_property(&properties, "__has_default_value") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isdefaultvalueconstant") if args.is_empty() => { + Self::object_property(&properties, "__is_default_value_constant") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "isdefault") if args.is_empty() => { self.bool_value(Self::object_property(&properties, "__has_default_value").is_some()) } @@ -730,6 +739,8 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED) != 0)?; let allows_null = self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL) != 0)?; + let is_default_value_constant = self + .bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT) != 0)?; properties.push(("__position".to_string(), position)); properties.push(("__is_optional".to_string(), is_optional)); properties.push(("__is_variadic".to_string(), is_variadic)); @@ -742,6 +753,11 @@ impl FakeOps { properties.push(("__allows_null".to_string(), allows_null)); properties.push(("__type".to_string(), method_objects)); properties.push(("__has_default_value".to_string(), has_default_value)); + properties.push(( + "__is_default_value_constant".to_string(), + is_default_value_constant, + )); + properties.push(("__default_value_constant_name".to_string(), constant_value)); properties.push(("__default_value".to_string(), property_objects)); properties.push(("__declaring_function".to_string(), interface_names)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index cdfb533672..54e2f6f1eb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -331,10 +331,14 @@ returns the declaring class-like symbol for eval method parameters, and object for eval free-function parameters or a `ReflectionMethod` object for the declaring eval method. `ReflectionFunction::invoke()` and `invokeArgs()` dispatch eval-declared functions with the same named/default/variadic argument -binding used by direct eval function calls. Defaulted eval method parameters are bound -when omitted and reported through -`ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, and -`getDefaultValue()`. Supported default expressions include +binding used by direct eval function calls. Defaulted eval method parameters are +bound when omitted and reported through `ReflectionParameter::isOptional()`, +`isDefaultValueAvailable()`, `isDefaultValueConstant()`, +`getDefaultValueConstantName()`, and `getDefaultValue()`. Constant-name metadata +is retained for predefined or eval-defined constant fetches, namespaced constant +fallback, and class/interface/trait/enum constant fetches; `::class` literals, +magic constants, and literal defaults are materialized as values but are not +reported as default-value constants. Supported default expressions include scalar literals, arrays whose keys and values are supported default expressions, magic constants, unary and binary operators supported by eval, ternary and null-coalescing expressions, predefined or eval-defined constant @@ -559,8 +563,9 @@ remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader -parameter and generated property default-value objects beyond the eval-supported -constant-expression subset, and broader generated/AOT method bridge signatures beyond the current public +parameter and generated property default-value materialization beyond the +eval-supported constant-expression subset, object-valued defaults in generated +metadata, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice. Generated/AOT method type metadata and generated/AOT method/property attributes are exposed for registered metadata slices, while broader non-public or unsupported bridge shapes remain diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index e8fd675abe..664ba31671 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -106,10 +106,14 @@ struct ReflectionOwnerLayout { parameter_type_hi: Option, has_default_value_lo: Option, has_default_value_hi: Option, + is_default_value_constant_lo: Option, + is_default_value_constant_hi: Option, is_promoted_lo: Option, is_promoted_hi: Option, default_value_lo: Option, default_value_hi: Option, + default_value_constant_name_lo: Option, + default_value_constant_name_hi: Option, declaring_function_lo: Option, declaring_function_hi: Option, allows_null_lo: Option, @@ -259,8 +263,12 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option Option fn emit_reflection_owner_new_stub(emitter: &mut Emitter) { match emitter.target.arch { Arch::AArch64 => { - emitter.instruction("mov x0, xzr"); // report helper failure when Reflection owner metadata is missing - emitter.instruction("ret"); // return the null pointer to Rust + emitter.instruction("mov x0, xzr"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust } Arch::X86_64 => { - emitter.instruction("xor eax, eax"); // report helper failure when Reflection owner metadata is missing - emitter.instruction("ret"); // return the null pointer to Rust + emitter.instruction("xor eax, eax"); // report helper failure when Reflection owner metadata is missing + emitter.instruction("ret"); // return the null pointer to Rust } } } @@ -398,56 +410,56 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection let named_type_label = "__elephc_eval_reflection_owner_new_named_type"; let union_type_label = "__elephc_eval_reflection_owner_new_union_type"; let intersection_type_label = "__elephc_eval_reflection_owner_new_intersection_type"; - emitter.instruction("sub sp, sp, #160"); // reserve helper frame for inputs, object, arrays, scratch, and fp/lr - emitter.instruction("stp x29, x30, [sp, #144]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #144"); // establish a stable helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the Reflection owner kind - emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer - emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length - emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array - emitter.instruction("str x4, [sp, #80]"); // save the boxed ReflectionClass interface-name array - emitter.instruction("str x5, [sp, #88]"); // save the boxed ReflectionClass trait-name array - emitter.instruction("str x6, [sp, #104]"); // save the boxed ReflectionClass method-name array - emitter.instruction("str x7, [sp, #112]"); // save the boxed ReflectionClass property-name array - emitter.instruction("ldr x8, [sp, #160]"); // load the boxed ReflectionClass method objects array from the first stack argument - emitter.instruction("str x8, [sp, #120]"); // save the boxed ReflectionClass method objects array - emitter.instruction("ldr x8, [sp, #168]"); // load the boxed ReflectionClass property objects array from the second stack argument - emitter.instruction("str x8, [sp, #128]"); // save the boxed ReflectionClass property objects array - emitter.instruction("ldr x8, [sp, #176]"); // load the boxed ReflectionClass parent value from the third stack argument - emitter.instruction("str x8, [sp, #136]"); // save the boxed ReflectionClass parent value - emitter.instruction("ldr x8, [sp, #184]"); // load ReflectionClass modifier flags from the fourth stack argument - emitter.instruction("str x8, [sp, #48]"); // save ReflectionClass modifier flags - emitter.instruction("ldr x8, [sp, #192]"); // load owner modifier/count metadata from the fifth stack argument - emitter.instruction("str x8, [sp, #96]"); // save owner modifier/count metadata - emitter.instruction("ldr x8, [sp, #200]"); // load ReflectionMethod getModifiers bitmask from the sixth stack argument - emitter.instruction("str x8, [sp, #72]"); // save ReflectionMethod getModifiers bitmask - emitter.instruction("ldr x8, [sp, #208]"); // load boxed ReflectionClassConstant value from the seventh stack argument - emitter.instruction("str x8, [sp, #56]"); // save boxed ReflectionClassConstant value - emitter.instruction("ldr x8, [sp, #216]"); // load boxed ReflectionEnumBackedCase backing value from the eighth stack argument - emitter.instruction("str x8, [sp, #64]"); // save boxed ReflectionEnumBackedCase backing value - emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass - emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner - emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod - emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner - emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty - emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner - emitter.instruction("cmp x0, #3"); // owner kind 3 means ReflectionClassConstant - emitter.instruction(&format!("b.eq {}", class_constant_label)); // allocate a ReflectionClassConstant owner - emitter.instruction("cmp x0, #4"); // owner kind 4 means ReflectionEnumUnitCase - emitter.instruction(&format!("b.eq {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner - emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase - emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner - emitter.instruction("cmp x0, #6"); // owner kind 6 means ReflectionParameter - emitter.instruction(&format!("b.eq {}", parameter_label)); // allocate a ReflectionParameter owner - emitter.instruction("cmp x0, #7"); // owner kind 7 means ReflectionNamedType - emitter.instruction(&format!("b.eq {}", named_type_label)); // allocate a ReflectionNamedType owner - emitter.instruction("cmp x0, #8"); // owner kind 8 means ReflectionUnionType - emitter.instruction(&format!("b.eq {}", union_type_label)); // allocate a ReflectionUnionType owner - emitter.instruction("cmp x0, #9"); // owner kind 9 means ReflectionIntersectionType - emitter.instruction(&format!("b.eq {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner - emitter.instruction("cmp x0, #10"); // owner kind 10 means ReflectionFunction - emitter.instruction(&format!("b.eq {}", function_label)); // allocate a ReflectionFunction owner - emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds + emitter.instruction("sub sp, sp, #160"); // reserve helper frame for inputs, object, arrays, scratch, and fp/lr + emitter.instruction("stp x29, x30, [sp, #144]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #144"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the Reflection owner kind + emitter.instruction("str x1, [sp, #8]"); // save the reflected-name pointer + emitter.instruction("str x2, [sp, #16]"); // save the reflected-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed ReflectionAttribute array + emitter.instruction("str x4, [sp, #80]"); // save the boxed ReflectionClass interface-name array + emitter.instruction("str x5, [sp, #88]"); // save the boxed ReflectionClass trait-name array + emitter.instruction("str x6, [sp, #104]"); // save the boxed ReflectionClass method-name array + emitter.instruction("str x7, [sp, #112]"); // save the boxed ReflectionClass property-name array + emitter.instruction("ldr x8, [sp, #160]"); // load the boxed ReflectionClass method objects array from the first stack argument + emitter.instruction("str x8, [sp, #120]"); // save the boxed ReflectionClass method objects array + emitter.instruction("ldr x8, [sp, #168]"); // load the boxed ReflectionClass property objects array from the second stack argument + emitter.instruction("str x8, [sp, #128]"); // save the boxed ReflectionClass property objects array + emitter.instruction("ldr x8, [sp, #176]"); // load the boxed ReflectionClass parent value from the third stack argument + emitter.instruction("str x8, [sp, #136]"); // save the boxed ReflectionClass parent value + emitter.instruction("ldr x8, [sp, #184]"); // load ReflectionClass modifier flags from the fourth stack argument + emitter.instruction("str x8, [sp, #48]"); // save ReflectionClass modifier flags + emitter.instruction("ldr x8, [sp, #192]"); // load owner modifier/count metadata from the fifth stack argument + emitter.instruction("str x8, [sp, #96]"); // save owner modifier/count metadata + emitter.instruction("ldr x8, [sp, #200]"); // load ReflectionMethod getModifiers bitmask from the sixth stack argument + emitter.instruction("str x8, [sp, #72]"); // save ReflectionMethod getModifiers bitmask + emitter.instruction("ldr x8, [sp, #208]"); // load boxed ReflectionClassConstant value from the seventh stack argument + emitter.instruction("str x8, [sp, #56]"); // save boxed ReflectionClassConstant value + emitter.instruction("ldr x8, [sp, #216]"); // load boxed ReflectionEnumBackedCase backing value from the eighth stack argument + emitter.instruction("str x8, [sp, #64]"); // save boxed ReflectionEnumBackedCase backing value + emitter.instruction("cmp x0, #0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("b.eq {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp x0, #1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("b.eq {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp x0, #2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("b.eq {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp x0, #3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("b.eq {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp x0, #4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("b.eq {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp x0, #5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("b.eq {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner + emitter.instruction("cmp x0, #6"); // owner kind 6 means ReflectionParameter + emitter.instruction(&format!("b.eq {}", parameter_label)); // allocate a ReflectionParameter owner + emitter.instruction("cmp x0, #7"); // owner kind 7 means ReflectionNamedType + emitter.instruction(&format!("b.eq {}", named_type_label)); // allocate a ReflectionNamedType owner + emitter.instruction("cmp x0, #8"); // owner kind 8 means ReflectionUnionType + emitter.instruction(&format!("b.eq {}", union_type_label)); // allocate a ReflectionUnionType owner + emitter.instruction("cmp x0, #9"); // owner kind 9 means ReflectionIntersectionType + emitter.instruction(&format!("b.eq {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner + emitter.instruction("cmp x0, #10"); // owner kind 10 means ReflectionFunction + emitter.instruction(&format!("b.eq {}", function_label)); // allocate a ReflectionFunction owner + emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body( emitter, class_label, @@ -537,17 +549,17 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection box_label, ); emitter.label(box_label); - emitter.instruction("mov x0, #6"); // runtime tag 6 = object - emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload - emitter.instruction("mov x2, xzr"); // object payloads do not use a high word - emitter.instruction("bl __rt_mixed_from_value"); // box the Reflection owner object for eval - emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("ldr x1, [sp, #32]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("bl __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("b {}", done_label)); // skip the fail-closed return path after boxing emitter.label(fail_label); - emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure + emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #144]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #160"); // release the helper frame - emitter.instruction("ret"); // return the boxed reflection owner to Rust + emitter.instruction("ldp x29, x30, [sp, #144]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #160"); // release the helper frame + emitter.instruction("ret"); // return the boxed reflection owner to Rust } /// Emits the x86_64 Reflection owner materializer helper body. @@ -566,58 +578,58 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let named_type_label = "__elephc_eval_reflection_owner_new_named_type_x"; let union_type_label = "__elephc_eval_reflection_owner_new_union_type_x"; let intersection_type_label = "__elephc_eval_reflection_owner_new_intersection_type_x"; - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 144"); // reserve slots for inputs, object, metadata arrays, and name parts - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the Reflection owner kind - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer - emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length - emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array - emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // save the boxed ReflectionClass interface-name array - emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // save the boxed ReflectionClass trait-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the boxed ReflectionClass method-name array from the first stack argument - emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the boxed ReflectionClass method-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the boxed ReflectionClass property-name array from the second stack argument - emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // save the boxed ReflectionClass property-name array - emitter.instruction("mov rax, QWORD PTR [rbp + 32]"); // load the boxed ReflectionClass method objects array from the third stack argument - emitter.instruction("mov QWORD PTR [rbp - 128], rax"); // save the boxed ReflectionClass method objects array - emitter.instruction("mov rax, QWORD PTR [rbp + 40]"); // load the boxed ReflectionClass property objects array from the fourth stack argument - emitter.instruction("mov QWORD PTR [rbp - 136], rax"); // save the boxed ReflectionClass property objects array - emitter.instruction("mov rax, QWORD PTR [rbp + 48]"); // load the boxed ReflectionClass parent value from the fifth stack argument - emitter.instruction("mov QWORD PTR [rbp - 144], rax"); // save the boxed ReflectionClass parent value - emitter.instruction("mov rax, QWORD PTR [rbp + 56]"); // load ReflectionClass modifier flags from the sixth stack argument - emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags - emitter.instruction("mov rax, QWORD PTR [rbp + 64]"); // load owner modifier/count metadata from the seventh stack argument - emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save owner modifier/count metadata - emitter.instruction("mov rax, QWORD PTR [rbp + 72]"); // load ReflectionMethod getModifiers bitmask from the eighth stack argument - emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save ReflectionMethod getModifiers bitmask - emitter.instruction("mov rax, QWORD PTR [rbp + 80]"); // load boxed ReflectionClassConstant value from the ninth stack argument - emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save boxed ReflectionClassConstant value - emitter.instruction("mov rax, QWORD PTR [rbp + 88]"); // load boxed ReflectionEnumBackedCase backing value from the tenth stack argument - emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save boxed ReflectionEnumBackedCase backing value - emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass - emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner - emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod - emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner - emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty - emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner - emitter.instruction("cmp rdi, 3"); // owner kind 3 means ReflectionClassConstant - emitter.instruction(&format!("je {}", class_constant_label)); // allocate a ReflectionClassConstant owner - emitter.instruction("cmp rdi, 4"); // owner kind 4 means ReflectionEnumUnitCase - emitter.instruction(&format!("je {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner - emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase - emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner - emitter.instruction("cmp rdi, 6"); // owner kind 6 means ReflectionParameter - emitter.instruction(&format!("je {}", parameter_label)); // allocate a ReflectionParameter owner - emitter.instruction("cmp rdi, 7"); // owner kind 7 means ReflectionNamedType - emitter.instruction(&format!("je {}", named_type_label)); // allocate a ReflectionNamedType owner - emitter.instruction("cmp rdi, 8"); // owner kind 8 means ReflectionUnionType - emitter.instruction(&format!("je {}", union_type_label)); // allocate a ReflectionUnionType owner - emitter.instruction("cmp rdi, 9"); // owner kind 9 means ReflectionIntersectionType - emitter.instruction(&format!("je {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner - emitter.instruction("cmp rdi, 10"); // owner kind 10 means ReflectionFunction - emitter.instruction(&format!("je {}", function_label)); // allocate a ReflectionFunction owner - emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 144"); // reserve slots for inputs, object, metadata arrays, and name parts + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the Reflection owner kind + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the reflected-name pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the reflected-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed ReflectionAttribute array + emitter.instruction("mov QWORD PTR [rbp - 88], r8"); // save the boxed ReflectionClass interface-name array + emitter.instruction("mov QWORD PTR [rbp - 96], r9"); // save the boxed ReflectionClass trait-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the boxed ReflectionClass method-name array from the first stack argument + emitter.instruction("mov QWORD PTR [rbp - 112], rax"); // save the boxed ReflectionClass method-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the boxed ReflectionClass property-name array from the second stack argument + emitter.instruction("mov QWORD PTR [rbp - 120], rax"); // save the boxed ReflectionClass property-name array + emitter.instruction("mov rax, QWORD PTR [rbp + 32]"); // load the boxed ReflectionClass method objects array from the third stack argument + emitter.instruction("mov QWORD PTR [rbp - 128], rax"); // save the boxed ReflectionClass method objects array + emitter.instruction("mov rax, QWORD PTR [rbp + 40]"); // load the boxed ReflectionClass property objects array from the fourth stack argument + emitter.instruction("mov QWORD PTR [rbp - 136], rax"); // save the boxed ReflectionClass property objects array + emitter.instruction("mov rax, QWORD PTR [rbp + 48]"); // load the boxed ReflectionClass parent value from the fifth stack argument + emitter.instruction("mov QWORD PTR [rbp - 144], rax"); // save the boxed ReflectionClass parent value + emitter.instruction("mov rax, QWORD PTR [rbp + 56]"); // load ReflectionClass modifier flags from the sixth stack argument + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save ReflectionClass modifier flags + emitter.instruction("mov rax, QWORD PTR [rbp + 64]"); // load owner modifier/count metadata from the seventh stack argument + emitter.instruction("mov QWORD PTR [rbp - 104], rax"); // save owner modifier/count metadata + emitter.instruction("mov rax, QWORD PTR [rbp + 72]"); // load ReflectionMethod getModifiers bitmask from the eighth stack argument + emitter.instruction("mov QWORD PTR [rbp - 80], rax"); // save ReflectionMethod getModifiers bitmask + emitter.instruction("mov rax, QWORD PTR [rbp + 80]"); // load boxed ReflectionClassConstant value from the ninth stack argument + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save boxed ReflectionClassConstant value + emitter.instruction("mov rax, QWORD PTR [rbp + 88]"); // load boxed ReflectionEnumBackedCase backing value from the tenth stack argument + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save boxed ReflectionEnumBackedCase backing value + emitter.instruction("cmp rdi, 0"); // owner kind 0 means ReflectionClass + emitter.instruction(&format!("je {}", class_label)); // allocate a ReflectionClass owner + emitter.instruction("cmp rdi, 1"); // owner kind 1 means ReflectionMethod + emitter.instruction(&format!("je {}", method_label)); // allocate a ReflectionMethod owner + emitter.instruction("cmp rdi, 2"); // owner kind 2 means ReflectionProperty + emitter.instruction(&format!("je {}", property_label)); // allocate a ReflectionProperty owner + emitter.instruction("cmp rdi, 3"); // owner kind 3 means ReflectionClassConstant + emitter.instruction(&format!("je {}", class_constant_label)); // allocate a ReflectionClassConstant owner + emitter.instruction("cmp rdi, 4"); // owner kind 4 means ReflectionEnumUnitCase + emitter.instruction(&format!("je {}", enum_unit_case_label)); // allocate a ReflectionEnumUnitCase owner + emitter.instruction("cmp rdi, 5"); // owner kind 5 means ReflectionEnumBackedCase + emitter.instruction(&format!("je {}", enum_backed_case_label)); // allocate a ReflectionEnumBackedCase owner + emitter.instruction("cmp rdi, 6"); // owner kind 6 means ReflectionParameter + emitter.instruction(&format!("je {}", parameter_label)); // allocate a ReflectionParameter owner + emitter.instruction("cmp rdi, 7"); // owner kind 7 means ReflectionNamedType + emitter.instruction(&format!("je {}", named_type_label)); // allocate a ReflectionNamedType owner + emitter.instruction("cmp rdi, 8"); // owner kind 8 means ReflectionUnionType + emitter.instruction(&format!("je {}", union_type_label)); // allocate a ReflectionUnionType owner + emitter.instruction("cmp rdi, 9"); // owner kind 9 means ReflectionIntersectionType + emitter.instruction(&format!("je {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner + emitter.instruction("cmp rdi, 10"); // owner kind 10 means ReflectionFunction + emitter.instruction(&format!("je {}", function_label)); // allocate a ReflectionFunction owner + emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body( emitter, class_label, @@ -707,17 +719,17 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO box_label, ); emitter.label(box_label); - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload - emitter.instruction("xor esi, esi"); // object payloads do not use a high word - emitter.instruction("mov eax, 6"); // runtime tag 6 = object - emitter.instruction("call __rt_mixed_from_value"); // box the Reflection owner object for eval - emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // move the Reflection owner object pointer into the Mixed payload + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("call __rt_mixed_from_value"); // box the Reflection owner object for eval + emitter.instruction(&format!("jmp {}", done_label)); // skip the fail-closed return path after boxing emitter.label(fail_label); - emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure + emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the boxed reflection owner to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed reflection owner to Rust } /// Emits one ARM64 owner-kind allocation and slot-population body. @@ -731,7 +743,7 @@ fn emit_aarch64_owner_kind_body( ) { emitter.label(label); emit_alloc_reflection_owner_object_aarch64(emitter, layout); - emitter.instruction("str x0, [sp, #32]"); // save the unboxed Reflection owner object pointer + emitter.instruction("str x0, [sp, #32]"); // save the unboxed Reflection owner object pointer if set_name { emit_set_owner_name_property_aarch64(emitter, layout); } @@ -744,12 +756,13 @@ fn emit_aarch64_owner_kind_body( emit_set_owner_parameter_property_aarch64(emitter, layout); emit_set_owner_parameter_type_property_aarch64(emitter, layout, fail_label); emit_set_owner_parameter_default_property_aarch64(emitter, layout, fail_label); + emit_set_owner_parameter_default_constant_name_property_aarch64(emitter, layout, fail_label); emit_set_owner_metadata_arrays_property_aarch64(emitter, layout, fail_label); emit_set_owner_constructor_property_aarch64(emitter, layout, fail_label); emit_set_owner_parent_class_property_aarch64(emitter, layout, fail_label); emit_set_owner_declaring_function_property_aarch64(emitter, layout, fail_label); emit_set_owner_attrs_property_aarch64(emitter, layout, fail_label); - emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object + emitter.instruction(&format!("b {}", box_label)); // box this populated Reflection owner object } /// Emits one x86_64 owner-kind allocation and slot-population body. @@ -763,7 +776,7 @@ fn emit_x86_64_owner_kind_body( ) { emitter.label(label); emit_alloc_reflection_owner_object_x86_64(emitter, layout); - emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the unboxed Reflection owner object pointer + emitter.instruction("mov QWORD PTR [rbp - 40], rax"); // save the unboxed Reflection owner object pointer if set_name { emit_set_owner_name_property_x86_64(emitter, layout); } @@ -776,12 +789,13 @@ fn emit_x86_64_owner_kind_body( emit_set_owner_parameter_property_x86_64(emitter, layout); emit_set_owner_parameter_type_property_x86_64(emitter, layout, fail_label); emit_set_owner_parameter_default_property_x86_64(emitter, layout, fail_label); + emit_set_owner_parameter_default_constant_name_property_x86_64(emitter, layout, fail_label); emit_set_owner_metadata_arrays_property_x86_64(emitter, layout, fail_label); emit_set_owner_constructor_property_x86_64(emitter, layout, fail_label); emit_set_owner_parent_class_property_x86_64(emitter, layout, fail_label); emit_set_owner_declaring_function_property_x86_64(emitter, layout, fail_label); emit_set_owner_attrs_property_x86_64(emitter, layout, fail_label); - emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object + emitter.instruction(&format!("jmp {}", box_label)); // box this populated Reflection owner object } /// Allocates a zero-initialized ARM64 Reflection owner object payload. @@ -790,12 +804,12 @@ fn emit_alloc_reflection_owner_object_aarch64( layout: &ReflectionOwnerLayout, ) { let payload_size = 8 + layout.property_count * 16; - emitter.instruction(&format!("mov x0, #{}", payload_size)); // request Reflection owner object payload storage + emitter.instruction(&format!("mov x0, #{}", payload_size)); // request Reflection owner object payload storage abi::emit_call_label(emitter, "__rt_heap_alloc"); - emitter.instruction("mov x9, #4"); // heap kind 4 marks the payload as an object - emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload - emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the Reflection owner class id - emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + emitter.instruction("mov x9, #4"); // heap kind 4 marks the payload as an object + emitter.instruction("str x9, [x0, #-8]"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov x10, #{}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero for index in 0..layout.property_count { let offset = 8 + index * 16; abi::emit_store_zero_to_address(emitter, "x0", offset); @@ -809,15 +823,15 @@ fn emit_alloc_reflection_owner_object_x86_64( layout: &ReflectionOwnerLayout, ) { let payload_size = 8 + layout.property_count * 16; - emitter.instruction(&format!("mov rax, {}", payload_size)); // request Reflection owner object payload storage + emitter.instruction(&format!("mov rax, {}", payload_size)); // request Reflection owner object payload storage abi::emit_call_label(emitter, "__rt_heap_alloc"); emitter.instruction(&format!( "mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4 - )); // materialize the x86_64 object heap kind word - emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload - emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id - emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + )); // materialize the x86_64 object heap kind word + emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the object heap header before the payload + emitter.instruction(&format!("mov r10, {}", layout.class_id)); // materialize the Reflection owner class id + emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero for index in 0..layout.property_count { let offset = 8 + index * 16; abi::emit_store_zero_to_address(emitter, "rax", offset); @@ -833,10 +847,10 @@ fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &Reflecti let Some(name_hi) = layout.name_hi else { return; }; - emitter.instruction("ldr x1, [sp, #8]"); // reload the reflected-name pointer for persistence - emitter.instruction("ldr x2, [sp, #16]"); // reload the reflected-name length for persistence - emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #8]"); // reload the reflected-name pointer for persistence + emitter.instruction("ldr x2, [sp, #16]"); // reload the reflected-name length for persistence + emitter.instruction("bl __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", name_lo); abi::emit_store_to_address(emitter, "x2", "x9", name_hi); let ( @@ -862,43 +876,43 @@ fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &Reflecti let found_label = "__elephc_eval_reflection_owner_name_scan_found"; let no_namespace_label = "__elephc_eval_reflection_owner_name_scan_none"; let store_parts_label = "__elephc_eval_reflection_owner_name_store_parts"; - emitter.instruction("ldr x3, [sp, #8]"); // reload the original reflected-name pointer for splitting - emitter.instruction("ldr x4, [sp, #16]"); // reload the original reflected-name length for splitting - emitter.instruction("mov x5, x4"); // start scanning from one byte past the final name byte - emitter.instruction(&format!("cbz x5, {}", no_namespace_label)); // empty names have no namespace component + emitter.instruction("ldr x3, [sp, #8]"); // reload the original reflected-name pointer for splitting + emitter.instruction("ldr x4, [sp, #16]"); // reload the original reflected-name length for splitting + emitter.instruction("mov x5, x4"); // start scanning from one byte past the final name byte + emitter.instruction(&format!("cbz x5, {}", no_namespace_label)); // empty names have no namespace component emitter.label(scan_loop_label); - emitter.instruction("sub x5, x5, #1"); // move the scan cursor to the previous byte - emitter.instruction("ldrb w6, [x3, x5]"); // read one reflected-name byte from the scan cursor - emitter.instruction("cmp w6, #92"); // compare against PHP namespace separator '\\' - emitter.instruction(&format!("b.eq {}", found_label)); // split at the final namespace separator - emitter.instruction(&format!("cbnz x5, {}", scan_loop_label)); // keep scanning until the first byte has been checked + emitter.instruction("sub x5, x5, #1"); // move the scan cursor to the previous byte + emitter.instruction("ldrb w6, [x3, x5]"); // read one reflected-name byte from the scan cursor + emitter.instruction("cmp w6, #92"); // compare against PHP namespace separator '\\' + emitter.instruction(&format!("b.eq {}", found_label)); // split at the final namespace separator + emitter.instruction(&format!("cbnz x5, {}", scan_loop_label)); // keep scanning until the first byte has been checked emitter.label(no_namespace_label); - emitter.instruction("str x3, [sp, #56]"); // short-name pointer is the original name pointer - emitter.instruction("str x4, [sp, #64]"); // short-name length is the full name length - emitter.instruction("str xzr, [sp, #72]"); // namespace length is zero for global names - emitter.instruction(&format!("b {}", store_parts_label)); // skip the namespaced split path + emitter.instruction("str x3, [sp, #56]"); // short-name pointer is the original name pointer + emitter.instruction("str x4, [sp, #64]"); // short-name length is the full name length + emitter.instruction("str xzr, [sp, #72]"); // namespace length is zero for global names + emitter.instruction(&format!("b {}", store_parts_label)); // skip the namespaced split path emitter.label(found_label); - emitter.instruction("add x6, x5, #1"); // compute the short-name byte offset after the separator - emitter.instruction("add x7, x3, x6"); // compute the short-name pointer - emitter.instruction("sub x8, x4, x6"); // compute the short-name length - emitter.instruction("str x7, [sp, #56]"); // save the short-name pointer across persistence calls - emitter.instruction("str x8, [sp, #64]"); // save the short-name length across persistence calls - emitter.instruction("str x5, [sp, #72]"); // namespace length is the separator offset + emitter.instruction("add x6, x5, #1"); // compute the short-name byte offset after the separator + emitter.instruction("add x7, x3, x6"); // compute the short-name pointer + emitter.instruction("sub x8, x4, x6"); // compute the short-name length + emitter.instruction("str x7, [sp, #56]"); // save the short-name pointer across persistence calls + emitter.instruction("str x8, [sp, #64]"); // save the short-name length across persistence calls + emitter.instruction("str x5, [sp, #72]"); // namespace length is the separator offset emitter.label(store_parts_label); - emitter.instruction("ldr x1, [sp, #8]"); // use the original name pointer for namespace persistence - emitter.instruction("ldr x2, [sp, #72]"); // reload the namespace byte length - emitter.instruction("bl __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #8]"); // use the original name pointer for namespace persistence + emitter.instruction("ldr x2, [sp, #72]"); // reload the namespace byte length + emitter.instruction("bl __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", namespace_name_lo); abi::emit_store_to_address(emitter, "x2", "x9", namespace_name_hi); - emitter.instruction("cmp x2, #0"); // detect whether a namespace component was present - emitter.instruction("cset x10, ne"); // materialize ReflectionClass::inNamespace() + emitter.instruction("cmp x2, #0"); // detect whether a namespace component was present + emitter.instruction("cset x10, ne"); // materialize ReflectionClass::inNamespace() abi::emit_store_to_address(emitter, "x10", "x9", in_namespace_lo); abi::emit_store_zero_to_address(emitter, "x9", in_namespace_hi); - emitter.instruction("ldr x1, [sp, #56]"); // reload the short-name pointer - emitter.instruction("ldr x2, [sp, #64]"); // reload the short-name byte length - emitter.instruction("bl __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x1, [sp, #56]"); // reload the short-name pointer + emitter.instruction("ldr x2, [sp, #64]"); // reload the short-name byte length + emitter.instruction("bl __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", short_name_lo); abi::emit_store_to_address(emitter, "x2", "x9", short_name_hi); } @@ -911,10 +925,10 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio let Some(name_hi) = layout.name_hi else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the reflected-name pointer for persistence - emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the reflected-name length for persistence - emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // reload the reflected-name pointer for persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 24]"); // reload the reflected-name length for persistence + emitter.instruction("call __rt_str_persist"); // copy the eval-owned name bytes for object ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", name_hi); let ( @@ -939,47 +953,47 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio let found_label = "__elephc_eval_reflection_owner_name_scan_found_x"; let no_namespace_label = "__elephc_eval_reflection_owner_name_scan_none_x"; let store_parts_label = "__elephc_eval_reflection_owner_name_store_parts_x"; - emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the original reflected-name pointer for splitting - emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the original reflected-name length for splitting - emitter.instruction("mov r11, r9"); // start scanning from one byte past the final name byte - emitter.instruction("test r11, r11"); // check whether the reflected name is empty - emitter.instruction(&format!("jz {}", no_namespace_label)); // empty names have no namespace component + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the original reflected-name pointer for splitting + emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the original reflected-name length for splitting + emitter.instruction("mov r11, r9"); // start scanning from one byte past the final name byte + emitter.instruction("test r11, r11"); // check whether the reflected name is empty + emitter.instruction(&format!("jz {}", no_namespace_label)); // empty names have no namespace component emitter.label(scan_loop_label); - emitter.instruction("sub r11, 1"); // move the scan cursor to the previous byte - emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // read one reflected-name byte from the scan cursor - emitter.instruction("cmp eax, 92"); // compare against PHP namespace separator '\\' - emitter.instruction(&format!("je {}", found_label)); // split at the final namespace separator - emitter.instruction("test r11, r11"); // check whether the first byte has been examined - emitter.instruction(&format!("jnz {}", scan_loop_label)); // keep scanning until the first byte has been checked + emitter.instruction("sub r11, 1"); // move the scan cursor to the previous byte + emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // read one reflected-name byte from the scan cursor + emitter.instruction("cmp eax, 92"); // compare against PHP namespace separator '\\' + emitter.instruction(&format!("je {}", found_label)); // split at the final namespace separator + emitter.instruction("test r11, r11"); // check whether the first byte has been examined + emitter.instruction(&format!("jnz {}", scan_loop_label)); // keep scanning until the first byte has been checked emitter.label(no_namespace_label); - emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // short-name pointer is the original name pointer - emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // short-name length is the full name length - emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // namespace length is zero for global names - emitter.instruction(&format!("jmp {}", store_parts_label)); // skip the namespaced split path + emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // short-name pointer is the original name pointer + emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // short-name length is the full name length + emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // namespace length is zero for global names + emitter.instruction(&format!("jmp {}", store_parts_label)); // skip the namespaced split path emitter.label(found_label); - emitter.instruction("lea rax, [r11 + 1]"); // compute the short-name byte offset after the separator - emitter.instruction("lea r10, [r8 + rax]"); // compute the short-name pointer - emitter.instruction("mov rcx, r9"); // copy the full name length before subtracting the prefix - emitter.instruction("sub rcx, rax"); // compute the short-name length - emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the short-name pointer across persistence calls - emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // save the short-name length across persistence calls - emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // namespace length is the separator offset + emitter.instruction("lea rax, [r11 + 1]"); // compute the short-name byte offset after the separator + emitter.instruction("lea r10, [r8 + rax]"); // compute the short-name pointer + emitter.instruction("mov rcx, r9"); // copy the full name length before subtracting the prefix + emitter.instruction("sub rcx, rax"); // compute the short-name length + emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the short-name pointer across persistence calls + emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // save the short-name length across persistence calls + emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // namespace length is the separator offset emitter.label(store_parts_label); - emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // use the original name pointer for namespace persistence - emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the namespace byte length - emitter.instruction("call __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // use the original name pointer for namespace persistence + emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the namespace byte length + emitter.instruction("call __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", namespace_name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", namespace_name_hi); - emitter.instruction("test rdx, rdx"); // detect whether a namespace component was present - emitter.instruction("setne al"); // materialize ReflectionClass::inNamespace() - emitter.instruction("movzx eax, al"); // widen the namespace boolean to a full word + emitter.instruction("test rdx, rdx"); // detect whether a namespace component was present + emitter.instruction("setne al"); // materialize ReflectionClass::inNamespace() + emitter.instruction("movzx eax, al"); // widen the namespace boolean to a full word abi::emit_store_to_address(emitter, "rax", "r10", in_namespace_lo); abi::emit_store_zero_to_address(emitter, "r10", in_namespace_hi); - emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the short-name pointer - emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // reload the short-name byte length - emitter.instruction("call __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the short-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 72]"); // reload the short-name byte length + emitter.instruction("call __rt_str_persist"); // copy the short-name bytes for ReflectionClass storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", short_name_lo); abi::emit_store_to_address(emitter, "rdx", "r10", short_name_hi); } @@ -1047,56 +1061,56 @@ fn emit_set_owner_class_flags_property_aarch64( else { return; }; - emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer - emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionClass modifier flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract the final-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); - emitter.instruction("lsr x10, x11, #1"); // move the abstract-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean + emitter.instruction("lsr x10, x11, #1"); // move the abstract-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); - emitter.instruction("lsr x10, x11, #2"); // move the interface bit into position - emitter.instruction("and x10, x10, #1"); // extract the interface flag as a boolean + emitter.instruction("lsr x10, x11, #2"); // move the interface bit into position + emitter.instruction("and x10, x10, #1"); // extract the interface flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_interface_lo); abi::emit_store_zero_to_address(emitter, "x9", is_interface_hi); - emitter.instruction("lsr x10, x11, #3"); // move the trait bit into position - emitter.instruction("and x10, x10, #1"); // extract the trait flag as a boolean + emitter.instruction("lsr x10, x11, #3"); // move the trait bit into position + emitter.instruction("and x10, x10, #1"); // extract the trait flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_trait_lo); abi::emit_store_zero_to_address(emitter, "x9", is_trait_hi); - emitter.instruction("lsr x10, x11, #4"); // move the enum bit into position - emitter.instruction("and x10, x10, #1"); // extract the enum flag as a boolean + emitter.instruction("lsr x10, x11, #4"); // move the enum bit into position + emitter.instruction("and x10, x10, #1"); // extract the enum flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_enum_lo); abi::emit_store_zero_to_address(emitter, "x9", is_enum_hi); - emitter.instruction("lsr x10, x11, #5"); // move the readonly-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the readonly-class flag as a boolean + emitter.instruction("lsr x10, x11, #5"); // move the readonly-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the readonly-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); - emitter.instruction("lsr x10, x11, #6"); // move the instantiable-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the instantiable-class flag as a boolean + emitter.instruction("lsr x10, x11, #6"); // move the instantiable-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the instantiable-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_instantiable_lo); abi::emit_store_zero_to_address(emitter, "x9", is_instantiable_hi); - emitter.instruction("lsr x10, x11, #7"); // move the cloneable-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the cloneable-class flag as a boolean + emitter.instruction("lsr x10, x11, #7"); // move the cloneable-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the cloneable-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_cloneable_lo); abi::emit_store_zero_to_address(emitter, "x9", is_cloneable_hi); - emitter.instruction("lsr x10, x11, #8"); // move the internal-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the internal-class flag as a boolean + emitter.instruction("lsr x10, x11, #8"); // move the internal-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the internal-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_internal_lo); abi::emit_store_zero_to_address(emitter, "x9", is_internal_hi); - emitter.instruction("lsr x10, x11, #9"); // move the user-defined-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the user-defined-class flag as a boolean + emitter.instruction("lsr x10, x11, #9"); // move the user-defined-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the user-defined-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_user_defined_lo); abi::emit_store_zero_to_address(emitter, "x9", is_user_defined_hi); - emitter.instruction("lsr x10, x11, #10"); // move the iterable-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the iterable-class flag as a boolean + emitter.instruction("lsr x10, x11, #10"); // move the iterable-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the iterable-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_iterable_lo); abi::emit_store_zero_to_address(emitter, "x9", is_iterable_hi); - emitter.instruction("lsr x10, x11, #11"); // move the anonymous-class bit into position - emitter.instruction("and x10, x10, #1"); // extract the anonymous-class flag as a boolean + emitter.instruction("lsr x10, x11, #11"); // move the anonymous-class bit into position + emitter.instruction("and x10, x10, #1"); // extract the anonymous-class flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_anonymous_lo); abi::emit_store_zero_to_address(emitter, "x9", is_anonymous_hi); - emitter.instruction("ldr x10, [sp, #96]"); // reload PHP ReflectionClass::getModifiers() bitmask + emitter.instruction("ldr x10, [sp, #96]"); // reload PHP ReflectionClass::getModifiers() bitmask abi::emit_store_to_address(emitter, "x10", "x9", modifiers_lo); abi::emit_store_zero_to_address(emitter, "x9", modifiers_hi); } @@ -1164,68 +1178,68 @@ fn emit_set_owner_class_flags_property_x86_64( else { return; }; - emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer - emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit - emitter.instruction("and rax, 1"); // extract the final-class flag as a boolean + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionClass modifier flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit + emitter.instruction("and rax, 1"); // extract the final-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit - emitter.instruction("shr rax, 1"); // move the abstract-class bit into position - emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit + emitter.instruction("shr rax, 1"); // move the abstract-class bit into position + emitter.instruction("and rax, 1"); // extract the abstract-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the interface bit - emitter.instruction("shr rax, 2"); // move the interface bit into position - emitter.instruction("and rax, 1"); // extract the interface flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the interface bit + emitter.instruction("shr rax, 2"); // move the interface bit into position + emitter.instruction("and rax, 1"); // extract the interface flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_interface_lo); abi::emit_store_zero_to_address(emitter, "r10", is_interface_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the trait bit - emitter.instruction("shr rax, 3"); // move the trait bit into position - emitter.instruction("and rax, 1"); // extract the trait flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the trait bit + emitter.instruction("shr rax, 3"); // move the trait bit into position + emitter.instruction("and rax, 1"); // extract the trait flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_trait_lo); abi::emit_store_zero_to_address(emitter, "r10", is_trait_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the enum bit - emitter.instruction("shr rax, 4"); // move the enum bit into position - emitter.instruction("and rax, 1"); // extract the enum flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the enum bit + emitter.instruction("shr rax, 4"); // move the enum bit into position + emitter.instruction("and rax, 1"); // extract the enum flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_enum_lo); abi::emit_store_zero_to_address(emitter, "r10", is_enum_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the readonly-class bit - emitter.instruction("shr rax, 5"); // move the readonly-class bit into position - emitter.instruction("and rax, 1"); // extract the readonly-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the readonly-class bit + emitter.instruction("shr rax, 5"); // move the readonly-class bit into position + emitter.instruction("and rax, 1"); // extract the readonly-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the instantiable bit - emitter.instruction("shr rax, 6"); // move the instantiable-class bit into position - emitter.instruction("and rax, 1"); // extract the instantiable-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the instantiable bit + emitter.instruction("shr rax, 6"); // move the instantiable-class bit into position + emitter.instruction("and rax, 1"); // extract the instantiable-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_instantiable_lo); abi::emit_store_zero_to_address(emitter, "r10", is_instantiable_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the cloneable bit - emitter.instruction("shr rax, 7"); // move the cloneable-class bit into position - emitter.instruction("and rax, 1"); // extract the cloneable-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the cloneable bit + emitter.instruction("shr rax, 7"); // move the cloneable-class bit into position + emitter.instruction("and rax, 1"); // extract the cloneable-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_cloneable_lo); abi::emit_store_zero_to_address(emitter, "r10", is_cloneable_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the internal bit - emitter.instruction("shr rax, 8"); // move the internal-class bit into position - emitter.instruction("and rax, 1"); // extract the internal-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the internal bit + emitter.instruction("shr rax, 8"); // move the internal-class bit into position + emitter.instruction("and rax, 1"); // extract the internal-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_internal_lo); abi::emit_store_zero_to_address(emitter, "r10", is_internal_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the user-defined bit - emitter.instruction("shr rax, 9"); // move the user-defined-class bit into position - emitter.instruction("and rax, 1"); // extract the user-defined-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the user-defined bit + emitter.instruction("shr rax, 9"); // move the user-defined-class bit into position + emitter.instruction("and rax, 1"); // extract the user-defined-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_user_defined_lo); abi::emit_store_zero_to_address(emitter, "r10", is_user_defined_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the iterable bit - emitter.instruction("shr rax, 10"); // move the iterable-class bit into position - emitter.instruction("and rax, 1"); // extract the iterable-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the iterable bit + emitter.instruction("shr rax, 10"); // move the iterable-class bit into position + emitter.instruction("and rax, 1"); // extract the iterable-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_iterable_lo); abi::emit_store_zero_to_address(emitter, "r10", is_iterable_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the anonymous bit - emitter.instruction("shr rax, 11"); // move the anonymous-class bit into position - emitter.instruction("and rax, 1"); // extract the anonymous-class flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the anonymous bit + emitter.instruction("shr rax, 11"); // move the anonymous-class bit into position + emitter.instruction("and rax, 1"); // extract the anonymous-class flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_anonymous_lo); abi::emit_store_zero_to_address(emitter, "r10", is_anonymous_hi); - emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP ReflectionClass::getModifiers() bitmask + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP ReflectionClass::getModifiers() bitmask abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); } @@ -1253,77 +1267,77 @@ fn emit_set_owner_member_flags_property_aarch64( else { return; }; - emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection member predicate flags - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection member predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer if let (Some(is_static_lo), Some(is_static_hi)) = (layout.is_static_lo, layout.is_static_hi) { - emitter.instruction("and x10, x11, #1"); // extract the static-member flag as a boolean + emitter.instruction("and x10, x11, #1"); // extract the static-member flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_static_lo); abi::emit_store_zero_to_address(emitter, "x9", is_static_hi); } - emitter.instruction("lsr x10, x11, #1"); // move the public-member bit into position - emitter.instruction("and x10, x10, #1"); // extract the public-member flag as a boolean + emitter.instruction("lsr x10, x11, #1"); // move the public-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the public-member flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_public_lo); abi::emit_store_zero_to_address(emitter, "x9", is_public_hi); - emitter.instruction("lsr x10, x11, #2"); // move the protected-member bit into position - emitter.instruction("and x10, x10, #1"); // extract the protected-member flag as a boolean + emitter.instruction("lsr x10, x11, #2"); // move the protected-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the protected-member flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_protected_lo); abi::emit_store_zero_to_address(emitter, "x9", is_protected_hi); - emitter.instruction("lsr x10, x11, #3"); // move the private-member bit into position - emitter.instruction("and x10, x10, #1"); // extract the private-member flag as a boolean + emitter.instruction("lsr x10, x11, #3"); // move the private-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the private-member flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_private_lo); abi::emit_store_zero_to_address(emitter, "x9", is_private_hi); if let (Some(is_enum_case_lo), Some(is_enum_case_hi)) = (layout.is_enum_case_lo, layout.is_enum_case_hi) { - emitter.instruction("lsr x10, x11, #7"); // move the enum-case flag into position - emitter.instruction("and x10, x10, #1"); // extract the enum-case flag as a boolean + emitter.instruction("lsr x10, x11, #7"); // move the enum-case flag into position + emitter.instruction("and x10, x10, #1"); // extract the enum-case flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_enum_case_lo); abi::emit_store_zero_to_address(emitter, "x9", is_enum_case_hi); } if let (Some(is_readonly_lo), Some(is_readonly_hi)) = (layout.is_readonly_lo, layout.is_readonly_hi) { - emitter.instruction("lsr x10, x11, #6"); // move the readonly-property bit into position - emitter.instruction("and x10, x10, #1"); // extract the readonly-property flag as a boolean + emitter.instruction("lsr x10, x11, #6"); // move the readonly-property bit into position + emitter.instruction("and x10, x10, #1"); // extract the readonly-property flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "x9", is_readonly_hi); } if let (Some(has_default_value_lo), Some(has_default_value_hi)) = (layout.has_default_value_lo, layout.has_default_value_hi) { - emitter.instruction("lsr x10, x11, #8"); // move the default-value bit into position - emitter.instruction("and x10, x10, #1"); // extract the default-value flag as a boolean + emitter.instruction("lsr x10, x11, #8"); // move the default-value bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-value flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", has_default_value_lo); abi::emit_store_zero_to_address(emitter, "x9", has_default_value_hi); } if let (Some(is_promoted_lo), Some(is_promoted_hi)) = (layout.is_promoted_lo, layout.is_promoted_hi) { - emitter.instruction("lsr x10, x11, #9"); // move the promoted-property bit into position - emitter.instruction("and x10, x10, #1"); // extract the promoted-property flag as a boolean + emitter.instruction("lsr x10, x11, #9"); // move the promoted-property bit into position + emitter.instruction("and x10, x10, #1"); // extract the promoted-property flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_promoted_lo); abi::emit_store_zero_to_address(emitter, "x9", is_promoted_hi); } if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { if layout.required_parameter_count_lo.is_some() { - emitter.instruction("ldr x10, [sp, #72]"); // reload PHP ReflectionMethod::getModifiers() bitmask + emitter.instruction("ldr x10, [sp, #72]"); // reload PHP ReflectionMethod::getModifiers() bitmask } else { - emitter.instruction("ldr x10, [sp, #96]"); // reload PHP Reflection member getModifiers() bitmask + emitter.instruction("ldr x10, [sp, #96]"); // reload PHP Reflection member getModifiers() bitmask } abi::emit_store_to_address(emitter, "x10", "x9", modifiers_lo); abi::emit_store_zero_to_address(emitter, "x9", modifiers_hi); } if let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) { - emitter.instruction("lsr x10, x11, #4"); // move the final-member bit into position - emitter.instruction("and x10, x10, #1"); // extract the final-member flag as a boolean + emitter.instruction("lsr x10, x11, #4"); // move the final-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the final-member flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); } if let (Some(is_abstract_lo), Some(is_abstract_hi)) = (layout.is_abstract_lo, layout.is_abstract_hi) { - emitter.instruction("lsr x10, x11, #5"); // move the abstract-member bit into position - emitter.instruction("and x10, x10, #1"); // extract the abstract-member flag as a boolean + emitter.instruction("lsr x10, x11, #5"); // move the abstract-member bit into position + emitter.instruction("and x10, x10, #1"); // extract the abstract-member flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "x9", is_abstract_hi); } @@ -1337,9 +1351,9 @@ fn emit_set_owner_low_bit_final_property_aarch64( let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) else { return; }; - emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection owner predicate flags - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer - emitter.instruction("and x10, x11, #1"); // extract bit-zero finality as a boolean + emitter.instruction("ldr x11, [sp, #48]"); // reload Reflection owner predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("and x10, x11, #1"); // extract bit-zero finality as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_final_lo); abi::emit_store_zero_to_address(emitter, "x9", is_final_hi); } @@ -1368,87 +1382,87 @@ fn emit_set_owner_member_flags_property_x86_64( emit_set_owner_low_bit_final_property_x86_64(emitter, layout); return; }; - emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection member predicate flags - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection member predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer if let (Some(is_static_lo), Some(is_static_hi)) = (layout.is_static_lo, layout.is_static_hi) { - emitter.instruction("mov rax, r11"); // copy flags before extracting the static bit - emitter.instruction("and rax, 1"); // extract the static-member flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the static bit + emitter.instruction("and rax, 1"); // extract the static-member flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_static_lo); abi::emit_store_zero_to_address(emitter, "r10", is_static_hi); } - emitter.instruction("mov rax, r11"); // copy flags before extracting the public bit - emitter.instruction("shr rax, 1"); // move the public-member bit into position - emitter.instruction("and rax, 1"); // extract the public-member flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the public bit + emitter.instruction("shr rax, 1"); // move the public-member bit into position + emitter.instruction("and rax, 1"); // extract the public-member flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_public_lo); abi::emit_store_zero_to_address(emitter, "r10", is_public_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the protected bit - emitter.instruction("shr rax, 2"); // move the protected-member bit into position - emitter.instruction("and rax, 1"); // extract the protected-member flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the protected bit + emitter.instruction("shr rax, 2"); // move the protected-member bit into position + emitter.instruction("and rax, 1"); // extract the protected-member flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_protected_lo); abi::emit_store_zero_to_address(emitter, "r10", is_protected_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the private bit - emitter.instruction("shr rax, 3"); // move the private-member bit into position - emitter.instruction("and rax, 1"); // extract the private-member flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the private bit + emitter.instruction("shr rax, 3"); // move the private-member bit into position + emitter.instruction("and rax, 1"); // extract the private-member flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_private_lo); abi::emit_store_zero_to_address(emitter, "r10", is_private_hi); if let (Some(is_enum_case_lo), Some(is_enum_case_hi)) = (layout.is_enum_case_lo, layout.is_enum_case_hi) { - emitter.instruction("mov rax, r11"); // copy flags before extracting the enum-case bit - emitter.instruction("shr rax, 7"); // move the enum-case bit into position - emitter.instruction("and rax, 1"); // extract the enum-case flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the enum-case bit + emitter.instruction("shr rax, 7"); // move the enum-case bit into position + emitter.instruction("and rax, 1"); // extract the enum-case flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_enum_case_lo); abi::emit_store_zero_to_address(emitter, "r10", is_enum_case_hi); } if let (Some(is_readonly_lo), Some(is_readonly_hi)) = (layout.is_readonly_lo, layout.is_readonly_hi) { - emitter.instruction("mov rax, r11"); // copy flags before extracting the readonly bit - emitter.instruction("shr rax, 6"); // move the readonly-property bit into position - emitter.instruction("and rax, 1"); // extract the readonly-property flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the readonly bit + emitter.instruction("shr rax, 6"); // move the readonly-property bit into position + emitter.instruction("and rax, 1"); // extract the readonly-property flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_readonly_lo); abi::emit_store_zero_to_address(emitter, "r10", is_readonly_hi); } if let (Some(has_default_value_lo), Some(has_default_value_hi)) = (layout.has_default_value_lo, layout.has_default_value_hi) { - emitter.instruction("mov rax, r11"); // copy flags before extracting the default-value bit - emitter.instruction("shr rax, 8"); // move the default-value bit into position - emitter.instruction("and rax, 1"); // extract the default-value flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the default-value bit + emitter.instruction("shr rax, 8"); // move the default-value bit into position + emitter.instruction("and rax, 1"); // extract the default-value flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", has_default_value_lo); abi::emit_store_zero_to_address(emitter, "r10", has_default_value_hi); } if let (Some(is_promoted_lo), Some(is_promoted_hi)) = (layout.is_promoted_lo, layout.is_promoted_hi) { - emitter.instruction("mov rax, r11"); // copy flags before extracting the promoted-property bit - emitter.instruction("shr rax, 9"); // move the promoted-property bit into position - emitter.instruction("and rax, 1"); // extract the promoted-property flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the promoted-property bit + emitter.instruction("shr rax, 9"); // move the promoted-property bit into position + emitter.instruction("and rax, 1"); // extract the promoted-property flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_promoted_lo); abi::emit_store_zero_to_address(emitter, "r10", is_promoted_hi); } if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { if layout.required_parameter_count_lo.is_some() { - emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload PHP ReflectionMethod::getModifiers() bitmask + emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload PHP ReflectionMethod::getModifiers() bitmask } else { - emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP Reflection member getModifiers() bitmask + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP Reflection member getModifiers() bitmask } abi::emit_store_to_address(emitter, "rax", "r10", modifiers_lo); abi::emit_store_zero_to_address(emitter, "r10", modifiers_hi); } if let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) { - emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit - emitter.instruction("shr rax, 4"); // move the final-member bit into position - emitter.instruction("and rax, 1"); // extract the final-member flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the final bit + emitter.instruction("shr rax, 4"); // move the final-member bit into position + emitter.instruction("and rax, 1"); // extract the final-member flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); } if let (Some(is_abstract_lo), Some(is_abstract_hi)) = (layout.is_abstract_lo, layout.is_abstract_hi) { - emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit - emitter.instruction("shr rax, 5"); // move the abstract-member bit into position - emitter.instruction("and rax, 1"); // extract the abstract-member flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the abstract bit + emitter.instruction("shr rax, 5"); // move the abstract-member bit into position + emitter.instruction("and rax, 1"); // extract the abstract-member flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_abstract_lo); abi::emit_store_zero_to_address(emitter, "r10", is_abstract_hi); } @@ -1462,10 +1476,10 @@ fn emit_set_owner_low_bit_final_property_x86_64( let (Some(is_final_lo), Some(is_final_hi)) = (layout.is_final_lo, layout.is_final_hi) else { return; }; - emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection owner predicate flags - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer - emitter.instruction("mov rax, r11"); // copy flags before extracting bit-zero finality - emitter.instruction("and rax, 1"); // extract bit-zero finality as a boolean + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload Reflection owner predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting bit-zero finality + emitter.instruction("and rax, 1"); // extract bit-zero finality as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_final_lo); abi::emit_store_zero_to_address(emitter, "r10", is_final_hi); } @@ -1481,8 +1495,8 @@ fn emit_set_owner_required_parameter_count_property_aarch64( ) else { return; }; - emitter.instruction("ldr x10, [sp, #96]"); // reload ReflectionMethod required-parameter count - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x10, [sp, #96]"); // reload ReflectionMethod required-parameter count + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x10", "x9", required_parameter_count_lo); abi::emit_store_zero_to_address(emitter, "x9", required_parameter_count_hi); } @@ -1498,8 +1512,8 @@ fn emit_set_owner_required_parameter_count_property_x86_64( ) else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload ReflectionMethod required-parameter count - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload ReflectionMethod required-parameter count + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rax", "r10", required_parameter_count_lo); abi::emit_store_zero_to_address(emitter, "r10", required_parameter_count_hi); } @@ -1522,6 +1536,8 @@ fn emit_set_owner_parameter_property_aarch64( Some(has_type_hi), Some(has_default_value_lo), Some(has_default_value_hi), + Some(is_default_value_constant_lo), + Some(is_default_value_constant_hi), Some(is_promoted_lo), Some(is_promoted_hi), Some(allows_null_lo), @@ -1539,6 +1555,8 @@ fn emit_set_owner_parameter_property_aarch64( layout.has_type_hi, layout.has_default_value_lo, layout.has_default_value_hi, + layout.is_default_value_constant_lo, + layout.is_default_value_constant_hi, layout.is_promoted_lo, layout.is_promoted_hi, layout.allows_null_lo, @@ -1547,38 +1565,42 @@ fn emit_set_owner_parameter_property_aarch64( else { return; }; - emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionParameter predicate flags - emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer - emitter.instruction("ldr x10, [sp, #96]"); // reload the zero-based parameter position + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionParameter predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + emitter.instruction("ldr x10, [sp, #96]"); // reload the zero-based parameter position abi::emit_store_to_address(emitter, "x10", "x9", position_lo); abi::emit_store_zero_to_address(emitter, "x9", position_hi); - emitter.instruction("and x10, x11, #1"); // extract the optional-parameter flag as a boolean + emitter.instruction("and x10, x11, #1"); // extract the optional-parameter flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_optional_lo); abi::emit_store_zero_to_address(emitter, "x9", is_optional_hi); - emitter.instruction("lsr x10, x11, #1"); // move the variadic-parameter bit into position - emitter.instruction("and x10, x10, #1"); // extract the variadic-parameter flag as a boolean + emitter.instruction("lsr x10, x11, #1"); // move the variadic-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the variadic-parameter flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_variadic_lo); abi::emit_store_zero_to_address(emitter, "x9", is_variadic_hi); - emitter.instruction("lsr x10, x11, #2"); // move the by-reference-parameter bit into position - emitter.instruction("and x10, x10, #1"); // extract the by-reference-parameter flag as a boolean + emitter.instruction("lsr x10, x11, #2"); // move the by-reference-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the by-reference-parameter flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_passed_by_reference_lo); abi::emit_store_zero_to_address(emitter, "x9", is_passed_by_reference_hi); - emitter.instruction("lsr x10, x11, #3"); // move the typed-parameter bit into position - emitter.instruction("and x10, x10, #1"); // extract the typed-parameter flag as a boolean + emitter.instruction("lsr x10, x11, #3"); // move the typed-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the typed-parameter flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", has_type_lo); abi::emit_store_zero_to_address(emitter, "x9", has_type_hi); - emitter.instruction("lsr x10, x11, #4"); // move the default-value bit into position - emitter.instruction("and x10, x10, #1"); // extract the default-value flag as a boolean + emitter.instruction("lsr x10, x11, #4"); // move the default-value bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-value flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", has_default_value_lo); abi::emit_store_zero_to_address(emitter, "x9", has_default_value_hi); - emitter.instruction("lsr x10, x11, #5"); // move the promoted-parameter bit into position - emitter.instruction("and x10, x10, #1"); // extract the promoted-parameter flag as a boolean + emitter.instruction("lsr x10, x11, #5"); // move the promoted-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the promoted-parameter flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_promoted_lo); abi::emit_store_zero_to_address(emitter, "x9", is_promoted_hi); - emitter.instruction("lsr x10, x11, #6"); // move the nullable-parameter bit into position - emitter.instruction("and x10, x10, #1"); // extract the nullable-parameter flag as a boolean + emitter.instruction("lsr x10, x11, #6"); // move the nullable-parameter bit into position + emitter.instruction("and x10, x10, #1"); // extract the nullable-parameter flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", allows_null_lo); abi::emit_store_zero_to_address(emitter, "x9", allows_null_hi); + emitter.instruction("lsr x10, x11, #7"); // move the default-constant bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-constant flag as a boolean + abi::emit_store_to_address(emitter, "x10", "x9", is_default_value_constant_lo); + abi::emit_store_zero_to_address(emitter, "x9", is_default_value_constant_hi); } /// Stores incoming ARM64 ReflectionParameter type metadata. @@ -1591,12 +1613,12 @@ fn emit_set_owner_parameter_type_property_aarch64( else { return; }; - emitter.instruction("ldr x0, [sp, #120]"); // reload the boxed ReflectionParameter type value - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null type metadata - emitter.instruction("str x0, [sp, #40]"); // save the boxed type value across incref - emitter.instruction("bl __rt_incref"); // retain the boxed type value for ReflectionParameter storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed type value - emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + emitter.instruction("ldr x0, [sp, #120]"); // reload the boxed ReflectionParameter type value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null type metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed type value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed type value for ReflectionParameter storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed type value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer abi::emit_store_to_address(emitter, "x1", "x9", type_lo); abi::emit_store_zero_to_address(emitter, "x9", type_hi); } @@ -1611,16 +1633,45 @@ fn emit_set_owner_parameter_default_property_aarch64( else { return; }; - emitter.instruction("ldr x0, [sp, #128]"); // reload the boxed ReflectionParameter default value - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null default metadata - emitter.instruction("str x0, [sp, #40]"); // save the boxed default value across incref - emitter.instruction("bl __rt_incref"); // retain the boxed default value for ReflectionParameter storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed default value - emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + emitter.instruction("ldr x0, [sp, #128]"); // reload the boxed ReflectionParameter default value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null default metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed default value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed default value for ReflectionParameter storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed default value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer abi::emit_store_to_address(emitter, "x1", "x9", default_lo); abi::emit_store_zero_to_address(emitter, "x9", default_hi); } +/// Stores incoming ARM64 ReflectionParameter default-constant-name metadata. +fn emit_set_owner_parameter_default_constant_name_property_aarch64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let done_label = "__elephc_eval_reflection_owner_parameter_default_constant_name_done"; + let (Some(name_lo), Some(name_hi)) = ( + layout.default_value_constant_name_lo, + layout.default_value_constant_name_hi, + ) else { + return; + }; + emitter.instruction("ldr x10, [sp, #48]"); // reload ReflectionParameter predicate flags + emitter.instruction("lsr x10, x10, #7"); // move the default-constant bit into position + emitter.instruction("and x10, x10, #1"); // extract the default-constant flag as a boolean + emitter.instruction(&format!("cbz x10, {}", done_label)); // keep the empty name when no constant default exists + emitter.instruction("ldr x0, [sp, #56]"); // reload the boxed ReflectionParameter default-constant name + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null default-constant metadata + emitter.instruction("bl __rt_mixed_unbox"); // expose the default-constant name string payload + emitter.instruction("cmp x0, #1"); // runtime tag 1 means string + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-string default-constant metadata + emitter.instruction("bl __rt_str_persist"); // copy the default-constant name bytes for parameter storage + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "x1", "x9", name_lo); + abi::emit_store_to_address(emitter, "x2", "x9", name_hi); + emitter.label(done_label); +} + /// Stores incoming ARM64 ReflectionNamedType predicate flags. fn emit_set_owner_named_type_flags_property_aarch64( emitter: &mut Emitter, @@ -1631,17 +1682,17 @@ fn emit_set_owner_named_type_flags_property_aarch64( else { return; }; - emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionNamedType predicate flags - emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionNamedType object pointer - emitter.instruction("and x10, x11, #1"); // extract the nullable-type flag as a boolean + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionNamedType predicate flags + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionNamedType object pointer + emitter.instruction("and x10, x11, #1"); // extract the nullable-type flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", allows_null_lo); abi::emit_store_zero_to_address(emitter, "x9", allows_null_hi); let (Some(is_builtin_lo), Some(is_builtin_hi)) = (layout.is_builtin_lo, layout.is_builtin_hi) else { return; }; - emitter.instruction("lsr x10, x11, #1"); // move the builtin-type bit into position - emitter.instruction("and x10, x10, #1"); // extract the builtin-type flag as a boolean + emitter.instruction("lsr x10, x11, #1"); // move the builtin-type bit into position + emitter.instruction("and x10, x10, #1"); // extract the builtin-type flag as a boolean abi::emit_store_to_address(emitter, "x10", "x9", is_builtin_lo); abi::emit_store_zero_to_address(emitter, "x9", is_builtin_hi); } @@ -1661,6 +1712,8 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl Some(has_type_hi), Some(has_default_value_lo), Some(has_default_value_hi), + Some(is_default_value_constant_lo), + Some(is_default_value_constant_hi), Some(is_promoted_lo), Some(is_promoted_hi), Some(allows_null_lo), @@ -1678,6 +1731,8 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl layout.has_type_hi, layout.has_default_value_lo, layout.has_default_value_hi, + layout.is_default_value_constant_lo, + layout.is_default_value_constant_hi, layout.is_promoted_lo, layout.is_promoted_hi, layout.allows_null_lo, @@ -1686,45 +1741,50 @@ fn emit_set_owner_parameter_property_x86_64(emitter: &mut Emitter, layout: &Refl else { return; }; - emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionParameter predicate flags - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer - emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload the zero-based parameter position + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionParameter predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload the zero-based parameter position abi::emit_store_to_address(emitter, "rax", "r10", position_lo); abi::emit_store_zero_to_address(emitter, "r10", position_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the optional bit - emitter.instruction("and rax, 1"); // extract the optional-parameter flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the optional bit + emitter.instruction("and rax, 1"); // extract the optional-parameter flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_optional_lo); abi::emit_store_zero_to_address(emitter, "r10", is_optional_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the variadic bit - emitter.instruction("shr rax, 1"); // move the variadic-parameter bit into position - emitter.instruction("and rax, 1"); // extract the variadic-parameter flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the variadic bit + emitter.instruction("shr rax, 1"); // move the variadic-parameter bit into position + emitter.instruction("and rax, 1"); // extract the variadic-parameter flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_variadic_lo); abi::emit_store_zero_to_address(emitter, "r10", is_variadic_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the by-reference bit - emitter.instruction("shr rax, 2"); // move the by-reference-parameter bit into position - emitter.instruction("and rax, 1"); // extract the by-reference-parameter flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the by-reference bit + emitter.instruction("shr rax, 2"); // move the by-reference-parameter bit into position + emitter.instruction("and rax, 1"); // extract the by-reference-parameter flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_passed_by_reference_lo); abi::emit_store_zero_to_address(emitter, "r10", is_passed_by_reference_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the typed bit - emitter.instruction("shr rax, 3"); // move the typed-parameter bit into position - emitter.instruction("and rax, 1"); // extract the typed-parameter flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the typed bit + emitter.instruction("shr rax, 3"); // move the typed-parameter bit into position + emitter.instruction("and rax, 1"); // extract the typed-parameter flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", has_type_lo); abi::emit_store_zero_to_address(emitter, "r10", has_type_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the default-value bit - emitter.instruction("shr rax, 4"); // move the default-value bit into position - emitter.instruction("and rax, 1"); // extract the default-value flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the default-value bit + emitter.instruction("shr rax, 4"); // move the default-value bit into position + emitter.instruction("and rax, 1"); // extract the default-value flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", has_default_value_lo); abi::emit_store_zero_to_address(emitter, "r10", has_default_value_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the promoted bit - emitter.instruction("shr rax, 5"); // move the promoted-parameter bit into position - emitter.instruction("and rax, 1"); // extract the promoted-parameter flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the promoted bit + emitter.instruction("shr rax, 5"); // move the promoted-parameter bit into position + emitter.instruction("and rax, 1"); // extract the promoted-parameter flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_promoted_lo); abi::emit_store_zero_to_address(emitter, "r10", is_promoted_hi); - emitter.instruction("mov rax, r11"); // copy flags before extracting the nullable bit - emitter.instruction("shr rax, 6"); // move the nullable-parameter bit into position - emitter.instruction("and rax, 1"); // extract the nullable-parameter flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the nullable bit + emitter.instruction("shr rax, 6"); // move the nullable-parameter bit into position + emitter.instruction("and rax, 1"); // extract the nullable-parameter flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", allows_null_lo); abi::emit_store_zero_to_address(emitter, "r10", allows_null_hi); + emitter.instruction("mov rax, r11"); // copy flags before extracting the default-constant bit + emitter.instruction("shr rax, 7"); // move the default-constant bit into position + emitter.instruction("and rax, 1"); // extract the default-constant flag as a boolean + abi::emit_store_to_address(emitter, "rax", "r10", is_default_value_constant_lo); + abi::emit_store_zero_to_address(emitter, "r10", is_default_value_constant_hi); } /// Stores incoming x86_64 ReflectionParameter type metadata. @@ -1737,13 +1797,13 @@ fn emit_set_owner_parameter_type_property_x86_64( else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 128]"); // reload the boxed ReflectionParameter type value - emitter.instruction("test rax, rax"); // check whether the boxed type value is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null type metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed type value across incref - emitter.instruction("call __rt_incref"); // retain the boxed type value for ReflectionParameter storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed type value - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 128]"); // reload the boxed ReflectionParameter type value + emitter.instruction("test rax, rax"); // check whether the boxed type value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null type metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed type value across incref + emitter.instruction("call __rt_incref"); // retain the boxed type value for ReflectionParameter storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed type value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer abi::emit_store_to_address(emitter, "rdi", "r10", type_lo); abi::emit_store_zero_to_address(emitter, "r10", type_hi); } @@ -1758,17 +1818,48 @@ fn emit_set_owner_parameter_default_property_x86_64( else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 136]"); // reload the boxed ReflectionParameter default value - emitter.instruction("test rax, rax"); // check whether the boxed default value is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null default metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed default value across incref - emitter.instruction("call __rt_incref"); // retain the boxed default value for ReflectionParameter storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed default value - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 136]"); // reload the boxed ReflectionParameter default value + emitter.instruction("test rax, rax"); // check whether the boxed default value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null default metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed default value across incref + emitter.instruction("call __rt_incref"); // retain the boxed default value for ReflectionParameter storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed default value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer abi::emit_store_to_address(emitter, "rdi", "r10", default_lo); abi::emit_store_zero_to_address(emitter, "r10", default_hi); } +/// Stores incoming x86_64 ReflectionParameter default-constant-name metadata. +fn emit_set_owner_parameter_default_constant_name_property_x86_64( + emitter: &mut Emitter, + layout: &ReflectionOwnerLayout, + fail_label: &str, +) { + let done_label = "__elephc_eval_reflection_owner_parameter_default_constant_name_done_x"; + let (Some(name_lo), Some(name_hi)) = ( + layout.default_value_constant_name_lo, + layout.default_value_constant_name_hi, + ) else { + return; + }; + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionParameter predicate flags + emitter.instruction("shr r11, 7"); // move the default-constant bit into position + emitter.instruction("and r11, 1"); // extract the default-constant flag as a boolean + emitter.instruction(&format!("jz {}", done_label)); // keep the empty name when no constant default exists + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the boxed ReflectionParameter default-constant name + emitter.instruction("test rax, rax"); // check whether the boxed default-constant name is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null default-constant metadata + emitter.instruction("call __rt_mixed_unbox"); // expose the default-constant name string payload + emitter.instruction("cmp rax, 1"); // runtime tag 1 means string + emitter.instruction(&format!("jne {}", fail_label)); // reject non-string default-constant metadata + emitter.instruction("mov rax, rdi"); // move the string pointer into the x86_64 persist argument + emitter.instruction("call __rt_str_persist"); // copy the default-constant name bytes for parameter storage + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionParameter object pointer + abi::emit_store_to_address(emitter, "rax", "r10", name_lo); + abi::emit_store_to_address(emitter, "rdx", "r10", name_hi); + emitter.label(done_label); +} + /// Stores incoming x86_64 ReflectionNamedType predicate flags. fn emit_set_owner_named_type_flags_property_x86_64( emitter: &mut Emitter, @@ -1779,19 +1870,19 @@ fn emit_set_owner_named_type_flags_property_x86_64( else { return; }; - emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionNamedType predicate flags - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionNamedType object pointer - emitter.instruction("mov rax, r11"); // copy flags before extracting the nullable bit - emitter.instruction("and rax, 1"); // extract the nullable-type flag as a boolean + emitter.instruction("mov r11, QWORD PTR [rbp - 56]"); // reload ReflectionNamedType predicate flags + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionNamedType object pointer + emitter.instruction("mov rax, r11"); // copy flags before extracting the nullable bit + emitter.instruction("and rax, 1"); // extract the nullable-type flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", allows_null_lo); abi::emit_store_zero_to_address(emitter, "r10", allows_null_hi); let (Some(is_builtin_lo), Some(is_builtin_hi)) = (layout.is_builtin_lo, layout.is_builtin_hi) else { return; }; - emitter.instruction("mov rax, r11"); // copy flags before extracting the builtin bit - emitter.instruction("shr rax, 1"); // move the builtin-type bit into position - emitter.instruction("and rax, 1"); // extract the builtin-type flag as a boolean + emitter.instruction("mov rax, r11"); // copy flags before extracting the builtin bit + emitter.instruction("shr rax, 1"); // move the builtin-type bit into position + emitter.instruction("and rax, 1"); // extract the builtin-type flag as a boolean abi::emit_store_to_address(emitter, "rax", "r10", is_builtin_lo); abi::emit_store_zero_to_address(emitter, "r10", is_builtin_hi); } @@ -1830,16 +1921,16 @@ fn emit_set_owner_metadata_array_slot_aarch64( high_offset: usize, fail_label: &str, ) { - emitter.instruction(&format!("ldr x0, [sp, #{}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null metadata-name arrays - emitter.instruction("bl __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer - emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array metadata-name metadata - emitter.instruction("str x1, [sp, #40]"); // save the unboxed metadata-name array across incref - emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register - emitter.instruction("bl __rt_incref"); // retain the metadata-name array for ReflectionClass storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained metadata-name array payload - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction(&format!("ldr x0, [sp, #{}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null metadata-name arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array metadata-name metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed metadata-name array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the metadata-name array for ReflectionClass storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained metadata-name array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", low_offset); abi::emit_load_int_immediate(emitter, "x10", 4); abi::emit_store_to_address(emitter, "x10", "x9", high_offset); @@ -1884,17 +1975,17 @@ fn emit_set_owner_metadata_array_slot_x86_64( } else { format!("+ {}", boxed_slot) }; - emitter.instruction(&format!("mov rax, QWORD PTR [rbp {}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array - emitter.instruction("test rax, rax"); // check whether the boxed metadata-name array is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null metadata-name arrays - emitter.instruction("call __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer - emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("jne {}", fail_label)); // reject non-array metadata-name metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed metadata-name array across incref - emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register - emitter.instruction("call __rt_incref"); // retain the metadata-name array for ReflectionClass storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained metadata-name array payload - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction(&format!("mov rax, QWORD PTR [rbp {}]", boxed_slot)); // reload the boxed ReflectionClass metadata-name array + emitter.instruction("test rax, rax"); // check whether the boxed metadata-name array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null metadata-name arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the metadata-name array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array metadata-name metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed metadata-name array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the metadata-name array for ReflectionClass storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained metadata-name array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", low_offset); abi::emit_load_int_immediate(emitter, "r11", 4); abi::emit_store_to_address(emitter, "r11", "r10", high_offset); @@ -1909,12 +2000,12 @@ fn emit_set_owner_constructor_property_aarch64( let (Some(low), Some(high)) = (layout.constructor_lo, layout.constructor_hi) else { return; }; - emitter.instruction("ldr x0, [sp, #224]"); // reload the boxed ReflectionClass constructor value - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null constructor metadata - emitter.instruction("str x0, [sp, #40]"); // save the boxed constructor value across incref - emitter.instruction("bl __rt_incref"); // retain the boxed constructor value for ReflectionClass storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed constructor value - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x0, [sp, #224]"); // reload the boxed ReflectionClass constructor value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null constructor metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed constructor value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed constructor value for ReflectionClass storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed constructor value + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", low); abi::emit_store_zero_to_address(emitter, "x9", high); } @@ -1928,13 +2019,13 @@ fn emit_set_owner_constructor_property_x86_64( let (Some(low), Some(high)) = (layout.constructor_lo, layout.constructor_hi) else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp + 96]"); // reload the boxed ReflectionClass constructor value - emitter.instruction("test rax, rax"); // check whether the boxed constructor value is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null constructor metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed constructor value across incref - emitter.instruction("call __rt_incref"); // retain the boxed constructor value for ReflectionClass storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed constructor value - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp + 96]"); // reload the boxed ReflectionClass constructor value + emitter.instruction("test rax, rax"); // check whether the boxed constructor value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null constructor metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed constructor value across incref + emitter.instruction("call __rt_incref"); // retain the boxed constructor value for ReflectionClass storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed constructor value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", low); abi::emit_store_zero_to_address(emitter, "r10", high); } @@ -1948,12 +2039,12 @@ fn emit_set_owner_parent_class_property_aarch64( let (Some(low), Some(high)) = (layout.parent_class_lo, layout.parent_class_hi) else { return; }; - emitter.instruction("ldr x0, [sp, #136]"); // reload the boxed ReflectionClass parent value - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null parent metadata - emitter.instruction("str x0, [sp, #40]"); // save the boxed parent value across incref - emitter.instruction("bl __rt_incref"); // retain the boxed parent value for ReflectionClass storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed parent value - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x0, [sp, #136]"); // reload the boxed ReflectionClass parent value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null parent metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed parent value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed parent value for ReflectionClass storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed parent value + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", low); abi::emit_store_zero_to_address(emitter, "x9", high); } @@ -1967,13 +2058,13 @@ fn emit_set_owner_parent_class_property_x86_64( let (Some(low), Some(high)) = (layout.parent_class_lo, layout.parent_class_hi) else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 144]"); // reload the boxed ReflectionClass parent value - emitter.instruction("test rax, rax"); // check whether the boxed parent value is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null parent metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed parent value across incref - emitter.instruction("call __rt_incref"); // retain the boxed parent value for ReflectionClass storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed parent value - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 144]"); // reload the boxed ReflectionClass parent value + emitter.instruction("test rax, rax"); // check whether the boxed parent value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null parent metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed parent value across incref + emitter.instruction("call __rt_incref"); // retain the boxed parent value for ReflectionClass storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed parent value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", low); abi::emit_store_zero_to_address(emitter, "r10", high); } @@ -1988,12 +2079,12 @@ fn emit_set_owner_declaring_function_property_aarch64( else { return; }; - emitter.instruction("ldr x0, [sp, #80]"); // reload the boxed ReflectionParameter declaring function - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null declaring-function metadata - emitter.instruction("str x0, [sp, #40]"); // save the boxed declaring function across incref - emitter.instruction("bl __rt_incref"); // retain the declaring function for ReflectionParameter storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained declaring function value - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x0, [sp, #80]"); // reload the boxed ReflectionParameter declaring function + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null declaring-function metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed declaring function across incref + emitter.instruction("bl __rt_incref"); // retain the declaring function for ReflectionParameter storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained declaring function value + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", low); abi::emit_store_zero_to_address(emitter, "x9", high); } @@ -2008,13 +2099,13 @@ fn emit_set_owner_declaring_function_property_x86_64( else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the boxed ReflectionParameter declaring function - emitter.instruction("test rax, rax"); // check whether the declaring-function value is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null declaring-function metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed declaring function across incref - emitter.instruction("call __rt_incref"); // retain the declaring function for ReflectionParameter storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained declaring function value - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 88]"); // reload the boxed ReflectionParameter declaring function + emitter.instruction("test rax, rax"); // check whether the declaring-function value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null declaring-function metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed declaring function across incref + emitter.instruction("call __rt_incref"); // retain the declaring function for ReflectionParameter storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained declaring function value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", low); abi::emit_store_zero_to_address(emitter, "r10", high); } @@ -2028,12 +2119,12 @@ fn emit_set_owner_constant_value_property_aarch64( let (Some(low), Some(high)) = (layout.value_lo, layout.value_hi) else { return; }; - emitter.instruction("ldr x0, [sp, #56]"); // reload the boxed ReflectionClassConstant value - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null constant-value metadata - emitter.instruction("str x0, [sp, #40]"); // save the boxed constant value across incref - emitter.instruction("bl __rt_incref"); // retain the boxed constant value for ReflectionClassConstant storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed constant value - emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionClassConstant object pointer + emitter.instruction("ldr x0, [sp, #56]"); // reload the boxed ReflectionClassConstant value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null constant-value metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed constant value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed constant value for ReflectionClassConstant storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed constant value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionClassConstant object pointer abi::emit_store_to_address(emitter, "x1", "x9", low); abi::emit_store_zero_to_address(emitter, "x9", high); } @@ -2047,13 +2138,13 @@ fn emit_set_owner_constant_value_property_x86_64( let (Some(low), Some(high)) = (layout.value_lo, layout.value_hi) else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the boxed ReflectionClassConstant value - emitter.instruction("test rax, rax"); // check whether the boxed constant value is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null constant-value metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed constant value across incref - emitter.instruction("call __rt_incref"); // retain the boxed constant value for ReflectionClassConstant storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed constant value - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionClassConstant object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 64]"); // reload the boxed ReflectionClassConstant value + emitter.instruction("test rax, rax"); // check whether the boxed constant value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null constant-value metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed constant value across incref + emitter.instruction("call __rt_incref"); // retain the boxed constant value for ReflectionClassConstant storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed constant value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionClassConstant object pointer abi::emit_store_to_address(emitter, "rdi", "r10", low); abi::emit_store_zero_to_address(emitter, "r10", high); } @@ -2067,12 +2158,12 @@ fn emit_set_owner_backing_value_property_aarch64( let (Some(low), Some(high)) = (layout.backing_value_lo, layout.backing_value_hi) else { return; }; - emitter.instruction("ldr x0, [sp, #64]"); // reload the boxed ReflectionEnumBackedCase backing value - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null backing-value metadata - emitter.instruction("str x0, [sp, #40]"); // save the boxed backing value across incref - emitter.instruction("bl __rt_incref"); // retain the boxed backing value for ReflectionEnumBackedCase storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed backing value - emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionEnumBackedCase object pointer + emitter.instruction("ldr x0, [sp, #64]"); // reload the boxed ReflectionEnumBackedCase backing value + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null backing-value metadata + emitter.instruction("str x0, [sp, #40]"); // save the boxed backing value across incref + emitter.instruction("bl __rt_incref"); // retain the boxed backing value for ReflectionEnumBackedCase storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained boxed backing value + emitter.instruction("ldr x9, [sp, #32]"); // reload the ReflectionEnumBackedCase object pointer abi::emit_store_to_address(emitter, "x1", "x9", low); abi::emit_store_zero_to_address(emitter, "x9", high); } @@ -2086,13 +2177,13 @@ fn emit_set_owner_backing_value_property_x86_64( let (Some(low), Some(high)) = (layout.backing_value_lo, layout.backing_value_hi) else { return; }; - emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the boxed ReflectionEnumBackedCase backing value - emitter.instruction("test rax, rax"); // check whether the boxed backing value is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null backing-value metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed backing value across incref - emitter.instruction("call __rt_incref"); // retain the boxed backing value for ReflectionEnumBackedCase storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed backing value - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionEnumBackedCase object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 72]"); // reload the boxed ReflectionEnumBackedCase backing value + emitter.instruction("test rax, rax"); // check whether the boxed backing value is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null backing-value metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rax"); // save the boxed backing value across incref + emitter.instruction("call __rt_incref"); // retain the boxed backing value for ReflectionEnumBackedCase storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained boxed backing value + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the ReflectionEnumBackedCase object pointer abi::emit_store_to_address(emitter, "rdi", "r10", low); abi::emit_store_zero_to_address(emitter, "r10", high); } @@ -2103,16 +2194,16 @@ fn emit_set_owner_attrs_property_aarch64( layout: &ReflectionOwnerLayout, fail_label: &str, ) { - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed ReflectionAttribute array - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null attribute arrays - emitter.instruction("bl __rt_mixed_unbox"); // expose the attribute array tag and payload pointer - emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array attribute metadata - emitter.instruction("str x1, [sp, #40]"); // save the unboxed attribute array across incref - emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register - emitter.instruction("bl __rt_incref"); // retain the attribute array for Reflection owner storage - emitter.instruction("ldr x1, [sp, #40]"); // reload the retained attribute array payload - emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed ReflectionAttribute array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("bl __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("str x1, [sp, #40]"); // save the unboxed attribute array across incref + emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register + emitter.instruction("bl __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("ldr x1, [sp, #40]"); // reload the retained attribute array payload + emitter.instruction("ldr x9, [sp, #32]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "x1", "x9", layout.attrs_lo); abi::emit_load_int_immediate(emitter, "x10", 4); abi::emit_store_to_address(emitter, "x10", "x9", layout.attrs_hi); @@ -2124,17 +2215,17 @@ fn emit_set_owner_attrs_property_x86_64( layout: &ReflectionOwnerLayout, fail_label: &str, ) { - emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed ReflectionAttribute array - emitter.instruction("test rax, rax"); // check whether the boxed attribute array is null - emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null attribute arrays - emitter.instruction("call __rt_mixed_unbox"); // expose the attribute array tag and payload pointer - emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array - emitter.instruction(&format!("jne {}", fail_label)); // reject non-array attribute metadata - emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed attribute array across incref - emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register - emitter.instruction("call __rt_incref"); // retain the attribute array for Reflection owner storage - emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained attribute array payload - emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed ReflectionAttribute array + emitter.instruction("test rax, rax"); // check whether the boxed attribute array is null + emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null attribute arrays + emitter.instruction("call __rt_mixed_unbox"); // expose the attribute array tag and payload pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("jne {}", fail_label)); // reject non-array attribute metadata + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // save the unboxed attribute array across incref + emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register + emitter.instruction("call __rt_incref"); // retain the attribute array for Reflection owner storage + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the retained attribute array payload + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload the Reflection owner object pointer abi::emit_store_to_address(emitter, "rdi", "r10", layout.attrs_lo); abi::emit_load_int_immediate(emitter, "r11", 4); abi::emit_store_to_address(emitter, "r11", "r10", layout.attrs_hi); diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 3f3bfeddd5..3ca07060b4 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -112,6 +112,7 @@ struct ReflectionParameterMember { allows_null: bool, type_metadata: Option, default_value: Option, + default_value_constant_name: Option, } /// Metadata needed for `ReflectionParameter::getDeclaringFunction()`. @@ -533,7 +534,7 @@ fn reflection_class_metadata_for_name( reflection_class_default_property_members(info, &property_names); let constant_reflection_members = reflection_class_constant_reflection_members(ctx, class_name, info)?; - let method_members = reflection_class_method_members(info, &method_names); + let method_members = reflection_class_method_members(ctx, class_name, info, &method_names)?; let property_members = reflection_class_property_members(ctx, class_name, info, &property_names); let constructor_member = reflection_constructor_member(&method_members); @@ -595,7 +596,10 @@ fn reflection_class_metadata_for_name( .module .interface_infos .get(interface_name) - .map(|info| reflection_interface_method_members(info, interface_name, &method_names)) + .map(|info| { + reflection_interface_method_members(ctx, info, interface_name, &method_names) + }) + .transpose()? .unwrap_or_else(|| default_method_members(&method_names, true, interface_name)); let property_members = default_property_members(&property_names, true, interface_name); let constructor_member = reflection_constructor_member(&method_members); @@ -661,7 +665,8 @@ fn reflection_class_metadata_for_name( .module .declared_trait_methods .get(trait_name) - .map(|methods| reflection_trait_method_members(methods, trait_name, &method_names)) + .map(|methods| reflection_trait_method_members(ctx, methods, trait_name, &method_names)) + .transpose()? .unwrap_or_else(|| default_method_members(&method_names, false, trait_name)); let property_members = default_property_members(&property_names, false, trait_name); let constructor_member = reflection_constructor_member(&method_members); @@ -759,11 +764,14 @@ fn reflection_function_metadata( metadata.attr_names = function.attribute_names.clone(); metadata.attr_args = function.attribute_args.clone(); metadata.parameter_members = reflection_parameter_members_with_declaring_function( + ctx, signature, + "", + None, None, Some(declaring_function), &[], - ); + )?; metadata.required_parameter_count = required_parameter_count; Ok(metadata) } @@ -783,14 +791,16 @@ fn reflection_method_metadata( let method_name = const_required_string_operand(ctx, method_operand, "ReflectionMethod")?; let method_key = php_symbol_key(&method_name); if let Some((_, info)) = resolve_reflection_class(ctx, &reflected_class) { - if let Some(member) = reflection_class_method_member(info, &method_key) { + if let Some(member) = + reflection_class_method_member(ctx, &reflected_class, info, &method_key)? + { return Ok(reflection_method_owner_metadata(&method_name, member)); } } if let Some(interface_name) = resolve_reflection_interface(ctx, &reflected_class) { if let Some(info) = ctx.module.interface_infos.get(interface_name) { if let Some(member) = - reflection_interface_method_member(info, interface_name, &method_key) + reflection_interface_method_member(ctx, info, interface_name, &method_key)? { return Ok(reflection_method_owner_metadata(&method_name, member)); } @@ -798,7 +808,9 @@ fn reflection_method_metadata( } if let Some(trait_name) = resolve_reflection_trait(ctx, &reflected_class) { if let Some(methods) = ctx.module.declared_trait_methods.get(trait_name) { - if let Some(member) = reflection_trait_method_member(methods, trait_name, &method_key) { + if let Some(member) = + reflection_trait_method_member(ctx, methods, trait_name, &method_key)? + { return Ok(reflection_method_owner_metadata(&method_name, member)); } } @@ -929,7 +941,7 @@ fn reflection_parameter_metadata( let method_name = const_required_string_operand(ctx, method_operand, "ReflectionParameter")?; let selector = const_parameter_selector_operand(ctx, parameter_operand)?; let method_key = php_symbol_key(&method_name); - let method = reflection_method_member_for_class_like(ctx, &reflected_class, &method_key); + let method = reflection_method_member_for_class_like(ctx, &reflected_class, &method_key)?; let Some(parameter) = method .as_ref() .and_then(|method| reflection_parameter_member_for_selector(&method.parameters, selector)) @@ -967,11 +979,14 @@ fn reflection_function_parameter_metadata( required_parameter_count: reflection_required_parameter_count(signature), }; let parameters = reflection_parameter_members_with_declaring_function( + ctx, signature, + "", + None, None, Some(declaring_function), &[], - ); + )?; let Some(parameter) = reflection_parameter_member_for_selector(¶meters, selector) else { return Ok(empty_reflection_metadata()); }; @@ -993,23 +1008,31 @@ fn reflection_method_member_for_class_like( ctx: &FunctionContext<'_>, reflected_class: &str, method_key: &str, -) -> Option { +) -> Result> { if let Some((_, info)) = resolve_reflection_class(ctx, reflected_class) { - return reflection_class_method_member(info, method_key); + return reflection_class_method_member(ctx, reflected_class, info, method_key); } if let Some(interface_name) = resolve_reflection_interface(ctx, reflected_class) { return ctx .module .interface_infos .get(interface_name) - .and_then(|info| reflection_interface_method_member(info, interface_name, method_key)); + .map(|info| reflection_interface_method_member(ctx, info, interface_name, method_key)) + .transpose() + .map(Option::flatten); } - resolve_reflection_trait(ctx, reflected_class).and_then(|trait_name| { - ctx.module - .declared_trait_methods - .get(trait_name) - .and_then(|methods| reflection_trait_method_member(methods, trait_name, method_key)) - }) + resolve_reflection_trait(ctx, reflected_class) + .and_then(|trait_name| { + ctx.module + .declared_trait_methods + .get(trait_name) + .map(|methods| (trait_name, methods)) + }) + .map(|(trait_name, methods)| { + reflection_trait_method_member(ctx, methods, trait_name, method_key) + }) + .transpose() + .map(Option::flatten) } /// Returns the selected parameter member by PHP name or zero-based position. @@ -2159,25 +2182,35 @@ fn reflection_property_member_flags( /// Builds ReflectionMethod array entries for the methods visible on one class. fn reflection_class_method_members( + ctx: &FunctionContext<'_>, + class_name: &str, info: &crate::types::ClassInfo, method_names: &[String], -) -> Vec { - method_names - .iter() - .filter_map(|method_name| reflection_class_method_member(info, method_name)) - .collect() +) -> Result> { + let mut members = Vec::new(); + for method_name in method_names { + if let Some(member) = reflection_class_method_member(ctx, class_name, info, method_name)? { + members.push(member); + } + } + Ok(members) } /// Builds one ReflectionMethod array entry from class metadata. fn reflection_class_method_member( + ctx: &FunctionContext<'_>, + class_name: &str, info: &crate::types::ClassInfo, method_name: &str, -) -> Option { +) -> Result> { let method_key = php_symbol_key(method_name); let sig = info .methods .get(&method_key) - .or_else(|| info.static_methods.get(&method_key))?; + .or_else(|| info.static_methods.get(&method_key)); + let Some(sig) = sig else { + return Ok(None); + }; let declaring_class_name = reflection_method_declaring_class_name(info, &method_key); let attr_names = info .method_attribute_names @@ -2189,7 +2222,9 @@ fn reflection_class_method_member( .get(&method_key) .cloned() .unwrap_or_default(); - let flags = reflection_method_member_flags(info, &method_key)?; + let Some(flags) = reflection_method_member_flags(info, &method_key) else { + return Ok(None); + }; let required_parameter_count = reflection_required_parameter_count(sig); let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), @@ -2200,12 +2235,15 @@ fn reflection_class_method_member( required_parameter_count, }; let parameters = reflection_parameter_members_with_declaring_class( + ctx, sig, + class_name, + Some(info), declaring_class_name.as_deref(), Some(declaring_function), &reflection_promoted_constructor_parameter_names(info, &method_key), - ); - Some(ReflectionListedMember { + )?; + Ok(Some(ReflectionListedMember { name: method_key.clone(), declaring_class_name, attr_names, @@ -2218,35 +2256,43 @@ fn reflection_class_method_member( default_value: None, required_parameter_count, parameters, - }) + })) } /// Builds ReflectionMethod array entries for methods declared by an interface. fn reflection_interface_method_members( + ctx: &FunctionContext<'_>, info: &InterfaceInfo, interface_name: &str, method_names: &[String], -) -> Vec { - method_names - .iter() - .filter_map(|method_name| { - reflection_interface_method_member(info, interface_name, method_name) - }) - .collect() +) -> Result> { + let mut members = Vec::new(); + for method_name in method_names { + if let Some(member) = + reflection_interface_method_member(ctx, info, interface_name, method_name)? + { + members.push(member); + } + } + Ok(members) } /// Builds one ReflectionMethod array entry from interface metadata. fn reflection_interface_method_member( + ctx: &FunctionContext<'_>, info: &InterfaceInfo, interface_name: &str, method_name: &str, -) -> Option { +) -> Result> { let method_key = php_symbol_key(method_name); - let (sig, is_static) = info + let Some((sig, is_static)) = info .methods .get(&method_key) .map(|sig| (sig, false)) - .or_else(|| info.static_methods.get(&method_key).map(|sig| (sig, true)))?; + .or_else(|| info.static_methods.get(&method_key).map(|sig| (sig, true))) + else { + return Ok(None); + }; let declaring_class_name = info .method_declaring_interfaces .get(&method_key) @@ -2264,12 +2310,15 @@ fn reflection_interface_method_member( required_parameter_count, }; let parameters = reflection_parameter_members_with_declaring_class( + ctx, sig, + declaring_class_name.as_str(), + None, Some(declaring_class_name.as_str()), Some(declaring_function), &[], - ); - Some(ReflectionListedMember { + )?; + Ok(Some(ReflectionListedMember { name: method_key, declaring_class_name: Some(declaring_class_name), attr_names: Vec::new(), @@ -2282,29 +2331,37 @@ fn reflection_interface_method_member( default_value: None, required_parameter_count, parameters, - }) + })) } /// Builds ReflectionMethod array entries for methods declared by a trait. fn reflection_trait_method_members( + ctx: &FunctionContext<'_>, methods: &std::collections::HashMap, trait_name: &str, method_names: &[String], -) -> Vec { - method_names - .iter() - .filter_map(|method_name| reflection_trait_method_member(methods, trait_name, method_name)) - .collect() +) -> Result> { + let mut members = Vec::new(); + for method_name in method_names { + if let Some(member) = reflection_trait_method_member(ctx, methods, trait_name, method_name)? + { + members.push(member); + } + } + Ok(members) } /// Builds one ReflectionMethod array entry from retained trait metadata. fn reflection_trait_method_member( + ctx: &FunctionContext<'_>, methods: &std::collections::HashMap, trait_name: &str, method_name: &str, -) -> Option { +) -> Result> { let method_key = php_symbol_key(method_name); - let info = methods.get(&method_key)?; + let Some(info) = methods.get(&method_key) else { + return Ok(None); + }; let flags = reflection_member_flags( info.is_static, &info.visibility, @@ -2322,7 +2379,16 @@ fn reflection_trait_method_member( flags, required_parameter_count, }; - Some(ReflectionListedMember { + let parameters = reflection_parameter_members_with_declaring_class( + ctx, + &info.signature, + trait_name, + None, + Some(trait_name), + Some(declaring_function), + &[], + )?; + Ok(Some(ReflectionListedMember { name: method_key, declaring_class_name: Some(trait_name.to_string()), attr_names: Vec::new(), @@ -2334,13 +2400,8 @@ fn reflection_trait_method_member( type_metadata: None, default_value: None, required_parameter_count, - parameters: reflection_parameter_members_with_declaring_class( - &info.signature, - Some(trait_name), - Some(declaring_function), - &[], - ), - }) + parameters, + })) } /// Builds ReflectionProperty array entries for the properties visible on one class. @@ -2441,7 +2502,7 @@ fn reflection_property_slot_default_value( default: Option<&Expr>, ) -> Option { match default { - Some(default) => reflection_parameter_default_value(default), + Some(default) => reflection_literal_parameter_default_value(default), None if !is_declared => Some(ReflectionParameterDefaultValue::Null), None => None, } @@ -2556,13 +2617,19 @@ fn reflection_promoted_constructor_parameter_names( /// Builds reflected parameter metadata and attaches declaring class metadata when present. fn reflection_parameter_members_with_declaring_class( + ctx: &FunctionContext<'_>, sig: &FunctionSig, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, declaring_class_name: Option<&str>, declaring_function: Option, promoted_parameter_names: &[String], -) -> Vec { +) -> Result> { reflection_parameter_members_with_declaring_function( + ctx, sig, + current_class, + current_info, declaring_class_name, declaring_function, promoted_parameter_names, @@ -2571,64 +2638,70 @@ fn reflection_parameter_members_with_declaring_class( /// Builds reflected parameter metadata with optional declaring owner metadata. fn reflection_parameter_members_with_declaring_function( + ctx: &FunctionContext<'_>, sig: &FunctionSig, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, declaring_class_name: Option<&str>, declaring_function: Option, promoted_parameter_names: &[String], -) -> Vec { - sig.params - .iter() - .enumerate() - .map(|(index, (name, ty))| { - let is_variadic = sig.variadic.as_deref() == Some(name.as_str()); - let has_type = sig.declared_params.get(index).copied().unwrap_or(false); - let type_metadata = reflection_parameter_type_metadata( - sig.param_type_exprs.get(index).and_then(Option::as_ref), - ty, - ) - .filter(|_| has_type); - let default_value = sig - .defaults +) -> Result> { + let mut parameters = Vec::new(); + for (index, (name, ty)) in sig.params.iter().enumerate() { + let is_variadic = sig.variadic.as_deref() == Some(name.as_str()); + let has_type = sig.declared_params.get(index).copied().unwrap_or(false); + let type_metadata = reflection_parameter_type_metadata( + sig.param_type_exprs.get(index).and_then(Option::as_ref), + ty, + ) + .filter(|_| has_type); + let default_expr = sig.defaults.get(index).and_then(Option::as_ref); + let default_value = default_expr + .map(|default| { + reflection_parameter_default_value(ctx, current_class, current_info, default) + }) + .transpose()? + .flatten(); + let default_value_constant_name = + default_expr.and_then(reflection_parameter_default_constant_name); + parameters.push(ReflectionParameterMember { + name: name.clone(), + declaring_class_name: declaring_class_name.map(str::to_string), + declaring_function: declaring_function.clone(), + attr_names: sig + .param_attributes .get(index) - .and_then(Option::as_ref) - .and_then(reflection_parameter_default_value); - ReflectionParameterMember { - name: name.clone(), - declaring_class_name: declaring_class_name.map(str::to_string), - declaring_function: declaring_function.clone(), - attr_names: sig - .param_attributes - .get(index) - .map(|groups| crate::types::collect_attribute_names(groups)) - .unwrap_or_default(), - attr_args: sig - .param_attributes + .map(|groups| crate::types::collect_attribute_names(groups)) + .unwrap_or_default(), + attr_args: sig + .param_attributes + .get(index) + .map(|groups| crate::types::collect_attribute_args(groups)) + .unwrap_or_default(), + position: index as i64, + is_optional: is_variadic + || sig + .defaults .get(index) - .map(|groups| crate::types::collect_attribute_args(groups)) - .unwrap_or_default(), - position: index as i64, - is_optional: is_variadic - || sig - .defaults - .get(index) - .map(|default| default.is_some()) - .unwrap_or(false), - is_variadic, - is_passed_by_reference: sig.ref_params.get(index).copied().unwrap_or(false), - is_promoted: promoted_parameter_names - .iter() - .any(|promoted_name| promoted_name == name), + .map(|default| default.is_some()) + .unwrap_or(false), + is_variadic, + is_passed_by_reference: sig.ref_params.get(index).copied().unwrap_or(false), + is_promoted: promoted_parameter_names + .iter() + .any(|promoted_name| promoted_name == name), + has_type, + allows_null: reflection_parameter_allows_null( has_type, - allows_null: reflection_parameter_allows_null( - has_type, - type_metadata.as_ref(), - default_value.as_ref(), - ), - type_metadata, - default_value, - } - }) - .collect() + type_metadata.as_ref(), + default_value.as_ref(), + ), + type_metadata, + default_value, + default_value_constant_name, + }); + } + Ok(parameters) } /// Returns PHP's `ReflectionParameter::allowsNull()` value for static metadata. @@ -2652,7 +2725,28 @@ fn reflection_type_allows_null(type_metadata: &ReflectionParameterTypeMetadata) } /// Converts a supported parameter default expression into Reflection metadata. -fn reflection_parameter_default_value(default: &Expr) -> Option { +fn reflection_parameter_default_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + default: &Expr, +) -> Result> { + if let Some(value) = reflection_literal_parameter_default_value(default) { + return Ok(Some(value)); + } + match &default.kind { + ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => { + let value = reflection_constant_value(ctx, current_class, current_info, default, 0)?; + Ok(reflection_parameter_default_from_constant_value(value)) + } + _ => Ok(None), + } +} + +/// Converts a literal parameter/property default expression into Reflection metadata. +fn reflection_literal_parameter_default_value( + default: &Expr, +) -> Option { match &default.kind { ExprKind::IntLiteral(value) => Some(ReflectionParameterDefaultValue::Int(*value)), ExprKind::BoolLiteral(value) => Some(ReflectionParameterDefaultValue::Bool(*value)), @@ -2670,6 +2764,44 @@ fn reflection_parameter_default_value(default: &Expr) -> Option Option { + match value { + ReflectionConstantValue::Int(value) => Some(ReflectionParameterDefaultValue::Int(value)), + ReflectionConstantValue::Bool(value) => Some(ReflectionParameterDefaultValue::Bool(value)), + ReflectionConstantValue::Float(value) => { + Some(ReflectionParameterDefaultValue::Float(value)) + } + ReflectionConstantValue::Str(value) => Some(ReflectionParameterDefaultValue::Str(value)), + ReflectionConstantValue::Null => Some(ReflectionParameterDefaultValue::Null), + ReflectionConstantValue::EnumCase { .. } => None, + } +} + +/// Returns PHP's constant-name metadata for parameter defaults that name a class constant. +fn reflection_parameter_default_constant_name(default: &Expr) -> Option { + match &default.kind { + ExprKind::ScopedConstantAccess { receiver, name } => Some(format!( + "{}::{}", + reflection_static_receiver_label(receiver), + name + )), + _ => None, + } +} + +/// Returns the PHP source-visible receiver label for ReflectionParameter constant defaults. +fn reflection_static_receiver_label(receiver: &StaticReceiver) -> String { + match receiver { + StaticReceiver::Named(name) => name.as_str().trim_start_matches('\\').to_string(), + StaticReceiver::Self_ => "self".to_string(), + StaticReceiver::Static => "static".to_string(), + StaticReceiver::Parent => "parent".to_string(), + } +} + /// Converts a normalized parameter type into a supported `ReflectionType` subset. fn reflection_parameter_type_metadata( type_expr: Option<&TypeExpr>, @@ -3863,8 +3995,8 @@ fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection constant value as the hash payload - ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word abi::emit_pop_reg(ctx.emitter, "x0"); abi::emit_symbol_address(ctx.emitter, "x1", &key_label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); @@ -3876,8 +4008,8 @@ fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str abi::emit_call_label(ctx.emitter, "__rt_hash_set"); } Arch::X86_64 => { - ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection constant value as the hash payload - ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection constant value as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word abi::emit_pop_reg(ctx.emitter, "rdi"); abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); @@ -4037,6 +4169,8 @@ fn emit_reflection_parameter_properties( .get("ReflectionParameter") .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let name_offset = reflection_property_offset(class_info, "__name")?; + let default_value_constant_name_offset = + reflection_property_offset(class_info, "__default_value_constant_name")?; emit_reflection_string_property(ctx, ¶meter.name, name_offset, name_offset + 8); emit_reflection_attrs_property( ctx, @@ -4093,6 +4227,21 @@ fn emit_reflection_parameter_properties( "__has_default_value", parameter.default_value.is_some(), )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_default_value_constant", + parameter.default_value_constant_name.is_some(), + )?; + emit_reflection_string_property( + ctx, + parameter + .default_value_constant_name + .as_deref() + .unwrap_or(""), + default_value_constant_name_offset, + default_value_constant_name_offset + 8, + ); emit_reflection_parameter_default_property(ctx, parameter)?; emit_reflection_parameter_declaring_class_property(ctx, parameter)?; emit_reflection_parameter_declaring_function_property(ctx, parameter)?; @@ -4554,32 +4703,32 @@ fn emit_reflection_string_array(ctx: &mut FunctionContext<'_>, names: &[String]) /// Appends ReflectionClass metadata names to the current ARM64 result array. fn emit_reflection_string_array_fill_aarch64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the metadata-name array while appending strings for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append + ctx.emitter.instruction("ldr x0, [sp]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final metadata-name array as the result } /// Appends ReflectionClass metadata names to the current x86_64 result array. fn emit_reflection_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String]) { - ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings - ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls + ctx.emitter.instruction("push rax"); // park the metadata-name array while appending strings + ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across append helper calls for name in names { let (label, len) = ctx.data.add_string(name.as_bytes()); - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the metadata-name array for this append abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_array_push_str"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown metadata-name array } - ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result + ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot + ctx.emitter.instruction("pop rax"); // restore the final metadata-name array as the result } /// Stores ReflectionMethod/ReflectionProperty boolean predicate slots when supported. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index d038171656..2f06726b35 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -627,6 +627,18 @@ fn builtin_reflection_parameter() -> FlattenedClass { Some(TypeExpr::Bool), bool_lit(false), ), + builtin_property( + "__is_default_value_constant", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__default_value_constant_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), builtin_property( "__default_value", Visibility::Private, @@ -671,6 +683,8 @@ fn builtin_reflection_parameter() -> FlattenedClass { "__has_default_value", TypeExpr::Bool, ), + builtin_reflection_parameter_is_default_value_constant_method(), + builtin_reflection_parameter_get_default_value_constant_name_method(), builtin_reflection_parameter_get_default_value_method(), builtin_reflection_slot_getter("getDeclaringClass", "__declaring_class", mixed_type()), builtin_reflection_slot_getter( @@ -765,6 +779,108 @@ fn builtin_reflection_class_new_instance_without_constructor_method() -> ClassMe } } +/// Builds `ReflectionParameter::isDefaultValueConstant()` over retained default metadata. +fn builtin_reflection_parameter_is_default_value_constant_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "isDefaultValueConstant".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + by_ref_return: false, + body: vec![ + reflection_parameter_throw_if_default_missing(dummy_span), + Stmt::new( + StmtKind::Return(Some(reflection_this_property( + "__is_default_value_constant", + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `ReflectionParameter::getDefaultValueConstantName()` over retained default metadata. +fn builtin_reflection_parameter_get_default_value_constant_name_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "getDefaultValueConstantName".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![ + reflection_parameter_throw_if_default_missing(dummy_span), + Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__is_default_value_constant", + dummy_span, + ))), + dummy_span, + ), + then_body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(reflection_this_property( + "__default_value_constant_name", + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds the PHP-compatible default-metadata guard shared by ReflectionParameter methods. +fn reflection_parameter_throw_if_default_missing(span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__has_default_value", + span, + ))), + span, + ), + then_body: vec![throw_new_reflection_exception( + string_lit("Internal error: Failed to retrieve the default value", span), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + /// Builds `ReflectionParameter::canBePassedByValue()` from the retained by-ref flag. fn builtin_reflection_parameter_can_be_passed_by_value_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -3031,12 +3147,13 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "hastype", "allowsnull", "isdefaultvalueavailable", + "isdefaultvalueconstant", ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; } } - for method_name in ["getType", "getDefaultValue"] { + for method_name in ["getType", "getDefaultValue", "getDefaultValueConstantName"] { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { sig.return_type = PhpType::Mixed; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 676e38bf13..3b9b265563 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8355,6 +8355,48 @@ foreach ($params as $param) { ); } +/// Verifies eval ReflectionParameter exposes PHP constant-default metadata. +#[test] +fn test_eval_reflection_parameter_reports_default_constant_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + echo $param->getName() . ":"; + echo $param->isDefaultValueAvailable() ? "D:" : "d:"; + if ($param->isDefaultValueAvailable()) { + if ($param->isDefaultValueConstant()) { + echo "C:"; + echo $param->getDefaultValueConstantName(); + echo ":"; + } else { + echo "c:null:"; + } + echo $param->getDefaultValue(); + } + echo "|"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "required:d:|global:D:C:EVAL_REFLECT_PARAM_DEFAULT_GLOBAL:G|self:D:C:self::LABEL:L|parent:D:C:parent::BASE:B|literal:D:c:null:7|" + ); +} + /// Verifies eval ReflectionMethod exposes eval-declared return type metadata. #[test] fn test_eval_reflection_method_reports_return_type_metadata() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 8ebe7ebafe..90db5c690b 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2665,6 +2665,51 @@ echo $direct->getDefaultValue(); assert_eq!(out.stdout, "d:E|D:7|D:null|D:ok"); } +/// Verifies `ReflectionParameter` exposes class-constant default metadata. +#[test] +fn test_reflection_parameter_exposes_default_constant_metadata() { + let out = compile_and_run_capture( + r##"getParameters(); +foreach ($params as $param) { + echo $param->getName() . ":"; + echo $param->isDefaultValueAvailable() ? "D:" : "d:"; + if ($param->isDefaultValueConstant()) { + echo "C:"; + echo $param->getDefaultValueConstantName(); + echo ":"; + } else { + echo "c:null:"; + } + echo $param->getDefaultValue(); + echo "|"; +} +$direct = new ReflectionParameter([ReflectDefaultConstTarget::class, "run"], "parent"); +echo "direct:"; +echo $direct->isDefaultValueConstant() ? "C:" : "c:"; +echo $direct->getDefaultValueConstantName(); +echo ":"; +echo $direct->getDefaultValue(); +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "self:D:C:self::LABEL:L|parent:D:C:parent::BASE:B|class:D:c:null:ReflectDefaultConstTarget|literal:D:c:null:7|direct:C:parent::BASE:B" + ); +} + /// Verifies direct `new ReflectionParameter()` construction for statically known /// class and interface method targets. #[test] From 02477653b2b1d87d88cf66814c24a77ce8483a0e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 00:35:41 +0200 Subject: [PATCH 0499/1208] Support ReflectionClass relation object APIs --- .../elephc-eval/src/interpreter/reflection.rs | 54 ++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 9 ++ .../tests/builtins_class_metadata.rs | 19 +++- .../interpreter/tests/support/object_ops.rs | 42 +++++++++ docs/php/eval.md | 8 +- src/codegen/lower_inst/objects/reflection.rs | 88 +++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 40 +++++++++ tests/codegen/eval.rs | 18 +++- tests/codegen/oop/attributes.rs | 10 ++- 9 files changed, 276 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 2eccbd4442..2eb1f49f68 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -414,6 +414,45 @@ pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( .map(Some) } +/// Handles eval-backed `ReflectionClass::getInterfaces()` and `getTraits()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let relation_kind = if method_name.eq_ignore_ascii_case("getInterfaces") { + "interfaces" + } else if method_name.eq_ignore_ascii_case("getTraits") { + "traits" + } else { + return Ok(None); + }; + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) + { + if relation_kind == "interfaces" { + metadata.interface_names + } else { + metadata.trait_names + } + } else if relation_kind == "interfaces" { + eval_reflection_aot_class_interface_names(&reflected_name, values)? + } else { + Vec::new() + }; + eval_reflection_class_object_map_result(&names, context, values).map(Some) +} + /// Handles eval-backed `ReflectionClass::getConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_constant_result( identity: u64, @@ -2430,6 +2469,21 @@ fn eval_reflection_string_array_result( Ok(result) } +/// Builds a name-keyed PHP array of full ReflectionClass objects. +fn eval_reflection_class_object_map_result( + names: &[String], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(names.len())?; + for name in names { + let key = values.string(name)?; + let object = eval_reflection_full_class_object_result(name, context, values)?; + result = values.array_set(result, key, object)?; + } + Ok(result) +} + /// Maps a synthetic reflection owner kind to PHP's `Attribute::TARGET_*` bitmask. fn eval_reflection_attribute_target(owner_kind: u64) -> u64 { match owner_kind { diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 6788fcf8fc..6c821bd96e 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3073,6 +3073,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_get_relation_objects_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_get_constant_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 5040966e2b..0097a4f3a7 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -498,6 +498,12 @@ echo count($interfaces); echo ":"; echo $interfaces[0]; echo ":"; echo count($traits); echo ":"; echo $traits[0]; echo ":"; $parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); echo count($parentInterfaces); echo ":"; echo $parentInterfaces[0]; +$interfaceObjects = $ref->getInterfaces(); +echo ":"; echo count($interfaceObjects); echo ":"; echo $interfaceObjects["EvalRelationIface"]->getName(); +$traitObjects = $ref->getTraits(); +echo ":"; echo count($traitObjects); echo ":"; echo $traitObjects["EvalRelationTrait"]->getName(); +$parentInterfaceObjects = (new ReflectionClass("EvalRelationChild"))->getInterfaces(); +echo ":"; echo count($parentInterfaceObjects); echo ":"; echo $parentInterfaceObjects["EvalRelationParent"]->getName(); return true;"#, ) .expect("parse eval fragment"); @@ -508,7 +514,7 @@ return true;"#, assert_eq!( values.output, - "1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent" + "1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent:1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -520,7 +526,11 @@ fn execute_program_reflects_aot_class_interface_names() { br#"$class_names = (new ReflectionClass("KnownClass"))->getInterfaceNames(); echo count($class_names); echo ":"; echo $class_names[0]; echo ":"; $interface_names = (new ReflectionClass("KnownInterface"))->getInterfaceNames(); -echo count($interface_names); echo ":"; echo $interface_names[0]; +echo count($interface_names); echo ":"; echo $interface_names[0]; echo ":"; +$class_objects = (new ReflectionClass("KnownClass"))->getInterfaces(); +echo count($class_objects); echo ":"; echo $class_objects["KnownInterface"]->getName(); echo ":"; +$interface_objects = (new ReflectionClass("KnownInterface"))->getInterfaces(); +echo count($interface_objects); echo ":"; echo $interface_objects["Traversable"]->getName(); return true;"#, ) .expect("parse eval fragment"); @@ -529,7 +539,10 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1:KnownInterface:1:Traversable"); + assert_eq!( + values.output, + "1:KnownInterface:1:Traversable:1:KnownInterface:1:Traversable" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index b35facc0a5..07b3004a21 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -303,10 +303,16 @@ impl FakeOps { Self::object_property(&properties, "__interface_names") .map_or_else(|| self.runtime_array_new(0), Ok) } + (FakeValue::Object(properties), "getinterfaces") if args.is_empty() => { + self.object_relation_reflection_classes(&properties, "__interface_names") + } (FakeValue::Object(properties), "gettraitnames") if args.is_empty() => { Self::object_property(&properties, "__trait_names") .map_or_else(|| self.runtime_array_new(0), Ok) } + (FakeValue::Object(properties), "gettraits") if args.is_empty() => { + self.object_relation_reflection_classes(&properties, "__trait_names") + } (FakeValue::Object(properties), "getmethods") if args.is_empty() => { Self::object_property(&properties, "__methods") .map_or_else(|| self.runtime_array_new(0), Ok) @@ -812,6 +818,42 @@ impl FakeOps { self.bool_value(contains) } + /// Builds a name-keyed fake ReflectionClass map from a private string-array property. + fn object_relation_reflection_classes( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + ) -> Result { + let result = self.runtime_assoc_new(0)?; + let Some(array) = Self::object_property(properties, property) else { + return Ok(result); + }; + let FakeValue::Array(elements) = self.get(array) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::String(name) = self.get(element) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let key = self.runtime_string(&name)?; + let object = self.fake_reflection_class_object(&name)?; + self.runtime_array_set(result, key, object)?; + } + Ok(result) + } + + /// Builds a minimal fake ReflectionClass object with a working `getName()` slot. + fn fake_reflection_class_object( + &mut self, + class_name: &str, + ) -> Result { + let name = self.string(class_name)?; + let object = self.alloc(FakeValue::Object(vec![("__name".to_string(), name)])); + self.object_classes + .insert(object.as_ptr() as usize, "ReflectionClass".to_string()); + Ok(object) + } + /// Finds one fake ReflectionMethod/ReflectionProperty object by its private name slot. fn object_named_member( &mut self, diff --git a/docs/php/eval.md b/docs/php/eval.md index 54e2f6f1eb..6f65b239e8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -230,13 +230,15 @@ user-defined class-like symbols. modifier bitmask for eval class-like metadata. `ReflectionClass::getInterfaceNames()` returns implemented interfaces for eval and generated/AOT classes, plus parent interfaces for eval and generated/AOT -interfaces. +interfaces. `ReflectionClass::getInterfaces()` materializes those names as a +name-keyed array of `ReflectionClass` objects. `ReflectionClass::getTraitNames()` +returns traits used directly by eval classes, and `ReflectionClass::getTraits()` +materializes those direct trait names as `ReflectionClass` objects. `ReflectionClass::implementsInterface()` checks those eval relations case-insensitively, returns true when reflecting the requested interface itself, and checks generated/AOT class-interface relations through runtime metadata. It throws catchable `ReflectionException` values when the argument names a class, -trait, enum, or missing interface, while `ReflectionClass::getTraitNames()` -returns traits used directly by eval classes. +trait, enum, or missing interface. `ReflectionClass::isSubclassOf()` checks eval parent-class chains and implemented or extended interfaces case-insensitively. It excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws a diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 3ca07060b4..515843b080 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -284,11 +284,17 @@ fn emit_reflection_owner_object( "__interface_names", &metadata.interface_names, )?; + emit_reflection_class_array_property_by_name( + ctx, + "__interfaces", + &metadata.interface_names, + )?; emit_reflection_string_array_property_by_name( ctx, "__trait_names", &metadata.trait_names, )?; + emit_reflection_class_array_property_by_name(ctx, "__traits", &metadata.trait_names)?; emit_reflection_string_array_property_by_name( ctx, "__parent_names", @@ -3626,6 +3632,40 @@ fn emit_reflection_string_array_property_by_name( Ok(()) } +/// Replaces a ReflectionClass private slot with name-keyed ReflectionClass objects. +fn emit_reflection_class_array_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + names: &[String], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_class_array(ctx, names)?; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate(ctx.emitter, abi::secondary_scratch_reg(ctx.emitter), 4); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + /// Replaces a ReflectionClass private slot with an associative constant-value array. fn emit_reflection_constant_array_property_by_name( ctx: &mut FunctionContext<'_>, @@ -3926,6 +3966,54 @@ fn emit_reflection_constant_array( Ok(()) } +/// Allocates and populates a name-keyed map of full ReflectionClass objects. +fn emit_reflection_class_array(ctx: &mut FunctionContext<'_>, names: &[String]) -> Result<()> { + emit_empty_assoc_array_literal_to_result( + ctx, + &PhpType::Object("ReflectionClass".to_string()), + ); + for name in names { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + let metadata = reflection_class_metadata_for_name(ctx, name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &metadata)?; + emit_reflection_class_hash_insert(ctx, name); + } + Ok(()) +} + +/// Inserts the current ReflectionClass object into the stacked associative array. +fn emit_reflection_class_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the ReflectionClass object as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // object hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Object("ReflectionClass".to_string())) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the ReflectionClass object as the hash payload + ctx.emitter.instruction("xor r8, r8"); // object hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Object("ReflectionClass".to_string())) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + /// Allocates and populates the associative ReflectionClass default-property map. fn emit_reflection_default_property_array( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 2f06726b35..6174cdad0c 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -274,6 +274,14 @@ fn object_array_type(class_name: &str) -> TypeExpr { TypeExpr::Array(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) } +/// Returns `array` for name-keyed reflection maps. +fn reflection_class_object_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Object("ReflectionClass".to_string())), + } +} + /// Returns a nullable object type expression for one synthetic reflection class. fn nullable_object_type(class_name: &str) -> TypeExpr { TypeExpr::Nullable(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) @@ -1153,12 +1161,24 @@ fn builtin_reflection_class() -> FlattenedClass { Some(string_array_type()), empty_array(), ), + builtin_property( + "__interfaces", + Visibility::Private, + Some(object_array_type("ReflectionClass")), + empty_array(), + ), builtin_property( "__trait_names", Visibility::Private, Some(string_array_type()), empty_array(), ), + builtin_property( + "__traits", + Visibility::Private, + Some(object_array_type("ReflectionClass")), + empty_array(), + ), builtin_property( "__parent_names", Visibility::Private, @@ -1251,11 +1271,21 @@ fn builtin_reflection_class() -> FlattenedClass { "__interface_names", string_array_type(), ), + builtin_reflection_class_array_method( + "getInterfaces", + "__interfaces", + object_array_type("ReflectionClass"), + ), builtin_reflection_class_array_method( "getTraitNames", "__trait_names", string_array_type(), ), + builtin_reflection_class_array_method( + "getTraits", + "__traits", + object_array_type("ReflectionClass"), + ), builtin_reflection_class_bool_method("isFinal", "__is_final"), builtin_reflection_class_bool_method("isAbstract", "__is_abstract"), builtin_reflection_class_bool_method("isInterface", "__is_interface"), @@ -2964,6 +2994,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Mixed; } if class_name == "ReflectionClass" { + for (property_name, property_type) in &mut class_info.properties { + if matches!(property_name.as_str(), "__interfaces" | "__traits") { + *property_type = reflection_class_object_map_type(); + } + } for method_name in [ "isfinal", "isabstract", @@ -2993,6 +3028,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Array(Box::new(PhpType::Str)); } } + for method_name in ["getinterfaces", "gettraits"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = reflection_class_object_map_type(); + } + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getMethods")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionMethod".to_string()))); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3b9b265563..7aa1d9eef9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6588,7 +6588,13 @@ $traits = $ref->getTraitNames(); echo count($interfaces) . ":" . $interfaces[0] . ":"; echo count($traits) . ":" . $traits[0] . ":"; $parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); -echo count($parentInterfaces) . ":" . $parentInterfaces[0];'); +echo count($parentInterfaces) . ":" . $parentInterfaces[0] . ":"; +$interfaceObjects = $ref->getInterfaces(); +echo count($interfaceObjects) . ":" . $interfaceObjects["EvalRelationIface"]->getName() . ":"; +$traitObjects = $ref->getTraits(); +echo count($traitObjects) . ":" . $traitObjects["EvalRelationTrait"]->getName() . ":"; +$parentInterfaceObjects = (new ReflectionClass("EvalRelationChild"))->getInterfaces(); +echo count($parentInterfaceObjects) . ":" . $parentInterfaceObjects["EvalRelationParent"]->getName();'); "#, ); assert!( @@ -6598,7 +6604,7 @@ echo count($parentInterfaces) . ":" . $parentInterfaces[0];'); ); assert_eq!( out.stdout, - "1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent" + "1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent:1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent" ); } @@ -6613,7 +6619,11 @@ class EvalAotReflectIfaceTarget implements EvalAotReflectIfaceChild {} eval('$interfaces = (new ReflectionClass("EvalAotReflectIfaceTarget"))->getInterfaceNames(); sort($interfaces); echo count($interfaces) . ":"; -echo implode(",", $interfaces);'); +echo implode(",", $interfaces) . ":"; +$interfaceObjects = (new ReflectionClass("EvalAotReflectIfaceTarget"))->getInterfaces(); +ksort($interfaceObjects); +echo count($interfaceObjects) . ":" . implode(",", array_keys($interfaceObjects)) . ":"; +echo $interfaceObjects["EvalAotReflectIfaceBase"]->getName();'); "#, ); assert!( @@ -6623,7 +6633,7 @@ echo implode(",", $interfaces);'); ); assert_eq!( out.stdout, - "2:EvalAotReflectIfaceBase,EvalAotReflectIfaceChild" + "2:EvalAotReflectIfaceBase,EvalAotReflectIfaceChild:2:EvalAotReflectIfaceBase,EvalAotReflectIfaceChild:EvalAotReflectIfaceBase" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 90db5c690b..f82414d976 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1606,12 +1606,18 @@ $traits = $ref->getTraitNames(); echo count($interfaces) . ":" . $interfaces[0] . ":"; echo count($traits) . ":" . $traits[0] . ":"; $parentInterfaces = (new ReflectionClass(StaticRelationChild::class))->getInterfaceNames(); -echo count($parentInterfaces) . ":" . $parentInterfaces[0]; +echo count($parentInterfaces) . ":" . $parentInterfaces[0] . ":"; +$interfaceObjects = $ref->getInterfaces(); +echo count($interfaceObjects) . ":" . $interfaceObjects["StaticRelationIface"]->getName() . ":"; +$traitObjects = $ref->getTraits(); +echo count($traitObjects) . ":" . $traitObjects["StaticRelationTrait"]->getName() . ":"; +$parentInterfaceObjects = (new ReflectionClass(StaticRelationChild::class))->getInterfaces(); +echo count($parentInterfaceObjects) . ":" . $parentInterfaceObjects["StaticRelationParent"]->getName(); "#, ); assert_eq!( out, - "1:StaticRelationIface:1:StaticRelationTrait:1:StaticRelationParent" + "1:StaticRelationIface:1:StaticRelationTrait:1:StaticRelationParent:1:StaticRelationIface:1:StaticRelationTrait:1:StaticRelationParent" ); } From 63d90aa79e4dff9426ea9c2d177bd02b5550e602 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 00:45:35 +0200 Subject: [PATCH 0500/1208] Support eval ReflectionClass constant filters --- .../elephc-eval/src/interpreter/reflection.rs | 36 ++++++++++++++----- .../tests/builtins_class_metadata.rs | 22 +++++++++--- docs/php/eval.md | 5 ++- tests/codegen/eval.rs | 19 ++++++++-- 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 2eb1f49f68..d067b4715b 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -489,9 +489,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( if !method_name.eq_ignore_ascii_case("getConstants") { return Ok(None); } - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } + let filter = eval_reflection_member_filter(evaluated_args, values)?; let Some(reflected_name) = context .eval_reflection_class_name(identity) .map(str::to_string) @@ -501,6 +499,9 @@ pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( let names = eval_reflection_constant_names(&reflected_name, context); let mut result = values.assoc_new(names.len())?; for name in names { + if !eval_reflection_constant_matches_filter(&reflected_name, &name, filter, context) { + continue; + } let Some(value) = eval_reflection_constant_value(&reflected_name, &name, context) else { continue; }; @@ -1021,9 +1022,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_res if !method_name.eq_ignore_ascii_case("getReflectionConstants") { return Ok(None); } - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } + let filter = eval_reflection_member_filter(evaluated_args, values)?; let Some(reflected_name) = context .eval_reflection_class_name(identity) .map(str::to_string) @@ -1032,11 +1031,16 @@ pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_res }; let names = eval_reflection_constant_names(&reflected_name, context); let mut result = values.array_new(names.len())?; - for (index, name) in names.iter().enumerate() { + let mut index = 0; + for name in &names { + if !eval_reflection_constant_matches_filter(&reflected_name, name, filter, context) { + continue; + } let object = eval_reflection_class_constant_object_result(&reflected_name, name, context, values)?; - let key = values.int(index as i64)?; + let key = values.int(index)?; result = values.array_set(result, key, object)?; + index += 1; } Ok(Some(result)) } @@ -1236,6 +1240,22 @@ fn eval_reflection_class_constant_object_result( ) } +/// Returns whether one class constant passes an optional `ReflectionClassConstant` filter. +fn eval_reflection_constant_matches_filter( + reflected_name: &str, + constant_name: &str, + filter: Option, + context: &ElephcEvalContext, +) -> bool { + let Some(filter) = filter else { + return true; + }; + eval_reflection_class_constant_metadata(reflected_name, constant_name, context) + .is_some_and(|(_, _, visibility, is_final, _)| { + eval_reflection_class_constant_modifiers(visibility, is_final) & filter != 0 + }) +} + /// Resolves the declared member spelling for eval `ReflectionClass` single-member lookups. fn eval_reflection_member_name( owner_kind: u64, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 0097a4f3a7..51e250a796 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1183,6 +1183,10 @@ enum EvalReflectConstEnum { } $ref = new ReflectionClass("EvalReflectConstChild"); $all = $ref->getConstants(); +$public = $ref->getConstants(ReflectionClassConstant::IS_PUBLIC); +$private = $ref->getConstants(filter: ReflectionClassConstant::IS_PRIVATE); +$none = $ref->getConstants(0); +$null = $ref->getConstants(null); echo $ref->getConstant("OWN"); echo ":"; echo $ref->getConstant("BASE"); echo ":"; echo $ref->getConstant("LIMIT"); echo ":"; @@ -1190,6 +1194,9 @@ echo $ref->getConstant("SECRET"); echo ":"; echo $ref->getConstant("SUM"); echo ":"; echo $ref->getConstant("own") ? "bad" : "missing"; echo ":"; echo count($all); echo ":"; echo $all["OWN"]; echo ":"; echo $all["BASE"]; echo ":"; echo $all["LIMIT"]; +echo ":"; echo count($public); echo ":"; echo $public["OWN"]; echo ":"; echo $public["BASE"]; +echo ":"; echo count($private); echo ":"; echo $private["SECRET"]; +echo ":"; echo count($none); echo ":"; echo count($null); $trait = new ReflectionClass("EvalReflectConstTrait"); $traitAll = $trait->getConstants(); echo ":"; echo $trait->getConstant("TRAIT_VALUE"); echo ":"; echo count($traitAll); echo ":"; echo $traitAll["TRAIT_VALUE"]; @@ -1208,7 +1215,7 @@ return true;"#, assert_eq!( values.output, - "own:1:2:9:5:missing:5:own:1:2:3:1:3:Ready:40:40:2" + "own:1:2:9:5:missing:5:own:1:2:4:own:1:1:9:0:5:3:1:3:Ready:40:40:2" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -1228,27 +1235,32 @@ fn execute_program_reflects_eval_class_constant_reflector_objects() { } class EvalReflectConstObjectTarget { #[EvalReflectConstMarker("const")] - public const ANSWER = 42; + final public const ANSWER = 42; } enum EvalReflectConstObjectEnum { #[EvalReflectConstMarker("case")] case Ready; - public const LEVEL = 7; + final public const LEVEL = 7; } $ref = new ReflectionClass("EvalReflectConstObjectTarget"); $single = $ref->getReflectionConstant("ANSWER"); $all = $ref->getReflectionConstants(); +$public = $ref->getReflectionConstants(ReflectionClassConstant::IS_PUBLIC); +$final = $ref->getReflectionConstants(filter: ReflectionClassConstant::IS_FINAL); echo $single->getName(); echo ":"; echo count($all); echo ":"; echo $all[0]->getName(); echo ":"; echo $single->getAttributes()[0]->newInstance()->label(); echo ":"; echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +echo ":"; echo count($public); echo ":"; echo $public[0]->getName(); +echo ":"; echo count($final); echo ":"; echo $final[0]->getName(); $enum = new ReflectionClass("EvalReflectConstObjectEnum"); $enumAll = $enum->getReflectionConstants(); +$enumFinal = $enum->getReflectionConstants(ReflectionClassConstant::IS_FINAL); $case = $enum->getReflectionConstant("Ready"); $level = $enum->getReflectionConstant("LEVEL"); echo ":"; echo count($enumAll); echo ":"; echo $enumAll[0]->getName(); echo ":"; echo $enumAll[1]->getName(); echo ":"; echo $case->getAttributes()[0]->newInstance()->label(); echo ":"; -echo count($level->getAttributes()); +echo count($level->getAttributes()); echo ":"; echo count($enumFinal); echo ":"; echo $enumFinal[0]->getName(); return true;"#, ) .expect("parse eval fragment"); @@ -1259,7 +1271,7 @@ return true;"#, assert_eq!( values.output, - "ANSWER:1:ANSWER:const:missing:2:Ready:LEVEL:case:0" + "ANSWER:1:ANSWER:const:missing:1:ANSWER:1:ANSWER:2:Ready:LEVEL:case:0:1:LEVEL" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 6f65b239e8..d2d2f89044 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -259,7 +259,10 @@ member lists to be materialized on the `ReflectionClass` object. class constants, interface constants, trait constants, enum constants, enum cases, supported materialized property defaults, and current eval-declared static property values. Constant lookup is case-sensitive; single-value -lookups return `false` when no constant or case is visible. +lookups return `false` when no constant or case is visible. `getConstants()` +and `getReflectionConstants()` accept PHP's `ReflectionClassConstant::IS_*` +visibility/finality filter bitmask; `null` means no filter and `0` returns no +constants. `ReflectionClass::getMethods()` and `ReflectionClass::getProperties()` return materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7aa1d9eef9..cdfb43f67e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7264,6 +7264,10 @@ enum EvalReflectConstEnum { } $ref = new ReflectionClass("EvalReflectConstChild"); $all = $ref->getConstants(); +$public = $ref->getConstants(ReflectionClassConstant::IS_PUBLIC); +$private = $ref->getConstants(filter: ReflectionClassConstant::IS_PRIVATE); +$none = $ref->getConstants(0); +$null = $ref->getConstants(null); echo $ref->getConstant("OWN") . ":"; echo $ref->getConstant("BASE") . ":"; echo $ref->getConstant("LIMIT") . ":"; @@ -7271,6 +7275,9 @@ echo $ref->getConstant("SECRET") . ":"; echo $ref->getConstant("SUM") . ":"; echo $ref->getConstant("own") ? "bad" : "missing"; echo ":" . count($all) . ":" . $all["OWN"] . ":" . $all["BASE"] . ":" . $all["LIMIT"]; +echo ":" . count($public) . ":" . $public["OWN"] . ":" . $public["BASE"]; +echo ":" . count($private) . ":" . $private["SECRET"]; +echo ":" . count($none) . ":" . count($null); $trait = new ReflectionClass("EvalReflectConstTrait"); $traitAll = $trait->getConstants(); echo ":" . $trait->getConstant("TRAIT_VALUE") . ":" . count($traitAll) . ":" . $traitAll["TRAIT_VALUE"]; @@ -7288,7 +7295,7 @@ echo ":" . $enum->getConstant("LEVEL") . ":" . $enumAll["LEVEL"] . ":" . count($ ); assert_eq!( out.stdout, - "own:1:2:9:5:missing:5:own:1:2:3:1:3:Ready:40:40:2" + "own:1:2:9:5:missing:5:own:1:2:4:own:1:1:9:0:5:3:1:3:Ready:40:40:2" ); } @@ -7318,19 +7325,25 @@ enum EvalReflectConstObjectEnum { $ref = new ReflectionClass("EvalReflectConstObjectTarget"); $single = $ref->getReflectionConstant("ANSWER"); $all = $ref->getReflectionConstants(); +$public = $ref->getReflectionConstants(ReflectionClassConstant::IS_PUBLIC); +$final = $ref->getReflectionConstants(filter: ReflectionClassConstant::IS_FINAL); echo $single->getName() . ":"; echo ($single->isFinal() ? "F" : "f") . ":"; echo count($all) . ":" . $all[0]->getName() . ":"; echo $single->getAttributes()[0]->newInstance()->label() . ":"; echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +echo ":" . count($public) . ":" . $public[0]->getName(); +echo ":" . count($final) . ":" . $final[0]->getName(); $enum = new ReflectionClass("EvalReflectConstObjectEnum"); $enumAll = $enum->getReflectionConstants(); +$enumFinal = $enum->getReflectionConstants(ReflectionClassConstant::IS_FINAL); $case = $enum->getReflectionConstant("Ready"); $level = $enum->getReflectionConstant("LEVEL"); echo ":" . count($enumAll) . ":" . $enumAll[0]->getName() . ":" . $enumAll[1]->getName(); echo ":" . $case->getAttributes()[0]->newInstance()->label() . ":"; echo count($level->getAttributes()) . ":"; -echo $level->isFinal() ? "F" : "f";'); +echo $level->isFinal() ? "F" : "f"; +echo ":" . count($enumFinal) . ":" . $enumFinal[0]->getName();'); "#, ); assert!( @@ -7340,7 +7353,7 @@ echo $level->isFinal() ? "F" : "f";'); ); assert_eq!( out.stdout, - "ANSWER:F:1:ANSWER:const:missing:2:Ready:LEVEL:case:0:F" + "ANSWER:F:1:ANSWER:const:missing:1:ANSWER:1:ANSWER:2:Ready:LEVEL:case:0:F:1:LEVEL" ); } From ca276f44361f1d7cd311e6c45c46a9ce928f0b71 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 00:52:40 +0200 Subject: [PATCH 0501/1208] Support eval ReflectionClass trait aliases --- crates/elephc-eval/src/context.rs | 53 ++++++++++++++++++- .../elephc-eval/src/interpreter/reflection.rs | 36 +++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 9 ++++ .../tests/builtins_class_metadata.rs | 24 +++++++-- docs/php/eval.md | 6 ++- tests/codegen/eval.rs | 26 ++++++--- 6 files changed, 140 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index ee0a6aea8f..e42c41b6f6 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -18,7 +18,7 @@ use crate::abi::ABI_VERSION; use crate::eval_ir::{ EvalAttribute, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalEnum, EvalFunction, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, - EvalTrait, EvalVisibility, + EvalTrait, EvalTraitAdaptation, EvalVisibility, }; use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; @@ -1197,6 +1197,57 @@ impl ElephcEvalContext { }) } + /// Returns trait method aliases declared directly by an eval-declared class. + pub fn class_trait_aliases(&self, class_name: &str) -> Vec<(String, String)> { + let Some(class) = self.class(class_name) else { + return Vec::new(); + }; + let mut aliases = Vec::new(); + for adaptation in class.trait_adaptations() { + let EvalTraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + .. + } = adaptation + else { + continue; + }; + let Some(source_trait) = + self.class_trait_alias_source(class, trait_name.as_deref(), method) + else { + continue; + }; + aliases.push((alias.clone(), format!("{source_trait}::{method}"))); + } + aliases + } + + /// Resolves the trait name shown in `ReflectionClass::getTraitAliases()`. + fn class_trait_alias_source( + &self, + class: &EvalClass, + explicit_trait: Option<&str>, + method: &str, + ) -> Option { + if let Some(trait_name) = explicit_trait { + return Some( + self.trait_decl(trait_name) + .map_or(trait_name, EvalTrait::name) + .trim_start_matches('\\') + .to_string(), + ); + } + class.traits().iter().find_map(|trait_name| { + let trait_decl = self.trait_decl(trait_name)?; + trait_decl + .methods() + .iter() + .any(|candidate| candidate.name().eq_ignore_ascii_case(method)) + .then(|| trait_decl.name().trim_start_matches('\\').to_string()) + }) + } + /// Returns PHP case-insensitive method names visible to `ReflectionClass::hasMethod()`. pub fn class_method_names(&self, class_name: &str) -> Vec { let mut names = Vec::new(); diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index d067b4715b..6854ac0b40 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -453,6 +453,28 @@ pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( eval_reflection_class_object_map_result(&names, context, values).map(Some) } +/// Handles eval-backed `ReflectionClass::getTraitAliases()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_trait_aliases_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getTraitAliases") { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + eval_reflection_string_assoc_result(context.class_trait_aliases(&reflected_name), values) + .map(Some) +} + /// Handles eval-backed `ReflectionClass::getConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_constant_result( identity: u64, @@ -2489,6 +2511,20 @@ fn eval_reflection_string_array_result( Ok(result) } +/// Builds a string-keyed PHP associative array from owned string pairs. +fn eval_reflection_string_assoc_result( + pairs: Vec<(String, String)>, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(pairs.len())?; + for (key, value) in pairs { + let key = values.string(&key)?; + let value = values.string(&value)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Builds a name-keyed PHP array of full ReflectionClass objects. fn eval_reflection_class_object_map_result( names: &[String], diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 6c821bd96e..2b7439adbc 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3082,6 +3082,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_get_trait_aliases_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_get_constant_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 51e250a796..f5832f7b33 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -485,25 +485,39 @@ return true;"#, fn execute_program_reflects_eval_class_relation_names() { let program = parse_fragment( br#"interface EvalRelationIface {} -trait EvalRelationTrait {} +trait EvalRelationTrait { + public function primary() {} +} +trait EvalRelationOtherTrait { + public function other() {} +} class EvalRelationTarget implements EvalRelationIface { - use EvalRelationTrait; + use EvalRelationTrait, EvalRelationOtherTrait { + EvalRelationTrait::primary as relationAlias; + EvalRelationOtherTrait::other as private hiddenOther; + EvalRelationOtherTrait::other as protected; + } } +class EvalRelationInherited extends EvalRelationTarget {} interface EvalRelationParent {} interface EvalRelationChild extends EvalRelationParent {} $ref = new ReflectionClass("EvalRelationTarget"); $interfaces = $ref->getInterfaceNames(); $traits = $ref->getTraitNames(); echo count($interfaces); echo ":"; echo $interfaces[0]; echo ":"; -echo count($traits); echo ":"; echo $traits[0]; echo ":"; +echo count($traits); echo ":"; echo $traits[0]; echo ":"; echo $traits[1]; echo ":"; $parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); echo count($parentInterfaces); echo ":"; echo $parentInterfaces[0]; $interfaceObjects = $ref->getInterfaces(); echo ":"; echo count($interfaceObjects); echo ":"; echo $interfaceObjects["EvalRelationIface"]->getName(); $traitObjects = $ref->getTraits(); -echo ":"; echo count($traitObjects); echo ":"; echo $traitObjects["EvalRelationTrait"]->getName(); +echo ":"; echo count($traitObjects); echo ":"; echo $traitObjects["EvalRelationTrait"]->getName(); echo ":"; echo $traitObjects["EvalRelationOtherTrait"]->getName(); $parentInterfaceObjects = (new ReflectionClass("EvalRelationChild"))->getInterfaces(); echo ":"; echo count($parentInterfaceObjects); echo ":"; echo $parentInterfaceObjects["EvalRelationParent"]->getName(); +$aliases = $ref->getTraitAliases(); +echo ":"; echo count($aliases); echo ":"; echo $aliases["relationAlias"]; echo ":"; echo $aliases["hiddenOther"]; +$inheritedAliases = (new ReflectionClass("EvalRelationInherited"))->getTraitAliases(); +echo ":"; echo count($inheritedAliases); return true;"#, ) .expect("parse eval fragment"); @@ -514,7 +528,7 @@ return true;"#, assert_eq!( values.output, - "1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent:1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent" + "1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:2:EvalRelationTrait::primary:EvalRelationOtherTrait::other:0" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index d2d2f89044..dd59c17bb1 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -232,8 +232,10 @@ modifier bitmask for eval class-like metadata. and generated/AOT classes, plus parent interfaces for eval and generated/AOT interfaces. `ReflectionClass::getInterfaces()` materializes those names as a name-keyed array of `ReflectionClass` objects. `ReflectionClass::getTraitNames()` -returns traits used directly by eval classes, and `ReflectionClass::getTraits()` -materializes those direct trait names as `ReflectionClass` objects. +returns traits used directly by eval classes, `ReflectionClass::getTraits()` +materializes those direct trait names as `ReflectionClass` objects, and +`ReflectionClass::getTraitAliases()` exposes direct eval trait `as` aliases as +PHP's alias-name to `Trait::method` map. `ReflectionClass::implementsInterface()` checks those eval relations case-insensitively, returns true when reflecting the requested interface itself, and checks generated/AOT class-interface relations through runtime metadata. It diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cdfb43f67e..c0ba5b6a25 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6576,25 +6576,39 @@ fn test_eval_reflection_class_relation_names() { let out = compile_and_run_capture( r#"getInterfaceNames(); $traits = $ref->getTraitNames(); echo count($interfaces) . ":" . $interfaces[0] . ":"; -echo count($traits) . ":" . $traits[0] . ":"; +echo count($traits) . ":" . $traits[0] . ":" . $traits[1] . ":"; $parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); echo count($parentInterfaces) . ":" . $parentInterfaces[0] . ":"; $interfaceObjects = $ref->getInterfaces(); echo count($interfaceObjects) . ":" . $interfaceObjects["EvalRelationIface"]->getName() . ":"; $traitObjects = $ref->getTraits(); -echo count($traitObjects) . ":" . $traitObjects["EvalRelationTrait"]->getName() . ":"; +echo count($traitObjects) . ":" . $traitObjects["EvalRelationTrait"]->getName() . ":" . $traitObjects["EvalRelationOtherTrait"]->getName() . ":"; $parentInterfaceObjects = (new ReflectionClass("EvalRelationChild"))->getInterfaces(); -echo count($parentInterfaceObjects) . ":" . $parentInterfaceObjects["EvalRelationParent"]->getName();'); +echo count($parentInterfaceObjects) . ":" . $parentInterfaceObjects["EvalRelationParent"]->getName() . ":"; +$aliases = $ref->getTraitAliases(); +echo count($aliases) . ":" . $aliases["relationAlias"] . ":" . $aliases["hiddenOther"] . ":"; +$inheritedAliases = (new ReflectionClass("EvalRelationInherited"))->getTraitAliases(); +echo count($inheritedAliases);'); "#, ); assert!( @@ -6604,7 +6618,7 @@ echo count($parentInterfaceObjects) . ":" . $parentInterfaceObjects["EvalRelatio ); assert_eq!( out.stdout, - "1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent:1:EvalRelationIface:1:EvalRelationTrait:1:EvalRelationParent" + "1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:2:EvalRelationTrait::primary:EvalRelationOtherTrait::other:0" ); } From 6b737e0a206861dcf568cc5a7b057aef2081e5b8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 01:11:11 +0200 Subject: [PATCH 0502/1208] Support eval ReflectionProperty hook metadata --- crates/elephc-eval/src/context.rs | 7 + .../elephc-eval/src/interpreter/reflection.rs | 330 +++++++++++++++++- .../elephc-eval/src/interpreter/statements.rs | 58 +++ .../tests/builtins_class_metadata.rs | 54 +++ docs/php/eval.md | 5 + tests/codegen/eval.rs | 53 +++ 6 files changed, 492 insertions(+), 15 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index e42c41b6f6..c2a19b31b9 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -833,6 +833,13 @@ impl ElephcEvalContext { .and_then(|class_key| self.classes.get(class_key)) } + /// Returns whether one dynamic object identity was registered with a class-like name. + pub fn dynamic_object_is_class(&self, identity: u64, class_name: &str) -> bool { + self.dynamic_objects + .get(&identity) + .is_some_and(|class_key| class_key == &normalize_class_name(class_name)) + } + /// Binds one eval object property slot to a persistent PHP reference target. pub fn bind_dynamic_property_alias( &mut self, diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 6854ac0b40..d15717a112 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -124,6 +124,36 @@ enum EvalReflectionParameterTypeKind { Intersection(EvalReflectionIntersectionTypeMetadata), } +/// Property hook kind accepted by `ReflectionProperty` hook APIs. +#[derive(Clone, Copy)] +enum EvalReflectionPropertyHook { + Get, + Set, +} + +impl EvalReflectionPropertyHook { + /// Returns the associative-array key PHP uses for this hook kind. + const fn key(self) -> &'static str { + match self { + Self::Get => "get", + Self::Set => "set", + } + } + + /// Returns the PHP-visible synthetic hook method name. + fn reflected_method_name(self, property_name: &str) -> String { + format!("${}::{}", property_name, self.key()) + } + + /// Returns the internal eval method name that stores the hook body. + fn synthetic_method_name(self, property_name: &str) -> String { + match self { + Self::Get => property_hook_get_method(property_name), + Self::Set => property_hook_set_method(property_name), + } + } +} + /// Eval metadata needed to materialize one `ReflectionNamedType` object. #[derive(Clone)] struct EvalReflectionNamedTypeMetadata { @@ -438,18 +468,18 @@ pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( else { return Ok(None); }; - let names = if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) - { - if relation_kind == "interfaces" { - metadata.interface_names + let names = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + if relation_kind == "interfaces" { + metadata.interface_names + } else { + metadata.trait_names + } + } else if relation_kind == "interfaces" { + eval_reflection_aot_class_interface_names(&reflected_name, values)? } else { - metadata.trait_names - } - } else if relation_kind == "interfaces" { - eval_reflection_aot_class_interface_names(&reflected_name, values)? - } else { - Vec::new() - }; + Vec::new() + }; eval_reflection_class_object_map_result(&names, context, values).map(Some) } @@ -909,6 +939,63 @@ pub(in crate::interpreter) fn eval_reflection_set_accessible_result( values.null().map(Some) } +/// Handles eval-backed `ReflectionProperty` hook-inspection calls. +pub(in crate::interpreter) fn eval_reflection_property_hooks_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let Some((property_class, property)) = + eval_reflection_property_for_hooks(&declaring_class, &property_name, context) + else { + return Ok(None); + }; + match method_name.to_ascii_lowercase().as_str() { + "hashooks" => { + eval_reflection_bind_no_args(evaluated_args)?; + let has_hooks = !eval_reflection_property_hook_kinds(&property).is_empty(); + values.bool_value(has_hooks).map(Some) + } + "hashook" => { + let hook = eval_reflection_property_hook_arg(evaluated_args, context, values)?; + values + .bool_value(eval_reflection_property_has_hook(&property, hook)) + .map(Some) + } + "gethook" => { + let hook = eval_reflection_property_hook_arg(evaluated_args, context, values)?; + if !eval_reflection_property_has_hook(&property, hook) { + return values.null().map(Some); + } + eval_reflection_property_hook_method_object( + &property_class, + &property, + hook, + context, + values, + ) + .map(Some) + } + "gethooks" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_property_hook_method_array(&property_class, &property, context, values) + .map(Some) + } + _ => Ok(None), + } +} + /// Handles eval-backed `ReflectionProperty::getValue()` calls. pub(in crate::interpreter) fn eval_reflection_property_get_value_result( identity: u64, @@ -1272,10 +1359,11 @@ fn eval_reflection_constant_matches_filter( let Some(filter) = filter else { return true; }; - eval_reflection_class_constant_metadata(reflected_name, constant_name, context) - .is_some_and(|(_, _, visibility, is_final, _)| { + eval_reflection_class_constant_metadata(reflected_name, constant_name, context).is_some_and( + |(_, _, visibility, is_final, _)| { eval_reflection_class_constant_modifiers(visibility, is_final) & filter != 0 - }) + }, + ) } /// Resolves the declared member spelling for eval `ReflectionClass` single-member lookups. @@ -4069,6 +4157,215 @@ fn eval_reflection_property_set_value_args( Ok((object_or_value, bound_args[1])) } +/// Returns the eval property metadata eligible for ReflectionProperty hook APIs. +fn eval_reflection_property_for_hooks( + declaring_class: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassProperty)> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + return context.class_property(declaring_class, property_name); + } + context.trait_decl(declaring_class).and_then(|trait_decl| { + trait_decl + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| (trait_decl.name().to_string(), property.clone())) + }) +} + +/// Binds the `PropertyHookType $type` argument used by ReflectionProperty hook APIs. +fn eval_reflection_property_hook_arg( + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = bind_evaluated_function_args(&[String::from("type")], evaluated_args)?; + eval_reflection_property_hook_type(args[0], context, values) +} + +/// Converts one synthetic `PropertyHookType` object into an eval reflection hook kind. +fn eval_reflection_property_hook_type( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(value)?; + if !context.dynamic_object_is_class(identity, "PropertyHookType") { + return Err(EvalStatus::RuntimeFatal); + } + let hook_value = values.property_get(value, "value")?; + match eval_reflection_string_arg(hook_value, values)?.as_str() { + "get" => Ok(EvalReflectionPropertyHook::Get), + "set" => Ok(EvalReflectionPropertyHook::Set), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns concrete hook kinds declared on one eval property. +fn eval_reflection_property_hook_kinds( + property: &EvalClassProperty, +) -> Vec { + let mut hooks = Vec::new(); + if property.has_get_hook() { + hooks.push(EvalReflectionPropertyHook::Get); + } + if property.has_set_hook() { + hooks.push(EvalReflectionPropertyHook::Set); + } + hooks +} + +/// Returns whether one eval property exposes the requested concrete hook. +fn eval_reflection_property_has_hook( + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> bool { + match hook { + EvalReflectionPropertyHook::Get => property.has_get_hook(), + EvalReflectionPropertyHook::Set => property.has_set_hook(), + } +} + +/// Builds PHP's string-keyed ReflectionMethod map returned by `getHooks()`. +fn eval_reflection_property_hook_method_array( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let hooks = eval_reflection_property_hook_kinds(property); + let mut result = values.assoc_new(hooks.len())?; + for hook in hooks { + let key = values.string(hook.key())?; + let method = eval_reflection_property_hook_method_object( + declaring_class, + property, + hook, + context, + values, + )?; + result = values.array_set(result, key, method)?; + } + Ok(result) +} + +/// Materializes a ReflectionMethod object for one concrete property hook. +fn eval_reflection_property_hook_method_object( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let metadata = eval_reflection_property_hook_method_metadata(declaring_class, property, hook); + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &hook.reflected_method_name(property.name()), + &metadata, + context, + values, + ) +} + +/// Builds ReflectionMethod metadata for one eval property hook accessor. +fn eval_reflection_property_hook_method_metadata( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> EvalReflectionMemberMetadata { + let parameters = eval_reflection_property_hook_parameters(declaring_class, property, hook); + let required_parameter_count = parameters.len(); + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class.to_string()), + attributes: Vec::new(), + visibility: property.visibility(), + is_static: false, + is_final: false, + is_abstract: false, + is_readonly: false, + is_promoted: false, + modifiers: eval_reflection_method_modifiers(property.visibility(), false, false, false), + type_metadata: None, + return_type_metadata: eval_reflection_property_hook_return_type(property, hook), + default_value: None, + required_parameter_count, + parameters, + } +} + +/// Builds the synthetic setter parameter metadata exposed by PHP hook reflection. +fn eval_reflection_property_hook_parameters( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> Vec { + if !matches!(hook, EvalReflectionPropertyHook::Set) { + return Vec::new(); + } + let type_metadata = property + .property_type() + .and_then(eval_reflection_parameter_type_metadata); + let has_type = type_metadata.is_some(); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: hook.reflected_method_name(property.name()), + declaring_class_name: Some(declaring_class.to_string()), + attributes: Vec::new(), + flags: eval_reflection_member_flags(property.visibility(), false, false, false, false), + required_parameter_count: 1, + }; + vec![EvalReflectionParameterMetadata { + name: "value".to_string(), + declaring_class_name: Some(declaring_class.to_string()), + declaring_function: Some(declaring_function), + attributes: Vec::new(), + position: 0, + is_optional: false, + is_variadic: false, + is_passed_by_reference: false, + is_promoted: false, + has_type, + allows_null: type_metadata + .as_ref() + .is_some_and(eval_reflection_type_allows_null), + type_metadata, + default_value: None, + default_value_constant_name: None, + }] +} + +/// Returns the ReflectionMethod return type metadata for a property hook. +fn eval_reflection_property_hook_return_type( + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> Option { + match hook { + EvalReflectionPropertyHook::Get => property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + EvalReflectionPropertyHook::Set => Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(eval_reflection_builtin_named_type( + "void", false, + )), + }), + } +} + +/// Maps PHP-visible property-hook method names back to eval's synthetic method names. +fn eval_reflection_property_hook_synthetic_method_name(method_name: &str) -> Option { + let body = method_name.strip_prefix('$')?; + let (property_name, hook_name) = body.rsplit_once("::")?; + match hook_name { + "get" => Some(EvalReflectionPropertyHook::Get.synthetic_method_name(property_name)), + "set" => Some(EvalReflectionPropertyHook::Set.synthetic_method_name(property_name)), + _ => None, + } +} + /// Binds `ReflectionMethod::invoke()` arguments and preserves forwarded named args. fn eval_reflection_method_invoke_args( evaluated_args: Vec, @@ -4153,7 +4450,10 @@ fn eval_reflection_method_invoke_dispatch( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if let Some((method_class, method)) = context.class_method(declaring_class, method_name) { + let lookup_method_name = eval_reflection_property_hook_synthetic_method_name(method_name) + .unwrap_or_else(|| method_name.to_string()); + if let Some((method_class, method)) = context.class_method(declaring_class, &lookup_method_name) + { if method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 2b7439adbc..b6e7948938 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2429,6 +2429,11 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( { return Ok(value); } + if let Some(value) = + eval_builtin_property_hook_type_case(class_name, constant_name, context, values)? + { + return Ok(value); + } let class_name = resolve_eval_static_class_like_name(class_name, context)?; if let Some(case) = context.enum_case(&class_name, constant_name) { return Ok(case); @@ -2495,6 +2500,50 @@ fn eval_builtin_reflection_class_constant( value.map(|value| values.int(value)).transpose() } +/// Resolves eval-visible `PropertyHookType` builtin enum cases. +fn eval_builtin_property_hook_type_case( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("PropertyHookType") + { + return Ok(None); + } + let Some((case_name, case_value)) = eval_property_hook_type_case_parts(constant_name) else { + return Ok(None); + }; + if let Some(case) = context.enum_case("PropertyHookType", case_name) { + return Ok(Some(case)); + } + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, "PropertyHookType"); + let name = values.string(case_name)?; + values.property_set(object, "name", name)?; + let value = values.string(case_value)?; + values.property_set(object, "value", value)?; + if let Some(replaced) = context.set_enum_case_value("PropertyHookType", case_name, value) { + values.release(replaced)?; + } + if let Some(replaced) = context.set_enum_case("PropertyHookType", case_name, object) { + values.release(replaced)?; + } + Ok(Some(object)) +} + +/// Returns the PHP case name and backed value for a builtin property-hook case. +fn eval_property_hook_type_case_parts(constant_name: &str) -> Option<(&'static str, &'static str)> { + match constant_name { + "Get" => Some(("Get", "get")), + "Set" => Some(("Set", "set")), + _ => None, + } +} + /// Returns the PHP class-name literal for `ClassName::class`-style eval expressions. pub(in crate::interpreter) fn eval_class_name_fetch_result( class_name: &str, @@ -3190,6 +3239,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_property_hooks_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_get_value_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index f5832f7b33..ba28b32256 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1880,6 +1880,60 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty exposes eval property hook metadata and methods. +#[test] +fn execute_program_reflection_property_gets_eval_hook_metadata() { + let program = parse_fragment( + br#"class EvalReflectHookedProperty { + public int $raw = 2; + public int $doubled { + get { return $this->raw * 2; } + set { $this->raw = $value; } + } + public int $readonlyHook { + get => $this->raw + 1; + } + public int $plain = 5; +} +$hooked = new ReflectionProperty("EvalReflectHookedProperty", "doubled"); +$plain = new ReflectionProperty("EvalReflectHookedProperty", "plain"); +$readonly = new ReflectionProperty("EvalReflectHookedProperty", "readonlyHook"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +echo $getCase->name; echo ":"; echo $getCase->value; echo ":"; +echo $hooked->hasHooks() ? "H" : "h"; echo ":"; +echo $hooked->hasHook($getCase) ? "G" : "g"; echo ":"; +echo $hooked->hasHook(type: $setCase) ? "S" : "s"; echo ":"; +$hooks = $hooked->getHooks(); +echo count($hooks); echo ":"; echo $hooks["get"]->getName(); echo ":"; echo $hooks["set"]->getName(); echo ":"; +$get = $hooked->getHook($getCase); +$set = $hooked->getHook(type: $setCase); +echo $get->getDeclaringClass()->getName(); echo ":"; echo $get->getNumberOfParameters(); echo ":"; +echo $set->getNumberOfParameters(); echo ":"; echo $set->getParameters()[0]->getName(); echo ":"; +$box = new EvalReflectHookedProperty(); +echo $get->invoke($box); echo ":"; +$set->invoke($box, 7); +echo $box->raw; echo ":"; +echo $readonly->hasHook($getCase) ? "R" : "r"; echo ":"; +echo $readonly->hasHook($setCase) ? "w" : "W"; echo ":"; +echo $readonly->getHook($setCase) === null ? "N" : "n"; echo ":"; +echo $plain->hasHooks() ? "bad" : "plain"; echo ":"; +echo count($plain->getHooks()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Get:get:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes and mutates eval static property values. #[test] fn execute_program_reflection_class_static_property_values() { diff --git a/docs/php/eval.md b/docs/php/eval.md index dd59c17bb1..b55a5410a1 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -375,6 +375,11 @@ metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose materialized property default metadata, including PHP's implicit `null` default for untyped concrete properties without an explicit initializer. +`ReflectionProperty::hasHooks()`, `hasHook()`, `getHooks()`, and `getHook()` +expose eval-declared class property get/set hook metadata and return hook +`ReflectionMethod` objects using PHP's `$property::get` / `$property::set` +names. Eval also exposes `PropertyHookType::Get` and `PropertyHookType::Set` +inside evaluated fragments for those APIs. `ReflectionProperty::setAccessible()` is accepted as a PHP-compatible no-op. `ReflectionProperty::getValue()` and `setValue()` read and write eval-declared instance and static property values, bypass public/protected/private visibility diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c0ba5b6a25..0e6035de7d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8650,6 +8650,59 @@ echo $doubled->getValue($hook);'); assert_eq!(out.stdout, "base:changed:Ada:Grace:1:5:6:4:5:10"); } +/// Verifies eval ReflectionProperty exposes property hook metadata and hook methods. +#[test] +fn test_eval_reflection_property_hook_metadata() { + let out = compile_and_run_capture( + r#"raw * 2; } + set { $this->raw = $value; } + } + public int $readonlyHook { + get => $this->raw + 1; + } + public int $plain = 5; +} +$hooked = new ReflectionProperty("EvalReflectHookedProperty", "doubled"); +$plain = new ReflectionProperty("EvalReflectHookedProperty", "plain"); +$readonly = new ReflectionProperty("EvalReflectHookedProperty", "readonlyHook"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +echo $getCase->name . ":" . $getCase->value . ":"; +echo ($hooked->hasHooks() ? "H" : "h") . ":"; +echo ($hooked->hasHook($getCase) ? "G" : "g") . ":"; +echo ($hooked->hasHook(type: $setCase) ? "S" : "s") . ":"; +$hooks = $hooked->getHooks(); +echo count($hooks) . ":" . $hooks["get"]->getName() . ":" . $hooks["set"]->getName() . ":"; +$get = $hooked->getHook($getCase); +$set = $hooked->getHook(type: $setCase); +echo $get->getDeclaringClass()->getName() . ":" . $get->getNumberOfParameters() . ":"; +echo $set->getNumberOfParameters() . ":" . $set->getParameters()[0]->getName() . ":"; +$box = new EvalReflectHookedProperty(); +echo $get->invoke($box) . ":"; +$set->invoke($box, 7); +echo $box->raw . ":"; +echo ($readonly->hasHook($getCase) ? "R" : "r") . ":"; +echo ($readonly->hasHook($setCase) ? "w" : "W") . ":"; +echo ($readonly->getHook($setCase) === null ? "N" : "n") . ":"; +echo ($plain->hasHooks() ? "bad" : "plain") . ":"; +echo count($plain->getHooks());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Get:get:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0" + ); +} + /// Verifies eval ReflectionClass static-property APIs use current runtime values. #[test] fn test_eval_reflection_class_static_property_values() { From fe7fc437ab5fd488406acf0b6bbb59ccc04952b8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 01:17:17 +0200 Subject: [PATCH 0503/1208] Support eval abstract property hook reflection --- .../elephc-eval/src/interpreter/reflection.rs | 51 ++++++++++++++----- .../tests/builtins_class_metadata.rs | 23 ++++++++- docs/php/eval.md | 9 ++-- tests/codegen/eval.rs | 23 ++++++++- 4 files changed, 84 insertions(+), 22 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index d15717a112..dc94263d65 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -2378,7 +2378,7 @@ fn eval_reflection_owner_object_with_members( context.register_eval_reflection_function(identity, reflected_name); } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { if let Some(declaring_class) = parent_class_name { - if context.has_class(declaring_class) { + if eval_reflection_class_like_exists(declaring_class, context) { let identity = values.object_identity(object)?; context.register_eval_reflection_property( identity, @@ -4166,13 +4166,31 @@ fn eval_reflection_property_for_hooks( if context.has_class(declaring_class) || context.has_enum(declaring_class) { return context.class_property(declaring_class, property_name); } - context.trait_decl(declaring_class).and_then(|trait_decl| { - trait_decl - .properties() - .iter() - .find(|property| property.name() == property_name) - .map(|property| (trait_decl.name().to_string(), property.clone())) - }) + context + .trait_decl(declaring_class) + .and_then(|trait_decl| { + trait_decl + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| (trait_decl.name().to_string(), property.clone())) + }) + .or_else(|| { + context + .interface_property_requirements(declaring_class) + .into_iter() + .find(|property| property.name() == property_name) + .map(|property| { + let property = EvalClassProperty::new(property.name(), None) + .with_type(property.property_type().cloned()) + .with_attributes(property.attributes().to_vec()) + .with_abstract_hook_contract( + property.requires_get(), + property.requires_set(), + ); + (declaring_class.to_string(), property) + }) + }) } /// Binds the `PropertyHookType $type` argument used by ReflectionProperty hook APIs. @@ -4211,10 +4229,10 @@ fn eval_reflection_property_hook_kinds( property: &EvalClassProperty, ) -> Vec { let mut hooks = Vec::new(); - if property.has_get_hook() { + if property.has_get_hook() || property.requires_get_hook() { hooks.push(EvalReflectionPropertyHook::Get); } - if property.has_set_hook() { + if property.has_set_hook() || property.requires_set_hook() { hooks.push(EvalReflectionPropertyHook::Set); } hooks @@ -4226,8 +4244,8 @@ fn eval_reflection_property_has_hook( hook: EvalReflectionPropertyHook, ) -> bool { match hook { - EvalReflectionPropertyHook::Get => property.has_get_hook(), - EvalReflectionPropertyHook::Set => property.has_set_hook(), + EvalReflectionPropertyHook::Get => property.has_get_hook() || property.requires_get_hook(), + EvalReflectionPropertyHook::Set => property.has_set_hook() || property.requires_set_hook(), } } @@ -4286,10 +4304,15 @@ fn eval_reflection_property_hook_method_metadata( visibility: property.visibility(), is_static: false, is_final: false, - is_abstract: false, + is_abstract: property.is_abstract(), is_readonly: false, is_promoted: false, - modifiers: eval_reflection_method_modifiers(property.visibility(), false, false, false), + modifiers: eval_reflection_method_modifiers( + property.visibility(), + false, + false, + property.is_abstract(), + ), type_metadata: None, return_type_metadata: eval_reflection_property_hook_return_type(property, hook), default_value: None, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index ba28b32256..bda1188c24 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1895,9 +1895,17 @@ fn execute_program_reflection_property_gets_eval_hook_metadata() { } public int $plain = 5; } +abstract class EvalReflectAbstractHookProperty { + abstract public int $contract { get; set; } +} +interface EvalReflectInterfaceHookProperty { + public int $iface { get; } +} $hooked = new ReflectionProperty("EvalReflectHookedProperty", "doubled"); $plain = new ReflectionProperty("EvalReflectHookedProperty", "plain"); $readonly = new ReflectionProperty("EvalReflectHookedProperty", "readonlyHook"); +$abstract = new ReflectionProperty("EvalReflectAbstractHookProperty", "contract"); +$iface = new ReflectionProperty("EvalReflectInterfaceHookProperty", "iface"); $getCase = PropertyHookType::Get; $setCase = PropertyHookType::Set; echo $getCase->name; echo ":"; echo $getCase->value; echo ":"; @@ -1918,7 +1926,18 @@ echo $readonly->hasHook($getCase) ? "R" : "r"; echo ":"; echo $readonly->hasHook($setCase) ? "w" : "W"; echo ":"; echo $readonly->getHook($setCase) === null ? "N" : "n"; echo ":"; echo $plain->hasHooks() ? "bad" : "plain"; echo ":"; -echo count($plain->getHooks()); +echo count($plain->getHooks()); echo ":"; +$abstractHooks = $abstract->getHooks(); +echo count($abstractHooks); echo ":"; +echo $abstract->hasHook($getCase) ? "AG" : "ag"; echo ":"; +echo $abstract->hasHook($setCase) ? "AS" : "as"; echo ":"; +echo $abstractHooks["get"]->getName(); echo ":"; echo $abstractHooks["get"]->isAbstract() ? "A" : "a"; echo ":"; +echo $abstractHooks["set"]->getName(); echo ":"; echo $abstractHooks["set"]->isAbstract() ? "A" : "a"; echo ":"; +$ifaceHook = $iface->getHook($getCase); +echo count($iface->getHooks()); echo ":"; +echo $iface->hasHook($getCase) ? "IG" : "ig"; echo ":"; +echo $iface->hasHook($setCase) ? "bad" : "is"; echo ":"; +echo $ifaceHook->isAbstract() ? "IA" : "ia"; return true;"#, ) .expect("parse eval fragment"); @@ -1929,7 +1948,7 @@ return true;"#, assert_eq!( values.output, - "Get:get:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0" + "Get:get:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index b55a5410a1..895ebab473 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -376,10 +376,11 @@ metadata through `ReflectionNamedType`, `ReflectionUnionType`, and materialized property default metadata, including PHP's implicit `null` default for untyped concrete properties without an explicit initializer. `ReflectionProperty::hasHooks()`, `hasHook()`, `getHooks()`, and `getHook()` -expose eval-declared class property get/set hook metadata and return hook -`ReflectionMethod` objects using PHP's `$property::get` / `$property::set` -names. Eval also exposes `PropertyHookType::Get` and `PropertyHookType::Set` -inside evaluated fragments for those APIs. +expose eval-declared concrete, abstract, and interface property get/set hook +metadata and return hook `ReflectionMethod` objects using PHP's +`$property::get` / `$property::set` names. Eval also exposes +`PropertyHookType::Get` and `PropertyHookType::Set` inside evaluated fragments +for those APIs. `ReflectionProperty::setAccessible()` is accepted as a PHP-compatible no-op. `ReflectionProperty::getValue()` and `setValue()` read and write eval-declared instance and static property values, bypass public/protected/private visibility diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0e6035de7d..dd194e86ca 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8666,9 +8666,17 @@ eval('class EvalReflectHookedProperty { } public int $plain = 5; } +abstract class EvalReflectAbstractHookProperty { + abstract public int $contract { get; set; } +} +interface EvalReflectInterfaceHookProperty { + public int $iface { get; } +} $hooked = new ReflectionProperty("EvalReflectHookedProperty", "doubled"); $plain = new ReflectionProperty("EvalReflectHookedProperty", "plain"); $readonly = new ReflectionProperty("EvalReflectHookedProperty", "readonlyHook"); +$abstract = new ReflectionProperty("EvalReflectAbstractHookProperty", "contract"); +$iface = new ReflectionProperty("EvalReflectInterfaceHookProperty", "iface"); $getCase = PropertyHookType::Get; $setCase = PropertyHookType::Set; echo $getCase->name . ":" . $getCase->value . ":"; @@ -8689,7 +8697,18 @@ echo ($readonly->hasHook($getCase) ? "R" : "r") . ":"; echo ($readonly->hasHook($setCase) ? "w" : "W") . ":"; echo ($readonly->getHook($setCase) === null ? "N" : "n") . ":"; echo ($plain->hasHooks() ? "bad" : "plain") . ":"; -echo count($plain->getHooks());'); +echo count($plain->getHooks()) . ":"; +$abstractHooks = $abstract->getHooks(); +echo count($abstractHooks) . ":"; +echo ($abstract->hasHook($getCase) ? "AG" : "ag") . ":"; +echo ($abstract->hasHook($setCase) ? "AS" : "as") . ":"; +echo $abstractHooks["get"]->getName() . ":" . ($abstractHooks["get"]->isAbstract() ? "A" : "a") . ":"; +echo $abstractHooks["set"]->getName() . ":" . ($abstractHooks["set"]->isAbstract() ? "A" : "a") . ":"; +$ifaceHook = $iface->getHook($getCase); +echo count($iface->getHooks()) . ":"; +echo ($iface->hasHook($getCase) ? "IG" : "ig") . ":"; +echo ($iface->hasHook($setCase) ? "bad" : "is") . ":"; +echo $ifaceHook->isAbstract() ? "IA" : "ia";'); "#, ); assert!( @@ -8699,7 +8718,7 @@ echo count($plain->getHooks());'); ); assert_eq!( out.stdout, - "Get:get:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0" + "Get:get:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" ); } From 98159e895ab9151dd21e2fa589d4f1ec15bd2303 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 01:21:48 +0200 Subject: [PATCH 0504/1208] Support eval PropertyHookType enum methods --- .../elephc-eval/src/interpreter/statements.rs | 96 +++++++++++++++++++ .../tests/builtins_class_metadata.rs | 6 +- docs/php/eval.md | 3 +- tests/codegen/eval.rs | 6 +- 4 files changed, 108 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index b6e7948938..cc631f8837 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -2586,6 +2586,15 @@ pub(in crate::interpreter) fn eval_static_method_call_result( values: &mut impl RuntimeValueOps, ) -> Result { let class_name = resolve_eval_static_method_class_name(class_name, context)?; + if let Some(result) = eval_builtin_property_hook_type_static_method_result( + &class_name, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if context.has_enum(&class_name) && eval_enum_static_builtin_name(method_name).is_some() { return eval_enum_builtin_static_method_result( &class_name, @@ -2646,6 +2655,93 @@ pub(in crate::interpreter) fn eval_static_method_call_result( values.static_method_call(&class_name, method_name, args) } +/// Dispatches static methods for eval's builtin `PropertyHookType` enum slice. +fn eval_builtin_property_hook_type_static_method_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("PropertyHookType") + { + return Ok(None); + } + match eval_enum_static_builtin_name(method_name) { + Some("cases") => { + eval_builtin_property_hook_type_cases(evaluated_args, context, values).map(Some) + } + Some("from") => { + eval_builtin_property_hook_type_from(evaluated_args, false, context, values).map(Some) + } + Some("tryFrom") => { + eval_builtin_property_hook_type_from(evaluated_args, true, context, values).map(Some) + } + _ => Ok(None), + } +} + +/// Builds the indexed case array for eval's builtin `PropertyHookType` enum slice. +fn eval_builtin_property_hook_type_cases( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let case_names = ["Get", "Set"]; + let mut array = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let key = values.int(index as i64)?; + let case = + eval_builtin_property_hook_type_case("PropertyHookType", case_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + array = values.array_set(array, key, case)?; + } + Ok(array) +} + +/// Evaluates builtin `PropertyHookType::from()` or `tryFrom()` inside eval. +fn eval_builtin_property_hook_type_from( + evaluated_args: Vec, + nullable_miss: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut args = bind_evaluated_function_args(&[String::from("value")], evaluated_args)?; + let value = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + let bytes = values.string_bytes(value)?; + let value_text = String::from_utf8_lossy(&bytes); + for constant_name in ["Get", "Set"] { + let Some((_, case_value)) = eval_property_hook_type_case_parts(constant_name) else { + continue; + }; + if value_text == case_value { + return eval_builtin_property_hook_type_case( + "PropertyHookType", + constant_name, + context, + values, + )? + .ok_or(EvalStatus::RuntimeFatal); + } + } + if nullable_miss { + values.null() + } else { + let message = eval_enum_invalid_backing_value_message( + "PropertyHookType", + EvalEnumBackingType::String, + value, + values, + )?; + eval_throw_value_error(&message, context, values) + } +} + /// Returns a recognized enum-provided static method name. fn eval_enum_static_builtin_name(method_name: &str) -> Option<&'static str> { if method_name.eq_ignore_ascii_case("cases") { diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index bda1188c24..caec43690c 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1909,6 +1909,10 @@ $iface = new ReflectionProperty("EvalReflectInterfaceHookProperty", "iface"); $getCase = PropertyHookType::Get; $setCase = PropertyHookType::Set; echo $getCase->name; echo ":"; echo $getCase->value; echo ":"; +$caseList = PropertyHookType::cases(); +echo count($caseList); echo ":"; echo $caseList[0]->name; echo ":"; echo $caseList[1]->value; echo ":"; +echo PropertyHookType::from("set")->name; echo ":"; +echo PropertyHookType::tryFrom("missing") === null ? "T" : "t"; echo ":"; echo $hooked->hasHooks() ? "H" : "h"; echo ":"; echo $hooked->hasHook($getCase) ? "G" : "g"; echo ":"; echo $hooked->hasHook(type: $setCase) ? "S" : "s"; echo ":"; @@ -1948,7 +1952,7 @@ return true;"#, assert_eq!( values.output, - "Get:get:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" + "Get:get:2:Get:set:Set:T:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 895ebab473..522c1577ab 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -380,7 +380,8 @@ expose eval-declared concrete, abstract, and interface property get/set hook metadata and return hook `ReflectionMethod` objects using PHP's `$property::get` / `$property::set` names. Eval also exposes `PropertyHookType::Get` and `PropertyHookType::Set` inside evaluated fragments -for those APIs. +for those APIs, including `PropertyHookType::cases()`, `from()`, and +`tryFrom()`. `ReflectionProperty::setAccessible()` is accepted as a PHP-compatible no-op. `ReflectionProperty::getValue()` and `setValue()` read and write eval-declared instance and static property values, bypass public/protected/private visibility diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index dd194e86ca..b3fa241d03 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8680,6 +8680,10 @@ $iface = new ReflectionProperty("EvalReflectInterfaceHookProperty", "iface"); $getCase = PropertyHookType::Get; $setCase = PropertyHookType::Set; echo $getCase->name . ":" . $getCase->value . ":"; +$caseList = PropertyHookType::cases(); +echo count($caseList) . ":" . $caseList[0]->name . ":" . $caseList[1]->value . ":"; +echo PropertyHookType::from("set")->name . ":"; +echo (PropertyHookType::tryFrom("missing") === null ? "T" : "t") . ":"; echo ($hooked->hasHooks() ? "H" : "h") . ":"; echo ($hooked->hasHook($getCase) ? "G" : "g") . ":"; echo ($hooked->hasHook(type: $setCase) ? "S" : "s") . ":"; @@ -8718,7 +8722,7 @@ echo $ifaceHook->isAbstract() ? "IA" : "ia";'); ); assert_eq!( out.stdout, - "Get:get:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" + "Get:get:2:Get:set:Set:T:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" ); } From 2009b1f749cc4b84e5dde695f7c7af9ad289ad31 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 01:37:08 +0200 Subject: [PATCH 0505/1208] Support eval ReflectionProperty raw values --- crates/elephc-eval/src/eval_ir.rs | 15 ++ .../elephc-eval/src/interpreter/reflection.rs | 115 ++++++++- .../elephc-eval/src/interpreter/statements.rs | 9 + .../tests/builtins_class_metadata.rs | 53 +++- crates/elephc-eval/src/parser/statements.rs | 232 +++++++++++++++++- docs/php/eval.md | 4 + tests/codegen/eval.rs | 50 +++- 7 files changed, 465 insertions(+), 13 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index b94ae1c169..2ed5928ea0 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1406,6 +1406,7 @@ pub struct EvalClassProperty { has_set_hook: bool, requires_get_hook: bool, requires_set_hook: bool, + is_virtual: bool, default: Option, } @@ -1475,6 +1476,7 @@ impl EvalClassProperty { has_set_hook: false, requires_get_hook: false, requires_set_hook: false, + is_virtual: false, default, } } @@ -1483,6 +1485,13 @@ impl EvalClassProperty { pub const fn with_hooks(mut self, has_get_hook: bool, has_set_hook: bool) -> Self { self.has_get_hook = has_get_hook; self.has_set_hook = has_set_hook; + self.is_virtual = has_get_hook || has_set_hook; + self + } + + /// Returns a copy of this property with explicit hook virtuality metadata. + pub const fn with_virtual(mut self, is_virtual: bool) -> Self { + self.is_virtual = is_virtual; self } @@ -1495,6 +1504,7 @@ impl EvalClassProperty { self.is_abstract = true; self.requires_get_hook = requires_get_hook; self.requires_set_hook = requires_set_hook; + self.is_virtual = true; self } @@ -1581,6 +1591,11 @@ impl EvalClassProperty { self.requires_set_hook } + /// Returns whether this property is virtual instead of backed by object storage. + pub const fn is_virtual(&self) -> bool { + self.is_virtual + } + /// Returns the property initializer expression, when one was declared. pub fn default(&self) -> Option<&EvalExpr> { self.default.as_ref() diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index dc94263d65..33a5b79132 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1091,6 +1091,49 @@ pub(in crate::interpreter) fn eval_reflection_property_set_value_result( values.null().map(Some) } +/// Handles eval-backed `ReflectionProperty::getRawValue()` and `setRawValue()` calls. +pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + if method_name.eq_ignore_ascii_case("getRawValue") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + return eval_reflection_instance_property_get_raw_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + .map(Some); + } + if method_name.eq_ignore_ascii_case("setRawValue") { + let (object, value) = eval_reflection_property_set_raw_value_args(evaluated_args)?; + eval_reflection_instance_property_set_raw_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + return values.null().map(Some); + } + Ok(None) +} + /// Handles eval-backed `ReflectionClass::getReflectionConstant()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_result( identity: u64, @@ -3558,12 +3601,9 @@ fn eval_reflection_property_modifiers( modifiers } -/// Returns whether an eval property is virtual because it has or requires hooks. +/// Returns whether eval retained this property as virtual rather than backed. fn eval_reflection_property_is_virtual(property: &EvalClassProperty) -> bool { - property.has_get_hook() - || property.has_set_hook() - || property.requires_get_hook() - || property.requires_set_hook() + property.is_virtual() } /// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from eval member flags. @@ -4157,6 +4197,25 @@ fn eval_reflection_property_set_value_args( Ok((object_or_value, bound_args[1])) } +/// Binds the required object argument for `ReflectionProperty::getRawValue()`. +fn eval_reflection_property_raw_value_arg( + evaluated_args: Vec, +) -> Result { + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + Ok(args[0]) +} + +/// Binds the object and value arguments for `ReflectionProperty::setRawValue()`. +fn eval_reflection_property_set_raw_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("object"), String::from("value")], + evaluated_args, + )?; + Ok((args[0], args[1])) +} + /// Returns the eval property metadata eligible for ReflectionProperty hook APIs. fn eval_reflection_property_for_hooks( declaring_class: &str, @@ -4683,6 +4742,52 @@ fn eval_reflection_instance_property_set_value( values.property_set(object, &storage_property_name, value) } +/// Reads one eval instance property through ReflectionProperty raw-storage semantics. +fn eval_reflection_instance_property_get_raw_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_get(object, &storage_property_name) +} + +/// Writes one eval instance property through ReflectionProperty raw-storage semantics. +fn eval_reflection_instance_property_set_raw_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_reflection_property_write(declaring_class, &property, context)?; + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_set(object, &storage_property_name, value) +} + /// Resolves and validates the object/property pair targeted by ReflectionProperty. fn eval_reflection_instance_property_target( declaring_class: &str, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index cc631f8837..3e2e5775b4 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3353,6 +3353,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_property_raw_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_set_value_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index caec43690c..76326e67ae 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1846,6 +1846,16 @@ class EvalReflectValueHook { get => $this->raw * 2; set { $this->raw = $value + 1; } } + public $backed { + get { return $this->backed * 2; } + set { $this->backed = $value; } + } + public $virtual { + get => $this->raw + 100; + } + public function __construct() { + $this->backed = 2; + } } $child = new EvalReflectValueChild(); $secret = new ReflectionProperty("EvalReflectValueBase", "secret"); @@ -1867,7 +1877,18 @@ $doubled = new ReflectionProperty("EvalReflectValueHook", "doubled"); echo $doubled->getValue($hook); echo ":"; $doubled->setValue($hook, 4); echo $hook->raw; echo ":"; -echo $doubled->getValue($hook); +echo $doubled->getValue($hook); echo ":"; +$backed = new ReflectionProperty("EvalReflectValueHook", "backed"); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +$backed->setValue($hook, 4); +echo $backed->getRawValue(object: $hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +$backed->setRawValue(object: $hook, value: 7); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +echo $backed->getModifiers(); echo ":"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers(); return true;"#, ) .expect("parse eval fragment"); @@ -1876,10 +1897,38 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "base:changed:Ada:Grace:1:5:6:4:5:10"); + assert_eq!( + values.output, + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:1:513" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty raw APIs reject virtual eval property hooks. +#[test] +fn execute_program_reflection_property_rejects_virtual_raw_value() { + let program = parse_fragment( + br#"class EvalReflectVirtualRawHook { + public $raw = 2; + public $virtual { + get => $this->raw * 2; + } +} +$object = new EvalReflectVirtualRawHook(); +$property = new ReflectionProperty("EvalReflectVirtualRawHook", "virtual"); +$property->getRawValue($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("virtual raw property read should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies ReflectionProperty exposes eval property hook metadata and methods. #[test] fn execute_program_reflection_property_gets_eval_hook_metadata() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 2cc32c2d1e..90a2953041 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -14,9 +14,9 @@ use crate::errors::EvalParseError; use crate::eval_ir::{ EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalCallArg, EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, - EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInterface, EvalInterfaceMethod, - EvalInterfaceProperty, EvalParameterType, EvalParameterTypeVariant, EvalStmt, EvalSwitchCase, - EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, + EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInstanceOfTarget, EvalInterface, + EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, EvalParameterTypeVariant, + EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::lexer::TokenKind; @@ -931,6 +931,8 @@ impl Parser { let default_is_some = default.is_some(); let (has_get_hook, has_set_hook, hook_methods) = self.parse_property_hook_tail(&name, is_static, effective_readonly, default_is_some)?; + let is_virtual = (has_get_hook || has_set_hook) + && !property_hook_methods_use_backing_slot(&hook_methods, &name); let property = EvalClassProperty::with_visibility_static_final_and_readonly( name, visibility, @@ -940,7 +942,8 @@ impl Parser { default, ) .with_type(property_type) - .with_hooks(has_get_hook, has_set_hook); + .with_hooks(has_get_hook, has_set_hook) + .with_virtual(is_virtual); Ok((property, hook_methods)) } @@ -2574,6 +2577,227 @@ fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { } } +/// Returns whether any parsed property hook accessor uses its own backing slot. +fn property_hook_methods_use_backing_slot( + hook_methods: &[EvalClassMethod], + property_name: &str, +) -> bool { + hook_methods.iter().any(|method| { + method + .body() + .iter() + .any(|stmt| eval_stmt_uses_this_property(stmt, property_name)) + }) +} + +/// Returns whether one statement touches `$this->{$property_name}` directly. +fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { + match stmt { + EvalStmt::ArrayAppendVar { value, .. } => { + eval_expr_uses_this_property(value, property_name) + } + EvalStmt::ArraySetVar { index, value, .. } => { + eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::Break + | EvalStmt::Continue + | EvalStmt::ClassDecl(_) + | EvalStmt::EnumDecl(_) + | EvalStmt::FunctionDecl { .. } + | EvalStmt::Global { .. } + | EvalStmt::InterfaceDecl(_) + | EvalStmt::ReferenceAssign { .. } + | EvalStmt::TraitDecl(_) + | EvalStmt::UnsetVar { .. } => false, + EvalStmt::DoWhile { body, condition } | EvalStmt::While { condition, body } => { + eval_expr_uses_this_property(condition, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::Echo(expr) + | EvalStmt::Expr(expr) + | EvalStmt::StaticVar { init: expr, .. } + | EvalStmt::Throw(expr) => eval_expr_uses_this_property(expr, property_name), + EvalStmt::For { + init, + condition, + update, + body, + } => { + eval_stmt_list_uses_this_property(init, property_name) + || condition + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_stmt_list_uses_this_property(update, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::Foreach { array, body, .. } => { + eval_expr_uses_this_property(array, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::If { + condition, + then_branch, + else_branch, + } => { + eval_expr_uses_this_property(condition, property_name) + || eval_stmt_list_uses_this_property(then_branch, property_name) + || eval_stmt_list_uses_this_property(else_branch, property_name) + } + EvalStmt::Return(expr) => expr + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)), + EvalStmt::PropertyReferenceBind { + object, property, .. + } + | EvalStmt::UnsetProperty { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalStmt::PropertySet { + object, + property, + value, + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::StaticPropertySet { value, .. } | EvalStmt::StoreVar { value, .. } => { + eval_expr_uses_this_property(value, property_name) + } + EvalStmt::Switch { expr, cases } => { + eval_expr_uses_this_property(expr, property_name) + || cases.iter().any(|case| { + case.condition + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_stmt_list_uses_this_property(&case.body, property_name) + }) + } + EvalStmt::Try { + body, + catches, + finally_body, + } => { + eval_stmt_list_uses_this_property(body, property_name) + || catches + .iter() + .any(|catch| eval_stmt_list_uses_this_property(&catch.body, property_name)) + || eval_stmt_list_uses_this_property(finally_body, property_name) + } + } +} + +/// Returns whether any statement in a list touches `$this->{$property_name}` directly. +fn eval_stmt_list_uses_this_property(stmts: &[EvalStmt], property_name: &str) -> bool { + stmts + .iter() + .any(|stmt| eval_stmt_uses_this_property(stmt, property_name)) +} + +/// Returns whether one expression touches `$this->{$property_name}` directly. +fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { + match expr { + EvalExpr::Array(elements) => elements.iter().any(|element| match element { + EvalArrayElement::Value(value) => eval_expr_uses_this_property(value, property_name), + EvalArrayElement::KeyValue { key, value } => { + eval_expr_uses_this_property(key, property_name) + || eval_expr_uses_this_property(value, property_name) + } + }), + EvalExpr::ArrayGet { array, index } => { + eval_expr_uses_this_property(array, property_name) + || eval_expr_uses_this_property(index, property_name) + } + EvalExpr::Call { args, .. } + | EvalExpr::NamespacedCall { args, .. } + | EvalExpr::NewObject { args, .. } + | EvalExpr::StaticMethodCall { args, .. } => args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), + EvalExpr::DynamicCall { callee, args } => { + eval_expr_uses_this_property(callee, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::Const(_) + | EvalExpr::ConstFetch(_) + | EvalExpr::ClassConstantFetch { .. } + | EvalExpr::ClassNameFetch { .. } + | EvalExpr::LoadVar(_) + | EvalExpr::Magic(_) + | EvalExpr::NamespacedConstFetch { .. } + | EvalExpr::StaticPropertyGet { .. } => false, + EvalExpr::Include { path, .. } + | EvalExpr::Clone(path) + | EvalExpr::Print(path) + | EvalExpr::Unary { expr: path, .. } => eval_expr_uses_this_property(path, property_name), + EvalExpr::InstanceOf { value, target } => { + eval_expr_uses_this_property(value, property_name) + || matches!( + target, + EvalInstanceOfTarget::Expr(target) + if eval_expr_uses_this_property(target, property_name) + ) + } + EvalExpr::Match { + subject, + arms, + default, + } => { + eval_expr_uses_this_property(subject, property_name) + || arms.iter().any(|arm| { + arm.patterns + .iter() + .any(|pattern| eval_expr_uses_this_property(pattern, property_name)) + || eval_expr_uses_this_property(&arm.value, property_name) + }) + || default + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + } + EvalExpr::MethodCall { object, args, .. } => { + eval_expr_uses_this_property(object, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::NewAnonymousClass { args, .. } => args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), + EvalExpr::NullCoalesce { value, default } => { + eval_expr_uses_this_property(value, property_name) + || eval_expr_uses_this_property(default, property_name) + } + EvalExpr::PropertyGet { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_expr_uses_this_property(condition, property_name) + || then_branch + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_expr_uses_this_property(else_branch, property_name) + } + EvalExpr::Binary { left, right, .. } => { + eval_expr_uses_this_property(left, property_name) + || eval_expr_uses_this_property(right, property_name) + } + } +} + +/// Returns whether one object/property pair is exactly `$this->{$property_name}`. +fn eval_is_this_property(object: &EvalExpr, property: &str, property_name: &str) -> bool { + matches!(object, EvalExpr::LoadVar(name) if name == "this") && property == property_name +} + /// Returns the synthetic get-hook method name for one property. fn property_hook_get_method(property_name: &str) -> String { format!("__propget_{property_name}") diff --git a/docs/php/eval.md b/docs/php/eval.md index 522c1577ab..b3db37f28f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -387,6 +387,10 @@ for those APIs, including `PropertyHookType::cases()`, `from()`, and instance and static property values, bypass public/protected/private visibility like PHP reflection, route concrete property hooks through their accessors, and still reject readonly writes. +`ReflectionProperty::getRawValue()` and `setRawValue()` are supported for +eval-declared backed instance properties, including backed property hooks, and +bypass concrete property hook accessors. Virtual property hooks reject raw +access like PHP. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b3fa241d03..bb56a1b095 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8618,6 +8618,16 @@ class EvalReflectValueHook { get => $this->raw * 2; set { $this->raw = $value + 1; } } + public $backed { + get { return $this->backed * 2; } + set { $this->backed = $value; } + } + public $virtual { + get => $this->raw + 100; + } + public function __construct() { + $this->backed = 2; + } } $child = new EvalReflectValueChild(); $secret = new ReflectionProperty("EvalReflectValueBase", "secret"); @@ -8639,7 +8649,18 @@ $doubled = new ReflectionProperty("EvalReflectValueHook", "doubled"); echo $doubled->getValue($hook) . ":"; $doubled->setValue($hook, 4); echo $hook->raw . ":"; -echo $doubled->getValue($hook);'); +echo $doubled->getValue($hook) . ":"; +$backed = new ReflectionProperty("EvalReflectValueHook", "backed"); +echo $backed->getRawValue($hook) . ":"; +echo $backed->getValue($hook) . ":"; +$backed->setValue($hook, 4); +echo $backed->getRawValue(object: $hook) . ":"; +echo $backed->getValue($hook) . ":"; +$backed->setRawValue(object: $hook, value: 7); +echo $backed->getRawValue($hook) . ":"; +echo $backed->getValue($hook) . ":"; +echo $backed->getModifiers() . ":"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers();'); "#, ); assert!( @@ -8647,7 +8668,32 @@ echo $doubled->getValue($hook);'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "base:changed:Ada:Grace:1:5:6:4:5:10"); + assert_eq!( + out.stdout, + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:1:513" + ); +} + +/// Verifies eval ReflectionProperty raw APIs reject virtual property hooks. +#[test] +fn test_eval_reflection_property_virtual_raw_value_fails() { + let err = compile_and_run_expect_failure( + r#" $this->raw * 2; + } +} +$object = new EvalReflectVirtualRawHook(); +$property = new ReflectionProperty("EvalReflectVirtualRawHook", "virtual"); +$property->getRawValue($object);'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); } /// Verifies eval ReflectionProperty exposes property hook metadata and hook methods. From a284cfbf018994b3d59389e2e5c8396161e97fed Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 01:47:34 +0200 Subject: [PATCH 0506/1208] Support eval ReflectionProperty isVirtual --- .../elephc-eval/src/interpreter/reflection.rs | 4 ++++ .../tests/builtins_class_metadata.rs | 4 +++- .../interpreter/tests/support/object_ops.rs | 8 ++++++++ docs/php/eval.md | 4 ++-- src/codegen/eval_reflection_owner_helpers.rs | 20 +++++++++++++++++++ src/codegen/lower_inst/objects/reflection.rs | 11 +++++++--- src/types/checker/builtin_types/reflection.rs | 10 ++++++++++ tests/codegen/eval.rs | 4 +++- 8 files changed, 58 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 33a5b79132..f62e7ddccc 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -40,6 +40,7 @@ pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_PROPERTY: u64 pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT: u64 = 16; pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_PARAMETER: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; +const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -3061,6 +3062,9 @@ fn eval_reflection_member_object_result( if member.is_promoted { flags |= EVAL_REFLECTION_MEMBER_FLAG_PROMOTED; } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 512) != 0 { + flags |= EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL; + } let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { member.required_parameter_count as u64 } else { diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 76326e67ae..b3bfc83d7f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1888,6 +1888,8 @@ $backed->setRawValue(object: $hook, value: 7); echo $backed->getRawValue($hook); echo ":"; echo $backed->getValue($hook); echo ":"; echo $backed->getModifiers(); echo ":"; +echo $backed->isVirtual() ? "V" : "b"; echo ":"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->isVirtual() ? "V" : "b"; echo ":"; echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers(); return true;"#, ) @@ -1899,7 +1901,7 @@ return true;"#, assert_eq!( values.output, - "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:1:513" + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:1:b:V:513" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 07b3004a21..aa41a7bd05 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -19,6 +19,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; +const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -182,6 +183,10 @@ impl FakeOps { Self::object_property(&properties, "__is_promoted") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isvirtual") if args.is_empty() => { + Self::object_property(&properties, "__is_virtual") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "isanonymous") if args.is_empty() => { Self::object_property(&properties, "__is_anonymous") .map_or_else(|| self.bool_value(false), Ok) @@ -667,6 +672,8 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE) != 0)?; let is_promoted = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED) != 0)?; + let is_virtual = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL) != 0)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_readonly".to_string(), is_readonly)); @@ -674,6 +681,7 @@ impl FakeOps { properties.push(("__type".to_string(), method_objects)); properties.push(("__has_default_value".to_string(), has_default_value)); properties.push(("__is_promoted".to_string(), is_promoted)); + properties.push(("__is_virtual".to_string(), is_virtual)); properties.push(("__default_value".to_string(), property_objects)); } } diff --git a/docs/php/eval.md b/docs/php/eval.md index b3db37f28f..6c7ffd24ef 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -365,8 +365,8 @@ when the variadic container itself is not rebound, and are reported through `ReflectionParameter::isPassedByReference()` and `ReflectionParameter::canBePassedByValue()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, -`isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isDefault()`, and -`getModifiers()` report eval property metadata with PHP-compatible +`isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isVirtual()`, +`isDefault()`, and `getModifiers()` report eval property metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the bitmask. `isPromoted()` reports generated/AOT and eval-declared promoted-property metadata. `ReflectionProperty::hasType()` and `getType()` expose retained property type diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 664ba31671..7ee7db0636 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -110,6 +110,8 @@ struct ReflectionOwnerLayout { is_default_value_constant_hi: Option, is_promoted_lo: Option, is_promoted_hi: Option, + is_virtual_lo: Option, + is_virtual_hi: Option, default_value_lo: Option, default_value_hi: Option, default_value_constant_name_lo: Option, @@ -266,6 +268,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option { emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; @@ -5114,7 +5119,7 @@ fn reflection_property_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 flags.is_final, flags.is_abstract, flags.is_readonly, - false, + flags.is_virtual, None, ) } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 6174cdad0c..8ed62aca6c 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2726,6 +2726,12 @@ fn add_reflection_member_flag_methods( Some(bool_type()), false_bool(), )); + properties.push(builtin_property( + "__is_virtual", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); properties.push(builtin_property( "__default_value", Visibility::Private, @@ -2752,6 +2758,10 @@ fn add_reflection_member_flag_methods( "isPromoted", "__is_promoted", )); + methods.push(builtin_reflection_class_bool_method( + "isVirtual", + "__is_virtual", + )); methods.push(builtin_reflection_constant_bool_method("isDefault", true)); methods.push(builtin_reflection_class_mixed_method( "getDefaultValue", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bb56a1b095..afec95fefa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8660,6 +8660,8 @@ $backed->setRawValue(object: $hook, value: 7); echo $backed->getRawValue($hook) . ":"; echo $backed->getValue($hook) . ":"; echo $backed->getModifiers() . ":"; +echo $backed->isVirtual() ? "V:" : "b:"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->isVirtual() ? "V:" : "b:"; echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers();'); "#, ); @@ -8670,7 +8672,7 @@ echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers() ); assert_eq!( out.stdout, - "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:1:513" + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:1:b:V:513" ); } From 5e2deea8a9844b67d770029fdfc4f132846b3eaa Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 01:57:31 +0200 Subject: [PATCH 0507/1208] Support eval ReflectionProperty initialization state --- crates/elephc-eval/src/context.rs | 45 ++++++++++ .../elephc-eval/src/interpreter/reflection.rs | 83 ++++++++++++++++++- .../elephc-eval/src/interpreter/statements.rs | 46 +++++++--- .../tests/builtins_class_metadata.rs | 49 +++++++++++ docs/php/eval.md | 9 +- tests/codegen/eval.rs | 48 +++++++++++ 6 files changed, 264 insertions(+), 16 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index c2a19b31b9..3dd957a38f 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -315,6 +315,7 @@ pub struct ElephcEvalContext { included_files: HashSet, dynamic_objects: HashMap, dynamic_property_aliases: HashMap<(u64, String), EvalReferenceTarget>, + dynamic_initialized_properties: HashSet<(u64, String)>, eval_reflection_attributes: HashMap, eval_reflection_classes: HashMap, eval_reflection_functions: HashMap, @@ -368,6 +369,7 @@ impl ElephcEvalContext { included_files: HashSet::new(), dynamic_objects: HashMap::new(), dynamic_property_aliases: HashMap::new(), + dynamic_initialized_properties: HashSet::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), eval_reflection_functions: HashMap::new(), @@ -422,6 +424,7 @@ impl ElephcEvalContext { included_files: HashSet::new(), dynamic_objects: HashMap::new(), dynamic_property_aliases: HashMap::new(), + dynamic_initialized_properties: HashSet::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), eval_reflection_functions: HashMap::new(), @@ -824,6 +827,8 @@ impl ElephcEvalContext { .unwrap_or_else(|| class_name.to_string()); self.dynamic_objects .insert(identity, normalize_class_name(&class_name)); + self.dynamic_initialized_properties + .retain(|(object, _)| *object != identity); } /// Returns the dynamic eval class metadata associated with one object identity. @@ -871,6 +876,36 @@ impl ElephcEvalContext { .remove(&(identity, storage_property_name.to_string())) } + /// Marks one eval object storage slot as initialized. + pub fn mark_dynamic_property_initialized( + &mut self, + identity: u64, + storage_property_name: &str, + ) { + self.dynamic_initialized_properties + .insert((identity, storage_property_name.to_string())); + } + + /// Marks one eval object storage slot as uninitialized. + pub fn mark_dynamic_property_uninitialized( + &mut self, + identity: u64, + storage_property_name: &str, + ) { + self.dynamic_initialized_properties + .remove(&(identity, storage_property_name.to_string())); + } + + /// Returns whether one eval object storage slot is known to be initialized. + pub fn dynamic_property_is_initialized( + &self, + identity: u64, + storage_property_name: &str, + ) -> bool { + self.dynamic_initialized_properties + .contains(&(identity, storage_property_name.to_string())) + } + /// Copies persistent property aliases from a source object identity to a clone identity. pub fn clone_dynamic_property_aliases(&mut self, source_identity: u64, clone_identity: u64) { let aliases = self @@ -883,6 +918,16 @@ impl ElephcEvalContext { for (property, target) in aliases { self.bind_dynamic_property_alias(clone_identity, &property, target); } + let initialized = self + .dynamic_initialized_properties + .iter() + .filter_map(|(identity, property)| { + (*identity == source_identity).then(|| property.clone()) + }) + .collect::>(); + for property in initialized { + self.mark_dynamic_property_initialized(clone_identity, &property); + } } /// Records eval-declared attribute metadata for one synthetic ReflectionAttribute object. diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index f62e7ddccc..b9733477f8 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1092,6 +1092,56 @@ pub(in crate::interpreter) fn eval_reflection_property_set_value_result( values.null().map(Some) } +/// Handles eval-backed `ReflectionProperty::isInitialized()` calls. +pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isInitialized") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let object = eval_reflection_property_get_value_arg(evaluated_args)?; + let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if member.is_static { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + return values + .bool_value( + context + .static_property(declaring_class, &property_name) + .is_some(), + ) + .map(Some); + } + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_instance_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + .and_then(|initialized| values.bool_value(initialized)) + .map(Some) +} + /// Handles eval-backed `ReflectionProperty::getRawValue()` and `setRawValue()` calls. pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( identity: u64, @@ -4743,7 +4793,33 @@ fn eval_reflection_instance_property_set_value( return Err(EvalStatus::RuntimeFatal); } let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); - values.property_set(object, &storage_property_name, value) + values.property_set(object, &storage_property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Returns whether one eval instance property is initialized for ReflectionProperty. +fn eval_reflection_instance_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Ok(true); + } + let identity = values.object_identity(object)?; + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + Ok(context.dynamic_property_is_initialized(identity, &storage_property_name)) } /// Reads one eval instance property through ReflectionProperty raw-storage semantics. @@ -4789,7 +4865,10 @@ fn eval_reflection_instance_property_set_raw_value( } validate_eval_reflection_property_write(declaring_class, &property, context)?; let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); - values.property_set(object, &storage_property_name, value) + values.property_set(object, &storage_property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) } /// Resolves and validates the object/property pair targeted by ReflectionProperty. diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 3e2e5775b4..0ea29fd92f 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -661,12 +661,18 @@ fn initialize_eval_static_properties( .filter(|property| property.is_static()) { let value = if let Some(default) = property.default() { - eval_expr(default, context, scope, values)? + Some(eval_expr(default, context, scope, values)?) + } else if property.property_type().is_none() { + Some(values.null()?) } else { - values.null()? + None }; - if let Some(replaced) = context.set_static_property(class.name(), property.name(), value) { - values.release(replaced)?; + if let Some(value) = value { + if let Some(replaced) = + context.set_static_property(class.name(), property.name(), value) + { + values.release(replaced)?; + } } } Ok(()) @@ -1944,9 +1950,12 @@ pub(in crate::interpreter) fn eval_property_set_result( context, values, )?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); return values.property_set(object, &storage_property_name, value); } - values.property_set(object, &storage_property_name, value) + values.property_set(object, &storage_property_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) } /// Binds one eval object property to a by-reference source parameter. @@ -1977,7 +1986,9 @@ fn eval_property_reference_bind_result( let target = eval_property_reference_target(source_name, context, scope, values)?; let value = eval_reference_target_value(&target, context, values)?; context.bind_dynamic_property_alias(identity, &storage_property_name, target); - values.property_set(object, &storage_property_name, value) + values.property_set(object, &storage_property_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) } /// Resolves a local by-reference source into a persistent property alias target. @@ -2118,6 +2129,7 @@ pub(in crate::interpreter) fn eval_property_unset_result( let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); context.remove_dynamic_property_alias(identity, &storage_property_name); + context.mark_dynamic_property_uninitialized(identity, &storage_property_name); let null = values.null()?; return values.property_set(object, &storage_property_name, null); } @@ -3098,14 +3110,17 @@ fn eval_dynamic_class_allocate_object( .filter(|property| !property.is_static() && !property.is_abstract()) { let value = if let Some(default) = property.default() { - eval_expr(default, context, caller_scope, values)? - } else if property.is_readonly() { - continue; + Some(eval_expr(default, context, caller_scope, values)?) + } else if property.property_type().is_none() { + Some(values.null()?) } else { - values.null()? + None }; let storage_name = eval_instance_property_storage_name(class.name(), property); - values.property_set(object, &storage_name, value)?; + if let Some(value) = value { + values.property_set(object, &storage_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_name); + } } } Ok(object) @@ -3344,6 +3359,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_property_is_initialized_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_get_value_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index b3bfc83d7f..bd5f3afc68 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1931,6 +1931,55 @@ return true;"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies ReflectionProperty reports eval instance and static initialization state. +#[test] +fn execute_program_reflection_property_reports_initialized_state() { + let program = parse_fragment( + br#"class EvalReflectInitializedTarget { + public int $typed; + public ?int $nullable; + public $plain; + public static int $staticTyped; + public static $staticPlain; + public $virtual { + get => 42; + } +} +$object = new EvalReflectInitializedTarget(); +$typed = new ReflectionProperty("EvalReflectInitializedTarget", "typed"); +$nullable = new ReflectionProperty("EvalReflectInitializedTarget", "nullable"); +$plain = new ReflectionProperty("EvalReflectInitializedTarget", "plain"); +$staticTyped = new ReflectionProperty("EvalReflectInitializedTarget", "staticTyped"); +$staticPlain = new ReflectionProperty("EvalReflectInitializedTarget", "staticPlain"); +$virtual = new ReflectionProperty("EvalReflectInitializedTarget", "virtual"); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +echo $plain->isInitialized(object: $object) ? "P" : "p"; echo ":"; +echo $staticTyped->isInitialized() ? "S" : "s"; echo ":"; +echo $staticPlain->isInitialized() ? "N" : "n"; echo ":"; +EvalReflectInitializedTarget::$staticTyped = 3; +echo $staticTyped->isInitialized() ? "S" : "s"; echo ":"; +$object->typed = 5; +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +unset($object->typed); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +$typed->setRawValue(object: $object, value: 9); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +echo $nullable->isInitialized($object) ? "Y" : "y"; echo ":"; +$nullable->setValue($object, null); +echo $nullable->isInitialized($object) ? "Y" : "y"; echo ":"; +echo $virtual->isInitialized($object) ? "V" : "v"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "t:P:s:N:S:T:t:T:y:Y:V"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionProperty exposes eval property hook metadata and methods. #[test] fn execute_program_reflection_property_gets_eval_hook_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 6c7ffd24ef..cff1e8d304 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -366,9 +366,12 @@ when the variadic container itself is not rebound, and are reported through `ReflectionParameter::canBePassedByValue()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isVirtual()`, -`isDefault()`, and `getModifiers()` report eval property metadata with PHP-compatible -`ReflectionProperty::IS_*` constants for the bitmask. `isPromoted()` reports -generated/AOT and eval-declared promoted-property metadata. +`isInitialized()`, `isDefault()`, and `getModifiers()` report eval property +metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the +bitmask. `isPromoted()` reports generated/AOT and eval-declared +promoted-property metadata. `isInitialized()` tracks eval-backed instance and +static property storage, including typed properties without defaults, unset +properties, and virtual property hooks. `ReflectionProperty::hasType()` and `getType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained a supported declared type. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index afec95fefa..a6762f2990 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8698,6 +8698,54 @@ $property->getRawValue($object);'); ); } +/// Verifies eval ReflectionProperty reports instance and static initialization state. +#[test] +fn test_eval_reflection_property_is_initialized() { + let out = compile_and_run_capture( + r#" 42; + } +} +$object = new EvalReflectInitializedTarget(); +$typed = new ReflectionProperty("EvalReflectInitializedTarget", "typed"); +$nullable = new ReflectionProperty("EvalReflectInitializedTarget", "nullable"); +$plain = new ReflectionProperty("EvalReflectInitializedTarget", "plain"); +$staticTyped = new ReflectionProperty("EvalReflectInitializedTarget", "staticTyped"); +$staticPlain = new ReflectionProperty("EvalReflectInitializedTarget", "staticPlain"); +$virtual = new ReflectionProperty("EvalReflectInitializedTarget", "virtual"); +echo $typed->isInitialized($object) ? "T:" : "t:"; +echo $plain->isInitialized(object: $object) ? "P:" : "p:"; +echo $staticTyped->isInitialized() ? "S:" : "s:"; +echo $staticPlain->isInitialized() ? "N:" : "n:"; +EvalReflectInitializedTarget::$staticTyped = 3; +echo $staticTyped->isInitialized() ? "S:" : "s:"; +$object->typed = 5; +echo $typed->isInitialized($object) ? "T:" : "t:"; +unset($object->typed); +echo $typed->isInitialized($object) ? "T:" : "t:"; +$typed->setRawValue(object: $object, value: 9); +echo $typed->isInitialized($object) ? "T:" : "t:"; +echo $nullable->isInitialized($object) ? "Y:" : "y:"; +$nullable->setValue($object, null); +echo $nullable->isInitialized($object) ? "Y:" : "y:"; +echo $virtual->isInitialized($object) ? "V" : "v";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "t:P:s:N:S:T:t:T:y:Y:V"); +} + /// Verifies eval ReflectionProperty exposes property hook metadata and hook methods. #[test] fn test_eval_reflection_property_hook_metadata() { From a1dfc6e9f5ee3b570351da747a523ffbbbfb0304 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 02:19:10 +0200 Subject: [PATCH 0508/1208] Support ReflectionProperty set visibility predicates --- .../elephc-eval/src/interpreter/reflection.rs | 25 ++++++---- .../tests/builtins_class_metadata.rs | 10 +++- .../interpreter/tests/support/object_ops.rs | 18 +++++++ docs/php/classes.md | 4 +- docs/php/eval.md | 9 ++-- src/codegen_support/runtime/data/user.rs | 7 +++ src/types/checker/builtin_types/reflection.rs | 48 +++++++++++++++++++ tests/codegen/eval.rs | 32 ++++++++++++- tests/codegen/oop/attributes.rs | 12 +++-- 9 files changed, 148 insertions(+), 17 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index b9733477f8..c2de401a99 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -41,6 +41,8 @@ pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT pub(in crate::interpreter) const EVAL_REFLECTION_ATTRIBUTE_TARGET_PARAMETER: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -2144,6 +2146,20 @@ fn eval_reflection_aot_property_metadata( let is_final = flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0; let is_abstract = flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0; let is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; + let is_virtual = flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL != 0; + let mut modifiers = eval_reflection_property_modifiers( + visibility, + is_static, + is_final, + is_abstract, + is_readonly, + is_virtual, + ); + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + modifiers |= 32 | 4096; + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + modifiers |= 2048; + } EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), attributes, @@ -2153,14 +2169,7 @@ fn eval_reflection_aot_property_metadata( is_abstract, is_readonly, is_promoted: flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED != 0, - modifiers: eval_reflection_property_modifiers( - visibility, - is_static, - is_final, - is_abstract, - is_readonly, - false, - ), + modifiers, type_metadata, return_type_metadata: None, default_value, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index bd5f3afc68..aa72c2f18c 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1338,6 +1338,8 @@ echo $staticProp->isProtected() ? "P" : "p"; echo $staticProp->isFinal() ? "F" : "f"; echo $staticProp->isAbstract() ? "A" : "a"; echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->isProtectedSet() ? "T" : "t"; +echo $staticProp->isPrivateSet() ? "D" : "d"; echo $staticProp->getModifiers(); echo ":"; $visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); @@ -1347,11 +1349,15 @@ echo $visibleProp->isPublic() ? "U" : "u"; echo $visibleProp->isFinal() ? "F" : "f"; echo $visibleProp->isAbstract() ? "A" : "a"; echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->isProtectedSet() ? "T" : "t"; +echo $visibleProp->isPrivateSet() ? "D" : "d"; echo $visibleProp->getModifiers(); echo ":"; $readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); echo $readonlyProp->isReadOnly() ? "R" : "r"; echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->isProtectedSet() ? "T" : "t"; +echo $readonlyProp->isPrivateSet() ? "D" : "d"; echo $readonlyProp->getModifiers(); echo ":"; $sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); @@ -1371,6 +1377,8 @@ echo $abstractProp->getModifiers(); echo ":"; $classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->isProtectedSet() ? "T" : "t"; +echo $classReadonlyProp->isPrivateSet() ? "D" : "d"; echo $classReadonlyProp->getModifiers(); return true;"#, ) @@ -1382,7 +1390,7 @@ return true;"#, assert_eq!( values.output, - "SPurfa:APs:FUs:SRpfar20:sPufar2:RU2177:FU33:FS49:Af577:C2177" + "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index aa41a7bd05..c1f271703e 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -229,6 +229,12 @@ impl FakeOps { (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) } + (FakeValue::Object(properties), "isprotectedset") if args.is_empty() => { + self.reflection_modifier_mask(&properties, 2048) + } + (FakeValue::Object(properties), "isprivateset") if args.is_empty() => { + self.reflection_modifier_mask(&properties, 4096) + } (FakeValue::Object(properties), "getvalue") if args.is_empty() => { Self::object_property(&properties, "__value").map_or_else(|| self.null(), Ok) } @@ -826,6 +832,18 @@ impl FakeOps { self.bool_value(contains) } + /// Returns whether a fake Reflection owner stores one modifier bit. + fn reflection_modifier_mask( + &mut self, + properties: &[(String, RuntimeCellHandle)], + mask: i64, + ) -> Result { + let modifiers = Self::object_property(properties, "__modifiers") + .map(|handle| self.fake_int(&self.get(handle))) + .unwrap_or(0); + self.bool_value((modifiers & mask) != 0) + } + /// Builds a name-keyed fake ReflectionClass map from a private string-array property. fn object_relation_reflection_classes( &mut self, diff --git a/docs/php/classes.md b/docs/php/classes.md index eda36e9a11..b5a1858e75 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1232,6 +1232,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isAbstract()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is abstract | | `ReflectionProperty::isReadOnly()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property is readonly | | `ReflectionProperty::isPromoted()` | `new ReflectionProperty($class_name, $property_name)` | Return whether the reflected property came from constructor property promotion | +| `ReflectionProperty::isProtectedSet()` | `new ReflectionProperty($class_name, $property_name)` | Return whether asymmetric write visibility is `protected(set)` | +| `ReflectionProperty::isPrivateSet()` | `new ReflectionProperty($class_name, $property_name)` | Return whether asymmetric write visibility is `private(set)` | | `ReflectionProperty::getModifiers()` | `new ReflectionProperty($class_name, $property_name)` | Return PHP's `ReflectionProperty::IS_*` visibility/static/finality/abstract/readonly/virtual/set-visibility bitmask | | `ReflectionProperty::isDefault()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property is declared/default rather than dynamic | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | @@ -1319,7 +1321,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index cff1e8d304..ae82cdd731 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -366,11 +366,14 @@ when the variadic container itself is not rebound, and are reported through `ReflectionParameter::canBePassedByValue()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isVirtual()`, -`isInitialized()`, `isDefault()`, and `getModifiers()` report eval property +`isProtectedSet()`, `isPrivateSet()`, `isInitialized()`, `isDefault()`, and +`getModifiers()` report eval property metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the bitmask. `isPromoted()` reports generated/AOT and eval-declared -promoted-property metadata. `isInitialized()` tracks eval-backed instance and -static property storage, including typed properties without defaults, unset +promoted-property metadata. `isProtectedSet()` and `isPrivateSet()` derive from +the retained modifier bitmask, including generated/AOT asymmetric visibility and +public readonly property metadata. `isInitialized()` tracks eval-backed instance +and static property storage, including typed properties without defaults, unset properties, and virtual property hooks. `ReflectionProperty::hasType()` and `getType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 6ea0cb81de..0af1ddb344 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -28,6 +28,8 @@ const EVAL_REFLECTION_PROPERTY_FLAG_ABSTRACT: u64 = 32; const EVAL_REFLECTION_PROPERTY_FLAG_READONLY: u64 = 64; const EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE: u64 = 256; const EVAL_REFLECTION_PROPERTY_FLAG_PROMOTED: u64 = 512; +const EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED_SET: u64 = 2048; +const EVAL_REFLECTION_PROPERTY_FLAG_PRIVATE_SET: u64 = 4096; const EVAL_REFLECTION_METHOD_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_METHOD_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_METHOD_FLAG_PROTECTED: u64 = 4; @@ -1404,6 +1406,11 @@ fn eval_reflection_instance_property_flags( if class_info.promoted_properties.contains(property_name) { flags |= EVAL_REFLECTION_PROPERTY_FLAG_PROMOTED; } + match class_info.property_set_visibilities.get(property_name) { + Some(Visibility::Protected) => flags |= EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED_SET, + Some(Visibility::Private) => flags |= EVAL_REFLECTION_PROPERTY_FLAG_PRIVATE_SET, + Some(Visibility::Public) | None => {} + } if class_info.defaults.get(slot).is_some_and(Option::is_some) { flags |= EVAL_REFLECTION_PROPERTY_FLAG_HAS_DEFAULT_VALUE; } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 8ed62aca6c..6a303a88d3 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2458,6 +2458,43 @@ fn builtin_reflection_property_has_type_method() -> ClassMethod { } } +/// Returns a public ReflectionProperty predicate over one `__modifiers` bit. +fn builtin_reflection_property_modifier_mask_method(method_name: &str, mask: i64) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let masked_modifiers = Expr::new( + ExprKind::BinaryOp { + left: Box::new(reflection_this_property("__modifiers", dummy_span)), + op: BinOp::BitAnd, + right: Box::new(Expr::new(ExprKind::IntLiteral(mask), dummy_span)), + }, + dummy_span, + ); + let comparison = Expr::new( + ExprKind::BinaryOp { + left: Box::new(masked_modifiers), + op: BinOp::StrictNotEq, + right: Box::new(Expr::new(ExprKind::IntLiteral(0), dummy_span)), + }, + dummy_span, + ); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new(StmtKind::Return(Some(comparison)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds a `FlattenedClass` for simple reflection owner classes /// with a private `__attrs` array property and two methods: `__construct` /// (public, accepting the supplied params) and `getAttributes` (public, @@ -2762,6 +2799,14 @@ fn add_reflection_member_flag_methods( "isVirtual", "__is_virtual", )); + methods.push(builtin_reflection_property_modifier_mask_method( + "isProtectedSet", + 2048, + )); + methods.push(builtin_reflection_property_modifier_mask_method( + "isPrivateSet", + 4096, + )); methods.push(builtin_reflection_constant_bool_method("isDefault", true)); methods.push(builtin_reflection_class_mixed_method( "getDefaultValue", @@ -3131,6 +3176,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isreadonly", "isdefault", "ispromoted", + "isvirtual", + "isprotectedset", + "isprivateset", ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a6762f2990..fee492d576 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7420,6 +7420,8 @@ echo $staticProp->isProtected() ? "P" : "p"; echo $staticProp->isFinal() ? "F" : "f"; echo $staticProp->isAbstract() ? "A" : "a"; echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->isProtectedSet() ? "T" : "t"; +echo $staticProp->isPrivateSet() ? "D" : "d"; echo $staticProp->getModifiers(); echo ":"; $visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); @@ -7429,11 +7431,15 @@ echo $visibleProp->isPublic() ? "U" : "u"; echo $visibleProp->isFinal() ? "F" : "f"; echo $visibleProp->isAbstract() ? "A" : "a"; echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->isProtectedSet() ? "T" : "t"; +echo $visibleProp->isPrivateSet() ? "D" : "d"; echo $visibleProp->getModifiers(); echo ":"; $readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); echo $readonlyProp->isReadOnly() ? "R" : "r"; echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->isProtectedSet() ? "T" : "t"; +echo $readonlyProp->isPrivateSet() ? "D" : "d"; echo $readonlyProp->getModifiers(); echo ":"; $sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); @@ -7453,6 +7459,8 @@ echo $abstractProp->getModifiers(); echo ":"; $classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->isProtectedSet() ? "T" : "t"; +echo $classReadonlyProp->isPrivateSet() ? "D" : "d"; echo $classReadonlyProp->getModifiers();'); "#, ); @@ -7463,10 +7471,32 @@ echo $classReadonlyProp->getModifiers();'); ); assert_eq!( out.stdout, - "SPurfa:APs:FUs:SRpfar20:sPufar2:RU2177:FU33:FS49:Af577:C2177" + "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177" ); } +/// Verifies eval ReflectionProperty reports generated asymmetric set-visibility predicates. +#[test] +fn test_eval_reflection_property_set_visibility_predicates_for_aot_class() { + let out = compile_and_run( + r#"isPrivateSet() ? "P" : "p"; +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->getModifiers(); echo ":"; +$protected = new ReflectionProperty("EvalAotReflectSetVisibility", "protectedSet"); +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->getModifiers();'); +"#, + ); + assert_eq!(out, "Pt4129:pT2049"); +} + /// Verifies eval can observe AOT constructor-promotion metadata through /// `ReflectionProperty::isPromoted()`. #[test] diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index f82414d976..5ae82402f6 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1253,11 +1253,17 @@ class ReflectSetVisibility { public private(set) int $privateSet = 1; public protected(set) int $protectedSet = 2; } -echo (new ReflectionProperty(ReflectSetVisibility::class, "privateSet"))->getModifiers() . ":"; -echo (new ReflectionProperty(ReflectSetVisibility::class, "protectedSet"))->getModifiers(); +$private = new ReflectionProperty(ReflectSetVisibility::class, "privateSet"); +echo $private->isPrivateSet() ? "P" : "p"; +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->getModifiers() . ":"; +$protected = new ReflectionProperty(ReflectSetVisibility::class, "protectedSet"); +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->getModifiers(); "#, ); - assert_eq!(out, "4129:2049"); + assert_eq!(out, "Pt4129:pT2049"); } /// Verifies that `ReflectionClass::isReadOnly()` reports readonly class metadata. From 6b85fa168e71bfa23ef0ef0fcb9ca5d78872e55b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 02:29:51 +0200 Subject: [PATCH 0509/1208] Support ReflectionProperty getSettableType --- .../interpreter/tests/builtins_class_metadata.rs | 10 ++++++++-- .../src/interpreter/tests/support/object_ops.rs | 2 +- docs/php/classes.md | 3 ++- docs/php/eval.md | 9 ++++++--- src/types/checker/builtin_types/reflection.rs | 9 +++++++++ tests/codegen/eval.rs | 12 +++++++++--- tests/codegen/oop/attributes.rs | 15 +++++++++++++-- 7 files changed, 48 insertions(+), 12 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index aa72c2f18c..c91d920dcb 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1660,7 +1660,7 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies ReflectionProperty exposes eval property type metadata. +/// Verifies ReflectionProperty exposes eval property get/set type metadata. #[test] fn execute_program_reflection_property_get_type_metadata() { let program = parse_fragment( @@ -1697,6 +1697,12 @@ $direct = new ReflectionProperty("EvalReflectPropertyTypeTarget", "dep"); $directType = $direct->getType(); echo "direct:"; echo $direct->hasType() ? "T:" : "t:"; echo $directType->getName(); +$directSettableType = $direct->getSettableType(); +echo ":set:"; echo $directSettableType->getName(); +$plain = new ReflectionProperty("EvalReflectPropertyTypeTarget", "plain"); +echo ":plainSet:"; echo $plain->getSettableType() === null ? "N" : "n"; +$directUnion = new ReflectionProperty("EvalReflectPropertyTypeTarget", "union"); +echo ":unionSet:"; echo count($directUnion->getSettableType()->getTypes()); return true;"##, ) .expect("parse eval fragment"); @@ -1707,7 +1713,7 @@ return true;"##, assert_eq!( values.output, - "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep" + "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep:set:EvalReflectPropertyTypeDep:plainSet:N:unionSet:2" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index c1f271703e..dc252459bb 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -377,7 +377,7 @@ impl FakeOps { (FakeValue::Object(properties), "getposition") if args.is_empty() => { Self::object_property(&properties, "__position").map_or_else(|| self.int(0), Ok) } - (FakeValue::Object(properties), "gettype") if args.is_empty() => { + (FakeValue::Object(properties), "gettype" | "getsettabletype") if args.is_empty() => { Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) } (FakeValue::Object(properties), "gettypes") if args.is_empty() => { diff --git a/docs/php/classes.md b/docs/php/classes.md index b5a1858e75..45ed164189 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1238,6 +1238,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isDefault()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property is declared/default rather than dynamic | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | +| `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | @@ -1321,7 +1322,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `hasType()`, `getType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index ae82cdd731..581e6888cc 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -375,9 +375,12 @@ the retained modifier bitmask, including generated/AOT asymmetric visibility and public readonly property metadata. `isInitialized()` tracks eval-backed instance and static property storage, including typed properties without defaults, unset properties, and virtual property hooks. -`ReflectionProperty::hasType()` and `getType()` expose retained property type -metadata through `ReflectionNamedType`, `ReflectionUnionType`, and -`ReflectionIntersectionType` where eval has retained a supported declared type. +`ReflectionProperty::hasType()`, `getType()`, and `getSettableType()` expose +retained property type metadata through `ReflectionNamedType`, +`ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained +a supported declared type. For the supported property surface, +`getSettableType()` currently returns the same retained type metadata as +`getType()`. `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose materialized property default metadata, including PHP's implicit `null` default for untyped concrete properties without an explicit initializer. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 6a303a88d3..9efe25c1a6 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2787,6 +2787,10 @@ fn add_reflection_member_flag_methods( )); methods.push(builtin_reflection_property_has_type_method()); methods.push(builtin_reflection_class_mixed_method("getType", "__type")); + methods.push(builtin_reflection_class_mixed_method( + "getSettableType", + "__type", + )); methods.push(builtin_reflection_class_bool_method( "hasDefaultValue", "__has_default_value", @@ -3196,6 +3200,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { { sig.return_type = PhpType::Mixed; } + for method_name in ["getType", "getSettableType"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Mixed; + } + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fee492d576..760cbd9da5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8499,7 +8499,7 @@ echo $void->isBuiltin() ? "B" : "b";'); assert_eq!(out.stdout, "I:string:B:static:b:void:n:B"); } -/// Verifies eval ReflectionProperty materializes property type metadata through the bridge. +/// Verifies eval ReflectionProperty materializes property get/set type metadata through the bridge. #[test] fn test_eval_reflection_property_get_type_metadata() { let out = compile_and_run_capture( @@ -8537,7 +8537,13 @@ $direct = new ReflectionProperty("EvalReflectPropertyTypeTarget", "dep"); $directType = $direct->getType(); echo "direct:"; echo $direct->hasType() ? "T:" : "t:"; -echo $directType->getName();'); +echo $directType->getName(); +$directSettableType = $direct->getSettableType(); +echo ":set:" . $directSettableType->getName(); +$plain = new ReflectionProperty("EvalReflectPropertyTypeTarget", "plain"); +echo ":plainSet:" . ($plain->getSettableType() === null ? "N" : "n"); +$directUnion = new ReflectionProperty("EvalReflectPropertyTypeTarget", "union"); +echo ":unionSet:" . count($directUnion->getSettableType()->getTypes());'); "#, ); assert!( @@ -8547,7 +8553,7 @@ echo $directType->getName();'); ); assert_eq!( out.stdout, - "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep" + "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep:set:EvalReflectPropertyTypeDep:plainSet:N:unionSet:2" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 5ae82402f6..c567890746 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2436,7 +2436,7 @@ if ($directType instanceof ReflectionNamedType) { ); } -/// Verifies that `ReflectionProperty::getType()` returns named and union metadata. +/// Verifies that `ReflectionProperty::getType()` and `getSettableType()` return type metadata. #[test] fn test_reflection_property_get_type_returns_type_metadata() { let out = compile_and_run_capture( @@ -2475,6 +2475,17 @@ $directType = $direct->getType(); if ($directType instanceof ReflectionNamedType) { echo "direct:" . $directType->getName(); } +$directSettableType = $direct->getSettableType(); +if ($directSettableType instanceof ReflectionNamedType) { + echo ":set:" . $directSettableType->getName(); +} +$plain = new ReflectionProperty(ReflectPropertyTypeTarget::class, "plain"); +echo ":plainSet:" . ($plain->getSettableType() === null ? "N" : "n"); +$directUnion = new ReflectionProperty(ReflectPropertyTypeTarget::class, "union"); +$directUnionSettableType = $directUnion->getSettableType(); +if ($directUnionSettableType instanceof ReflectionUnionType) { + echo ":unionSet:" . count($directUnionSettableType->getTypes()); +} "#, ); assert!( @@ -2484,7 +2495,7 @@ if ($directType instanceof ReflectionNamedType) { ); assert_eq!( out.stdout, - "id:T:int!B|name:T:string?B|dep:T:ReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:ReflectPropertyTypeDep" + "id:T:int!B|name:T:string?B|dep:T:ReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:ReflectPropertyTypeDep:set:ReflectPropertyTypeDep:plainSet:N:unionSet:2" ); } From 6794abddc0469793a8698af4e85ce5d9cf8edb31 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 02:35:58 +0200 Subject: [PATCH 0510/1208] Support ReflectionProperty isDynamic --- .../src/interpreter/tests/builtins_class_metadata.rs | 4 +++- .../src/interpreter/tests/support/object_ops.rs | 4 ++++ docs/php/classes.md | 6 +++++- docs/php/eval.md | 12 +++++++----- src/types/checker/builtin_types/reflection.rs | 11 +++++++++++ tests/codegen/eval.rs | 6 ++++-- tests/codegen/oop/attributes.rs | 4 +++- 7 files changed, 37 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index c91d920dcb..9bb04cdd86 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1380,6 +1380,8 @@ echo $classReadonlyProp->isReadOnly() ? "C" : "c"; echo $classReadonlyProp->isProtectedSet() ? "T" : "t"; echo $classReadonlyProp->isPrivateSet() ? "D" : "d"; echo $classReadonlyProp->getModifiers(); +echo ":"; +echo $visibleProp->isDynamic() ? "D" : "d"; return true;"#, ) .expect("parse eval fragment"); @@ -1390,7 +1392,7 @@ return true;"#, assert_eq!( values.output, - "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177" + "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177:d" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index dc252459bb..36a6e428f4 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -380,6 +380,10 @@ impl FakeOps { (FakeValue::Object(properties), "gettype" | "getsettabletype") if args.is_empty() => { Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "isdynamic") if args.is_empty() => { + Self::object_property(&properties, "__is_dynamic") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "gettypes") if args.is_empty() => { Self::object_property(&properties, "__types") .map_or_else(|| self.runtime_array_new(0), Ok) diff --git a/docs/php/classes.md b/docs/php/classes.md index 45ed164189..0c45df2f0d 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1236,6 +1236,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isPrivateSet()` | `new ReflectionProperty($class_name, $property_name)` | Return whether asymmetric write visibility is `private(set)` | | `ReflectionProperty::getModifiers()` | `new ReflectionProperty($class_name, $property_name)` | Return PHP's `ReflectionProperty::IS_*` visibility/static/finality/abstract/readonly/virtual/set-visibility bitmask | | `ReflectionProperty::isDefault()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property is declared/default rather than dynamic | +| `ReflectionProperty::isDynamic()` | Same as `ReflectionProperty::isDefault()` | Return `false` for supported declared properties; dynamic object properties are not yet materialized as `ReflectionProperty` objects | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | @@ -1322,13 +1323,16 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as `ReflectionProperty` instances, so supported reflected properties report `true`. +`ReflectionProperty::isDynamic()` reports `false` for supported declared +properties for the same reason. + ### Class constants ```php diff --git a/docs/php/eval.md b/docs/php/eval.md index 581e6888cc..7382d06bf4 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -366,15 +366,17 @@ when the variadic container itself is not rebound, and are reported through `ReflectionParameter::canBePassedByValue()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isVirtual()`, -`isProtectedSet()`, `isPrivateSet()`, `isInitialized()`, `isDefault()`, and -`getModifiers()` report eval property +`isDynamic()`, `isProtectedSet()`, `isPrivateSet()`, `isInitialized()`, +`isDefault()`, and `getModifiers()` report eval property metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the bitmask. `isPromoted()` reports generated/AOT and eval-declared promoted-property metadata. `isProtectedSet()` and `isPrivateSet()` derive from the retained modifier bitmask, including generated/AOT asymmetric visibility and -public readonly property metadata. `isInitialized()` tracks eval-backed instance -and static property storage, including typed properties without defaults, unset -properties, and virtual property hooks. +public readonly property metadata. `isDynamic()` reports `false` for supported +declared properties because eval does not yet materialize dynamic object +properties as `ReflectionProperty` objects. `isInitialized()` tracks eval-backed +instance and static property storage, including typed properties without +defaults, unset properties, and virtual property hooks. `ReflectionProperty::hasType()`, `getType()`, and `getSettableType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 9efe25c1a6..a5a7eb047e 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2769,6 +2769,12 @@ fn add_reflection_member_flag_methods( Some(bool_type()), false_bool(), )); + properties.push(builtin_property( + "__is_dynamic", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); properties.push(builtin_property( "__default_value", Visibility::Private, @@ -2803,6 +2809,10 @@ fn add_reflection_member_flag_methods( "isVirtual", "__is_virtual", )); + methods.push(builtin_reflection_class_bool_method( + "isDynamic", + "__is_dynamic", + )); methods.push(builtin_reflection_property_modifier_mask_method( "isProtectedSet", 2048, @@ -3181,6 +3191,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isdefault", "ispromoted", "isvirtual", + "isdynamic", "isprotectedset", "isprivateset", ] { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 760cbd9da5..ac2655edfa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7461,7 +7461,9 @@ $classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classRe echo $classReadonlyProp->isReadOnly() ? "C" : "c"; echo $classReadonlyProp->isProtectedSet() ? "T" : "t"; echo $classReadonlyProp->isPrivateSet() ? "D" : "d"; -echo $classReadonlyProp->getModifiers();'); +echo $classReadonlyProp->getModifiers(); +echo ":"; +echo $visibleProp->isDynamic() ? "D" : "d";'); "#, ); assert!( @@ -7471,7 +7473,7 @@ echo $classReadonlyProp->getModifiers();'); ); assert_eq!( out.stdout, - "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177" + "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177:d" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index c567890746..6025b787cc 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1261,9 +1261,11 @@ $protected = new ReflectionProperty(ReflectSetVisibility::class, "protectedSet") echo $protected->isPrivateSet() ? "P" : "p"; echo $protected->isProtectedSet() ? "T" : "t"; echo $protected->getModifiers(); +echo ":"; +echo $protected->isDynamic() ? "D" : "d"; "#, ); - assert_eq!(out, "Pt4129:pT2049"); + assert_eq!(out, "Pt4129:pT2049:d"); } /// Verifies that `ReflectionClass::isReadOnly()` reports readonly class metadata. From e601800f887431a38dd6970a95cb60e3a3823117 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 02:52:50 +0200 Subject: [PATCH 0511/1208] Support non-lazy ReflectionProperty APIs --- .../elephc-eval/src/interpreter/reflection.rs | 63 +++++++++++++- .../elephc-eval/src/interpreter/statements.rs | 9 ++ .../tests/builtins_class_metadata.rs | 7 +- docs/php/classes.md | 8 +- docs/php/eval.md | 6 +- src/types/checker/builtin_types/reflection.rs | 86 +++++++++++++++++++ tests/codegen/eval.rs | 7 +- tests/codegen/oop/attributes.rs | 7 +- 8 files changed, 186 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index c2de401a99..8771e4860f 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1144,7 +1144,46 @@ pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( .map(Some) } -/// Handles eval-backed `ReflectionProperty::getRawValue()` and `setRawValue()` calls. +/// Handles eval-backed `ReflectionProperty::isLazy()` and `skipLazyInitialization()` calls. +pub(in crate::interpreter) fn eval_reflection_property_lazy_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + if method_name.eq_ignore_ascii_case("isLazy") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + eval_reflection_property_validate_object(&declaring_class, object, context, values)?; + return values.bool_value(false).map(Some); + } + if method_name.eq_ignore_ascii_case("skipLazyInitialization") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + let (_, property) = eval_reflection_instance_property_target( + &declaring_class, + &property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + return values.null().map(Some); + } + Ok(None) +} + +/// Handles eval-backed `ReflectionProperty::getRawValue()` and raw write calls. pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( identity: u64, method_name: &str, @@ -1172,7 +1211,9 @@ pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( ) .map(Some); } - if method_name.eq_ignore_ascii_case("setRawValue") { + if method_name.eq_ignore_ascii_case("setRawValue") + || method_name.eq_ignore_ascii_case("setRawValueWithoutLazyInitialization") + { let (object, value) = eval_reflection_property_set_raw_value_args(evaluated_args)?; eval_reflection_instance_property_set_raw_value( &declaring_class, @@ -4880,6 +4921,24 @@ fn eval_reflection_instance_property_set_raw_value( Ok(()) } +/// Validates the object argument shared by non-mutating ReflectionProperty instance APIs. +fn eval_reflection_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let identity = values.object_identity(object)?; + let object_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + .ok_or(EvalStatus::RuntimeFatal)?; + if !context.class_is_a(&object_class_name, declaring_class, false) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + /// Resolves and validates the object/property pair targeted by ReflectionProperty. fn eval_reflection_instance_property_target( declaring_class: &str, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 0ea29fd92f..eb613f4505 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3368,6 +3368,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_property_lazy_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_get_value_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 9bb04cdd86..2d422cda60 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1903,6 +1903,11 @@ echo $backed->getValue($hook); echo ":"; $backed->setRawValue(object: $hook, value: 7); echo $backed->getRawValue($hook); echo ":"; echo $backed->getValue($hook); echo ":"; +echo $backed->isLazy($hook) ? "L" : "l"; echo ":"; +$backed->skipLazyInitialization(object: $hook); +$backed->setRawValueWithoutLazyInitialization(object: $hook, value: 8); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; echo $backed->getModifiers(); echo ":"; echo $backed->isVirtual() ? "V" : "b"; echo ":"; echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->isVirtual() ? "V" : "b"; echo ":"; @@ -1917,7 +1922,7 @@ return true;"#, assert_eq!( values.output, - "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:1:b:V:513" + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:l:8:16:1:b:V:513" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/classes.md b/docs/php/classes.md index 0c45df2f0d..88c3d1abc4 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1237,6 +1237,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getModifiers()` | `new ReflectionProperty($class_name, $property_name)` | Return PHP's `ReflectionProperty::IS_*` visibility/static/finality/abstract/readonly/virtual/set-visibility bitmask | | `ReflectionProperty::isDefault()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property is declared/default rather than dynamic | | `ReflectionProperty::isDynamic()` | Same as `ReflectionProperty::isDefault()` | Return `false` for supported declared properties; dynamic object properties are not yet materialized as `ReflectionProperty` objects | +| `ReflectionProperty::isLazy()` | Same as `ReflectionProperty::isDefault()` plus an object argument | Return `false` for supported declared properties because elephc does not implement lazy properties | +| `ReflectionProperty::skipLazyInitialization()` | Same as `ReflectionProperty::isLazy()` | Accepted as a no-op for supported non-static, non-virtual declared properties | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | @@ -1323,7 +1325,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as @@ -1333,6 +1335,10 @@ properties. elephc does not currently materialize dynamic object properties as `ReflectionProperty::isDynamic()` reports `false` for supported declared properties for the same reason. +`ReflectionProperty::isLazy()` reports `false`, and +`skipLazyInitialization()` is accepted as a no-op for supported non-lazy +declared properties. + ### Class constants ```php diff --git a/docs/php/eval.md b/docs/php/eval.md index 7382d06bf4..80219540f0 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -401,7 +401,11 @@ still reject readonly writes. `ReflectionProperty::getRawValue()` and `setRawValue()` are supported for eval-declared backed instance properties, including backed property hooks, and bypass concrete property hook accessors. Virtual property hooks reject raw -access like PHP. +access like PHP. `ReflectionProperty::isLazy()` reports `false` for +eval-declared properties because eval does not implement lazy properties; +`skipLazyInitialization()` is a no-op for supported non-static backed +properties, and `setRawValueWithoutLazyInitialization()` follows the same raw +storage write path as `setRawValue()`. `ReflectionClassConstant::getAttributes()`, `ReflectionEnumUnitCase::getAttributes()`, and `ReflectionEnumBackedCase::getAttributes()` expose eval-retained class-constant diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index a5a7eb047e..bb2585462f 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2495,6 +2495,90 @@ fn builtin_reflection_property_modifier_mask_method(method_name: &str, mask: i64 } } +/// Returns `ReflectionProperty::isLazy()` for the non-lazy property model elephc supports. +fn builtin_reflection_property_is_lazy_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "isLazy".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(object_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::skipLazyInitialization()` as a no-op for non-lazy properties. +fn builtin_reflection_property_skip_lazy_initialization_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let static_modifier = binary_expr( + binary_expr( + reflection_this_property("__modifiers", dummy_span), + BinOp::BitAnd, + Expr::new(ExprKind::IntLiteral(16), dummy_span), + dummy_span, + ), + BinOp::StrictNotEq, + Expr::new(ExprKind::IntLiteral(0), dummy_span), + dummy_span, + ); + ClassMethod { + name: "skipLazyInitialization".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(object_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Void), + body: vec![ + Stmt::new( + StmtKind::If { + condition: static_modifier, + then_body: vec![throw_new_reflection_exception( + string_lit( + "Can not use skipLazyInitialization on static property", + dummy_span, + ), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_virtual", dummy_span), + then_body: vec![throw_new_reflection_exception( + string_lit( + "Can not use skipLazyInitialization on virtual property", + dummy_span, + ), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds a `FlattenedClass` for simple reflection owner classes /// with a private `__attrs` array property and two methods: `__construct` /// (public, accepting the supplied params) and `getAttributes` (public, @@ -2813,6 +2897,8 @@ fn add_reflection_member_flag_methods( "isDynamic", "__is_dynamic", )); + methods.push(builtin_reflection_property_is_lazy_method()); + methods.push(builtin_reflection_property_skip_lazy_initialization_method()); methods.push(builtin_reflection_property_modifier_mask_method( "isProtectedSet", 2048, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ac2655edfa..96f1bf3a57 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8697,6 +8697,11 @@ echo $backed->getValue($hook) . ":"; $backed->setRawValue(object: $hook, value: 7); echo $backed->getRawValue($hook) . ":"; echo $backed->getValue($hook) . ":"; +echo $backed->isLazy($hook) ? "L:" : "l:"; +$backed->skipLazyInitialization(object: $hook); +$backed->setRawValueWithoutLazyInitialization(object: $hook, value: 8); +echo $backed->getRawValue($hook) . ":"; +echo $backed->getValue($hook) . ":"; echo $backed->getModifiers() . ":"; echo $backed->isVirtual() ? "V:" : "b:"; echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->isVirtual() ? "V:" : "b:"; @@ -8710,7 +8715,7 @@ echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers() ); assert_eq!( out.stdout, - "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:1:b:V:513" + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:l:8:16:1:b:V:513" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 6025b787cc..db19cf0761 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1263,9 +1263,14 @@ echo $protected->isProtectedSet() ? "T" : "t"; echo $protected->getModifiers(); echo ":"; echo $protected->isDynamic() ? "D" : "d"; +echo ":"; +$object = new ReflectSetVisibility(); +echo $protected->isLazy($object) ? "L" : "l"; +$protected->skipLazyInitialization($object); +echo ":ok"; "#, ); - assert_eq!(out, "Pt4129:pT2049:d"); + assert_eq!(out, "Pt4129:pT2049:d:l:ok"); } /// Verifies that `ReflectionClass::isReadOnly()` reports readonly class metadata. From 0047c7328ce0c36d556077999e0e6f15cd61a03b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 03:02:58 +0200 Subject: [PATCH 0512/1208] Support ReflectionProperty string formatting in eval --- .../elephc-eval/src/interpreter/reflection.rs | 146 ++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 9 ++ .../tests/builtins_class_metadata.rs | 31 ++++ docs/php/eval.md | 3 + tests/codegen/eval.rs | 30 ++++ 5 files changed, 219 insertions(+) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 8771e4860f..ccd1f27bdc 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1183,6 +1183,44 @@ pub(in crate::interpreter) fn eval_reflection_property_lazy_result( Ok(None) } +/// Handles eval-backed `ReflectionProperty::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_property_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let member = if let Some(member) = + eval_reflection_property_metadata(&declaring_class, &property_name, context) + { + member + } else { + eval_reflection_aot_property_metadata_if_exists( + &declaring_class, + &property_name, + context, + values, + )? + .ok_or(EvalStatus::RuntimeFatal)? + }; + let text = eval_reflection_property_to_string(&property_name, &member); + values.string(&text).map(Some) +} + /// Handles eval-backed `ReflectionProperty::getRawValue()` and raw write calls. pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( identity: u64, @@ -3705,6 +3743,114 @@ fn eval_reflection_property_modifiers( modifiers } +/// Formats one reflected property similarly to PHP's `ReflectionProperty::__toString()`. +fn eval_reflection_property_to_string( + property_name: &str, + member: &EvalReflectionMemberMetadata, +) -> String { + let mut parts = Vec::new(); + if member.is_abstract { + parts.push(String::from("abstract")); + } + if member.is_final { + parts.push(String::from("final")); + } + parts.push(eval_reflection_visibility_label(member.visibility).to_string()); + if member.is_static { + parts.push(String::from("static")); + } + if member.is_readonly { + parts.push(String::from("readonly")); + } + if let Some(type_name) = member + .type_metadata + .as_ref() + .map(eval_reflection_type_metadata_to_string) + { + parts.push(type_name); + } + parts.push(format!("${property_name}")); + + let default = if member.modifiers & 512 != 0 { + String::new() + } else { + member + .default_value + .as_ref() + .and_then(eval_reflection_default_expr_to_string) + .map(|value| format!(" = {value}")) + .unwrap_or_default() + }; + format!("Property [ {}{} ]", parts.join(" "), default) +} + +/// Returns PHP's lowercase label for one reflected visibility. +fn eval_reflection_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} + +/// Formats retained ReflectionType metadata for `ReflectionProperty::__toString()`. +fn eval_reflection_type_metadata_to_string( + type_metadata: &EvalReflectionParameterTypeMetadata, +) -> String { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named) => { + if named.allows_null && named.name != "mixed" { + format!("?{}", named.name) + } else { + named.name.clone() + } + } + EvalReflectionParameterTypeKind::Union(union) => { + let mut names = union + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>(); + if union.allows_null && names.iter().all(|name| name != "null") { + names.push(String::from("null")); + } + names.join("|") + } + EvalReflectionParameterTypeKind::Intersection(intersection) => intersection + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>() + .join("&"), + } +} + +/// Formats retained literal defaults for `ReflectionProperty::__toString()`. +fn eval_reflection_default_expr_to_string(default: &EvalExpr) -> Option { + match default { + EvalExpr::Const(EvalConst::Null) => Some(String::from("NULL")), + EvalExpr::Const(EvalConst::Bool(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::Int(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::Float(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::String(value)) => Some(format!("'{value}'")), + EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr, + } => eval_reflection_default_expr_to_string(expr), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => eval_reflection_default_expr_to_string(expr).map(|value| format!("-{value}")), + EvalExpr::ConstFetch(name) => Some(name.clone()), + EvalExpr::NamespacedConstFetch { name, .. } => Some(name.clone()), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => Some(format!("{class_name}::{constant}")), + _ => None, + } +} + /// Returns whether eval retained this property as virtual rather than backed. fn eval_reflection_property_is_virtual(property: &EvalClassProperty) -> bool { property.is_virtual() diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index eb613f4505..93249fc5de 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3377,6 +3377,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_property_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_get_value_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 2d422cda60..866c7c3b4f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1762,6 +1762,37 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty formats retained property metadata through `__toString()`. +#[test] +fn execute_program_reflection_property_to_string() { + let program = parse_fragment( + br#"class EvalReflectPropertyStringTarget { + public int $id = 7; + protected static string $label = "ok"; + private $implicit; + public $virtual { + get => 1; + } +} +foreach (["id", "label", "implicit", "virtual"] as $name) { + echo (new ReflectionProperty("EvalReflectPropertyStringTarget", $name))->__toString(); + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public $virtual ]|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionParameter reports eval constant-default metadata. #[test] fn execute_program_reflection_parameter_reports_default_constant_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 80219540f0..28a5f8f903 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -386,6 +386,9 @@ a supported declared type. For the supported property surface, `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose materialized property default metadata, including PHP's implicit `null` default for untyped concrete properties without an explicit initializer. +`ReflectionProperty::__toString()` formats retained eval/generated property +metadata as a PHP-style `Property [ ... ]` descriptor for the supported +visibility, static, type, default, and virtual-property surface. `ReflectionProperty::hasHooks()`, `hasHook()`, `getHooks()`, and `getHook()` expose eval-declared concrete, abstract, and interface property get/set hook metadata and return hook `ReflectionMethod` objects using PHP's diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 96f1bf3a57..80df63c934 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8599,6 +8599,36 @@ echo $listed->getDefaultValue() === null ? "null" : "bad";'); ); } +/// Verifies eval ReflectionProperty formats retained property metadata through `__toString()`. +#[test] +fn test_eval_reflection_property_to_string() { + let out = compile_and_run_capture( + r#" 1; + } +} +foreach (["id", "label", "implicit", "virtual"] as $name) { + echo (new ReflectionProperty("EvalReflectPropertyStringTarget", $name))->__toString(); + echo "|"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public $virtual ]|" + ); +} + /// Verifies eval ReflectionClass materializes property default metadata through the bridge. #[test] fn test_eval_reflection_class_get_default_properties_metadata() { From 22130a2abd3e46b280aca38b08cf3b987a470a2b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 03:42:35 +0200 Subject: [PATCH 0513/1208] Support ReflectionParameter legacy type predicates --- .../elephc-eval/src/interpreter/reflection.rs | 109 ++++++++++++++++++ .../elephc-eval/src/interpreter/statements.rs | 8 ++ .../tests/builtins_class_metadata.rs | 6 +- .../interpreter/tests/support/object_ops.rs | 16 +++ docs/php/classes.md | 20 +++- docs/php/eval.md | 4 +- src/codegen/eval_reflection_owner_helpers.rs | 54 ++++++++- src/codegen/lower_inst/objects/reflection.rs | 44 +++++-- src/types/checker/builtin_types/reflection.rs | 16 +++ tests/codegen/eval.rs | 6 +- 10 files changed, 264 insertions(+), 19 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index ccd1f27bdc..21625fcb28 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -51,6 +51,8 @@ const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; const EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT: u64 = 128; +const EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE: u64 = 256; +const EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE: u64 = 512; const EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL: u64 = 1; const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; @@ -98,6 +100,8 @@ struct EvalReflectionParameterMetadata { is_promoted: bool, has_type: bool, allows_null: bool, + is_array_type: bool, + is_callable_type: bool, type_metadata: Option, default_value: Option, default_value_constant_name: Option, @@ -866,6 +870,81 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( } } +/// Handles eval-backed `ReflectionParameter::isArray()` and `isCallable()` calls. +pub(in crate::interpreter) fn eval_reflection_parameter_legacy_type_predicate_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(expected_type) = eval_reflection_parameter_legacy_type_name(method_name) else { + return Ok(None); + }; + if !eval_reflection_object_has_class(object, "ReflectionParameter", values)? { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + if let Some(flag_property) = eval_reflection_parameter_legacy_type_flag_property(method_name) { + let flag = values.property_get(object, flag_property)?; + if values.type_tag(flag)? == EVAL_TAG_BOOL { + return Ok(Some(flag)); + } + } + let type_value = values.method_call(object, "getType", Vec::new())?; + if values.is_null(type_value)? { + return values.bool_value(false).map(Some); + } + if !eval_reflection_object_has_class(type_value, "ReflectionNamedType", values)? { + return values.bool_value(false).map(Some); + } + let name = values.method_call(type_value, "getName", Vec::new())?; + let bytes = values.string_bytes(name)?; + let name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values + .bool_value(name.eq_ignore_ascii_case(expected_type)) + .map(Some) +} + +/// Maps a legacy ReflectionParameter predicate method to its target named type. +fn eval_reflection_parameter_legacy_type_name(method_name: &str) -> Option<&'static str> { + if method_name.eq_ignore_ascii_case("isArray") { + Some("array") + } else if method_name.eq_ignore_ascii_case("isCallable") { + Some("callable") + } else { + None + } +} + +/// Maps a legacy ReflectionParameter predicate method to its precomputed flag slot. +fn eval_reflection_parameter_legacy_type_flag_property(method_name: &str) -> Option<&'static str> { + if method_name.eq_ignore_ascii_case("isArray") { + Some("__is_array_type") + } else if method_name.eq_ignore_ascii_case("isCallable") { + Some("__is_callable_type") + } else { + None + } +} + +/// Returns whether one runtime object cell has the requested PHP class name. +fn eval_reflection_object_has_class( + object: RuntimeCellHandle, + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Ok(false); + } + let actual = values.object_class_name(object)?; + let bytes = values.string_bytes(actual); + values.release(actual)?; + let actual = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(actual + .trim_start_matches('\\') + .eq_ignore_ascii_case(class_name)) +} + /// Handles eval-backed `ReflectionMethod::hasPrototype()` and `getPrototype()` calls. pub(in crate::interpreter) fn eval_reflection_method_prototype_result( identity: u64, @@ -4643,6 +4722,9 @@ fn eval_reflection_property_hook_parameters( .property_type() .and_then(eval_reflection_parameter_type_metadata); let has_type = type_metadata.is_some(); + let is_array_type = eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: hook.reflected_method_name(property.name()), declaring_class_name: Some(declaring_class.to_string()), @@ -4664,6 +4746,8 @@ fn eval_reflection_property_hook_parameters( allows_null: type_metadata .as_ref() .is_some_and(eval_reflection_type_allows_null), + is_array_type, + is_callable_type, type_metadata, default_value: None, default_value_constant_name: None, @@ -5198,6 +5282,10 @@ fn eval_reflection_parameters_from_names_and_type_flags( .and_then(Option::as_ref) .and_then(eval_reflection_parameter_type_metadata) .filter(|_| has_type); + let is_array_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); EvalReflectionParameterMetadata { name: name.clone(), declaring_class_name: declaring_class_name.map(str::to_string), @@ -5220,6 +5308,8 @@ fn eval_reflection_parameters_from_names_and_type_flags( type_metadata.as_ref(), default_value.as_ref(), ), + is_array_type, + is_callable_type, type_metadata, default_value, default_value_constant_name, @@ -5228,6 +5318,19 @@ fn eval_reflection_parameters_from_names_and_type_flags( .collect() } +/// Returns whether retained parameter metadata is one named type with the requested name. +fn eval_reflection_parameter_has_named_type( + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + expected_name: &str, +) -> bool { + matches!( + type_metadata, + Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(named) + }) if named.name.eq_ignore_ascii_case(expected_name) + ) +} + /// Returns PHP's ReflectionParameter default-constant name for retained eval defaults. fn eval_reflection_default_constant_name(default: &EvalExpr) -> Option { match default { @@ -5696,6 +5799,12 @@ fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) if parameter.default_value_constant_name.is_some() { flags |= EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT; } + if parameter.is_array_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE; + } + if parameter.is_callable_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE; + } flags } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 93249fc5de..3e5265c4a1 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3179,6 +3179,14 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( return values.bool_value(attribute_metadata.is_repeated()); } } + if let Some(result) = eval_reflection_parameter_legacy_type_predicate_result( + object, + method_name, + evaluated_args.clone(), + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_implements_interface_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 866c7c3b4f..f53d963741 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1537,7 +1537,7 @@ fn execute_program_reflects_eval_method_parameters() { br##"interface EvalReflectLeft {} interface EvalReflectRight {} class EvalReflectParamTarget { - public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, &...$rest) {} + public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, ?array $items = null, ?callable $callback = null, \App\Name|null $second = null, &...$rest) {} } $method = new ReflectionMethod("EvalReflectParamTarget", "run"); echo $method->getNumberOfParameters(); echo "/"; @@ -1551,6 +1551,8 @@ foreach ($params as $param) { echo $param->canBePassedByValue() ? "Y" : "N"; echo $param->hasType() ? "T" : "t"; echo $param->allowsNull() ? "N" : "n"; + echo $param->isArray() ? "A" : "a"; + echo $param->isCallable() ? "C" : "c"; $type = $param->getType(); if ($param->getName() == "union") { echo ":union"; @@ -1596,7 +1598,7 @@ return true;"##, assert_eq!( values.output, - "5/3:first#0rvRNTn:int!B:A1:EvalParamTag:first:d|union#1rvbYTn:union!:intB:stringB:A0:d|both#2rvbYTn:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second#3OvbYTN:App\\Name?C:A0:D=null|rest#4OVRNtN:null:A0:d|" + "7/3:first#0rvRNTnac:int!B:A1:EvalParamTag:first:d|union#1rvbYTnac:union!:intB:stringB:A0:d|both#2rvbYTnac:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|items#3OvbYTNAc:array?B:A0:D=null|callback#4OvbYTNaC:callable?B:A0:D=null|second#5OvbYTNac:App\\Name?C:A0:D=null|rest#6OVRNtNac:null:A0:d|" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 36a6e428f4..f956a70a23 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -28,6 +28,8 @@ const EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE: u64 = 16; const EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED: u64 = 32; const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; const EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT: u64 = 128; +const EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE: u64 = 256; +const EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE: u64 = 512; impl FakeOps { /// Reads one fake object property by name. @@ -459,6 +461,14 @@ impl FakeOps { Self::object_property(&properties, "__allows_null") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isarray") if args.is_empty() => { + Self::object_property(&properties, "__is_array_type") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "iscallable") if args.is_empty() => { + Self::object_property(&properties, "__is_callable_type") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "isbuiltin") if args.is_empty() => { Self::object_property(&properties, "__is_builtin") .map_or_else(|| self.bool_value(false), Ok) @@ -765,6 +775,10 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL) != 0)?; let is_default_value_constant = self .bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT) != 0)?; + let is_array_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE) != 0)?; + let is_callable_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE) != 0)?; properties.push(("__position".to_string(), position)); properties.push(("__is_optional".to_string(), is_optional)); properties.push(("__is_variadic".to_string(), is_variadic)); @@ -775,6 +789,8 @@ impl FakeOps { properties.push(("__is_promoted".to_string(), is_promoted)); properties.push(("__has_type".to_string(), has_type)); properties.push(("__allows_null".to_string(), allows_null)); + properties.push(("__is_array_type".to_string(), is_array_type)); + properties.push(("__is_callable_type".to_string(), is_callable_type)); properties.push(("__type".to_string(), method_objects)); properties.push(("__has_default_value".to_string(), has_default_value)); properties.push(( diff --git a/docs/php/classes.md b/docs/php/classes.md index 88c3d1abc4..9efe12f98a 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1210,14 +1210,19 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::isOptional()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | | `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | | `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | +| `ReflectionParameter::canBePassedByValue()` | Same construction forms as `ReflectionParameter::isPassedByReference()` | Return whether the parameter is not by-reference | | `ReflectionParameter::isPromoted()` | Same construction forms as `ReflectionParameter::isPassedByReference()` | Return whether the parameter came from constructor property promotion | | `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, a `ReflectionIntersectionType` for intersection parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | +| `ReflectionParameter::allowsNull()` | Same as `ReflectionParameter::hasType()` | Return PHP nullability for retained parameter type/default metadata | +| `ReflectionParameter::isArray()` / `isCallable()` | Same as `ReflectionParameter::hasType()` | Return PHP's legacy named-type predicates for nullable or non-nullable `array` / `callable` parameter declarations; union declarations report `false` | | `ReflectionParameter::getAttributes()` | Same construction forms as `ReflectionParameter::hasType()` | Return `ReflectionAttribute` objects for method parameter attributes | | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | | `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null default values, or throw `ReflectionException` when no default is available | +| `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | +| `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | | `ReflectionUnionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return non-null union members as `ReflectionNamedType` objects and report whether `null` was part of the union | | `ReflectionIntersectionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return intersection members as `ReflectionNamedType` objects and report `false` for nullability | @@ -1306,11 +1311,18 @@ name with `isBuiltin()` false. | `ReflectionParameter::getPosition()` | `int` | Zero-based parameter index | | `ReflectionParameter::isOptional()` | `bool` | True for a parameter with a default or variadic, and any after it | | `ReflectionParameter::isVariadic()` | `bool` | True for the `...$rest` parameter | +| `ReflectionParameter::isPassedByReference()` | `bool` | True for by-reference parameters | +| `ReflectionParameter::canBePassedByValue()` | `bool` | False for by-reference parameters | +| `ReflectionParameter::isPromoted()` | `bool` | True for constructor-promoted parameters | | `ReflectionParameter::hasType()` | `bool` | True when the parameter declares a type | | `ReflectionParameter::getType()` | `?ReflectionNamedType\|ReflectionUnionType\|ReflectionIntersectionType` | The declared type, or `null` when untyped | +| `ReflectionParameter::allowsNull()` | `bool` | PHP nullability for the retained parameter metadata | +| `ReflectionParameter::isArray()` / `isCallable()` | `bool` | Legacy named-type predicates for direct `array` / `callable` hints | | `ReflectionParameter::getAttributes()` | `ReflectionAttribute[]` | The reflected method parameter's attributes | | `ReflectionParameter::isDefaultValueAvailable()` | `bool` | True when a supported default value is available | | `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar or null default value, or throws when unavailable | +| `ReflectionParameter::isDefaultValueConstant()` | `bool` | True when the retained default came from a named constant | +| `ReflectionParameter::getDefaultValueConstantName()` | `?string` | The constant name for constant-backed defaults, or `null` | | `ReflectionNamedType::getName()` | `string` | The type name (`int`, `string`, a class name, …) | | `ReflectionNamedType::isBuiltin()` | `bool` | True for builtin types, false for class types | | `ReflectionNamedType::allowsNull()` | `bool` | True when the declared type is nullable | @@ -1325,7 +1337,13 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, and `getDefaultValue()` for supported scalar/null defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. + +`ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, +and `isCallable()` are supported for retained parameter metadata. +`isArray()` / `isCallable()` follow PHP's legacy behavior: nullable or +non-nullable direct `array` / `callable` declarations report `true`, while +union declarations report `false`. `ReflectionProperty::isDefault()` is also supported for materialized declared properties. elephc does not currently materialize dynamic object properties as diff --git a/docs/php/eval.md b/docs/php/eval.md index 28a5f8f903..210bedb07d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -325,7 +325,9 @@ values when native method/static-method signatures are registered. Eval-declared functions and methods expose declared-type presence for parameters and return types, simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, -`allowsNull()`, and `isBuiltin()`, and multi-member union metadata through +`allowsNull()`, and `isBuiltin()`, and the legacy +`ReflectionParameter::isArray()` / `isCallable()` predicates for named `array` +and `callable` parameter types. Multi-member union metadata is exposed through `ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter metadata is exposed through `ReflectionIntersectionType::getTypes()` and `allowsNull()`. Function, method, and parameter attributes are exposed through diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 7ee7db0636..adaa767afb 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -120,6 +120,10 @@ struct ReflectionOwnerLayout { declaring_function_hi: Option, allows_null_lo: Option, allows_null_hi: Option, + is_array_type_lo: Option, + is_array_type_hi: Option, + is_callable_type_lo: Option, + is_callable_type_hi: Option, is_builtin_lo: Option, is_builtin_hi: Option, } @@ -274,6 +278,8 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option, default_value: Option, default_value_constant_name: Option, @@ -2673,6 +2675,9 @@ fn reflection_parameter_members_with_declaring_function( .flatten(); let default_value_constant_name = default_expr.and_then(reflection_parameter_default_constant_name); + let is_array_type = reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = + reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); parameters.push(ReflectionParameterMember { name: name.clone(), declaring_class_name: declaring_class_name.map(str::to_string), @@ -2705,6 +2710,8 @@ fn reflection_parameter_members_with_declaring_function( type_metadata.as_ref(), default_value.as_ref(), ), + is_array_type, + is_callable_type, type_metadata, default_value, default_value_constant_name, @@ -2713,6 +2720,18 @@ fn reflection_parameter_members_with_declaring_function( Ok(parameters) } +/// Returns whether retained parameter metadata is one named type with the requested name. +fn reflection_parameter_has_named_type( + type_metadata: Option<&ReflectionParameterTypeMetadata>, + expected_name: &str, +) -> bool { + matches!( + type_metadata, + Some(ReflectionParameterTypeMetadata::Named(named)) + if named.name.eq_ignore_ascii_case(expected_name) + ) +} + /// Returns PHP's `ReflectionParameter::allowsNull()` value for static metadata. fn reflection_parameter_allows_null( has_type: bool, @@ -3972,10 +3991,7 @@ fn emit_reflection_constant_array( /// Allocates and populates a name-keyed map of full ReflectionClass objects. fn emit_reflection_class_array(ctx: &mut FunctionContext<'_>, names: &[String]) -> Result<()> { - emit_empty_assoc_array_literal_to_result( - ctx, - &PhpType::Object("ReflectionClass".to_string()), - ); + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Object("ReflectionClass".to_string())); for name in names { abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); let metadata = reflection_class_metadata_for_name(ctx, name)?; @@ -3990,8 +4006,8 @@ fn emit_reflection_class_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x3, x0"); // pass the ReflectionClass object as the hash payload - ctx.emitter.instruction("mov x4, xzr"); // object hash payloads do not use the high word + ctx.emitter.instruction("mov x3, x0"); // pass the ReflectionClass object as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // object hash payloads do not use the high word abi::emit_pop_reg(ctx.emitter, "x0"); abi::emit_symbol_address(ctx.emitter, "x1", &key_label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); @@ -4003,8 +4019,8 @@ fn emit_reflection_class_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { abi::emit_call_label(ctx.emitter, "__rt_hash_set"); } Arch::X86_64 => { - ctx.emitter.instruction("mov rcx, rax"); // pass the ReflectionClass object as the hash payload - ctx.emitter.instruction("xor r8, r8"); // object hash payloads do not use the high word + ctx.emitter.instruction("mov rcx, rax"); // pass the ReflectionClass object as the hash payload + ctx.emitter.instruction("xor r8, r8"); // object hash payloads do not use the high word abi::emit_pop_reg(ctx.emitter, "rdi"); abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); @@ -4312,6 +4328,18 @@ fn emit_reflection_parameter_properties( "__allows_null", parameter.allows_null, )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_array_type", + parameter.is_array_type, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionParameter", + "__is_callable_type", + parameter.is_callable_type, + )?; emit_reflection_parameter_type_property(ctx, parameter)?; emit_reflection_owner_bool_property( ctx, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index bb2585462f..6e30c1d212 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -628,6 +628,18 @@ fn builtin_reflection_parameter() -> FlattenedClass { Some(TypeExpr::Bool), bool_lit(true), ), + builtin_property( + "__is_array_type", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), + builtin_property( + "__is_callable_type", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), builtin_property( "__has_default_value", @@ -684,6 +696,8 @@ fn builtin_reflection_parameter() -> FlattenedClass { builtin_reflection_slot_getter("isPromoted", "__is_promoted", TypeExpr::Bool), builtin_reflection_slot_getter("hasType", "__has_type", TypeExpr::Bool), builtin_reflection_slot_getter("allowsNull", "__allows_null", TypeExpr::Bool), + builtin_reflection_slot_getter("isArray", "__is_array_type", TypeExpr::Bool), + builtin_reflection_slot_getter("isCallable", "__is_callable_type", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), builtin_reflection_owner_get_attributes_method(), builtin_reflection_slot_getter( @@ -3350,6 +3364,8 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ispromoted", "hastype", "allowsnull", + "isarray", + "iscallable", "isdefaultvalueavailable", "isdefaultvalueconstant", ] { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 80df63c934..a161ad54df 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8362,7 +8362,7 @@ fn test_eval_reflection_method_lists_parameters() { eval('interface EvalReflectLeft {} interface EvalReflectRight {} class EvalReflectParamTarget { - public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, \App\Name|null $second = null, &...$rest) {} + public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, ?array $items = null, ?callable $callback = null, \App\Name|null $second = null, &...$rest) {} } $method = new ReflectionMethod("EvalReflectParamTarget", "run"); echo $method->getNumberOfParameters() . "/"; @@ -8376,6 +8376,8 @@ foreach ($params as $param) { echo $param->canBePassedByValue() ? "Y" : "N"; echo $param->hasType() ? "T" : "t"; echo $param->allowsNull() ? "N" : "n"; + echo $param->isArray() ? "A" : "a"; + echo $param->isCallable() ? "C" : "c"; $type = $param->getType(); if ($param->getName() == "union") { echo ":union"; @@ -8420,7 +8422,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "5/3:first@0rvRNTn:int!B:A1:EvalParamTag:first:d|union@1rvbYTn:union!:intB:stringB:A0:d|both@2rvbYTn:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|second@3OvbYTN:App\\Name?C:A0:D=null|rest@4OVRNtN:null:A0:d|" + "7/3:first@0rvRNTnac:int!B:A1:EvalParamTag:first:d|union@1rvbYTnac:union!:intB:stringB:A0:d|both@2rvbYTnac:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|items@3OvbYTNAc:array?B:A0:D=null|callback@4OvbYTNaC:callable?B:A0:D=null|second@5OvbYTNac:App\\Name?C:A0:D=null|rest@6OVRNtNac:null:A0:d|" ); } From 54d6ac8ce91601576f375dcf8aafccb4485945ee Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 04:08:05 +0200 Subject: [PATCH 0514/1208] Support dynamic ReflectionProperty in eval --- crates/elephc-eval/src/context.rs | 20 ++ .../elephc-eval/src/interpreter/reflection.rs | 272 +++++++++++++++++- .../tests/builtins_class_metadata.rs | 46 +++ .../interpreter/tests/support/object_ops.rs | 11 +- docs/php/classes.md | 15 +- docs/php/eval.md | 10 +- src/codegen/eval_reflection_owner_helpers.rs | 22 ++ src/types/checker/builtin_types/reflection.rs | 16 +- tests/codegen/eval.rs | 41 +++ 9 files changed, 432 insertions(+), 21 deletions(-) diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-eval/src/context.rs index 3dd957a38f..519af2a5d9 100644 --- a/crates/elephc-eval/src/context.rs +++ b/crates/elephc-eval/src/context.rs @@ -321,6 +321,7 @@ pub struct ElephcEvalContext { eval_reflection_functions: HashMap, eval_reflection_methods: HashMap, eval_reflection_properties: HashMap, + eval_dynamic_reflection_properties: HashSet, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, class_stack: Vec, @@ -375,6 +376,7 @@ impl ElephcEvalContext { eval_reflection_functions: HashMap::new(), eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), + eval_dynamic_reflection_properties: HashSet::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -430,6 +432,7 @@ impl ElephcEvalContext { eval_reflection_functions: HashMap::new(), eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), + eval_dynamic_reflection_properties: HashSet::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -1015,6 +1018,18 @@ impl ElephcEvalContext { property_name.to_string(), ), ); + self.eval_dynamic_reflection_properties.remove(&identity); + } + + /// Records reflected dynamic-property metadata for one synthetic ReflectionProperty object. + pub fn register_eval_dynamic_reflection_property( + &mut self, + identity: u64, + declaring_class: &str, + property_name: &str, + ) { + self.register_eval_reflection_property(identity, declaring_class, property_name); + self.eval_dynamic_reflection_properties.insert(identity); } /// Returns the declaring class and property name attached to a synthetic ReflectionProperty. @@ -1024,6 +1039,11 @@ impl ElephcEvalContext { .map(|(class, property)| (class.as_str(), property.as_str())) } + /// Returns whether a synthetic ReflectionProperty represents a dynamic property. + pub fn eval_reflection_property_is_dynamic(&self, identity: u64) -> bool { + self.eval_dynamic_reflection_properties.contains(&identity) + } + /// Returns eval-declared class metadata from parent to child for construction. pub fn class_chain(&self, name: &str) -> Vec { let mut chain = Vec::new(); diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 21625fcb28..94973d8c03 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -43,6 +43,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; +const EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC: u64 = 8192; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -79,6 +80,7 @@ struct EvalReflectionMemberMetadata { is_abstract: bool, is_readonly: bool, is_promoted: bool, + is_dynamic: bool, modifiers: u64, type_metadata: Option, return_type_metadata: Option, @@ -1099,6 +1101,17 @@ pub(in crate::interpreter) fn eval_reflection_property_get_value_result( return Ok(None); }; let object = eval_reflection_property_get_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + return eval_reflection_dynamic_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + .map(Some); + } let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) else { return Err(EvalStatus::RuntimeFatal); @@ -1144,11 +1157,23 @@ pub(in crate::interpreter) fn eval_reflection_property_set_value_result( else { return Ok(None); }; + let (object_or_value, value) = eval_reflection_property_set_value_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let value = value.ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_dynamic_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + return values.null().map(Some); + } let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) else { return Err(EvalStatus::RuntimeFatal); }; - let (object_or_value, value) = eval_reflection_property_set_value_args(evaluated_args)?; if member.is_static { let value = value.unwrap_or(object_or_value); let declaring_class = member @@ -1194,6 +1219,18 @@ pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( return Ok(None); }; let object = eval_reflection_property_get_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + return eval_reflection_dynamic_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + .and_then(|initialized| values.bool_value(initialized)) + .map(Some); + } let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) else { return Err(EvalStatus::RuntimeFatal); @@ -1242,11 +1279,29 @@ pub(in crate::interpreter) fn eval_reflection_property_lazy_result( }; if method_name.eq_ignore_ascii_case("isLazy") { let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + return values.bool_value(false).map(Some); + } eval_reflection_property_validate_object(&declaring_class, object, context, values)?; return values.bool_value(false).map(Some); } if method_name.eq_ignore_ascii_case("skipLazyInitialization") { let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + return Err(EvalStatus::RuntimeFatal); + } let (_, property) = eval_reflection_instance_property_target( &declaring_class, &property_name, @@ -1283,6 +1338,11 @@ pub(in crate::interpreter) fn eval_reflection_property_to_string_result( return Ok(None); }; eval_reflection_bind_no_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let member = eval_reflection_dynamic_property_metadata(&declaring_class); + let text = eval_reflection_property_to_string(&property_name, &member); + return values.string(&text).map(Some); + } let member = if let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) { @@ -1319,6 +1379,16 @@ pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( }; if method_name.eq_ignore_ascii_case("getRawValue") { let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + return eval_reflection_dynamic_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + .map(Some); + } return eval_reflection_instance_property_get_raw_value( &declaring_class, &property_name, @@ -1332,6 +1402,17 @@ pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( || method_name.eq_ignore_ascii_case("setRawValueWithoutLazyInitialization") { let (object, value) = eval_reflection_property_set_raw_value_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_set_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + return values.null().map(Some); + } eval_reflection_instance_property_set_raw_value( &declaring_class, &property_name, @@ -1940,9 +2021,12 @@ fn eval_reflection_property_new( &[String::from("class_name"), String::from("property_name")], evaluated_args, )?; + let property_name = eval_reflection_string_arg(args[1], values)?; + if values.type_tag(args[0])? == EVAL_TAG_OBJECT { + return eval_reflection_property_new_for_object(args[0], &property_name, context, values); + } let class_name = eval_reflection_string_arg(args[0], values)?; if !eval_reflection_class_like_exists(&class_name, context) { - let property_name = eval_reflection_string_arg(args[1], values)?; if let Some(property) = eval_reflection_aot_property_metadata_if_exists( &class_name, &property_name, @@ -1960,7 +2044,6 @@ fn eval_reflection_property_new( } return Ok(None); } - let property_name = eval_reflection_string_arg(args[1], values)?; let property = eval_reflection_property_metadata(&class_name, &property_name, context) .ok_or(EvalStatus::RuntimeFatal)?; eval_reflection_member_object_result( @@ -1973,6 +2056,104 @@ fn eval_reflection_property_new( .map(Some) } +/// Builds a ReflectionProperty from an object argument, including dynamic properties. +fn eval_reflection_property_new_for_object( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = eval_reflection_object_class_name(object, context, values)?; + if let Some(property) = eval_reflection_property_metadata(&class_name, property_name, context) { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); + } + if !eval_reflection_object_dynamic_property_exists(object, property_name, values)? { + return Ok(None); + } + let property = eval_reflection_dynamic_property_metadata(&class_name); + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some) +} + +/// Returns the class name for an object passed to a Reflection constructor. +fn eval_reflection_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().trim_start_matches('\\').to_string()); + } + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Returns whether one object has a public dynamic property by exact PHP name. +fn eval_reflection_object_dynamic_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + if property_name.contains('\0') { + return Ok(false); + } + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} + +/// Builds PHP reflection metadata for a public dynamic object property. +fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflectionMemberMetadata { + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + attributes: Vec::new(), + visibility: EvalVisibility::Public, + is_static: false, + is_final: false, + is_abstract: false, + is_readonly: false, + is_promoted: false, + is_dynamic: true, + modifiers: eval_reflection_property_modifiers( + EvalVisibility::Public, + false, + false, + false, + false, + false, + ), + type_metadata: None, + return_type_metadata: None, + default_value: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} + /// Returns generated AOT ReflectionMethod metadata when the runtime table has a matching row. fn eval_reflection_aot_method_metadata_if_exists( class_name: &str, @@ -2074,6 +2255,7 @@ fn eval_reflection_aot_method_metadata( is_abstract: flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0, is_readonly: false, is_promoted: false, + is_dynamic: false, modifiers: eval_reflection_method_modifiers_from_flags(flags), type_metadata: None, return_type_metadata, @@ -2327,6 +2509,7 @@ fn eval_reflection_aot_property_metadata( is_abstract, is_readonly, is_promoted: flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED != 0, + is_dynamic: false, modifiers, type_metadata, return_type_metadata: None, @@ -2639,7 +2822,14 @@ fn eval_reflection_owner_object_with_members( context.register_eval_reflection_function(identity, reflected_name); } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { if let Some(declaring_class) = parent_class_name { - if eval_reflection_class_like_exists(declaring_class, context) { + if flags & EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC != 0 { + let identity = values.object_identity(object)?; + context.register_eval_dynamic_reflection_property( + identity, + declaring_class, + reflected_name, + ); + } else if eval_reflection_class_like_exists(declaring_class, context) { let identity = values.object_identity(object)?; context.register_eval_reflection_property( identity, @@ -3282,6 +3472,9 @@ fn eval_reflection_member_object_result( if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 512) != 0 { flags |= EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL; } + if member.is_dynamic { + flags |= EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC; + } let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { member.required_parameter_count as u64 } else { @@ -3827,6 +4020,9 @@ fn eval_reflection_property_to_string( property_name: &str, member: &EvalReflectionMemberMetadata, ) -> String { + if member.is_dynamic { + return format!("Property [ public ${property_name} ]\n"); + } let mut parts = Vec::new(); if member.is_abstract { parts.push(String::from("abstract")); @@ -4140,6 +4336,7 @@ fn eval_reflection_method_metadata( is_abstract: method.is_abstract(), is_readonly: false, is_promoted: false, + is_dynamic: false, modifiers: eval_reflection_method_modifiers( method.visibility(), method.is_static(), @@ -4202,6 +4399,7 @@ fn eval_reflection_method_metadata( is_abstract: true, is_readonly: false, is_promoted: false, + is_dynamic: false, modifiers: eval_reflection_method_modifiers( EvalVisibility::Public, method.is_static(), @@ -4266,6 +4464,7 @@ fn eval_reflection_method_metadata( is_abstract: method.is_abstract(), is_readonly: false, is_promoted: false, + is_dynamic: false, modifiers: eval_reflection_method_modifiers( method.visibility(), method.is_static(), @@ -4301,6 +4500,7 @@ fn eval_reflection_property_metadata( is_abstract: property.is_abstract(), is_readonly: property.is_readonly(), is_promoted: property.is_promoted(), + is_dynamic: false, modifiers: eval_reflection_property_modifiers( property.visibility(), property.is_static(), @@ -4334,6 +4534,7 @@ fn eval_reflection_property_metadata( is_abstract: true, is_readonly: false, is_promoted: false, + is_dynamic: false, modifiers: eval_reflection_property_modifiers( EvalVisibility::Public, false, @@ -4367,6 +4568,7 @@ fn eval_reflection_property_metadata( is_abstract: property.is_abstract(), is_readonly: property.is_readonly(), is_promoted: property.is_promoted(), + is_dynamic: false, modifiers: eval_reflection_property_modifiers( property.visibility(), property.is_static(), @@ -4695,6 +4897,7 @@ fn eval_reflection_property_hook_method_metadata( is_abstract: property.is_abstract(), is_readonly: false, is_promoted: false, + is_dynamic: false, modifiers: eval_reflection_method_modifiers( property.visibility(), false, @@ -5151,6 +5354,67 @@ fn eval_reflection_instance_property_set_raw_value( Ok(()) } +/// Reads a public dynamic property through ReflectionProperty semantics. +fn eval_reflection_dynamic_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + values.property_get(object, property_name) +} + +/// Writes a public dynamic property through ReflectionProperty semantics. +fn eval_reflection_dynamic_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + values.property_set(object, property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, property_name); + Ok(()) +} + +/// Returns whether a public dynamic property currently exists on the target object. +fn eval_reflection_dynamic_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + eval_reflection_object_dynamic_property_exists(object, property_name, values) +} + +/// Validates the object argument used by dynamic ReflectionProperty operations. +fn eval_reflection_dynamic_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let object_class_name = eval_reflection_object_class_name(object, context, values)?; + if eval_reflection_class_like_exists(declaring_class, context) { + if context.class_is_a(&object_class_name, declaring_class, false) { + return Ok(()); + } + } else if object_class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case(declaring_class.trim_start_matches('\\')) + { + return Ok(()); + } + Err(EvalStatus::RuntimeFatal) +} + /// Validates the object argument shared by non-mutating ReflectionProperty instance APIs. fn eval_reflection_property_validate_object( declaring_class: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index f53d963741..e5aa014659 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -2034,6 +2034,52 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty materializes and operates on public dynamic properties. +#[test] +fn execute_program_reflection_property_supports_dynamic_properties() { + let program = parse_fragment( + br#"class EvalReflectDynamicBase {} +class EvalReflectDynamicChild extends EvalReflectDynamicBase {} +$object = new EvalReflectDynamicBase(); +$object->dynamic = "first"; +$child = new EvalReflectDynamicChild(); +$child->dynamic = "child"; +$empty = new EvalReflectDynamicChild(); +$property = new ReflectionProperty($object, "dynamic"); +echo $property->getName(); echo ":"; +echo $property->isDynamic() ? "D" : "d"; echo ":"; +echo $property->isDefault() ? "Y" : "N"; echo ":"; +echo $property->getModifiers(); echo ":"; +echo is_null($property->getType()) ? "T" : "t"; echo ":"; +echo is_null($property->getSettableType()) ? "S" : "s"; echo ":"; +echo $property->hasDefaultValue() ? "H" : "h"; echo ":"; +echo is_null($property->getDefaultValue()) ? "V" : "v"; echo ":"; +echo $property->isLazy($object) ? "L" : "l"; echo ":"; +echo $property->isInitialized($object) ? "I" : "i"; echo ":"; +echo $property->getValue($object); echo ":"; +echo $property->getValue($child); echo ":"; +echo $property->isInitialized($empty) ? "E" : "e"; echo ":"; +echo is_null($property->getValue($empty)) ? "null" : "bad"; echo ":"; +$property->setValue($empty, "filled"); +echo $property->getValue($empty); echo ":"; +$property->setRawValue($object, "raw"); +echo $property->getRawValue($object); echo ":"; +echo str_replace("\n", "\\n", $property->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "dynamic:D:N:1:T:S:h:V:l:I:first:child:e:null:filled:raw:Property [ public $dynamic ]\\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionProperty exposes eval property hook metadata and methods. #[test] fn execute_program_reflection_property_gets_eval_hook_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index f956a70a23..fb0838bc8d 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -20,6 +20,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; +const EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC: u64 = 8192; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -455,7 +456,12 @@ impl FakeOps { .map_or_else(|| self.bool_value(false), Ok) } (FakeValue::Object(properties), "isdefault") if args.is_empty() => { - self.bool_value(Self::object_property(&properties, "__has_default_value").is_some()) + match Self::object_property(&properties, "__is_dynamic") { + Some(is_dynamic) => { + self.bool_value(!matches!(self.get(is_dynamic), FakeValue::Bool(true))) + } + None => self.bool_value(true), + } } (FakeValue::Object(properties), "allowsnull") if args.is_empty() => { Self::object_property(&properties, "__allows_null") @@ -694,6 +700,8 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED) != 0)?; let is_virtual = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL) != 0)?; + let is_dynamic = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC) != 0)?; properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__is_readonly".to_string(), is_readonly)); @@ -702,6 +710,7 @@ impl FakeOps { properties.push(("__has_default_value".to_string(), has_default_value)); properties.push(("__is_promoted".to_string(), is_promoted)); properties.push(("__is_virtual".to_string(), is_virtual)); + properties.push(("__is_dynamic".to_string(), is_dynamic)); properties.push(("__default_value".to_string(), property_objects)); } } diff --git a/docs/php/classes.md b/docs/php/classes.md index 9efe12f98a..70e0533202 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1240,9 +1240,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isProtectedSet()` | `new ReflectionProperty($class_name, $property_name)` | Return whether asymmetric write visibility is `protected(set)` | | `ReflectionProperty::isPrivateSet()` | `new ReflectionProperty($class_name, $property_name)` | Return whether asymmetric write visibility is `private(set)` | | `ReflectionProperty::getModifiers()` | `new ReflectionProperty($class_name, $property_name)` | Return PHP's `ReflectionProperty::IS_*` visibility/static/finality/abstract/readonly/virtual/set-visibility bitmask | -| `ReflectionProperty::isDefault()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property is declared/default rather than dynamic | -| `ReflectionProperty::isDynamic()` | Same as `ReflectionProperty::isDefault()` | Return `false` for supported declared properties; dynamic object properties are not yet materialized as `ReflectionProperty` objects | -| `ReflectionProperty::isLazy()` | Same as `ReflectionProperty::isDefault()` plus an object argument | Return `false` for supported declared properties because elephc does not implement lazy properties | +| `ReflectionProperty::isDefault()` | `new ReflectionProperty($class_name, $property_name)`, `new ReflectionProperty($object, $property_name)`, or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property is declared/default rather than dynamic | +| `ReflectionProperty::isDynamic()` | Same as `ReflectionProperty::isDefault()` | Return `false` for supported declared properties and `true` for public dynamic object properties materialized from an object argument | +| `ReflectionProperty::isLazy()` | Same as `ReflectionProperty::isDefault()` plus an object argument | Return `false` for supported declared and dynamic properties because elephc does not implement lazy properties | | `ReflectionProperty::skipLazyInitialization()` | Same as `ReflectionProperty::isLazy()` | Accepted as a no-op for supported non-static, non-virtual declared properties | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | @@ -1346,12 +1346,13 @@ non-nullable direct `array` / `callable` declarations report `true`, while union declarations report `false`. `ReflectionProperty::isDefault()` is also supported for materialized declared -properties. elephc does not currently materialize dynamic object properties as -`ReflectionProperty` instances, so supported reflected properties report -`true`. +and dynamic properties. Declared properties report `true`; supported dynamic +properties materialized with `new ReflectionProperty($object, $property_name)` +report `false`. `ReflectionProperty::isDynamic()` reports `false` for supported declared -properties for the same reason. +properties and `true` for public dynamic object properties materialized with +`new ReflectionProperty($object, $property_name)`. `ReflectionProperty::isLazy()` reports `false`, and `skipLazyInitialization()` is accepted as a no-op for supported non-lazy diff --git a/docs/php/eval.md b/docs/php/eval.md index 210bedb07d..df8e29cff6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -375,10 +375,12 @@ bitmask. `isPromoted()` reports generated/AOT and eval-declared promoted-property metadata. `isProtectedSet()` and `isPrivateSet()` derive from the retained modifier bitmask, including generated/AOT asymmetric visibility and public readonly property metadata. `isDynamic()` reports `false` for supported -declared properties because eval does not yet materialize dynamic object -properties as `ReflectionProperty` objects. `isInitialized()` tracks eval-backed -instance and static property storage, including typed properties without -defaults, unset properties, and virtual property hooks. +declared properties and `true` for public dynamic object properties +materialized with `new ReflectionProperty($object, $property_name)`. +`ReflectionProperty::isDefault()` is the inverse for those supported dynamic +properties. `isInitialized()` tracks eval-backed instance and static property +storage, including typed properties without defaults, unset properties, virtual +property hooks, and public dynamic properties on the inspected object. `ReflectionProperty::hasType()`, `getType()`, and `getSettableType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index adaa767afb..896d1e3a49 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -112,6 +112,8 @@ struct ReflectionOwnerLayout { is_promoted_hi: Option, is_virtual_lo: Option, is_virtual_hi: Option, + is_dynamic_lo: Option, + is_dynamic_hi: Option, default_value_lo: Option, default_value_hi: Option, default_value_constant_name_lo: Option, @@ -273,6 +275,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option Option Cl } } -/// Returns a public Reflection boolean method that always reports one literal value. -fn builtin_reflection_constant_bool_method(method_name: &str, value: bool) -> ClassMethod { +/// Returns `ReflectionProperty::isDefault()` backed by the dynamic-property slot. +fn builtin_reflection_property_is_default_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); ClassMethod { - name: method_name.to_string(), + name: "isDefault".to_string(), visibility: Visibility::Public, is_static: false, is_abstract: false, @@ -2354,7 +2354,13 @@ fn builtin_reflection_constant_bool_method(method_name: &str, value: bool) -> Cl variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new( - StmtKind::Return(if value { true_bool() } else { false_bool() }), + StmtKind::Return(Some(Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__is_dynamic", + dummy_span, + ))), + dummy_span, + ))), dummy_span, )], span: dummy_span, @@ -2921,7 +2927,7 @@ fn add_reflection_member_flag_methods( "isPrivateSet", 4096, )); - methods.push(builtin_reflection_constant_bool_method("isDefault", true)); + methods.push(builtin_reflection_property_is_default_method()); methods.push(builtin_reflection_class_mixed_method( "getDefaultValue", "__default_value", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a161ad54df..614c528beb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8601,6 +8601,47 @@ echo $listed->getDefaultValue() === null ? "null" : "bad";'); ); } +/// Verifies eval ReflectionProperty materializes dynamic object properties through the bridge. +#[test] +fn test_eval_reflection_property_supports_dynamic_properties() { + let out = compile_and_run_capture( + r#"dynamic = "first"; +$child = new EvalReflectDynamicBridgeChild(); +$child->dynamic = "child"; +$empty = new EvalReflectDynamicBridgeChild(); +$property = new ReflectionProperty($object, "dynamic"); +echo $property->getName(); echo ":"; +echo $property->isDynamic() ? "D" : "d"; echo ":"; +echo $property->isDefault() ? "Y" : "N"; echo ":"; +echo $property->getModifiers(); echo ":"; +echo $property->hasDefaultValue() ? "H" : "h"; echo ":"; +echo is_null($property->getType()) ? "T" : "t"; echo ":"; +echo $property->isInitialized($object) ? "I" : "i"; echo ":"; +echo $property->getValue($object); echo ":"; +echo $property->getValue($child); echo ":"; +echo $property->isInitialized($empty) ? "E" : "e"; echo ":"; +$property->setValue($empty, "filled"); +echo $property->getValue($empty); echo ":"; +$property->setRawValue($object, "raw"); +echo $property->getRawValue($object); echo ":"; +echo str_replace("\n", "\\n", $property->__toString());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dynamic:D:N:1:h:T:I:first:child:e:filled:raw:Property [ public $dynamic ]\n" + ); +} + /// Verifies eval ReflectionProperty formats retained property metadata through `__toString()`. #[test] fn test_eval_reflection_property_to_string() { From 62029c588b8c4afe1b694c363dbf5c72c709d78f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 04:15:28 +0200 Subject: [PATCH 0515/1208] Support ReflectionClass object targets in eval --- crates/elephc-eval/src/interpreter/reflection.rs | 14 +++++++++++++- .../interpreter/tests/builtins_class_metadata.rs | 6 +++++- docs/php/classes.md | 2 +- docs/php/eval.md | 3 +++ tests/codegen/eval.rs | 13 ++++++++++--- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 94973d8c03..eb27769f02 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1747,7 +1747,7 @@ fn eval_reflection_class_new( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; - let class_name = eval_reflection_string_arg(args[0], values)?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; let reflected_name = context .resolve_class_like_name(&class_name) .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); @@ -1813,6 +1813,18 @@ fn eval_reflection_class_new( .map(Some) } +/// Resolves a ReflectionClass constructor target from a class-name string or object. +fn eval_reflection_class_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(target)? == EVAL_TAG_OBJECT { + return eval_reflection_object_class_name(target, context, values); + } + eval_reflection_string_arg(target, values) +} + /// Returns generated/AOT class flags for synthetic ReflectionClass fallback objects. fn eval_reflection_aot_class_flags( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index e5aa014659..ec278a047c 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -711,6 +711,10 @@ $iface = new ReflectionClass("EvalInstanceIface"); $trait = new ReflectionClass("EvalInstanceTrait"); $enum = new ReflectionClass("EvalInstanceEnum"); $childObj = new EvalInstanceChild(); +$objectRef = new ReflectionClass($childObj); +echo $objectRef->getName(); echo ":"; +echo $objectRef->getParentClass()->getName(); echo ":"; +echo $objectRef->isInstance($childObj) ? "O" : "o"; echo ":"; echo $base->isInstance($childObj) ? "B" : "b"; echo $child->isInstance(new EvalInstanceBase()) ? "C" : "c"; echo $iface->isInstance($childObj) ? "I" : "i"; @@ -725,7 +729,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "BcItEN"); + assert_eq!(values.output, "EvalInstanceChild:EvalInstanceBase:O:BcItEN"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/classes.md b/docs/php/classes.md index 70e0533202..d6abce4e4e 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1337,7 +1337,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index df8e29cff6..b15a07dfa0 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -193,6 +193,9 @@ for those owners. `ReflectionClass`, `ReflectionFunction`, `ReflectionMethod`, eval does not retain docblock text. `ReflectionClass`, `ReflectionFunction`, and `ReflectionMethod` expose `getExtensionName()` and `getExtension()` and report `false` / `null` for eval-declared user symbols. +`ReflectionClass` construction accepts class-name strings and object arguments; +object arguments reflect the runtime class of eval-created or generated/AOT +objects. `ReflectionClass::getShortName()`, `ReflectionClass::getNamespaceName()`, and `ReflectionClass::inNamespace()` derive namespace-aware parts from the resolved eval class-like name. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 614c528beb..7cb5b2c4af 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6832,6 +6832,10 @@ $iface = new ReflectionClass("EvalInstanceIface"); $trait = new ReflectionClass("EvalInstanceTrait"); $enum = new ReflectionClass("EvalInstanceEnum"); $childObj = new EvalInstanceChild(); +$objectRef = new ReflectionClass($childObj); +echo $objectRef->getName(); echo ":"; +echo $objectRef->getParentClass()->getName(); echo ":"; +echo $objectRef->isInstance($childObj) ? "O" : "o"; echo ":"; echo $base->isInstance($childObj) ? "B" : "b"; echo $child->isInstance(new EvalInstanceBase()) ? "C" : "c"; echo $iface->isInstance($childObj) ? "I" : "i"; @@ -6845,7 +6849,7 @@ echo $iface->isInstance(EvalInstanceEnum::Ready) ? "N" : "n";'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "BcItEN"); + assert_eq!(out.stdout, "EvalInstanceChild:EvalInstanceBase:O:BcItEN"); } /// Verifies eval ReflectionClass::isInstance can query generated AOT object @@ -6863,7 +6867,10 @@ echo $parent->isInstance(new EvalAotInstanceChild()) ? "P" : "p"; $child = new ReflectionClass("EvalAotInstanceChild"); echo $child->isInstance(new EvalAotInstanceParent()) ? "C" : "c"; $iface = new ReflectionClass("EvalAotInstanceIface"); -echo $iface->isInstance(new EvalAotInstanceImpl()) ? "I" : "i";'); +$objectRef = new ReflectionClass(new EvalAotInstanceChild()); +echo $iface->isInstance(new EvalAotInstanceImpl()) ? "I" : "i"; echo ":"; +echo $objectRef->getName(); echo ":"; +echo $objectRef->getParentClass()->getName();'); "#, ); assert!( @@ -6871,7 +6878,7 @@ echo $iface->isInstance(new EvalAotInstanceImpl()) ? "I" : "i";'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "PcI"); + assert_eq!(out.stdout, "PcI:EvalAotInstanceChild:EvalAotInstanceParent"); } /// Verifies eval ReflectionClass::getParentClass crosses the generated runtime bridge. From 98f16b8489bfbb33a26d69e51eb71017fd8afb94 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 04:21:30 +0200 Subject: [PATCH 0516/1208] Support ReflectionMethod object targets in eval --- .../elephc-eval/src/interpreter/reflection.rs | 2 +- .../tests/builtins_class_metadata.rs | 34 ++++++++++++++ docs/php/classes.md | 2 +- docs/php/eval.md | 2 + tests/codegen/eval.rs | 46 +++++++++++++++++++ 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index eb27769f02..15cf30537f 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -1982,7 +1982,7 @@ fn eval_reflection_method_new( &[String::from("class_name"), String::from("method_name")], evaluated_args, )?; - let class_name = eval_reflection_string_arg(args[0], values)?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; if !eval_reflection_class_like_exists(&class_name, context) { let method_name = eval_reflection_string_arg(args[1], values)?; if let Some(method) = eval_reflection_aot_method_metadata_with_signature_if_exists( diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index ec278a047c..41ff180220 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1493,6 +1493,40 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod accepts object targets and reflects the runtime class. +#[test] +fn execute_program_reflection_method_accepts_object_targets() { + let program = parse_fragment( + br#"class EvalReflectMethodObjectBase { + public function MiXeDCase() { return "base"; } +} +class EvalReflectMethodObjectChild extends EvalReflectMethodObjectBase { + public function childCase() { return "child"; } +} +$object = new EvalReflectMethodObjectChild(); +$inherited = new ReflectionMethod($object, "mixedcase"); +echo $inherited->getName(); echo ":"; +echo $inherited->getDeclaringClass()->getName(); echo ":"; +echo $inherited->invoke($object); echo ":"; +$own = new ReflectionMethod($object, "CHILDCASE"); +echo $own->getName(); echo ":"; +echo $own->getDeclaringClass()->getName(); echo ":"; +echo $own->invoke($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "MiXeDCase:EvalReflectMethodObjectBase:base:childCase:EvalReflectMethodObjectChild:child" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval member and enum-case reflectors expose their declaring class. #[test] fn execute_program_reflects_eval_declaring_class_metadata() { diff --git a/docs/php/classes.md b/docs/php/classes.md index d6abce4e4e..16453e2b91 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1337,7 +1337,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index b15a07dfa0..297e29d64f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -196,6 +196,8 @@ report `false` / `null` for eval-declared user symbols. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. +`ReflectionMethod` construction accepts class-name strings and object +arguments; object arguments resolve to the runtime class before method lookup. `ReflectionClass::getShortName()`, `ReflectionClass::getNamespaceName()`, and `ReflectionClass::inNamespace()` derive namespace-aware parts from the resolved eval class-like name. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7cb5b2c4af..eae1d5eca3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8179,6 +8179,52 @@ echo $listed->invoke($object);'); assert_eq!(out.stdout, "MiXeDCase:MiXeDCase:base:childCase:child"); } +/// Verifies eval ReflectionMethod accepts object targets through the bridge. +#[test] +fn test_eval_reflection_method_accepts_object_targets() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $inherited->getDeclaringClass()->getName() . ":"; +echo $inherited->invoke($object) . ":"; +$own = new ReflectionMethod($object, "CHILDCASE"); +echo $own->getName() . ":"; +echo $own->getDeclaringClass()->getName() . ":"; +echo $own->invoke($object) . "|"; +$aot = new EvalAotReflectMethodObjectChild(); +$aotInherited = new ReflectionMethod($aot, "aotbase"); +echo $aotInherited->getName() . ":"; +echo $aotInherited->getDeclaringClass()->getName() . ":"; +$aotOwn = new ReflectionMethod($aot, "aotchild"); +echo $aotOwn->getName() . ":"; +echo $aotOwn->getDeclaringClass()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "MiXeDCase:EvalReflectMethodObjectBase:base:childCase:EvalReflectMethodObjectChild:child|aotbase:EvalAotReflectMethodObjectBase:aotchild:EvalAotReflectMethodObjectChild" + ); +} + /// Verifies eval-declared final properties cannot be redeclared by subclasses. #[test] fn test_eval_declared_final_property_override_fails() { From 245cc5642860bf75916555a0d6ba009848aedf32 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 04:40:40 +0200 Subject: [PATCH 0517/1208] Support eval asymmetric property visibility --- crates/elephc-eval/src/eval_ir.rs | 21 +++ .../elephc-eval/src/interpreter/reflection.rs | 13 +- .../elephc-eval/src/interpreter/statements.rs | 50 ++++++-- .../tests/builtins_class_metadata.rs | 33 +++++ .../src/interpreter/tests/classes.rs | 121 ++++++++++++++++++ crates/elephc-eval/src/parser/statements.rs | 115 ++++++++++++++--- .../src/parser/tests/classes_errors.rs | 57 +++++++++ docs/php/eval.md | 9 +- tests/codegen/eval.rs | 35 +++++ 9 files changed, 421 insertions(+), 33 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 2ed5928ea0..3fec24609e 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1397,6 +1397,7 @@ pub struct EvalClassProperty { attributes: Vec, property_type: Option, visibility: EvalVisibility, + set_visibility: Option, is_static: bool, is_final: bool, is_readonly: bool, @@ -1467,6 +1468,7 @@ impl EvalClassProperty { attributes: Vec::new(), property_type: None, visibility, + set_visibility: None, is_static, is_final, is_readonly, @@ -1520,6 +1522,12 @@ impl EvalClassProperty { self } + /// Returns a copy of this property with PHP asymmetric write visibility metadata. + pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { + self.set_visibility = set_visibility; + self + } + /// Returns a copy of this property marked as coming from constructor promotion. pub const fn with_promoted(mut self) -> Self { self.is_promoted = true; @@ -1546,6 +1554,19 @@ impl EvalClassProperty { self.visibility } + /// Returns the PHP asymmetric write visibility, if it differs from read visibility. + pub const fn set_visibility(&self) -> Option { + self.set_visibility + } + + /// Returns the visibility that applies to writes for this property. + pub const fn write_visibility(&self) -> EvalVisibility { + match self.set_visibility { + Some(visibility) => visibility, + None => self.visibility, + } + } + /// Returns whether this property was declared `static`. pub const fn is_static(&self) -> bool { self.is_static diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 15cf30537f..123fac0222 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -2152,6 +2152,7 @@ fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflection is_dynamic: true, modifiers: eval_reflection_property_modifiers( EvalVisibility::Public, + None, false, false, false, @@ -2501,6 +2502,7 @@ fn eval_reflection_aot_property_metadata( let is_virtual = flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL != 0; let mut modifiers = eval_reflection_property_modifiers( visibility, + None, is_static, is_final, is_abstract, @@ -3995,6 +3997,7 @@ fn eval_reflection_method_modifiers( /// Computes PHP's `ReflectionProperty::getModifiers()` bitmask for eval metadata. fn eval_reflection_property_modifiers( visibility: EvalVisibility, + set_visibility: Option, is_static: bool, is_final: bool, is_abstract: bool, @@ -4021,8 +4024,11 @@ fn eval_reflection_property_modifiers( if is_virtual { modifiers |= 512; } - if is_readonly && visibility == EvalVisibility::Public { - modifiers |= 2048; + match set_visibility { + Some(EvalVisibility::Private) => modifiers |= 32 | 4096, + Some(EvalVisibility::Protected) => modifiers |= 2048, + _ if is_readonly && visibility == EvalVisibility::Public => modifiers |= 2048, + _ => {} } modifiers } @@ -4515,6 +4521,7 @@ fn eval_reflection_property_metadata( is_dynamic: false, modifiers: eval_reflection_property_modifiers( property.visibility(), + property.set_visibility(), property.is_static(), property.is_final(), property.is_abstract(), @@ -4549,6 +4556,7 @@ fn eval_reflection_property_metadata( is_dynamic: false, modifiers: eval_reflection_property_modifiers( EvalVisibility::Public, + None, false, false, true, @@ -4583,6 +4591,7 @@ fn eval_reflection_property_metadata( is_dynamic: false, modifiers: eval_reflection_property_modifiers( property.visibility(), + property.set_visibility(), property.is_static(), property.is_final(), property.is_abstract(), diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 3e5265c4a1..01dbd91f4c 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1181,6 +1181,16 @@ fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus if property.is_static() && property.is_readonly() { return Err(EvalStatus::RuntimeFatal); } + if let Some(set_visibility) = property.set_visibility() { + if property.is_static() || property.property_type().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if property_visibility_rank(set_visibility) + > property_visibility_rank(property.visibility()) + { + return Err(EvalStatus::RuntimeFatal); + } + } if property.is_final() && property.visibility() == EvalVisibility::Private { return Err(EvalStatus::RuntimeFatal); } @@ -1454,9 +1464,10 @@ fn apply_class_abstract_property_requirements( let key = property.name().to_string(); if property.is_abstract() { if let Some(existing) = requirements.get(&key) { - property_contract_visibility_allows(existing, property) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal)?; + (property_contract_visibility_allows(existing, property) + && property_contract_write_visibility_allows(existing, property)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal)?; requirements.insert(key, merge_abstract_property_contracts(existing, property)); } else { requirements.insert(key, property.clone()); @@ -1491,6 +1502,16 @@ fn property_contract_visibility_allows( >= property_visibility_rank(inherited.visibility()) } +/// Returns whether a redeclared property keeps compatible write visibility. +fn property_contract_write_visibility_allows( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> bool { + !inherited.requires_set_hook() + || property_visibility_rank(redeclared.write_visibility()) + >= property_visibility_rank(inherited.write_visibility()) +} + /// Returns whether a concrete property satisfies an abstract hook contract. fn class_property_satisfies_abstract_contract( property: &EvalClassProperty, @@ -1503,7 +1524,8 @@ fn class_property_satisfies_abstract_contract( return false; } if requirement.requires_set_hook() { - return property.has_set_hook() || (!property.has_get_hook() && !property.is_readonly()); + return property_contract_write_visibility_allows(requirement, property) + && (property.has_set_hook() || (!property.has_get_hook() && !property.is_readonly())); } requirement.requires_get_hook() } @@ -1753,8 +1775,9 @@ fn class_has_interface_property( return false; } if requirement.requires_set() { - return property.has_set_hook() - || (!property.has_get_hook() && !property.is_readonly()); + return property.write_visibility() == EvalVisibility::Public + && (property.has_set_hook() + || (!property.has_get_hook() && !property.is_readonly())); } requirement.requires_get() }) @@ -1891,6 +1914,7 @@ pub(in crate::interpreter) fn eval_property_set_result( } return Err(EvalStatus::RuntimeFatal); } + validate_eval_property_write_access(&declaring_class, &property, context)?; validate_eval_readonly_property_write(&declaring_class, &property, context)?; storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); if property.has_set_hook() { @@ -1978,7 +2002,7 @@ fn eval_property_reference_bind_result( let (declaring_class, property) = eval_dynamic_property_for_access(&object_class_name, property_name, context) .ok_or(EvalStatus::RuntimeFatal)?; - validate_eval_member_access(&declaring_class, property.visibility(), context)?; + validate_eval_property_write_access(&declaring_class, &property, context)?; if property.is_readonly() { return Err(EvalStatus::RuntimeFatal); } @@ -2125,6 +2149,7 @@ pub(in crate::interpreter) fn eval_property_unset_result( eval_dynamic_property_for_access(&object_class_name, property_name, context) { if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { + validate_eval_property_write_access(&declaring_class, &property, context)?; validate_eval_readonly_property_write(&declaring_class, &property, context)?; let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); @@ -2410,6 +2435,15 @@ pub(in crate::interpreter) fn eval_instance_property_storage_name( } } +/// Validates the visibility that applies to property writes, including asymmetric `set` visibility. +fn validate_eval_property_write_access( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + validate_eval_member_access(declaring_class, property.write_visibility(), context) +} + /// Reads one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_get_result( class_name: &str, @@ -2581,7 +2615,7 @@ pub(in crate::interpreter) fn eval_static_property_set_result( if !property.is_static() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, property.visibility(), context)?; + validate_eval_property_write_access(&declaring_class, &property, context)?; validate_eval_readonly_property_write(&declaring_class, &property, context)?; if let Some(replaced) = context.set_static_property(&declaring_class, property.name(), value) { values.release(replaced)?; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 41ff180220..a10191b159 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1401,6 +1401,39 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty reports eval-declared asymmetric set visibility. +#[test] +fn execute_program_reflects_eval_asymmetric_property_set_visibility() { + let program = parse_fragment( + br#"class EvalReflectAsymmetricProperty { + public private(set) int $privateSet = 1; + public protected(set) int $protectedSet = 2; + protected private(set) int $protectedPrivateSet = 3; +} +$private = new ReflectionProperty("EvalReflectAsymmetricProperty", "privateSet"); +echo $private->isPrivateSet() ? "P" : "p"; +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->getModifiers(); echo ":"; +$protected = new ReflectionProperty("EvalReflectAsymmetricProperty", "protectedSet"); +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->getModifiers(); echo ":"; +$protectedPrivate = new ReflectionProperty("EvalReflectAsymmetricProperty", "protectedPrivateSet"); +echo $protectedPrivate->isPrivateSet() ? "P" : "p"; +echo $protectedPrivate->isProtectedSet() ? "T" : "t"; +echo $protectedPrivate->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Pt4129:pT2049:Pt4130"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionProperty reports eval constructor-promotion metadata. #[test] fn execute_program_reflection_property_reports_eval_promoted_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 1e1d0208fc..69ad8d4287 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -312,6 +312,127 @@ return $box->id();"#, assert_eq!(values.get(result), FakeValue::Int(13)); } +/// Verifies eval-declared asymmetric properties allow owner and subclass writes as PHP does. +#[test] +fn execute_program_allows_asymmetric_property_writes_from_allowed_scopes() { + let program = parse_fragment( + br#"class EvalAsymWriteBase { + public private(set) int $privateValue = 1; + public protected(set) string $protectedName = "base"; + public function ownerWrite($value, $name) { + $this->privateValue = $value; + $this->protectedName = $name; + } +} +class EvalAsymWriteChild extends EvalAsymWriteBase { + public function childWrite($name) { + $this->protectedName = $name; + } +} +$box = new EvalAsymWriteChild(); +echo $box->privateValue; echo ":"; echo $box->protectedName; echo ":"; +$box->ownerWrite(7, "owner"); +echo $box->privateValue; echo ":"; echo $box->protectedName; echo ":"; +$box->childWrite("child"); +echo $box->protectedName; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:base:7:owner:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `private(set)` rejects global writes without dispatching `__set`. +#[test] +fn execute_program_rejects_private_set_property_write_outside_declaring_class() { + let program = parse_fragment( + br#"class EvalAsymPrivateSetBox { + public private(set) int $value = 1; + public function __set($name, $value) { + echo "bad"; + } +} +$box = new EvalAsymPrivateSetBox(); +$box->value = 2;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("global private(set) property write should fail"); + + assert_eq!(values.output, ""); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared `protected(set)` rejects global writes. +#[test] +fn execute_program_rejects_protected_set_property_write_outside_hierarchy() { + let program = parse_fragment( + br#"class EvalAsymProtectedSetBox { + public protected(set) int $value = 1; +} +$box = new EvalAsymProtectedSetBox(); +$box->value = 2;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("global protected(set) property write should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies asymmetric write restrictions cannot satisfy a public interface set contract. +#[test] +fn execute_program_rejects_private_set_property_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalAsymSetContract { + public int $value { get; set; } +} +class EvalAsymSetContractBox implements EvalAsymSetContract { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) property should fail public interface set contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies asymmetric write restrictions cannot satisfy a public abstract set contract. +#[test] +fn execute_program_rejects_private_set_property_for_abstract_set_contract() { + let program = parse_fragment( + br#"abstract class EvalAsymAbstractSetBase { + abstract public int $value { get; set; } +} +class EvalAsymAbstractSetBox extends EvalAsymAbstractSetBase { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) property should fail public abstract set contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies readonly class inheritance requires matching readonly status. #[test] fn execute_program_rejects_readonly_class_extending_non_readonly_parent() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 90a2953041..8aff383d9e 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -539,7 +539,7 @@ impl Parser { trait_adaptations: &mut Vec, ) -> Result<(), EvalParseError> { let attributes = self.parse_optional_member_attributes()?; - let (visibility, is_static, is_abstract, is_final, is_readonly) = + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = self.parse_class_member_modifiers()?; if is_abstract && is_final { @@ -551,6 +551,7 @@ impl Parser { && !is_abstract && !is_final && !is_readonly + && set_visibility.is_none() && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) { if !attributes.is_empty() { @@ -561,7 +562,7 @@ impl Parser { } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_abstract || is_readonly { + if is_static || is_abstract || is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } constants.push( @@ -575,7 +576,7 @@ impl Parser { } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if is_readonly { + if is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } let (method, promoted_properties) = self.parse_class_method_decl( @@ -592,6 +593,7 @@ impl Parser { let visibility = visibility.unwrap_or(EvalVisibility::Public); let (property, mut hook_methods) = self.parse_class_property_decl( visibility, + set_visibility, is_static, is_final, is_readonly, @@ -758,8 +760,19 @@ impl Parser { /// Parses method modifiers supported by eval class declarations. pub(super) fn parse_class_member_modifiers( &mut self, - ) -> Result<(Option, bool, bool, bool, bool), EvalParseError> { + ) -> Result< + ( + Option, + Option, + bool, + bool, + bool, + bool, + ), + EvalParseError, + > { let mut visibility = None; + let mut set_visibility = None; let mut is_static = false; let mut is_abstract = false; let mut is_final = false; @@ -767,25 +780,43 @@ impl Parser { loop { match self.current() { TokenKind::Ident(name) if ident_eq(name, "public") => { - if visibility.is_some() { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Public); + } else if visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Public); } - visibility = Some(EvalVisibility::Public); - self.advance(); } TokenKind::Ident(name) if ident_eq(name, "protected") => { - if visibility.is_some() { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Protected); + } else if visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Protected); } - visibility = Some(EvalVisibility::Protected); - self.advance(); } TokenKind::Ident(name) if ident_eq(name, "private") => { - if visibility.is_some() { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Private); + } else if visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Private); } - visibility = Some(EvalVisibility::Private); - self.advance(); } TokenKind::Ident(name) if ident_eq(name, "static") => { if is_static { @@ -818,11 +849,42 @@ impl Parser { TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { return Err(EvalParseError::UnsupportedConstruct); } - _ => return Ok((visibility, is_static, is_abstract, is_final, is_readonly)), + _ => { + return Ok(( + visibility, + set_visibility, + is_static, + is_abstract, + is_final, + is_readonly, + )) + } } } } + /// Consumes a PHP asymmetric visibility `(set)` marker after a visibility keyword. + fn consume_set_marker(&mut self) -> Result { + if !self.consume(TokenKind::LParen) { + return Ok(false); + } + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "set") => self.advance(), + _ => return Err(EvalParseError::UnsupportedConstruct), + } + self.expect(TokenKind::RParen)?; + Ok(true) + } + + /// Returns a comparable visibility rank where larger means less restrictive. + fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } + } + /// Parses one class/trait/enum method and returns constructor-promoted properties. pub(super) fn parse_class_method_decl( &mut self, @@ -887,6 +949,7 @@ impl Parser { pub(super) fn parse_class_property_decl( &mut self, visibility: EvalVisibility, + set_visibility: Option, is_static: bool, is_final: bool, is_readonly: bool, @@ -896,8 +959,19 @@ impl Parser { if is_static && is_readonly { return Err(EvalParseError::UnsupportedConstruct); } + if set_visibility.is_some() && is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + if set_visibility.is_some_and(|set_visibility| { + Self::eval_visibility_rank(set_visibility) > Self::eval_visibility_rank(visibility) + }) { + return Err(EvalParseError::UnsupportedConstruct); + } let effective_readonly = is_readonly || (is_readonly_class && !is_static); let property_type = self.parse_optional_property_type()?; + if set_visibility.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; @@ -925,6 +999,7 @@ impl Parser { None, ) .with_type(property_type) + .with_set_visibility(set_visibility) .with_abstract_hook_contract(requires_get_hook, requires_set_hook); return Ok((property, Vec::new())); } @@ -942,6 +1017,7 @@ impl Parser { default, ) .with_type(property_type) + .with_set_visibility(set_visibility) .with_hooks(has_get_hook, has_set_hook) .with_virtual(is_virtual); Ok((property, hook_methods)) @@ -1101,13 +1177,13 @@ impl Parser { methods: &mut Vec, ) -> Result<(), EvalParseError> { let attributes = self.parse_optional_member_attributes()?; - let (visibility, is_static, is_abstract, is_final, is_readonly) = + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = self.parse_class_member_modifiers()?; if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_abstract || is_readonly { + if is_static || is_abstract || is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } constants.push( @@ -1120,7 +1196,7 @@ impl Parser { return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if is_readonly { + if is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } let (method, promoted_properties) = self.parse_class_method_decl( @@ -1136,6 +1212,7 @@ impl Parser { let visibility = visibility.unwrap_or(EvalVisibility::Public); let (property, mut hook_methods) = self.parse_class_property_decl( visibility, + set_visibility, is_static, is_final, is_readonly, @@ -1215,9 +1292,9 @@ impl Parser { cases.push(self.parse_enum_case_decl()?.with_attributes(attributes)); return Ok(()); } - let (visibility, is_static, is_abstract, is_final, is_readonly) = + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = self.parse_class_member_modifiers()?; - if is_abstract || is_readonly { + if is_abstract || is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 52cef39474..06278e7a54 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -583,6 +583,63 @@ fn parse_fragment_accepts_readonly_class_property() { ); } +/// Verifies asymmetric property visibility lowers into eval class metadata. +#[test] +fn parse_fragment_accepts_asymmetric_property_visibility() { + let program = parse_fragment( + b"class DynEvalAsymmetric { public private(set) int $id = 1; protected(set) string $name = \"x\"; }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalAsymmetric", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + Some(EvalExpr::Const(EvalConst::Int(1))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_set_visibility(Some(EvalVisibility::Private)), + EvalClassProperty::with_visibility_static_final_and_readonly( + "name", + EvalVisibility::Public, + false, + false, + false, + Some(EvalExpr::Const(EvalConst::String("x".to_string()))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_set_visibility(Some(EvalVisibility::Protected)), + ], + Vec::new() + ))] + ); +} + +/// Verifies eval rejects asymmetric property visibility forms that PHP rejects. +#[test] +fn parse_fragment_rejects_invalid_asymmetric_property_visibility() { + parse_fragment(b"class DynEvalAsymUntyped { public private(set) $id = 1; }") + .expect_err("asymmetric properties must be typed"); + parse_fragment(b"class DynEvalAsymStatic { public private(set) static int $id = 1; }") + .expect_err("asymmetric properties cannot be static"); + parse_fragment(b"class DynEvalAsymWeak { private public(set) int $id = 1; }") + .expect_err("set visibility cannot be weaker than read visibility"); + parse_fragment(b"class DynEvalAsymMethod { public private(set) function run() {} }") + .expect_err("asymmetric visibility is property-only"); +} + /// Verifies readonly class modifiers lower into class and property metadata. #[test] fn parse_fragment_accepts_readonly_class_modifier() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 297e29d64f..a04c6ebf35 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -137,7 +137,8 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties -and methods, constructor property promotion including by-reference promotion +and methods, asymmetric property write visibility (`private(set)` / +`protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property `get` / `set` hooks, interface property hook contract checks, abstract property hook contracts, property-level `readonly`, `readonly class`, @@ -378,8 +379,8 @@ when the variadic container itself is not rebound, and are reported through metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the bitmask. `isPromoted()` reports generated/AOT and eval-declared promoted-property metadata. `isProtectedSet()` and `isPrivateSet()` derive from -the retained modifier bitmask, including generated/AOT asymmetric visibility and -public readonly property metadata. `isDynamic()` reports `false` for supported +the retained modifier bitmask, including eval-declared and generated/AOT +asymmetric visibility plus public readonly property metadata. `isDynamic()` reports `false` for supported declared properties and `true` for public dynamic object properties materialized with `new ReflectionProperty($object, $property_name)`. `ReflectionProperty::isDefault()` is the inverse for those supported dynamic diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eae1d5eca3..b43c896ccd 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7506,6 +7506,41 @@ echo $protected->getModifiers();'); assert_eq!(out, "Pt4129:pT2049"); } +/// Verifies eval-declared asymmetric property visibility enforces writes and reflects metadata. +#[test] +fn test_eval_declared_asymmetric_property_visibility() { + let out = compile_and_run( + r#"privateValue = $value; + $this->protectedName = $name; + } +} +class EvalDeclaredAsymChild extends EvalDeclaredAsymBase { + public function childWrite($name) { + $this->protectedName = $name; + } +} +$box = new EvalDeclaredAsymChild(); +echo $box->privateValue . ":" . $box->protectedName . ":"; +$box->ownerWrite(7, "owner"); +echo $box->privateValue . ":" . $box->protectedName . ":"; +$box->childWrite("child"); +echo $box->protectedName . ":"; +$private = new ReflectionProperty("EvalDeclaredAsymBase", "privateValue"); +echo ($private->isPrivateSet() ? "P" : "p") . ($private->isProtectedSet() ? "T" : "t"); +echo $private->getModifiers() . ":"; +$protected = new ReflectionProperty("EvalDeclaredAsymBase", "protectedName"); +echo ($protected->isPrivateSet() ? "P" : "p") . ($protected->isProtectedSet() ? "T" : "t"); +echo $protected->getModifiers();'); +"#, + ); + assert_eq!(out, "1:base:7:owner:child:Pt4129:pT2049"); +} + /// Verifies eval can observe AOT constructor-promotion metadata through /// `ReflectionProperty::isPromoted()`. #[test] From 5a5cba758d8efaf3956412b19dbcd4b0888f5c48 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 04:50:16 +0200 Subject: [PATCH 0518/1208] Support eval asymmetric property contracts --- crates/elephc-eval/src/eval_ir.rs | 58 +++++++++++++++ .../elephc-eval/src/interpreter/reflection.rs | 10 ++- .../elephc-eval/src/interpreter/statements.rs | 11 ++- .../tests/builtins_class_metadata.rs | 30 ++++++++ .../src/interpreter/tests/classes.rs | 71 +++++++++++++++++++ crates/elephc-eval/src/parser/statements.rs | 42 +++++++++-- .../src/parser/tests/classes_errors.rs | 45 ++++++++++++ docs/php/classes.md | 1 + docs/php/eval.md | 10 +-- tests/codegen/eval.rs | 26 +++++++ 10 files changed, 290 insertions(+), 14 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index 3fec24609e..f00f11ce14 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -652,6 +652,7 @@ pub struct EvalInterfaceProperty { name: String, attributes: Vec, property_type: Option, + set_visibility: Option, requires_get: bool, requires_set: bool, } @@ -663,6 +664,7 @@ impl EvalInterfaceProperty { name: name.into(), attributes: Vec::new(), property_type: None, + set_visibility: None, requires_get, requires_set, } @@ -674,6 +676,12 @@ impl EvalInterfaceProperty { self } + /// Returns a copy of this interface property with PHP asymmetric write visibility metadata. + pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { + self.set_visibility = set_visibility; + self + } + /// Returns a copy of this interface property with declaration attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; @@ -695,6 +703,19 @@ impl EvalInterfaceProperty { self.property_type.as_ref() } + /// Returns the PHP asymmetric write visibility declared by this contract, if any. + pub const fn set_visibility(&self) -> Option { + self.set_visibility + } + + /// Returns the visibility required for writes by this property contract. + pub const fn write_visibility(&self) -> EvalVisibility { + match self.set_visibility { + Some(visibility) => visibility, + None => EvalVisibility::Public, + } + } + /// Returns whether the interface requires the property to be readable. pub const fn requires_get(&self) -> bool { self.requires_get @@ -714,12 +735,49 @@ impl EvalInterfaceProperty { .property_type .clone() .or_else(|| other.property_type.clone()), + set_visibility: merge_eval_property_set_visibility( + self.set_visibility, + other.set_visibility, + ), requires_get: self.requires_get || other.requires_get, requires_set: self.requires_set || other.requires_set, } } } +/// Merges interface property set-visibility contracts by keeping the stricter write requirement. +const fn merge_eval_property_set_visibility( + left: Option, + right: Option, +) -> Option { + let left = match left { + Some(visibility) => visibility, + None => EvalVisibility::Public, + }; + let right = match right { + Some(visibility) => visibility, + None => EvalVisibility::Public, + }; + let merged = if eval_visibility_rank(left) < eval_visibility_rank(right) { + left + } else { + right + }; + match merged { + EvalVisibility::Public => None, + visibility => Some(visibility), + } +} + +/// Returns a comparable visibility rank where smaller means more restrictive. +const fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} + /// Method signature metadata for a runtime eval interface. #[derive(Debug, Clone, PartialEq)] pub struct EvalInterfaceMethod { diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 123fac0222..04f16e4785 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -3486,6 +3486,14 @@ fn eval_reflection_member_object_result( if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 512) != 0 { flags |= EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL; } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 4096) != 0 { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET | EVAL_REFLECTION_MEMBER_FLAG_FINAL; + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + && (member.modifiers & 2048) != 0 + && !member.is_readonly + { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET; + } if member.is_dynamic { flags |= EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC; } @@ -4556,7 +4564,7 @@ fn eval_reflection_property_metadata( is_dynamic: false, modifiers: eval_reflection_property_modifiers( EvalVisibility::Public, - None, + property.set_visibility(), false, false, true, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 01dbd91f4c..f728a6899a 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -1221,7 +1221,9 @@ fn validate_property_parent_redeclaration( if parent_property.visibility() == EvalVisibility::Private { return Ok(()); } - if parent_property.is_final() { + if parent_property.is_final() + || parent_property.set_visibility() == Some(EvalVisibility::Private) + { return Err(EvalStatus::RuntimeFatal); } Ok(()) @@ -1524,7 +1526,8 @@ fn class_property_satisfies_abstract_contract( return false; } if requirement.requires_set_hook() { - return property_contract_write_visibility_allows(requirement, property) + return requirement.set_visibility() != Some(EvalVisibility::Private) + && property_contract_write_visibility_allows(requirement, property) && (property.has_set_hook() || (!property.has_get_hook() && !property.is_readonly())); } requirement.requires_get_hook() @@ -1775,7 +1778,9 @@ fn class_has_interface_property( return false; } if requirement.requires_set() { - return property.write_visibility() == EvalVisibility::Public + return requirement.set_visibility() != Some(EvalVisibility::Private) + && property_visibility_rank(property.write_visibility()) + >= property_visibility_rank(requirement.write_visibility()) && (property.has_set_hook() || (!property.has_get_hook() && !property.is_readonly())); } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index a10191b159..a3cf17e978 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1434,6 +1434,36 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty reports asymmetric set visibility on eval interface contracts. +#[test] +fn execute_program_reflects_eval_interface_asymmetric_property_set_visibility() { + let program = parse_fragment( + br#"interface EvalReflectAsymmetricIfaceProperty { + public protected(set) string $name { get; set; } + private(set) int $id { get; set; } +} +$protected = new ReflectionProperty("EvalReflectAsymmetricIfaceProperty", "name"); +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isFinal() ? "F" : "f"; +echo $protected->getModifiers(); echo ":"; +$private = new ReflectionProperty("EvalReflectAsymmetricIfaceProperty", "id"); +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->isPrivateSet() ? "P" : "p"; +echo $private->isFinal() ? "F" : "f"; +echo $private->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Tpf2625:tPF4705"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionProperty reports eval constructor-promotion metadata. #[test] fn execute_program_reflection_property_reports_eval_promoted_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 69ad8d4287..a073bc3762 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -433,6 +433,77 @@ class EvalAsymAbstractSetBox extends EvalAsymAbstractSetBase { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval interface protected(set) property contracts accept compatible implementations. +#[test] +fn execute_program_allows_interface_protected_set_property_contract() { + let program = parse_fragment( + br#"interface EvalAsymProtectedSetContract { + public protected(set) string $name { get; set; } +} +class EvalAsymProtectedSetBase implements EvalAsymProtectedSetContract { + public protected(set) string $name = "base"; +} +class EvalAsymProtectedSetChild extends EvalAsymProtectedSetBase { + public function rename($name) { $this->name = $name; } +} +$box = new EvalAsymProtectedSetChild(); +echo $box->name; echo ":"; +$box->rename("child"); +echo $box->name; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "base:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies private(set) interface contracts are final and cannot be implemented by a class. +#[test] +fn execute_program_rejects_private_set_interface_property_contract_implementation() { + let program = parse_fragment( + br#"interface EvalAsymPrivateSetInterfaceContract { + public private(set) int $value { get; set; } +} +class EvalAsymPrivateSetInterfaceBox implements EvalAsymPrivateSetInterfaceContract { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) interface contract should be final"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies private(set) abstract properties behave as final contracts. +#[test] +fn execute_program_rejects_private_set_abstract_property_redeclaration() { + let program = parse_fragment( + br#"abstract class EvalAsymPrivateSetAbstractBase { + abstract public private(set) int $value { get; set; } +} +class EvalAsymPrivateSetAbstractBox extends EvalAsymPrivateSetAbstractBase { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) abstract property should be final"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies readonly class inheritance requires matching readonly status. #[test] fn execute_program_rejects_readonly_class_extending_non_readonly_parent() { diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 8aff383d9e..3f5630a273 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -1407,14 +1407,35 @@ impl Parser { let mut is_static = false; let mut is_final = false; let mut saw_public = false; + let mut set_visibility = None; loop { match self.current() { TokenKind::Ident(name) if ident_eq(name, "public") => { - if saw_public { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Public); + } else if saw_public { return Err(EvalParseError::UnsupportedConstruct); + } else { + saw_public = true; } - saw_public = true; + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { self.advance(); + if !self.consume_set_marker()? || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Protected); + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + self.advance(); + if !self.consume_set_marker()? || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Private); } TokenKind::Ident(name) if ident_eq(name, "static") => { if is_static { @@ -1437,7 +1458,7 @@ impl Parser { } } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static { + if is_static || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } constants.push( @@ -1447,7 +1468,7 @@ impl Parser { return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if is_final { + if is_final || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } methods.push( @@ -1460,7 +1481,7 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } properties.push( - self.parse_interface_property_decl()? + self.parse_interface_property_decl(set_visibility)? .with_attributes(attributes), ); Ok(()) @@ -1506,8 +1527,12 @@ impl Parser { /// Parses one interface property hook contract. pub(super) fn parse_interface_property_decl( &mut self, + set_visibility: Option, ) -> Result { let property_type = self.parse_optional_property_type()?; + if set_visibility.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; @@ -1517,7 +1542,12 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } let (requires_get, requires_set) = self.parse_interface_property_hook_contracts()?; - Ok(EvalInterfaceProperty::new(name, requires_get, requires_set).with_type(property_type)) + if set_visibility.is_some() && !requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalInterfaceProperty::new(name, requires_get, requires_set) + .with_type(property_type) + .with_set_visibility(set_visibility)) } /// Parses `{ get; set; }` hook contracts for an abstract or interface property. diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-eval/src/parser/tests/classes_errors.rs index 06278e7a54..4db8538dc7 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-eval/src/parser/tests/classes_errors.rs @@ -356,6 +356,43 @@ fn parse_fragment_accepts_interface_property_hook_contracts() { ); } +/// Verifies interface property contracts retain asymmetric set visibility. +#[test] +fn parse_fragment_accepts_interface_asymmetric_property_contracts() { + let program = parse_fragment( + br#"interface DynEvalAsymmetricIface { + public protected(set) string $name { get; set; } + private(set) int $id { get; set; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl( + EvalInterface::with_constants_and_properties( + "DynEvalAsymmetricIface", + Vec::new(), + Vec::new(), + vec![ + EvalInterfaceProperty::new("name", true, true) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_set_visibility(Some(EvalVisibility::Protected)), + EvalInterfaceProperty::new("id", true, true) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_set_visibility(Some(EvalVisibility::Private)), + ], + Vec::new(), + ) + )] + ); +} + /// Verifies public property and method class members lower into dynamic class metadata. #[test] fn parse_fragment_accepts_public_class_members() { @@ -832,6 +869,14 @@ fn parse_fragment_rejects_invalid_interface_property_hooks() { .expect_err("interface property hooks require at least one contract"); parse_fragment(b"interface DynEvalIfaceHookDefault { public int $id = 1 { get; } }") .expect_err("interface property hook contracts cannot have defaults"); + parse_fragment( + b"interface DynEvalIfaceAsymReadOnly { public protected(set) int $id { get; } }", + ) + .expect_err("readonly virtual property cannot have asymmetric visibility"); + parse_fragment( + b"interface DynEvalIfaceAsymUntyped { public protected(set) $id { get; set; } }", + ) + .expect_err("asymmetric interface property must be typed"); } /// Verifies abstract and final class modifiers lower into dynamic class metadata. diff --git a/docs/php/classes.md b/docs/php/classes.md index 16453e2b91..959e20e5b6 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -302,6 +302,7 @@ Rules: - The write visibility must not be weaker than the read visibility (`private public(set)` is rejected). - The property must be typed, and the modifier is not allowed on static properties. - Indirect writes through an array element (`$obj->items[] = x`, `$obj->items['k'] = x`) are writes too, so they honor the `set` visibility — not the (wider) read visibility. +- Abstract and interface property hook contracts may carry asymmetric write visibility on writable (`{ set; }`) contracts. `private(set)` contracts are final and cannot be implemented or redeclared by a concrete child property. ### Property redeclaration diff --git a/docs/php/eval.md b/docs/php/eval.md index a04c6ebf35..ae61f798fb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -140,8 +140,9 @@ Eval-declared classes support inheritance, public/protected/private properties and methods, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, -concrete property `get` / `set` hooks, interface property hook contract checks, -abstract property hook contracts, property-level `readonly`, `readonly class`, +concrete property `get` / `set` hooks, interface property hook contract checks +including asymmetric write visibility, abstract property hook contracts +including asymmetric write visibility, property-level `readonly`, `readonly class`, `__construct()`, abstract classes and methods, final classes, methods, and properties, trait composition with `insteadof` conflict resolution and `as` aliases/visibility adaptations, interface implementation checks, static @@ -379,8 +380,9 @@ when the variadic container itself is not rebound, and are reported through metadata with PHP-compatible `ReflectionProperty::IS_*` constants for the bitmask. `isPromoted()` reports generated/AOT and eval-declared promoted-property metadata. `isProtectedSet()` and `isPrivateSet()` derive from -the retained modifier bitmask, including eval-declared and generated/AOT -asymmetric visibility plus public readonly property metadata. `isDynamic()` reports `false` for supported +the retained modifier bitmask, including eval-declared class, abstract-property, +interface-property, and generated/AOT asymmetric visibility plus public readonly +property metadata. `isDynamic()` reports `false` for supported declared properties and `true` for public dynamic object properties materialized with `new ReflectionProperty($object, $property_name)`. `ReflectionProperty::isDefault()` is the inverse for those supported dynamic diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b43c896ccd..b1821ff71e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7541,6 +7541,32 @@ echo $protected->getModifiers();'); assert_eq!(out, "1:base:7:owner:child:Pt4129:pT2049"); } +/// Verifies eval-declared interface asymmetric property contracts are enforced and reflected. +#[test] +fn test_eval_declared_interface_asymmetric_property_contract() { + let out = compile_and_run( + r#"name = $name; } +} +$box = new EvalIfaceAsymChild(); +echo $box->name . ":"; +$box->rename("child"); +echo $box->name . ":"; +$ref = new ReflectionProperty("EvalIfaceAsymContract", "name"); +echo ($ref->isProtectedSet() ? "T" : "t") . ($ref->isPrivateSet() ? "P" : "p"); +echo $ref->getModifiers();'); +"#, + ); + assert_eq!(out, "base:child:Tp2625"); +} + /// Verifies eval can observe AOT constructor-promotion metadata through /// `ReflectionProperty::isPromoted()`. #[test] From a0a1f75264565ec2551fac3cef930473e9e27481 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 05:03:23 +0200 Subject: [PATCH 0519/1208] Support eval ReflectionParameter object targets --- .../elephc-eval/src/interpreter/reflection.rs | 170 ++++++++++++++++++ .../tests/builtins_class_metadata.rs | 30 ++++ docs/php/classes.md | 2 +- docs/php/eval.md | 5 +- tests/codegen/eval.rs | 30 ++++ 5 files changed, 235 insertions(+), 2 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 04f16e4785..defe4c87f4 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -140,6 +140,12 @@ enum EvalReflectionPropertyHook { Set, } +/// Constructor selector accepted by `ReflectionParameter`. +enum EvalReflectionParameterSelector { + Name(String), + Position(i64), +} + impl EvalReflectionPropertyHook { /// Returns the associative-array key PHP uses for this hook kind. const fn key(self) -> &'static str { @@ -218,6 +224,9 @@ pub(in crate::interpreter) fn eval_reflection_owner_new_object( Some(EVAL_REFLECTION_OWNER_PROPERTY) => { eval_reflection_property_new(evaluated_args, context, values) } + Some(EVAL_REFLECTION_OWNER_PARAMETER) => { + eval_reflection_parameter_new(evaluated_args, context, values) + } Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT) => { eval_reflection_class_constant_new(evaluated_args, context, values) } @@ -1972,6 +1981,166 @@ fn eval_reflection_function_object_result( ) } +/// Builds an eval-backed `ReflectionParameter` object for a function or method parameter. +fn eval_reflection_parameter_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("function"), String::from("param")], + evaluated_args, + )?; + let selector = eval_reflection_parameter_selector(args[1], values)?; + let Some(parameter) = + eval_reflection_parameter_constructor_metadata(args[0], selector, context, values)? + else { + return Ok(None); + }; + eval_reflection_parameter_object_result(¶meter, context, values).map(Some) +} + +/// Resolves `ReflectionParameter` constructor target metadata. +fn eval_reflection_parameter_constructor_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_array_like(target)? { + return eval_reflection_method_parameter_metadata(target, selector, context, values); + } + if values.type_tag(target)? == EVAL_TAG_STRING { + return eval_reflection_function_parameter_metadata(target, selector, context, values); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Builds selected parameter metadata for an eval or native free function. +fn eval_reflection_function_parameter_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let requested_name = eval_reflection_string_arg(target, values)?; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if let Some(function) = context.function(&lookup_name).cloned() { + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + return Ok(eval_reflection_parameter_for_selector(parameters, selector)); + } + if let Some(function) = context.native_function(&lookup_name) { + let reflected_name = requested_name.trim_start_matches('\\'); + let parameter_names = eval_reflection_native_function_parameter_names(&function); + let parameter_attributes = vec![Vec::new(); parameter_names.len()]; + let parameter_types: Vec> = vec![None; parameter_names.len()]; + let parameter_defaults = vec![None; parameter_names.len()]; + let parameter_is_by_ref = vec![false; parameter_names.len()]; + let parameter_is_variadic = vec![false; parameter_names.len()]; + let parameters = eval_reflection_function_parameters( + reflected_name, + ¶meter_names, + Vec::new(), + ¶meter_attributes, + ¶meter_types, + ¶meter_defaults, + ¶meter_is_by_ref, + ¶meter_is_variadic, + ); + return Ok(eval_reflection_parameter_for_selector(parameters, selector)); + } + Ok(None) +} + +/// Builds selected parameter metadata for an eval or generated/AOT method target. +fn eval_reflection_method_parameter_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(target)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(target, zero)?; + let method = values.array_get(target, one)?; + let method_name = eval_reflection_string_arg(method, values)?; + let class_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_reflection_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let member = if eval_reflection_class_like_exists(&class_name, context) { + let reflected_method_name = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &class_name, + &method_name, + context, + ) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_method_metadata(&class_name, &reflected_method_name, context) + .ok_or(EvalStatus::RuntimeFatal)? + } else { + let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( + &class_name, + &method_name, + context, + values, + )? + else { + return Ok(None); + }; + member + }; + Ok(eval_reflection_parameter_for_selector( + member.parameters, + selector, + )) +} + +/// Converts a `ReflectionParameter` selector runtime value to a supported selector. +fn eval_reflection_parameter_selector( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_STRING => { + eval_reflection_string_arg(value, values).map(EvalReflectionParameterSelector::Name) + } + EVAL_TAG_INT => { + eval_int_value(value, values).map(EvalReflectionParameterSelector::Position) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Selects a parameter by PHP name or zero-based position. +fn eval_reflection_parameter_for_selector( + parameters: Vec, + selector: EvalReflectionParameterSelector, +) -> Option { + match selector { + EvalReflectionParameterSelector::Name(name) => parameters + .into_iter() + .find(|parameter| parameter.name == name), + EvalReflectionParameterSelector::Position(position) if position >= 0 => { + parameters.into_iter().nth(position as usize) + } + EvalReflectionParameterSelector::Position(_) => None, + } +} + /// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. fn eval_reflection_method_new( evaluated_args: Vec, @@ -6142,6 +6311,7 @@ fn reflection_owner_kind(class_name: &str) -> Option { "reflectionfunction" => Some(EVAL_REFLECTION_OWNER_FUNCTION), "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), "reflectionproperty" => Some(EVAL_REFLECTION_OWNER_PROPERTY), + "reflectionparameter" => Some(EVAL_REFLECTION_OWNER_PARAMETER), "reflectionclassconstant" => Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT), "reflectionenumunitcase" => Some(EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE), "reflectionenumbackedcase" => Some(EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE), diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index a3cf17e978..1549a19f2d 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -2348,6 +2348,36 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies direct ReflectionParameter construction accepts runtime object method targets. +#[test] +fn execute_program_reflection_parameter_accepts_object_expression_target() { + let program = parse_fragment( + br#"class EvalDirectParamObjectTarget { + public function run(int $id, ?string $name = null) {} +} +$param = new ReflectionParameter([new EvalDirectParamObjectTarget(), "run"], "name"); +echo $param->getName(); echo ":"; +echo $param->getPosition(); echo ":"; +echo $param->getDeclaringClass()->getName(); echo ":"; +echo $param->getDeclaringFunction()->getName(); echo ":"; +echo $param->isOptional() ? "O" : "R"; echo ":"; +echo $param->getType()->getName(); echo ":"; +echo $param->allowsNull() ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "name:1:EvalDirectParamObjectTarget:run:O:string:N" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::getMethods preserves eval method parameter metadata. #[test] fn execute_program_reflection_class_lists_eval_method_parameters() { diff --git a/docs/php/classes.md b/docs/php/classes.md index 959e20e5b6..0bb625a951 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1338,7 +1338,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), plus `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`) outside eval; inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index ae61f798fb..fd13cb7e76 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -345,7 +345,10 @@ flags are retained for eval-declared functions and methods, including returns the declaring class-like symbol for eval method parameters, and `ReflectionParameter::getDeclaringFunction()` returns a `ReflectionFunction` object for eval free-function parameters or a `ReflectionMethod` object for the -declaring eval method. `ReflectionFunction::invoke()` and `invokeArgs()` +declaring eval method. Direct `new ReflectionParameter(...)` construction +accepts eval free-function names, class/interface/trait method arrays, and +object-method arrays resolved from the evaluated runtime object, including +inline `new` expressions. `ReflectionFunction::invoke()` and `invokeArgs()` dispatch eval-declared functions with the same named/default/variadic argument binding used by direct eval function calls. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b1821ff71e..2a0f0f205c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8540,6 +8540,36 @@ foreach ($params as $param) { ); } +/// Verifies eval direct ReflectionParameter construction accepts runtime object method targets. +#[test] +fn test_eval_reflection_parameter_accepts_object_expression_target() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo $param->getPosition() . ":"; +echo $param->getDeclaringClass()->getName() . ":"; +echo $param->getDeclaringFunction()->getName() . ":"; +echo ($param->isOptional() ? "O" : "R") . ":"; +echo $param->getType()->getName() . ":"; +echo $param->allowsNull() ? "N" : "n"; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "name:1:EvalDirectParamObjectTarget:run:O:string:N" + ); +} + /// Verifies eval ReflectionParameter exposes PHP constant-default metadata. #[test] fn test_eval_reflection_parameter_reports_default_constant_metadata() { From 1b1dbbb7066076ad7a24434910eecb7d2429aef8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 05:17:41 +0200 Subject: [PATCH 0520/1208] Support inline ReflectionParameter object targets --- docs/php/classes.md | 2 +- src/ir_lower/expr/mod.rs | 80 ++++++++++++++++--- .../checker/inference/objects/constructors.rs | 39 +++++---- .../codegen/objects/constructor_promotion.rs | 8 +- 4 files changed, 99 insertions(+), 30 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 0bb625a951..a03ecebfbd 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1338,7 +1338,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`) outside eval; inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and arbitrary object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), and inline `new ClassName()` object-method arrays outside eval; inline object construction is still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and broader non-literal object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 0092618e96..7479bd6f53 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9184,7 +9184,15 @@ fn lower_clone(ctx: &mut LoweringContext<'_, '_>, inner: &Expr, expr: &Expr) -> /// Metadata operand source for direct `ReflectionParameter` constructor lowering. enum ReflectionParameterConstructorOperand { Expr(Expr), - ClassName { name: String, span: Span }, + ClassName { + name: String, + span: Span, + }, + ObjectExprClassName { + expr: Expr, + name: String, + span: Span, + }, } /// Lowers validated `ReflectionParameter` constructor arguments into metadata operands. @@ -9211,21 +9219,37 @@ fn lower_reflection_parameter_constructor_operand( ) -> ValueId { match operand { ReflectionParameterConstructorOperand::Expr(expr) => lower_expr(ctx, expr).value, + ReflectionParameterConstructorOperand::ObjectExprClassName { expr, name, span } => { + let object = lower_expr(ctx, expr); + if ctx.value_is_owning_temporary(object) { + crate::ir_lower::ownership::release_if_owned(ctx, object, Some(*span)); + } + emit_reflection_parameter_class_name_operand(ctx, name, *span) + } ReflectionParameterConstructorOperand::ClassName { name, span } => { - let data = ctx.intern_class_name(name); - ctx.emit_value( - Op::ConstClassName, - Vec::new(), - Some(Immediate::Data(data)), - PhpType::Str, - Op::ConstClassName.default_effects(), - Some(*span), - ) - .value + emit_reflection_parameter_class_name_operand(ctx, name, *span) } } } +/// Emits one class-name operand for direct `ReflectionParameter` metadata. +fn emit_reflection_parameter_class_name_operand( + ctx: &mut LoweringContext<'_, '_>, + name: &str, + span: Span, +) -> ValueId { + let data = ctx.intern_class_name(name); + ctx.emit_value( + Op::ConstClassName, + Vec::new(), + Some(Immediate::Data(data)), + PhpType::Str, + Op::ConstClassName.default_effects(), + Some(span), + ) + .value +} + /// Returns metadata operand expressions from a normalized static `ReflectionParameter` call. fn reflection_parameter_constructor_arg_exprs( ctx: &LoweringContext<'_, '_>, @@ -9288,6 +9312,18 @@ fn reflection_parameter_method_owner_operand( owner: &Expr, ) -> Option { match &owner.kind { + ExprKind::StringLiteral(name) => Some(ReflectionParameterConstructorOperand::ClassName { + name: name.clone(), + span: owner.span, + }), + ExprKind::ClassConstant { receiver } => { + static_receiver_class_name(ctx, receiver).map(|name| { + ReflectionParameterConstructorOperand::ClassName { + name, + span: owner.span, + } + }) + } ExprKind::Variable(name) => { let PhpType::Object(class_name) = ctx.local_type(name).codegen_repr() else { return None; @@ -9308,8 +9344,28 @@ fn reflection_parameter_method_owner_operand( span: owner.span, }) } - _ => Some(ReflectionParameterConstructorOperand::Expr(owner.clone())), + _ => reflection_parameter_object_expr_class_name(ctx, owner).map(|name| { + ReflectionParameterConstructorOperand::ObjectExprClassName { + expr: owner.clone(), + name, + span: owner.span, + } + }), + } +} + +/// Returns the concrete class name for an object expression usable as reflection metadata. +fn reflection_parameter_object_expr_class_name( + ctx: &LoweringContext<'_, '_>, + owner: &Expr, +) -> Option { + let PhpType::Object(class_name) = fallback_expr_type(owner).codegen_repr() else { + return None; + }; + if class_name.is_empty() || !ctx.classes.contains_key(class_name.as_str()) { + return None; } + Some(class_name) } /// Lowers PHP `new $class(...)` into the generic dynamic-new EIR opcode. diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 9e73457837..512771643c 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -468,21 +468,8 @@ impl Checker { ExprKind::StringLiteral(_) | ExprKind::ClassConstant { .. } => { self.reflection_class_literal_arg("ReflectionParameter", arg, env) } - ExprKind::Variable(_) | ExprKind::This => { - let target_ty = self.infer_type(arg, env)?.codegen_repr(); - let PhpType::Object(class_name) = target_ty else { - return Err(CompileError::new( - arg.span, - "ReflectionParameter::__construct() object target must have a concrete class type", - )); - }; - if class_name.is_empty() || !self.classes.contains_key(class_name.as_str()) { - return Err(CompileError::new( - arg.span, - "ReflectionParameter::__construct() object target must have a concrete class type", - )); - } - Ok(class_name) + ExprKind::Variable(_) | ExprKind::This | ExprKind::NewObject { .. } => { + self.reflection_parameter_concrete_object_class_name(arg, env) } _ => Err(CompileError::new( arg.span, @@ -491,6 +478,28 @@ impl Checker { } } + /// Resolves a statically known object expression to the reflected class name. + fn reflection_parameter_concrete_object_class_name( + &mut self, + arg: &Expr, + env: &TypeEnv, + ) -> Result { + let target_ty = self.infer_type(arg, env)?.codegen_repr(); + let PhpType::Object(class_name) = target_ty else { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() object target must have a concrete class type", + )); + }; + if class_name.is_empty() || !self.classes.contains_key(class_name.as_str()) { + return Err(CompileError::new( + arg.span, + "ReflectionParameter::__construct() object target must have a concrete class type", + )); + } + Ok(class_name) + } + /// Returns the reflected signature for a statically declared user function. fn reflection_function_signature( &mut self, diff --git a/tests/codegen/objects/constructor_promotion.rs b/tests/codegen/objects/constructor_promotion.rs index c4e62b5302..f2492daec3 100644 --- a/tests/codegen/objects/constructor_promotion.rs +++ b/tests/codegen/objects/constructor_promotion.rs @@ -192,7 +192,9 @@ fn test_reflection_parameter_is_promoted() { let out = compile_and_run( r#"isPromoted() ? "D" : "d"; $run = new ReflectionParameter([ReflectPromotedParamUser::class, "run"], "id"); echo $run->isPromoted() ? "R" : "r"; +$inline = new ReflectionParameter([new ReflectPromotedParamUser(1), "run"], 0); +echo ":" . $inline->getName(); "#, ); - assert_eq!(out, "InDr"); + assert_eq!(out, "InDrC:id"); } From 63649a60388f3e3b33b349195bc97d800f2e366c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 05:23:48 +0200 Subject: [PATCH 0521/1208] Support typed ReflectionParameter object expressions --- docs/php/classes.md | 14 ++++---- src/ir_lower/expr/mod.rs | 34 +++++++------------ .../checker/inference/objects/constructors.rs | 8 +---- .../codegen/objects/constructor_promotion.rs | 11 +++++- 4 files changed, 31 insertions(+), 36 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index a03ecebfbd..75ef150aad 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1206,14 +1206,14 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | -| `ReflectionParameter::getName()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | -| `ReflectionParameter::getPosition()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | -| `ReflectionParameter::isOptional()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | -| `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | -| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | +| `ReflectionParameter::getName()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | +| `ReflectionParameter::getPosition()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | +| `ReflectionParameter::isOptional()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | +| `ReflectionParameter::isVariadic()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return whether the parameter is variadic | +| `ReflectionParameter::isPassedByReference()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return whether the parameter is passed by reference | | `ReflectionParameter::canBePassedByValue()` | Same construction forms as `ReflectionParameter::isPassedByReference()` | Return whether the parameter is not by-reference | | `ReflectionParameter::isPromoted()` | Same construction forms as `ReflectionParameter::isPassedByReference()` | Return whether the parameter came from constructor property promotion | -| `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object, "method"], "name" or 0)`, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | +| `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, a `ReflectionIntersectionType` for intersection parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | | `ReflectionParameter::allowsNull()` | Same as `ReflectionParameter::hasType()` | Return PHP nullability for retained parameter type/default metadata | | `ReflectionParameter::isArray()` / `isCallable()` | Same as `ReflectionParameter::hasType()` | Return PHP's legacy named-type predicates for nullable or non-nullable `array` / `callable` parameter declarations; union declarations report `false` | @@ -1338,7 +1338,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed local/`$this` object method arrays (`[$object, "method"]`), and inline `new ClassName()` object-method arrays outside eval; inline object construction is still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and broader non-literal object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 7479bd6f53..40489dbfed 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9184,15 +9184,8 @@ fn lower_clone(ctx: &mut LoweringContext<'_, '_>, inner: &Expr, expr: &Expr) -> /// Metadata operand source for direct `ReflectionParameter` constructor lowering. enum ReflectionParameterConstructorOperand { Expr(Expr), - ClassName { - name: String, - span: Span, - }, - ObjectExprClassName { - expr: Expr, - name: String, - span: Span, - }, + ClassName { name: String, span: Span }, + ObjectExpr { expr: Expr, span: Span }, } /// Lowers validated `ReflectionParameter` constructor arguments into metadata operands. @@ -9219,12 +9212,14 @@ fn lower_reflection_parameter_constructor_operand( ) -> ValueId { match operand { ReflectionParameterConstructorOperand::Expr(expr) => lower_expr(ctx, expr).value, - ReflectionParameterConstructorOperand::ObjectExprClassName { expr, name, span } => { + ReflectionParameterConstructorOperand::ObjectExpr { expr, span } => { let object = lower_expr(ctx, expr); + let class_name = reflection_parameter_lowered_object_class_name(ctx, object.value) + .expect("ReflectionParameter object target must be type-checked as a known object"); if ctx.value_is_owning_temporary(object) { crate::ir_lower::ownership::release_if_owned(ctx, object, Some(*span)); } - emit_reflection_parameter_class_name_operand(ctx, name, *span) + emit_reflection_parameter_class_name_operand(ctx, &class_name, *span) } ReflectionParameterConstructorOperand::ClassName { name, span } => { emit_reflection_parameter_class_name_operand(ctx, name, *span) @@ -9344,22 +9339,19 @@ fn reflection_parameter_method_owner_operand( span: owner.span, }) } - _ => reflection_parameter_object_expr_class_name(ctx, owner).map(|name| { - ReflectionParameterConstructorOperand::ObjectExprClassName { - expr: owner.clone(), - name, - span: owner.span, - } + _ => Some(ReflectionParameterConstructorOperand::ObjectExpr { + expr: owner.clone(), + span: owner.span, }), } } -/// Returns the concrete class name for an object expression usable as reflection metadata. -fn reflection_parameter_object_expr_class_name( +/// Returns the concrete class name from a lowered object target. +fn reflection_parameter_lowered_object_class_name( ctx: &LoweringContext<'_, '_>, - owner: &Expr, + value: ValueId, ) -> Option { - let PhpType::Object(class_name) = fallback_expr_type(owner).codegen_repr() else { + let PhpType::Object(class_name) = ctx.builder.value_php_type(value).codegen_repr() else { return None; }; if class_name.is_empty() || !ctx.classes.contains_key(class_name.as_str()) { diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 512771643c..39c3dc8783 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -468,13 +468,7 @@ impl Checker { ExprKind::StringLiteral(_) | ExprKind::ClassConstant { .. } => { self.reflection_class_literal_arg("ReflectionParameter", arg, env) } - ExprKind::Variable(_) | ExprKind::This | ExprKind::NewObject { .. } => { - self.reflection_parameter_concrete_object_class_name(arg, env) - } - _ => Err(CompileError::new( - arg.span, - "ReflectionParameter::__construct() requires a literal class name or statically typed object target", - )), + _ => self.reflection_parameter_concrete_object_class_name(arg, env), } } diff --git a/tests/codegen/objects/constructor_promotion.rs b/tests/codegen/objects/constructor_promotion.rs index f2492daec3..cfeb44a4e3 100644 --- a/tests/codegen/objects/constructor_promotion.rs +++ b/tests/codegen/objects/constructor_promotion.rs @@ -197,6 +197,12 @@ class ReflectPromotedParamUser { } public function run(int $id) {} } +class ReflectPromotedParamFactory { + public function make(): ReflectPromotedParamUser { + echo "F"; + return new ReflectPromotedParamUser(2); + } +} $ctor = new ReflectionMethod(ReflectPromotedParamUser::class, "__construct"); $params = $ctor->getParameters(); echo $params[0]->isPromoted() ? "I" : "i"; @@ -207,7 +213,10 @@ $run = new ReflectionParameter([ReflectPromotedParamUser::class, "run"], "id"); echo $run->isPromoted() ? "R" : "r"; $inline = new ReflectionParameter([new ReflectPromotedParamUser(1), "run"], 0); echo ":" . $inline->getName(); +$factory = new ReflectPromotedParamFactory(); +$fromReturn = new ReflectionParameter([$factory->make(), "run"], 0); +echo ":" . $fromReturn->getName(); "#, ); - assert_eq!(out, "InDrC:id"); + assert_eq!(out, "InDrC:idFC:id"); } From 5b358f265e7ded9d8d5862a0d326872db26f1897 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 05:40:39 +0200 Subject: [PATCH 0522/1208] Support array reflection defaults --- docs/php/classes.md | 6 +- src/codegen/lower_inst/objects/reflection.rs | 99 ++++++++++++++++---- tests/codegen/oop/attributes.rs | 32 ++++++- 3 files changed, 113 insertions(+), 24 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 75ef150aad..47a35741a6 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1221,7 +1221,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | -| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null default values, or throw `ReflectionException` when no default is available | +| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, and indexed-array literal default values, or throw `ReflectionException` when no default is available | | `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | @@ -1321,7 +1321,7 @@ name with `isBuiltin()` false. | `ReflectionParameter::isArray()` / `isCallable()` | `bool` | Legacy named-type predicates for direct `array` / `callable` hints | | `ReflectionParameter::getAttributes()` | `ReflectionAttribute[]` | The reflected method parameter's attributes | | `ReflectionParameter::isDefaultValueAvailable()` | `bool` | True when a supported default value is available | -| `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar or null default value, or throws when unavailable | +| `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar, null, class-constant, or indexed-array literal default value, or throws when unavailable | | `ReflectionParameter::isDefaultValueConstant()` | `bool` | True when the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | `?string` | The constant name for constant-backed defaults, or `null` | | `ReflectionNamedType::getName()` | `string` | The type name (`int`, `string`, a class name, …) | @@ -1338,7 +1338,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/default-constant metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/indexed-array default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index c2a1bcd88d..33a1ce3819 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -18,7 +18,9 @@ use crate::codegen::literal_defaults::{ emit_boxed_string_literal_default_to_result, emit_empty_assoc_array_literal_to_result, }; use crate::codegen::{ - abi, emit_box_current_value_as_mixed, runtime_value_tag, CodegenIrError, Result, + abi, emit_array_value_type_stamp, emit_box_current_owned_value_as_mixed, + emit_box_current_value_as_mixed, emit_release_pushed_refcounted_temp_after_array_push, + runtime_value_tag, CodegenIrError, Result, }; use crate::ir::{Immediate, Instruction, Op, TraitMethodInfo, ValueDef, ValueId}; use crate::names::{ @@ -173,6 +175,7 @@ enum ReflectionParameterDefaultValue { Float(f64), Str(String), Null, + Array(Vec), } /// Metadata for one constant entry returned by `ReflectionClass::getConstants()`. @@ -2781,6 +2784,11 @@ fn reflection_literal_parameter_default_value( ExprKind::FloatLiteral(value) => Some(ReflectionParameterDefaultValue::Float(*value)), ExprKind::StringLiteral(value) => Some(ReflectionParameterDefaultValue::Str(value.clone())), ExprKind::Null => Some(ReflectionParameterDefaultValue::Null), + ExprKind::ArrayLiteral(items) => items + .iter() + .map(reflection_literal_parameter_default_value) + .collect::>>() + .map(ReflectionParameterDefaultValue::Array), ExprKind::Negate(inner) => match &inner.kind { ExprKind::IntLiteral(value) => value .checked_neg() @@ -4095,6 +4103,78 @@ fn emit_reflection_default_value_as_mixed( emit_boxed_string_literal_default_to_result(ctx, value) } ReflectionParameterDefaultValue::Null => emit_boxed_null_literal_to_result(ctx), + ReflectionParameterDefaultValue::Array(elements) => { + emit_reflection_indexed_array_default_as_mixed(ctx, elements) + } + } +} + +/// Materializes an indexed Reflection default array as a boxed Mixed cell. +fn emit_reflection_indexed_array_default_as_mixed( + ctx: &mut FunctionContext<'_>, + elements: &[ReflectionParameterDefaultValue], +) { + emit_reflection_indexed_array_default_to_result(ctx, elements); + emit_box_current_owned_value_as_mixed(ctx.emitter, &PhpType::Array(Box::new(PhpType::Mixed))); +} + +/// Allocates and populates an indexed array whose slots hold boxed Mixed defaults. +fn emit_reflection_indexed_array_default_to_result( + ctx: &mut FunctionContext<'_>, + elements: &[ReflectionParameterDefaultValue], +) { + emit_reflection_mixed_array_allocation(ctx, elements.len()); + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + for element in elements { + emit_reflection_default_value_as_mixed(ctx, element); + append_reflection_mixed_array_default_element(ctx); + } + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); +} + +/// Allocates an indexed array stamped for boxed Mixed payload slots. +fn emit_reflection_mixed_array_allocation(ctx: &mut FunctionContext<'_>, element_count: usize) { + let capacity = element_count.max(4); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_int_immediate(ctx.emitter, "x0", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "x1", 8); + } + Arch::X86_64 => { + abi::emit_load_int_immediate(ctx.emitter, "rdi", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "rsi", 8); + } + } + abi::emit_call_label(ctx.emitter, "__rt_array_new"); + emit_array_value_type_stamp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &PhpType::Mixed, + ); +} + +/// Appends the boxed Mixed result value to the indexed array saved on the stack. +#[rustfmt::skip] +fn append_reflection_mixed_array_default_element(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_pop_reg(ctx.emitter, "x9"); + abi::emit_push_reg(ctx.emitter, "x0"); + ctx.emitter.instruction("mov x1, x0"); // pass the boxed Reflection default to the array append helper + ctx.emitter.instruction("mov x0, x9"); // pass the saved default-array pointer to the append helper + abi::emit_call_label(ctx.emitter, "__rt_array_push_refcounted"); + emit_release_pushed_refcounted_temp_after_array_push(ctx.emitter, &PhpType::Mixed); + abi::emit_push_reg(ctx.emitter, "x0"); + } + Arch::X86_64 => { + abi::emit_pop_reg(ctx.emitter, "r11"); + abi::emit_push_reg(ctx.emitter, "rax"); + ctx.emitter.instruction("mov rsi, rax"); // pass the boxed Reflection default to the array append helper + ctx.emitter.instruction("mov rdi, r11"); // pass the saved default-array pointer to the append helper + abi::emit_call_label(ctx.emitter, "__rt_array_push_refcounted"); + emit_release_pushed_refcounted_temp_after_array_push(ctx.emitter, &PhpType::Mixed); + abi::emit_push_reg(ctx.emitter, "rax"); + } } } @@ -4561,21 +4641,8 @@ fn emit_reflection_owner_default_value_property( let object_reg = abi::symbol_scratch_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, result_reg); match default_value { - Some(ReflectionParameterDefaultValue::Int(value)) => { - emit_boxed_int_literal_to_result(ctx, *value) - } - Some(ReflectionParameterDefaultValue::Bool(value)) => { - emit_boxed_bool_literal_to_result(ctx, *value) - } - Some(ReflectionParameterDefaultValue::Float(value)) => { - emit_boxed_float_literal_to_result(ctx, *value) - } - Some(ReflectionParameterDefaultValue::Str(value)) => { - emit_boxed_string_literal_default_to_result(ctx, value) - } - Some(ReflectionParameterDefaultValue::Null) | None => { - emit_boxed_null_literal_to_result(ctx) - } + Some(value) => emit_reflection_default_value_as_mixed(ctx, value), + None => emit_boxed_null_literal_to_result(ctx), } abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, default_offset); diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index db19cf0761..79770da65e 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2518,6 +2518,7 @@ class ReflectPropertyDefaultTarget { public $explicitNull = null; public int $count = 7; public static string $label = "ok"; + public array $items = [2, "b", null]; } $implicit = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "implicit"); echo $implicit->getName() . ":"; @@ -2555,6 +2556,14 @@ echo $label->isDefault() ? "Y:" : "N:"; echo $label->hasDefaultValue() ? "D:" : "d:"; echo $label->getDefaultValue() === null ? "null" : $label->getDefaultValue(); echo "|"; +$items = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "items"); +$itemDefault = $items->getDefaultValue(); +echo $items->getName() . ":"; +echo $items->isDefault() ? "Y:" : "N:"; +echo $items->hasDefaultValue() ? "D:" : "d:"; +echo count($itemDefault) . ":" . $itemDefault[0] . ":" . $itemDefault[1] . ":"; +echo $itemDefault[2] === null ? "null" : "value"; +echo "|"; $listed = (new ReflectionClass(ReflectPropertyDefaultTarget::class))->getProperty("implicit"); echo "listed:"; echo $listed->isDefault() ? "Y:" : "N:"; @@ -2569,7 +2578,7 @@ echo $listed->getDefaultValue() === null ? "null" : "bad"; ); assert_eq!( out.stdout, - "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|listed:Y:D:null" + "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|items:Y:D:3:2:b:null|listed:Y:D:null" ); } @@ -2591,6 +2600,7 @@ class ReflectClassDefaultChild extends ReflectClassDefaultBase { private int $shadow = 9; public static int $childStatic = 7; public $explicitNull = null; + public array $items = [8, "i"]; } $defaults = (new ReflectionClass(ReflectClassDefaultChild::class))->getDefaultProperties(); echo $defaults["childStatic"] . ":"; @@ -2599,6 +2609,9 @@ echo $defaults["child"] . ":"; echo $defaults["shadow"] . ":"; echo $defaults["base"] . ":"; echo $defaults["prot"] . ":"; +echo count($defaults["items"]) . ":"; +echo $defaults["items"][0] . ":"; +echo $defaults["items"][1] . ":"; $implicit = "i"; $explicitNull = "e"; $typed = "t"; @@ -2621,7 +2634,7 @@ echo $implicit . ":" . $explicitNull . ":" . $typed . ":" . count($defaults); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "7:bs:5:9:1:p:I:E:t:8"); + assert_eq!(out.stdout, "7:bs:5:9:1:p:2:8:i:I:E:t:9"); } /// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. @@ -2662,12 +2675,12 @@ echo "direct:" . count($directAttrs) . ":" . $directAttrs[0]->getName() . ":" . ); } -/// Verifies that `ReflectionParameter` exposes supported scalar/null defaults. +/// Verifies that `ReflectionParameter` exposes supported scalar/null/array defaults. #[test] fn test_reflection_parameter_exposes_default_values() { let out = compile_and_run_capture( r##"getParameters(); echo $params[0]->isDefaultValueAvailable() ? "D" : "d"; try { @@ -2685,6 +2698,15 @@ echo "|"; $direct = new ReflectionParameter("reflect_default_function", "label"); echo $direct->isDefaultValueAvailable() ? "D:" : "d:"; echo $direct->getDefaultValue(); +echo "|"; +$items = $params[4]->getDefaultValue(); +echo $params[4]->isDefaultValueAvailable() ? "D:" : "d:"; +echo count($items) . ":" . $items[0] . ":" . $items[1] . ":"; +echo $items[2] === null ? "null" : "value"; +echo ":" . count($items[3]) . ":" . $items[3][0] . ":" . ($items[3][1] ? "T" : "F"); +echo "|"; +$directItems = (new ReflectionParameter("reflect_default_function", "items"))->getDefaultValue(); +echo count($directItems) . ":" . $directItems[1]; "##, ); assert!( @@ -2692,7 +2714,7 @@ echo $direct->getDefaultValue(); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "d:E|D:7|D:null|D:ok"); + assert_eq!(out.stdout, "d:E|D:7|D:null|D:ok|D:4:1:two:null:2:3:F|4:two"); } /// Verifies `ReflectionParameter` exposes class-constant default metadata. From 79543e8fc8f7a2034454ed9e82fc279aae196096 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 06:12:24 +0200 Subject: [PATCH 0523/1208] Support associative array reflection defaults --- docs/php/classes.md | 8 +- src/codegen/lower_inst/objects/reflection.rs | 166 ++++++++++++++++++- tests/codegen/oop/attributes.rs | 59 +++++-- 3 files changed, 211 insertions(+), 22 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 47a35741a6..9a7334a984 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1221,7 +1221,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | -| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, and indexed-array literal default values, or throw `ReflectionException` when no default is available | +| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, and array literal default values, or throw `ReflectionException` when no default is available | | `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | @@ -1321,7 +1321,7 @@ name with `isBuiltin()` false. | `ReflectionParameter::isArray()` / `isCallable()` | `bool` | Legacy named-type predicates for direct `array` / `callable` hints | | `ReflectionParameter::getAttributes()` | `ReflectionAttribute[]` | The reflected method parameter's attributes | | `ReflectionParameter::isDefaultValueAvailable()` | `bool` | True when a supported default value is available | -| `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar, null, class-constant, or indexed-array literal default value, or throws when unavailable | +| `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar, null, class-constant, or array literal default value, or throws when unavailable | | `ReflectionParameter::isDefaultValueConstant()` | `bool` | True when the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | `?string` | The constant name for constant-backed defaults, or `null` | | `ReflectionNamedType::getName()` | `string` | The type name (`int`, `string`, a class name, …) | @@ -1338,7 +1338,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/indexed-array default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as array/object/class-constant parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. @@ -1385,4 +1385,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, scalar/null parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, supported scalar/null/class-constant/array parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 33a1ce3819..c239364137 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -27,7 +27,10 @@ use crate::names::{ enum_case_symbol, php_symbol_key, property_hook_get_method, property_hook_set_method, }; use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, TypeExpr, Visibility}; -use crate::types::{AttrArgEntry, EnumCaseInfo, EnumCaseValue, FunctionSig, InterfaceInfo, PhpType}; +use crate::types::{ + is_php_integer_array_key, AttrArgEntry, EnumCaseInfo, EnumCaseValue, FunctionSig, + InterfaceInfo, PhpType, +}; use super::super::super::context::FunctionContext; @@ -176,6 +179,21 @@ enum ReflectionParameterDefaultValue { Str(String), Null, Array(Vec), + AssocArray(Vec), +} + +/// Metadata for one key/value pair in an associative Reflection default array. +#[derive(Clone)] +struct ReflectionDefaultAssocEntry { + key: ReflectionDefaultArrayKey, + value: ReflectionParameterDefaultValue, +} + +/// Normalized PHP key forms for associative Reflection default arrays. +#[derive(Clone)] +enum ReflectionDefaultArrayKey { + Int(i64), + Str(String), } /// Metadata for one constant entry returned by `ReflectionClass::getConstants()`. @@ -2789,6 +2807,7 @@ fn reflection_literal_parameter_default_value( .map(reflection_literal_parameter_default_value) .collect::>>() .map(ReflectionParameterDefaultValue::Array), + ExprKind::ArrayLiteralAssoc(entries) => reflection_assoc_array_default_value(entries), ExprKind::Negate(inner) => match &inner.kind { ExprKind::IntLiteral(value) => value .checked_neg() @@ -2800,6 +2819,50 @@ fn reflection_literal_parameter_default_value( } } +/// Converts an associative array literal into normalized Reflection default metadata. +fn reflection_assoc_array_default_value( + entries: &[(Expr, Expr)], +) -> Option { + entries + .iter() + .map(|(key, value)| { + let key = reflection_default_array_key(key)?; + let value = reflection_literal_parameter_default_value(value)?; + Some(ReflectionDefaultAssocEntry { key, value }) + }) + .collect::>>() + .map(ReflectionParameterDefaultValue::AssocArray) +} + +/// Converts one supported associative-array key expression into PHP-normalized metadata. +fn reflection_default_array_key(key: &Expr) -> Option { + match &key.kind { + ExprKind::IntLiteral(value) => Some(ReflectionDefaultArrayKey::Int(*value)), + ExprKind::BoolLiteral(value) => Some(ReflectionDefaultArrayKey::Int(i64::from(*value))), + ExprKind::FloatLiteral(value) => Some(ReflectionDefaultArrayKey::Int(*value as i64)), + ExprKind::StringLiteral(value) => reflection_default_string_array_key(value), + ExprKind::Null => Some(ReflectionDefaultArrayKey::Str(String::new())), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value.checked_neg().map(ReflectionDefaultArrayKey::Int), + ExprKind::FloatLiteral(value) => Some(ReflectionDefaultArrayKey::Int((-*value) as i64)), + _ => None, + }, + _ => None, + } +} + +/// Normalizes a string array key according to PHP integer-string key rules. +fn reflection_default_string_array_key(value: &str) -> Option { + if is_php_integer_array_key(value) { + value + .parse::() + .ok() + .map(ReflectionDefaultArrayKey::Int) + } else { + Some(ReflectionDefaultArrayKey::Str(value.to_string())) + } +} + /// Converts scalar/null constant metadata into a parameter default value. fn reflection_parameter_default_from_constant_value( value: ReflectionConstantValue, @@ -4106,6 +4169,9 @@ fn emit_reflection_default_value_as_mixed( ReflectionParameterDefaultValue::Array(elements) => { emit_reflection_indexed_array_default_as_mixed(ctx, elements) } + ReflectionParameterDefaultValue::AssocArray(entries) => { + emit_reflection_assoc_array_default_as_mixed(ctx, entries) + } } } @@ -4178,6 +4244,104 @@ fn append_reflection_mixed_array_default_element(ctx: &mut FunctionContext<'_>) } } +/// Materializes an associative Reflection default array as a boxed Mixed cell. +fn emit_reflection_assoc_array_default_as_mixed( + ctx: &mut FunctionContext<'_>, + entries: &[ReflectionDefaultAssocEntry], +) { + emit_reflection_assoc_array_default_to_result(ctx, entries); + emit_box_current_owned_value_as_mixed( + ctx.emitter, + &PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }, + ); +} + +/// Allocates and populates an associative array whose values are boxed Mixed defaults. +fn emit_reflection_assoc_array_default_to_result( + ctx: &mut FunctionContext<'_>, + entries: &[ReflectionDefaultAssocEntry], +) { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Mixed); + for entry in entries { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_default_value_as_mixed(ctx, &entry.value); + emit_reflection_assoc_array_default_insert(ctx, &entry.key); + } +} + +/// Inserts the current boxed Mixed default value into the stacked associative default array. +#[rustfmt::skip] +fn emit_reflection_assoc_array_default_insert( + ctx: &mut FunctionContext<'_>, + key: &ReflectionDefaultArrayKey, +) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection default as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + emit_reflection_default_array_key_aarch64(ctx, key); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection default as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + emit_reflection_default_array_key_x86_64(ctx, key); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + +/// Materializes an associative default-array key in AArch64 hash-key registers. +fn emit_reflection_default_array_key_aarch64( + ctx: &mut FunctionContext<'_>, + key: &ReflectionDefaultArrayKey, +) { + match key { + ReflectionDefaultArrayKey::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, "x1", *value); + abi::emit_load_int_immediate(ctx.emitter, "x2", -1); + } + ReflectionDefaultArrayKey::Str(value) => { + let (key_label, key_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + } + } +} + +/// Materializes an associative default-array key in x86_64 SysV hash-key registers. +fn emit_reflection_default_array_key_x86_64( + ctx: &mut FunctionContext<'_>, + key: &ReflectionDefaultArrayKey, +) { + match key { + ReflectionDefaultArrayKey::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, "rsi", *value); + abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); + } + ReflectionDefaultArrayKey::Str(value) => { + let (key_label, key_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + } + } +} + /// Inserts the current boxed Mixed constant value into the stacked associative array. fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 79770da65e..5fe6fa3030 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2519,9 +2519,12 @@ class ReflectPropertyDefaultTarget { public int $count = 7; public static string $label = "ok"; public array $items = [2, "b", null]; -} -$implicit = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "implicit"); -echo $implicit->getName() . ":"; + public $assoc = ["name" => "Ada", "1" => "one", false => "zero"]; + } + $obj = new ReflectPropertyDefaultTarget(); + echo "runtime:" . $obj->assoc["name"] . ":" . $obj->assoc[1] . "|"; + $implicit = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "implicit"); + echo $implicit->getName() . ":"; echo $implicit->isDefault() ? "Y:" : "N:"; echo $implicit->hasDefaultValue() ? "D:" : "d:"; echo $implicit->getDefaultValue() === null ? "null" : $implicit->getDefaultValue(); @@ -2564,6 +2567,14 @@ echo $items->hasDefaultValue() ? "D:" : "d:"; echo count($itemDefault) . ":" . $itemDefault[0] . ":" . $itemDefault[1] . ":"; echo $itemDefault[2] === null ? "null" : "value"; echo "|"; +$assoc = new ReflectionProperty(ReflectPropertyDefaultTarget::class, "assoc"); +$assocDefault = $assoc->getDefaultValue(); +echo $assoc->getName() . ":"; +echo $assoc->isDefault() ? "Y:" : "N:"; +echo $assoc->hasDefaultValue() ? "D:" : "d:"; +echo count($assocDefault) . ":" . $assocDefault["name"] . ":" . $assocDefault[1] . ":"; +echo $assocDefault[0]; +echo "|"; $listed = (new ReflectionClass(ReflectPropertyDefaultTarget::class))->getProperty("implicit"); echo "listed:"; echo $listed->isDefault() ? "Y:" : "N:"; @@ -2577,9 +2588,9 @@ echo $listed->getDefaultValue() === null ? "null" : "bad"; out.stdout, out.stderr ); assert_eq!( - out.stdout, - "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|items:Y:D:3:2:b:null|listed:Y:D:null" - ); + out.stdout, + "runtime:Ada:one|implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|items:Y:D:3:2:b:null|assoc:Y:D:3:Ada:one:zero|listed:Y:D:null" + ); } /// Verifies that `ReflectionClass::getDefaultProperties()` exposes supported property defaults. @@ -2596,22 +2607,28 @@ class ReflectClassDefaultBase { public static string $baseStatic = "bs"; } class ReflectClassDefaultChild extends ReflectClassDefaultBase { - public int $child = 5; - private int $shadow = 9; - public static int $childStatic = 7; - public $explicitNull = null; - public array $items = [8, "i"]; + public int $child = 5; + private int $shadow = 9; + public static int $childStatic = 7; + public static $assocStatic = ["s" => "S2", "2" => "two"]; + public $explicitNull = null; + public array $items = [8, "i"]; + public $assoc = ["side" => "S", "4" => "four"]; } $defaults = (new ReflectionClass(ReflectClassDefaultChild::class))->getDefaultProperties(); echo $defaults["childStatic"] . ":"; echo $defaults["baseStatic"] . ":"; echo $defaults["child"] . ":"; echo $defaults["shadow"] . ":"; -echo $defaults["base"] . ":"; -echo $defaults["prot"] . ":"; -echo count($defaults["items"]) . ":"; + echo $defaults["base"] . ":"; + echo $defaults["prot"] . ":"; + echo ReflectClassDefaultChild::$assocStatic["s"] . ":"; + echo count($defaults["items"]) . ":"; echo $defaults["items"][0] . ":"; echo $defaults["items"][1] . ":"; +echo count($defaults["assoc"]) . ":"; +echo $defaults["assoc"]["side"] . ":"; +echo $defaults["assoc"][4] . ":"; $implicit = "i"; $explicitNull = "e"; $typed = "t"; @@ -2634,7 +2651,7 @@ echo $implicit . ":" . $explicitNull . ":" . $typed . ":" . count($defaults); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "7:bs:5:9:1:p:2:8:i:I:E:t:9"); + assert_eq!(out.stdout, "7:bs:5:9:1:p:S2:2:8:i:2:S:four:I:E:t:11"); } /// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. @@ -2680,7 +2697,7 @@ echo "direct:" . count($directAttrs) . ":" . $directAttrs[0]->getName() . ":" . fn test_reflection_parameter_exposes_default_values() { let out = compile_and_run_capture( r##" "Ada", "1" => "one", false => "zero", 3 => ["deep" => 4]]) {} $params = (new ReflectionFunction("reflect_default_function"))->getParameters(); echo $params[0]->isDefaultValueAvailable() ? "D" : "d"; try { @@ -2707,6 +2724,11 @@ echo ":" . count($items[3]) . ":" . $items[3][0] . ":" . ($items[3][1] ? "T" : " echo "|"; $directItems = (new ReflectionParameter("reflect_default_function", "items"))->getDefaultValue(); echo count($directItems) . ":" . $directItems[1]; +echo "|"; +$assoc = $params[5]->getDefaultValue(); +echo $params[5]->isDefaultValueAvailable() ? "D:" : "d:"; +echo count($assoc) . ":" . $assoc["name"] . ":" . $assoc[1] . ":"; +echo $assoc[0] . ":" . $assoc[3]["deep"]; "##, ); assert!( @@ -2714,7 +2736,10 @@ echo count($directItems) . ":" . $directItems[1]; "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "d:E|D:7|D:null|D:ok|D:4:1:two:null:2:3:F|4:two"); + assert_eq!( + out.stdout, + "d:E|D:7|D:null|D:ok|D:4:1:two:null:2:3:F|4:two|D:4:Ada:one:zero:4" + ); } /// Verifies `ReflectionParameter` exposes class-constant default metadata. From 194165dc2f0eeebf0dbe67fbf95a112edad99c3f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 06:33:33 +0200 Subject: [PATCH 0524/1208] Support reflection for function parameter attributes --- docs/php/classes.md | 8 ++-- src/conditional/stmts.rs | 2 + src/ir_lower/program.rs | 1 + src/magic_constants/walker/stmts.rs | 2 + src/name_resolver/declarations.rs | 5 +++ src/optimize/control/dce.rs | 2 + src/optimize/control/fold.rs | 2 + src/optimize/control/prune/statements.rs | 2 + src/optimize/propagate/stmt.rs | 2 + src/parser/ast/stmt.rs | 3 ++ src/parser/stmt/params.rs | 13 ++++-- src/resolver/engine.rs | 12 +++++- src/resolver/stmt_exprs.rs | 2 + src/types/checker/driver/functions.rs | 4 +- src/types/checker/functions/resolution/mod.rs | 2 +- .../checker/functions/resolution/signature.rs | 4 +- src/types/checker/mod.rs | 2 + tests/codegen/oop/attributes.rs | 41 +++++++++++++++++++ tests/parser_tests/attributes.rs | 16 ++++++-- 19 files changed, 108 insertions(+), 17 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 9a7334a984..64b3ab1aa8 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1217,7 +1217,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, a `ReflectionIntersectionType` for intersection parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | | `ReflectionParameter::allowsNull()` | Same as `ReflectionParameter::hasType()` | Return PHP nullability for retained parameter type/default metadata | | `ReflectionParameter::isArray()` / `isCallable()` | Same as `ReflectionParameter::hasType()` | Return PHP's legacy named-type predicates for nullable or non-nullable `array` / `callable` parameter declarations; union declarations report `false` | -| `ReflectionParameter::getAttributes()` | Same construction forms as `ReflectionParameter::hasType()` | Return `ReflectionAttribute` objects for method parameter attributes | +| `ReflectionParameter::getAttributes()` | Same construction forms as `ReflectionParameter::hasType()` | Return `ReflectionAttribute` objects for function and method parameter attributes | | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | @@ -1319,7 +1319,7 @@ name with `isBuiltin()` false. | `ReflectionParameter::getType()` | `?ReflectionNamedType\|ReflectionUnionType\|ReflectionIntersectionType` | The declared type, or `null` when untyped | | `ReflectionParameter::allowsNull()` | `bool` | PHP nullability for the retained parameter metadata | | `ReflectionParameter::isArray()` / `isCallable()` | `bool` | Legacy named-type predicates for direct `array` / `callable` hints | -| `ReflectionParameter::getAttributes()` | `ReflectionAttribute[]` | The reflected method parameter's attributes | +| `ReflectionParameter::getAttributes()` | `ReflectionAttribute[]` | The reflected function or method parameter's attributes | | `ReflectionParameter::isDefaultValueAvailable()` | `bool` | True when a supported default value is available | | `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar, null, class-constant, or array literal default value, or throws when unavailable | | `ReflectionParameter::isDefaultValueConstant()` | `bool` | True when the retained default came from a named constant | @@ -1338,7 +1338,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter default values, top-level function parameter attributes outside eval, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter default values, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. @@ -1385,4 +1385,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, method parameter attributes, supported scalar/null/class-constant/array parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. Per-parameter attributes on top-level functions are not yet reflected. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, function and method parameter attributes, supported scalar/null/class-constant/array parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/src/conditional/stmts.rs b/src/conditional/stmts.rs index cec6a35000..be1a2528e7 100644 --- a/src/conditional/stmts.rs +++ b/src/conditional/stmts.rs @@ -235,6 +235,7 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, @@ -248,6 +249,7 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { (name, type_ann, default.map(|expr| rewrite_expr(expr, defines)), is_ref) }) .collect(), + param_attributes, variadic, variadic_type, return_type, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 07b42c279a..7757e004e6 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -697,6 +697,7 @@ fn lower_function_declarations( variadic_type: _, return_type, body, + .. } => function::lower_user_function( name, params, diff --git a/src/magic_constants/walker/stmts.rs b/src/magic_constants/walker/stmts.rs index 82ae4ef377..06c72a5edf 100644 --- a/src/magic_constants/walker/stmts.rs +++ b/src/magic_constants/walker/stmts.rs @@ -241,6 +241,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, @@ -259,6 +260,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { by_ref_return, name, params: new_params, + param_attributes, variadic, variadic_type, return_type, diff --git a/src/name_resolver/declarations.rs b/src/name_resolver/declarations.rs index fa14c9e949..f7ba579a68 100644 --- a/src/name_resolver/declarations.rs +++ b/src/name_resolver/declarations.rs @@ -40,6 +40,7 @@ pub(super) fn resolve_decl_stmt( by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, @@ -51,6 +52,10 @@ pub(super) fn resolve_decl_stmt( by_ref_return: *by_ref_return, name: canonical_name_for_decl(namespace, name), params: resolve_params(params, namespace, imports, symbols), + param_attributes: param_attributes + .iter() + .map(|groups| resolve_attribute_groups(groups, namespace, imports, symbols)) + .collect(), variadic: variadic.clone(), variadic_type: variadic_type.clone(), return_type: return_type diff --git a/src/optimize/control/dce.rs b/src/optimize/control/dce.rs index 8558911652..cc0b6b6815 100644 --- a/src/optimize/control/dce.rs +++ b/src/optimize/control/dce.rs @@ -496,6 +496,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, @@ -505,6 +506,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, diff --git a/src/optimize/control/fold.rs b/src/optimize/control/fold.rs index 2db6332fa9..69328c2875 100644 --- a/src/optimize/control/fold.rs +++ b/src/optimize/control/fold.rs @@ -173,6 +173,7 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, @@ -181,6 +182,7 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { by_ref_return, name, params: fold_params(params), + param_attributes, variadic, variadic_type, return_type, diff --git a/src/optimize/control/prune/statements.rs b/src/optimize/control/prune/statements.rs index 59943a3b1b..ebaac31e7e 100644 --- a/src/optimize/control/prune/statements.rs +++ b/src/optimize/control/prune/statements.rs @@ -244,6 +244,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, @@ -253,6 +254,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, diff --git a/src/optimize/propagate/stmt.rs b/src/optimize/propagate/stmt.rs index bb646d61b4..0b9d3a4cdc 100644 --- a/src/optimize/propagate/stmt.rs +++ b/src/optimize/propagate/stmt.rs @@ -287,6 +287,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, @@ -297,6 +298,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv by_ref_return, name, params: propagate_params(params), + param_attributes, variadic, variadic_type, return_type, diff --git a/src/parser/ast/stmt.rs b/src/parser/ast/stmt.rs index c080997c16..0e8fee0209 100644 --- a/src/parser/ast/stmt.rs +++ b/src/parser/ast/stmt.rs @@ -175,6 +175,9 @@ pub enum StmtKind { FunctionDecl { name: String, params: Vec<(String, Option, Option, bool)>, + /// PHP 8 attribute groups attached to each function parameter, aligned with `params` + /// plus the variadic parameter when present. + param_attributes: Vec>, variadic: Option, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. Each /// argument collected into the variadic is checked against this type. diff --git a/src/parser/stmt/params.rs b/src/parser/stmt/params.rs index 6dc4d27f1f..0c7f886c41 100644 --- a/src/parser/stmt/params.rs +++ b/src/parser/stmt/params.rs @@ -11,7 +11,7 @@ use crate::errors::CompileError; use crate::lexer::Token; use crate::names::Name; -use crate::parser::ast::{Expr, Stmt, StmtKind, TypeExpr}; +use crate::parser::ast::{AttributeGroup, Expr, Stmt, StmtKind, TypeExpr}; use crate::parser::expr::parse_expr; use crate::span::Span; @@ -45,7 +45,7 @@ pub(super) fn parse_function_decl( &Token::LParen, "Expected '(' after function name", )?; - let (params, variadic, variadic_type) = parse_params(tokens, pos, span)?; + let (params, param_attributes, variadic, variadic_type) = parse_params(tokens, pos, span)?; expect_token(tokens, pos, &Token::RParen, "Expected ')' after parameters")?; // Parse optional return type: `: TypeExpr` @@ -62,6 +62,7 @@ pub(super) fn parse_function_decl( StmtKind::FunctionDecl { name, params, + param_attributes, variadic, variadic_type, return_type, @@ -346,12 +347,14 @@ pub(super) fn parse_params( ) -> Result< ( Vec<(String, Option, Option, bool)>, + Vec>, Option, Option, ), CompileError, > { let mut params = Vec::new(); + let mut param_attributes = Vec::new(); let mut variadic = None; let mut variadic_type = None; while *pos < tokens.len() && tokens[*pos].0 != Token::RParen { @@ -368,7 +371,7 @@ pub(super) fn parse_params( } } // PHP 8.0 parameter attributes (`function f(#[Sensitive] $s)`). - crate::parser::consume_attribute_lists(tokens, pos)?; + let attributes = crate::parser::parse_attribute_lists(tokens, pos)?; if variadic.is_some() { return Err(CompileError::new( span, @@ -395,6 +398,7 @@ pub(super) fn parse_params( Some(Token::Variable(n)) => { variadic = Some(n.clone()); variadic_type = type_ann; + param_attributes.push(attributes); *pos += 1; } _ => return Err(CompileError::new(span, "Expected variable after '...'")), @@ -411,12 +415,13 @@ pub(super) fn parse_params( } else { None }; + param_attributes.push(attributes); params.push((n, type_ann, default, is_ref)); } _ => return Err(CompileError::new(span, "Expected parameter variable")), } } - Ok((params, variadic, variadic_type)) + Ok((params, param_attributes, variadic, variadic_type)) } /// Parses a comma-separated list of `Name`s until a token that does not start a name is diff --git a/src/resolver/engine.rs b/src/resolver/engine.rs index 67670e1728..54f57fc39c 100644 --- a/src/resolver/engine.rs +++ b/src/resolver/engine.rs @@ -398,7 +398,16 @@ pub(super) fn resolve_stmts( stmt.span, )); } - StmtKind::FunctionDecl { name, params, variadic, variadic_type, return_type, by_ref_return, body } => { + StmtKind::FunctionDecl { + name, + params, + param_attributes, + variadic, + variadic_type, + return_type, + by_ref_return, + body, + } => { let body = resolve_isolated( body.clone(), base_dir, @@ -411,6 +420,7 @@ pub(super) fn resolve_stmts( StmtKind::FunctionDecl { name: name.clone(), params: params.clone(), + param_attributes: param_attributes.clone(), variadic: variadic.clone(), variadic_type: variadic_type.clone(), return_type: return_type.clone(), diff --git a/src/resolver/stmt_exprs.rs b/src/resolver/stmt_exprs.rs index 4d9cc5f405..a9e1fc7e53 100644 --- a/src/resolver/stmt_exprs.rs +++ b/src/resolver/stmt_exprs.rs @@ -372,6 +372,7 @@ pub(super) fn resolve_stmt_exprs( by_ref_return, name, params, + param_attributes, variadic, variadic_type, return_type, @@ -387,6 +388,7 @@ pub(super) fn resolve_stmt_exprs( state, function_variants, )?, + param_attributes, variadic, variadic_type, return_type, diff --git a/src/types/checker/driver/functions.rs b/src/types/checker/driver/functions.rs index 0da9193c0f..9e7a5714b0 100644 --- a/src/types/checker/driver/functions.rs +++ b/src/types/checker/driver/functions.rs @@ -53,6 +53,7 @@ impl Checker { if let StmtKind::FunctionDecl { name, params, + param_attributes, variadic, variadic_type, return_type, @@ -89,6 +90,7 @@ impl Checker { FnDecl { params: param_names, param_types: param_type_anns, + param_attributes: param_attributes.clone(), defaults, ref_params: ref_flags, variadic: variadic.clone(), @@ -276,7 +278,7 @@ impl Checker { .cloned() .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) .collect(), - param_attributes: Vec::new(), + param_attributes: decl.param_attributes.clone(), defaults: decl.defaults, return_type: crate::types::PhpType::Int, declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index d44acb2be3..1f254cbfb4 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -197,7 +197,7 @@ impl Checker { .cloned() .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) .collect(), - param_attributes: Vec::new(), + param_attributes: decl.param_attributes.clone(), defaults: decl.defaults.clone(), return_type: PhpType::Int, declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/functions/resolution/signature.rs b/src/types/checker/functions/resolution/signature.rs index 555b4472fb..1594fefe7b 100644 --- a/src/types/checker/functions/resolution/signature.rs +++ b/src/types/checker/functions/resolution/signature.rs @@ -98,7 +98,7 @@ impl Checker { .cloned() .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) .collect(), - param_attributes: Vec::new(), + param_attributes: decl.param_attributes.clone(), defaults: decl.defaults.clone(), return_type: PhpType::Int, declared_return: decl.return_type.is_some(), @@ -242,7 +242,7 @@ impl Checker { .cloned() .chain(decl.variadic.iter().map(|_| decl.variadic_type.clone())) .collect(), - param_attributes: Vec::new(), + param_attributes: decl.param_attributes.clone(), defaults: decl.defaults.clone(), return_type: return_type.clone(), declared_return: decl.return_type.is_some(), diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index db50e94126..9b61dfe047 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -189,6 +189,8 @@ pub(crate) struct Checker { pub(crate) struct FnDecl { pub params: Vec, pub param_types: Vec>, + /// Attribute groups aligned with the declared parameters plus the variadic parameter, if any. + pub param_attributes: Vec>, pub defaults: Vec>, pub ref_params: Vec, pub variadic: Option, diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 5fe6fa3030..1cf52da217 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2692,6 +2692,47 @@ echo "direct:" . count($directAttrs) . ":" . $directAttrs[0]->getName() . ":" . ); } +/// Verifies `ReflectionFunction` and direct `ReflectionParameter` expose attributes on +/// top-level function parameters. +#[test] +fn test_reflection_function_parameter_get_attributes_returns_parameter_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $attrs = $param->getAttributes(); + echo $param->getName() . ":" . count($attrs); + if (count($attrs) > 0) { + echo ":" . $attrs[0]->getName(); + echo ":" . $attrs[0]->getArguments()[0]; + } + echo "|"; +} +$direct = new ReflectionParameter("reflect_function_param_attrs", "name"); +$directAttrs = $direct->getAttributes(); +echo "direct:" . count($directAttrs) . ":" . $directAttrs[0]->getName() . ":" . $directAttrs[0]->getArguments()[0]; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:1:ReflectFunctionParamTag:id|name:1:ReflectFunctionParamTag:name|plain:0|direct:1:ReflectFunctionParamTag:name" + ); +} + /// Verifies that `ReflectionParameter` exposes supported scalar/null/array defaults. #[test] fn test_reflection_parameter_exposes_default_values() { diff --git a/tests/parser_tests/attributes.rs b/tests/parser_tests/attributes.rs index 749cf27683..286ee94232 100644 --- a/tests/parser_tests/attributes.rs +++ b/tests/parser_tests/attributes.rs @@ -128,11 +128,19 @@ fn test_qualified_attribute_name_parses() { /// Verifies attribute on function parameter. #[test] fn test_attribute_on_function_parameter() { - // PHP 8 allows `#[Sensitive]` immediately before a function parameter. - // The attribute parses without error and does not alter the AST. let with_attr = parse_source(" { + assert_eq!(param_attributes.len(), 1); + assert_eq!( + param_attributes[0][0].attributes[0].name.as_str(), + "Sensitive" + ); + } + other => panic!("expected FunctionDecl, got {:?}", other), + } } /// Verifies attribute on method parameter. From c75317195ae9016c05b748573ef631bf284cf9c5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 06:48:28 +0200 Subject: [PATCH 0525/1208] Support object defaults in ReflectionParameter --- docs/php/classes.md | 8 ++-- src/codegen/lower_inst/objects/reflection.rs | 27 ++++++++++++ src/types/checker/builtin_types/reflection.rs | 32 ++++++++++++++ tests/codegen/oop/attributes.rs | 44 +++++++++++++++++++ 4 files changed, 107 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 64b3ab1aa8..fb6ed30ea9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1221,7 +1221,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | -| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, and array literal default values, or throw `ReflectionException` when no default is available | +| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and zero-argument object default values, or throw `ReflectionException` when no default is available | | `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | @@ -1321,7 +1321,7 @@ name with `isBuiltin()` false. | `ReflectionParameter::isArray()` / `isCallable()` | `bool` | Legacy named-type predicates for direct `array` / `callable` hints | | `ReflectionParameter::getAttributes()` | `ReflectionAttribute[]` | The reflected function or method parameter's attributes | | `ReflectionParameter::isDefaultValueAvailable()` | `bool` | True when a supported default value is available | -| `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar, null, class-constant, or array literal default value, or throws when unavailable | +| `ReflectionParameter::getDefaultValue()` | `mixed` | The scalar, null, class-constant, array literal, or zero-argument object default value, or throws when unavailable | | `ReflectionParameter::isDefaultValueConstant()` | `bool` | True when the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | `?string` | The constant name for constant-backed defaults, or `null` | | `ReflectionNamedType::getName()` | `string` | The type name (`int`, `string`, a class name, …) | @@ -1338,7 +1338,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter default values, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. @@ -1385,4 +1385,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, function and method parameter attributes, supported scalar/null/class-constant/array parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, function and method parameter attributes, supported scalar/null/class-constant/array/zero-argument-object parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index c239364137..28f1e3ce18 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -178,6 +178,9 @@ enum ReflectionParameterDefaultValue { Float(f64), Str(String), Null, + Object { + class_name: String, + }, Array(Vec), AssocArray(Vec), } @@ -2802,6 +2805,11 @@ fn reflection_literal_parameter_default_value( ExprKind::FloatLiteral(value) => Some(ReflectionParameterDefaultValue::Float(*value)), ExprKind::StringLiteral(value) => Some(ReflectionParameterDefaultValue::Str(value.clone())), ExprKind::Null => Some(ReflectionParameterDefaultValue::Null), + ExprKind::NewObject { class_name, args } if args.is_empty() => { + Some(ReflectionParameterDefaultValue::Object { + class_name: class_name.as_str().to_string(), + }) + } ExprKind::ArrayLiteral(items) => items .iter() .map(reflection_literal_parameter_default_value) @@ -4166,6 +4174,7 @@ fn emit_reflection_default_value_as_mixed( emit_boxed_string_literal_default_to_result(ctx, value) } ReflectionParameterDefaultValue::Null => emit_boxed_null_literal_to_result(ctx), + ReflectionParameterDefaultValue::Object { .. } => emit_boxed_null_literal_to_result(ctx), ReflectionParameterDefaultValue::Array(elements) => { emit_reflection_indexed_array_default_as_mixed(ctx, elements) } @@ -4523,6 +4532,8 @@ fn emit_reflection_parameter_properties( let name_offset = reflection_property_offset(class_info, "__name")?; let default_value_constant_name_offset = reflection_property_offset(class_info, "__default_value_constant_name")?; + let default_value_object_class_offset = + reflection_property_offset(class_info, "__default_value_object_class")?; emit_reflection_string_property(ctx, ¶meter.name, name_offset, name_offset + 8); emit_reflection_attrs_property( ctx, @@ -4606,12 +4617,28 @@ fn emit_reflection_parameter_properties( default_value_constant_name_offset, default_value_constant_name_offset + 8, ); + emit_reflection_string_property( + ctx, + reflection_parameter_default_object_class(parameter.default_value.as_ref()).unwrap_or(""), + default_value_object_class_offset, + default_value_object_class_offset + 8, + ); emit_reflection_parameter_default_property(ctx, parameter)?; emit_reflection_parameter_declaring_class_property(ctx, parameter)?; emit_reflection_parameter_declaring_function_property(ctx, parameter)?; Ok(()) } +/// Returns the class name for object parameter defaults that are materialized lazily. +fn reflection_parameter_default_object_class( + default_value: Option<&ReflectionParameterDefaultValue>, +) -> Option<&str> { + match default_value { + Some(ReflectionParameterDefaultValue::Object { class_name }) => Some(class_name), + _ => None, + } +} + /// Writes one ReflectionParameter object's declaring-function slot. fn emit_reflection_parameter_declaring_function_property( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 3e0bb3cd30..07c41cd637 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -665,6 +665,12 @@ fn builtin_reflection_parameter() -> FlattenedClass { Some(mixed_type()), null_lit(), ), + builtin_property( + "__default_value_object_class", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), builtin_property( "__declaring_class", Visibility::Private, @@ -759,6 +765,32 @@ fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { }, dummy_span, ), + Stmt::new( + StmtKind::If { + condition: binary_expr( + reflection_this_property("__default_value_object_class", dummy_span), + BinOp::NotEq, + string_lit("", dummy_span), + dummy_span, + ), + then_body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::NewDynamic { + name_expr: Box::new(reflection_this_property( + "__default_value_object_class", + dummy_span, + )), + args: Vec::new(), + }, + dummy_span, + ))), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), Stmt::new( StmtKind::Return(Some(reflection_this_property( "__default_value", diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 1cf52da217..ebc8cd33cb 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2783,6 +2783,50 @@ echo $assoc[0] . ":" . $assoc[3]["deep"]; ); } +/// Verifies `ReflectionParameter::getDefaultValue()` materializes object defaults lazily. +#[test] +fn test_reflection_parameter_exposes_object_default_values() { + let out = compile_and_run_capture( + r##"label = "ctor"; + } +} +function reflect_object_default(ReflectObjectDefaultValue $value = new ReflectObjectDefaultValue()) {} +class ReflectObjectDefaultMethod { + public function run(ReflectObjectDefaultValue $value = new ReflectObjectDefaultValue()) {} +} +$param = (new ReflectionFunction("reflect_object_default"))->getParameters()[0]; +echo $param->isDefaultValueAvailable() ? "D:" : "d:"; +$first = $param->getDefaultValue(); +$second = $param->getDefaultValue(); +if ($first instanceof ReflectObjectDefaultValue) { + echo "object:" . $first->label . ":"; +} else { + echo "not-object:"; +} +echo $first === $second ? "same" : "diff"; +$direct = (new ReflectionParameter("reflect_object_default", "value"))->getDefaultValue(); +echo $direct instanceof ReflectObjectDefaultValue ? ":direct:" . $direct->label : ":direct:bad"; +$method = (new ReflectionMethod(ReflectObjectDefaultMethod::class, "run"))->getParameters()[0]->getDefaultValue(); +echo $method instanceof ReflectObjectDefaultValue ? ":method:" . $method->label : ":method:bad"; +$directMethod = (new ReflectionParameter([ReflectObjectDefaultMethod::class, "run"], "value"))->getDefaultValue(); +echo $directMethod instanceof ReflectObjectDefaultValue ? ":direct-method:" . $directMethod->label : ":direct-method:bad"; +"##, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "D:object:ctor:diff:direct:ctor:method:ctor:direct-method:ctor" + ); +} + /// Verifies `ReflectionParameter` exposes class-constant default metadata. #[test] fn test_reflection_parameter_exposes_default_constant_metadata() { From 3e68d74a6ac1523b64b34d67870199a8d71de3b3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 07:03:18 +0200 Subject: [PATCH 0526/1208] Support ReflectionClass newInstanceArgs --- src/ir_lower/expr/mod.rs | 105 ++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 41 +++++++ tests/codegen/oop/attributes.rs | 36 ++++++ 3 files changed, 182 insertions(+) diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 40489dbfed..efc0ac16c3 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9911,6 +9911,11 @@ fn lower_method_call( if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { return lower_reflection_class_new_instance(ctx, Some(object_expr), object, args, expr); } + if op == Op::MethodCall + && is_reflection_class_new_instance_args_call(ctx, object.value, method) + { + return lower_reflection_class_new_instance_args(ctx, Some(object_expr), object, args, expr); + } if op == Op::MethodCall && is_reflection_class_new_instance_without_constructor_call(ctx, object.value, method) { @@ -10141,6 +10146,20 @@ fn lower_reflection_class_new_instance( ) } +/// Lowers `ReflectionClass::newInstanceArgs()` by unpacking one static argument array. +fn lower_reflection_class_new_instance_args( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + object: LoweredValue, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let Some(forwarded_args) = reflection_class_new_instance_args_array(args) else { + return lower_reflection_class_new_instance_args_unsupported(ctx, expr); + }; + lower_reflection_class_new_instance(ctx, object_expr, object, &forwarded_args, expr) +} + /// Lowers `ReflectionClass::newInstanceWithoutConstructor()` to constructorless allocation. fn lower_reflection_class_new_instance_without_constructor( ctx: &mut LoweringContext<'_, '_>, @@ -10170,6 +10189,58 @@ fn reflection_class_new_instance_args(args: &[Expr]) -> Vec { args.to_vec() } +/// Returns constructor arguments carried by a static `newInstanceArgs()` array argument. +fn reflection_class_new_instance_args_array(args: &[Expr]) -> Option> { + let args = reflection_class_new_instance_args(args); + match args.as_slice() { + [] => Some(Vec::new()), + [arg] => reflection_class_new_instance_args_value(arg), + _ => None, + } +} + +/// Extracts the actual array value passed to the `newInstanceArgs()` `$args` parameter. +fn reflection_class_new_instance_args_value(arg: &Expr) -> Option> { + let array_expr = match &arg.kind { + ExprKind::NamedArg { name, value } if php_symbol_key(name) == "args" => value.as_ref(), + ExprKind::NamedArg { .. } => return None, + _ => arg, + }; + match &array_expr.kind { + ExprKind::ArrayLiteral(items) => Some(items.clone()), + ExprKind::ArrayLiteralAssoc(entries) => reflection_class_new_instance_assoc_args(entries), + _ => None, + } +} + +/// Converts a static associative argument array into positional and named call arguments. +fn reflection_class_new_instance_assoc_args(entries: &[(Expr, Expr)]) -> Option> { + entries + .iter() + .map(|(key, value)| reflection_class_new_instance_assoc_arg(key, value)) + .collect() +} + +/// Converts one `newInstanceArgs()` associative-array element into a constructor argument. +fn reflection_class_new_instance_assoc_arg(key: &Expr, value: &Expr) -> Option { + match &key.kind { + ExprKind::IntLiteral(_) | ExprKind::BoolLiteral(_) | ExprKind::FloatLiteral(_) => { + Some(value.clone()) + } + ExprKind::StringLiteral(name) if crate::types::is_php_integer_array_key(name) => { + Some(value.clone()) + } + ExprKind::StringLiteral(name) => Some(Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(value.clone()), + }, + value.span, + )), + _ => None, + } +} + /// Returns the reflected constructor signature when the ReflectionClass receiver /// is an inline `new ReflectionClass(Known::class)` expression. fn reflection_class_new_instance_constructor_signature<'a>( @@ -10276,6 +10347,19 @@ fn lower_reflection_class_new_instance_unsupported( result } +/// Emits a runtime fatal for unsupported `newInstanceArgs()` argument-array forms. +fn lower_reflection_class_new_instance_args_unsupported( + ctx: &mut LoweringContext<'_, '_>, + expr: &Expr, +) -> LoweredValue { + let result = lower_boxed_null(ctx, expr); + let message = ctx.intern_string( + "Fatal error: unsupported ReflectionClass::newInstanceArgs() argument array\n", + ); + ctx.builder.terminate(Terminator::Fatal { message }); + result +} + /// Emits a runtime fatal for unsupported `newInstanceWithoutConstructor()` argument forms. fn lower_reflection_class_new_instance_without_constructor_unsupported( ctx: &mut LoweringContext<'_, '_>, @@ -10305,6 +10389,22 @@ fn is_reflection_class_new_instance_call( php_symbol_key(class_name.trim_start_matches('\\')) == "reflectionclass" } +/// Returns true when a method call targets `ReflectionClass::newInstanceArgs()`. +fn is_reflection_class_new_instance_args_call( + ctx: &LoweringContext<'_, '_>, + object: ValueId, + method: &str, +) -> bool { + if php_symbol_key(method) != "newinstanceargs" { + return false; + } + let object_ty = ctx.builder.value_php_type(object); + let Some((class_name, false)) = singular_object_class(&object_ty) else { + return false; + }; + php_symbol_key(class_name.trim_start_matches('\\')) == "reflectionclass" +} + /// Returns true when a method call targets `ReflectionClass::newInstanceWithoutConstructor()`. fn is_reflection_class_new_instance_without_constructor_call( ctx: &LoweringContext<'_, '_>, @@ -10423,6 +10523,11 @@ fn lower_method_call_with_receiver( if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { return lower_reflection_class_new_instance(ctx, None, object, args, expr); } + if op == Op::MethodCall + && is_reflection_class_new_instance_args_call(ctx, object.value, method) + { + return lower_reflection_class_new_instance_args(ctx, None, object, args, expr); + } if op == Op::MethodCall && is_reflection_class_new_instance_without_constructor_call(ctx, object.value, method) { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 07c41cd637..c55a7b8eb2 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -466,6 +466,40 @@ fn builtin_reflection_class_new_instance_method() -> ClassMethod { } } +/// Returns a public `ReflectionClass::newInstanceArgs()` method. +/// +/// Direct calls are lowered specially so the provided argument array becomes +/// constructor arguments for the reflected class. The placeholder body keeps +/// the synthetic class metadata coherent for non-special paths. +fn builtin_reflection_class_new_instance_args_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "newInstanceArgs".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![( + "args".to_string(), + Some(array_type()), + empty_array(), + false, + )], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + by_ref_return: false, + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public no-op method that returns the private `property` slot typed /// `return_type`. Reflection getters are populated at codegen; their bodies just /// surface the corresponding private slot. @@ -1393,6 +1427,7 @@ fn builtin_reflection_class() -> FlattenedClass { false, ), builtin_reflection_class_new_instance_method(), + builtin_reflection_class_new_instance_args_method(), builtin_reflection_class_new_instance_without_constructor_method(), builtin_reflection_owner_get_attributes_method(), ], @@ -3307,6 +3342,12 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.declared_params.push(false); } } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("newInstanceArgs")) + { + sig.return_type = PhpType::Mixed; + } if let Some(sig) = class_info .methods .get_mut(&php_symbol_key("newInstanceWithoutConstructor")) diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index ebc8cd33cb..32d0ceb759 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -3182,6 +3182,42 @@ echo $second->label(); assert_eq!(out, "AB:CD"); } +/// Verifies that `ReflectionClass::newInstanceArgs()` constructs reflected +/// classes from static positional and named argument arrays. +#[test] +fn test_reflection_class_new_instance_args_constructs_reflected_class() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} +class ReflectEmptyNewArgsTarget { + public function label(): string { + return "empty"; + } +} +$ref = new ReflectionClass(ReflectNewArgsTarget::class); +$first = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs(["right" => "Y", "left" => "X"]); +echo $first->label() . ":"; +$second = $ref->newInstanceArgs(["Q", "R"]); +echo $second->label() . ":"; +$third = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs(args: ["left" => "L"]); +echo $third->label() . ":"; +$fourth = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs(...[["left" => "M", "right" => "N"]]); +echo $fourth->label() . ":"; +$empty = (new ReflectionClass(ReflectEmptyNewArgsTarget::class))->newInstanceArgs(); +echo $empty->label(); +"#, + ); + assert_eq!(out, "XY:QR:LB:MN:empty"); +} + /// Verifies that `ReflectionClass::newInstance()` accepts zero constructor /// arguments for classes with no-argument or absent constructors. #[test] From a0ac6c15fb7e39043e41d2892c9ff184e8c79d49 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 07:24:03 +0200 Subject: [PATCH 0527/1208] Support ReflectionClass static property values --- docs/php/classes.md | 4 + src/ir_lower/expr/mod.rs | 188 ++++++++++++++++++++++++++++++++ tests/codegen/oop/attributes.rs | 25 +++++ 3 files changed, 217 insertions(+) diff --git a/docs/php/classes.md b/docs/php/classes.md index fb6ed30ea9..ea2cddf4c9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1169,6 +1169,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getConstant()` | `new ReflectionClass($class_name)` | Return the reflected constant value, enum case object, or `false` when no such constant is visible | | `ReflectionClass::getConstants()` | `new ReflectionClass($class_name)` | Return an associative array of visible constant values keyed by constant or enum-case name | | `ReflectionClass::getDefaultProperties()` | `new ReflectionClass($class_name)` | Return an associative array of supported materialized instance/static property defaults keyed by property name | +| `ReflectionClass::getStaticProperties()` | `new ReflectionClass($class_name)` | Return an associative array of supported materialized static property values keyed by property name | +| `ReflectionClass::getStaticPropertyValue()` | `new ReflectionClass($class_name)` | Return a supported static property value, or the explicit default when the property is missing | +| `ReflectionClass::setStaticPropertyValue()` | `new ReflectionClass($class_name)` | Write a supported static property value | | `ReflectionClass::getReflectionConstant()` | `new ReflectionClass($class_name)` | Return a `ReflectionClassConstant` object for the named visible constant or enum case, or `false` when no such constant is visible | | `ReflectionClass::getReflectionConstants()` | `new ReflectionClass($class_name)` | Return indexed `ReflectionClassConstant` objects for visible class constants and enum cases | | `ReflectionClass::implementsInterface()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol implements, extends, or is the requested interface; interface lookup is case-insensitive and invalid names throw `ReflectionException` | @@ -1339,6 +1342,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, `getStaticPropertyValue()` and `setStaticPropertyValue()` are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression and the property name is a literal string; missing literal properties can return the explicit default argument. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index efc0ac16c3..b095941621 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9921,6 +9921,17 @@ fn lower_method_call( { return lower_reflection_class_new_instance_without_constructor(ctx, object, args, expr); } + if op == Op::MethodCall { + if let Some(value) = lower_reflection_class_static_property_value_call( + ctx, + Some(object_expr), + method, + args, + expr, + ) { + return value; + } + } if matches!( ctx.builder.value_php_type(object.value).codegen_repr(), PhpType::Callable @@ -10181,6 +10192,183 @@ fn lower_reflection_class_new_instance_without_constructor( ) } +/// Lowers live static-property value access for statically-known `ReflectionClass` calls. +fn lower_reflection_class_static_property_value_call( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let class_name = reflection_class_new_instance_reflected_class(ctx, object_expr?)?; + match php_symbol_key(method).as_str() { + "getstaticpropertyvalue" => { + lower_reflection_class_get_static_property_value(ctx, &class_name, args, expr) + } + "setstaticpropertyvalue" => { + lower_reflection_class_set_static_property_value(ctx, &class_name, args, expr) + } + _ => None, + } +} + +/// Lowers `ReflectionClass::getStaticPropertyValue()` to a live static-property read. +fn lower_reflection_class_get_static_property_value( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let (property, default) = reflection_class_get_static_property_value_args(args)?; + if let Some(property_ty) = reflection_class_static_property_type(ctx, class_name, &property) { + if default.is_none() { + return Some(lower_static_property_get_by_class_name( + ctx, + class_name, + &property, + property_ty, + expr, + )); + } + return None; + } + default.map(|default| lower_expr(ctx, &default)) +} + +/// Lowers `ReflectionClass::setStaticPropertyValue()` to a live static-property write. +fn lower_reflection_class_set_static_property_value( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let (property, value) = reflection_class_set_static_property_value_args(args)?; + reflection_class_static_property_type(ctx, class_name, &property)?; + let value = lower_expr(ctx, &value); + store_static_property_by_class_name(ctx, class_name, &property, value.value, expr.span); + Some(lower_null(ctx, expr)) +} + +/// Returns the literal property name and optional explicit default argument for a get call. +fn reflection_class_get_static_property_value_args( + args: &[Expr], +) -> Option<(String, Option)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (name, default) = + reflection_class_static_property_regular_args(&args, "name", Some("default"))?; + let property = reflection_class_static_property_name_arg(name.as_ref()?)?; + Some((property, default)) +} + +/// Returns the literal property name and value expression for a set call. +fn reflection_class_set_static_property_value_args( + args: &[Expr], +) -> Option<(String, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (name, value) = + reflection_class_static_property_regular_args(&args, "name", Some("value"))?; + let property = reflection_class_static_property_name_arg(name.as_ref()?)?; + let value = value?; + Some((property, value)) +} + +/// Normalizes supported static-property method arguments into parameter order. +fn reflection_class_static_property_regular_args( + args: &[Expr], + first_name: &str, + second_name: Option<&str>, +) -> Option<(Option, Option)> { + if !crate::types::call_args::has_named_args(args) { + return match args { + [first] => Some((Some(first.clone()), None)), + [first, second] => Some((Some(first.clone()), Some(second.clone()))), + _ => None, + }; + } + + let mut first = None; + let mut second = None; + for arg in args { + match &arg.kind { + ExprKind::NamedArg { name, value } if php_symbol_key(name) == first_name => { + first = Some((**value).clone()); + } + ExprKind::NamedArg { name, value } + if second_name.is_some_and(|expected| php_symbol_key(name) == expected) => + { + second = Some((**value).clone()); + } + _ => return None, + } + } + Some((first, second)) +} + +/// Extracts a literal property name from a ReflectionClass static-property call argument. +fn reflection_class_static_property_name_arg(arg: &Expr) -> Option { + match &arg.kind { + ExprKind::StringLiteral(name) => Some(name.clone()), + _ => None, + } +} + +/// Returns the retained PHP type for one static property on a reflected class. +fn reflection_class_static_property_type( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + property: &str, +) -> Option { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + class_info + .static_properties + .iter() + .find(|(name, _)| name == property) + .map(|(_, property_ty)| normalize_value_php_type(property_ty.codegen_repr())) +} + +/// Emits a live static-property read from a known reflected class and property name. +fn lower_static_property_get_by_class_name( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + result_type: PhpType, + expr: &Expr, +) -> LoweredValue { + let data = ctx.intern_string(&format!("{}::{}", class_name, property)); + ctx.emit_value( + Op::LoadStaticProperty, + Vec::new(), + Some(Immediate::Data(data)), + result_type, + Op::LoadStaticProperty.default_effects(), + Some(expr.span), + ) +} + +/// Emits a live static-property write for a known reflected class and property name. +fn store_static_property_by_class_name( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + value: ValueId, + span: Span, +) { + let data = ctx.intern_string(&format!("{}::{}", class_name, property)); + ctx.emit_void( + Op::StoreStaticProperty, + vec![value], + Some(Immediate::Data(data)), + Op::StoreStaticProperty.default_effects(), + Some(span), + ); +} + /// Returns the source arguments that can be forwarded to `new $class(...)`. fn reflection_class_new_instance_args(args: &[Expr]) -> Vec { if has_static_call_spread_args(args) { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 32d0ceb759..cd5756325d 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2654,6 +2654,31 @@ echo $implicit . ":" . $explicitNull . ":" . $typed . ":" . count($defaults); assert_eq!(out.stdout, "7:bs:5:9:1:p:S2:2:8:i:2:S:four:I:E:t:11"); } +/// Verifies `ReflectionClass::getStaticPropertyValue()` and +/// `setStaticPropertyValue()` use live static-property storage for known classes. +#[test] +fn test_reflection_class_static_property_value_accesses_live_storage() { + let out = compile_and_run_capture( + r#"getStaticPropertyValue("count"); +ReflectStaticValueTarget::$count = 9; +echo ":" . (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticPropertyValue("count"); +(new ReflectionClass(ReflectStaticValueTarget::class))->setStaticPropertyValue("count", 11); +echo ":" . ReflectStaticValueTarget::$count; +echo ":" . (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticPropertyValue("missing", "fallback"); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:9:11:fallback"); +} + /// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. #[test] fn test_reflection_parameter_get_attributes_returns_parameter_metadata() { From d0ba57a2e604b0d6f3f99b21fcac40d9671cf625 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 07:34:43 +0200 Subject: [PATCH 0528/1208] Track local ReflectionClass metadata --- docs/php/classes.md | 2 +- src/ir_lower/context.rs | 25 +++++++++++++++++++++++++ src/ir_lower/expr/mod.rs | 29 +++++++++++++++++++++++++++-- src/ir_lower/stmt/mod.rs | 12 ++++++++++-- tests/codegen/oop/attributes.rs | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+), 5 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index ea2cddf4c9..8e36390d69 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1342,7 +1342,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, `getStaticPropertyValue()` and `setStaticPropertyValue()` are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression and the property name is a literal string; missing literal properties can return the explicit default argument. Fully dynamic class/property lookup still requires richer runtime metadata. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, `getStaticPropertyValue()` and `setStaticPropertyValue()` are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one, and the property name is a literal string; missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 4cc60363d5..7a9edc3e27 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -120,6 +120,7 @@ pub(crate) struct LoweringContext<'m, 'f> { pub loop_stack: Vec, pub finally_stack: Vec, static_callable_locals: HashMap, + reflection_class_locals: HashMap, fiber_start_sigs: HashMap, ref_bound_locals: HashSet, ref_cell_owner_locals: HashMap, @@ -194,6 +195,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { loop_stack: Vec::new(), finally_stack: Vec::new(), static_callable_locals: HashMap::new(), + reflection_class_locals: HashMap::new(), fiber_start_sigs: HashMap::new(), ref_bound_locals: HashSet::new(), ref_cell_owner_locals: HashMap::new(), @@ -697,6 +699,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Emits a store to a PHP local slot, updates type facts, and returns the stored value. pub(crate) fn store_local(&mut self, name: &str, value: LoweredValue, php_type: PhpType, span: Option) -> LoweredValue { self.clear_static_callable_local(name); + self.clear_reflection_class_local(name); self.clear_fiber_start_sig(name); if let Some(extern_type) = self.extern_global_type(name) { let release_source_after_store = self.value_is_owning_temporary(value); @@ -917,6 +920,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { span: Option, ) -> LoweredValue { self.clear_static_callable_local(name); + self.clear_reflection_class_local(name); self.clear_fiber_start_sig(name); let previous_kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, previous_kind); @@ -948,6 +952,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { return self.store_local(name, null, PhpType::Void, span); } self.clear_static_callable_local(name); + self.clear_reflection_class_local(name); self.clear_fiber_start_sig(name); let slot = self.declare_local(name, PhpType::Void); self.release_ref_cell_owner(name, span); @@ -1014,6 +1019,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.promote_local_ref_cell(source, span); } self.clear_static_callable_local(target); + self.clear_reflection_class_local(target); self.clear_fiber_start_sig(target); self.release_replaced_local_before_ref_alias(target, span); let source_slot = self.declare_local(source, source_ty.clone()); @@ -1289,6 +1295,19 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.static_callable_locals.get(name).cloned() } + /// Records that a PHP local currently holds a statically-known `ReflectionClass` object. + pub(crate) fn bind_reflection_class_local(&mut self, name: &str, reflected_class: String) { + if self.can_track_static_callable_local(name) { + self.reflection_class_locals + .insert(name.to_string(), reflected_class); + } + } + + /// Returns the reflected class associated with a local `ReflectionClass`, if known. + pub(crate) fn reflection_class_local(&self, name: &str) -> Option { + self.reflection_class_locals.get(name).cloned() + } + /// Records that a PHP local currently holds a Fiber with a known callback signature. pub(crate) fn bind_fiber_start_sig(&mut self, name: &str, sig: FunctionSig) { if self.can_track_static_callable_local(name) { @@ -1317,6 +1336,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.static_callable_locals.remove(name); } + /// Clears the compile-time `ReflectionClass` association for one local. + pub(crate) fn clear_reflection_class_local(&mut self, name: &str) { + self.reflection_class_locals.remove(name); + } + /// Clears the known Fiber callback association for one local. pub(crate) fn clear_fiber_start_sig(&mut self, name: &str) { self.fiber_start_sigs.remove(name); @@ -1325,6 +1349,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Clears all compile-time callable associations after a control-flow join. pub(crate) fn clear_static_callable_locals(&mut self) { self.static_callable_locals.clear(); + self.reflection_class_locals.clear(); self.fiber_start_sigs.clear(); } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index b095941621..24acc5660d 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1511,6 +1511,7 @@ fn lower_assignment_expr( } } let static_callable = assigned_name.and_then(|_| static_callable_binding_for_expr(ctx, value)); + let reflected_class = assigned_name.and_then(|_| reflection_class_binding_for_expr(ctx, value)); let fiber_start_sig = assigned_name.and_then(|_| crate::ir_lower::fibers::start_sig_for_expr(ctx, value)); let callable_array = assigned_name @@ -1545,6 +1546,9 @@ fn lower_assignment_expr( if let Some(target) = static_callable { ctx.bind_static_callable_local(name, target); } + if let Some(reflected_class) = reflected_class { + ctx.bind_reflection_class_local(name, reflected_class); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -3584,6 +3588,14 @@ pub(crate) fn static_callable_binding_for_expr( } } +/// Returns the reflected class captured by a statically-known `ReflectionClass` expression. +pub(crate) fn reflection_class_binding_for_expr( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, +) -> Option { + reflection_class_new_instance_reflected_class(ctx, expr) +} + /// EIR value and callable binding produced by a callable-array assignment. pub(crate) struct LoweredCallableArrayAssignment { pub(crate) value: LoweredValue, @@ -10200,7 +10212,7 @@ fn lower_reflection_class_static_property_value_call( args: &[Expr], expr: &Expr, ) -> Option { - let class_name = reflection_class_new_instance_reflected_class(ctx, object_expr?)?; + let class_name = reflection_class_reflected_class(ctx, object_expr?)?; match php_symbol_key(method).as_str() { "getstaticpropertyvalue" => { lower_reflection_class_get_static_property_value(ctx, &class_name, args, expr) @@ -10436,7 +10448,7 @@ fn reflection_class_new_instance_constructor_signature<'a>( object_expr: Option<&Expr>, forwarded_args: &[Expr], ) -> Option<&'a FunctionSig> { - let class_name = reflection_class_new_instance_reflected_class(ctx, object_expr?)?; + let class_name = reflection_class_reflected_class(ctx, object_expr?)?; if forwarded_args.is_empty() && constructor_signature_for_class_name(ctx, &class_name).is_none() { return None; @@ -10465,6 +10477,19 @@ fn reflection_class_new_instance_reflected_class( resolve_known_class_name(ctx, &raw_class_name) } +/// Resolves a reflected class from an inline constructor or tracked local receiver. +fn reflection_class_reflected_class( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option { + reflection_class_new_instance_reflected_class(ctx, object_expr).or_else(|| { + let ExprKind::Variable(name) = &object_expr.kind else { + return None; + }; + ctx.reflection_class_local(name) + }) +} + /// Returns the `ReflectionClass::__construct()` class-name argument after static /// spread and named-argument normalization. fn reflection_class_constructor_class_arg( diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index a9b98d2f04..5c62e800ab 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -20,8 +20,8 @@ use crate::ir_lower::effects_lookup; use crate::ir_lower::expr::{ array_access_element_result_type, coerce_to_int_at_span, lower_callable_array_for_assignment, lower_array_literal_with_expected_type, lower_closure_for_assignment, lower_expr, - static_callable_binding_for_expr, string_op_uses_scratch_storage, - type_satisfies_array_access_for_ir, + reflection_class_binding_for_expr, static_callable_binding_for_expr, + string_op_uses_scratch_storage, type_satisfies_array_access_for_ir, }; use crate::names::{php_symbol_key, property_hook_set_method}; use crate::parser::ast::{ @@ -234,6 +234,7 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa let direct_closure = matches!(value.kind, ExprKind::Closure { .. }) || bound_closure; ctx.clear_pending_static_callable_result(); let static_callable = static_callable_binding_for_expr(ctx, value); + let reflected_class = reflection_class_binding_for_expr(ctx, value); let fiber_start_sig = crate::ir_lower::fibers::start_sig_for_expr(ctx, value); let callable_array = lower_callable_array_for_assignment(ctx, value, static_callable.as_ref()); let lowered = callable_array @@ -263,6 +264,9 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa ctx.bind_static_callable_local(name, target); } } + if let Some(reflected_class) = reflected_class { + ctx.bind_reflection_class_local(name, reflected_class); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -1023,6 +1027,7 @@ fn lower_typed_assign( ctx.clear_pending_static_callable_result(); let php_type = ctx.type_expr_to_php_type_for_value(type_expr); let static_callable = static_callable_binding_for_expr(ctx, value); + let reflected_class = reflection_class_binding_for_expr(ctx, value); let fiber_start_sig = crate::ir_lower::fibers::start_sig_for_expr(ctx, value); let callable_array = lower_callable_array_for_assignment(ctx, value, static_callable.as_ref()); let lowered = callable_array @@ -1045,6 +1050,9 @@ fn lower_typed_assign( if let Some(target) = static_callable { ctx.bind_static_callable_local(name, target); } + if let Some(reflected_class) = reflected_class { + ctx.bind_reflection_class_local(name, reflected_class); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index cd5756325d..6999d23a85 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2679,6 +2679,38 @@ echo ":" . (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticProp assert_eq!(out.stdout, "7:9:11:fallback"); } +/// Verifies a local `ReflectionClass` receiver keeps statically-known class metadata. +#[test] +fn test_reflection_class_tracked_local_receiver_uses_static_metadata() { + let out = compile_and_run_capture( + r#"getStaticPropertyValue("count"); +ReflectTrackedClassTarget::$count = 9; +echo ":" . $ref->getStaticPropertyValue("count"); +$ref->setStaticPropertyValue("count", 11); +echo ":" . ReflectTrackedClassTarget::$count; +echo ":" . $ref->getStaticPropertyValue("missing", "fallback"); +$obj = $ref->newInstance(id: 5, name: "Ada"); +echo ":" . $obj->id . ":" . $obj->name; +$obj2 = $ref->newInstanceArgs(["name" => "Bob", "id" => 6]); +echo ":" . $obj2->id . ":" . $obj2->name; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:9:11:fallback:5:Ada:6:Bob"); +} + /// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. #[test] fn test_reflection_parameter_get_attributes_returns_parameter_metadata() { From eea2c17931e17746a4cf7f158b0633b2cfe22cc3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 07:43:06 +0200 Subject: [PATCH 0529/1208] Support live ReflectionClass static property maps --- docs/php/classes.md | 2 +- src/ir_lower/expr/mod.rs | 116 +++++++++++++++++++++++++++++--- tests/codegen/oop/attributes.rs | 32 +++++++++ 3 files changed, 140 insertions(+), 10 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 8e36390d69..ef83d6d50e 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1342,7 +1342,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, `getStaticPropertyValue()` and `setStaticPropertyValue()` are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one, and the property name is a literal string; missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 24acc5660d..d36cb228e6 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10214,6 +10214,9 @@ fn lower_reflection_class_static_property_value_call( ) -> Option { let class_name = reflection_class_reflected_class(ctx, object_expr?)?; match php_symbol_key(method).as_str() { + "getstaticproperties" => { + lower_reflection_class_get_static_properties(ctx, &class_name, args, expr) + } "getstaticpropertyvalue" => { lower_reflection_class_get_static_property_value(ctx, &class_name, args, expr) } @@ -10224,6 +10227,51 @@ fn lower_reflection_class_static_property_value_call( } } +/// Lowers `ReflectionClass::getStaticProperties()` to a live static-property map. +fn lower_reflection_class_get_static_properties( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + if !args.is_empty() { + return None; + } + let properties = reflection_class_static_property_map_entries(ctx, class_name)?; + let hash_ty = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }; + let hash = ctx.emit_value( + Op::HashNew, + Vec::new(), + Some(Immediate::Capacity(properties.len() as u32)), + hash_ty, + Op::HashNew.default_effects(), + Some(expr.span), + ); + for (property, declaring_class, property_ty) in properties { + let key_expr = Expr::new(ExprKind::StringLiteral(property.clone()), expr.span); + let key = lower_string_literal(ctx, &property, &key_expr); + let value = lower_static_property_get_by_class_name( + ctx, + &declaring_class, + &property, + property_ty, + expr, + ); + let value = box_value_as_mixed(ctx, value, expr.span); + ctx.emit_void( + Op::HashSet, + vec![hash.value, key.value, value.value], + None, + Op::HashSet.default_effects(), + Some(expr.span), + ); + } + Some(hash) +} + /// Lowers `ReflectionClass::getStaticPropertyValue()` to a live static-property read. fn lower_reflection_class_get_static_property_value( ctx: &mut LoweringContext<'_, '_>, @@ -10232,11 +10280,13 @@ fn lower_reflection_class_get_static_property_value( expr: &Expr, ) -> Option { let (property, default) = reflection_class_get_static_property_value_args(args)?; - if let Some(property_ty) = reflection_class_static_property_type(ctx, class_name, &property) { + if let Some((declaring_class, property_ty)) = + reflection_class_static_property_target(ctx, class_name, &property) + { if default.is_none() { return Some(lower_static_property_get_by_class_name( ctx, - class_name, + &declaring_class, &property, property_ty, expr, @@ -10255,12 +10305,54 @@ fn lower_reflection_class_set_static_property_value( expr: &Expr, ) -> Option { let (property, value) = reflection_class_set_static_property_value_args(args)?; - reflection_class_static_property_type(ctx, class_name, &property)?; + let (declaring_class, _) = reflection_class_static_property_target(ctx, class_name, &property)?; let value = lower_expr(ctx, &value); - store_static_property_by_class_name(ctx, class_name, &property, value.value, expr.span); + store_static_property_by_class_name(ctx, &declaring_class, &property, value.value, expr.span); Some(lower_null(ctx, expr)) } +/// Returns synthetic array entries for current static-property values on a reflected class. +fn reflection_class_static_property_map_entries( + ctx: &LoweringContext<'_, '_>, + class_name: &str, +) -> Option> { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + Some( + class_info + .static_properties + .iter() + .map(|(property, property_ty)| { + let declaring_class = class_info + .static_property_declaring_classes + .get(property) + .cloned() + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let property_ty = normalize_value_php_type(property_ty.codegen_repr()); + (property.clone(), declaring_class, property_ty) + }) + .collect(), + ) +} + +/// Boxes a concrete PHP value into the runtime `Mixed` cell representation. +fn box_value_as_mixed( + ctx: &mut LoweringContext<'_, '_>, + value: LoweredValue, + span: Span, +) -> LoweredValue { + if ctx.builder.value_php_type(value.value).codegen_repr() == PhpType::Mixed { + return value; + } + ctx.emit_value( + Op::MixedBox, + vec![value.value], + None, + PhpType::Mixed, + Op::MixedBox.default_effects(), + Some(span), + ) +} + /// Returns the literal property name and optional explicit default argument for a get call. fn reflection_class_get_static_property_value_args( args: &[Expr], @@ -10330,18 +10422,24 @@ fn reflection_class_static_property_name_arg(arg: &Expr) -> Option { } } -/// Returns the retained PHP type for one static property on a reflected class. -fn reflection_class_static_property_type( +/// Returns the declaring class and retained PHP type for one reflected static property. +fn reflection_class_static_property_target( ctx: &LoweringContext<'_, '_>, class_name: &str, property: &str, -) -> Option { +) -> Option<(String, PhpType)> { let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; - class_info + let property_ty = class_info .static_properties .iter() .find(|(name, _)| name == property) - .map(|(_, property_ty)| normalize_value_php_type(property_ty.codegen_repr())) + .map(|(_, property_ty)| normalize_value_php_type(property_ty.codegen_repr()))?; + let declaring_class = class_info + .static_property_declaring_classes + .get(property) + .cloned() + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + Some((declaring_class, property_ty)) } /// Emits a live static-property read from a known reflected class and property name. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 6999d23a85..6585292696 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2711,6 +2711,38 @@ echo ":" . $obj2->id . ":" . $obj2->name; assert_eq!(out.stdout, "7:9:11:fallback:5:Ada:6:Bob"); } +/// Verifies `ReflectionClass::getStaticProperties()` reads current AOT static values. +#[test] +fn test_reflection_class_get_static_properties_reads_live_storage() { + let out = compile_and_run_capture( + r#"getStaticProperties(); +echo $props["base"] . ":" . $props["count"] . ":" . $props["label"]; +$inline = (new ReflectionClass(ReflectStaticPropertiesChild::class))->getStaticProperties(); +echo ":" . $inline["count"]; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "B2:4:new:4"); +} + /// Verifies `ReflectionParameter::getAttributes()` exposes parameter attributes. #[test] fn test_reflection_parameter_get_attributes_returns_parameter_metadata() { From cf2114d572b51c7ca74c9d28dda88f396665e300 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 08:04:44 +0200 Subject: [PATCH 0530/1208] Support inline ReflectionProperty value access --- docs/php/classes.md | 3 + src/codegen/lower_inst/objects/reflection.rs | 12 +- src/ir_lower/expr/mod.rs | 146 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 78 ++++++++++ tests/codegen/oop.rs | 2 + tests/codegen/oop/reflection_properties.rs | 35 +++++ 6 files changed, 266 insertions(+), 10 deletions(-) create mode 100644 tests/codegen/oop/reflection_properties.rs diff --git a/docs/php/classes.md b/docs/php/classes.md index ef83d6d50e..3a8e9cde2e 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1253,6 +1253,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | +| `ReflectionProperty::getValue()` | Inline `new ReflectionProperty($class_name, $property_name)` with an explicit object argument | Read supported public instance property storage | +| `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported public instance property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | @@ -1342,6 +1344,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionProperty::getValue()` and `setValue()` are supported for inline `new ReflectionProperty(Known::class, "property")` receivers that target public instance properties and receive an explicit object argument. Full PHP visibility-bypassing access, reflector objects returned from `ReflectionClass::getProperty()` / `getProperties()`, and static `ReflectionProperty` value forms are not yet available. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 28f1e3ce18..558df404cb 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -914,8 +914,16 @@ fn reflection_property_metadata( reflection_property_declaring_class_name(info, &property_name); Some(ReflectionOwnerMetadata { reflected_name: Some(property_name.clone()), - attr_names: info.property_attribute_names.get(&property_name)?.clone(), - attr_args: info.property_attribute_args.get(&property_name)?.clone(), + attr_names: info + .property_attribute_names + .get(&property_name) + .cloned() + .unwrap_or_default(), + attr_args: info + .property_attribute_args + .get(&property_name) + .cloned() + .unwrap_or_default(), interface_names: Vec::new(), trait_names: Vec::new(), parent_names: Vec::new(), diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index d36cb228e6..8a1255a08f 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9923,10 +9923,15 @@ fn lower_method_call( if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { return lower_reflection_class_new_instance(ctx, Some(object_expr), object, args, expr); } - if op == Op::MethodCall - && is_reflection_class_new_instance_args_call(ctx, object.value, method) + if op == Op::MethodCall && is_reflection_class_new_instance_args_call(ctx, object.value, method) { - return lower_reflection_class_new_instance_args(ctx, Some(object_expr), object, args, expr); + return lower_reflection_class_new_instance_args( + ctx, + Some(object_expr), + object, + args, + expr, + ); } if op == Op::MethodCall && is_reflection_class_new_instance_without_constructor_call(ctx, object.value, method) @@ -9944,6 +9949,13 @@ fn lower_method_call( return value; } } + if op == Op::MethodCall { + if let Some(value) = + lower_reflection_property_value_call(ctx, Some(object_expr), method, args, expr) + { + return value; + } + } if matches!( ctx.builder.value_php_type(object.value).codegen_repr(), PhpType::Callable @@ -10227,6 +10239,127 @@ fn lower_reflection_class_static_property_value_call( } } +/// Lowers `ReflectionProperty::getValue($object)` when the reflected property is known. +fn lower_reflection_property_value_call( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + if php_symbol_key(method) != "getvalue" { + return None; + } + let (_, property, _) = reflection_property_instance_target(ctx, object_expr?)?; + let object_arg = reflection_property_get_value_object_arg(args)?; + let object = lower_expr(ctx, &object_arg); + Some(lower_property_get_from_value( + ctx, + object, + &property, + Op::PropGet, + expr, + )) +} + +/// Returns the explicit object argument passed to `ReflectionProperty::getValue()`. +fn reflection_property_get_value_object_arg(args: &[Expr]) -> Option { + match args { + [arg] => Some(arg.clone()), + _ => None, + } +} + +/// Resolves an inline `new ReflectionProperty(Known::class, "prop")` instance property target. +fn reflection_property_instance_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String, PhpType)> { + let (class_name, property) = reflection_property_constructor_target(ctx, object_expr)?; + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + if class_info + .static_properties + .iter() + .any(|(name, _)| name == &property) + { + return None; + } + if class_info.property_visibilities.get(&property) != Some(&Visibility::Public) { + return None; + } + let (_, (_, property_ty)) = class_info.visible_property(&property)?; + Some(( + class_name, + property, + normalize_value_php_type(property_ty.codegen_repr()), + )) +} + +/// Extracts the known class and property name from an inline ReflectionProperty constructor. +fn reflection_property_constructor_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::NewObject { class_name, args } = &object_expr.kind else { + return None; + }; + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionproperty" { + return None; + } + let (class_arg, property_arg) = reflection_property_constructor_regular_args(ctx, args)?; + let raw_class_name = match &class_arg.kind { + ExprKind::StringLiteral(value) => value.clone(), + ExprKind::ClassConstant { receiver } => static_receiver_class_name(ctx, receiver)?, + _ => return None, + }; + let class_name = resolve_known_class_name(ctx, &raw_class_name)?; + let ExprKind::StringLiteral(property) = property_arg.kind else { + return None; + }; + Some((class_name, property)) +} + +/// Returns normalized constructor args for `ReflectionProperty($class, $property)`. +fn reflection_property_constructor_regular_args( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option<(Expr, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [class_arg, property_arg] => Some((class_arg.clone(), property_arg.clone())), + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionProperty") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + let class_arg = planned_regular_arg_expr(plan.regular_args.first()?)?.clone(); + let property_arg = planned_regular_arg_expr(plan.regular_args.get(1)?)?.clone(); + Some((class_arg, property_arg)) +} + /// Lowers `ReflectionClass::getStaticProperties()` to a live static-property map. fn lower_reflection_class_get_static_properties( ctx: &mut LoweringContext<'_, '_>, @@ -10368,9 +10501,7 @@ fn reflection_class_get_static_property_value_args( } /// Returns the literal property name and value expression for a set call. -fn reflection_class_set_static_property_value_args( - args: &[Expr], -) -> Option<(String, Expr)> { +fn reflection_class_set_static_property_value_args(args: &[Expr]) -> Option<(String, Expr)> { let args = reflection_class_new_instance_args(args); if args.iter().any(is_spread_arg) { return None; @@ -10834,8 +10965,7 @@ fn lower_method_call_with_receiver( if op == Op::MethodCall && is_reflection_class_new_instance_call(ctx, object.value, method) { return lower_reflection_class_new_instance(ctx, None, object, args, expr); } - if op == Op::MethodCall - && is_reflection_class_new_instance_args_call(ctx, object.value, method) + if op == Op::MethodCall && is_reflection_class_new_instance_args_call(ctx, object.value, method) { return lower_reflection_class_new_instance_args(ctx, None, object, args, expr); } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index c55a7b8eb2..915c6cd4ed 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2582,6 +2582,82 @@ fn builtin_reflection_property_modifier_mask_method(method_name: &str, mask: i64 } } +/// Returns the fallback `ReflectionProperty::getValue()` body for unsupported dynamic receivers. +fn builtin_reflection_property_get_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "getValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(mixed_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![throw_new_reflection_exception( + string_lit( + "ReflectionProperty::getValue() requires an inline known public instance property", + dummy_span, + ), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::setValue()` for explicit public instance-object writes. +fn builtin_reflection_property_set_value_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let target = reflection_dynamic_object_property(dummy_span); + let value = Expr::new(ExprKind::Variable("value".to_string()), dummy_span); + ClassMethod { + name: "setValue".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![ + ("object".to_string(), Some(mixed_type()), None, false), + ("value".to_string(), Some(mixed_type()), None, false), + ], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Void), + body: vec![Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::Assignment { + target: Box::new(target), + value: Box::new(value), + result_target: None, + prelude: Vec::new(), + conditional_value_temp: None, + }, + dummy_span, + )), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `$object->{$this->__name}` for ReflectionProperty value accessors. +fn reflection_dynamic_object_property(span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::DynamicPropertyAccess { + object: Box::new(Expr::new(ExprKind::Variable("object".to_string()), span)), + property: Box::new(reflection_this_property("__name", span)), + }, + span, + ) +} + /// Returns `ReflectionProperty::isLazy()` for the non-lazy property model elephc supports. fn builtin_reflection_property_is_lazy_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -2986,6 +3062,8 @@ fn add_reflection_member_flag_methods( )); methods.push(builtin_reflection_property_is_lazy_method()); methods.push(builtin_reflection_property_skip_lazy_initialization_method()); + methods.push(builtin_reflection_property_get_value_method()); + methods.push(builtin_reflection_property_set_value_method()); methods.push(builtin_reflection_property_modifier_mask_method( "isProtectedSet", 2048, diff --git a/tests/codegen/oop.rs b/tests/codegen/oop.rs index a3d303b00e..048bfc9f60 100644 --- a/tests/codegen/oop.rs +++ b/tests/codegen/oop.rs @@ -43,3 +43,5 @@ mod abstract_properties; mod property_hooks; #[path = "oop/datetime.rs"] mod datetime; +#[path = "oop/reflection_properties.rs"] +mod reflection_properties; diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs new file mode 100644 index 0000000000..bb1aeef951 --- /dev/null +++ b/tests/codegen/oop/reflection_properties.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! End-to-end codegen tests for ReflectionProperty value accessors on supported +//! object-property storage. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Covers explicit object arguments for public instance properties. +//! - Static properties and visibility-bypassing reflection access remain separate surfaces. + +use super::*; + +/// Verifies `ReflectionProperty::getValue()` and `setValue()` read and write +/// public instance properties for inline reflectors with explicit object args. +#[test] +fn test_reflection_property_value_accessors_for_public_instance_properties() { + let out = compile_and_run( + r#"getValue($target); +(new ReflectionProperty(ReflectValueAccessTarget::class, "count"))->setValue($target, 7); +echo ":" . $target->count; +echo ":" . (new ReflectionProperty(ReflectValueAccessTarget::class, "label"))->getValue($target); +(new ReflectionProperty(ReflectValueAccessTarget::class, "label"))->setValue($target, "new"); +echo ":" . $target->label; +"#, + ); + assert_eq!(out, "1:7:old:new"); +} From 4da1d8ef8742d2723086dabc9cc131d619efef28 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 08:09:07 +0200 Subject: [PATCH 0531/1208] Support ReflectionClass getProperty value access --- docs/php/classes.md | 4 +-- src/ir_lower/expr/mod.rs | 42 +++++++++++++++++++++- tests/codegen/oop/reflection_properties.rs | 8 +++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 3a8e9cde2e..70fc113fb2 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1253,7 +1253,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | -| `ReflectionProperty::getValue()` | Inline `new ReflectionProperty($class_name, $property_name)` with an explicit object argument | Read supported public instance property storage | +| `ReflectionProperty::getValue()` | Inline `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` with an explicit object argument | Read supported public instance property storage | | `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported public instance property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | @@ -1344,7 +1344,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for inline `new ReflectionProperty(Known::class, "property")` receivers that target public instance properties and receive an explicit object argument. Full PHP visibility-bypassing access, reflector objects returned from `ReflectionClass::getProperty()` / `getProperties()`, and static `ReflectionProperty` value forms are not yet available. +- `ReflectionProperty::getValue()` and `setValue()` are supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers that target public instance properties and receive an explicit object argument. Full PHP visibility-bypassing access, reflector objects held only in runtime storage, listed reflectors returned from `ReflectionClass::getProperties()`, and static `ReflectionProperty` value forms are not yet available. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 8a1255a08f..7f33cc6952 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10275,7 +10275,7 @@ fn reflection_property_instance_target( ctx: &LoweringContext<'_, '_>, object_expr: &Expr, ) -> Option<(String, String, PhpType)> { - let (class_name, property) = reflection_property_constructor_target(ctx, object_expr)?; + let (class_name, property) = reflection_property_reflected_target(ctx, object_expr)?; let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; if class_info .static_properties @@ -10295,6 +10295,15 @@ fn reflection_property_instance_target( )) } +/// Extracts the known class and property name from a supported ReflectionProperty source. +fn reflection_property_reflected_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + reflection_property_constructor_target(ctx, object_expr) + .or_else(|| reflection_property_class_get_property_target(ctx, object_expr)) +} + /// Extracts the known class and property name from an inline ReflectionProperty constructor. fn reflection_property_constructor_target( ctx: &LoweringContext<'_, '_>, @@ -10319,6 +10328,37 @@ fn reflection_property_constructor_target( Some((class_name, property)) } +/// Extracts the property target from inline `ReflectionClass::getProperty()` calls. +fn reflection_property_class_get_property_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::MethodCall { + object, + method, + args, + } = &object_expr.kind + else { + return None; + }; + if php_symbol_key(method) != "getproperty" { + return None; + } + let class_name = reflection_class_reflected_class(ctx, object)?; + let property = reflection_class_member_name_arg(args)?; + Some((class_name, property)) +} + +/// Returns the literal name argument passed to a ReflectionClass member lookup. +fn reflection_class_member_name_arg(args: &[Expr]) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (name, _) = reflection_class_static_property_regular_args(&args, "name", None)?; + reflection_class_static_property_name_arg(name.as_ref()?) +} + /// Returns normalized constructor args for `ReflectionProperty($class, $property)`. fn reflection_property_constructor_regular_args( ctx: &LoweringContext<'_, '_>, diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index bb1aeef951..01688b1704 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -12,7 +12,8 @@ use super::*; /// Verifies `ReflectionProperty::getValue()` and `setValue()` read and write -/// public instance properties for inline reflectors with explicit object args. +/// public instance properties for inline reflectors with explicit object args, +/// including reflectors returned by `ReflectionClass::getProperty()`. #[test] fn test_reflection_property_value_accessors_for_public_instance_properties() { let out = compile_and_run( @@ -29,7 +30,10 @@ echo ":" . $target->count; echo ":" . (new ReflectionProperty(ReflectValueAccessTarget::class, "label"))->getValue($target); (new ReflectionProperty(ReflectValueAccessTarget::class, "label"))->setValue($target, "new"); echo ":" . $target->label; +echo ":" . (new ReflectionClass(ReflectValueAccessTarget::class))->getProperty("count")->getValue($target); +(new ReflectionClass(ReflectValueAccessTarget::class))->getProperty("count")->setValue($target, 11); +echo ":" . $target->count; "#, ); - assert_eq!(out, "1:7:old:new"); + assert_eq!(out, "1:7:old:new:7:11"); } From b3b1a21e35e012faae9c89ac9e6a5db7f152d6c0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 08:15:23 +0200 Subject: [PATCH 0532/1208] Lower ReflectionProperty setValue directly --- docs/php/classes.md | 4 +- src/ir_lower/expr/mod.rs | 61 ++++++++++++++++--- src/types/checker/builtin_types/reflection.rs | 36 ++--------- tests/codegen/oop/reflection_properties.rs | 5 +- 4 files changed, 64 insertions(+), 42 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 70fc113fb2..24f339e8b9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1253,7 +1253,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | -| `ReflectionProperty::getValue()` | Inline `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` with an explicit object argument | Read supported public instance property storage | +| `ReflectionProperty::getValue()` | Inline `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` with an explicit object argument | Read supported public instance property storage; positional and named arguments are normalized | | `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported public instance property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | @@ -1344,7 +1344,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers that target public instance properties and receive an explicit object argument. Full PHP visibility-bypassing access, reflector objects held only in runtime storage, listed reflectors returned from `ReflectionClass::getProperties()`, and static `ReflectionProperty` value forms are not yet available. +- `ReflectionProperty::getValue()` and `setValue()` are supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers that target public instance properties and receive explicit positional or named object/value arguments. Full PHP visibility-bypassing access, reflector objects held only in runtime storage, listed reflectors returned from `ReflectionClass::getProperties()`, and static `ReflectionProperty` value forms are not yet available. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 7f33cc6952..a2a049e062 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10247,27 +10247,70 @@ fn lower_reflection_property_value_call( args: &[Expr], expr: &Expr, ) -> Option { - if php_symbol_key(method) != "getvalue" { - return None; - } let (_, property, _) = reflection_property_instance_target(ctx, object_expr?)?; - let object_arg = reflection_property_get_value_object_arg(args)?; + match php_symbol_key(method).as_str() { + "getvalue" => lower_reflection_property_get_value(ctx, &property, args, expr), + "setvalue" => lower_reflection_property_set_value(ctx, &property, args, expr), + _ => None, + } +} + +/// Lowers `ReflectionProperty::getValue($object)` to a direct property read. +fn lower_reflection_property_get_value( + ctx: &mut LoweringContext<'_, '_>, + property: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let object_arg = reflection_property_get_value_arg(args)?; let object = lower_expr(ctx, &object_arg); Some(lower_property_get_from_value( ctx, object, - &property, + property, Op::PropGet, expr, )) } +/// Lowers `ReflectionProperty::setValue($object, $value)` to a direct property write. +fn lower_reflection_property_set_value( + ctx: &mut LoweringContext<'_, '_>, + property: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let (object_arg, value_arg) = reflection_property_set_value_args(args)?; + let target = Expr::new( + ExprKind::PropertyAccess { + object: Box::new(object_arg), + property: property.to_string(), + }, + expr.span, + ); + lower_non_local_assignment_write(ctx, &target, &value_arg, expr.span); + Some(lower_null(ctx, expr)) +} + /// Returns the explicit object argument passed to `ReflectionProperty::getValue()`. -fn reflection_property_get_value_object_arg(args: &[Expr]) -> Option { - match args { - [arg] => Some(arg.clone()), - _ => None, +fn reflection_property_get_value_arg(args: &[Expr]) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (object, _) = reflection_class_static_property_regular_args(&args, "object", None)?; + object +} + +/// Returns the explicit object and value arguments passed to `ReflectionProperty::setValue()`. +fn reflection_property_set_value_args(args: &[Expr]) -> Option<(Expr, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; } + let (object, value) = + reflection_class_static_property_regular_args(&args, "object", Some("value"))?; + Some((object?, value?)) } /// Resolves an inline `new ReflectionProperty(Known::class, "prop")` instance property target. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 915c6cd4ed..590006e248 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -480,12 +480,7 @@ fn builtin_reflection_class_new_instance_args_method() -> ClassMethod { is_abstract: false, is_final: false, has_body: true, - params: vec![( - "args".to_string(), - Some(array_type()), - empty_array(), - false, - )], + params: vec![("args".to_string(), Some(array_type()), empty_array(), false)], param_attributes: Vec::new(), variadic: None, variadic_type: None, @@ -2609,11 +2604,9 @@ fn builtin_reflection_property_get_value_method() -> ClassMethod { } } -/// Returns `ReflectionProperty::setValue()` for explicit public instance-object writes. +/// Returns the fallback `ReflectionProperty::setValue()` body for unsupported dynamic receivers. fn builtin_reflection_property_set_value_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); - let target = reflection_dynamic_object_property(dummy_span); - let value = Expr::new(ExprKind::Variable("value".to_string()), dummy_span); ClassMethod { name: "setValue".to_string(), visibility: Visibility::Public, @@ -2629,17 +2622,11 @@ fn builtin_reflection_property_set_value_method() -> ClassMethod { variadic: None, variadic_type: None, return_type: Some(TypeExpr::Void), - body: vec![Stmt::new( - StmtKind::ExprStmt(Expr::new( - ExprKind::Assignment { - target: Box::new(target), - value: Box::new(value), - result_target: None, - prelude: Vec::new(), - conditional_value_temp: None, - }, + body: vec![throw_new_reflection_exception( + string_lit( + "ReflectionProperty::setValue() requires an inline known public instance property", dummy_span, - )), + ), dummy_span, )], span: dummy_span, @@ -2647,17 +2634,6 @@ fn builtin_reflection_property_set_value_method() -> ClassMethod { } } -/// Builds `$object->{$this->__name}` for ReflectionProperty value accessors. -fn reflection_dynamic_object_property(span: crate::span::Span) -> Expr { - Expr::new( - ExprKind::DynamicPropertyAccess { - object: Box::new(Expr::new(ExprKind::Variable("object".to_string()), span)), - property: Box::new(reflection_this_property("__name", span)), - }, - span, - ) -} - /// Returns `ReflectionProperty::isLazy()` for the non-lazy property model elephc supports. fn builtin_reflection_property_is_lazy_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 01688b1704..0728909135 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -33,7 +33,10 @@ echo ":" . $target->label; echo ":" . (new ReflectionClass(ReflectValueAccessTarget::class))->getProperty("count")->getValue($target); (new ReflectionClass(ReflectValueAccessTarget::class))->getProperty("count")->setValue($target, 11); echo ":" . $target->count; +echo ":" . (new ReflectionProperty(ReflectValueAccessTarget::class, "count"))->getValue(object: $target); +(new ReflectionClass(ReflectValueAccessTarget::class))->getProperty("count")->setValue(value: 13, object: $target); +echo ":" . $target->count; "#, ); - assert_eq!(out, "1:7:old:new:7:11"); + assert_eq!(out, "1:7:old:new:7:11:11:13"); } From aea094e59028c13272180eee3dbf9e5a96518bdf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 08:23:11 +0200 Subject: [PATCH 0533/1208] Support static ReflectionProperty values --- docs/php/classes.md | 6 +- src/ir_lower/expr/mod.rs | 155 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 11 +- tests/codegen/oop/reflection_properties.rs | 29 +++- 4 files changed, 189 insertions(+), 12 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 24f339e8b9..b6a15bf437 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1253,8 +1253,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | -| `ReflectionProperty::getValue()` | Inline `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` with an explicit object argument | Read supported public instance property storage; positional and named arguments are normalized | -| `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported public instance property storage | +| `ReflectionProperty::getValue()` | Inline `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` with an explicit object argument for instance properties, or an omitted/ignored object argument for static properties | Read supported public instance/static property storage; positional and named arguments are normalized | +| `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported public instance/static property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | @@ -1344,7 +1344,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers that target public instance properties and receive explicit positional or named object/value arguments. Full PHP visibility-bypassing access, reflector objects held only in runtime storage, listed reflectors returned from `ReflectionClass::getProperties()`, and static `ReflectionProperty` value forms are not yet available. +- `ReflectionProperty::getValue()` and `setValue()` are supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers that target public instance properties with explicit positional or named object/value arguments, or public static properties with omitted/ignored object arguments. Full PHP visibility-bypassing access, reflector objects held only in runtime storage, and listed reflectors returned from `ReflectionClass::getProperties()` are not yet available. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index a2a049e062..b155ce9a43 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10247,10 +10247,39 @@ fn lower_reflection_property_value_call( args: &[Expr], expr: &Expr, ) -> Option { - let (_, property, _) = reflection_property_instance_target(ctx, object_expr?)?; + let object_expr = object_expr?; match php_symbol_key(method).as_str() { - "getvalue" => lower_reflection_property_get_value(ctx, &property, args, expr), - "setvalue" => lower_reflection_property_set_value(ctx, &property, args, expr), + "getvalue" => { + if let Some((declaring_class, property, property_ty)) = + reflection_property_static_target(ctx, object_expr) + { + return lower_reflection_property_get_static_value( + ctx, + &declaring_class, + &property, + property_ty, + args, + expr, + ); + } + let (_, property, _) = reflection_property_instance_target(ctx, object_expr)?; + lower_reflection_property_get_value(ctx, &property, args, expr) + } + "setvalue" => { + if let Some((declaring_class, property, _)) = + reflection_property_static_target(ctx, object_expr) + { + return lower_reflection_property_set_static_value( + ctx, + &declaring_class, + &property, + args, + expr, + ); + } + let (_, property, _) = reflection_property_instance_target(ctx, object_expr)?; + lower_reflection_property_set_value(ctx, &property, args, expr) + } _ => None, } } @@ -10292,18 +10321,100 @@ fn lower_reflection_property_set_value( Some(lower_null(ctx, expr)) } +/// Lowers static `ReflectionProperty::getValue()` to a direct static-property read. +fn lower_reflection_property_get_static_value( + ctx: &mut LoweringContext<'_, '_>, + declaring_class: &str, + property: &str, + property_ty: PhpType, + args: &[Expr], + expr: &Expr, +) -> Option { + if let Some(ignored_object) = reflection_property_static_get_value_ignored_arg(args)? { + lower_ignored_reflection_argument(ctx, &ignored_object); + } + Some(lower_static_property_get_by_class_name( + ctx, + declaring_class, + property, + property_ty, + expr, + )) +} + +/// Lowers static `ReflectionProperty::setValue(null, $value)` to a static-property write. +fn lower_reflection_property_set_static_value( + ctx: &mut LoweringContext<'_, '_>, + declaring_class: &str, + property: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let (ignored_object, value_arg) = reflection_property_static_set_value_args(args)?; + lower_ignored_reflection_argument(ctx, &ignored_object); + let value = lower_expr(ctx, &value_arg); + store_static_property_by_class_name(ctx, declaring_class, property, value.value, expr.span); + Some(lower_null(ctx, expr)) +} + +/// Evaluates an ignored Reflection argument and releases temporary objects. +fn lower_ignored_reflection_argument(ctx: &mut LoweringContext<'_, '_>, arg: &Expr) { + let value = lower_expr(ctx, arg); + if ctx.value_is_owning_temporary(value) { + crate::ir_lower::ownership::release_if_owned(ctx, value, Some(arg.span)); + } +} + /// Returns the explicit object argument passed to `ReflectionProperty::getValue()`. fn reflection_property_get_value_arg(args: &[Expr]) -> Option { let args = reflection_class_new_instance_args(args); if args.iter().any(is_spread_arg) { return None; } - let (object, _) = reflection_class_static_property_regular_args(&args, "object", None)?; - object + let object = if !crate::types::call_args::has_named_args(&args) { + match args.as_slice() { + [object] => object.clone(), + _ => return None, + } + } else { + reflection_property_named_object_arg(&args)? + }; + (!matches!(&object.kind, ExprKind::Null)).then_some(object) } /// Returns the explicit object and value arguments passed to `ReflectionProperty::setValue()`. fn reflection_property_set_value_args(args: &[Expr]) -> Option<(Expr, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + let (object, value) = + reflection_class_static_property_regular_args(&args, "object", Some("value"))?; + let object = object?; + if matches!(&object.kind, ExprKind::Null) { + return None; + } + Some((object, value?)) +} + +/// Returns the optional ignored object argument for static `ReflectionProperty::getValue()`. +fn reflection_property_static_get_value_ignored_arg(args: &[Expr]) -> Option> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [] => Some(None), + [object] => Some(Some(object.clone())), + _ => None, + }; + } + reflection_property_named_optional_object_arg(&args) +} + +/// Returns the ignored object and value arguments for static `ReflectionProperty::setValue()`. +fn reflection_property_static_set_value_args(args: &[Expr]) -> Option<(Expr, Expr)> { let args = reflection_class_new_instance_args(args); if args.iter().any(is_spread_arg) { return None; @@ -10313,6 +10424,25 @@ fn reflection_property_set_value_args(args: &[Expr]) -> Option<(Expr, Expr)> { Some((object?, value?)) } +/// Returns a required named `object` argument for ReflectionProperty value access. +fn reflection_property_named_object_arg(args: &[Expr]) -> Option { + reflection_property_named_optional_object_arg(args)? +} + +/// Returns an optional named `object` argument for ReflectionProperty value access. +fn reflection_property_named_optional_object_arg(args: &[Expr]) -> Option> { + let mut object = None; + for arg in args { + match &arg.kind { + ExprKind::NamedArg { name, value } if php_symbol_key(name) == "object" => { + object = Some((**value).clone()); + } + _ => return None, + } + } + Some(object) +} + /// Resolves an inline `new ReflectionProperty(Known::class, "prop")` instance property target. fn reflection_property_instance_target( ctx: &LoweringContext<'_, '_>, @@ -10338,6 +10468,21 @@ fn reflection_property_instance_target( )) } +/// Resolves an inline `ReflectionProperty` target for a public static property. +fn reflection_property_static_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String, PhpType)> { + let (class_name, property) = reflection_property_reflected_target(ctx, object_expr)?; + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + if class_info.static_property_visibilities.get(&property) != Some(&Visibility::Public) { + return None; + } + let (declaring_class, property_ty) = + reflection_class_static_property_target(ctx, &class_name, &property)?; + Some((declaring_class, property, property_ty)) +} + /// Extracts the known class and property name from a supported ReflectionProperty source. fn reflection_property_reflected_target( ctx: &LoweringContext<'_, '_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 590006e248..b60510b859 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2587,14 +2587,19 @@ fn builtin_reflection_property_get_value_method() -> ClassMethod { is_abstract: false, is_final: false, has_body: true, - params: vec![("object".to_string(), Some(mixed_type()), None, false)], + params: vec![( + "object".to_string(), + Some(mixed_type()), + null_expr(), + false, + )], param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(mixed_type()), body: vec![throw_new_reflection_exception( string_lit( - "ReflectionProperty::getValue() requires an inline known public instance property", + "ReflectionProperty::getValue() requires an inline known public instance or static property", dummy_span, ), dummy_span, @@ -2624,7 +2629,7 @@ fn builtin_reflection_property_set_value_method() -> ClassMethod { return_type: Some(TypeExpr::Void), body: vec![throw_new_reflection_exception( string_lit( - "ReflectionProperty::setValue() requires an inline known public instance property", + "ReflectionProperty::setValue() requires an inline known public instance or static property", dummy_span, ), dummy_span, diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 0728909135..c53dc4090f 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -7,7 +7,8 @@ //! //! Key details: //! - Covers explicit object arguments for public instance properties. -//! - Static properties and visibility-bypassing reflection access remain separate surfaces. +//! - Covers static properties where PHP permits no object argument. +//! - Visibility-bypassing reflection access remains a separate surface. use super::*; @@ -40,3 +41,29 @@ echo ":" . $target->count; ); assert_eq!(out, "1:7:old:new:7:11:11:13"); } + +/// Verifies `ReflectionProperty::getValue()` and `setValue()` read and write +/// public static properties for inline reflectors. +#[test] +fn test_reflection_property_value_accessors_for_public_static_properties() { + let out = compile_and_run( + r#"getValue(); +(new ReflectionProperty(ReflectStaticValueAccessTarget::class, "count"))->setValue(null, 17); +echo ":" . ReflectStaticValueAccessTarget::$count; +ReflectStaticValueAccessTarget::$count = 19; +echo ":" . (new ReflectionProperty(ReflectStaticValueAccessTarget::class, "count"))->getValue(object: null); +echo ":" . (new ReflectionClass(ReflectStaticValueAccessTarget::class))->getProperty("label")->getValue(null); +(new ReflectionClass(ReflectStaticValueAccessTarget::class))->getProperty("label")->setValue(null, "new"); +echo ":" . ReflectStaticValueAccessTarget::$label; +(new ReflectionClass(ReflectStaticValueAccessTarget::class))->getProperty("count")->setValue(object: null, value: 23); +echo ":" . ReflectStaticValueAccessTarget::$count; +"#, + ); + assert_eq!(out, "2:17:19:old:new:23"); +} From 0243ae13e8d231321b390b8a51c1375e6b027425 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 08:35:44 +0200 Subject: [PATCH 0534/1208] Support runtime ReflectionProperty instance values --- docs/php/classes.md | 4 +- src/codegen/lower_inst/objects.rs | 638 ++++++++++-------- src/ir_lower/expr/mod.rs | 3 + src/types/checker/builtin_types/reflection.rs | 110 ++- tests/codegen/oop/reflection_properties.rs | 28 + 5 files changed, 494 insertions(+), 289 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index b6a15bf437..45cf5430a2 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1253,7 +1253,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | -| `ReflectionProperty::getValue()` | Inline `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` with an explicit object argument for instance properties, or an omitted/ignored object argument for static properties | Read supported public instance/static property storage; positional and named arguments are normalized | +| `ReflectionProperty::getValue()` | `ReflectionProperty` for public instance properties with an explicit object argument, or inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for public static properties | Read supported public instance/static property storage; positional and named arguments are normalized | | `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported public instance/static property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | @@ -1344,7 +1344,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers that target public instance properties with explicit positional or named object/value arguments, or public static properties with omitted/ignored object arguments. Full PHP visibility-bypassing access, reflector objects held only in runtime storage, and listed reflectors returned from `ReflectionClass::getProperties()` are not yet available. +- `ReflectionProperty::getValue()` and `setValue()` are supported for public instance-property reflectors with explicit positional or named object/value arguments, including reflector objects held in variables and entries returned from `ReflectionClass::getProperties()`. Public static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers with omitted/ignored object arguments. Full PHP visibility-bypassing access and dynamic static-property lookup from reflector objects held only in runtime storage are not yet available. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index d53b06cadd..cca3186e9a 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -675,28 +675,28 @@ fn emit_push_iterator_iterator_downcast_status_from_lookup(ctx: &mut FunctionCon let done = ctx.next_label("iterator_iterator_downcast_lookup_done"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #0"); // did the downcast class-string resolve to metadata? - ctx.emitter.instruction(&format!("b.eq {}", invalid)); // invalid downcast names throw for IteratorAggregate inputs - ctx.emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface - ctx.emitter.instruction(&format!("b.ne {}", invalid)); // interface names are invalid downcast targets - ctx.emitter.instruction("mov x0, #1"); // status 1 means x1 carries a concrete downcast class id - ctx.emitter.instruction(&format!("b {}", done)); // preserve the resolved class id for later validation + ctx.emitter.instruction("cmp x0, #0"); // did the downcast class-string resolve to metadata? + ctx.emitter.instruction(&format!("b.eq {}", invalid)); // invalid downcast names throw for IteratorAggregate inputs + ctx.emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface + ctx.emitter.instruction(&format!("b.ne {}", invalid)); // interface names are invalid downcast targets + ctx.emitter.instruction("mov x0, #1"); // status 1 means x1 carries a concrete downcast class id + ctx.emitter.instruction(&format!("b {}", done)); // preserve the resolved class id for later validation ctx.emitter.label(&invalid); - ctx.emitter.instruction("mov x0, #2"); // status 2 means the downcast must throw for aggregates - ctx.emitter.instruction("mov x1, #0"); // invalid downcast targets have no usable class id + ctx.emitter.instruction("mov x0, #2"); // status 2 means the downcast must throw for aggregates + ctx.emitter.instruction("mov x1, #0"); // invalid downcast targets have no usable class id } Arch::X86_64 => { - ctx.emitter.instruction("test rax, rax"); // did the downcast class-string resolve to metadata? - ctx.emitter.instruction(&format!("je {}", invalid)); // invalid downcast names throw for IteratorAggregate inputs - ctx.emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface - ctx.emitter.instruction(&format!("jne {}", invalid)); // interface names are invalid downcast targets - ctx.emitter.instruction("mov rax, 1"); // status 1 means rdi carries a concrete downcast class id - ctx.emitter.instruction(&format!("jmp {}", done)); // preserve the resolved class id for later validation + ctx.emitter.instruction("test rax, rax"); // did the downcast class-string resolve to metadata? + ctx.emitter.instruction(&format!("je {}", invalid)); // invalid downcast names throw for IteratorAggregate inputs + ctx.emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface + ctx.emitter.instruction(&format!("jne {}", invalid)); // interface names are invalid downcast targets + ctx.emitter.instruction("mov rax, 1"); // status 1 means rdi carries a concrete downcast class id + ctx.emitter.instruction(&format!("jmp {}", done)); // preserve the resolved class id for later validation ctx.emitter.label(&invalid); - ctx.emitter.instruction("mov rax, 2"); // status 2 means the downcast must throw for aggregates - ctx.emitter.instruction("xor edi, edi"); // invalid downcast targets have no usable class id + ctx.emitter.instruction("mov rax, 2"); // status 2 means the downcast must throw for aggregates + ctx.emitter.instruction("xor edi, edi"); // invalid downcast targets have no usable class id } } ctx.emitter.label(&done); @@ -735,41 +735,41 @@ fn emit_validate_iterator_iterator_aggregate_downcast(ctx: &mut FunctionContext< let throw = ctx.next_label("iterator_iterator_downcast_throw"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("ldr x9, [sp, #16]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid - ctx.emitter.instruction(&format!("cbz x9, {}", skip)); // omitted/null class arguments do not constrain aggregates - ctx.emitter.instruction("cmp x9, #1"); // only status 1 carries a valid concrete class id - ctx.emitter.instruction(&format!("b.ne {}", throw)); // invalid names and interfaces throw for aggregates - ctx.emitter.instruction("ldr x0, [sp]"); // pass the saved IteratorAggregate object to the class matcher - ctx.emitter.instruction("ldr x1, [sp, #24]"); // pass the requested downcast class id to the class matcher + ctx.emitter.instruction("ldr x9, [sp, #16]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid + ctx.emitter.instruction(&format!("cbz x9, {}", skip)); // omitted/null class arguments do not constrain aggregates + ctx.emitter.instruction("cmp x9, #1"); // only status 1 carries a valid concrete class id + ctx.emitter.instruction(&format!("b.ne {}", throw)); // invalid names and interfaces throw for aggregates + ctx.emitter.instruction("ldr x0, [sp]"); // pass the saved IteratorAggregate object to the class matcher + ctx.emitter.instruction("ldr x1, [sp, #24]"); // pass the requested downcast class id to the class matcher abi::emit_load_int_immediate(ctx.emitter, "x2", 0); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); - ctx.emitter.instruction("cmp x0, #0"); // did the aggregate object match the requested class? - ctx.emitter.instruction(&format!("b.eq {}", throw)); // non-base downcast classes are rejected like PHP - ctx.emitter.instruction("ldr x0, [sp, #24]"); // pass the requested class id to the interface checker + ctx.emitter.instruction("cmp x0, #0"); // did the aggregate object match the requested class? + ctx.emitter.instruction(&format!("b.eq {}", throw)); // non-base downcast classes are rejected like PHP + ctx.emitter.instruction("ldr x0, [sp, #24]"); // pass the requested class id to the interface checker abi::emit_load_int_immediate(ctx.emitter, "x1", aggregate_interface_id); abi::emit_call_label(ctx.emitter, "__rt_class_implements_interface"); - ctx.emitter.instruction("cmp x0, #0"); // did the downcast class implement IteratorAggregate? - ctx.emitter.instruction(&format!("b.eq {}", throw)); // non-Traversable base classes are rejected like PHP - ctx.emitter.instruction(&format!("b {}", skip)); // the aggregate downcast class is valid + ctx.emitter.instruction("cmp x0, #0"); // did the downcast class implement IteratorAggregate? + ctx.emitter.instruction(&format!("b.eq {}", throw)); // non-Traversable base classes are rejected like PHP + ctx.emitter.instruction(&format!("b {}", skip)); // the aggregate downcast class is valid } Arch::X86_64 => { - ctx.emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid - ctx.emitter.instruction("test r10, r10"); // is there an explicit downcast class to validate? - ctx.emitter.instruction(&format!("je {}", skip)); // omitted/null class arguments do not constrain aggregates - ctx.emitter.instruction("cmp r10, 1"); // only status 1 carries a valid concrete class id - ctx.emitter.instruction(&format!("jne {}", throw)); // invalid names and interfaces throw for aggregates - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // pass the saved IteratorAggregate object to the class matcher - ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // pass the requested downcast class id to the class matcher + ctx.emitter.instruction("mov r10, QWORD PTR [rsp + 16]"); // load downcast status: 0 omitted/null, 1 class id, 2 invalid + ctx.emitter.instruction("test r10, r10"); // is there an explicit downcast class to validate? + ctx.emitter.instruction(&format!("je {}", skip)); // omitted/null class arguments do not constrain aggregates + ctx.emitter.instruction("cmp r10, 1"); // only status 1 carries a valid concrete class id + ctx.emitter.instruction(&format!("jne {}", throw)); // invalid names and interfaces throw for aggregates + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // pass the saved IteratorAggregate object to the class matcher + ctx.emitter.instruction("mov rsi, QWORD PTR [rsp + 24]"); // pass the requested downcast class id to the class matcher abi::emit_load_int_immediate(ctx.emitter, "rdx", 0); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); - ctx.emitter.instruction("test rax, rax"); // did the aggregate object match the requested class? - ctx.emitter.instruction(&format!("je {}", throw)); // non-base downcast classes are rejected like PHP - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 24]"); // pass the requested class id to the interface checker + ctx.emitter.instruction("test rax, rax"); // did the aggregate object match the requested class? + ctx.emitter.instruction(&format!("je {}", throw)); // non-base downcast classes are rejected like PHP + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 24]"); // pass the requested class id to the interface checker abi::emit_load_int_immediate(ctx.emitter, "rsi", aggregate_interface_id); abi::emit_call_label(ctx.emitter, "__rt_class_implements_interface"); - ctx.emitter.instruction("test rax, rax"); // did the downcast class implement IteratorAggregate? - ctx.emitter.instruction(&format!("je {}", throw)); // non-Traversable base classes are rejected like PHP - ctx.emitter.instruction(&format!("jmp {}", skip)); // the aggregate downcast class is valid + ctx.emitter.instruction("test rax, rax"); // did the downcast class implement IteratorAggregate? + ctx.emitter.instruction(&format!("je {}", throw)); // non-Traversable base classes are rejected like PHP + ctx.emitter.instruction(&format!("jmp {}", skip)); // the aggregate downcast class is valid } } @@ -783,49 +783,49 @@ fn emit_validate_iterator_iterator_aggregate_downcast(ctx: &mut FunctionContext< fn emit_throw_iterator_iterator_downcast_logic_exception(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage + ctx.emitter.instruction("mov x0, #32"); // request Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks object instances - ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object + ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks object instances + ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp allocation as a runtime object abi::emit_symbol_address(ctx.emitter, "x9", "_spl_logic_exception_class_id"); - ctx.emitter.instruction("ldr x9, [x9]"); // load LogicException's runtime class id - ctx.emitter.instruction("str x9, [x0]"); // store the class id at object header + ctx.emitter.instruction("ldr x9, [x9]"); // load LogicException's runtime class id + ctx.emitter.instruction("str x9, [x0]"); // store the class id at object header abi::emit_symbol_address(ctx.emitter, "x9", "_iterator_iterator_downcast_msg"); - ctx.emitter.instruction("str x9, [x0, #8]"); // store static exception message pointer + ctx.emitter.instruction("str x9, [x0, #8]"); // store static exception message pointer ctx.emitter.instruction(&format!( "mov x9, #{}", ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len() )); // load static exception message length - ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length - ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero + ctx.emitter.instruction("str x9, [x0, #16]"); // store static exception message length + ctx.emitter.instruction("str xzr, [x0, #24]"); // exception code defaults to zero abi::emit_symbol_address(ctx.emitter, "x9", "_exc_value"); - ctx.emitter.instruction("str x0, [x9]"); // publish the active exception object - ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder + ctx.emitter.instruction("str x0, [x9]"); // publish the active exception object + ctx.emitter.instruction("b __rt_throw_current"); // enter the standard exception unwinder } Arch::X86_64 => { - ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation - ctx.emitter.instruction("mov rbp, rsp"); // establish an aligned helper frame - ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call aligned - ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage + ctx.emitter.instruction("push rbp"); // preserve caller frame pointer for exception allocation + ctx.emitter.instruction("mov rbp, rsp"); // establish an aligned helper frame + ctx.emitter.instruction("sub rsp, 16"); // keep the nested heap allocation call aligned + ctx.emitter.instruction("mov rax, 32"); // request Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap kind word - ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object + ctx.emitter.instruction("mov r10, 0x4548504c00000006"); // materialize the x86_64 object heap kind word + ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp allocation as a runtime object ctx.emitter .instruction("mov r10, QWORD PTR [rip + _spl_logic_exception_class_id]"); // load LogicException's runtime class id - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object header + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object header ctx.emitter .instruction("lea r10, [rip + _iterator_iterator_downcast_msg]"); // materialize static exception message pointer - ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static exception message pointer + ctx.emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store static exception message pointer ctx.emitter.instruction(&format!( "mov QWORD PTR [rax + 16], {}", ITERATOR_ITERATOR_DOWNCAST_MESSAGE.len() )); // store static exception message length - ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero + ctx.emitter.instruction("mov QWORD PTR [rax + 24], 0"); // exception code defaults to zero ctx.emitter .instruction("mov QWORD PTR [rip + _exc_value], rax"); // publish the active exception object - ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing - ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing - ctx.emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder + ctx.emitter.instruction("mov rsp, rbp"); // release helper frame before throwing + ctx.emitter.instruction("pop rbp"); // restore caller frame pointer before throwing + ctx.emitter.instruction("jmp __rt_throw_current"); // enter the standard exception unwinder } } } @@ -847,16 +847,16 @@ fn emit_branch_if_saved_traversable_implements( abi::emit_load_int_immediate(ctx.emitter, "x1", interface_id); abi::emit_load_int_immediate(ctx.emitter, "x2", 1); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); - ctx.emitter.instruction("cmp x0, #0"); // test whether the saved Traversable matches this interface - ctx.emitter.instruction(&format!("b.ne {}", target_label)); // select the matching IteratorIterator normalization path + ctx.emitter.instruction("cmp x0, #0"); // test whether the saved Traversable matches this interface + ctx.emitter.instruction(&format!("b.ne {}", target_label)); // select the matching IteratorIterator normalization path } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); abi::emit_load_int_immediate(ctx.emitter, "rsi", interface_id); abi::emit_load_int_immediate(ctx.emitter, "rdx", 1); abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); - ctx.emitter.instruction("test rax, rax"); // test whether the saved Traversable matches this interface - ctx.emitter.instruction(&format!("jne {}", target_label)); // select the matching IteratorIterator normalization path + ctx.emitter.instruction("test rax, rax"); // test whether the saved Traversable matches this interface + ctx.emitter.instruction(&format!("jne {}", target_label)); // select the matching IteratorIterator normalization path } } Ok(()) @@ -865,7 +865,7 @@ fn emit_branch_if_saved_traversable_implements( /// Moves the object result into the receiver ABI slot before an interface method call. fn move_result_to_receiver_arg(ctx: &mut FunctionContext<'_>) { if ctx.emitter.target.arch == Arch::X86_64 { - ctx.emitter.instruction("mov rdi, rax"); // pass the normalized object result as the method receiver + ctx.emitter.instruction("mov rdi, rax"); // pass the normalized object result as the method receiver } } @@ -983,23 +983,23 @@ fn class_declares_own_constructor(class_name: &str, class_info: &ClassInfo) -> b fn emit_throwable_allocation(ctx: &mut FunctionContext<'_>, class_id: u64) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #32"); // request compact Throwable payload storage + ctx.emitter.instruction("mov x0, #32"); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks runtime object payloads - ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the Throwable payload - ctx.emitter.instruction(&format!("mov x9, #{}", class_id)); // materialize the Throwable runtime class id - ctx.emitter.instruction("str x9, [x0]"); // store class id at payload offset zero + ctx.emitter.instruction("mov x9, #6"); // heap kind 6 marks runtime object payloads + ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the Throwable payload + ctx.emitter.instruction(&format!("mov x9, #{}", class_id)); // materialize the Throwable runtime class id + ctx.emitter.instruction("str x9, [x0]"); // store class id at payload offset zero } Arch::X86_64 => { - ctx.emitter.instruction("mov rax, 32"); // request compact Throwable payload storage + ctx.emitter.instruction("mov rax, 32"); // request compact Throwable payload storage abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); ctx.emitter.instruction(&format!( "mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 6 )); // materialize the x86_64 Throwable heap kind word - ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the Throwable payload - ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the Throwable runtime class id - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at payload offset zero + ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the Throwable payload + ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the Throwable runtime class id + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store class id at payload offset zero } } } @@ -1036,9 +1036,9 @@ fn emit_throwable_message_fields_aarch64( } else { emit_empty_string_to_regs(ctx, "x1", "x2"); } - ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for message initialization - ctx.emitter.instruction("str x1, [x9, #8]"); // store Throwable message pointer - ctx.emitter.instruction("str x2, [x9, #16]"); // store Throwable message length + ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for message initialization + ctx.emitter.instruction("str x1, [x9, #8]"); // store Throwable message pointer + ctx.emitter.instruction("str x2, [x9, #16]"); // store Throwable message length Ok(()) } @@ -1053,9 +1053,9 @@ fn emit_throwable_message_fields_x86_64( } else { emit_empty_string_to_regs(ctx, "rax", "rdx"); } - ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for message initialization - ctx.emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store Throwable message pointer - ctx.emitter.instruction("mov QWORD PTR [r11 + 16], rdx"); // store Throwable message length + ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for message initialization + ctx.emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store Throwable message pointer + ctx.emitter.instruction("mov QWORD PTR [r11 + 16], rdx"); // store Throwable message length Ok(()) } @@ -1084,8 +1084,8 @@ fn emit_throwable_code_field_aarch64( } else { abi::emit_load_int_immediate(ctx.emitter, "x1", 0); } - ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for code initialization - ctx.emitter.instruction("str x1, [x9, #24]"); // store Throwable code + ctx.emitter.instruction("ldr x9, [sp]"); // reload the saved Throwable object for code initialization + ctx.emitter.instruction("str x1, [x9, #24]"); // store Throwable code Ok(()) } @@ -1099,8 +1099,8 @@ fn emit_throwable_code_field_x86_64( } else { abi::emit_load_int_immediate(ctx.emitter, "rax", 0); } - ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for code initialization - ctx.emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store Throwable code + ctx.emitter.instruction("mov r11, QWORD PTR [rsp]"); // reload the saved Throwable object for code initialization + ctx.emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store Throwable code Ok(()) } @@ -1353,15 +1353,15 @@ fn emit_generic_dynamic_new_class_string( abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #1"); // require a boxed string class name for dynamic construction + ctx.emitter.instruction("cmp x0, #1"); // require a boxed string class name for dynamic construction ctx.emitter .instruction(&format!("b.ne {}", non_string_label)); // non-string class names produce the runtime null fallback } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 1"); // require a boxed string class name for dynamic construction + ctx.emitter.instruction("cmp rax, 1"); // require a boxed string class name for dynamic construction ctx.emitter .instruction(&format!("jne {}", non_string_label)); // non-string class names produce the runtime null fallback - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the string result register + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the string result register } } Ok(true) @@ -1569,8 +1569,8 @@ fn emit_branch_if_dynamic_new_mixed_class_name_matches( abi::emit_symbol_address(ctx.emitter, "x3", &candidate_label); abi::emit_load_int_immediate(ctx.emitter, "x4", candidate_len as i64); abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); - ctx.emitter.instruction("cmp x0, #0"); // check whether the dynamic class-string matches this AOT class - ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // select this AOT allocation path on a class-name match + ctx.emitter.instruction("cmp x0, #0"); // check whether the dynamic class-string matches this AOT class + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // select this AOT allocation path on a class-name match } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); @@ -1578,8 +1578,8 @@ fn emit_branch_if_dynamic_new_mixed_class_name_matches( abi::emit_symbol_address(ctx.emitter, "rdx", &candidate_label); abi::emit_load_int_immediate(ctx.emitter, "rcx", candidate_len as i64); abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); - ctx.emitter.instruction("test rax, rax"); // check whether the dynamic class-string matches this AOT class - ctx.emitter.instruction(&format!("je {}", matched_label)); // select this AOT allocation path on a class-name match + ctx.emitter.instruction("test rax, rax"); // check whether the dynamic class-string matches this AOT class + ctx.emitter.instruction(&format!("je {}", matched_label)); // select this AOT allocation path on a class-name match } } } @@ -1758,10 +1758,10 @@ fn emit_dynamic_new_mixed_fallback(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); abi::emit_call_label(ctx.emitter, "__rt_new_by_name"); - ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // registry miss is PHP's class-not-found fatal for source-level dynamic construction + ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // registry miss is PHP's class-not-found fatal for source-level dynamic construction abi::emit_release_temporary_stack(ctx.emitter, 16); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(String::new())); - ctx.emitter.instruction(&format!("b {}", done_label)); // skip the fatal path after a registry allocation + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the fatal path after a registry allocation ctx.emitter.label(&miss_label); emit_dynamic_new_class_not_found_fatal(ctx); ctx.emitter.label(&done_label); @@ -1770,11 +1770,11 @@ fn emit_dynamic_new_mixed_fallback(ctx: &mut FunctionContext<'_>) { abi::emit_load_temporary_stack_slot(ctx.emitter, "rax", 0); abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", 8); abi::emit_call_label(ctx.emitter, "__rt_new_by_name"); - ctx.emitter.instruction("test rax, rax"); // did the runtime class registry produce an object? - ctx.emitter.instruction(&format!("jz {}", miss_label)); // registry miss is PHP's class-not-found fatal + ctx.emitter.instruction("test rax, rax"); // did the runtime class registry produce an object? + ctx.emitter.instruction(&format!("jz {}", miss_label)); // registry miss is PHP's class-not-found fatal abi::emit_release_temporary_stack(ctx.emitter, 16); emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Object(String::new())); - ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the fatal path after a registry allocation + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the fatal path after a registry allocation ctx.emitter.label(&miss_label); emit_dynamic_new_class_not_found_fatal(ctx); ctx.emitter.label(&done_label); @@ -1998,17 +1998,17 @@ fn emit_dynamic_new_mixed_class_string(ctx: &mut FunctionContext<'_>, required_p abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic factory argument is a string - ctx.emitter.instruction(&format!("b.eq {}", string_label)); // continue only when the boxed factory argument is a class-string + ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic factory argument is a string + ctx.emitter.instruction(&format!("b.eq {}", string_label)); // continue only when the boxed factory argument is a class-string emit_dynamic_new_fatal(ctx, required_parent); ctx.emitter.label(&string_label); } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic factory argument is a string - ctx.emitter.instruction(&format!("je {}", string_label)); // continue only when the boxed factory argument is a class-string + ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic factory argument is a string + ctx.emitter.instruction(&format!("je {}", string_label)); // continue only when the boxed factory argument is a class-string emit_dynamic_new_fatal(ctx, required_parent); ctx.emitter.label(&string_label); - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the lookup input register + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the lookup input register } } } @@ -2017,16 +2017,16 @@ fn emit_dynamic_new_mixed_class_string(ctx: &mut FunctionContext<'_>, required_p fn emit_branch_if_dynamic_new_lookup_invalid(ctx: &mut FunctionContext<'_>, invalid_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #0"); // did the dynamic factory class-string resolve to metadata? - ctx.emitter.instruction(&format!("b.eq {}", invalid_label)); // abort unresolved factory classes before construction - ctx.emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface - ctx.emitter.instruction(&format!("b.ne {}", invalid_label)); // abort interface targets because factories instantiate objects + ctx.emitter.instruction("cmp x0, #0"); // did the dynamic factory class-string resolve to metadata? + ctx.emitter.instruction(&format!("b.eq {}", invalid_label)); // abort unresolved factory classes before construction + ctx.emitter.instruction("cmp x2, #0"); // target kind 0 means a concrete class, not an interface + ctx.emitter.instruction(&format!("b.ne {}", invalid_label)); // abort interface targets because factories instantiate objects } Arch::X86_64 => { - ctx.emitter.instruction("test rax, rax"); // did the dynamic factory class-string resolve to metadata? - ctx.emitter.instruction(&format!("je {}", invalid_label)); // abort unresolved factory classes before construction - ctx.emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface - ctx.emitter.instruction(&format!("jne {}", invalid_label)); // abort interface targets because factories instantiate objects + ctx.emitter.instruction("test rax, rax"); // did the dynamic factory class-string resolve to metadata? + ctx.emitter.instruction(&format!("je {}", invalid_label)); // abort unresolved factory classes before construction + ctx.emitter.instruction("test rdx, rdx"); // target kind 0 means a concrete class, not an interface + ctx.emitter.instruction(&format!("jne {}", invalid_label)); // abort interface targets because factories instantiate objects } } } @@ -2051,12 +2051,12 @@ fn emit_compare_dynamic_new_class_id( Arch::AArch64 => { ctx.emitter .instruction(&format!("cmp {}, #{}", scratch, class_id)); // compare the requested factory class with this candidate class id - ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // branch when the runtime class-string selected this constructor + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // branch when the runtime class-string selected this constructor } Arch::X86_64 => { ctx.emitter .instruction(&format!("cmp {}, {}", scratch, class_id)); // compare the requested factory class with this candidate class id - ctx.emitter.instruction(&format!("je {}", matched_label)); // branch when the runtime class-string selected this constructor + ctx.emitter.instruction(&format!("je {}", matched_label)); // branch when the runtime class-string selected this constructor } } } @@ -2110,37 +2110,37 @@ fn emit_dynamic_new_class_not_found_fatal(ctx: &mut FunctionContext<'_>) { let (suffix_label, suffix_len) = ctx.data.add_string(b"\" not found\n"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // select stderr for the class-not-found prefix + ctx.emitter.instruction("mov x0, #2"); // select stderr for the class-not-found prefix ctx.emitter.adrp("x1", &prefix_label); ctx.emitter.add_lo12("x1", "x1", &prefix_label); - ctx.emitter.instruction(&format!("mov x2, #{}", prefix_len)); // pass the class-not-found prefix byte length + ctx.emitter.instruction(&format!("mov x2, #{}", prefix_len)); // pass the class-not-found prefix byte length ctx.emitter.syscall(4); - ctx.emitter.instruction("mov x0, #2"); // select stderr for the missing class name + ctx.emitter.instruction("mov x0, #2"); // select stderr for the missing class name abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", 0); abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); ctx.emitter.syscall(4); - ctx.emitter.instruction("mov x0, #2"); // select stderr for the class-not-found suffix + ctx.emitter.instruction("mov x0, #2"); // select stderr for the class-not-found suffix ctx.emitter.adrp("x1", &suffix_label); ctx.emitter.add_lo12("x1", "x1", &suffix_label); - ctx.emitter.instruction(&format!("mov x2, #{}", suffix_len)); // pass the class-not-found suffix byte length + ctx.emitter.instruction(&format!("mov x2, #{}", suffix_len)); // pass the class-not-found suffix byte length ctx.emitter.syscall(4); } Arch::X86_64 => { abi::emit_symbol_address(ctx.emitter, "rsi", &prefix_label); - ctx.emitter.instruction(&format!("mov edx, {}", prefix_len)); // pass the class-not-found prefix byte length - ctx.emitter.instruction("mov edi, 2"); // select stderr for the class-not-found prefix - ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the prefix - ctx.emitter.instruction("syscall"); // write the class-not-found prefix + ctx.emitter.instruction(&format!("mov edx, {}", prefix_len)); // pass the class-not-found prefix byte length + ctx.emitter.instruction("mov edi, 2"); // select stderr for the class-not-found prefix + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the prefix + ctx.emitter.instruction("syscall"); // write the class-not-found prefix abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", 0); abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", 8); - ctx.emitter.instruction("mov edi, 2"); // select stderr for the missing class name - ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the class name - ctx.emitter.instruction("syscall"); // write the missing class name + ctx.emitter.instruction("mov edi, 2"); // select stderr for the missing class name + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the class name + ctx.emitter.instruction("syscall"); // write the missing class name abi::emit_symbol_address(ctx.emitter, "rsi", &suffix_label); - ctx.emitter.instruction(&format!("mov edx, {}", suffix_len)); // pass the class-not-found suffix byte length - ctx.emitter.instruction("mov edi, 2"); // select stderr for the class-not-found suffix - ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the suffix - ctx.emitter.instruction("syscall"); // write the class-not-found suffix + ctx.emitter.instruction(&format!("mov edx, {}", suffix_len)); // pass the class-not-found suffix byte length + ctx.emitter.instruction("mov edi, 2"); // select stderr for the class-not-found suffix + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall for the suffix + ctx.emitter.instruction("syscall"); // write the class-not-found suffix } } abi::emit_exit(ctx.emitter, 1); @@ -2159,7 +2159,7 @@ fn emit_fatal_message(ctx: &mut FunctionContext<'_>, message: &[u8]) { let (message_label, message_len) = ctx.data.add_string(message); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // select stderr for the fatal diagnostic + ctx.emitter.instruction("mov x0, #2"); // select stderr for the fatal diagnostic ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); ctx.emitter @@ -2170,9 +2170,9 @@ fn emit_fatal_message(ctx: &mut FunctionContext<'_>, message: &[u8]) { abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); ctx.emitter .instruction(&format!("mov edx, {}", message_len)); // pass the fatal diagnostic byte length to write() - ctx.emitter.instruction("mov edi, 2"); // select stderr for the fatal diagnostic - ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall - ctx.emitter.instruction("syscall"); // write the fatal diagnostic bytes + ctx.emitter.instruction("mov edi, 2"); // select stderr for the fatal diagnostic + ctx.emitter.instruction("mov eax, 1"); // select Linux write syscall + ctx.emitter.instruction("syscall"); // write the fatal diagnostic bytes } } abi::emit_exit(ctx.emitter, 1); @@ -2661,9 +2661,9 @@ fn lower_allow_dynamic_prop_get( abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); abi::emit_call_label(ctx.emitter, "__rt_hash_get"); - ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // missing dynamic properties read as PHP null - ctx.emitter.instruction("mov x0, x1"); // return the boxed Mixed cell stored in the hash entry - ctx.emitter.instruction(&format!("b {}", done_label)); // skip the null fallback after a successful dynamic-property hit + ctx.emitter.instruction(&format!("cbz x0, {}", miss_label)); // missing dynamic properties read as PHP null + ctx.emitter.instruction("mov x0, x1"); // return the boxed Mixed cell stored in the hash entry + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the null fallback after a successful dynamic-property hit } Arch::X86_64 => { ctx.emitter.instruction(&format!( @@ -2673,10 +2673,10 @@ fn lower_allow_dynamic_prop_get( abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); abi::emit_call_label(ctx.emitter, "__rt_hash_get"); - ctx.emitter.instruction("test rax, rax"); // check whether the dynamic-property key was present - ctx.emitter.instruction(&format!("je {}", miss_label)); // missing dynamic properties read as PHP null - ctx.emitter.instruction("mov rax, rdi"); // return the boxed Mixed cell stored in the hash entry - ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the null fallback after a successful dynamic-property hit + ctx.emitter.instruction("test rax, rax"); // check whether the dynamic-property key was present + ctx.emitter.instruction(&format!("je {}", miss_label)); // missing dynamic properties read as PHP null + ctx.emitter.instruction("mov rax, rdi"); // return the boxed Mixed cell stored in the hash entry + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the null fallback after a successful dynamic-property hit } } ctx.emitter.label(&miss_label); @@ -2835,14 +2835,14 @@ fn declared_mixed_property_candidates( fn emit_mixed_object_payload_or_null(ctx: &mut FunctionContext<'_>, null_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // check whether the Mixed receiver holds an object payload - ctx.emitter.instruction(&format!("b.ne {}", null_label)); // non-object Mixed receivers produce a null property result - ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object payload for class-id dispatch + ctx.emitter.instruction("cmp x0, #6"); // check whether the Mixed receiver holds an object payload + ctx.emitter.instruction(&format!("b.ne {}", null_label)); // non-object Mixed receivers produce a null property result + ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object payload for class-id dispatch } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // check whether the Mixed receiver holds an object payload - ctx.emitter.instruction(&format!("jne {}", null_label)); // non-object Mixed receivers produce a null property result - ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object payload for class-id dispatch + ctx.emitter.instruction("cmp rax, 6"); // check whether the Mixed receiver holds an object payload + ctx.emitter.instruction(&format!("jne {}", null_label)); // non-object Mixed receivers produce a null property result + ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object payload for class-id dispatch } } } @@ -2856,20 +2856,20 @@ fn emit_mixed_property_class_dispatch( ) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("ldr x9, [x0]"); // load the receiver class id for Mixed property dispatch + ctx.emitter.instruction("ldr x9, [x0]"); // load the receiver class id for Mixed property dispatch for (candidate, label) in candidates.iter().zip(match_labels.iter()) { abi::emit_load_int_immediate(ctx.emitter, "x10", candidate.class_id as i64); - ctx.emitter.instruction("cmp x9, x10"); // compare the receiver class id against this declared-property owner - ctx.emitter.instruction(&format!("b.eq {}", label)); // read the declared property when the class id matches + ctx.emitter.instruction("cmp x9, x10"); // compare the receiver class id against this declared-property owner + ctx.emitter.instruction(&format!("b.eq {}", label)); // read the declared property when the class id matches } emit_branch_to_stdclass_candidate(ctx, "x9", "x10", stdclass_label); } Arch::X86_64 => { - ctx.emitter.instruction("mov r11, QWORD PTR [rax]"); // load the receiver class id for Mixed property dispatch + ctx.emitter.instruction("mov r11, QWORD PTR [rax]"); // load the receiver class id for Mixed property dispatch for (candidate, label) in candidates.iter().zip(match_labels.iter()) { abi::emit_load_int_immediate(ctx.emitter, "r10", candidate.class_id as i64); - ctx.emitter.instruction("cmp r11, r10"); // compare the receiver class id against this declared-property owner - ctx.emitter.instruction(&format!("je {}", label)); // read the declared property when the class id matches + ctx.emitter.instruction("cmp r11, r10"); // compare the receiver class id against this declared-property owner + ctx.emitter.instruction(&format!("je {}", label)); // read the declared property when the class id matches } emit_branch_to_stdclass_candidate(ctx, "r11", "r10", stdclass_label); } @@ -2891,12 +2891,12 @@ fn emit_branch_to_stdclass_candidate( Arch::AArch64 => { ctx.emitter .instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage - ctx.emitter.instruction(&format!("b.eq {}", stdclass_label)); // route stdClass reads through the hash-backed helper + ctx.emitter.instruction(&format!("b.eq {}", stdclass_label)); // route stdClass reads through the hash-backed helper } Arch::X86_64 => { ctx.emitter .instruction(&format!("cmp {}, {}", class_id_reg, scratch_reg)); // check whether the object uses stdClass dynamic storage - ctx.emitter.instruction(&format!("je {}", stdclass_label)); // route stdClass reads through the hash-backed helper + ctx.emitter.instruction(&format!("je {}", stdclass_label)); // route stdClass reads through the hash-backed helper } } } @@ -2930,7 +2930,7 @@ fn emit_stdclass_get_from_loaded_object(ctx: &mut FunctionContext<'_>, property: abi::emit_call_label(ctx.emitter, "__rt_stdclass_get"); } Arch::X86_64 => { - ctx.emitter.instruction("mov rdi, rax"); // pass the unboxed stdClass object pointer to the dynamic getter + ctx.emitter.instruction("mov rdi, rax"); // pass the unboxed stdClass object pointer to the dynamic getter abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); abi::emit_call_label(ctx.emitter, "__rt_stdclass_get"); @@ -2942,12 +2942,12 @@ fn emit_stdclass_get_from_loaded_object(ctx: &mut FunctionContext<'_>, property: fn emit_branch_if_mixed_unboxed_object(ctx: &mut FunctionContext<'_>, object_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the boxed union holds an object payload - ctx.emitter.instruction(&format!("b.eq {}", object_label)); // read the declared property only for object payloads + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the boxed union holds an object payload + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // read the declared property only for object payloads } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the boxed union holds an object payload - ctx.emitter.instruction(&format!("je {}", object_label)); // read the declared property only for object payloads + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the boxed union holds an object payload + ctx.emitter.instruction(&format!("je {}", object_label)); // read the declared property only for object payloads } } } @@ -2956,10 +2956,10 @@ fn emit_branch_if_mixed_unboxed_object(ctx: &mut FunctionContext<'_>, object_lab fn move_mixed_unboxed_object_payload(ctx: &mut FunctionContext<'_>, base_reg: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov {}, x1", base_reg)); // use the unboxed object pointer as the declared-property base + ctx.emitter.instruction(&format!("mov {}, x1", base_reg)); // use the unboxed object pointer as the declared-property base } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov {}, rdi", base_reg)); // use the unboxed object pointer as the declared-property base + ctx.emitter.instruction(&format!("mov {}, rdi", base_reg)); // use the unboxed object pointer as the declared-property base } } } @@ -3112,18 +3112,63 @@ fn lower_runtime_dynamic_mixed_prop_get( property_value: ValueId, ) -> Result<()> { ensure_runtime_dynamic_property_name(ctx, property_value, inst)?; - match ctx.emitter.target.arch { - Arch::AArch64 => { - ctx.load_value_to_reg(object, "x0")?; - ctx.load_string_value_to_regs(property_value, "x1", "x2")?; - } - Arch::X86_64 => { - ctx.load_value_to_reg(object, "rdi")?; - ctx.load_string_value_to_regs(property_value, "rsi", "rdx")?; + ensure_dynamic_property_miss_supported(inst)?; + let candidates = declared_mixed_property_get_candidates(ctx, inst)?; + let done_label = ctx.next_label("mixed_dyn_prop_get_done"); + let miss_label = ctx.next_label("mixed_dyn_prop_get_miss"); + let miss_no_stack_label = ctx.next_label("mixed_dyn_prop_get_miss_no_stack"); + let stdclass_label = ctx.next_label("mixed_dyn_prop_get_stdclass"); + let match_labels = candidates + .iter() + .map(|candidate| { + ctx.next_label(&format!( + "mixed_dyn_prop_get_{}", + label_fragment(&candidate.slot.property) + )) + }) + .collect::>(); + + ctx.load_value_to_reg(object, abi::int_result_reg(ctx.emitter))?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_mixed_unboxed_not_object(ctx, &miss_no_stack_label); + push_mixed_unboxed_object_payload(ctx); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + ctx.load_string_value_to_regs(property_value, ptr_reg, len_reg)?; + abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); + + for (candidate, label) in candidates.iter().zip(match_labels.iter()) { + emit_branch_if_mixed_dynamic_property_candidate_matches(ctx, candidate, label); + } + emit_branch_if_stacked_object_is_stdclass(ctx, 16, &stdclass_label); + abi::emit_jump(ctx.emitter, &miss_label); + + for (candidate, label) in candidates.iter().zip(match_labels.iter()) { + ctx.emitter.label(label); + let base_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, base_reg, 16); + if candidate.slot.is_declared { + emit_uninitialized_typed_property_guard(ctx, &candidate.slot, base_reg); } + emit_property_load(ctx, &candidate.slot, base_reg)?; + materialize_loaded_property_result(ctx, inst, &candidate.slot.php_type)?; + abi::emit_release_temporary_stack(ctx.emitter, 32); + abi::emit_jump(ctx.emitter, &done_label); } - abi::emit_call_label(ctx.emitter, "__rt_mixed_property_get"); - cast_loaded_mixed_pointer_to_result(ctx, &inst.result_php_type.codegen_repr())?; + + ctx.emitter.label(&stdclass_label); + emit_runtime_stdclass_get_for_stacked_name(ctx, inst, 16, 0)?; + abi::emit_release_temporary_stack(ctx.emitter, 32); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&miss_label); + abi::emit_release_temporary_stack(ctx.emitter, 32); + emit_dynamic_property_miss_result(ctx, inst); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&miss_no_stack_label); + emit_dynamic_property_miss_result(ctx, inst); + + ctx.emitter.label(&done_label); store_if_result(ctx, inst) } @@ -3259,6 +3304,36 @@ fn declared_dynamic_property_slots( .collect() } +/// Collects declared-property candidates readable from a boxed Mixed receiver. +fn declared_mixed_property_get_candidates( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result> { + let mut candidates = Vec::new(); + let mut sorted_classes = ctx.module.class_infos.iter().collect::>(); + sorted_classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in sorted_classes { + if crate::types::checker::builtin_stdclass::is_stdclass(class_name) { + continue; + } + for (property, _) in &class_info.properties { + let Ok(slot) = resolve_property_slot_for_class(ctx, class_name, property, inst) else { + continue; + }; + candidates.push(MixedPropertyCandidate { + class_id: class_info.class_id, + slot, + }); + } + } + candidates.sort_by(|left, right| { + left.class_id + .cmp(&right.class_id) + .then_with(|| left.slot.property.cmp(&right.slot.property)) + }); + Ok(candidates) +} + /// Verifies that the EIR result type can receive every declared property candidate. fn ensure_dynamic_property_slot_results_supported( slots: &[PropertySlot], @@ -3345,7 +3420,7 @@ fn emit_branch_if_dynamic_name_matches( abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", 8); abi::emit_symbol_address(ctx.emitter, "x3", &label); abi::emit_load_int_immediate(ctx.emitter, "x4", len as i64); - ctx.emitter.instruction("bl __rt_str_eq"); // compare the runtime property name against this declared property + ctx.emitter.instruction("bl __rt_str_eq"); // compare the runtime property name against this declared property ctx.emitter .instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the declared property slot when the names match } @@ -3354,9 +3429,9 @@ fn emit_branch_if_dynamic_name_matches( abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", 8); abi::emit_symbol_address(ctx.emitter, "rdx", &label); abi::emit_load_int_immediate(ctx.emitter, "rcx", len as i64); - ctx.emitter.instruction("call __rt_str_eq"); // compare the runtime property name against this declared property - ctx.emitter.instruction("test rax, rax"); // check whether the runtime string comparison matched - ctx.emitter.instruction(&format!("jne {}", target_label)); // dispatch to the declared property slot when the names match + ctx.emitter.instruction("call __rt_str_eq"); // compare the runtime property name against this declared property + ctx.emitter.instruction("test rax, rax"); // check whether the runtime string comparison matched + ctx.emitter.instruction(&format!("jne {}", target_label)); // dispatch to the declared property slot when the names match } } } @@ -3562,7 +3637,7 @@ fn lower_runtime_mixed_prop_set( abi::emit_push_reg_pair(ctx.emitter, ptr_reg, len_reg); for (candidate, label) in candidates.iter().zip(match_labels.iter()) { - emit_branch_if_mixed_dynamic_set_candidate_matches(ctx, candidate, label); + emit_branch_if_mixed_dynamic_property_candidate_matches(ctx, candidate, label); } emit_branch_if_stacked_object_is_stdclass(ctx, 16, &stdclass_label); abi::emit_jump(ctx.emitter, &miss_label); @@ -3656,12 +3731,12 @@ fn declared_mixed_property_set_candidates( fn emit_branch_if_mixed_unboxed_not_object(ctx: &mut FunctionContext<'_>, target_label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // check whether the boxed receiver holds an object payload - ctx.emitter.instruction(&format!("b.ne {}", target_label)); // non-object dynamic property writes are ignored + ctx.emitter.instruction("cmp x0, #6"); // check whether the boxed receiver holds an object payload + ctx.emitter.instruction(&format!("b.ne {}", target_label)); // non-object dynamic property writes are ignored } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // check whether the boxed receiver holds an object payload - ctx.emitter.instruction(&format!("jne {}", target_label)); // non-object dynamic property writes are ignored + ctx.emitter.instruction("cmp rax, 6"); // check whether the boxed receiver holds an object payload + ctx.emitter.instruction(&format!("jne {}", target_label)); // non-object dynamic property writes are ignored } } } @@ -3675,7 +3750,7 @@ fn push_mixed_unboxed_object_payload(ctx: &mut FunctionContext<'_>) { } /// Branches when both the stacked object class id and runtime property name match. -fn emit_branch_if_mixed_dynamic_set_candidate_matches( +fn emit_branch_if_mixed_dynamic_property_candidate_matches( ctx: &mut FunctionContext<'_>, candidate: &MixedPropertyCandidate, matched_label: &str, @@ -3684,17 +3759,17 @@ fn emit_branch_if_mixed_dynamic_set_candidate_matches( match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", 16); - ctx.emitter.instruction("ldr x10, [x9]"); // load the candidate receiver class id + ctx.emitter.instruction("ldr x10, [x9]"); // load the candidate receiver class id abi::emit_load_int_immediate(ctx.emitter, "x11", candidate.class_id as i64); - ctx.emitter.instruction("cmp x10, x11"); // compare receiver class id before checking the property name - ctx.emitter.instruction(&format!("b.ne {}", next_label)); // skip name comparison for unrelated classes + ctx.emitter.instruction("cmp x10, x11"); // compare receiver class id before checking the property name + ctx.emitter.instruction(&format!("b.ne {}", next_label)); // skip name comparison for unrelated classes } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "r11", 16); - ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load the candidate receiver class id + ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load the candidate receiver class id abi::emit_load_int_immediate(ctx.emitter, "r12", candidate.class_id as i64); - ctx.emitter.instruction("cmp r10, r12"); // compare receiver class id before checking the property name - ctx.emitter.instruction(&format!("jne {}", next_label)); // skip name comparison for unrelated classes + ctx.emitter.instruction("cmp r10, r12"); // compare receiver class id before checking the property name + ctx.emitter.instruction(&format!("jne {}", next_label)); // skip name comparison for unrelated classes } } emit_branch_if_dynamic_name_matches(ctx, &candidate.slot.property, matched_label); @@ -3713,19 +3788,42 @@ fn emit_branch_if_stacked_object_is_stdclass( match ctx.emitter.target.arch { Arch::AArch64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "x9", object_stack_offset); - ctx.emitter.instruction("ldr x10, [x9]"); // load the stacked object's class id + ctx.emitter.instruction("ldr x10, [x9]"); // load the stacked object's class id abi::emit_load_int_immediate(ctx.emitter, "x11", stdclass_id as i64); - ctx.emitter.instruction("cmp x10, x11"); // check whether the runtime receiver is stdClass - ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // route stdClass writes through the dynamic-property helper + ctx.emitter.instruction("cmp x10, x11"); // check whether the runtime receiver is stdClass + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // route stdClass writes through the dynamic-property helper } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "r11", object_stack_offset); - ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load the stacked object's class id + ctx.emitter.instruction("mov r10, QWORD PTR [r11]"); // load the stacked object's class id abi::emit_load_int_immediate(ctx.emitter, "r12", stdclass_id as i64); - ctx.emitter.instruction("cmp r10, r12"); // check whether the runtime receiver is stdClass - ctx.emitter.instruction(&format!("je {}", matched_label)); // route stdClass writes through the dynamic-property helper + ctx.emitter.instruction("cmp r10, r12"); // check whether the runtime receiver is stdClass + ctx.emitter.instruction(&format!("je {}", matched_label)); // route stdClass writes through the dynamic-property helper + } + } +} + +/// Calls `__rt_stdclass_get` using a stacked object pointer and runtime name pair. +fn emit_runtime_stdclass_get_for_stacked_name( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object_stack_offset: usize, + name_stack_offset: usize, +) -> Result<()> { + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "x0", object_stack_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x1", name_stack_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x2", name_stack_offset + 8); + } + Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", object_stack_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rsi", name_stack_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, "rdx", name_stack_offset + 8); } } + abi::emit_call_label(ctx.emitter, "__rt_stdclass_get"); + cast_loaded_mixed_pointer_to_result(ctx, &inst.result_php_type.codegen_repr()) } /// Calls `__rt_stdclass_set` using a stacked object pointer and runtime name pair. @@ -3831,15 +3929,15 @@ fn lower_allow_dynamic_prop_set( materialize_dynamic_property_mixed_value(ctx, value, &value_ty)?; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("mov {}, x0", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore + ctx.emitter.instruction(&format!("mov {}, x0", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore abi::emit_pop_reg(ctx.emitter, object_reg); ctx.emitter .instruction(&format!("ldr x0, [{}, #{}]", object_reg, hash_offset)); // load the dynamic-property hash pointer from the receiver abi::emit_push_reg(ctx.emitter, object_reg); abi::emit_symbol_address(ctx.emitter, "x1", &label); abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); - ctx.emitter.instruction(&format!("mov x3, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload - ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use the high payload word + ctx.emitter.instruction(&format!("mov x3, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use the high payload word abi::emit_load_int_immediate( ctx.emitter, "x5", @@ -3850,7 +3948,7 @@ fn lower_allow_dynamic_prop_set( abi::emit_store_to_address(ctx.emitter, "x0", object_reg, hash_offset); } Arch::X86_64 => { - ctx.emitter.instruction(&format!("mov {}, rax", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore + ctx.emitter.instruction(&format!("mov {}, rax", boxed_reg)); // preserve the boxed dynamic-property value across receiver restore abi::emit_pop_reg(ctx.emitter, object_reg); ctx.emitter.instruction(&format!( "mov rdi, QWORD PTR [{} + {}]", @@ -3859,8 +3957,8 @@ fn lower_allow_dynamic_prop_set( abi::emit_push_reg(ctx.emitter, object_reg); abi::emit_symbol_address(ctx.emitter, "rsi", &label); abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); - ctx.emitter.instruction(&format!("mov rcx, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload - ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash entries do not use the high payload word + ctx.emitter.instruction(&format!("mov rcx, {}", boxed_reg)); // pass the boxed Mixed cell as the hash value payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash entries do not use the high payload word abi::emit_load_int_immediate( ctx.emitter, "r9", @@ -3926,7 +4024,7 @@ fn emit_property_assign_on_null_fatal(ctx: &mut FunctionContext<'_>, property: & let (message_label, message_len) = ctx.data.add_string(message.as_bytes()); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, #2"); // write the property-assign-on-null fatal to stderr + ctx.emitter.instruction("mov x0, #2"); // write the property-assign-on-null fatal to stderr ctx.emitter.adrp("x1", &message_label); ctx.emitter.add_lo12("x1", "x1", &message_label); ctx.emitter @@ -3935,12 +4033,12 @@ fn emit_property_assign_on_null_fatal(ctx: &mut FunctionContext<'_>, property: & abi::emit_exit(ctx.emitter, 1); } Arch::X86_64 => { - ctx.emitter.instruction("mov edi, 2"); // write the property-assign-on-null fatal to Linux stderr + ctx.emitter.instruction("mov edi, 2"); // write the property-assign-on-null fatal to Linux stderr abi::emit_symbol_address(ctx.emitter, "rsi", &message_label); ctx.emitter .instruction(&format!("mov edx, {}", message_len)); // pass the property-assign-on-null fatal byte length - ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write - ctx.emitter.instruction("syscall"); // emit the property-assign-on-null fatal before exiting + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the property-assign-on-null fatal before exiting abi::emit_exit(ctx.emitter, 1); } } @@ -4016,10 +4114,10 @@ fn emit_object_allocation( ctx.emitter .instruction(&format!("mov x0, #{}", payload_size)); // request object payload storage for the class id and property slots abi::emit_call_label(ctx.emitter, "__rt_heap_alloc"); - ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers - ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the object payload - ctx.emitter.instruction(&format!("mov x10, #{}", class_id)); // materialize the compile-time class id - ctx.emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero + ctx.emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers + ctx.emitter.instruction("str x9, [x0, #-8]"); // stamp the heap header before the object payload + ctx.emitter.instruction(&format!("mov x10, #{}", class_id)); // materialize the compile-time class id + ctx.emitter.instruction("str x10, [x0]"); // store the class id at object payload offset zero } Arch::X86_64 => { ctx.emitter @@ -4029,9 +4127,9 @@ fn emit_object_allocation( "mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 4 )); // materialize the x86_64 object heap kind word - ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the object payload - ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the compile-time class id - ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero + ctx.emitter.instruction("mov QWORD PTR [rax - 8], r10"); // stamp the heap header before the object payload + ctx.emitter.instruction(&format!("mov r10, {}", class_id)); // materialize the compile-time class id + ctx.emitter.instruction("mov QWORD PTR [rax], r10"); // store the class id at object payload offset zero } } let object_reg = abi::int_result_reg(ctx.emitter); @@ -4169,11 +4267,13 @@ fn emit_clone_dynamic_property_hash( abi::emit_load_from_address(ctx.emitter, result_reg, source_reg, offset); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cbz {}, {}", result_reg, null_label)); // missing dynamic-property hash clones as a null hash pointer + ctx.emitter + .instruction(&format!("cbz {}, {}", result_reg, null_label)); // missing dynamic-property hash clones as a null hash pointer } Arch::X86_64 => { - ctx.emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // check whether the source dynamic-property hash exists - ctx.emitter.instruction(&format!("jz {}", null_label)); // missing dynamic-property hash clones as a null hash pointer + ctx.emitter + .instruction(&format!("test {}, {}", result_reg, result_reg)); // check whether the source dynamic-property hash exists + ctx.emitter.instruction(&format!("jz {}", null_label)); // missing dynamic-property hash clones as a null hash pointer } } abi::emit_push_reg(ctx.emitter, source_reg); @@ -4217,7 +4317,7 @@ fn emit_dynamic_property_hash_init(ctx: &mut FunctionContext<'_>, object_reg: &s runtime_value_tag(&PhpType::Mixed) as i64, ); abi::emit_call_label(ctx.emitter, "__rt_hash_new"); - ctx.emitter.instruction(&format!("mov {}, x0", hash_reg)); // preserve the dynamic-property hash across object restore + ctx.emitter.instruction(&format!("mov {}, x0", hash_reg)); // preserve the dynamic-property hash across object restore } Arch::X86_64 => { abi::emit_load_int_immediate(ctx.emitter, "rdi", 4); @@ -4227,7 +4327,7 @@ fn emit_dynamic_property_hash_init(ctx: &mut FunctionContext<'_>, object_reg: &s runtime_value_tag(&PhpType::Mixed) as i64, ); abi::emit_call_label(ctx.emitter, "__rt_hash_new"); - ctx.emitter.instruction(&format!("mov {}, rax", hash_reg)); // preserve the dynamic-property hash across object restore + ctx.emitter.instruction(&format!("mov {}, rax", hash_reg)); // preserve the dynamic-property hash across object restore } } abi::emit_pop_reg(ctx.emitter, object_reg); @@ -4610,14 +4710,14 @@ pub(super) fn emit_nullable_receiver_object_payload( abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #8"); // check whether the nullable receiver holds PHP null - ctx.emitter.instruction(&format!("b.eq {}", null_label)); // short-circuit property access for nullsafe null receivers - ctx.emitter.instruction(&format!("mov {}, x1", object_reg)); // promote the unboxed object payload into the property base register + ctx.emitter.instruction("cmp x0, #8"); // check whether the nullable receiver holds PHP null + ctx.emitter.instruction(&format!("b.eq {}", null_label)); // short-circuit property access for nullsafe null receivers + ctx.emitter.instruction(&format!("mov {}, x1", object_reg)); // promote the unboxed object payload into the property base register } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 8"); // check whether the nullable receiver holds PHP null - ctx.emitter.instruction(&format!("je {}", null_label)); // short-circuit property access for nullsafe null receivers - ctx.emitter.instruction(&format!("mov {}, rdi", object_reg)); // promote the unboxed object payload into the property base register + ctx.emitter.instruction("cmp rax, 8"); // check whether the nullable receiver holds PHP null + ctx.emitter.instruction(&format!("je {}", null_label)); // short-circuit property access for nullsafe null receivers + ctx.emitter.instruction(&format!("mov {}, rdi", object_reg)); // promote the unboxed object payload into the property base register } } Ok(()) @@ -4970,8 +5070,8 @@ fn emit_property_load( } _ => { return Err(CodegenIrError::unsupported(format!( - "property load for PHP type {:?}", - slot.php_type + "property load for PHP type {:?}", + slot.php_type ))) } } @@ -5007,8 +5107,8 @@ fn emit_reference_property_load( } ty => { return Err(CodegenIrError::unsupported(format!( - "reference property load for PHP type {:?}", - ty + "reference property load for PHP type {:?}", + ty ))) } } @@ -5058,8 +5158,8 @@ fn emit_packed_field_load( } _ => { return Err(CodegenIrError::unsupported(format!( - "packed field load for PHP type {:?}", - slot.php_type + "packed field load for PHP type {:?}", + slot.php_type ))) } } @@ -5137,8 +5237,8 @@ fn emit_property_store( } _ => { return Err(CodegenIrError::unsupported(format!( - "property store for PHP type {:?}", - slot.php_type + "property store for PHP type {:?}", + slot.php_type ))) } } @@ -5324,8 +5424,8 @@ fn store_current_result_to_reference_cell( } ty => { return Err(CodegenIrError::unsupported(format!( - "reference property store for PHP type {:?}", - ty + "reference property store for PHP type {:?}", + ty ))) } } @@ -5483,8 +5583,8 @@ fn emit_packed_field_store( } _ => { return Err(CodegenIrError::unsupported(format!( - "packed field store for PHP type {:?}", - slot.php_type + "packed field store for PHP type {:?}", + slot.php_type ))) } } @@ -5664,20 +5764,20 @@ fn emit_mixed_instanceof_value_normalization(ctx: &mut FunctionContext<'_>) { abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the tested mixed payload is an object - ctx.emitter.instruction(&format!("b.eq {}", object_label)); // object payloads can be matched after dynamic target resolution - ctx.emitter.instruction("mov x0, #0"); // scalar mixed payloads become null so the matcher returns false - ctx.emitter.instruction(&format!("b {}", done)); // skip object-payload promotion for scalar payloads + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the tested mixed payload is an object + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // object payloads can be matched after dynamic target resolution + ctx.emitter.instruction("mov x0, #0"); // scalar mixed payloads become null so the matcher returns false + ctx.emitter.instruction(&format!("b {}", done)); // skip object-payload promotion for scalar payloads ctx.emitter.label(&object_label); - ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the normal result register + ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the normal result register } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the tested mixed payload is an object - ctx.emitter.instruction(&format!("je {}", object_label)); // object payloads can be matched after dynamic target resolution - ctx.emitter.instruction("xor eax, eax"); // scalar mixed payloads become null so the matcher returns false - ctx.emitter.instruction(&format!("jmp {}", done)); // skip object-payload promotion for scalar payloads + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the tested mixed payload is an object + ctx.emitter.instruction(&format!("je {}", object_label)); // object payloads can be matched after dynamic target resolution + ctx.emitter.instruction("xor eax, eax"); // scalar mixed payloads become null so the matcher returns false + ctx.emitter.instruction(&format!("jmp {}", done)); // skip object-payload promotion for scalar payloads ctx.emitter.label(&object_label); - ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the normal result register + ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the normal result register } } ctx.emitter.label(&done); @@ -5714,15 +5814,15 @@ fn emit_lookup_string_target(ctx: &mut FunctionContext<'_>, false_label: &str) { abi::emit_call_label(ctx.emitter, "__rt_instanceof_lookup"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #0"); // did the dynamic string resolve to a known class or interface? - ctx.emitter.instruction(&format!("b.eq {}", false_label)); // unresolved class-string targets make instanceof false - ctx.emitter.instruction("mov x0, x1"); // move the resolved target id into the matcher target-id register - ctx.emitter.instruction("mov x1, x2"); // move the resolved target kind into the matcher target-kind register + ctx.emitter.instruction("cmp x0, #0"); // did the dynamic string resolve to a known class or interface? + ctx.emitter.instruction(&format!("b.eq {}", false_label)); // unresolved class-string targets make instanceof false + ctx.emitter.instruction("mov x0, x1"); // move the resolved target id into the matcher target-id register + ctx.emitter.instruction("mov x1, x2"); // move the resolved target kind into the matcher target-kind register } Arch::X86_64 => { - ctx.emitter.instruction("test rax, rax"); // did the dynamic string resolve to a known class or interface? - ctx.emitter.instruction(&format!("je {}", false_label)); // unresolved class-string targets make instanceof false - ctx.emitter.instruction("mov rax, rdi"); // move the resolved target id into the matcher target-id register + ctx.emitter.instruction("test rax, rax"); // did the dynamic string resolve to a known class or interface? + ctx.emitter.instruction(&format!("je {}", false_label)); // unresolved class-string targets make instanceof false + ctx.emitter.instruction("mov rax, rdi"); // move the resolved target id into the matcher target-id register } } } @@ -5732,19 +5832,19 @@ fn emit_object_target_metadata(ctx: &mut FunctionContext<'_>) { let ok_label = ctx.next_label("instanceof_dynamic_object_target_ok"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cbnz x0, {}", ok_label)); // non-null object targets can provide runtime class metadata + ctx.emitter.instruction(&format!("cbnz x0, {}", ok_label)); // non-null object targets can provide runtime class metadata emit_invalid_dynamic_target_fatal(ctx); ctx.emitter.label(&ok_label); - ctx.emitter.instruction("ldr x0, [x0]"); // load the runtime class id from the target object header - ctx.emitter.instruction("mov x1, #0"); // object targets always resolve to class target kind + ctx.emitter.instruction("ldr x0, [x0]"); // load the runtime class id from the target object header + ctx.emitter.instruction("mov x1, #0"); // object targets always resolve to class target kind } Arch::X86_64 => { - ctx.emitter.instruction("test rax, rax"); // null object targets are not valid dynamic instanceof targets - ctx.emitter.instruction(&format!("jne {}", ok_label)); // non-null object targets can provide runtime class metadata + ctx.emitter.instruction("test rax, rax"); // null object targets are not valid dynamic instanceof targets + ctx.emitter.instruction(&format!("jne {}", ok_label)); // non-null object targets can provide runtime class metadata emit_invalid_dynamic_target_fatal(ctx); ctx.emitter.label(&ok_label); - ctx.emitter.instruction("mov rax, QWORD PTR [rax]"); // load the runtime class id from the target object header - ctx.emitter.instruction("xor edx, edx"); // object targets always resolve to class target kind + ctx.emitter.instruction("mov rax, QWORD PTR [rax]"); // load the runtime class id from the target object header + ctx.emitter.instruction("xor edx, edx"); // object targets always resolve to class target kind } } } @@ -5757,30 +5857,30 @@ fn emit_mixed_target_metadata(ctx: &mut FunctionContext<'_>, false_label: &str) abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string - ctx.emitter.instruction(&format!("b.eq {}", string_label)); // resolve boxed string targets through class-string lookup - ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object - ctx.emitter.instruction(&format!("b.eq {}", object_label)); // resolve boxed object targets through their runtime class id + ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter.instruction(&format!("b.eq {}", string_label)); // resolve boxed string targets through class-string lookup + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // resolve boxed object targets through their runtime class id emit_invalid_dynamic_target_fatal(ctx); ctx.emitter.label(&string_label); emit_lookup_string_target(ctx, false_label); abi::emit_jump(ctx.emitter, &done); ctx.emitter.label(&object_label); - ctx.emitter.instruction("mov x0, x1"); // move the unboxed target object pointer into the result register + ctx.emitter.instruction("mov x0, x1"); // move the unboxed target object pointer into the result register emit_object_target_metadata(ctx); } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string - ctx.emitter.instruction(&format!("je {}", string_label)); // resolve boxed string targets through class-string lookup - ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object - ctx.emitter.instruction(&format!("je {}", object_label)); // resolve boxed object targets through their runtime class id + ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter.instruction(&format!("je {}", string_label)); // resolve boxed string targets through class-string lookup + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter.instruction(&format!("je {}", object_label)); // resolve boxed object targets through their runtime class id emit_invalid_dynamic_target_fatal(ctx); ctx.emitter.label(&string_label); - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed target string pointer into the lookup input register + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed target string pointer into the lookup input register emit_lookup_string_target(ctx, false_label); abi::emit_jump(ctx.emitter, &done); ctx.emitter.label(&object_label); - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed target object pointer into the result register + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed target object pointer into the result register emit_object_target_metadata(ctx); } } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index b155ce9a43..fd1ea76e64 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9773,6 +9773,9 @@ fn dynamic_property_get_result_type( return property_get_result_type(ctx, object, name, Op::DynamicPropGet, expr); } let object_ty = ctx.builder.value_php_type(object); + if matches!(object_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + return PhpType::Mixed; + } let Some((class_name, nullable)) = singular_object_class(&object_ty) else { return fallback_expr_type(expr); }; diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index b60510b859..8059c026af 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2127,6 +2127,11 @@ fn throw_new_reflection_exception(message: Expr, span: crate::span::Span) -> Stm ) } +/// Builds a `null` expression for synthetic Reflection method bodies. +fn null_value(span: crate::span::Span) -> Expr { + Expr::new(ExprKind::Null, span) +} + /// Builds `$this->{$property}` for synthetic ReflectionClass method bodies. fn reflection_this_property(property: &str, span: crate::span::Span) -> Expr { Expr::new( @@ -2577,9 +2582,10 @@ fn builtin_reflection_property_modifier_mask_method(method_name: &str, mask: i64 } } -/// Returns the fallback `ReflectionProperty::getValue()` body for unsupported dynamic receivers. +/// Returns `ReflectionProperty::getValue()` for dynamic public instance reflectors. fn builtin_reflection_property_get_value_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); + let object = variable_expr("object", dummy_span); ClassMethod { name: "getValue".to_string(), visibility: Visibility::Public, @@ -2587,31 +2593,29 @@ fn builtin_reflection_property_get_value_method() -> ClassMethod { is_abstract: false, is_final: false, has_body: true, - params: vec![( - "object".to_string(), - Some(mixed_type()), - null_expr(), - false, - )], + params: vec![("object".to_string(), Some(mixed_type()), null_expr(), false)], param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(mixed_type()), - body: vec![throw_new_reflection_exception( - string_lit( - "ReflectionProperty::getValue() requires an inline known public instance or static property", + body: vec![ + reflection_property_static_value_guard("getValue", dummy_span), + reflection_property_object_required_guard("getValue", dummy_span), + Stmt::new( + StmtKind::Return(Some(reflection_dynamic_object_property(object, dummy_span))), dummy_span, ), - dummy_span, - )], + ], span: dummy_span, attributes: Vec::new(), } } -/// Returns the fallback `ReflectionProperty::setValue()` body for unsupported dynamic receivers. +/// Returns `ReflectionProperty::setValue()` for dynamic public instance reflectors. fn builtin_reflection_property_set_value_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); + let object = variable_expr("object", dummy_span); + let value = variable_expr("value", dummy_span); ClassMethod { name: "setValue".to_string(), visibility: Visibility::Public, @@ -2627,18 +2631,88 @@ fn builtin_reflection_property_set_value_method() -> ClassMethod { variadic: None, variadic_type: None, return_type: Some(TypeExpr::Void), - body: vec![throw_new_reflection_exception( - string_lit( - "ReflectionProperty::setValue() requires an inline known public instance or static property", + body: vec![ + reflection_property_static_value_guard("setValue", dummy_span), + reflection_property_object_required_guard("setValue", dummy_span), + Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::Assignment { + target: Box::new(reflection_dynamic_object_property(object, dummy_span)), + value: Box::new(value), + result_target: None, + prelude: Vec::new(), + conditional_value_temp: None, + }, + dummy_span, + )), dummy_span, ), - dummy_span, - )], + ], span: dummy_span, attributes: Vec::new(), } } +/// Builds a guard for static property value access that still needs inline lowering. +fn reflection_property_static_value_guard(method: &str, span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_static", span), + then_body: vec![throw_new_reflection_exception( + string_lit( + &format!( + "ReflectionProperty::{}() for static properties requires an inline known public static property", + method + ), + span, + ), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds a guard requiring an object argument for instance property value access. +fn reflection_property_object_required_guard(method: &str, span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: binary_expr( + variable_expr("object", span), + BinOp::StrictEq, + null_value(span), + span, + ), + then_body: vec![throw_new_reflection_exception( + string_lit( + &format!( + "ReflectionProperty::{}() requires an object for instance properties", + method + ), + span, + ), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds `$object->{$this->__name}` for ReflectionProperty value access. +fn reflection_dynamic_object_property(object: Expr, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::DynamicPropertyAccess { + object: Box::new(object), + property: Box::new(reflection_this_property("__name", span)), + }, + span, + ) +} + /// Returns `ReflectionProperty::isLazy()` for the non-lazy property model elephc supports. fn builtin_reflection_property_is_lazy_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index c53dc4090f..7f855a67c2 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -7,6 +7,7 @@ //! //! Key details: //! - Covers explicit object arguments for public instance properties. +//! - Covers runtime-held public instance property reflectors. //! - Covers static properties where PHP permits no object argument. //! - Visibility-bypassing reflection access remains a separate surface. @@ -67,3 +68,30 @@ echo ":" . ReflectStaticValueAccessTarget::$count; ); assert_eq!(out, "2:17:19:old:new:23"); } + +/// Verifies runtime-held `ReflectionProperty` objects can read and write public +/// instance properties through their retained property names. +#[test] +fn test_reflection_property_value_accessors_for_runtime_instance_reflectors() { + let out = compile_and_run( + r#"getValue($target); +$count->setValue($target, 8); +echo ":" . $target->count; + +$listed = (new ReflectionClass(ReflectRuntimeValueAccessTarget::class))->getProperties()[1]; +echo ":" . $listed->getName(); +echo ":" . $listed->getValue($target); +$listed->setValue($target, "new"); +echo ":" . $target->label; +"#, + ); + assert_eq!(out, "4:8:label:old:new"); +} From 99edd9e2f6fed72c6bb79778c9e8e049119c93c4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 08:41:28 +0200 Subject: [PATCH 0535/1208] Cover ReflectionProperty visibility bypass --- docs/php/classes.md | 6 ++--- tests/codegen/oop/reflection_properties.rs | 28 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 45cf5430a2..ce66ccacb9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1253,8 +1253,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | -| `ReflectionProperty::getValue()` | `ReflectionProperty` for public instance properties with an explicit object argument, or inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for public static properties | Read supported public instance/static property storage; positional and named arguments are normalized | -| `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported public instance/static property storage | +| `ReflectionProperty::getValue()` | `ReflectionProperty` for instance properties with an explicit object argument, or inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for public static properties | Read supported instance/static property storage; positional and named arguments are normalized | +| `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported instance/static property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | @@ -1344,7 +1344,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for public instance-property reflectors with explicit positional or named object/value arguments, including reflector objects held in variables and entries returned from `ReflectionClass::getProperties()`. Public static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers with omitted/ignored object arguments. Full PHP visibility-bypassing access and dynamic static-property lookup from reflector objects held only in runtime storage are not yet available. +- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Public static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers with omitted/ignored object arguments. Dynamic static-property lookup from reflector objects held only in runtime storage and non-public static-property value access are not yet available. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 7f855a67c2..32cd2a6da3 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -6,7 +6,7 @@ //! - `cargo test` through Rust's test harness. //! //! Key details: -//! - Covers explicit object arguments for public instance properties. +//! - Covers explicit object arguments for public and non-public instance properties. //! - Covers runtime-held public instance property reflectors. //! - Covers static properties where PHP permits no object argument. //! - Visibility-bypassing reflection access remains a separate surface. @@ -95,3 +95,29 @@ echo ":" . $target->label; ); assert_eq!(out, "4:8:label:old:new"); } + +/// Verifies ReflectionProperty value access bypasses visibility for private +/// and protected instance properties, matching PHP's Reflection behavior. +#[test] +fn test_reflection_property_value_accessors_bypass_instance_visibility() { + let out = compile_and_run( + r#"getValue($target); +$count->setValue($target, 8); +echo ":" . $count->getValue($target); + +$label = (new ReflectionClass(ReflectHiddenValueAccessTarget::class))->getProperty("label"); +echo ":" . $label->getValue($target); +$label->setValue($target, "new"); +echo ":" . $label->getValue($target); +"#, + ); + assert_eq!(out, "4:8:old:new"); +} From c590fa2a7f136c7a2b9869081565f822d783553a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 09:22:27 +0200 Subject: [PATCH 0536/1208] Avoid unused enum singleton initialization --- src/codegen/block_emit.rs | 4 +- src/codegen/mod.rs | 187 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 2 deletions(-) diff --git a/src/codegen/block_emit.rs b/src/codegen/block_emit.rs index cb80d7355a..c6198c5501 100644 --- a/src/codegen/block_emit.rs +++ b/src/codegen/block_emit.rs @@ -801,11 +801,11 @@ fn is_main(function: &Function) -> bool { /// Emits global singleton objects for enum cases used by EIR user code. fn emit_enum_singleton_initializers(ctx: &mut FunctionContext<'_>) { - let allowed_class_names = super::runtime_referenced_class_names(ctx.module); + let allowed_enum_names = super::runtime_referenced_enum_singleton_names(ctx.module); let mut sorted_enums = ctx.module.enum_infos.iter().collect::>(); sorted_enums.sort_by_key(|(name, _)| name.as_str()); for (enum_name, enum_info) in sorted_enums { - if !allowed_class_names.contains(enum_name) { + if !allowed_enum_names.contains(enum_name) { continue; } let Some(class_info) = ctx.module.class_infos.get(enum_name) else { diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index ef8030b882..a5fa7ebbaa 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -528,6 +528,193 @@ fn runtime_referenced_class_names(module: &Module) -> HashSet { names } +/// Returns enum names whose singleton case slots must be allocated before main runs. +fn runtime_referenced_enum_singleton_names(module: &Module) -> HashSet { + let mut names = HashSet::new(); + for function in all_runtime_scanned_functions(module) { + for inst in &function.instructions { + collect_scoped_constant_enum_singleton_name(module, inst, &mut names); + collect_static_method_enum_singleton_name(module, function, inst, &mut names); + collect_reflection_enum_singleton_name(module, function, inst, &mut names); + } + } + names +} + +/// Iterates all function-like EIR bodies considered by runtime metadata scanners. +fn all_runtime_scanned_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Adds enum names referenced by `Enum::Case` scoped constant reads. +fn collect_scoped_constant_enum_singleton_name( + module: &Module, + inst: &crate::ir::Instruction, + names: &mut HashSet, +) { + if !matches!(inst.op, Op::ScopedConstantGet) { + return; + } + let Some(label) = data_string_immediate(module, inst) else { + return; + }; + let Some((class_name, case_name)) = label.rsplit_once("::") else { + return; + }; + let Some(enum_name) = canonical_module_enum_name(module, class_name) else { + return; + }; + if module + .enum_infos + .get(&enum_name) + .is_some_and(|info| info.cases.iter().any(|case| case.name == case_name)) + { + names.insert(enum_name); + } +} + +/// Adds enum names referenced through `cases()`, `from()`, or `tryFrom()`. +fn collect_static_method_enum_singleton_name( + module: &Module, + function: &Function, + inst: &crate::ir::Instruction, + names: &mut HashSet, +) { + if !matches!(inst.op, Op::StaticMethodCall) { + return; + } + let Some(label) = data_string_immediate(module, inst) else { + return; + }; + let Some((receiver, method_name)) = label.rsplit_once("::") else { + return; + }; + let Some(receiver) = resolve_static_method_metadata_class(module, function, receiver) else { + return; + }; + let Some(enum_name) = canonical_module_enum_name(module, &receiver) else { + return; + }; + if enum_static_method_needs_singletons(method_name) { + names.insert(enum_name); + } +} + +/// Adds enum names whose case values can be materialized by known Reflection objects. +fn collect_reflection_enum_singleton_name( + module: &Module, + function: &Function, + inst: &crate::ir::Instruction, + names: &mut HashSet, +) { + if !matches!(inst.op, Op::ObjectNew) || inst.operands.is_empty() { + return; + } + let Some(class_name) = class_name_immediate(module, inst) else { + return; + }; + if !reflection_constructor_can_materialize_enum_case(class_name) { + return; + } + let Some(reflected_name) = const_class_like_name_value(module, function, inst.operands[0]) + else { + return; + }; + if let Some(enum_name) = canonical_module_enum_name(module, reflected_name) { + names.insert(enum_name); + } +} + +/// Returns true for enum static helpers that load one or more singleton case objects. +fn enum_static_method_needs_singletons(method_name: &str) -> bool { + matches!( + php_symbol_key(method_name).as_str(), + "cases" | "from" | "tryfrom" + ) +} + +/// Returns true for Reflection constructors whose methods can expose enum case values. +fn reflection_constructor_can_materialize_enum_case(class_name: &str) -> bool { + matches!( + php_symbol_key(class_name.trim_start_matches('\\')).as_str(), + "reflectionclass" + | "reflectionclassconstant" + | "reflectionenumunitcase" + | "reflectionenumbackedcase" + ) +} + +/// Returns a data-string immediate attached to an EIR instruction. +fn data_string_immediate<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { + let Some(Immediate::Data(data)) = inst.immediate else { + return None; + }; + module + .data + .strings + .get(data.as_raw() as usize) + .map(String::as_str) +} + +/// Returns a class-name immediate attached to an EIR instruction. +fn class_name_immediate<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { + let Some(Immediate::Data(data)) = inst.immediate else { + return None; + }; + module + .data + .class_names + .get(data.as_raw() as usize) + .map(String::as_str) +} + +/// Returns a compile-time class-like name from a string or `::class` value. +fn const_class_like_name_value<'a>( + module: &'a Module, + function: &'a Function, + value: crate::ir::ValueId, +) -> Option<&'a str> { + let value_ref = function.value(value)?; + let ValueDef::Instruction { inst, .. } = value_ref.def else { + return None; + }; + let inst_ref = function.instruction(inst)?; + let Some(Immediate::Data(data)) = inst_ref.immediate else { + return None; + }; + match inst_ref.op { + Op::ConstClassName => module + .data + .class_names + .get(data.as_raw() as usize) + .map(String::as_str), + Op::ConstStr => module + .data + .strings + .get(data.as_raw() as usize) + .map(String::as_str), + _ => None, + } +} + +/// Resolves an enum name against module metadata using PHP case-insensitive rules. +fn canonical_module_enum_name(module: &Module, enum_name: &str) -> Option { + let wanted = php_symbol_key(enum_name.trim_start_matches('\\')); + module + .enum_infos + .keys() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == wanted) + .cloned() +} + /// Adds builtin throwable classes that runtime helpers can materialize without EIR class references. fn seed_runtime_throwable_class_names(module: &Module, names: &mut HashSet) { if names.contains("Fiber") && module.class_infos.contains_key("FiberError") { From 9dcb8379505a678f813421d065adaf7dc9e891bf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 09:52:25 +0200 Subject: [PATCH 0537/1208] Support non-public AOT clone hooks in eval --- .../elephc-eval/src/interpreter/reflection.rs | 16 ++- .../elephc-eval/src/interpreter/statements.rs | 43 +++++- docs/php/eval.md | 15 +-- src/codegen/eval_method_helpers.rs | 26 ++-- tests/codegen/eval.rs | 122 ++++++++++++++++++ 5 files changed, 195 insertions(+), 27 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index defe4c87f4..864cf8442b 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -2398,10 +2398,20 @@ pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata( class_name: &str, method_name: &str, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { Ok( - eval_reflection_aot_method_metadata_if_exists(class_name, method_name, values)? - .map(|member| (member.visibility, member.is_static, member.is_abstract)), + eval_reflection_aot_method_metadata_if_exists(class_name, method_name, values)?.map( + |member| { + ( + member + .declaring_class_name + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()), + member.visibility, + member.is_static, + member.is_abstract, + ) + }, + ), ) } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index f728a6899a..5a1120d90b 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3069,7 +3069,7 @@ pub(in crate::interpreter) fn eval_object_clone_result( validate_eval_member_access(declaring_class, method.visibility(), context)?; } let should_call_aot_clone_hook = if dynamic_class_name.is_none() { - eval_aot_clone_hook_is_callable(object, values)? + eval_aot_clone_hook_is_callable(object, context, values)? } else { false }; @@ -3097,20 +3097,22 @@ pub(in crate::interpreter) fn eval_object_clone_result( Ok(clone) } -/// Returns whether a public instance AOT `__clone()` hook should run. +/// Returns whether an accessible instance AOT `__clone()` hook should run. fn eval_aot_clone_hook_is_callable( object: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let class_name = eval_runtime_object_class_name(object, values)?; - let Some((visibility, is_static, is_abstract)) = + let Some((declaring_class, visibility, is_static, is_abstract)) = eval_aot_method_dispatch_metadata(&class_name, "__clone", values)? else { return Ok(false); }; - if visibility != EvalVisibility::Public || is_static || is_abstract { + if is_static || is_abstract { return Err(EvalStatus::RuntimeFatal); } + validate_eval_member_access(&declaring_class, visibility, context)?; Ok(true) } @@ -3516,6 +3518,16 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( } let Some(class) = context.dynamic_object_class(identity) else { let class_name = runtime_object_class_name(object, values)?; + if method_name.eq_ignore_ascii_case("__clone") { + if let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&class_name, method_name, values)? + { + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, visibility, context)?; + } + } let evaluated_args = bind_native_callable_args( context.native_method_signature(&class_name, method_name), evaluated_args, @@ -3851,11 +3863,32 @@ fn same_eval_class_name(left: &str, right: &str) -> bool { .eq_ignore_ascii_case(right.trim_start_matches('\\')) } -/// Returns true when two eval classes are in the same inheritance family. +/// Returns true when two eval or generated classes are in the same inheritance family. fn eval_classes_are_related(left: &str, right: &str, context: &ElephcEvalContext) -> bool { same_eval_class_name(left, right) || context.class_is_a(left, right, false) || context.class_is_a(right, left, false) + || native_class_is_a(left, right, context) + || native_class_is_a(right, left, context) +} + +/// Returns true when generated AOT parent metadata proves one class extends another. +fn native_class_is_a(class_name: &str, target: &str, context: &ElephcEvalContext) -> bool { + let mut current = class_name.trim_start_matches('\\').to_string(); + let target = target.trim_start_matches('\\'); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return false; + } + if same_eval_class_name(¤t, target) { + return true; + } + let Some(parent) = context.native_class_parent(¤t) else { + return false; + }; + current = parent.to_string(); + } } /// Binds method parameters into a fresh method scope and marks by-reference params as aliases. diff --git a/docs/php/eval.md b/docs/php/eval.md index fd13cb7e76..1eddac1018 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar or null default arguments for supported public scalar/Mixed constructor signatures. | -| Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared `__clone()` hooks run after the copy and obey public/protected/private visibility; public emitted/AOT `__clone()` hooks run through the method bridge. | +| Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar or null default arguments for supported public scalar/Mixed/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | @@ -453,8 +453,9 @@ during eval object construction fail with an eval runtime fatal diagnostic. `clone $object` creates a shallow copy for eval-declared objects, `stdClass` objects, and ordinary emitted/AOT objects. Eval `__clone()` hooks are invoked on the cloned object after storage copying and use the same runtime visibility -checks as method calls. Public emitted/AOT `__clone()` hooks are invoked through -the generated method bridge after the clone storage has been copied. +checks as method calls. Emitted/AOT `__clone()` hooks, including non-public hooks +visible from the current eval class scope, are invoked through the generated +method bridge after the clone storage has been copied. AOT and eval-declared class-name probes are visible through `class_exists()`. Eval object relation probes through `instanceof`, `is_a()`, and `is_subclass_of()` use @@ -617,13 +618,11 @@ property, and function/method return metadata, broader parameter and generated property default-value materialization beyond the eval-supported constant-expression subset, object-valued defaults in generated metadata, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed/object slice. Generated/AOT method type +non-by-reference fixed scalar/Mixed/object slice plus visibility-checked +`__clone()` hooks. Generated/AOT method type metadata and generated/AOT method/property attributes are exposed for registered metadata slices, while broader non-public or unsupported bridge shapes remain -outside that slice. Eval object cloning covers ordinary -emitted/AOT storage and public AOT `__clone()` hooks, but non-public AOT -`__clone()` scope checks and broader bridge signatures remain outside that -bridge slice. +outside that slice. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 5cf50898ff..a22f26f5be 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -1,6 +1,6 @@ //! Purpose: -//! Emits user-assembly helpers that let libelephc-eval call public native -//! instance and static methods known to the current module. +//! Emits user-assembly helpers that let libelephc-eval call native instance and +//! static methods known to the current module. //! //! Called from: //! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. @@ -8,9 +8,8 @@ //! Key details: //! - The cacheable runtime object cannot know user class ids, method symbols, //! or return types, so this bridge is emitted into the user assembly. -//! - This method-call slice supports public AOT methods with fixed non-by-ref -//! scalar/Mixed/object argument lists and reports unsupported calls as -//! runtime failure. +//! - This method-call slice supports public AOT methods plus non-public +//! `__clone` hooks after interpreter-side visibility checks. use std::collections::BTreeMap; @@ -20,7 +19,7 @@ use crate::codegen::emit::Emitter; use crate::codegen::emit_box_current_value_as_mixed; use crate::codegen::platform::Arch; use crate::ir::{Function, LocalKind, Module}; -use crate::names::{method_symbol, static_method_symbol}; +use crate::names::{method_symbol, php_symbol_key, static_method_symbol}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -116,7 +115,7 @@ fn function_uses_eval(function: &Function) -> bool { }) } -/// Collects public bridge-supported instance methods backed by emitted EIR symbols. +/// Collects bridge-supported instance methods backed by emitted EIR symbols. fn collect_eval_method_slots(module: &Module) -> Vec { let emitted_methods = super::eir_class_method_keys(module); let mut slots = Vec::new(); @@ -152,7 +151,7 @@ fn collect_builtin_throwable_method_class_ids(module: &Module) -> Vec { class_ids } -/// Adds bridge-supported public methods for one class. +/// Adds bridge-supported instance methods for one class. fn collect_class_method_slots( class_name: &str, class_info: &ClassInfo, @@ -162,7 +161,7 @@ fn collect_class_method_slots( let mut methods = class_info.methods.iter().collect::>(); methods.sort_by_key(|(method, _)| method.as_str()); for (method, sig) in methods { - if !method_is_public(class_info, method) + if !method_can_dispatch_through_eval_bridge(class_info, method) || !method_signature_supported(sig) || !method_return_supported(&sig.return_type) { @@ -222,6 +221,11 @@ fn collect_class_static_method_slots( } } +/// Returns true when an instance method can be reached through eval's native bridge. +fn method_can_dispatch_through_eval_bridge(class_info: &ClassInfo, method: &str) -> bool { + method_is_public(class_info, method) || php_symbol_key(method) == "__clone" +} + /// Returns true when a method is publicly visible to runtime eval. fn method_is_public(class_info: &ClassInfo, method: &str) -> bool { class_info @@ -397,7 +401,7 @@ fn emit_method_call_aarch64( builtin_throwable_class_ids, ); emit_aarch64_method_dispatch(module, emitter, data, slots); - emitter.instruction(&format!("b {}", fail_label)); // no supported public method matched the request + emitter.instruction(&format!("b {}", fail_label)); // no supported method matched the request emit_aarch64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); emit_aarch64_method_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); @@ -438,7 +442,7 @@ fn emit_method_call_x86_64( builtin_throwable_class_ids, ); emit_x86_64_method_dispatch(module, emitter, data, slots); - emitter.instruction(&format!("jmp {}", fail_label)); // no supported public method matched the request + emitter.instruction(&format!("jmp {}", fail_label)); // no supported method matched the request emit_x86_64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); emit_x86_64_method_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2a0f0f205c..2431e04787 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9617,6 +9617,128 @@ echo $copy->name;'); assert_eq!(out, "A:A:hook"); } +/// Verifies eval `clone` invokes private AOT `__clone()` hooks from the declaring scope. +#[test] +fn test_eval_clone_aot_object_runs_private_clone_hook_in_scope() { + let out = compile_and_run( + r#"name = $name; + } + + private function __clone(): void { + $this->name = $this->name . ":private"; + } + + public function run(): void { + eval('$copy = clone $this; +echo $this->name; echo ":"; +echo $copy->name;'); + } +} + +(new EvalCloneAotPrivateHookBox("A"))->run(); +"#, + ); + assert_eq!(out, "A:A:private"); +} + +/// Verifies eval `clone` invokes protected AOT `__clone()` hooks from child scopes. +#[test] +fn test_eval_clone_aot_object_runs_protected_clone_hook_in_child_scope() { + let out = compile_and_run( + r#"name = $name; + } + + protected function __clone(): void { + $this->name = $this->name . ":protected"; + } +} + +class EvalCloneAotProtectedHookChild extends EvalCloneAotProtectedHookBase { + public function run(): void { + eval('$copy = clone $this; +echo $this->name; echo ":"; +echo $copy->name;'); + } +} + +(new EvalCloneAotProtectedHookChild("B"))->run(); +"#, + ); + assert_eq!(out, "B:B:protected"); +} + +/// Verifies eval `clone` rejects private AOT `__clone()` hooks outside allowed scopes. +#[test] +fn test_eval_clone_aot_object_rejects_private_clone_hook_outside_scope() { + let out = compile_and_run_capture( + r#"name = $name; + } + + private function __clone(): void { + $this->name = $this->name . ":private"; + } +} + +$object = new EvalCloneAotPrivateOutsideBox("A"); +eval('$copy = clone $object; echo $copy->name;'); +"#, + ); + assert!( + !out.success, + "program unexpectedly succeeded: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert!( + out.stderr.contains("eval() runtime failed"), + "expected eval runtime failure for private clone hook, got stderr={}", + out.stderr + ); +} + +/// Verifies eval method calls cannot directly invoke private AOT `__clone()` out of scope. +#[test] +fn test_eval_rejects_direct_private_aot_clone_method_call_outside_scope() { + let out = compile_and_run_capture( + r#"name = "private"; + } +} + +$object = new EvalCloneAotPrivateDirectBox(); +eval('$object->__clone();'); +"#, + ); + assert!( + !out.success, + "program unexpectedly succeeded: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert!( + out.stderr.contains("eval() runtime failed"), + "expected eval runtime failure for private __clone call, got stderr={}", + out.stderr + ); +} + /// Verifies eval ReflectionClass::isIterable reports eval and builtin class metadata. #[test] fn test_eval_reflection_class_iterable_predicate() { From c42e13dbb0f4944c9616031353b093a996da0f66 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 10:14:52 +0200 Subject: [PATCH 0538/1208] Support non-public static reflection values --- docs/php/classes.md | 6 +- src/codegen/lower_inst.rs | 6 ++ src/codegen/lower_inst/static_properties.rs | 44 ++++++++- src/codegen/mod.rs | 8 +- src/ir/instr.rs | 13 ++- src/ir/validator.rs | 12 ++- src/ir_lower/context.rs | 30 ++++++ src/ir_lower/expr/mod.rs | 99 +++++++++++++++---- src/ir_lower/stmt/mod.rs | 13 ++- src/ir_passes/peephole/load_store.rs | 1 + src/types/checker/builtin_types/reflection.rs | 2 +- tests/codegen/oop/reflection_properties.rs | 61 +++++++++++- 12 files changed, 252 insertions(+), 43 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index ce66ccacb9..0ca770228f 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1253,7 +1253,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | -| `ReflectionProperty::getValue()` | `ReflectionProperty` for instance properties with an explicit object argument, or inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for public static properties | Read supported instance/static property storage; positional and named arguments are normalized | +| `ReflectionProperty::getValue()` | `ReflectionProperty` for instance properties with an explicit object argument, or inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for known static properties | Read supported instance/static property storage; positional and named arguments are normalized | | `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported instance/static property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | @@ -1344,8 +1344,8 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Public static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers and inline `ReflectionClass::getProperty("property")` receivers with omitted/ignored object arguments. Dynamic static-property lookup from reflector objects held only in runtime storage and non-public static-property value access are not yet available. -- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. +- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, and straight-line locals assigned from either form with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 06c0e9faca..0e01627059 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -189,6 +189,12 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::InitStaticLocal => static_locals::lower_init_static_local(ctx, &inst), Op::LoadStaticProperty => static_properties::lower_load_static_property(ctx, &inst), Op::StoreStaticProperty => static_properties::lower_store_static_property(ctx, &inst), + Op::LoadReflectionStaticProperty => { + static_properties::lower_load_reflection_static_property(ctx, &inst) + } + Op::StoreReflectionStaticProperty => { + static_properties::lower_store_reflection_static_property(ctx, &inst) + } Op::Call => lower_direct_call(ctx, &inst), Op::ClosureCall => callables::lower_closure_call(ctx, &inst), Op::ExprCall => callables::lower_expr_call(ctx, &inst), diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index c5a2bc9898..ed2491cbd2 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -46,7 +46,7 @@ struct StaticPropertyBranch { /// Lowers a direct static property read into the current result register(s). pub(super) fn lower_load_static_property(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - let slot = resolve_static_property_slot(ctx, inst)?; + let slot = resolve_static_property_slot(ctx, inst, true)?; ensure_static_property_type_supported(&slot.php_type, inst)?; if slot.late_bound && !slot.branches.is_empty() { let class_id_reg = class_id_work_reg(ctx.emitter); @@ -62,7 +62,7 @@ pub(super) fn lower_load_static_property(ctx: &mut FunctionContext<'_>, inst: &I /// Lowers a direct static property write from one SSA operand into symbol-backed storage. pub(super) fn lower_store_static_property(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let value = expect_operand(inst, 0)?; - let slot = resolve_static_property_slot(ctx, inst)?; + let slot = resolve_static_property_slot(ctx, inst, true)?; ensure_static_property_type_supported(&slot.php_type, inst)?; let value_ty = ctx.value_php_type(value)?; ensure_static_property_value_supported(&slot, &value_ty, inst)?; @@ -79,6 +79,33 @@ pub(super) fn lower_store_static_property(ctx: &mut FunctionContext<'_>, inst: & Ok(()) } +/// Lowers a Reflection static property read, bypassing PHP member visibility. +pub(super) fn lower_load_reflection_static_property( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let slot = resolve_static_property_slot(ctx, inst, false)?; + ensure_static_property_type_supported(&slot.php_type, inst)?; + emit_direct_load_static_property_result(ctx, &slot); + store_if_result(ctx, inst) +} + +/// Lowers a Reflection static property write, bypassing PHP member visibility. +pub(super) fn lower_store_reflection_static_property( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let value = expect_operand(inst, 0)?; + let slot = resolve_static_property_slot(ctx, inst, false)?; + ensure_static_property_type_supported(&slot.php_type, inst)?; + let value_ty = ctx.value_php_type(value)?; + ensure_static_property_value_supported(&slot, &value_ty, inst)?; + load_static_property_store_value_to_result(ctx, value, &slot.php_type)?; + let release_previous = !value_is_same_static_property_load(ctx, value, &slot)?; + emit_direct_store_static_property_result(ctx, &slot, release_previous); + Ok(()) +} + /// Returns true when a store writes back the same static slot it just loaded. fn value_is_same_static_property_load( ctx: &FunctionContext<'_>, @@ -94,16 +121,21 @@ fn value_is_same_static_property_load( let Some(inst_ref) = ctx.function.instruction(inst) else { return Err(CodegenIrError::missing_entry("instruction", inst.as_raw())); }; - if inst_ref.op != crate::ir::Op::LoadStaticProperty { + if !matches!( + inst_ref.op, + crate::ir::Op::LoadStaticProperty | crate::ir::Op::LoadReflectionStaticProperty + ) { return Ok(false); } - Ok(resolve_static_property_slot(ctx, inst_ref)?.symbol == slot.symbol) + let enforce_visibility = inst_ref.op == crate::ir::Op::LoadStaticProperty; + Ok(resolve_static_property_slot(ctx, inst_ref, enforce_visibility)?.symbol == slot.symbol) } /// Resolves a static property immediate into declaring-class symbol metadata. fn resolve_static_property_slot( ctx: &FunctionContext<'_>, inst: &Instruction, + enforce_visibility: bool, ) -> Result { let label = static_property_label(ctx, inst)?; let (receiver, property) = parse_static_property_label(label)?; @@ -135,7 +167,9 @@ fn resolve_static_property_slot( .class_infos .get(declaring_class) .ok_or_else(|| CodegenIrError::unsupported(format!("unknown static property declaring class {}", declaring_class)))?; - ensure_static_property_visibility(ctx, declaring_class, property, declaring_info, inst)?; + if enforce_visibility { + ensure_static_property_visibility(ctx, declaring_class, property, declaring_info, inst)?; + } let (raw_receiver, _) = parse_static_property_label(label)?; let late_bound = raw_receiver.trim_start_matches('\\') == "static"; let branches = dynamic_static_property_branches(ctx, late_bound, property, declaring_class); diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index a5fa7ebbaa..ac62c79f38 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -993,7 +993,13 @@ fn referenced_static_property_class_names(module: &Module) -> HashSet { .chain(module.runtime_callable_invokers.iter()) { for inst in &function.instructions { - if !matches!(inst.op, Op::LoadStaticProperty | Op::StoreStaticProperty) { + if !matches!( + inst.op, + Op::LoadStaticProperty + | Op::StoreStaticProperty + | Op::LoadReflectionStaticProperty + | Op::StoreReflectionStaticProperty + ) { continue; } let Some(Immediate::Data(data)) = inst.immediate else { diff --git a/src/ir/instr.rs b/src/ir/instr.rs index c840516b28..30423780a5 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -203,6 +203,8 @@ pub enum Op { InitStaticLocal, LoadStaticProperty, StoreStaticProperty, + LoadReflectionStaticProperty, + StoreReflectionStaticProperty, IAdd, ISub, IMul, @@ -446,10 +448,11 @@ impl Op { }, AliasLocalRefCell => E::READS_LOCAL | E::WRITES_LOCAL, ReleaseLocalRefCell => E::READS_LOCAL | E::WRITES_LOCAL | E::WRITES_HEAP | E::REFCOUNT_OP, - LoadGlobal | LoadStaticProperty | ScopedConstantGet | ClassAttrNames - | ClassAttrArgs | ClassGetAttributes | CatchCurrent => E::READS_GLOBAL, - StoreGlobal | StoreStaticLocal | StoreStaticProperty | InitStaticLocal | IncludeOnceMark - | FunctionVariantMark | TryPushHandler | TryPopHandler => E::WRITES_GLOBAL, + LoadGlobal | LoadStaticProperty | LoadReflectionStaticProperty | ScopedConstantGet + | ClassAttrNames | ClassAttrArgs | ClassGetAttributes | CatchCurrent => E::READS_GLOBAL, + StoreGlobal | StoreStaticLocal | StoreStaticProperty | StoreReflectionStaticProperty + | InitStaticLocal | IncludeOnceMark | FunctionVariantMark | TryPushHandler + | TryPopHandler => E::WRITES_GLOBAL, IncludeOnceGuard => E::READS_GLOBAL | E::WRITES_GLOBAL, IToStr | FToStr | ResourceToStr | StrConcat | StrCharAt | StrInterpolate | MixedCastString | VarDump | PrintR => E::ALLOC_CONCAT, @@ -574,6 +577,8 @@ impl Op { InitStaticLocal => "init_static_local", LoadStaticProperty => "load_static_property", StoreStaticProperty => "store_static_property", + LoadReflectionStaticProperty => "load_reflection_static_property", + StoreReflectionStaticProperty => "store_reflection_static_property", IAdd => "iadd", ISub => "isub", IMul => "imul", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 5f8031a2b2..846b7ee7ab 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -394,16 +394,18 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio check_operand_type(function, inst_id, inst, 1, IrType::I64, "I64") } BufferNew => check_unary(function, inst_id, inst, IrType::I64, "I64"), - LoadLocal | LoadRefCell | LoadGlobal | LoadStaticLocal | LoadStaticProperty | ExternGlobalLoad => { + LoadLocal | LoadRefCell | LoadGlobal | LoadStaticLocal | LoadStaticProperty + | LoadReflectionStaticProperty | ExternGlobalLoad => { check_count(inst_id, inst, 0, "0") } UnsetLocal | PromoteLocalRefCell | AliasLocalRefCell | ReleaseLocalRefCell => { check_count(inst_id, inst, 0, "0") } - StoreLocal | StoreGlobal | StoreStaticLocal | InitStaticLocal | StoreStaticProperty | ExternGlobalStore - | StoreRefCell | BindRefCellPtr | Acquire | Release | Move | Borrow | EnsureOwned - | EchoValue | PrintValue | WriteStdout | WriteStrStdout | VarDump | PrintR - | ThrowException | GeneratorReturn | PtrCheckNonnull => { + StoreLocal | StoreGlobal | StoreStaticLocal | InitStaticLocal | StoreStaticProperty + | StoreReflectionStaticProperty | ExternGlobalStore | StoreRefCell | BindRefCellPtr + | Acquire | Release | Move | Borrow | EnsureOwned | EchoValue | PrintValue | WriteStdout + | WriteStrStdout | VarDump | PrintR | ThrowException | GeneratorReturn + | PtrCheckNonnull => { check_count(inst_id, inst, 1, "1") } MixedTagOf | MixedUnbox | MixedCastBool | MixedCastInt | MixedCastFloat diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 7a9edc3e27..eff255dd7d 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -121,6 +121,7 @@ pub(crate) struct LoweringContext<'m, 'f> { pub finally_stack: Vec, static_callable_locals: HashMap, reflection_class_locals: HashMap, + reflection_property_locals: HashMap, fiber_start_sigs: HashMap, ref_bound_locals: HashSet, ref_cell_owner_locals: HashMap, @@ -196,6 +197,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { finally_stack: Vec::new(), static_callable_locals: HashMap::new(), reflection_class_locals: HashMap::new(), + reflection_property_locals: HashMap::new(), fiber_start_sigs: HashMap::new(), ref_bound_locals: HashSet::new(), ref_cell_owner_locals: HashMap::new(), @@ -700,6 +702,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { pub(crate) fn store_local(&mut self, name: &str, value: LoweredValue, php_type: PhpType, span: Option) -> LoweredValue { self.clear_static_callable_local(name); self.clear_reflection_class_local(name); + self.clear_reflection_property_local(name); self.clear_fiber_start_sig(name); if let Some(extern_type) = self.extern_global_type(name) { let release_source_after_store = self.value_is_owning_temporary(value); @@ -921,6 +924,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { ) -> LoweredValue { self.clear_static_callable_local(name); self.clear_reflection_class_local(name); + self.clear_reflection_property_local(name); self.clear_fiber_start_sig(name); let previous_kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, previous_kind); @@ -953,6 +957,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } self.clear_static_callable_local(name); self.clear_reflection_class_local(name); + self.clear_reflection_property_local(name); self.clear_fiber_start_sig(name); let slot = self.declare_local(name, PhpType::Void); self.release_ref_cell_owner(name, span); @@ -1020,6 +1025,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } self.clear_static_callable_local(target); self.clear_reflection_class_local(target); + self.clear_reflection_property_local(target); self.clear_fiber_start_sig(target); self.release_replaced_local_before_ref_alias(target, span); let source_slot = self.declare_local(source, source_ty.clone()); @@ -1308,6 +1314,24 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_class_locals.get(name).cloned() } + /// Records that a PHP local currently holds a statically-known `ReflectionProperty` object. + pub(crate) fn bind_reflection_property_local( + &mut self, + name: &str, + reflected_class: String, + reflected_property: String, + ) { + if self.can_track_static_callable_local(name) { + self.reflection_property_locals + .insert(name.to_string(), (reflected_class, reflected_property)); + } + } + + /// Returns the reflected class/property associated with a local `ReflectionProperty`. + pub(crate) fn reflection_property_local(&self, name: &str) -> Option<(String, String)> { + self.reflection_property_locals.get(name).cloned() + } + /// Records that a PHP local currently holds a Fiber with a known callback signature. pub(crate) fn bind_fiber_start_sig(&mut self, name: &str, sig: FunctionSig) { if self.can_track_static_callable_local(name) { @@ -1341,6 +1365,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_class_locals.remove(name); } + /// Clears the compile-time `ReflectionProperty` association for one local. + pub(crate) fn clear_reflection_property_local(&mut self, name: &str) { + self.reflection_property_locals.remove(name); + } + /// Clears the known Fiber callback association for one local. pub(crate) fn clear_fiber_start_sig(&mut self, name: &str) { self.fiber_start_sigs.remove(name); @@ -1350,6 +1379,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { pub(crate) fn clear_static_callable_locals(&mut self) { self.static_callable_locals.clear(); self.reflection_class_locals.clear(); + self.reflection_property_locals.clear(); self.fiber_start_sigs.clear(); } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index fd1ea76e64..c57cb0a771 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1512,6 +1512,8 @@ fn lower_assignment_expr( } let static_callable = assigned_name.and_then(|_| static_callable_binding_for_expr(ctx, value)); let reflected_class = assigned_name.and_then(|_| reflection_class_binding_for_expr(ctx, value)); + let reflected_property = + assigned_name.and_then(|_| reflection_property_binding_for_expr(ctx, value)); let fiber_start_sig = assigned_name.and_then(|_| crate::ir_lower::fibers::start_sig_for_expr(ctx, value)); let callable_array = assigned_name @@ -1549,6 +1551,9 @@ fn lower_assignment_expr( if let Some(reflected_class) = reflected_class { ctx.bind_reflection_class_local(name, reflected_class); } + if let Some((reflected_class, reflected_property)) = reflected_property { + ctx.bind_reflection_property_local(name, reflected_class, reflected_property); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -3596,6 +3601,14 @@ pub(crate) fn reflection_class_binding_for_expr( reflection_class_new_instance_reflected_class(ctx, expr) } +/// Returns the reflected property captured by a statically-known `ReflectionProperty` expression. +pub(crate) fn reflection_property_binding_for_expr( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, +) -> Option<(String, String)> { + reflection_property_reflected_target(ctx, expr) +} + /// EIR value and callable binding produced by a callable-array assignment. pub(crate) struct LoweredCallableArrayAssignment { pub(crate) value: LoweredValue, @@ -10324,7 +10337,7 @@ fn lower_reflection_property_set_value( Some(lower_null(ctx, expr)) } -/// Lowers static `ReflectionProperty::getValue()` to a direct static-property read. +/// Lowers static `ReflectionProperty::getValue()` to a reflection static-property read. fn lower_reflection_property_get_static_value( ctx: &mut LoweringContext<'_, '_>, declaring_class: &str, @@ -10336,7 +10349,7 @@ fn lower_reflection_property_get_static_value( if let Some(ignored_object) = reflection_property_static_get_value_ignored_arg(args)? { lower_ignored_reflection_argument(ctx, &ignored_object); } - Some(lower_static_property_get_by_class_name( + Some(lower_reflection_static_property_get_by_class_name( ctx, declaring_class, property, @@ -10345,7 +10358,7 @@ fn lower_reflection_property_get_static_value( )) } -/// Lowers static `ReflectionProperty::setValue(null, $value)` to a static-property write. +/// Lowers static `ReflectionProperty::setValue(null, $value)` to a reflection static-property write. fn lower_reflection_property_set_static_value( ctx: &mut LoweringContext<'_, '_>, declaring_class: &str, @@ -10356,7 +10369,13 @@ fn lower_reflection_property_set_static_value( let (ignored_object, value_arg) = reflection_property_static_set_value_args(args)?; lower_ignored_reflection_argument(ctx, &ignored_object); let value = lower_expr(ctx, &value_arg); - store_static_property_by_class_name(ctx, declaring_class, property, value.value, expr.span); + store_reflection_static_property_by_class_name( + ctx, + declaring_class, + property, + value.value, + expr.span, + ); Some(lower_null(ctx, expr)) } @@ -10471,16 +10490,12 @@ fn reflection_property_instance_target( )) } -/// Resolves an inline `ReflectionProperty` target for a public static property. +/// Resolves an inline `ReflectionProperty` target for a static property. fn reflection_property_static_target( ctx: &LoweringContext<'_, '_>, object_expr: &Expr, ) -> Option<(String, String, PhpType)> { let (class_name, property) = reflection_property_reflected_target(ctx, object_expr)?; - let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; - if class_info.static_property_visibilities.get(&property) != Some(&Visibility::Public) { - return None; - } let (declaring_class, property_ty) = reflection_class_static_property_target(ctx, &class_name, &property)?; Some((declaring_class, property, property_ty)) @@ -10493,6 +10508,12 @@ fn reflection_property_reflected_target( ) -> Option<(String, String)> { reflection_property_constructor_target(ctx, object_expr) .or_else(|| reflection_property_class_get_property_target(ctx, object_expr)) + .or_else(|| { + let ExprKind::Variable(name) = &object_expr.kind else { + return None; + }; + ctx.reflection_property_local(name) + }) } /// Extracts the known class and property name from an inline ReflectionProperty constructor. @@ -10617,7 +10638,7 @@ fn lower_reflection_class_get_static_properties( for (property, declaring_class, property_ty) in properties { let key_expr = Expr::new(ExprKind::StringLiteral(property.clone()), expr.span); let key = lower_string_literal(ctx, &property, &key_expr); - let value = lower_static_property_get_by_class_name( + let value = lower_reflection_static_property_get_by_class_name( ctx, &declaring_class, &property, @@ -10648,7 +10669,7 @@ fn lower_reflection_class_get_static_property_value( reflection_class_static_property_target(ctx, class_name, &property) { if default.is_none() { - return Some(lower_static_property_get_by_class_name( + return Some(lower_reflection_static_property_get_by_class_name( ctx, &declaring_class, &property, @@ -10671,7 +10692,7 @@ fn lower_reflection_class_set_static_property_value( let (property, value) = reflection_class_set_static_property_value_args(args)?; let (declaring_class, _) = reflection_class_static_property_target(ctx, class_name, &property)?; let value = lower_expr(ctx, &value); - store_static_property_by_class_name(ctx, &declaring_class, &property, value.value, expr.span); + store_reflection_static_property_by_class_name(ctx, &declaring_class, &property, value.value, expr.span); Some(lower_null(ctx, expr)) } @@ -10804,39 +10825,77 @@ fn reflection_class_static_property_target( Some((declaring_class, property_ty)) } -/// Emits a live static-property read from a known reflected class and property name. -fn lower_static_property_get_by_class_name( +/// Emits a visibility-bypassing reflection static-property read. +fn lower_reflection_static_property_get_by_class_name( ctx: &mut LoweringContext<'_, '_>, class_name: &str, property: &str, result_type: PhpType, expr: &Expr, +) -> LoweredValue { + lower_static_property_get_by_class_name_with_op( + ctx, + class_name, + property, + result_type, + expr, + Op::LoadReflectionStaticProperty, + ) +} + +/// Emits a static-property read using the requested static-property opcode. +fn lower_static_property_get_by_class_name_with_op( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + result_type: PhpType, + expr: &Expr, + op: Op, ) -> LoweredValue { let data = ctx.intern_string(&format!("{}::{}", class_name, property)); ctx.emit_value( - Op::LoadStaticProperty, + op, Vec::new(), Some(Immediate::Data(data)), result_type, - Op::LoadStaticProperty.default_effects(), + op.default_effects(), Some(expr.span), ) } -/// Emits a live static-property write for a known reflected class and property name. -fn store_static_property_by_class_name( +/// Emits a visibility-bypassing reflection static-property write. +fn store_reflection_static_property_by_class_name( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + value: ValueId, + span: Span, +) { + store_static_property_by_class_name_with_op( + ctx, + class_name, + property, + value, + span, + Op::StoreReflectionStaticProperty, + ); +} + +/// Emits a static-property write using the requested static-property opcode. +fn store_static_property_by_class_name_with_op( ctx: &mut LoweringContext<'_, '_>, class_name: &str, property: &str, value: ValueId, span: Span, + op: Op, ) { let data = ctx.intern_string(&format!("{}::{}", class_name, property)); ctx.emit_void( - Op::StoreStaticProperty, + op, vec![value], Some(Immediate::Data(data)), - Op::StoreStaticProperty.default_effects(), + op.default_effects(), Some(span), ); } diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index 5c62e800ab..efce7fafba 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -20,8 +20,9 @@ use crate::ir_lower::effects_lookup; use crate::ir_lower::expr::{ array_access_element_result_type, coerce_to_int_at_span, lower_callable_array_for_assignment, lower_array_literal_with_expected_type, lower_closure_for_assignment, lower_expr, - reflection_class_binding_for_expr, static_callable_binding_for_expr, - string_op_uses_scratch_storage, type_satisfies_array_access_for_ir, + reflection_class_binding_for_expr, reflection_property_binding_for_expr, + static_callable_binding_for_expr, string_op_uses_scratch_storage, + type_satisfies_array_access_for_ir, }; use crate::names::{php_symbol_key, property_hook_set_method}; use crate::parser::ast::{ @@ -235,6 +236,7 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa ctx.clear_pending_static_callable_result(); let static_callable = static_callable_binding_for_expr(ctx, value); let reflected_class = reflection_class_binding_for_expr(ctx, value); + let reflected_property = reflection_property_binding_for_expr(ctx, value); let fiber_start_sig = crate::ir_lower::fibers::start_sig_for_expr(ctx, value); let callable_array = lower_callable_array_for_assignment(ctx, value, static_callable.as_ref()); let lowered = callable_array @@ -267,6 +269,9 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa if let Some(reflected_class) = reflected_class { ctx.bind_reflection_class_local(name, reflected_class); } + if let Some((reflected_class, reflected_property)) = reflected_property { + ctx.bind_reflection_property_local(name, reflected_class, reflected_property); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -1028,6 +1033,7 @@ fn lower_typed_assign( let php_type = ctx.type_expr_to_php_type_for_value(type_expr); let static_callable = static_callable_binding_for_expr(ctx, value); let reflected_class = reflection_class_binding_for_expr(ctx, value); + let reflected_property = reflection_property_binding_for_expr(ctx, value); let fiber_start_sig = crate::ir_lower::fibers::start_sig_for_expr(ctx, value); let callable_array = lower_callable_array_for_assignment(ctx, value, static_callable.as_ref()); let lowered = callable_array @@ -1053,6 +1059,9 @@ fn lower_typed_assign( if let Some(reflected_class) = reflected_class { ctx.bind_reflection_class_local(name, reflected_class); } + if let Some((reflected_class, reflected_property)) = reflected_property { + ctx.bind_reflection_property_local(name, reflected_class, reflected_property); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } diff --git a/src/ir_passes/peephole/load_store.rs b/src/ir_passes/peephole/load_store.rs index d66f685a60..7c4ebea310 100644 --- a/src/ir_passes/peephole/load_store.rs +++ b/src/ir_passes/peephole/load_store.rs @@ -217,6 +217,7 @@ fn consumes_operands_by_value(op: Op) -> bool { | StoreGlobal | StoreStaticLocal | StoreStaticProperty + | StoreReflectionStaticProperty | InitStaticLocal | ThrowException | GeneratorYield diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 8059c026af..4b041cb417 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2661,7 +2661,7 @@ fn reflection_property_static_value_guard(method: &str, span: crate::span::Span) then_body: vec![throw_new_reflection_exception( string_lit( &format!( - "ReflectionProperty::{}() for static properties requires an inline known public static property", + "ReflectionProperty::{}() for static properties requires an inline known static property", method ), span, diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 32cd2a6da3..b9094786fe 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -8,8 +8,8 @@ //! Key details: //! - Covers explicit object arguments for public and non-public instance properties. //! - Covers runtime-held public instance property reflectors. -//! - Covers static properties where PHP permits no object argument. -//! - Visibility-bypassing reflection access remains a separate surface. +//! - Covers static properties where PHP permits omitted or ignored object args. +//! - Covers Reflection visibility bypass for instance and inline static properties. use super::*; @@ -69,6 +69,63 @@ echo ":" . ReflectStaticValueAccessTarget::$count; assert_eq!(out, "2:17:19:old:new:23"); } +/// Verifies ReflectionProperty static value access bypasses visibility for +/// private and protected properties when the reflected target is statically known. +#[test] +fn test_reflection_property_value_accessors_bypass_static_visibility() { + let out = compile_and_run( + r#"getValue(); +(new ReflectionProperty(ReflectHiddenStaticValueAccessTarget::class, "count"))->setValue(null, 8); +echo ":" . ReflectHiddenStaticValueAccessTarget::count(); + +$label = (new ReflectionClass(ReflectHiddenStaticValueAccessTarget::class))->getProperty("label"); +echo ":" . $label->getValue(null); +$label->setValue(object: null, value: "new"); +echo ":" . ReflectHiddenStaticValueAccessTarget::label(); +"#, + ); + assert_eq!(out, "4:8:old:new"); +} + +/// Verifies ReflectionClass static property value helpers bypass visibility and +/// operate on the same live static storage as direct class methods. +#[test] +fn test_reflection_class_static_value_accessors_bypass_visibility() { + let out = compile_and_run( + r#"getStaticPropertyValue("count"); +$ref->setStaticPropertyValue("count", 9); +echo ":" . ReflectClassHiddenStaticValueTarget::count(); +echo ":" . $ref->getStaticPropertyValue("label"); +$ref->setStaticPropertyValue(name: "label", value: "new"); +echo ":" . ReflectClassHiddenStaticValueTarget::label(); + +$props = $ref->getStaticProperties(); +echo ":" . $props["count"]; +echo ":" . $props["label"]; +"#, + ); + assert_eq!(out, "5:9:old:new:9:new"); +} + /// Verifies runtime-held `ReflectionProperty` objects can read and write public /// instance properties through their retained property names. #[test] From cfca872b504fb2b2c891d915a1a47f9fec8823fa Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 10:21:08 +0200 Subject: [PATCH 0539/1208] Resolve indexed static ReflectionProperty lists --- docs/php/classes.md | 2 +- src/ir_lower/expr/mod.rs | 47 ++++++++++++++++++++++ tests/codegen/oop/reflection_properties.rs | 29 +++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 0ca770228f..d3f7052f0a 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1344,7 +1344,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, and straight-line locals assigned from either form with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index c57cb0a771..6f01d1a562 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10508,6 +10508,7 @@ fn reflection_property_reflected_target( ) -> Option<(String, String)> { reflection_property_constructor_target(ctx, object_expr) .or_else(|| reflection_property_class_get_property_target(ctx, object_expr)) + .or_else(|| reflection_property_class_get_properties_index_target(ctx, object_expr)) .or_else(|| { let ExprKind::Variable(name) = &object_expr.kind else { return None; @@ -10516,6 +10517,52 @@ fn reflection_property_reflected_target( }) } +/// Extracts a known ReflectionProperty from `ReflectionClass::getProperties()[N]`. +fn reflection_property_class_get_properties_index_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::ArrayAccess { array, index } = &object_expr.kind else { + return None; + }; + let ExprKind::IntLiteral(raw_index) = &index.kind else { + return None; + }; + if *raw_index < 0 { + return None; + } + let ExprKind::MethodCall { + object, + method, + args, + } = &array.kind + else { + return None; + }; + if php_symbol_key(method) != "getproperties" || !args.is_empty() { + return None; + } + let class_name = reflection_class_reflected_class(ctx, object)?; + let property = reflection_class_property_name_at_index(ctx, &class_name, *raw_index as usize)?; + Some((class_name, property)) +} + +/// Returns the `ReflectionClass::getProperties()` property name at a known index. +fn reflection_class_property_name_at_index( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + index: usize, +) -> Option { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + class_info + .properties + .iter() + .chain(class_info.static_properties.iter()) + .map(|(name, _)| name) + .nth(index) + .cloned() +} + /// Extracts the known class and property name from an inline ReflectionProperty constructor. fn reflection_property_constructor_target( ctx: &LoweringContext<'_, '_>, diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index b9094786fe..5d1daea44f 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -96,6 +96,35 @@ echo ":" . ReflectHiddenStaticValueAccessTarget::label(); assert_eq!(out, "4:8:old:new"); } +/// Verifies static ReflectionProperty objects selected from `getProperties()` +/// with known indexes can read and write their reflected static storage. +#[test] +fn test_reflection_property_value_accessors_for_indexed_static_property_lists() { + let out = compile_and_run( + r#"getProperties()[0]; +echo $count->getName() . ":" . $count->getValue(); +$count->setValue(null, 8); +echo ":" . ReflectListedStaticValueAccessTarget::count(); + +$ref = new ReflectionClass(ReflectListedStaticValueAccessTarget::class); +$label = $ref->getProperties()[1]; +echo ":" . $label->getName() . ":" . $label->getValue(null); +$label->setValue(null, "new"); +echo ":" . ReflectListedStaticValueAccessTarget::label(); +"#, + ); + assert_eq!(out, "count:4:8:label:old:new"); +} + /// Verifies ReflectionClass static property value helpers bypass visibility and /// operate on the same live static storage as direct class methods. #[test] From 0eb7c3c18fcdb1c220e58635f51116e18e1de78b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 10:59:37 +0200 Subject: [PATCH 0540/1208] Support filtered ReflectionClass member lists --- docs/php/classes.md | 3 +- docs/php/eval.md | 6 +- src/ir_lower/expr/mod.rs | 434 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 93 +++- tests/codegen/oop/reflection_properties.rs | 45 ++ 5 files changed, 563 insertions(+), 18 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index d3f7052f0a..973b2ccb1c 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1344,7 +1344,8 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionClass::getMethods()` and `getProperties()` modifier filters are materialized for inline or straight-line tracked `ReflectionClass(Known::class)` receivers when the filter is a known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` constant. Dynamic non-zero filters on runtime-only `ReflectionClass` objects still require richer runtime member filtering. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/docs/php/eval.md b/docs/php/eval.md index 1eddac1018..3553d417be 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -277,8 +277,10 @@ materialized `ReflectionMethod` and `ReflectionProperty` objects for the same visible member metadata, including supported member attributes and predicate flags. For generated/AOT classes, `ReflectionClass::getMethod()` / `getProperty()` and `getMethods()` / `getProperties()` materialize reflection -objects from emitted member-name and predicate metadata, including optional -modifier filters. AOT method reflection also exposes registered parameter +objects from emitted member-name and predicate metadata. Optional modifier +filters are lowered for inline or tracked `ReflectionClass` receivers when the +filter is a known integer or `ReflectionMethod::IS_*` / +`ReflectionProperty::IS_*` constant. AOT method reflection also exposes registered parameter names, declared parameter types, declared return types, required/optional counts, and registered scalar or null default values for generated constructor, instance-method, and static-method signatures. AOT property diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 6f01d1a562..ea0b628057 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -19,7 +19,7 @@ use crate::ir_lower::context::{ }; use crate::ir_lower::effects_lookup; use crate::ir_lower::function; -use crate::names::{php_symbol_key, property_hook_get_method, Name}; +use crate::names::{php_symbol_key, property_hook_get_method, property_hook_set_method, Name}; use crate::parser::ast::{ is_compound_assignment_self_read, BinOp, CallableTarget, CastType, Expr, ExprKind, InstanceOfTarget, MagicConstant, StaticReceiver, Stmt, StmtKind, TypeExpr, Visibility, @@ -9965,6 +9965,13 @@ fn lower_method_call( return value; } } + if op == Op::MethodCall { + if let Some(value) = + lower_reflection_class_member_list_call(ctx, Some(object_expr), method, args, expr) + { + return value; + } + } if op == Op::MethodCall { if let Some(value) = lower_reflection_property_value_call(ctx, Some(object_expr), method, args, expr) @@ -10255,6 +10262,110 @@ fn lower_reflection_class_static_property_value_call( } } +/// Lowers statically-known filtered ReflectionClass member-list calls. +fn lower_reflection_class_member_list_call( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let class_name = reflection_class_reflected_class(ctx, object_expr?)?; + let (member_class, items): (&str, Vec) = match php_symbol_key(method).as_str() { + "getproperties" => { + let filter = reflection_class_get_properties_filter_arg(ctx, args)?; + ( + "ReflectionProperty", + reflection_class_property_names_for_filter(ctx, &class_name, filter)? + .into_iter() + .map(|property| { + reflection_member_constructor_expr( + "ReflectionProperty", + &class_name, + &property, + expr.span, + ) + }) + .collect::>(), + ) + } + "getmethods" => { + let filter = reflection_class_get_methods_filter_arg(ctx, args)?; + ( + "ReflectionMethod", + reflection_class_method_names_for_filter(ctx, &class_name, filter)? + .into_iter() + .map(|method| { + reflection_member_constructor_expr( + "ReflectionMethod", + &class_name, + &method, + expr.span, + ) + }) + .collect::>(), + ) + } + _ => return None, + }; + Some(lower_reflection_member_array( + ctx, + member_class, + &items, + expr, + )) +} + +/// Lowers a statically materialized Reflection member list with an explicit element type. +fn lower_reflection_member_array( + ctx: &mut LoweringContext<'_, '_>, + member_class: &str, + items: &[Expr], + expr: &Expr, +) -> LoweredValue { + let elem_ty = PhpType::Object(member_class.to_string()); + let array_ty = PhpType::Array(Box::new(elem_ty.clone())); + let array = ctx.emit_value( + Op::ArrayNew, + Vec::new(), + Some(Immediate::Capacity(items.len() as u32)), + array_ty, + Op::ArrayNew.default_effects(), + Some(expr.span), + ); + for item in items { + let value = lower_expr(ctx, item); + ctx.emit_void( + Op::ArrayPush, + vec![array.value, value.value], + None, + Op::ArrayPush.default_effects(), + Some(item.span), + ); + release_value_after_retaining_insert(ctx, Some(&elem_ty), value, item.span); + } + array +} + +/// Builds a direct Reflection member constructor expression for known metadata. +fn reflection_member_constructor_expr( + reflection_class: &str, + reflected_class: &str, + member: &str, + span: Span, +) -> Expr { + Expr::new( + ExprKind::NewObject { + class_name: Name::unqualified(reflection_class), + args: vec![ + Expr::new(ExprKind::StringLiteral(reflected_class.to_string()), span), + Expr::new(ExprKind::StringLiteral(member.to_string()), span), + ], + }, + span, + ) +} + /// Lowers `ReflectionProperty::getValue($object)` when the reflected property is known. fn lower_reflection_property_value_call( ctx: &mut LoweringContext<'_, '_>, @@ -10539,11 +10650,13 @@ fn reflection_property_class_get_properties_index_target( else { return None; }; - if php_symbol_key(method) != "getproperties" || !args.is_empty() { + if php_symbol_key(method) != "getproperties" { return None; } + let filter = reflection_class_get_properties_filter_arg(ctx, args)?; let class_name = reflection_class_reflected_class(ctx, object)?; - let property = reflection_class_property_name_at_index(ctx, &class_name, *raw_index as usize)?; + let property = + reflection_class_property_name_at_index(ctx, &class_name, *raw_index as usize, filter)?; Some((class_name, property)) } @@ -10552,15 +10665,312 @@ fn reflection_class_property_name_at_index( ctx: &LoweringContext<'_, '_>, class_name: &str, index: usize, + filter: Option, ) -> Option { + reflection_class_property_names_for_filter(ctx, class_name, filter)? + .into_iter() + .nth(index) +} + +/// Returns `ReflectionClass::getProperties()` names after applying a known filter. +fn reflection_class_property_names_for_filter( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + filter: Option, +) -> Option> { let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; - class_info + Some( + class_info + .properties + .iter() + .chain(class_info.static_properties.iter()) + .map(|(name, _)| name) + .filter(|name| reflection_property_matches_filter(class_info, name, filter)) + .cloned() + .collect(), + ) +} + +/// Returns `ReflectionClass::getMethods()` names after applying a known filter. +fn reflection_class_method_names_for_filter( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + filter: Option, +) -> Option> { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in class_info + .methods + .keys() + .chain(class_info.static_methods.keys()) + { + if seen.insert(php_symbol_key(name)) + && reflection_method_matches_filter(class_info, name, filter) + { + names.push(name.clone()); + } + } + Some(names) +} + +/// Returns the optional `ReflectionClass::getProperties()` modifier filter. +fn reflection_class_get_properties_filter_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + reflection_class_member_filter_arg(ctx, args, "ReflectionProperty") +} + +/// Returns the optional `ReflectionClass::getMethods()` modifier filter. +fn reflection_class_get_methods_filter_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + reflection_class_member_filter_arg(ctx, args, "ReflectionMethod") +} + +/// Returns the optional ReflectionClass member-list modifier filter. +fn reflection_class_member_filter_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], + constant_class: &str, +) -> Option> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [] => Some(None), + [filter] => reflection_member_filter_value(ctx, filter, constant_class), + _ => None, + }; + } + let (filter, _) = reflection_class_static_property_regular_args(&args, "filter", None)?; + filter + .as_ref() + .map(|filter| reflection_member_filter_value(ctx, filter, constant_class)) + .unwrap_or(Some(None)) +} + +/// Returns a known integer modifier filter expression. +fn reflection_member_filter_value( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, + constant_class: &str, +) -> Option> { + match &expr.kind { + ExprKind::Null => Some(None), + ExprKind::IntLiteral(value) => Some(Some(*value)), + ExprKind::ScopedConstantAccess { receiver, name } => Some(Some( + reflection_member_filter_constant(ctx, receiver, name, constant_class)?, + )), + _ => None, + } +} + +/// Resolves a `Reflection*::IS_*` class constant to its integer value. +fn reflection_member_filter_constant( + ctx: &LoweringContext<'_, '_>, + receiver: &StaticReceiver, + name: &str, + constant_class: &str, +) -> Option { + let class_name = static_receiver_class_name(ctx, receiver)?; + if php_symbol_key(class_name.trim_start_matches('\\')) != php_symbol_key(constant_class) { + return None; + } + let value = ctx.scoped_constant_value(&class_name, name)?; + let ExprKind::IntLiteral(value) = value.kind else { + return None; + }; + Some(value) +} + +/// Returns whether a method should be present for a modifier filter. +fn reflection_method_matches_filter( + class_info: &crate::types::ClassInfo, + method: &str, + filter: Option, +) -> bool { + let Some(filter) = filter else { + return true; + }; + reflection_method_filter_modifiers(class_info, method) + .is_some_and(|modifiers| modifiers & filter != 0) +} + +/// Returns whether a property should be present for a modifier filter. +fn reflection_property_matches_filter( + class_info: &crate::types::ClassInfo, + property: &str, + filter: Option, +) -> bool { + let Some(filter) = filter else { + return true; + }; + reflection_property_filter_modifiers(class_info, property) + .is_some_and(|modifiers| modifiers & filter != 0) +} + +/// Computes ReflectionMethod modifier bits for static filter resolution. +fn reflection_method_filter_modifiers( + class_info: &crate::types::ClassInfo, + method: &str, +) -> Option { + let method_key = php_symbol_key(method); + if class_info.methods.contains_key(&method_key) { + let visibility = class_info + .method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public); + return Some(reflection_method_filter_modifier_bits( + visibility, + false, + class_info.final_methods.contains(&method_key), + !class_info.method_impl_classes.contains_key(&method_key), + )); + } + if class_info.static_methods.contains_key(&method_key) { + let visibility = class_info + .static_method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public); + return Some(reflection_method_filter_modifier_bits( + visibility, + true, + class_info.final_static_methods.contains(&method_key), + !class_info + .static_method_impl_classes + .contains_key(&method_key), + )); + } + None +} + +/// Computes ReflectionProperty modifier bits for static filter resolution. +fn reflection_property_filter_modifiers( + class_info: &crate::types::ClassInfo, + property: &str, +) -> Option { + if class_info .properties .iter() - .chain(class_info.static_properties.iter()) - .map(|(name, _)| name) - .nth(index) - .cloned() + .any(|(name, _)| name == property) + { + let visibility = class_info + .property_visibilities + .get(property) + .unwrap_or(&Visibility::Public); + return Some(reflection_property_filter_modifier_bits( + visibility, + false, + class_info.final_properties.contains(property), + class_info.abstract_properties.contains(property), + class_info.readonly_properties.contains(property), + reflection_property_filter_is_virtual(class_info, property), + class_info.property_set_visibilities.get(property), + )); + } + if class_info + .static_properties + .iter() + .any(|(name, _)| name == property) + { + let visibility = class_info + .static_property_visibilities + .get(property) + .unwrap_or(&Visibility::Public); + return Some(reflection_property_filter_modifier_bits( + visibility, + true, + class_info.final_static_properties.contains(property), + false, + false, + false, + None, + )); + } + None +} + +/// Builds the ReflectionMethod modifier bitmask for filter matching. +fn reflection_method_filter_modifier_bits( + visibility: &Visibility, + is_static: bool, + is_final: bool, + is_abstract: bool, +) -> i64 { + let mut modifiers = match visibility { + Visibility::Public => 1, + Visibility::Protected => 2, + Visibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + modifiers +} + +/// Returns whether a property has hook metadata that makes it virtual. +fn reflection_property_filter_is_virtual( + class_info: &crate::types::ClassInfo, + property: &str, +) -> bool { + let get_method = php_symbol_key(&property_hook_get_method(property)); + let set_method = php_symbol_key(&property_hook_set_method(property)); + class_info.abstract_property_hooks.contains_key(property) + || class_info.methods.contains_key(&get_method) + || class_info.methods.contains_key(&set_method) +} + +/// Builds the ReflectionProperty modifier bitmask for filter matching. +fn reflection_property_filter_modifier_bits( + visibility: &Visibility, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_virtual: bool, + set_visibility: Option<&Visibility>, +) -> i64 { + let mut modifiers = match visibility { + Visibility::Public => 1, + Visibility::Protected => 2, + Visibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly { + modifiers |= 128; + } + if is_virtual { + modifiers |= 512; + } + match set_visibility { + Some(Visibility::Private) => modifiers |= 32 | 4096, + Some(Visibility::Protected) => modifiers |= 2048, + Some(Visibility::Public) | None => { + if is_readonly && visibility == &Visibility::Public { + modifiers |= 2048; + } + } + } + modifiers } /// Extracts the known class and property name from an inline ReflectionProperty constructor. @@ -10739,7 +11149,13 @@ fn lower_reflection_class_set_static_property_value( let (property, value) = reflection_class_set_static_property_value_args(args)?; let (declaring_class, _) = reflection_class_static_property_target(ctx, class_name, &property)?; let value = lower_expr(ctx, &value); - store_reflection_static_property_by_class_name(ctx, &declaring_class, &property, value.value, expr.span); + store_reflection_static_property_by_class_name( + ctx, + &declaring_class, + &property, + value.value, + expr.span, + ); Some(lower_null(ctx, expr)) } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 4b041cb417..670d9c524b 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1393,11 +1393,6 @@ fn builtin_reflection_class() -> FlattenedClass { builtin_reflection_class_implements_interface_method(), builtin_reflection_class_is_subclass_of_method(), builtin_reflection_class_is_instance_method(), - builtin_reflection_class_array_method( - "getMethods", - "__methods", - object_array_type("ReflectionMethod"), - ), builtin_reflection_class_get_member_method( "getMethod", "__methods", @@ -1410,7 +1405,12 @@ fn builtin_reflection_class() -> FlattenedClass { "ReflectionMethod", ), builtin_reflection_class_mixed_method("getParentClass", "__parent_class"), - builtin_reflection_class_array_method( + builtin_reflection_class_filtered_array_method( + "getMethods", + "__methods", + object_array_type("ReflectionMethod"), + ), + builtin_reflection_class_filtered_array_method( "getProperties", "__properties", object_array_type("ReflectionProperty"), @@ -2199,6 +2199,87 @@ fn builtin_reflection_class_array_method( } } +/// Returns a public `ReflectionClass` array method with a fail-closed optional filter. +fn builtin_reflection_class_filtered_array_method( + method_name: &str, + property: &str, + return_type: TypeExpr, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let filter = variable_expr("filter", dummy_span); + let source = reflection_this_property(property, dummy_span); + let filter_is_null = binary_expr( + filter.clone(), + BinOp::StrictEq, + Expr::new(ExprKind::Null, dummy_span), + dummy_span, + ); + let filter_is_zero = binary_expr( + filter, + BinOp::StrictEq, + Expr::new(ExprKind::IntLiteral(0), dummy_span), + dummy_span, + ); + let empty_result = Expr::new(ExprKind::ArrayLiteral(Vec::new()), dummy_span); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![( + "filter".to_string(), + Some(TypeExpr::Nullable(Box::new(TypeExpr::Int))), + null_expr(), + false, + )], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(return_type), + body: vec![ + Stmt::new( + StmtKind::If { + condition: filter_is_null, + then_body: vec![Stmt::new( + StmtKind::Return(Some(source.clone())), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: filter_is_zero, + then_body: vec![Stmt::new( + StmtKind::Return(Some(empty_result.clone())), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + throw_new_reflection_exception( + string_lit( + &format!( + "ReflectionClass::{}() dynamic filters are not supported", + method_name + ), + dummy_span, + ), + dummy_span, + ), + Stmt::new(StmtKind::Return(Some(empty_result)), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass::getMethod()` or `getProperty()` lookup method. fn builtin_reflection_class_get_member_method( method_name: &str, diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 5d1daea44f..08477a9fa3 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -125,6 +125,51 @@ echo ":" . ReflectListedStaticValueAccessTarget::label(); assert_eq!(out, "count:4:8:label:old:new"); } +/// Verifies `ReflectionClass::getProperties()` applies modifier filters and +/// that filtered static property entries retain direct value accessor support. +#[test] +fn test_reflection_property_value_accessors_for_filtered_static_property_lists() { + let out = compile_and_run( + r#"getProperties(ReflectionProperty::IS_STATIC); +echo count($static) . ":" . $static[0]->getName(); + +$count = $ref->getProperties(ReflectionProperty::IS_STATIC)[0]; +echo ":" . $count->getValue(); +$count->setValue(null, 8); +echo ":" . ReflectFilteredStaticValueAccessTarget::count(); + +$label = $ref->getProperties(filter: ReflectionProperty::IS_PROTECTED)[0]; +echo ":" . $label->getName() . ":" . $label->getValue(null); +$label->setValue(null, "new"); +echo ":" . ReflectFilteredStaticValueAccessTarget::label(); + +$public = $ref->getProperties(...["filter" => ReflectionProperty::IS_PUBLIC]); +$none = $ref->getProperties(0); +echo ":" . count($public) . ":" . $public[0]->getName() . ":" . count($none); + +$staticMethods = $ref->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $ref->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$noMethods = $ref->getMethods(0); +echo ":" . count($staticMethods) . ":" . count($privateMethods) . ":" . count($noMethods); +"#, + ); + assert_eq!(out, "2:count:4:8:label:old:new:1:instance:0:3:1:0"); +} + /// Verifies ReflectionClass static property value helpers bypass visibility and /// operate on the same live static storage as direct class methods. #[test] From edb71daa478a58711898ae9dfec022fdd4a96cb9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 11:12:06 +0200 Subject: [PATCH 0541/1208] Support runtime ReflectionClass member filters --- docs/php/classes.md | 2 +- docs/php/eval.md | 7 ++- src/ir_lower/stmt/mod.rs | 6 +- src/types/checker/builtin_types/reflection.rs | 63 +++++++++++++++---- tests/codegen/oop/reflection_properties.rs | 34 ++++++++++ 5 files changed, 94 insertions(+), 18 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 973b2ccb1c..cbd2c7dd97 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1345,7 +1345,7 @@ Limitations today: - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. -- `ReflectionClass::getMethods()` and `getProperties()` modifier filters are materialized for inline or straight-line tracked `ReflectionClass(Known::class)` receivers when the filter is a known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` constant. Dynamic non-zero filters on runtime-only `ReflectionClass` objects still require richer runtime member filtering. +- `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, diff --git a/docs/php/eval.md b/docs/php/eval.md index 3553d417be..f71f2d40b7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -278,9 +278,10 @@ visible member metadata, including supported member attributes and predicate flags. For generated/AOT classes, `ReflectionClass::getMethod()` / `getProperty()` and `getMethods()` / `getProperties()` materialize reflection objects from emitted member-name and predicate metadata. Optional modifier -filters are lowered for inline or tracked `ReflectionClass` receivers when the -filter is a known integer or `ReflectionMethod::IS_*` / -`ReflectionProperty::IS_*` constant. AOT method reflection also exposes registered parameter +filters are supported for materialized `ReflectionClass` member lists; inline +or tracked receivers with known integer or `ReflectionMethod::IS_*` / +`ReflectionProperty::IS_*` constants can also be statically narrowed before +materialization. AOT method reflection also exposes registered parameter names, declared parameter types, declared return types, required/optional counts, and registered scalar or null default values for generated constructor, instance-method, and static-method signatures. AOT property diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index efce7fafba..aa93a36d5d 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -1234,7 +1234,11 @@ fn lower_foreach( /// Returns the by-value foreach local type when Phase 04 can keep a concrete element. fn foreach_value_type(source_ty: &PhpType) -> PhpType { match source_ty.codegen_repr() { - PhpType::Array(elem) if elem.codegen_repr() == PhpType::Callable => PhpType::Callable, + PhpType::Array(elem) => match elem.codegen_repr() { + PhpType::Callable => PhpType::Callable, + PhpType::Object(class_name) => PhpType::Object(class_name), + _ => PhpType::Mixed, + }, PhpType::Object(class_name) if class_name == "Phar" || class_name == "PharData" => { PhpType::Object("PharFileInfo".to_string()) } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 670d9c524b..bc9a9a0d42 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2183,7 +2183,7 @@ fn builtin_reflection_class_array_method( param_attributes: Vec::new(), variadic: None, variadic_type: None, - return_type: Some(return_type), + return_type: Some(return_type.clone()), body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { @@ -2199,7 +2199,7 @@ fn builtin_reflection_class_array_method( } } -/// Returns a public `ReflectionClass` array method with a fail-closed optional filter. +/// Returns a public `ReflectionClass` array method with an optional modifier filter. fn builtin_reflection_class_filtered_array_method( method_name: &str, property: &str, @@ -2207,6 +2207,7 @@ fn builtin_reflection_class_filtered_array_method( ) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); let filter = variable_expr("filter", dummy_span); + let member = variable_expr("member", dummy_span); let source = reflection_this_property(property, dummy_span); let filter_is_null = binary_expr( filter.clone(), @@ -2215,12 +2216,23 @@ fn builtin_reflection_class_filtered_array_method( dummy_span, ); let filter_is_zero = binary_expr( - filter, + filter.clone(), BinOp::StrictEq, Expr::new(ExprKind::IntLiteral(0), dummy_span), dummy_span, ); let empty_result = Expr::new(ExprKind::ArrayLiteral(Vec::new()), dummy_span); + let modifier_match = binary_expr( + binary_expr( + method_call_expr(member.clone(), "getModifiers", Vec::new(), dummy_span), + BinOp::BitAnd, + filter, + dummy_span, + ), + BinOp::StrictNotEq, + Expr::new(ExprKind::IntLiteral(0), dummy_span), + dummy_span, + ); ClassMethod { name: method_name.to_string(), visibility: Visibility::Public, @@ -2237,7 +2249,7 @@ fn builtin_reflection_class_filtered_array_method( param_attributes: Vec::new(), variadic: None, variadic_type: None, - return_type: Some(return_type), + return_type: Some(return_type.clone()), body: vec![ Stmt::new( StmtKind::If { @@ -2263,17 +2275,42 @@ fn builtin_reflection_class_filtered_array_method( }, dummy_span, ), - throw_new_reflection_exception( - string_lit( - &format!( - "ReflectionClass::{}() dynamic filters are not supported", - method_name - ), - dummy_span, - ), + Stmt::new( + StmtKind::TypedAssign { + type_expr: return_type.clone(), + name: "result".to_string(), + value: empty_result, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Foreach { + array: source, + key_var: None, + value_var: "member".to_string(), + value_by_ref: false, + body: vec![Stmt::new( + StmtKind::If { + condition: modifier_match, + then_body: vec![Stmt::new( + StmtKind::ArrayPush { + array: "result".to_string(), + value: member, + }, + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )], + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(variable_expr("result", dummy_span))), dummy_span, ), - Stmt::new(StmtKind::Return(Some(empty_result)), dummy_span), ], span: dummy_span, attributes: Vec::new(), diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 08477a9fa3..dbb2ef9097 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -170,6 +170,40 @@ echo ":" . count($staticMethods) . ":" . count($privateMethods) . ":" . count($n assert_eq!(out, "2:count:4:8:label:old:new:1:instance:0:3:1:0"); } +/// Verifies runtime-held `ReflectionClass` objects apply dynamic member filters +/// without degrading returned reflector elements to unusable mixed payloads. +#[test] +fn test_reflection_class_runtime_member_filters_return_usable_reflectors() { + let out = compile_and_run( + r#"getProperties($propertyFilter); + echo count($props) . ":" . $props[0]->getName() . ":" . $props[1]->getName(); + + $methods = $ref->getMethods($methodFilter); + echo ":" . count($methods) . ":" . $methods[0]->getName() . ":" . $methods[0]->isPrivate(); +} + +inspect_members( + new ReflectionClass(ReflectRuntimeFilteredMemberTarget::class), + ReflectionProperty::IS_STATIC, + ReflectionMethod::IS_PRIVATE +); +"#, + ); + assert_eq!(out, "2:count:label:1:secret:1"); +} + /// Verifies ReflectionClass static property value helpers bypass visibility and /// operate on the same live static storage as direct class methods. #[test] From f13ccf73fac73c8e150ba42d62539708cc393b7c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 11:41:36 +0200 Subject: [PATCH 0542/1208] Support ReflectionClass trait aliases --- docs/php/classes.md | 5 +- src/codegen/lower_inst/objects/reflection.rs | 99 +++++++++++++++++++ src/codegen_support/runtime/data/user.rs | 1 + src/ir_lower/tests/exhaustive.rs | 1 + src/name_resolver/declarations.rs | 8 +- src/types/checker/builtin_iterators.rs | 1 + .../checker/builtin_spl_classes/append.rs | 1 + .../append_array_iterator.rs | 1 + .../checker/builtin_spl_classes/caching.rs | 1 + .../checker/builtin_spl_classes/containers.rs | 5 + .../checker/builtin_spl_classes/filesystem.rs | 8 ++ .../checker/builtin_spl_classes/filters.rs | 2 + .../checker/builtin_spl_classes/forwarding.rs | 4 + .../checker/builtin_spl_classes/heaps.rs | 4 + .../checker/builtin_spl_classes/multiple.rs | 1 + .../builtin_spl_classes/object_storage.rs | 1 + src/types/checker/builtin_spl_classes/phar.rs | 2 + .../checker/builtin_spl_classes/recursive.rs | 3 + .../builtin_spl_classes/recursive_array.rs | 1 + .../recursive_iterator_iterator.rs | 1 + .../checker/builtin_spl_classes/regex.rs | 2 + .../checker/builtin_spl_classes/storage.rs | 3 + src/types/checker/builtin_spl_exceptions.rs | 1 + src/types/checker/builtin_stdclass.rs | 1 + .../checker/builtin_types/declarations.rs | 9 ++ src/types/checker/builtin_types/reflection.rs | 35 ++++++- src/types/checker/builtin_user_filter.rs | 1 + src/types/checker/driver/mod.rs | 1 + src/types/checker/schema/classes/state.rs | 1 + src/types/checker/schema/enums.rs | 1 + src/types/schema.rs | 2 + src/types/traits.rs | 50 +++++++++- tests/codegen/oop/attributes.rs | 28 ++++++ 33 files changed, 278 insertions(+), 7 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index cbd2c7dd97..43d7895d79 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1178,7 +1178,10 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::isSubclassOf()` | `new ReflectionClass($class_name)` | Return whether the reflected class-like symbol extends the requested class or implements/extends the requested interface; self is excluded, trait/enum targets return `false`, and missing names throw `ReflectionException` | | `ReflectionClass::isInstance()` | `new ReflectionClass($class_name)` | Return whether an object is an instance of the reflected class-like symbol, including parent-class, interface, and enum-case relations; trait targets return `false` | | `ReflectionClass::getInterfaceNames()` | `new ReflectionClass($class_name)` | Return implemented interface names for classes or parent interface names for interfaces | +| `ReflectionClass::getInterfaces()` | `new ReflectionClass($class_name)` | Return implemented or parent interfaces as name-keyed `ReflectionClass` objects | | `ReflectionClass::getTraitNames()` | `new ReflectionClass($class_name)` | Return trait names used directly by classes or traits | +| `ReflectionClass::getTraits()` | `new ReflectionClass($class_name)` | Return directly used traits as name-keyed `ReflectionClass` objects | +| `ReflectionClass::getTraitAliases()` | `new ReflectionClass($class_name)` | Return direct trait method aliases as an alias-name to `Trait::method` map | | `ReflectionClass::getMethods()` | `new ReflectionClass($class_name)` | Return `ReflectionMethod` objects for methods visible through the reflected class-like metadata | | `ReflectionClass::getConstructor()` | `new ReflectionClass($class_name)` | Return the reflected constructor as a `ReflectionMethod`, or `null` when no constructor is visible | | `ReflectionClass::getParentClass()` | `new ReflectionClass($class_name)` | Return the reflected parent class as a `ReflectionClass`, or `false` when the reflected class-like symbol has no parent class | @@ -1343,7 +1346,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getTraitNames()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 558df404cb..69544f2275 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -16,6 +16,7 @@ use crate::codegen::literal_defaults::{ emit_boxed_bool_literal_to_result, emit_boxed_float_literal_to_result, emit_boxed_int_literal_to_result, emit_boxed_null_literal_to_result, emit_boxed_string_literal_default_to_result, emit_empty_assoc_array_literal_to_result, + emit_string_literal_default_to_result, }; use crate::codegen::{ abi, emit_array_value_type_stamp, emit_box_current_owned_value_as_mixed, @@ -41,6 +42,7 @@ struct ReflectionOwnerMetadata { attr_args: Vec>>, interface_names: Vec, trait_names: Vec, + trait_aliases: Vec<(String, String)>, parent_names: Vec, method_names: Vec, property_names: Vec, @@ -322,6 +324,11 @@ fn emit_reflection_owner_object( &metadata.trait_names, )?; emit_reflection_class_array_property_by_name(ctx, "__traits", &metadata.trait_names)?; + emit_reflection_string_assoc_property_by_name( + ctx, + "__trait_aliases", + &metadata.trait_aliases, + )?; emit_reflection_string_array_property_by_name( ctx, "__parent_names", @@ -581,6 +588,7 @@ fn reflection_class_metadata_for_name( attr_args: info.attribute_args.clone(), interface_names: info.interfaces.clone(), trait_names: info.used_traits.clone(), + trait_aliases: info.trait_aliases.clone(), parent_names: reflection_parent_class_names(ctx, info), method_names, property_names, @@ -642,6 +650,7 @@ fn reflection_class_metadata_for_name( attr_args: Vec::new(), interface_names: reflection_interface_parent_names(ctx, interface_name), trait_names: Vec::new(), + trait_aliases: Vec::new(), parent_names: Vec::new(), method_names, property_names, @@ -709,6 +718,7 @@ fn reflection_class_metadata_for_name( attr_args: Vec::new(), interface_names: Vec::new(), trait_names, + trait_aliases: Vec::new(), parent_names: Vec::new(), method_names, property_names, @@ -862,6 +872,7 @@ fn reflection_method_owner_metadata( attr_args: member.attr_args, interface_names: Vec::new(), trait_names: Vec::new(), + trait_aliases: Vec::new(), parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), @@ -926,6 +937,7 @@ fn reflection_property_metadata( .unwrap_or_default(), interface_names: Vec::new(), trait_names: Vec::new(), + trait_aliases: Vec::new(), parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), @@ -1117,6 +1129,7 @@ fn reflection_class_constant_metadata( attr_args: case.attribute_args.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + trait_aliases: Vec::new(), parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), @@ -1188,6 +1201,7 @@ fn reflection_enum_case_metadata( attr_args: case.attribute_args.clone(), interface_names: Vec::new(), trait_names: Vec::new(), + trait_aliases: Vec::new(), parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), @@ -1241,6 +1255,7 @@ fn reflection_class_constant_owner_metadata( attr_args: metadata.attr_args, interface_names: Vec::new(), trait_names: Vec::new(), + trait_aliases: Vec::new(), parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), @@ -3457,6 +3472,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { attr_args: Vec::new(), interface_names: Vec::new(), trait_names: Vec::new(), + trait_aliases: Vec::new(), parent_names: Vec::new(), method_names: Vec::new(), property_names: Vec::new(), @@ -4121,6 +4137,89 @@ fn emit_reflection_class_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { } } +/// Replaces a ReflectionClass private slot with a string-keyed string-value map. +fn emit_reflection_string_assoc_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + entries: &[(String, String)], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_hash"); + emit_reflection_string_assoc_array(ctx, entries); + let assoc_type = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + }; + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + runtime_value_tag(&assoc_type) as i64, + ); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + +/// Allocates and populates a string-keyed associative array of string values. +fn emit_reflection_string_assoc_array( + ctx: &mut FunctionContext<'_>, + entries: &[(String, String)], +) { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Str); + for (key, value) in entries { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_string_literal_default_to_result(ctx, value); + emit_reflection_string_hash_insert(ctx, key); + } +} + +/// Inserts the current owned string value into the stacked associative array. +#[rustfmt::skip] +fn emit_reflection_string_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + ctx.emitter.instruction("mov x3, x1"); // pass the persistent Reflection string as the hash payload pointer + ctx.emitter.instruction("mov x4, x2"); // pass the Reflection string length as the hash payload high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate(ctx.emitter, "x5", runtime_value_tag(&PhpType::Str) as i64); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + ctx.emitter.instruction("mov rcx, rax"); // pass the persistent Reflection string as the hash payload pointer + ctx.emitter.instruction("mov r8, rdx"); // pass the Reflection string length as the hash payload high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate(ctx.emitter, "r9", runtime_value_tag(&PhpType::Str) as i64); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + /// Allocates and populates the associative ReflectionClass default-property map. fn emit_reflection_default_property_array( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 0af1ddb344..2ad36d1091 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -1919,6 +1919,7 @@ mod tests { constant_attribute_names: HashMap::new(), constant_attribute_args: HashMap::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), properties: Vec::new(), property_offsets: HashMap::new(), property_declaring_classes: HashMap::new(), diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 0366fa7a83..7d0acb33ea 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -178,6 +178,7 @@ fn class_info(_class_name: &str) -> ClassInfo { constant_attribute_names: HashMap::new(), constant_attribute_args: HashMap::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), properties: Vec::new(), property_offsets: HashMap::new(), property_declaring_classes: HashMap::new(), diff --git a/src/name_resolver/declarations.rs b/src/name_resolver/declarations.rs index f7ba579a68..c2a5563d8e 100644 --- a/src/name_resolver/declarations.rs +++ b/src/name_resolver/declarations.rs @@ -381,8 +381,10 @@ fn resolve_properties( .collect() } -/// Resolves a trait use statement by rewriting its trait names and adaptations -/// (aliases and instead-of rules) through `resolved_class_name` and `php_symbol_key`. +/// Resolves a trait use statement by rewriting trait names and method selectors. +/// +/// Alias display names keep their declared spelling because Reflection exposes +/// them, while later method lookup still normalizes through `php_symbol_key`. pub(super) fn resolve_trait_use( trait_use: &TraitUse, current_namespace: Option<&str>, @@ -421,7 +423,7 @@ pub(super) fn resolve_trait_use( )) }), method: php_symbol_key(method), - alias: alias.as_ref().map(|alias| php_symbol_key(alias)), + alias: alias.clone(), visibility: visibility.clone(), }), TraitAdaptation::InsteadOf { diff --git a/src/types/checker/builtin_iterators.rs b/src/types/checker/builtin_iterators.rs index 75b3fec39b..80c921270a 100644 --- a/src/types/checker/builtin_iterators.rs +++ b/src/types/checker/builtin_iterators.rs @@ -72,6 +72,7 @@ pub(crate) fn inject_builtin_iterators( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); diff --git a/src/types/checker/builtin_spl_classes/append.rs b/src/types/checker/builtin_spl_classes/append.rs index c8dccbb5e8..d5236b5b0f 100644 --- a/src/types/checker/builtin_spl_classes/append.rs +++ b/src/types/checker/builtin_spl_classes/append.rs @@ -35,6 +35,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); append_array_iterator::insert_class(class_map); diff --git a/src/types/checker/builtin_spl_classes/append_array_iterator.rs b/src/types/checker/builtin_spl_classes/append_array_iterator.rs index d10d432887..7c68fffbf4 100644 --- a/src/types/checker/builtin_spl_classes/append_array_iterator.rs +++ b/src/types/checker/builtin_spl_classes/append_array_iterator.rs @@ -32,6 +32,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/caching.rs b/src/types/checker/builtin_spl_classes/caching.rs index eca7c0fa4f..bfba95afa7 100644 --- a/src/types/checker/builtin_spl_classes/caching.rs +++ b/src/types/checker/builtin_spl_classes/caching.rs @@ -37,6 +37,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: caching_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/containers.rs b/src/types/checker/builtin_spl_classes/containers.rs index e22e097d02..a4d1c3088a 100644 --- a/src/types/checker/builtin_spl_classes/containers.rs +++ b/src/types/checker/builtin_spl_classes/containers.rs @@ -38,6 +38,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: spl_doubly_linked_list_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -55,6 +56,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -75,6 +77,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -97,6 +100,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -114,6 +118,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/filesystem.rs b/src/types/checker/builtin_spl_classes/filesystem.rs index 17fefe2930..2d8ef6c613 100644 --- a/src/types/checker/builtin_spl_classes/filesystem.rs +++ b/src/types/checker/builtin_spl_classes/filesystem.rs @@ -52,6 +52,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -69,6 +70,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: spl_file_object_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -86,6 +88,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: spl_file_object_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -103,6 +106,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -120,6 +124,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: filesystem_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -137,6 +142,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: filesystem_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -154,6 +160,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: filesystem_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -171,6 +178,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/filters.rs b/src/types/checker/builtin_spl_classes/filters.rs index 31b10fdc44..e5c9d71442 100644 --- a/src/types/checker/builtin_spl_classes/filters.rs +++ b/src/types/checker/builtin_spl_classes/filters.rs @@ -33,6 +33,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -50,6 +51,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/forwarding.rs b/src/types/checker/builtin_spl_classes/forwarding.rs index 4c8def47a6..1c42433c58 100644 --- a/src/types/checker/builtin_spl_classes/forwarding.rs +++ b/src/types/checker/builtin_spl_classes/forwarding.rs @@ -33,6 +33,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -50,6 +51,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -67,6 +69,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -84,6 +87,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/heaps.rs b/src/types/checker/builtin_spl_classes/heaps.rs index 0ef59eb74c..66b09c7a6e 100644 --- a/src/types/checker/builtin_spl_classes/heaps.rs +++ b/src/types/checker/builtin_spl_classes/heaps.rs @@ -32,6 +32,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -49,6 +50,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -66,6 +68,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -83,6 +86,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: spl_priority_queue_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/multiple.rs b/src/types/checker/builtin_spl_classes/multiple.rs index 30ec0963aa..8de2d45b39 100644 --- a/src/types/checker/builtin_spl_classes/multiple.rs +++ b/src/types/checker/builtin_spl_classes/multiple.rs @@ -32,6 +32,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: multiple_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/object_storage.rs b/src/types/checker/builtin_spl_classes/object_storage.rs index 3008924cc5..8177d726c4 100644 --- a/src/types/checker/builtin_spl_classes/object_storage.rs +++ b/src/types/checker/builtin_spl_classes/object_storage.rs @@ -36,6 +36,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/phar.rs b/src/types/checker/builtin_spl_classes/phar.rs index 9e46c47cd4..be66d78183 100644 --- a/src/types/checker/builtin_spl_classes/phar.rs +++ b/src/types/checker/builtin_spl_classes/phar.rs @@ -46,6 +46,7 @@ fn insert_phar_like_class(class_map: &mut HashMap, name: attributes: Vec::new(), constants: phar_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -66,6 +67,7 @@ fn insert_phar_file_info_class(class_map: &mut HashMap) attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/recursive.rs b/src/types/checker/builtin_spl_classes/recursive.rs index 55fe7cb721..c1f93b48e0 100644 --- a/src/types/checker/builtin_spl_classes/recursive.rs +++ b/src/types/checker/builtin_spl_classes/recursive.rs @@ -35,6 +35,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -52,6 +53,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -69,6 +71,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/recursive_array.rs b/src/types/checker/builtin_spl_classes/recursive_array.rs index 643837e07d..d307ae12f8 100644 --- a/src/types/checker/builtin_spl_classes/recursive_array.rs +++ b/src/types/checker/builtin_spl_classes/recursive_array.rs @@ -32,6 +32,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs b/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs index c3d81833c6..c3ff9275b3 100644 --- a/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs +++ b/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs @@ -33,6 +33,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { attributes: Vec::new(), constants: recursive_iterator_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/regex.rs b/src/types/checker/builtin_spl_classes/regex.rs index a684ba1f27..fe656eb33e 100644 --- a/src/types/checker/builtin_spl_classes/regex.rs +++ b/src/types/checker/builtin_spl_classes/regex.rs @@ -47,6 +47,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: regex_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -64,6 +65,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: regex_iterator_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_classes/storage.rs b/src/types/checker/builtin_spl_classes/storage.rs index 55fee7d98c..68776d222e 100644 --- a/src/types/checker/builtin_spl_classes/storage.rs +++ b/src/types/checker/builtin_spl_classes/storage.rs @@ -32,6 +32,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -54,6 +55,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -75,6 +77,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_spl_exceptions.rs b/src/types/checker/builtin_spl_exceptions.rs index f62d973d2d..a4f83c3d7b 100644 --- a/src/types/checker/builtin_spl_exceptions.rs +++ b/src/types/checker/builtin_spl_exceptions.rs @@ -85,6 +85,7 @@ pub(crate) fn inject_builtin_spl_exceptions( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } diff --git a/src/types/checker/builtin_stdclass.rs b/src/types/checker/builtin_stdclass.rs index 4c41d96e17..f83e927944 100644 --- a/src/types/checker/builtin_stdclass.rs +++ b/src/types/checker/builtin_stdclass.rs @@ -52,6 +52,7 @@ pub(crate) fn inject_builtin_stdclass( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); diff --git a/src/types/checker/builtin_types/declarations.rs b/src/types/checker/builtin_types/declarations.rs index b7d2b75532..93a959b739 100644 --- a/src/types/checker/builtin_types/declarations.rs +++ b/src/types/checker/builtin_types/declarations.rs @@ -132,6 +132,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( @@ -161,6 +162,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); // RuntimeException, ReflectionException, and JsonException inherit the @@ -180,6 +182,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( @@ -196,6 +199,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( @@ -212,6 +216,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -229,6 +234,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( @@ -245,6 +251,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert( @@ -283,6 +290,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); @@ -301,6 +309,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index bc9a9a0d42..cf14f9fc3c 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -115,6 +115,7 @@ pub(crate) fn inject_builtin_reflection( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); class_map.insert("ReflectionClass".to_string(), builtin_reflection_class()); @@ -282,6 +283,14 @@ fn reflection_class_object_map_type() -> PhpType { } } +/// Returns `array` for trait-alias reflection maps. +fn reflection_string_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Str), + } +} + /// Returns a nullable object type expression for one synthetic reflection class. fn nullable_object_type(class_name: &str) -> TypeExpr { TypeExpr::Nullable(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) @@ -1254,6 +1263,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(object_array_type("ReflectionClass")), empty_array(), ), + builtin_property( + "__trait_aliases", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), builtin_property( "__parent_names", Visibility::Private, @@ -1361,6 +1376,11 @@ fn builtin_reflection_class() -> FlattenedClass { "__traits", object_array_type("ReflectionClass"), ), + builtin_reflection_class_array_method( + "getTraitAliases", + "__trait_aliases", + string_array_type(), + ), builtin_reflection_class_bool_method("isFinal", "__is_final"), builtin_reflection_class_bool_method("isAbstract", "__is_abstract"), builtin_reflection_class_bool_method("isInterface", "__is_interface"), @@ -1429,6 +1449,7 @@ fn builtin_reflection_class() -> FlattenedClass { attributes: Vec::new(), constants: reflection_class_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } @@ -3013,6 +3034,7 @@ fn builtin_reflection_owner_class( attributes: Vec::new(), constants: reflection_owner_constants(name), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } @@ -3488,8 +3510,14 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } if class_name == "ReflectionClass" { for (property_name, property_type) in &mut class_info.properties { - if matches!(property_name.as_str(), "__interfaces" | "__traits") { - *property_type = reflection_class_object_map_type(); + match property_name.as_str() { + "__interfaces" | "__traits" => { + *property_type = reflection_class_object_map_type(); + } + "__trait_aliases" => { + *property_type = reflection_string_map_type(); + } + _ => {} } } for method_name in [ @@ -3526,6 +3554,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = reflection_class_object_map_type(); } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTraitAliases")) { + sig.return_type = reflection_string_map_type(); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getMethods")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object("ReflectionMethod".to_string()))); diff --git a/src/types/checker/builtin_user_filter.rs b/src/types/checker/builtin_user_filter.rs index 6bb12159b4..57f71ebe99 100644 --- a/src/types/checker/builtin_user_filter.rs +++ b/src/types/checker/builtin_user_filter.rs @@ -47,6 +47,7 @@ pub(crate) fn inject_builtin_user_filter( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 0e8ad3717c..5dbd18e30b 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -493,6 +493,7 @@ fn flatten_enum_methods( attributes: stmt.attributes.clone(), constants: constants.clone(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }; substitute_relative_class_types_in_methods(&mut flattened.methods, name, None); units.push(flattened); diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index 3295ba3e35..c33a21dcce 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -161,6 +161,7 @@ impl ClassBuildState { constant_attribute_names, constant_attribute_args, used_traits: class.used_traits.clone(), + trait_aliases: class.trait_aliases.clone(), properties: self.prop_types, property_offsets: self.property_offsets, property_declaring_classes: self.property_declaring_classes, diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index b35557cd73..0f9ae4ccf4 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -444,6 +444,7 @@ pub(crate) fn insert_enum_metadata( constant_attribute_names, constant_attribute_args, used_traits: Vec::new(), + trait_aliases: Vec::new(), properties, property_offsets, property_declaring_classes, diff --git a/src/types/schema.rs b/src/types/schema.rs index 68eed670da..85b90190f0 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -284,6 +284,8 @@ pub struct ClassInfo { pub constant_attribute_args: HashMap>>>, /// Trait names used directly by this class declaration, preserving source order. pub used_traits: Vec, + /// Trait method aliases declared directly by this class, as `(alias, Trait::method)`. + pub trait_aliases: Vec<(String, String)>, pub properties: Vec<(String, PhpType)>, pub property_offsets: HashMap, pub property_declaring_classes: HashMap, diff --git a/src/types/traits.rs b/src/types/traits.rs index af76c50e93..db1af70f81 100644 --- a/src/types/traits.rs +++ b/src/types/traits.rs @@ -13,7 +13,7 @@ use std::collections::{HashMap, HashSet}; use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::{ - ClassConst, ClassMethod, ClassProperty, Program, StmtKind, TraitUse, + ClassConst, ClassMethod, ClassProperty, Program, StmtKind, TraitAdaptation, TraitUse, }; use crate::span::Span; @@ -37,6 +37,7 @@ pub struct FlattenedClass { pub attributes: Vec, pub constants: Vec, pub used_traits: Vec, + pub trait_aliases: Vec<(String, String)>, } #[derive(Clone)] @@ -212,6 +213,7 @@ pub fn flatten_classes( attributes: stmt.attributes.clone(), constants: constants.clone(), used_traits: used_trait_names(trait_uses), + trait_aliases: used_trait_aliases(trait_uses, &trait_map), }); } StmtKind::EnumDecl { @@ -284,6 +286,7 @@ pub fn flatten_classes( attributes: stmt.attributes.clone(), constants: constants.clone(), used_traits: used_trait_names(trait_uses), + trait_aliases: used_trait_aliases(trait_uses, &trait_map), }, ); } @@ -306,3 +309,48 @@ fn used_trait_names(trait_uses: &[TraitUse]) -> Vec { }) .collect() } + +/// Returns direct trait aliases in PHP's `alias => Trait::method` reflection format. +fn used_trait_aliases( + trait_uses: &[TraitUse], + trait_map: &HashMap, +) -> Vec<(String, String)> { + trait_uses + .iter() + .flat_map(|use_decl| { + use_decl.adaptations.iter().filter_map(|adaptation| { + let TraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + .. + } = adaptation + else { + return None; + }; + let source_trait = trait_name + .as_ref() + .map(|name| name.as_str().to_string()) + .or_else(|| trait_alias_source_trait(use_decl, method, trait_map))?; + Some((alias.clone(), format!("{source_trait}::{method}"))) + }) + }) + .collect() +} + +/// Resolves the direct trait that supplies one unqualified alias adaptation target. +fn trait_alias_source_trait( + trait_use: &TraitUse, + method: &str, + trait_map: &HashMap, +) -> Option { + let method_key = php_symbol_key(method); + trait_use.trait_names.iter().find_map(|trait_name| { + let trait_info = trait_map.get(trait_name.as_str())?; + trait_info + .methods + .iter() + .any(|candidate| php_symbol_key(&candidate.name) == method_key) + .then(|| trait_name.as_str().to_string()) + }) +} diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 6585292696..7006bedd92 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1634,6 +1634,34 @@ echo count($parentInterfaceObjects) . ":" . $parentInterfaceObjects["StaticRelat ); } +/// Verifies that `ReflectionClass::getTraitAliases()` reports direct trait +/// method aliases as PHP's alias-name to `Trait::method` map. +#[test] +fn test_reflection_class_reports_trait_aliases() { + let out = compile_and_run( + r#"getTraitAliases(); +echo count($aliases) . ":" . $aliases["relationAlias"] . ":" . $aliases["hiddenOther"]; +"#, + ); + assert_eq!( + out, + "2:StaticAliasOne::first:StaticAliasTwo::second" + ); +} + /// Verifies that `ReflectionClass::implementsInterface()` reports class, enum, /// and interface metadata using case-insensitive interface names. #[test] From 3ca756a1c559578df6ac23b80f1eb620e49458bb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 11:58:45 +0200 Subject: [PATCH 0543/1208] Support runtime ReflectionClass static property maps --- docs/php/classes.md | 2 +- src/codegen/lower_inst/objects/reflection.rs | 168 ++++++++++++++++++- tests/codegen/oop/reflection_properties.rs | 33 ++++ 3 files changed, 201 insertions(+), 2 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 43d7895d79..1b7471b904 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1349,7 +1349,7 @@ Limitations today: - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. -- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Missing literal properties can return the explicit default argument. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup still requires richer runtime metadata. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on the tracked live path. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 69544f2275..452ab4ff6e 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -21,11 +21,12 @@ use crate::codegen::literal_defaults::{ use crate::codegen::{ abi, emit_array_value_type_stamp, emit_box_current_owned_value_as_mixed, emit_box_current_value_as_mixed, emit_release_pushed_refcounted_temp_after_array_push, - runtime_value_tag, CodegenIrError, Result, + runtime_value_tag, CodegenIrError, Result, UNINITIALIZED_TYPED_PROPERTY_SENTINEL, }; use crate::ir::{Immediate, Instruction, Op, TraitMethodInfo, ValueDef, ValueId}; use crate::names::{ enum_case_symbol, php_symbol_key, property_hook_get_method, property_hook_set_method, + static_property_symbol, }; use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, TypeExpr, Visibility}; use crate::types::{ @@ -49,6 +50,7 @@ struct ReflectionOwnerMetadata { constant_names: Vec, constant_members: Vec, default_property_members: Vec, + static_property_members: Vec, constant_reflection_members: Vec, method_members: Vec, property_members: Vec, @@ -215,6 +217,14 @@ struct ReflectionDefaultPropertyMember { value: ReflectionParameterDefaultValue, } +/// Metadata for one live static-property value exposed by ReflectionClass. +struct ReflectionStaticPropertyMember { + name: String, + declaring_class_name: String, + php_type: PhpType, + is_declared: bool, +} + /// Compile-time value forms supported by Reflection constant metadata emission. #[derive(Clone)] enum ReflectionConstantValue { @@ -359,6 +369,11 @@ fn emit_reflection_owner_object( "__default_properties", &metadata.default_property_members, )?; + emit_reflection_static_property_array_property_by_name( + ctx, + "__static_properties", + &metadata.static_property_members, + )?; emit_reflection_member_array_property_by_name( ctx, "__reflection_constants", @@ -572,6 +587,7 @@ fn reflection_class_metadata_for_name( let constant_members = reflection_class_constant_members(ctx, class_name, info)?; let default_property_members = reflection_class_default_property_members(info, &property_names); + let static_property_members = reflection_class_static_property_members(class_name, info); let constant_reflection_members = reflection_class_constant_reflection_members(ctx, class_name, info)?; let method_members = reflection_class_method_members(ctx, class_name, info, &method_names)?; @@ -595,6 +611,7 @@ fn reflection_class_metadata_for_name( constant_names, constant_members, default_property_members, + static_property_members, constant_reflection_members, method_members, property_members, @@ -657,6 +674,7 @@ fn reflection_class_metadata_for_name( constant_names, constant_members, default_property_members: Vec::new(), + static_property_members: Vec::new(), constant_reflection_members, method_members, property_members, @@ -725,6 +743,7 @@ fn reflection_class_metadata_for_name( constant_names, constant_members, default_property_members: Vec::new(), + static_property_members: Vec::new(), constant_reflection_members, method_members, property_members, @@ -879,6 +898,7 @@ fn reflection_method_owner_metadata( constant_names: Vec::new(), constant_members: Vec::new(), default_property_members: Vec::new(), + static_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -944,6 +964,7 @@ fn reflection_property_metadata( constant_names: Vec::new(), constant_members: Vec::new(), default_property_members: Vec::new(), + static_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -1136,6 +1157,7 @@ fn reflection_class_constant_metadata( constant_names: Vec::new(), constant_members: Vec::new(), default_property_members: Vec::new(), + static_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -1208,6 +1230,7 @@ fn reflection_enum_case_metadata( constant_names: Vec::new(), constant_members: Vec::new(), default_property_members: Vec::new(), + static_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -1262,6 +1285,7 @@ fn reflection_class_constant_owner_metadata( constant_names: Vec::new(), constant_members: Vec::new(), default_property_members: Vec::new(), + static_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -1857,6 +1881,29 @@ fn reflection_class_default_property_members( .collect() } +/// Returns static-property storage slots for `ReflectionClass::getStaticProperties()`. +fn reflection_class_static_property_members( + class_name: &str, + info: &crate::types::ClassInfo, +) -> Vec { + info.static_properties + .iter() + .map(|(property_name, php_type)| { + let declaring_class_name = info + .static_property_declaring_classes + .get(property_name) + .cloned() + .unwrap_or_else(|| class_name.to_string()); + ReflectionStaticPropertyMember { + name: property_name.clone(), + declaring_class_name, + php_type: php_type.clone(), + is_declared: info.declared_static_properties.contains(property_name), + } + }) + .collect() +} + /// Returns materializable interface constant values for ReflectionClass metadata. fn reflection_interface_constant_members( ctx: &FunctionContext<'_>, @@ -3479,6 +3526,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { constant_names: Vec::new(), constant_members: Vec::new(), default_property_members: Vec::new(), + static_property_members: Vec::new(), constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), @@ -3858,6 +3906,39 @@ fn emit_reflection_default_property_array_property_by_name( Ok(()) } +/// Replaces a ReflectionClass private slot with current static-property values. +fn emit_reflection_static_property_array_property_by_name( + ctx: &mut FunctionContext<'_>, + property_name: &str, + members: &[ReflectionStaticPropertyMember], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionClass") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_static_property_array(ctx, members); + let assoc_type = PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + }; + emit_box_current_value_as_mixed(ctx.emitter, &assoc_type); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + /// Replaces a ReflectionClass private array slot with ReflectionMethod/Property objects. fn emit_reflection_member_array_property_by_name( ctx: &mut FunctionContext<'_>, @@ -4233,6 +4314,57 @@ fn emit_reflection_default_property_array( } } +/// Allocates and populates current static-property values for ReflectionClass. +fn emit_reflection_static_property_array( + ctx: &mut FunctionContext<'_>, + members: &[ReflectionStaticPropertyMember], +) { + emit_empty_assoc_array_literal_to_result(ctx, &PhpType::Mixed); + for member in members { + let skip_label = emit_skip_if_static_property_uninitialized(ctx, member); + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + let symbol = static_property_symbol(&member.declaring_class_name, &member.name); + abi::emit_load_symbol_to_result(ctx.emitter, &symbol, &member.php_type); + emit_box_current_value_as_mixed(ctx.emitter, &member.php_type.codegen_repr()); + emit_reflection_static_property_hash_insert(ctx, &member.name); + if let Some(skip_label) = skip_label { + ctx.emitter.label(&skip_label); + } + } +} + +/// Emits a branch over uninitialized typed static properties, matching PHP reflection. +#[rustfmt::skip] +fn emit_skip_if_static_property_uninitialized( + ctx: &mut FunctionContext<'_>, + member: &ReflectionStaticPropertyMember, +) -> Option { + if !member.is_declared { + return None; + } + let skip_label = ctx.next_label("reflection_static_uninitialized"); + let symbol = static_property_symbol(&member.declaring_class_name, &member.name); + let marker_reg = abi::secondary_scratch_reg(ctx.emitter); + let sentinel_reg = abi::tertiary_scratch_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, marker_reg, &symbol, 8); + abi::emit_load_int_immediate( + ctx.emitter, + sentinel_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the static property marker against the uninitialized sentinel + ctx.emitter.instruction(&format!("b.eq {}", skip_label)); // omit uninitialized typed static properties from the reflection map + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the static property marker against the uninitialized sentinel + ctx.emitter.instruction(&format!("je {}", skip_label)); // omit uninitialized typed static properties from the reflection map + } + } + Some(skip_label) +} + /// Materializes one Reflection constant value as a boxed Mixed cell. fn emit_reflection_constant_value_as_mixed( ctx: &mut FunctionContext<'_>, @@ -4491,6 +4623,40 @@ fn emit_reflection_constant_hash_insert(ctx: &mut FunctionContext<'_>, key: &str } } +/// Inserts the current boxed Mixed static-property value into the stacked associative array. +#[rustfmt::skip] +fn emit_reflection_static_property_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the boxed Reflection static value as the hash payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed Reflection static value as the hash payload + ctx.emitter.instruction("xor r8, r8"); // boxed Mixed hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Mixed) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + /// Allocates and populates one ReflectionMethod/ReflectionProperty object. fn emit_reflection_member_object( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index dbb2ef9097..47e58d9840 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -234,6 +234,39 @@ echo ":" . $props["label"]; assert_eq!(out, "5:9:old:new:9:new"); } +/// Verifies runtime-held `ReflectionClass` objects expose materialized AOT +/// static-property values and omit uninitialized typed static properties. +#[test] +fn test_reflection_class_runtime_static_properties_materialize_aot_values() { + let out = compile_and_run( + r#"getStaticProperties(); + echo count($props); + echo ":" . $props["count"]; + echo ":" . $props["label"]; + echo ":" . ($props["unset"] ?? "missing"); + echo ":" . $ref->getStaticPropertyValue("count", "fallback"); +} + +ReflectRuntimeStaticPropertiesTarget::$count = 5; +ReflectRuntimeStaticPropertiesTarget::rename("new"); +inspect_static_props(new ReflectionClass(ReflectRuntimeStaticPropertiesTarget::class)); +"#, + ); + assert_eq!(out, "2:5:new:missing:5"); +} + /// Verifies runtime-held `ReflectionProperty` objects can read and write public /// instance properties through their retained property names. #[test] From 5bda1f4d971e092d1bc2ea1d06b3b26ec2ff01ed Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 12:05:03 +0200 Subject: [PATCH 0544/1208] Support ReflectionClass static defaults from runtime maps --- docs/php/classes.md | 2 +- src/codegen/lower_inst/objects/reflection.rs | 5 --- src/types/checker/builtin_types/reflection.rs | 43 ++++++++++++++++++- tests/codegen/oop/reflection_properties.rs | 6 ++- 4 files changed, 47 insertions(+), 9 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 1b7471b904..851f89aac2 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1349,7 +1349,7 @@ Limitations today: - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. -- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on the tracked live path. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 452ab4ff6e..54c7ef7627 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -3926,11 +3926,6 @@ fn emit_reflection_static_property_array_property_by_name( abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); abi::emit_call_label(ctx.emitter, "__rt_decref_array"); emit_reflection_static_property_array(ctx, members); - let assoc_type = PhpType::AssocArray { - key: Box::new(PhpType::Str), - value: Box::new(PhpType::Mixed), - }; - emit_box_current_value_as_mixed(ctx.emitter, &assoc_type); abi::emit_pop_reg(ctx.emitter, object_reg); abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index cf14f9fc3c..39e1494ce9 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -291,6 +291,14 @@ fn reflection_string_map_type() -> PhpType { } } +/// Returns `array` for static-property value reflection maps. +fn reflection_static_properties_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Mixed), + } +} + /// Returns a nullable object type expression for one synthetic reflection class. fn nullable_object_type(class_name: &str) -> TypeExpr { TypeExpr::Nullable(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) @@ -1650,10 +1658,29 @@ fn builtin_reflection_class_get_constant_method() -> ClassMethod { fn builtin_reflection_class_get_static_property_value_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); let name = variable_expr("name", dummy_span); + let default = variable_expr("default", dummy_span); let value_read = Expr::new( ExprKind::ArrayAccess { array: Box::new(reflection_this_property("__static_properties", dummy_span)), - index: Box::new(name), + index: Box::new(name.clone()), + }, + dummy_span, + ); + let key_exists = Expr::new( + ExprKind::FunctionCall { + name: Name::unqualified("array_key_exists"), + args: vec![ + name, + reflection_this_property("__static_properties", dummy_span), + ], + }, + dummy_span, + ); + let value_or_default = Expr::new( + ExprKind::Ternary { + condition: Box::new(key_exists), + then_expr: Box::new(value_read), + else_expr: Box::new(default), }, dummy_span, ); @@ -1677,7 +1704,10 @@ fn builtin_reflection_class_get_static_property_value_method() -> ClassMethod { variadic: None, variadic_type: None, return_type: Some(mixed_type()), - body: vec![Stmt::new(StmtKind::Return(Some(value_read)), dummy_span)], + body: vec![Stmt::new( + StmtKind::Return(Some(value_or_default)), + dummy_span, + )], span: dummy_span, attributes: Vec::new(), } @@ -3517,6 +3547,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "__trait_aliases" => { *property_type = reflection_string_map_type(); } + "__static_properties" => { + *property_type = reflection_static_properties_map_type(); + } _ => {} } } @@ -3585,6 +3618,12 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getProperty")) { sig.return_type = PhpType::Object("ReflectionProperty".to_string()); } + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getStaticProperties")) + { + sig.return_type = reflection_static_properties_map_type(); + } if let Some(sig) = class_info .methods .get_mut(&php_symbol_key("getConstructor")) diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 47e58d9840..91cc671174 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -243,6 +243,7 @@ fn test_reflection_class_runtime_static_properties_materialize_aot_values() { class ReflectRuntimeStaticPropertiesTarget { public static int $count = 2; private static string $label = "old"; + public static ?string $nullable = null; public static int $unset; public static function rename(string $value): void { @@ -255,8 +256,11 @@ function inspect_static_props(ReflectionClass $ref): void { echo count($props); echo ":" . $props["count"]; echo ":" . $props["label"]; + echo ":" . (array_key_exists("nullable", $props) && $props["nullable"] === null ? "null" : "bad"); echo ":" . ($props["unset"] ?? "missing"); echo ":" . $ref->getStaticPropertyValue("count", "fallback"); + echo ":" . ($ref->getStaticPropertyValue("nullable", "fallback") === null ? "null" : "bad"); + echo ":" . $ref->getStaticPropertyValue("missing", "fallback"); } ReflectRuntimeStaticPropertiesTarget::$count = 5; @@ -264,7 +268,7 @@ ReflectRuntimeStaticPropertiesTarget::rename("new"); inspect_static_props(new ReflectionClass(ReflectRuntimeStaticPropertiesTarget::class)); "#, ); - assert_eq!(out, "2:5:new:missing:5"); + assert_eq!(out, "3:5:new:null:missing:5:null:fallback"); } /// Verifies runtime-held `ReflectionProperty` objects can read and write public From 3bdb73344233bff902a54ee1187e0da8cb3f6d3c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 12:22:35 +0200 Subject: [PATCH 0545/1208] Support ReflectionProperty initialization probes --- docs/php/classes.md | 5 +- src/codegen/lower_inst.rs | 4 + src/codegen/lower_inst/objects.rs | 45 +++++++++ src/codegen/lower_inst/static_properties.rs | 44 +++++++++ src/codegen/mod.rs | 1 + src/ir/instr.rs | 14 ++- src/ir/validator.rs | 8 +- src/ir_lower/expr/mod.rs | 93 +++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 53 +++++++++++ tests/codegen/oop/reflection_properties.rs | 60 ++++++++++++ 10 files changed, 318 insertions(+), 9 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 851f89aac2..25985e4369 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1256,6 +1256,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | +| `ReflectionProperty::isInitialized()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` for supported known properties | Return whether a supported instance/static property slot is initialized without reading the property value | | `ReflectionProperty::getValue()` | `ReflectionProperty` for instance properties with an explicit object argument, or inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for known static properties | Read supported instance/static property storage; positional and named arguments are normalized | | `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported instance/static property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | @@ -1346,8 +1347,8 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, and `getDefaultValue()` for supported property defaults. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 0e01627059..6b68284cdc 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -174,6 +174,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) objects::lower_dynamic_object_new_without_constructor_mixed(ctx, &inst) } Op::PropGet => objects::lower_prop_get(ctx, &inst), + Op::PropInitialized => objects::lower_prop_initialized(ctx, &inst), Op::LoadPropRefCell => objects::lower_load_prop_ref_cell(ctx, &inst), Op::LoadArrayElemRefCell => arrays::lower_load_array_elem_ref_cell(ctx, &inst), Op::BindRefCellPtr => lower_bind_ref_cell_ptr(ctx, &inst), @@ -192,6 +193,9 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::LoadReflectionStaticProperty => { static_properties::lower_load_reflection_static_property(ctx, &inst) } + Op::ReflectionStaticPropertyInitialized => { + static_properties::lower_reflection_static_property_initialized(ctx, &inst) + } Op::StoreReflectionStaticProperty => { static_properties::lower_store_reflection_static_property(ctx, &inst) } diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index cca3186e9a..bc6c043f18 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -2531,6 +2531,24 @@ fn lower_mixed_load_prop_ref_cell( store_ref_cell_pointer_result(ctx, inst) } +/// Lowers a declared object-property initialization probe. +pub(super) fn lower_prop_initialized( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let object = expect_operand(inst, 0)?; + let property = property_name_immediate(ctx, inst)?.to_string(); + let slot = resolve_property_slot(ctx, object, &property, inst)?; + if !slot.is_declared { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + return store_if_result(ctx, inst); + } + let base_reg = abi::symbol_scratch_reg(ctx.emitter); + ctx.load_value_to_reg(object, base_reg)?; + emit_typed_property_initialized_bool(ctx, &slot, base_reg); + store_if_result(ctx, inst) +} + /// Returns the receiver class when an undeclared property should route through `__get`. fn magic_get_receiver_class( ctx: &FunctionContext<'_>, @@ -5642,6 +5660,33 @@ fn emit_uninitialized_typed_property_guard( ctx.emitter.label(&initialized_label); } +/// Compares a typed instance-property marker with the uninitialized sentinel. +fn emit_typed_property_initialized_bool( + ctx: &mut FunctionContext<'_>, + slot: &PropertySlot, + object_reg: &str, +) { + let marker_reg = abi::secondary_scratch_reg(ctx.emitter); + let sentinel_reg = abi::tertiary_scratch_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, marker_reg, object_reg, slot.offset + 8); + abi::emit_load_int_immediate( + ctx.emitter, + sentinel_reg, + UNINITIALIZED_TYPED_PROPERTY_SENTINEL, + ); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel + ctx.emitter.instruction("cset x0, ne"); // materialize true when the instance property is initialized + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the property marker against the uninitialized sentinel + ctx.emitter.instruction("setne al"); // materialize true when the instance property is initialized + ctx.emitter.instruction("movzx rax, al"); // widen the initialization flag into the integer result register + } + } +} + /// Emits the runtime throw for an uninitialized typed-property read. /// /// Constructs an `Error` object with the diagnostic message, publishes it to diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index ed2491cbd2..45342f0195 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -90,6 +90,16 @@ pub(super) fn lower_load_reflection_static_property( store_if_result(ctx, inst) } +/// Lowers a Reflection static-property initialization probe. +pub(super) fn lower_reflection_static_property_initialized( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let slot = resolve_static_property_slot(ctx, inst, false)?; + emit_direct_static_property_initialized_result(ctx, &slot); + store_if_result(ctx, inst) +} + /// Lowers a Reflection static property write, bypassing PHP member visibility. pub(super) fn lower_store_reflection_static_property( ctx: &mut FunctionContext<'_>, @@ -221,6 +231,18 @@ fn emit_direct_load_static_property_result( abi::emit_load_symbol_to_result(ctx.emitter, &slot.symbol, &slot.php_type); } +/// Emits `true` when the static-property slot is initialized. +fn emit_direct_static_property_initialized_result( + ctx: &mut FunctionContext<'_>, + slot: &StaticPropertySlot, +) { + if !slot.is_declared { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + return; + } + emit_static_property_initialized_bool(ctx, slot); +} + /// Emits a direct static property store into the fallback declaring-class symbol. fn emit_direct_store_static_property_result( ctx: &mut FunctionContext<'_>, @@ -231,6 +253,28 @@ fn emit_direct_store_static_property_result( clear_uninitialized_marker_after_static_store(ctx, &slot.symbol, &slot.php_type); } +/// Compares a typed static-property marker with the uninitialized sentinel. +fn emit_static_property_initialized_bool( + ctx: &mut FunctionContext<'_>, + slot: &StaticPropertySlot, +) { + let marker_reg = abi::secondary_scratch_reg(ctx.emitter); + let sentinel_reg = abi::tertiary_scratch_reg(ctx.emitter); + abi::emit_load_symbol_to_reg(ctx.emitter, marker_reg, &slot.symbol, 8); + abi::emit_load_int_immediate(ctx.emitter, sentinel_reg, UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the static property marker against the uninitialized sentinel + ctx.emitter.instruction("cset x0, ne"); // materialize true when the static property is initialized + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", marker_reg, sentinel_reg)); // compare the static property marker against the uninitialized sentinel + ctx.emitter.instruction("setne al"); // materialize true when the static property is initialized + ctx.emitter.instruction("movzx rax, al"); // widen the initialization flag into the integer result register + } + } +} + /// Loads the forwarded called-class id into `dest_reg` when the current frame has it. fn emit_called_class_id_to_reg( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index ac62c79f38..ca7a1b666c 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -998,6 +998,7 @@ fn referenced_static_property_class_names(module: &Module) -> HashSet { Op::LoadStaticProperty | Op::StoreStaticProperty | Op::LoadReflectionStaticProperty + | Op::ReflectionStaticPropertyInitialized | Op::StoreReflectionStaticProperty ) { continue; diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 30423780a5..49cd41459f 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -205,6 +205,7 @@ pub enum Op { StoreStaticProperty, LoadReflectionStaticProperty, StoreReflectionStaticProperty, + ReflectionStaticPropertyInitialized, IAdd, ISub, IMul, @@ -317,6 +318,7 @@ pub enum Op { DynamicObjectNewMixed, DynamicObjectNewWithoutConstructorMixed, PropGet, + PropInitialized, PropSet, /// Loads the raw reference-cell pointer stored in a reference property's slot, /// without dereferencing it. Used to alias a local to `$obj->prop` and to return @@ -448,8 +450,9 @@ impl Op { }, AliasLocalRefCell => E::READS_LOCAL | E::WRITES_LOCAL, ReleaseLocalRefCell => E::READS_LOCAL | E::WRITES_LOCAL | E::WRITES_HEAP | E::REFCOUNT_OP, - LoadGlobal | LoadStaticProperty | LoadReflectionStaticProperty | ScopedConstantGet - | ClassAttrNames | ClassAttrArgs | ClassGetAttributes | CatchCurrent => E::READS_GLOBAL, + LoadGlobal | LoadStaticProperty | LoadReflectionStaticProperty + | ReflectionStaticPropertyInitialized | ScopedConstantGet | ClassAttrNames + | ClassAttrArgs | ClassGetAttributes | CatchCurrent => E::READS_GLOBAL, StoreGlobal | StoreStaticLocal | StoreStaticProperty | StoreReflectionStaticProperty | InitStaticLocal | IncludeOnceMark | FunctionVariantMark | TryPushHandler | TryPopHandler => E::WRITES_GLOBAL, @@ -473,9 +476,8 @@ impl Op { | HashCloneShallow | ObjectCloneShallow => { E::READS_HEAP | E::ALLOC_HEAP | E::REFCOUNT_OP } - ArrayLen | HashLen | ArrayKeyExists | OffsetExists | PropGet | LoadPropRefCell => { - E::READS_HEAP - } + ArrayLen | HashLen | ArrayKeyExists | OffsetExists | PropGet | PropInitialized + | LoadPropRefCell => E::READS_HEAP, LoadArrayElemRefCell => E::READS_HEAP | E::MAY_FATAL, BindRefCellPtr => E::WRITES_LOCAL, ArraySet | HashSet | HashUnset | ArrayPush | HashAppend | OffsetUnset | PropSet @@ -579,6 +581,7 @@ impl Op { StoreStaticProperty => "store_static_property", LoadReflectionStaticProperty => "load_reflection_static_property", StoreReflectionStaticProperty => "store_reflection_static_property", + ReflectionStaticPropertyInitialized => "reflection_static_property_initialized", IAdd => "iadd", ISub => "isub", IMul => "imul", @@ -693,6 +696,7 @@ impl Op { "dynamic_object_new_without_constructor_mixed" } PropGet => "prop_get", + PropInitialized => "prop_initialized", PropSet => "prop_set", LoadPropRefCell => "load_prop_ref_cell", LoadArrayElemRefCell => "load_array_elem_ref_cell", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 846b7ee7ab..487223e93a 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -275,7 +275,10 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionCallArray | EvalFunctionExists | EvalClassExists | EvalConstantExists | EvalConstantFetch - | EnumBackingStringToInt | EnumBackingMixedToInt => { + | EnumBackingStringToInt + | EnumBackingMixedToInt + | PropInitialized + | ReflectionStaticPropertyInitialized => { require_immediate(inst_id, inst, "data id", |imm| matches!(imm, Imm::Data(_))) } LoadLocal | StoreLocal | UnsetLocal | LoadRefCell | StoreRefCell | ReleaseLocalRefCell @@ -395,7 +398,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio } BufferNew => check_unary(function, inst_id, inst, IrType::I64, "I64"), LoadLocal | LoadRefCell | LoadGlobal | LoadStaticLocal | LoadStaticProperty - | LoadReflectionStaticProperty | ExternGlobalLoad => { + | LoadReflectionStaticProperty | ReflectionStaticPropertyInitialized | ExternGlobalLoad => { check_count(inst_id, inst, 0, "0") } UnsetLocal | PromoteLocalRefCell | AliasLocalRefCell | ReleaseLocalRefCell => { @@ -439,6 +442,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio } DynamicObjectNewWithoutConstructorMixed | PropGet + | PropInitialized | PropSet | LoadPropRefCell | DynamicPropGet diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index ea0b628057..8799292949 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10407,6 +10407,21 @@ fn lower_reflection_property_value_call( let (_, property, _) = reflection_property_instance_target(ctx, object_expr)?; lower_reflection_property_set_value(ctx, &property, args, expr) } + "isinitialized" => { + if let Some((declaring_class, property, _)) = + reflection_property_static_target(ctx, object_expr) + { + return lower_reflection_property_static_is_initialized( + ctx, + &declaring_class, + &property, + args, + expr, + ); + } + let (_, property, _) = reflection_property_any_instance_target(ctx, object_expr)?; + lower_reflection_property_is_initialized(ctx, &property, args, expr) + } _ => None, } } @@ -10448,6 +10463,26 @@ fn lower_reflection_property_set_value( Some(lower_null(ctx, expr)) } +/// Lowers `ReflectionProperty::isInitialized($object)` to a direct slot probe. +fn lower_reflection_property_is_initialized( + ctx: &mut LoweringContext<'_, '_>, + property: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let object_arg = reflection_property_get_value_arg(args)?; + let object = lower_expr(ctx, &object_arg); + let data = ctx.intern_string(property); + Some(ctx.emit_value( + Op::PropInitialized, + vec![object.value], + Some(Immediate::Data(data)), + PhpType::Bool, + Op::PropInitialized.default_effects(), + Some(expr.span), + )) +} + /// Lowers static `ReflectionProperty::getValue()` to a reflection static-property read. fn lower_reflection_property_get_static_value( ctx: &mut LoweringContext<'_, '_>, @@ -10469,6 +10504,25 @@ fn lower_reflection_property_get_static_value( )) } +/// Lowers static `ReflectionProperty::isInitialized()` to a direct static-slot probe. +fn lower_reflection_property_static_is_initialized( + ctx: &mut LoweringContext<'_, '_>, + declaring_class: &str, + property: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + if let Some(ignored_object) = reflection_property_static_get_value_ignored_arg(args)? { + lower_ignored_reflection_argument(ctx, &ignored_object); + } + Some(lower_reflection_static_property_initialized_by_class_name( + ctx, + declaring_class, + property, + expr, + )) +} + /// Lowers static `ReflectionProperty::setValue(null, $value)` to a reflection static-property write. fn lower_reflection_property_set_static_value( ctx: &mut LoweringContext<'_, '_>, @@ -10601,6 +10655,28 @@ fn reflection_property_instance_target( )) } +/// Resolves a known non-static ReflectionProperty target without enforcing visibility. +fn reflection_property_any_instance_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String, PhpType)> { + let (class_name, property) = reflection_property_reflected_target(ctx, object_expr)?; + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + if class_info + .static_properties + .iter() + .any(|(name, _)| name == &property) + { + return None; + } + let (_, (_, property_ty)) = class_info.visible_property(&property)?; + Some(( + class_name, + property, + normalize_value_php_type(property_ty.codegen_repr()), + )) +} + /// Resolves an inline `ReflectionProperty` target for a static property. fn reflection_property_static_target( ctx: &LoweringContext<'_, '_>, @@ -11306,6 +11382,23 @@ fn lower_reflection_static_property_get_by_class_name( ) } +/// Emits a visibility-bypassing static-property initialization probe. +fn lower_reflection_static_property_initialized_by_class_name( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + expr: &Expr, +) -> LoweredValue { + lower_static_property_get_by_class_name_with_op( + ctx, + class_name, + property, + PhpType::Bool, + expr, + Op::ReflectionStaticPropertyInitialized, + ) +} + /// Emits a static-property read using the requested static-property opcode. fn lower_static_property_get_by_class_name_with_op( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 39e1494ce9..2dfcaed8c2 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2822,6 +2822,57 @@ fn builtin_reflection_property_set_value_method() -> ClassMethod { } } +/// Returns `ReflectionProperty::isInitialized()` for supported materialized reflectors. +fn builtin_reflection_property_is_initialized_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let dynamic_return = Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_dynamic", dummy_span), + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ); + let defaulted_return = Stmt::new( + StmtKind::If { + condition: reflection_this_property("__has_default_value", dummy_span), + then_body: vec![Stmt::new(StmtKind::Return(true_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ); + ClassMethod { + name: "isInitialized".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(mixed_type()), null_expr(), false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![ + reflection_property_static_value_guard("isInitialized", dummy_span), + reflection_property_object_required_guard("isInitialized", dummy_span), + dynamic_return, + defaulted_return, + throw_new_reflection_exception( + string_lit( + "ReflectionProperty::isInitialized() for typed properties without defaults requires an inline known property", + dummy_span, + ), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Builds a guard for static property value access that still needs inline lowering. fn reflection_property_static_value_guard(method: &str, span: crate::span::Span) -> Stmt { Stmt::new( @@ -3289,6 +3340,7 @@ fn add_reflection_member_flag_methods( methods.push(builtin_reflection_property_skip_lazy_initialization_method()); methods.push(builtin_reflection_property_get_value_method()); methods.push(builtin_reflection_property_set_value_method()); + methods.push(builtin_reflection_property_is_initialized_method()); methods.push(builtin_reflection_property_modifier_mask_method( "isProtectedSet", 2048, @@ -3692,6 +3744,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ispromoted", "isvirtual", "isdynamic", + "isinitialized", "isprotectedset", "isprivateset", ] { diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 91cc671174..9979767d64 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -69,6 +69,66 @@ echo ":" . ReflectStaticValueAccessTarget::$count; assert_eq!(out, "2:17:19:old:new:23"); } +/// Verifies `ReflectionProperty::isInitialized()` observes typed instance +/// property initialization without reading the property value. +#[test] +fn test_reflection_property_is_initialized_for_instance_properties() { + let out = compile_and_run( + r#"hidden = 7; + } +} + +$target = new ReflectInitializedInstanceTarget(); +$typed = new ReflectionProperty(ReflectInitializedInstanceTarget::class, "typed"); +echo $typed->isInitialized($target) ? "bad" : "uninit"; +$target->typed = 3; +echo ":" . ($typed->isInitialized(object: $target) ? "typed" : "bad"); +echo ":" . ((new ReflectionProperty(ReflectInitializedInstanceTarget::class, "nullable"))->isInitialized($target) ? "nullable" : "bad"); +echo ":" . ((new ReflectionClass(ReflectInitializedInstanceTarget::class))->getProperty("implicit")->isInitialized($target) ? "implicit" : "bad"); +echo ":" . ((new ReflectionProperty(ReflectInitializedInstanceTarget::class, "hidden"))->isInitialized($target) ? "hidden" : "bad"); +"#, + ); + assert_eq!(out, "uninit:typed:nullable:implicit:hidden"); +} + +/// Verifies `ReflectionProperty::isInitialized()` observes static-property +/// initialization while bypassing property visibility. +#[test] +fn test_reflection_property_is_initialized_for_static_properties() { + let out = compile_and_run( + r#"isInitialized() ? "bad" : "uninit"; +ReflectInitializedStaticTarget::$typed = 3; +echo ":" . ($typed->isInitialized(object: null) ? "typed" : "bad"); +echo ":" . ((new ReflectionProperty(ReflectInitializedStaticTarget::class, "nullable"))->isInitialized() ? "nullable" : "bad"); +$hidden = (new ReflectionClass(ReflectInitializedStaticTarget::class))->getProperty("hidden"); +echo ":" . ($hidden->isInitialized() ? "bad" : "hidden-uninit"); +ReflectInitializedStaticTarget::initHidden(); +echo ":" . ($hidden->isInitialized() ? "hidden" : "bad"); +"#, + ); + assert_eq!(out, "uninit:typed:nullable:hidden-uninit:hidden"); +} + /// Verifies ReflectionProperty static value access bypasses visibility for /// private and protected properties when the reflected target is statically known. #[test] From d9cd6148308c1778023fbbf810b445fcdf62dac7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 12:52:37 +0200 Subject: [PATCH 0546/1208] Throw on missing ReflectionClass static value --- docs/php/classes.md | 4 ++-- src/ir_lower/expr/mod.rs | 32 +++++++++++++++++++++++++++++++- tests/codegen/oop/attributes.rs | 11 ++++++++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 25985e4369..fcee329e8d 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1170,7 +1170,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::getConstants()` | `new ReflectionClass($class_name)` | Return an associative array of visible constant values keyed by constant or enum-case name | | `ReflectionClass::getDefaultProperties()` | `new ReflectionClass($class_name)` | Return an associative array of supported materialized instance/static property defaults keyed by property name | | `ReflectionClass::getStaticProperties()` | `new ReflectionClass($class_name)` | Return an associative array of supported materialized static property values keyed by property name | -| `ReflectionClass::getStaticPropertyValue()` | `new ReflectionClass($class_name)` | Return a supported static property value, or the explicit default when the property is missing | +| `ReflectionClass::getStaticPropertyValue()` | `new ReflectionClass($class_name)` | Return a supported static property value, return the explicit default when the property is missing, or throw `ReflectionException` for supported missing-property lookups without a default | | `ReflectionClass::setStaticPropertyValue()` | `new ReflectionClass($class_name)` | Write a supported static property value | | `ReflectionClass::getReflectionConstant()` | `new ReflectionClass($class_name)` | Return a `ReflectionClassConstant` object for the named visible constant or enum case, or `false` when no such constant is visible | | `ReflectionClass::getReflectionConstants()` | `new ReflectionClass($class_name)` | Return indexed `ReflectionClassConstant` objects for visible class constants and enum cases | @@ -1350,7 +1350,7 @@ Limitations today: - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. -- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; inline tracked receivers also throw a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 8799292949..24cd292dff 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -11212,7 +11212,10 @@ fn lower_reflection_class_get_static_property_value( } return None; } - default.map(|default| lower_expr(ctx, &default)) + Some(match default { + Some(default) => lower_expr(ctx, &default), + None => lower_reflection_class_missing_static_property(ctx, class_name, &property, expr), + }) } /// Lowers `ReflectionClass::setStaticPropertyValue()` to a live static-property write. @@ -11235,6 +11238,33 @@ fn lower_reflection_class_set_static_property_value( Some(lower_null(ctx, expr)) } +/// Lowers a missing static-property lookup to PHP's catchable ReflectionException. +fn lower_reflection_class_missing_static_property( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + property: &str, + expr: &Expr, +) -> LoweredValue { + let message = format!( + "Property {}::${} does not exist", + class_name.trim_start_matches('\\'), + property + ); + let exception = Expr::new( + ExprKind::NewObject { + class_name: Name::unqualified("ReflectionException"), + args: vec![Expr::new(ExprKind::StringLiteral(message), expr.span)], + }, + expr.span, + ); + let placeholder = lower_null(ctx, expr); + let exception = lower_expr(ctx, &exception); + ctx.builder.terminate(Terminator::Throw { + value: exception.value, + }); + placeholder +} + /// Returns synthetic array entries for current static-property values on a reflected class. fn reflection_class_static_property_map_entries( ctx: &LoweringContext<'_, '_>, diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 7006bedd92..ffd091ea5d 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2697,6 +2697,12 @@ echo ":" . (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticProp (new ReflectionClass(ReflectStaticValueTarget::class))->setStaticPropertyValue("count", 11); echo ":" . ReflectStaticValueTarget::$count; echo ":" . (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticPropertyValue("missing", "fallback"); +try { + (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticPropertyValue("missing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} "#, ); assert!( @@ -2704,7 +2710,10 @@ echo ":" . (new ReflectionClass(ReflectStaticValueTarget::class))->getStaticProp "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "7:9:11:fallback"); + assert_eq!( + out.stdout, + "7:9:11:fallback:ReflectionException:Property ReflectStaticValueTarget::$missing does not exist" + ); } /// Verifies a local `ReflectionClass` receiver keeps statically-known class metadata. From 43cccb2cf920c93ce5d587ac9367b4a8b70c84ef Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 12:57:35 +0200 Subject: [PATCH 0547/1208] Preserve ReflectionClass metadata in try bodies --- docs/php/classes.md | 2 +- src/ir_lower/stmt/mod.rs | 4 ++-- tests/codegen/oop/attributes.rs | 11 ++++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index fcee329e8d..3fa5aed903 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1350,7 +1350,7 @@ Limitations today: - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. -- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; inline tracked receivers also throw a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index aa93a36d5d..807427f914 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -1563,7 +1563,6 @@ fn lower_try_catch( let after_block = ctx.builder.create_named_block("try.after", Vec::new()); let handler_token = handler_block.as_raw() as i64; - ctx.clear_static_callable_locals(); ctx.emit_void( Op::TryPushHandler, Vec::new(), @@ -1578,6 +1577,7 @@ fn lower_try_catch( } ctx.builder.position_at_end(handler_block); + ctx.clear_static_callable_locals(); emit_try_pop_handler(ctx, handler_token, span); lower_catch_dispatch(ctx, catches, after_block, span); ctx.builder.position_at_end(after_block); @@ -1625,7 +1625,6 @@ fn lower_try_catch_finally( let after_block = ctx.builder.create_named_block("try.after", Vec::new()); let handler_token = handler_block.as_raw() as i64; - ctx.clear_static_callable_locals(); ctx.emit_void( Op::TryPushHandler, Vec::new(), @@ -1643,6 +1642,7 @@ fn lower_try_catch_finally( } ctx.builder.position_at_end(handler_block); + ctx.clear_static_callable_locals(); emit_try_pop_handler(ctx, handler_token, span); lower_catch_dispatch_with_finally(ctx, catches, after_block, finally_body, span); ctx.builder.position_at_end(after_block); diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index ffd091ea5d..af250493ad 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2738,6 +2738,12 @@ $obj = $ref->newInstance(id: 5, name: "Ada"); echo ":" . $obj->id . ":" . $obj->name; $obj2 = $ref->newInstanceArgs(["name" => "Bob", "id" => 6]); echo ":" . $obj2->id . ":" . $obj2->name; +try { + $ref->getStaticPropertyValue("missing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":" . get_class($e) . ":" . $e->getMessage(); +} "#, ); assert!( @@ -2745,7 +2751,10 @@ echo ":" . $obj2->id . ":" . $obj2->name; "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "7:9:11:fallback:5:Ada:6:Bob"); + assert_eq!( + out.stdout, + "7:9:11:fallback:5:Ada:6:Bob:ReflectionException:Property ReflectTrackedClassTarget::$missing does not exist" + ); } /// Verifies `ReflectionClass::getStaticProperties()` reads current AOT static values. From dd78006d693095f5948dc1f31a26354f7de0d2cf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 13:27:14 +0200 Subject: [PATCH 0548/1208] Support AOT ReflectionMethod invoke --- docs/php/classes.md | 3 +- docs/php/eval.md | 8 +- src/ir_lower/context.rs | 29 ++ src/ir_lower/expr/mod.rs | 383 ++++++++++++++++++ src/ir_lower/stmt/mod.rs | 12 +- src/types/checker/builtin_types/reflection.rs | 35 ++ tests/codegen/oop.rs | 2 + tests/codegen/oop/reflection_methods.rs | 58 +++ 8 files changed, 526 insertions(+), 4 deletions(-) create mode 100644 tests/codegen/oop/reflection_methods.rs diff --git a/docs/php/classes.md b/docs/php/classes.md index 3fa5aed903..6bb5a39068 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1208,7 +1208,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isDestructor()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is the destructor | | `ReflectionMethod::getModifiers()` | `new ReflectionMethod($class_name, $method_name)` | Return PHP's `ReflectionMethod::IS_*` visibility/static/finality/abstract bitmask | | `ReflectionMethod::setAccessible()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Accepted as a PHP-compatible no-op for eval-backed method reflection | -| `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments | +| `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments; `invoke()` also lowers inline/tracked generated/AOT methods with declared parameter types | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | @@ -1349,6 +1349,7 @@ Limitations today: - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionMethod::invoke()` is supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, method-name case-insensitivity, defaults, and named arguments. `ReflectionMethod::invokeArgs()` and invoke targets whose parameter types only come from call-site inference still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index f71f2d40b7..95b293ec9a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -315,7 +315,13 @@ methods, bypass public/protected/private visibility like PHP reflection, preserve named arguments for the invoked method, follow PHP's by-value `invoke()` variadic forwarding, accept `null` or an object for static methods, and throw catchable `ReflectionException` values when an instance receiver is -not compatible with the reflected declaring class. +not compatible with the reflected declaring class. For generated/AOT classes, +`ReflectionMethod::invoke()` is also lowered for inline or straight-line +tracked reflectors when the reflected method has declared parameter types; +the lowered call supports instance and static methods, method-name +case-insensitivity, defaults, and named arguments. Generated/AOT +`ReflectionMethod::invokeArgs()` and invoke targets whose parameter types only +come from call-site inference still require richer runtime/typechecker support. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index eff255dd7d..bfc47f2be9 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -122,6 +122,7 @@ pub(crate) struct LoweringContext<'m, 'f> { static_callable_locals: HashMap, reflection_class_locals: HashMap, reflection_property_locals: HashMap, + reflection_method_locals: HashMap, fiber_start_sigs: HashMap, ref_bound_locals: HashSet, ref_cell_owner_locals: HashMap, @@ -198,6 +199,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { static_callable_locals: HashMap::new(), reflection_class_locals: HashMap::new(), reflection_property_locals: HashMap::new(), + reflection_method_locals: HashMap::new(), fiber_start_sigs: HashMap::new(), ref_bound_locals: HashSet::new(), ref_cell_owner_locals: HashMap::new(), @@ -703,6 +705,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.clear_static_callable_local(name); self.clear_reflection_class_local(name); self.clear_reflection_property_local(name); + self.clear_reflection_method_local(name); self.clear_fiber_start_sig(name); if let Some(extern_type) = self.extern_global_type(name) { let release_source_after_store = self.value_is_owning_temporary(value); @@ -925,6 +928,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.clear_static_callable_local(name); self.clear_reflection_class_local(name); self.clear_reflection_property_local(name); + self.clear_reflection_method_local(name); self.clear_fiber_start_sig(name); let previous_kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, previous_kind); @@ -958,6 +962,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.clear_static_callable_local(name); self.clear_reflection_class_local(name); self.clear_reflection_property_local(name); + self.clear_reflection_method_local(name); self.clear_fiber_start_sig(name); let slot = self.declare_local(name, PhpType::Void); self.release_ref_cell_owner(name, span); @@ -1332,6 +1337,24 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_property_locals.get(name).cloned() } + /// Records that a PHP local currently holds a statically-known `ReflectionMethod` object. + pub(crate) fn bind_reflection_method_local( + &mut self, + name: &str, + reflected_class: String, + reflected_method: String, + ) { + if self.can_track_static_callable_local(name) { + self.reflection_method_locals + .insert(name.to_string(), (reflected_class, reflected_method)); + } + } + + /// Returns the reflected class/method associated with a local `ReflectionMethod`. + pub(crate) fn reflection_method_local(&self, name: &str) -> Option<(String, String)> { + self.reflection_method_locals.get(name).cloned() + } + /// Records that a PHP local currently holds a Fiber with a known callback signature. pub(crate) fn bind_fiber_start_sig(&mut self, name: &str, sig: FunctionSig) { if self.can_track_static_callable_local(name) { @@ -1370,6 +1393,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_property_locals.remove(name); } + /// Clears the compile-time `ReflectionMethod` association for one local. + pub(crate) fn clear_reflection_method_local(&mut self, name: &str) { + self.reflection_method_locals.remove(name); + } + /// Clears the known Fiber callback association for one local. pub(crate) fn clear_fiber_start_sig(&mut self, name: &str) { self.fiber_start_sigs.remove(name); @@ -1380,6 +1408,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.static_callable_locals.clear(); self.reflection_class_locals.clear(); self.reflection_property_locals.clear(); + self.reflection_method_locals.clear(); self.fiber_start_sigs.clear(); } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 24cd292dff..68b18911c4 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1514,6 +1514,8 @@ fn lower_assignment_expr( let reflected_class = assigned_name.and_then(|_| reflection_class_binding_for_expr(ctx, value)); let reflected_property = assigned_name.and_then(|_| reflection_property_binding_for_expr(ctx, value)); + let reflected_method = + assigned_name.and_then(|_| reflection_method_binding_for_expr(ctx, value)); let fiber_start_sig = assigned_name.and_then(|_| crate::ir_lower::fibers::start_sig_for_expr(ctx, value)); let callable_array = assigned_name @@ -1554,6 +1556,9 @@ fn lower_assignment_expr( if let Some((reflected_class, reflected_property)) = reflected_property { ctx.bind_reflection_property_local(name, reflected_class, reflected_property); } + if let Some((reflected_class, reflected_method)) = reflected_method { + ctx.bind_reflection_method_local(name, reflected_class, reflected_method); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -3609,6 +3614,14 @@ pub(crate) fn reflection_property_binding_for_expr( reflection_property_reflected_target(ctx, expr) } +/// Returns the reflected method captured by a statically-known `ReflectionMethod` expression. +pub(crate) fn reflection_method_binding_for_expr( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, +) -> Option<(String, String)> { + reflection_method_reflected_target(ctx, expr) +} + /// EIR value and callable binding produced by a callable-array assignment. pub(crate) struct LoweredCallableArrayAssignment { pub(crate) value: LoweredValue, @@ -9972,6 +9985,17 @@ fn lower_method_call( return value; } } + if op == Op::MethodCall { + if let Some(value) = lower_reflection_method_invoke_call( + ctx, + Some(object_expr), + method, + args, + expr, + ) { + return value; + } + } if op == Op::MethodCall { if let Some(value) = lower_reflection_property_value_call(ctx, Some(object_expr), method, args, expr) @@ -10366,6 +10390,201 @@ fn reflection_member_constructor_expr( ) } +/// Lowers `ReflectionMethod::invoke()` for statically-known reflected methods. +fn lower_reflection_method_invoke_call( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + if php_symbol_key(method) != "invoke" { + return None; + } + let object_expr = object_expr?; + let (class_name, reflected_method) = reflection_method_reflected_target(ctx, object_expr)?; + let Some((object_arg, forwarded_args)) = reflection_method_invoke_args(args) else { + return Some(lower_reflection_method_invoke_unsupported(ctx, expr)); + }; + let Some(target_kind) = reflection_method_target_kind(ctx, &class_name, &reflected_method) + else { + return Some(lower_reflection_method_invoke_unsupported(ctx, expr)); + }; + if !reflection_method_target_has_declared_params( + ctx, + &class_name, + &reflected_method, + target_kind, + ) + { + return Some(lower_reflection_method_invoke_unsupported(ctx, expr)); + } + match target_kind { + ReflectionMethodTargetKind::Static => Some(lower_reflection_static_method_invoke( + ctx, + &class_name, + &reflected_method, + &object_arg, + &forwarded_args, + expr, + )), + ReflectionMethodTargetKind::Instance => Some(lower_reflection_instance_method_invoke( + ctx, + &reflected_method, + &object_arg, + &forwarded_args, + expr, + )), + } +} + +/// Lowers a static reflected-method invocation after evaluating the ignored object slot. +fn lower_reflection_static_method_invoke( + ctx: &mut LoweringContext<'_, '_>, + class_name: &str, + reflected_method: &str, + object_arg: &Expr, + forwarded_args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let ignored_object = lower_expr(ctx, object_arg); + if ctx.value_is_owning_temporary(ignored_object) { + crate::ir_lower::ownership::release_if_owned( + ctx, + ignored_object, + Some(object_arg.span), + ); + } + let receiver = StaticReceiver::Named(Name::from(class_name.to_string())); + lower_static_method_call(ctx, &receiver, reflected_method, forwarded_args, expr) +} + +/// Lowers an instance reflected-method invocation using the first invoke argument as receiver. +fn lower_reflection_instance_method_invoke( + ctx: &mut LoweringContext<'_, '_>, + reflected_method: &str, + object_arg: &Expr, + forwarded_args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let object = lower_expr(ctx, object_arg); + if value_is_definitely_null(ctx, object.value) { + let null_value = lower_null(ctx, expr); + terminate_method_call_on_null(ctx, reflected_method); + return null_value; + } + if value_is_nullable(ctx, object.value) { + return lower_nullable_regular_method_call(ctx, object, reflected_method, forwarded_args, expr); + } + lower_method_call_with_receiver( + ctx, + object, + reflected_method, + forwarded_args, + Op::MethodCall, + expr, + ) +} + +/// Splits `ReflectionMethod::invoke($object, ...$args)` into receiver and method args. +fn reflection_method_invoke_args(args: &[Expr]) -> Option<(Expr, Vec)> { + let args = reflection_class_new_instance_args(args); + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [object, forwarded @ ..] => Some((object.clone(), forwarded.to_vec())), + _ => None, + }; + } + let mut object = None; + let mut forwarded = Vec::new(); + let mut args = args.into_iter(); + if let Some(first) = args.next() { + match first.kind { + ExprKind::NamedArg { ref name, ref value } if php_symbol_key(name) == "object" => { + object = Some((**value).clone()); + } + ExprKind::NamedArg { .. } => forwarded.push(first), + _ => object = Some(first), + } + } + for arg in args { + match arg.kind { + ExprKind::NamedArg { ref name, ref value } if php_symbol_key(name) == "object" => { + if object.replace((**value).clone()).is_some() { + return None; + } + } + _ => forwarded.push(arg), + } + } + object.map(|object| (object, forwarded)) +} + +/// Classifies whether a known reflected method is static or instance-dispatched. +fn reflection_method_target_kind( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + method: &str, +) -> Option { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + let method_key = php_symbol_key(method); + if class_info.static_methods.contains_key(&method_key) { + return Some(ReflectionMethodTargetKind::Static); + } + if class_info.methods.contains_key(&method_key) { + return Some(ReflectionMethodTargetKind::Instance); + } + None +} + +/// Returns true when reflected invocation can trust the target method's parameter types. +fn reflection_method_target_has_declared_params( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + method: &str, + target_kind: ReflectionMethodTargetKind, +) -> bool { + let signature = match target_kind { + ReflectionMethodTargetKind::Instance => { + class_method_signature( + ctx, + class_name.trim_start_matches('\\'), + &php_symbol_key(method), + ) + } + ReflectionMethodTargetKind::Static => { + let receiver = StaticReceiver::Named(Name::from(class_name.to_string())); + static_method_implementation_signature(ctx, &receiver, method) + } + }; + signature.is_some_and(|signature| { + signature + .declared_params + .iter() + .all(|is_declared| *is_declared) + }) +} + +/// Dispatch kind for a statically-known reflected method. +#[derive(Clone, Copy)] +enum ReflectionMethodTargetKind { + Instance, + Static, +} + +/// Emits a runtime fatal for ReflectionMethod invocation forms not yet lowered. +fn lower_reflection_method_invoke_unsupported( + ctx: &mut LoweringContext<'_, '_>, + expr: &Expr, +) -> LoweredValue { + let result = lower_boxed_null(ctx, expr); + let message = ctx.intern_string( + "Fatal error: unsupported ReflectionMethod::invoke() target or argument forwarding\n", + ); + ctx.builder.terminate(Terminator::Fatal { message }); + result +} + /// Lowers `ReflectionProperty::getValue($object)` when the reflected property is known. fn lower_reflection_property_value_call( ctx: &mut LoweringContext<'_, '_>, @@ -10704,6 +10923,66 @@ fn reflection_property_reflected_target( }) } +/// Extracts the known class and method name from a supported ReflectionMethod source. +fn reflection_method_reflected_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + reflection_method_constructor_target(ctx, object_expr) + .or_else(|| reflection_method_class_get_method_target(ctx, object_expr)) + .or_else(|| reflection_method_class_get_methods_index_target(ctx, object_expr)) + .or_else(|| { + let ExprKind::Variable(name) = &object_expr.kind else { + return None; + }; + ctx.reflection_method_local(name) + }) +} + +/// Extracts a known ReflectionMethod from `ReflectionClass::getMethods()[N]`. +fn reflection_method_class_get_methods_index_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::ArrayAccess { array, index } = &object_expr.kind else { + return None; + }; + let ExprKind::IntLiteral(raw_index) = &index.kind else { + return None; + }; + if *raw_index < 0 { + return None; + } + let ExprKind::MethodCall { + object, + method, + args, + } = &array.kind + else { + return None; + }; + if php_symbol_key(method) != "getmethods" { + return None; + } + let filter = reflection_class_get_methods_filter_arg(ctx, args)?; + let class_name = reflection_class_reflected_class(ctx, object)?; + let method = + reflection_class_method_name_at_index(ctx, &class_name, *raw_index as usize, filter)?; + Some((class_name, method)) +} + +/// Returns the `ReflectionClass::getMethods()` method name at a known index. +fn reflection_class_method_name_at_index( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + index: usize, + filter: Option, +) -> Option { + reflection_class_method_names_for_filter(ctx, class_name, filter)? + .into_iter() + .nth(index) +} + /// Extracts a known ReflectionProperty from `ReflectionClass::getProperties()[N]`. fn reflection_property_class_get_properties_index_target( ctx: &LoweringContext<'_, '_>, @@ -11073,6 +11352,31 @@ fn reflection_property_constructor_target( Some((class_name, property)) } +/// Extracts the known class and method name from an inline ReflectionMethod constructor. +fn reflection_method_constructor_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::NewObject { class_name, args } = &object_expr.kind else { + return None; + }; + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionmethod" { + return None; + } + let (class_arg, method_arg) = reflection_method_constructor_regular_args(ctx, args)?; + let raw_class_name = match &class_arg.kind { + ExprKind::StringLiteral(value) => value.clone(), + ExprKind::ClassConstant { receiver } => static_receiver_class_name(ctx, receiver)?, + _ => return None, + }; + let class_name = resolve_known_class_name(ctx, &raw_class_name)?; + let ExprKind::StringLiteral(method) = method_arg.kind else { + return None; + }; + let method = resolve_known_class_method_name(ctx, &class_name, &method)?; + Some((class_name, method)) +} + /// Extracts the property target from inline `ReflectionClass::getProperty()` calls. fn reflection_property_class_get_property_target( ctx: &LoweringContext<'_, '_>, @@ -11094,6 +11398,28 @@ fn reflection_property_class_get_property_target( Some((class_name, property)) } +/// Extracts the method target from inline `ReflectionClass::getMethod()` calls. +fn reflection_method_class_get_method_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::MethodCall { + object, + method, + args, + } = &object_expr.kind + else { + return None; + }; + if php_symbol_key(method) != "getmethod" { + return None; + } + let class_name = reflection_class_reflected_class(ctx, object)?; + let method = reflection_class_member_name_arg(args)?; + let method = resolve_known_class_method_name(ctx, &class_name, &method)?; + Some((class_name, method)) +} + /// Returns the literal name argument passed to a ReflectionClass member lookup. fn reflection_class_member_name_arg(args: &[Expr]) -> Option { let args = reflection_class_new_instance_args(args); @@ -11145,6 +11471,47 @@ fn reflection_property_constructor_regular_args( Some((class_arg, property_arg)) } +/// Returns normalized constructor args for `ReflectionMethod($class, $method)`. +fn reflection_method_constructor_regular_args( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option<(Expr, Expr)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [class_arg, method_arg] => Some((class_arg.clone(), method_arg.clone())), + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionMethod") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + let class_arg = planned_regular_arg_expr(plan.regular_args.first()?)?.clone(); + let method_arg = planned_regular_arg_expr(plan.regular_args.get(1)?)?.clone(); + Some((class_arg, method_arg)) +} + /// Lowers `ReflectionClass::getStaticProperties()` to a live static-property map. fn lower_reflection_class_get_static_properties( ctx: &mut LoweringContext<'_, '_>, @@ -11641,6 +12008,22 @@ fn resolve_known_class_name(ctx: &LoweringContext<'_, '_>, class_name: &str) -> .cloned() } +/// Resolves a PHP method name case-insensitively against known class metadata. +fn resolve_known_class_method_name( + ctx: &LoweringContext<'_, '_>, + class_name: &str, + method: &str, +) -> Option { + let class_info = ctx.classes.get(class_name.trim_start_matches('\\'))?; + let key = php_symbol_key(method); + class_info + .methods + .keys() + .chain(class_info.static_methods.keys()) + .find(|candidate| php_symbol_key(candidate) == key) + .cloned() +} + /// Returns constructor signature metadata for a known class name. fn constructor_signature_for_class_name<'a>( ctx: &'a LoweringContext<'_, '_>, diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index 807427f914..0cff976b42 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -20,8 +20,9 @@ use crate::ir_lower::effects_lookup; use crate::ir_lower::expr::{ array_access_element_result_type, coerce_to_int_at_span, lower_callable_array_for_assignment, lower_array_literal_with_expected_type, lower_closure_for_assignment, lower_expr, - reflection_class_binding_for_expr, reflection_property_binding_for_expr, - static_callable_binding_for_expr, string_op_uses_scratch_storage, + reflection_class_binding_for_expr, reflection_method_binding_for_expr, + reflection_property_binding_for_expr, static_callable_binding_for_expr, + string_op_uses_scratch_storage, type_satisfies_array_access_for_ir, }; use crate::names::{php_symbol_key, property_hook_set_method}; @@ -202,6 +203,9 @@ fn lower_block(ctx: &mut LoweringContext<'_, '_>, body: &[Stmt]) { /// Emits EIR for `echo`. fn lower_echo(ctx: &mut LoweringContext<'_, '_>, expr: &Expr, span: Span) { let value = lower_expr(ctx, expr); + if ctx.builder.insertion_block_is_terminated() { + return; + } ctx.emit_void( Op::EchoValue, vec![value.value], @@ -237,6 +241,7 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa let static_callable = static_callable_binding_for_expr(ctx, value); let reflected_class = reflection_class_binding_for_expr(ctx, value); let reflected_property = reflection_property_binding_for_expr(ctx, value); + let reflected_method = reflection_method_binding_for_expr(ctx, value); let fiber_start_sig = crate::ir_lower::fibers::start_sig_for_expr(ctx, value); let callable_array = lower_callable_array_for_assignment(ctx, value, static_callable.as_ref()); let lowered = callable_array @@ -272,6 +277,9 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa if let Some((reflected_class, reflected_property)) = reflected_property { ctx.bind_reflection_property_local(name, reflected_class, reflected_property); } + if let Some((reflected_class, reflected_method)) = reflected_method { + ctx.bind_reflection_method_local(name, reflected_class, reflected_method); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 2dfcaed8c2..a209faf18c 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -483,6 +483,34 @@ fn builtin_reflection_class_new_instance_method() -> ClassMethod { } } +/// Returns a public variadic `ReflectionMethod::invoke()` method shell. +/// +/// Direct AOT calls are lowered specially so the first argument becomes the +/// invocation receiver and the remaining source arguments are normalized +/// against the reflected method's own signature. +fn builtin_reflection_method_invoke_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "invoke".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("object".to_string(), Some(mixed_type()), None, false)], + param_attributes: Vec::new(), + variadic: Some("args".to_string()), + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass::newInstanceArgs()` method. /// /// Direct calls are lowered specially so the provided argument array becomes @@ -3096,6 +3124,9 @@ fn builtin_reflection_owner_class( "__required_parameter_count", )); } + if name == "ReflectionMethod" { + methods.push(builtin_reflection_method_invoke_method()); + } properties.push(builtin_property( "__attrs", Visibility::Private, @@ -3782,6 +3813,10 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invoke")) { + sig.return_type = PhpType::Mixed; + sig.variadic = Some("args".to_string()); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object( "ReflectionParameter".to_string(), diff --git a/tests/codegen/oop.rs b/tests/codegen/oop.rs index 048bfc9f60..4a728f2a01 100644 --- a/tests/codegen/oop.rs +++ b/tests/codegen/oop.rs @@ -45,3 +45,5 @@ mod property_hooks; mod datetime; #[path = "oop/reflection_properties.rs"] mod reflection_properties; +#[path = "oop/reflection_methods.rs"] +mod reflection_methods; diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs new file mode 100644 index 0000000000..2f111fa7b4 --- /dev/null +++ b/tests/codegen/oop/reflection_methods.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! End-to-end codegen tests for ReflectionMethod invocation paths over AOT +//! class metadata. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - `ReflectionMethod::invoke()` is lowered only for statically-known +//! reflectors whose target method has declared parameter types. +//! - Tests cover inline constructors, ReflectionClass lookup, local tracking, +//! case-insensitive method names, default values, and named arguments. + +use super::*; + +/// Verifies `ReflectionMethod::invoke()` calls declared AOT instance and static methods. +#[test] +fn test_reflection_method_invoke_calls_declared_aot_methods() { + let out = compile_and_run( + r#"invoke($object, "A", "C"); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeTarget::class, "JOIN"))->invoke($object, "D"); +echo ":"; +echo (new ReflectionClass(ReflectInvokeTarget::class))->getMethod("join")->invoke($object, "E", "F"); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeTarget::class, "make"))->invoke(null, right: "Y", left: "X"); +echo ":"; +$method = new ReflectionMethod(ReflectInvokeTarget::class, "join"); +echo $method->invoke($object, "L", "M"); +"#, + ); + assert_eq!(out, "AC:DB:EF:XY:LM"); +} + +/// Verifies inferred AOT method signatures are rejected instead of miscompiled. +#[test] +fn test_reflection_method_invoke_rejects_inferred_aot_signature() { + let err = compile_and_run_expect_failure( + r#"invoke($object, "A", "B"); +"#, + ); + assert!( + err.contains("Fatal error: unsupported ReflectionMethod::invoke()"), + "unexpected stderr: {err}" + ); +} From b888fe409e89bf7c74eb796af821d940b82001b1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 13:32:34 +0200 Subject: [PATCH 0549/1208] Support AOT ReflectionMethod invokeArgs --- docs/php/classes.md | 4 +- docs/php/eval.md | 6 +- src/ir_lower/expr/mod.rs | 76 ++++++++++++++++--- src/types/checker/builtin_types/reflection.rs | 34 +++++++++ tests/codegen/oop/reflection_methods.rs | 28 ++++++- 5 files changed, 131 insertions(+), 17 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 6bb5a39068..120fb9e84e 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1208,7 +1208,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isDestructor()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is the destructor | | `ReflectionMethod::getModifiers()` | `new ReflectionMethod($class_name, $method_name)` | Return PHP's `ReflectionMethod::IS_*` visibility/static/finality/abstract bitmask | | `ReflectionMethod::setAccessible()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Accepted as a PHP-compatible no-op for eval-backed method reflection | -| `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments; `invoke()` also lowers inline/tracked generated/AOT methods with declared parameter types | +| `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments; inline/tracked generated/AOT methods with declared parameter types are also lowered | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | @@ -1349,7 +1349,7 @@ Limitations today: - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. -- `ReflectionMethod::invoke()` is supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, method-name case-insensitivity, defaults, and named arguments. `ReflectionMethod::invokeArgs()` and invoke targets whose parameter types only come from call-site inference still require richer runtime/typechecker support. +- `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, method-name case-insensitivity, defaults, named arguments, and static `invokeArgs()` argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index 95b293ec9a..fcea52b42c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -316,12 +316,12 @@ preserve named arguments for the invoked method, follow PHP's by-value `invoke()` variadic forwarding, accept `null` or an object for static methods, and throw catchable `ReflectionException` values when an instance receiver is not compatible with the reflected declaring class. For generated/AOT classes, -`ReflectionMethod::invoke()` is also lowered for inline or straight-line +`ReflectionMethod::invoke()` and `invokeArgs()` are also lowered for inline or straight-line tracked reflectors when the reflected method has declared parameter types; the lowered call supports instance and static methods, method-name case-insensitivity, defaults, and named arguments. Generated/AOT -`ReflectionMethod::invokeArgs()` and invoke targets whose parameter types only -come from call-site inference still require richer runtime/typechecker support. +invoke targets whose parameter types only come from call-site inference and +runtime-only argument arrays still require richer runtime/typechecker support. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 68b18911c4..32b515d0f5 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10390,7 +10390,7 @@ fn reflection_member_constructor_expr( ) } -/// Lowers `ReflectionMethod::invoke()` for statically-known reflected methods. +/// Lowers reflected method invocation for statically-known `ReflectionMethod` objects. fn lower_reflection_method_invoke_call( ctx: &mut LoweringContext<'_, '_>, object_expr: Option<&Expr>, @@ -10398,17 +10398,19 @@ fn lower_reflection_method_invoke_call( args: &[Expr], expr: &Expr, ) -> Option { - if php_symbol_key(method) != "invoke" { - return None; - } + let method_key = php_symbol_key(method); let object_expr = object_expr?; let (class_name, reflected_method) = reflection_method_reflected_target(ctx, object_expr)?; - let Some((object_arg, forwarded_args)) = reflection_method_invoke_args(args) else { - return Some(lower_reflection_method_invoke_unsupported(ctx, expr)); + let Some((object_arg, forwarded_args)) = (match method_key.as_str() { + "invoke" => reflection_method_invoke_args(args), + "invokeargs" => reflection_method_invoke_args_array(ctx, args), + _ => return None, + }) else { + return Some(lower_reflection_method_invoke_unsupported(ctx, &method_key, expr)); }; let Some(target_kind) = reflection_method_target_kind(ctx, &class_name, &reflected_method) else { - return Some(lower_reflection_method_invoke_unsupported(ctx, expr)); + return Some(lower_reflection_method_invoke_unsupported(ctx, &method_key, expr)); }; if !reflection_method_target_has_declared_params( ctx, @@ -10417,7 +10419,7 @@ fn lower_reflection_method_invoke_call( target_kind, ) { - return Some(lower_reflection_method_invoke_unsupported(ctx, expr)); + return Some(lower_reflection_method_invoke_unsupported(ctx, &method_key, expr)); } match target_kind { ReflectionMethodTargetKind::Static => Some(lower_reflection_static_method_invoke( @@ -10520,6 +10522,51 @@ fn reflection_method_invoke_args(args: &[Expr]) -> Option<(Expr, Vec)> { object.map(|object| (object, forwarded)) } +/// Splits `ReflectionMethod::invokeArgs($object, $args)` into receiver and method args. +fn reflection_method_invoke_args_array( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option<(Expr, Vec)> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [object, forwarded] => { + let forwarded = reflection_class_new_instance_args_value(forwarded)?; + Some((object.clone(), forwarded)) + } + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionMethod") + .and_then(|class_info| class_info.methods.get(&php_symbol_key("invokeArgs")))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + let object = planned_regular_arg_expr(plan.regular_args.first()?)?.clone(); + let forwarded_arg = planned_regular_arg_expr(plan.regular_args.get(1)?)?; + let forwarded = reflection_class_new_instance_args_value(forwarded_arg)?; + Some((object, forwarded)) +} + /// Classifies whether a known reflected method is static or instance-dispatched. fn reflection_method_target_kind( ctx: &LoweringContext<'_, '_>, @@ -10575,12 +10622,19 @@ enum ReflectionMethodTargetKind { /// Emits a runtime fatal for ReflectionMethod invocation forms not yet lowered. fn lower_reflection_method_invoke_unsupported( ctx: &mut LoweringContext<'_, '_>, + method_key: &str, expr: &Expr, ) -> LoweredValue { let result = lower_boxed_null(ctx, expr); - let message = ctx.intern_string( - "Fatal error: unsupported ReflectionMethod::invoke() target or argument forwarding\n", - ); + let method_name = if method_key == "invokeargs" { + "invokeArgs" + } else { + "invoke" + }; + let message = ctx.intern_string(&format!( + "Fatal error: unsupported ReflectionMethod::{}() target or argument forwarding\n", + method_name + )); ctx.builder.terminate(Terminator::Fatal { message }); result } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index a209faf18c..80d4b441db 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -511,6 +511,36 @@ fn builtin_reflection_method_invoke_method() -> ClassMethod { } } +/// Returns a public `ReflectionMethod::invokeArgs()` method shell. +/// +/// Direct AOT calls are lowered specially so the provided argument array becomes +/// the reflected method's source argument list. +fn builtin_reflection_method_invoke_args_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "invokeArgs".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![ + ("object".to_string(), Some(mixed_type()), None, false), + ("args".to_string(), Some(array_type()), None, false), + ], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass::newInstanceArgs()` method. /// /// Direct calls are lowered specially so the provided argument array becomes @@ -3126,6 +3156,7 @@ fn builtin_reflection_owner_class( } if name == "ReflectionMethod" { methods.push(builtin_reflection_method_invoke_method()); + methods.push(builtin_reflection_method_invoke_args_method()); } properties.push(builtin_property( "__attrs", @@ -3817,6 +3848,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Mixed; sig.variadic = Some("args".to_string()); } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invokeArgs")) { + sig.return_type = PhpType::Mixed; + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object( "ReflectionParameter".to_string(), diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index 2f111fa7b4..b2a4824557 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -6,7 +6,7 @@ //! - `cargo test` through Rust's test harness. //! //! Key details: -//! - `ReflectionMethod::invoke()` is lowered only for statically-known +//! - `ReflectionMethod::invoke()` and `invokeArgs()` are lowered only for statically-known //! reflectors whose target method has declared parameter types. //! - Tests cover inline constructors, ReflectionClass lookup, local tracking, //! case-insensitive method names, default values, and named arguments. @@ -39,6 +39,32 @@ echo $method->invoke($object, "L", "M"); assert_eq!(out, "AC:DB:EF:XY:LM"); } +/// Verifies `ReflectionMethod::invokeArgs()` forwards static argument arrays. +#[test] +fn test_reflection_method_invoke_args_calls_declared_aot_methods() { + let out = compile_and_run( + r#"invokeArgs($object, ["right" => "Y", "left" => "X"]); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeArgsTarget::class, "JOIN"))->invokeArgs($object, ["Q"]); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeArgsTarget::class, "make"))->invokeArgs(null, ["right" => "N", "left" => "M"]); +echo ":"; +echo (new ReflectionClass(ReflectInvokeArgsTarget::class))->getMethod("join")->invokeArgs(object: $object, args: ["left" => "L"]); +echo ":"; +$method = new ReflectionMethod(ReflectInvokeArgsTarget::class, "join"); +echo $method->invokeArgs(...[$object, ["A", "C"]]); +"#, + ); + assert_eq!(out, "XY:QB:MN:LB:AC"); +} + /// Verifies inferred AOT method signatures are rejected instead of miscompiled. #[test] fn test_reflection_method_invoke_rejects_inferred_aot_signature() { From 8247b0b9975cfe167115617ade807439991bd57a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 13:41:24 +0200 Subject: [PATCH 0550/1208] Support reflected constructor invocation --- docs/php/classes.md | 2 +- docs/php/eval.md | 3 +- src/ir_lower/expr/mod.rs | 47 +++++++++++++++++++------ tests/codegen/oop/reflection_methods.rs | 27 ++++++++++++++ 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 120fb9e84e..03189a6954 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1349,7 +1349,7 @@ Limitations today: - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. -- `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, method-name case-insensitivity, defaults, named arguments, and static `invokeArgs()` argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only argument arrays still require richer runtime/typechecker support. +- `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, and static `invokeArgs()` argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index fcea52b42c..b67e862ac6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -318,7 +318,8 @@ and throw catchable `ReflectionException` values when an instance receiver is not compatible with the reflected declaring class. For generated/AOT classes, `ReflectionMethod::invoke()` and `invokeArgs()` are also lowered for inline or straight-line tracked reflectors when the reflected method has declared parameter types; -the lowered call supports instance and static methods, method-name +the lowered call supports instance and static methods, constructors returned by +`ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, and named arguments. Generated/AOT invoke targets whose parameter types only come from call-site inference and runtime-only argument arrays still require richer runtime/typechecker support. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 32b515d0f5..e26da9a6bc 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9946,6 +9946,17 @@ fn lower_method_call( terminate_method_call_on_null(ctx, method); return null_value; } + if op == Op::MethodCall { + if let Some(value) = lower_reflection_method_invoke_call( + ctx, + Some(object_expr), + method, + args, + expr, + ) { + return value; + } + } if op == Op::MethodCall && value_is_nullable(ctx, object.value) { return lower_nullable_regular_method_call(ctx, object, method, args, expr); } @@ -9985,17 +9996,6 @@ fn lower_method_call( return value; } } - if op == Op::MethodCall { - if let Some(value) = lower_reflection_method_invoke_call( - ctx, - Some(object_expr), - method, - args, - expr, - ) { - return value; - } - } if op == Op::MethodCall { if let Some(value) = lower_reflection_property_value_call(ctx, Some(object_expr), method, args, expr) @@ -10983,6 +10983,7 @@ fn reflection_method_reflected_target( object_expr: &Expr, ) -> Option<(String, String)> { reflection_method_constructor_target(ctx, object_expr) + .or_else(|| reflection_method_class_get_constructor_target(ctx, object_expr)) .or_else(|| reflection_method_class_get_method_target(ctx, object_expr)) .or_else(|| reflection_method_class_get_methods_index_target(ctx, object_expr)) .or_else(|| { @@ -11431,6 +11432,30 @@ fn reflection_method_constructor_target( Some((class_name, method)) } +/// Extracts the constructor target from inline `ReflectionClass::getConstructor()` calls. +fn reflection_method_class_get_constructor_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option<(String, String)> { + let ExprKind::MethodCall { + object, + method, + args, + } = &object_expr.kind + else { + return None; + }; + if php_symbol_key(method) != "getconstructor" { + return None; + } + if !reflection_class_new_instance_args(args).is_empty() { + return None; + } + let class_name = reflection_class_reflected_class(ctx, object)?; + let method = resolve_known_class_method_name(ctx, &class_name, "__construct")?; + Some((class_name, method)) +} + /// Extracts the property target from inline `ReflectionClass::getProperty()` calls. fn reflection_property_class_get_property_target( ctx: &LoweringContext<'_, '_>, diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index b2a4824557..af08b46f30 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -65,6 +65,33 @@ echo $method->invokeArgs(...[$object, ["A", "C"]]); assert_eq!(out, "XY:QB:MN:LB:AC"); } +/// Verifies constructors returned by `ReflectionClass::getConstructor()` can be invoked. +#[test] +fn test_reflection_method_invoke_calls_aot_constructor_from_reflection_class() { + let out = compile_and_run( + r#"label = $left . $right; + } + public function label(): string { + return $this->label; + } +} + +$object = new ReflectInvokeCtorTarget("A", "A"); +$result = (new ReflectionClass(ReflectInvokeCtorTarget::class))->getConstructor()->invoke($object, "X", "Y"); +echo ($result === null ? "null" : "value") . ":" . $object->label(); +echo ":"; +$ctor = (new ReflectionClass(ReflectInvokeCtorTarget::class))->getConstructor(); +$ctor->invokeArgs($object, ["right" => "N", "left" => "M"]); +echo $object->label(); +"#, + ); + assert_eq!(out, "null:XY:MN"); +} + /// Verifies inferred AOT method signatures are rejected instead of miscompiled. #[test] fn test_reflection_method_invoke_rejects_inferred_aot_signature() { From f2019b53d2209d4bf7acd859b21838833bbb2031 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 13:48:39 +0200 Subject: [PATCH 0551/1208] Track static reflection argument arrays --- docs/php/classes.md | 4 +- docs/php/eval.md | 3 +- src/ir_lower/context.rs | 26 ++++++++ src/ir_lower/expr/mod.rs | 82 +++++++++++++++++++++++-- src/ir_lower/stmt/mod.rs | 9 ++- tests/codegen/oop/attributes.rs | 5 +- tests/codegen/oop/reflection_methods.rs | 5 +- 7 files changed, 121 insertions(+), 13 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 03189a6954..19877a9539 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1349,9 +1349,9 @@ Limitations today: - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. -- `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, and static `invokeArgs()` argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only argument arrays still require richer runtime/typechecker support. +- `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. -- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. +- `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, and `isCallable()` are supported for retained parameter metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index b67e862ac6..ee5d543e5f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -322,7 +322,8 @@ the lowered call supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, and named arguments. Generated/AOT invoke targets whose parameter types only come from call-site inference and -runtime-only argument arrays still require richer runtime/typechecker support. +runtime-only or non-literal argument arrays still require richer +runtime/typechecker support. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index bfc47f2be9..53f0c5a2a8 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -123,6 +123,7 @@ pub(crate) struct LoweringContext<'m, 'f> { reflection_class_locals: HashMap, reflection_property_locals: HashMap, reflection_method_locals: HashMap, + reflection_arg_array_locals: HashMap>, fiber_start_sigs: HashMap, ref_bound_locals: HashSet, ref_cell_owner_locals: HashMap, @@ -200,6 +201,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { reflection_class_locals: HashMap::new(), reflection_property_locals: HashMap::new(), reflection_method_locals: HashMap::new(), + reflection_arg_array_locals: HashMap::new(), fiber_start_sigs: HashMap::new(), ref_bound_locals: HashSet::new(), ref_cell_owner_locals: HashMap::new(), @@ -706,6 +708,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.clear_reflection_class_local(name); self.clear_reflection_property_local(name); self.clear_reflection_method_local(name); + self.clear_reflection_arg_array_local(name); self.clear_fiber_start_sig(name); if let Some(extern_type) = self.extern_global_type(name) { let release_source_after_store = self.value_is_owning_temporary(value); @@ -929,6 +932,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.clear_reflection_class_local(name); self.clear_reflection_property_local(name); self.clear_reflection_method_local(name); + self.clear_reflection_arg_array_local(name); self.clear_fiber_start_sig(name); let previous_kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, previous_kind); @@ -963,6 +967,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.clear_reflection_class_local(name); self.clear_reflection_property_local(name); self.clear_reflection_method_local(name); + self.clear_reflection_arg_array_local(name); self.clear_fiber_start_sig(name); let slot = self.declare_local(name, PhpType::Void); self.release_ref_cell_owner(name, span); @@ -1031,6 +1036,8 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.clear_static_callable_local(target); self.clear_reflection_class_local(target); self.clear_reflection_property_local(target); + self.clear_reflection_method_local(target); + self.clear_reflection_arg_array_local(target); self.clear_fiber_start_sig(target); self.release_replaced_local_before_ref_alias(target, span); let source_slot = self.declare_local(source, source_ty.clone()); @@ -1355,6 +1362,19 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_method_locals.get(name).cloned() } + /// Records that a PHP local currently holds a safe static argument array for reflection. + pub(crate) fn bind_reflection_arg_array_local(&mut self, name: &str, args: Vec) { + if self.can_track_static_callable_local(name) { + self.reflection_arg_array_locals + .insert(name.to_string(), args); + } + } + + /// Returns the static reflection argument array associated with a local. + pub(crate) fn reflection_arg_array_local(&self, name: &str) -> Option> { + self.reflection_arg_array_locals.get(name).cloned() + } + /// Records that a PHP local currently holds a Fiber with a known callback signature. pub(crate) fn bind_fiber_start_sig(&mut self, name: &str, sig: FunctionSig) { if self.can_track_static_callable_local(name) { @@ -1398,6 +1418,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_method_locals.remove(name); } + /// Clears the compile-time reflection argument-array association for one local. + pub(crate) fn clear_reflection_arg_array_local(&mut self, name: &str) { + self.reflection_arg_array_locals.remove(name); + } + /// Clears the known Fiber callback association for one local. pub(crate) fn clear_fiber_start_sig(&mut self, name: &str) { self.fiber_start_sigs.remove(name); @@ -1409,6 +1434,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_class_locals.clear(); self.reflection_property_locals.clear(); self.reflection_method_locals.clear(); + self.reflection_arg_array_locals.clear(); self.fiber_start_sigs.clear(); } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index e26da9a6bc..17187e9ca3 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1516,6 +1516,8 @@ fn lower_assignment_expr( assigned_name.and_then(|_| reflection_property_binding_for_expr(ctx, value)); let reflected_method = assigned_name.and_then(|_| reflection_method_binding_for_expr(ctx, value)); + let reflected_args = + assigned_name.and_then(|_| reflection_arg_array_binding_for_expr(value)); let fiber_start_sig = assigned_name.and_then(|_| crate::ir_lower::fibers::start_sig_for_expr(ctx, value)); let callable_array = assigned_name @@ -1559,6 +1561,9 @@ fn lower_assignment_expr( if let Some((reflected_class, reflected_method)) = reflected_method { ctx.bind_reflection_method_local(name, reflected_class, reflected_method); } + if let Some(reflected_args) = reflected_args { + ctx.bind_reflection_arg_array_local(name, reflected_args); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } @@ -3622,6 +3627,52 @@ pub(crate) fn reflection_method_binding_for_expr( reflection_method_reflected_target(ctx, expr) } +/// Returns a safe static argument array that can be replayed for reflection forwarding. +pub(crate) fn reflection_arg_array_binding_for_expr(expr: &Expr) -> Option> { + let args = reflection_class_new_instance_args_value_without_locals(expr)?; + if args.iter().all(reflection_arg_expr_can_track) { + Some(args) + } else { + None + } +} + +/// Returns true when replaying an argument expression cannot duplicate side effects. +fn reflection_arg_expr_can_track(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + | ExprKind::ConstRef(_) + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } + | ExprKind::MagicConstant(_) => true, + ExprKind::Negate(inner) => matches!( + &inner.kind, + ExprKind::IntLiteral(_) | ExprKind::FloatLiteral(_) + ), + ExprKind::NamedArg { value, .. } => reflection_arg_expr_can_track(value), + ExprKind::ArrayLiteral(items) => items.iter().all(reflection_arg_expr_can_track), + ExprKind::ArrayLiteralAssoc(entries) => entries.iter().all(|(key, value)| { + reflection_arg_array_key_can_track(key) && reflection_arg_expr_can_track(value) + }), + _ => false, + } +} + +/// Returns true when an associative array key is stable enough for replay. +fn reflection_arg_array_key_can_track(expr: &Expr) -> bool { + matches!( + expr.kind, + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::FloatLiteral(_) + ) +} + /// EIR value and callable binding produced by a callable-array assignment. pub(crate) struct LoweredCallableArrayAssignment { pub(crate) value: LoweredValue, @@ -10236,7 +10287,7 @@ fn lower_reflection_class_new_instance_args( args: &[Expr], expr: &Expr, ) -> LoweredValue { - let Some(forwarded_args) = reflection_class_new_instance_args_array(args) else { + let Some(forwarded_args) = reflection_class_new_instance_args_array(ctx, args) else { return lower_reflection_class_new_instance_args_unsupported(ctx, expr); }; lower_reflection_class_new_instance(ctx, object_expr, object, &forwarded_args, expr) @@ -10534,7 +10585,7 @@ fn reflection_method_invoke_args_array( if !crate::types::call_args::has_named_args(&args) { return match args.as_slice() { [object, forwarded] => { - let forwarded = reflection_class_new_instance_args_value(forwarded)?; + let forwarded = reflection_class_new_instance_args_value(ctx, forwarded)?; Some((object.clone(), forwarded)) } _ => None, @@ -10563,7 +10614,7 @@ fn reflection_method_invoke_args_array( } let object = planned_regular_arg_expr(plan.regular_args.first()?)?.clone(); let forwarded_arg = planned_regular_arg_expr(plan.regular_args.get(1)?)?; - let forwarded = reflection_class_new_instance_args_value(forwarded_arg)?; + let forwarded = reflection_class_new_instance_args_value(ctx, forwarded_arg)?; Some((object, forwarded)) } @@ -11941,17 +11992,36 @@ fn reflection_class_new_instance_args(args: &[Expr]) -> Vec { } /// Returns constructor arguments carried by a static `newInstanceArgs()` array argument. -fn reflection_class_new_instance_args_array(args: &[Expr]) -> Option> { +fn reflection_class_new_instance_args_array( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { let args = reflection_class_new_instance_args(args); match args.as_slice() { [] => Some(Vec::new()), - [arg] => reflection_class_new_instance_args_value(arg), + [arg] => reflection_class_new_instance_args_value(ctx, arg), _ => None, } } /// Extracts the actual array value passed to the `newInstanceArgs()` `$args` parameter. -fn reflection_class_new_instance_args_value(arg: &Expr) -> Option> { +fn reflection_class_new_instance_args_value( + ctx: &LoweringContext<'_, '_>, + arg: &Expr, +) -> Option> { + let array_expr = match &arg.kind { + ExprKind::NamedArg { name, value } if php_symbol_key(name) == "args" => value.as_ref(), + ExprKind::NamedArg { .. } => return None, + _ => arg, + }; + if let ExprKind::Variable(name) = &array_expr.kind { + return ctx.reflection_arg_array_local(name); + } + reflection_class_new_instance_args_value_without_locals(array_expr) +} + +/// Extracts an inline static array value passed to a reflection argument-array API. +fn reflection_class_new_instance_args_value_without_locals(arg: &Expr) -> Option> { let array_expr = match &arg.kind { ExprKind::NamedArg { name, value } if php_symbol_key(name) == "args" => value.as_ref(), ExprKind::NamedArg { .. } => return None, diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index 0cff976b42..adf5353d2e 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -20,8 +20,9 @@ use crate::ir_lower::effects_lookup; use crate::ir_lower::expr::{ array_access_element_result_type, coerce_to_int_at_span, lower_callable_array_for_assignment, lower_array_literal_with_expected_type, lower_closure_for_assignment, lower_expr, - reflection_class_binding_for_expr, reflection_method_binding_for_expr, - reflection_property_binding_for_expr, static_callable_binding_for_expr, + reflection_arg_array_binding_for_expr, reflection_class_binding_for_expr, + reflection_method_binding_for_expr, reflection_property_binding_for_expr, + static_callable_binding_for_expr, string_op_uses_scratch_storage, type_satisfies_array_access_for_ir, }; @@ -242,6 +243,7 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa let reflected_class = reflection_class_binding_for_expr(ctx, value); let reflected_property = reflection_property_binding_for_expr(ctx, value); let reflected_method = reflection_method_binding_for_expr(ctx, value); + let reflected_args = reflection_arg_array_binding_for_expr(value); let fiber_start_sig = crate::ir_lower::fibers::start_sig_for_expr(ctx, value); let callable_array = lower_callable_array_for_assignment(ctx, value, static_callable.as_ref()); let lowered = callable_array @@ -280,6 +282,9 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa if let Some((reflected_class, reflected_method)) = reflected_method { ctx.bind_reflection_method_local(name, reflected_class, reflected_method); } + if let Some(reflected_args) = reflected_args { + ctx.bind_reflection_arg_array_local(name, reflected_args); + } if let Some(sig) = fiber_start_sig { ctx.bind_fiber_start_sig(name, sig); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index af250493ad..e3fbd472aa 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -3346,11 +3346,14 @@ $third = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs(arg echo $third->label() . ":"; $fourth = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs(...[["left" => "M", "right" => "N"]]); echo $fourth->label() . ":"; +$localArgs = ["right" => "P", "left" => "O"]; +$fifth = (new ReflectionClass(ReflectNewArgsTarget::class))->newInstanceArgs($localArgs); +echo $fifth->label() . ":"; $empty = (new ReflectionClass(ReflectEmptyNewArgsTarget::class))->newInstanceArgs(); echo $empty->label(); "#, ); - assert_eq!(out, "XY:QR:LB:MN:empty"); + assert_eq!(out, "XY:QR:LB:MN:OP:empty"); } /// Verifies that `ReflectionClass::newInstance()` accepts zero constructor diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index af08b46f30..aea5478bb0 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -60,9 +60,12 @@ echo (new ReflectionClass(ReflectInvokeArgsTarget::class))->getMethod("join")->i echo ":"; $method = new ReflectionMethod(ReflectInvokeArgsTarget::class, "join"); echo $method->invokeArgs(...[$object, ["A", "C"]]); +echo ":"; +$localArgs = ["right" => "P", "left" => "O"]; +echo (new ReflectionMethod(ReflectInvokeArgsTarget::class, "join"))->invokeArgs($object, $localArgs); "#, ); - assert_eq!(out, "XY:QB:MN:LB:AC"); + assert_eq!(out, "XY:QB:MN:LB:AC:OP"); } /// Verifies constructors returned by `ReflectionClass::getConstructor()` can be invoked. From 35bd5fdaf7204adf7d05888c892999abb6e9f93a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 14:10:36 +0200 Subject: [PATCH 0552/1208] Support AOT ReflectionFunction invocation --- docs/php/classes.md | 4 +- docs/php/eval.md | 4 +- src/ir_lower/context.rs | 29 +++ src/ir_lower/expr/mod.rs | 212 ++++++++++++++++++ src/ir_lower/stmt/mod.rs | 7 +- src/types/checker/builtin_types/reflection.rs | 76 +++++++ tests/codegen/oop.rs | 2 + tests/codegen/oop/reflection_functions.rs | 82 +++++++ tests/codegen/oop/reflection_methods.rs | 8 +- 9 files changed, 420 insertions(+), 4 deletions(-) create mode 100644 tests/codegen/oop/reflection_functions.rs diff --git a/docs/php/classes.md b/docs/php/classes.md index 19877a9539..6cbfb68472 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1195,6 +1195,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::getParameters()` | `new ReflectionFunction($function_name)` | Return `ReflectionParameter` objects for the reflected function parameters | | `ReflectionFunction::getNumberOfParameters()` | `new ReflectionFunction($function_name)` | Return the total number of reflected function parameters | | `ReflectionFunction::getNumberOfRequiredParameters()` | `new ReflectionFunction($function_name)` | Return the number of required reflected function parameters | +| `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared parameter types are also lowered | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getDeclaringClass()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected method | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | @@ -1347,8 +1348,9 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` for statically known user functions. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `invoke()`, and `invokeArgs()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index ee5d543e5f..f55b4f53dc 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -361,7 +361,9 @@ accepts eval free-function names, class/interface/trait method arrays, and object-method arrays resolved from the evaluated runtime object, including inline `new` expressions. `ReflectionFunction::invoke()` and `invokeArgs()` dispatch eval-declared functions with the same named/default/variadic argument -binding used by direct eval function calls. Defaulted eval method parameters are +binding used by direct eval function calls; generated/AOT function invocation is +covered by the general Reflection support documented in `docs/php/classes.md`. +Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, `isDefaultValueConstant()`, `getDefaultValueConstantName()`, and `getDefaultValue()`. Constant-name metadata diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 53f0c5a2a8..181234b51a 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -121,6 +121,7 @@ pub(crate) struct LoweringContext<'m, 'f> { pub finally_stack: Vec, static_callable_locals: HashMap, reflection_class_locals: HashMap, + reflection_function_locals: HashMap, reflection_property_locals: HashMap, reflection_method_locals: HashMap, reflection_arg_array_locals: HashMap>, @@ -199,6 +200,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { finally_stack: Vec::new(), static_callable_locals: HashMap::new(), reflection_class_locals: HashMap::new(), + reflection_function_locals: HashMap::new(), reflection_property_locals: HashMap::new(), reflection_method_locals: HashMap::new(), reflection_arg_array_locals: HashMap::new(), @@ -706,6 +708,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { pub(crate) fn store_local(&mut self, name: &str, value: LoweredValue, php_type: PhpType, span: Option) -> LoweredValue { self.clear_static_callable_local(name); self.clear_reflection_class_local(name); + self.clear_reflection_function_local(name); self.clear_reflection_property_local(name); self.clear_reflection_method_local(name); self.clear_reflection_arg_array_local(name); @@ -930,6 +933,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { ) -> LoweredValue { self.clear_static_callable_local(name); self.clear_reflection_class_local(name); + self.clear_reflection_function_local(name); self.clear_reflection_property_local(name); self.clear_reflection_method_local(name); self.clear_reflection_arg_array_local(name); @@ -965,6 +969,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } self.clear_static_callable_local(name); self.clear_reflection_class_local(name); + self.clear_reflection_function_local(name); self.clear_reflection_property_local(name); self.clear_reflection_method_local(name); self.clear_reflection_arg_array_local(name); @@ -1035,6 +1040,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } self.clear_static_callable_local(target); self.clear_reflection_class_local(target); + self.clear_reflection_function_local(target); self.clear_reflection_property_local(target); self.clear_reflection_method_local(target); self.clear_reflection_arg_array_local(target); @@ -1326,6 +1332,23 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_class_locals.get(name).cloned() } + /// Records that a PHP local currently holds a statically-known `ReflectionFunction`. + pub(crate) fn bind_reflection_function_local( + &mut self, + name: &str, + reflected_function: String, + ) { + if self.can_track_static_callable_local(name) { + self.reflection_function_locals + .insert(name.to_string(), reflected_function); + } + } + + /// Returns the reflected function associated with a local `ReflectionFunction`. + pub(crate) fn reflection_function_local(&self, name: &str) -> Option { + self.reflection_function_locals.get(name).cloned() + } + /// Records that a PHP local currently holds a statically-known `ReflectionProperty` object. pub(crate) fn bind_reflection_property_local( &mut self, @@ -1408,6 +1431,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.reflection_class_locals.remove(name); } + /// Clears the compile-time `ReflectionFunction` association for one local. + pub(crate) fn clear_reflection_function_local(&mut self, name: &str) { + self.reflection_function_locals.remove(name); + } + /// Clears the compile-time `ReflectionProperty` association for one local. pub(crate) fn clear_reflection_property_local(&mut self, name: &str) { self.reflection_property_locals.remove(name); @@ -1432,6 +1460,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { pub(crate) fn clear_static_callable_locals(&mut self) { self.static_callable_locals.clear(); self.reflection_class_locals.clear(); + self.reflection_function_locals.clear(); self.reflection_property_locals.clear(); self.reflection_method_locals.clear(); self.reflection_arg_array_locals.clear(); diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 17187e9ca3..371b0309d8 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1512,6 +1512,8 @@ fn lower_assignment_expr( } let static_callable = assigned_name.and_then(|_| static_callable_binding_for_expr(ctx, value)); let reflected_class = assigned_name.and_then(|_| reflection_class_binding_for_expr(ctx, value)); + let reflected_function = + assigned_name.and_then(|_| reflection_function_binding_for_expr(ctx, value)); let reflected_property = assigned_name.and_then(|_| reflection_property_binding_for_expr(ctx, value)); let reflected_method = @@ -1555,6 +1557,9 @@ fn lower_assignment_expr( if let Some(reflected_class) = reflected_class { ctx.bind_reflection_class_local(name, reflected_class); } + if let Some(reflected_function) = reflected_function { + ctx.bind_reflection_function_local(name, reflected_function); + } if let Some((reflected_class, reflected_property)) = reflected_property { ctx.bind_reflection_property_local(name, reflected_class, reflected_property); } @@ -3611,6 +3616,14 @@ pub(crate) fn reflection_class_binding_for_expr( reflection_class_new_instance_reflected_class(ctx, expr) } +/// Returns the reflected function captured by a statically-known `ReflectionFunction` expression. +pub(crate) fn reflection_function_binding_for_expr( + ctx: &LoweringContext<'_, '_>, + expr: &Expr, +) -> Option { + reflection_function_reflected_target(ctx, expr) +} + /// Returns the reflected property captured by a statically-known `ReflectionProperty` expression. pub(crate) fn reflection_property_binding_for_expr( ctx: &LoweringContext<'_, '_>, @@ -9998,6 +10011,15 @@ fn lower_method_call( return null_value; } if op == Op::MethodCall { + if let Some(value) = lower_reflection_function_invoke_call( + ctx, + Some(object_expr), + method, + args, + expr, + ) { + return value; + } if let Some(value) = lower_reflection_method_invoke_call( ctx, Some(object_expr), @@ -10441,6 +10463,117 @@ fn reflection_member_constructor_expr( ) } +/// Lowers reflected function invocation for statically-known `ReflectionFunction` objects. +fn lower_reflection_function_invoke_call( + ctx: &mut LoweringContext<'_, '_>, + object_expr: Option<&Expr>, + method: &str, + args: &[Expr], + expr: &Expr, +) -> Option { + let method_key = php_symbol_key(method); + let object_expr = object_expr?; + let function_name = reflection_function_reflected_target(ctx, object_expr)?; + let Some(forwarded_args) = (match method_key.as_str() { + "invoke" => Some(reflection_function_invoke_args(args)), + "invokeargs" => reflection_function_invoke_args_array(ctx, args), + _ => return None, + }) else { + return Some(lower_reflection_function_invoke_unsupported( + ctx, + &method_key, + expr, + )); + }; + if !reflection_function_target_has_declared_params(ctx, &function_name) { + return Some(lower_reflection_function_invoke_unsupported( + ctx, + &method_key, + expr, + )); + } + let name = Name::from(function_name); + Some(lower_function_call(ctx, &name, &forwarded_args, expr)) +} + +/// Returns direct `ReflectionFunction::invoke(...$args)` arguments after static spread expansion. +fn reflection_function_invoke_args(args: &[Expr]) -> Vec { + reflection_class_new_instance_args(args) +} + +/// Extracts the argument list passed to `ReflectionFunction::invokeArgs($args)`. +fn reflection_function_invoke_args_array( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [forwarded] => reflection_class_new_instance_args_value(ctx, forwarded), + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionFunction") + .and_then(|class_info| class_info.methods.get(&php_symbol_key("invokeArgs")))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + let forwarded_arg = planned_regular_arg_expr(plan.regular_args.first()?)?; + reflection_class_new_instance_args_value(ctx, forwarded_arg) +} + +/// Returns true when reflected invocation can trust the target function's parameter types. +fn reflection_function_target_has_declared_params( + ctx: &LoweringContext<'_, '_>, + function_name: &str, +) -> bool { + ctx.functions.get(function_name).is_some_and(|signature| { + signature + .declared_params + .iter() + .all(|is_declared| *is_declared) + }) +} + +/// Emits a runtime fatal for ReflectionFunction invocation forms not yet lowered. +fn lower_reflection_function_invoke_unsupported( + ctx: &mut LoweringContext<'_, '_>, + method_key: &str, + expr: &Expr, +) -> LoweredValue { + let result = lower_boxed_null(ctx, expr); + let method_name = if method_key == "invokeargs" { + "invokeArgs" + } else { + "invoke" + }; + let message = ctx.intern_string(&format!( + "Fatal error: unsupported ReflectionFunction::{}() target or argument forwarding\n", + method_name + )); + ctx.builder.terminate(Terminator::Fatal { message }); + result +} + /// Lowers reflected method invocation for statically-known `ReflectionMethod` objects. fn lower_reflection_method_invoke_call( ctx: &mut LoweringContext<'_, '_>, @@ -11045,6 +11178,19 @@ fn reflection_method_reflected_target( }) } +/// Extracts the known function name from a supported ReflectionFunction source. +fn reflection_function_reflected_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option { + reflection_function_constructor_target(ctx, object_expr).or_else(|| { + let ExprKind::Variable(name) = &object_expr.kind else { + return None; + }; + ctx.reflection_function_local(name) + }) +} + /// Extracts a known ReflectionMethod from `ReflectionClass::getMethods()[N]`. fn reflection_method_class_get_methods_index_target( ctx: &LoweringContext<'_, '_>, @@ -11434,6 +11580,24 @@ fn reflection_property_filter_modifier_bits( modifiers } +/// Extracts the known function name from an inline ReflectionFunction constructor. +fn reflection_function_constructor_target( + ctx: &LoweringContext<'_, '_>, + object_expr: &Expr, +) -> Option { + let ExprKind::NewObject { class_name, args } = &object_expr.kind else { + return None; + }; + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionfunction" { + return None; + } + let function_arg = reflection_function_constructor_regular_arg(ctx, args)?; + let ExprKind::StringLiteral(function_name) = function_arg.kind else { + return None; + }; + resolve_known_function_name(ctx, &function_name) +} + /// Extracts the known class and property name from an inline ReflectionProperty constructor. fn reflection_property_constructor_target( ctx: &LoweringContext<'_, '_>, @@ -11560,6 +11724,45 @@ fn reflection_class_member_name_arg(args: &[Expr]) -> Option { reflection_class_static_property_name_arg(name.as_ref()?) } +/// Returns normalized constructor args for `ReflectionFunction($function)`. +fn reflection_function_constructor_regular_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return match args.as_slice() { + [function_arg] => Some(function_arg.clone()), + _ => None, + }; + } + let sig = ctx + .classes + .get("ReflectionFunction") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + planned_regular_arg_expr(plan.regular_args.first()?).cloned() +} + /// Returns normalized constructor args for `ReflectionProperty($class, $property)`. fn reflection_property_constructor_regular_args( ctx: &LoweringContext<'_, '_>, @@ -12157,6 +12360,15 @@ fn resolve_known_class_name(ctx: &LoweringContext<'_, '_>, class_name: &str) -> .cloned() } +/// Resolves a PHP function name case-insensitively against known user functions. +fn resolve_known_function_name(ctx: &LoweringContext<'_, '_>, function_name: &str) -> Option { + let key = php_symbol_key(function_name.trim_start_matches('\\')); + ctx.functions + .keys() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == key) + .cloned() +} + /// Resolves a PHP method name case-insensitively against known class metadata. fn resolve_known_class_method_name( ctx: &LoweringContext<'_, '_>, diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index adf5353d2e..3a61365e7e 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -21,7 +21,8 @@ use crate::ir_lower::expr::{ array_access_element_result_type, coerce_to_int_at_span, lower_callable_array_for_assignment, lower_array_literal_with_expected_type, lower_closure_for_assignment, lower_expr, reflection_arg_array_binding_for_expr, reflection_class_binding_for_expr, - reflection_method_binding_for_expr, reflection_property_binding_for_expr, + reflection_function_binding_for_expr, reflection_method_binding_for_expr, + reflection_property_binding_for_expr, static_callable_binding_for_expr, string_op_uses_scratch_storage, type_satisfies_array_access_for_ir, @@ -241,6 +242,7 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa ctx.clear_pending_static_callable_result(); let static_callable = static_callable_binding_for_expr(ctx, value); let reflected_class = reflection_class_binding_for_expr(ctx, value); + let reflected_function = reflection_function_binding_for_expr(ctx, value); let reflected_property = reflection_property_binding_for_expr(ctx, value); let reflected_method = reflection_method_binding_for_expr(ctx, value); let reflected_args = reflection_arg_array_binding_for_expr(value); @@ -276,6 +278,9 @@ fn lower_assign(ctx: &mut LoweringContext<'_, '_>, name: &str, value: &Expr, spa if let Some(reflected_class) = reflected_class { ctx.bind_reflection_class_local(name, reflected_class); } + if let Some(reflected_function) = reflected_function { + ctx.bind_reflection_function_local(name, reflected_function); + } if let Some((reflected_class, reflected_property)) = reflected_property { ctx.bind_reflection_property_local(name, reflected_class, reflected_property); } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 80d4b441db..02b051a210 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -541,6 +541,60 @@ fn builtin_reflection_method_invoke_args_method() -> ClassMethod { } } +/// Returns a public variadic `ReflectionFunction::invoke()` method shell. +/// +/// Direct generated/AOT calls are lowered specially so the variadic source +/// arguments are normalized against the reflected function's own signature. +fn builtin_reflection_function_invoke_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "invoke".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: Some("args".to_string()), + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns a public `ReflectionFunction::invokeArgs()` method shell. +/// +/// Direct generated/AOT calls are lowered specially so the provided argument +/// array becomes the reflected function's source argument list. +fn builtin_reflection_function_invoke_args_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "invokeArgs".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("args".to_string(), Some(array_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(mixed_type()), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `ReflectionClass::newInstanceArgs()` method. /// /// Direct calls are lowered specially so the provided argument array becomes @@ -3158,6 +3212,10 @@ fn builtin_reflection_owner_class( methods.push(builtin_reflection_method_invoke_method()); methods.push(builtin_reflection_method_invoke_args_method()); } + if name == "ReflectionFunction" { + methods.push(builtin_reflection_function_invoke_method()); + methods.push(builtin_reflection_function_invoke_args_method()); + } properties.push(builtin_property( "__attrs", Visibility::Private, @@ -3572,6 +3630,15 @@ fn builtin_reflection_owner_get_attributes_method() -> ClassMethod { } } +/// Marks a synthesized variadic method signature as callable with no variadic arguments. +fn make_reflection_variadic_optional(sig: &mut crate::types::FunctionSig) { + if sig.variadic.is_some() { + if let Some(default) = sig.defaults.last_mut() { + *default = empty_array(); + } + } +} + /// Overrides the return types on the synthesized reflection class methods inside /// `checker` to match PHP's actual signatures: /// - `__construct` → `void` @@ -3847,6 +3914,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invoke")) { sig.return_type = PhpType::Mixed; sig.variadic = Some("args".to_string()); + make_reflection_variadic_optional(sig); } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invokeArgs")) { sig.return_type = PhpType::Mixed; @@ -3863,6 +3931,14 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionFunction" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invoke")) { + sig.return_type = PhpType::Mixed; + sig.variadic = Some("args".to_string()); + make_reflection_variadic_optional(sig); + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invokeArgs")) { + sig.return_type = PhpType::Mixed; + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object( "ReflectionParameter".to_string(), diff --git a/tests/codegen/oop.rs b/tests/codegen/oop.rs index 4a728f2a01..5e799d4b9e 100644 --- a/tests/codegen/oop.rs +++ b/tests/codegen/oop.rs @@ -45,5 +45,7 @@ mod property_hooks; mod datetime; #[path = "oop/reflection_properties.rs"] mod reflection_properties; +#[path = "oop/reflection_functions.rs"] +mod reflection_functions; #[path = "oop/reflection_methods.rs"] mod reflection_methods; diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs new file mode 100644 index 0000000000..881437f821 --- /dev/null +++ b/tests/codegen/oop/reflection_functions.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! End-to-end codegen tests for ReflectionFunction invocation paths over AOT +//! function metadata. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - `ReflectionFunction::invoke()` and `invokeArgs()` are lowered only for +//! statically-known reflectors whose target function has declared parameter +//! types. +//! - Tests cover inline constructors, local tracking, case-insensitive function +//! names, defaults, named arguments, and static argument arrays. + +use super::*; + +/// Verifies `ReflectionFunction::invoke()` calls declared AOT functions. +#[test] +fn test_reflection_function_invoke_calls_declared_aot_functions() { + let out = compile_and_run( + r#"invoke("A", "C"); +echo ":"; +echo (new ReflectionFunction(function: "\\reflect_function_invoke_target"))->invoke(right: "Y", left: "X"); +echo ":"; +$ref = new ReflectionFunction("reflect_function_invoke_target"); +echo $ref->invoke("L"); +echo ":"; +echo (new ReflectionFunction("reflect_function_invoke_zero"))->invoke(); +"#, + ); + assert_eq!(out, "AC:XY:LB:Z"); +} + +/// Verifies `ReflectionFunction::invokeArgs()` forwards static argument arrays. +#[test] +fn test_reflection_function_invoke_args_calls_declared_aot_functions() { + let out = compile_and_run( + r#"invokeArgs(["right" => "Y", "left" => "X"]); +echo ":"; +$localArgs = ["right" => "P", "left" => "O"]; +$ref = new ReflectionFunction("reflect_function_invoke_args_target"); +echo $ref->invokeArgs($localArgs); +echo ":"; +echo $ref->invokeArgs(...[["A", "C"]]); +echo ":"; +echo $ref->invokeArgs(args: ["Q"]); +"#, + ); + assert_eq!(out, "XY:OP:AC:QB"); +} + +/// Verifies inferred AOT function signatures are rejected instead of miscompiled. +#[test] +fn test_reflection_function_invoke_rejects_inferred_aot_signature() { + let err = compile_and_run_expect_failure( + r#"invoke("A", "B"); +"#, + ); + assert!( + err.contains("Fatal error: unsupported ReflectionFunction::invoke()"), + "unexpected stderr: {err}" + ); +} diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index aea5478bb0..284c3d0dd7 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -20,7 +20,9 @@ fn test_reflection_method_invoke_calls_declared_aot_methods() { r#"invoke(null, ri echo ":"; $method = new ReflectionMethod(ReflectInvokeTarget::class, "join"); echo $method->invoke($object, "L", "M"); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeTarget::class, "zero"))->invoke($object); +echo ":"; +echo (new ReflectionMethod(ReflectInvokeTarget::class, "staticZero"))->invoke(null); "#, ); - assert_eq!(out, "AC:DB:EF:XY:LM"); + assert_eq!(out, "AC:DB:EF:XY:LM:Z:T"); } /// Verifies `ReflectionMethod::invokeArgs()` forwards static argument arrays. From e8cfb2b548632f193c5583356e6feb4f16969a3b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 14:16:51 +0200 Subject: [PATCH 0553/1208] Support AOT reflection setAccessible no-ops --- docs/php/classes.md | 4 +-- src/types/checker/builtin_types/reflection.rs | 28 +++++++++++++++++++ tests/codegen/oop/reflection_methods.rs | 24 ++++++++++++++++ tests/codegen/oop/reflection_properties.rs | 25 +++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 6cbfb68472..ea114e4df3 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1208,7 +1208,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isConstructor()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getConstructor()` | Return whether the reflected method is the constructor | | `ReflectionMethod::isDestructor()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is the destructor | | `ReflectionMethod::getModifiers()` | `new ReflectionMethod($class_name, $method_name)` | Return PHP's `ReflectionMethod::IS_*` visibility/static/finality/abstract bitmask | -| `ReflectionMethod::setAccessible()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Accepted as a PHP-compatible no-op for eval-backed method reflection | +| `ReflectionMethod::setAccessible()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Accepted as a PHP-compatible no-op for eval-backed and generated/AOT method reflection | | `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments; inline/tracked generated/AOT methods with declared parameter types are also lowered | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | @@ -1260,7 +1260,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isInitialized()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` for supported known properties | Return whether a supported instance/static property slot is initialized without reading the property value | | `ReflectionProperty::getValue()` | `ReflectionProperty` for instance properties with an explicit object argument, or inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for known static properties | Read supported instance/static property storage; positional and named arguments are normalized | | `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported instance/static property storage | -| `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed property reflection | +| `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed and generated/AOT property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | | `ReflectionClassConstant::getDeclaringClass()` | Same as `ReflectionClassConstant::getName()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected constant or enum case | diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 02b051a210..065404dcf2 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -541,6 +541,27 @@ fn builtin_reflection_method_invoke_args_method() -> ClassMethod { } } +/// Returns a public `setAccessible(bool $accessible)` no-op method shell. +fn builtin_reflection_set_accessible_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "setAccessible".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("accessible".to_string(), Some(bool_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Void), + body: Vec::new(), + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public variadic `ReflectionFunction::invoke()` method shell. /// /// Direct generated/AOT calls are lowered specially so the variadic source @@ -3211,11 +3232,15 @@ fn builtin_reflection_owner_class( if name == "ReflectionMethod" { methods.push(builtin_reflection_method_invoke_method()); methods.push(builtin_reflection_method_invoke_args_method()); + methods.push(builtin_reflection_set_accessible_method()); } if name == "ReflectionFunction" { methods.push(builtin_reflection_function_invoke_method()); methods.push(builtin_reflection_function_invoke_args_method()); } + if name == "ReflectionProperty" { + methods.push(builtin_reflection_set_accessible_method()); + } properties.push(builtin_property( "__attrs", Visibility::Private, @@ -3863,6 +3888,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Bool; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("setAccessible")) { + sig.return_type = PhpType::Void; + } } if class_name == "ReflectionProperty" { for method_name in [ diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index 284c3d0dd7..3640fd401c 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -101,6 +101,30 @@ echo $object->label(); assert_eq!(out, "null:XY:MN"); } +/// Verifies `ReflectionMethod::setAccessible()` is a no-op for AOT reflectors. +#[test] +fn test_reflection_method_set_accessible_is_noop_for_aot_methods() { + let out = compile_and_run( + r#"setAccessible(false)) ? "M" : "m"; +echo ":" . $method->invoke($object); +echo ":"; +$listed = (new ReflectionClass(ReflectMethodAccessTarget::class))->getMethod("hidden"); +echo is_null($listed->setAccessible(accessible: true)) ? "L" : "l"; +echo ":" . $listed->invoke($object); +"#, + ); + assert_eq!(out, "M:secret:L:secret"); +} + /// Verifies inferred AOT method signatures are rejected instead of miscompiled. #[test] fn test_reflection_method_invoke_rejects_inferred_aot_signature() { diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 9979767d64..8078ce8afb 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -383,3 +383,28 @@ echo ":" . $label->getValue($target); ); assert_eq!(out, "4:8:old:new"); } + +/// Verifies `ReflectionProperty::setAccessible()` is a no-op for AOT reflectors. +#[test] +fn test_reflection_property_set_accessible_is_noop_for_aot_properties() { + let out = compile_and_run( + r#"setAccessible(false)) ? "P" : "p"; +echo ":" . $count->getValue($target); +$count->setValue($target, 9); +echo ":" . $count->getValue($target); +echo ":"; +$label = (new ReflectionClass(ReflectPropertyAccessTarget::class))->getProperty("label"); +echo is_null($label->setAccessible(accessible: true)) ? "L" : "l"; +echo ":" . $label->getValue($target); +"#, + ); + assert_eq!(out, "P:4:9:L:old"); +} From c82daa08b8b54dc1b85e99c21da44c67974f9685 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 14:28:56 +0200 Subject: [PATCH 0554/1208] Fix eval reflection owner name labels --- src/codegen/eval_reflection_owner_helpers.rs | 56 ++++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 896d1e3a49..b5f58ef3dd 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -892,33 +892,45 @@ fn emit_set_owner_name_property_aarch64(emitter: &mut Emitter, layout: &Reflecti emit_set_owner_low_bit_final_property_aarch64(emitter, layout); return; }; - let scan_loop_label = "__elephc_eval_reflection_owner_name_scan_loop"; - let found_label = "__elephc_eval_reflection_owner_name_scan_found"; - let no_namespace_label = "__elephc_eval_reflection_owner_name_scan_none"; - let store_parts_label = "__elephc_eval_reflection_owner_name_store_parts"; + let scan_loop_label = format!( + "__elephc_eval_reflection_owner_name_scan_loop_{}", + layout.class_id + ); + let found_label = format!( + "__elephc_eval_reflection_owner_name_scan_found_{}", + layout.class_id + ); + let no_namespace_label = format!( + "__elephc_eval_reflection_owner_name_scan_none_{}", + layout.class_id + ); + let store_parts_label = format!( + "__elephc_eval_reflection_owner_name_store_parts_{}", + layout.class_id + ); emitter.instruction("ldr x3, [sp, #8]"); // reload the original reflected-name pointer for splitting emitter.instruction("ldr x4, [sp, #16]"); // reload the original reflected-name length for splitting emitter.instruction("mov x5, x4"); // start scanning from one byte past the final name byte emitter.instruction(&format!("cbz x5, {}", no_namespace_label)); // empty names have no namespace component - emitter.label(scan_loop_label); + emitter.label(&scan_loop_label); emitter.instruction("sub x5, x5, #1"); // move the scan cursor to the previous byte emitter.instruction("ldrb w6, [x3, x5]"); // read one reflected-name byte from the scan cursor emitter.instruction("cmp w6, #92"); // compare against PHP namespace separator '\\' emitter.instruction(&format!("b.eq {}", found_label)); // split at the final namespace separator emitter.instruction(&format!("cbnz x5, {}", scan_loop_label)); // keep scanning until the first byte has been checked - emitter.label(no_namespace_label); + emitter.label(&no_namespace_label); emitter.instruction("str x3, [sp, #56]"); // short-name pointer is the original name pointer emitter.instruction("str x4, [sp, #64]"); // short-name length is the full name length emitter.instruction("str xzr, [sp, #72]"); // namespace length is zero for global names emitter.instruction(&format!("b {}", store_parts_label)); // skip the namespaced split path - emitter.label(found_label); + emitter.label(&found_label); emitter.instruction("add x6, x5, #1"); // compute the short-name byte offset after the separator emitter.instruction("add x7, x3, x6"); // compute the short-name pointer emitter.instruction("sub x8, x4, x6"); // compute the short-name length emitter.instruction("str x7, [sp, #56]"); // save the short-name pointer across persistence calls emitter.instruction("str x8, [sp, #64]"); // save the short-name length across persistence calls emitter.instruction("str x5, [sp, #72]"); // namespace length is the separator offset - emitter.label(store_parts_label); + emitter.label(&store_parts_label); emitter.instruction("ldr x1, [sp, #8]"); // use the original name pointer for namespace persistence emitter.instruction("ldr x2, [sp, #72]"); // reload the namespace byte length emitter.instruction("bl __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage @@ -969,28 +981,40 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio else { return; }; - let scan_loop_label = "__elephc_eval_reflection_owner_name_scan_loop_x"; - let found_label = "__elephc_eval_reflection_owner_name_scan_found_x"; - let no_namespace_label = "__elephc_eval_reflection_owner_name_scan_none_x"; - let store_parts_label = "__elephc_eval_reflection_owner_name_store_parts_x"; + let scan_loop_label = format!( + "__elephc_eval_reflection_owner_name_scan_loop_{}_x", + layout.class_id + ); + let found_label = format!( + "__elephc_eval_reflection_owner_name_scan_found_{}_x", + layout.class_id + ); + let no_namespace_label = format!( + "__elephc_eval_reflection_owner_name_scan_none_{}_x", + layout.class_id + ); + let store_parts_label = format!( + "__elephc_eval_reflection_owner_name_store_parts_{}_x", + layout.class_id + ); emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload the original reflected-name pointer for splitting emitter.instruction("mov r9, QWORD PTR [rbp - 24]"); // reload the original reflected-name length for splitting emitter.instruction("mov r11, r9"); // start scanning from one byte past the final name byte emitter.instruction("test r11, r11"); // check whether the reflected name is empty emitter.instruction(&format!("jz {}", no_namespace_label)); // empty names have no namespace component - emitter.label(scan_loop_label); + emitter.label(&scan_loop_label); emitter.instruction("sub r11, 1"); // move the scan cursor to the previous byte emitter.instruction("movzx eax, BYTE PTR [r8 + r11]"); // read one reflected-name byte from the scan cursor emitter.instruction("cmp eax, 92"); // compare against PHP namespace separator '\\' emitter.instruction(&format!("je {}", found_label)); // split at the final namespace separator emitter.instruction("test r11, r11"); // check whether the first byte has been examined emitter.instruction(&format!("jnz {}", scan_loop_label)); // keep scanning until the first byte has been checked - emitter.label(no_namespace_label); + emitter.label(&no_namespace_label); emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // short-name pointer is the original name pointer emitter.instruction("mov QWORD PTR [rbp - 72], r9"); // short-name length is the full name length emitter.instruction("mov QWORD PTR [rbp - 80], 0"); // namespace length is zero for global names emitter.instruction(&format!("jmp {}", store_parts_label)); // skip the namespaced split path - emitter.label(found_label); + emitter.label(&found_label); emitter.instruction("lea rax, [r11 + 1]"); // compute the short-name byte offset after the separator emitter.instruction("lea r10, [r8 + rax]"); // compute the short-name pointer emitter.instruction("mov rcx, r9"); // copy the full name length before subtracting the prefix @@ -998,7 +1022,7 @@ fn emit_set_owner_name_property_x86_64(emitter: &mut Emitter, layout: &Reflectio emitter.instruction("mov QWORD PTR [rbp - 64], r10"); // save the short-name pointer across persistence calls emitter.instruction("mov QWORD PTR [rbp - 72], rcx"); // save the short-name length across persistence calls emitter.instruction("mov QWORD PTR [rbp - 80], r11"); // namespace length is the separator offset - emitter.label(store_parts_label); + emitter.label(&store_parts_label); emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // use the original name pointer for namespace persistence emitter.instruction("mov rdx, QWORD PTR [rbp - 80]"); // reload the namespace byte length emitter.instruction("call __rt_str_persist"); // copy the namespace bytes for ReflectionClass storage From 17de7f4787eb07002f0cf843359bc3d8debc3e11 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 14:29:07 +0200 Subject: [PATCH 0555/1208] Expose AOT reflection origin predicates --- docs/php/classes.md | 6 +- src/codegen/lower_inst/objects/reflection.rs | 78 +++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 74 ++++++++++++++++++ tests/codegen/oop/reflection_functions.rs | 21 +++++ tests/codegen/oop/reflection_methods.rs | 23 ++++++ 5 files changed, 201 insertions(+), 1 deletion(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index ea114e4df3..5ff9b8aba8 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1191,12 +1191,16 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::newInstanceWithoutConstructor()` | `new ReflectionClass($class_name)` | Allocate an instance of the reflected class without running `__construct()` | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | | `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user function name | +| `ReflectionFunction::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionFunction($function_name)` | Return namespace-aware name metadata for the reflected user function | +| `ReflectionFunction::isInternal()` / `isUserDefined()` | `new ReflectionFunction($function_name)` | Return origin predicates for supported reflected functions | | `ReflectionFunction::getAttributes()` | `new ReflectionFunction($function_name)` | Return `ReflectionAttribute` objects for function attributes | | `ReflectionFunction::getParameters()` | `new ReflectionFunction($function_name)` | Return `ReflectionParameter` objects for the reflected function parameters | | `ReflectionFunction::getNumberOfParameters()` | `new ReflectionFunction($function_name)` | Return the total number of reflected function parameters | | `ReflectionFunction::getNumberOfRequiredParameters()` | `new ReflectionFunction($function_name)` | Return the number of required reflected function parameters | | `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared parameter types are also lowered | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | +| `ReflectionMethod::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP method-name metadata; methods report an empty namespace and `false` for `inNamespace()` | +| `ReflectionMethod::isInternal()` / `isUserDefined()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return origin predicates for supported reflected methods | | `ReflectionMethod::getDeclaringClass()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected method | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | | `ReflectionMethod::isStatic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is static | @@ -1348,7 +1352,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `invoke()`, and `invokeArgs()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `invoke()`, and `invokeArgs()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 54c7ef7627..c521a617c4 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -398,6 +398,12 @@ fn emit_reflection_owner_object( let (_, short_name) = reflection_name_parts(reflected_name); emit_reflection_string_property_by_name(ctx, "__short_name", short_name)?; } + if class_name == "ReflectionFunction" { + emit_reflection_function_name_parts(ctx, reflected_name)?; + } + if class_name == "ReflectionMethod" { + emit_reflection_method_name_parts(ctx, reflected_name)?; + } } emit_reflection_attrs_property(ctx, class_name, &metadata.attr_names, &metadata.attr_args)?; if class_name == "ReflectionClass" { @@ -438,6 +444,13 @@ fn emit_reflection_owner_object( )?; } if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { + emit_reflection_owner_bool_property(ctx, class_name, "__is_internal", false)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_user_defined", + metadata.reflected_name.is_some(), + )?; emit_reflection_parameter_array_property_by_name( ctx, class_name, @@ -531,6 +544,54 @@ fn emit_reflection_class_name_parts( Ok(()) } +/// Stores namespace-aware name parts for a statically materialized ReflectionFunction. +fn emit_reflection_function_name_parts( + ctx: &mut FunctionContext<'_>, + reflected_name: &str, +) -> Result<()> { + let (namespace_name, short_name) = reflection_name_parts(reflected_name); + emit_reflection_owner_string_property_by_name( + ctx, + "ReflectionFunction", + "__short_name", + short_name, + )?; + emit_reflection_owner_string_property_by_name( + ctx, + "ReflectionFunction", + "__namespace_name", + namespace_name, + )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionFunction", + "__in_namespace", + !namespace_name.is_empty(), + )?; + Ok(()) +} + +/// Stores PHP's method reflection name parts for a statically materialized method. +fn emit_reflection_method_name_parts( + ctx: &mut FunctionContext<'_>, + reflected_name: &str, +) -> Result<()> { + emit_reflection_owner_string_property_by_name( + ctx, + "ReflectionMethod", + "__short_name", + reflected_name, + )?; + emit_reflection_owner_string_property_by_name( + ctx, + "ReflectionMethod", + "__namespace_name", + "", + )?; + emit_reflection_owner_bool_property(ctx, "ReflectionMethod", "__in_namespace", false)?; + Ok(()) +} + /// Splits a canonical PHP class-like name into namespace and short-name parts. fn reflection_name_parts(reflected_name: &str) -> (&str, &str) { match reflected_name.rfind('\\') { @@ -3715,6 +3776,23 @@ fn emit_reflection_string_property_by_name( Ok(()) } +/// Writes a heap-persisted string into a named Reflection owner property slot. +fn emit_reflection_owner_string_property_by_name( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + value: &str, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + emit_reflection_string_property(ctx, value, low_offset, low_offset + 8); + Ok(()) +} + /// Replaces the Reflection object's default `__attrs` array with populated metadata. fn emit_reflection_attrs_property( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 065404dcf2..59cff487ce 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3185,6 +3185,7 @@ fn builtin_reflection_owner_class( "getExtension", )); } + add_reflection_function_method_origin_methods(name, &mut properties, &mut methods); add_reflection_member_flag_methods(name, &mut properties, &mut methods); if matches!( name, @@ -3355,6 +3356,67 @@ fn builtin_reflection_parameter_count_method() -> ClassMethod { } } +/// Adds namespace/name-origin accessors shared by ReflectionFunction and ReflectionMethod. +fn add_reflection_function_method_origin_methods( + class_name: &str, + properties: &mut Vec, + methods: &mut Vec, +) { + if !matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { + return; + } + properties.push(builtin_property( + "__short_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + properties.push(builtin_property( + "__namespace_name", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + properties.push(builtin_property( + "__in_namespace", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_internal", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_user_defined", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + methods.push(builtin_reflection_class_string_method( + "getShortName", + "__short_name", + )); + methods.push(builtin_reflection_class_string_method( + "getNamespaceName", + "__namespace_name", + )); + methods.push(builtin_reflection_class_bool_method( + "inNamespace", + "__in_namespace", + )); + methods.push(builtin_reflection_class_bool_method( + "isInternal", + "__is_internal", + )); + methods.push(builtin_reflection_class_bool_method( + "isUserDefined", + "__is_user_defined", + )); +} + /// Adds member visibility/staticity predicates for method and property reflection owners. fn add_reflection_member_flag_methods( class_name: &str, @@ -3892,6 +3954,18 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Void; } } + if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { + for method_name in ["getShortName", "getNamespaceName"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Str; + } + } + for method_name in ["inNamespace", "isInternal", "isUserDefined"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Bool; + } + } + } if class_name == "ReflectionProperty" { for method_name in [ "isfinal", diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs index 881437f821..acf502b25c 100644 --- a/tests/codegen/oop/reflection_functions.rs +++ b/tests/codegen/oop/reflection_functions.rs @@ -14,6 +14,27 @@ use super::*; +/// Verifies `ReflectionFunction` exposes AOT function name and origin metadata. +#[test] +fn test_reflection_function_reports_aot_name_origin_predicates() { + let out = compile_and_run( + r#"getName() . ":"; +echo $ref->getShortName() . ":"; +echo $ref->getNamespaceName() . ":"; +echo ($ref->inNamespace() ? "Y" : "N") . ":"; +echo ($ref->isInternal() ? "I" : "i") . ":"; +echo $ref->isUserDefined() ? "U" : "u"; +"#, + ); + assert_eq!(out, "ReflectFunctionMetaNs\\sample:sample:ReflectFunctionMetaNs:Y:i:U"); +} + /// Verifies `ReflectionFunction::invoke()` calls declared AOT functions. #[test] fn test_reflection_function_invoke_calls_declared_aot_functions() { diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index 3640fd401c..c61e27097d 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -13,6 +13,29 @@ use super::*; +/// Verifies `ReflectionMethod` exposes AOT method name and origin metadata. +#[test] +fn test_reflection_method_reports_aot_name_origin_predicates() { + let out = compile_and_run( + r#"getName() . ":"; +echo $method->getShortName() . ":"; +echo $method->getNamespaceName() . ":"; +echo ($method->inNamespace() ? "Y" : "N") . ":"; +echo ($method->isInternal() ? "I" : "i") . ":"; +echo $method->isUserDefined() ? "U" : "u"; +"#, + ); + assert_eq!(out, "run:run::N:i:U"); +} + /// Verifies `ReflectionMethod::invoke()` calls declared AOT instance and static methods. #[test] fn test_reflection_method_invoke_calls_declared_aot_methods() { From afec40e10e7d0f7d7b716a7e0fb02df6a5a2806b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 14:39:56 +0200 Subject: [PATCH 0556/1208] Support AOT reflection variadic predicates --- docs/php/classes.md | 4 +- src/types/checker/builtin_types/reflection.rs | 75 +++++++++++++++++++ tests/codegen/oop/reflection_functions.rs | 18 +++++ tests/codegen/oop/reflection_methods.rs | 20 +++++ 4 files changed, 116 insertions(+), 1 deletion(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 5ff9b8aba8..0746055b3d 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1197,6 +1197,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::getParameters()` | `new ReflectionFunction($function_name)` | Return `ReflectionParameter` objects for the reflected function parameters | | `ReflectionFunction::getNumberOfParameters()` | `new ReflectionFunction($function_name)` | Return the total number of reflected function parameters | | `ReflectionFunction::getNumberOfRequiredParameters()` | `new ReflectionFunction($function_name)` | Return the number of required reflected function parameters | +| `ReflectionFunction::isVariadic()` | `new ReflectionFunction($function_name)` | Return whether the reflected function declares a variadic parameter | | `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared parameter types are also lowered | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP method-name metadata; methods report an empty namespace and `false` for `inNamespace()` | @@ -1217,6 +1218,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | +| `ReflectionMethod::isVariadic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method declares a variadic parameter | | `ReflectionParameter::getName()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | | `ReflectionParameter::getPosition()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | | `ReflectionParameter::isOptional()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return whether the parameter has a default value or is variadic | @@ -1352,7 +1354,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `invoke()`, and `invokeArgs()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isVariadic()`, `invoke()`, and `invokeArgs()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isVariadic()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 59cff487ce..1f7b0cd091 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3229,6 +3229,7 @@ fn builtin_reflection_owner_class( "getNumberOfRequiredParameters", "__required_parameter_count", )); + methods.push(builtin_reflection_function_method_is_variadic_method()); } if name == "ReflectionMethod" { methods.push(builtin_reflection_method_invoke_method()); @@ -3356,6 +3357,80 @@ fn builtin_reflection_parameter_count_method() -> ClassMethod { } } +/// Builds `ReflectionFunctionAbstract::isVariadic()` from the retained parameter list. +fn builtin_reflection_function_method_is_variadic_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let parameters = variable_expr("parameters", dummy_span); + let count = variable_expr("count", dummy_span); + let last_index = binary_expr( + count.clone(), + BinOp::Sub, + Expr::new(ExprKind::IntLiteral(1), dummy_span), + dummy_span, + ); + let last_parameter = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(parameters.clone()), + index: Box::new(last_index), + }, + dummy_span, + ); + ClassMethod { + name: "isVariadic".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![ + Stmt::new( + StmtKind::Assign { + name: "parameters".to_string(), + value: reflection_this_property("__parameters", dummy_span), + }, + dummy_span, + ), + Stmt::new( + StmtKind::Assign { + name: "count".to_string(), + value: function_call("count", vec![parameters], dummy_span), + }, + dummy_span, + ), + Stmt::new( + StmtKind::If { + condition: binary_expr( + count.clone(), + BinOp::StrictEq, + Expr::new(ExprKind::IntLiteral(0), dummy_span), + dummy_span, + ), + then_body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(method_call_expr( + last_parameter, + "isVariadic", + Vec::new(), + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Adds namespace/name-origin accessors shared by ReflectionFunction and ReflectionMethod. fn add_reflection_function_method_origin_methods( class_name: &str, diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs index acf502b25c..66e61a71ec 100644 --- a/tests/codegen/oop/reflection_functions.rs +++ b/tests/codegen/oop/reflection_functions.rs @@ -14,6 +14,24 @@ use super::*; +/// Verifies `ReflectionFunction::isVariadic()` reports the function-level variadic flag. +#[test] +fn test_reflection_function_reports_aot_variadic_flag() { + let out = compile_and_run( + r#"isVariadic() ? "V" : "v") . ":"; +echo $variadic->getNumberOfParameters() . ":"; +echo ($fixed->isVariadic() ? "V" : "v"); +"#, + ); + assert_eq!(out, "V:2:v"); +} + /// Verifies `ReflectionFunction` exposes AOT function name and origin metadata. #[test] fn test_reflection_function_reports_aot_name_origin_predicates() { diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index c61e27097d..30dcf5128c 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -13,6 +13,26 @@ use super::*; +/// Verifies `ReflectionMethod::isVariadic()` reports the method-level variadic flag. +#[test] +fn test_reflection_method_reports_aot_variadic_flag() { + let out = compile_and_run( + r#"isVariadic() ? "V" : "v") . ":"; +echo $variadic->getNumberOfParameters() . ":"; +echo ($fixed->isVariadic() ? "V" : "v"); +"#, + ); + assert_eq!(out, "V:2:v"); +} + /// Verifies `ReflectionMethod` exposes AOT method name and origin metadata. #[test] fn test_reflection_method_reports_aot_name_origin_predicates() { From 8e36e98aea5b5ae8806f5d2c2eaaefb813c45c96 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 14:52:03 +0200 Subject: [PATCH 0557/1208] Expose AOT reflection return types --- docs/php/classes.md | 4 +- src/codegen/lower_inst/objects/reflection.rs | 57 +++++++++++++++++-- src/types/checker/builtin_types/reflection.rs | 28 ++++++++- tests/codegen/oop/reflection_functions.rs | 41 +++++++++++++ tests/codegen/oop/reflection_methods.rs | 32 +++++++++++ 5 files changed, 156 insertions(+), 6 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 0746055b3d..628a244ea9 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1197,6 +1197,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::getParameters()` | `new ReflectionFunction($function_name)` | Return `ReflectionParameter` objects for the reflected function parameters | | `ReflectionFunction::getNumberOfParameters()` | `new ReflectionFunction($function_name)` | Return the total number of reflected function parameters | | `ReflectionFunction::getNumberOfRequiredParameters()` | `new ReflectionFunction($function_name)` | Return the number of required reflected function parameters | +| `ReflectionFunction::hasReturnType()` / `getReturnType()` | `new ReflectionFunction($function_name)` | Return retained declared named, nullable, union, `void`, or `never` return type metadata as `ReflectionNamedType`, `ReflectionUnionType`, or `null` when no supported return type is declared | | `ReflectionFunction::isVariadic()` | `new ReflectionFunction($function_name)` | Return whether the reflected function declares a variadic parameter | | `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared parameter types are also lowered | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | @@ -1218,6 +1219,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | +| `ReflectionMethod::hasReturnType()` / `getReturnType()` | `new ReflectionMethod($class_name, $method_name)` | Return retained declared named, nullable, union, `void`, or `never` return type metadata as `ReflectionNamedType`, `ReflectionUnionType`, or `null` when no supported return type is declared | | `ReflectionMethod::isVariadic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method declares a variadic parameter | | `ReflectionParameter::getName()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return the parameter name without `$` | | `ReflectionParameter::getPosition()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return the zero-based parameter position | @@ -1354,7 +1356,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isVariadic()`, `invoke()`, and `invokeArgs()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `isVariadic()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `invoke()`, and `invokeArgs()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index c521a617c4..a8f63d690b 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -134,6 +134,7 @@ enum ReflectionDeclaringFunctionMember { attr_names: Vec, attr_args: Vec>>, required_parameter_count: i64, + type_metadata: Option, }, Method { name: String, @@ -142,6 +143,7 @@ enum ReflectionDeclaringFunctionMember { attr_args: Vec>>, flags: ReflectionMemberFlags, required_parameter_count: i64, + type_metadata: Option, }, } @@ -463,6 +465,13 @@ fn emit_reflection_owner_object( "__required_parameter_count", metadata.required_parameter_count, )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__has_return_type", + metadata.type_metadata.is_some(), + )?; + emit_reflection_owner_type_property(ctx, class_name, metadata.type_metadata.as_ref())?; } if matches!( class_name, @@ -876,11 +885,13 @@ fn reflection_function_metadata( }; let reflected_name = function.name.trim_start_matches('\\').to_string(); let required_parameter_count = reflection_required_parameter_count(signature); + let type_metadata = reflection_return_type_metadata(signature); let declaring_function = ReflectionDeclaringFunctionMember::Function { name: reflected_name.clone(), attr_names: function.attribute_names.clone(), attr_args: function.attribute_args.clone(), required_parameter_count, + type_metadata: type_metadata.clone(), }; let mut metadata = empty_reflection_metadata(); metadata.reflected_name = Some(reflected_name); @@ -896,6 +907,7 @@ fn reflection_function_metadata( &[], )?; metadata.required_parameter_count = required_parameter_count; + metadata.type_metadata = type_metadata; Ok(metadata) } @@ -969,7 +981,7 @@ fn reflection_method_owner_metadata( backing_value: None, is_enum_case: member.is_enum_case, parameter_members: member.parameters, - type_metadata: None, + type_metadata: member.type_metadata, property_default_value: None, required_parameter_count: member.required_parameter_count, is_final: false, @@ -1107,11 +1119,13 @@ fn reflection_function_parameter_metadata( return Ok(empty_reflection_metadata()); }; let reflected_name = function.name.trim_start_matches('\\').to_string(); + let type_metadata = reflection_return_type_metadata(signature); let declaring_function = ReflectionDeclaringFunctionMember::Function { name: reflected_name, attr_names: function.attribute_names.clone(), attr_args: function.attribute_args.clone(), required_parameter_count: reflection_required_parameter_count(signature), + type_metadata, }; let parameters = reflection_parameter_members_with_declaring_function( ctx, @@ -2392,6 +2406,7 @@ fn reflection_class_method_member( return Ok(None); }; let required_parameter_count = reflection_required_parameter_count(sig); + let type_metadata = reflection_return_type_metadata(sig); let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), declaring_class_name: declaring_class_name.clone(), @@ -2399,6 +2414,7 @@ fn reflection_class_method_member( attr_args: attr_args.clone(), flags, required_parameter_count, + type_metadata: type_metadata.clone(), }; let parameters = reflection_parameter_members_with_declaring_class( ctx, @@ -2418,7 +2434,7 @@ fn reflection_class_method_member( is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), - type_metadata: None, + type_metadata, default_value: None, required_parameter_count, parameters, @@ -2467,6 +2483,7 @@ fn reflection_interface_method_member( .unwrap_or_else(|| interface_name.to_string()); let required_parameter_count = reflection_required_parameter_count(sig); let flags = reflection_member_flags(is_static, &Visibility::Public, false, true, false, false); + let type_metadata = reflection_return_type_metadata(sig); let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), declaring_class_name: Some(declaring_class_name.clone()), @@ -2474,6 +2491,7 @@ fn reflection_interface_method_member( attr_args: Vec::new(), flags, required_parameter_count, + type_metadata: type_metadata.clone(), }; let parameters = reflection_parameter_members_with_declaring_class( ctx, @@ -2493,7 +2511,7 @@ fn reflection_interface_method_member( is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), - type_metadata: None, + type_metadata, default_value: None, required_parameter_count, parameters, @@ -2537,6 +2555,7 @@ fn reflection_trait_method_member( false, ); let required_parameter_count = reflection_required_parameter_count(&info.signature); + let type_metadata = reflection_return_type_metadata(&info.signature); let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), declaring_class_name: Some(trait_name.to_string()), @@ -2544,6 +2563,7 @@ fn reflection_trait_method_member( attr_args: Vec::new(), flags, required_parameter_count, + type_metadata: type_metadata.clone(), }; let parameters = reflection_parameter_members_with_declaring_class( ctx, @@ -2563,7 +2583,7 @@ fn reflection_trait_method_member( is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), - type_metadata: None, + type_metadata, default_value: None, required_parameter_count, parameters, @@ -3054,6 +3074,23 @@ fn reflection_parameter_type_metadata( } } +/// Converts a declared return type into the supported `ReflectionType` subset. +fn reflection_return_type_metadata(sig: &FunctionSig) -> Option { + if !sig.declared_return { + return None; + } + match &sig.return_type { + PhpType::Void => Some(ReflectionParameterTypeMetadata::Named( + reflection_builtin_named_type("void", false), + )), + PhpType::Never => Some(ReflectionParameterTypeMetadata::Named( + reflection_builtin_named_type("never", false), + )), + PhpType::Union(members) => reflection_union_or_nullable_type_metadata(members), + ty => reflection_named_type_metadata(ty).map(ReflectionParameterTypeMetadata::Named), + } +} + /// Converts a normalized non-union parameter type into a simple `ReflectionNamedType`. fn reflection_named_type_metadata(ty: &PhpType) -> Option { match ty { @@ -4788,6 +4825,13 @@ fn emit_reflection_member_object( "__required_parameter_count", member.required_parameter_count, )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__has_return_type", + member.type_metadata.is_some(), + )?; + emit_reflection_owner_type_property(ctx, member_class_name, member.type_metadata.as_ref())?; emit_reflection_owner_int_property( ctx, member_class_name, @@ -5007,12 +5051,14 @@ fn emit_reflection_parameter_declaring_function_property( attr_names, attr_args, required_parameter_count, + type_metadata, }) => { let mut metadata = empty_reflection_metadata(); metadata.reflected_name = Some(name.clone()); metadata.attr_names = attr_names.clone(); metadata.attr_args = attr_args.clone(); metadata.required_parameter_count = *required_parameter_count; + metadata.type_metadata = type_metadata.clone(); emit_reflection_owner_object(ctx, "ReflectionFunction", &metadata)?; emit_box_current_value_as_mixed( ctx.emitter, @@ -5026,6 +5072,7 @@ fn emit_reflection_parameter_declaring_function_property( attr_args, flags, required_parameter_count, + type_metadata, }) => { let mut metadata = empty_reflection_metadata(); metadata.reflected_name = Some(name.clone()); @@ -5034,6 +5081,8 @@ fn emit_reflection_parameter_declaring_function_property( metadata.attr_args = attr_args.clone(); metadata.member_flags = *flags; metadata.required_parameter_count = *required_parameter_count; + metadata.type_metadata = type_metadata.clone(); + metadata.modifiers = reflection_method_modifiers_from_flags(*flags); emit_reflection_owner_object(ctx, "ReflectionMethod", &metadata)?; emit_box_current_value_as_mixed( ctx.emitter, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 1f7b0cd091..94d586b82b 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3213,6 +3213,18 @@ fn builtin_reflection_owner_class( Some(object_array_type("ReflectionParameter")), empty_array(), )); + properties.push(builtin_property( + "__type", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + properties.push(builtin_property( + "__has_return_type", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); properties.push(builtin_property( "__required_parameter_count", Visibility::Private, @@ -3229,6 +3241,11 @@ fn builtin_reflection_owner_class( "getNumberOfRequiredParameters", "__required_parameter_count", )); + methods.push(builtin_reflection_class_bool_method( + "hasReturnType", + "__has_return_type", + )); + methods.push(builtin_reflection_class_mixed_method("getReturnType", "__type")); methods.push(builtin_reflection_function_method_is_variadic_method()); } if name == "ReflectionMethod" { @@ -4035,11 +4052,20 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Str; } } - for method_name in ["inNamespace", "isInternal", "isUserDefined"] { + for method_name in [ + "inNamespace", + "isInternal", + "isUserDefined", + "hasReturnType", + "isVariadic", + ] { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { sig.return_type = PhpType::Bool; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getReturnType")) { + sig.return_type = PhpType::Mixed; + } } if class_name == "ReflectionProperty" { for method_name in [ diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs index 66e61a71ec..8e43891881 100644 --- a/tests/codegen/oop/reflection_functions.rs +++ b/tests/codegen/oop/reflection_functions.rs @@ -14,6 +14,47 @@ use super::*; +/// Verifies `ReflectionFunction` exposes declared AOT return type metadata. +#[test] +fn test_reflection_function_reports_aot_return_type_metadata() { + let out = compile_and_run( + r#"getReturnType(); +echo ($namedRef->hasReturnType() ? "T" : "t") . ":"; +echo $named->getName() . ":"; +echo ($named->allowsNull() ? "N" : "n") . ":"; +echo ($named->isBuiltin() ? "B" : "b") . ":"; +$declaring = $namedRef->getParameters()[0]->getDeclaringFunction()->getReturnType(); +echo $declaring->getName() . ":"; +$union = (new ReflectionFunction("reflect_return_union"))->getReturnType(); +if ($union instanceof ReflectionUnionType) { + echo count($union->getTypes()) . ":"; + foreach ($union->getTypes() as $type) { + echo $type->getName(); + echo $type->isBuiltin() ? "B" : "b"; + } +} else { + echo "not-union"; +} +echo ":"; +$never = (new ReflectionFunction("reflect_return_never"))->getReturnType(); +echo $never->getName() . ":"; +echo ($never->allowsNull() ? "N" : "n") . ":"; +echo ($never->isBuiltin() ? "B" : "b") . ":"; +$plain = new ReflectionFunction("reflect_return_plain"); +echo ($plain->hasReturnType() ? "P" : "p") . ":"; +echo $plain->getReturnType() === null ? "Q" : "q"; +"#, + ); + assert_eq!(out, "T:int:N:B:int:2:intBstringB:never:n:B:p:Q"); +} + /// Verifies `ReflectionFunction::isVariadic()` reports the function-level variadic flag. #[test] fn test_reflection_function_reports_aot_variadic_flag() { diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index 30dcf5128c..15177eeb39 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -13,6 +13,38 @@ use super::*; +/// Verifies `ReflectionMethod` exposes declared AOT return type metadata. +#[test] +fn test_reflection_method_reports_aot_return_type_metadata() { + let out = compile_and_run( + r#"getReturnType(); +echo ($namedRef->hasReturnType() ? "T" : "t") . ":"; +echo $named->getName() . ":"; +echo ($named->allowsNull() ? "N" : "n") . ":"; +echo ($named->isBuiltin() ? "B" : "b") . ":"; +$declaring = $namedRef->getParameters()[0]->getDeclaringFunction()->getReturnType(); +echo $declaring->getName() . ":"; +$static = (new ReflectionClass(ReflectMethodReturnTarget::class))->getMethod("factory")->getReturnType(); +echo $static->getName() . ":"; +echo ($static->allowsNull() ? "N" : "n") . ":"; +echo ($static->isBuiltin() ? "B" : "b") . ":"; +$plain = new ReflectionMethod(ReflectMethodReturnTarget::class, "plain"); +echo ($plain->hasReturnType() ? "P" : "p") . ":"; +echo $plain->getReturnType() === null ? "Q" : "q"; +"#, + ); + assert_eq!(out, "T:string:N:B:string:ReflectMethodReturnDep:n:b:p:Q"); +} + /// Verifies `ReflectionMethod::isVariadic()` reports the method-level variadic flag. #[test] fn test_reflection_method_reports_aot_variadic_flag() { From 0c8fce94c573499d1aa668617224ed393e6f87f8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 15:04:48 +0200 Subject: [PATCH 0558/1208] Expose AOT reflection function predicates --- docs/php/classes.md | 6 +- src/codegen/lower_inst/objects/reflection.rs | 107 ++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 68 ++++++++++- tests/codegen/oop/reflection_functions.rs | 27 +++++ tests/codegen/oop/reflection_methods.rs | 30 +++++ 5 files changed, 235 insertions(+), 3 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 628a244ea9..531be9b0e2 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1193,6 +1193,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user function name | | `ReflectionFunction::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionFunction($function_name)` | Return namespace-aware name metadata for the reflected user function | | `ReflectionFunction::isInternal()` / `isUserDefined()` | `new ReflectionFunction($function_name)` | Return origin predicates for supported reflected functions | +| `ReflectionFunction::isClosure()` / `isDeprecated()` / `returnsReference()` / `isGenerator()` | `new ReflectionFunction($function_name)` | Return retained function predicates; AOT reflection reports `false` for closures and return-by-reference, uses `#[Deprecated]` metadata, and reports generator functions from lowered generator flags | +| `ReflectionFunction::hasTentativeReturnType()` / `getTentativeReturnType()` / `isDisabled()` | `new ReflectionFunction($function_name)` | Return PHP-compatible defaults for supported user functions: no tentative return type and not disabled | | `ReflectionFunction::getAttributes()` | `new ReflectionFunction($function_name)` | Return `ReflectionAttribute` objects for function attributes | | `ReflectionFunction::getParameters()` | `new ReflectionFunction($function_name)` | Return `ReflectionParameter` objects for the reflected function parameters | | `ReflectionFunction::getNumberOfParameters()` | `new ReflectionFunction($function_name)` | Return the total number of reflected function parameters | @@ -1203,6 +1205,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP method-name metadata; methods report an empty namespace and `false` for `inNamespace()` | | `ReflectionMethod::isInternal()` / `isUserDefined()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return origin predicates for supported reflected methods | +| `ReflectionMethod::isClosure()` / `isDeprecated()` / `returnsReference()` / `isGenerator()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return retained method predicates; AOT reflection reports `false` for closures and return-by-reference, uses `#[Deprecated]` metadata, and reports generator methods from lowered generator flags | +| `ReflectionMethod::hasTentativeReturnType()` / `getTentativeReturnType()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP-compatible defaults for supported user methods: no tentative return type | | `ReflectionMethod::getDeclaringClass()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected method | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | | `ReflectionMethod::isStatic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is static | @@ -1356,7 +1360,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `invoke()`, and `invokeArgs()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index a8f63d690b..2dfc4200a0 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -63,6 +63,8 @@ struct ReflectionOwnerMetadata { type_metadata: Option, property_default_value: Option, required_parameter_count: i64, + is_deprecated: bool, + is_generator: bool, is_final: bool, is_abstract: bool, is_interface: bool, @@ -101,6 +103,8 @@ struct ReflectionListedMember { type_metadata: Option, default_value: Option, required_parameter_count: i64, + is_deprecated: bool, + is_generator: bool, parameters: Vec, } @@ -135,6 +139,8 @@ enum ReflectionDeclaringFunctionMember { attr_args: Vec>>, required_parameter_count: i64, type_metadata: Option, + is_deprecated: bool, + is_generator: bool, }, Method { name: String, @@ -144,6 +150,8 @@ enum ReflectionDeclaringFunctionMember { flags: ReflectionMemberFlags, required_parameter_count: i64, type_metadata: Option, + is_deprecated: bool, + is_generator: bool, }, } @@ -472,6 +480,18 @@ fn emit_reflection_owner_object( metadata.type_metadata.is_some(), )?; emit_reflection_owner_type_property(ctx, class_name, metadata.type_metadata.as_ref())?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_deprecated", + metadata.is_deprecated, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_generator", + metadata.is_generator, + )?; } if matches!( class_name, @@ -694,6 +714,8 @@ fn reflection_class_metadata_for_name( type_metadata: None, property_default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, is_final: info.is_final, is_abstract: info.is_abstract, is_interface: false, @@ -757,6 +779,8 @@ fn reflection_class_metadata_for_name( type_metadata: None, property_default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, is_final: false, is_abstract: false, is_interface: true, @@ -826,6 +850,8 @@ fn reflection_class_metadata_for_name( type_metadata: None, property_default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, is_final: false, is_abstract: false, is_interface: false, @@ -892,6 +918,8 @@ fn reflection_function_metadata( attr_args: function.attribute_args.clone(), required_parameter_count, type_metadata: type_metadata.clone(), + is_deprecated: signature.deprecation.is_some(), + is_generator: function.flags.is_generator, }; let mut metadata = empty_reflection_metadata(); metadata.reflected_name = Some(reflected_name); @@ -908,6 +936,8 @@ fn reflection_function_metadata( )?; metadata.required_parameter_count = required_parameter_count; metadata.type_metadata = type_metadata; + metadata.is_deprecated = signature.deprecation.is_some(); + metadata.is_generator = function.flags.is_generator; Ok(metadata) } @@ -984,6 +1014,8 @@ fn reflection_method_owner_metadata( type_metadata: member.type_metadata, property_default_value: None, required_parameter_count: member.required_parameter_count, + is_deprecated: member.is_deprecated, + is_generator: member.is_generator, is_final: false, is_abstract: false, is_interface: false, @@ -1050,6 +1082,8 @@ fn reflection_property_metadata( type_metadata: reflection_property_type_metadata(info, &property_name), property_default_value: reflection_property_default_value(info, &property_name), required_parameter_count: 0, + is_deprecated: false, + is_generator: false, is_final: false, is_abstract: false, is_interface: false, @@ -1126,6 +1160,8 @@ fn reflection_function_parameter_metadata( attr_args: function.attribute_args.clone(), required_parameter_count: reflection_required_parameter_count(signature), type_metadata, + is_deprecated: signature.deprecation.is_some(), + is_generator: function.flags.is_generator, }; let parameters = reflection_parameter_members_with_declaring_function( ctx, @@ -1248,6 +1284,8 @@ fn reflection_class_constant_metadata( type_metadata: None, property_default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, is_final: false, is_abstract: false, is_interface: false, @@ -1321,6 +1359,8 @@ fn reflection_enum_case_metadata( type_metadata: None, property_default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, is_final: false, is_abstract: false, is_interface: false, @@ -1373,6 +1413,8 @@ fn reflection_class_constant_owner_metadata( type_metadata: None, property_default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, is_final, is_abstract: false, is_interface: false, @@ -2216,6 +2258,8 @@ fn push_unique_constant_reflection_member( type_metadata: None, default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, parameters: Vec::new(), }); } @@ -2407,6 +2451,11 @@ fn reflection_class_method_member( }; let required_parameter_count = reflection_required_parameter_count(sig); let type_metadata = reflection_return_type_metadata(sig); + let is_generator = reflection_method_is_generator( + ctx, + declaring_class_name.as_deref().unwrap_or(class_name), + &method_key, + ); let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), declaring_class_name: declaring_class_name.clone(), @@ -2415,6 +2464,8 @@ fn reflection_class_method_member( flags, required_parameter_count, type_metadata: type_metadata.clone(), + is_deprecated: sig.deprecation.is_some(), + is_generator, }; let parameters = reflection_parameter_members_with_declaring_class( ctx, @@ -2437,6 +2488,8 @@ fn reflection_class_method_member( type_metadata, default_value: None, required_parameter_count, + is_deprecated: sig.deprecation.is_some(), + is_generator, parameters, })) } @@ -2492,6 +2545,8 @@ fn reflection_interface_method_member( flags, required_parameter_count, type_metadata: type_metadata.clone(), + is_deprecated: sig.deprecation.is_some(), + is_generator: false, }; let parameters = reflection_parameter_members_with_declaring_class( ctx, @@ -2514,6 +2569,8 @@ fn reflection_interface_method_member( type_metadata, default_value: None, required_parameter_count, + is_deprecated: sig.deprecation.is_some(), + is_generator: false, parameters, })) } @@ -2556,6 +2613,7 @@ fn reflection_trait_method_member( ); let required_parameter_count = reflection_required_parameter_count(&info.signature); let type_metadata = reflection_return_type_metadata(&info.signature); + let is_generator = reflection_method_is_generator(ctx, trait_name, &method_key); let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), declaring_class_name: Some(trait_name.to_string()), @@ -2564,6 +2622,8 @@ fn reflection_trait_method_member( flags, required_parameter_count, type_metadata: type_metadata.clone(), + is_deprecated: info.signature.deprecation.is_some(), + is_generator, }; let parameters = reflection_parameter_members_with_declaring_class( ctx, @@ -2586,10 +2646,29 @@ fn reflection_trait_method_member( type_metadata, default_value: None, required_parameter_count, + is_deprecated: info.signature.deprecation.is_some(), + is_generator, parameters, })) } +/// Returns whether the lowered method body is a generator function. +fn reflection_method_is_generator( + ctx: &FunctionContext<'_>, + declaring_class_name: &str, + method_name: &str, +) -> bool { + let expected_key = php_symbol_key(&format!( + "{}::{}", + declaring_class_name.trim_start_matches('\\'), + method_name + )); + ctx.module.class_methods.iter().any(|function| { + php_symbol_key(function.name.trim_start_matches('\\')) == expected_key + && function.flags.is_generator + }) +} + /// Builds ReflectionProperty array entries for the properties visible on one class. fn reflection_class_property_members( ctx: &FunctionContext<'_>, @@ -2644,6 +2723,8 @@ fn reflection_class_property_member( type_metadata, default_value, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, parameters: Vec::new(), }) } @@ -2728,6 +2809,8 @@ fn default_method_members( type_metadata: None, default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, parameters: Vec::new(), }) .collect() @@ -2768,6 +2851,8 @@ fn default_property_members( type_metadata: None, default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, parameters: Vec::new(), }) .collect() @@ -3637,6 +3722,8 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { type_metadata: None, property_default_value: None, required_parameter_count: 0, + is_deprecated: false, + is_generator: false, is_final: false, is_abstract: false, is_interface: false, @@ -4832,6 +4919,18 @@ fn emit_reflection_member_object( member.type_metadata.is_some(), )?; emit_reflection_owner_type_property(ctx, member_class_name, member.type_metadata.as_ref())?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__is_deprecated", + member.is_deprecated, + )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__is_generator", + member.is_generator, + )?; emit_reflection_owner_int_property( ctx, member_class_name, @@ -5052,6 +5151,8 @@ fn emit_reflection_parameter_declaring_function_property( attr_args, required_parameter_count, type_metadata, + is_deprecated, + is_generator, }) => { let mut metadata = empty_reflection_metadata(); metadata.reflected_name = Some(name.clone()); @@ -5059,6 +5160,8 @@ fn emit_reflection_parameter_declaring_function_property( metadata.attr_args = attr_args.clone(); metadata.required_parameter_count = *required_parameter_count; metadata.type_metadata = type_metadata.clone(); + metadata.is_deprecated = *is_deprecated; + metadata.is_generator = *is_generator; emit_reflection_owner_object(ctx, "ReflectionFunction", &metadata)?; emit_box_current_value_as_mixed( ctx.emitter, @@ -5073,6 +5176,8 @@ fn emit_reflection_parameter_declaring_function_property( flags, required_parameter_count, type_metadata, + is_deprecated, + is_generator, }) => { let mut metadata = empty_reflection_metadata(); metadata.reflected_name = Some(name.clone()); @@ -5083,6 +5188,8 @@ fn emit_reflection_parameter_declaring_function_property( metadata.required_parameter_count = *required_parameter_count; metadata.type_metadata = type_metadata.clone(); metadata.modifiers = reflection_method_modifiers_from_flags(*flags); + metadata.is_deprecated = *is_deprecated; + metadata.is_generator = *is_generator; emit_reflection_owner_object(ctx, "ReflectionMethod", &metadata)?; emit_box_current_value_as_mixed( ctx.emitter, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 94d586b82b..5b4b2087fa 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2758,6 +2758,27 @@ fn builtin_reflection_constant_false_union_method(method_name: &str) -> ClassMet } } +/// Returns a public Reflection predicate that always reports PHP `false`. +fn builtin_reflection_constant_false_bool_method(method_name: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public Reflection method that always reports PHP `null` as mixed. fn builtin_reflection_constant_null_mixed_method(method_name: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -3213,6 +3234,18 @@ fn builtin_reflection_owner_class( Some(object_array_type("ReflectionParameter")), empty_array(), )); + properties.push(builtin_property( + "__is_deprecated", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__is_generator", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); properties.push(builtin_property( "__type", Visibility::Private, @@ -3246,6 +3279,24 @@ fn builtin_reflection_owner_class( "__has_return_type", )); methods.push(builtin_reflection_class_mixed_method("getReturnType", "__type")); + methods.push(builtin_reflection_constant_false_bool_method("isClosure")); + methods.push(builtin_reflection_class_bool_method( + "isDeprecated", + "__is_deprecated", + )); + methods.push(builtin_reflection_constant_false_bool_method( + "returnsReference", + )); + methods.push(builtin_reflection_class_bool_method( + "isGenerator", + "__is_generator", + )); + methods.push(builtin_reflection_constant_false_bool_method( + "hasTentativeReturnType", + )); + methods.push(builtin_reflection_constant_null_mixed_method( + "getTentativeReturnType", + )); methods.push(builtin_reflection_function_method_is_variadic_method()); } if name == "ReflectionMethod" { @@ -3256,6 +3307,9 @@ fn builtin_reflection_owner_class( if name == "ReflectionFunction" { methods.push(builtin_reflection_function_invoke_method()); methods.push(builtin_reflection_function_invoke_args_method()); + methods.push(builtin_reflection_constant_false_bool_method( + "isDisabled", + )); } if name == "ReflectionProperty" { methods.push(builtin_reflection_set_accessible_method()); @@ -4058,13 +4112,20 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "isUserDefined", "hasReturnType", "isVariadic", + "isClosure", + "isDeprecated", + "returnsReference", + "isGenerator", + "hasTentativeReturnType", ] { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { sig.return_type = PhpType::Bool; } } - if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getReturnType")) { - sig.return_type = PhpType::Mixed; + for method_name in ["getReturnType", "getTentativeReturnType"] { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key(method_name)) { + sig.return_type = PhpType::Mixed; + } } } if class_name == "ReflectionProperty" { @@ -4152,6 +4213,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Int; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("isDisabled")) { + sig.return_type = PhpType::Bool; + } } if class_name == "ReflectionParameter" { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPosition")) { diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs index 8e43891881..42908d2f27 100644 --- a/tests/codegen/oop/reflection_functions.rs +++ b/tests/codegen/oop/reflection_functions.rs @@ -14,6 +14,33 @@ use super::*; +/// Verifies AOT `ReflectionFunction` exposes function-abstract predicate metadata. +#[test] +fn test_reflection_function_reports_aot_function_abstract_predicates() { + let out = compile_and_run( + r#"isDeprecated() ? "D" : "d") . ":"; +echo ($plain->isDeprecated() ? "D" : "d") . ":"; +echo ($generator->isGenerator() ? "G" : "g") . ":"; +echo ($plain->isGenerator() ? "G" : "g") . ":"; +echo ($plain->isClosure() ? "C" : "c") . ":"; +echo ($plain->returnsReference() ? "R" : "r") . ":"; +echo ($plain->hasTentativeReturnType() ? "H" : "h") . ":"; +echo ($plain->getTentativeReturnType() === null ? "Q" : "q") . ":"; +echo $plain->isDisabled() ? "X" : "x"; +"#, + ); + assert_eq!(out, "D:d:G:g:c:r:h:Q:x"); +} + /// Verifies `ReflectionFunction` exposes declared AOT return type metadata. #[test] fn test_reflection_function_reports_aot_return_type_metadata() { diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index 15177eeb39..6d77f04347 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -13,6 +13,36 @@ use super::*; +/// Verifies AOT `ReflectionMethod` exposes function-abstract predicate metadata. +#[test] +fn test_reflection_method_reports_aot_function_abstract_predicates() { + let out = compile_and_run( + r#"getMethod("generator"); +echo ($deprecated->isDeprecated() ? "D" : "d") . ":"; +echo ($plain->isDeprecated() ? "D" : "d") . ":"; +echo ($generator->isGenerator() ? "G" : "g") . ":"; +echo ($listed->isGenerator() ? "L" : "l") . ":"; +echo ($plain->isGenerator() ? "G" : "g") . ":"; +echo ($plain->isClosure() ? "C" : "c") . ":"; +echo ($plain->returnsReference() ? "R" : "r") . ":"; +echo ($plain->hasTentativeReturnType() ? "H" : "h") . ":"; +echo $plain->getTentativeReturnType() === null ? "Q" : "q"; +"#, + ); + assert_eq!(out, "D:d:G:L:g:c:r:h:Q"); +} + /// Verifies `ReflectionMethod` exposes declared AOT return type metadata. #[test] fn test_reflection_method_reports_aot_return_type_metadata() { From a549cbefad087eac93c47a45e2963fac49f74899 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 15:16:54 +0200 Subject: [PATCH 0559/1208] Expose AOT reflection method prototypes --- docs/php/classes.md | 3 +- src/codegen/lower_inst/objects/reflection.rs | 186 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 75 ++++++- tests/codegen/oop/reflection_methods.rs | 55 ++++++ 4 files changed, 315 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 531be9b0e2..d25bdbe278 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1207,6 +1207,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isInternal()` / `isUserDefined()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return origin predicates for supported reflected methods | | `ReflectionMethod::isClosure()` / `isDeprecated()` / `returnsReference()` / `isGenerator()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return retained method predicates; AOT reflection reports `false` for closures and return-by-reference, uses `#[Deprecated]` metadata, and reports generator methods from lowered generator flags | | `ReflectionMethod::hasTentativeReturnType()` / `getTentativeReturnType()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP-compatible defaults for supported user methods: no tentative return type | +| `ReflectionMethod::hasPrototype()` / `getPrototype()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return retained parent/interface prototype metadata for supported reflected method overrides and interface implementations | | `ReflectionMethod::getDeclaringClass()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected method | | `ReflectionMethod::getAttributes()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionAttribute` objects for method attributes | | `ReflectionMethod::isStatic()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is static | @@ -1360,7 +1361,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 2dfc4200a0..bcffb92e3e 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -65,6 +65,7 @@ struct ReflectionOwnerMetadata { required_parameter_count: i64, is_deprecated: bool, is_generator: bool, + prototype_member: Option>, is_final: bool, is_abstract: bool, is_interface: bool, @@ -105,6 +106,7 @@ struct ReflectionListedMember { required_parameter_count: i64, is_deprecated: bool, is_generator: bool, + prototype_member: Option>, parameters: Vec, } @@ -521,6 +523,13 @@ fn emit_reflection_owner_object( } if class_name == "ReflectionMethod" { emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__has_prototype", + metadata.prototype_member.is_some(), + )?; + emit_reflection_method_prototype_property(ctx, metadata.prototype_member.as_deref())?; } if class_name == "ReflectionProperty" { emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; @@ -716,6 +725,7 @@ fn reflection_class_metadata_for_name( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, is_final: info.is_final, is_abstract: info.is_abstract, is_interface: false, @@ -781,6 +791,7 @@ fn reflection_class_metadata_for_name( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, is_final: false, is_abstract: false, is_interface: true, @@ -852,6 +863,7 @@ fn reflection_class_metadata_for_name( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -1016,6 +1028,7 @@ fn reflection_method_owner_metadata( required_parameter_count: member.required_parameter_count, is_deprecated: member.is_deprecated, is_generator: member.is_generator, + prototype_member: member.prototype_member, is_final: false, is_abstract: false, is_interface: false, @@ -1084,6 +1097,7 @@ fn reflection_property_metadata( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -1286,6 +1300,7 @@ fn reflection_class_constant_metadata( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -1361,6 +1376,7 @@ fn reflection_enum_case_metadata( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -1415,6 +1431,7 @@ fn reflection_class_constant_owner_metadata( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, is_final, is_abstract: false, is_interface: false, @@ -2260,6 +2277,7 @@ fn push_unique_constant_reflection_member( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, parameters: Vec::new(), }); } @@ -2305,12 +2323,129 @@ fn reflection_method_declaring_class_name( info: &crate::types::ClassInfo, method_key: &str, ) -> Option { - info.method_impl_classes + info.method_declaring_classes .get(method_key) - .or_else(|| info.static_method_impl_classes.get(method_key)) + .or_else(|| info.static_method_declaring_classes.get(method_key)) .cloned() } +/// Returns a prototype method for a reflected generated/AOT class method, if PHP exposes one. +fn reflection_class_method_prototype_member( + ctx: &FunctionContext<'_>, + class_name: &str, + info: &crate::types::ClassInfo, + method_key: &str, + flags: ReflectionMemberFlags, +) -> Result>> { + if !reflection_method_is_declared_on_class(info, class_name, method_key, flags.is_static) { + return Ok(None); + } + if let Some(member) = + reflection_parent_method_prototype_member(ctx, info, method_key, flags.is_static)? + { + return Ok(Some(Box::new(member))); + } + reflection_interface_method_prototype_member(ctx, info, method_key, flags.is_static) + .map(|member| member.map(Box::new)) +} + +/// Returns whether a visible method entry is declared by the reflected class itself. +fn reflection_method_is_declared_on_class( + info: &crate::types::ClassInfo, + class_name: &str, + method_key: &str, + is_static: bool, +) -> bool { + let declaring_class = if is_static { + info.static_method_declaring_classes.get(method_key) + } else { + info.method_declaring_classes.get(method_key) + }; + declaring_class + .map(|declaring_class| { + php_symbol_key(declaring_class.trim_start_matches('\\')) + == php_symbol_key(class_name.trim_start_matches('\\')) + }) + .unwrap_or(false) +} + +/// Finds the nearest parent-class method that is a valid PHP prototype. +fn reflection_parent_method_prototype_member( + ctx: &FunctionContext<'_>, + info: &crate::types::ClassInfo, + method_key: &str, + is_static: bool, +) -> Result> { + let mut current = reflection_parent_class_name(ctx, info); + let mut seen = std::collections::HashSet::new(); + while let Some(parent_name) = current { + if !seen.insert(php_symbol_key(&parent_name)) { + break; + } + let Some((resolved_parent_name, parent_info)) = resolve_reflection_class(ctx, &parent_name) + else { + break; + }; + if reflection_class_has_method_kind(parent_info, method_key, is_static) { + if let Some(member) = + reflection_class_method_member(ctx, resolved_parent_name, parent_info, method_key)? + { + if !member.flags.is_private && member.flags.is_static == is_static { + return Ok(Some(member)); + } + } + } + current = reflection_parent_class_name(ctx, parent_info); + } + Ok(None) +} + +/// Returns whether a class metadata entry has a method with the requested staticness. +fn reflection_class_has_method_kind( + info: &crate::types::ClassInfo, + method_key: &str, + is_static: bool, +) -> bool { + if is_static { + info.static_methods.contains_key(method_key) + } else { + info.methods.contains_key(method_key) + } +} + +/// Finds the first implemented interface method that is a valid PHP prototype. +fn reflection_interface_method_prototype_member( + ctx: &FunctionContext<'_>, + info: &crate::types::ClassInfo, + method_key: &str, + is_static: bool, +) -> Result> { + for interface_name in &info.interfaces { + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + continue; + }; + let Some(interface_info) = ctx.module.interface_infos.get(interface_name) else { + continue; + }; + let has_method = if is_static { + interface_info.static_methods.contains_key(method_key) + } else { + interface_info.methods.contains_key(method_key) + }; + if !has_method { + continue; + } + if let Some(member) = + reflection_interface_method_member(ctx, interface_info, interface_name, method_key)? + { + if member.flags.is_static == is_static { + return Ok(Some(member)); + } + } + } + Ok(None) +} + /// Returns the class that declares one reflected instance or static property. fn reflection_property_declaring_class_name( info: &crate::types::ClassInfo, @@ -2456,6 +2591,8 @@ fn reflection_class_method_member( declaring_class_name.as_deref().unwrap_or(class_name), &method_key, ); + let prototype_member = + reflection_class_method_prototype_member(ctx, class_name, info, &method_key, flags)?; let declaring_function = ReflectionDeclaringFunctionMember::Method { name: method_key.clone(), declaring_class_name: declaring_class_name.clone(), @@ -2490,6 +2627,7 @@ fn reflection_class_method_member( required_parameter_count, is_deprecated: sig.deprecation.is_some(), is_generator, + prototype_member, parameters, })) } @@ -2571,6 +2709,7 @@ fn reflection_interface_method_member( required_parameter_count, is_deprecated: sig.deprecation.is_some(), is_generator: false, + prototype_member: None, parameters, })) } @@ -2648,6 +2787,7 @@ fn reflection_trait_method_member( required_parameter_count, is_deprecated: info.signature.deprecation.is_some(), is_generator, + prototype_member: None, parameters, })) } @@ -2725,6 +2865,7 @@ fn reflection_class_property_member( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, parameters: Vec::new(), }) } @@ -2811,6 +2952,7 @@ fn default_method_members( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, parameters: Vec::new(), }) .collect() @@ -2853,6 +2995,7 @@ fn default_property_members( required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, parameters: Vec::new(), }) .collect() @@ -3724,6 +3867,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { required_parameter_count: 0, is_deprecated: false, is_generator: false, + prototype_member: None, is_final: false, is_abstract: false, is_interface: false, @@ -4202,6 +4346,37 @@ fn emit_reflection_constructor_property( Ok(()) } +/// Replaces a ReflectionMethod private prototype slot with `ReflectionMethod|null`. +fn emit_reflection_method_prototype_property( + ctx: &mut FunctionContext<'_>, + member: Option<&ReflectionListedMember>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get("ReflectionMethod") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, "__prototype")?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(member) = member { + emit_reflection_member_object(ctx, "ReflectionMethod", member)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionMethod".to_string()), + ); + } else { + emit_boxed_null_literal_to_result(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + /// Replaces the ReflectionClass private parent slot with `ReflectionClass|false`. fn emit_reflection_parent_class_property( ctx: &mut FunctionContext<'_>, @@ -4937,6 +5112,13 @@ fn emit_reflection_member_object( "__modifiers", member.modifiers, )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__has_prototype", + member.prototype_member.is_some(), + )?; + emit_reflection_method_prototype_property(ctx, member.prototype_member.as_deref())?; } if member_class_name == "ReflectionProperty" { emit_reflection_owner_int_property( diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 5b4b2087fa..6b997060c2 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2707,6 +2707,53 @@ fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> Cl } } +/// Returns `ReflectionMethod::getPrototype()` backed by a retained prototype reflector. +fn builtin_reflection_method_get_prototype_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "getPrototype".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Named(Name::unqualified("ReflectionMethod"))), + body: vec![ + Stmt::new( + StmtKind::If { + condition: Expr::new( + ExprKind::Not(Box::new(reflection_this_property( + "__has_prototype", + dummy_span, + ))), + dummy_span, + ), + then_body: vec![throw_new_reflection_exception( + string_lit("Method does not have a prototype", dummy_span), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(reflection_this_property( + "__prototype", + dummy_span, + ))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns `ReflectionProperty::isDefault()` backed by the dynamic-property slot. fn builtin_reflection_property_is_default_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -3300,6 +3347,23 @@ fn builtin_reflection_owner_class( methods.push(builtin_reflection_function_method_is_variadic_method()); } if name == "ReflectionMethod" { + properties.push(builtin_property( + "__has_prototype", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__prototype", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + methods.push(builtin_reflection_class_bool_method( + "hasPrototype", + "__has_prototype", + )); + methods.push(builtin_reflection_method_get_prototype_method()); methods.push(builtin_reflection_method_invoke_method()); methods.push(builtin_reflection_method_invoke_args_method()); methods.push(builtin_reflection_set_accessible_method()); @@ -4167,11 +4231,20 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionMethod" { - for method_name in ["isfinal", "isabstract", "isconstructor", "isdestructor"] { + for method_name in [ + "isfinal", + "isabstract", + "isconstructor", + "isdestructor", + "hasPrototype", + ] { if let Some(sig) = class_info.methods.get_mut(method_name) { sig.return_type = PhpType::Bool; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPrototype")) { + sig.return_type = PhpType::Object("ReflectionMethod".to_string()); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index 6d77f04347..77d5342aa3 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -118,6 +118,61 @@ echo $method->isUserDefined() ? "U" : "u"; assert_eq!(out, "run:run::N:i:U"); } +/// Verifies AOT `ReflectionMethod::hasPrototype()` and `getPrototype()` follow PHP inheritance rules. +#[test] +fn test_reflection_method_reports_aot_prototypes() { + let out = compile_and_run( + r#"getPrototype(); +echo ($override->hasPrototype() ? "Y" : "N") . ":"; +echo $overrideProto->getDeclaringClass()->getName() . "::"; +echo $overrideProto->getName() . ":"; +$iface = (new ReflectionClass(ReflectMethodProtoChild::class))->getMethod("iface"); +$ifaceProto = $iface->getPrototype(); +echo ($iface->hasPrototype() ? "Y" : "N") . ":"; +echo $ifaceProto->getDeclaringClass()->getName() . "::"; +echo $ifaceProto->getName() . ":"; +$parentIface = new ReflectionMethod(ReflectMethodProtoChild::class, "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName() . "::"; +echo $parentIfaceProto->getName() . ":"; +$own = new ReflectionMethod(ReflectMethodProtoChild::class, "own"); +echo ($own->hasPrototype() ? "Y" : "N") . ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod(ReflectMethodProtoChild::class, "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N"; +"#, + ); + assert_eq!( + out, + "Y:ReflectMethodProtoBase::run:Y:ReflectMethodProtoIface::iface:ReflectMethodProtoParentIface::parented:N:E:N" + ); +} + /// Verifies `ReflectionMethod::invoke()` calls declared AOT instance and static methods. #[test] fn test_reflection_method_invoke_calls_declared_aot_methods() { From e7fe070ee26ea130af1a477e0dc111744e3a519d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 15:31:02 +0200 Subject: [PATCH 0560/1208] Expose AOT ReflectionProperty string metadata --- docs/php/classes.md | 3 +- src/codegen/lower_inst/objects/reflection.rs | 129 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 13 ++ tests/codegen/oop/reflection_properties.rs | 30 ++++ 4 files changed, 172 insertions(+), 3 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index d25bdbe278..ae927fa3b8 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1265,6 +1265,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isDynamic()` | Same as `ReflectionProperty::isDefault()` | Return `false` for supported declared properties and `true` for public dynamic object properties materialized from an object argument | | `ReflectionProperty::isLazy()` | Same as `ReflectionProperty::isDefault()` plus an object argument | Return `false` for supported declared and dynamic properties because elephc does not implement lazy properties | | `ReflectionProperty::skipLazyInitialization()` | Same as `ReflectionProperty::isLazy()` | Accepted as a no-op for supported non-static, non-virtual declared properties | +| `ReflectionProperty::__toString()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Format retained generated/eval property metadata as a PHP-style `Property [ ... ]` descriptor | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | @@ -1361,7 +1362,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index bcffb92e3e..9223fa694b 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -545,6 +545,18 @@ fn emit_reflection_owner_object( class_name, metadata.property_default_value.as_ref(), )?; + let property_string = reflection_property_to_string( + metadata.reflected_name.as_deref().unwrap_or(""), + metadata.member_flags, + metadata.type_metadata.as_ref(), + metadata.property_default_value.as_ref(), + ); + emit_reflection_owner_string_property_by_name( + ctx, + class_name, + "__string", + &property_string, + )?; } if class_name == "ReflectionParameter" { if let Some(parameter) = metadata.parameter_members.first() { @@ -2875,10 +2887,17 @@ fn reflection_property_type_metadata( info: &crate::types::ClassInfo, property_name: &str, ) -> Option { - if !info.visible_property_is_declared(property_name) { + if info.visible_property_is_declared(property_name) { + let (_, (_, property_type)) = info.visible_property(property_name)?; + return reflection_parameter_type_metadata(None, property_type); + } + if !info.declared_static_properties.contains(property_name) { return None; } - let (_, (_, property_type)) = info.visible_property(property_name)?; + let (_, property_type) = info + .static_properties + .iter() + .find(|(name, _)| name == property_name)?; reflection_parameter_type_metadata(None, property_type) } @@ -2916,6 +2935,100 @@ fn reflection_property_slot_default_value( } } +/// Formats retained generated property metadata for `ReflectionProperty::__toString()`. +fn reflection_property_to_string( + property_name: &str, + flags: ReflectionMemberFlags, + type_metadata: Option<&ReflectionParameterTypeMetadata>, + default_value: Option<&ReflectionParameterDefaultValue>, +) -> String { + let mut parts = Vec::new(); + if flags.is_abstract { + parts.push(String::from("abstract")); + } + if flags.is_final { + parts.push(String::from("final")); + } + parts.push(reflection_property_visibility_label(flags).to_string()); + if flags.is_static { + parts.push(String::from("static")); + } + if flags.is_readonly { + parts.push(String::from("readonly")); + } + if let Some(type_name) = type_metadata.map(reflection_type_metadata_to_string) { + parts.push(type_name); + } + parts.push(format!("${property_name}")); + + let default = if flags.is_virtual { + String::new() + } else { + default_value + .and_then(reflection_default_value_to_string) + .map(|value| format!(" = {value}")) + .unwrap_or_default() + }; + format!("Property [ {}{} ]", parts.join(" "), default) +} + +/// Returns PHP's lowercase visibility label for one reflected property. +fn reflection_property_visibility_label(flags: ReflectionMemberFlags) -> &'static str { + if flags.is_private { + "private" + } else if flags.is_protected { + "protected" + } else { + "public" + } +} + +/// Formats retained ReflectionType metadata for property string output. +fn reflection_type_metadata_to_string(type_metadata: &ReflectionParameterTypeMetadata) -> String { + match type_metadata { + ReflectionParameterTypeMetadata::Named(named) => { + if named.allows_null && named.name != "mixed" { + format!("?{}", named.name) + } else { + named.name.clone() + } + } + ReflectionParameterTypeMetadata::Union(union) => { + let mut names = union + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>(); + if union.allows_null && names.iter().all(|name| name != "null") { + names.push(String::from("null")); + } + names.join("|") + } + ReflectionParameterTypeMetadata::Intersection(intersection) => intersection + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>() + .join("&"), + } +} + +/// Formats retained scalar defaults for property string output. +fn reflection_default_value_to_string( + default: &ReflectionParameterDefaultValue, +) -> Option { + match default { + ReflectionParameterDefaultValue::Int(value) => Some(value.to_string()), + ReflectionParameterDefaultValue::Bool(value) => Some(value.to_string()), + ReflectionParameterDefaultValue::Float(value) => Some(value.to_string()), + ReflectionParameterDefaultValue::Str(value) => Some(format!("'{value}'")), + ReflectionParameterDefaultValue::Null => Some(String::from("NULL")), + ReflectionParameterDefaultValue::Object { .. } + | ReflectionParameterDefaultValue::Array(_) + | ReflectionParameterDefaultValue::AssocArray(_) => None, + } +} + /// Builds placeholder ReflectionMethod entries for class-like metadata without full method schemas. fn default_method_members( method_names: &[String], @@ -5139,6 +5252,18 @@ fn emit_reflection_member_object( member_class_name, member.default_value.as_ref(), )?; + let property_string = reflection_property_to_string( + &member.name, + member.flags, + member.type_metadata.as_ref(), + member.default_value.as_ref(), + ); + emit_reflection_owner_string_property_by_name( + ctx, + member_class_name, + "__string", + &property_string, + )?; } if member_class_name == "ReflectionClassConstant" { if let Some(value) = &member.constant_value { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 6b997060c2..2b64d9e04a 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3753,6 +3753,16 @@ fn add_reflection_member_flag_methods( "isDynamic", "__is_dynamic", )); + properties.push(builtin_property( + "__string", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + methods.push(builtin_reflection_class_string_method( + "__toString", + "__string", + )); methods.push(builtin_reflection_property_is_lazy_method()); methods.push(builtin_reflection_property_skip_lazy_initialization_method()); methods.push(builtin_reflection_property_get_value_method()); @@ -4229,6 +4239,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } } if class_name == "ReflectionMethod" { for method_name in [ diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 8078ce8afb..a0ae31a414 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -408,3 +408,33 @@ echo ":" . $label->getValue($target); ); assert_eq!(out, "P:4:9:L:old"); } + +/// Verifies AOT `ReflectionProperty::__toString()` formats retained generated +/// property metadata. +#[test] +fn test_reflection_property_to_string_formats_aot_metadata() { + let out = compile_and_run( + r#"__toString(); +echo "|"; +echo (new ReflectionProperty(ReflectPropertyStringTarget::class, "label"))->__toString(); +echo "|"; +echo (new ReflectionProperty(ReflectPropertyStringTarget::class, "implicit"))->__toString(); +echo "|"; +echo (new ReflectionProperty(ReflectPropertyStringTarget::class, "union"))->__toString(); +echo "|"; +echo (new ReflectionClass(ReflectPropertyStringTarget::class))->getProperty("label")->__toString(); +"#, + ); + assert_eq!( + out, + "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public int|string $union ]|Property [ protected static string $label = 'ok' ]" + ); +} From 695f253265d67df1cfaa0c343a7f14207dbeb6b5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 16:12:59 +0200 Subject: [PATCH 0561/1208] Expose AOT reflection property hooks --- docs/php/classes.md | 6 +- src/codegen/eval_reflection_owner_helpers.rs | 56 ++- src/codegen/lower_inst/objects/reflection.rs | 339 +++++++++++++++++- src/types/checker/builtin_types/reflection.rs | 38 ++ tests/codegen/oop/attributes.rs | 3 +- tests/codegen/oop/reflection_properties.rs | 40 +++ 6 files changed, 459 insertions(+), 23 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index ae927fa3b8..2695bfa005 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1266,6 +1266,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::isLazy()` | Same as `ReflectionProperty::isDefault()` plus an object argument | Return `false` for supported declared and dynamic properties because elephc does not implement lazy properties | | `ReflectionProperty::skipLazyInitialization()` | Same as `ReflectionProperty::isLazy()` | Accepted as a no-op for supported non-static, non-virtual declared properties | | `ReflectionProperty::__toString()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Format retained generated/eval property metadata as a PHP-style `Property [ ... ]` descriptor | +| `ReflectionProperty::hasHooks()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has retained get/set hook metadata | +| `ReflectionProperty::getHooks()` | Same as `ReflectionProperty::hasHooks()` | Return a string-keyed map of retained concrete generated/AOT property hook `ReflectionMethod` objects | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | @@ -1362,8 +1364,8 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionProperty::hasHooks()` and `getHooks()` expose retained concrete generated/AOT property get/set hooks as string-keyed `ReflectionMethod` objects; `hasHook()` and `getHook()` still require eval-backed `PropertyHookType` support. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index b5f58ef3dd..7e2f9122de 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -485,6 +485,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection class_label, &layouts.class, true, + false, fail_label, box_label, ); @@ -493,6 +494,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection function_label, &layouts.function, true, + false, fail_label, box_label, ); @@ -501,6 +503,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection method_label, &layouts.method, true, + true, fail_label, box_label, ); @@ -509,6 +512,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection property_label, &layouts.property, true, + false, fail_label, box_label, ); @@ -517,6 +521,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection class_constant_label, &layouts.class_constant, true, + false, fail_label, box_label, ); @@ -525,6 +530,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection enum_unit_case_label, &layouts.enum_unit_case, true, + false, fail_label, box_label, ); @@ -533,6 +539,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection enum_backed_case_label, &layouts.enum_backed_case, true, + false, fail_label, box_label, ); @@ -541,6 +548,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection parameter_label, &layouts.parameter, true, + false, fail_label, box_label, ); @@ -549,6 +557,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection named_type_label, &layouts.named_type, true, + false, fail_label, box_label, ); @@ -557,6 +566,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection union_type_label, &layouts.union_type, false, + false, fail_label, box_label, ); @@ -565,6 +575,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection intersection_type_label, &layouts.intersection_type, false, + false, fail_label, box_label, ); @@ -655,6 +666,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO class_label, &layouts.class, true, + false, fail_label, box_label, ); @@ -663,6 +675,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO function_label, &layouts.function, true, + false, fail_label, box_label, ); @@ -671,6 +684,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO method_label, &layouts.method, true, + true, fail_label, box_label, ); @@ -679,6 +693,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO property_label, &layouts.property, true, + false, fail_label, box_label, ); @@ -687,6 +702,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO class_constant_label, &layouts.class_constant, true, + false, fail_label, box_label, ); @@ -695,6 +711,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO enum_unit_case_label, &layouts.enum_unit_case, true, + false, fail_label, box_label, ); @@ -703,6 +720,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO enum_backed_case_label, &layouts.enum_backed_case, true, + false, fail_label, box_label, ); @@ -711,6 +729,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO parameter_label, &layouts.parameter, true, + false, fail_label, box_label, ); @@ -719,6 +738,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO named_type_label, &layouts.named_type, true, + false, fail_label, box_label, ); @@ -727,6 +747,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO union_type_label, &layouts.union_type, false, + false, fail_label, box_label, ); @@ -735,6 +756,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO intersection_type_label, &layouts.intersection_type, false, + false, fail_label, box_label, ); @@ -758,6 +780,7 @@ fn emit_aarch64_owner_kind_body( label: &str, layout: &ReflectionOwnerLayout, set_name: bool, + is_method: bool, fail_label: &str, box_label: &str, ) { @@ -768,7 +791,7 @@ fn emit_aarch64_owner_kind_body( emit_set_owner_name_property_aarch64(emitter, layout); } emit_set_owner_class_flags_property_aarch64(emitter, layout); - emit_set_owner_member_flags_property_aarch64(emitter, layout); + emit_set_owner_member_flags_property_aarch64(emitter, layout, is_method); emit_set_owner_constant_value_property_aarch64(emitter, layout, fail_label); emit_set_owner_backing_value_property_aarch64(emitter, layout, fail_label); emit_set_owner_required_parameter_count_property_aarch64(emitter, layout); @@ -791,6 +814,7 @@ fn emit_x86_64_owner_kind_body( label: &str, layout: &ReflectionOwnerLayout, set_name: bool, + is_method: bool, fail_label: &str, box_label: &str, ) { @@ -801,7 +825,7 @@ fn emit_x86_64_owner_kind_body( emit_set_owner_name_property_x86_64(emitter, layout); } emit_set_owner_class_flags_property_x86_64(emitter, layout); - emit_set_owner_member_flags_property_x86_64(emitter, layout); + emit_set_owner_member_flags_property_x86_64(emitter, layout, is_method); emit_set_owner_constant_value_property_x86_64(emitter, layout, fail_label); emit_set_owner_backing_value_property_x86_64(emitter, layout, fail_label); emit_set_owner_required_parameter_count_property_x86_64(emitter, layout); @@ -1293,6 +1317,7 @@ fn emit_set_owner_class_flags_property_x86_64( fn emit_set_owner_member_flags_property_aarch64( emitter: &mut Emitter, layout: &ReflectionOwnerLayout, + is_method: bool, ) { let ( Some(is_public_lo), @@ -1378,8 +1403,16 @@ fn emit_set_owner_member_flags_property_aarch64( abi::emit_store_zero_to_address(emitter, "x9", is_dynamic_hi); } if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { - if layout.required_parameter_count_lo.is_some() { - emitter.instruction("ldr x10, [sp, #72]"); // reload PHP ReflectionMethod::getModifiers() bitmask + if is_method { + emitter.instruction("ldr x11, [sp, #48]"); // reload ReflectionMethod predicate flags + emitter.instruction("and x10, x11, #14"); // keep public/protected/private visibility flags + emitter.instruction("lsr x10, x10, #1"); // shift visibility flags into PHP modifier bit positions + emitter.instruction("and x12, x11, #1"); // isolate the static-method flag + emitter.instruction("lsl x12, x12, #4"); // move static into PHP modifier bit 16 + emitter.instruction("orr x10, x10, x12"); // merge static into the method modifier bitmask + emitter.instruction("and x12, x11, #48"); // keep final/abstract method flags + emitter.instruction("lsl x12, x12, #1"); // move final/abstract into PHP modifier bit positions + emitter.instruction("orr x10, x10, x12"); // merge final/abstract into the method modifier bitmask } else { emitter.instruction("ldr x10, [sp, #96]"); // reload PHP Reflection member getModifiers() bitmask } @@ -1422,6 +1455,7 @@ fn emit_set_owner_low_bit_final_property_aarch64( fn emit_set_owner_member_flags_property_x86_64( emitter: &mut Emitter, layout: &ReflectionOwnerLayout, + is_method: bool, ) { let ( Some(is_public_lo), @@ -1518,8 +1552,18 @@ fn emit_set_owner_member_flags_property_x86_64( abi::emit_store_zero_to_address(emitter, "r10", is_dynamic_hi); } if let (Some(modifiers_lo), Some(modifiers_hi)) = (layout.modifiers_lo, layout.modifiers_hi) { - if layout.required_parameter_count_lo.is_some() { - emitter.instruction("mov rax, QWORD PTR [rbp - 80]"); // reload PHP ReflectionMethod::getModifiers() bitmask + if is_method { + emitter.instruction("mov rax, r11"); // copy ReflectionMethod predicate flags for visibility + emitter.instruction("and rax, 14"); // keep public/protected/private visibility flags + emitter.instruction("shr rax, 1"); // shift visibility flags into PHP modifier bit positions + emitter.instruction("mov rdx, r11"); // copy ReflectionMethod predicate flags for static + emitter.instruction("and rdx, 1"); // isolate the static-method flag + emitter.instruction("shl rdx, 4"); // move static into PHP modifier bit 16 + emitter.instruction("or rax, rdx"); // merge static into the method modifier bitmask + emitter.instruction("mov rdx, r11"); // copy ReflectionMethod predicate flags for final/abstract + emitter.instruction("and rdx, 48"); // keep final/abstract method flags + emitter.instruction("shl rdx, 1"); // move final/abstract into PHP modifier bit positions + emitter.instruction("or rax, rdx"); // merge final/abstract into the method modifier bitmask } else { emitter.instruction("mov rax, QWORD PTR [rbp - 104]"); // reload PHP Reflection member getModifiers() bitmask } diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 9223fa694b..333fbe48d2 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -54,6 +54,7 @@ struct ReflectionOwnerMetadata { constant_reflection_members: Vec, method_members: Vec, property_members: Vec, + property_hook_members: Vec<(String, ReflectionListedMember)>, constructor_member: Option, parent_class_name: Option, constant_value: Option, @@ -103,6 +104,7 @@ struct ReflectionListedMember { modifiers: i64, type_metadata: Option, default_value: Option, + property_hook_members: Vec<(String, ReflectionListedMember)>, required_parameter_count: i64, is_deprecated: bool, is_generator: bool, @@ -545,6 +547,18 @@ fn emit_reflection_owner_object( class_name, metadata.property_default_value.as_ref(), )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__has_hooks", + !metadata.property_hook_members.is_empty(), + )?; + emit_reflection_property_hook_array_property_by_name( + ctx, + class_name, + "__hooks", + &metadata.property_hook_members, + )?; let property_string = reflection_property_to_string( metadata.reflected_name.as_deref().unwrap_or(""), metadata.member_flags, @@ -726,6 +740,7 @@ fn reflection_class_metadata_for_name( constant_reflection_members, method_members, property_members, + property_hook_members: Vec::new(), constructor_member, parent_class_name: reflection_parent_class_name(ctx, info), constant_value: None, @@ -792,6 +807,7 @@ fn reflection_class_metadata_for_name( constant_reflection_members, method_members, property_members, + property_hook_members: Vec::new(), constructor_member, parent_class_name: None, constant_value: None, @@ -864,6 +880,7 @@ fn reflection_class_metadata_for_name( constant_reflection_members, method_members, property_members, + property_hook_members: Vec::new(), constructor_member, parent_class_name: None, constant_value: None, @@ -1029,6 +1046,7 @@ fn reflection_method_owner_metadata( constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + property_hook_members: Vec::new(), constructor_member: None, parent_class_name: member.declaring_class_name, constant_value: member.constant_value, @@ -1073,6 +1091,15 @@ fn reflection_property_metadata( .and_then(|(_, info)| { let declaring_class_name = reflection_property_declaring_class_name(info, &property_name); + let type_metadata = reflection_property_type_metadata(info, &property_name); + let member_flags = reflection_property_member_flags(info, &property_name)?; + let property_hook_members = reflection_property_hook_members( + info, + &property_name, + declaring_class_name.as_deref(), + member_flags, + type_metadata.as_ref(), + ); Some(ReflectionOwnerMetadata { reflected_name: Some(property_name.clone()), attr_names: info @@ -1098,13 +1125,14 @@ fn reflection_property_metadata( constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + property_hook_members, constructor_member: None, parent_class_name: declaring_class_name, constant_value: None, backing_value: None, is_enum_case: false, parameter_members: Vec::new(), - type_metadata: reflection_property_type_metadata(info, &property_name), + type_metadata, property_default_value: reflection_property_default_value(info, &property_name), required_parameter_count: 0, is_deprecated: false, @@ -1121,7 +1149,7 @@ fn reflection_property_metadata( is_cloneable: false, is_iterable: false, modifiers: reflection_property_modifiers_for_info(info, &property_name)?, - member_flags: reflection_property_member_flags(info, &property_name)?, + member_flags, }) }) .unwrap_or_else(empty_reflection_metadata)) @@ -1298,6 +1326,7 @@ fn reflection_class_constant_metadata( constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + property_hook_members: Vec::new(), constructor_member: None, parent_class_name: Some(enum_name.to_string()), constant_value: Some(ReflectionConstantValue::EnumCase { @@ -1374,6 +1403,7 @@ fn reflection_enum_case_metadata( constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + property_hook_members: Vec::new(), constructor_member: None, parent_class_name: Some(enum_name.to_string()), constant_value: Some(ReflectionConstantValue::EnumCase { @@ -1432,6 +1462,7 @@ fn reflection_class_constant_owner_metadata( constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + property_hook_members: Vec::new(), constructor_member: None, parent_class_name: Some(metadata.declaring_class_name), constant_value: Some(metadata.value), @@ -2286,6 +2317,7 @@ fn push_unique_constant_reflection_member( modifiers: reflection_class_constant_modifiers(&visibility, is_final), type_metadata: None, default_value: None, + property_hook_members: Vec::new(), required_parameter_count: 0, is_deprecated: false, is_generator: false, @@ -2636,6 +2668,7 @@ fn reflection_class_method_member( modifiers: reflection_method_modifiers_from_flags(flags), type_metadata, default_value: None, + property_hook_members: Vec::new(), required_parameter_count, is_deprecated: sig.deprecation.is_some(), is_generator, @@ -2718,6 +2751,7 @@ fn reflection_interface_method_member( modifiers: reflection_method_modifiers_from_flags(flags), type_metadata, default_value: None, + property_hook_members: Vec::new(), required_parameter_count, is_deprecated: sig.deprecation.is_some(), is_generator: false, @@ -2796,6 +2830,7 @@ fn reflection_trait_method_member( modifiers: reflection_method_modifiers_from_flags(flags), type_metadata, default_value: None, + property_hook_members: Vec::new(), required_parameter_count, is_deprecated: info.signature.deprecation.is_some(), is_generator, @@ -2850,13 +2885,21 @@ fn reflection_class_property_member( })?; let type_metadata = reflection_property_type_metadata(info, property_name); let default_value = reflection_property_default_value(info, property_name); + let declaring_class_name = reflection_property_declaring_class_name(info, property_name) + .or_else(|| { + (is_reflection_enum(ctx, class_name) && property_name == "name") + .then(|| class_name.to_string()) + }); + let property_hook_members = reflection_property_hook_members( + info, + property_name, + declaring_class_name.as_deref(), + flags, + type_metadata.as_ref(), + ); Some(ReflectionListedMember { name: property_name.to_string(), - declaring_class_name: reflection_property_declaring_class_name(info, property_name) - .or_else(|| { - (is_reflection_enum(ctx, class_name) && property_name == "name") - .then(|| class_name.to_string()) - }), + declaring_class_name, attr_names: info .property_attribute_names .get(property_name) @@ -2874,6 +2917,7 @@ fn reflection_class_property_member( .unwrap_or_else(|| reflection_property_modifiers_from_flags(flags)), type_metadata, default_value, + property_hook_members, required_parameter_count: 0, is_deprecated: false, is_generator: false, @@ -2901,6 +2945,153 @@ fn reflection_property_type_metadata( reflection_parameter_type_metadata(None, property_type) } +/// Builds concrete or abstract property-hook ReflectionMethod metadata for one property. +fn reflection_property_hook_members( + info: &crate::types::ClassInfo, + property_name: &str, + declaring_class_name: Option<&str>, + property_flags: ReflectionMemberFlags, + property_type_metadata: Option<&ReflectionParameterTypeMetadata>, +) -> Vec<(String, ReflectionListedMember)> { + let mut members = Vec::new(); + let declaring_class_name = declaring_class_name.map(str::to_string).or_else(|| { + info.abstract_property_hooks + .get(property_name) + .map(|contract| contract.declaring_type.clone()) + }); + let has_concrete_get = info + .methods + .contains_key(&php_symbol_key(&property_hook_get_method(property_name))); + let has_concrete_set = info + .methods + .contains_key(&php_symbol_key(&property_hook_set_method(property_name))); + let contract = info.abstract_property_hooks.get(property_name); + if has_concrete_get || contract.and_then(|contract| contract.get_type.as_ref()).is_some() { + let return_type = contract + .and_then(|contract| contract.get_type.as_ref()) + .and_then(|ty| reflection_parameter_type_metadata(None, ty)) + .or_else(|| property_type_metadata.cloned()); + members.push(( + String::from("get"), + reflection_property_hook_method_member( + property_name, + "get", + declaring_class_name.clone(), + property_flags, + !has_concrete_get, + return_type, + None, + ), + )); + } + if has_concrete_set || contract.and_then(|contract| contract.set_type.as_ref()).is_some() { + let parameter_type = contract + .and_then(|contract| contract.set_type.as_ref()) + .and_then(|ty| reflection_parameter_type_metadata(None, ty)) + .or_else(|| property_type_metadata.cloned()); + members.push(( + String::from("set"), + reflection_property_hook_method_member( + property_name, + "set", + declaring_class_name, + property_flags, + !has_concrete_set, + Some(ReflectionParameterTypeMetadata::Named( + reflection_builtin_named_type("void", false), + )), + parameter_type, + ), + )); + } + members +} + +/// Builds one ReflectionMethod metadata record for a property hook. +fn reflection_property_hook_method_member( + property_name: &str, + hook_name: &str, + declaring_class_name: Option, + property_flags: ReflectionMemberFlags, + is_abstract: bool, + return_type: Option, + parameter_type: Option, +) -> ReflectionListedMember { + let visibility = reflection_visibility_from_member_flags(property_flags); + let flags = reflection_member_flags(false, &visibility, false, is_abstract, false, false); + let name = format!("${property_name}::{hook_name}"); + let required_parameter_count = i64::from(hook_name == "set"); + let declaring_function = ReflectionDeclaringFunctionMember::Method { + name: name.clone(), + declaring_class_name: declaring_class_name.clone(), + attr_names: Vec::new(), + attr_args: Vec::new(), + flags, + required_parameter_count, + type_metadata: return_type.clone(), + is_deprecated: false, + is_generator: false, + }; + let parameters = if hook_name == "set" { + vec![reflection_property_hook_parameter_member( + declaring_class_name.clone(), + declaring_function.clone(), + parameter_type, + )] + } else { + Vec::new() + }; + ReflectionListedMember { + name, + declaring_class_name, + attr_names: Vec::new(), + attr_args: Vec::new(), + constant_value: None, + is_enum_case: false, + flags, + modifiers: reflection_method_modifiers_from_flags(flags), + type_metadata: return_type, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count, + is_deprecated: false, + is_generator: false, + prototype_member: None, + parameters, + } +} + +/// Builds the synthetic `value` parameter exposed by set-hook ReflectionMethod objects. +fn reflection_property_hook_parameter_member( + declaring_class_name: Option, + declaring_function: ReflectionDeclaringFunctionMember, + type_metadata: Option, +) -> ReflectionParameterMember { + let has_type = type_metadata.is_some(); + let allows_null = type_metadata.as_ref().is_some_and(reflection_type_allows_null); + let is_array_type = reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); + ReflectionParameterMember { + name: String::from("value"), + declaring_class_name, + declaring_function: Some(declaring_function), + attr_names: Vec::new(), + attr_args: Vec::new(), + position: 0, + is_optional: false, + is_variadic: false, + is_passed_by_reference: false, + is_promoted: false, + has_type, + allows_null, + is_array_type, + is_callable_type, + type_metadata, + default_value: None, + default_value_constant_name: None, + } +} + /// Returns supported default metadata for one reflected property. fn reflection_property_default_value( info: &crate::types::ClassInfo, @@ -3062,6 +3253,7 @@ fn default_method_members( )), type_metadata: None, default_value: None, + property_hook_members: Vec::new(), required_parameter_count: 0, is_deprecated: false, is_generator: false, @@ -3105,6 +3297,7 @@ fn default_property_members( ), type_metadata: None, default_value: None, + property_hook_members: Vec::new(), required_parameter_count: 0, is_deprecated: false, is_generator: false, @@ -3969,6 +4162,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { constant_reflection_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), + property_hook_members: Vec::new(), constructor_member: None, parent_class_name: None, constant_value: None, @@ -4428,6 +4622,46 @@ fn emit_reflection_member_array_property_by_name( Ok(()) } +/// Replaces a ReflectionProperty private slot with string-keyed hook ReflectionMethod objects. +fn emit_reflection_property_hook_array_property_by_name( + ctx: &mut FunctionContext<'_>, + owner_class_name: &str, + property_name: &str, + members: &[(String, ReflectionListedMember)], +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(owner_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let low_offset = reflection_property_offset(class_info, property_name)?; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_reg, 0); + abi::emit_load_from_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_call_label(ctx.emitter, "__rt_decref_array"); + emit_reflection_property_hook_array(ctx, members)?; + let assoc_type = reflection_property_hook_map_type(); + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_load_int_immediate( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + runtime_value_tag(&assoc_type) as i64, + ); + abi::emit_store_to_address( + ctx.emitter, + abi::secondary_scratch_reg(ctx.emitter), + object_reg, + high_offset, + ); + abi::emit_push_reg(ctx.emitter, object_reg); + abi::emit_pop_reg(ctx.emitter, result_reg); + Ok(()) +} + /// Replaces the ReflectionClass private constructor slot with `ReflectionMethod|null`. fn emit_reflection_constructor_property( ctx: &mut FunctionContext<'_>, @@ -4622,6 +4856,23 @@ fn emit_reflection_member_array( Ok(()) } +/// Allocates a string-keyed hook map with populated ReflectionMethod objects. +fn emit_reflection_property_hook_array( + ctx: &mut FunctionContext<'_>, + members: &[(String, ReflectionListedMember)], +) -> Result<()> { + emit_empty_assoc_array_literal_to_result( + ctx, + &PhpType::Object("ReflectionMethod".to_string()), + ); + for (key, member) in members { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_member_object(ctx, "ReflectionMethod", member)?; + emit_reflection_method_hash_insert(ctx, key); + } + Ok(()) +} + /// Allocates an indexed array of populated ReflectionParameter objects. fn emit_reflection_parameter_array( ctx: &mut FunctionContext<'_>, @@ -4703,6 +4954,47 @@ fn emit_reflection_class_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { } } +/// Inserts the current ReflectionMethod object into the stacked associative array. +fn emit_reflection_method_hash_insert(ctx: &mut FunctionContext<'_>, key: &str) { + let (key_label, key_len) = ctx.data.add_string(key.as_bytes()); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x3, x0"); // pass the ReflectionMethod object as the hook hash payload + ctx.emitter.instruction("mov x4, xzr"); // object hook hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "x0"); + abi::emit_symbol_address(ctx.emitter, "x1", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "x2", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + runtime_value_tag(&PhpType::Object("ReflectionMethod".to_string())) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rcx, rax"); // pass the ReflectionMethod object as the hook hash payload + ctx.emitter.instruction("xor r8, r8"); // object hook hash payloads do not use the high word + abi::emit_pop_reg(ctx.emitter, "rdi"); + abi::emit_symbol_address(ctx.emitter, "rsi", &key_label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", key_len as i64); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + runtime_value_tag(&PhpType::Object("ReflectionMethod".to_string())) as i64, + ); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + } + } +} + +/// Returns the associative map type used by `ReflectionProperty::getHooks()`. +fn reflection_property_hook_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Object("ReflectionMethod".to_string())), + } +} + /// Replaces a ReflectionClass private slot with a string-keyed string-value map. fn emit_reflection_string_assoc_property_by_name( ctx: &mut FunctionContext<'_>, @@ -5252,6 +5544,20 @@ fn emit_reflection_member_object( member_class_name, member.default_value.as_ref(), )?; + emit_reflection_owner_bool_property( + ctx, + member_class_name, + "__has_hooks", + !member.property_hook_members.is_empty(), + )?; + if !member.property_hook_members.is_empty() { + emit_reflection_property_hook_array_property_by_name( + ctx, + member_class_name, + "__hooks", + &member.property_hook_members, + )?; + } let property_string = reflection_property_to_string( &member.name, member.flags, @@ -6201,13 +6507,7 @@ fn reflection_property_modifiers( /// Computes PHP's `ReflectionProperty::getModifiers()` bitmask from predicate flags. fn reflection_property_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 { - let visibility = if flags.is_private { - Visibility::Private - } else if flags.is_protected { - Visibility::Protected - } else { - Visibility::Public - }; + let visibility = reflection_visibility_from_member_flags(flags); reflection_property_modifiers( &visibility, flags.is_static, @@ -6219,6 +6519,17 @@ fn reflection_property_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 ) } +/// Converts retained member visibility flags back into a `Visibility` value. +fn reflection_visibility_from_member_flags(flags: ReflectionMemberFlags) -> Visibility { + if flags.is_private { + Visibility::Private + } else if flags.is_protected { + Visibility::Protected + } else { + Visibility::Public + } +} + /// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from method flags. fn reflection_method_modifiers_from_flags(flags: ReflectionMemberFlags) -> i64 { let mut modifiers = 0; diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 2b64d9e04a..cd3ea2c33f 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -299,6 +299,14 @@ fn reflection_static_properties_map_type() -> PhpType { } } +/// Returns `array` for property-hook reflection maps. +fn reflection_property_hook_map_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Str), + value: Box::new(PhpType::Object("ReflectionMethod".to_string())), + } +} + /// Returns a nullable object type expression for one synthetic reflection class. fn nullable_object_type(class_name: &str) -> TypeExpr { TypeExpr::Nullable(Box::new(TypeExpr::Named(Name::unqualified(class_name)))) @@ -3715,6 +3723,18 @@ fn add_reflection_member_flag_methods( Some(bool_type()), false_bool(), )); + properties.push(builtin_property( + "__has_hooks", + Visibility::Private, + Some(bool_type()), + false_bool(), + )); + properties.push(builtin_property( + "__hooks", + Visibility::Private, + Some(array_type()), + empty_array(), + )); properties.push(builtin_property( "__default_value", Visibility::Private, @@ -3753,6 +3773,15 @@ fn add_reflection_member_flag_methods( "isDynamic", "__is_dynamic", )); + methods.push(builtin_reflection_class_bool_method( + "hasHooks", + "__has_hooks", + )); + methods.push(builtin_reflection_class_array_method( + "getHooks", + "__hooks", + array_type(), + )); properties.push(builtin_property( "__string", Visibility::Private, @@ -4203,6 +4232,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionProperty" { + for (property_name, property_type) in &mut class_info.properties { + if property_name == "__hooks" { + *property_type = reflection_property_hook_map_type(); + } + } for method_name in [ "isfinal", "isabstract", @@ -4211,6 +4245,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ispromoted", "isvirtual", "isdynamic", + "hashooks", "isinitialized", "isprotectedset", "isprivateset", @@ -4236,6 +4271,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Mixed; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getHooks")) { + sig.return_type = reflection_property_hook_map_type(); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index e3fbd472aa..64d5c9c19a 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1357,7 +1357,7 @@ echo (new ReflectionClass(ReflectionClass::class))->isCloneable() ? "C" : "c"; /// report PHP Traversable-compatible class metadata. #[test] fn test_reflection_class_is_iterable() { - let out = compile_and_run( + let out = compile_and_run_with_heap_size( r#"isIterable() ? "S" : "s"; echo (new ReflectionClass(ReflectIterableEnum::class))->isIterable() ? "E" : "e"; echo (new ReflectionClass(ReflectIterableTrait::class))->isIterable() ? "H" : "h"; "#, + 67_108_864, ); assert_eq!(out, "pIAGbftRseh"); } diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index a0ae31a414..1752f02b4f 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -438,3 +438,43 @@ echo (new ReflectionClass(ReflectPropertyStringTarget::class))->getProperty("lab "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public int|string $union ]|Property [ protected static string $label = 'ok' ]" ); } + +/// Verifies AOT `ReflectionProperty::hasHooks()` and `getHooks()` expose +/// concrete property hook metadata as ReflectionMethod objects. +#[test] +fn test_reflection_property_get_hooks_formats_aot_hook_metadata() { + let out = compile_and_run( + r#"raw * 2; } + set { $this->raw = $value; } + } + public int $readonlyHook { + get => $this->raw + 1; + } + public int $plain = 5; +} + +$hooked = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "doubled"); +$readonly = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "readonlyHook"); +$plain = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "plain"); + +echo ($hooked->hasHooks() ? "H" : "h") . ":"; +$hooks = $hooked->getHooks(); +echo count($hooks) . ":" . $hooks["get"]->getName() . ":" . $hooks["set"]->getName() . ":"; +echo $hooks["get"]->getDeclaringClass()->getName() . ":"; +echo $hooks["get"]->getNumberOfParameters() . ":"; +echo $hooks["set"]->getNumberOfParameters() . ":" . $hooks["set"]->getParameters()[0]->getName() . ":"; +echo ($readonly->hasHooks() ? "R" : "r") . ":"; +$readonlyHooks = $readonly->getHooks(); +echo count($readonlyHooks) . ":" . $readonlyHooks["get"]->getName() . ":"; +echo ($plain->hasHooks() ? "bad" : "plain") . ":" . count($plain->getHooks()); +"#, + ); + assert_eq!( + out, + "H:2:$doubled::get:$doubled::set:ReflectPropertyHookMetadataTarget:0:1:value:R:1:$readonlyHook::get:plain:0" + ); +} From ab79393ea35c9b3acf02d06e07bf530abdd7e8e5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 16:32:19 +0200 Subject: [PATCH 0562/1208] Support AOT ReflectionProperty hook lookups --- docs/php/classes.md | 6 +- src/types/checker/builtin_enums.rs | 28 ++++- src/types/checker/builtin_types/reflection.rs | 118 ++++++++++++++++++ tests/codegen/oop/reflection_properties.rs | 16 ++- 4 files changed, 164 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 2695bfa005..88a3614fd1 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1268,6 +1268,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::__toString()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Format retained generated/eval property metadata as a PHP-style `Property [ ... ]` descriptor | | `ReflectionProperty::hasHooks()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has retained get/set hook metadata | | `ReflectionProperty::getHooks()` | Same as `ReflectionProperty::hasHooks()` | Return a string-keyed map of retained concrete generated/AOT property hook `ReflectionMethod` objects | +| `ReflectionProperty::hasHook(PropertyHookType $type)` | Same as `ReflectionProperty::hasHooks()` plus `PropertyHookType::Get` / `PropertyHookType::Set` | Return whether the reflected property exposes the requested get/set hook | +| `ReflectionProperty::getHook(PropertyHookType $type)` | Same as `ReflectionProperty::hasHook()` | Return the requested hook `ReflectionMethod`, or `null` when that hook is not present | | `ReflectionProperty::hasType()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return whether the reflected property has a retained declared type | | `ReflectionProperty::getType()` | Same as `ReflectionProperty::hasType()` | Return a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType` for supported property types, or `null` when no type is declared | | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | @@ -1364,8 +1366,8 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. -- `ReflectionProperty::hasHooks()` and `getHooks()` expose retained concrete generated/AOT property get/set hooks as string-keyed `ReflectionMethod` objects; `hasHook()` and `getHook()` still require eval-backed `PropertyHookType` support. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. diff --git a/src/types/checker/builtin_enums.rs b/src/types/checker/builtin_enums.rs index b71f36a7b8..6617aa0392 100644 --- a/src/types/checker/builtin_enums.rs +++ b/src/types/checker/builtin_enums.rs @@ -11,12 +11,13 @@ use crate::errors::CompileError; use crate::names::php_symbol_key; use crate::parser::ast::{Program, Stmt, StmtKind}; -use crate::types::EnumCaseInfo; +use crate::types::{EnumCaseInfo, EnumCaseValue, PhpType}; use super::schema::insert_enum_metadata; use super::Checker; const SORT_DIRECTION: &str = "SortDirection"; +const PROPERTY_HOOK_TYPE: &str = "PropertyHookType"; /// Injects all builtin enum declarations into the checker. /// @@ -50,6 +51,31 @@ pub(crate) fn inject_builtin_enums( &[], checker, next_class_id, + )?; + + ensure_builtin_enum_name_available(program, checker, PROPERTY_HOOK_TYPE)?; + insert_enum_metadata( + PROPERTY_HOOK_TYPE, + Some(PhpType::Str), + vec![ + EnumCaseInfo { + name: "Get".to_string(), + value: Some(EnumCaseValue::Str("get".to_string())), + attribute_names: Vec::new(), + attribute_args: Vec::new(), + }, + EnumCaseInfo { + name: "Set".to_string(), + value: Some(EnumCaseValue::Str("set".to_string())), + attribute_names: Vec::new(), + attribute_args: Vec::new(), + }, + ], + &[], + &[], + &[], + checker, + next_class_id, ) } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index cd3ea2c33f..75a6f1c05c 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2960,6 +2960,95 @@ fn builtin_reflection_property_modifier_mask_method(method_name: &str, mask: i64 } } +/// Returns `ReflectionProperty::hasHook()` backed by the retained hook method map. +fn builtin_reflection_property_has_hook_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let hook_kind = reflection_property_hook_type_value(dummy_span); + let has_hook = function_call( + "array_key_exists", + vec![ + hook_kind, + reflection_this_property("__hooks", dummy_span), + ], + dummy_span, + ); + ClassMethod { + name: "hasHook".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("type".to_string(), Some(mixed_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(bool_type()), + body: vec![Stmt::new(StmtKind::Return(Some(has_hook)), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Returns `ReflectionProperty::getHook()` backed by the retained hook method map. +fn builtin_reflection_property_get_hook_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let hook_kind = reflection_property_hook_type_value(dummy_span); + let hooks = reflection_this_property("__hooks", dummy_span); + let hook_method = Expr::new( + ExprKind::ArrayAccess { + array: Box::new(hooks.clone()), + index: Box::new(hook_kind.clone()), + }, + dummy_span, + ); + let has_hook = function_call("array_key_exists", vec![hook_kind, hooks], dummy_span); + ClassMethod { + name: "getHook".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("type".to_string(), Some(mixed_type()), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(nullable_object_type("ReflectionMethod")), + body: vec![ + Stmt::new( + StmtKind::If { + condition: has_hook, + then_body: vec![Stmt::new( + StmtKind::Return(Some(hook_method)), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new( + StmtKind::Return(Some(null_value(dummy_span))), + dummy_span, + ), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `$type->value` for `PropertyHookType` arguments accepted by hook APIs. +fn reflection_property_hook_type_value(span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::PropertyAccess { + object: Box::new(variable_expr("type", span)), + property: "value".to_string(), + }, + span, + ) +} + /// Returns `ReflectionProperty::getValue()` for dynamic public instance reflectors. fn builtin_reflection_property_get_value_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -3782,6 +3871,8 @@ fn add_reflection_member_flag_methods( "__hooks", array_type(), )); + methods.push(builtin_reflection_property_has_hook_method()); + methods.push(builtin_reflection_property_get_hook_method()); properties.push(builtin_property( "__string", Visibility::Private, @@ -4274,6 +4365,33 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getHooks")) { sig.return_type = reflection_property_hook_map_type(); } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("hasHook")) { + sig.params = vec![( + "type".to_string(), + PhpType::Object("PropertyHookType".to_string()), + )]; + sig.param_type_exprs = + vec![Some(TypeExpr::Named(Name::unqualified("PropertyHookType")))]; + sig.defaults = vec![None]; + sig.ref_params = vec![false]; + sig.declared_params = vec![true]; + sig.return_type = PhpType::Bool; + } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getHook")) { + sig.params = vec![( + "type".to_string(), + PhpType::Object("PropertyHookType".to_string()), + )]; + sig.param_type_exprs = + vec![Some(TypeExpr::Named(Name::unqualified("PropertyHookType")))]; + sig.defaults = vec![None]; + sig.ref_params = vec![false]; + sig.declared_params = vec![true]; + sig.return_type = PhpType::Union(vec![ + PhpType::Object("ReflectionMethod".to_string()), + PhpType::Void, + ]); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getModifiers")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 1752f02b4f..516305b447 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -460,21 +460,35 @@ class ReflectPropertyHookMetadataTarget { $hooked = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "doubled"); $readonly = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "readonlyHook"); $plain = new ReflectionProperty(ReflectPropertyHookMetadataTarget::class, "plain"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +$caseList = PropertyHookType::cases(); +echo count($caseList) . ":" . $caseList[0]->value . ":"; +echo PropertyHookType::from("set")->name . ":"; +echo (PropertyHookType::tryFrom("missing") === null ? "T" : "t") . ":"; echo ($hooked->hasHooks() ? "H" : "h") . ":"; $hooks = $hooked->getHooks(); echo count($hooks) . ":" . $hooks["get"]->getName() . ":" . $hooks["set"]->getName() . ":"; echo $hooks["get"]->getDeclaringClass()->getName() . ":"; echo $hooks["get"]->getNumberOfParameters() . ":"; echo $hooks["set"]->getNumberOfParameters() . ":" . $hooks["set"]->getParameters()[0]->getName() . ":"; +echo ($hooked->hasHook($getCase) ? "G" : "g") . ":"; +echo ($hooked->hasHook(type: $setCase) ? "S" : "s") . ":"; +$get = $hooked->getHook($getCase); +$set = $hooked->getHook(type: $setCase); +echo $get->getName() . ":" . $set->getName() . ":"; echo ($readonly->hasHooks() ? "R" : "r") . ":"; $readonlyHooks = $readonly->getHooks(); echo count($readonlyHooks) . ":" . $readonlyHooks["get"]->getName() . ":"; +echo ($readonly->hasHook($getCase) ? "RG" : "rg") . ":"; +echo ($readonly->hasHook($setCase) ? "bad" : "RS") . ":"; +echo ($readonly->getHook($setCase) === null ? "N" : "n") . ":"; echo ($plain->hasHooks() ? "bad" : "plain") . ":" . count($plain->getHooks()); "#, ); assert_eq!( out, - "H:2:$doubled::get:$doubled::set:ReflectPropertyHookMetadataTarget:0:1:value:R:1:$readonlyHook::get:plain:0" + "2:get:Set:T:H:2:$doubled::get:$doubled::set:ReflectPropertyHookMetadataTarget:0:1:value:G:S:$doubled::get:$doubled::set:R:1:$readonlyHook::get:RG:RS:N:plain:0" ); } From 8feb50c16fe3930524a1464b0dee7c526bb0ace2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 16:56:26 +0200 Subject: [PATCH 0563/1208] Support ReflectionParameter object default args --- docs/php/classes.md | 4 +- docs/php/eval.md | 4 +- src/codegen/lower_inst/objects.rs | 11 -- src/codegen/lower_inst/objects/reflection.rs | 67 ++++++++- src/ir_lower/program.rs | 3 - src/types/checker/builtin_types/reflection.rs | 140 +++++++++++++----- tests/codegen/oop/attributes.rs | 18 ++- 7 files changed, 187 insertions(+), 60 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 88a3614fd1..690e5ef7f4 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1241,7 +1241,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | -| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and zero-argument object default values, or throw `ReflectionException` when no default is available | +| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and object default values with up to three explicit supported literal constructor arguments, or throw `ReflectionException` when no default is available | | `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | @@ -1366,7 +1366,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/array/zero-argument-object default metadata. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults with constructor arguments, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three explicit supported literal constructor arguments. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited explicit constructor-argument slice, constructor-default normalization inside object parameter defaults, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/docs/php/eval.md b/docs/php/eval.md index f55b4f53dc..3695b3ae09 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -629,8 +629,8 @@ ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/Intersect and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader parameter and generated property default-value materialization beyond the -eval-supported constant-expression subset, object-valued defaults in generated -metadata, and broader generated/AOT method bridge signatures beyond the current public +eval-supported constant-expression subset, object-valued generated defaults +beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type metadata and generated/AOT method/property attributes are exposed for registered diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index bc6c043f18..f390cbb2a7 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1449,17 +1449,6 @@ fn supported_dynamic_new_builtin_class_names() -> &'static [&'static str] { "RangeException", "RecursiveCallbackFilterIterator", "ReflectionException", - "ReflectionClass", - "ReflectionClassConstant", - "ReflectionEnumBackedCase", - "ReflectionEnumUnitCase", - "ReflectionFunction", - "ReflectionMethod", - "ReflectionNamedType", - "ReflectionParameter", - "ReflectionProperty", - "ReflectionUnionType", - "ReflectionIntersectionType", "RuntimeException", "SplDoublyLinkedList", "SplFixedArray", diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 333fbe48d2..0be7c9861f 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -198,6 +198,7 @@ enum ReflectionParameterDefaultValue { Null, Object { class_name: String, + args: Vec, }, Array(Vec), AssocArray(Vec), @@ -3468,6 +3469,9 @@ fn reflection_parameter_default_value( current_info: Option<&crate::types::ClassInfo>, default: &Expr, ) -> Result> { + if let Some(value) = reflection_object_parameter_default_value(default) { + return Ok(Some(value)); + } if let Some(value) = reflection_literal_parameter_default_value(default) { return Ok(Some(value)); } @@ -3480,6 +3484,55 @@ fn reflection_parameter_default_value( } } +/// Converts a top-level object parameter default into Reflection metadata. +fn reflection_object_parameter_default_value( + default: &Expr, +) -> Option { + let ExprKind::NewObject { class_name, args } = &default.kind else { + return None; + }; + if args.len() > 3 { + return None; + } + let args = args + .iter() + .map(reflection_literal_parameter_default_non_object_value) + .collect::>>()?; + Some(ReflectionParameterDefaultValue::Object { + class_name: class_name.as_str().to_string(), + args, + }) +} + +/// Converts constructor arguments for object defaults, rejecting nested objects. +fn reflection_literal_parameter_default_non_object_value( + default: &Expr, +) -> Option { + let value = reflection_literal_parameter_default_value(default)?; + if reflection_default_value_contains_object(&value) { + return None; + } + Some(value) +} + +/// Returns whether a retained default contains an object value. +fn reflection_default_value_contains_object(value: &ReflectionParameterDefaultValue) -> bool { + match value { + ReflectionParameterDefaultValue::Object { .. } => true, + ReflectionParameterDefaultValue::Array(elements) => elements + .iter() + .any(reflection_default_value_contains_object), + ReflectionParameterDefaultValue::AssocArray(entries) => entries + .iter() + .any(|entry| reflection_default_value_contains_object(&entry.value)), + ReflectionParameterDefaultValue::Int(_) + | ReflectionParameterDefaultValue::Bool(_) + | ReflectionParameterDefaultValue::Float(_) + | ReflectionParameterDefaultValue::Str(_) + | ReflectionParameterDefaultValue::Null => false, + } +} + /// Converts a literal parameter/property default expression into Reflection metadata. fn reflection_literal_parameter_default_value( default: &Expr, @@ -3490,11 +3543,6 @@ fn reflection_literal_parameter_default_value( ExprKind::FloatLiteral(value) => Some(ReflectionParameterDefaultValue::Float(*value)), ExprKind::StringLiteral(value) => Some(ReflectionParameterDefaultValue::Str(value.clone())), ExprKind::Null => Some(ReflectionParameterDefaultValue::Null), - ExprKind::NewObject { class_name, args } if args.is_empty() => { - Some(ReflectionParameterDefaultValue::Object { - class_name: class_name.as_str().to_string(), - }) - } ExprKind::ArrayLiteral(items) => items .iter() .map(reflection_literal_parameter_default_value) @@ -5190,7 +5238,12 @@ fn emit_reflection_default_value_as_mixed( emit_boxed_string_literal_default_to_result(ctx, value) } ReflectionParameterDefaultValue::Null => emit_boxed_null_literal_to_result(ctx), - ReflectionParameterDefaultValue::Object { .. } => emit_boxed_null_literal_to_result(ctx), + ReflectionParameterDefaultValue::Object { args, .. } if args.is_empty() => { + emit_boxed_null_literal_to_result(ctx) + } + ReflectionParameterDefaultValue::Object { args, .. } => { + emit_reflection_indexed_array_default_as_mixed(ctx, args) + } ReflectionParameterDefaultValue::Array(elements) => { emit_reflection_indexed_array_default_as_mixed(ctx, elements) } @@ -5736,7 +5789,7 @@ fn reflection_parameter_default_object_class( default_value: Option<&ReflectionParameterDefaultValue>, ) -> Option<&str> { match default_value { - Some(ReflectionParameterDefaultValue::Object { class_name }) => Some(class_name), + Some(ReflectionParameterDefaultValue::Object { class_name, .. }) => Some(class_name), _ => None, } } diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 7757e004e6..75841ebf6d 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -1178,9 +1178,6 @@ fn supported_dynamic_new_builtin_class_name(class_name: &str) -> bool { | "overflowexception" | "rangeexception" | "recursivecallbackfilteriterator" - | "reflectionclass" - | "reflectionmethod" - | "reflectionproperty" | "runtimeexception" | "spldoublylinkedlist" | "splfixedarray" diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 75a6f1c05c..dfdb3d0bab 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -917,6 +917,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { /// Builds `ReflectionParameter::getDefaultValue()` over the retained default slot. fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); + let object_default_body = reflection_parameter_get_default_object_body(dummy_span); ClassMethod { name: "getDefaultValue".to_string(), visibility: Visibility::Public, @@ -931,27 +932,7 @@ fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { return_type: Some(mixed_type()), by_ref_return: false, body: vec![ - Stmt::new( - StmtKind::If { - condition: Expr::new( - ExprKind::Not(Box::new(reflection_this_property( - "__has_default_value", - dummy_span, - ))), - dummy_span, - ), - then_body: vec![throw_new_reflection_exception( - string_lit( - "Internal error: Failed to retrieve the default value", - dummy_span, - ), - dummy_span, - )], - elseif_clauses: Vec::new(), - else_body: None, - }, - dummy_span, - ), + reflection_parameter_throw_if_default_missing(dummy_span), Stmt::new( StmtKind::If { condition: binary_expr( @@ -960,19 +941,7 @@ fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { string_lit("", dummy_span), dummy_span, ), - then_body: vec![Stmt::new( - StmtKind::Return(Some(Expr::new( - ExprKind::NewDynamic { - name_expr: Box::new(reflection_this_property( - "__default_value_object_class", - dummy_span, - )), - args: Vec::new(), - }, - dummy_span, - ))), - dummy_span, - )], + then_body: object_default_body, elseif_clauses: Vec::new(), else_body: None, }, @@ -991,6 +960,109 @@ fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { } } +/// Builds the object-default branch for `ReflectionParameter::getDefaultValue()`. +fn reflection_parameter_get_default_object_body(span: crate::span::Span) -> Vec { + let arg_count_var = "__default_value_arg_count"; + let mut body = vec![ + Stmt::new( + StmtKind::If { + condition: binary_expr( + reflection_this_property("__default_value", span), + BinOp::StrictEq, + null_value(span), + span, + ), + then_body: vec![reflection_parameter_return_dynamic_default_object( + Vec::new(), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ), + Stmt::new( + StmtKind::Assign { + name: arg_count_var.to_string(), + value: function_call( + "count", + vec![reflection_this_property("__default_value", span)], + span, + ), + }, + span, + ), + ]; + for arg_count in 1..=3 { + body.push(reflection_parameter_default_object_arg_count_branch( + arg_count, + arg_count_var, + span, + )); + } + body.push(throw_new_reflection_exception( + string_lit("Internal error: Failed to retrieve the default value", span), + span, + )); + body +} + +/// Builds one constructor-argument-count branch for object defaults. +fn reflection_parameter_default_object_arg_count_branch( + arg_count: usize, + arg_count_var: &str, + span: crate::span::Span, +) -> Stmt { + let args = (0..arg_count) + .map(|index| reflection_parameter_default_object_arg(index, span)) + .collect(); + Stmt::new( + StmtKind::If { + condition: binary_expr( + variable_expr(arg_count_var, span), + BinOp::StrictEq, + Expr::new(ExprKind::IntLiteral(arg_count as i64), span), + span, + ), + then_body: vec![reflection_parameter_return_dynamic_default_object(args, span)], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Builds a return statement that constructs the retained object default class. +fn reflection_parameter_return_dynamic_default_object( + args: Vec, + span: crate::span::Span, +) -> Stmt { + Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::NewDynamic { + name_expr: Box::new(reflection_this_property( + "__default_value_object_class", + span, + )), + args, + }, + span, + ))), + span, + ) +} + +/// Builds `$this->__default_value[$index]` for retained object-default args. +fn reflection_parameter_default_object_arg(index: usize, span: crate::span::Span) -> Expr { + Expr::new( + ExprKind::ArrayAccess { + array: Box::new(reflection_this_property("__default_value", span)), + index: Box::new(Expr::new(ExprKind::IntLiteral(index as i64), span)), + }, + span, + ) +} + /// Returns a public `ReflectionClass::newInstanceWithoutConstructor()` method. /// /// Eval dispatch supplies the real constructorless allocation. The body remains diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 64d5c9c19a..09fd9f1a85 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2930,9 +2930,19 @@ class ReflectObjectDefaultValue { $this->label = "ctor"; } } +class ReflectObjectDefaultValueArgs { + public mixed $label; + public mixed $extra; + public function __construct(mixed $label, mixed $extra) { + $this->label = $label; + $this->extra = $extra; + } +} function reflect_object_default(ReflectObjectDefaultValue $value = new ReflectObjectDefaultValue()) {} +function reflect_object_default_args(ReflectObjectDefaultValueArgs $value = new ReflectObjectDefaultValueArgs("arg", 42)) {} class ReflectObjectDefaultMethod { public function run(ReflectObjectDefaultValue $value = new ReflectObjectDefaultValue()) {} + public function withArgs(ReflectObjectDefaultValueArgs $value = new ReflectObjectDefaultValueArgs("method-arg", "M")) {} } $param = (new ReflectionFunction("reflect_object_default"))->getParameters()[0]; echo $param->isDefaultValueAvailable() ? "D:" : "d:"; @@ -2950,6 +2960,12 @@ $method = (new ReflectionMethod(ReflectObjectDefaultMethod::class, "run"))->getP echo $method instanceof ReflectObjectDefaultValue ? ":method:" . $method->label : ":method:bad"; $directMethod = (new ReflectionParameter([ReflectObjectDefaultMethod::class, "run"], "value"))->getDefaultValue(); echo $directMethod instanceof ReflectObjectDefaultValue ? ":direct-method:" . $directMethod->label : ":direct-method:bad"; +$args = (new ReflectionFunction("reflect_object_default_args"))->getParameters()[0]->getDefaultValue(); +echo $args instanceof ReflectObjectDefaultValueArgs ? ":args:" . $args->label . ":" . $args->extra : ":args:bad"; +$methodArgs = (new ReflectionMethod(ReflectObjectDefaultMethod::class, "withArgs"))->getParameters()[0]->getDefaultValue(); +echo $methodArgs instanceof ReflectObjectDefaultValueArgs ? ":method-args:" . $methodArgs->label . ":" . $methodArgs->extra : ":method-args:bad"; +$directMethodArgs = (new ReflectionParameter([ReflectObjectDefaultMethod::class, "withArgs"], "value"))->getDefaultValue(); +echo $directMethodArgs instanceof ReflectObjectDefaultValueArgs ? ":direct-method-args:" . $directMethodArgs->label . ":" . $directMethodArgs->extra : ":direct-method-args:bad"; "##, ); assert!( @@ -2959,7 +2975,7 @@ echo $directMethod instanceof ReflectObjectDefaultValue ? ":direct-method:" . $d ); assert_eq!( out.stdout, - "D:object:ctor:diff:direct:ctor:method:ctor:direct-method:ctor" + "D:object:ctor:diff:direct:ctor:method:ctor:direct-method:ctor:args:arg:42:method-args:method-arg:M:direct-method-args:method-arg:M" ); } From a5c9101fc345003bb0bc529c4db70c638d51f959 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 17:04:09 +0200 Subject: [PATCH 0564/1208] Expand ReflectionParameter object default constructor defaults --- docs/php/classes.md | 4 +- docs/php/eval.md | 2 +- src/codegen/lower_inst/objects/reflection.rs | 44 +++++++++++++++++--- tests/codegen/oop/attributes.rs | 8 ++-- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 690e5ef7f4..e9377830c3 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1241,7 +1241,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | -| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and object default values with up to three explicit supported literal constructor arguments, or throw `ReflectionException` when no default is available | +| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and object default values with up to three supported literal constructor arguments, including omitted constructor defaults, or throw `ReflectionException` when no default is available | | `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | @@ -1366,7 +1366,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three explicit supported literal constructor arguments. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited explicit constructor-argument slice, constructor-default normalization inside object parameter defaults, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported literal constructor arguments, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/docs/php/eval.md b/docs/php/eval.md index 3695b3ae09..3cdc3d9a30 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -630,7 +630,7 @@ and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader parameter and generated property default-value materialization beyond the eval-supported constant-expression subset, object-valued generated defaults -beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public +beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type metadata and generated/AOT method/property attributes are exposed for registered diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 0be7c9861f..626884df27 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -3469,7 +3469,7 @@ fn reflection_parameter_default_value( current_info: Option<&crate::types::ClassInfo>, default: &Expr, ) -> Result> { - if let Some(value) = reflection_object_parameter_default_value(default) { + if let Some(value) = reflection_object_parameter_default_value(ctx, default) { return Ok(Some(value)); } if let Some(value) = reflection_literal_parameter_default_value(default) { @@ -3486,22 +3486,54 @@ fn reflection_parameter_default_value( /// Converts a top-level object parameter default into Reflection metadata. fn reflection_object_parameter_default_value( + ctx: &FunctionContext<'_>, default: &Expr, ) -> Option { let ExprKind::NewObject { class_name, args } = &default.kind else { return None; }; + let args = reflection_object_parameter_default_args(ctx, class_name.as_str(), args)?; + Some(ReflectionParameterDefaultValue::Object { + class_name: class_name.as_str().to_string(), + args, + }) +} + +/// Returns constructor args for an object default, including supported omitted defaults. +fn reflection_object_parameter_default_args( + ctx: &FunctionContext<'_>, + class_name: &str, + args: &[Expr], +) -> Option> { if args.len() > 3 { return None; } - let args = args + let mut values = args .iter() .map(reflection_literal_parameter_default_non_object_value) .collect::>>()?; - Some(ReflectionParameterDefaultValue::Object { - class_name: class_name.as_str().to_string(), - args, - }) + let Some((_, class_info)) = resolve_reflection_class(ctx, class_name) else { + return Some(values); + }; + let constructor = class_info.methods.get(&php_symbol_key("__construct")); + let Some(constructor) = constructor else { + return if values.is_empty() { Some(values) } else { None }; + }; + if constructor.variadic.is_some() + || values.len() > constructor.params.len() + || constructor.params.len() > 3 + { + return None; + } + for default in constructor.defaults.iter().skip(values.len()) { + let default = default.as_ref()?; + values.push(reflection_literal_parameter_default_non_object_value(default)?); + } + if values.len() == constructor.params.len() { + Some(values) + } else { + None + } } /// Converts constructor arguments for object defaults, rejecting nested objects. diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 09fd9f1a85..0938a05789 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2925,9 +2925,11 @@ fn test_reflection_parameter_exposes_object_default_values() { let out = compile_and_run_capture( r##"label = "ctor"; + public mixed $label; + public mixed $extra; + public function __construct(mixed $label = "ctor", mixed $extra = null) { + $this->label = $label; + $this->extra = $extra; } } class ReflectObjectDefaultValueArgs { From 6654f70c4c3b212ab1c1af3118ac426d7871c2cf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 17:13:10 +0200 Subject: [PATCH 0565/1208] Support constant args in ReflectionParameter object defaults --- docs/php/classes.md | 5 +- docs/php/eval.md | 2 +- src/codegen/lower_inst/objects/reflection.rs | 92 ++++++++++++++++---- tests/codegen/oop/attributes.rs | 16 ++-- 4 files changed, 88 insertions(+), 27 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index e9377830c3..abcf0bf96c 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1241,7 +1241,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | -| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and object default values with up to three supported literal constructor arguments, including omitted constructor defaults, or throw `ReflectionException` when no default is available | +| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and object default values with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults, or throw `ReflectionException` when no default is available | | `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | @@ -1366,7 +1366,8 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported literal constructor arguments, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/docs/php/eval.md b/docs/php/eval.md index 3cdc3d9a30..56d434e8d7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -630,7 +630,7 @@ and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader parameter and generated property default-value materialization beyond the eval-supported constant-expression subset, object-valued generated defaults -beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public +beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type metadata and generated/AOT method/property attributes are exposed for registered diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 626884df27..6627b82f54 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -3469,7 +3469,9 @@ fn reflection_parameter_default_value( current_info: Option<&crate::types::ClassInfo>, default: &Expr, ) -> Result> { - if let Some(value) = reflection_object_parameter_default_value(ctx, default) { + if let Some(value) = + reflection_object_parameter_default_value(ctx, current_class, current_info, default)? + { return Ok(Some(value)); } if let Some(value) = reflection_literal_parameter_default_value(default) { @@ -3487,52 +3489,104 @@ fn reflection_parameter_default_value( /// Converts a top-level object parameter default into Reflection metadata. fn reflection_object_parameter_default_value( ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, default: &Expr, -) -> Option { +) -> Result> { let ExprKind::NewObject { class_name, args } = &default.kind else { - return None; + return Ok(None); + }; + let Some(args) = reflection_object_parameter_default_args( + ctx, + current_class, + current_info, + class_name.as_str(), + args, + )? + else { + return Ok(None); }; - let args = reflection_object_parameter_default_args(ctx, class_name.as_str(), args)?; - Some(ReflectionParameterDefaultValue::Object { + Ok(Some(ReflectionParameterDefaultValue::Object { class_name: class_name.as_str().to_string(), args, - }) + })) } /// Returns constructor args for an object default, including supported omitted defaults. fn reflection_object_parameter_default_args( ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, class_name: &str, args: &[Expr], -) -> Option> { +) -> Result>> { if args.len() > 3 { - return None; + return Ok(None); + } + let mut values = Vec::with_capacity(args.len()); + for arg in args { + let Some(value) = + reflection_parameter_default_non_object_value(ctx, current_class, current_info, arg)? + else { + return Ok(None); + }; + values.push(value); } - let mut values = args - .iter() - .map(reflection_literal_parameter_default_non_object_value) - .collect::>>()?; let Some((_, class_info)) = resolve_reflection_class(ctx, class_name) else { - return Some(values); + return Ok(Some(values)); }; let constructor = class_info.methods.get(&php_symbol_key("__construct")); let Some(constructor) = constructor else { - return if values.is_empty() { Some(values) } else { None }; + return if values.is_empty() { + Ok(Some(values)) + } else { + Ok(None) + }; }; if constructor.variadic.is_some() || values.len() > constructor.params.len() || constructor.params.len() > 3 { - return None; + return Ok(None); } for default in constructor.defaults.iter().skip(values.len()) { - let default = default.as_ref()?; - values.push(reflection_literal_parameter_default_non_object_value(default)?); + let Some(default) = default.as_ref() else { + return Ok(None); + }; + let Some(value) = reflection_parameter_default_non_object_value( + ctx, + class_name, + Some(class_info), + default, + )? + else { + return Ok(None); + }; + values.push(value); } if values.len() == constructor.params.len() { - Some(values) + Ok(Some(values)) } else { - None + Ok(None) + } +} + +/// Converts a supported non-object parameter default expression into metadata. +fn reflection_parameter_default_non_object_value( + ctx: &FunctionContext<'_>, + current_class: &str, + current_info: Option<&crate::types::ClassInfo>, + default: &Expr, +) -> Result> { + if let Some(value) = reflection_literal_parameter_default_non_object_value(default) { + return Ok(Some(value)); + } + match &default.kind { + ExprKind::ClassConstant { .. } | ExprKind::ScopedConstantAccess { .. } => { + let value = reflection_constant_value(ctx, current_class, current_info, default, 0)?; + Ok(reflection_parameter_default_from_constant_value(value)) + } + _ => Ok(None), } } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 0938a05789..18fee47eec 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2925,14 +2925,20 @@ fn test_reflection_parameter_exposes_object_default_values() { let out = compile_and_run_capture( r##"label = $label; $this->extra = $extra; } } class ReflectObjectDefaultValueArgs { + const ARG_LABEL = "arg"; + const ARG_EXTRA = 42; + const METHOD_LABEL = "method-arg"; + const METHOD_EXTRA = "M"; public mixed $label; public mixed $extra; public function __construct(mixed $label, mixed $extra) { @@ -2941,17 +2947,17 @@ class ReflectObjectDefaultValueArgs { } } function reflect_object_default(ReflectObjectDefaultValue $value = new ReflectObjectDefaultValue()) {} -function reflect_object_default_args(ReflectObjectDefaultValueArgs $value = new ReflectObjectDefaultValueArgs("arg", 42)) {} +function reflect_object_default_args(ReflectObjectDefaultValueArgs $value = new ReflectObjectDefaultValueArgs(ReflectObjectDefaultValueArgs::ARG_LABEL, ReflectObjectDefaultValueArgs::ARG_EXTRA)) {} class ReflectObjectDefaultMethod { public function run(ReflectObjectDefaultValue $value = new ReflectObjectDefaultValue()) {} - public function withArgs(ReflectObjectDefaultValueArgs $value = new ReflectObjectDefaultValueArgs("method-arg", "M")) {} + public function withArgs(ReflectObjectDefaultValueArgs $value = new ReflectObjectDefaultValueArgs(ReflectObjectDefaultValueArgs::METHOD_LABEL, ReflectObjectDefaultValueArgs::METHOD_EXTRA)) {} } $param = (new ReflectionFunction("reflect_object_default"))->getParameters()[0]; echo $param->isDefaultValueAvailable() ? "D:" : "d:"; $first = $param->getDefaultValue(); $second = $param->getDefaultValue(); if ($first instanceof ReflectObjectDefaultValue) { - echo "object:" . $first->label . ":"; + echo "object:" . $first->label . ":" . $first->extra . ":"; } else { echo "not-object:"; } @@ -2977,7 +2983,7 @@ echo $directMethodArgs instanceof ReflectObjectDefaultValueArgs ? ":direct-metho ); assert_eq!( out.stdout, - "D:object:ctor:diff:direct:ctor:method:ctor:direct-method:ctor:args:arg:42:method-args:method-arg:M:direct-method-args:method-arg:M" + "D:object:ctor:default-extra:diff:direct:ctor:method:ctor:direct-method:ctor:args:arg:42:method-args:method-arg:M:direct-method-args:method-arg:M" ); } From 7a27f791c87c6b06c25492c9e4f363bb2fac4e86 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 17:15:19 +0200 Subject: [PATCH 0566/1208] Clarify ReflectionParameter object default docs --- docs/php/classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index abcf0bf96c..caf3e014eb 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1419,4 +1419,4 @@ Class constants (PHP 7.1+ visibility, PHP 8.1+ `final`) live on classes, interfa - `readonly static` properties are rejected to match PHP. Static properties in a `readonly class` are still mutable. - Backed property hooks may read and write their own backing slot. - Class constants must be literal-or-foldable expressions; cyclic constant references are not supported. -- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, function and method parameter attributes, supported scalar/null/class-constant/array/zero-argument-object parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. +- Class and function attribute names and supported literal args are exposed at runtime through `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and the supported `ReflectionClass`/`ReflectionFunction`/`ReflectionMethod`/`ReflectionProperty`/`ReflectionClassConstant`/`ReflectionEnumUnitCase`/`ReflectionEnumBackedCase::getAttributes()` APIs; function and method parameter names, counts, positions, optional/variadic/by-reference flags, declared-type presence, simple named, union, and intersection type metadata, function and method parameter attributes, supported scalar/null/class-constant/array/object parameter defaults, parameter declaring-class/function metadata, and reflected member/constant declaring-class metadata are exposed through the supported Reflection APIs. `#[\Override]`, `#[\Deprecated]`, and `#[\AllowDynamicProperties]` are enforced/diagnosed/honored at compile time and runtime; `#[\SensitiveParameter]` is parsed but not yet propagated to stack traces. From 9007adf85baf0115d97df7ffed5ded6d59464f41 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 17:31:43 +0200 Subject: [PATCH 0567/1208] Expose builtin ReflectionFunction metadata --- docs/php/classes.md | 8 +- docs/php/eval.md | 5 +- src/codegen/lower_inst/objects/reflection.rs | 79 ++++++++++++++++++- .../checker/inference/objects/constructors.rs | 18 +++-- .../objects/constructors/reflection.rs | 10 ++- tests/codegen/oop/reflection_functions.rs | 24 ++++++ 6 files changed, 128 insertions(+), 16 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index caf3e014eb..80f9a1c133 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1190,11 +1190,11 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::newInstanceArgs()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class from an argument array | | `ReflectionClass::newInstanceWithoutConstructor()` | `new ReflectionClass($class_name)` | Allocate an instance of the reflected class without running `__construct()` | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | -| `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user function name | -| `ReflectionFunction::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionFunction($function_name)` | Return namespace-aware name metadata for the reflected user function | +| `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user or supported callable-builtin function name | +| `ReflectionFunction::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionFunction($function_name)` | Return namespace-aware name metadata for the reflected user or supported callable-builtin function | | `ReflectionFunction::isInternal()` / `isUserDefined()` | `new ReflectionFunction($function_name)` | Return origin predicates for supported reflected functions | | `ReflectionFunction::isClosure()` / `isDeprecated()` / `returnsReference()` / `isGenerator()` | `new ReflectionFunction($function_name)` | Return retained function predicates; AOT reflection reports `false` for closures and return-by-reference, uses `#[Deprecated]` metadata, and reports generator functions from lowered generator flags | -| `ReflectionFunction::hasTentativeReturnType()` / `getTentativeReturnType()` / `isDisabled()` | `new ReflectionFunction($function_name)` | Return PHP-compatible defaults for supported user functions: no tentative return type and not disabled | +| `ReflectionFunction::hasTentativeReturnType()` / `getTentativeReturnType()` / `isDisabled()` | `new ReflectionFunction($function_name)` | Return PHP-compatible defaults for supported functions: no tentative return type and not disabled | | `ReflectionFunction::getAttributes()` | `new ReflectionFunction($function_name)` | Return `ReflectionAttribute` objects for function attributes | | `ReflectionFunction::getParameters()` | `new ReflectionFunction($function_name)` | Return `ReflectionParameter` objects for the reflected function parameters | | `ReflectionFunction::getNumberOfParameters()` | `new ReflectionFunction($function_name)` | Return the total number of reflected function parameters | @@ -1366,7 +1366,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions, with AOT invocation limited to reflected functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, internal-function parameter metadata, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/docs/php/eval.md b/docs/php/eval.md index 56d434e8d7..873562ed22 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -339,7 +339,10 @@ reflected method is `__construct` or `__destruct`. `getNumberOfRequiredParameters()` report retained eval-declared function and method metadata, plus registered generated/AOT method parameter names, declared parameter and return types, required/optional counts, and scalar or null default -values when native method/static-method signatures are registered. Eval-declared +values when native method/static-method signatures are registered. Eval code can +also reflect supported callable-builtin signatures, including +internal origin, parameter names, parameter types, and return type metadata. +Eval-declared functions and methods expose declared-type presence for parameters and return types, simple named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 6627b82f54..4c333aca11 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -459,12 +459,13 @@ fn emit_reflection_owner_object( )?; } if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { - emit_reflection_owner_bool_property(ctx, class_name, "__is_internal", false)?; + let is_internal = reflection_function_or_method_is_internal(class_name, &metadata); + emit_reflection_owner_bool_property(ctx, class_name, "__is_internal", is_internal)?; emit_reflection_owner_bool_property( ctx, class_name, "__is_user_defined", - metadata.reflected_name.is_some(), + metadata.reflected_name.is_some() && !is_internal, )?; emit_reflection_parameter_array_property_by_name( ctx, @@ -946,6 +947,11 @@ fn reflection_function_metadata( }; let function_name = const_required_string_operand(ctx, function_operand, "ReflectionFunction")?; let Some(function) = ctx.function_by_name(&function_name) else { + if let Some((builtin_name, signature)) = + reflection_builtin_function_signature(&function_name) + { + return reflection_builtin_function_metadata(ctx, &builtin_name, &signature); + } return Ok(empty_reflection_metadata()); }; let Some(signature) = function.signature.as_ref() else { @@ -983,6 +989,64 @@ fn reflection_function_metadata( Ok(metadata) } +/// Builds metadata for a supported builtin `ReflectionFunction`. +fn reflection_builtin_function_metadata( + ctx: &FunctionContext<'_>, + function_name: &str, + signature: &FunctionSig, +) -> Result { + let required_parameter_count = reflection_required_parameter_count(signature); + let type_metadata = reflection_return_type_metadata(signature); + let declaring_function = ReflectionDeclaringFunctionMember::Function { + name: function_name.to_string(), + attr_names: Vec::new(), + attr_args: Vec::new(), + required_parameter_count, + type_metadata: type_metadata.clone(), + is_deprecated: false, + is_generator: false, + }; + let mut metadata = empty_reflection_metadata(); + metadata.reflected_name = Some(function_name.to_string()); + metadata.parameter_members = reflection_parameter_members_with_declaring_function( + ctx, + signature, + "", + None, + None, + Some(declaring_function), + &[], + )?; + metadata.required_parameter_count = required_parameter_count; + metadata.type_metadata = type_metadata; + Ok(metadata) +} + +/// Returns the canonical callable-builtin name and signature for ReflectionFunction. +fn reflection_builtin_function_signature(function_name: &str) -> Option<(String, FunctionSig)> { + let builtin_key = php_symbol_key(function_name.trim_start_matches('\\')); + crate::types::first_class_callable_builtin_sig(&builtin_key) + .map(|signature| (builtin_key, signature)) +} + +/// Returns whether a reflected function or method represents compiler builtin metadata. +fn reflection_function_or_method_is_internal( + class_name: &str, + metadata: &ReflectionOwnerMetadata, +) -> bool { + if class_name == "ReflectionFunction" { + return metadata + .reflected_name + .as_deref() + .and_then(reflection_builtin_function_signature) + .is_some(); + } + metadata + .parent_class_name + .as_deref() + .is_some_and(reflection_class_like_is_internal) +} + /// Resolves `ReflectionMethod(class, method)` metadata. fn reflection_method_metadata( ctx: &FunctionContext<'_>, @@ -1202,6 +1266,17 @@ fn reflection_function_parameter_metadata( const_required_string_operand(ctx, function_operand, "ReflectionParameter")?; let selector = const_parameter_selector_operand(ctx, parameter_operand)?; let Some(function) = ctx.function_by_name(&function_name) else { + if let Some((builtin_name, signature)) = + reflection_builtin_function_signature(&function_name) + { + let metadata = reflection_builtin_function_metadata(ctx, &builtin_name, &signature)?; + let Some(parameter) = + reflection_parameter_member_for_selector(&metadata.parameter_members, selector) + else { + return Ok(empty_reflection_metadata()); + }; + return Ok(reflection_parameter_owner_metadata(parameter)); + } return Ok(empty_reflection_metadata()); }; let Some(signature) = function.signature.as_ref() else { diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 39c3dc8783..d31da4f434 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -301,7 +301,7 @@ impl Checker { } } - /// Validates `new ReflectionFunction(function)` for a statically known user function. + /// Validates `new ReflectionFunction(function)` for supported static function metadata. fn validate_reflection_function_constructor( &mut self, args: &[Expr], @@ -331,9 +331,10 @@ impl Checker { /// Validates `new ReflectionParameter(target, param)`. /// - /// Supported targets are statically known user function names and - /// class/interface/trait method arrays. The parameter selector must be an - /// integer position or string name known at compile time. + /// Supported targets are statically known user function names, supported + /// callable-builtin function names, and class/interface/trait method arrays. + /// The parameter selector must be an integer position or string name known + /// at compile time. fn validate_reflection_parameter_constructor( &mut self, args: &[Expr], @@ -494,13 +495,18 @@ impl Checker { Ok(class_name) } - /// Returns the reflected signature for a statically declared user function. + /// Returns the reflected signature for a user function or supported callable builtin. fn reflection_function_signature( &mut self, function_name: &str, ) -> Result, CompileError> { + let lookup_name = function_name.trim_start_matches('\\'); + let builtin_key = php_symbol_key(lookup_name); + if let Some(sig) = crate::types::first_class_callable_builtin_sig(&builtin_key) { + return Ok(Some(sig)); + } let canonical = - match self.canonical_function_name_folded(function_name.trim_start_matches('\\')) { + match self.canonical_function_name_folded(lookup_name) { Some(canonical) => canonical, None => return Ok(None), }; diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index f9b1765228..3fe8028576 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -21,9 +21,9 @@ type ReflectionAttributeArgs = Vec>>; impl Checker { /// Validates function-level attributes for `ReflectionFunction`. /// - /// The reflected function must be a statically declared user function; when - /// it has attributes, their captured argument metadata must be materializable - /// by `ReflectionAttribute::getArguments()`. + /// The reflected function must be a statically declared user function or a + /// supported callable builtin; user-function attributes must have + /// materializable `ReflectionAttribute::getArguments()` metadata. pub(super) fn validate_reflection_function_attrs( &self, function_name: &str, @@ -32,6 +32,10 @@ impl Checker { let Some(canonical) = self.canonical_function_name_folded(function_name.trim_start_matches('\\')) else { + let builtin_key = php_symbol_key(function_name.trim_start_matches('\\')); + if crate::types::first_class_callable_builtin_sig(&builtin_key).is_some() { + return Ok(()); + } return Err(CompileError::new( expr.span, &format!( diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs index 42908d2f27..779d6987af 100644 --- a/tests/codegen/oop/reflection_functions.rs +++ b/tests/codegen/oop/reflection_functions.rs @@ -121,6 +121,30 @@ echo $ref->isUserDefined() ? "U" : "u"; assert_eq!(out, "ReflectFunctionMetaNs\\sample:sample:ReflectFunctionMetaNs:Y:i:U"); } +/// Verifies `ReflectionFunction` exposes supported callable-builtin metadata. +#[test] +fn test_reflection_function_reports_builtin_metadata() { + let out = compile_and_run( + r#"getName() . ":"; +echo $ref->getShortName() . ":"; +echo ($ref->isInternal() ? "I" : "i") . ":"; +echo ($ref->isUserDefined() ? "U" : "u") . ":"; +echo ($ref->hasReturnType() ? "T" : "t") . ":"; +echo $ref->getReturnType()->getName() . ":"; +$params = $ref->getParameters(); +echo count($params) . ":"; +echo $params[0]->getName() . ":"; +echo ($params[0]->hasType() ? "P" : "p") . ":"; +echo $params[0]->getType()->getName() . ":"; +echo ($params[0]->getDeclaringFunction()->isInternal() ? "D" : "d") . ":"; +echo (new ReflectionParameter("strlen", "string"))->getDeclaringFunction()->getName(); +"#, + ); + assert_eq!(out, "strlen:strlen:I:u:T:int:1:string:P:string:D:strlen"); +} + /// Verifies `ReflectionFunction::invoke()` calls declared AOT functions. #[test] fn test_reflection_function_invoke_calls_declared_aot_functions() { From 6281af85b97b84d8a48e95b79d2da88264c4eddc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 17:40:53 +0200 Subject: [PATCH 0568/1208] Support ReflectionFunction builtin invocation --- docs/php/classes.md | 6 +-- docs/php/eval.md | 2 +- src/ir_lower/expr/mod.rs | 54 +++++++++++++++++++++-- tests/codegen/oop/reflection_functions.rs | 18 ++++++++ 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 80f9a1c133..db4de7324c 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1201,7 +1201,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::getNumberOfRequiredParameters()` | `new ReflectionFunction($function_name)` | Return the number of required reflected function parameters | | `ReflectionFunction::hasReturnType()` / `getReturnType()` | `new ReflectionFunction($function_name)` | Return retained declared named, nullable, union, `void`, or `never` return type metadata as `ReflectionNamedType`, `ReflectionUnionType`, or `null` when no supported return type is declared | | `ReflectionFunction::isVariadic()` | `new ReflectionFunction($function_name)` | Return whether the reflected function declares a variadic parameter | -| `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared parameter types are also lowered | +| `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared parameter types and supported callable builtins are also lowered | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP method-name metadata; methods report an empty namespace and `false` for `inNamespace()` | | `ReflectionMethod::isInternal()` / `isUserDefined()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return origin predicates for supported reflected methods | @@ -1366,10 +1366,10 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. -- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. +- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index 873562ed22..893155221c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -364,7 +364,7 @@ accepts eval free-function names, class/interface/trait method arrays, and object-method arrays resolved from the evaluated runtime object, including inline `new` expressions. `ReflectionFunction::invoke()` and `invokeArgs()` dispatch eval-declared functions with the same named/default/variadic argument -binding used by direct eval function calls; generated/AOT function invocation is +binding used by direct eval function calls; generated/AOT function invocation and supported callable-builtin invocation are covered by the general Reflection support documented in `docs/php/classes.md`. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 371b0309d8..98fa592662 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10485,7 +10485,16 @@ fn lower_reflection_function_invoke_call( expr, )); }; - if !reflection_function_target_has_declared_params(ctx, &function_name) { + if let Some(signature) = first_class_builtin_signature(&function_name) { + return Some(lower_reflection_builtin_function_call( + ctx, + &function_name, + &signature, + &forwarded_args, + expr, + )); + } + if !reflection_user_function_target_has_declared_params(ctx, &function_name) { return Some(lower_reflection_function_invoke_unsupported( ctx, &method_key, @@ -10496,6 +10505,28 @@ fn lower_reflection_function_invoke_call( Some(lower_function_call(ctx, &name, &forwarded_args, expr)) } +/// Lowers reflected invocation of a supported callable builtin. +fn lower_reflection_builtin_function_call( + ctx: &mut LoweringContext<'_, '_>, + function_name: &str, + signature: &FunctionSig, + args: &[Expr], + expr: &Expr, +) -> LoweredValue { + let operands = lower_builtin_call_args(ctx, function_name, Some(signature), args); + let php_type = call_return_type_for_args(ctx, function_name, args, &operands) + .unwrap_or_else(|| call_return_type(ctx, function_name, &operands)); + let data = ctx.intern_function_name(function_name); + ctx.emit_value( + Op::BuiltinCall, + operands, + Some(Immediate::Data(data)), + php_type, + effects_lookup::builtin_effects(function_name), + Some(expr.span), + ) +} + /// Returns direct `ReflectionFunction::invoke(...$args)` arguments after static spread expansion. fn reflection_function_invoke_args(args: &[Expr]) -> Vec { reflection_class_new_instance_args(args) @@ -10541,8 +10572,8 @@ fn reflection_function_invoke_args_array( reflection_class_new_instance_args_value(ctx, forwarded_arg) } -/// Returns true when reflected invocation can trust the target function's parameter types. -fn reflection_function_target_has_declared_params( +/// Returns true when reflected invocation can trust a user function's parameter types. +fn reflection_user_function_target_has_declared_params( ctx: &LoweringContext<'_, '_>, function_name: &str, ) -> bool { @@ -11595,7 +11626,22 @@ fn reflection_function_constructor_target( let ExprKind::StringLiteral(function_name) = function_arg.kind else { return None; }; - resolve_known_function_name(ctx, &function_name) + resolve_known_reflection_function_name(ctx, &function_name) +} + +/// Resolves function names accepted by static `ReflectionFunction` metadata. +fn resolve_known_reflection_function_name( + ctx: &LoweringContext<'_, '_>, + function_name: &str, +) -> Option { + resolve_known_function_name(ctx, function_name) + .or_else(|| resolve_known_reflection_builtin_name(function_name)) +} + +/// Resolves a supported callable builtin name for `ReflectionFunction`. +fn resolve_known_reflection_builtin_name(function_name: &str) -> Option { + let canonical = canonical_builtin_function_name(function_name.trim_start_matches('\\'))?; + first_class_builtin_signature(&canonical).map(|_| canonical) } /// Extracts the known class and property name from an inline ReflectionProperty constructor. diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs index 779d6987af..9f7029a057 100644 --- a/tests/codegen/oop/reflection_functions.rs +++ b/tests/codegen/oop/reflection_functions.rs @@ -145,6 +145,24 @@ echo (new ReflectionParameter("strlen", "string"))->getDeclaringFunction()->getN assert_eq!(out, "strlen:strlen:I:u:T:int:1:string:P:string:D:strlen"); } +/// Verifies `ReflectionFunction::invoke()` and `invokeArgs()` call supported builtins. +#[test] +fn test_reflection_function_invoke_calls_builtin_functions() { + let out = compile_and_run( + r#"invoke("abc"); +echo ":"; +echo (new ReflectionFunction("strlen"))->invoke(string: "abcd"); +echo ":"; +$ref = new ReflectionFunction("strlen"); +echo $ref->invokeArgs(["abcde"]); +echo ":"; +echo $ref->invokeArgs(args: ["string" => "abcdef"]); +"#, + ); + assert_eq!(out, "3:4:5:6"); +} + /// Verifies `ReflectionFunction::invoke()` calls declared AOT functions. #[test] fn test_reflection_function_invoke_calls_declared_aot_functions() { From d9909da9fadd331398d6ba1e5842b8496e2189d5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 17:48:54 +0200 Subject: [PATCH 0569/1208] Support ReflectionFunction closure variable defaults --- docs/php/classes.md | 3 ++- docs/php/eval.md | 2 ++ src/types/checker/builtin_types/reflection.rs | 24 ++++++++++++++++++ tests/codegen/oop/reflection_functions.rs | 25 ++++++++++++++++--- 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index db4de7324c..b990ca4062 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1201,6 +1201,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::getNumberOfRequiredParameters()` | `new ReflectionFunction($function_name)` | Return the number of required reflected function parameters | | `ReflectionFunction::hasReturnType()` / `getReturnType()` | `new ReflectionFunction($function_name)` | Return retained declared named, nullable, union, `void`, or `never` return type metadata as `ReflectionNamedType`, `ReflectionUnionType`, or `null` when no supported return type is declared | | `ReflectionFunction::isVariadic()` | `new ReflectionFunction($function_name)` | Return whether the reflected function declares a variadic parameter | +| `ReflectionFunction::getClosureUsedVariables()` | `new ReflectionFunction($function_name)` | Return an empty array for supported non-closure function reflectors | | `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared parameter types and supported callable builtins are also lowered | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP method-name metadata; methods report an empty namespace and `false` for `inNamespace()` | @@ -1366,7 +1367,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/docs/php/eval.md b/docs/php/eval.md index 893155221c..6a429e1b97 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -216,6 +216,8 @@ and `getReturnType()` expose retained eval return type metadata for supported named, nullable, union, and intersection declarations, including `void` and `never` as builtin non-nullable named types. `ReflectionFunction::isDisabled()` reports `false` for eval-visible functions. +`ReflectionFunction::getClosureUsedVariables()` reports an empty array for +supported non-closure function reflectors. `ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval class-like metadata, including diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index dfdb3d0bab..30d2f5a072 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -2906,6 +2906,27 @@ fn builtin_reflection_constant_false_bool_method(method_name: &str) -> ClassMeth } } +/// Returns a public Reflection method that always reports an empty array. +fn builtin_reflection_constant_empty_array_method(method_name: &str) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(array_type()), + body: vec![Stmt::new(StmtKind::Return(empty_array()), dummy_span)], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public Reflection method that always reports PHP `null` as mixed. fn builtin_reflection_constant_null_mixed_method(method_name: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -3540,6 +3561,9 @@ fn builtin_reflection_owner_class( if name == "ReflectionFunction" { methods.push(builtin_reflection_function_invoke_method()); methods.push(builtin_reflection_function_invoke_args_method()); + methods.push(builtin_reflection_constant_empty_array_method( + "getClosureUsedVariables", + )); methods.push(builtin_reflection_constant_false_bool_method( "isDisabled", )); diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs index 9f7029a057..6517a1b382 100644 --- a/tests/codegen/oop/reflection_functions.rs +++ b/tests/codegen/oop/reflection_functions.rs @@ -6,9 +6,9 @@ //! - `cargo test` through Rust's test harness. //! //! Key details: -//! - `ReflectionFunction::invoke()` and `invokeArgs()` are lowered only for -//! statically-known reflectors whose target function has declared parameter -//! types. +//! - `ReflectionFunction::invoke()` and `invokeArgs()` are lowered for +//! statically-known reflectors whose target user function has declared +//! parameter types, plus supported callable builtins. //! - Tests cover inline constructors, local tracking, case-insensitive function //! names, defaults, named arguments, and static argument arrays. @@ -163,6 +163,25 @@ echo $ref->invokeArgs(args: ["string" => "abcdef"]); assert_eq!(out, "3:4:5:6"); } +/// Verifies non-closure `ReflectionFunction` objects report no used variables. +#[test] +fn test_reflection_function_reports_empty_closure_used_variables() { + let out = compile_and_run( + r#"getClosureUsedVariables()) . ":"; +echo count($builtin->getClosureUsedVariables()) . ":"; +$vars = $user->getClosureUsedVariables(); +$vars["x"] = "changed"; +echo count($user->getClosureUsedVariables()); +"#, + ); + assert_eq!(out, "0:0:0"); +} + /// Verifies `ReflectionFunction::invoke()` calls declared AOT functions. #[test] fn test_reflection_function_invoke_calls_declared_aot_functions() { From 2e4c5dc5b6e88d4970f84c2e56860277c2d02f84 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 18:05:58 +0200 Subject: [PATCH 0570/1208] Support ReflectionParameter getClass metadata --- docs/php/classes.md | 7 ++- src/codegen/lower_inst/objects/reflection.rs | 44 +++++++++++++++++++ src/types/checker/builtin_types/reflection.rs | 2 + tests/codegen/oop/attributes.rs | 34 ++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index b990ca4062..f4ffa7a168 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1236,6 +1236,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::isPromoted()` | Same construction forms as `ReflectionParameter::isPassedByReference()` | Return whether the parameter came from constructor property promotion | | `ReflectionParameter::hasType()` | `new ReflectionParameter("function", "name" or 0)`, `new ReflectionParameter([ClassLike::class, "method"], "name" or 0)`, `new ReflectionParameter([$object_expr, "method"], "name" or 0)` with a statically known object type, or `ReflectionMethod::getParameters()` | Return whether the parameter has a declared type | | `ReflectionParameter::getType()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionNamedType` for simple named parameter types, a `ReflectionUnionType` for multi-member union parameter types, a `ReflectionIntersectionType` for intersection parameter types, or `null` when no type is declared or the declared type needs a broader Reflection type object | +| `ReflectionParameter::getClass()` | Same as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for nullable or non-nullable named object parameter types, or `null` for builtin, union, intersection, or untyped parameters | | `ReflectionParameter::allowsNull()` | Same as `ReflectionParameter::hasType()` | Return PHP nullability for retained parameter type/default metadata | | `ReflectionParameter::isArray()` / `isCallable()` | Same as `ReflectionParameter::hasType()` | Return PHP's legacy named-type predicates for nullable or non-nullable `array` / `callable` parameter declarations; union declarations report `false` | | `ReflectionParameter::getAttributes()` | Same construction forms as `ReflectionParameter::hasType()` | Return `ReflectionAttribute` objects for function and method parameter attributes | @@ -1376,10 +1377,12 @@ Limitations today: - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, -and `isCallable()` are supported for retained parameter metadata. +`isCallable()`, and `getClass()` are supported for retained parameter metadata. `isArray()` / `isCallable()` follow PHP's legacy behavior: nullable or non-nullable direct `array` / `callable` declarations report `true`, while -union declarations report `false`. +union declarations report `false`. `getClass()` follows PHP's legacy behavior +for nullable or non-nullable named object types and reports `null` for builtin, +union, intersection, or untyped parameters. `ReflectionProperty::isDefault()` is also supported for materialized declared and dynamic properties. Declared properties report `true`; supported dynamic diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 4c333aca11..7362732c22 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -5912,6 +5912,7 @@ fn emit_reflection_parameter_properties( parameter.is_callable_type, )?; emit_reflection_parameter_type_property(ctx, parameter)?; + emit_reflection_parameter_class_property(ctx, parameter)?; emit_reflection_owner_bool_property( ctx, "ReflectionParameter", @@ -6083,6 +6084,49 @@ fn emit_reflection_parameter_type_property( ) } +/// Writes one ReflectionParameter object's legacy nullable class-type slot. +fn emit_reflection_parameter_class_property( + ctx: &mut FunctionContext<'_>, + parameter: &ReflectionParameterMember, +) -> Result<()> { + let class_offset = { + let class_info = ctx + .module + .class_infos + .get("ReflectionParameter") + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + reflection_property_offset(class_info, "__class")? + }; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(class_name) = reflection_parameter_class_name(parameter) { + let class_metadata = reflection_shallow_class_metadata_for_name(ctx, class_name)?; + emit_reflection_owner_object(ctx, "ReflectionClass", &class_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionClass".to_string()), + ); + } else { + emit_boxed_null_literal_to_result(ctx); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, class_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, class_offset + 8); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + +/// Returns the retained object class name for ReflectionParameter::getClass(). +fn reflection_parameter_class_name(parameter: &ReflectionParameterMember) -> Option<&str> { + match parameter.type_metadata.as_ref()? { + ReflectionParameterTypeMetadata::Named(metadata) if !metadata.is_builtin => { + Some(metadata.name.as_str()) + } + _ => None, + } +} + /// Writes one reflection owner's nullable type slot. fn emit_reflection_owner_type_property( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 30d2f5a072..8446996291 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -828,6 +828,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { bool_lit(false), ), builtin_property("__type", Visibility::Private, Some(mixed_type()), null_lit()), + builtin_property("__class", Visibility::Private, Some(mixed_type()), null_lit()), builtin_property( "__has_default_value", Visibility::Private, @@ -892,6 +893,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { builtin_reflection_slot_getter("isArray", "__is_array_type", TypeExpr::Bool), builtin_reflection_slot_getter("isCallable", "__is_callable_type", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), + builtin_reflection_slot_getter("getClass", "__class", mixed_type()), builtin_reflection_owner_get_attributes_method(), builtin_reflection_slot_getter( "isDefaultValueAvailable", diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 18fee47eec..cc0b7d8a46 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2472,6 +2472,40 @@ if ($directType instanceof ReflectionNamedType) { ); } +/// Verifies that `ReflectionParameter::getClass()` exposes legacy object-type metadata. +#[test] +fn test_reflection_parameter_get_class_returns_named_object_type() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $class = $param->getClass(); + echo $param->getName() . ":" . ($class ? $class->getName() : "null") . "|"; +} +$direct = new ReflectionParameter([ReflectParamClassTarget::class, "run"], "nullable"); +echo "direct:" . $direct->getClass()->getName() . "|"; +$functionParam = new ReflectionParameter("reflect_param_class_function", "dep"); +echo "function:" . $functionParam->getClass()->getName(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dep:ReflectParamClassDep|nullable:ReflectParamClassDep|id:null|unionObject:null|intersection:null|plain:null|direct:ReflectParamClassDep|function:ReflectParamClassDep" + ); +} + /// Verifies that `ReflectionProperty::getType()` and `getSettableType()` return type metadata. #[test] fn test_reflection_property_get_type_returns_type_metadata() { From 2f369fe54d5de68569081d704453b1cd67b29b6a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 18:23:45 +0200 Subject: [PATCH 0571/1208] Support eval ReflectionParameter getClass --- .../elephc-eval/src/interpreter/reflection.rs | 34 +++++++++++-- .../interpreter/tests/support/object_ops.rs | 35 +++++++++++++ docs/php/eval.md | 5 +- src/codegen/eval_reflection_owner_helpers.rs | 50 +++++++++++++++++++ tests/codegen/eval.rs | 35 +++++++++++++ 5 files changed, 154 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index 864cf8442b..d72bbc819f 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -3333,12 +3333,13 @@ fn eval_reflection_parameter_object_result( Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, None => values.null()?, }; + let class_value = eval_reflection_parameter_class_value(parameter, context, values)?; let default_value = eval_reflection_parameter_default_value(parameter, context, values)?; let default_value_constant_name = match parameter.default_value_constant_name.as_deref() { Some(name) => values.string(name)?, None => values.null()?, }; - let backing_value = values.null()?; + let constructor = values.null()?; let flags = eval_reflection_parameter_flags(parameter); let object = values.reflection_owner_new( EVAL_REFLECTION_OWNER_PARAMETER, @@ -3355,8 +3356,8 @@ fn eval_reflection_parameter_object_result( parameter.position as u64, 0, default_value_constant_name, - backing_value, - backing_value, + class_value, + constructor, )?; values.release(attrs)?; values.release(declaring_function)?; @@ -3368,10 +3369,35 @@ fn eval_reflection_parameter_object_result( values.release(default_value)?; values.release(parent_class)?; values.release(default_value_constant_name)?; - values.release(backing_value)?; + values.release(class_value)?; + values.release(constructor)?; Ok(object) } +/// Materializes the legacy ReflectionParameter::getClass() value for known named object types. +fn eval_reflection_parameter_class_value( + parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_reflection_parameter_class_name(parameter) { + Some(class_name) => eval_reflection_shallow_class_object_result(class_name, context, values), + None => values.null(), + } +} + +/// Returns the retained object type name used by ReflectionParameter::getClass(). +fn eval_reflection_parameter_class_name( + parameter: &EvalReflectionParameterMetadata, +) -> Option<&str> { + match ¶meter.type_metadata.as_ref()?.kind { + EvalReflectionParameterTypeKind::Named(named_type) if !named_type.is_builtin => { + Some(named_type.name.as_str()) + } + _ => None, + } +} + /// Materializes one ReflectionParameter default using the declaring class scope when present. fn eval_reflection_parameter_default_value( parameter: &EvalReflectionParameterMetadata, diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index fb0838bc8d..eb98aaafec 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -383,6 +383,9 @@ impl FakeOps { (FakeValue::Object(properties), "gettype" | "getsettabletype") if args.is_empty() => { Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "getclass") if args.is_empty() => { + Self::object_property(&properties, "__class").map_or_else(|| self.null(), Ok) + } (FakeValue::Object(properties), "isdynamic") if args.is_empty() => { Self::object_property(&properties, "__is_dynamic") .map_or_else(|| self.bool_value(false), Ok) @@ -788,6 +791,7 @@ impl FakeOps { self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE) != 0)?; let is_callable_type = self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE) != 0)?; + let class_value = self.reflection_parameter_class_value(method_objects)?; properties.push(("__position".to_string(), position)); properties.push(("__is_optional".to_string(), is_optional)); properties.push(("__is_variadic".to_string(), is_variadic)); @@ -801,6 +805,7 @@ impl FakeOps { properties.push(("__is_array_type".to_string(), is_array_type)); properties.push(("__is_callable_type".to_string(), is_callable_type)); properties.push(("__type".to_string(), method_objects)); + properties.push(("__class".to_string(), class_value)); properties.push(("__has_default_value".to_string(), has_default_value)); properties.push(( "__is_default_value_constant".to_string(), @@ -831,6 +836,36 @@ impl FakeOps { .insert(object.as_ptr() as usize, class_name.to_string()); Ok(object) } + + /// Builds the fake `ReflectionParameter::getClass()` value from a named non-builtin type. + fn reflection_parameter_class_value( + &mut self, + type_value: RuntimeCellHandle, + ) -> Result { + if !self + .object_classes + .get(&(type_value.as_ptr() as usize)) + .is_some_and(|class_name| class_name == "ReflectionNamedType") + { + return self.null(); + } + let FakeValue::Object(type_properties) = self.get(type_value) else { + return self.null(); + }; + let is_builtin = Self::object_property(&type_properties, "__is_builtin") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if is_builtin { + return self.null(); + } + let Some(name) = Self::object_property(&type_properties, "__name") else { + return self.null(); + }; + let FakeValue::String(name) = self.get(name) else { + return self.null(); + }; + self.fake_reflection_class_object(&name) + } + /// Checks whether a private fake object array property contains one string. fn object_string_array_contains( &mut self, diff --git a/docs/php/eval.md b/docs/php/eval.md index 6a429e1b97..1dcd81bc6f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -350,7 +350,10 @@ named type metadata through `ReflectionParameter::getType()` / `ReflectionNamedType::getName()`, `allowsNull()`, and `isBuiltin()`, and the legacy `ReflectionParameter::isArray()` / `isCallable()` predicates for named `array` -and `callable` parameter types. Multi-member union metadata is exposed through +and `callable` parameter types. `ReflectionParameter::getClass()` returns a +`ReflectionClass` object for retained nullable or non-nullable named object +parameter types and `null` for builtin, union, intersection, or untyped +parameters. Multi-member union metadata is exposed through `ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter metadata is exposed through `ReflectionIntersectionType::getTypes()` and `allowsNull()`. Function, method, and parameter attributes are exposed through diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 7e2f9122de..616c72abd5 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -104,6 +104,8 @@ struct ReflectionOwnerLayout { has_type_hi: Option, parameter_type_lo: Option, parameter_type_hi: Option, + parameter_class_lo: Option, + parameter_class_hi: Option, has_default_value_lo: Option, has_default_value_hi: Option, is_default_value_constant_lo: Option, @@ -270,6 +272,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option OptiongetParameters(); +foreach ($params as $param) { + $class = $param->getClass(); + echo $param->getName() . ":" . ($class ? $class->getName() : "null") . "|"; +} +$direct = new ReflectionParameter(["EvalReflectParamClassTarget", "run"], "nullable"); +echo "direct:" . $direct->getClass()->getName() . "|"; +$functionParam = new ReflectionParameter("eval_reflect_param_class_function", "dep"); +echo "function:" . $functionParam->getClass()->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dep:EvalReflectParamClassDep|nullable:EvalReflectParamClassDep|id:null|unionObject:null|intersection:null|plain:null|direct:EvalReflectParamClassDep|function:EvalReflectParamClassDep" + ); +} + /// Verifies eval direct ReflectionParameter construction accepts runtime object method targets. #[test] fn test_eval_reflection_parameter_accepts_object_expression_target() { From a9f7d5bb64a5d6c64053d089d36f3a6701cdeaae Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 18:42:34 +0200 Subject: [PATCH 0572/1208] Support ReflectionType toString metadata --- .../elephc-eval/src/interpreter/reflection.rs | 88 +++++++++ .../elephc-eval/src/interpreter/statements.rs | 8 + .../tests/builtins_class_metadata.rs | 31 +++ .../interpreter/tests/support/object_ops.rs | 79 ++++++++ docs/php/classes.md | 7 +- docs/php/eval.md | 6 +- src/types/checker/builtin_types/reflection.rs | 178 ++++++++++++++++++ tests/codegen/eval.rs | 31 +++ tests/codegen/oop/attributes.rs | 30 +++ 9 files changed, 453 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-eval/src/interpreter/reflection.rs index d72bbc819f..5e2e7379d3 100644 --- a/crates/elephc-eval/src/interpreter/reflection.rs +++ b/crates/elephc-eval/src/interpreter/reflection.rs @@ -916,6 +916,94 @@ pub(in crate::interpreter) fn eval_reflection_parameter_legacy_type_predicate_re .map(Some) } +/// Handles eval-backed `ReflectionType::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_type_to_string_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let type_kind = if eval_reflection_object_has_class(object, "ReflectionNamedType", values)? { + Some(("ReflectionNamedType", "")) + } else if eval_reflection_object_has_class(object, "ReflectionUnionType", values)? { + Some(("ReflectionUnionType", "|")) + } else if eval_reflection_object_has_class(object, "ReflectionIntersectionType", values)? { + Some(("ReflectionIntersectionType", "&")) + } else { + None + }; + let Some((class_name, separator)) = type_kind else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = if class_name == "ReflectionNamedType" { + eval_reflection_named_type_to_string(object, values)? + } else { + eval_reflection_composite_type_to_string(object, separator, values)? + }; + values.string(&rendered).map(Some) +} + +/// Formats one eval-visible ReflectionNamedType object from its public methods. +fn eval_reflection_named_type_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_reflection_type_method_string(object, "getName", values)?; + let allows_null = eval_reflection_type_method_bool(object, "allowsNull", values)?; + if allows_null && name != "mixed" { + Ok(format!("?{name}")) + } else { + Ok(name) + } +} + +/// Formats one eval-visible ReflectionUnionType or ReflectionIntersectionType object. +fn eval_reflection_composite_type_to_string( + object: RuntimeCellHandle, + separator: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let types = values.method_call(object, "getTypes", Vec::new())?; + let mut names = Vec::new(); + for position in 0..values.array_len(types)? { + let key = values.array_iter_key(types, position)?; + let member = values.array_get(types, key)?; + names.push(eval_reflection_type_method_string(member, "getName", values)?); + } + if separator == "|" && eval_reflection_type_method_bool(object, "allowsNull", values)? { + names.push(String::from("null")); + } + Ok(names.join(separator)) +} + +/// Calls one no-arg ReflectionType method and returns its string result. +fn eval_reflection_type_method_string( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Calls one no-arg ReflectionType method and returns its bool result. +fn eval_reflection_type_method_bool( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + if values.type_tag(value)? != EVAL_TAG_BOOL { + return Err(EvalStatus::RuntimeFatal); + } + values.truthy(value) +} + /// Maps a legacy ReflectionParameter predicate method to its target named type. fn eval_reflection_parameter_legacy_type_name(method_name: &str) -> Option<&'static str> { if method_name.eq_ignore_ascii_case("isArray") { diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 5a1120d90b..c6ad3e3c10 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -3228,6 +3228,14 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_type_to_string_result( + object, + method_name, + evaluated_args.clone(), + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_implements_interface_result( identity, method_name, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 1549a19f2d..32b20ccf34 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1704,6 +1704,37 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionType objects stringify retained eval parameter metadata. +#[test] +fn execute_program_reflection_type_to_string() { + let program = parse_fragment( +br##"class EvalReflectTypeStringDep {} +interface EvalReflectTypeStringLeft {} +interface EvalReflectTypeStringRight {} +class EvalReflectTypeStringTarget { + public function run(?EvalReflectTypeStringDep $dep, int|string|null $union, EvalReflectTypeStringLeft&EvalReflectTypeStringRight $both, mixed $mixed, ?array $items) {} +} +$params = (new ReflectionMethod("EvalReflectTypeStringTarget", "run"))->getParameters(); +foreach ($params as $param) { + $type = $param->getType(); + echo $param->getName(); echo ":"; + echo $type->__toString(); echo "|"; +} +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod exposes eval-declared return type metadata. #[test] fn execute_program_reflection_method_reports_return_type_metadata() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index eb98aaafec..60b36a47c1 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -139,6 +139,10 @@ impl FakeOps { self.null() } (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), + (FakeValue::Object(properties), "__tostring") if args.is_empty() => { + let class_name = self.object_classes.get(&(object.as_ptr() as usize)).cloned(); + self.reflection_type_to_string(class_name.as_deref(), &properties) + } (FakeValue::Object(properties), "getname") if args.is_empty() => { Self::object_property(&properties, "__name").map_or_else(|| self.string(""), Ok) } @@ -944,6 +948,81 @@ impl FakeOps { Ok(object) } + /// Formats fake ReflectionType objects through their synthetic `__toString()` method. + fn reflection_type_to_string( + &mut self, + class_name: Option<&str>, + properties: &[(String, RuntimeCellHandle)], + ) -> Result { + match class_name { + Some("ReflectionNamedType") => self.reflection_named_type_to_string(properties), + Some("ReflectionUnionType") => self.reflection_composite_type_to_string( + properties, + "|", + true, + ), + Some("ReflectionIntersectionType") => self.reflection_composite_type_to_string( + properties, + "&", + false, + ), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Formats one fake ReflectionNamedType object using retained name/nullability slots. + fn reflection_named_type_to_string( + &mut self, + properties: &[(String, RuntimeCellHandle)], + ) -> Result { + let Some(name) = Self::object_property(properties, "__name") else { + return self.string(""); + }; + let FakeValue::String(name) = self.get(name) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let allows_null = Self::object_property(properties, "__allows_null") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if allows_null && name != "mixed" { + self.string(&format!("?{name}")) + } else { + self.string(&name) + } + } + + /// Formats one fake ReflectionUnionType or ReflectionIntersectionType object. + fn reflection_composite_type_to_string( + &mut self, + properties: &[(String, RuntimeCellHandle)], + separator: &str, + append_null: bool, + ) -> Result { + let mut names = Vec::new(); + if let Some(types) = Self::object_property(properties, "__types") { + let FakeValue::Array(elements) = self.get(types) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::Object(type_properties) = self.get(element) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let Some(name) = Self::object_property(&type_properties, "__name") else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(name) = self.get(name) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + names.push(name); + } + } + let allows_null = Self::object_property(properties, "__allows_null") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if append_null && allows_null { + names.push("null".to_string()); + } + self.string(&names.join(separator)) + } + /// Finds one fake ReflectionMethod/ReflectionProperty object by its private name slot. fn object_named_member( &mut self, diff --git a/docs/php/classes.md b/docs/php/classes.md index f4ffa7a168..f50047b8d4 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1246,9 +1246,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and object default values with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults, or throw `ReflectionException` when no default is available | | `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | -| `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` | `ReflectionParameter::getType()` | Return simple parameter type metadata | -| `ReflectionUnionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return non-null union members as `ReflectionNamedType` objects and report whether `null` was part of the union | -| `ReflectionIntersectionType::getTypes()` / `allowsNull()` | `ReflectionParameter::getType()` | Return intersection members as `ReflectionNamedType` objects and report `false` for nullability | +| `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` / `__toString()` | `ReflectionParameter::getType()` | Return and stringify simple parameter type metadata | +| `ReflectionUnionType::getTypes()` / `allowsNull()` / `__toString()` | `ReflectionParameter::getType()` | Return non-null union members as `ReflectionNamedType` objects, report whether `null` was part of the union, and stringify retained union metadata | +| `ReflectionIntersectionType::getTypes()` / `allowsNull()` / `__toString()` | `ReflectionParameter::getType()` | Return intersection members as `ReflectionNamedType` objects, report `false` for nullability, and stringify retained intersection metadata | | `ReflectionProperty::getName()` | `new ReflectionProperty($class_name, $property_name)` | Return the reflected property name | | `ReflectionProperty::getDeclaringClass()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Return a `ReflectionClass` object for the class-like symbol that declares the reflected property | | `ReflectionProperty::getAttributes()` | `new ReflectionProperty($class_name, $property_name)` | Return `ReflectionAttribute` objects for property attributes | @@ -1369,6 +1369,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. diff --git a/docs/php/eval.md b/docs/php/eval.md index 1dcd81bc6f..0e285c67f9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -354,9 +354,11 @@ and `callable` parameter types. `ReflectionParameter::getClass()` returns a `ReflectionClass` object for retained nullable or non-nullable named object parameter types and `null` for builtin, union, intersection, or untyped parameters. Multi-member union metadata is exposed through -`ReflectionUnionType::getTypes()` and `allowsNull()`. Intersection parameter +`ReflectionUnionType::getTypes()`, `allowsNull()`, and `__toString()`. Intersection parameter metadata is exposed through `ReflectionIntersectionType::getTypes()` and -`allowsNull()`. Function, method, and parameter attributes are exposed through +`allowsNull()` / `__toString()`. Named, nullable named, union, and intersection +`ReflectionType` objects stringify using the retained eval type metadata. +Function, method, and parameter attributes are exposed through `getAttributes()` using materialized `ReflectionAttribute` objects. Parameter default values, optionality, nullability, variadic flags, and by-reference flags are retained for eval-declared functions and methods, including diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 8446996291..436e0455c5 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1257,6 +1257,7 @@ fn builtin_reflection_named_type() -> FlattenedClass { methods: vec![ builtin_reflection_private_constructor_method(), builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), + builtin_reflection_named_type_to_string_method(), builtin_reflection_slot_getter("allowsNull", "__allows_null", TypeExpr::Bool), builtin_reflection_slot_getter("isBuiltin", "__is_builtin", TypeExpr::Bool), ], @@ -1297,6 +1298,7 @@ fn builtin_reflection_union_type() -> FlattenedClass { "__types", object_array_type("ReflectionNamedType"), ), + builtin_reflection_composite_type_to_string_method("|", true), builtin_reflection_class_bool_method("allowsNull", "__allows_null"), ], attributes: Vec::new(), @@ -1336,6 +1338,7 @@ fn builtin_reflection_intersection_type() -> FlattenedClass { "__types", object_array_type("ReflectionNamedType"), ), + builtin_reflection_composite_type_to_string_method("&", false), builtin_reflection_class_bool_method("allowsNull", "__allows_null"), ], attributes: Vec::new(), @@ -1344,6 +1347,172 @@ fn builtin_reflection_intersection_type() -> FlattenedClass { } } +/// Builds `ReflectionNamedType::__toString()` from retained name/nullability slots. +fn builtin_reflection_named_type_to_string_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let name = reflection_this_property("__name", dummy_span); + let nullable_named_type = binary_expr( + reflection_this_property("__allows_null", dummy_span), + BinOp::And, + binary_expr( + name.clone(), + BinOp::StrictNotEq, + string_lit("mixed", dummy_span), + dummy_span, + ), + dummy_span, + ); + ClassMethod { + name: "__toString".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Str), + by_ref_return: false, + body: vec![ + Stmt::new( + StmtKind::If { + condition: nullable_named_type, + then_body: vec![Stmt::new( + StmtKind::Return(Some(concat_expr( + string_lit("?", dummy_span), + name.clone(), + dummy_span, + ))), + dummy_span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + ), + Stmt::new(StmtKind::Return(Some(name)), dummy_span), + ], + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds `ReflectionUnionType::__toString()` or `ReflectionIntersectionType::__toString()`. +fn builtin_reflection_composite_type_to_string_method( + separator: &'static str, + append_null: bool, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + let mut body = vec![ + Stmt::new( + StmtKind::TypedAssign { + type_expr: TypeExpr::Str, + name: "result".to_string(), + value: string_lit("", dummy_span), + }, + dummy_span, + ), + Stmt::new( + StmtKind::Foreach { + array: reflection_this_property("__types", dummy_span), + key_var: None, + value_var: "type".to_string(), + value_by_ref: false, + body: reflection_composite_type_append_body( + method_call_expr( + variable_expr("type", dummy_span), + "getName", + Vec::new(), + dummy_span, + ), + separator, + dummy_span, + ), + }, + dummy_span, + ), + ]; + if append_null { + body.push(Stmt::new( + StmtKind::If { + condition: reflection_this_property("__allows_null", dummy_span), + then_body: reflection_composite_type_append_body( + string_lit("null", dummy_span), + separator, + dummy_span, + ), + elseif_clauses: Vec::new(), + else_body: None, + }, + dummy_span, + )); + } + body.push(Stmt::new( + StmtKind::Return(Some(variable_expr("result", dummy_span))), + dummy_span, + )); + ClassMethod { + name: "__toString".to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Str), + by_ref_return: false, + body, + span: dummy_span, + attributes: Vec::new(), + } +} + +/// Builds the statements that append one rendered type segment to `$result`. +fn reflection_composite_type_append_body( + value: Expr, + separator: &'static str, + span: crate::span::Span, +) -> Vec { + vec![ + Stmt::new( + StmtKind::If { + condition: binary_expr( + variable_expr("result", span), + BinOp::StrictNotEq, + string_lit("", span), + span, + ), + then_body: vec![Stmt::new( + StmtKind::Assign { + name: "result".to_string(), + value: concat_expr( + variable_expr("result", span), + string_lit(separator, span), + span, + ), + }, + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ), + Stmt::new( + StmtKind::Assign { + name: "result".to_string(), + value: concat_expr(variable_expr("result", span), value, span), + }, + span, + ), + ] +} + /// Builds the `ReflectionClass` shell with retained eval metadata accessors. fn builtin_reflection_class() -> FlattenedClass { FlattenedClass { @@ -4590,6 +4759,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Bool; } } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } } if class_name == "ReflectionUnionType" { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTypes")) { @@ -4600,6 +4772,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut("allowsnull") { sig.return_type = PhpType::Bool; } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } } if class_name == "ReflectionIntersectionType" { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getTypes")) { @@ -4610,6 +4785,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut("allowsnull") { sig.return_type = PhpType::Bool; } + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getAttributes")) { sig.return_type = diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3d2056dd55..30a65f2e22 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8540,6 +8540,37 @@ foreach ($params as $param) { ); } +/// Verifies eval ReflectionType objects stringify retained parameter metadata. +#[test] +fn test_eval_reflection_type_to_string_formats_retained_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $type = $param->getType(); + echo $param->getName() . ":"; + echo $type->__toString() . "|"; +} +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|" + ); +} + /// Verifies eval ReflectionParameter::getClass() reports retained object type metadata. #[test] fn test_eval_reflection_parameter_get_class_reports_named_object_type() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index cc0b7d8a46..034b4a0b6f 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2472,6 +2472,36 @@ if ($directType instanceof ReflectionNamedType) { ); } +/// Verifies `ReflectionType::__toString()` formats retained type metadata. +#[test] +fn test_reflection_type_to_string_formats_retained_metadata() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + $type = $param->getType(); + echo $param->getName() . ":"; + echo $type->__toString() . "|"; +} +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "dep:?ReflectTypeStringDep|union:int|string|null|both:ReflectTypeStringLeft&ReflectTypeStringRight|mixed:mixed|items:?array|" + ); +} + /// Verifies that `ReflectionParameter::getClass()` exposes legacy object-type metadata. #[test] fn test_reflection_parameter_get_class_returns_named_object_type() { From f62d1405101937b0545279b982bbff92219259eb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 19:16:59 +0200 Subject: [PATCH 0573/1208] Improve eval object string contexts --- crates/elephc-eval/src/eval_ir.rs | 13 ++ .../src/interpreter/dynamic_functions.rs | 31 ++- .../src/interpreter/expressions.rs | 21 ++ crates/elephc-eval/src/interpreter/mod.rs | 8 +- .../tests/builtins_class_metadata.rs | 7 +- .../src/interpreter/tests/builtins_scalars.rs | 6 +- crates/elephc-eval/src/parser/expressions.rs | 38 ++- crates/elephc-eval/src/parser/statements.rs | 2 + .../elephc-eval/src/parser/tests/operators.rs | 18 ++ src/codegen/lower_inst.rs | 8 +- src/codegen/lower_inst/conversions.rs | 217 +++++++++++++++++- tests/codegen/eval.rs | 7 +- tests/codegen/oop/attributes.rs | 7 +- 13 files changed, 363 insertions(+), 20 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index f00f11ce14..f019d574c2 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -1923,6 +1923,10 @@ pub enum EvalExpr { name: String, args: Vec, }, + Cast { + target: EvalCastType, + expr: Box, + }, Const(EvalConst), ConstFetch(String), DynamicCall { @@ -2141,6 +2145,15 @@ pub enum EvalBinOp { Spaceship, } +/// Scalar cast targets supported by runtime eval expressions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalCastType { + Int, + Float, + String, + Bool, +} + /// Unary operations supported by the initial EvalIR parser. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EvalUnaryOp { diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index bb990b5a49..27ce0703de 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -664,7 +664,7 @@ pub(in crate::interpreter) fn eval_method_parameter_scalar_coercion( } } -/// Converts eval-declared objects in string contexts through `__toString()`. +/// Converts objects in string contexts through the applicable `__toString()` dispatch path. pub(in crate::interpreter) fn eval_string_context_value( value: RuntimeCellHandle, context: &mut ElephcEvalContext, @@ -676,7 +676,7 @@ pub(in crate::interpreter) fn eval_string_context_value( eval_dynamic_object_string_context_value(value, context, values) } -/// Invokes `__toString()` for eval-created objects and rejects missing invalid hooks. +/// Invokes `__toString()` for eval-declared objects and rejects missing invalid hooks. fn eval_dynamic_object_string_context_value( value: RuntimeCellHandle, context: &mut ElephcEvalContext, @@ -684,7 +684,7 @@ fn eval_dynamic_object_string_context_value( ) -> Result { let identity = values.object_identity(value)?; let Some(class) = context.dynamic_object_class(identity) else { - return Ok(value); + return eval_runtime_object_string_context_value(value, context, values); }; let called_class_name = class.name().to_string(); let Some((declaring_class, method)) = context.class_method(&called_class_name, "__toString") @@ -707,6 +707,30 @@ fn eval_dynamic_object_string_context_value( context, values, )?; + eval_tostring_result_to_string(result, values) +} + +/// Invokes the interpreter method dispatcher for AOT/native objects in string contexts. +fn eval_runtime_object_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_method_call_result_with_evaluated_args( + value, + "__toString", + Vec::new(), + context, + values, + )?; + eval_tostring_result_to_string(result, values) +} + +/// Normalizes one `__toString()` result to a boxed string cell. +fn eval_tostring_result_to_string( + result: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { if values.type_tag(result)? == EVAL_TAG_STRING { return Ok(result); } @@ -781,6 +805,7 @@ fn eval_method_default_expr_is_supported(expr: &EvalExpr) -> bool { .is_none_or(eval_method_default_expr_is_supported) && eval_method_default_expr_is_supported(else_branch) } + EvalExpr::Cast { expr, .. } => eval_method_default_expr_is_supported(expr), EvalExpr::Unary { expr, .. } => eval_method_default_expr_is_supported(expr), EvalExpr::Binary { left, right, .. } => { eval_method_default_expr_is_supported(left) diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 312f1d9a40..4462c37676 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -35,6 +35,7 @@ pub(in crate::interpreter) fn eval_expr( values.array_get(array, index) } EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), + EvalExpr::Cast { target, expr } => eval_cast_expr(target, expr, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), EvalExpr::DynamicCall { callee, args } => { @@ -246,6 +247,26 @@ pub(in crate::interpreter) fn eval_expr( } } +/// Evaluates one PHP scalar cast expression through the runtime conversion hooks. +fn eval_cast_expr( + target: &EvalCastType, + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(expr, context, scope, values)?; + match target { + EvalCastType::Int => values.cast_int(value), + EvalCastType::Float => values.cast_float(value), + EvalCastType::String => { + let value = eval_string_context_value(value, context, values)?; + values.cast_string(value) + } + EvalCastType::Bool => values.cast_bool(value), + } +} + /// Resolves special class names used by `new` while preserving AOT fallback names. fn eval_new_object_class_name( class_name: &str, diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-eval/src/interpreter/mod.rs index adcbb824e6..884f40ec69 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-eval/src/interpreter/mod.rs @@ -37,10 +37,10 @@ use crate::context::{ use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, - EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, - EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalFunction, EvalInstanceOfTarget, EvalInterface, - EvalInterfaceMethod, EvalInterfaceProperty, EvalMagicConst, EvalMatchArm, EvalParameterType, - EvalParameterTypeVariant, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, + EvalCastType, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, + EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalFunction, EvalInstanceOfTarget, + EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalMagicConst, EvalMatchArm, + EvalParameterType, EvalParameterTypeVariant, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs index 32b20ccf34..c7bb86c1c0 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs @@ -1720,6 +1720,11 @@ foreach ($params as $param) { echo $param->getName(); echo ":"; echo $type->__toString(); echo "|"; } +$unionType = $params[1]->getType(); +echo "cast:" . (string)$unionType . "|"; +echo "concat:" . $unionType . "|"; +echo "echo:"; +echo $unionType; return true;"##, ) .expect("parse eval fragment"); @@ -1730,7 +1735,7 @@ return true;"##, assert_eq!( values.output, - "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|" + "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|cast:int|string|null|concat:int|string|null|echo:int|string|null" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs b/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs index 4bf47ec712..3e672a6f11 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs @@ -108,6 +108,10 @@ fn execute_program_dispatches_cast_builtins() { echo floatval("3.5"); echo ":"; echo strval(12); echo ":"; echo boolval("0") ? "bad" : "false"; +echo ":"; echo (string)12; +echo ":"; echo (int)"9"; +echo ":"; echo (float)"3.5"; +echo ":"; echo (bool)"0" ? "bad" : "false"; echo ":"; echo call_user_func("strval", 7); return call_user_func_array("intval", ["9"]);"#, ) @@ -117,7 +121,7 @@ return call_user_func_array("intval", ["9"]);"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "42:3.5:12:false:7"); + assert_eq!(values.output, "42:3.5:12:false:12:9:3.5:false:7"); assert_eq!(values.get(result), FakeValue::Int(9)); } /// Verifies eval `settype()` mutates direct variables and warns for callable by-value dispatch. diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-eval/src/parser/expressions.rs index 064c09280e..f9959b0269 100644 --- a/crates/elephc-eval/src/parser/expressions.rs +++ b/crates/elephc-eval/src/parser/expressions.rs @@ -12,8 +12,8 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalConst, EvalExpr, EvalInstanceOfTarget, - EvalMagicConst, EvalMatchArm, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCastType, EvalConst, EvalExpr, + EvalInstanceOfTarget, EvalMagicConst, EvalMatchArm, EvalUnaryOp, }; use crate::lexer::TokenKind; @@ -305,6 +305,16 @@ impl Parser { /// Parses right-associative unary prefix expressions. pub(super) fn parse_unary(&mut self) -> Result { + if let Some(target) = self.peek_scalar_cast_type() { + self.advance(); + self.advance(); + self.advance(); + let expr = self.parse_concat()?; + return Ok(EvalExpr::Cast { + target, + expr: Box::new(expr), + }); + } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "clone")) { self.advance(); let expr = self.parse_unary()?; @@ -341,6 +351,30 @@ impl Parser { self.parse_instanceof() } + /// Returns the scalar cast target represented by the current `(type)` token window. + fn peek_scalar_cast_type(&self) -> Option { + if !matches!(self.current(), TokenKind::LParen) { + return None; + } + let Some(TokenKind::Ident(name)) = self.tokens.get(self.pos + 1) else { + return None; + }; + if !matches!(self.tokens.get(self.pos + 2), Some(TokenKind::RParen)) { + return None; + } + if ident_eq(name, "int") || ident_eq(name, "integer") { + Some(EvalCastType::Int) + } else if ident_eq(name, "float") || ident_eq(name, "double") || ident_eq(name, "real") { + Some(EvalCastType::Float) + } else if ident_eq(name, "string") { + Some(EvalCastType::String) + } else if ident_eq(name, "bool") || ident_eq(name, "boolean") { + Some(EvalCastType::Bool) + } else { + None + } + } + /// Parses left-associative `instanceof` with PHP's high operator precedence. pub(super) fn parse_instanceof(&mut self) -> Result { let mut expr = self.parse_power()?; diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 3f5630a273..025a3ae8dc 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -2620,6 +2620,7 @@ fn eval_constant_expression_default_is_supported(expr: &EvalExpr) -> bool { .is_none_or(eval_constant_expression_default_is_supported) && eval_constant_expression_default_is_supported(else_branch) } + EvalExpr::Cast { expr, .. } => eval_constant_expression_default_is_supported(expr), EvalExpr::Unary { expr, .. } => eval_constant_expression_default_is_supported(expr), EvalExpr::Binary { left, right, .. } => { eval_constant_expression_default_is_supported(left) @@ -2838,6 +2839,7 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { | EvalExpr::NamespacedConstFetch { .. } | EvalExpr::StaticPropertyGet { .. } => false, EvalExpr::Include { path, .. } + | EvalExpr::Cast { expr: path, .. } | EvalExpr::Clone(path) | EvalExpr::Print(path) | EvalExpr::Unary { expr: path, .. } => eval_expr_uses_this_property(path, property_name), diff --git a/crates/elephc-eval/src/parser/tests/operators.rs b/crates/elephc-eval/src/parser/tests/operators.rs index 42d9d4e589..e39612c14e 100644 --- a/crates/elephc-eval/src/parser/tests/operators.rs +++ b/crates/elephc-eval/src/parser/tests/operators.rs @@ -129,6 +129,24 @@ fn parse_fragment_accepts_dynamic_instanceof_targets() { ); } +/// Verifies scalar cast syntax parses with PHP cast precedence across concatenation. +#[test] +fn parse_fragment_accepts_scalar_cast_source() { + let program = + parse_fragment(br#"return (string)$value . "!";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Cast { + target: EvalCastType::String, + expr: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::LoadVar("value".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::String("!".to_string()))), + }), + }))] + ); +} + /// Verifies logical operators parse with `&&` binding tighter than `||`. #[test] fn parse_fragment_accepts_short_circuit_logical_source() { diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 6b68284cdc..9bdde11382 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -6810,8 +6810,12 @@ fn lower_invoker_ref_arg(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R /// Lowers PHP echo output for a previously computed SSA value. fn lower_echo_value(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { let value = expect_operand(inst, 0)?; - if let PhpType::Object(class_name) = ctx.value_php_type(value)?.codegen_repr() { - return lower_object_echo_value(ctx, value, &class_name); + match ctx.value_php_type(value)?.codegen_repr() { + PhpType::Object(class_name) => return lower_object_echo_value(ctx, value, &class_name), + PhpType::Mixed | PhpType::Union(_) => { + return conversions::emit_mixed_string_context_stdout(ctx, value); + } + _ => {} } let ty = ctx.load_value_to_result(value)?; let raw_ty = ctx.raw_value_php_type(value)?; diff --git a/src/codegen/lower_inst/conversions.rs b/src/codegen/lower_inst/conversions.rs index dfdfb8dc19..c0cff434a4 100644 --- a/src/codegen/lower_inst/conversions.rs +++ b/src/codegen/lower_inst/conversions.rs @@ -11,13 +11,16 @@ use crate::codegen::abi; use crate::codegen::platform::Arch; -use crate::ir::{Immediate, Instruction, IrType}; +use crate::ir::{Immediate, Instruction, IrType, ValueId}; +use crate::names::method_symbol; use crate::types::PhpType; use super::super::context::FunctionContext; use super::{ - expect_operand, load_value_to_first_int_arg, lower_runtime_object_method_call, predicates, - store_if_result, strings, + direct_call_stack_pad_bytes, emit_dynamic_instance_method_call, + emit_mixed_method_class_dispatch, expect_operand, load_value_to_first_int_arg, + lower_runtime_object_method_call, materialize_method_call_args_with_receiver_reg_and_refs, + mixed_method_candidates, predicates, store_if_result, strings, }; use crate::codegen::{CodegenIrError, Result}; @@ -181,8 +184,7 @@ pub(super) fn lower_cast_to_string( strings::lower_int_like_to_string(ctx, inst) } PhpType::Mixed | PhpType::Union(_) => { - load_value_to_first_int_arg(ctx, value)?; - abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + emit_mixed_string_context_result(ctx, value)?; store_if_result(ctx, inst) } PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Iterable => { @@ -196,6 +198,211 @@ pub(super) fn lower_cast_to_string( } } +/// Leaves a string result for a boxed Mixed value, dispatching objects through `__toString()`. +pub(super) fn emit_mixed_string_context_result( + ctx: &mut FunctionContext<'_>, + value: ValueId, +) -> Result<()> { + emit_mixed_string_context(ctx, value, MixedStringContextMode::Result) +} + +/// Writes a boxed Mixed value to stdout, dispatching objects through `__toString()`. +pub(super) fn emit_mixed_string_context_stdout( + ctx: &mut FunctionContext<'_>, + value: ValueId, +) -> Result<()> { + emit_mixed_string_context(ctx, value, MixedStringContextMode::Stdout) +} + +/// Describes whether a Mixed string context should leave a string result or write it. +enum MixedStringContextMode { + Result, + Stdout, +} + +/// Handles PHP string contexts for boxed Mixed values with an object-aware branch. +fn emit_mixed_string_context( + ctx: &mut FunctionContext<'_>, + value: ValueId, + mode: MixedStringContextMode, +) -> Result<()> { + let candidates = mixed_method_candidates(ctx, "__toString", 1)?; + let receiver_reg = abi::nested_call_reg(ctx.emitter); + let object_label = ctx.next_label("mixed_string_object"); + let no_match_label = ctx.next_label("mixed_string_no_match"); + let done_label = ctx.next_label("mixed_string_done"); + let match_labels = candidates + .iter() + .map(|candidate| { + ctx.next_label(&format!( + "mixed_string_{}", + super::label_fragment(&candidate.class_name) + )) + }) + .collect::>(); + + ctx.load_value_to_result(value)?; + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_unboxed_mixed_object(ctx, &object_label); + emit_mixed_string_scalar_fallback(ctx, &mode)?; + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&object_label); + discard_preserved_mixed_pointer(ctx); + move_unboxed_mixed_object_payload(ctx, receiver_reg); + emit_mixed_method_class_dispatch( + ctx, + receiver_reg, + &candidates, + &match_labels, + &no_match_label, + ); + + for (candidate, label) in candidates.iter().zip(match_labels.iter()) { + ctx.emitter.label(label); + let return_ty = emit_mixed_tostring_candidate_call(ctx, value, receiver_reg, candidate)?; + coerce_tostring_return_to_string_result(ctx, &return_ty)?; + if matches!(mode, MixedStringContextMode::Stdout) { + abi::emit_write_stdout(ctx.emitter, &PhpType::Str); + } + abi::emit_jump(ctx.emitter, &done_label); + } + + ctx.emitter.label(&no_match_label); + emit_mixed_missing_tostring_fatal(ctx); + ctx.emitter.label(&done_label); + Ok(()) +} + +/// Branches to the object path when `__rt_mixed_unbox` returned an object tag. +fn emit_branch_if_unboxed_mixed_object(ctx: &mut FunctionContext<'_>, object_label: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // check whether the boxed Mixed value contains an object + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // dispatch object string contexts through __toString + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // check whether the boxed Mixed value contains an object + ctx.emitter.instruction(&format!("je {}", object_label)); // dispatch object string contexts through __toString + } + } +} + +/// Runs the existing scalar Mixed string behavior after restoring the original box. +fn emit_mixed_string_scalar_fallback( + ctx: &mut FunctionContext<'_>, + mode: &MixedStringContextMode, +) -> Result<()> { + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + match mode { + MixedStringContextMode::Result => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + } + MixedStringContextMode::Stdout => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_write_stdout"); + } + } + Ok(()) +} + +/// Discards the saved boxed Mixed pointer once the object branch no longer needs it. +fn discard_preserved_mixed_pointer(ctx: &mut FunctionContext<'_>) { + abi::emit_pop_reg(ctx.emitter, abi::temp_int_reg(ctx.emitter.target)); +} + +/// Moves the unboxed object payload into the callee-saved receiver dispatch register. +fn move_unboxed_mixed_object_payload(ctx: &mut FunctionContext<'_>, receiver_reg: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("mov {}, x1", receiver_reg)); // preserve the unboxed object pointer for __toString dispatch + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("mov {}, rdi", receiver_reg)); // preserve the unboxed object pointer for __toString dispatch + } + } +} + +/// Emits one concrete `__toString()` candidate call for a boxed Mixed object. +fn emit_mixed_tostring_candidate_call( + ctx: &mut FunctionContext<'_>, + value: ValueId, + receiver_reg: &str, + candidate: &super::MixedMethodCandidate, +) -> Result { + let receiver_ty = PhpType::Object(candidate.class_name.clone()); + let mut param_types = Vec::with_capacity(candidate.target.params.len() + 1); + param_types.push(receiver_ty.clone()); + param_types.extend(candidate.target.params.iter().map(|param| param.codegen_repr())); + let mut ref_params = Vec::with_capacity(candidate.target.ref_params.len() + 1); + ref_params.push(false); + ref_params.extend(candidate.target.ref_params.iter().copied()); + let operands = [value]; + let call_args = materialize_method_call_args_with_receiver_reg_and_refs( + ctx, + receiver_reg, + &receiver_ty, + &operands, + ¶m_types, + &ref_params, + )?; + let caller_stack_pad_bytes = direct_call_stack_pad_bytes(ctx, call_args.overflow_bytes); + abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + if let Some(slot) = candidate.target.dynamic_slot { + emit_dynamic_instance_method_call(ctx, slot); + } else { + abi::emit_call_label( + ctx.emitter, + &method_symbol(&candidate.target.impl_class, &candidate.target.method_key), + ); + } + abi::emit_release_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(ctx.emitter, call_args.overflow_bytes); + Ok(candidate.target.return_ty.clone()) +} + +/// Normalizes a `__toString()` return into a string result pair. +fn coerce_tostring_return_to_string_result( + ctx: &mut FunctionContext<'_>, + return_ty: &PhpType, +) -> Result<()> { + match return_ty.codegen_repr() { + PhpType::Str => Ok(()), + PhpType::Mixed | PhpType::Union(_) => { + super::cast_loaded_mixed_pointer_to_result(ctx, &PhpType::Str) + } + other => Err(CodegenIrError::unsupported(format!( + "__toString return value for PHP type {:?}", + other + ))), + } +} + +/// Emits a fatal when a boxed Mixed object has no matching public `__toString()`. +fn emit_mixed_missing_tostring_fatal(ctx: &mut FunctionContext<'_>) { + let (label, len) = ctx + .data + .add_string(b"Fatal error: Object could not be converted to string\n"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #2"); // write the object string-cast fatal to stderr + ctx.emitter.adrp("x1", &label); + ctx.emitter.add_lo12("x1", "x1", &label); + ctx.emitter.instruction(&format!("mov x2, #{}", len)); // pass the object string-cast fatal byte length + ctx.emitter.syscall(4); + abi::emit_exit(ctx.emitter, 1); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov edi, 2"); // write the object string-cast fatal to Linux stderr + abi::emit_symbol_address(ctx.emitter, "rsi", &label); + ctx.emitter.instruction(&format!("mov edx, {}", len)); // pass the object string-cast fatal byte length + ctx.emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + ctx.emitter.instruction("syscall"); // emit the object string-cast fatal before exiting + abi::emit_exit(ctx.emitter, 1); + } + } +} + /// Lowers an object string cast through `__toString()` or PHP's conversion fatal. fn lower_object_to_string( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 30a65f2e22..737d973956 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8557,6 +8557,11 @@ foreach ($params as $param) { echo $param->getName() . ":"; echo $type->__toString() . "|"; } +$unionType = (new ReflectionParameter(["EvalReflectTypeStringTarget", "run"], "union"))->getType(); +echo "cast:" . (string)$unionType . "|"; +echo "concat:" . $unionType . "|"; +echo "echo:"; +echo $unionType; '); "#, ); @@ -8567,7 +8572,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|" + "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|cast:int|string|null|concat:int|string|null|echo:int|string|null" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 034b4a0b6f..8319b42e08 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -2489,6 +2489,11 @@ foreach ($params as $param) { echo $param->getName() . ":"; echo $type->__toString() . "|"; } +$unionType = (new ReflectionParameter([ReflectTypeStringTarget::class, "run"], "union"))->getType(); +echo "cast:" . (string)$unionType . "|"; +echo "concat:" . $unionType . "|"; +echo "echo:"; +echo $unionType; "#, ); assert!( @@ -2498,7 +2503,7 @@ foreach ($params as $param) { ); assert_eq!( out.stdout, - "dep:?ReflectTypeStringDep|union:int|string|null|both:ReflectTypeStringLeft&ReflectTypeStringRight|mixed:mixed|items:?array|" + "dep:?ReflectTypeStringDep|union:int|string|null|both:ReflectTypeStringLeft&ReflectTypeStringRight|mixed:mixed|items:?array|cast:int|string|null|concat:int|string|null|echo:int|string|null" ); } From 6ef4a2bc92f00b29dc4f648a059b2f97117f970a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 19:30:17 +0200 Subject: [PATCH 0574/1208] Support Countable dispatch in eval count --- .../builtins/registry/dispatch/arrays.rs | 4 +-- .../src/interpreter/core_builtins.rs | 29 +++++++++++++++---- .../src/interpreter/tests/builtins_json.rs | 26 +++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 1 + docs/php/eval.md | 5 ++++ tests/codegen/eval.rs | 17 +++++++++++ 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs index a390fd6469..71df18a0b8 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs @@ -223,8 +223,8 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( eval_range_result(*start, *end, values)? } "count" => match evaluated_args { - [value] => eval_count_result(*value, None, values)?, - [value, mode] => eval_count_result(*value, Some(*mode), values)?, + [value] => eval_count_result(*value, None, context, values)?, + [value, mode] => eval_count_result(*value, Some(*mode), context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, "iterator_apply" => match evaluated_args { diff --git a/crates/elephc-eval/src/interpreter/core_builtins.rs b/crates/elephc-eval/src/interpreter/core_builtins.rs index e357fb785f..6555ef6894 100644 --- a/crates/elephc-eval/src/interpreter/core_builtins.rs +++ b/crates/elephc-eval/src/interpreter/core_builtins.rs @@ -48,7 +48,7 @@ pub(in crate::interpreter) fn eval_ord_result( values.int(i64::from(bytes.first().copied().unwrap_or(0))) } -/// Evaluates the builtin `count(...)` for one runtime array-like argument. +/// Evaluates the builtin `count(...)` for arrays and `Countable` objects. pub(in crate::interpreter) fn eval_builtin_count( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -58,36 +58,55 @@ pub(in crate::interpreter) fn eval_builtin_count( match args { [value] => { let value = eval_expr(value, context, scope, values)?; - eval_count_result(value, None, values) + eval_count_result(value, None, context, values) } [value, mode] => { let value = eval_expr(value, context, scope, values)?; let mode = eval_expr(mode, context, scope, values)?; - eval_count_result(value, Some(mode), values) + eval_count_result(value, Some(mode), context, values) } _ => Err(EvalStatus::RuntimeFatal), } } -/// Counts an eval array with PHP normal or recursive mode semantics. +/// Counts an eval array or dispatches top-level `Countable` objects. pub(in crate::interpreter) fn eval_count_result( value: RuntimeCellHandle, mode: Option, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let mode = match mode { Some(mode) => eval_int_value(mode, values)?, None => EVAL_COUNT_NORMAL, }; + if !matches!(mode, EVAL_COUNT_NORMAL | EVAL_COUNT_RECURSIVE) { + return Err(EvalStatus::RuntimeFatal); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT + && eval_countable_object_matches(value, context, values)? + { + return eval_method_call_result(value, "count", Vec::new(), context, values); + } let len = match mode { EVAL_COUNT_NORMAL => values.array_len(value)?, EVAL_COUNT_RECURSIVE => eval_count_recursive_len(value, values, &mut Vec::new())?, - _ => return Err(EvalStatus::RuntimeFatal), + _ => unreachable!("count mode was validated before dispatch"), }; let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len) } +/// Returns whether an object value satisfies PHP's `Countable` interface. +fn eval_countable_object_matches( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, "Countable", false, context, values)? + .map_or_else(|| values.object_is_a(value, "Countable", false), Ok) +} + /// Recursively counts nested eval arrays for `count($value, COUNT_RECURSIVE)`. pub(in crate::interpreter) fn eval_count_recursive_len( value: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_json.rs b/crates/elephc-eval/src/interpreter/tests/builtins_json.rs index 85e5de8254..39bd8621a3 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_json.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_json.rs @@ -29,6 +29,32 @@ return defined("COUNT_RECURSIVE");"#, assert_eq!(values.output, "3:3:6:2:3:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval `count()` dispatches to eval-declared `Countable` objects. +#[test] +fn execute_program_counts_eval_countable_objects() { + let program = parse_fragment( + br#"class EvalCountableBag implements Countable { + private int $n; + public function __construct($n) { $this->n = $n; } + public function count(): int { echo "count:"; return $this->n; } +} +$bag = new EvalCountableBag(4); +echo count($bag); echo ":"; +echo count($bag, COUNT_RECURSIVE); echo ":"; +echo call_user_func_array("count", ["value" => $bag]); +return function_exists("count");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "count:4:count:4:count:4"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. #[test] fn execute_program_dispatches_json_encode_builtin() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 60b36a47c1..3857e85731 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -1233,6 +1233,7 @@ impl FakeOps { pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { Ok([ "KnownInterface", + "Countable", "Iterator", "IteratorAggregate", "Traversable", diff --git a/docs/php/eval.md b/docs/php/eval.md index 0e285c67f9..acd31446dd 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -570,6 +570,11 @@ Eval `array_map()` supports one or more source arrays with a string callback or are reindexed, missing source values are padded with `null`, and `array_map(null, ...)` returns zipped row arrays. +Eval `count()` supports normal and recursive array counting and dispatches +top-level eval-declared or generated/AOT objects implementing `Countable` +through their `count()` method. `COUNT_RECURSIVE` still validates as a mode for +`Countable` objects, but the method result is used directly like PHP. + Eval `array_filter()` supports the PHP default omitted/null callback form, filters falsey values, preserves source keys, and supports `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 737d973956..e9c1695e9e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -829,6 +829,23 @@ echo defined("COUNT_RECURSIVE") ? "C" : "bad";'); assert_eq!(out, "4:2:3:6:2:3:C"); } +/// Verifies eval `count()` dispatches through `Countable` for generated/AOT objects. +#[test] +fn test_eval_counts_aot_countable_objects() { + let out = compile_and_run( + r#"n = $n; } + public function count(): int { echo "count:"; return $this->n; } +} +$bag = new EvalAotCountableBag(5); +eval('echo count($bag); echo ":"; echo count($bag, COUNT_RECURSIVE); echo ":"; echo call_user_func_array("count", ["value" => $bag]);'); +"#, + ); + assert_eq!(out, "count:5:count:5:count:5"); +} + /// Verifies eval `json_encode()` serializes scalar, indexed, and associative values. #[test] fn test_eval_dispatches_json_encode_builtin_call() { From fe19bfa4375283d7a90f429772129bef656146b0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 19:33:21 +0200 Subject: [PATCH 0575/1208] Cover relative eval object construction --- .../src/interpreter/tests/classes.rs | 39 +++++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 29 ++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index a073bc3762..0fcce6241d 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -34,6 +34,45 @@ return $user->label();"#, assert_eq!(values.get(result), FakeValue::String("7:Ada".to_string())); } +/// Verifies `new self/static/parent` resolve inside eval-declared methods. +#[test] +fn execute_program_constructs_relative_class_names_from_eval_methods() { + let program = parse_fragment( + br#"class EvalRelativeFactoryBase { + public string $label; + public function __construct($label = "base") { $this->label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class EvalRelativeFactoryChild extends EvalRelativeFactoryBase { + public function parentFactory() { return new parent("parent"); } +} +$child = new EvalRelativeFactoryChild("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label; +return $self instanceof EvalRelativeFactoryBase + && !($self instanceof EvalRelativeFactoryChild) + && $static instanceof EvalRelativeFactoryChild + && $parent instanceof EvalRelativeFactoryBase + && !($parent instanceof EvalRelativeFactoryChild);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRelativeFactoryBase:self:EvalRelativeFactoryChild:static:EvalRelativeFactoryBase:parent" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies by-reference promoted properties stay aliased to caller variables. #[test] fn execute_program_aliases_by_reference_promoted_variable_properties() { diff --git a/docs/php/eval.md b/docs/php/eval.md index acd31446dd..e7981d073b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -73,7 +73,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar or null default arguments for supported public scalar/Mixed constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar or null default arguments for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar or null default arguments for supported public scalar/Mixed/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e9c1695e9e..0abaf83764 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4757,6 +4757,35 @@ echo eval('$box = new EvalDynamicNewNamedCtor(right: "F", left: "E"); return $bo assert_eq!(out, "EF"); } +/// Verifies eval-declared methods resolve `new self/static/parent` through the bridge. +#[test] +fn test_eval_declared_methods_construct_relative_class_names() { + let out = compile_and_run( + r#"label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class EvalRelativeFactoryChild extends EvalRelativeFactoryBase { + public function parentFactory() { return new parent("parent"); } +} +$child = new EvalRelativeFactoryChild("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label;'); +"#, + ); + assert_eq!( + out, + "EvalRelativeFactoryBase:self:EvalRelativeFactoryChild:static:EvalRelativeFactoryBase:parent" + ); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From 8429c7e27d1ad44e71d72a02c414bdddc6a534e6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 19:39:32 +0200 Subject: [PATCH 0576/1208] Support ArrayAccess reads in eval --- .../src/interpreter/dynamic_functions.rs | 5 +++- .../src/interpreter/expressions.rs | 28 ++++++++++++++++++- .../src/interpreter/tests/classes.rs | 27 ++++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 1 + docs/php/eval.md | 2 +- tests/codegen/eval.rs | 21 ++++++++++++++ 6 files changed, 81 insertions(+), 3 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 27ce0703de..39999d43a7 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -109,7 +109,10 @@ fn eval_call_arg_value( let array = visible_scope_cell(context, caller_scope, array_name) .map_or_else(|| values.null(), Ok)?; let index = eval_expr(index, context, caller_scope, values)?; - let value = values.array_get(array, index)?; + let value = eval_array_get_result(array, index, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return Ok((value, None)); + } Ok(( value, Some(EvalReferenceTarget::ArrayElement { diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 4462c37676..972727c7df 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -32,7 +32,7 @@ pub(in crate::interpreter) fn eval_expr( EvalExpr::ArrayGet { array, index } => { let array = eval_expr(array, context, scope, values)?; let index = eval_expr(index, context, scope, values)?; - values.array_get(array, index) + eval_array_get_result(array, index, context, values) } EvalExpr::Call { name, args } => eval_call(name, args, context, scope, values), EvalExpr::Cast { target, expr } => eval_cast_expr(target, expr, context, scope, values), @@ -247,6 +247,32 @@ pub(in crate::interpreter) fn eval_expr( } } +/// Reads an array element or dispatches `ArrayAccess::offsetGet()` for objects. +pub(in crate::interpreter) fn eval_array_get_result( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(array)? != EVAL_TAG_OBJECT { + return values.array_get(array, index); + } + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_method_call_result(array, "offsetGet", vec![index], context, values) +} + +/// Returns whether an object value satisfies PHP's `ArrayAccess` interface. +fn eval_array_access_object_matches( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, "ArrayAccess", false, context, values)? + .map_or_else(|| values.object_is_a(value, "ArrayAccess", false), Ok) +} + /// Evaluates one PHP scalar cast expression through the runtime conversion hooks. fn eval_cast_expr( target: &EvalCastType, diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 0fcce6241d..f2a930d663 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -73,6 +73,33 @@ return $self instanceof EvalRelativeFactoryBase assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies array reads on eval-declared `ArrayAccess` objects dispatch `offsetGet()`. +#[test] +fn execute_program_reads_eval_array_access_objects() { + let program = parse_fragment( + br#"class EvalArrayAccessBox implements ArrayAccess { + public function offsetExists(mixed $offset): bool { return true; } + public function offsetGet(mixed $offset): mixed { + echo "get:" . $offset . ":"; + return "v" . $offset; + } + public function offsetSet(mixed $offset, mixed $value): void {} + public function offsetUnset(mixed $offset): void {} +} +$box = new EvalArrayAccessBox(); +echo $box["x"]; +return $box["y"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "get:x:vxget:y:"); + assert_eq!(values.get(result), FakeValue::String("vy".to_string())); +} + /// Verifies by-reference promoted properties stay aliased to caller variables. #[test] fn execute_program_aliases_by_reference_promoted_variable_properties() { diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs index 3857e85731..a9330d01c6 100644 --- a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs @@ -1233,6 +1233,7 @@ impl FakeOps { pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { Ok([ "KnownInterface", + "ArrayAccess", "Countable", "Iterator", "IteratorAggregate", diff --git a/docs/php/eval.md b/docs/php/eval.md index e7981d073b..7948dcba1a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | -| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, and string-key reads/writes. | +| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and read access on eval-declared or generated/AOT `ArrayAccess` objects through `offsetGet()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar or null default arguments for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0abaf83764..bd41aff876 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -846,6 +846,27 @@ eval('echo count($bag); echo ":"; echo count($bag, COUNT_RECURSIVE); echo ":"; e assert_eq!(out, "count:5:count:5:count:5"); } +/// Verifies eval array reads dispatch through `ArrayAccess::offsetGet()` on AOT objects. +#[test] +fn test_eval_reads_aot_array_access_objects() { + let out = compile_and_run( + r#" Date: Mon, 22 Jun 2026 19:51:39 +0200 Subject: [PATCH 0577/1208] Support ArrayAccess writes in eval --- .../src/interpreter/builtins/symbols.rs | 48 +++++ .../src/interpreter/expressions.rs | 2 +- .../elephc-eval/src/interpreter/statements.rs | 179 +++++++++++++----- .../src/interpreter/tests/classes.rs | 35 +++- docs/php/eval.md | 2 +- tests/codegen/eval.rs | 36 +++- 6 files changed, 240 insertions(+), 62 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-eval/src/interpreter/builtins/symbols.rs index f0fe95e51e..d54b0e2e5c 100644 --- a/crates/elephc-eval/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-eval/src/interpreter/builtins/symbols.rs @@ -648,6 +648,15 @@ pub(in crate::interpreter) fn eval_empty_arg( let value = eval_property_get_result(object, property, context, values)?; return Ok(!values.truthy(value)?); } + if let EvalExpr::ArrayGet { array, index } = arg { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return eval_array_access_empty_result(array, index, context, values); + } + let value = values.array_get(array, index)?; + return Ok(!values.truthy(value)?); + } let value = eval_expr(arg, context, scope, values)?; Ok(!values.truthy(value)?) } @@ -669,10 +678,49 @@ pub(in crate::interpreter) fn eval_isset_arg( let object = eval_expr(object, context, scope, values)?; return eval_property_isset_result(object, property, context, values); } + if let EvalExpr::ArrayGet { array, index } = arg { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return eval_array_access_isset_result(array, index, context, values); + } + let value = values.array_get(array, index)?; + return Ok(!values.is_null(value)?); + } let value = eval_expr(arg, context, scope, values)?; Ok(!values.is_null(value)?) } +/// Evaluates `empty($object[$key])` through `ArrayAccess::offsetExists()` and `offsetGet()`. +fn eval_array_access_empty_result( + object: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_array_access_isset_result(object, index, context, values)? { + return Ok(true); + } + let value = eval_array_get_result(object, index, context, values)?; + Ok(!values.truthy(value)?) +} + +/// Evaluates `isset($object[$key])` through `ArrayAccess::offsetExists()`. +fn eval_array_access_isset_result( + object: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_method_call_result(object, "offsetExists", vec![index], context, values)?; + let exists = values.truthy(result)?; + values.release(result)?; + Ok(exists) +} + /// Returns true when a PHP function name is visible to eval builtin probes. pub(in crate::interpreter) fn eval_function_probe_exists( context: &ElephcEvalContext, diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-eval/src/interpreter/expressions.rs index 972727c7df..99242ff59a 100644 --- a/crates/elephc-eval/src/interpreter/expressions.rs +++ b/crates/elephc-eval/src/interpreter/expressions.rs @@ -264,7 +264,7 @@ pub(in crate::interpreter) fn eval_array_get_result( } /// Returns whether an object value satisfies PHP's `ArrayAccess` interface. -fn eval_array_access_object_matches( +pub(in crate::interpreter) fn eval_array_access_object_matches( value: RuntimeCellHandle, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index c6ad3e3c10..bc528104e5 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -35,51 +35,11 @@ pub(in crate::interpreter) fn execute_stmt( ) -> Result { match stmt { EvalStmt::ArrayAppendVar { name, value } => { - let mut ownership = ScopeCellOwnership::Owned; - let array = if let Some(existing) = - scope_entry(context, scope, name).filter(|entry| entry.flags().is_visible()) - { - if values.is_array_like(existing.cell())? { - let tag = values.type_tag(existing.cell())?; - if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::UnsupportedConstruct); - } - ownership = existing.flags().ownership; - existing.cell() - } else { - values.array_new(1)? - } - } else { - values.array_new(1)? - }; - let index = eval_array_append_key(array, values)?; - let value = eval_expr(value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { - values.release(replaced)?; - } + eval_array_append_var_stmt(name, value, context, scope, values)?; Ok(EvalControl::None) } EvalStmt::ArraySetVar { name, index, value } => { - let mut ownership = ScopeCellOwnership::Owned; - let array = if let Some(existing) = - scope_entry(context, scope, name).filter(|entry| entry.flags().is_visible()) - { - if values.is_array_like(existing.cell())? { - ownership = existing.flags().ownership; - existing.cell() - } else { - values.array_new(1)? - } - } else { - values.array_new(1)? - }; - let index = eval_expr(index, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { - values.release(replaced)?; - } + eval_array_set_var_stmt(name, index, value, context, scope, values)?; Ok(EvalControl::None) } EvalStmt::Break => Ok(EvalControl::Break), @@ -284,6 +244,132 @@ pub(in crate::interpreter) fn execute_stmt( } } +/// Executes `$var[] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. +fn eval_array_append_var_stmt( + name: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + if let Some((object, _)) = existing { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return eval_non_object_array_append_var_stmt( + name, value, existing, context, scope, values, + ); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = + eval_method_call_result(object, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + + eval_non_object_array_append_var_stmt(name, value, existing, context, scope, values) +} + +/// Executes the non-object `$var[] = value` path with the existing array semantics. +fn eval_non_object_array_append_var_stmt( + name: &str, + value: &EvalExpr, + existing: Option<(RuntimeCellHandle, ScopeCellOwnership)>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some((cell, flags_ownership)) = existing { + if values.is_array_like(cell)? { + let tag = values.type_tag(cell)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + ownership = flags_ownership; + cell + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { + values.release(replaced)?; + } + Ok(()) +} + +/// Executes `$var[index] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. +fn eval_array_set_var_stmt( + name: &str, + index: &EvalExpr, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + if let Some((object, _)) = existing { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return eval_non_object_array_set_var_stmt( + name, index, value, existing, context, scope, values, + ); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = + eval_method_call_result(object, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + + eval_non_object_array_set_var_stmt(name, index, value, existing, context, scope, values) +} + +/// Executes the non-object `$var[index] = value` path with the existing array semantics. +fn eval_non_object_array_set_var_stmt( + name: &str, + index: &EvalExpr, + value: &EvalExpr, + existing: Option<(RuntimeCellHandle, ScopeCellOwnership)>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some((cell, flags_ownership)) = existing { + if values.is_array_like(cell)? { + ownership = flags_ownership; + cell + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_expr(index, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { + values.release(replaced)?; + } + Ok(()) +} + /// Executes an eval `try` body and handles supported `catch` clauses. pub(in crate::interpreter) fn execute_try_stmt( body: &[EvalStmt], @@ -3228,12 +3314,9 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } - if let Some(result) = eval_reflection_type_to_string_result( - object, - method_name, - evaluated_args.clone(), - values, - )? { + if let Some(result) = + eval_reflection_type_to_string_result(object, method_name, evaluated_args.clone(), values)? + { return Ok(result); } if let Some(result) = eval_reflection_class_implements_interface_result( diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index f2a930d663..8609dad64c 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -73,21 +73,41 @@ return $self instanceof EvalRelativeFactoryBase assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies array reads on eval-declared `ArrayAccess` objects dispatch `offsetGet()`. +/// Verifies eval-declared `ArrayAccess` objects dispatch reads, writes, append, and probes. #[test] -fn execute_program_reads_eval_array_access_objects() { +fn execute_program_dispatches_eval_array_access_objects() { let program = parse_fragment( br#"class EvalArrayAccessBox implements ArrayAccess { - public function offsetExists(mixed $offset): bool { return true; } + public function offsetExists(mixed $offset): bool { + echo "exists:" . $offset . ":"; + if ($offset === "missing") { + return false; + } + return true; + } public function offsetGet(mixed $offset): mixed { echo "get:" . $offset . ":"; + if ($offset === "empty") { + return ""; + } return "v" . $offset; } - public function offsetSet(mixed $offset, mixed $value): void {} + public function offsetSet(mixed $offset, mixed $value): void { + if ($offset === null) { + echo "set:null:" . $value . ":"; + } else { + echo "set:" . $offset . ":" . $value . ":"; + } + } public function offsetUnset(mixed $offset): void {} } $box = new EvalArrayAccessBox(); -echo $box["x"]; +$box["x"] = "1"; +$box[] = "tail"; +if (isset($box["x"])) { echo "I:"; } else { echo "i:"; } +if (isset($box["missing"])) { echo "M:"; } else { echo "m:"; } +if (empty($box["empty"])) { echo "E:"; } else { echo "e:"; } +if (empty($box["missing"])) { echo "N:"; } else { echo "n:"; } return $box["y"];"#, ) .expect("parse eval fragment"); @@ -96,7 +116,10 @@ return $box["y"];"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "get:x:vxget:y:"); + assert_eq!( + values.output, + "set:x:1:set:null:tail:exists:x:I:exists:missing:m:exists:empty:get:empty:E:exists:missing:N:get:y:" + ); assert_eq!(values.get(result), FakeValue::String("vy".to_string())); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 7948dcba1a..b106281f12 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | -| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and read access on eval-declared or generated/AOT `ArrayAccess` objects through `offsetGet()`. | +| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, and `empty()` through `offsetGet()`, `offsetSet()`, and `offsetExists()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar or null default arguments for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bd41aff876..eb6dfb8ae5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -846,25 +846,49 @@ eval('echo count($bag); echo ":"; echo count($bag, COUNT_RECURSIVE); echo ":"; e assert_eq!(out, "count:5:count:5:count:5"); } -/// Verifies eval array reads dispatch through `ArrayAccess::offsetGet()` on AOT objects. +/// Verifies eval dispatches `ArrayAccess` reads, writes, append, and probes on AOT objects. #[test] -fn test_eval_reads_aot_array_access_objects() { +fn test_eval_dispatches_aot_array_access_objects() { let out = compile_and_run( r#" Date: Mon, 22 Jun 2026 19:57:29 +0200 Subject: [PATCH 0578/1208] Support ArrayAccess unset in eval --- crates/elephc-eval/src/eval_ir.rs | 4 +++ .../src/interpreter/dynamic_functions.rs | 1 + .../elephc-eval/src/interpreter/statements.rs | 25 +++++++++++++++++++ .../src/interpreter/tests/classes.rs | 9 ++++--- crates/elephc-eval/src/parser/statements.rs | 10 +++++++- .../src/parser/tests/exceptions_control.rs | 9 +++++-- docs/php/eval.md | 2 +- tests/codegen/eval.rs | 9 ++++--- 8 files changed, 59 insertions(+), 10 deletions(-) diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-eval/src/eval_ir.rs index f019d574c2..d95563213c 100644 --- a/crates/elephc-eval/src/eval_ir.rs +++ b/crates/elephc-eval/src/eval_ir.rs @@ -136,6 +136,10 @@ pub enum EvalStmt { catches: Vec, finally_body: Vec, }, + UnsetArrayElement { + array: EvalExpr, + index: EvalExpr, + }, UnsetProperty { object: EvalExpr, property: String, diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-eval/src/interpreter/dynamic_functions.rs index 39999d43a7..9a33c99ad3 100644 --- a/crates/elephc-eval/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-eval/src/interpreter/dynamic_functions.rs @@ -1050,6 +1050,7 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has | EvalStmt::StoreVar { .. } | EvalStmt::Throw(_) | EvalStmt::TraitDecl(_) + | EvalStmt::UnsetArrayElement { .. } | EvalStmt::UnsetProperty { .. } | EvalStmt::UnsetVar { .. } => {} } diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index bc528104e5..6f527269f1 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -211,6 +211,10 @@ pub(in crate::interpreter) fn execute_stmt( catches, finally_body, } => execute_try_stmt(body, catches, finally_body, context, scope, values), + EvalStmt::UnsetArrayElement { array, index } => { + eval_array_unset_element_stmt(array, index, context, scope, values)?; + Ok(EvalControl::None) + } EvalStmt::UnsetProperty { object, property } => { let object = eval_expr(object, context, scope, values)?; eval_property_unset_result(object, property, context, values)?; @@ -244,6 +248,27 @@ pub(in crate::interpreter) fn execute_stmt( } } +/// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. +fn eval_array_unset_element_stmt( + array: &EvalExpr, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::UnsupportedConstruct); + } + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_method_call_result(array, "offsetUnset", vec![index], context, values)?; + values.release(result)?; + Ok(()) +} + /// Executes `$var[] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. fn eval_array_append_var_stmt( name: &str, diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-eval/src/interpreter/tests/classes.rs index 8609dad64c..caf347ced1 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-eval/src/interpreter/tests/classes.rs @@ -73,7 +73,7 @@ return $self instanceof EvalRelativeFactoryBase assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval-declared `ArrayAccess` objects dispatch reads, writes, append, and probes. +/// Verifies eval-declared `ArrayAccess` objects dispatch reads, writes, append, probes, and unset. #[test] fn execute_program_dispatches_eval_array_access_objects() { let program = parse_fragment( @@ -99,11 +99,14 @@ fn execute_program_dispatches_eval_array_access_objects() { echo "set:" . $offset . ":" . $value . ":"; } } - public function offsetUnset(mixed $offset): void {} + public function offsetUnset(mixed $offset): void { + echo "unset:" . $offset . ":"; + } } $box = new EvalArrayAccessBox(); $box["x"] = "1"; $box[] = "tail"; +unset($box["drop"]); if (isset($box["x"])) { echo "I:"; } else { echo "i:"; } if (isset($box["missing"])) { echo "M:"; } else { echo "m:"; } if (empty($box["empty"])) { echo "E:"; } else { echo "e:"; } @@ -118,7 +121,7 @@ return $box["y"];"#, assert_eq!( values.output, - "set:x:1:set:null:tail:exists:x:I:exists:missing:m:exists:empty:get:empty:E:exists:missing:N:get:y:" + "set:x:1:set:null:tail:unset:drop:exists:x:I:exists:missing:m:exists:empty:get:empty:E:exists:missing:N:get:y:" ); assert_eq!(values.get(result), FakeValue::String("vy".to_string())); } diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-eval/src/parser/statements.rs index 025a3ae8dc..11767adbdd 100644 --- a/crates/elephc-eval/src/parser/statements.rs +++ b/crates/elephc-eval/src/parser/statements.rs @@ -2504,7 +2504,7 @@ impl Parser { Ok(body) } - /// Parses `unset($name[, ...]);` with variable and object-property operands. + /// Parses `unset($name[, ...]);` with variable, array-access, and property operands. pub(super) fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { self.advance(); self.expect(TokenKind::LParen)?; @@ -2512,6 +2512,10 @@ impl Parser { loop { let target = self.parse_expr()?; let stmt = match target { + EvalExpr::ArrayGet { array, index } => EvalStmt::UnsetArrayElement { + array: *array, + index: *index, + }, EvalExpr::LoadVar(name) => EvalStmt::UnsetVar { name }, EvalExpr::PropertyGet { object, property } => EvalStmt::UnsetProperty { object: *object, @@ -2718,6 +2722,10 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { | EvalStmt::ReferenceAssign { .. } | EvalStmt::TraitDecl(_) | EvalStmt::UnsetVar { .. } => false, + EvalStmt::UnsetArrayElement { array, index } => { + eval_expr_uses_this_property(array, property_name) + || eval_expr_uses_this_property(index, property_name) + } EvalStmt::DoWhile { body, condition } | EvalStmt::While { condition, body } => { eval_expr_uses_this_property(condition, property_name) || eval_stmt_list_uses_this_property(body, property_name) diff --git a/crates/elephc-eval/src/parser/tests/exceptions_control.rs b/crates/elephc-eval/src/parser/tests/exceptions_control.rs index bf96242ff6..89ab1a70e4 100644 --- a/crates/elephc-eval/src/parser/tests/exceptions_control.rs +++ b/crates/elephc-eval/src/parser/tests/exceptions_control.rs @@ -251,10 +251,11 @@ fn parse_fragment_accepts_eval_finally_source() { }] ); } -/// Verifies unset fragments expand variable and object-property operands. +/// Verifies unset fragments expand variable, array-access, and object-property operands. #[test] fn parse_fragment_accepts_unset_source() { - let program = parse_fragment(b"unset($x, $this->name, $y);").expect("fragment should parse"); + let program = parse_fragment(br#"unset($x, $this->name, $box["k"], $y);"#) + .expect("fragment should parse"); assert_eq!( program.statements(), &[ @@ -265,6 +266,10 @@ fn parse_fragment_accepts_unset_source() { object: EvalExpr::LoadVar("this".to_string()), property: "name".to_string(), }, + EvalStmt::UnsetArrayElement { + array: EvalExpr::LoadVar("box".to_string()), + index: EvalExpr::Const(EvalConst::String("k".to_string())), + }, EvalStmt::UnsetVar { name: "y".to_string() }, diff --git a/docs/php/eval.md b/docs/php/eval.md index b106281f12..51c9aa8968 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | -| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, and `empty()` through `offsetGet()`, `offsetSet()`, and `offsetExists()`. | +| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar or null default arguments for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eb6dfb8ae5..6815f3c052 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -846,7 +846,7 @@ eval('echo count($bag); echo ":"; echo count($bag, COUNT_RECURSIVE); echo ":"; e assert_eq!(out, "count:5:count:5:count:5"); } -/// Verifies eval dispatches `ArrayAccess` reads, writes, append, and probes on AOT objects. +/// Verifies eval dispatches `ArrayAccess` reads, writes, append, probes, and unset on AOT objects. #[test] fn test_eval_dispatches_aot_array_access_objects() { let out = compile_and_run( @@ -873,11 +873,14 @@ class EvalAotArrayAccessBox implements ArrayAccess { echo "set:" . $offset . ":" . $value . ":"; } } - public function offsetUnset(mixed $offset): void {} + public function offsetUnset(mixed $offset): void { + echo "unset:" . $offset . ":"; + } } $box = new EvalAotArrayAccessBox(); eval('$box["x"] = "1"; $box[] = "tail"; +unset($box["drop"]); if (isset($box["x"])) { echo "I:"; } else { echo "i:"; } if (isset($box["missing"])) { echo "M:"; } else { echo "m:"; } if (empty($box["empty"])) { echo "E:"; } else { echo "e:"; } @@ -887,7 +890,7 @@ echo $box["y"];'); ); assert_eq!( out, - "set:x:1:set:null:tail:exists:x:I:exists:missing:m:exists:empty:get:empty:E:exists:missing:N:get:y:vy" + "set:x:1:set:null:tail:unset:drop:exists:x:I:exists:missing:m:exists:empty:get:empty:E:exists:missing:N:get:y:vy" ); } From 7b32a8027230a4bdd4943132132ee312deeae7ff Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 20:04:56 +0200 Subject: [PATCH 0579/1208] Support Traversable is_iterable in eval --- .../builtins/registry/dispatch/scalars.rs | 2 +- .../src/interpreter/builtins/scalars/types.rs | 29 +++++++++++++++++-- .../src/interpreter/tests/builtins_scalars.rs | 16 ++++++++-- docs/php/eval.md | 2 +- tests/codegen/eval.rs | 17 ++++++++++- 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs index accc9df1e7..0afec7d3f9 100644 --- a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs +++ b/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs @@ -103,7 +103,7 @@ pub(in crate::interpreter) fn eval_scalars_builtin_with_values( let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_type_predicate_result(name, *value, values)? + eval_type_predicate_result(name, *value, context, values)? } _ => return Ok(None), }; diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs b/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs index 7b99efedc1..adac645226 100644 --- a/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs @@ -226,13 +226,14 @@ pub(in crate::interpreter) fn eval_builtin_type_predicate( return Err(EvalStatus::RuntimeFatal); }; let value = eval_expr(value, context, scope, values)?; - eval_type_predicate_result(name, value, values) + eval_type_predicate_result(name, value, context, values) } /// Converts a concrete runtime tag into a PHP `is_*` predicate result. pub(in crate::interpreter) fn eval_type_predicate_result( name: &str, value: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let tag = values.type_tag(value)?; @@ -242,7 +243,8 @@ pub(in crate::interpreter) fn eval_type_predicate_result( "is_string" => tag == EVAL_TAG_STRING, "is_bool" => tag == EVAL_TAG_BOOL, "is_null" => tag == EVAL_TAG_NULL, - "is_array" | "is_iterable" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_array" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), + "is_iterable" => eval_is_iterable_value(tag, value, context, values)?, "is_object" => tag == EVAL_TAG_OBJECT, "is_resource" => tag == EVAL_TAG_RESOURCE, "is_nan" => eval_float_value(value, values)?.is_nan(), @@ -258,6 +260,29 @@ pub(in crate::interpreter) fn eval_type_predicate_result( values.bool_value(result) } +/// Returns PHP's `is_iterable()` result for arrays and Traversable-compatible objects. +fn eval_is_iterable_value( + tag: u64, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Ok(true); + } + if tag != EVAL_TAG_OBJECT { + return Ok(false); + } + for target in ["Traversable", "Iterator", "IteratorAggregate"] { + if dynamic_object_is_a(value, target, false, context, values)? + .map_or_else(|| values.object_is_a(value, target, false), Ok)? + { + return Ok(true); + } + } + Ok(false) +} + /// Matches the static backend's legacy ASCII numeric-string scan. pub(in crate::interpreter) fn eval_is_numeric_string(bytes: &[u8]) -> bool { if bytes.is_empty() { diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs b/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs index 3e672a6f11..b0655f59dc 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs +++ b/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs @@ -14,11 +14,21 @@ use super::support::*; #[test] fn execute_program_dispatches_type_predicate_builtins() { let program = parse_fragment( - br#"echo is_int(1); echo is_integer(1); echo is_long(1); + br#"class EvalPredicateIterator implements Iterator { + public function current() { return null; } + public function key() { return null; } + public function next() {} + public function valid() { return false; } + public function rewind() {} +} +$iterator = new EvalPredicateIterator(); +echo is_int(1); echo is_integer(1); echo is_long(1); echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); echo is_string("x"); echo is_bool(false); echo is_null(null); echo is_array([1]); echo is_array(["a" => 1]); echo is_iterable([1]); echo is_iterable(["a" => 1]); +echo is_iterable($iterator) ? "I" : "bad"; +echo is_iterable($object) ? "bad" : "s"; echo is_iterable(1) ? "bad" : "T"; echo is_array(1) ? "bad" : "ok"; echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); @@ -37,6 +47,8 @@ echo ":"; echo call_user_func("is_string", "x"); echo call_user_func_array("is_array", [[1]]); echo call_user_func("is_numeric", "12"); echo call_user_func("is_iterable", [1]); +echo call_user_func("is_iterable", $iterator) ? "C" : "bad"; +echo call_user_func_array("is_iterable", ["value" => $iterator]) ? "D" : "bad"; echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; echo call_user_func("is_object", $object) ? "O" : "bad"; echo call_user_func_array("is_object", ["value" => 1]) ? "bad" : "o"; @@ -55,7 +67,7 @@ return function_exists("is_infinite");"#, assert_eq!( values.output, - "1111111111111Tok11111NBROoNIiFf:1111tOo1111111" + "1111111111111IsTok11111NBROoNIiFf:1111CDtOo1111111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 51c9aa8968..baa6e60b7d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -559,7 +559,7 @@ where listed below unless a note says otherwise. | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | | Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | -| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_resource()` | | Debug output | `print_r()`, `var_dump()` | | Constants | `define()`, `defined()` | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6815f3c052..006bbca794 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3168,11 +3168,21 @@ fn test_eval_dispatches_type_predicate_builtin_calls() { let out = compile_and_run( r#"i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } + public function valid(): bool { return $this->i < 0; } + public function rewind(): void { $this->i = 0; } +} +$iterator = new EvalAotPredicateIterator(); eval('echo is_int(1); echo is_integer(1); echo is_long(1); echo is_float(1.5); echo is_double(1.5); echo is_real(1.5); echo is_string("x"); echo is_bool(false); echo is_null(null); echo is_array([1]); echo is_array(["a" => 1]); echo is_iterable([1]); echo is_iterable(["a" => 1]); +echo is_iterable($iterator) ? "I" : "bad"; echo is_iterable(1) ? "bad" : "T"; echo is_array(1) ? "bad" : "ok"; echo is_numeric(42); echo is_numeric(3.14); echo is_numeric("42"); @@ -3194,6 +3204,8 @@ echo call_user_func("is_string", "x"); echo call_user_func_array("is_array", [[1]]); echo call_user_func("is_numeric", "12"); echo call_user_func("is_iterable", [1]); +echo call_user_func("is_iterable", $iterator) ? "C" : "bad"; +echo call_user_func_array("is_iterable", ["value" => $iterator]) ? "D" : "bad"; echo call_user_func_array("is_iterable", ["value" => 1]) ? "bad" : "t"; echo call_user_func("is_resource", $h); echo call_user_func_array("is_resource", [$h]); @@ -3205,7 +3217,10 @@ echo function_exists("is_double"); echo function_exists("is_numeric"); echo func echo function_exists("is_nan"); echo function_exists("is_finite"); echo function_exists("is_iterable"); echo function_exists("is_infinite");'); "#, ); - assert_eq!(out, "1111111111111Tok11111NBROoNIiFfH:1111t11OoNF11111111"); + assert_eq!( + out, + "1111111111111ITok11111NBROoNIiFfH:1111CDt11OoNF11111111" + ); } /// Verifies eval resource introspection builtins inspect boxed runtime resources. From ff94948fea5245117757fd932605351944728e06 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 22 Jun 2026 20:23:25 +0200 Subject: [PATCH 0580/1208] Support Traversable foreach in eval --- .../src/interpreter/return_values.rs | 29 +++- .../elephc-eval/src/interpreter/statements.rs | 133 +++++++++++++++++- .../src/interpreter/tests/control_flow.rs | 37 +++++ docs/php/eval.md | 5 +- tests/codegen/eval.rs | 33 +++++ 5 files changed, 232 insertions(+), 5 deletions(-) diff --git a/crates/elephc-eval/src/interpreter/return_values.rs b/crates/elephc-eval/src/interpreter/return_values.rs index 9a9d8fc1f3..258c3ce427 100644 --- a/crates/elephc-eval/src/interpreter/return_values.rs +++ b/crates/elephc-eval/src/interpreter/return_values.rs @@ -253,9 +253,34 @@ fn eval_declared_return_class_accepts( )?; let identity = values.object_identity(value)?; if let Some(class) = context.dynamic_object_class(identity) { - return Ok(context.class_is_a(class.name(), &target, false)); + return Ok(eval_declared_dynamic_object_is_a( + class.name(), + &target, + context, + )); + } + if values.object_is_a(value, &target, false)? { + return Ok(true); + } + if target.eq_ignore_ascii_case("Traversable") { + return Ok(values.object_is_a(value, "Iterator", false)? + || values.object_is_a(value, "IteratorAggregate", false)?); + } + Ok(false) +} + +/// Returns whether one eval-created object class satisfies a declared return target. +fn eval_declared_dynamic_object_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + if context.class_is_a(class_name, target, false) { + return true; } - values.object_is_a(value, &target, false) + target.eq_ignore_ascii_case("Traversable") + && (context.class_is_a(class_name, "Iterator", false) + || context.class_is_a(class_name, "IteratorAggregate", false)) } /// Resolves class keywords and aliases in a declared return type atom. diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-eval/src/interpreter/statements.rs index 6f527269f1..737a0a4bac 100644 --- a/crates/elephc-eval/src/interpreter/statements.rs +++ b/crates/elephc-eval/src/interpreter/statements.rs @@ -4674,7 +4674,7 @@ pub(in crate::interpreter) fn execute_for_stmt( Ok(EvalControl::None) } -/// Executes a PHP `foreach` loop over eval array values. +/// Executes a PHP `foreach` loop over eval array and Traversable object values. pub(in crate::interpreter) fn execute_foreach_stmt( array: &EvalExpr, key_name: Option<&str>, @@ -4685,6 +4685,27 @@ pub(in crate::interpreter) fn execute_foreach_stmt( values: &mut impl RuntimeValueOps, ) -> Result { let array = eval_expr(array, context, scope, values)?; + match values.type_tag(array)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { + execute_foreach_array_stmt(array, key_name, value_name, body, context, scope, values) + } + EVAL_TAG_OBJECT => { + execute_foreach_object_stmt(array, key_name, value_name, body, context, scope, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Executes `foreach` over a PHP array value using insertion-order runtime hooks. +fn execute_foreach_array_stmt( + array: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { let len = values.array_len(array)?; for index in 0..len { let key = values.array_iter_key(array, index)?; @@ -4722,6 +4743,116 @@ pub(in crate::interpreter) fn execute_foreach_stmt( Ok(EvalControl::None) } +/// Executes `foreach` over an Iterator or IteratorAggregate object. +fn execute_foreach_object_stmt( + object: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_foreach_object_is_a(object, "Iterator", context, values)? { + return execute_foreach_iterator_stmt( + object, key_name, value_name, body, context, scope, values, + ); + } + if eval_foreach_object_is_a(object, "IteratorAggregate", context, values)? { + let iterator = eval_method_call_result(object, "getIterator", Vec::new(), context, values)?; + return match values.type_tag(iterator)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => execute_foreach_array_stmt( + iterator, key_name, value_name, body, context, scope, values, + ), + EVAL_TAG_OBJECT if eval_foreach_object_is_a(iterator, "Iterator", context, values)? => { + execute_foreach_iterator_stmt( + iterator, key_name, value_name, body, context, scope, values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + }; + } + Err(EvalStatus::RuntimeFatal) +} + +/// Drives one Iterator object through PHP's `foreach` method-call sequence. +fn execute_foreach_iterator_stmt( + iterator: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_method_call_result(iterator, "rewind", Vec::new(), context, values)?; + values.release(result)?; + loop { + let valid = eval_method_call_result(iterator, "valid", Vec::new(), context, values)?; + let is_valid = values.truthy(valid)?; + values.release(valid)?; + if !is_valid { + return Ok(EvalControl::None); + } + + let value = eval_method_call_result(iterator, "current", Vec::new(), context, values)?; + let key = if key_name.is_some() { + Some(eval_method_call_result( + iterator, + "key", + Vec::new(), + context, + values, + )?) + } else { + None + }; + if let Some((key_name, key)) = key_name.zip(key) { + for replaced in set_scope_cell( + context, + scope, + key_name.to_string(), + key, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } + for replaced in set_scope_cell( + context, + scope, + value_name.to_string(), + value, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => { + let result = + eval_method_call_result(iterator, "next", Vec::new(), context, values)?; + values.release(result)?; + } + EvalControl::Break => return Ok(EvalControl::None), + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } +} + +/// Returns whether a foreach object satisfies one iterator interface. +fn eval_foreach_object_is_a( + object: RuntimeCellHandle, + target: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(object, target, false, context, values)? + .map_or_else(|| values.object_is_a(object, target, false), Ok) +} + /// Returns PHP's next automatic integer key for `$array[]` append writes. pub(in crate::interpreter) fn eval_array_append_key( array: RuntimeCellHandle, diff --git a/crates/elephc-eval/src/interpreter/tests/control_flow.rs b/crates/elephc-eval/src/interpreter/tests/control_flow.rs index 5b9f22ce0c..36f1f07b87 100644 --- a/crates/elephc-eval/src/interpreter/tests/control_flow.rs +++ b/crates/elephc-eval/src/interpreter/tests/control_flow.rs @@ -402,3 +402,40 @@ fn execute_program_foreach_honors_break_and_continue() { assert_eq!(values.output, "2"); } + +/// Verifies foreach drives eval-declared Iterator and IteratorAggregate objects. +#[test] +fn execute_program_foreach_iterates_eval_traversable_objects() { + let program = parse_fragment( + br#"class EvalForeachIterator implements Iterator { + private int $i = 0; + public function rewind(): void { echo "rewind:"; $this->i = 0; } + public function valid(): bool { echo "valid" . $this->i . ":"; return $this->i < 2; } + public function current(): mixed { echo "current" . $this->i . ":"; return "v" . $this->i; } + public function key(): mixed { echo "key" . $this->i . ":"; return "k" . $this->i; } + public function next(): void { echo "next" . $this->i . ":"; $this->i = $this->i + 1; } +} +class EvalForeachAggregate implements IteratorAggregate { + public function getIterator(): Traversable { echo "agg:"; return new EvalForeachIterator(); } +} +foreach (new EvalForeachIterator() as $key => $item) { + echo $key . "=" . $item . ":"; + if ($item === "v0") { continue; } + break; +} +echo "|"; +foreach (new EvalForeachAggregate() as $item) { + echo $item . ":"; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "rewind:valid0:current0:key0:k0=v0:next0:valid1:current1:key1:k1=v1:|agg:rewind:valid0:current0:v0:next0:valid1:current1:v1:next1:valid2:" + ); +} diff --git a/docs/php/eval.md b/docs/php/eval.md index baa6e60b7d..e8c76a60e6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -55,8 +55,9 @@ such a local alias removes the alias without unsetting the global value. | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | `foreach` supports value-only and key-value iteration over indexed and -associative arrays. Eval associative arrays preserve PHP insertion order for -iteration. +associative arrays, plus eval-declared and generated/AOT `Iterator` and +`IteratorAggregate` objects. Eval associative arrays preserve PHP insertion +order for iteration. Includes follow PHP's cwd-first lookup and then fall back to the eval call-site directory. Included PHP files may contain normal `` blocks, raw text diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 006bbca794..006f80cd3d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -593,6 +593,39 @@ echo ":" . $item; assert_eq!(out, "2:2"); } +/// Verifies foreach inside eval drives AOT Iterator and IteratorAggregate objects. +#[test] +fn test_eval_foreach_iterates_aot_traversable_objects() { + let out = compile_and_run( + r#"i = 0; } + public function valid(): bool { echo "valid" . $this->i . ":"; return $this->i < 2; } + public function current(): mixed { echo "current" . $this->i . ":"; return "v" . $this->i; } + public function key(): mixed { echo "key" . $this->i . ":"; return "k" . $this->i; } + public function next(): void { echo "next" . $this->i . ":"; $this->i = $this->i + 1; } +} +class EvalAotForeachAggregate implements IteratorAggregate { + public function getIterator(): Traversable { echo "agg:"; return new EvalAotForeachIterator(); } +} +eval('foreach (new EvalAotForeachIterator() as $key => $item) { + echo $key . "=" . $item . ":"; + if ($item === "v0") { continue; } + break; +} +echo "|"; +foreach (new EvalAotForeachAggregate() as $item) { + echo $item . ":"; +}'); +"#, + ); + assert_eq!( + out, + "rewind:valid0:current0:key0:k0=v0:next0:valid1:current1:key1:k1=v1:|agg:rewind:valid0:current0:v0:next0:valid1:current1:v1:next1:valid2:" + ); +} + /// Verifies value-only foreach loops inside eval iterate associative array values. #[test] fn test_eval_foreach_iterates_assoc_values() { From a21672150f47b6661e7a3ac4fb15c36ab5622904 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 23 Jun 2026 18:30:51 +0200 Subject: [PATCH 0581/1208] Rename eval crate to elephc-magician --- .plans/elephc-eval-complete-plan.md | 50 +++++++++---------- Cargo.lock | 4 +- Cargo.toml | 12 ++--- .../Cargo.toml | 2 +- .../src/abi.rs | 0 .../src/context.rs | 0 .../src/errors.rs | 0 .../src/eval_ir.rs | 0 .../src/ffi/context.rs | 0 .../src/ffi/execute.rs | 0 .../src/ffi/function_calls.rs | 0 .../src/ffi/mod.rs | 0 .../src/ffi/native_functions.rs | 0 .../src/ffi/native_methods.rs | 0 .../src/ffi/scope.rs | 0 .../src/ffi/symbols.rs | 0 .../src/ffi/tests.rs | 2 +- .../src/ffi/util.rs | 0 .../src/interpreter/array_literals.rs | 0 .../src/interpreter/builtins/arrays/access.rs | 0 .../src/interpreter/builtins/arrays/core.rs | 0 .../builtins/arrays/filters/chunk.rs | 0 .../builtins/arrays/filters/filter.rs | 0 .../builtins/arrays/filters/flip.rs | 0 .../builtins/arrays/filters/iterator.rs | 0 .../builtins/arrays/filters/mod.rs | 0 .../builtins/arrays/filters/pad.rs | 0 .../builtins/arrays/filters/projection.rs | 0 .../builtins/arrays/filters/reverse.rs | 0 .../builtins/arrays/filters/slice.rs | 0 .../builtins/arrays/filters/unique.rs | 0 .../src/interpreter/builtins/arrays/mod.rs | 0 .../interpreter/builtins/arrays/mutation.rs | 0 .../interpreter/builtins/arrays/push_pop.rs | 0 .../builtins/arrays/sort/direct.rs | 0 .../interpreter/builtins/arrays/sort/mod.rs | 0 .../builtins/arrays/sort/standard.rs | 0 .../interpreter/builtins/arrays/sort/user.rs | 0 .../src/interpreter/builtins/arrays/splice.rs | 0 .../interpreter/builtins/class_metadata.rs | 0 .../class_metadata/oop_introspection.rs | 0 .../builtins/filesystem/directories.rs | 0 .../builtins/filesystem/file_io.rs | 0 .../builtins/filesystem/fnmatch.rs | 0 .../interpreter/builtins/filesystem/mod.rs | 0 .../builtins/filesystem/ops/disk.rs | 0 .../builtins/filesystem/ops/glob.rs | 0 .../builtins/filesystem/ops/links.rs | 0 .../builtins/filesystem/ops/listing.rs | 0 .../builtins/filesystem/ops/mod.rs | 0 .../builtins/filesystem/ops/path_bool.rs | 0 .../builtins/filesystem/ops/tempnam.rs | 0 .../builtins/filesystem/ops/touch.rs | 0 .../builtins/filesystem/ops/umask.rs | 0 .../interpreter/builtins/filesystem/path.rs | 2 +- .../builtins/filesystem/process_pipes.rs | 0 .../builtins/filesystem/readline.rs | 0 .../builtins/filesystem/stream_context.rs | 0 .../builtins/filesystem/stream_extensions.rs | 0 .../builtins/filesystem/stream_settings.rs | 0 .../builtins/filesystem/stream_sockets.rs | 0 .../builtins/filesystem/streams.rs | 0 .../interpreter/builtins/formatting/common.rs | 0 .../builtins/formatting/dispatch.rs | 0 .../interpreter/builtins/formatting/math.rs | 0 .../interpreter/builtins/formatting/mod.rs | 0 .../builtins/formatting/number_format.rs | 0 .../interpreter/builtins/formatting/printf.rs | 0 .../builtins/formatting/sprintf_format.rs | 0 .../interpreter/builtins/formatting/sscanf.rs | 0 .../src/interpreter/builtins/mod.rs | 0 .../interpreter/builtins/network_env/cache.rs | 0 .../interpreter/builtins/network_env/env.rs | 0 .../interpreter/builtins/network_env/hosts.rs | 0 .../interpreter/builtins/network_env/ip.rs | 0 .../interpreter/builtins/network_env/mod.rs | 0 .../builtins/network_env/process.rs | 0 .../builtins/network_env/protocols.rs | 0 .../interpreter/builtins/process_control.rs | 0 .../interpreter/builtins/regex/captures.rs | 0 .../interpreter/builtins/regex/match_all.rs | 0 .../interpreter/builtins/regex/match_one.rs | 0 .../src/interpreter/builtins/regex/mod.rs | 0 .../src/interpreter/builtins/regex/pattern.rs | 0 .../src/interpreter/builtins/regex/replace.rs | 0 .../interpreter/builtins/regex/replacement.rs | 0 .../src/interpreter/builtins/regex/split.rs | 0 .../builtins/regex/split_helpers.rs | 0 .../interpreter/builtins/registry/binding.rs | 0 .../interpreter/builtins/registry/callable.rs | 0 .../builtins/registry/dispatch/arrays.rs | 0 .../builtins/registry/dispatch/core.rs | 0 .../builtins/registry/dispatch/filesystem.rs | 0 .../builtins/registry/dispatch/formatting.rs | 0 .../builtins/registry/dispatch/json.rs | 0 .../builtins/registry/dispatch/mod.rs | 0 .../builtins/registry/dispatch/network_env.rs | 0 .../builtins/registry/dispatch/regex.rs | 0 .../builtins/registry/dispatch/scalars.rs | 0 .../builtins/registry/dispatch/strings.rs | 0 .../builtins/registry/dispatch/symbols.rs | 0 .../builtins/registry/dispatch/time.rs | 0 .../src/interpreter/builtins/registry/mod.rs | 0 .../interpreter/builtins/registry/names.rs | 0 .../interpreter/builtins/scalars/base64.rs | 0 .../interpreter/builtins/scalars/common.rs | 0 .../src/interpreter/builtins/scalars/hex.rs | 0 .../src/interpreter/builtins/scalars/math.rs | 0 .../src/interpreter/builtins/scalars/mod.rs | 0 .../interpreter/builtins/scalars/search.rs | 0 .../interpreter/builtins/scalars/slashes.rs | 0 .../interpreter/builtins/scalars/trim_case.rs | 0 .../src/interpreter/builtins/scalars/types.rs | 0 .../src/interpreter/builtins/spl_autoload.rs | 0 .../src/interpreter/builtins/strings/ctype.rs | 0 .../builtins/strings/grapheme_strrev.rs | 0 .../src/interpreter/builtins/strings/gzip.rs | 0 .../src/interpreter/builtins/strings/hash.rs | 0 .../builtins/strings/hash_context.rs | 0 .../src/interpreter/builtins/strings/html.rs | 0 .../builtins/strings/introspection.rs | 0 .../src/interpreter/builtins/strings/mod.rs | 0 .../src/interpreter/builtins/strings/nl2br.rs | 0 .../src/interpreter/builtins/strings/pad.rs | 0 .../interpreter/builtins/strings/repeat.rs | 0 .../interpreter/builtins/strings/replace.rs | 0 .../interpreter/builtins/strings/simple.rs | 0 .../src/interpreter/builtins/strings/split.rs | 0 .../interpreter/builtins/strings/substr.rs | 0 .../src/interpreter/builtins/strings/url.rs | 0 .../src/interpreter/builtins/symbols.rs | 0 .../src/interpreter/builtins/time/clock.rs | 0 .../src/interpreter/builtins/time/date.rs | 0 .../src/interpreter/builtins/time/mktime.rs | 0 .../src/interpreter/builtins/time/mod.rs | 0 .../src/interpreter/builtins/time/sleep.rs | 0 .../interpreter/builtins/time/strtotime.rs | 0 .../src/interpreter/builtins/time/system.rs | 0 .../src/interpreter/constant_eval.rs | 0 .../src/interpreter/constants.rs | 0 .../src/interpreter/control.rs | 0 .../src/interpreter/core_builtins.rs | 0 .../src/interpreter/debug_output.rs | 0 .../src/interpreter/dynamic_functions.rs | 0 .../src/interpreter/expressions.rs | 0 .../src/interpreter/include_exec.rs | 0 .../src/interpreter/json.rs | 0 .../src/interpreter/libc_shims.rs | 0 .../src/interpreter/mod.rs | 2 +- .../src/interpreter/reflection.rs | 0 .../src/interpreter/return_type_compat.rs | 0 .../src/interpreter/return_values.rs | 0 .../src/interpreter/runtime_ops.rs | 0 .../src/interpreter/scope_cells.rs | 0 .../src/interpreter/statements.rs | 0 .../src/interpreter/tests/array_literals.rs | 2 +- .../interpreter/tests/builtins_arrays_core.rs | 2 +- .../tests/builtins_arrays_iterators.rs | 2 +- .../interpreter/tests/builtins_arrays_sets.rs | 2 +- .../tests/builtins_class_metadata.rs | 2 +- .../tests/builtins_directory_streams.rs | 4 +- .../tests/builtins_file_streams.rs | 24 ++++----- .../tests/builtins_filesystem_metadata.rs | 24 ++++----- .../tests/builtins_filesystem_ops.rs | 48 +++++++++--------- .../src/interpreter/tests/builtins_json.rs | 2 +- .../tests/builtins_language_constructs.rs | 2 +- .../tests/builtins_math_formatting.rs | 2 +- .../tests/builtins_process_pipes.rs | 4 +- .../interpreter/tests/builtins_readline.rs | 2 +- .../tests/builtins_reflection_functions.rs | 2 +- .../src/interpreter/tests/builtins_scalars.rs | 2 +- .../tests/builtins_spl_autoload.rs | 2 +- .../tests/builtins_stream_contexts.rs | 2 +- .../tests/builtins_stream_extensions.rs | 4 +- .../tests/builtins_stream_settings.rs | 4 +- .../tests/builtins_stream_sockets.rs | 2 +- .../tests/builtins_strings_binary.rs | 2 +- .../tests/builtins_strings_encoding.rs | 4 +- .../tests/builtins_strings_text.rs | 2 +- .../src/interpreter/tests/builtins_symbols.rs | 2 +- .../tests/builtins_system_network.rs | 2 +- .../src/interpreter/tests/class_constants.rs | 2 +- .../src/interpreter/tests/classes.rs | 2 +- .../src/interpreter/tests/control_flow.rs | 2 +- .../src/interpreter/tests/core.rs | 10 ++-- .../src/interpreter/tests/dynamic_calls.rs | 2 +- .../src/interpreter/tests/enums.rs | 2 +- .../src/interpreter/tests/expressions.rs | 2 +- .../interpreter/tests/functions_namespaces.rs | 2 +- .../src/interpreter/tests/method_arguments.rs | 2 +- .../src/interpreter/tests/mod.rs | 2 +- .../src/interpreter/tests/native_scope.rs | 2 +- .../src/interpreter/tests/static_members.rs | 2 +- .../interpreter/tests/support/array_ops.rs | 0 .../src/interpreter/tests/support/cell_ops.rs | 0 .../interpreter/tests/support/conversions.rs | 0 .../tests/support/lifecycle_ops.rs | 0 .../src/interpreter/tests/support/mod.rs | 0 .../interpreter/tests/support/numeric_ops.rs | 0 .../interpreter/tests/support/object_ops.rs | 0 .../interpreter/tests/support/runtime_ops.rs | 0 .../interpreter/tests/trait_adaptations.rs | 2 +- .../src/json_validate.rs | 0 .../src/lexer/mod.rs | 0 .../src/lexer/scan.rs | 0 .../src/lexer/token.rs | 0 .../src/lib.rs | 2 +- .../src/lower.rs | 0 .../src/parser/cursor.rs | 0 .../src/parser/expressions.rs | 0 .../src/parser/mod.rs | 0 .../src/parser/state.rs | 0 .../src/parser/statements.rs | 0 .../src/parser/tests/arrays_objects.rs | 2 +- .../src/parser/tests/assignments.rs | 2 +- .../src/parser/tests/calls.rs | 2 +- .../src/parser/tests/class_constants.rs | 2 +- .../src/parser/tests/classes_errors.rs | 2 +- .../src/parser/tests/control_statements.rs | 2 +- .../src/parser/tests/enums.rs | 2 +- .../src/parser/tests/exceptions_control.rs | 2 +- .../src/parser/tests/magic_comments.rs | 2 +- .../src/parser/tests/mod.rs | 2 +- .../src/parser/tests/namespaces.rs | 2 +- .../src/parser/tests/operators.rs | 2 +- .../src/parser/tests/static_members.rs | 2 +- .../src/parser/tests/support.rs | 0 .../src/parser/tests/trait_adaptations.rs | 2 +- .../src/runtime_hooks/externs.rs | 0 .../src/runtime_hooks/mod.rs | 0 .../src/runtime_hooks/ops.rs | 0 .../src/runtime_hooks/tags.rs | 0 .../src/scope.rs | 0 .../src/stream_resources.rs | 2 +- .../src/value.rs | 0 docs/internals/the-runtime.md | 2 +- docs/php/eval.md | 4 +- scripts/test-linux-arm64.sh | 7 ++- scripts/test-linux-x86_64.sh | 7 ++- src/codegen/eval_constructor_helpers.rs | 2 +- src/codegen/eval_method_helpers.rs | 2 +- src/codegen/eval_property_helpers.rs | 2 +- src/codegen/eval_reflection_helpers.rs | 2 +- src/codegen/eval_reflection_owner_helpers.rs | 2 +- src/codegen/lower_inst/builtins/eval.rs | 4 +- src/codegen_support/runtime/eval_bridge.rs | 4 +- src/codegen_support/runtime_features.rs | 10 ++-- src/linker.rs | 34 ++++++------- tests/codegen/eval.rs | 18 +++---- tests/codegen/support/runner.rs | 4 +- 250 files changed, 197 insertions(+), 199 deletions(-) rename crates/{elephc-eval => elephc-magician}/Cargo.toml (92%) rename crates/{elephc-eval => elephc-magician}/src/abi.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/context.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/errors.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/eval_ir.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/context.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/execute.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/function_calls.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/native_functions.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/native_methods.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/scope.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/symbols.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/ffi/tests.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/ffi/util.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/array_literals.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/access.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/core.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/chunk.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/filter.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/flip.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/iterator.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/pad.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/projection.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/reverse.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/slice.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/filters/unique.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/mutation.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/push_pop.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/sort/direct.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/sort/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/sort/standard.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/sort/user.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/arrays/splice.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/class_metadata.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/class_metadata/oop_introspection.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/directories.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/file_io.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/fnmatch.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/disk.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/glob.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/links.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/listing.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/path_bool.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/tempnam.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/touch.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/ops/umask.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/path.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/process_pipes.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/readline.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/stream_context.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/stream_extensions.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/stream_settings.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/stream_sockets.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/filesystem/streams.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/formatting/common.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/formatting/dispatch.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/formatting/math.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/formatting/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/formatting/number_format.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/formatting/printf.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/formatting/sprintf_format.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/formatting/sscanf.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/network_env/cache.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/network_env/env.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/network_env/hosts.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/network_env/ip.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/network_env/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/network_env/process.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/network_env/protocols.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/process_control.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/captures.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/match_all.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/match_one.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/pattern.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/replace.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/replacement.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/split.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/regex/split_helpers.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/binding.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/callable.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/arrays.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/core.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/filesystem.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/formatting.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/json.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/network_env.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/regex.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/scalars.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/strings.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/symbols.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/dispatch/time.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/registry/names.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/base64.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/common.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/hex.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/math.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/search.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/slashes.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/trim_case.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/scalars/types.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/spl_autoload.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/ctype.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/grapheme_strrev.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/gzip.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/hash.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/hash_context.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/html.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/introspection.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/nl2br.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/pad.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/repeat.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/replace.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/simple.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/split.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/substr.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/strings/url.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/symbols.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/time/clock.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/time/date.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/time/mktime.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/time/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/time/sleep.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/time/strtotime.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/builtins/time/system.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/constant_eval.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/constants.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/control.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/core_builtins.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/debug_output.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/dynamic_functions.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/expressions.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/include_exec.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/json.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/libc_shims.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/mod.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/reflection.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/return_type_compat.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/return_values.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/runtime_ops.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/scope_cells.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/statements.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/array_literals.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_arrays_core.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_arrays_iterators.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_arrays_sets.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_class_metadata.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_directory_streams.rs (94%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_file_streams.rs (92%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_filesystem_metadata.rs (92%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_filesystem_ops.rs (90%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_json.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_language_constructs.rs (97%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_math_formatting.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_process_pipes.rs (93%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_readline.rs (93%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_reflection_functions.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_scalars.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_spl_autoload.rs (97%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_stream_contexts.rs (97%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_stream_extensions.rs (95%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_stream_settings.rs (94%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_stream_sockets.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_strings_binary.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_strings_encoding.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_strings_text.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_symbols.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/builtins_system_network.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/class_constants.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/classes.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/control_flow.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/core.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/dynamic_calls.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/enums.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/expressions.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/functions_namespaces.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/method_arguments.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/mod.rs (95%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/native_scope.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/static_members.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/support/array_ops.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/support/cell_ops.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/support/conversions.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/support/lifecycle_ops.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/support/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/support/numeric_ops.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/support/object_ops.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/support/runtime_ops.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/interpreter/tests/trait_adaptations.rs (97%) rename crates/{elephc-eval => elephc-magician}/src/json_validate.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/lexer/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/lexer/scan.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/lexer/token.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/lib.rs (91%) rename crates/{elephc-eval => elephc-magician}/src/lower.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/parser/cursor.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/parser/expressions.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/parser/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/parser/state.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/parser/statements.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/arrays_objects.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/assignments.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/calls.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/class_constants.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/classes_errors.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/control_statements.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/enums.rs (97%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/exceptions_control.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/magic_comments.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/mod.rs (90%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/namespaces.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/operators.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/static_members.rs (98%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/support.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/parser/tests/trait_adaptations.rs (97%) rename crates/{elephc-eval => elephc-magician}/src/runtime_hooks/externs.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/runtime_hooks/mod.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/runtime_hooks/ops.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/runtime_hooks/tags.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/scope.rs (100%) rename crates/{elephc-eval => elephc-magician}/src/stream_resources.rs (99%) rename crates/{elephc-eval => elephc-magician}/src/value.rs (100%) diff --git a/.plans/elephc-eval-complete-plan.md b/.plans/elephc-eval-complete-plan.md index 54798dc9b1..2678840e8a 100644 --- a/.plans/elephc-eval-complete-plan.md +++ b/.plans/elephc-eval-complete-plan.md @@ -1,4 +1,4 @@ -# Piano dettagliato: supporto completo a eval tramite libelephc-eval +# Piano dettagliato: supporto completo a eval tramite libelephc-magician Nota percorso: la richiesta indicava `~/Downlaods`, che sembra un refuso. Questo file e' stato scritto in `~/Downloads`. @@ -17,7 +17,7 @@ programma senza eval programma con eval -> runtime elephc normale - -> + libelephc-eval linkata condizionalmente + -> + libelephc-magician linkata condizionalmente -> solo gli scope che arrivano a eval pagano il costo dinamico ``` @@ -31,9 +31,9 @@ al punto eval -> valuta l'argomento in source order -> materializza lo scope se non esiste -> flush dei locals vivi nello scope dinamico - -> chiama libelephc-eval + -> chiama libelephc-magician -libelephc-eval +libelephc-magician -> parse della stringa PHP -> lowering a EvalIR/EIR dinamico -> interpretazione usando lo scope ricevuto @@ -45,7 +45,7 @@ dopo eval ## Decisioni architetturali -1. `libelephc-eval` e' una bridge staticlib, come `elephc_tls`, `elephc_pdo`, `elephc_crypto` e `elephc_phar`. +1. `libelephc-magician` e' una bridge staticlib, come `elephc_tls`, `elephc_pdo`, `elephc_crypto` e `elephc_phar`. 2. Il linker la include solo quando il programma contiene una chiamata PHP a `eval`. 3. Il backend attivo resta AST -> EIR -> `src/codegen_ir/`; non si estende il backend AST legacy. 4. Il codice statico resta nativo. Non si interpreta tutta la funzione salvo dove necessario. @@ -94,7 +94,7 @@ var_dump(function_exists('A\\f_eval_test'), function_exists('f_eval_test')); Aggiungere una nuova crate: ```text -crates/elephc-eval/ +crates/elephc-magician/ Cargo.toml src/lib.rs src/abi.rs @@ -118,7 +118,7 @@ members = [ "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", - "crates/elephc-eval", + "crates/elephc-magician", ] default-members = [ ".", @@ -126,7 +126,7 @@ default-members = [ "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", - "crates/elephc-eval", + "crates/elephc-magician", ] ``` @@ -138,9 +138,9 @@ In `src/linker.rs`, aggiungere una entry a `BRIDGES`: ```rust BridgeStaticlib { - lib_name: "elephc_eval", - env_var: "ELEPHC_EVAL_LIB_DIR", - crate_name: "elephc-eval", + lib_name: "elephc_magician", + env_var: "ELEPHC_MAGICIAN_LIB_DIR", + crate_name: "elephc-magician", whole_archive: false, macos_frameworks: &[], needs_libdl: true, @@ -173,13 +173,13 @@ Required libraries: ```rust if features.eval { - libs.push("elephc_eval".to_string()); + libs.push("elephc_magician".to_string()); } ``` Importante: `eval` deve essere rilevato anche se scritto con casing diverso, se PHP lo consente, e anche in presenza di namespace fallback. -## ABI nativa verso libelephc-eval +## ABI nativa verso libelephc-magician L'ABI deve essere piccola, stabile e target-aware. Non passare enum Rust non `repr(C)` attraverso il confine staticlib. @@ -315,7 +315,7 @@ Il codice nativo non deve fidarsi dei valori statici precedenti. Strategia consigliata: 1. Marcare invalidi tutti i locals flushati. -2. Se libelephc-eval restituisce una dirty set affidabile, invalidare almeno quella. +2. Se libelephc-magician restituisce una dirty set affidabile, invalidare almeno quella. 3. Se non c'e' dirty set o se eval usa variabili variabili, references, `unset`, `global`, invalidare tutto lo scope materializzato. 4. Al prossimo uso statico di `$x`, fare reload lazy: @@ -571,7 +571,7 @@ Per chiamate statiche a simboli non risolti dopo eval: 1. Il checker deve permettere lookup dinamico se il path di controllo puo' aver attraversato eval. 2. Il lowering deve generare `__rt_dynamic_call("dyn", args)` invece di direct symbol call. 3. `__rt_dynamic_call` cerca prima funzioni native note, poi funzioni eval-defined. -4. Le funzioni definite da eval possono essere interpretate da libelephc-eval. +4. Le funzioni definite da eval possono essere interpretate da libelephc-magician. Per chiamate note staticamente: @@ -646,12 +646,12 @@ Deliverable: ### Fase 1: link condizionale e stub ABI -Obiettivo: linkare `libelephc-eval` solo quando serve. +Obiettivo: linkare `libelephc-magician` solo quando serve. Modifiche: -1. Aggiungere crate `crates/elephc-eval`. -2. Aggiungere `elephc_eval` a `BRIDGES` in `src/linker.rs`. +1. Aggiungere crate `crates/elephc-magician`. +2. Aggiungere `elephc_magician` a `BRIDGES` in `src/linker.rs`. 3. Aggiungere `RuntimeFeatures.eval`. 4. Aggiungere detection `program_requires_eval`. 5. Aggiungere builtin/language construct `eval` al checker. @@ -659,9 +659,9 @@ Modifiche: Test: -1. Programma senza eval non linka `elephc_eval`. -2. Programma con eval linka `elephc_eval`. -3. Test linker per `ELEPHC_EVAL_LIB_DIR`. +1. Programma senza eval non linka `elephc_magician`. +2. Programma con eval linka `elephc_magician`. +3. Test linker per `ELEPHC_MAGICIAN_LIB_DIR`. 4. Errore chiaro se la libreria non e' trovata. ### Fase 2: MaterializedScope minimo @@ -698,7 +698,7 @@ Obiettivo: parse della stringa eval. Opzioni: 1. Estrarre lexer/parser in crate riusabile, ad esempio `crates/elephc-frontend`. -2. Oppure duplicare temporaneamente solo il parser necessario in `crates/elephc-eval`, ma e' sconsigliato. +2. Oppure duplicare temporaneamente solo il parser necessario in `crates/elephc-magician`, ma e' sconsigliato. Regole: @@ -957,9 +957,9 @@ git diff --check La feature e' considerata completa solo quando: -1. Programmi senza eval non linkano `elephc_eval`. +1. Programmi senza eval non linkano `elephc_magician`. 2. Programmi senza eval non cambiano assembly/performance in modo osservabile. -3. Programmi con eval linkano `elephc_eval` automaticamente. +3. Programmi con eval linkano `elephc_magician` automaticamente. 4. Le funzioni con eval usano flush/invalidate/reload corretto. 5. Eval modifica, crea e unsetta variabili dello scope chiamante. 6. `return` dentro eval ritorna il valore di `eval`. @@ -972,7 +972,7 @@ La feature e' considerata completa solo quando: Sequenza pragmatica: -1. Link condizionale `elephc_eval` + stub ABI. +1. Link condizionale `elephc_magician` + stub ABI. 2. `contains_eval` + lowering call runtime nel backend EIR. 3. `MaterializedScope` + flush/invalidate/reload con stub test-only. 4. Parser runtime per fragment eval. diff --git a/Cargo.lock b/Cargo.lock index d1ea11e56f..cd55dee8ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,8 +583,8 @@ dependencies = [ "bitflags 2.13.0", "bzip2-rs", "elephc-crypto", - "elephc-eval", "elephc-image", + "elephc-magician", "elephc-pdo", "elephc-phar", "elephc-tls", @@ -617,7 +617,7 @@ dependencies = [ ] [[package]] -name = "elephc-eval" +name = "elephc-magician" version = "0.1.0" dependencies = [ "elephc-crypto", diff --git a/Cargo.toml b/Cargo.toml index 7fdc1250a6..9394134f5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,12 @@ categories = ["compilers", "command-line-utilities"] # (hash/HMAC family), libelephc_phar.a (runtime phar:// archive reads), # libelephc_tz.a (DateTimeZone introspection: getLocation/getTransitions/ # listAbbreviations), libelephc_image.a (GD/Exif/Imagick/Gmagick/Cairo), and -# libelephc_web.a (--web prefork HTTP server), and libelephc_eval.a (optional -# eval runtime) into target//, where linker.rs locates them for -# programs that use those features. +# libelephc_web.a (--web prefork HTTP server), and libelephc_magician.a +# (optional eval runtime) into target//, where linker.rs locates them +# for programs that use those features. [workspace] -members = [".", "crates/elephc-tls", "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", "crates/elephc-tz", "crates/elephc-image", "crates/elephc-web", "crates/elephc-eval"] -default-members = [".", "crates/elephc-tls", "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", "crates/elephc-tz", "crates/elephc-image", "crates/elephc-web", "crates/elephc-eval"] +members = [".", "crates/elephc-tls", "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", "crates/elephc-tz", "crates/elephc-image", "crates/elephc-web", "crates/elephc-magician"] +default-members = [".", "crates/elephc-tls", "crates/elephc-pdo", "crates/elephc-crypto", "crates/elephc-phar", "crates/elephc-tz", "crates/elephc-image", "crates/elephc-web", "crates/elephc-magician"] resolver = "2" [[bin]] @@ -61,7 +61,7 @@ elephc-phar = { path = "crates/elephc-phar" } elephc-tz = { path = "crates/elephc-tz" } elephc-image = { path = "crates/elephc-image" } elephc-web = { path = "crates/elephc-web" } -elephc-eval = { path = "crates/elephc-eval" } +elephc-magician = { path = "crates/elephc-magician" } # flate2 lets the phar:// codegen tests build gzip (raw-DEFLATE) fixture entries # with the same encoder the compiler decodes, guaranteeing a version-stable # round-trip without shelling out to php. diff --git a/crates/elephc-eval/Cargo.toml b/crates/elephc-magician/Cargo.toml similarity index 92% rename from crates/elephc-eval/Cargo.toml rename to crates/elephc-magician/Cargo.toml index 261f0f971b..e25a41e1fa 100644 --- a/crates/elephc-eval/Cargo.toml +++ b/crates/elephc-magician/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "elephc-eval" +name = "elephc-magician" version = "0.1.0" edition = "2021" license = "MIT" diff --git a/crates/elephc-eval/src/abi.rs b/crates/elephc-magician/src/abi.rs similarity index 100% rename from crates/elephc-eval/src/abi.rs rename to crates/elephc-magician/src/abi.rs diff --git a/crates/elephc-eval/src/context.rs b/crates/elephc-magician/src/context.rs similarity index 100% rename from crates/elephc-eval/src/context.rs rename to crates/elephc-magician/src/context.rs diff --git a/crates/elephc-eval/src/errors.rs b/crates/elephc-magician/src/errors.rs similarity index 100% rename from crates/elephc-eval/src/errors.rs rename to crates/elephc-magician/src/errors.rs diff --git a/crates/elephc-eval/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs similarity index 100% rename from crates/elephc-eval/src/eval_ir.rs rename to crates/elephc-magician/src/eval_ir.rs diff --git a/crates/elephc-eval/src/ffi/context.rs b/crates/elephc-magician/src/ffi/context.rs similarity index 100% rename from crates/elephc-eval/src/ffi/context.rs rename to crates/elephc-magician/src/ffi/context.rs diff --git a/crates/elephc-eval/src/ffi/execute.rs b/crates/elephc-magician/src/ffi/execute.rs similarity index 100% rename from crates/elephc-eval/src/ffi/execute.rs rename to crates/elephc-magician/src/ffi/execute.rs diff --git a/crates/elephc-eval/src/ffi/function_calls.rs b/crates/elephc-magician/src/ffi/function_calls.rs similarity index 100% rename from crates/elephc-eval/src/ffi/function_calls.rs rename to crates/elephc-magician/src/ffi/function_calls.rs diff --git a/crates/elephc-eval/src/ffi/mod.rs b/crates/elephc-magician/src/ffi/mod.rs similarity index 100% rename from crates/elephc-eval/src/ffi/mod.rs rename to crates/elephc-magician/src/ffi/mod.rs diff --git a/crates/elephc-eval/src/ffi/native_functions.rs b/crates/elephc-magician/src/ffi/native_functions.rs similarity index 100% rename from crates/elephc-eval/src/ffi/native_functions.rs rename to crates/elephc-magician/src/ffi/native_functions.rs diff --git a/crates/elephc-eval/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs similarity index 100% rename from crates/elephc-eval/src/ffi/native_methods.rs rename to crates/elephc-magician/src/ffi/native_methods.rs diff --git a/crates/elephc-eval/src/ffi/scope.rs b/crates/elephc-magician/src/ffi/scope.rs similarity index 100% rename from crates/elephc-eval/src/ffi/scope.rs rename to crates/elephc-magician/src/ffi/scope.rs diff --git a/crates/elephc-eval/src/ffi/symbols.rs b/crates/elephc-magician/src/ffi/symbols.rs similarity index 100% rename from crates/elephc-eval/src/ffi/symbols.rs rename to crates/elephc-magician/src/ffi/symbols.rs diff --git a/crates/elephc-eval/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs similarity index 99% rename from crates/elephc-eval/src/ffi/tests.rs rename to crates/elephc-magician/src/ffi/tests.rs index 10f454f1bc..9028c72b1d 100644 --- a/crates/elephc-eval/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -4,7 +4,7 @@ //! dynamic symbol registration without requiring generated runtime assembly. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Execute remains a controlled unsupported stub in crate unit tests. diff --git a/crates/elephc-eval/src/ffi/util.rs b/crates/elephc-magician/src/ffi/util.rs similarity index 100% rename from crates/elephc-eval/src/ffi/util.rs rename to crates/elephc-magician/src/ffi/util.rs diff --git a/crates/elephc-eval/src/interpreter/array_literals.rs b/crates/elephc-magician/src/interpreter/array_literals.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/array_literals.rs rename to crates/elephc-magician/src/interpreter/array_literals.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/access.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/access.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/access.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/core.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/core.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/core.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/chunk.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/chunk.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/chunk.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/chunk.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/filter.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/filter.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/flip.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/flip.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/flip.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/flip.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/iterator.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/mod.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/pad.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/pad.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/pad.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/pad.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/projection.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/projection.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/projection.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/projection.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/reverse.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/reverse.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/reverse.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/reverse.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/slice.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/slice.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/slice.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/slice.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/filters/unique.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/unique.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/filters/unique.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/filters/unique.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/mod.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/mutation.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/push_pop.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/push_pop.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/push_pop.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/push_pop.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort/direct.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/sort/direct.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort/mod.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/sort/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/sort/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort/standard.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/standard.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/sort/standard.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/sort/standard.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/sort/user.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/sort/user.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/arrays/splice.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/arrays/splice.rs rename to crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/class_metadata.rs rename to crates/elephc-magician/src/interpreter/builtins/class_metadata.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/class_metadata/oop_introspection.rs rename to crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/directories.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/directories.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/file_io.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/fnmatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/fnmatch.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/disk.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/disk.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/disk.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/disk.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/glob.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/glob.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/glob.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/glob.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/links.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/links.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/listing.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/listing.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/listing.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/listing.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/path_bool.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/tempnam.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/tempnam.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/tempnam.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/tempnam.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/touch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/touch.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/ops/umask.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/umask.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/ops/umask.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/ops/umask.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs index 77a71375a7..957f328857 100644 --- a/crates/elephc-eval/src/interpreter/builtins/filesystem/path.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs @@ -40,7 +40,7 @@ pub(in crate::interpreter) fn eval_path_is_writable(path: &std::path::Path) -> b return false; } let probe = path.join(format!( - ".elephc_eval_writable_probe_{}", + ".elephc_magician_writable_probe_{}", std::process::id() )); match std::fs::OpenOptions::new() diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/process_pipes.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/process_pipes.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/process_pipes.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/process_pipes.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/readline.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/readline.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_context.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/stream_context.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/stream_extensions.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_settings.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/stream_settings.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/stream_sockets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/stream_sockets.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/filesystem/streams.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/common.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/common.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/formatting/common.rs rename to crates/elephc-magician/src/interpreter/builtins/formatting/common.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/formatting/dispatch.rs rename to crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/math.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/math.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/formatting/math.rs rename to crates/elephc-magician/src/interpreter/builtins/formatting/math.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/mod.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/formatting/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/number_format.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/formatting/number_format.rs rename to crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/printf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/formatting/printf.rs rename to crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/sprintf_format.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf_format.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/formatting/sprintf_format.rs rename to crates/elephc-magician/src/interpreter/builtins/formatting/sprintf_format.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/formatting/sscanf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/formatting/sscanf.rs rename to crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/cache.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/cache.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/network_env/cache.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/cache.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/env.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/env.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/network_env/env.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/env.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/hosts.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/hosts.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/network_env/hosts.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/hosts.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/ip.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/ip.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/network_env/ip.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/ip.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/network_env/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/process.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/process.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/network_env/process.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/process.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/network_env/protocols.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/protocols.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/network_env/protocols.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/protocols.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/process_control.rs b/crates/elephc-magician/src/interpreter/builtins/process_control.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/process_control.rs rename to crates/elephc-magician/src/interpreter/builtins/process_control.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/captures.rs b/crates/elephc-magician/src/interpreter/builtins/regex/captures.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/captures.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/captures.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/match_all.rs b/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/match_all.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/match_one.rs b/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/match_one.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/pattern.rs b/crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/pattern.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/replace.rs b/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/replace.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/replace.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/replacement.rs b/crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/replacement.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/split.rs b/crates/elephc-magician/src/interpreter/builtins/regex/split.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/split.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/split.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/regex/split_helpers.rs b/crates/elephc-magician/src/interpreter/builtins/regex/split_helpers.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/regex/split_helpers.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/split_helpers.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/binding.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/binding.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/callable.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/callable.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/arrays.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/core.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/filesystem.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/formatting.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/formatting.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/json.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/json.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/json.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/json.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/network_env.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/regex.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/regex.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/scalars.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/strings.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/symbols.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/dispatch/time.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/dispatch/time.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/registry/names.rs rename to crates/elephc-magician/src/interpreter/builtins/registry/names.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/base64.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/base64.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/base64.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/base64.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/common.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/common.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/common.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/common.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/hex.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/hex.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/hex.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/hex.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/math.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/math.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/math.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/math.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/mod.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/search.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/search.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/search.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/search.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/slashes.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/slashes.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/slashes.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/slashes.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/trim_case.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/trim_case.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/trim_case.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/trim_case.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/scalars/types.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/scalars/types.rs rename to crates/elephc-magician/src/interpreter/builtins/scalars/types.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/spl_autoload.rs b/crates/elephc-magician/src/interpreter/builtins/spl_autoload.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/spl_autoload.rs rename to crates/elephc-magician/src/interpreter/builtins/spl_autoload.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/ctype.rs b/crates/elephc-magician/src/interpreter/builtins/strings/ctype.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/ctype.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/ctype.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/grapheme_strrev.rs b/crates/elephc-magician/src/interpreter/builtins/strings/grapheme_strrev.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/grapheme_strrev.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/grapheme_strrev.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/gzip.rs b/crates/elephc-magician/src/interpreter/builtins/strings/gzip.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/gzip.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/gzip.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/hash.rs b/crates/elephc-magician/src/interpreter/builtins/strings/hash.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/hash.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/hash.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/hash_context.rs b/crates/elephc-magician/src/interpreter/builtins/strings/hash_context.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/hash_context.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/hash_context.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/html.rs b/crates/elephc-magician/src/interpreter/builtins/strings/html.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/html.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/html.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs b/crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/introspection.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/mod.rs b/crates/elephc-magician/src/interpreter/builtins/strings/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/nl2br.rs b/crates/elephc-magician/src/interpreter/builtins/strings/nl2br.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/nl2br.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/nl2br.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/pad.rs b/crates/elephc-magician/src/interpreter/builtins/strings/pad.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/pad.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/pad.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/repeat.rs b/crates/elephc-magician/src/interpreter/builtins/strings/repeat.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/repeat.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/repeat.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/replace.rs b/crates/elephc-magician/src/interpreter/builtins/strings/replace.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/replace.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/replace.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/simple.rs b/crates/elephc-magician/src/interpreter/builtins/strings/simple.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/simple.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/simple.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/split.rs b/crates/elephc-magician/src/interpreter/builtins/strings/split.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/split.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/split.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/substr.rs b/crates/elephc-magician/src/interpreter/builtins/strings/substr.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/substr.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/substr.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/strings/url.rs b/crates/elephc-magician/src/interpreter/builtins/strings/url.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/strings/url.rs rename to crates/elephc-magician/src/interpreter/builtins/strings/url.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/symbols.rs rename to crates/elephc-magician/src/interpreter/builtins/symbols.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/time/clock.rs b/crates/elephc-magician/src/interpreter/builtins/time/clock.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/time/clock.rs rename to crates/elephc-magician/src/interpreter/builtins/time/clock.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/time/date.rs b/crates/elephc-magician/src/interpreter/builtins/time/date.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/time/date.rs rename to crates/elephc-magician/src/interpreter/builtins/time/date.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/time/mktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/time/mktime.rs rename to crates/elephc-magician/src/interpreter/builtins/time/mktime.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/time/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/time/mod.rs rename to crates/elephc-magician/src/interpreter/builtins/time/mod.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/time/sleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/time/sleep.rs rename to crates/elephc-magician/src/interpreter/builtins/time/sleep.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/time/strtotime.rs b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/time/strtotime.rs rename to crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs diff --git a/crates/elephc-eval/src/interpreter/builtins/time/system.rs b/crates/elephc-magician/src/interpreter/builtins/time/system.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/builtins/time/system.rs rename to crates/elephc-magician/src/interpreter/builtins/time/system.rs diff --git a/crates/elephc-eval/src/interpreter/constant_eval.rs b/crates/elephc-magician/src/interpreter/constant_eval.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/constant_eval.rs rename to crates/elephc-magician/src/interpreter/constant_eval.rs diff --git a/crates/elephc-eval/src/interpreter/constants.rs b/crates/elephc-magician/src/interpreter/constants.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/constants.rs rename to crates/elephc-magician/src/interpreter/constants.rs diff --git a/crates/elephc-eval/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/control.rs rename to crates/elephc-magician/src/interpreter/control.rs diff --git a/crates/elephc-eval/src/interpreter/core_builtins.rs b/crates/elephc-magician/src/interpreter/core_builtins.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/core_builtins.rs rename to crates/elephc-magician/src/interpreter/core_builtins.rs diff --git a/crates/elephc-eval/src/interpreter/debug_output.rs b/crates/elephc-magician/src/interpreter/debug_output.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/debug_output.rs rename to crates/elephc-magician/src/interpreter/debug_output.rs diff --git a/crates/elephc-eval/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/dynamic_functions.rs rename to crates/elephc-magician/src/interpreter/dynamic_functions.rs diff --git a/crates/elephc-eval/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/expressions.rs rename to crates/elephc-magician/src/interpreter/expressions.rs diff --git a/crates/elephc-eval/src/interpreter/include_exec.rs b/crates/elephc-magician/src/interpreter/include_exec.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/include_exec.rs rename to crates/elephc-magician/src/interpreter/include_exec.rs diff --git a/crates/elephc-eval/src/interpreter/json.rs b/crates/elephc-magician/src/interpreter/json.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/json.rs rename to crates/elephc-magician/src/interpreter/json.rs diff --git a/crates/elephc-eval/src/interpreter/libc_shims.rs b/crates/elephc-magician/src/interpreter/libc_shims.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/libc_shims.rs rename to crates/elephc-magician/src/interpreter/libc_shims.rs diff --git a/crates/elephc-eval/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/mod.rs rename to crates/elephc-magician/src/interpreter/mod.rs index 884f40ec69..2c9b943413 100644 --- a/crates/elephc-eval/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -5,7 +5,7 @@ //! //! Called from: //! - Future `crate::__elephc_eval_execute()` implementation. -//! - `cargo test -p elephc-eval` for scope/value-flow validation. +//! - `cargo test -p elephc-magician` for scope/value-flow validation. //! //! Key details: //! - This module does not own PHP values. Constants and operations are delegated diff --git a/crates/elephc-eval/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/reflection.rs rename to crates/elephc-magician/src/interpreter/reflection.rs diff --git a/crates/elephc-eval/src/interpreter/return_type_compat.rs b/crates/elephc-magician/src/interpreter/return_type_compat.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/return_type_compat.rs rename to crates/elephc-magician/src/interpreter/return_type_compat.rs diff --git a/crates/elephc-eval/src/interpreter/return_values.rs b/crates/elephc-magician/src/interpreter/return_values.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/return_values.rs rename to crates/elephc-magician/src/interpreter/return_values.rs diff --git a/crates/elephc-eval/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/runtime_ops.rs rename to crates/elephc-magician/src/interpreter/runtime_ops.rs diff --git a/crates/elephc-eval/src/interpreter/scope_cells.rs b/crates/elephc-magician/src/interpreter/scope_cells.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/scope_cells.rs rename to crates/elephc-magician/src/interpreter/scope_cells.rs diff --git a/crates/elephc-eval/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/statements.rs rename to crates/elephc-magician/src/interpreter/statements.rs diff --git a/crates/elephc-eval/src/interpreter/tests/array_literals.rs b/crates/elephc-magician/src/interpreter/tests/array_literals.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/array_literals.rs rename to crates/elephc-magician/src/interpreter/tests/array_literals.rs index a548ff0661..af619cb715 100644 --- a/crates/elephc-eval/src/interpreter/tests/array_literals.rs +++ b/crates/elephc-magician/src/interpreter/tests/array_literals.rs @@ -2,7 +2,7 @@ //! Interpreter tests for EvalIR array literal key allocation and reads. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases focus on PHP array-key normalization during literal evaluation. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_core.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_arrays_core.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs index 4cdf123941..49dff13451 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_core.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs @@ -2,7 +2,7 @@ //! Interpreter tests for array aggregation, mapping, mutation, and sorting builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover array builtins that transform or reorder array values. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_iterators.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_iterators.rs similarity index 98% rename from crates/elephc-eval/src/interpreter/tests/builtins_arrays_iterators.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_arrays_iterators.rs index fd68eaff15..f5ea27e774 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_iterators.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_iterators.rs @@ -2,7 +2,7 @@ //! Interpreter tests for iterator-style array builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases exercise callback iteration and object iterator dispatch. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_sets.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_arrays_sets.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs index e1e9a2f768..0686df941f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_arrays_sets.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_sets.rs @@ -2,7 +2,7 @@ //! Interpreter tests for array set, projection, shape, range, and lookup builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover array builtins that preserve or query collection structure. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index c7bb86c1c0..52f9301c2a 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval class metadata and relation builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Eval class declarations expose parent/interface metadata plus class-level diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_directory_streams.rs b/crates/elephc-magician/src/interpreter/tests/builtins_directory_streams.rs similarity index 94% rename from crates/elephc-eval/src/interpreter/tests/builtins_directory_streams.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_directory_streams.rs index 0db98c9dd2..48771e860f 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_directory_streams.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_directory_streams.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval local directory resource builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Each test uses a process-unique directory and removes it before and after execution. @@ -15,7 +15,7 @@ use super::support::*; #[test] fn execute_program_dispatches_directory_stream_builtins() { let pid = std::process::id(); - let dir = format!("elephc_eval_dir_stream_{pid}"); + let dir = format!("elephc_magician_dir_stream_{pid}"); let source = format!( r#"mkdir("{dir}"); file_put_contents("{dir}/a.txt", "a"); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs similarity index 92% rename from crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs index e862f27272..4e4c653e0d 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval local file stream resource builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases use process-unique local files and clean them before and after execution. @@ -15,9 +15,9 @@ use super::support::*; #[test] fn execute_program_dispatches_file_stream_builtins() { let pid = std::process::id(); - let file = format!("elephc_eval_stream_file_{pid}.txt"); - let copy = format!("elephc_eval_stream_copy_{pid}.txt"); - let call = format!("elephc_eval_stream_call_{pid}.txt"); + let file = format!("elephc_magician_stream_file_{pid}.txt"); + let copy = format!("elephc_magician_stream_copy_{pid}.txt"); + let call = format!("elephc_magician_stream_call_{pid}.txt"); let source = format!( r#"$h = fopen(filename: "{file}", mode: "w+"); echo is_resource($h) ? "open" : "bad"; echo ":"; @@ -91,7 +91,7 @@ return true;"# #[test] fn execute_program_dispatches_file_stream_flock_builtin() { let pid = std::process::id(); - let file = format!("elephc_eval_stream_lock_{pid}.txt"); + let file = format!("elephc_magician_stream_lock_{pid}.txt"); let source = format!( r#"file_put_contents("{file}", "x"); $h = fopen("{file}", "r+"); @@ -129,9 +129,9 @@ return true;"# #[test] fn execute_program_dispatches_file_stream_line_builtins() { let pid = std::process::id(); - let file = format!("elephc_eval_stream_lines_{pid}.txt"); - let ending = format!("elephc_eval_stream_ending_{pid}.txt"); - let formatted = format!("elephc_eval_stream_formatted_{pid}.txt"); + let file = format!("elephc_magician_stream_lines_{pid}.txt"); + let ending = format!("elephc_magician_stream_ending_{pid}.txt"); + let formatted = format!("elephc_magician_stream_formatted_{pid}.txt"); let source = format!( r#"file_put_contents("{file}", "a\nbc\nxyz"); $h = fopen("{file}", "r"); @@ -190,10 +190,10 @@ return true;"# #[test] fn execute_program_dispatches_file_stream_csv_builtins() { let pid = std::process::id(); - let file = format!("elephc_eval_stream_csv_{pid}.txt"); - let semi = format!("elephc_eval_stream_csv_semi_{pid}.txt"); - let call = format!("elephc_eval_stream_csv_call_{pid}.txt"); - let scan = format!("elephc_eval_stream_scan_{pid}.txt"); + let file = format!("elephc_magician_stream_csv_{pid}.txt"); + let semi = format!("elephc_magician_stream_csv_semi_{pid}.txt"); + let call = format!("elephc_magician_stream_csv_call_{pid}.txt"); + let scan = format!("elephc_magician_stream_scan_{pid}.txt"); let source = format!( r#"$h = fopen("{file}", "w+"); echo fputcsv($h, ["a", "b,c", "d\"e"]) > 0 ? "put" : "bad"; echo ":"; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs similarity index 92% rename from crates/elephc-eval/src/interpreter/tests/builtins_filesystem_metadata.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs index badde7709b..2c18e4bcf8 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_metadata.rs @@ -2,7 +2,7 @@ //! Interpreter tests for filesystem path, metadata, stat, and space builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases use temporary files while keeping filesystem metadata assertions focused. @@ -41,9 +41,9 @@ return function_exists("dirname");"#, fn execute_program_dispatches_realpath_builtin() { let program = parse_fragment( br#"echo realpath(".") !== false ? "resolved" : "bad"; echo ":"; -echo realpath(path: "elephc-eval-missing-path") === false ? "false" : "bad"; echo ":"; +echo realpath(path: "elephc-magician-missing-path") === false ? "false" : "bad"; echo ":"; echo call_user_func("realpath", ".") !== false ? "call" : "bad"; echo ":"; -echo call_user_func_array("realpath", ["path" => "elephc-eval-missing-path"]) === false ? "array-false" : "bad"; +echo call_user_func_array("realpath", ["path" => "elephc-magician-missing-path"]) === false ? "array-false" : "bad"; echo ":"; return function_exists("realpath");"#, ) @@ -120,8 +120,8 @@ return PATHINFO_ALL;"#, /// Verifies eval local filesystem builtins read, write, stat, delete, and dispatch. #[test] fn execute_program_dispatches_filesystem_builtins() { - let filename = format!("elephc_eval_fs_probe_{}.txt", std::process::id()); - let missing = format!("elephc_eval_fs_missing_{}.txt", std::process::id()); + let filename = format!("elephc_magician_fs_probe_{}.txt", std::process::id()); + let missing = format!("elephc_magician_fs_missing_{}.txt", std::process::id()); let source = format!( r#"echo file_put_contents("{filename}", "hello") . ":"; echo file_get_contents("{filename}") . ":"; @@ -163,7 +163,7 @@ fn execute_program_dispatches_disk_space_builtins() { br#"echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; -echo disk_free_space("no/such/path/elephc-eval") === 0.0 ? "missing" : "bad"; echo ":"; +echo disk_free_space("no/such/path/elephc-magician") === 0.0 ? "missing" : "bad"; echo ":"; echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; echo ":"; echo function_exists("disk_free_space"); @@ -181,9 +181,9 @@ return function_exists("disk_total_space");"#, /// Verifies eval stat metadata builtins expose scalar file metadata and link probes. #[test] fn execute_program_dispatches_stat_metadata_builtins() { - let filename = format!("elephc_eval_stat_probe_{}.txt", std::process::id()); - let missing = format!("elephc_eval_stat_missing_{}.txt", std::process::id()); - let link = format!("elephc_eval_stat_link_{}.txt", std::process::id()); + let filename = format!("elephc_magician_stat_probe_{}.txt", std::process::id()); + let missing = format!("elephc_magician_stat_missing_{}.txt", std::process::id()); + let link = format!("elephc_magician_stat_link_{}.txt", std::process::id()); let source = format!( r#"echo filemtime("{filename}") > 0 ? "mtime" : "bad"; echo ":"; echo fileatime("{filename}") > 0 ? "atime" : "bad"; echo ":"; @@ -236,9 +236,9 @@ return true;"# #[test] fn execute_program_dispatches_stat_array_builtins() { let pid = std::process::id(); - let filename = format!("elephc_eval_stat_array_{pid}.txt"); - let link = format!("elephc_eval_lstat_array_{pid}.txt"); - let missing = format!("elephc_eval_stat_array_missing_{pid}.txt"); + let filename = format!("elephc_magician_stat_array_{pid}.txt"); + let link = format!("elephc_magician_lstat_array_{pid}.txt"); + let missing = format!("elephc_magician_stat_array_missing_{pid}.txt"); let source = format!( r#"$stat = stat("{filename}"); $lstat = lstat("{link}"); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs similarity index 90% rename from crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs index 28d6f51a21..1774ddc2d2 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_filesystem_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs @@ -2,7 +2,7 @@ //! Interpreter tests for filesystem listing and mutating builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover directory traversal, globbing, file changes, and touch behavior. @@ -14,13 +14,13 @@ use super::support::*; #[test] fn execute_program_dispatches_path_operation_builtins() { let pid = std::process::id(); - let dir = format!("elephc_eval_ops_dir_{pid}"); - let call_dir = format!("elephc_eval_ops_call_dir_{pid}"); - let src = format!("elephc_eval_ops_src_{pid}.txt"); - let copy = format!("elephc_eval_ops_copy_{pid}.txt"); - let moved = format!("elephc_eval_ops_moved_{pid}.txt"); - let symlink = format!("elephc_eval_ops_symlink_{pid}.txt"); - let hardlink = format!("elephc_eval_ops_hardlink_{pid}.txt"); + let dir = format!("elephc_magician_ops_dir_{pid}"); + let call_dir = format!("elephc_magician_ops_call_dir_{pid}"); + let src = format!("elephc_magician_ops_src_{pid}.txt"); + let copy = format!("elephc_magician_ops_copy_{pid}.txt"); + let moved = format!("elephc_magician_ops_moved_{pid}.txt"); + let symlink = format!("elephc_magician_ops_symlink_{pid}.txt"); + let hardlink = format!("elephc_magician_ops_hardlink_{pid}.txt"); let source = format!( r#"file_put_contents("{src}", "hello"); echo mkdir("{dir}") ? "mkdir" : "bad"; echo ":"; @@ -41,7 +41,7 @@ echo function_exists("mkdir"); echo function_exists("rmdir"); echo function_exis echo function_exists("rename"); echo function_exists("symlink"); echo function_exists("link"); echo function_exists("readlink"); echo function_exists("linkinfo"); echo function_exists("clearstatcache"); return true;"#, - missing = format!("elephc_eval_ops_missing_{pid}.txt"), + missing = format!("elephc_magician_ops_missing_{pid}.txt"), ); let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); for path in [&symlink, &hardlink, &moved, ©, &src] { @@ -71,8 +71,8 @@ return true;"#, #[test] fn execute_program_dispatches_stream_resolve_include_path_builtin() { let pid = std::process::id(); - let file = format!("elephc_eval_stream_resolve_{pid}.txt"); - let missing = format!("elephc_eval_stream_resolve_missing_{pid}.txt"); + let file = format!("elephc_magician_stream_resolve_{pid}.txt"); + let missing = format!("elephc_magician_stream_resolve_missing_{pid}.txt"); let source = format!( r#"file_put_contents("{file}", "payload"); $resolved = stream_resolve_include_path("{file}"); @@ -106,10 +106,10 @@ return function_exists("stream_resolve_include_path");"#, #[test] fn execute_program_dispatches_file_listing_builtins() { let pid = std::process::id(); - let lines = format!("elephc_eval_listing_lines_{pid}.txt"); - let empty = format!("elephc_eval_listing_empty_{pid}.txt"); - let missing = format!("elephc_eval_listing_missing_{pid}.txt"); - let dir = format!("elephc_eval_listing_dir_{pid}"); + let lines = format!("elephc_magician_listing_lines_{pid}.txt"); + let empty = format!("elephc_magician_listing_empty_{pid}.txt"); + let missing = format!("elephc_magician_listing_missing_{pid}.txt"); + let dir = format!("elephc_magician_listing_dir_{pid}"); let source = format!( r#"file_put_contents("{lines}", "one\ntwo"); file_put_contents("{empty}", ""); @@ -164,7 +164,7 @@ return true;"# #[test] fn execute_program_dispatches_glob_builtin() { let pid = std::process::id(); - let dir = format!("elephc_eval_glob_dir_{pid}"); + let dir = format!("elephc_magician_glob_dir_{pid}"); let source = format!( r#"mkdir("{dir}"); file_put_contents("{dir}/a.txt", "a"); @@ -216,8 +216,8 @@ return true;"# #[test] fn execute_program_dispatches_file_modify_builtins() { let pid = std::process::id(); - let filename = format!("elephc_eval_modify_{pid}.txt"); - let missing = format!("elephc_eval_modify_missing_{pid}.txt"); + let filename = format!("elephc_magician_modify_{pid}.txt"); + let missing = format!("elephc_magician_modify_missing_{pid}.txt"); let prefix = format!("evm{pid}_"); let call_prefix = format!("evc{pid}_"); let source = format!( @@ -277,9 +277,9 @@ return true;"# #[test] fn execute_program_dispatches_file_ownership_builtins() { let pid = std::process::id(); - let filename = format!("elephc_eval_ownership_{pid}.txt"); - let link = format!("elephc_eval_ownership_link_{pid}.txt"); - let missing = format!("elephc_eval_ownership_missing_{pid}.txt"); + let filename = format!("elephc_magician_ownership_{pid}.txt"); + let link = format!("elephc_magician_ownership_link_{pid}.txt"); + let missing = format!("elephc_magician_ownership_missing_{pid}.txt"); let uid = unsafe { libc::geteuid() }; let gid = unsafe { libc::getegid() }; let source = format!( @@ -320,9 +320,9 @@ return function_exists("lchgrp");"# #[test] fn execute_program_dispatches_touch_builtin() { let pid = std::process::id(); - let created = format!("elephc_eval_touch_created_{pid}.txt"); - let stamped = format!("elephc_eval_touch_stamped_{pid}.txt"); - let missing = format!("elephc_eval_touch_missing_{pid}/x.txt"); + let created = format!("elephc_magician_touch_created_{pid}.txt"); + let stamped = format!("elephc_magician_touch_stamped_{pid}.txt"); + let missing = format!("elephc_magician_touch_missing_{pid}/x.txt"); let source = format!( r#"echo touch(filename: "{created}") && file_exists("{created}") ? "create" : "bad"; echo ":"; file_put_contents("{stamped}", "x"); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_json.rs b/crates/elephc-magician/src/interpreter/tests/builtins_json.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_json.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_json.rs index 39bd8621a3..77f52908cf 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_json.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_json.rs @@ -2,7 +2,7 @@ //! Interpreter tests for core and JSON builtin dispatch. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases assert JSON state, flags, throwable behavior, and callable dispatch. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_language_constructs.rs b/crates/elephc-magician/src/interpreter/tests/builtins_language_constructs.rs similarity index 97% rename from crates/elephc-eval/src/interpreter/tests/builtins_language_constructs.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_language_constructs.rs index 80cd9e583f..1bb7073ee6 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_language_constructs.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_language_constructs.rs @@ -3,7 +3,7 @@ //! through eval's builtin registry. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - `exit` and `die` are probed for visibility only because executing them diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_math_formatting.rs b/crates/elephc-magician/src/interpreter/tests/builtins_math_formatting.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_math_formatting.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_math_formatting.rs index 76a7813fe0..768c9dfd30 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_math_formatting.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_math_formatting.rs @@ -2,7 +2,7 @@ //! Interpreter tests for math and formatting builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover numeric hooks, printf-family formatting, min/max, clamp, and constants. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_process_pipes.rs b/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs similarity index 93% rename from crates/elephc-eval/src/interpreter/tests/builtins_process_pipes.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs index c24fccbdf9..e517fad9aa 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_process_pipes.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_process_pipes.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval process pipe stream builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Tests use shell commands that exit immediately and clean up their temp file. @@ -15,7 +15,7 @@ use super::support::*; #[test] fn execute_program_dispatches_process_pipe_builtins() { let pid = std::process::id(); - let file = format!("elephc_eval_popen_{pid}.txt"); + let file = format!("elephc_magician_popen_{pid}.txt"); let source = format!( r#"$h = popen("printf eval-popen", "r"); echo is_resource($h) ? "open" : "bad"; echo ":"; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_readline.rs b/crates/elephc-magician/src/interpreter/tests/builtins_readline.rs similarity index 93% rename from crates/elephc-eval/src/interpreter/tests/builtins_readline.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_readline.rs index 21e3a7280f..455358420e 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_readline.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_readline.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval's `readline()` builtin. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - The test harness runs with stdin at EOF, so `readline()` returns false diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs index 0d9251906a..e385b45fc5 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval-backed ReflectionFunction objects. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Free eval functions retain function and parameter metadata used by diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs index b0655f59dc..fd701128da 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_scalars.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs @@ -2,7 +2,7 @@ //! Interpreter tests for scalar type, resource, cast, and class-introspection builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases check runtime tag inspection and mutating scalar conversions. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_spl_autoload.rs b/crates/elephc-magician/src/interpreter/tests/builtins_spl_autoload.rs similarity index 97% rename from crates/elephc-eval/src/interpreter/tests/builtins_spl_autoload.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_spl_autoload.rs index b287de1be6..4e789b9cf7 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_spl_autoload.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_spl_autoload.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval SPL autoload helper builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Autoload registration/call helpers mirror the main backend's conservative stubs. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_stream_contexts.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_contexts.rs similarity index 97% rename from crates/elephc-eval/src/interpreter/tests/builtins_stream_contexts.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_stream_contexts.rs index 4e3f574bf1..5869b223da 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_stream_contexts.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_contexts.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval stream context metadata builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Context resources are eval-owned resource cells with inspectable options. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_stream_extensions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_extensions.rs similarity index 95% rename from crates/elephc-eval/src/interpreter/tests/builtins_stream_extensions.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_stream_extensions.rs index 40bd313449..387514fb48 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_stream_extensions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_extensions.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval stream wrapper, filter, and bucket helper builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Filter resources are eval-local handles and do not transform stream bytes. @@ -15,7 +15,7 @@ use super::support::*; #[test] fn execute_program_dispatches_stream_extension_builtins() { let pid = std::process::id(); - let file = format!("elephc_eval_stream_extensions_{pid}.txt"); + let file = format!("elephc_magician_stream_extensions_{pid}.txt"); let source = format!( r#"file_put_contents("{file}", "abc"); $h = fopen("{file}", "r+"); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_stream_settings.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_settings.rs similarity index 94% rename from crates/elephc-eval/src/interpreter/tests/builtins_stream_settings.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_stream_settings.rs index 97c9734e27..35437a5806 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_stream_settings.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_settings.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval stream descriptor setting builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Local file streams expose terminal/blocking probes through host libc. @@ -16,7 +16,7 @@ use super::support::*; #[test] fn execute_program_dispatches_stream_setting_builtins() { let pid = std::process::id(); - let file = format!("elephc_eval_stream_settings_{pid}.txt"); + let file = format!("elephc_magician_stream_settings_{pid}.txt"); let source = format!( r#"file_put_contents("{file}", "x"); $h = fopen("{file}", "r+"); diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_stream_sockets.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs similarity index 98% rename from crates/elephc-eval/src/interpreter/tests/builtins_stream_sockets.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs index 2ac57fe097..df446b7d7a 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_stream_sockets.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval TCP stream socket builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - Tests bind localhost port 0 so the OS chooses available ports. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs index 2eeff51eeb..d425a64573 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_strings_binary.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_binary.rs @@ -2,7 +2,7 @@ //! Interpreter tests for binary string conversion and escaping builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover byte-preserving string helpers and base64 conversion. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs index ed8332be2a..907e76af15 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_strings_encoding.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs @@ -2,7 +2,7 @@ //! Interpreter tests for string splitting, replacing, regex, entity, URL, ctype, and hash builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover byte-string and encoded string builtin behavior. @@ -322,7 +322,7 @@ return count($algos);"#, /// Verifies eval one-shot hash digest builtins use the crypto bridge and dispatch dynamically. #[test] fn execute_program_dispatches_hash_digest_builtins() { - let filename = format!("elephc_eval_hash_file_{}.txt", std::process::id()); + let filename = format!("elephc_magician_hash_file_{}.txt", std::process::id()); let source = format!( r#"echo md5("abc"); echo ":"; echo sha1(string: "abc"); echo ":"; diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_strings_text.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_text.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_strings_text.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_strings_text.rs index a1abf455d3..ae40a2b1c1 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_strings_text.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_text.rs @@ -2,7 +2,7 @@ //! Interpreter tests for text-case, wrapping, search, comparison, and trim string builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover PHP byte-string behavior for text-oriented helpers. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs index 167ad2c7db..51f6f319df 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs @@ -2,7 +2,7 @@ //! Interpreter tests for isset, empty, function/class probes, dynamic constants, and class declarations. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover symbol-table and metadata probes backed by the eval context. diff --git a/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs rename to crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs index 568c96f984..9242320ac3 100644 --- a/crates/elephc-eval/src/interpreter/tests/builtins_system_network.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs @@ -2,7 +2,7 @@ //! Interpreter tests for time, system, SPL, environment, host, protocol, and IP builtins. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases isolate platform-facing builtins behind deterministic fake assertions where possible. diff --git a/crates/elephc-eval/src/interpreter/tests/class_constants.rs b/crates/elephc-magician/src/interpreter/tests/class_constants.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/class_constants.rs rename to crates/elephc-magician/src/interpreter/tests/class_constants.rs index 3d19b8cbcb..bbfa8d3e1d 100644 --- a/crates/elephc-eval/src/interpreter/tests/class_constants.rs +++ b/crates/elephc-magician/src/interpreter/tests/class_constants.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval-declared class constants. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover inherited lookup, scoped receivers, visibility, and dynamic storage. diff --git a/crates/elephc-eval/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/classes.rs rename to crates/elephc-magician/src/interpreter/tests/classes.rs index caf347ced1..7e038b00e9 100644 --- a/crates/elephc-eval/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval-declared class runtime behavior. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover class property semantics that need eval runtime state. diff --git a/crates/elephc-eval/src/interpreter/tests/control_flow.rs b/crates/elephc-magician/src/interpreter/tests/control_flow.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/control_flow.rs rename to crates/elephc-magician/src/interpreter/tests/control_flow.rs index 36f1f07b87..d51dfd38c7 100644 --- a/crates/elephc-eval/src/interpreter/tests/control_flow.rs +++ b/crates/elephc-magician/src/interpreter/tests/control_flow.rs @@ -2,7 +2,7 @@ //! Interpreter tests for branching, loops, comparisons, match, logical operators, and foreach. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases exercise control-flow outcomes without leaving the fake runtime. diff --git a/crates/elephc-eval/src/interpreter/tests/core.rs b/crates/elephc-magician/src/interpreter/tests/core.rs similarity index 98% rename from crates/elephc-eval/src/interpreter/tests/core.rs rename to crates/elephc-magician/src/interpreter/tests/core.rs index 87ebaa12c1..a6d3ab7fc2 100644 --- a/crates/elephc-eval/src/interpreter/tests/core.rs +++ b/crates/elephc-magician/src/interpreter/tests/core.rs @@ -2,7 +2,7 @@ //! Interpreter tests for scope mutation, exceptions, includes, and early execution results. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover baseline eval execution before builtin-specific dispatch. @@ -401,7 +401,7 @@ fn execute_program_nested_eval_propagates_throw_as_uncaught_outcome() { #[test] fn execute_program_include_uses_call_site_and_returns_file_result() { let dir = std::env::temp_dir().join(format!( - "elephc-eval-include-{}-call-site", + "elephc-magician-include-{}-call-site", std::process::id() )); let path = dir.join("piece.php"); @@ -442,7 +442,7 @@ fn execute_program_include_uses_call_site_and_returns_file_result() { /// Verifies regular include marks a file so later include_once skips it and returns true. #[test] fn execute_program_include_once_skips_regularly_included_file() { - let dir = std::env::temp_dir().join(format!("elephc-eval-include-{}-once", std::process::id())); + let dir = std::env::temp_dir().join(format!("elephc-magician-include-{}-once", std::process::id())); let path = dir.join("once.php"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("create include_once fixture directory"); @@ -468,7 +468,7 @@ fn execute_program_include_once_skips_regularly_included_file() { #[test] fn execute_program_missing_include_warns_and_returns_false() { let missing = std::env::temp_dir().join(format!( - "elephc-eval-missing-{}-include.php", + "elephc-magician-missing-{}-include.php", std::process::id() )); let source = format!(r#"return include "{}";"#, missing.to_string_lossy()); @@ -487,7 +487,7 @@ fn execute_program_missing_include_warns_and_returns_false() { #[test] fn execute_program_missing_require_is_runtime_fatal() { let missing = std::env::temp_dir().join(format!( - "elephc-eval-missing-{}-require.php", + "elephc-magician-missing-{}-require.php", std::process::id() )); let source = format!(r#"require "{}";"#, missing.to_string_lossy()); diff --git a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs rename to crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 9de01147cc..6c1e81c8b4 100644 --- a/crates/elephc-eval/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -2,7 +2,7 @@ //! Interpreter tests for variable calls, call_user_func, call_user_func_array, and duplicate dynamic declarations. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases verify callable dispatch across eval functions, builtins, native functions, and object methods. diff --git a/crates/elephc-eval/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs similarity index 98% rename from crates/elephc-eval/src/interpreter/tests/enums.rs rename to crates/elephc-magician/src/interpreter/tests/enums.rs index a4dc4045d3..a117d3ed6f 100644 --- a/crates/elephc-eval/src/interpreter/tests/enums.rs +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval-declared enum runtime behavior. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases verify enum singleton cases, class-like metadata, backed values, diff --git a/crates/elephc-eval/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/expressions.rs rename to crates/elephc-magician/src/interpreter/tests/expressions.rs index 0f02001f40..d0d9248804 100644 --- a/crates/elephc-eval/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -2,7 +2,7 @@ //! Interpreter tests for scalar expressions, echo/print, objects, and construction. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases assert EvalIR expression execution against fake runtime values. diff --git a/crates/elephc-eval/src/interpreter/tests/functions_namespaces.rs b/crates/elephc-magician/src/interpreter/tests/functions_namespaces.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/functions_namespaces.rs rename to crates/elephc-magician/src/interpreter/tests/functions_namespaces.rs index b045a28ef3..44c5231ad0 100644 --- a/crates/elephc-eval/src/interpreter/tests/functions_namespaces.rs +++ b/crates/elephc-magician/src/interpreter/tests/functions_namespaces.rs @@ -2,7 +2,7 @@ //! Interpreter tests for nested eval, magic constants, namespaces, functions, globals, and argument rules. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases share a persistent eval context when declarations must survive across calls. diff --git a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/method_arguments.rs rename to crates/elephc-magician/src/interpreter/tests/method_arguments.rs index 5d9940fdfe..30af1fedc0 100644 --- a/crates/elephc-eval/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval-declared method and constructor argument binding. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover named arguments and named unpacking on instance methods, diff --git a/crates/elephc-eval/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs similarity index 95% rename from crates/elephc-eval/src/interpreter/tests/mod.rs rename to crates/elephc-magician/src/interpreter/tests/mod.rs index acb80fd20c..9b86ee2874 100644 --- a/crates/elephc-eval/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -4,7 +4,7 @@ //! execution surface instead of one large mixed test bucket. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - `support` exposes the fake runtime cells used by all interpreter tests. diff --git a/crates/elephc-eval/src/interpreter/tests/native_scope.rs b/crates/elephc-magician/src/interpreter/tests/native_scope.rs similarity index 99% rename from crates/elephc-eval/src/interpreter/tests/native_scope.rs rename to crates/elephc-magician/src/interpreter/tests/native_scope.rs index cbb404dc56..a2525e9a91 100644 --- a/crates/elephc-eval/src/interpreter/tests/native_scope.rs +++ b/crates/elephc-magician/src/interpreter/tests/native_scope.rs @@ -2,7 +2,7 @@ //! Interpreter tests for native function dispatch, scope array mutation, ownership, break, and continue. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover integration edges between scope cells and fake runtime hooks. diff --git a/crates/elephc-eval/src/interpreter/tests/static_members.rs b/crates/elephc-magician/src/interpreter/tests/static_members.rs similarity index 98% rename from crates/elephc-eval/src/interpreter/tests/static_members.rs rename to crates/elephc-magician/src/interpreter/tests/static_members.rs index d82c07a987..4d6d450301 100644 --- a/crates/elephc-eval/src/interpreter/tests/static_members.rs +++ b/crates/elephc-magician/src/interpreter/tests/static_members.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval-declared static properties and static methods. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover storage persistence, visibility checks, and late static binding. diff --git a/crates/elephc-eval/src/interpreter/tests/support/array_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/tests/support/array_ops.rs rename to crates/elephc-magician/src/interpreter/tests/support/array_ops.rs diff --git a/crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/tests/support/cell_ops.rs rename to crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs diff --git a/crates/elephc-eval/src/interpreter/tests/support/conversions.rs b/crates/elephc-magician/src/interpreter/tests/support/conversions.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/tests/support/conversions.rs rename to crates/elephc-magician/src/interpreter/tests/support/conversions.rs diff --git a/crates/elephc-eval/src/interpreter/tests/support/lifecycle_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/lifecycle_ops.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/tests/support/lifecycle_ops.rs rename to crates/elephc-magician/src/interpreter/tests/support/lifecycle_ops.rs diff --git a/crates/elephc-eval/src/interpreter/tests/support/mod.rs b/crates/elephc-magician/src/interpreter/tests/support/mod.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/tests/support/mod.rs rename to crates/elephc-magician/src/interpreter/tests/support/mod.rs diff --git a/crates/elephc-eval/src/interpreter/tests/support/numeric_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/numeric_ops.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/tests/support/numeric_ops.rs rename to crates/elephc-magician/src/interpreter/tests/support/numeric_ops.rs diff --git a/crates/elephc-eval/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/tests/support/object_ops.rs rename to crates/elephc-magician/src/interpreter/tests/support/object_ops.rs diff --git a/crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs similarity index 100% rename from crates/elephc-eval/src/interpreter/tests/support/runtime_ops.rs rename to crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs diff --git a/crates/elephc-eval/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs similarity index 97% rename from crates/elephc-eval/src/interpreter/tests/trait_adaptations.rs rename to crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index a4c058a6b7..0f572e0802 100644 --- a/crates/elephc-eval/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -2,7 +2,7 @@ //! Interpreter tests for eval-declared trait adaptations. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover conflict resolution, aliases, and visibility changes diff --git a/crates/elephc-eval/src/json_validate.rs b/crates/elephc-magician/src/json_validate.rs similarity index 100% rename from crates/elephc-eval/src/json_validate.rs rename to crates/elephc-magician/src/json_validate.rs diff --git a/crates/elephc-eval/src/lexer/mod.rs b/crates/elephc-magician/src/lexer/mod.rs similarity index 100% rename from crates/elephc-eval/src/lexer/mod.rs rename to crates/elephc-magician/src/lexer/mod.rs diff --git a/crates/elephc-eval/src/lexer/scan.rs b/crates/elephc-magician/src/lexer/scan.rs similarity index 100% rename from crates/elephc-eval/src/lexer/scan.rs rename to crates/elephc-magician/src/lexer/scan.rs diff --git a/crates/elephc-eval/src/lexer/token.rs b/crates/elephc-magician/src/lexer/token.rs similarity index 100% rename from crates/elephc-eval/src/lexer/token.rs rename to crates/elephc-magician/src/lexer/token.rs diff --git a/crates/elephc-eval/src/lib.rs b/crates/elephc-magician/src/lib.rs similarity index 91% rename from crates/elephc-eval/src/lib.rs rename to crates/elephc-magician/src/lib.rs index 1077e78add..315237b116 100644 --- a/crates/elephc-eval/src/lib.rs +++ b/crates/elephc-magician/src/lib.rs @@ -5,7 +5,7 @@ //! //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_*` symbols. -//! - `cargo test -p elephc-eval` for ABI-shape validation. +//! - `cargo test -p elephc-magician` for ABI-shape validation. //! //! Key details: //! - No Rust panic or Rust-specific enum crosses the ABI boundary. diff --git a/crates/elephc-eval/src/lower.rs b/crates/elephc-magician/src/lower.rs similarity index 100% rename from crates/elephc-eval/src/lower.rs rename to crates/elephc-magician/src/lower.rs diff --git a/crates/elephc-eval/src/parser/cursor.rs b/crates/elephc-magician/src/parser/cursor.rs similarity index 100% rename from crates/elephc-eval/src/parser/cursor.rs rename to crates/elephc-magician/src/parser/cursor.rs diff --git a/crates/elephc-eval/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs similarity index 100% rename from crates/elephc-eval/src/parser/expressions.rs rename to crates/elephc-magician/src/parser/expressions.rs diff --git a/crates/elephc-eval/src/parser/mod.rs b/crates/elephc-magician/src/parser/mod.rs similarity index 100% rename from crates/elephc-eval/src/parser/mod.rs rename to crates/elephc-magician/src/parser/mod.rs diff --git a/crates/elephc-eval/src/parser/state.rs b/crates/elephc-magician/src/parser/state.rs similarity index 100% rename from crates/elephc-eval/src/parser/state.rs rename to crates/elephc-magician/src/parser/state.rs diff --git a/crates/elephc-eval/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs similarity index 100% rename from crates/elephc-eval/src/parser/statements.rs rename to crates/elephc-magician/src/parser/statements.rs diff --git a/crates/elephc-eval/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs similarity index 99% rename from crates/elephc-eval/src/parser/tests/arrays_objects.rs rename to crates/elephc-magician/src/parser/tests/arrays_objects.rs index 38d81d44ef..7bec8dc577 100644 --- a/crates/elephc-eval/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -2,7 +2,7 @@ //! Parser tests for arrays, array writes, object properties, methods, and object construction. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover postfix and aggregate expression syntax. diff --git a/crates/elephc-eval/src/parser/tests/assignments.rs b/crates/elephc-magician/src/parser/tests/assignments.rs similarity index 99% rename from crates/elephc-eval/src/parser/tests/assignments.rs rename to crates/elephc-magician/src/parser/tests/assignments.rs index f77972a95f..f95023c4b8 100644 --- a/crates/elephc-eval/src/parser/tests/assignments.rs +++ b/crates/elephc-magician/src/parser/tests/assignments.rs @@ -2,7 +2,7 @@ //! Parser tests for assignment, compound assignment, increment/decrement, and echo statements. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases assert direct statement lowering into EvalIR stores and echoes. diff --git a/crates/elephc-eval/src/parser/tests/calls.rs b/crates/elephc-magician/src/parser/tests/calls.rs similarity index 99% rename from crates/elephc-eval/src/parser/tests/calls.rs rename to crates/elephc-magician/src/parser/tests/calls.rs index 6dfc863bfe..8079d3e8d8 100644 --- a/crates/elephc-eval/src/parser/tests/calls.rs +++ b/crates/elephc-magician/src/parser/tests/calls.rs @@ -2,7 +2,7 @@ //! Parser tests for print, strings, calls, includes, constants, named args, spread args, isset, and empty. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover function-like and call-expression parsing. diff --git a/crates/elephc-eval/src/parser/tests/class_constants.rs b/crates/elephc-magician/src/parser/tests/class_constants.rs similarity index 98% rename from crates/elephc-eval/src/parser/tests/class_constants.rs rename to crates/elephc-magician/src/parser/tests/class_constants.rs index 804b718f3c..966f567067 100644 --- a/crates/elephc-eval/src/parser/tests/class_constants.rs +++ b/crates/elephc-magician/src/parser/tests/class_constants.rs @@ -2,7 +2,7 @@ //! Parser tests for eval class constant declarations and fetches. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases verify `const` members and `Class::CONST` lower to EvalIR. diff --git a/crates/elephc-eval/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs similarity index 99% rename from crates/elephc-eval/src/parser/tests/classes_errors.rs rename to crates/elephc-magician/src/parser/tests/classes_errors.rs index 4db8538dc7..30d86d4f00 100644 --- a/crates/elephc-eval/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -2,7 +2,7 @@ //! Parser tests for class declarations and parser diagnostics. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover dynamic class metadata and malformed fragment errors. diff --git a/crates/elephc-eval/src/parser/tests/control_statements.rs b/crates/elephc-magician/src/parser/tests/control_statements.rs similarity index 98% rename from crates/elephc-eval/src/parser/tests/control_statements.rs rename to crates/elephc-magician/src/parser/tests/control_statements.rs index d319f4ba85..359d1511f4 100644 --- a/crates/elephc-eval/src/parser/tests/control_statements.rs +++ b/crates/elephc-magician/src/parser/tests/control_statements.rs @@ -2,7 +2,7 @@ //! Parser tests for branch, loop, switch, foreach, and function declaration statements. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases verify statement body shapes and ordered EvalIR blocks. diff --git a/crates/elephc-eval/src/parser/tests/enums.rs b/crates/elephc-magician/src/parser/tests/enums.rs similarity index 97% rename from crates/elephc-eval/src/parser/tests/enums.rs rename to crates/elephc-magician/src/parser/tests/enums.rs index 3e350d0cc0..15e4b2887f 100644 --- a/crates/elephc-eval/src/parser/tests/enums.rs +++ b/crates/elephc-magician/src/parser/tests/enums.rs @@ -2,7 +2,7 @@ //! Parser tests for eval-declared pure and backed enum declarations. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover enum cases, backing types, interfaces, constants, and methods. diff --git a/crates/elephc-eval/src/parser/tests/exceptions_control.rs b/crates/elephc-magician/src/parser/tests/exceptions_control.rs similarity index 99% rename from crates/elephc-eval/src/parser/tests/exceptions_control.rs rename to crates/elephc-magician/src/parser/tests/exceptions_control.rs index 89ab1a70e4..b2edbffc3e 100644 --- a/crates/elephc-eval/src/parser/tests/exceptions_control.rs +++ b/crates/elephc-magician/src/parser/tests/exceptions_control.rs @@ -2,7 +2,7 @@ //! Parser tests for while, do-while, break, continue, return, throw, try/catch/finally, and unset. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases cover control-transfer and exception statement parsing. diff --git a/crates/elephc-eval/src/parser/tests/magic_comments.rs b/crates/elephc-magician/src/parser/tests/magic_comments.rs similarity index 98% rename from crates/elephc-eval/src/parser/tests/magic_comments.rs rename to crates/elephc-magician/src/parser/tests/magic_comments.rs index 6bd1338a53..249478f1d3 100644 --- a/crates/elephc-eval/src/parser/tests/magic_comments.rs +++ b/crates/elephc-magician/src/parser/tests/magic_comments.rs @@ -2,7 +2,7 @@ //! Parser tests for static/global declarations, magic constants, and comments. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases verify source metadata and comment error handling. diff --git a/crates/elephc-eval/src/parser/tests/mod.rs b/crates/elephc-magician/src/parser/tests/mod.rs similarity index 90% rename from crates/elephc-eval/src/parser/tests/mod.rs rename to crates/elephc-magician/src/parser/tests/mod.rs index 476774d0c7..a256e85aa9 100644 --- a/crates/elephc-eval/src/parser/tests/mod.rs +++ b/crates/elephc-magician/src/parser/tests/mod.rs @@ -4,7 +4,7 @@ //! diagnostic cases separate. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - `support` re-exports parser helpers and EvalIR types for test assertions. diff --git a/crates/elephc-eval/src/parser/tests/namespaces.rs b/crates/elephc-magician/src/parser/tests/namespaces.rs similarity index 99% rename from crates/elephc-eval/src/parser/tests/namespaces.rs rename to crates/elephc-magician/src/parser/tests/namespaces.rs index e3d6a4e3d8..67d4e4749c 100644 --- a/crates/elephc-eval/src/parser/tests/namespaces.rs +++ b/crates/elephc-magician/src/parser/tests/namespaces.rs @@ -2,7 +2,7 @@ //! Parser tests for namespaces and import resolution inside eval fragments. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases assert function, constant, class, and grouped imports. diff --git a/crates/elephc-eval/src/parser/tests/operators.rs b/crates/elephc-magician/src/parser/tests/operators.rs similarity index 99% rename from crates/elephc-eval/src/parser/tests/operators.rs rename to crates/elephc-magician/src/parser/tests/operators.rs index e39612c14e..e9286240d6 100644 --- a/crates/elephc-eval/src/parser/tests/operators.rs +++ b/crates/elephc-magician/src/parser/tests/operators.rs @@ -2,7 +2,7 @@ //! Parser tests for comparison, equality, logical, ternary, match, and unary expressions. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases focus on PHP precedence and associativity in EvalIR. diff --git a/crates/elephc-eval/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs similarity index 98% rename from crates/elephc-eval/src/parser/tests/static_members.rs rename to crates/elephc-magician/src/parser/tests/static_members.rs index 9cfb5de9ae..6fed5633ba 100644 --- a/crates/elephc-eval/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -2,7 +2,7 @@ //! Parser tests for eval static property and static method syntax. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases verify `::` receivers lower to EvalIR static-member nodes. diff --git a/crates/elephc-eval/src/parser/tests/support.rs b/crates/elephc-magician/src/parser/tests/support.rs similarity index 100% rename from crates/elephc-eval/src/parser/tests/support.rs rename to crates/elephc-magician/src/parser/tests/support.rs diff --git a/crates/elephc-eval/src/parser/tests/trait_adaptations.rs b/crates/elephc-magician/src/parser/tests/trait_adaptations.rs similarity index 97% rename from crates/elephc-eval/src/parser/tests/trait_adaptations.rs rename to crates/elephc-magician/src/parser/tests/trait_adaptations.rs index df21c07733..f68a2b03f1 100644 --- a/crates/elephc-eval/src/parser/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/parser/tests/trait_adaptations.rs @@ -2,7 +2,7 @@ //! Parser tests for eval class trait adaptation syntax. //! //! Called from: -//! - `cargo test -p elephc-eval` through Rust's test harness. +//! - `cargo test -p elephc-magician` through Rust's test harness. //! //! Key details: //! - These cases verify `insteadof` and `as` clauses lower into class metadata. diff --git a/crates/elephc-eval/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs similarity index 100% rename from crates/elephc-eval/src/runtime_hooks/externs.rs rename to crates/elephc-magician/src/runtime_hooks/externs.rs diff --git a/crates/elephc-eval/src/runtime_hooks/mod.rs b/crates/elephc-magician/src/runtime_hooks/mod.rs similarity index 100% rename from crates/elephc-eval/src/runtime_hooks/mod.rs rename to crates/elephc-magician/src/runtime_hooks/mod.rs diff --git a/crates/elephc-eval/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs similarity index 100% rename from crates/elephc-eval/src/runtime_hooks/ops.rs rename to crates/elephc-magician/src/runtime_hooks/ops.rs diff --git a/crates/elephc-eval/src/runtime_hooks/tags.rs b/crates/elephc-magician/src/runtime_hooks/tags.rs similarity index 100% rename from crates/elephc-eval/src/runtime_hooks/tags.rs rename to crates/elephc-magician/src/runtime_hooks/tags.rs diff --git a/crates/elephc-eval/src/scope.rs b/crates/elephc-magician/src/scope.rs similarity index 100% rename from crates/elephc-eval/src/scope.rs rename to crates/elephc-magician/src/scope.rs diff --git a/crates/elephc-eval/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs similarity index 99% rename from crates/elephc-eval/src/stream_resources.rs rename to crates/elephc-magician/src/stream_resources.rs index 2ac0cb5afd..0181766897 100644 --- a/crates/elephc-eval/src/stream_resources.rs +++ b/crates/elephc-magician/src/stream_resources.rs @@ -869,7 +869,7 @@ impl EvalOpenMode { fn eval_tmpfile_path() -> PathBuf { let mut path = std::env::temp_dir(); path.push(format!( - "elephc-eval-tmpfile-{}-{}", + "elephc-magician-tmpfile-{}-{}", std::process::id(), eval_tmpfile_nonce() )); diff --git a/crates/elephc-eval/src/value.rs b/crates/elephc-magician/src/value.rs similarity index 100% rename from crates/elephc-eval/src/value.rs rename to crates/elephc-magician/src/value.rs diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a7e2b15c27..359d826d93 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -13,7 +13,7 @@ These routines end up in every compiled binary. In the CLI flow they are usually ## Optional eval bridge -Programs that use PHP's `eval()` link one extra static bridge library, `elephc_eval`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. +Programs that use PHP's `eval()` link one extra static bridge library, `elephc_magician`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. diff --git a/docs/php/eval.md b/docs/php/eval.md index e8c76a60e6..4b432d34f6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -10,7 +10,7 @@ caller-visible local scope. It is a PHP language construct, not a normal callable: `function_exists("eval")` and `is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. -Programs that call `eval()` link the optional `elephc_eval` bridge. Programs +Programs that call `eval()` link the optional `elephc_magician` bridge. Programs that do not use `eval()` keep the ordinary fully native runtime path and do not link the bridge. @@ -625,7 +625,7 @@ and nested values through eval value hooks. ## Current limitations -Eval executes through the `elephc_eval` interpreter bridge, not through the full +Eval executes through the `elephc_magician` interpreter bridge, not through the full static AST -> EIR -> native codegen pipeline used for ordinary elephc source. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. diff --git a/scripts/test-linux-arm64.sh b/scripts/test-linux-arm64.sh index adb1deb796..16c9425001 100755 --- a/scripts/test-linux-arm64.sh +++ b/scripts/test-linux-arm64.sh @@ -70,8 +70,7 @@ trap cleanup EXIT INT TERM # Run tests with the project mounted as a volume. Build the bridge staticlib # crates first so libelephc_tls.a / libelephc_pdo.a / libelephc_crypto.a / # libelephc_phar.a / libelephc_tz.a / libelephc_image.a / libelephc_web.a / -# libelephc_eval.a -# exist in the target dir — +# libelephc_magician.a exist in the target dir — # `cargo test` alone never emits the staticlib crate-type. if [ "$TEST_ARG_COUNT" -eq 0 ]; then echo "Running all tests on Linux ARM64 with RUST_TEST_THREADS=$TEST_THREADS using temporary target volume '$TARGET_VOLUME'..." @@ -87,7 +86,7 @@ if [ "$TEST_ARG_COUNT" -eq 0 ]; then -v "$TARGET_VOLUME:/cargo-target" \ -w /app \ "$IMAGE" \ - sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-eval && cargo test' + sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-magician && cargo test' else echo "Running tests matching '${TEST_ARGS[*]}' on Linux ARM64 with RUST_TEST_THREADS=$TEST_THREADS using temporary target volume '$TARGET_VOLUME'..." docker run \ @@ -102,5 +101,5 @@ else -v "$TARGET_VOLUME:/cargo-target" \ -w /app \ "$IMAGE" \ - sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-eval && cargo test "$@"' sh "${TEST_ARGS[@]}" + sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-magician && cargo test "$@"' sh "${TEST_ARGS[@]}" fi diff --git a/scripts/test-linux-x86_64.sh b/scripts/test-linux-x86_64.sh index 4d24ab2b23..948659f968 100755 --- a/scripts/test-linux-x86_64.sh +++ b/scripts/test-linux-x86_64.sh @@ -70,8 +70,7 @@ trap cleanup EXIT INT TERM # Run tests with the project mounted as a volume. Build the bridge staticlib # crates first so libelephc_tls.a / libelephc_pdo.a / libelephc_crypto.a / # libelephc_phar.a / libelephc_tz.a / libelephc_image.a / libelephc_web.a / -# libelephc_eval.a -# exist in the target dir — +# libelephc_magician.a exist in the target dir — # `cargo test` alone never emits the staticlib crate-type. if [ "$TEST_ARG_COUNT" -eq 0 ]; then echo "Running all tests on Linux x86_64 with RUST_TEST_THREADS=$TEST_THREADS using temporary target volume '$TARGET_VOLUME'..." @@ -87,7 +86,7 @@ if [ "$TEST_ARG_COUNT" -eq 0 ]; then -v "$TARGET_VOLUME:/cargo-target" \ -w /app \ "$IMAGE" \ - sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-eval && cargo test' + sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-magician && cargo test' else echo "Running tests matching '${TEST_ARGS[*]}' on Linux x86_64 with RUST_TEST_THREADS=$TEST_THREADS using temporary target volume '$TARGET_VOLUME'..." docker run \ @@ -102,5 +101,5 @@ else -v "$TARGET_VOLUME:/cargo-target" \ -w /app \ "$IMAGE" \ - sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-eval && cargo test "$@"' sh "${TEST_ARGS[@]}" + sh -c 'cargo build -p elephc-tls -p elephc-pdo -p elephc-crypto -p elephc-phar -p elephc-tz -p elephc-image -p elephc-web -p elephc-magician && cargo test "$@"' sh "${TEST_ARGS[@]}" fi diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 68a3294192..490faa9ce3 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits user-assembly helpers that let libelephc-eval run public native +//! Emits user-assembly helpers that let libelephc-magician run public native //! constructors after allocating AOT objects by class name. //! //! Called from: diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index a22f26f5be..7431f98907 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits user-assembly helpers that let libelephc-eval call native instance and +//! Emits user-assembly helpers that let libelephc-magician call native instance and //! static methods known to the current module. //! //! Called from: diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index e3b1a8320a..e5f75387c4 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits user-assembly helpers that let libelephc-eval access properties on +//! Emits user-assembly helpers that let libelephc-magician access properties on //! native objects using the current module's class metadata. //! //! Called from: diff --git a/src/codegen/eval_reflection_helpers.rs b/src/codegen/eval_reflection_helpers.rs index 0dedbf7947..58cbdc8fb4 100644 --- a/src/codegen/eval_reflection_helpers.rs +++ b/src/codegen/eval_reflection_helpers.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits user-assembly helpers that let libelephc-eval materialize selected +//! Emits user-assembly helpers that let libelephc-magician materialize selected //! synthetic reflection objects using the current module's private layouts. //! //! Called from: diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 616c72abd5..350b42bc36 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits user-assembly helpers that let libelephc-eval materialize +//! Emits user-assembly helpers that let libelephc-magician materialize //! ReflectionClass, ReflectionFunction, ReflectionMethod, ReflectionParameter, //! ReflectionProperty, ReflectionClassConstant, ReflectionEnum*, and ReflectionType objects //! with private metadata slots populated from runtime eval declarations. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 8614f7c157..928607801f 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Lowers PHP `eval()` calls to the optional libelephc-eval bridge ABI. +//! Lowers PHP `eval()` calls to the optional libelephc-magician bridge ABI. //! Materializes a persistent per-function eval scope handle, flushes visible //! locals into that scope, calls the bridge, and reloads synchronized locals //! from boxed Mixed cells after the call returns. @@ -129,7 +129,7 @@ struct EvalNativeMemberAttributeRegistration { attribute_args: Option>, } -/// Scalar native callable default that can be registered with libelephc-eval. +/// Scalar native callable default that can be registered with libelephc-magician. enum EvalNativeCallableDefault { Scalar { kind: i64, payload: i64 }, String(String), diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 432314214f..fab96e22cf 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits C-ABI wrappers used by the optional `elephc-eval` bridge crate. +//! Emits C-ABI wrappers used by the optional `elephc-magician` bridge crate. //! Adapts Rust staticlib calls to elephc's internal runtime value helper ABI. //! //! Called from: @@ -21,7 +21,7 @@ fn x86_64_mixed_heap_kind_instruction() -> String { format!("mov r10, 0x{:x}", (X86_64_HEAP_MAGIC_HI32 << 32) | 5) } -/// Emits every eval value wrapper required by `libelephc-eval`. +/// Emits every eval value wrapper required by `libelephc-magician`. pub(crate) fn emit_eval_bridge_runtime(emitter: &mut Emitter) { emitter.blank(); emitter.comment("--- runtime: eval bridge value wrappers ---"); diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index 6e6bd1b16e..cdc9ebd130 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -14,7 +14,7 @@ //! - The dynamic builtin dispatcher (descriptor invoker) emits per-builtin //! wrappers — including md5/sha1/hash — that reference the `elephc_crypto` //! staticlib, so its detection forces that crate to link. -//! - `eval()` enables the optional libelephc-eval bridge only when lowered EIR +//! - `eval()` enables the optional libelephc-magician bridge only when lowered EIR //! actually references the eval bridge call path. use std::collections::HashMap; @@ -101,7 +101,7 @@ pub fn required_libraries_for_runtime_features(features: RuntimeFeatures) -> Vec libs.push("elephc_crypto".to_string()); } if features.eval { - libs.push("elephc_eval".to_string()); + libs.push("elephc_magician".to_string()); } libs } @@ -928,15 +928,15 @@ mod tests { assert!(required_libraries_for_runtime_features(RuntimeFeatures::none()).is_empty()); } - /// Verifies eval runtime features request the eval bridge staticlib for final linking. + /// Verifies eval runtime features request the magician bridge staticlib for final linking. #[test] - fn test_eval_runtime_features_require_elephc_eval_library() { + fn test_eval_runtime_features_require_elephc_magician_library() { assert_eq!( required_libraries_for_runtime_features(RuntimeFeatures { eval: true, ..RuntimeFeatures::none() }), - vec!["elephc_eval".to_string()] + vec!["elephc_magician".to_string()] ); } diff --git a/src/linker.rs b/src/linker.rs index 8b198bf6c7..ca339b4e3f 100644 --- a/src/linker.rs +++ b/src/linker.rs @@ -135,9 +135,9 @@ const BRIDGES: &[BridgeStaticlib] = &[ needs_libdl: true, }, BridgeStaticlib { - lib_name: "elephc_eval", - env_var: "ELEPHC_EVAL_LIB_DIR", - crate_name: "elephc-eval", + lib_name: "elephc_magician", + env_var: "ELEPHC_MAGICIAN_LIB_DIR", + crate_name: "elephc-magician", flag_name: "eval", whole_archive: false, macos_frameworks: &[], @@ -794,14 +794,14 @@ mod tests { /// Verifies the optional eval bridge is registered for programs that use `eval()`. #[test] - fn bridges_includes_elephc_eval() { + fn bridges_includes_elephc_magician() { let entry = BRIDGES .iter() - .find(|b| b.lib_name == "elephc_eval") - .expect("elephc_eval must be a registered bridge"); - assert_eq!(entry.crate_name, "elephc-eval"); - assert_eq!(entry.env_var, "ELEPHC_EVAL_LIB_DIR"); - assert_eq!(entry.archive_filename(), "libelephc_eval.a"); + .find(|b| b.lib_name == "elephc_magician") + .expect("elephc_magician must be a registered bridge"); + assert_eq!(entry.crate_name, "elephc-magician"); + assert_eq!(entry.env_var, "ELEPHC_MAGICIAN_LIB_DIR"); + assert_eq!(entry.archive_filename(), "libelephc_magician.a"); assert!(!entry.whole_archive, "eval bridge must not force-load"); } @@ -834,26 +834,26 @@ mod tests { assert_eq!(crate_flag_names().len(), BRIDGES.len()); } - /// Verifies the eval bridge honors `ELEPHC_EVAL_LIB_DIR` before filesystem discovery. + /// Verifies the eval bridge honors `ELEPHC_MAGICIAN_LIB_DIR` before filesystem discovery. #[test] fn eval_bridge_lib_dir_uses_env_override() { let _guard = ENV_LOCK .get_or_init(|| Mutex::new(())) .lock() .expect("env lock should not be poisoned"); - let previous = std::env::var_os("ELEPHC_EVAL_LIB_DIR"); - let override_dir = "/tmp/elephc-eval-lib-dir-override"; - std::env::set_var("ELEPHC_EVAL_LIB_DIR", override_dir); + let previous = std::env::var_os("ELEPHC_MAGICIAN_LIB_DIR"); + let override_dir = "/tmp/elephc-magician-lib-dir-override"; + std::env::set_var("ELEPHC_MAGICIAN_LIB_DIR", override_dir); let entry = BRIDGES .iter() - .find(|b| b.lib_name == "elephc_eval") - .expect("elephc_eval must be a registered bridge"); + .find(|b| b.lib_name == "elephc_magician") + .expect("elephc_magician must be a registered bridge"); let resolved = entry.lib_dir(); match previous { - Some(value) => std::env::set_var("ELEPHC_EVAL_LIB_DIR", value), - None => std::env::remove_var("ELEPHC_EVAL_LIB_DIR"), + Some(value) => std::env::set_var("ELEPHC_MAGICIAN_LIB_DIR", value), + None => std::env::remove_var("ELEPHC_MAGICIAN_LIB_DIR"), } assert_eq!(resolved.as_deref(), Some(override_dir)); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 006f80cd3d..fe97c59923 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -25,10 +25,10 @@ echo is_callable("eval") ? "1" : "0"; assert_eq!(out, "00"); } -/// Verifies a program containing `eval()` references the bridge symbol and requests libelephc-eval. +/// Verifies a program containing `eval()` references the bridge symbol and requests libelephc-magician. #[test] fn test_eval_codegen_requires_eval_bridge() { - let dir = make_cli_test_dir("elephc_eval_bridge_asm"); + let dir = make_cli_test_dir("elephc_magician_bridge_asm"); let (user_asm, _runtime_asm, required_libraries) = compile_source_to_asm_with_options(" "elephc-eval-missing-path"]) === false ? "array-false" : "bad"; +echo call_user_func_array("realpath", ["path" => "elephc-magician-missing-path"]) === false ? "array-false" : "bad"; echo ":"; echo function_exists("realpath");'); "#, ); @@ -2774,7 +2774,7 @@ fn test_eval_dispatches_disk_space_builtin_calls() { eval('echo disk_free_space(".") > 0 ? "free" : "bad"; echo ":"; echo disk_total_space(directory: ".") > 0 ? "total" : "bad"; echo ":"; echo disk_total_space(".") >= disk_free_space(".") ? "ordered" : "bad"; echo ":"; -echo disk_free_space("no/such/path/elephc-eval") === 0.0 ? "missing" : "bad"; echo ":"; +echo disk_free_space("no/such/path/elephc-magician") === 0.0 ? "missing" : "bad"; echo ":"; echo call_user_func("disk_free_space", ".") > 0 ? "call" : "bad"; echo ":"; echo call_user_func_array("disk_total_space", ["directory" => "."]) > 0 ? "spread" : "bad"; echo ":"; echo function_exists("disk_free_space"); echo function_exists("disk_total_space");'); diff --git a/tests/codegen/support/runner.rs b/tests/codegen/support/runner.rs index 33ce95a72a..a499daeb97 100644 --- a/tests/codegen/support/runner.rs +++ b/tests/codegen/support/runner.rs @@ -51,8 +51,8 @@ const TEST_BRIDGE_STATICLIBS: &[TestBridgeStaticlib] = &[ package: "elephc-image", }, TestBridgeStaticlib { - lib_name: "elephc_eval", - package: "elephc-eval", + lib_name: "elephc_magician", + package: "elephc-magician", }, ]; From e9382a710971a2710b340d12b4a88cfc9fa53943 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 14:41:27 +0200 Subject: [PATCH 0582/1208] Fix FunctionDecl test fixtures --- src/ir_lower/tests/exhaustive.rs | 1 + src/optimize/tests/dce/basics.rs | 1 + .../tests/dce/guards/composite_guards/composite_regions.rs | 5 +++++ .../tests/dce/guards/composite_guards/elseif_suffixes.rs | 4 ++++ src/optimize/tests/dce/guards/excluded_guards.rs | 5 +++++ .../tests/dce/guards/outer_guards/boolean_guards.rs | 4 ++++ .../tests/dce/guards/outer_guards/scalar_guards.rs | 7 +++++++ src/optimize/tests/dce/switches/basics.rs | 1 + src/optimize/tests/dce/switches/case_shadowing.rs | 3 +++ src/optimize/tests/dce/switches/exhaustive_suffixes.rs | 5 +++++ .../tests/dce/switches/guarded_cases/cumulative.rs | 2 ++ .../tests/dce/switches/guarded_cases/impossible.rs | 5 +++++ .../tests/dce/switches/guarded_cases/truthiness.rs | 3 +++ src/optimize/tests/dce/switches/tail_paths.rs | 3 +++ src/optimize/tests/dce/tail_sinking.rs | 5 +++++ src/optimize/tests/dce/tries/catch_pruning.rs | 5 +++++ src/optimize/tests/dce/tries/finally_paths.rs | 3 +++ src/optimize/tests/dce/tries/tail_paths.rs | 2 ++ src/optimize/tests/dce/tries/try_pruning.rs | 7 +++++++ src/optimize/tests/effects/basic_calls.rs | 3 +++ src/optimize/tests/effects/callable_aliases/path_merges.rs | 3 +++ src/optimize/tests/effects/callable_aliases/tracking.rs | 5 +++++ src/optimize/tests/prune.rs | 4 ++++ 23 files changed, 86 insertions(+) diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 7d0acb33ea..5e6aaba187 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -441,6 +441,7 @@ fn lowers_every_stmt_variant_smoke() { stmt(StmtKind::FunctionDecl { name: "f".to_string(), params: vec![("x".to_string(), Some(TypeExpr::Int), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: Some(TypeExpr::Int), diff --git a/src/optimize/tests/dce/basics.rs b/src/optimize/tests/dce/basics.rs index 0b75e78070..3ccb51bb75 100644 --- a/src/optimize/tests/dce/basics.rs +++ b/src/optimize/tests/dce/basics.rs @@ -28,6 +28,7 @@ fn test_eliminate_dead_code_invalidates_outer_strict_bool_guard_after_local_writ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs b/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs index 6e0dd299bc..34a93eb9e0 100644 --- a/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs +++ b/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs @@ -26,6 +26,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_demorgan_equivalent_gua StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -74,6 +75,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_loose_comparison_guard( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -121,6 +123,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_relational_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -168,6 +171,7 @@ fn test_eliminate_dead_code_prunes_nested_elseif_from_composite_guard_refinement StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -231,6 +235,7 @@ fn test_eliminate_dead_code_prunes_nested_subexpr_from_composite_guard_refinemen StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs b/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs index b02e104f37..dec1321d18 100644 --- a/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs +++ b/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs @@ -41,6 +41,7 @@ fn test_eliminate_dead_code_rebuilds_empty_elseif_tail_as_needed_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -99,6 +100,7 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_cumulative_fal StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -165,6 +167,7 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_negated_compos StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -239,6 +242,7 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_demorgan_equiv StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/guards/excluded_guards.rs b/src/optimize/tests/dce/guards/excluded_guards.rs index 236aa381a8..893a80c021 100644 --- a/src/optimize/tests/dce/guards/excluded_guards.rs +++ b/src/optimize/tests/dce/guards/excluded_guards.rs @@ -19,6 +19,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_zero_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -68,6 +69,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_null_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -122,6 +124,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_empty_string_g StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -179,6 +182,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_string_zero_gu StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -236,6 +240,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_float_guard() StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs b/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs index 22101406db..3ed509e693 100644 --- a/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs +++ b/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs @@ -35,6 +35,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_strict_bool_guard StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -86,6 +87,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_and_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -134,6 +136,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_negated_and_guard StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -190,6 +193,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_or_false_branch() StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs b/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs index 18a052f308..f487944507 100644 --- a/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs +++ b/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs @@ -20,6 +20,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -69,6 +70,7 @@ fn test_eliminate_dead_code_invalidates_outer_guard_after_local_write() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -120,6 +122,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_null_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -175,6 +178,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_zero_guard() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -222,6 +226,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_empty_string_guar StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -273,6 +278,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_string_zero_guard StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -324,6 +330,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_zero_float_guard( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/switches/basics.rs b/src/optimize/tests/dce/switches/basics.rs index b01703e602..95291edbd4 100644 --- a/src/optimize/tests/dce/switches/basics.rs +++ b/src/optimize/tests/dce/switches/basics.rs @@ -31,6 +31,7 @@ fn test_eliminate_dead_code_drops_empty_switch_shell_created_by_branch_dce() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/switches/case_shadowing.rs b/src/optimize/tests/dce/switches/case_shadowing.rs index 0215c7f2f8..3b0709b80c 100644 --- a/src/optimize/tests/dce/switches/case_shadowing.rs +++ b/src/optimize/tests/dce/switches/case_shadowing.rs @@ -17,6 +17,7 @@ fn test_eliminate_dead_code_drops_switch_case_shadowed_by_terminating_duplicate_ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -75,6 +76,7 @@ fn test_eliminate_dead_code_merges_fallthrough_body_from_fully_shadowed_switch_c StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -128,6 +130,7 @@ fn test_eliminate_dead_code_prunes_dead_label_inside_live_mixed_switch_case() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/switches/exhaustive_suffixes.rs b/src/optimize/tests/dce/switches/exhaustive_suffixes.rs index 492865e52f..4f551f959b 100644 --- a/src/optimize/tests/dce/switches/exhaustive_suffixes.rs +++ b/src/optimize/tests/dce/switches/exhaustive_suffixes.rs @@ -25,6 +25,7 @@ fn test_eliminate_dead_code_prunes_negated_strict_switch_true_case() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -80,6 +81,7 @@ fn test_eliminate_dead_code_prunes_exhaustive_negated_and_switch_true_default() StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -126,6 +128,7 @@ fn test_eliminate_dead_code_prunes_exhaustive_negated_or_switch_true_default() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -174,6 +177,7 @@ fn test_eliminate_dead_code_prunes_switch_true_suffix_after_exhaustive_multi_pat StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -221,6 +225,7 @@ fn test_eliminate_dead_code_prunes_scalar_switch_suffix_after_exhaustive_multi_p StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs b/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs index aecbcc2761..6910504dd3 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs @@ -23,6 +23,7 @@ fn test_eliminate_dead_code_prunes_exhaustive_switch_true_default_from_cumulativ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -90,6 +91,7 @@ fn test_eliminate_dead_code_uses_cumulative_switch_true_guards_inside_case_body( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/switches/guarded_cases/impossible.rs b/src/optimize/tests/dce/switches/guarded_cases/impossible.rs index 7dd4ceef38..b1106efd10 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/impossible.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/impossible.rs @@ -33,6 +33,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_switch_bool_guard_case( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -85,6 +86,7 @@ fn test_eliminate_dead_code_drops_impossible_switch_cases_from_outer_exact_guard StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -136,6 +138,7 @@ fn test_eliminate_dead_code_drops_impossible_switch_cases_from_outer_excluded_gu StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -210,6 +213,7 @@ fn test_eliminate_dead_code_drops_impossible_switch_true_cases_from_outer_guard( StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -267,6 +271,7 @@ fn test_eliminate_dead_code_invalidates_switch_bool_guard_after_local_write() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs b/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs index 898215b306..e424d0075a 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs @@ -17,6 +17,7 @@ fn test_eliminate_dead_code_prunes_truthy_switch_cases_and_default() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -82,6 +83,7 @@ fn test_eliminate_dead_code_prunes_falsy_scalar_labels_from_truthy_switch_subjec StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -145,6 +147,7 @@ fn test_eliminate_dead_code_combines_exclusion_and_truthy_switch_guards() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/switches/tail_paths.rs b/src/optimize/tests/dce/switches/tail_paths.rs index 9ca980c978..34e19c12f8 100644 --- a/src/optimize/tests/dce/switches/tail_paths.rs +++ b/src/optimize/tests/dce/switches/tail_paths.rs @@ -31,6 +31,7 @@ fn test_eliminate_dead_code_drops_trailing_empty_switch_cases() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -90,6 +91,7 @@ fn test_eliminate_dead_code_sinks_tail_into_switch_exit_paths() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -140,6 +142,7 @@ fn test_eliminate_dead_code_sinks_tail_into_switch_break_paths() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/tail_sinking.rs b/src/optimize/tests/dce/tail_sinking.rs index e3c2dbeaf5..be86f665b6 100644 --- a/src/optimize/tests/dce/tail_sinking.rs +++ b/src/optimize/tests/dce/tail_sinking.rs @@ -42,6 +42,7 @@ fn test_eliminate_dead_code_reduces_empty_if_chain_to_needed_condition_checks() StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -90,6 +91,7 @@ fn test_eliminate_dead_code_sinks_tail_into_if_fallthrough_branch() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -144,6 +146,7 @@ fn test_eliminate_dead_code_sinks_tail_into_implicit_else_path() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -199,6 +202,7 @@ fn test_eliminate_dead_code_sinks_tail_into_ifdef_fallthrough_paths() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -265,6 +269,7 @@ fn test_eliminate_dead_code_reduces_empty_if_to_effectful_condition_eval() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/tries/catch_pruning.rs b/src/optimize/tests/dce/tries/catch_pruning.rs index 69971fce08..8695303a63 100644 --- a/src/optimize/tests/dce/tries/catch_pruning.rs +++ b/src/optimize/tests/dce/tries/catch_pruning.rs @@ -20,6 +20,7 @@ fn test_eliminate_dead_code_drops_unreachable_catches_after_non_throwing_try() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -59,6 +60,7 @@ fn test_eliminate_dead_code_drops_unreachable_catches_before_finally() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -98,6 +100,7 @@ fn test_eliminate_dead_code_drops_catches_shadowed_by_throwable() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -153,6 +156,7 @@ fn test_eliminate_dead_code_drops_duplicate_shadowed_catch_types() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -210,6 +214,7 @@ fn test_eliminate_dead_code_merges_identical_catches_exposed_by_shadow_drop() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/tries/finally_paths.rs b/src/optimize/tests/dce/tries/finally_paths.rs index b1ac912ccb..f6ed69217b 100644 --- a/src/optimize/tests/dce/tries/finally_paths.rs +++ b/src/optimize/tests/dce/tries/finally_paths.rs @@ -22,6 +22,7 @@ fn test_eliminate_dead_code_drops_statements_after_try_finally_exit() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -70,6 +71,7 @@ fn test_eliminate_dead_code_preserves_outer_guard_for_finally_when_only_other_lo StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -129,6 +131,7 @@ fn test_eliminate_dead_code_sinks_tail_into_safe_finally_path() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/tries/tail_paths.rs b/src/optimize/tests/dce/tries/tail_paths.rs index f1de297d8a..d3138b541c 100644 --- a/src/optimize/tests/dce/tries/tail_paths.rs +++ b/src/optimize/tests/dce/tries/tail_paths.rs @@ -19,6 +19,7 @@ fn test_eliminate_dead_code_keeps_statements_after_fallthrough_try() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -71,6 +72,7 @@ fn test_eliminate_dead_code_sinks_tail_into_try_fallthrough_paths() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/dce/tries/try_pruning.rs b/src/optimize/tests/dce/tries/try_pruning.rs index 1a9be765e6..97d0be27c7 100644 --- a/src/optimize/tests/dce/tries/try_pruning.rs +++ b/src/optimize/tests/dce/tries/try_pruning.rs @@ -18,6 +18,7 @@ fn test_eliminate_dead_code_drops_statements_after_exhaustive_try_catch() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -82,6 +83,7 @@ fn test_eliminate_dead_code_drops_empty_try_shell_created_by_branch_dce() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -119,6 +121,7 @@ fn test_eliminate_dead_code_keeps_unknown_truthy_switch_entry_before_matching_ca StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -185,6 +188,7 @@ fn test_eliminate_dead_code_invalidates_outer_guard_before_catch_body() { StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -267,6 +271,7 @@ fn test_eliminate_dead_code_invalidates_outer_guard_before_catch_body_from_switc StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -354,6 +359,7 @@ fn test_eliminate_dead_code_ignores_unreachable_switch_throw_path_writes_before_ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -445,6 +451,7 @@ fn test_eliminate_dead_code_preserves_outer_guard_for_catch_when_only_non_throw_ StmtKind::FunctionDecl { name: "main".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/effects/basic_calls.rs b/src/optimize/tests/effects/basic_calls.rs index 423e44abc2..a49cb4c91a 100644 --- a/src/optimize/tests/effects/basic_calls.rs +++ b/src/optimize/tests/effects/basic_calls.rs @@ -78,6 +78,7 @@ fn test_program_function_effects_recognize_pure_user_functions() { StmtKind::FunctionDecl { name: "len3".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -111,6 +112,7 @@ fn test_program_function_effects_propagate_throwing_calls() { StmtKind::FunctionDecl { name: "boom".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -132,6 +134,7 @@ fn test_program_function_effects_propagate_throwing_calls() { StmtKind::FunctionDecl { name: "wrapper".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/effects/callable_aliases/path_merges.rs b/src/optimize/tests/effects/callable_aliases/path_merges.rs index a053121702..30c1fbb8f9 100644 --- a/src/optimize/tests/effects/callable_aliases/path_merges.rs +++ b/src/optimize/tests/effects/callable_aliases/path_merges.rs @@ -81,6 +81,7 @@ fn test_program_function_effects_merge_callable_aliases_across_if_paths() { StmtKind::FunctionDecl { name: "relay".to_string(), params: vec![("flag".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -149,6 +150,7 @@ fn test_program_function_effects_merge_callable_aliases_across_try_paths() { StmtKind::FunctionDecl { name: "relay".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -223,6 +225,7 @@ fn test_program_function_effects_merge_callable_aliases_across_switch_paths() { StmtKind::FunctionDecl { name: "relay".to_string(), params: vec![("flag".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/effects/callable_aliases/tracking.rs b/src/optimize/tests/effects/callable_aliases/tracking.rs index 91bbb25cc8..944f0e0272 100644 --- a/src/optimize/tests/effects/callable_aliases/tracking.rs +++ b/src/optimize/tests/effects/callable_aliases/tracking.rs @@ -19,6 +19,7 @@ fn test_program_function_effects_track_closure_alias_locals() { StmtKind::FunctionDecl { name: "relay".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -86,6 +87,7 @@ fn test_program_function_effects_track_callable_alias_through_ternary() { StmtKind::FunctionDecl { name: "relay".to_string(), params: vec![("flag".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -147,6 +149,7 @@ fn test_program_function_effects_track_callable_alias_through_match() { StmtKind::FunctionDecl { name: "relay".to_string(), params: vec![("flag".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -211,6 +214,7 @@ fn test_program_function_effects_track_callable_alias_through_null_coalesce() { StmtKind::FunctionDecl { name: "relay".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -271,6 +275,7 @@ fn test_program_function_effects_track_callable_alias_locals() { StmtKind::FunctionDecl { name: "relay".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, diff --git a/src/optimize/tests/prune.rs b/src/optimize/tests/prune.rs index f3f0c6c07c..4b49c76858 100644 --- a/src/optimize/tests/prune.rs +++ b/src/optimize/tests/prune.rs @@ -127,6 +127,7 @@ fn test_prune_block_drops_statements_after_return() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -156,6 +157,7 @@ fn test_prune_drops_pure_expr_stmt() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -230,6 +232,7 @@ fn test_prune_block_drops_statements_after_exhaustive_if() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, @@ -275,6 +278,7 @@ fn test_prune_block_drops_statements_after_exhaustive_switch() { StmtKind::FunctionDecl { name: "answer".into(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_type: None, return_type: None, From 31f229dc3caad1e0412f4329b24f8d0fa11b11bd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 14:41:42 +0200 Subject: [PATCH 0583/1208] Expose AOT parameter flags to eval reflection --- crates/elephc-magician/src/context.rs | 169 ++++++++++ .../elephc-magician/src/ffi/native_methods.rs | 303 +++++++++++++++++ crates/elephc-magician/src/ffi/tests.rs | 49 +++ .../src/interpreter/reflection.rs | 8 +- .../src/interpreter/statements.rs | 3 + docs/php/eval.md | 13 +- src/codegen/lower_inst/builtins/eval.rs | 307 ++++++++++++++++-- tests/codegen/eval.rs | 63 ++++ 8 files changed, 887 insertions(+), 28 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 519af2a5d9..bb94154001 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -131,7 +131,10 @@ pub struct NativeCallableSignature { param_names: Vec, param_types: Vec>, param_defaults: Vec>, + param_by_ref: Vec, + variadic_index: Option, return_type: Option, + bridge_supported: bool, } impl NativeCallableSignature { @@ -142,7 +145,10 @@ impl NativeCallableSignature { param_names: Vec::new(), param_types: Vec::new(), param_defaults: Vec::new(), + param_by_ref: Vec::new(), + variadic_index: None, return_type: None, + bridge_supported: true, } } @@ -187,11 +193,37 @@ impl NativeCallableSignature { true } + /// Records whether one positional callable parameter is by-reference. + pub fn set_param_by_ref(&mut self, index: usize, by_ref: bool) -> bool { + if index >= self.param_count { + return false; + } + if self.param_by_ref.len() < self.param_count { + self.param_by_ref.resize(self.param_count, false); + } + self.param_by_ref[index] = by_ref; + true + } + + /// Records which positional callable parameter is variadic. + pub fn set_variadic_index(&mut self, index: usize) -> bool { + if index >= self.param_count { + return false; + } + self.variadic_index = Some(index); + true + } + /// Records the PHP declared return type metadata for this callable. pub fn set_return_type(&mut self, return_type: EvalParameterType) { self.return_type = Some(return_type); } + /// Records whether eval may dispatch this callable through the generated bridge. + pub fn set_bridge_supported(&mut self, supported: bool) { + self.bridge_supported = supported; + } + /// Returns the PHP-visible parameter names registered for this callable. pub fn param_names(&self) -> &[String] { &self.param_names @@ -217,8 +249,28 @@ impl NativeCallableSignature { self.param_defaults.get(index).and_then(Option::as_ref) } + /// Returns whether one registered parameter is by-reference. + pub fn param_by_ref(&self, index: usize) -> bool { + self.param_by_ref.get(index).copied().unwrap_or(false) + } + + /// Returns whether one registered parameter is the variadic parameter. + pub fn param_variadic(&self, index: usize) -> bool { + self.variadic_index == Some(index) + } + + /// Returns whether eval may dispatch this callable through the generated bridge. + pub const fn bridge_supported(&self) -> bool { + self.bridge_supported + } + /// Returns the minimum number of arguments required by registered defaults. pub fn required_param_count(&self) -> usize { + if let Some(index) = self.variadic_index { + return (0..index) + .rfind(|position| self.param_default(*position).is_none()) + .map_or(0, |position| position + 1); + } (0..self.param_count) .rfind(|index| self.param_default(*index).is_none()) .map_or(0, |index| index + 1) @@ -1780,6 +1832,46 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_default(index, default)) } + /// Records whether one native AOT instance-method parameter is by-reference. + pub fn define_native_method_param_by_ref( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT instance-method parameter is variadic. + pub fn define_native_method_variadic_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT instance method. + pub fn define_native_method_bridge_supported( + &mut self, + class_name: &str, + method_name: &str, + supported: bool, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + /// Records one return type for registered native AOT instance-method metadata. pub fn define_native_method_return_type( &mut self, @@ -1834,6 +1926,46 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_default(index, default)) } + /// Records whether one native AOT static-method parameter is by-reference. + pub fn define_native_static_method_param_by_ref( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT static-method parameter is variadic. + pub fn define_native_static_method_variadic_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT static method. + pub fn define_native_static_method_bridge_supported( + &mut self, + class_name: &str, + method_name: &str, + supported: bool, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + /// Records one return type for registered native AOT static-method metadata. pub fn define_native_static_method_return_type( &mut self, @@ -1885,6 +2017,43 @@ impl ElephcEvalContext { .is_some_and(|signature| signature.set_param_default(index, default)) } + /// Records whether one native AOT constructor parameter is by-reference. + pub fn define_native_constructor_param_by_ref( + &mut self, + class_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT constructor parameter is variadic. + pub fn define_native_constructor_variadic_param( + &mut self, + class_name: &str, + index: usize, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT constructor. + pub fn define_native_constructor_bridge_supported( + &mut self, + class_name: &str, + supported: bool, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + /// Returns native AOT instance-method signature metadata by PHP class and method name. pub fn native_method_signature( &self, diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index 90c8e3a7c7..ac0a5d1b8f 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -128,6 +128,110 @@ pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param( .unwrap_or(0) } +/// Registers generated native PHP method parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_flags( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_flags_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP static-method parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_flags( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_flags_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP method. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_bridge_support( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_bridge_support_inner( + ctx, + method_key_ptr, + method_key_len, + false, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP static method. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_bridge_support( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_bridge_support_inner( + ctx, + method_key_ptr, + method_key_len, + true, + supported, + ) + }) + .unwrap_or(0) +} + /// Registers one generated native PHP method parameter declared type in an eval context. /// /// # Safety @@ -393,6 +497,56 @@ pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param( .unwrap_or(0) } +/// Registers generated native PHP constructor parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class name must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_flags( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_flags_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP constructor. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class name must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_bridge_support( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_bridge_support_inner( + ctx, + class_name_ptr, + class_name_len, + supported, + ) + }) + .unwrap_or(0) +} + /// Registers one generated native PHP constructor parameter declared type. /// /// # Safety @@ -676,6 +830,98 @@ unsafe fn register_native_method_param_inner( } } +/// Runs native method parameter-flag registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_flags`; invalid handles, +/// names, or indexes fail closed as `false`. +unsafe fn register_native_method_param_flags_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let by_ref_registered = if is_static { + context.define_native_static_method_param_by_ref( + class_name, + method_name, + param_index, + is_by_ref != 0, + ) + } else { + context.define_native_method_param_by_ref( + class_name, + method_name, + param_index, + is_by_ref != 0, + ) + }; + if !by_ref_registered { + return 0; + } + if is_variadic == 0 { + return 1; + } + i32::from(if is_static { + context.define_native_static_method_variadic_param(class_name, method_name, param_index) + } else { + context.define_native_method_variadic_param(class_name, method_name, param_index) + }) +} + +/// Runs native method bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_bridge_support`; invalid +/// handles or names fail closed as `false`. +unsafe fn register_native_method_bridge_support_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + i32::from(if is_static { + context.define_native_static_method_bridge_supported( + class_name, + method_name, + supported != 0, + ) + } else { + context.define_native_method_bridge_supported(class_name, method_name, supported != 0) + }) +} + /// Runs native method parameter-type registration after installing a panic boundary. /// /// # Safety @@ -931,6 +1177,63 @@ unsafe fn register_native_constructor_param_inner( i32::from(context.define_native_constructor_param(&class_name, param_index, param_name)) } +/// Runs native constructor parameter-flag registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_flags`; invalid +/// handles, names, or indexes fail closed as `false`. +unsafe fn register_native_constructor_param_flags_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if !context.define_native_constructor_param_by_ref(&class_name, param_index, is_by_ref != 0) { + return 0; + } + if is_variadic == 0 { + return 1; + } + i32::from(context.define_native_constructor_variadic_param(&class_name, param_index)) +} + +/// Runs native constructor bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_bridge_support`; invalid +/// handles or names fail closed as `false`. +unsafe fn register_native_constructor_bridge_support_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + i32::from(context.define_native_constructor_bridge_supported(&class_name, supported != 0)) +} + /// Runs native constructor parameter-type registration after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 9028c72b1d..90260e8c68 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -348,6 +348,16 @@ fn register_native_methods_record_signature_metadata() { method_type.len() as u64, ) }; + let method_param_flags_registered = unsafe { + __elephc_eval_register_native_method_param_flags( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + 1, + 0, + ) + }; let static_registered = unsafe { __elephc_eval_register_native_static_method( &mut ctx, @@ -376,6 +386,16 @@ fn register_native_methods_record_signature_metadata() { static_type.len() as u64, ) }; + let static_param_flags_registered = unsafe { + __elephc_eval_register_native_static_method_param_flags( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 1, + 0, + 1, + ) + }; let static_return_type_registered = unsafe { __elephc_eval_register_native_static_method_return_type( &mut ctx, @@ -408,6 +428,24 @@ fn register_native_methods_record_signature_metadata() { constructor_type.len() as u64, ) }; + let constructor_param_flags_registered = unsafe { + __elephc_eval_register_native_constructor_param_flags( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + 1, + 1, + ) + }; + let constructor_bridge_support_registered = unsafe { + __elephc_eval_register_native_constructor_bridge_support( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + ) + }; let method_default_registered = unsafe { __elephc_eval_register_native_method_param_default_string( &mut ctx, @@ -442,13 +480,17 @@ fn register_native_methods_record_signature_metadata() { assert_eq!(method_registered, 1); assert_eq!(method_param_registered, 1); assert_eq!(method_param_type_registered, 1); + assert_eq!(method_param_flags_registered, 1); assert_eq!(static_registered, 1); assert_eq!(static_param_registered, 1); assert_eq!(static_param_type_registered, 1); + assert_eq!(static_param_flags_registered, 1); assert_eq!(static_return_type_registered, 1); assert_eq!(constructor_registered, 1); assert_eq!(constructor_param_registered, 1); assert_eq!(constructor_param_type_registered, 1); + assert_eq!(constructor_param_flags_registered, 1); + assert_eq!(constructor_bridge_support_registered, 1); assert_eq!(method_default_registered, 1); assert_eq!(static_default_registered, 1); assert_eq!(constructor_default_registered, 1); @@ -472,6 +514,8 @@ fn register_native_methods_record_signature_metadata() { EvalParameterTypeVariant::String ] ); + assert!(method_signature.param_by_ref(1)); + assert!(!method_signature.param_variadic(1)); assert_eq!( ctx.native_static_method_signature("KnownClass", "SUM") .expect("static method metadata") @@ -486,6 +530,8 @@ fn register_native_methods_record_signature_metadata() { .expect("static method parameter type"); assert!(static_type.allows_null()); assert_eq!(static_type.variants(), &[EvalParameterTypeVariant::String]); + assert!(!static_signature.param_by_ref(1)); + assert!(static_signature.param_variadic(1)); assert_eq!( static_signature .return_type() @@ -509,6 +555,9 @@ fn register_native_methods_record_signature_metadata() { .variants(), &[EvalParameterTypeVariant::Class("KnownDep".to_string())] ); + assert!(constructor_signature.param_by_ref(0)); + assert!(constructor_signature.param_variadic(0)); + assert!(!constructor_signature.bridge_supported()); assert_eq!( ctx.native_method_signature("knownclass", "JOIN") .expect("method metadata") diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 5e2e7379d3..bdd12e6abf 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2592,8 +2592,12 @@ fn eval_reflection_native_callable_parameters( .collect::>(); let parameter_attributes = vec![Vec::new(); parameter_count]; let defaults = eval_reflection_native_callable_parameter_defaults(signature); - let by_ref_flags = vec![false; parameter_count]; - let variadic_flags = vec![false; parameter_count]; + let by_ref_flags = (0..parameter_count) + .map(|index| signature.param_by_ref(index)) + .collect::>(); + let variadic_flags = (0..parameter_count) + .map(|index| signature.param_variadic(index)) + .collect::>(); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method_name.to_ascii_lowercase(), declaring_class_name: Some(declaring_class_name.trim_start_matches('\\').to_string()), diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 737a0a4bac..819f29de45 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4480,6 +4480,9 @@ pub(in crate::interpreter) fn bind_native_callable_args( let Some(signature) = signature else { return positional_evaluated_arg_values(args); }; + if !signature.bridge_supported() { + return Err(EvalStatus::RuntimeFatal); + } if signature.param_names().len() == signature.param_count() { bind_native_signature_args(&signature, args, values) } else { diff --git a/docs/php/eval.md b/docs/php/eval.md index 4b432d34f6..6ad0b24376 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -341,9 +341,9 @@ reflected method is `__construct` or `__destruct`. `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report retained eval-declared function and method metadata, plus registered generated/AOT method parameter names, declared -parameter and return types, required/optional counts, and scalar or null default -values when native method/static-method signatures are registered. Eval code can -also reflect supported callable-builtin signatures, including +parameter and return types, required/optional counts, by-reference and variadic +flags, and scalar or null default values when native method/static-method or +constructor signatures are registered. Eval code can also reflect supported callable-builtin signatures, including internal origin, parameter names, parameter types, and return type metadata. Eval-declared functions and methods expose declared-type presence for parameters and return types, simple @@ -649,9 +649,10 @@ eval-supported constant-expression subset, object-valued generated defaults beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type -metadata and generated/AOT method/property attributes are exposed for registered -metadata slices, while broader non-public or unsupported bridge shapes remain -outside that slice. +metadata, by-reference and variadic parameter flags, and generated/AOT +method/property attributes are exposed for registered metadata slices, while +unsupported bridge shapes remain metadata-only rather than invocable through +eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 928607801f..c3535d2b6f 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -98,12 +98,14 @@ struct EvalNativeMethodRegistration { method_name: String, is_static: bool, signature: FunctionSig, + bridge_supported: bool, } /// A module-local constructor signature that can be registered with the eval context. struct EvalNativeConstructorRegistration { class_name: String, signature: FunctionSig, + bridge_supported: bool, } /// A module-local property type that can be registered with the eval context. @@ -526,7 +528,7 @@ fn eval_native_function_registrations( .collect() } -/// Collects public AOT methods whose parameter names can be exposed to eval binding. +/// Collects AOT method signatures whose metadata can be exposed to eval. fn eval_native_method_registrations( ctx: &FunctionContext<'_>, ) -> Vec { @@ -540,7 +542,7 @@ fn eval_native_method_registrations( registrations } -/// Collects public AOT constructors whose parameter names can be exposed to eval binding. +/// Collects AOT constructors whose metadata can be exposed to eval. fn eval_native_constructor_registrations( ctx: &FunctionContext<'_>, ) -> Vec { @@ -552,14 +554,12 @@ fn eval_native_constructor_registrations( let Some(signature) = class_info.methods.get(&method_key) else { continue; }; - if !class_method_is_public(class_info, &method_key) - || !method_signature_can_register_with_eval(signature) - { - continue; - } + let bridge_supported = class_method_is_public(class_info, &method_key) + && constructor_signature_can_bridge_with_eval(signature); registrations.push(EvalNativeConstructorRegistration { class_name: class_name.clone(), signature: signature.clone(), + bridge_supported, }); } registrations @@ -896,7 +896,7 @@ fn eval_native_property_attribute_declaring_class<'a>( .unwrap_or(reflected_class) } -/// Adds eligible public instance methods for one class to eval signature registration. +/// Adds instance method metadata for one class to eval signature registration. fn collect_eval_native_instance_methods( class_name: &str, class_info: &ClassInfo, @@ -905,22 +905,22 @@ fn collect_eval_native_instance_methods( let mut methods = class_info.methods.iter().collect::>(); methods.sort_by_key(|(method, _)| method.as_str()); for (method_name, signature) in methods { - if method_name == "__construct" - || !class_method_is_public(class_info, method_name) - || !method_signature_can_register_with_eval(signature) - { + if method_name == "__construct" { continue; } + let bridge_supported = class_method_is_public(class_info, method_name) + && method_signature_can_bridge_with_eval(signature); registrations.push(EvalNativeMethodRegistration { class_name: class_name.to_string(), method_name: method_name.clone(), is_static: false, signature: signature.clone(), + bridge_supported, }); } } -/// Adds eligible public static methods for one class to eval signature registration. +/// Adds static method metadata for one class to eval signature registration. fn collect_eval_native_static_methods( class_name: &str, class_info: &ClassInfo, @@ -929,16 +929,14 @@ fn collect_eval_native_static_methods( let mut methods = class_info.static_methods.iter().collect::>(); methods.sort_by_key(|(method, _)| method.as_str()); for (method_name, signature) in methods { - if !class_static_method_is_public(class_info, method_name) - || !method_signature_can_register_with_eval(signature) - { - continue; - } + let bridge_supported = class_static_method_is_public(class_info, method_name) + && method_signature_can_bridge_with_eval(signature); registrations.push(EvalNativeMethodRegistration { class_name: class_name.to_string(), method_name: method_name.clone(), is_static: true, signature: signature.clone(), + bridge_supported, }); } } @@ -953,11 +951,76 @@ fn function_can_register_with_eval(function: &Function) -> bool { .all(|param| !param.by_ref && !param.variadic) } -/// Returns true when eval can bind native method arguments by fixed parameter names. -fn method_signature_can_register_with_eval(signature: &FunctionSig) -> bool { +/// Returns true when eval can dispatch a native method through the generated bridge. +fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { + signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS + && signature.variadic.is_none() + && signature.ref_params.iter().all(|is_ref| !*is_ref) + && signature + .params + .iter() + .all(|(_, ty)| eval_native_method_param_supported(ty)) + && eval_native_method_return_supported(&signature.return_type) +} + +/// Returns true when eval can dispatch a native constructor through the generated bridge. +fn constructor_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS && signature.variadic.is_none() && signature.ref_params.iter().all(|is_ref| !*is_ref) + && signature + .params + .iter() + .all(|(_, ty)| eval_native_constructor_param_supported(ty)) +} + +/// Returns true when one native method argument type fits the eval method bridge. +fn eval_native_method_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Object(_) + ) +} + +/// Returns true when one native constructor argument type fits the eval bridge. +fn eval_native_constructor_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str | PhpType::Mixed + ) +} + +/// Returns true when one native method return type can be boxed back for eval. +fn eval_native_method_return_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Void + | PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Union(_) + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } + ) +} + +/// Returns true when the indexed parameter is the signature's variadic slot. +fn signature_param_is_variadic(signature: &FunctionSig, index: usize, param_name: &str) -> bool { + signature.variadic.as_deref().is_some_and(|variadic| { + variadic == param_name + || signature + .params + .get(index) + .is_some_and(|(name, _)| name == variadic) + }) } /// Returns generated type specs for declared native callable parameters. @@ -1234,6 +1297,14 @@ fn register_eval_native_method( .extern_symbol("__elephc_eval_register_native_method") }; abi::emit_call_label(ctx.emitter, &symbol); + register_eval_native_method_bridge_support( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + registration.bridge_supported, + ); let param_type_specs = eval_native_callable_param_type_specs(®istration.signature); for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { register_eval_native_method_param( @@ -1245,6 +1316,21 @@ fn register_eval_native_method( index, param_name, ); + register_eval_native_method_param_flags( + ctx, + context_offset, + &method_key_label, + method_key_len, + registration.is_static, + index, + registration + .signature + .ref_params + .get(index) + .copied() + .unwrap_or(false), + signature_param_is_variadic(®istration.signature, index, param_name), + ); if let Some(type_spec) = param_type_specs.get(index).and_then(Option::as_deref) { register_eval_native_method_param_type( ctx, @@ -1283,6 +1369,43 @@ fn register_eval_native_method( } } +/// Emits one native method bridge-support registration call. +fn register_eval_native_method_bridge_support( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + bridge_supported: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + if bridge_supported { 1 } else { 0 }, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_bridge_support") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_bridge_support") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native method parameter-name registration call. fn register_eval_native_method_param( ctx: &mut FunctionContext<'_>, @@ -1332,6 +1455,55 @@ fn register_eval_native_method_param( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native method parameter-flags registration call. +fn register_eval_native_method_param_flags( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + method_key_label: &str, + method_key_len: usize, + is_static: bool, + param_index: usize, + is_by_ref: bool, + is_variadic: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + method_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + method_key_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + if is_by_ref { 1 } else { 0 }, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + if is_variadic { 1 } else { 0 }, + ); + let symbol = if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_param_flags") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_flags") + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native method parameter-type registration call. fn register_eval_native_method_param_type( ctx: &mut FunctionContext<'_>, @@ -1527,6 +1699,13 @@ fn register_eval_native_constructor( .target .extern_symbol("__elephc_eval_register_native_constructor"); abi::emit_call_label(ctx.emitter, &symbol); + register_eval_native_constructor_bridge_support( + ctx, + context_offset, + &class_name_label, + class_name_len, + registration.bridge_supported, + ); let param_type_specs = eval_native_callable_param_type_specs(®istration.signature); for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { register_eval_native_constructor_param( @@ -1537,6 +1716,20 @@ fn register_eval_native_constructor( index, param_name, ); + register_eval_native_constructor_param_flags( + ctx, + context_offset, + &class_name_label, + class_name_len, + index, + registration + .signature + .ref_params + .get(index) + .copied() + .unwrap_or(false), + signature_param_is_variadic(®istration.signature, index, param_name), + ); if let Some(type_spec) = param_type_specs.get(index).and_then(Option::as_deref) { register_eval_native_constructor_param_type( ctx, @@ -1563,6 +1756,37 @@ fn register_eval_native_constructor( } } +/// Emits one native constructor bridge-support registration call. +fn register_eval_native_constructor_bridge_support( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + bridge_supported: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + if bridge_supported { 1 } else { 0 }, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_bridge_support"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native class-parent metadata registration call into the eval context. fn register_eval_native_class_parent( ctx: &mut FunctionContext<'_>, @@ -1822,6 +2046,49 @@ fn register_eval_native_constructor_param( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native constructor parameter-flags registration call. +fn register_eval_native_constructor_param_flags( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + class_name_label: &str, + class_name_len: usize, + param_index: usize, + is_by_ref: bool, + is_variadic: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + class_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + if is_by_ref { 1 } else { 0 }, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + if is_variadic { 1 } else { 0 }, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_flags"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native constructor parameter-type registration call. fn register_eval_native_constructor_param_type( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fe97c59923..c496146e4b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8214,6 +8214,69 @@ echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[2]- ); } +/// Verifies eval ReflectionMethod exposes generated/AOT by-ref and variadic parameter flags. +#[test] +fn test_eval_reflection_method_exposes_aot_parameter_flags() { + let out = compile_and_run_capture( + r#"getParameters(); +echo $method->getNumberOfParameters() . "/" . $method->getNumberOfRequiredParameters() . ":"; +foreach ($params as $param) { + echo $param->getName(); + echo $param->isPassedByReference() ? "R" : "b"; + echo $param->isVariadic() ? "V" : "v"; + echo $param->isOptional() ? "O" : "r"; + echo ";"; +} +$static = new ReflectionMethod("EvalAotReflectParamFlagsTarget", "collect"); +$item = $static->getParameters()[0]; +echo ":" . $item->getName(); +echo $item->isPassedByReference() ? "R" : "b"; +echo $item->isVariadic() ? "V" : "v"; +echo $static->getNumberOfRequiredParameters(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2/1:valueRvr;partsbVO;:itemsbV0"); +} + +/// Verifies metadata-only generated/AOT signatures remain non-dispatchable through eval. +#[test] +fn test_eval_aot_variadic_method_metadata_only_call_fails() { + let err = compile_and_run_expect_failure( + r#"getParameters()[0]->isVariadic() ? "V" : "v"; +$target->collect("A");'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies eval ReflectionMethod exposes generated/AOT declared type metadata. #[test] fn test_eval_reflection_method_exposes_aot_type_metadata() { From a515c097f04d13ff2a1c14a2034a9ab56f26665c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 15:01:46 +0200 Subject: [PATCH 0584/1208] Expose empty array AOT eval defaults --- crates/elephc-magician/src/context.rs | 3 +- .../elephc-magician/src/ffi/native_methods.rs | 4 +- crates/elephc-magician/src/ffi/tests.rs | 17 +++++ .../src/interpreter/reflection.rs | 15 ++--- .../src/interpreter/statements.rs | 3 +- .../elephc-magician/src/runtime_hooks/mod.rs | 4 +- docs/php/eval.md | 10 +-- src/codegen/lower_inst/builtins/eval.rs | 9 ++- tests/codegen/eval.rs | 62 +++++++++++++++++++ 9 files changed, 110 insertions(+), 17 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index bb94154001..7eb0dcde14 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -114,7 +114,7 @@ impl NativeFunction { } } -/// Scalar default value for a native AOT callable parameter visible to eval fragments. +/// Default value for a native AOT callable parameter visible to eval fragments. #[derive(Clone, Debug, PartialEq)] pub enum NativeCallableDefault { Null, @@ -122,6 +122,7 @@ pub enum NativeCallableDefault { Int(i64), Float(f64), String(String), + EmptyArray, } /// Native AOT method or constructor signature metadata visible to eval fragments. diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index ac0a5d1b8f..36dcb544b0 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -21,6 +21,7 @@ const NATIVE_DEFAULT_NULL: u64 = 0; const NATIVE_DEFAULT_BOOL: u64 = 1; const NATIVE_DEFAULT_INT: u64 = 2; const NATIVE_DEFAULT_FLOAT: u64 = 3; +pub(crate) const NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; @@ -1612,7 +1613,7 @@ fn native_attribute_take_bytes<'a>( Some(chunk) } -/// Decodes scalar default kind/payload ABI fields into native callable metadata. +/// Decodes tagged default kind/payload ABI fields into native callable metadata. fn native_callable_scalar_default( default_kind: u64, default_payload: u64, @@ -1624,6 +1625,7 @@ fn native_callable_scalar_default( NATIVE_DEFAULT_FLOAT => Some(NativeCallableDefault::Float(f64::from_bits( default_payload, ))), + NATIVE_DEFAULT_EMPTY_ARRAY => Some(NativeCallableDefault::EmptyArray), _ => None, } } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 90260e8c68..ddb90646a9 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -466,6 +466,16 @@ fn register_native_methods_record_signature_metadata() { 42, ) }; + let static_empty_array_default_registered = unsafe { + __elephc_eval_register_native_static_method_param_default_scalar( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 1, + NATIVE_DEFAULT_EMPTY_ARRAY, + 0, + ) + }; let constructor_default_registered = unsafe { __elephc_eval_register_native_constructor_param_default_scalar( &mut ctx, @@ -493,6 +503,7 @@ fn register_native_methods_record_signature_metadata() { assert_eq!(constructor_bridge_support_registered, 1); assert_eq!(method_default_registered, 1); assert_eq!(static_default_registered, 1); + assert_eq!(static_empty_array_default_registered, 1); assert_eq!(constructor_default_registered, 1); assert_eq!( ctx.native_method_signature("knownclass", "JOIN") @@ -570,6 +581,12 @@ fn register_native_methods_record_signature_metadata() { .param_default(0), Some(&NativeCallableDefault::Int(42)) ); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_default(1), + Some(&NativeCallableDefault::EmptyArray) + ); assert_eq!( ctx.native_constructor_signature("knownclass") .expect("constructor metadata") diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index bdd12e6abf..53d6a3177c 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2659,13 +2659,14 @@ fn eval_reflection_native_callable_parameter_defaults( /// Converts one registered native default into an eval constant expression. fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) -> EvalExpr { - EvalExpr::Const(match default { - NativeCallableDefault::Null => EvalConst::Null, - NativeCallableDefault::Bool(value) => EvalConst::Bool(*value), - NativeCallableDefault::Int(value) => EvalConst::Int(*value), - NativeCallableDefault::Float(value) => EvalConst::Float(*value), - NativeCallableDefault::String(value) => EvalConst::String(value.clone()), - }) + match default { + NativeCallableDefault::Null => EvalExpr::Const(EvalConst::Null), + NativeCallableDefault::Bool(value) => EvalExpr::Const(EvalConst::Bool(*value)), + NativeCallableDefault::Int(value) => EvalExpr::Const(EvalConst::Int(*value)), + NativeCallableDefault::Float(value) => EvalExpr::Const(EvalConst::Float(*value)), + NativeCallableDefault::String(value) => EvalExpr::Const(EvalConst::String(value.clone())), + NativeCallableDefault::EmptyArray => EvalExpr::Array(Vec::new()), + } } /// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 819f29de45..e46a7882be 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4526,7 +4526,7 @@ fn bind_native_signature_args( .ok_or(EvalStatus::RuntimeFatal) } -/// Allocates a fresh runtime cell for one registered native AOT scalar default. +/// Allocates a fresh runtime cell for one invocation-safe native AOT default. fn materialize_native_callable_default( default: &NativeCallableDefault, values: &mut impl RuntimeValueOps, @@ -4537,6 +4537,7 @@ fn materialize_native_callable_default( NativeCallableDefault::Int(value) => values.int(*value), NativeCallableDefault::Float(value) => values.float(*value), NativeCallableDefault::String(value) => values.string(value), + NativeCallableDefault::EmptyArray => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/runtime_hooks/mod.rs b/crates/elephc-magician/src/runtime_hooks/mod.rs index 424b43eff5..f8804efa49 100644 --- a/crates/elephc-magician/src/runtime_hooks/mod.rs +++ b/crates/elephc-magician/src/runtime_hooks/mod.rs @@ -50,10 +50,10 @@ impl ElephcRuntimeOps { /// Packs source-order argument cells into the boxed eval array ABI. fn arg_array(args: Vec) -> Result { let arg_array = unsafe { __elephc_eval_value_array_new(args.len() as u64) }; - let arg_array = Self::handle(arg_array)?; + let mut arg_array = Self::handle(arg_array)?; for (index, value) in args.into_iter().enumerate() { let index = Self::handle(unsafe { __elephc_eval_value_int(index as i64) })?; - Self::handle(unsafe { + arg_array = Self::handle(unsafe { __elephc_eval_value_array_set(arg_array.as_ptr(), index.as_ptr(), value.as_ptr()) })?; } diff --git a/docs/php/eval.md b/docs/php/eval.md index 6ad0b24376..ad93885198 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -342,8 +342,9 @@ reflected method is `__construct` or `__destruct`. `getNumberOfRequiredParameters()` report retained eval-declared function and method metadata, plus registered generated/AOT method parameter names, declared parameter and return types, required/optional counts, by-reference and variadic -flags, and scalar or null default values when native method/static-method or -constructor signatures are registered. Eval code can also reflect supported callable-builtin signatures, including +flags, and scalar, null, or empty-array default values when native +method/static-method or constructor signatures are registered. Eval code can +also reflect supported callable-builtin signatures, including internal origin, parameter names, parameter types, and return type metadata. Eval-declared functions and methods expose declared-type presence for parameters and return types, simple @@ -644,8 +645,9 @@ remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader -parameter and generated property default-value materialization beyond the -eval-supported constant-expression subset, object-valued generated defaults +parameter and generated property default-value materialization beyond scalar +or null defaults during generated/AOT invocation, empty-array defaults beyond +metadata/reflection exposure, object-valued generated defaults beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index c3535d2b6f..97ac377388 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -55,6 +55,7 @@ const NATIVE_DEFAULT_NULL: i64 = 0; const NATIVE_DEFAULT_BOOL: i64 = 1; const NATIVE_DEFAULT_INT: i64 = 2; const NATIVE_DEFAULT_FLOAT: i64 = 3; +const NATIVE_DEFAULT_EMPTY_ARRAY: i64 = 4; const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; @@ -131,7 +132,7 @@ struct EvalNativeMemberAttributeRegistration { attribute_args: Option>, } -/// Scalar native callable default that can be registered with libelephc-magician. +/// Native callable default that can be registered with libelephc-magician. enum EvalNativeCallableDefault { Scalar { kind: i64, payload: i64 }, String(String), @@ -1142,6 +1143,12 @@ fn eval_native_callable_default(expr: &Expr) -> Option Some(EvalNativeCallableDefault::String(value.clone())), + ExprKind::ArrayLiteral(elements) if elements.is_empty() => { + Some(EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_EMPTY_ARRAY, + payload: 0, + }) + } ExprKind::Negate(inner) => eval_native_callable_negated_default(inner), _ => None, } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c496146e4b..f8a02fd455 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8214,6 +8214,50 @@ echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[2]- ); } +/// Verifies eval ReflectionMethod exposes generated/AOT empty-array defaults. +#[test] +fn test_eval_reflection_method_exposes_aot_empty_array_default() { + let out = compile_and_run_capture( + r#"getParameters()[0]; +echo $param->isOptional() ? "O" : "r"; +echo $param->isDefaultValueAvailable() ? "=" : "-"; +$default = $param->getDefaultValue(); +echo is_array($default) ? count($default) : "bad"; +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "O=0"); +} + +/// Verifies eval rejects generated/AOT empty-array defaults during method dispatch. +#[test] +fn test_eval_aot_method_call_rejects_empty_array_default() { + let out = compile_and_run_capture( + r#"countItems();'); +"#, + ); + assert!(!out.success, "program unexpectedly succeeded: {:?}", out.stdout); +} + /// Verifies eval ReflectionMethod exposes generated/AOT by-ref and variadic parameter flags. #[test] fn test_eval_reflection_method_exposes_aot_parameter_flags() { @@ -10669,6 +10713,24 @@ return $second->label;'); assert_eq!(out, "AB:XY"); } +/// Verifies eval rejects generated/AOT empty-array defaults during constructor dispatch. +#[test] +fn test_eval_dynamic_new_rejects_constructor_empty_array_default() { + let out = compile_and_run_capture( + r#"count = 1; + } +} +echo eval('$box = new EvalDynamicNewArrayDefaultCtor(); +return $box->count;'); +"#, + ); + assert!(!out.success, "program unexpectedly succeeded: {:?}", out.stdout); +} + /// Verifies eval object construction passes more than two arguments to an AOT constructor. #[test] fn test_eval_dynamic_new_runs_constructor_with_many_args() { From ebd17b55013ed10f638355c022b84f54e6564e3e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 15:35:01 +0200 Subject: [PATCH 0585/1208] Fix eval AOT mixed method bridge --- .../src/interpreter/statements.rs | 2 +- docs/php/eval.md | 14 +- src/ir_lower/function.rs | 17 +- src/ir_lower/program.rs | 203 +++++++++++++++++- tests/codegen/eval.rs | 66 ++++-- 5 files changed, 273 insertions(+), 29 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e46a7882be..b1e9b177bb 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4537,7 +4537,7 @@ fn materialize_native_callable_default( NativeCallableDefault::Int(value) => values.int(*value), NativeCallableDefault::Float(value) => values.float(*value), NativeCallableDefault::String(value) => values.string(value), - NativeCallableDefault::EmptyArray => Err(EvalStatus::RuntimeFatal), + NativeCallableDefault::EmptyArray => values.array_new(0), } } diff --git a/docs/php/eval.md b/docs/php/eval.md index ad93885198..6693595331 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar or null default arguments for supported public scalar/Mixed constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar, null, or empty-array default arguments for supported public scalar/Mixed constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar or null default arguments for supported public scalar/Mixed/object method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -286,7 +286,7 @@ or tracked receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` constants can also be statically narrowed before materialization. AOT method reflection also exposes registered parameter names, declared parameter types, declared return types, required/optional -counts, and registered scalar or null default values for generated +counts, and registered scalar, null, or empty-array default values for generated constructor, instance-method, and static-method signatures. AOT property reflection exposes registered declared property types and supported scalar, string, or null default values for generated property metadata. AOT method and @@ -645,10 +645,10 @@ remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader -parameter and generated property default-value materialization beyond scalar -or null defaults during generated/AOT invocation, empty-array defaults beyond -metadata/reflection exposure, object-valued generated defaults -beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public +parameter and generated property default-value materialization beyond scalar, +null, or empty-array defaults during generated/AOT invocation, object-valued +generated defaults beyond the supported parameter +`new C()` slice, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 3a6af39173..b04a1f9cdb 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -264,11 +264,12 @@ pub(crate) fn lower_class_method( fiber_return_sigs: &std::collections::HashMap, ) { let fallback = signature_from_ast(params, return_type); - let signature = check_result - .classes + let signature = module + .class_infos .get(class_name) .and_then(|class| method_signature(class, method_name, is_static)) - .unwrap_or(&fallback); + .cloned() + .unwrap_or(fallback); let name = format!("{}::{}", class_name, method_name); // Generator methods lower their body as a Mixed-returning coroutine; see // `generator_body_return_type`. @@ -284,9 +285,9 @@ pub(crate) fn lower_class_method( by_ref_return: signature.by_ref_return, ..FunctionFlags::default() }; - function.source_signature = Some(source_signature(&name, signature)); - function.signature = Some(eir_runtime_metadata_signature(signature)); - let mut env = env_from_signature(signature); + function.source_signature = Some(source_signature(&name, &signature)); + function.signature = Some(eir_runtime_metadata_signature(&signature)); + let mut env = env_from_signature(&signature); let mut body_params = signature.params.clone(); if is_static { let hidden_called_class = (CALLED_CLASS_ID_PARAM.to_string(), PhpType::Int); @@ -311,7 +312,7 @@ pub(crate) fn lower_class_method( env.insert("this".to_string(), this_type.clone()); body_params.insert(0, ("this".to_string(), this_type)); } - function.params.extend(function_params(signature)); + function.params.extend(function_params(&signature)); attach_generator_source_if_needed(&mut function, body, body_params.len()); let closures = lower_body_into_function( &mut function, @@ -868,7 +869,7 @@ fn function_params(signature: &FunctionSig) -> Vec { } /// Returns an EIR ABI signature that keeps dynamic untyped PHP parameters boxed. -fn eir_signature_with_php_param_contracts( +pub(crate) fn eir_signature_with_php_param_contracts( owner_name: &str, signature: &FunctionSig, callable_param_sigs: &std::collections::HashMap<(String, String), FunctionSig>, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 75841ebf6d..4c08acb14f 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -22,7 +22,7 @@ use crate::ir::{ use crate::ir_lower::{builtin_datetime, function, LoweringError}; use crate::names::php_symbol_key; use crate::parser::ast::{ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind, Visibility}; -use crate::types::{CheckResult, ClassInfo, InterfaceInfo, PhpType}; +use crate::types::{CheckResult, ClassInfo, FunctionSig, InterfaceInfo, PhpType}; /// Lowers an optimized typed AST program into a validated EIR module. pub(crate) fn lower( @@ -82,6 +82,7 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec collect_declared_trait_constant_visibilities(program); module.declared_trait_final_constants = collect_declared_trait_final_constants(program); module.class_infos = check_result.classes.clone(); + normalize_class_method_signatures_for_eir(module, &check_result.callable_param_sigs); module.interface_infos = check_result.interfaces.clone(); module.enum_infos = check_result.enums.clone(); module.extern_class_infos = check_result.extern_classes.clone(); @@ -112,6 +113,206 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec crate::codegen::runtime_features_for_program_and_classes(program, &check_result.classes); } +/// Normalizes class method metadata to the ABI contracts emitted in EIR. +fn normalize_class_method_signatures_for_eir( + module: &mut Module, + callable_param_sigs: &HashMap<(String, String), FunctionSig>, +) { + for (class_name, class_info) in module.class_infos.iter_mut() { + normalize_method_map_for_eir( + class_name, + &mut class_info.methods, + &class_info.method_decls, + false, + callable_param_sigs, + ); + normalize_method_map_for_eir( + class_name, + &mut class_info.static_methods, + &class_info.method_decls, + true, + callable_param_sigs, + ); + } +} + +/// Normalizes one instance/static method table for EIR call and bridge metadata. +fn normalize_method_map_for_eir( + class_name: &str, + methods: &mut HashMap, + method_decls: &[ClassMethod], + is_static: bool, + callable_param_sigs: &HashMap<(String, String), FunctionSig>, +) { + for (method_key, signature) in methods.iter_mut() { + let owner_name = format!("{}::{}", class_name, method_key); + let mut normalized = function::eir_signature_with_php_param_contracts( + &owner_name, + signature, + callable_param_sigs, + ); + if method_decls + .iter() + .find(|method| { + method.is_static == is_static && php_symbol_key(&method.name) == *method_key + }) + .is_some_and(|method| { + method_return_exposes_dynamic_param( + method, + signature, + &owner_name, + callable_param_sigs, + ) + }) + { + normalized.return_type = PhpType::Mixed; + } + *signature = normalized; + } +} + +/// Returns true when an untyped method return can expose a dynamic parameter directly. +fn method_return_exposes_dynamic_param( + method: &ClassMethod, + signature: &FunctionSig, + owner_name: &str, + callable_param_sigs: &HashMap<(String, String), FunctionSig>, +) -> bool { + if signature.declared_return { + return false; + } + let dynamic_params = dynamic_untyped_param_names(owner_name, signature, callable_param_sigs); + !dynamic_params.is_empty() && body_returns_dynamic_param(&method.body, &dynamic_params) +} + +/// Collects untyped by-value parameter names that need a boxed EIR ABI. +fn dynamic_untyped_param_names( + owner_name: &str, + signature: &FunctionSig, + callable_param_sigs: &HashMap<(String, String), FunctionSig>, +) -> HashSet { + let mut names = HashSet::new(); + for (index, (name, php_type)) in signature.params.iter().enumerate() { + let declared = signature.declared_params.get(index).copied().unwrap_or(false); + let by_ref = signature.ref_params.get(index).copied().unwrap_or(false); + let variadic = signature.variadic.as_deref() == Some(name.as_str()); + let preserved = matches!(php_type.codegen_repr(), PhpType::Callable) + || callable_param_sigs.contains_key(&(owner_name.to_string(), name.to_string())); + if !declared && !by_ref && !variadic && !preserved { + names.insert(name.clone()); + } + } + names +} + +/// Recursively scans a method body for returns that expose dynamic parameters. +fn body_returns_dynamic_param(body: &[Stmt], dynamic_params: &HashSet) -> bool { + body.iter() + .any(|stmt| stmt_returns_dynamic_param(stmt, dynamic_params)) +} + +/// Returns true when one statement can return a dynamic parameter directly. +fn stmt_returns_dynamic_param(stmt: &Stmt, dynamic_params: &HashSet) -> bool { + match &stmt.kind { + StmtKind::Return(Some(expr)) => expr_exposes_dynamic_param(expr, dynamic_params), + StmtKind::Return(None) => false, + StmtKind::If { + then_body, + elseif_clauses, + else_body, + .. + } => { + body_returns_dynamic_param(then_body, dynamic_params) + || elseif_clauses + .iter() + .any(|(_, body)| body_returns_dynamic_param(body, dynamic_params)) + || else_body + .as_ref() + .is_some_and(|body| body_returns_dynamic_param(body, dynamic_params)) + } + StmtKind::IfDef { + then_body, + else_body, + .. + } => { + body_returns_dynamic_param(then_body, dynamic_params) + || else_body + .as_ref() + .is_some_and(|body| body_returns_dynamic_param(body, dynamic_params)) + } + StmtKind::While { body, .. } + | StmtKind::DoWhile { body, .. } + | StmtKind::Foreach { body, .. } + | StmtKind::NamespaceBlock { body, .. } + | StmtKind::IncludeOnceGuard { body, .. } + | StmtKind::Synthetic(body) => body_returns_dynamic_param(body, dynamic_params), + StmtKind::For { + init, + update, + body, + .. + } => { + init.as_ref() + .is_some_and(|stmt| stmt_returns_dynamic_param(stmt.as_ref(), dynamic_params)) + || update + .as_ref() + .is_some_and(|stmt| stmt_returns_dynamic_param(stmt.as_ref(), dynamic_params)) + || body_returns_dynamic_param(body, dynamic_params) + } + StmtKind::Switch { cases, default, .. } => { + cases + .iter() + .any(|(_, body)| body_returns_dynamic_param(body, dynamic_params)) + || default + .as_ref() + .is_some_and(|body| body_returns_dynamic_param(body, dynamic_params)) + } + StmtKind::Try { + try_body, + catches, + finally_body, + } => { + body_returns_dynamic_param(try_body, dynamic_params) + || catches + .iter() + .any(|catch| body_returns_dynamic_param(&catch.body, dynamic_params)) + || finally_body + .as_ref() + .is_some_and(|body| body_returns_dynamic_param(body, dynamic_params)) + } + _ => false, + } +} + +/// Returns true when an expression can yield one of the dynamic parameters directly. +fn expr_exposes_dynamic_param(expr: &Expr, dynamic_params: &HashSet) -> bool { + match &expr.kind { + ExprKind::Variable(name) => dynamic_params.contains(name), + ExprKind::NullCoalesce { value, default } + | ExprKind::ShortTernary { value, default } => { + expr_exposes_dynamic_param(value, dynamic_params) + || expr_exposes_dynamic_param(default, dynamic_params) + } + ExprKind::Ternary { + then_expr, + else_expr, + .. + } => { + expr_exposes_dynamic_param(then_expr, dynamic_params) + || expr_exposes_dynamic_param(else_expr, dynamic_params) + } + ExprKind::Match { arms, default, .. } => { + arms.iter() + .any(|(_, arm)| expr_exposes_dynamic_param(arm, dynamic_params)) + || default + .as_ref() + .is_some_and(|expr| expr_exposes_dynamic_param(expr, dynamic_params)) + } + ExprKind::ErrorSuppress(inner) => expr_exposes_dynamic_param(inner, dynamic_params), + _ => false, + } +} + /// Adds optional runtime features referenced by synthetic or lowered EIR functions. fn include_lowered_runtime_features(module: &mut Module) { let features = lowered_runtime_features(module); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f8a02fd455..401f0d8ec8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -44,7 +44,9 @@ fn test_eval_codegen_requires_eval_bridge() { "user assembly should free the persistent eval context:\n{user_asm}" ); assert!( - required_libraries.iter().any(|lib| lib == "elephc_magician"), + required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), "required libraries should include elephc_magician: {required_libraries:?}" ); let _ = fs::remove_dir_all(&dir); @@ -65,7 +67,9 @@ fn test_non_eval_program_does_not_request_eval_bridge() { "non-eval runtime assembly should not reference eval bridge:\n{runtime_asm}" ); assert!( - !required_libraries.iter().any(|lib| lib == "elephc_magician"), + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), "non-eval required libraries should not include elephc_magician: {required_libraries:?}" ); let _ = fs::remove_dir_all(&dir); @@ -4818,6 +4822,44 @@ echo (new EvalAotNamedMethodBox())->run(); assert_eq!(out, "AB"); } +/// Verifies eval preserves string values passed through an untyped AOT method parameter. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_string_arg() { + let out = compile_and_run( + r#"relay("abc"); +return gettype($value) . ":" . $value;'); +"#, + ); + assert_eq!(out, "string:abc"); +} + +/// Verifies eval preserves array values passed through an untyped AOT method parameter. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_array_arg() { + let out = compile_and_run( + r#"relay([]); +return gettype($value) . ":" . (is_array($value) ? count($value) : 9);'); +"#, + ); + assert_eq!(out, "array:0"); +} + /// Verifies eval binds named arguments before dispatching an AOT static method. #[test] fn test_eval_fragment_dispatches_aot_static_method_with_named_args() { @@ -8241,21 +8283,21 @@ echo is_array($default) ? count($default) : "bad"; assert_eq!(out.stdout, "O=0"); } -/// Verifies eval rejects generated/AOT empty-array defaults during method dispatch. +/// Verifies eval materializes generated/AOT empty-array defaults during method dispatch. #[test] -fn test_eval_aot_method_call_rejects_empty_array_default() { - let out = compile_and_run_capture( +fn test_eval_aot_method_call_uses_empty_array_default() { + let out = compile_and_run( r#"countItems();'); "#, ); - assert!(!out.success, "program unexpectedly succeeded: {:?}", out.stdout); + assert_eq!(out, "0"); } /// Verifies eval ReflectionMethod exposes generated/AOT by-ref and variadic parameter flags. @@ -10713,22 +10755,22 @@ return $second->label;'); assert_eq!(out, "AB:XY"); } -/// Verifies eval rejects generated/AOT empty-array defaults during constructor dispatch. +/// Verifies eval materializes generated/AOT empty-array defaults during constructor dispatch. #[test] -fn test_eval_dynamic_new_rejects_constructor_empty_array_default() { - let out = compile_and_run_capture( +fn test_eval_dynamic_new_uses_constructor_empty_array_default() { + let out = compile_and_run( r#"count = 1; + $this->count = is_array($items) ? 0 : 9; } } echo eval('$box = new EvalDynamicNewArrayDefaultCtor(); return $box->count;'); "#, ); - assert!(!out.success, "program unexpectedly succeeded: {:?}", out.stdout); + assert_eq!(out, "0"); } /// Verifies eval object construction passes more than two arguments to an AOT constructor. From a896e332c9f7e5c41003cd25b7d20d3b170b331d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 15:44:47 +0200 Subject: [PATCH 0586/1208] Support eval AOT object constructor args --- docs/php/eval.md | 4 ++-- src/codegen/eval_constructor_helpers.rs | 21 +++++++++++++++++-- src/codegen/lower_inst/builtins/eval.rs | 7 ++++++- tests/codegen/eval.rs | 27 +++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 6693595331..5c4f786d07 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and registered scalar, null, or empty-array default arguments for supported public scalar/Mixed constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, object-typed arguments, and registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | @@ -636,7 +636,7 @@ particular, advanced native callable descriptors and closure callback values are still outside eval fragments. Runtime/AOT object-method and static-method fallback from eval remain limited to the generated public non-by-reference fixed scalar/Mixed/object bridge slice, while runtime/AOT constructor fallback remains -limited to public non-by-reference fixed scalar/Mixed signatures. Variadic, +limited to public non-by-reference fixed scalar/Mixed/object signatures. Variadic, by-reference, and broader parameter/return ABI shapes are still outside those bridge paths. diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 490faa9ce3..3e768f6f5b 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -9,7 +9,7 @@ //! - The cacheable runtime object can allocate by name, but only user assembly //! knows constructor symbols and parameter ABI shapes. //! - Classes without constructors are treated as successful no-ops, matching PHP. -//! - Constructors are bridged for fixed non-by-ref scalar/Mixed arguments. +//! - Constructors are bridged for fixed non-by-ref scalar/Mixed/object arguments. use std::collections::BTreeMap; @@ -175,7 +175,12 @@ fn constructor_signature_supported(sig: &FunctionSig) -> bool { fn constructor_param_supported(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), - PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str | PhpType::Mixed + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Object(_) ) } @@ -631,6 +636,12 @@ fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { PhpType::Mixed => { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for a Mixed constructor parameter } + PhpType::Object(_) => { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for object unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the eval object payload for the constructor ABI + emitter.instruction("mov x0, x1"); // move the unboxed object payload into the result register + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); + } _ => {} } } @@ -657,6 +668,12 @@ fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { PhpType::Mixed => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for a Mixed constructor parameter } + PhpType::Object(_) => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for object unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the eval object payload for the constructor ABI + emitter.instruction("mov rax, rdi"); // move the unboxed object payload into the result register + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); + } _ => {} } } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 97ac377388..8ac72d5bdc 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -992,7 +992,12 @@ fn eval_native_method_param_supported(ty: &PhpType) -> bool { fn eval_native_constructor_param_supported(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), - PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str | PhpType::Mixed + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Mixed + | PhpType::Object(_) ) } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 401f0d8ec8..4504518cda 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4895,6 +4895,33 @@ echo eval('$box = new EvalDynamicNewNamedCtor(right: "F", left: "E"); return $bo assert_eq!(out, "EF"); } +/// Verifies eval object construction passes object-typed arguments to AOT constructors. +#[test] +fn test_eval_dynamic_new_passes_object_arg_to_constructor() { + let out = compile_and_run( + r#"name = $name; + } +} + +class EvalDynamicNewObjectArgTarget { + public string $label = ""; + public function __construct(EvalDynamicNewObjectArgSource $source) { + $this->label = $source->name; + } +} + +echo eval('$source = new EvalDynamicNewObjectArgSource("Ada"); +$box = new EvalDynamicNewObjectArgTarget($source); +return $box->label;'); +"#, + ); + assert_eq!(out, "Ada"); +} + /// Verifies eval-declared methods resolve `new self/static/parent` through the bridge. #[test] fn test_eval_declared_methods_construct_relative_class_names() { From cedf8ac422d010e449c23801727abd2036799116 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 15:56:11 +0200 Subject: [PATCH 0587/1208] Support eval AOT array bridge args --- docs/php/eval.md | 14 ++--- src/codegen/eval_constructor_helpers.rs | 74 +++++++++++++++++++++---- src/codegen/eval_method_helpers.rs | 34 ++++++++++++ src/codegen/lower_inst/builtins/eval.rs | 4 ++ tests/codegen/eval.rs | 44 +++++++++++++++ 5 files changed, 152 insertions(+), 18 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 5c4f786d07..97d7f923d5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, object-typed arguments, and registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/object constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, array-typed arguments, object-typed arguments, and registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/array/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/object method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/array/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -127,7 +127,7 @@ as callable. Static method callables can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method -fallback supports the same named-argument binding for public scalar/Mixed/object +fallback supports the same named-argument binding for public scalar/Mixed/array/object signatures supported by the generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -511,7 +511,7 @@ constants. Direct `new EnumName()` and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native -methods are bridged to eval. Public fixed scalar/Mixed/object method calls +methods are bridged to eval. Public fixed scalar/Mixed/array/object method calls through `$this->method(...)` are supported by the native method bridge, including registered named arguments and string-keyed unpacking. @@ -635,8 +635,8 @@ The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are still outside eval fragments. Runtime/AOT object-method and static-method fallback from eval remain limited to the generated public non-by-reference fixed -scalar/Mixed/object bridge slice, while runtime/AOT constructor fallback remains -limited to public non-by-reference fixed scalar/Mixed/object signatures. Variadic, +scalar/Mixed/array/object bridge slice, while runtime/AOT constructor fallback remains +limited to public non-by-reference fixed scalar/Mixed/array/object signatures. Variadic, by-reference, and broader parameter/return ABI shapes are still outside those bridge paths. @@ -649,7 +649,7 @@ parameter and generated property default-value materialization beyond scalar, null, or empty-array defaults during generated/AOT invocation, object-valued generated defaults beyond the supported parameter `new C()` slice, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed/object slice plus visibility-checked +non-by-reference fixed scalar/Mixed/array/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT method/property attributes are exposed for registered metadata slices, while diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 3e768f6f5b..13b5c3b3b5 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -9,7 +9,7 @@ //! - The cacheable runtime object can allocate by name, but only user assembly //! knows constructor symbols and parameter ABI shapes. //! - Classes without constructors are treated as successful no-ops, matching PHP. -//! - Constructors are bridged for fixed non-by-ref scalar/Mixed/object arguments. +//! - Constructors are bridged for fixed non-by-ref scalar/Mixed/array/object arguments. use std::collections::BTreeMap; @@ -180,6 +180,8 @@ fn constructor_param_supported(ty: &PhpType) -> bool { | PhpType::Float | PhpType::Str | PhpType::Mixed + | PhpType::Array(_) + | PhpType::AssocArray { .. } | PhpType::Object(_) ) } @@ -337,7 +339,7 @@ fn emit_aarch64_builtin_throwable_constructor_body( emitter.instruction("cmp x9, #0"); // did the eval call pass a message argument? emitter.instruction(&format!("b.eq {}", success_label)); // keep the empty Throwable defaults when no message was supplied emit_aarch64_load_eval_arg(module, emitter, 0); - emit_aarch64_cast_eval_arg(emitter, &PhpType::Str); + emit_aarch64_cast_eval_arg(emitter, &PhpType::Str, fail_label); emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for message initialization emitter.instruction("str x1, [x9, #8]"); // store the message pointer in the compact Throwable payload emitter.instruction("str x2, [x9, #16]"); // store the message length in the compact Throwable payload @@ -345,7 +347,7 @@ fn emit_aarch64_builtin_throwable_constructor_body( emitter.instruction("cmp x9, #1"); // did the eval call pass a code argument? emitter.instruction(&format!("b.le {}", success_label)); // keep code zero when only the message was supplied emit_aarch64_load_eval_arg(module, emitter, 1); - emit_aarch64_cast_eval_arg(emitter, &PhpType::Int); + emit_aarch64_cast_eval_arg(emitter, &PhpType::Int, fail_label); emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for code initialization emitter.instruction("str x0, [x9, #24]"); // store the integer exception code emitter.instruction(&format!("b {}", success_label)); // builtin Throwable construction completed @@ -364,7 +366,7 @@ fn emit_x86_64_builtin_throwable_constructor_body( emitter.instruction("cmp r11, 0"); // did the eval call pass a message argument? emitter.instruction(&format!("je {}", success_label)); // keep the empty Throwable defaults when no message was supplied emit_x86_64_load_eval_arg(module, emitter, 0); - emit_x86_64_cast_eval_arg(emitter, &PhpType::Str); + emit_x86_64_cast_eval_arg(emitter, &PhpType::Str, fail_label); emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for message initialization emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store the message pointer in the compact Throwable payload emitter.instruction("mov QWORD PTR [r11 + 16], rdx"); // store the message length in the compact Throwable payload @@ -372,7 +374,7 @@ fn emit_x86_64_builtin_throwable_constructor_body( emitter.instruction("cmp r11, 1"); // did the eval call pass a code argument? emitter.instruction(&format!("jle {}", success_label)); // keep code zero when only the message was supplied emit_x86_64_load_eval_arg(module, emitter, 1); - emit_x86_64_cast_eval_arg(emitter, &PhpType::Int); + emit_x86_64_cast_eval_arg(emitter, &PhpType::Int, fail_label); emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for code initialization emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store the integer exception code emitter.instruction(&format!("jmp {}", success_label)); // builtin Throwable construction completed @@ -479,7 +481,7 @@ fn emit_aarch64_constructor_body( return; } emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = emit_aarch64_prepare_constructor_args(module, emitter, slot); + let overflow_bytes = emit_aarch64_prepare_constructor_args(module, emitter, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); @@ -501,7 +503,7 @@ fn emit_x86_64_constructor_body( return; } emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = emit_x86_64_prepare_constructor_args(module, emitter, slot); + let overflow_bytes = emit_x86_64_prepare_constructor_args(module, emitter, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); @@ -545,13 +547,14 @@ fn emit_aarch64_prepare_constructor_args( module: &Module, emitter: &mut Emitter, slot: &EvalConstructorSlot, + fail_label: &str, ) -> usize { let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first constructor argument abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { emit_aarch64_load_eval_arg(module, emitter, index); - emit_aarch64_cast_eval_arg(emitter, param_ty); + emit_aarch64_cast_eval_arg(emitter, param_ty, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_constructor_args(module, emitter, &receiver_ty, &slot.params) @@ -562,13 +565,14 @@ fn emit_x86_64_prepare_constructor_args( module: &Module, emitter: &mut Emitter, slot: &EvalConstructorSlot, + fail_label: &str, ) -> usize { let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first constructor argument abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { emit_x86_64_load_eval_arg(module, emitter, index); - emit_x86_64_cast_eval_arg(emitter, param_ty); + emit_x86_64_cast_eval_arg(emitter, param_ty, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_constructor_args(module, emitter, &receiver_ty, &slot.params) @@ -615,7 +619,7 @@ fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usiz } /// Casts one boxed eval argument into ARM64 result registers for temporary staging. -fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { +fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType, fail_label: &str) { match param_ty.codegen_repr() { PhpType::Int => { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for integer coercion @@ -639,15 +643,39 @@ fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { PhpType::Object(_) => { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for object unboxing emitter.instruction("bl __rt_mixed_unbox"); // expose the eval object payload for the constructor ABI + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the eval argument is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // reject malformed non-object constructor arguments emitter.instruction("mov x0, x1"); // move the unboxed object payload into the result register abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); } + PhpType::Array(_) => { + emit_aarch64_cast_eval_array_arg(emitter, param_ty, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_aarch64_cast_eval_array_arg(emitter, param_ty, 5, fail_label); + } _ => {} } } +/// Validates and unboxes one ARM64 array-typed eval argument for native constructors. +fn emit_aarch64_cast_eval_array_arg( + emitter: &mut Emitter, + param_ty: &PhpType, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for array unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the eval array payload for the constructor ABI + abi::emit_load_int_immediate(emitter, "x9", expected_tag); + emitter.instruction("cmp x0, x9"); // compare the eval payload tag with the expected array ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject array payloads with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // move the unboxed array payload into the result register + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); +} + /// Casts one boxed eval argument into x86_64 result registers for temporary staging. -fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { +fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType, fail_label: &str) { match param_ty.codegen_repr() { PhpType::Int => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion @@ -671,13 +699,37 @@ fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType) { PhpType::Object(_) => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for object unboxing emitter.instruction("call __rt_mixed_unbox"); // expose the eval object payload for the constructor ABI + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the eval argument is an object + emitter.instruction(&format!("jne {}", fail_label)); // reject malformed non-object constructor arguments emitter.instruction("mov rax, rdi"); // move the unboxed object payload into the result register abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); } + PhpType::Array(_) => { + emit_x86_64_cast_eval_array_arg(emitter, param_ty, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_x86_64_cast_eval_array_arg(emitter, param_ty, 5, fail_label); + } _ => {} } } +/// Validates and unboxes one x86_64 array-typed eval argument for native constructors. +fn emit_x86_64_cast_eval_array_arg( + emitter: &mut Emitter, + param_ty: &PhpType, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for array unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the eval array payload for the constructor ABI + abi::emit_load_int_immediate(emitter, "r10", expected_tag); + emitter.instruction("cmp rax, r10"); // compare the eval payload tag with the expected array ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject array payloads with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // move the unboxed array payload into the result register + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); +} + /// Groups constructor slots by class id while preserving sorted class order. fn grouped_slots(slots: &[EvalConstructorSlot]) -> BTreeMap> { let mut grouped = BTreeMap::new(); diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 7431f98907..bb26d1f098 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -259,6 +259,8 @@ fn method_param_supported(ty: &PhpType) -> bool { | PhpType::Float | PhpType::Str | PhpType::Mixed + | PhpType::Array(_) + | PhpType::AssocArray { .. } | PhpType::Object(_) ) } @@ -1138,6 +1140,12 @@ fn emit_aarch64_cast_eval_arg( PhpType::Object(class_name) => { emit_aarch64_cast_eval_object_arg(module, emitter, data, &class_name, fail_label); } + PhpType::Array(_) => { + emit_aarch64_cast_eval_array_arg(emitter, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_aarch64_cast_eval_array_arg(emitter, 5, fail_label); + } _ => {} } } @@ -1165,6 +1173,16 @@ fn emit_aarch64_cast_eval_object_arg( emitter.instruction("mov x0, x1"); // place the unboxed object pointer in the result register } +/// Validates and unboxes one ARM64 array-typed eval argument for native method dispatch. +fn emit_aarch64_cast_eval_array_arg(emitter: &mut Emitter, expected_tag: i64, fail_label: &str) { + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for array unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the array payload for the native method call + abi::emit_load_int_immediate(emitter, "x9", expected_tag); + emitter.instruction("cmp x0, x9"); // compare the eval payload tag with the expected array ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject array payloads with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // place the unboxed array pointer in the result register +} + /// Casts one boxed eval argument into x86_64 result registers for temporary staging. fn emit_x86_64_cast_eval_arg( module: &Module, @@ -1196,6 +1214,12 @@ fn emit_x86_64_cast_eval_arg( PhpType::Object(class_name) => { emit_x86_64_cast_eval_object_arg(module, emitter, data, &class_name, fail_label); } + PhpType::Array(_) => { + emit_x86_64_cast_eval_array_arg(emitter, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_x86_64_cast_eval_array_arg(emitter, 5, fail_label); + } _ => {} } } @@ -1224,6 +1248,16 @@ fn emit_x86_64_cast_eval_object_arg( emitter.instruction("mov rax, rdi"); // place the unboxed object pointer in the result register } +/// Validates and unboxes one x86_64 array-typed eval argument for native method dispatch. +fn emit_x86_64_cast_eval_array_arg(emitter: &mut Emitter, expected_tag: i64, fail_label: &str) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for array unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the array payload for the native method call + abi::emit_load_int_immediate(emitter, "r10", expected_tag); + emitter.instruction("cmp rax, r10"); // compare the eval payload tag with the expected array ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject array payloads with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // place the unboxed array pointer in the result register +} + /// Boxes the current native method result as the Mixed cell expected by eval. fn emit_box_method_result(module: &Module, emitter: &mut Emitter, return_ty: &PhpType) { if return_ty.codegen_repr() == PhpType::Void { diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 8ac72d5bdc..1ce1c22600 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -984,6 +984,8 @@ fn eval_native_method_param_supported(ty: &PhpType) -> bool { | PhpType::Float | PhpType::Str | PhpType::Mixed + | PhpType::Array(_) + | PhpType::AssocArray { .. } | PhpType::Object(_) ) } @@ -997,6 +999,8 @@ fn eval_native_constructor_param_supported(ty: &PhpType) -> bool { | PhpType::Float | PhpType::Str | PhpType::Mixed + | PhpType::Array(_) + | PhpType::AssocArray { .. } | PhpType::Object(_) ) } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4504518cda..4f14d0e019 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4624,6 +4624,31 @@ echo (new EvalMethodObjectArgBox())->run(); assert_eq!(out, "Obj:Obj!"); } +/// Verifies eval fragments can pass array-typed arguments to public AOT methods. +#[test] +fn test_eval_fragment_can_call_aot_method_with_array_arg() { + let out = compile_and_run( + r#"countItems([1, 2, 3]) . ":" . EvalMethodArrayArgBox::countStatic([4, 5]);'); + } +} + +echo (new EvalMethodArrayArgBox())->run(); +"#, + ); + assert_eq!(out, "3:2"); +} + /// Verifies eval fragments inherit lexical `self::` from an AOT instance method. #[test] fn test_eval_fragment_in_aot_method_resolves_self_scope() { @@ -4922,6 +4947,25 @@ return $box->label;'); assert_eq!(out, "Ada"); } +/// Verifies eval object construction passes array-typed arguments to AOT constructors. +#[test] +fn test_eval_dynamic_new_passes_array_arg_to_constructor() { + let out = compile_and_run( + r#"count = count($items); + } +} + +echo eval('$box = new EvalDynamicNewArrayArgTarget([1, 2, 3, 4]); +return $box->count;'); +"#, + ); + assert_eq!(out, "4"); +} + /// Verifies eval-declared methods resolve `new self/static/parent` through the bridge. #[test] fn test_eval_declared_methods_construct_relative_class_names() { From fb12f397dae18f89f170c9bdf78e6cef1dc81313 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 16:10:59 +0200 Subject: [PATCH 0588/1208] Support eval AOT object defaults --- crates/elephc-magician/src/context.rs | 4 + .../elephc-magician/src/ffi/native_methods.rs | 187 +++++++++++++++++- .../src/interpreter/reflection.rs | 12 ++ .../src/interpreter/statements.rs | 21 ++ docs/php/eval.md | 16 +- src/codegen/lower_inst/builtins/eval.rs | 110 ++++++++++- tests/codegen/eval.rs | 57 ++++++ 7 files changed, 397 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 7eb0dcde14..f00102028e 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -123,6 +123,10 @@ pub enum NativeCallableDefault { Float(f64), String(String), EmptyArray, + Object { + class_name: String, + args: Vec, + }, } /// Native AOT method or constructor signature metadata visible to eval fragments. diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index 36dcb544b0..a62c084b24 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -7,7 +7,7 @@ //! //! Key details: //! - Invalid names, handles, or indexes fail closed as `false`. -//! - The metadata records parameter names and scalar defaults; generated user +//! - The metadata records parameter names and supported defaults; generated user //! helpers still perform the actual method, static method, and constructor calls. use super::util::abi_name_to_string; @@ -30,6 +30,9 @@ const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; +const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; +const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 3; #[derive(Clone, Copy)] enum NativeCallableTypePosition { @@ -453,6 +456,62 @@ pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_defau .unwrap_or(0) } +/// Registers one generated native PHP method object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_object( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_object_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_object( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_object_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + /// Registers a generated native PHP constructor signature in an eval context. /// /// # Safety @@ -629,6 +688,33 @@ pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default .unwrap_or(0) } +/// Registers one generated native PHP constructor object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_object( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_object_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + /// Registers generated native PHP parent-class metadata in an eval context. /// /// # Safety @@ -1073,6 +1159,33 @@ unsafe fn register_native_method_param_default_string_inner( ) } +/// Runs native method object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_object`; invalid +/// handles, names, indexes, or object specs fail closed as `false`. +unsafe fn register_native_method_param_default_object_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + /// Records a native method parameter default in the selected instance/static table. /// /// # Safety @@ -1320,6 +1433,31 @@ unsafe fn register_native_constructor_param_default_string_inner( ) } +/// Runs native constructor object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_object`; +/// invalid handles, names, indexes, or object specs fail closed as `false`. +unsafe fn register_native_constructor_param_default_object_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + /// Records a native constructor parameter default in the constructor signature table. /// /// # Safety @@ -1630,6 +1768,53 @@ fn native_callable_scalar_default( } } +/// Decodes an object-valued native callable default from a generated binary spec. +/// +/// # Safety +/// `spec_ptr` must be readable for `spec_len` bytes when non-null. +unsafe fn native_callable_object_default( + spec_ptr: *const u8, + spec_len: u64, +) -> Option { + let len = usize::try_from(spec_len).ok()?; + let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; + let mut offset = 0; + let class_name = native_attribute_take_string(bytes, &mut offset)?; + let arg_count = usize::from(native_attribute_take_u8(bytes, &mut offset)?); + if arg_count > MAX_NATIVE_OBJECT_DEFAULT_ARGS { + return None; + } + let mut args = Vec::with_capacity(arg_count); + for _ in 0..arg_count { + args.push(native_callable_object_default_arg(bytes, &mut offset)?); + } + (offset == bytes.len()).then_some(NativeCallableDefault::Object { class_name, args }) +} + +/// Decodes one object-default constructor argument from a generated binary spec. +fn native_callable_object_default_arg( + bytes: &[u8], + offset: &mut usize, +) -> Option { + match native_attribute_take_u8(bytes, offset)? { + NATIVE_OBJECT_DEFAULT_ARG_SCALAR => { + let kind = native_attribute_take_u64(bytes, offset)?; + let payload = native_attribute_take_u64(bytes, offset)?; + native_callable_scalar_default(kind, payload) + } + NATIVE_OBJECT_DEFAULT_ARG_STRING => { + native_attribute_take_string(bytes, offset).map(NativeCallableDefault::String) + } + _ => None, + } +} + +/// Reads one little-endian u64 from a native binary metadata record. +fn native_attribute_take_u64(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(u64::from_le_bytes(chunk.try_into().ok()?)) +} + /// Decodes one generated type-spec string into eval Reflection type metadata. fn native_callable_type_from_abi( type_spec_ptr: *const u8, diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 53d6a3177c..a817ac710b 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2666,9 +2666,21 @@ fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) NativeCallableDefault::Float(value) => EvalExpr::Const(EvalConst::Float(*value)), NativeCallableDefault::String(value) => EvalExpr::Const(EvalConst::String(value.clone())), NativeCallableDefault::EmptyArray => EvalExpr::Array(Vec::new()), + NativeCallableDefault::Object { class_name, args } => EvalExpr::NewObject { + class_name: class_name.clone(), + args: args + .iter() + .map(eval_reflection_native_callable_default_arg) + .collect(), + }, } } +/// Converts one native object-default constructor argument into a positional eval call arg. +fn eval_reflection_native_callable_default_arg(default: &NativeCallableDefault) -> EvalCallArg { + EvalCallArg::positional(eval_reflection_native_callable_default_expr(default)) +} + /// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. fn eval_reflection_aot_property_metadata_if_exists( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b1e9b177bb..a95bede247 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4538,7 +4538,28 @@ fn materialize_native_callable_default( NativeCallableDefault::Float(value) => values.float(*value), NativeCallableDefault::String(value) => values.string(value), NativeCallableDefault::EmptyArray => values.array_new(0), + NativeCallableDefault::Object { class_name, args } => { + materialize_native_callable_object_default(class_name, args, values) + } + } +} + +/// Allocates and constructs one object-valued native AOT parameter default. +fn materialize_native_callable_object_default( + class_name: &str, + args: &[NativeCallableDefault], + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object(class_name)?; + let mut constructor_args = Vec::with_capacity(args.len()); + for arg in args { + constructor_args.push(materialize_native_callable_default(arg, values)?); } + if let Err(err) = values.construct_object(object, constructor_args) { + let _ = values.release(object); + return Err(err); + } + Ok(object) } /// Executes a PHP `static $name = expr;` declaration in the current eval scope. diff --git a/docs/php/eval.md b/docs/php/eval.md index 97d7f923d5..c77d2bb77c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, array-typed arguments, object-typed arguments, and registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/array/object constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, array-typed arguments, object-typed arguments, and registered scalar, null, empty-array, or supported object-valued default arguments for public scalar/Mixed/array/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar, null, or empty-array default arguments for supported public scalar/Mixed/array/object method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar, null, empty-array, or supported object-valued default arguments for public scalar/Mixed/array/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -286,7 +286,7 @@ or tracked receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` constants can also be statically narrowed before materialization. AOT method reflection also exposes registered parameter names, declared parameter types, declared return types, required/optional -counts, and registered scalar, null, or empty-array default values for generated +counts, and registered scalar, null, empty-array, or supported object-valued default values for generated constructor, instance-method, and static-method signatures. AOT property reflection exposes registered declared property types and supported scalar, string, or null default values for generated property metadata. AOT method and @@ -301,7 +301,7 @@ methods that are not overridden report no prototype, matching PHP reflection. `ReflectionClass::getConstructor()` returns a materialized `ReflectionMethod` for direct, inherited, interface, trait, and generated/AOT constructors, including registered generated/AOT constructor parameter names, -counts, and scalar/null defaults where available; it returns `null` when no +counts, and supported defaults where available; it returns `null` when no constructor is visible. `ReflectionClass::getParentClass()` returns a materialized `ReflectionClass` for eval-declared and generated/AOT parent classes or `false` when no parent class exists. @@ -342,7 +342,7 @@ reflected method is `__construct` or `__destruct`. `getNumberOfRequiredParameters()` report retained eval-declared function and method metadata, plus registered generated/AOT method parameter names, declared parameter and return types, required/optional counts, by-reference and variadic -flags, and scalar, null, or empty-array default values when native +flags, and scalar, null, empty-array, or supported object-valued default values when native method/static-method or constructor signatures are registered. Eval code can also reflect supported callable-builtin signatures, including internal origin, parameter names, parameter types, and return type metadata. @@ -646,9 +646,9 @@ ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/Intersect and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, -null, or empty-array defaults during generated/AOT invocation, object-valued -generated defaults beyond the supported parameter -`new C()` slice, and broader generated/AOT method bridge signatures beyond the current public +null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, +object-valued generated defaults beyond the positional +`new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public non-by-reference fixed scalar/Mixed/array/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 1ce1c22600..e2a56dfdf2 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -64,6 +64,9 @@ const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; +const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; +const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 3; /// Local slot metadata needed for conservative eval scope synchronization. #[derive(Clone)] @@ -136,6 +139,10 @@ struct EvalNativeMemberAttributeRegistration { enum EvalNativeCallableDefault { Scalar { kind: i64, payload: i64 }, String(String), + Object { + class_name: String, + args: Vec, + }, } /// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. @@ -1134,6 +1141,11 @@ fn eval_native_php_type_member_specs(members: &[PhpType]) -> Option { /// Converts a PHP signature default into the compact eval bridge default ABI. fn eval_native_callable_default(expr: &Expr) -> Option { + eval_native_literal_default(expr).or_else(|| eval_native_object_default(expr)) +} + +/// Converts scalar/string/empty-array defaults into the compact eval bridge default ABI. +fn eval_native_literal_default(expr: &Expr) -> Option { match &expr.kind { ExprKind::Null => Some(EvalNativeCallableDefault::Scalar { kind: NATIVE_DEFAULT_NULL, @@ -1163,6 +1175,24 @@ fn eval_native_callable_default(expr: &Expr) -> Option Option { + let ExprKind::NewObject { class_name, args } = &expr.kind else { + return None; + }; + if args.len() > MAX_NATIVE_OBJECT_DEFAULT_ARGS { + return None; + } + let mut default_args = Vec::with_capacity(args.len()); + for arg in args { + default_args.push(eval_native_literal_default(arg)?); + } + Some(EvalNativeCallableDefault::Object { + class_name: class_name.as_canonical(), + args: default_args, + }) +} + /// Converts supported property defaults into the compact eval bridge default ABI. fn eval_native_property_default( default: Option<&Expr>, @@ -1170,7 +1200,7 @@ fn eval_native_property_default( is_abstract: bool, ) -> Option { if let Some(default) = default { - return eval_native_callable_default(default); + return eval_native_literal_default(default); } (!is_declared && !is_abstract).then_some(EvalNativeCallableDefault::Scalar { kind: NATIVE_DEFAULT_NULL, @@ -1197,6 +1227,43 @@ fn eval_native_callable_negated_default(expr: &Expr) -> Option Vec { + let EvalNativeCallableDefault::Object { class_name, args } = default else { + return Vec::new(); + }; + let mut bytes = Vec::new(); + encode_eval_native_default_string(&mut bytes, class_name); + bytes.push(args.len() as u8); + for arg in args { + encode_eval_native_object_default_arg(&mut bytes, arg); + } + bytes +} + +/// Encodes one object-default constructor argument for libelephc-magician. +fn encode_eval_native_object_default_arg(bytes: &mut Vec, default: &EvalNativeCallableDefault) { + match default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_SCALAR); + bytes.extend_from_slice(&(*kind as u64).to_le_bytes()); + bytes.extend_from_slice(&(*payload as u64).to_le_bytes()); + } + EvalNativeCallableDefault::String(value) => { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_STRING); + encode_eval_native_default_string(bytes, value); + } + EvalNativeCallableDefault::Object { .. } => {} + } +} + +/// Encodes one UTF-8 string with a little-endian u32 byte-length prefix. +fn encode_eval_native_default_string(bytes: &mut Vec, value: &str) { + let len = u32::try_from(value.len()).unwrap_or(u32::MAX); + bytes.extend_from_slice(&len.to_le_bytes()); + bytes.extend_from_slice(value.as_bytes()); +} + /// Returns true when an instance method is public in the class metadata. fn class_method_is_public(class_info: &ClassInfo, method_name: &str) -> bool { class_info @@ -1682,6 +1749,29 @@ fn register_eval_native_method_param_default( .extern_symbol("__elephc_eval_register_native_method_param_default_string") } } + EvalNativeCallableDefault::Object { .. } => { + let spec = encode_eval_native_object_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + if is_static { + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_static_method_param_default_object", + ) + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_default_object") + } + } }; abi::emit_call_label(ctx.emitter, &symbol); } @@ -1934,6 +2024,7 @@ fn register_eval_native_property_default( .target .extern_symbol("__elephc_eval_register_native_property_default_string") } + EvalNativeCallableDefault::Object { .. } => return, }; abi::emit_call_label(ctx.emitter, &symbol); } @@ -2205,6 +2296,23 @@ fn register_eval_native_constructor_param_default( .target .extern_symbol("__elephc_eval_register_native_constructor_param_default_string") } + EvalNativeCallableDefault::Object { .. } => { + let spec = encode_eval_native_object_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_default_object") + } }; abi::emit_call_label(ctx.emitter, &symbol); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4f14d0e019..6ac17a25c7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8371,6 +8371,37 @@ return $obj->countItems();'); assert_eq!(out, "0"); } +/// Verifies eval materializes generated/AOT object defaults during method dispatch. +#[test] +fn test_eval_aot_method_call_uses_object_default() { + let out = compile_and_run( + r#"label = $label; + } +} + +class EvalAotObjectDefaultMethodTarget { + public function describe(EvalAotObjectDefaultMethodDep $dep = new EvalAotObjectDefaultMethodDep("method")): string { + return $dep->label; + } + + public static function describeStatic(EvalAotObjectDefaultMethodDep $dep = new EvalAotObjectDefaultMethodDep("static")): string { + return $dep->label; + } +} + +echo eval('$obj = new EvalAotObjectDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotObjectDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +return $obj->describe() . ":" . EvalAotObjectDefaultMethodTarget::describeStatic() . ":" . $default->label;'); +"#, + ); + assert_eq!(out, "method:static:method"); +} + /// Verifies eval ReflectionMethod exposes generated/AOT by-ref and variadic parameter flags. #[test] fn test_eval_reflection_method_exposes_aot_parameter_flags() { @@ -10844,6 +10875,32 @@ return $box->count;'); assert_eq!(out, "0"); } +/// Verifies eval materializes generated/AOT object defaults during constructor dispatch. +#[test] +fn test_eval_dynamic_new_uses_constructor_object_default() { + let out = compile_and_run( + r#"label = $label; + } +} + +class EvalDynamicNewObjectDefaultCtor { + public string $label = ""; + public function __construct(EvalDynamicNewObjectDefaultDep $dep = new EvalDynamicNewObjectDefaultDep("ctor")) { + $this->label = $dep->label; + } +} + +echo eval('$box = new EvalDynamicNewObjectDefaultCtor(); +return $box->label;'); +"#, + ); + assert_eq!(out, "ctor"); +} + /// Verifies eval object construction passes more than two arguments to an AOT constructor. #[test] fn test_eval_dynamic_new_runs_constructor_with_many_args() { From 0970d26632d69928b503a20d5ada9d6d37a84a8c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 16:26:08 +0200 Subject: [PATCH 0589/1208] Support eval AOT variadic bridge args --- .../src/interpreter/statements.rs | 97 ++++++++++++++++++- docs/php/eval.md | 22 ++--- src/codegen/eval_constructor_helpers.rs | 4 +- src/codegen/eval_method_helpers.rs | 1 - src/codegen/lower_inst/builtins/eval.rs | 2 - tests/codegen/eval.rs | 42 ++++++-- 6 files changed, 142 insertions(+), 26 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index a95bede247..45406219c9 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4497,17 +4497,40 @@ fn bind_native_signature_args( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let mut bound_args = vec![None; signature.param_count()]; + let variadic_index = native_callable_variadic_index(signature); let mut next_positional = 0; + let mut next_variadic_index = 0_i64; + + if let Some(index) = variadic_index { + let array = values.array_new(args.len())?; + bound_args[index] = Some(array); + } for arg in args { if let Some(name) = arg.name { - bind_dynamic_named_arg(signature.param_names(), &mut bound_args, &name, arg.value)?; + bind_native_named_signature_arg( + signature, + variadic_index, + &mut bound_args, + &name, + arg.value, + )?; } else { - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + bind_native_positional_signature_arg( + &mut bound_args, + variadic_index, + &mut next_positional, + &mut next_variadic_index, + arg.value, + values, + )?; } } for (position, value) in bound_args.iter_mut().enumerate() { + if Some(position) == variadic_index { + continue; + } if value.is_some() { continue; } @@ -4526,6 +4549,76 @@ fn bind_native_signature_args( .ok_or(EvalStatus::RuntimeFatal) } +/// Returns the native callable variadic slot, if metadata registered one. +fn native_callable_variadic_index(signature: &NativeCallableSignature) -> Option { + (0..signature.param_count()).find(|index| signature.param_variadic(*index)) +} + +/// Binds one positional native AOT argument to a fixed slot or variadic array. +fn bind_native_positional_signature_arg( + bound_args: &mut [Option], + variadic_index: Option, + next_positional: &mut usize, + next_variadic_index: &mut i64, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if variadic_index.is_some_and(|index| *next_positional >= index) { + let key = values.int(*next_variadic_index)?; + *next_variadic_index = next_variadic_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + return bind_native_variadic_arg(bound_args, variadic_index, key, value, values); + } + bind_dynamic_positional_arg(bound_args, next_positional, value) +} + +/// Binds one named native AOT argument to a fixed non-variadic slot. +fn bind_native_named_signature_arg( + signature: &NativeCallableSignature, + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + if let Some(param_index) = native_regular_param_index(signature, variadic_index, name) { + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + return Ok(()); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Returns the matching non-variadic native parameter index for one named arg. +fn native_regular_param_index( + signature: &NativeCallableSignature, + variadic_index: Option, + name: &str, +) -> Option { + signature + .param_names() + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + +/// Appends one value into the native AOT variadic argument array. +fn bind_native_variadic_arg( + bound_args: &mut [Option], + variadic_index: Option, + key: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; + let bound = bound_args[index].ok_or(EvalStatus::RuntimeFatal)?; + let array = values.array_set(bound, key, value)?; + bound_args[index] = Some(array); + Ok(()) +} + /// Allocates a fresh runtime cell for one invocation-safe native AOT default. fn materialize_native_callable_default( default: &NativeCallableDefault, diff --git a/docs/php/eval.md b/docs/php/eval.md index c77d2bb77c..270d29cba1 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, array-typed arguments, object-typed arguments, and registered scalar, null, empty-array, or supported object-valued default arguments for public scalar/Mixed/array/object constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, object-typed arguments, and registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same argument binding plus registered scalar, null, empty-array, or supported object-valued default arguments for public scalar/Mixed/array/object method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/object method signatures. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -127,8 +127,9 @@ as callable. Static method callables can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method -fallback supports the same named-argument binding for public scalar/Mixed/array/object -signatures supported by the generated bridge. +fallback supports the same named-argument and positional variadic-tail binding for public +non-by-reference scalar/Mixed/array/object signatures supported by the +generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -633,12 +634,11 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are -still outside eval fragments. Runtime/AOT object-method and static-method -fallback from eval remain limited to the generated public non-by-reference fixed -scalar/Mixed/array/object bridge slice, while runtime/AOT constructor fallback remains -limited to public non-by-reference fixed scalar/Mixed/array/object signatures. Variadic, -by-reference, and broader parameter/return ABI shapes are still outside those -bridge paths. +still outside eval fragments. Runtime/AOT object-method, static-method, and +constructor fallback from eval remain limited to generated public +non-by-reference scalar/Mixed/array/object bridge signatures, including the +generated positional variadic array slot when present. By-reference parameters and broader +parameter/return ABI shapes are still outside those bridge paths. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported @@ -649,7 +649,7 @@ parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional `new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference fixed scalar/Mixed/array/object slice plus visibility-checked +non-by-reference scalar/Mixed/array/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT method/property attributes are exposed for registered metadata slices, while diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 13b5c3b3b5..ba31e5b733 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -9,7 +9,8 @@ //! - The cacheable runtime object can allocate by name, but only user assembly //! knows constructor symbols and parameter ABI shapes. //! - Classes without constructors are treated as successful no-ops, matching PHP. -//! - Constructors are bridged for fixed non-by-ref scalar/Mixed/array/object arguments. +//! - Constructors are bridged for non-by-ref scalar/Mixed/array/object arguments, +//! including a generated variadic array slot when the signature has one. use std::collections::BTreeMap; @@ -163,7 +164,6 @@ fn constructor_is_public(class_info: &ClassInfo, method_key: &str) -> bool { /// Returns true for constructor signatures supported by this eval bridge slice. fn constructor_signature_supported(sig: &FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_CONSTRUCTOR_ARGS - && sig.variadic.is_none() && sig.ref_params.iter().all(|is_ref| !*is_ref) && sig .params diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index bb26d1f098..c0d3294efd 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -245,7 +245,6 @@ fn static_method_is_public(class_info: &ClassInfo, method: &str) -> bool { /// Returns true for method signatures supported by the eval bridge. fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_METHOD_ARGS - && sig.variadic.is_none() && sig.ref_params.iter().all(|is_ref| !*is_ref) && sig.params.iter().all(|(_, ty)| method_param_supported(ty)) } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index e2a56dfdf2..262eafebe1 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -962,7 +962,6 @@ fn function_can_register_with_eval(function: &Function) -> bool { /// Returns true when eval can dispatch a native method through the generated bridge. fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS - && signature.variadic.is_none() && signature.ref_params.iter().all(|is_ref| !*is_ref) && signature .params @@ -974,7 +973,6 @@ fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { /// Returns true when eval can dispatch a native constructor through the generated bridge. fn constructor_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS - && signature.variadic.is_none() && signature.ref_params.iter().all(|is_ref| !*is_ref) && signature .params diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6ac17a25c7..bf43b501d8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8444,19 +8444,45 @@ echo $static->getNumberOfRequiredParameters(); assert_eq!(out.stdout, "2/1:valueRvr;partsbVO;:itemsbV0"); } -/// Verifies metadata-only generated/AOT signatures remain non-dispatchable through eval. +/// Verifies eval can dispatch generated/AOT variadic methods and constructors. #[test] -fn test_eval_aot_variadic_method_metadata_only_call_fails() { +fn test_eval_aot_variadic_method_and_constructor_bridge() { + let out = compile_and_run( + r#"label = $head . ":" . count($items) . ":" . $items[0] . ":" . $items[1]; + } + + public function collect($head, ...$items): string { + return $head . ":" . count($items) . ":" . $items[0] . ":" . $items[1]; + } + + public static function collectStatic(...$items): string { + return count($items) . ":" . $items[0] . ":" . $items[1]; + } +} +echo eval('$target = new EvalAotVariadicBridgeTarget("C", "D", "E"); +return $target->collect("H", "A", "B") . "|" . EvalAotVariadicBridgeTarget::collectStatic("S", "T") . "|" . $target->label;'); +"#, + ); + assert_eq!(out, "H:2:A:B|2:S:T|C:2:D:E"); +} + +/// Verifies generated/AOT variadic bridge rejects named arguments captured by the tail. +#[test] +fn test_eval_aot_variadic_method_rejects_named_tail() { let err = compile_and_run_expect_failure( r#"getParameters()[0]->isVariadic() ? "V" : "v"; -$target->collect("A");'); +eval('$target = new EvalAotVariadicNamedTailTarget(); +$target->collect(named: "A");'); "#, ); assert!( From c66a9cf611073554e804afdcabdacbc7e28f4a28 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 16:32:53 +0200 Subject: [PATCH 0590/1208] Raise eval object default arg limit --- crates/elephc-magician/src/ffi/native_methods.rs | 2 +- docs/php/classes.md | 4 ++-- docs/php/eval.md | 2 +- src/codegen/lower_inst/builtins/eval.rs | 2 +- src/codegen/lower_inst/objects/reflection.rs | 4 ++-- src/types/checker/builtin_types/reflection.rs | 2 +- tests/codegen/eval.rs | 16 ++++++++-------- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index a62c084b24..58996d1a0a 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -32,7 +32,7 @@ const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; -const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 3; +const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; #[derive(Clone, Copy)] enum NativeCallableTypePosition { diff --git a/docs/php/classes.md b/docs/php/classes.md index f50047b8d4..e4b9c66f72 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1243,7 +1243,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionParameter::getDeclaringClass()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionClass` object for method parameters, or `null` for function parameters | | `ReflectionParameter::getDeclaringFunction()` | Same construction forms as `ReflectionParameter::hasType()` | Return a `ReflectionMethod` object for method parameters or a `ReflectionFunction` object for function parameters | | `ReflectionParameter::isDefaultValueAvailable()` | Same construction forms as `ReflectionParameter::isOptional()` | Return whether a reflected parameter has a materialized default value | -| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and object default values with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults, or throw `ReflectionException` when no default is available | +| `ReflectionParameter::getDefaultValue()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return supported scalar/null, class-constant, array literal, and object default values with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults, or throw `ReflectionException` when no default is available | | `ReflectionParameter::isDefaultValueConstant()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return whether the retained default came from a named constant | | `ReflectionParameter::getDefaultValueConstantName()` | Same construction forms as `ReflectionParameter::isDefaultValueAvailable()` | Return the retained constant name when the default is constant-backed, `null` otherwise | | `ReflectionNamedType::getName()` / `allowsNull()` / `isBuiltin()` / `__toString()` | `ReflectionParameter::getType()` | Return and stringify simple parameter type metadata | @@ -1368,7 +1368,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to three supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. diff --git a/docs/php/eval.md b/docs/php/eval.md index 270d29cba1..a6e5d82f14 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -648,7 +648,7 @@ property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional -`new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public +`new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public non-by-reference scalar/Mixed/array/object slice plus visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 262eafebe1..5fdeb9196f 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -66,7 +66,7 @@ const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; -const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 3; +const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; /// Local slot metadata needed for conservative eval scope synchronization. #[derive(Clone)] diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 7362732c22..2aa9841943 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -3595,7 +3595,7 @@ fn reflection_object_parameter_default_args( class_name: &str, args: &[Expr], ) -> Result>> { - if args.len() > 3 { + if args.len() > 8 { return Ok(None); } let mut values = Vec::with_capacity(args.len()); @@ -3620,7 +3620,7 @@ fn reflection_object_parameter_default_args( }; if constructor.variadic.is_some() || values.len() > constructor.params.len() - || constructor.params.len() > 3 + || constructor.params.len() > 8 { return Ok(None); } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 436e0455c5..079b426ae3 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -995,7 +995,7 @@ fn reflection_parameter_get_default_object_body(span: crate::span::Span) -> Vec< span, ), ]; - for arg_count in 1..=3 { + for arg_count in 1..=8 { body.push(reflection_parameter_default_object_arg_count_branch( arg_count, arg_count_var, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bf43b501d8..2dae4b4f50 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8378,17 +8378,17 @@ fn test_eval_aot_method_call_uses_object_default() { r#"label = $label; + public function __construct(string $left = "d", string $right = "e", string $third = "p", string $fourth = "") { + $this->label = $left . $right . $third . $fourth; } } class EvalAotObjectDefaultMethodTarget { - public function describe(EvalAotObjectDefaultMethodDep $dep = new EvalAotObjectDefaultMethodDep("method")): string { + public function describe(EvalAotObjectDefaultMethodDep $dep = new EvalAotObjectDefaultMethodDep("m", "e", "t", "h")): string { return $dep->label; } - public static function describeStatic(EvalAotObjectDefaultMethodDep $dep = new EvalAotObjectDefaultMethodDep("static")): string { + public static function describeStatic(EvalAotObjectDefaultMethodDep $dep = new EvalAotObjectDefaultMethodDep("s", "t", "a", "t")): string { return $dep->label; } } @@ -8399,7 +8399,7 @@ $default = $method->getParameters()[0]->getDefaultValue(); return $obj->describe() . ":" . EvalAotObjectDefaultMethodTarget::describeStatic() . ":" . $default->label;'); "#, ); - assert_eq!(out, "method:static:method"); + assert_eq!(out, "meth:stat:meth"); } /// Verifies eval ReflectionMethod exposes generated/AOT by-ref and variadic parameter flags. @@ -10908,14 +10908,14 @@ fn test_eval_dynamic_new_uses_constructor_object_default() { r#"label = $label; + public function __construct(string $left = "d", string $right = "e", string $third = "p", string $fourth = "") { + $this->label = $left . $right . $third . $fourth; } } class EvalDynamicNewObjectDefaultCtor { public string $label = ""; - public function __construct(EvalDynamicNewObjectDefaultDep $dep = new EvalDynamicNewObjectDefaultDep("ctor")) { + public function __construct(EvalDynamicNewObjectDefaultDep $dep = new EvalDynamicNewObjectDefaultDep("c", "t", "o", "r")) { $this->label = $dep->label; } } From ac00e96af5e4598264976dc32e76968b01161058 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 16:43:59 +0200 Subject: [PATCH 0591/1208] Support eval AOT iterable returns --- docs/php/eval.md | 15 +++++++------- src/codegen/eval_method_helpers.rs | 1 + src/codegen/lower_inst.rs | 9 +++++++++ src/codegen/lower_inst/builtins/eval.rs | 1 + tests/codegen/eval.rs | 27 +++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index a6e5d82f14..16a1de1d7b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -76,7 +76,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, object-typed arguments, and registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/object method signatures. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/object parameters; scalar/Mixed/array/iterable/object return values are boxed back to eval. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -128,7 +128,7 @@ as callable. Static method callables can use `["ClassName", "method"]` or `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method fallback supports the same named-argument and positional variadic-tail binding for public -non-by-reference scalar/Mixed/array/object signatures supported by the +non-by-reference scalar/Mixed/array/object parameter signatures supported by the generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -512,9 +512,10 @@ constants. Direct `new EnumName()` and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native -methods are bridged to eval. Public fixed scalar/Mixed/array/object method calls -through `$this->method(...)` are supported by the native method bridge, -including registered named arguments and string-keyed unpacking. +methods are bridged to eval. Public fixed scalar/Mixed/array/object method +parameters through `$this->method(...)` are supported by the native method +bridge, including registered named arguments and string-keyed unpacking; method +returns may be scalar/Mixed/array/iterable/object values. ## Namespaces and constants @@ -636,7 +637,7 @@ The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval remain limited to generated public -non-by-reference scalar/Mixed/array/object bridge signatures, including the +non-by-reference scalar/Mixed/array/object parameter bridge signatures, including the generated positional variadic array slot when present. By-reference parameters and broader parameter/return ABI shapes are still outside those bridge paths. @@ -649,7 +650,7 @@ parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional `new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference scalar/Mixed/array/object slice plus visibility-checked +non-by-reference scalar/Mixed/array/object parameter slice plus scalar/Mixed/array/iterable/object returns and visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT method/property attributes are exposed for registered metadata slices, while diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index c0d3294efd..529916f4e3 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -275,6 +275,7 @@ fn method_return_supported(ty: &PhpType) -> bool { | PhpType::Str | PhpType::Mixed | PhpType::Union(_) + | PhpType::Iterable | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 9bdde11382..b5b35121ba 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -1928,6 +1928,15 @@ fn lower_runtime_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Resu emit_object_tostring_call(ctx, value, normalized)?; return store_if_result(ctx, inst); } + if inst.result_php_type.codegen_repr() == PhpType::Iterable + && matches!( + source_ty, + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) | PhpType::Iterable + ) + { + ctx.load_value_to_result(value)?; + return store_if_result(ctx, inst); + } if inst.result_php_type.codegen_repr() == PhpType::TaggedScalar { match source_ty { PhpType::Int | PhpType::Bool | PhpType::Callable => { diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 5fdeb9196f..ea57a56051 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -1021,6 +1021,7 @@ fn eval_native_method_return_supported(ty: &PhpType) -> bool { | PhpType::Str | PhpType::Mixed | PhpType::Union(_) + | PhpType::Iterable | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2dae4b4f50..4dd07cdf23 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4649,6 +4649,33 @@ echo (new EvalMethodArrayArgBox())->run(); assert_eq!(out, "3:2"); } +/// Verifies eval fragments can read iterable return values from AOT methods. +#[test] +fn test_eval_fragment_dispatches_aot_method_with_iterable_return() { + let out = compile_and_run( + r#" "L", "right" => "R"]; + } + + public function run() { + return eval('$items = $this->items(); +$labels = EvalAotIterableReturnBox::labels(); +return is_iterable($items) . ":" . count($items) . ":" . $items[1] . ":" . is_iterable($labels) . ":" . $labels["right"];'); + } +} + +echo (new EvalAotIterableReturnBox())->run(); +"#, + ); + assert_eq!(out, "1:3:2:1:R"); +} + /// Verifies eval fragments inherit lexical `self::` from an AOT instance method. #[test] fn test_eval_fragment_in_aot_method_resolves_self_scope() { From e9b01e6c49ffc0aa0b78b74d6c04b5e16f98a529 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 16:55:24 +0200 Subject: [PATCH 0592/1208] Support eval AOT iterable parameters --- docs/php/eval.md | 12 +- src/codegen/eval_constructor_helpers.rs | 195 +++++++++++++++++++++++- src/codegen/eval_method_helpers.rs | 139 ++++++++++++++++- src/codegen/lower_inst/builtins/eval.rs | 2 + tests/codegen/eval.rs | 47 ++++++ 5 files changed, 377 insertions(+), 18 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 16a1de1d7b..af99037621 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, object-typed arguments, and registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/object constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/iterable/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/object parameters; scalar/Mixed/array/iterable/object return values are boxed back to eval. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/iterable/object parameters; scalar/Mixed/array/iterable/object return values are boxed back to eval. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -128,7 +128,7 @@ as callable. Static method callables can use `["ClassName", "method"]` or `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method fallback supports the same named-argument and positional variadic-tail binding for public -non-by-reference scalar/Mixed/array/object parameter signatures supported by the +non-by-reference scalar/Mixed/array/iterable/object parameter signatures supported by the generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -512,7 +512,7 @@ constants. Direct `new EnumName()` and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native -methods are bridged to eval. Public fixed scalar/Mixed/array/object method +methods are bridged to eval. Public fixed scalar/Mixed/array/iterable/object method parameters through `$this->method(...)` are supported by the native method bridge, including registered named arguments and string-keyed unpacking; method returns may be scalar/Mixed/array/iterable/object values. @@ -637,7 +637,7 @@ The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval remain limited to generated public -non-by-reference scalar/Mixed/array/object parameter bridge signatures, including the +non-by-reference scalar/Mixed/array/iterable/object parameter bridge signatures, including the generated positional variadic array slot when present. By-reference parameters and broader parameter/return ABI shapes are still outside those bridge paths. @@ -650,7 +650,7 @@ parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional `new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference scalar/Mixed/array/object parameter slice plus scalar/Mixed/array/iterable/object returns and visibility-checked +non-by-reference scalar/Mixed/array/iterable/object parameter slice plus scalar/Mixed/array/iterable/object returns and visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT method/property attributes are exposed for registered metadata slices, while diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index ba31e5b733..ca59e80272 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -180,6 +180,7 @@ fn constructor_param_supported(ty: &PhpType) -> bool { | PhpType::Float | PhpType::Str | PhpType::Mixed + | PhpType::Iterable | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) @@ -339,7 +340,13 @@ fn emit_aarch64_builtin_throwable_constructor_body( emitter.instruction("cmp x9, #0"); // did the eval call pass a message argument? emitter.instruction(&format!("b.eq {}", success_label)); // keep the empty Throwable defaults when no message was supplied emit_aarch64_load_eval_arg(module, emitter, 0); - emit_aarch64_cast_eval_arg(emitter, &PhpType::Str, fail_label); + emit_aarch64_cast_eval_arg( + module, + emitter, + &PhpType::Str, + "__elephc_eval_builtin_throwable_message", + fail_label, + ); emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for message initialization emitter.instruction("str x1, [x9, #8]"); // store the message pointer in the compact Throwable payload emitter.instruction("str x2, [x9, #16]"); // store the message length in the compact Throwable payload @@ -347,7 +354,13 @@ fn emit_aarch64_builtin_throwable_constructor_body( emitter.instruction("cmp x9, #1"); // did the eval call pass a code argument? emitter.instruction(&format!("b.le {}", success_label)); // keep code zero when only the message was supplied emit_aarch64_load_eval_arg(module, emitter, 1); - emit_aarch64_cast_eval_arg(emitter, &PhpType::Int, fail_label); + emit_aarch64_cast_eval_arg( + module, + emitter, + &PhpType::Int, + "__elephc_eval_builtin_throwable_code", + fail_label, + ); emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for code initialization emitter.instruction("str x0, [x9, #24]"); // store the integer exception code emitter.instruction(&format!("b {}", success_label)); // builtin Throwable construction completed @@ -366,7 +379,13 @@ fn emit_x86_64_builtin_throwable_constructor_body( emitter.instruction("cmp r11, 0"); // did the eval call pass a message argument? emitter.instruction(&format!("je {}", success_label)); // keep the empty Throwable defaults when no message was supplied emit_x86_64_load_eval_arg(module, emitter, 0); - emit_x86_64_cast_eval_arg(emitter, &PhpType::Str, fail_label); + emit_x86_64_cast_eval_arg( + module, + emitter, + &PhpType::Str, + "__elephc_eval_builtin_throwable_message_x", + fail_label, + ); emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for message initialization emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store the message pointer in the compact Throwable payload emitter.instruction("mov QWORD PTR [r11 + 16], rdx"); // store the message length in the compact Throwable payload @@ -374,7 +393,13 @@ fn emit_x86_64_builtin_throwable_constructor_body( emitter.instruction("cmp r11, 1"); // did the eval call pass a code argument? emitter.instruction(&format!("jle {}", success_label)); // keep code zero when only the message was supplied emit_x86_64_load_eval_arg(module, emitter, 1); - emit_x86_64_cast_eval_arg(emitter, &PhpType::Int, fail_label); + emit_x86_64_cast_eval_arg( + module, + emitter, + &PhpType::Int, + "__elephc_eval_builtin_throwable_code_x", + fail_label, + ); emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for code initialization emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store the integer exception code emitter.instruction(&format!("jmp {}", success_label)); // builtin Throwable construction completed @@ -554,7 +579,8 @@ fn emit_aarch64_prepare_constructor_args( abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { emit_aarch64_load_eval_arg(module, emitter, index); - emit_aarch64_cast_eval_arg(emitter, param_ty, fail_label); + let label_prefix = constructor_arg_label(module, slot, index); + emit_aarch64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_constructor_args(module, emitter, &receiver_ty, &slot.params) @@ -572,7 +598,8 @@ fn emit_x86_64_prepare_constructor_args( abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { emit_x86_64_load_eval_arg(module, emitter, index); - emit_x86_64_cast_eval_arg(emitter, param_ty, fail_label); + let label_prefix = constructor_arg_label(module, slot, index); + emit_x86_64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_constructor_args(module, emitter, &receiver_ty, &slot.params) @@ -619,7 +646,13 @@ fn emit_x86_64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usiz } /// Casts one boxed eval argument into ARM64 result registers for temporary staging. -fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType, fail_label: &str) { +fn emit_aarch64_cast_eval_arg( + module: &Module, + emitter: &mut Emitter, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, +) { match param_ty.codegen_repr() { PhpType::Int => { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for integer coercion @@ -654,6 +687,9 @@ fn emit_aarch64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType, fail_la PhpType::AssocArray { .. } => { emit_aarch64_cast_eval_array_arg(emitter, param_ty, 5, fail_label); } + PhpType::Iterable => { + emit_aarch64_cast_eval_iterable_arg(module, emitter, param_ty, label_prefix, fail_label); + } _ => {} } } @@ -674,8 +710,71 @@ fn emit_aarch64_cast_eval_array_arg( abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); } +/// Validates and unboxes one ARM64 iterable-typed eval argument for native constructors. +fn emit_aarch64_cast_eval_iterable_arg( + module: &Module, + emitter: &mut Emitter, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, +) { + let payload_ok = format!("{}_iterable_payload", label_prefix); + let object_case = format!("{}_iterable_object", label_prefix); + let object_ok = format!("{}_iterable_object_ok", label_prefix); + let done = format!("{}_iterable_done", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for iterable unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete iterable payload tag and pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.eq {}", payload_ok)); // indexed arrays satisfy iterable parameters + emitter.instruction("cmp x0, #5"); // runtime tag 5 means associative array + emitter.instruction(&format!("b.eq {}", payload_ok)); // associative arrays satisfy iterable parameters + emitter.instruction("cmp x0, #6"); // runtime tag 6 means object + emitter.instruction(&format!("b.eq {}", object_case)); // object values need Traversable interface validation + emitter.instruction(&format!("b {}", fail_label)); // reject scalar values for iterable parameters + emitter.label(&payload_ok); + emitter.instruction("mov x0, x1"); // move the array payload into the result register + emitter.instruction(&format!("b {}", done)); // skip object-specific interface validation + emitter.label(&object_case); + emit_aarch64_validate_iterable_object(module, emitter, &object_ok, fail_label); + emitter.label(&object_ok); + emitter.instruction("ldr x0, [sp], #16"); // restore the iterable object pointer as the result + emitter.label(&done); + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); +} + +/// Validates the ARM64 object payload saved in `x1` against Traversable interfaces. +fn emit_aarch64_validate_iterable_object( + module: &Module, + emitter: &mut Emitter, + object_ok: &str, + fail_label: &str, +) { + let interface_ids = traversable_interface_ids(module); + if interface_ids.is_empty() { + emitter.instruction(&format!("b {}", fail_label)); // reject objects when no Traversable interface metadata exists + return; + } + emitter.instruction("str x1, [sp, #-16]!"); // preserve the object payload across Traversable checks + for interface_id in interface_ids { + emitter.instruction("ldr x0, [sp]"); // reload the object pointer as matcher argument 1 + abi::emit_load_int_immediate(emitter, "x1", interface_id as i64); + abi::emit_load_int_immediate(emitter, "x2", 1); + abi::emit_call_label(emitter, "__rt_exception_matches"); + emitter.instruction("cmp x0, #0"); // test whether the object implements this Traversable interface + emitter.instruction(&format!("b.ne {}", object_ok)); // matching Iterator metadata accepts the object + } + emitter.instruction("add sp, sp, #16"); // discard the rejected object payload + emitter.instruction(&format!("b {}", fail_label)); // reject non-Traversable objects for iterable parameters +} + /// Casts one boxed eval argument into x86_64 result registers for temporary staging. -fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType, fail_label: &str) { +fn emit_x86_64_cast_eval_arg( + module: &Module, + emitter: &mut Emitter, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, +) { match param_ty.codegen_repr() { PhpType::Int => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion @@ -710,6 +809,9 @@ fn emit_x86_64_cast_eval_arg(emitter: &mut Emitter, param_ty: &PhpType, fail_lab PhpType::AssocArray { .. } => { emit_x86_64_cast_eval_array_arg(emitter, param_ty, 5, fail_label); } + PhpType::Iterable => { + emit_x86_64_cast_eval_iterable_arg(module, emitter, param_ty, label_prefix, fail_label); + } _ => {} } } @@ -730,6 +832,63 @@ fn emit_x86_64_cast_eval_array_arg( abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); } +/// Validates and unboxes one x86_64 iterable-typed eval argument for native constructors. +fn emit_x86_64_cast_eval_iterable_arg( + module: &Module, + emitter: &mut Emitter, + param_ty: &PhpType, + label_prefix: &str, + fail_label: &str, +) { + let payload_ok = format!("{}_iterable_payload", label_prefix); + let object_case = format!("{}_iterable_object", label_prefix); + let object_ok = format!("{}_iterable_object_ok", label_prefix); + let done = format!("{}_iterable_done", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for iterable unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete iterable payload tag and pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("je {}", payload_ok)); // indexed arrays satisfy iterable parameters + emitter.instruction("cmp rax, 5"); // runtime tag 5 means associative array + emitter.instruction(&format!("je {}", payload_ok)); // associative arrays satisfy iterable parameters + emitter.instruction("cmp rax, 6"); // runtime tag 6 means object + emitter.instruction(&format!("je {}", object_case)); // object values need Traversable interface validation + emitter.instruction(&format!("jmp {}", fail_label)); // reject scalar values for iterable parameters + emitter.label(&payload_ok); + emitter.instruction("mov rax, rdi"); // move the array payload into the result register + emitter.instruction(&format!("jmp {}", done)); // skip object-specific interface validation + emitter.label(&object_case); + emit_x86_64_validate_iterable_object(module, emitter, &object_ok, fail_label); + emitter.label(&object_ok); + abi::emit_pop_reg(emitter, "rax"); + emitter.label(&done); + abi::emit_incref_if_refcounted(emitter, ¶m_ty.codegen_repr()); +} + +/// Validates the x86_64 object payload saved in `rdi` against Traversable interfaces. +fn emit_x86_64_validate_iterable_object( + module: &Module, + emitter: &mut Emitter, + object_ok: &str, + fail_label: &str, +) { + let interface_ids = traversable_interface_ids(module); + if interface_ids.is_empty() { + emitter.instruction(&format!("jmp {}", fail_label)); // reject objects when no Traversable interface metadata exists + return; + } + abi::emit_push_reg(emitter, "rdi"); + for interface_id in interface_ids { + emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the object pointer as matcher argument 1 + abi::emit_load_int_immediate(emitter, "rsi", interface_id as i64); + abi::emit_load_int_immediate(emitter, "rdx", 1); + abi::emit_call_label(emitter, "__rt_exception_matches"); + emitter.instruction("test rax, rax"); // test whether the object implements this Traversable interface + emitter.instruction(&format!("jne {}", object_ok)); // matching Iterator metadata accepts the object + } + abi::emit_pop_reg(emitter, "r10"); + emitter.instruction(&format!("jmp {}", fail_label)); // reject non-Traversable objects for iterable parameters +} + /// Groups constructor slots by class id while preserving sorted class order. fn grouped_slots(slots: &[EvalConstructorSlot]) -> BTreeMap> { let mut grouped = BTreeMap::new(); @@ -742,6 +901,26 @@ fn grouped_slots(slots: &[EvalConstructorSlot]) -> BTreeMap String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!( + "__elephc_eval_constructor_{}_arg_{}{}", + slot.class_id, index, suffix + ) +} + +/// Returns runtime interface ids for object values accepted by PHP iterable parameters. +fn traversable_interface_ids(module: &Module) -> Vec { + ["Iterator", "IteratorAggregate"] + .into_iter() + .filter_map(|name| module.interface_infos.get(name).map(|info| info.interface_id)) + .collect() +} + /// Emits a platform-C global label for a user assembly helper. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { emitter.label_global(&module.target.extern_symbol(name)); diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 529916f4e3..604a13c704 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -258,6 +258,7 @@ fn method_param_supported(ty: &PhpType) -> bool { | PhpType::Float | PhpType::Str | PhpType::Mixed + | PhpType::Iterable | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) @@ -995,7 +996,8 @@ fn emit_aarch64_prepare_method_args( abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { emit_aarch64_load_eval_arg(module, emitter, index); - emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, fail_label); + let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); + emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_method_args(module, emitter, &receiver_ty, &slot.params) @@ -1013,7 +1015,8 @@ fn emit_aarch64_prepare_static_method_args( abi::emit_push_result_value(emitter, &PhpType::Int); for (index, param_ty) in slot.params.iter().enumerate() { emit_aarch64_load_eval_arg(module, emitter, index); - emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, fail_label); + let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); + emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_static_method_args(module, emitter, slot) @@ -1032,7 +1035,8 @@ fn emit_x86_64_prepare_method_args( abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { emit_x86_64_load_eval_arg(module, emitter, index); - emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, fail_label); + let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); + emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_method_args(module, emitter, &receiver_ty, &slot.params) @@ -1050,7 +1054,8 @@ fn emit_x86_64_prepare_static_method_args( abi::emit_push_result_value(emitter, &PhpType::Int); for (index, param_ty) in slot.params.iter().enumerate() { emit_x86_64_load_eval_arg(module, emitter, index); - emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, fail_label); + let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); + emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } materialize_static_method_args(module, emitter, slot) @@ -1115,6 +1120,7 @@ fn emit_aarch64_cast_eval_arg( emitter: &mut Emitter, data: &mut DataSection, param_ty: &PhpType, + label_prefix: &str, fail_label: &str, ) { match param_ty.codegen_repr() { @@ -1146,6 +1152,9 @@ fn emit_aarch64_cast_eval_arg( PhpType::AssocArray { .. } => { emit_aarch64_cast_eval_array_arg(emitter, 5, fail_label); } + PhpType::Iterable => { + emit_aarch64_cast_eval_iterable_arg(module, emitter, label_prefix, fail_label); + } _ => {} } } @@ -1183,12 +1192,68 @@ fn emit_aarch64_cast_eval_array_arg(emitter: &mut Emitter, expected_tag: i64, fa emitter.instruction("mov x0, x1"); // place the unboxed array pointer in the result register } +/// Validates and unboxes one ARM64 iterable-typed eval argument for native method dispatch. +fn emit_aarch64_cast_eval_iterable_arg( + module: &Module, + emitter: &mut Emitter, + label_prefix: &str, + fail_label: &str, +) { + let payload_ok = format!("{}_iterable_payload", label_prefix); + let object_case = format!("{}_iterable_object", label_prefix); + let object_ok = format!("{}_iterable_object_ok", label_prefix); + let done = format!("{}_iterable_done", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for iterable unboxing + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete iterable payload tag and pointer + emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.eq {}", payload_ok)); // indexed arrays satisfy iterable parameters + emitter.instruction("cmp x0, #5"); // runtime tag 5 means associative array + emitter.instruction(&format!("b.eq {}", payload_ok)); // associative arrays satisfy iterable parameters + emitter.instruction("cmp x0, #6"); // runtime tag 6 means object + emitter.instruction(&format!("b.eq {}", object_case)); // object values need Traversable interface validation + emitter.instruction(&format!("b {}", fail_label)); // reject scalar values for iterable parameters + emitter.label(&payload_ok); + emitter.instruction("mov x0, x1"); // place the array payload pointer in the result register + emitter.instruction(&format!("b {}", done)); // skip object-specific interface validation + emitter.label(&object_case); + emit_aarch64_validate_iterable_object(module, emitter, &object_ok, fail_label); + emitter.label(&object_ok); + emitter.instruction("ldr x0, [sp], #16"); // restore the iterable object pointer as the result + emitter.label(&done); +} + +/// Validates the ARM64 object payload saved in `x1` against Traversable interfaces. +fn emit_aarch64_validate_iterable_object( + module: &Module, + emitter: &mut Emitter, + object_ok: &str, + fail_label: &str, +) { + let interface_ids = traversable_interface_ids(module); + if interface_ids.is_empty() { + emitter.instruction(&format!("b {}", fail_label)); // reject objects when no Traversable interface metadata exists + return; + } + emitter.instruction("str x1, [sp, #-16]!"); // preserve the object payload across Traversable checks + for interface_id in interface_ids { + emitter.instruction("ldr x0, [sp]"); // reload the object pointer as matcher argument 1 + abi::emit_load_int_immediate(emitter, "x1", interface_id as i64); + abi::emit_load_int_immediate(emitter, "x2", 1); + abi::emit_call_label(emitter, "__rt_exception_matches"); + emitter.instruction("cmp x0, #0"); // test whether the object implements this Traversable interface + emitter.instruction(&format!("b.ne {}", object_ok)); // matching Iterator metadata accepts the object + } + emitter.instruction("add sp, sp, #16"); // discard the rejected object payload + emitter.instruction(&format!("b {}", fail_label)); // reject non-Traversable objects for iterable parameters +} + /// Casts one boxed eval argument into x86_64 result registers for temporary staging. fn emit_x86_64_cast_eval_arg( module: &Module, emitter: &mut Emitter, data: &mut DataSection, param_ty: &PhpType, + label_prefix: &str, fail_label: &str, ) { match param_ty.codegen_repr() { @@ -1220,6 +1285,9 @@ fn emit_x86_64_cast_eval_arg( PhpType::AssocArray { .. } => { emit_x86_64_cast_eval_array_arg(emitter, 5, fail_label); } + PhpType::Iterable => { + emit_x86_64_cast_eval_iterable_arg(module, emitter, label_prefix, fail_label); + } _ => {} } } @@ -1258,6 +1326,61 @@ fn emit_x86_64_cast_eval_array_arg(emitter: &mut Emitter, expected_tag: i64, fai emitter.instruction("mov rax, rdi"); // place the unboxed array pointer in the result register } +/// Validates and unboxes one x86_64 iterable-typed eval argument for native method dispatch. +fn emit_x86_64_cast_eval_iterable_arg( + module: &Module, + emitter: &mut Emitter, + label_prefix: &str, + fail_label: &str, +) { + let payload_ok = format!("{}_iterable_payload", label_prefix); + let object_case = format!("{}_iterable_object", label_prefix); + let object_ok = format!("{}_iterable_object_ok", label_prefix); + let done = format!("{}_iterable_done", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for iterable unboxing + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete iterable payload tag and pointer + emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("je {}", payload_ok)); // indexed arrays satisfy iterable parameters + emitter.instruction("cmp rax, 5"); // runtime tag 5 means associative array + emitter.instruction(&format!("je {}", payload_ok)); // associative arrays satisfy iterable parameters + emitter.instruction("cmp rax, 6"); // runtime tag 6 means object + emitter.instruction(&format!("je {}", object_case)); // object values need Traversable interface validation + emitter.instruction(&format!("jmp {}", fail_label)); // reject scalar values for iterable parameters + emitter.label(&payload_ok); + emitter.instruction("mov rax, rdi"); // place the array payload pointer in the result register + emitter.instruction(&format!("jmp {}", done)); // skip object-specific interface validation + emitter.label(&object_case); + emit_x86_64_validate_iterable_object(module, emitter, &object_ok, fail_label); + emitter.label(&object_ok); + abi::emit_pop_reg(emitter, "rax"); + emitter.label(&done); +} + +/// Validates the x86_64 object payload saved in `rdi` against Traversable interfaces. +fn emit_x86_64_validate_iterable_object( + module: &Module, + emitter: &mut Emitter, + object_ok: &str, + fail_label: &str, +) { + let interface_ids = traversable_interface_ids(module); + if interface_ids.is_empty() { + emitter.instruction(&format!("jmp {}", fail_label)); // reject objects when no Traversable interface metadata exists + return; + } + abi::emit_push_reg(emitter, "rdi"); + for interface_id in interface_ids { + emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the object pointer as matcher argument 1 + abi::emit_load_int_immediate(emitter, "rsi", interface_id as i64); + abi::emit_load_int_immediate(emitter, "rdx", 1); + abi::emit_call_label(emitter, "__rt_exception_matches"); + emitter.instruction("test rax, rax"); // test whether the object implements this Traversable interface + emitter.instruction(&format!("jne {}", object_ok)); // matching Iterator metadata accepts the object + } + abi::emit_pop_reg(emitter, "r10"); + emitter.instruction(&format!("jmp {}", fail_label)); // reject non-Traversable objects for iterable parameters +} + /// Boxes the current native method result as the Mixed cell expected by eval. fn emit_box_method_result(module: &Module, emitter: &mut Emitter, return_ty: &PhpType) { if return_ty.codegen_repr() == PhpType::Void { @@ -1268,6 +1391,14 @@ fn emit_box_method_result(module: &Module, emitter: &mut Emitter, return_ty: &Ph } } +/// Returns runtime interface ids for object values accepted by PHP iterable parameters. +fn traversable_interface_ids(module: &Module) -> Vec { + ["Iterator", "IteratorAggregate"] + .into_iter() + .filter_map(|name| module.interface_infos.get(name).map(|info| info.interface_id)) + .collect() +} + /// Groups method slots by class id while preserving sorted class order. fn grouped_slots(slots: &[EvalMethodSlot]) -> BTreeMap> { let mut grouped = BTreeMap::new(); diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index ea57a56051..c622611faf 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -989,6 +989,7 @@ fn eval_native_method_param_supported(ty: &PhpType) -> bool { | PhpType::Float | PhpType::Str | PhpType::Mixed + | PhpType::Iterable | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) @@ -1004,6 +1005,7 @@ fn eval_native_constructor_param_supported(ty: &PhpType) -> bool { | PhpType::Float | PhpType::Str | PhpType::Mixed + | PhpType::Iterable | PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4dd07cdf23..d68ba27140 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4649,6 +4649,53 @@ echo (new EvalMethodArrayArgBox())->run(); assert_eq!(out, "3:2"); } +/// Verifies eval fragments can pass iterable arguments to AOT methods and constructors. +#[test] +fn test_eval_fragment_dispatches_aot_iterable_parameters() { + let out = compile_and_run( + r#"i = 0; } + public function valid(): bool { return $this->i < 2; } + public function current(): mixed { return "I" . $this->i; } + public function key(): mixed { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } +} + +class EvalAotIterableParamBox { + public string $label; + + public function __construct(iterable $items) { + $this->label = self::join($items); + } + + public function describe(iterable $items): string { + return self::join($items); + } + + public static function describeStatic(iterable $items): string { + return self::join($items); + } + + private static function join(iterable $items): string { + $out = ""; + foreach ($items as $item) { + $out .= $item; + } + return $out; + } +} + +echo eval('$box = new EvalAotIterableParamBox(["C", "D"]); +$fromIterator = new EvalAotIterableParamBox(new EvalAotIterableParamIterator()); +return $box->describe(["A", "B"]) . ":" . EvalAotIterableParamBox::describeStatic(new EvalAotIterableParamIterator()) . ":" . $box->label . ":" . $fromIterator->label;'); +"#, + ); + assert_eq!(out, "AB:I0I1:CD:I0I1"); +} + /// Verifies eval fragments can read iterable return values from AOT methods. #[test] fn test_eval_fragment_dispatches_aot_method_with_iterable_return() { From 23d373a9cb8b615affbbdbbf6fa1586b515a8e77 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 17:02:52 +0200 Subject: [PATCH 0593/1208] Support eval AOT nullable int params --- docs/php/eval.md | 12 +++---- src/codegen/eval_constructor_helpers.rs | 43 +++++++++++++++++++++++++ src/codegen/eval_method_helpers.rs | 43 +++++++++++++++++++++++++ src/codegen/lower_inst/builtins/eval.rs | 2 ++ tests/codegen/eval.rs | 33 +++++++++++++++++++ 5 files changed, 127 insertions(+), 6 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index af99037621..45bb61d0c5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/iterable/object constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/Mixed/array/iterable/object parameters; scalar/Mixed/array/iterable/object return values are boxed back to eval. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameters; scalar/Mixed/array/iterable/object return values are boxed back to eval. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -128,7 +128,7 @@ as callable. Static method callables can use `["ClassName", "method"]` or `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method fallback supports the same named-argument and positional variadic-tail binding for public -non-by-reference scalar/Mixed/array/iterable/object parameter signatures supported by the +non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter signatures supported by the generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -512,7 +512,7 @@ constants. Direct `new EnumName()` and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native -methods are bridged to eval. Public fixed scalar/Mixed/array/iterable/object method +methods are bridged to eval. Public fixed scalar/nullable-int/Mixed/array/iterable/object method parameters through `$this->method(...)` are supported by the native method bridge, including registered named arguments and string-keyed unpacking; method returns may be scalar/Mixed/array/iterable/object values. @@ -637,7 +637,7 @@ The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval remain limited to generated public -non-by-reference scalar/Mixed/array/iterable/object parameter bridge signatures, including the +non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter bridge signatures, including the generated positional variadic array slot when present. By-reference parameters and broader parameter/return ABI shapes are still outside those bridge paths. @@ -650,7 +650,7 @@ parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional `new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference scalar/Mixed/array/iterable/object parameter slice plus scalar/Mixed/array/iterable/object returns and visibility-checked +non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/Mixed/array/iterable/object returns and visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT method/property attributes are exposed for registered metadata slices, while diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index ca59e80272..75c9792515 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -179,6 +179,7 @@ fn constructor_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Iterable | PhpType::Array(_) @@ -670,6 +671,9 @@ fn emit_aarch64_cast_eval_arg( emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 } + PhpType::TaggedScalar => { + emit_aarch64_cast_eval_tagged_scalar_arg(emitter, label_prefix); + } PhpType::Mixed => { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for a Mixed constructor parameter } @@ -694,6 +698,25 @@ fn emit_aarch64_cast_eval_arg( } } +/// Coerces one ARM64 eval argument into the inline nullable-int tagged-scalar ABI pair. +fn emit_aarch64_cast_eval_tagged_scalar_arg(emitter: &mut Emitter, label_prefix: &str) { + let null_label = format!("{}_tagged_scalar_null", label_prefix); + let done_label = format!("{}_tagged_scalar_done", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for nullable-int inspection + emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed eval argument across tag inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete eval argument tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the nullable-int argument is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null eval arguments + emitter.instruction("ldr x0, [sp]"); // reload the boxed eval argument for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the non-null eval argument to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("b {}", done_label)); // skip the null materialization path after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + emitter.instruction("add sp, sp, #16"); // discard the preserved boxed eval argument +} + /// Validates and unboxes one ARM64 array-typed eval argument for native constructors. fn emit_aarch64_cast_eval_array_arg( emitter: &mut Emitter, @@ -792,6 +815,9 @@ fn emit_x86_64_cast_eval_arg( emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair } + PhpType::TaggedScalar => { + emit_x86_64_cast_eval_tagged_scalar_arg(emitter, label_prefix); + } PhpType::Mixed => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for a Mixed constructor parameter } @@ -816,6 +842,23 @@ fn emit_x86_64_cast_eval_arg( } } +/// Coerces one x86_64 eval argument into the inline nullable-int tagged-scalar ABI pair. +fn emit_x86_64_cast_eval_tagged_scalar_arg(emitter: &mut Emitter, label_prefix: &str) { + let null_label = format!("{}_tagged_scalar_null", label_prefix); + let done_label = format!("{}_tagged_scalar_done", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete eval argument tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the nullable-int argument is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null eval arguments + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce the non-null eval argument to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // skip the null materialization path after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); +} + /// Validates and unboxes one x86_64 array-typed eval argument for native constructors. fn emit_x86_64_cast_eval_array_arg( emitter: &mut Emitter, diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 604a13c704..1c8911f864 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -257,6 +257,7 @@ fn method_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Iterable | PhpType::Array(_) @@ -1140,6 +1141,9 @@ fn emit_aarch64_cast_eval_arg( emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 } + PhpType::TaggedScalar => { + emit_aarch64_cast_eval_tagged_scalar_arg(emitter, label_prefix); + } PhpType::Mixed => { emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for a Mixed method parameter } @@ -1159,6 +1163,25 @@ fn emit_aarch64_cast_eval_arg( } } +/// Coerces one ARM64 eval argument into the inline nullable-int tagged-scalar ABI pair. +fn emit_aarch64_cast_eval_tagged_scalar_arg(emitter: &mut Emitter, label_prefix: &str) { + let null_label = format!("{}_tagged_scalar_null", label_prefix); + let done_label = format!("{}_tagged_scalar_done", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for nullable-int inspection + emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed eval argument across tag inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the concrete eval argument tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the nullable-int argument is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null eval arguments + emitter.instruction("ldr x0, [sp]"); // reload the boxed eval argument for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce the non-null eval argument to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("b {}", done_label)); // skip the null materialization path after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + emitter.instruction("add sp, sp, #16"); // discard the preserved boxed eval argument +} + /// Validates and unboxes one ARM64 object-typed eval argument for native method dispatch. fn emit_aarch64_cast_eval_object_arg( module: &Module, @@ -1273,6 +1296,9 @@ fn emit_x86_64_cast_eval_arg( emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair } + PhpType::TaggedScalar => { + emit_x86_64_cast_eval_tagged_scalar_arg(emitter, label_prefix); + } PhpType::Mixed => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for a Mixed method parameter } @@ -1292,6 +1318,23 @@ fn emit_x86_64_cast_eval_arg( } } +/// Coerces one x86_64 eval argument into the inline nullable-int tagged-scalar ABI pair. +fn emit_x86_64_cast_eval_tagged_scalar_arg(emitter: &mut Emitter, label_prefix: &str) { + let null_label = format!("{}_tagged_scalar_null", label_prefix); + let done_label = format!("{}_tagged_scalar_done", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the concrete eval argument tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the nullable-int argument is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null eval arguments + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce the non-null eval argument to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // skip the null materialization path after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); +} + /// Validates and unboxes one x86_64 object-typed eval argument for native method dispatch. fn emit_x86_64_cast_eval_object_arg( module: &Module, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index c622611faf..ba519e7081 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -988,6 +988,7 @@ fn eval_native_method_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Iterable | PhpType::Array(_) @@ -1004,6 +1005,7 @@ fn eval_native_constructor_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Iterable | PhpType::Array(_) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d68ba27140..63edf5c8b6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4696,6 +4696,39 @@ return $box->describe(["A", "B"]) . ":" . EvalAotIterableParamBox::describeStati assert_eq!(out, "AB:I0I1:CD:I0I1"); } +/// Verifies eval fragments can pass nullable-int arguments to AOT methods and constructors. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_int_parameters() { + let out = compile_and_run( + r#"label = self::format($count); + } + + public function describe(?int $count): string { + return self::format($count); + } + + public static function describeStatic(?int $count = null): string { + return self::format($count); + } + + private static function format(?int $count): string { + return $count === null ? "N" : "I" . $count; + } +} + +echo eval('$defaulted = new EvalAotNullableIntParamBox(); +$fromInt = new EvalAotNullableIntParamBox(7); +return $defaulted->label . ":" . $fromInt->label . ":" . $fromInt->describe(null) . ":" . $fromInt->describe("42") . ":" . EvalAotNullableIntParamBox::describeStatic() . ":" . EvalAotNullableIntParamBox::describeStatic(5);'); +"#, + ); + assert_eq!(out, "N:I7:N:I42:N:I5"); +} + /// Verifies eval fragments can read iterable return values from AOT methods. #[test] fn test_eval_fragment_dispatches_aot_method_with_iterable_return() { From 6678f87da45f72d2a162a3e818a8515a3fd12382 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 17:06:21 +0200 Subject: [PATCH 0594/1208] Support eval AOT nullable int returns --- docs/php/eval.md | 6 +++--- src/codegen/eval_method_helpers.rs | 1 + src/codegen/lower_inst/builtins/eval.rs | 1 + tests/codegen/eval.rs | 25 +++++++++++++++++++++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 45bb61d0c5..35ce26c87e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -76,7 +76,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameters; scalar/Mixed/array/iterable/object return values are boxed back to eval. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameters; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -515,7 +515,7 @@ Public declared property reads/writes through `$this->property` from native methods are bridged to eval. Public fixed scalar/nullable-int/Mixed/array/iterable/object method parameters through `$this->method(...)` are supported by the native method bridge, including registered named arguments and string-keyed unpacking; method -returns may be scalar/Mixed/array/iterable/object values. +returns may be scalar/nullable-int/Mixed/array/iterable/object values. ## Namespaces and constants @@ -650,7 +650,7 @@ parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional `new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/Mixed/array/iterable/object returns and visibility-checked +non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and visibility-checked `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT method/property attributes are exposed for registered metadata slices, while diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 1c8911f864..f3c533f48e 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -275,6 +275,7 @@ fn method_return_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Union(_) | PhpType::Iterable diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index ba519e7081..df85fd155d 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -1023,6 +1023,7 @@ fn eval_native_method_return_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Union(_) | PhpType::Iterable diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 63edf5c8b6..f2470d9981 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4729,6 +4729,31 @@ return $defaulted->label . ":" . $fromInt->label . ":" . $fromInt->describe(null assert_eq!(out, "N:I7:N:I42:N:I5"); } +/// Verifies eval fragments can read nullable-int return values from AOT methods. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_int_returns() { + let out = compile_and_run( + r#"maybe(true) === 7 ? "I7" : "bad") . ":" . (is_null($this->maybe(false)) ? "N" : "bad") . ":" . (EvalAotNullableIntReturnBox::maybeStatic(true) === 11 ? "S11" : "bad") . ":" . (is_null(EvalAotNullableIntReturnBox::maybeStatic(false)) ? "SN" : "bad");'); + } +} + +echo (new EvalAotNullableIntReturnBox())->run(); +"#, + ); + assert_eq!(out, "I7:N:S11:SN"); +} + /// Verifies eval fragments can read iterable return values from AOT methods. #[test] fn test_eval_fragment_dispatches_aot_method_with_iterable_return() { From e4fc6611a35b04d1dfeeb87b615a9b4b5bbfa119 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 17:22:55 +0200 Subject: [PATCH 0595/1208] Support eval AOT nullable int properties --- docs/php/eval.md | 2 +- src/codegen/eval_property_helpers.rs | 67 ++++++++++++++++++++- src/codegen/lower_inst/objects.rs | 88 +++++++++++++++++++++++++--- tests/codegen/eval.rs | 26 ++++++++ 4 files changed, 172 insertions(+), 11 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 35ce26c87e..4d403e8478 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index e5f75387c4..9106a86e16 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -13,7 +13,7 @@ use std::collections::BTreeMap; -use crate::codegen::abi; +use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; @@ -127,6 +127,7 @@ fn property_type_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Union(_) | PhpType::Object(_) @@ -545,6 +546,11 @@ fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot emitter.instruction("mov x0, #1"); // runtime tag 1 = string emitter.instruction("bl __rt_mixed_from_value"); // persist and box the string property payload } + PhpType::TaggedScalar => { + emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the nullable integer property payload + emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset + 8)); //load the nullable integer property tag + emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); + } PhpType::Mixed | PhpType::Union(_) => { let null_label = format!("{}_mixed_null", label_fragment(&slot_body_label_raw(slot, "get"))); let done_label = format!("{}_mixed_done", label_fragment(&slot_body_label_raw(slot, "get"))); @@ -587,6 +593,11 @@ fn emit_x86_64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) emitter.instruction("mov eax, 1"); // runtime tag 1 = string emitter.instruction("call __rt_mixed_from_value"); // persist and box the string property payload } + PhpType::TaggedScalar => { + emitter.instruction(&format!("mov rax, QWORD PTR [r11 + {}]", slot.offset)); //load the nullable integer property payload + emitter.instruction(&format!("mov rdx, QWORD PTR [r11 + {}]", slot.offset + 8)); //load the nullable integer property tag + emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); + } PhpType::Mixed | PhpType::Union(_) => { let null_label = format!("{}_mixed_null_x", label_fragment(&slot_body_label_raw(slot, "get"))); let done_label = format!("{}_mixed_done_x", label_fragment(&slot_body_label_raw(slot, "get"))); @@ -625,6 +636,7 @@ fn emit_aarch64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySl emitter.instruction(&format!("str x1, [x9, #{}]", slot.offset)); // store the coerced string pointer into the property slot emitter.instruction(&format!("str x2, [x9, #{}]", slot.offset + 8)); // store the coerced string length into the property slot } + PhpType::TaggedScalar => emit_aarch64_store_tagged_scalar_property(emitter, slot), PhpType::Mixed | PhpType::Union(_) => { emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value being assigned emitter.instruction("bl __rt_incref"); // retain the Mixed cell for property ownership @@ -654,6 +666,7 @@ fn emit_x86_64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlo emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); // store the coerced string pointer into the property slot emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rdx", slot.offset + 8)); // store the coerced string length into the property slot } + PhpType::TaggedScalar => emit_x86_64_store_tagged_scalar_property(emitter, slot), PhpType::Mixed | PhpType::Union(_) => { emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value being assigned emitter.instruction("call __rt_incref"); // retain the Mixed cell for property ownership @@ -678,6 +691,32 @@ fn emit_aarch64_store_cast_scalar( emitter.instruction(&format!("str {}, [x9, #{}]", result_reg, slot.offset)); // store the coerced scalar into the property slot } +/// Stores a boxed eval value into an ARM64 nullable-int tagged-scalar property slot. +fn emit_aarch64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalPropertySlot) { + let null_label = format!( + "{}_tagged_scalar_null", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + let done_label = format!( + "{}_tagged_scalar_done", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null property writes + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("b {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the nullable integer payload into the property slot + emitter.instruction(&format!("str x1, [x9, #{}]", slot.offset + 8)); // store the nullable integer tag into the property slot +} + /// Emits an x86_64 scalar property store after Mixed coercion. fn emit_x86_64_store_cast_scalar( emitter: &mut Emitter, @@ -691,6 +730,32 @@ fn emit_x86_64_store_cast_scalar( emitter.instruction(&format!("mov QWORD PTR [r11 + {}], {}", slot.offset, result_reg)); // store the coerced scalar into the property slot } +/// Stores a boxed eval value into an x86_64 nullable-int tagged-scalar property slot. +fn emit_x86_64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalPropertySlot) { + let null_label = format!( + "{}_tagged_scalar_null_x", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + let done_label = format!( + "{}_tagged_scalar_done_x", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null property writes + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); //store the nullable integer payload into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rdx", slot.offset + 8)); //store the nullable integer tag into the property slot +} + /// Groups property slots by class id while preserving sorted class order. fn grouped_slots(slots: &[EvalPropertySlot]) -> BTreeMap> { let mut grouped = BTreeMap::new(); diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index f390cbb2a7..7cf32e6d7d 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -30,8 +30,9 @@ use crate::types::{ClassInfo, InterfaceInfo, PhpType}; use super::super::context::FunctionContext; use super::{ callables, cast_loaded_mixed_pointer_to_result, direct_call_stack_pad_bytes, expect_data, - emit_loaded_indexed_array_to_mixed, emit_mixed_string_for_persistent_store, - emit_ref_arg_writebacks, expect_operand, iterators, load_value_to_first_int_arg, + coerce_loaded_value_to_tagged_scalar, emit_loaded_indexed_array_to_mixed, + emit_mixed_string_for_persistent_store, emit_ref_arg_writebacks, expect_operand, iterators, + load_value_to_first_int_arg, materialize_direct_call_args_with_refs, materialize_method_call_args_with_receiver_reg_and_refs, resolve_method_call_target, store_if_result, store_method_call_result, @@ -4781,15 +4782,16 @@ fn resolve_packed_field_slot( /// Verifies that this slice knows how to represent the property type in an object slot. fn ensure_property_type_supported(php_type: &PhpType, inst: &Instruction) -> Result<()> { - match php_type { + match php_type.codegen_repr() { PhpType::Bool | PhpType::False | PhpType::Int | PhpType::Float | PhpType::Str + | PhpType::TaggedScalar | PhpType::Void | PhpType::Never => Ok(()), - ty if is_pointer_sized_property_type(ty) => Ok(()), + ref ty if is_pointer_sized_property_type(ty) => Ok(()), _ => Err(CodegenIrError::unsupported(format!( "{} for property PHP type {:?}", inst.op.name(), @@ -4823,6 +4825,9 @@ fn ensure_property_value_supported( if can_convert_indexed_array_to_mixed_property(value_ty, &slot.php_type) { return Ok(()); } + if can_store_value_as_tagged_scalar_property(value_ty, &slot.php_type) { + return Ok(()); + } if can_coerce_tagged_scalar_to_int_property(value_ty, &slot.php_type) { return Ok(()); } @@ -4975,6 +4980,24 @@ fn can_coerce_mixed_to_scalar_property(value_ty: &PhpType, slot_ty: &PhpType) -> ) } +/// Returns true when a value can materialize nullable-int tagged-scalar property storage. +fn can_store_value_as_tagged_scalar_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { + if slot_ty.codegen_repr() != PhpType::TaggedScalar { + return false; + } + matches!( + value_ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Callable + | PhpType::Void + | PhpType::Never + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Union(_) + ) +} + /// Returns true when a nullable inline scalar can be narrowed into int property storage. fn can_coerce_tagged_scalar_to_int_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { value_ty.codegen_repr() == PhpType::TaggedScalar && slot_ty.codegen_repr() == PhpType::Int @@ -5052,7 +5075,7 @@ fn emit_property_load( if slot.is_reference { return emit_reference_property_load(ctx, slot, base_reg); } - match &slot.php_type { + match slot.php_type.codegen_repr() { PhpType::Str => { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); if base_reg == ptr_reg { @@ -5071,7 +5094,13 @@ fn emit_property_load( let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); } - ty if is_pointer_sized_property_type(ty) => { + PhpType::TaggedScalar => { + let int_reg = abi::int_result_reg(ctx.emitter); + let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); + abi::emit_load_from_address(ctx.emitter, tag_reg, base_reg, slot.offset + 8); + } + ty if is_pointer_sized_property_type(&ty) => { let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, int_reg, base_reg, slot.offset); } @@ -5103,6 +5132,12 @@ fn emit_reference_property_load( let float_reg = abi::float_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, float_reg, pointer_reg, 0); } + PhpType::TaggedScalar => { + let int_reg = abi::int_result_reg(ctx.emitter); + let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, int_reg, pointer_reg, 0); + abi::emit_load_from_address(ctx.emitter, tag_reg, pointer_reg, 8); + } ty if is_pointer_sized_property_type(&ty) || matches!( ty, @@ -5128,7 +5163,7 @@ fn emit_packed_field_load( slot: &PropertySlot, base_reg: &str, ) -> Result<()> { - match &slot.php_type { + match slot.php_type.codegen_repr() { PhpType::Float => { let float_reg = abi::float_result_reg(ctx.emitter); abi::emit_load_from_address(ctx.emitter, float_reg, base_reg, slot.offset); @@ -5195,7 +5230,7 @@ fn emit_property_store( abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset + 8); return Ok(()); } - match &slot.php_type { + match slot.php_type.codegen_repr() { PhpType::Str => { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); abi::emit_push_reg(ctx.emitter, base_reg); @@ -5227,7 +5262,16 @@ fn emit_property_store( abi::emit_store_to_address(ctx.emitter, int_reg, base_reg, slot.offset); abi::emit_store_zero_to_address(ctx.emitter, base_reg, slot.offset + 8); } - ty if is_pointer_sized_property_type(ty) => { + PhpType::TaggedScalar => { + let int_reg = abi::int_result_reg(ctx.emitter); + let tag_reg = crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, base_reg); + load_property_store_value_to_result(ctx, value, &slot.php_type)?; + abi::emit_pop_reg(ctx.emitter, base_reg); + abi::emit_store_to_address(ctx.emitter, int_reg, base_reg, slot.offset); + abi::emit_store_to_address(ctx.emitter, tag_reg, base_reg, slot.offset + 8); + } + ty if is_pointer_sized_property_type(&ty) => { let int_reg = abi::int_result_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, base_reg); load_property_store_value_to_result(ctx, value, &slot.php_type)?; @@ -5416,6 +5460,20 @@ fn store_current_result_to_reference_cell( 0, ); } + PhpType::TaggedScalar => { + abi::emit_store_to_address( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + pointer_reg, + 0, + ); + abi::emit_store_to_address( + ctx.emitter, + crate::codegen::sentinels::tagged_scalar_tag_reg(ctx.emitter), + pointer_reg, + 8, + ); + } ty if is_pointer_sized_property_type(&ty) || matches!( ty, @@ -5533,6 +5591,18 @@ fn load_property_store_value_to_result( abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Array(Box::new(PhpType::Mixed))); return Ok(()); } + if can_store_value_as_tagged_scalar_property(&value_ty, slot_ty) { + match value_ty.codegen_repr() { + PhpType::Void | PhpType::Never => { + crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); + } + _ => { + ctx.load_value_to_result(value)?; + coerce_loaded_value_to_tagged_scalar(ctx, &value_ty)?; + } + } + return Ok(()); + } if can_coerce_tagged_scalar_to_int_property(&value_ty, slot_ty) { ctx.load_value_to_result(value)?; crate::codegen::sentinels::emit_tagged_scalar_to_int_null_as_zero(ctx.emitter); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f2470d9981..d2d2108701 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4430,6 +4430,32 @@ echo $box->x; assert_eq!(out, "2"); } +/// Verifies eval fragments can read and write public nullable-int AOT properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_nullable_int_property() { + let out = compile_and_run( + r#"count === null) ? "N" : "n"; + $this->count = 7; + $out = $out . ":" . (($this->count === 7) ? "I7" : "bad"); + $this->count = "42"; + $out = $out . ":" . (($this->count === 42) ? "I42" : "bad"); + $this->count = null; + return $out . ":" . (($this->count === null) ? "N" : "bad");'); + } +} + +$box = new EvalNullableIntPropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "N:I7:I42:N"); +} + /// Verifies eval keeps PHP property names case-sensitive while parsing keywords case-insensitively. #[test] fn test_eval_fragment_preserves_this_property_case() { From 82c8645e9002ddafe8b8737f1d9e60c1bfbeaf97 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 17:49:24 +0200 Subject: [PATCH 0596/1208] Support eval AOT static properties --- .../src/interpreter/runtime_ops.rs | 15 + .../src/interpreter/statements.rs | 67 +- .../interpreter/tests/support/runtime_ops.rs | 17 + .../src/runtime_hooks/externs.rs | 13 + .../elephc-magician/src/runtime_hooks/ops.rs | 40 + docs/php/eval.md | 2 +- src/codegen/eval_static_property_helpers.rs | 784 ++++++++++++++++++ src/codegen/mod.rs | 2 + tests/codegen/eval.rs | 21 + 9 files changed, 938 insertions(+), 23 deletions(-) create mode 100644 src/codegen/eval_static_property_helpers.rs diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index d26659ec38..0922d76d62 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -79,6 +79,21 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result<(), EvalStatus>; + /// Reads a public generated/AOT static property through the generated bridge. + fn static_property_get( + &mut self, + class_name: &str, + property: &str, + ) -> Result, EvalStatus>; + + /// Writes a public generated/AOT static property through the generated bridge. + fn static_property_set( + &mut self, + class_name: &str, + property: &str, + value: RuntimeCellHandle, + ) -> Result; + /// Creates a shallow clone of a runtime object held in a boxed Mixed cell. fn object_clone_shallow( &mut self, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 45406219c9..5ff2470198 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2565,18 +2565,23 @@ pub(in crate::interpreter) fn eval_static_property_get_result( class_name: &str, property_name: &str, context: &mut ElephcEvalContext, - _values: &mut impl RuntimeValueOps, + values: &mut impl RuntimeValueOps, ) -> Result { - let class_name = resolve_eval_static_class_name(class_name, context)?; - let (declaring_class, property) = context - .class_property(&class_name, property_name) - .ok_or(EvalStatus::RuntimeFatal)?; - if !property.is_static() { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + return context + .static_property(&declaring_class, property.name()) + .ok_or(EvalStatus::RuntimeFatal); + } + if eval_static_member_context_owns_class(&class_name, context) { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, property.visibility(), context)?; - context - .static_property(&declaring_class, property.name()) + values + .static_property_get(&class_name, property_name)? .ok_or(EvalStatus::RuntimeFatal) } @@ -2724,19 +2729,26 @@ pub(in crate::interpreter) fn eval_static_property_set_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { - let class_name = resolve_eval_static_class_name(class_name, context)?; - let (declaring_class, property) = context - .class_property(&class_name, property_name) - .ok_or(EvalStatus::RuntimeFatal)?; - if !property.is_static() { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_property_write_access(&declaring_class, &property, context)?; + validate_eval_readonly_property_write(&declaring_class, &property, context)?; + if let Some(replaced) = context.set_static_property(&declaring_class, property.name(), value) { + values.release(replaced)?; + } + return Ok(()); + } + if eval_static_member_context_owns_class(&class_name, context) { return Err(EvalStatus::RuntimeFatal); } - validate_eval_property_write_access(&declaring_class, &property, context)?; - validate_eval_readonly_property_write(&declaring_class, &property, context)?; - if let Some(replaced) = context.set_static_property(&declaring_class, property.name(), value) { - values.release(replaced)?; + if values.static_property_set(&class_name, property_name, value)? { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) } - Ok(()) } /// Dispatches a static method call to an eval-declared static method. @@ -2747,7 +2759,7 @@ pub(in crate::interpreter) fn eval_static_method_call_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let class_name = resolve_eval_static_method_class_name(class_name, context)?; + let class_name = resolve_eval_static_member_class_name(class_name, context)?; if let Some(result) = eval_builtin_property_hook_type_static_method_result( &class_name, method_name, @@ -3096,8 +3108,8 @@ pub(in crate::interpreter) fn resolve_eval_static_class_name( } } -/// Resolves static method receivers while allowing non-eval class names to reach AOT lookup. -fn resolve_eval_static_method_class_name( +/// Resolves static member receivers while allowing non-eval class names to reach AOT lookup. +fn resolve_eval_static_member_class_name( class_name: &str, context: &ElephcEvalContext, ) -> Result { @@ -3109,6 +3121,17 @@ fn resolve_eval_static_method_class_name( } } +/// Returns true when an eval-declared class-like symbol should not fall through to AOT lookup. +fn eval_static_member_context_owns_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context.has_class(class_name) + || context.has_interface(class_name) + || context.has_trait(class_name) + || context.has_enum(class_name) +} + /// Resolves `self`, `parent`, `static`, and named class-like receivers for constant access. fn resolve_eval_static_class_like_name( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index d15472fcc8..54eca01c3c 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -83,6 +83,23 @@ impl RuntimeValueOps for FakeOps { ) -> Result<(), EvalStatus> { self.runtime_property_set(object, property, value) } + /// Reports no fake AOT static property match. + fn static_property_get( + &mut self, + _class_name: &str, + _property: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports a failed fake AOT static property write. + fn static_property_set( + &mut self, + _class_name: &str, + _property: &str, + _value: RuntimeCellHandle, + ) -> Result { + Ok(false) + } /// Creates one shallow fake object clone. fn object_clone_shallow( &mut self, diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 73d85d3ecf..b14d130b71 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -51,6 +51,19 @@ unsafe extern "C" { name_len: u64, value: *mut RuntimeCell, ) -> u64; + pub(super) fn __elephc_eval_value_static_property_get( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_static_property_set( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + value: *mut RuntimeCell, + ) -> u64; /// Returns a boxed shallow clone for stdClass/eval object storage. pub(super) fn __elephc_eval_value_object_clone_shallow( object: *mut RuntimeCell, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 4ae55e7f64..3321646cec 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -125,6 +125,46 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } + /// Reads a public AOT static property through the generated user helper. + fn static_property_get( + &mut self, + class_name: &str, + property: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_value_static_property_get( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + + /// Writes a public AOT static property through the generated user helper. + fn static_property_set( + &mut self, + class_name: &str, + property: &str, + value: RuntimeCellHandle, + ) -> Result { + let ok = unsafe { + __elephc_eval_value_static_property_set( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + value.as_ptr(), + ) + }; + Ok(ok != 0) + } + /// Creates a shallow clone of a boxed Mixed stdClass/eval object through the generated wrapper. fn object_clone_shallow( &mut self, diff --git a/docs/php/eval.md b/docs/php/eval.md index 4d403e8478..229f5b4e9a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, static property access, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public scalar/string/Mixed/nullable-int AOT static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | diff --git a/src/codegen/eval_static_property_helpers.rs b/src/codegen/eval_static_property_helpers.rs new file mode 100644 index 0000000000..6b76334f88 --- /dev/null +++ b/src/codegen/eval_static_property_helpers.rs @@ -0,0 +1,784 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician access public native +//! static properties through symbol-backed storage. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - The cacheable runtime object cannot know user static-property symbols, so +//! these C-ABI bridge symbols are emitted into the user assembly. +//! - Null helper returns mean "no bridge match"; boxed PHP null is returned as +//! a real Mixed cell pointer. + +use std::collections::BTreeMap; + +use crate::codegen::{abi, emit_box_current_value_as_mixed}; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::static_property_symbol; +use crate::parser::ast::Visibility; +use crate::types::{ClassInfo, PhpType}; + +/// Static property slot metadata needed by eval bridge dispatch. +#[derive(Clone)] +struct EvalStaticPropertySlot { + class_name: String, + property: String, + symbol: String, + ty: PhpType, + is_declared: bool, +} + +/// Emits eval static-property helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_static_property_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_static_property_slots(module); + emit_static_property_get_helper(module, emitter, data, &slots); + emit_static_property_set_helper(module, emitter, data, &slots); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Collects public static properties with storage layouts the bridge can access. +fn collect_eval_static_property_slots(module: &Module) -> Vec { + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + collect_class_static_property_slots(module, class_name, class_info, &mut slots); + } + slots +} + +/// Adds bridge-supported public static properties visible from one class. +fn collect_class_static_property_slots( + module: &Module, + class_name: &str, + class_info: &ClassInfo, + slots: &mut Vec, +) { + let mut properties = class_info.static_properties.iter().collect::>(); + properties.sort_by(|(left, _), (right, _)| left.cmp(right)); + for (property, ty) in properties { + if !static_property_is_public(class_info, property) || !static_property_type_supported(ty) { + continue; + } + let declaring_class = class_info + .static_property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name); + let Some(declaring_info) = module.class_infos.get(declaring_class) else { + continue; + }; + slots.push(EvalStaticPropertySlot { + class_name: class_name.to_string(), + property: property.clone(), + symbol: static_property_symbol(declaring_class, property), + ty: ty.codegen_repr(), + is_declared: declaring_info.declared_static_properties.contains(property), + }); + } +} + +/// Returns true when a static property is publicly visible to eval. +fn static_property_is_public(class_info: &ClassInfo, property: &str) -> bool { + class_info + .static_property_visibilities + .get(property) + .is_none_or(|visibility| matches!(visibility, Visibility::Public)) +} + +/// Returns true for static-property storage shapes the bridge can box and update. +fn static_property_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::TaggedScalar + | PhpType::Mixed + | PhpType::Union(_) + ) +} + +/// Emits `__elephc_eval_value_static_property_get(class, name) -> Mixed*`. +fn emit_static_property_get_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user static property get ---"); + label_c_global(module, emitter, "__elephc_eval_value_static_property_get"); + match module.target.arch { + Arch::AArch64 => emit_static_property_get_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_static_property_get_x86_64(module, emitter, data, slots), + } +} + +/// Emits `__elephc_eval_value_static_property_set(class, name, Mixed*) -> bool`. +fn emit_static_property_set_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user static property set ---"); + label_c_global(module, emitter, "__elephc_eval_value_static_property_set"); + match module.target.arch { + Arch::AArch64 => emit_static_property_set_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_static_property_set_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 static-property get helper body. +fn emit_static_property_get_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let done_label = "__elephc_eval_value_static_property_get_done"; + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class/property slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + emit_aarch64_static_property_dispatch(module, emitter, data, slots, "get"); + emitter.instruction("mov x0, xzr"); // report bridge miss with a null pointer + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emit_aarch64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed static property value to Rust +} + +/// Emits the x86_64 static-property get helper body. +fn emit_static_property_get_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let done_label = "__elephc_eval_value_static_property_get_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and property slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + emit_x86_64_static_property_dispatch(module, emitter, data, slots, "get"); + emitter.instruction("xor eax, eax"); // report bridge miss with a null pointer + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emit_x86_64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed static property value to Rust +} + +/// Emits the ARM64 static-property set helper body. +fn emit_static_property_set_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let fail_label = "__elephc_eval_value_static_property_set_fail"; + let done_label = "__elephc_eval_value_static_property_set_done"; + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for class/property, value, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + emitter.instruction("str x4, [sp, #32]"); // save the boxed value being assigned + emit_aarch64_static_property_dispatch(module, emitter, data, slots, "set"); + emitter.instruction(&format!("b {}", fail_label)); // no supported public static property matched the request + emit_aarch64_set_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("mov x0, #0"); // report a failed eval static-property write to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the write-status flag to Rust +} + +/// Emits the x86_64 static-property set helper body. +fn emit_static_property_set_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let fail_label = "__elephc_eval_value_static_property_set_fail_x"; + let done_label = "__elephc_eval_value_static_property_set_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, property, and value + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the boxed value being assigned + emit_x86_64_static_property_dispatch(module, emitter, data, slots, "set"); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported public static property matched the request + emit_x86_64_set_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report a failed eval static-property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after failure + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the write-status flag to Rust +} + +/// Emits ARM64 class-name and property-name dispatch for static property helpers. +fn emit_aarch64_static_property_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], + mode: &str, +) { + for (class_name, class_slots) in grouped_slots(slots) { + let next_label = format!( + "__elephc_eval_static_property_{}_next_{}", + mode, + label_fragment(class_name) + ); + emit_aarch64_static_class_name_compare(emitter, data, class_name, &next_label); + for slot in class_slots { + emit_aarch64_static_property_name_compare(module, emitter, data, slot, mode); + } + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-name and property-name dispatch for static property helpers. +fn emit_x86_64_static_property_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], + mode: &str, +) { + for (class_name, class_slots) in grouped_slots(slots) { + let next_label = format!( + "__elephc_eval_static_property_{}_next_{}_x", + mode, + label_fragment(class_name) + ); + emit_x86_64_static_class_name_compare(emitter, data, class_name, &next_label); + for slot in class_slots { + emit_x86_64_static_property_name_compare(module, emitter, data, slot, mode); + } + emitter.label(&next_label); + } +} + +/// Emits one ARM64 case-insensitive class-name comparison for a static property group. +fn emit_aarch64_static_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction(&format!("cbnz x0, {}", next_label)); // try the next class when names differ +} + +/// Emits one x86_64 case-insensitive class-name comparison for a static property group. +fn emit_x86_64_static_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the class names matched + emitter.instruction(&format!("jne {}", next_label)); // try the next class when names differ +} + +/// Emits one ARM64 property-name comparison and branch to the matching body. +fn emit_aarch64_static_property_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + mode: &str, +) { + let (label, len) = data.add_string(slot.property.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested property-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare property names with PHP case-sensitive rules + let target_label = slot_body_label(module, slot, mode); + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the static property body when names match +} + +/// Emits one x86_64 property-name comparison and branch to the matching body. +fn emit_x86_64_static_property_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + mode: &str, +) { + let (label, len) = data.add_string(slot.property.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested property-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare property names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the property names matched + let target_label = slot_body_label(module, slot, mode); + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the static property body when names match +} + +/// Emits ARM64 get bodies for every bridge-supported static property slot. +fn emit_aarch64_get_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "get")); + emit_aarch64_uninitialized_guard(emitter, slot, done_label); + emit_aarch64_box_static_property_slot(emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the static property value + } +} + +/// Emits x86_64 get bodies for every bridge-supported static property slot. +fn emit_x86_64_get_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "get")); + emit_x86_64_uninitialized_guard(emitter, slot, done_label); + emit_x86_64_box_static_property_slot(emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the static property value + } +} + +/// Emits ARM64 set bodies for every bridge-supported static property slot. +fn emit_aarch64_set_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "set")); + emit_aarch64_store_static_property_slot(emitter, slot); + emitter.instruction("mov x0, #1"); // report a successful eval static-property write to Rust + emitter.instruction(&format!("b {}", done_label)); // return after storing the static property value + } +} + +/// Emits x86_64 set bodies for every bridge-supported static property slot. +fn emit_x86_64_set_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "set")); + emit_x86_64_store_static_property_slot(emitter, slot); + emitter.instruction("mov rax, 1"); // report a successful eval static-property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // return after storing the static property value + } +} + +/// Emits an ARM64 uninitialized typed-static-property guard before boxing. +fn emit_aarch64_uninitialized_guard( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + done_label: &str, +) { + if !slot.is_declared { + return; + } + let initialized_label = format!( + "{}_initialized", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + abi::emit_load_symbol_to_reg(emitter, "x10", &slot.symbol, 8); + abi::emit_load_int_immediate(emitter, "x11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp x10, x11"); // check whether the typed static property is initialized + emitter.instruction(&format!("b.ne {}", initialized_label)); // continue boxing once the static slot is initialized + emitter.instruction("mov x0, xzr"); // report uninitialized static property as a bridge failure + emitter.instruction(&format!("b {}", done_label)); // return the failure to Rust without boxing storage + emitter.label(&initialized_label); +} + +/// Emits an x86_64 uninitialized typed-static-property guard before boxing. +fn emit_x86_64_uninitialized_guard( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + done_label: &str, +) { + if !slot.is_declared { + return; + } + let initialized_label = format!( + "{}_initialized_x", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + abi::emit_load_symbol_to_reg(emitter, "r10", &slot.symbol, 8); + abi::emit_load_int_immediate(emitter, "r11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp r10, r11"); // check whether the typed static property is initialized + emitter.instruction(&format!("jne {}", initialized_label)); // continue boxing once the static slot is initialized + emitter.instruction("xor eax, eax"); // report uninitialized static property as a bridge failure + emitter.instruction(&format!("jmp {}", done_label)); // return the failure to Rust without boxing storage + emitter.label(&initialized_label); +} + +/// Boxes an ARM64 static property symbol payload into a Mixed cell. +fn emit_aarch64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { + match slot.ty.codegen_repr() { + PhpType::Int | PhpType::Bool => { + abi::emit_load_symbol_to_reg(emitter, "x0", &slot.symbol, 0); + emit_box_current_value_as_mixed(emitter, &slot.ty); + } + PhpType::Float => { + abi::emit_load_symbol_to_reg(emitter, "d0", &slot.symbol, 0); + emit_box_current_value_as_mixed(emitter, &PhpType::Float); + } + PhpType::Str => { + abi::emit_load_symbol_to_reg(emitter, "x1", &slot.symbol, 0); + abi::emit_load_symbol_to_reg(emitter, "x2", &slot.symbol, 8); + emit_box_current_value_as_mixed(emitter, &PhpType::Str); + } + PhpType::TaggedScalar => { + abi::emit_load_symbol_to_reg(emitter, "x0", &slot.symbol, 0); + abi::emit_load_symbol_to_reg(emitter, "x1", &slot.symbol, 8); + emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); + } + PhpType::Mixed | PhpType::Union(_) => { + let null_label = format!( + "{}_mixed_null", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + let done_label = format!( + "{}_mixed_done", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + abi::emit_load_symbol_to_reg(emitter, "x0", &slot.symbol, 0); + emitter.instruction(&format!("cbz x0, {}", null_label)); // null static storage reads as PHP null + emitter.instruction("bl __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("b {}", done_label)); // skip null materialization after a retained hit + emitter.label(&null_label); + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(&done_label); + } + _ => { + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } + } +} + +/// Boxes an x86_64 static property symbol payload into a Mixed cell. +fn emit_x86_64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { + match slot.ty.codegen_repr() { + PhpType::Int | PhpType::Bool => { + abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); + emit_box_current_value_as_mixed(emitter, &slot.ty); + } + PhpType::Float => { + abi::emit_load_symbol_to_reg(emitter, "xmm0", &slot.symbol, 0); + emit_box_current_value_as_mixed(emitter, &PhpType::Float); + } + PhpType::Str => { + abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); + abi::emit_load_symbol_to_reg(emitter, "rdx", &slot.symbol, 8); + emit_box_current_value_as_mixed(emitter, &PhpType::Str); + } + PhpType::TaggedScalar => { + abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); + abi::emit_load_symbol_to_reg(emitter, "rdx", &slot.symbol, 8); + emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); + } + PhpType::Mixed | PhpType::Union(_) => { + let null_label = format!( + "{}_mixed_null_x", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + let done_label = format!( + "{}_mixed_done_x", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); + emitter.instruction("test rax, rax"); // check whether static storage holds a Mixed cell + emitter.instruction(&format!("jz {}", null_label)); // null static storage reads as PHP null + emitter.instruction("call __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("jmp {}", done_label)); // skip null materialization after a retained hit + emitter.label(&null_label); + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + emitter.label(&done_label); + } + _ => { + let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(emitter, &null_symbol); + } + } +} + +/// Stores a boxed Mixed eval value into an ARM64 static property symbol. +fn emit_aarch64_store_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { + match slot.ty.codegen_repr() { + PhpType::Int => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "x0"), + PhpType::Bool => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "x0"), + PhpType::Float => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval value to a PHP float + abi::emit_store_reg_to_symbol(emitter, "d0", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); + } + PhpType::Str => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for string coercion + emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval value to a PHP string pair + abi::emit_store_reg_to_symbol(emitter, "x1", &slot.symbol, 0); + abi::emit_store_reg_to_symbol(emitter, "x2", &slot.symbol, 8); + } + PhpType::TaggedScalar => emit_aarch64_store_tagged_scalar_static_property(emitter, slot), + PhpType::Mixed | PhpType::Union(_) => { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value being assigned + emitter.instruction("bl __rt_incref"); // retain the Mixed cell for static-property ownership + abi::emit_store_reg_to_symbol(emitter, "x0", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); + } + _ => {} + } +} + +/// Stores a boxed Mixed eval value into an x86_64 static property symbol. +fn emit_x86_64_store_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { + match slot.ty.codegen_repr() { + PhpType::Int => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "rax"), + PhpType::Bool => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "rax"), + PhpType::Float => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for float coercion + emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval value to a PHP float + abi::emit_store_reg_to_symbol(emitter, "xmm0", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); + } + PhpType::Str => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for string coercion + emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval value to a PHP string pair + abi::emit_store_reg_to_symbol(emitter, "rax", &slot.symbol, 0); + abi::emit_store_reg_to_symbol(emitter, "rdx", &slot.symbol, 8); + } + PhpType::TaggedScalar => emit_x86_64_store_tagged_scalar_static_property(emitter, slot), + PhpType::Mixed | PhpType::Union(_) => { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value being assigned + emitter.instruction("call __rt_incref"); // retain the Mixed cell for static-property ownership + abi::emit_store_reg_to_symbol(emitter, "rax", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); + } + _ => {} + } +} + +/// Emits an ARM64 scalar static-property store after Mixed coercion. +fn emit_aarch64_store_cast_scalar( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + helper: &str, + result_reg: &str, +) { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for scalar coercion + emitter.instruction(&format!("bl {}", helper)); // coerce the eval value to the declared static-property type + abi::emit_store_reg_to_symbol(emitter, result_reg, &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); +} + +/// Emits an x86_64 scalar static-property store after Mixed coercion. +fn emit_x86_64_store_cast_scalar( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + helper: &str, + result_reg: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for scalar coercion + emitter.instruction(&format!("call {}", helper)); // coerce the eval value to the declared static-property type + abi::emit_store_reg_to_symbol(emitter, result_reg, &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); +} + +/// Stores a boxed eval value into an ARM64 nullable-int static property symbol. +fn emit_aarch64_store_tagged_scalar_static_property( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + let null_label = format!( + "{}_tagged_scalar_null", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + let done_label = format!( + "{}_tagged_scalar_done", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null static-property writes + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("b {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + abi::emit_store_reg_to_symbol(emitter, "x0", &slot.symbol, 0); + abi::emit_store_reg_to_symbol(emitter, "x1", &slot.symbol, 8); +} + +/// Stores a boxed eval value into an x86_64 nullable-int static property symbol. +fn emit_x86_64_store_tagged_scalar_static_property( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + let null_label = format!( + "{}_tagged_scalar_null_x", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + let done_label = format!( + "{}_tagged_scalar_done_x", + label_fragment(&slot_body_label_raw(slot, "set")) + ); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null static-property writes + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); + emitter.instruction(&format!("jmp {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.label(&null_label); + crate::codegen::sentinels::emit_tagged_scalar_null(emitter); + emitter.label(&done_label); + abi::emit_store_reg_to_symbol(emitter, "rax", &slot.symbol, 0); + abi::emit_store_reg_to_symbol(emitter, "rdx", &slot.symbol, 8); +} + +/// Clears the typed-property high word after a successful non-pair static store. +fn clear_uninitialized_marker_after_static_store( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + if !matches!(slot.ty.codegen_repr(), PhpType::Str | PhpType::TaggedScalar) { + abi::emit_store_zero_to_symbol(emitter, &slot.symbol, 8); + } +} + +/// Groups static property slots by PHP-visible class name in deterministic order. +fn grouped_slots(slots: &[EvalStaticPropertySlot]) -> BTreeMap<&str, Vec<&EvalStaticPropertySlot>> { + let mut grouped = BTreeMap::new(); + for slot in slots { + grouped + .entry(slot.class_name.as_str()) + .or_insert_with(Vec::new) + .push(slot); + } + grouped +} + +/// Returns a platform-safe body label for a get/set static property slot. +fn slot_body_label(module: &Module, slot: &EvalStaticPropertySlot, mode: &str) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!("{}{}", slot_body_label_raw(slot, mode), suffix) +} + +/// Returns the architecture-independent body label stem for a static property slot. +fn slot_body_label_raw(slot: &EvalStaticPropertySlot, mode: &str) -> String { + format!( + "__elephc_eval_static_property_{}_{}_{}", + mode, + label_fragment(&slot.class_name), + label_fragment(&slot.property) + ) +} + +/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +/// Emits a C-visible global label with target-specific symbol mangling. +fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { + let symbol = module.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index ca7a1b666c..799e9287fe 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -16,6 +16,7 @@ mod eval_method_helpers; mod eval_property_helpers; mod eval_reflection_helpers; mod eval_reflection_owner_helpers; +mod eval_static_property_helpers; mod fibers; mod frame; mod function_variants; @@ -197,6 +198,7 @@ fn finalize_user_asm( exported_functions: &HashMap, ) -> String { eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); + eval_static_property_helpers::emit_eval_static_property_helpers(module, &mut emitter, &mut data); eval_constructor_helpers::emit_eval_constructor_helpers(module, &mut emitter); eval_method_helpers::emit_eval_method_helpers(module, &mut emitter, &mut data); eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d2d2108701..c9f0ec3990 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4456,6 +4456,27 @@ $box->run(); assert_eq!(out, "N:I7:I42:N"); } +/// Verifies eval fragments can read and write public nullable-int AOT static properties. +#[test] +fn test_eval_fragment_can_mutate_aot_nullable_int_static_property() { + let out = compile_and_run( + r#" Date: Wed, 24 Jun 2026 18:09:06 +0200 Subject: [PATCH 0597/1208] Support eval AOT heap property writes --- docs/php/eval.md | 2 +- src/codegen/eval_property_helpers.rs | 125 ++++++++++++++++- src/codegen/eval_static_property_helpers.rs | 148 ++++++++++++++++++-- tests/codegen/eval.rs | 99 +++++++++++++ 4 files changed, 359 insertions(+), 15 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 229f5b4e9a..0b9a4dc7da 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public scalar/string/Mixed/nullable-int AOT static property access, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public scalar/nullable-int/string/Mixed/array/object AOT static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index 9106a86e16..6e6b970029 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -262,7 +262,7 @@ fn emit_property_set_aarch64( emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id emit_aarch64_property_dispatch(module, emitter, data, slots, "set"); emit_aarch64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); - emit_aarch64_set_slot_bodies(module, emitter, slots, done_label); + emit_aarch64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, #0"); // report a failed eval property write to Rust emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure @@ -298,7 +298,7 @@ fn emit_property_set_x86_64( emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id emit_x86_64_property_dispatch(module, emitter, data, slots, "set"); emit_x86_64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); - emit_x86_64_set_slot_bodies(module, emitter, slots, done_label); + emit_x86_64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // report a failed eval property write to Rust emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after failure @@ -497,12 +497,14 @@ fn emit_x86_64_get_slot_bodies( fn emit_aarch64_set_slot_bodies( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalPropertySlot], done_label: &str, + fail_label: &str, ) { for slot in slots { emitter.label(&slot_body_label(module, slot, "set")); - emit_aarch64_store_property_slot(emitter, slot); + emit_aarch64_store_property_slot(module, emitter, data, slot, fail_label); emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust emitter.instruction(&format!("b {}", done_label)); // return after storing the declared property value } @@ -512,12 +514,14 @@ fn emit_aarch64_set_slot_bodies( fn emit_x86_64_set_slot_bodies( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalPropertySlot], done_label: &str, + fail_label: &str, ) { for slot in slots { emitter.label(&slot_body_label(module, slot, "set")); - emit_x86_64_store_property_slot(emitter, slot); + emit_x86_64_store_property_slot(module, emitter, data, slot, fail_label); emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust emitter.instruction(&format!("jmp {}", done_label)); // return after storing the declared property value } @@ -619,7 +623,13 @@ fn emit_x86_64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) } /// Stores a boxed Mixed eval value into an ARM64 object property slot. -fn emit_aarch64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { +fn emit_aarch64_store_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + fail_label: &str, +) { match slot.ty.codegen_repr() { PhpType::Int => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "x0"), PhpType::Bool => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "x0"), @@ -637,6 +647,13 @@ fn emit_aarch64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySl emitter.instruction(&format!("str x2, [x9, #{}]", slot.offset + 8)); // store the coerced string length into the property slot } PhpType::TaggedScalar => emit_aarch64_store_tagged_scalar_property(emitter, slot), + PhpType::Array(_) => emit_aarch64_store_heap_property_slot(emitter, slot, 4, fail_label), + PhpType::AssocArray { .. } => { + emit_aarch64_store_heap_property_slot(emitter, slot, 5, fail_label); + } + PhpType::Object(class_name) => { + emit_aarch64_store_object_property_slot(module, emitter, data, slot, &class_name, fail_label); + } PhpType::Mixed | PhpType::Union(_) => { emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value being assigned emitter.instruction("bl __rt_incref"); // retain the Mixed cell for property ownership @@ -649,7 +666,13 @@ fn emit_aarch64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySl } /// Stores a boxed Mixed eval value into an x86_64 object property slot. -fn emit_x86_64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { +fn emit_x86_64_store_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + fail_label: &str, +) { match slot.ty.codegen_repr() { PhpType::Int => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "rax"), PhpType::Bool => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "rax"), @@ -667,6 +690,13 @@ fn emit_x86_64_store_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlo emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rdx", slot.offset + 8)); // store the coerced string length into the property slot } PhpType::TaggedScalar => emit_x86_64_store_tagged_scalar_property(emitter, slot), + PhpType::Array(_) => emit_x86_64_store_heap_property_slot(emitter, slot, 4, fail_label), + PhpType::AssocArray { .. } => { + emit_x86_64_store_heap_property_slot(emitter, slot, 5, fail_label); + } + PhpType::Object(class_name) => { + emit_x86_64_store_object_property_slot(module, emitter, data, slot, &class_name, fail_label); + } PhpType::Mixed | PhpType::Union(_) => { emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value being assigned emitter.instruction("call __rt_incref"); // retain the Mixed cell for property ownership @@ -730,6 +760,89 @@ fn emit_x86_64_store_cast_scalar( emitter.instruction(&format!("mov QWORD PTR [r11 + {}], {}", slot.offset, result_reg)); // store the coerced scalar into the property slot } +/// Stores a boxed ARM64 eval heap value into an array-like property slot. +fn emit_aarch64_store_heap_property_slot( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + abi::emit_load_int_immediate(emitter, "x10", expected_tag); + emitter.instruction("cmp x0, x10"); // compare the assigned value tag with the property storage ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // move the unboxed heap pointer into the retained-result register + abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the heap store + emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the retained heap pointer into the property slot + emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the typed-property initialization marker +} + +/// Validates and stores a boxed ARM64 eval object into an object property slot. +fn emit_aarch64_store_object_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + class_name: &str, + fail_label: &str, +) { + if !class_name.is_empty() { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for object type validation + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emitter.instruction("mov x3, xzr"); // allow exact class matches for object property type hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject values that fail the object property type hint + } + emit_aarch64_store_heap_property_slot(emitter, slot, 6, fail_label); +} + +/// Stores a boxed x86_64 eval heap value into an array-like property slot. +fn emit_x86_64_store_heap_property_slot( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + abi::emit_load_int_immediate(emitter, "r10", expected_tag); + emitter.instruction("cmp rax, r10"); // compare the assigned value tag with the property storage ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // move the unboxed heap pointer into the retained-result register + abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the heap store + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); //store the retained heap pointer into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); //clear the typed-property initialization marker +} + +/// Validates and stores a boxed x86_64 eval object into an object property slot. +fn emit_x86_64_store_object_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + class_name: &str, + fail_label: &str, +) { + if !class_name.is_empty() { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed eval value for object type validation + abi::emit_symbol_address(emitter, "rsi", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emitter.instruction("xor ecx, ecx"); // allow exact class matches for object property type hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction("test rax, rax"); // check whether the value satisfied the object property type hint + emitter.instruction(&format!("je {}", fail_label)); // reject values that fail the object property type hint + } + emit_x86_64_store_heap_property_slot(emitter, slot, 6, fail_label); +} + /// Stores a boxed eval value into an x86_64 nullable-int tagged-scalar property slot. fn emit_x86_64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalPropertySlot) { let null_label = format!( diff --git a/src/codegen/eval_static_property_helpers.rs b/src/codegen/eval_static_property_helpers.rs index 6b76334f88..e3b9c6e0bd 100644 --- a/src/codegen/eval_static_property_helpers.rs +++ b/src/codegen/eval_static_property_helpers.rs @@ -136,6 +136,9 @@ fn static_property_type_supported(ty: &PhpType) -> bool { | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Union(_) + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } ) } @@ -240,7 +243,7 @@ fn emit_static_property_set_aarch64( emitter.instruction("str x4, [sp, #32]"); // save the boxed value being assigned emit_aarch64_static_property_dispatch(module, emitter, data, slots, "set"); emitter.instruction(&format!("b {}", fail_label)); // no supported public static property matched the request - emit_aarch64_set_slot_bodies(module, emitter, slots, done_label); + emit_aarch64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, #0"); // report a failed eval static-property write to Rust emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure @@ -269,7 +272,7 @@ fn emit_static_property_set_x86_64( emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the boxed value being assigned emit_x86_64_static_property_dispatch(module, emitter, data, slots, "set"); emitter.instruction(&format!("jmp {}", fail_label)); // no supported public static property matched the request - emit_x86_64_set_slot_bodies(module, emitter, slots, done_label); + emit_x86_64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // report a failed eval static-property write to Rust emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after failure @@ -427,12 +430,14 @@ fn emit_x86_64_get_slot_bodies( fn emit_aarch64_set_slot_bodies( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalStaticPropertySlot], done_label: &str, + fail_label: &str, ) { for slot in slots { emitter.label(&slot_body_label(module, slot, "set")); - emit_aarch64_store_static_property_slot(emitter, slot); + emit_aarch64_store_static_property_slot(module, emitter, data, slot, fail_label); emitter.instruction("mov x0, #1"); // report a successful eval static-property write to Rust emitter.instruction(&format!("b {}", done_label)); // return after storing the static property value } @@ -442,12 +447,14 @@ fn emit_aarch64_set_slot_bodies( fn emit_x86_64_set_slot_bodies( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalStaticPropertySlot], done_label: &str, + fail_label: &str, ) { for slot in slots { emitter.label(&slot_body_label(module, slot, "set")); - emit_x86_64_store_static_property_slot(emitter, slot); + emit_x86_64_store_static_property_slot(module, emitter, data, slot, fail_label); emitter.instruction("mov rax, 1"); // report a successful eval static-property write to Rust emitter.instruction(&format!("jmp {}", done_label)); // return after storing the static property value } @@ -500,7 +507,7 @@ fn emit_x86_64_uninitialized_guard( /// Boxes an ARM64 static property symbol payload into a Mixed cell. fn emit_aarch64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { match slot.ty.codegen_repr() { - PhpType::Int | PhpType::Bool => { + PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { abi::emit_load_symbol_to_reg(emitter, "x0", &slot.symbol, 0); emit_box_current_value_as_mixed(emitter, &slot.ty); } @@ -546,7 +553,7 @@ fn emit_aarch64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStati /// Boxes an x86_64 static property symbol payload into a Mixed cell. fn emit_x86_64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { match slot.ty.codegen_repr() { - PhpType::Int | PhpType::Bool => { + PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { abi::emit_load_symbol_to_reg(emitter, "rax", &slot.symbol, 0); emit_box_current_value_as_mixed(emitter, &slot.ty); } @@ -591,7 +598,13 @@ fn emit_x86_64_box_static_property_slot(emitter: &mut Emitter, slot: &EvalStatic } /// Stores a boxed Mixed eval value into an ARM64 static property symbol. -fn emit_aarch64_store_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { +fn emit_aarch64_store_static_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + fail_label: &str, +) { match slot.ty.codegen_repr() { PhpType::Int => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "x0"), PhpType::Bool => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "x0"), @@ -608,6 +621,22 @@ fn emit_aarch64_store_static_property_slot(emitter: &mut Emitter, slot: &EvalSta abi::emit_store_reg_to_symbol(emitter, "x2", &slot.symbol, 8); } PhpType::TaggedScalar => emit_aarch64_store_tagged_scalar_static_property(emitter, slot), + PhpType::Array(_) => { + emit_aarch64_store_heap_static_property_slot(emitter, slot, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_aarch64_store_heap_static_property_slot(emitter, slot, 5, fail_label); + } + PhpType::Object(class_name) => { + emit_aarch64_store_object_static_property_slot( + module, + emitter, + data, + slot, + &class_name, + fail_label, + ); + } PhpType::Mixed | PhpType::Union(_) => { emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value being assigned emitter.instruction("bl __rt_incref"); // retain the Mixed cell for static-property ownership @@ -619,7 +648,13 @@ fn emit_aarch64_store_static_property_slot(emitter: &mut Emitter, slot: &EvalSta } /// Stores a boxed Mixed eval value into an x86_64 static property symbol. -fn emit_x86_64_store_static_property_slot(emitter: &mut Emitter, slot: &EvalStaticPropertySlot) { +fn emit_x86_64_store_static_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + fail_label: &str, +) { match slot.ty.codegen_repr() { PhpType::Int => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "rax"), PhpType::Bool => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "rax"), @@ -636,6 +671,22 @@ fn emit_x86_64_store_static_property_slot(emitter: &mut Emitter, slot: &EvalStat abi::emit_store_reg_to_symbol(emitter, "rdx", &slot.symbol, 8); } PhpType::TaggedScalar => emit_x86_64_store_tagged_scalar_static_property(emitter, slot), + PhpType::Array(_) => { + emit_x86_64_store_heap_static_property_slot(emitter, slot, 4, fail_label); + } + PhpType::AssocArray { .. } => { + emit_x86_64_store_heap_static_property_slot(emitter, slot, 5, fail_label); + } + PhpType::Object(class_name) => { + emit_x86_64_store_object_static_property_slot( + module, + emitter, + data, + slot, + &class_name, + fail_label, + ); + } PhpType::Mixed | PhpType::Union(_) => { emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value being assigned emitter.instruction("call __rt_incref"); // retain the Mixed cell for static-property ownership @@ -672,6 +723,87 @@ fn emit_x86_64_store_cast_scalar( clear_uninitialized_marker_after_static_store(emitter, slot); } +/// Stores a boxed ARM64 eval heap value into an array-like static property symbol. +fn emit_aarch64_store_heap_static_property_slot( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + abi::emit_load_int_immediate(emitter, "x10", expected_tag); + emitter.instruction("cmp x0, x10"); // compare the assigned value tag with the static-property ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // move the unboxed heap pointer into the retained-result register + abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); + abi::emit_store_reg_to_symbol(emitter, "x0", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); +} + +/// Validates and stores a boxed ARM64 eval object into an object static property symbol. +fn emit_aarch64_store_object_static_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + class_name: &str, + fail_label: &str, +) { + if !class_name.is_empty() { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed eval value for object type validation + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emitter.instruction("mov x3, xzr"); // allow exact class matches for object static-property hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject values that fail the object static-property type hint + } + emit_aarch64_store_heap_static_property_slot(emitter, slot, 6, fail_label); +} + +/// Stores a boxed x86_64 eval heap value into an array-like static property symbol. +fn emit_x86_64_store_heap_static_property_slot( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, + expected_tag: i64, + fail_label: &str, +) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + abi::emit_load_int_immediate(emitter, "r10", expected_tag); + emitter.instruction("cmp rax, r10"); // compare the assigned value tag with the static-property ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // move the unboxed heap pointer into the retained-result register + abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); + abi::emit_store_reg_to_symbol(emitter, "rax", &slot.symbol, 0); + clear_uninitialized_marker_after_static_store(emitter, slot); +} + +/// Validates and stores a boxed x86_64 eval object into an object static property symbol. +fn emit_x86_64_store_object_static_property_slot( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + class_name: &str, + fail_label: &str, +) { + if !class_name.is_empty() { + let (label, len) = data.add_string(class_name.as_bytes()); + let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed eval value for object type validation + abi::emit_symbol_address(emitter, "rsi", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emitter.instruction("xor ecx, ecx"); // allow exact class matches for object static-property hints + abi::emit_call_label(emitter, &is_a_symbol); + emitter.instruction("test rax, rax"); // check whether the value satisfied the static-property hint + emitter.instruction(&format!("je {}", fail_label)); // reject values that fail the object static-property type hint + } + emit_x86_64_store_heap_static_property_slot(emitter, slot, 6, fail_label); +} + /// Stores a boxed eval value into an ARM64 nullable-int static property symbol. fn emit_aarch64_store_tagged_scalar_static_property( emitter: &mut Emitter, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c9f0ec3990..cc27793192 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4456,6 +4456,61 @@ $box->run(); assert_eq!(out, "N:I7:I42:N"); } +/// Verifies eval fragments can replace public array AOT properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_array_property() { + let out = compile_and_run( + r#"items = [3, 4]; + return count($this->items) . ":" . $this->items[0] . ":" . $this->items[1];'); + } +} + +$box = new EvalArrayPropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "2:3:4"); +} + +/// Verifies eval fragments can replace public object AOT properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_object_property() { + let out = compile_and_run( + r#"n = $n; + } +} + +class EvalObjectPropBox { + public EvalObjectPropValue $value; + + public function __construct() { + $this->value = new EvalObjectPropValue(1); + } + + public function run(): void { + echo eval('$this->value = new EvalObjectPropValue(7); + $value = $this->value; + return $value->n;'); + } +} + +$box = new EvalObjectPropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "7"); +} + /// Verifies eval fragments can read and write public nullable-int AOT static properties. #[test] fn test_eval_fragment_can_mutate_aot_nullable_int_static_property() { @@ -4477,6 +4532,50 @@ echo eval('$out = (EvalNullableIntStaticPropBox::$count === null) ? "N" : "n"; assert_eq!(out, "N:I9:I33:N"); } +/// Verifies eval fragments can replace public array AOT static properties. +#[test] +fn test_eval_fragment_can_mutate_aot_array_static_property() { + let out = compile_and_run( + r#"n = $n; + } +} + +class EvalObjectStaticPropBox { + public static EvalObjectStaticPropValue $value; +} + +EvalObjectStaticPropBox::$value = new EvalObjectStaticPropValue(1); + +echo eval('EvalObjectStaticPropBox::$value = new EvalObjectStaticPropValue(8); + $value = EvalObjectStaticPropBox::$value; + return $value->n;'); +"#, + ); + assert_eq!(out, "8"); +} + /// Verifies eval keeps PHP property names case-sensitive while parsing keywords case-insensitively. #[test] fn test_eval_fragment_preserves_this_property_case() { From 4da98197cee334d5f0082eb1d33cf96ae4057fc9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 18:30:36 +0200 Subject: [PATCH 0598/1208] Support eval AOT private property scope --- crates/elephc-magician/src/ffi/execute.rs | 2 +- .../elephc-magician/src/ffi/function_calls.rs | 4 +- .../src/runtime_hooks/externs.rs | 4 + .../elephc-magician/src/runtime_hooks/mod.rs | 28 ++- .../elephc-magician/src/runtime_hooks/ops.rs | 6 + docs/php/eval.md | 8 +- src/codegen/eval_property_helpers.rs | 175 +++++++++++++++--- tests/codegen/eval.rs | 39 ++++ 8 files changed, 227 insertions(+), 39 deletions(-) diff --git a/crates/elephc-magician/src/ffi/execute.rs b/crates/elephc-magician/src/ffi/execute.rs index f8b556fa0d..e1e603d7cb 100644 --- a/crates/elephc-magician/src/ffi/execute.rs +++ b/crates/elephc-magician/src/ffi/execute.rs @@ -105,7 +105,7 @@ unsafe fn execute_parsed_eval( fallback_scope = ElephcEvalScope::new(); &mut fallback_scope }; - let mut values = ElephcRuntimeOps::new(); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); match interpreter::execute_program_outcome_with_context(context, program, scope, &mut values) { Ok(outcome) => write_outcome(outcome, out).code(), Err(status) => status.code(), diff --git a/crates/elephc-magician/src/ffi/function_calls.rs b/crates/elephc-magician/src/ffi/function_calls.rs index 9c940c7dbe..5ab1ecf30e 100644 --- a/crates/elephc-magician/src/ffi/function_calls.rs +++ b/crates/elephc-magician/src/ffi/function_calls.rs @@ -119,7 +119,7 @@ unsafe fn call_eval_function_inner( .collect() }; clear_result(out); - let mut values = ElephcRuntimeOps::new(); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); match interpreter::execute_context_function_outcome( context, &name.to_ascii_lowercase(), @@ -157,7 +157,7 @@ unsafe fn call_eval_function_array_inner( return EvalStatus::RuntimeFatal.code(); } clear_result(out); - let mut values = ElephcRuntimeOps::new(); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); match interpreter::execute_context_function_call_array_outcome( context, &name.to_ascii_lowercase(), diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index b14d130b71..606bd3816e 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -44,12 +44,16 @@ unsafe extern "C" { object: *mut RuntimeCell, name_ptr: *const u8, name_len: u64, + scope_ptr: *const u8, + scope_len: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_property_set( object: *mut RuntimeCell, name_ptr: *const u8, name_len: u64, value: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, ) -> u64; pub(super) fn __elephc_eval_value_static_property_get( class_ptr: *const u8, diff --git a/crates/elephc-magician/src/runtime_hooks/mod.rs b/crates/elephc-magician/src/runtime_hooks/mod.rs index f8804efa49..c0c22487fc 100644 --- a/crates/elephc-magician/src/runtime_hooks/mod.rs +++ b/crates/elephc-magician/src/runtime_hooks/mod.rs @@ -21,6 +21,8 @@ mod tags; #[cfg(not(test))] use crate::errors::EvalStatus; #[cfg(not(test))] +use crate::abi::ElephcEvalContext; +#[cfg(not(test))] use crate::value::{RuntimeCell, RuntimeCellHandle}; #[cfg(not(test))] use externs::{ @@ -29,13 +31,22 @@ use externs::{ /// Runtime hook adapter that produces and consumes boxed elephc Mixed cells. #[cfg(not(test))] -pub struct ElephcRuntimeOps; +pub struct ElephcRuntimeOps { + context: *const ElephcEvalContext, +} #[cfg(not(test))] impl ElephcRuntimeOps { - /// Creates a new stateless runtime hook adapter. + /// Creates a runtime hook adapter without caller-sensitive eval context. pub const fn new() -> Self { - Self + Self { + context: std::ptr::null(), + } + } + + /// Creates a runtime hook adapter that can expose the active class scope to generated helpers. + pub const fn with_context(context: *const ElephcEvalContext) -> Self { + Self { context } } /// Converts a runtime wrapper result into an interpreter handle. @@ -59,4 +70,15 @@ impl ElephcRuntimeOps { } Ok(arg_array) } + + /// Returns the active eval class-scope bytes in the generated helper ABI shape. + fn current_class_scope_abi(&self) -> (*const u8, u64) { + let Some(context) = (unsafe { self.context.as_ref() }) else { + return (std::ptr::null(), 0); + }; + let Some(class_scope) = context.current_class_scope() else { + return (std::ptr::null(), 0); + }; + (class_scope.as_ptr(), class_scope.len() as u64) + } } diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 3321646cec..1149e528ce 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -94,11 +94,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { object: RuntimeCellHandle, property: &str, ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); Self::handle(unsafe { __elephc_eval_value_property_get( object.as_ptr(), property.as_ptr(), property.len() as u64, + scope_ptr, + scope_len, ) }) } @@ -110,12 +113,15 @@ impl RuntimeValueOps for ElephcRuntimeOps { property: &str, value: RuntimeCellHandle, ) -> Result<(), EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); let ok = unsafe { __elephc_eval_value_property_set( object.as_ptr(), property.as_ptr(), property.len() as u64, value.as_ptr(), + scope_ptr, + scope_len, ) }; if ok == 0 { diff --git a/docs/php/eval.md b/docs/php/eval.md index 0b9a4dc7da..5b565fc740 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public scalar/nullable-int/string/Mixed/array/object AOT static property access, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots plus private AOT property slots when eval is executing in the declaring native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public scalar/nullable-int/string/Mixed/array/object AOT static property access, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | @@ -512,8 +512,10 @@ constants. Direct `new EnumName()` and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native -methods are bridged to eval. Public fixed scalar/nullable-int/Mixed/array/iterable/object method -parameters through `$this->method(...)` are supported by the native method +methods are bridged to eval, and private declared property reads/writes are +bridged when the eval fragment runs inside the declaring native class scope. +Public fixed scalar/nullable-int/Mixed/array/iterable/object method parameters +through `$this->method(...)` are supported by the native method bridge, including registered named arguments and string-keyed unpacking; method returns may be scalar/nullable-int/Mixed/array/iterable/object values. diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index 6e6b970029..2a5128209f 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -8,8 +8,8 @@ //! Key details: //! - The cacheable runtime object cannot know user class ids or property //! offsets, so these C-ABI symbols are emitted into the user assembly. -//! - The first slice supports public declared properties with scalar/Mixed -//! storage, which covers `$this->prop` inside eval-called native methods. +//! - Supported slots include public properties and private properties when the +//! active eval class scope exactly matches the declaring AOT class. use std::collections::BTreeMap; @@ -27,7 +27,9 @@ use crate::types::{ClassInfo, PhpType}; struct EvalPropertySlot { class_id: u64, class_name: String, + declaring_class: String, property: String, + visibility: Visibility, offset: usize, ty: PhpType, } @@ -77,7 +79,7 @@ fn function_uses_eval(function: &Function) -> bool { }) } -/// Collects public declared properties with storage layouts the bridge can access. +/// Collects declared properties with storage layouts and visibility rules the bridge can access. fn collect_eval_property_slots(module: &Module) -> Vec { let mut slots = Vec::new(); let mut classes = module.class_infos.iter().collect::>(); @@ -88,7 +90,7 @@ fn collect_eval_property_slots(module: &Module) -> Vec { slots } -/// Adds bridge-supported public properties for one class. +/// Adds bridge-supported properties for one class. fn collect_class_property_slots( class_name: &str, class_info: &ClassInfo, @@ -98,25 +100,38 @@ fn collect_class_property_slots( if class_info.visible_property_index(property) != Some(index) { continue; } - if !property_is_public(class_info, property) || !property_type_supported(ty) { + let visibility = property_visibility(class_info, property); + if !property_visibility_supported(visibility) || !property_type_supported(ty) { continue; } + let declaring_class = class_info + .property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name); slots.push(EvalPropertySlot { class_id: class_info.class_id, class_name: class_name.to_string(), + declaring_class: declaring_class.to_string(), property: property.clone(), + visibility: visibility.clone(), offset: 8 + index * 16, ty: ty.codegen_repr(), }); } } -/// Returns true when the property is visible to PHP eval from method scope. -fn property_is_public(class_info: &ClassInfo, property: &str) -> bool { +/// Returns the declared property visibility, defaulting to public metadata. +fn property_visibility<'a>(class_info: &'a ClassInfo, property: &str) -> &'a Visibility { class_info .property_visibilities .get(property) - .is_none_or(|visibility| matches!(visibility, Visibility::Public)) + .unwrap_or(&Visibility::Public) +} + +/// Returns true when the eval property bridge can enforce this visibility. +fn property_visibility_supported(visibility: &Visibility) -> bool { + matches!(visibility, Visibility::Public | Visibility::Private) } /// Returns true for property storage shapes the bridge can box and update. @@ -136,7 +151,7 @@ fn property_type_supported(ty: &PhpType) -> bool { ) } -/// Emits `__elephc_eval_value_property_get(Mixed*, name, len) -> Mixed*`. +/// Emits `__elephc_eval_value_property_get(Mixed*, name, len, scope, scope_len) -> Mixed*`. fn emit_property_get_helper( module: &Module, emitter: &mut Emitter, @@ -152,7 +167,7 @@ fn emit_property_get_helper( } } -/// Emits `__elephc_eval_value_property_set(Mixed*, name, len, Mixed*) -> bool`. +/// Emits `__elephc_eval_value_property_set(Mixed*, name, len, Mixed*, scope, scope_len) -> bool`. fn emit_property_set_helper( module: &Module, emitter: &mut Emitter, @@ -176,29 +191,35 @@ fn emit_property_get_aarch64( slots: &[EvalPropertySlot], ) { let null_label = "__elephc_eval_value_property_get_null"; + let fail_label = "__elephc_eval_value_property_get_fail"; let done_label = "__elephc_eval_value_property_get_done"; - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for saved name/object and fp/lr - emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for saved inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length emitter.instruction("str x0, [sp, #24]"); // save the boxed receiver for stdClass fallback reads + emitter.instruction("str x3, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope length emitter.instruction(&format!("cbz x0, {}", null_label)); // null Mixed receiver reads as PHP null emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object emitter.instruction(&format!("b.ne {}", null_label)); // non-object receivers read as PHP null emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property loads emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id - emit_aarch64_property_dispatch(module, emitter, data, slots, "get"); + emit_aarch64_property_dispatch(module, emitter, data, slots, "get", fail_label); emit_aarch64_stdclass_property_get_fallback(emitter); emitter.instruction(&format!("b {}", done_label)); // return after stdClass fallback get or null result emit_aarch64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // report an inaccessible declared property read to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after access failure emitter.label(null_label); let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); abi::emit_call_label(emitter, &null_symbol); emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame emitter.instruction("ret"); // return the boxed property value to Rust } @@ -210,13 +231,16 @@ fn emit_property_get_x86_64( slots: &[EvalPropertySlot], ) { let null_label = "__elephc_eval_value_property_get_null_x"; + let fail_label = "__elephc_eval_value_property_get_fail_x"; let done_label = "__elephc_eval_value_property_get_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // reserve aligned slots for name, length, and object + emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, length, object, and scope emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the boxed receiver for stdClass fallback reads + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope length emitter.instruction(&format!("test rdi, rdi")); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", null_label)); // null Mixed receiver reads as PHP null emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register @@ -225,10 +249,13 @@ fn emit_property_get_x86_64( emitter.instruction(&format!("jne {}", null_label)); // non-object receivers read as PHP null emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property loads emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id - emit_x86_64_property_dispatch(module, emitter, data, slots, "get"); + emit_x86_64_property_dispatch(module, emitter, data, slots, "get", fail_label); emit_x86_64_stdclass_property_get_fallback(emitter); emitter.instruction(&format!("jmp {}", done_label)); // return after stdClass fallback get or null result emit_x86_64_get_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report an inaccessible declared property read to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after access failure emitter.label(null_label); let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); abi::emit_call_label(emitter, &null_symbol); @@ -247,28 +274,30 @@ fn emit_property_set_aarch64( ) { let fail_label = "__elephc_eval_value_property_set_fail"; let done_label = "__elephc_eval_value_property_set_done"; - emitter.instruction("sub sp, sp, #80"); // reserve helper frame for inputs, object, and fp/lr - emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("sub sp, sp, #96"); // reserve helper frame for inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #80]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #80"); // establish a stable helper frame pointer emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length emitter.instruction("str x3, [sp, #24]"); // save the boxed value being assigned emitter.instruction("str x0, [sp, #32]"); // save the boxed receiver for stdClass fallback writes + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #48]"); // save the active eval class-scope length emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot accept a property write emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers reject the property write emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property stores emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id - emit_aarch64_property_dispatch(module, emitter, data, slots, "set"); + emit_aarch64_property_dispatch(module, emitter, data, slots, "set", fail_label); emit_aarch64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); emit_aarch64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, #0"); // report a failed eval property write to Rust emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #96"); // release the helper frame emitter.instruction("ret"); // return the write-status flag to Rust } @@ -283,11 +312,13 @@ fn emit_property_set_x86_64( let done_label = "__elephc_eval_value_property_set_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, length, object, and value + emitter.instruction("sub rsp, 64"); // reserve aligned slots for name, length, object, value, and scope emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed value being assigned emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the boxed receiver for stdClass fallback writes + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope length emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot accept a property write emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register @@ -296,7 +327,7 @@ fn emit_property_set_x86_64( emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers reject the property write emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property stores emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id - emit_x86_64_property_dispatch(module, emitter, data, slots, "set"); + emit_x86_64_property_dispatch(module, emitter, data, slots, "set", fail_label); emit_x86_64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); emit_x86_64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); @@ -390,6 +421,7 @@ fn emit_aarch64_property_dispatch( data: &mut DataSection, slots: &[EvalPropertySlot], mode: &str, + fail_label: &str, ) { for (class_id, class_slots) in grouped_slots(slots) { let class_label = format!("__elephc_eval_property_{}_class_{}", mode, class_id); @@ -398,7 +430,7 @@ fn emit_aarch64_property_dispatch( emitter.instruction("cmp x9, x10"); // compare receiver class id against this eval bridge class emitter.instruction(&format!("b.ne {}", next_label)); // try the next class when ids differ for slot in class_slots { - emit_aarch64_property_name_compare(module, emitter, data, slot, mode); + emit_aarch64_property_name_compare(module, emitter, data, slot, mode, fail_label); } emitter.label(&class_label); emitter.instruction(&format!("b {}", next_label)); // fall through to the next class after a name miss @@ -413,6 +445,7 @@ fn emit_x86_64_property_dispatch( data: &mut DataSection, slots: &[EvalPropertySlot], mode: &str, + fail_label: &str, ) { for (class_id, class_slots) in grouped_slots(slots) { let class_label = format!("__elephc_eval_property_{}_class_{}_x", mode, class_id); @@ -421,7 +454,7 @@ fn emit_x86_64_property_dispatch( emitter.instruction("cmp r11, r10"); // compare receiver class id against this eval bridge class emitter.instruction(&format!("jne {}", next_label)); // try the next class when ids differ for slot in class_slots { - emit_x86_64_property_name_compare(module, emitter, data, slot, mode); + emit_x86_64_property_name_compare(module, emitter, data, slot, mode, fail_label); } emitter.label(&class_label); emitter.instruction(&format!("jmp {}", next_label)); // fall through to the next class after a name miss @@ -436,6 +469,7 @@ fn emit_aarch64_property_name_compare( data: &mut DataSection, slot: &EvalPropertySlot, mode: &str, + fail_label: &str, ) { let (label, len) = data.add_string(slot.property.as_bytes()); emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer @@ -444,7 +478,15 @@ fn emit_aarch64_property_name_compare( abi::emit_load_int_immediate(emitter, "x4", len as i64); emitter.instruction("bl __rt_str_eq"); // compare requested property name with this declared property let target_label = slot_body_label(module, slot, mode); - emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the property body when the names match + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the property body when the names match + return; + } + let miss_label = slot_access_miss_label(module, slot, mode); + emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue property dispatch when names differ + emit_aarch64_private_property_scope_check(emitter, data, slot, mode, fail_label); + emitter.instruction(&format!("b {}", target_label)); // dispatch after private visibility is satisfied + emitter.label(&miss_label); } /// Emits one x86_64 property-name comparison and branch to the matching body. @@ -454,6 +496,7 @@ fn emit_x86_64_property_name_compare( data: &mut DataSection, slot: &EvalPropertySlot, mode: &str, + fail_label: &str, ) { let (label, len) = data.add_string(slot.property.as_bytes()); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer @@ -462,7 +505,74 @@ fn emit_x86_64_property_name_compare( abi::emit_load_int_immediate(emitter, "rcx", len as i64); emitter.instruction("call __rt_str_eq"); // compare requested property name with this declared property emitter.instruction("test rax, rax"); // check whether the property names matched - emitter.instruction(&format!("jne {}", slot_body_label(module, slot, mode))); // dispatch to the property body when the names match + let target_label = slot_body_label(module, slot, mode); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the property body when the names match + return; + } + let miss_label = slot_access_miss_label(module, slot, mode); + emitter.instruction(&format!("je {}", miss_label)); // continue property dispatch when names differ + emit_x86_64_private_property_scope_check(emitter, data, slot, mode, fail_label); + emitter.instruction(&format!("jmp {}", target_label)); // dispatch after private visibility is satisfied + emitter.label(&miss_label); +} + +/// Emits an ARM64 exact-class check for a private property bridge hit. +fn emit_aarch64_private_property_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + mode: &str, + fail_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = aarch64_scope_offsets(mode); + let (label, len) = data.add_string(slot.declaring_class.as_bytes()); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject private property access outside a class scope + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval class scope with the declaring class + emitter.instruction(&format!("cbnz x0, {}", fail_label)); // reject private property access from a different class +} + +/// Emits an x86_64 exact-class check for a private property bridge hit. +fn emit_x86_64_private_property_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalPropertySlot, + mode: &str, + fail_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = x86_64_scope_offsets(mode); + let (label, len) = data.add_string(slot.declaring_class.as_bytes()); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); //reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); //reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", fail_label)); // reject private property access outside a class scope + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval class scope with the declaring class + emitter.instruction("test rax, rax"); // check whether the scope matched the declaring class + emitter.instruction(&format!("jne {}", fail_label)); // reject private property access from a different class +} + +/// Returns ARM64 stack offsets for the class-scope pointer and length. +fn aarch64_scope_offsets(mode: &str) -> (usize, usize) { + match mode { + "get" => (32, 40), + "set" => (40, 48), + _ => unreachable!("eval property helpers only use get/set modes"), + } +} + +/// Returns x86_64 frame offsets for the class-scope pointer and length. +fn x86_64_scope_offsets(mode: &str) -> (usize, usize) { + match mode { + "get" => (40, 48), + "set" => (48, 56), + _ => unreachable!("eval property helpers only use get/set modes"), + } } /// Emits ARM64 property-get bodies for every bridge-supported property slot. @@ -887,6 +997,11 @@ fn slot_body_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> Stri format!("{}{}", slot_body_label_raw(slot, mode), suffix) } +/// Returns a platform-safe label for continuing after a private property name miss. +fn slot_access_miss_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> String { + format!("{}_access_miss", slot_body_label(module, slot, mode)) +} + /// Returns the architecture-independent body label stem for a property slot. fn slot_body_label_raw(slot: &EvalPropertySlot, mode: &str) -> String { format!( diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cc27793192..48e062693e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4430,6 +4430,45 @@ echo $box->x; assert_eq!(out, "2"); } +/// Verifies eval fragments inherit native method scope for private AOT property access. +#[test] +fn test_eval_fragment_can_mutate_this_private_property_from_declaring_method() { + let out = compile_and_run( + r#"x = $this->x + 4; return $this->x;'); + } +} + +$box = new EvalPrivatePropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments outside the declaring scope cannot read private AOT properties. +#[test] +fn test_eval_fragment_rejects_private_property_outside_declaring_scope() { + let err = compile_and_run_expect_failure( + r#"x;'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + /// Verifies eval fragments can read and write public nullable-int AOT properties through `$this`. #[test] fn test_eval_fragment_can_mutate_this_nullable_int_property() { From ee78034ebe34fc64f0409fd54c40fc9ce866f5e2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 18:47:58 +0200 Subject: [PATCH 0599/1208] Support eval AOT private static property scope --- .../src/interpreter/runtime_ops.rs | 4 +- .../src/runtime_hooks/externs.rs | 4 + .../elephc-magician/src/runtime_hooks/ops.rs | 10 +- docs/php/eval.md | 5 +- src/codegen/eval_static_property_helpers.rs | 142 +++++++++++++++--- tests/codegen/eval.rs | 45 ++++++ 6 files changed, 187 insertions(+), 23 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 0922d76d62..bab3882788 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -79,14 +79,14 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result<(), EvalStatus>; - /// Reads a public generated/AOT static property through the generated bridge. + /// Reads a generated/AOT static property through the generated bridge. fn static_property_get( &mut self, class_name: &str, property: &str, ) -> Result, EvalStatus>; - /// Writes a public generated/AOT static property through the generated bridge. + /// Writes a generated/AOT static property through the generated bridge. fn static_property_set( &mut self, class_name: &str, diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 606bd3816e..eb98176a92 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -60,6 +60,8 @@ unsafe extern "C" { class_len: u64, name_ptr: *const u8, name_len: u64, + scope_ptr: *const u8, + scope_len: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_static_property_set( class_ptr: *const u8, @@ -67,6 +69,8 @@ unsafe extern "C" { name_ptr: *const u8, name_len: u64, value: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, ) -> u64; /// Returns a boxed shallow clone for stdClass/eval object storage. pub(super) fn __elephc_eval_value_object_clone_shallow( diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 1149e528ce..b06befdf0d 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -131,18 +131,21 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } - /// Reads a public AOT static property through the generated user helper. + /// Reads an AOT static property through the generated user helper. fn static_property_get( &mut self, class_name: &str, property: &str, ) -> Result, EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); let ptr = unsafe { __elephc_eval_value_static_property_get( class_name.as_ptr(), class_name.len() as u64, property.as_ptr(), property.len() as u64, + scope_ptr, + scope_len, ) }; if ptr.is_null() { @@ -152,13 +155,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } - /// Writes a public AOT static property through the generated user helper. + /// Writes an AOT static property through the generated user helper. fn static_property_set( &mut self, class_name: &str, property: &str, value: RuntimeCellHandle, ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); let ok = unsafe { __elephc_eval_value_static_property_set( class_name.as_ptr(), @@ -166,6 +170,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { property.as_ptr(), property.len() as u64, value.as_ptr(), + scope_ptr, + scope_len, ) }; Ok(ok != 0) diff --git a/docs/php/eval.md b/docs/php/eval.md index 5b565fc740..1c02b57445 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots plus private AOT property slots when eval is executing in the declaring native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public scalar/nullable-int/string/Mixed/array/object AOT static property access, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots plus private AOT property slots when eval is executing in the declaring native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public scalar/nullable-int/string/Mixed/array/object AOT static property access plus private AOT static property access from the declaring native class scope, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | @@ -514,6 +514,9 @@ rejected. Public declared property reads/writes through `$this->property` from native methods are bridged to eval, and private declared property reads/writes are bridged when the eval fragment runs inside the declaring native class scope. +Public declared static property reads/writes are bridged to eval, and private +declared static property reads/writes are bridged from the declaring native +class scope. Public fixed scalar/nullable-int/Mixed/array/iterable/object method parameters through `$this->method(...)` are supported by the native method bridge, including registered named arguments and string-keyed unpacking; method diff --git a/src/codegen/eval_static_property_helpers.rs b/src/codegen/eval_static_property_helpers.rs index e3b9c6e0bd..d3fcd8e0ab 100644 --- a/src/codegen/eval_static_property_helpers.rs +++ b/src/codegen/eval_static_property_helpers.rs @@ -1,6 +1,6 @@ //! Purpose: -//! Emits user-assembly helpers that let libelephc-magician access public native -//! static properties through symbol-backed storage. +//! Emits user-assembly helpers that let libelephc-magician access native static +//! properties through symbol-backed storage. //! //! Called from: //! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. @@ -8,6 +8,8 @@ //! Key details: //! - The cacheable runtime object cannot know user static-property symbols, so //! these C-ABI bridge symbols are emitted into the user assembly. +//! - Supported slots include public static properties and private static +//! properties when the active eval class scope matches the declaring class. //! - Null helper returns mean "no bridge match"; boxed PHP null is returned as //! a real Mixed cell pointer. @@ -27,7 +29,9 @@ use crate::types::{ClassInfo, PhpType}; #[derive(Clone)] struct EvalStaticPropertySlot { class_name: String, + declaring_class: String, property: String, + visibility: Visibility, symbol: String, ty: PhpType, is_declared: bool, @@ -75,7 +79,7 @@ fn function_uses_eval(function: &Function) -> bool { }) } -/// Collects public static properties with storage layouts the bridge can access. +/// Collects static properties with storage layouts and visibility rules the bridge can access. fn collect_eval_static_property_slots(module: &Module) -> Vec { let mut slots = Vec::new(); let mut classes = module.class_infos.iter().collect::>(); @@ -86,7 +90,7 @@ fn collect_eval_static_property_slots(module: &Module) -> Vec>(); properties.sort_by(|(left, _), (right, _)| left.cmp(right)); for (property, ty) in properties { - if !static_property_is_public(class_info, property) || !static_property_type_supported(ty) { + let visibility = static_property_visibility(class_info, property); + if !static_property_visibility_supported(visibility) + || !static_property_type_supported(ty) + { continue; } let declaring_class = class_info @@ -109,7 +116,9 @@ fn collect_class_static_property_slots( }; slots.push(EvalStaticPropertySlot { class_name: class_name.to_string(), + declaring_class: declaring_class.to_string(), property: property.clone(), + visibility: visibility.clone(), symbol: static_property_symbol(declaring_class, property), ty: ty.codegen_repr(), is_declared: declaring_info.declared_static_properties.contains(property), @@ -117,12 +126,17 @@ fn collect_class_static_property_slots( } } -/// Returns true when a static property is publicly visible to eval. -fn static_property_is_public(class_info: &ClassInfo, property: &str) -> bool { +/// Returns the declared static-property visibility, defaulting to public metadata. +fn static_property_visibility<'a>(class_info: &'a ClassInfo, property: &str) -> &'a Visibility { class_info .static_property_visibilities .get(property) - .is_none_or(|visibility| matches!(visibility, Visibility::Public)) + .unwrap_or(&Visibility::Public) +} + +/// Returns true when the eval static-property bridge can enforce this visibility. +fn static_property_visibility_supported(visibility: &Visibility) -> bool { + matches!(visibility, Visibility::Public | Visibility::Private) } /// Returns true for static-property storage shapes the bridge can box and update. @@ -142,7 +156,7 @@ fn static_property_type_supported(ty: &PhpType) -> bool { ) } -/// Emits `__elephc_eval_value_static_property_get(class, name) -> Mixed*`. +/// Emits `__elephc_eval_value_static_property_get(class, name, scope, scope_len) -> Mixed*`. fn emit_static_property_get_helper( module: &Module, emitter: &mut Emitter, @@ -158,7 +172,7 @@ fn emit_static_property_get_helper( } } -/// Emits `__elephc_eval_value_static_property_set(class, name, Mixed*) -> bool`. +/// Emits `__elephc_eval_value_static_property_set(class, name, Mixed*, scope, scope_len) -> bool`. fn emit_static_property_set_helper( module: &Module, emitter: &mut Emitter, @@ -182,13 +196,15 @@ fn emit_static_property_get_aarch64( slots: &[EvalStaticPropertySlot], ) { let done_label = "__elephc_eval_value_static_property_get_done"; - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class/property slices and fp/lr + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class/property/scope slices and fp/lr emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length emit_aarch64_static_property_dispatch(module, emitter, data, slots, "get"); emitter.instruction("mov x0, xzr"); // report bridge miss with a null pointer emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss @@ -209,11 +225,13 @@ fn emit_static_property_get_x86_64( let done_label = "__elephc_eval_value_static_property_get_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and property slices + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, property, and scope slices emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope length emit_x86_64_static_property_dispatch(module, emitter, data, slots, "get"); emitter.instruction("xor eax, eax"); // report bridge miss with a null pointer emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss @@ -233,7 +251,7 @@ fn emit_static_property_set_aarch64( ) { let fail_label = "__elephc_eval_value_static_property_set_fail"; let done_label = "__elephc_eval_value_static_property_set_done"; - emitter.instruction("sub sp, sp, #80"); // reserve helper frame for class/property, value, and fp/lr + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for class/property, value, scope, and fp/lr emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer @@ -241,8 +259,10 @@ fn emit_static_property_set_aarch64( emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length emitter.instruction("str x4, [sp, #32]"); // save the boxed value being assigned + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope pointer + emitter.instruction("str x6, [sp, #48]"); // save the active eval class-scope length emit_aarch64_static_property_dispatch(module, emitter, data, slots, "set"); - emitter.instruction(&format!("b {}", fail_label)); // no supported public static property matched the request + emitter.instruction(&format!("b {}", fail_label)); // no supported static property matched the request emit_aarch64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, #0"); // report a failed eval static-property write to Rust @@ -264,14 +284,17 @@ fn emit_static_property_set_x86_64( let done_label = "__elephc_eval_value_static_property_set_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, property, and value + emitter.instruction("sub rsp, 64"); // reserve aligned slots for class, property, value, and scope emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the boxed value being assigned + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope pointer + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval class-scope length stack argument + emitter.instruction("mov QWORD PTR [rbp - 56], rax"); // save the active eval class-scope length emit_x86_64_static_property_dispatch(module, emitter, data, slots, "set"); - emitter.instruction(&format!("jmp {}", fail_label)); // no supported public static property matched the request + emitter.instruction(&format!("jmp {}", fail_label)); // no supported static property matched the request emit_x86_64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // report a failed eval static-property write to Rust @@ -374,7 +397,14 @@ fn emit_aarch64_static_property_name_compare( abi::emit_load_int_immediate(emitter, "x4", len as i64); emitter.instruction("bl __rt_str_eq"); // compare property names with PHP case-sensitive rules let target_label = slot_body_label(module, slot, mode); - emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the static property body when names match + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the static property body when names match + return; + } + let miss_label = slot_access_miss_label(module, slot, mode); + emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue static-property dispatch when names differ + emit_aarch64_private_static_property_scope_check(emitter, data, slot, mode, &target_label); + emitter.label(&miss_label); } /// Emits one x86_64 property-name comparison and branch to the matching body. @@ -393,7 +423,74 @@ fn emit_x86_64_static_property_name_compare( emitter.instruction("call __rt_str_eq"); // compare property names with PHP case-sensitive rules emitter.instruction("test rax, rax"); // check whether the property names matched let target_label = slot_body_label(module, slot, mode); - emitter.instruction(&format!("jne {}", target_label)); // dispatch to the static property body when names match + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the static property body when names match + return; + } + let miss_label = slot_access_miss_label(module, slot, mode); + emitter.instruction(&format!("je {}", miss_label)); // continue static-property dispatch when names differ + emit_x86_64_private_static_property_scope_check(emitter, data, slot, mode, &target_label); + emitter.label(&miss_label); +} + +/// Emits an ARM64 exact-class check for a private static-property bridge hit. +fn emit_aarch64_private_static_property_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + mode: &str, + target_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = aarch64_scope_offsets(mode); + let (label, len) = data.add_string(slot.declaring_class.as_bytes()); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction("cbz x1, 1f"); // skip private dispatch outside a class scope + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval class scope with the declaring class + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch when private visibility is satisfied + emitter.label("1"); +} + +/// Emits an x86_64 exact-class check for a private static-property bridge hit. +fn emit_x86_64_private_static_property_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalStaticPropertySlot, + mode: &str, + target_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = x86_64_scope_offsets(mode); + let (label, len) = data.add_string(slot.declaring_class.as_bytes()); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction("jz 1f"); // skip private dispatch outside a class scope + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval class scope with the declaring class + emitter.instruction("test rax, rax"); // check whether the scope matched the declaring class + emitter.instruction(&format!("je {}", target_label)); // dispatch when private visibility is satisfied + emitter.label("1"); +} + +/// Returns ARM64 stack offsets for the class-scope pointer and length. +fn aarch64_scope_offsets(mode: &str) -> (usize, usize) { + match mode { + "get" => (32, 40), + "set" => (40, 48), + _ => unreachable!("eval static property helpers only use get/set modes"), + } +} + +/// Returns x86_64 frame offsets for the class-scope pointer and length. +fn x86_64_scope_offsets(mode: &str) -> (usize, usize) { + match mode { + "get" => (40, 48), + "set" => (48, 56), + _ => unreachable!("eval static property helpers only use get/set modes"), + } } /// Emits ARM64 get bodies for every bridge-supported static property slot. @@ -891,6 +988,15 @@ fn slot_body_label(module: &Module, slot: &EvalStaticPropertySlot, mode: &str) - format!("{}{}", slot_body_label_raw(slot, mode), suffix) } +/// Returns a platform-safe label for continuing after a private static-property name miss. +fn slot_access_miss_label( + module: &Module, + slot: &EvalStaticPropertySlot, + mode: &str, +) -> String { + format!("{}_access_miss", slot_body_label(module, slot, mode)) +} + /// Returns the architecture-independent body label stem for a static property slot. fn slot_body_label_raw(slot: &EvalStaticPropertySlot, mode: &str) -> String { format!( diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 48e062693e..ed68155425 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4571,6 +4571,51 @@ echo eval('$out = (EvalNullableIntStaticPropBox::$count === null) ? "N" : "n"; assert_eq!(out, "N:I9:I33:N"); } +/// Verifies eval fragments inherit native class scope for private AOT static properties. +#[test] +fn test_eval_fragment_can_mutate_private_aot_static_property_from_declaring_method() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject private AOT static properties outside the declaring scope. +#[test] +fn test_eval_fragment_rejects_private_aot_static_property_outside_declaring_scope() { + let err = compile_and_run_expect_failure( + r#"run(); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + /// Verifies eval fragments can replace public array AOT static properties. #[test] fn test_eval_fragment_can_mutate_aot_array_static_property() { From f874bcb4e0a138da79f82d7bd050c2f373748be4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 19:03:19 +0200 Subject: [PATCH 0600/1208] Support eval AOT protected property scope --- docs/php/eval.md | 13 +- src/codegen/eval_property_helpers.rs | 142 +++++++++++++++---- src/codegen/eval_static_property_helpers.rs | 118 ++++++++++++---- tests/codegen/eval.rs | 145 ++++++++++++++++++++ 4 files changed, 359 insertions(+), 59 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 1c02b57445..aa31e55161 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public scalar, nullable-int, string, Mixed, array, and object AOT property slots plus private AOT property slots when eval is executing in the declaring native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public scalar/nullable-int/string/Mixed/array/object AOT static property access plus private AOT static property access from the declaring native class scope, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | @@ -512,11 +512,12 @@ constants. Direct `new EnumName()` and property writes to enum cases are rejected. Public declared property reads/writes through `$this->property` from native -methods are bridged to eval, and private declared property reads/writes are -bridged when the eval fragment runs inside the declaring native class scope. -Public declared static property reads/writes are bridged to eval, and private -declared static property reads/writes are bridged from the declaring native -class scope. +methods are bridged to eval. Protected and private declared property +reads/writes are bridged when the eval fragment runs inside a native class scope +that satisfies PHP visibility for the declaring class. +Public declared static property reads/writes are bridged to eval. Protected and +private declared static property reads/writes are bridged from native class +scopes that satisfy PHP visibility for the declaring class. Public fixed scalar/nullable-int/Mixed/array/iterable/object method parameters through `$this->method(...)` are supported by the native method bridge, including registered named arguments and string-keyed unpacking; method diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index 2a5128209f..a98fa688e0 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -8,8 +8,8 @@ //! Key details: //! - The cacheable runtime object cannot know user class ids or property //! offsets, so these C-ABI symbols are emitted into the user assembly. -//! - Supported slots include public properties and private properties when the -//! active eval class scope exactly matches the declaring AOT class. +//! - Supported slots include public properties plus protected/private +//! properties when the active eval class scope satisfies PHP visibility. use std::collections::BTreeMap; @@ -28,6 +28,7 @@ struct EvalPropertySlot { class_id: u64, class_name: String, declaring_class: String, + allowed_scopes: Vec, property: String, visibility: Visibility, offset: usize, @@ -85,13 +86,14 @@ fn collect_eval_property_slots(module: &Module) -> Vec { let mut classes = module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_property_slots(class_name, class_info, &mut slots); + collect_class_property_slots(module, class_name, class_info, &mut slots); } slots } /// Adds bridge-supported properties for one class. fn collect_class_property_slots( + module: &Module, class_name: &str, class_info: &ClassInfo, slots: &mut Vec, @@ -113,6 +115,7 @@ fn collect_class_property_slots( class_id: class_info.class_id, class_name: class_name.to_string(), declaring_class: declaring_class.to_string(), + allowed_scopes: visibility_scope_names(module, declaring_class, visibility), property: property.clone(), visibility: visibility.clone(), offset: 8 + index * 16, @@ -131,7 +134,10 @@ fn property_visibility<'a>(class_info: &'a ClassInfo, property: &str) -> &'a Vis /// Returns true when the eval property bridge can enforce this visibility. fn property_visibility_supported(visibility: &Visibility) -> bool { - matches!(visibility, Visibility::Public | Visibility::Private) + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) } /// Returns true for property storage shapes the bridge can box and update. @@ -484,8 +490,10 @@ fn emit_aarch64_property_name_compare( } let miss_label = slot_access_miss_label(module, slot, mode); emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue property dispatch when names differ - emit_aarch64_private_property_scope_check(emitter, data, slot, mode, fail_label); - emitter.instruction(&format!("b {}", target_label)); // dispatch after private visibility is satisfied + let scope_ok_label = slot_scope_ok_label(module, slot, mode); + emit_aarch64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, fail_label); + emitter.label(&scope_ok_label); + emitter.instruction(&format!("b {}", target_label)); // dispatch after scoped visibility is satisfied emitter.label(&miss_label); } @@ -512,49 +520,63 @@ fn emit_x86_64_property_name_compare( } let miss_label = slot_access_miss_label(module, slot, mode); emitter.instruction(&format!("je {}", miss_label)); // continue property dispatch when names differ - emit_x86_64_private_property_scope_check(emitter, data, slot, mode, fail_label); - emitter.instruction(&format!("jmp {}", target_label)); // dispatch after private visibility is satisfied + let scope_ok_label = slot_scope_ok_label(module, slot, mode); + emit_x86_64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, fail_label); + emitter.label(&scope_ok_label); + emitter.instruction(&format!("jmp {}", target_label)); // dispatch after scoped visibility is satisfied emitter.label(&miss_label); } -/// Emits an ARM64 exact-class check for a private property bridge hit. -fn emit_aarch64_private_property_scope_check( +/// Emits ARM64 visibility checks for a protected/private property bridge hit. +fn emit_aarch64_property_scope_check( emitter: &mut Emitter, data: &mut DataSection, slot: &EvalPropertySlot, mode: &str, + success_label: &str, fail_label: &str, ) { let (scope_ptr_offset, scope_len_offset) = aarch64_scope_offsets(mode); - let (label, len) = data.add_string(slot.declaring_class.as_bytes()); emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length - emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject private property access outside a class scope - abi::emit_symbol_address(emitter, "x3", &label); - abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_strcasecmp"); // compare current eval class scope with the declaring class - emitter.instruction(&format!("cbnz x0, {}", fail_label)); // reject private property access from a different class + emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject scoped property access outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", success_label)); // accept access when the current scope is allowed + } + emitter.instruction(&format!("b {}", fail_label)); // reject scoped property access from unrelated classes } -/// Emits an x86_64 exact-class check for a private property bridge hit. -fn emit_x86_64_private_property_scope_check( +/// Emits x86_64 visibility checks for a protected/private property bridge hit. +fn emit_x86_64_property_scope_check( emitter: &mut Emitter, data: &mut DataSection, slot: &EvalPropertySlot, mode: &str, + success_label: &str, fail_label: &str, ) { let (scope_ptr_offset, scope_len_offset) = x86_64_scope_offsets(mode); - let (label, len) = data.add_string(slot.declaring_class.as_bytes()); - emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); //reload the active eval class-scope pointer - emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); //reload the active eval class-scope length + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope - emitter.instruction(&format!("jz {}", fail_label)); // reject private property access outside a class scope - abi::emit_symbol_address(emitter, "rdx", &label); - abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_strcasecmp"); // compare current eval class scope with the declaring class - emitter.instruction("test rax, rax"); // check whether the scope matched the declaring class - emitter.instruction(&format!("jne {}", fail_label)); // reject private property access from a different class + emitter.instruction(&format!("jz {}", fail_label)); // reject scoped property access outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", success_label)); // accept access when the current scope is allowed + } + emitter.instruction(&format!("jmp {}", fail_label)); // reject scoped property access from unrelated classes } /// Returns ARM64 stack offsets for the class-scope pointer and length. @@ -997,21 +1019,83 @@ fn slot_body_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> Stri format!("{}{}", slot_body_label_raw(slot, mode), suffix) } -/// Returns a platform-safe label for continuing after a private property name miss. +/// Returns a platform-safe label for continuing after a scoped property name miss. fn slot_access_miss_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> String { format!("{}_access_miss", slot_body_label(module, slot, mode)) } +/// Returns a platform-safe label for a successful scoped property visibility check. +fn slot_scope_ok_label(module: &Module, slot: &EvalPropertySlot, mode: &str) -> String { + format!("{}_scope_ok", slot_body_label(module, slot, mode)) +} + /// Returns the architecture-independent body label stem for a property slot. fn slot_body_label_raw(slot: &EvalPropertySlot, mode: &str) -> String { format!( - "__elephc_eval_property_{}_{}_{}", + "__elephc_eval_property_{}_{}_{}_{}", mode, label_fragment(&slot.class_name), + label_fragment(&slot.declaring_class), label_fragment(&slot.property) ) } +/// Returns class scopes that satisfy one member visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + /// Converts arbitrary PHP metadata names into assembly-label-safe fragments. fn label_fragment(value: &str) -> String { value diff --git a/src/codegen/eval_static_property_helpers.rs b/src/codegen/eval_static_property_helpers.rs index d3fcd8e0ab..349ea31d84 100644 --- a/src/codegen/eval_static_property_helpers.rs +++ b/src/codegen/eval_static_property_helpers.rs @@ -8,8 +8,8 @@ //! Key details: //! - The cacheable runtime object cannot know user static-property symbols, so //! these C-ABI bridge symbols are emitted into the user assembly. -//! - Supported slots include public static properties and private static -//! properties when the active eval class scope matches the declaring class. +//! - Supported slots include public static properties plus protected/private +//! static properties when the active eval class scope satisfies PHP visibility. //! - Null helper returns mean "no bridge match"; boxed PHP null is returned as //! a real Mixed cell pointer. @@ -30,6 +30,7 @@ use crate::types::{ClassInfo, PhpType}; struct EvalStaticPropertySlot { class_name: String, declaring_class: String, + allowed_scopes: Vec, property: String, visibility: Visibility, symbol: String, @@ -117,6 +118,7 @@ fn collect_class_static_property_slots( slots.push(EvalStaticPropertySlot { class_name: class_name.to_string(), declaring_class: declaring_class.to_string(), + allowed_scopes: visibility_scope_names(module, declaring_class, visibility), property: property.clone(), visibility: visibility.clone(), symbol: static_property_symbol(declaring_class, property), @@ -136,7 +138,10 @@ fn static_property_visibility<'a>(class_info: &'a ClassInfo, property: &str) -> /// Returns true when the eval static-property bridge can enforce this visibility. fn static_property_visibility_supported(visibility: &Visibility) -> bool { - matches!(visibility, Visibility::Public | Visibility::Private) + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) } /// Returns true for static-property storage shapes the bridge can box and update. @@ -403,7 +408,7 @@ fn emit_aarch64_static_property_name_compare( } let miss_label = slot_access_miss_label(module, slot, mode); emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue static-property dispatch when names differ - emit_aarch64_private_static_property_scope_check(emitter, data, slot, mode, &target_label); + emit_aarch64_static_property_scope_check(emitter, data, slot, mode, &target_label); emitter.label(&miss_label); } @@ -429,12 +434,12 @@ fn emit_x86_64_static_property_name_compare( } let miss_label = slot_access_miss_label(module, slot, mode); emitter.instruction(&format!("je {}", miss_label)); // continue static-property dispatch when names differ - emit_x86_64_private_static_property_scope_check(emitter, data, slot, mode, &target_label); + emit_x86_64_static_property_scope_check(emitter, data, slot, mode, &target_label); emitter.label(&miss_label); } -/// Emits an ARM64 exact-class check for a private static-property bridge hit. -fn emit_aarch64_private_static_property_scope_check( +/// Emits ARM64 visibility checks for a protected/private static-property bridge hit. +fn emit_aarch64_static_property_scope_check( emitter: &mut Emitter, data: &mut DataSection, slot: &EvalStaticPropertySlot, @@ -442,19 +447,23 @@ fn emit_aarch64_private_static_property_scope_check( target_label: &str, ) { let (scope_ptr_offset, scope_len_offset) = aarch64_scope_offsets(mode); - let (label, len) = data.add_string(slot.declaring_class.as_bytes()); emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length - emitter.instruction("cbz x1, 1f"); // skip private dispatch outside a class scope - abi::emit_symbol_address(emitter, "x3", &label); - abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_strcasecmp"); // compare current eval class scope with the declaring class - emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch when private visibility is satisfied + emitter.instruction("cbz x1, 1f"); // skip scoped dispatch outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch when scoped visibility is satisfied + } emitter.label("1"); } -/// Emits an x86_64 exact-class check for a private static-property bridge hit. -fn emit_x86_64_private_static_property_scope_check( +/// Emits x86_64 visibility checks for a protected/private static-property bridge hit. +fn emit_x86_64_static_property_scope_check( emitter: &mut Emitter, data: &mut DataSection, slot: &EvalStaticPropertySlot, @@ -462,16 +471,20 @@ fn emit_x86_64_private_static_property_scope_check( target_label: &str, ) { let (scope_ptr_offset, scope_len_offset) = x86_64_scope_offsets(mode); - let (label, len) = data.add_string(slot.declaring_class.as_bytes()); emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope - emitter.instruction("jz 1f"); // skip private dispatch outside a class scope - abi::emit_symbol_address(emitter, "rdx", &label); - abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_strcasecmp"); // compare current eval class scope with the declaring class - emitter.instruction("test rax, rax"); // check whether the scope matched the declaring class - emitter.instruction(&format!("je {}", target_label)); // dispatch when private visibility is satisfied + emitter.instruction("jz 1f"); // skip scoped dispatch outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", target_label)); // dispatch when scoped visibility is satisfied + } emitter.label("1"); } @@ -988,7 +1001,7 @@ fn slot_body_label(module: &Module, slot: &EvalStaticPropertySlot, mode: &str) - format!("{}{}", slot_body_label_raw(slot, mode), suffix) } -/// Returns a platform-safe label for continuing after a private static-property name miss. +/// Returns a platform-safe label for continuing after a scoped static-property name miss. fn slot_access_miss_label( module: &Module, slot: &EvalStaticPropertySlot, @@ -1000,13 +1013,70 @@ fn slot_access_miss_label( /// Returns the architecture-independent body label stem for a static property slot. fn slot_body_label_raw(slot: &EvalStaticPropertySlot, mode: &str) -> String { format!( - "__elephc_eval_static_property_{}_{}_{}", + "__elephc_eval_static_property_{}_{}_{}_{}", mode, label_fragment(&slot.class_name), + label_fragment(&slot.declaring_class), label_fragment(&slot.property) ) } +/// Returns class scopes that satisfy one member visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + /// Converts arbitrary PHP metadata names into assembly-label-safe fragments. fn label_fragment(value: &str) -> String { value diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ed68155425..d77c17d2e9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4469,6 +4469,77 @@ eval('return $box->x;'); ); } +/// Verifies eval fragments can access inherited protected AOT properties from child scopes. +#[test] +fn test_eval_fragment_can_mutate_protected_aot_property_from_child_method() { + let out = compile_and_run( + r#"x = $this->x + 4; return $this->x;'); + } +} + +$box = new EvalProtectedPropChild(); +$box->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments allow parent scopes to access child-declared protected AOT properties. +#[test] +fn test_eval_fragment_can_mutate_child_protected_aot_property_from_parent_method() { + let out = compile_and_run( + r#"x = $this->x + 4; return $this->x;'); + } +} + +class EvalParentScopeProtectedPropChild extends EvalParentScopeProtectedPropBase { + protected int $x = 1; +} + +$box = new EvalParentScopeProtectedPropChild(); +$box->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject protected AOT properties between sibling class scopes. +#[test] +fn test_eval_fragment_rejects_protected_aot_property_from_sibling_scope() { + let err = compile_and_run_expect_failure( + r#"x;'); + } +} + +$right = new EvalProtectedPropRight(); +$right->run(); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + /// Verifies eval fragments can read and write public nullable-int AOT properties through `$this`. #[test] fn test_eval_fragment_can_mutate_this_nullable_int_property() { @@ -4616,6 +4687,80 @@ $box->run(); ); } +/// Verifies eval fragments can access inherited protected AOT static properties from child scopes. +#[test] +fn test_eval_fragment_can_mutate_protected_aot_static_property_from_child_method() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments allow parent scopes to access child-declared protected AOT static properties. +#[test] +fn test_eval_fragment_can_mutate_child_protected_aot_static_property_from_parent_method() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject protected AOT static properties between sibling class scopes. +#[test] +fn test_eval_fragment_rejects_protected_aot_static_property_from_sibling_scope() { + let err = compile_and_run_expect_failure( + r#"run(); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + /// Verifies eval fragments can replace public array AOT static properties. #[test] fn test_eval_fragment_can_mutate_aot_array_static_property() { From 9458098f0f026b8d1df89ae5dda5c32a51683cc8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 19:27:30 +0200 Subject: [PATCH 0601/1208] Support eval AOT non-public method scope --- .../src/interpreter/runtime_ops.rs | 2 +- .../src/runtime_hooks/externs.rs | 4 + .../elephc-magician/src/runtime_hooks/ops.rs | 8 +- docs/php/eval.md | 36 +- src/codegen/eval_method_helpers.rs | 308 +++++++++++++++--- src/codegen/lower_inst/builtins/eval.rs | 31 +- tests/codegen/eval.rs | 240 ++++++++++++++ 7 files changed, 555 insertions(+), 74 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index bab3882788..5ee45eac7b 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -118,7 +118,7 @@ pub trait RuntimeValueOps { args: Vec, ) -> Result; - /// Calls a named public static method through the generated AOT bridge. + /// Calls a named static method through the generated AOT bridge. fn static_method_call( &mut self, class_name: &str, diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index eb98176a92..4924cd4e93 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -86,6 +86,8 @@ unsafe extern "C" { name_ptr: *const u8, name_len: u64, args: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_static_method_call( class_ptr: *const u8, @@ -93,6 +95,8 @@ unsafe extern "C" { name_ptr: *const u8, name_len: u64, args: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_reflection_attribute_new( name_ptr: *const u8, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index b06befdf0d..32a159d9aa 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -209,6 +209,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { method: &str, args: Vec, ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); let arg_array = Self::arg_array(args)?; let result = Self::handle(unsafe { __elephc_eval_value_method_call( @@ -216,6 +217,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { method.as_ptr(), method.len() as u64, arg_array.as_ptr(), + scope_ptr, + scope_len, ) }); unsafe { @@ -224,13 +227,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { result } - /// Calls a public AOT static method through the generated user helper. + /// Calls an AOT static method through the generated user helper. fn static_method_call( &mut self, class_name: &str, method: &str, args: Vec, ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); let arg_array = Self::arg_array(args)?; let result = Self::handle(unsafe { __elephc_eval_value_static_method_call( @@ -239,6 +243,8 @@ impl RuntimeValueOps for ElephcRuntimeOps { method.as_ptr(), method.len() as u64, arg_array.as_ptr(), + scope_ptr, + scope_len, ) }); unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index aa31e55161..97e00cfe1e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -76,7 +76,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameters; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -127,9 +127,10 @@ as callable. Static method callables can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method -fallback supports the same named-argument and positional variadic-tail binding for public -non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter signatures supported by the -generated bridge. +fallback supports the same named-argument and positional variadic-tail binding +for public/protected/private non-by-reference scalar/nullable-int/Mixed/array/iterable/object +parameter signatures when the current eval class scope satisfies PHP visibility +for the generated bridge. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -518,10 +519,12 @@ that satisfies PHP visibility for the declaring class. Public declared static property reads/writes are bridged to eval. Protected and private declared static property reads/writes are bridged from native class scopes that satisfy PHP visibility for the declaring class. -Public fixed scalar/nullable-int/Mixed/array/iterable/object method parameters -through `$this->method(...)` are supported by the native method -bridge, including registered named arguments and string-keyed unpacking; method -returns may be scalar/nullable-int/Mixed/array/iterable/object values. +Public/protected/private fixed scalar/nullable-int/Mixed/array/iterable/object +method parameters through `$this->method(...)` and `Class::method(...)` are +supported by the native method bridge when the eval fragment runs inside a +native class scope that satisfies PHP visibility for the declaring class, +including registered named arguments and string-keyed unpacking; method returns +may be scalar/nullable-int/Mixed/array/iterable/object values. ## Namespaces and constants @@ -641,11 +644,14 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are -still outside eval fragments. Runtime/AOT object-method, static-method, and -constructor fallback from eval remain limited to generated public -non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter bridge signatures, including the -generated positional variadic array slot when present. By-reference parameters and broader -parameter/return ABI shapes are still outside those bridge paths. +still outside eval fragments. Runtime/AOT object-method and static-method +fallback from eval remain limited to generated visibility-checked +non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter +bridge signatures, including the generated positional variadic array slot when +present. Constructor fallback remains limited to generated public +non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter +bridge signatures. By-reference parameters and broader parameter/return ABI +shapes are still outside those bridge paths. Eval class support is still smaller than the full static class system. The main remaining class-system gaps are broader reflection APIs beyond the supported @@ -655,8 +661,8 @@ property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional -`new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current public -non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and visibility-checked +`new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current visibility-checked +non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT method/property attributes are exposed for registered metadata slices, while diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index f3c533f48e..3f0cc5ace4 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -8,8 +8,8 @@ //! Key details: //! - The cacheable runtime object cannot know user class ids, method symbols, //! or return types, so this bridge is emitted into the user assembly. -//! - This method-call slice supports public AOT methods plus non-public -//! `__clone` hooks after interpreter-side visibility checks. +//! - This method-call slice supports public methods plus protected/private +//! methods when the active eval class scope satisfies PHP visibility. use std::collections::BTreeMap; @@ -19,7 +19,7 @@ use crate::codegen::emit::Emitter; use crate::codegen::emit_box_current_value_as_mixed; use crate::codegen::platform::Arch; use crate::ir::{Function, LocalKind, Module}; -use crate::names::{method_symbol, php_symbol_key, static_method_symbol}; +use crate::names::{method_symbol, static_method_symbol}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -30,6 +30,8 @@ struct EvalMethodSlot { class_name: String, method: String, impl_class: String, + visibility: Visibility, + allowed_scopes: Vec, params: Vec, return_ty: PhpType, } @@ -41,6 +43,8 @@ struct EvalStaticMethodSlot { class_name: String, method: String, impl_class: String, + visibility: Visibility, + allowed_scopes: Vec, params: Vec, return_ty: PhpType, } @@ -122,19 +126,19 @@ fn collect_eval_method_slots(module: &Module) -> Vec { let mut classes = module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_method_slots(class_name, class_info, &emitted_methods, &mut slots); + collect_class_method_slots(module, class_name, class_info, &emitted_methods, &mut slots); } slots } -/// Collects public bridge-supported static methods backed by emitted EIR symbols. +/// Collects bridge-supported static methods backed by emitted EIR symbols. fn collect_eval_static_method_slots(module: &Module) -> Vec { let emitted_methods = super::eir_class_method_keys(module); let mut slots = Vec::new(); let mut classes = module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_static_method_slots(class_name, class_info, &emitted_methods, &mut slots); + collect_class_static_method_slots(module, class_name, class_info, &emitted_methods, &mut slots); } slots } @@ -153,6 +157,7 @@ fn collect_builtin_throwable_method_class_ids(module: &Module) -> Vec { /// Adds bridge-supported instance methods for one class. fn collect_class_method_slots( + module: &Module, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -161,7 +166,8 @@ fn collect_class_method_slots( let mut methods = class_info.methods.iter().collect::>(); methods.sort_by_key(|(method, _)| method.as_str()); for (method, sig) in methods { - if !method_can_dispatch_through_eval_bridge(class_info, method) + let visibility = method_visibility(class_info, method); + if !method_visibility_supported(visibility) || !method_signature_supported(sig) || !method_return_supported(&sig.return_type) { @@ -180,14 +186,17 @@ fn collect_class_method_slots( class_name: class_name.to_string(), method: method.clone(), impl_class: impl_class.to_string(), + visibility: visibility.clone(), + allowed_scopes: visibility_scope_names(module, impl_class, visibility), params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), }); } } -/// Adds bridge-supported public static methods for one class. +/// Adds bridge-supported static methods for one class. fn collect_class_static_method_slots( + module: &Module, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -196,7 +205,8 @@ fn collect_class_static_method_slots( let mut methods = class_info.static_methods.iter().collect::>(); methods.sort_by_key(|(method, _)| method.as_str()); for (method, sig) in methods { - if !static_method_is_public(class_info, method) + let visibility = static_method_visibility(class_info, method); + if !method_visibility_supported(visibility) || !method_signature_supported(sig) || !method_return_supported(&sig.return_type) { @@ -215,31 +225,36 @@ fn collect_class_static_method_slots( class_name: class_name.to_string(), method: method.clone(), impl_class: impl_class.to_string(), + visibility: visibility.clone(), + allowed_scopes: visibility_scope_names(module, impl_class, visibility), params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), }); } } -/// Returns true when an instance method can be reached through eval's native bridge. -fn method_can_dispatch_through_eval_bridge(class_info: &ClassInfo, method: &str) -> bool { - method_is_public(class_info, method) || php_symbol_key(method) == "__clone" -} - -/// Returns true when a method is publicly visible to runtime eval. -fn method_is_public(class_info: &ClassInfo, method: &str) -> bool { +/// Returns the declared instance-method visibility, defaulting to public metadata. +fn method_visibility<'a>(class_info: &'a ClassInfo, method: &str) -> &'a Visibility { class_info .method_visibilities .get(method) - .is_none_or(|visibility| matches!(visibility, Visibility::Public)) + .unwrap_or(&Visibility::Public) } -/// Returns true when a static method is publicly visible to runtime eval. -fn static_method_is_public(class_info: &ClassInfo, method: &str) -> bool { +/// Returns the declared static-method visibility, defaulting to public metadata. +fn static_method_visibility<'a>(class_info: &'a ClassInfo, method: &str) -> &'a Visibility { class_info .static_method_visibilities .get(method) - .is_none_or(|visibility| matches!(visibility, Visibility::Public)) + .unwrap_or(&Visibility::Public) +} + +/// Returns true when the eval method bridge can enforce this visibility. +fn method_visibility_supported(visibility: &Visibility) -> bool { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) } /// Returns true for method signatures supported by the eval bridge. @@ -285,7 +300,7 @@ fn method_return_supported(ty: &PhpType) -> bool { ) } -/// Emits `__elephc_eval_value_method_call(Mixed*, name, len, MixedArray*) -> Mixed*`. +/// Emits `__elephc_eval_value_method_call(Mixed*, name, len, MixedArray*, scope, scope_len) -> Mixed*`. fn emit_method_call_helper( module: &Module, emitter: &mut Emitter, @@ -306,7 +321,7 @@ fn emit_method_call_helper( } } -/// Emits `__elephc_eval_value_static_method_call(class, method, MixedArray*) -> Mixed*`. +/// Emits `__elephc_eval_value_static_method_call(class, method, MixedArray*, scope, scope_len) -> Mixed*`. fn emit_static_method_call_helper( module: &Module, emitter: &mut Emitter, @@ -331,22 +346,24 @@ fn emit_static_method_call_aarch64( ) { let fail_label = "__elephc_eval_value_static_method_call_fail"; let done_label = "__elephc_eval_value_static_method_call_done"; - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class, method, args, and fp/lr - emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for class, method, args, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length emitter.instruction("str x2, [sp, #16]"); // save the requested method-name pointer emitter.instruction("str x4, [sp, #24]"); // save the boxed eval argument array + emitter.instruction("str x5, [sp, #32]"); // save the active eval class-scope pointer emitter.instruction("str x3, [sp, #40]"); // save the requested method-name length - emit_aarch64_static_method_dispatch(module, emitter, data, slots); - emitter.instruction(&format!("b {}", fail_label)); // no supported public static method matched the request + emitter.instruction("str x6, [sp, #48]"); // save the active eval class-scope length + emit_aarch64_static_method_dispatch(module, emitter, data, slots, fail_label); + emitter.instruction(&format!("b {}", fail_label)); // no supported static method matched the request emit_aarch64_static_method_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame emitter.instruction("ret"); // return the boxed static method result to Rust } @@ -361,14 +378,17 @@ fn emit_static_method_call_x86_64( let done_label = "__elephc_eval_value_static_method_call_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, method, args, and one argument + emitter.instruction("sub rsp, 80"); // reserve aligned slots for class, method, args, scope, and one argument emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested method-name pointer emitter.instruction("mov QWORD PTR [rbp - 32], r8"); // save the boxed eval argument array emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save the requested method-name length - emit_x86_64_static_method_dispatch(module, emitter, data, slots); - emitter.instruction(&format!("jmp {}", fail_label)); // no supported public static method matched the request + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope pointer + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval class-scope length stack argument + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the active eval class-scope length + emit_x86_64_static_method_dispatch(module, emitter, data, slots, fail_label); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported static method matched the request emit_x86_64_static_method_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure @@ -388,12 +408,14 @@ fn emit_method_call_aarch64( ) { let fail_label = "__elephc_eval_value_method_call_fail"; let done_label = "__elephc_eval_value_method_call_done"; - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for inputs, object, and fp/lr + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for inputs, object, scope, and fp/lr emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer emitter.instruction("str x1, [sp, #0]"); // save the requested method-name pointer emitter.instruction("str x2, [sp, #8]"); // save the requested method-name length emitter.instruction("str x3, [sp, #24]"); // save the boxed eval argument array + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot dispatch a method emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object @@ -405,7 +427,7 @@ fn emit_method_call_aarch64( data, builtin_throwable_class_ids, ); - emit_aarch64_method_dispatch(module, emitter, data, slots); + emit_aarch64_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("b {}", fail_label)); // no supported method matched the request emit_aarch64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); emit_aarch64_method_bodies(module, emitter, data, slots, done_label, fail_label); @@ -429,10 +451,12 @@ fn emit_method_call_x86_64( let done_label = "__elephc_eval_value_method_call_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, length, object, args, and first argument + emitter.instruction("sub rsp, 64"); // reserve aligned slots for name, length, object, args, scope, and first argument emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested method-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested method-name length emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed eval argument array + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope length emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot dispatch a method emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register @@ -446,7 +470,7 @@ fn emit_method_call_x86_64( data, builtin_throwable_class_ids, ); - emit_x86_64_method_dispatch(module, emitter, data, slots); + emit_x86_64_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("jmp {}", fail_label)); // no supported method matched the request emit_x86_64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); emit_x86_64_method_bodies(module, emitter, data, slots, done_label, fail_label); @@ -464,6 +488,7 @@ fn emit_aarch64_method_dispatch( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalMethodSlot], + fail_label: &str, ) { for (class_id, class_slots) in grouped_slots(slots) { let next_label = format!("__elephc_eval_method_next_{}", class_id); @@ -473,7 +498,7 @@ fn emit_aarch64_method_dispatch( emitter.instruction("cmp x9, x10"); // compare receiver class id against this eval bridge class emitter.instruction(&format!("b.ne {}", next_label)); // try the next class when ids differ for slot in class_slots { - emit_aarch64_method_name_compare(module, emitter, data, slot); + emit_aarch64_method_name_compare(module, emitter, data, slot, fail_label); } emitter.label(&next_label); } @@ -485,6 +510,7 @@ fn emit_x86_64_method_dispatch( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalMethodSlot], + fail_label: &str, ) { for (class_id, class_slots) in grouped_slots(slots) { let next_label = format!("__elephc_eval_method_next_{}_x", class_id); @@ -494,7 +520,7 @@ fn emit_x86_64_method_dispatch( emitter.instruction("cmp r11, r10"); // compare receiver class id against this eval bridge class emitter.instruction(&format!("jne {}", next_label)); // try the next class when ids differ for slot in class_slots { - emit_x86_64_method_name_compare(module, emitter, data, slot); + emit_x86_64_method_name_compare(module, emitter, data, slot, fail_label); } emitter.label(&next_label); } @@ -506,6 +532,7 @@ fn emit_aarch64_static_method_dispatch( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalStaticMethodSlot], + fail_label: &str, ) { for (class_name, class_slots) in grouped_static_slots(slots) { let next_label = format!( @@ -514,7 +541,7 @@ fn emit_aarch64_static_method_dispatch( ); emit_aarch64_static_class_name_compare(emitter, data, class_name, &next_label); for slot in class_slots { - emit_aarch64_static_method_name_compare(module, emitter, data, slot); + emit_aarch64_static_method_name_compare(module, emitter, data, slot, fail_label); } emitter.label(&next_label); } @@ -526,6 +553,7 @@ fn emit_x86_64_static_method_dispatch( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalStaticMethodSlot], + fail_label: &str, ) { for (class_name, class_slots) in grouped_static_slots(slots) { let next_label = format!( @@ -534,7 +562,7 @@ fn emit_x86_64_static_method_dispatch( ); emit_x86_64_static_class_name_compare(emitter, data, class_name, &next_label); for slot in class_slots { - emit_x86_64_static_method_name_compare(module, emitter, data, slot); + emit_x86_64_static_method_name_compare(module, emitter, data, slot, fail_label); } emitter.label(&next_label); } @@ -678,15 +706,23 @@ fn emit_aarch64_method_name_compare( emitter: &mut Emitter, data: &mut DataSection, slot: &EvalMethodSlot, + fail_label: &str, ) { let (label, len) = data.add_string(slot.method.as_bytes()); emitter.instruction("ldr x1, [sp, #0]"); // reload requested method-name pointer emitter.instruction("ldr x2, [sp, #8]"); // reload requested method-name length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_strcasecmp"); // compare public method names with PHP case-insensitive rules + emitter.instruction("bl __rt_strcasecmp"); // compare method names with PHP case-insensitive rules let target_label = method_body_label(module, slot); - emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the method body when the names match + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the method body when the names match + return; + } + let miss_label = method_access_miss_label(module, slot); + emitter.instruction(&format!("cbnz x0, {}", miss_label)); // continue method dispatch when names differ + emit_aarch64_method_scope_check(emitter, data, &slot.allowed_scopes, false, &target_label, fail_label); + emitter.label(&miss_label); } /// Emits one x86_64 method-name comparison and branch to the matching body. @@ -695,15 +731,24 @@ fn emit_x86_64_method_name_compare( emitter: &mut Emitter, data: &mut DataSection, slot: &EvalMethodSlot, + fail_label: &str, ) { let (label, len) = data.add_string(slot.method.as_bytes()); emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested method-name pointer emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested method-name length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_strcasecmp"); // compare public method names with PHP case-insensitive rules + emitter.instruction("call __rt_strcasecmp"); // compare method names with PHP case-insensitive rules emitter.instruction("test rax, rax"); // check whether the method names matched - emitter.instruction(&format!("je {}", method_body_label(module, slot))); // dispatch to the method body when the names match + let target_label = method_body_label(module, slot); + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("je {}", target_label)); // dispatch to the method body when the names match + return; + } + let miss_label = method_access_miss_label(module, slot); + emitter.instruction(&format!("jne {}", miss_label)); // continue method dispatch when names differ + emit_x86_64_method_scope_check(emitter, data, &slot.allowed_scopes, false, &target_label, fail_label); + emitter.label(&miss_label); } /// Emits one ARM64 static method-name comparison and branch to the matching body. @@ -712,6 +757,7 @@ fn emit_aarch64_static_method_name_compare( emitter: &mut Emitter, data: &mut DataSection, slot: &EvalStaticMethodSlot, + fail_label: &str, ) { let (label, len) = data.add_string(slot.method.as_bytes()); emitter.instruction("ldr x1, [sp, #16]"); // reload requested static method-name pointer @@ -720,7 +766,14 @@ fn emit_aarch64_static_method_name_compare( abi::emit_load_int_immediate(emitter, "x4", len as i64); emitter.instruction("bl __rt_strcasecmp"); // compare static method names with PHP case-insensitive rules let target_label = static_method_body_label(module, slot); - emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the static method body when the names match + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch to the static method body when the names match + return; + } + let miss_label = static_method_access_miss_label(module, slot); + emitter.instruction(&format!("cbnz x0, {}", miss_label)); // continue static method dispatch when names differ + emit_aarch64_method_scope_check(emitter, data, &slot.allowed_scopes, true, &target_label, fail_label); + emitter.label(&miss_label); } /// Emits one x86_64 static method-name comparison and branch to the matching body. @@ -729,6 +782,7 @@ fn emit_x86_64_static_method_name_compare( emitter: &mut Emitter, data: &mut DataSection, slot: &EvalStaticMethodSlot, + fail_label: &str, ) { let (label, len) = data.add_string(slot.method.as_bytes()); emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested static method-name pointer @@ -738,7 +792,84 @@ fn emit_x86_64_static_method_name_compare( emitter.instruction("call __rt_strcasecmp"); // compare static method names with PHP case-insensitive rules emitter.instruction("test rax, rax"); // check whether the static method names matched let target_label = static_method_body_label(module, slot); - emitter.instruction(&format!("je {}", target_label)); // dispatch to the static method body when the names match + if matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("je {}", target_label)); // dispatch to the static method body when the names match + return; + } + let miss_label = static_method_access_miss_label(module, slot); + emitter.instruction(&format!("jne {}", miss_label)); // continue static method dispatch when names differ + emit_x86_64_method_scope_check(emitter, data, &slot.allowed_scopes, true, &target_label, fail_label); + emitter.label(&miss_label); +} + +/// Emits ARM64 visibility checks for a protected/private method bridge hit. +fn emit_aarch64_method_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + allowed_scopes: &[String], + is_static: bool, + success_label: &str, + fail_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = aarch64_method_scope_offsets(is_static); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject scoped method access outside a class scope + for scope_name in allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", success_label)); // dispatch when scoped visibility is satisfied + } + emitter.instruction(&format!("b {}", fail_label)); // reject scoped method access from unrelated classes +} + +/// Emits x86_64 visibility checks for a protected/private method bridge hit. +fn emit_x86_64_method_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + allowed_scopes: &[String], + is_static: bool, + success_label: &str, + fail_label: &str, +) { + let (scope_ptr_offset, scope_len_offset) = x86_64_method_scope_offsets(is_static); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", fail_label)); // reject scoped method access outside a class scope + for scope_name in allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", success_label)); // dispatch when scoped visibility is satisfied + } + emitter.instruction(&format!("jmp {}", fail_label)); // reject scoped method access from unrelated classes +} + +/// Returns ARM64 stack offsets for method class-scope pointer and length. +fn aarch64_method_scope_offsets(is_static: bool) -> (usize, usize) { + if is_static { + (32, 48) + } else { + (32, 40) + } +} + +/// Returns x86_64 frame offsets for method class-scope pointer and length. +fn x86_64_method_scope_offsets(is_static: bool) -> (usize, usize) { + if is_static { + (56, 64) + } else { + (48, 56) + } } /// Emits ARM64 bodies for compact Throwable methods used by eval. @@ -997,7 +1128,7 @@ fn emit_aarch64_prepare_method_args( emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first method argument abi::emit_push_result_value(emitter, &receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { - emit_aarch64_load_eval_arg(module, emitter, index); + emit_aarch64_load_eval_arg(module, emitter, index, 24); let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); @@ -1016,7 +1147,7 @@ fn emit_aarch64_prepare_static_method_args( abi::emit_load_int_immediate(emitter, "x0", slot.class_id as i64); abi::emit_push_result_value(emitter, &PhpType::Int); for (index, param_ty) in slot.params.iter().enumerate() { - emit_aarch64_load_eval_arg(module, emitter, index); + emit_aarch64_load_eval_arg(module, emitter, index, 40); let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); @@ -1091,14 +1222,19 @@ fn materialize_static_method_args( } /// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. -fn emit_aarch64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { +fn emit_aarch64_load_eval_arg( + module: &Module, + emitter: &mut Emitter, + index: usize, + arg_array_frame_offset: usize, +) { let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); abi::emit_load_int_immediate(emitter, "x0", index as i64); abi::emit_call_label(emitter, &value_int_symbol); emitter.instruction("str x0, [x29, #-16]"); // save the boxed index while loading from the argument array emitter.instruction("ldr x1, [x29, #-16]"); // pass the boxed index to the eval array reader - emitter.instruction("ldr x0, [x29, #-24]"); // pass the eval argument array to the reader + emitter.instruction(&format!("ldr x0, [x29, #-{}]", arg_array_frame_offset)); // pass the eval argument array to the reader abi::emit_call_label(emitter, &array_get_symbol); emitter.instruction("str x0, [x29, #-16]"); // save the boxed eval argument for coercion } @@ -1476,13 +1612,19 @@ fn method_body_label(module: &Module, slot: &EvalMethodSlot) -> String { Arch::X86_64 => "_x", }; format!( - "__elephc_eval_method_{}_{}{}", + "__elephc_eval_method_{}_{}_{}{}", label_fragment(&slot.class_name), + label_fragment(&slot.impl_class), label_fragment(&slot.method), suffix ) } +/// Returns a platform-safe label for continuing after a scoped method name miss. +fn method_access_miss_label(module: &Module, slot: &EvalMethodSlot) -> String { + format!("{}_access_miss", method_body_label(module, slot)) +} + /// Returns a platform-safe body label for a static method slot. fn static_method_body_label(module: &Module, slot: &EvalStaticMethodSlot) -> String { let suffix = match module.target.arch { @@ -1490,13 +1632,75 @@ fn static_method_body_label(module: &Module, slot: &EvalStaticMethodSlot) -> Str Arch::X86_64 => "_x", }; format!( - "__elephc_eval_static_method_{}_{}{}", + "__elephc_eval_static_method_{}_{}_{}{}", label_fragment(&slot.class_name), + label_fragment(&slot.impl_class), label_fragment(&slot.method), suffix ) } +/// Returns a platform-safe label for continuing after a scoped static method name miss. +fn static_method_access_miss_label(module: &Module, slot: &EvalStaticMethodSlot) -> String { + format!("{}_access_miss", static_method_body_label(module, slot)) +} + +/// Returns class scopes that satisfy one method visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + /// Converts arbitrary PHP metadata names into assembly-label-safe fragments. fn label_fragment(value: &str) -> String { value diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index df85fd155d..bd2ecdf6db 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -916,7 +916,7 @@ fn collect_eval_native_instance_methods( if method_name == "__construct" { continue; } - let bridge_supported = class_method_is_public(class_info, method_name) + let bridge_supported = class_method_visibility_bridge_supported(class_info, method_name) && method_signature_can_bridge_with_eval(signature); registrations.push(EvalNativeMethodRegistration { class_name: class_name.to_string(), @@ -937,7 +937,7 @@ fn collect_eval_native_static_methods( let mut methods = class_info.static_methods.iter().collect::>(); methods.sort_by_key(|(method, _)| method.as_str()); for (method_name, signature) in methods { - let bridge_supported = class_static_method_is_public(class_info, method_name) + let bridge_supported = class_static_method_visibility_bridge_supported(class_info, method_name) && method_signature_can_bridge_with_eval(signature); registrations.push(EvalNativeMethodRegistration { class_name: class_name.to_string(), @@ -1276,12 +1276,33 @@ fn class_method_is_public(class_info: &ClassInfo, method_name: &str) -> bool { .is_none_or(|visibility| matches!(visibility, Visibility::Public)) } -/// Returns true when a static method is public in the class metadata. -fn class_static_method_is_public(class_info: &ClassInfo, method_name: &str) -> bool { +/// Returns true when eval can enforce this instance method visibility in the bridge. +fn class_method_visibility_bridge_supported(class_info: &ClassInfo, method_name: &str) -> bool { + class_info + .method_visibilities + .get(method_name) + .is_none_or(|visibility| { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) + }) +} + +/// Returns true when eval can enforce this static method visibility in the bridge. +fn class_static_method_visibility_bridge_supported( + class_info: &ClassInfo, + method_name: &str, +) -> bool { class_info .static_method_visibilities .get(method_name) - .is_none_or(|visibility| matches!(visibility, Visibility::Public)) + .is_none_or(|visibility| { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) + }) } /// Emits one native-function registration call into the just-created eval context. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d77c17d2e9..6019d87642 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4849,6 +4849,125 @@ $box->run(); assert_eq!(out, "42"); } +/// Verifies eval fragments can call private AOT instance methods from the declaring scope. +#[test] +fn test_eval_fragment_can_call_private_aot_method_from_declaring_scope() { + let out = compile_and_run( + r#"secret(3);'); + } +} + +(new EvalPrivateAotMethodBox())->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval callable arrays can call private AOT instance methods from the declaring scope. +#[test] +fn test_eval_fragment_callable_array_can_call_private_aot_method_from_declaring_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject private AOT instance methods from child scopes. +#[test] +fn test_eval_fragment_rejects_private_aot_method_from_child_scope() { + let err = compile_and_run_expect_failure( + r#"secret(3);'); + } +} + +(new EvalPrivateAotMethodChild())->run(); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + +/// Verifies eval fragments can call inherited protected AOT methods from child scopes. +#[test] +fn test_eval_fragment_can_call_protected_aot_method_from_child_scope() { + let out = compile_and_run( + r#"add(3);'); + } +} + +(new EvalProtectedAotMethodChild())->run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject protected AOT instance methods between sibling scopes. +#[test] +fn test_eval_fragment_rejects_protected_aot_method_from_sibling_scope() { + let err = compile_and_run_expect_failure( + r#"add(3);'); + } +} + +(new EvalProtectedAotMethodRight())->run(); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + /// Verifies eval fragments pass one scalar argument to public AOT methods through `$this`. #[test] fn test_eval_fragment_can_call_this_public_one_arg_method() { @@ -5316,6 +5435,127 @@ echo EvalAotStaticBox::sum4(1, 2, 3, 4);'); assert_eq!(out.stdout, "AB:CD:EF:7:10"); } +/// Verifies eval fragments can call private AOT static methods from the declaring scope. +#[test] +fn test_eval_fragment_can_call_private_aot_static_method_from_declaring_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject private AOT static methods from child scopes. +#[test] +fn test_eval_fragment_rejects_private_aot_static_method_from_child_scope() { + let err = compile_and_run_expect_failure( + r#"run(); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + +/// Verifies eval fragments can call inherited protected AOT static methods from child scopes. +#[test] +fn test_eval_fragment_can_call_protected_aot_static_method_from_child_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval static method callables can call protected AOT methods from child scopes. +#[test] +fn test_eval_fragment_callable_can_call_protected_aot_static_method_from_child_scope() { + let out = compile_and_run( + r#"run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval fragments reject protected AOT static methods between sibling scopes. +#[test] +fn test_eval_fragment_rejects_protected_aot_static_method_from_sibling_scope() { + let err = compile_and_run_expect_failure( + r#"run(); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + /// Verifies eval static dispatch passes AOT static method arguments on the caller stack. #[test] fn test_eval_fragment_dispatches_aot_static_method_with_stack_string_arg() { From 27681d50e74e1c5361491ae683260f6fd3a1f5a5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 19:35:12 +0200 Subject: [PATCH 0602/1208] Support eval AOT reflection invoke visibility bypass --- .../src/interpreter/reflection.rs | 28 ++++++++++++++--- docs/php/eval.md | 7 +++-- tests/codegen/eval.rs | 31 ++++++++++++++++++- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index a817ac710b..91c7981425 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -5399,7 +5399,7 @@ fn eval_reflection_function_invoke_dispatch( eval_callable_with_call_array_args(&function_key, function_args, context, values) } -/// Dispatches one reflected method invocation through eval or public AOT bridges. +/// Dispatches one reflected method invocation through eval or AOT bridges. fn eval_reflection_method_invoke_dispatch( declaring_class: &str, method_name: &str, @@ -5478,7 +5478,7 @@ fn eval_reflection_method_instance_called_class( Ok(object_class_name) } -/// Invokes one reflected generated/AOT method when it fits the public bridge slice. +/// Invokes one reflected generated/AOT method when it fits the bridge slice. fn eval_reflection_aot_method_invoke_dispatch( declaring_class: &str, method_name: &str, @@ -5490,7 +5490,7 @@ fn eval_reflection_aot_method_invoke_dispatch( let member = eval_reflection_aot_method_metadata_if_exists(declaring_class, method_name, values)? .ok_or(EvalStatus::RuntimeFatal)?; - if member.visibility != EvalVisibility::Public || member.is_abstract { + if member.is_abstract { return Err(EvalStatus::RuntimeFatal); } if member.is_static { @@ -5499,7 +5499,9 @@ fn eval_reflection_aot_method_invoke_dispatch( method_args, values, )?; - return values.static_method_call(declaring_class, method_name, args); + return eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.static_method_call(declaring_class, method_name, args) + }); } if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { return Err(EvalStatus::RuntimeFatal); @@ -5519,7 +5521,23 @@ fn eval_reflection_aot_method_invoke_dispatch( method_args, values, )?; - values.method_call(object, method_name, args) + eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.method_call(object, method_name, args) + }) +} + +/// Runs a reflected AOT invocation with the declaring class as visibility scope. +fn eval_reflection_with_declaring_class_scope( + declaring_class: &str, + context: &mut ElephcEvalContext, + action: impl FnOnce() -> Result, +) -> Result { + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + let result = action(); + context.pop_called_class_scope(); + context.pop_class_scope(); + result } /// Reads one eval instance property through ReflectionProperty semantics. diff --git a/docs/php/eval.md b/docs/php/eval.md index 97e00cfe1e..faba3af7d5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -326,9 +326,10 @@ tracked reflectors when the reflected method has declared parameter types; the lowered call supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, and named arguments. Generated/AOT -invoke targets whose parameter types only come from call-site inference and -runtime-only or non-literal argument arrays still require richer -runtime/typechecker support. +bridge-supported invoke targets also bypass public/protected/private visibility +like PHP reflection. Generated/AOT invoke targets whose parameter types only +come from call-site inference and runtime-only or non-literal argument arrays +still require richer runtime/typechecker support. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6019d87642..e8d631fc38 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8975,7 +8975,7 @@ echo $listed->getDeclaringClass()->getName(); ); } -/// Verifies eval ReflectionMethod::invoke can dispatch public generated/AOT methods. +/// Verifies eval ReflectionMethod::invoke can dispatch generated/AOT methods. #[test] fn test_eval_reflection_method_invoke_calls_aot_method() { let out = compile_and_run_capture( @@ -9016,6 +9016,35 @@ return $join->invokeArgs($object, ["b" => "2", "a" => "1"]);'); ); } +/// Verifies eval ReflectionMethod::invoke bypasses generated/AOT method visibility. +#[test] +fn test_eval_reflection_method_invoke_bypasses_aot_method_visibility() { + let out = compile_and_run_capture( + r#"invoke($object, 3) . ":" . $label->invoke(null, "x");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5:Hx"); +} + /// Verifies eval ReflectionMethod exposes registered generated/AOT parameter metadata. #[test] fn test_eval_reflection_method_exposes_aot_parameter_metadata() { From c8fc633d558c3e03b4a214cd167f05aedaf6b35b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 19:54:08 +0200 Subject: [PATCH 0603/1208] Support eval AOT constructor visibility --- .../src/interpreter/statements.rs | 44 ++-- .../src/runtime_hooks/externs.rs | 2 + .../elephc-magician/src/runtime_hooks/ops.rs | 13 +- docs/php/eval.md | 19 +- src/codegen/eval_constructor_helpers.rs | 189 ++++++++++++++++-- src/codegen/lower_inst/builtins/eval.rs | 10 +- tests/codegen/eval.rs | 137 +++++++++++++ 7 files changed, 359 insertions(+), 55 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 5ff2470198..701978a7e1 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3843,27 +3843,25 @@ fn eval_reflection_class_new_instance_result( return Ok(None); }; if let Some(class) = context.class(&reflected_name).cloned() { - let mut scope = ElephcEvalScope::new(); - return eval_dynamic_class_new_object( - &class, - constructor_args, - context, - &mut scope, - values, - ) - .map(Some); + return eval_reflection_public_constructor_scope(context, values, |context, values| { + let mut scope = ElephcEvalScope::new(); + eval_dynamic_class_new_object(&class, constructor_args, context, &mut scope, values) + .map(Some) + }); } let class_name = context .resolve_class_name(&reflected_name) .unwrap_or(reflected_name); - let args = bind_native_callable_args( - context.native_constructor_signature(&class_name), - constructor_args, - values, - )?; - let instance = values.new_object(&class_name)?; - values.construct_object(instance, args)?; - Ok(Some(instance)) + eval_reflection_public_constructor_scope(context, values, |context, values| { + let args = bind_native_callable_args( + context.native_constructor_signature(&class_name), + constructor_args, + values, + )?; + let instance = values.new_object(&class_name)?; + values.construct_object(instance, args)?; + Ok(Some(instance)) + }) } /// Expands the single `ReflectionClass::newInstanceArgs()` array argument. @@ -3875,6 +3873,18 @@ fn eval_reflection_class_new_instance_args( eval_array_call_arg_values(args[0], values) } +/// Runs ReflectionClass construction with only public constructor visibility. +fn eval_reflection_public_constructor_scope( + context: &mut ElephcEvalContext, + values: &mut V, + action: impl FnOnce(&mut ElephcEvalContext, &mut V) -> Result, +) -> Result { + context.push_class_scope(String::new()); + let result = action(context, values); + context.pop_class_scope(); + result +} + /// Allocates the class named by a materialized eval `ReflectionClass` without running `__construct()`. fn eval_reflection_class_new_instance_without_constructor_result( identity: u64, diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 4924cd4e93..92465774a3 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -167,6 +167,8 @@ unsafe extern "C" { pub(super) fn __elephc_eval_value_construct_object( object: *mut RuntimeCell, args: *mut RuntimeCell, + scope_ptr: *const u8, + scope_len: u64, ) -> u64; pub(super) fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; pub(super) fn __elephc_eval_interface_exists(name_ptr: *const u8, name_len: u64) -> u64; diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 32a159d9aa..eb9686d2df 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -450,15 +450,22 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } - /// Calls a public AOT constructor through the generated user bridge when one exists. + /// Calls an AOT constructor through the generated user bridge when one exists. fn construct_object( &mut self, object: RuntimeCellHandle, args: Vec, ) -> Result<(), EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); let arg_array = Self::arg_array(args)?; - let ok = - unsafe { __elephc_eval_value_construct_object(object.as_ptr(), arg_array.as_ptr()) }; + let ok = unsafe { + __elephc_eval_value_construct_object( + object.as_ptr(), + arg_array.as_ptr(), + scope_ptr, + scope_len, + ) + }; unsafe { __elephc_eval_value_release(arg_array.as_ptr()); } diff --git a/docs/php/eval.md b/docs/php/eval.md index faba3af7d5..9fc9276f55 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and class constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | @@ -307,11 +307,12 @@ counts, and supported defaults where available; it returns `null` when no constructor is visible. `ReflectionClass::getParentClass()` returns a materialized `ReflectionClass` for eval-declared and generated/AOT parent classes or `false` when no parent class exists. -`ReflectionClass::newInstance()` constructs -eval-declared reflected classes and forwards constructor arguments through -eval's positional, named, and unpacking-aware call binding. -`ReflectionClass::newInstanceArgs()` constructs eval-declared reflected classes -from an argument array, treating string keys as named constructor arguments. +`ReflectionClass::newInstance()` constructs eval-declared and bridge-supported +generated/AOT reflected classes with public constructors and forwards +constructor arguments through eval's positional, named, and unpacking-aware call +binding. Non-public constructors fail like PHP reflection construction. +`ReflectionClass::newInstanceArgs()` constructs those reflected classes from an +argument array, treating string keys as named constructor arguments. `ReflectionClass::newInstanceWithoutConstructor()` allocates eval-declared reflected classes, initializes supported property defaults, and skips `__construct()`. @@ -649,9 +650,9 @@ still outside eval fragments. Runtime/AOT object-method and static-method fallback from eval remain limited to generated visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter bridge signatures, including the generated positional variadic array slot when -present. Constructor fallback remains limited to generated public +present. Constructor fallback uses the same generated visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter -bridge signatures. By-reference parameters and broader parameter/return ABI +bridge slice. By-reference parameters and broader parameter/return ABI shapes are still outside those bridge paths. Eval class support is still smaller than the full static class system. The main @@ -662,7 +663,7 @@ property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional -`new C()` parameter slice, and broader generated/AOT method bridge signatures beyond the current visibility-checked +`new C()` parameter slice, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 75c9792515..5626b3d1ae 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Emits user-assembly helpers that let libelephc-magician run public native +//! Emits user-assembly helpers that let libelephc-magician run native //! constructors after allocating AOT objects by class name. //! //! Called from: @@ -11,10 +11,13 @@ //! - Classes without constructors are treated as successful no-ops, matching PHP. //! - Constructors are bridged for non-by-ref scalar/Mixed/array/object arguments, //! including a generated variadic array slot when the signature has one. +//! - Non-public constructors are accepted when the active eval class scope +//! satisfies PHP visibility. use std::collections::BTreeMap; use crate::codegen::abi; +use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::ir::{Function, LocalKind, Module}; @@ -52,18 +55,24 @@ struct EvalConstructorSlot { class_id: u64, class_name: String, impl_class: String, + visibility: Visibility, + allowed_scopes: Vec, params: Vec, supported: bool, } /// Emits eval constructor helpers when any lowered function owns an eval context. -pub(super) fn emit_eval_constructor_helpers(module: &Module, emitter: &mut Emitter) { +pub(super) fn emit_eval_constructor_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { if !module_uses_eval(module) { return; } let slots = collect_eval_constructor_slots(module); let builtin_throwable_class_ids = collect_builtin_throwable_constructor_class_ids(module); - emit_constructor_helper(module, emitter, &slots, &builtin_throwable_class_ids); + emit_constructor_helper(module, emitter, data, &slots, &builtin_throwable_class_ids); } /// Returns true when the EIR module contains a function that can call eval. @@ -101,7 +110,7 @@ fn collect_eval_constructor_slots(module: &Module) -> Vec { let mut classes = module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_class_constructor_slot(class_name, class_info, &emitted_methods, &mut slots); + collect_class_constructor_slot(module, class_name, class_info, &emitted_methods, &mut slots); } slots } @@ -120,6 +129,7 @@ fn collect_builtin_throwable_constructor_class_ids(module: &Module) -> Vec /// Adds one constructor slot for a class when the constructor has emitted code. fn collect_class_constructor_slot( + module: &Module, class_name: &str, class_info: &ClassInfo, emitted_methods: &std::collections::HashSet<(String, String, bool)>, @@ -137,8 +147,9 @@ fn collect_class_constructor_slot( if !emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), false)) { return; } + let visibility = constructor_visibility(class_info, &method_key); let supported = - constructor_is_public(class_info, &method_key) && constructor_signature_supported(sig); + constructor_visibility_supported(visibility) && constructor_signature_supported(sig); let params = if supported { sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect() } else { @@ -148,17 +159,27 @@ fn collect_class_constructor_slot( class_id: class_info.class_id, class_name: class_name.to_string(), impl_class: impl_class.to_string(), + visibility: visibility.clone(), + allowed_scopes: visibility_scope_names(module, impl_class, visibility), params, supported, }); } -/// Returns true when the constructor is publicly visible to runtime eval. -fn constructor_is_public(class_info: &ClassInfo, method_key: &str) -> bool { +/// Returns the declared constructor visibility, defaulting to public metadata. +fn constructor_visibility<'a>(class_info: &'a ClassInfo, method_key: &str) -> &'a Visibility { class_info .method_visibilities .get(method_key) - .is_none_or(|visibility| matches!(visibility, Visibility::Public)) + .unwrap_or(&Visibility::Public) +} + +/// Returns true when the eval constructor bridge can enforce this visibility. +fn constructor_visibility_supported(visibility: &Visibility) -> bool { + matches!( + visibility, + Visibility::Public | Visibility::Protected | Visibility::Private + ) } /// Returns true for constructor signatures supported by this eval bridge slice. @@ -188,10 +209,11 @@ fn constructor_param_supported(ty: &PhpType) -> bool { ) } -/// Emits `__elephc_eval_value_construct_object(Mixed*, MixedArray*) -> bool`. +/// Emits `__elephc_eval_value_construct_object(Mixed*, MixedArray*, scope, scope_len) -> bool`. fn emit_constructor_helper( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalConstructorSlot], builtin_throwable_class_ids: &[u64], ) { @@ -200,10 +222,10 @@ fn emit_constructor_helper( label_c_global(module, emitter, "__elephc_eval_value_construct_object"); match module.target.arch { Arch::AArch64 => { - emit_constructor_aarch64(module, emitter, slots, builtin_throwable_class_ids) + emit_constructor_aarch64(module, emitter, data, slots, builtin_throwable_class_ids) } Arch::X86_64 => { - emit_constructor_x86_64(module, emitter, slots, builtin_throwable_class_ids) + emit_constructor_x86_64(module, emitter, data, slots, builtin_throwable_class_ids) } } } @@ -212,15 +234,18 @@ fn emit_constructor_helper( fn emit_constructor_aarch64( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalConstructorSlot], builtin_throwable_class_ids: &[u64], ) { let success_label = "__elephc_eval_value_construct_success"; let fail_label = "__elephc_eval_value_construct_fail"; let done_label = "__elephc_eval_value_construct_done"; - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for args, object, and fp/lr + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for scope, args, object, and fp/lr emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x2, [sp, #0]"); // save the active eval class-scope pointer + emitter.instruction("str x3, [sp, #8]"); // save the active eval class-scope length emitter.instruction("str x1, [sp, #24]"); // save the boxed eval argument array emitter.instruction(&format!("cbz x0, {}", success_label)); // a null object pointer means there is nothing to construct emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload @@ -234,7 +259,7 @@ fn emit_constructor_aarch64( fail_label, success_label, ); - emit_aarch64_constructor_dispatch(module, emitter, slots, fail_label, success_label); + emit_aarch64_constructor_dispatch(module, emitter, data, slots, fail_label, success_label); emitter.instruction(&format!("b {}", success_label)); // no constructor metadata matched this class id emitter.label(fail_label); emitter.instruction("mov x0, #0"); // report constructor dispatch failure to Rust @@ -251,6 +276,7 @@ fn emit_constructor_aarch64( fn emit_constructor_x86_64( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalConstructorSlot], builtin_throwable_class_ids: &[u64], ) { @@ -259,7 +285,9 @@ fn emit_constructor_x86_64( let done_label = "__elephc_eval_value_construct_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 48"); // reserve aligned slots for object, args, and temp values + emitter.instruction("sub rsp, 64"); // reserve aligned slots for object, args, scope, and temp values + emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // save the active eval class-scope length emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the boxed eval argument array emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", success_label)); // a null object pointer means there is nothing to construct @@ -275,7 +303,7 @@ fn emit_constructor_x86_64( fail_label, success_label, ); - emit_x86_64_constructor_dispatch(module, emitter, slots, fail_label, success_label); + emit_x86_64_constructor_dispatch(module, emitter, data, slots, fail_label, success_label); emitter.instruction(&format!("jmp {}", success_label)); // no constructor metadata matched this class id emitter.label(fail_label); emitter.instruction("xor eax, eax"); // report constructor dispatch failure to Rust @@ -454,6 +482,7 @@ fn emit_x86_64_default_builtin_throwable_fields(emitter: &mut Emitter) { fn emit_aarch64_constructor_dispatch( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalConstructorSlot], fail_label: &str, success_label: &str, @@ -466,7 +495,7 @@ fn emit_aarch64_constructor_dispatch( emitter.instruction("cmp x9, x10"); // compare receiver class id against this constructor class emitter.instruction(&format!("b.ne {}", next_label)); // try the next constructor class when ids differ for slot in class_slots { - emit_aarch64_constructor_body(module, emitter, slot, fail_label, success_label); + emit_aarch64_constructor_body(module, emitter, data, slot, fail_label, success_label); } emitter.label(&next_label); } @@ -476,6 +505,7 @@ fn emit_aarch64_constructor_dispatch( fn emit_x86_64_constructor_dispatch( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slots: &[EvalConstructorSlot], fail_label: &str, success_label: &str, @@ -488,7 +518,7 @@ fn emit_x86_64_constructor_dispatch( emitter.instruction("cmp r11, r10"); // compare receiver class id against this constructor class emitter.instruction(&format!("jne {}", next_label)); // try the next constructor class when ids differ for slot in class_slots { - emit_x86_64_constructor_body(module, emitter, slot, fail_label, success_label); + emit_x86_64_constructor_body(module, emitter, data, slot, fail_label, success_label); } emitter.label(&next_label); } @@ -498,6 +528,7 @@ fn emit_x86_64_constructor_dispatch( fn emit_aarch64_constructor_body( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slot: &EvalConstructorSlot, fail_label: &str, success_label: &str, @@ -506,6 +537,11 @@ fn emit_aarch64_constructor_body( emitter.instruction(&format!("b {}", fail_label)); // reject constructors outside this bridge's supported ABI slice return; } + if !matches!(slot.visibility, Visibility::Public) { + let scope_ok_label = constructor_scope_ok_label(module, slot); + emit_aarch64_constructor_scope_check(emitter, data, slot, &scope_ok_label, fail_label); + emitter.label(&scope_ok_label); + } emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); let overflow_bytes = emit_aarch64_prepare_constructor_args(module, emitter, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); @@ -520,6 +556,7 @@ fn emit_aarch64_constructor_body( fn emit_x86_64_constructor_body( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slot: &EvalConstructorSlot, fail_label: &str, success_label: &str, @@ -528,6 +565,11 @@ fn emit_x86_64_constructor_body( emitter.instruction(&format!("jmp {}", fail_label)); // reject constructors outside this bridge's supported ABI slice return; } + if !matches!(slot.visibility, Visibility::Public) { + let scope_ok_label = constructor_scope_ok_label(module, slot); + emit_x86_64_constructor_scope_check(emitter, data, slot, &scope_ok_label, fail_label); + emitter.label(&scope_ok_label); + } emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); let overflow_bytes = emit_x86_64_prepare_constructor_args(module, emitter, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); @@ -538,6 +580,54 @@ fn emit_x86_64_constructor_body( emitter.instruction(&format!("jmp {}", success_label)); // constructor returned normally } +/// Emits ARM64 visibility checks for a protected/private constructor bridge hit. +fn emit_aarch64_constructor_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalConstructorSlot, + success_label: &str, + fail_label: &str, +) { + emitter.instruction("ldr x1, [sp, #0]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject scoped constructor access outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", success_label)); // run the constructor when scoped visibility is satisfied + } + emitter.instruction(&format!("b {}", fail_label)); // reject constructor access from unrelated classes +} + +/// Emits x86_64 visibility checks for a protected/private constructor bridge hit. +fn emit_x86_64_constructor_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalConstructorSlot, + success_label: &str, + fail_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", fail_label)); // reject scoped constructor access outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 56]"); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", success_label)); // run the constructor when scoped visibility is satisfied + } + emitter.instruction(&format!("jmp {}", fail_label)); // reject constructor access from unrelated classes +} + /// Emits ARM64 arity validation for one constructor body. fn emit_aarch64_validate_constructor_arg_count( module: &Module, @@ -956,6 +1046,15 @@ fn constructor_arg_label(module: &Module, slot: &EvalConstructorSlot, index: usi ) } +/// Returns a platform-safe label for a successful scoped constructor access check. +fn constructor_scope_ok_label(module: &Module, slot: &EvalConstructorSlot) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!("__elephc_eval_constructor_{}_scope_ok{}", slot.class_id, suffix) +} + /// Returns runtime interface ids for object values accepted by PHP iterable parameters. fn traversable_interface_ids(module: &Module) -> Vec { ["Iterator", "IteratorAggregate"] @@ -964,6 +1063,62 @@ fn traversable_interface_ids(module: &Module) -> Vec { .collect() } +/// Returns class scopes that satisfy one constructor visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + /// Emits a platform-C global label for a user assembly helper. fn label_c_global(module: &Module, emitter: &mut Emitter, name: &str) { emitter.label_global(&module.target.extern_symbol(name)); diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index bd2ecdf6db..4b95d63bb7 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -562,7 +562,7 @@ fn eval_native_constructor_registrations( let Some(signature) = class_info.methods.get(&method_key) else { continue; }; - let bridge_supported = class_method_is_public(class_info, &method_key) + let bridge_supported = class_method_visibility_bridge_supported(class_info, &method_key) && constructor_signature_can_bridge_with_eval(signature); registrations.push(EvalNativeConstructorRegistration { class_name: class_name.clone(), @@ -1268,14 +1268,6 @@ fn encode_eval_native_default_string(bytes: &mut Vec, value: &str) { bytes.extend_from_slice(value.as_bytes()); } -/// Returns true when an instance method is public in the class metadata. -fn class_method_is_public(class_info: &ClassInfo, method_name: &str) -> bool { - class_info - .method_visibilities - .get(method_name) - .is_none_or(|visibility| matches!(visibility, Visibility::Public)) -} - /// Returns true when eval can enforce this instance method visibility in the bridge. fn class_method_visibility_bridge_supported(class_info: &ClassInfo, method_name: &str) -> bool { class_info diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e8d631fc38..e4b0f1cf8f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11097,6 +11097,49 @@ echo $fourth->label();'); assert_eq!(out, "EF:GH:IJ:KL"); } +/// Verifies eval ReflectionClass::newInstance rejects non-public eval constructors like PHP. +#[test] +fn test_eval_reflection_class_new_instance_rejects_private_eval_constructor() { + let err = compile_and_run_expect_failure( + r#"newInstance();'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + +/// Verifies eval ReflectionClass::newInstance rejects non-public AOT constructors like PHP. +#[test] +fn test_eval_reflection_class_new_instance_rejects_protected_aot_constructor_from_child_scope() { + let err = compile_and_run_expect_failure( + r#"newInstance();'); + } +} + +EvalReflectNewProtectedAotCtorChild::run(); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr: {err}" + ); +} + /// Verifies eval ReflectionMethod::invoke and invokeArgs call eval-declared methods. #[test] fn test_eval_reflection_method_invoke_calls_eval_method() { @@ -11639,6 +11682,100 @@ echo eval('$box = new EvalDynamicNewOneArgCtor(11); return $box->x;'); assert_eq!(out, "11"); } +/// Verifies eval object construction can call private AOT constructors from the declaring scope. +#[test] +fn test_eval_dynamic_new_runs_private_constructor_from_declaring_scope() { + let out = compile_and_run( + r#"x = $x + 2; } + + public static function run(): void { + $box = eval('return new EvalDynamicNewPrivateCtor(3);'); + echo $box->x; + } +} + +EvalDynamicNewPrivateCtor::run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval object construction rejects private AOT constructors outside the declaring scope. +#[test] +fn test_eval_dynamic_new_rejects_private_constructor_from_child_scope() { + let err = compile_and_run_expect_failure( + r#"x = $x + 2; } +} + +class EvalDynamicNewProtectedCtorChild extends EvalDynamicNewProtectedCtorBase { + public static function run(): void { + $box = eval('return new EvalDynamicNewProtectedCtorBase(3);'); + echo $box->x; + } +} + +EvalDynamicNewProtectedCtorChild::run(); +"#, + ); + assert_eq!(out, "5"); +} + +/// Verifies eval object construction rejects protected AOT constructors between sibling scopes. +#[test] +fn test_eval_dynamic_new_rejects_protected_constructor_from_sibling_scope() { + let err = compile_and_run_expect_failure( + r#" Date: Wed, 24 Jun 2026 20:04:02 +0200 Subject: [PATCH 0604/1208] Support eval AOT lifecycle reflection predicates --- .../src/interpreter/reflection.rs | 29 ++++++++++++++++++ docs/php/eval.md | 9 +++--- tests/codegen/eval.rs | 30 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 91c7981425..4b58ec2e8f 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1953,10 +1953,39 @@ fn eval_reflection_aot_class_flags( if eval_reflection_builtin_class_is_iterable(runtime_class_name) { flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; } + if is_class { + if eval_reflection_aot_lifecycle_method_allows_public_reflection( + runtime_class_name, + "__construct", + values, + )? { + flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; + } + if eval_reflection_aot_lifecycle_method_allows_public_reflection( + runtime_class_name, + "__clone", + values, + )? { + flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; + } + } let modifiers = if is_enum { 32 } else { 0 }; Ok(Some((flags, modifiers))) } +/// Returns whether an absent or public AOT lifecycle method allows public reflection. +fn eval_reflection_aot_lifecycle_method_allows_public_reflection( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(true); + }; + Ok(flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC != 0 + && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0) +} + /// Builds an eval-backed `ReflectionFunction` object for eval or registered native functions. fn eval_reflection_function_new( evaluated_args: Vec, diff --git a/docs/php/eval.md b/docs/php/eval.md index 9fc9276f55..607a8cd4ee 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -228,10 +228,11 @@ PHP-compatible enum finality and class-like kind checks for eval interfaces, traits, and enums. `ReflectionClass::isReadOnly()` reports eval `readonly class` metadata. `ReflectionClass::isAnonymous()` reports true for eval anonymous classes and false for eval-declared named class-like symbols. -`ReflectionClass::isInstantiable()` reports whether eval class-like metadata -describes a concrete class with no constructor or a public constructor. -`ReflectionClass::isCloneable()` reports whether eval class metadata describes -a concrete class with no `__clone()` or a public `__clone()`. +`ReflectionClass::isInstantiable()` reports whether eval or generated/AOT +class-like metadata describes a concrete class with no constructor or a public +constructor. `ReflectionClass::isCloneable()` reports whether eval or +generated/AOT class metadata describes a concrete class with no `__clone()` or +a public `__clone()`. `ReflectionClass::isIterable()` and `isIterateable()` report whether eval or generated class metadata describes a concrete `Iterator` or `IteratorAggregate` class. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e4b0f1cf8f..5235e60976 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8111,6 +8111,36 @@ echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e";'); assert_eq!(out.stdout, "aBCprite"); } +/// Verifies eval ReflectionClass lifecycle predicates use generated/AOT lifecycle visibility. +#[test] +fn test_eval_reflection_class_aot_lifecycle_visibility_predicates() { + let out = compile_and_run( + r#"isInstantiable() ? "N" : "n"; +echo (new ReflectionClass("EvalAotInstPublicCtor"))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass("EvalAotInstPrivateCtor"))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass("EvalAotInstProtectedCtor"))->isInstantiable() ? "T" : "t"; +echo ":"; +echo (new ReflectionClass("EvalAotCloneNoHook"))->isCloneable() ? "N" : "n"; +echo (new ReflectionClass("EvalAotClonePublicHook"))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass("EvalAotClonePrivateHook"))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass("EvalAotCloneProtectedHook"))->isCloneable() ? "T" : "t"; +'); +"#, + ); + assert_eq!(out, "NPrt:NPrt"); +} + /// Verifies eval ReflectionClass reports named eval class-like symbols as non-anonymous through /// the generated reflection-owner bridge. #[test] From 737301cab81717892a1150a3e6ce211a852cf727 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 20:21:55 +0200 Subject: [PATCH 0605/1208] Expose AOT class modifiers to eval reflection --- .../src/interpreter/reflection.rs | 17 ++- .../src/interpreter/runtime_ops.rs | 3 + .../interpreter/tests/support/object_ops.rs | 15 +++ .../interpreter/tests/support/runtime_ops.rs | 4 + .../src/runtime_hooks/externs.rs | 4 + .../elephc-magician/src/runtime_hooks/ops.rs | 8 ++ docs/php/eval.md | 11 +- src/codegen_support/runtime/data/user.rs | 53 +++++++++ src/codegen_support/runtime/eval_bridge.rs | 101 ++++++++++++++++++ tests/codegen/eval.rs | 35 ++++++ 10 files changed, 244 insertions(+), 7 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 4b58ec2e8f..cb54461531 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1941,6 +1941,14 @@ fn eval_reflection_aot_class_flags( } else { flags |= EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; } + let mut class_flags = values.reflection_class_flags(runtime_class_name)?.unwrap_or(0); + if is_enum { + class_flags &= !EVAL_REFLECTION_CLASS_FLAG_READONLY; + } + flags |= class_flags + & (EVAL_REFLECTION_CLASS_FLAG_FINAL + | EVAL_REFLECTION_CLASS_FLAG_ABSTRACT + | EVAL_REFLECTION_CLASS_FLAG_READONLY); if is_interface { flags |= EVAL_REFLECTION_CLASS_FLAG_INTERFACE; } @@ -1953,7 +1961,7 @@ fn eval_reflection_aot_class_flags( if eval_reflection_builtin_class_is_iterable(runtime_class_name) { flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; } - if is_class { + if is_class && !is_enum && flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT == 0 { if eval_reflection_aot_lifecycle_method_allows_public_reflection( runtime_class_name, "__construct", @@ -1969,7 +1977,12 @@ fn eval_reflection_aot_class_flags( flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; } } - let modifiers = if is_enum { 32 } else { 0 }; + let modifiers = eval_reflection_class_modifiers( + flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0, + is_enum, + ); Ok(Some((flags, modifiers))) } diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 5ee45eac7b..1ff0e1556c 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -176,6 +176,9 @@ pub trait RuntimeValueOps { class_name: &str, ) -> Result; + /// Returns generated AOT ReflectionClass modifier flags for one class. + fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus>; + /// Returns generated AOT ReflectionProperty flags for a class/property pair. fn reflection_property_flags( &mut self, diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index a9330d01c6..6f09c53b89 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -9,6 +9,9 @@ use super::*; +const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; +const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; +const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; @@ -1133,6 +1136,18 @@ impl FakeOps { pub(super) fn runtime_class_exists(&mut self, name: &str) -> Result { Ok(name.eq_ignore_ascii_case("KnownClass")) } + /// Reports fake generated AOT ReflectionClass flags for eval metadata unit tests. + pub(super) fn runtime_reflection_class_flags( + &mut self, + class_name: &str, + ) -> Result, EvalStatus> { + match class_name.to_ascii_lowercase().as_str() { + "knownabstract" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_ABSTRACT)), + "knownfinal" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_FINAL)), + "knownreadonly" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_READONLY)), + _ => Ok(None), + } + } /// Reports fake generated AOT ReflectionMethod flags for eval metadata unit tests. pub(super) fn runtime_reflection_method_flags( &mut self, diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index 54eca01c3c..c2fb19f3b2 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -209,6 +209,10 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_reflection_method_names(class_name) } + /// Reports fake generated AOT ReflectionClass flags for metadata bridge tests. + fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { + self.runtime_reflection_class_flags(class_name) + } /// Reports fake generated AOT ReflectionProperty flags for metadata bridge tests. fn reflection_property_flags( &mut self, diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 92465774a3..e9cd36518f 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -140,6 +140,10 @@ unsafe extern "C" { class_ptr: *const u8, class_len: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_flags( + class_ptr: *const u8, + class_len: u64, + ) -> u64; pub(super) fn __elephc_eval_reflection_property_flags( class_ptr: *const u8, class_len: u64, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index eb9686d2df..06568d0f1c 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -367,6 +367,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns generated AOT ReflectionClass modifier flags, or `None` when no row matches. + fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_class_flags(class_name.as_ptr(), class_name.len() as u64) + }; + Ok((flags != 0).then_some(flags)) + } + /// Returns generated AOT ReflectionProperty flags, or `None` when no row matches. fn reflection_property_flags( &mut self, diff --git a/docs/php/eval.md b/docs/php/eval.md index 607a8cd4ee..539c5c8956 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -223,11 +223,12 @@ named, nullable, union, and intersection declarations, including `void` and supported non-closure function reflectors. `ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and -`ReflectionClass::isEnum()` report eval class-like metadata, including -PHP-compatible enum finality and class-like kind checks for eval interfaces, -traits, and enums. `ReflectionClass::isReadOnly()` reports eval `readonly class` -metadata. `ReflectionClass::isAnonymous()` reports true for eval anonymous -classes and false for eval-declared named class-like symbols. +`ReflectionClass::isEnum()` report eval and generated/AOT class-like metadata, +including PHP-compatible enum finality and class-like kind checks for eval +interfaces, traits, and enums. `ReflectionClass::isReadOnly()` reports eval and +generated/AOT `readonly class` metadata. `ReflectionClass::isAnonymous()` +reports true for eval anonymous classes and false for eval-declared named +class-like symbols. `ReflectionClass::isInstantiable()` reports whether eval or generated/AOT class-like metadata describes a concrete class with no constructor or a public constructor. `ReflectionClass::isCloneable()` reports whether eval or diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 2ad36d1091..c64635b181 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -19,6 +19,9 @@ use crate::types::{ClassInfo, EnumInfo, FunctionSig, InterfaceInfo, PhpType}; use super::instanceof::{escaped_ascii, escaped_bytes}; +const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; +const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; +const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; const EVAL_REFLECTION_PROPERTY_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_PROPERTY_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED: u64 = 4; @@ -476,6 +479,8 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); + emit_eval_reflection_class_lookup_data(&mut out, &sorted_classes); + out.push_str(".p2align 3\n"); emit_eval_reflection_class_interface_lookup_data(&mut out, &sorted_classes, interfaces); // -- class-level PHP 8 attribute metadata table -- @@ -1257,6 +1262,54 @@ fn eval_reflection_static_property_declaring_class<'a>( .unwrap_or(reflected_class) } +/// Emits AOT class flag rows consumed by eval ReflectionClass metadata probes. +fn emit_eval_reflection_class_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + let flags = eval_reflection_class_flags(class_info); + if flags == 0 { + continue; + } + let class_label = format!("_eval_reflection_class_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + entries.push((class_label, class_name.len(), flags)); + index += 1; + } + + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_class_count\n_eval_reflection_class_count:\n"); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_classes\n_eval_reflection_classes:\n"); + for (class_label, class_len, flags) in entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", flags)); + } +} + +/// Returns eval ReflectionClass flag bits retained for one generated/AOT class. +fn eval_reflection_class_flags(class_info: &ClassInfo) -> u64 { + let mut flags = 0; + if class_info.is_final { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL; + } + if class_info.is_abstract { + flags |= EVAL_REFLECTION_CLASS_FLAG_ABSTRACT; + } + if class_info.is_readonly_class { + flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; + } + flags +} + /// Emits class-like/interface-name rows consumed by eval ReflectionClass metadata probes. fn emit_eval_reflection_class_interface_lookup_data( out: &mut String, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index fab96e22cf..60fc57f65f 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -174,6 +174,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emit_aarch64_eval_reflection_method_names(emitter); emit_aarch64_eval_reflection_property_names(emitter); emit_aarch64_eval_reflection_class_interface_names(emitter); + emit_aarch64_eval_reflection_class_flags(emitter); emit_aarch64_eval_reflection_method_flags(emitter); emit_aarch64_eval_reflection_method_declaring_class(emitter); emit_aarch64_eval_reflection_property_declaring_class(emitter); @@ -1597,6 +1598,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emit_x86_64_eval_reflection_method_names(emitter); emit_x86_64_eval_reflection_property_names(emitter); emit_x86_64_eval_reflection_class_interface_names(emitter); + emit_x86_64_eval_reflection_class_flags(emitter); emit_x86_64_eval_reflection_method_flags(emitter); emit_x86_64_eval_reflection_method_declaring_class(emitter); emit_x86_64_eval_reflection_property_declaring_class(emitter); @@ -3858,6 +3860,105 @@ fn emit_x86_64_eval_name_table_exists( emitter.instruction("ret"); // return the metadata-name existence flag to Rust } +/// Emits the ARM64 eval hook that returns AOT ReflectionClass flags. +fn emit_aarch64_eval_reflection_class_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_class_flags"); + emitter.instruction("sub sp, sp, #64"); // reserve class-flag scan state across string comparisons + emitter.instruction("stp x29, x30, [sp, #48]"); // save frame pointer and return address across helper calls + emitter.instruction("add x29, sp, #48"); // establish a stable class-flag scan frame + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_class_count"); + emitter.instruction("ldr x9, [x9]"); // load the class-flag metadata row count + emitter.instruction("cbz x9, __elephc_eval_reflection_class_flags_miss"); // an empty table cannot contain class flags + emitter.instruction("str x9, [sp, #16]"); // save the class-flag row count + abi::emit_symbol_address(emitter, "x10", "_eval_reflection_classes"); + emitter.instruction("str x10, [sp, #24]"); // save the current class-flag metadata row + emitter.instruction("mov x11, #0"); // start scanning at class-flag row zero + emitter.label("__elephc_eval_reflection_class_flags_loop"); + emitter.instruction("ldr x9, [sp, #16]"); // reload the class-flag metadata row count + emitter.instruction("cmp x11, x9"); // have all class-flag rows been scanned? + emitter.instruction("b.ge __elephc_eval_reflection_class_flags_miss"); // no row matched before the end of the table + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-flag metadata row + emitter.instruction("ldr x12, [x10, #8]"); // load the stored class-name length + emitter.instruction("ldr x2, [sp, #8]"); // reload the requested class-name length + emitter.instruction("cmp x12, x2"); // compare stored and requested class-name lengths + emitter.instruction("b.ne __elephc_eval_reflection_class_flags_skip"); // length mismatch means this row belongs to another class + emitter.instruction("str x11, [sp, #32]"); // save the row index across the class-name compare + emitter.instruction("ldr x1, [sp, #0]"); // pass the requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // pass the requested class-name length + emitter.instruction("ldr x3, [x10]"); // pass the stored class-name pointer + emitter.instruction("mov x4, x12"); // pass the stored class-name length + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("ldr x11, [sp, #32]"); // restore the row index after the class-name compare + emitter.instruction("cmp x0, #0"); // did the requested class name match this row? + emitter.instruction("b.ne __elephc_eval_reflection_class_flags_skip"); // class mismatch means scanning must continue + emitter.instruction("ldr x10, [sp, #24]"); // reload the matched class-flag metadata row + emitter.instruction("ldr x0, [x10, #16]"); // return the row's ReflectionClass predicate flags + emitter.instruction("b __elephc_eval_reflection_class_flags_done"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_class_flags_skip"); + emitter.instruction("ldr x10, [sp, #24]"); // reload the current class-flag metadata row + emitter.instruction("add x10, x10, #24"); // advance to the next 24-byte class-flag row + emitter.instruction("str x10, [sp, #24]"); // persist the advanced row cursor + emitter.instruction("add x11, x11, #1"); // advance the row index + emitter.instruction("b __elephc_eval_reflection_class_flags_loop"); // continue scanning class-flag metadata rows + emitter.label("__elephc_eval_reflection_class_flags_miss"); + emitter.instruction("mov x0, #0"); // return zero when no AOT class flags matched + emitter.label("__elephc_eval_reflection_class_flags_done"); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #64"); // release the class-flag metadata scan frame + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionClass flags. +fn emit_x86_64_eval_reflection_class_flags(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_reflection_class_flags"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable class-flag scan frame + emitter.instruction("sub rsp, 48"); // reserve class-name, count, cursor, and index slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_class_count"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // load the class-flag metadata row count + emitter.instruction("test r10, r10"); // is the class-flag metadata table empty? + emitter.instruction("jz __elephc_eval_reflection_class_flags_miss_x86"); // an empty table cannot contain class flags + emitter.instruction("mov QWORD PTR [rbp - 24], r10"); // save the class-flag row count + abi::emit_symbol_address(emitter, "r11", "_eval_reflection_classes"); + emitter.instruction("mov QWORD PTR [rbp - 32], r11"); // save the current class-flag metadata row + emitter.instruction("xor r11d, r11d"); // start scanning at class-flag row zero + emitter.label("__elephc_eval_reflection_class_flags_loop_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the class-flag metadata row count + emitter.instruction("cmp r11, r10"); // have all class-flag rows been scanned? + emitter.instruction("jae __elephc_eval_reflection_class_flags_miss_x86"); // no row matched before the end of the table + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-flag metadata row + emitter.instruction("mov rcx, QWORD PTR [r10 + 8]"); // load the stored class-name length + emitter.instruction("cmp rcx, QWORD PTR [rbp - 16]"); // compare stored and requested class-name lengths + emitter.instruction("jne __elephc_eval_reflection_class_flags_skip_x86"); // length mismatch means this row belongs to another class + emitter.instruction("mov QWORD PTR [rbp - 40], r11"); // save the row index across the class-name compare + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // pass the requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // pass the requested class-name length + emitter.instruction("mov rdx, QWORD PTR [r10]"); // pass the stored class-name pointer + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("mov r11, QWORD PTR [rbp - 40]"); // restore the row index after the class-name compare + emitter.instruction("test rax, rax"); // did the requested class name match this row? + emitter.instruction("jne __elephc_eval_reflection_class_flags_skip_x86"); // class mismatch means scanning must continue + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the matched class-flag metadata row + emitter.instruction("mov rax, QWORD PTR [r10 + 16]"); // return the row's ReflectionClass predicate flags + emitter.instruction("jmp __elephc_eval_reflection_class_flags_done_x86"); // restore the wrapper frame after a match + emitter.label("__elephc_eval_reflection_class_flags_skip_x86"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the current class-flag metadata row + emitter.instruction("add r10, 24"); // advance to the next 24-byte class-flag row + emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // persist the advanced row cursor + emitter.instruction("inc r11"); // advance the row index + emitter.instruction("jmp __elephc_eval_reflection_class_flags_loop_x86"); // continue scanning class-flag metadata rows + emitter.label("__elephc_eval_reflection_class_flags_miss_x86"); + emitter.instruction("xor eax, eax"); // return zero when no AOT class flags matched + emitter.label("__elephc_eval_reflection_class_flags_done_x86"); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return flags, or zero for a miss, to Rust +} + /// Emits the ARM64 eval bridge wrapper for cloning boxed object cells. fn emit_aarch64_object_clone_shallow_wrapper(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_object_clone_shallow"); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5235e60976..e77269714f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8111,6 +8111,41 @@ echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e";'); assert_eq!(out.stdout, "aBCprite"); } +/// Verifies eval ReflectionClass modifier and lifecycle predicates use generated/AOT class flags. +#[test] +fn test_eval_reflection_class_aot_modifier_flags() { + let out = compile_and_run( + r#"isAbstract() ? "A" : "a"; + echo $ref->isFinal() ? "F" : "f"; + echo $ref->isReadOnly() ? "R" : "r"; + echo $ref->isEnum() ? "E" : "e"; + echo "/" . $ref->getModifiers() . "/"; + echo $ref->isInstantiable() ? "I" : "i"; + echo $ref->isCloneable() ? "C" : "c"; + echo ":"; +} +eval_aot_modifier_line("EvalAotModifierAbstract"); +eval_aot_modifier_line("EvalAotModifierFinal"); +eval_aot_modifier_line("EvalAotModifierReadonly"); +eval_aot_modifier_line("EvalAotModifierEnum"); +'); +"#, + ); + assert_eq!( + out, + "Afre/64/ic:aFre/32/IC:afRe/65536/IC:aFrE/32/ic:" + ); +} + /// Verifies eval ReflectionClass lifecycle predicates use generated/AOT lifecycle visibility. #[test] fn test_eval_reflection_class_aot_lifecycle_visibility_predicates() { From 4754b545ae67a91a9b1957a6b25318e44fe0b583 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 21:01:43 +0200 Subject: [PATCH 0606/1208] Support eval AOT class constants --- .../src/interpreter/reflection.rs | 142 +- .../src/interpreter/runtime_ops.rs | 34 + .../src/interpreter/statements.rs | 32 +- .../interpreter/tests/support/runtime_ops.rs | 39 + .../src/runtime_hooks/externs.rs | 30 + .../elephc-magician/src/runtime_hooks/ops.rs | 97 ++ docs/php/eval.md | 7 +- src/codegen/eval_class_constant_helpers.rs | 1449 +++++++++++++++++ src/codegen/mod.rs | 8 +- tests/codegen/eval.rs | 121 ++ 10 files changed, 1895 insertions(+), 64 deletions(-) create mode 100644 src/codegen/eval_class_constant_helpers.rs diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index cb54461531..dc98b3fdaa 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -450,13 +450,7 @@ pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( }; let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; let constant_name = eval_reflection_string_arg(args[0], values)?; - let constant_names = if context.has_interface(&reflected_name) { - context.interface_constant_names(&reflected_name) - } else if context.has_trait(&reflected_name) { - context.trait_constant_names(&reflected_name) - } else { - context.class_constant_names(&reflected_name) - }; + let constant_names = eval_reflection_constant_names(&reflected_name, context, values)?; values .bool_value(constant_names.iter().any(|name| name == &constant_name)) .map(Some) @@ -542,7 +536,9 @@ pub(in crate::interpreter) fn eval_reflection_class_get_constant_result( }; let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; let constant_name = eval_reflection_string_arg(args[0], values)?; - if let Some(value) = eval_reflection_constant_value(&reflected_name, &constant_name, context) { + if let Some(value) = + eval_reflection_constant_value(&reflected_name, &constant_name, context, values)? + { return Ok(Some(value)); } values.bool_value(false).map(Some) @@ -566,13 +562,15 @@ pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( else { return Ok(None); }; - let names = eval_reflection_constant_names(&reflected_name, context); + let names = eval_reflection_constant_names(&reflected_name, context, values)?; let mut result = values.assoc_new(names.len())?; for name in names { - if !eval_reflection_constant_matches_filter(&reflected_name, &name, filter, context) { + if !eval_reflection_constant_matches_filter(&reflected_name, &name, filter, context, values)? + { continue; } - let Some(value) = eval_reflection_constant_value(&reflected_name, &name, context) else { + let Some(value) = eval_reflection_constant_value(&reflected_name, &name, context, values)? + else { continue; }; let key = values.string(&name)?; @@ -1542,7 +1540,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_resu }; let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; let requested_name = eval_reflection_string_arg(args[0], values)?; - if !eval_reflection_constant_names(&reflected_name, context) + if !eval_reflection_constant_names(&reflected_name, context, values)? .iter() .any(|name| name == &requested_name) { @@ -1570,11 +1568,12 @@ pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_res else { return Ok(None); }; - let names = eval_reflection_constant_names(&reflected_name, context); + let names = eval_reflection_constant_names(&reflected_name, context, values)?; let mut result = values.array_new(names.len())?; let mut index = 0; for name in &names { - if !eval_reflection_constant_matches_filter(&reflected_name, name, filter, context) { + if !eval_reflection_constant_matches_filter(reflected_name.as_str(), name, filter, context, values)? + { continue; } let object = @@ -1715,22 +1714,37 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( .map(Some) } -/// Returns the constant names visible through eval-backed `ReflectionClass`. +/// Returns generated/AOT constant names visible through eval ReflectionClass. +fn eval_reflection_aot_constant_names( + reflected_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = reflected_name.trim_start_matches('\\'); + let names_array = values.reflection_constant_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns constant names from eval metadata or generated/AOT runtime metadata. fn eval_reflection_constant_names( reflected_name: &str, context: &ElephcEvalContext, -) -> Vec { + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { if context.has_interface(reflected_name) { - context.interface_constant_names(reflected_name) + Ok(context.interface_constant_names(reflected_name)) } else if context.has_trait(reflected_name) { - context.trait_constant_names(reflected_name) + Ok(context.trait_constant_names(reflected_name)) + } else if context.has_class(reflected_name) || context.has_enum(reflected_name) { + Ok(context.class_constant_names(reflected_name)) } else { - context.class_constant_names(reflected_name) + eval_reflection_aot_constant_names(reflected_name, values) } } /// Returns a materialized eval constant value for Reflection without visibility checks. -fn eval_reflection_constant_value( +fn eval_reflection_eval_constant_value( reflected_name: &str, constant_name: &str, context: &ElephcEvalContext, @@ -1742,6 +1756,24 @@ fn eval_reflection_constant_value( context.class_constant_cell(&declaring_class, constant.name()) } +/// Returns a materialized eval or AOT constant value for Reflection without visibility checks. +fn eval_reflection_constant_value( + reflected_name: &str, + constant_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if eval_reflection_class_like_exists(reflected_name, context) { + return Ok(eval_reflection_eval_constant_value( + reflected_name, + constant_name, + context, + )); + } + let runtime_class_name = reflected_name.trim_start_matches('\\'); + values.reflection_constant_value(runtime_class_name, constant_name) +} + /// Builds one eval-backed `ReflectionClassConstant` object for a visible constant name. fn eval_reflection_class_constant_object_result( reflected_name: &str, @@ -1750,10 +1782,11 @@ fn eval_reflection_class_constant_object_result( values: &mut impl RuntimeValueOps, ) -> Result { let (declaring_class_name, attributes, visibility, is_final, is_enum_case) = - eval_reflection_class_constant_metadata(reflected_name, constant_name, context) + eval_reflection_class_constant_metadata(reflected_name, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let constant_value = + eval_reflection_constant_value(reflected_name, constant_name, context, values)? .ok_or(EvalStatus::RuntimeFatal)?; - let constant_value = eval_reflection_constant_value(reflected_name, constant_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); if is_enum_case { flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; @@ -1787,15 +1820,15 @@ fn eval_reflection_constant_matches_filter( constant_name: &str, filter: Option, context: &ElephcEvalContext, -) -> bool { + values: &mut impl RuntimeValueOps, +) -> Result { let Some(filter) = filter else { - return true; + return Ok(true); }; - eval_reflection_class_constant_metadata(reflected_name, constant_name, context).is_some_and( - |(_, _, visibility, is_final, _)| { + Ok(eval_reflection_class_constant_metadata(reflected_name, constant_name, context, values)? + .is_some_and(|(_, _, visibility, is_final, _)| { eval_reflection_class_constant_modifiers(visibility, is_final) & filter != 0 - }, - ) + })) } /// Resolves the declared member spelling for eval `ReflectionClass` single-member lookups. @@ -2870,14 +2903,17 @@ fn eval_reflection_class_constant_new( evaluated_args, )?; let class_name = eval_reflection_string_arg(args[0], values)?; - if !eval_reflection_class_like_exists(&class_name, context) { - return Ok(None); - } let constant_name = eval_reflection_string_arg(args[1], values)?; - let (declaring_class_name, attributes, visibility, is_final, is_enum_case) = - eval_reflection_class_constant_metadata(&class_name, &constant_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; - let constant_value = eval_reflection_constant_value(&class_name, &constant_name, context) + let Some((declaring_class_name, attributes, visibility, is_final, is_enum_case)) = + eval_reflection_class_constant_metadata(&class_name, &constant_name, context, values)? + else { + return if eval_reflection_class_like_exists(&class_name, context) { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(None) + }; + }; + let constant_value = eval_reflection_constant_value(&class_name, &constant_name, context, values)? .ok_or(EvalStatus::RuntimeFatal)?; let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); if is_enum_case { @@ -4538,19 +4574,20 @@ fn eval_reflection_class_constant_metadata( class_name: &str, constant_name: &str, context: &ElephcEvalContext, -) -> Option<(String, Vec, EvalVisibility, bool, bool)> { + values: &mut impl RuntimeValueOps, +) -> Result, EvalVisibility, bool, bool)>, EvalStatus> { if let Some(enum_decl) = context.enum_decl(class_name) { if let Some(case) = enum_decl.case(constant_name) { - return Some(( + return Ok(Some(( enum_decl.name().to_string(), case.attributes().to_vec(), EvalVisibility::Public, false, true, - )); + ))); } } - context + if let Some(metadata) = context .class_constant(class_name, constant_name) .map(|(declaring_class, constant)| { ( @@ -4560,7 +4597,30 @@ fn eval_reflection_class_constant_metadata( constant.is_final(), false, ) - }) + }) { + return Ok(Some(metadata)); + } + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_constant_flags(runtime_class_name, constant_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_constant_declaring_class(runtime_class_name, constant_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + Ok(Some(( + declaring_class, + Vec::new(), + visibility, + flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE != 0, + ))) } /// Returns true when a name resolves to an eval-declared class-like symbol. diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 1ff0e1556c..3a2d3b7078 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -94,6 +94,13 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result; + /// Reads a generated/AOT class-like constant through the generated bridge. + fn class_constant_get( + &mut self, + class_name: &str, + constant: &str, + ) -> Result, EvalStatus>; + /// Creates a shallow clone of a runtime object held in a boxed Mixed cell. fn object_clone_shallow( &mut self, @@ -199,6 +206,33 @@ pub trait RuntimeValueOps { class_name: &str, ) -> Result; + /// Returns generated AOT ReflectionClassConstant values without visibility checks. + fn reflection_constant_value( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus>; + + /// Returns generated AOT ReflectionClassConstant flags for a class/constant pair. + fn reflection_constant_flags( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus>; + + /// Returns the generated AOT declaring class for a class/constant pair. + fn reflection_constant_declaring_class( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus>; + + /// Returns generated AOT ReflectionClassConstant names visible for one class. + fn reflection_constant_names( + &mut self, + class_name: &str, + ) -> Result; + /// Returns generated/AOT interface names visible for one reflected class-like symbol. fn reflection_class_interface_names( &mut self, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 701978a7e1..e1a653b072 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2601,16 +2601,21 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( { return Ok(value); } - let class_name = resolve_eval_static_class_like_name(class_name, context)?; + let class_name = resolve_eval_static_member_class_name(class_name, context)?; if let Some(case) = context.enum_case(&class_name, constant_name) { return Ok(case); } - let (declaring_class, constant) = context - .class_constant(&class_name, constant_name) - .ok_or(EvalStatus::RuntimeFatal)?; - validate_eval_member_access(&declaring_class, constant.visibility(), context)?; - context - .class_constant_cell(&declaring_class, constant.name()) + if let Some((declaring_class, constant)) = context.class_constant(&class_name, constant_name) { + validate_eval_member_access(&declaring_class, constant.visibility(), context)?; + return context + .class_constant_cell(&declaring_class, constant.name()) + .ok_or(EvalStatus::RuntimeFatal); + } + if eval_static_member_context_owns_class(&class_name, context) { + return Err(EvalStatus::RuntimeFatal); + } + values + .class_constant_get(&class_name, constant_name)? .ok_or(EvalStatus::RuntimeFatal) } @@ -3132,19 +3137,6 @@ fn eval_static_member_context_owns_class( || context.has_enum(class_name) } -/// Resolves `self`, `parent`, `static`, and named class-like receivers for constant access. -fn resolve_eval_static_class_like_name( - class_name: &str, - context: &ElephcEvalContext, -) -> Result { - match class_name.to_ascii_lowercase().as_str() { - "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), - _ => context - .resolve_class_like_name(class_name) - .ok_or(EvalStatus::RuntimeFatal), - } -} - /// Resolves class-name literal receivers without requiring named classes to exist. fn resolve_eval_class_name_literal( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index c2fb19f3b2..b6f8e903ef 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -100,6 +100,14 @@ impl RuntimeValueOps for FakeOps { ) -> Result { Ok(false) } + /// Reports no fake AOT class constant match. + fn class_constant_get( + &mut self, + _class_name: &str, + _constant: &str, + ) -> Result, EvalStatus> { + Ok(None) + } /// Creates one shallow fake object clone. fn object_clone_shallow( &mut self, @@ -236,6 +244,37 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_reflection_property_names(class_name) } + /// Reports no fake generated AOT ReflectionClassConstant value. + fn reflection_constant_value( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports no fake generated AOT ReflectionClassConstant flags. + fn reflection_constant_flags( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports no fake generated AOT ReflectionClassConstant declaring class. + fn reflection_constant_declaring_class( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports an empty fake generated AOT ReflectionClassConstant name list. + fn reflection_constant_names( + &mut self, + _class_name: &str, + ) -> Result { + self.runtime_string_array_new(0) + } /// Reports fake generated/AOT ReflectionClass interface names for metadata bridge tests. fn reflection_class_interface_names( &mut self, diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index e9cd36518f..15b76964ff 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -72,6 +72,14 @@ unsafe extern "C" { scope_ptr: *const u8, scope_len: u64, ) -> u64; + pub(super) fn __elephc_eval_value_class_constant_get( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + scope_ptr: *const u8, + scope_len: u64, + ) -> *mut RuntimeCell; /// Returns a boxed shallow clone for stdClass/eval object storage. pub(super) fn __elephc_eval_value_object_clone_shallow( object: *mut RuntimeCell, @@ -160,6 +168,28 @@ unsafe extern "C" { class_ptr: *const u8, class_len: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_constant_value( + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_constant_flags( + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + ) -> u64; + pub(super) fn __elephc_eval_reflection_constant_declaring_class( + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_constant_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_reflection_class_interface_names( class_ptr: *const u8, class_len: u64, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 06568d0f1c..814c29da6a 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -177,6 +177,30 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(ok != 0) } + /// Reads an AOT class-like constant through the generated user helper. + fn class_constant_get( + &mut self, + class_name: &str, + constant: &str, + ) -> Result, EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ptr = unsafe { + __elephc_eval_value_class_constant_get( + class_name.as_ptr(), + class_name.len() as u64, + constant.as_ptr(), + constant.len() as u64, + scope_ptr, + scope_len, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + /// Creates a shallow clone of a boxed Mixed stdClass/eval object through the generated wrapper. fn object_clone_shallow( &mut self, @@ -427,6 +451,79 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns generated AOT ReflectionClassConstant values without visibility checks. + fn reflection_constant_value( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_constant_value( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + + /// Returns generated AOT ReflectionClassConstant flags for one constant. + fn reflection_constant_flags( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_constant_flags( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionClassConstant declaring class metadata. + fn reflection_constant_declaring_class( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_constant_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionClassConstant names visible for one class. + fn reflection_constant_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_constant_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + /// Returns generated AOT interface names visible for one reflected class-like symbol. fn reflection_class_interface_names( &mut self, diff --git a/docs/php/eval.md b/docs/php/eval.md index 539c5c8956..cef2b0f6d6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and class constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. | @@ -274,7 +274,10 @@ member lists to be materialized on the `ReflectionClass` object. `getReflectionConstant()`, and `getReflectionConstants()` expose eval-visible class constants, interface constants, trait constants, enum constants, enum cases, supported materialized property defaults, and current eval-declared -static property values. Constant lookup is case-sensitive; single-value +static property values. For generated/AOT class-like symbols, the constant APIs +also expose materializable scalar, string, null, `::class`, simple arithmetic or +concatenation, and enum-case constant metadata through runtime hooks. Constant +lookup is case-sensitive; single-value lookups return `false` when no constant or case is visible. `getConstants()` and `getReflectionConstants()` accept PHP's `ReflectionClassConstant::IS_*` visibility/finality filter bitmask; `null` means no filter and `0` returns no diff --git a/src/codegen/eval_class_constant_helpers.rs b/src/codegen/eval_class_constant_helpers.rs new file mode 100644 index 0000000000..97f9f7ef9b --- /dev/null +++ b/src/codegen/eval_class_constant_helpers.rs @@ -0,0 +1,1449 @@ +//! Purpose: +//! Emits user-assembly helpers that let libelephc-magician read generated/AOT +//! class-like constants and their Reflection metadata from eval fragments. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()` when an EIR module uses eval. +//! +//! Key details: +//! - Direct `Aot::CONST` reads enforce PHP visibility with the active eval class scope. +//! - Reflection probes intentionally bypass visibility, matching PHP Reflection. +//! - Values are limited to the same scalar and enum-case constant forms emitted by static Reflection metadata. + +use std::collections::HashSet; + +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; +use crate::codegen::{abi, emit_box_current_value_as_mixed}; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::{enum_case_symbol, php_symbol_key}; +use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, Visibility}; +use crate::types::{ClassInfo, InterfaceInfo, PhpType}; + +const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; +const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; +const EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE: u64 = 128; + +/// Constant slot metadata needed by eval direct reads and Reflection probes. +#[derive(Clone)] +struct EvalClassConstantSlot { + reflected_class: String, + declaring_class: String, + allowed_scopes: Vec, + constant: String, + visibility: Visibility, + is_final: bool, + is_enum_case: bool, + value: EvalClassConstantValue, +} + +/// Constant value forms the eval bridge can materialize as boxed Mixed cells. +#[derive(Clone)] +enum EvalClassConstantValue { + Int(i64), + Bool(bool), + Float(f64), + Str(String), + Null, + EnumCase { enum_name: String, case_name: String }, +} + +/// Emits eval class-constant helpers when any lowered function owns an eval context. +pub(super) fn emit_eval_class_constant_helpers( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + if !module_uses_eval(module) { + return; + } + let slots = collect_eval_class_constant_slots(module); + emit_class_constant_get_helper(module, emitter, data, &slots); + emit_reflection_constant_value_helper(module, emitter, data, &slots); + emit_reflection_constant_names_helper(module, emitter, data, &slots); + emit_reflection_constant_flags_helper(module, emitter, data, &slots); + emit_reflection_constant_declaring_class_helper(module, emitter, data, &slots); +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Collects class-like constants visible to direct eval reads and Reflection APIs. +fn collect_eval_class_constant_slots(module: &Module) -> Vec { + let mut slots = Vec::new(); + let mut classes = module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, _) in classes { + collect_reflected_class_constant_slots(module, class_name, &mut slots); + } + + let mut interfaces = module.interface_infos.iter().collect::>(); + interfaces.sort_by_key(|(_, interface_info)| interface_info.interface_id); + for (interface_name, _) in interfaces { + collect_reflected_interface_constant_slots(module, interface_name, &mut slots); + } + + let mut traits = module.declared_trait_names.clone(); + traits.sort(); + for trait_name in traits { + collect_reflected_trait_constant_slots(module, &trait_name, &mut slots); + } + slots +} + +/// Adds constants visible when reflecting or reading one generated/AOT class. +fn collect_reflected_class_constant_slots( + module: &Module, + class_name: &str, + slots: &mut Vec, +) { + for constant_name in class_constant_names(module, class_name) { + let slot = if let Some(slot) = enum_case_slot(module, class_name, &constant_name) { + Some(slot) + } else { + resolve_class_constant_slot(module, class_name, &constant_name) + }; + if let Some(mut slot) = slot { + slot.reflected_class = class_name.to_string(); + slots.push(slot); + } + } +} + +/// Adds constants visible when reflecting or reading one generated/AOT interface. +fn collect_reflected_interface_constant_slots( + module: &Module, + interface_name: &str, + slots: &mut Vec, +) { + for constant_name in interface_constant_names(module, interface_name) { + if let Some(mut slot) = resolve_interface_constant_slot(module, interface_name, &constant_name) { + slot.reflected_class = interface_name.to_string(); + slots.push(slot); + } + } +} + +/// Adds constants visible when reflecting or reading one generated/AOT trait. +fn collect_reflected_trait_constant_slots( + module: &Module, + trait_name: &str, + slots: &mut Vec, +) { + let Some(constants) = module.declared_trait_constants.get(trait_name) else { + return; + }; + let mut names = module + .declared_trait_constant_names + .get(trait_name) + .cloned() + .unwrap_or_else(|| constants.keys().cloned().collect()); + names.sort(); + for constant_name in names { + if let Some(mut slot) = trait_constant_slot(module, trait_name, &constant_name) { + slot.reflected_class = trait_name.to_string(); + slots.push(slot); + } + } +} + +/// Returns class constant names in the same order as eval-backed ReflectionClass. +fn class_constant_names(module: &Module, class_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + if let Some(enum_info) = module.enum_infos.get(class_name) { + for case in &enum_info.cases { + push_unique_constant_name(&case.name, &mut names, &mut seen); + } + } + for (declaring_class, class_info) in class_chain(module, class_name) { + let mut own = class_info.constants.keys().cloned().collect::>(); + own.sort(); + for constant_name in own { + if is_private_inherited_class_constant( + class_name, + declaring_class, + class_info, + &constant_name, + ) { + continue; + } + push_unique_constant_name(&constant_name, &mut names, &mut seen); + } + for interface_name in &class_info.interfaces { + for constant_name in interface_constant_names(module, interface_name) { + push_unique_constant_name(&constant_name, &mut names, &mut seen); + } + } + } + names +} + +/// Returns whether a parent private constant is hidden from a reflected child. +fn is_private_inherited_class_constant( + reflected_class: &str, + declaring_class: &str, + declaring_info: &ClassInfo, + constant_name: &str, +) -> bool { + php_symbol_key(reflected_class) != php_symbol_key(declaring_class) + && declaring_info + .constant_visibilities + .get(constant_name) + .is_some_and(|visibility| matches!(visibility, Visibility::Private)) +} + +/// Returns the inheritance chain from reflected class toward its parents. +fn class_chain<'a>(module: &'a Module, class_name: &'a str) -> Vec<(&'a str, &'a ClassInfo)> { + let mut result = Vec::new(); + let mut current = Some(class_name); + let mut seen = HashSet::new(); + while let Some(name) = current { + let Some((resolved_name, info)) = resolve_class(module, name) else { + break; + }; + if !seen.insert(php_symbol_key(resolved_name)) { + break; + } + result.push((resolved_name, info)); + current = info.parent.as_deref(); + } + result +} + +/// Returns interface constant names with parent interfaces first. +fn interface_constant_names(module: &Module, interface_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + collect_interface_constant_names(module, interface_name, &mut names, &mut seen); + names +} + +/// Recursively collects interface constant names without duplicates. +fn collect_interface_constant_names( + module: &Module, + interface_name: &str, + names: &mut Vec, + seen: &mut HashSet, +) { + let Some((_, interface_info)) = resolve_interface(module, interface_name) else { + return; + }; + for parent in &interface_info.parents { + collect_interface_constant_names(module, parent, names, seen); + } + let mut own = interface_info.constants.keys().cloned().collect::>(); + own.sort(); + for constant_name in own { + push_unique_constant_name(&constant_name, names, seen); + } +} + +/// Pushes one case-sensitive PHP constant name once. +fn push_unique_constant_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + if seen.insert(name.to_string()) { + names.push(name.to_string()); + } +} + +/// Resolves one class constant against class parents and implemented interfaces. +fn resolve_class_constant_slot( + module: &Module, + class_name: &str, + constant_name: &str, +) -> Option { + let (resolved_name, class_info) = resolve_class(module, class_name)?; + if let Some(value_expr) = class_info.constants.get(constant_name) { + return class_constant_slot(module, resolved_name, class_info, constant_name, value_expr); + } + if let Some(parent) = class_info.parent.as_deref() { + if let Some(slot) = resolve_class_constant_slot(module, parent, constant_name) { + return Some(slot); + } + } + for interface_name in &class_info.interfaces { + if let Some(slot) = resolve_interface_constant_slot(module, interface_name, constant_name) { + return Some(slot); + } + } + None +} + +/// Builds metadata for one class-declared constant. +fn class_constant_slot( + module: &Module, + declaring_class: &str, + class_info: &ClassInfo, + constant_name: &str, + value_expr: &Expr, +) -> Option { + let value = eval_class_constant_value(module, declaring_class, Some(class_info), value_expr, 0)?; + let visibility = class_info + .constant_visibilities + .get(constant_name) + .cloned() + .unwrap_or(Visibility::Public); + Some(EvalClassConstantSlot { + reflected_class: declaring_class.to_string(), + declaring_class: declaring_class.to_string(), + allowed_scopes: visibility_scope_names(module, declaring_class, &visibility), + constant: constant_name.to_string(), + visibility, + is_final: class_info.final_constants.contains(constant_name), + is_enum_case: false, + value, + }) +} + +/// Resolves one interface constant and preserves its original declaring interface. +fn resolve_interface_constant_slot( + module: &Module, + interface_name: &str, + constant_name: &str, +) -> Option { + let (resolved_name, interface_info) = resolve_interface(module, interface_name)?; + let value_expr = interface_info.constants.get(constant_name)?; + let declaring_interface = + interface_constant_declaring_interface(interface_info, resolved_name, constant_name); + let value = eval_class_constant_value(module, declaring_interface, None, value_expr, 0)?; + Some(EvalClassConstantSlot { + reflected_class: resolved_name.to_string(), + declaring_class: declaring_interface.to_string(), + allowed_scopes: Vec::new(), + constant: constant_name.to_string(), + visibility: Visibility::Public, + is_final: module + .interface_infos + .get(declaring_interface) + .is_some_and(|info| info.final_constants.contains(constant_name)), + is_enum_case: false, + value, + }) +} + +/// Builds metadata for one trait-declared constant. +fn trait_constant_slot( + module: &Module, + trait_name: &str, + constant_name: &str, +) -> Option { + let value_expr = module + .declared_trait_constants + .get(trait_name) + .and_then(|constants| constants.get(constant_name))?; + let value = eval_class_constant_value(module, trait_name, None, value_expr, 0)?; + let visibility = module + .declared_trait_constant_visibilities + .get(trait_name) + .and_then(|constants| constants.get(constant_name)) + .cloned() + .unwrap_or(Visibility::Public); + Some(EvalClassConstantSlot { + reflected_class: trait_name.to_string(), + declaring_class: trait_name.to_string(), + allowed_scopes: visibility_scope_names(module, trait_name, &visibility), + constant: constant_name.to_string(), + visibility, + is_final: module + .declared_trait_final_constants + .get(trait_name) + .is_some_and(|constants| constants.contains(constant_name)), + is_enum_case: false, + value, + }) +} + +/// Builds metadata for one enum case exposed as a class constant. +fn enum_case_slot( + module: &Module, + enum_name: &str, + case_name: &str, +) -> Option { + let enum_info = module.enum_infos.get(enum_name)?; + enum_info.cases.iter().any(|case| case.name == case_name).then(|| { + EvalClassConstantSlot { + reflected_class: enum_name.to_string(), + declaring_class: enum_name.to_string(), + allowed_scopes: Vec::new(), + constant: case_name.to_string(), + visibility: Visibility::Public, + is_final: false, + is_enum_case: true, + value: EvalClassConstantValue::EnumCase { + enum_name: enum_name.to_string(), + case_name: case_name.to_string(), + }, + } + }) +} + +/// Returns the interface that originally declared one inherited constant. +fn interface_constant_declaring_interface<'a>( + info: &'a InterfaceInfo, + fallback_interface: &'a str, + constant_name: &str, +) -> &'a str { + info.constant_declaring_interfaces + .get(constant_name) + .map(String::as_str) + .unwrap_or(fallback_interface) +} + +/// Evaluates one supported AOT class-like constant expression. +fn eval_class_constant_value( + module: &Module, + current_class: &str, + current_info: Option<&ClassInfo>, + expr: &Expr, + depth: usize, +) -> Option { + if depth > 16 { + return None; + } + match &expr.kind { + ExprKind::IntLiteral(value) => Some(EvalClassConstantValue::Int(*value)), + ExprKind::BoolLiteral(value) => Some(EvalClassConstantValue::Bool(*value)), + ExprKind::FloatLiteral(value) => Some(EvalClassConstantValue::Float(*value)), + ExprKind::StringLiteral(value) => Some(EvalClassConstantValue::Str(value.clone())), + ExprKind::Null => Some(EvalClassConstantValue::Null), + ExprKind::Negate(inner) => { + match eval_class_constant_value(module, current_class, current_info, inner, depth + 1)? { + EvalClassConstantValue::Int(value) => { + value.checked_neg().map(EvalClassConstantValue::Int) + } + EvalClassConstantValue::Float(value) => Some(EvalClassConstantValue::Float(-value)), + _ => None, + } + } + ExprKind::BinaryOp { left, op, right } => { + eval_binary_class_constant_value(module, current_class, current_info, left, op, right, depth + 1) + } + ExprKind::ClassConstant { receiver } => { + let class_name = static_receiver_name(current_class, current_info, receiver)?; + Some(EvalClassConstantValue::Str(class_name)) + } + ExprKind::ScopedConstantAccess { receiver, name } => { + let class_name = static_receiver_name(current_class, current_info, receiver)?; + eval_scoped_class_constant_value(module, &class_name, name, depth + 1) + } + _ => None, + } +} + +/// Evaluates one supported binary class-constant expression. +fn eval_binary_class_constant_value( + module: &Module, + current_class: &str, + current_info: Option<&ClassInfo>, + left: &Expr, + op: &BinOp, + right: &Expr, + depth: usize, +) -> Option { + let left = eval_class_constant_value(module, current_class, current_info, left, depth)?; + let right = eval_class_constant_value(module, current_class, current_info, right, depth)?; + match (left, op, right) { + (EvalClassConstantValue::Int(left), BinOp::Add, EvalClassConstantValue::Int(right)) => { + left.checked_add(right).map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Int(left), BinOp::Sub, EvalClassConstantValue::Int(right)) => { + left.checked_sub(right).map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Int(left), BinOp::Mul, EvalClassConstantValue::Int(right)) => { + left.checked_mul(right).map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Int(left), BinOp::Mod, EvalClassConstantValue::Int(right)) => { + left.checked_rem(right).map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Int(left), BinOp::Pow, EvalClassConstantValue::Int(right)) + if right >= 0 => + { + let exponent = u32::try_from(right).ok()?; + left.checked_pow(exponent).map(EvalClassConstantValue::Int) + } + ( + EvalClassConstantValue::Str(left), + BinOp::Concat, + EvalClassConstantValue::Str(right), + ) => Some(EvalClassConstantValue::Str(format!("{}{}", left, right))), + _ => None, + } +} + +/// Evaluates a scoped class-like constant expression. +fn eval_scoped_class_constant_value( + module: &Module, + class_name: &str, + constant_name: &str, + depth: usize, +) -> Option { + if let Some((resolved_name, info)) = resolve_class(module, class_name) { + if let Some(value_expr) = info.constants.get(constant_name) { + return eval_class_constant_value(module, resolved_name, Some(info), value_expr, depth); + } + if let Some(parent) = info.parent.as_deref() { + if let Some(value) = eval_scoped_class_constant_value(module, parent, constant_name, depth) { + return Some(value); + } + } + for interface_name in &info.interfaces { + if let Some(value) = eval_scoped_class_constant_value(module, interface_name, constant_name, depth) { + return Some(value); + } + } + } + if let Some((resolved_name, info)) = resolve_interface(module, class_name) { + if let Some(value_expr) = info.constants.get(constant_name) { + return eval_class_constant_value(module, resolved_name, None, value_expr, depth); + } + } + if let Some(value_expr) = module + .declared_trait_constants + .get(class_name) + .and_then(|constants| constants.get(constant_name)) + { + return eval_class_constant_value(module, class_name, None, value_expr, depth); + } + if module + .enum_infos + .get(class_name) + .is_some_and(|info| info.cases.iter().any(|case| case.name == constant_name)) + { + return Some(EvalClassConstantValue::EnumCase { + enum_name: class_name.to_string(), + case_name: constant_name.to_string(), + }); + } + None +} + +/// Resolves `self`, `static`, `parent`, or named receivers in constant expressions. +fn static_receiver_name( + current_class: &str, + current_info: Option<&ClassInfo>, + receiver: &StaticReceiver, +) -> Option { + match receiver { + StaticReceiver::Named(name) => Some(name.as_str().trim_start_matches('\\').to_string()), + StaticReceiver::Self_ | StaticReceiver::Static => Some(current_class.to_string()), + StaticReceiver::Parent => current_info.and_then(|info| info.parent.clone()), + } +} + +/// Looks up class metadata by PHP-style case-insensitive name. +fn resolve_class<'a>(module: &'a Module, class_name: &str) -> Option<(&'a str, &'a ClassInfo)> { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + module + .class_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == class_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Looks up interface metadata by PHP-style case-insensitive name. +fn resolve_interface<'a>( + module: &'a Module, + interface_name: &str, +) -> Option<(&'a str, &'a InterfaceInfo)> { + let interface_key = php_symbol_key(interface_name.trim_start_matches('\\')); + module + .interface_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == interface_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Emits `__elephc_eval_value_class_constant_get(class, constant, scope) -> Mixed*`. +fn emit_class_constant_get_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user class constant get ---"); + label_c_global(module, emitter, "__elephc_eval_value_class_constant_get"); + match module.target.arch { + Arch::AArch64 => emit_value_helper_aarch64(module, emitter, data, slots, ValueHelperMode::DirectGet), + Arch::X86_64 => emit_value_helper_x86_64(module, emitter, data, slots, ValueHelperMode::DirectGet), + } +} + +/// Emits `__elephc_eval_reflection_constant_value(class, constant) -> Mixed*`. +fn emit_reflection_constant_value_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: reflection class constant value ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_constant_value"); + match module.target.arch { + Arch::AArch64 => emit_value_helper_aarch64(module, emitter, data, slots, ValueHelperMode::ReflectionValue), + Arch::X86_64 => emit_value_helper_x86_64(module, emitter, data, slots, ValueHelperMode::ReflectionValue), + } +} + +/// Distinguishes direct class-constant reads from Reflection value probes. +#[derive(Clone, Copy)] +enum ValueHelperMode { + DirectGet, + ReflectionValue, +} + +impl ValueHelperMode { + /// Returns the shared label suffix for this value helper mode. + const fn suffix(self) -> &'static str { + match self { + Self::DirectGet => "get", + Self::ReflectionValue => "reflection_value", + } + } + + /// Returns whether this helper must enforce constant visibility. + const fn checks_visibility(self) -> bool { + matches!(self, Self::DirectGet) + } +} + +/// Emits an ARM64 class-constant value helper body. +fn emit_value_helper_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, +) { + let done_label = format!("__elephc_eval_class_constant_{}_done", mode.suffix()); + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class, constant, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + if mode.checks_visibility() { + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length + } + emit_aarch64_constant_value_dispatch(module, emitter, data, slots, mode); + emitter.instruction("mov x0, xzr"); // report bridge miss with a null pointer + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emit_aarch64_value_slot_bodies(module, emitter, data, slots, mode, &done_label); + emitter.label(&done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed class constant value to Rust +} + +/// Emits an x86_64 class-constant value helper body. +fn emit_value_helper_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, +) { + let done_label = format!("__elephc_eval_class_constant_{}_done_x", mode.suffix()); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, constant, and scope slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + if mode.checks_visibility() { + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope length + } + emit_x86_64_constant_value_dispatch(module, emitter, data, slots, mode); + emitter.instruction("xor eax, eax"); // report bridge miss with a null pointer + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emit_x86_64_value_slot_bodies(module, emitter, data, slots, mode, &done_label); + emitter.label(&done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed class constant value to Rust +} + +/// Emits ARM64 class-name and constant-name dispatch for value helpers. +fn emit_aarch64_constant_value_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, +) { + for slot in slots { + let next_label = slot_miss_label(module, slot, mode.suffix()); + emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_aarch64_constant_name_compare(module, emitter, data, slot, mode, &next_label); + emitter.label(&next_label); + } +} + +/// Emits x86_64 class-name and constant-name dispatch for value helpers. +fn emit_x86_64_constant_value_dispatch( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, +) { + for slot in slots { + let next_label = slot_miss_label(module, slot, mode.suffix()); + emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_x86_64_constant_name_compare(module, emitter, data, slot, mode, &next_label); + emitter.label(&next_label); + } +} + +/// Emits one ARM64 case-insensitive class-name comparison. +fn emit_aarch64_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #0]"); // reload requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction(&format!("cbnz x0, {}", next_label)); // continue dispatch when class names differ +} + +/// Emits one x86_64 case-insensitive class-name comparison. +fn emit_x86_64_class_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + class_name: &str, + next_label: &str, +) { + let (label, len) = data.add_string(class_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested class-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the class names matched + emitter.instruction(&format!("jne {}", next_label)); // continue dispatch when class names differ +} + +/// Emits one ARM64 case-sensitive constant-name comparison. +fn emit_aarch64_constant_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + mode: ValueHelperMode, + next_label: &str, +) { + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + let target_label = slot_body_label(module, slot, mode.suffix()); + if !mode.checks_visibility() || matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the constant value body when names match + return; + } + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + emit_aarch64_constant_scope_check(emitter, data, slot, &target_label, next_label); +} + +/// Emits one x86_64 case-sensitive constant-name comparison. +fn emit_x86_64_constant_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + mode: ValueHelperMode, + next_label: &str, +) { + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched + let target_label = slot_body_label(module, slot, mode.suffix()); + if !mode.checks_visibility() || matches!(slot.visibility, Visibility::Public) { + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the constant value body when names match + return; + } + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + emit_x86_64_constant_scope_check(emitter, data, slot, &target_label, next_label); +} + +/// Emits ARM64 visibility checks for a protected/private constant bridge hit. +fn emit_aarch64_constant_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + target_label: &str, + next_label: &str, +) { + emitter.instruction("ldr x1, [sp, #32]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", next_label)); // reject scoped constants outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction("ldr x1, [sp, #32]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch when scoped visibility is satisfied + } + emitter.instruction(&format!("b {}", next_label)); // continue dispatch after a visibility miss +} + +/// Emits x86_64 visibility checks for a protected/private constant bridge hit. +fn emit_x86_64_constant_scope_check( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + target_label: &str, + next_label: &str, +) { + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", next_label)); // reject scoped constants outside a class scope + for scope_name in &slot.allowed_scopes { + let (label, len) = data.add_string(scope_name.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", target_label)); // dispatch when scoped visibility is satisfied + } + emitter.instruction(&format!("jmp {}", next_label)); // continue dispatch after a visibility miss +} + +/// Emits ARM64 value bodies for all class-constant slots. +fn emit_aarch64_value_slot_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, mode.suffix())); + emit_aarch64_constant_value(emitter, data, &slot.value); + emitter.instruction(&format!("b {}", done_label)); // return after boxing the constant value + } +} + +/// Emits x86_64 value bodies for all class-constant slots. +fn emit_x86_64_value_slot_bodies( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], + mode: ValueHelperMode, + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, mode.suffix())); + emit_x86_64_constant_value(emitter, data, &slot.value); + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the constant value + } +} + +/// Emits one ARM64 boxed Mixed value for a supported class constant. +fn emit_aarch64_constant_value( + emitter: &mut Emitter, + data: &mut DataSection, + value: &EvalClassConstantValue, +) { + match value { + EvalClassConstantValue::Int(value) => { + abi::emit_load_int_immediate(emitter, "x0", *value); + emit_box_current_value_as_mixed(emitter, &PhpType::Int); + } + EvalClassConstantValue::Bool(value) => { + abi::emit_load_int_immediate(emitter, "x0", i64::from(*value)); + emit_box_current_value_as_mixed(emitter, &PhpType::Bool); + } + EvalClassConstantValue::Float(value) => { + let label = data.add_float(*value); + abi::emit_symbol_address(emitter, "x9", &label); + emitter.instruction("ldr d0, [x9]"); // load the float constant through the data-section symbol + emit_box_current_value_as_mixed(emitter, &PhpType::Float); + } + EvalClassConstantValue::Str(value) => { + let (label, len) = data.add_string(value.as_bytes()); + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emit_box_current_value_as_mixed(emitter, &PhpType::Str); + } + EvalClassConstantValue::Null => { + abi::emit_load_int_immediate(emitter, "x0", 0x7fff_ffff_ffff_fffe); + emit_box_current_value_as_mixed(emitter, &PhpType::Void); + } + EvalClassConstantValue::EnumCase { + enum_name, + case_name, + } => { + let case_label = enum_case_symbol(enum_name, case_name); + abi::emit_load_symbol_to_reg(emitter, "x0", &case_label, 0); + emit_box_current_value_as_mixed(emitter, &PhpType::Object(enum_name.clone())); + } + } +} + +/// Emits one x86_64 boxed Mixed value for a supported class constant. +fn emit_x86_64_constant_value( + emitter: &mut Emitter, + data: &mut DataSection, + value: &EvalClassConstantValue, +) { + match value { + EvalClassConstantValue::Int(value) => { + abi::emit_load_int_immediate(emitter, "rax", *value); + emit_box_current_value_as_mixed(emitter, &PhpType::Int); + } + EvalClassConstantValue::Bool(value) => { + abi::emit_load_int_immediate(emitter, "rax", i64::from(*value)); + emit_box_current_value_as_mixed(emitter, &PhpType::Bool); + } + EvalClassConstantValue::Float(value) => { + let label = data.add_float(*value); + abi::emit_symbol_address(emitter, "r10", &label); + emitter.instruction("movsd xmm0, QWORD PTR [r10]"); // load the float constant through the data-section symbol + emit_box_current_value_as_mixed(emitter, &PhpType::Float); + } + EvalClassConstantValue::Str(value) => { + let (label, len) = data.add_string(value.as_bytes()); + abi::emit_symbol_address(emitter, "rax", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emit_box_current_value_as_mixed(emitter, &PhpType::Str); + } + EvalClassConstantValue::Null => { + abi::emit_load_int_immediate(emitter, "rax", 0x7fff_ffff_ffff_fffe); + emit_box_current_value_as_mixed(emitter, &PhpType::Void); + } + EvalClassConstantValue::EnumCase { + enum_name, + case_name, + } => { + let case_label = enum_case_symbol(enum_name, case_name); + abi::emit_load_symbol_to_reg(emitter, "rax", &case_label, 0); + emit_box_current_value_as_mixed(emitter, &PhpType::Object(enum_name.clone())); + } + } +} + +/// Emits `__elephc_eval_reflection_constant_names(class) -> string-array Mixed*`. +fn emit_reflection_constant_names_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: reflection class constant names ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_constant_names"); + match module.target.arch { + Arch::AArch64 => emit_reflection_constant_names_aarch64(emitter, data, slots), + Arch::X86_64 => emit_reflection_constant_names_x86_64(emitter, data, slots), + } +} + +/// Emits the ARM64 Reflection constant-name array helper. +fn emit_reflection_constant_names_aarch64( + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_names_done"; + let fail_label = "__elephc_eval_reflection_constant_names_fail"; + let array_new = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_new"); + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class slice, array, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + abi::emit_load_int_immediate(emitter, "x0", slots.len() as i64); + emitter.instruction(&format!("bl {}", array_new)); // allocate the boxed string-array result + emitter.instruction(&format!("cbz x0, {}", fail_label)); // fail if the runtime could not allocate the result array + emitter.instruction("str x0, [sp, #16]"); // save the accumulated boxed string array + for (index, slot) in slots.iter().enumerate() { + emit_aarch64_constant_name_push(emitter, data, slot, index, fail_label); + } + emitter.instruction("ldr x0, [sp, #16]"); // return the accumulated boxed string array + emitter.instruction(&format!("b {}", done_label)); // skip null failure after a successful scan + emitter.label(fail_label); + emitter.instruction("mov x0, xzr"); // return null when allocation or append fails + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed constant-name array to Rust +} + +/// Emits the x86_64 Reflection constant-name array helper. +fn emit_reflection_constant_names_x86_64( + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_names_done_x"; + let fail_label = "__elephc_eval_reflection_constant_names_fail_x"; + let array_new = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_new"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class slice and result array + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + abi::emit_load_int_immediate(emitter, "rdi", slots.len() as i64); + emitter.instruction(&format!("call {}", array_new)); // allocate the boxed string-array result + emitter.instruction("test rax, rax"); // check whether allocation returned a boxed array + emitter.instruction(&format!("jz {}", fail_label)); // fail if the runtime could not allocate the result array + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the accumulated boxed string array + for (index, slot) in slots.iter().enumerate() { + emit_x86_64_constant_name_push(emitter, data, slot, index, fail_label); + } + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the accumulated boxed string array + emitter.instruction(&format!("jmp {}", done_label)); // skip null failure after a successful scan + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // return null when allocation or append fails + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed constant-name array to Rust +} + +/// Emits one ARM64 conditional append for a reflected constant name. +fn emit_aarch64_constant_name_push( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + index: usize, + fail_label: &str, +) { + let skip_label = format!("__elephc_eval_reflection_constant_names_skip_{}", index); + let push_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_push"); + emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &skip_label); + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed result string array + abi::emit_symbol_address(emitter, "x1", &label); + abi::emit_load_int_immediate(emitter, "x2", len as i64); + emitter.instruction(&format!("bl {}", push_symbol)); // append the matched constant name to the result array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // fail if appending returned a null array pointer + emitter.instruction("str x0, [sp, #16]"); // save the updated boxed string array + emitter.label(&skip_label); +} + +/// Emits one x86_64 conditional append for a reflected constant name. +fn emit_x86_64_constant_name_push( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + index: usize, + fail_label: &str, +) { + let skip_label = format!("__elephc_eval_reflection_constant_names_skip_{}_x", index); + let push_symbol = emitter + .target + .extern_symbol("__elephc_eval_value_string_array_push"); + emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &skip_label); + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the boxed result string array + abi::emit_symbol_address(emitter, "rsi", &label); + abi::emit_load_int_immediate(emitter, "rdx", len as i64); + emitter.instruction(&format!("call {}", push_symbol)); // append the matched constant name to the result array + emitter.instruction("test rax, rax"); // check whether append returned an updated array + emitter.instruction(&format!("jz {}", fail_label)); // fail if appending returned a null array pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the updated boxed string array + emitter.label(&skip_label); +} + +/// Emits `__elephc_eval_reflection_constant_flags(class, constant) -> flags`. +fn emit_reflection_constant_flags_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: reflection class constant flags ---"); + label_c_global(module, emitter, "__elephc_eval_reflection_constant_flags"); + match module.target.arch { + Arch::AArch64 => emit_reflection_constant_flags_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_reflection_constant_flags_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 Reflection constant flags helper. +fn emit_reflection_constant_flags_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_flags_done"; + emitter.instruction("sub sp, sp, #48"); // reserve helper frame for class/constant slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + for slot in slots { + let next_label = slot_miss_label(module, slot, "flags"); + emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_aarch64_flags_constant_name_compare(module, emitter, data, slot, &next_label); + emitter.label(&next_label); + } + emitter.instruction("mov x0, #0"); // return zero when no constant metadata matched + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #48"); // release the helper frame + emitter.instruction("ret"); // return the matched flags, or zero, to Rust +} + +/// Emits the x86_64 Reflection constant flags helper. +fn emit_reflection_constant_flags_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_flags_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and constant slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + for slot in slots { + let next_label = slot_miss_label(module, slot, "flags_x"); + emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_x86_64_flags_constant_name_compare(module, emitter, data, slot, &next_label); + emitter.label(&next_label); + } + emitter.instruction("xor eax, eax"); // return zero when no constant metadata matched + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the matched flags, or zero, to Rust +} + +/// Emits an ARM64 constant-name comparison that returns flags on a match. +fn emit_aarch64_flags_constant_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + next_label: &str, +) { + let done_label = "__elephc_eval_reflection_constant_flags_done"; + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "x3", &label); + abi::emit_load_int_immediate(emitter, "x4", len as i64); + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + abi::emit_load_int_immediate(emitter, "x0", slot_flags(slot) as i64); + emitter.instruction(&format!("b {}", done_label)); // return the matched constant flags + let _ = module; +} + +/// Emits an x86_64 constant-name comparison that returns flags on a match. +fn emit_x86_64_flags_constant_name_compare( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + next_label: &str, +) { + let done_label = "__elephc_eval_reflection_constant_flags_done_x"; + let (label, len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "rdx", &label); + abi::emit_load_int_immediate(emitter, "rcx", len as i64); + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + abi::emit_load_int_immediate(emitter, "rax", slot_flags(slot) as i64); + emitter.instruction(&format!("jmp {}", done_label)); // return the matched constant flags + let _ = module; +} + +/// Emits `__elephc_eval_reflection_constant_declaring_class(class, constant) -> string Mixed*`. +fn emit_reflection_constant_declaring_class_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: reflection class constant declaring class ---"); + label_c_global( + module, + emitter, + "__elephc_eval_reflection_constant_declaring_class", + ); + match module.target.arch { + Arch::AArch64 => emit_reflection_constant_declaring_class_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_reflection_constant_declaring_class_x86_64(module, emitter, data, slots), + } +} + +/// Emits the ARM64 Reflection constant declaring-class helper. +fn emit_reflection_constant_declaring_class_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_declaring_class_done"; + emitter.instruction("sub sp, sp, #48"); // reserve helper frame for class/constant slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + for slot in slots { + let next_label = slot_miss_label(module, slot, "declaring"); + emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_aarch64_declaring_constant_name_compare(emitter, data, slot, &next_label, done_label); + emitter.label(&next_label); + } + emitter.instruction("mov x0, xzr"); // return null when no constant metadata matched + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #48"); // release the helper frame + emitter.instruction("ret"); // return the declaring class string, or null, to Rust +} + +/// Emits the x86_64 Reflection constant declaring-class helper. +fn emit_reflection_constant_declaring_class_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalClassConstantSlot], +) { + let done_label = "__elephc_eval_reflection_constant_declaring_class_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and constant slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + for slot in slots { + let next_label = slot_miss_label(module, slot, "declaring_x"); + emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); + emit_x86_64_declaring_constant_name_compare(emitter, data, slot, &next_label, done_label); + emitter.label(&next_label); + } + emitter.instruction("xor eax, eax"); // return null when no constant metadata matched + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the declaring class string, or null, to Rust +} + +/// Emits an ARM64 constant-name comparison that returns declaring class on a match. +fn emit_aarch64_declaring_constant_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + next_label: &str, + done_label: &str, +) { + let (constant_label, constant_len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "x3", &constant_label); + abi::emit_load_int_immediate(emitter, "x4", constant_len as i64); + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + let (class_label, class_len) = data.add_string(slot.declaring_class.as_bytes()); + abi::emit_symbol_address(emitter, "x1", &class_label); + abi::emit_load_int_immediate(emitter, "x2", class_len as i64); + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction(&format!("b {}", done_label)); // return the matched declaring class name +} + +/// Emits an x86_64 constant-name comparison that returns declaring class on a match. +fn emit_x86_64_declaring_constant_name_compare( + emitter: &mut Emitter, + data: &mut DataSection, + slot: &EvalClassConstantSlot, + next_label: &str, + done_label: &str, +) { + let (constant_label, constant_len) = data.add_string(slot.constant.as_bytes()); + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + abi::emit_symbol_address(emitter, "rdx", &constant_label); + abi::emit_load_int_immediate(emitter, "rcx", constant_len as i64); + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + let (class_label, class_len) = data.add_string(slot.declaring_class.as_bytes()); + abi::emit_symbol_address(emitter, "rdi", &class_label); + abi::emit_load_int_immediate(emitter, "rsi", class_len as i64); + abi::emit_load_int_immediate(emitter, "rax", 1); + emitter.instruction("call __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction(&format!("jmp {}", done_label)); // return the matched declaring class name +} + +/// Returns ReflectionClassConstant-style member flags for one slot. +fn slot_flags(slot: &EvalClassConstantSlot) -> u64 { + let mut flags = match slot.visibility { + Visibility::Public => EVAL_REFLECTION_MEMBER_FLAG_PUBLIC, + Visibility::Protected => EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, + Visibility::Private => EVAL_REFLECTION_MEMBER_FLAG_PRIVATE, + }; + if slot.is_final { + flags |= EVAL_REFLECTION_MEMBER_FLAG_FINAL; + } + if slot.is_enum_case { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + } + flags +} + +/// Returns class scopes that satisfy one constant visibility for a declaring class. +fn visibility_scope_names( + module: &Module, + declaring_class: &str, + visibility: &Visibility, +) -> Vec { + match visibility { + Visibility::Public => Vec::new(), + Visibility::Private => vec![declaring_class.to_string()], + Visibility::Protected => related_class_scope_names(module, declaring_class), + } +} + +/// Returns AOT classes in the same inheritance line as `declaring_class`. +fn related_class_scope_names(module: &Module, declaring_class: &str) -> Vec { + let mut scopes = module + .class_infos + .keys() + .filter(|class_name| { + is_same_or_descendant(module, class_name, declaring_class) + || is_same_or_descendant(module, declaring_class, class_name) + }) + .cloned() + .collect::>(); + scopes.sort_by(|left, right| { + class_id_for_scope(module, left) + .cmp(&class_id_for_scope(module, right)) + .then_with(|| left.cmp(right)) + }); + scopes +} + +/// Returns true when `class_name` is `ancestor` or descends from it. +fn is_same_or_descendant(module: &Module, class_name: &str, ancestor: &str) -> bool { + let mut cursor = Some(class_name); + while let Some(name) = cursor { + if name == ancestor { + return true; + } + cursor = module + .class_infos + .get(name) + .and_then(|class_info| class_info.parent.as_deref()); + } + false +} + +/// Returns the deterministic class id used to order generated scope checks. +fn class_id_for_scope(module: &Module, class_name: &str) -> u64 { + module + .class_infos + .get(class_name) + .map(|class_info| class_info.class_id) + .unwrap_or(u64::MAX) +} + +/// Returns a platform-safe body label for one class-constant slot. +fn slot_body_label(module: &Module, slot: &EvalClassConstantSlot, mode: &str) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!( + "__elephc_eval_class_constant_{}_{}_{}_{}{}", + mode, + label_fragment(&slot.reflected_class), + label_fragment(&slot.declaring_class), + label_fragment(&slot.constant), + suffix + ) +} + +/// Returns a platform-safe label for continuing after one slot miss. +fn slot_miss_label(module: &Module, slot: &EvalClassConstantSlot, mode: &str) -> String { + format!("{}_miss", slot_body_label(module, slot, mode)) +} + +/// Converts arbitrary PHP metadata names into assembly-label-safe fragments. +fn label_fragment(value: &str) -> String { + value + .chars() + .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) + .collect() +} + +/// Emits a C-global label using the target's symbol spelling. +fn label_c_global(module: &Module, emitter: &mut Emitter, symbol: &str) { + let label = module.target.extern_symbol(symbol); + emitter.label_global(&label); +} diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 799e9287fe..a65cb90416 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -11,6 +11,7 @@ mod block_emit; pub(crate) mod context; +mod eval_class_constant_helpers; mod eval_constructor_helpers; mod eval_method_helpers; mod eval_property_helpers; @@ -199,7 +200,12 @@ fn finalize_user_asm( ) -> String { eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); eval_static_property_helpers::emit_eval_static_property_helpers(module, &mut emitter, &mut data); - eval_constructor_helpers::emit_eval_constructor_helpers(module, &mut emitter); + eval_class_constant_helpers::emit_eval_class_constant_helpers( + module, + &mut emitter, + &mut data, + ); + eval_constructor_helpers::emit_eval_constructor_helpers(module, &mut emitter, &mut data); eval_method_helpers::emit_eval_method_helpers(module, &mut emitter, &mut data); eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); eval_reflection_owner_helpers::emit_eval_reflection_owner_helpers(module, &mut emitter); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e77269714f..5a707b904a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5346,6 +5346,127 @@ echo (new EvalAotScopeParentChild())->run(); assert_eq!(out, "parent"); } +/// Verifies eval fragments read generated/AOT class constants through inherited class scope. +#[test] +fn test_eval_fragment_reads_aot_class_constants() { + let out = compile_and_run( + r#"run(); +echo ":"; +echo eval('return EvalAotConstChild::PUBLIC_BASE . ":" . (EvalAotConstState::Ready === EvalAotConstState::Ready ? "case" : "bad");'); +"#, + ); + assert_eq!(out, "secret:base:10:4:case"); +} + +/// Verifies eval Reflection hides private AOT constants inherited from a parent class. +#[test] +fn test_eval_reflection_hides_private_inherited_aot_class_constants() { + let out = compile_and_run( + r#"getConstants(ReflectionClassConstant::IS_PRIVATE); +echo $ref->hasConstant("HIDDEN") ? "bad" : "ok"; +echo ":" . count($private); +echo ":" . $ref->getConstant("VISIBLE"); +return "";'); +"#, + ); + assert_eq!(out, "ok:0:visible"); +} + +/// Verifies eval rejects direct fetches of private AOT constants through child names. +#[test] +fn test_eval_rejects_private_inherited_aot_class_constant_fetch() { + let err = compile_and_run_expect_failure( + r#"hasConstant("OWN") ? "O" : "o"; +echo $ref->hasConstant("BASE") ? "B" : "b"; +echo $ref->hasConstant("IFACE_LIMIT") ? "I" : "i"; +echo $ref->hasConstant("own") ? "bad" : "z"; +$all = $ref->getConstants(); +$private = $ref->getConstants(ReflectionClassConstant::IS_PRIVATE); +$final = $ref->getReflectionConstants(filter: ReflectionClassConstant::IS_FINAL); +$own = $ref->getReflectionConstant("OWN"); +$direct = new ReflectionClassConstant("EvalAotReflectConstChild", "SECRET"); +echo ":" . $ref->getConstant("OWN") . ":" . $ref->getConstant("SECRET") . ":" . $all["BASE"] . ":" . $all["IFACE_LIMIT"]; +echo ":" . count($private) . ":" . $private["SECRET"]; +echo ":" . count($final) . ":" . $own->getName() . ":" . $own->getValue() . ":" . ($own->isFinal() ? "F" : "f"); +echo ":" . $direct->getDeclaringClass()->getName() . ":" . $direct->getValue(); +$enum = new ReflectionClass("EvalAotReflectConstEnum"); +$case = $enum->getReflectionConstant("Ready"); +echo ":" . ($case->getValue() === EvalAotReflectConstEnum::Ready ? "case" : "bad") . ":" . $enum->getConstant("LEVEL"); +return "";'); +"#, + ); + assert_eq!(out, "OBIz:10:s:4:8:1:s:1:OWN:10:F:EvalAotReflectConstChild:s:case:7"); +} + /// Verifies eval fragments can unpack numeric arrays into public AOT method calls. #[test] fn test_eval_fragment_can_call_this_public_method_with_spread_args() { From a4fee9a96b8f95fab3f8a7ae8eabb39d2c6b217e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 24 Jun 2026 23:53:40 +0200 Subject: [PATCH 0607/1208] Expose AOT constant attributes to eval reflection --- crates/elephc-magician/src/context.rs | 41 +++++++++++++++++++ .../elephc-magician/src/ffi/native_methods.rs | 4 ++ crates/elephc-magician/src/ffi/tests.rs | 21 ++++++++++ .../src/interpreter/reflection.rs | 22 +++++++++- docs/php/eval.md | 6 +-- src/codegen/lower_inst/builtins/eval.rs | 30 ++++++++++++++ tests/codegen/eval.rs | 22 ++++++++-- 7 files changed, 138 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index f00102028e..deb0c4db5a 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -363,6 +363,7 @@ pub struct ElephcEvalContext { native_constructors: HashMap, native_class_parents: HashMap, native_method_attributes: HashMap<(String, String), Vec>, + native_constant_attributes: HashMap<(String, String), Vec>, native_property_types: HashMap<(String, String), EvalParameterType>, native_property_defaults: HashMap<(String, String), NativeCallableDefault>, native_property_attributes: HashMap<(String, String), Vec>, @@ -418,6 +419,7 @@ impl ElephcEvalContext { native_constructors: HashMap::new(), native_class_parents: HashMap::new(), native_method_attributes: HashMap::new(), + native_constant_attributes: HashMap::new(), native_property_types: HashMap::new(), native_property_defaults: HashMap::new(), native_property_attributes: HashMap::new(), @@ -474,6 +476,7 @@ impl ElephcEvalContext { native_constructors: HashMap::new(), native_class_parents: HashMap::new(), native_method_attributes: HashMap::new(), + native_constant_attributes: HashMap::new(), native_property_types: HashMap::new(), native_property_defaults: HashMap::new(), native_property_attributes: HashMap::new(), @@ -2140,6 +2143,36 @@ impl ElephcEvalContext { .unwrap_or_default() } + /// Appends generated AOT class-constant attribute metadata for eval reflection. + pub fn define_native_constant_attribute( + &mut self, + class_name: &str, + constant_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_constant_key(class_name, constant_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_constant_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT class-constant attribute metadata by PHP class and constant name. + pub fn native_constant_attributes( + &self, + class_name: &str, + constant_name: &str, + ) -> Vec { + self.native_constant_attributes + .get(&native_constant_key(class_name, constant_name)) + .cloned() + .unwrap_or_default() + } + /// Defines generated AOT property type metadata for eval reflection. pub fn define_native_property_type( &mut self, @@ -2513,6 +2546,14 @@ fn native_property_key(class_name: &str, property_name: &str) -> (String, String ) } +/// Builds the case-sensitive native class-constant metadata key used for eval reflection. +fn native_constant_key(class_name: &str, constant_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + constant_name.to_string(), + ) +} + /// Pushes a PHP class-like name once, preserving the first visible spelling. fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSet) { let key = normalize_class_name(name); diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index 58996d1a0a..f7026f839c 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -24,6 +24,7 @@ const NATIVE_DEFAULT_FLOAT: u64 = 3; pub(crate) const NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; +const NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT: u8 = 2; const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; const NATIVE_ATTRIBUTE_ARGS_SUPPORTED: u8 = 1; const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; @@ -1646,6 +1647,9 @@ unsafe fn register_native_member_attribute_inner( member_name, record.attribute, )), + NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => i32::from( + context.define_native_constant_attribute(class_name, member_name, record.attribute), + ), _ => 0, } } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index ddb90646a9..6e9b457e5d 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -726,6 +726,12 @@ fn register_native_member_attribute_records_metadata() { ]), ); let property_record = native_member_attribute_record(1, "KnownClass::id", "Column", None); + let constant_record = native_member_attribute_record( + 2, + "KnownClass::LIMIT", + "Limit", + Some(&[EvalAttributeArg::Int(100)]), + ); let invalid_record = [99, 0, 0, 0, 0]; let method_registered = unsafe { @@ -742,6 +748,13 @@ fn register_native_member_attribute_records_metadata() { property_record.len() as u64, ) }; + let constant_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + constant_record.as_ptr(), + constant_record.len() as u64, + ) + }; let invalid_registered = unsafe { __elephc_eval_register_native_member_attribute( &mut ctx, @@ -771,6 +784,14 @@ fn register_native_member_attribute_records_metadata() { assert_eq!(property_attributes.len(), 1); assert_eq!(property_attributes[0].name(), "Column"); assert!(property_attributes[0].args().is_none()); + assert_eq!(constant_registered, 1); + let constant_attributes = ctx.native_constant_attributes("KnownClass", "LIMIT"); + assert_eq!(constant_attributes.len(), 1); + assert_eq!(constant_attributes[0].name(), "Limit"); + assert_eq!( + constant_attributes[0].args(), + Some([EvalAttributeArg::Int(100)].as_slice()) + ); assert_eq!(invalid_registered, 0); } diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index dc98b3fdaa..83889e1f8b 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -4607,6 +4607,12 @@ fn eval_reflection_class_constant_metadata( let declaring_class = values .reflection_constant_declaring_class(runtime_class_name, constant_name)? .unwrap_or_else(|| runtime_class_name.to_string()); + let attributes = eval_reflection_aot_constant_attributes( + runtime_class_name, + &declaring_class, + constant_name, + context, + ); let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { EvalVisibility::Private } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { @@ -4616,13 +4622,27 @@ fn eval_reflection_class_constant_metadata( }; Ok(Some(( declaring_class, - Vec::new(), + attributes, visibility, flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE != 0, ))) } +/// Returns registered generated/AOT class-constant attributes for one reflected constant. +fn eval_reflection_aot_constant_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_constant_attributes(declaring_class_name, constant_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_constant_attributes(runtime_class_name, constant_name) +} + /// Returns true when a name resolves to an eval-declared class-like symbol. fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> bool { context.has_class(name) diff --git a/docs/php/eval.md b/docs/php/eval.md index cef2b0f6d6..dfb6d7fb49 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -297,8 +297,8 @@ counts, and registered scalar, null, empty-array, or supported object-valued def constructor, instance-method, and static-method signatures. AOT property reflection exposes registered declared property types and supported scalar, string, or null default values for generated property metadata. AOT method and -property reflection expose generated member attributes when their arguments fit -the materializable literal subset. +property/class-constant reflection expose generated member attributes when +their arguments fit the materializable literal subset. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected @@ -672,7 +672,7 @@ object-valued generated defaults beyond the positional non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT -method/property attributes are exposed for registered metadata slices, while +method/property/class-constant attributes are exposed for registered metadata slices, while unsupported bridge shapes remain metadata-only rather than invocable through eval. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 4b95d63bb7..bad2bfe9b6 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -58,6 +58,7 @@ const NATIVE_DEFAULT_FLOAT: i64 = 3; const NATIVE_DEFAULT_EMPTY_ARRAY: i64 = 4; const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; +const NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT: u8 = 2; const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; const NATIVE_ATTRIBUTE_ARGS_SUPPORTED: u8 = 1; const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; @@ -611,6 +612,7 @@ fn eval_native_member_attribute_registrations( for (class_name, class_info) in classes { collect_eval_native_method_attributes(class_name, class_info, &mut registrations); collect_eval_native_property_attributes(class_name, class_info, &mut registrations); + collect_eval_native_class_constant_attributes(class_name, class_info, &mut registrations); } dedupe_eval_native_member_attribute_registrations(registrations) } @@ -710,6 +712,34 @@ fn collect_eval_native_property_attributes( } } +/// Adds class-constant attribute metadata for one class to eval registration. +fn collect_eval_native_class_constant_attributes( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + let mut constants = class_info + .constant_attribute_names + .iter() + .collect::>(); + constants.sort_by_key(|(constant_name, _)| constant_name.as_str()); + for (constant_name, attribute_names) in constants { + let attribute_args = class_info + .constant_attribute_args + .get(constant_name) + .cloned() + .unwrap_or_default(); + collect_eval_native_member_attributes( + NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT, + class_name, + constant_name, + attribute_names, + &attribute_args, + registrations, + ); + } +} + /// Adds aligned attribute name/argument metadata for one AOT member. fn collect_eval_native_member_attributes( owner_kind: u8, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5a707b904a..3bb5c9cf94 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8971,8 +8971,7 @@ echo $listed->getDefaultValue(); ); } -/// Verifies eval exposes generated/AOT method and property attributes through -/// `ReflectionMethod::getAttributes()` and `ReflectionProperty::getAttributes()`. +/// Verifies eval exposes generated/AOT member attributes through Reflection. #[test] fn test_eval_reflection_member_exposes_aot_attributes() { let out = compile_and_run_capture( @@ -8991,6 +8990,12 @@ class EvalAotReflectAttrTarget extends EvalAotReflectAttrBase { public function run() {} #[EvalAotMemberAttr("property", -3)] public int $id = 2; + #[EvalAotMemberAttr("constant", 11)] + public const LIMIT = 5; +} +enum EvalAotReflectAttrEnum { + #[EvalAotMemberAttr("case")] + case Ready; } echo eval('$methodAttrs = (new ReflectionMethod("EvalAotReflectAttrTarget", "run"))->getAttributes(); echo "M" . count($methodAttrs) . ":"; @@ -9009,7 +9014,16 @@ echo "BP" . count($basePropertyAttrs) . ":" . $basePropertyAttrs[0]->getArgument $listedMethod = (new ReflectionClass("EvalAotReflectAttrTarget"))->getMethod("run"); echo count($listedMethod->getAttributes()) . ":"; $listedProperty = (new ReflectionClass("EvalAotReflectAttrTarget"))->getProperty("id"); -echo count($listedProperty->getAttributes()); +echo count($listedProperty->getAttributes()) . ":"; +$constantAttrs = (new ReflectionClassConstant("EvalAotReflectAttrTarget", "LIMIT"))->getAttributes(); +echo "C" . count($constantAttrs) . ":"; +echo $constantAttrs[0]->getName() . ":"; +$constantArgs = $constantAttrs[0]->getArguments(); +echo $constantArgs[0] . ":" . $constantArgs[1] . ":"; +$listedConstant = (new ReflectionClass("EvalAotReflectAttrTarget"))->getReflectionConstant("LIMIT"); +echo count($listedConstant->getAttributes()) . ":"; +$caseAttrs = (new ReflectionClassConstant("EvalAotReflectAttrEnum", "Ready"))->getAttributes(); +echo "E" . count($caseAttrs) . ":" . $caseAttrs[0]->getArguments()[0]; '); "#, ); @@ -9020,7 +9034,7 @@ echo count($listedProperty->getAttributes()); ); assert_eq!( out.stdout, - "M1:EvalAotMemberAttr:method:P1:EvalAotMemberAttr:property:-3:BM1:base:7:T:N:BP1:baseProp:1:1" + "M1:EvalAotMemberAttr:method:P1:EvalAotMemberAttr:property:-3:BM1:base:7:T:N:BP1:baseProp:1:1:C1:EvalAotMemberAttr:constant:11:1:E1:case" ); } From 32233421765e3fcfabc665b069818d002f6e19bf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 00:05:06 +0200 Subject: [PATCH 0608/1208] Expose AOT property defaults in eval reflection class maps --- .../src/interpreter/reflection.rs | 36 +++++++++++++++++-- docs/php/eval.md | 3 +- tests/codegen/eval.rs | 12 ++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 83889e1f8b..fde5bebea2 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -599,10 +599,11 @@ pub(in crate::interpreter) fn eval_reflection_class_get_default_properties_resul else { return Ok(None); }; - let property_names = eval_reflection_default_property_names(&reflected_name, context); + let property_names = eval_reflection_default_property_names(&reflected_name, context, values)?; let mut result = values.assoc_new(property_names.len())?; for name in property_names { - let Some(member) = eval_reflection_property_metadata(&reflected_name, &name, context) + let Some(member) = + eval_reflection_default_property_metadata(&reflected_name, &name, context, values)? else { continue; }; @@ -5054,6 +5055,35 @@ fn eval_reflection_property_metadata( fn eval_reflection_default_property_names( reflected_name: &str, context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(reflected_name) + || context.has_enum(reflected_name) + || context.has_trait(reflected_name) + || context.has_interface(reflected_name) + { + return Ok(eval_reflection_eval_property_names(reflected_name, context)); + } + eval_reflection_aot_member_names(EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, values) +} + +/// Returns eval or generated/AOT property metadata for default-property materialization. +fn eval_reflection_default_property_metadata( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) { + return Ok(Some(member)); + } + eval_reflection_aot_property_metadata_if_exists(reflected_name, property_name, context, values) +} + +/// Returns eval-declared property names for reflection APIs that do not use AOT lists. +fn eval_reflection_eval_property_names( + reflected_name: &str, + context: &ElephcEvalContext, ) -> Vec { if context.has_trait(reflected_name) { return context.trait_property_names(reflected_name); @@ -5069,7 +5099,7 @@ fn eval_reflection_static_property_names( reflected_name: &str, context: &ElephcEvalContext, ) -> Vec { - eval_reflection_default_property_names(reflected_name, context) + eval_reflection_eval_property_names(reflected_name, context) .into_iter() .filter(|name| { eval_reflection_property_metadata(reflected_name, name, context) diff --git a/docs/php/eval.md b/docs/php/eval.md index dfb6d7fb49..261b086fac 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -296,7 +296,8 @@ names, declared parameter types, declared return types, required/optional counts, and registered scalar, null, empty-array, or supported object-valued default values for generated constructor, instance-method, and static-method signatures. AOT property reflection exposes registered declared property types and supported scalar, -string, or null default values for generated property metadata. AOT method and +string, or null default values for generated property metadata, including +`ReflectionClass::getDefaultProperties()`. AOT method and property/class-constant reflection expose generated member attributes when their arguments fit the materializable literal subset. `ReflectionMethod::getDeclaringClass()` and diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3bb5c9cf94..3361ae2b98 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8957,6 +8957,16 @@ foreach ((new ReflectionClass("EvalAotReflectPropertyDefaultTarget"))->getProper echo "listed:"; echo $listed->hasDefaultValue() ? "D:" : "d:"; echo $listed->getDefaultValue(); +$defaults = (new ReflectionClass("EvalAotReflectPropertyDefaultTarget"))->getDefaultProperties(); +echo "|defaults:"; +echo $defaults["label"] . ":"; +echo $defaults["count"] . ":"; +echo $defaults["base"] . ":"; +echo array_key_exists("implicit", $defaults) && $defaults["implicit"] === null ? "I:" : "i:"; +echo array_key_exists("nullable", $defaults) && $defaults["nullable"] === null ? "N:" : "n:"; +echo $defaults["flag"] ? "F:" : "f:"; +echo $defaults["neg"] . ":"; +echo array_key_exists("typed", $defaults) ? "T" : "t"; '); "#, ); @@ -8967,7 +8977,7 @@ echo $listed->getDefaultValue(); ); assert_eq!( out.stdout, - "count:D:7|label:D:ok|nullable:D:null|implicit:D:null|typed:d:null|base:D:3|flag:D:1|neg:D:-1.5|listed:D:7" + "count:D:7|label:D:ok|nullable:D:null|implicit:D:null|typed:d:null|base:D:3|flag:D:1|neg:D:-1.5|listed:D:7|defaults:ok:7:3:I:N:F:-1.5:t" ); } From ab9c959a72b3e647488e4f2a9eac259fb49cec74 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 00:13:22 +0200 Subject: [PATCH 0609/1208] Bridge AOT static property reflection in eval --- .../src/interpreter/reflection.rs | 103 +++++++++++++----- docs/php/eval.md | 4 +- tests/codegen/eval.rs | 38 +++++++ 3 files changed, 114 insertions(+), 31 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index fde5bebea2..b0cc9c33b3 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -637,7 +637,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_static_properties_result else { return Ok(None); }; - let property_names = eval_reflection_static_property_names(&reflected_name, context); + let property_names = eval_reflection_static_property_names(&reflected_name, context, values)?; let mut result = values.assoc_new(property_names.len())?; for name in property_names { let Some(value) = @@ -711,7 +711,8 @@ pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_re evaluated_args, )?; let property_name = eval_reflection_string_arg(args[0], values)?; - let Some(member) = eval_reflection_property_metadata(&reflected_name, &property_name, context) + let Some(member) = + eval_reflection_static_property_metadata(&reflected_name, &property_name, context, values)? else { return eval_reflection_static_property_missing_for_set( &reflected_name, @@ -728,12 +729,23 @@ pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_re values, ); } - let declaring_class = member - .declaring_class_name - .as_deref() - .ok_or(EvalStatus::RuntimeFatal)?; - if let Some(replaced) = context.set_static_property(declaring_class, &property_name, args[1]) { - values.release(replaced)?; + if eval_reflection_class_like_exists(&reflected_name, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(replaced) = + context.set_static_property(declaring_class, &property_name, args[1]) + { + values.release(replaced)?; + } + } else if !values.static_property_set(&reflected_name, &property_name, args[1])? { + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); } values.null().map(Some) } @@ -5094,46 +5106,77 @@ fn eval_reflection_eval_property_names( context.class_property_names(reflected_name) } -/// Returns eval property names that can contribute to `ReflectionClass::getStaticProperties()`. +/// Returns property names that can contribute to `ReflectionClass::getStaticProperties()`. fn eval_reflection_static_property_names( reflected_name: &str, context: &ElephcEvalContext, -) -> Vec { - eval_reflection_eval_property_names(reflected_name, context) - .into_iter() - .filter(|name| { - eval_reflection_property_metadata(reflected_name, name, context) - .is_some_and(|property| property.is_static) - }) - .collect() + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if eval_reflection_class_like_exists(reflected_name, context) { + return Ok(eval_reflection_eval_property_names(reflected_name, context) + .into_iter() + .filter(|name| { + eval_reflection_property_metadata(reflected_name, name, context) + .is_some_and(|property| property.is_static) + }) + .collect()); + } + let names = + eval_reflection_aot_member_names(EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, values)?; + let mut result = Vec::new(); + for name in names { + if eval_reflection_aot_property_metadata_if_exists(reflected_name, &name, context, values)? + .is_some_and(|property| property.is_static) + { + result.push(name); + } + } + Ok(result) } -/// Returns the current eval static property value or the trait metadata default. +/// Returns eval or generated/AOT property metadata for static-property reflection. +fn eval_reflection_static_property_metadata( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) { + return Ok(Some(member)); + } + eval_reflection_aot_property_metadata_if_exists(reflected_name, property_name, context, values) +} + +/// Returns the current eval or generated/AOT static property value. fn eval_reflection_static_property_value( reflected_name: &str, property_name: &str, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) + let Some(member) = + eval_reflection_static_property_metadata(reflected_name, property_name, context, values)? else { return Ok(None); }; if !member.is_static { return Ok(None); } - let declaring_class = member - .declaring_class_name - .as_deref() - .ok_or(EvalStatus::RuntimeFatal)?; - if let Some(value) = context.static_property(declaring_class, property_name) { - return Ok(Some(value)); + if eval_reflection_class_like_exists(reflected_name, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(value) = context.static_property(declaring_class, property_name) { + return Ok(Some(value)); + } + return member + .default_value + .as_ref() + .map(|default| eval_method_parameter_default(default, context, values)) + .transpose(); } - member - .default_value - .as_ref() - .map(|default| eval_method_parameter_default(default, context, values)) - .transpose() + values.static_property_get(reflected_name, property_name) } /// Binds `getStaticPropertyValue()` arguments while preserving whether a default was supplied. diff --git a/docs/php/eval.md b/docs/php/eval.md index 261b086fac..336e269388 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -276,7 +276,9 @@ class constants, interface constants, trait constants, enum constants, enum cases, supported materialized property defaults, and current eval-declared static property values. For generated/AOT class-like symbols, the constant APIs also expose materializable scalar, string, null, `::class`, simple arithmetic or -concatenation, and enum-case constant metadata through runtime hooks. Constant +concatenation, and enum-case constant metadata through runtime hooks, and the +static-property APIs expose bridge-supported public generated/AOT static +property values. Constant lookup is case-sensitive; single-value lookups return `false` when no constant or case is visible. `getConstants()` and `getReflectionConstants()` accept PHP's `ReflectionClassConstant::IS_*` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3361ae2b98..44f75c3f2b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10615,6 +10615,44 @@ try { ); } +/// Verifies eval ReflectionClass static-property APIs bridge generated/AOT values. +#[test] +fn test_eval_reflection_class_static_property_values_aot() { + let out = compile_and_run_capture( + r#"getStaticProperties(); +echo count($statics) . ":"; +echo $statics["label"] . ":"; +echo $statics["count"] . ":"; +echo $ref->getStaticPropertyValue("label") . ":"; +$ref->setStaticPropertyValue("label", "changed"); +echo EvalAotReflectStaticPropertyTarget::$label . ":"; +$ref->setStaticPropertyValue(name: "count", value: 9); +echo $ref->getStaticPropertyValue("count") . ":"; +echo EvalAotReflectStaticPropertyTarget::$count . ":"; +echo $ref->getStaticPropertyValue("instance", "fallback") . ":"; +try { + $ref->setStaticPropertyValue("instance", "bad"); + echo "bad"; +} catch (ReflectionException $e) { + echo "S"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:start:2:start:changed:9:9:fallback:S"); +} + /// Verifies eval ReflectionParameter exposes the declaring class for method parameters. #[test] fn test_eval_reflection_parameter_reports_declaring_class() { From 4eea38a2f018ce979221950b7c186a2c7fc44db9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 00:26:03 +0200 Subject: [PATCH 0610/1208] Bridge AOT ReflectionProperty values in eval --- .../src/interpreter/reflection.rs | 183 +++++++++++++++--- docs/php/eval.md | 6 +- tests/codegen/eval.rs | 43 ++++ 3 files changed, 198 insertions(+), 34 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index b0cc9c33b3..6b798fd630 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -739,7 +739,17 @@ pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_re { values.release(replaced)?; } - } else if !values.static_property_set(&reflected_name, &property_name, args[1])? { + } else { + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(reflected_name.as_str()); + let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.static_property_set(&reflected_name, &property_name, args[1]) + })?; + if updated { + return values.null().map(Some); + } return eval_reflection_static_property_missing_for_set( &reflected_name, &property_name, @@ -1220,7 +1230,8 @@ pub(in crate::interpreter) fn eval_reflection_property_get_value_result( ) .map(Some); } - let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? else { return Err(EvalStatus::RuntimeFatal); }; @@ -1235,13 +1246,23 @@ pub(in crate::interpreter) fn eval_reflection_property_get_value_result( .ok_or(EvalStatus::RuntimeFatal); } let object = object.ok_or(EvalStatus::RuntimeFatal)?; - eval_reflection_instance_property_get_value( - &declaring_class, - &property_name, - object, - context, - values, - ) + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } .map(Some) } @@ -1278,31 +1299,57 @@ pub(in crate::interpreter) fn eval_reflection_property_set_value_result( )?; return values.null().map(Some); } - let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? else { return Err(EvalStatus::RuntimeFatal); }; if member.is_static { let value = value.unwrap_or(object_or_value); - let declaring_class = member - .declaring_class_name - .as_deref() - .ok_or(EvalStatus::RuntimeFatal)?; - if let Some(replaced) = context.set_static_property(declaring_class, &property_name, value) - { - values.release(replaced)?; + if eval_reflection_class_like_exists(&declaring_class, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(replaced) = + context.set_static_property(declaring_class, &property_name, value) + { + values.release(replaced)?; + } + } else { + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(declaring_class.as_str()); + let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.static_property_set(declaring_class, &property_name, value) + })?; + if !updated { + return Err(EvalStatus::RuntimeFatal); + } } return values.null().map(Some); } let value = value.ok_or(EvalStatus::RuntimeFatal)?; - eval_reflection_instance_property_set_value( - &declaring_class, - &property_name, - object_or_value, - value, - context, - values, - )?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + } else { + eval_reflection_aot_instance_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + } values.null().map(Some) } @@ -3218,7 +3265,7 @@ fn eval_reflection_owner_object_with_members( declaring_class, reflected_name, ); - } else if eval_reflection_class_like_exists(declaring_class, context) { + } else { let identity = values.object_identity(object)?; context.register_eval_reflection_property( identity, @@ -5092,6 +5139,19 @@ fn eval_reflection_default_property_metadata( eval_reflection_aot_property_metadata_if_exists(reflected_name, property_name, context, values) } +/// Returns eval or generated/AOT metadata for a materialized `ReflectionProperty`. +fn eval_reflection_reflected_property_metadata( + declaring_class: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(member) = eval_reflection_property_metadata(declaring_class, property_name, context) { + return Ok(Some(member)); + } + eval_reflection_aot_property_metadata_if_exists(declaring_class, property_name, context, values) +} + /// Returns eval-declared property names for reflection APIs that do not use AOT lists. fn eval_reflection_eval_property_names( reflected_name: &str, @@ -5141,10 +5201,7 @@ fn eval_reflection_static_property_metadata( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - if let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) { - return Ok(Some(member)); - } - eval_reflection_aot_property_metadata_if_exists(reflected_name, property_name, context, values) + eval_reflection_reflected_property_metadata(reflected_name, property_name, context, values) } /// Returns the current eval or generated/AOT static property value. @@ -5176,7 +5233,13 @@ fn eval_reflection_static_property_value( .map(|default| eval_method_parameter_default(default, context, values)) .transpose(); } - values.static_property_get(reflected_name, property_name) + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(reflected_name); + eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.static_property_get(reflected_name, property_name) + }) } /// Binds `getStaticPropertyValue()` arguments while preserving whether a default was supplied. @@ -5834,6 +5897,64 @@ fn eval_reflection_instance_property_set_value( Ok(()) } +/// Reads one generated/AOT instance property through ReflectionProperty semantics. +fn eval_reflection_aot_instance_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.property_get(object, property_name) + }) +} + +/// Writes one generated/AOT instance property through ReflectionProperty semantics. +fn eval_reflection_aot_instance_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.property_set(object, property_name, value) + }) +} + +/// Verifies a generated/AOT ReflectionProperty instance target is compatible. +fn eval_reflection_aot_instance_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let is_instance = dynamic_object_is_a(object, declaring_class, false, context, values)? + .map_or_else(|| values.object_is_a(object, declaring_class, false), Ok)?; + if is_instance { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + /// Returns whether one eval instance property is initialized for ReflectionProperty. fn eval_reflection_instance_property_is_initialized( declaring_class: &str, diff --git a/docs/php/eval.md b/docs/php/eval.md index 336e269388..6d24d1ce5a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -450,9 +450,9 @@ for those APIs, including `PropertyHookType::cases()`, `from()`, and `tryFrom()`. `ReflectionProperty::setAccessible()` is accepted as a PHP-compatible no-op. `ReflectionProperty::getValue()` and `setValue()` read and write eval-declared -instance and static property values, bypass public/protected/private visibility -like PHP reflection, route concrete property hooks through their accessors, and -still reject readonly writes. +and bridge-supported generated/AOT instance and static property values, bypass +public/protected/private visibility like PHP reflection, route concrete eval +property hooks through their accessors, and still reject readonly writes. `ReflectionProperty::getRawValue()` and `setRawValue()` are supported for eval-declared backed instance properties, including backed property hooks, and bypass concrete property hook accessors. Virtual property hooks reject raw diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 44f75c3f2b..61769aae16 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10653,6 +10653,49 @@ try { assert_eq!(out.stdout, "2:start:2:start:changed:9:9:fallback:S"); } +/// Verifies eval ReflectionProperty value APIs bridge generated/AOT storage. +#[test] +fn test_eval_reflection_property_value_apis_bridge_aot_storage() { + let out = compile_and_run_capture( + r#"secret . "," . self::$hidden; + } +} +$object = new EvalAotReflectPropertyValueTarget(); +echo eval('$name = new ReflectionProperty("EvalAotReflectPropertyValueTarget", "name"); +$secret = new ReflectionProperty("EvalAotReflectPropertyValueTarget", "secret"); +$label = new ReflectionProperty("EvalAotReflectPropertyValueTarget", "label"); +$hidden = new ReflectionProperty("EvalAotReflectPropertyValueTarget", "hidden"); +echo $name->getValue($object) . ":"; +$name->setValue($object, "Grace"); +echo $object->name . ":"; +echo $secret->getValue($object) . ":"; +$secret->setValue($object, 9); +echo $object->reveal() . ":"; +echo $label->getValue() . ":"; +$label->setValue("changed"); +echo EvalAotReflectPropertyValueTarget::$label . ":"; +echo $hidden->getValue() . ":"; +$hidden->setValue(8); +echo $object->reveal(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada:Grace:3:9,4:start:changed:4:9,8"); +} + /// Verifies eval ReflectionParameter exposes the declaring class for method parameters. #[test] fn test_eval_reflection_parameter_reports_declaring_class() { From a69aeb322cc628ba225f2433354b3579db02fdc3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 08:47:09 +0200 Subject: [PATCH 0611/1208] Bridge AOT ReflectionProperty initialization checks --- .../src/interpreter/reflection.rs | 80 ++++++-- .../src/interpreter/runtime_ops.rs | 14 ++ .../interpreter/tests/support/object_ops.rs | 13 ++ .../interpreter/tests/support/runtime_ops.rs | 16 ++ .../src/runtime_hooks/externs.rs | 15 ++ .../elephc-magician/src/runtime_hooks/ops.rs | 39 ++++ docs/php/eval.md | 7 +- src/codegen/eval_property_helpers.rs | 171 +++++++++++++++++- src/codegen/eval_static_property_helpers.rs | 142 ++++++++++++++- tests/codegen/eval.rs | 50 +++++ 10 files changed, 520 insertions(+), 27 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 6b798fd630..db1a2e1e8d 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1353,7 +1353,7 @@ pub(in crate::interpreter) fn eval_reflection_property_set_value_result( values.null().map(Some) } -/// Handles eval-backed `ReflectionProperty::isInitialized()` calls. +/// Handles `ReflectionProperty::isInitialized()` calls for eval and generated/AOT properties. pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( identity: u64, method_name: &str, @@ -1386,7 +1386,8 @@ pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( .and_then(|initialized| values.bool_value(initialized)) .map(Some); } - let Some(member) = eval_reflection_property_metadata(&declaring_class, &property_name, context) + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? else { return Err(EvalStatus::RuntimeFatal); }; @@ -1395,22 +1396,38 @@ pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( .declaring_class_name .as_deref() .ok_or(EvalStatus::RuntimeFatal)?; - return values - .bool_value( - context - .static_property(declaring_class, &property_name) - .is_some(), - ) - .map(Some); + let initialized = if eval_reflection_class_like_exists(declaring_class, context) { + context + .static_property(declaring_class, &property_name) + .is_some() + } else { + eval_reflection_aot_static_property_is_initialized( + declaring_class, + &property_name, + context, + values, + )? + }; + return values.bool_value(initialized).map(Some); } let object = object.ok_or(EvalStatus::RuntimeFatal)?; - eval_reflection_instance_property_is_initialized( - &declaring_class, - &property_name, - object, - context, - values, - ) + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + } .and_then(|initialized| values.bool_value(initialized)) .map(Some) } @@ -5936,6 +5953,37 @@ fn eval_reflection_aot_instance_property_set_value( }) } +/// Checks one generated/AOT instance property initialization marker through ReflectionProperty. +fn eval_reflection_aot_instance_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.property_is_initialized(object, property_name) + }) +} + +/// Checks one generated/AOT static property initialization marker through ReflectionProperty. +fn eval_reflection_aot_static_property_is_initialized( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_with_declaring_class_scope(declaring_class, context, || { + values.static_property_is_initialized(declaring_class, property_name) + }) +} + /// Verifies a generated/AOT ReflectionProperty instance target is compatible. fn eval_reflection_aot_instance_property_validate_object( declaring_class: &str, diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 3a2d3b7078..c0bbe588fe 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -71,6 +71,13 @@ pub trait RuntimeValueOps { property: &str, ) -> Result; + /// Checks whether a generated/AOT instance property is initialized. + fn property_is_initialized( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result; + /// Writes a named property on a runtime object held in a boxed Mixed cell. fn property_set( &mut self, @@ -86,6 +93,13 @@ pub trait RuntimeValueOps { property: &str, ) -> Result, EvalStatus>; + /// Checks whether a generated/AOT static property is initialized. + fn static_property_is_initialized( + &mut self, + class_name: &str, + property: &str, + ) -> Result; + /// Writes a generated/AOT static property through the generated bridge. fn static_property_set( &mut self, diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 6f09c53b89..c922dde73c 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -50,6 +50,19 @@ impl FakeOps { _ => Err(EvalStatus::UnsupportedConstruct), } } + /// Returns whether one fake object property exists by name. + pub(super) fn runtime_property_is_initialized( + &self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + Ok(properties.iter().any(|(name, _)| name == property)) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } /// Writes one fake object property by name. pub(super) fn runtime_property_set( &mut self, diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index b6f8e903ef..ca5af2f749 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -74,6 +74,14 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_property_get(object, property) } + /// Checks whether one fake object property exists by name. + fn property_is_initialized( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + self.runtime_property_is_initialized(object, property) + } /// Writes one fake object property by name. fn property_set( &mut self, @@ -91,6 +99,14 @@ impl RuntimeValueOps for FakeOps { ) -> Result, EvalStatus> { Ok(None) } + /// Reports no fake AOT static property initialization match. + fn static_property_is_initialized( + &mut self, + _class_name: &str, + _property: &str, + ) -> Result { + Ok(false) + } /// Reports a failed fake AOT static property write. fn static_property_set( &mut self, diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 15b76964ff..b9c8f8ebd9 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -47,6 +47,13 @@ unsafe extern "C" { scope_ptr: *const u8, scope_len: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_property_is_initialized( + object: *mut RuntimeCell, + name_ptr: *const u8, + name_len: u64, + scope_ptr: *const u8, + scope_len: u64, + ) -> u64; pub(super) fn __elephc_eval_value_property_set( object: *mut RuntimeCell, name_ptr: *const u8, @@ -63,6 +70,14 @@ unsafe extern "C" { scope_ptr: *const u8, scope_len: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_static_property_is_initialized( + class_ptr: *const u8, + class_len: u64, + name_ptr: *const u8, + name_len: u64, + scope_ptr: *const u8, + scope_len: u64, + ) -> u64; pub(super) fn __elephc_eval_value_static_property_set( class_ptr: *const u8, class_len: u64, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 814c29da6a..3b9fd9087a 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -106,6 +106,25 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Checks an AOT instance property initialization marker through the generated helper. + fn property_is_initialized( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let initialized = unsafe { + __elephc_eval_value_property_is_initialized( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }; + Ok(initialized != 0) + } + /// Writes a boxed Mixed object property through the generated user helper. fn property_set( &mut self, @@ -155,6 +174,26 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } + /// Checks an AOT static property initialization marker through the generated helper. + fn static_property_is_initialized( + &mut self, + class_name: &str, + property: &str, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let initialized = unsafe { + __elephc_eval_value_static_property_is_initialized( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }; + Ok(initialized != 0) + } + /// Writes an AOT static property through the generated user helper. fn static_property_set( &mut self, diff --git a/docs/php/eval.md b/docs/php/eval.md index 6d24d1ce5a..73f0be738c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -426,9 +426,10 @@ property metadata. `isDynamic()` reports `false` for supported declared properties and `true` for public dynamic object properties materialized with `new ReflectionProperty($object, $property_name)`. `ReflectionProperty::isDefault()` is the inverse for those supported dynamic -properties. `isInitialized()` tracks eval-backed instance and static property -storage, including typed properties without defaults, unset properties, virtual -property hooks, and public dynamic properties on the inspected object. +properties. `isInitialized()` tracks eval-backed and bridge-supported +generated/AOT instance and static property storage, including typed properties +without defaults, unset eval properties, virtual property hooks, and public +dynamic properties on the inspected object. `ReflectionProperty::hasType()`, `getType()`, and `getSettableType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index a98fa688e0..aa76dd6fd0 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -18,6 +18,7 @@ use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::codegen::runtime_value_tag; +use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; use crate::ir::{Function, LocalKind, Module}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -33,6 +34,7 @@ struct EvalPropertySlot { visibility: Visibility, offset: usize, ty: PhpType, + is_declared: bool, } /// Emits eval property helpers when any lowered function owns an eval context. @@ -46,6 +48,7 @@ pub(super) fn emit_eval_property_helpers( } let slots = collect_eval_property_slots(module); emit_property_get_helper(module, emitter, data, &slots); + emit_property_is_initialized_helper(module, emitter, data, &slots); emit_property_set_helper(module, emitter, data, &slots); } @@ -120,6 +123,7 @@ fn collect_class_property_slots( visibility: visibility.clone(), offset: 8 + index * 16, ty: ty.codegen_repr(), + is_declared: class_info.property_slot_is_declared(index, property), }); } } @@ -173,6 +177,22 @@ fn emit_property_get_helper( } } +/// Emits `__elephc_eval_value_property_is_initialized(Mixed*, name, len, scope, scope_len) -> bool`. +fn emit_property_is_initialized_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user property initialization probe ---"); + label_c_global(module, emitter, "__elephc_eval_value_property_is_initialized"); + match module.target.arch { + Arch::AArch64 => emit_property_is_initialized_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_property_is_initialized_x86_64(module, emitter, data, slots), + } +} + /// Emits `__elephc_eval_value_property_set(Mixed*, name, len, Mixed*, scope, scope_len) -> bool`. fn emit_property_set_helper( module: &Module, @@ -271,6 +291,76 @@ fn emit_property_get_x86_64( emitter.instruction("ret"); // return the boxed property value to Rust } +/// Emits the ARM64 property-initialization helper body. +fn emit_property_is_initialized_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let fail_label = "__elephc_eval_value_property_is_initialized_fail"; + let done_label = "__elephc_eval_value_property_is_initialized_done"; + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for saved inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x3, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope length + emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot have an initialized declared property + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers cannot have initialized declared properties + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for marker loads + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emit_aarch64_property_dispatch(module, emitter, data, slots, "is_initialized", fail_label); + emitter.instruction(&format!("b {}", fail_label)); // no supported declared property matched the request + emit_aarch64_initialized_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("mov x0, #0"); // report an initialization miss to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the initialization flag to Rust +} + +/// Emits the x86_64 property-initialization helper body. +fn emit_property_is_initialized_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalPropertySlot], +) { + let fail_label = "__elephc_eval_value_property_is_initialized_fail_x"; + let done_label = "__elephc_eval_value_property_is_initialized_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, object, and scope + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot have an initialized declared property + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers cannot have initialized declared properties + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for marker loads + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emit_x86_64_property_dispatch(module, emitter, data, slots, "is_initialized", fail_label); + emitter.instruction(&format!("jmp {}", fail_label)); // no supported declared property matched the request + emit_x86_64_initialized_slot_bodies(module, emitter, slots, done_label); + emitter.label(fail_label); + emitter.instruction("xor eax, eax"); // report an initialization miss to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the initialization flag to Rust +} + /// Emits the ARM64 property-set helper body. fn emit_property_set_aarch64( module: &Module, @@ -582,18 +672,18 @@ fn emit_x86_64_property_scope_check( /// Returns ARM64 stack offsets for the class-scope pointer and length. fn aarch64_scope_offsets(mode: &str) -> (usize, usize) { match mode { - "get" => (32, 40), + "get" | "is_initialized" => (32, 40), "set" => (40, 48), - _ => unreachable!("eval property helpers only use get/set modes"), + _ => unreachable!("eval property helpers only use get/set/is_initialized modes"), } } /// Returns x86_64 frame offsets for the class-scope pointer and length. fn x86_64_scope_offsets(mode: &str) -> (usize, usize) { match mode { - "get" => (40, 48), + "get" | "is_initialized" => (40, 48), "set" => (48, 56), - _ => unreachable!("eval property helpers only use get/set modes"), + _ => unreachable!("eval property helpers only use get/set/is_initialized modes"), } } @@ -625,6 +715,34 @@ fn emit_x86_64_get_slot_bodies( } } +/// Emits ARM64 property-initialization bodies for every bridge-supported property slot. +fn emit_aarch64_initialized_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "is_initialized")); + emit_aarch64_property_initialized_flag(emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after materializing the initialization flag + } +} + +/// Emits x86_64 property-initialization bodies for every bridge-supported property slot. +fn emit_x86_64_initialized_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "is_initialized")); + emit_x86_64_property_initialized_flag(emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after materializing the initialization flag + } +} + /// Emits ARM64 property-set bodies for every bridge-supported property slot. fn emit_aarch64_set_slot_bodies( module: &Module, @@ -659,6 +777,33 @@ fn emit_x86_64_set_slot_bodies( } } +/// Emits an ARM64 boolean for one declared property's initialized state. +fn emit_aarch64_property_initialized_flag(emitter: &mut Emitter, slot: &EvalPropertySlot) { + if !slot.is_declared { + emitter.instruction("mov x0, #1"); // non-typed declared properties are always initialized + return; + } + emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer + emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker + abi::emit_load_int_immediate(emitter, "x12", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp x11, x12"); // compare the property marker against the uninitialized sentinel + emitter.instruction("cset x0, ne"); // materialize true when the instance property is initialized +} + +/// Emits an x86_64 boolean for one declared property's initialized state. +fn emit_x86_64_property_initialized_flag(emitter: &mut Emitter, slot: &EvalPropertySlot) { + if !slot.is_declared { + emitter.instruction("mov rax, 1"); // non-typed declared properties are always initialized + return; + } + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer + emitter.instruction(&format!("mov rax, QWORD PTR [r11 + {}]", slot.offset + 8)); // load the typed-property initialization marker + abi::emit_load_int_immediate(emitter, "r10", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp rax, r10"); // compare the property marker against the uninitialized sentinel + emitter.instruction("setne al"); // materialize true when the instance property is initialized + emitter.instruction("movzx rax, al"); // widen the initialization flag into the return register +} + /// Boxes a property value loaded from an ARM64 object slot into a Mixed cell. fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer @@ -770,6 +915,7 @@ fn emit_aarch64_store_property_slot( emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval value to a PHP float emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store emitter.instruction(&format!("str d0, [x9, #{}]", slot.offset)); // store the coerced float into the property slot + emit_aarch64_clear_scalar_property_marker(emitter, slot); } PhpType::Str => { emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for string coercion @@ -813,6 +959,7 @@ fn emit_x86_64_store_property_slot( emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval value to a PHP float emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store emitter.instruction(&format!("movsd QWORD PTR [r11 + {}], xmm0", slot.offset)); // store the coerced float into the property slot + emit_x86_64_clear_scalar_property_marker(emitter, slot); } PhpType::Str => { emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for string coercion @@ -851,6 +998,14 @@ fn emit_aarch64_store_cast_scalar( emitter.instruction(&format!("bl {}", helper)); // coerce the eval value to the declared property type emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store emitter.instruction(&format!("str {}, [x9, #{}]", result_reg, slot.offset)); // store the coerced scalar into the property slot + emit_aarch64_clear_scalar_property_marker(emitter, slot); +} + +/// Clears an ARM64 one-word scalar typed-property marker after a successful store. +fn emit_aarch64_clear_scalar_property_marker(emitter: &mut Emitter, slot: &EvalPropertySlot) { + if slot.is_declared { + emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the typed-property initialization marker + } } /// Stores a boxed eval value into an ARM64 nullable-int tagged-scalar property slot. @@ -890,6 +1045,14 @@ fn emit_x86_64_store_cast_scalar( emitter.instruction(&format!("call {}", helper)); // coerce the eval value to the declared property type emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store emitter.instruction(&format!("mov QWORD PTR [r11 + {}], {}", slot.offset, result_reg)); // store the coerced scalar into the property slot + emit_x86_64_clear_scalar_property_marker(emitter, slot); +} + +/// Clears an x86_64 one-word scalar typed-property marker after a successful store. +fn emit_x86_64_clear_scalar_property_marker(emitter: &mut Emitter, slot: &EvalPropertySlot) { + if slot.is_declared { + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); // clear the typed-property initialization marker + } } /// Stores a boxed ARM64 eval heap value into an array-like property slot. diff --git a/src/codegen/eval_static_property_helpers.rs b/src/codegen/eval_static_property_helpers.rs index 349ea31d84..8073517d82 100644 --- a/src/codegen/eval_static_property_helpers.rs +++ b/src/codegen/eval_static_property_helpers.rs @@ -49,6 +49,7 @@ pub(super) fn emit_eval_static_property_helpers( } let slots = collect_eval_static_property_slots(module); emit_static_property_get_helper(module, emitter, data, &slots); + emit_static_property_is_initialized_helper(module, emitter, data, &slots); emit_static_property_set_helper(module, emitter, data, &slots); } @@ -177,6 +178,26 @@ fn emit_static_property_get_helper( } } +/// Emits `__elephc_eval_value_static_property_is_initialized(class, name, scope, scope_len) -> bool`. +fn emit_static_property_is_initialized_helper( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + emitter.blank(); + emitter.comment("--- eval bridge: user static-property initialization probe ---"); + label_c_global( + module, + emitter, + "__elephc_eval_value_static_property_is_initialized", + ); + match module.target.arch { + Arch::AArch64 => emit_static_property_is_initialized_aarch64(module, emitter, data, slots), + Arch::X86_64 => emit_static_property_is_initialized_x86_64(module, emitter, data, slots), + } +} + /// Emits `__elephc_eval_value_static_property_set(class, name, Mixed*, scope, scope_len) -> bool`. fn emit_static_property_set_helper( module: &Module, @@ -247,6 +268,60 @@ fn emit_static_property_get_x86_64( emitter.instruction("ret"); // return the boxed static property value to Rust } +/// Emits the ARM64 static-property-initialization helper body. +fn emit_static_property_is_initialized_aarch64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let done_label = "__elephc_eval_value_static_property_is_initialized_done"; + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class/property/scope slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested property-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested property-name length + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length + emit_aarch64_static_property_dispatch(module, emitter, data, slots, "is_initialized"); + emitter.instruction("mov x0, #0"); // report an initialization miss to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emit_aarch64_static_initialized_slot_bodies(module, emitter, slots, done_label); + emitter.label(done_label); + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the initialization flag to Rust +} + +/// Emits the x86_64 static-property-initialization helper body. +fn emit_static_property_is_initialized_x86_64( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + slots: &[EvalStaticPropertySlot], +) { + let done_label = "__elephc_eval_value_static_property_is_initialized_done_x"; + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, property, and scope slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope length + emit_x86_64_static_property_dispatch(module, emitter, data, slots, "is_initialized"); + emitter.instruction("xor eax, eax"); // report an initialization miss to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emit_x86_64_static_initialized_slot_bodies(module, emitter, slots, done_label); + emitter.label(done_label); + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the initialization flag to Rust +} + /// Emits the ARM64 static-property set helper body. fn emit_static_property_set_aarch64( module: &Module, @@ -491,18 +566,18 @@ fn emit_x86_64_static_property_scope_check( /// Returns ARM64 stack offsets for the class-scope pointer and length. fn aarch64_scope_offsets(mode: &str) -> (usize, usize) { match mode { - "get" => (32, 40), + "get" | "is_initialized" => (32, 40), "set" => (40, 48), - _ => unreachable!("eval static property helpers only use get/set modes"), + _ => unreachable!("eval static property helpers only use get/set/is_initialized modes"), } } /// Returns x86_64 frame offsets for the class-scope pointer and length. fn x86_64_scope_offsets(mode: &str) -> (usize, usize) { match mode { - "get" => (40, 48), + "get" | "is_initialized" => (40, 48), "set" => (48, 56), - _ => unreachable!("eval static property helpers only use get/set modes"), + _ => unreachable!("eval static property helpers only use get/set/is_initialized modes"), } } @@ -536,6 +611,34 @@ fn emit_x86_64_get_slot_bodies( } } +/// Emits ARM64 static-property-initialization bodies for every supported slot. +fn emit_aarch64_static_initialized_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "is_initialized")); + emit_aarch64_static_property_initialized_flag(emitter, slot); + emitter.instruction(&format!("b {}", done_label)); // return after materializing the static initialization flag + } +} + +/// Emits x86_64 static-property-initialization bodies for every supported slot. +fn emit_x86_64_static_initialized_slot_bodies( + module: &Module, + emitter: &mut Emitter, + slots: &[EvalStaticPropertySlot], + done_label: &str, +) { + for slot in slots { + emitter.label(&slot_body_label(module, slot, "is_initialized")); + emit_x86_64_static_property_initialized_flag(emitter, slot); + emitter.instruction(&format!("jmp {}", done_label)); // return after materializing the static initialization flag + } +} + /// Emits ARM64 set bodies for every bridge-supported static property slot. fn emit_aarch64_set_slot_bodies( module: &Module, @@ -570,6 +673,37 @@ fn emit_x86_64_set_slot_bodies( } } +/// Emits an ARM64 boolean for one static property's initialized state. +fn emit_aarch64_static_property_initialized_flag( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + if !slot.is_declared { + emitter.instruction("mov x0, #1"); // non-typed declared static properties are always initialized + return; + } + abi::emit_load_symbol_to_reg(emitter, "x10", &slot.symbol, 8); + abi::emit_load_int_immediate(emitter, "x11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp x10, x11"); // compare the static property marker against the uninitialized sentinel + emitter.instruction("cset x0, ne"); // materialize true when the static property is initialized +} + +/// Emits an x86_64 boolean for one static property's initialized state. +fn emit_x86_64_static_property_initialized_flag( + emitter: &mut Emitter, + slot: &EvalStaticPropertySlot, +) { + if !slot.is_declared { + emitter.instruction("mov rax, 1"); // non-typed declared static properties are always initialized + return; + } + abi::emit_load_symbol_to_reg(emitter, "r10", &slot.symbol, 8); + abi::emit_load_int_immediate(emitter, "r11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp r10, r11"); // compare the static property marker against the uninitialized sentinel + emitter.instruction("setne al"); // materialize true when the static property is initialized + emitter.instruction("movzx rax, al"); // widen the initialization flag into the return register +} + /// Emits an ARM64 uninitialized typed-static-property guard before boxing. fn emit_aarch64_uninitialized_guard( emitter: &mut Emitter, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 61769aae16..4c8de0bf63 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10482,6 +10482,56 @@ echo $virtual->isInitialized($object) ? "V" : "v";'); assert_eq!(out.stdout, "t:P:s:N:S:T:t:T:y:Y:V"); } +/// Verifies eval ReflectionProperty initialization checks bridge generated/AOT storage. +#[test] +fn test_eval_reflection_property_is_initialized_bridge_aot_storage() { + let out = compile_and_run_capture( + r#"secret . "," . self::$hidden; + } +} +$object = new EvalAotReflectInitializedTarget(); +echo eval('$name = new ReflectionProperty("EvalAotReflectInitializedTarget", "name"); +$typed = new ReflectionProperty("EvalAotReflectInitializedTarget", "typed"); +$secret = new ReflectionProperty("EvalAotReflectInitializedTarget", "secret"); +$label = new ReflectionProperty("EvalAotReflectInitializedTarget", "label"); +$staticTyped = new ReflectionProperty("EvalAotReflectInitializedTarget", "staticTyped"); +$hidden = new ReflectionProperty("EvalAotReflectInitializedTarget", "hidden"); +echo $name->isInitialized($object) ? "N:" : "n:"; +echo $typed->isInitialized($object) ? "T:" : "t:"; +echo $secret->isInitialized($object) ? "P:" : "p:"; +echo $label->isInitialized() ? "L:" : "l:"; +echo $staticTyped->isInitialized() ? "S:" : "s:"; +echo $hidden->isInitialized() ? "H:" : "h:"; +$typed->setValue($object, 5); +$secret->setValue($object, 7); +$staticTyped->setValue(11); +$hidden->setValue(13); +echo $typed->isInitialized($object) ? "T:" : "t:"; +echo $secret->isInitialized($object) ? "P:" : "p:"; +echo $staticTyped->isInitialized() ? "S:" : "s:"; +echo $hidden->isInitialized() ? "H:" : "h:"; +echo $object->reveal(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "N:t:p:L:s:h:T:P:S:H:7,13"); +} + /// Verifies eval ReflectionProperty exposes property hook metadata and hook methods. #[test] fn test_eval_reflection_property_hook_metadata() { From 392e71feaa6874704f6f830a6968ce8673f08649 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 08:54:59 +0200 Subject: [PATCH 0612/1208] Guard AOT eval property reads for uninitialized typed slots --- src/codegen/eval_property_helpers.rs | 48 ++++++++++++++++++++++++++++ tests/codegen/eval.rs | 30 +++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index aa76dd6fd0..a36ed6adf9 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -696,6 +696,7 @@ fn emit_aarch64_get_slot_bodies( ) { for slot in slots { emitter.label(&slot_body_label(module, slot, "get")); + emit_aarch64_uninitialized_property_get_guard(emitter, slot, done_label); emit_aarch64_box_property_slot(emitter, slot); emitter.instruction(&format!("b {}", done_label)); // return after boxing the declared property value } @@ -710,6 +711,7 @@ fn emit_x86_64_get_slot_bodies( ) { for slot in slots { emitter.label(&slot_body_label(module, slot, "get")); + emit_x86_64_uninitialized_property_get_guard(emitter, slot, done_label); emit_x86_64_box_property_slot(emitter, slot); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the declared property value } @@ -804,6 +806,52 @@ fn emit_x86_64_property_initialized_flag(emitter: &mut Emitter, slot: &EvalPrope emitter.instruction("movzx rax, al"); // widen the initialization flag into the return register } +/// Emits an ARM64 typed-property guard before boxing an eval bridge property read. +fn emit_aarch64_uninitialized_property_get_guard( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + done_label: &str, +) { + if !slot.is_declared { + return; + } + let initialized_label = format!( + "{}_initialized", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer for marker inspection + emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker + abi::emit_load_int_immediate(emitter, "x12", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp x11, x12"); // compare the property marker against the uninitialized sentinel + emitter.instruction(&format!("b.ne {}", initialized_label)); // continue boxing once the instance property is initialized + emitter.instruction("mov x0, xzr"); // report uninitialized property reads as bridge failures + emitter.instruction(&format!("b {}", done_label)); // return the failure to Rust without boxing storage + emitter.label(&initialized_label); +} + +/// Emits an x86_64 typed-property guard before boxing an eval bridge property read. +fn emit_x86_64_uninitialized_property_get_guard( + emitter: &mut Emitter, + slot: &EvalPropertySlot, + done_label: &str, +) { + if !slot.is_declared { + return; + } + let initialized_label = format!( + "{}_initialized_x", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for marker inspection + emitter.instruction(&format!("mov rax, QWORD PTR [r10 + {}]", slot.offset + 8)); // load the typed-property initialization marker + abi::emit_load_int_immediate(emitter, "r11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); + emitter.instruction("cmp rax, r11"); // compare the property marker against the uninitialized sentinel + emitter.instruction(&format!("jne {}", initialized_label)); // continue boxing once the instance property is initialized + emitter.instruction("xor eax, eax"); // report uninitialized property reads as bridge failures + emitter.instruction(&format!("jmp {}", done_label)); // return the failure to Rust without boxing storage + emitter.label(&initialized_label); +} + /// Boxes a property value loaded from an ARM64 object slot into a Mixed cell. fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4c8de0bf63..e9f828e72d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10532,6 +10532,36 @@ echo $object->reveal(); assert_eq!(out.stdout, "N:t:p:L:s:h:T:P:S:H:7,13"); } +/// Verifies eval ReflectionProperty getValue rejects uninitialized generated/AOT typed storage. +#[test] +fn test_eval_reflection_property_get_value_rejects_uninitialized_aot_storage() { + let out = compile_and_run_capture( + r#"getValue($object) . ":"; +echo $typed->getValue($object); +'); +"#, + ); + assert!( + !out.success, + "program unexpectedly succeeded: stdout={:?}", + out.stdout + ); + assert_eq!(out.stdout, "Ada:"); + assert!( + out.stderr.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {}", + out.stderr + ); +} + /// Verifies eval ReflectionProperty exposes property hook metadata and hook methods. #[test] fn test_eval_reflection_property_hook_metadata() { From c6980e0493ad0e21eac90630a70c5e88c64b49ad Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 09:00:41 +0200 Subject: [PATCH 0613/1208] Bridge AOT ReflectionProperty raw value APIs --- .../src/interpreter/reflection.rs | 53 +++++++++++++------ docs/php/eval.md | 3 +- tests/codegen/eval.rs | 36 +++++++++++++ 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index db1a2e1e8d..13157aa6c5 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1532,7 +1532,7 @@ pub(in crate::interpreter) fn eval_reflection_property_to_string_result( values.string(&text).map(Some) } -/// Handles eval-backed `ReflectionProperty::getRawValue()` and raw write calls. +/// Handles `ReflectionProperty::getRawValue()` and raw write calls. pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( identity: u64, method_name: &str, @@ -1561,13 +1561,23 @@ pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( ) .map(Some); } - return eval_reflection_instance_property_get_raw_value( - &declaring_class, - &property_name, - object, - context, - values, - ) + return if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_get_raw_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } .map(Some); } if method_name.eq_ignore_ascii_case("setRawValue") @@ -1585,14 +1595,25 @@ pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( )?; return values.null().map(Some); } - eval_reflection_instance_property_set_raw_value( - &declaring_class, - &property_name, - object, - value, - context, - values, - )?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_set_raw_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + } else { + eval_reflection_aot_instance_property_set_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + } return values.null().map(Some); } Ok(None) diff --git a/docs/php/eval.md b/docs/php/eval.md index 73f0be738c..c566b74bf2 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -456,7 +456,8 @@ public/protected/private visibility like PHP reflection, route concrete eval property hooks through their accessors, and still reject readonly writes. `ReflectionProperty::getRawValue()` and `setRawValue()` are supported for eval-declared backed instance properties, including backed property hooks, and -bypass concrete property hook accessors. Virtual property hooks reject raw +for bridge-supported generated/AOT instance properties. Raw access bypasses +concrete eval property hook accessors. Virtual property hooks reject raw access like PHP. `ReflectionProperty::isLazy()` reports `false` for eval-declared properties because eval does not implement lazy properties; `skipLazyInitialization()` is a no-op for supported non-static backed diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e9f828e72d..307a9c627b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10562,6 +10562,42 @@ echo $typed->getValue($object); ); } +/// Verifies eval ReflectionProperty raw APIs bridge generated/AOT instance storage. +#[test] +fn test_eval_reflection_property_raw_value_apis_bridge_aot_storage() { + let out = compile_and_run_capture( + r#"secret . ":" . $this->typed; + } +} +$object = new EvalAotReflectRawPropertyTarget(); +echo eval('$name = new ReflectionProperty("EvalAotReflectRawPropertyTarget", "name"); +$secret = new ReflectionProperty("EvalAotReflectRawPropertyTarget", "secret"); +$typed = new ReflectionProperty("EvalAotReflectRawPropertyTarget", "typed"); +echo $name->getRawValue($object) . ":"; +$name->setRawValue($object, "Grace"); +echo $object->name . ":"; +echo $secret->getRawValue($object) . ":"; +$secret->setRawValue($object, 9); +$typed->setRawValueWithoutLazyInitialization(object: $object, value: 11); +echo $object->reveal(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada:Grace:3:9:11"); +} + /// Verifies eval ReflectionProperty exposes property hook metadata and hook methods. #[test] fn test_eval_reflection_property_hook_metadata() { From 24000d078bbba72f4b3d77cd802bced76bf55653 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 09:09:56 +0200 Subject: [PATCH 0614/1208] Bridge AOT ReflectionProperty lazy APIs --- .../src/interpreter/reflection.rs | 40 ++++++++++++++----- docs/php/eval.md | 3 +- tests/codegen/eval.rs | 4 +- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 13157aa6c5..e7529161ee 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1432,7 +1432,7 @@ pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( .map(Some) } -/// Handles eval-backed `ReflectionProperty::isLazy()` and `skipLazyInitialization()` calls. +/// Handles `ReflectionProperty::isLazy()` and `skipLazyInitialization()` calls. pub(in crate::interpreter) fn eval_reflection_property_lazy_result( identity: u64, method_name: &str, @@ -1460,7 +1460,16 @@ pub(in crate::interpreter) fn eval_reflection_property_lazy_result( )?; return values.bool_value(false).map(Some); } - eval_reflection_property_validate_object(&declaring_class, object, context, values)?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_property_validate_object(&declaring_class, object, context, values)?; + } else { + eval_reflection_aot_instance_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + } return values.bool_value(false).map(Some); } if method_name.eq_ignore_ascii_case("skipLazyInitialization") { @@ -1474,15 +1483,24 @@ pub(in crate::interpreter) fn eval_reflection_property_lazy_result( )?; return Err(EvalStatus::RuntimeFatal); } - let (_, property) = eval_reflection_instance_property_target( - &declaring_class, - &property_name, - object, - context, - values, - )?; - if property.is_virtual() { - return Err(EvalStatus::RuntimeFatal); + if eval_reflection_class_like_exists(&declaring_class, context) { + let (_, property) = eval_reflection_instance_property_target( + &declaring_class, + &property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + } else { + eval_reflection_aot_instance_property_validate_object( + &declaring_class, + object, + context, + values, + )?; } return values.null().map(Some); } diff --git a/docs/php/eval.md b/docs/php/eval.md index c566b74bf2..2a24e21fe8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -459,7 +459,8 @@ eval-declared backed instance properties, including backed property hooks, and for bridge-supported generated/AOT instance properties. Raw access bypasses concrete eval property hook accessors. Virtual property hooks reject raw access like PHP. `ReflectionProperty::isLazy()` reports `false` for -eval-declared properties because eval does not implement lazy properties; +eval-declared and bridge-supported generated/AOT properties because eval does +not implement lazy properties; `skipLazyInitialization()` is a no-op for supported non-static backed properties, and `setRawValueWithoutLazyInitialization()` follows the same raw storage write path as `setRawValue()`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 307a9c627b..2d78d81e73 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10585,6 +10585,8 @@ $name->setRawValue($object, "Grace"); echo $object->name . ":"; echo $secret->getRawValue($object) . ":"; $secret->setRawValue($object, 9); +echo $typed->isLazy($object) ? "L:" : "l:"; +$typed->skipLazyInitialization(object: $object); $typed->setRawValueWithoutLazyInitialization(object: $object, value: 11); echo $object->reveal(); '); @@ -10595,7 +10597,7 @@ echo $object->reveal(); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "Ada:Grace:3:9:11"); + assert_eq!(out.stdout, "Ada:Grace:3:l:9:11"); } /// Verifies eval ReflectionProperty exposes property hook metadata and hook methods. From 80bad52ce3d840c84f08e266f96ff8fe411a223a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 09:19:32 +0200 Subject: [PATCH 0615/1208] Guard AOT newInstanceWithoutConstructor --- .../src/interpreter/reflection.rs | 15 ++++++ .../src/interpreter/statements.rs | 5 ++ docs/php/eval.md | 7 +-- tests/codegen/eval.rs | 53 +++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index e7529161ee..9c5bf31d0c 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2135,6 +2135,21 @@ fn eval_reflection_aot_class_flags( Ok(Some((flags, modifiers))) } +/// Returns whether a generated/AOT reflected class can be allocated without its constructor. +pub(in crate::interpreter) fn eval_reflection_aot_class_allows_without_constructor_allocation( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + let rejected_flags = EVAL_REFLECTION_CLASS_FLAG_ABSTRACT + | EVAL_REFLECTION_CLASS_FLAG_INTERFACE + | EVAL_REFLECTION_CLASS_FLAG_TRAIT + | EVAL_REFLECTION_CLASS_FLAG_ENUM; + Ok(Some(flags & rejected_flags == 0)) +} + /// Returns whether an absent or public AOT lifecycle method allows public reflection. fn eval_reflection_aot_lifecycle_method_allows_public_reflection( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e1a653b072..51f529e8f1 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3910,6 +3910,11 @@ fn eval_reflection_class_new_instance_without_constructor_result( let class_name = context .resolve_class_name(&reflected_name) .unwrap_or(reflected_name); + if eval_reflection_aot_class_allows_without_constructor_allocation(&class_name, values)? + == Some(false) + { + return Err(EvalStatus::RuntimeFatal); + } values.new_object(&class_name).map(Some) } diff --git a/docs/php/eval.md b/docs/php/eval.md index 2a24e21fe8..2ca0d345dc 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -321,9 +321,10 @@ constructor arguments through eval's positional, named, and unpacking-aware call binding. Non-public constructors fail like PHP reflection construction. `ReflectionClass::newInstanceArgs()` constructs those reflected classes from an argument array, treating string keys as named constructor arguments. -`ReflectionClass::newInstanceWithoutConstructor()` allocates eval-declared -reflected classes, initializes supported property defaults, and skips -`__construct()`. +`ReflectionClass::newInstanceWithoutConstructor()` allocates eval-declared and +generated/AOT reflected classes, initializes supported property defaults, skips +`__construct()`, and rejects reflected abstract classes, interfaces, traits, +and enums. `ReflectionMethod::invoke()` and `invokeArgs()` call eval-declared reflected methods, bypass public/protected/private visibility like PHP reflection, preserve named arguments for the invoked method, follow PHP's by-value diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2d78d81e73..56eb93f91a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11674,6 +11674,59 @@ echo $with->label();'); assert_eq!(out, "default:hidden:ctor"); } +/// Verifies eval ReflectionClass::newInstanceWithoutConstructor allocates generated/AOT classes. +#[test] +fn test_eval_reflection_class_new_instance_without_constructor_allocates_aot_class() { + let out = compile_and_run( + r#"value = 9; + } +} + +echo eval('$ref = new ReflectionClass("EvalReflectNoCtorAotTarget"); +$object = $ref->newInstanceWithoutConstructor(); +echo $object->value . ":"; +echo $ref->isInstantiable() ? "I" : "i";'); +"#, + ); + assert_eq!(out, "4:i"); +} + +/// Verifies eval ReflectionClass::newInstanceWithoutConstructor rejects non-allocatable AOT class-likes. +#[test] +fn test_eval_reflection_class_new_instance_without_constructor_rejects_aot_non_classes() { + let cases = [ + ( + "abstract class EvalReflectNoCtorAotAbstract {}", + "EvalReflectNoCtorAotAbstract", + ), + ("interface EvalReflectNoCtorAotIface {}", "EvalReflectNoCtorAotIface"), + ("trait EvalReflectNoCtorAotTrait {}", "EvalReflectNoCtorAotTrait"), + ( + "enum EvalReflectNoCtorAotEnum { case Ready; }", + "EvalReflectNoCtorAotEnum", + ), + ]; + for (declaration, class_name) in cases { + let source = format!( + r#"newInstanceWithoutConstructor();'); +"# + ); + let err = compile_and_run_expect_failure(&source); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr for {class_name}: {err}" + ); + } +} + /// Verifies eval ReflectionClassConstant/EnumCase expose eval-declared attributes. #[test] fn test_eval_reflection_constant_and_enum_case_attributes() { From ee0d20d2cc40db2f58f31348822ba89535003f91 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 09:30:35 +0200 Subject: [PATCH 0616/1208] Instantiate AOT attributes from eval reflection --- .../src/interpreter/statements.rs | 34 +++++++++++--- docs/php/eval.md | 7 +-- tests/codegen/eval.rs | 44 +++++++++++++++++++ 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 51f529e8f1..0c163390c5 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3918,18 +3918,40 @@ fn eval_reflection_class_new_instance_without_constructor_result( values.new_object(&class_name).map(Some) } -/// Instantiates an eval-declared attribute class for `ReflectionAttribute::newInstance()`. +/// Instantiates an attribute class for `ReflectionAttribute::newInstance()`. fn eval_reflection_attribute_new_instance_result( attribute: &EvalAttribute, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let Some(class) = context.class(attribute.name()).cloned() else { - return values.null(); - }; let args = eval_reflection_attribute_arg_values(attribute, values)?; - let mut scope = ElephcEvalScope::new(); - eval_dynamic_class_new_object(&class, positional_args(args), context, &mut scope, values) + if let Some(class) = context.class(attribute.name()).cloned() { + let mut scope = ElephcEvalScope::new(); + return eval_dynamic_class_new_object( + &class, + positional_args(args), + context, + &mut scope, + values, + ); + } + let class_name = context + .resolve_class_name(attribute.name()) + .unwrap_or_else(|| attribute.name().trim_start_matches('\\').to_string()); + if !values.class_exists(&class_name)? { + return values.null(); + } + let args = bind_native_callable_args( + context.native_constructor_signature(&class_name), + positional_args(args), + values, + )?; + let object = values.new_object(&class_name)?; + if let Err(err) = values.construct_object(object, args) { + let _ = values.release(object); + return Err(err); + } + Ok(object) } /// Materializes eval attribute literal arguments as constructor argument cells. diff --git a/docs/php/eval.md b/docs/php/eval.md index 2ca0d345dc..ede2b5daa9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -179,9 +179,10 @@ interfaces, traits, and enums are visible through `class_attribute_names()`, `class_attribute_args()`, and `class_get_attributes()` when their arguments are supported literal positional values (`string`, `int`, `bool`, `null`, or negated integer literals). `ReflectionAttribute::newInstance()` instantiates -eval-declared attribute classes from those materialized attributes, and -`ReflectionAttribute::getTarget()` / `isRepeated()` report the reflected owner -target and same-owner repetition metadata. +eval-declared or bridge-supported generated/AOT attribute classes from those +materialized attributes, and `ReflectionAttribute::getTarget()` / +`isRepeated()` report the reflected owner target and same-owner repetition +metadata. Attribute names remain visible when an attribute uses unsupported argument syntax, but requesting those arguments is a runtime fatal. Private parent properties shadowed by same-named child properties use separate diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 56eb93f91a..72b8bbf6e6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9048,6 +9048,50 @@ echo "E" . count($caseAttrs) . ":" . $caseAttrs[0]->getArguments()[0]; ); } +/// Verifies eval ReflectionAttribute::newInstance constructs generated/AOT attribute classes. +#[test] +fn test_eval_reflection_attribute_new_instance_constructs_aot_attribute() { + let out = compile_and_run_capture( + r#"name = $name; + $this->value = $value; + } + + public function label(): string { + return $this->name . ":" . $this->value; + } +} +class EvalAotAttributeInstanceTarget { + #[EvalAotAttributeInstance("method", 2)] + public function run() {} + + #[EvalAotAttributeInstance("property", 3)] + public int $id = 0; + + #[EvalAotAttributeInstance("constant", 4)] + public const LIMIT = 5; +} +echo eval('$methodAttr = (new ReflectionMethod("EvalAotAttributeInstanceTarget", "run"))->getAttributes()[0]; +echo $methodAttr->newInstance()->label() . ":"; +$propertyAttr = (new ReflectionProperty("EvalAotAttributeInstanceTarget", "id"))->getAttributes()[0]; +echo $propertyAttr->newInstance()->label() . ":"; +$constantAttr = (new ReflectionClassConstant("EvalAotAttributeInstanceTarget", "LIMIT"))->getAttributes()[0]; +echo $constantAttr->newInstance()->label();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "method:2:property:3:constant:4"); +} + /// Verifies eval can probe generated/AOT method predicate metadata through /// `ReflectionClass::hasMethod()`, `getMethod()`, and direct `ReflectionMethod`. #[test] From 1c60a82b30ab338b14cd1f5bcbd9aef477a9efd3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 09:44:27 +0200 Subject: [PATCH 0617/1208] Expose AOT class attributes to eval --- crates/elephc-magician/src/context.rs | 28 +++++++++++ .../elephc-magician/src/ffi/native_methods.rs | 47 ++++++++++++------- crates/elephc-magician/src/ffi/tests.rs | 21 +++++++++ .../interpreter/builtins/class_metadata.rs | 26 +++++----- .../src/interpreter/reflection.rs | 9 ++-- docs/php/eval.md | 20 ++++---- src/codegen/lower_inst/builtins/eval.rs | 28 +++++++++-- tests/codegen/eval.rs | 45 ++++++++++++++++++ 8 files changed, 181 insertions(+), 43 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index deb0c4db5a..895643ffdc 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -362,6 +362,7 @@ pub struct ElephcEvalContext { native_static_methods: HashMap<(String, String), NativeCallableSignature>, native_constructors: HashMap, native_class_parents: HashMap, + native_class_attributes: HashMap>, native_method_attributes: HashMap<(String, String), Vec>, native_constant_attributes: HashMap<(String, String), Vec>, native_property_types: HashMap<(String, String), EvalParameterType>, @@ -418,6 +419,7 @@ impl ElephcEvalContext { native_static_methods: HashMap::new(), native_constructors: HashMap::new(), native_class_parents: HashMap::new(), + native_class_attributes: HashMap::new(), native_method_attributes: HashMap::new(), native_constant_attributes: HashMap::new(), native_property_types: HashMap::new(), @@ -475,6 +477,7 @@ impl ElephcEvalContext { native_static_methods: HashMap::new(), native_constructors: HashMap::new(), native_class_parents: HashMap::new(), + native_class_attributes: HashMap::new(), native_method_attributes: HashMap::new(), native_constant_attributes: HashMap::new(), native_property_types: HashMap::new(), @@ -2113,6 +2116,31 @@ impl ElephcEvalContext { .map(String::as_str) } + /// Appends generated AOT class attribute metadata for eval reflection. + pub fn define_native_class_attribute( + &mut self, + class_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = normalize_class_name(class_name); + if key.is_empty() { + return false; + } + self.native_class_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT class attribute metadata by PHP class name. + pub fn native_class_attributes(&self, class_name: &str) -> Vec { + self.native_class_attributes + .get(&normalize_class_name(class_name)) + .cloned() + .unwrap_or_default() + } + /// Appends generated AOT method attribute metadata for eval reflection. pub fn define_native_method_attribute( &mut self, diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index f7026f839c..db867388d2 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -25,6 +25,7 @@ pub(crate) const NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; const NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT: u8 = 2; +const NATIVE_MEMBER_ATTRIBUTE_CLASS: u8 = 3; const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; const NATIVE_ATTRIBUTE_ARGS_SUPPORTED: u8 = 1; const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; @@ -817,7 +818,7 @@ pub unsafe extern "C" fn __elephc_eval_register_native_property_default_string( .unwrap_or(0) } -/// Registers one generated native PHP method/property attribute in an eval context. +/// Registers one generated native PHP class/member attribute in an eval context. /// /// # Safety /// `ctx` must be a valid eval context handle. `record_ptr` must point to one @@ -1633,23 +1634,35 @@ unsafe fn register_native_member_attribute_inner( let Some(record) = native_member_attribute_record_from_abi(record_ptr, record_len) else { return 0; }; - let Some((class_name, member_name)) = split_method_key(&record.member_key) else { - return 0; - }; match record.owner_kind { - NATIVE_MEMBER_ATTRIBUTE_METHOD => i32::from(context.define_native_method_attribute( - class_name, - member_name, - record.attribute, - )), - NATIVE_MEMBER_ATTRIBUTE_PROPERTY => i32::from(context.define_native_property_attribute( - class_name, - member_name, - record.attribute, - )), - NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => i32::from( - context.define_native_constant_attribute(class_name, member_name, record.attribute), - ), + NATIVE_MEMBER_ATTRIBUTE_CLASS => { + i32::from(context.define_native_class_attribute(&record.member_key, record.attribute)) + } + NATIVE_MEMBER_ATTRIBUTE_METHOD + | NATIVE_MEMBER_ATTRIBUTE_PROPERTY + | NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => { + let Some((class_name, member_name)) = split_method_key(&record.member_key) else { + return 0; + }; + match record.owner_kind { + NATIVE_MEMBER_ATTRIBUTE_METHOD => i32::from(context.define_native_method_attribute( + class_name, + member_name, + record.attribute, + )), + NATIVE_MEMBER_ATTRIBUTE_PROPERTY => i32::from( + context.define_native_property_attribute(class_name, member_name, record.attribute), + ), + NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => { + i32::from(context.define_native_constant_attribute( + class_name, + member_name, + record.attribute, + )) + } + _ => 0, + } + } _ => 0, } } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 6e9b457e5d..499cd95fb7 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -732,6 +732,12 @@ fn register_native_member_attribute_records_metadata() { "Limit", Some(&[EvalAttributeArg::Int(100)]), ); + let class_record = native_member_attribute_record( + 3, + "KnownClass", + "Entity", + Some(&[EvalAttributeArg::String("model".to_string())]), + ); let invalid_record = [99, 0, 0, 0, 0]; let method_registered = unsafe { @@ -755,6 +761,13 @@ fn register_native_member_attribute_records_metadata() { constant_record.len() as u64, ) }; + let class_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + class_record.as_ptr(), + class_record.len() as u64, + ) + }; let invalid_registered = unsafe { __elephc_eval_register_native_member_attribute( &mut ctx, @@ -792,6 +805,14 @@ fn register_native_member_attribute_records_metadata() { constant_attributes[0].args(), Some([EvalAttributeArg::Int(100)].as_slice()) ); + assert_eq!(class_registered, 1); + let class_attributes = ctx.native_class_attributes("knownclass"); + assert_eq!(class_attributes.len(), 1); + assert_eq!(class_attributes[0].name(), "Entity"); + assert_eq!( + class_attributes[0].args(), + Some([EvalAttributeArg::String("model".to_string())].as_slice()) + ); assert_eq!(invalid_registered, 0); } diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index c3b2b7b9c8..cac5f10d28 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -121,17 +121,12 @@ pub(in crate::interpreter) fn eval_class_attribute_metadata_result( match (name, evaluated_args) { ("class_attribute_names", [class_name]) => { let class_name = eval_class_metadata_name(*class_name, values)?; - let Some(attributes) = eval_class_like_attributes(context, &class_name) else { - return values.array_new(0); - }; - eval_class_attribute_names_result(attributes, values) + let attributes = eval_class_like_attribute_metadata(context, &class_name); + eval_class_attribute_names_result(&attributes, values) } ("class_get_attributes", [class_name]) => { let class_name = eval_class_metadata_name(*class_name, values)?; - let Some(attributes) = eval_class_like_attributes(context, &class_name) else { - return values.array_new(0); - }; - let attributes = attributes.to_vec(); + let attributes = eval_class_like_attribute_metadata(context, &class_name); eval_reflection_attribute_array_result( &attributes, EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS, @@ -142,9 +137,7 @@ pub(in crate::interpreter) fn eval_class_attribute_metadata_result( ("class_attribute_args", [class_name, attribute_name]) => { let class_name = eval_class_metadata_name(*class_name, values)?; let attribute_name = eval_class_metadata_name(*attribute_name, values)?; - let Some(attributes) = eval_class_like_attributes(context, &class_name) else { - return values.array_new(0); - }; + let attributes = eval_class_like_attribute_metadata(context, &class_name); let Some(attribute) = attributes .iter() .find(|attribute| eval_attribute_name_matches(attribute.name(), &attribute_name)) @@ -177,6 +170,17 @@ fn eval_class_like_attributes<'a>( context.enum_decl(name).map(EvalEnum::attributes) } +/// Returns class-like attributes for eval declarations or generated AOT metadata. +fn eval_class_like_attribute_metadata( + context: &ElephcEvalContext, + name: &str, +) -> Vec { + if let Some(attributes) = eval_class_like_attributes(context, name) { + return attributes.to_vec(); + } + context.native_class_attributes(name) +} + /// Builds the indexed string array returned by `class_attribute_names()`. fn eval_class_attribute_names_result( attributes: &[EvalAttribute], diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 9c5bf31d0c..1d9230382f 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2014,10 +2014,11 @@ fn eval_reflection_class_new( )?; let interface_names = eval_reflection_aot_class_interface_names(&reflected_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(&reflected_name, values)?; + let attributes = context.native_class_attributes(&reflected_name); return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, &reflected_name, - &[], + &attributes, &interface_names, &[], &method_names, @@ -3415,10 +3416,11 @@ fn eval_reflection_full_class_object_result( let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; + let attributes = context.native_class_attributes(runtime_class_name); return eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, runtime_class_name, - &[], + &attributes, &interface_names, &[], &method_names, @@ -3469,10 +3471,11 @@ fn eval_reflection_shallow_class_object_result( return values.bool_value(false); }; let interface_names = eval_reflection_aot_class_interface_names(class_name, values)?; + let attributes = context.native_class_attributes(class_name); return eval_reflection_owner_object_with_members( EVAL_REFLECTION_OWNER_CLASS, class_name.trim_start_matches('\\'), - &[], + &attributes, &interface_names, &[], &[], diff --git a/docs/php/eval.md b/docs/php/eval.md index ede2b5daa9..85998248f0 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -175,10 +175,11 @@ when dynamic classes or traits are declared. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, -interfaces, traits, and enums are visible through `class_attribute_names()`, -`class_attribute_args()`, and `class_get_attributes()` when their arguments are -supported literal positional values (`string`, `int`, `bool`, `null`, or negated -integer literals). `ReflectionAttribute::newInstance()` instantiates +interfaces, traits, and enums, plus bridge-registered generated/AOT class-level +attributes, are visible through `class_attribute_names()`, +`class_attribute_args()`, and `class_get_attributes()` when their arguments fit +the supported literal positional subset (`string`, `int`, `bool`, `null`, or +negated integer literals). `ReflectionAttribute::newInstance()` instantiates eval-declared or bridge-supported generated/AOT attribute classes from those materialized attributes, and `ReflectionAttribute::getTarget()` / `isRepeated()` report the reflected owner target and same-owner repetition @@ -189,10 +190,13 @@ Private parent properties shadowed by same-named child properties use separate runtime storage, so parent methods keep seeing the private parent value while child methods and public access see the child property. `ReflectionClass::getAttributes()`, `ReflectionMethod::getAttributes()`, -`ReflectionProperty::getAttributes()`, and `ReflectionParameter::getAttributes()` -expose eval-retained class, method, property, and method-parameter attributes -for eval-declared class-like symbols when their arguments fit the same literal -subset, and `getName()` returns the reflected class, member, or parameter name +`ReflectionProperty::getAttributes()`, `ReflectionClassConstant::getAttributes()`, +and `ReflectionParameter::getAttributes()` expose eval-retained class, method, +property, class-constant, and method-parameter attributes for eval-declared +class-like symbols and bridge-registered generated/AOT class-level, method, +property, and class-constant attributes when their arguments fit the same +literal subset, and `getName()` returns the reflected class, member, or +parameter name for those owners. `ReflectionClass`, `ReflectionFunction`, `ReflectionMethod`, `ReflectionProperty`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` expose `getDocComment()` and report `false` because diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index bad2bfe9b6..a4b49ccbd6 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -59,6 +59,7 @@ const NATIVE_DEFAULT_EMPTY_ARRAY: i64 = 4; const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; const NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT: u8 = 2; +const NATIVE_MEMBER_ATTRIBUTE_CLASS: u8 = 3; const NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED: u8 = 0; const NATIVE_ATTRIBUTE_ARGS_SUPPORTED: u8 = 1; const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; @@ -610,6 +611,7 @@ fn eval_native_member_attribute_registrations( let mut classes = ctx.module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { + collect_eval_native_class_attributes(class_name, class_info, &mut registrations); collect_eval_native_method_attributes(class_name, class_info, &mut registrations); collect_eval_native_property_attributes(class_name, class_info, &mut registrations); collect_eval_native_class_constant_attributes(class_name, class_info, &mut registrations); @@ -659,6 +661,22 @@ fn register_eval_native_class_parents(ctx: &mut FunctionContext<'_>, context_off } } +/// Adds class-level attribute metadata for one class-like symbol to eval registration. +fn collect_eval_native_class_attributes( + class_name: &str, + class_info: &ClassInfo, + registrations: &mut Vec, +) { + collect_eval_native_member_attributes( + NATIVE_MEMBER_ATTRIBUTE_CLASS, + class_name, + "", + &class_info.attribute_names, + &class_info.attribute_args, + registrations, + ); +} + /// Adds method attribute metadata for one class to eval registration. fn collect_eval_native_method_attributes( class_name: &str, @@ -2108,10 +2126,12 @@ fn eval_native_member_attribute_record( ) -> Vec { let mut record = Vec::new(); record.push(registration.owner_kind); - eval_native_member_attribute_push_string( - &mut record, - &format!("{}::{}", registration.class_name, registration.member_name), - ); + let member_key = if registration.owner_kind == NATIVE_MEMBER_ATTRIBUTE_CLASS { + registration.class_name.clone() + } else { + format!("{}::{}", registration.class_name, registration.member_name) + }; + eval_native_member_attribute_push_string(&mut record, &member_key); eval_native_member_attribute_push_string(&mut record, ®istration.attribute_name); match ®istration.attribute_args { Some(args) => { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 72b8bbf6e6..d3164e9a8a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9048,6 +9048,51 @@ echo "E" . count($caseAttrs) . ":" . $caseAttrs[0]->getArguments()[0]; ); } +/// Verifies eval class-attribute helpers expose generated/AOT class attributes. +#[test] +fn test_eval_reflection_class_exposes_aot_attributes() { + let out = compile_and_run_capture( + r#"name = $name; + $this->value = $value; + } + + public function label(): string { + return $this->name . ":" . $this->value; + } +} +#[EvalAotClassAttr("class", 1), EvalAotClassAttr("again", 2)] +class EvalAotClassAttrTarget {} +echo eval('$names = class_attribute_names("EvalAotClassAttrTarget"); +echo count($names) . ":" . $names[0] . ":" . $names[1] . ":"; +$args = class_attribute_args("evalaotclassattrtarget", "EvalAotClassAttr"); +echo count($args) . ":" . $args[0] . ":" . $args[1] . ":"; +$attrs = class_get_attributes("EvalAotClassAttrTarget"); +echo count($attrs) . ":" . $attrs[0]->getName() . ":"; +echo ($attrs[0]->isRepeated() ? "R" : "r") . ":"; +echo $attrs[0]->getArguments()[0] . ":" . $attrs[0]->getArguments()[1] . ":"; +echo $attrs[0]->getTarget() . ":" . $attrs[0]->newInstance()->label() . ":"; +$refAttrs = (new ReflectionClass("EvalAotClassAttrTarget"))->getAttributes(); +echo count($refAttrs) . ":" . $refAttrs[1]->getArguments()[0] . ":"; +echo $refAttrs[1]->newInstance()->label();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "2:EvalAotClassAttr:EvalAotClassAttr:2:class:1:2:EvalAotClassAttr:R:class:1:1:class:1:2:again:again:2" + ); +} + /// Verifies eval ReflectionAttribute::newInstance constructs generated/AOT attribute classes. #[test] fn test_eval_reflection_attribute_new_instance_constructs_aot_attribute() { From ba94ed3fe1ab493a674042dc10706260737f671e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 09:58:59 +0200 Subject: [PATCH 0618/1208] Validate eval magic return contracts --- .../src/interpreter/statements.rs | 52 ++++++++++++++++++- .../src/interpreter/tests/classes.rs | 16 ++++++ docs/php/eval.md | 7 +-- tests/codegen/eval.rs | 39 ++++++++++---- 4 files changed, 100 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 0c163390c5..79770a909e 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1191,18 +1191,24 @@ fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalSt Ok(()) } -/// Validates staticness, visibility, and arity for one eval magic method. +/// Validates staticness, visibility, arity, and declared return type for one eval magic method. fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus> { match method.name().to_ascii_lowercase().as_str() { "__tostring" => { validate_magic_non_static(method)?; validate_magic_public(method)?; validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::String)?; } "__get" | "__isset" | "__unset" => { validate_magic_non_static(method)?; validate_magic_public(method)?; validate_magic_arity(method, 1)?; + if method.name().eq_ignore_ascii_case("__isset") { + validate_magic_declared_return_type(method, MagicReturnType::Bool)?; + } else if method.name().eq_ignore_ascii_case("__unset") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } } "__set" | "__call" => { validate_magic_non_static(method)?; @@ -1221,6 +1227,9 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus "__clone" | "__destruct" => { validate_magic_non_static(method)?; validate_magic_arity(method, 0)?; + if method.name().eq_ignore_ascii_case("__clone") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } } "__construct" => { if method.is_static() { @@ -1232,6 +1241,14 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus Ok(()) } +/// Magic method return types that eval can validate from retained declarations. +#[derive(Clone, Copy)] +enum MagicReturnType { + Bool, + String, + Void, +} + /// Rejects static declarations for magic methods that must be instance methods. fn validate_magic_non_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { if method.is_static() { @@ -1272,6 +1289,39 @@ fn validate_magic_arity(method: &EvalClassMethod, expected: usize) -> Result<(), } } +/// Rejects incompatible explicit return types on PHP magic methods. +fn validate_magic_declared_return_type( + method: &EvalClassMethod, + expected: MagicReturnType, +) -> Result<(), EvalStatus> { + method.return_type().map_or(Ok(()), |return_type| { + if magic_return_type_matches(return_type, expected) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } + }) +} + +/// Returns whether one retained eval return type is exactly the expected magic return atom. +fn magic_return_type_matches( + return_type: &EvalParameterType, + expected: MagicReturnType, +) -> bool { + if return_type.allows_null() || return_type.is_intersection() { + return false; + } + let [variant] = return_type.variants() else { + return false; + }; + matches!( + (expected, variant), + (MagicReturnType::Bool, EvalParameterTypeVariant::Bool) + | (MagicReturnType::String, EvalParameterTypeVariant::String) + | (MagicReturnType::Void, EvalParameterTypeVariant::Void) + ) +} + /// Validates property declarations that can be checked before class registration. fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 7e038b00e9..9b8c9adca4 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -980,10 +980,22 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadToString { private function __toString() { return "x"; } }"#.as_slice(), "private __toString", ), + ( + br#"class EvalBadToStringReturn { public function __toString(): int { return 1; } }"#.as_slice(), + "bad __toString return type", + ), ( br#"class EvalBadGet { protected function __get($name) { return "x"; } }"#.as_slice(), "protected __get", ), + ( + br#"class EvalBadIssetReturn { public function __isset($name): string { return "yes"; } }"#.as_slice(), + "bad __isset return type", + ), + ( + br#"class EvalBadUnsetReturn { public function __unset($name): int { return 1; } }"#.as_slice(), + "bad __unset return type", + ), ( br#"class EvalBadCall { public function __call($name, ...$args) { return "x"; } }"#.as_slice(), "variadic __call", @@ -1000,6 +1012,10 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadClone { public static function __clone() {} }"#.as_slice(), "static __clone", ), + ( + br#"class EvalBadCloneReturn { public function __clone(): int {} }"#.as_slice(), + "bad __clone return type", + ), ( br#"class EvalBadDestruct { public static function __destruct() {} }"#.as_slice(), "static __destruct", diff --git a/docs/php/eval.md b/docs/php/eval.md index 85998248f0..df74a2bfb5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -168,7 +168,7 @@ Eval-declared objects in string contexts dispatch through public parameterless `__toString()` for `echo`, `print`, concatenation, `strval()`, callable `strval()` dispatch, and weak `string` parameter coercion. Classes with a compatible `__toString()` satisfy `Stringable` implicitly. -Eval validates magic method staticness, visibility, and arity contracts for +Eval validates magic method staticness, visibility, arity, and relevant declared return-type contracts for `__toString()`, `__get()`, `__set()`, `__isset()`, `__unset()`, `__call()`, `__callStatic()`, `__invoke()`, `__clone()`, `__destruct()`, and `__construct()` when dynamic classes or traits are declared. @@ -673,7 +673,8 @@ bridge slice. By-reference parameters and broader parameter/return ABI shapes are still outside those bridge paths. Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are broader reflection APIs beyond the supported +remaining class-system gaps are eval-declared `__destruct()` execution on +dynamic-object release, broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader @@ -684,7 +685,7 @@ object-valued generated defaults beyond the positional non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT -method/property/class-constant attributes are exposed for registered metadata slices, while +class/method/property/class-constant attributes are exposed for registered metadata slices, while unsupported bridge shapes remain metadata-only rather than invocable through eval. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d3164e9a8a..fcee139910 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7371,19 +7371,38 @@ echo EvalMagicStaticBox::Hidden("C", name: "D");'); /// Verifies eval rejects invalid magic method contracts during dynamic class declaration. #[test] fn test_eval_rejects_invalid_magic_method_contracts() { - let err = compile_and_run_expect_failure( - r#" Date: Thu, 25 Jun 2026 10:21:26 +0200 Subject: [PATCH 0619/1208] Run eval dynamic destructors on final release --- crates/elephc-magician/src/context.rs | 25 ++++++ .../src/interpreter/runtime_ops.rs | 6 ++ .../src/interpreter/statements.rs | 61 ++++++++++++-- .../src/interpreter/tests/classes.rs | 25 ++++++ .../interpreter/tests/support/runtime_ops.rs | 11 +++ .../src/runtime_hooks/externs.rs | 1 + .../elephc-magician/src/runtime_hooks/ops.rs | 9 +++ docs/php/eval.md | 9 ++- src/codegen_support/runtime/eval_bridge.rs | 81 +++++++++++++++++-- tests/codegen/eval.rs | 19 +++++ 10 files changed, 234 insertions(+), 13 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 895643ffdc..0f483bbd62 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -373,6 +373,8 @@ pub struct ElephcEvalContext { class_constants: HashMap<(String, String), RuntimeCellHandle>, included_files: HashSet, dynamic_objects: HashMap, + dynamic_destructing_objects: HashSet, + dynamic_destructed_objects: HashSet, dynamic_property_aliases: HashMap<(u64, String), EvalReferenceTarget>, dynamic_initialized_properties: HashSet<(u64, String)>, eval_reflection_attributes: HashMap, @@ -430,6 +432,8 @@ impl ElephcEvalContext { class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), + dynamic_destructing_objects: HashSet::new(), + dynamic_destructed_objects: HashSet::new(), dynamic_property_aliases: HashMap::new(), dynamic_initialized_properties: HashSet::new(), eval_reflection_attributes: HashMap::new(), @@ -488,6 +492,8 @@ impl ElephcEvalContext { class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), + dynamic_destructing_objects: HashSet::new(), + dynamic_destructed_objects: HashSet::new(), dynamic_property_aliases: HashMap::new(), dynamic_initialized_properties: HashSet::new(), eval_reflection_attributes: HashMap::new(), @@ -893,6 +899,8 @@ impl ElephcEvalContext { .unwrap_or_else(|| class_name.to_string()); self.dynamic_objects .insert(identity, normalize_class_name(&class_name)); + self.dynamic_destructing_objects.remove(&identity); + self.dynamic_destructed_objects.remove(&identity); self.dynamic_initialized_properties .retain(|(object, _)| *object != identity); } @@ -904,6 +912,23 @@ impl ElephcEvalContext { .and_then(|class_key| self.classes.get(class_key)) } + /// Marks one dynamic object's destructor as active if it has not already run. + pub fn begin_dynamic_object_destructor(&mut self, identity: u64) -> bool { + if self.dynamic_destructed_objects.contains(&identity) { + return false; + } + if !self.dynamic_destructing_objects.insert(identity) { + return false; + } + self.dynamic_destructed_objects.insert(identity); + true + } + + /// Clears the active destructor guard for one dynamic object identity. + pub fn finish_dynamic_object_destructor(&mut self, identity: u64) { + self.dynamic_destructing_objects.remove(&identity); + } + /// Returns whether one dynamic object identity was registered with a class-like name. pub fn dynamic_object_is_class(&self, identity: u64, class_name: &str) -> bool { self.dynamic_objects diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index c0bbe588fe..08c259aba4 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -310,6 +310,12 @@ pub trait RuntimeValueOps { /// Returns the unboxed object payload pointer used for PHP object identity. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result; + /// Returns the object identity that would be freed by releasing this owned cell, if any. + fn final_object_identity_for_release( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus>; + /// Releases one owned runtime cell that is no longer held by the eval scope. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus>; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 79770a909e..13111e91b9 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -192,7 +192,7 @@ pub(in crate::interpreter) fn execute_stmt( value, ScopeCellOwnership::Owned, )? { - values.release(replaced)?; + eval_release_value(context, values, replaced)?; } Ok(EvalControl::None) } @@ -222,7 +222,7 @@ pub(in crate::interpreter) fn execute_stmt( } EvalStmt::UnsetVar { name } => { if let Some(replaced) = unset_scope_cell(scope, name.clone()) { - values.release(replaced)?; + eval_release_value(context, values, replaced)?; } Ok(EvalControl::None) } @@ -242,12 +242,61 @@ pub(in crate::interpreter) fn execute_stmt( Ok(EvalControl::None) } EvalStmt::Expr(expr) => { - let _ = eval_expr(expr, context, scope, values)?; + let result = eval_expr(expr, context, scope, values)?; + eval_release_value(context, values, result)?; Ok(EvalControl::None) } } } +/// Releases one eval-owned value after running an eval-declared dynamic destructor if needed. +fn eval_release_value( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + if let Some(identity) = values.final_object_identity_for_release(value)? { + eval_dynamic_destructor_for_release(identity, value, context, values)?; + } + values.release(value) +} + +/// Calls a dynamic eval `__destruct()` hook immediately before the runtime frees the object. +fn eval_dynamic_destructor_for_release( + identity: u64, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(class_name) = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + else { + return Ok(()); + }; + let Some((declaring_class, method)) = context.class_method(&class_name, "__destruct") else { + return Ok(()); + }; + if !context.begin_dynamic_object_destructor(identity) { + return Ok(()); + } + let result = eval_dynamic_method_with_values( + &declaring_class, + &class_name, + &method, + object, + Vec::new(), + context, + values, + ); + let release_result = match result { + Ok(result) => values.release(result), + Err(status) => Err(status), + }; + context.finish_dynamic_object_destructor(identity); + release_result +} + /// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. fn eval_array_unset_element_stmt( array: &EvalExpr, @@ -3213,7 +3262,7 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( context.class_method(class.name(), "__construct") { validate_eval_member_access(&constructor_class, constructor.visibility(), context)?; - eval_dynamic_method_with_values( + let result = eval_dynamic_method_with_values( &constructor_class, class.name(), &constructor, @@ -3222,6 +3271,7 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( context, values, )?; + eval_release_value(context, values, result)?; } else if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } @@ -3256,7 +3306,7 @@ pub(in crate::interpreter) fn eval_object_clone_result( context.register_dynamic_object(clone_identity, &class_name); context.clone_dynamic_property_aliases(identity, clone_identity); if let Some((declaring_class, method)) = clone_method { - eval_dynamic_method_with_values( + let result = eval_dynamic_method_with_values( &declaring_class, &class_name, &method, @@ -3265,6 +3315,7 @@ pub(in crate::interpreter) fn eval_object_clone_result( context, values, )?; + eval_release_value(context, values, result)?; } } else if should_call_aot_clone_hook { let result = values.method_call(clone, "__clone", Vec::new())?; diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 9b8c9adca4..1929b32a70 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -700,6 +700,31 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval-declared `__destruct()` runs for explicit unset and discarded temporaries. +#[test] +fn execute_program_runs_eval_destructor_on_final_release() { + let program = parse_fragment( + br#"class EvalDestructRuntimeBox { + public string $name; + public function __construct($name) { $this->name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +$box = new EvalDestructRuntimeBox("A"); +unset($box); +new EvalDestructRuntimeBox("B"); +echo "after"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "drop:A:drop:B:after"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies private `__clone()` is not callable through a global clone expression. #[test] fn execute_program_rejects_private_clone_hook_outside_declaring_class() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index ca5af2f749..56fafa8bc2 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -369,6 +369,17 @@ impl RuntimeValueOps for FakeOps { fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { self.runtime_object_identity(object) } + /// Returns fake object identity for releases that target object cells. + fn final_object_identity_for_release( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus> { + if self.runtime_type_tag(value)? == EVAL_TAG_OBJECT { + self.runtime_object_identity(value).map(Some) + } else { + Ok(None) + } + } /// Records fake releases without freeing handles needed for assertions. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { self.runtime_release(value) diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index b9c8f8ebd9..cc665f889e 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -322,6 +322,7 @@ unsafe extern "C" { out_len: *mut u64, ) -> u64; pub(super) fn __elephc_eval_value_truthy(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_final_object_identity(value: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_value_release(value: *mut RuntimeCell); pub(super) fn __elephc_eval_value_retain(value: *mut RuntimeCell) -> *mut RuntimeCell; } diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 3b9fd9087a..baa63a57da 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -704,6 +704,15 @@ impl RuntimeValueOps for ElephcRuntimeOps { } } + /// Returns the object payload that the next release would destroy, when known. + fn final_object_identity_for_release( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus> { + let identity = unsafe { __elephc_eval_value_final_object_identity(value.as_ptr()) }; + Ok((identity != 0).then_some(identity)) + } + /// Releases one boxed Mixed cell through the generated runtime wrapper. fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index df74a2bfb5..00e542355c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -672,9 +672,12 @@ non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter bridge slice. By-reference parameters and broader parameter/return ABI shapes are still outside those bridge paths. -Eval class support is still smaller than the full static class system. The main -remaining class-system gaps are eval-declared `__destruct()` execution on -dynamic-object release, broader reflection APIs beyond the supported +Eval class support is still smaller than the full static class system. +Eval-declared `__destruct()` hooks run when an eval-owned dynamic object reaches +final release through ordinary eval statement execution, such as `unset($obj)` or +a discarded temporary expression; destructor execution from runtime cycle +collection is still outside the bridge. The main remaining class-system gaps are +broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 60fc57f65f..4e74db396e 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -47,9 +47,9 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("b __rt_mixed_from_value"); // box the bool payload and return to Rust label_c_global(emitter, "__elephc_eval_value_new_object"); - emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame across dynamic object lookup - emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across runtime calls - emitter.instruction("mov x29, sp"); // establish a stable wrapper frame pointer + emitter.instruction("sub sp, sp, #32"); // allocate a wrapper frame with object and boxed-result slots + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across runtime calls + emitter.instruction("add x29, sp, #16"); // establish a stable wrapper frame pointer emitter.instruction("cmp x1, #8"); // stdClass has an 8-byte class name emitter.instruction("b.ne __elephc_eval_value_new_object_generic"); // use the generic factory for non-stdClass lengths emitter.instruction("ldrb w9, [x0]"); // load candidate byte 0 for stdClass comparison @@ -84,10 +84,17 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("bl __rt_new_by_name"); // allocate the named AOT class object, or return null on miss emitter.instruction("cbz x0, __elephc_eval_value_new_object_null"); // box PHP null when no runtime class matched the eval name emitter.label("__elephc_eval_value_new_object_box"); + emitter.instruction("str x0, [sp, #0]"); // save the raw object owner before boxing it for eval emitter.instruction("mov x1, x0"); // move the allocated object pointer into the Mixed payload emitter.instruction("mov x0, #6"); // runtime tag 6 = object emitter.instruction("mov x2, xzr"); // object payloads do not use a high word emitter.instruction("bl __rt_mixed_from_value"); // box the allocated object for Rust + emitter.instruction("str x0, [sp, #8]"); // save the boxed Mixed while consuming the raw object owner + emitter.instruction("ldr x0, [sp, #0]"); // reload the raw object owner created by the allocator + emitter.instruction("ldr w9, [x0, #-12]"); // load the raw object refcount after Mixed boxing retained it + emitter.instruction("sub w9, w9, #1"); // consume the allocator-owned object reference locally + emitter.instruction("str w9, [x0, #-12]"); // leave the boxed Mixed as the sole object owner + emitter.instruction("ldr x0, [sp, #8]"); // restore the boxed object Mixed as the Rust return value emitter.instruction("b __elephc_eval_value_new_object_done"); // skip the null boxing path after successful allocation emitter.label("__elephc_eval_value_new_object_null"); emitter.instruction("mov x0, #8"); // runtime tag 8 = null @@ -95,8 +102,8 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov x2, xzr"); // null has no high payload word emitter.instruction("bl __rt_mixed_from_value"); // box null for unknown eval class names emitter.label("__elephc_eval_value_new_object_done"); - emitter.instruction("ldp x29, x30, [sp]"); // restore frame pointer and return address - emitter.instruction("add sp, sp, #16"); // release the dynamic-object wrapper frame + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the dynamic-object wrapper frame emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust emit_aarch64_object_clone_shallow_wrapper(emitter); @@ -1454,6 +1461,31 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_retain"); emitter.instruction("b __rt_incref"); // retain one eval-owned boxed Mixed cell + label_c_global(emitter, "__elephc_eval_value_final_object_identity"); + emitter.instruction("cbz x0, __elephc_eval_value_final_object_none"); // null handles cannot release an object + emitter.label("__elephc_eval_value_final_object_loop"); + emitter.instruction("ldr w9, [x0, #-12]"); // load the current Mixed wrapper refcount + emitter.instruction("cmp w9, #1"); // only the last Mixed owner can free the wrapped payload + emitter.instruction("b.ne __elephc_eval_value_final_object_none"); // non-final wrapper releases cannot run destructors yet + emitter.instruction("ldr x9, [x0]"); // load the current Mixed runtime tag + emitter.instruction("cmp x9, #7"); // tag 7 means the payload is another boxed Mixed + emitter.instruction("b.ne __elephc_eval_value_final_object_check_object"); // concrete payloads can be tested for object ownership + emitter.instruction("ldr x0, [x0, #8]"); // follow the nested Mixed payload pointer + emitter.instruction("cbnz x0, __elephc_eval_value_final_object_loop"); // continue while nested Mixed payloads are present + emitter.instruction("b __elephc_eval_value_final_object_none"); // null nested payloads cannot release an object + emitter.label("__elephc_eval_value_final_object_check_object"); + emitter.instruction("cmp x9, #6"); // tag 6 means the concrete payload is a PHP object + emitter.instruction("b.ne __elephc_eval_value_final_object_none"); // non-object releases have no dynamic destructor + emitter.instruction("ldr x9, [x0, #8]"); // load the object payload pointer from the Mixed cell + emitter.instruction("cbz x9, __elephc_eval_value_final_object_none"); // null object payloads are treated as non-final + emitter.instruction("ldr w10, [x9, #-12]"); // load the object refcount that the Mixed release will decrement + emitter.instruction("cmp w10, #1"); // only the final object owner can run the destructor + emitter.instruction("csel x0, x9, xzr, eq"); // return object identity only for the final release + emitter.instruction("ret"); // return the candidate object identity to Rust + emitter.label("__elephc_eval_value_final_object_none"); + emitter.instruction("mov x0, xzr"); // report that no dynamic destructor should run + emitter.instruction("ret"); // return zero to Rust + label_c_global(emitter, "__elephc_eval_warning"); emitter.instruction("mov x2, x1"); // move warning length into the runtime diagnostic length register emitter.instruction("mov x1, x0"); // move warning pointer into the runtime diagnostic buffer register @@ -1483,6 +1515,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { label_c_global(emitter, "__elephc_eval_value_new_object"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer across runtime calls emitter.instruction("mov rbp, rsp"); // establish a stable dynamic-object wrapper frame + emitter.instruction("sub rsp, 16"); // reserve slots for the raw object and boxed result emitter.instruction("cmp rsi, 8"); // stdClass has an 8-byte class name emitter.instruction("jne __elephc_eval_value_new_object_generic_x86"); // use the generic factory for non-stdClass lengths emitter.instruction("cmp BYTE PTR [rdi], 115"); // byte 0 must be 's' @@ -1510,10 +1543,17 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("test rax, rax"); // did the runtime class-name lookup allocate an object? emitter.instruction("jz __elephc_eval_value_new_object_null_x86"); // box PHP null when no runtime class matched the eval name emitter.label("__elephc_eval_value_new_object_box_x86"); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the raw object owner before boxing it for eval emitter.instruction("mov rdi, rax"); // move the allocated object pointer into the Mixed payload emitter.instruction("mov eax, 6"); // runtime tag 6 = object emitter.instruction("xor esi, esi"); // object payloads do not use a high word emitter.instruction("call __rt_mixed_from_value"); // box the allocated object for Rust + emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the boxed Mixed while consuming the raw object owner + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // reload the raw object owner created by the allocator + emitter.instruction("mov r10d, DWORD PTR [rax - 12]"); // load the raw object refcount after Mixed boxing retained it + emitter.instruction("sub r10d, 1"); // consume the allocator-owned object reference locally + emitter.instruction("mov DWORD PTR [rax - 12], r10d"); // leave the boxed Mixed as the sole object owner + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // restore the boxed object Mixed as the Rust return value emitter.instruction("jmp __elephc_eval_value_new_object_done_x86"); // skip the null boxing path after successful allocation emitter.label("__elephc_eval_value_new_object_null_x86"); emitter.instruction("mov eax, 8"); // runtime tag 8 = null @@ -1521,6 +1561,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("xor esi, esi"); // null has no high payload word emitter.instruction("call __rt_mixed_from_value"); // box null for unknown eval class names emitter.label("__elephc_eval_value_new_object_done_x86"); + emitter.instruction("mov rsp, rbp"); // release the dynamic-object wrapper slots emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust @@ -2973,6 +3014,36 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rax, rdi"); // move the C boxed Mixed argument into the internal retain register emitter.instruction("jmp __rt_incref"); // retain one eval-owned boxed Mixed cell + label_c_global(emitter, "__elephc_eval_value_final_object_identity"); + emitter.instruction("mov rax, rdi"); // inspect the C boxed Mixed argument without changing refcounts + emitter.instruction("test rax, rax"); // null handles cannot release an object + emitter.instruction("jz __elephc_eval_value_final_object_none_x86"); // report no final object for null handles + emitter.label("__elephc_eval_value_final_object_loop_x86"); + emitter.instruction("mov r10d, DWORD PTR [rax - 12]"); // load the current Mixed wrapper refcount + emitter.instruction("cmp r10d, 1"); // only the last Mixed owner can free the wrapped payload + emitter.instruction("jne __elephc_eval_value_final_object_none_x86"); // non-final wrapper releases cannot run destructors yet + emitter.instruction("mov r10, QWORD PTR [rax]"); // load the current Mixed runtime tag + emitter.instruction("cmp r10, 7"); // tag 7 means the payload is another boxed Mixed + emitter.instruction("jne __elephc_eval_value_final_object_check_object_x86"); // concrete payloads can be tested for object ownership + emitter.instruction("mov rax, QWORD PTR [rax + 8]"); // follow the nested Mixed payload pointer + emitter.instruction("test rax, rax"); // null nested payloads cannot release an object + emitter.instruction("jnz __elephc_eval_value_final_object_loop_x86"); // continue while nested Mixed payloads are present + emitter.instruction("jmp __elephc_eval_value_final_object_none_x86"); // return zero for null nested payloads + emitter.label("__elephc_eval_value_final_object_check_object_x86"); + emitter.instruction("cmp r10, 6"); // tag 6 means the concrete payload is a PHP object + emitter.instruction("jne __elephc_eval_value_final_object_none_x86"); // non-object releases have no dynamic destructor + emitter.instruction("mov r10, QWORD PTR [rax + 8]"); // load the object payload pointer from the Mixed cell + emitter.instruction("test r10, r10"); // null object payloads are treated as non-final + emitter.instruction("jz __elephc_eval_value_final_object_none_x86"); // return zero for null object payloads + emitter.instruction("mov r11d, DWORD PTR [r10 - 12]"); // load the object refcount that the Mixed release will decrement + emitter.instruction("cmp r11d, 1"); // only the final object owner can run the destructor + emitter.instruction("jne __elephc_eval_value_final_object_none_x86"); // defer destructor execution until object refcount is final + emitter.instruction("mov rax, r10"); // return the object identity pointer to Rust + emitter.instruction("ret"); // finish the final-object query + emitter.label("__elephc_eval_value_final_object_none_x86"); + emitter.instruction("xor eax, eax"); // report that no dynamic destructor should run + emitter.instruction("ret"); // return zero to Rust + label_c_global(emitter, "__elephc_eval_warning"); emitter.instruction("jmp __rt_diag_warning"); // emit or suppress one eval runtime warning diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fcee139910..d279cc5ae7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11334,6 +11334,25 @@ echo $second->name;'); assert_eq!(out, "A:A:clone:A:B"); } +/// Verifies eval-declared `__destruct()` runs before final release of dynamic objects. +#[test] +fn test_eval_dynamic_object_runs_destructor_on_final_release() { + let out = compile_and_run( + r#"name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +$box = new EvalDestructRuntimeBox("A"); +unset($box); +new EvalDestructRuntimeBox("B"); +echo "after";'); +"#, + ); + assert_eq!(out, "drop:A:drop:B:after"); +} + /// Verifies eval `clone` shallow-copies ordinary emitted AOT objects. #[test] fn test_eval_clone_aot_object_expression() { From b2f1677fce0b90cff5f40006b87c330a3b16a240 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 10:28:18 +0200 Subject: [PATCH 0620/1208] Support eval reflection closure variable metadata --- crates/elephc-magician/src/interpreter/reflection.rs | 4 ++++ .../src/interpreter/tests/builtins_class_metadata.rs | 5 +++-- docs/php/eval.md | 5 +++-- tests/codegen/eval.rs | 6 ++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 1d9230382f..84fdf6d3dd 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -898,6 +898,10 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( eval_reflection_bind_no_args(evaluated_args)?; values.null().map(Some) } + "getclosureusedvariables" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.array_new(0).map(Some) + } _ => Ok(None), } } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 52f9301c2a..e11917ec3a 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -384,7 +384,8 @@ echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; echo $ref->isGenerator() ? "G" : "g"; echo ":"; echo $ref->isVariadic() ? "V" : "v"; echo ":"; echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; -echo $ref->getTentativeReturnType() === null ? "Q" : "q"; +echo $ref->getTentativeReturnType() === null ? "Q" : "q"; echo ":"; +echo count($ref->getClosureUsedVariables()); return true;"#, ) .expect("parse eval fragment"); @@ -393,7 +394,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "run::N:iU:c:d:r:t:N:g:V:h:Q"); + assert_eq!(values.output, "run::N:iU:c:d:r:t:N:g:V:h:Q:0"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 00e542355c..d4835588ea 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -224,8 +224,9 @@ and `getReturnType()` expose retained eval return type metadata for supported named, nullable, union, and intersection declarations, including `void` and `never` as builtin non-nullable named types. `ReflectionFunction::isDisabled()` reports `false` for eval-visible functions. -`ReflectionFunction::getClosureUsedVariables()` reports an empty array for -supported non-closure function reflectors. +`ReflectionFunction::getClosureUsedVariables()` and +`ReflectionMethod::getClosureUsedVariables()` report empty arrays for supported +non-closure reflectors. `ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval and generated/AOT class-like metadata, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d279cc5ae7..c1e1f8fb1a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11138,6 +11138,7 @@ echo ($fn->isGenerator() ? "G" : "g") . ":"; echo ($fn->isVariadic() ? "V" : "v") . ":"; echo ($fn->hasTentativeReturnType() ? "H" : "h") . ":"; echo ($fn->getTentativeReturnType() === null ? "Q" : "q") . ":"; +echo count($fn->getClosureUsedVariables()) . ":"; echo ($fn->isDisabled() ? "X" : "x") . "|"; echo $method->getShortName() . ":"; echo $method->getNamespaceName() . ":"; @@ -11152,7 +11153,8 @@ echo ($method->getReturnType() === null ? "N" : "n") . ":"; echo ($method->isGenerator() ? "G" : "g") . ":"; echo ($method->isVariadic() ? "V" : "v") . ":"; echo ($method->hasTentativeReturnType() ? "H" : "h") . ":"; -echo $method->getTentativeReturnType() === null ? "Q" : "q";'); +echo ($method->getTentativeReturnType() === null ? "Q" : "q") . ":"; +echo count($method->getClosureUsedVariables());'); "#, ); assert!( @@ -11162,7 +11164,7 @@ echo $method->getTentativeReturnType() === null ? "Q" : "q";'); ); assert_eq!( out.stdout, - "sample:EvalReflectNameNs:Y:iU:c:d:r:t:N:g:V:h:Q:x|run::N:iU:c:d:r:t:N:g:V:h:Q" + "sample:EvalReflectNameNs:Y:iU:c:d:r:t:N:g:V:h:Q:0:x|run::N:iU:c:d:r:t:N:g:V:h:Q:0" ); } From 30780aea4fd0242a6589803e6b2134d7c62113c6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 10:54:42 +0200 Subject: [PATCH 0621/1208] Track eval reflection source locations --- crates/elephc-magician/src/eval_ir.rs | 257 +++++++++++++++++- .../src/interpreter/reflection.rs | 95 +++++++ .../src/interpreter/statements.rs | 33 ++- .../tests/builtins_class_metadata.rs | 39 +++ .../tests/builtins_reflection_functions.rs | 26 ++ crates/elephc-magician/src/lexer/mod.rs | 2 +- crates/elephc-magician/src/lexer/scan.rs | 17 +- crates/elephc-magician/src/lexer/token.rs | 29 ++ crates/elephc-magician/src/parser/cursor.rs | 5 + crates/elephc-magician/src/parser/state.rs | 8 +- .../elephc-magician/src/parser/statements.rs | 130 +++++++-- .../src/parser/tests/namespaces.rs | 3 + docs/php/eval.md | 4 + tests/codegen/eval.rs | 25 ++ 14 files changed, 619 insertions(+), 54 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index d95563213c..0498f80a35 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -43,6 +43,38 @@ impl EvalProgram { } } +/// One source range inside the current eval fragment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EvalSourceLocation { + start_line: i64, + end_line: i64, +} + +impl EvalSourceLocation { + /// Creates a source range using one-based eval-fragment line numbers. + pub const fn new(start_line: i64, end_line: i64) -> Self { + Self { + start_line, + end_line, + } + } + + /// Creates a single-line source range. + pub const fn single_line(line: i64) -> Self { + Self::new(line, line) + } + + /// Returns the one-based line where the declaration starts. + pub const fn start_line(&self) -> i64 { + self.start_line + } + + /// Returns the one-based line where the declaration ends. + pub const fn end_line(&self) -> i64 { + self.end_line + } +} + /// Dynamic eval statements that operate on a materialized activation scope. #[derive(Debug, Clone, PartialEq)] pub enum EvalStmt { @@ -80,6 +112,7 @@ pub enum EvalStmt { }, FunctionDecl { name: String, + source_location: Option, attributes: Vec, params: Vec, parameter_attributes: Vec>, @@ -163,9 +196,10 @@ pub struct EvalCatch { } /// Runtime user function declared by an eval fragment. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EvalFunction { name: String, + source_location: Option, attributes: Vec, params: Vec, parameter_attributes: Vec>, @@ -177,6 +211,22 @@ pub struct EvalFunction { body: Vec, } +impl PartialEq for EvalFunction { + /// Compares function metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + && self.body == other.body + } +} + impl EvalFunction { /// Creates a dynamic eval function with source-order parameters and body. pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { @@ -187,6 +237,7 @@ impl EvalFunction { let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), + source_location: None, attributes: Vec::new(), params, parameter_attributes, @@ -199,6 +250,12 @@ impl EvalFunction { } } + /// Returns a copy of this function with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + /// Returns a copy of this function with declaration attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; @@ -249,6 +306,11 @@ impl EvalFunction { &self.name } + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + /// Returns attributes declared directly on this eval function. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes @@ -399,9 +461,10 @@ impl EvalAttribute { } /// Runtime enum declared by an eval fragment. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EvalEnum { name: String, + source_location: Option, backing_type: Option, interfaces: Vec, attributes: Vec, @@ -410,6 +473,19 @@ pub struct EvalEnum { methods: Vec, } +impl PartialEq for EvalEnum { + /// Compares enum metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.backing_type == other.backing_type + && self.interfaces == other.interfaces + && self.attributes == other.attributes + && self.cases == other.cases + && self.constants == other.constants + && self.methods == other.methods + } +} + impl EvalEnum { /// Creates a dynamic eval enum with cases and optional backing type. pub fn new( @@ -438,6 +514,7 @@ impl EvalEnum { ) -> Self { Self { name: name.into(), + source_location: None, backing_type, interfaces, attributes: Vec::new(), @@ -447,6 +524,12 @@ impl EvalEnum { } } + /// Returns a copy of this enum with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + /// Returns a copy of this enum with class-like attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; @@ -458,6 +541,11 @@ impl EvalEnum { &self.name } + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + /// Returns the optional scalar backing type for this enum. pub const fn backing_type(&self) -> Option { self.backing_type @@ -507,6 +595,7 @@ impl EvalEnum { self.methods.clone(), ) .with_attributes(self.attributes.clone()) + .with_source_location_option(self.source_location) } } @@ -558,9 +647,10 @@ impl EvalEnumCase { } /// Runtime interface declared by an eval fragment. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EvalInterface { name: String, + source_location: Option, parents: Vec, attributes: Vec, constants: Vec, @@ -568,6 +658,18 @@ pub struct EvalInterface { methods: Vec, } +impl PartialEq for EvalInterface { + /// Compares interface metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.parents == other.parents + && self.attributes == other.attributes + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + impl EvalInterface { /// Creates a dynamic eval interface with optional parent interfaces and methods. pub fn new( @@ -598,6 +700,7 @@ impl EvalInterface { ) -> Self { Self { name: name.into(), + source_location: None, parents, attributes: Vec::new(), constants, @@ -606,6 +709,12 @@ impl EvalInterface { } } + /// Returns a copy of this interface with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + /// Returns a copy of this interface with class-like attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; @@ -617,6 +726,11 @@ impl EvalInterface { &self.name } + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + /// Returns interface names extended directly by this eval interface. pub fn parents(&self) -> &[String] { &self.parents @@ -783,9 +897,10 @@ const fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { } /// Method signature metadata for a runtime eval interface. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EvalInterfaceMethod { name: String, + source_location: Option, attributes: Vec, is_static: bool, params: Vec, @@ -798,6 +913,23 @@ pub struct EvalInterfaceMethod { return_type: Option, } +impl PartialEq for EvalInterfaceMethod { + /// Compares interface method metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.is_static == other.is_static + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_has_types == other.parameter_has_types + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + } +} + impl EvalInterfaceMethod { /// Creates one dynamic eval interface method signature. pub fn new(name: impl Into, params: Vec) -> Self { @@ -809,6 +941,7 @@ impl EvalInterfaceMethod { let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), + source_location: None, attributes: Vec::new(), is_static: false, params, @@ -822,6 +955,12 @@ impl EvalInterfaceMethod { } } + /// Returns a copy of this interface method with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + /// Returns a copy of this interface method with its static modifier flag set. pub fn with_static(mut self, is_static: bool) -> Self { self.is_static = is_static; @@ -885,6 +1024,11 @@ impl EvalInterfaceMethod { &self.name } + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + /// Returns attributes declared directly on this interface method. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes @@ -937,9 +1081,10 @@ impl EvalInterfaceMethod { } /// Runtime class declared by an eval fragment. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EvalClass { name: String, + source_location: Option, is_abstract: bool, is_final: bool, is_readonly_class: bool, @@ -954,6 +1099,25 @@ pub struct EvalClass { methods: Vec, } +impl PartialEq for EvalClass { + /// Compares class metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.is_abstract == other.is_abstract + && self.is_final == other.is_final + && self.is_readonly_class == other.is_readonly_class + && self.is_anonymous == other.is_anonymous + && self.parent == other.parent + && self.interfaces == other.interfaces + && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + impl EvalClass { /// Creates a dynamic eval class with public properties and methods, and no relations. pub fn new( @@ -1169,6 +1333,7 @@ impl EvalClass { ) -> Self { Self { name: name.into(), + source_location: None, is_abstract, is_final, is_readonly_class, @@ -1184,6 +1349,21 @@ impl EvalClass { } } + /// Returns a copy of this class with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this class with optional source-location metadata attached. + pub const fn with_source_location_option( + mut self, + source_location: Option, + ) -> Self { + self.source_location = source_location; + self + } + /// Returns a copy of this class with class-like attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; @@ -1213,6 +1393,11 @@ impl EvalClass { &self.name } + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + /// Returns whether this eval-declared class was declared `abstract`. pub const fn is_abstract(&self) -> bool { self.is_abstract @@ -1378,15 +1563,27 @@ impl EvalClassConstant { } /// Runtime trait declared by an eval fragment. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EvalTrait { name: String, + source_location: Option, attributes: Vec, constants: Vec, properties: Vec, methods: Vec, } +impl PartialEq for EvalTrait { + /// Compares trait metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + impl EvalTrait { /// Creates a dynamic eval trait with public properties and methods. pub fn new( @@ -1406,6 +1603,7 @@ impl EvalTrait { ) -> Self { Self { name: name.into(), + source_location: None, attributes: Vec::new(), constants, properties, @@ -1413,6 +1611,12 @@ impl EvalTrait { } } + /// Returns a copy of this trait with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + /// Returns a copy of this trait with class-like attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; @@ -1424,6 +1628,11 @@ impl EvalTrait { &self.name } + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + /// Returns attributes declared directly on this eval trait. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes @@ -1694,9 +1903,10 @@ pub enum EvalVisibility { } /// Public method metadata for a runtime eval class. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct EvalClassMethod { name: String, + source_location: Option, attributes: Vec, visibility: EvalVisibility, is_static: bool, @@ -1713,6 +1923,27 @@ pub struct EvalClassMethod { body: Vec, } +impl PartialEq for EvalClassMethod { + /// Compares class method metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.visibility == other.visibility + && self.is_static == other.is_static + && self.is_abstract == other.is_abstract + && self.is_final == other.is_final + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_has_types == other.parameter_has_types + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + && self.body == other.body + } +} + impl EvalClassMethod { /// Creates a public eval class method with source-order parameters and body. pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { @@ -1756,6 +1987,7 @@ impl EvalClassMethod { let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), + source_location: None, attributes: Vec::new(), visibility, is_static, @@ -1773,11 +2005,22 @@ impl EvalClassMethod { } } + /// Returns a copy of this method with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + /// Returns the PHP-visible method name. pub fn name(&self) -> &str { &self.name } + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + /// Returns a copy of this method with declaration attributes attached. pub fn with_attributes(mut self, attributes: Vec) -> Self { self.attributes = attributes; diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 84fdf6d3dd..51fcfcef8b 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -11,6 +11,7 @@ //! - Generated/AOT targets use focused runtime hooks for supported point lookups. use super::*; +use crate::eval_ir::EvalSourceLocation; const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; @@ -60,6 +61,7 @@ const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; /// Eval metadata needed to materialize one `ReflectionClass` owner object. struct EvalReflectionClassMetadata { resolved_name: String, + source_location: Option, attributes: Vec, flags: u64, modifiers: u64, @@ -73,6 +75,7 @@ struct EvalReflectionClassMetadata { /// Eval metadata needed to materialize one `ReflectionMethod` or `ReflectionProperty` owner object. struct EvalReflectionMemberMetadata { declaring_class_name: Option, + source_location: Option, attributes: Vec, visibility: EvalVisibility, is_static: bool, @@ -181,11 +184,13 @@ struct EvalReflectionNamedTypeMetadata { enum EvalReflectionFunctionMethodTarget { Function { name: String, + source_location: Option, is_variadic: bool, return_type_metadata: Option, }, Method { name: String, + source_location: Option, is_variadic: bool, return_type_metadata: Option, }, @@ -362,6 +367,35 @@ pub(in crate::interpreter) fn eval_reflection_class_is_instance_result( values.bool_value(result).map(Some) } +/// Handles eval-backed `ReflectionClass` source-location metadata calls. +pub(in crate::interpreter) fn eval_reflection_class_source_location_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method_key = method_name.to_ascii_lowercase(); + if !matches!( + method_key.as_str(), + "getfilename" | "getstartline" | "getendline" + ) { + return Ok(None); + } + let Some(reflected_name) = context.eval_reflection_class_name(identity) else { + return Ok(None); + }; + let source_location = eval_reflection_class_like_attributes(reflected_name, context) + .and_then(|metadata| metadata.source_location); + eval_reflection_source_location_result( + method_key.as_str(), + source_location, + evaluated_args, + context, + values, + ) +} + /// Handles eval-backed `ReflectionClass::hasMethod()` calls. pub(in crate::interpreter) fn eval_reflection_class_has_method_result( identity: u64, @@ -857,6 +891,15 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( .bool_value(!eval_reflection_function_method_namespace_name(&target).is_empty()) .map(Some) } + "getfilename" | "getstartline" | "getendline" => { + eval_reflection_source_location_result( + method_key.as_str(), + eval_reflection_function_method_source_location(&target), + evaluated_args, + context, + values, + ) + } "isinternal" | "isclosure" | "isdeprecated" @@ -2610,6 +2653,7 @@ fn eval_reflection_object_dynamic_property_exists( fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflectionMemberMetadata { EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_location: None, attributes: Vec::new(), visibility: EvalVisibility::Public, is_static: false, @@ -2739,6 +2783,7 @@ fn eval_reflection_aot_method_metadata( .and_then(eval_reflection_parameter_type_metadata); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_location: None, attributes, visibility, is_static: flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, @@ -3011,6 +3056,7 @@ fn eval_reflection_aot_property_metadata( } EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_location: None, attributes, visibility, is_static, @@ -4270,6 +4316,7 @@ fn eval_reflection_class_like_attributes( ); return Some(EvalReflectionClassMetadata { resolved_name: class.name().trim_start_matches('\\').to_string(), + source_location: class.source_location(), attributes: class.attributes().to_vec(), interface_names: context.class_interface_names(class.name()), trait_names: context.class_trait_names(class.name()), @@ -4283,6 +4330,7 @@ fn eval_reflection_class_like_attributes( if let Some(interface) = context.interface(name) { return Some(EvalReflectionClassMetadata { resolved_name: interface.name().trim_start_matches('\\').to_string(), + source_location: interface.source_location(), attributes: interface.attributes().to_vec(), interface_names: context.interface_parent_names(interface.name()), trait_names: Vec::new(), @@ -4296,6 +4344,7 @@ fn eval_reflection_class_like_attributes( if let Some(trait_decl) = context.trait_decl(name) { return Some(EvalReflectionClassMetadata { resolved_name: trait_decl.name().trim_start_matches('\\').to_string(), + source_location: trait_decl.source_location(), attributes: trait_decl.attributes().to_vec(), interface_names: Vec::new(), trait_names: Vec::new(), @@ -4310,6 +4359,7 @@ fn eval_reflection_class_like_attributes( .enum_decl(name) .map(|enum_decl| EvalReflectionClassMetadata { resolved_name: enum_decl.name().trim_start_matches('\\').to_string(), + source_location: enum_decl.source_location(), attributes: enum_decl.attributes().to_vec(), interface_names: context.class_interface_names(enum_decl.name()), trait_names: Vec::new(), @@ -4925,6 +4975,7 @@ fn eval_reflection_method_metadata( ); EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class), + source_location: method.source_location(), attributes: method.attributes().to_vec(), visibility: method.visibility(), is_static: method.is_static(), @@ -4988,6 +5039,7 @@ fn eval_reflection_method_metadata( ); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.to_string()), + source_location: method.source_location(), attributes: method.attributes().to_vec(), visibility: EvalVisibility::Public, is_static: method.is_static(), @@ -5053,6 +5105,7 @@ fn eval_reflection_method_metadata( ); EvalReflectionMemberMetadata { declaring_class_name: Some(trait_decl.name().to_string()), + source_location: method.source_location(), attributes: method.attributes().to_vec(), visibility: method.visibility(), is_static: method.is_static(), @@ -5089,6 +5142,7 @@ fn eval_reflection_property_metadata( let default_value = eval_reflection_property_default_value(&property); EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class), + source_location: None, attributes: property.attributes().to_vec(), visibility: property.visibility(), is_static: property.is_static(), @@ -5124,6 +5178,7 @@ fn eval_reflection_property_metadata( .find(|property| property.name() == property_name) .map(|property| EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.to_string()), + source_location: None, attributes: property.attributes().to_vec(), visibility: EvalVisibility::Public, is_static: false, @@ -5159,6 +5214,7 @@ fn eval_reflection_property_metadata( let default_value = eval_reflection_property_default_value(property); EvalReflectionMemberMetadata { declaring_class_name: Some(trait_decl.name().to_string()), + source_location: None, attributes: property.attributes().to_vec(), visibility: property.visibility(), is_static: property.is_static(), @@ -5565,6 +5621,7 @@ fn eval_reflection_property_hook_method_metadata( let required_parameter_count = parameters.len(); EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class.to_string()), + source_location: None, attributes: Vec::new(), visibility: property.visibility(), is_static: false, @@ -6590,11 +6647,13 @@ fn eval_reflection_function_method_target( let function = context.function(&name.to_ascii_lowercase()); let is_variadic = function .is_some_and(|function| function.parameter_is_variadic().iter().any(|flag| *flag)); + let source_location = function.and_then(EvalFunction::source_location); let return_type_metadata = function .and_then(EvalFunction::return_type) .and_then(eval_reflection_parameter_type_metadata); return Ok(Some(EvalReflectionFunctionMethodTarget::Function { name: name.to_string(), + source_location, is_variadic, return_type_metadata, })); @@ -6620,9 +6679,11 @@ fn eval_reflection_function_method_target( .iter() .any(|parameter| parameter.is_variadic) }); + let source_location = method_metadata.as_ref().and_then(|method| method.source_location); let return_type_metadata = method_metadata.and_then(|method| method.return_type_metadata); Ok(Some(EvalReflectionFunctionMethodTarget::Method { name: method_name.to_string(), + source_location, is_variadic, return_type_metadata, })) @@ -6643,6 +6704,26 @@ fn eval_reflection_false_metadata_result( values.bool_value(false).map(Some) } +/// Returns source file or line metadata for eval-backed reflection objects. +fn eval_reflection_source_location_result( + method_key: &str, + source_location: Option, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + let Some(source_location) = source_location else { + return values.bool_value(false).map(Some); + }; + match method_key { + "getfilename" => values.string(&context.eval_file_magic()).map(Some), + "getstartline" => values.int(source_location.start_line()).map(Some), + "getendline" => values.int(source_location.end_line()).map(Some), + _ => Ok(None), + } +} + /// Returns PHP's short name for a ReflectionFunction or ReflectionMethod target. fn eval_reflection_function_method_short_name( target: &EvalReflectionFunctionMethodTarget, @@ -6655,6 +6736,20 @@ fn eval_reflection_function_method_short_name( } } +/// Returns eval-fragment source metadata for a ReflectionFunction or ReflectionMethod target. +fn eval_reflection_function_method_source_location( + target: &EvalReflectionFunctionMethodTarget, +) -> Option { + match target { + EvalReflectionFunctionMethodTarget::Function { + source_location, .. + } + | EvalReflectionFunctionMethodTarget::Method { + source_location, .. + } => *source_location, + } +} + /// Returns PHP's namespace name for a ReflectionFunction or ReflectionMethod target. fn eval_reflection_function_method_namespace_name( target: &EvalReflectionFunctionMethodTarget, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 13111e91b9..5f9a54f054 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -99,6 +99,7 @@ pub(in crate::interpreter) fn execute_stmt( ), EvalStmt::FunctionDecl { name, + source_location, attributes, params, parameter_attributes, @@ -110,18 +111,19 @@ pub(in crate::interpreter) fn execute_stmt( body, } => { let key = name.to_ascii_lowercase(); + let mut function = EvalFunction::new(name.clone(), params.clone(), body.clone()) + .with_attributes(attributes.clone()) + .with_parameter_attributes(parameter_attributes.clone()) + .with_parameter_types(parameter_types.clone()) + .with_parameter_defaults(parameter_defaults.clone()) + .with_parameter_by_ref_flags(parameter_is_by_ref.clone()) + .with_parameter_variadic_flags(parameter_is_variadic.clone()) + .with_return_type(return_type.clone()); + if let Some(source_location) = source_location { + function = function.with_source_location(*source_location); + } context - .define_function( - key, - EvalFunction::new(name.clone(), params.clone(), body.clone()) - .with_attributes(attributes.clone()) - .with_parameter_attributes(parameter_attributes.clone()) - .with_parameter_types(parameter_types.clone()) - .with_parameter_defaults(parameter_defaults.clone()) - .with_parameter_by_ref_flags(parameter_is_by_ref.clone()) - .with_parameter_variadic_flags(parameter_is_variadic.clone()) - .with_return_type(return_type.clone()), - ) + .define_function(key, function) .map_err(|_| EvalStatus::RuntimeFatal)?; Ok(EvalControl::None) } @@ -3487,6 +3489,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_source_location_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_has_method_result( identity, method_name, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index e11917ec3a..fdb90a5eb4 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -362,6 +362,45 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass and ReflectionMethod report eval source-location metadata. +#[test] +fn execute_program_reflection_class_and_method_report_source_location() { + let program = parse_fragment( + br#"class EvalReflectSource { + public function run() { + return 1; + } +} +interface EvalReflectSourceIface { + public function iface(); +} +$class = new ReflectionClass("EvalReflectSource"); +$method = new ReflectionMethod("EvalReflectSource", "run"); +$iface = new ReflectionClass("EvalReflectSourceIface"); +$ifaceMethod = new ReflectionMethod("EvalReflectSourceIface", "iface"); +echo $class->getFileName(); echo ":"; +echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; +echo $method->getStartLine(); echo ":"; echo $method->getEndLine(); echo ":"; +echo $iface->getStartLine(); echo ":"; echo $iface->getEndLine(); echo ":"; +echo $ifaceMethod->getStartLine(); echo ":"; echo $ifaceMethod->getEndLine(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/eval-class-source.php", "/tmp", 23); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.output, + "/tmp/eval-class-source.php(23) : eval()'d code:1:5:2:4:6:8:7:7" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod exposes PHP-compatible name and origin predicate metadata. #[test] fn execute_program_reflection_method_reports_name_and_origin_predicates() { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs index e385b45fc5..0cb5cfd6f3 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -185,6 +185,32 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionFunction reports eval source file and line metadata. +#[test] +fn execute_program_reflection_function_reports_source_location() { + let program = parse_fragment( + br#"function eval_reflect_source_fn() { + return 1; +} +$ref = new ReflectionFunction("eval_reflect_source_fn"); +echo $ref->getFileName(); echo ":"; +echo $ref->getStartLine(); echo ":"; +echo $ref->getEndLine(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/eval-source.php", "/tmp", 17); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "/tmp/eval-source.php(17) : eval()'d code:1:3"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval-declared functions bind named, default, by-reference, and variadic arguments. #[test] fn execute_program_calls_eval_function_with_rich_argument_binding() { diff --git a/crates/elephc-magician/src/lexer/mod.rs b/crates/elephc-magician/src/lexer/mod.rs index eb56280b62..9c3ba4cdf3 100644 --- a/crates/elephc-magician/src/lexer/mod.rs +++ b/crates/elephc-magician/src/lexer/mod.rs @@ -14,4 +14,4 @@ mod scan; mod token; pub(crate) use scan::tokenize; -pub(crate) use token::TokenKind; +pub(crate) use token::{Token, TokenKind}; diff --git a/crates/elephc-magician/src/lexer/scan.rs b/crates/elephc-magician/src/lexer/scan.rs index 59ca47147d..35af3a220f 100644 --- a/crates/elephc-magician/src/lexer/scan.rs +++ b/crates/elephc-magician/src/lexer/scan.rs @@ -10,12 +10,12 @@ //! - Comments and whitespace advance line metadata for `__LINE__`. //! - Unterminated strings or block comments return parse errors before grammar parsing. -use super::TokenKind; +use super::{Token, TokenKind}; use crate::errors::EvalParseError; use crate::eval_ir::EvalMagicConst; /// Tokenizes a complete source fragment and appends an EOF sentinel. -pub(crate) fn tokenize(source: &str) -> Result, EvalParseError> { +pub(crate) fn tokenize(source: &str) -> Result, EvalParseError> { Lexer::new(source).tokenize() } @@ -37,11 +37,11 @@ impl<'a> Lexer<'a> { } /// Tokenizes the complete source and appends an EOF sentinel. - fn tokenize(mut self) -> Result, EvalParseError> { + fn tokenize(mut self) -> Result, EvalParseError> { let mut tokens = Vec::new(); loop { let token = self.next_token()?; - let done = token == TokenKind::Eof; + let done = *token.kind() == TokenKind::Eof; tokens.push(token); if done { break; @@ -51,13 +51,13 @@ impl<'a> Lexer<'a> { } /// Reads the next token from the source. - fn next_token(&mut self) -> Result { + fn next_token(&mut self) -> Result { self.skip_trivia()?; let Some(ch) = self.peek_char() else { - return Ok(TokenKind::Eof); + return Ok(Token::new(TokenKind::Eof, self.line)); }; let line = self.line; - match ch { + let kind = match ch { '$' => self.lex_variable(), '\'' | '"' => self.lex_string(ch), '0'..='9' => self.lex_number(), @@ -307,7 +307,8 @@ impl<'a> Lexer<'a> { Ok(magic_const_token(&ident, line).unwrap_or(TokenKind::Ident(ident))) } _ => Err(EvalParseError::UnexpectedToken), - } + }?; + Ok(Token::new(kind, line)) } /// Reads a `$name` token. diff --git a/crates/elephc-magician/src/lexer/token.rs b/crates/elephc-magician/src/lexer/token.rs index 6902dbcd3b..628ed2a805 100644 --- a/crates/elephc-magician/src/lexer/token.rs +++ b/crates/elephc-magician/src/lexer/token.rs @@ -12,6 +12,35 @@ use crate::eval_ir::EvalMagicConst; +/// One token plus its eval-fragment source line. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct Token { + kind: TokenKind, + line: i64, +} + +impl Token { + /// Creates one token at the given eval-fragment line. + pub(crate) const fn new(kind: TokenKind, line: i64) -> Self { + Self { kind, line } + } + + /// Returns the parser-visible token kind. + pub(crate) fn kind(&self) -> &TokenKind { + &self.kind + } + + /// Consumes the token and returns its parser-visible kind. + pub(crate) fn into_kind(self) -> TokenKind { + self.kind + } + + /// Returns the one-based eval-fragment line where this token starts. + pub(crate) const fn line(&self) -> i64 { + self.line + } +} + /// Token kinds used by the initial eval fragment parser. #[derive(Debug, Clone, PartialEq)] pub(crate) enum TokenKind { diff --git a/crates/elephc-magician/src/parser/cursor.rs b/crates/elephc-magician/src/parser/cursor.rs index 2ec77de93f..960b09581c 100644 --- a/crates/elephc-magician/src/parser/cursor.rs +++ b/crates/elephc-magician/src/parser/cursor.rs @@ -52,6 +52,11 @@ impl Parser { self.tokens.get(self.pos).unwrap_or(&TokenKind::Eof) } + /// Returns the line attached to the current token. + pub(super) fn current_line(&self) -> i64 { + self.token_lines.get(self.pos).copied().unwrap_or(1) + } + /// Returns the next token without advancing. pub(super) fn peek(&self) -> &TokenKind { self.tokens.get(self.pos + 1).unwrap_or(&TokenKind::Eof) diff --git a/crates/elephc-magician/src/parser/state.rs b/crates/elephc-magician/src/parser/state.rs index 74953afa16..e407adfb9b 100644 --- a/crates/elephc-magician/src/parser/state.rs +++ b/crates/elephc-magician/src/parser/state.rs @@ -14,7 +14,7 @@ use super::cursor::split_first_name_segment; use crate::errors::EvalParseError; use crate::eval_ir::EvalProgram; -use crate::lexer::TokenKind; +use crate::lexer::{Token, TokenKind}; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -23,6 +23,7 @@ static ANONYMOUS_CLASS_COUNTER: AtomicUsize = AtomicUsize::new(0); /// Parses tokenized eval fragments into EvalIR. pub(super) struct Parser { pub(super) tokens: Vec, + pub(super) token_lines: Vec, pub(super) pos: usize, pub(super) source_len: usize, pub(super) namespace: String, @@ -99,9 +100,12 @@ impl NamespaceImports { impl Parser { /// Creates a parser over tokens produced from a source fragment. - pub(super) fn new(tokens: Vec, source_len: usize) -> Self { + pub(super) fn new(tokens: Vec, source_len: usize) -> Self { + let token_lines = tokens.iter().map(Token::line).collect(); + let tokens = tokens.into_iter().map(Token::into_kind).collect(); Self { tokens, + token_lines, pos: 0, source_len, namespace: String::new(), diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 11767adbdd..973bee5aa0 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -16,7 +16,8 @@ use crate::eval_ir::{ EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInstanceOfTarget, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, EvalParameterTypeVariant, - EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, + EvalSourceLocation, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, + EvalVisibility, }; use crate::lexer::TokenKind; @@ -34,6 +35,7 @@ pub(super) struct ParsedMethodParams { /// Class-body members collected while parsing a named or anonymous eval class. struct ParsedClassBody { + source_end_line: i64, constants: Vec, properties: Vec, methods: Vec, @@ -370,6 +372,7 @@ impl Parser { if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { return Err(EvalParseError::UnsupportedConstruct); } + let source_start_line = self.current_line(); self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -379,6 +382,7 @@ impl Parser { let parent = self.parse_class_parent_clause()?; let interfaces = self.parse_class_interface_clause()?; let body = self.parse_class_body_members(is_readonly_class)?; + let source_location = EvalSourceLocation::new(source_start_line, body.source_end_line); self.consume_semicolon(); Ok(vec![EvalStmt::ClassDecl( EvalClass::with_class_modifiers_traits_adaptations_and_constants( @@ -394,6 +398,7 @@ impl Parser { body.properties, body.methods, ) + .with_source_location(source_location) .with_attributes(attributes), )]) } @@ -406,6 +411,7 @@ impl Parser { if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { return Err(EvalParseError::UnsupportedConstruct); } + let source_start_line = self.current_line(); self.advance(); let args = if matches!(self.current(), TokenKind::LParen) { self.parse_call_args()? @@ -415,6 +421,7 @@ impl Parser { let parent = self.parse_class_parent_clause()?; let interfaces = self.parse_class_interface_clause()?; let body = self.parse_class_body_members(is_readonly_class)?; + let source_location = EvalSourceLocation::new(source_start_line, body.source_end_line); let name = next_anonymous_class_name(); let class = EvalClass::with_class_modifiers_traits_adaptations_and_constants( name, @@ -429,6 +436,7 @@ impl Parser { body.properties, body.methods, ) + .with_source_location(source_location) .with_anonymous(); Ok(EvalExpr::NewAnonymousClass { class, args }) } @@ -444,7 +452,19 @@ impl Parser { let mut methods = Vec::new(); let mut traits = Vec::new(); let mut trait_adaptations = Vec::new(); - while !self.consume(TokenKind::RBrace) { + loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + return Ok(ParsedClassBody { + source_end_line, + constants, + properties, + methods, + traits, + trait_adaptations, + }); + } if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } @@ -457,13 +477,6 @@ impl Parser { &mut trait_adaptations, )?; } - Ok(ParsedClassBody { - constants, - properties, - methods, - traits, - trait_adaptations, - }) } /// Parses class-level `abstract`, `final`, and `readonly` modifiers before `class`. @@ -893,6 +906,7 @@ impl Parser { is_abstract: bool, is_final: bool, ) -> Result<(EvalClassMethod, Vec), EvalParseError> { + let source_start_line = self.current_line(); self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -914,16 +928,18 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } let return_type = self.parse_optional_return_type()?; - let body = if is_abstract { + let (body, source_end_line) = if is_abstract { + let source_end_line = self.current_line(); self.expect_semicolon()?; - Vec::new() + (Vec::new(), source_end_line) } else { - let body = self.parse_block()?; - if promoted_assignments.is_empty() { + let (body, source_end_line) = self.parse_block_with_end_line()?; + let body = if promoted_assignments.is_empty() { body } else { promoted_assignments.into_iter().chain(body).collect() - } + }; + (body, source_end_line) }; Ok(( EvalClassMethod::with_visibility_and_modifiers( @@ -935,6 +951,7 @@ impl Parser { params, body, ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) .with_parameter_types(parameter_types) .with_parameter_attributes(parameter_attributes) .with_parameter_defaults(parameter_defaults) @@ -1084,22 +1101,24 @@ impl Parser { if !is_get && !is_set { return Err(EvalParseError::UnsupportedConstruct); } + let source_start_line = self.current_line(); self.advance(); let params = if is_set { vec![self.parse_property_set_hook_param()?] } else { Vec::new() }; - let body = match self.current() { + let (body, source_end_line) = match self.current() { TokenKind::Semicolon => return Err(EvalParseError::UnsupportedConstruct), TokenKind::FatArrow if is_get => { self.advance(); let expr = self.parse_expr()?; + let source_end_line = self.current_line(); self.expect_semicolon()?; - vec![EvalStmt::Return(Some(expr))] + (vec![EvalStmt::Return(Some(expr))], source_end_line) } TokenKind::FatArrow => return Err(EvalParseError::UnsupportedConstruct), - TokenKind::LBrace => self.parse_block()?, + TokenKind::LBrace => self.parse_block_with_end_line()?, _ => return Err(EvalParseError::UnexpectedToken), }; let method_name = if is_get { @@ -1117,7 +1136,8 @@ impl Parser { false, params, body, - ), + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)), )) } @@ -1146,6 +1166,7 @@ impl Parser { &mut self, attributes: Vec, ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1156,15 +1177,21 @@ impl Parser { let mut constants = Vec::new(); let mut properties = Vec::new(); let mut methods = Vec::new(); - while !self.consume(TokenKind::RBrace) { + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } self.parse_trait_member(&mut constants, &mut properties, &mut methods)?; - } + }; self.consume_semicolon(); Ok(vec![EvalStmt::TraitDecl( EvalTrait::with_constants(name, constants, properties, methods) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) .with_attributes(attributes), )]) } @@ -1234,6 +1261,7 @@ impl Parser { &mut self, attributes: Vec, ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1246,15 +1274,21 @@ impl Parser { let mut cases = Vec::new(); let mut constants = Vec::new(); let mut methods = Vec::new(); - while !self.consume(TokenKind::RBrace) { + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } self.parse_enum_member(&mut cases, &mut constants, &mut methods)?; - } + }; self.consume_semicolon(); Ok(vec![EvalStmt::EnumDecl( EvalEnum::with_members(name, backing_type, interfaces, cases, constants, methods) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) .with_attributes(attributes), )]) } @@ -1353,6 +1387,7 @@ impl Parser { &mut self, attributes: Vec, ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1364,17 +1399,23 @@ impl Parser { let mut constants = Vec::new(); let mut properties = Vec::new(); let mut methods = Vec::new(); - while !self.consume(TokenKind::RBrace) { + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } self.parse_interface_member(&mut constants, &mut properties, &mut methods)?; - } + }; self.consume_semicolon(); Ok(vec![EvalStmt::InterfaceDecl( EvalInterface::with_constants_and_properties( name, parents, constants, properties, methods, ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) .with_attributes(attributes), )]) } @@ -1492,6 +1533,7 @@ impl Parser { &mut self, is_static: bool, ) -> Result { + let source_start_line = self.current_line(); self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1513,8 +1555,10 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } let return_type = self.parse_optional_return_type()?; + let source_end_line = self.current_line(); self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) .with_static(is_static) .with_parameter_types(parameter_types) .with_parameter_attributes(parameter_attributes) @@ -1620,6 +1664,7 @@ impl Parser { &mut self, attributes: Vec, ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); self.advance(); let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1641,9 +1686,10 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } let return_type = self.parse_optional_return_type()?; - let body = self.parse_block()?; + let (body, source_end_line) = self.parse_block_with_end_line()?; Ok(vec![EvalStmt::FunctionDecl { name, + source_location: Some(EvalSourceLocation::new(source_start_line, source_end_line)), attributes, params, parameter_attributes, @@ -2558,6 +2604,14 @@ impl Parser { self.parse_nested_block_contents() } + /// Parses a brace-delimited statement block and returns the closing-brace line. + pub(super) fn parse_block_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + self.expect(TokenKind::LBrace)?; + self.parse_nested_block_contents_with_end_line() + } + /// Parses one nested statement where import declarations are not legal. pub(super) fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { let previous = std::mem::replace(&mut self.allow_use_imports, false); @@ -2574,6 +2628,16 @@ impl Parser { result } + /// Parses a nested block and returns the closing-brace line. + pub(super) fn parse_nested_block_contents_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_block_contents_with_end_line(); + self.allow_use_imports = previous; + result + } + /// Parses statements until the closing brace for the current block. pub(super) fn parse_block_contents(&mut self) -> Result, EvalParseError> { let mut statements = Vec::new(); @@ -2586,6 +2650,22 @@ impl Parser { self.expect(TokenKind::RBrace)?; Ok(statements) } + + /// Parses statements until the closing brace and returns that brace's line. + pub(super) fn parse_block_contents_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + statements.extend(self.parse_stmt()?); + } + let source_end_line = self.current_line(); + self.expect(TokenKind::RBrace)?; + Ok((statements, source_end_line)) + } } /// Returns whether an eval method parameter default can be materialized safely. diff --git a/crates/elephc-magician/src/parser/tests/namespaces.rs b/crates/elephc-magician/src/parser/tests/namespaces.rs index 67d4e4749c..03473ef6ad 100644 --- a/crates/elephc-magician/src/parser/tests/namespaces.rs +++ b/crates/elephc-magician/src/parser/tests/namespaces.rs @@ -18,6 +18,7 @@ fn parse_fragment_accepts_function_declaration_source() { program.statements(), &[EvalStmt::FunctionDecl { name: "dyn".to_string(), + source_location: Some(EvalSourceLocation::single_line(1)), attributes: Vec::new(), params: vec!["x".to_string()], parameter_attributes: vec![Vec::new()], @@ -48,6 +49,7 @@ return dyn();"#, &[ EvalStmt::FunctionDecl { name: "Eval\\Ns\\dyn".to_string(), + source_location: Some(EvalSourceLocation::single_line(2)), attributes: Vec::new(), params: Vec::new(), parameter_attributes: Vec::new(), @@ -240,6 +242,7 @@ function dyn() { return alias(); }"#, program.statements(), &[EvalStmt::FunctionDecl { name: "Eval\\UseNs\\dyn".to_string(), + source_location: Some(EvalSourceLocation::single_line(3)), attributes: Vec::new(), params: Vec::new(), parameter_attributes: Vec::new(), diff --git a/docs/php/eval.md b/docs/php/eval.md index d4835588ea..356a16e1d2 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -203,6 +203,10 @@ for those owners. `ReflectionClass`, `ReflectionFunction`, `ReflectionMethod`, eval does not retain docblock text. `ReflectionClass`, `ReflectionFunction`, and `ReflectionMethod` expose `getExtensionName()` and `getExtension()` and report `false` / `null` for eval-declared user symbols. +`ReflectionClass`, `ReflectionFunction`, and `ReflectionMethod` expose +`getFileName()`, `getStartLine()`, and `getEndLine()` for parser-backed eval +declarations. File names use PHP's synthetic eval file format from the current +eval call site, and line numbers are one-based inside the evaluated fragment. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c1e1f8fb1a..cbd255f2b3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7644,6 +7644,31 @@ echo $propertyAttrs[0]->getArguments()[0] . ":" . $propertyAttrs[0]->newInstance ); } +/// Verifies eval ReflectionClass/Method/Function expose source-location metadata. +#[test] +fn test_eval_reflection_source_locations() { + let out = compile_and_run( + r#"getFileName() === false ? "f" : "F"; echo ":"; +echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; +echo $method->getStartLine(); echo ":"; echo $method->getEndLine(); echo ":"; +echo $function->getStartLine(); echo ":"; echo $function->getEndLine();'); +"#, + ); + assert_eq!(out, "F:1:5:2:4:6:8"); +} + /// Verifies eval ReflectionAttribute exposes owner target and repetition metadata. #[test] fn test_eval_reflection_attribute_target_and_repetition() { From 6dd02eae255b52b8a57e25768bbaedff27a1e92d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 11:13:02 +0200 Subject: [PATCH 0622/1208] Expose eval reflection static variables --- .../src/interpreter/dynamic_functions.rs | 59 +++++-- .../src/interpreter/reflection.rs | 159 +++++++++++++++++- .../tests/builtins_class_metadata.rs | 32 ++++ .../tests/builtins_reflection_functions.rs | 28 +++ docs/php/eval.md | 4 + tests/codegen/eval.rs | 43 +++++ 6 files changed, 310 insertions(+), 15 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 9a33c99ad3..3625b018fc 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -989,36 +989,67 @@ pub(super) fn persist_static_locals( Ok(()) } +/// One source-order static local declaration and its initializer expression. +#[derive(Clone)] +pub(in crate::interpreter) struct EvalStaticVarInitializer { + pub name: String, + pub init: EvalExpr, +} + /// Returns the distinct static local names declared anywhere in an eval function body. pub(in crate::interpreter) fn static_var_names(body: &[EvalStmt]) -> Vec { - let mut names = std::collections::HashSet::new(); - collect_static_var_names(body, &mut names); - names.into_iter().collect() + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + visit_static_var_declarations(body, &mut seen, &mut |name, _| { + names.push(name.to_string()); + }); + names +} + +/// Returns static local declarations and initializers in first-seen source order. +pub(in crate::interpreter) fn static_var_initializers( + body: &[EvalStmt], +) -> Vec { + let mut vars = Vec::new(); + let mut seen = std::collections::HashSet::new(); + visit_static_var_declarations(body, &mut seen, &mut |name, init| { + vars.push(EvalStaticVarInitializer { + name: name.to_string(), + init: init.clone(), + }); + }); + vars } -/// Recursively collects static local declaration names from eval statements. -fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::HashSet) { +/// Visits distinct static local declarations in first-seen source order. +fn visit_static_var_declarations( + body: &[EvalStmt], + seen: &mut std::collections::HashSet, + visitor: &mut impl FnMut(&str, &EvalExpr), +) { for stmt in body { match stmt { - EvalStmt::StaticVar { name, .. } => { - names.insert(name.clone()); + EvalStmt::StaticVar { name, init } => { + if seen.insert(name.clone()) { + visitor(name, init); + } } EvalStmt::DoWhile { body, .. } | EvalStmt::Foreach { body, .. } | EvalStmt::For { body, .. } - | EvalStmt::While { body, .. } => collect_static_var_names(body, names), + | EvalStmt::While { body, .. } => visit_static_var_declarations(body, seen, visitor), EvalStmt::FunctionDecl { .. } => {} EvalStmt::If { then_branch, else_branch, .. } => { - collect_static_var_names(then_branch, names); - collect_static_var_names(else_branch, names); + visit_static_var_declarations(then_branch, seen, visitor); + visit_static_var_declarations(else_branch, seen, visitor); } EvalStmt::Switch { cases, .. } => { for case in cases { - collect_static_var_names(&case.body, names); + visit_static_var_declarations(&case.body, seen, visitor); } } EvalStmt::Try { @@ -1026,11 +1057,11 @@ fn collect_static_var_names(body: &[EvalStmt], names: &mut std::collections::Has catches, finally_body, } => { - collect_static_var_names(body, names); + visit_static_var_declarations(body, seen, visitor); for catch in catches { - collect_static_var_names(&catch.body, names); + visit_static_var_declarations(&catch.body, seen, visitor); } - collect_static_var_names(finally_body, names); + visit_static_var_declarations(finally_body, seen, visitor); } EvalStmt::ArrayAppendVar { .. } | EvalStmt::ArraySetVar { .. } diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 51fcfcef8b..7a7d9d7f39 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -184,12 +184,17 @@ struct EvalReflectionNamedTypeMetadata { enum EvalReflectionFunctionMethodTarget { Function { name: String, + static_key: Option, + static_variables: Vec, source_location: Option, is_variadic: bool, return_type_metadata: Option, }, Method { + declaring_class: Option, name: String, + static_key: Option, + static_variables: Vec, source_location: Option, is_variadic: bool, return_type_metadata: Option, @@ -865,7 +870,7 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( identity: u64, method_name: &str, evaluated_args: Vec, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { @@ -941,6 +946,11 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( eval_reflection_bind_no_args(evaluated_args)?; values.null().map(Some) } + "getstaticvariables" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_method_static_variables_result(&target, context, values) + .map(Some) + } "getclosureusedvariables" => { eval_reflection_bind_no_args(evaluated_args)?; values.array_new(0).map(Some) @@ -6651,8 +6661,14 @@ fn eval_reflection_function_method_target( let return_type_metadata = function .and_then(EvalFunction::return_type) .and_then(eval_reflection_parameter_type_metadata); + let static_key = function.map(|function| function.name().to_string()); + let static_variables = function + .map(|function| static_var_initializers(function.body())) + .unwrap_or_default(); return Ok(Some(EvalReflectionFunctionMethodTarget::Function { name: name.to_string(), + static_key, + static_variables, source_location, is_variadic, return_type_metadata, @@ -6681,14 +6697,155 @@ fn eval_reflection_function_method_target( }); let source_location = method_metadata.as_ref().and_then(|method| method.source_location); let return_type_metadata = method_metadata.and_then(|method| method.return_type_metadata); + let static_method = eval_reflection_eval_method_static_target(declaring_class, method_name, context); + let declaring_class = static_method + .as_ref() + .map(|(declaring_class, _)| declaring_class.clone()); + let static_key = static_method + .as_ref() + .map(|(declaring_class, method)| eval_method_static_local_key(declaring_class, method.name())); + let static_variables = static_method + .as_ref() + .map(|(_, method)| static_var_initializers(method.body())) + .unwrap_or_default(); Ok(Some(EvalReflectionFunctionMethodTarget::Method { + declaring_class, name: method_name.to_string(), + static_key, + static_variables, source_location, is_variadic, return_type_metadata, })) } +/// Returns an eval method body that can contribute ReflectionMethod static locals. +fn eval_reflection_eval_method_static_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + return context.class_method(declaring_class, method_name); + } + let trait_decl = context.trait_decl(declaring_class)?; + trait_decl + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| (trait_decl.name().to_string(), method.clone())) +} + +/// Builds the static-local storage key shared by method execution and reflection. +fn eval_method_static_local_key(class_name: &str, method_name: &str) -> String { + format!("{}::{}", class_name.trim_start_matches('\\'), method_name) +} + +/// Builds the associative `getStaticVariables()` result for eval-backed reflection. +fn eval_reflection_function_method_static_variables_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (Some(static_key), static_variables, declaring_class) = + eval_reflection_function_method_static_target(target) + else { + return values.array_new(0); + }; + let mut result = values.assoc_new(static_variables.len())?; + for variable in static_variables { + let key = values.string(&variable.name)?; + let value = eval_reflection_static_local_value( + static_key, + variable, + declaring_class, + context, + values, + )?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns static-local storage details retained for a reflected eval function or method. +fn eval_reflection_function_method_static_target( + target: &EvalReflectionFunctionMethodTarget, +) -> ( + Option<&str>, + &[EvalStaticVarInitializer], + Option<&str>, +) { + match target { + EvalReflectionFunctionMethodTarget::Function { + static_key, + static_variables, + .. + } => (static_key.as_deref(), static_variables, None), + EvalReflectionFunctionMethodTarget::Method { + declaring_class, + static_key, + static_variables, + .. + } => ( + static_key.as_deref(), + static_variables, + declaring_class.as_deref(), + ), + } +} + +/// Returns the retained current static value or initializes it for reflection. +fn eval_reflection_static_local_value( + static_key: &str, + variable: &EvalStaticVarInitializer, + declaring_class: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = context.static_local(static_key, &variable.name) { + return values.retain(value); + } + let value = eval_reflection_static_local_initializer_value( + static_key, + &variable.init, + declaring_class, + context, + values, + )?; + if let Some(replaced) = + context.set_static_local(static_key.to_string(), variable.name.clone(), value) + { + values.release(replaced)?; + } + values.retain(value) +} + +/// Evaluates a static-local initializer with PHP magic class/function context. +fn eval_reflection_static_local_initializer_value( + static_key: &str, + init: &EvalExpr, + declaring_class: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(declaring_class) = declaring_class { + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + } + context.push_function(static_key.to_string()); + let mut scope = ElephcEvalScope::new(); + let result = eval_expr(init, context, &mut scope, values); + for cell in scope.drain_owned_cells() { + values.release(cell)?; + } + context.pop_function(); + if declaring_class.is_some() { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + result +} + /// Validates that a synthetic reflection metadata call received no arguments. fn eval_reflection_bind_no_args(evaluated_args: Vec) -> Result<(), EvalStatus> { let _ = bind_evaluated_function_args(&[], evaluated_args)?; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index fdb90a5eb4..d6eafee8d0 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -437,6 +437,38 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod exposes eval static locals using the declaring class key. +#[test] +fn execute_program_reflection_method_reports_static_variables() { + let program = parse_fragment( + br#"class EvalReflectMethodStaticBase { + public function tick() { + static $count = 3; + static $label = "method"; + $count = $count + 1; + return $count; + } +} +class EvalReflectMethodStaticChild extends EvalReflectMethodStaticBase {} +$object = new EvalReflectMethodStaticChild(); +$ref = new ReflectionMethod("EvalReflectMethodStaticChild", "tick"); +$before = $ref->getStaticVariables(); +echo $before["count"]; echo ":"; echo $before["label"]; echo ":"; +echo $ref->invoke($object); echo ":"; +$after = $ref->getStaticVariables(); +echo $after["count"]; echo ":"; echo $after["label"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:method:4:4:method"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod exposes eval parent and interface prototypes. #[test] fn execute_program_reflection_method_reports_eval_prototypes() { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs index 0cb5cfd6f3..39f6df1c40 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -211,6 +211,34 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionFunction exposes eval static locals before and after execution. +#[test] +fn execute_program_reflection_function_reports_static_variables() { + let program = parse_fragment( + br#"function eval_reflect_static_vars() { + static $count = 1; + static $label = "fn"; + $count = $count + 1; + return $count; +} +$ref = new ReflectionFunction("eval_reflect_static_vars"); +$before = $ref->getStaticVariables(); +echo $before["count"]; echo ":"; echo $before["label"]; echo ":"; +echo eval_reflect_static_vars(); echo ":"; +$after = $ref->getStaticVariables(); +echo $after["count"]; echo ":"; echo $after["label"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:fn:2:2:fn"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval-declared functions bind named, default, by-reference, and variadic arguments. #[test] fn execute_program_calls_eval_function_with_rich_argument_binding() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 356a16e1d2..98b7a2cc14 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -228,6 +228,10 @@ and `getReturnType()` expose retained eval return type metadata for supported named, nullable, union, and intersection declarations, including `void` and `never` as builtin non-nullable named types. `ReflectionFunction::isDisabled()` reports `false` for eval-visible functions. +`ReflectionFunction::getStaticVariables()` and +`ReflectionMethod::getStaticVariables()` expose eval-declared static local +variables, materializing initializer values before the first invocation and +returning updated values after reflected or direct calls. `ReflectionFunction::getClosureUsedVariables()` and `ReflectionMethod::getClosureUsedVariables()` report empty arrays for supported non-closure reflectors. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cbd255f2b3..7adc8b16a9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11193,6 +11193,49 @@ echo count($method->getClosureUsedVariables());'); ); } +/// Verifies eval ReflectionFunction/Method expose static local variables through the bridge. +#[test] +fn test_eval_reflection_function_and_method_static_variables() { + let out = compile_and_run_capture( + r#"getStaticVariables(); +echo $beforeFn["count"] . ":" . $beforeFn["label"] . ":"; +echo eval_reflect_static_fn() . ":"; +$afterFn = $fn->getStaticVariables(); +echo $afterFn["count"] . ":" . $afterFn["label"] . "|"; +$object = new EvalReflectStaticMethodChild(); +$method = new ReflectionMethod("EvalReflectStaticMethodChild", "tick"); +$beforeMethod = $method->getStaticVariables(); +echo $beforeMethod["count"] . ":" . $beforeMethod["label"] . ":"; +echo $method->invoke($object) . ":"; +$afterMethod = $method->getStaticVariables(); +echo $afterMethod["count"] . ":" . $afterMethod["label"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1:fn:2:2:fn|3:method:4:4:method"); +} + /// Verifies eval ReflectionMethod hasPrototype/getPrototype follow PHP inheritance rules. #[test] fn test_eval_reflection_method_reports_eval_prototypes() { From 0b9f4a3afbad718a78e8fc89afaa81daf8231754 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 11:19:46 +0200 Subject: [PATCH 0623/1208] Fill eval reflection function metadata gaps --- .../src/interpreter/reflection.rs | 31 +++++++++++++++++++ .../tests/builtins_class_metadata.rs | 11 +++++-- .../tests/builtins_reflection_functions.rs | 7 ++++- docs/php/eval.md | 6 +++- tests/codegen/eval.rs | 16 ++++++++-- 5 files changed, 65 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 7a7d9d7f39..d511b53a89 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -188,6 +188,7 @@ enum EvalReflectionFunctionMethodTarget { static_variables: Vec, source_location: Option, is_variadic: bool, + is_static: bool, return_type_metadata: Option, }, Method { @@ -197,6 +198,7 @@ enum EvalReflectionFunctionMethodTarget { static_variables: Vec, source_location: Option, is_variadic: bool, + is_static: bool, return_type_metadata: Option, }, } @@ -911,6 +913,12 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( | "returnsreference" | "isgenerator" | "hastentativereturntype" => eval_reflection_false_metadata_result(evaluated_args, values), + "isanonymous" => match target { + EvalReflectionFunctionMethodTarget::Function { .. } => { + eval_reflection_false_metadata_result(evaluated_args, values) + } + EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), + }, "hasreturntype" => { eval_reflection_bind_no_args(evaluated_args)?; values @@ -927,6 +935,12 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( .bool_value(eval_reflection_function_method_is_variadic(&target)) .map(Some) } + "isstatic" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_static(&target)) + .map(Some) + } "isdisabled" => match target { EvalReflectionFunctionMethodTarget::Function { .. } => { eval_reflection_false_metadata_result(evaluated_args, values) @@ -955,6 +969,10 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( eval_reflection_bind_no_args(evaluated_args)?; values.array_new(0).map(Some) } + "getclosurethis" | "getclosurescopeclass" | "getclosurecalledclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.null().map(Some) + } _ => Ok(None), } } @@ -6671,6 +6689,7 @@ fn eval_reflection_function_method_target( static_variables, source_location, is_variadic, + is_static: false, return_type_metadata, })); } @@ -6695,6 +6714,9 @@ fn eval_reflection_function_method_target( .iter() .any(|parameter| parameter.is_variadic) }); + let is_static = method_metadata + .as_ref() + .is_some_and(|method| method.is_static); let source_location = method_metadata.as_ref().and_then(|method| method.source_location); let return_type_metadata = method_metadata.and_then(|method| method.return_type_metadata); let static_method = eval_reflection_eval_method_static_target(declaring_class, method_name, context); @@ -6715,6 +6737,7 @@ fn eval_reflection_function_method_target( static_variables, source_location, is_variadic, + is_static, return_type_metadata, })) } @@ -6929,6 +6952,14 @@ fn eval_reflection_function_method_is_variadic( } } +/// Returns whether the reflected function-like target is static. +fn eval_reflection_function_method_is_static(target: &EvalReflectionFunctionMethodTarget) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_static, .. } + | EvalReflectionFunctionMethodTarget::Method { is_static, .. } => *is_static, + } +} + /// Returns the retained return type metadata for a reflected function or method. fn eval_reflection_function_method_return_type( target: &EvalReflectionFunctionMethodTarget, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index d6eafee8d0..83cbb5e55c 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -408,6 +408,7 @@ fn execute_program_reflection_method_reports_name_and_origin_predicates() { br#"namespace EvalReflectMethodNs; class Target { public function run(...$items) {} + public static function stat() {} } $ref = new \ReflectionMethod(Target::class, "run"); echo $ref->getShortName(); echo ":"; @@ -417,6 +418,7 @@ echo $ref->isInternal() ? "I" : "i"; echo $ref->isUserDefined() ? "U" : "u"; echo ":"; echo $ref->isClosure() ? "C" : "c"; echo ":"; echo $ref->isDeprecated() ? "D" : "d"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; echo $ref->returnsReference() ? "R" : "r"; echo ":"; echo $ref->hasReturnType() ? "T" : "t"; echo ":"; echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; @@ -424,7 +426,12 @@ echo $ref->isGenerator() ? "G" : "g"; echo ":"; echo $ref->isVariadic() ? "V" : "v"; echo ":"; echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; echo $ref->getTentativeReturnType() === null ? "Q" : "q"; echo ":"; -echo count($ref->getClosureUsedVariables()); +echo count($ref->getClosureUsedVariables()); echo ":"; +echo $ref->getClosureThis() === null ? "T" : "t"; echo ":"; +echo $ref->getClosureScopeClass() === null ? "S" : "s"; echo ":"; +echo $ref->getClosureCalledClass() === null ? "L" : "l"; echo ":"; +$static = new \ReflectionMethod(Target::class, "stat"); +echo $static->isStatic() ? "S" : "s"; return true;"#, ) .expect("parse eval fragment"); @@ -433,7 +440,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "run::N:iU:c:d:r:t:N:g:V:h:Q:0"); + assert_eq!(values.output, "run::N:iU:c:d:s:r:t:N:g:V:h:Q:0:T:S:L:S"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs index 39f6df1c40..382d5a078c 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -160,8 +160,10 @@ echo $ref->getNamespaceName(); echo ":"; echo $ref->inNamespace() ? "Y" : "N"; echo ":"; echo $ref->isInternal() ? "I" : "i"; echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->isAnonymous() ? "A" : "a"; echo ":"; echo $ref->isClosure() ? "C" : "c"; echo ":"; echo $ref->isDeprecated() ? "D" : "d"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; echo $ref->returnsReference() ? "R" : "r"; echo ":"; echo $ref->hasReturnType() ? "T" : "t"; echo ":"; echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; @@ -169,6 +171,9 @@ echo $ref->isGenerator() ? "G" : "g"; echo ":"; echo $ref->isVariadic() ? "V" : "v"; echo ":"; echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; echo $ref->getTentativeReturnType() === null ? "Q" : "q"; echo ":"; +echo $ref->getClosureThis() === null ? "T" : "t"; echo ":"; +echo $ref->getClosureScopeClass() === null ? "S" : "s"; echo ":"; +echo $ref->getClosureCalledClass() === null ? "L" : "l"; echo ":"; echo $ref->isDisabled() ? "X" : "x"; return true;"#, ) @@ -180,7 +185,7 @@ return true;"#, assert_eq!( values.output, - "sample:EvalReflectFnNs:Y:iU:c:d:r:t:N:g:V:h:Q:x" + "sample:EvalReflectFnNs:Y:iU:a:c:d:s:r:t:N:g:V:h:Q:T:S:L:x" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 98b7a2cc14..30531f9b58 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -222,11 +222,15 @@ while `ReflectionMethod::getNamespaceName()` reports an empty string and `inNamespace()` reports `false`, matching PHP's method reflection behavior. `ReflectionFunction` and `ReflectionMethod` report eval user-symbol defaults through `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, -`returnsReference()`, `isGenerator()`, `isVariadic()`, +`returnsReference()`, `isGenerator()`, `isVariadic()`, `isStatic()`, `hasTentativeReturnType()`, and `getTentativeReturnType()`. `hasReturnType()` and `getReturnType()` expose retained eval return type metadata for supported named, nullable, union, and intersection declarations, including `void` and `never` as builtin non-nullable named types. +`ReflectionFunction::isAnonymous()` reports `false` for eval-declared named +functions, and `getClosureThis()`, `getClosureScopeClass()`, and +`getClosureCalledClass()` report `null` for eval-visible non-closure function +and method reflectors. `ReflectionFunction::isDisabled()` reports `false` for eval-visible functions. `ReflectionFunction::getStaticVariables()` and `ReflectionMethod::getStaticVariables()` expose eval-declared static local diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7adc8b16a9..e6cd5a7fe3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11146,6 +11146,7 @@ eval('namespace EvalReflectNameNs; function sample(...$items) {} class Target { public function run(...$items) {} + public static function stat() {} } $fn = new \ReflectionFunction("EvalReflectNameNs\\\\sample"); $method = new \ReflectionMethod(Target::class, "run"); @@ -11154,8 +11155,10 @@ echo $fn->getNamespaceName() . ":"; echo ($fn->inNamespace() ? "Y" : "N") . ":"; echo ($fn->isInternal() ? "I" : "i"); echo ($fn->isUserDefined() ? "U" : "u") . ":"; +echo ($fn->isAnonymous() ? "A" : "a") . ":"; echo ($fn->isClosure() ? "C" : "c") . ":"; echo ($fn->isDeprecated() ? "D" : "d") . ":"; +echo ($fn->isStatic() ? "S" : "s") . ":"; echo ($fn->returnsReference() ? "R" : "r") . ":"; echo ($fn->hasReturnType() ? "T" : "t") . ":"; echo ($fn->getReturnType() === null ? "N" : "n") . ":"; @@ -11164,6 +11167,9 @@ echo ($fn->isVariadic() ? "V" : "v") . ":"; echo ($fn->hasTentativeReturnType() ? "H" : "h") . ":"; echo ($fn->getTentativeReturnType() === null ? "Q" : "q") . ":"; echo count($fn->getClosureUsedVariables()) . ":"; +echo ($fn->getClosureThis() === null ? "T" : "t") . ":"; +echo ($fn->getClosureScopeClass() === null ? "S" : "s") . ":"; +echo ($fn->getClosureCalledClass() === null ? "L" : "l") . ":"; echo ($fn->isDisabled() ? "X" : "x") . "|"; echo $method->getShortName() . ":"; echo $method->getNamespaceName() . ":"; @@ -11172,6 +11178,7 @@ echo ($method->isInternal() ? "I" : "i"); echo ($method->isUserDefined() ? "U" : "u") . ":"; echo ($method->isClosure() ? "C" : "c") . ":"; echo ($method->isDeprecated() ? "D" : "d") . ":"; +echo ($method->isStatic() ? "S" : "s") . ":"; echo ($method->returnsReference() ? "R" : "r") . ":"; echo ($method->hasReturnType() ? "T" : "t") . ":"; echo ($method->getReturnType() === null ? "N" : "n") . ":"; @@ -11179,7 +11186,12 @@ echo ($method->isGenerator() ? "G" : "g") . ":"; echo ($method->isVariadic() ? "V" : "v") . ":"; echo ($method->hasTentativeReturnType() ? "H" : "h") . ":"; echo ($method->getTentativeReturnType() === null ? "Q" : "q") . ":"; -echo count($method->getClosureUsedVariables());'); +echo count($method->getClosureUsedVariables()) . ":"; +echo ($method->getClosureThis() === null ? "T" : "t") . ":"; +echo ($method->getClosureScopeClass() === null ? "S" : "s") . ":"; +echo ($method->getClosureCalledClass() === null ? "L" : "l") . ":"; +$static = new \ReflectionMethod(Target::class, "stat"); +echo ($static->isStatic() ? "S" : "s");'); "#, ); assert!( @@ -11189,7 +11201,7 @@ echo count($method->getClosureUsedVariables());'); ); assert_eq!( out.stdout, - "sample:EvalReflectNameNs:Y:iU:c:d:r:t:N:g:V:h:Q:0:x|run::N:iU:c:d:r:t:N:g:V:h:Q:0" + "sample:EvalReflectNameNs:Y:iU:a:c:d:s:r:t:N:g:V:h:Q:0:T:S:L:x|run::N:iU:c:d:s:r:t:N:g:V:h:Q:0:T:S:L:S" ); } From e7f800b0a0b85b02a1549c77fb4d504dd0c10770 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 11:28:14 +0200 Subject: [PATCH 0624/1208] Expose eval reflection constant type defaults --- .../tests/builtins_class_metadata.rs | 38 +++++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 4 ++ docs/php/eval.md | 3 ++ src/types/checker/builtin_types/reflection.rs | 10 +++++ tests/codegen/eval.rs | 37 ++++++++++++++++++ 5 files changed, 92 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 83cbb5e55c..b48ef7027c 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -2897,6 +2897,44 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClassConstant and enum case metadata expose PHP's untyped defaults. +#[test] +fn execute_program_reflects_eval_constant_type_metadata_defaults() { + let program = parse_fragment( + br#"class EvalConstTypeTarget { + public const ANSWER = 42; +} +enum EvalConstTypeEnum: string { + case Ready = "ready"; +} +$constant = new ReflectionClassConstant("EvalConstTypeTarget", "ANSWER"); +echo $constant->isDeprecated() ? "D" : "d"; echo ":"; +echo $constant->hasType() ? "T" : "t"; echo ":"; +echo $constant->getType() === null ? "N" : "n"; echo ":"; +$case = new ReflectionClassConstant("EvalConstTypeEnum", "Ready"); +echo $case->isDeprecated() ? "D" : "d"; echo ":"; +echo $case->hasType() ? "T" : "t"; echo ":"; +echo $case->getType() === null ? "N" : "n"; echo ":"; +$unit = new ReflectionEnumUnitCase("EvalConstTypeEnum", "Ready"); +echo $unit->isDeprecated() ? "D" : "d"; echo ":"; +echo $unit->hasType() ? "T" : "t"; echo ":"; +echo $unit->getType() === null ? "N" : "n"; echo ":"; +$backed = new ReflectionEnumBackedCase("EvalConstTypeEnum", "Ready"); +echo $backed->isDeprecated() ? "D" : "d"; echo ":"; +echo $backed->hasType() ? "T" : "t"; echo ":"; +echo $backed->getType() === null ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "d:t:N:d:t:N:d:t:N:d:t:N"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index c922dde73c..17422ef38c 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -234,6 +234,10 @@ impl FakeOps { Self::object_property(&properties, "__is_user_defined") .map_or_else(|| self.bool_value(false), Ok) } + (FakeValue::Object(properties), "isdeprecated") if args.is_empty() => { + Self::object_property(&properties, "__is_deprecated") + .map_or_else(|| self.bool_value(false), Ok) + } (FakeValue::Object(properties), "getparentclass") if args.is_empty() => { Self::object_property(&properties, "__parent_class") .map_or_else(|| self.bool_value(false), Ok) diff --git a/docs/php/eval.md b/docs/php/eval.md index 30531f9b58..a7c592ccd9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -494,6 +494,9 @@ case object, while `ReflectionEnumUnitCase::getValue()` and `ReflectionEnumBackedCase::getBackingValue()` returns the scalar backing value, and `getDeclaringClass()` returns the declaring class or enum as a `ReflectionClass`. `ReflectionClassConstant::isEnumCase()` reports enum cases. +`ReflectionClassConstant`, `ReflectionEnumUnitCase`, and +`ReflectionEnumBackedCase` expose `isDeprecated()`, `hasType()`, and +`getType()` with PHP's current untyped defaults: `false`, `false`, and `null`. `ReflectionClassConstant::isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()` report visibility/finality metadata with PHP's `ReflectionClassConstant::IS_*` bitmasks; enum cases report public, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 079b426ae3..138ee2c130 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3635,6 +3635,16 @@ fn builtin_reflection_owner_class( "__declaring_class", )); } + if matches!( + name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + methods.push(builtin_reflection_constant_false_bool_method( + "isDeprecated", + )); + methods.push(builtin_reflection_constant_false_bool_method("hasType")); + methods.push(builtin_reflection_constant_null_mixed_method("getType")); + } if matches!(name, "ReflectionFunction" | "ReflectionMethod") { properties.push(builtin_property( "__parameters", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e6cd5a7fe3..7cedc759be 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11989,6 +11989,43 @@ echo $backedAttrs[0]->newInstance()->label();'); ); } +/// Verifies eval ReflectionClassConstant/EnumCase expose PHP's untyped metadata defaults. +#[test] +fn test_eval_reflection_constant_type_metadata_defaults() { + let out = compile_and_run_capture( + r#"isDeprecated() ? "D" : "d"; echo ":"; +echo $constant->hasType() ? "T" : "t"; echo ":"; +echo $constant->getType() === null ? "N" : "n"; echo ":"; +$case = new ReflectionClassConstant("EvalConstTypeEnum", "Ready"); +echo $case->isDeprecated() ? "D" : "d"; echo ":"; +echo $case->hasType() ? "T" : "t"; echo ":"; +echo $case->getType() === null ? "N" : "n"; echo ":"; +$unit = new ReflectionEnumUnitCase("EvalConstTypeEnum", "Ready"); +echo $unit->isDeprecated() ? "D" : "d"; echo ":"; +echo $unit->hasType() ? "T" : "t"; echo ":"; +echo $unit->getType() === null ? "N" : "n"; echo ":"; +$backed = new ReflectionEnumBackedCase("EvalConstTypeEnum", "Ready"); +echo $backed->isDeprecated() ? "D" : "d"; echo ":"; +echo $backed->hasType() ? "T" : "t"; echo ":"; +echo $backed->getType() === null ? "N" : "n";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "d:t:N:d:t:N:d:t:N:d:t:N"); +} + /// Verifies eval ReflectionClassConstant exposes visibility predicates and modifiers. #[test] fn test_eval_reflection_class_constant_visibility_and_modifiers() { From f870d5e74eb38785efe70953e4eabc1da4fd6f71 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 11:41:00 +0200 Subject: [PATCH 0625/1208] Add eval reflection constant string output --- crates/elephc-magician/src/context.rs | 28 +++++ .../src/interpreter/reflection.rs | 118 ++++++++++++++++++ .../src/interpreter/statements.rs | 9 ++ .../tests/builtins_class_metadata.rs | 38 ++++++ docs/php/eval.md | 2 + src/types/checker/builtin_types/reflection.rs | 10 ++ tests/codegen/eval.rs | 37 ++++++ 7 files changed, 242 insertions(+) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 0f483bbd62..d09c913d27 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -383,6 +383,7 @@ pub struct ElephcEvalContext { eval_reflection_methods: HashMap, eval_reflection_properties: HashMap, eval_dynamic_reflection_properties: HashSet, + eval_reflection_class_constants: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, class_stack: Vec, @@ -442,6 +443,7 @@ impl ElephcEvalContext { eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), eval_dynamic_reflection_properties: HashSet::new(), + eval_reflection_class_constants: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -502,6 +504,7 @@ impl ElephcEvalContext { eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), eval_dynamic_reflection_properties: HashSet::new(), + eval_reflection_class_constants: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -1132,6 +1135,31 @@ impl ElephcEvalContext { self.eval_dynamic_reflection_properties.contains(&identity) } + /// Records reflected class constant or enum case metadata for one synthetic object. + pub fn register_eval_reflection_class_constant( + &mut self, + identity: u64, + declaring_class: &str, + constant_name: &str, + owner_kind: u64, + ) { + self.eval_reflection_class_constants.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + constant_name.to_string(), + owner_kind, + ), + ); + } + + /// Returns the declaring class, name, and reflection owner kind for a synthetic constant. + pub fn eval_reflection_class_constant(&self, identity: u64) -> Option<(&str, &str, u64)> { + self.eval_reflection_class_constants + .get(&identity) + .map(|(class, constant, owner_kind)| (class.as_str(), constant.as_str(), *owner_kind)) + } + /// Returns eval-declared class metadata from parent to child for construction. pub fn class_chain(&self, name: &str) -> Vec { let mut chain = Vec::new(); diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index d511b53a89..07b61cd3e0 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1625,6 +1625,41 @@ pub(in crate::interpreter) fn eval_reflection_property_to_string_result( values.string(&text).map(Some) } +/// Handles eval-backed `ReflectionClassConstant` and enum-case `__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_class_constant_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some((declaring_class, constant_name, owner_kind)) = + context + .eval_reflection_class_constant(identity) + .map(|(declaring_class, constant_name, owner_kind)| { + ( + declaring_class.to_string(), + constant_name.to_string(), + owner_kind, + ) + }) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let text = eval_reflection_class_constant_to_string( + &declaring_class, + &constant_name, + owner_kind, + context, + values, + )?; + values.string(&text).map(Some) +} + /// Handles `ReflectionProperty::getRawValue()` and raw write calls. pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( identity: u64, @@ -3424,6 +3459,21 @@ fn eval_reflection_owner_object_with_members( ); } } + } else if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + if let Some(declaring_class) = parent_class_name { + let identity = values.object_identity(object)?; + context.register_eval_reflection_class_constant( + identity, + declaring_class, + reflected_name, + owner_kind, + ); + } } values.release(attrs)?; values.release(interface_names_array)?; @@ -4689,6 +4739,74 @@ fn eval_reflection_property_to_string( format!("Property [ {}{} ]", parts.join(" "), default) } +/// Formats one class constant or enum case like PHP's `ReflectionClassConstant::__toString()`. +fn eval_reflection_class_constant_to_string( + declaring_class: &str, + constant_name: &str, + owner_kind: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, _, visibility, is_final, is_enum_case) = + eval_reflection_class_constant_metadata(declaring_class, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let value = eval_reflection_constant_value(declaring_class, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let mut parts = Vec::new(); + if is_final { + parts.push(String::from("final")); + } + parts.push(eval_reflection_visibility_label(visibility).to_string()); + parts.push(eval_reflection_class_constant_type_label( + declaring_class, + value, + is_enum_case + || matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ), + values, + )?); + parts.push(constant_name.to_string()); + let value = eval_reflection_class_constant_display_value(value, values)?; + Ok(format!("Constant [ {} ] {{ {} }}\n", parts.join(" "), value)) +} + +/// Returns the type label PHP prints for a reflected class constant value. +fn eval_reflection_class_constant_type_label( + declaring_class: &str, + value: RuntimeCellHandle, + is_enum_case: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if is_enum_case { + return Ok(declaring_class.trim_start_matches('\\').to_string()); + } + Ok(match values.type_tag(value)? { + EVAL_TAG_INT => String::from("int"), + EVAL_TAG_STRING => String::from("string"), + EVAL_TAG_FLOAT => String::from("float"), + EVAL_TAG_BOOL => String::from("bool"), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("array"), + EVAL_TAG_OBJECT => String::from("object"), + EVAL_TAG_NULL => String::from("null"), + _ => String::from("mixed"), + }) +} + +/// Returns the value display PHP prints inside ReflectionClassConstant braces. +fn eval_reflection_class_constant_display_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(match values.type_tag(value)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("Array"), + EVAL_TAG_OBJECT => String::from("Object"), + EVAL_TAG_NULL => String::new(), + _ => String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(), + }) +} + /// Returns PHP's lowercase label for one reflected visibility. fn eval_reflection_visibility_label(visibility: EvalVisibility) -> &'static str { match visibility { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 5f9a54f054..41fdc403da 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3678,6 +3678,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_constant_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_get_value_result( identity, method_name, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index b48ef7027c..c053cc7250 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -2935,6 +2935,44 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClassConstant and enum case objects stringify retained metadata. +#[test] +fn execute_program_reflects_eval_constant_to_string() { + let program = parse_fragment( + br#"class EvalConstStringTarget { + public const ANSWER = 42; + final protected const LIMIT = 7; + private const FLAG = true; + public const LABEL = "ok"; + public const NOTHING = null; +} +enum EvalConstStringEnum: string { + case Ready = "ready"; +} +foreach (["ANSWER", "LIMIT", "FLAG", "LABEL", "NOTHING"] as $name) { + echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringTarget", $name))->__toString()); + echo "|"; +} +echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumUnitCase("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumBackedCase("EvalConstStringEnum", "Ready"))->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Constant [ public int ANSWER ] { 42 }\\n|Constant [ final protected int LIMIT ] { 7 }\\n|Constant [ private bool FLAG ] { 1 }\\n|Constant [ public string LABEL ] { ok }\\n|Constant [ public null NOTHING ] { }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index a7c592ccd9..c16dbe9f14 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -497,6 +497,8 @@ and `getDeclaringClass()` returns the declaring class or enum as a `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` expose `isDeprecated()`, `hasType()`, and `getType()` with PHP's current untyped defaults: `false`, `false`, and `null`. +Their `__toString()` methods format retained constant and enum-case metadata in +PHP's `Constant [ ... ] { ... }` shape. `ReflectionClassConstant::isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()` report visibility/finality metadata with PHP's `ReflectionClassConstant::IS_*` bitmasks; enum cases report public, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 138ee2c130..8112677ca1 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3639,6 +3639,16 @@ fn builtin_reflection_owner_class( name, "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" ) { + properties.push(builtin_property( + "__string", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); + methods.push(builtin_reflection_class_string_method( + "__toString", + "__string", + )); methods.push(builtin_reflection_constant_false_bool_method( "isDeprecated", )); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7cedc759be..2501814486 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12026,6 +12026,43 @@ echo $backed->getType() === null ? "N" : "n";'); assert_eq!(out.stdout, "d:t:N:d:t:N:d:t:N:d:t:N"); } +/// Verifies eval ReflectionClassConstant/EnumCase stringify retained metadata. +#[test] +fn test_eval_reflection_constant_to_string() { + let out = compile_and_run_capture( + r#"__toString()); + echo "|"; +} +echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumUnitCase("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumBackedCase("EvalConstStringEnum", "Ready"))->__toString());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Constant [ public int ANSWER ] { 42 }\n|Constant [ final protected int LIMIT ] { 7 }\n|Constant [ private bool FLAG ] { 1 }\n|Constant [ public string LABEL ] { ok }\n|Constant [ public null NOTHING ] { }\n|Constant [ public EvalConstStringEnum Ready ] { Object }\n|Constant [ public EvalConstStringEnum Ready ] { Object }\n|Constant [ public EvalConstStringEnum Ready ] { Object }\n" + ); +} + /// Verifies eval ReflectionClassConstant exposes visibility predicates and modifiers. #[test] fn test_eval_reflection_class_constant_visibility_and_modifiers() { From 2452563de035ef272d6f28392693666a6c284971 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 11:51:55 +0200 Subject: [PATCH 0626/1208] Expose enum case reflection modifiers --- .../src/interpreter/reflection.rs | 7 ++- .../src/interpreter/statements.rs | 5 +- .../tests/builtins_class_metadata.rs | 49 ++++++++++++++++- .../interpreter/tests/support/object_ops.rs | 9 +++- docs/php/eval.md | 10 ++-- src/codegen/lower_inst/objects/reflection.rs | 18 +++++-- src/types/checker/builtin_types/reflection.rs | 16 +++++- tests/codegen/eval.rs | 54 +++++++++++++++++++ 8 files changed, 152 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 07b61cd3e0..30343f9730 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3228,6 +3228,9 @@ fn eval_reflection_enum_case_new( .case(&case_name) .map(|case| case.attributes().to_vec()) .ok_or(EvalStatus::RuntimeFatal)?; + let flags = eval_reflection_member_flags(EvalVisibility::Public, false, false, false, false) + | EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + let modifiers = eval_reflection_class_constant_modifiers(EvalVisibility::Public, false); eval_reflection_owner_object( owner_kind, &case_name, @@ -3240,8 +3243,8 @@ fn eval_reflection_enum_case_new( &[], None, None, - 0, - 0, + flags, + modifiers, 0, Some(case_value), backing_value, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 41fdc403da..1d4f99fab7 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2759,7 +2759,10 @@ fn eval_builtin_reflection_class_constant( "IS_FINAL" => Some(32), _ => None, } - } else if class_name.eq_ignore_ascii_case("ReflectionClassConstant") { + } else if class_name.eq_ignore_ascii_case("ReflectionClassConstant") + || class_name.eq_ignore_ascii_case("ReflectionEnumUnitCase") + || class_name.eq_ignore_ascii_case("ReflectionEnumBackedCase") + { match constant_name { "IS_PUBLIC" => Some(1), "IS_PROTECTED" => Some(2), diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index c053cc7250..fee699b772 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -911,7 +911,15 @@ echo ReflectionProperty::IS_FINAL; echo ":"; echo ReflectionClassConstant::IS_PUBLIC; echo ":"; echo ReflectionClassConstant::IS_PROTECTED; echo ":"; echo ReflectionClassConstant::IS_PRIVATE; echo ":"; -echo ReflectionClassConstant::IS_FINAL; +echo ReflectionClassConstant::IS_FINAL; echo ":"; +echo ReflectionEnumUnitCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumUnitCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumUnitCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumUnitCase::IS_FINAL; echo ":"; +echo ReflectionEnumBackedCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumBackedCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumBackedCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumBackedCase::IS_FINAL; return true;"#, ) .expect("parse eval fragment"); @@ -922,7 +930,7 @@ return true;"#, assert_eq!( values.output, - "32:64:65536:16:4:64:16:128:1:2:4:64:2048:4096:512:32:1:2:4:32" + "32:64:65536:16:4:64:16:128:1:2:4:64:2048:4096:512:32:1:2:4:32:1:2:4:32:1:2:4:32" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } @@ -2973,6 +2981,43 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies enum-case reflection owners expose inherited constant metadata predicates. +#[test] +fn execute_program_reflects_enum_case_visibility_and_modifiers() { + let program = parse_fragment( + br#"enum EvalEnumCaseVisibility: string { + case Ready = "ready"; +} +$unit = new ReflectionEnumUnitCase("EvalEnumCaseVisibility", "Ready"); +$backed = new ReflectionEnumBackedCase("EvalEnumCaseVisibility", "Ready"); +foreach ([$unit, $backed] as $case) { + echo $case->isEnumCase() ? "E" : "e"; + echo $case->isPrivate() ? "R" : "r"; + echo $case->isProtected() ? "P" : "p"; + echo $case->isPublic() ? "U" : "u"; + echo $case->isFinal() ? "F" : "f"; + echo $case->getModifiers(); echo ":"; +} +echo ReflectionEnumUnitCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumUnitCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumUnitCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumUnitCase::IS_FINAL; echo ":"; +echo ReflectionEnumBackedCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumBackedCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumBackedCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumBackedCase::IS_FINAL; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ErpUf1:ErpUf1:1:2:4:32:1:2:4:32"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 17422ef38c..e0b4d6bf34 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -768,7 +768,12 @@ impl FakeOps { properties.push(("__is_abstract".to_string(), is_abstract)); properties.push(("__modifiers".to_string(), method_modifiers)); } - if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; let is_protected = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; @@ -782,6 +787,8 @@ impl FakeOps { properties.push(("__is_final".to_string(), is_final)); properties.push(("__is_enum_case".to_string(), is_enum_case)); properties.push(("__modifiers".to_string(), modifiers_cell)); + } + if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { properties.push(("__value".to_string(), constant_value)); } if matches!( diff --git a/docs/php/eval.md b/docs/php/eval.md index c16dbe9f14..27be1e3257 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -499,10 +499,12 @@ and `getDeclaringClass()` returns the declaring class or enum as a `getType()` with PHP's current untyped defaults: `false`, `false`, and `null`. Their `__toString()` methods format retained constant and enum-case metadata in PHP's `Constant [ ... ] { ... }` shape. -`ReflectionClassConstant::isPublic()`, `isProtected()`, `isPrivate()`, -`isFinal()`, and `getModifiers()` report visibility/finality metadata with -PHP's `ReflectionClassConstant::IS_*` bitmasks; enum cases report public, -non-final constants. +`ReflectionClassConstant`, `ReflectionEnumUnitCase`, and +`ReflectionEnumBackedCase` expose `isEnumCase()`, `isPublic()`, +`isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()` with PHP's +`ReflectionClassConstant::IS_*` bitmasks. Enum cases report public, +non-final constant metadata, and the enum-case reflection classes expose the +same inherited `IS_*` constants as PHP. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 2aa9841943..8b99792c05 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -516,7 +516,10 @@ fn emit_reflection_owner_object( emit_reflection_owner_mixed_property_from_result(ctx, class_name, "__backing_value")?; } } - if class_name == "ReflectionClassConstant" { + if matches!( + class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { emit_reflection_owner_bool_property( ctx, class_name, @@ -1505,8 +1508,15 @@ fn reflection_enum_case_metadata( is_instantiable: false, is_cloneable: false, is_iterable: false, - modifiers: 0, - member_flags: ReflectionMemberFlags::default(), + modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + member_flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), }) .unwrap_or_else(empty_reflection_metadata), ) @@ -6538,7 +6548,7 @@ fn emit_reflection_member_flag_properties( )?; emit_reflection_owner_bool_property(ctx, class_name, "__is_virtual", flags.is_virtual)?; } - "ReflectionClassConstant" => { + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" => { emit_reflection_owner_bool_property(ctx, class_name, "__is_public", flags.is_public)?; emit_reflection_owner_bool_property( ctx, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 8112677ca1..caae5ede8d 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3829,7 +3829,10 @@ fn reflection_owner_constants(class_name: &str) -> Vec { builtin_class_const("IS_FINAL", 32), ]; } - if class_name == "ReflectionClassConstant" { + if matches!( + class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { return vec![ builtin_class_const("IS_PUBLIC", 1), builtin_class_const("IS_PROTECTED", 2), @@ -4024,7 +4027,11 @@ fn add_reflection_member_flag_methods( ]; if matches!( class_name, - "ReflectionMethod" | "ReflectionProperty" | "ReflectionClassConstant" + "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" ) { for (property, method) in visibility_flags { properties.push(builtin_property( @@ -4216,6 +4223,11 @@ fn add_reflection_member_flag_methods( Some(Expr::new(ExprKind::Null, crate::span::Span::dummy())), )); methods.push(builtin_reflection_class_mixed_method("getValue", "__value")); + } + if matches!( + class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { properties.push(builtin_property( "__is_enum_case", Visibility::Private, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2501814486..af09f61987 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12129,6 +12129,60 @@ echo ReflectionClassConstant::IS_FINAL; ); } +/// Verifies eval and AOT enum-case reflectors expose inherited constant predicates. +#[test] +fn test_eval_reflection_enum_case_visibility_and_modifiers() { + let out = compile_and_run_capture( + r#"isEnumCase() ? "E" : "e"; + echo $case->isPrivate() ? "R" : "r"; + echo $case->isProtected() ? "P" : "p"; + echo $case->isPublic() ? "U" : "u"; + echo $case->isFinal() ? "F" : "f"; + echo $case->getModifiers() . ":"; +} +eval('enum EvalEnumCaseVisibility: string { + case Ready = "ready"; +} +$unit = new ReflectionEnumUnitCase("EvalEnumCaseVisibility", "Ready"); +$backed = new ReflectionEnumBackedCase("EvalEnumCaseVisibility", "Ready"); +echo "\nEVAL:"; +foreach ([$unit, $backed] as $case) { + echo $case->isEnumCase() ? "E" : "e"; + echo $case->isPrivate() ? "R" : "r"; + echo $case->isProtected() ? "P" : "p"; + echo $case->isPublic() ? "U" : "u"; + echo $case->isFinal() ? "F" : "f"; + echo $case->getModifiers() . ":"; +} +echo ReflectionEnumUnitCase::IS_PUBLIC . ":"; +echo ReflectionEnumUnitCase::IS_PROTECTED . ":"; +echo ReflectionEnumUnitCase::IS_PRIVATE . ":"; +echo ReflectionEnumUnitCase::IS_FINAL . ":"; +echo ReflectionEnumBackedCase::IS_PUBLIC . ":"; +echo ReflectionEnumBackedCase::IS_PROTECTED . ":"; +echo ReflectionEnumBackedCase::IS_PRIVATE . ":"; +echo ReflectionEnumBackedCase::IS_FINAL;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "AOT:ErpUf1:ErpUf1:\nEVAL:ErpUf1:ErpUf1:1:2:4:32:1:2:4:32" + ); +} + /// Verifies eval interface and trait constants work through the bridge. #[test] fn test_eval_declared_interface_and_trait_constants() { From 58fcaf526093772b2d4e121339558ec0f5ec29b0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 12:50:19 +0200 Subject: [PATCH 0627/1208] Add eval ReflectionEnum support --- .../src/interpreter/reflection.rs | 418 +++++++++++++++++- .../src/interpreter/runtime_ops.rs | 1 + .../src/interpreter/statements.rs | 28 ++ .../tests/builtins_class_metadata.rs | 54 +++ .../interpreter/tests/support/object_ops.rs | 6 +- docs/php/classes.md | 4 + docs/php/eval.md | 18 +- src/codegen/eval_reflection_owner_helpers.rs | 26 ++ src/codegen/lower_inst/objects.rs | 1 + src/codegen/lower_inst/objects/reflection.rs | 347 ++++++++++++--- src/codegen/mod.rs | 1 + src/ir_lower/program.rs | 1 + src/types/checker/builtin_types/reflection.rs | 84 +++- .../checker/inference/objects/constructors.rs | 4 +- .../objects/constructors/reflection.rs | 26 ++ tests/codegen/eval.rs | 52 +++ tests/codegen/oop/attributes.rs | 30 ++ 17 files changed, 1023 insertions(+), 78 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 30343f9730..753784229c 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -227,6 +227,9 @@ pub(in crate::interpreter) fn eval_reflection_owner_new_object( Some(EVAL_REFLECTION_OWNER_CLASS) => { eval_reflection_class_new(evaluated_args, context, values) } + Some(EVAL_REFLECTION_OWNER_ENUM) => { + eval_reflection_enum_new(evaluated_args, context, values) + } Some(EVAL_REFLECTION_OWNER_FUNCTION) => { eval_reflection_function_new(evaluated_args, context, values) } @@ -403,6 +406,168 @@ pub(in crate::interpreter) fn eval_reflection_class_source_location_result( ) } +/// Handles eval-backed `ReflectionClass` scalar metadata methods. +pub(in crate::interpreter) fn eval_reflection_class_basic_metadata_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { + return Ok(None); + }; + let method_key = method_name.to_ascii_lowercase(); + match method_key.as_str() { + "getname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.string(&metadata.resolved_name).map(Some) + } + "getshortname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_short_name(&metadata.resolved_name)) + .map(Some) + } + "getnamespacename" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_namespace_name(&metadata.resolved_name)) + .map(Some) + } + "innamespace" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(!eval_reflection_namespace_name(&metadata.resolved_name).is_empty()) + .map(Some) + } + "getinterfacenames" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_string_array_result(&metadata.interface_names, values).map(Some) + } + "gettraitnames" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_string_array_result(&metadata.trait_names, values).map(Some) + } + "getparentclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_related_class_result( + EVAL_REFLECTION_OWNER_CLASS, + metadata.parent_class_name.as_deref(), + true, + context, + values, + ) + .map(Some) + } + "getconstructor" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_constructor_object_result( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + true, + context, + values, + ) + .map(Some) + } + "getmodifiers" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.int(metadata.modifiers as i64).map(Some) + } + "isfinal" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_FINAL, + evaluated_args, + values, + ), + "isabstract" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ABSTRACT, + evaluated_args, + values, + ), + "isinterface" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INTERFACE, + evaluated_args, + values, + ), + "istrait" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_TRAIT, + evaluated_args, + values, + ), + "isenum" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ENUM, + evaluated_args, + values, + ), + "isreadonly" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_READONLY, + evaluated_args, + values, + ), + "isanonymous" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS, + evaluated_args, + values, + ), + "isinstantiable" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE, + evaluated_args, + values, + ), + "iscloneable" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_CLONEABLE, + evaluated_args, + values, + ), + "isiterable" | "isiterateable" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ITERABLE, + evaluated_args, + values, + ), + "isinternal" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INTERNAL, + evaluated_args, + values, + ), + "isuserdefined" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + evaluated_args, + values, + ), + _ => Ok(None), + } +} + +/// Returns one boolean ReflectionClass flag after validating a no-arg call. +fn eval_reflection_class_flag_result( + flags: u64, + flag: u64, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(flags & flag != 0).map(Some) +} + /// Handles eval-backed `ReflectionClass::hasMethod()` calls. pub(in crate::interpreter) fn eval_reflection_class_has_method_result( identity: u64, @@ -497,6 +662,94 @@ pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( .map(Some) } +/// Handles eval-backed `ReflectionEnum` methods that are not inherited from `ReflectionClass`. +pub(in crate::interpreter) fn eval_reflection_enum_methods_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_object_has_class(object, "ReflectionEnum", values)? { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let Some(enum_name) = context.resolve_enum_name(&reflected_name) else { + return Ok(None); + }; + match method_name.to_ascii_lowercase().as_str() { + "hascase" => { + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let exists = context + .enum_decl(&enum_name) + .and_then(|enum_decl| enum_decl.case(&requested_name)) + .is_some(); + values.bool_value(exists).map(Some) + } + "getcase" => { + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let owner_kind = eval_reflection_enum_case_owner_kind(&enum_name, context)?; + let result = eval_reflection_enum_case_object_result( + owner_kind, + &enum_name, + &requested_name, + context, + values, + )?; + Ok(Some(result)) + } + "getcases" => { + eval_reflection_bind_no_args(evaluated_args)?; + let (case_names, owner_kind) = { + let enum_decl = context.enum_decl(&enum_name).ok_or(EvalStatus::RuntimeFatal)?; + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + (case_names, eval_reflection_enum_case_owner_kind(&enum_name, context)?) + }; + let mut result = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let case_object = eval_reflection_enum_case_object_result( + owner_kind, &enum_name, case_name, context, values, + )?; + let key = values.int(index as i64)?; + result = values.array_set(result, key, case_object)?; + } + Ok(Some(result)) + } + "isbacked" => { + eval_reflection_bind_no_args(evaluated_args)?; + let is_backed = context + .enum_decl(&enum_name) + .and_then(EvalEnum::backing_type) + .is_some(); + values.bool_value(is_backed).map(Some) + } + "getbackingtype" => { + eval_reflection_bind_no_args(evaluated_args)?; + let backing_type = context + .enum_decl(&enum_name) + .and_then(EvalEnum::backing_type); + let Some(backing_type) = backing_type else { + return values.null().map(Some); + }; + let metadata = eval_reflection_enum_backing_type_metadata(backing_type); + eval_reflection_type_object_result(&metadata, values).map(Some) + } + _ => Ok(None), + } +} + /// Handles eval-backed `ReflectionClass::getInterfaces()` and `getTraits()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( identity: u64, @@ -1660,6 +1913,33 @@ pub(in crate::interpreter) fn eval_reflection_class_constant_to_string_result( values.string(&text).map(Some) } +/// Handles `ReflectionEnumUnitCase::getEnum()` and `ReflectionEnumBackedCase::getEnum()`. +pub(in crate::interpreter) fn eval_reflection_enum_case_get_enum_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getEnum") { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let Some((declaring_class, _, owner_kind)) = context + .eval_reflection_class_constant(identity) + .map(|(class, constant, owner_kind)| (class.to_string(), constant.to_string(), owner_kind)) + else { + return Ok(None); + }; + if !matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + return Ok(None); + } + eval_reflection_enum_object_result(&declaring_class, context, values).map(Some) +} + /// Handles `ReflectionProperty::getRawValue()` and raw write calls. pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( identity: u64, @@ -2170,6 +2450,64 @@ fn eval_reflection_class_new( .map(Some) } +/// Builds an eval-backed `ReflectionEnum` object for a declared enum. +fn eval_reflection_enum_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; + let reflected_name = context + .resolve_enum_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if context.enum_decl(&reflected_name).is_none() { + return if eval_reflection_class_like_exists(&reflected_name, context) { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(None) + }; + } + eval_reflection_enum_object_result(&reflected_name, context, values).map(Some) +} + +/// Materializes one eval-backed `ReflectionEnum` owner object. +fn eval_reflection_enum_object_result( + enum_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let reflected_name = context + .resolve_enum_name(enum_name) + .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); + let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { + return Err(EvalStatus::RuntimeFatal); + }; + if !context.has_enum(&metadata.resolved_name) { + return Err(EvalStatus::RuntimeFatal); + } + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_ENUM, + &metadata.resolved_name, + &metadata.attributes, + &metadata.interface_names, + &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, + metadata.parent_class_name.as_deref(), + &[], + None, + None, + metadata.flags, + metadata.modifiers, + 0, + None, + None, + context, + values, + ) +} + /// Resolves a ReflectionClass constructor target from a class-name string or object. fn eval_reflection_class_target_name( target: RuntimeCellHandle, @@ -3212,28 +3550,54 @@ fn eval_reflection_enum_case_new( } let case_name = eval_reflection_string_arg(args[1], values)?; let declaring_class_name = enum_decl.name().to_string(); + eval_reflection_enum_case_object_result( + owner_kind, + &declaring_class_name, + &case_name, + context, + values, + ) + .map(Some) +} + +/// Builds one eval-backed enum-case reflection owner object. +fn eval_reflection_enum_case_object_result( + owner_kind: u64, + enum_name: &str, + case_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (declaring_class_name, attributes, is_backed) = { + let enum_decl = context.enum_decl(enum_name).ok_or(EvalStatus::RuntimeFatal)?; + let case = enum_decl.case(case_name).ok_or(EvalStatus::RuntimeFatal)?; + ( + enum_decl.name().to_string(), + case.attributes().to_vec(), + enum_decl.backing_type().is_some(), + ) + }; + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && !is_backed { + return Err(EvalStatus::RuntimeFatal); + } let case_value = context - .enum_case(&declaring_class_name, &case_name) + .enum_case(&declaring_class_name, case_name) .ok_or(EvalStatus::RuntimeFatal)?; let backing_value = if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { Some( context - .enum_case_value(&declaring_class_name, &case_name) + .enum_case_value(&declaring_class_name, case_name) .ok_or(EvalStatus::RuntimeFatal)?, ) } else { None }; - let attributes = enum_decl - .case(&case_name) - .map(|case| case.attributes().to_vec()) - .ok_or(EvalStatus::RuntimeFatal)?; let flags = eval_reflection_member_flags(EvalVisibility::Public, false, false, false, false) | EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; let modifiers = eval_reflection_class_constant_modifiers(EvalVisibility::Public, false); eval_reflection_owner_object( owner_kind, - &case_name, + case_name, &attributes, &[], &[], @@ -3251,7 +3615,34 @@ fn eval_reflection_enum_case_new( context, values, ) - .map(Some) +} + +/// Selects the concrete enum-case reflector class for one enum. +fn eval_reflection_enum_case_owner_kind( + enum_name: &str, + context: &ElephcEvalContext, +) -> Result { + let enum_decl = context.enum_decl(enum_name).ok_or(EvalStatus::RuntimeFatal)?; + Ok(if enum_decl.backing_type().is_some() { + EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + } else { + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + }) +} + +/// Builds `ReflectionNamedType` metadata for an enum backing type. +fn eval_reflection_enum_backing_type_metadata( + backing_type: EvalEnumBackingType, +) -> EvalReflectionParameterTypeMetadata { + let name = match backing_type { + EvalEnumBackingType::Int => "int", + EvalEnumBackingType::String => "string", + }; + EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(eval_reflection_builtin_named_type( + name, false, + )), + } } /// Materializes one Reflection owner object and transfers the temporary attribute array. @@ -3433,7 +3824,10 @@ fn eval_reflection_owner_object_with_members( backing_value_cell, constructor, )?; - if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_ENUM + ) { let identity = values.object_identity(object)?; context.register_eval_reflection_class(identity, reflected_name); } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { @@ -3723,7 +4117,9 @@ fn eval_reflection_class_object_map_result( /// Maps a synthetic reflection owner kind to PHP's `Attribute::TARGET_*` bitmask. fn eval_reflection_attribute_target(owner_kind: u64) -> u64 { match owner_kind { - EVAL_REFLECTION_OWNER_CLASS => EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS, + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_ENUM => { + EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS + } EVAL_REFLECTION_OWNER_FUNCTION => EVAL_REFLECTION_ATTRIBUTE_TARGET_FUNCTION, EVAL_REFLECTION_OWNER_METHOD => EVAL_REFLECTION_ATTRIBUTE_TARGET_METHOD, EVAL_REFLECTION_OWNER_PROPERTY => EVAL_REFLECTION_ATTRIBUTE_TARGET_PROPERTY, @@ -4582,6 +4978,7 @@ fn eval_reflection_class_like_is_internal(class_name: &str) -> bool { | "reflectionattribute" | "reflectionclass" | "reflectionclassconstant" + | "reflectionenum" | "reflectionenumbackedcase" | "reflectionenumunitcase" | "reflectionexception" @@ -7296,6 +7693,7 @@ fn reflection_owner_kind(class_name: &str) -> Option { .as_str() { "reflectionclass" => Some(EVAL_REFLECTION_OWNER_CLASS), + "reflectionenum" => Some(EVAL_REFLECTION_OWNER_ENUM), "reflectionfunction" => Some(EVAL_REFLECTION_OWNER_FUNCTION), "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), "reflectionproperty" => Some(EVAL_REFLECTION_OWNER_PROPERTY), diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 08c259aba4..14409d4695 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -500,3 +500,4 @@ pub(super) const EVAL_REFLECTION_OWNER_NAMED_TYPE: u64 = 7; pub(super) const EVAL_REFLECTION_OWNER_UNION_TYPE: u64 = 8; pub(super) const EVAL_REFLECTION_OWNER_INTERSECTION_TYPE: u64 = 9; pub(super) const EVAL_REFLECTION_OWNER_FUNCTION: u64 = 10; +pub(super) const EVAL_REFLECTION_OWNER_ENUM: u64 = 11; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 1d4f99fab7..4587a95308 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3501,6 +3501,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_class_basic_metadata_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_has_method_result( identity, method_name, @@ -3528,6 +3537,16 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_enum_methods_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_get_relation_objects_result( identity, method_name, @@ -3690,6 +3709,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_enum_case_get_enum_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_property_get_value_result( identity, method_name, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index fee699b772..e3d0060ecb 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3018,6 +3018,60 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionEnum exposes eval-declared enum cases and backing metadata. +#[test] +fn execute_program_reflects_eval_enum_owner_metadata() { + let program = parse_fragment( + br#"enum EvalReflectPure { + case Ready; + case Done; +} +enum EvalReflectBacked: string { + case Ready = "ready"; + case Done = "done"; +} +$pure = new ReflectionEnum("EvalReflectPure"); +echo $pure->getName(); echo ":"; +echo $pure->isEnum() ? "E" : "e"; echo ":"; +echo $pure->isBacked() ? "B" : "b"; echo ":"; +echo $pure->getBackingType() === null ? "N" : "n"; echo ":"; +echo $pure->hasCase("Ready") ? "R" : "r"; +echo $pure->hasCase("Missing") ? "M" : "m"; echo ":"; +$case = $pure->getCase("Done"); +echo $case->getName(); echo ":"; +echo $case->getEnum()->getName(); echo ":"; +$cases = $pure->getCases(); +echo count($cases); echo ":"; +echo $cases[0]->getName(); echo ":"; +echo $cases[1]->getEnum()->getName(); echo ":"; +$backed = new ReflectionEnum("EvalReflectBacked"); +$type = $backed->getBackingType(); +echo $backed->isBacked() ? "B" : "b"; echo ":"; +echo $type->getName(); echo ":"; +echo $type->isBuiltin() ? "I" : "i"; echo ":"; +$backed_case = $backed->getCase("Ready"); +echo $backed_case->getName(); echo ":"; +echo $backed_case->getBackingValue(); echo ":"; +echo $backed_case->getEnum()->isBacked() ? "E" : "e"; echo ":"; +$backed_cases = $backed->getCases(); +echo count($backed_cases); echo ":"; +echo $backed_cases[1]->getBackingValue(); echo ":"; +echo $backed_cases[0]->getEnum()->getBackingType()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalReflectPure:E:b:N:Rm:Done:EvalReflectPure:2:Ready:EvalReflectPure:B:string:I:Ready:ready:E:2:done:string" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index e0b4d6bf34..cd920b6618 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -639,6 +639,7 @@ impl FakeOps { ) -> Result { let class_name = match owner_kind { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", + EVAL_REFLECTION_OWNER_ENUM => "ReflectionEnum", EVAL_REFLECTION_OWNER_FUNCTION => "ReflectionFunction", EVAL_REFLECTION_OWNER_METHOD => "ReflectionMethod", EVAL_REFLECTION_OWNER_PROPERTY => "ReflectionProperty", @@ -666,7 +667,10 @@ impl FakeOps { let is_anonymous = self.bool_value((flags & 2048) != 0)?; let modifiers_cell = self.int(modifiers as i64)?; let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; - if owner_kind == EVAL_REFLECTION_OWNER_CLASS { + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_ENUM + ) { let (namespace_name, short_name) = reflection_name_parts(reflected_name); let has_namespace = !namespace_name.is_empty(); let namespace_name = self.string(namespace_name)?; diff --git a/docs/php/classes.md b/docs/php/classes.md index e4b9c66f72..0d96bbbaf2 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1190,6 +1190,8 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::newInstanceArgs()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class from an argument array | | `ReflectionClass::newInstanceWithoutConstructor()` | `new ReflectionClass($class_name)` | Allocate an instance of the reflected class without running `__construct()` | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | +| `ReflectionEnum::isBacked()` / `getBackingType()` | `new ReflectionEnum($enum_name)` | Return whether the reflected enum is backed and expose `int`/`string` backing metadata as `ReflectionNamedType` | +| `ReflectionEnum::hasCase()` / `getCase()` / `getCases()` | Eval-backed `new ReflectionEnum($enum_name)` | Return enum-case presence and `ReflectionEnumUnitCase` / `ReflectionEnumBackedCase` objects for eval-declared enum cases | | `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user or supported callable-builtin function name | | `ReflectionFunction::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionFunction($function_name)` | Return namespace-aware name metadata for the reflected user or supported callable-builtin function | | `ReflectionFunction::isInternal()` / `isUserDefined()` | `new ReflectionFunction($function_name)` | Return origin predicates for supported reflected functions | @@ -1296,6 +1298,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionEnumBackedCase::getBackingValue()` | `new ReflectionEnumBackedCase($enum_name, $case_name)` | Return the scalar backing value for the reflected backed enum case | | `ReflectionEnumUnitCase::getAttributes()` / `ReflectionEnumBackedCase::getAttributes()` | Same as enum-case `getName()` | Return `ReflectionAttribute` objects for enum-case attributes | | `ReflectionEnumUnitCase::getDeclaringClass()` / `ReflectionEnumBackedCase::getDeclaringClass()` | Same as enum-case `getName()` | Return a `ReflectionClass` object for the enum that declares the reflected case | +| `ReflectionEnumUnitCase::getEnum()` / `ReflectionEnumBackedCase::getEnum()` | Same as enum-case `getName()` | Return a `ReflectionEnum` object for the enum that declares the reflected case | | `ReflectionAttribute::newInstance()` | Internal only | Instantiate the attribute class from captured literal args | Functions and their parameters can also be reflected. `ReflectionFunction` reads @@ -1369,6 +1372,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionEnum` additionally supports backing metadata (`isBacked()`, `getBackingType()`); inside eval fragments it also supports enum-case lookup (`hasCase()`, `getCase()`, `getCases()`). Enum-case reflectors expose `getEnum()`. - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. diff --git a/docs/php/eval.md b/docs/php/eval.md index 27be1e3257..9246586b4d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -189,9 +189,10 @@ syntax, but requesting those arguments is a runtime fatal. Private parent properties shadowed by same-named child properties use separate runtime storage, so parent methods keep seeing the private parent value while child methods and public access see the child property. -`ReflectionClass::getAttributes()`, `ReflectionMethod::getAttributes()`, -`ReflectionProperty::getAttributes()`, `ReflectionClassConstant::getAttributes()`, -and `ReflectionParameter::getAttributes()` expose eval-retained class, method, +`ReflectionClass::getAttributes()`, `ReflectionEnum::getAttributes()`, +`ReflectionMethod::getAttributes()`, `ReflectionProperty::getAttributes()`, +`ReflectionClassConstant::getAttributes()`, and +`ReflectionParameter::getAttributes()` expose eval-retained class, enum, method, property, class-constant, and method-parameter attributes for eval-declared class-like symbols and bridge-registered generated/AOT class-level, method, property, and class-constant attributes when their arguments fit the same @@ -210,6 +211,10 @@ eval call site, and line numbers are one-based inside the evaluated fragment. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. +`ReflectionEnum` construction accepts enum-name strings for eval-declared +enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and +`getBackingType()` for eval enum metadata, returning `ReflectionEnumUnitCase`, +`ReflectionEnumBackedCase`, and `ReflectionNamedType` objects where PHP does. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionClass::getShortName()`, @@ -492,8 +497,9 @@ shape; their `getName()` methods return the reflected constant or case name, case object, while `ReflectionEnumUnitCase::getValue()` and `ReflectionEnumBackedCase::getValue()` return the reflected enum-case object. `ReflectionEnumBackedCase::getBackingValue()` returns the scalar backing value, -and `getDeclaringClass()` returns the declaring class or enum as a -`ReflectionClass`. `ReflectionClassConstant::isEnumCase()` reports enum cases. +`getDeclaringClass()` returns the declaring class or enum as a +`ReflectionClass`, and `getEnum()` returns the declaring enum as a +`ReflectionEnum`. `ReflectionClassConstant::isEnumCase()` reports enum cases. `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` expose `isDeprecated()`, `hasType()`, and `getType()` with PHP's current untyped defaults: `false`, `false`, and `null`. @@ -699,7 +705,7 @@ a discarded temporary expression; destructor execution from runtime cycle collection is still outside the bridge. The main remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType -and attribute slice, Reflection type APIs beyond retained parameter, generated +and Enum/attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 350b42bc36..b7b3d8f0a6 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -135,6 +135,7 @@ struct ReflectionOwnerLayout { /// Layouts for the Reflection owner classes eval can materialize. struct ReflectionOwnerLayouts { class: ReflectionOwnerLayout, + enum_class: ReflectionOwnerLayout, function: ReflectionOwnerLayout, method: ReflectionOwnerLayout, property: ReflectionOwnerLayout, @@ -197,6 +198,7 @@ fn function_uses_eval(function: &Function) -> bool { fn reflection_owner_layouts(module: &Module) -> Option { Some(ReflectionOwnerLayouts { class: reflection_owner_layout(module.class_infos.get("ReflectionClass")?, true)?, + enum_class: reflection_owner_layout(module.class_infos.get("ReflectionEnum")?, true)?, function: reflection_owner_layout(module.class_infos.get("ReflectionFunction")?, true)?, method: reflection_owner_layout(module.class_infos.get("ReflectionMethod")?, true)?, property: reflection_owner_layout(module.class_infos.get("ReflectionProperty")?, true)?, @@ -425,6 +427,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection let done_label = "__elephc_eval_reflection_owner_new_done"; let box_label = "__elephc_eval_reflection_owner_new_box"; let class_label = "__elephc_eval_reflection_owner_new_class"; + let enum_label = "__elephc_eval_reflection_owner_new_enum"; let function_label = "__elephc_eval_reflection_owner_new_function"; let method_label = "__elephc_eval_reflection_owner_new_method"; let property_label = "__elephc_eval_reflection_owner_new_property"; @@ -484,6 +487,8 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction(&format!("b.eq {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner emitter.instruction("cmp x0, #10"); // owner kind 10 means ReflectionFunction emitter.instruction(&format!("b.eq {}", function_label)); // allocate a ReflectionFunction owner + emitter.instruction("cmp x0, #11"); // owner kind 11 means ReflectionEnum + emitter.instruction(&format!("b.eq {}", enum_label)); // allocate a ReflectionEnum owner emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body( emitter, @@ -494,6 +499,15 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection fail_label, box_label, ); + emit_aarch64_owner_kind_body( + emitter, + enum_label, + &layouts.enum_class, + true, + false, + fail_label, + box_label, + ); emit_aarch64_owner_kind_body( emitter, function_label, @@ -604,6 +618,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let done_label = "__elephc_eval_reflection_owner_new_done_x"; let box_label = "__elephc_eval_reflection_owner_new_box_x"; let class_label = "__elephc_eval_reflection_owner_new_class_x"; + let enum_label = "__elephc_eval_reflection_owner_new_enum_x"; let function_label = "__elephc_eval_reflection_owner_new_function_x"; let method_label = "__elephc_eval_reflection_owner_new_method_x"; let property_label = "__elephc_eval_reflection_owner_new_property_x"; @@ -665,6 +680,8 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction(&format!("je {}", intersection_type_label)); // allocate a ReflectionIntersectionType owner emitter.instruction("cmp rdi, 10"); // owner kind 10 means ReflectionFunction emitter.instruction(&format!("je {}", function_label)); // allocate a ReflectionFunction owner + emitter.instruction("cmp rdi, 11"); // owner kind 11 means ReflectionEnum + emitter.instruction(&format!("je {}", enum_label)); // allocate a ReflectionEnum owner emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body( emitter, @@ -675,6 +692,15 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO fail_label, box_label, ); + emit_x86_64_owner_kind_body( + emitter, + enum_label, + &layouts.enum_class, + true, + false, + fail_label, + box_label, + ); emit_x86_64_owner_kind_body( emitter, function_label, diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 7cf32e6d7d..9f154e596f 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1511,6 +1511,7 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "RecursiveRegexIterator", "ReflectionAttribute", "ReflectionClass", + "ReflectionEnum", "ReflectionClassConstant", "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 8b99792c05..5477d90e6e 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -52,6 +52,7 @@ struct ReflectionOwnerMetadata { default_property_members: Vec, static_property_members: Vec, constant_reflection_members: Vec, + enum_case_members: Vec, method_members: Vec, property_members: Vec, property_hook_members: Vec<(String, ReflectionListedMember)>, @@ -99,6 +100,7 @@ struct ReflectionListedMember { attr_names: Vec, attr_args: Vec>>, constant_value: Option, + backing_value: Option, is_enum_case: bool, flags: ReflectionMemberFlags, modifiers: i64, @@ -284,6 +286,7 @@ pub(super) fn is_reflection_owner_class(class_name: &str) -> bool { | "ReflectionProperty" | "ReflectionParameter" | "ReflectionClassConstant" + | "ReflectionEnum" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" ) @@ -331,8 +334,10 @@ fn emit_reflection_owner_object( )?; if let Some(reflected_name) = metadata.reflected_name.as_deref() { emit_reflection_string_property(ctx, reflected_name, 8, 16); + if matches!(class_name, "ReflectionClass" | "ReflectionEnum") { + emit_reflection_class_name_parts(ctx, class_name, reflected_name)?; + } if class_name == "ReflectionClass" { - emit_reflection_class_name_parts(ctx, reflected_name)?; emit_reflection_string_array_property_by_name( ctx, "__interface_names", @@ -391,12 +396,14 @@ fn emit_reflection_owner_object( )?; emit_reflection_member_array_property_by_name( ctx, + "ReflectionClass", "__reflection_constants", "ReflectionClassConstant", &metadata.constant_reflection_members, )?; emit_reflection_member_array_property_by_name( ctx, + "ReflectionClass", "__methods", "ReflectionMethod", &metadata.method_members, @@ -405,6 +412,7 @@ fn emit_reflection_owner_object( emit_reflection_parent_class_property(ctx, metadata.parent_class_name.as_deref())?; emit_reflection_member_array_property_by_name( ctx, + "ReflectionClass", "__properties", "ReflectionProperty", &metadata.property_members, @@ -413,6 +421,43 @@ fn emit_reflection_owner_object( let (_, short_name) = reflection_name_parts(reflected_name); emit_reflection_string_property_by_name(ctx, "__short_name", short_name)?; } + if class_name == "ReflectionEnum" { + let case_names = metadata + .enum_case_members + .iter() + .map(|member| member.name.clone()) + .collect::>(); + let case_class = if metadata.type_metadata.is_some() { + "ReflectionEnumBackedCase" + } else { + "ReflectionEnumUnitCase" + }; + emit_reflection_owner_string_array_property_by_name( + ctx, + class_name, + "__case_names", + &case_names, + )?; + emit_reflection_member_array_property_by_name( + ctx, + "ReflectionEnum", + "__cases", + case_class, + &metadata.enum_case_members, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__is_backed", + metadata.type_metadata.is_some(), + )?; + emit_reflection_owner_type_property_by_name( + ctx, + class_name, + "__backing_type", + metadata.type_metadata.as_ref(), + )?; + } if class_name == "ReflectionFunction" { emit_reflection_function_name_parts(ctx, reflected_name)?; } @@ -421,28 +466,29 @@ fn emit_reflection_owner_object( } } emit_reflection_attrs_property(ctx, class_name, &metadata.attr_names, &metadata.attr_args)?; - if class_name == "ReflectionClass" { - emit_reflection_bool_property(ctx, "__is_final", metadata.is_final)?; - emit_reflection_bool_property(ctx, "__is_abstract", metadata.is_abstract)?; - emit_reflection_bool_property(ctx, "__is_interface", metadata.is_interface)?; - emit_reflection_bool_property(ctx, "__is_trait", metadata.is_trait)?; - emit_reflection_bool_property(ctx, "__is_enum", metadata.is_enum)?; - emit_reflection_bool_property(ctx, "__is_readonly", metadata.is_readonly)?; - emit_reflection_bool_property(ctx, "__is_anonymous", metadata.is_anonymous)?; - emit_reflection_bool_property(ctx, "__is_instantiable", metadata.is_instantiable)?; - emit_reflection_bool_property(ctx, "__is_cloneable", metadata.is_cloneable)?; - emit_reflection_bool_property(ctx, "__is_iterable", metadata.is_iterable)?; + if matches!(class_name, "ReflectionClass" | "ReflectionEnum") { + emit_reflection_owner_bool_property(ctx, class_name, "__is_final", metadata.is_final)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_abstract", metadata.is_abstract)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_interface", metadata.is_interface)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_trait", metadata.is_trait)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_enum", metadata.is_enum)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_readonly", metadata.is_readonly)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_anonymous", metadata.is_anonymous)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_instantiable", metadata.is_instantiable)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_cloneable", metadata.is_cloneable)?; + emit_reflection_owner_bool_property(ctx, class_name, "__is_iterable", metadata.is_iterable)?; let is_internal = metadata .reflected_name .as_deref() .is_some_and(reflection_class_like_is_internal); - emit_reflection_bool_property(ctx, "__is_internal", is_internal)?; - emit_reflection_bool_property( + emit_reflection_owner_bool_property(ctx, class_name, "__is_internal", is_internal)?; + emit_reflection_owner_bool_property( ctx, + class_name, "__is_user_defined", metadata.reflected_name.is_some() && !is_internal, )?; - emit_reflection_int_property_by_name(ctx, "__modifiers", metadata.modifiers)?; + emit_reflection_owner_int_property(ctx, class_name, "__modifiers", metadata.modifiers)?; } if matches!( class_name, @@ -457,6 +503,9 @@ fn emit_reflection_owner_object( class_name, metadata.parent_class_name.as_deref(), )?; + if matches!(class_name, "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase") { + emit_reflection_enum_property(ctx, class_name, metadata.parent_class_name.as_deref())?; + } } if matches!(class_name, "ReflectionFunction" | "ReflectionMethod") { let is_internal = reflection_function_or_method_is_internal(class_name, &metadata); @@ -601,15 +650,26 @@ fn emit_reflection_int_property( abi::emit_store_to_address(ctx.emitter, scratch, object_reg, high_offset); } -/// Stores namespace-aware name parts for a statically materialized ReflectionClass. +/// Stores namespace-aware name parts for a statically materialized class-like reflector. fn emit_reflection_class_name_parts( ctx: &mut FunctionContext<'_>, + class_name: &str, reflected_name: &str, ) -> Result<()> { let (namespace_name, short_name) = reflection_name_parts(reflected_name); - emit_reflection_string_property_by_name(ctx, "__short_name", short_name)?; - emit_reflection_string_property_by_name(ctx, "__namespace_name", namespace_name)?; - emit_reflection_bool_property(ctx, "__in_namespace", !namespace_name.is_empty())?; + emit_reflection_owner_string_property_by_name(ctx, class_name, "__short_name", short_name)?; + emit_reflection_owner_string_property_by_name( + ctx, + class_name, + "__namespace_name", + namespace_name, + )?; + emit_reflection_owner_bool_property( + ctx, + class_name, + "__in_namespace", + !namespace_name.is_empty(), + )?; Ok(()) } @@ -680,6 +740,7 @@ fn reflection_owner_metadata( ) -> Result { match class_name { "ReflectionClass" => reflection_class_metadata(ctx, inst), + "ReflectionEnum" => reflection_enum_metadata(ctx, inst), "ReflectionFunction" => reflection_function_metadata(ctx, inst), "ReflectionMethod" => reflection_method_metadata(ctx, inst), "ReflectionProperty" => reflection_property_metadata(ctx, inst), @@ -704,6 +765,30 @@ fn reflection_class_metadata( reflection_class_metadata_for_name(ctx, &reflected_class) } +/// Resolves `ReflectionEnum(enum)` metadata for a known enum name. +fn reflection_enum_metadata( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result { + let Some(enum_operand) = inst.operands.first().copied() else { + return Ok(empty_reflection_metadata()); + }; + let reflected_enum = const_string_or_class_operand(ctx, enum_operand, "ReflectionEnum")?; + let mut metadata = reflection_class_metadata_for_name(ctx, &reflected_enum)?; + let Some(enum_name) = metadata.reflected_name.as_deref() else { + return Ok(empty_reflection_metadata()); + }; + let Some(enum_info) = ctx.module.enum_infos.get(enum_name) else { + return Ok(empty_reflection_metadata()); + }; + metadata.type_metadata = enum_info + .backing_type + .as_ref() + .and_then(reflection_named_type_metadata) + .map(ReflectionParameterTypeMetadata::Named); + Ok(metadata) +} + /// Resolves `ReflectionClass(name)` metadata for a known class-like name. fn reflection_class_metadata_for_name( ctx: &FunctionContext<'_>, @@ -720,6 +805,11 @@ fn reflection_class_metadata_for_name( let static_property_members = reflection_class_static_property_members(class_name, info); let constant_reflection_members = reflection_class_constant_reflection_members(ctx, class_name, info)?; + let enum_case_members = if is_enum { + reflection_enum_case_members(ctx, class_name) + } else { + Vec::new() + }; let method_members = reflection_class_method_members(ctx, class_name, info, &method_names)?; let property_members = reflection_class_property_members(ctx, class_name, info, &property_names); @@ -743,6 +833,7 @@ fn reflection_class_metadata_for_name( default_property_members, static_property_members, constant_reflection_members, + enum_case_members, method_members, property_members, property_hook_members: Vec::new(), @@ -810,6 +901,7 @@ fn reflection_class_metadata_for_name( default_property_members: Vec::new(), static_property_members: Vec::new(), constant_reflection_members, + enum_case_members: Vec::new(), method_members, property_members, property_hook_members: Vec::new(), @@ -883,6 +975,7 @@ fn reflection_class_metadata_for_name( default_property_members: Vec::new(), static_property_members: Vec::new(), constant_reflection_members, + enum_case_members: Vec::new(), method_members, property_members, property_hook_members: Vec::new(), @@ -933,6 +1026,37 @@ fn reflection_shallow_class_metadata_for_name( metadata.constant_names.clear(); metadata.constant_members.clear(); metadata.constant_reflection_members.clear(); + metadata.enum_case_members.clear(); + metadata.method_members.clear(); + metadata.property_members.clear(); + metadata.constructor_member = None; + metadata.parent_class_name = None; + Ok(metadata) +} + +/// Resolves `ReflectionEnum` metadata for nested enum-case slots. +fn reflection_enum_metadata_for_name( + ctx: &FunctionContext<'_>, + reflected_enum: &str, +) -> Result { + let mut metadata = reflection_class_metadata_for_name(ctx, reflected_enum)?; + let Some(enum_name) = metadata.reflected_name.as_deref() else { + return Ok(empty_reflection_metadata()); + }; + let Some(enum_info) = ctx.module.enum_infos.get(enum_name) else { + return Ok(empty_reflection_metadata()); + }; + metadata.type_metadata = enum_info + .backing_type + .as_ref() + .and_then(reflection_named_type_metadata) + .map(ReflectionParameterTypeMetadata::Named); + metadata.method_names.clear(); + metadata.property_names.clear(); + metadata.constant_names.clear(); + metadata.constant_members.clear(); + metadata.constant_reflection_members.clear(); + metadata.enum_case_members.clear(); metadata.method_members.clear(); metadata.property_members.clear(); metadata.constructor_member = None; @@ -1112,13 +1236,14 @@ fn reflection_method_owner_metadata( default_property_members: Vec::new(), static_property_members: Vec::new(), constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), property_hook_members: Vec::new(), constructor_member: None, parent_class_name: member.declaring_class_name, constant_value: member.constant_value, - backing_value: None, + backing_value: member.backing_value, is_enum_case: member.is_enum_case, parameter_members: member.parameters, type_metadata: member.type_metadata, @@ -1191,6 +1316,7 @@ fn reflection_property_metadata( default_property_members: Vec::new(), static_property_members: Vec::new(), constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), property_hook_members, @@ -1403,6 +1529,7 @@ fn reflection_class_constant_metadata( default_property_members: Vec::new(), static_property_members: Vec::new(), constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), property_hook_members: Vec::new(), @@ -1480,6 +1607,7 @@ fn reflection_enum_case_metadata( default_property_members: Vec::new(), static_property_members: Vec::new(), constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), property_hook_members: Vec::new(), @@ -1546,6 +1674,7 @@ fn reflection_class_constant_owner_metadata( default_property_members: Vec::new(), static_property_members: Vec::new(), constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), property_hook_members: Vec::new(), @@ -2296,6 +2425,49 @@ fn reflection_class_constant_reflection_members( Ok(members) } +/// Returns enum-case reflector members for `ReflectionEnum::getCases()`. +fn reflection_enum_case_members( + ctx: &FunctionContext<'_>, + enum_name: &str, +) -> Vec { + let Some(enum_info) = ctx.module.enum_infos.get(enum_name) else { + return Vec::new(); + }; + enum_info + .cases + .iter() + .map(|case| ReflectionListedMember { + name: case.name.clone(), + declaring_class_name: Some(enum_name.to_string()), + attr_names: case.attribute_names.clone(), + attr_args: case.attribute_args.clone(), + constant_value: Some(ReflectionConstantValue::EnumCase { + enum_name: enum_name.to_string(), + case_name: case.name.clone(), + }), + backing_value: reflection_enum_case_backing_value(case), + is_enum_case: true, + flags: reflection_member_flags( + false, + &Visibility::Public, + false, + false, + false, + false, + ), + modifiers: reflection_class_constant_modifiers(&Visibility::Public, false), + type_metadata: None, + default_value: None, + property_hook_members: Vec::new(), + required_parameter_count: 0, + is_deprecated: false, + is_generator: false, + prototype_member: None, + parameters: Vec::new(), + }) + .collect() +} + /// Returns constant-reflector objects for interface constants. fn reflection_interface_constant_reflection_members( ctx: &FunctionContext<'_>, @@ -2398,6 +2570,7 @@ fn push_unique_constant_reflection_member( attr_names, attr_args, constant_value: Some(value), + backing_value: None, is_enum_case, flags: reflection_member_flags(false, &visibility, is_final, false, false, false), modifiers: reflection_class_constant_modifiers(&visibility, is_final), @@ -2749,6 +2922,7 @@ fn reflection_class_method_member( attr_names, attr_args, constant_value: None, + backing_value: None, is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), @@ -2832,6 +3006,7 @@ fn reflection_interface_method_member( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + backing_value: None, is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), @@ -2911,6 +3086,7 @@ fn reflection_trait_method_member( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + backing_value: None, is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), @@ -2997,6 +3173,7 @@ fn reflection_class_property_member( .cloned() .unwrap_or_default(), constant_value: None, + backing_value: None, is_enum_case: false, flags, modifiers: reflection_property_modifiers_for_info(info, property_name) @@ -3133,6 +3310,7 @@ fn reflection_property_hook_method_member( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + backing_value: None, is_enum_case: false, flags, modifiers: reflection_method_modifiers_from_flags(flags), @@ -3320,6 +3498,7 @@ fn default_method_members( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + backing_value: None, is_enum_case: false, flags: reflection_member_flags( false, @@ -3363,6 +3542,7 @@ fn default_property_members( attr_names: Vec::new(), attr_args: Vec::new(), constant_value: None, + backing_value: None, is_enum_case: false, flags: reflection_member_flags( false, @@ -4379,6 +4559,7 @@ fn empty_reflection_metadata() -> ReflectionOwnerMetadata { default_property_members: Vec::new(), static_property_members: Vec::new(), constant_reflection_members: Vec::new(), + enum_case_members: Vec::new(), method_members: Vec::new(), property_members: Vec::new(), property_hook_members: Vec::new(), @@ -4554,22 +4735,6 @@ fn emit_reflection_string_property( abi::emit_pop_reg(ctx.emitter, result_reg); } -/// Writes a heap-persisted string into a named ReflectionClass property slot. -fn emit_reflection_string_property_by_name( - ctx: &mut FunctionContext<'_>, - property_name: &str, - value: &str, -) -> Result<()> { - let class_info = ctx - .module - .class_infos - .get("ReflectionClass") - .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; - let low_offset = reflection_property_offset(class_info, property_name)?; - emit_reflection_string_property(ctx, value, low_offset, low_offset + 8); - Ok(()) -} - /// Writes a heap-persisted string into a named Reflection owner property slot. fn emit_reflection_owner_string_property_by_name( ctx: &mut FunctionContext<'_>, @@ -4649,11 +4814,26 @@ fn emit_reflection_string_array_property_by_name( ctx: &mut FunctionContext<'_>, property_name: &str, names: &[String], +) -> Result<()> { + emit_reflection_owner_string_array_property_by_name( + ctx, + "ReflectionClass", + property_name, + names, + ) +} + +/// Replaces a Reflection owner private array slot with an indexed string array. +fn emit_reflection_owner_string_array_property_by_name( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + names: &[String], ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; let high_offset = low_offset + 8; @@ -4806,9 +4986,10 @@ fn emit_reflection_static_property_array_property_by_name( Ok(()) } -/// Replaces a ReflectionClass private array slot with ReflectionMethod/Property objects. +/// Replaces a reflection-owner private array slot with member reflector objects. fn emit_reflection_member_array_property_by_name( ctx: &mut FunctionContext<'_>, + owner_class_name: &str, property_name: &str, member_class_name: &str, members: &[ReflectionListedMember], @@ -4816,7 +4997,7 @@ fn emit_reflection_member_array_property_by_name( let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(owner_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; let high_offset = low_offset + 8; @@ -5017,6 +5198,42 @@ fn emit_reflection_declaring_class_property( Ok(()) } +/// Replaces an enum-case reflector's private enum slot with `ReflectionEnum`. +fn emit_reflection_enum_property( + ctx: &mut FunctionContext<'_>, + member_class_name: &str, + enum_name: Option<&str>, +) -> Result<()> { + let class_info = ctx + .module + .class_infos + .get(member_class_name) + .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; + let Some(low_offset) = class_info.property_offsets.get("__enum").copied() else { + return Ok(()); + }; + let high_offset = low_offset + 8; + let result_reg = abi::int_result_reg(ctx.emitter); + let object_reg = abi::symbol_scratch_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + if let Some(enum_name) = enum_name { + let enum_metadata = reflection_enum_metadata_for_name(ctx, enum_name)?; + emit_reflection_owner_object(ctx, "ReflectionEnum", &enum_metadata)?; + emit_box_current_value_as_mixed( + ctx.emitter, + &PhpType::Object("ReflectionEnum".to_string()), + ); + } else { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Void); + } + abi::emit_pop_reg(ctx.emitter, object_reg); + abi::emit_store_to_address(ctx.emitter, result_reg, object_reg, low_offset); + abi::emit_store_zero_to_address(ctx.emitter, object_reg, high_offset); + abi::emit_reg_move(ctx.emitter, result_reg, object_reg); + Ok(()) +} + /// Replaces a ReflectionMethod private array slot with ReflectionParameter objects. fn emit_reflection_parameter_array_property_by_name( ctx: &mut FunctionContext<'_>, @@ -5795,7 +6012,10 @@ fn emit_reflection_member_object( &property_string, )?; } - if member_class_name == "ReflectionClassConstant" { + if matches!( + member_class_name, + "ReflectionClassConstant" | "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { if let Some(value) = &member.constant_value { abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); emit_reflection_constant_value_as_mixed(ctx, value); @@ -5814,6 +6034,23 @@ fn emit_reflection_member_object( member.modifiers, )?; } + if member_class_name == "ReflectionEnumBackedCase" { + if let Some(value) = &member.backing_value { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_reflection_constant_value_as_mixed(ctx, value); + emit_reflection_owner_mixed_property_from_result( + ctx, + member_class_name, + "__backing_value", + )?; + } + } + if matches!( + member_class_name, + "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" + ) { + emit_reflection_enum_property(ctx, member_class_name, member.declaring_class_name.as_deref())?; + } emit_reflection_member_flag_properties(ctx, member_class_name, member.flags)?; Ok(()) } @@ -6142,6 +6379,16 @@ fn emit_reflection_owner_type_property( ctx: &mut FunctionContext<'_>, class_name: &str, type_metadata: Option<&ReflectionParameterTypeMetadata>, +) -> Result<()> { + emit_reflection_owner_type_property_by_name(ctx, class_name, "__type", type_metadata) +} + +/// Writes one reflection owner's nullable type-like slot. +fn emit_reflection_owner_type_property_by_name( + ctx: &mut FunctionContext<'_>, + class_name: &str, + property_name: &str, + type_metadata: Option<&ReflectionParameterTypeMetadata>, ) -> Result<()> { let type_offset = { let class_info = ctx @@ -6149,7 +6396,7 @@ fn emit_reflection_owner_type_property( .class_infos .get(class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; - reflection_property_offset(class_info, "__type")? + reflection_property_offset(class_info, property_name)? }; let result_reg = abi::int_result_reg(ctx.emitter); let object_reg = abi::symbol_scratch_reg(ctx.emitter); @@ -6581,24 +6828,6 @@ fn emit_reflection_owner_bool_property( Ok(()) } -/// Stores one boolean property on the current ReflectionClass object result. -fn emit_reflection_bool_property( - ctx: &mut FunctionContext<'_>, - property_name: &str, - value: bool, -) -> Result<()> { - emit_reflection_owner_bool_property(ctx, "ReflectionClass", property_name, value) -} - -/// Stores one integer property on the current ReflectionClass object result. -fn emit_reflection_int_property_by_name( - ctx: &mut FunctionContext<'_>, - property_name: &str, - value: i64, -) -> Result<()> { - emit_reflection_owner_int_property(ctx, "ReflectionClass", property_name, value) -} - /// Stores one integer property on the current Reflection owner object result. fn emit_reflection_owner_int_property( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index a65cb90416..9008bcbccd 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -761,6 +761,7 @@ fn seed_builtin_reflection_class_names(module: &Module, names: &mut HashSet FlattenedClass { extends: None, implements: Vec::new(), is_abstract: false, - is_final: true, + is_final: false, is_readonly_class: false, properties: vec![ builtin_property( @@ -1845,6 +1847,79 @@ fn builtin_reflection_class() -> FlattenedClass { } } +/// Builds the synthetic `ReflectionEnum` class with flattened ReflectionClass members. +fn builtin_reflection_enum_class() -> FlattenedClass { + let mut class = builtin_reflection_class(); + class.name = "ReflectionEnum".to_string(); + class + .methods + .retain(|method| reflection_enum_inherited_method_is_supported(&method.name)); + class.properties.extend([ + builtin_property( + "__case_names", + Visibility::Private, + Some(string_array_type()), + empty_array(), + ), + builtin_property( + "__cases", + Visibility::Private, + Some(array_type()), + empty_array(), + ), + builtin_property( + "__is_backed", + Visibility::Private, + Some(bool_type()), + false_bool(), + ), + builtin_property( + "__backing_type", + Visibility::Private, + Some(mixed_type()), + null_expr(), + ), + ]); + class.methods.extend([ + builtin_reflection_class_bool_method("isBacked", "__is_backed"), + builtin_reflection_class_mixed_method("getBackingType", "__backing_type"), + ]); + class +} + +/// Returns whether a flattened ReflectionClass method is safe on ReflectionEnum. +fn reflection_enum_inherited_method_is_supported(method_name: &str) -> bool { + matches!( + method_name.to_ascii_lowercase().as_str(), + "__construct" + | "getname" + | "getshortname" + | "getnamespacename" + | "innamespace" + | "isfinal" + | "isabstract" + | "isinterface" + | "istrait" + | "isenum" + | "isreadonly" + | "isanonymous" + | "isinstantiable" + | "iscloneable" + | "isiterable" + | "isiterateable" + | "isinternal" + | "isuserdefined" + | "getmodifiers" + | "getattributes" + | "getdoccomment" + | "getextensionname" + | "getextension" + | "getfilename" + | "getstartline" + | "getendline" + ) +} + /// Returns the public modifier constants exposed by PHP's `ReflectionClass`. fn reflection_class_constants() -> Vec { vec![ @@ -4263,6 +4338,13 @@ fn add_reflection_member_flag_methods( class_name, "ReflectionEnumUnitCase" | "ReflectionEnumBackedCase" ) { + properties.push(builtin_property( + "__enum", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); + methods.push(builtin_reflection_class_mixed_method("getEnum", "__enum")); properties.push(builtin_property( "__value", Visibility::Private, diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index d31da4f434..8ba2833493 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -248,6 +248,7 @@ impl Checker { self.reflection_class_literal_arg(class_name, &normalized_args[0], env)?; match class_name { "ReflectionClass" => self.validate_reflection_class_attrs(&reflected_class, expr), + "ReflectionEnum" => self.validate_reflection_enum_attrs(&reflected_class, expr), "ReflectionMethod" => { let method_name = self.reflection_string_literal_arg( class_name, @@ -1148,13 +1149,14 @@ fn fiber_callable_array_parts(expr: &Expr) -> Option<(&Expr, &str)> { } /// Returns `true` if `class_name` is a reflection owner class -/// (`ReflectionClass`, `ReflectionMethod`, `ReflectionProperty`, +/// (`ReflectionClass`, `ReflectionEnum`, `ReflectionMethod`, `ReflectionProperty`, /// `ReflectionParameter`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, /// `ReflectionEnumBackedCase`). fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, "ReflectionClass" + | "ReflectionEnum" | "ReflectionFunction" | "ReflectionMethod" | "ReflectionProperty" diff --git a/src/types/checker/inference/objects/constructors/reflection.rs b/src/types/checker/inference/objects/constructors/reflection.rs index 3fe8028576..2ee5ffccc8 100644 --- a/src/types/checker/inference/objects/constructors/reflection.rs +++ b/src/types/checker/inference/objects/constructors/reflection.rs @@ -86,6 +86,32 @@ impl Checker { )) } + /// Validates enum-level attributes for `ReflectionEnum`. + /// + /// The reflected symbol must be a declared enum; enum attributes are stored + /// on the parallel class metadata produced for enum declarations. + pub(super) fn validate_reflection_enum_attrs( + &self, + enum_name: &str, + expr: &Expr, + ) -> Result<(), CompileError> { + if !self.enums.contains_key(enum_name) { + return Err(CompileError::new( + expr.span, + &format!("ReflectionEnum::__construct(): {} is not an enum", enum_name), + )); + } + if let Some(class_info) = self.classes.get(enum_name) { + return self.validate_reflection_attribute_metadata( + &class_info.attribute_names, + &class_info.attribute_args, + expr, + "ReflectionEnum::getAttributes(): enum has attribute argument metadata that is not supported yet", + ); + } + Ok(()) + } + /// Validates method-level attributes for `ReflectionMethod`. /// /// Checks that the method exists on the reflected class before validating diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index af09f61987..f978bbdd20 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12183,6 +12183,58 @@ echo ReflectionEnumBackedCase::IS_FINAL;'); ); } +/// Verifies ReflectionEnum methods work for enums declared inside eval. +#[test] +fn test_eval_reflection_enum_owner_methods() { + let out = compile_and_run_capture( + r#"getName() . ":"; +echo ($pure->isBacked() ? "B" : "b") . ":"; +echo ($pure->getBackingType() === null ? "N" : "n") . ":"; +echo ($pure->hasCase("Ready") ? "R" : "r"); +echo ($pure->hasCase("Missing") ? "M" : "m") . ":"; +$case = $pure->getCase("Done"); +echo $case->getName() . ":"; +echo $case->getEnum()->getName() . ":"; +$cases = $pure->getCases(); +echo count($cases) . ":"; +echo $cases[0]->getName() . ":"; +echo $cases[1]->getEnum()->getName() . ":"; +$backed = new ReflectionEnum("EvalBridgeBacked"); +$type = $backed->getBackingType(); +echo ($backed->isBacked() ? "B" : "b") . ":"; +echo $type->getName() . ":"; +echo ($type->isBuiltin() ? "I" : "i") . ":"; +$backedCase = $backed->getCase("Ready"); +echo $backedCase->getName() . ":"; +echo $backedCase->getBackingValue() . ":"; +echo ($backedCase->getEnum()->isBacked() ? "E" : "e") . ":"; +$backedCases = $backed->getCases(); +echo count($backedCases) . ":"; +echo $backedCases[1]->getBackingValue() . ":"; +echo $backedCases[0]->getEnum()->getBackingType()->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalBridgePure:b:N:Rm:Done:EvalBridgePure:2:Ready:EvalBridgePure:B:string:I:Ready:ready:E:2:done:string" + ); +} + /// Verifies eval interface and trait constants work through the bridge. #[test] fn test_eval_declared_interface_and_trait_constants() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 8319b42e08..bb34b2e3b4 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -3624,6 +3624,36 @@ echo $backedAttrs[0]->newInstance()->label(); ); } +/// Verifies `ReflectionEnum` exposes AOT enum name and backing metadata. +#[test] +fn test_reflection_enum_owner_metadata() { + let out = compile_and_run( + r#"getName() . ":"; +echo ($pure->isBacked() ? "B" : "b") . ":"; +echo ($pure->getBackingType() === null ? "N" : "n") . ":"; +$backed = new ReflectionEnum(ReflectBackedEnum::class); +$type = $backed->getBackingType(); +echo ($backed->isBacked() ? "B" : "b") . ":"; +echo $type->getName() . ":"; +echo ($type->isBuiltin() ? "I" : "i"); +"#, + ); + assert_eq!( + out, + "ReflectPureEnum:b:N:B:int:I" + ); +} + /// Verifies `ReflectionClassConstant` exposes visibility predicates and modifiers. #[test] fn test_reflection_class_constant_visibility_and_modifiers() { From de4702d164c3bf5e15e85c5dbfdb600ac0f8b7c6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 13:05:45 +0200 Subject: [PATCH 0628/1208] Honor native eval by-ref metadata --- .../src/interpreter/expressions.rs | 16 +- .../src/interpreter/reflection.rs | 49 ++-- .../src/interpreter/statements.rs | 238 +++++++++++++++--- .../src/interpreter/tests/dynamic_calls.rs | 20 ++ .../src/interpreter/tests/expressions.rs | 19 ++ .../src/interpreter/tests/method_arguments.rs | 23 ++ docs/php/eval.md | 28 +-- 7 files changed, 316 insertions(+), 77 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 99242ff59a..370952c6a9 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -82,14 +82,18 @@ pub(in crate::interpreter) fn eval_expr( if let Some(class) = context.class(&class_name).cloned() { eval_dynamic_class_new_object(&class, args, context, scope, values) } else { - let args = bind_native_callable_args( - context.native_constructor_signature(&class_name), + let object = values.new_object(&class_name)?; + if let Err(err) = eval_native_constructor_with_evaluated_args( + &class_name, + object, args, + context, values, - )?; - values - .new_object(&class_name) - .and_then(|object| values.construct_object(object, args).map(|()| object)) + ) { + let _ = values.release(object); + return Err(err); + } + Ok(object) } } EvalExpr::NewAnonymousClass { class, args } => { diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 753784229c..e8953742f8 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1038,7 +1038,7 @@ pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_re .declaring_class_name .as_deref() .unwrap_or(reflected_name.as_str()); - let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, || { + let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { values.static_property_set(&reflected_name, &property_name, args[1]) })?; if updated { @@ -1649,7 +1649,7 @@ pub(in crate::interpreter) fn eval_reflection_property_set_value_result( .declaring_class_name .as_deref() .unwrap_or(declaring_class.as_str()); - let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, || { + let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { values.static_property_set(declaring_class, &property_name, value) })?; if !updated { @@ -5917,7 +5917,7 @@ fn eval_reflection_static_property_value( .declaring_class_name .as_deref() .unwrap_or(reflected_name); - eval_reflection_with_declaring_class_scope(declaring_class, context, || { + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { values.static_property_get(reflected_name, property_name) }) } @@ -6433,13 +6433,14 @@ fn eval_reflection_aot_method_invoke_dispatch( return Err(EvalStatus::RuntimeFatal); } if member.is_static { - let args = bind_native_callable_args( - context.native_static_method_signature(declaring_class, method_name), - method_args, - values, - )?; - return eval_reflection_with_declaring_class_scope(declaring_class, context, || { - values.static_method_call(declaring_class, method_name, args) + return eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { + eval_native_static_method_with_evaluated_args( + declaring_class, + method_name, + method_args, + context, + values, + ) }); } if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { @@ -6455,13 +6456,15 @@ fn eval_reflection_aot_method_invoke_dispatch( )?; return Err(EvalStatus::UncaughtThrowable); } - let args = bind_native_callable_args( - context.native_method_signature(declaring_class, method_name), - method_args, - values, - )?; - eval_reflection_with_declaring_class_scope(declaring_class, context, || { - values.method_call(object, method_name, args) + eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { + eval_native_method_with_evaluated_args( + object, + declaring_class, + method_name, + method_args, + context, + values, + ) }) } @@ -6469,11 +6472,11 @@ fn eval_reflection_aot_method_invoke_dispatch( fn eval_reflection_with_declaring_class_scope( declaring_class: &str, context: &mut ElephcEvalContext, - action: impl FnOnce() -> Result, + action: impl FnOnce(&mut ElephcEvalContext) -> Result, ) -> Result { context.push_class_scope(declaring_class.to_string()); context.push_called_class_scope(declaring_class.to_string()); - let result = action(); + let result = action(context); context.pop_called_class_scope(); context.pop_class_scope(); result @@ -6592,7 +6595,7 @@ fn eval_reflection_aot_instance_property_get_value( context, values, )?; - eval_reflection_with_declaring_class_scope(declaring_class, context, || { + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { values.property_get(object, property_name) }) } @@ -6612,7 +6615,7 @@ fn eval_reflection_aot_instance_property_set_value( context, values, )?; - eval_reflection_with_declaring_class_scope(declaring_class, context, || { + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { values.property_set(object, property_name, value) }) } @@ -6631,7 +6634,7 @@ fn eval_reflection_aot_instance_property_is_initialized( context, values, )?; - eval_reflection_with_declaring_class_scope(declaring_class, context, || { + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { values.property_is_initialized(object, property_name) }) } @@ -6643,7 +6646,7 @@ fn eval_reflection_aot_static_property_is_initialized( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_reflection_with_declaring_class_scope(declaring_class, context, || { + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { values.static_property_is_initialized(declaring_class, property_name) }) } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 4587a95308..a0b7eeec05 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2930,12 +2930,13 @@ pub(in crate::interpreter) fn eval_static_method_call_result( } return Err(EvalStatus::RuntimeFatal); } - let args = bind_native_callable_args( - context.native_static_method_signature(&class_name, method_name), + eval_native_static_method_with_evaluated_args( + &class_name, + method_name, evaluated_args, + context, values, - )?; - values.static_method_call(&class_name, method_name, args) + ) } /// Dispatches static methods for eval's builtin `PropertyHookType` enum slice. @@ -3811,12 +3812,14 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( validate_eval_member_access(&declaring_class, visibility, context)?; } } - let evaluated_args = bind_native_callable_args( - context.native_method_signature(&class_name, method_name), + return eval_native_method_with_evaluated_args( + object, + &class_name, + method_name, evaluated_args, + context, values, - )?; - return values.method_call(object, method_name, evaluated_args); + ); }; let called_class_name = class.name().to_string(); if let Some((class_name, method)) = @@ -3997,13 +4000,14 @@ fn eval_reflection_class_new_instance_result( .resolve_class_name(&reflected_name) .unwrap_or(reflected_name); eval_reflection_public_constructor_scope(context, values, |context, values| { - let args = bind_native_callable_args( - context.native_constructor_signature(&class_name), + let instance = values.new_object(&class_name)?; + eval_native_constructor_with_evaluated_args( + &class_name, + instance, constructor_args, + context, values, )?; - let instance = values.new_object(&class_name)?; - values.construct_object(instance, args)?; Ok(Some(instance)) }) } @@ -4093,13 +4097,14 @@ fn eval_reflection_attribute_new_instance_result( if !values.class_exists(&class_name)? { return values.null(); } - let args = bind_native_callable_args( - context.native_constructor_signature(&class_name), + let object = values.new_object(&class_name)?; + if let Err(err) = eval_native_constructor_with_evaluated_args( + &class_name, + object, positional_args(args), + context, values, - )?; - let object = values.new_object(&class_name)?; - if let Err(err) = values.construct_object(object, args) { + ) { let _ = values.release(object); return Err(err); } @@ -4675,14 +4680,14 @@ pub(in crate::interpreter) fn positional_evaluated_arg_values( Ok(args.into_iter().map(|arg| arg.value).collect()) } -/// Binds native AOT callable args by registered names, or falls back to positional-only args. -pub(in crate::interpreter) fn bind_native_callable_args( +/// Binds native AOT callable args while preserving by-reference caller targets. +pub(in crate::interpreter) fn bind_native_callable_bound_args( signature: Option, args: Vec, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let Some(signature) = signature else { - return positional_evaluated_arg_values(args); + return positional_evaluated_bound_args(None, args); }; if !signature.bridge_supported() { return Err(EvalStatus::RuntimeFatal); @@ -4690,8 +4695,58 @@ pub(in crate::interpreter) fn bind_native_callable_args( if signature.param_names().len() == signature.param_count() { bind_native_signature_args(&signature, args, values) } else { - positional_evaluated_arg_values(args) + positional_evaluated_bound_args(Some(&signature), args) + } +} + +/// Binds positional-only native AOT args and validates registered by-reference slots. +fn positional_evaluated_bound_args( + signature: Option<&NativeCallableSignature>, + args: Vec, +) -> Result, EvalStatus> { + if args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); } + args.into_iter() + .enumerate() + .map(|(index, arg)| { + let ref_target = if signature.is_some_and(|signature| signature.param_by_ref(index)) { + Some(arg.ref_target.ok_or(EvalStatus::RuntimeFatal)?) + } else { + None + }; + Ok(BoundMethodArg { + value: arg.value, + ref_target, + variadic_ref_targets: Vec::new(), + }) + }) + .collect() +} + +/// Returns only runtime cell values from bound native AOT call arguments. +pub(in crate::interpreter) fn native_bound_arg_values( + args: &[BoundMethodArg], +) -> Vec { + args.iter().map(|arg| arg.value).collect() +} + +/// Writes native AOT by-reference argument cells back to their eval caller targets. +pub(in crate::interpreter) fn write_back_native_callable_ref_args( + bound_args: &[BoundMethodArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for bound_arg in bound_args { + if let Some(target) = bound_arg.ref_target.as_ref() { + write_back_method_ref_target(target, bound_arg.value, context, values)?; + } + for (key, target) in &bound_arg.variadic_ref_targets { + let value = values.array_get(bound_arg.value, *key)?; + write_back_method_ref_target(target, value, context, values)?; + } + } + Ok(()) } /// Binds native AOT callable args and fills omitted scalar defaults from metadata. @@ -4699,7 +4754,7 @@ fn bind_native_signature_args( signature: &NativeCallableSignature, args: Vec, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let mut bound_args = vec![None; signature.param_count()]; let variadic_index = native_callable_variadic_index(signature); let mut next_positional = 0; @@ -4707,7 +4762,11 @@ fn bind_native_signature_args( if let Some(index) = variadic_index { let array = values.array_new(args.len())?; - bound_args[index] = Some(array); + bound_args[index] = Some(BoundMethodArg { + value: array, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); } for arg in args { @@ -4718,14 +4777,17 @@ fn bind_native_signature_args( &mut bound_args, &name, arg.value, + arg.ref_target, )?; } else { bind_native_positional_signature_arg( + signature, &mut bound_args, variadic_index, &mut next_positional, &mut next_variadic_index, arg.value, + arg.ref_target, values, )?; } @@ -4744,7 +4806,11 @@ fn bind_native_signature_args( let Some(default) = signature.param_default(position) else { return Err(EvalStatus::RuntimeFatal); }; - *value = Some(materialize_native_callable_default(default, values)?); + *value = Some(BoundMethodArg { + value: materialize_native_callable_default(default, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); } bound_args @@ -4760,11 +4826,13 @@ fn native_callable_variadic_index(signature: &NativeCallableSignature) -> Option /// Binds one positional native AOT argument to a fixed slot or variadic array. fn bind_native_positional_signature_arg( - bound_args: &mut [Option], + signature: &NativeCallableSignature, + bound_args: &mut [Option], variadic_index: Option, next_positional: &mut usize, next_variadic_index: &mut i64, value: RuntimeCellHandle, + ref_target: Option, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if variadic_index.is_some_and(|index| *next_positional >= index) { @@ -4772,29 +4840,62 @@ fn bind_native_positional_signature_arg( *next_variadic_index = next_variadic_index .checked_add(1) .ok_or(EvalStatus::RuntimeFatal)?; - return bind_native_variadic_arg(bound_args, variadic_index, key, value, values); + let ref_target = native_parameter_ref_target(signature, variadic_index, ref_target)?; + return bind_native_variadic_arg(bound_args, variadic_index, key, value, ref_target, values); } - bind_dynamic_positional_arg(bound_args, next_positional, value) + let param_index = *next_positional; + if param_index >= bound_args.len() || bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = native_parameter_ref_target(signature, Some(param_index), ref_target)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) } /// Binds one named native AOT argument to a fixed non-variadic slot. fn bind_native_named_signature_arg( signature: &NativeCallableSignature, variadic_index: Option, - bound_args: &mut [Option], + bound_args: &mut [Option], name: &str, value: RuntimeCellHandle, + ref_target: Option, ) -> Result<(), EvalStatus> { if let Some(param_index) = native_regular_param_index(signature, variadic_index, name) { if bound_args[param_index].is_some() { return Err(EvalStatus::RuntimeFatal); } - bound_args[param_index] = Some(value); + let ref_target = native_parameter_ref_target(signature, Some(param_index), ref_target)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); return Ok(()); } Err(EvalStatus::RuntimeFatal) } +/// Returns the caller writeback target required by a native by-reference parameter. +fn native_parameter_ref_target( + signature: &NativeCallableSignature, + param_index: Option, + ref_target: Option, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !signature.param_by_ref(param_index) { + return Ok(None); + } + ref_target.map(Some).ok_or(EvalStatus::RuntimeFatal) +} + /// Returns the matching non-variadic native parameter index for one named arg. fn native_regular_param_index( signature: &NativeCallableSignature, @@ -4810,19 +4911,88 @@ fn native_regular_param_index( /// Appends one value into the native AOT variadic argument array. fn bind_native_variadic_arg( - bound_args: &mut [Option], + bound_args: &mut [Option], variadic_index: Option, key: RuntimeCellHandle, value: RuntimeCellHandle, + ref_target: Option, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; - let bound = bound_args[index].ok_or(EvalStatus::RuntimeFatal)?; - let array = values.array_set(bound, key, value)?; - bound_args[index] = Some(array); + let bound = bound_args[index].as_mut().ok_or(EvalStatus::RuntimeFatal)?; + let array = values.array_set(bound.value, key, value)?; + bound.value = array; + if let Some(ref_target) = ref_target { + bound.variadic_ref_targets.push((key, ref_target)); + } Ok(()) } +/// Calls one generated/AOT instance method after native signature binding. +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bound_args = bind_native_callable_bound_args( + context.native_method_signature(class_name, method_name), + evaluated_args, + values, + )?; + let result = values.method_call(object, method_name, native_bound_arg_values(&bound_args)); + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => Ok(result), + } +} + +/// Calls one generated/AOT static method after native signature binding. +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bound_args = bind_native_callable_bound_args( + context.native_static_method_signature(class_name, method_name), + evaluated_args, + values, + )?; + let result = + values.static_method_call(class_name, method_name, native_bound_arg_values(&bound_args)); + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => Ok(result), + } +} + +/// Runs one generated/AOT constructor after native signature binding. +pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( + class_name: &str, + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let bound_args = bind_native_callable_bound_args( + context.native_constructor_signature(class_name), + evaluated_args, + values, + )?; + let result = values.construct_object(object, native_bound_arg_values(&bound_args)); + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(()), Ok(())) => Ok(()), + } +} + /// Allocates a fresh runtime cell for one invocation-safe native AOT default. fn materialize_native_callable_default( default: &NativeCallableDefault, diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 6c1e81c8b4..8a81b63e98 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -323,6 +323,26 @@ return call_user_func_array(["KnownClass", "join"], ["left" => "C"]);"#, assert_eq!(values.get(result), FakeValue::String("CB".to_string())); } +/// Verifies runtime AOT static method fallback honors by-reference parameter metadata. +#[test] +fn execute_program_static_runtime_method_hook_rejects_by_ref_temporary_arg() { + let program = parse_fragment(br#"return KnownClass::sum(1, 2);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a static by-reference method parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies runtime AOT static method fallback rejects named arguments without metadata. #[test] fn execute_program_static_runtime_method_hook_rejects_unregistered_named_args() { diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index d0d9248804..d14f9c0453 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -326,6 +326,25 @@ fn execute_program_constructs_named_object_with_registered_named_args() { assert_eq!(values.get(result), FakeValue::Int(9)); } +/// Verifies runtime/AOT constructor fallback honors by-reference parameter metadata. +#[test] +fn execute_program_rejects_runtime_constructor_by_ref_temporary_arg() { + let program = parse_fragment(br#"$box = new KnownClass(9); return $box->read_x();"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a constructor by-reference parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval-declared classes create objects with properties and methods. #[test] fn execute_program_constructs_eval_declared_class_with_method() { diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs index 30af1fedc0..39079f4654 100644 --- a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs @@ -555,6 +555,29 @@ return $box->add2_x(right: 2, left: 3);"#, assert_eq!(values.get(result), FakeValue::Int(15)); } +/// Verifies runtime/AOT method fallback honors registered by-reference parameter metadata. +#[test] +fn execute_program_rejects_runtime_method_by_ref_temporary_arg() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +return $box->add2_x(1, 2);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a runtime by-reference method parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies runtime/AOT method fallback rejects named arguments without metadata. #[test] fn execute_program_rejects_unregistered_named_args_for_runtime_method_fallback() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 9246586b4d..e6740871b1 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private non-by-reference scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch still only marks non-by-reference signatures as invocable. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails except by-reference parameters, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch still only marks non-by-reference signatures as invocable. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -128,9 +128,10 @@ as callable. Static method callables can use `["ClassName", "method"]` or `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method fallback supports the same named-argument and positional variadic-tail binding -for public/protected/private non-by-reference scalar/nullable-int/Mixed/array/iterable/object -parameter signatures when the current eval class scope satisfies PHP visibility -for the generated bridge. +for public/protected/private scalar/nullable-int/Mixed/array/iterable/object +parameter signatures, including registered by-reference lvalue validation, when +the current eval class scope satisfies PHP visibility. Generated AOT bridge +dispatch still only marks non-by-reference signatures as invocable. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -689,14 +690,13 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and closure callback values are -still outside eval fragments. Runtime/AOT object-method and static-method -fallback from eval remain limited to generated visibility-checked -non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter -bridge signatures, including the generated positional variadic array slot when -present. Constructor fallback uses the same generated visibility-checked -non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter -bridge slice. By-reference parameters and broader parameter/return ABI -shapes are still outside those bridge paths. +still outside eval fragments. Runtime/AOT object-method, static-method, and +constructor fallback from eval can bind registered names, defaults, positional +variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch +still remains limited to visibility-checked non-by-reference +scalar/nullable-int/Mixed/array/iterable/object parameter signatures, including +the generated positional variadic array slot when present. Broader +parameter/return ABI shapes are still outside those bridge paths. Eval class support is still smaller than the full static class system. Eval-declared `__destruct()` hooks run when an eval-owned dynamic object reaches @@ -711,7 +711,7 @@ parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, object-valued generated defaults beyond the positional `new C()` parameter slice, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked -non-by-reference scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and +scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while From 113d78b76d4c1ba922c213e2b04555738da816cd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 13:38:11 +0200 Subject: [PATCH 0629/1208] Support eval ReflectionObject --- .../src/interpreter/reflection.rs | 85 +++++++++++++++---- .../src/interpreter/runtime_ops.rs | 1 + .../tests/builtins_class_metadata.rs | 51 +++++++++++ .../interpreter/tests/support/object_ops.rs | 10 ++- docs/php/eval.md | 7 +- src/autoload/mod.rs | 1 + src/codegen/eval_reflection_owner_helpers.rs | 28 +++++- src/codegen/lower_inst/objects.rs | 1 + src/ir_lower/program.rs | 1 + src/types/checker/builtin_types/reflection.rs | 25 ++++++ .../checker/inference/objects/constructors.rs | 6 +- 11 files changed, 193 insertions(+), 23 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index e8953742f8..511d0af814 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -227,6 +227,9 @@ pub(in crate::interpreter) fn eval_reflection_owner_new_object( Some(EVAL_REFLECTION_OWNER_CLASS) => { eval_reflection_class_new(evaluated_args, context, values) } + Some(EVAL_REFLECTION_OWNER_OBJECT) => { + eval_reflection_object_new(evaluated_args, context, values) + } Some(EVAL_REFLECTION_OWNER_ENUM) => { eval_reflection_enum_new(evaluated_args, context, values) } @@ -2387,27 +2390,65 @@ fn eval_reflection_class_new( let reflected_name = context .resolve_class_like_name(&class_name) .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { - let Some((flags, modifiers)) = eval_reflection_aot_class_flags(&reflected_name, values)? + eval_reflection_class_owner_object_result( + EVAL_REFLECTION_OWNER_CLASS, + &reflected_name, + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionObject` from an object instance. +fn eval_reflection_object_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + if values.type_tag(args[0])? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let reflected_name = eval_reflection_object_class_name(args[0], context, values)?; + let Some(object) = eval_reflection_class_owner_object_result( + EVAL_REFLECTION_OWNER_OBJECT, + &reflected_name, + context, + values, + )? + else { + return Err(EvalStatus::RuntimeFatal); + }; + Ok(Some(object)) +} + +/// Materializes class metadata for `ReflectionClass` or `ReflectionObject`. +fn eval_reflection_class_owner_object_result( + owner_kind: u64, + reflected_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) else { + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(reflected_name, values)? else { return Ok(None); }; let method_names = eval_reflection_aot_member_names( EVAL_REFLECTION_OWNER_METHOD, - &reflected_name, + reflected_name, values, )?; let property_names = eval_reflection_aot_member_names( EVAL_REFLECTION_OWNER_PROPERTY, - &reflected_name, + reflected_name, values, )?; - let interface_names = eval_reflection_aot_class_interface_names(&reflected_name, values)?; - let parent_class_name = eval_reflection_aot_parent_class_name(&reflected_name, values)?; - let attributes = context.native_class_attributes(&reflected_name); + let interface_names = eval_reflection_aot_class_interface_names(reflected_name, values)?; + let parent_class_name = eval_reflection_aot_parent_class_name(reflected_name, values)?; + let attributes = context.native_class_attributes(reflected_name); return eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_CLASS, - &reflected_name, + owner_kind, + reflected_name, &attributes, &interface_names, &[], @@ -2428,7 +2469,7 @@ fn eval_reflection_class_new( .map(Some); }; eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_CLASS, + owner_kind, &metadata.resolved_name, &metadata.attributes, &metadata.interface_names, @@ -3721,9 +3762,10 @@ fn eval_reflection_owner_object_with_members( let trait_names_array = eval_reflection_string_array_result(trait_names, values)?; let method_names_array = eval_reflection_string_array_result(method_names, values)?; let property_names_array = eval_reflection_string_array_result(property_names, values)?; - let is_eval_class = owner_kind == EVAL_REFLECTION_OWNER_CLASS + let class_metadata_owner = eval_reflection_owner_uses_class_metadata(owner_kind); + let is_eval_class = class_metadata_owner && eval_reflection_class_like_exists(reflected_name, context); - let method_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS && include_class_members { + let method_objects = if class_metadata_owner && include_class_members { if is_eval_class { eval_reflection_member_object_array_result( EVAL_REFLECTION_OWNER_METHOD, @@ -3756,7 +3798,7 @@ fn eval_reflection_owner_object_with_members( } else { values.array_new(0)? }; - let property_objects = if owner_kind == EVAL_REFLECTION_OWNER_CLASS && include_class_members { + let property_objects = if class_metadata_owner && include_class_members { if is_eval_class { eval_reflection_member_object_array_result( EVAL_REFLECTION_OWNER_PROPERTY, @@ -3826,7 +3868,7 @@ fn eval_reflection_owner_object_with_members( )?; if matches!( owner_kind, - EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_ENUM + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM ) { let identity = values.object_identity(object)?; context.register_eval_reflection_class(identity, reflected_name); @@ -3901,7 +3943,7 @@ fn eval_reflection_related_class_result( let Some(related_class_name) = related_class_name else { return values.bool_value(false); }; - if owner_kind == EVAL_REFLECTION_OWNER_CLASS && include_class_members { + if eval_reflection_owner_uses_class_metadata(owner_kind) && include_class_members { return eval_reflection_full_class_object_result(related_class_name, context, values); } if matches!( @@ -4117,7 +4159,7 @@ fn eval_reflection_class_object_map_result( /// Maps a synthetic reflection owner kind to PHP's `Attribute::TARGET_*` bitmask. fn eval_reflection_attribute_target(owner_kind: u64) -> u64 { match owner_kind { - EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_ENUM => { + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM => { EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS } EVAL_REFLECTION_OWNER_FUNCTION => EVAL_REFLECTION_ATTRIBUTE_TARGET_FUNCTION, @@ -4130,6 +4172,14 @@ fn eval_reflection_attribute_target(owner_kind: u64) -> u64 { } } +/// Returns whether a synthetic owner stores `ReflectionClass`-style metadata. +fn eval_reflection_owner_uses_class_metadata(owner_kind: u64) -> bool { + matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT + ) +} + /// Builds an indexed array of populated ReflectionParameter objects. fn eval_reflection_parameter_object_array_result( parameters: &[EvalReflectionParameterMetadata], @@ -4480,7 +4530,7 @@ fn eval_reflection_constructor_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if owner_kind != EVAL_REFLECTION_OWNER_CLASS || !include_class_members { + if !eval_reflection_owner_uses_class_metadata(owner_kind) || !include_class_members { return values.null(); } let Some(member) = eval_reflection_method_metadata(class_name, "__construct", context) else { @@ -7696,6 +7746,7 @@ fn reflection_owner_kind(class_name: &str) -> Option { .as_str() { "reflectionclass" => Some(EVAL_REFLECTION_OWNER_CLASS), + "reflectionobject" => Some(EVAL_REFLECTION_OWNER_OBJECT), "reflectionenum" => Some(EVAL_REFLECTION_OWNER_ENUM), "reflectionfunction" => Some(EVAL_REFLECTION_OWNER_FUNCTION), "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 14409d4695..7851005733 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -501,3 +501,4 @@ pub(super) const EVAL_REFLECTION_OWNER_UNION_TYPE: u64 = 8; pub(super) const EVAL_REFLECTION_OWNER_INTERSECTION_TYPE: u64 = 9; pub(super) const EVAL_REFLECTION_OWNER_FUNCTION: u64 = 10; pub(super) const EVAL_REFLECTION_OWNER_ENUM: u64 = 11; +pub(super) const EVAL_REFLECTION_OWNER_OBJECT: u64 = 12; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index e3d0060ecb..009efff171 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3072,6 +3072,57 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionObject reflects the runtime class of an eval object instance. +#[test] +fn execute_program_reflection_object_reflects_eval_instances() { + let program = parse_fragment( + br#"class EvalReflectObjectBase { + public function inherited(): string { + return "base"; + } +} +class EvalReflectObjectChild extends EvalReflectObjectBase { + public int $count = 3; +} +$ref = new ReflectionObject(new EvalReflectObjectChild()); +echo get_class($ref); echo ":"; +echo ($ref instanceof ReflectionObject) ? "O" : "o"; +echo ($ref instanceof ReflectionClass) ? "C" : "c"; echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->getParentClass()->getName(); echo ":"; +echo $ref->hasMethod("inherited") ? "M" : "m"; +echo $ref->hasProperty("count") ? "P" : "p"; echo ":"; +$object = $ref->newInstanceWithoutConstructor(); +echo get_class($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ReflectionObject:OC:EvalReflectObjectChild:EvalReflectObjectBase:MP:EvalReflectObjectChild" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionObject rejects non-object constructor arguments. +#[test] +fn execute_program_reflection_object_rejects_non_objects() { + let program = parse_fragment(br#"new ReflectionObject("EvalReflectObjectChild");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("ReflectionObject requires an object"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index cd920b6618..7c29a3a759 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -639,6 +639,7 @@ impl FakeOps { ) -> Result { let class_name = match owner_kind { EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", + EVAL_REFLECTION_OWNER_OBJECT => "ReflectionObject", EVAL_REFLECTION_OWNER_ENUM => "ReflectionEnum", EVAL_REFLECTION_OWNER_FUNCTION => "ReflectionFunction", EVAL_REFLECTION_OWNER_METHOD => "ReflectionMethod", @@ -669,7 +670,9 @@ impl FakeOps { let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; if matches!( owner_kind, - EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_ENUM + EVAL_REFLECTION_OWNER_CLASS + | EVAL_REFLECTION_OWNER_OBJECT + | EVAL_REFLECTION_OWNER_ENUM ) { let (namespace_name, short_name) = reflection_name_parts(reflected_name); let has_namespace = !namespace_name.is_empty(); @@ -1403,6 +1406,11 @@ fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: { return true; } + if class_name.eq_ignore_ascii_case("ReflectionObject") + && target_class.eq_ignore_ascii_case("ReflectionClass") + { + return true; + } if target_class.eq_ignore_ascii_case("Throwable") { return fake_runtime_exception_like_class(class_name); } diff --git a/docs/php/eval.md b/docs/php/eval.md index e6740871b1..90423bd35f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -199,7 +199,7 @@ class-like symbols and bridge-registered generated/AOT class-level, method, property, and class-constant attributes when their arguments fit the same literal subset, and `getName()` returns the reflected class, member, or parameter name -for those owners. `ReflectionClass`, `ReflectionFunction`, `ReflectionMethod`, +for those owners. `ReflectionClass`, `ReflectionObject`, `ReflectionFunction`, `ReflectionMethod`, `ReflectionProperty`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` expose `getDocComment()` and report `false` because eval does not retain docblock text. `ReflectionClass`, `ReflectionFunction`, @@ -211,7 +211,8 @@ declarations. File names use PHP's synthetic eval file format from the current eval call site, and line numbers are one-based inside the evaluated fragment. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT -objects. +objects. `ReflectionObject` construction accepts object arguments and exposes the +same class metadata through a `ReflectionObject` instance. `ReflectionEnum` construction accepts enum-name strings for eval-declared enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and `getBackingType()` for eval enum metadata, returning `ReflectionEnumUnitCase`, @@ -704,7 +705,7 @@ final release through ordinary eval statement execution, such as `unset($obj)` o a discarded temporary expression; destructor execution from runtime cycle collection is still outside the bridge. The main remaining class-system gaps are broader reflection APIs beyond the supported -ReflectionClass/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType +ReflectionClass/Object/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and Enum/attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index 7f0b001970..e358a3934d 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -74,6 +74,7 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "RecursiveIteratorIterator", "ReflectionAttribute", "ReflectionClass", + "ReflectionObject", "ReflectionClassConstant", "ReflectionEnumBackedCase", "ReflectionEnumUnitCase", diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index b7b3d8f0a6..11571211a0 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -1,6 +1,6 @@ //! Purpose: //! Emits user-assembly helpers that let libelephc-magician materialize -//! ReflectionClass, ReflectionFunction, ReflectionMethod, ReflectionParameter, +//! ReflectionClass, ReflectionObject, ReflectionFunction, ReflectionMethod, ReflectionParameter, //! ReflectionProperty, ReflectionClassConstant, ReflectionEnum*, and ReflectionType objects //! with private metadata slots populated from runtime eval declarations. //! @@ -135,6 +135,7 @@ struct ReflectionOwnerLayout { /// Layouts for the Reflection owner classes eval can materialize. struct ReflectionOwnerLayouts { class: ReflectionOwnerLayout, + object_class: ReflectionOwnerLayout, enum_class: ReflectionOwnerLayout, function: ReflectionOwnerLayout, method: ReflectionOwnerLayout, @@ -198,6 +199,7 @@ fn function_uses_eval(function: &Function) -> bool { fn reflection_owner_layouts(module: &Module) -> Option { Some(ReflectionOwnerLayouts { class: reflection_owner_layout(module.class_infos.get("ReflectionClass")?, true)?, + object_class: reflection_owner_layout(module.class_infos.get("ReflectionObject")?, true)?, enum_class: reflection_owner_layout(module.class_infos.get("ReflectionEnum")?, true)?, function: reflection_owner_layout(module.class_infos.get("ReflectionFunction")?, true)?, method: reflection_owner_layout(module.class_infos.get("ReflectionMethod")?, true)?, @@ -427,6 +429,7 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection let done_label = "__elephc_eval_reflection_owner_new_done"; let box_label = "__elephc_eval_reflection_owner_new_box"; let class_label = "__elephc_eval_reflection_owner_new_class"; + let object_label = "__elephc_eval_reflection_owner_new_object"; let enum_label = "__elephc_eval_reflection_owner_new_enum"; let function_label = "__elephc_eval_reflection_owner_new_function"; let method_label = "__elephc_eval_reflection_owner_new_method"; @@ -489,6 +492,8 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection emitter.instruction(&format!("b.eq {}", function_label)); // allocate a ReflectionFunction owner emitter.instruction("cmp x0, #11"); // owner kind 11 means ReflectionEnum emitter.instruction(&format!("b.eq {}", enum_label)); // allocate a ReflectionEnum owner + emitter.instruction("cmp x0, #12"); // owner kind 12 means ReflectionObject + emitter.instruction(&format!("b.eq {}", object_label)); // allocate a ReflectionObject owner emitter.instruction(&format!("b {}", fail_label)); // reject unknown owner kinds emit_aarch64_owner_kind_body( emitter, @@ -499,6 +504,15 @@ fn emit_reflection_owner_new_aarch64(emitter: &mut Emitter, layouts: &Reflection fail_label, box_label, ); + emit_aarch64_owner_kind_body( + emitter, + object_label, + &layouts.object_class, + true, + false, + fail_label, + box_label, + ); emit_aarch64_owner_kind_body( emitter, enum_label, @@ -618,6 +632,7 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO let done_label = "__elephc_eval_reflection_owner_new_done_x"; let box_label = "__elephc_eval_reflection_owner_new_box_x"; let class_label = "__elephc_eval_reflection_owner_new_class_x"; + let object_label = "__elephc_eval_reflection_owner_new_object_x"; let enum_label = "__elephc_eval_reflection_owner_new_enum_x"; let function_label = "__elephc_eval_reflection_owner_new_function_x"; let method_label = "__elephc_eval_reflection_owner_new_method_x"; @@ -682,6 +697,8 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO emitter.instruction(&format!("je {}", function_label)); // allocate a ReflectionFunction owner emitter.instruction("cmp rdi, 11"); // owner kind 11 means ReflectionEnum emitter.instruction(&format!("je {}", enum_label)); // allocate a ReflectionEnum owner + emitter.instruction("cmp rdi, 12"); // owner kind 12 means ReflectionObject + emitter.instruction(&format!("je {}", object_label)); // allocate a ReflectionObject owner emitter.instruction(&format!("jmp {}", fail_label)); // reject unknown owner kinds emit_x86_64_owner_kind_body( emitter, @@ -692,6 +709,15 @@ fn emit_reflection_owner_new_x86_64(emitter: &mut Emitter, layouts: &ReflectionO fail_label, box_label, ); + emit_x86_64_owner_kind_body( + emitter, + object_label, + &layouts.object_class, + true, + false, + fail_label, + box_label, + ); emit_x86_64_owner_kind_body( emitter, enum_label, diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 9f154e596f..31b058c306 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1511,6 +1511,7 @@ fn known_dynamic_new_builtin_class_names() -> &'static [&'static str] { "RecursiveRegexIterator", "ReflectionAttribute", "ReflectionClass", + "ReflectionObject", "ReflectionEnum", "ReflectionClassConstant", "ReflectionEnumBackedCase", diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 0a468e3ebe..9eaa3a8829 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -1106,6 +1106,7 @@ fn lower_builtin_reflection_methods( for class_name in [ "ReflectionAttribute", "ReflectionClass", + "ReflectionObject", "ReflectionEnum", "ReflectionClassConstant", "ReflectionEnumBackedCase", diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index f681df3a13..b9f434099a 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -35,6 +35,7 @@ pub(crate) fn inject_builtin_reflection( for builtin_name in [ "ReflectionAttribute", "ReflectionClass", + "ReflectionObject", "ReflectionEnum", "ReflectionFunction", "ReflectionMethod", @@ -120,6 +121,10 @@ pub(crate) fn inject_builtin_reflection( }, ); class_map.insert("ReflectionClass".to_string(), builtin_reflection_class()); + class_map.insert( + "ReflectionObject".to_string(), + builtin_reflection_object_class(), + ); class_map.insert("ReflectionEnum".to_string(), builtin_reflection_enum_class()); class_map.insert("ReflectionFunction".to_string(), builtin_reflection_function()); class_map.insert( @@ -1847,6 +1852,26 @@ fn builtin_reflection_class() -> FlattenedClass { } } +/// Builds the synthetic `ReflectionObject` class with ReflectionClass metadata slots. +fn builtin_reflection_object_class() -> FlattenedClass { + let mut class = builtin_reflection_class(); + class.name = "ReflectionObject".to_string(); + class.extends = Some("ReflectionClass".to_string()); + if let Some(constructor) = class + .methods + .iter_mut() + .find(|method| method.name == "__construct") + { + *constructor = builtin_reflection_owner_constructor_method(vec![( + "object", + Some(object_type()), + None, + false, + )]); + } + class +} + /// Builds the synthetic `ReflectionEnum` class with flattened ReflectionClass members. fn builtin_reflection_enum_class() -> FlattenedClass { let mut class = builtin_reflection_class(); diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 8ba2833493..2e8bb68acc 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -243,6 +243,9 @@ impl Checker { if class_name == "ReflectionFunction" { return self.validate_reflection_function_constructor(&normalized_args, expr, env); } + if class_name == "ReflectionObject" { + return Ok(()); + } let reflected_class = self.reflection_class_literal_arg(class_name, &normalized_args[0], env)?; @@ -1149,13 +1152,14 @@ fn fiber_callable_array_parts(expr: &Expr) -> Option<(&Expr, &str)> { } /// Returns `true` if `class_name` is a reflection owner class -/// (`ReflectionClass`, `ReflectionEnum`, `ReflectionMethod`, `ReflectionProperty`, +/// (`ReflectionClass`, `ReflectionObject`, `ReflectionEnum`, `ReflectionMethod`, `ReflectionProperty`, /// `ReflectionParameter`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, /// `ReflectionEnumBackedCase`). fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, "ReflectionClass" + | "ReflectionObject" | "ReflectionEnum" | "ReflectionFunction" | "ReflectionMethod" From 9e39d050482808d27b4b256b08dc3ebeb722df83 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 14:06:21 +0200 Subject: [PATCH 0630/1208] Support static ReflectionObject runtime metadata --- docs/php/classes.md | 2 + src/codegen/lower_inst/objects/reflection.rs | 361 +++++++++++++++--- src/ir_lower/expr/mod.rs | 8 +- src/types/checker/builtin_types/reflection.rs | 4 +- tests/codegen/oop/attributes.rs | 39 ++ 5 files changed, 364 insertions(+), 50 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 0d96bbbaf2..b59e556389 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1190,6 +1190,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClass::newInstanceArgs()` | `new ReflectionClass($class_name)` | Construct an instance of the reflected class from an argument array | | `ReflectionClass::newInstanceWithoutConstructor()` | `new ReflectionClass($class_name)` | Allocate an instance of the reflected class without running `__construct()` | | `ReflectionClass::getAttributes()` | `new ReflectionClass($class_name)` | Return `ReflectionAttribute` objects for class attributes | +| `ReflectionObject::*` inherited class metadata methods | `new ReflectionObject($object)` | Return the same reflected class metadata as `ReflectionClass`, with the reflected class taken from the object's runtime class id | | `ReflectionEnum::isBacked()` / `getBackingType()` | `new ReflectionEnum($enum_name)` | Return whether the reflected enum is backed and expose `int`/`string` backing metadata as `ReflectionNamedType` | | `ReflectionEnum::hasCase()` / `getCase()` / `getCases()` | Eval-backed `new ReflectionEnum($enum_name)` | Return enum-case presence and `ReflectionEnumUnitCase` / `ReflectionEnumBackedCase` objects for eval-declared enum cases | | `ReflectionFunction::getName()` | `new ReflectionFunction($function_name)` | Return the canonical user or supported callable-builtin function name | @@ -1372,6 +1373,7 @@ Limitations today: - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionObject` supports the inherited class-metadata methods for object arguments and materializes the same metadata through a `ReflectionObject` instance. - `ReflectionEnum` additionally supports backing metadata (`isBacked()`, `getBackingType()`); inside eval fragments it also supports enum-case lookup (`hasCase()`, `getCase()`, `getCases()`). Enum-case reflectors expose `getEnum()`. - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 5477d90e6e..8c14add7b0 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -6,7 +6,7 @@ //! - `crate::codegen::lower_inst::objects::lower_object_new()`. //! //! Key details: -//! - `ReflectionClass`, `ReflectionFunction`, `ReflectionMethod`, +//! - `ReflectionClass`, `ReflectionObject`, `ReflectionFunction`, `ReflectionMethod`, //! `ReflectionProperty`, `ReflectionClassConstant`, and `ReflectionEnum*` //! constructors are compile-time metadata lookups that populate private //! metadata slots instead of running their public empty bodies. @@ -276,11 +276,18 @@ struct ReflectionMemberFlags { is_virtual: bool, } +/// Runtime class candidate used when object reflection must dispatch by object class id. +struct ReflectionRuntimeClassCandidate { + class_name: String, + class_id: u64, +} + /// Returns true for reflection owner classes that need metadata-aware construction. pub(super) fn is_reflection_owner_class(class_name: &str) -> bool { matches!( class_name, "ReflectionClass" + | "ReflectionObject" | "ReflectionFunction" | "ReflectionMethod" | "ReflectionProperty" @@ -298,20 +305,259 @@ pub(super) fn lower_reflection_owner_new( inst: &Instruction, class_name: &str, ) -> Result<()> { - let metadata = reflection_owner_metadata(ctx, class_name, inst)?; - emit_reflection_owner_object(ctx, class_name, &metadata)?; + if let Some(object_operand) = reflection_object_operand(ctx, class_name, inst)? { + emit_reflection_owner_from_runtime_object(ctx, class_name, object_operand)?; + } else { + let metadata = reflection_owner_metadata(ctx, class_name, inst)?; + emit_reflection_owner_object(ctx, class_name, &metadata)?; + } let result = inst .result .ok_or_else(|| CodegenIrError::invalid_module("reflection object_new missing result"))?; ctx.store_result_value(result) } +/// Returns the constructor object operand for ReflectionClass/Object object reflection. +fn reflection_object_operand( + ctx: &FunctionContext<'_>, + class_name: &str, + inst: &Instruction, +) -> Result> { + if !matches!(class_name, "ReflectionClass" | "ReflectionObject") { + return Ok(None); + } + let Some(object_operand) = inst.operands.first().copied() else { + return Ok(None); + }; + if matches!(ctx.value_php_type(object_operand)?, PhpType::Object(_)) { + Ok(Some(object_operand)) + } else { + Ok(None) + } +} + +/// Materializes ReflectionClass/Object metadata by dispatching on the object's runtime class id. +fn emit_reflection_owner_from_runtime_object( + ctx: &mut FunctionContext<'_>, + class_name: &str, + object_operand: ValueId, +) -> Result<()> { + let candidates = reflection_runtime_class_candidates(ctx, object_operand)?; + if candidates.is_empty() { + return Err(CodegenIrError::unsupported(format!( + "{} constructor for object with no known runtime class candidates", + class_name + ))); + } + + let fallback_label = ctx.next_label("reflection_object_fallback"); + let done_label = ctx.next_label("reflection_object_done"); + let case_labels = candidates + .iter() + .map(|_| ctx.next_label("reflection_object_case")) + .collect::>(); + + emit_runtime_object_class_dispatch(ctx, object_operand, &candidates, &case_labels, &fallback_label)?; + + let fallback_metadata = reflection_class_metadata_for_name(ctx, &candidates[0].class_name)?; + emit_reflection_owner_object(ctx, class_name, &fallback_metadata)?; + emit_reflection_dispatch_jump(ctx, &done_label); // skip runtime reflection candidates after fallback allocation + + for (candidate, label) in candidates.iter().zip(case_labels.iter()) { + ctx.emitter.label(label); + let metadata = reflection_class_metadata_for_name(ctx, &candidate.class_name)?; + emit_reflection_owner_object(ctx, class_name, &metadata)?; + emit_reflection_dispatch_jump(ctx, &done_label); // finish after materializing the matched runtime class + } + + ctx.emitter.label(&done_label); + Ok(()) +} + +/// Emits an unconditional jump for the reflection runtime-class dispatch. +fn emit_reflection_dispatch_jump(ctx: &mut FunctionContext<'_>, label: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("b {}", label)); // continue after the selected reflection object is ready + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("jmp {}", label)); // continue after the selected reflection object is ready + } + } +} + +/// Emits target-specific class-id comparisons for runtime object reflection. +fn emit_runtime_object_class_dispatch( + ctx: &mut FunctionContext<'_>, + object_operand: ValueId, + candidates: &[ReflectionRuntimeClassCandidate], + case_labels: &[String], + fallback_label: &str, +) -> Result<()> { + ctx.load_value_to_result(object_operand)?; + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cbz x0, {}", fallback_label)); // use fallback metadata for null object pointers + ctx.emitter.instruction("ldr x9, [x0]"); // load the object's concrete runtime class id + for (candidate, label) in candidates.iter().zip(case_labels.iter()) { + abi::emit_load_int_immediate(ctx.emitter, "x10", candidate.class_id as i64); + ctx.emitter.instruction("cmp x9, x10"); // compare the object class id with this reflection candidate + ctx.emitter.instruction(&format!("b.eq {}", label)); // select metadata for the matched runtime class + } + ctx.emitter.instruction(&format!("b {}", fallback_label)); // fall back when no generated candidate matches + } + Arch::X86_64 => { + ctx.emitter.instruction("test rax, rax"); // use fallback metadata for null object pointers + ctx.emitter.instruction(&format!("je {}", fallback_label)); // skip class-id loading when the object pointer is null + ctx.emitter.instruction("mov r11, QWORD PTR [rax]"); // load the object's concrete runtime class id + for (candidate, label) in candidates.iter().zip(case_labels.iter()) { + abi::emit_load_int_immediate(ctx.emitter, "r10", candidate.class_id as i64); + ctx.emitter.instruction("cmp r11, r10"); // compare the object class id with this reflection candidate + ctx.emitter.instruction(&format!("je {}", label)); // select metadata for the matched runtime class + } + ctx.emitter.instruction(&format!("jmp {}", fallback_label)); // fall back when no generated candidate matches + } + } + ctx.emitter.label(fallback_label); + Ok(()) +} + +/// Returns runtime class candidates compatible with the object's static type metadata. +fn reflection_runtime_class_candidates( + ctx: &FunctionContext<'_>, + object_operand: ValueId, +) -> Result> { + let static_type = reflection_object_static_type_name(ctx, object_operand)?; + let mut candidates = ctx + .module + .class_infos + .iter() + .filter(|(class_name, _)| reflection_class_matches_object_type(ctx, class_name, &static_type)) + .map(|(class_name, class_info)| ReflectionRuntimeClassCandidate { + class_name: class_name.clone(), + class_id: class_info.class_id, + }) + .collect::>(); + candidates.sort_by_key(|candidate| candidate.class_id); + candidates.dedup_by_key(|candidate| candidate.class_id); + Ok(candidates) +} + +/// Resolves the static object type name used to bound runtime ReflectionObject dispatch. +fn reflection_object_static_type_name( + ctx: &FunctionContext<'_>, + object_operand: ValueId, +) -> Result { + match ctx.value_php_type(object_operand)? { + PhpType::Object(class_name) if class_name.is_empty() => reflection_current_method_class(ctx) + .map(str::to_string) + .ok_or_else(|| { + CodegenIrError::unsupported( + "ReflectionObject constructor for object with unknown static class", + ) + }), + PhpType::Object(class_name) => Ok(class_name), + other => Err(CodegenIrError::unsupported(format!( + "ReflectionObject constructor for PHP type {:?}", + other + ))), + } +} + +/// Returns the lexical class name encoded in the current EIR method name, if any. +fn reflection_current_method_class<'a>(ctx: &'a FunctionContext<'_>) -> Option<&'a str> { + ctx.function + .name + .rsplit_once("::") + .map(|(class_name, _)| class_name) +} + +/// Returns true when a runtime candidate class can inhabit the operand's static object type. +fn reflection_class_matches_object_type( + ctx: &FunctionContext<'_>, + class_name: &str, + static_type: &str, +) -> bool { + if reflection_same_php_type_name(class_name, static_type) { + return true; + } + if resolve_reflection_interface(ctx, static_type).is_some() { + return reflection_class_implements_interface(ctx, class_name, static_type); + } + reflection_class_extends_class(ctx, class_name, static_type) +} + +/// Returns true when two PHP type names compare case-insensitively after namespace trimming. +fn reflection_same_php_type_name(left: &str, right: &str) -> bool { + php_symbol_key(left.trim_start_matches('\\')) == php_symbol_key(right.trim_start_matches('\\')) +} + +/// Returns true when a runtime class candidate is or extends `target_class`. +fn reflection_class_extends_class( + ctx: &FunctionContext<'_>, + class_name: &str, + target_class: &str, +) -> bool { + let mut current = Some(class_name.to_string()); + while let Some(name) = current { + if reflection_same_php_type_name(&name, target_class) { + return true; + } + current = resolve_reflection_class(ctx, &name) + .and_then(|(_, class_info)| class_info.parent.clone()); + } + false +} + +/// Returns true when a runtime class candidate implements the requested interface. +fn reflection_class_implements_interface( + ctx: &FunctionContext<'_>, + class_name: &str, + target_interface: &str, +) -> bool { + let mut current = Some(class_name.to_string()); + while let Some(name) = current { + let Some((_, class_info)) = resolve_reflection_class(ctx, &name) else { + return false; + }; + if class_info.interfaces.iter().any(|interface_name| { + reflection_interface_extends_interface(ctx, interface_name, target_interface) + }) { + return true; + } + current = class_info.parent.clone(); + } + false +} + +/// Returns true when an interface is or extends the requested interface target. +fn reflection_interface_extends_interface( + ctx: &FunctionContext<'_>, + interface_name: &str, + target_interface: &str, +) -> bool { + if reflection_same_php_type_name(interface_name, target_interface) { + return true; + } + let Some(interface_name) = resolve_reflection_interface(ctx, interface_name) else { + return false; + }; + let Some(interface) = ctx.module.interface_infos.get(interface_name) else { + return false; + }; + interface + .parents + .iter() + .any(|parent| reflection_interface_extends_interface(ctx, parent, target_interface)) +} + /// Allocates and populates one builtin Reflection owner object from metadata. fn emit_reflection_owner_object( ctx: &mut FunctionContext<'_>, class_name: &str, metadata: &ReflectionOwnerMetadata, ) -> Result<()> { + let is_reflection_class_owner = matches!(class_name, "ReflectionClass" | "ReflectionObject"); let (class_id, property_count, uninitialized_marker_offsets) = { let class_info = ctx .module @@ -333,86 +579,110 @@ fn emit_reflection_owner_object( &[], )?; if let Some(reflected_name) = metadata.reflected_name.as_deref() { - emit_reflection_string_property(ctx, reflected_name, 8, 16); - if matches!(class_name, "ReflectionClass" | "ReflectionEnum") { + emit_reflection_owner_string_property_by_name(ctx, class_name, "__name", reflected_name)?; + if is_reflection_class_owner || class_name == "ReflectionEnum" { emit_reflection_class_name_parts(ctx, class_name, reflected_name)?; } - if class_name == "ReflectionClass" { - emit_reflection_string_array_property_by_name( + if is_reflection_class_owner { + emit_reflection_owner_string_array_property_by_name( ctx, + class_name, "__interface_names", &metadata.interface_names, )?; emit_reflection_class_array_property_by_name( ctx, + class_name, "__interfaces", &metadata.interface_names, )?; - emit_reflection_string_array_property_by_name( + emit_reflection_owner_string_array_property_by_name( ctx, + class_name, "__trait_names", &metadata.trait_names, )?; - emit_reflection_class_array_property_by_name(ctx, "__traits", &metadata.trait_names)?; + emit_reflection_class_array_property_by_name( + ctx, + class_name, + "__traits", + &metadata.trait_names, + )?; emit_reflection_string_assoc_property_by_name( ctx, + class_name, "__trait_aliases", &metadata.trait_aliases, )?; - emit_reflection_string_array_property_by_name( + emit_reflection_owner_string_array_property_by_name( ctx, + class_name, "__parent_names", &metadata.parent_names, )?; - emit_reflection_string_array_property_by_name( + emit_reflection_owner_string_array_property_by_name( ctx, + class_name, "__method_names", &metadata.method_names, )?; - emit_reflection_string_array_property_by_name( + emit_reflection_owner_string_array_property_by_name( ctx, + class_name, "__property_names", &metadata.property_names, )?; - emit_reflection_string_array_property_by_name( + emit_reflection_owner_string_array_property_by_name( ctx, + class_name, "__constant_names", &metadata.constant_names, )?; emit_reflection_constant_array_property_by_name( ctx, + class_name, "__constants", &metadata.constant_members, )?; emit_reflection_default_property_array_property_by_name( ctx, + class_name, "__default_properties", &metadata.default_property_members, )?; emit_reflection_static_property_array_property_by_name( ctx, + class_name, "__static_properties", &metadata.static_property_members, )?; emit_reflection_member_array_property_by_name( ctx, - "ReflectionClass", + class_name, "__reflection_constants", "ReflectionClassConstant", &metadata.constant_reflection_members, )?; emit_reflection_member_array_property_by_name( ctx, - "ReflectionClass", + class_name, "__methods", "ReflectionMethod", &metadata.method_members, )?; - emit_reflection_constructor_property(ctx, metadata.constructor_member.as_ref())?; - emit_reflection_parent_class_property(ctx, metadata.parent_class_name.as_deref())?; + emit_reflection_constructor_property( + ctx, + class_name, + metadata.constructor_member.as_ref(), + )?; + emit_reflection_parent_class_property( + ctx, + class_name, + metadata.parent_class_name.as_deref(), + )?; emit_reflection_member_array_property_by_name( ctx, - "ReflectionClass", + class_name, "__properties", "ReflectionProperty", &metadata.property_members, @@ -466,7 +736,7 @@ fn emit_reflection_owner_object( } } emit_reflection_attrs_property(ctx, class_name, &metadata.attr_names, &metadata.attr_args)?; - if matches!(class_name, "ReflectionClass" | "ReflectionEnum") { + if is_reflection_class_owner || class_name == "ReflectionEnum" { emit_reflection_owner_bool_property(ctx, class_name, "__is_final", metadata.is_final)?; emit_reflection_owner_bool_property(ctx, class_name, "__is_abstract", metadata.is_abstract)?; emit_reflection_owner_bool_property(ctx, class_name, "__is_interface", metadata.is_interface)?; @@ -4789,7 +5059,9 @@ fn emit_reflection_attrs_property( /// Returns PHP's `Attribute::TARGET_*` bitmask for attributes on one Reflection owner type. fn reflection_attribute_target_for_owner(class_name: &str) -> i64 { match class_name { - "ReflectionClass" => super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_CLASS, + "ReflectionClass" | "ReflectionObject" => { + super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_CLASS + } "ReflectionFunction" => { super::super::builtins::attributes::REFLECTION_ATTRIBUTE_TARGET_FUNCTION } @@ -4809,20 +5081,6 @@ fn reflection_attribute_target_for_owner(class_name: &str) -> i64 { } } -/// Replaces a ReflectionClass private array slot with an indexed string array. -fn emit_reflection_string_array_property_by_name( - ctx: &mut FunctionContext<'_>, - property_name: &str, - names: &[String], -) -> Result<()> { - emit_reflection_owner_string_array_property_by_name( - ctx, - "ReflectionClass", - property_name, - names, - ) -} - /// Replaces a Reflection owner private array slot with an indexed string array. fn emit_reflection_owner_string_array_property_by_name( ctx: &mut FunctionContext<'_>, @@ -4858,16 +5116,17 @@ fn emit_reflection_owner_string_array_property_by_name( Ok(()) } -/// Replaces a ReflectionClass private slot with name-keyed ReflectionClass objects. +/// Replaces a ReflectionClass-like private slot with name-keyed ReflectionClass objects. fn emit_reflection_class_array_property_by_name( ctx: &mut FunctionContext<'_>, + owner_class_name: &str, property_name: &str, names: &[String], ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(owner_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; let high_offset = low_offset + 8; @@ -4892,16 +5151,17 @@ fn emit_reflection_class_array_property_by_name( Ok(()) } -/// Replaces a ReflectionClass private slot with an associative constant-value array. +/// Replaces a ReflectionClass-like private slot with an associative constant-value array. fn emit_reflection_constant_array_property_by_name( ctx: &mut FunctionContext<'_>, + owner_class_name: &str, property_name: &str, members: &[ReflectionConstantMember], ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(owner_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; let high_offset = low_offset + 8; @@ -4925,16 +5185,17 @@ fn emit_reflection_constant_array_property_by_name( Ok(()) } -/// Replaces a ReflectionClass private slot with an associative default-property array. +/// Replaces a ReflectionClass-like private slot with an associative default-property array. fn emit_reflection_default_property_array_property_by_name( ctx: &mut FunctionContext<'_>, + owner_class_name: &str, property_name: &str, members: &[ReflectionDefaultPropertyMember], ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(owner_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; let high_offset = low_offset + 8; @@ -4958,16 +5219,17 @@ fn emit_reflection_default_property_array_property_by_name( Ok(()) } -/// Replaces a ReflectionClass private slot with current static-property values. +/// Replaces a ReflectionClass-like private slot with current static-property values. fn emit_reflection_static_property_array_property_by_name( ctx: &mut FunctionContext<'_>, + owner_class_name: &str, property_name: &str, members: &[ReflectionStaticPropertyMember], ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(owner_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; let high_offset = low_offset + 8; @@ -5062,15 +5324,16 @@ fn emit_reflection_property_hook_array_property_by_name( Ok(()) } -/// Replaces the ReflectionClass private constructor slot with `ReflectionMethod|null`. +/// Replaces a ReflectionClass-like private constructor slot with `ReflectionMethod|null`. fn emit_reflection_constructor_property( ctx: &mut FunctionContext<'_>, + owner_class_name: &str, member: Option<&ReflectionListedMember>, ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(owner_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, "__constructor")?; let high_offset = low_offset + 8; @@ -5124,15 +5387,16 @@ fn emit_reflection_method_prototype_property( Ok(()) } -/// Replaces the ReflectionClass private parent slot with `ReflectionClass|false`. +/// Replaces a ReflectionClass-like private parent slot with `ReflectionClass|false`. fn emit_reflection_parent_class_property( ctx: &mut FunctionContext<'_>, + owner_class_name: &str, parent_class_name: Option<&str>, ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(owner_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, "__parent_class")?; let high_offset = low_offset + 8; @@ -5431,16 +5695,17 @@ fn reflection_property_hook_map_type() -> PhpType { } } -/// Replaces a ReflectionClass private slot with a string-keyed string-value map. +/// Replaces a ReflectionClass-like private slot with a string-keyed string-value map. fn emit_reflection_string_assoc_property_by_name( ctx: &mut FunctionContext<'_>, + owner_class_name: &str, property_name: &str, entries: &[(String, String)], ) -> Result<()> { let class_info = ctx .module .class_infos - .get("ReflectionClass") + .get(owner_class_name) .ok_or_else(|| CodegenIrError::missing_entry("class", 0))?; let low_offset = reflection_property_offset(class_info, property_name)?; let high_offset = low_offset + 8; diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 98fa592662..a8641428a3 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9236,7 +9236,7 @@ fn lower_new_object( ) } -/// Lowers `ReflectionClass(object)` to the object's statically-known class name. +/// Lowers `ReflectionClass(object)` while preserving object operands for runtime class metadata. fn lower_reflection_class_constructor_operands( ctx: &mut LoweringContext<'_, '_>, args: &[Expr], @@ -9244,6 +9244,12 @@ fn lower_reflection_class_constructor_operands( let reflected_arg = reflection_class_constructor_class_arg(ctx, args)?; let class_name = instance_callable_object_class(ctx, &reflected_arg)?; let lowered = lower_expr(ctx, &reflected_arg); + if matches!( + ctx.builder.value_php_type(lowered.value).codegen_repr(), + PhpType::Object(_) + ) { + return Some(vec![lowered.value]); + } if ctx.value_is_owning_temporary(lowered) { crate::ir_lower::ownership::release_if_owned(ctx, lowered, Some(reflected_arg.span)); } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index b9f434099a..332b2246b7 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -4507,6 +4507,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } for class_name in [ "ReflectionClass", + "ReflectionObject", "ReflectionFunction", "ReflectionMethod", "ReflectionProperty", @@ -4525,6 +4526,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if matches!( class_name, "ReflectionClass" + | "ReflectionObject" | "ReflectionFunction" | "ReflectionMethod" | "ReflectionProperty" @@ -4552,7 +4554,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getExtension")) { sig.return_type = PhpType::Mixed; } - if class_name == "ReflectionClass" { + if matches!(class_name, "ReflectionClass" | "ReflectionObject") { for (property_name, property_type) in &mut class_info.properties { match property_name.as_str() { "__interfaces" | "__traits" => { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index bb34b2e3b4..0eccb37e34 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1813,6 +1813,45 @@ echo $iface->isInstance(StaticInstanceEnum::Ready) ? "N" : "n"; assert_eq!(out.stdout, "BcItEN"); } +/// Verifies ReflectionObject and object-argument ReflectionClass construction use +/// the runtime object class rather than the declared static return type. +#[test] +fn test_reflection_object_uses_runtime_class_metadata() { + let out = compile_and_run_capture( + r#"getName(); +echo ":" . $objectRef->getParentClass()->getName(); +$classRef = new ReflectionClass($object); +echo ":" . get_class($classRef); +echo ":" . $classRef->getName(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionObject:OC:StaticReflectObjectChild:StaticReflectObjectBase:ReflectionClass:StaticReflectObjectChild" + ); +} + /// Verifies that `ReflectionClass::getParentClass()` returns a ReflectionClass /// object for subclasses and `false` for parentless classes. #[test] From 36134abe0a92a79901a436d3c31041895b71e249 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 14:10:25 +0200 Subject: [PATCH 0631/1208] Cover ReflectionObject inherited metadata --- tests/codegen/oop/attributes.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 0eccb37e34..3219997adb 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1836,6 +1836,8 @@ echo $objectRef instanceof ReflectionObject ? "O" : "o"; echo $objectRef instanceof ReflectionClass ? "C" : "c"; echo ":" . $objectRef->getName(); echo ":" . $objectRef->getParentClass()->getName(); +echo ":" . ($objectRef->hasMethod("childMethod") ? "M" : "m"); +echo ":" . ($objectRef->hasProperty("value") ? "P" : "p"); $classRef = new ReflectionClass($object); echo ":" . get_class($classRef); echo ":" . $classRef->getName(); @@ -1848,7 +1850,7 @@ echo ":" . $classRef->getName(); ); assert_eq!( out.stdout, - "ReflectionObject:OC:StaticReflectObjectChild:StaticReflectObjectBase:ReflectionClass:StaticReflectObjectChild" + "ReflectionObject:OC:StaticReflectObjectChild:StaticReflectObjectBase:M:P:ReflectionClass:StaticReflectObjectChild" ); } From cf790392f87a2ecb4d924d7f132661515d46731a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 14:21:01 +0200 Subject: [PATCH 0632/1208] Type ReflectionObject newInstance results --- docs/php/classes.md | 1 + docs/php/eval.md | 3 ++- src/types/checker/builtin_types/reflection.rs | 4 ++-- tests/codegen/oop/attributes.rs | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index b59e556389..d447753713 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1146,6 +1146,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | Reflection method | Supported constructor | Description | |---|---|---| +| `ReflectionObject::*` inherited class metadata and construction methods | `new ReflectionObject($object)` | Use the same reflected class metadata and construction helpers as `ReflectionClass`, with the reflected class taken from the object's runtime class id | | `ReflectionClass::getName()` | `new ReflectionClass($class_name)` | Return the resolved class-like name | | `ReflectionClass::getShortName()` | `new ReflectionClass($class_name)` | Return the class-like name after the final namespace separator | | `ReflectionClass::getNamespaceName()` | `new ReflectionClass($class_name)` | Return the namespace portion of the reflected class-like name | diff --git a/docs/php/eval.md b/docs/php/eval.md index 90423bd35f..ffdabf5c72 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -212,7 +212,8 @@ eval call site, and line numbers are one-based inside the evaluated fragment. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. `ReflectionObject` construction accepts object arguments and exposes the -same class metadata through a `ReflectionObject` instance. +same class metadata and inherited construction helpers through a +`ReflectionObject` instance. `ReflectionEnum` construction accepts enum-name strings for eval-declared enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and `getBackingType()` for eval enum metadata, returning `ReflectionEnumUnitCase`, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 332b2246b7..92324d19aa 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -475,7 +475,7 @@ fn builtin_reflection_class_new_instance_method() -> ClassMethod { param_attributes: Vec::new(), variadic: Some("args".to_string()), variadic_type: None, - return_type: Some(mixed_type()), + return_type: Some(object_type()), by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( @@ -4659,7 +4659,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Int; } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("newInstance")) { - sig.return_type = PhpType::Mixed; + sig.return_type = PhpType::Object(String::new()); sig.variadic = Some("args".to_string()); let variadic_default = Some(Expr::new( ExprKind::ArrayLiteral(Vec::new()), diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 3219997adb..cbf020546c 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -1838,6 +1838,8 @@ echo ":" . $objectRef->getName(); echo ":" . $objectRef->getParentClass()->getName(); echo ":" . ($objectRef->hasMethod("childMethod") ? "M" : "m"); echo ":" . ($objectRef->hasProperty("value") ? "P" : "p"); +$made = $objectRef->newInstance(); +echo ":" . get_class($made); $classRef = new ReflectionClass($object); echo ":" . get_class($classRef); echo ":" . $classRef->getName(); @@ -1850,7 +1852,7 @@ echo ":" . $classRef->getName(); ); assert_eq!( out.stdout, - "ReflectionObject:OC:StaticReflectObjectChild:StaticReflectObjectBase:M:P:ReflectionClass:StaticReflectObjectChild" + "ReflectionObject:OC:StaticReflectObjectChild:StaticReflectObjectBase:M:P:StaticReflectObjectChild:ReflectionClass:StaticReflectObjectChild" ); } From bae5aa9bf4a5ace881dc74757a581dee2a1ff00a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 14:40:43 +0200 Subject: [PATCH 0633/1208] Support ReflectionObject construction helpers --- docs/php/classes.md | 2 +- docs/php/eval.md | 5 ++- src/codegen/lower_inst/builtins/types.rs | 41 ++++++++++++++---- src/ir_lower/expr/mod.rs | 25 ++++++----- tests/codegen/oop.rs | 2 + tests/codegen/oop/reflection_construction.rs | 45 ++++++++++++++++++++ 6 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 tests/codegen/oop/reflection_construction.rs diff --git a/docs/php/classes.md b/docs/php/classes.md index d447753713..5304a5b0c7 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1146,7 +1146,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | Reflection method | Supported constructor | Description | |---|---|---| -| `ReflectionObject::*` inherited class metadata and construction methods | `new ReflectionObject($object)` | Use the same reflected class metadata and construction helpers as `ReflectionClass`, with the reflected class taken from the object's runtime class id | +| `ReflectionObject::*` inherited class metadata and construction methods | `new ReflectionObject($object)` | Use the same reflected class metadata as `ReflectionClass`; positional `newInstance()`, positional `newInstanceArgs()`, and `newInstanceWithoutConstructor()` use the object's runtime class id | | `ReflectionClass::getName()` | `new ReflectionClass($class_name)` | Return the resolved class-like name | | `ReflectionClass::getShortName()` | `new ReflectionClass($class_name)` | Return the class-like name after the final namespace separator | | `ReflectionClass::getNamespaceName()` | `new ReflectionClass($class_name)` | Return the namespace portion of the reflected class-like name | diff --git a/docs/php/eval.md b/docs/php/eval.md index ffdabf5c72..e695f95a7e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -212,8 +212,9 @@ eval call site, and line numbers are one-based inside the evaluated fragment. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. `ReflectionObject` construction accepts object arguments and exposes the -same class metadata and inherited construction helpers through a -`ReflectionObject` instance. +same class metadata through a `ReflectionObject` instance. Its inherited +positional `newInstance()`, positional `newInstanceArgs()`, and +`newInstanceWithoutConstructor()` helpers use the object's runtime class id. `ReflectionEnum` construction accepts enum-name strings for eval-declared enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and `getBackingType()` for eval enum metadata, returning `ReflectionEnumUnitCase`, diff --git a/src/codegen/lower_inst/builtins/types.rs b/src/codegen/lower_inst/builtins/types.rs index 3c00f3d4d5..bf2b081e10 100644 --- a/src/codegen/lower_inst/builtins/types.rs +++ b/src/codegen/lower_inst/builtins/types.rs @@ -345,20 +345,17 @@ pub(crate) fn lower_class_name_lookup( ctx.load_value_to_result(value)?; emit_dynamic_object_class_name(ctx, name); } + PhpType::Mixed | PhpType::Union(_) => { + ctx.load_value_to_result(value)?; + emit_mixed_object_class_name(ctx, name); + } PhpType::Str if name == "get_parent_class" => { let class_name = const_string_operand(ctx, value)?; let parent = parent_of(ctx, &class_name); emit_string_result(ctx, parent.as_bytes()); } - other => { + _ => { ctx.load_value_to_result(value)?; - if matches!(other, PhpType::Mixed | PhpType::Union(_)) { - return Err(CodegenIrError::unsupported(format!( - "{} for PHP type {:?}", - name, - other - ))); - } emit_string_result(ctx, b""); } } @@ -453,6 +450,34 @@ fn emit_dynamic_object_class_name(ctx: &mut FunctionContext<'_>, name: &str) { } } +/// Emits class-name lookup for a boxed Mixed value that may contain an object. +fn emit_mixed_object_class_name(ctx: &mut FunctionContext<'_>, name: &str) { + let empty_label = ctx.next_label("get_class_mixed_empty"); + let done_label = ctx.next_label("get_class_mixed_done"); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // require a boxed object payload for class-name lookup + ctx.emitter + .instruction(&format!("b.ne {}", empty_label)); // non-object Mixed payloads produce an empty class name + ctx.emitter.instruction("mov x0, x1"); // expose the unboxed object pointer to the object lookup path + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // require a boxed object payload for class-name lookup + ctx.emitter + .instruction(&format!("jne {}", empty_label)); // non-object Mixed payloads produce an empty class name + ctx.emitter.instruction("mov rax, rdi"); // expose the unboxed object pointer to the object lookup path + } + } + emit_dynamic_object_class_name(ctx, name); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&empty_label); + emit_string_result(ctx, b""); + + ctx.emitter.label(&done_label); +} + /// Emits AArch64 runtime object class-name lookup for `get_class()` and `get_parent_class()`. fn emit_dynamic_object_class_name_aarch64( ctx: &mut FunctionContext<'_>, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index a8641428a3..40ca872f70 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -12496,11 +12496,7 @@ fn is_reflection_class_new_instance_call( if php_symbol_key(method) != "newinstance" { return false; } - let object_ty = ctx.builder.value_php_type(object); - let Some((class_name, false)) = singular_object_class(&object_ty) else { - return false; - }; - php_symbol_key(class_name.trim_start_matches('\\')) == "reflectionclass" + is_reflection_class_construction_receiver(ctx, object) } /// Returns true when a method call targets `ReflectionClass::newInstanceArgs()`. @@ -12512,11 +12508,7 @@ fn is_reflection_class_new_instance_args_call( if php_symbol_key(method) != "newinstanceargs" { return false; } - let object_ty = ctx.builder.value_php_type(object); - let Some((class_name, false)) = singular_object_class(&object_ty) else { - return false; - }; - php_symbol_key(class_name.trim_start_matches('\\')) == "reflectionclass" + is_reflection_class_construction_receiver(ctx, object) } /// Returns true when a method call targets `ReflectionClass::newInstanceWithoutConstructor()`. @@ -12528,11 +12520,22 @@ fn is_reflection_class_new_instance_without_constructor_call( if php_symbol_key(method) != "newinstancewithoutconstructor" { return false; } + is_reflection_class_construction_receiver(ctx, object) +} + +/// Returns true when a receiver can use ReflectionClass construction helper lowering. +fn is_reflection_class_construction_receiver( + ctx: &LoweringContext<'_, '_>, + object: ValueId, +) -> bool { let object_ty = ctx.builder.value_php_type(object); let Some((class_name, false)) = singular_object_class(&object_ty) else { return false; }; - php_symbol_key(class_name.trim_start_matches('\\')) == "reflectionclass" + matches!( + php_symbol_key(class_name.trim_start_matches('\\')).as_str(), + "reflectionclass" | "reflectionobject" + ) } /// Emits the PHP fatal terminator for an ordinary method call on null. diff --git a/tests/codegen/oop.rs b/tests/codegen/oop.rs index 5e799d4b9e..3ed007614b 100644 --- a/tests/codegen/oop.rs +++ b/tests/codegen/oop.rs @@ -49,3 +49,5 @@ mod reflection_properties; mod reflection_functions; #[path = "oop/reflection_methods.rs"] mod reflection_methods; +#[path = "oop/reflection_construction.rs"] +mod reflection_construction; diff --git a/tests/codegen/oop/reflection_construction.rs b/tests/codegen/oop/reflection_construction.rs new file mode 100644 index 0000000000..2a3d27beb1 --- /dev/null +++ b/tests/codegen/oop/reflection_construction.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! End-to-end codegen tests for ReflectionClass and ReflectionObject +//! construction helpers over reflected class metadata. +//! +//! Called from: +//! - `cargo test` through Rust's test harness. +//! +//! Key details: +//! - Covers inherited ReflectionObject construction helpers that must route +//! through the same dynamic class-name lowering as ReflectionClass. + +use super::*; + +/// Verifies `ReflectionObject` inherits working construction helpers from `ReflectionClass`. +#[test] +fn test_reflection_object_construction_helpers_use_runtime_class() { + let out = compile_and_run_capture( + r#"newInstance("A", "B"); +echo get_class($first) . ":"; +$second = $ref->newInstanceArgs(["X", "Y"]); +echo get_class($second) . ":"; +$third = $ref->newInstanceWithoutConstructor(); +echo get_class($third); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "IN|AB|ReflectObjectConstructChild:XY|ReflectObjectConstructChild:ReflectObjectConstructChild" + ); +} From e8381dbeb818bc7e3ff67b2f4ca6e8186375123f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 14:48:09 +0200 Subject: [PATCH 0634/1208] Track ReflectionObject construction targets --- docs/php/classes.md | 4 +- docs/php/eval.md | 6 +- src/ir_lower/expr/mod.rs | 58 +++++++++++++++++++- tests/codegen/oop/reflection_construction.rs | 10 ++-- 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 5304a5b0c7..16dbee7e30 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1146,7 +1146,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | Reflection method | Supported constructor | Description | |---|---|---| -| `ReflectionObject::*` inherited class metadata and construction methods | `new ReflectionObject($object)` | Use the same reflected class metadata as `ReflectionClass`; positional `newInstance()`, positional `newInstanceArgs()`, and `newInstanceWithoutConstructor()` use the object's runtime class id | +| `ReflectionObject::*` inherited class metadata and construction methods | `new ReflectionObject($object)` | Use the same reflected class metadata as `ReflectionClass`; construction helpers use the object's runtime class id, and straight-line receivers built from statically typed objects normalize named constructor arguments | | `ReflectionClass::getName()` | `new ReflectionClass($class_name)` | Return the resolved class-like name | | `ReflectionClass::getShortName()` | `new ReflectionClass($class_name)` | Return the class-like name after the final namespace separator | | `ReflectionClass::getNamespaceName()` | `new ReflectionClass($class_name)` | Return the namespace portion of the reflected class-like name | @@ -1384,6 +1384,8 @@ Limitations today: - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. +- `ReflectionObject` construction helpers normalize named constructor arguments through the reflected constructor signature for straight-line receivers built from statically typed objects. + `ReflectionParameter::canBePassedByValue()`, `allowsNull()`, `isArray()`, `isCallable()`, and `getClass()` are supported for retained parameter metadata. `isArray()` / `isCallable()` follow PHP's legacy behavior: nullable or diff --git a/docs/php/eval.md b/docs/php/eval.md index e695f95a7e..70343e86dc 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -213,8 +213,10 @@ eval call site, and line numbers are one-based inside the evaluated fragment. object arguments reflect the runtime class of eval-created or generated/AOT objects. `ReflectionObject` construction accepts object arguments and exposes the same class metadata through a `ReflectionObject` instance. Its inherited -positional `newInstance()`, positional `newInstanceArgs()`, and -`newInstanceWithoutConstructor()` helpers use the object's runtime class id. +`newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()` +helpers use the object's runtime class id. Straight-line `ReflectionObject` +receivers built from statically typed objects also normalize named constructor +arguments through the reflected constructor signature. `ReflectionEnum` construction accepts enum-name strings for eval-declared enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and `getBackingType()` for eval enum metadata, returning `ReflectionEnumUnitCase`, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 40ca872f70..70e74b8bb3 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -12341,9 +12341,18 @@ fn reflection_class_new_instance_reflected_class( let ExprKind::NewObject { class_name, args } = &object_expr.kind else { return None; }; - if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionclass" { - return None; + match php_symbol_key(class_name.as_str().trim_start_matches('\\')).as_str() { + "reflectionclass" => reflection_class_reflected_class_from_args(ctx, args), + "reflectionobject" => reflection_object_reflected_class_from_args(ctx, args), + _ => None, } +} + +/// Resolves the target class from a static `ReflectionClass(...)` argument list. +fn reflection_class_reflected_class_from_args( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { let reflected_arg = reflection_class_constructor_class_arg(ctx, args)?; let raw_class_name = match &reflected_arg.kind { ExprKind::StringLiteral(value) => value.clone(), @@ -12353,6 +12362,15 @@ fn reflection_class_new_instance_reflected_class( resolve_known_class_name(ctx, &raw_class_name) } +/// Resolves the target class from a static `ReflectionObject(...)` argument list. +fn reflection_object_reflected_class_from_args( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { + let object_arg = reflection_object_constructor_object_arg(ctx, args)?; + isset_object_expr_class(ctx, &object_arg).map(|(class_name, _)| class_name) +} + /// Resolves a reflected class from an inline constructor or tracked local receiver. fn reflection_class_reflected_class( ctx: &LoweringContext<'_, '_>, @@ -12403,6 +12421,42 @@ fn reflection_class_constructor_class_arg( planned_regular_arg_expr(plan.regular_args.first()?).cloned() } +/// Returns the `ReflectionObject::__construct()` object argument after normalization. +fn reflection_object_constructor_object_arg( + ctx: &LoweringContext<'_, '_>, + args: &[Expr], +) -> Option { + let args = reflection_class_new_instance_args(args); + if args.iter().any(is_spread_arg) { + return None; + } + if !crate::types::call_args::has_named_args(&args) { + return args.first().cloned(); + } + let sig = ctx + .classes + .get("ReflectionObject") + .and_then(|class_info| class_info.methods.get("__construct"))?; + let call_span = args + .first() + .map(|arg| arg.span) + .unwrap_or_else(crate::span::Span::dummy); + let plan = crate::types::call_args::plan_call_args_with_regular_param_count_and_assoc_spreads( + sig, + &args, + call_span, + crate::types::call_args::regular_param_count(sig), + false, + true, + &assoc_spread_sources(ctx, &args), + ) + .ok()?; + if plan.has_spread_args() { + return None; + } + planned_regular_arg_expr(plan.regular_args.first()?).cloned() +} + /// Resolves a PHP class name case-insensitively against known class metadata. fn resolve_known_class_name(ctx: &LoweringContext<'_, '_>, class_name: &str) -> Option { let key = php_symbol_key(class_name.trim_start_matches('\\')); diff --git a/tests/codegen/oop/reflection_construction.rs b/tests/codegen/oop/reflection_construction.rs index 2a3d27beb1..5fe7c23b62 100644 --- a/tests/codegen/oop/reflection_construction.rs +++ b/tests/codegen/oop/reflection_construction.rs @@ -27,10 +27,12 @@ $object = new ReflectObjectConstructChild("I", "N"); $ref = new ReflectionObject($object); $first = $ref->newInstance("A", "B"); echo get_class($first) . ":"; -$second = $ref->newInstanceArgs(["X", "Y"]); +$second = $ref->newInstanceArgs(["right" => "Y", "left" => "X"]); echo get_class($second) . ":"; -$third = $ref->newInstanceWithoutConstructor(); -echo get_class($third); +$third = $ref->newInstance(right: "N", left: "M"); +echo get_class($third) . ":"; +$fourth = $ref->newInstanceWithoutConstructor(); +echo get_class($fourth); "#, ); assert!( @@ -40,6 +42,6 @@ echo get_class($third); ); assert_eq!( out.stdout, - "IN|AB|ReflectObjectConstructChild:XY|ReflectObjectConstructChild:ReflectObjectConstructChild" + "IN|AB|ReflectObjectConstructChild:XY|ReflectObjectConstructChild:MN|ReflectObjectConstructChild:ReflectObjectConstructChild" ); } From 76ebb09cd968ff647248f05710469140f36fdf6c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 14:56:04 +0200 Subject: [PATCH 0635/1208] Support reflected untyped AOT invocation --- docs/php/classes.md | 8 ++-- docs/php/eval.md | 14 +++--- src/ir_lower/expr/mod.rs | 57 ----------------------- tests/codegen/oop/reflection_functions.rs | 11 ++--- tests/codegen/oop/reflection_methods.rs | 11 ++--- 5 files changed, 19 insertions(+), 82 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 16dbee7e30..66cfb43dc0 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1206,7 +1206,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::hasReturnType()` / `getReturnType()` | `new ReflectionFunction($function_name)` | Return retained declared named, nullable, union, `void`, or `never` return type metadata as `ReflectionNamedType`, `ReflectionUnionType`, or `null` when no supported return type is declared | | `ReflectionFunction::isVariadic()` | `new ReflectionFunction($function_name)` | Return whether the reflected function declares a variadic parameter | | `ReflectionFunction::getClosureUsedVariables()` | `new ReflectionFunction($function_name)` | Return an empty array for supported non-closure function reflectors | -| `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared parameter types and supported callable builtins are also lowered | +| `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared or inferred/untyped parameter contracts and supported callable builtins are also lowered | | `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | | `ReflectionMethod::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP method-name metadata; methods report an empty namespace and `false` for `inNamespace()` | | `ReflectionMethod::isInternal()` / `isUserDefined()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return origin predicates for supported reflected methods | @@ -1225,7 +1225,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionMethod::isDestructor()` | `new ReflectionMethod($class_name, $method_name)` | Return whether the reflected method is the destructor | | `ReflectionMethod::getModifiers()` | `new ReflectionMethod($class_name, $method_name)` | Return PHP's `ReflectionMethod::IS_*` visibility/static/finality/abstract bitmask | | `ReflectionMethod::setAccessible()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Accepted as a PHP-compatible no-op for eval-backed and generated/AOT method reflection | -| `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments; inline/tracked generated/AOT methods with declared parameter types are also lowered | +| `ReflectionMethod::invoke()` / `invokeArgs()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Invoke eval-declared reflected methods with PHP visibility-bypassing reflection semantics and forwarded named arguments; inline/tracked generated/AOT methods with declared or inferred/untyped parameter contracts are also lowered | | `ReflectionMethod::getParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return `ReflectionParameter` objects for the reflected method parameters | | `ReflectionMethod::getNumberOfParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the total number of reflected method parameters | | `ReflectionMethod::getNumberOfRequiredParameters()` | `new ReflectionMethod($class_name, $method_name)` | Return the number of required reflected method parameters | @@ -1379,8 +1379,8 @@ Limitations today: - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. -- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected function has declared parameter types, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. -- `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors when the reflected method has declared parameter types. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays. Invoke targets whose parameter types only come from call-site inference and runtime-only or non-literal argument arrays still require richer runtime/typechecker support. +- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Runtime-only or non-literal argument arrays still require richer runtime/typechecker support. +- `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index 70343e86dc..7a75112964 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -361,14 +361,14 @@ preserve named arguments for the invoked method, follow PHP's by-value and throw catchable `ReflectionException` values when an instance receiver is not compatible with the reflected declaring class. For generated/AOT classes, `ReflectionMethod::invoke()` and `invokeArgs()` are also lowered for inline or straight-line -tracked reflectors when the reflected method has declared parameter types; -the lowered call supports instance and static methods, constructors returned by -`ReflectionClass::getConstructor()`, method-name -case-insensitivity, defaults, and named arguments. Generated/AOT +tracked reflectors with declared or inferred/untyped parameter contracts; untyped +parameters are forwarded through the boxed `Mixed` ABI used by EIR. The lowered +call supports instance and static methods, constructors returned by +`ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, +and named arguments. Generated/AOT bridge-supported invoke targets also bypass public/protected/private visibility -like PHP reflection. Generated/AOT invoke targets whose parameter types only -come from call-site inference and runtime-only or non-literal argument arrays -still require richer runtime/typechecker support. +like PHP reflection. Runtime-only or non-literal argument arrays still require +richer runtime/typechecker support. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 70e74b8bb3..5dfdfa8948 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10500,13 +10500,6 @@ fn lower_reflection_function_invoke_call( expr, )); } - if !reflection_user_function_target_has_declared_params(ctx, &function_name) { - return Some(lower_reflection_function_invoke_unsupported( - ctx, - &method_key, - expr, - )); - } let name = Name::from(function_name); Some(lower_function_call(ctx, &name, &forwarded_args, expr)) } @@ -10578,19 +10571,6 @@ fn reflection_function_invoke_args_array( reflection_class_new_instance_args_value(ctx, forwarded_arg) } -/// Returns true when reflected invocation can trust a user function's parameter types. -fn reflection_user_function_target_has_declared_params( - ctx: &LoweringContext<'_, '_>, - function_name: &str, -) -> bool { - ctx.functions.get(function_name).is_some_and(|signature| { - signature - .declared_params - .iter() - .all(|is_declared| *is_declared) - }) -} - /// Emits a runtime fatal for ReflectionFunction invocation forms not yet lowered. fn lower_reflection_function_invoke_unsupported( ctx: &mut LoweringContext<'_, '_>, @@ -10633,15 +10613,6 @@ fn lower_reflection_method_invoke_call( else { return Some(lower_reflection_method_invoke_unsupported(ctx, &method_key, expr)); }; - if !reflection_method_target_has_declared_params( - ctx, - &class_name, - &reflected_method, - target_kind, - ) - { - return Some(lower_reflection_method_invoke_unsupported(ctx, &method_key, expr)); - } match target_kind { ReflectionMethodTargetKind::Static => Some(lower_reflection_static_method_invoke( ctx, @@ -10805,34 +10776,6 @@ fn reflection_method_target_kind( None } -/// Returns true when reflected invocation can trust the target method's parameter types. -fn reflection_method_target_has_declared_params( - ctx: &LoweringContext<'_, '_>, - class_name: &str, - method: &str, - target_kind: ReflectionMethodTargetKind, -) -> bool { - let signature = match target_kind { - ReflectionMethodTargetKind::Instance => { - class_method_signature( - ctx, - class_name.trim_start_matches('\\'), - &php_symbol_key(method), - ) - } - ReflectionMethodTargetKind::Static => { - let receiver = StaticReceiver::Named(Name::from(class_name.to_string())); - static_method_implementation_signature(ctx, &receiver, method) - } - }; - signature.is_some_and(|signature| { - signature - .declared_params - .iter() - .all(|is_declared| *is_declared) - }) -} - /// Dispatch kind for a statically-known reflected method. #[derive(Clone, Copy)] enum ReflectionMethodTargetKind { diff --git a/tests/codegen/oop/reflection_functions.rs b/tests/codegen/oop/reflection_functions.rs index 6517a1b382..ec67997134 100644 --- a/tests/codegen/oop/reflection_functions.rs +++ b/tests/codegen/oop/reflection_functions.rs @@ -231,10 +231,10 @@ echo $ref->invokeArgs(args: ["Q"]); assert_eq!(out, "XY:OP:AC:QB"); } -/// Verifies inferred AOT function signatures are rejected instead of miscompiled. +/// Verifies `ReflectionFunction::invoke()` supports inferred AOT signatures. #[test] -fn test_reflection_function_invoke_rejects_inferred_aot_signature() { - let err = compile_and_run_expect_failure( +fn test_reflection_function_invoke_calls_inferred_aot_signature() { + let out = compile_and_run( r#"invoke("A", "B"); "#, ); - assert!( - err.contains("Fatal error: unsupported ReflectionFunction::invoke()"), - "unexpected stderr: {err}" - ); + assert_eq!(out, "AB"); } diff --git a/tests/codegen/oop/reflection_methods.rs b/tests/codegen/oop/reflection_methods.rs index 77d5342aa3..b9c8c0791f 100644 --- a/tests/codegen/oop/reflection_methods.rs +++ b/tests/codegen/oop/reflection_methods.rs @@ -285,10 +285,10 @@ echo ":" . $listed->invoke($object); assert_eq!(out, "M:secret:L:secret"); } -/// Verifies inferred AOT method signatures are rejected instead of miscompiled. +/// Verifies `ReflectionMethod::invoke()` supports inferred AOT signatures. #[test] -fn test_reflection_method_invoke_rejects_inferred_aot_signature() { - let err = compile_and_run_expect_failure( +fn test_reflection_method_invoke_calls_inferred_aot_signature() { + let out = compile_and_run( r#"invoke($object, "A", "B"); "#, ); - assert!( - err.contains("Fatal error: unsupported ReflectionMethod::invoke()"), - "unexpected stderr: {err}" - ); + assert_eq!(out, "AB"); } From 2c667262d97ac81fc3e05d02cbf4a1ed33bbc146 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 15:07:14 +0200 Subject: [PATCH 0636/1208] Support runtime static ReflectionProperty reads --- docs/php/classes.md | 8 +-- src/types/checker/builtin_types/reflection.rs | 54 ++++++++++++++++++- tests/codegen/oop/reflection_construction.rs | 25 +++++++++ tests/codegen/oop/reflection_properties.rs | 36 +++++++++++++ 4 files changed, 117 insertions(+), 6 deletions(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 66cfb43dc0..2bb2cfcfbc 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1281,9 +1281,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionProperty::getSettableType()` | Same as `ReflectionProperty::hasType()` | Return the retained settable property type as a `ReflectionNamedType`, `ReflectionUnionType`, or `ReflectionIntersectionType`, or `null` when no type is declared; for the supported property surface this currently matches `getType()` | | `ReflectionProperty::hasDefaultValue()` | Same as `ReflectionProperty::hasType()` | Return whether the reflected property has a materialized default value; untyped concrete properties without an explicit initializer report PHP's implicit `null` default | | `ReflectionProperty::getDefaultValue()` | Same as `ReflectionProperty::hasDefaultValue()` | Return supported materialized property defaults, or `null` when no default is available | -| `ReflectionProperty::isInitialized()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` for supported known properties | Return whether a supported instance/static property slot is initialized without reading the property value | -| `ReflectionProperty::getValue()` | `ReflectionProperty` for instance properties with an explicit object argument, or inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for known static properties | Read supported instance/static property storage; positional and named arguments are normalized | -| `ReflectionProperty::setValue()` | Same as `ReflectionProperty::getValue()` | Write supported instance/static property storage | +| `ReflectionProperty::isInitialized()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` for supported known properties; runtime-held static reflectors use the materialized declaring-class snapshot | Return whether a supported instance/static property slot is initialized without reading the property value | +| `ReflectionProperty::getValue()` | `ReflectionProperty` for instance properties with an explicit object argument, inline `new ReflectionProperty($class_name, $property_name)` / `ReflectionClass::getProperty()` for known static properties, or runtime-held static reflectors backed by the declaring-class snapshot | Read supported instance/static property storage; positional and named arguments are normalized | +| `ReflectionProperty::setValue()` | Instance reflectors with an explicit object/value argument, or inline/tracked known static property reflectors | Write supported instance/static property storage | | `ReflectionProperty::setAccessible()` | `new ReflectionProperty($class_name, $property_name)` or `ReflectionClass::getProperty()` / `getProperties()` | Accepted as a PHP-compatible no-op for eval-backed and generated/AOT property reflection | | `ReflectionClassConstant::getName()` | `new ReflectionClassConstant($class_name, $constant_name)` or `ReflectionClass::getReflectionConstant()` / `getReflectionConstants()` | Return the reflected class constant or enum-case name | | `ReflectionClassConstant::getAttributes()` | Same as `ReflectionClassConstant::getName()` | Return `ReflectionAttribute` objects for class-constant or enum-case attributes | @@ -1378,7 +1378,7 @@ Limitations today: - `ReflectionEnum` additionally supports backing metadata (`isBacked()`, `getBackingType()`); inside eval fragments it also supports enum-case lookup (`hasCase()`, `getCase()`, `getCases()`). Enum-case reflectors expose `getEnum()`. - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. -- `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Dynamic static-property lookup from reflector objects without a compile-time-tracked target is not yet available. +- `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Runtime-held static `ReflectionProperty` objects can read `getValue()` and `isInitialized()` from the materialized declaring-class static-property snapshot. Live refresh after reflector construction and dynamic static `setValue()` still require richer runtime metadata. - `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 92324d19aa..95b5256453 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3430,7 +3430,7 @@ fn builtin_reflection_property_get_value_method() -> ClassMethod { variadic_type: None, return_type: Some(mixed_type()), body: vec![ - reflection_property_static_value_guard("getValue", dummy_span), + reflection_property_static_get_value_return(dummy_span), reflection_property_object_required_guard("getValue", dummy_span), Stmt::new( StmtKind::Return(Some(reflection_dynamic_object_property(object, dummy_span))), @@ -3487,6 +3487,7 @@ fn builtin_reflection_property_set_value_method() -> ClassMethod { /// Returns `ReflectionProperty::isInitialized()` for supported materialized reflectors. fn builtin_reflection_property_is_initialized_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); + let static_return = reflection_property_static_is_initialized_return(dummy_span); let dynamic_return = Stmt::new( StmtKind::If { condition: reflection_this_property("__is_dynamic", dummy_span), @@ -3518,7 +3519,7 @@ fn builtin_reflection_property_is_initialized_method() -> ClassMethod { variadic_type: None, return_type: Some(bool_type()), body: vec![ - reflection_property_static_value_guard("isInitialized", dummy_span), + static_return, reflection_property_object_required_guard("isInitialized", dummy_span), dynamic_return, defaulted_return, @@ -3535,6 +3536,55 @@ fn builtin_reflection_property_is_initialized_method() -> ClassMethod { } } +/// Returns a static-property `getValue()` branch backed by the declaring ReflectionClass snapshot. +fn reflection_property_static_get_value_return(span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_static", span), + then_body: vec![Stmt::new( + StmtKind::Return(Some(method_call_expr( + reflection_this_property("__declaring_class", span), + "getStaticPropertyValue", + vec![reflection_this_property("__name", span)], + span, + ))), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + +/// Returns a static-property `isInitialized()` branch backed by materialized static values. +fn reflection_property_static_is_initialized_return(span: crate::span::Span) -> Stmt { + Stmt::new( + StmtKind::If { + condition: reflection_this_property("__is_static", span), + then_body: vec![Stmt::new( + StmtKind::Return(Some(function_call( + "array_key_exists", + vec![ + reflection_this_property("__name", span), + method_call_expr( + reflection_this_property("__declaring_class", span), + "getStaticProperties", + Vec::new(), + span, + ), + ], + span, + ))), + span, + )], + elseif_clauses: Vec::new(), + else_body: None, + }, + span, + ) +} + /// Builds a guard for static property value access that still needs inline lowering. fn reflection_property_static_value_guard(method: &str, span: crate::span::Span) -> Stmt { Stmt::new( diff --git a/tests/codegen/oop/reflection_construction.rs b/tests/codegen/oop/reflection_construction.rs index 5fe7c23b62..48448e7b8b 100644 --- a/tests/codegen/oop/reflection_construction.rs +++ b/tests/codegen/oop/reflection_construction.rs @@ -45,3 +45,28 @@ echo get_class($fourth); "IN|AB|ReflectObjectConstructChild:XY|ReflectObjectConstructChild:MN|ReflectObjectConstructChild:ReflectObjectConstructChild" ); } + +/// Verifies `ReflectionClass` construction helpers support inferred constructor signatures. +#[test] +fn test_reflection_class_construction_helpers_call_inferred_constructor_signature() { + let out = compile_and_run_capture( + r#"newInstance("A", "C"); +$ref->newInstanceArgs(["right" => "Y", "left" => "X"]); +$ref->newInstance(right: "N", left: "M"); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "AC|XY|MN|"); +} diff --git a/tests/codegen/oop/reflection_properties.rs b/tests/codegen/oop/reflection_properties.rs index 516305b447..f1f9c8c950 100644 --- a/tests/codegen/oop/reflection_properties.rs +++ b/tests/codegen/oop/reflection_properties.rs @@ -331,6 +331,42 @@ inspect_static_props(new ReflectionClass(ReflectRuntimeStaticPropertiesTarget::c assert_eq!(out, "3:5:new:null:missing:5:null:fallback"); } +/// Verifies runtime-held static `ReflectionProperty` objects can read +/// materialized static values and report initialization from their declaring class. +#[test] +fn test_reflection_property_runtime_static_reflectors_read_materialized_values() { + let out = compile_and_run( + r#"getValue(); + echo ":" . ($count->isInitialized() ? "count" : "bad"); + echo ":" . $label->getValue(null); + echo ":" . ($label->isInitialized(object: null) ? "label" : "bad"); + echo ":" . ($unset->isInitialized() ? "bad" : "unset"); +} + +ReflectRuntimeStaticValuePropertyTarget::$count = 7; +ReflectRuntimeStaticValuePropertyTarget::rename("new"); +inspect_static_reflectors( + new ReflectionProperty(ReflectRuntimeStaticValuePropertyTarget::class, "count"), + new ReflectionProperty(ReflectRuntimeStaticValuePropertyTarget::class, "label"), + new ReflectionProperty(ReflectRuntimeStaticValuePropertyTarget::class, "unset") +); +"#, + ); + assert_eq!(out, "7:count:new:label:unset"); +} + /// Verifies runtime-held `ReflectionProperty` objects can read and write public /// instance properties through their retained property names. #[test] From e6c4e729c16521a3f210eac3a099b91751742160 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 15:40:44 +0200 Subject: [PATCH 0637/1208] Support named attribute metadata --- crates/elephc-magician/src/eval_ir.rs | 22 ++++ .../elephc-magician/src/ffi/native_methods.rs | 9 ++ crates/elephc-magician/src/ffi/tests.rs | 13 ++ .../interpreter/builtins/class_metadata.rs | 10 +- .../src/interpreter/statements.rs | 27 ++-- .../tests/builtins_class_metadata.rs | 30 ++++- .../interpreter/tests/support/array_ops.rs | 43 ++++-- .../elephc-magician/src/parser/statements.rs | 16 ++- .../src/parser/tests/classes_errors.rs | 8 +- docs/php/classes.md | 23 ++-- docs/php/eval.md | 13 +- src/codegen/lower_inst/builtins/attributes.rs | 122 ++++++++++++++---- src/codegen/lower_inst/builtins/eval.rs | 6 + src/ir_lower/expr/mod.rs | 30 +++-- src/types/checker/builtin_types/reflection.rs | 20 ++- tests/codegen/oop/attributes.rs | 58 +++++++++ 16 files changed, 359 insertions(+), 91 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 0498f80a35..4578bdf11e 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -431,6 +431,28 @@ pub enum EvalAttributeArg { Int(i64), Bool(bool), Null, + Named { + name: String, + value: Box, + }, +} + +impl EvalAttributeArg { + /// Returns the PHP named-argument key when this attribute arg is named. + pub fn name(&self) -> Option<&str> { + match self { + EvalAttributeArg::Named { name, .. } => Some(name), + _ => None, + } + } + + /// Returns the scalar payload, unwrapping a named-argument wrapper. + pub fn value(&self) -> &EvalAttributeArg { + match self { + EvalAttributeArg::Named { value, .. } => value, + _ => self, + } + } } /// Attribute metadata retained for eval class-like declarations. diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index db867388d2..9155389eb0 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -32,6 +32,7 @@ const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; +const NATIVE_ATTRIBUTE_ARG_NAMED: u8 = 4; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; @@ -1728,6 +1729,14 @@ fn native_attribute_take_arg(bytes: &[u8], offset: &mut usize) -> Option { native_attribute_take_string(bytes, offset).map(EvalAttributeArg::String) } + NATIVE_ATTRIBUTE_ARG_NAMED => { + let name = native_attribute_take_string(bytes, offset)?; + let value = native_attribute_take_arg(bytes, offset)?; + Some(EvalAttributeArg::Named { + name, + value: Box::new(value), + }) + } _ => None, } } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 499cd95fb7..0e2a6ce95c 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -74,6 +74,11 @@ fn native_member_attribute_push_arg(record: &mut Vec, arg: &EvalAttributeArg record.push(3); native_member_attribute_push_string(record, value); } + EvalAttributeArg::Named { name, value } => { + record.push(4); + native_member_attribute_push_string(record, name); + native_member_attribute_push_arg(record, value); + } } } @@ -720,6 +725,10 @@ fn register_native_member_attribute_records_metadata() { "Route", Some(&[ EvalAttributeArg::String("api".to_string()), + EvalAttributeArg::Named { + name: "path".to_string(), + value: Box::new(EvalAttributeArg::String("/users".to_string())), + }, EvalAttributeArg::Int(7), EvalAttributeArg::Bool(true), EvalAttributeArg::Null, @@ -785,6 +794,10 @@ fn register_native_member_attribute_records_metadata() { Some( [ EvalAttributeArg::String("api".to_string()), + EvalAttributeArg::Named { + name: "path".to_string(), + value: Box::new(EvalAttributeArg::String("/users".to_string())), + }, EvalAttributeArg::Int(7), EvalAttributeArg::Bool(true), EvalAttributeArg::Null, diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index cac5f10d28..c16aef85b8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -229,15 +229,18 @@ fn eval_attribute_is_repeated(attributes: &[EvalAttribute], name: &str) -> bool .is_some() } -/// Builds the indexed mixed array returned by `class_attribute_args()`. +/// Builds the mixed PHP array returned by `class_attribute_args()`. fn eval_class_attribute_args_result( args: &[EvalAttributeArg], values: &mut impl RuntimeValueOps, ) -> Result { let mut result = values.array_new(args.len())?; for (index, arg) in args.iter().enumerate() { - let key = values.int(index as i64)?; - let value = eval_class_attribute_arg_value(arg, values)?; + let key = match arg.name() { + Some(name) => values.string(name)?, + None => values.int(index as i64)?, + }; + let value = eval_class_attribute_arg_value(arg.value(), values)?; result = values.array_set(result, key, value)?; } Ok(result) @@ -253,6 +256,7 @@ fn eval_class_attribute_arg_value( EvalAttributeArg::Int(value) => values.int(*value), EvalAttributeArg::Bool(value) => values.bool_value(*value), EvalAttributeArg::Null => values.null(), + EvalAttributeArg::Named { value, .. } => eval_class_attribute_arg_value(value, values), } } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index a0b7eeec05..28207c6a6a 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4080,16 +4080,10 @@ fn eval_reflection_attribute_new_instance_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let args = eval_reflection_attribute_arg_values(attribute, values)?; + let args = eval_reflection_attribute_evaluated_args(attribute, values)?; if let Some(class) = context.class(attribute.name()).cloned() { let mut scope = ElephcEvalScope::new(); - return eval_dynamic_class_new_object( - &class, - positional_args(args), - context, - &mut scope, - values, - ); + return eval_dynamic_class_new_object(&class, args, context, &mut scope, values); } let class_name = context .resolve_class_name(attribute.name()) @@ -4101,7 +4095,7 @@ fn eval_reflection_attribute_new_instance_result( if let Err(err) = eval_native_constructor_with_evaluated_args( &class_name, object, - positional_args(args), + args, context, values, ) { @@ -4111,16 +4105,22 @@ fn eval_reflection_attribute_new_instance_result( Ok(object) } -/// Materializes eval attribute literal arguments as constructor argument cells. -fn eval_reflection_attribute_arg_values( +/// Materializes eval attribute literal arguments as evaluated constructor args. +fn eval_reflection_attribute_evaluated_args( attribute: &EvalAttribute, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let Some(args) = attribute.args() else { return Err(EvalStatus::RuntimeFatal); }; args.iter() - .map(|arg| eval_reflection_attribute_arg_value(arg, values)) + .map(|arg| { + Ok(EvaluatedCallArg { + name: arg.name().map(str::to_string), + value: eval_reflection_attribute_arg_value(arg.value(), values)?, + ref_target: None, + }) + }) .collect() } @@ -4134,6 +4134,7 @@ fn eval_reflection_attribute_arg_value( EvalAttributeArg::Int(value) => values.int(*value), EvalAttributeArg::Bool(value) => values.bool_value(*value), EvalAttributeArg::Null => values.null(), + EvalAttributeArg::Named { value, .. } => eval_reflection_attribute_arg_value(value, values), } } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 009efff171..986ccf4801 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -190,6 +190,34 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval class attribute metadata preserves named literal arguments. +#[test] +fn execute_program_dispatches_named_class_attribute_args() { + let program = parse_fragment( + br#"#[Route(path: "/eval", secure: true, code: 9)] +class EvalNamedAttrMeta {} +$args = class_attribute_args("EvalNamedAttrMeta", "Route"); +echo count($args); echo ":"; +echo $args["path"]; echo ":"; +echo $args["secure"] ? "T" : "F"; echo ":"; +echo $args["code"]; echo ":"; +$attrs = class_get_attributes("EvalNamedAttrMeta"); +$attr_args = $attrs[0]->getArguments(); +echo $attr_args["path"]; echo ":"; +echo $attr_args["secure"] ? "T" : "F"; echo ":"; +echo $attr_args["code"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:/eval:T:9:/eval:T:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionAttribute::newInstance instantiates eval-declared attribute classes. #[test] fn execute_program_instantiates_eval_declared_reflection_attribute() { @@ -207,7 +235,7 @@ fn execute_program_instantiates_eval_declared_reflection_attribute() { return $this->path . ":" . $this->code . ":" . ($this->enabled ? "T" : "F"); } } -#[EvalRoute("/home", -7, true)] +#[EvalRoute(enabled: true, code: -7, path: "/home")] class EvalRouteTarget {} $attrs = class_get_attributes("EvalRouteTarget"); $instance = $attrs[0]->newInstance(); diff --git a/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs index 8970629b9b..405e813bf3 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs @@ -107,6 +107,9 @@ impl FakeOps { } } /// Writes one fake indexed or associative array element. + /// + /// String keys promote indexed arrays to associative arrays so fake PHP arrays can + /// model mixed integer/string keys produced by runtime metadata helpers. pub(super) fn runtime_array_set( &mut self, array: RuntimeCellHandle, @@ -115,21 +118,35 @@ impl FakeOps { ) -> Result { let key = self.key(index)?; let id = array.as_ptr() as usize; - match self.values.get_mut(&id) { - Some(FakeValue::Array(elements)) => { - let FakeKey::Int(index) = key else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if index < 0 { - return Err(EvalStatus::UnsupportedConstruct); + let Some(slot) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + match slot { + FakeValue::Array(elements) => match key { + FakeKey::Int(index) => { + if index < 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let index = index as usize; + while elements.len() <= index { + elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); + } + elements[index] = value; } - let index = index as usize; - while elements.len() <= index { - elements.push(RuntimeCellHandle::from_raw(std::ptr::null_mut())); + key => { + let mut entries = std::mem::take(elements) + .into_iter() + .enumerate() + .filter_map(|(index, value)| { + (!value.as_ptr().is_null()) + .then_some((FakeKey::Int(index as i64), value)) + }) + .collect::>(); + entries.push((key, value)); + *slot = FakeValue::Assoc(entries); } - elements[index] = value; - } - Some(FakeValue::Assoc(entries)) => { + }, + FakeValue::Assoc(entries) => { if let Some((_, existing_value)) = entries.iter_mut().find(|(entry_key, _)| entry_key == &key) { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 973bee5aa0..a774e6088c 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -202,7 +202,7 @@ impl Parser { Ok(attributes) } - /// Parses one attribute name and optional literal positional arguments. + /// Parses one attribute name and optional literal positional/named arguments. pub(super) fn parse_attribute(&mut self) -> Result { let name = self.parse_qualified_name()?; let name = self.resolve_class_name(name); @@ -211,12 +211,18 @@ impl Parser { let mut supported = true; if !self.consume(TokenKind::RParen) { loop { - let arg = self.parse_call_arg()?; + let call_arg = self.parse_call_arg()?; if supported { - if arg.name().is_some() || arg.is_spread() { + if call_arg.is_spread() { supported = false; - } else if let Some(arg) = eval_attribute_arg_from_expr(arg.value()) { - args.push(arg); + } else if let Some(arg) = eval_attribute_arg_from_expr(call_arg.value()) { + args.push(match call_arg.name() { + Some(name) => EvalAttributeArg::Named { + name: name.to_string(), + value: Box::new(arg), + }, + None => arg, + }); } else { supported = false; } diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 30d86d4f00..f4510121f3 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -138,7 +138,13 @@ class DynEvalAttributed {}"#, EvalAttributeArg::Null, ]), ), - EvalAttribute::new("Tag", None), + EvalAttribute::new( + "Tag", + Some(vec![EvalAttributeArg::Named { + name: "name".to_string(), + value: Box::new(EvalAttributeArg::String("named".to_string())), + }]), + ), ]) )] ); diff --git a/docs/php/classes.md b/docs/php/classes.md index 2bb2cfcfbc..7c62f01928 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1019,6 +1019,7 @@ function double(int $x): int { return $x * 2; } Supported syntax: - single attribute: `#[Foo]` - attribute with arguments: `#[Bar(1, "two")]` +- named attribute arguments: `#[Bar(path: "/home", secure: true)]` - multiple attributes per group: `#[A, B(1)]` - stacked groups: `#[A] #[B]` - fully-qualified names: `#[\Symfony\Contracts\Service\Attribute\Required]` @@ -1049,7 +1050,7 @@ echo $b->name; // "elephc" echo $b->missing; // empty (Mixed null) ``` -User-defined attributes (e.g. `#[Author]`, `#[Pure]`, `#[Memoized]`) parse and persist in the AST. They have no compile-time semantics, but their **names** and **literal arguments** (positional and named) are reachable at runtime through lightweight helper builtins and the supported Reflection API: +User-defined attributes (e.g. `#[Author]`, `#[Pure]`, `#[Memoized]`) parse and persist in the AST. They have no compile-time semantics, but their **names** and literal positional or named **arguments** are reachable at runtime through lightweight helper builtins and the supported Reflection API: ```php $arg) { + echo $key, "=", $arg, "\n"; } -// /api/users -// GET -// 1 ← `true` echoes as 1 in PHP +// path=/api/users +// method=GET +// secure=1 ← `true` echoes as 1 in PHP ``` -`class_attribute_args()` returns an `array` whose elements preserve their original PHP type — strings stay strings, ints stay ints, booleans stay booleans, and `null` is `null`. The args are interned at compile time and boxed into mixed cells on demand at the call site. +`class_attribute_args()` returns an `array`: positional arguments keep integer keys, named arguments keep their PHP argument names as string keys, and values preserve their original PHP type — strings stay strings, ints stay ints, booleans stay booleans, and `null` is `null`. The args are interned at compile time and boxed into mixed cells on demand at the call site. For a more PHP-idiomatic API, `class_get_attributes()` and `ReflectionClass::getAttributes()` return the same data wrapped as `ReflectionAttribute` instances: @@ -1120,7 +1121,7 @@ $property = new ReflectionProperty('Controller', 'id'); echo $property->getAttributes()[0]->getName(); // Column ``` -`ReflectionAttribute` is a final synthetic built-in class with `getName(): string`, `getArguments(): array`, and `newInstance(): mixed` methods. It is populated internally by `class_get_attributes()` and the supported Reflection lookups and cannot be constructed or populated directly from user code; its metadata slots are private. `newInstance()` constructs the attribute class on demand when the attribute class exists in the program and the captured arguments are supported literals: +`ReflectionAttribute` is a final synthetic built-in class with `getName(): string`, `getArguments(): array`, and `newInstance(): mixed` methods. It is populated internally by `class_get_attributes()` and the supported Reflection lookups and cannot be constructed or populated directly from user code; its metadata slots are private. `getArguments()` returns the same `array` shape as `class_attribute_args()`. `newInstance()` constructs the attribute class on demand when the attribute class exists in the program and the captured positional or named arguments are supported literals: ```php , inst: &Instruction, @@ -429,56 +429,128 @@ fn emit_string_array_fill_x86_64(ctx: &mut FunctionContext<'_>, names: &[String] ctx.emitter.instruction("pop rax"); // restore the final attribute-name array as the result } -/// Allocates and fills an indexed array of boxed Mixed attribute arguments. +/// Allocates and fills a PHP hash array of boxed Mixed attribute arguments. fn emit_mixed_array(ctx: &mut FunctionContext<'_>, attr_args: &[AttrArgEntry]) -> Result<()> { - allocate_indexed_array(ctx, attr_args.len().max(1), 8); - crate::codegen::emit_array_value_type_stamp( - ctx.emitter, - abi::int_result_reg(ctx.emitter), - &PhpType::Mixed, - ); + allocate_mixed_hash(ctx, attr_args.len().max(1)); match ctx.emitter.target.arch { Arch::AArch64 => emit_mixed_array_fill_aarch64(ctx, attr_args), Arch::X86_64 => emit_mixed_array_fill_x86_64(ctx, attr_args), } } -/// Appends boxed Mixed attribute arguments to the current result array on AArch64. +/// Inserts boxed Mixed attribute arguments into the current result hash on AArch64. fn emit_mixed_array_fill_aarch64( ctx: &mut FunctionContext<'_>, attr_args: &[AttrArgEntry], ) -> Result<()> { - ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the attribute-arg array while boxing values - for entry in attr_args { + ctx.emitter.instruction("str x0, [sp, #-16]!"); // park the attribute-arg hash while boxing values + for (index, entry) in attr_args.iter().enumerate() { emit_box_arg(ctx, &entry.value)?; - ctx.emitter.instruction("mov x1, x0"); // pass the boxed attribute argument as the append value - ctx.emitter.instruction("ldr x0, [sp]"); // reload the attribute-arg array for this append - abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); - ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown attribute-arg array + ctx.emitter.instruction("mov x3, x0"); // pass the boxed argument as the hash value payload + ctx.emitter.instruction("mov x4, xzr"); // boxed Mixed hash entries do not use a high payload word + abi::emit_load_int_immediate( + ctx.emitter, + "x5", + crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64, + ); + ctx.emitter.instruction("ldr x0, [sp]"); // reload the attribute-arg hash for this insertion + emit_attribute_arg_key_aarch64(ctx, index, entry); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + ctx.emitter.instruction("str x0, [sp]"); // preserve the possibly-grown attribute-arg hash } - ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final attribute-arg array as the result + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the final attribute-arg hash as the result Ok(()) } -/// Appends boxed Mixed attribute arguments to the current result array on x86_64. +/// Inserts boxed Mixed attribute arguments into the current result hash on x86_64. fn emit_mixed_array_fill_x86_64( ctx: &mut FunctionContext<'_>, attr_args: &[AttrArgEntry], ) -> Result<()> { - ctx.emitter.instruction("push rax"); // park the attribute-arg array while boxing values + ctx.emitter.instruction("push rax"); // park the attribute-arg hash while boxing values ctx.emitter.instruction("sub rsp, 8"); // keep stack alignment stable across helper calls - for entry in attr_args { + for (index, entry) in attr_args.iter().enumerate() { emit_box_arg(ctx, &entry.value)?; - ctx.emitter.instruction("mov rsi, rax"); // pass the boxed attribute argument as the append value - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the attribute-arg array for this append - abi::emit_call_label(ctx.emitter, "__rt_array_push_int"); - ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown attribute-arg array + ctx.emitter.instruction("mov rcx, rax"); // pass the boxed argument as the hash value payload + abi::emit_load_int_immediate(ctx.emitter, "r8", 0); + abi::emit_load_int_immediate( + ctx.emitter, + "r9", + crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64, + ); + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp + 8]"); // reload the attribute-arg hash for this insertion + emit_attribute_arg_key_x86_64(ctx, index, entry); + abi::emit_call_label(ctx.emitter, "__rt_hash_set"); + ctx.emitter.instruction("mov QWORD PTR [rsp + 8], rax"); // preserve the possibly-grown attribute-arg hash } ctx.emitter.instruction("add rsp, 8"); // drop the temporary alignment slot - ctx.emitter.instruction("pop rax"); // restore the final attribute-arg array as the result + ctx.emitter.instruction("pop rax"); // restore the final attribute-arg hash as the result Ok(()) } +/// Materializes the hash key for one attribute argument on AArch64. +fn emit_attribute_arg_key_aarch64( + ctx: &mut FunctionContext<'_>, + index: usize, + entry: &AttrArgEntry, +) { + match &entry.key { + Some(AttrKey::Str(name)) => { + let (label, len) = ctx.data.add_string(name.as_bytes()); + abi::emit_symbol_address(ctx.emitter, "x1", &label); + abi::emit_load_int_immediate(ctx.emitter, "x2", len as i64); + } + Some(AttrKey::Int(value)) => { + abi::emit_load_int_immediate(ctx.emitter, "x1", *value); + abi::emit_load_int_immediate(ctx.emitter, "x2", -1); + } + None => { + abi::emit_load_int_immediate(ctx.emitter, "x1", index as i64); + abi::emit_load_int_immediate(ctx.emitter, "x2", -1); + } + } +} + +/// Materializes the hash key for one attribute argument on x86_64. +fn emit_attribute_arg_key_x86_64( + ctx: &mut FunctionContext<'_>, + index: usize, + entry: &AttrArgEntry, +) { + match &entry.key { + Some(AttrKey::Str(name)) => { + let (label, len) = ctx.data.add_string(name.as_bytes()); + abi::emit_symbol_address(ctx.emitter, "rsi", &label); + abi::emit_load_int_immediate(ctx.emitter, "rdx", len as i64); + } + Some(AttrKey::Int(value)) => { + abi::emit_load_int_immediate(ctx.emitter, "rsi", *value); + abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); + } + None => { + abi::emit_load_int_immediate(ctx.emitter, "rsi", index as i64); + abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); + } + } +} + +/// Allocates a Mixed-valued PHP hash with room for captured attribute args. +fn allocate_mixed_hash(ctx: &mut FunctionContext<'_>, capacity: usize) { + let capacity = (capacity * 2).max(16); + let value_tag = crate::codegen::runtime_value_tag(&PhpType::Mixed) as i64; + match ctx.emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_int_immediate(ctx.emitter, "x0", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "x1", value_tag); + } + Arch::X86_64 => { + abi::emit_load_int_immediate(ctx.emitter, "rdi", capacity as i64); + abi::emit_load_int_immediate(ctx.emitter, "rsi", value_tag); + } + } + abi::emit_call_label(ctx.emitter, "__rt_hash_new"); +} + /// Boxes one captured attribute argument into a Mixed cell, leaving the cell in /// the integer result register. A nested array is built recursively and boxed /// with the array tag; scalars dispatch to the per-architecture boxer. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index a4b49ccbd6..5425b11537 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -66,6 +66,7 @@ const NATIVE_ATTRIBUTE_ARG_NULL: u8 = 0; const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; +const NATIVE_ATTRIBUTE_ARG_NAMED: u8 = 4; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; @@ -2162,6 +2163,11 @@ fn eval_native_member_attribute_push_arg(record: &mut Vec, arg: &AttrArgValu record.push(NATIVE_ATTRIBUTE_ARG_STRING); eval_native_member_attribute_push_string(record, value); } + AttrArgValue::Named { name, value } => { + record.push(NATIVE_ATTRIBUTE_ARG_NAMED); + eval_native_member_attribute_push_string(record, name); + eval_native_member_attribute_push_arg(record, value); + } } } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 5dfdfa8948..6bad67615a 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -7096,14 +7096,28 @@ fn builtin_return_type_override(name: &str) -> Option { Some(PhpType::Mixed) } "spl_autoload_functions" => Some(PhpType::Array(Box::new(PhpType::Int))), - "__elephc_phar_list_entries" | "class_attribute_names" | "explode" | "fgetcsv" - | "file" | "get_declared_classes" | "fscanf" | "get_declared_interfaces" - | "get_declared_traits" | "glob" | "hash_algos" | "scandir" | "spl_classes" - | "str_split" | "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" - | "sscanf" => { - Some(PhpType::Array(Box::new(PhpType::Str))) - } - "class_attribute_args" => Some(PhpType::Array(Box::new(PhpType::Mixed))), + "__elephc_phar_list_entries" + | "class_attribute_names" + | "explode" + | "fgetcsv" + | "file" + | "get_declared_classes" + | "fscanf" + | "get_declared_interfaces" + | "get_declared_traits" + | "glob" + | "hash_algos" + | "scandir" + | "spl_classes" + | "str_split" + | "stream_get_filters" + | "stream_get_transports" + | "stream_get_wrappers" + | "sscanf" => Some(PhpType::Array(Box::new(PhpType::Str))), + "class_attribute_args" => Some(PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }), "class_get_attributes" => Some(PhpType::Array(Box::new(PhpType::Object( "ReflectionAttribute".to_string(), )))), diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 95b5256453..0b6d338c02 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -306,6 +306,14 @@ fn reflection_static_properties_map_type() -> PhpType { } } +/// Returns `array` for ReflectionAttribute argument maps. +fn reflection_attribute_args_type() -> PhpType { + PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + } +} + /// Returns `array` for property-hook reflection maps. fn reflection_property_hook_map_type() -> PhpType { PhpType::AssocArray { @@ -4531,6 +4539,11 @@ fn make_reflection_variadic_optional(sig: &mut crate::types::FunctionSig) { /// - `getAttributes` → `array` pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(class_info) = checker.classes.get_mut("ReflectionAttribute") { + for (name, ty) in &mut class_info.properties { + if name == "__args" { + *ty = reflection_attribute_args_type(); + } + } if let Some(sig) = class_info.methods.get_mut("__construct") { sig.return_type = PhpType::Void; } @@ -4538,12 +4551,7 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Str; } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getArguments")) { - // Attribute arguments can be keyed (named arguments / associative - // arrays), so the result is an associative array of mixed values. - sig.return_type = PhpType::AssocArray { - key: Box::new(PhpType::Mixed), - value: Box::new(PhpType::Mixed), - }; + sig.return_type = reflection_attribute_args_type(); } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("newInstance")) { sig.return_type = PhpType::Mixed; diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index cbf020546c..bf4f24dee4 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -697,6 +697,31 @@ echo $args[0]; assert_eq!(out, "/x"); } +/// Verifies that named PHP attribute arguments are returned under string keys +/// while positional arguments keep integer keys. +#[test] +fn test_class_attribute_args_preserves_named_literal_arguments() { + let out = compile_and_run( + r#"path = $path; + $this->method = $method; + } +} +#[Route(method: "POST", path: "/submit")] +class Controller {} +$attr = (new ReflectionClass(Controller::class))->getAttributes()[0]; +$args = $attr->getArguments(); +echo count($args); +echo ":"; +echo $args["path"]; +echo ":"; +echo $args["method"]; +echo ":"; +$instance = $attr->newInstance(); +echo $instance->path; +echo ":"; +echo $instance->method; +"#, + ); + assert_eq!(out, "2:/submit:POST:/submit:POST"); +} + /// Verifies that `ReflectionAttribute::getTarget()` and `isRepeated()` report /// PHP-compatible owner target bits and duplicate-owner metadata. #[test] From 07017cecbf091edc087cbbcd08bfd85f597e4613 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 16:05:42 +0200 Subject: [PATCH 0638/1208] Support float attribute metadata --- crates/elephc-magician/src/eval_ir.rs | 1 + .../elephc-magician/src/ffi/native_methods.rs | 4 ++ crates/elephc-magician/src/ffi/tests.rs | 6 +++ .../interpreter/builtins/class_metadata.rs | 3 +- .../src/interpreter/statements.rs | 1 + .../tests/builtins_class_metadata.rs | 8 ++-- .../elephc-magician/src/parser/statements.rs | 4 ++ .../src/parser/tests/classes_errors.rs | 4 +- docs/php/eval.md | 4 +- src/codegen/eval_reflection_helpers.rs | 14 ++++++- src/codegen/lower_inst/builtins/eval.rs | 5 +++ tests/codegen/eval.rs | 30 +++++++++++++-- tests/codegen/oop/attributes.rs | 37 +++++++++++++++++++ 13 files changed, 107 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 4578bdf11e..bd143fdc94 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -429,6 +429,7 @@ impl EvalParameterType { pub enum EvalAttributeArg { String(String), Int(i64), + Float(u64), Bool(bool), Null, Named { diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index 9155389eb0..f7ee0d1226 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -33,6 +33,7 @@ const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; const NATIVE_ATTRIBUTE_ARG_NAMED: u8 = 4; +const NATIVE_ATTRIBUTE_ARG_FLOAT: u8 = 5; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; @@ -1726,6 +1727,9 @@ fn native_attribute_take_arg(bytes: &[u8], offset: &mut usize) -> Option Some(EvalAttributeArg::Int(native_attribute_take_i64( bytes, offset, )?)), + NATIVE_ATTRIBUTE_ARG_FLOAT => Some(EvalAttributeArg::Float( + native_attribute_take_u64(bytes, offset)?, + )), NATIVE_ATTRIBUTE_ARG_STRING => { native_attribute_take_string(bytes, offset).map(EvalAttributeArg::String) } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 0e2a6ce95c..44513d9a72 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -70,6 +70,10 @@ fn native_member_attribute_push_arg(record: &mut Vec, arg: &EvalAttributeArg record.push(2); record.extend_from_slice(&value.to_le_bytes()); } + EvalAttributeArg::Float(bits) => { + record.push(5); + record.extend_from_slice(&bits.to_le_bytes()); + } EvalAttributeArg::String(value) => { record.push(3); native_member_attribute_push_string(record, value); @@ -730,6 +734,7 @@ fn register_native_member_attribute_records_metadata() { value: Box::new(EvalAttributeArg::String("/users".to_string())), }, EvalAttributeArg::Int(7), + EvalAttributeArg::Float(1.5f64.to_bits()), EvalAttributeArg::Bool(true), EvalAttributeArg::Null, ]), @@ -799,6 +804,7 @@ fn register_native_member_attribute_records_metadata() { value: Box::new(EvalAttributeArg::String("/users".to_string())), }, EvalAttributeArg::Int(7), + EvalAttributeArg::Float(1.5f64.to_bits()), EvalAttributeArg::Bool(true), EvalAttributeArg::Null, ] diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index c16aef85b8..ef8f2e2854 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -234,7 +234,7 @@ fn eval_class_attribute_args_result( args: &[EvalAttributeArg], values: &mut impl RuntimeValueOps, ) -> Result { - let mut result = values.array_new(args.len())?; + let mut result = values.assoc_new(args.len())?; for (index, arg) in args.iter().enumerate() { let key = match arg.name() { Some(name) => values.string(name)?, @@ -254,6 +254,7 @@ fn eval_class_attribute_arg_value( match arg { EvalAttributeArg::String(value) => values.string(value), EvalAttributeArg::Int(value) => values.int(*value), + EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), EvalAttributeArg::Bool(value) => values.bool_value(*value), EvalAttributeArg::Null => values.null(), EvalAttributeArg::Named { value, .. } => eval_class_attribute_arg_value(value, values), diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 28207c6a6a..7e2eb4e7ae 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4132,6 +4132,7 @@ fn eval_reflection_attribute_arg_value( match arg { EvalAttributeArg::String(value) => values.string(value), EvalAttributeArg::Int(value) => values.int(*value), + EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), EvalAttributeArg::Bool(value) => values.bool_value(*value), EvalAttributeArg::Null => values.null(), EvalAttributeArg::Named { value, .. } => eval_reflection_attribute_arg_value(value, values), diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 986ccf4801..bf967f6c09 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -146,14 +146,14 @@ return true;"#, #[test] fn execute_program_dispatches_class_attribute_metadata_builtins() { let program = parse_fragment( - br#"#[Route("/home", -1, true, null)] + br#"#[Route("/home", -1, 1.5, true, null)] #[Tag("first"), Tag("second")] class EvalAttrMeta {} $names = class_attribute_names("EvalAttrMeta"); echo count($names); echo ":"; echo $names[0]; echo ":"; echo $names[1]; echo ":"; echo $names[2]; echo ":"; $args = class_attribute_args("EvalAttrMeta", "route"); echo count($args); echo ":"; echo $args[0]; echo ":"; echo $args[1]; echo ":"; -echo $args[2] ? "T" : "F"; echo ":"; echo is_null($args[3]) ? "N" : "bad"; echo ":"; +echo $args[2]; echo ":"; echo $args[3] ? "T" : "F"; echo ":"; echo is_null($args[4]) ? "N" : "bad"; echo ":"; $tag = class_attribute_args("evalattrmeta", "Tag"); echo $tag[0]; echo ":"; $missing = class_attribute_args("EvalAttrMeta", "Missing"); @@ -162,7 +162,7 @@ $attrs = class_get_attributes("EvalAttrMeta"); echo count($attrs); echo ":"; echo $attrs[0]->getName(); echo ":"; $attr_args = $attrs[0]->getArguments(); echo count($attr_args); echo ":"; echo $attr_args[0]; echo ":"; echo $attr_args[1]; echo ":"; -echo $attr_args[2] ? "T" : "F"; echo ":"; echo is_null($attr_args[3]) ? "N" : "bad"; echo ":"; +echo $attr_args[2]; echo ":"; echo $attr_args[3] ? "T" : "F"; echo ":"; echo is_null($attr_args[4]) ? "N" : "bad"; echo ":"; $tag_attr_args = $attrs[1]->getArguments(); echo $attrs[1]->getName(); echo ":"; echo $tag_attr_args[0]; echo ":"; echo is_null($attrs[0]->newInstance()) ? "N" : "bad"; echo ":"; @@ -185,7 +185,7 @@ return true;"#, assert_eq!( values.output, - "3:Route:Tag:Tag:4:/home:-1:T:N:first:0:3:Route:4:/home:-1:T:N:Tag:first:N:Route:/home:111" + "3:Route:Tag:Tag:5:/home:-1:1.5:T:N:first:0:3:Route:5:/home:-1:1.5:T:N:Tag:first:N:Route:/home:111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index a774e6088c..37101e98d0 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2760,6 +2760,7 @@ fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { match expr { EvalExpr::Const(EvalConst::String(value)) => Some(EvalAttributeArg::String(value.clone())), EvalExpr::Const(EvalConst::Int(value)) => Some(EvalAttributeArg::Int(*value)), + EvalExpr::Const(EvalConst::Float(value)) => Some(EvalAttributeArg::Float(value.to_bits())), EvalExpr::Const(EvalConst::Bool(value)) => Some(EvalAttributeArg::Bool(*value)), EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Null), EvalExpr::Unary { @@ -2769,6 +2770,9 @@ fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { EvalExpr::Const(EvalConst::Int(value)) => { Some(EvalAttributeArg::Int(value.wrapping_neg())) } + EvalExpr::Const(EvalConst::Float(value)) => { + Some(EvalAttributeArg::Float((-*value).to_bits())) + } _ => None, }, _ => None, diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index f4510121f3..3aedadfb41 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -120,7 +120,7 @@ fn parse_fragment_rejects_invalid_void_and_never_return_type_forms() { #[test] fn parse_fragment_accepts_class_attribute_metadata() { let program = parse_fragment( - br#"#[Route("/home", -1, true, null)] + br#"#[Route("/home", -1, 1.5, -2.5, true, null)] #[Tag(name: "named")] class DynEvalAttributed {}"#, ) @@ -134,6 +134,8 @@ class DynEvalAttributed {}"#, Some(vec![ EvalAttributeArg::String("/home".to_string()), EvalAttributeArg::Int(-1), + EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Float((-2.5f64).to_bits()), EvalAttributeArg::Bool(true), EvalAttributeArg::Null, ]), diff --git a/docs/php/eval.md b/docs/php/eval.md index 635b79395c..ec749ddf9a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -179,8 +179,8 @@ static/class-constant accesses. Class-level attributes declared on eval classes, interfaces, traits, and enums, plus bridge-registered generated/AOT class-level attributes, are visible through `class_attribute_names()`, `class_attribute_args()`, and `class_get_attributes()` when their arguments fit -the supported literal positional/named subset (`string`, `int`, `bool`, `null`, -or negated integer literals). Positional arguments keep integer keys in +the supported literal positional/named subset (`string`, `int`, `float`, `bool`, +`null`, or negated numeric literals). Positional arguments keep integer keys in `class_attribute_args()` / `ReflectionAttribute::getArguments()`, and named arguments keep their PHP names as string keys. `ReflectionAttribute::newInstance()` instantiates eval-declared or diff --git a/src/codegen/eval_reflection_helpers.rs b/src/codegen/eval_reflection_helpers.rs index 58cbdc8fb4..d48866e52b 100644 --- a/src/codegen/eval_reflection_helpers.rs +++ b/src/codegen/eval_reflection_helpers.rs @@ -260,18 +260,23 @@ fn emit_set_args_property_aarch64( layout: &ReflectionAttributeLayout, fail_label: &str, ) { + let args_ok_label = "__elephc_eval_reflection_attribute_args_ok"; emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed eval attribute-argument array emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject malformed null argument arrays emitter.instruction("bl __rt_mixed_unbox"); // expose the argument array tag and payload pointer emitter.instruction("cmp x0, #4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("b.eq {}", args_ok_label)); // accept indexed argument arrays from older eval paths + emitter.instruction("cmp x0, #5"); // runtime tag 5 means associative array emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array argument metadata + emitter.label(args_ok_label); emitter.instruction("str x1, [sp, #32]"); // save the unboxed argument array across incref + emitter.instruction("str x0, [sp, #56]"); // save the argument array tag for the object slot emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register emitter.instruction("bl __rt_incref"); // retain the argument array for ReflectionAttribute ownership emitter.instruction("ldr x1, [sp, #32]"); // reload the retained argument array payload emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer abi::emit_store_to_address(emitter, "x1", "x9", layout.args_lo); - abi::emit_load_int_immediate(emitter, "x10", 4); + emitter.instruction("ldr x10, [sp, #56]"); // reload the original argument array tag abi::emit_store_to_address(emitter, "x10", "x9", layout.args_hi); } @@ -281,19 +286,24 @@ fn emit_set_args_property_x86_64( layout: &ReflectionAttributeLayout, fail_label: &str, ) { + let args_ok_label = "__elephc_eval_reflection_attribute_args_ok_x"; emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // reload the boxed eval attribute-argument array emitter.instruction("test rax, rax"); // check whether the boxed argument array is null emitter.instruction(&format!("jz {}", fail_label)); // reject malformed null argument arrays emitter.instruction("call __rt_mixed_unbox"); // expose the argument array tag and payload pointer emitter.instruction("cmp rax, 4"); // runtime tag 4 means indexed array + emitter.instruction(&format!("je {}", args_ok_label)); // accept indexed argument arrays from older eval paths + emitter.instruction("cmp rax, 5"); // runtime tag 5 means associative array emitter.instruction(&format!("jne {}", fail_label)); // reject non-array argument metadata + emitter.label(args_ok_label); emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // save the unboxed argument array across incref + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the argument array tag for the object slot emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register emitter.instruction("call __rt_incref"); // retain the argument array for ReflectionAttribute ownership emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // reload the retained argument array payload emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the ReflectionAttribute object pointer abi::emit_store_to_address(emitter, "rdi", "r10", layout.args_lo); - abi::emit_load_int_immediate(emitter, "r11", 4); + emitter.instruction("mov r11, QWORD PTR [rbp - 64]"); // reload the original argument array tag abi::emit_store_to_address(emitter, "r11", "r10", layout.args_hi); } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 5425b11537..78558c86be 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -67,6 +67,7 @@ const NATIVE_ATTRIBUTE_ARG_BOOL: u8 = 1; const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; const NATIVE_ATTRIBUTE_ARG_NAMED: u8 = 4; +const NATIVE_ATTRIBUTE_ARG_FLOAT: u8 = 5; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; @@ -2159,6 +2160,10 @@ fn eval_native_member_attribute_push_arg(record: &mut Vec, arg: &AttrArgValu record.push(NATIVE_ATTRIBUTE_ARG_INT); record.extend_from_slice(&value.to_le_bytes()); } + AttrArgValue::Float(bits) => { + record.push(NATIVE_ATTRIBUTE_ARG_FLOAT); + record.extend_from_slice(&bits.to_le_bytes()); + } AttrArgValue::Str(value) => { record.push(NATIVE_ATTRIBUTE_ARG_STRING); eval_native_member_attribute_push_string(record, value); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f978bbdd20..c61a7a557e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7535,21 +7535,21 @@ echo EvalClassNameChild::staticName();'); fn test_eval_declared_class_attribute_metadata() { let out = compile_and_run_capture( r#"getName() . ":"; $attrArgs = $attrs[0]->getArguments(); echo count($attrArgs) . ":" . $attrArgs[0] . ":" . $attrArgs[1] . ":"; -echo ($attrArgs[2] ? "T" : "F") . ":" . (is_null($attrArgs[3]) ? "N" : "bad") . ":"; +echo $attrArgs[2] . ":" . ($attrArgs[3] ? "T" : "F") . ":" . (is_null($attrArgs[4]) ? "N" : "bad") . ":"; $tagArgs = $attrs[1]->getArguments(); echo $attrs[1]->getName() . ":" . $tagArgs[0] . ":"; echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); @@ -7562,10 +7562,32 @@ echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); ); assert_eq!( out.stdout, - "3:Route:Tag:Tag:4:/home:-1:T:N:first:3:Route:4:/home:-1:T:N:Tag:first:N" + "3:Route:Tag:Tag:5:/home:-1:1.5:T:N:first:3:Route:5:/home:-1:1.5:T:N:Tag:first:N" ); } +/// Verifies eval can read generated/AOT float attribute arguments. +#[test] +fn test_eval_reflection_class_exposes_aot_float_attribute_args() { + let out = compile_and_run_capture( + r#"getArguments(); +echo count($attrArgs) . ":" . $attrArgs[0] . ":" . $attrArgs["value"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:1.5:-2.25:2:1.5:-2.25"); +} + /// Verifies eval ReflectionAttribute::newInstance builds eval-declared attribute objects. #[test] fn test_eval_reflection_attribute_new_instance_for_eval_class() { diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index bf4f24dee4..1f0ab79140 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -613,6 +613,43 @@ echo $args[2]; assert_eq!(out, "3/kept,42,also-kept"); } +/// Verifies that float literal attribute args are preserved through class +/// helpers, ReflectionAttribute arguments, and attribute instantiation. +#[test] +fn test_class_attribute_args_preserves_float_literals() { + let out = compile_and_run( + r#"getArguments(); +echo ":"; +echo $attrArgs[0]; +echo ":"; +echo $attrArgs["named"]; + +$instance = $attr->newInstance(); +echo ":"; +echo $instance->ratio; +echo ":"; +echo $instance->named; +"#, + ); + assert_eq!(out, "2:1.5:-2.25:1.5:-2.25:1.5:-2.25"); +} + /// Verifies that boolean (`true`, `false`) and `null` literal arguments are /// preserved by `class_attribute_args()` as strings. Booleans render as /// PHP echo would (true → "1", false → ""), null renders as empty string, From 480d0f74c8ecb3e47853a5620f4673a20e3343c2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 16:17:03 +0200 Subject: [PATCH 0639/1208] Support class-name attribute metadata --- .../tests/builtins_class_metadata.rs | 7 +++- .../elephc-magician/src/parser/statements.rs | 15 +++++++ .../src/parser/tests/classes_errors.rs | 3 +- docs/php/classes.md | 2 +- docs/php/eval.md | 2 +- src/types/schema.rs | 3 +- tests/codegen/eval.rs | 7 +++- tests/codegen/oop/attributes.rs | 42 +++++++++++++++++++ 8 files changed, 73 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index bf967f6c09..e5288fb34e 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -146,7 +146,8 @@ return true;"#, #[test] fn execute_program_dispatches_class_attribute_metadata_builtins() { let program = parse_fragment( - br#"#[Route("/home", -1, 1.5, true, null)] + br#"class EvalAttrDep {} +#[Route("/home", -1, 1.5, true, null, EvalAttrDep::class)] #[Tag("first"), Tag("second")] class EvalAttrMeta {} $names = class_attribute_names("EvalAttrMeta"); @@ -154,6 +155,7 @@ echo count($names); echo ":"; echo $names[0]; echo ":"; echo $names[1]; echo ":" $args = class_attribute_args("EvalAttrMeta", "route"); echo count($args); echo ":"; echo $args[0]; echo ":"; echo $args[1]; echo ":"; echo $args[2]; echo ":"; echo $args[3] ? "T" : "F"; echo ":"; echo is_null($args[4]) ? "N" : "bad"; echo ":"; +echo $args[5]; echo ":"; $tag = class_attribute_args("evalattrmeta", "Tag"); echo $tag[0]; echo ":"; $missing = class_attribute_args("EvalAttrMeta", "Missing"); @@ -163,6 +165,7 @@ echo count($attrs); echo ":"; echo $attrs[0]->getName(); echo ":"; $attr_args = $attrs[0]->getArguments(); echo count($attr_args); echo ":"; echo $attr_args[0]; echo ":"; echo $attr_args[1]; echo ":"; echo $attr_args[2]; echo ":"; echo $attr_args[3] ? "T" : "F"; echo ":"; echo is_null($attr_args[4]) ? "N" : "bad"; echo ":"; +echo $attr_args[5]; echo ":"; $tag_attr_args = $attrs[1]->getArguments(); echo $attrs[1]->getName(); echo ":"; echo $tag_attr_args[0]; echo ":"; echo is_null($attrs[0]->newInstance()) ? "N" : "bad"; echo ":"; @@ -185,7 +188,7 @@ return true;"#, assert_eq!( values.output, - "3:Route:Tag:Tag:5:/home:-1:1.5:T:N:first:0:3:Route:5:/home:-1:1.5:T:N:Tag:first:N:Route:/home:111" + "3:Route:Tag:Tag:6:/home:-1:1.5:T:N:EvalAttrDep:first:0:3:Route:6:/home:-1:1.5:T:N:EvalAttrDep:Tag:first:N:Route:/home:111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 37101e98d0..da2ffd6c1a 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2775,10 +2775,25 @@ fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { } _ => None, }, + EvalExpr::ClassNameFetch { class_name } => { + eval_attribute_class_name_arg(class_name).map(EvalAttributeArg::String) + } _ => None, } } +/// Returns a compile-time class-name string for named `ClassName::class` attribute args. +fn eval_attribute_class_name_arg(class_name: &str) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if ["self", "parent", "static"] + .iter() + .any(|special| class_name.eq_ignore_ascii_case(special)) + { + return None; + } + Some(class_name.to_string()) +} + /// Returns whether any parsed property hook accessor uses its own backing slot. fn property_hook_methods_use_backing_slot( hook_methods: &[EvalClassMethod], diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 3aedadfb41..af972cb277 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -120,7 +120,7 @@ fn parse_fragment_rejects_invalid_void_and_never_return_type_forms() { #[test] fn parse_fragment_accepts_class_attribute_metadata() { let program = parse_fragment( - br#"#[Route("/home", -1, 1.5, -2.5, true, null)] + br#"#[Route("/home", -1, 1.5, -2.5, true, null, EvalAttrDep::class)] #[Tag(name: "named")] class DynEvalAttributed {}"#, ) @@ -138,6 +138,7 @@ class DynEvalAttributed {}"#, EvalAttributeArg::Float((-2.5f64).to_bits()), EvalAttributeArg::Bool(true), EvalAttributeArg::Null, + EvalAttributeArg::String("EvalAttrDep".to_string()), ]), ), EvalAttribute::new( diff --git a/docs/php/classes.md b/docs/php/classes.md index 7c62f01928..efd978a9dd 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1370,7 +1370,7 @@ name with `isBuiltin()` false. Limitations today: - All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Function/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, traits, and statically typed object expressions, including anonymous-class objects; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name->id lookup table that is not yet implemented. -- Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** - a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` -> `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. +- Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, `ClassName::class` strings, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** - a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` -> `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. diff --git a/docs/php/eval.md b/docs/php/eval.md index ec749ddf9a..f8118c4fea 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -180,7 +180,7 @@ interfaces, traits, and enums, plus bridge-registered generated/AOT class-level attributes, are visible through `class_attribute_names()`, `class_attribute_args()`, and `class_get_attributes()` when their arguments fit the supported literal positional/named subset (`string`, `int`, `float`, `bool`, -`null`, or negated numeric literals). Positional arguments keep integer keys in +`null`, negated numeric literals, or `ClassName::class` strings). Positional arguments keep integer keys in `class_attribute_args()` / `ReflectionAttribute::getArguments()`, and named arguments keep their PHP names as string keys. `ReflectionAttribute::newInstance()` instantiates eval-declared or diff --git a/src/types/schema.rs b/src/types/schema.rs index 85b90190f0..35ce5e21c8 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -18,7 +18,8 @@ use super::{FunctionSig, PhpType}; /// Compile-time attribute argument value. Captures the subset of PHP /// attribute argument expressions that reflection helpers can materialize: -/// scalars (string/int/bool/null/float), and nested arrays of the same. +/// scalars (string/int/bool/null/float), `ClassName::class` strings, symbolic +/// references, and nested arrays of the same. /// /// `Float` stores the IEEE-754 bit pattern (`f64::to_bits`) rather than an /// `f64` so the enum can keep deriving `Eq`/`Hash`/`Ord` (used by the diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c61a7a557e..e4c43570d0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7535,7 +7535,8 @@ echo EvalClassNameChild::staticName();'); fn test_eval_declared_class_attribute_metadata() { let out = compile_and_run_capture( r#"getName() . ":"; $attrArgs = $attrs[0]->getArguments(); echo count($attrArgs) . ":" . $attrArgs[0] . ":" . $attrArgs[1] . ":"; echo $attrArgs[2] . ":" . ($attrArgs[3] ? "T" : "F") . ":" . (is_null($attrArgs[4]) ? "N" : "bad") . ":"; +echo $attrArgs[5] . ":"; $tagArgs = $attrs[1]->getArguments(); echo $attrs[1]->getName() . ":" . $tagArgs[0] . ":"; echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); @@ -7562,7 +7565,7 @@ echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); ); assert_eq!( out.stdout, - "3:Route:Tag:Tag:5:/home:-1:1.5:T:N:first:3:Route:5:/home:-1:1.5:T:N:Tag:first:N" + "3:Route:Tag:Tag:6:/home:-1:1.5:T:N:EvalAttrDep:first:3:Route:6:/home:-1:1.5:T:N:EvalAttrDep:Tag:first:N" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index 1f0ab79140..f4143365d6 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -650,6 +650,48 @@ echo $instance->named; assert_eq!(out, "2:1.5:-2.25:1.5:-2.25:1.5:-2.25"); } +/// Verifies that `ClassName::class` attribute args are retained as strings. +#[test] +fn test_class_attribute_args_preserves_class_name_literals() { + let out = compile_and_run( + r#"getArguments(); +echo ":"; +echo $attrArgs[0]; +echo ":"; +echo $attrArgs["named"]; + +$instance = $attr->newInstance(); +echo ":"; +echo $instance->target; +echo ":"; +echo $instance->named; +"#, + ); + assert_eq!( + out, + "2:ClassNameArgTarget:ClassNameArgOther:ClassNameArgTarget:ClassNameArgOther:ClassNameArgTarget:ClassNameArgOther" + ); +} + /// Verifies that boolean (`true`, `false`) and `null` literal arguments are /// preserved by `class_attribute_args()` as strings. Booleans render as /// PHP echo would (true → "1", false → ""), null renders as empty string, From 68dc27dc8f7a8c2717a5022f82186f7b79cecb5e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 16:37:35 +0200 Subject: [PATCH 0640/1208] Support array attribute metadata --- crates/elephc-magician/src/eval_ir.rs | 1 + .../elephc-magician/src/ffi/native_methods.rs | 9 ++++ crates/elephc-magician/src/ffi/tests.rs | 15 ++++++ .../interpreter/builtins/class_metadata.rs | 15 ++++++ .../src/interpreter/statements.rs | 17 ++++++ .../tests/builtins_class_metadata.rs | 6 ++- .../elephc-magician/src/parser/statements.rs | 15 ++++++ .../src/parser/tests/classes_errors.rs | 6 ++- docs/php/classes.md | 4 +- docs/php/eval.md | 6 ++- src/codegen/lower_inst/builtins/eval.rs | 8 +++ tests/codegen/eval.rs | 20 ++++--- tests/codegen/oop/attributes.rs | 52 +++++++++++++++++++ 13 files changed, 161 insertions(+), 13 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index bd143fdc94..4f4f4af7a3 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -432,6 +432,7 @@ pub enum EvalAttributeArg { Float(u64), Bool(bool), Null, + Array(Vec), Named { name: String, value: Box, diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index f7ee0d1226..d7a946030b 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -34,6 +34,7 @@ const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; const NATIVE_ATTRIBUTE_ARG_NAMED: u8 = 4; const NATIVE_ATTRIBUTE_ARG_FLOAT: u8 = 5; +const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; @@ -1741,6 +1742,14 @@ fn native_attribute_take_arg(bytes: &[u8], offset: &mut usize) -> Option { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut elements = Vec::with_capacity(len); + for _ in 0..len { + elements.push(native_attribute_take_arg(bytes, offset)?); + } + Some(EvalAttributeArg::Array(elements)) + } _ => None, } } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 44513d9a72..0f6d689ce3 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -83,6 +83,13 @@ fn native_member_attribute_push_arg(record: &mut Vec, arg: &EvalAttributeArg native_member_attribute_push_string(record, name); native_member_attribute_push_arg(record, value); } + EvalAttributeArg::Array(elements) => { + record.push(6); + record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); + for element in elements { + native_member_attribute_push_arg(record, element); + } + } } } @@ -735,6 +742,10 @@ fn register_native_member_attribute_records_metadata() { }, EvalAttributeArg::Int(7), EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::Int(1), + EvalAttributeArg::String("two".to_string()), + ]), EvalAttributeArg::Bool(true), EvalAttributeArg::Null, ]), @@ -805,6 +816,10 @@ fn register_native_member_attribute_records_metadata() { }, EvalAttributeArg::Int(7), EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::Int(1), + EvalAttributeArg::String("two".to_string()), + ]), EvalAttributeArg::Bool(true), EvalAttributeArg::Null, ] diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index ef8f2e2854..a549cfbc68 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -257,10 +257,25 @@ fn eval_class_attribute_arg_value( EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), EvalAttributeArg::Bool(value) => values.bool_value(*value), EvalAttributeArg::Null => values.null(), + EvalAttributeArg::Array(elements) => eval_class_attribute_array_arg_value(elements, values), EvalAttributeArg::Named { value, .. } => eval_class_attribute_arg_value(value, values), } } +/// Materializes one retained positional attribute array literal as a runtime array cell. +fn eval_class_attribute_array_arg_value( + elements: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(elements.len())?; + for (index, element) in elements.iter().enumerate() { + let key = values.int(index as i64)?; + let value = eval_class_attribute_arg_value(element.value(), values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Returns whether a query names the same PHP attribute class case-insensitively. fn eval_attribute_name_matches(attribute_name: &str, query: &str) -> bool { attribute_name diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 7e2eb4e7ae..925d4a1bc1 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4135,10 +4135,27 @@ fn eval_reflection_attribute_arg_value( EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), EvalAttributeArg::Bool(value) => values.bool_value(*value), EvalAttributeArg::Null => values.null(), + EvalAttributeArg::Array(elements) => { + eval_reflection_attribute_array_arg_value(elements, values) + } EvalAttributeArg::Named { value, .. } => eval_reflection_attribute_arg_value(value, values), } } +/// Materializes one retained positional attribute array literal for constructor calls. +fn eval_reflection_attribute_array_arg_value( + elements: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(elements.len())?; + for (index, element) in elements.iter().enumerate() { + let key = values.int(index as i64)?; + let value = eval_reflection_attribute_arg_value(element.value(), values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Resolves the method metadata visible from the current class scope. pub(in crate::interpreter) fn eval_dynamic_method_for_call( object_class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index e5288fb34e..f65dee139b 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -147,7 +147,7 @@ return true;"#, fn execute_program_dispatches_class_attribute_metadata_builtins() { let program = parse_fragment( br#"class EvalAttrDep {} -#[Route("/home", -1, 1.5, true, null, EvalAttrDep::class)] +#[Route("/home", -1, 1.5, true, null, EvalAttrDep::class, ["nested", 2])] #[Tag("first"), Tag("second")] class EvalAttrMeta {} $names = class_attribute_names("EvalAttrMeta"); @@ -156,6 +156,7 @@ $args = class_attribute_args("EvalAttrMeta", "route"); echo count($args); echo ":"; echo $args[0]; echo ":"; echo $args[1]; echo ":"; echo $args[2]; echo ":"; echo $args[3] ? "T" : "F"; echo ":"; echo is_null($args[4]) ? "N" : "bad"; echo ":"; echo $args[5]; echo ":"; +echo count($args[6]); echo ":"; echo $args[6][0]; echo ":"; echo $args[6][1]; echo ":"; $tag = class_attribute_args("evalattrmeta", "Tag"); echo $tag[0]; echo ":"; $missing = class_attribute_args("EvalAttrMeta", "Missing"); @@ -166,6 +167,7 @@ $attr_args = $attrs[0]->getArguments(); echo count($attr_args); echo ":"; echo $attr_args[0]; echo ":"; echo $attr_args[1]; echo ":"; echo $attr_args[2]; echo ":"; echo $attr_args[3] ? "T" : "F"; echo ":"; echo is_null($attr_args[4]) ? "N" : "bad"; echo ":"; echo $attr_args[5]; echo ":"; +echo count($attr_args[6]); echo ":"; echo $attr_args[6][0]; echo ":"; echo $attr_args[6][1]; echo ":"; $tag_attr_args = $attrs[1]->getArguments(); echo $attrs[1]->getName(); echo ":"; echo $tag_attr_args[0]; echo ":"; echo is_null($attrs[0]->newInstance()) ? "N" : "bad"; echo ":"; @@ -188,7 +190,7 @@ return true;"#, assert_eq!( values.output, - "3:Route:Tag:Tag:6:/home:-1:1.5:T:N:EvalAttrDep:first:0:3:Route:6:/home:-1:1.5:T:N:EvalAttrDep:Tag:first:N:Route:/home:111" + "3:Route:Tag:Tag:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:first:0:3:Route:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:Tag:first:N:Route:/home:111" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index da2ffd6c1a..15cdcdfc3b 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2778,10 +2778,25 @@ fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { EvalExpr::ClassNameFetch { class_name } => { eval_attribute_class_name_arg(class_name).map(EvalAttributeArg::String) } + EvalExpr::Array(elements) => eval_attribute_array_arg_from_elements(elements), _ => None, } } +/// Converts a positional eval array literal into retained attribute metadata. +fn eval_attribute_array_arg_from_elements( + elements: &[EvalArrayElement], +) -> Option { + elements + .iter() + .map(|element| match element { + EvalArrayElement::Value(value) => eval_attribute_arg_from_expr(value), + EvalArrayElement::KeyValue { .. } => None, + }) + .collect::>>() + .map(EvalAttributeArg::Array) +} + /// Returns a compile-time class-name string for named `ClassName::class` attribute args. fn eval_attribute_class_name_arg(class_name: &str) -> Option { let class_name = class_name.trim_start_matches('\\'); diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index af972cb277..4dc7f59e9d 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -120,7 +120,7 @@ fn parse_fragment_rejects_invalid_void_and_never_return_type_forms() { #[test] fn parse_fragment_accepts_class_attribute_metadata() { let program = parse_fragment( - br#"#[Route("/home", -1, 1.5, -2.5, true, null, EvalAttrDep::class)] + br#"#[Route("/home", -1, 1.5, -2.5, true, null, EvalAttrDep::class, ["nested", 2])] #[Tag(name: "named")] class DynEvalAttributed {}"#, ) @@ -139,6 +139,10 @@ class DynEvalAttributed {}"#, EvalAttributeArg::Bool(true), EvalAttributeArg::Null, EvalAttributeArg::String("EvalAttrDep".to_string()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::String("nested".to_string()), + EvalAttributeArg::Int(2), + ]), ]), ), EvalAttribute::new( diff --git a/docs/php/classes.md b/docs/php/classes.md index efd978a9dd..5b78b4a8a5 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1079,7 +1079,7 @@ foreach (class_attribute_args('UserController', 'Route') as $key => $arg) { // secure=1 ← `true` echoes as 1 in PHP ``` -`class_attribute_args()` returns an `array`: positional arguments keep integer keys, named arguments keep their PHP argument names as string keys, and values preserve their original PHP type — strings stay strings, ints stay ints, booleans stay booleans, and `null` is `null`. The args are interned at compile time and boxed into mixed cells on demand at the call site. +`class_attribute_args()` returns an `array`: positional arguments keep integer keys, named arguments keep their PHP argument names as string keys, and values preserve their original PHP type — strings stay strings, ints stay ints, floats stay floats, booleans stay booleans, `null` is `null`, and supported nested arrays stay arrays. The args are interned at compile time and boxed into mixed cells on demand at the call site. For a more PHP-idiomatic API, `class_get_attributes()` and `ReflectionClass::getAttributes()` return the same data wrapped as `ReflectionAttribute` instances: @@ -1372,7 +1372,7 @@ Limitations today: - All arguments to `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, and `new ReflectionClass/Function/Method/Property/Parameter(...)` must be compile-time class/member/function strings or literal parameter selectors. `ClassName::class` is accepted for supported class-name arguments, and normal named-argument / static associative-spread normalization runs before the literal-string check. `ReflectionClass` can resolve statically known classes, enums, interfaces, traits, and statically typed object expressions, including anonymous-class objects; class-attribute materialization is currently backed by class/enum metadata. Dynamic class, method, property, function, parameter, or attribute names require a runtime name->id lookup table that is not yet implemented. - Attribute arguments materialized by reflection today include: string, int, float, bool, null, negation (`-N`) of a numeric literal, `ClassName::class` strings, arrays (positional and associative, nested, with heterogeneous element types), **named arguments**, and **symbolic references** - a global constant (`#[A(SOME_CONST)]`), a class/interface constant (`#[A(C::BAR)]`), or an enum case (`#[A(E::Case)]`). `ReflectionClass`/`ReflectionMethod`/`ReflectionProperty::getAttributes()` -> `getArguments()` returns named arguments under their string keys and positional arguments under their integer keys, matching PHP. A constant reference resolves to its value and an enum-case reference resolves to the case object (so reading its `->value`/`->name` works just like PHP), and `newInstance()` constructs the attribute with those arguments. - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. -- The flat `class_attribute_args()` helper returns a positional array of scalars only; it rejects attributes whose arguments are keyed (named arguments or associative arrays, at any depth) or contain a symbolic reference. Use `ReflectionClass::getAttributes()->getArguments()` for those. +- The flat `class_attribute_args()` helper materializes supported literal and array values directly, but symbolic references are resolved through `ReflectionClass::getAttributes()->getArguments()` / `ReflectionAttribute::newInstance()` instead. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. - `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionObject` supports the inherited class-metadata methods for object arguments and materializes the same metadata through a `ReflectionObject` instance. diff --git a/docs/php/eval.md b/docs/php/eval.md index f8118c4fea..d52de40f0b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -180,7 +180,8 @@ interfaces, traits, and enums, plus bridge-registered generated/AOT class-level attributes, are visible through `class_attribute_names()`, `class_attribute_args()`, and `class_get_attributes()` when their arguments fit the supported literal positional/named subset (`string`, `int`, `float`, `bool`, -`null`, negated numeric literals, or `ClassName::class` strings). Positional arguments keep integer keys in +`null`, negated numeric literals, `ClassName::class` strings, or positional +array literals containing the same supported values). Positional arguments keep integer keys in `class_attribute_args()` / `ReflectionAttribute::getArguments()`, and named arguments keep their PHP names as string keys. `ReflectionAttribute::newInstance()` instantiates eval-declared or @@ -200,7 +201,8 @@ child methods and public access see the child property. property, class-constant, and method-parameter attributes for eval-declared class-like symbols and bridge-registered generated/AOT class-level, method, property, and class-constant attributes when their arguments fit the same -literal positional/named subset, and `getName()` returns the reflected class, member, or +literal positional/named subset. Associative or dynamic attribute arrays are +still unsupported metadata. `getName()` returns the reflected class, member, or parameter name for those owners. `ReflectionClass`, `ReflectionObject`, `ReflectionFunction`, `ReflectionMethod`, `ReflectionProperty`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 78558c86be..556f80967a 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -68,6 +68,7 @@ const NATIVE_ATTRIBUTE_ARG_INT: u8 = 2; const NATIVE_ATTRIBUTE_ARG_STRING: u8 = 3; const NATIVE_ATTRIBUTE_ARG_NAMED: u8 = 4; const NATIVE_ATTRIBUTE_ARG_FLOAT: u8 = 5; +const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; @@ -2173,6 +2174,13 @@ fn eval_native_member_attribute_push_arg(record: &mut Vec, arg: &AttrArgValu eval_native_member_attribute_push_string(record, name); eval_native_member_attribute_push_arg(record, value); } + AttrArgValue::Array(elements) => { + record.push(NATIVE_ATTRIBUTE_ARG_ARRAY); + eval_native_member_attribute_push_u32(record, elements.len()); + for element in elements { + eval_native_member_attribute_push_arg(record, element); + } + } } } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e4c43570d0..9bb1263ca8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7536,7 +7536,7 @@ fn test_eval_declared_class_attribute_metadata() { let out = compile_and_run_capture( r#"getArguments(); echo count($attrArgs) . ":" . $attrArgs[0] . ":" . $attrArgs[1] . ":"; echo $attrArgs[2] . ":" . ($attrArgs[3] ? "T" : "F") . ":" . (is_null($attrArgs[4]) ? "N" : "bad") . ":"; echo $attrArgs[5] . ":"; +echo count($attrArgs[6]) . ":" . $attrArgs[6][0] . ":" . $attrArgs[6][1] . ":"; $tagArgs = $attrs[1]->getArguments(); echo $attrs[1]->getName() . ":" . $tagArgs[0] . ":"; echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); @@ -7565,7 +7567,7 @@ echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); ); assert_eq!( out.stdout, - "3:Route:Tag:Tag:6:/home:-1:1.5:T:N:EvalAttrDep:first:3:Route:6:/home:-1:1.5:T:N:EvalAttrDep:Tag:first:N" + "3:Route:Tag:Tag:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:first:3:Route:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:Tag:first:N" ); } @@ -9125,29 +9127,35 @@ fn test_eval_reflection_class_exposes_aot_attributes() { class EvalAotClassAttr { public string $name = ""; public int $value = 0; + public array $items = []; - public function __construct(string $name, int $value = 0) { + public function __construct(string $name, int $value = 0, array $items = []) { $this->name = $name; $this->value = $value; + $this->items = $items; } public function label(): string { return $this->name . ":" . $this->value; } } -#[EvalAotClassAttr("class", 1), EvalAotClassAttr("again", 2)] +#[EvalAotClassAttr("class", 1, ["nested", 3]), EvalAotClassAttr("again", 2, ["later"])] class EvalAotClassAttrTarget {} echo eval('$names = class_attribute_names("EvalAotClassAttrTarget"); echo count($names) . ":" . $names[0] . ":" . $names[1] . ":"; $args = class_attribute_args("evalaotclassattrtarget", "EvalAotClassAttr"); echo count($args) . ":" . $args[0] . ":" . $args[1] . ":"; +echo count($args[2]) . ":" . $args[2][0] . ":" . $args[2][1] . ":"; $attrs = class_get_attributes("EvalAotClassAttrTarget"); echo count($attrs) . ":" . $attrs[0]->getName() . ":"; echo ($attrs[0]->isRepeated() ? "R" : "r") . ":"; echo $attrs[0]->getArguments()[0] . ":" . $attrs[0]->getArguments()[1] . ":"; -echo $attrs[0]->getTarget() . ":" . $attrs[0]->newInstance()->label() . ":"; +echo count($attrs[0]->getArguments()[2]) . ":" . $attrs[0]->getArguments()[2][0] . ":"; +$firstInstance = $attrs[0]->newInstance(); +echo $attrs[0]->getTarget() . ":" . $firstInstance->label() . ":" . count($firstInstance->items) . ":" . $firstInstance->items[0] . ":"; $refAttrs = (new ReflectionClass("EvalAotClassAttrTarget"))->getAttributes(); echo count($refAttrs) . ":" . $refAttrs[1]->getArguments()[0] . ":"; +echo count($refAttrs[1]->getArguments()[2]) . ":" . $refAttrs[1]->getArguments()[2][0] . ":"; echo $refAttrs[1]->newInstance()->label();'); "#, ); @@ -9158,7 +9166,7 @@ echo $refAttrs[1]->newInstance()->label();'); ); assert_eq!( out.stdout, - "2:EvalAotClassAttr:EvalAotClassAttr:2:class:1:2:EvalAotClassAttr:R:class:1:1:class:1:2:again:again:2" + "2:EvalAotClassAttr:EvalAotClassAttr:3:class:1:2:nested:3:2:EvalAotClassAttr:R:class:1:2:nested:1:class:1:2:nested:2:again:1:later:again:2" ); } diff --git a/tests/codegen/oop/attributes.rs b/tests/codegen/oop/attributes.rs index f4143365d6..4dc363663c 100644 --- a/tests/codegen/oop/attributes.rs +++ b/tests/codegen/oop/attributes.rs @@ -650,6 +650,58 @@ echo $instance->named; assert_eq!(out, "2:1.5:-2.25:1.5:-2.25:1.5:-2.25"); } +/// Verifies positional array literal attribute args survive helper reads and instantiation. +#[test] +fn test_class_attribute_args_preserves_array_literals() { + let out = compile_and_run( + r#"getArguments(); +echo ":"; +echo count($attrArgs[0]); +echo ":"; +echo $attrArgs[0][0]; +echo ":"; +echo $attrArgs[0][1]; +echo ":"; +echo $attrArgs[0][2] ? "T" : "F"; +echo ":"; +echo is_null($attrArgs[0][3]) ? "N" : "bad"; + +$instance = $attr->newInstance(); +echo ":"; +echo count($instance->items); +echo ":"; +echo $instance->items[0]; +echo ":"; +echo $instance->items[1]; +echo ":"; +echo $instance->items[2] ? "T" : "F"; +echo ":"; +echo is_null($instance->items[3]) ? "N" : "bad"; +"#, + ); + assert_eq!(out, "4:one:2:T:N:4:one:2:T:N:4:one:2:T:N"); +} + /// Verifies that `ClassName::class` attribute args are retained as strings. #[test] fn test_class_attribute_args_preserves_class_name_literals() { From 724c9c4abe3816d1a108ea422f166d39d12136ef Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 17:03:16 +0200 Subject: [PATCH 0641/1208] Support eval AOT mixed ref bridges --- docs/php/eval.md | 17 +- src/codegen/eval_constructor_helpers.rs | 154 +++++++++++++-- src/codegen/eval_method_helpers.rs | 239 +++++++++++++++++++++--- src/codegen/eval_ref_arg_helpers.rs | 218 +++++++++++++++++++++ src/codegen/lower_inst/builtins/eval.rs | 5 +- src/codegen/mod.rs | 1 + tests/codegen/eval.rs | 58 ++++++ 7 files changed, 633 insertions(+), 59 deletions(-) create mode 100644 src/codegen/eval_ref_arg_helpers.rs diff --git a/docs/php/eval.md b/docs/php/eval.md index d52de40f0b..d689bd28a8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch still only marks non-by-reference signatures as invocable. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures and `mixed`/untyped by-reference constructor parameters; typed by-reference constructor parameters remain metadata-only. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch still only marks non-by-reference signatures as invocable. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures and `mixed`/untyped by-reference parameters; typed by-reference method parameters remain metadata-only. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -131,7 +131,8 @@ fallback supports the same named-argument and positional variadic-tail binding for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameter signatures, including registered by-reference lvalue validation, when the current eval class scope satisfies PHP visibility. Generated AOT bridge -dispatch still only marks non-by-reference signatures as invocable. +dispatch can invoke `mixed`/untyped by-reference method and constructor +parameters, while typed by-reference parameters remain metadata-only. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -703,9 +704,9 @@ particular, advanced native callable descriptors and closure callback values are still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch -still remains limited to visibility-checked non-by-reference -scalar/nullable-int/Mixed/array/iterable/object parameter signatures, including -the generated positional variadic array slot when present. Broader +is limited to visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object +parameter signatures plus `mixed`/untyped by-reference method and constructor +parameters, including the generated positional variadic array slot when present. Broader parameter/return ABI shapes are still outside those bridge paths. Eval class support is still smaller than the full static class system. @@ -725,8 +726,8 @@ scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/null `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while -unsupported bridge shapes remain metadata-only rather than invocable through -eval. +typed by-reference bridge shapes and other unsupported bridge shapes remain +metadata-only rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 5626b3d1ae..4603d7a2e8 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -9,8 +9,8 @@ //! - The cacheable runtime object can allocate by name, but only user assembly //! knows constructor symbols and parameter ABI shapes. //! - Classes without constructors are treated as successful no-ops, matching PHP. -//! - Constructors are bridged for non-by-ref scalar/Mixed/array/object arguments, -//! including a generated variadic array slot when the signature has one. +//! - Constructors are bridged for scalar/Mixed/array/object arguments, including +//! generated variadic array slots and untyped/Mixed by-reference parameters. //! - Non-public constructors are accepted when the active eval class scope //! satisfies PHP visibility. @@ -25,6 +25,13 @@ use crate::names::{method_symbol, php_symbol_key}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, FunctionSig, PhpType}; +use super::eval_ref_arg_helpers::{ + EvalMixedRefArgSlot, eval_abi_param_types_for_refs, eval_arg_temp_slot_size, + eval_mixed_ref_arg_slots, eval_normalized_ref_params, + eval_signature_mixed_ref_params_supported, emit_aarch64_write_back_mixed_ref_args, + emit_x86_64_write_back_mixed_ref_args, +}; + const MAX_EVAL_CONSTRUCTOR_ARGS: usize = 8; const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ "Error", @@ -58,6 +65,7 @@ struct EvalConstructorSlot { visibility: Visibility, allowed_scopes: Vec, params: Vec, + ref_params: Vec, supported: bool, } @@ -155,6 +163,11 @@ fn collect_class_constructor_slot( } else { Vec::new() }; + let ref_params = if supported { + eval_normalized_ref_params(sig.params.len(), &sig.ref_params) + } else { + Vec::new() + }; slots.push(EvalConstructorSlot { class_id: class_info.class_id, class_name: class_name.to_string(), @@ -162,6 +175,7 @@ fn collect_class_constructor_slot( visibility: visibility.clone(), allowed_scopes: visibility_scope_names(module, impl_class, visibility), params, + ref_params, supported, }); } @@ -185,7 +199,7 @@ fn constructor_visibility_supported(visibility: &Visibility) -> bool { /// Returns true for constructor signatures supported by this eval bridge slice. fn constructor_signature_supported(sig: &FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_CONSTRUCTOR_ARGS - && sig.ref_params.iter().all(|is_ref| !*is_ref) + && eval_signature_mixed_ref_params_supported(sig) && sig .params .iter() @@ -543,12 +557,20 @@ fn emit_aarch64_constructor_body( emitter.label(&scope_ok_label); } emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = emit_aarch64_prepare_constructor_args(module, emitter, slot, fail_label); + let (overflow_bytes, ref_slots) = + emit_aarch64_prepare_constructor_args(module, emitter, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_aarch64_write_back_mixed_ref_args( + emitter, + &ref_slots, + 0, + &constructor_body_label(module, slot), + ); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); emitter.instruction(&format!("b {}", success_label)); // constructor returned normally } @@ -571,12 +593,20 @@ fn emit_x86_64_constructor_body( emitter.label(&scope_ok_label); } emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = emit_x86_64_prepare_constructor_args(module, emitter, slot, fail_label); + let (overflow_bytes, ref_slots) = + emit_x86_64_prepare_constructor_args(module, emitter, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); + emit_x86_64_write_back_mixed_ref_args( + emitter, + &ref_slots, + 0, + &constructor_body_label(module, slot), + ); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); emitter.instruction(&format!("jmp {}", success_label)); // constructor returned normally } @@ -664,17 +694,39 @@ fn emit_aarch64_prepare_constructor_args( emitter: &mut Emitter, slot: &EvalConstructorSlot, fail_label: &str, -) -> usize { +) -> (usize, Vec) { + let ref_slots = emit_aarch64_constructor_mixed_ref_arg_cells(module, emitter, &slot.ref_params); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first constructor argument abi::emit_push_result_value(emitter, &receiver_ty); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { - emit_aarch64_load_eval_arg(module, emitter, index); - let label_prefix = constructor_arg_label(module, slot, index); - emit_aarch64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); - abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_aarch64_load_eval_arg(module, emitter, index); + let label_prefix = constructor_arg_label(module, slot, index); + emit_aarch64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - materialize_constructor_args(module, emitter, &receiver_ty, &slot.params) + ( + materialize_constructor_args( + module, + emitter, + &receiver_ty, + &slot.params, + &slot.ref_params, + ), + ref_slots, + ) } /// Prepares x86_64 constructor ABI registers for the supported argument shapes. @@ -683,17 +735,39 @@ fn emit_x86_64_prepare_constructor_args( emitter: &mut Emitter, slot: &EvalConstructorSlot, fail_label: &str, -) -> usize { +) -> (usize, Vec) { + let ref_slots = emit_x86_64_constructor_mixed_ref_arg_cells(module, emitter, &slot.ref_params); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first constructor argument abi::emit_push_result_value(emitter, &receiver_ty); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { - emit_x86_64_load_eval_arg(module, emitter, index); - let label_prefix = constructor_arg_label(module, slot, index); - emit_x86_64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); - abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_x86_64_load_eval_arg(module, emitter, index); + let label_prefix = constructor_arg_label(module, slot, index); + emit_x86_64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - materialize_constructor_args(module, emitter, &receiver_ty, &slot.params) + ( + materialize_constructor_args( + module, + emitter, + &receiver_ty, + &slot.params, + &slot.ref_params, + ), + ref_slots, + ) } /// Materializes the pushed receiver and eval arguments into the target method ABI. @@ -702,14 +776,49 @@ fn materialize_constructor_args( emitter: &mut Emitter, receiver_ty: &PhpType, params: &[PhpType], + ref_params: &[bool], ) -> usize { let mut arg_types = Vec::with_capacity(params.len() + 1); arg_types.push(receiver_ty.clone()); - arg_types.extend(params.iter().map(|param| param.codegen_repr())); + arg_types.extend(eval_abi_param_types_for_refs(params, ref_params)); let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); abi::materialize_outgoing_args(emitter, &assignments) } +/// Prepares ARM64 stack cells for eval-supplied by-reference constructor arguments. +fn emit_aarch64_constructor_mixed_ref_arg_cells( + module: &Module, + emitter: &mut Emitter, + ref_params: &[bool], +) -> Vec { + let ref_slots = eval_mixed_ref_arg_slots(ref_params); + for slot in &ref_slots { + emit_aarch64_load_eval_arg(module, emitter, slot.param_index); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the original eval Mixed cell for by-reference writeback + abi::emit_push_result_value(emitter, &PhpType::Mixed); + emitter.instruction("ldr x0, [x29, #-16]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } + ref_slots +} + +/// Prepares x86_64 stack cells for eval-supplied by-reference constructor arguments. +fn emit_x86_64_constructor_mixed_ref_arg_cells( + module: &Module, + emitter: &mut Emitter, + ref_params: &[bool], +) -> Vec { + let ref_slots = eval_mixed_ref_arg_slots(ref_params); + for slot in &ref_slots { + emit_x86_64_load_eval_arg(module, emitter, slot.param_index); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the original eval Mixed cell for by-reference writeback + abi::emit_push_result_value(emitter, &PhpType::Mixed); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } + ref_slots +} + /// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. fn emit_aarch64_load_eval_arg(module: &Module, emitter: &mut Emitter, index: usize) { let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); @@ -1046,6 +1155,15 @@ fn constructor_arg_label(module: &Module, slot: &EvalConstructorSlot, index: usi ) } +/// Returns a label-safe constructor body prefix for bridge-local writeback branches. +fn constructor_body_label(module: &Module, slot: &EvalConstructorSlot) -> String { + let suffix = match module.target.arch { + Arch::AArch64 => "", + Arch::X86_64 => "_x", + }; + format!("__elephc_eval_constructor_{}{}", slot.class_id, suffix) +} + /// Returns a platform-safe label for a successful scoped constructor access check. fn constructor_scope_ok_label(module: &Module, slot: &EvalConstructorSlot) -> String { let suffix = match module.target.arch { diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 3f0cc5ace4..4d6700df45 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -23,6 +23,13 @@ use crate::names::{method_symbol, static_method_symbol}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; +use super::eval_ref_arg_helpers::{ + EvalMixedRefArgSlot, eval_abi_param_types_for_refs, eval_arg_temp_slot_size, + eval_mixed_ref_arg_slots, eval_normalized_ref_params, + eval_signature_mixed_ref_params_supported, emit_aarch64_write_back_mixed_ref_args, + emit_x86_64_write_back_mixed_ref_args, +}; + /// Method metadata needed by eval method-call bridge dispatch. #[derive(Clone)] struct EvalMethodSlot { @@ -33,6 +40,7 @@ struct EvalMethodSlot { visibility: Visibility, allowed_scopes: Vec, params: Vec, + ref_params: Vec, return_ty: PhpType, } @@ -46,6 +54,7 @@ struct EvalStaticMethodSlot { visibility: Visibility, allowed_scopes: Vec, params: Vec, + ref_params: Vec, return_ty: PhpType, } @@ -189,6 +198,7 @@ fn collect_class_method_slots( visibility: visibility.clone(), allowed_scopes: visibility_scope_names(module, impl_class, visibility), params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), + ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), return_ty: sig.return_type.codegen_repr(), }); } @@ -228,6 +238,7 @@ fn collect_class_static_method_slots( visibility: visibility.clone(), allowed_scopes: visibility_scope_names(module, impl_class, visibility), params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), + ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), return_ty: sig.return_type.codegen_repr(), }); } @@ -260,7 +271,7 @@ fn method_visibility_supported(visibility: &Visibility) -> bool { /// Returns true for method signatures supported by the eval bridge. fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_METHOD_ARGS - && sig.ref_params.iter().all(|is_ref| !*is_ref) + && eval_signature_mixed_ref_params_supported(sig) && sig.params.iter().all(|(_, ty)| method_param_supported(ty)) } @@ -962,7 +973,7 @@ fn emit_aarch64_method_bodies( for slot in slots { emitter.label(&method_body_label(module, slot)); emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = + let (overflow_bytes, ref_slots) = emit_aarch64_prepare_method_args(module, emitter, data, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); @@ -971,6 +982,11 @@ fn emit_aarch64_method_bodies( abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); + preserve_result_and_write_back_aarch64_ref_args( + emitter, + &ref_slots, + &method_body_label(module, slot), + ); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result } } @@ -987,7 +1003,7 @@ fn emit_x86_64_method_bodies( for slot in slots { emitter.label(&method_body_label(module, slot)); emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = + let (overflow_bytes, ref_slots) = emit_x86_64_prepare_method_args(module, emitter, data, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); @@ -996,6 +1012,11 @@ fn emit_x86_64_method_bodies( abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); + preserve_result_and_write_back_x86_64_ref_args( + emitter, + &ref_slots, + &method_body_label(module, slot), + ); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result } } @@ -1012,7 +1033,7 @@ fn emit_aarch64_static_method_bodies( for slot in slots { emitter.label(&static_method_body_label(module, slot)); emit_aarch64_validate_static_method_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = + let (overflow_bytes, ref_slots) = emit_aarch64_prepare_static_method_args(module, emitter, data, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); @@ -1024,6 +1045,11 @@ fn emit_aarch64_static_method_bodies( abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); + preserve_result_and_write_back_aarch64_ref_args( + emitter, + &ref_slots, + &static_method_body_label(module, slot), + ); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native static method result } } @@ -1040,7 +1066,7 @@ fn emit_x86_64_static_method_bodies( for slot in slots { emitter.label(&static_method_body_label(module, slot)); emit_x86_64_validate_static_method_arg_count(module, emitter, slot, fail_label); - let overflow_bytes = + let (overflow_bytes, ref_slots) = emit_x86_64_prepare_static_method_args(module, emitter, data, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); @@ -1052,6 +1078,11 @@ fn emit_x86_64_static_method_bodies( abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); + preserve_result_and_write_back_x86_64_ref_args( + emitter, + &ref_slots, + &static_method_body_label(module, slot), + ); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native static method result } } @@ -1123,17 +1154,44 @@ fn emit_aarch64_prepare_method_args( data: &mut DataSection, slot: &EvalMethodSlot, fail_label: &str, -) -> usize { +) -> (usize, Vec) { + let ref_slots = emit_aarch64_mixed_ref_arg_cells( + module, + emitter, + &slot.ref_params, + 24, + ); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first method argument abi::emit_push_result_value(emitter, &receiver_ty); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { - emit_aarch64_load_eval_arg(module, emitter, index, 24); - let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); - emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); - abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_aarch64_load_eval_arg(module, emitter, index, 24); + let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); + emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - materialize_method_args(module, emitter, &receiver_ty, &slot.params) + ( + materialize_method_args( + module, + emitter, + &receiver_ty, + &slot.params, + &slot.ref_params, + ), + ref_slots, + ) } /// Prepares ARM64 static method ABI registers for the supported argument shapes. @@ -1143,16 +1201,34 @@ fn emit_aarch64_prepare_static_method_args( data: &mut DataSection, slot: &EvalStaticMethodSlot, fail_label: &str, -) -> usize { +) -> (usize, Vec) { + let ref_slots = emit_aarch64_mixed_ref_arg_cells( + module, + emitter, + &slot.ref_params, + 40, + ); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); abi::emit_load_int_immediate(emitter, "x0", slot.class_id as i64); abi::emit_push_result_value(emitter, &PhpType::Int); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&PhpType::Int); for (index, param_ty) in slot.params.iter().enumerate() { - emit_aarch64_load_eval_arg(module, emitter, index, 40); - let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); - emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); - abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_aarch64_load_eval_arg(module, emitter, index, 40); + let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); + emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - materialize_static_method_args(module, emitter, slot) + (materialize_static_method_args(module, emitter, slot), ref_slots) } /// Prepares x86_64 method ABI registers for the supported argument shapes. @@ -1162,17 +1238,39 @@ fn emit_x86_64_prepare_method_args( data: &mut DataSection, slot: &EvalMethodSlot, fail_label: &str, -) -> usize { +) -> (usize, Vec) { + let ref_slots = emit_x86_64_mixed_ref_arg_cells(module, emitter, &slot.ref_params); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first method argument abi::emit_push_result_value(emitter, &receiver_ty); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { - emit_x86_64_load_eval_arg(module, emitter, index); - let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); - emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); - abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_x86_64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); + emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - materialize_method_args(module, emitter, &receiver_ty, &slot.params) + ( + materialize_method_args( + module, + emitter, + &receiver_ty, + &slot.params, + &slot.ref_params, + ), + ref_slots, + ) } /// Prepares x86_64 static method ABI registers for the supported argument shapes. @@ -1182,16 +1280,29 @@ fn emit_x86_64_prepare_static_method_args( data: &mut DataSection, slot: &EvalStaticMethodSlot, fail_label: &str, -) -> usize { +) -> (usize, Vec) { + let ref_slots = emit_x86_64_mixed_ref_arg_cells(module, emitter, &slot.ref_params); + let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); abi::emit_load_int_immediate(emitter, "rax", slot.class_id as i64); abi::emit_push_result_value(emitter, &PhpType::Int); + let mut arg_temp_bytes = eval_arg_temp_slot_size(&PhpType::Int); for (index, param_ty) in slot.params.iter().enumerate() { - emit_x86_64_load_eval_arg(module, emitter, index); - let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); - emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); - abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + abi::emit_temporary_stack_address( + emitter, + abi::int_result_reg(emitter), + arg_temp_bytes + ref_slot.raw_offset, + ); + abi::emit_push_result_value(emitter, &PhpType::Int); + } else { + emit_x86_64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); + emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + } + arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - materialize_static_method_args(module, emitter, slot) + (materialize_static_method_args(module, emitter, slot), ref_slots) } /// Materializes the pushed receiver and eval arguments into the target method ABI. @@ -1200,10 +1311,11 @@ fn materialize_method_args( emitter: &mut Emitter, receiver_ty: &PhpType, params: &[PhpType], + ref_params: &[bool], ) -> usize { let mut arg_types = Vec::with_capacity(params.len() + 1); arg_types.push(receiver_ty.clone()); - arg_types.extend(params.iter().map(|param| param.codegen_repr())); + arg_types.extend(eval_abi_param_types_for_refs(params, ref_params)); let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); abi::materialize_outgoing_args(emitter, &assignments) } @@ -1216,11 +1328,76 @@ fn materialize_static_method_args( ) -> usize { let mut arg_types = Vec::with_capacity(slot.params.len() + 1); arg_types.push(PhpType::Int); - arg_types.extend(slot.params.iter().map(|param| param.codegen_repr())); + arg_types.extend(eval_abi_param_types_for_refs(&slot.params, &slot.ref_params)); let assignments = abi::build_outgoing_arg_assignments_for_target(module.target, &arg_types, 0); abi::materialize_outgoing_args(emitter, &assignments) } +/// Prepares ARM64 stack cells for eval-supplied by-reference Mixed arguments. +fn emit_aarch64_mixed_ref_arg_cells( + module: &Module, + emitter: &mut Emitter, + ref_params: &[bool], + arg_array_frame_offset: usize, +) -> Vec { + let ref_slots = eval_mixed_ref_arg_slots(ref_params); + for slot in &ref_slots { + emit_aarch64_load_eval_arg(module, emitter, slot.param_index, arg_array_frame_offset); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the original eval Mixed cell for by-reference writeback + abi::emit_push_result_value(emitter, &PhpType::Mixed); + emitter.instruction("ldr x0, [x29, #-16]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } + ref_slots +} + +/// Prepares x86_64 stack cells for eval-supplied by-reference Mixed arguments. +fn emit_x86_64_mixed_ref_arg_cells( + module: &Module, + emitter: &mut Emitter, + ref_params: &[bool], +) -> Vec { + let ref_slots = eval_mixed_ref_arg_slots(ref_params); + for slot in &ref_slots { + emit_x86_64_load_eval_arg(module, emitter, slot.param_index); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the original eval Mixed cell for by-reference writeback + abi::emit_push_result_value(emitter, &PhpType::Mixed); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } + ref_slots +} + +/// Preserves an ARM64 boxed return value while by-reference eval args are written back. +fn preserve_result_and_write_back_aarch64_ref_args( + emitter: &mut Emitter, + ref_slots: &[EvalMixedRefArgSlot], + label_prefix: &str, +) { + if ref_slots.is_empty() { + return; + } + abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); + emit_aarch64_write_back_mixed_ref_args(emitter, ref_slots, 16, label_prefix); + abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); +} + +/// Preserves an x86_64 boxed return value while by-reference eval args are written back. +fn preserve_result_and_write_back_x86_64_ref_args( + emitter: &mut Emitter, + ref_slots: &[EvalMixedRefArgSlot], + label_prefix: &str, +) { + if ref_slots.is_empty() { + return; + } + abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); + emit_x86_64_write_back_mixed_ref_args(emitter, ref_slots, 16, label_prefix); + abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); +} + /// Loads one eval argument into an ARM64 spill slot as a boxed Mixed cell. fn emit_aarch64_load_eval_arg( module: &Module, diff --git a/src/codegen/eval_ref_arg_helpers.rs b/src/codegen/eval_ref_arg_helpers.rs new file mode 100644 index 0000000000..954814ef8b --- /dev/null +++ b/src/codegen/eval_ref_arg_helpers.rs @@ -0,0 +1,218 @@ +//! Purpose: +//! Shares the by-reference argument bridge helpers used by eval-dispatched AOT +//! methods and constructors. +//! +//! Called from: +//! - `crate::codegen::eval_method_helpers` +//! - `crate::codegen::eval_constructor_helpers` +//! - `crate::codegen::lower_inst::builtins::eval` +//! +//! Key details: +//! - The generated eval bridge can invoke by-reference AOT parameters only when +//! the parameter storage is already a boxed `Mixed` cell. +//! - Typed by-reference bridge dispatch stays disabled until raw typed temp +//! writeback is implemented for each supported ABI shape. + +use crate::codegen::abi; +use crate::codegen::emit::Emitter; +use crate::types::{FunctionSig, PhpType}; + +/// Describes the stack storage for one eval-supplied by-reference `Mixed` argument. +#[derive(Clone)] +pub(crate) struct EvalMixedRefArgSlot { + pub(crate) param_index: usize, + pub(crate) raw_offset: usize, + pub(crate) original_offset: usize, +} + +const EVAL_MIXED_REF_ARG_BYTES: usize = 32; + +/// Returns true when an eval bridge by-reference parameter can use Mixed-cell storage. +pub(crate) fn eval_mixed_ref_param_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) +} + +/// Returns true when every by-reference parameter in a native signature is bridgeable. +pub(crate) fn eval_signature_mixed_ref_params_supported(signature: &FunctionSig) -> bool { + signature.params.iter().enumerate().all(|(index, (_, ty))| { + !signature.ref_params.get(index).copied().unwrap_or(false) + || eval_mixed_ref_param_supported(ty) + }) +} + +/// Normalizes a sparse ref-parameter vector to the visible parameter count. +pub(crate) fn eval_normalized_ref_params(param_count: usize, ref_params: &[bool]) -> Vec { + (0..param_count) + .map(|index| ref_params.get(index).copied().unwrap_or(false)) + .collect() +} + +/// Converts visible parameter types to the ABI shape used by eval bridge calls. +pub(crate) fn eval_abi_param_types_for_refs( + param_types: &[PhpType], + ref_params: &[bool], +) -> Vec { + param_types + .iter() + .enumerate() + .map(|(index, ty)| { + if ref_params.get(index).copied().unwrap_or(false) { + PhpType::Int + } else { + ty.codegen_repr() + } + }) + .collect() +} + +/// Plans the stack offsets for eval `Mixed` by-reference argument cells. +pub(crate) fn eval_mixed_ref_arg_slots(ref_params: &[bool]) -> Vec { + let total = ref_params.iter().filter(|is_ref| **is_ref).count(); + let mut seen = 0usize; + let mut slots = Vec::with_capacity(total); + for (param_index, is_ref) in ref_params.iter().enumerate() { + if !*is_ref { + continue; + } + let reverse_index = total - seen - 1; + let base_offset = reverse_index * EVAL_MIXED_REF_ARG_BYTES; + slots.push(EvalMixedRefArgSlot { + param_index, + raw_offset: base_offset, + original_offset: base_offset + 16, + }); + seen += 1; + } + slots +} + +/// Returns the temporary outgoing-argument stack slot size for one ABI argument. +pub(crate) fn eval_arg_temp_slot_size(ty: &PhpType) -> usize { + if matches!(ty.codegen_repr(), PhpType::Void | PhpType::Never) { + 0 + } else { + 16 + } +} + +/// Writes changed ARM64 `Mixed` ref-argument cells back into the original eval cells. +pub(crate) fn emit_aarch64_write_back_mixed_ref_args( + emitter: &mut Emitter, + ref_slots: &[EvalMixedRefArgSlot], + stack_offset: usize, + label_prefix: &str, +) { + for slot in ref_slots { + let done_label = format!("{}_ref_{}_done", label_prefix, slot.param_index); + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("cmp x9, x10"); // skip writeback when the native call kept the same Mixed cell + emitter.instruction(&format!("b.eq {}", done_label)); // avoid self-copying and releasing the original cell payload + emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); + emitter.label(&done_label); + } +} + +/// Writes changed x86_64 `Mixed` ref-argument cells back into the original eval cells. +pub(crate) fn emit_x86_64_write_back_mixed_ref_args( + emitter: &mut Emitter, + ref_slots: &[EvalMixedRefArgSlot], + stack_offset: usize, + label_prefix: &str, +) { + for slot in ref_slots { + let done_label = format!("{}_ref_{}_done_x", label_prefix, slot.param_index); + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("cmp r10, r11"); // skip writeback when the native call kept the same Mixed cell + emitter.instruction(&format!("je {}", done_label)); // avoid self-copying and releasing the original cell payload + emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); + emitter.label(&done_label); + } +} + +/// Copies one replacement ARM64 Mixed cell payload into an existing target cell. +fn emit_aarch64_replace_mixed_cell( + emitter: &mut Emitter, + label_prefix: &str, + param_index: usize, + target_reg: &str, + replacement_reg: &str, +) { + let release_string = format!("{}_ref_{}_release_string", label_prefix, param_index); + let copy_new = format!("{}_ref_{}_copy_new", label_prefix, param_index); + let done = format!("{}_ref_{}_assign_done", label_prefix, param_index); + + abi::emit_push_reg_pair(emitter, target_reg, replacement_reg); + emitter.instruction("ldr x9, [sp]"); // reload the original Mixed cell pointer for old-payload inspection + emitter.instruction("ldr x11, [x9]"); // inspect the old payload tag before overwriting the cell + emitter.instruction("cmp x11, #1"); // strings own a persisted heap payload that needs safe free + emitter.instruction(&format!("b.eq {}", release_string)); // release string payloads through the string-safe free path + emitter.instruction("cmp x11, #4"); // tags below array/hash/object/mixed are scalar payloads + emitter.instruction(&format!("b.lo {}", copy_new)); // scalar payloads can be overwritten directly + emitter.instruction("cmp x11, #7"); // tags above the refcounted payload range are not released here + emitter.instruction(&format!("b.hi {}", copy_new)); // unknown/null payload tags can be overwritten directly + emitter.instruction("ldr x0, [x9, #8]"); // pass the old refcounted child payload to the generic release helper + abi::emit_call_label(emitter, "__rt_decref_any"); + emitter.instruction(&format!("b {}", copy_new)); // continue with replacement after releasing the old child + emitter.label(&release_string); + emitter.instruction("ldr x9, [sp]"); // reload the original Mixed cell before reading its string payload + emitter.instruction("ldr x0, [x9, #8]"); // pass the old string payload pointer to the safe free helper + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + emitter.label(©_new); + emitter.instruction("ldr x9, [sp]"); // reload the original Mixed cell pointer for replacement + emitter.instruction("ldr x10, [sp, #8]"); // reload the replacement Mixed cell pointer + emitter.instruction("ldr x11, [x10]"); // copy the replacement runtime tag + emitter.instruction("str x11, [x9]"); // overwrite the target cell tag + emitter.instruction("ldr x11, [x10, #8]"); // copy the replacement low payload word + emitter.instruction("str x11, [x9, #8]"); // overwrite the target cell low payload word + emitter.instruction("ldr x11, [x10, #16]"); // copy the replacement high payload word + emitter.instruction("str x11, [x9, #16]"); // overwrite the target cell high payload word + emitter.instruction("mov x0, x10"); // pass the now-empty replacement cell storage to heap_free + abi::emit_call_label(emitter, "__rt_heap_free"); + emitter.label(&done); + abi::emit_release_temporary_stack(emitter, 16); +} + +/// Copies one replacement x86_64 Mixed cell payload into an existing target cell. +fn emit_x86_64_replace_mixed_cell( + emitter: &mut Emitter, + label_prefix: &str, + param_index: usize, + target_reg: &str, + replacement_reg: &str, +) { + let release_string = format!("{}_ref_{}_release_string_x", label_prefix, param_index); + let copy_new = format!("{}_ref_{}_copy_new_x", label_prefix, param_index); + let done = format!("{}_ref_{}_assign_done_x", label_prefix, param_index); + + abi::emit_push_reg_pair(emitter, target_reg, replacement_reg); + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the original Mixed cell pointer for old-payload inspection + emitter.instruction("mov r9, QWORD PTR [r10]"); // inspect the old payload tag before overwriting the cell + emitter.instruction("cmp r9, 1"); // strings own a persisted heap payload that needs safe free + emitter.instruction(&format!("je {}", release_string)); // release string payloads through the string-safe free path + emitter.instruction("cmp r9, 4"); // tags below array/hash/object/mixed are scalar payloads + emitter.instruction(&format!("jl {}", copy_new)); // scalar payloads can be overwritten directly + emitter.instruction("cmp r9, 7"); // tags above the refcounted payload range are not released here + emitter.instruction(&format!("jg {}", copy_new)); // unknown/null payload tags can be overwritten directly + emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // pass the old refcounted child payload to the generic release helper + abi::emit_call_label(emitter, "__rt_decref_any"); + emitter.instruction(&format!("jmp {}", copy_new)); // continue with replacement after releasing the old child + emitter.label(&release_string); + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the original Mixed cell before reading its string payload + emitter.instruction("mov rax, QWORD PTR [r10 + 8]"); // pass the old string payload pointer to the safe free helper + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + emitter.label(©_new); + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the original Mixed cell pointer for replacement + emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // reload the replacement Mixed cell pointer + emitter.instruction("mov r9, QWORD PTR [r11]"); // copy the replacement runtime tag + emitter.instruction("mov QWORD PTR [r10], r9"); // overwrite the target cell tag + emitter.instruction("mov r9, QWORD PTR [r11 + 8]"); // copy the replacement low payload word + emitter.instruction("mov QWORD PTR [r10 + 8], r9"); // overwrite the target cell low payload word + emitter.instruction("mov r9, QWORD PTR [r11 + 16]"); // copy the replacement high payload word + emitter.instruction("mov QWORD PTR [r10 + 16], r9"); // overwrite the target cell high payload word + emitter.instruction("mov rax, r11"); // pass the now-empty replacement cell storage to heap_free + abi::emit_call_label(emitter, "__rt_heap_free"); + emitter.label(&done); + abi::emit_release_temporary_stack(emitter, 16); +} diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 556f80967a..36c0bf77c7 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -14,6 +14,7 @@ use std::path::Path; +use crate::codegen::eval_ref_arg_helpers::eval_signature_mixed_ref_params_supported; use crate::codegen::platform::Arch; use crate::codegen::{ abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, @@ -1013,7 +1014,7 @@ fn function_can_register_with_eval(function: &Function) -> bool { /// Returns true when eval can dispatch a native method through the generated bridge. fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS - && signature.ref_params.iter().all(|is_ref| !*is_ref) + && eval_signature_mixed_ref_params_supported(signature) && signature .params .iter() @@ -1024,7 +1025,7 @@ fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { /// Returns true when eval can dispatch a native constructor through the generated bridge. fn constructor_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS - && signature.ref_params.iter().all(|is_ref| !*is_ref) + && eval_signature_mixed_ref_params_supported(signature) && signature .params .iter() diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 9008bcbccd..3bd5a39005 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -15,6 +15,7 @@ mod eval_class_constant_helpers; mod eval_constructor_helpers; mod eval_method_helpers; mod eval_property_helpers; +mod eval_ref_arg_helpers; mod eval_reflection_helpers; mod eval_reflection_owner_helpers; mod eval_static_property_helpers; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9bb1263ca8..cf9d123fb2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5715,6 +5715,26 @@ echo (new EvalAotNamedMethodBox())->run(); assert_eq!(out, "AB"); } +/// Verifies eval dispatches generated/AOT instance methods with untyped by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_by_ref_arg() { + let out = compile_and_run( + r#"mutate($value); +return $value;'); +"#, + ); + assert_eq!(out, "15"); +} + /// Verifies eval preserves string values passed through an untyped AOT method parameter. #[test] fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_string_arg() { @@ -5770,6 +5790,25 @@ eval('echo EvalAotNamedStaticBox::join(right: "D", left: "C");'); assert_eq!(out, "CD"); } +/// Verifies eval dispatches generated/AOT static methods with untyped by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_mixed_by_ref_arg() { + let out = compile_and_run( + r#"x;'); assert_eq!(out, "11"); } +/// Verifies eval dispatches generated/AOT constructors with untyped by-reference params. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_mixed_by_ref_arg() { + let out = compile_and_run( + r#" Date: Thu, 25 Jun 2026 17:18:22 +0200 Subject: [PATCH 0642/1208] Support eval AOT typed ref scalars --- docs/php/eval.md | 20 +-- src/codegen/eval_constructor_helpers.rs | 93 ++++++++----- src/codegen/eval_method_helpers.rs | 127 ++++++++++++----- src/codegen/eval_ref_arg_helpers.rs | 172 +++++++++++++++++++----- src/codegen/lower_inst/builtins/eval.rs | 6 +- tests/codegen/eval.rs | 80 +++++++++++ 6 files changed, 380 insertions(+), 118 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index d689bd28a8..19c4cda147 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures and `mixed`/untyped by-reference constructor parameters; typed by-reference constructor parameters remain metadata-only. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped and scalar/nullable-int by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures and `mixed`/untyped by-reference parameters; typed by-reference method parameters remain metadata-only. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped and scalar/nullable-int by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -131,8 +131,8 @@ fallback supports the same named-argument and positional variadic-tail binding for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameter signatures, including registered by-reference lvalue validation, when the current eval class scope satisfies PHP visibility. Generated AOT bridge -dispatch can invoke `mixed`/untyped by-reference method and constructor -parameters, while typed by-reference parameters remain metadata-only. +dispatch can invoke `mixed`/untyped and scalar/nullable-int by-reference method +and constructor parameters. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -705,9 +705,10 @@ still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch is limited to visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object -parameter signatures plus `mixed`/untyped by-reference method and constructor -parameters, including the generated positional variadic array slot when present. Broader -parameter/return ABI shapes are still outside those bridge paths. +parameter signatures plus `mixed`/untyped and scalar/nullable-int by-reference +method and constructor parameters, including the generated positional variadic +array slot when present. Broader parameter/return ABI shapes are still outside +those bridge paths. Eval class support is still smaller than the full static class system. Eval-declared `__destruct()` hooks run when an eval-owned dynamic object reaches @@ -726,8 +727,9 @@ scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/null `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while -typed by-reference bridge shapes and other unsupported bridge shapes remain -metadata-only rather than invocable through eval. +remaining typed by-reference bridge shapes such as string, array, iterable, +and object references, plus other unsupported bridge shapes, remain metadata-only +rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 4603d7a2e8..2fca80b43f 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -10,7 +10,7 @@ //! knows constructor symbols and parameter ABI shapes. //! - Classes without constructors are treated as successful no-ops, matching PHP. //! - Constructors are bridged for scalar/Mixed/array/object arguments, including -//! generated variadic array slots and untyped/Mixed by-reference parameters. +//! generated variadic array slots and supported scalar/Mixed by-reference parameters. //! - Non-public constructors are accepted when the active eval class scope //! satisfies PHP visibility. @@ -26,10 +26,9 @@ use crate::parser::ast::Visibility; use crate::types::{ClassInfo, FunctionSig, PhpType}; use super::eval_ref_arg_helpers::{ - EvalMixedRefArgSlot, eval_abi_param_types_for_refs, eval_arg_temp_slot_size, - eval_mixed_ref_arg_slots, eval_normalized_ref_params, - eval_signature_mixed_ref_params_supported, emit_aarch64_write_back_mixed_ref_args, - emit_x86_64_write_back_mixed_ref_args, + EvalRefArgSlot, eval_abi_param_types_for_refs, eval_arg_temp_slot_size, + eval_normalized_ref_params, eval_ref_arg_slots, eval_signature_ref_params_supported, + emit_aarch64_write_back_ref_args, emit_x86_64_write_back_ref_args, }; const MAX_EVAL_CONSTRUCTOR_ARGS: usize = 8; @@ -199,7 +198,7 @@ fn constructor_visibility_supported(visibility: &Visibility) -> bool { /// Returns true for constructor signatures supported by this eval bridge slice. fn constructor_signature_supported(sig: &FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_CONSTRUCTOR_ARGS - && eval_signature_mixed_ref_params_supported(sig) + && eval_signature_ref_params_supported(sig) && sig .params .iter() @@ -564,7 +563,7 @@ fn emit_aarch64_constructor_body( abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); - emit_aarch64_write_back_mixed_ref_args( + emit_aarch64_write_back_ref_args( emitter, &ref_slots, 0, @@ -600,7 +599,7 @@ fn emit_x86_64_constructor_body( abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); - emit_x86_64_write_back_mixed_ref_args( + emit_x86_64_write_back_ref_args( emitter, &ref_slots, 0, @@ -694,8 +693,16 @@ fn emit_aarch64_prepare_constructor_args( emitter: &mut Emitter, slot: &EvalConstructorSlot, fail_label: &str, -) -> (usize, Vec) { - let ref_slots = emit_aarch64_constructor_mixed_ref_arg_cells(module, emitter, &slot.ref_params); +) -> (usize, Vec) { + let body_label = constructor_body_label(module, slot); + let ref_slots = emit_aarch64_constructor_ref_arg_cells( + module, + emitter, + &slot.params, + &slot.ref_params, + &body_label, + fail_label, + ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first constructor argument @@ -711,7 +718,7 @@ fn emit_aarch64_prepare_constructor_args( abi::emit_push_result_value(emitter, &PhpType::Int); } else { emit_aarch64_load_eval_arg(module, emitter, index); - let label_prefix = constructor_arg_label(module, slot, index); + let label_prefix = format!("{}_arg_{}", body_label, index); emit_aarch64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } @@ -735,8 +742,16 @@ fn emit_x86_64_prepare_constructor_args( emitter: &mut Emitter, slot: &EvalConstructorSlot, fail_label: &str, -) -> (usize, Vec) { - let ref_slots = emit_x86_64_constructor_mixed_ref_arg_cells(module, emitter, &slot.ref_params); +) -> (usize, Vec) { + let body_label = constructor_body_label(module, slot); + let ref_slots = emit_x86_64_constructor_ref_arg_cells( + module, + emitter, + &slot.params, + &slot.ref_params, + &body_label, + fail_label, + ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first constructor argument @@ -752,7 +767,7 @@ fn emit_x86_64_prepare_constructor_args( abi::emit_push_result_value(emitter, &PhpType::Int); } else { emit_x86_64_load_eval_arg(module, emitter, index); - let label_prefix = constructor_arg_label(module, slot, index); + let label_prefix = format!("{}_arg_{}", body_label, index); emit_x86_64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } @@ -786,35 +801,53 @@ fn materialize_constructor_args( } /// Prepares ARM64 stack cells for eval-supplied by-reference constructor arguments. -fn emit_aarch64_constructor_mixed_ref_arg_cells( +fn emit_aarch64_constructor_ref_arg_cells( module: &Module, emitter: &mut Emitter, + param_types: &[PhpType], ref_params: &[bool], -) -> Vec { - let ref_slots = eval_mixed_ref_arg_slots(ref_params); + label_prefix: &str, + fail_label: &str, +) -> Vec { + let ref_slots = eval_ref_arg_slots(param_types, ref_params); for slot in &ref_slots { emit_aarch64_load_eval_arg(module, emitter, slot.param_index); emitter.instruction("ldr x0, [x29, #-16]"); // reload the original eval Mixed cell for by-reference writeback abi::emit_push_result_value(emitter, &PhpType::Mixed); - emitter.instruction("ldr x0, [x29, #-16]"); // seed the mutable by-reference Mixed slot with the original cell - abi::emit_push_result_value(emitter, &PhpType::Mixed); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emitter.instruction("ldr x0, [x29, #-16]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } else { + let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); + emit_aarch64_cast_eval_arg(module, emitter, &slot.param_ty, &arg_label, fail_label); + abi::emit_push_result_value(emitter, &slot.param_ty); + } } ref_slots } /// Prepares x86_64 stack cells for eval-supplied by-reference constructor arguments. -fn emit_x86_64_constructor_mixed_ref_arg_cells( +fn emit_x86_64_constructor_ref_arg_cells( module: &Module, emitter: &mut Emitter, + param_types: &[PhpType], ref_params: &[bool], -) -> Vec { - let ref_slots = eval_mixed_ref_arg_slots(ref_params); + label_prefix: &str, + fail_label: &str, +) -> Vec { + let ref_slots = eval_ref_arg_slots(param_types, ref_params); for slot in &ref_slots { emit_x86_64_load_eval_arg(module, emitter, slot.param_index); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the original eval Mixed cell for by-reference writeback abi::emit_push_result_value(emitter, &PhpType::Mixed); - emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // seed the mutable by-reference Mixed slot with the original cell - abi::emit_push_result_value(emitter, &PhpType::Mixed); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } else { + let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); + emit_x86_64_cast_eval_arg(module, emitter, &slot.param_ty, &arg_label, fail_label); + abi::emit_push_result_value(emitter, &slot.param_ty); + } } ref_slots } @@ -1143,18 +1176,6 @@ fn grouped_slots(slots: &[EvalConstructorSlot]) -> BTreeMap String { - let suffix = match module.target.arch { - Arch::AArch64 => "", - Arch::X86_64 => "_x", - }; - format!( - "__elephc_eval_constructor_{}_arg_{}{}", - slot.class_id, index, suffix - ) -} - /// Returns a label-safe constructor body prefix for bridge-local writeback branches. fn constructor_body_label(module: &Module, slot: &EvalConstructorSlot) -> String { let suffix = match module.target.arch { diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 4d6700df45..ac81f2bad7 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -24,10 +24,9 @@ use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; use super::eval_ref_arg_helpers::{ - EvalMixedRefArgSlot, eval_abi_param_types_for_refs, eval_arg_temp_slot_size, - eval_mixed_ref_arg_slots, eval_normalized_ref_params, - eval_signature_mixed_ref_params_supported, emit_aarch64_write_back_mixed_ref_args, - emit_x86_64_write_back_mixed_ref_args, + EvalRefArgSlot, eval_abi_param_types_for_refs, eval_arg_temp_slot_size, + eval_normalized_ref_params, eval_ref_arg_slots, eval_signature_ref_params_supported, + emit_aarch64_write_back_ref_args, emit_x86_64_write_back_ref_args, }; /// Method metadata needed by eval method-call bridge dispatch. @@ -271,7 +270,7 @@ fn method_visibility_supported(visibility: &Visibility) -> bool { /// Returns true for method signatures supported by the eval bridge. fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { sig.params.len() <= MAX_EVAL_METHOD_ARGS - && eval_signature_mixed_ref_params_supported(sig) + && eval_signature_ref_params_supported(sig) && sig.params.iter().all(|(_, ty)| method_param_supported(ty)) } @@ -1154,12 +1153,17 @@ fn emit_aarch64_prepare_method_args( data: &mut DataSection, slot: &EvalMethodSlot, fail_label: &str, -) -> (usize, Vec) { - let ref_slots = emit_aarch64_mixed_ref_arg_cells( +) -> (usize, Vec) { + let body_label = method_body_label(module, slot); + let ref_slots = emit_aarch64_ref_arg_cells( module, emitter, + data, + &slot.params, &slot.ref_params, 24, + &body_label, + fail_label, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); @@ -1176,7 +1180,7 @@ fn emit_aarch64_prepare_method_args( abi::emit_push_result_value(emitter, &PhpType::Int); } else { emit_aarch64_load_eval_arg(module, emitter, index, 24); - let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); + let label_prefix = format!("{}_arg_{}", body_label, index); emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } @@ -1201,12 +1205,17 @@ fn emit_aarch64_prepare_static_method_args( data: &mut DataSection, slot: &EvalStaticMethodSlot, fail_label: &str, -) -> (usize, Vec) { - let ref_slots = emit_aarch64_mixed_ref_arg_cells( +) -> (usize, Vec) { + let body_label = static_method_body_label(module, slot); + let ref_slots = emit_aarch64_ref_arg_cells( module, emitter, + data, + &slot.params, &slot.ref_params, 40, + &body_label, + fail_label, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); abi::emit_load_int_immediate(emitter, "x0", slot.class_id as i64); @@ -1222,7 +1231,7 @@ fn emit_aarch64_prepare_static_method_args( abi::emit_push_result_value(emitter, &PhpType::Int); } else { emit_aarch64_load_eval_arg(module, emitter, index, 40); - let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); + let label_prefix = format!("{}_arg_{}", body_label, index); emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } @@ -1238,8 +1247,17 @@ fn emit_x86_64_prepare_method_args( data: &mut DataSection, slot: &EvalMethodSlot, fail_label: &str, -) -> (usize, Vec) { - let ref_slots = emit_x86_64_mixed_ref_arg_cells(module, emitter, &slot.ref_params); +) -> (usize, Vec) { + let body_label = method_body_label(module, slot); + let ref_slots = emit_x86_64_ref_arg_cells( + module, + emitter, + data, + &slot.params, + &slot.ref_params, + &body_label, + fail_label, + ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // load the unboxed receiver as the first method argument @@ -1255,7 +1273,7 @@ fn emit_x86_64_prepare_method_args( abi::emit_push_result_value(emitter, &PhpType::Int); } else { emit_x86_64_load_eval_arg(module, emitter, index); - let label_prefix = format!("{}_arg_{}", method_body_label(module, slot), index); + let label_prefix = format!("{}_arg_{}", body_label, index); emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } @@ -1280,8 +1298,17 @@ fn emit_x86_64_prepare_static_method_args( data: &mut DataSection, slot: &EvalStaticMethodSlot, fail_label: &str, -) -> (usize, Vec) { - let ref_slots = emit_x86_64_mixed_ref_arg_cells(module, emitter, &slot.ref_params); +) -> (usize, Vec) { + let body_label = static_method_body_label(module, slot); + let ref_slots = emit_x86_64_ref_arg_cells( + module, + emitter, + data, + &slot.params, + &slot.ref_params, + &body_label, + fail_label, + ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); abi::emit_load_int_immediate(emitter, "rax", slot.class_id as i64); abi::emit_push_result_value(emitter, &PhpType::Int); @@ -1296,7 +1323,7 @@ fn emit_x86_64_prepare_static_method_args( abi::emit_push_result_value(emitter, &PhpType::Int); } else { emit_x86_64_load_eval_arg(module, emitter, index); - let label_prefix = format!("{}_arg_{}", static_method_body_label(module, slot), index); + let label_prefix = format!("{}_arg_{}", body_label, index); emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } @@ -1333,37 +1360,71 @@ fn materialize_static_method_args( abi::materialize_outgoing_args(emitter, &assignments) } -/// Prepares ARM64 stack cells for eval-supplied by-reference Mixed arguments. -fn emit_aarch64_mixed_ref_arg_cells( +/// Prepares ARM64 stack cells for eval-supplied by-reference arguments. +fn emit_aarch64_ref_arg_cells( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, + param_types: &[PhpType], ref_params: &[bool], arg_array_frame_offset: usize, -) -> Vec { - let ref_slots = eval_mixed_ref_arg_slots(ref_params); + label_prefix: &str, + fail_label: &str, +) -> Vec { + let ref_slots = eval_ref_arg_slots(param_types, ref_params); for slot in &ref_slots { emit_aarch64_load_eval_arg(module, emitter, slot.param_index, arg_array_frame_offset); emitter.instruction("ldr x0, [x29, #-16]"); // reload the original eval Mixed cell for by-reference writeback abi::emit_push_result_value(emitter, &PhpType::Mixed); - emitter.instruction("ldr x0, [x29, #-16]"); // seed the mutable by-reference Mixed slot with the original cell - abi::emit_push_result_value(emitter, &PhpType::Mixed); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emitter.instruction("ldr x0, [x29, #-16]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } else { + let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); + emit_aarch64_cast_eval_arg( + module, + emitter, + data, + &slot.param_ty, + &arg_label, + fail_label, + ); + abi::emit_push_result_value(emitter, &slot.param_ty); + } } ref_slots } -/// Prepares x86_64 stack cells for eval-supplied by-reference Mixed arguments. -fn emit_x86_64_mixed_ref_arg_cells( +/// Prepares x86_64 stack cells for eval-supplied by-reference arguments. +fn emit_x86_64_ref_arg_cells( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, + param_types: &[PhpType], ref_params: &[bool], -) -> Vec { - let ref_slots = eval_mixed_ref_arg_slots(ref_params); + label_prefix: &str, + fail_label: &str, +) -> Vec { + let ref_slots = eval_ref_arg_slots(param_types, ref_params); for slot in &ref_slots { emit_x86_64_load_eval_arg(module, emitter, slot.param_index); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the original eval Mixed cell for by-reference writeback abi::emit_push_result_value(emitter, &PhpType::Mixed); - emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // seed the mutable by-reference Mixed slot with the original cell - abi::emit_push_result_value(emitter, &PhpType::Mixed); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // seed the mutable by-reference Mixed slot with the original cell + abi::emit_push_result_value(emitter, &PhpType::Mixed); + } else { + let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); + emit_x86_64_cast_eval_arg( + module, + emitter, + data, + &slot.param_ty, + &arg_label, + fail_label, + ); + abi::emit_push_result_value(emitter, &slot.param_ty); + } } ref_slots } @@ -1371,14 +1432,14 @@ fn emit_x86_64_mixed_ref_arg_cells( /// Preserves an ARM64 boxed return value while by-reference eval args are written back. fn preserve_result_and_write_back_aarch64_ref_args( emitter: &mut Emitter, - ref_slots: &[EvalMixedRefArgSlot], + ref_slots: &[EvalRefArgSlot], label_prefix: &str, ) { if ref_slots.is_empty() { return; } abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - emit_aarch64_write_back_mixed_ref_args(emitter, ref_slots, 16, label_prefix); + emit_aarch64_write_back_ref_args(emitter, ref_slots, 16, label_prefix); abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); } @@ -1386,14 +1447,14 @@ fn preserve_result_and_write_back_aarch64_ref_args( /// Preserves an x86_64 boxed return value while by-reference eval args are written back. fn preserve_result_and_write_back_x86_64_ref_args( emitter: &mut Emitter, - ref_slots: &[EvalMixedRefArgSlot], + ref_slots: &[EvalRefArgSlot], label_prefix: &str, ) { if ref_slots.is_empty() { return; } abi::emit_push_reg(emitter, abi::int_result_reg(emitter)); - emit_x86_64_write_back_mixed_ref_args(emitter, ref_slots, 16, label_prefix); + emit_x86_64_write_back_ref_args(emitter, ref_slots, 16, label_prefix); abi::emit_pop_reg(emitter, abi::int_result_reg(emitter)); abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); } diff --git a/src/codegen/eval_ref_arg_helpers.rs b/src/codegen/eval_ref_arg_helpers.rs index 954814ef8b..94ab872510 100644 --- a/src/codegen/eval_ref_arg_helpers.rs +++ b/src/codegen/eval_ref_arg_helpers.rs @@ -8,35 +8,39 @@ //! - `crate::codegen::lower_inst::builtins::eval` //! //! Key details: -//! - The generated eval bridge can invoke by-reference AOT parameters only when -//! the parameter storage is already a boxed `Mixed` cell. -//! - Typed by-reference bridge dispatch stays disabled until raw typed temp -//! writeback is implemented for each supported ABI shape. +//! - The generated eval bridge writes back through the original eval `Mixed` +//! cells after native AOT methods mutate by-reference argument storage. +//! - Boxed `Mixed`/union references use a pointer slot; supported typed scalar +//! references use raw ABI storage that is boxed again during writeback. -use crate::codegen::abi; use crate::codegen::emit::Emitter; +use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::types::{FunctionSig, PhpType}; -/// Describes the stack storage for one eval-supplied by-reference `Mixed` argument. +/// Describes the stack storage for one eval-supplied by-reference argument. #[derive(Clone)] -pub(crate) struct EvalMixedRefArgSlot { +pub(crate) struct EvalRefArgSlot { pub(crate) param_index: usize, + pub(crate) param_ty: PhpType, pub(crate) raw_offset: usize, pub(crate) original_offset: usize, } -const EVAL_MIXED_REF_ARG_BYTES: usize = 32; +const EVAL_REF_ARG_BYTES: usize = 32; -/// Returns true when an eval bridge by-reference parameter can use Mixed-cell storage. -pub(crate) fn eval_mixed_ref_param_supported(ty: &PhpType) -> bool { - matches!(ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) +/// Returns true when an eval bridge by-reference parameter can be staged safely. +pub(crate) fn eval_ref_param_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Mixed | PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::TaggedScalar + ) } /// Returns true when every by-reference parameter in a native signature is bridgeable. -pub(crate) fn eval_signature_mixed_ref_params_supported(signature: &FunctionSig) -> bool { +pub(crate) fn eval_signature_ref_params_supported(signature: &FunctionSig) -> bool { signature.params.iter().enumerate().all(|(index, (_, ty))| { !signature.ref_params.get(index).copied().unwrap_or(false) - || eval_mixed_ref_param_supported(ty) + || eval_ref_param_supported(ty) }) } @@ -65,8 +69,11 @@ pub(crate) fn eval_abi_param_types_for_refs( .collect() } -/// Plans the stack offsets for eval `Mixed` by-reference argument cells. -pub(crate) fn eval_mixed_ref_arg_slots(ref_params: &[bool]) -> Vec { +/// Plans the stack offsets for eval by-reference argument cells. +pub(crate) fn eval_ref_arg_slots( + param_types: &[PhpType], + ref_params: &[bool], +) -> Vec { let total = ref_params.iter().filter(|is_ref| **is_ref).count(); let mut seen = 0usize; let mut slots = Vec::with_capacity(total); @@ -75,9 +82,10 @@ pub(crate) fn eval_mixed_ref_arg_slots(ref_params: &[bool]) -> Vec usize { } } -/// Writes changed ARM64 `Mixed` ref-argument cells back into the original eval cells. -pub(crate) fn emit_aarch64_write_back_mixed_ref_args( +/// Writes changed ARM64 ref-argument cells back into the original eval cells. +pub(crate) fn emit_aarch64_write_back_ref_args( emitter: &mut Emitter, - ref_slots: &[EvalMixedRefArgSlot], + ref_slots: &[EvalRefArgSlot], stack_offset: usize, label_prefix: &str, ) { for slot in ref_slots { - let done_label = format!("{}_ref_{}_done", label_prefix, slot.param_index); - abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); - abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); - emitter.instruction("cmp x9, x10"); // skip writeback when the native call kept the same Mixed cell - emitter.instruction(&format!("b.eq {}", done_label)); // avoid self-copying and releasing the original cell payload - emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); - emitter.label(&done_label); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emit_aarch64_write_back_mixed_ref_arg(emitter, slot, stack_offset, label_prefix); + } else { + emit_aarch64_write_back_typed_ref_arg(emitter, slot, stack_offset, label_prefix); + } } } -/// Writes changed x86_64 `Mixed` ref-argument cells back into the original eval cells. -pub(crate) fn emit_x86_64_write_back_mixed_ref_args( +/// Writes changed x86_64 ref-argument cells back into the original eval cells. +pub(crate) fn emit_x86_64_write_back_ref_args( emitter: &mut Emitter, - ref_slots: &[EvalMixedRefArgSlot], + ref_slots: &[EvalRefArgSlot], stack_offset: usize, label_prefix: &str, ) { for slot in ref_slots { - let done_label = format!("{}_ref_{}_done_x", label_prefix, slot.param_index); - abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); - abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); - emitter.instruction("cmp r10, r11"); // skip writeback when the native call kept the same Mixed cell - emitter.instruction(&format!("je {}", done_label)); // avoid self-copying and releasing the original cell payload - emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); - emitter.label(&done_label); + if matches!(slot.param_ty.codegen_repr(), PhpType::Mixed) { + emit_x86_64_write_back_mixed_ref_arg(emitter, slot, stack_offset, label_prefix); + } else { + emit_x86_64_write_back_typed_ref_arg(emitter, slot, stack_offset, label_prefix); + } + } +} + +/// Writes one ARM64 boxed `Mixed` ref slot back when native code replaced the cell pointer. +fn emit_aarch64_write_back_mixed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let done_label = format!("{}_ref_{}_done", label_prefix, slot.param_index); + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("cmp x9, x10"); // skip writeback when the native call kept the same Mixed cell + emitter.instruction(&format!("b.eq {}", done_label)); // avoid self-copying and releasing the original cell payload + emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); + emitter.label(&done_label); +} + +/// Writes one x86_64 boxed `Mixed` ref slot back when native code replaced the cell pointer. +fn emit_x86_64_write_back_mixed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let done_label = format!("{}_ref_{}_done_x", label_prefix, slot.param_index); + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("cmp r10, r11"); // skip writeback when the native call kept the same Mixed cell + emitter.instruction(&format!("je {}", done_label)); // avoid self-copying and releasing the original cell payload + emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); + emitter.label(&done_label); +} + +/// Boxes one ARM64 typed scalar ref slot and replaces the original eval Mixed cell. +fn emit_aarch64_write_back_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + emit_aarch64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_box_current_value_as_mixed(emitter, &slot.param_ty); + emitter.instruction("mov x10, x0"); // keep the newly boxed ref value available for cell replacement + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); +} + +/// Boxes one x86_64 typed scalar ref slot and replaces the original eval Mixed cell. +fn emit_x86_64_write_back_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + emit_x86_64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_box_current_value_as_mixed(emitter, &slot.param_ty); + emitter.instruction("mov r11, rax"); // keep the newly boxed ref value available for cell replacement + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); +} + +/// Loads one ARM64 typed scalar ref slot into the canonical result registers. +fn emit_aarch64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { + match ty.codegen_repr() { + PhpType::Float => { + abi::emit_load_temporary_stack_slot(emitter, "d0", offset); + } + PhpType::TaggedScalar => { + abi::emit_load_temporary_stack_slot(emitter, "x0", offset); + abi::emit_load_temporary_stack_slot(emitter, "x1", offset + 8); + } + PhpType::Int | PhpType::Bool => { + abi::emit_load_temporary_stack_slot(emitter, "x0", offset); + } + _ => {} + } +} + +/// Loads one x86_64 typed scalar ref slot into the canonical result registers. +fn emit_x86_64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { + match ty.codegen_repr() { + PhpType::Float => { + abi::emit_load_temporary_stack_slot(emitter, "xmm0", offset); + } + PhpType::TaggedScalar => { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + abi::emit_load_temporary_stack_slot(emitter, "rdx", offset + 8); + } + PhpType::Int | PhpType::Bool => { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + } + _ => {} } } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 36c0bf77c7..1aa4e1a59d 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -14,7 +14,7 @@ use std::path::Path; -use crate::codegen::eval_ref_arg_helpers::eval_signature_mixed_ref_params_supported; +use crate::codegen::eval_ref_arg_helpers::eval_signature_ref_params_supported; use crate::codegen::platform::Arch; use crate::codegen::{ abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, @@ -1014,7 +1014,7 @@ fn function_can_register_with_eval(function: &Function) -> bool { /// Returns true when eval can dispatch a native method through the generated bridge. fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS - && eval_signature_mixed_ref_params_supported(signature) + && eval_signature_ref_params_supported(signature) && signature .params .iter() @@ -1025,7 +1025,7 @@ fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { /// Returns true when eval can dispatch a native constructor through the generated bridge. fn constructor_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS - && eval_signature_mixed_ref_params_supported(signature) + && eval_signature_ref_params_supported(signature) && signature .params .iter() diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cf9d123fb2..c5a2ba308f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5735,6 +5735,48 @@ return $value;'); assert_eq!(out, "15"); } +/// Verifies eval dispatches generated/AOT instance methods with typed scalar by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_typed_by_ref_args() { + let out = compile_and_run( + r#"mutate($value, $flag); +return $value . ":" . ($flag ? "T" : "F");'); +"#, + ); + assert_eq!(out, "15:F"); +} + +/// Verifies eval writes nullable-int by-reference AOT method results back as boxed eval values. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_int_by_ref_arg() { + let out = compile_and_run( + r#"clear($value); +return $value === null ? "N" : "bad";'); +"#, + ); + assert_eq!(out, "N"); +} + /// Verifies eval preserves string values passed through an untyped AOT method parameter. #[test] fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_string_arg() { @@ -5809,6 +5851,25 @@ return $value;'); assert_eq!(out, "27"); } +/// Verifies eval dispatches generated/AOT static methods with float by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_float_by_ref_arg() { + let out = compile_and_run( + r#" Date: Thu, 25 Jun 2026 17:25:53 +0200 Subject: [PATCH 0643/1208] Support eval AOT string ref bridges --- docs/php/eval.md | 10 ++--- src/codegen/eval_ref_arg_helpers.rs | 35 ++++++++++++++++- tests/codegen/eval.rs | 58 +++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 7 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 19c4cda147..3660256618 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped and scalar/nullable-int by-reference constructor parameters. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, and nullable-int by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped and scalar/nullable-int by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, and nullable-int by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -131,7 +131,7 @@ fallback supports the same named-argument and positional variadic-tail binding for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameter signatures, including registered by-reference lvalue validation, when the current eval class scope satisfies PHP visibility. Generated AOT bridge -dispatch can invoke `mixed`/untyped and scalar/nullable-int by-reference method +dispatch can invoke `mixed`/untyped, scalar including string, and nullable-int by-reference method and constructor parameters. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -705,7 +705,7 @@ still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch is limited to visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object -parameter signatures plus `mixed`/untyped and scalar/nullable-int by-reference +parameter signatures plus `mixed`/untyped, scalar including string, and nullable-int by-reference method and constructor parameters, including the generated positional variadic array slot when present. Broader parameter/return ABI shapes are still outside those bridge paths. @@ -727,7 +727,7 @@ scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/null `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while -remaining typed by-reference bridge shapes such as string, array, iterable, +remaining typed by-reference bridge shapes such as array, iterable, and object references, plus other unsupported bridge shapes, remain metadata-only rather than invocable through eval. diff --git a/src/codegen/eval_ref_arg_helpers.rs b/src/codegen/eval_ref_arg_helpers.rs index 94ab872510..8d642bc564 100644 --- a/src/codegen/eval_ref_arg_helpers.rs +++ b/src/codegen/eval_ref_arg_helpers.rs @@ -11,7 +11,7 @@ //! - The generated eval bridge writes back through the original eval `Mixed` //! cells after native AOT methods mutate by-reference argument storage. //! - Boxed `Mixed`/union references use a pointer slot; supported typed scalar -//! references use raw ABI storage that is boxed again during writeback. +//! and string references use raw ABI storage that is boxed again during writeback. use crate::codegen::emit::Emitter; use crate::codegen::{abi, emit_box_current_value_as_mixed}; @@ -32,7 +32,12 @@ const EVAL_REF_ARG_BYTES: usize = 32; pub(crate) fn eval_ref_param_supported(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), - PhpType::Mixed | PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::TaggedScalar + PhpType::Mixed + | PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::TaggedScalar ) } @@ -179,6 +184,7 @@ fn emit_aarch64_write_back_typed_ref_arg( emitter.instruction("mov x10, x0"); // keep the newly boxed ref value available for cell replacement abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); + emit_aarch64_release_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); } /// Boxes one x86_64 typed scalar ref slot and replaces the original eval Mixed cell. @@ -193,11 +199,16 @@ fn emit_x86_64_write_back_typed_ref_arg( emitter.instruction("mov r11, rax"); // keep the newly boxed ref value available for cell replacement abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); + emit_x86_64_release_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); } /// Loads one ARM64 typed scalar ref slot into the canonical result registers. fn emit_aarch64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { match ty.codegen_repr() { + PhpType::Str => { + abi::emit_load_temporary_stack_slot(emitter, "x1", offset); + abi::emit_load_temporary_stack_slot(emitter, "x2", offset + 8); + } PhpType::Float => { abi::emit_load_temporary_stack_slot(emitter, "d0", offset); } @@ -215,6 +226,10 @@ fn emit_aarch64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: /// Loads one x86_64 typed scalar ref slot into the canonical result registers. fn emit_x86_64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { match ty.codegen_repr() { + PhpType::Str => { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + abi::emit_load_temporary_stack_slot(emitter, "rdx", offset + 8); + } PhpType::Float => { abi::emit_load_temporary_stack_slot(emitter, "xmm0", offset); } @@ -229,6 +244,22 @@ fn emit_x86_64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: } } +/// Releases any owned ARM64 payload left in one typed raw ref slot after writeback. +fn emit_aarch64_release_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { + if matches!(ty.codegen_repr(), PhpType::Str) { + abi::emit_load_temporary_stack_slot(emitter, "x0", offset); + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + } +} + +/// Releases any owned x86_64 payload left in one typed raw ref slot after writeback. +fn emit_x86_64_release_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { + if matches!(ty.codegen_repr(), PhpType::Str) { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + } +} + /// Copies one replacement ARM64 Mixed cell payload into an existing target cell. fn emit_aarch64_replace_mixed_cell( emitter: &mut Emitter, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c5a2ba308f..15591c60ff 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5777,6 +5777,26 @@ return $value === null ? "N" : "bad";'); assert_eq!(out, "N"); } +/// Verifies eval dispatches generated/AOT instance methods with string by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_string_by_ref_arg() { + let out = compile_and_run( + r#"mutate($value); +return $value;'); +"#, + ); + assert_eq!(out, "eval-method"); +} + /// Verifies eval preserves string values passed through an untyped AOT method parameter. #[test] fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_string_arg() { @@ -5870,6 +5890,25 @@ return $value;'); assert_eq!(out, "2.75"); } +/// Verifies eval dispatches generated/AOT static methods with string by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_string_by_ref_arg() { + let out = compile_and_run( + r#" Date: Thu, 25 Jun 2026 17:33:33 +0200 Subject: [PATCH 0644/1208] Support eval AOT array ref bridges --- docs/php/eval.md | 12 +-- src/codegen/eval_constructor_helpers.rs | 4 +- src/codegen/eval_method_helpers.rs | 4 +- src/codegen/eval_ref_arg_helpers.rs | 108 +++++++++++++++++++++--- tests/codegen/eval.rs | 64 ++++++++++++++ 5 files changed, 170 insertions(+), 22 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 3660256618..7cf16f7e59 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, and nullable-int by-reference constructor parameters. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, and array by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, and nullable-int by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, and array by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -131,7 +131,7 @@ fallback supports the same named-argument and positional variadic-tail binding for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameter signatures, including registered by-reference lvalue validation, when the current eval class scope satisfies PHP visibility. Generated AOT bridge -dispatch can invoke `mixed`/untyped, scalar including string, and nullable-int by-reference method +dispatch can invoke `mixed`/untyped, scalar including string, nullable-int, and array by-reference method and constructor parameters. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -705,7 +705,7 @@ still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch is limited to visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object -parameter signatures plus `mixed`/untyped, scalar including string, and nullable-int by-reference +parameter signatures plus `mixed`/untyped, scalar including string, nullable-int, and array by-reference method and constructor parameters, including the generated positional variadic array slot when present. Broader parameter/return ABI shapes are still outside those bridge paths. @@ -727,8 +727,8 @@ scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/null `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while -remaining typed by-reference bridge shapes such as array, iterable, -and object references, plus other unsupported bridge shapes, remain metadata-only +remaining typed by-reference bridge shapes such as iterable and object +references, plus other unsupported bridge shapes, remain metadata-only rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 2fca80b43f..afa4ab3978 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -809,7 +809,7 @@ fn emit_aarch64_constructor_ref_arg_cells( label_prefix: &str, fail_label: &str, ) -> Vec { - let ref_slots = eval_ref_arg_slots(param_types, ref_params); + let ref_slots = eval_ref_arg_slots(param_types, ref_params, true); for slot in &ref_slots { emit_aarch64_load_eval_arg(module, emitter, slot.param_index); emitter.instruction("ldr x0, [x29, #-16]"); // reload the original eval Mixed cell for by-reference writeback @@ -835,7 +835,7 @@ fn emit_x86_64_constructor_ref_arg_cells( label_prefix: &str, fail_label: &str, ) -> Vec { - let ref_slots = eval_ref_arg_slots(param_types, ref_params); + let ref_slots = eval_ref_arg_slots(param_types, ref_params, true); for slot in &ref_slots { emit_x86_64_load_eval_arg(module, emitter, slot.param_index); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the original eval Mixed cell for by-reference writeback diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index ac81f2bad7..f907719821 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -1371,7 +1371,7 @@ fn emit_aarch64_ref_arg_cells( label_prefix: &str, fail_label: &str, ) -> Vec { - let ref_slots = eval_ref_arg_slots(param_types, ref_params); + let ref_slots = eval_ref_arg_slots(param_types, ref_params, false); for slot in &ref_slots { emit_aarch64_load_eval_arg(module, emitter, slot.param_index, arg_array_frame_offset); emitter.instruction("ldr x0, [x29, #-16]"); // reload the original eval Mixed cell for by-reference writeback @@ -1405,7 +1405,7 @@ fn emit_x86_64_ref_arg_cells( label_prefix: &str, fail_label: &str, ) -> Vec { - let ref_slots = eval_ref_arg_slots(param_types, ref_params); + let ref_slots = eval_ref_arg_slots(param_types, ref_params, false); for slot in &ref_slots { emit_x86_64_load_eval_arg(module, emitter, slot.param_index); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the original eval Mixed cell for by-reference writeback diff --git a/src/codegen/eval_ref_arg_helpers.rs b/src/codegen/eval_ref_arg_helpers.rs index 8d642bc564..b7856128d9 100644 --- a/src/codegen/eval_ref_arg_helpers.rs +++ b/src/codegen/eval_ref_arg_helpers.rs @@ -10,8 +10,9 @@ //! Key details: //! - The generated eval bridge writes back through the original eval `Mixed` //! cells after native AOT methods mutate by-reference argument storage. -//! - Boxed `Mixed`/union references use a pointer slot; supported typed scalar -//! and string references use raw ABI storage that is boxed again during writeback. +//! - Boxed `Mixed`/union references use a pointer slot; supported typed scalar, +//! string, and array references use raw ABI storage that is boxed again during +//! writeback. use crate::codegen::emit::Emitter; use crate::codegen::{abi, emit_box_current_value_as_mixed}; @@ -24,6 +25,7 @@ pub(crate) struct EvalRefArgSlot { pub(crate) param_ty: PhpType, pub(crate) raw_offset: usize, pub(crate) original_offset: usize, + pub(crate) raw_refcounted_owned: bool, } const EVAL_REF_ARG_BYTES: usize = 32; @@ -37,6 +39,8 @@ pub(crate) fn eval_ref_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Array(_) + | PhpType::AssocArray { .. } | PhpType::TaggedScalar ) } @@ -78,6 +82,7 @@ pub(crate) fn eval_abi_param_types_for_refs( pub(crate) fn eval_ref_arg_slots( param_types: &[PhpType], ref_params: &[bool], + raw_refcounted_owned: bool, ) -> Vec { let total = ref_params.iter().filter(|is_ref| **is_ref).count(); let mut seen = 0usize; @@ -93,6 +98,7 @@ pub(crate) fn eval_ref_arg_slots( param_ty: param_types[param_index].codegen_repr(), raw_offset: base_offset, original_offset: base_offset + 16, + raw_refcounted_owned, }); seen += 1; } @@ -184,7 +190,7 @@ fn emit_aarch64_write_back_typed_ref_arg( emitter.instruction("mov x10, x0"); // keep the newly boxed ref value available for cell replacement abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); - emit_aarch64_release_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_aarch64_release_typed_ref_slot(emitter, slot, stack_offset, label_prefix); } /// Boxes one x86_64 typed scalar ref slot and replaces the original eval Mixed cell. @@ -199,7 +205,7 @@ fn emit_x86_64_write_back_typed_ref_arg( emitter.instruction("mov r11, rax"); // keep the newly boxed ref value available for cell replacement abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); - emit_x86_64_release_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_x86_64_release_typed_ref_slot(emitter, slot, stack_offset, label_prefix); } /// Loads one ARM64 typed scalar ref slot into the canonical result registers. @@ -219,6 +225,9 @@ fn emit_aarch64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: PhpType::Int | PhpType::Bool => { abi::emit_load_temporary_stack_slot(emitter, "x0", offset); } + PhpType::Array(_) | PhpType::AssocArray { .. } => { + abi::emit_load_temporary_stack_slot(emitter, "x0", offset); + } _ => {} } } @@ -240,24 +249,99 @@ fn emit_x86_64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: PhpType::Int | PhpType::Bool => { abi::emit_load_temporary_stack_slot(emitter, "rax", offset); } + PhpType::Array(_) | PhpType::AssocArray { .. } => { + abi::emit_load_temporary_stack_slot(emitter, "rax", offset); + } _ => {} } } /// Releases any owned ARM64 payload left in one typed raw ref slot after writeback. -fn emit_aarch64_release_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - if matches!(ty.codegen_repr(), PhpType::Str) { - abi::emit_load_temporary_stack_slot(emitter, "x0", offset); - abi::emit_call_label(emitter, "__rt_heap_free_safe"); +fn emit_aarch64_release_typed_ref_slot( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let raw_offset = stack_offset + slot.raw_offset; + match slot.param_ty.codegen_repr() { + PhpType::Str => { + abi::emit_load_temporary_stack_slot(emitter, "x0", raw_offset); + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + } + ty @ (PhpType::Array(_) | PhpType::AssocArray { .. }) => { + emit_aarch64_release_refcounted_raw_slot(emitter, slot, stack_offset, label_prefix, &ty); + } + _ => {} } } /// Releases any owned x86_64 payload left in one typed raw ref slot after writeback. -fn emit_x86_64_release_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { - if matches!(ty.codegen_repr(), PhpType::Str) { - abi::emit_load_temporary_stack_slot(emitter, "rax", offset); - abi::emit_call_label(emitter, "__rt_heap_free_safe"); +fn emit_x86_64_release_typed_ref_slot( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let raw_offset = stack_offset + slot.raw_offset; + match slot.param_ty.codegen_repr() { + PhpType::Str => { + abi::emit_load_temporary_stack_slot(emitter, "rax", raw_offset); + abi::emit_call_label(emitter, "__rt_heap_free_safe"); + } + ty @ (PhpType::Array(_) | PhpType::AssocArray { .. }) => { + emit_x86_64_release_refcounted_raw_slot(emitter, slot, stack_offset, label_prefix, &ty); + } + _ => {} + } +} + +/// Releases an ARM64 raw refcounted slot when it owns a value not retained by the eval cell. +fn emit_aarch64_release_refcounted_raw_slot( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, + ty: &PhpType, +) { + let release_label = format!("{}_ref_{}_release_raw", label_prefix, slot.param_index); + let done_label = format!("{}_ref_{}_release_raw_done", label_prefix, slot.param_index); + if !slot.raw_refcounted_owned { + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("ldr x11, [x9, #8]"); // load the original eval cell payload for borrowed-slot comparison + emitter.instruction("cmp x10, x11"); // changed raw pointers are owned by the native assignment path + emitter.instruction(&format!("b.ne {}", release_label)); // release only raw heap values introduced by the native call + emitter.instruction(&format!("b {}", done_label)); // keep borrowed original payloads owned by the eval cell } + emitter.label(&release_label); + abi::emit_load_temporary_stack_slot(emitter, "x0", stack_offset + slot.raw_offset); + abi::emit_decref_if_refcounted(emitter, ty); + emitter.label(&done_label); +} + +/// Releases an x86_64 raw refcounted slot when it owns a value not retained by the eval cell. +fn emit_x86_64_release_refcounted_raw_slot( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, + ty: &PhpType, +) { + let release_label = format!("{}_ref_{}_release_raw_x", label_prefix, slot.param_index); + let done_label = format!("{}_ref_{}_release_raw_done_x", label_prefix, slot.param_index); + if !slot.raw_refcounted_owned { + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("mov r9, QWORD PTR [r10 + 8]"); // load the original eval cell payload for borrowed-slot comparison + emitter.instruction("cmp r11, r9"); // changed raw pointers are owned by the native assignment path + emitter.instruction(&format!("jne {}", release_label)); // release only raw heap values introduced by the native call + emitter.instruction(&format!("jmp {}", done_label)); // keep borrowed original payloads owned by the eval cell + } + emitter.label(&release_label); + abi::emit_load_temporary_stack_slot(emitter, "rax", stack_offset + slot.raw_offset); + abi::emit_decref_if_refcounted(emitter, ty); + emitter.label(&done_label); } /// Copies one replacement ARM64 Mixed cell payload into an existing target cell. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 15591c60ff..065b0ce463 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5797,6 +5797,32 @@ return $value;'); assert_eq!(out, "eval-method"); } +/// Verifies eval dispatches generated/AOT instance methods with array by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_array_by_ref_arg() { + let out = compile_and_run( + r#"append($items); +$afterAppend = count($items) . ":" . $items[2]; +$box->replace($items); +return $afterAppend . ":" . count($items) . ":" . $items[0] . ":" . $items[1];'); +"#, + ); + assert_eq!(out, "3:3:2:4:5"); +} + /// Verifies eval preserves string values passed through an untyped AOT method parameter. #[test] fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_string_arg() { @@ -5909,6 +5935,25 @@ return $value;'); assert_eq!(out, "static-eval"); } +/// Verifies eval dispatches generated/AOT static methods with array by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_array_by_ref_arg() { + let out = compile_and_run( + r#" Date: Thu, 25 Jun 2026 17:41:01 +0200 Subject: [PATCH 0645/1208] Support eval AOT object ref bridges --- docs/php/eval.md | 12 ++--- src/codegen/eval_ref_arg_helpers.rs | 13 ++--- tests/codegen/eval.rs | 78 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 7cf16f7e59..27f4d3505d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, and array by-reference constructor parameters. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, and array by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -131,7 +131,7 @@ fallback supports the same named-argument and positional variadic-tail binding for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameter signatures, including registered by-reference lvalue validation, when the current eval class scope satisfies PHP visibility. Generated AOT bridge -dispatch can invoke `mixed`/untyped, scalar including string, nullable-int, and array by-reference method +dispatch can invoke `mixed`/untyped, scalar including string, nullable-int, array, and object by-reference method and constructor parameters. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -705,7 +705,7 @@ still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch is limited to visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object -parameter signatures plus `mixed`/untyped, scalar including string, nullable-int, and array by-reference +parameter signatures plus `mixed`/untyped, scalar including string, nullable-int, array, and object by-reference method and constructor parameters, including the generated positional variadic array slot when present. Broader parameter/return ABI shapes are still outside those bridge paths. @@ -727,8 +727,8 @@ scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/null `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while -remaining typed by-reference bridge shapes such as iterable and object -references, plus other unsupported bridge shapes, remain metadata-only +remaining typed by-reference bridge shapes such as iterable references, plus +other unsupported bridge shapes, remain metadata-only rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after diff --git a/src/codegen/eval_ref_arg_helpers.rs b/src/codegen/eval_ref_arg_helpers.rs index b7856128d9..c65ccf07c7 100644 --- a/src/codegen/eval_ref_arg_helpers.rs +++ b/src/codegen/eval_ref_arg_helpers.rs @@ -11,8 +11,8 @@ //! - The generated eval bridge writes back through the original eval `Mixed` //! cells after native AOT methods mutate by-reference argument storage. //! - Boxed `Mixed`/union references use a pointer slot; supported typed scalar, -//! string, and array references use raw ABI storage that is boxed again during -//! writeback. +//! string, array, and object references use raw ABI storage that is boxed +//! again during writeback. use crate::codegen::emit::Emitter; use crate::codegen::{abi, emit_box_current_value_as_mixed}; @@ -41,6 +41,7 @@ pub(crate) fn eval_ref_param_supported(ty: &PhpType) -> bool { | PhpType::Str | PhpType::Array(_) | PhpType::AssocArray { .. } + | PhpType::Object(_) | PhpType::TaggedScalar ) } @@ -225,7 +226,7 @@ fn emit_aarch64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: PhpType::Int | PhpType::Bool => { abi::emit_load_temporary_stack_slot(emitter, "x0", offset); } - PhpType::Array(_) | PhpType::AssocArray { .. } => { + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { abi::emit_load_temporary_stack_slot(emitter, "x0", offset); } _ => {} @@ -249,7 +250,7 @@ fn emit_x86_64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: PhpType::Int | PhpType::Bool => { abi::emit_load_temporary_stack_slot(emitter, "rax", offset); } - PhpType::Array(_) | PhpType::AssocArray { .. } => { + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { abi::emit_load_temporary_stack_slot(emitter, "rax", offset); } _ => {} @@ -269,7 +270,7 @@ fn emit_aarch64_release_typed_ref_slot( abi::emit_load_temporary_stack_slot(emitter, "x0", raw_offset); abi::emit_call_label(emitter, "__rt_heap_free_safe"); } - ty @ (PhpType::Array(_) | PhpType::AssocArray { .. }) => { + ty @ (PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_)) => { emit_aarch64_release_refcounted_raw_slot(emitter, slot, stack_offset, label_prefix, &ty); } _ => {} @@ -289,7 +290,7 @@ fn emit_x86_64_release_typed_ref_slot( abi::emit_load_temporary_stack_slot(emitter, "rax", raw_offset); abi::emit_call_label(emitter, "__rt_heap_free_safe"); } - ty @ (PhpType::Array(_) | PhpType::AssocArray { .. }) => { + ty @ (PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_)) => { emit_x86_64_release_refcounted_raw_slot(emitter, slot, stack_offset, label_prefix, &ty); } _ => {} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 065b0ce463..b8dcc16b6a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5823,6 +5823,37 @@ return $afterAppend . ":" . count($items) . ":" . $items[0] . ":" . $items[1];') assert_eq!(out, "3:3:2:4:5"); } +/// Verifies eval dispatches generated/AOT instance methods with object by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_object_by_ref_arg() { + let out = compile_and_run( + r#"value = 7; + } + + public function replace(EvalAotObjectRefMethodPayload &$payload): void { + $payload = new EvalAotObjectRefMethodPayload(); + $payload->value = 9; + } +} + +echo eval('$box = new EvalAotObjectRefMethodBox(); +$payload = new EvalAotObjectRefMethodPayload(); +$box->mutate($payload); +$afterMutate = $payload->value; +$box->replace($payload); +return $afterMutate . ":" . $payload->value;'); +"#, + ); + assert_eq!(out, "7:9"); +} + /// Verifies eval preserves string values passed through an untyped AOT method parameter. #[test] fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_string_arg() { @@ -5954,6 +5985,29 @@ return count($items) . ":" . $items[2];'); assert_eq!(out, "3:8"); } +/// Verifies eval dispatches generated/AOT static methods with object by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_object_by_ref_arg() { + let out = compile_and_run( + r#"value = 8; + } +} + +echo eval('$payload = new EvalAotObjectRefStaticPayload(); +EvalAotObjectRefStaticBox::mutate($payload); +return $payload->value;'); +"#, + ); + assert_eq!(out, "8"); +} + /// Verifies eval binds named arguments before dispatching an AOT constructor. #[test] fn test_eval_dynamic_new_runs_constructor_with_named_args() { @@ -12826,6 +12880,30 @@ return count($items) . ":" . $items[0] . ":" . $items[1];'); assert_eq!(out, "2:9:10"); } +/// Verifies eval dispatches generated/AOT constructors with object by-reference params. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_object_by_ref_arg() { + let out = compile_and_run( + r#"value = 11; + } +} + +echo eval('$payload = new EvalDynamicNewObjectRefCtorPayload(); +$box = new EvalDynamicNewObjectRefCtor($payload); +return $payload->value;'); +"#, + ); + assert_eq!(out, "11"); +} + /// Verifies eval object construction can call private AOT constructors from the declaring scope. #[test] fn test_eval_dynamic_new_runs_private_constructor_from_declaring_scope() { From 1569d0a27a0ffd60fcb001b794e6536a6ecaf700 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 17:47:27 +0200 Subject: [PATCH 0646/1208] Support eval AOT iterable ref bridges --- docs/php/eval.md | 11 +++--- src/codegen/eval_ref_arg_helpers.rs | 33 +++++++++++----- tests/codegen/eval.rs | 58 +++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 27f4d3505d..07af4cdc45 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, and object by-reference constructor parameters. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -131,7 +131,7 @@ fallback supports the same named-argument and positional variadic-tail binding for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameter signatures, including registered by-reference lvalue validation, when the current eval class scope satisfies PHP visibility. Generated AOT bridge -dispatch can invoke `mixed`/untyped, scalar including string, nullable-int, array, and object by-reference method +dispatch can invoke `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference method and constructor parameters. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -705,7 +705,7 @@ still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch is limited to visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object -parameter signatures plus `mixed`/untyped, scalar including string, nullable-int, array, and object by-reference +parameter signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference method and constructor parameters, including the generated positional variadic array slot when present. Broader parameter/return ABI shapes are still outside those bridge paths. @@ -727,8 +727,7 @@ scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/null `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while -remaining typed by-reference bridge shapes such as iterable references, plus -other unsupported bridge shapes, remain metadata-only +other unsupported bridge shapes remain metadata-only rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after diff --git a/src/codegen/eval_ref_arg_helpers.rs b/src/codegen/eval_ref_arg_helpers.rs index c65ccf07c7..7feab0aed3 100644 --- a/src/codegen/eval_ref_arg_helpers.rs +++ b/src/codegen/eval_ref_arg_helpers.rs @@ -11,8 +11,8 @@ //! - The generated eval bridge writes back through the original eval `Mixed` //! cells after native AOT methods mutate by-reference argument storage. //! - Boxed `Mixed`/union references use a pointer slot; supported typed scalar, -//! string, array, and object references use raw ABI storage that is boxed -//! again during writeback. +//! string, array, iterable, and object references use raw ABI storage that is +//! boxed again during writeback. use crate::codegen::emit::Emitter; use crate::codegen::{abi, emit_box_current_value_as_mixed}; @@ -41,6 +41,7 @@ pub(crate) fn eval_ref_param_supported(ty: &PhpType) -> bool { | PhpType::Str | PhpType::Array(_) | PhpType::AssocArray { .. } + | PhpType::Iterable | PhpType::Object(_) | PhpType::TaggedScalar ) @@ -179,7 +180,7 @@ fn emit_x86_64_write_back_mixed_ref_arg( emitter.label(&done_label); } -/// Boxes one ARM64 typed scalar ref slot and replaces the original eval Mixed cell. +/// Boxes one ARM64 typed raw ref slot and replaces the original eval Mixed cell. fn emit_aarch64_write_back_typed_ref_arg( emitter: &mut Emitter, slot: &EvalRefArgSlot, @@ -194,7 +195,7 @@ fn emit_aarch64_write_back_typed_ref_arg( emit_aarch64_release_typed_ref_slot(emitter, slot, stack_offset, label_prefix); } -/// Boxes one x86_64 typed scalar ref slot and replaces the original eval Mixed cell. +/// Boxes one x86_64 typed raw ref slot and replaces the original eval Mixed cell. fn emit_x86_64_write_back_typed_ref_arg( emitter: &mut Emitter, slot: &EvalRefArgSlot, @@ -209,7 +210,7 @@ fn emit_x86_64_write_back_typed_ref_arg( emit_x86_64_release_typed_ref_slot(emitter, slot, stack_offset, label_prefix); } -/// Loads one ARM64 typed scalar ref slot into the canonical result registers. +/// Loads one ARM64 typed raw ref slot into the canonical result registers. fn emit_aarch64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { match ty.codegen_repr() { PhpType::Str => { @@ -226,14 +227,17 @@ fn emit_aarch64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: PhpType::Int | PhpType::Bool => { abi::emit_load_temporary_stack_slot(emitter, "x0", offset); } - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_) => { abi::emit_load_temporary_stack_slot(emitter, "x0", offset); } _ => {} } } -/// Loads one x86_64 typed scalar ref slot into the canonical result registers. +/// Loads one x86_64 typed raw ref slot into the canonical result registers. fn emit_x86_64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: usize) { match ty.codegen_repr() { PhpType::Str => { @@ -250,7 +254,10 @@ fn emit_x86_64_load_typed_ref_slot(emitter: &mut Emitter, ty: &PhpType, offset: PhpType::Int | PhpType::Bool => { abi::emit_load_temporary_stack_slot(emitter, "rax", offset); } - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_) => { + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_) => { abi::emit_load_temporary_stack_slot(emitter, "rax", offset); } _ => {} @@ -270,7 +277,10 @@ fn emit_aarch64_release_typed_ref_slot( abi::emit_load_temporary_stack_slot(emitter, "x0", raw_offset); abi::emit_call_label(emitter, "__rt_heap_free_safe"); } - ty @ (PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_)) => { + ty @ (PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_)) => { emit_aarch64_release_refcounted_raw_slot(emitter, slot, stack_offset, label_prefix, &ty); } _ => {} @@ -290,7 +300,10 @@ fn emit_x86_64_release_typed_ref_slot( abi::emit_load_temporary_stack_slot(emitter, "rax", raw_offset); abi::emit_call_label(emitter, "__rt_heap_free_safe"); } - ty @ (PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Object(_)) => { + ty @ (PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_)) => { emit_x86_64_release_refcounted_raw_slot(emitter, slot, stack_offset, label_prefix, &ty); } _ => {} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b8dcc16b6a..fe20a8fcc4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5854,6 +5854,26 @@ return $afterMutate . ":" . $payload->value;'); assert_eq!(out, "7:9"); } +/// Verifies eval dispatches generated/AOT instance methods with iterable by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_instance_method_with_iterable_by_ref_arg() { + let out = compile_and_run( + r#"replace($items); +return is_iterable($items) . ":" . count($items) . ":" . $items[0] . ":" . $items[1];'); +"#, + ); + assert_eq!(out, "1:2:4:5"); +} + /// Verifies eval preserves string values passed through an untyped AOT method parameter. #[test] fn test_eval_fragment_dispatches_aot_instance_method_with_mixed_string_arg() { @@ -6008,6 +6028,25 @@ return $payload->value;'); assert_eq!(out, "8"); } +/// Verifies eval dispatches generated/AOT static methods with iterable by-reference params. +#[test] +fn test_eval_fragment_dispatches_aot_static_method_with_iterable_by_ref_arg() { + let out = compile_and_run( + r#" "static"]; + } +} + +echo eval('$items = [6, 7]; +EvalAotIterableRefStaticBox::replace($items); +return is_iterable($items) . ":" . $items["name"];'); +"#, + ); + assert_eq!(out, "1:static"); +} + /// Verifies eval binds named arguments before dispatching an AOT constructor. #[test] fn test_eval_dynamic_new_runs_constructor_with_named_args() { @@ -12904,6 +12943,25 @@ return $payload->value;'); assert_eq!(out, "11"); } +/// Verifies eval dispatches generated/AOT constructors with iterable by-reference params. +#[test] +fn test_eval_dynamic_new_runs_constructor_with_iterable_by_ref_arg() { + let out = compile_and_run( + r#" Date: Thu, 25 Jun 2026 17:58:21 +0200 Subject: [PATCH 0647/1208] Support nested eval AOT object defaults --- .../elephc-magician/src/ffi/native_methods.rs | 19 +++- crates/elephc-magician/src/ffi/tests.rs | 95 +++++++++++++++++++ docs/php/eval.md | 4 +- src/codegen/lower_inst/builtins/eval.rs | 8 +- tests/codegen/eval.rs | 72 ++++++++++++++ 5 files changed, 190 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index d7a946030b..26f31175fb 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -37,6 +37,7 @@ const NATIVE_ATTRIBUTE_ARG_FLOAT: u8 = 5; const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; #[derive(Clone, Copy)] @@ -1818,16 +1819,25 @@ unsafe fn native_callable_object_default( let len = usize::try_from(spec_len).ok()?; let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; let mut offset = 0; - let class_name = native_attribute_take_string(bytes, &mut offset)?; - let arg_count = usize::from(native_attribute_take_u8(bytes, &mut offset)?); + let default = native_callable_object_default_from_bytes(bytes, &mut offset)?; + (offset == bytes.len()).then_some(default) +} + +/// Decodes an object-valued native callable default from a generated binary spec slice. +fn native_callable_object_default_from_bytes( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let class_name = native_attribute_take_string(bytes, offset)?; + let arg_count = usize::from(native_attribute_take_u8(bytes, offset)?); if arg_count > MAX_NATIVE_OBJECT_DEFAULT_ARGS { return None; } let mut args = Vec::with_capacity(arg_count); for _ in 0..arg_count { - args.push(native_callable_object_default_arg(bytes, &mut offset)?); + args.push(native_callable_object_default_arg(bytes, offset)?); } - (offset == bytes.len()).then_some(NativeCallableDefault::Object { class_name, args }) + Some(NativeCallableDefault::Object { class_name, args }) } /// Decodes one object-default constructor argument from a generated binary spec. @@ -1844,6 +1854,7 @@ fn native_callable_object_default_arg( NATIVE_OBJECT_DEFAULT_ARG_STRING => { native_attribute_take_string(bytes, offset).map(NativeCallableDefault::String) } + NATIVE_OBJECT_DEFAULT_ARG_OBJECT => native_callable_object_default_from_bytes(bytes, offset), _ => None, } } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 0f6d689ce3..253abd7c55 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -26,6 +26,15 @@ use crate::eval_ir::{EvalAttributeArg, EvalParameterTypeVariant}; use crate::value::{RuntimeCell, RuntimeCellHandle}; use std::ffi::c_void; +const TEST_NATIVE_DEFAULT_NULL: u64 = 0; +const TEST_NATIVE_DEFAULT_BOOL: u64 = 1; +const TEST_NATIVE_DEFAULT_INT: u64 = 2; +const TEST_NATIVE_DEFAULT_FLOAT: u64 = 3; +const TEST_NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; + /// Test native invoker placeholder used only to validate ABI registration. unsafe extern "C" fn fake_native_invoker( _descriptor: *mut c_void, @@ -99,6 +108,53 @@ fn native_member_attribute_push_string(record: &mut Vec, value: &str) { record.extend_from_slice(value.as_bytes()); } +/// Builds one object-valued native parameter default ABI record for registration tests. +fn native_object_default_record(class_name: &str, args: &[NativeCallableDefault]) -> Vec { + let mut record = Vec::new(); + native_member_attribute_push_string(&mut record, class_name); + record.push(args.len() as u8); + for arg in args { + native_object_default_push_arg(&mut record, arg); + } + record +} + +/// Appends one object-default constructor argument to a native parameter default record. +fn native_object_default_push_arg(record: &mut Vec, arg: &NativeCallableDefault) { + match arg { + NativeCallableDefault::Null => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_NULL, 0) + } + NativeCallableDefault::Bool(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_BOOL, u64::from(*value)) + } + NativeCallableDefault::Int(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_INT, *value as u64) + } + NativeCallableDefault::Float(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_FLOAT, value.to_bits()) + } + NativeCallableDefault::String(value) => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING); + native_member_attribute_push_string(record, value); + } + NativeCallableDefault::EmptyArray => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_EMPTY_ARRAY, 0) + } + NativeCallableDefault::Object { class_name, args } => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT); + record.extend_from_slice(&native_object_default_record(class_name, args)); + } + } +} + +/// Appends one scalar object-default constructor argument to a native parameter default record. +fn native_object_default_push_scalar(record: &mut Vec, kind: u64, payload: u64) { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR); + record.extend_from_slice(&kind.to_le_bytes()); + record.extend_from_slice(&payload.to_le_bytes()); +} + /// Verifies the exported version entry point reports the crate ABI constant. #[test] fn abi_version_matches_constant() { @@ -611,6 +667,45 @@ fn register_native_methods_record_signature_metadata() { ); } +/// Verifies native AOT object defaults can carry nested object constructor args. +#[test] +fn register_native_object_default_decodes_nested_objects() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownClass"; + let nested_args = vec![NativeCallableDefault::Object { + class_name: "InnerDefault".to_string(), + args: vec![NativeCallableDefault::String("leaf".to_string())], + }]; + let expected_default = NativeCallableDefault::Object { + class_name: "OuterDefault".to_string(), + args: nested_args.clone(), + }; + let spec = native_object_default_record("OuterDefault", &nested_args); + + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_object( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(constructor_registered, 1); + assert_eq!(default_registered, 1); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); +} + /// Verifies native AOT parent metadata is available for eval static-scope resolution. #[test] fn register_native_class_parent_records_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 07af4cdc45..fe5d48148e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -721,8 +721,8 @@ and Enum/attribute slice, Reflection type APIs beyond retained parameter, genera property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, -object-valued generated defaults beyond the positional -`new C()` parameter slice, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked +object-valued generated defaults that need named, spread, or more than eight +constructor arguments at one object-default level, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 1aa4e1a59d..09a5292929 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -72,6 +72,7 @@ const NATIVE_ATTRIBUTE_ARG_FLOAT: u8 = 5; const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; /// Local slot metadata needed for conservative eval scope synchronization. @@ -1241,7 +1242,7 @@ fn eval_native_object_default(expr: &Expr) -> Option } let mut default_args = Vec::with_capacity(args.len()); for arg in args { - default_args.push(eval_native_literal_default(arg)?); + default_args.push(eval_native_callable_default(arg)?); } Some(EvalNativeCallableDefault::Object { class_name: class_name.as_canonical(), @@ -1309,7 +1310,10 @@ fn encode_eval_native_object_default_arg(bytes: &mut Vec, default: &EvalNati bytes.push(NATIVE_OBJECT_DEFAULT_ARG_STRING); encode_eval_native_default_string(bytes, value); } - EvalNativeCallableDefault::Object { .. } => {} + EvalNativeCallableDefault::Object { .. } => { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_OBJECT); + bytes.extend_from_slice(&encode_eval_native_object_default(default)); + } } } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fe20a8fcc4..4ce9a06a93 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9831,6 +9831,42 @@ return $obj->describe() . ":" . EvalAotObjectDefaultMethodTarget::describeStatic assert_eq!(out, "meth:stat:meth"); } +/// Verifies eval materializes nested generated/AOT object defaults during method dispatch. +#[test] +fn test_eval_aot_method_call_uses_nested_object_default() { + let out = compile_and_run( + r#"label = $label; + } +} + +class EvalAotNestedObjectDefaultOuter { + public EvalAotNestedObjectDefaultInner $inner; + + public function __construct(EvalAotNestedObjectDefaultInner $inner = new EvalAotNestedObjectDefaultInner("outer")) { + $this->inner = $inner; + } +} + +class EvalAotNestedObjectDefaultMethodTarget { + public function describe(EvalAotNestedObjectDefaultOuter $outer = new EvalAotNestedObjectDefaultOuter(new EvalAotNestedObjectDefaultInner("method"))): string { + return $outer->inner->label; + } +} + +echo eval('$obj = new EvalAotNestedObjectDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotNestedObjectDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +return $obj->describe() . ":" . $default->inner->label;'); +"#, + ); + assert_eq!(out, "method:method"); +} + /// Verifies eval ReflectionMethod exposes generated/AOT by-ref and variadic parameter flags. #[test] fn test_eval_reflection_method_exposes_aot_parameter_flags() { @@ -13120,6 +13156,42 @@ return $box->label;'); assert_eq!(out, "ctor"); } +/// Verifies eval materializes nested generated/AOT object defaults during constructor dispatch. +#[test] +fn test_eval_dynamic_new_uses_constructor_nested_object_default() { + let out = compile_and_run( + r#"label = $label; + } +} + +class EvalDynamicNewNestedDefaultOuter { + public EvalDynamicNewNestedDefaultInner $inner; + + public function __construct(EvalDynamicNewNestedDefaultInner $inner = new EvalDynamicNewNestedDefaultInner("outer")) { + $this->inner = $inner; + } +} + +class EvalDynamicNewNestedDefaultCtor { + public string $label = ""; + + public function __construct(EvalDynamicNewNestedDefaultOuter $outer = new EvalDynamicNewNestedDefaultOuter(new EvalDynamicNewNestedDefaultInner("ctor"))) { + $this->label = $outer->inner->label; + } +} + +echo eval('$box = new EvalDynamicNewNestedDefaultCtor(); +return $box->label;'); +"#, + ); + assert_eq!(out, "ctor"); +} + /// Verifies eval object construction passes more than two arguments to an AOT constructor. #[test] fn test_eval_dynamic_new_runs_constructor_with_many_args() { From cb8a50fe8350e3e7edb78b9fe1bf215960f0d993 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 18:14:43 +0200 Subject: [PATCH 0648/1208] Support larger eval AOT object defaults --- .../elephc-magician/src/ffi/native_methods.rs | 2 +- crates/elephc-magician/src/ffi/tests.rs | 39 +++++++++++++++++++ .../src/interpreter/statements.rs | 38 +++++++++++++----- docs/php/eval.md | 4 +- src/codegen/lower_inst/builtins/eval.rs | 2 +- tests/codegen/eval.rs | 37 ++++++++++++++++++ 6 files changed, 109 insertions(+), 13 deletions(-) diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index 26f31175fb..308101a230 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -38,7 +38,7 @@ const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; -const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; +const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; #[derive(Clone, Copy)] enum NativeCallableTypePosition { diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 253abd7c55..59117693fc 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -34,6 +34,7 @@ const TEST_NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; +const TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; /// Test native invoker placeholder used only to validate ABI registration. unsafe extern "C" fn fake_native_invoker( @@ -706,6 +707,44 @@ fn register_native_object_default_decodes_nested_objects() { ); } +/// Verifies native AOT object defaults decode the full u8 constructor argument range. +#[test] +fn register_native_object_default_decodes_full_u8_arg_count() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownClass"; + let args = (0..TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS) + .map(|index| NativeCallableDefault::String(format!("arg{}", index))) + .collect::>(); + let expected_default = NativeCallableDefault::Object { + class_name: "LargeDefault".to_string(), + args: args.clone(), + }; + let spec = native_object_default_record("LargeDefault", &args); + + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_object( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(constructor_registered, 1); + assert_eq!(default_registered, 1); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); +} + /// Verifies native AOT parent metadata is available for eval static-scope resolution. #[test] fn register_native_class_parent_records_metadata() { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 925d4a1bc1..a46efcd161 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4703,6 +4703,7 @@ pub(in crate::interpreter) fn positional_evaluated_arg_values( pub(in crate::interpreter) fn bind_native_callable_bound_args( signature: Option, args: Vec, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(signature) = signature else { @@ -4712,7 +4713,7 @@ pub(in crate::interpreter) fn bind_native_callable_bound_args( return Err(EvalStatus::RuntimeFatal); } if signature.param_names().len() == signature.param_count() { - bind_native_signature_args(&signature, args, values) + bind_native_signature_args(&signature, args, context, values) } else { positional_evaluated_bound_args(Some(&signature), args) } @@ -4768,10 +4769,11 @@ pub(in crate::interpreter) fn write_back_native_callable_ref_args( Ok(()) } -/// Binds native AOT callable args and fills omitted scalar defaults from metadata. +/// Binds native AOT callable args and fills omitted defaults from metadata. fn bind_native_signature_args( signature: &NativeCallableSignature, args: Vec, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let mut bound_args = vec![None; signature.param_count()]; @@ -4826,7 +4828,7 @@ fn bind_native_signature_args( return Err(EvalStatus::RuntimeFatal); }; *value = Some(BoundMethodArg { - value: materialize_native_callable_default(default, values)?, + value: materialize_native_callable_default(default, context, values)?, ref_target: None, variadic_ref_targets: Vec::new(), }); @@ -4956,9 +4958,11 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + let signature = context.native_method_signature(class_name, method_name); let bound_args = bind_native_callable_bound_args( - context.native_method_signature(class_name, method_name), + signature, evaluated_args, + context, values, )?; let result = values.method_call(object, method_name, native_bound_arg_values(&bound_args)); @@ -4977,9 +4981,11 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + let signature = context.native_static_method_signature(class_name, method_name); let bound_args = bind_native_callable_bound_args( - context.native_static_method_signature(class_name, method_name), + signature, evaluated_args, + context, values, )?; let result = @@ -4999,9 +5005,11 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { + let signature = context.native_constructor_signature(class_name); let bound_args = bind_native_callable_bound_args( - context.native_constructor_signature(class_name), + signature, evaluated_args, + context, values, )?; let result = values.construct_object(object, native_bound_arg_values(&bound_args)); @@ -5015,6 +5023,7 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( /// Allocates a fresh runtime cell for one invocation-safe native AOT default. fn materialize_native_callable_default( default: &NativeCallableDefault, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match default { @@ -5025,7 +5034,7 @@ fn materialize_native_callable_default( NativeCallableDefault::String(value) => values.string(value), NativeCallableDefault::EmptyArray => values.array_new(0), NativeCallableDefault::Object { class_name, args } => { - materialize_native_callable_object_default(class_name, args, values) + materialize_native_callable_object_default(class_name, args, context, values) } } } @@ -5034,14 +5043,25 @@ fn materialize_native_callable_default( fn materialize_native_callable_object_default( class_name: &str, args: &[NativeCallableDefault], + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let object = values.new_object(class_name)?; let mut constructor_args = Vec::with_capacity(args.len()); for arg in args { - constructor_args.push(materialize_native_callable_default(arg, values)?); + constructor_args.push(EvaluatedCallArg { + name: None, + value: materialize_native_callable_default(arg, context, values)?, + ref_target: None, + }); } - if let Err(err) = values.construct_object(object, constructor_args) { + if let Err(err) = eval_native_constructor_with_evaluated_args( + class_name, + object, + constructor_args, + context, + values, + ) { let _ = values.release(object); return Err(err); } diff --git a/docs/php/eval.md b/docs/php/eval.md index fe5d48148e..d856b428c6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -721,8 +721,8 @@ and Enum/attribute slice, Reflection type APIs beyond retained parameter, genera property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, -object-valued generated defaults that need named, spread, or more than eight -constructor arguments at one object-default level, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked +object-valued generated defaults that need named or spread constructor arguments, +and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 09a5292929..b6ecf8980b 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -73,7 +73,7 @@ const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; -const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = 8; +const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; /// Local slot metadata needed for conservative eval scope synchronization. #[derive(Clone)] diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4ce9a06a93..d53a8878cb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9867,6 +9867,43 @@ return $obj->describe() . ":" . $default->inner->label;'); assert_eq!(out, "method:method"); } +/// Verifies eval materializes generated/AOT object defaults with more than eight constructor args. +#[test] +fn test_eval_aot_object_default_uses_large_constructor_arg_list() { + let out = compile_and_run( + r#"label = implode("", $parts); + } +} + +class EvalAotLargeObjectDefaultMethodTarget { + public function describe(EvalAotLargeObjectDefaultDep $dep = new EvalAotLargeObjectDefaultDep("A", "B", "C", "D", "E", "F", "G", "H", "I")): string { + return $dep->label; + } +} + +class EvalAotLargeObjectDefaultCtorTarget { + public string $label = ""; + + public function __construct(EvalAotLargeObjectDefaultDep $dep = new EvalAotLargeObjectDefaultDep("J", "K", "L", "M", "N", "O", "P", "Q", "R")) { + $this->label = $dep->label; + } +} + +echo eval('$obj = new EvalAotLargeObjectDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotLargeObjectDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +$box = new EvalAotLargeObjectDefaultCtorTarget(); +return $obj->describe() . ":" . $default->label . ":" . $box->label;'); +"#, + ); + assert_eq!(out, "ABCDEFGHI:ABCDEFGHI:JKLMNOPQR"); +} + /// Verifies eval ReflectionMethod exposes generated/AOT by-ref and variadic parameter flags. #[test] fn test_eval_reflection_method_exposes_aot_parameter_flags() { From de73f49649a3110ef69574a8a47ebe60e76ade8b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 18:26:57 +0200 Subject: [PATCH 0649/1208] Support named eval AOT object defaults --- crates/elephc-magician/src/context.rs | 24 +++++- .../elephc-magician/src/ffi/native_methods.rs | 32 ++++++- crates/elephc-magician/src/ffi/tests.rs | 83 +++++++++++++++++-- .../src/interpreter/reflection.rs | 11 ++- .../src/interpreter/statements.rs | 7 +- docs/php/eval.md | 1 - src/codegen/lower_inst/builtins/eval.rs | 43 +++++++++- tests/codegen/eval.rs | 37 +++++++++ 8 files changed, 216 insertions(+), 22 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index d09c913d27..d61437fbf9 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -125,10 +125,32 @@ pub enum NativeCallableDefault { EmptyArray, Object { class_name: String, - args: Vec, + args: Vec, }, } +/// Constructor argument for an object-valued native AOT callable default. +#[derive(Clone, Debug, PartialEq)] +pub struct NativeCallableObjectDefaultArg { + pub name: Option, + pub value: NativeCallableDefault, +} + +impl NativeCallableObjectDefaultArg { + /// Creates one positional constructor argument for an object-valued default. + pub fn positional(value: NativeCallableDefault) -> Self { + Self { name: None, value } + } + + /// Creates one named constructor argument for an object-valued default. + pub fn named(name: impl Into, value: NativeCallableDefault) -> Self { + Self { + name: Some(name.into()), + value, + } + } +} + /// Native AOT method or constructor signature metadata visible to eval fragments. #[derive(Clone)] pub struct NativeCallableSignature { diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index 308101a230..cfbffe43d3 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -12,7 +12,9 @@ use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ABI_VERSION}; -use crate::context::{NativeCallableDefault, NativeCallableSignature}; +use crate::context::{ + NativeCallableDefault, NativeCallableObjectDefaultArg, NativeCallableSignature, +}; use crate::eval_ir::{ EvalAttribute, EvalAttributeArg, EvalParameterType, EvalParameterTypeVariant, }; @@ -38,6 +40,7 @@ const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; +const NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; #[derive(Clone, Copy)] @@ -1844,8 +1847,33 @@ fn native_callable_object_default_from_bytes( fn native_callable_object_default_arg( bytes: &[u8], offset: &mut usize, +) -> Option { + let tag = native_attribute_take_u8(bytes, offset)?; + if tag == NATIVE_OBJECT_DEFAULT_ARG_NAMED { + let name = native_attribute_take_string(bytes, offset)?; + let value = native_callable_object_default_arg_value(bytes, offset)?; + return Some(NativeCallableObjectDefaultArg::named(name, value)); + } + native_callable_object_default_arg_value_for_tag(tag, bytes, offset) + .map(NativeCallableObjectDefaultArg::positional) +} + +/// Decodes one object-default constructor argument value from a generated binary spec. +fn native_callable_object_default_arg_value( + bytes: &[u8], + offset: &mut usize, ) -> Option { - match native_attribute_take_u8(bytes, offset)? { + let tag = native_attribute_take_u8(bytes, offset)?; + native_callable_object_default_arg_value_for_tag(tag, bytes, offset) +} + +/// Decodes one tagged object-default constructor argument value. +fn native_callable_object_default_arg_value_for_tag( + tag: u8, + bytes: &[u8], + offset: &mut usize, +) -> Option { + match tag { NATIVE_OBJECT_DEFAULT_ARG_SCALAR => { let kind = native_attribute_take_u64(bytes, offset)?; let payload = native_attribute_take_u64(bytes, offset)?; diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 59117693fc..fa1f62e5b3 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -20,7 +20,7 @@ use crate::abi::{ ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_DIRTY, SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, }; -use crate::context::NativeCallableDefault; +use crate::context::{NativeCallableDefault, NativeCallableObjectDefaultArg}; use crate::errors::EvalStatus; use crate::eval_ir::{EvalAttributeArg, EvalParameterTypeVariant}; use crate::value::{RuntimeCell, RuntimeCellHandle}; @@ -34,6 +34,7 @@ const TEST_NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; const TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; /// Test native invoker placeholder used only to validate ABI registration. @@ -110,7 +111,10 @@ fn native_member_attribute_push_string(record: &mut Vec, value: &str) { } /// Builds one object-valued native parameter default ABI record for registration tests. -fn native_object_default_record(class_name: &str, args: &[NativeCallableDefault]) -> Vec { +fn native_object_default_record( + class_name: &str, + args: &[NativeCallableObjectDefaultArg], +) -> Vec { let mut record = Vec::new(); native_member_attribute_push_string(&mut record, class_name); record.push(args.len() as u8); @@ -121,8 +125,17 @@ fn native_object_default_record(class_name: &str, args: &[NativeCallableDefault] } /// Appends one object-default constructor argument to a native parameter default record. -fn native_object_default_push_arg(record: &mut Vec, arg: &NativeCallableDefault) { - match arg { +fn native_object_default_push_arg(record: &mut Vec, arg: &NativeCallableObjectDefaultArg) { + if let Some(name) = &arg.name { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED); + native_member_attribute_push_string(record, name); + } + native_object_default_push_arg_value(record, &arg.value); +} + +/// Appends one object-default constructor argument value to a native parameter default record. +fn native_object_default_push_arg_value(record: &mut Vec, value: &NativeCallableDefault) { + match value { NativeCallableDefault::Null => { native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_NULL, 0) } @@ -673,10 +686,14 @@ fn register_native_methods_record_signature_metadata() { fn register_native_object_default_decodes_nested_objects() { let mut ctx = ElephcEvalContext::new(); let class = b"KnownClass"; - let nested_args = vec![NativeCallableDefault::Object { - class_name: "InnerDefault".to_string(), - args: vec![NativeCallableDefault::String("leaf".to_string())], - }]; + let nested_args = vec![NativeCallableObjectDefaultArg::positional( + NativeCallableDefault::Object { + class_name: "InnerDefault".to_string(), + args: vec![NativeCallableObjectDefaultArg::positional( + NativeCallableDefault::String("leaf".to_string()), + )], + }, + )]; let expected_default = NativeCallableDefault::Object { class_name: "OuterDefault".to_string(), args: nested_args.clone(), @@ -707,13 +724,61 @@ fn register_native_object_default_decodes_nested_objects() { ); } +/// Verifies native AOT object defaults can carry named constructor args. +#[test] +fn register_native_object_default_decodes_named_args() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownClass"; + let args = vec![ + NativeCallableObjectDefaultArg::named( + "right", + NativeCallableDefault::String("R".to_string()), + ), + NativeCallableObjectDefaultArg::named( + "left", + NativeCallableDefault::String("L".to_string()), + ), + ]; + let expected_default = NativeCallableDefault::Object { + class_name: "NamedDefault".to_string(), + args: args.clone(), + }; + let spec = native_object_default_record("NamedDefault", &args); + + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_object( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(constructor_registered, 1); + assert_eq!(default_registered, 1); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); +} + /// Verifies native AOT object defaults decode the full u8 constructor argument range. #[test] fn register_native_object_default_decodes_full_u8_arg_count() { let mut ctx = ElephcEvalContext::new(); let class = b"KnownClass"; let args = (0..TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS) - .map(|index| NativeCallableDefault::String(format!("arg{}", index))) + .map(|index| { + let value = NativeCallableDefault::String(format!("arg{}", index)); + NativeCallableObjectDefaultArg::positional(value) + }) .collect::>(); let expected_default = NativeCallableDefault::Object { class_name: "LargeDefault".to_string(), diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 511d0af814..d7662c5325 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -11,6 +11,7 @@ //! - Generated/AOT targets use focused runtime hooks for supported point lookups. use super::*; +use crate::context::NativeCallableObjectDefaultArg; use crate::eval_ir::EvalSourceLocation; const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; @@ -3374,9 +3375,13 @@ fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) } } -/// Converts one native object-default constructor argument into a positional eval call arg. -fn eval_reflection_native_callable_default_arg(default: &NativeCallableDefault) -> EvalCallArg { - EvalCallArg::positional(eval_reflection_native_callable_default_expr(default)) +/// Converts one native object-default constructor argument into an eval call arg. +fn eval_reflection_native_callable_default_arg(arg: &NativeCallableObjectDefaultArg) -> EvalCallArg { + let value = eval_reflection_native_callable_default_expr(&arg.value); + match &arg.name { + Some(name) => EvalCallArg::named(name, value), + None => EvalCallArg::positional(value), + } } /// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index a46efcd161..5672afd6a6 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -9,6 +9,7 @@ //! - Scope writes flow through shared scope-cell helpers so global aliases and reference aliases stay coherent. use super::*; +use crate::context::NativeCallableObjectDefaultArg; /// Executes statements in source order and propagates the first eval `return`. pub(in crate::interpreter) fn execute_statements( @@ -5042,7 +5043,7 @@ fn materialize_native_callable_default( /// Allocates and constructs one object-valued native AOT parameter default. fn materialize_native_callable_object_default( class_name: &str, - args: &[NativeCallableDefault], + args: &[NativeCallableObjectDefaultArg], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -5050,8 +5051,8 @@ fn materialize_native_callable_object_default( let mut constructor_args = Vec::with_capacity(args.len()); for arg in args { constructor_args.push(EvaluatedCallArg { - name: None, - value: materialize_native_callable_default(arg, context, values)?, + name: arg.name.clone(), + value: materialize_native_callable_default(&arg.value, context, values)?, ref_target: None, }); } diff --git a/docs/php/eval.md b/docs/php/eval.md index d856b428c6..01579c3991 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -721,7 +721,6 @@ and Enum/attribute slice, Reflection type APIs beyond retained parameter, genera property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, -object-valued generated defaults that need named or spread constructor arguments, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index b6ecf8980b..37dd941f76 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -73,6 +73,7 @@ const NATIVE_ATTRIBUTE_ARG_ARRAY: u8 = 6; const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; +const NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; /// Local slot metadata needed for conservative eval scope synchronization. @@ -148,10 +149,16 @@ enum EvalNativeCallableDefault { String(String), Object { class_name: String, - args: Vec, + args: Vec, }, } +/// Constructor argument metadata for an object-valued native callable default. +struct EvalNativeCallableObjectDefaultArg { + name: Option, + default: EvalNativeCallableDefault, +} + /// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { super::ensure_arg_count(inst, "eval", 1)?; @@ -1242,7 +1249,7 @@ fn eval_native_object_default(expr: &Expr) -> Option } let mut default_args = Vec::with_capacity(args.len()); for arg in args { - default_args.push(eval_native_callable_default(arg)?); + default_args.push(eval_native_object_default_arg(arg)?); } Some(EvalNativeCallableDefault::Object { class_name: class_name.as_canonical(), @@ -1250,6 +1257,21 @@ fn eval_native_object_default(expr: &Expr) -> Option }) } +/// Converts one object-valued default constructor argument into bridge metadata. +fn eval_native_object_default_arg(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::NamedArg { name, value } => Some(EvalNativeCallableObjectDefaultArg { + name: Some(name.clone()), + default: eval_native_callable_default(value)?, + }), + ExprKind::Spread(_) => None, + _ => Some(EvalNativeCallableObjectDefaultArg { + name: None, + default: eval_native_callable_default(expr)?, + }), + } +} + /// Converts supported property defaults into the compact eval bridge default ABI. fn eval_native_property_default( default: Option<&Expr>, @@ -1299,7 +1321,22 @@ fn encode_eval_native_object_default(default: &EvalNativeCallableDefault) -> Vec } /// Encodes one object-default constructor argument for libelephc-magician. -fn encode_eval_native_object_default_arg(bytes: &mut Vec, default: &EvalNativeCallableDefault) { +fn encode_eval_native_object_default_arg( + bytes: &mut Vec, + arg: &EvalNativeCallableObjectDefaultArg, +) { + if let Some(name) = &arg.name { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_NAMED); + encode_eval_native_default_string(bytes, name); + } + encode_eval_native_object_default_arg_value(bytes, &arg.default); +} + +/// Encodes one object-default constructor argument value for libelephc-magician. +fn encode_eval_native_object_default_arg_value( + bytes: &mut Vec, + default: &EvalNativeCallableDefault, +) { match default { EvalNativeCallableDefault::Scalar { kind, payload } => { bytes.push(NATIVE_OBJECT_DEFAULT_ARG_SCALAR); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d53a8878cb..2e7a12ba0c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9831,6 +9831,43 @@ return $obj->describe() . ":" . EvalAotObjectDefaultMethodTarget::describeStatic assert_eq!(out, "meth:stat:meth"); } +/// Verifies eval preserves named constructor args in generated/AOT object defaults. +#[test] +fn test_eval_aot_object_default_uses_named_constructor_args() { + let out = compile_and_run( + r#"label = $left . $right; + } +} + +class EvalAotNamedObjectDefaultMethodTarget { + public function describe(EvalAotNamedObjectDefaultDep $dep = new EvalAotNamedObjectDefaultDep(right: "R", left: "L")): string { + return $dep->label; + } +} + +class EvalAotNamedObjectDefaultCtorTarget { + public string $label = ""; + + public function __construct(EvalAotNamedObjectDefaultDep $dep = new EvalAotNamedObjectDefaultDep(right: "B", left: "A")) { + $this->label = $dep->label; + } +} + +echo eval('$obj = new EvalAotNamedObjectDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotNamedObjectDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +$box = new EvalAotNamedObjectDefaultCtorTarget(); +return $obj->describe() . ":" . $default->label . ":" . $box->label;'); +"#, + ); + assert_eq!(out, "LR:LR:AB"); +} + /// Verifies eval materializes nested generated/AOT object defaults during method dispatch. #[test] fn test_eval_aot_method_call_uses_nested_object_default() { From bfe5ff8a6c4561b205cc92fbaf4a4cc94e48c439 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 18:50:51 +0200 Subject: [PATCH 0650/1208] Support eval AOT array defaults --- crates/elephc-magician/src/context.rs | 30 +++ .../elephc-magician/src/ffi/native_methods.rs | 235 +++++++++++++++++- crates/elephc-magician/src/ffi/tests.rs | 130 +++++++++- .../src/interpreter/reflection.rs | 29 ++- .../src/interpreter/statements.rs | 47 +++- docs/php/eval.md | 12 +- src/codegen/lower_inst/builtins/eval.rs | 163 +++++++++++- tests/codegen/eval.rs | 68 +++++ 8 files changed, 702 insertions(+), 12 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index d61437fbf9..7b95e593c0 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -123,12 +123,42 @@ pub enum NativeCallableDefault { Float(f64), String(String), EmptyArray, + Array(Vec), Object { class_name: String, args: Vec, }, } +/// One element in an array-valued native AOT callable default. +#[derive(Clone, Debug, PartialEq)] +pub struct NativeCallableArrayDefaultElement { + pub key: Option, + pub value: NativeCallableDefault, +} + +impl NativeCallableArrayDefaultElement { + /// Creates one auto-indexed element for an array-valued default. + pub fn positional(value: NativeCallableDefault) -> Self { + Self { key: None, value } + } + + /// Creates one explicitly keyed element for an array-valued default. + pub fn keyed(key: NativeCallableArrayDefaultKey, value: NativeCallableDefault) -> Self { + Self { + key: Some(key), + value, + } + } +} + +/// Static PHP array key retained for an array-valued native AOT callable default. +#[derive(Clone, Debug, PartialEq)] +pub enum NativeCallableArrayDefaultKey { + Int(i64), + String(String), +} + /// Constructor argument for an object-valued native AOT callable default. #[derive(Clone, Debug, PartialEq)] pub struct NativeCallableObjectDefaultArg { diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index cfbffe43d3..2e70c35f47 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -13,7 +13,8 @@ use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ABI_VERSION}; use crate::context::{ - NativeCallableDefault, NativeCallableObjectDefaultArg, NativeCallableSignature, + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, NativeCallableDefault, + NativeCallableObjectDefaultArg, NativeCallableSignature, }; use crate::eval_ir::{ EvalAttribute, EvalAttributeArg, EvalParameterType, EvalParameterTypeVariant, @@ -41,6 +42,10 @@ const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; const NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; +const NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; +const NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; +const NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; +const NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; #[derive(Clone, Copy)] @@ -521,6 +526,62 @@ pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_defau .unwrap_or(0) } +/// Registers one generated native PHP method array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_array( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_array_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_array( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_array_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + /// Registers a generated native PHP constructor signature in an eval context. /// /// # Safety @@ -724,6 +785,33 @@ pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default .unwrap_or(0) } +/// Registers one generated native PHP constructor array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_array( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_array_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + /// Registers generated native PHP parent-class metadata in an eval context. /// /// # Safety @@ -825,6 +913,31 @@ pub unsafe extern "C" fn __elephc_eval_register_native_property_default_string( .unwrap_or(0) } +/// Registers one generated native PHP property array default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key and encoded +/// default pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_array( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_array_inner( + ctx, + property_key_ptr, + property_key_len, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + /// Registers one generated native PHP class/member attribute in an eval context. /// /// # Safety @@ -1195,6 +1308,33 @@ unsafe fn register_native_method_param_default_object_inner( ) } +/// Runs native method array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_array`; invalid +/// handles, names, indexes, or array specs fail closed as `false`. +unsafe fn register_native_method_param_default_array_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + /// Records a native method parameter default in the selected instance/static table. /// /// # Safety @@ -1467,6 +1607,31 @@ unsafe fn register_native_constructor_param_default_object_inner( ) } +/// Runs native constructor array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_array`; +/// invalid handles, names, indexes, or array specs fail closed as `false`. +unsafe fn register_native_constructor_param_default_array_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + /// Records a native constructor parameter default in the constructor signature table. /// /// # Safety @@ -1596,6 +1761,24 @@ unsafe fn register_native_property_default_string_inner( ) } +/// Runs native property array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_array`; invalid +/// handles, names, or array specs fail closed as `false`. +unsafe fn register_native_property_default_array_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_property_default_inner(ctx, property_key_ptr, property_key_len, default) +} + /// Records a native property default in the property metadata table. /// /// # Safety @@ -1826,6 +2009,21 @@ unsafe fn native_callable_object_default( (offset == bytes.len()).then_some(default) } +/// Decodes an array-valued native callable default from a generated binary spec. +/// +/// # Safety +/// `spec_ptr` must be readable for `spec_len` bytes when non-null. +unsafe fn native_callable_array_default( + spec_ptr: *const u8, + spec_len: u64, +) -> Option { + let len = usize::try_from(spec_len).ok()?; + let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; + let mut offset = 0; + let default = native_callable_array_default_from_bytes(bytes, &mut offset)?; + (offset == bytes.len()).then_some(default) +} + /// Decodes an object-valued native callable default from a generated binary spec slice. fn native_callable_object_default_from_bytes( bytes: &[u8], @@ -1843,6 +2041,40 @@ fn native_callable_object_default_from_bytes( Some(NativeCallableDefault::Object { class_name, args }) } +/// Decodes an array-valued native callable default from a generated binary spec slice. +fn native_callable_array_default_from_bytes( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut elements = Vec::with_capacity(len); + for _ in 0..len { + elements.push(native_callable_array_default_element(bytes, offset)?); + } + Some(NativeCallableDefault::Array(elements)) +} + +/// Decodes one array-default element and its optional static key. +fn native_callable_array_default_element( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let key = match native_attribute_take_u8(bytes, offset)? { + NATIVE_ARRAY_DEFAULT_KEY_AUTO => None, + NATIVE_ARRAY_DEFAULT_KEY_INT => { + Some(NativeCallableArrayDefaultKey::Int(native_attribute_take_i64( + bytes, offset, + )?)) + } + NATIVE_ARRAY_DEFAULT_KEY_STRING => Some(NativeCallableArrayDefaultKey::String( + native_attribute_take_string(bytes, offset)?, + )), + _ => return None, + }; + let value = native_callable_object_default_arg_value(bytes, offset)?; + Some(NativeCallableArrayDefaultElement { key, value }) +} + /// Decodes one object-default constructor argument from a generated binary spec. fn native_callable_object_default_arg( bytes: &[u8], @@ -1883,6 +2115,7 @@ fn native_callable_object_default_arg_value_for_tag( native_attribute_take_string(bytes, offset).map(NativeCallableDefault::String) } NATIVE_OBJECT_DEFAULT_ARG_OBJECT => native_callable_object_default_from_bytes(bytes, offset), + NATIVE_OBJECT_DEFAULT_ARG_ARRAY => native_callable_array_default_from_bytes(bytes, offset), _ => None, } } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index fa1f62e5b3..772c4c7cee 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -20,7 +20,10 @@ use crate::abi::{ ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_DIRTY, SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, }; -use crate::context::{NativeCallableDefault, NativeCallableObjectDefaultArg}; +use crate::context::{ + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, NativeCallableDefault, + NativeCallableObjectDefaultArg, +}; use crate::errors::EvalStatus; use crate::eval_ir::{EvalAttributeArg, EvalParameterTypeVariant}; use crate::value::{RuntimeCell, RuntimeCellHandle}; @@ -35,6 +38,10 @@ const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; const TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; const TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; /// Test native invoker placeholder used only to validate ABI registration. @@ -124,6 +131,35 @@ fn native_object_default_record( record } +/// Builds one array-valued native parameter default ABI record for registration tests. +fn native_array_default_record(elements: &[NativeCallableArrayDefaultElement]) -> Vec { + let mut record = Vec::new(); + record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); + for element in elements { + native_array_default_push_element(&mut record, element); + } + record +} + +/// Appends one array-default element and optional static key to a default record. +fn native_array_default_push_element( + record: &mut Vec, + element: &NativeCallableArrayDefaultElement, +) { + match &element.key { + Some(NativeCallableArrayDefaultKey::Int(value)) => { + record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_INT); + record.extend_from_slice(&value.to_le_bytes()); + } + Some(NativeCallableArrayDefaultKey::String(value)) => { + record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_STRING); + native_member_attribute_push_string(record, value); + } + None => record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_AUTO), + } + native_object_default_push_arg_value(record, &element.value); +} + /// Appends one object-default constructor argument to a native parameter default record. fn native_object_default_push_arg(record: &mut Vec, arg: &NativeCallableObjectDefaultArg) { if let Some(name) = &arg.name { @@ -159,6 +195,10 @@ fn native_object_default_push_arg_value(record: &mut Vec, value: &NativeCall record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT); record.extend_from_slice(&native_object_default_record(class_name, args)); } + NativeCallableDefault::Array(elements) => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_ARRAY); + record.extend_from_slice(&native_array_default_record(elements)); + } } } @@ -810,6 +850,94 @@ fn register_native_object_default_decodes_full_u8_arg_count() { ); } +/// Verifies native AOT array defaults can carry keyed and nested default values. +#[test] +fn register_native_array_default_decodes_nested_values() { + let mut ctx = ElephcEvalContext::new(); + let method = b"KnownClass::items"; + let class = b"KnownClass"; + let property = b"KnownClass::items"; + let elements = vec![ + NativeCallableArrayDefaultElement::keyed( + NativeCallableArrayDefaultKey::String("left".to_string()), + NativeCallableDefault::String("L".to_string()), + ), + NativeCallableArrayDefaultElement::keyed( + NativeCallableArrayDefaultKey::Int(2), + NativeCallableDefault::Array(vec![NativeCallableArrayDefaultElement::positional( + NativeCallableDefault::Int(7), + )]), + ), + NativeCallableArrayDefaultElement::positional(NativeCallableDefault::Object { + class_name: "ArrayDefaultDep".to_string(), + args: vec![NativeCallableObjectDefaultArg::named( + "label", + NativeCallableDefault::String("dep".to_string()), + )], + }), + ]; + let expected_default = NativeCallableDefault::Array(elements.clone()); + let spec = native_array_default_record(&elements); + + let method_registered = unsafe { + __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 1) + }; + let method_default_registered = unsafe { + __elephc_eval_register_native_method_param_default_array( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let constructor_default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_array( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + let property_default_registered = unsafe { + __elephc_eval_register_native_property_default_array( + &mut ctx, + property.as_ptr(), + property.len() as u64, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(method_registered, 1); + assert_eq!(method_default_registered, 1); + assert_eq!(constructor_registered, 1); + assert_eq!(constructor_default_registered, 1); + assert_eq!(property_default_registered, 1); + assert_eq!( + ctx.native_method_signature("knownclass", "ITEMS") + .expect("method metadata") + .param_default(0), + Some(&expected_default) + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); + assert_eq!( + ctx.native_property_default("KnownClass", "items"), + Some(expected_default) + ); +} + /// Verifies native AOT parent metadata is available for eval static-scope resolution. #[test] fn register_native_class_parent_records_metadata() { diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index d7662c5325..d61af69849 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -11,7 +11,10 @@ //! - Generated/AOT targets use focused runtime hooks for supported point lookups. use super::*; -use crate::context::NativeCallableObjectDefaultArg; +use crate::context::{ + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, + NativeCallableObjectDefaultArg, +}; use crate::eval_ir::EvalSourceLocation; const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; @@ -3365,6 +3368,12 @@ fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) NativeCallableDefault::Float(value) => EvalExpr::Const(EvalConst::Float(*value)), NativeCallableDefault::String(value) => EvalExpr::Const(EvalConst::String(value.clone())), NativeCallableDefault::EmptyArray => EvalExpr::Array(Vec::new()), + NativeCallableDefault::Array(elements) => EvalExpr::Array( + elements + .iter() + .map(eval_reflection_native_callable_default_array_element) + .collect(), + ), NativeCallableDefault::Object { class_name, args } => EvalExpr::NewObject { class_name: class_name.clone(), args: args @@ -3375,6 +3384,24 @@ fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) } } +/// Converts one native array-default element into an eval array literal element. +fn eval_reflection_native_callable_default_array_element( + element: &NativeCallableArrayDefaultElement, +) -> EvalArrayElement { + let value = eval_reflection_native_callable_default_expr(&element.value); + match &element.key { + Some(NativeCallableArrayDefaultKey::Int(key)) => EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::Int(*key)), + value, + }, + Some(NativeCallableArrayDefaultKey::String(key)) => EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String(key.clone())), + value, + }, + None => EvalArrayElement::Value(value), + } +} + /// Converts one native object-default constructor argument into an eval call arg. fn eval_reflection_native_callable_default_arg(arg: &NativeCallableObjectDefaultArg) -> EvalCallArg { let value = eval_reflection_native_callable_default_expr(&arg.value); diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 5672afd6a6..bb7745a3d5 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -9,7 +9,10 @@ //! - Scope writes flow through shared scope-cell helpers so global aliases and reference aliases stay coherent. use super::*; -use crate::context::NativeCallableObjectDefaultArg; +use crate::context::{ + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, + NativeCallableObjectDefaultArg, +}; /// Executes statements in source order and propagates the first eval `return`. pub(in crate::interpreter) fn execute_statements( @@ -5034,12 +5037,54 @@ fn materialize_native_callable_default( NativeCallableDefault::Float(value) => values.float(*value), NativeCallableDefault::String(value) => values.string(value), NativeCallableDefault::EmptyArray => values.array_new(0), + NativeCallableDefault::Array(elements) => { + materialize_native_callable_array_default(elements, context, values) + } NativeCallableDefault::Object { class_name, args } => { materialize_native_callable_object_default(class_name, args, context, values) } } } +/// Allocates one array-valued native AOT parameter default with fresh element cells. +fn materialize_native_callable_array_default( + elements: &[NativeCallableArrayDefaultElement], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let has_string_key = elements.iter().any(|element| { + matches!( + element.key, + Some(NativeCallableArrayDefaultKey::String(_)) + ) + }); + let mut array = if has_string_key { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; + let mut next_auto_key = 0; + for element in elements { + let key = match &element.key { + Some(NativeCallableArrayDefaultKey::Int(value)) => { + if *value >= next_auto_key { + next_auto_key = value.saturating_add(1); + } + values.int(*value)? + } + Some(NativeCallableArrayDefaultKey::String(value)) => values.string(value)?, + None => { + let key = values.int(next_auto_key)?; + next_auto_key = next_auto_key.saturating_add(1); + key + } + }; + let value = materialize_native_callable_default(&element.value, context, values)?; + array = values.array_set(array, key, value)?; + } + Ok(array) +} + /// Allocates and constructs one object-valued native AOT parameter default. fn materialize_native_callable_object_default( class_name: &str, diff --git a/docs/php/eval.md b/docs/php/eval.md index 01579c3991..61b7f85fc7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference constructor parameters. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -330,10 +330,10 @@ or tracked receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` constants can also be statically narrowed before materialization. AOT method reflection also exposes registered parameter names, declared parameter types, declared return types, required/optional -counts, and registered scalar, null, empty-array, or supported object-valued default values for generated +counts, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default values for generated constructor, instance-method, and static-method signatures. AOT property reflection exposes registered declared property types and supported scalar, -string, or null default values for generated property metadata, including +string, null, empty-array, or supported array-valued default values for generated property metadata, including `ReflectionClass::getDefaultProperties()`. AOT method and property/class-constant reflection expose generated member attributes when their arguments fit the materializable literal subset. @@ -390,7 +390,7 @@ reflected method is `__construct` or `__destruct`. `getNumberOfRequiredParameters()` report retained eval-declared function and method metadata, plus registered generated/AOT method parameter names, declared parameter and return types, required/optional counts, by-reference and variadic -flags, and scalar, null, empty-array, or supported object-valued default values when native +flags, and scalar, null, empty-array, supported array-valued, or supported object-valued default values when native method/static-method or constructor signatures are registered. Eval code can also reflect supported callable-builtin signatures, including internal origin, parameter names, parameter types, and return type metadata. @@ -720,7 +720,7 @@ ReflectionClass/Object/Function/Method/Parameter/Property/NamedType/UnionType/In and Enum/attribute slice, Reflection type APIs beyond retained parameter, generated property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, -null, empty-array, and supported object-valued parameter defaults during generated/AOT invocation, +null, empty-array, supported array-valued defaults, and supported object-valued parameter defaults during generated/AOT invocation, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 37dd941f76..9b1e3f37c3 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -74,6 +74,10 @@ const NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; const NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; +const NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; +const NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; +const NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; +const NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; /// Local slot metadata needed for conservative eval scope synchronization. @@ -147,12 +151,25 @@ struct EvalNativeMemberAttributeRegistration { enum EvalNativeCallableDefault { Scalar { kind: i64, payload: i64 }, String(String), + Array(Vec), Object { class_name: String, args: Vec, }, } +/// Array element metadata for a native callable default registered with eval. +struct EvalNativeCallableArrayDefaultElement { + key: Option, + default: EvalNativeCallableDefault, +} + +/// Static array key metadata for a native callable default registered with eval. +enum EvalNativeCallableArrayDefaultKey { + Int(i64), + String(String), +} + /// Constructor argument metadata for an object-valued native callable default. struct EvalNativeCallableObjectDefaultArg { name: Option, @@ -1205,7 +1222,9 @@ fn eval_native_php_type_member_specs(members: &[PhpType]) -> Option { /// Converts a PHP signature default into the compact eval bridge default ABI. fn eval_native_callable_default(expr: &Expr) -> Option { - eval_native_literal_default(expr).or_else(|| eval_native_object_default(expr)) + eval_native_literal_default(expr) + .or_else(|| eval_native_object_default(expr)) + .or_else(|| eval_native_array_default(expr)) } /// Converts scalar/string/empty-array defaults into the compact eval bridge default ABI. @@ -1272,6 +1291,53 @@ fn eval_native_object_default_arg(expr: &Expr) -> Option Option { + match &expr.kind { + ExprKind::ArrayLiteral(elements) => { + let mut default_elements = Vec::with_capacity(elements.len()); + for element in elements { + if matches!(element.kind, ExprKind::Spread(_)) { + return None; + } + default_elements.push(EvalNativeCallableArrayDefaultElement { + key: None, + default: eval_native_callable_default(element)?, + }); + } + Some(EvalNativeCallableDefault::Array(default_elements)) + } + ExprKind::ArrayLiteralAssoc(elements) => { + let mut default_elements = Vec::with_capacity(elements.len()); + for (key, value) in elements { + default_elements.push(EvalNativeCallableArrayDefaultElement { + key: Some(eval_native_array_default_key(key)?), + default: eval_native_callable_default(value)?, + }); + } + Some(EvalNativeCallableDefault::Array(default_elements)) + } + _ => None, + } +} + +/// Converts one supported static array key into bridge metadata. +fn eval_native_array_default_key(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => Some(EvalNativeCallableArrayDefaultKey::Int(*value)), + ExprKind::StringLiteral(value) => { + Some(EvalNativeCallableArrayDefaultKey::String(value.clone())) + } + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value + .checked_neg() + .map(EvalNativeCallableArrayDefaultKey::Int), + _ => None, + }, + _ => None, + } +} + /// Converts supported property defaults into the compact eval bridge default ABI. fn eval_native_property_default( default: Option<&Expr>, @@ -1279,7 +1345,7 @@ fn eval_native_property_default( is_abstract: bool, ) -> Option { if let Some(default) = default { - return eval_native_literal_default(default); + return eval_native_literal_default(default).or_else(|| eval_native_array_default(default)); } (!is_declared && !is_abstract).then_some(EvalNativeCallableDefault::Scalar { kind: NATIVE_DEFAULT_NULL, @@ -1320,6 +1386,38 @@ fn encode_eval_native_object_default(default: &EvalNativeCallableDefault) -> Vec bytes } +/// Encodes an array-valued native callable default for libelephc-magician. +fn encode_eval_native_array_default(default: &EvalNativeCallableDefault) -> Vec { + let EvalNativeCallableDefault::Array(elements) = default else { + return Vec::new(); + }; + let mut bytes = Vec::new(); + bytes.extend_from_slice(&(elements.len() as u32).to_le_bytes()); + for element in elements { + encode_eval_native_array_default_element(&mut bytes, element); + } + bytes +} + +/// Encodes one array-default element and its optional static key. +fn encode_eval_native_array_default_element( + bytes: &mut Vec, + element: &EvalNativeCallableArrayDefaultElement, +) { + match &element.key { + Some(EvalNativeCallableArrayDefaultKey::Int(value)) => { + bytes.push(NATIVE_ARRAY_DEFAULT_KEY_INT); + bytes.extend_from_slice(&value.to_le_bytes()); + } + Some(EvalNativeCallableArrayDefaultKey::String(value)) => { + bytes.push(NATIVE_ARRAY_DEFAULT_KEY_STRING); + encode_eval_native_default_string(bytes, value); + } + None => bytes.push(NATIVE_ARRAY_DEFAULT_KEY_AUTO), + } + encode_eval_native_object_default_arg_value(bytes, &element.default); +} + /// Encodes one object-default constructor argument for libelephc-magician. fn encode_eval_native_object_default_arg( bytes: &mut Vec, @@ -1351,6 +1449,10 @@ fn encode_eval_native_object_default_arg_value( bytes.push(NATIVE_OBJECT_DEFAULT_ARG_OBJECT); bytes.extend_from_slice(&encode_eval_native_object_default(default)); } + EvalNativeCallableDefault::Array(_) => { + bytes.push(NATIVE_OBJECT_DEFAULT_ARG_ARRAY); + bytes.extend_from_slice(&encode_eval_native_array_default(default)); + } } } @@ -1882,6 +1984,29 @@ fn register_eval_native_method_param_default( .extern_symbol("__elephc_eval_register_native_method_param_default_object") } } + EvalNativeCallableDefault::Array(_) => { + let spec = encode_eval_native_array_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + if is_static { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_static_method_param_default_array") + } else { + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_method_param_default_array") + } + } }; abi::emit_call_label(ctx.emitter, &symbol); } @@ -2134,6 +2259,23 @@ fn register_eval_native_property_default( .target .extern_symbol("__elephc_eval_register_native_property_default_string") } + EvalNativeCallableDefault::Array(_) => { + let spec = encode_eval_native_array_default(®istration.default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_property_default_array") + } EvalNativeCallableDefault::Object { .. } => return, }; abi::emit_call_label(ctx.emitter, &symbol); @@ -2441,6 +2583,23 @@ fn register_eval_native_constructor_param_default( .target .extern_symbol("__elephc_eval_register_native_constructor_param_default_object") } + EvalNativeCallableDefault::Array(_) => { + let spec = encode_eval_native_array_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_constructor_param_default_array") + } }; abi::emit_call_label(ctx.emitter, &symbol); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2e7a12ba0c..54e90e73c3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9329,6 +9329,28 @@ echo array_key_exists("typed", $defaults) ? "T" : "t"; ); } +/// Verifies eval ReflectionProperty exposes generated/AOT non-empty array defaults. +#[test] +fn test_eval_reflection_property_exposes_aot_array_default_values() { + let out = compile_and_run_capture( + r#" "L", 2 => "R", "tail"]; +} +echo eval('$property = new ReflectionProperty("EvalAotReflectArrayPropertyDefaultTarget", "items"); +$items = $property->getDefaultValue(); +$defaults = (new ReflectionClass("EvalAotReflectArrayPropertyDefaultTarget"))->getDefaultProperties(); +return $items["left"] . ":" . $items[2] . ":" . $items[3] . ":" . $defaults["items"]["left"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "L:R:tail:L"); +} + /// Verifies eval exposes generated/AOT member attributes through Reflection. #[test] fn test_eval_reflection_member_exposes_aot_attributes() { @@ -9800,6 +9822,34 @@ return $obj->countItems();'); assert_eq!(out, "0"); } +/// Verifies eval materializes generated/AOT non-empty array defaults during method dispatch. +#[test] +fn test_eval_aot_method_call_uses_array_default_values() { + let out = compile_and_run_capture( + r#"getParameters()[0]->getDefaultValue(); +return $obj->describe() . ":" . EvalAotArrayValueDefaultMethodTarget::describeStatic() . ":" . $default[0] . $default[1] . $default[2];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "4:5:6:7:8:9:456"); +} + /// Verifies eval materializes generated/AOT object defaults during method dispatch. #[test] fn test_eval_aot_method_call_uses_object_default() { @@ -13204,6 +13254,24 @@ return $box->count;'); assert_eq!(out, "0"); } +/// Verifies eval materializes generated/AOT non-empty array defaults during constructor dispatch. +#[test] +fn test_eval_dynamic_new_uses_constructor_array_default_values() { + let out = compile_and_run( + r#"label = $items[0] . ":" . $items[1] . ":" . $items[2]; + } +} +echo eval('$box = new EvalDynamicNewArrayValueDefaultCtor(); +return $box->label;'); +"#, + ); + assert_eq!(out, "4:5:6"); +} + /// Verifies eval materializes generated/AOT object defaults during constructor dispatch. #[test] fn test_eval_dynamic_new_uses_constructor_object_default() { From a80a7fd99a81f58a511afb28630085b585bea60a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 19:01:43 +0200 Subject: [PATCH 0651/1208] Harden eval ReflectionClass AOT instantiation --- .../src/interpreter/reflection.rs | 13 ++++ .../src/interpreter/statements.rs | 43 ++++++++++++- docs/php/eval.md | 3 +- tests/codegen/eval.rs | 63 ++++++++++++++++++- 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index d61af69849..5f070c2ef6 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2644,6 +2644,19 @@ pub(in crate::interpreter) fn eval_reflection_aot_class_allows_without_construct Ok(Some(flags & rejected_flags == 0)) } +/// Returns whether a generated/AOT reflected class can be constructed through a public constructor. +pub(in crate::interpreter) fn eval_reflection_aot_class_allows_public_instantiation( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + Ok(Some( + flags & EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE == EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE, + )) +} + /// Returns whether an absent or public AOT lifecycle method allows public reflection. fn eval_reflection_aot_lifecycle_method_allows_public_reflection( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index bb7745a3d5..b076a44570 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -441,8 +441,9 @@ fn eval_non_object_array_set_var_stmt( } else { values.array_new(1)? }; - let index = eval_expr(index, context, scope, values)?; + let index = eval_array_set_index(index, context, scope, values)?; let value = eval_expr(value, context, scope, values)?; + let array = eval_array_set_target_for_index(array, index, values)?; let array = values.array_set(array, index, value)?; for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { values.release(replaced)?; @@ -450,6 +451,43 @@ fn eval_non_object_array_set_var_stmt( Ok(()) } +/// Evaluates an array-set index and normalizes PHP integer-string keys. +fn eval_array_set_index( + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(index)? != EVAL_TAG_STRING { + return Ok(index); + } + let bytes = values.string_bytes(index)?; + match eval_numeric_string_array_key(&bytes) { + Some(key) => values.int(key), + None => Ok(index), + } +} + +/// Converts indexed arrays to associative arrays before writing a non-numeric string key. +fn eval_array_set_target_for_index( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(array)? != EVAL_TAG_ARRAY || values.type_tag(index)? != EVAL_TAG_STRING { + return Ok(array); + } + let len = values.array_len(array)?; + let mut assoc = values.assoc_new(len + 1)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + assoc = values.array_set(assoc, key, value)?; + } + Ok(assoc) +} + /// Executes an eval `try` body and handles supported `catch` clauses. pub(in crate::interpreter) fn execute_try_stmt( body: &[EvalStmt], @@ -4003,6 +4041,9 @@ fn eval_reflection_class_new_instance_result( let class_name = context .resolve_class_name(&reflected_name) .unwrap_or(reflected_name); + if eval_reflection_aot_class_allows_public_instantiation(&class_name, values)? == Some(false) { + return Err(EvalStatus::RuntimeFatal); + } eval_reflection_public_constructor_scope(context, values, |context, values| { let instance = values.new_object(&class_name)?; eval_native_constructor_with_evaluated_args( diff --git a/docs/php/eval.md b/docs/php/eval.md index 61b7f85fc7..dc95e0f3f9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -355,7 +355,8 @@ generated/AOT reflected classes with public constructors and forwards constructor arguments through eval's positional, named, and unpacking-aware call binding. Non-public constructors fail like PHP reflection construction. `ReflectionClass::newInstanceArgs()` constructs those reflected classes from an -argument array, treating string keys as named constructor arguments. +indexed or string-keyed argument array, including arrays built at eval runtime, +and treats string keys as named constructor arguments. `ReflectionClass::newInstanceWithoutConstructor()` allocates eval-declared and generated/AOT reflected classes, initializes supported property defaults, skips `__construct()`, and rejects reflected abstract classes, interfaces, traits, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 54e90e73c3..421f3596ae 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12228,6 +12228,60 @@ EvalReflectNewProtectedAotCtorChild::run(); ); } +/// Verifies eval ReflectionClass::newInstance constructs generated/AOT classes. +#[test] +fn test_eval_reflection_class_new_instance_constructs_aot_class() { + let out = compile_and_run( + r#"label = $left . $right; + } +} + +echo eval('$ref = new ReflectionClass("EvalReflectNewAotTarget"); +$first = $ref->newInstance("A"); +echo $first->label . ":"; +$second = $ref->newInstance(right: "Y", left: "X"); +return $second->label;'); +"#, + ); + assert_eq!(out, "AB:XY"); +} + +/// Verifies eval ReflectionClass::newInstance rejects non-instantiable AOT class-likes. +#[test] +fn test_eval_reflection_class_new_instance_rejects_aot_non_instantiable_class_likes() { + let cases = [ + ( + "abstract class EvalReflectNewAotAbstract {}", + "EvalReflectNewAotAbstract", + ), + ("interface EvalReflectNewAotIface {}", "EvalReflectNewAotIface"), + ("trait EvalReflectNewAotTrait {}", "EvalReflectNewAotTrait"), + ( + "enum EvalReflectNewAotEnum { case Ready; }", + "EvalReflectNewAotEnum", + ), + ]; + for (declaration, class_name) in cases { + let source = format!( + r#"newInstance();'); +"# + ); + let err = compile_and_run_expect_failure(&source); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "unexpected stderr for {class_name}: {err}" + ); + } +} + /// Verifies eval ReflectionMethod::invoke and invokeArgs call eval-declared methods. #[test] fn test_eval_reflection_method_invoke_calls_eval_method() { @@ -13383,10 +13437,15 @@ echo eval('$ref = new ReflectionClass("EvalReflectNewArgsAotTarget"); $first = $ref->newInstanceArgs(["right" => "Y", "left" => "X"]); echo $first->label . ":"; $second = $ref->newInstanceArgs(["Q", "R"]); -return $second->label;'); +echo $second->label . ":"; +$args = []; +$args["right"] = "N"; +$args["left"] = "M"; +$third = $ref->newInstanceArgs($args); +return $third->label;'); "#, ); - assert_eq!(out, "XY:QR"); + assert_eq!(out, "XY:QR:MN"); } /// Verifies eval object construction passes AOT constructor arguments on the caller stack. From c142c0fcf4234739a6e10394ebac1b6c94611023 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 19:04:58 +0200 Subject: [PATCH 0652/1208] Cover eval AOT invokeArgs runtime arrays --- docs/php/eval.md | 6 ++++-- tests/codegen/eval.rs | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index dc95e0f3f9..b2cc6d6faf 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -374,8 +374,10 @@ call supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, and named arguments. Generated/AOT bridge-supported invoke targets also bypass public/protected/private visibility -like PHP reflection. Runtime-only or non-literal argument arrays still require -richer runtime/typechecker support. +like PHP reflection. Inside eval fragments, `invokeArgs()` accepts literal or +runtime-built indexed/string-keyed argument arrays for those bridge-supported +generated/AOT targets; unsupported runtime-only reflector shapes outside that +signature slice still fail at runtime. Eval-declared method parameter type hints are checked when the method is entered. Supported checks include scalar hints with PHP-style weak scalar coercion, `array`, `object`, `iterable`, `mixed`, nullable/union forms, and diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 421f3596ae..527c0f46b0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9691,6 +9691,41 @@ return $join->invokeArgs($object, ["b" => "2", "a" => "1"]);'); ); } +/// Verifies eval ReflectionMethod::invokeArgs accepts runtime-built AOT argument arrays. +#[test] +fn test_eval_reflection_method_invoke_args_accepts_runtime_aot_arg_arrays() { + let out = compile_and_run_capture( + r#"invokeArgs($object, $args) . ":"; +$static = new ReflectionMethod("EvalAotReflectRuntimeInvokeArgsTarget", "make"); +$staticArgs = []; +$staticArgs["right"] = "Y"; +$staticArgs["left"] = "X"; +return $static->invokeArgs(null, $staticArgs);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "12:XY"); +} + /// Verifies eval ReflectionMethod::invoke bypasses generated/AOT method visibility. #[test] fn test_eval_reflection_method_invoke_bypasses_aot_method_visibility() { From 7e3e856cc42965890b9219db2cd5fae80702f157 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 19:16:28 +0200 Subject: [PATCH 0653/1208] Document eval nullable scalar AOT bridge --- docs/php/eval.md | 4 +-- tests/codegen/eval.rs | 77 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index b2cc6d6faf..cc03f4dd1b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -707,7 +707,7 @@ particular, advanced native callable descriptors and closure callback values are still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch -is limited to visibility-checked non-by-reference scalar/nullable-int/Mixed/array/iterable/object +is limited to visibility-checked non-by-reference scalar/nullable scalar/Mixed/array/iterable/object parameter signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference method and constructor parameters, including the generated positional variadic array slot when present. Broader parameter/return ABI shapes are still outside @@ -725,7 +725,7 @@ property, and function/method return metadata, broader parameter and generated property default-value materialization beyond scalar, null, empty-array, supported array-valued defaults, and supported object-valued parameter defaults during generated/AOT invocation, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked -scalar/nullable-int/Mixed/array/iterable/object parameter slice plus scalar/nullable-int/Mixed/array/iterable/object returns and +scalar/nullable scalar/Mixed/array/iterable/object parameter slice plus scalar/nullable scalar/Mixed/array/iterable/object returns and `__clone()` hooks. Generated/AOT method type metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 527c0f46b0..913fba3f68 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5248,6 +5248,83 @@ echo (new EvalAotNullableIntReturnBox())->run(); assert_eq!(out, "I7:N:S11:SN"); } +/// Verifies eval fragments can pass nullable scalar arguments to AOT methods and constructors. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_scalar_parameters() { + let out = compile_and_run( + r#"label = self::format($name, $flag, $ratio); + } + + public function describe(?string $name, ?bool $flag, ?float $ratio): string { + return self::format($name, $flag, $ratio); + } + + public static function describeStatic(?string $name = null, ?bool $flag = null, ?float $ratio = null): string { + return self::format($name, $flag, $ratio); + } + + private static function format(?string $name, ?bool $flag, ?float $ratio): string { + $namePart = $name === null ? "N" : (is_string($name) ? "S" . $name : "badName"); + $flagPart = $flag === null ? "N" : (is_bool($flag) ? ($flag ? "BT" : "BF") : "badFlag"); + $ratioPart = $ratio === null ? "N" : (is_float($ratio) ? "F" . $ratio : "badRatio"); + return $namePart . "/" . $flagPart . "/" . $ratioPart; + } +} + +echo eval('$defaulted = new EvalAotNullableScalarParamBox(); +$filled = new EvalAotNullableScalarParamBox("Ada", true, 2.5); +return $defaulted->label . ":" . $filled->label . ":" . $filled->describe(null, false, 3.5) . ":" . EvalAotNullableScalarParamBox::describeStatic("Bea", true, 4.5);'); +"#, + ); + assert_eq!(out, "N/N/N:SAda/BT/F2.5:N/BF/F3.5:SBea/BT/F4.5"); +} + +/// Verifies eval fragments can read nullable scalar return values from AOT methods. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_scalar_returns() { + let out = compile_and_run( + r#"maybeName(true) === "Ada" ? "S" : "bad") . ":" . (is_null($this->maybeName(false)) ? "SN" : "bad") . ":" . ($this->maybeFlag(true) === false ? "BF" : "bad") . ":" . (is_null($this->maybeFlag(false)) ? "BN" : "bad") . ":" . ($this->maybeRatio(true) === 1.5 ? "F15" : "bad") . ":" . (is_null($this->maybeRatio(false)) ? "FN" : "bad") . ":" . (EvalAotNullableScalarReturnBox::maybeStaticName(true) === "Bea" ? "SS" : "bad") . ":" . (is_null(EvalAotNullableScalarReturnBox::maybeStaticName(false)) ? "SSN" : "bad") . ":" . (EvalAotNullableScalarReturnBox::maybeStaticFlag(true) === true ? "SBT" : "bad") . ":" . (is_null(EvalAotNullableScalarReturnBox::maybeStaticFlag(false)) ? "SBN" : "bad") . ":" . (EvalAotNullableScalarReturnBox::maybeStaticRatio(true) === 2.5 ? "SF25" : "bad") . ":" . (is_null(EvalAotNullableScalarReturnBox::maybeStaticRatio(false)) ? "SFN" : "bad");'); + } +} + +echo (new EvalAotNullableScalarReturnBox())->run(); +"#, + ); + assert_eq!(out, "S:SN:BF:BN:F15:FN:SS:SSN:SBT:SBN:SF25:SFN"); +} + /// Verifies eval fragments can read iterable return values from AOT methods. #[test] fn test_eval_fragment_dispatches_aot_method_with_iterable_return() { From 65d0232800bcc9ad9958cbd894301a2b72c7aed1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 19:26:20 +0200 Subject: [PATCH 0654/1208] Cover eval nullable scalar OOP bridges --- docs/php/eval.md | 16 ++--- tests/codegen/eval.rs | 140 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 8 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index cc03f4dd1b..cffb9ef6fe 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,12 +71,12 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar, nullable-int, string, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar/nullable-int/string/Mixed/array/object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, nullable-int, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference constructor parameters. | +| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, nullable-int, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable-int/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable-int/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | @@ -128,10 +128,10 @@ as callable. Static method callables can use `["ClassName", "method"]` or `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method fallback supports the same named-argument and positional variadic-tail binding -for public/protected/private scalar/nullable-int/Mixed/array/iterable/object +for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameter signatures, including registered by-reference lvalue validation, when the current eval class scope satisfies PHP visibility. Generated AOT bridge -dispatch can invoke `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference method +dispatch can invoke `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference method and constructor parameters. Post-barrier native direct calls and string-literal `call_user_func()` callbacks @@ -579,12 +579,12 @@ that satisfies PHP visibility for the declaring class. Public declared static property reads/writes are bridged to eval. Protected and private declared static property reads/writes are bridged from native class scopes that satisfy PHP visibility for the declaring class. -Public/protected/private fixed scalar/nullable-int/Mixed/array/iterable/object +Public/protected/private fixed scalar/nullable scalar/Mixed/array/iterable/object method parameters through `$this->method(...)` and `Class::method(...)` are supported by the native method bridge when the eval fragment runs inside a native class scope that satisfies PHP visibility for the declaring class, including registered named arguments and string-keyed unpacking; method returns -may be scalar/nullable-int/Mixed/array/iterable/object values. +may be scalar/nullable scalar/Mixed/array/iterable/object values. ## Namespaces and constants @@ -708,7 +708,7 @@ still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch is limited to visibility-checked non-by-reference scalar/nullable scalar/Mixed/array/iterable/object -parameter signatures plus `mixed`/untyped, scalar including string, nullable-int, array, iterable, and object by-reference +parameter signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference method and constructor parameters, including the generated positional variadic array slot when present. Broader parameter/return ABI shapes are still outside those bridge paths. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 913fba3f68..ff8df2391b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4566,6 +4566,36 @@ $box->run(); assert_eq!(out, "N:I7:I42:N"); } +/// Verifies eval fragments can read and write public nullable scalar AOT properties through `$this`. +#[test] +fn test_eval_fragment_can_mutate_this_nullable_scalar_properties() { + let out = compile_and_run( + r#"name === null && $this->flag === null && $this->ratio === null) ? "N" : "bad"; + $this->name = "Ada"; + $this->flag = true; + $this->ratio = 2.5; + $out = $out . ":" . (($this->name === "Ada" && $this->flag === true && $this->ratio === 2.5) ? "set" : "bad"); + $this->name = null; + $this->flag = null; + $this->ratio = null; + return $out . ":" . (($this->name === null && $this->flag === null && $this->ratio === null) ? "N" : "bad");'); + } +} + +$box = new EvalNullableScalarPropBox(); +$box->run(); +"#, + ); + assert_eq!(out, "N:set:N"); +} + /// Verifies eval fragments can replace public array AOT properties through `$this`. #[test] fn test_eval_fragment_can_mutate_this_array_property() { @@ -4642,6 +4672,31 @@ echo eval('$out = (EvalNullableIntStaticPropBox::$count === null) ? "N" : "n"; assert_eq!(out, "N:I9:I33:N"); } +/// Verifies eval fragments can read and write public nullable scalar AOT static properties. +#[test] +fn test_eval_fragment_can_mutate_aot_nullable_scalar_static_properties() { + let out = compile_and_run( + r#"mutate($name, $flag, $ratio); +$first = $name . ":" . ($flag ? "T" : "F") . ":" . $ratio; +$name = null; +$flag = null; +$ratio = null; +$box->mutate($name, $flag, $ratio); +return $first . ":" . $name . ":" . ($flag ? "T" : "F") . ":" . $ratio;'); +"#, + ); + assert_eq!(out, "eval-method:T:3:method:T:1.5"); +} + /// Verifies eval dispatches generated/AOT instance methods with string by-reference params. #[test] fn test_eval_fragment_dispatches_aot_instance_method_with_string_by_ref_arg() { @@ -6025,6 +6109,34 @@ return $value;'); assert_eq!(out, "27"); } +/// Verifies eval writes nullable scalar by-reference AOT static method results back to eval variables. +#[test] +fn test_eval_fragment_dispatches_aot_nullable_scalar_static_by_ref_args() { + let out = compile_and_run( + r#" Date: Thu, 25 Jun 2026 19:30:51 +0200 Subject: [PATCH 0655/1208] Cover eval AOT static property reflection visibility --- docs/php/eval.md | 2 +- tests/codegen/eval.rs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index cffb9ef6fe..1abb2ed4c8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -312,7 +312,7 @@ cases, supported materialized property defaults, and current eval-declared static property values. For generated/AOT class-like symbols, the constant APIs also expose materializable scalar, string, null, `::class`, simple arithmetic or concatenation, and enum-case constant metadata through runtime hooks, and the -static-property APIs expose bridge-supported public generated/AOT static +static-property APIs expose bridge-supported public/protected/private generated/AOT static property values. Constant lookup is case-sensitive; single-value lookups return `false` when no constant or case is visible. `getConstants()` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ff8df2391b..9fe95cd2d0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11568,6 +11568,8 @@ fn test_eval_reflection_class_static_property_values_aot() { class EvalAotReflectStaticPropertyTarget { public static string $label = "start"; public static int $count = 2; + protected static string $secret = "prot"; + private static int $hidden = 4; public string $instance = "plain"; } echo eval('$ref = new ReflectionClass("EvalAotReflectStaticPropertyTarget"); @@ -11575,12 +11577,20 @@ $statics = $ref->getStaticProperties(); echo count($statics) . ":"; echo $statics["label"] . ":"; echo $statics["count"] . ":"; +echo $statics["secret"] . ":"; +echo $statics["hidden"] . ":"; echo $ref->getStaticPropertyValue("label") . ":"; $ref->setStaticPropertyValue("label", "changed"); echo EvalAotReflectStaticPropertyTarget::$label . ":"; $ref->setStaticPropertyValue(name: "count", value: 9); echo $ref->getStaticPropertyValue("count") . ":"; echo EvalAotReflectStaticPropertyTarget::$count . ":"; +echo $ref->getStaticPropertyValue("secret") . ":"; +$ref->setStaticPropertyValue("secret", "changed-prot"); +echo $ref->getStaticPropertyValue("secret") . ":"; +echo $ref->getStaticPropertyValue("hidden") . ":"; +$ref->setStaticPropertyValue("hidden", 8); +echo $ref->getStaticPropertyValue("hidden") . ":"; echo $ref->getStaticPropertyValue("instance", "fallback") . ":"; try { $ref->setStaticPropertyValue("instance", "bad"); @@ -11595,7 +11605,10 @@ try { "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:start:2:start:changed:9:9:fallback:S"); + assert_eq!( + out.stdout, + "4:start:2:prot:4:start:changed:9:9:prot:changed-prot:4:8:fallback:S" + ); } /// Verifies eval ReflectionProperty value APIs bridge generated/AOT storage. From 9966dbdbcc1e1acb71e24ca1d9f4c2d4dfb467c3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 19:44:10 +0200 Subject: [PATCH 0656/1208] Support eval reflection function strings --- .../src/interpreter/reflection.rs | 196 ++++++++++++++++-- .../src/interpreter/statements.rs | 9 + .../tests/builtins_class_metadata.rs | 26 +++ .../tests/builtins_reflection_functions.rs | 24 +++ src/types/checker/builtin_types/reflection.rs | 16 ++ tests/codegen/eval.rs | 31 +++ 6 files changed, 290 insertions(+), 12 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 5f070c2ef6..a442a67110 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -190,6 +190,7 @@ enum EvalReflectionFunctionMethodTarget { name: String, static_key: Option, static_variables: Vec, + parameters: Vec, source_location: Option, is_variadic: bool, is_static: bool, @@ -200,9 +201,13 @@ enum EvalReflectionFunctionMethodTarget { name: String, static_key: Option, static_variables: Vec, + parameters: Vec, source_location: Option, + visibility: Option, is_variadic: bool, is_static: bool, + is_final: bool, + is_abstract: bool, return_type_metadata: Option, }, } @@ -1237,6 +1242,25 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( } } +/// Handles eval-backed `ReflectionFunction::__toString()` and `ReflectionMethod::__toString()`. +pub(in crate::interpreter) fn eval_reflection_function_method_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_function_method_to_string(&target); + values.string(&rendered).map(Some) +} + /// Handles eval-backed `ReflectionParameter::isArray()` and `isCallable()` calls. pub(in crate::interpreter) fn eval_reflection_parameter_legacy_type_predicate_result( object: RuntimeCellHandle, @@ -5190,6 +5214,117 @@ fn eval_reflection_property_modifiers( modifiers } +/// Formats one reflected function or method similarly to PHP's `__toString()` output. +fn eval_reflection_function_method_to_string( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + let mut rendered = format!( + "{} {{\n - Parameters [{}] {{\n", + eval_reflection_function_method_header(target), + eval_reflection_function_method_parameters(target).len() + ); + for parameter in eval_reflection_function_method_parameters(target) { + rendered.push_str(" "); + rendered.push_str(&eval_reflection_function_method_parameter_to_string(parameter)); + rendered.push('\n'); + } + rendered.push_str(" }\n"); + if let Some(return_type) = eval_reflection_function_method_return_type(target) { + rendered.push_str(" - Return [ "); + rendered.push_str(&eval_reflection_type_metadata_to_string(return_type)); + rendered.push_str(" ]\n"); + } + rendered.push_str("}\n"); + rendered +} + +/// Returns the PHP-like header line for a reflected function or method. +fn eval_reflection_function_method_header( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + format!( + "Function [ function {} ]", + name.trim_start_matches('\\') + ) + } + EvalReflectionFunctionMethodTarget::Method { + name, + visibility, + is_static, + is_final, + is_abstract, + .. + } => { + let mut parts = Vec::new(); + if *is_abstract { + parts.push(String::from("abstract")); + } + if *is_final { + parts.push(String::from("final")); + } + if *is_static { + parts.push(String::from("static")); + } + if let Some(visibility) = visibility { + parts.push(eval_reflection_visibility_label(*visibility).to_string()); + } + parts.push(String::from("method")); + parts.push(name.clone()); + format!("Method [ {} ]", parts.join(" ")) + } + } +} + +/// Returns retained parameters for a reflected function or method target. +fn eval_reflection_function_method_parameters( + target: &EvalReflectionFunctionMethodTarget, +) -> &[EvalReflectionParameterMetadata] { + match target { + EvalReflectionFunctionMethodTarget::Function { parameters, .. } + | EvalReflectionFunctionMethodTarget::Method { parameters, .. } => parameters, + } +} + +/// Formats one parameter line for function-like `__toString()` output. +fn eval_reflection_function_method_parameter_to_string( + parameter: &EvalReflectionParameterMetadata, +) -> String { + let requiredness = if parameter.is_optional { + "optional" + } else { + "required" + }; + let mut signature_parts = Vec::new(); + if let Some(type_metadata) = parameter.type_metadata.as_ref() { + signature_parts.push(eval_reflection_type_metadata_to_string(type_metadata)); + } + let mut variable = String::new(); + if parameter.is_passed_by_reference { + variable.push('&'); + } + if parameter.is_variadic { + variable.push_str("..."); + } + variable.push('$'); + variable.push_str(¶meter.name); + signature_parts.push(variable); + let default = parameter + .default_value + .as_ref() + .and_then(eval_reflection_default_expr_to_string) + .map(|value| format!(" = {value}")) + .unwrap_or_default(); + format!( + "Parameter #{} [ <{}> {}{} ]", + parameter.position, + requiredness, + signature_parts.join(" "), + default + ) +} + /// Formats one reflected property similarly to PHP's `ReflectionProperty::__toString()`. fn eval_reflection_property_to_string( property_name: &str, @@ -7291,6 +7426,20 @@ fn eval_reflection_function_method_target( let function = context.function(&name.to_ascii_lowercase()); let is_variadic = function .is_some_and(|function| function.parameter_is_variadic().iter().any(|flag| *flag)); + let parameters = function + .map(|function| { + eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ) + }) + .unwrap_or_default(); let source_location = function.and_then(EvalFunction::source_location); let return_type_metadata = function .and_then(EvalFunction::return_type) @@ -7303,6 +7452,7 @@ fn eval_reflection_function_method_target( name: name.to_string(), static_key, static_variables, + parameters, source_location, is_variadic, is_static: false, @@ -7324,18 +7474,36 @@ fn eval_reflection_function_method_target( values, )? }; - let is_variadic = method_metadata.as_ref().is_some_and(|method| { - method - .parameters - .iter() - .any(|parameter| parameter.is_variadic) - }); - let is_static = method_metadata - .as_ref() - .is_some_and(|method| method.is_static); - let source_location = method_metadata.as_ref().and_then(|method| method.source_location); - let return_type_metadata = method_metadata.and_then(|method| method.return_type_metadata); - let static_method = eval_reflection_eval_method_static_target(declaring_class, method_name, context); + let ( + parameters, + source_location, + visibility, + is_variadic, + is_static, + is_final, + is_abstract, + return_type_metadata, + ) = match method_metadata { + Some(method) => { + let is_variadic = method + .parameters + .iter() + .any(|parameter| parameter.is_variadic); + ( + method.parameters, + method.source_location, + Some(method.visibility), + is_variadic, + method.is_static, + method.is_final, + method.is_abstract, + method.return_type_metadata, + ) + } + None => (Vec::new(), None, None, false, false, false, false, None), + }; + let static_method = + eval_reflection_eval_method_static_target(declaring_class, method_name, context); let declaring_class = static_method .as_ref() .map(|(declaring_class, _)| declaring_class.clone()); @@ -7351,9 +7519,13 @@ fn eval_reflection_function_method_target( name: method_name.to_string(), static_key, static_variables, + parameters, source_location, + visibility, is_variadic, is_static, + is_final, + is_abstract, return_type_metadata, })) } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b076a44570..f0bde8d5e5 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3689,6 +3689,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_function_method_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_method_prototype_result( identity, method_name, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index f65dee139b..c6a125e35c 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -1896,6 +1896,32 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod formats retained eval method metadata through `__toString()`. +#[test] +fn execute_program_reflection_method_to_string() { + let program = parse_fragment( + br#"class EvalReflectMethodStringTarget { + final public static function run(?int $id, string $label = "ok"): ?string { + return $label; + } +} +$ref = new ReflectionMethod("EvalReflectMethodStringTarget", "run"); +echo str_replace("\n", "|", $ref->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Method [ final static public method run ] {| - Parameters [2] {| Parameter #0 [ ?int $id ]| Parameter #1 [ string $label = 'ok' ]| }| - Return [ ?string ]|}|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionParameter reports eval constructor-promotion metadata. #[test] fn execute_program_reflection_parameter_reports_eval_promoted_metadata() { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs index 382d5a078c..e5cd4e9a15 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -127,6 +127,30 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionFunction formats retained eval function metadata through `__toString()`. +#[test] +fn execute_program_reflection_function_to_string() { + let program = parse_fragment( + br#"function eval_reflect_string(string $name, int $count = 3, &...$items): ?string { + return $name; +} +$ref = new ReflectionFunction("eval_reflect_string"); +echo str_replace("\n", "|", $ref->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Function [ function eval_reflect_string ] {| - Parameters [3] {| Parameter #0 [ string $name ]| Parameter #1 [ int $count = 3 ]| Parameter #2 [ &...$items ]| }| - Return [ ?string ]|}|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionFunction origin metadata APIs report eval user-defined defaults. #[test] fn execute_program_reflection_function_reports_origin_metadata_defaults() { diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 0b6d338c02..5ae3414e73 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3814,6 +3814,12 @@ fn builtin_reflection_owner_class( methods.push(builtin_reflection_constant_null_mixed_method("getType")); } if matches!(name, "ReflectionFunction" | "ReflectionMethod") { + properties.push(builtin_property( + "__string", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + )); properties.push(builtin_property( "__parameters", Visibility::Private, @@ -3884,6 +3890,10 @@ fn builtin_reflection_owner_class( "getTentativeReturnType", )); methods.push(builtin_reflection_function_method_is_variadic_method()); + methods.push(builtin_reflection_class_string_method( + "__toString", + "__string", + )); } if name == "ReflectionMethod" { properties.push(builtin_property( @@ -4866,6 +4876,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionMethod" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } for method_name in [ "isfinal", "isabstract", @@ -4903,6 +4916,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionFunction" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invoke")) { sig.return_type = PhpType::Mixed; sig.variadic = Some("args".to_string()); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9fe95cd2d0..8649b4dfd3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11800,6 +11800,37 @@ echo $plain->getReturnType() === null ? "Q" : "q";'); assert_eq!(out.stdout, "T:int:N:B:2:intBstringB:never:n:B:p:Q"); } +/// Verifies eval ReflectionFunction and ReflectionMethod stringify retained signatures. +#[test] +fn test_eval_reflection_function_method_to_string() { + let out = compile_and_run_capture( + r#"__toString()); +echo "::"; +$method = new ReflectionMethod("EvalReflectMethodStringTextTarget", "run"); +echo str_replace("\n", "|", $method->__toString());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Function [ function eval_reflect_string_text ] {| - Parameters [3] {| Parameter #0 [ string $name ]| Parameter #1 [ int $count = 3 ]| Parameter #2 [ &...$items ]| }| - Return [ ?string ]|}|::Method [ final static public method run ] {| - Parameters [2] {| Parameter #0 [ ?int $id ]| Parameter #1 [ string $label = 'ok' ]| }| - Return [ ?string ]|}|" + ); +} + /// Verifies eval Reflection origin metadata APIs are present on supported owners. #[test] fn test_eval_reflection_origin_metadata_defaults() { From ae8389196ac7c7e74c874aca4e840a38f9a2de3c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 20:00:57 +0200 Subject: [PATCH 0657/1208] Support eval reflection class strings --- .../src/interpreter/reflection.rs | 246 ++++++++++++++++++ .../src/interpreter/statements.rs | 9 + .../tests/builtins_class_metadata.rs | 25 ++ docs/php/eval.md | 3 + src/types/checker/builtin_types/reflection.rs | 16 ++ tests/codegen/eval.rs | 24 ++ 6 files changed, 323 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index a442a67110..2eeba41069 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -569,6 +569,28 @@ pub(in crate::interpreter) fn eval_reflection_class_basic_metadata_result( } } +/// Handles `ReflectionClass::__toString()` calls for eval-visible class metadata. +pub(in crate::interpreter) fn eval_reflection_class_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_class_to_string(&reflected_name, context, values)?; + values.string(&rendered).map(Some) +} + /// Returns one boolean ReflectionClass flag after validating a no-arg call. fn eval_reflection_class_flag_result( flags: u64, @@ -5214,6 +5236,230 @@ fn eval_reflection_property_modifiers( modifiers } +/// Formats one reflected class-like symbol similarly to PHP's `__toString()` output. +fn eval_reflection_class_to_string( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let metadata = eval_reflection_class_to_string_metadata(reflected_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let constant_lines = + eval_reflection_class_constant_string_lines(&metadata.resolved_name, context, values)?; + let property_lines = + eval_reflection_class_property_string_lines(&metadata, context, values)?; + let method_lines = eval_reflection_class_method_string_lines(&metadata, context, values)?; + + let mut rendered = format!("{} {{\n", eval_reflection_class_to_string_header(&metadata)); + eval_reflection_class_append_string_section(&mut rendered, "Constants", &constant_lines); + eval_reflection_class_append_string_section(&mut rendered, "Properties", &property_lines); + eval_reflection_class_append_string_section(&mut rendered, "Methods", &method_lines); + rendered.push_str("}\n"); + Ok(rendered) +} + +/// Returns eval or AOT class metadata for a ReflectionClass string dump. +fn eval_reflection_class_to_string_metadata( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) { + return Ok(Some(metadata)); + } + let runtime_class_name = reflected_name.trim_start_matches('\\'); + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(runtime_class_name, values)? + else { + return Ok(None); + }; + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + runtime_class_name, + values, + )?; + let property_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + runtime_class_name, + values, + )?; + let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; + let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; + Ok(Some(EvalReflectionClassMetadata { + resolved_name: runtime_class_name.to_string(), + source_location: None, + attributes: context.native_class_attributes(runtime_class_name), + flags, + modifiers, + interface_names, + trait_names: Vec::new(), + method_names, + property_names, + parent_class_name, + })) +} + +/// Returns the PHP-like header line for `ReflectionClass::__toString()`. +fn eval_reflection_class_to_string_header(metadata: &EvalReflectionClassMetadata) -> String { + let origin = if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_INTERNAL != 0 { + "" + } else { + "" + }; + let kind = eval_reflection_class_to_string_kind(metadata.flags); + let mut parts = Vec::new(); + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + parts.push(String::from("interface")); + parts.push(metadata.resolved_name.clone()); + if !metadata.interface_names.is_empty() { + parts.push(String::from("extends")); + parts.push(metadata.interface_names.join(", ")); + } + } else if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + parts.push(String::from("trait")); + parts.push(metadata.resolved_name.clone()); + } else { + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0 { + parts.push(String::from("abstract")); + } + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0 { + parts.push(String::from("final")); + } + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0 { + parts.push(String::from("readonly")); + } + parts.push(String::from("class")); + parts.push(metadata.resolved_name.clone()); + if let Some(parent_class_name) = metadata.parent_class_name.as_ref() { + parts.push(String::from("extends")); + parts.push(parent_class_name.clone()); + } + if !metadata.interface_names.is_empty() { + parts.push(String::from("implements")); + parts.push(metadata.interface_names.join(", ")); + } + } + format!("{kind} [ {origin} {} ]", parts.join(" ")) +} + +/// Returns the ReflectionClass string header owner kind label. +fn eval_reflection_class_to_string_kind(flags: u64) -> &'static str { + if flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + "Interface" + } else if flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + "Trait" + } else { + "Class" + } +} + +/// Formats all constants visible to `ReflectionClass::__toString()`. +fn eval_reflection_class_constant_string_lines( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let constant_names = eval_reflection_constant_names(reflected_name, context, values)?; + let mut lines = Vec::with_capacity(constant_names.len()); + for constant_name in constant_names { + let line = eval_reflection_class_constant_to_string( + reflected_name, + &constant_name, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + context, + values, + )?; + lines.push(line.trim_end_matches('\n').to_string()); + } + Ok(lines) +} + +/// Formats all properties visible to `ReflectionClass::__toString()`. +fn eval_reflection_class_property_string_lines( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut lines = Vec::with_capacity(metadata.property_names.len()); + for property_name in &metadata.property_names { + let Some(member) = eval_reflection_reflected_property_metadata( + &metadata.resolved_name, + property_name, + context, + values, + )? + else { + continue; + }; + lines.push(eval_reflection_property_to_string(property_name, &member)); + } + Ok(lines) +} + +/// Formats all methods visible to `ReflectionClass::__toString()`. +fn eval_reflection_class_method_string_lines( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut lines = Vec::with_capacity(metadata.method_names.len()); + for method_name in &metadata.method_names { + let member = + if let Some(member) = + eval_reflection_method_metadata(&metadata.resolved_name, method_name, context) + { + Some(member) + } else { + eval_reflection_aot_method_metadata_with_signature_if_exists( + &metadata.resolved_name, + method_name, + context, + values, + )? + }; + let Some(member) = member else { + continue; + }; + lines.push(eval_reflection_method_summary_to_string(method_name, &member)); + } + Ok(lines) +} + +/// Appends one named section to a ReflectionClass string dump. +fn eval_reflection_class_append_string_section( + rendered: &mut String, + label: &str, + lines: &[String], +) { + rendered.push_str(&format!(" - {label} [{}] {{\n", lines.len())); + for line in lines { + rendered.push_str(" "); + rendered.push_str(line); + rendered.push('\n'); + } + rendered.push_str(" }\n"); +} + +/// Formats one reflected method line for `ReflectionClass::__toString()`. +fn eval_reflection_method_summary_to_string( + method_name: &str, + member: &EvalReflectionMemberMetadata, +) -> String { + let mut parts = Vec::new(); + if member.is_abstract { + parts.push(String::from("abstract")); + } + if member.is_final { + parts.push(String::from("final")); + } + if member.is_static { + parts.push(String::from("static")); + } + parts.push(eval_reflection_visibility_label(member.visibility).to_string()); + parts.push(String::from("method")); + parts.push(method_name.to_string()); + format!("Method [ {} ]", parts.join(" ")) +} + /// Formats one reflected function or method similarly to PHP's `__toString()` output. fn eval_reflection_function_method_to_string( target: &EvalReflectionFunctionMethodTarget, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index f0bde8d5e5..0fc4a267e2 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3508,6 +3508,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( { return Ok(result); } + if let Some(result) = eval_reflection_class_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_class_implements_interface_result( identity, method_name, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index c6a125e35c..1d51a42530 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -1751,6 +1751,31 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass stringifies retained eval class metadata. +#[test] +fn execute_program_reflection_class_to_string() { + let program = parse_fragment( + br#"class EvalReflectClassStringTarget { + public const ANSWER = 42; + public int $id = 7; + public function read(string $name = "Ada"): ?string { return $name; } +} +echo (new ReflectionClass("EvalReflectClassStringTarget"))->__toString(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Class [ class EvalReflectClassStringTarget ] {\n - Constants [1] {\n Constant [ public int ANSWER ] { 42 }\n }\n - Properties [1] {\n Property [ public int $id = 7 ]\n }\n - Methods [1] {\n Method [ public method read ]\n }\n}\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod exposes eval method parameter objects with names and positions. #[test] fn execute_program_reflects_eval_method_parameters() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 1abb2ed4c8..0ae93229b5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -337,6 +337,9 @@ string, null, empty-array, or supported array-valued default values for generate `ReflectionClass::getDefaultProperties()`. AOT method and property/class-constant reflection expose generated member attributes when their arguments fit the materializable literal subset. +`ReflectionClass`, `ReflectionObject`, and `ReflectionEnum` expose a compact +`__toString()` dump for eval-visible class-like metadata, including supported +constants, properties, and methods. `ReflectionMethod::getDeclaringClass()` and `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 5ae3414e73..2b6cf3f32d 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1544,6 +1544,12 @@ fn builtin_reflection_class() -> FlattenedClass { Some(TypeExpr::Str), empty_string(), ), + builtin_property( + "__string", + Visibility::Private, + Some(TypeExpr::Str), + empty_string(), + ), builtin_property( "__attrs", Visibility::Private, @@ -1757,6 +1763,7 @@ fn builtin_reflection_class() -> FlattenedClass { false, )]), builtin_reflection_class_string_method("getName", "__name"), + builtin_reflection_class_string_method("__toString", "__string"), builtin_reflection_constant_false_union_method("getDocComment"), builtin_reflection_constant_false_union_method("getExtensionName"), builtin_reflection_constant_null_mixed_method("getExtension"), @@ -1925,6 +1932,7 @@ fn reflection_enum_inherited_method_is_supported(method_name: &str) -> bool { matches!( method_name.to_ascii_lowercase().as_str(), "__construct" + | "__tostring" | "getname" | "getshortname" | "getnamespacename" @@ -4760,6 +4768,14 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { sig.return_type = PhpType::Mixed; } } + if matches!( + class_name, + "ReflectionClass" | "ReflectionObject" | "ReflectionEnum" + ) { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } + } if matches!(class_name, "ReflectionMethod" | "ReflectionProperty") { for method_name in ["isstatic", "ispublic", "isprotected", "isprivate"] { if let Some(sig) = class_info.methods.get_mut(method_name) { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8649b4dfd3..3d91d5038d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11831,6 +11831,30 @@ echo str_replace("\n", "|", $method->__toString());'); ); } +/// Verifies eval ReflectionClass stringifies retained class metadata sections. +#[test] +fn test_eval_reflection_class_to_string() { + let out = compile_and_run_capture( + r#"__toString()); +echo "::"; +echo str_replace("\n", "|", (new ReflectionObject(new EvalReflectClassStringTarget()))->__toString());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + let expected = "Class [ class EvalReflectClassStringTarget ] {| - Constants [1] {| Constant [ public int ANSWER ] { 42 }| }| - Properties [1] {| Property [ public int $id = 7 ]| }| - Methods [1] {| Method [ public method read ]| }|}|"; + assert_eq!(out.stdout, format!("{expected}::{expected}")); +} + /// Verifies eval Reflection origin metadata APIs are present on supported owners. #[test] fn test_eval_reflection_origin_metadata_defaults() { From 1589606a415959638d47d6cfaf135bae59cd392f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 20:17:22 +0200 Subject: [PATCH 0658/1208] Support eval reflection parameter strings --- .../src/interpreter/reflection.rs | 158 +++++++++++++++++- .../src/interpreter/statements.rs | 8 + .../tests/builtins_class_metadata.rs | 28 ++++ docs/php/eval.md | 3 +- src/types/checker/builtin_types/reflection.rs | 4 + tests/codegen/eval.rs | 27 +++ 6 files changed, 225 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 2eeba41069..82a10f08e0 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1318,6 +1318,24 @@ pub(in crate::interpreter) fn eval_reflection_parameter_legacy_type_predicate_re .map(Some) } +/// Handles eval-backed `ReflectionParameter::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_parameter_to_string_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + if !eval_reflection_object_has_class(object, "ReflectionParameter", values)? { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_parameter_object_to_string(object, values)?; + values.string(&rendered).map(Some) +} + /// Handles eval-backed `ReflectionType::__toString()` calls. pub(in crate::interpreter) fn eval_reflection_type_to_string_result( object: RuntimeCellHandle, @@ -1328,6 +1346,143 @@ pub(in crate::interpreter) fn eval_reflection_type_to_string_result( if !method_name.eq_ignore_ascii_case("__toString") { return Ok(None); } + let Some(rendered) = eval_reflection_type_object_to_string(object, values)? else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + values.string(&rendered).map(Some) +} + +/// Formats one ReflectionParameter object through its public metadata methods. +fn eval_reflection_parameter_object_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let position = eval_reflection_no_arg_int_method(object, "getPosition", values)?; + let name = eval_reflection_no_arg_string_method(object, "getName", values)?; + let is_optional = eval_reflection_no_arg_bool_method(object, "isOptional", values)?; + let is_passed_by_reference = + eval_reflection_no_arg_bool_method(object, "isPassedByReference", values)?; + let is_variadic = eval_reflection_no_arg_bool_method(object, "isVariadic", values)?; + let type_value = values.method_call(object, "getType", Vec::new())?; + let type_text = if values.is_null(type_value)? { + None + } else { + eval_reflection_type_object_to_string(type_value, values)? + }; + + let mut signature_parts = Vec::new(); + if let Some(type_text) = type_text { + signature_parts.push(type_text); + } + let mut variable = String::new(); + if is_passed_by_reference { + variable.push('&'); + } + if is_variadic { + variable.push_str("..."); + } + variable.push('$'); + variable.push_str(&name); + signature_parts.push(variable); + let requiredness = if is_optional { "optional" } else { "required" }; + let default = eval_reflection_parameter_object_default_to_string(object, values)? + .map(|value| format!(" = {value}")) + .unwrap_or_default(); + + Ok(format!( + "Parameter #{} [ <{}> {}{} ]", + position, + requiredness, + signature_parts.join(" "), + default + )) +} + +/// Formats a ReflectionParameter default through its public metadata methods. +fn eval_reflection_parameter_object_default_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_no_arg_bool_method(object, "isDefaultValueAvailable", values)? { + return Ok(None); + } + if eval_reflection_no_arg_bool_method(object, "isDefaultValueConstant", values)? { + let constant_name = + eval_reflection_no_arg_string_method(object, "getDefaultValueConstantName", values)?; + if !constant_name.is_empty() { + return Ok(Some(constant_name)); + } + } + let default_value = values.method_call(object, "getDefaultValue", Vec::new())?; + eval_reflection_runtime_default_value_to_string(default_value, values).map(Some) +} + +/// Formats one materialized scalar-ish default value for reflection string output. +fn eval_reflection_runtime_default_value_to_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(match values.type_tag(value)? { + EVAL_TAG_NULL => String::from("NULL"), + EVAL_TAG_BOOL => { + if values.truthy(value)? { + String::from("true") + } else { + String::from("false") + } + } + EVAL_TAG_INT | EVAL_TAG_FLOAT => String::from_utf8_lossy(&values.string_bytes(value)?) + .into_owned(), + EVAL_TAG_STRING => { + let value = String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(); + format!("'{value}'") + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC if values.array_len(value)? == 0 => String::from("[]"), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("Array"), + EVAL_TAG_OBJECT => String::from("Object"), + _ => String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(), + }) +} + +/// Calls one no-arg Reflection method and returns its string result. +fn eval_reflection_no_arg_string_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + eval_reflection_string_arg(value, values) +} + +/// Calls one no-arg Reflection method and returns its bool result. +fn eval_reflection_no_arg_bool_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + if values.type_tag(value)? != EVAL_TAG_BOOL { + return Err(EvalStatus::RuntimeFatal); + } + values.truthy(value) +} + +/// Calls one no-arg Reflection method and returns its int result. +fn eval_reflection_no_arg_int_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + eval_int_value(value, values) +} + +/// Formats one eval-visible ReflectionType object if the value is a retained type object. +fn eval_reflection_type_object_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { let type_kind = if eval_reflection_object_has_class(object, "ReflectionNamedType", values)? { Some(("ReflectionNamedType", "")) } else if eval_reflection_object_has_class(object, "ReflectionUnionType", values)? { @@ -1340,13 +1495,12 @@ pub(in crate::interpreter) fn eval_reflection_type_to_string_result( let Some((class_name, separator)) = type_kind else { return Ok(None); }; - eval_reflection_bind_no_args(evaluated_args)?; let rendered = if class_name == "ReflectionNamedType" { eval_reflection_named_type_to_string(object, values)? } else { eval_reflection_composite_type_to_string(object, separator, values)? }; - values.string(&rendered).map(Some) + Ok(Some(rendered)) } /// Formats one eval-visible ReflectionNamedType object from its public methods. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 0fc4a267e2..3c9109da4c 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3503,6 +3503,14 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some(result) = eval_reflection_parameter_to_string_result( + object, + method_name, + evaluated_args.clone(), + values, + )? { + return Ok(result); + } if let Some(result) = eval_reflection_type_to_string_result(object, method_name, evaluated_args.clone(), values)? { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 1d51a42530..6d74679fe2 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -1885,6 +1885,34 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionParameter formats retained eval parameter metadata through `__toString()`. +#[test] +fn execute_program_reflection_parameter_to_string() { + let program = parse_fragment( + br#"class EvalReflectParameterStringTarget { + const LABEL = "L"; + public function run(string $name, int $count = 3, $label = self::LABEL, &...$items) {} +} +$params = (new ReflectionMethod("EvalReflectParameterStringTarget", "run"))->getParameters(); +foreach ($params as $param) { + echo $param->__toString(); + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Parameter #0 [ string $name ]|Parameter #1 [ int $count = 3 ]|Parameter #2 [ $label = self::LABEL ]|Parameter #3 [ &...$items ]|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod exposes eval-declared return type metadata. #[test] fn execute_program_reflection_method_reports_return_type_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 0ae93229b5..e6c41a7147 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -418,7 +418,8 @@ Function, method, and parameter attributes are exposed through `getAttributes()` using materialized `ReflectionAttribute` objects. Parameter default values, optionality, nullability, variadic flags, and by-reference flags are retained for eval-declared functions and methods, including -`ReflectionParameter::allowsNull()`. `ReflectionParameter::getDeclaringClass()` +`ReflectionParameter::allowsNull()` and `ReflectionParameter::__toString()`. +`ReflectionParameter::getDeclaringClass()` returns the declaring class-like symbol for eval method parameters, and `ReflectionParameter::getDeclaringFunction()` returns a `ReflectionFunction` object for eval free-function parameters or a `ReflectionMethod` object for the diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 2b6cf3f32d..e02d536eb7 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -909,6 +909,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { builtin_reflection_slot_getter("isCallable", "__is_callable_type", TypeExpr::Bool), builtin_reflection_slot_getter("getType", "__type", mixed_type()), builtin_reflection_slot_getter("getClass", "__class", mixed_type()), + builtin_reflection_slot_getter("__toString", "__name", TypeExpr::Str), builtin_reflection_owner_get_attributes_method(), builtin_reflection_slot_getter( "isDefaultValueAvailable", @@ -4958,6 +4959,9 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } } if class_name == "ReflectionParameter" { + if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { + sig.return_type = PhpType::Str; + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getPosition")) { sig.return_type = PhpType::Int; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3d91d5038d..cdc97b0e3b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10808,6 +10808,33 @@ echo $unionType; ); } +/// Verifies eval ReflectionParameter stringifies retained parameter metadata. +#[test] +fn test_eval_reflection_parameter_to_string() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + echo $param->__toString(); + echo "|"; +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Parameter #0 [ string $name ]|Parameter #1 [ int $count = 3 ]|Parameter #2 [ $label = self::LABEL ]|Parameter #3 [ &...$items ]|" + ); +} + /// Verifies eval ReflectionParameter::getClass() reports retained object type metadata. #[test] fn test_eval_reflection_parameter_get_class_reports_named_object_type() { From 52ac13c0177f6082a2578cb15487d62632a8cecd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 20:31:34 +0200 Subject: [PATCH 0659/1208] Support eval reflection method factory --- .../src/interpreter/reflection.rs | 88 +++++++++++++++---- .../src/interpreter/statements.rs | 9 ++ .../tests/builtins_class_metadata.rs | 23 +++++ docs/php/eval.md | 3 + src/types/checker/builtin_types/reflection.rs | 44 ++++++++++ tests/codegen/eval.rs | 32 +++++++ 6 files changed, 183 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 82a10f08e0..af19c817a8 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3142,26 +3142,63 @@ fn eval_reflection_parameter_for_selector( } } -/// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. -fn eval_reflection_method_new( +/// Handles eval-backed `ReflectionMethod::createFromMethodName()` static calls. +pub(in crate::interpreter) fn eval_reflection_method_create_from_method_name_result( + class_name: &str, + method_name: &str, evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let args = bind_evaluated_function_args( - &[String::from("class_name"), String::from("method_name")], - evaluated_args, - )?; - let class_name = eval_reflection_class_target_name(args[0], context, values)?; - if !eval_reflection_class_like_exists(&class_name, context) { - let method_name = eval_reflection_string_arg(args[1], values)?; + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("ReflectionMethod") + || !method_name.eq_ignore_ascii_case("createFromMethodName") + { + return Ok(None); + } + let args = bind_evaluated_function_args(&[String::from("method")], evaluated_args)?; + let target = eval_reflection_string_arg(args[0], values)?; + let (class_name, method_name) = eval_reflection_method_target_parts(&target)?; + eval_reflection_method_object_result_if_exists( + &class_name, + &method_name, + context, + values, + )? + .map(Some) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Splits PHP's `ClassName::methodName` reflection-method target string. +fn eval_reflection_method_target_parts(target: &str) -> Result<(String, String), EvalStatus> { + let Some((class_name, method_name)) = target.rsplit_once("::") else { + return Err(EvalStatus::RuntimeFatal); + }; + if class_name.is_empty() || method_name.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + Ok((class_name.to_string(), method_name.to_string())) +} + +/// Builds a `ReflectionMethod` object when the reflected method exists in eval or AOT metadata. +fn eval_reflection_method_object_result_if_exists( + class_name: &str, + requested_method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_exists(&reflected_name, context) { if let Some(method) = eval_reflection_aot_method_metadata_with_signature_if_exists( - &class_name, - &method_name, + &reflected_name, + requested_method_name, context, values, )? { - let method_name = method_name.to_ascii_lowercase(); + let method_name = requested_method_name.to_ascii_lowercase(); return eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_METHOD, &method_name, @@ -3173,15 +3210,14 @@ fn eval_reflection_method_new( } return Ok(None); } - let requested_method_name = eval_reflection_string_arg(args[1], values)?; let method_name = eval_reflection_member_name( EVAL_REFLECTION_OWNER_METHOD, - &class_name, - &requested_method_name, + &reflected_name, + requested_method_name, context, ) .ok_or(EvalStatus::RuntimeFatal)?; - let method = eval_reflection_method_metadata(&class_name, &method_name, context) + let method = eval_reflection_method_metadata(&reflected_name, &method_name, context) .ok_or(EvalStatus::RuntimeFatal)?; eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_METHOD, @@ -3193,6 +3229,26 @@ fn eval_reflection_method_new( .map(Some) } +/// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. +fn eval_reflection_method_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("method_name")], + evaluated_args, + )?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; + let requested_method_name = eval_reflection_string_arg(args[1], values)?; + eval_reflection_method_object_result_if_exists( + &class_name, + &requested_method_name, + context, + values, + ) +} + /// Builds an eval-backed `ReflectionProperty` object when the reflected property exists in eval. fn eval_reflection_property_new( evaluated_args: Vec, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 3c9109da4c..eb214e259d 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2920,6 +2920,15 @@ pub(in crate::interpreter) fn eval_static_method_call_result( )? { return Ok(result); } + if let Some(result) = eval_reflection_method_create_from_method_name_result( + &class_name, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if context.has_enum(&class_name) && eval_enum_static_builtin_name(method_name).is_some() { return eval_enum_builtin_static_method_result( &class_name, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 6d74679fe2..c6020644f9 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -1710,6 +1710,29 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod::createFromMethodName resolves eval method strings. +#[test] +fn execute_program_reflection_method_create_from_method_name() { + let program = parse_fragment( + br#"class EvalReflectCreateMethodTarget { + public function MiXeDCase() { return "ok"; } +} +$ref = ReflectionMethod::createFromMethodName("EvalReflectCreateMethodTarget::mixedcase"); +echo $ref->getDeclaringClass()->getName(); echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->invoke(new EvalReflectCreateMethodTarget()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectCreateMethodTarget:MiXeDCase:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval member and enum-case reflectors expose their declaring class. #[test] fn execute_program_reflects_eval_declaring_class_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index e6c41a7147..0a2a89bc89 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -229,6 +229,9 @@ enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and `ReflectionEnumBackedCase`, and `ReflectionNamedType` objects where PHP does. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. +`ReflectionMethod::createFromMethodName()` accepts `ClassName::method` strings +for eval-visible and generated/AOT methods and returns retained method +metadata equivalent to direct `ReflectionMethod` construction. `ReflectionClass::getShortName()`, `ReflectionClass::getNamespaceName()`, and `ReflectionClass::inNamespace()` derive namespace-aware parts from the resolved eval class-like name. diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index e02d536eb7..6166a49030 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -564,6 +564,39 @@ fn builtin_reflection_method_invoke_args_method() -> ClassMethod { } } +/// Returns a public static `ReflectionMethod::createFromMethodName()` method shell. +fn builtin_reflection_method_create_from_method_name_method() -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: "createFromMethodName".to_string(), + visibility: Visibility::Public, + is_static: true, + is_abstract: false, + is_final: false, + has_body: true, + params: vec![("method".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Named(Name::unqualified("ReflectionMethod"))), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::NewObject { + class_name: Name::unqualified("ReflectionMethod"), + args: vec![ + Expr::new(ExprKind::StringLiteral(String::new()), dummy_span), + Expr::new(ExprKind::StringLiteral(String::new()), dummy_span), + ], + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public `setAccessible(bool $accessible)` no-op method shell. fn builtin_reflection_set_accessible_method() -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -3924,6 +3957,7 @@ fn builtin_reflection_owner_class( methods.push(builtin_reflection_method_get_prototype_method()); methods.push(builtin_reflection_method_invoke_method()); methods.push(builtin_reflection_method_invoke_args_method()); + methods.push(builtin_reflection_method_create_from_method_name_method()); methods.push(builtin_reflection_set_accessible_method()); } if name == "ReflectionFunction" { @@ -4921,6 +4955,16 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invokeArgs")) { sig.return_type = PhpType::Mixed; } + if let Some(sig) = + class_info.methods.get_mut(&php_symbol_key("createFromMethodName")) + { + sig.params = vec![("method".to_string(), PhpType::Str)]; + sig.param_type_exprs = vec![Some(TypeExpr::Str)]; + sig.defaults = vec![None]; + sig.ref_params = vec![false]; + sig.declared_params = vec![true]; + sig.return_type = PhpType::Object("ReflectionMethod".to_string()); + } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getParameters")) { sig.return_type = PhpType::Array(Box::new(PhpType::Object( "ReflectionParameter".to_string(), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cdc97b0e3b..d8119dd71e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10518,6 +10518,38 @@ echo $aotOwn->getDeclaringClass()->getName();'); ); } +/// Verifies eval ReflectionMethod::createFromMethodName resolves eval and AOT method strings. +#[test] +fn test_eval_reflection_method_create_from_method_name() { + let out = compile_and_run_capture( + r#"getDeclaringClass()->getName() . ":"; +echo $ref->getName() . ":"; +echo $ref->invoke(new EvalReflectCreateMethodTarget()) . "|"; +$aot = ReflectionMethod::createFromMethodName("EvalAotReflectCreateMethodTarget::aotrun"); +echo $aot->getDeclaringClass()->getName() . ":"; +echo $aot->getName() . ":"; +echo $aot->invoke(new EvalAotReflectCreateMethodTarget());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalReflectCreateMethodTarget:MiXeDCase:ok|EvalAotReflectCreateMethodTarget:aotrun:aot" + ); +} + /// Verifies eval-declared final properties cannot be redeclared by subclasses. #[test] fn test_eval_declared_final_property_override_fails() { From 3ac9e4ce54e8756986b35a79a4efed3ba8a4c380 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 20:44:53 +0200 Subject: [PATCH 0660/1208] Support eval reflection method string constructor --- .../src/interpreter/reflection.rs | 30 ++++++++ .../tests/builtins_class_metadata.rs | 23 ++++++ docs/php/classes.md | 2 +- docs/php/eval.md | 2 + src/ir_lower/expr/mod.rs | 55 ++++++++++++++ src/types/checker/builtin_types/reflection.rs | 7 +- .../checker/inference/objects/constructors.rs | 74 +++++++++++++++++++ tests/codegen/eval.rs | 32 ++++++++ 8 files changed, 223 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index af19c817a8..45abc2008d 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3181,6 +3181,25 @@ fn eval_reflection_method_target_parts(target: &str) -> Result<(String, String), Ok((class_name.to_string(), method_name.to_string())) } +/// Extracts the deprecated one-argument `ReflectionMethod("Class::method")` target. +fn eval_reflection_method_single_target_arg( + evaluated_args: Vec, +) -> Result { + let mut args = evaluated_args.into_iter(); + let Some(arg) = args.next() else { + return Err(EvalStatus::RuntimeFatal); + }; + if args.next().is_some() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(name) = arg.name.as_deref() { + if !matches!(name, "class_name" | "objectOrMethod") { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(arg.value) +} + /// Builds a `ReflectionMethod` object when the reflected method exists in eval or AOT metadata. fn eval_reflection_method_object_result_if_exists( class_name: &str, @@ -3235,6 +3254,17 @@ fn eval_reflection_method_new( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { + if evaluated_args.len() == 1 { + let target = eval_reflection_method_single_target_arg(evaluated_args)?; + let target = eval_reflection_string_arg(target, values)?; + let (class_name, method_name) = eval_reflection_method_target_parts(&target)?; + return eval_reflection_method_object_result_if_exists( + &class_name, + &method_name, + context, + values, + ); + } let args = bind_evaluated_function_args( &[String::from("class_name"), String::from("method_name")], evaluated_args, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index c6020644f9..2a7dc0bebe 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -1733,6 +1733,29 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod accepts PHP's deprecated one-string method target. +#[test] +fn execute_program_reflection_method_accepts_single_method_string() { + let program = parse_fragment( + br#"class EvalReflectCtorMethodTarget { + public function MiXeDCase() { return "ok"; } +} +$ref = new ReflectionMethod(objectOrMethod: "EvalReflectCtorMethodTarget::mixedcase"); +echo $ref->getDeclaringClass()->getName(); echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->invoke(new EvalReflectCtorMethodTarget()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectCtorMethodTarget:MiXeDCase:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval member and enum-case reflectors expose their declaring class. #[test] fn execute_program_reflects_eval_declaring_class_metadata() { diff --git a/docs/php/classes.md b/docs/php/classes.md index 5b78b4a8a5..32976e84b1 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1208,7 +1208,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionFunction::isVariadic()` | `new ReflectionFunction($function_name)` | Return whether the reflected function declares a variadic parameter | | `ReflectionFunction::getClosureUsedVariables()` | `new ReflectionFunction($function_name)` | Return an empty array for supported non-closure function reflectors | | `ReflectionFunction::invoke()` / `invokeArgs()` | `new ReflectionFunction($function_name)` | Invoke eval-declared reflected functions with forwarded named/default/variadic arguments; inline/tracked generated/AOT functions with declared or inferred/untyped parameter contracts and supported callable builtins are also lowered | -| `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` | Return the reflected method name | +| `ReflectionMethod::getName()` | `new ReflectionMethod($class_name, $method_name)` or deprecated `new ReflectionMethod("ClassName::method")` | Return the reflected method name | | `ReflectionMethod::getShortName()` / `getNamespaceName()` / `inNamespace()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return PHP method-name metadata; methods report an empty namespace and `false` for `inNamespace()` | | `ReflectionMethod::isInternal()` / `isUserDefined()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return origin predicates for supported reflected methods | | `ReflectionMethod::isClosure()` / `isDeprecated()` / `returnsReference()` / `isGenerator()` | `new ReflectionMethod($class_name, $method_name)` or `ReflectionClass::getMethod()` / `getMethods()` / `getConstructor()` | Return retained method predicates; AOT reflection reports `false` for closures and return-by-reference, uses `#[Deprecated]` metadata, and reports generator methods from lowered generator flags | diff --git a/docs/php/eval.md b/docs/php/eval.md index 0a2a89bc89..e7345528eb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -229,6 +229,8 @@ enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and `ReflectionEnumBackedCase`, and `ReflectionNamedType` objects where PHP does. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. +It also accepts PHP's deprecated one-argument `ClassName::method` string form +for eval-visible and generated/AOT methods. `ReflectionMethod::createFromMethodName()` accepts `ClassName::method` strings for eval-visible and generated/AOT methods and returns retained method metadata equivalent to direct `ReflectionMethod` construction. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 6bad67615a..7b2f62d366 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9236,6 +9236,20 @@ fn lower_new_object( ); } } + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) == "reflectionmethod" { + if let Some(operands) = lower_reflection_method_constructor_operands(ctx, args) { + let php_type = PhpType::Object(class_name.as_str().to_string()); + let data = ctx.intern_class_name(class_name.as_str()); + return ctx.emit_value( + Op::ObjectNew, + operands, + Some(Immediate::Data(data)), + php_type, + Op::ObjectNew.default_effects(), + Some(expr.span), + ); + } + } let sig = constructor_signature(ctx, class_name).cloned(); let operands = lower_args_with_signature(ctx, sig.as_ref(), args); let php_type = PhpType::Object(class_name.as_str().to_string()); @@ -9279,6 +9293,18 @@ fn lower_reflection_class_constructor_operands( Some(vec![value.value]) } +/// Lowers direct `ReflectionMethod` constructor operands to literal class and method names. +fn lower_reflection_method_constructor_operands( + ctx: &mut LoweringContext<'_, '_>, + args: &[Expr], +) -> Option> { + let (class_arg, method_arg) = reflection_method_constructor_regular_args(ctx, args)?; + Some(vec![ + lower_expr(ctx, &class_arg).value, + lower_expr(ctx, &method_arg).value, + ]) +} + /// Lowers PHP `clone $object` to a shallow object-copy opcode and optional `__clone()` hook. fn lower_clone(ctx: &mut LoweringContext<'_, '_>, inner: &Expr, expr: &Expr) -> LoweredValue { let object = lower_expr(ctx, inner); @@ -11822,6 +11848,9 @@ fn reflection_method_constructor_regular_args( if args.iter().any(is_spread_arg) { return None; } + if args.len() == 1 { + return reflection_method_constructor_single_target(ctx, &args[0]); + } if !crate::types::call_args::has_named_args(&args) { return match args.as_slice() { [class_arg, method_arg] => Some((class_arg.clone(), method_arg.clone())), @@ -11854,6 +11883,32 @@ fn reflection_method_constructor_regular_args( Some((class_arg, method_arg)) } +/// Splits deprecated `ReflectionMethod("Class::method")` constructor syntax. +fn reflection_method_constructor_single_target( + ctx: &LoweringContext<'_, '_>, + arg: &Expr, +) -> Option<(Expr, Expr)> { + let arg = match &arg.kind { + ExprKind::NamedArg { name, value } if name == "class_name" => value.as_ref(), + ExprKind::NamedArg { name, value } if name == "objectOrMethod" => value.as_ref(), + ExprKind::NamedArg { .. } => return None, + _ => arg, + }; + let ExprKind::StringLiteral(target) = &arg.kind else { + return None; + }; + let (raw_class_name, raw_method_name) = target.rsplit_once("::")?; + if raw_class_name.is_empty() || raw_method_name.is_empty() { + return None; + } + let class_name = resolve_known_class_name(ctx, raw_class_name)?; + let method_name = resolve_known_class_method_name(ctx, &class_name, raw_method_name)?; + Some(( + Expr::new(ExprKind::StringLiteral(class_name), arg.span), + Expr::new(ExprKind::StringLiteral(method_name), arg.span), + )) +} + /// Lowers `ReflectionClass::getStaticProperties()` to a live static-property map. fn lower_reflection_class_get_static_properties( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 6166a49030..0b40c4b9fe 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -134,7 +134,12 @@ pub(crate) fn inject_builtin_reflection( true, vec![ ("class_name", Some(TypeExpr::Str), None, false), - ("method_name", Some(TypeExpr::Str), None, false), + ( + "method_name", + Some(TypeExpr::Nullable(Box::new(TypeExpr::Str))), + null_expr(), + false, + ), ], ), ); diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 2e8bb68acc..51e4e2cc28 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -207,6 +207,9 @@ impl Checker { expr: &Expr, env: &TypeEnv, ) -> Result<(), CompileError> { + if class_name == "ReflectionMethod" && args.len() == 1 { + return self.validate_reflection_method_constructor_from_method_name(args, expr, env); + } let sig = self .classes .get(class_name) @@ -305,6 +308,77 @@ impl Checker { } } + /// Validates deprecated `new ReflectionMethod("Class::method")` calls. + fn validate_reflection_method_constructor_from_method_name( + &mut self, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result<(), CompileError> { + let arg = match &args[0].kind { + ExprKind::NamedArg { name, value } if name == "class_name" => value.as_ref(), + ExprKind::NamedArg { name, value } if name == "objectOrMethod" => value.as_ref(), + ExprKind::NamedArg { name, .. } => { + return Err(CompileError::new( + args[0].span, + &format!( + "Constructor 'ReflectionMethod::__construct' has no parameter ${}", + name + ), + )); + } + _ => &args[0], + }; + let (class_name, method_name) = self.reflection_method_name_literal_arg(arg, env)?; + self.validate_reflection_method_attrs(&class_name, &method_name, expr) + } + + /// Extracts a literal `ClassName::methodName` target for `ReflectionMethod`. + fn reflection_method_name_literal_arg( + &mut self, + arg: &Expr, + env: &TypeEnv, + ) -> Result<(String, String), CompileError> { + let arg_ty = self.infer_type(arg, env)?; + if !matches!(arg_ty.codegen_repr(), PhpType::Str) { + return Err(CompileError::new( + arg.span, + "ReflectionMethod::__construct() first argument must be a string method name", + )); + } + let ExprKind::StringLiteral(target) = &arg.kind else { + return Err(CompileError::new( + arg.span, + "ReflectionMethod::__construct() requires a string literal method name (dynamic lookup is not yet supported)", + )); + }; + let Some((raw_class_name, method_name)) = target.rsplit_once("::") else { + return Err(CompileError::new( + arg.span, + "ReflectionMethod::__construct() one-argument form requires ClassName::method", + )); + }; + if raw_class_name.is_empty() || method_name.is_empty() { + return Err(CompileError::new( + arg.span, + "ReflectionMethod::__construct() one-argument form requires ClassName::method", + )); + } + let class_name = self + .resolve_reflection_class_name(raw_class_name) + .map(str::to_string) + .ok_or_else(|| { + CompileError::new( + arg.span, + &format!( + "ReflectionMethod::__construct(): undefined class '{}'", + raw_class_name + ), + ) + })?; + Ok((class_name, method_name.to_string())) + } + /// Validates `new ReflectionFunction(function)` for supported static function metadata. fn validate_reflection_function_constructor( &mut self, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d8119dd71e..48d91a70f7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10550,6 +10550,38 @@ echo $aot->invoke(new EvalAotReflectCreateMethodTarget());'); ); } +/// Verifies eval ReflectionMethod accepts PHP's deprecated one-string constructor target. +#[test] +fn test_eval_reflection_method_accepts_single_method_string() { + let out = compile_and_run_capture( + r#"getDeclaringClass()->getName() . ":"; +echo $aot->getName() . ":"; +echo $aot->invoke(new EvalAotReflectCtorMethodTarget()) . "|"; +eval('class EvalReflectCtorMethodTarget { + public function MiXeDCase() { return "ok"; } +} +$ref = new ReflectionMethod(objectOrMethod: "EvalReflectCtorMethodTarget::mixedcase"); +echo $ref->getDeclaringClass()->getName() . ":"; +echo $ref->getName() . ":"; +echo $ref->invoke(new EvalReflectCtorMethodTarget());'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotReflectCtorMethodTarget:aotrun:aot|EvalReflectCtorMethodTarget:MiXeDCase:ok" + ); +} + /// Verifies eval-declared final properties cannot be redeclared by subclasses. #[test] fn test_eval_declared_final_property_override_fails() { From faa2eae0df75061f72b461d19ca3b76a40cc2451 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 21:06:10 +0200 Subject: [PATCH 0661/1208] Support eval ReflectionObject dynamic properties --- .../src/interpreter/reflection.rs | 154 ++++++++++++++++++ .../src/interpreter/statements.rs | 2 + .../tests/builtins_class_metadata.rs | 33 ++++ docs/php/eval.md | 4 +- src/types/checker/builtin_types/reflection.rs | 6 + tests/codegen/eval.rs | 32 ++++ 6 files changed, 230 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 45abc2008d..b5842b0f64 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2300,6 +2300,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_res /// Handles eval-backed `ReflectionClass::getMethods()` and `getProperties()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_members_result( + object: RuntimeCellHandle, identity: u64, method_name: &str, evaluated_args: Vec, @@ -2334,6 +2335,17 @@ pub(in crate::interpreter) fn eval_reflection_class_get_members_result( context, values, ) + .and_then(|result| { + eval_reflection_object_dynamic_property_array_result( + object, + owner_kind, + &reflected_name, + filter, + result, + context, + values, + ) + }) .map(Some); } let names = eval_reflection_aot_member_names(owner_kind, &reflected_name, values)?; @@ -2345,11 +2357,23 @@ pub(in crate::interpreter) fn eval_reflection_class_get_members_result( context, values, ) + .and_then(|result| { + eval_reflection_object_dynamic_property_array_result( + object, + owner_kind, + &reflected_name, + filter, + result, + context, + values, + ) + }) .map(Some) } /// Handles eval-backed `ReflectionClass::getMethod()` and `getProperty()` calls. pub(in crate::interpreter) fn eval_reflection_class_get_member_result( + object: RuntimeCellHandle, identity: u64, method_name: &str, evaluated_args: Vec, @@ -2413,6 +2437,29 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( .map(Some); } } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + if let Some(dynamic_object) = + eval_reflection_object_reflected_object(object, context, values)? + { + let exists = eval_reflection_object_dynamic_property_exists( + dynamic_object, + &requested_name, + values, + ); + values.release(dynamic_object)?; + if exists? { + let member = eval_reflection_dynamic_property_metadata(&reflected_name); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } + } + } let message_name = eval_reflection_class_like_attributes(&reflected_name, context) .map(|metadata| metadata.resolved_name) .unwrap_or_else(|| reflected_name.clone()); @@ -2622,6 +2669,9 @@ fn eval_reflection_object_new( else { return Err(EvalStatus::RuntimeFatal); }; + eval_reflection_with_declaring_class_scope("ReflectionObject", context, |_| { + values.property_set(object, "__object", args[0]) + })?; Ok(Some(object)) } @@ -3394,6 +3444,110 @@ fn eval_reflection_object_dynamic_property_exists( Ok(false) } +/// Returns the object captured by a `ReflectionObject` instance, when present. +fn eval_reflection_object_reflected_object( + reflection_object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_object_has_class(reflection_object, "ReflectionObject", values)? { + return Ok(None); + } + let object = eval_reflection_with_declaring_class_scope("ReflectionObject", context, |_| { + values.property_get(reflection_object, "__object") + })?; + if values.type_tag(object)? == EVAL_TAG_OBJECT { + Ok(Some(object)) + } else { + Ok(None) + } +} + +/// Appends dynamic public properties to `ReflectionObject::getProperties()` results. +fn eval_reflection_object_dynamic_property_array_result( + reflection_object: RuntimeCellHandle, + owner_kind: u64, + reflected_name: &str, + filter: Option, + mut result: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if owner_kind != EVAL_REFLECTION_OWNER_PROPERTY { + return Ok(result); + } + let Some(object) = eval_reflection_object_reflected_object(reflection_object, context, values)? + else { + return Ok(result); + }; + let property_names = + eval_reflection_object_dynamic_property_names(object, reflected_name, context, values); + values.release(object)?; + let property_names = property_names?; + for name in property_names { + let member = eval_reflection_dynamic_property_metadata(reflected_name); + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } + let member_object = eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &name, + &member, + context, + values, + )?; + let next_index = values.array_len(result)? as i64; + let key = values.int(next_index)?; + result = values.array_set(result, key, member_object)?; + } + Ok(result) +} + +/// Enumerates public dynamic property names on the object behind `ReflectionObject`. +fn eval_reflection_object_dynamic_property_names( + object: RuntimeCellHandle, + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let property_name = + String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + if !eval_reflection_dynamic_property_name_is_visible( + reflected_name, + &property_name, + context, + ) { + continue; + } + if !names.iter().any(|name| name == &property_name) { + names.push(property_name); + } + } + Ok(names) +} + +/// Returns true when an object-storage name represents a public dynamic property. +fn eval_reflection_dynamic_property_name_is_visible( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> bool { + !property_name.contains('\0') + && eval_reflection_member_name( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_name, + context, + ) + .is_none() +} + /// Builds PHP reflection metadata for a public dynamic object property. fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflectionMemberMetadata { EvalReflectionMemberMetadata { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index eb214e259d..113864c5a0 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3842,6 +3842,7 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( return Ok(result); } if let Some(result) = eval_reflection_class_get_members_result( + object, identity, method_name, evaluated_args.clone(), @@ -3851,6 +3852,7 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( return Ok(result); } if let Some(result) = eval_reflection_class_get_member_result( + object, identity, method_name, evaluated_args.clone(), diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 2a7dc0bebe..0d55b29b32 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3267,6 +3267,39 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionObject lists dynamic public properties from the reflected instance. +#[test] +fn execute_program_reflection_object_lists_dynamic_properties() { + let program = parse_fragment( + br#"class EvalReflectObjectDynamicTarget { + public $declared = "declared"; +} +$object = new EvalReflectObjectDynamicTarget(); +$object->dynamic = "value"; +$ref = new ReflectionObject($object); +$properties = $ref->getProperties(); +foreach ($properties as $property) { + echo $property->getName(); echo ":"; + echo $property->isDynamic() ? "D" : "d"; echo "|"; +} +echo ":"; +$dynamic = $ref->getProperty("dynamic"); +echo $dynamic->isDynamic() ? "D" : "d"; echo ":"; +echo $dynamic->getValue($object); echo ":"; +echo count($ref->getProperties(ReflectionProperty::IS_PUBLIC)); echo ":"; +echo count($ref->getProperties(ReflectionProperty::IS_STATIC)); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "declared:d|dynamic:D|:D:value:2:0"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionObject rejects non-object constructor arguments. #[test] fn execute_program_reflection_object_rejects_non_objects() { diff --git a/docs/php/eval.md b/docs/php/eval.md index e7345528eb..6471d80a45 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -222,7 +222,9 @@ same class metadata through a `ReflectionObject` instance. Its inherited `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()` helpers use the object's runtime class id. Straight-line `ReflectionObject` receivers built from statically typed objects also normalize named constructor -arguments through the reflected constructor signature. +arguments through the reflected constructor signature. Its inherited +`getProperty()` and `getProperties()` include public dynamic properties from the +reflected instance and mark those `ReflectionProperty` objects as dynamic. `ReflectionEnum` construction accepts enum-name strings for eval-declared enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and `getBackingType()` for eval enum metadata, returning `ReflectionEnumUnitCase`, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 0b40c4b9fe..3865fe0f88 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1911,6 +1911,12 @@ fn builtin_reflection_object_class() -> FlattenedClass { let mut class = builtin_reflection_class(); class.name = "ReflectionObject".to_string(); class.extends = Some("ReflectionClass".to_string()); + class.properties.push(builtin_property( + "__object", + Visibility::Private, + Some(mixed_type()), + null_expr(), + )); if let Some(constructor) = class .methods .iter_mut() diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 48d91a70f7..79bfb2a5b7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11946,6 +11946,38 @@ echo str_replace("\n", "|", (new ReflectionObject(new EvalReflectClassStringTarg assert_eq!(out.stdout, format!("{expected}::{expected}")); } +/// Verifies eval ReflectionObject lists dynamic public properties from its reflected instance. +#[test] +fn test_eval_reflection_object_lists_dynamic_properties() { + let out = compile_and_run_capture( + r#"dynamic = "value"; +$ref = new ReflectionObject($object); +$properties = $ref->getProperties(); +foreach ($properties as $property) { + echo $property->getName() . ":"; + echo ($property->isDynamic() ? "D" : "d") . "|"; +} +echo ":"; +$dynamic = $ref->getProperty("dynamic"); +echo ($dynamic->isDynamic() ? "D" : "d") . ":"; +echo $dynamic->getValue($object) . ":"; +echo count($ref->getProperties(ReflectionProperty::IS_PUBLIC)) . ":"; +echo count($ref->getProperties(ReflectionProperty::IS_STATIC));'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "declared:d|dynamic:D|:D:value:2:0"); +} + /// Verifies eval Reflection origin metadata APIs are present on supported owners. #[test] fn test_eval_reflection_origin_metadata_defaults() { From fdd43412f290c06bf30574c46c6b2fdb75a02d08 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 21:13:03 +0200 Subject: [PATCH 0662/1208] Support eval ReflectionObject dynamic hasProperty --- .../src/interpreter/reflection.rs | 18 ++++++++++++++++-- .../src/interpreter/statements.rs | 1 + .../tests/builtins_class_metadata.rs | 8 ++++++-- docs/php/classes.md | 2 +- docs/php/eval.md | 5 +++-- tests/codegen/eval.rs | 8 ++++++-- 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index b5842b0f64..7cc97c2044 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -634,8 +634,9 @@ pub(in crate::interpreter) fn eval_reflection_class_has_method_result( values.bool_value(exists).map(Some) } -/// Handles eval-backed `ReflectionClass::hasProperty()` calls. +/// Handles eval-backed `ReflectionClass::hasProperty()` and inherited `ReflectionObject` calls. pub(in crate::interpreter) fn eval_reflection_class_has_property_result( + object: RuntimeCellHandle, identity: u64, method_name: &str, evaluated_args: Vec, @@ -653,7 +654,7 @@ pub(in crate::interpreter) fn eval_reflection_class_has_property_result( }; let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; let property_name = eval_reflection_string_arg(args[0], values)?; - let exists = + let mut exists = if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { metadata .property_names @@ -668,6 +669,19 @@ pub(in crate::interpreter) fn eval_reflection_class_has_property_result( )? .is_some() }; + if !exists { + if let Some(dynamic_object) = + eval_reflection_object_reflected_object(object, context, values)? + { + let dynamic_exists = eval_reflection_object_dynamic_property_exists( + dynamic_object, + &property_name, + values, + ); + values.release(dynamic_object)?; + exists = dynamic_exists?; + } + } values.bool_value(exists).map(Some) } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 113864c5a0..2233c0c569 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3589,6 +3589,7 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( return Ok(result); } if let Some(result) = eval_reflection_class_has_property_result( + object, identity, method_name, evaluated_args.clone(), diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 0d55b29b32..b96612ad1d 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3287,7 +3287,11 @@ $dynamic = $ref->getProperty("dynamic"); echo $dynamic->isDynamic() ? "D" : "d"; echo ":"; echo $dynamic->getValue($object); echo ":"; echo count($ref->getProperties(ReflectionProperty::IS_PUBLIC)); echo ":"; -echo count($ref->getProperties(ReflectionProperty::IS_STATIC)); +echo count($ref->getProperties(ReflectionProperty::IS_STATIC)); echo ":"; +echo $ref->hasProperty("dynamic") ? "H" : "h"; +echo $ref->hasProperty("declared") ? "D" : "d"; +echo $ref->hasProperty("missing") ? "M" : "m"; +echo (new ReflectionClass($object))->hasProperty("dynamic") ? "C" : "c"; return true;"#, ) .expect("parse eval fragment"); @@ -3296,7 +3300,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "declared:d|dynamic:D|:D:value:2:0"); + assert_eq!(values.output, "declared:d|dynamic:D|:D:value:2:0:HDmc"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/classes.md b/docs/php/classes.md index 32976e84b1..2f6ef657de 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1147,7 +1147,7 @@ echo ($instance instanceof Route) ? "yes" : "no"; | Reflection method | Supported constructor | Description | |---|---|---| -| `ReflectionObject::*` inherited class metadata and construction methods | `new ReflectionObject($object)` | Use the same reflected class metadata as `ReflectionClass`; construction helpers use the object's runtime class id, and straight-line receivers built from statically typed objects normalize named constructor arguments | +| `ReflectionObject::*` inherited class metadata and construction methods | `new ReflectionObject($object)` | Use the same reflected class metadata as `ReflectionClass`; construction helpers use the object's runtime class id, straight-line receivers built from statically typed objects normalize named constructor arguments, and `hasProperty()` / `getProperty()` / `getProperties()` include public dynamic properties from the reflected instance | | `ReflectionClass::getName()` | `new ReflectionClass($class_name)` | Return the resolved class-like name | | `ReflectionClass::getShortName()` | `new ReflectionClass($class_name)` | Return the class-like name after the final namespace separator | | `ReflectionClass::getNamespaceName()` | `new ReflectionClass($class_name)` | Return the namespace portion of the reflected class-like name | diff --git a/docs/php/eval.md b/docs/php/eval.md index 6471d80a45..96f276238b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -223,8 +223,9 @@ same class metadata through a `ReflectionObject` instance. Its inherited helpers use the object's runtime class id. Straight-line `ReflectionObject` receivers built from statically typed objects also normalize named constructor arguments through the reflected constructor signature. Its inherited -`getProperty()` and `getProperties()` include public dynamic properties from the -reflected instance and mark those `ReflectionProperty` objects as dynamic. +`hasProperty()`, `getProperty()`, and `getProperties()` include public dynamic +properties from the reflected instance and mark those `ReflectionProperty` +objects as dynamic. `ReflectionEnum` construction accepts enum-name strings for eval-declared enums. It exposes `hasCase()`, `getCase()`, `getCases()`, `isBacked()`, and `getBackingType()` for eval enum metadata, returning `ReflectionEnumUnitCase`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 79bfb2a5b7..fa4bae4d99 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11967,7 +11967,11 @@ $dynamic = $ref->getProperty("dynamic"); echo ($dynamic->isDynamic() ? "D" : "d") . ":"; echo $dynamic->getValue($object) . ":"; echo count($ref->getProperties(ReflectionProperty::IS_PUBLIC)) . ":"; -echo count($ref->getProperties(ReflectionProperty::IS_STATIC));'); +echo count($ref->getProperties(ReflectionProperty::IS_STATIC)) . ":"; +echo $ref->hasProperty("dynamic") ? "H" : "h"; +echo $ref->hasProperty("declared") ? "D" : "d"; +echo $ref->hasProperty("missing") ? "M" : "m"; +echo (new ReflectionClass($object))->hasProperty("dynamic") ? "C" : "c";'); "#, ); assert!( @@ -11975,7 +11979,7 @@ echo count($ref->getProperties(ReflectionProperty::IS_STATIC));'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "declared:d|dynamic:D|:D:value:2:0"); + assert_eq!(out.stdout, "declared:d|dynamic:D|:D:value:2:0:HDmc"); } /// Verifies eval Reflection origin metadata APIs are present on supported owners. From 16e1e17fe5d098e439927c3ccee68208980f7571 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 21:24:26 +0200 Subject: [PATCH 0663/1208] Reject eval readonly class dynamic properties --- .../src/interpreter/statements.rs | 4 ++ .../src/interpreter/tests/classes.rs | 44 +++++++++++++++++++ docs/php/eval.md | 6 ++- tests/codegen/eval.rs | 33 ++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 2233c0c569..b8b359f1fe 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2154,6 +2154,7 @@ pub(in crate::interpreter) fn eval_property_set_result( if context.has_enum(&object_class_name) { return Err(EvalStatus::RuntimeFatal); } + let class_is_readonly = class.is_readonly_class(); let mut storage_property_name = property_name.to_string(); let mut declared_property_found = false; if let Some((declaring_class, property)) = @@ -2221,6 +2222,9 @@ pub(in crate::interpreter) fn eval_property_set_result( { return Ok(()); } + if !declared_property_found && class_is_readonly { + return Err(EvalStatus::RuntimeFatal); + } if let Some(target) = context .dynamic_property_alias(identity, &storage_property_name) .cloned() diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 1929b32a70..2b6d99ea77 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -358,6 +358,50 @@ $box->replace(12);"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies readonly classes reject dynamic property creation when no magic setter handles it. +#[test] +fn execute_program_rejects_readonly_class_dynamic_property_creation() { + let program = parse_fragment( + br#"readonly class EvalReadonlyDynamicFailBox { + public int $id; + public function __construct($id) { $this->id = $id; } +} +$box = new EvalReadonlyDynamicFailBox(11); +$box->dynamic = 12;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly class dynamic property creation should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly classes may still handle missing property writes through `__set()`. +#[test] +fn execute_program_allows_readonly_class_magic_set_for_missing_properties() { + let program = parse_fragment( + br#"readonly class EvalReadonlyMagicSetBox { + public function __set($name, $value) { + echo $name; echo ":"; echo $value; + } +} +$box = new EvalReadonlyMagicSetBox(); +$box->dynamic = 12; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "dynamic:12"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies readonly class static properties remain mutable. #[test] fn execute_program_allows_readonly_class_static_property_mutation() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 96f276238b..53c3c3682d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -543,8 +543,10 @@ route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the constructor of the declaring class and later writes fail as eval runtime fatals. A `readonly class` makes instance properties readonly implicitly while leaving -static properties mutable. `self::`, `parent::`, and late-bound `static::` work -for supported static members, class constants, and class-name literals. +static properties mutable. Missing-property writes can still dispatch through +`__set()`, but readonly classes reject actual dynamic property creation. +`self::`, `parent::`, and late-bound `static::` work for supported static +members, class constants, and class-name literals. Eval object construction can allocate eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime class metadata. Missing class names diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fa4bae4d99..14b80e1ba8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7204,6 +7204,39 @@ $box->replace(8);'); "stderr did not contain eval runtime fatal diagnostic: {err}" ); + let dynamic_err = compile_and_run_expect_failure( + r#"id = $id; } +} +$box = new EvalReadonlyClassDynamicFailBox(7); +$box->dynamic = 8;'); +"#, + ); + assert!( + dynamic_err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {dynamic_err}" + ); + + let magic = compile_and_run_capture( + r#"dynamic = 8;'); +"#, + ); + assert!( + magic.success, + "program failed: stdout={:?} stderr={}", + magic.stdout, magic.stderr + ); + assert_eq!(magic.stdout, "dynamic:8"); + let parent_err = compile_and_run_expect_failure( r#" Date: Thu, 25 Jun 2026 21:31:30 +0200 Subject: [PATCH 0664/1208] Reject eval readonly dynamic attribute --- .../src/interpreter/statements.rs | 11 ++++++ .../src/interpreter/tests/classes.rs | 34 +++++++++++++++++++ docs/php/eval.md | 2 ++ tests/codegen/eval.rs | 10 ++++++ 4 files changed, 57 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b8b359f1fe..48ff76d819 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1249,6 +1249,9 @@ fn validate_eval_class_modifiers( if class.is_abstract() && class.is_final() { return Err(EvalStatus::RuntimeFatal); } + if class.is_readonly_class() && eval_class_has_allow_dynamic_properties_attribute(class) { + return Err(EvalStatus::RuntimeFatal); + } validate_eval_declared_constants(class.constants())?; for constant in class.constants() { validate_constant_parent_redeclaration(class, constant, context)?; @@ -1276,6 +1279,14 @@ fn validate_eval_class_modifiers( Ok(()) } +/// Returns whether a class carries PHP's global `#[AllowDynamicProperties]` attribute. +fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool { + class + .attributes() + .iter() + .any(|attribute| attribute.name().eq_ignore_ascii_case("AllowDynamicProperties")) +} + /// Validates PHP magic-method contracts for one eval class-like method list. fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalStatus> { for method in methods { diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 2b6d99ea77..e42761d3a1 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -402,6 +402,40 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies readonly classes reject PHP's global dynamic-property marker attribute. +#[test] +fn execute_program_rejects_allow_dynamic_properties_on_readonly_class() { + let program = + parse_fragment(br#"#[\AllowDynamicProperties] readonly class EvalReadonlyAllowDynamic {}"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("AllowDynamicProperties cannot apply to readonly classes"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies namespaced non-builtin attributes do not trigger the readonly-class marker rule. +#[test] +fn execute_program_allows_namespaced_allow_dynamic_properties_on_readonly_class() { + let program = parse_fragment( + br#"namespace EvalReadonlyAttrNs; +#[AllowDynamicProperties] readonly class Box {} +echo class_attribute_names("EvalReadonlyAttrNs\Box")[0]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReadonlyAttrNs\\AllowDynamicProperties"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies readonly class static properties remain mutable. #[test] fn execute_program_allows_readonly_class_static_property_mutation() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 53c3c3682d..8904f7e5ff 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -545,6 +545,8 @@ constructor of the declaring class and later writes fail as eval runtime fatals. A `readonly class` makes instance properties readonly implicitly while leaving static properties mutable. Missing-property writes can still dispatch through `__set()`, but readonly classes reject actual dynamic property creation. +PHP's global `#[AllowDynamicProperties]` marker is rejected on eval-declared +readonly classes. `self::`, `parent::`, and late-bound `static::` work for supported static members, class constants, and class-name literals. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 14b80e1ba8..5335d13b1a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7219,6 +7219,16 @@ $box->dynamic = 8;'); "stderr did not contain eval runtime fatal diagnostic: {dynamic_err}" ); + let attribute_err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 21:45:07 +0200 Subject: [PATCH 0665/1208] Validate eval method parameter variance --- .../src/interpreter/return_type_compat.rs | 91 +++++++++++- .../src/interpreter/statements.rs | 20 +++ .../src/interpreter/tests/expressions.rs | 136 ++++++++++++++++++ docs/php/eval.md | 7 +- tests/codegen/eval.rs | 129 +++++++++++++++++ 5 files changed, 373 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/return_type_compat.rs b/crates/elephc-magician/src/interpreter/return_type_compat.rs index 012fb4ed7d..ba3ffc21da 100644 --- a/crates/elephc-magician/src/interpreter/return_type_compat.rs +++ b/crates/elephc-magician/src/interpreter/return_type_compat.rs @@ -1,16 +1,52 @@ //! Purpose: -//! Validates covariant return type compatibility for eval-declared methods. -//! This keeps class/interface signature checks out of the statement dispatcher. +//! Validates method type compatibility for eval-declared method signatures. +//! This keeps class/interface parameter contravariance and return covariance +//! checks out of the statement dispatcher. //! //! Called from: //! - `crate::interpreter::statements` while registering eval class-like declarations. //! //! Key details: //! - `self`, `parent`, and `static` are resolved relative to the declaration owner. +//! - Parameter checks reverse the return-type relation because PHP parameters are contravariant. //! - Pending class declarations are checked before they are registered in the eval context. use super::*; +/// Returns whether an implementation can accept every required declared parameter type. +pub(super) fn method_parameter_type_signature_accepts( + implementation_types: &[Option], + implementation_variadics: &[bool], + implementation_owner: &str, + required_types: &[Option], + required_variadics: &[bool], + required_owner: &str, + required_param_count: usize, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + (0..required_param_count).all(|position| { + let implementation_type = method_signature_effective_parameter_type( + implementation_types, + implementation_variadics, + position, + ); + let required_type = method_signature_effective_parameter_type( + required_types, + required_variadics, + position, + ); + eval_parameter_type_accepts( + implementation_type, + implementation_owner, + required_type, + required_owner, + pending_class, + context, + ) + }) +} + /// Returns whether a method preserves the required declared return type contract. pub(super) fn method_return_type_signature_accepts( implementation_type: Option<&EvalParameterType>, @@ -35,6 +71,45 @@ pub(super) fn method_return_type_signature_accepts( }) } +/// Returns the parameter type that applies at one source-order argument position. +fn method_signature_effective_parameter_type<'a>( + parameter_types: &'a [Option], + variadics: &[bool], + position: usize, +) -> Option<&'a EvalParameterType> { + if let Some(variadic_index) = variadics.iter().position(|is_variadic| *is_variadic) { + if position >= variadic_index { + return parameter_types + .get(variadic_index) + .and_then(Option::as_ref); + } + } + parameter_types.get(position).and_then(Option::as_ref) +} + +/// Returns whether an implementation parameter type is a PHP contravariant supertype. +fn eval_parameter_type_accepts( + implementation_type: Option<&EvalParameterType>, + implementation_owner: &str, + required_type: Option<&EvalParameterType>, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + match (implementation_type, required_type) { + (None, _) => true, + (Some(implementation_type), None) => eval_type_is_mixed(implementation_type), + (Some(implementation_type), Some(required_type)) => eval_return_type_accepts( + implementation_type, + implementation_owner, + required_type, + required_owner, + pending_class, + context, + ), + } +} + /// Returns whether `actual_type` is a covariant subtype of `expected_type`. fn eval_return_type_accepts( expected_type: &EvalParameterType, @@ -90,11 +165,13 @@ fn eval_return_type_accepts( /// Returns whether a return type can produce PHP null, including standalone `mixed`. fn eval_return_type_allows_null(return_type: &EvalParameterType) -> bool { return_type.allows_null() - || (!return_type.is_intersection() - && return_type - .variants() - .iter() - .any(|variant| matches!(variant, EvalParameterTypeVariant::Mixed))) + || eval_type_is_mixed(return_type) +} + +/// Returns whether one retained type is PHP's standalone `mixed` atom. +fn eval_type_is_mixed(parameter_type: &EvalParameterType) -> bool { + !parameter_type.is_intersection() + && matches!(parameter_type.variants(), [EvalParameterTypeVariant::Mixed]) } /// Returns whether a return type is exactly PHP `never`. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 48ff76d819..452039776c 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1613,6 +1613,16 @@ fn class_method_signature_accepts( required.parameter_defaults(), required.parameter_is_by_ref(), required.parameter_is_variadic(), + ) && method_parameter_type_signature_accepts( + method.parameter_types(), + method.parameter_is_variadic(), + method_owner, + required.parameter_types(), + required.parameter_is_variadic(), + required_owner, + required.params().len(), + pending_class, + context, ) && method_return_type_signature_accepts( method.return_type(), method_owner, @@ -1924,6 +1934,16 @@ fn class_method_satisfies_interface_signature( requirement.parameter_defaults(), requirement.parameter_is_by_ref(), requirement.parameter_is_variadic(), + ) && method_parameter_type_signature_accepts( + method.parameter_types(), + method.parameter_is_variadic(), + method_owner, + requirement.parameter_types(), + requirement.parameter_is_variadic(), + requirement_owner, + requirement.params().len(), + pending_class, + context, ) && method_return_type_signature_accepts( method.return_type(), method_owner, diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index d14f9c0453..30fdb9ae0d 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -912,6 +912,84 @@ class EvalArityChild extends EvalArityBase { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval accepts PHP-contravariant method parameter type overrides. +#[test] +fn execute_program_accepts_contravariant_method_parameter_type_overrides() { + let program = parse_fragment( + br#"class EvalParamBase { + public function anyInt(int $value) { return $value; } + public function maybeInt(int $value) { return $value; } + public function untypedInt(int $value) { return $value; } +} +class EvalParamChild extends EvalParamBase { + public function anyInt(mixed $value) { return $value . ":mixed"; } + public function maybeInt(?int $value) { return $value; } + public function untypedInt($value) { return $value; } +} +$child = new EvalParamChild(); +echo $child->anyInt(7); echo ":"; +echo $child->untypedInt("ok"); +return $child->maybeInt(null) === null;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:mixed:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval rejects method parameter overrides that narrow PHP's accepted type set. +#[test] +fn execute_program_rejects_incompatible_method_parameter_type_overrides() { + let incompatible_type = parse_fragment( + br#"class EvalParamTypeBase { + public function read(int $value) { return $value; } +} +class EvalParamStringChild extends EvalParamTypeBase { + public function read(string $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible parameter override type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let narrower_nullable = parse_fragment( + br#"class EvalParamNullableBase { + public function maybe(?int $value) { return $value; } +} +class EvalParamNonNullChild extends EvalParamNullableBase { + public function maybe(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&narrower_nullable, &mut scope, &mut values) + .expect_err("narrower nullable parameter override type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let untyped_to_typed = parse_fragment( + br#"class EvalParamUntypedBase { + public function read($value) { return $value; } +} +class EvalParamTypedChild extends EvalParamUntypedBase { + public function read(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&untyped_to_typed, &mut scope, &mut values) + .expect_err("typed parameter override of untyped parent should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval accepts covariant method return type overrides. #[test] fn execute_program_accepts_covariant_method_return_type_overrides() { @@ -1188,6 +1266,64 @@ class EvalWiderReturnImpl implements EvalNeedsStringReturn { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval accepts PHP-contravariant parameter types for interface contracts. +#[test] +fn execute_program_accepts_contravariant_interface_method_parameter_types() { + let program = parse_fragment( + br#"interface EvalParamContract { + function read(int $value); +} +class EvalParamContractReader implements EvalParamContract { + public function read(mixed $value) { + return $value . ":ok"; + } +} +$reader = new EvalParamContractReader(); +return $reader->read(8);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("8:ok".to_string())); +} + +/// Verifies eval rejects interface implementations with incompatible parameter types. +#[test] +fn execute_program_rejects_incompatible_interface_method_parameter_types() { + let incompatible_type = parse_fragment( + br#"interface EvalParamStringContract { + function read(int $value); +} +class EvalParamStringReader implements EvalParamStringContract { + public function read(string $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible interface parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let untyped_to_typed = parse_fragment( + br#"interface EvalParamUntypedContract { + function read($value); +} +class EvalParamTypedReader implements EvalParamUntypedContract { + public function read(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&untyped_to_typed, &mut scope, &mut values) + .expect_err("typed parameter implementation of untyped contract should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval static interface method contracts are satisfied by public static methods. #[test] fn execute_program_accepts_static_dynamic_interface_method() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 8904f7e5ff..4a50404034 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -155,9 +155,10 @@ properties, static methods, static interface method contracts, class, interface, trait, and enum constants including `final` constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and `__callStatic()`, and magic property fallback through `__get()` and `__set()`. -Eval validates method override and interface method return types with PHP-style -covariance for supported declared return type metadata, including nullable, -union, `mixed`, `self`, `parent`, `static`, class, and interface return types. +Eval validates method override and interface method parameter and return types +with PHP-style parameter contravariance and return covariance for supported +declared type metadata, including nullable, union, `mixed`, `self`, `parent`, +`static`, class, and interface types. Eval-declared method calls also enforce declared return values at runtime, with weak scalar coercions and PHP-style handling for `void`, `never`, `self`, `parent`, and late-bound `static`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5335d13b1a..79f56b93f4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6697,6 +6697,80 @@ class EvalReturnMixedChildBad extends EvalReturnNullableBase { ); } +/// Verifies eval-declared method overrides enforce contravariant parameter types. +#[test] +fn test_eval_declared_method_parameter_type_override_contracts() { + let out = compile_and_run_capture( + r#"anyInt(7) . ":"; +echo $child->untypedInt("ok") . ":"; +echo $child->maybeInt(null) === null ? "null" : "bad";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:mixed:ok:null"); + + let err = compile_and_run_expect_failure( + r#"read(8);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "8:ok"); + + let err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 21:59:06 +0200 Subject: [PATCH 0666/1208] Validate eval property redeclarations --- .../src/interpreter/return_type_compat.rs | 134 +++++++++++++++++- .../src/interpreter/statements.rs | 21 ++- .../src/interpreter/tests/classes.rs | 125 ++++++++++++++++ docs/php/eval.md | 3 + tests/codegen/eval.rs | 102 +++++++++++++ 5 files changed, 381 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/return_type_compat.rs b/crates/elephc-magician/src/interpreter/return_type_compat.rs index ba3ffc21da..464d241938 100644 --- a/crates/elephc-magician/src/interpreter/return_type_compat.rs +++ b/crates/elephc-magician/src/interpreter/return_type_compat.rs @@ -1,7 +1,7 @@ //! Purpose: -//! Validates method type compatibility for eval-declared method signatures. -//! This keeps class/interface parameter contravariance and return covariance -//! checks out of the statement dispatcher. +//! Validates type compatibility for eval-declared method and property signatures. +//! This keeps method parameter contravariance, return covariance, and property +//! type invariance checks out of the statement dispatcher. //! //! Called from: //! - `crate::interpreter::statements` while registering eval class-like declarations. @@ -71,6 +71,29 @@ pub(super) fn method_return_type_signature_accepts( }) } +/// Returns whether a redeclared property preserves PHP's invariant type contract. +pub(super) fn property_type_signature_matches( + property_type: Option<&EvalParameterType>, + property_owner: &str, + required_type: Option<&EvalParameterType>, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + match (property_type, required_type) { + (None, None) => true, + (Some(property_type), Some(required_type)) => eval_property_type_matches( + property_type, + property_owner, + required_type, + required_owner, + pending_class, + context, + ), + _ => false, + } +} + /// Returns the parameter type that applies at one source-order argument position. fn method_signature_effective_parameter_type<'a>( parameter_types: &'a [Option], @@ -87,6 +110,111 @@ fn method_signature_effective_parameter_type<'a>( parameter_types.get(position).and_then(Option::as_ref) } +/// Returns whether two property type declarations are PHP-equivalent. +fn eval_property_type_matches( + property_type: &EvalParameterType, + property_owner: &str, + required_type: &EvalParameterType, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + property_type.allows_null() == required_type.allows_null() + && property_type.is_intersection() == required_type.is_intersection() + && eval_property_type_variants_match( + property_type.variants(), + property_owner, + required_type.variants(), + required_owner, + pending_class, + context, + ) +} + +/// Returns whether two property type variant sets match, ignoring source order. +fn eval_property_type_variants_match( + property_variants: &[EvalParameterTypeVariant], + property_owner: &str, + required_variants: &[EvalParameterTypeVariant], + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if property_variants.len() != required_variants.len() { + return false; + } + let mut matched = vec![false; property_variants.len()]; + required_variants.iter().all(|required_variant| { + property_variants + .iter() + .enumerate() + .find(|(index, property_variant)| { + !matched[*index] + && eval_property_type_variant_matches( + property_variant, + property_owner, + required_variant, + required_owner, + pending_class, + context, + ) + }) + .is_some_and(|(index, _)| { + matched[index] = true; + true + }) + }) +} + +/// Returns whether two property type atoms match PHP's invariant redeclaration rule. +fn eval_property_type_variant_matches( + property_variant: &EvalParameterTypeVariant, + property_owner: &str, + required_variant: &EvalParameterTypeVariant, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + match (property_variant, required_variant) { + ( + EvalParameterTypeVariant::Class(property_name), + EvalParameterTypeVariant::Class(required_name), + ) => eval_property_class_type_matches( + property_name, + property_owner, + required_name, + required_owner, + pending_class, + context, + ), + _ => property_variant == required_variant, + } +} + +/// Returns whether two class-name property type atoms name the same PHP type. +fn eval_property_class_type_matches( + property_name: &str, + property_owner: &str, + required_name: &str, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if property_name + .trim_start_matches('\\') + .eq_ignore_ascii_case(required_name.trim_start_matches('\\')) + { + return true; + } + let Some(property_resolved) = + eval_resolve_return_class_type_name(property_name, property_owner, pending_class, context) + else { + return false; + }; + eval_resolve_return_class_type_name(required_name, required_owner, pending_class, context) + .is_some_and(|required_resolved| property_resolved.eq_ignore_ascii_case(&required_resolved)) +} + /// Returns whether an implementation parameter type is a PHP contravariant supertype. fn eval_parameter_type_accepts( implementation_type: Option<&EvalParameterType>, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 452039776c..6f606512ae 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1480,7 +1480,9 @@ fn validate_property_parent_redeclaration( let Some(parent) = class.parent() else { return Ok(()); }; - let Some((_, parent_property)) = context.class_property(parent, property.name()) else { + let Some((parent_declaring_class, parent_property)) = + context.class_property(parent, property.name()) + else { return Ok(()); }; if parent_property.visibility() == EvalVisibility::Private { @@ -1491,6 +1493,23 @@ fn validate_property_parent_redeclaration( { return Err(EvalStatus::RuntimeFatal); } + if parent_property.is_static() != property.is_static() + || parent_property.is_readonly() != property.is_readonly() + || property_visibility_rank(property.visibility()) + < property_visibility_rank(parent_property.visibility()) + || property_visibility_rank(property.write_visibility()) + < property_visibility_rank(parent_property.write_visibility()) + || !property_type_signature_matches( + property.property_type(), + class.name(), + parent_property.property_type(), + &parent_declaring_class, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } Ok(()) } diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index e42761d3a1..b08fd52356 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -674,6 +674,131 @@ class EvalAsymPrivateSetAbstractBox extends EvalAsymPrivateSetAbstractBase { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval property redeclarations may widen visibility while preserving invariant types. +#[test] +fn execute_program_accepts_compatible_property_redeclarations() { + let program = parse_fragment( + br#"class EvalPropertyRedeclareBase { + protected int|string $value; +} +class EvalPropertyRedeclareChild extends EvalPropertyRedeclareBase { + public string|int $value; +} +class EvalPropertyRelativeBase { + public self $selfValue; + public EvalPropertyRelativeBase $parentValue; +} +class EvalPropertyRelativeChild extends EvalPropertyRelativeBase { + public self $selfValue; + public parent $parentValue; +} +$box = new EvalPropertyRedeclareChild(); +$box->value = "ok"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok".to_string())); +} + +/// Verifies eval rejects inherited property redeclarations that violate PHP invariance. +#[test] +fn execute_program_rejects_incompatible_property_redeclarations() { + let incompatible_type = parse_fragment( + br#"class EvalPropertyTypeBase { + public int $value; +} +class EvalPropertyStringChild extends EvalPropertyTypeBase { + public string $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible inherited property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_visibility = parse_fragment( + br#"class EvalPropertyPublicBase { + public int $value; +} +class EvalPropertyProtectedChild extends EvalPropertyPublicBase { + protected int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_visibility, &mut scope, &mut values) + .expect_err("reduced inherited property visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let typed_from_untyped = parse_fragment( + br#"class EvalPropertyUntypedBase { + public $value; +} +class EvalPropertyTypedChild extends EvalPropertyUntypedBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&typed_from_untyped, &mut scope, &mut values) + .expect_err("typed inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let static_mismatch = parse_fragment( + br#"class EvalPropertyStaticBase { + public static int $value; +} +class EvalPropertyInstanceChild extends EvalPropertyStaticBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&static_mismatch, &mut scope, &mut values) + .expect_err("static inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let readonly_mismatch = parse_fragment( + br#"class EvalPropertyReadonlyBase { + public readonly int $value; +} +class EvalPropertyMutableChild extends EvalPropertyReadonlyBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&readonly_mismatch, &mut scope, &mut values) + .expect_err("readonly inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_write_visibility = parse_fragment( + br#"class EvalPropertyProtectedSetBase { + public protected(set) int $value; +} +class EvalPropertyPrivateSetChild extends EvalPropertyProtectedSetBase { + public private(set) int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_write_visibility, &mut scope, &mut values) + .expect_err("reduced inherited property write visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies readonly class inheritance requires matching readonly status. #[test] fn execute_program_rejects_readonly_class_extending_non_readonly_parent() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 4a50404034..285a99db9e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -159,6 +159,9 @@ Eval validates method override and interface method parameter and return types with PHP-style parameter contravariance and return covariance for supported declared type metadata, including nullable, union, `mixed`, `self`, `parent`, `static`, class, and interface types. +Eval validates inherited property redeclarations with PHP-style invariant +property types, matching static/readonly modifiers, and compatible read/write +visibility. Eval-declared method calls also enforce declared return values at runtime, with weak scalar coercions and PHP-style handling for `void`, `never`, `self`, `parent`, and late-bound `static`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 79f56b93f4..3d9b1c485c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9438,6 +9438,108 @@ echo $protected->getModifiers();'); assert_eq!(out, "1:base:7:owner:child:Pt4129:pT2049"); } +/// Verifies eval-declared inherited properties preserve PHP redeclaration invariants. +#[test] +fn test_eval_declared_inherited_property_redeclaration_contracts() { + let out = compile_and_run( + r#"value = "ok"; +echo $box->value;'); +"#, + ); + assert_eq!(out, "ok"); + + let err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 22:11:13 +0200 Subject: [PATCH 0667/1208] Validate eval constant visibility --- .../src/interpreter/statements.rs | 30 +++++- .../src/interpreter/tests/class_constants.rs | 96 +++++++++++++++++++ docs/php/eval.md | 2 + tests/codegen/eval.rs | 71 ++++++++++++++ 4 files changed, 197 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 6f606512ae..140952e98e 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -912,6 +912,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( } } validate_eval_declared_constants(interface.constants())?; + validate_eval_interface_constants(interface.constants())?; validate_interface_constant_parent_redeclarations(interface, context)?; if context.define_interface(interface.clone()) { initialize_eval_declared_constants( @@ -1527,6 +1528,16 @@ fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<( Ok(()) } +/// Validates declarations that are specific to PHP interface constants. +fn validate_eval_interface_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { + for constant in constants { + if constant.visibility() != EvalVisibility::Public { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + /// Validates interface constants against inherited parent-interface constants. fn validate_interface_constant_parent_redeclarations( interface: &EvalInterface, @@ -1553,7 +1564,10 @@ fn validate_constant_parent_redeclaration( ) -> Result<(), EvalStatus> { if let Some(parent) = class.parent() { if let Some((_, parent_constant)) = context.class_constant(parent, constant.name()) { - if parent_constant.visibility() != EvalVisibility::Private && parent_constant.is_final() + if parent_constant.visibility() != EvalVisibility::Private + && (parent_constant.is_final() + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(parent_constant.visibility())) { return Err(EvalStatus::RuntimeFatal); } @@ -1563,7 +1577,10 @@ fn validate_constant_parent_redeclaration( if let Some((_, interface_constant)) = context.interface_constant(interface, constant.name()) { - if interface_constant.is_final() { + if interface_constant.is_final() + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(interface_constant.visibility()) + { return Err(EvalStatus::RuntimeFatal); } } @@ -1571,6 +1588,15 @@ fn validate_constant_parent_redeclaration( Ok(()) } +/// Returns a comparable rank where larger means less restrictive constant visibility. +fn constant_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} + /// Validates one method declaration against inherited eval method metadata. fn validate_method_parent_override( class: &EvalClass, diff --git a/crates/elephc-magician/src/interpreter/tests/class_constants.rs b/crates/elephc-magician/src/interpreter/tests/class_constants.rs index bbfa8d3e1d..1c035d86eb 100644 --- a/crates/elephc-magician/src/interpreter/tests/class_constants.rs +++ b/crates/elephc-magician/src/interpreter/tests/class_constants.rs @@ -101,6 +101,102 @@ class EvalFinalConstChild extends EvalFinalConstBase { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval accepts class constant redeclarations that keep compatible visibility. +#[test] +fn execute_program_accepts_compatible_eval_class_constant_redeclaration() { + let program = parse_fragment( + br#"class EvalConstVisibilityBase { + protected const SEED = 2; +} +class EvalConstVisibilityChild extends EvalConstVisibilityBase { + public const SEED = 7; +} +interface EvalConstVisibilityIface { + public const TOKEN = 3; +} +class EvalConstVisibilityImpl implements EvalConstVisibilityIface { + public const TOKEN = 5; +} +echo EvalConstVisibilityChild::SEED; echo ":"; +return EvalConstVisibilityImpl::TOKEN;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies eval rejects inherited class constant redeclarations with reduced visibility. +#[test] +fn execute_program_rejects_reduced_eval_class_constant_visibility() { + let reduced_parent_visibility = parse_fragment( + br#"class EvalConstPublicBase { + public const SEED = 1; +} +class EvalConstProtectedChild extends EvalConstPublicBase { + protected const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_parent_visibility, &mut scope, &mut values) + .expect_err("reduced class constant visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_protected_parent_visibility = parse_fragment( + br#"class EvalConstProtectedBase { + protected const SEED = 1; +} +class EvalConstPrivateChild extends EvalConstProtectedBase { + private const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_protected_parent_visibility, &mut scope, &mut values) + .expect_err("private class constant redeclaration should fail protected parent"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_interface_visibility = parse_fragment( + br#"interface EvalConstPublicContract { + public const SEED = 1; +} +class EvalConstProtectedImpl implements EvalConstPublicContract { + protected const SEED = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_interface_visibility, &mut scope, &mut values) + .expect_err("reduced interface constant visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval rejects non-public interface constants like PHP. +#[test] +fn execute_program_rejects_non_public_eval_interface_constants() { + parse_fragment( + br#"interface EvalConstProtectedIface { + protected const SEED = 1; +}"#, + ) + .expect_err("protected interface constant should fail while parsing"); + + parse_fragment( + br#"interface EvalConstPrivateIface { + private const SEED = 1; +}"#, + ) + .expect_err("private interface constant should fail while parsing"); +} + /// Verifies private eval constants cannot be declared final. #[test] fn execute_program_rejects_final_private_eval_class_constant() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 285a99db9e..df4ac450ef 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -162,6 +162,8 @@ declared type metadata, including nullable, union, `mixed`, `self`, `parent`, Eval validates inherited property redeclarations with PHP-style invariant property types, matching static/readonly modifiers, and compatible read/write visibility. +Eval validates inherited class/interface constant redeclarations with PHP-style +visibility compatibility, and rejects non-public interface constants. Eval-declared method calls also enforce declared return values at runtime, with weak scalar coercions and PHP-style handling for `void`, `never`, `self`, `parent`, and late-bound `static`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3d9b1c485c..8704328901 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8122,6 +8122,77 @@ class EvalFinalConstChild extends EvalFinalConstBase { ); } +/// Verifies eval-declared class constants preserve PHP visibility redeclaration rules. +#[test] +fn test_eval_declared_class_constant_visibility_contracts() { + let out = compile_and_run_capture( + r#" Date: Thu, 25 Jun 2026 22:22:25 +0200 Subject: [PATCH 0668/1208] Validate abstract eval interface members --- crates/elephc-magician/src/context.rs | 19 +- .../src/interpreter/statements.rs | 217 ++++++++++++++++-- .../src/interpreter/tests/classes.rs | 88 +++++++ .../src/interpreter/tests/expressions.rs | 72 ++++++ docs/php/eval.md | 3 + tests/codegen/eval.rs | 100 ++++++++ 6 files changed, 473 insertions(+), 26 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 7b95e593c0..4b755a71b2 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -1727,6 +1727,17 @@ impl ElephcEvalContext { &self, interface_name: &str, ) -> Vec { + self.interface_property_requirements_with_owners(interface_name) + .into_iter() + .map(|(_, property)| property) + .collect() + } + + /// Returns direct and inherited property contracts with their declaring interface. + pub fn interface_property_requirements_with_owners( + &self, + interface_name: &str, + ) -> Vec<(String, EvalInterfaceProperty)> { let mut properties = Vec::new(); let mut seen_interfaces = HashSet::new(); self.collect_interface_property_requirements( @@ -1741,7 +1752,7 @@ impl ElephcEvalContext { fn collect_interface_property_requirements( &self, interface_name: &str, - properties: &mut Vec, + properties: &mut Vec<(String, EvalInterfaceProperty)>, seen_interfaces: &mut HashSet, ) { let key = normalize_class_name(interface_name); @@ -1755,13 +1766,13 @@ impl ElephcEvalContext { self.collect_interface_property_requirements(parent, properties, seen_interfaces); } for property in interface.properties() { - if let Some(existing) = properties + if let Some((_, existing)) = properties .iter_mut() - .find(|existing| existing.name() == property.name()) + .find(|(_, existing)| existing.name() == property.name()) { *existing = existing.merged_with(property); } else { - properties.push(property.clone()); + properties.push((interface.name().to_string(), property.clone())); } } } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 140952e98e..cdd8e766cd 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -637,6 +637,7 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( return Err(EvalStatus::RuntimeFatal); } } + validate_declared_class_interface_members(class, context)?; if !class.is_abstract() { validate_concrete_class_requirements(class, context)?; } @@ -1597,6 +1598,94 @@ fn constant_visibility_rank(visibility: EvalVisibility) -> u8 { } } +/// Validates declared or inherited class members that already cover eval interface contracts. +fn validate_declared_class_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for interface in pending_class_interface_names(class, context) { + if !context.has_interface(&interface) { + continue; + } + validate_declared_class_interface_methods(class, &interface, context)?; + validate_declared_class_interface_properties(class, &interface, context)?; + } + Ok(()) +} + +/// Validates class methods present for an eval interface, even on abstract classes. +fn validate_declared_class_interface_methods( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + context.interface_method_requirements_with_owners(interface_name) + { + let Some((declaring_class, method)) = + pending_class_method(class, requirement.name(), context) + else { + continue; + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static() + || !class_method_satisfies_interface_signature( + &method, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates class properties present for an eval interface, even on abstract classes. +fn validate_declared_class_interface_properties( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + context.interface_property_requirements_with_owners(interface_name) + { + let Some((declaring_class, property)) = + pending_class_property_with_owner(class, requirement.name(), context) + else { + continue; + }; + if !class_property_can_cover_interface_contract( + &property, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns a method from a pending class or one of its already registered parents. +fn pending_class_method( + class: &EvalClass, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(method) = class.method(method_name) { + return Some((class.name().to_string(), method.clone())); + } + class + .parent() + .and_then(|parent| context.class_method(parent, method_name)) +} + /// Validates one method declaration against inherited eval method metadata. fn validate_method_parent_override( class: &EvalClass, @@ -1915,8 +2004,10 @@ fn validate_class_implements_eval_interface( return Err(EvalStatus::RuntimeFatal); } } - for requirement in context.interface_property_requirements(interface_name) { - if !class_has_interface_property(class, &requirement, context) { + for (requirement_owner, requirement) in + context.interface_property_requirements_with_owners(interface_name) + { + if !class_has_interface_property(class, &requirement_owner, &requirement, context) { return Err(EvalStatus::RuntimeFatal); } } @@ -1999,6 +2090,91 @@ fn class_method_satisfies_interface_signature( ) } +/// Returns whether one class property is compatible with one interface property contract. +fn class_property_can_cover_interface_contract( + property: &EvalClassProperty, + property_owner: &str, + requirement: &EvalInterfaceProperty, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if property.visibility() != EvalVisibility::Public || property.is_static() { + return false; + } + if !class_property_type_satisfies_interface_contract( + property.property_type(), + property_owner, + requirement, + requirement_owner, + pending_class, + context, + ) { + return false; + } + if requirement.requires_get() && !class_property_supports_interface_get(property) { + return false; + } + if requirement.requires_set() { + return requirement.set_visibility() != Some(EvalVisibility::Private) + && property_visibility_rank(property.write_visibility()) + >= property_visibility_rank(requirement.write_visibility()) + && class_property_supports_interface_set(property); + } + requirement.requires_get() +} + +/// Returns whether one property type is compatible with interface get/set hook signatures. +fn class_property_type_satisfies_interface_contract( + property_type: Option<&EvalParameterType>, + property_owner: &str, + requirement: &EvalInterfaceProperty, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if requirement.requires_get() + && !method_return_type_signature_accepts( + property_type, + property_owner, + requirement.property_type(), + requirement_owner, + pending_class, + context, + ) + { + return false; + } + if requirement.requires_set() { + let property_types = vec![property_type.cloned()]; + let requirement_types = vec![requirement.property_type().cloned()]; + return method_parameter_type_signature_accepts( + &property_types, + &[], + property_owner, + &requirement_types, + &[], + requirement_owner, + 1, + pending_class, + context, + ); + } + true +} + +/// Returns whether one property can satisfy an interface `get` hook. +fn class_property_supports_interface_get(property: &EvalClassProperty) -> bool { + property.has_get_hook() || property.requires_get_hook() || !property.is_virtual() +} + +/// Returns whether one property can satisfy an interface `set` hook. +fn class_property_supports_interface_set(property: &EvalClassProperty) -> bool { + property.has_set_hook() + || property.requires_set_hook() + || (!property.is_virtual() && !property.is_readonly()) +} + /// Returns whether an implementing method accepts the full required arity range. fn method_signature_accepts( implementation_param_count: usize, @@ -2097,44 +2273,41 @@ fn method_signature_max_arity(param_count: usize, variadics: &[bool]) -> Option< /// Returns whether a class or its eval parents satisfy one interface property contract. fn class_has_interface_property( class: &EvalClass, + requirement_owner: &str, requirement: &EvalInterfaceProperty, context: &ElephcEvalContext, ) -> bool { - pending_class_property(class, requirement.name(), context).is_some_and(|property| { - if property.is_abstract() - || property.visibility() != EvalVisibility::Public - || property.is_static() - { - return false; - } - if requirement.requires_set() { - return requirement.set_visibility() != Some(EvalVisibility::Private) - && property_visibility_rank(property.write_visibility()) - >= property_visibility_rank(requirement.write_visibility()) - && (property.has_set_hook() - || (!property.has_get_hook() && !property.is_readonly())); - } - requirement.requires_get() - }) + pending_class_property_with_owner(class, requirement.name(), context).is_some_and( + |(declaring_class, property)| { + !property.is_abstract() + && class_property_can_cover_interface_contract( + &property, + &declaring_class, + requirement, + requirement_owner, + Some(class), + context, + ) + }, + ) } /// Returns a property from a pending class or one of its already registered parents. -fn pending_class_property( +fn pending_class_property_with_owner( class: &EvalClass, property_name: &str, context: &ElephcEvalContext, -) -> Option { +) -> Option<(String, EvalClassProperty)> { if let Some(property) = class .properties() .iter() .find(|property| property.name() == property_name) { - return Some(property.clone()); + return Some((class.name().to_string(), property.clone())); } class .parent() .and_then(|parent| context.class_property(parent, property_name)) - .map(|(_, property)| property) } /// Reads one object property while enforcing eval-declared member visibility. diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index b08fd52356..867ba5c189 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1371,6 +1371,94 @@ return $box->value;"#, assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); } +/// Verifies interface property hook types are checked on abstract and concrete classes. +#[test] +fn execute_program_validates_interface_property_hook_types() { + let valid_program = parse_fragment( + br#"interface EvalIfaceGetWide { + public int|string $value { get; } +} +interface EvalIfaceSetNarrow { + public int $slot { set; } +} +abstract class EvalIfacePropertyDeferred implements EvalIfaceGetWide {} +abstract class EvalIfacePropertyGood implements EvalIfaceGetWide, EvalIfaceSetNarrow { + abstract public int $value { get; } + abstract public int|string $slot { set; } +} +class EvalIfacePropertyConcrete implements EvalIfaceGetWide { + public int $value = 4; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&valid_program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + + let bad_abstract_get = parse_fragment( + br#"interface EvalIfaceGetInt { + public int $value { get; } +} +abstract class EvalIfaceGetWideBad implements EvalIfaceGetInt { + abstract public int|string $value { get; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_get, &mut scope, &mut values) + .expect_err("wider abstract get property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_abstract_set = parse_fragment( + br#"interface EvalIfaceSetWide { + public int|string $value { set; } +} +abstract class EvalIfaceSetNarrowBad implements EvalIfaceSetWide { + abstract public int $value { set; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_set, &mut scope, &mut values) + .expect_err("narrower abstract set property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_concrete_get = parse_fragment( + br#"interface EvalIfaceConcreteGetInt { + public int $value { get; } +} +class EvalIfaceConcreteGetWideBad implements EvalIfaceConcreteGetInt { + public int|string $value = 4; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_concrete_get, &mut scope, &mut values) + .expect_err("wider concrete get property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_inherited_property = parse_fragment( + br#"interface EvalIfaceInheritedGet { + public int $value { get; } +} +abstract class EvalIfaceInheritedPropertyBase { + public string $value = "bad"; +} +abstract class EvalIfaceInheritedPropertyChild extends EvalIfaceInheritedPropertyBase implements EvalIfaceInheritedGet {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_inherited_property, &mut scope, &mut values) + .expect_err("inherited incompatible interface property should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies a get-only hook cannot satisfy a writable eval interface contract. #[test] fn execute_program_rejects_get_only_hook_for_interface_set_contract() { diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index 30fdb9ae0d..f21935cafd 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -1266,6 +1266,78 @@ class EvalWiderReturnImpl implements EvalNeedsStringReturn { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies abstract eval classes must keep declared interface method signatures compatible. +#[test] +fn execute_program_rejects_incompatible_abstract_interface_method_declarations() { + let bad_abstract_param = parse_fragment( + br#"interface EvalAbstractIfaceParam { + function read(int $value); +} +abstract class EvalAbstractIfaceParamBase implements EvalAbstractIfaceParam { + abstract public function read(string $value); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_param, &mut scope, &mut values) + .expect_err("abstract interface method parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_abstract_return = parse_fragment( + br#"interface EvalAbstractIfaceReturn { + function read(): int; +} +abstract class EvalAbstractIfaceReturnBase implements EvalAbstractIfaceReturn { + abstract public function read(): string; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_return, &mut scope, &mut values) + .expect_err("abstract interface method return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_inherited_method = parse_fragment( + br#"interface EvalInheritedIfaceMethod { + function read(int $value); +} +abstract class EvalInheritedIfaceMethodBase { + public function read(string $value) {} +} +abstract class EvalInheritedIfaceMethodChild extends EvalInheritedIfaceMethodBase implements EvalInheritedIfaceMethod {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_inherited_method, &mut scope, &mut values) + .expect_err("inherited incompatible interface method should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract eval classes may defer missing compatible interface methods. +#[test] +fn execute_program_accepts_deferred_abstract_interface_method_declarations() { + let program = parse_fragment( + br#"interface EvalAbstractIfaceDeferred { + function read(int $value): int; +} +abstract class EvalAbstractIfaceDeferredBase implements EvalAbstractIfaceDeferred {} +abstract class EvalAbstractIfaceDeferredTyped implements EvalAbstractIfaceDeferred { + abstract public function read(mixed $value): int; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval accepts PHP-contravariant parameter types for interface contracts. #[test] fn execute_program_accepts_contravariant_interface_method_parameter_types() { diff --git a/docs/php/eval.md b/docs/php/eval.md index df4ac450ef..1f03387b24 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -159,6 +159,9 @@ Eval validates method override and interface method parameter and return types with PHP-style parameter contravariance and return covariance for supported declared type metadata, including nullable, union, `mixed`, `self`, `parent`, `static`, class, and interface types. +Abstract classes may defer missing interface methods and property contracts, but +declared or inherited members that cover an interface contract are validated at +declaration time. Eval validates inherited property redeclarations with PHP-style invariant property types, matching static/readonly modifiers, and compatible read/write visibility. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8704328901..1d191a07cd 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6890,6 +6890,60 @@ class EvalParamTypedReader implements EvalParamUntypedContract { ); } +/// Verifies eval-declared abstract classes validate declared interface method signatures. +#[test] +fn test_eval_declared_abstract_interface_method_contracts() { + let out = compile_and_run_capture( + r#" 42; } }'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); + + let err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 22:31:28 +0200 Subject: [PATCH 0669/1208] Validate eval enum magic methods --- .../src/interpreter/statements.rs | 26 +++++++- .../src/interpreter/tests/enums.rs | 62 ++++++++++++++----- docs/php/eval.md | 5 +- tests/codegen/eval.rs | 17 +++++ 4 files changed, 92 insertions(+), 18 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index cdd8e766cd..760e9edc94 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -754,10 +754,10 @@ fn validate_eval_enum_case_declarations(enum_decl: &EvalEnum) -> Result<(), Eval /// Validates enum method declarations that PHP reserves or forbids on enums. fn validate_eval_enum_method_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { for method in enum_decl.methods() { - if method.name().eq_ignore_ascii_case("__construct") - || method.name().eq_ignore_ascii_case("cases") + if method.name().eq_ignore_ascii_case("cases") || method.name().eq_ignore_ascii_case("from") || method.name().eq_ignore_ascii_case("tryFrom") + || is_forbidden_eval_enum_magic_method(method.name()) { return Err(EvalStatus::RuntimeFatal); } @@ -765,6 +765,28 @@ fn validate_eval_enum_method_declarations(enum_decl: &EvalEnum) -> Result<(), Ev Ok(()) } +/// Returns whether PHP forbids this magic method name inside enum declarations. +fn is_forbidden_eval_enum_magic_method(name: &str) -> bool { + [ + "__construct", + "__destruct", + "__clone", + "__get", + "__set", + "__isset", + "__unset", + "__sleep", + "__wakeup", + "__serialize", + "__unserialize", + "__toString", + "__debugInfo", + "__set_state", + ] + .iter() + .any(|method| name.eq_ignore_ascii_case(method)) +} + /// Initializes enum singleton case objects for a newly declared eval enum. fn initialize_eval_enum_cases( enum_decl: &EvalEnum, diff --git a/crates/elephc-magician/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs index a117d3ed6f..a2a4addcaa 100644 --- a/crates/elephc-magician/src/interpreter/tests/enums.rs +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -11,6 +11,17 @@ use super::super::*; use super::support::*; +/// Executes an eval enum fragment and asserts it fails during runtime validation. +fn assert_invalid_enum_fragment(source: &[u8], message: &str) { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err(message); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies pure eval enums expose singleton cases and class-like introspection. #[test] fn execute_program_declares_pure_eval_enum_cases() { @@ -121,33 +132,56 @@ return EvalSuit::fallback() === EvalSuit::Hearts;"#, /// Verifies eval rejects enum members that conflict with PHP enum rules. #[test] fn execute_program_rejects_invalid_eval_enum_members() { - let program = parse_fragment( + assert_invalid_enum_fragment( br#"enum EvalInvalidEnum { case Ready; public const Ready = 1; }"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("case and constant name collision should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); + "case and constant name collision should fail", + ); - let program = parse_fragment( + assert_invalid_enum_fragment( br#"enum EvalInvalidEnumMethod { case Ready; public static function cases() { return []; } }"#, + "reserved enum method should fail", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidEnumMagicMethod { + case Ready; + public function __destruct() {} +}"#, + "forbidden enum magic method should fail", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidEnumMagicMethodCase { + case Ready; + public function __GET($name) {} +}"#, + "forbidden enum magic method lookup should be case-insensitive", + ); +} + +/// Verifies eval allows the enum magic methods PHP permits. +#[test] +fn execute_program_allows_supported_eval_enum_magic_methods() { + let program = parse_fragment( + br#"enum EvalAllowedEnumMagic { + case Ready; + public function __call($name, $arguments) { return $name; } + public static function __callStatic($name, $arguments) { return $name; } + public function __invoke() { return "invoke"; } +} +return enum_exists("EvalAllowedEnumMagic");"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("reserved enum method should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 1f03387b24..323cf166ca 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -594,8 +594,9 @@ eval-declared classes. Pure and backed enum cases are singleton objects, values. `EnumName::from()` misses throw a catchable `ValueError`, while `EnumName::tryFrom()` misses return `null`. Enums can implement eval-declared or generated interfaces and can use their own instance/static methods and class -constants. Direct `new EnumName()` and property writes to enum cases are -rejected. +constants. Eval rejects enum declarations that redeclare reserved enum methods +or PHP-forbidden enum magic methods. Direct `new EnumName()` and property writes +to enum cases are rejected. Public declared property reads/writes through `$this->property` from native methods are bridged to eval. Protected and private declared property diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1d191a07cd..91b0364ab8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6469,6 +6469,23 @@ try { ); } +/// Verifies eval-declared enums reject magic methods PHP forbids on enums. +#[test] +fn test_eval_fragment_rejects_forbidden_enum_magic_method() { + let err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 22:39:12 +0200 Subject: [PATCH 0670/1208] Reject readonly class static properties in eval --- crates/elephc-magician/src/eval_ir.rs | 8 +++----- .../src/interpreter/statements.rs | 2 +- .../src/interpreter/tests/classes.rs | 16 ++++++---------- docs/php/eval.md | 6 +++--- tests/codegen/eval.rs | 18 ++++++++++++++---- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 4f4f4af7a3..b1ae8e274b 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -1400,13 +1400,11 @@ impl EvalClass { self } - /// Marks all instance properties readonly when this metadata represents a `readonly class`. - pub fn with_readonly_instance_properties(mut self) -> Self { + /// Marks all properties readonly when this metadata represents a `readonly class`. + pub fn with_readonly_properties(mut self) -> Self { if self.is_readonly_class { for property in &mut self.properties { - if !property.is_static { - property.is_readonly = true; - } + property.is_readonly = true; } } self diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 760e9edc94..ccdb042879 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -618,7 +618,7 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( { return Err(EvalStatus::RuntimeFatal); } - let class = expand_eval_class_traits(class, context)?.with_readonly_instance_properties(); + let class = expand_eval_class_traits(class, context)?.with_readonly_properties(); let class = &class; validate_eval_class_modifiers(class, context)?; if let Some(parent) = class.parent() { diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 867ba5c189..ff6017a287 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -436,26 +436,22 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies readonly class static properties remain mutable. +/// Verifies readonly classes reject static properties because PHP makes them readonly. #[test] -fn execute_program_allows_readonly_class_static_property_mutation() { +fn execute_program_rejects_readonly_class_static_property() { let program = parse_fragment( br#"readonly class EvalReadonlyStaticBox { public static int $count = 1; -} -EvalReadonlyStaticBox::$count = 5; -echo EvalReadonlyStaticBox::$count; echo ":"; -EvalReadonlyStaticBox::$count = EvalReadonlyStaticBox::$count + 1; -return EvalReadonlyStaticBox::$count;"#, +}"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly class static property should fail"); - assert_eq!(values.output, "5:"); - assert_eq!(values.get(result), FakeValue::Int(6)); + assert_eq!(err, EvalStatus::RuntimeFatal); } /// Verifies readonly classes may extend readonly parents and use inherited constructors. diff --git a/docs/php/eval.md b/docs/php/eval.md index 323cf166ca..50fea61872 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -551,9 +551,9 @@ Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties may be assigned from the constructor of the declaring class and later writes fail as eval runtime fatals. -A `readonly class` makes instance properties readonly implicitly while leaving -static properties mutable. Missing-property writes can still dispatch through -`__set()`, but readonly classes reject actual dynamic property creation. +A `readonly class` makes declared properties readonly implicitly, so static +properties are rejected like PHP. Missing-property writes can still dispatch +through `__set()`, but readonly classes reject actual dynamic property creation. PHP's global `#[AllowDynamicProperties]` marker is rejected on eval-declared readonly classes. `self::`, `parent::`, and late-bound `static::` work for supported static diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 91b0364ab8..2a747874d8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7370,15 +7370,13 @@ fn test_eval_declared_readonly_class_rules() { r#"id = $id; } public function id() { return $this->id; } } readonly class EvalReadonlyClassChild extends EvalReadonlyClassBox {} $box = new EvalReadonlyClassBox(7); $child = new EvalReadonlyClassChild(9); -EvalReadonlyClassBox::$count = 5; -echo $box->id() . ":" . EvalReadonlyClassBox::$count . ":" . $child->id();'); +echo $box->id() . ":" . $child->id();'); "#, ); assert!( @@ -7386,7 +7384,19 @@ echo $box->id() . ":" . EvalReadonlyClassBox::$count . ":" . $child->id();'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "7:5:9"); + assert_eq!(out.stdout, "7:9"); + + let static_err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 22:44:22 +0200 Subject: [PATCH 0671/1208] Require readonly eval property types --- .../src/interpreter/statements.rs | 3 ++ .../src/interpreter/tests/classes.rs | 32 +++++++++++++++++++ docs/php/eval.md | 11 ++++--- tests/codegen/eval.rs | 12 +++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index ccdb042879..f8f3e4dc93 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1483,6 +1483,9 @@ fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus if property.is_final() && property.visibility() == EvalVisibility::Private { return Err(EvalStatus::RuntimeFatal); } + if property.is_readonly() && property.property_type().is_none() { + return Err(EvalStatus::RuntimeFatal); + } if property.is_readonly() && property.default().is_some() { return Err(EvalStatus::RuntimeFatal); } diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index ff6017a287..2889b807cf 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -313,6 +313,38 @@ $box->replace(8);"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies readonly eval properties must declare a type like PHP requires. +#[test] +fn execute_program_rejects_untyped_readonly_properties() { + let explicit = parse_fragment( + br#"class EvalReadonlyUntypedBox { + public readonly $value; +}"#, + ) + .expect("parse explicit readonly property"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&explicit, &mut scope, &mut values) + .expect_err("explicit readonly property without type should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let readonly_class = parse_fragment( + br#"readonly class EvalReadonlyClassUntypedBox { + public $value; +}"#, + ) + .expect("parse readonly class property"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&readonly_class, &mut scope, &mut values) + .expect_err("readonly class property without type should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies readonly classes make instance properties readonly implicitly. #[test] fn execute_program_initializes_readonly_class_property_in_constructor() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 50fea61872..af8c520864 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -549,11 +549,12 @@ non-final constant metadata, and the enum-case reflection classes expose the same inherited `IS_*` constants as PHP. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the -raw backing slot. `readonly` eval properties may be assigned from the -constructor of the declaring class and later writes fail as eval runtime fatals. -A `readonly class` makes declared properties readonly implicitly, so static -properties are rejected like PHP. Missing-property writes can still dispatch -through `__set()`, but readonly classes reject actual dynamic property creation. +raw backing slot. `readonly` eval properties require a declared type, may be +assigned from the constructor of the declaring class, and later writes fail as +eval runtime fatals. A `readonly class` makes declared properties readonly +implicitly, so static and untyped properties are rejected like PHP. +Missing-property writes can still dispatch through `__set()`, but readonly +classes reject actual dynamic property creation. PHP's global `#[AllowDynamicProperties]` marker is rejected on eval-declared readonly classes. `self::`, `parent::`, and late-bound `static::` work for supported static diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2a747874d8..0ad1bf7b6d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7398,6 +7398,18 @@ eval('readonly class EvalReadonlyStaticPropertyBox { "stderr did not contain eval runtime fatal diagnostic: {static_err}" ); + let untyped_err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 22:52:22 +0200 Subject: [PATCH 0672/1208] Validate eval trait property conflicts --- .../src/interpreter/statements.rs | 59 +++++++++++----- .../interpreter/tests/trait_adaptations.rs | 69 +++++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 44 ++++++++++++ 4 files changed, 154 insertions(+), 20 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index f8f3e4dc93..4c2f2fb62b 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -993,10 +993,9 @@ fn expand_eval_class_traits( return Ok(class.clone()); } let class_method_names = class_method_name_set(class); - let class_property_names = class_property_name_set(class); let class_constant_names = class_constant_name_set(class); let mut trait_method_names = std::collections::HashSet::new(); - let mut trait_property_names = std::collections::HashSet::new(); + let mut trait_properties = std::collections::HashMap::new(); let mut trait_constant_names = std::collections::HashSet::new(); let mut constants = Vec::new(); let mut properties = Vec::new(); @@ -1013,8 +1012,8 @@ fn expand_eval_class_traits( )?; append_eval_trait_properties( trait_decl, - &class_property_names, - &mut trait_property_names, + class.properties(), + &mut trait_properties, &mut properties, )?; append_eval_trait_methods( @@ -1066,15 +1065,6 @@ fn class_constant_name_set(class: &EvalClass) -> std::collections::HashSet std::collections::HashSet { - class - .properties() - .iter() - .map(|property| property.name().to_string()) - .collect() -} - /// Appends trait constants unless the class provides a same-name constant. fn append_eval_trait_constants( trait_decl: &EvalTrait, @@ -1097,25 +1087,56 @@ fn append_eval_trait_constants( Ok(()) } -/// Appends trait properties unless the class provides a same-name property. +/// Appends trait properties while enforcing PHP-compatible same-name conflicts. fn append_eval_trait_properties( trait_decl: &EvalTrait, - class_property_names: &std::collections::HashSet, - trait_property_names: &mut std::collections::HashSet, + class_properties: &[EvalClassProperty], + trait_properties: &mut std::collections::HashMap, properties: &mut Vec, ) -> Result<(), EvalStatus> { for property in trait_decl.properties() { - if class_property_names.contains(property.name()) { + if let Some(class_property) = class_properties + .iter() + .find(|class_property| class_property.name() == property.name()) + { + validate_eval_trait_property_compatibility(class_property, property)?; continue; } - if !trait_property_names.insert(property.name().to_string()) { - return Err(EvalStatus::RuntimeFatal); + if let Some(existing) = trait_properties.get(property.name()) { + validate_eval_trait_property_compatibility(existing, property)?; + continue; } + trait_properties.insert(property.name().to_string(), property.clone()); properties.push(property.clone()); } Ok(()) } +/// Validates that a same-name trait property definition is compatible with PHP composition. +fn validate_eval_trait_property_compatibility( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> Result<(), EvalStatus> { + if existing.visibility() == incoming.visibility() + && existing.set_visibility() == incoming.set_visibility() + && existing.is_static() == incoming.is_static() + && existing.is_final() == incoming.is_final() + && existing.is_readonly() == incoming.is_readonly() + && existing.is_abstract() == incoming.is_abstract() + && existing.has_get_hook() == incoming.has_get_hook() + && existing.has_set_hook() == incoming.has_set_hook() + && existing.requires_get_hook() == incoming.requires_get_hook() + && existing.requires_set_hook() == incoming.requires_set_hook() + && existing.is_virtual() == incoming.is_virtual() + && existing.property_type() == incoming.property_type() + && existing.default() == incoming.default() + { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + /// Appends trait methods unless the class provides a same-name method. fn append_eval_trait_methods( trait_decl: &EvalTrait, diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index 0f572e0802..98f0ef4662 100644 --- a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -94,3 +94,72 @@ class EvalConflictBox { assert_eq!(err, EvalStatus::RuntimeFatal); } + +/// Verifies compatible same-name trait properties are deduplicated during composition. +#[test] +fn execute_program_allows_compatible_eval_trait_property_conflicts() { + let program = parse_fragment( + br#"trait EvalCompatibleTraitPropA { + public int $value; +} +trait EvalCompatibleTraitPropB { + public int $value; +} +class EvalCompatibleTraitPropBox { + use EvalCompatibleTraitPropA, EvalCompatibleTraitPropB; + public int $value; + public function __construct($value) { $this->value = $value; } +} +$box = new EvalCompatibleTraitPropBox(7); +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies incompatible same-name class and trait properties fail like PHP. +#[test] +fn execute_program_rejects_incompatible_eval_trait_property_conflicts() { + let class_conflict = parse_fragment( + br#"trait EvalClassTraitPropConflict { + public int $value; +} +class EvalClassTraitPropConflictBox { + use EvalClassTraitPropConflict; + public string $value; +}"#, + ) + .expect("parse class/trait property conflict"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&class_conflict, &mut scope, &mut values) + .expect_err("incompatible class/trait property should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let trait_conflict = parse_fragment( + br#"trait EvalTraitPropConflictA { + public int $value; +} +trait EvalTraitPropConflictB { + public string $value; +} +class EvalTraitPropConflictBox { + use EvalTraitPropConflictA, EvalTraitPropConflictB; +}"#, + ) + .expect("parse trait/trait property conflict"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&trait_conflict, &mut scope, &mut values) + .expect_err("incompatible trait/trait property should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/docs/php/eval.md b/docs/php/eval.md index af8c520864..2f11491be6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0ad1bf7b6d..8b2fadf03e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7200,6 +7200,50 @@ class EvalTraitMissingConcrete { ); } +/// Verifies eval-declared trait property conflicts follow PHP compatibility rules. +#[test] +fn test_eval_declared_trait_property_conflict_rules() { + let out = compile_and_run_capture( + r#"value = $value; } +} +$box = new EvalTraitPropCompatBox(9); +echo $box->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "9"); + + let err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 22:59:31 +0200 Subject: [PATCH 0673/1208] Validate eval trait constant conflicts --- .../src/interpreter/statements.rs | 52 ++++++++------ .../src/interpreter/tests/class_constants.rs | 72 +++++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 39 +++++++++- 4 files changed, 140 insertions(+), 25 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 4c2f2fb62b..a4b02cb127 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -993,10 +993,9 @@ fn expand_eval_class_traits( return Ok(class.clone()); } let class_method_names = class_method_name_set(class); - let class_constant_names = class_constant_name_set(class); let mut trait_method_names = std::collections::HashSet::new(); let mut trait_properties = std::collections::HashMap::new(); - let mut trait_constant_names = std::collections::HashSet::new(); + let mut trait_constants = std::collections::HashMap::new(); let mut constants = Vec::new(); let mut properties = Vec::new(); let mut methods = Vec::new(); @@ -1006,8 +1005,8 @@ fn expand_eval_class_traits( }; append_eval_trait_constants( trait_decl, - &class_constant_names, - &mut trait_constant_names, + class.constants(), + &mut trait_constants, &mut constants, )?; append_eval_trait_properties( @@ -1056,37 +1055,46 @@ fn class_method_name_set(class: &EvalClass) -> std::collections::HashSet .collect() } -/// Returns constant names declared directly by a pending class. -fn class_constant_name_set(class: &EvalClass) -> std::collections::HashSet { - class - .constants() - .iter() - .map(|constant| constant.name().to_string()) - .collect() -} - -/// Appends trait constants unless the class provides a same-name constant. +/// Appends trait constants while enforcing PHP-compatible same-name conflicts. fn append_eval_trait_constants( trait_decl: &EvalTrait, - class_constant_names: &std::collections::HashSet, - trait_constant_names: &mut std::collections::HashSet, + class_constants: &[EvalClassConstant], + trait_constants: &mut std::collections::HashMap, constants: &mut Vec, ) -> Result<(), EvalStatus> { for constant in trait_decl.constants() { - if class_constant_names.contains(constant.name()) { - if constant.is_final() { - return Err(EvalStatus::RuntimeFatal); - } + if let Some(class_constant) = class_constants + .iter() + .find(|class_constant| class_constant.name() == constant.name()) + { + validate_eval_trait_constant_compatibility(class_constant, constant)?; continue; } - if !trait_constant_names.insert(constant.name().to_string()) { - return Err(EvalStatus::RuntimeFatal); + if let Some(existing) = trait_constants.get(constant.name()) { + validate_eval_trait_constant_compatibility(existing, constant)?; + continue; } + trait_constants.insert(constant.name().to_string(), constant.clone()); constants.push(constant.clone()); } Ok(()) } +/// Validates that a same-name trait constant definition is compatible with PHP composition. +fn validate_eval_trait_constant_compatibility( + existing: &EvalClassConstant, + incoming: &EvalClassConstant, +) -> Result<(), EvalStatus> { + if existing.visibility() == incoming.visibility() + && existing.is_final() == incoming.is_final() + && existing.value() == incoming.value() + { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + /// Appends trait properties while enforcing PHP-compatible same-name conflicts. fn append_eval_trait_properties( trait_decl: &EvalTrait, diff --git a/crates/elephc-magician/src/interpreter/tests/class_constants.rs b/crates/elephc-magician/src/interpreter/tests/class_constants.rs index 1c035d86eb..0277042d39 100644 --- a/crates/elephc-magician/src/interpreter/tests/class_constants.rs +++ b/crates/elephc-magician/src/interpreter/tests/class_constants.rs @@ -338,3 +338,75 @@ return EvalConstTraitBox::readTraitSeed();"#, assert_eq!(values.output, "6:6:"); assert_eq!(values.get(result), FakeValue::Int(6)); } + +/// Verifies compatible same-name trait constants are deduplicated during composition. +#[test] +fn execute_program_allows_compatible_eval_trait_constant_conflicts() { + let program = parse_fragment( + br#"trait EvalConstCompatibleA { + public const SEED = 6; +} +trait EvalConstCompatibleB { + public const SEED = 6; +} +class EvalConstCompatibleTraitBox { + use EvalConstCompatibleA, EvalConstCompatibleB; +} +class EvalConstCompatibleClassBox { + use EvalConstCompatibleA; + public const SEED = 6; +} +echo EvalConstCompatibleTraitBox::SEED; echo ":"; +return EvalConstCompatibleClassBox::SEED;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "6:"); + assert_eq!(values.get(result), FakeValue::Int(6)); +} + +/// Verifies incompatible same-name class and trait constants fail like PHP. +#[test] +fn execute_program_rejects_incompatible_eval_trait_constant_conflicts() { + let class_conflict = parse_fragment( + br#"trait EvalConstClassTraitConflict { + public const SEED = 6; +} +class EvalConstClassTraitConflictBox { + use EvalConstClassTraitConflict; + public const SEED = 7; +}"#, + ) + .expect("parse class/trait constant conflict"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&class_conflict, &mut scope, &mut values) + .expect_err("incompatible class/trait constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let trait_conflict = parse_fragment( + br#"trait EvalConstTraitConflictA { + public const SEED = 6; +} +trait EvalConstTraitConflictB { + public const SEED = 7; +} +class EvalConstTraitConflictBox { + use EvalConstTraitConflictA, EvalConstTraitConflictB; +}"#, + ) + .expect("parse trait/trait constant conflict"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&trait_conflict, &mut scope, &mut values) + .expect_err("incompatible trait/trait constant should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/docs/php/eval.md b/docs/php/eval.md index 2f11491be6..87dd4152b5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8b2fadf03e..1ebe3f2558 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -13723,16 +13723,31 @@ trait EvalConstReusableTrait { return self::SEED; } } +trait EvalConstDuplicateA { + public const DUP = 9; +} +trait EvalConstDuplicateB { + public const DUP = 9; +} class EvalConstIfaceTraitBox implements EvalConstChildIface { use EvalConstReusableTrait; } +class EvalConstDuplicateTraitBox { + use EvalConstDuplicateA, EvalConstDuplicateB; +} +class EvalConstDuplicateClassBox { + use EvalConstDuplicateA; + public const DUP = 9; +} echo EvalConstParentIface::BASE . ":"; echo EvalConstChildIface::BASE . ":"; echo EvalConstIfaceTraitBox::BASE . ":"; echo EvalConstIfaceTraitBox::LOCAL . ":"; echo EvalConstReusableTrait::SEED . ":"; echo EvalConstIfaceTraitBox::SEED . ":"; -echo EvalConstIfaceTraitBox::readTraitSeed();'); +echo EvalConstIfaceTraitBox::readTraitSeed() . ":"; +echo EvalConstDuplicateTraitBox::DUP . ":"; +echo EvalConstDuplicateClassBox::DUP;'); "#, ); assert!( @@ -13740,7 +13755,27 @@ echo EvalConstIfaceTraitBox::readTraitSeed();'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "2:2:2:3:6:6:6"); + assert_eq!(out.stdout, "2:2:2:3:6:6:6:9:9"); +} + +/// Verifies eval-declared trait constant conflicts follow PHP compatibility rules. +#[test] +fn test_eval_declared_trait_constant_conflict_rules() { + let err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 23:05:42 +0200 Subject: [PATCH 0674/1208] Align eval trait alias collisions --- .../src/interpreter/statements.rs | 11 +++- .../interpreter/tests/trait_adaptations.rs | 56 +++++++++++++++++++ tests/codegen/eval.rs | 50 +++++++++++++++++ 3 files changed, 116 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index a4b02cb127..2081131ef4 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1219,7 +1219,16 @@ fn append_eval_trait_method_aliases( alias_method = alias_method.with_visibility_override(*visibility); } let key = alias_method.name().to_ascii_lowercase(); - if class_method_names.contains(&key) || !trait_method_names.insert(key) { + if class_method_names.contains(&key) { + continue; + } + if trait_method_names.contains(&key) + && source_method.name().eq_ignore_ascii_case(alias) + && alias_method.visibility() == source_method.visibility() + { + continue; + } + if !trait_method_names.insert(key) { return Err(EvalStatus::RuntimeFatal); } methods.push(alias_method); diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index 98f0ef4662..9f5f08987f 100644 --- a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -71,6 +71,62 @@ return $box->hidden();"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies trait aliases that collide with class methods or no-op names follow PHP rules. +#[test] +fn execute_program_applies_eval_trait_alias_collision_rules() { + let program = parse_fragment( + br#"trait EvalAliasSource { + public function source() { return "T"; } +} +class EvalAliasClassCollisionBox { + use EvalAliasSource { + source as target; + } + public function target() { return "C"; } + public function read() { return $this->source() . $this->target(); } +} +class EvalAliasNoopBox { + use EvalAliasSource { + source as source; + } +} +$box = new EvalAliasClassCollisionBox(); +echo $box->read(); echo ":"; +return (new EvalAliasNoopBox())->source();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "TC:"); + assert_eq!(values.get(result), FakeValue::String("T".to_string())); +} + +/// Verifies same-name trait aliases that change visibility remain composition fatals. +#[test] +fn execute_program_rejects_eval_trait_same_name_visibility_alias() { + let program = parse_fragment( + br#"trait EvalAliasVisibilitySource { + public function source() { return "T"; } +} +class EvalAliasVisibilityBox { + use EvalAliasVisibilitySource { + source as private source; + } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("same-name trait alias with visibility change should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies unresolved same-name trait methods remain a declaration-time fatal. #[test] fn execute_program_rejects_unresolved_eval_trait_method_conflict() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1ebe3f2558..6f712534a4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7181,6 +7181,56 @@ echo $box->hidden();'); ); } +/// Verifies eval-declared trait aliases follow PHP collision and no-op rules. +#[test] +fn test_eval_declared_trait_alias_collision_rules() { + let out = compile_and_run_capture( + r#"source() . $this->target(); } +} +class EvalAliasNoopBox { + use EvalAliasSource { + source as source; + } +} +$box = new EvalAliasClassCollisionBox(); +echo $box->read() . ":"; +echo (new EvalAliasNoopBox())->source();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "TC:T"); + + let err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 23:11:30 +0200 Subject: [PATCH 0675/1208] Validate eval trait adaptation targets --- .../src/interpreter/statements.rs | 79 +++++++++++++++++++ .../interpreter/tests/trait_adaptations.rs | 61 ++++++++++++++ tests/codegen/eval.rs | 21 +++++ 3 files changed, 161 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 2081131ef4..f782841268 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -992,6 +992,7 @@ fn expand_eval_class_traits( if class.traits().is_empty() { return Ok(class.clone()); } + validate_eval_trait_adaptations(class, context)?; let class_method_names = class_method_name_set(class); let mut trait_method_names = std::collections::HashSet::new(); let mut trait_properties = std::collections::HashMap::new(); @@ -1046,6 +1047,84 @@ fn expand_eval_class_traits( Ok(expanded) } +/// Validates that trait adaptations reference used traits and existing methods. +fn validate_eval_trait_adaptations( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for adaptation in class.trait_adaptations() { + match adaptation { + EvalTraitAdaptation::Alias { + trait_name, method, .. + } => { + validate_eval_trait_adaptation_method(class, context, trait_name.as_deref(), method)? + } + EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + } => { + let Some(trait_name) = trait_name.as_deref() else { + return Err(EvalStatus::RuntimeFatal); + }; + validate_eval_trait_adaptation_method(class, context, Some(trait_name), method)?; + for suppressed in instead_of { + if eval_used_trait_decl(class, context, suppressed).is_none() { + return Err(EvalStatus::RuntimeFatal); + } + } + } + } + } + Ok(()) +} + +/// Validates one adaptation method target, allowing unqualified alias targets. +fn validate_eval_trait_adaptation_method( + class: &EvalClass, + context: &ElephcEvalContext, + trait_name: Option<&str>, + method: &str, +) -> Result<(), EvalStatus> { + if let Some(trait_name) = trait_name { + let Some(trait_decl) = eval_used_trait_decl(class, context, trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + return trait_has_method(trait_decl, method) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal); + } + class + .traits() + .iter() + .filter_map(|trait_name| context.trait_decl(trait_name)) + .any(|trait_decl| trait_has_method(trait_decl, method)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns a trait declaration only when the pending class directly uses that trait. +fn eval_used_trait_decl<'a>( + class: &EvalClass, + context: &'a ElephcEvalContext, + trait_name: &str, +) -> Option<&'a EvalTrait> { + class + .traits() + .iter() + .any(|used_trait| same_eval_class_name(used_trait, trait_name)) + .then(|| context.trait_decl(trait_name)) + .flatten() +} + +/// Returns whether a trait declares a method by PHP case-insensitive method name. +fn trait_has_method(trait_decl: &EvalTrait, method: &str) -> bool { + trait_decl + .methods() + .iter() + .any(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) +} + /// Returns case-insensitive method names declared directly by a pending class. fn class_method_name_set(class: &EvalClass) -> std::collections::HashSet { class diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index 9f5f08987f..cf9a2f2c5c 100644 --- a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -127,6 +127,67 @@ class EvalAliasVisibilityBox { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies trait adaptations reject missing trait or method targets. +#[test] +fn execute_program_rejects_invalid_eval_trait_adaptation_targets() { + let missing_method = parse_fragment( + br#"trait EvalAdaptMissingMethod { + public function source() { return "T"; } +} +class EvalAdaptMissingMethodBox { + use EvalAdaptMissingMethod { + EvalAdaptMissingMethod::missing insteadof EvalAdaptMissingMethod; + } +}"#, + ) + .expect("parse missing method adaptation"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&missing_method, &mut scope, &mut values) + .expect_err("missing adaptation method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let missing_trait = parse_fragment( + br#"trait EvalAdaptMissingTrait { + public function source() { return "T"; } +} +class EvalAdaptMissingTraitBox { + use EvalAdaptMissingTrait { + EvalAdaptMissingTrait::source insteadof EvalAdaptNotUsedTrait; + } +}"#, + ) + .expect("parse missing trait adaptation"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&missing_trait, &mut scope, &mut values) + .expect_err("missing adaptation trait should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let missing_unqualified_alias = parse_fragment( + br#"trait EvalAdaptMissingAlias { + public function source() { return "T"; } +} +class EvalAdaptMissingAliasBox { + use EvalAdaptMissingAlias { + missing as alias; + } +}"#, + ) + .expect("parse missing unqualified alias"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&missing_unqualified_alias, &mut scope, &mut values) + .expect_err("missing unqualified alias method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies unresolved same-name trait methods remain a declaration-time fatal. #[test] fn execute_program_rejects_unresolved_eval_trait_method_conflict() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6f712534a4..89d532378b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7231,6 +7231,27 @@ class EvalAliasVisibilityBox { ); } +/// Verifies eval-declared trait adaptations reject missing method targets. +#[test] +fn test_eval_declared_invalid_trait_adaptation_target_fails() { + let err = compile_and_run_expect_failure( + r#" Date: Thu, 25 Jun 2026 23:17:53 +0200 Subject: [PATCH 0676/1208] Validate eval serialization magic methods --- .../src/interpreter/statements.rs | 19 +++++++++++++++++- .../src/interpreter/tests/classes.rs | 20 +++++++++++++++++++ docs/php/eval.md | 5 +++-- tests/codegen/eval.rs | 13 ++++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index f782841268..fcdb9c0af5 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1465,6 +1465,21 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus validate_magic_public(method)?; validate_magic_arity(method, 2)?; } + "__sleep" | "__serialize" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::Array)?; + } + "__wakeup" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } + "__unserialize" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 1)?; + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } "__invoke" => { validate_magic_non_static(method)?; validate_magic_public(method)?; @@ -1489,6 +1504,7 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus /// Magic method return types that eval can validate from retained declarations. #[derive(Clone, Copy)] enum MagicReturnType { + Array, Bool, String, Void, @@ -1561,7 +1577,8 @@ fn magic_return_type_matches( }; matches!( (expected, variant), - (MagicReturnType::Bool, EvalParameterTypeVariant::Bool) + (MagicReturnType::Array, EvalParameterTypeVariant::Array) + | (MagicReturnType::Bool, EvalParameterTypeVariant::Bool) | (MagicReturnType::String, EvalParameterTypeVariant::String) | (MagicReturnType::Void, EvalParameterTypeVariant::Void) ) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 2889b807cf..dfc54cb18c 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1260,6 +1260,26 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadCallStatic { public function __callStatic($name, $args) { return "x"; } }"#.as_slice(), "instance __callStatic", ), + ( + br#"class EvalBadSleepReturn { public function __sleep(): string { return "x"; } }"#.as_slice(), + "bad __sleep return type", + ), + ( + br#"class EvalBadSerializeStatic { public static function __serialize(): array { return []; } }"#.as_slice(), + "static __serialize", + ), + ( + br#"class EvalBadWakeupArity { public function __wakeup($value): void {} }"#.as_slice(), + "bad __wakeup arity", + ), + ( + br#"class EvalBadUnserializeArity { public function __unserialize(): void {} }"#.as_slice(), + "bad __unserialize arity", + ), + ( + br#"class EvalBadUnserializeReturn { public function __unserialize(array $data): int { return 1; } }"#.as_slice(), + "bad __unserialize return type", + ), ( br#"class EvalBadInvoke { private function __invoke() { return 1; } }"#.as_slice(), "private __invoke", diff --git a/docs/php/eval.md b/docs/php/eval.md index 87dd4152b5..fd87813cd3 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -181,8 +181,9 @@ callable `strval()` dispatch, and weak `string` parameter coercion. Classes with a compatible `__toString()` satisfy `Stringable` implicitly. Eval validates magic method staticness, visibility, arity, and relevant declared return-type contracts for `__toString()`, `__get()`, `__set()`, `__isset()`, `__unset()`, `__call()`, -`__callStatic()`, `__invoke()`, `__clone()`, `__destruct()`, and `__construct()` -when dynamic classes or traits are declared. +`__callStatic()`, `__sleep()`, `__wakeup()`, `__serialize()`, +`__unserialize()`, `__invoke()`, `__clone()`, `__destruct()`, and +`__construct()` when dynamic classes or traits are declared. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 89d532378b..e1a3deb902 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8283,6 +8283,19 @@ fn test_eval_rejects_invalid_magic_method_contracts() { public function __unset($name): int { return 1; } +}');"#, + r#"eval('class EvalInvalidSleepReturn { + public function __sleep(): string { + return "x"; + } +}');"#, + r#"eval('class EvalInvalidSerializeStatic { + public static function __serialize(): array { + return []; + } +}');"#, + r#"eval('class EvalInvalidUnserializeArity { + public function __unserialize(): void {} }');"#, r#"eval('class EvalInvalidCloneReturn { public function __clone(): int {} From d9d5f56024afb545067c5503b547c702095eb75a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 23:24:54 +0200 Subject: [PATCH 0677/1208] Validate eval debug magic methods --- .../src/interpreter/statements.rs | 16 +++++++- .../src/interpreter/tests/classes.rs | 37 +++++++++++++++++++ docs/php/eval.md | 5 ++- tests/codegen/eval.rs | 8 ++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index fcdb9c0af5..7f4fed6785 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1480,6 +1480,15 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus validate_magic_arity(method, 1)?; validate_magic_declared_return_type(method, MagicReturnType::Void)?; } + "__debuginfo" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::NullableArray)?; + } + "__set_state" => { + validate_magic_static(method)?; + validate_magic_arity(method, 1)?; + } "__invoke" => { validate_magic_non_static(method)?; validate_magic_public(method)?; @@ -1506,6 +1515,7 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus enum MagicReturnType { Array, Bool, + NullableArray, String, Void, } @@ -1569,7 +1579,10 @@ fn magic_return_type_matches( return_type: &EvalParameterType, expected: MagicReturnType, ) -> bool { - if return_type.allows_null() || return_type.is_intersection() { + if return_type.is_intersection() { + return false; + } + if return_type.allows_null() && !matches!(expected, MagicReturnType::NullableArray) { return false; } let [variant] = return_type.variants() else { @@ -1579,6 +1592,7 @@ fn magic_return_type_matches( (expected, variant), (MagicReturnType::Array, EvalParameterTypeVariant::Array) | (MagicReturnType::Bool, EvalParameterTypeVariant::Bool) + | (MagicReturnType::NullableArray, EvalParameterTypeVariant::Array) | (MagicReturnType::String, EvalParameterTypeVariant::String) | (MagicReturnType::Void, EvalParameterTypeVariant::Void) ) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index dfc54cb18c..92fe9f7a57 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1280,6 +1280,22 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadUnserializeReturn { public function __unserialize(array $data): int { return 1; } }"#.as_slice(), "bad __unserialize return type", ), + ( + br#"class EvalBadDebugInfoReturn { public function __debugInfo(): string { return "x"; } }"#.as_slice(), + "bad __debugInfo return type", + ), + ( + br#"class EvalBadDebugInfoStatic { public static function __debugInfo(): array { return []; } }"#.as_slice(), + "static __debugInfo", + ), + ( + br#"class EvalBadSetStateInstance { public function __set_state($data) {} }"#.as_slice(), + "instance __set_state", + ), + ( + br#"class EvalBadSetStateArity { public static function __set_state($data, $extra) {} }"#.as_slice(), + "bad __set_state arity", + ), ( br#"class EvalBadInvoke { private function __invoke() { return 1; } }"#.as_slice(), "private __invoke", @@ -1311,6 +1327,27 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { } } +/// Verifies eval accepts PHP-compatible debug and set-state magic method contracts. +#[test] +fn execute_program_accepts_debug_and_set_state_magic_contracts() { + let program = parse_fragment( + br#"class EvalGoodDebugInfoMagic { + public function __debugInfo(): ?array { return null; } +} +class EvalGoodSetStateMagic { + public static function __set_state($data) {} +} +return class_exists("EvalGoodDebugInfoMagic") && class_exists("EvalGoodSetStateMagic");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies get-only property hooks reject writes outside a set accessor. #[test] fn execute_program_rejects_write_to_get_only_eval_property_hook() { diff --git a/docs/php/eval.md b/docs/php/eval.md index fd87813cd3..25d644d53c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -182,8 +182,9 @@ with a compatible `__toString()` satisfy `Stringable` implicitly. Eval validates magic method staticness, visibility, arity, and relevant declared return-type contracts for `__toString()`, `__get()`, `__set()`, `__isset()`, `__unset()`, `__call()`, `__callStatic()`, `__sleep()`, `__wakeup()`, `__serialize()`, -`__unserialize()`, `__invoke()`, `__clone()`, `__destruct()`, and -`__construct()` when dynamic classes or traits are declared. +`__unserialize()`, `__debugInfo()`, `__set_state()`, `__invoke()`, +`__clone()`, `__destruct()`, and `__construct()` when dynamic classes or traits +are declared. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e1a3deb902..ae82607ac3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8296,6 +8296,14 @@ fn test_eval_rejects_invalid_magic_method_contracts() { }');"#, r#"eval('class EvalInvalidUnserializeArity { public function __unserialize(): void {} +}');"#, + r#"eval('class EvalInvalidDebugInfoReturn { + public function __debugInfo(): string { + return "x"; + } +}');"#, + r#"eval('class EvalInvalidSetStateInstance { + public function __set_state($data) {} }');"#, r#"eval('class EvalInvalidCloneReturn { public function __clone(): int {} From 934eea71a16415fc7b40b25bafc5ec55008e6457 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 23:29:37 +0200 Subject: [PATCH 0678/1208] Validate eval set magic return type --- crates/elephc-magician/src/interpreter/statements.rs | 3 +++ crates/elephc-magician/src/interpreter/tests/classes.rs | 4 ++++ tests/codegen/eval.rs | 5 +++++ 3 files changed, 12 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 7f4fed6785..5a96f83632 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1459,6 +1459,9 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus validate_magic_non_static(method)?; validate_magic_public(method)?; validate_magic_arity(method, 2)?; + if method.name().eq_ignore_ascii_case("__set") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } } "__callstatic" => { validate_magic_static(method)?; diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 92fe9f7a57..5f55983189 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1252,6 +1252,10 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadUnsetReturn { public function __unset($name): int { return 1; } }"#.as_slice(), "bad __unset return type", ), + ( + br#"class EvalBadSetReturn { public function __set($name, $value): int { return 1; } }"#.as_slice(), + "bad __set return type", + ), ( br#"class EvalBadCall { public function __call($name, ...$args) { return "x"; } }"#.as_slice(), "variadic __call", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ae82607ac3..3a742bd5e5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8283,6 +8283,11 @@ fn test_eval_rejects_invalid_magic_method_contracts() { public function __unset($name): int { return 1; } +}');"#, + r#"eval('class EvalInvalidSetReturn { + public function __set($name, $value): int { + return 1; + } }');"#, r#"eval('class EvalInvalidSleepReturn { public function __sleep(): string { From 01d74c8c6264746ab380ac77c00c2a2664c19b30 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 23:34:45 +0200 Subject: [PATCH 0679/1208] Reject eval constructor return types --- crates/elephc-magician/src/interpreter/statements.rs | 12 ++++++++++++ .../elephc-magician/src/interpreter/tests/classes.rs | 8 ++++++++ tests/codegen/eval.rs | 6 ++++++ 3 files changed, 26 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 5a96f83632..74130ee9a8 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1501,12 +1501,15 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus validate_magic_arity(method, 0)?; if method.name().eq_ignore_ascii_case("__clone") { validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } else { + validate_magic_no_declared_return_type(method)?; } } "__construct" => { if method.is_static() { return Err(EvalStatus::RuntimeFatal); } + validate_magic_no_declared_return_type(method)?; } _ => {} } @@ -1563,6 +1566,15 @@ fn validate_magic_arity(method: &EvalClassMethod, expected: usize) -> Result<(), } } +/// Rejects PHP magic methods that cannot declare any return type. +fn validate_magic_no_declared_return_type(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.return_type().is_some() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + /// Rejects incompatible explicit return types on PHP magic methods. fn validate_magic_declared_return_type( method: &EvalClassMethod, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 5f55983189..8bcea529d3 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1316,6 +1316,14 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadDestruct { public static function __destruct() {} }"#.as_slice(), "static __destruct", ), + ( + br#"class EvalBadConstructReturn { public function __construct(): void {} }"#.as_slice(), + "bad __construct return type", + ), + ( + br#"class EvalBadDestructReturn { public function __destruct(): void {} }"#.as_slice(), + "bad __destruct return type", + ), ( br#"trait EvalBadMagicTrait { public static function __isset($name) { return true; } }"#.as_slice(), "trait static __isset", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3a742bd5e5..9212295816 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8312,6 +8312,12 @@ fn test_eval_rejects_invalid_magic_method_contracts() { }');"#, r#"eval('class EvalInvalidCloneReturn { public function __clone(): int {} +}');"#, + r#"eval('class EvalInvalidConstructReturn { + public function __construct(): void {} +}');"#, + r#"eval('class EvalInvalidDestructReturn { + public function __destruct(): void {} }');"#, ]; for source in cases { From 6b3da3ac5a245d7ebbdbcfec0d52053aa17456f4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 23:41:11 +0200 Subject: [PATCH 0680/1208] Reject by-ref eval magic parameters --- .../src/interpreter/statements.rs | 43 ++++++++++++++++++- .../src/interpreter/tests/classes.rs | 4 ++ docs/php/eval.md | 3 +- tests/codegen/eval.rs | 5 +++ 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 74130ee9a8..47ee69b84d 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1438,7 +1438,11 @@ fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalSt /// Validates staticness, visibility, arity, and declared return type for one eval magic method. fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus> { - match method.name().to_ascii_lowercase().as_str() { + let name = method.name().to_ascii_lowercase(); + if is_validated_eval_magic_method(&name) { + validate_magic_no_by_ref_params(method)?; + } + match name.as_str() { "__tostring" => { validate_magic_non_static(method)?; validate_magic_public(method)?; @@ -1516,6 +1520,30 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus Ok(()) } +/// Returns whether eval knows PHP declaration-time rules for this magic method. +fn is_validated_eval_magic_method(name: &str) -> bool { + matches!( + name, + "__tostring" + | "__get" + | "__isset" + | "__unset" + | "__set" + | "__call" + | "__callstatic" + | "__sleep" + | "__serialize" + | "__wakeup" + | "__unserialize" + | "__debuginfo" + | "__set_state" + | "__invoke" + | "__clone" + | "__destruct" + | "__construct" + ) +} + /// Magic method return types that eval can validate from retained declarations. #[derive(Clone, Copy)] enum MagicReturnType { @@ -1566,6 +1594,19 @@ fn validate_magic_arity(method: &EvalClassMethod, expected: usize) -> Result<(), } } +/// Rejects by-reference parameters on PHP magic methods. +fn validate_magic_no_by_ref_params(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method + .parameter_is_by_ref() + .iter() + .any(|is_by_ref| *is_by_ref) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + /// Rejects PHP magic methods that cannot declare any return type. fn validate_magic_no_declared_return_type(method: &EvalClassMethod) -> Result<(), EvalStatus> { if method.return_type().is_some() { diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 8bcea529d3..af63a794fa 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1244,6 +1244,10 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadGet { protected function __get($name) { return "x"; } }"#.as_slice(), "protected __get", ), + ( + br#"class EvalBadGetByRef { public function __get(&$name) { return "x"; } }"#.as_slice(), + "by-ref __get", + ), ( br#"class EvalBadIssetReturn { public function __isset($name): string { return "yes"; } }"#.as_slice(), "bad __isset return type", diff --git a/docs/php/eval.md b/docs/php/eval.md index 25d644d53c..2e92e17083 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -179,7 +179,8 @@ Eval-declared objects in string contexts dispatch through public parameterless `__toString()` for `echo`, `print`, concatenation, `strval()`, callable `strval()` dispatch, and weak `string` parameter coercion. Classes with a compatible `__toString()` satisfy `Stringable` implicitly. -Eval validates magic method staticness, visibility, arity, and relevant declared return-type contracts for +Eval validates magic method staticness, visibility, arity, by-reference +parameter bans, and relevant declared return-type contracts for `__toString()`, `__get()`, `__set()`, `__isset()`, `__unset()`, `__call()`, `__callStatic()`, `__sleep()`, `__wakeup()`, `__serialize()`, `__unserialize()`, `__debugInfo()`, `__set_state()`, `__invoke()`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9212295816..4125f81c79 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8278,6 +8278,11 @@ fn test_eval_rejects_invalid_magic_method_contracts() { public function __isset($name): string { return "yes"; } +}');"#, + r#"eval('class EvalInvalidGetByRef { + public function __get(&$name) { + return "x"; + } }');"#, r#"eval('class EvalInvalidUnsetReturn { public function __unset($name): int { From 762a3f46d62d1846fcab186727c5ed91e87c493b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 23:47:55 +0200 Subject: [PATCH 0681/1208] Validate eval magic parameter types --- .../src/interpreter/statements.rs | 41 +++++++++++++++++++ .../src/interpreter/tests/classes.rs | 16 ++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 3 ++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 47ee69b84d..ec6398cb0e 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1465,12 +1465,15 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus validate_magic_arity(method, 2)?; if method.name().eq_ignore_ascii_case("__set") { validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } else { + validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; } } "__callstatic" => { validate_magic_static(method)?; validate_magic_public(method)?; validate_magic_arity(method, 2)?; + validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; } "__sleep" | "__serialize" => { validate_magic_non_static(method)?; @@ -1485,6 +1488,7 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus "__unserialize" => { validate_magic_non_static(method)?; validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::Array)?; validate_magic_declared_return_type(method, MagicReturnType::Void)?; } "__debuginfo" => { @@ -1495,6 +1499,7 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus "__set_state" => { validate_magic_static(method)?; validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::Array)?; } "__invoke" => { validate_magic_non_static(method)?; @@ -1554,6 +1559,12 @@ enum MagicReturnType { Void, } +/// Magic method parameter types that eval can validate from retained declarations. +#[derive(Clone, Copy)] +enum MagicParamType { + Array, +} + /// Rejects static declarations for magic methods that must be instance methods. fn validate_magic_non_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { if method.is_static() { @@ -1607,6 +1618,36 @@ fn validate_magic_no_by_ref_params(method: &EvalClassMethod) -> Result<(), EvalS } } +/// Rejects incompatible explicit parameter types on PHP magic methods. +fn validate_magic_declared_param_type( + method: &EvalClassMethod, + position: usize, + expected: MagicParamType, +) -> Result<(), EvalStatus> { + let Some(Some(parameter_type)) = method.parameter_types().get(position) else { + return Ok(()); + }; + if magic_param_type_matches(parameter_type, expected) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether one retained eval parameter type is exactly the expected magic atom. +fn magic_param_type_matches( + parameter_type: &EvalParameterType, + expected: MagicParamType, +) -> bool { + if parameter_type.allows_null() || parameter_type.is_intersection() { + return false; + } + let [variant] = parameter_type.variants() else { + return false; + }; + matches!((expected, variant), (MagicParamType::Array, EvalParameterTypeVariant::Array)) +} + /// Rejects PHP magic methods that cannot declare any return type. fn validate_magic_no_declared_return_type(method: &EvalClassMethod) -> Result<(), EvalStatus> { if method.return_type().is_some() { diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index af63a794fa..69899e114c 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1264,10 +1264,18 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadCall { public function __call($name, ...$args) { return "x"; } }"#.as_slice(), "variadic __call", ), + ( + br#"class EvalBadCallArgsType { public function __call(string $name, string $args) {} }"#.as_slice(), + "bad __call args type", + ), ( br#"class EvalBadCallStatic { public function __callStatic($name, $args) { return "x"; } }"#.as_slice(), "instance __callStatic", ), + ( + br#"class EvalBadCallStaticArgsType { public static function __callStatic(string $name, string $args) {} }"#.as_slice(), + "bad __callStatic args type", + ), ( br#"class EvalBadSleepReturn { public function __sleep(): string { return "x"; } }"#.as_slice(), "bad __sleep return type", @@ -1288,6 +1296,10 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadUnserializeReturn { public function __unserialize(array $data): int { return 1; } }"#.as_slice(), "bad __unserialize return type", ), + ( + br#"class EvalBadUnserializeParam { public function __unserialize(string $data): void {} }"#.as_slice(), + "bad __unserialize parameter type", + ), ( br#"class EvalBadDebugInfoReturn { public function __debugInfo(): string { return "x"; } }"#.as_slice(), "bad __debugInfo return type", @@ -1304,6 +1316,10 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadSetStateArity { public static function __set_state($data, $extra) {} }"#.as_slice(), "bad __set_state arity", ), + ( + br#"class EvalBadSetStateParam { public static function __set_state(string $data) {} }"#.as_slice(), + "bad __set_state parameter type", + ), ( br#"class EvalBadInvoke { private function __invoke() { return 1; } }"#.as_slice(), "private __invoke", diff --git a/docs/php/eval.md b/docs/php/eval.md index 2e92e17083..47ffeae6ac 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -180,7 +180,7 @@ parameterless `__toString()` for `echo`, `print`, concatenation, `strval()`, callable `strval()` dispatch, and weak `string` parameter coercion. Classes with a compatible `__toString()` satisfy `Stringable` implicitly. Eval validates magic method staticness, visibility, arity, by-reference -parameter bans, and relevant declared return-type contracts for +parameter bans, and relevant declared parameter/return-type contracts for `__toString()`, `__get()`, `__set()`, `__isset()`, `__unset()`, `__call()`, `__callStatic()`, `__sleep()`, `__wakeup()`, `__serialize()`, `__unserialize()`, `__debugInfo()`, `__set_state()`, `__invoke()`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4125f81c79..9d131276c4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8293,6 +8293,9 @@ fn test_eval_rejects_invalid_magic_method_contracts() { public function __set($name, $value): int { return 1; } +}');"#, + r#"eval('class EvalInvalidCallArgsType { + public function __call(string $name, string $args) {} }');"#, r#"eval('class EvalInvalidSleepReturn { public function __sleep(): string { From ed4be32dffcc46673d4447492717c8d67dc53cc8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 25 Jun 2026 23:54:29 +0200 Subject: [PATCH 0682/1208] Validate eval magic string parameters --- crates/elephc-magician/src/interpreter/statements.rs | 10 +++++++++- .../elephc-magician/src/interpreter/tests/classes.rs | 12 ++++++++++++ tests/codegen/eval.rs | 5 +++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index ec6398cb0e..114380fbc4 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1453,6 +1453,7 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus validate_magic_non_static(method)?; validate_magic_public(method)?; validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; if method.name().eq_ignore_ascii_case("__isset") { validate_magic_declared_return_type(method, MagicReturnType::Bool)?; } else if method.name().eq_ignore_ascii_case("__unset") { @@ -1463,6 +1464,7 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus validate_magic_non_static(method)?; validate_magic_public(method)?; validate_magic_arity(method, 2)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; if method.name().eq_ignore_ascii_case("__set") { validate_magic_declared_return_type(method, MagicReturnType::Void)?; } else { @@ -1473,6 +1475,7 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus validate_magic_static(method)?; validate_magic_public(method)?; validate_magic_arity(method, 2)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; } "__sleep" | "__serialize" => { @@ -1563,6 +1566,7 @@ enum MagicReturnType { #[derive(Clone, Copy)] enum MagicParamType { Array, + String, } /// Rejects static declarations for magic methods that must be instance methods. @@ -1645,7 +1649,11 @@ fn magic_param_type_matches( let [variant] = parameter_type.variants() else { return false; }; - matches!((expected, variant), (MagicParamType::Array, EvalParameterTypeVariant::Array)) + matches!( + (expected, variant), + (MagicParamType::Array, EvalParameterTypeVariant::Array) + | (MagicParamType::String, EvalParameterTypeVariant::String) + ) } /// Rejects PHP magic methods that cannot declare any return type. diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 69899e114c..b12280450e 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1248,6 +1248,10 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadGetByRef { public function __get(&$name) { return "x"; } }"#.as_slice(), "by-ref __get", ), + ( + br#"class EvalBadGetParamType { public function __get(int $name) { return "x"; } }"#.as_slice(), + "bad __get parameter type", + ), ( br#"class EvalBadIssetReturn { public function __isset($name): string { return "yes"; } }"#.as_slice(), "bad __isset return type", @@ -1260,6 +1264,10 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadSetReturn { public function __set($name, $value): int { return 1; } }"#.as_slice(), "bad __set return type", ), + ( + br#"class EvalBadSetParamType { public function __set(int $name, $value): void {} }"#.as_slice(), + "bad __set parameter type", + ), ( br#"class EvalBadCall { public function __call($name, ...$args) { return "x"; } }"#.as_slice(), "variadic __call", @@ -1268,6 +1276,10 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadCallArgsType { public function __call(string $name, string $args) {} }"#.as_slice(), "bad __call args type", ), + ( + br#"class EvalBadCallNameType { public function __call(int $name, array $args) {} }"#.as_slice(), + "bad __call name type", + ), ( br#"class EvalBadCallStatic { public function __callStatic($name, $args) { return "x"; } }"#.as_slice(), "instance __callStatic", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9d131276c4..8000d0e9ae 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8283,6 +8283,11 @@ fn test_eval_rejects_invalid_magic_method_contracts() { public function __get(&$name) { return "x"; } +}');"#, + r#"eval('class EvalInvalidGetParamType { + public function __get(int $name) { + return "x"; + } }');"#, r#"eval('class EvalInvalidUnsetReturn { public function __unset($name): int { From ff1f06201a9ad94b877c91a0352c8f73b3d909cd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 00:04:01 +0200 Subject: [PATCH 0683/1208] Reject eval trait self exclusion --- .../src/interpreter/statements.rs | 3 +++ .../interpreter/tests/trait_adaptations.rs | 19 +++++++++++++++++ tests/codegen/eval.rs | 21 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 114380fbc4..de8ad3a1b6 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1072,6 +1072,9 @@ fn validate_eval_trait_adaptations( if eval_used_trait_decl(class, context, suppressed).is_none() { return Err(EvalStatus::RuntimeFatal); } + if same_eval_class_name(suppressed, trait_name) { + return Err(EvalStatus::RuntimeFatal); + } } } } diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index cf9a2f2c5c..5a400905fc 100644 --- a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -168,6 +168,25 @@ class EvalAdaptMissingTraitBox { assert_eq!(err, EvalStatus::RuntimeFatal); + let self_exclusion = parse_fragment( + br#"trait EvalAdaptSelfExcluded { + public function source() { return "T"; } +} +class EvalAdaptSelfExcludedBox { + use EvalAdaptSelfExcluded { + EvalAdaptSelfExcluded::source insteadof EvalAdaptSelfExcluded; + } +}"#, + ) + .expect("parse self-excluded trait adaptation"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&self_exclusion, &mut scope, &mut values) + .expect_err("self-excluded adaptation trait should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + let missing_unqualified_alias = parse_fragment( br#"trait EvalAdaptMissingAlias { public function source() { return "T"; } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8000d0e9ae..77ddc8a946 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7252,6 +7252,27 @@ class EvalAdaptMissingMethodBox { ); } +/// Verifies eval-declared `insteadof` rejects excluding the selected trait itself. +#[test] +fn test_eval_declared_trait_insteadof_self_exclusion_fails() { + let err = compile_and_run_expect_failure( + r#" Date: Fri, 26 Jun 2026 00:14:17 +0200 Subject: [PATCH 0684/1208] Reject invalid eval type atoms --- .../elephc-magician/src/parser/statements.rs | 36 +++++++++++++++++-- .../src/parser/tests/classes_errors.rs | 10 ++++-- tests/codegen/eval.rs | 36 +++++++++++++++++++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 15cdcdfc3b..17517c8006 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -43,10 +43,11 @@ struct ParsedClassBody { trait_adaptations: Vec, } -/// Type-declaration position controls PHP-only atoms such as `void` and `never`. +/// Type-declaration position controls PHP-only atoms such as `void`, `static`, and `callable`. #[derive(Clone, Copy)] enum EvalTypePosition { Parameter, + Property, Return, } @@ -1657,7 +1658,13 @@ impl Parser { pub(super) fn parse_optional_property_type( &mut self, ) -> Result, EvalParseError> { - self.parse_optional_parameter_type() + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis + ) { + return Ok(None); + } + self.parse_type_decl(EvalTypePosition::Property) } /// Parses `function name($param, ...) { ... }` declarations. @@ -2022,7 +2029,11 @@ impl Parser { if promotion.is_some() && !method_name.eq_ignore_ascii_case("__construct") { return Err(EvalParseError::UnsupportedConstruct); } - let param_type = self.parse_optional_parameter_type()?; + let param_type = if promotion.is_some() { + self.parse_optional_promoted_property_type()? + } else { + self.parse_optional_parameter_type()? + }; let is_by_ref = self.consume(TokenKind::Ampersand); let is_variadic = self.consume(TokenKind::Ellipsis); let TokenKind::DollarIdent(name) = self.current() else { @@ -2143,6 +2154,19 @@ impl Parser { } } + /// Parses a constructor-promoted parameter type using PHP property-type restrictions. + fn parse_optional_promoted_property_type( + &mut self, + ) -> Result, EvalParseError> { + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis + ) { + return Ok(None); + } + self.parse_type_decl(EvalTypePosition::Property) + } + /// Consumes a supported method parameter type and returns retained metadata. fn parse_optional_parameter_type( &mut self, @@ -2246,6 +2270,9 @@ impl Parser { let builtin = match lower.as_str() { "array" => Some(EvalParameterTypeVariant::Array), "bool" => Some(EvalParameterTypeVariant::Bool), + "callable" if matches!(position, EvalTypePosition::Property) => { + return Err(EvalParseError::UnsupportedConstruct); + } "callable" => Some(EvalParameterTypeVariant::Callable), "float" => Some(EvalParameterTypeVariant::Float), "int" => Some(EvalParameterTypeVariant::Int), @@ -2261,6 +2288,9 @@ impl Parser { Some(EvalParameterTypeVariant::Void) } "void" | "never" => return Err(EvalParseError::UnsupportedConstruct), + "static" if !matches!(position, EvalTypePosition::Return) => { + return Err(EvalParseError::UnsupportedConstruct); + } "self" | "parent" | "static" => { Some(EvalParameterTypeVariant::Class(lower.to_string())) } diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 4dc7f59e9d..8d8948a46a 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -99,15 +99,21 @@ class DynEvalReturnClass { ); } -/// Verifies return-only type atoms reject nullable, union, and intersection forms. +/// Verifies type atoms are rejected in positions where PHP forbids them. #[test] -fn parse_fragment_rejects_invalid_void_and_never_return_type_forms() { +fn parse_fragment_rejects_invalid_type_atom_forms() { for source in [ b"function DynEvalBadVoid(): ?void {}" as &[u8], b"function DynEvalBadVoidUnion(): void|null {}", b"function DynEvalBadNeverUnion(): never|int {}", b"function DynEvalBadNeverIntersection(): never&Countable {}", b"function DynEvalBadVoidParam(void $value) {}", + b"function DynEvalBadStaticParam(static $value) {}", + b"class DynEvalBadCallableProperty { public callable $value; }", + b"class DynEvalBadStaticProperty { public static static $value; }", + b"interface DynEvalBadCallableInterfaceProperty { public callable $value { get; } }", + b"class DynEvalBadCallablePromoted { public function __construct(public callable $value) {} }", + b"class DynEvalBadStaticPromoted { public function __construct(public static $value) {} }", ] { assert_eq!( parse_fragment(source), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 77ddc8a946..9506ffd1cf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7867,6 +7867,42 @@ abstract class EvalIfaceInheritedPropertyChild extends EvalIfaceInheritedPropert ); } +/// Verifies eval rejects PHP-forbidden callable/static type atoms by declaration position. +#[test] +fn test_eval_rejects_invalid_property_and_parameter_type_atoms() { + for source in [ + r#" Date: Fri, 26 Jun 2026 00:21:50 +0200 Subject: [PATCH 0685/1208] Reject eval class-scope type atoms --- .../elephc-magician/src/parser/statements.rs | 64 ++++++++++++++----- .../src/parser/tests/classes_errors.rs | 6 ++ tests/codegen/eval.rs | 11 ++++ 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 17517c8006..0d0bbab0f7 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -46,9 +46,11 @@ struct ParsedClassBody { /// Type-declaration position controls PHP-only atoms such as `void`, `static`, and `callable`. #[derive(Clone, Copy)] enum EvalTypePosition { - Parameter, + FunctionParameter, + MethodParameter, Property, - Return, + FunctionReturn, + MethodReturn, } impl Parser { @@ -930,11 +932,11 @@ impl Parser { parameter_is_variadic, promoted_properties, promoted_assignments, - } = self.parse_method_params(&name)?; + } = self.parse_method_params(&name, true)?; if !promoted_properties.is_empty() && (is_abstract || is_static) { return Err(EvalParseError::UnsupportedConstruct); } - let return_type = self.parse_optional_return_type()?; + let return_type = self.parse_optional_return_type(EvalTypePosition::MethodReturn)?; let (body, source_end_line) = if is_abstract { let source_end_line = self.current_line(); self.expect_semicolon()?; @@ -1557,11 +1559,11 @@ impl Parser { parameter_is_variadic, promoted_properties, promoted_assignments, - } = self.parse_method_params(&name)?; + } = self.parse_method_params(&name, true)?; if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { return Err(EvalParseError::UnsupportedConstruct); } - let return_type = self.parse_optional_return_type()?; + let return_type = self.parse_optional_return_type(EvalTypePosition::MethodReturn)?; let source_end_line = self.current_line(); self.expect_semicolon()?; Ok(EvalInterfaceMethod::new(name, params) @@ -1694,11 +1696,11 @@ impl Parser { parameter_is_variadic, promoted_properties, promoted_assignments, - } = self.parse_method_params("")?; + } = self.parse_method_params("", false)?; if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { return Err(EvalParseError::UnsupportedConstruct); } - let return_type = self.parse_optional_return_type()?; + let return_type = self.parse_optional_return_type(EvalTypePosition::FunctionReturn)?; let (body, source_end_line) = self.parse_block_with_end_line()?; Ok(vec![EvalStmt::FunctionDecl { name, @@ -2002,6 +2004,7 @@ impl Parser { pub(super) fn parse_method_params( &mut self, method_name: &str, + allow_class_scope_types: bool, ) -> Result { let mut params = Vec::new(); let mut parameter_attributes = Vec::new(); @@ -2032,7 +2035,12 @@ impl Parser { let param_type = if promotion.is_some() { self.parse_optional_promoted_property_type()? } else { - self.parse_optional_parameter_type()? + let position = if allow_class_scope_types { + EvalTypePosition::MethodParameter + } else { + EvalTypePosition::FunctionParameter + }; + self.parse_optional_parameter_type(position)? }; let is_by_ref = self.consume(TokenKind::Ampersand); let is_variadic = self.consume(TokenKind::Ellipsis); @@ -2170,6 +2178,7 @@ impl Parser { /// Consumes a supported method parameter type and returns retained metadata. fn parse_optional_parameter_type( &mut self, + position: EvalTypePosition, ) -> Result, EvalParseError> { if matches!( self.current(), @@ -2177,15 +2186,18 @@ impl Parser { ) { return Ok(None); } - self.parse_type_decl(EvalTypePosition::Parameter) + self.parse_type_decl(position) } /// Consumes a supported function or method return type after `:`. - fn parse_optional_return_type(&mut self) -> Result, EvalParseError> { + fn parse_optional_return_type( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { if !self.consume(TokenKind::Colon) { return Ok(None); } - self.parse_type_decl(EvalTypePosition::Return) + self.parse_type_decl(position) } /// Parses one PHP type declaration and returns retained eval metadata. @@ -2278,20 +2290,24 @@ impl Parser { "int" => Some(EvalParameterTypeVariant::Int), "iterable" => Some(EvalParameterTypeVariant::Iterable), "mixed" => Some(EvalParameterTypeVariant::Mixed), - "never" if matches!(position, EvalTypePosition::Return) => { + "never" if type_position_allows_return_only_atoms(position) => { Some(EvalParameterTypeVariant::Never) } "null" => return Ok(None), "object" => Some(EvalParameterTypeVariant::Object), "string" => Some(EvalParameterTypeVariant::String), - "void" if matches!(position, EvalTypePosition::Return) => { + "void" if type_position_allows_return_only_atoms(position) => { Some(EvalParameterTypeVariant::Void) } "void" | "never" => return Err(EvalParseError::UnsupportedConstruct), - "static" if !matches!(position, EvalTypePosition::Return) => { + "static" if matches!(position, EvalTypePosition::MethodReturn) => { + Some(EvalParameterTypeVariant::Class(lower.to_string())) + } + "static" => return Err(EvalParseError::UnsupportedConstruct), + "self" | "parent" if !type_position_allows_class_scope_atoms(position) => { return Err(EvalParseError::UnsupportedConstruct); } - "self" | "parent" | "static" => { + "self" | "parent" => { Some(EvalParameterTypeVariant::Class(lower.to_string())) } _ => None, @@ -2778,6 +2794,22 @@ fn type_variants_contain_standalone_return_only_atoms( }) } +/// Returns whether the type position accepts standalone return-only atoms. +fn type_position_allows_return_only_atoms(position: EvalTypePosition) -> bool { + matches!( + position, + EvalTypePosition::FunctionReturn | EvalTypePosition::MethodReturn + ) +} + +/// Returns whether `self` and `parent` are legal in this type position. +fn type_position_allows_class_scope_atoms(position: EvalTypePosition) -> bool { + !matches!( + position, + EvalTypePosition::FunctionParameter | EvalTypePosition::FunctionReturn + ) +} + /// Returns whether a class-like receiver is legal in a compile-time method default. fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { !class_name diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 8d8948a46a..3da8ebbad3 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -108,7 +108,13 @@ fn parse_fragment_rejects_invalid_type_atom_forms() { b"function DynEvalBadNeverUnion(): never|int {}", b"function DynEvalBadNeverIntersection(): never&Countable {}", b"function DynEvalBadVoidParam(void $value) {}", + b"function DynEvalBadSelfParam(self $value) {}", + b"function DynEvalBadParentParam(parent $value) {}", b"function DynEvalBadStaticParam(static $value) {}", + b"function DynEvalBadSelfReturn(): self {}", + b"function DynEvalBadParentReturn(): parent {}", + b"function DynEvalBadStaticReturn(): static {}", + b"class DynEvalBadStaticMethodParam { public function read(static $value) {} }", b"class DynEvalBadCallableProperty { public callable $value; }", b"class DynEvalBadStaticProperty { public static static $value; }", b"interface DynEvalBadCallableInterfaceProperty { public callable $value { get; } }", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9506ffd1cf..50e73027a3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7888,6 +7888,17 @@ eval('class EvalBadCallablePromoted { "#, r#" Date: Fri, 26 Jun 2026 00:32:13 +0200 Subject: [PATCH 0686/1208] Allow non-public eval magic fallbacks --- .../interpreter/builtins/registry/callable.rs | 15 ++--- .../src/interpreter/builtins/symbols.rs | 3 + .../src/interpreter/statements.rs | 44 +++++++++--- .../src/interpreter/tests/classes.rs | 67 ++++++++++++++++--- tests/codegen/eval.rs | 16 ++--- 5 files changed, 110 insertions(+), 35 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index e2209f43bf..da269d1b62 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -180,7 +180,12 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( eval_callable_with_values(name, evaluated_args, context, values) } EvaluatedCallable::InvokableObject { object } => { - eval_method_call_result(*object, "__invoke", evaluated_args, context, values) + eval_invokable_object_call_result( + *object, + positional_args(evaluated_args), + context, + values, + ) } EvaluatedCallable::ObjectMethod { object, method } => { eval_method_call_result(*object, method, evaluated_args, context, values) @@ -207,13 +212,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( eval_callable_with_call_array_args(name, evaluated_args, context, values) } EvaluatedCallable::InvokableObject { object } => { - eval_method_call_result_with_evaluated_args( - *object, - "__invoke", - evaluated_args, - context, - values, - ) + eval_invokable_object_call_result(*object, evaluated_args, context, values) } EvaluatedCallable::ObjectMethod { object, method } => { eval_method_call_result_with_evaluated_args( diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index d54b0e2e5c..88b7f47464 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -792,6 +792,9 @@ fn eval_object_method_callable_probe( if method.is_static() || method.is_abstract() { return Ok(false); } + if method_name.eq_ignore_ascii_case("__invoke") { + return Ok(true); + } Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok()) } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index de8ad3a1b6..19b567a617 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1454,7 +1454,6 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus } "__get" | "__isset" | "__unset" => { validate_magic_non_static(method)?; - validate_magic_public(method)?; validate_magic_arity(method, 1)?; validate_magic_declared_param_type(method, 0, MagicParamType::String)?; if method.name().eq_ignore_ascii_case("__isset") { @@ -1465,7 +1464,6 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus } "__set" | "__call" => { validate_magic_non_static(method)?; - validate_magic_public(method)?; validate_magic_arity(method, 2)?; validate_magic_declared_param_type(method, 0, MagicParamType::String)?; if method.name().eq_ignore_ascii_case("__set") { @@ -1476,7 +1474,6 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus } "__callstatic" => { validate_magic_static(method)?; - validate_magic_public(method)?; validate_magic_arity(method, 2)?; validate_magic_declared_param_type(method, 0, MagicParamType::String)?; validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; @@ -1509,7 +1506,6 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus } "__invoke" => { validate_magic_non_static(method)?; - validate_magic_public(method)?; } "__clone" | "__destruct" => { validate_magic_non_static(method)?; @@ -2978,7 +2974,6 @@ fn eval_magic_property_get( if method.is_static() || method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, method.visibility(), context)?; let property = values.string(property_name)?; eval_dynamic_method_with_values( &declaring_class, @@ -3007,7 +3002,6 @@ fn eval_magic_property_set( if method.is_static() || method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, method.visibility(), context)?; let property = values.string(property_name)?; let result = eval_dynamic_method_with_values( &declaring_class, @@ -3036,7 +3030,6 @@ fn eval_magic_property_isset( if method.is_static() || method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, method.visibility(), context)?; let property = values.string(property_name)?; let result = eval_dynamic_method_with_values( &declaring_class, @@ -3066,7 +3059,6 @@ fn eval_magic_property_unset( if method.is_static() || method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, method.visibility(), context)?; let property = values.string(property_name)?; let result = eval_dynamic_method_with_values( &declaring_class, @@ -4477,6 +4469,40 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( Err(EvalStatus::RuntimeFatal) } +/// Dispatches an invokable object through `__invoke()` without enforcing hook visibility. +pub(in crate::interpreter) fn eval_invokable_object_call_result( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; + return values.method_call(object, "__invoke", evaluated_args); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; + return values.method_call(object, "__invoke", evaluated_args); + }; + let called_class_name = class.name().to_string(); + let Some((declaring_class, method)) = context.class_method(&called_class_name, "__invoke") + else { + return Err(EvalStatus::RuntimeFatal); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ) +} + /// Dispatches a missing or inaccessible eval instance method through `__call()`. fn eval_magic_instance_method_call( object: RuntimeCellHandle, @@ -4492,7 +4518,6 @@ fn eval_magic_instance_method_call( if method.is_static() || method.is_abstract() { return Ok(None); } - validate_eval_member_access(&declaring_class, method.visibility(), context)?; let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; eval_dynamic_method_with_values( &declaring_class, @@ -4520,7 +4545,6 @@ fn eval_magic_static_method_call( if !method.is_static() || method.is_abstract() { return Ok(None); } - validate_eval_member_access(&declaring_class, method.visibility(), context)?; let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; eval_dynamic_static_method_with_values( &declaring_class, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index b12280450e..fcd24d9945 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1062,6 +1062,63 @@ return $box->events;"#, ); } +/// Verifies eval invokes non-public magic methods that PHP accepts with warnings. +#[test] +fn execute_program_dispatches_non_public_eval_magic_methods() { + let program = parse_fragment( + br#"class EvalNonPublicMagicBox { + public string $events = ""; + protected function __get(string $name) { + $this->events = $this->events . "get:" . $name . ";"; + return "value:" . $name; + } + protected function __set(string $name, $value): void { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } + private function __isset(string $name): bool { + $this->events = $this->events . "isset:" . $name . ";"; + return true; + } + private function __unset(string $name): void { + $this->events = $this->events . "unset:" . $name . ";"; + } + private function __call(string $name, array $args) { + return $name . ":" . $args[0] . ":" . $args["name"]; + } + private static function __callStatic(string $name, array $args) { + return $name . ":" . $args[0] . ":" . $args["name"]; + } + private function __invoke(string $left = "I", string $right = "J") { + return "invoke:" . $left . $right; + } +} +$box = new EvalNonPublicMagicBox(); +echo is_callable($box) ? "callable:" : "bad:"; +echo $box->missing; echo ":"; +$box->other = "B"; +echo isset($box->probe) ? "isset:" : "bad:"; +unset($box->gone); +echo $box->run("A", name: "B"); echo ":"; +echo EvalNonPublicMagicBox::staticRun("C", name: "D"); echo ":"; +echo $box(right: "F", left: "E"); +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "callable:value:missing:isset:run:A:B:staticRun:C:D:invoke:EF" + ); + assert_eq!( + values.get(result), + FakeValue::String("get:missing;set:other=B;isset:probe;unset:gone;".to_string()) + ); +} + /// Verifies inaccessible eval properties dispatch through magic property methods. #[test] fn execute_program_dispatches_inaccessible_eval_properties_to_magic_methods() { @@ -1228,7 +1285,7 @@ echo $box;"#, execute_program(&program, &mut scope, &mut values).expect_err("missing __toString should fail"); } -/// Verifies eval rejects magic methods whose staticness, visibility, or arity is invalid. +/// Verifies eval rejects magic methods whose staticness, arity, or fatal contracts are invalid. #[test] fn execute_program_rejects_invalid_eval_magic_method_contracts() { let cases: Vec<(&[u8], &str)> = vec![ @@ -1240,10 +1297,6 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadToStringReturn { public function __toString(): int { return 1; } }"#.as_slice(), "bad __toString return type", ), - ( - br#"class EvalBadGet { protected function __get($name) { return "x"; } }"#.as_slice(), - "protected __get", - ), ( br#"class EvalBadGetByRef { public function __get(&$name) { return "x"; } }"#.as_slice(), "by-ref __get", @@ -1332,10 +1385,6 @@ fn execute_program_rejects_invalid_eval_magic_method_contracts() { br#"class EvalBadSetStateParam { public static function __set_state(string $data) {} }"#.as_slice(), "bad __set_state parameter type", ), - ( - br#"class EvalBadInvoke { private function __invoke() { return 1; } }"#.as_slice(), - "private __invoke", - ), ( br#"class EvalBadClone { public static function __clone() {} }"#.as_slice(), "static __clone", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 50e73027a3..ed77e20c96 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7669,11 +7669,11 @@ eval('class EvalMagicPropertyBox { private string $secret = "raw"; public string $events = ""; public function readOwn() { return $this->secret; } - public function __get($name) { + protected function __get($name) { $this->events = $this->events . "get:" . $name . ";"; return "read:" . $name; } - public function __set($name, $value) { + private function __set($name, $value) { $this->events = $this->events . "set:" . $name . "=" . $value . ";"; } } @@ -7732,15 +7732,15 @@ eval('class EvalMagicPropertyProbeBox { public string $present = "ready"; public $nullish = null; private string $secret = "raw"; - public function __isset($name) { + private function __isset($name) { $this->events = $this->events . "isset:" . $name . ";"; return $name !== "no"; } - public function __get($name) { + protected function __get($name) { $this->events = $this->events . "get:" . $name . ";"; return $name === "empty" ? "" : "value:" . $name; } - public function __unset($name) { + private function __unset($name) { $this->events = $this->events . "unset:" . $name . ";"; } } @@ -8258,7 +8258,7 @@ eval('class EvalInvokableBox { public function __construct($label = "box") { $this->label = $label; } - public function __invoke($left = "A", $right = "B") { + private function __invoke($left = "A", $right = "B") { return $this->label . ":" . $left . $right; } } @@ -8288,7 +8288,7 @@ fn test_eval_declared_magic_call_method_fallback() { r#" Date: Fri, 26 Jun 2026 00:51:06 +0200 Subject: [PATCH 0687/1208] Recognize eval enum marker interfaces --- .../interpreter/builtins/class_metadata.rs | 2 +- .../src/interpreter/builtins/symbols.rs | 6 +- .../src/interpreter/reflection.rs | 8 +- .../src/interpreter/statements.rs | 73 +++++++++++++++++-- .../src/interpreter/tests/builtins_symbols.rs | 10 ++- .../src/interpreter/tests/enums.rs | 59 +++++++++++++++ tests/codegen/eval.rs | 56 +++++++++++++- 7 files changed, 194 insertions(+), 20 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index a549cfbc68..d0d02eec30 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -309,7 +309,7 @@ fn eval_class_relation_name_exists( || context.has_trait(name) || context.has_enum(name) || values.class_exists(name)? - || values.interface_exists(name)? + || eval_runtime_interface_exists(name, values)? || values.trait_exists(name)? { return Ok(true); diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 88b7f47464..b3eec77a8b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -232,7 +232,7 @@ pub(in crate::interpreter) fn eval_class_alias_result( if alias.is_empty() || context.resolve_class_like_name(&alias).is_some() || values.class_exists(&alias)? - || values.interface_exists(&alias)? + || eval_runtime_interface_exists(&alias, values)? || values.trait_exists(&alias)? || values.enum_exists(&alias)? { @@ -244,7 +244,7 @@ pub(in crate::interpreter) fn eval_class_alias_result( context.define_external_enum_alias(&class, &alias) } else if values.class_exists(&class)? { context.define_external_class_alias(&class, &alias) - } else if values.interface_exists(&class)? { + } else if eval_runtime_interface_exists(&class, values)? { context.define_external_interface_alias(&class, &alias) } else if values.trait_exists(&class)? { context.define_external_trait_alias(&class, &alias) @@ -355,7 +355,7 @@ pub(in crate::interpreter) fn eval_interface_exists_name( let name = values.string_bytes(name)?; let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; let name = name.trim_start_matches('\\'); - Ok(context.has_interface(name) || values.interface_exists(name)?) + Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) } /// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 7cc97c2044..34ddb25606 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -309,7 +309,7 @@ pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( } let result = if eval_reflection_class_like_exists(&reflected_name, context) { eval_reflection_class_implements_interface_name(&reflected_name, &interface_name, context) - } else if values.interface_exists(&reflected_name)? { + } else if eval_runtime_interface_exists(&reflected_name, values)? { eval_reflection_same_class_like_name(&reflected_name, &interface_name) } else { let reflected_class = values.string(&reflected_name)?; @@ -341,7 +341,7 @@ pub(in crate::interpreter) fn eval_reflection_class_is_subclass_of_result( let target_name = eval_reflection_string_arg(args[0], values)?; if !eval_reflection_class_like_exists(&target_name, context) && !values.class_exists(&target_name)? - && !values.interface_exists(&target_name)? + && !eval_runtime_interface_exists(&target_name, values)? && !values.trait_exists(&target_name)? && !values.enum_exists(&target_name)? { @@ -2836,7 +2836,7 @@ fn eval_reflection_aot_class_flags( ) -> Result, EvalStatus> { let runtime_class_name = class_name.trim_start_matches('\\'); let is_class = values.class_exists(runtime_class_name)?; - let is_interface = values.interface_exists(runtime_class_name)?; + let is_interface = eval_runtime_interface_exists(runtime_class_name, values)?; let is_trait = values.trait_exists(runtime_class_name)?; let is_enum = values.enum_exists(runtime_class_name)?; if !(is_class || is_interface || is_trait || is_enum) { @@ -6275,7 +6275,7 @@ fn eval_reflection_interface_exists( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - Ok(context.has_interface(name) || values.interface_exists(name)?) + Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) } /// Returns true when one name exists as a non-interface class-like symbol. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 19b567a617..733d65e4f1 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -599,6 +599,26 @@ pub(in crate::interpreter) fn catch_types_match_thrown( Ok(false) } +/// Returns whether one name is a PHP native enum interface. +pub(in crate::interpreter) fn eval_builtin_enum_interface_name(name: &str) -> bool { + let name = name.trim_start_matches('\\'); + name.eq_ignore_ascii_case("UnitEnum") || name.eq_ignore_ascii_case("BackedEnum") +} + +/// Returns whether one name is PHP's native backed-enum interface. +fn eval_builtin_backed_enum_interface_name(name: &str) -> bool { + name.trim_start_matches('\\') + .eq_ignore_ascii_case("BackedEnum") +} + +/// Returns whether one name is visible as a native/runtime interface to eval. +pub(in crate::interpreter) fn eval_runtime_interface_exists( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_builtin_enum_interface_name(name) || values.interface_exists(name)?) +} + /// Registers an eval-declared class in the dynamic class table. pub(in crate::interpreter) fn execute_class_decl_stmt( class: &EvalClass, @@ -612,7 +632,7 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( || context.has_trait(name) || context.has_enum(name) || values.class_exists(name)? - || values.interface_exists(name)? + || eval_runtime_interface_exists(name, values)? || values.trait_exists(name)? || values.enum_exists(name)? { @@ -633,10 +653,11 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( } } for interface in class.interfaces() { - if !context.has_interface(interface) && !values.interface_exists(interface)? { + if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { return Err(EvalStatus::RuntimeFatal); } } + validate_eval_class_does_not_implement_enum_interfaces(class, context)?; validate_declared_class_interface_members(class, context)?; if !class.is_abstract() { validate_concrete_class_requirements(class, context)?; @@ -689,7 +710,7 @@ pub(in crate::interpreter) fn execute_enum_decl_stmt( || context.has_trait(name) || values.enum_exists(name)? || values.class_exists(name)? - || values.interface_exists(name)? + || eval_runtime_interface_exists(name, values)? || values.trait_exists(name)? { return Err(EvalStatus::RuntimeFatal); @@ -720,12 +741,33 @@ fn validate_eval_enum_decl( validate_eval_enum_method_declarations(enum_decl)?; let enum_class = enum_decl.as_class_metadata(); validate_eval_class_modifiers(&enum_class, context)?; + validate_eval_enum_interfaces(enum_decl, &enum_class, context, values)?; + validate_concrete_class_requirements(&enum_class, context) +} + +/// Validates PHP's special enum interface rules for one eval enum declaration. +fn validate_eval_enum_interfaces( + enum_decl: &EvalEnum, + enum_class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { for interface in enum_decl.interfaces() { - if !context.has_interface(interface) && !values.interface_exists(interface)? { + if eval_builtin_enum_interface_name(interface) { + return Err(EvalStatus::RuntimeFatal); + } + if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { return Err(EvalStatus::RuntimeFatal); } } - validate_concrete_class_requirements(&enum_class, context) + if enum_decl.backing_type().is_none() + && pending_class_interface_names(enum_class, context) + .iter() + .any(|interface| eval_builtin_backed_enum_interface_name(interface)) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) } /// Validates enum case names and pure/backed declaration shape. @@ -916,7 +958,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( if context.has_interface(name) || context.has_class(name) || context.has_enum(name) - || values.interface_exists(name)? + || eval_runtime_interface_exists(name, values)? || values.class_exists(name)? || values.enum_exists(name)? { @@ -930,7 +972,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( { return Err(EvalStatus::RuntimeFatal); } - if !context.has_interface(parent) && !values.interface_exists(parent)? { + if !context.has_interface(parent) && !eval_runtime_interface_exists(parent, values)? { return Err(EvalStatus::RuntimeFatal); } } @@ -964,7 +1006,7 @@ pub(in crate::interpreter) fn execute_trait_decl_stmt( || context.has_enum(name) || values.trait_exists(name)? || values.class_exists(name)? - || values.interface_exists(name)? + || eval_runtime_interface_exists(name, values)? || values.enum_exists(name)? { return Err(EvalStatus::RuntimeFatal); @@ -1385,6 +1427,21 @@ fn trait_adaptation_target_matches( }) } +/// Rejects non-enum classes that implement PHP's native enum interfaces. +fn validate_eval_class_does_not_implement_enum_interfaces( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if pending_class_interface_names(class, context) + .iter() + .any(|interface| eval_builtin_enum_interface_name(interface)) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + /// Validates abstract/final modifiers on an eval-declared class and its methods. fn validate_eval_class_modifiers( class: &EvalClass, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs index 51f6f319df..d444423297 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs @@ -137,6 +137,9 @@ fn execute_program_interface_exists_uses_runtime_probe() { br#"echo interface_exists("KnownInterface") ? "Y" : "N"; echo interface_exists("knowninterface") ? "Y" : "N"; echo interface_exists("KnownClass") ? "Y" : "N"; +echo interface_exists("UnitEnum") ? "U" : "u"; +echo interface_exists("BackedEnum") ? "B" : "b"; +echo class_exists("UnitEnum") ? "C" : "c"; echo call_user_func("interface_exists", "KnownInterface") ? "Y" : "N"; echo call_user_func_array("interface_exists", ["interface" => "KnownInterface"]) ? "Y" : "N"; echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N";"#, @@ -147,7 +150,7 @@ echo interface_exists(interface: "MissingInterface", autoload: false) ? "Y" : "N let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "YYNYYN"); + assert_eq!(values.output, "YYNUBcYYN"); } /// Verifies eval-declared interfaces are visible to interface symbol probes. #[test] @@ -322,6 +325,9 @@ echo interface_exists("DynAliasIfaceCopy") ? "iface-exists" : "bad"; echo ":"; echo class_exists("DynAliasIfaceCopy") ? "bad" : "iface-not-class"; echo ":"; echo is_a("DynAliasIfaceCopy", "DynAliasIface", true) ? "iface-isa" : "bad"; echo ":"; echo (new ReflectionClass("DynAliasIfaceCopy"))->isInterface() ? "iface-reflect" : "bad"; echo ":"; +echo class_alias("UnitEnum", "DynAliasUnitEnum") ? "unit-alias" : "bad"; echo ":"; +echo interface_exists("DynAliasUnitEnum") ? "unit-exists" : "bad"; echo ":"; +echo class_exists("DynAliasUnitEnum") ? "bad" : "unit-not-class"; echo ":"; echo class_alias("DynAliasTrait", "DynAliasTraitCopy") ? "trait-alias" : "bad"; echo ":"; echo trait_exists("DynAliasTraitCopy") ? "trait-exists" : "bad"; echo ":"; echo class_exists("DynAliasTraitCopy") ? "bad" : "trait-not-class"; echo ":"; @@ -349,7 +355,7 @@ return is_callable("class_alias");"#, assert_eq!( values.output, - "alias:exists:DynAliasBox:7:isa:iface-alias:iface-exists:iface-not-class:iface-isa:iface-reflect:trait-alias:trait-exists:trait-not-class:trait-isa:enum-alias:enum-exists:enum-class:DynAliasEnum:ready:duplicate:missing:call:call-exists:aot:known:1" + "alias:exists:DynAliasBox:7:isa:iface-alias:iface-exists:iface-not-class:iface-isa:iface-reflect:unit-alias:unit-exists:unit-not-class:trait-alias:trait-exists:trait-not-class:trait-isa:enum-alias:enum-exists:enum-class:DynAliasEnum:ready:duplicate:missing:call:call-exists:aot:known:1" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs index a2a4addcaa..3990a27cfc 100644 --- a/crates/elephc-magician/src/interpreter/tests/enums.rs +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -129,6 +129,37 @@ return EvalSuit::fallback() === EvalSuit::Hearts;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval enum interfaces can inherit PHP's native enum marker interfaces. +#[test] +fn execute_program_allows_eval_enum_marker_interface_inheritance() { + let program = parse_fragment( + br#"interface EvalUnitMarker extends UnitEnum {} +interface EvalBackedMarker extends BackedEnum {} +enum EvalMarkedUnit implements EvalUnitMarker { + case Ready; +} +enum EvalMarkedBacked: string implements EvalBackedMarker { + case Ready = "ready"; +} +echo interface_exists("UnitEnum") ? "U" : "u"; echo ":"; +echo interface_exists("BackedEnum") ? "B" : "b"; echo ":"; +echo is_a(EvalMarkedUnit::Ready, "EvalUnitMarker") ? "unit" : "bad"; echo ":"; +echo is_a(EvalMarkedBacked::Ready, "EvalBackedMarker") ? "backed" : "bad"; echo ":"; +return EvalMarkedBacked::Ready->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "U:B:unit:backed:"); + assert_eq!( + values.get(result), + FakeValue::String("ready".to_string()) + ); +} + /// Verifies eval rejects enum members that conflict with PHP enum rules. #[test] fn execute_program_rejects_invalid_eval_enum_members() { @@ -163,6 +194,34 @@ fn execute_program_rejects_invalid_eval_enum_members() { }"#, "forbidden enum magic method lookup should be case-insensitive", ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidExplicitUnitEnum implements UnitEnum { + case Ready; +}"#, + "enum cannot explicitly implement UnitEnum", + ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidExplicitBackedEnum: string implements BackedEnum { + case Ready = "ready"; +}"#, + "enum cannot explicitly implement BackedEnum", + ); + + assert_invalid_enum_fragment( + br#"interface EvalBackedMarker extends BackedEnum {} +enum EvalInvalidPureBackedMarker implements EvalBackedMarker { + case Ready; +}"#, + "pure enum cannot implement BackedEnum through a marker", + ); + + assert_invalid_enum_fragment( + br#"interface EvalUnitMarker extends UnitEnum {} +class EvalInvalidUnitEnumClass implements EvalUnitMarker {}"#, + "non-enum class cannot implement UnitEnum through a marker", + ); } /// Verifies eval allows the enum magic methods PHP permits. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ed77e20c96..27e8b324c8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6388,12 +6388,15 @@ eval('echo interface_exists("EvalInterfaceExistsProbe") ? "Y" : "N"; echo interface_exists("evalinterfaceexistsprobe") ? "Y" : "N"; echo interface_exists("\EvalInterfaceExistsProbe") ? "Y" : "N"; echo interface_exists("EvalInterfaceExistsImpl") ? "Y" : "N"; +echo interface_exists("UnitEnum") ? "U" : "u"; +echo interface_exists("BackedEnum") ? "B" : "b"; +echo class_exists("UnitEnum") ? "C" : "c"; echo call_user_func("interface_exists", "EvalInterfaceExistsProbe") ? "Y" : "N"; echo call_user_func_array("interface_exists", ["autoload" => false, "interface" => "\EvalInterfaceExistsProbe"]) ? "Y" : "N"; echo function_exists("interface_exists");'); "#, ); - assert_eq!(out, "YYYNYY1"); + assert_eq!(out, "YYYNUBcYY1"); } /// Verifies eval `trait_exists()` and `enum_exists()` probe generated AOT metadata. @@ -6447,6 +6450,52 @@ echo is_a(EvalDynColor::Red, "EvalDynLabel") ? "I" : "i";'); assert_eq!(out, "EC2Gcolor:Green:gFNI"); } +/// Verifies eval enums support user interfaces derived from PHP enum marker interfaces. +#[test] +fn test_eval_declared_enum_marker_interface_inheritance() { + let out = compile_and_run( + r#"value;'); +"#, + ); + assert_eq!(out, "UBready"); + + let err = compile_and_run_expect_failure( + r#"isInterface() ? "IR" : "ir"; echo ":"; +echo class_alias("UnitEnum", "EvalAliasUnitEnum") ? "U" : "u"; echo ":"; +echo interface_exists("EvalAliasUnitEnum") ? "UE" : "ue"; echo ":"; +echo class_exists("EvalAliasUnitEnum") ? "bad" : "UC"; echo ":"; echo class_alias("EvalAliasTrait", "EvalAliasTraitCopy") ? "T" : "t"; echo ":"; echo trait_exists("EvalAliasTraitCopy") ? "TE" : "te"; echo ":"; echo class_exists("EvalAliasTraitCopy") ? "bad" : "TC"; echo ":"; @@ -14175,7 +14227,7 @@ return count(get_declared_traits());'); ); assert_eq!( out, - "I:IE:IC:II:IR:T:TE:TC:TI:E:EE:EC:EvalAliasEnum:ready:C:CE:2:1:1" + "I:IE:IC:II:IR:U:UE:UC:T:TE:TC:TI:E:EE:EC:EvalAliasEnum:ready:C:CE:2:1:1" ); } From 5b0384ef03e22e9957008ec3785f8cdf4f28efde Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 01:05:01 +0200 Subject: [PATCH 0688/1208] Support eval trait composition --- crates/elephc-magician/src/context.rs | 63 ++++++++ crates/elephc-magician/src/eval_ir.rs | 35 ++++ .../src/interpreter/reflection.rs | 10 +- .../src/interpreter/statements.rs | 150 ++++++++++++++++++ .../interpreter/tests/trait_adaptations.rs | 59 +++++++ .../elephc-magician/src/parser/statements.rs | 39 ++++- .../src/parser/tests/classes_errors.rs | 31 ++++ tests/codegen/eval.rs | 41 +++++ 8 files changed, 421 insertions(+), 7 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 4b755a71b2..9e89760483 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -1463,6 +1463,44 @@ impl ElephcEvalContext { aliases } + /// Returns trait names used directly by an eval-declared trait. + pub fn trait_trait_names(&self, trait_name: &str) -> Vec { + self.trait_decl(trait_name).map_or_else(Vec::new, |trait_decl| { + let mut traits = Vec::new(); + let mut seen = HashSet::new(); + for used_trait in trait_decl.traits() { + push_unique_class_name(used_trait, &mut traits, &mut seen); + } + traits + }) + } + + /// Returns trait method aliases declared directly by an eval-declared trait. + pub fn trait_trait_aliases(&self, trait_name: &str) -> Vec<(String, String)> { + let Some(trait_decl) = self.trait_decl(trait_name) else { + return Vec::new(); + }; + let mut aliases = Vec::new(); + for adaptation in trait_decl.trait_adaptations() { + let EvalTraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + .. + } = adaptation + else { + continue; + }; + let Some(source_trait) = + self.trait_trait_alias_source(trait_decl, trait_name.as_deref(), method) + else { + continue; + }; + aliases.push((alias.clone(), format!("{source_trait}::{method}"))); + } + aliases + } + /// Resolves the trait name shown in `ReflectionClass::getTraitAliases()`. fn class_trait_alias_source( &self, @@ -1485,6 +1523,31 @@ impl ElephcEvalContext { .iter() .any(|candidate| candidate.name().eq_ignore_ascii_case(method)) .then(|| trait_decl.name().trim_start_matches('\\').to_string()) + }) + } + + /// Resolves the trait name shown for a trait's internal `getTraitAliases()`. + fn trait_trait_alias_source( + &self, + trait_decl: &EvalTrait, + explicit_trait: Option<&str>, + method: &str, + ) -> Option { + if let Some(trait_name) = explicit_trait { + return Some( + self.trait_decl(trait_name) + .map_or(trait_name, EvalTrait::name) + .trim_start_matches('\\') + .to_string(), + ); + } + trait_decl.traits().iter().find_map(|trait_name| { + let used_trait_decl = self.trait_decl(trait_name)?; + used_trait_decl + .methods() + .iter() + .any(|candidate| candidate.name().eq_ignore_ascii_case(method)) + .then(|| used_trait_decl.name().trim_start_matches('\\').to_string()) }) } diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index b1ae8e274b..4e39e4b422 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -1590,6 +1590,8 @@ pub struct EvalTrait { name: String, source_location: Option, attributes: Vec, + traits: Vec, + trait_adaptations: Vec, constants: Vec, properties: Vec, methods: Vec, @@ -1600,6 +1602,8 @@ impl PartialEq for EvalTrait { fn eq(&self, other: &Self) -> bool { self.name == other.name && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations && self.constants == other.constants && self.properties == other.properties && self.methods == other.methods @@ -1622,11 +1626,32 @@ impl EvalTrait { constants: Vec, properties: Vec, methods: Vec, + ) -> Self { + Self::with_constants_traits_adaptations( + name, + constants, + properties, + methods, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval trait with trait uses, adaptations, constants, properties, and methods. + pub fn with_constants_traits_adaptations( + name: impl Into, + constants: Vec, + properties: Vec, + methods: Vec, + traits: Vec, + trait_adaptations: Vec, ) -> Self { Self { name: name.into(), source_location: None, attributes: Vec::new(), + traits, + trait_adaptations, constants, properties, methods, @@ -1660,6 +1685,16 @@ impl EvalTrait { &self.attributes } + /// Returns trait names used directly by this eval trait. + pub fn traits(&self) -> &[String] { + &self.traits + } + + /// Returns trait adaptations declared directly by this eval trait. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + /// Returns constants declared directly by this eval trait. pub fn constants(&self) -> &[EvalClassConstant] { &self.constants diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 34ddb25606..d830ff2240 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -855,8 +855,12 @@ pub(in crate::interpreter) fn eval_reflection_class_get_trait_aliases_result( else { return Ok(None); }; - eval_reflection_string_assoc_result(context.class_trait_aliases(&reflected_name), values) - .map(Some) + let aliases = if context.trait_decl(&reflected_name).is_some() { + context.trait_trait_aliases(&reflected_name) + } else { + context.class_trait_aliases(&reflected_name) + }; + eval_reflection_string_assoc_result(aliases, values).map(Some) } /// Handles eval-backed `ReflectionClass::getConstant()` calls. @@ -5373,7 +5377,7 @@ fn eval_reflection_class_like_attributes( source_location: trait_decl.source_location(), attributes: trait_decl.attributes().to_vec(), interface_names: Vec::new(), - trait_names: Vec::new(), + trait_names: context.trait_trait_names(trait_decl.name()), method_names: context.trait_method_names(trait_decl.name()), property_names: context.trait_property_names(trait_decl.name()), parent_class_name: None, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 733d65e4f1..e8f46424fa 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1011,6 +1011,7 @@ pub(in crate::interpreter) fn execute_trait_decl_stmt( { return Err(EvalStatus::RuntimeFatal); } + let trait_decl = expand_eval_trait_traits(trait_decl, context)?; validate_eval_declared_constants(trait_decl.constants())?; validate_eval_magic_methods(trait_decl.methods())?; if context.define_trait(trait_decl.clone()) { @@ -1026,6 +1027,146 @@ pub(in crate::interpreter) fn execute_trait_decl_stmt( } } +/// Expands nested eval trait uses into the trait metadata registered by eval. +fn expand_eval_trait_traits( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, +) -> Result { + if trait_decl.traits().is_empty() { + return Ok(trait_decl.clone()); + } + validate_eval_trait_decl_adaptations(trait_decl, context)?; + let trait_method_names = trait_method_name_set(trait_decl); + let mut imported_method_names = std::collections::HashSet::new(); + let mut imported_properties = std::collections::HashMap::new(); + let mut imported_constants = std::collections::HashMap::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for used_trait_name in trait_decl.traits() { + let Some(used_trait_decl) = context.trait_decl(used_trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_constants( + used_trait_decl, + trait_decl.constants(), + &mut imported_constants, + &mut constants, + )?; + append_eval_trait_properties( + used_trait_decl, + trait_decl.properties(), + &mut imported_properties, + &mut properties, + )?; + append_eval_trait_methods( + used_trait_decl, + trait_decl.trait_adaptations(), + &trait_method_names, + &mut imported_method_names, + &mut methods, + )?; + } + constants.extend(trait_decl.constants().iter().cloned()); + properties.extend(trait_decl.properties().iter().cloned()); + methods.extend(trait_decl.methods().iter().cloned()); + let mut expanded = EvalTrait::with_constants_traits_adaptations( + trait_decl.name().to_string(), + constants, + properties, + methods, + trait_decl.traits().to_vec(), + trait_decl.trait_adaptations().to_vec(), + ) + .with_attributes(trait_decl.attributes().to_vec()); + if let Some(source_location) = trait_decl.source_location() { + expanded = expanded.with_source_location(source_location); + } + Ok(expanded) +} + +/// Validates that trait-level adaptations reference directly used traits and methods. +fn validate_eval_trait_decl_adaptations( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for adaptation in trait_decl.trait_adaptations() { + match adaptation { + EvalTraitAdaptation::Alias { + trait_name, method, .. + } => validate_eval_trait_decl_adaptation_method( + trait_decl, + context, + trait_name.as_deref(), + method, + )?, + EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + } => { + let Some(trait_name) = trait_name.as_deref() else { + return Err(EvalStatus::RuntimeFatal); + }; + validate_eval_trait_decl_adaptation_method( + trait_decl, + context, + Some(trait_name), + method, + )?; + for suppressed in instead_of { + if eval_trait_used_trait_decl(trait_decl, context, suppressed).is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if same_eval_class_name(suppressed, trait_name) { + return Err(EvalStatus::RuntimeFatal); + } + } + } + } + } + Ok(()) +} + +/// Validates one trait-level adaptation method target. +fn validate_eval_trait_decl_adaptation_method( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, + trait_name: Option<&str>, + method: &str, +) -> Result<(), EvalStatus> { + if let Some(trait_name) = trait_name { + let Some(used_trait_decl) = eval_trait_used_trait_decl(trait_decl, context, trait_name) + else { + return Err(EvalStatus::RuntimeFatal); + }; + return trait_has_method(used_trait_decl, method) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal); + } + trait_decl + .traits() + .iter() + .filter_map(|trait_name| context.trait_decl(trait_name)) + .any(|used_trait_decl| trait_has_method(used_trait_decl, method)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns a trait declaration only when the pending trait directly uses that trait. +fn eval_trait_used_trait_decl<'a>( + trait_decl: &EvalTrait, + context: &'a ElephcEvalContext, + trait_name: &str, +) -> Option<&'a EvalTrait> { + trait_decl + .traits() + .iter() + .any(|used_trait| same_eval_class_name(used_trait, trait_name)) + .then(|| context.trait_decl(trait_name)) + .flatten() +} + /// Expands eval trait uses into the class metadata used by dynamic dispatch. fn expand_eval_class_traits( class: &EvalClass, @@ -1170,6 +1311,15 @@ fn trait_has_method(trait_decl: &EvalTrait, method: &str) -> bool { .any(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) } +/// Returns case-insensitive method names declared directly by a pending trait. +fn trait_method_name_set(trait_decl: &EvalTrait) -> std::collections::HashSet { + trait_decl + .methods() + .iter() + .map(|method| method.name().to_ascii_lowercase()) + .collect() +} + /// Returns case-insensitive method names declared directly by a pending class. fn class_method_name_set(class: &EvalClass) -> std::collections::HashSet { class diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index 5a400905fc..f115c01adf 100644 --- a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -104,6 +104,65 @@ return (new EvalAliasNoopBox())->source();"#, assert_eq!(values.get(result), FakeValue::String("T".to_string())); } +/// Verifies traits can compose other eval traits before classes import them. +#[test] +fn execute_program_expands_eval_trait_used_by_trait() { + let program = parse_fragment( + br#"trait EvalNestedInner { + public const WORD = "in"; + public function word() { return self::WORD; } +} +trait EvalNestedOuter { + use EvalNestedInner { + word as private hiddenWord; + } + public function read() { return $this->word() . $this->hiddenWord(); } +} +class EvalNestedBox { + use EvalNestedOuter; +} +$box = new EvalNestedBox(); +echo $box->read(); echo ":"; +$ref = new ReflectionClass("EvalNestedOuter"); +$traits = $ref->getTraitNames(); +echo count($traits); echo ":"; echo $traits[0]; echo ":"; +$aliases = $ref->getTraitAliases(); +echo $aliases["hiddenWord"]; echo ":"; +$uses = class_uses($box); +echo count($uses); echo ":"; echo $uses["EvalNestedOuter"]; echo ":"; +return $box->word();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "inin:1:EvalNestedInner:EvalNestedInner::word:1:EvalNestedOuter:" + ); + assert_eq!(values.get(result), FakeValue::String("in".to_string())); +} + +/// Verifies trait-to-trait composition rejects missing inner traits. +#[test] +fn execute_program_rejects_missing_eval_trait_used_by_trait() { + let program = parse_fragment( + br#"trait EvalMissingNestedOuter { + use EvalMissingNestedInner; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing nested trait use should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies same-name trait aliases that change visibility remain composition fatals. #[test] fn execute_program_rejects_eval_trait_same_name_visibility_alias() { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 0d0bbab0f7..85d69315c3 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -1183,6 +1183,8 @@ impl Parser { let name = self.qualify_name_in_current_namespace(name); self.advance(); self.expect(TokenKind::LBrace)?; + let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); let mut constants = Vec::new(); let mut properties = Vec::new(); let mut methods = Vec::new(); @@ -1195,13 +1197,26 @@ impl Parser { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - self.parse_trait_member(&mut constants, &mut properties, &mut methods)?; + self.parse_trait_member( + &mut constants, + &mut properties, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; }; self.consume_semicolon(); Ok(vec![EvalStmt::TraitDecl( - EvalTrait::with_constants(name, constants, properties, methods) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) - .with_attributes(attributes), + EvalTrait::with_constants_traits_adaptations( + name, + constants, + properties, + methods, + traits, + trait_adaptations, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_attributes(attributes), )]) } @@ -1211,6 +1226,8 @@ impl Parser { constants: &mut Vec, properties: &mut Vec, methods: &mut Vec, + traits: &mut Vec, + trait_adaptations: &mut Vec, ) -> Result<(), EvalParseError> { let attributes = self.parse_optional_member_attributes()?; let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = @@ -1218,6 +1235,20 @@ impl Parser { if is_abstract && is_final { return Err(EvalParseError::UnsupportedConstruct); } + if visibility.is_none() + && !is_static + && !is_abstract + && !is_final + && !is_readonly + && set_visibility.is_none() + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + self.parse_class_trait_use(traits, trait_adaptations)?; + return Ok(()); + } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { if is_static || is_abstract || is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 3da8ebbad3..a4696f7fa3 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -1154,6 +1154,37 @@ class DynEvalUsesTrait { ] ); } + +/// Verifies trait declarations can compose other traits with adaptations. +#[test] +fn parse_fragment_accepts_trait_use_inside_trait() { + let program = parse_fragment( + br#"trait DynEvalInnerTrait { + public function read() { return "inner"; } +} +trait DynEvalOuterTrait { + use DynEvalInnerTrait { + read as private hiddenRead; + } + public function expose() { return $this->hiddenRead(); } +}"#, + ) + .expect("fragment should parse"); + + let [_, EvalStmt::TraitDecl(trait_decl)] = program.statements() else { + panic!("second statement should be a trait declaration"); + }; + assert_eq!(trait_decl.traits(), &["DynEvalInnerTrait".to_string()]); + assert_eq!( + trait_decl.trait_adaptations(), + &[EvalTraitAdaptation::Alias { + trait_name: None, + method: "read".to_string(), + alias: Some("hiddenRead".to_string()), + visibility: Some(EvalVisibility::Private), + }] + ); +} /// Verifies malformed object construction reports an unexpected token. #[test] fn parse_fragment_rejects_new_without_class_name() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 27e8b324c8..511c686996 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7207,6 +7207,47 @@ echo $box->talk();'); assert_eq!(out.stdout, "A:B:secret:A"); } +/// Verifies eval trait declarations can compose other eval traits. +#[test] +fn test_eval_declared_trait_uses_trait_composition() { + let out = compile_and_run_capture( + r#"word() . $this->hiddenWord(); } +} +class EvalNestedBox { + use EvalNestedOuter; +} +$box = new EvalNestedBox(); +echo $box->read() . ":"; +$ref = new ReflectionClass("EvalNestedOuter"); +$traits = $ref->getTraitNames(); +echo count($traits) . ":" . $traits[0] . ":"; +$aliases = $ref->getTraitAliases(); +echo $aliases["hiddenWord"] . ":"; +$uses = class_uses($box); +echo count($uses) . ":" . $uses["EvalNestedOuter"] . ":"; +echo $box->word();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "inin:1:EvalNestedInner:EvalNestedInner::word:1:EvalNestedOuter:in" + ); +} + /// Verifies eval-declared trait visibility adaptations affect bridge access checks. #[test] fn test_eval_declared_trait_visibility_adaptation_fails() { From 03101283eb4cdaac9d7c3b173e2611e660ce1d0e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 01:14:16 +0200 Subject: [PATCH 0689/1208] Support eval enum trait use --- crates/elephc-magician/src/eval_ir.rs | 44 ++++++++++++- .../src/interpreter/reflection.rs | 2 +- .../src/interpreter/statements.rs | 65 +++++++++++++++++++ .../src/interpreter/tests/enums.rs | 50 ++++++++++++++ .../elephc-magician/src/parser/statements.rs | 38 +++++++++-- .../elephc-magician/src/parser/tests/enums.rs | 28 ++++++++ tests/codegen/eval.rs | 50 ++++++++++++++ 7 files changed, 270 insertions(+), 7 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 4e39e4b422..eabcf57d1d 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -492,6 +492,8 @@ pub struct EvalEnum { backing_type: Option, interfaces: Vec, attributes: Vec, + traits: Vec, + trait_adaptations: Vec, cases: Vec, constants: Vec, methods: Vec, @@ -504,6 +506,8 @@ impl PartialEq for EvalEnum { && self.backing_type == other.backing_type && self.interfaces == other.interfaces && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations && self.cases == other.cases && self.constants == other.constants && self.methods == other.methods @@ -535,6 +539,29 @@ impl EvalEnum { cases: Vec, constants: Vec, methods: Vec, + ) -> Self { + Self::with_members_traits_adaptations( + name, + backing_type, + interfaces, + cases, + constants, + methods, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval enum with traits, adaptations, cases, constants, and methods. + pub fn with_members_traits_adaptations( + name: impl Into, + backing_type: Option, + interfaces: Vec, + cases: Vec, + constants: Vec, + methods: Vec, + traits: Vec, + trait_adaptations: Vec, ) -> Self { Self { name: name.into(), @@ -542,6 +569,8 @@ impl EvalEnum { backing_type, interfaces, attributes: Vec::new(), + traits, + trait_adaptations, cases, constants, methods, @@ -585,6 +614,16 @@ impl EvalEnum { &self.attributes } + /// Returns trait names used directly by this eval enum. + pub fn traits(&self) -> &[String] { + &self.traits + } + + /// Returns trait adaptations declared directly by this eval enum. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + /// Returns cases declared directly by this eval enum. pub fn cases(&self) -> &[EvalEnumCase] { &self.cases @@ -607,13 +646,14 @@ impl EvalEnum { /// Builds class-shaped metadata used for enum method and relation dispatch. pub fn as_class_metadata(&self) -> EvalClass { - EvalClass::with_modifiers_traits_and_constants( + EvalClass::with_modifiers_traits_adaptations_and_constants( self.name.clone(), false, true, None, self.interfaces.clone(), - Vec::new(), + self.traits.clone(), + self.trait_adaptations.clone(), self.constants.clone(), Vec::new(), self.methods.clone(), diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index d830ff2240..9847d9695a 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -5392,7 +5392,7 @@ fn eval_reflection_class_like_attributes( source_location: enum_decl.source_location(), attributes: enum_decl.attributes().to_vec(), interface_names: context.class_interface_names(enum_decl.name()), - trait_names: Vec::new(), + trait_names: context.class_trait_names(enum_decl.name()), method_names: context.class_method_names(enum_decl.name()), property_names: context.class_property_names(enum_decl.name()), parent_class_name: None, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e8f46424fa..0d8fc65a03 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -715,6 +715,8 @@ pub(in crate::interpreter) fn execute_enum_decl_stmt( { return Err(EvalStatus::RuntimeFatal); } + let enum_decl = expand_eval_enum_traits(enum_decl, context)?; + let enum_decl = &enum_decl; validate_eval_enum_decl(enum_decl, context, values)?; if context.define_enum(enum_decl.clone()) { initialize_eval_declared_constants( @@ -730,6 +732,69 @@ pub(in crate::interpreter) fn execute_enum_decl_stmt( } } +/// Expands eval trait uses into enum metadata while rejecting imported properties. +fn expand_eval_enum_traits( + enum_decl: &EvalEnum, + context: &ElephcEvalContext, +) -> Result { + if enum_decl.traits().is_empty() { + return Ok(enum_decl.clone()); + } + let enum_class = enum_decl.as_class_metadata(); + validate_eval_trait_adaptations(&enum_class, context)?; + let enum_method_names = class_method_name_set(&enum_class); + let mut trait_method_names = std::collections::HashSet::new(); + let mut trait_properties = std::collections::HashMap::new(); + let mut trait_constants = std::collections::HashMap::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for trait_name in enum_decl.traits() { + let Some(trait_decl) = context.trait_decl(trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_constants( + trait_decl, + enum_decl.constants(), + &mut trait_constants, + &mut constants, + )?; + append_eval_trait_properties( + trait_decl, + &[], + &mut trait_properties, + &mut properties, + )?; + append_eval_trait_methods( + trait_decl, + enum_decl.trait_adaptations(), + &enum_method_names, + &mut trait_method_names, + &mut methods, + )?; + } + if !properties.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + constants.extend(enum_decl.constants().iter().cloned()); + methods.extend(enum_decl.methods().iter().cloned()); + let mut expanded = EvalEnum::with_members_traits_adaptations( + enum_decl.name().to_string(), + enum_decl.backing_type(), + enum_decl.interfaces().to_vec(), + enum_decl.cases().to_vec(), + constants, + methods, + enum_decl.traits().to_vec(), + enum_decl.trait_adaptations().to_vec(), + ) + .with_attributes(enum_decl.attributes().to_vec()); + if let Some(source_location) = enum_decl.source_location() { + expanded = expanded.with_source_location(source_location); + } + Ok(expanded) +} + /// Validates enum metadata before it is inserted into the dynamic context. fn validate_eval_enum_decl( enum_decl: &EvalEnum, diff --git a/crates/elephc-magician/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs index 3990a27cfc..67fb9b69d4 100644 --- a/crates/elephc-magician/src/interpreter/tests/enums.rs +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -129,6 +129,45 @@ return EvalSuit::fallback() === EvalSuit::Hearts;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval enums can import trait methods and expose direct trait metadata. +#[test] +fn execute_program_dispatches_eval_enum_trait_use() { + let program = parse_fragment( + br#"trait EvalEnumTrait { + public function label() { return $this->name; } + public static function suffix() { return "S"; } +} +enum EvalTraitEnum { + use EvalEnumTrait { + label as private hiddenLabel; + } + case Ready; + public function read() { return $this->label() . ":" . $this->hiddenLabel(); } +} +echo EvalTraitEnum::Ready->read(); echo ":"; +echo EvalTraitEnum::suffix(); echo ":"; +$ref = new ReflectionClass("EvalTraitEnum"); +$traits = $ref->getTraitNames(); +echo count($traits); echo ":"; echo $traits[0]; echo ":"; +$aliases = $ref->getTraitAliases(); +echo $aliases["hiddenLabel"]; echo ":"; +$uses = class_uses(EvalTraitEnum::Ready); +echo count($uses); echo ":"; echo $uses["EvalEnumTrait"]; echo ":"; +return EvalTraitEnum::Ready->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Ready:Ready:S:1:EvalEnumTrait:EvalEnumTrait::label:1:EvalEnumTrait:" + ); + assert_eq!(values.get(result), FakeValue::String("Ready".to_string())); +} + /// Verifies eval enum interfaces can inherit PHP's native enum marker interfaces. #[test] fn execute_program_allows_eval_enum_marker_interface_inheritance() { @@ -195,6 +234,17 @@ fn execute_program_rejects_invalid_eval_enum_members() { "forbidden enum magic method lookup should be case-insensitive", ); + assert_invalid_enum_fragment( + br#"trait EvalInvalidEnumPropertyTrait { + public int $x = 1; +} +enum EvalInvalidEnumTraitProperty { + use EvalInvalidEnumPropertyTrait; + case Ready; +}"#, + "enum cannot import trait properties", + ); + assert_invalid_enum_fragment( br#"enum EvalInvalidExplicitUnitEnum implements UnitEnum { case Ready; diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 85d69315c3..a576e9c74a 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -1311,6 +1311,8 @@ impl Parser { let backing_type = self.parse_enum_backing_type()?; let interfaces = self.parse_class_interface_clause()?; self.expect(TokenKind::LBrace)?; + let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); let mut cases = Vec::new(); let mut constants = Vec::new(); let mut methods = Vec::new(); @@ -1323,13 +1325,28 @@ impl Parser { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - self.parse_enum_member(&mut cases, &mut constants, &mut methods)?; + self.parse_enum_member( + &mut cases, + &mut constants, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; }; self.consume_semicolon(); Ok(vec![EvalStmt::EnumDecl( - EvalEnum::with_members(name, backing_type, interfaces, cases, constants, methods) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) - .with_attributes(attributes), + EvalEnum::with_members_traits_adaptations( + name, + backing_type, + interfaces, + cases, + constants, + methods, + traits, + trait_adaptations, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_attributes(attributes), )]) } @@ -1360,6 +1377,8 @@ impl Parser { cases: &mut Vec, constants: &mut Vec, methods: &mut Vec, + traits: &mut Vec, + trait_adaptations: &mut Vec, ) -> Result<(), EvalParseError> { let attributes = self.parse_optional_member_attributes()?; if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) { @@ -1371,6 +1390,17 @@ impl Parser { if is_abstract || is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } + if visibility.is_none() + && !is_static + && !is_final + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + self.parse_class_trait_use(traits, trait_adaptations)?; + return Ok(()); + } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { if is_static { return Err(EvalParseError::UnsupportedConstruct); diff --git a/crates/elephc-magician/src/parser/tests/enums.rs b/crates/elephc-magician/src/parser/tests/enums.rs index 15e4b2887f..de8bb0da1d 100644 --- a/crates/elephc-magician/src/parser/tests/enums.rs +++ b/crates/elephc-magician/src/parser/tests/enums.rs @@ -76,3 +76,31 @@ fn parse_fragment_accepts_backed_enum_members() { ))] ); } + +/// Verifies enum declarations retain trait uses and adaptations. +#[test] +fn parse_fragment_accepts_enum_trait_use() { + let program = parse_fragment( + br#"enum EvalTraitEnum { + use EvalEnumTrait { + label as private hiddenLabel; + } + case Ready; +}"#, + ) + .expect("parse eval enum with trait use"); + + let [EvalStmt::EnumDecl(enum_decl)] = program.statements() else { + panic!("fragment should contain one enum declaration"); + }; + assert_eq!(enum_decl.traits(), &["EvalEnumTrait".to_string()]); + assert_eq!( + enum_decl.trait_adaptations(), + &[EvalTraitAdaptation::Alias { + trait_name: None, + method: "label".to_string(), + alias: Some("hiddenLabel".to_string()), + visibility: Some(EvalVisibility::Private), + }] + ); +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 511c686996..2f10a8fe5d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6450,6 +6450,56 @@ echo is_a(EvalDynColor::Red, "EvalDynLabel") ? "I" : "i";'); assert_eq!(out, "EC2Gcolor:Green:gFNI"); } +/// Verifies eval enums can import trait methods and report direct trait metadata. +#[test] +fn test_eval_declared_enum_trait_use() { + let out = compile_and_run( + r#"name; } + public static function suffix() { return "S"; } +} +enum EvalDynTraitEnum { + use EvalDynEnumTrait { + label as private hiddenLabel; + } + case Ready; + public function read() { return $this->label() . ":" . $this->hiddenLabel(); } +} +echo EvalDynTraitEnum::Ready->read(); echo ":"; +echo EvalDynTraitEnum::suffix(); echo ":"; +$ref = new ReflectionClass("EvalDynTraitEnum"); +$traits = $ref->getTraitNames(); +echo count($traits) . ":" . $traits[0] . ":"; +$aliases = $ref->getTraitAliases(); +echo $aliases["hiddenLabel"] . ":"; +$uses = class_uses(EvalDynTraitEnum::Ready); +echo count($uses) . ":" . $uses["EvalDynEnumTrait"] . ":"; +echo EvalDynTraitEnum::Ready->label();'); +"#, + ); + assert_eq!( + out, + "Ready:Ready:S:1:EvalDynEnumTrait:EvalDynEnumTrait::label:1:EvalDynEnumTrait:Ready" + ); + + let err = compile_and_run_expect_failure( + r#" Date: Fri, 26 Jun 2026 01:22:21 +0200 Subject: [PATCH 0690/1208] Expose eval enum marker relations --- crates/elephc-magician/src/context.rs | 40 ++++++++++++++++++- .../src/interpreter/tests/enums.rs | 11 ++++- tests/codegen/eval.rs | 11 ++++- 3 files changed, 59 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 9e89760483..ebc407944c 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -1416,15 +1416,47 @@ impl ElephcEvalContext { pub fn class_interface_names(&self, class_name: &str) -> Vec { let mut interfaces = Vec::new(); let mut seen = HashSet::new(); + let is_enum = self.enum_decl(class_name).is_some(); for class in self.class_chain(class_name) { for interface in class.interfaces() { push_unique_class_name(interface, &mut interfaces, &mut seen); - self.collect_interface_parent_names(interface, &mut interfaces, &mut seen); + self.collect_class_interface_parent_names( + interface, + is_enum, + &mut interfaces, + &mut seen, + ); + } + } + if let Some(enum_decl) = self.enum_decl(class_name) { + push_unique_class_name("UnitEnum", &mut interfaces, &mut seen); + if enum_decl.backing_type().is_some() { + push_unique_class_name("BackedEnum", &mut interfaces, &mut seen); } } interfaces } + /// Collects interface parents while preserving PHP enum marker interface ordering. + fn collect_class_interface_parent_names( + &self, + interface_name: &str, + skip_enum_markers: bool, + names: &mut Vec, + seen: &mut HashSet, + ) { + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + if skip_enum_markers && is_php_enum_marker_interface(parent) { + continue; + } + push_unique_class_name(parent, names, seen); + self.collect_class_interface_parent_names(parent, skip_enum_markers, names, seen); + } + } + /// Returns trait names used directly by an eval-declared class. pub fn class_trait_names(&self, class_name: &str) -> Vec { self.class(class_name).map_or_else(Vec::new, |class| { @@ -2774,6 +2806,12 @@ fn same_class_name(left: &str, right: &str) -> bool { normalize_class_name(left) == normalize_class_name(right) } +/// Returns whether a class-like name is one of PHP's native enum marker interfaces. +fn is_php_enum_marker_interface(name: &str) -> bool { + let name = name.trim_start_matches('\\'); + name.eq_ignore_ascii_case("UnitEnum") || name.eq_ignore_ascii_case("BackedEnum") +} + /// Pushes a case-insensitive PHP method name once for ReflectionClass metadata. fn push_unique_method_name(name: &str, names: &mut Vec, seen: &mut HashSet) { let key = normalize_method_name(name); diff --git a/crates/elephc-magician/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs index 67fb9b69d4..55ea6ffe89 100644 --- a/crates/elephc-magician/src/interpreter/tests/enums.rs +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -184,6 +184,12 @@ echo interface_exists("UnitEnum") ? "U" : "u"; echo ":"; echo interface_exists("BackedEnum") ? "B" : "b"; echo ":"; echo is_a(EvalMarkedUnit::Ready, "EvalUnitMarker") ? "unit" : "bad"; echo ":"; echo is_a(EvalMarkedBacked::Ready, "EvalBackedMarker") ? "backed" : "bad"; echo ":"; +$unitInterfaces = class_implements("EvalMarkedUnit"); +echo count($unitInterfaces); echo ":"; echo $unitInterfaces["EvalUnitMarker"]; echo ":"; +echo $unitInterfaces["UnitEnum"]; echo ":"; +$backedInterfaces = (new ReflectionClass("EvalMarkedBacked"))->getInterfaceNames(); +echo count($backedInterfaces); echo ":"; echo $backedInterfaces[0]; echo ":"; +echo $backedInterfaces[1]; echo ":"; echo $backedInterfaces[2]; echo ":"; return EvalMarkedBacked::Ready->value;"#, ) .expect("parse eval fragment"); @@ -192,7 +198,10 @@ return EvalMarkedBacked::Ready->value;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "U:B:unit:backed:"); + assert_eq!( + values.output, + "U:B:unit:backed:2:EvalUnitMarker:UnitEnum:3:EvalBackedMarker:UnitEnum:BackedEnum:" + ); assert_eq!( values.get(result), FakeValue::String("ready".to_string()) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2f10a8fe5d..e120f6071d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6515,10 +6515,19 @@ enum EvalDynMarkedBacked: string implements EvalDynBackedMarker { } echo is_a(EvalDynMarkedUnit::Ready, "EvalDynUnitMarker") ? "U" : "u"; echo is_a(EvalDynMarkedBacked::Ready, "EvalDynBackedMarker") ? "B" : "b"; +$unitInterfaces = class_implements("EvalDynMarkedUnit"); +echo count($unitInterfaces) . ":" . $unitInterfaces["EvalDynUnitMarker"] . ":"; +echo $unitInterfaces["UnitEnum"] . ":"; +$backedInterfaces = (new ReflectionClass("EvalDynMarkedBacked"))->getInterfaceNames(); +echo count($backedInterfaces) . ":" . $backedInterfaces[0] . ":"; +echo $backedInterfaces[1] . ":" . $backedInterfaces[2] . ":"; echo EvalDynMarkedBacked::Ready->value;'); "#, ); - assert_eq!(out, "UBready"); + assert_eq!( + out, + "UB2:EvalDynUnitMarker:UnitEnum:3:EvalDynBackedMarker:UnitEnum:BackedEnum:ready" + ); let err = compile_and_run_expect_failure( r#" Date: Fri, 26 Jun 2026 01:34:35 +0200 Subject: [PATCH 0691/1208] Align eval enum synthetic methods --- .../src/interpreter/builtins/symbols.rs | 8 +- .../src/interpreter/statements.rs | 71 ++++++++++++++--- .../src/interpreter/tests/enums.rs | 77 +++++++++++++++++++ tests/codegen/eval.rs | 54 +++++++++++++ 4 files changed, 200 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index b3eec77a8b..6e90842ee0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -784,12 +784,15 @@ fn eval_object_method_callable_probe( let Some(class) = context.dynamic_object_class(identity) else { return Ok(false); }; + if eval_enum_static_builtin_applies(class.name(), method_name, context).is_some() { + return Ok(true); + } let Some((declaring_class, method)) = eval_dynamic_method_for_call(class.name(), method_name, context) else { return Ok(false); }; - if method.is_static() || method.is_abstract() { + if method.is_abstract() { return Ok(false); } if method_name.eq_ignore_ascii_case("__invoke") { @@ -804,6 +807,9 @@ fn eval_static_method_callable_probe( method_name: &str, context: &ElephcEvalContext, ) -> bool { + if eval_enum_static_builtin_applies(class_name, method_name, context).is_some() { + return true; + } let Some((declaring_class, method)) = context.class_method(class_name, method_name) else { return false; }; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 0d8fc65a03..be83598811 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -715,6 +715,7 @@ pub(in crate::interpreter) fn execute_enum_decl_stmt( { return Err(EvalStatus::RuntimeFatal); } + validate_eval_enum_direct_method_declarations(enum_decl)?; let enum_decl = expand_eval_enum_traits(enum_decl, context)?; let enum_decl = &enum_decl; validate_eval_enum_decl(enum_decl, context, values)?; @@ -742,7 +743,8 @@ fn expand_eval_enum_traits( } let enum_class = enum_decl.as_class_metadata(); validate_eval_trait_adaptations(&enum_class, context)?; - let enum_method_names = class_method_name_set(&enum_class); + let mut enum_method_names = class_method_name_set(&enum_class); + insert_eval_enum_synthetic_method_names(enum_decl, &mut enum_method_names); let mut trait_method_names = std::collections::HashSet::new(); let mut trait_properties = std::collections::HashMap::new(); let mut trait_constants = std::collections::HashMap::new(); @@ -795,6 +797,18 @@ fn expand_eval_enum_traits( Ok(expanded) } +/// Adds PHP's enum-provided method names to the set that hides trait imports. +fn insert_eval_enum_synthetic_method_names( + enum_decl: &EvalEnum, + method_names: &mut std::collections::HashSet, +) { + method_names.insert(String::from("cases")); + if enum_decl.backing_type().is_some() { + method_names.insert(String::from("from")); + method_names.insert(String::from("tryfrom")); + } +} + /// Validates enum metadata before it is inserted into the dynamic context. fn validate_eval_enum_decl( enum_decl: &EvalEnum, @@ -803,7 +817,7 @@ fn validate_eval_enum_decl( ) -> Result<(), EvalStatus> { validate_eval_declared_constants(enum_decl.constants())?; validate_eval_enum_case_declarations(enum_decl)?; - validate_eval_enum_method_declarations(enum_decl)?; + validate_eval_enum_forbidden_magic_methods(enum_decl)?; let enum_class = enum_decl.as_class_metadata(); validate_eval_class_modifiers(&enum_class, context)?; validate_eval_enum_interfaces(enum_decl, &enum_class, context, values)?; @@ -858,16 +872,31 @@ fn validate_eval_enum_case_declarations(enum_decl: &EvalEnum) -> Result<(), Eval Ok(()) } -/// Validates enum method declarations that PHP reserves or forbids on enums. -fn validate_eval_enum_method_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { +/// Validates direct enum methods that PHP reserves on enum declarations. +fn validate_eval_enum_direct_method_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { for method in enum_decl.methods() { - if method.name().eq_ignore_ascii_case("cases") - || method.name().eq_ignore_ascii_case("from") - || method.name().eq_ignore_ascii_case("tryFrom") - || is_forbidden_eval_enum_magic_method(method.name()) + if method.name().eq_ignore_ascii_case("cases") { + return Err(EvalStatus::RuntimeFatal); + } + if enum_decl.backing_type().is_some() + && (method.name().eq_ignore_ascii_case("from") + || method.name().eq_ignore_ascii_case("tryFrom")) { return Err(EvalStatus::RuntimeFatal); } + if is_forbidden_eval_enum_magic_method(method.name()) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates enum methods, including trait imports, that PHP forbids by magic name. +fn validate_eval_enum_forbidden_magic_methods(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + for method in enum_decl.methods() { + if is_forbidden_eval_enum_magic_method(method.name()) { + return Err(EvalStatus::RuntimeFatal); + } } Ok(()) } @@ -3727,7 +3756,7 @@ pub(in crate::interpreter) fn eval_static_method_call_result( )? { return Ok(result); } - if context.has_enum(&class_name) && eval_enum_static_builtin_name(method_name).is_some() { + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { return eval_enum_builtin_static_method_result( &class_name, method_name, @@ -3888,6 +3917,21 @@ fn eval_enum_static_builtin_name(method_name: &str) -> Option<&'static str> { } } +/// Returns a synthetic enum method only when that enum actually provides it. +pub(in crate::interpreter) fn eval_enum_static_builtin_applies( + enum_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<&'static str> { + let enum_decl = context.enum_decl(enum_name)?; + match eval_enum_static_builtin_name(method_name)? { + "cases" => Some("cases"), + "from" if enum_decl.backing_type().is_some() => Some("from"), + "tryFrom" if enum_decl.backing_type().is_some() => Some("tryFrom"), + _ => None, + } +} + /// Dispatches enum-provided static methods for eval-declared enums. fn eval_enum_builtin_static_method_result( enum_name: &str, @@ -4700,6 +4744,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( ); }; let called_class_name = class.name().to_string(); + if eval_enum_static_builtin_applies(&called_class_name, method_name, context).is_some() { + return eval_enum_builtin_static_method_result( + &called_class_name, + method_name, + evaluated_args, + context, + values, + ); + } if let Some((class_name, method)) = eval_dynamic_method_for_call(&called_class_name, method_name, context) { diff --git a/crates/elephc-magician/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs index 55ea6ffe89..a1ee259a59 100644 --- a/crates/elephc-magician/src/interpreter/tests/enums.rs +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -168,6 +168,75 @@ return EvalTraitEnum::Ready->label();"#, assert_eq!(values.get(result), FakeValue::String("Ready".to_string())); } +/// Verifies enum synthetic methods hide conflicting trait imports like PHP. +#[test] +fn execute_program_uses_enum_synthetic_methods_over_trait_imports() { + let program = parse_fragment( + br#"trait EvalEnumSyntheticTrait { + public function cases() { return "trait-cases"; } + public static function from($value) { return "trait-from"; } + public static function tryFrom($value) { return "trait-try"; } +} +enum EvalPureSynthetic { + use EvalEnumSyntheticTrait { + cases as traitCases; + } + case Ready; +} +enum EvalBackedSynthetic: string { + use EvalEnumSyntheticTrait { + cases as traitCases; + from as traitFrom; + } + case Ready = "ready"; +} +echo is_array(EvalPureSynthetic::Ready->cases()) ? "cases" : "bad"; echo ":"; +echo EvalPureSynthetic::Ready->traitCases(); echo ":"; +echo EvalPureSynthetic::from("x"); echo ":"; +echo EvalPureSynthetic::Ready->from("x"); echo ":"; +echo EvalPureSynthetic::tryFrom("x"); echo ":"; +echo EvalBackedSynthetic::from("ready")->value; echo ":"; +echo EvalBackedSynthetic::Ready->from("ready")->value; echo ":"; +echo EvalBackedSynthetic::tryFrom("missing") === null ? "null" : "bad"; echo ":"; +echo EvalBackedSynthetic::traitFrom("x"); echo ":"; +echo EvalBackedSynthetic::Ready->traitCases(); echo ":"; +echo is_callable([EvalBackedSynthetic::Ready, "cases"]) ? "callable" : "bad";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "cases:trait-cases:trait-from:trait-from:trait-try:ready:ready:null:trait-from:trait-cases:callable" + ); +} + +/// Verifies pure eval enums may declare `from` and `tryFrom` methods directly. +#[test] +fn execute_program_allows_pure_eval_enum_direct_from_methods() { + let program = parse_fragment( + br#"enum EvalPureDirectFrom { + case Ready; + public static function from($value) { return "from:" . $value; } + public static function tryFrom($value) { return "try:" . $value; } +} +echo EvalPureDirectFrom::from("x"); echo ":"; +echo EvalPureDirectFrom::Ready->tryFrom("y"); echo ":"; +return is_callable([EvalPureDirectFrom::Ready, "from"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "from:x:try:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval enum interfaces can inherit PHP's native enum marker interfaces. #[test] fn execute_program_allows_eval_enum_marker_interface_inheritance() { @@ -227,6 +296,14 @@ fn execute_program_rejects_invalid_eval_enum_members() { "reserved enum method should fail", ); + assert_invalid_enum_fragment( + br#"enum EvalInvalidBackedEnumMethod: string { + case Ready = "ready"; + public static function from($value) { return self::Ready; } +}"#, + "backed enum from method should fail", + ); + assert_invalid_enum_fragment( br#"enum EvalInvalidEnumMagicMethod { case Ready; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e120f6071d..a32ae9af5c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6500,6 +6500,60 @@ enum EvalDynInvalidTraitEnum { ); } +/// Verifies eval enum synthetic methods hide conflicting trait imports like PHP. +#[test] +fn test_eval_declared_enum_trait_synthetic_method_precedence() { + let out = compile_and_run( + r#"cases()) ? "cases" : "bad"; echo ":"; +echo EvalDynPureSynthetic::Ready->traitCases(); echo ":"; +echo EvalDynPureSynthetic::from("x"); echo ":"; +echo EvalDynPureSynthetic::Ready->from("x"); echo ":"; +echo EvalDynBackedSynthetic::from("ready")->value; echo ":"; +echo EvalDynBackedSynthetic::Ready->from("ready")->value; echo ":"; +echo EvalDynBackedSynthetic::tryFrom("missing") === null ? "null" : "bad"; echo ":"; +echo EvalDynBackedSynthetic::traitFrom("x"); echo ":"; +echo EvalDynBackedSynthetic::Ready->traitCases(); echo ":"; +echo is_callable([EvalDynBackedSynthetic::Ready, "cases"]) ? "callable" : "bad";'); +"#, + ); + assert_eq!( + out, + "cases:trait-cases:trait-from:trait-from:ready:ready:null:trait-from:trait-cases:callable" + ); + + let err = compile_and_run_expect_failure( + r#" Date: Fri, 26 Jun 2026 01:48:39 +0200 Subject: [PATCH 0692/1208] Reflect eval enum synthetic methods --- .../src/interpreter/reflection.rs | 213 +++++++++++++----- .../src/interpreter/statements.rs | 2 +- .../src/interpreter/tests/enums.rs | 59 ++++- tests/codegen/eval.rs | 39 ++++ 4 files changed, 251 insertions(+), 62 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 9847d9695a..58b19fb379 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -6366,71 +6366,70 @@ fn eval_reflection_method_metadata( context: &ElephcEvalContext, ) -> Option { if context.has_class(class_name) || context.has_enum(class_name) { - return context - .class_method(class_name, method_name) - .map(|(declaring_class, method)| { - let required_parameter_count = eval_reflection_required_parameter_count( - method.parameter_defaults(), - method.parameter_is_variadic(), - ); - let flags = eval_reflection_member_flags( + if let Some((declaring_class, method)) = context.class_method(class_name, method_name) { + let required_parameter_count = eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ); + let flags = eval_reflection_member_flags( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + false, + ); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(declaring_class.clone()), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let promoted_parameter_names = eval_reflection_promoted_parameter_names( + &declaring_class, + method.name(), + context, + ); + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(declaring_class.as_str()), + Some(&declaring_function), + method.params(), + method.parameter_has_types(), + method.parameter_types(), + method.parameter_attributes(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + &promoted_parameter_names, + ); + return Some(EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + source_location: method.source_location(), + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( method.visibility(), method.is_static(), method.is_final(), method.is_abstract(), - false, - ); - let return_type_metadata = method - .return_type() - .and_then(eval_reflection_parameter_type_metadata); - let declaring_function = EvalReflectionDeclaringFunctionMetadata { - name: method.name().to_string(), - declaring_class_name: Some(declaring_class.clone()), - attributes: method.attributes().to_vec(), - flags, - required_parameter_count, - }; - let promoted_parameter_names = eval_reflection_promoted_parameter_names( - &declaring_class, - method.name(), - context, - ); - let parameters = eval_reflection_parameters_from_names_and_type_flags( - Some(declaring_class.as_str()), - Some(&declaring_function), - method.params(), - method.parameter_has_types(), - method.parameter_types(), - method.parameter_attributes(), - method.parameter_defaults(), - method.parameter_is_by_ref(), - method.parameter_is_variadic(), - &promoted_parameter_names, - ); - EvalReflectionMemberMetadata { - declaring_class_name: Some(declaring_class), - source_location: method.source_location(), - attributes: method.attributes().to_vec(), - visibility: method.visibility(), - is_static: method.is_static(), - is_final: method.is_final(), - is_abstract: method.is_abstract(), - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_method_modifiers( - method.visibility(), - method.is_static(), - method.is_final(), - method.is_abstract(), - ), - type_metadata: None, - return_type_metadata, - default_value: None, - required_parameter_count, - parameters, - } + ), + type_metadata: None, + return_type_metadata, + default_value: None, + required_parameter_count, + parameters, }); + } + return eval_reflection_enum_synthetic_method_metadata(class_name, method_name, context); } if context.has_interface(class_name) { return context @@ -6564,6 +6563,91 @@ fn eval_reflection_method_metadata( }) } +/// Builds ReflectionMethod metadata for PHP's enum-provided synthetic methods. +fn eval_reflection_enum_synthetic_method_metadata( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option { + let synthetic_name = eval_enum_static_builtin_applies(class_name, method_name, context)?; + let enum_decl = context.enum_decl(class_name)?; + let declaring_class_name = enum_decl.name().trim_start_matches('\\').to_string(); + let flags = eval_reflection_member_flags(EvalVisibility::Public, true, false, false, false); + let (parameter_names, parameter_types, return_type_metadata) = match synthetic_name { + "cases" => ( + Vec::new(), + Vec::new(), + Some(eval_reflection_parameter_type_metadata(&EvalParameterType::new( + vec![EvalParameterTypeVariant::Array], + false, + ))?), + ), + "from" | "tryFrom" => { + let return_type = EvalParameterType::new( + vec![EvalParameterTypeVariant::Class(String::from("static"))], + synthetic_name == "tryFrom", + ); + ( + vec![String::from("value")], + vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String, EvalParameterTypeVariant::Int], + false, + ))], + Some(eval_reflection_parameter_type_metadata(&return_type)?), + ) + } + _ => return None, + }; + let parameter_count = parameter_names.len(); + let parameter_has_types = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); + let parameter_attributes = vec![Vec::new(); parameter_count]; + let parameter_defaults = vec![None; parameter_count]; + let parameter_is_by_ref = vec![false; parameter_count]; + let parameter_is_variadic = vec![false; parameter_count]; + let required_parameter_count = + eval_reflection_required_parameter_count(¶meter_defaults, ¶meter_is_variadic); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: synthetic_name.to_string(), + declaring_class_name: Some(declaring_class_name.clone()), + attributes: Vec::new(), + flags, + required_parameter_count, + }; + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(&declaring_class_name), + Some(&declaring_function), + ¶meter_names, + ¶meter_has_types, + ¶meter_types, + ¶meter_attributes, + ¶meter_defaults, + ¶meter_is_by_ref, + ¶meter_is_variadic, + &[], + ); + Some(EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class_name), + source_location: None, + attributes: Vec::new(), + visibility: EvalVisibility::Public, + is_static: true, + is_final: false, + is_abstract: false, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers(EvalVisibility::Public, true, false, false), + type_metadata: None, + return_type_metadata, + default_value: None, + required_parameter_count, + parameters, + }) +} + /// Returns property metadata for a property-like member on an eval class-like symbol. fn eval_reflection_property_metadata( class_name: &str, @@ -7267,6 +7351,15 @@ fn eval_reflection_method_invoke_dispatch( values, ); } + if eval_enum_static_builtin_applies(declaring_class, &lookup_method_name, context).is_some() { + return eval_enum_builtin_static_method_result( + declaring_class, + &lookup_method_name, + method_args, + context, + values, + ); + } eval_reflection_aot_method_invoke_dispatch( declaring_class, method_name, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index be83598811..25a4090021 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3933,7 +3933,7 @@ pub(in crate::interpreter) fn eval_enum_static_builtin_applies( } /// Dispatches enum-provided static methods for eval-declared enums. -fn eval_enum_builtin_static_method_result( +pub(in crate::interpreter) fn eval_enum_builtin_static_method_result( enum_name: &str, method_name: &str, evaluated_args: Vec, diff --git a/crates/elephc-magician/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs index a1ee259a59..33f8a5415c 100644 --- a/crates/elephc-magician/src/interpreter/tests/enums.rs +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -206,7 +206,12 @@ echo is_callable([EvalBackedSynthetic::Ready, "cases"]) ? "callable" : "bad";"#, let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); assert_eq!( values.output, @@ -237,6 +242,58 @@ return is_callable([EvalPureDirectFrom::Ready, "from"]);"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod metadata and invocation for enum synthetic methods. +#[test] +fn execute_program_reflects_eval_enum_synthetic_methods() { + let program = parse_fragment( + br#"enum EvalReflectSyntheticEnum: string { + case Ready = "ready"; +} +enum EvalReflectPureSyntheticEnum { + case Ready; +} +$ref = new ReflectionClass("EvalReflectSyntheticEnum"); +$methods = $ref->getMethods(ReflectionMethod::IS_STATIC); +echo count($methods); echo ":"; +echo $methods[0]->getName(); echo "/"; +echo $methods[1]->getName(); echo "/"; +echo $methods[2]->getName(); echo ":"; +$cases = $ref->getMethod("cases"); +echo $cases->getReturnType(); echo ":"; +echo count($cases->invoke(null)); echo ":"; +$from = new ReflectionMethod("EvalReflectSyntheticEnum", "from"); +$params = $from->getParameters(); +echo $from->getDeclaringClass()->getName(); echo ":"; +echo $from->getNumberOfParameters(); echo "/"; +echo $from->getNumberOfRequiredParameters(); echo ":"; +echo $params[0]->getName(); echo "/"; +echo $params[0]->getType(); echo ":"; +echo $from->getReturnType(); echo ":"; +echo $from->invoke(null, "ready")->name; echo ":"; +$try = ReflectionMethod::createFromMethodName("EvalReflectSyntheticEnum::tryFrom"); +echo $try->getReturnType(); echo ":"; +echo $try->invokeArgs(null, ["missing"]) === null ? "null" : "bad"; echo ":"; +$pure = new ReflectionClass("EvalReflectPureSyntheticEnum"); +echo count($pure->getMethods()); echo ":"; +echo $pure->hasMethod("from") ? "bad" : "nofrom";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "3:cases/from/tryFrom:array:1:EvalReflectSyntheticEnum:1/1:value/string|int:static:Ready:?static:null:1:nofrom" + ); +} + /// Verifies eval enum interfaces can inherit PHP's native enum marker interfaces. #[test] fn execute_program_allows_eval_enum_marker_interface_inheritance() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a32ae9af5c..0dff5374fa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6554,6 +6554,45 @@ eval('enum EvalDynInvalidBackedFrom: string { ); } +/// Verifies eval ReflectionMethod supports enum synthetic method metadata and invocation. +#[test] +fn test_eval_reflection_enum_synthetic_methods() { + let out = compile_and_run( + r#"getMethods(ReflectionMethod::IS_STATIC); +echo count($methods) . ":"; +echo $methods[0]->getName() . "/" . $methods[1]->getName() . "/" . $methods[2]->getName() . ":"; +$cases = $ref->getMethod("cases"); +echo $cases->getReturnType() . ":"; +echo count($cases->invoke(null)) . ":"; +$from = new ReflectionMethod("EvalDynReflectSyntheticEnum", "from"); +$params = $from->getParameters(); +echo $from->getDeclaringClass()->getName() . ":"; +echo $from->getNumberOfParameters() . "/" . $from->getNumberOfRequiredParameters() . ":"; +echo $params[0]->getName() . "/" . $params[0]->getType() . ":"; +echo $from->getReturnType() . ":"; +echo $from->invoke(null, "ready")->name . ":"; +$try = ReflectionMethod::createFromMethodName("EvalDynReflectSyntheticEnum::tryFrom"); +echo $try->getReturnType() . ":"; +echo ($try->invokeArgs(null, ["missing"]) === null ? "null" : "bad") . ":"; +$pure = new ReflectionClass("EvalDynReflectPureSyntheticEnum"); +echo count($pure->getMethods()) . ":"; +echo $pure->hasMethod("from") ? "bad" : "nofrom";'); +"#, + ); + assert_eq!( + out, + "3:cases/from/tryFrom:array:1:EvalDynReflectSyntheticEnum:1/1:value/string|int:static:Ready:?static:null:1:nofrom" + ); +} + /// Verifies eval enums support user interfaces derived from PHP enum marker interfaces. #[test] fn test_eval_declared_enum_marker_interface_inheritance() { From d4707411f3ea4dc672236657c21f4a3fbeb0785c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 02:03:47 +0200 Subject: [PATCH 0693/1208] Throw eval reflection method lookup errors --- .../src/interpreter/reflection.rs | 94 +++++++++++++++---- .../tests/builtins_class_metadata.rs | 59 ++++++++++++ tests/codegen/eval.rs | 55 +++++++++++ 3 files changed, 192 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 58b19fb379..a9f3af74fa 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3227,26 +3227,30 @@ pub(in crate::interpreter) fn eval_reflection_method_create_from_method_name_res } let args = bind_evaluated_function_args(&[String::from("method")], evaluated_args)?; let target = eval_reflection_string_arg(args[0], values)?; - let (class_name, method_name) = eval_reflection_method_target_parts(&target)?; - eval_reflection_method_object_result_if_exists( + let Some((class_name, method_name)) = eval_reflection_method_target_parts(&target) else { + return eval_throw_reflection_exception( + "ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name", + context, + values, + ); + }; + eval_reflection_method_object_result_or_throw( &class_name, &method_name, context, values, - )? - .map(Some) - .ok_or(EvalStatus::RuntimeFatal) + ) } /// Splits PHP's `ClassName::methodName` reflection-method target string. -fn eval_reflection_method_target_parts(target: &str) -> Result<(String, String), EvalStatus> { +fn eval_reflection_method_target_parts(target: &str) -> Option<(String, String)> { let Some((class_name, method_name)) = target.rsplit_once("::") else { - return Err(EvalStatus::RuntimeFatal); + return None; }; if class_name.is_empty() || method_name.is_empty() { - return Err(EvalStatus::RuntimeFatal); + return None; } - Ok((class_name.to_string(), method_name.to_string())) + Some((class_name.to_string(), method_name.to_string())) } /// Extracts the deprecated one-argument `ReflectionMethod("Class::method")` target. @@ -3302,10 +3306,14 @@ fn eval_reflection_method_object_result_if_exists( &reflected_name, requested_method_name, context, - ) - .ok_or(EvalStatus::RuntimeFatal)?; - let method = eval_reflection_method_metadata(&reflected_name, &method_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; + ); + let Some(method_name) = method_name else { + return Ok(None); + }; + let Some(method) = eval_reflection_method_metadata(&reflected_name, &method_name, context) + else { + return Ok(None); + }; eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_METHOD, &method_name, @@ -3316,6 +3324,41 @@ fn eval_reflection_method_object_result_if_exists( .map(Some) } +/// Builds a `ReflectionMethod` object or throws PHP's catchable reflection error. +fn eval_reflection_method_object_result_or_throw( + class_name: &str, + requested_method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(result) = eval_reflection_method_object_result_if_exists( + class_name, + requested_method_name, + context, + values, + )? { + return Ok(Some(result)); + } + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + eval_throw_reflection_exception( + &format!( + "Method {}::{}() does not exist", + reflected_name, requested_method_name + ), + context, + values, + ) +} + /// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. fn eval_reflection_method_new( evaluated_args: Vec, @@ -3325,8 +3368,14 @@ fn eval_reflection_method_new( if evaluated_args.len() == 1 { let target = eval_reflection_method_single_target_arg(evaluated_args)?; let target = eval_reflection_string_arg(target, values)?; - let (class_name, method_name) = eval_reflection_method_target_parts(&target)?; - return eval_reflection_method_object_result_if_exists( + let Some((class_name, method_name)) = eval_reflection_method_target_parts(&target) else { + return eval_throw_reflection_exception( + "ReflectionMethod::__construct(): Argument #1 ($objectOrMethod) must be a valid method name", + context, + values, + ); + }; + return eval_reflection_method_object_result_or_throw( &class_name, &method_name, context, @@ -3339,7 +3388,7 @@ fn eval_reflection_method_new( )?; let class_name = eval_reflection_class_target_name(args[0], context, values)?; let requested_method_name = eval_reflection_string_arg(args[1], values)?; - eval_reflection_method_object_result_if_exists( + eval_reflection_method_object_result_or_throw( &class_name, &requested_method_name, context, @@ -6273,6 +6322,19 @@ fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> || context.has_enum(name) } +/// Returns true when a name resolves to eval or runtime class-like metadata. +fn eval_reflection_class_like_or_runtime_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_reflection_class_like_exists(name, context) + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.trait_exists(name)? + || values.enum_exists(name)?) +} + /// Returns true when one name exists as an eval or runtime interface. fn eval_reflection_interface_exists( name: &str, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index b96612ad1d..9163a84024 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -1756,6 +1756,65 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_method_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingMethodTarget {} +try { + new ReflectionMethod("EvalReflectMissingMethodTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalReflectMissingMethodTarget::missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + ReflectionMethod::createFromMethodName("EvalReflectMissingMethodTarget::missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalReflectMissingClass", "run"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + ReflectionMethod::createFromMethodName("not-a-method"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Method EvalReflectMissingMethodTarget::missing() does not exist|Method EvalReflectMissingMethodTarget::missing() does not exist|Method EvalReflectMissingMethodTarget::missing() does not exist|Class \"EvalReflectMissingClass\" does not exist|ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + /// Verifies eval member and enum-case reflectors expose their declaring class. #[test] fn execute_program_reflects_eval_declaring_class_metadata() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0dff5374fa..b80bc211fd 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11536,6 +11536,61 @@ echo $ref->invoke(new EvalReflectCtorMethodTarget());'); ); } +/// Verifies eval ReflectionMethod construction errors are catchable ReflectionException objects. +#[test] +fn test_eval_reflection_method_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +class EvalDynReflectMissingMethodTarget {} +try { + new ReflectionMethod("EvalDynReflectMissingMethodTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalDynReflectMissingMethodTarget::missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalDynReflectMissingClass", "run"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + ReflectionMethod::createFromMethodName("not-a-method"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Method EvalAotReflectMissingMethodTarget::missing() does not exist|Method EvalDynReflectMissingMethodTarget::missing() does not exist|Method EvalDynReflectMissingMethodTarget::missing() does not exist|Class \"EvalDynReflectMissingClass\" does not exist|ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name" + ); +} + /// Verifies eval-declared final properties cannot be redeclared by subclasses. #[test] fn test_eval_declared_final_property_override_fails() { From 0ed3a8043d97ad42889aa1c097873d87917dc0f7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 02:10:08 +0200 Subject: [PATCH 0694/1208] Throw eval reflection property lookup errors --- .../src/interpreter/reflection.rs | 92 +++++++++++++++---- .../tests/builtins_class_metadata.rs | 50 ++++++++++ tests/codegen/eval.rs | 56 +++++++++++ 3 files changed, 179 insertions(+), 19 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index a9f3af74fa..0c9e28bb34 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3411,29 +3411,45 @@ fn eval_reflection_property_new( return eval_reflection_property_new_for_object(args[0], &property_name, context, values); } let class_name = eval_reflection_string_arg(args[0], values)?; - if !eval_reflection_class_like_exists(&class_name, context) { - if let Some(property) = eval_reflection_aot_property_metadata_if_exists( - &class_name, - &property_name, + eval_reflection_property_object_result_or_throw(&class_name, &property_name, context, values) +} + +/// Builds a `ReflectionProperty` object when the reflected property exists. +fn eval_reflection_property_object_result_if_exists( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_exists(&reflected_name, context) { + let Some(property) = eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + property_name, context, values, - )? { - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - &property_name, - &property, - context, - values, - ) - .map(Some); - } - return Ok(None); + )? + else { + return Ok(None); + }; + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); } - let property = eval_reflection_property_metadata(&class_name, &property_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; + let Some(property) = eval_reflection_property_metadata(&reflected_name, property_name, context) + else { + return Ok(None); + }; eval_reflection_member_object_result( EVAL_REFLECTION_OWNER_PROPERTY, - &property_name, + property_name, &property, context, values, @@ -3441,6 +3457,39 @@ fn eval_reflection_property_new( .map(Some) } +/// Builds a `ReflectionProperty` object or throws PHP's catchable reflection error. +fn eval_reflection_property_object_result_or_throw( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(result) = eval_reflection_property_object_result_if_exists( + class_name, + property_name, + context, + values, + )? { + return Ok(Some(result)); + } + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + let message = eval_reflection_missing_member_message( + EVAL_REFLECTION_OWNER_PROPERTY, + &reflected_name, + property_name, + ); + eval_throw_reflection_exception(&message, context, values) +} + /// Builds a ReflectionProperty from an object argument, including dynamic properties. fn eval_reflection_property_new_for_object( object: RuntimeCellHandle, @@ -3460,7 +3509,12 @@ fn eval_reflection_property_new_for_object( .map(Some); } if !eval_reflection_object_dynamic_property_exists(object, property_name, values)? { - return Ok(None); + let message = eval_reflection_missing_member_message( + EVAL_REFLECTION_OWNER_PROPERTY, + &class_name, + property_name, + ); + return eval_throw_reflection_exception(&message, context, values); } let property = eval_reflection_dynamic_property_metadata(&class_name); eval_reflection_member_object_result( diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 9163a84024..24a154b6cd 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -1815,6 +1815,56 @@ return true;"#, assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_property_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingPropertyTarget {} +$object = new EvalReflectMissingPropertyTarget(); +$object->dynamic = 1; +try { + new ReflectionProperty("EvalReflectMissingPropertyTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty("EvalReflectMissingPropertyClass", "value"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty($object, "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +$property = new ReflectionProperty($object, "dynamic"); +echo $property->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Property EvalReflectMissingPropertyTarget::$missing does not exist|Class \"EvalReflectMissingPropertyClass\" does not exist|Property EvalReflectMissingPropertyTarget::$missing does not exist|dynamic" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + /// Verifies eval member and enum-case reflectors expose their declaring class. #[test] fn execute_program_reflects_eval_declaring_class_metadata() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b80bc211fd..6c2cff76d6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11591,6 +11591,62 @@ try { ); } +/// Verifies eval ReflectionProperty construction errors are catchable ReflectionException objects. +#[test] +fn test_eval_reflection_property_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +class EvalDynReflectMissingPropertyTarget {} +$object = new EvalDynReflectMissingPropertyTarget(); +$object->dynamic = 1; +try { + new ReflectionProperty("EvalDynReflectMissingPropertyTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty("EvalDynReflectMissingPropertyClass", "value"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty($object, "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionProperty($object, "dynamic"))->getName() . ":"; +echo (new ReflectionProperty("EvalAotReflectMissingPropertyTarget", "known"))->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Property EvalAotReflectMissingPropertyTarget::$missing does not exist|Property EvalDynReflectMissingPropertyTarget::$missing does not exist|Class \"EvalDynReflectMissingPropertyClass\" does not exist|Property EvalDynReflectMissingPropertyTarget::$missing does not exist|dynamic:known" + ); +} + /// Verifies eval-declared final properties cannot be redeclared by subclasses. #[test] fn test_eval_declared_final_property_override_fails() { From 1d4de6065df47e8efd9dfc223e0aab8081c63ff4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 02:21:26 +0200 Subject: [PATCH 0695/1208] Throw eval reflection constant lookup errors --- .../src/interpreter/reflection.rs | 130 +++++++++++++++--- .../tests/builtins_class_metadata.rs | 121 ++++++++++++++++ tests/codegen/eval.rs | 123 +++++++++++++++++ 3 files changed, 357 insertions(+), 17 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 0c9e28bb34..06d6795ff7 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -4134,16 +4134,32 @@ fn eval_reflection_class_constant_new( )?; let class_name = eval_reflection_string_arg(args[0], values)?; let constant_name = eval_reflection_string_arg(args[1], values)?; + eval_reflection_class_constant_object_result_or_throw( + &class_name, + &constant_name, + context, + values, + ) +} + +/// Builds a `ReflectionClassConstant` object or throws PHP's catchable error. +fn eval_reflection_class_constant_object_result_or_throw( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { let Some((declaring_class_name, attributes, visibility, is_final, is_enum_case)) = - eval_reflection_class_constant_metadata(&class_name, &constant_name, context, values)? + eval_reflection_class_constant_metadata(class_name, constant_name, context, values)? else { - return if eval_reflection_class_like_exists(&class_name, context) { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(None) - }; + return eval_reflection_missing_class_constant_exception( + class_name, + constant_name, + context, + values, + ); }; - let constant_value = eval_reflection_constant_value(&class_name, &constant_name, context, values)? + let constant_value = eval_reflection_constant_value(class_name, constant_name, context, values)? .ok_or(EvalStatus::RuntimeFatal)?; let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); if is_enum_case { @@ -4152,7 +4168,7 @@ fn eval_reflection_class_constant_new( let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS_CONSTANT, - &constant_name, + constant_name, &attributes, &[], &[], @@ -4173,6 +4189,30 @@ fn eval_reflection_class_constant_new( .map(Some) } +/// Throws the catchable ReflectionException for a missing class-like constant. +fn eval_reflection_missing_class_constant_exception( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + eval_throw_reflection_exception( + &format!("Constant {}::{} does not exist", reflected_name, constant_name), + context, + values, + ) +} + /// Builds an eval-backed ReflectionEnumUnitCase/BackedCase object for an enum case. fn eval_reflection_enum_case_new( owner_kind: u64, @@ -4185,18 +4225,57 @@ fn eval_reflection_enum_case_new( evaluated_args, )?; let enum_name = eval_reflection_string_arg(args[0], values)?; + let case_name = eval_reflection_string_arg(args[1], values)?; let Some(enum_decl) = context.enum_decl(&enum_name) else { - return if eval_reflection_class_like_exists(&enum_name, context) { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(None) - }; + if eval_reflection_class_constant_metadata(&enum_name, &case_name, context, values)? + .is_some() + { + return eval_reflection_not_enum_case_exception( + &enum_name, &case_name, context, values, + ); + } + if !eval_reflection_class_like_exists(&enum_name, context) + && eval_reflection_class_like_or_runtime_exists(&enum_name, context, values)? + { + return Ok(None); + } + return eval_reflection_missing_class_constant_exception( + &enum_name, &case_name, context, values, + ); }; - if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && enum_decl.backing_type().is_none() { - return Err(EvalStatus::RuntimeFatal); - } - let case_name = eval_reflection_string_arg(args[1], values)?; let declaring_class_name = enum_decl.name().to_string(); + let has_case = enum_decl.case(&case_name).is_some(); + let is_backed = enum_decl.backing_type().is_some(); + if !has_case { + if eval_reflection_class_constant_metadata( + &declaring_class_name, + &case_name, + context, + values, + )? + .is_some() + { + return eval_reflection_not_enum_case_exception( + &declaring_class_name, + &case_name, + context, + values, + ); + } + return eval_reflection_missing_class_constant_exception( + &declaring_class_name, + &case_name, + context, + values, + ); + } + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && !is_backed { + return eval_throw_reflection_exception( + &format!("Enum case {}::{} is not a backed case", declaring_class_name, case_name), + context, + values, + ); + } eval_reflection_enum_case_object_result( owner_kind, &declaring_class_name, @@ -4207,6 +4286,23 @@ fn eval_reflection_enum_case_new( .map(Some) } +/// Throws the catchable ReflectionException for a constant that is not an enum case. +fn eval_reflection_not_enum_case_exception( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + eval_throw_reflection_exception( + &format!("Constant {}::{} is not a case", reflected_name, constant_name), + context, + values, + ) +} + /// Builds one eval-backed enum-case reflection owner object. fn eval_reflection_enum_case_object_result( owner_kind: u64, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 24a154b6cd..36952ff0bc 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -1865,6 +1865,127 @@ return true;"#, assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); } +/// Verifies ReflectionClassConstant construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_class_constant_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingConstantTarget { + public const OK = 1; +} +try { + new ReflectionClassConstant("EvalReflectMissingConstantTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionClassConstant("EvalReflectMissingConstantClass", "VALUE"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionClassConstant("EvalReflectMissingConstantTarget", "OK"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Constant EvalReflectMissingConstantTarget::missing does not exist|Class \"EvalReflectMissingConstantClass\" does not exist|OK" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionEnumUnitCase/BackedCase construction throws PHP reflection errors. +#[test] +fn execute_program_reflection_enum_case_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"enum EvalReflectMissingCaseUnit { + case Ready; + public const TOKEN = 1; +} +enum EvalReflectMissingCaseBacked: string { + case Ready = "ready"; + public const TOKEN = 1; +} +class EvalReflectMissingCaseClass { + public const TOKEN = 1; +} +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseUnit", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseClass", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseUnit", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseUnit", "Ready"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseBacked", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseClass", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnumUnitCase("EvalReflectMissingCaseBacked", "Ready"))->getName(); echo ":"; +echo (new ReflectionEnumBackedCase("EvalReflectMissingCaseBacked", "Ready"))->getBackingValue(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Constant EvalReflectMissingCaseUnit::Missing does not exist|Constant EvalReflectMissingCaseClass::TOKEN is not a case|Constant EvalReflectMissingCaseUnit::TOKEN is not a case|Enum case EvalReflectMissingCaseUnit::Ready is not a backed case|Constant EvalReflectMissingCaseBacked::Missing does not exist|Constant EvalReflectMissingCaseClass::Missing does not exist|Ready:ready" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + /// Verifies eval member and enum-case reflectors expose their declaring class. #[test] fn execute_program_reflects_eval_declaring_class_metadata() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6c2cff76d6..26db63afb4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11647,6 +11647,129 @@ echo (new ReflectionProperty("EvalAotReflectMissingPropertyTarget", "known"))->g ); } +/// Verifies eval ReflectionClassConstant construction errors are catchable objects. +#[test] +fn test_eval_reflection_class_constant_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +class EvalDynReflectMissingConstantTarget { + public const OK = 1; +} +try { + new ReflectionClassConstant("EvalDynReflectMissingConstantTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionClassConstant("EvalDynReflectMissingConstantClass", "VALUE"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionClassConstant("EvalDynReflectMissingConstantTarget", "OK"))->getName() . ":"; +echo (new ReflectionClassConstant("EvalAotReflectMissingConstantTarget", "OK"))->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Constant EvalAotReflectMissingConstantTarget::missing does not exist|Constant EvalDynReflectMissingConstantTarget::missing does not exist|Class \"EvalDynReflectMissingConstantClass\" does not exist|OK:OK" + ); +} + +/// Verifies eval ReflectionEnumUnitCase/BackedCase construction errors are catchable objects. +#[test] +fn test_eval_reflection_enum_case_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalDynReflectMissingCaseClass", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalDynReflectMissingCaseUnit", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalDynReflectMissingCaseUnit", "Ready"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalDynReflectMissingCaseBacked", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalDynReflectMissingCaseClass", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnumUnitCase("EvalDynReflectMissingCaseBacked", "Ready"))->getName() . ":"; +echo (new ReflectionEnumBackedCase("EvalDynReflectMissingCaseBacked", "Ready"))->getBackingValue(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Constant EvalDynReflectMissingCaseUnit::Missing does not exist|Constant EvalDynReflectMissingCaseClass::TOKEN is not a case|Constant EvalDynReflectMissingCaseUnit::TOKEN is not a case|Enum case EvalDynReflectMissingCaseUnit::Ready is not a backed case|Constant EvalDynReflectMissingCaseBacked::Missing does not exist|Constant EvalDynReflectMissingCaseClass::Missing does not exist|Ready:ready" + ); +} + /// Verifies eval-declared final properties cannot be redeclared by subclasses. #[test] fn test_eval_declared_final_property_override_fails() { From e4c848f581b44d69d26741659e2753aafaaddc11 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 02:27:47 +0200 Subject: [PATCH 0696/1208] Throw eval reflection enum constructor errors --- .../src/interpreter/reflection.rs | 25 +++++--- .../tests/builtins_class_metadata.rs | 59 +++++++++++++++++++ tests/codegen/eval.rs | 54 +++++++++++++++++ 3 files changed, 131 insertions(+), 7 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 06d6795ff7..415173f7e7 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2773,15 +2773,26 @@ fn eval_reflection_enum_new( let class_name = eval_reflection_class_target_name(args[0], context, values)?; let reflected_name = context .resolve_enum_name(&class_name) + .or_else(|| context.resolve_class_like_name(&class_name)) .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - if context.enum_decl(&reflected_name).is_none() { - return if eval_reflection_class_like_exists(&reflected_name, context) { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(None) - }; + if context.enum_decl(&reflected_name).is_some() { + return eval_reflection_enum_object_result(&reflected_name, context, values).map(Some); + } + if eval_reflection_class_like_exists(&reflected_name, context) { + return eval_throw_reflection_exception( + &format!("Class \"{}\" is not an enum", reflected_name), + context, + values, + ); + } + if eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return Ok(None); } - eval_reflection_enum_object_result(&reflected_name, context, values).map(Some) + eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ) } /// Materializes one eval-backed `ReflectionEnum` owner object. diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 36952ff0bc..15ec9412d1 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3460,6 +3460,65 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionEnum construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_enum_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectNotEnumClass {} +interface EvalReflectNotEnumIface {} +trait EvalReflectNotEnumTrait {} +enum EvalReflectActualEnum { + case Ready; +} +try { + new ReflectionEnum("EvalReflectNotEnumClass"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectNotEnumIface"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectNotEnumTrait"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectMissingEnum"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnum("EvalReflectActualEnum"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Class \"EvalReflectNotEnumClass\" is not an enum|Class \"EvalReflectNotEnumIface\" is not an enum|Class \"EvalReflectNotEnumTrait\" is not an enum|Class \"EvalReflectMissingEnum\" does not exist|EvalReflectActualEnum" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + /// Verifies ReflectionObject reflects the runtime class of an eval object instance. #[test] fn execute_program_reflection_object_reflects_eval_instances() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 26db63afb4..79086da0df 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14366,6 +14366,60 @@ echo $backedCases[0]->getEnum()->getBackingType()->getName();'); ); } +/// Verifies eval ReflectionEnum construction errors are catchable objects. +#[test] +fn test_eval_reflection_enum_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalDynReflectNotEnumIface"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalDynReflectNotEnumTrait"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalDynReflectMissingEnum"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnum("EvalDynReflectActualEnum"))->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:Class \"EvalDynReflectNotEnumClass\" is not an enum|Class \"EvalDynReflectNotEnumIface\" is not an enum|Class \"EvalDynReflectNotEnumTrait\" is not an enum|Class \"EvalDynReflectMissingEnum\" does not exist|EvalDynReflectActualEnum" + ); +} + /// Verifies eval interface and trait constants work through the bridge. #[test] fn test_eval_declared_interface_and_trait_constants() { From 5eb9ffe15ed4636876e48dda0aaf63c34b99b4c9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 02:35:07 +0200 Subject: [PATCH 0697/1208] Throw eval reflection parameter lookup errors --- .../src/interpreter/reflection.rs | 158 +++++++++++++++++- .../tests/builtins_class_metadata.rs | 64 +++++++ tests/codegen/eval.rs | 59 +++++++ 3 files changed, 272 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 415173f7e7..4fea2da696 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -148,6 +148,7 @@ enum EvalReflectionPropertyHook { } /// Constructor selector accepted by `ReflectionParameter`. +#[derive(Clone)] enum EvalReflectionParameterSelector { Name(String), Position(i64), @@ -3073,13 +3074,129 @@ fn eval_reflection_parameter_new( )?; let selector = eval_reflection_parameter_selector(args[1], values)?; let Some(parameter) = - eval_reflection_parameter_constructor_metadata(args[0], selector, context, values)? + eval_reflection_parameter_constructor_metadata(args[0], selector.clone(), context, values)? else { - return Ok(None); + return eval_reflection_parameter_constructor_error(args[0], &selector, context, values); }; eval_reflection_parameter_object_result(¶meter, context, values).map(Some) } +/// Throws the PHP constructor error for eval-backed `ReflectionParameter` misses. +fn eval_reflection_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_array_like(target)? { + return eval_reflection_method_parameter_constructor_error(target, selector, context, values); + } + if values.type_tag(target)? == EVAL_TAG_STRING { + return eval_reflection_function_parameter_constructor_error( + target, selector, context, values, + ); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Throws the PHP constructor error for eval-backed function parameter misses. +fn eval_reflection_function_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let requested_name = eval_reflection_string_arg(target, values)?; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if context.function(&lookup_name).is_some() || context.native_function(&lookup_name).is_some() { + return eval_reflection_parameter_selector_error(selector, context, values); + } + Ok(None) +} + +/// Throws the PHP constructor error for eval-backed method parameter misses. +fn eval_reflection_method_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(target)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(target, zero)?; + let method = values.array_get(target, one)?; + let method_name = eval_reflection_string_arg(method, values)?; + let class_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_reflection_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if eval_reflection_class_like_exists(&reflected_name, context) { + let Some(reflected_method_name) = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &reflected_name, + &method_name, + context, + ) else { + return eval_throw_reflection_exception( + &format!("Method {}::{}() does not exist", reflected_name, method_name), + context, + values, + ); + }; + if eval_reflection_method_metadata(&reflected_name, &reflected_method_name, context) + .is_some() + { + return eval_reflection_parameter_selector_error(selector, context, values); + } + return Err(EvalStatus::RuntimeFatal); + } + if eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + &method_name, + context, + values, + )? + .is_some() + { + return eval_reflection_parameter_selector_error(selector, context, values); + } + Ok(None) +} + +/// Throws PHP's selector-specific ReflectionParameter constructor error. +fn eval_reflection_parameter_selector_error( + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match selector { + EvalReflectionParameterSelector::Name(_) => eval_throw_reflection_exception( + "The parameter specified by its name could not be found", + context, + values, + ), + EvalReflectionParameterSelector::Position(position) if *position < 0 => { + eval_throw_value_error( + "ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0", + context, + values, + ) + } + EvalReflectionParameterSelector::Position(_) => eval_throw_reflection_exception( + "The parameter specified by its offset could not be found", + context, + values, + ), + } +} + /// Resolves `ReflectionParameter` constructor target metadata. fn eval_reflection_parameter_constructor_metadata( target: RuntimeCellHandle, @@ -3161,19 +3278,28 @@ fn eval_reflection_method_parameter_metadata( EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, _ => return Err(EvalStatus::RuntimeFatal), }; - let member = if eval_reflection_class_like_exists(&class_name, context) { + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let member = if eval_reflection_class_like_exists(&reflected_name, context) { let reflected_method_name = eval_reflection_member_name( EVAL_REFLECTION_OWNER_METHOD, - &class_name, + &reflected_name, &method_name, context, - ) - .ok_or(EvalStatus::RuntimeFatal)?; - eval_reflection_method_metadata(&class_name, &reflected_method_name, context) - .ok_or(EvalStatus::RuntimeFatal)? + ); + let Some(reflected_method_name) = reflected_method_name else { + return Ok(None); + }; + let Some(method) = + eval_reflection_method_metadata(&reflected_name, &reflected_method_name, context) + else { + return Ok(None); + }; + method } else { let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( - &class_name, + &reflected_name, &method_name, context, values, @@ -6582,6 +6708,20 @@ fn eval_throw_reflection_exception( Err(EvalStatus::UncaughtThrowable) } +/// Creates a catchable `ValueError` and propagates it through eval throw state. +fn eval_throw_value_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ValueError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + /// Returns method metadata for a method-like member on an eval class-like symbol. fn eval_reflection_method_metadata( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 15ec9412d1..a76d970494 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -2889,6 +2889,70 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionParameter construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_parameter_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"function eval_reflect_param_error_function($known) {} +class EvalReflectParamErrorTarget { + public function run($known) {} +} +try { + new ReflectionParameter("eval_reflect_param_error_function", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], 3); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "missing"], "known"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], -1); + echo "bad"; +} catch (ValueError $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], "known"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:The parameter specified by its name could not be found|The parameter specified by its name could not be found|The parameter specified by its offset could not be found|Method EvalReflectParamErrorTarget::missing() does not exist|ValueError:ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0|known" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass::getMethods preserves eval method parameter metadata. #[test] fn execute_program_reflection_class_lists_eval_method_parameters() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 79086da0df..ecd853ea66 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12152,6 +12152,65 @@ echo $param->allowsNull() ? "N" : "n"; ); } +/// Verifies eval ReflectionParameter construction errors are catchable objects. +#[test] +fn test_eval_reflection_parameter_constructor_throws_reflection_exceptions() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalDynReflectParamErrorTarget", "run"], "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalDynReflectParamErrorTarget", "run"], 3); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalDynReflectParamErrorTarget", "missing"], "known"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalDynReflectParamErrorTarget", "run"], -1); + echo "bad"; +} catch (ValueError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +echo (new ReflectionParameter(["EvalDynReflectParamErrorTarget", "run"], "known"))->getName(); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "ReflectionException:The parameter specified by its name could not be found|The parameter specified by its name could not be found|The parameter specified by its offset could not be found|Method EvalDynReflectParamErrorTarget::missing() does not exist|ValueError:ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0|known" + ); +} + /// Verifies eval ReflectionParameter exposes PHP constant-default metadata. #[test] fn test_eval_reflection_parameter_reports_default_constant_metadata() { From 13149cb1fb6688dec941b78ecf7e78735bd994e2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 02:41:22 +0200 Subject: [PATCH 0698/1208] Throw eval reflection object type errors --- .../src/interpreter/reflection.rs | 41 ++++++++++++++++++- .../tests/builtins_class_metadata.rs | 37 +++++++++++++---- .../interpreter/tests/support/object_ops.rs | 3 +- tests/codegen/eval.rs | 31 ++++++++++++++ 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 4fea2da696..8162324e02 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2675,8 +2675,16 @@ fn eval_reflection_object_new( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; - if values.type_tag(args[0])? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); + let tag = values.type_tag(args[0])?; + if tag != EVAL_TAG_OBJECT { + return eval_throw_type_error( + &format!( + "ReflectionObject::__construct(): Argument #1 ($object) must be of type object, {} given", + eval_reflection_type_error_type_name(tag) + ), + context, + values, + ); } let reflected_name = eval_reflection_object_class_name(args[0], context, values)?; let Some(object) = eval_reflection_class_owner_object_result( @@ -6722,6 +6730,35 @@ fn eval_throw_value_error( Err(EvalStatus::UncaughtThrowable) } +/// Creates a catchable `TypeError` and propagates it through eval throw state. +fn eval_throw_type_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("TypeError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Returns PHP's type name spelling used in argument type error messages. +fn eval_reflection_type_error_type_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "int", + EVAL_TAG_STRING => "string", + EVAL_TAG_FLOAT => "float", + EVAL_TAG_BOOL => "bool", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_NULL => "null", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_OBJECT => "object", + _ => "unknown", + } +} + /// Returns method metadata for a method-like member on an eval class-like symbol. fn eval_reflection_method_metadata( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index a76d970494..c223894c0b 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3657,18 +3657,41 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies ReflectionObject rejects non-object constructor arguments. +/// Verifies ReflectionObject constructor type errors are catchable. #[test] -fn execute_program_reflection_object_rejects_non_objects() { - let program = parse_fragment(br#"new ReflectionObject("EvalReflectObjectChild");"#) - .expect("parse eval fragment"); +fn execute_program_reflection_object_constructor_throws_type_errors() { + let program = parse_fragment( + br#"try { + new ReflectionObject("EvalReflectObjectChild"); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionObject([]); + echo "bad"; +} catch (TypeError $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("ReflectionObject requires an object"); + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "TypeError:ReflectionObject::__construct(): Argument #1 ($object) must be of type object, string given|ReflectionObject::__construct(): Argument #1 ($object) must be of type object, array given" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); } /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 7c29a3a759..8479bab03c 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -1375,6 +1375,7 @@ fn fake_runtime_exception_like_class(class_name: &str) -> bool { "ReflectionException", "Error", "ValueError", + "TypeError", ] .iter() .any(|known| class_name.eq_ignore_ascii_case(known)) @@ -1420,7 +1421,7 @@ fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: .any(|known| class_name.eq_ignore_ascii_case(known)); } if target_class.eq_ignore_ascii_case("Error") { - return ["Error", "ValueError"] + return ["Error", "ValueError", "TypeError"] .iter() .any(|known| class_name.eq_ignore_ascii_case(known)); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ecd853ea66..91db6b5ec8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -13229,6 +13229,37 @@ echo (new ReflectionClass($object))->hasProperty("dynamic") ? "C" : "c";'); assert_eq!(out.stdout, "declared:d|dynamic:D|:D:value:2:0:HDmc"); } +/// Verifies eval ReflectionObject constructor type errors are catchable objects. +#[test] +fn test_eval_reflection_object_constructor_throws_type_errors() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + new ReflectionObject([]); + echo "bad"; +} catch (TypeError $e) { + echo $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "TypeError:ReflectionObject::__construct(): Argument #1 ($object) must be of type object, string given|ReflectionObject::__construct(): Argument #1 ($object) must be of type object, array given" + ); +} + /// Verifies eval Reflection origin metadata APIs are present on supported owners. #[test] fn test_eval_reflection_origin_metadata_defaults() { From a562cf5b02e85e0da8878adfb66cfdc2e3f591f3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 02:49:40 +0200 Subject: [PATCH 0699/1208] Throw eval reflection constructor access errors --- .../src/interpreter/statements.rs | 26 +++++++++++++ .../tests/builtins_class_metadata.rs | 38 +++++++++++++++++++ tests/codegen/eval.rs | 28 ++++++++++---- 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 25a4090021..1164fc001a 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4054,6 +4054,20 @@ fn eval_throw_value_error( Err(EvalStatus::UncaughtThrowable) } +/// Creates and schedules a `ReflectionException` through eval's normal Throwable channel. +fn eval_throw_reflection_exception( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ReflectionException")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + /// Resolves a static method using private-method scope rules. fn eval_dynamic_static_method_for_call( class_name: &str, @@ -4953,6 +4967,18 @@ fn eval_reflection_class_new_instance_result( return Ok(None); }; if let Some(class) = context.class(&reflected_name).cloned() { + if let Some((_, constructor)) = context.class_method(class.name(), "__construct") { + if constructor.visibility() != EvalVisibility::Public { + return eval_throw_reflection_exception( + &format!( + "Access to non-public constructor of class {}", + class.name() + ), + context, + values, + ); + } + } return eval_reflection_public_constructor_scope(context, values, |context, values| { let mut scope = ElephcEvalScope::new(); eval_dynamic_class_new_object(&class, constructor_args, context, &mut scope, values) diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index c223894c0b..229fcb4e7d 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3151,6 +3151,44 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass::newInstance throws for non-public eval constructors. +#[test] +fn execute_program_reflection_class_new_instance_rejects_non_public_eval_constructors() { + let program = parse_fragment( + br#"class EvalReflectNewPrivateCtor { + private function __construct() {} +} +class EvalReflectNewProtectedCtor { + protected function __construct() {} +} +try { + (new ReflectionClass("EvalReflectNewPrivateCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + (new ReflectionClass("EvalReflectNewProtectedCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ReflectionException:Access to non-public constructor of class EvalReflectNewPrivateCtor|ReflectionException:Access to non-public constructor of class EvalReflectNewProtectedCtor" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod::invoke dispatches eval-declared methods. #[test] fn execute_program_reflection_method_invoke_calls_eval_method() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 91db6b5ec8..1cebae8d19 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -13884,19 +13884,33 @@ echo $fourth->label();'); /// Verifies eval ReflectionClass::newInstance rejects non-public eval constructors like PHP. #[test] -fn test_eval_reflection_class_new_instance_rejects_private_eval_constructor() { - let err = compile_and_run_expect_failure( +fn test_eval_reflection_class_new_instance_rejects_non_public_eval_constructors() { + let out = compile_and_run( r#"newInstance();'); +class EvalReflectNewProtectedCtor { + protected function __construct() {} +} +try { + (new ReflectionClass("EvalReflectNewPrivateCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + (new ReflectionClass("EvalReflectNewProtectedCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "ReflectionException:Access to non-public constructor of class EvalReflectNewPrivateCtor|ReflectionException:Access to non-public constructor of class EvalReflectNewProtectedCtor" ); } From 1fa70a78fd217d706224f7a43055bf91a953067c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 02:56:38 +0200 Subject: [PATCH 0700/1208] Throw eval reflection instantiation errors --- .../src/interpreter/statements.rs | 49 +++++++++++++++++++ .../tests/builtins_class_metadata.rs | 44 +++++++++++++++++ tests/codegen/eval.rs | 38 ++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 1164fc001a..62b204b352 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4068,6 +4068,20 @@ fn eval_throw_reflection_exception( Err(EvalStatus::UncaughtThrowable) } +/// Creates and schedules an `Error` through eval's normal Throwable channel. +fn eval_throw_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("Error")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + /// Resolves a static method using private-method scope rules. fn eval_dynamic_static_method_for_call( class_name: &str, @@ -4966,6 +4980,11 @@ fn eval_reflection_class_new_instance_result( else { return Ok(None); }; + if let Some(message) = + eval_reflection_eval_instantiation_error_message(&reflected_name, context) + { + return eval_throw_error(&message, context, values); + } if let Some(class) = context.class(&reflected_name).cloned() { if let Some((_, constructor)) = context.class_method(class.name(), "__construct") { if constructor.visibility() != EvalVisibility::Public { @@ -5045,6 +5064,11 @@ fn eval_reflection_class_new_instance_without_constructor_result( else { return Ok(None); }; + if let Some(message) = + eval_reflection_eval_instantiation_error_message(&reflected_name, context) + { + return eval_throw_error(&message, context, values); + } if let Some(class) = context.class(&reflected_name).cloned() { let mut scope = ElephcEvalScope::new(); return eval_dynamic_class_allocate_object(&class, context, &mut scope, values).map(Some); @@ -5066,6 +5090,31 @@ fn eval_reflection_class_new_instance_without_constructor_result( values.new_object(&class_name).map(Some) } +/// Builds PHP's reflection instantiation error for eval non-instantiable class-likes. +fn eval_reflection_eval_instantiation_error_message( + reflected_name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(class) = context.class(reflected_name) { + if class.is_abstract() { + return Some(format!("Cannot instantiate abstract class {}", class.name())); + } + if let Some(enum_decl) = context.enum_decl(class.name()) { + return Some(format!("Cannot instantiate enum {}", enum_decl.name())); + } + return None; + } + if let Some(interface) = context.interface(reflected_name) { + return Some(format!("Cannot instantiate interface {}", interface.name())); + } + if let Some(trait_decl) = context.trait_decl(reflected_name) { + return Some(format!("Cannot instantiate trait {}", trait_decl.name())); + } + context + .enum_decl(reflected_name) + .map(|enum_decl| format!("Cannot instantiate enum {}", enum_decl.name())) +} + /// Instantiates an attribute class for `ReflectionAttribute::newInstance()`. fn eval_reflection_attribute_new_instance_result( attribute: &EvalAttribute, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 229fcb4e7d..815f1926d7 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3189,6 +3189,50 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionClass instantiation throws Error for eval non-instantiable class-likes. +#[test] +fn execute_program_reflection_class_new_instance_rejects_eval_non_instantiable_class_likes() { + let program = parse_fragment( + br#"abstract class EvalReflectNewAbstract {} +interface EvalReflectNewIface {} +trait EvalReflectNewTrait {} +enum EvalReflectNewEnum { case Ready; } +function eval_reflect_new_error($class, $without) { + try { + $ref = new ReflectionClass($class); + if ($without) { + $ref->newInstanceWithoutConstructor(); + } else { + $ref->newInstance(); + } + echo "bad"; + } catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); + } +} +eval_reflect_new_error("EvalReflectNewAbstract", false); echo "|"; +eval_reflect_new_error("EvalReflectNewAbstract", true); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", false); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", true); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", false); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", true); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", false); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", true); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate enum EvalReflectNewEnum|Error:Cannot instantiate enum EvalReflectNewEnum" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod::invoke dispatches eval-declared methods. #[test] fn execute_program_reflection_method_invoke_calls_eval_method() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1cebae8d19..56e87c8aa6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -13993,6 +13993,44 @@ $ref->newInstance();'); } } +/// Verifies eval ReflectionClass instantiation rejects eval non-instantiable class-likes like PHP. +#[test] +fn test_eval_reflection_class_new_instance_rejects_eval_non_instantiable_class_likes() { + let out = compile_and_run( + r#"newInstanceWithoutConstructor(); + } else { + $ref->newInstance(); + } + echo "bad"; + } catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); + } +} +eval_reflect_new_error("EvalReflectNewAbstract", false); echo "|"; +eval_reflect_new_error("EvalReflectNewAbstract", true); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", false); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", true); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", false); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", true); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", false); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", true);'); +"#, + ); + assert_eq!( + out, + "Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate enum EvalReflectNewEnum|Error:Cannot instantiate enum EvalReflectNewEnum" + ); +} + /// Verifies eval ReflectionMethod::invoke and invokeArgs call eval-declared methods. #[test] fn test_eval_reflection_method_invoke_calls_eval_method() { From 10131d4c913ce8c79d76293ccd297dcfb81b43f7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 03:03:12 +0200 Subject: [PATCH 0701/1208] Throw AOT reflection instantiation errors --- .../src/interpreter/reflection.rs | 59 ++++++++++---- .../src/interpreter/statements.rs | 27 +++++-- tests/codegen/eval.rs | 81 +++++++++++++------ 3 files changed, 122 insertions(+), 45 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 8162324e02..6baa47f350 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -62,6 +62,12 @@ const EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE: u64 = 512; const EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL: u64 = 1; const EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN: u64 = 2; +/// Exception category and message for failed ReflectionClass instantiation. +pub(in crate::interpreter) enum EvalReflectionInstantiationError { + ThrowableError(String), + ReflectionException(String), +} + /// Eval metadata needed to materialize one `ReflectionClass` owner object. struct EvalReflectionClassMetadata { resolved_name: String, @@ -2917,32 +2923,57 @@ fn eval_reflection_aot_class_flags( Ok(Some((flags, modifiers))) } -/// Returns whether a generated/AOT reflected class can be allocated without its constructor. -pub(in crate::interpreter) fn eval_reflection_aot_class_allows_without_constructor_allocation( +/// Returns the catchable error for generated/AOT allocation without constructor, if any. +pub(in crate::interpreter) fn eval_reflection_aot_class_without_constructor_error( class_name: &str, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { return Ok(None); }; - let rejected_flags = EVAL_REFLECTION_CLASS_FLAG_ABSTRACT - | EVAL_REFLECTION_CLASS_FLAG_INTERFACE - | EVAL_REFLECTION_CLASS_FLAG_TRAIT - | EVAL_REFLECTION_CLASS_FLAG_ENUM; - Ok(Some(flags & rejected_flags == 0)) + Ok(eval_reflection_non_instantiable_error_message( + class_name, flags, + )) } -/// Returns whether a generated/AOT reflected class can be constructed through a public constructor. -pub(in crate::interpreter) fn eval_reflection_aot_class_allows_public_instantiation( +/// Returns the catchable error for generated/AOT public ReflectionClass construction, if any. +pub(in crate::interpreter) fn eval_reflection_aot_class_public_instantiation_error( class_name: &str, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { return Ok(None); }; - Ok(Some( - flags & EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE == EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE, - )) + if let Some(message) = eval_reflection_non_instantiable_error_message(class_name, flags) { + return Ok(Some(EvalReflectionInstantiationError::ThrowableError(message))); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE == 0 { + return Ok(Some( + EvalReflectionInstantiationError::ReflectionException(format!( + "Access to non-public constructor of class {}", + class_name.trim_start_matches('\\') + )), + )); + } + Ok(None) +} + +/// Builds PHP's non-instantiable class-like message from ReflectionClass flags. +fn eval_reflection_non_instantiable_error_message(class_name: &str, flags: u64) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0 { + return Some(format!("Cannot instantiate abstract class {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + return Some(format!("Cannot instantiate interface {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + return Some(format!("Cannot instantiate trait {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_ENUM != 0 { + return Some(format!("Cannot instantiate enum {}", class_name)); + } + None } /// Returns whether an absent or public AOT lifecycle method allows public reflection. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 62b204b352..0bb53e9c63 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4082,6 +4082,22 @@ fn eval_throw_error( Err(EvalStatus::UncaughtThrowable) } +/// Schedules the Throwable category required by one ReflectionClass instantiation error. +fn eval_throw_reflection_instantiation_error( + error: EvalReflectionInstantiationError, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match error { + EvalReflectionInstantiationError::ThrowableError(message) => { + eval_throw_error(&message, context, values) + } + EvalReflectionInstantiationError::ReflectionException(message) => { + eval_throw_reflection_exception(&message, context, values) + } + } +} + /// Resolves a static method using private-method scope rules. fn eval_dynamic_static_method_for_call( class_name: &str, @@ -5007,8 +5023,9 @@ fn eval_reflection_class_new_instance_result( let class_name = context .resolve_class_name(&reflected_name) .unwrap_or(reflected_name); - if eval_reflection_aot_class_allows_public_instantiation(&class_name, values)? == Some(false) { - return Err(EvalStatus::RuntimeFatal); + if let Some(error) = eval_reflection_aot_class_public_instantiation_error(&class_name, values)? + { + return eval_throw_reflection_instantiation_error(error, context, values); } eval_reflection_public_constructor_scope(context, values, |context, values| { let instance = values.new_object(&class_name)?; @@ -5082,10 +5099,10 @@ fn eval_reflection_class_new_instance_without_constructor_result( let class_name = context .resolve_class_name(&reflected_name) .unwrap_or(reflected_name); - if eval_reflection_aot_class_allows_without_constructor_allocation(&class_name, values)? - == Some(false) + if let Some(message) = + eval_reflection_aot_class_without_constructor_error(&class_name, values)? { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_error(&message, context, values); } values.new_object(&class_name).map(Some) } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 56e87c8aa6..eee0706054 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -13917,7 +13917,7 @@ try { /// Verifies eval ReflectionClass::newInstance rejects non-public AOT constructors like PHP. #[test] fn test_eval_reflection_class_new_instance_rejects_protected_aot_constructor_from_child_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"newInstance();'); + eval('try { + $ref = new ReflectionClass("EvalReflectNewProtectedAotCtorBase"); + $ref->newInstance(); + echo "bad"; + } catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); + }'); } } EvalReflectNewProtectedAotCtorChild::run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "ReflectionException:Access to non-public constructor of class EvalReflectNewProtectedAotCtorBase" ); } @@ -13969,27 +13974,39 @@ fn test_eval_reflection_class_new_instance_rejects_aot_non_instantiable_class_li ( "abstract class EvalReflectNewAotAbstract {}", "EvalReflectNewAotAbstract", + "Error:Cannot instantiate abstract class EvalReflectNewAotAbstract", + ), + ( + "interface EvalReflectNewAotIface {}", + "EvalReflectNewAotIface", + "Error:Cannot instantiate interface EvalReflectNewAotIface", + ), + ( + "trait EvalReflectNewAotTrait {}", + "EvalReflectNewAotTrait", + "Error:Cannot instantiate trait EvalReflectNewAotTrait", ), - ("interface EvalReflectNewAotIface {}", "EvalReflectNewAotIface"), - ("trait EvalReflectNewAotTrait {}", "EvalReflectNewAotTrait"), ( "enum EvalReflectNewAotEnum { case Ready; }", "EvalReflectNewAotEnum", + "Error:Cannot instantiate enum EvalReflectNewAotEnum", ), ]; - for (declaration, class_name) in cases { + for (declaration, class_name, expected) in cases { let source = format!( r#"newInstance();'); +eval('try {{ + $ref = new ReflectionClass("{class_name}"); + $ref->newInstance(); + echo "bad"; +}} catch (Error $e) {{ + echo get_class($e) . ":" . $e->getMessage(); +}}'); "# ); - let err = compile_and_run_expect_failure(&source); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr for {class_name}: {err}" - ); + let out = compile_and_run(&source); + assert_eq!(out, expected, "unexpected stdout for {class_name}"); } } @@ -14185,27 +14202,39 @@ fn test_eval_reflection_class_new_instance_without_constructor_rejects_aot_non_c ( "abstract class EvalReflectNoCtorAotAbstract {}", "EvalReflectNoCtorAotAbstract", + "Error:Cannot instantiate abstract class EvalReflectNoCtorAotAbstract", + ), + ( + "interface EvalReflectNoCtorAotIface {}", + "EvalReflectNoCtorAotIface", + "Error:Cannot instantiate interface EvalReflectNoCtorAotIface", + ), + ( + "trait EvalReflectNoCtorAotTrait {}", + "EvalReflectNoCtorAotTrait", + "Error:Cannot instantiate trait EvalReflectNoCtorAotTrait", ), - ("interface EvalReflectNoCtorAotIface {}", "EvalReflectNoCtorAotIface"), - ("trait EvalReflectNoCtorAotTrait {}", "EvalReflectNoCtorAotTrait"), ( "enum EvalReflectNoCtorAotEnum { case Ready; }", "EvalReflectNoCtorAotEnum", + "Error:Cannot instantiate enum EvalReflectNoCtorAotEnum", ), ]; - for (declaration, class_name) in cases { + for (declaration, class_name, expected) in cases { let source = format!( r#"newInstanceWithoutConstructor();'); +eval('try {{ + $ref = new ReflectionClass("{class_name}"); + $ref->newInstanceWithoutConstructor(); + echo "bad"; +}} catch (Error $e) {{ + echo get_class($e) . ":" . $e->getMessage(); +}}'); "# ); - let err = compile_and_run_expect_failure(&source); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr for {class_name}: {err}" - ); + let out = compile_and_run(&source); + assert_eq!(out, expected, "unexpected stdout for {class_name}"); } } From 16390816ead3ea3ad7d035eff91d5293e149998e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 03:09:33 +0200 Subject: [PATCH 0702/1208] Throw eval dynamic constructor access errors --- .../src/interpreter/reflection.rs | 22 +++++++ .../src/interpreter/statements.rs | 59 +++++++++++++++++++ tests/codegen/eval.rs | 30 ++++++---- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 6baa47f350..9dbfe14bb1 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2989,6 +2989,28 @@ fn eval_reflection_aot_lifecycle_method_allows_public_reflection( && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0) } +/// Returns AOT constructor access metadata when the constructor is not public. +pub(in crate::interpreter) fn eval_reflection_aot_non_public_constructor( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, "__construct")? else { + return Ok(None); + }; + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + return Ok(None); + }; + let declaring_class = values + .reflection_method_declaring_class(runtime_class_name, "__construct")? + .unwrap_or_else(|| runtime_class_name.to_string()); + Ok(Some((declaring_class, visibility))) +} + /// Builds an eval-backed `ReflectionFunction` object for eval or registered native functions. fn eval_reflection_function_new( evaluated_args: Vec, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 0bb53e9c63..6293a5a9c2 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -6063,6 +6063,9 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { + if let Some(message) = eval_native_constructor_access_error(class_name, context, values)? { + return eval_throw_error(&message, context, values).map(|_| ()); + } let signature = context.native_constructor_signature(class_name); let bound_args = bind_native_callable_bound_args( signature, @@ -6078,6 +6081,62 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( } } +/// Returns PHP's constructor access error for generated/AOT constructors, if inaccessible. +fn eval_native_constructor_access_error( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility)) = + eval_reflection_aot_non_public_constructor(class_name, values)? + else { + return Ok(None); + }; + if eval_native_constructor_access_allowed(&declaring_class, visibility, context) { + return Ok(None); + } + Ok(Some(format!( + "Call to {} {}::__construct() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + eval_native_constructor_scope_label(context) + ))) +} + +/// Returns whether the current eval scope may call one generated/AOT constructor. +fn eval_native_constructor_access_allowed( + declaring_class: &str, + visibility: EvalVisibility, + context: &ElephcEvalContext, +) -> bool { + match visibility { + EvalVisibility::Public => true, + EvalVisibility::Private => context + .current_class_scope() + .is_some_and(|current| same_eval_class_name(current, declaring_class)), + EvalVisibility::Protected => context + .current_class_scope() + .is_some_and(|current| eval_classes_are_related(current, declaring_class, context)), + } +} + +/// Returns PHP's scope phrase for constructor access diagnostics. +fn eval_native_constructor_scope_label(context: &ElephcEvalContext) -> String { + context.current_class_scope().map_or_else( + || String::from("global scope"), + |class_name| format!("scope {}", class_name.trim_start_matches('\\')), + ) +} + +/// Returns PHP's lowercase visibility label. +fn eval_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} + /// Allocates a fresh runtime cell for one invocation-safe native AOT default. fn materialize_native_callable_default( default: &NativeCallableDefault, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eee0706054..4a9bd6a2f6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -15098,7 +15098,7 @@ EvalDynamicNewPrivateCtor::run(); /// Verifies eval object construction rejects private AOT constructors outside the declaring scope. #[test] fn test_eval_dynamic_new_rejects_private_constructor_from_child_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"getMessage(); + }'); } } EvalDynamicNewPrivateCtorChild::run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Call to private EvalDynamicNewPrivateCtorBase::__construct() from scope EvalDynamicNewPrivateCtorChild" ); } @@ -15145,7 +15150,7 @@ EvalDynamicNewProtectedCtorChild::run(); /// Verifies eval object construction rejects protected AOT constructors between sibling scopes. #[test] fn test_eval_dynamic_new_rejects_protected_constructor_from_sibling_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"getMessage(); + }'); } } EvalDynamicNewProtectedCtorRight::run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Call to protected EvalDynamicNewProtectedCtorLeft::__construct() from scope EvalDynamicNewProtectedCtorRight" ); } From ded2fc936a9365b361930dedd9c7392ae3c8cff7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 03:18:04 +0200 Subject: [PATCH 0703/1208] Throw eval member access errors --- .../src/interpreter/statements.rs | 102 ++++++++++++++++-- .../src/interpreter/tests/class_constants.rs | 21 +++- .../src/interpreter/tests/expressions.rs | 21 ++-- .../src/interpreter/tests/static_members.rs | 21 +++- tests/codegen/eval.rs | 57 ++++++---- 5 files changed, 176 insertions(+), 46 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 6293a5a9c2..b2ff7ea4ba 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2914,7 +2914,13 @@ pub(in crate::interpreter) fn eval_property_get_result( { return Ok(result); } - return Err(EvalStatus::RuntimeFatal); + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); } storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); if property.has_get_hook() @@ -2999,9 +3005,23 @@ pub(in crate::interpreter) fn eval_property_set_result( )? { return Ok(()); } - return Err(EvalStatus::RuntimeFatal); + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); + } + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.write_visibility(), + context, + values, + ); } - validate_eval_property_write_access(&declaring_class, &property, context)?; validate_eval_readonly_property_write(&declaring_class, &property, context)?; storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); if property.has_set_hook() { @@ -3530,6 +3550,46 @@ fn validate_eval_property_write_access( validate_eval_member_access(declaring_class, property.write_visibility(), context) } +/// Throws PHP's inaccessible property error for eval-declared properties. +fn eval_throw_property_access_error( + declaring_class: &str, + property_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot access {} property {}::${}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's inaccessible constant error for eval-declared class constants. +fn eval_throw_constant_access_error( + declaring_class: &str, + constant_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot access {} constant {}::{}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + constant_name + ), + context, + values, + ) +} + /// Reads one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_get_result( class_name: &str, @@ -3542,7 +3602,15 @@ pub(in crate::interpreter) fn eval_static_property_get_result( if !property.is_static() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, property.visibility(), context)?; + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); + } return context .static_property(&declaring_class, property.name()) .ok_or(EvalStatus::RuntimeFatal); @@ -3576,7 +3644,15 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( return Ok(case); } if let Some((declaring_class, constant)) = context.class_constant(&class_name, constant_name) { - validate_eval_member_access(&declaring_class, constant.visibility(), context)?; + if validate_eval_member_access(&declaring_class, constant.visibility(), context).is_err() { + return eval_throw_constant_access_error( + &declaring_class, + constant.name(), + constant.visibility(), + context, + values, + ); + } return context .class_constant_cell(&declaring_class, constant.name()) .ok_or(EvalStatus::RuntimeFatal); @@ -3712,7 +3788,15 @@ pub(in crate::interpreter) fn eval_static_property_set_result( if !property.is_static() { return Err(EvalStatus::RuntimeFatal); } - validate_eval_property_write_access(&declaring_class, &property, context)?; + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.write_visibility(), + context, + values, + ); + } validate_eval_readonly_property_write(&declaring_class, &property, context)?; if let Some(replaced) = context.set_static_property(&declaring_class, property.name(), value) { values.release(replaced)?; @@ -4069,11 +4153,11 @@ fn eval_throw_reflection_exception( } /// Creates and schedules an `Error` through eval's normal Throwable channel. -fn eval_throw_error( +fn eval_throw_error( message: &str, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result { let exception = values.new_object("Error")?; let message = values.string(message)?; let code = values.int(0)?; @@ -6064,7 +6148,7 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if let Some(message) = eval_native_constructor_access_error(class_name, context, values)? { - return eval_throw_error(&message, context, values).map(|_| ()); + return eval_throw_error(&message, context, values); } let signature = context.native_constructor_signature(class_name); let bound_args = bind_native_callable_bound_args( diff --git a/crates/elephc-magician/src/interpreter/tests/class_constants.rs b/crates/elephc-magician/src/interpreter/tests/class_constants.rs index 0277042d39..34bfc98b48 100644 --- a/crates/elephc-magician/src/interpreter/tests/class_constants.rs +++ b/crates/elephc-magician/src/interpreter/tests/class_constants.rs @@ -46,21 +46,32 @@ return EvalConstChild::hidden();"#, assert_eq!(values.get(result), FakeValue::Int(5)); } -/// Verifies protected class constants are not readable from global eval scope. +/// Verifies protected class constant access from global eval scope throws Error. #[test] -fn execute_program_rejects_protected_eval_class_constant_from_global_scope() { +fn execute_program_protected_eval_class_constant_from_global_scope_throws_error() { let program = parse_fragment( br#"class EvalConstProtected { protected const SECRET = 4; } -return EvalConstProtected::SECRET;"#, +try { + echo EvalConstProtected::SECRET; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - execute_program(&program, &mut scope, &mut values) - .expect_err("global protected class constant access should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot access protected constant EvalConstProtected::SECRET" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies duplicate class constants in one eval class are rejected. diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index f21935cafd..538e0d5e7f 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -831,25 +831,34 @@ echo $box->value;"#, assert_eq!(values.output, "1:3:3"); } -/// Verifies eval rejects private member access from global scope. +/// Verifies eval throws Error for private property access from global scope. #[test] -fn execute_program_rejects_private_eval_member_access_from_global_scope() { +fn execute_program_private_eval_member_access_from_global_scope_throws_error() { let program = parse_fragment( br#"class EvalPrivateGlobalBox { private int $secret = 4; private function read() { return $this->secret; } } $box = new EvalPrivateGlobalBox(); -echo $box->secret;"#, +try { + echo $box->secret; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("global private property access should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Cannot access private property EvalPrivateGlobalBox::$secret" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval rejects calls to private methods from global scope. #[test] diff --git a/crates/elephc-magician/src/interpreter/tests/static_members.rs b/crates/elephc-magician/src/interpreter/tests/static_members.rs index 4d6d450301..cc5b7c5c79 100644 --- a/crates/elephc-magician/src/interpreter/tests/static_members.rs +++ b/crates/elephc-magician/src/interpreter/tests/static_members.rs @@ -66,21 +66,32 @@ return EvalStaticBase::baseRead();"#, assert_eq!(values.get(result), FakeValue::Int(5)); } -/// Verifies private static properties are not readable from global eval scope. +/// Verifies private static property access from global eval scope throws Error. #[test] -fn execute_program_rejects_private_eval_static_property_from_global_scope() { +fn execute_program_private_eval_static_property_from_global_scope_throws_error() { let program = parse_fragment( br#"class EvalStaticPrivate { private static int $secret = 4; } -return EvalStaticPrivate::$secret;"#, +try { + echo EvalStaticPrivate::$secret; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - execute_program(&program, &mut scope, &mut values) - .expect_err("global private static property access should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot access private property EvalStaticPrivate::$secret" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval rejects static-style calls to non-static methods. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4a9bd6a2f6..ab432d5ff6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14663,55 +14663,70 @@ class EvalTraitConstBadBox { ); } -/// Verifies eval rejects private member access from outside the declaring class. +/// Verifies eval throws Error for private member access from outside the declaring class. #[test] -fn test_eval_declared_private_member_access_fails() { - let err = compile_and_run_expect_failure( +fn test_eval_declared_private_member_access_throws_error() { + let out = compile_and_run( r#"secret;'); +try { + echo $box->secret; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" + assert_eq!( + out, + "Error:Cannot access private property EvalPrivateAccessBox::$secret" ); } -/// Verifies eval rejects protected class constant access from outside the declaring class. +/// Verifies eval throws Error for protected class constant access from outside the declaring class. #[test] -fn test_eval_declared_protected_class_constant_access_fails() { - let err = compile_and_run_expect_failure( +fn test_eval_declared_protected_class_constant_access_throws_error() { + let out = compile_and_run( r#"getMessage(); +}'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" + assert_eq!( + out, + "Error:Cannot access protected constant EvalProtectedConstAccessBox::SECRET" ); } -/// Verifies eval rejects private static member access from outside the declaring class. +/// Verifies eval throws Error for private static member access from outside the declaring class. #[test] -fn test_eval_declared_private_static_member_access_fails() { - let err = compile_and_run_expect_failure( +fn test_eval_declared_private_static_member_access_throws_error() { + let out = compile_and_run( r#"getMessage(); +}'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" + assert_eq!( + out, + "Error:Cannot access private property EvalPrivateStaticAccessBox::$secret" ); } From 5af1ab1800701e92b0f3228ccef8336d88c08ffb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 03:24:40 +0200 Subject: [PATCH 0704/1208] Throw eval method access errors --- .../src/interpreter/statements.rs | 53 ++++++++++++++++++- .../src/interpreter/tests/expressions.rs | 21 +++++--- tests/codegen/eval.rs | 31 +++++++++++ 3 files changed, 97 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b2ff7ea4ba..b2dd883dd9 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3590,6 +3590,27 @@ fn eval_throw_constant_access_error( ) } +/// Throws PHP's inaccessible method error for eval-declared methods. +fn eval_throw_method_access_error( + declaring_class: &str, + method_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} method {}::{}() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + method_name, + eval_native_constructor_scope_label(context) + ), + context, + values, + ) +} + /// Reads one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_get_result( class_name: &str, @@ -3865,7 +3886,13 @@ pub(in crate::interpreter) fn eval_static_method_call_result( )? { return Ok(result); } - return Err(EvalStatus::RuntimeFatal); + return eval_throw_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); } return eval_dynamic_static_method_with_values( &declaring_class, @@ -4288,7 +4315,18 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( if let Some((constructor_class, constructor)) = context.class_method(class.name(), "__construct") { - validate_eval_member_access(&constructor_class, constructor.visibility(), context)?; + if validate_eval_member_access(&constructor_class, constructor.visibility(), context) + .is_err() + { + let _ = values.release(object); + return eval_throw_method_access_error( + &constructor_class, + constructor.name(), + constructor.visibility(), + context, + values, + ); + } let result = eval_dynamic_method_with_values( &constructor_class, class.name(), @@ -4881,6 +4919,7 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( values, ); } + let mut inaccessible_method = None; if let Some((class_name, method)) = eval_dynamic_method_for_call(&called_class_name, method_name, context) { @@ -4908,6 +4947,7 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( values, ); } + inaccessible_method = Some((class_name, method)); } if let Some(result) = eval_magic_instance_method_call( object, @@ -4919,6 +4959,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( )? { return Ok(result); } + if let Some((declaring_class, method)) = inaccessible_method { + return eval_throw_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } Err(EvalStatus::RuntimeFatal) } diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index 538e0d5e7f..59e3ffa38e 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -860,24 +860,33 @@ return true;"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval rejects calls to private methods from global scope. +/// Verifies eval throws Error for calls to private methods from global scope. #[test] -fn execute_program_rejects_private_eval_method_call_from_global_scope() { +fn execute_program_private_eval_method_call_from_global_scope_throws_error() { let program = parse_fragment( br#"class EvalPrivateMethodBox { private function read() { return 4; } } $box = new EvalPrivateMethodBox(); -return $box->read();"#, +try { + echo $box->read(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("global private method call should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Call to private method EvalPrivateMethodBox::read() from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval rejects overriding a public method with lower visibility. #[test] diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ab432d5ff6..65ced18782 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14686,6 +14686,37 @@ try { ); } +/// Verifies eval throws Error for inaccessible eval-declared method calls. +#[test] +fn test_eval_declared_inaccessible_method_access_throws_error() { + let out = compile_and_run( + r#"hidden(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + echo EvalPrivateMethodAccessBox::seed(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Call to private method EvalPrivateMethodAccessBox::hidden() from global scope|Error:Call to protected method EvalPrivateMethodAccessBox::seed() from global scope" + ); +} + /// Verifies eval throws Error for protected class constant access from outside the declaring class. #[test] fn test_eval_declared_protected_class_constant_access_throws_error() { From d088ef85f8a313404fad17f3c48c8a70df76de66 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 03:32:32 +0200 Subject: [PATCH 0705/1208] Throw AOT method access errors --- .../src/interpreter/statements.rs | 66 +++++++++++++++++++ tests/codegen/eval.rs | 56 ++++++++++------ 2 files changed, 102 insertions(+), 20 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b2dd883dd9..b6eb156b99 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -6149,6 +6149,17 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if let Some((declaring_class, visibility)) = + eval_native_method_access_error(class_name, method_name, context, values)? + { + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } let signature = context.native_method_signature(class_name, method_name); let bound_args = bind_native_callable_bound_args( signature, @@ -6172,6 +6183,17 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if let Some((declaring_class, visibility)) = + eval_native_method_access_error(class_name, method_name, context, values)? + { + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } let signature = context.native_static_method_signature(class_name, method_name); let bound_args = bind_native_callable_bound_args( signature, @@ -6188,6 +6210,50 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( } } +/// Returns generated/AOT method access metadata when current eval scope cannot call it. +fn eval_native_method_access_error( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + return Ok(None); + }; + if is_abstract { + return Ok(None); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() { + return Ok(None); + } + Ok(Some((declaring_class, visibility))) +} + +/// Finds generated/AOT method metadata on a class or its native parent chain. +fn eval_aot_method_dispatch_metadata_in_hierarchy( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return Ok(None); + } + if let Some(metadata) = eval_aot_method_dispatch_metadata(¤t, method_name, values)? { + return Ok(Some(metadata)); + } + let Some(parent) = context.native_class_parent(¤t) else { + return Ok(None); + }; + current = parent.to_string(); + } +} + /// Runs one generated/AOT constructor after native signature binding. pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( class_name: &str, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 65ced18782..ee9796a169 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4949,7 +4949,7 @@ class EvalPrivateAotCallableMethodBox { /// Verifies eval fragments reject private AOT instance methods from child scopes. #[test] fn test_eval_fragment_rejects_private_aot_method_from_child_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"secret(3);'); + echo eval('try { + return $this->secret(3); + } catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); + }'); } } (new EvalPrivateAotMethodChild())->run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Call to private method EvalPrivateAotMethodBase::secret() from scope EvalPrivateAotMethodChild" ); } @@ -4998,7 +5002,7 @@ class EvalProtectedAotMethodChild extends EvalProtectedAotMethodBase { /// Verifies eval fragments reject protected AOT instance methods between sibling scopes. #[test] fn test_eval_fragment_rejects_protected_aot_method_from_sibling_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"add(3);'); + echo eval('try { + return (new EvalProtectedAotMethodLeft())->add(3); + } catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); + }'); } } (new EvalProtectedAotMethodRight())->run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Call to protected method EvalProtectedAotMethodLeft::add() from scope EvalProtectedAotMethodRight" ); } @@ -5712,7 +5720,7 @@ class EvalPrivateAotStaticMethodBox { /// Verifies eval fragments reject private AOT static methods from child scopes. #[test] fn test_eval_fragment_rejects_private_aot_static_method_from_child_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"getMessage(); + }'); } } (new EvalPrivateAotStaticMethodChild())->run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Call to private method EvalPrivateAotStaticMethodBase::secret() from scope EvalPrivateAotStaticMethodChild" ); } @@ -5784,7 +5796,7 @@ class EvalProtectedAotStaticCallableChild extends EvalProtectedAotStaticCallable /// Verifies eval fragments reject protected AOT static methods between sibling scopes. #[test] fn test_eval_fragment_rejects_protected_aot_static_method_from_sibling_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"getMessage(); + }'); } } (new EvalProtectedAotStaticMethodRight())->run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Call to protected method EvalProtectedAotStaticMethodLeft::add() from scope EvalProtectedAotStaticMethodRight" ); } From 236f09c3e6b04226769e1b28a3024783f3a5a4f1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 03:40:06 +0200 Subject: [PATCH 0706/1208] Throw AOT property access errors --- .../src/interpreter/reflection.rs | 71 +++++++++++++++++++ .../src/interpreter/statements.rs | 68 ++++++++++++++++++ tests/codegen/eval.rs | 56 +++++++++------ 3 files changed, 175 insertions(+), 20 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 9dbfe14bb1..2ce778b5b8 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -4224,6 +4224,77 @@ fn eval_reflection_aot_property_metadata_if_exists( ))) } +/// Returns generated/AOT property access metadata for an exact class/property pair. +pub(in crate::interpreter) fn eval_reflection_aot_property_access_metadata( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_property_declaring_class(runtime_class_name, property_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + Ok(Some(eval_reflection_aot_property_access_metadata_from_flags( + declaring_class, + flags, + ))) +} + +/// Returns generated/AOT static property metadata from a class or native parent chain. +pub(in crate::interpreter) fn eval_reflection_aot_static_property_access_metadata( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return Ok(None); + } + if let Some(metadata) = + eval_reflection_aot_property_access_metadata(¤t, property_name, values)? + { + return Ok(Some(metadata)); + } + let Some(parent) = context.native_class_parent(¤t) else { + return Ok(None); + }; + current = parent.to_string(); + } +} + +/// Converts AOT ReflectionProperty flags into access-check metadata. +fn eval_reflection_aot_property_access_metadata_from_flags( + declaring_class: String, + flags: u64, +) -> (String, EvalVisibility, EvalVisibility, bool) { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + let write_visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + EvalVisibility::Protected + } else { + visibility + }; + ( + declaring_class, + visibility, + write_visibility, + flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, + ) +} + /// Returns registered generated/AOT property type metadata for one reflected property. fn eval_reflection_aot_property_type_metadata( runtime_class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b6eb156b99..c60af2778b 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2899,6 +2899,20 @@ pub(in crate::interpreter) fn eval_property_get_result( return values.property_get(object, property_name); }; let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_runtime_object_class_name(object, values)?; + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_property_access_metadata(&class_name, property_name, values)? + { + if !is_static && validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + } return values.property_get(object, property_name); }; let object_class_name = class.name().to_string(); @@ -2981,6 +2995,22 @@ pub(in crate::interpreter) fn eval_property_set_result( return values.property_set(object, property_name, value); }; let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_runtime_object_class_name(object, values)?; + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_property_access_metadata(&class_name, property_name, values)? + { + if !is_static + && validate_eval_member_access(&declaring_class, write_visibility, context).is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + } return values.property_set(object, property_name, value); }; let object_class_name = class.name().to_string(); @@ -3639,6 +3669,24 @@ pub(in crate::interpreter) fn eval_static_property_get_result( if eval_static_member_context_owns_class(&class_name, context) { return Err(EvalStatus::RuntimeFatal); } + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if is_static && validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + } values .static_property_get(&class_name, property_name)? .ok_or(EvalStatus::RuntimeFatal) @@ -3827,6 +3875,26 @@ pub(in crate::interpreter) fn eval_static_property_set_result( if eval_static_member_context_owns_class(&class_name, context) { return Err(EvalStatus::RuntimeFatal); } + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if is_static + && validate_eval_member_access(&declaring_class, write_visibility, context).is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + } if values.static_property_set(&class_name, property_name, value)? { Ok(()) } else { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ee9796a169..52664a1e5f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4453,19 +4453,23 @@ $box->run(); /// Verifies eval fragments outside the declaring scope cannot read private AOT properties. #[test] fn test_eval_fragment_rejects_private_property_outside_declaring_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"x;'); +echo eval('try { + return $box->x; +} catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); +}'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Cannot access private property EvalPrivatePropOutsideBox::$x" ); } @@ -4516,7 +4520,7 @@ $box->run(); /// Verifies eval fragments reject protected AOT properties between sibling class scopes. #[test] fn test_eval_fragment_rejects_protected_aot_property_from_sibling_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"x;'); + echo eval('try { + return (new EvalProtectedPropLeft())->x; + } catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); + }'); } } @@ -4534,9 +4542,9 @@ $right = new EvalProtectedPropRight(); $right->run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Cannot access protected property EvalProtectedPropLeft::$x" ); } @@ -4720,7 +4728,7 @@ $box->run(); /// Verifies eval fragments reject private AOT static properties outside the declaring scope. #[test] fn test_eval_fragment_rejects_private_aot_static_property_outside_declaring_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"getMessage(); + }'); } } @@ -4736,9 +4748,9 @@ $box = new EvalPrivateStaticPropChild(); $box->run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Cannot access private property EvalPrivateStaticPropBase::$x" ); } @@ -4792,7 +4804,7 @@ $box->run(); /// Verifies eval fragments reject protected AOT static properties between sibling class scopes. #[test] fn test_eval_fragment_rejects_protected_aot_static_property_from_sibling_scope() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"getMessage(); + }'); } } @@ -4810,9 +4826,9 @@ $right = new EvalProtectedStaticPropRight(); $right->run(); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Cannot access protected property EvalProtectedStaticPropLeft::$x" ); } From 11d2f13ac458cb411e538bccab2569d0de2a1fa4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 03:44:21 +0200 Subject: [PATCH 0707/1208] Throw AOT class constant fetch errors --- .../elephc-magician/src/interpreter/statements.rs | 15 ++++++++++++--- tests/codegen/eval.rs | 14 +++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index c60af2778b..fb94ae1346 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3729,9 +3729,18 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( if eval_static_member_context_owns_class(&class_name, context) { return Err(EvalStatus::RuntimeFatal); } - values - .class_constant_get(&class_name, constant_name)? - .ok_or(EvalStatus::RuntimeFatal) + if let Some(value) = values.class_constant_get(&class_name, constant_name)? { + return Ok(value); + } + eval_throw_error( + &format!( + "Undefined constant {}::{}", + class_name.trim_start_matches('\\'), + constant_name + ), + context, + values, + ) } /// Resolves eval-visible built-in Reflection class constants. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 52664a1e5f..fa21867fb2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5559,7 +5559,7 @@ return "";'); /// Verifies eval rejects direct fetches of private AOT constants through child names. #[test] fn test_eval_rejects_private_inherited_aot_class_constant_fetch() { - let err = compile_and_run_expect_failure( + let out = compile_and_run( r#"getMessage(); +}'); "#, ); - assert!( - err.contains("Fatal error: eval() runtime failed"), - "unexpected stderr: {err}" + assert_eq!( + out, + "Error:Undefined constant EvalAotPrivateConstFetchChild::HIDDEN" ); } From fa23502a2cd6b9e0e6565c34fb43f2bab2cabe28 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 03:55:57 +0200 Subject: [PATCH 0708/1208] Throw eval readonly property errors --- .../src/interpreter/statements.rs | 88 ++++++++++++++++++- .../src/interpreter/tests/classes.rs | 84 +++++++++++++----- tests/codegen/eval.rs | 79 ++++++++++++++--- 3 files changed, 211 insertions(+), 40 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index fb94ae1346..066dedd997 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3052,7 +3052,14 @@ pub(in crate::interpreter) fn eval_property_set_result( values, ); } - validate_eval_readonly_property_write(&declaring_class, &property, context)?; + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); if property.has_set_hook() { if !current_eval_property_hook_is( @@ -3100,7 +3107,12 @@ pub(in crate::interpreter) fn eval_property_set_result( return Ok(()); } if !declared_property_found && class_is_readonly { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_dynamic_property_creation_error( + &object_class_name, + property_name, + context, + values, + ); } if let Some(target) = context .dynamic_property_alias(identity, &storage_property_name) @@ -3290,7 +3302,14 @@ pub(in crate::interpreter) fn eval_property_unset_result( { if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { validate_eval_property_write_access(&declaring_class, &property, context)?; - validate_eval_readonly_property_write(&declaring_class, &property, context)?; + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_unset_error( + &declaring_class, + property.name(), + context, + values, + ); + } let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); context.remove_dynamic_property_alias(identity, &storage_property_name); @@ -3600,6 +3619,60 @@ fn eval_throw_property_access_error( ) } +/// Throws PHP's readonly property assignment error for eval-declared properties. +fn eval_throw_readonly_property_modification_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot modify readonly property {}::${}", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's readonly property unset error for eval-declared properties. +fn eval_throw_readonly_property_unset_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot unset readonly property {}::${}", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's dynamic property creation error for readonly eval-declared classes. +fn eval_throw_dynamic_property_creation_error( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot create dynamic property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + /// Throws PHP's inaccessible constant error for eval-declared class constants. fn eval_throw_constant_access_error( declaring_class: &str, @@ -3875,7 +3948,14 @@ pub(in crate::interpreter) fn eval_static_property_set_result( values, ); } - validate_eval_readonly_property_write(&declaring_class, &property, context)?; + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } if let Some(replaced) = context.set_static_property(&declaring_class, property.name(), value) { values.release(replaced)?; } diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index fcd24d9945..9bc706dfce 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -246,9 +246,9 @@ new EvalPromotedReadonlyRefBox($value);"#, assert_eq!(err, EvalStatus::RuntimeFatal); } -/// Verifies promoted readonly properties keep the normal constructor-only write rule. +/// Verifies promoted readonly properties throw Error outside their constructor. #[test] -fn execute_program_rejects_promoted_readonly_property_write_after_constructor() { +fn execute_program_promoted_readonly_property_write_after_constructor_throws_error() { let program = parse_fragment( br#"class EvalPromotedReadonlyBox { public function __construct(public readonly int $id) {} @@ -256,16 +256,25 @@ fn execute_program_rejects_promoted_readonly_property_write_after_constructor() } $box = new EvalPromotedReadonlyBox(7); echo $box->id; -$box->replace(8);"#, +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("promoted readonly property write should fail outside constructor"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "7:Error:Cannot modify readonly property EvalPromotedReadonlyBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies readonly eval properties can be initialized inside their constructor. @@ -291,9 +300,9 @@ return $box->id();"#, assert_eq!(values.get(result), FakeValue::Int(7)); } -/// Verifies readonly eval properties reject writes outside the declaring constructor. +/// Verifies readonly eval properties throw Error on writes outside the declaring constructor. #[test] -fn execute_program_rejects_readonly_property_write_after_constructor() { +fn execute_program_readonly_property_write_after_constructor_throws_error() { let program = parse_fragment( br#"class EvalReadonlyBox { public readonly int $id; @@ -301,16 +310,25 @@ fn execute_program_rejects_readonly_property_write_after_constructor() { public function replace($id) { $this->id = $id; } } $box = new EvalReadonlyBox(7); -$box->replace(8);"#, +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("readonly property write should fail outside constructor"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Cannot modify readonly property EvalReadonlyBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies readonly eval properties must declare a type like PHP requires. @@ -368,9 +386,9 @@ return $box->id();"#, assert_eq!(values.get(result), FakeValue::Int(11)); } -/// Verifies readonly class instance properties reject writes after construction. +/// Verifies readonly class instance properties throw Error on writes after construction. #[test] -fn execute_program_rejects_readonly_class_property_write_after_constructor() { +fn execute_program_readonly_class_property_write_after_constructor_throws_error() { let program = parse_fragment( br#"readonly class EvalReadonlyClassFailBox { public int $id; @@ -378,37 +396,55 @@ fn execute_program_rejects_readonly_class_property_write_after_constructor() { public function replace($id) { $this->id = $id; } } $box = new EvalReadonlyClassFailBox(11); -$box->replace(12);"#, +try { + $box->replace(12); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("readonly class property write should fail outside constructor"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Cannot modify readonly property EvalReadonlyClassFailBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies readonly classes reject dynamic property creation when no magic setter handles it. +/// Verifies readonly classes throw Error on dynamic property creation without a magic setter. #[test] -fn execute_program_rejects_readonly_class_dynamic_property_creation() { +fn execute_program_readonly_class_dynamic_property_creation_throws_error() { let program = parse_fragment( br#"readonly class EvalReadonlyDynamicFailBox { public int $id; public function __construct($id) { $this->id = $id; } } $box = new EvalReadonlyDynamicFailBox(11); -$box->dynamic = 12;"#, +try { + $box->dynamic = 12; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("readonly class dynamic property creation should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Cannot create dynamic property EvalReadonlyDynamicFailBox::$dynamic" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies readonly classes may still handle missing property writes through `__set()`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fa21867fb2..79b47c46a2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7760,7 +7760,7 @@ echo $box->id();'); ); assert_eq!(out.stdout, "7"); - let err = compile_and_run_expect_failure( + let err = compile_and_run_capture( r#"id = $id; } } $box = new EvalReadonlyFailBox(7); -$box->replace(8);'); +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); "#, ); assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" + err.success, + "program failed: stdout={:?} stderr={}", + err.stdout, err.stderr + ); + assert_eq!( + err.stdout, + "Error:Cannot modify readonly property EvalReadonlyFailBox::$id" + ); + + let unset = compile_and_run_capture( + r#"id = $id; } +} +$box = new EvalReadonlyUnsetBox(7); +try { + unset($box->id); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + unset.success, + "program failed: stdout={:?} stderr={}", + unset.stdout, unset.stderr + ); + assert_eq!( + unset.stdout, + "Error:Cannot unset readonly property EvalReadonlyUnsetBox::$id" ); } @@ -7824,7 +7859,7 @@ eval('readonly class EvalReadonlyUntypedPropertyBox { "stderr did not contain eval runtime fatal diagnostic: {untyped_err}" ); - let err = compile_and_run_expect_failure( + let err = compile_and_run_capture( r#"id = $id; } } $box = new EvalReadonlyClassFailBox(7); -$box->replace(8);'); +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); "#, ); assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" + err.success, + "program failed: stdout={:?} stderr={}", + err.stdout, err.stderr + ); + assert_eq!( + err.stdout, + "Error:Cannot modify readonly property EvalReadonlyClassFailBox::$id" ); - let dynamic_err = compile_and_run_expect_failure( + let dynamic_err = compile_and_run_capture( r#"id = $id; } } $box = new EvalReadonlyClassDynamicFailBox(7); -$box->dynamic = 8;'); +try { + $box->dynamic = 8; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); "#, ); assert!( - dynamic_err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {dynamic_err}" + dynamic_err.success, + "program failed: stdout={:?} stderr={}", + dynamic_err.stdout, dynamic_err.stderr + ); + assert_eq!( + dynamic_err.stdout, + "Error:Cannot create dynamic property EvalReadonlyClassDynamicFailBox::$dynamic" ); let attribute_err = compile_and_run_expect_failure( From 1885cf0b31f54b24d90ab3da51e3806357dc6d83 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 04:05:56 +0200 Subject: [PATCH 0709/1208] Throw eval static property errors --- .../src/interpreter/statements.rs | 146 +++++++++++++++--- .../src/interpreter/tests/static_members.rs | 86 +++++++++++ tests/codegen/eval.rs | 96 ++++++++++++ 3 files changed, 308 insertions(+), 20 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 066dedd997..122f887a3b 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3673,6 +3673,55 @@ fn eval_throw_dynamic_property_creation_error( ) } +/// Throws PHP's undeclared static property error for static property access. +fn eval_throw_undeclared_static_property_error( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Access to undeclared static property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's uninitialized typed static property error. +fn eval_throw_uninitialized_static_property_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Typed static property {}::${} must not be accessed before initialization", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's class-not-found error for unresolved static member receivers. +fn eval_throw_class_not_found_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!("Class \"{}\" not found", class_name.trim_start_matches('\\')), + context, + values, + ) +} + /// Throws PHP's inaccessible constant error for eval-declared class constants. fn eval_throw_constant_access_error( declaring_class: &str, @@ -3724,7 +3773,12 @@ pub(in crate::interpreter) fn eval_static_property_get_result( let class_name = resolve_eval_static_member_class_name(class_name, context)?; if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { if !property.is_static() { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); } if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { return eval_throw_property_access_error( @@ -3735,12 +3789,23 @@ pub(in crate::interpreter) fn eval_static_property_get_result( values, ); } - return context - .static_property(&declaring_class, property.name()) - .ok_or(EvalStatus::RuntimeFatal); + if let Some(value) = context.static_property(&declaring_class, property.name()) { + return Ok(value); + } + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property.name(), + context, + values, + ); } if eval_static_member_context_owns_class(&class_name, context) { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); } if let Some((declaring_class, visibility, _, is_static)) = eval_reflection_aot_static_property_access_metadata( @@ -3750,19 +3815,34 @@ pub(in crate::interpreter) fn eval_static_property_get_result( values, )? { - if is_static && validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_throw_property_access_error( - &declaring_class, - property_name, - visibility, - context, - values, - ); + if is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + if !values.static_property_is_initialized(&declaring_class, property_name)? { + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property_name, + context, + values, + ); + } } } - values - .static_property_get(&class_name, property_name)? - .ok_or(EvalStatus::RuntimeFatal) + if let Some(value) = values.static_property_get(&class_name, property_name)? { + return Ok(value); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) + } else { + eval_throw_class_not_found_error(&class_name, context, values) + } } /// Reads one eval-declared class constant after resolving the class-like receiver. @@ -3937,7 +4017,12 @@ pub(in crate::interpreter) fn eval_static_property_set_result( let class_name = resolve_eval_static_member_class_name(class_name, context)?; if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { if !property.is_static() { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); } if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { return eval_throw_property_access_error( @@ -3962,7 +4047,12 @@ pub(in crate::interpreter) fn eval_static_property_set_result( return Ok(()); } if eval_static_member_context_owns_class(&class_name, context) { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); } if let Some((declaring_class, _, write_visibility, is_static)) = eval_reflection_aot_static_property_access_metadata( @@ -3985,9 +4075,12 @@ pub(in crate::interpreter) fn eval_static_property_set_result( } } if values.static_property_set(&class_name, property_name, value)? { - Ok(()) + return Ok(()); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) } else { - Err(EvalStatus::RuntimeFatal) + eval_throw_class_not_found_error(&class_name, context, values) } } @@ -4447,6 +4540,19 @@ fn eval_static_member_context_owns_class( || context.has_enum(class_name) } +/// Returns whether a static member receiver exists in eval metadata or generated metadata. +fn eval_runtime_class_like_exists( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_static_member_context_owns_class(class_name, context) + || values.class_exists(class_name)? + || eval_runtime_interface_exists(class_name, values)? + || values.trait_exists(class_name)? + || values.enum_exists(class_name)?) +} + /// Resolves class-name literal receivers without requiring named classes to exist. fn resolve_eval_class_name_literal( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/tests/static_members.rs b/crates/elephc-magician/src/interpreter/tests/static_members.rs index cc5b7c5c79..b38f2fd427 100644 --- a/crates/elephc-magician/src/interpreter/tests/static_members.rs +++ b/crates/elephc-magician/src/interpreter/tests/static_members.rs @@ -94,6 +94,92 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies invalid eval-declared static property access throws PHP-compatible Error values. +#[test] +fn execute_program_invalid_eval_static_property_access_throws_error() { + let program = parse_fragment( + br#"class EvalStaticPropertyErrors { + public int $instance = 1; + public static int $typed; +} +try { + echo EvalStaticPropertyErrors::$missing; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + echo EvalStaticPropertyErrors::$instance; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + echo EvalStaticPropertyErrors::$typed; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + EvalStaticPropertyErrors::$missing = 9; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Access to undeclared static property EvalStaticPropertyErrors::$missing|\ +Error:Access to undeclared static property EvalStaticPropertyErrors::$instance|\ +Error:Typed static property EvalStaticPropertyErrors::$typed must not be accessed before initialization|\ +Error:Access to undeclared static property EvalStaticPropertyErrors::$missing" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies generated/AOT static property misses distinguish missing classes from missing properties. +#[test] +fn execute_program_invalid_runtime_static_property_access_throws_error() { + let program = parse_fragment( + br#"try { + echo KnownClass::$missing; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + echo MissingRuntimeStaticClass::$missing; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Access to undeclared static property KnownClass::$missing|\ +Error:Class \"MissingRuntimeStaticClass\" not found" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval rejects static-style calls to non-static methods. #[test] fn execute_program_rejects_static_call_to_eval_instance_method() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 79b47c46a2..26dc8417e4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4832,6 +4832,54 @@ $right->run(); ); } +/// Verifies eval fragments throw Error for invalid AOT static property access. +#[test] +fn test_eval_fragment_invalid_aot_static_property_access_throws_error() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|"; +try { + EvalInvalidAotStaticPropBox::$instance; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalInvalidAotStaticPropBox::$typed; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalMissingAotStaticPropBox::$missing; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Access to undeclared static property EvalInvalidAotStaticPropBox::$missing|\ +Error:Access to undeclared static property EvalInvalidAotStaticPropBox::$instance|\ +Error:Typed static property EvalInvalidAotStaticPropBox::$typed must not be accessed before initialization|\ +Error:Class \"EvalMissingAotStaticPropBox\" not found" + ); +} + /// Verifies eval fragments can replace public array AOT static properties. #[test] fn test_eval_fragment_can_mutate_aot_array_static_property() { @@ -8330,6 +8378,54 @@ echo EvalStaticBase::baseRead();'); out.stdout, out.stderr ); assert_eq!(out.stdout, "1:3:3:14:5:5"); + + let errors = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + EvalInvalidStaticPropBox::$instance; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalInvalidStaticPropBox::$typed; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalInvalidStaticPropBox::$missing = 9; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + errors.success, + "program failed: stdout={:?} stderr={}", + errors.stdout, errors.stderr + ); + assert_eq!( + errors.stdout, + "Error:Access to undeclared static property EvalInvalidStaticPropBox::$missing|\ +Error:Access to undeclared static property EvalInvalidStaticPropBox::$instance|\ +Error:Typed static property EvalInvalidStaticPropBox::$typed must not be accessed before initialization|\ +Error:Access to undeclared static property EvalInvalidStaticPropBox::$missing" + ); } /// Verifies eval-declared static interface methods are validated and reflected. From 8b0e5ab40b8bdf87f0af18052fc2735fc1589c07 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 04:12:15 +0200 Subject: [PATCH 0710/1208] Throw eval static method errors --- .../src/interpreter/statements.rs | 78 ++++++++++++++++++- .../src/interpreter/tests/static_members.rs | 41 ++++++++-- tests/codegen/eval.rs | 47 +++++++++++ 3 files changed, 158 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 122f887a3b..523bf1a3a3 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3763,6 +3763,60 @@ fn eval_throw_method_access_error( ) } +/// Throws PHP's error for calling an instance method through static syntax. +fn eval_throw_non_static_method_call_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Non-static method {}::{}() cannot be called statically", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's error for calling an abstract method directly. +fn eval_throw_abstract_method_call_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot call abstract method {}::{}()", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's undefined method error after static magic fallback misses. +fn eval_throw_undefined_method_call_error( + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to undefined method {}::{}()", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + /// Reads one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_get_result( class_name: &str, @@ -4123,8 +4177,21 @@ pub(in crate::interpreter) fn eval_static_method_call_result( if let Some((declaring_class, method)) = eval_dynamic_static_method_for_call(&class_name, method_name, context) { - if !method.is_static() || method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); + if !method.is_static() { + return eval_throw_non_static_method_call_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if method.is_abstract() { + return eval_throw_abstract_method_call_error( + &declaring_class, + method.name(), + context, + values, + ); } if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { if let Some(result) = eval_magic_static_method_call( @@ -4167,7 +4234,12 @@ pub(in crate::interpreter) fn eval_static_method_call_result( )? { return Ok(result); } - return Err(EvalStatus::RuntimeFatal); + return eval_throw_undefined_method_call_error( + &class_name, + method_name, + context, + values, + ); } eval_native_static_method_with_evaluated_args( &class_name, diff --git a/crates/elephc-magician/src/interpreter/tests/static_members.rs b/crates/elephc-magician/src/interpreter/tests/static_members.rs index b38f2fd427..6480550ae2 100644 --- a/crates/elephc-magician/src/interpreter/tests/static_members.rs +++ b/crates/elephc-magician/src/interpreter/tests/static_members.rs @@ -180,21 +180,52 @@ Error:Class \"MissingRuntimeStaticClass\" not found" assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval rejects static-style calls to non-static methods. +/// Verifies invalid eval-declared static method calls throw PHP-compatible Error values. #[test] -fn execute_program_rejects_static_call_to_eval_instance_method() { +fn execute_program_invalid_static_method_calls_throw_error() { let program = parse_fragment( br#"class EvalStaticCallRules { public function read() { return 1; } } -return EvalStaticCallRules::read();"#, +class EvalStaticMissingRules {} +abstract class EvalStaticAbstractRules { + abstract public static function abs(); +} +try { + EvalStaticCallRules::read(); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + EvalStaticMissingRules::missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + EvalStaticAbstractRules::abs(); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - execute_program(&program, &mut scope, &mut values) - .expect_err("static call to instance method should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Non-static method EvalStaticCallRules::read() cannot be called statically|\ +Error:Call to undefined method EvalStaticMissingRules::missing()|\ +Error:Cannot call abstract method EvalStaticAbstractRules::abs()" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval allows object-style calls to accessible static methods. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 26dc8417e4..285b4fc65e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8428,6 +8428,53 @@ Error:Access to undeclared static property EvalInvalidStaticPropBox::$missing" ); } +/// Verifies invalid eval-declared static method calls throw catchable Error objects. +#[test] +fn test_eval_declared_invalid_static_method_calls_throw_error() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + EvalMissingStaticCallBox::missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalAbstractStaticCallBox::abs(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Non-static method EvalInvalidStaticCallBox::read() cannot be called statically|\ +Error:Call to undefined method EvalMissingStaticCallBox::missing()|\ +Error:Cannot call abstract method EvalAbstractStaticCallBox::abs()" + ); +} + /// Verifies eval-declared static interface methods are validated and reflected. #[test] fn test_eval_declared_static_interface_methods() { From 062a26fbff2ddc769d849fa71db74b88b46be04f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 04:19:03 +0200 Subject: [PATCH 0711/1208] Throw eval property write errors --- .../src/interpreter/statements.rs | 102 ++++++++++++++++-- .../src/interpreter/tests/classes.rs | 64 +++++++---- tests/codegen/eval.rs | 48 ++++++++- 3 files changed, 183 insertions(+), 31 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 523bf1a3a3..f5c08186af 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3044,10 +3044,9 @@ pub(in crate::interpreter) fn eval_property_set_result( ); } if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { - return eval_throw_property_access_error( + return eval_throw_property_write_access_error( &declaring_class, - property.name(), - property.write_visibility(), + &property, context, values, ); @@ -3091,7 +3090,12 @@ pub(in crate::interpreter) fn eval_property_set_result( return Ok(()); } } else if property.has_get_hook() { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_property_hook_readonly_error( + &declaring_class, + property.name(), + context, + values, + ); } } if !declared_property_found @@ -3301,7 +3305,14 @@ pub(in crate::interpreter) fn eval_property_unset_result( eval_dynamic_property_for_access(&object_class_name, property_name, context) { if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { - validate_eval_property_write_access(&declaring_class, &property, context)?; + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_unset_access_error( + &declaring_class, + &property, + context, + values, + ); + } if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { return eval_throw_readonly_property_unset_error( &declaring_class, @@ -3619,6 +3630,82 @@ fn eval_throw_property_access_error( ) } +/// Throws PHP's write access error for eval-declared properties. +fn eval_throw_property_write_access_error( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(set_visibility) = property.set_visibility() { + return eval_throw_error( + &format!( + "Cannot modify {}(set) property {}::${} from {}", + eval_visibility_label(set_visibility), + declaring_class.trim_start_matches('\\'), + property.name(), + eval_native_constructor_scope_label(context) + ), + context, + values, + ); + } + eval_throw_property_access_error( + declaring_class, + property.name(), + property.write_visibility(), + context, + values, + ) +} + +/// Throws PHP's unset access error for asymmetric eval-declared properties. +fn eval_throw_property_unset_access_error( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(set_visibility) = property.set_visibility() { + return eval_throw_error( + &format!( + "Cannot unset {}(set) property {}::${} from {}", + eval_visibility_label(set_visibility), + declaring_class.trim_start_matches('\\'), + property.name(), + eval_native_constructor_scope_label(context) + ), + context, + values, + ); + } + eval_throw_property_access_error( + declaring_class, + property.name(), + property.write_visibility(), + context, + values, + ) +} + +/// Throws PHP's read-only property-hook write error. +fn eval_throw_property_hook_readonly_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Property {}::${} is read-only", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + /// Throws PHP's readonly property assignment error for eval-declared properties. fn eval_throw_readonly_property_modification_error( declaring_class: &str, @@ -4079,10 +4166,9 @@ pub(in crate::interpreter) fn eval_static_property_set_result( ); } if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { - return eval_throw_property_access_error( + return eval_throw_property_write_access_error( &declaring_class, - property.name(), - property.write_visibility(), + &property, context, values, ); diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 9bc706dfce..5e38922787 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -581,9 +581,9 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval-declared `private(set)` rejects global writes without dispatching `__set`. +/// Verifies eval-declared `private(set)` throws Error without dispatching `__set`. #[test] -fn execute_program_rejects_private_set_property_write_outside_declaring_class() { +fn execute_program_private_set_property_write_outside_declaring_class_throws_error() { let program = parse_fragment( br#"class EvalAsymPrivateSetBox { public private(set) int $value = 1; @@ -592,37 +592,54 @@ fn execute_program_rejects_private_set_property_write_outside_declaring_class() } } $box = new EvalAsymPrivateSetBox(); -$box->value = 2;"#, +try { + $box->value = 2; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("global private(set) property write should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, ""); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Cannot modify private(set) property EvalAsymPrivateSetBox::$value from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval-declared `protected(set)` rejects global writes. +/// Verifies eval-declared `protected(set)` throws Error for global writes. #[test] -fn execute_program_rejects_protected_set_property_write_outside_hierarchy() { +fn execute_program_protected_set_property_write_outside_hierarchy_throws_error() { let program = parse_fragment( br#"class EvalAsymProtectedSetBox { public protected(set) int $value = 1; } $box = new EvalAsymProtectedSetBox(); -$box->value = 2;"#, +try { + $box->value = 2; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("global protected(set) property write should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Cannot modify protected(set) property EvalAsymProtectedSetBox::$value from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies asymmetric write restrictions cannot satisfy a public interface set contract. @@ -1477,9 +1494,9 @@ return class_exists("EvalGoodDebugInfoMagic") && class_exists("EvalGoodSetStateM assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies get-only property hooks reject writes outside a set accessor. +/// Verifies get-only property hooks throw Error on writes outside a set accessor. #[test] -fn execute_program_rejects_write_to_get_only_eval_property_hook() { +fn execute_program_write_to_get_only_eval_property_hook_throws_error() { let program = parse_fragment( br#"class EvalHookReadOnly { public int $answer { @@ -1487,16 +1504,25 @@ fn execute_program_rejects_write_to_get_only_eval_property_hook() { } } $box = new EvalHookReadOnly(); -$box->answer = 7;"#, +try { + $box->answer = 7; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("get-only property hook write should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Property EvalHookReadOnly::$answer is read-only" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval subclasses inherit parent property hooks. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 285b4fc65e..4596a4a8b0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8024,7 +8024,7 @@ echo $box->value . ":" . $box->shout();'); ); assert_eq!(out.stdout, "Ada!:Ada!?"); - let err = compile_and_run_expect_failure( + let err = compile_and_run_capture( r#"answer = 7;'); +try { + $box->answer = 7; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); "#, ); assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" + err.success, + "program failed: stdout={:?} stderr={}", + err.stdout, err.stderr ); + assert_eq!(err.stdout, "Error:Property EvalHookReadOnly::$answer is read-only"); } /// Verifies eval-declared magic property methods handle missing and inaccessible properties. @@ -10350,6 +10357,39 @@ echo $protected->getModifiers();'); "#, ); assert_eq!(out, "1:base:7:owner:child:Pt4129:pT2049"); + + let errors = compile_and_run_capture( + r#"privateValue = 7; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + unset($box->protectedValue); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + errors.success, + "program failed: stdout={:?} stderr={}", + errors.stdout, errors.stderr + ); + assert_eq!( + errors.stdout, + "Error:Cannot modify private(set) property EvalDeclaredAsymErrorBox::$privateValue from global scope|\ +Error:Cannot unset protected(set) property EvalDeclaredAsymErrorBox::$protectedValue from global scope" + ); } /// Verifies eval-declared inherited properties preserve PHP redeclaration invariants. From 783c268806da5de65852175628ad017b41ba68e2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 04:25:29 +0200 Subject: [PATCH 0712/1208] Throw eval clone access errors --- .../src/interpreter/statements.rs | 44 +++++++++++++++++-- .../src/interpreter/tests/classes.rs | 21 ++++++--- tests/codegen/eval.rs | 36 +++++++++------ 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index f5c08186af..7f75135a93 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3850,6 +3850,25 @@ fn eval_throw_method_access_error( ) } +/// Throws PHP's inaccessible clone-expression error for `__clone()` hooks. +fn eval_throw_clone_access_error( + declaring_class: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} {}::__clone() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + eval_native_constructor_scope_label(context) + ), + context, + values, + ) +} + /// Throws PHP's error for calling an instance method through static syntax. fn eval_throw_non_static_method_call_error( declaring_class: &str, @@ -4778,7 +4797,14 @@ pub(in crate::interpreter) fn eval_object_clone_result( .as_deref() .and_then(|class_name| context.class_method(class_name, "__clone")); if let Some((declaring_class, method)) = &clone_method { - validate_eval_member_access(declaring_class, method.visibility(), context)?; + if validate_eval_member_access(declaring_class, method.visibility(), context).is_err() { + return eval_throw_clone_access_error( + declaring_class, + method.visibility(), + context, + values, + ); + } } let should_call_aot_clone_hook = if dynamic_class_name.is_none() { eval_aot_clone_hook_is_callable(object, context, values)? @@ -4813,7 +4839,7 @@ pub(in crate::interpreter) fn eval_object_clone_result( /// Returns whether an accessible instance AOT `__clone()` hook should run. fn eval_aot_clone_hook_is_callable( object: RuntimeCellHandle, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let class_name = eval_runtime_object_class_name(object, values)?; @@ -4825,7 +4851,9 @@ fn eval_aot_clone_hook_is_callable( if is_static || is_abstract { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, visibility, context)?; + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_clone_access_error(&declaring_class, visibility, context, values); + } Ok(true) } @@ -5318,7 +5346,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( if is_static || is_abstract { return Err(EvalStatus::RuntimeFatal); } - validate_eval_member_access(&declaring_class, visibility, context)?; + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } } } return eval_native_method_with_evaluated_args( diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 5e38922787..797068ddfb 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1009,24 +1009,33 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies private `__clone()` is not callable through a global clone expression. +/// Verifies private `__clone()` throws Error through a global clone expression. #[test] -fn execute_program_rejects_private_clone_hook_outside_declaring_class() { +fn execute_program_private_clone_hook_outside_declaring_class_throws_error() { let program = parse_fragment( br#"class EvalCloneRuntimePrivateFail { private function __clone() {} } $box = new EvalCloneRuntimePrivateFail(); -clone $box;"#, +try { + clone $box; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("private clone hook should be inaccessible outside the class"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!( + values.output, + "Error:Call to private EvalCloneRuntimePrivateFail::__clone() from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies a get-only property hook computes a virtual eval property. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4596a4a8b0..e61a45b833 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14020,18 +14020,22 @@ class EvalCloneAotPrivateOutsideBox { } $object = new EvalCloneAotPrivateOutsideBox("A"); -eval('$copy = clone $object; echo $copy->name;'); +eval('try { + $copy = clone $object; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); "#, ); assert!( - !out.success, - "program unexpectedly succeeded: stdout={:?} stderr={}", + out.success, + "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert!( - out.stderr.contains("eval() runtime failed"), - "expected eval runtime failure for private clone hook, got stderr={}", - out.stderr + assert_eq!( + out.stdout, + "Error:Call to private EvalCloneAotPrivateOutsideBox::__clone() from global scope" ); } @@ -14049,18 +14053,22 @@ class EvalCloneAotPrivateDirectBox { } $object = new EvalCloneAotPrivateDirectBox(); -eval('$object->__clone();'); +eval('try { + $object->__clone(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); "#, ); assert!( - !out.success, - "program unexpectedly succeeded: stdout={:?} stderr={}", + out.success, + "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert!( - out.stderr.contains("eval() runtime failed"), - "expected eval runtime failure for private __clone call, got stderr={}", - out.stderr + assert_eq!( + out.stdout, + "Error:Call to private method EvalCloneAotPrivateDirectBox::__clone() from global scope" ); } From faa088b3eb9217f8a322b0ca0e1ac8ef7c6988eb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 04:41:39 +0200 Subject: [PATCH 0713/1208] Throw eval object string errors --- .../src/interpreter/dynamic_functions.rs | 60 ++++++++++++++++++- crates/elephc-magician/src/interpreter/mod.rs | 2 + .../src/interpreter/statements.rs | 18 +----- .../tests/builtins_class_metadata.rs | 3 +- .../src/interpreter/tests/classes.rs | 13 +++- .../src/interpreter/throwables.rs | 25 ++++++++ tests/codegen/eval.rs | 40 +++++++++++++ 7 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/throwables.rs diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 3625b018fc..f8009dfe6b 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -679,7 +679,7 @@ pub(in crate::interpreter) fn eval_string_context_value( eval_dynamic_object_string_context_value(value, context, values) } -/// Invokes `__toString()` for eval-declared objects and rejects missing invalid hooks. +/// Invokes `__toString()` for eval-declared objects or throws PHP's missing-hook error. fn eval_dynamic_object_string_context_value( value: RuntimeCellHandle, context: &mut ElephcEvalContext, @@ -692,7 +692,7 @@ fn eval_dynamic_object_string_context_value( let called_class_name = class.name().to_string(); let Some((declaring_class, method)) = context.class_method(&called_class_name, "__toString") else { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_object_to_string_error(&called_class_name, context, values); }; if method.visibility() != EvalVisibility::Public || method.is_static() @@ -719,6 +719,19 @@ fn eval_runtime_object_string_context_value( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + let identity = values.object_identity(value)?; + let class_name = runtime_object_class_name(value, values)?; + if !eval_runtime_object_has_interpreter_tostring(identity, &class_name, context) + && eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__toString", + context, + values, + )? + .is_none() + { + return eval_throw_object_to_string_error(&class_name, context, values); + } let result = eval_method_call_result_with_evaluated_args( value, "__toString", @@ -729,6 +742,49 @@ fn eval_runtime_object_string_context_value( eval_tostring_result_to_string(result, values) } +/// Returns whether eval owns a synthetic `__toString()` implementation for the runtime object. +fn eval_runtime_object_has_interpreter_tostring( + identity: u64, + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context.eval_reflection_class_name(identity).is_some() + || context.eval_reflection_function_name(identity).is_some() + || context.eval_reflection_method(identity).is_some() + || context.eval_reflection_property(identity).is_some() + || context.eval_reflection_class_constant(identity).is_some() + || eval_runtime_class_name_has_reflection_tostring(class_name) +} + +/// Returns whether a synthetic reflection class has `__toString()` handled by eval dispatch. +fn eval_runtime_class_name_has_reflection_tostring(class_name: &str) -> bool { + let class_name = class_name.trim_start_matches('\\'); + [ + "ReflectionParameter", + "ReflectionNamedType", + "ReflectionUnionType", + "ReflectionIntersectionType", + ] + .iter() + .any(|reflection_class| class_name.eq_ignore_ascii_case(reflection_class)) +} + +/// Throws PHP's catchable object-to-string conversion error. +fn eval_throw_object_to_string_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Object of class {} could not be converted to string", + class_name.trim_start_matches('\\') + ), + context, + values, + ) +} + /// Normalizes one `__toString()` result to a boxed string cell. fn eval_tostring_result_to_string( result: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 2c9b943413..e3d5ad1011 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -29,6 +29,7 @@ mod return_values; mod runtime_ops; mod scope_cells; mod statements; +mod throwables; use crate::context::{ ElephcEvalContext, ElephcEvalExecutionScope, EvalReferenceTarget, NativeCallableDefault, @@ -71,6 +72,7 @@ pub use runtime_ops::RuntimeValueOps; use runtime_ops::*; use scope_cells::*; use statements::*; +use throwables::*; use std::ffi::{CStr, CString}; use std::mem::MaybeUninit; use std::net::ToSocketAddrs; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 7f75135a93..d8c781bedc 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4606,20 +4606,6 @@ fn eval_throw_reflection_exception( Err(EvalStatus::UncaughtThrowable) } -/// Creates and schedules an `Error` through eval's normal Throwable channel. -fn eval_throw_error( - message: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exception = values.new_object("Error")?; - let message = values.string(message)?; - let code = values.int(0)?; - values.construct_object(exception, vec![message, code])?; - context.set_pending_throw(exception); - Err(EvalStatus::UncaughtThrowable) -} - /// Schedules the Throwable category required by one ReflectionClass instantiation error. fn eval_throw_reflection_instantiation_error( error: EvalReflectionInstantiationError, @@ -5555,7 +5541,7 @@ fn eval_magic_call_arg_array( } /// Returns the runtime-visible class name for a non-eval object receiver. -fn runtime_object_class_name( +pub(in crate::interpreter) fn runtime_object_class_name( object: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { @@ -6689,7 +6675,7 @@ fn eval_native_method_access_error( } /// Finds generated/AOT method metadata on a class or its native parent chain. -fn eval_aot_method_dispatch_metadata_in_hierarchy( +pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata_in_hierarchy( class_name: &str, method_name: &str, context: &ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 815f1926d7..2d038fb3d7 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -2036,7 +2036,8 @@ fn execute_program_reflection_class_to_string() { public int $id = 7; public function read(string $name = "Ada"): ?string { return $name; } } -echo (new ReflectionClass("EvalReflectClassStringTarget"))->__toString(); +$ref = new ReflectionClass("EvalReflectClassStringTarget"); +echo $ref; return true;"#, ) .expect("parse eval fragment"); diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 797068ddfb..03b2f79460 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1338,13 +1338,22 @@ fn execute_program_rejects_eval_object_string_context_without_tostring() { let program = parse_fragment( br#"class EvalPlainStringContext {} $box = new EvalPlainStringContext(); -echo $box;"#, +try { + echo $box; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - execute_program(&program, &mut scope, &mut values).expect_err("missing __toString should fail"); + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Object of class EvalPlainStringContext could not be converted to string" + ); } /// Verifies eval rejects magic methods whose staticness, arity, or fatal contracts are invalid. diff --git a/crates/elephc-magician/src/interpreter/throwables.rs b/crates/elephc-magician/src/interpreter/throwables.rs new file mode 100644 index 0000000000..ab3e3290ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/throwables.rs @@ -0,0 +1,25 @@ +//! Purpose: +//! Builds PHP Throwable objects for interpreter paths that need catchable runtime errors. +//! +//! Called from: +//! - `crate::interpreter::statements` and dynamic dispatch helpers. +//! +//! Key details: +//! - Helpers schedule the object in `ElephcEvalContext` and return `UncaughtThrowable` +//! so surrounding try/catch execution can consume it. + +use super::*; + +/// Creates and schedules an `Error` through eval's normal Throwable channel. +pub(in crate::interpreter) fn eval_throw_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("Error")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e61a45b833..ef3cd9ac72 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3324,6 +3324,46 @@ echo $box->accepts($box);'); ); } +/// Verifies eval-declared objects without `__toString()` throw catchable PHP errors in string contexts. +#[test] +fn test_eval_declared_object_string_context_without_tostring_throws_error() { + let out = compile_and_run( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Object of class EvalPlainStringContext could not be converted to string" + ); +} + +/// Verifies AOT objects stringified from eval throw PHP's catchable conversion error. +#[test] +fn test_eval_aot_object_string_context_without_tostring_throws_error() { + let out = compile_and_run( + r#"getMessage(); +}'); +"#, + ); + assert_eq!( + out, + "Error:Object of class EvalAotPlainStringContext could not be converted to string" + ); +} + /// Verifies eval `settype()` mutates direct variables and supports named arguments. #[test] fn test_eval_dispatches_settype_builtin_call() { From d394587b7d8476d834219080a527efc13db2b7e9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 04:49:04 +0200 Subject: [PATCH 0714/1208] Throw eval missing method errors --- .../src/interpreter/statements.rs | 2 +- .../src/interpreter/tests/expressions.rs | 28 +++++++++++++++++++ tests/codegen/eval.rs | 26 +++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index d8c781bedc..8f06382a1b 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -5411,7 +5411,7 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( values, ); } - Err(EvalStatus::RuntimeFatal) + eval_throw_undefined_method_call_error(&called_class_name, method_name, context, values) } /// Dispatches an invokable object through `__invoke()` without enforcing hook visibility. diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index 59e3ffa38e..2e5b153ecf 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -888,6 +888,34 @@ return true;"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies missing eval-declared instance methods throw PHP-compatible Error values. +#[test] +fn execute_program_missing_eval_method_call_throws_error() { + let program = parse_fragment( + br#"class EvalMissingMethodBox {} +$box = new EvalMissingMethodBox(); +try { + echo $box->missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Call to undefined method EvalMissingMethodBox::missing()" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval rejects overriding a public method with lower visibility. #[test] fn execute_program_rejects_method_override_with_reduced_visibility() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ef3cd9ac72..a1a01dcb78 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8828,6 +8828,32 @@ echo $box->hidden("C", name: "D");'); assert_eq!(out.stdout, "DoThing:A:B:hidden:C:D"); } +/// Verifies missing eval-declared instance methods throw catchable PHP errors. +#[test] +fn test_eval_declared_missing_instance_method_throws_error() { + let out = compile_and_run_capture( + r#"missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Call to undefined method EvalMissingInstanceCallBox::missing()" + ); +} + /// Verifies eval static method fallback dispatches missing and inaccessible methods through `__callStatic`. #[test] fn test_eval_declared_magic_call_static_method_fallback() { From a9484d8e577d2a48603f139d6392cb425489e8b9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 04:58:12 +0200 Subject: [PATCH 0715/1208] Throw eval non-callable object errors --- .../src/interpreter/expressions.rs | 5 +++ .../src/interpreter/statements.rs | 40 ++++++++++++++++++- .../src/interpreter/tests/dynamic_calls.rs | 18 ++++++++- tests/codegen/eval.rs | 18 ++++++++- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 370952c6a9..acf74ef0ed 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -561,6 +561,11 @@ pub(in crate::interpreter) fn eval_dynamic_call( values: &mut impl RuntimeValueOps, ) -> Result { let callback = eval_expr(callee, context, scope, values)?; + if values.type_tag(callback)? == EVAL_TAG_OBJECT { + eval_invokable_object_precheck(callback, context, values)?; + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + return eval_invokable_object_call_result(callback, evaluated_args, context, values); + } let callback = eval_callable(callback, context, values)?; let evaluated_args = eval_call_arg_values(args, context, scope, values)?; eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 8f06382a1b..ffe6e981cc 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3923,6 +3923,22 @@ fn eval_throw_undefined_method_call_error( ) } +/// Throws PHP's error for invoking an object without `__invoke()`. +pub(in crate::interpreter) fn eval_throw_object_not_callable_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Object of type {} is not callable", + class_name.trim_start_matches('\\') + ), + context, + values, + ) +} + /// Reads one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_get_result( class_name: &str, @@ -5432,7 +5448,7 @@ pub(in crate::interpreter) fn eval_invokable_object_call_result( let called_class_name = class.name().to_string(); let Some((declaring_class, method)) = context.class_method(&called_class_name, "__invoke") else { - return Err(EvalStatus::RuntimeFatal); + return eval_throw_object_not_callable_error(&called_class_name, context, values); }; if method.is_static() || method.is_abstract() { return Err(EvalStatus::RuntimeFatal); @@ -5448,6 +5464,28 @@ pub(in crate::interpreter) fn eval_invokable_object_call_result( ) } +/// Rejects non-invokable eval-declared objects before dynamic-call arguments are evaluated. +pub(in crate::interpreter) fn eval_invokable_object_precheck( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(()); + }; + let called_class_name = class.name().to_string(); + let Some((_, method)) = context.class_method(&called_class_name, "__invoke") else { + return eval_throw_object_not_callable_error(&called_class_name, context, values); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + /// Dispatches a missing or inaccessible eval instance method through `__call()`. fn eval_magic_instance_method_call( object: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 8a81b63e98..28e8c43428 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -205,7 +205,11 @@ return $named(right: "H", left: "G");"#, #[test] fn execute_program_invokes_eval_object_callables() { let program = parse_fragment( - br#"class EvalInvokableBox { + br#"function eval_plain_call_side_effect() { + echo "bad"; + return "x"; +} +class EvalInvokableBox { public function __construct($label = "box") { $this->label = $label; } @@ -219,6 +223,13 @@ $plain = new EvalPlainCallableProbe(); echo is_callable($box) ? "Y:" : "N:"; echo is_callable($plain) ? "bad:" : "plain:"; echo $box(right: "D", left: "C"); echo ":"; +try { + $plain(eval_plain_call_side_effect()); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo ":"; echo (new EvalInvokableBox("new"))("E", "F"); echo ":"; echo call_user_func($box, "G", "H"); echo ":"; return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, @@ -229,7 +240,10 @@ return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "Y:plain:box:CD:new:EF:box:GH:"); + assert_eq!( + values.output, + "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:" + ); assert_eq!(values.get(result), FakeValue::String("box:IJ".to_string())); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a1a01dcb78..1cc76fae9e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8777,7 +8777,11 @@ echo $named(right: "H", left: "G");'); fn test_eval_declared_invokable_object_dynamic_callables() { let out = compile_and_run_capture( r#"label = $label; } @@ -8791,6 +8795,13 @@ $plain = new EvalPlainCallableProbe(); echo is_callable($box) ? "Y:" : "N:"; echo is_callable($plain) ? "bad:" : "plain:"; echo $box(right: "D", left: "C") . ":"; +try { + $plain(eval_plain_call_side_effect()); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo ":"; echo (new EvalInvokableBox("new"))("E", "F") . ":"; echo call_user_func($box, "G", "H") . ":"; echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); @@ -8801,7 +8812,10 @@ echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "Y:plain:box:CD:new:EF:box:GH:box:IJ"); + assert_eq!( + out.stdout, + "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:box:IJ" + ); } /// Verifies eval object method fallback dispatches missing and inaccessible methods through `__call`. From f7126edbc085424c649713a77840c68780fb7292 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 05:04:21 +0200 Subject: [PATCH 0716/1208] Throw eval callable object type errors --- .../interpreter/builtins/registry/callable.rs | 27 ++++++++++++-- .../src/interpreter/tests/dynamic_calls.rs | 35 +++++++++++++++++++ .../src/interpreter/throwables.rs | 14 ++++++++ tests/codegen/eval.rs | 34 ++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index da269d1b62..2b504a9e25 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -50,7 +50,7 @@ pub(in crate::interpreter) fn eval_call_user_func_array_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable(callback, context, values)?; + let callback = eval_call_user_func_callback(callback, "call_user_func_array", context, values)?; if !values.is_array_like(arg_array)? { return Err(EvalStatus::RuntimeFatal); } @@ -67,10 +67,33 @@ pub(in crate::interpreter) fn eval_call_user_func_with_values( let Some((callback, callback_args)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; - let callback = eval_callable(*callback, context, values)?; + let callback = eval_call_user_func_callback(*callback, "call_user_func", context, values)?; eval_evaluated_callable_with_values(&callback, callback_args.to_vec(), context, values) } +/// Normalizes a `call_user_func*` callback and maps non-invokable objects to PHP's TypeError. +fn eval_call_user_func_callback( + callback: RuntimeCellHandle, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_callable(callback, context, values) { + Ok(callback) => Ok(callback), + Err(EvalStatus::UnsupportedConstruct) if values.type_tag(callback)? == EVAL_TAG_OBJECT => { + eval_throw_type_error( + &format!( + "{}(): Argument #1 ($callback) must be a valid callback, no array or string given", + function_name + ), + context, + values, + ) + } + Err(status) => Err(status), + } +} + /// Normalizes one PHP callback value for eval dynamic callable dispatch. pub(in crate::interpreter) fn eval_callable( callback: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 28e8c43428..695f6865af 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -247,6 +247,41 @@ return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, assert_eq!(values.get(result), FakeValue::String("box:IJ".to_string())); } +/// Verifies call_user_func rejects non-invokable eval objects with PHP's TypeError. +#[test] +fn execute_program_call_user_func_rejects_non_invokable_eval_object() { + let program = parse_fragment( + br#"class EvalPlainCallbackError {} +$plain = new EvalPlainCallbackError(); +try { + call_user_func($plain); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func_array($plain, []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, no array or string given" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies object-method callable arrays preserve eval named-argument binding. #[test] fn execute_program_object_method_callable_array_binds_eval_named_args() { diff --git a/crates/elephc-magician/src/interpreter/throwables.rs b/crates/elephc-magician/src/interpreter/throwables.rs index ab3e3290ac..bdaa266c90 100644 --- a/crates/elephc-magician/src/interpreter/throwables.rs +++ b/crates/elephc-magician/src/interpreter/throwables.rs @@ -23,3 +23,17 @@ pub(in crate::interpreter) fn eval_throw_error( context.set_pending_throw(exception); Err(EvalStatus::UncaughtThrowable) } + +/// Creates and schedules a `TypeError` through eval's normal Throwable channel. +pub(in crate::interpreter) fn eval_throw_type_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("TypeError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1cc76fae9e..fc5935a7a3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8818,6 +8818,40 @@ echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); ); } +/// Verifies eval call_user_func rejects non-invokable objects with PHP's TypeError. +#[test] +fn test_eval_call_user_func_rejects_non_invokable_object() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + call_user_func_array($plain, []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, no array or string given" + ); +} + /// Verifies eval object method fallback dispatches missing and inaccessible methods through `__call`. #[test] fn test_eval_declared_magic_call_method_fallback() { From 5e21c2c9ec07eaaebed8061f3881cb65249e7f2a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 05:26:29 +0200 Subject: [PATCH 0717/1208] Validate eval method callbacks --- .../interpreter/builtins/registry/callable.rs | 13 +- .../builtins/registry/callable_validation.rs | 388 ++++++++++++++++++ .../src/interpreter/builtins/registry/mod.rs | 2 + .../src/interpreter/builtins/symbols.rs | 35 +- .../src/interpreter/statements.rs | 2 +- .../src/interpreter/tests/dynamic_calls.rs | 93 +++++ tests/codegen/eval.rs | 89 ++++ 7 files changed, 609 insertions(+), 13 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 2b504a9e25..d4ce3518ba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -79,13 +79,14 @@ fn eval_call_user_func_callback( values: &mut impl RuntimeValueOps, ) -> Result { match eval_callable(callback, context, values) { - Ok(callback) => Ok(callback), + Ok(callback) => { + eval_validate_call_user_func_callback(&callback, function_name, context, values)?; + Ok(callback) + } Err(EvalStatus::UnsupportedConstruct) if values.type_tag(callback)? == EVAL_TAG_OBJECT => { - eval_throw_type_error( - &format!( - "{}(): Argument #1 ($callback) must be a valid callback, no array or string given", - function_name - ), + eval_call_user_func_type_error( + function_name, + "no array or string given", context, values, ) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs new file mode 100644 index 0000000000..2b01aabbb5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -0,0 +1,388 @@ +//! Purpose: +//! Validates normalized call_user_func callback targets before dispatch. +//! Keeps PHP's invalid-callback TypeError messages out of generic callable normalization. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::callable`. +//! +//! Key details: +//! - Direct callable invocation still uses normal method-call errors; these helpers +//! are scoped to `call_user_func` and `call_user_func_array`. + +use super::super::super::*; + +/// Validates callback targets whose PHP errors depend on method metadata. +pub(in crate::interpreter) fn eval_validate_call_user_func_callback( + callback: &EvaluatedCallable, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match callback { + EvaluatedCallable::ObjectMethod { object, method } => { + eval_validate_call_user_func_object_method( + *object, + method, + function_name, + context, + values, + ) + } + EvaluatedCallable::StaticMethod { class_name, method } => { + eval_validate_call_user_func_static_method( + class_name, + method, + function_name, + context, + values, + ) + } + EvaluatedCallable::Named(_) | EvaluatedCallable::InvokableObject { .. } => Ok(()), + } +} + +/// Validates `[$object, "method"]` callbacks before call_user_func dispatch. +fn eval_validate_call_user_func_object_method( + object: RuntimeCellHandle, + method_name: &str, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return eval_validate_call_user_func_native_object_method( + object, + method_name, + function_name, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + let Some((declaring_class, method)) = + eval_dynamic_method_for_call(&called_class_name, method_name, context) + else { + if eval_call_user_func_instance_magic_callable(&called_class_name, context) { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + function_name, + &called_class_name, + method_name, + context, + values, + ); + }; + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_call_user_func_instance_magic_callable(&called_class_name, context) + { + return Ok(()); + } + eval_call_user_func_method_access_type_error( + function_name, + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ) +} + +/// Validates generated/AOT object-method callbacks when method metadata is available. +fn eval_validate_call_user_func_native_object_method( + object: RuntimeCellHandle, + method_name: &str, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = runtime_object_class_name(object, values)?; + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? + else { + if eval_call_user_func_native_instance_magic_callable(&class_name, context, values)? + || !values.class_exists(&class_name)? + { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + function_name, + &class_name, + method_name, + context, + values, + ); + }; + if is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_call_user_func_native_instance_magic_callable(&class_name, context, values)? + { + return Ok(()); + } + eval_call_user_func_method_access_type_error( + function_name, + &declaring_class, + method_name, + visibility, + context, + values, + ) +} + +/// Validates `["Class", "method"]` callbacks before call_user_func dispatch. +fn eval_validate_call_user_func_static_method( + class_name: &str, + method_name: &str, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { + return Ok(()); + } + if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { + if !method.is_static() { + return eval_call_user_func_non_static_method_type_error( + function_name, + &declaring_class, + method.name(), + context, + values, + ); + } + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_call_user_func_static_magic_callable(&class_name, context) + { + return Ok(()); + } + return eval_call_user_func_method_access_type_error( + function_name, + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { + if eval_call_user_func_static_magic_callable(&class_name, context) { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + function_name, + &class_name, + method_name, + context, + values, + ); + } + eval_validate_call_user_func_native_static_method( + &class_name, + method_name, + function_name, + context, + values, + ) +} + +/// Validates generated/AOT static-method callbacks when method metadata is available. +fn eval_validate_call_user_func_native_static_method( + class_name: &str, + method_name: &str, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + if eval_call_user_func_native_static_magic_callable(class_name, context, values)? + || !values.class_exists(class_name)? + { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + function_name, + class_name, + method_name, + context, + values, + ); + }; + if !is_static { + return eval_call_user_func_non_static_method_type_error( + function_name, + &declaring_class, + method_name, + context, + values, + ); + } + if is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_call_user_func_native_static_magic_callable(class_name, context, values)? + { + return Ok(()); + } + eval_call_user_func_method_access_type_error( + function_name, + &declaring_class, + method_name, + visibility, + context, + values, + ) +} + +/// Returns whether an eval class has an instance magic-call fallback. +fn eval_call_user_func_instance_magic_callable( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a static magic-call fallback. +fn eval_call_user_func_static_magic_callable( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) +} + +/// Returns whether an AOT class has an instance magic-call fallback. +fn eval_call_user_func_native_instance_magic_callable( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether an AOT class has a static magic-call fallback. +fn eval_call_user_func_native_static_magic_callable( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Throws call_user_func's TypeError for a missing object or static method callback. +fn eval_call_user_func_missing_method_type_error( + function_name: &str, + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_call_user_func_type_error( + function_name, + &format!( + "class {} does not have a method \"{}\"", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws call_user_func's TypeError for an inaccessible method callback. +fn eval_call_user_func_method_access_type_error( + function_name: &str, + class_name: &str, + method_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_call_user_func_type_error( + function_name, + &format!( + "cannot access {} method {}::{}()", + eval_call_user_func_visibility_label(visibility), + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws call_user_func's TypeError for non-static static-method callbacks. +fn eval_call_user_func_non_static_method_type_error( + function_name: &str, + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_call_user_func_type_error( + function_name, + &format!( + "non-static method {}::{}() cannot be called statically", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws a call_user_func or call_user_func_array invalid-callback TypeError. +pub(in crate::interpreter) fn eval_call_user_func_type_error( + function_name: &str, + reason: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_type_error( + &format!( + "{}(): Argument #1 ($callback) must be a valid callback, {}", + function_name, reason + ), + context, + values, + ) +} + +/// Returns PHP's lowercase visibility label used in callback TypeError messages. +fn eval_call_user_func_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index df9a2c1891..887213ab4c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -11,10 +11,12 @@ mod binding; mod callable; +mod callable_validation; mod dispatch; mod names; pub(in crate::interpreter) use binding::*; pub(in crate::interpreter) use callable::*; +pub(in crate::interpreter) use callable_validation::*; pub(in crate::interpreter) use dispatch::*; pub(in crate::interpreter) use names::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 6e90842ee0..c9fe9f468a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -790,7 +790,10 @@ fn eval_object_method_callable_probe( let Some((declaring_class, method)) = eval_dynamic_method_for_call(class.name(), method_name, context) else { - return Ok(false); + return Ok(eval_instance_magic_method_callable_probe( + class.name(), + context, + )); }; if method.is_abstract() { return Ok(false); @@ -798,7 +801,8 @@ fn eval_object_method_callable_probe( if method_name.eq_ignore_ascii_case("__invoke") { return Ok(true); } - Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok()) + Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_instance_magic_method_callable_probe(class.name(), context)) } /// Returns whether one static method can be called from the current eval scope. @@ -811,9 +815,28 @@ fn eval_static_method_callable_probe( return true; } let Some((declaring_class, method)) = context.class_method(class_name, method_name) else { - return false; + return eval_static_magic_method_callable_probe(class_name, context); }; - method.is_static() - && !method.is_abstract() - && validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + if !method.is_static() || method.is_abstract() { + return false; + } + validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_static_magic_method_callable_probe(class_name, context) +} + +/// Returns whether an eval class has a callable instance `__call()` fallback. +fn eval_instance_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a callable static `__callStatic()` fallback. +fn eval_static_magic_method_callable_probe(class_name: &str, context: &ElephcEvalContext) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index ffe6e981cc..cea4042741 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4696,7 +4696,7 @@ pub(in crate::interpreter) fn resolve_eval_static_class_name( } /// Resolves static member receivers while allowing non-eval class names to reach AOT lookup. -fn resolve_eval_static_member_class_name( +pub(in crate::interpreter) fn resolve_eval_static_member_class_name( class_name: &str, context: &ElephcEvalContext, ) -> Result { diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 695f6865af..1946499547 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -282,6 +282,99 @@ TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callba assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies call_user_func rejects invalid object-method callable arrays with PHP's TypeError. +#[test] +fn execute_program_call_user_func_rejects_invalid_object_method_arrays() { + let program = parse_fragment( + br#"class EvalMissingCallbackArray {} +class EvalPrivateCallbackArray { + private function hidden() { + return "bad"; + } +} +class EvalInstanceCallbackArray { + public function inst() { + return "bad"; + } +} +$missing = new EvalMissingCallbackArray(); +try { + call_user_func([$missing, "MiSsInG"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func_array([$missing, "missing"], []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func([new EvalPrivateCallbackArray(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func(["EvalInstanceCallbackArray", "inst"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"MiSsInG\"|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"missing\"|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateCallbackArray::hidden()|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, non-static method EvalInstanceCallbackArray::inst() cannot be called statically" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies call_user_func callable arrays still dispatch through magic method fallbacks. +#[test] +fn execute_program_call_user_func_arrays_dispatch_magic_method_fallbacks() { + let program = parse_fragment( + br#"class EvalMagicCallbackArray { + public function __call($method, $args) { + return $method . ":" . $args[0]; + } + public static function __callStatic($method, $args) { + return $method . ":" . $args[0]; + } +} +$box = new EvalMagicCallbackArray(); +echo is_callable([$box, "missing"]) ? "Y:" : "N:"; +echo call_user_func([$box, "missing"], "A") . ":"; +echo call_user_func_array([$box, "missing"], ["B"]) . ":"; +echo is_callable(["EvalMagicCallbackArray", "static_missing"]) ? "S:" : "s:"; +return call_user_func(["EvalMagicCallbackArray", "static_missing"], "C");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:missing:A:missing:B:S:"); + assert_eq!( + values.get(result), + FakeValue::String("static_missing:C".to_string()) + ); +} + /// Verifies object-method callable arrays preserve eval named-argument binding. #[test] fn execute_program_object_method_callable_array_binds_eval_named_args() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fc5935a7a3..c8b8983d51 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8852,6 +8852,95 @@ TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callba ); } +/// Verifies eval call_user_func rejects invalid method callable arrays with PHP's TypeError. +#[test] +fn test_eval_call_user_func_rejects_invalid_method_callable_arrays() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + call_user_func_array([$missing, "missing"], []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func([new EvalPrivateCallbackArray(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func(["EvalInstanceCallbackArray", "inst"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"MiSsInG\"|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"missing\"|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateCallbackArray::hidden()|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, non-static method EvalInstanceCallbackArray::inst() cannot be called statically" + ); +} + +/// Verifies eval callable arrays use `__call` and `__callStatic` magic fallbacks. +#[test] +fn test_eval_call_user_func_method_callable_arrays_use_magic_fallbacks() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 05:28:50 +0200 Subject: [PATCH 0718/1208] Cover eval AOT method callback errors --- tests/codegen/eval.rs | 53 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c8b8983d51..3b36d2c27a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8912,6 +8912,59 @@ TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no ); } +/// Verifies eval call_user_func validates method callable arrays for AOT classes too. +#[test] +fn test_eval_call_user_func_rejects_invalid_aot_method_callable_arrays() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +try { + call_user_func([new EvalAotPrivateCallbackArray(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func_array(["EvalAotInstanceCallbackArray", "inst"], []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalAotMissingCallbackArray does not have a method \"missing\"|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalAotPrivateCallbackArray::hidden()|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, non-static method EvalAotInstanceCallbackArray::inst() cannot be called statically" + ); +} + /// Verifies eval callable arrays use `__call` and `__callStatic` magic fallbacks. #[test] fn test_eval_call_user_func_method_callable_arrays_use_magic_fallbacks() { From dfb1943580f93c9b29a63c5832ae71c2b33f83ba Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 05:35:47 +0200 Subject: [PATCH 0719/1208] Probe AOT method callables in eval --- .../src/interpreter/builtins/symbols.rs | 102 +++++++++++++++--- tests/codegen/eval.rs | 33 ++++++ 2 files changed, 122 insertions(+), 13 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index c9fe9f468a..b3bdd6c480 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -765,9 +765,9 @@ fn eval_callable_probe_exists( EvaluatedCallable::ObjectMethod { object, method } => { eval_object_method_callable_probe(*object, method, context, values) } - EvaluatedCallable::StaticMethod { class_name, method } => Ok( - eval_static_method_callable_probe(class_name, method, context), - ), + EvaluatedCallable::StaticMethod { class_name, method } => { + eval_static_method_callable_probe(class_name, method, context, values) + } } } @@ -782,7 +782,7 @@ fn eval_object_method_callable_probe( return Ok(false); }; let Some(class) = context.dynamic_object_class(identity) else { - return Ok(false); + return eval_aot_object_method_callable_probe(object, method_name, context, values); }; if eval_enum_static_builtin_applies(class.name(), method_name, context).is_some() { return Ok(true); @@ -810,18 +810,94 @@ fn eval_static_method_callable_probe( class_name: &str, method_name: &str, context: &ElephcEvalContext, -) -> bool { - if eval_enum_static_builtin_applies(class_name, method_name, context).is_some() { - return true; + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { + return Ok(true); + } + if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { + if !method.is_static() || method.is_abstract() { + return Ok(false); + } + return Ok(validate_eval_member_access(&declaring_class, method.visibility(), context) + .is_ok() + || eval_static_magic_method_callable_probe(&class_name, context)); + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { + return Ok(eval_static_magic_method_callable_probe(&class_name, context)); } - let Some((declaring_class, method)) = context.class_method(class_name, method_name) else { - return eval_static_magic_method_callable_probe(class_name, context); + eval_aot_static_method_callable_probe(&class_name, method_name, context, values) +} + +/// Returns whether a generated/AOT object method can be called from the current eval scope. +fn eval_aot_object_method_callable_probe( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = runtime_object_class_name(object, values)?; + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? + else { + return eval_aot_instance_magic_method_callable_probe(&class_name, context, values); }; - if !method.is_static() || method.is_abstract() { - return false; + if is_abstract { + return Ok(false); } - validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() - || eval_static_magic_method_callable_probe(class_name, context) + Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_aot_instance_magic_method_callable_probe(&class_name, context, values)?) +} + +/// Returns whether a generated/AOT static method can be called from the current eval scope. +fn eval_aot_static_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + return eval_aot_static_magic_method_callable_probe(class_name, context, values); + }; + if !is_static || is_abstract { + return Ok(false); + } + Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_aot_static_magic_method_callable_probe(class_name, context, values)?) +} + +/// Returns whether a generated/AOT class has a callable instance `__call()` fallback. +fn eval_aot_instance_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether a generated/AOT class has a callable static `__callStatic()` fallback. +fn eval_aot_static_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) } /// Returns whether an eval class has a callable instance `__call()` fallback. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3b36d2c27a..1dc3aff393 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8965,6 +8965,39 @@ TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callba ); } +/// Verifies eval `is_callable()` probes method callable arrays for AOT classes. +#[test] +fn test_eval_is_callable_checks_aot_method_callable_arrays() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 05:41:40 +0200 Subject: [PATCH 0720/1208] Fix eval AOT inherited member introspection --- .../class_metadata/oop_introspection.rs | 28 +++++++++++-- tests/codegen/eval.rs | 42 +++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 00fb265e07..e1ba03bdb5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -16,6 +16,7 @@ use super::{eval_class_metadata_name, eval_class_relation_name_exists}; use std::collections::HashSet; const EVAL_CLASS_METADATA_FLAG_PUBLIC: u64 = 2; +const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; /// Evaluates `method_exists()` or `property_exists()` from eval expressions. pub(in crate::interpreter) fn eval_builtin_member_exists( @@ -205,6 +206,15 @@ fn eval_method_exists_on_class( .iter() .any(|name| name.eq_ignore_ascii_case(method_name))); } + if target_is_object { + return Ok(eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + method_name, + context, + values, + )? + .is_some()); + } values .reflection_method_flags(class_name, method_name) .map(|flags| flags.is_some()) @@ -235,9 +245,21 @@ fn eval_property_exists_on_class( .iter() .any(|name| name == property_name)); } - values - .reflection_property_flags(class_name, property_name) - .map(|flags| flags.is_some()) + let Some(flags) = values.reflection_property_flags(class_name, property_name)? else { + return Ok(false); + }; + if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE == 0 { + return Ok(true); + } + let Some(declaring_class) = values.reflection_property_declaring_class( + class_name, + property_name, + )? else { + return Ok(true); + }; + Ok(declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(class_name.trim_start_matches('\\'))) } /// Resolves an object-or-class argument to a PHP class name and records whether it was an object. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1dc3aff393..2020f9170d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7797,6 +7797,48 @@ echo function_exists("get_class_methods"); echo function_exists("get_object_vars ); } +/// Verifies eval OOP introspection builtins honor AOT inherited private-member rules. +#[test] +fn test_eval_oop_introspection_builtins_for_aot_inherited_private_members() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 05:46:38 +0200 Subject: [PATCH 0721/1208] Skip eval uninitialized object vars --- .../class_metadata/oop_introspection.rs | 7 ++++ tests/codegen/eval.rs | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index e1ba03bdb5..76fbe3a959 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -421,6 +421,7 @@ fn eval_add_declared_object_vars( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + let identity = values.object_identity(object)?; for class in context.class_chain(class_name) { for property in class.properties() { if property.is_static() @@ -430,6 +431,12 @@ fn eval_add_declared_object_vars( { continue; } + let storage_property_name = eval_instance_property_storage_name(class.name(), property); + if !property.is_virtual() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + continue; + } result = eval_add_object_var( result, object, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2020f9170d..02bbe8ad4e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7839,6 +7839,38 @@ echo property_exists($object, "childSecret") ? "objectChildPrivateProperty" : "b ); } +/// Verifies eval `get_object_vars()` skips uninitialized typed properties like PHP. +#[test] +fn test_eval_get_object_vars_skips_uninitialized_declared_properties() { + let out = compile_and_run_capture( + r#"a = 5; +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars)); echo ":"; +unset($object->a); +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars));'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "PA:b:a,b:b"); +} + /// Verifies eval-declared private parent properties keep separate storage when a child shadows them. #[test] fn test_eval_declared_private_parent_property_shadowing() { From 3810a6be9435b3255f9dbd1de64f15ceb95a1d72 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 05:54:47 +0200 Subject: [PATCH 0722/1208] Expose AOT object vars in eval --- .../class_metadata/oop_introspection.rs | 91 ++++++++++++++++++- tests/codegen/eval.rs | 50 ++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 76fbe3a959..eee7236e7e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -15,7 +15,9 @@ use super::super::super::*; use super::{eval_class_metadata_name, eval_class_relation_name_exists}; use std::collections::HashSet; +const EVAL_CLASS_METADATA_FLAG_STATIC: u64 = 1; const EVAL_CLASS_METADATA_FLAG_PUBLIC: u64 = 2; +const EVAL_CLASS_METADATA_FLAG_PROTECTED: u64 = 4; const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; /// Evaluates `method_exists()` or `property_exists()` from eval expressions. @@ -114,7 +116,8 @@ pub(in crate::interpreter) fn eval_get_object_vars_result( return eval_public_object_vars_result(*object, values); }; let Some(class) = context.dynamic_object_class(identity) else { - return eval_public_object_vars_result(*object, values); + let class_name = eval_object_class_metadata_name(*object, context, values)?; + return eval_runtime_object_vars_result(*object, &class_name, context, values); }; let class_name = class.name().to_string(); eval_dynamic_object_vars_result(*object, &class_name, context, values) @@ -392,6 +395,92 @@ fn eval_dynamic_object_vars_result( eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &storage_keys, values) } +/// Builds `get_object_vars()` for generated/AOT objects from reflection metadata. +fn eval_runtime_object_vars_result( + object: RuntimeCellHandle, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_names = values.reflection_property_names(class_name)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(declared_names.len() + property_count)?; + let mut emitted_keys = HashSet::new(); + result = eval_add_runtime_declared_object_vars( + result, + object, + class_name, + &declared_names, + &mut emitted_keys, + context, + values, + )?; + eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &HashSet::new(), values) +} + +/// Adds generated/AOT declared instance properties visible from the current eval scope. +fn eval_add_runtime_declared_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + property_names: &[String], + emitted_keys: &mut HashSet, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + for property_name in property_names { + let Some((declaring_class, visibility, is_static)) = + eval_runtime_property_access_metadata(class_name, property_name, values)? + else { + continue; + }; + if is_static + || validate_eval_member_access(&declaring_class, visibility, context).is_err() + || emitted_keys.contains(property_name) + || !values.property_is_initialized(object, property_name)? + { + continue; + } + emitted_keys.insert(property_name.clone()); + let key = values.string(property_name)?; + let value = values.property_get(object, property_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns access metadata for one generated/AOT property name, if reflection exposes it. +fn eval_runtime_property_access_metadata( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_property_flags(class_name, property_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_property_declaring_class(class_name, property_name)? + .unwrap_or_else(|| class_name.to_string()); + Ok(Some(( + declaring_class, + eval_runtime_property_visibility(flags), + flags & EVAL_CLASS_METADATA_FLAG_STATIC != 0, + ))) +} + +/// Converts generated/AOT reflection property flags into eval visibility metadata. +fn eval_runtime_property_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_CLASS_METADATA_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + /// Adds synthetic enum properties exposed by PHP enum case objects. fn eval_add_enum_object_vars( mut result: RuntimeCellHandle, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 02bbe8ad4e..79d4c8afde 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7871,6 +7871,56 @@ echo implode(",", array_keys($vars));'); assert_eq!(out.stdout, "PA:b:a,b:b"); } +/// Verifies eval `get_object_vars()` exposes initialized generated/AOT properties by scope. +#[test] +fn test_eval_get_object_vars_exposes_initialized_aot_properties_by_scope() { + let out = compile_and_run_capture( + r#"childView(); echo ":"; +echo $object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "baseNullable,basePublic,childNullable,childPublic,implicit:baseNullable,baseProtected,basePublic,childNullable,childProtected,childPublic,childSecret,implicit:baseNullable,baseProtected,basePublic,baseSecret,childNullable,childProtected,childPublic,implicit" + ); +} + /// Verifies eval-declared private parent properties keep separate storage when a child shadows them. #[test] fn test_eval_declared_private_parent_property_shadowing() { From f5a6252c69543da926ce47d47a8a377b7ebd7362 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 06:06:14 +0200 Subject: [PATCH 0723/1208] Respect AOT private shadowed properties in eval --- .../class_metadata/oop_introspection.rs | 45 ++++++++ src/codegen/eval_property_helpers.rs | 101 +++++++++++++++--- tests/codegen/eval.rs | 39 +++++++ 3 files changed, 170 insertions(+), 15 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index eee7236e7e..6680815b72 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -408,6 +408,13 @@ fn eval_runtime_object_vars_result( let property_count = values.object_property_len(object)?; let mut result = values.assoc_new(declared_names.len() + property_count)?; let mut emitted_keys = HashSet::new(); + result = eval_add_runtime_scope_private_object_vars( + result, + object, + &mut emitted_keys, + context, + values, + )?; result = eval_add_runtime_declared_object_vars( result, object, @@ -420,6 +427,44 @@ fn eval_runtime_object_vars_result( eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &HashSet::new(), values) } +/// Adds generated/AOT private properties declared by the current eval class scope. +fn eval_add_runtime_scope_private_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + emitted_keys: &mut HashSet, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(current_class) = context.current_class_scope() else { + return Ok(result); + }; + if !values.object_is_a(object, current_class, false)? { + return Ok(result); + } + let property_names = values.reflection_property_names(current_class)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + for property_name in declared_names { + let Some((_, visibility, is_static)) = + eval_runtime_property_access_metadata(current_class, &property_name, values)? + else { + continue; + }; + if is_static + || visibility != EvalVisibility::Private + || emitted_keys.contains(&property_name) + || !values.property_is_initialized(object, &property_name)? + { + continue; + } + emitted_keys.insert(property_name.clone()); + let key = values.string(&property_name)?; + let value = values.property_get(object, &property_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Adds generated/AOT declared instance properties visible from the current eval scope. fn eval_add_runtime_declared_object_vars( mut result: RuntimeCellHandle, diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index a36ed6adf9..a6b3e2cbac 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -35,6 +35,7 @@ struct EvalPropertySlot { offset: usize, ty: PhpType, is_declared: bool, + is_hidden_shadow: bool, } /// Emits eval property helpers when any lowered function owns an eval context. @@ -102,32 +103,92 @@ fn collect_class_property_slots( slots: &mut Vec, ) { for (index, (property, ty)) in class_info.properties.iter().enumerate() { - if class_info.visible_property_index(property) != Some(index) { + let visible_index = class_info.visible_property_index(property); + let (declaring_class, visibility, is_hidden_shadow) = if visible_index == Some(index) { + let declaring_class = class_info + .property_declaring_classes + .get(property) + .map(String::as_str) + .unwrap_or(class_name) + .to_string(); + (declaring_class, property_visibility(class_info, property).clone(), false) + } else { + let Some((declaring_class, visibility)) = + hidden_private_property_slot_metadata(module, class_name, index, property) + else { + continue; + }; + (declaring_class, visibility, true) + }; + if !property_visibility_supported(&visibility) || !property_type_supported(ty) { continue; } - let visibility = property_visibility(class_info, property); - if !property_visibility_supported(visibility) || !property_type_supported(ty) { - continue; - } - let declaring_class = class_info - .property_declaring_classes - .get(property) - .map(String::as_str) - .unwrap_or(class_name); slots.push(EvalPropertySlot { class_id: class_info.class_id, class_name: class_name.to_string(), - declaring_class: declaring_class.to_string(), - allowed_scopes: visibility_scope_names(module, declaring_class, visibility), + declaring_class: declaring_class.clone(), + allowed_scopes: visibility_scope_names(module, &declaring_class, &visibility), property: property.clone(), - visibility: visibility.clone(), + visibility, offset: 8 + index * 16, ty: ty.codegen_repr(), is_declared: class_info.property_slot_is_declared(index, property), + is_hidden_shadow, }); } } +/// Returns metadata for a private parent slot hidden by a same-named child property. +fn hidden_private_property_slot_metadata( + module: &Module, + class_name: &str, + index: usize, + property: &str, +) -> Option<(String, Visibility)> { + for (ancestor_name, ancestor_info) in class_ancestry(module, class_name) { + let Some((ancestor_property, _)) = ancestor_info.properties.get(index) else { + continue; + }; + if ancestor_property != property || ancestor_info.visible_property_index(property) != Some(index) + { + continue; + } + let visibility = property_visibility(ancestor_info, property).clone(); + if visibility != Visibility::Private { + continue; + } + let declaring_class = ancestor_info + .property_declaring_classes + .get(property) + .cloned() + .unwrap_or_else(|| ancestor_name.to_string()); + return Some((declaring_class, visibility)); + } + None +} + +/// Returns class metadata from root parent to the requested class. +fn class_ancestry<'a>(module: &'a Module, class_name: &'a str) -> Vec<(&'a str, &'a ClassInfo)> { + let mut chain = Vec::new(); + collect_class_ancestry(module, class_name, &mut chain); + chain +} + +/// Recursively collects class metadata from parent to child. +fn collect_class_ancestry<'a>( + module: &'a Module, + class_name: &'a str, + chain: &mut Vec<(&'a str, &'a ClassInfo)>, +) { + let Some(class_info) = module.class_infos.get(class_name) else { + return; + }; + if let Some(parent) = class_info.parent.as_deref() { + collect_class_ancestry(module, parent, chain); + } + chain.push((class_name, class_info)); +} + /// Returns the declared property visibility, defaulting to public metadata. fn property_visibility<'a>(class_info: &'a ClassInfo, property: &str) -> &'a Visibility { class_info @@ -581,7 +642,12 @@ fn emit_aarch64_property_name_compare( let miss_label = slot_access_miss_label(module, slot, mode); emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue property dispatch when names differ let scope_ok_label = slot_scope_ok_label(module, slot, mode); - emit_aarch64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, fail_label); + let scope_fail_label = if slot.is_hidden_shadow { + miss_label.as_str() + } else { + fail_label + }; + emit_aarch64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, scope_fail_label); emitter.label(&scope_ok_label); emitter.instruction(&format!("b {}", target_label)); // dispatch after scoped visibility is satisfied emitter.label(&miss_label); @@ -611,7 +677,12 @@ fn emit_x86_64_property_name_compare( let miss_label = slot_access_miss_label(module, slot, mode); emitter.instruction(&format!("je {}", miss_label)); // continue property dispatch when names differ let scope_ok_label = slot_scope_ok_label(module, slot, mode); - emit_x86_64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, fail_label); + let scope_fail_label = if slot.is_hidden_shadow { + miss_label.as_str() + } else { + fail_label + }; + emit_x86_64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, scope_fail_label); emitter.label(&scope_ok_label); emitter.instruction(&format!("jmp {}", target_label)); // dispatch after scoped visibility is satisfied emitter.label(&miss_label); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 79d4c8afde..1419790957 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7921,6 +7921,45 @@ echo $object->parentView();'); ); } +/// Verifies AOT `get_object_vars()` lets parent scopes see shadowed private slots. +#[test] +fn test_eval_get_object_vars_uses_aot_private_parent_shadowing_scope() { + let out = compile_and_run_capture( + r#"childView(); echo "|"; +echo $object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "value:c|value:c|value:p"); +} + /// Verifies eval-declared private parent properties keep separate storage when a child shadows them. #[test] fn test_eval_declared_private_parent_property_shadowing() { From 996b9768833d045ef0bccc9e9ce39febaab70681 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 06:19:24 +0200 Subject: [PATCH 0724/1208] Respect AOT private parent methods in eval --- src/codegen/eval_method_helpers.rs | 105 ++++++++++++++++++++++++++- tests/codegen/eval.rs | 109 +++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index f907719821..a96c1e81d3 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -41,6 +41,7 @@ struct EvalMethodSlot { params: Vec, ref_params: Vec, return_ty: PhpType, + is_hidden_shadow: bool, } /// Static method metadata needed by eval static method-call bridge dispatch. @@ -171,6 +172,13 @@ fn collect_class_method_slots( emitted_methods: &std::collections::HashSet<(String, String, bool)>, slots: &mut Vec, ) { + collect_hidden_private_ancestor_method_slots( + module, + class_name, + class_info, + emitted_methods, + slots, + ); let mut methods = class_info.methods.iter().collect::>(); methods.sort_by_key(|(method, _)| method.as_str()); for (method, sig) in methods { @@ -199,10 +207,57 @@ fn collect_class_method_slots( params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), return_ty: sig.return_type.codegen_repr(), + is_hidden_shadow: false, }); } } +/// Adds private ancestor instance methods hidden by descendant class method lookup. +fn collect_hidden_private_ancestor_method_slots( + module: &Module, + class_name: &str, + class_info: &ClassInfo, + emitted_methods: &std::collections::HashSet<(String, String, bool)>, + slots: &mut Vec, +) { + for (ancestor_name, ancestor_info) in class_ancestry(module, class_name) { + if ancestor_name == class_name { + continue; + } + let mut methods = ancestor_info.methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method, sig) in methods { + let visibility = method_visibility(ancestor_info, method); + if visibility != &Visibility::Private + || !method_signature_supported(sig) + || !method_return_supported(&sig.return_type) + { + continue; + } + let impl_class = ancestor_info + .method_impl_classes + .get(method) + .map(String::as_str) + .unwrap_or(ancestor_name); + if !emitted_methods.contains(&(impl_class.to_string(), method.clone(), false)) { + continue; + } + slots.push(EvalMethodSlot { + class_id: class_info.class_id, + class_name: class_name.to_string(), + method: method.clone(), + impl_class: impl_class.to_string(), + visibility: visibility.clone(), + allowed_scopes: visibility_scope_names(module, impl_class, visibility), + params: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), + ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), + return_ty: sig.return_type.codegen_repr(), + is_hidden_shadow: true, + }); + } + } +} + /// Adds bridge-supported static methods for one class. fn collect_class_static_method_slots( module: &Module, @@ -259,6 +314,28 @@ fn static_method_visibility<'a>(class_info: &'a ClassInfo, method: &str) -> &'a .unwrap_or(&Visibility::Public) } +/// Returns class metadata from root parent to the requested class. +fn class_ancestry<'a>(module: &'a Module, class_name: &'a str) -> Vec<(&'a str, &'a ClassInfo)> { + let mut chain = Vec::new(); + collect_class_ancestry(module, class_name, &mut chain); + chain +} + +/// Recursively collects class metadata from parent to child. +fn collect_class_ancestry<'a>( + module: &'a Module, + class_name: &'a str, + chain: &mut Vec<(&'a str, &'a ClassInfo)>, +) { + let Some(class_info) = module.class_infos.get(class_name) else { + return; + }; + if let Some(parent) = class_info.parent.as_deref() { + collect_class_ancestry(module, parent, chain); + } + chain.push((class_name, class_info)); +} + /// Returns true when the eval method bridge can enforce this visibility. fn method_visibility_supported(visibility: &Visibility) -> bool { matches!( @@ -731,7 +808,19 @@ fn emit_aarch64_method_name_compare( } let miss_label = method_access_miss_label(module, slot); emitter.instruction(&format!("cbnz x0, {}", miss_label)); // continue method dispatch when names differ - emit_aarch64_method_scope_check(emitter, data, &slot.allowed_scopes, false, &target_label, fail_label); + let scope_fail_label = if slot.is_hidden_shadow { + miss_label.as_str() + } else { + fail_label + }; + emit_aarch64_method_scope_check( + emitter, + data, + &slot.allowed_scopes, + false, + &target_label, + scope_fail_label, + ); emitter.label(&miss_label); } @@ -757,7 +846,19 @@ fn emit_x86_64_method_name_compare( } let miss_label = method_access_miss_label(module, slot); emitter.instruction(&format!("jne {}", miss_label)); // continue method dispatch when names differ - emit_x86_64_method_scope_check(emitter, data, &slot.allowed_scopes, false, &target_label, fail_label); + let scope_fail_label = if slot.is_hidden_shadow { + miss_label.as_str() + } else { + fail_label + }; + emit_x86_64_method_scope_check( + emitter, + data, + &slot.allowed_scopes, + false, + &target_label, + scope_fail_label, + ); emitter.label(&miss_label); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1419790957..f128ce39fe 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4817,6 +4817,41 @@ $box->run(); assert_eq!(out, "5"); } +/// Verifies eval fragments use AOT private parent static slots when child classes shadow them. +#[test] +fn test_eval_fragment_uses_aot_private_parent_static_property_shadowing_scope() { + let out = compile_and_run_capture( + r#"secret();'); + } +} +class EvalPrivateAotParentReceiverChild extends EvalPrivateAotParentReceiverBase {} +$object = new EvalPrivateAotParentReceiverChild(); +eval('echo $object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "p"); +} + +/// Verifies eval fragments dispatch AOT private parent methods shadowed by child methods. +#[test] +fn test_eval_fragment_dispatches_aot_private_parent_method_shadowing_scope() { + let out = compile_and_run_capture( + r#"secret();'); + } + public function parentCallback() { + return eval('$cb = [$this, "secret"]; return call_user_func($cb);'); + } + public static function parentStaticView() { + return eval('return self::staticSecret();'); + } +} +class EvalPrivateAotMethodShadowChild extends EvalPrivateAotMethodShadowParent { + public function secret() { return "c"; } + public static function staticSecret() { return "cs"; } + public function childView() { + return eval('return $this->secret();'); + } + public function childCallback() { + return eval('$cb = [$this, "secret"]; return call_user_func($cb);'); + } + public static function childStaticView() { + return eval('return self::staticSecret();'); + } +} +$object = new EvalPrivateAotMethodShadowChild(); +eval('echo $object->secret(); echo "|"; +echo $object->childView(); echo "|"; +echo $object->parentView(); echo "|"; +echo $object->childCallback(); echo "|"; +echo $object->parentCallback(); echo "|"; +echo EvalPrivateAotMethodShadowChild::staticSecret(); echo "|"; +echo EvalPrivateAotMethodShadowChild::childStaticView(); echo "|"; +echo EvalPrivateAotMethodShadowChild::parentStaticView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "c|c|p|c|p|cs|cs|ps"); +} + /// Verifies eval fragments can call inherited protected AOT methods from child scopes. #[test] fn test_eval_fragment_can_call_protected_aot_method_from_child_scope() { From 5180c4fb929bc130b04834f5d9777420190dba6b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 06:55:58 +0200 Subject: [PATCH 0725/1208] Dispatch AOT magic method fallbacks in eval --- .../src/interpreter/statements.rs | 181 +++++++++++++++--- tests/codegen/eval.rs | 39 ++++ 2 files changed, 192 insertions(+), 28 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index cea4042741..3bb9bb931c 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -6630,17 +6630,59 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if let Some((declaring_class, visibility)) = - eval_native_method_access_error(class_name, method_name, context, values)? - { - return eval_throw_method_access_error( - &declaring_class, + let metadata = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; + if let Some((declaring_class, visibility, _, is_abstract)) = metadata { + if !is_abstract + && validate_eval_member_access(&declaring_class, visibility, context).is_err() + { + if eval_native_instance_magic_method_available(class_name, context, values)? { + return eval_native_magic_instance_method_call( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + } else if eval_native_instance_magic_method_available(class_name, context, values)? { + return eval_native_magic_instance_method_call( + object, + class_name, method_name, - visibility, + evaluated_args, context, values, ); } + eval_native_method_with_evaluated_args_unchecked( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ) +} + +/// Calls one generated/AOT instance method without enforcing member visibility. +fn eval_native_method_with_evaluated_args_unchecked( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { let signature = context.native_method_signature(class_name, method_name); let bound_args = bind_native_callable_bound_args( signature, @@ -6664,17 +6706,56 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if let Some((declaring_class, visibility)) = - eval_native_method_access_error(class_name, method_name, context, values)? - { - return eval_throw_method_access_error( - &declaring_class, + let metadata = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; + if let Some((declaring_class, visibility, is_static, is_abstract)) = metadata { + if is_static + && !is_abstract + && validate_eval_member_access(&declaring_class, visibility, context).is_err() + { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + } else if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, method_name, - visibility, + evaluated_args, context, values, ); } + eval_native_static_method_with_evaluated_args_unchecked( + class_name, + method_name, + evaluated_args, + context, + values, + ) +} + +/// Calls one generated/AOT static method without enforcing member visibility. +fn eval_native_static_method_with_evaluated_args_unchecked( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { let signature = context.native_static_method_signature(class_name, method_name); let bound_args = bind_native_callable_bound_args( signature, @@ -6691,25 +6772,69 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( } } -/// Returns generated/AOT method access metadata when current eval scope cannot call it. -fn eval_native_method_access_error( +/// Returns whether a generated/AOT class has an instance `__call()` fallback. +fn eval_native_instance_magic_method_available( class_name: &str, - method_name: &str, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, visibility, _, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? - else { - return Ok(None); - }; - if is_abstract { - return Ok(None); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_ok() { - return Ok(None); - } - Ok(Some((declaring_class, visibility))) +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether a generated/AOT class has a static `__callStatic()` fallback. +fn eval_native_static_magic_method_available( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Dispatches a missing or inaccessible generated/AOT instance method through `__call()`. +fn eval_native_magic_instance_method_call( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_native_method_with_evaluated_args_unchecked( + object, + class_name, + "__call", + magic_args, + context, + values, + ) +} + +/// Dispatches a missing or inaccessible generated/AOT static method through `__callStatic()`. +fn eval_native_magic_static_method_call( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_native_static_method_with_evaluated_args_unchecked( + class_name, + "__callStatic", + magic_args, + context, + values, + ) } /// Finds generated/AOT method metadata on a class or its native parent chain. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f128ce39fe..961bede511 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9372,6 +9372,45 @@ echo EvalMagicStaticBox::Hidden("C", name: "D");'); assert_eq!(out.stdout, "DoStatic:A:B:Hidden:C:D"); } +/// Verifies eval AOT method fallback dispatches missing and inaccessible methods through `__call`. +#[test] +fn test_eval_aot_magic_call_method_fallback() { + let out = compile_and_run_capture( + r#"DoThing("A", "B") . ":"; +echo $box->hidden("C", "D") . ":"; +echo EvalAotMagicStaticBox::DoStatic("E", "F") . ":"; +echo EvalAotMagicStaticBox::Hidden("G", "H") . ":"; +echo is_callable([$box, "hidden"]) ? "OC:" : "bad:"; +echo call_user_func([$box, "hidden"], "I", "J") . ":"; +echo is_callable(["EvalAotMagicStaticBox", "Hidden"]) ? "SC:" : "bad:"; +echo call_user_func(["EvalAotMagicStaticBox", "Hidden"], "K", "L");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "DoThing:A:B:hidden:C:D:DoStatic:E:F:Hidden:G:H:OC:hidden:I:J:SC:Hidden:K:L" + ); +} + /// Verifies eval rejects invalid magic method contracts during dynamic class declaration. #[test] fn test_eval_rejects_invalid_magic_method_contracts() { From 5629ca6a593bf3ccf79d39822b8324b6c70390ed Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 08:05:25 +0200 Subject: [PATCH 0726/1208] Coerce native eval method arguments --- .../builtins/registry/callable_validation.rs | 6 ++ .../src/interpreter/dynamic_functions.rs | 2 +- .../src/interpreter/statements.rs | 66 +++++++++++++++++-- .../src/interpreter/tests/dynamic_calls.rs | 22 ++++++- .../src/interpreter/tests/method_arguments.rs | 2 +- tests/codegen/eval.rs | 41 +++++++++++- 6 files changed, 125 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index 2b01aabbb5..49d0528cc3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -107,6 +107,9 @@ fn eval_validate_call_user_func_native_object_method( eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? else { if eval_call_user_func_native_instance_magic_callable(&class_name, context, values)? + || context + .native_method_signature(&class_name, method_name) + .is_some() || !values.class_exists(&class_name)? { return Ok(()); @@ -213,6 +216,9 @@ fn eval_validate_call_user_func_native_static_method( eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? else { if eval_call_user_func_native_static_magic_callable(class_name, context, values)? + || context + .native_static_method_signature(class_name, method_name) + .is_some() || !values.class_exists(class_name)? { return Ok(()); diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index f8009dfe6b..26dacf454d 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -500,7 +500,7 @@ fn bind_dynamic_variadic_arg( } /// Applies one eval method parameter type to a bound runtime value. -fn eval_method_parameter_value( +pub(in crate::interpreter) fn eval_method_parameter_value( param_type: &EvalParameterType, value: RuntimeCellHandle, context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 3bb9bb931c..51317a219b 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1743,7 +1743,7 @@ fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalSt /// Validates staticness, visibility, arity, and declared return type for one eval magic method. fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus> { let name = method.name().to_ascii_lowercase(); - if is_validated_eval_magic_method(&name) { + if validated_eval_magic_method_rejects_by_ref_params(&name) { validate_magic_no_by_ref_params(method)?; } match name.as_str() { @@ -1828,6 +1828,11 @@ fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus Ok(()) } +/// Returns whether PHP rejects by-reference parameters for this magic method. +fn validated_eval_magic_method_rejects_by_ref_params(name: &str) -> bool { + is_validated_eval_magic_method(name) && !matches!(name, "__construct" | "__invoke") +} + /// Returns whether eval knows PHP declaration-time rules for this magic method. fn is_validated_eval_magic_method(name: &str) -> bool { matches!( @@ -6379,7 +6384,7 @@ pub(in crate::interpreter) fn bind_native_callable_bound_args( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(signature) = signature else { - return positional_evaluated_bound_args(None, args); + return positional_evaluated_bound_args(None, args, context, values); }; if !signature.bridge_supported() { return Err(EvalStatus::RuntimeFatal); @@ -6387,7 +6392,7 @@ pub(in crate::interpreter) fn bind_native_callable_bound_args( if signature.param_names().len() == signature.param_count() { bind_native_signature_args(&signature, args, context, values) } else { - positional_evaluated_bound_args(Some(&signature), args) + positional_evaluated_bound_args(Some(&signature), args, context, values) } } @@ -6395,11 +6400,14 @@ pub(in crate::interpreter) fn bind_native_callable_bound_args( fn positional_evaluated_bound_args( signature: Option<&NativeCallableSignature>, args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { if args.iter().any(|arg| arg.name.is_some()) { return Err(EvalStatus::RuntimeFatal); } - args.into_iter() + let mut bound_args = args + .into_iter() .enumerate() .map(|(index, arg)| { let ref_target = if signature.is_some_and(|signature| signature.param_by_ref(index)) { @@ -6413,7 +6421,11 @@ fn positional_evaluated_bound_args( variadic_ref_targets: Vec::new(), }) }) - .collect() + .collect::, _>>()?; + if let Some(signature) = signature { + apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; + } + Ok(bound_args) } /// Returns only runtime cell values from bound native AOT call arguments. @@ -6506,10 +6518,50 @@ fn bind_native_signature_args( }); } - bound_args + let mut bound_args = bound_args .into_iter() .collect::>>() - .ok_or(EvalStatus::RuntimeFatal) + .ok_or(EvalStatus::RuntimeFatal)?; + apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; + Ok(bound_args) +} + +/// Applies registered native AOT parameter types after argument binding and default filling. +fn apply_native_callable_bound_arg_types( + signature: &NativeCallableSignature, + bound_args: &mut [BoundMethodArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, bound_arg) in bound_args.iter_mut().enumerate() { + let Some(param_type) = signature.param_type(position) else { + continue; + }; + if signature.param_variadic(position) { + apply_native_callable_variadic_arg_type(param_type, bound_arg, context, values)?; + } else { + bound_arg.value = + eval_method_parameter_value(param_type, bound_arg.value, context, values)?; + } + } + Ok(()) +} + +/// Applies one registered native variadic parameter type to each collected argument. +fn apply_native_callable_variadic_arg_type( + param_type: &EvalParameterType, + bound_arg: &mut BoundMethodArg, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let len = values.array_len(bound_arg.value)?; + for position in 0..len { + let key = values.array_iter_key(bound_arg.value, position)?; + let value = values.array_get(bound_arg.value, key)?; + let value = eval_method_parameter_value(param_type, value, context, values)?; + bound_arg.value = values.array_set(bound_arg.value, key, value)?; + } + Ok(()) } /// Returns the native callable variadic slot, if metadata registered one. diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 1946499547..28fd1b59cf 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -411,10 +411,28 @@ echo $named("E", "F"); echo ":"; return call_user_func_array(["KnownClass", "sum"], [2, 5]);"#, ) .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut join_signature = NativeCallableSignature::new(2); + assert!(join_signature.set_param_name(0, "left")); + assert!(join_signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature( + "KnownClass", + "join", + join_signature + )); + let mut sum_signature = NativeCallableSignature::new(2); + assert!(sum_signature.set_param_name(0, "left")); + assert!(sum_signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature( + "KnownClass", + "sum", + sum_signature + )); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); assert_eq!(values.output, "AB:CD:EF:"); assert_eq!(values.get(result), FakeValue::Int(7)); @@ -498,7 +516,7 @@ fn execute_program_static_runtime_method_hook_rejects_unregistered_named_args() let error = execute_program(&program, &mut scope, &mut values).expect_err("named AOT call should fail"); - assert_eq!(error, EvalStatus::RuntimeFatal); + assert_eq!(error, EvalStatus::UncaughtThrowable); } /// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs index 39079f4654..7879cbe885 100644 --- a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs @@ -349,7 +349,7 @@ $changer->set($box->value);"#, let mut values = FakeOps::default(); let err = execute_program(&private_program, &mut scope, &mut values) .expect_err("private property by-ref target should fail from global scope"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(err, EvalStatus::UncaughtThrowable); let readonly_program = parse_fragment( br#"class EvalByRefReadonlyPropertyFailChanger { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 961bede511..0aa3d1f6bf 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3324,6 +3324,37 @@ echo $box->accepts($box);'); ); } +/// Verifies eval string contexts dispatch AOT `__toString()` through the runtime method bridge. +#[test] +fn test_eval_aot_tostring_string_contexts() { + let out = compile_and_run( + r#"name; + } + public function accepts(string $value) { + return "typed:" . $value; + } +} +$box = new EvalAotStringableBox(); +eval('echo $box; echo ":"; +print $box; echo ":"; +echo "pre" . $box; echo ":"; +echo strval($box); echo ":"; +echo call_user_func("strval", $box); echo ":"; +echo call_user_func_array("strval", [$box]); echo ":"; +echo $box instanceof Stringable ? "S" : "s"; echo ":"; +echo $box->accepts($box);'); +"#, + ); + assert_eq!( + out, + "box:Ada:box:Ada:prebox:Ada:box:Ada:box:Ada:box:Ada:S:typed:box:Ada" + ); +} + /// Verifies eval-declared objects without `__toString()` throw catchable PHP errors in string contexts. #[test] fn test_eval_declared_object_string_context_without_tostring_throws_error() { @@ -8980,6 +9011,9 @@ fn test_eval_declared_method_by_ref_arguments() { let out = compile_and_run_capture( r#"change($value); EvalByRefMethodBox::changeStatic($value); @@ -9004,7 +9039,7 @@ $items = ["k" => "C"]; $box->change($items["k"]); $prop = new EvalByRefPropertyBox(); $box->change($prop->value); -echo $value . ":" . $named . ":" . $items["k"] . ":" . $prop->value;'); +echo $ctor . ":" . $value . ":" . $named . ":" . $items["k"] . ":" . $prop->value;'); "#, ); assert!( @@ -9014,7 +9049,7 @@ echo $value . ":" . $named . ":" . $items["k"] . ":" . $prop->value;'); ); assert_eq!( out.stdout, - "A-method-static-variadic:B-named:C-method:D-method" + "Z-ctor:A-method-static-variadic:B-named:C-method:D-method" ); } From d7941c6687bcab240490e140ed4ceb0b1849903c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 08:21:00 +0200 Subject: [PATCH 0727/1208] Support eval nullsafe member access --- crates/elephc-magician/src/eval_ir.rs | 9 ++++ .../src/interpreter/expressions.rs | 25 ++++++++++ crates/elephc-magician/src/lexer/scan.rs | 6 ++- crates/elephc-magician/src/lexer/token.rs | 1 + .../elephc-magician/src/parser/expressions.rs | 49 +++++++++++++------ .../elephc-magician/src/parser/statements.rs | 10 ++++ .../src/parser/tests/arrays_objects.rs | 25 ++++++++++ tests/codegen/eval.rs | 36 ++++++++++++++ 8 files changed, 144 insertions(+), 17 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index eabcf57d1d..8b932ea52c 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2307,6 +2307,11 @@ pub enum EvalExpr { method: String, args: Vec, }, + NullsafeMethodCall { + object: Box, + method: String, + args: Vec, + }, Magic(EvalMagicConst), NewObject { class_name: String, @@ -2336,6 +2341,10 @@ pub enum EvalExpr { value: Box, default: Box, }, + NullsafePropertyGet { + object: Box, + property: String, + }, PropertyGet { object: Box, property: String, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index acf74ef0ed..e90f10136f 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -139,6 +139,24 @@ pub(in crate::interpreter) fn eval_expr( values, ) } + EvalExpr::NullsafeMethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return values.null(); + } + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_method_call_result_with_evaluated_args( + object, + method, + evaluated_args, + context, + values, + ) + } EvalExpr::NullCoalesce { value, default } => { let value = eval_expr(value, context, scope, values)?; if values.is_null(value)? { @@ -147,6 +165,13 @@ pub(in crate::interpreter) fn eval_expr( Ok(value) } } + EvalExpr::NullsafePropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return values.null(); + } + eval_property_get_result(object, property, context, values) + } EvalExpr::PropertyGet { object, property } => { let object = eval_expr(object, context, scope, values)?; eval_property_get_result(object, property, context, values) diff --git a/crates/elephc-magician/src/lexer/scan.rs b/crates/elephc-magician/src/lexer/scan.rs index 35af3a220f..f006dc1e59 100644 --- a/crates/elephc-magician/src/lexer/scan.rs +++ b/crates/elephc-magician/src/lexer/scan.rs @@ -245,7 +245,11 @@ impl<'a> Lexer<'a> { } '?' => { self.bump_char(); - if self.peek_char() == Some('?') { + if self.peek_char() == Some('-') && self.peek_next_char() == Some('>') { + self.bump_char(); + self.bump_char(); + Ok(TokenKind::QuestionArrow) + } else if self.peek_char() == Some('?') { self.bump_char(); Ok(TokenKind::QuestionQuestion) } else { diff --git a/crates/elephc-magician/src/lexer/token.rs b/crates/elephc-magician/src/lexer/token.rs index 628ed2a805..739d653ff0 100644 --- a/crates/elephc-magician/src/lexer/token.rs +++ b/crates/elephc-magician/src/lexer/token.rs @@ -94,6 +94,7 @@ pub(crate) enum TokenKind { GreaterGreaterEqual, FatArrow, Question, + QuestionArrow, QuestionQuestion, Semicolon, LParen, diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index f9959b0269..7c6dd8aa42 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -492,28 +492,45 @@ impl Parser { }; continue; } - if self.consume(TokenKind::Arrow) { - let TokenKind::Ident(member) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let member = member.clone(); - self.advance(); - if matches!(self.current(), TokenKind::LParen) { - let args = self.parse_call_args()?; - expr = EvalExpr::MethodCall { + let nullsafe = if self.consume(TokenKind::Arrow) { + false + } else if self.consume(TokenKind::QuestionArrow) { + true + } else { + break; + }; + let TokenKind::Ident(member) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + expr = if nullsafe { + EvalExpr::NullsafeMethodCall { object: Box::new(expr), method: member, args, - }; + } } else { - expr = EvalExpr::PropertyGet { + EvalExpr::MethodCall { object: Box::new(expr), - property: member, - }; - } - continue; + method: member, + args, + } + }; + } else if nullsafe { + expr = EvalExpr::NullsafePropertyGet { + object: Box::new(expr), + property: member, + }; + } else { + expr = EvalExpr::PropertyGet { + object: Box::new(expr), + property: member, + }; } - break; + continue; } Ok(expr) } diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index a576e9c74a..bd820bb48a 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3124,6 +3124,12 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { .iter() .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) } + EvalExpr::NullsafeMethodCall { object, args, .. } => { + eval_expr_uses_this_property(object, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } EvalExpr::NewAnonymousClass { args, .. } => args .iter() .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), @@ -3135,6 +3141,10 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { eval_is_this_property(object, property, property_name) || eval_expr_uses_this_property(object, property_name) } + EvalExpr::NullsafePropertyGet { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } EvalExpr::Ternary { condition, then_branch, diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index 7bec8dc577..7a0efdabb3 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -135,6 +135,31 @@ fn parse_fragment_accepts_method_call_source() { }))] ); } +/// Verifies nullsafe object property reads parse as dedicated postfix EvalIR expressions. +#[test] +fn parse_fragment_accepts_nullsafe_property_read_source() { + let program = parse_fragment(br#"return $this?->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafePropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + ); +} +/// Verifies nullsafe object method calls parse as dedicated postfix EvalIR call expressions. +#[test] +fn parse_fragment_accepts_nullsafe_method_call_source() { + let program = parse_fragment(br#"return $this?->Answer();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "Answer".to_string(), + args: Vec::new(), + }))] + ); +} /// Verifies object construction parses as a named EvalIR expression. #[test] fn parse_fragment_accepts_new_object_source() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0aa3d1f6bf..91a06399aa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3324,6 +3324,42 @@ echo $box->accepts($box);'); ); } +/// Verifies eval-declared objects support nullsafe property reads and method calls. +#[test] +fn test_eval_declared_nullsafe_property_and_method_access() { + let out = compile_and_run( + r#"name . ":" . $value; + } +} + +class EvalNullsafeUser { + public $profile = null; +} + +function eval_nullsafe_side() { + echo "bad"; + return "side"; +} + +$with = new EvalNullsafeUser(); +$with->profile = new EvalNullsafeProfile(); +$without = new EvalNullsafeUser(); + +echo $with->profile?->name ?? "none"; echo "|"; +echo $without->profile?->name ?? "none"; echo "|"; +echo $with?->profile?->label("ok") ?? "none"; echo "|"; +echo $without?->profile?->label(eval_nullsafe_side()) ?? "none";'); +"#, + ); + assert_eq!(out, "Ada|none|method:Ada:ok|none"); +} + /// Verifies eval string contexts dispatch AOT `__toString()` through the runtime method bridge. #[test] fn test_eval_aot_tostring_string_contexts() { From 90eecf9c0c6c4d042fbfc5e6563916f7a7877df9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 08:30:35 +0200 Subject: [PATCH 0728/1208] Support eval dynamic member access --- crates/elephc-magician/src/eval_ir.rs | 18 +++ .../src/interpreter/expressions.rs | 61 ++++++++ .../elephc-magician/src/parser/expressions.rs | 131 +++++++++++++----- .../elephc-magician/src/parser/statements.rs | 37 +++++ .../src/parser/tests/arrays_objects.rs | 50 +++++++ tests/codegen/eval.rs | 53 +++++++ 6 files changed, 319 insertions(+), 31 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 8b932ea52c..d48a0abcac 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2277,6 +2277,15 @@ pub enum EvalExpr { callee: Box, args: Vec, }, + DynamicMethodCall { + object: Box, + method: Box, + args: Vec, + }, + DynamicPropertyGet { + object: Box, + property: Box, + }, Include { path: Box, required: bool, @@ -2312,6 +2321,11 @@ pub enum EvalExpr { method: String, args: Vec, }, + NullsafeDynamicMethodCall { + object: Box, + method: Box, + args: Vec, + }, Magic(EvalMagicConst), NewObject { class_name: String, @@ -2345,6 +2359,10 @@ pub enum EvalExpr { object: Box, property: String, }, + NullsafeDynamicPropertyGet { + object: Box, + property: Box, + }, PropertyGet { object: Box, property: String, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index e90f10136f..037d4c1ac6 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -41,6 +41,27 @@ pub(in crate::interpreter) fn eval_expr( EvalExpr::DynamicCall { callee, args } => { eval_dynamic_call(callee, args, context, scope, values) } + EvalExpr::DynamicMethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_method_call_result_with_evaluated_args( + object, + &method, + evaluated_args, + context, + values, + ) + } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_get_result(object, &property, context, values) + } EvalExpr::Include { path, required, @@ -157,6 +178,25 @@ pub(in crate::interpreter) fn eval_expr( values, ) } + EvalExpr::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return values.null(); + } + let method = eval_dynamic_member_name(method, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_method_call_result_with_evaluated_args( + object, + &method, + evaluated_args, + context, + values, + ) + } EvalExpr::NullCoalesce { value, default } => { let value = eval_expr(value, context, scope, values)?; if values.is_null(value)? { @@ -172,6 +212,14 @@ pub(in crate::interpreter) fn eval_expr( } eval_property_get_result(object, property, context, values) } + EvalExpr::NullsafeDynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return values.null(); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_get_result(object, &property, context, values) + } EvalExpr::PropertyGet { object, property } => { let object = eval_expr(object, context, scope, values)?; eval_property_get_result(object, property, context, values) @@ -276,6 +324,19 @@ pub(in crate::interpreter) fn eval_expr( } } +/// Evaluates a runtime property or method name expression and returns its PHP string bytes as UTF-8. +fn eval_dynamic_member_name( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(expr, context, scope, values)?; + let value = eval_string_context_value(value, context, values)?; + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + /// Reads an array element or dispatches `ArrayAccess::offsetGet()` for objects. pub(in crate::interpreter) fn eval_array_get_result( array: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 7c6dd8aa42..73116fe5d4 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -499,42 +499,111 @@ impl Parser { } else { break; }; - let TokenKind::Ident(member) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let member = member.clone(); - self.advance(); - if matches!(self.current(), TokenKind::LParen) { - let args = self.parse_call_args()?; - expr = if nullsafe { - EvalExpr::NullsafeMethodCall { - object: Box::new(expr), - method: member, - args, - } - } else { - EvalExpr::MethodCall { - object: Box::new(expr), - method: member, - args, - } - }; - } else if nullsafe { - expr = EvalExpr::NullsafePropertyGet { - object: Box::new(expr), - property: member, - }; - } else { - expr = EvalExpr::PropertyGet { - object: Box::new(expr), - property: member, - }; - } + expr = self.parse_object_member_postfix(expr, nullsafe)?; continue; } Ok(expr) } + /// Parses the member name after `->` or `?->` and builds the corresponding postfix expression. + fn parse_object_member_postfix( + &mut self, + object: EvalExpr, + nullsafe: bool, + ) -> Result { + match self.current() { + TokenKind::Ident(member) => { + let member = member.clone(); + self.advance(); + self.parse_named_object_member_postfix(object, member, nullsafe) + } + TokenKind::DollarIdent(name) => { + let member = EvalExpr::LoadVar(name.clone()); + self.advance(); + self.parse_dynamic_object_member_postfix(object, member, nullsafe) + } + TokenKind::LBrace => { + self.advance(); + let member = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + self.parse_dynamic_object_member_postfix(object, member, nullsafe) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Builds a static-name property read or method call after parsing the member name. + fn parse_named_object_member_postfix( + &mut self, + object: EvalExpr, + member: String, + nullsafe: bool, + ) -> Result { + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(if nullsafe { + EvalExpr::NullsafeMethodCall { + object: Box::new(object), + method: member, + args, + } + } else { + EvalExpr::MethodCall { + object: Box::new(object), + method: member, + args, + } + }); + } + Ok(if nullsafe { + EvalExpr::NullsafePropertyGet { + object: Box::new(object), + property: member, + } + } else { + EvalExpr::PropertyGet { + object: Box::new(object), + property: member, + } + }) + } + + /// Builds a runtime-name property read or method call after parsing the member expression. + fn parse_dynamic_object_member_postfix( + &mut self, + object: EvalExpr, + member: EvalExpr, + nullsafe: bool, + ) -> Result { + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(if nullsafe { + EvalExpr::NullsafeDynamicMethodCall { + object: Box::new(object), + method: Box::new(member), + args, + } + } else { + EvalExpr::DynamicMethodCall { + object: Box::new(object), + method: Box::new(member), + args, + } + }); + } + Ok(if nullsafe { + EvalExpr::NullsafeDynamicPropertyGet { + object: Box::new(object), + property: Box::new(member), + } + } else { + EvalExpr::DynamicPropertyGet { + object: Box::new(object), + property: Box::new(member), + } + }) + } + /// Parses primary expressions supported by the initial eval subset. pub(super) fn parse_primary(&mut self) -> Result { match self.current() { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index bd820bb48a..5c0cad83a0 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3081,6 +3081,22 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { .iter() .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) } + EvalExpr::DynamicMethodCall { + object, + method, + args, + } + | EvalExpr::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(method, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } EvalExpr::Const(_) | EvalExpr::ConstFetch(_) | EvalExpr::ClassConstantFetch { .. } @@ -3145,6 +3161,12 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { eval_is_this_property(object, property, property_name) || eval_expr_uses_this_property(object, property_name) } + EvalExpr::DynamicPropertyGet { object, property } + | EvalExpr::NullsafeDynamicPropertyGet { object, property } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } EvalExpr::Ternary { condition, then_branch, @@ -3168,6 +3190,21 @@ fn eval_is_this_property(object: &EvalExpr, property: &str, property_name: &str) matches!(object, EvalExpr::LoadVar(name) if name == "this") && property == property_name } +/// Returns whether one dynamic object/property pair is exactly `$this->{"property"}`. +fn eval_is_this_dynamic_property( + object: &EvalExpr, + property: &EvalExpr, + property_name: &str, +) -> bool { + matches!( + (object, property), + ( + EvalExpr::LoadVar(object_name), + EvalExpr::Const(EvalConst::String(property)) + ) if object_name == "this" && property == property_name + ) +} + /// Returns the synthetic get-hook method name for one property. fn property_hook_get_method(property_name: &str) -> String { format!("__propget_{property_name}") diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index 7a0efdabb3..f7bdaac419 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -135,6 +135,31 @@ fn parse_fragment_accepts_method_call_source() { }))] ); } +/// Verifies braced dynamic object property reads parse as runtime-name EvalIR expressions. +#[test] +fn parse_fragment_accepts_dynamic_property_read_source() { + let program = parse_fragment(br#"return $this->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }))] + ); +} +/// Verifies variable-name dynamic object method calls parse as runtime-name EvalIR calls. +#[test] +fn parse_fragment_accepts_dynamic_method_call_source() { + let program = parse_fragment(br#"return $this->$method();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: Vec::new(), + }))] + ); +} /// Verifies nullsafe object property reads parse as dedicated postfix EvalIR expressions. #[test] fn parse_fragment_accepts_nullsafe_property_read_source() { @@ -160,6 +185,31 @@ fn parse_fragment_accepts_nullsafe_method_call_source() { }))] ); } +/// Verifies nullsafe braced dynamic property reads parse as runtime-name EvalIR expressions. +#[test] +fn parse_fragment_accepts_nullsafe_dynamic_property_read_source() { + let program = parse_fragment(br#"return $this?->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeDynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }))] + ); +} +/// Verifies nullsafe dynamic method calls parse as runtime-name EvalIR call expressions. +#[test] +fn parse_fragment_accepts_nullsafe_dynamic_method_call_source() { + let program = parse_fragment(br#"return $this?->$method();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeDynamicMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: Vec::new(), + }))] + ); +} /// Verifies object construction parses as a named EvalIR expression. #[test] fn parse_fragment_accepts_new_object_source() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 91a06399aa..79da091ea2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3360,6 +3360,59 @@ echo $without?->profile?->label(eval_nullsafe_side()) ?? "none";'); assert_eq!(out, "Ada|none|method:Ada:ok|none"); } +/// Verifies eval-declared objects support runtime-name property reads and method calls. +#[test] +fn test_eval_declared_dynamic_property_and_method_access() { + let out = compile_and_run( + r#"name . ":" . $value; + } +} + +class EvalDynamicMemberUser { + public $profile = null; +} + +function eval_dynamic_member_name() { + echo "name:"; + return "profile"; +} + +function eval_dynamic_member_method() { + echo "methodName:"; + return "label"; +} + +function eval_dynamic_member_bad() { + echo "bad"; + return "profile"; +} + +$with = new EvalDynamicMemberUser(); +$with->profile = new EvalDynamicMemberProfile(); +$missing = null; +$name = "profile"; +$method = "label"; + +echo $with->{$name}->name; echo "|"; +echo $with?->{eval_dynamic_member_name()}?->name ?? "none"; echo "|"; +echo $with->{$name}->$method("ok"); echo "|"; +echo $with->{$name}->{eval_dynamic_member_method()}("yes"); echo "|"; +echo $missing?->{eval_dynamic_member_bad()} ?? "none"; echo "|"; +echo $missing?->{eval_dynamic_member_bad()}(eval_dynamic_member_bad()) ?? "none";'); +"#, + ); + assert_eq!( + out, + "Ada|name:Ada|call:Ada:ok|methodName:call:Ada:yes|none|none" + ); +} + /// Verifies eval string contexts dispatch AOT `__toString()` through the runtime method bridge. #[test] fn test_eval_aot_tostring_string_contexts() { From a02f6a2875c02e26d939e928cfbcae8cf17769b4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 08:38:25 +0200 Subject: [PATCH 0729/1208] Support eval dynamic property writes --- crates/elephc-magician/src/eval_ir.rs | 9 +++ .../src/interpreter/builtins/symbols.rs | 57 +++++++++++++++++++ .../src/interpreter/dynamic_functions.rs | 2 + .../src/interpreter/expressions.rs | 2 +- .../src/interpreter/statements.rs | 17 ++++++ .../elephc-magician/src/parser/statements.rs | 44 +++++++++++--- .../src/parser/tests/arrays_objects.rs | 27 +++++++++ tests/codegen/eval.rs | 45 +++++++++++++++ 8 files changed, 194 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index d48a0abcac..d16a090a34 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -141,6 +141,11 @@ pub enum EvalStmt { property: String, source: String, }, + DynamicPropertySet { + object: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, PropertySet { object: EvalExpr, property: String, @@ -177,6 +182,10 @@ pub enum EvalStmt { object: EvalExpr, property: String, }, + UnsetDynamicProperty { + object: EvalExpr, + property: EvalExpr, + }, UnsetVar { name: String, }, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index b3bdd6c480..baab6be213 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -582,6 +582,11 @@ pub(in crate::interpreter) fn eval_builtin_unset( let object = eval_expr(object, context, scope, values)?; eval_property_unset_result(object, property, context, values)?; } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_unset_result(object, &property, context, values)?; + } _ => return Err(EvalStatus::RuntimeFatal), } } @@ -648,6 +653,38 @@ pub(in crate::interpreter) fn eval_empty_arg( let value = eval_property_get_result(object, property, context, values)?; return Ok(!values.truthy(value)?); } + if let EvalExpr::DynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_property_isset_result(object, &property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::NullsafePropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(true); + } + if !eval_property_isset_result(object, property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(true); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_property_isset_result(object, &property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, &property, context, values)?; + return Ok(!values.truthy(value)?); + } if let EvalExpr::ArrayGet { array, index } = arg { let array = eval_expr(array, context, scope, values)?; let index = eval_expr(index, context, scope, values)?; @@ -678,6 +715,26 @@ pub(in crate::interpreter) fn eval_isset_arg( let object = eval_expr(object, context, scope, values)?; return eval_property_isset_result(object, property, context, values); } + if let EvalExpr::DynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_property_isset_result(object, &property, context, values); + } + if let EvalExpr::NullsafePropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(false); + } + return eval_property_isset_result(object, property, context, values); + } + if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(false); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_property_isset_result(object, &property, context, values); + } if let EvalExpr::ArrayGet { array, index } = arg { let array = eval_expr(array, context, scope, values)?; let index = eval_expr(index, context, scope, values)?; diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 26dacf454d..cf4a9fbf59 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1129,6 +1129,7 @@ fn visit_static_var_declarations( | EvalStmt::Expr(_) | EvalStmt::Global { .. } | EvalStmt::InterfaceDecl(_) + | EvalStmt::DynamicPropertySet { .. } | EvalStmt::PropertyReferenceBind { .. } | EvalStmt::PropertySet { .. } | EvalStmt::ReferenceAssign { .. } @@ -1138,6 +1139,7 @@ fn visit_static_var_declarations( | EvalStmt::Throw(_) | EvalStmt::TraitDecl(_) | EvalStmt::UnsetArrayElement { .. } + | EvalStmt::UnsetDynamicProperty { .. } | EvalStmt::UnsetProperty { .. } | EvalStmt::UnsetVar { .. } => {} } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 037d4c1ac6..131059612a 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -325,7 +325,7 @@ pub(in crate::interpreter) fn eval_expr( } /// Evaluates a runtime property or method name expression and returns its PHP string bytes as UTF-8. -fn eval_dynamic_member_name( +pub(in crate::interpreter) fn eval_dynamic_member_name( expr: &EvalExpr, context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 51317a219b..737d5eecf1 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -166,6 +166,17 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_reference_bind_result(object, property, source, context, scope, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicPropertySet { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_property_set_result(object, &property, value, context, values)?; + Ok(EvalControl::None) + } EvalStmt::StaticVar { name, init } => { execute_static_var_stmt(name, init, context, scope, values)?; Ok(EvalControl::None) @@ -226,6 +237,12 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_unset_result(object, property, context, values)?; Ok(EvalControl::None) } + EvalStmt::UnsetDynamicProperty { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_unset_result(object, &property, context, values)?; + Ok(EvalControl::None) + } EvalStmt::UnsetVar { name } => { if let Some(replaced) = unset_scope_cell(scope, name.clone()) { eval_release_value(context, values, replaced)?; diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 5c0cad83a0..f4abfa496d 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2563,18 +2563,25 @@ impl Parser { } return Ok(vec![EvalStmt::Expr(target)]); } - let EvalExpr::PropertyGet { object, property } = target else { - return Err(EvalParseError::UnexpectedToken); - }; let value = self.parse_expr()?; if require_semicolon { self.expect_semicolon()?; } - Ok(vec![EvalStmt::PropertySet { - object: *object, - property, - value, - }]) + match target { + EvalExpr::PropertyGet { object, property } => Ok(vec![EvalStmt::PropertySet { + object: *object, + property, + value, + }]), + EvalExpr::DynamicPropertyGet { object, property } => { + Ok(vec![EvalStmt::DynamicPropertySet { + object: *object, + property: *property, + value, + }]) + } + _ => Err(EvalParseError::UnexpectedToken), + } } /// Parses a complete `if` statement after consuming the `if` keyword. @@ -2680,6 +2687,12 @@ impl Parser { object: *object, property, }, + EvalExpr::DynamicPropertyGet { object, property } => { + EvalStmt::UnsetDynamicProperty { + object: *object, + property: *property, + } + } _ => return Err(EvalParseError::ExpectedVariable), }; statements.push(stmt); @@ -3013,6 +3026,21 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { eval_is_this_property(object, property, property_name) || eval_expr_uses_this_property(object, property_name) } + EvalStmt::UnsetDynamicProperty { object, property } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::DynamicPropertySet { + object, + property, + value, + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(value, property_name) + } EvalStmt::PropertySet { object, property, diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index f7bdaac419..96446a1a58 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -354,3 +354,30 @@ fn parse_fragment_accepts_property_write_source() { }] ); } + +/// Verifies dynamic object property writes parse as runtime-name EvalIR statements. +#[test] +fn parse_fragment_accepts_dynamic_property_write_source() { + let program = parse_fragment(br#"$this->{$name} = 7;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicPropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + value: EvalExpr::Const(EvalConst::Int(7)), + }] + ); +} + +/// Verifies dynamic object property unsets parse as runtime-name EvalIR statements. +#[test] +fn parse_fragment_accepts_dynamic_property_unset_source() { + let program = parse_fragment(br#"unset($this->{$name});"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::UnsetDynamicProperty { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + }] + ); +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 79da091ea2..0ac1423be9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3413,6 +3413,51 @@ echo $missing?->{eval_dynamic_member_bad()}(eval_dynamic_member_bad()) ?? "none" ); } +/// Verifies eval-declared objects support runtime-name property writes and probes. +#[test] +fn test_eval_declared_dynamic_property_write_unset_isset_empty() { + let out = compile_and_run( + r#"{eval_dynamic_write_name()} = eval_dynamic_write_value(); +echo $box->name; echo "|"; +echo isset($box->{$name}) ? "set" : "bad"; echo "|"; +echo empty($box->{$blank}) ? "empty" : "bad"; echo "|"; +unset($box->{$name}); +echo isset($box->{$name}) ? "bad" : "unset"; echo "|"; +$missing = null; +echo isset($missing?->{eval_dynamic_write_bad()}) ? "bad" : "nullset"; echo "|"; +echo empty($missing?->{eval_dynamic_write_bad()}) ? "nullempty" : "bad";'); +"#, + ); + assert_eq!( + out, + "name:value:Ada|set|empty|unset|nullset|nullempty" + ); +} + /// Verifies eval string contexts dispatch AOT `__toString()` through the runtime method bridge. #[test] fn test_eval_aot_tostring_string_contexts() { From f0123408cb2853e419b1feb5ff1472fea4179d9e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 08:47:30 +0200 Subject: [PATCH 0730/1208] Support eval dynamic object construction --- crates/elephc-magician/src/eval_ir.rs | 4 ++ .../src/interpreter/expressions.rs | 71 +++++++++++++------ .../elephc-magician/src/parser/expressions.rs | 9 +++ .../elephc-magician/src/parser/statements.rs | 6 ++ .../src/parser/tests/arrays_objects.rs | 15 ++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 35 +++++++++ 7 files changed, 120 insertions(+), 22 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index d16a090a34..78e1286075 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2291,6 +2291,10 @@ pub enum EvalExpr { method: Box, args: Vec, }, + DynamicNewObject { + class_name: Box, + args: Vec, + }, DynamicPropertyGet { object: Box, property: Box, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 131059612a..e2499988e8 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -57,6 +57,12 @@ pub(in crate::interpreter) fn eval_expr( values, ) } + EvalExpr::DynamicNewObject { class_name, args } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_new_object_class_name(class_name, context, values)?; + let args = eval_method_call_arg_values(args, context, scope, values)?; + eval_new_object_result(&class_name, args, context, scope, values) + } EvalExpr::DynamicPropertyGet { object, property } => { let object = eval_expr(object, context, scope, values)?; let property = eval_dynamic_member_name(property, context, scope, values)?; @@ -95,27 +101,7 @@ pub(in crate::interpreter) fn eval_expr( EvalExpr::NewObject { class_name, args } => { let args = eval_method_call_arg_values(args, context, scope, values)?; let class_name = eval_new_object_class_name(class_name, context)?; - if let Some(object) = - eval_reflection_owner_new_object(&class_name, args.clone(), context, values)? - { - return Ok(object); - } - if let Some(class) = context.class(&class_name).cloned() { - eval_dynamic_class_new_object(&class, args, context, scope, values) - } else { - let object = values.new_object(&class_name)?; - if let Err(err) = eval_native_constructor_with_evaluated_args( - &class_name, - object, - args, - context, - values, - ) { - let _ = values.release(object); - return Err(err); - } - Ok(object) - } + eval_new_object_result(&class_name, args, context, scope, values) } EvalExpr::NewAnonymousClass { class, args } => { ensure_eval_anonymous_class_decl(class, context, scope, values)?; @@ -383,6 +369,32 @@ fn eval_cast_expr( } } +/// Constructs an object after the target class name and constructor arguments have been evaluated. +fn eval_new_object_result( + class_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(object) = + eval_reflection_owner_new_object(class_name, args.clone(), context, values)? + { + return Ok(object); + } + if let Some(class) = context.class(class_name).cloned() { + return eval_dynamic_class_new_object(&class, args, context, scope, values); + } + let object = values.new_object(class_name)?; + if let Err(err) = + eval_native_constructor_with_evaluated_args(class_name, object, args, context, values) + { + let _ = values.release(object); + return Err(err); + } + Ok(object) +} + /// Resolves special class names used by `new` while preserving AOT fallback names. fn eval_new_object_class_name( class_name: &str, @@ -396,6 +408,23 @@ fn eval_new_object_class_name( } } +/// Resolves a runtime class-name value used by `new $class(...)`. +fn eval_dynamic_new_object_class_name( + class_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(class_name)? { + EVAL_TAG_STRING => { + let bytes = values.string_bytes(class_name)?; + let class_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + eval_new_object_class_name(&class_name, context) + } + EVAL_TAG_OBJECT => eval_instanceof_object_target_name(class_name, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Evaluates PHP's `instanceof` operator over static and dynamic class targets. fn eval_instanceof_expr( value: &EvalExpr, diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 73116fe5d4..ea0ca832c1 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -844,6 +844,15 @@ impl Parser { if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { return self.parse_anonymous_class_expr(is_readonly_anonymous); } + if let TokenKind::DollarIdent(name) = self.current() { + let class_name = EvalExpr::LoadVar(name.clone()); + self.advance(); + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicNewObject { + class_name: Box::new(class_name), + args, + }); + } let class_name = self.parse_qualified_name()?; let class_name = self.resolve_class_name(class_name); let args = self.parse_call_args()?; diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index f4abfa496d..5a7e530453 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3109,6 +3109,12 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { .iter() .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) } + EvalExpr::DynamicNewObject { class_name, args } => { + eval_expr_uses_this_property(class_name, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } EvalExpr::DynamicMethodCall { object, method, diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index 96446a1a58..b53e97dca2 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -222,6 +222,21 @@ fn parse_fragment_accepts_new_object_source() { }))] ); } +/// Verifies object construction accepts a runtime class-name variable. +#[test] +fn parse_fragment_accepts_dynamic_new_object_source() { + let program = + parse_fragment(br#"return new $className("Ada");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::LoadVar("className".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string() + )))], + }))] + ); +} /// Verifies object construction accepts explicitly qualified class names. #[test] fn parse_fragment_accepts_qualified_new_object_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 47ffeae6ac..901de2580f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` for eval-declared classes, including constructor named arguments and unpacking; `new self()`, `new static()`, and `new parent()` inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions; `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | +| Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0ac1423be9..21dd5a1821 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6652,6 +6652,41 @@ echo eval('$box = new EvalDynamicNewNamedCtor(right: "F", left: "E"); return $bo assert_eq!(out, "EF"); } +/// Verifies eval object construction accepts runtime class-name variables. +#[test] +fn test_eval_dynamic_new_accepts_runtime_class_name() { + let out = compile_and_run( + r#"label = "aot:" . $label; + } +} + +eval('class EvalRuntimeNewTarget { + public string $label; + public function __construct(string $label) { + $this->label = "eval:" . $label; + } +} + +$evalClass = "EvalRuntimeNewTarget"; +$evalBox = new $evalClass("Ada"); +echo $evalBox->label; echo "|"; + +$aotClass = "EvalAotRuntimeNewTarget"; +$aotBox = new $aotClass("Turing"); +echo $aotBox->label; echo "|"; + +$prototype = new EvalRuntimeNewTarget("proto"); +$copy = new $prototype("Grace"); +echo $copy->label;'); +"#, + ); + assert_eq!(out, "eval:Ada|aot:Turing|eval:Grace"); +} + /// Verifies eval object construction passes object-typed arguments to AOT constructors. #[test] fn test_eval_dynamic_new_passes_object_arg_to_constructor() { From 029dc2dc4253c0fe3d61dfd3ed203d546ce43413 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 09:02:19 +0200 Subject: [PATCH 0731/1208] Support eval dynamic static receivers --- crates/elephc-magician/src/eval_ir.rs | 16 ++++ .../src/interpreter/expressions.rs | 76 ++++++++++++++++++- .../elephc-magician/src/parser/expressions.rs | 59 +++++++++++++- .../elephc-magician/src/parser/statements.rs | 16 ++++ .../src/parser/tests/static_members.rs | 60 +++++++++++++++ docs/php/eval.md | 6 +- tests/codegen/eval.rs | 45 +++++++++++ 7 files changed, 270 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 78e1286075..84b6054687 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2299,6 +2299,22 @@ pub enum EvalExpr { object: Box, property: Box, }, + DynamicStaticMethodCall { + class_name: Box, + method: Box, + args: Vec, + }, + DynamicStaticPropertyGet { + class_name: Box, + property: String, + }, + DynamicClassConstantFetch { + class_name: Box, + constant: String, + }, + DynamicClassNameFetch { + class_name: Box, + }, Include { path: Box, required: bool, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index e2499988e8..433e483908 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -59,7 +59,7 @@ pub(in crate::interpreter) fn eval_expr( } EvalExpr::DynamicNewObject { class_name, args } => { let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_new_object_class_name(class_name, context, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; let args = eval_method_call_arg_values(args, context, scope, values)?; eval_new_object_result(&class_name, args, context, scope, values) } @@ -68,6 +68,37 @@ pub(in crate::interpreter) fn eval_expr( let property = eval_dynamic_member_name(property, context, scope, values)?; eval_property_get_result(object, &property, context, values) } + EvalExpr::DynamicStaticMethodCall { + class_name, + method, + args, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; + eval_static_method_call_result(&class_name, &method, evaluated_args, context, values) + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_get_result(&class_name, property, context, values) + } + EvalExpr::DynamicClassConstantFetch { + class_name, + constant, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_class_constant_fetch_result(&class_name, constant, context, values) + } + EvalExpr::DynamicClassNameFetch { class_name } => { + let class_name = eval_expr(class_name, context, scope, values)?; + eval_dynamic_class_name_fetch_result(class_name, context, values) + } EvalExpr::Include { path, required, @@ -408,8 +439,8 @@ fn eval_new_object_class_name( } } -/// Resolves a runtime class-name value used by `new $class(...)`. -fn eval_dynamic_new_object_class_name( +/// Resolves a runtime class-name value used by dynamic class operations. +fn eval_dynamic_class_name( class_name: RuntimeCellHandle, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -418,13 +449,50 @@ fn eval_dynamic_new_object_class_name( EVAL_TAG_STRING => { let bytes = values.string_bytes(class_name)?; let class_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - eval_new_object_class_name(&class_name, context) + Ok(context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())) } EVAL_TAG_OBJECT => eval_instanceof_object_target_name(class_name, context, values), _ => Err(EvalStatus::RuntimeFatal), } } +/// Returns the runtime class name for `$object::class` and rejects non-object dynamic receivers. +fn eval_dynamic_class_name_fetch_result( + class_name: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(class_name)?; + if tag == EVAL_TAG_OBJECT { + let class_name = eval_instanceof_object_target_name(class_name, context, values)?; + return values.string(&class_name); + } + eval_throw_type_error( + &format!( + "Cannot use \"::class\" on {}", + eval_class_name_fetch_type_error_name(tag) + ), + context, + values, + ) +} + +/// Returns PHP's type label for dynamic `::class` TypeError diagnostics. +fn eval_class_name_fetch_type_error_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "int", + EVAL_TAG_FLOAT => "float", + EVAL_TAG_STRING => "string", + EVAL_TAG_BOOL => "bool", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_NULL => "null", + _ => "null", + } +} + /// Evaluates PHP's `instanceof` operator over static and dynamic class targets. fn eval_instanceof_expr( value: &EvalExpr, diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index ea0ca832c1..7f82386f29 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -625,7 +625,12 @@ impl Parser { TokenKind::DollarIdent(name) => { let name = name.clone(); self.advance(); - Ok(EvalExpr::LoadVar(name)) + let expr = EvalExpr::LoadVar(name); + if self.consume(TokenKind::DoubleColon) { + self.parse_dynamic_static_member_expr(expr) + } else { + Ok(expr) + } } TokenKind::Magic(EvalMagicConst::Namespace) => { let namespace = self.namespace.clone(); @@ -833,6 +838,58 @@ impl Parser { } } + /// Parses `$class::member` expressions whose static receiver is runtime-valued. + fn parse_dynamic_static_member_expr( + &mut self, + class_name: EvalExpr, + ) -> Result { + match self.current() { + TokenKind::DollarIdent(member) => { + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(EvalExpr::LoadVar(member)), + args, + }); + } + Ok(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name), + property: member, + }) + } + TokenKind::Ident(name) + if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => + { + self.advance(); + Ok(EvalExpr::DynamicClassNameFetch { + class_name: Box::new(class_name), + }) + } + TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { + let method = method.clone(); + self.advance(); + let args = self.parse_call_args()?; + Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(EvalExpr::Const(EvalConst::String(method))), + args, + }) + } + TokenKind::Ident(constant) => { + let constant = constant.clone(); + self.advance(); + Ok(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(class_name), + constant, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + /// Parses `new ClassName(...)` and anonymous `new class {}` expressions in eval fragments. pub(super) fn parse_new_object_expr(&mut self) -> Result { self.advance(); diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 5a7e530453..a72d19cca4 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3115,6 +3115,22 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { .iter() .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) } + EvalExpr::DynamicStaticMethodCall { + class_name, + method, + args, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(method, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::DynamicStaticPropertyGet { class_name, .. } + | EvalExpr::DynamicClassConstantFetch { class_name, .. } + | EvalExpr::DynamicClassNameFetch { class_name } => { + eval_expr_uses_this_property(class_name, property_name) + } EvalExpr::DynamicMethodCall { object, method, diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 6fed5633ba..9b8de2c3c7 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -82,6 +82,66 @@ fn parse_fragment_accepts_named_static_method_call_expression() { ); } +/// Verifies runtime-valued static receivers lower to dynamic static method calls. +#[test] +fn parse_fragment_accepts_dynamic_static_method_receiver() { + let program = + parse_fragment(br#"return $class::Read(step: 2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + args: vec![EvalCallArg::named( + "step", + EvalExpr::Const(EvalConst::Int(2)), + )], + }))] + ); +} + +/// Verifies runtime-valued static receivers support variable method names. +#[test] +fn parse_fragment_accepts_dynamic_static_method_name() { + let program = + parse_fragment(br#"return $class::$method(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + +/// Verifies runtime-valued static receivers support properties, constants, and `::class`. +#[test] +fn parse_fragment_accepts_dynamic_static_metadata_receiver() { + let program = parse_fragment(br#"return $class::$count . $class::VALUE . $object::class;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + constant: "VALUE".to_string(), + }), + }), + right: Box::new(EvalExpr::DynamicClassNameFetch { + class_name: Box::new(EvalExpr::LoadVar("object".to_string())), + }), + }))] + ); +} + /// Verifies static property compound assignments lower to one read-modify-write statement. #[test] fn parse_fragment_accepts_static_property_compound_assignment() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 901de2580f..0223b11b95 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,15 +71,15 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()` and `$class::$method()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | -| Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, and bare constant fetches are supported. | +| Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, and `$object::class` are supported. | | Ternaries | Full ternary and short ternary (`?:`). | | Match | Strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default`. A miss without `default` is reported as an eval runtime fatal. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 21dd5a1821..a468632679 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9248,6 +9248,51 @@ echo $named(right: "H", left: "G");'); assert_eq!(out.stdout, "AB:CD:EF:GH"); } +/// Verifies eval dynamic static receivers dispatch methods, properties, constants, and `::class`. +#[test] +fn test_eval_dynamic_static_receivers() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 09:09:58 +0200 Subject: [PATCH 0732/1208] Support eval dynamic static property writes --- crates/elephc-magician/src/eval_ir.rs | 5 ++ .../src/interpreter/dynamic_functions.rs | 1 + .../src/interpreter/expressions.rs | 2 +- .../src/interpreter/statements.rs | 11 ++++ .../elephc-magician/src/parser/statements.rs | 62 +++++++++++++++++++ .../src/parser/tests/static_members.rs | 35 +++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 35 +++++++++++ 8 files changed, 151 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 84b6054687..4e4f42ed8e 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -156,6 +156,11 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + DynamicStaticPropertySet { + class_name: EvalExpr, + property: String, + value: EvalExpr, + }, StaticVar { name: String, init: EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index cf4a9fbf59..0cd5234403 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1130,6 +1130,7 @@ fn visit_static_var_declarations( | EvalStmt::Global { .. } | EvalStmt::InterfaceDecl(_) | EvalStmt::DynamicPropertySet { .. } + | EvalStmt::DynamicStaticPropertySet { .. } | EvalStmt::PropertyReferenceBind { .. } | EvalStmt::PropertySet { .. } | EvalStmt::ReferenceAssign { .. } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 433e483908..572dc3ade5 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -440,7 +440,7 @@ fn eval_new_object_class_name( } /// Resolves a runtime class-name value used by dynamic class operations. -fn eval_dynamic_class_name( +pub(in crate::interpreter) fn eval_dynamic_class_name( class_name: RuntimeCellHandle, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 737d5eecf1..6ef0126340 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -200,6 +200,17 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_set_result(class_name, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(&class_name, property, value, context, values)?; + Ok(EvalControl::None) + } EvalStmt::StoreVar { name, value } => { let value = eval_expr(value, context, scope, values)?; for replaced in set_scope_cell( diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index a72d19cca4..9e66e37c67 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -137,6 +137,9 @@ impl Parser { { self.parse_static_property_set_stmt(true) } + TokenKind::DollarIdent(_) if self.current_starts_dynamic_static_property_assignment() => { + self.parse_dynamic_static_property_set_stmt(true) + } TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(true) @@ -273,6 +276,17 @@ impl Parser { .is_some_and(|token| assignment_op(token).is_some()) } + /// Returns true when the current tokens form `$class::$property `. + pub(super) fn current_starts_dynamic_static_property_assignment(&self) -> bool { + matches!(self.current(), TokenKind::DollarIdent(_)) + && matches!(self.peek(), TokenKind::DoubleColon) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::DollarIdent(_))) + && self + .tokens + .get(self.pos + 3) + .is_some_and(|token| assignment_op(token).is_some()) + } + /// Parses `do { ... } while (expr);`. pub(super) fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -2518,6 +2532,48 @@ impl Parser { }]) } + /// Parses `$class::$property = expr` and compound assignments with a dynamic receiver. + pub(super) fn parse_dynamic_static_property_set_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let TokenKind::DollarIdent(class_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let class_name = EvalExpr::LoadVar(class_name.clone()); + self.advance(); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name.clone()), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + }]) + } + /// Parses prefix `++$name` and `--$name` as simple statement effects. pub(super) fn parse_prefix_inc_dec_stmt( &mut self, @@ -3050,6 +3106,12 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { || eval_expr_uses_this_property(object, property_name) || eval_expr_uses_this_property(value, property_name) } + EvalStmt::DynamicStaticPropertySet { + class_name, value, .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(value, property_name) + } EvalStmt::StaticPropertySet { value, .. } | EvalStmt::StoreVar { value, .. } => { eval_expr_uses_this_property(value, property_name) } diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 9b8de2c3c7..f090fd6898 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -142,6 +142,41 @@ fn parse_fragment_accepts_dynamic_static_metadata_receiver() { ); } +/// Verifies runtime-valued static receivers support property writes. +#[test] +fn parse_fragment_accepts_dynamic_static_property_assignment() { + let program = parse_fragment(br#"$class::$count = 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicStaticPropertySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }] + ); +} + +/// Verifies dynamic static property compound assignments lower to read-modify-write EvalIR. +#[test] +fn parse_fragment_accepts_dynamic_static_property_compound_assignment() { + let program = parse_fragment(br#"$class::$count += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicStaticPropertySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }] + ); +} + /// Verifies static property compound assignments lower to one read-modify-write statement. #[test] fn parse_fragment_accepts_static_property_compound_assignment() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 0223b11b95..240d54a00c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a468632679..1a957f79fc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9293,6 +9293,41 @@ echo $aotClass::KIND;'); ); } +/// Verifies eval dynamic static receivers write eval-declared and AOT static properties. +#[test] +fn test_eval_dynamic_static_receiver_property_writes() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 09:16:04 +0200 Subject: [PATCH 0733/1208] Support eval static property probes --- .../src/interpreter/builtins/symbols.rs | 40 +++++++++++++++ .../src/interpreter/statements.rs | 49 +++++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 43 ++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index baab6be213..10632654f1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -685,6 +685,30 @@ pub(in crate::interpreter) fn eval_empty_arg( let value = eval_property_get_result(object, &property, context, values)?; return Ok(!values.truthy(value)?); } + if let EvalExpr::StaticPropertyGet { + class_name, + property, + } = arg + { + if !eval_static_property_isset_result(class_name, property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(class_name, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + if !eval_static_property_isset_result(&class_name, property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(&class_name, property, context, values)?; + return Ok(!values.truthy(value)?); + } if let EvalExpr::ArrayGet { array, index } = arg { let array = eval_expr(array, context, scope, values)?; let index = eval_expr(index, context, scope, values)?; @@ -735,6 +759,22 @@ pub(in crate::interpreter) fn eval_isset_arg( let property = eval_dynamic_member_name(property, context, scope, values)?; return eval_property_isset_result(object, &property, context, values); } + if let EvalExpr::StaticPropertyGet { + class_name, + property, + } = arg + { + return eval_static_property_isset_result(class_name, property, context, values); + } + if let EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + return eval_static_property_isset_result(&class_name, property, context, values); + } if let EvalExpr::ArrayGet { array, index } = arg { let array = eval_expr(array, context, scope, values)?; let index = eval_expr(index, context, scope, values)?; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 6ef0126340..7943392de5 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4054,6 +4054,55 @@ pub(in crate::interpreter) fn eval_static_property_get_result( } } +/// Returns whether a static property is PHP-`isset()` without throwing for missing properties. +pub(in crate::interpreter) fn eval_static_property_isset_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + return Ok(false); + } + let Some(value) = context.static_property(&declaring_class, property.name()) else { + return Ok(false); + }; + return Ok(!values.is_null(value)?); + } + if eval_static_member_context_owns_class(&class_name, context) { + return Ok(false); + } + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if !is_static { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return Ok(false); + } + if !values.static_property_is_initialized(&declaring_class, property_name)? { + return Ok(false); + } + } else if !eval_runtime_class_like_exists(&class_name, context, values)? { + return eval_throw_class_not_found_error(&class_name, context, values); + } + if let Some(value) = values.static_property_get(&class_name, property_name)? { + return Ok(!values.is_null(value)?); + } + Ok(false) +} + /// Reads one eval-declared class constant after resolving the class-like receiver. pub(in crate::interpreter) fn eval_class_constant_fetch_result( class_name: &str, diff --git a/docs/php/eval.md b/docs/php/eval.md index 240d54a00c..64a435e548 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes and `isset()` / `empty()` probes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1a957f79fc..43006ecff1 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9328,6 +9328,49 @@ echo $aotClass::$count;'); assert_eq!(out.stdout, "set:tail|5|AB|15"); } +/// Verifies eval `isset()` and `empty()` probe static properties without normal read fatals. +#[test] +fn test_eval_static_property_isset_empty_probes() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 09:23:34 +0200 Subject: [PATCH 0734/1208] Handle eval static property unset errors --- crates/elephc-magician/src/eval_ir.rs | 8 +++ .../src/interpreter/dynamic_functions.rs | 2 + .../src/interpreter/statements.rs | 38 ++++++++++++++ .../elephc-magician/src/parser/statements.rs | 18 +++++++ .../src/parser/tests/static_members.rs | 27 ++++++++++ docs/php/eval.md | 2 + tests/codegen/eval.rs | 49 +++++++++++++++++++ 7 files changed, 144 insertions(+) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 4e4f42ed8e..75cc2c2b4e 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -191,6 +191,14 @@ pub enum EvalStmt { object: EvalExpr, property: EvalExpr, }, + UnsetStaticProperty { + class_name: String, + property: String, + }, + UnsetDynamicStaticProperty { + class_name: EvalExpr, + property: String, + }, UnsetVar { name: String, }, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 0cd5234403..ab1a04af8d 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1141,7 +1141,9 @@ fn visit_static_var_declarations( | EvalStmt::TraitDecl(_) | EvalStmt::UnsetArrayElement { .. } | EvalStmt::UnsetDynamicProperty { .. } + | EvalStmt::UnsetDynamicStaticProperty { .. } | EvalStmt::UnsetProperty { .. } + | EvalStmt::UnsetStaticProperty { .. } | EvalStmt::UnsetVar { .. } => {} } } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 7943392de5..b9561847ae 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -254,6 +254,22 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_unset_result(object, &property, context, values)?; Ok(EvalControl::None) } + EvalStmt::UnsetStaticProperty { + class_name, + property, + } => { + eval_static_property_unset_result(class_name, property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetDynamicStaticProperty { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_unset_result(&class_name, property, context, values)?; + Ok(EvalControl::None) + } EvalStmt::UnsetVar { name } => { if let Some(replaced) = unset_scope_cell(scope, name.clone()) { eval_release_value(context, values, replaced)?; @@ -4103,6 +4119,28 @@ pub(in crate::interpreter) fn eval_static_property_isset_result( Ok(false) } +/// Throws PHP's catchable error for attempts to unset an existing static property target. +pub(in crate::interpreter) fn eval_static_property_unset_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if !eval_runtime_class_like_exists(&class_name, context, values)? { + return eval_throw_class_not_found_error(&class_name, context, values); + } + eval_throw_error( + &format!( + "Attempt to unset static property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + /// Reads one eval-declared class constant after resolving the class-like receiver. pub(in crate::interpreter) fn eval_class_constant_fetch_result( class_name: &str, diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 9e66e37c67..4914bfb951 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2749,6 +2749,20 @@ impl Parser { property: *property, } } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => EvalStmt::UnsetStaticProperty { + class_name, + property, + }, + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => EvalStmt::UnsetDynamicStaticProperty { + class_name: *class_name, + property, + }, _ => return Err(EvalParseError::ExpectedVariable), }; statements.push(stmt); @@ -3087,6 +3101,10 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { || eval_expr_uses_this_property(object, property_name) || eval_expr_uses_this_property(property, property_name) } + EvalStmt::UnsetDynamicStaticProperty { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalStmt::UnsetStaticProperty { .. } => false, EvalStmt::DynamicPropertySet { object, property, diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index f090fd6898..862633e250 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -177,6 +177,33 @@ fn parse_fragment_accepts_dynamic_static_property_compound_assignment() { ); } +/// Verifies static property unsets parse as explicit static-unset statements. +#[test] +fn parse_fragment_accepts_static_property_unset() { + let program = + parse_fragment(br#"unset(EvalStaticBox::$count);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::UnsetStaticProperty { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + }] + ); +} + +/// Verifies runtime-valued static property unsets preserve the class receiver expression. +#[test] +fn parse_fragment_accepts_dynamic_static_property_unset() { + let program = parse_fragment(br#"unset($class::$count);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::UnsetDynamicStaticProperty { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + }] + ); +} + /// Verifies static property compound assignments lower to one read-modify-write statement. #[test] fn parse_fragment_accepts_static_property_compound_assignment() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 64a435e548..31ab2bd0bb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -562,6 +562,8 @@ PHP's global `#[AllowDynamicProperties]` marker is rejected on eval-declared readonly classes. `self::`, `parent::`, and late-bound `static::` work for supported static members, class constants, and class-name literals. +Attempts to `unset()` static properties parse normally and throw PHP's +catchable `Error`, including runtime-valued static receivers. Eval object construction can allocate eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime class metadata. Missing class names diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 43006ecff1..41e8319452 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9371,6 +9371,55 @@ echo empty($aotClass::$missing) ? "aot-missing-empty" : "bad";'); ); } +/// Verifies eval static property unsets throw PHP's catchable Error. +#[test] +fn test_eval_static_property_unset_throws_error() { + let out = compile_and_run_capture( + r#"getMessage(); +} +echo "|"; +$class = "EvalDynamicStaticUnset"; +try { + unset($class::$value); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +$aot = "EvalAotStaticUnset"; +try { + unset($aot::$value); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + unset(EvalMissingStaticUnset::$value); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalAotStaticUnset::$value|Error:Class \"EvalMissingStaticUnset\" not found" + ); +} + /// Verifies eval invokable objects dispatch through variable and callback call paths. #[test] fn test_eval_declared_invokable_object_dynamic_callables() { From 3567464a54173e8fcb4b331a209802f91ac2bc21 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 09:32:30 +0200 Subject: [PATCH 0735/1208] Support eval static property inc dec --- .../elephc-magician/src/parser/statements.rs | 217 +++++++++++++++++- .../src/parser/tests/static_members.rs | 73 ++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 40 ++++ 4 files changed, 327 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 4914bfb951..08b03a79e2 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -12,8 +12,8 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalCallArg, EvalCatch, EvalClass, - EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, + EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, + EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInstanceOfTarget, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, EvalParameterTypeVariant, EvalSourceLocation, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, @@ -137,10 +137,32 @@ impl Parser { { self.parse_static_property_set_stmt(true) } + TokenKind::Ident(_) | TokenKind::Backslash + if self.current_starts_static_property_postfix_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(false, true) + } TokenKind::DollarIdent(_) if self.current_starts_dynamic_static_property_assignment() => { self.parse_dynamic_static_property_set_stmt(true) } - TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(true), + TokenKind::DollarIdent(_) + if self.current_starts_dynamic_static_property_postfix_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(false, true) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_static_property_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(true, true) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_dynamic_static_property_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(true, true) + } + TokenKind::PlusPlus | TokenKind::MinusMinus => { + self.parse_prefix_inc_dec_stmt(true) + } TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(true) } @@ -287,6 +309,67 @@ impl Parser { .is_some_and(|token| assignment_op(token).is_some()) } + /// Returns true when the current tokens form `Class::$property++` or `Class::$property--`. + pub(super) fn current_starts_static_property_postfix_inc_dec(&self) -> bool { + self.static_property_tokens_end(self.pos).is_some_and(|pos| { + matches!( + self.tokens.get(pos), + Some(TokenKind::PlusPlus | TokenKind::MinusMinus) + ) + }) + } + + /// Returns true when the current tokens form `$class::$property++` or `$class::$property--`. + pub(super) fn current_starts_dynamic_static_property_postfix_inc_dec(&self) -> bool { + matches!(self.current(), TokenKind::DollarIdent(_)) + && matches!(self.peek(), TokenKind::DoubleColon) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::DollarIdent(_))) + && matches!( + self.tokens.get(self.pos + 3), + Some(TokenKind::PlusPlus | TokenKind::MinusMinus) + ) + } + + /// Returns true when the current tokens form `++Class::$property` or `--Class::$property`. + pub(super) fn current_starts_prefixed_static_property_inc_dec(&self) -> bool { + matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) + && self.static_property_tokens_end(self.pos + 1).is_some() + } + + /// Returns true when the current tokens form `++$class::$property` or `--$class::$property`. + pub(super) fn current_starts_prefixed_dynamic_static_property_inc_dec(&self) -> bool { + matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::DollarIdent(_))) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::DoubleColon)) + && matches!(self.tokens.get(self.pos + 3), Some(TokenKind::DollarIdent(_))) + } + + /// Returns the token position after `Class::$property` when present at `pos`. + fn static_property_tokens_end(&self, mut pos: usize) -> Option { + if matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { + pos += 1; + } + if !matches!(self.tokens.get(pos), Some(TokenKind::Ident(_))) { + return None; + } + pos += 1; + while matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { + pos += 1; + if !matches!(self.tokens.get(pos), Some(TokenKind::Ident(_))) { + return None; + } + pos += 1; + } + if !matches!(self.tokens.get(pos), Some(TokenKind::DoubleColon)) { + return None; + } + pos += 1; + if !matches!(self.tokens.get(pos), Some(TokenKind::DollarIdent(_))) { + return None; + } + Some(pos + 1) + } + /// Parses `do { ... } while (expr);`. pub(super) fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -2417,10 +2500,32 @@ impl Parser { /// Parses one statement-like `for` clause without consuming a delimiter. pub(super) fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { match self.current() { - TokenKind::PlusPlus | TokenKind::MinusMinus => self.parse_prefix_inc_dec_stmt(false), + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_static_property_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(true, false) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_dynamic_static_property_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(true, false) + } + TokenKind::PlusPlus | TokenKind::MinusMinus => { + self.parse_prefix_inc_dec_stmt(false) + } + TokenKind::Ident(_) | TokenKind::Backslash + if self.current_starts_static_property_postfix_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(false, false) + } TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { self.parse_array_set_clause(name.clone()) } + TokenKind::DollarIdent(_) + if self.current_starts_dynamic_static_property_postfix_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(false, false) + } TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { self.parse_property_stmt(false) } @@ -2574,6 +2679,97 @@ impl Parser { }]) } + /// Parses static property increment/decrement as read-modify-write. + pub(super) fn parse_static_property_inc_dec_stmt( + &mut self, + prefixed: bool, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let prefix_increment = if prefixed { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + Some(increment) + } else { + None + }; + let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_static_class_name(class_name); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + let increment = if let Some(increment) = prefix_increment { + increment + } else { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + increment + }; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::StaticPropertySet { + value: inc_dec_static_property_value( + EvalExpr::StaticPropertyGet { + class_name: class_name.clone(), + property: property.clone(), + }, + increment, + ), + class_name, + property, + }]) + } + + /// Parses dynamic static property increment/decrement as read-modify-write. + pub(super) fn parse_dynamic_static_property_inc_dec_stmt( + &mut self, + prefixed: bool, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let prefix_increment = if prefixed { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + Some(increment) + } else { + None + }; + let TokenKind::DollarIdent(class_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let class_name = EvalExpr::LoadVar(class_name.clone()); + self.advance(); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + let increment = if let Some(increment) = prefix_increment { + increment + } else { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + increment + }; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::DynamicStaticPropertySet { + value: inc_dec_static_property_value( + EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name.clone()), + property: property.clone(), + }, + increment, + ), + class_name, + property, + }]) + } + /// Parses prefix `++$name` and `--$name` as simple statement effects. pub(super) fn parse_prefix_inc_dec_stmt( &mut self, @@ -2961,6 +3157,19 @@ fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { .eq_ignore_ascii_case("static") } +/// Builds the right-hand value for static property increment/decrement statements. +fn inc_dec_static_property_value(target: EvalExpr, increment: bool) -> EvalExpr { + EvalExpr::Binary { + op: if increment { + EvalBinOp::Add + } else { + EvalBinOp::Sub + }, + left: Box::new(target), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + } +} + /// Converts a parsed attribute argument expression into retained literal metadata. fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { match expr { diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 862633e250..32a5383dc8 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -204,6 +204,79 @@ fn parse_fragment_accepts_dynamic_static_property_unset() { ); } +/// Verifies static property increment/decrement parse as read-modify-write assignments. +#[test] +fn parse_fragment_accepts_static_property_inc_dec() { + let program = + parse_fragment(br#"EvalStaticBox::$count++; --EvalStaticBox::$count;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertySet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::StaticPropertyGet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::StaticPropertySet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::StaticPropertyGet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + ] + ); +} + +/// Verifies dynamic static property increment/decrement preserves the receiver expression. +#[test] +fn parse_fragment_accepts_dynamic_static_property_inc_dec() { + let program = + parse_fragment(br#"$class::$count++; --$class::$count;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + EvalStmt::DynamicStaticPropertySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Sub, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }, + ] + ); +} + /// Verifies static property compound assignments lower to one read-modify-write statement. #[test] fn parse_fragment_accepts_static_property_compound_assignment() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 31ab2bd0bb..e6fe73cacc 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes and `isset()` / `empty()` probes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 41e8319452..a961a02eca 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9328,6 +9328,46 @@ echo $aotClass::$count;'); assert_eq!(out.stdout, "set:tail|5|AB|15"); } +/// Verifies eval static property increment/decrement works with nominal and dynamic receivers. +#[test] +fn test_eval_static_property_inc_dec_statements() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 09:54:58 +0200 Subject: [PATCH 0736/1208] Support eval property inc dec statements --- crates/elephc-magician/src/eval_ir.rs | 20 ++++ .../src/interpreter/dynamic_functions.rs | 4 + .../src/interpreter/statements.rs | 85 ++++++++++++++ .../elephc-magician/src/parser/statements.rs | 105 +++++++++++++----- .../src/parser/tests/arrays_objects.rs | 43 +++++++ .../src/parser/tests/static_members.rs | 51 ++------- docs/php/eval.md | 2 +- tests/codegen/eval.rs | 45 ++++++++ 8 files changed, 285 insertions(+), 70 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 75cc2c2b4e..073a3d2deb 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -146,21 +146,41 @@ pub enum EvalStmt { property: EvalExpr, value: EvalExpr, }, + DynamicPropertyIncDec { + object: EvalExpr, + property: EvalExpr, + increment: bool, + }, PropertySet { object: EvalExpr, property: String, value: EvalExpr, }, + PropertyIncDec { + object: EvalExpr, + property: String, + increment: bool, + }, StaticPropertySet { class_name: String, property: String, value: EvalExpr, }, + StaticPropertyIncDec { + class_name: String, + property: String, + increment: bool, + }, DynamicStaticPropertySet { class_name: EvalExpr, property: String, value: EvalExpr, }, + DynamicStaticPropertyIncDec { + class_name: EvalExpr, + property: String, + increment: bool, + }, StaticVar { name: String, init: EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index ab1a04af8d..40c9ee4b38 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1129,12 +1129,16 @@ fn visit_static_var_declarations( | EvalStmt::Expr(_) | EvalStmt::Global { .. } | EvalStmt::InterfaceDecl(_) + | EvalStmt::DynamicPropertyIncDec { .. } | EvalStmt::DynamicPropertySet { .. } + | EvalStmt::DynamicStaticPropertyIncDec { .. } | EvalStmt::DynamicStaticPropertySet { .. } | EvalStmt::PropertyReferenceBind { .. } + | EvalStmt::PropertyIncDec { .. } | EvalStmt::PropertySet { .. } | EvalStmt::ReferenceAssign { .. } | EvalStmt::Return(_) + | EvalStmt::StaticPropertyIncDec { .. } | EvalStmt::StaticPropertySet { .. } | EvalStmt::StoreVar { .. } | EvalStmt::Throw(_) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b9561847ae..e46b04b1d3 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -177,6 +177,16 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_set_result(object, &property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicPropertyIncDec { + object, + property, + increment, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_inc_dec_result(object, &property, *increment, context, values)?; + Ok(EvalControl::None) + } EvalStmt::StaticVar { name, init } => { execute_static_var_stmt(name, init, context, scope, values)?; Ok(EvalControl::None) @@ -191,6 +201,15 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_set_result(object, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::PropertyIncDec { + object, + property, + increment, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_inc_dec_result(object, property, *increment, context, values)?; + Ok(EvalControl::None) + } EvalStmt::StaticPropertySet { class_name, property, @@ -200,6 +219,16 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_set_result(class_name, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::StaticPropertyIncDec { + class_name, + property, + increment, + } => { + eval_static_property_inc_dec_result( + class_name, property, *increment, context, values, + )?; + Ok(EvalControl::None) + } EvalStmt::DynamicStaticPropertySet { class_name, property, @@ -211,6 +240,22 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_set_result(&class_name, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicStaticPropertyIncDec { + class_name, + property, + increment, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_inc_dec_result( + &class_name, + property, + *increment, + context, + values, + )?; + Ok(EvalControl::None) + } EvalStmt::StoreVar { name, value } => { let value = eval_expr(value, context, scope, values)?; for replaced in set_scope_cell( @@ -299,6 +344,46 @@ pub(in crate::interpreter) fn execute_stmt( } } +/// Applies member increment/decrement to a runtime value using PHP numeric semantics. +fn eval_inc_dec_value( + current: RuntimeCellHandle, + increment: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let one = values.int(1)?; + if increment { + values.add(current, one) + } else { + values.sub(current, one) + } +} + +/// Reads, updates, and writes one object property after the receiver/name are evaluated. +fn eval_property_inc_dec_result( + object: RuntimeCellHandle, + property: &str, + increment: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_property_get_result(object, property, context, values)?; + let value = eval_inc_dec_value(current, increment, values)?; + eval_property_set_result(object, property, value, context, values) +} + +/// Reads, updates, and writes one static property after the receiver/name are resolved. +fn eval_static_property_inc_dec_result( + class_name: &str, + property: &str, + increment: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_static_property_get_result(class_name, property, context, values)?; + let value = eval_inc_dec_value(current, increment, values)?; + eval_static_property_set_result(class_name, property, value, context, values) +} + /// Releases one eval-owned value after running an eval-declared dynamic destructor if needed. fn eval_release_value( context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 08b03a79e2..48046f584b 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -12,8 +12,8 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, - EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, + EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalCallArg, EvalCatch, EvalClass, + EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInstanceOfTarget, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, EvalParameterTypeVariant, EvalSourceLocation, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, @@ -160,6 +160,11 @@ impl Parser { { self.parse_dynamic_static_property_inc_dec_stmt(true, true) } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_property_inc_dec() => + { + self.parse_prefixed_property_inc_dec_stmt(true) + } TokenKind::PlusPlus | TokenKind::MinusMinus => { self.parse_prefix_inc_dec_stmt(true) } @@ -344,6 +349,13 @@ impl Parser { && matches!(self.tokens.get(self.pos + 3), Some(TokenKind::DollarIdent(_))) } + /// Returns true when the current tokens form `++$object->property` or `--$object->property`. + pub(super) fn current_starts_prefixed_property_inc_dec(&self) -> bool { + matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::DollarIdent(_))) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::Arrow)) + } + /// Returns the token position after `Class::$property` when present at `pos`. fn static_property_tokens_end(&self, mut pos: usize) -> Option { if matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { @@ -2510,6 +2522,11 @@ impl Parser { { self.parse_dynamic_static_property_inc_dec_stmt(true, false) } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_property_inc_dec() => + { + self.parse_prefixed_property_inc_dec_stmt(false) + } TokenKind::PlusPlus | TokenKind::MinusMinus => { self.parse_prefix_inc_dec_stmt(false) } @@ -2710,16 +2727,10 @@ impl Parser { if require_semicolon { self.expect_semicolon()?; } - Ok(vec![EvalStmt::StaticPropertySet { - value: inc_dec_static_property_value( - EvalExpr::StaticPropertyGet { - class_name: class_name.clone(), - property: property.clone(), - }, - increment, - ), + Ok(vec![EvalStmt::StaticPropertyIncDec { class_name, property, + increment, }]) } @@ -2757,16 +2768,10 @@ impl Parser { if require_semicolon { self.expect_semicolon()?; } - Ok(vec![EvalStmt::DynamicStaticPropertySet { - value: inc_dec_static_property_value( - EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(class_name.clone()), - property: property.clone(), - }, - increment, - ), + Ok(vec![EvalStmt::DynamicStaticPropertyIncDec { class_name, property, + increment, }]) } @@ -2803,12 +2808,34 @@ impl Parser { Ok(vec![inc_dec_store(name, increment)]) } + /// Parses prefix property increment/decrement as read-modify-write. + pub(super) fn parse_prefixed_property_inc_dec_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + let target = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]) + } + /// Parses `$object->property` as either an expression statement or property write. pub(super) fn parse_property_stmt( &mut self, require_semicolon: bool, ) -> Result, EvalParseError> { let target = self.parse_expr()?; + if matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]); + } if !self.consume(TokenKind::Equal) { if require_semicolon { self.expect_semicolon()?; @@ -3157,16 +3184,20 @@ fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { .eq_ignore_ascii_case("static") } -/// Builds the right-hand value for static property increment/decrement statements. -fn inc_dec_static_property_value(target: EvalExpr, increment: bool) -> EvalExpr { - EvalExpr::Binary { - op: if increment { - EvalBinOp::Add - } else { - EvalBinOp::Sub - }, - left: Box::new(target), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), +/// Builds an object-property increment/decrement statement from a parsed property target. +fn property_inc_dec_stmt(target: EvalExpr, increment: bool) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyIncDec { + object: *object, + property, + increment, + }), + EvalExpr::DynamicPropertyGet { object, property } => Ok(EvalStmt::DynamicPropertyIncDec { + object: *object, + property: *property, + increment, + }), + _ => Err(EvalParseError::UnexpectedToken), } } @@ -3313,7 +3344,7 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { EvalStmt::UnsetDynamicStaticProperty { class_name, .. } => { eval_expr_uses_this_property(class_name, property_name) } - EvalStmt::UnsetStaticProperty { .. } => false, + EvalStmt::StaticPropertyIncDec { .. } | EvalStmt::UnsetStaticProperty { .. } => false, EvalStmt::DynamicPropertySet { object, property, @@ -3324,6 +3355,13 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { || eval_expr_uses_this_property(property, property_name) || eval_expr_uses_this_property(value, property_name) } + EvalStmt::DynamicPropertyIncDec { + object, property, .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } EvalStmt::PropertySet { object, property, @@ -3333,12 +3371,21 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { || eval_expr_uses_this_property(object, property_name) || eval_expr_uses_this_property(value, property_name) } + EvalStmt::PropertyIncDec { + object, property, .. + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } EvalStmt::DynamicStaticPropertySet { class_name, value, .. } => { eval_expr_uses_this_property(class_name, property_name) || eval_expr_uses_this_property(value, property_name) } + EvalStmt::DynamicStaticPropertyIncDec { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } EvalStmt::StaticPropertySet { value, .. } | EvalStmt::StoreVar { value, .. } => { eval_expr_uses_this_property(value, property_name) } diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index b53e97dca2..46e1ba6eab 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -370,6 +370,27 @@ fn parse_fragment_accepts_property_write_source() { ); } +/// Verifies object property increment/decrement parses as dedicated member updates. +#[test] +fn parse_fragment_accepts_property_inc_dec_source() { + let program = parse_fragment(br#"$this->x++; --$this->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + increment: true, + }, + EvalStmt::PropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + increment: false, + }, + ] + ); +} + /// Verifies dynamic object property writes parse as runtime-name EvalIR statements. #[test] fn parse_fragment_accepts_dynamic_property_write_source() { @@ -384,6 +405,28 @@ fn parse_fragment_accepts_dynamic_property_write_source() { ); } +/// Verifies dynamic object property increment/decrement keeps the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_inc_dec_source() { + let program = + parse_fragment(br#"$this->{$name}++; --$this->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + increment: true, + }, + EvalStmt::DynamicPropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + increment: false, + }, + ] + ); +} + /// Verifies dynamic object property unsets parse as runtime-name EvalIR statements. #[test] fn parse_fragment_accepts_dynamic_property_unset_source() { diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 32a5383dc8..d54a85ec49 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -204,38 +204,23 @@ fn parse_fragment_accepts_dynamic_static_property_unset() { ); } -/// Verifies static property increment/decrement parse as read-modify-write assignments. +/// Verifies static property increment/decrement parse as dedicated member updates. #[test] fn parse_fragment_accepts_static_property_inc_dec() { - let program = - parse_fragment(br#"EvalStaticBox::$count++; --EvalStaticBox::$count;"#) - .expect("fragment should parse"); + let program = parse_fragment(br#"EvalStaticBox::$count++; --EvalStaticBox::$count;"#) + .expect("fragment should parse"); assert_eq!( program.statements(), &[ - EvalStmt::StaticPropertySet { + EvalStmt::StaticPropertyIncDec { class_name: "EvalStaticBox".to_string(), property: "count".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::StaticPropertyGet { - class_name: "EvalStaticBox".to_string(), - property: "count".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, + increment: true, }, - EvalStmt::StaticPropertySet { + EvalStmt::StaticPropertyIncDec { class_name: "EvalStaticBox".to_string(), property: "count".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Sub, - left: Box::new(EvalExpr::StaticPropertyGet { - class_name: "EvalStaticBox".to_string(), - property: "count".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, + increment: false, }, ] ); @@ -249,29 +234,15 @@ fn parse_fragment_accepts_dynamic_static_property_inc_dec() { assert_eq!( program.statements(), &[ - EvalStmt::DynamicStaticPropertySet { + EvalStmt::DynamicStaticPropertyIncDec { class_name: EvalExpr::LoadVar("class".to_string()), property: "count".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - property: "count".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, + increment: true, }, - EvalStmt::DynamicStaticPropertySet { + EvalStmt::DynamicStaticPropertyIncDec { class_name: EvalExpr::LoadVar("class".to_string()), property: "count".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Sub, - left: Box::new(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - property: "count".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, + increment: false, }, ] ); diff --git a/docs/php/eval.md b/docs/php/eval.md index e6fe73cacc..580cb3e649 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -45,7 +45,7 @@ such a local alias removes the alias without unsetting the global value. | Comments | PHP comments are accepted inside fragments. | | Output | `echo` supports comma-separated arguments. `print` is an expression. | | Variables | Reads, writes, by-name assignment, by-reference assignment, `unset()`, `isset()`, and `empty()` are supported. | -| Assignment forms | Simple variable assignment, compound assignment (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), and simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`) are supported. | +| Assignment forms | Simple variable assignment, compound assignment (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a961a02eca..5ec6788ccd 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3458,6 +3458,51 @@ echo empty($missing?->{eval_dynamic_write_bad()}) ? "nullempty" : "bad";'); ); } +/// Verifies eval object property increment/decrement works with named and dynamic properties. +#[test] +fn test_eval_object_property_inc_dec_statements() { + let out = compile_and_run_capture( + r#"count++; +++$box->count; +$name = "count"; +$box->{$name}++; +--$box->{$name}; +++$box->{$name}; +$box->{eval_property_inc_name()}++; +--$box->{eval_property_inc_name()}; +$i = 0; +for (; $i < 3; $box->count++) { + $i++; +} +echo $box->count; echo "|"; +$aot = new EvalAotPropertyIncDec(); +$aot->count++; +++$aot->count; +$aot->count--; +--$aot->count; +echo $aot->count;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "n|n|7|10"); +} + /// Verifies eval string contexts dispatch AOT `__toString()` through the runtime method bridge. #[test] fn test_eval_aot_tostring_string_contexts() { From be43d0d67a6f8d42c48e88dfecc4b1af02148995 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 10:03:55 +0200 Subject: [PATCH 0737/1208] Support eval object property compound assignment --- crates/elephc-magician/src/eval_ir.rs | 12 +++ .../src/interpreter/dynamic_functions.rs | 2 + .../src/interpreter/expressions.rs | 79 +++++++++++-------- .../src/interpreter/statements.rs | 27 +++++++ .../elephc-magician/src/parser/statements.rs | 39 +++++++-- .../src/parser/tests/arrays_objects.rs | 49 ++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 44 +++++++++++ 8 files changed, 213 insertions(+), 41 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 073a3d2deb..e4553bcd4a 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -146,6 +146,12 @@ pub enum EvalStmt { property: EvalExpr, value: EvalExpr, }, + DynamicPropertyCompoundAssign { + object: EvalExpr, + property: EvalExpr, + op: EvalBinOp, + value: EvalExpr, + }, DynamicPropertyIncDec { object: EvalExpr, property: EvalExpr, @@ -156,6 +162,12 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + PropertyCompoundAssign { + object: EvalExpr, + property: String, + op: EvalBinOp, + value: EvalExpr, + }, PropertyIncDec { object: EvalExpr, property: String, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 40c9ee4b38..3573b03fe8 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1129,11 +1129,13 @@ fn visit_static_var_declarations( | EvalStmt::Expr(_) | EvalStmt::Global { .. } | EvalStmt::InterfaceDecl(_) + | EvalStmt::DynamicPropertyCompoundAssign { .. } | EvalStmt::DynamicPropertyIncDec { .. } | EvalStmt::DynamicPropertySet { .. } | EvalStmt::DynamicStaticPropertyIncDec { .. } | EvalStmt::DynamicStaticPropertySet { .. } | EvalStmt::PropertyReferenceBind { .. } + | EvalStmt::PropertyCompoundAssign { .. } | EvalStmt::PropertyIncDec { .. } | EvalStmt::PropertySet { .. } | EvalStmt::ReferenceAssign { .. } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 572dc3ade5..7932bd68d5 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -302,45 +302,54 @@ pub(in crate::interpreter) fn eval_expr( } let left = eval_expr(left, context, scope, values)?; let right = eval_expr(right, context, scope, values)?; - match op { - EvalBinOp::Add => values.add(left, right), - EvalBinOp::Sub => values.sub(left, right), - EvalBinOp::Mul => values.mul(left, right), - EvalBinOp::Div => values.div(left, right), - EvalBinOp::Mod => values.modulo(left, right), - EvalBinOp::Pow => values.pow(left, right), - EvalBinOp::BitAnd - | EvalBinOp::BitOr - | EvalBinOp::BitXor - | EvalBinOp::ShiftLeft - | EvalBinOp::ShiftRight => values.bitwise(*op, left, right), - EvalBinOp::Concat => { - let left = eval_string_context_value(left, context, values)?; - let right = eval_string_context_value(right, context, values)?; - values.concat(left, right) - } - EvalBinOp::LogicalXor => { - let left_truthy = values.truthy(left)?; - let right_truthy = values.truthy(right)?; - values.bool_value(left_truthy ^ right_truthy) - } - EvalBinOp::LooseEq - | EvalBinOp::LooseNotEq - | EvalBinOp::StrictEq - | EvalBinOp::StrictNotEq - | EvalBinOp::Lt - | EvalBinOp::LtEq - | EvalBinOp::Gt - | EvalBinOp::GtEq => values.compare(*op, left, right), - EvalBinOp::Spaceship => values.spaceship(left, right), - EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => { - Err(EvalStatus::UnsupportedConstruct) - } - } + eval_binary_result(*op, left, right, context, values) } } } +/// Applies one already-evaluated binary operation with eval runtime semantics. +pub(in crate::interpreter) fn eval_binary_result( + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match op { + EvalBinOp::Add => values.add(left, right), + EvalBinOp::Sub => values.sub(left, right), + EvalBinOp::Mul => values.mul(left, right), + EvalBinOp::Div => values.div(left, right), + EvalBinOp::Mod => values.modulo(left, right), + EvalBinOp::Pow => values.pow(left, right), + EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight => values.bitwise(op, left, right), + EvalBinOp::Concat => { + let left = eval_string_context_value(left, context, values)?; + let right = eval_string_context_value(right, context, values)?; + values.concat(left, right) + } + EvalBinOp::LogicalXor => { + let left_truthy = values.truthy(left)?; + let right_truthy = values.truthy(right)?; + values.bool_value(left_truthy ^ right_truthy) + } + EvalBinOp::LooseEq + | EvalBinOp::LooseNotEq + | EvalBinOp::StrictEq + | EvalBinOp::StrictNotEq + | EvalBinOp::Lt + | EvalBinOp::LtEq + | EvalBinOp::Gt + | EvalBinOp::GtEq => values.compare(op, left, right), + EvalBinOp::Spaceship => values.spaceship(left, right), + EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => Err(EvalStatus::UnsupportedConstruct), + } +} + /// Evaluates a runtime property or method name expression and returns its PHP string bytes as UTF-8. pub(in crate::interpreter) fn eval_dynamic_member_name( expr: &EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e46b04b1d3..b5f138a3a2 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -177,6 +177,20 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_set_result(object, &property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicPropertyCompoundAssign { + object, + property, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let current = eval_property_get_result(object, &property, context, values)?; + let right = eval_expr(value, context, scope, values)?; + let value = eval_binary_result(*op, current, right, context, values)?; + eval_property_set_result(object, &property, value, context, values)?; + Ok(EvalControl::None) + } EvalStmt::DynamicPropertyIncDec { object, property, @@ -201,6 +215,19 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_set_result(object, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::PropertyCompoundAssign { + object, + property, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let current = eval_property_get_result(object, property, context, values)?; + let right = eval_expr(value, context, scope, values)?; + let value = eval_binary_result(*op, current, right, context, values)?; + eval_property_set_result(object, property, value, context, values)?; + Ok(EvalControl::None) + } EvalStmt::PropertyIncDec { object, property, diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 48046f584b..45cdf6438b 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2836,29 +2836,46 @@ impl Parser { } return property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]); } - if !self.consume(TokenKind::Equal) { + let Some(op) = assignment_op(self.current()) else { if require_semicolon { self.expect_semicolon()?; } return Ok(vec![EvalStmt::Expr(target)]); - } + }; + self.advance(); let value = self.parse_expr()?; if require_semicolon { self.expect_semicolon()?; } - match target { - EvalExpr::PropertyGet { object, property } => Ok(vec![EvalStmt::PropertySet { + match (target, op) { + (EvalExpr::PropertyGet { object, property }, None) => Ok(vec![EvalStmt::PropertySet { object: *object, property, value, }]), - EvalExpr::DynamicPropertyGet { object, property } => { + (EvalExpr::PropertyGet { object, property }, Some(op)) => { + Ok(vec![EvalStmt::PropertyCompoundAssign { + object: *object, + property, + op, + value, + }]) + } + (EvalExpr::DynamicPropertyGet { object, property }, None) => { Ok(vec![EvalStmt::DynamicPropertySet { object: *object, property: *property, value, }]) } + (EvalExpr::DynamicPropertyGet { object, property }, Some(op)) => { + Ok(vec![EvalStmt::DynamicPropertyCompoundAssign { + object: *object, + property: *property, + op, + value, + }]) + } _ => Err(EvalParseError::UnexpectedToken), } } @@ -3349,6 +3366,12 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { object, property, value, + } + | EvalStmt::DynamicPropertyCompoundAssign { + object, + property, + value, + .. } => { eval_is_this_dynamic_property(object, property, property_name) || eval_expr_uses_this_property(object, property_name) @@ -3366,6 +3389,12 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { object, property, value, + } + | EvalStmt::PropertyCompoundAssign { + object, + property, + value, + .. } => { eval_is_this_property(object, property, property_name) || eval_expr_uses_this_property(object, property_name) diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index 46e1ba6eab..a9fca90111 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -405,6 +405,55 @@ fn parse_fragment_accepts_dynamic_property_write_source() { ); } +/// Verifies object property compound assignment parses as a dedicated member update. +#[test] +fn parse_fragment_accepts_property_compound_assignment_source() { + let program = parse_fragment(br#"$this->x += 2; $this->label .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + op: EvalBinOp::Add, + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::PropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: "label".to_string(), + op: EvalBinOp::Concat, + value: EvalExpr::Const(EvalConst::String("ok".to_string())), + }, + ] + ); +} + +/// Verifies dynamic object property compound assignment keeps the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_compound_assignment_source() { + let program = + parse_fragment(br#"$this->{$name} += 2; $this->{$label} .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + op: EvalBinOp::Add, + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicPropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("label".to_string()), + op: EvalBinOp::Concat, + value: EvalExpr::Const(EvalConst::String("ok".to_string())), + }, + ] + ); +} + /// Verifies dynamic object property increment/decrement keeps the runtime property expression. #[test] fn parse_fragment_accepts_dynamic_property_inc_dec_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 580cb3e649..6077788648 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -45,7 +45,7 @@ such a local alias removes the alias without unsetting the global value. | Comments | PHP comments are accepted inside fragments. | | Output | `echo` supports comma-separated arguments. `print` is an expression. | | Variables | Reads, writes, by-name assignment, by-reference assignment, `unset()`, `isset()`, and `empty()` are supported. | -| Assignment forms | Simple variable assignment, compound assignment (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | +| Assignment forms | Simple variable assignment, compound assignment for variables, object properties, and static properties (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5ec6788ccd..2a6ce5c9c2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3503,6 +3503,50 @@ echo $aot->count;'); assert_eq!(out.stdout, "n|n|7|10"); } +/// Verifies eval object property compound assignments work for named and dynamic properties. +#[test] +fn test_eval_object_property_compound_assignment_statements() { + let out = compile_and_run_capture( + r#"count += 2; +$box->count *= 3; +$box->label .= "y"; +$name = "count"; +$box->{$name} -= 1; +$box->{eval_property_compound_name()} += eval_property_compound_rhs(); +echo $box->count; echo ":"; echo $box->label; echo "|"; +$aot = new EvalAotPropertyCompoundAssign(); +$aot->count += 5; +$aot->count %= 6; +$aot->label .= "b"; +echo $aot->count; echo ":"; echo $aot->label;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "n|r|12:xy|3:ab"); +} + /// Verifies eval string contexts dispatch AOT `__toString()` through the runtime method bridge. #[test] fn test_eval_aot_tostring_string_contexts() { From a50bce8ae64938d1212cb67dd8ad5850732c640d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 10:20:44 +0200 Subject: [PATCH 0738/1208] Support eval property array writes --- crates/elephc-magician/src/eval_ir.rs | 24 ++++ .../src/interpreter/dynamic_functions.rs | 4 + .../src/interpreter/statements.rs | 135 ++++++++++++++++++ .../elephc-magician/src/parser/expressions.rs | 10 ++ .../elephc-magician/src/parser/statements.rs | 106 +++++++++++++- .../src/parser/tests/arrays_objects.rs | 64 +++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 41 ++++++ 8 files changed, 384 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index e4553bcd4a..462c6bcf1c 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -146,6 +146,18 @@ pub enum EvalStmt { property: EvalExpr, value: EvalExpr, }, + DynamicPropertyArrayAppend { + object: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicPropertyArraySet { + object: EvalExpr, + property: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, DynamicPropertyCompoundAssign { object: EvalExpr, property: EvalExpr, @@ -162,6 +174,18 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + PropertyArrayAppend { + object: EvalExpr, + property: String, + value: EvalExpr, + }, + PropertyArraySet { + object: EvalExpr, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, PropertyCompoundAssign { object: EvalExpr, property: String, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 3573b03fe8..5582849a79 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1129,12 +1129,16 @@ fn visit_static_var_declarations( | EvalStmt::Expr(_) | EvalStmt::Global { .. } | EvalStmt::InterfaceDecl(_) + | EvalStmt::DynamicPropertyArrayAppend { .. } + | EvalStmt::DynamicPropertyArraySet { .. } | EvalStmt::DynamicPropertyCompoundAssign { .. } | EvalStmt::DynamicPropertyIncDec { .. } | EvalStmt::DynamicPropertySet { .. } | EvalStmt::DynamicStaticPropertyIncDec { .. } | EvalStmt::DynamicStaticPropertySet { .. } | EvalStmt::PropertyReferenceBind { .. } + | EvalStmt::PropertyArrayAppend { .. } + | EvalStmt::PropertyArraySet { .. } | EvalStmt::PropertyCompoundAssign { .. } | EvalStmt::PropertyIncDec { .. } | EvalStmt::PropertySet { .. } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b5f138a3a2..385500fda6 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -177,6 +177,30 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_set_result(object, &property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicPropertyArrayAppend { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_array_append_result(object, &property, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyArraySet { + object, + property, + index, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_array_set_result( + object, &property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } EvalStmt::DynamicPropertyCompoundAssign { object, property, @@ -215,6 +239,28 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_set_result(object, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::PropertyArrayAppend { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_array_append_result(object, property, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertyArraySet { + object, + property, + index, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_array_set_result( + object, property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } EvalStmt::PropertyCompoundAssign { object, property, @@ -607,6 +653,95 @@ fn eval_non_object_array_set_var_stmt( Ok(()) } +/// Executes `$object->property[] = value`, dispatching ArrayAccess property values when needed. +fn eval_property_array_append_result( + object: RuntimeCellHandle, + property: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_property_get_result(object, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let array = if values.is_array_like(array)? { + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + array + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_property_set_result(object, property, array, context, values) +} + +/// Executes `$object->property[index] = value` and compound indexed property writes. +fn eval_property_array_set_result( + object: RuntimeCellHandle, + property: &str, + index: &EvalExpr, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_property_get_result(object, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let index = eval_array_set_index(index, context, scope, values)?; + let array = if values.is_array_like(array)? { + array + } else { + values.array_new(1)? + }; + let array = eval_array_set_target_for_index(array, index, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_property_set_result(object, property, array, context, values) +} + +/// Computes the value written by a simple or compound property-array assignment. +fn eval_property_array_set_value( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(op) = op else { + return eval_expr(value, context, scope, values); + }; + let current = eval_array_get_result(array, index, context, values)?; + let right = eval_expr(value, context, scope, values)?; + eval_binary_result(op, current, right, context, values) +} + /// Evaluates an array-set index and normalizes PHP integer-string keys. fn eval_array_set_index( index: &EvalExpr, diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 7f82386f29..fca4c46ff7 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -428,6 +428,11 @@ impl Parser { let mut expr = EvalExpr::LoadVar(name.clone()); self.advance(); loop { + if matches!(self.current(), TokenKind::LBracket) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::RBracket)) + { + break; + } if self.consume(TokenKind::LBracket) { let index = self.parse_expr()?; self.expect(TokenKind::RBracket)?; @@ -483,6 +488,11 @@ impl Parser { }; continue; } + if matches!(self.current(), TokenKind::LBracket) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::RBracket)) + { + break; + } if self.consume(TokenKind::LBracket) { let index = self.parse_expr()?; self.expect(TokenKind::RBracket)?; diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 45cdf6438b..3c4f4ea096 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -12,7 +12,7 @@ use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalCallArg, EvalCatch, EvalClass, + EvalArrayElement, EvalAttribute, EvalAttributeArg, EvalBinOp, EvalCallArg, EvalCatch, EvalClass, EvalClassConstant, EvalClassMethod, EvalClassProperty, EvalConst, EvalEnum, EvalEnumBackingType, EvalEnumCase, EvalExpr, EvalInstanceOfTarget, EvalInterface, EvalInterfaceMethod, EvalInterfaceProperty, EvalParameterType, EvalParameterTypeVariant, @@ -2836,6 +2836,27 @@ impl Parser { } return property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]); } + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return property_array_append_stmt(target, value).map(|stmt| vec![stmt]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return property_array_set_stmt(target, index, op, value).map(|stmt| vec![stmt]); + } let Some(op) = assignment_op(self.current()) else { if require_semicolon { self.expect_semicolon()?; @@ -2848,6 +2869,9 @@ impl Parser { self.expect_semicolon()?; } match (target, op) { + (EvalExpr::ArrayGet { array, index }, op) => { + property_array_set_stmt(*array, *index, op, value).map(|stmt| vec![stmt]) + } (EvalExpr::PropertyGet { object, property }, None) => Ok(vec![EvalStmt::PropertySet { object: *object, property, @@ -3218,6 +3242,51 @@ fn property_inc_dec_stmt(target: EvalExpr, increment: bool) -> Result Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyArrayAppend { + object: *object, + property, + value, + }), + EvalExpr::DynamicPropertyGet { object, property } => { + Ok(EvalStmt::DynamicPropertyArrayAppend { + object: *object, + property: *property, + value, + }) + } + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Builds an object-property array write statement from a parsed property target. +fn property_array_set_stmt( + target: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, +) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyArraySet { + object: *object, + property, + index, + op, + value, + }), + EvalExpr::DynamicPropertyGet { object, property } => Ok(EvalStmt::DynamicPropertyArraySet { + object: *object, + property: *property, + index, + op, + value, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + /// Converts a parsed attribute argument expression into retained literal metadata. fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { match expr { @@ -3367,6 +3436,11 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { property, value, } + | EvalStmt::DynamicPropertyArrayAppend { + object, + property, + value, + } | EvalStmt::DynamicPropertyCompoundAssign { object, property, @@ -3378,6 +3452,19 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { || eval_expr_uses_this_property(property, property_name) || eval_expr_uses_this_property(value, property_name) } + EvalStmt::DynamicPropertyArraySet { + object, + property, + index, + value, + .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } EvalStmt::DynamicPropertyIncDec { object, property, .. } => { @@ -3390,6 +3477,11 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { property, value, } + | EvalStmt::PropertyArrayAppend { + object, + property, + value, + } | EvalStmt::PropertyCompoundAssign { object, property, @@ -3400,6 +3492,18 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { || eval_expr_uses_this_property(object, property_name) || eval_expr_uses_this_property(value, property_name) } + EvalStmt::PropertyArraySet { + object, + property, + index, + value, + .. + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } EvalStmt::PropertyIncDec { object, property, .. } => { diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index a9fca90111..eae7549266 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -370,6 +370,46 @@ fn parse_fragment_accepts_property_write_source() { ); } +/// Verifies object property array writes and appends parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_array_write_source() { + let program = parse_fragment(br#"$this->items[0] = "x"; $this->items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::PropertyArrayAppend { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + +/// Verifies property array compound assignment retains the indexed property target. +#[test] +fn parse_fragment_accepts_property_array_compound_assignment_source() { + let program = parse_fragment(br#"$this->items[0] += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::PropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: Some(EvalBinOp::Add), + value: EvalExpr::Const(EvalConst::Int(2)), + }] + ); +} + /// Verifies object property increment/decrement parses as dedicated member updates. #[test] fn parse_fragment_accepts_property_inc_dec_source() { @@ -405,6 +445,30 @@ fn parse_fragment_accepts_dynamic_property_write_source() { ); } +/// Verifies dynamic property array writes keep the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_array_write_source() { + let program = parse_fragment(br#"$this->{$name}[0] = "x"; $this->{$name}[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::DynamicPropertyArrayAppend { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + /// Verifies object property compound assignment parses as a dedicated member update. #[test] fn parse_fragment_accepts_property_compound_assignment_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 6077788648..fc97caf587 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -72,7 +72,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | -| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | +| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property array writes/appends, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2a6ce5c9c2..bd05ad2e7e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3547,6 +3547,47 @@ echo $aot->count; echo ":"; echo $aot->label;'); assert_eq!(out.stdout, "n|r|12:xy|3:ab"); } +/// Verifies eval object property array writes and appends update property storage. +#[test] +fn test_eval_object_property_array_write_statements() { + let out = compile_and_run_capture( + r#"items[0] = "a"; +$box->items[] = "b"; +$box->items[0] .= "A"; +$name = "dyn"; +$box->{$name}[1] = "x"; +$box->{$name}[] = "y"; +$box->{eval_property_array_name()}[] = "z"; +echo $box->items[0] . ":" . $box->items[1] . ":"; +echo $box->dyn[1] . ":" . $box->dyn[2] . ":" . $box->dyn[3] . "|"; +$aot = new EvalAotPropertyArrayWrite(); +$aot->items[0] = "m"; +$aot->items[] = "n"; +$aot->items[0] .= "M"; +echo $aot->items[0] . ":" . $aot->items[1];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "n|aA:b:x:y:z|mM:n"); +} + /// Verifies eval string contexts dispatch AOT `__toString()` through the runtime method bridge. #[test] fn test_eval_aot_tostring_string_contexts() { From 4e16bcf7524654184baae30042cf281863301b88 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 10:34:39 +0200 Subject: [PATCH 0739/1208] Support eval static property array writes --- crates/elephc-magician/src/eval_ir.rs | 24 +++ .../src/interpreter/dynamic_functions.rs | 4 + .../src/interpreter/statements.rs | 131 +++++++++++++++ .../elephc-magician/src/parser/statements.rs | 154 ++++++++++++++---- .../src/parser/tests/static_members.rs | 66 ++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 63 +++++++ 7 files changed, 410 insertions(+), 34 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 462c6bcf1c..f9af0ec28c 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -202,6 +202,18 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + StaticPropertyArrayAppend { + class_name: String, + property: String, + value: EvalExpr, + }, + StaticPropertyArraySet { + class_name: String, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, StaticPropertyIncDec { class_name: String, property: String, @@ -212,6 +224,18 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + DynamicStaticPropertyArrayAppend { + class_name: EvalExpr, + property: String, + value: EvalExpr, + }, + DynamicStaticPropertyArraySet { + class_name: EvalExpr, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, DynamicStaticPropertyIncDec { class_name: EvalExpr, property: String, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 5582849a79..f868247ab1 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1134,6 +1134,8 @@ fn visit_static_var_declarations( | EvalStmt::DynamicPropertyCompoundAssign { .. } | EvalStmt::DynamicPropertyIncDec { .. } | EvalStmt::DynamicPropertySet { .. } + | EvalStmt::DynamicStaticPropertyArrayAppend { .. } + | EvalStmt::DynamicStaticPropertyArraySet { .. } | EvalStmt::DynamicStaticPropertyIncDec { .. } | EvalStmt::DynamicStaticPropertySet { .. } | EvalStmt::PropertyReferenceBind { .. } @@ -1144,6 +1146,8 @@ fn visit_static_var_declarations( | EvalStmt::PropertySet { .. } | EvalStmt::ReferenceAssign { .. } | EvalStmt::Return(_) + | EvalStmt::StaticPropertyArrayAppend { .. } + | EvalStmt::StaticPropertyArraySet { .. } | EvalStmt::StaticPropertyIncDec { .. } | EvalStmt::StaticPropertySet { .. } | EvalStmt::StoreVar { .. } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 385500fda6..6eed58428d 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -292,6 +292,28 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_set_result(class_name, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::StaticPropertyArrayAppend { + class_name, + property, + value, + } => { + eval_static_property_array_append_result( + class_name, property, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyArraySet { + class_name, + property, + index, + op, + value, + } => { + eval_static_property_array_set_result( + class_name, property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } EvalStmt::StaticPropertyIncDec { class_name, property, @@ -313,6 +335,44 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_set_result(&class_name, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_array_append_result( + &class_name, + property, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyArraySet { + class_name, + property, + index, + op, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_array_set_result( + &class_name, + property, + index, + *op, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } EvalStmt::DynamicStaticPropertyIncDec { class_name, property, @@ -742,6 +802,77 @@ fn eval_property_array_set_value( eval_binary_result(op, current, right, context, values) } +/// Executes `Class::$property[] = value`, including ArrayAccess static-property values. +fn eval_static_property_array_append_result( + class_name: &str, + property: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let array = if values.is_array_like(array)? { + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + array + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_static_property_set_result(class_name, property, array, context, values) +} + +/// Executes `Class::$property[index] = value` and compound indexed static-property writes. +fn eval_static_property_array_set_result( + class_name: &str, + property: &str, + index: &EvalExpr, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let index = eval_array_set_index(index, context, scope, values)?; + let array = if values.is_array_like(array)? { + array + } else { + values.array_new(1)? + }; + let array = eval_array_set_target_for_index(array, index, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_static_property_set_result(class_name, property, array, context, values) +} + /// Evaluates an array-set index and normalizes PHP integer-string keys. fn eval_array_set_index( index: &EvalExpr, diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 3c4f4ea096..0050c62f0e 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -273,45 +273,18 @@ impl Parser { Ok(EvalAttribute::new(name, args)) } - /// Returns true when the current tokens form `Class::$property `. + /// Returns true when current tokens form direct or indexed `Class::$property` assignment. pub(super) fn current_starts_static_property_assignment(&self) -> bool { - let mut pos = self.pos; - if matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { - pos += 1; - } - if !matches!(self.tokens.get(pos), Some(TokenKind::Ident(_))) { - return false; - } - pos += 1; - while matches!(self.tokens.get(pos), Some(TokenKind::Backslash)) { - pos += 1; - if !matches!(self.tokens.get(pos), Some(TokenKind::Ident(_))) { - return false; - } - pos += 1; - } - if !matches!(self.tokens.get(pos), Some(TokenKind::DoubleColon)) { - return false; - } - pos += 1; - let Some(TokenKind::DollarIdent(_)) = self.tokens.get(pos) else { - return false; - }; - pos += 1; - self.tokens - .get(pos) - .is_some_and(|token| assignment_op(token).is_some()) + self.static_property_tokens_end(self.pos) + .is_some_and(|pos| self.static_property_assignment_suffix_starts(pos)) } - /// Returns true when the current tokens form `$class::$property `. + /// Returns true when current tokens form direct or indexed `$class::$property` assignment. pub(super) fn current_starts_dynamic_static_property_assignment(&self) -> bool { matches!(self.current(), TokenKind::DollarIdent(_)) && matches!(self.peek(), TokenKind::DoubleColon) && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::DollarIdent(_))) - && self - .tokens - .get(self.pos + 3) - .is_some_and(|token| assignment_op(token).is_some()) + && self.static_property_assignment_suffix_starts(self.pos + 3) } /// Returns true when the current tokens form `Class::$property++` or `Class::$property--`. @@ -382,6 +355,40 @@ impl Parser { Some(pos + 1) } + /// Returns true when tokens after a static property form direct or indexed assignment. + fn static_property_assignment_suffix_starts(&self, pos: usize) -> bool { + self.tokens + .get(pos) + .is_some_and(|token| assignment_op(token).is_some()) + || self.static_property_array_assignment_suffix_starts(pos) + } + + /// Returns true when tokens at `pos` form `[expr] ` or `[] =`. + fn static_property_array_assignment_suffix_starts(&self, pos: usize) -> bool { + if !matches!(self.tokens.get(pos), Some(TokenKind::LBracket)) { + return false; + } + let mut cursor = pos; + let mut depth = 0usize; + loop { + match self.tokens.get(cursor) { + Some(TokenKind::LBracket) => depth += 1, + Some(TokenKind::RBracket) => { + depth = depth.saturating_sub(1); + if depth == 0 { + return self + .tokens + .get(cursor + 1) + .is_some_and(|token| assignment_op(token).is_some()); + } + } + Some(TokenKind::Eof) | None => return false, + _ => {} + } + cursor += 1; + } + } + /// Parses `do { ... } while (expr);`. pub(super) fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { self.advance(); @@ -2628,6 +2635,37 @@ impl Parser { }; let property = property.clone(); self.advance(); + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyArrayAppend { + class_name, + property, + value, + }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyArraySet { + class_name, + property, + index, + op, + value, + }]); + } let Some(op) = assignment_op(self.current()) else { return Err(EvalParseError::UnexpectedToken); }; @@ -2670,6 +2708,37 @@ impl Parser { }; let property = property.clone(); self.advance(); + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, + property, + value, + }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyArraySet { + class_name, + property, + index, + op, + value, + }]); + } let Some(op) = assignment_op(self.current()) else { return Err(EvalParseError::UnexpectedToken); }; @@ -3512,16 +3581,35 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { } EvalStmt::DynamicStaticPropertySet { class_name, value, .. + } + | EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, value, .. } => { eval_expr_uses_this_property(class_name, property_name) || eval_expr_uses_this_property(value, property_name) } + EvalStmt::DynamicStaticPropertyArraySet { + class_name, + index, + value, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } EvalStmt::DynamicStaticPropertyIncDec { class_name, .. } => { eval_expr_uses_this_property(class_name, property_name) } - EvalStmt::StaticPropertySet { value, .. } | EvalStmt::StoreVar { value, .. } => { + EvalStmt::StaticPropertySet { value, .. } + | EvalStmt::StaticPropertyArrayAppend { value, .. } + | EvalStmt::StoreVar { value, .. } => { eval_expr_uses_this_property(value, property_name) } + EvalStmt::StaticPropertyArraySet { index, value, .. } => { + eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } EvalStmt::Switch { expr, cases } => { eval_expr_uses_this_property(expr, property_name) || cases.iter().any(|case| { diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index d54a85ec49..eebd52ed99 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -268,3 +268,69 @@ fn parse_fragment_accepts_static_property_compound_assignment() { }] ); } + +/// Verifies indexed static-property writes parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_static_property_array_write_source() { + let program = + parse_fragment(br#"EvalStaticBox::$items[0] = "x"; EvalStaticBox::$items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertyArraySet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::StaticPropertyArrayAppend { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + +/// Verifies indexed static-property compound assignment retains the static target. +#[test] +fn parse_fragment_accepts_static_property_array_compound_assignment_source() { + let program = + parse_fragment(br#"EvalStaticBox::$items[0] .= "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StaticPropertyArraySet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: Some(EvalBinOp::Concat), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} + +/// Verifies runtime-valued static receivers support indexed static-property writes. +#[test] +fn parse_fragment_accepts_dynamic_static_property_array_write_source() { + let program = parse_fragment(br#"$class::$items[0] = "x"; $class::$items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertyArraySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} diff --git a/docs/php/eval.md b/docs/php/eval.md index fc97caf587..ed2a27b76b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -72,7 +72,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | -| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property array writes/appends, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | +| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bd05ad2e7e..6ca46a1d70 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9498,6 +9498,69 @@ echo EvalAotStaticIncDec::$count;'); assert_eq!(out.stdout, "6|11"); } +/// Verifies eval static property array writes and appends update static storage. +#[test] +fn test_eval_static_property_array_write_statements() { + let out = compile_and_run_capture( + r#"data[$offset]; + } + public function offsetSet(mixed $offset, mixed $value): void { + if ($offset === null) { + $this->data[] = $value; + } else { + $this->data[$offset] = $value; + } + } + public function offsetUnset(mixed $offset): void { + unset($this->data[$offset]); + } +} +class EvalStaticPropertyArrayAccessHolder { + public static $box; +} +EvalDynamicStaticPropertyArrayWrite::$items[0] = "a"; +EvalDynamicStaticPropertyArrayWrite::$items[] = "b"; +EvalDynamicStaticPropertyArrayWrite::$items[0] .= "A"; +$class = "EvalDynamicStaticPropertyArrayWrite"; +$class::$dyn[1] = "x"; +$class::$dyn[] = "y"; +echo EvalDynamicStaticPropertyArrayWrite::$items[0] . ":"; +echo EvalDynamicStaticPropertyArrayWrite::$items[1] . ":"; +echo EvalDynamicStaticPropertyArrayWrite::$dyn[1] . ":"; +echo EvalDynamicStaticPropertyArrayWrite::$dyn[2] . "|"; +EvalAotStaticPropertyArrayWrite::$items[0] = "m"; +EvalAotStaticPropertyArrayWrite::$items[] = "n"; +EvalAotStaticPropertyArrayWrite::$items[0] .= "M"; +echo EvalAotStaticPropertyArrayWrite::$items[0] . ":"; +echo EvalAotStaticPropertyArrayWrite::$items[1] . "|"; +EvalStaticPropertyArrayAccessHolder::$box = new EvalStaticPropertyArrayAccessBox(); +EvalStaticPropertyArrayAccessHolder::$box[] = "q"; +EvalStaticPropertyArrayAccessHolder::$box[0] .= "Q"; +echo EvalStaticPropertyArrayAccessHolder::$box[0];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "aA:b:x:y|mM:n|qQ"); +} + /// Verifies eval `isset()` and `empty()` probe static properties without normal read fatals. #[test] fn test_eval_static_property_isset_empty_probes() { From fd942bbab8c703f8e6966b3c2aa8740777310a3d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 10:50:43 +0200 Subject: [PATCH 0740/1208] Support eval property array unsets --- .../src/interpreter/statements.rs | 122 ++++++++++++++++++ .../src/parser/tests/arrays_objects.rs | 26 ++++ .../src/parser/tests/static_members.rs | 26 ++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 68 ++++++++++ 5 files changed, 243 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 6eed58428d..21c85a5427 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -573,7 +573,104 @@ fn eval_array_unset_element_stmt( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { + match array { + EvalExpr::LoadVar(name) => { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + let Some((array, ownership)) = existing else { + return Ok(()); + }; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { + values.release(replaced)?; + } + } + return Ok(()); + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let array = eval_property_get_result(object, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_property_set_result(object, property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let array = eval_property_get_result(object, &property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_property_set_result(object, &property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(class_name, property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let array = eval_static_property_get_result(&class_name, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(&class_name, property, array, context, values)?; + } + return Ok(()); + } + _ => {} + } let array = eval_expr(array, context, scope, values)?; + eval_array_access_unset_result(array, index, context, scope, values) +} + +/// Unsets one offset from an already-resolved array-like target and returns a replacement array. +fn eval_array_unset_target_result( + array: RuntimeCellHandle, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(array)? == EVAL_TAG_OBJECT { + eval_array_access_unset_result(array, index, context, scope, values)?; + return Ok(None); + } + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + let index = eval_array_set_index(index, context, scope, values)?; + eval_array_without_key_result(array, index, values).map(Some) +} + +/// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. +fn eval_array_access_unset_result( + array: RuntimeCellHandle, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { let index = eval_expr(index, context, scope, values)?; if values.type_tag(array)? != EVAL_TAG_OBJECT { return Err(EvalStatus::UnsupportedConstruct); @@ -586,6 +683,31 @@ fn eval_array_unset_element_stmt( Ok(()) } +/// Rebuilds an array without the strict-equal key requested by `unset($array[$key])`. +fn eval_array_without_key_result( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let tag = values.type_tag(array)?; + let mut result = if tag == EVAL_TAG_ASSOC { + values.assoc_new(len.saturating_sub(1))? + } else { + values.array_new(len.saturating_sub(1))? + }; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let equal = values.compare(EvalBinOp::StrictEq, key, index)?; + if values.truthy(equal)? { + continue; + } + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Executes `$var[] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. fn eval_array_append_var_stmt( name: &str, diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index eae7549266..c399bcc705 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -552,3 +552,29 @@ fn parse_fragment_accepts_dynamic_property_unset_source() { }] ); } + +/// Verifies unsetting object-property array elements keeps the property expression target. +#[test] +fn parse_fragment_accepts_property_array_unset_source() { + let program = parse_fragment(br#"unset($this->items[0], $this->{$name}[1]);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetArrayElement { + array: EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(0)), + }, + EvalStmt::UnsetArrayElement { + array: EvalExpr::DynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }, + index: EvalExpr::Const(EvalConst::Int(1)), + }, + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index eebd52ed99..34e33c996c 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -204,6 +204,32 @@ fn parse_fragment_accepts_dynamic_static_property_unset() { ); } +/// Verifies static-property array element unset preserves the static member expression. +#[test] +fn parse_fragment_accepts_static_property_array_unset() { + let program = parse_fragment(br#"unset(EvalStaticBox::$items[0], $class::$items[1]);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetArrayElement { + array: EvalExpr::StaticPropertyGet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(0)), + }, + EvalStmt::UnsetArrayElement { + array: EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(1)), + }, + ] + ); +} + /// Verifies static property increment/decrement parse as dedicated member updates. #[test] fn parse_fragment_accepts_static_property_inc_dec() { diff --git a/docs/php/eval.md b/docs/php/eval.md index ed2a27b76b..42629eb6c1 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -72,7 +72,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | -| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | +| Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6ca46a1d70..753f907555 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9561,6 +9561,74 @@ echo EvalStaticPropertyArrayAccessHolder::$box[0];'); assert_eq!(out.stdout, "aA:b:x:y|mM:n|qQ"); } +/// Verifies eval unsets object-property and static-property array elements. +#[test] +fn test_eval_property_and_static_property_array_unset_statements() { + let out = compile_and_run_capture( + r#"removed = $offset; + } +} + +class EvalAotPropertyArrayUnset { + public array $items = ["a", "b"]; + public static array $staticItems = ["x", "y"]; +} + +eval('class EvalDynamicPropertyArrayUnset { + public array $items = ["a", "b"]; + public static $staticItems = ["x", "y"]; + public $box; + public static $staticBox; +} +$dyn = new EvalDynamicPropertyArrayUnset(); +unset($dyn->items[0]); +$name = "items"; +unset($dyn->{$name}[1]); +echo isset($dyn->items[0]) ? "bad" : "d0"; echo ":"; +echo isset($dyn->items[1]) ? "bad" : "d1"; echo "|"; +unset(EvalDynamicPropertyArrayUnset::$staticItems[0]); +$class = "EvalDynamicPropertyArrayUnset"; +unset($class::$staticItems[1]); +echo isset(EvalDynamicPropertyArrayUnset::$staticItems[0]) ? "bad" : "s0"; echo ":"; +echo isset(EvalDynamicPropertyArrayUnset::$staticItems[1]) ? "bad" : "s1"; echo "|"; +$aot = new EvalAotPropertyArrayUnset(); +unset($aot->items[0]); +unset(EvalAotPropertyArrayUnset::$staticItems[0]); +echo isset($aot->items[0]) ? "bad" : "a0"; echo ":"; +echo isset(EvalAotPropertyArrayUnset::$staticItems[0]) ? "bad" : "as0"; echo "|"; +$dyn->box = new EvalArrayUnsetAccessBox(); +unset($dyn->box["drop"]); +echo $dyn->box->removed . ":" . $dyn->box["keep"] . "|"; +EvalDynamicPropertyArrayUnset::$staticBox = new EvalArrayUnsetAccessBox(); +unset(EvalDynamicPropertyArrayUnset::$staticBox["drop"]); +echo EvalDynamicPropertyArrayUnset::$staticBox->removed . ":"; +echo EvalDynamicPropertyArrayUnset::$staticBox["keep"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "d0:d1|s0:s1|a0:as0|drop:K|drop:K"); +} + /// Verifies eval `isset()` and `empty()` probe static properties without normal read fatals. #[test] fn test_eval_static_property_isset_empty_probes() { From 5755080e06327d516d1a28dda9f1d8f080c87440 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 10:57:01 +0200 Subject: [PATCH 0741/1208] Support eval named receiver dynamic static calls --- .../elephc-magician/src/parser/expressions.rs | 8 ++++++ .../src/parser/tests/static_members.rs | 17 +++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 28 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index fca4c46ff7..5620dd3cc3 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -815,6 +815,14 @@ impl Parser { TokenKind::DollarIdent(property) => { let property = property.clone(); self.advance(); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + method: Box::new(EvalExpr::LoadVar(property)), + args, + }); + } Ok(EvalExpr::StaticPropertyGet { class_name, property, diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 34e33c996c..8396a7dd3f 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -115,6 +115,23 @@ fn parse_fragment_accepts_dynamic_static_method_name() { ); } +/// Verifies named static receivers support variable method names. +#[test] +fn parse_fragment_accepts_named_receiver_dynamic_static_method_name() { + let program = + parse_fragment(br#"return EvalStaticBox::$method(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + /// Verifies runtime-valued static receivers support properties, constants, and `::class`. #[test] fn parse_fragment_accepts_dynamic_static_metadata_receiver() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 42629eb6c1..f8dfa802ae 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -76,7 +76,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()` and `$class::$method()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()` and `$class::$method()`), variable static method names on named receivers (`ClassName::$method()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, and `$object::class` are supported. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 753f907555..8c67b5cd27 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10049,6 +10049,34 @@ echo EvalMagicStaticBox::Hidden("C", name: "D");'); assert_eq!(out.stdout, "DoStatic:A:B:Hidden:C:D"); } +/// Verifies eval supports variable static method names with a named receiver. +#[test] +fn test_eval_named_receiver_dynamic_static_method_name() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 11:03:58 +0200 Subject: [PATCH 0742/1208] Support eval braced dynamic static calls --- .../elephc-magician/src/parser/expressions.rs | 28 +++++++++++++++++++ .../src/parser/tests/static_members.rs | 26 +++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 28 +++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 5620dd3cc3..2dca9e00db 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -844,6 +844,20 @@ impl Parser { args, }) } + TokenKind::LBrace => { + self.advance(); + let method = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + if !matches!(self.current(), TokenKind::LParen) { + return Err(EvalParseError::UnsupportedConstruct); + } + let args = self.parse_call_args()?; + Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + method: Box::new(method), + args, + }) + } TokenKind::Ident(constant) => { let constant = constant.clone(); self.advance(); @@ -896,6 +910,20 @@ impl Parser { args, }) } + TokenKind::LBrace => { + self.advance(); + let method = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + if !matches!(self.current(), TokenKind::LParen) { + return Err(EvalParseError::UnsupportedConstruct); + } + let args = self.parse_call_args()?; + Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(method), + args, + }) + } TokenKind::Ident(constant) => { let constant = constant.clone(); self.advance(); diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 8396a7dd3f..69f4333ca3 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -132,6 +132,32 @@ fn parse_fragment_accepts_named_receiver_dynamic_static_method_name() { ); } +/// Verifies braced dynamic static method names preserve their method expression. +#[test] +fn parse_fragment_accepts_braced_dynamic_static_method_name() { + let program = + parse_fragment(br#"return EvalStaticBox::{$method}(2) . $class::{"Read"}(3);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }), + right: Box::new(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(3)))], + }), + }))] + ); +} + /// Verifies runtime-valued static receivers support properties, constants, and `::class`. #[test] fn parse_fragment_accepts_dynamic_static_metadata_receiver() { diff --git a/docs/php/eval.md b/docs/php/eval.md index f8dfa802ae..7001b6f033 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -76,7 +76,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()` and `$class::$method()`), variable static method names on named receivers (`ClassName::$method()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()` and `$class::$method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, and `$object::class` are supported. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8c67b5cd27..d02df53858 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10077,6 +10077,34 @@ echo EvalNamedReceiverDynamicStaticMethod::$missing("B");'); assert_eq!(out.stdout, "read:A|later:B"); } +/// Verifies eval supports braced dynamic static method names. +#[test] +fn test_eval_braced_dynamic_static_method_name() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 11:13:07 +0200 Subject: [PATCH 0743/1208] Support eval expression new targets --- .../elephc-magician/src/parser/expressions.rs | 22 ++++++++++- .../src/parser/tests/arrays_objects.rs | 39 +++++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 27 +++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 2dca9e00db..4285053396 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -950,7 +950,16 @@ impl Parser { if let TokenKind::DollarIdent(name) = self.current() { let class_name = EvalExpr::LoadVar(name.clone()); self.advance(); - let args = self.parse_call_args()?; + let args = self.parse_optional_constructor_args()?; + return Ok(EvalExpr::DynamicNewObject { + class_name: Box::new(class_name), + args, + }); + } + if self.consume(TokenKind::LParen) { + let class_name = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let args = self.parse_optional_constructor_args()?; return Ok(EvalExpr::DynamicNewObject { class_name: Box::new(class_name), args, @@ -958,10 +967,19 @@ impl Parser { } let class_name = self.parse_qualified_name()?; let class_name = self.resolve_class_name(class_name); - let args = self.parse_call_args()?; + let args = self.parse_optional_constructor_args()?; Ok(EvalExpr::NewObject { class_name, args }) } + /// Parses an optional constructor argument list after `new` class targets. + fn parse_optional_constructor_args(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::LParen) { + self.parse_call_args() + } else { + Ok(Vec::new()) + } + } + /// Parses a simple or explicitly qualified PHP name. pub(super) fn parse_qualified_name(&mut self) -> Result { let absolute = self.consume(TokenKind::Backslash); diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index c399bcc705..2277d533e0 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -237,6 +237,45 @@ fn parse_fragment_accepts_dynamic_new_object_source() { }))] ); } +/// Verifies object construction accepts a parenthesized runtime class-name expression. +#[test] +fn parse_fragment_accepts_expression_new_object_source() { + let program = + parse_fragment(br#"return new ($factory->className)("Ada");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("factory".to_string())), + property: "className".to_string(), + }), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string() + )))], + }))] + ); +} +/// Verifies PHP constructor parentheses are optional for named and runtime class targets. +#[test] +fn parse_fragment_accepts_new_object_without_constructor_parentheses_source() { + let named = parse_fragment(br#"return new Box;"#).expect("fragment should parse"); + assert_eq!( + named.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Box".to_string(), + args: Vec::new(), + }))] + ); + + let dynamic = parse_fragment(br#"return new $className;"#).expect("fragment should parse"); + assert_eq!( + dynamic.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::LoadVar("className".to_string())), + args: Vec::new(), + }))] + ); +} /// Verifies object construction accepts explicitly qualified class names. #[test] fn parse_fragment_accepts_qualified_new_object_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 7001b6f033..96ee7a1593 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName(...)` and `new $className(...)` for eval-declared classes, including constructor named arguments and unpacking; runtime class-name variables may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | +| Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()` and `$class::$method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d02df53858..fe4bf36b73 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6817,6 +6817,33 @@ echo $copy->label;'); assert_eq!(out, "eval:Ada|aot:Turing|eval:Grace"); } +/// Verifies eval object construction accepts parenthesized class expressions and optional constructor parentheses. +#[test] +fn test_eval_dynamic_new_accepts_expression_class_name() { + let out = compile_and_run( + r#"label = $label; + } +} + +function eval_expression_new_target() { + return "EvalExpressionNewTarget"; +} + +$direct = new (eval_expression_new_target())("Ada"); +$class = "EvalExpressionNewTarget"; +$withoutDynamicParens = new $class; +$withoutNamedParens = new EvalExpressionNewTarget; +return $direct->label . "|" . $withoutDynamicParens->label . "|" . $withoutNamedParens->label;'); +"#, + ); + assert_eq!(out, "Ada|default|default"); +} + /// Verifies eval object construction passes object-typed arguments to AOT constructors. #[test] fn test_eval_dynamic_new_passes_object_arg_to_constructor() { From 9f37eb8148803b419b32ef480e46a9c9a16f7c9c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 11:20:31 +0200 Subject: [PATCH 0744/1208] Support eval dynamic class constant names --- crates/elephc-magician/src/eval_ir.rs | 4 +++ .../src/interpreter/expressions.rs | 9 +++++ .../elephc-magician/src/parser/expressions.rs | 34 +++++++++++-------- .../elephc-magician/src/parser/statements.rs | 7 ++++ .../src/parser/tests/static_members.rs | 24 +++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 23 +++++++++++++ 7 files changed, 88 insertions(+), 15 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index f9af0ec28c..2d485f1bcb 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2405,6 +2405,10 @@ pub enum EvalExpr { class_name: Box, constant: String, }, + DynamicClassConstantNameFetch { + class_name: Box, + constant: Box, + }, DynamicClassNameFetch { class_name: Box, }, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 7932bd68d5..ac5c2a04df 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -95,6 +95,15 @@ pub(in crate::interpreter) fn eval_expr( let class_name = eval_dynamic_class_name(class_name, context, values)?; eval_class_constant_fetch_result(&class_name, constant, context, values) } + EvalExpr::DynamicClassConstantNameFetch { + class_name, + constant, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let constant = eval_dynamic_member_name(constant, context, scope, values)?; + eval_class_constant_fetch_result(&class_name, &constant, context, values) + } EvalExpr::DynamicClassNameFetch { class_name } => { let class_name = eval_expr(class_name, context, scope, values)?; eval_dynamic_class_name_fetch_result(class_name, context, values) diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 4285053396..ff11281f62 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -846,16 +846,19 @@ impl Parser { } TokenKind::LBrace => { self.advance(); - let method = self.parse_expr()?; + let member = self.parse_expr()?; self.expect(TokenKind::RBrace)?; - if !matches!(self.current(), TokenKind::LParen) { - return Err(EvalParseError::UnsupportedConstruct); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + method: Box::new(member), + args, + }); } - let args = self.parse_call_args()?; - Ok(EvalExpr::DynamicStaticMethodCall { + Ok(EvalExpr::DynamicClassConstantNameFetch { class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), - method: Box::new(method), - args, + constant: Box::new(member), }) } TokenKind::Ident(constant) => { @@ -912,16 +915,19 @@ impl Parser { } TokenKind::LBrace => { self.advance(); - let method = self.parse_expr()?; + let member = self.parse_expr()?; self.expect(TokenKind::RBrace)?; - if !matches!(self.current(), TokenKind::LParen) { - return Err(EvalParseError::UnsupportedConstruct); + if matches!(self.current(), TokenKind::LParen) { + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(member), + args, + }); } - let args = self.parse_call_args()?; - Ok(EvalExpr::DynamicStaticMethodCall { + Ok(EvalExpr::DynamicClassConstantNameFetch { class_name: Box::new(class_name), - method: Box::new(method), - args, + constant: Box::new(member), }) } TokenKind::Ident(constant) => { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 0050c62f0e..ac7f5f7b57 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3688,6 +3688,13 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { | EvalExpr::DynamicClassNameFetch { class_name } => { eval_expr_uses_this_property(class_name, property_name) } + EvalExpr::DynamicClassConstantNameFetch { + class_name, + constant, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(constant, property_name) + } EvalExpr::DynamicMethodCall { object, method, diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 69f4333ca3..0b280e39f9 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -158,6 +158,30 @@ fn parse_fragment_accepts_braced_dynamic_static_method_name() { ); } +/// Verifies braced dynamic class constant names preserve their constant-name expression. +#[test] +fn parse_fragment_accepts_braced_dynamic_class_constant_name() { + let program = + parse_fragment(br#"return EvalStaticBox::{$constant} . $class::{"VALUE"};"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + constant: Box::new(EvalExpr::LoadVar("constant".to_string())), + }), + right: Box::new(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + constant: Box::new(EvalExpr::Const(EvalConst::String("VALUE".to_string()))), + }), + }))] + ); +} + /// Verifies runtime-valued static receivers support properties, constants, and `::class`. #[test] fn parse_fragment_accepts_dynamic_static_metadata_receiver() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 96ee7a1593..c33b71bf41 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -79,7 +79,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()` and `$class::$method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | -| Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, and `$object::class` are supported. | +| Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}`), and `$object::class` are supported. | | Ternaries | Full ternary and short ternary (`?:`). | | Match | Strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default`. A miss without `default` is reported as an eval runtime fatal. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fe4bf36b73..db79368160 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10132,6 +10132,29 @@ echo $class::{"missing"}("B");'); assert_eq!(out.stdout, "read:A|missing:B"); } +/// Verifies eval supports braced dynamic class constant names. +#[test] +fn test_eval_braced_dynamic_class_constant_name() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 11:27:24 +0200 Subject: [PATCH 0745/1208] Support eval expression static receivers --- .../elephc-magician/src/parser/expressions.rs | 4 +++ .../src/parser/tests/static_members.rs | 35 +++++++++++++++++++ docs/php/eval.md | 6 ++-- tests/codegen/eval.rs | 33 +++++++++++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index ff11281f62..46bd0dd24f 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -502,6 +502,10 @@ impl Parser { }; continue; } + if self.consume(TokenKind::DoubleColon) { + expr = self.parse_dynamic_static_member_expr(expr)?; + continue; + } let nullsafe = if self.consume(TokenKind::Arrow) { false } else if self.consume(TokenKind::QuestionArrow) { diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 0b280e39f9..4173301dd5 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -182,6 +182,41 @@ fn parse_fragment_accepts_braced_dynamic_class_constant_name() { ); } +/// Verifies parenthesized static receivers parse through the dynamic receiver path. +#[test] +fn parse_fragment_accepts_expression_static_receiver_members() { + let program = parse_fragment( + br#"return [($class)::read(2), (factory())::VALUE, (factory())::{"VALUE"}, (factory())::$count];"#, + ) + .expect("fragment should parse"); + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("read".to_string()))), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }), + EvalArrayElement::Value(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(factory_call()), + constant: "VALUE".to_string(), + }), + EvalArrayElement::Value(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(factory_call()), + constant: Box::new(EvalExpr::Const(EvalConst::String("VALUE".to_string()))), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(factory_call()), + property: "count".to_string(), + }), + ])))] + ); +} + /// Verifies runtime-valued static receivers support properties, constants, and `::class`. #[test] fn parse_fragment_accepts_dynamic_static_metadata_receiver() { diff --git a/docs/php/eval.md b/docs/php/eval.md index c33b71bf41..74cc016b85 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,15 +71,15 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()` and `$class::$method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | -| Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}`), and `$object::class` are supported. | +| Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | | Ternaries | Full ternary and short ternary (`?:`). | | Match | Strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default`. A miss without `default` is reported as an eval runtime fatal. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index db79368160..104dfcd40c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10155,6 +10155,39 @@ echo $class::{"FALLBACK"};'); assert_eq!(out.stdout, "read|fallback"); } +/// Verifies eval supports parenthesized expression static receivers for reads and calls. +#[test] +fn test_eval_expression_static_receiver_members() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 11:34:27 +0200 Subject: [PATCH 0746/1208] Support eval expression static property writes --- .../elephc-magician/src/parser/statements.rs | 65 ++++++++++++++++++- .../src/parser/tests/static_members.rs | 56 ++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 33 ++++++++++ 4 files changed, 152 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index ac7f5f7b57..dc2af7a3ca 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -186,8 +186,7 @@ impl Parser { TokenKind::Eof => Err(EvalParseError::UnexpectedEof), _ => { let expr = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::Expr(expr)]) + self.parse_property_like_stmt_tail(expr, true) } } } @@ -2564,7 +2563,7 @@ impl Parser { } _ => { let expr = self.parse_expr()?; - Ok(vec![EvalStmt::Expr(expr)]) + self.parse_property_like_stmt_tail(expr, false) } } } @@ -2897,6 +2896,15 @@ impl Parser { require_semicolon: bool, ) -> Result, EvalParseError> { let target = self.parse_expr()?; + self.parse_property_like_stmt_tail(target, require_semicolon) + } + + /// Parses assignment, array-write, or inc/dec tails after a parsed property-like target. + fn parse_property_like_stmt_tail( + &mut self, + target: EvalExpr, + require_semicolon: bool, + ) -> Result, EvalParseError> { if matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) { let increment = matches!(self.current(), TokenKind::PlusPlus); self.advance(); @@ -2969,6 +2977,31 @@ impl Parser { value, }]) } + ( + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + }, + op, + ) => { + let class_name = *class_name; + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name.clone()), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + }]) + } _ => Err(EvalParseError::UnexpectedToken), } } @@ -3307,6 +3340,14 @@ fn property_inc_dec_stmt(target: EvalExpr, increment: bool) -> Result Ok(EvalStmt::DynamicStaticPropertyIncDec { + class_name: *class_name, + property, + increment, + }), _ => Err(EvalParseError::UnexpectedToken), } } @@ -3326,6 +3367,14 @@ fn property_array_append_stmt(target: EvalExpr, value: EvalExpr) -> Result Ok(EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: *class_name, + property, + value, + }), _ => Err(EvalParseError::UnexpectedToken), } } @@ -3352,6 +3401,16 @@ fn property_array_set_stmt( op, value, }), + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyArraySet { + class_name: *class_name, + property, + index, + op, + value, + }), _ => Err(EvalParseError::UnexpectedToken), } } diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 4173301dd5..dcb2900201 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -217,6 +217,62 @@ fn parse_fragment_accepts_expression_static_receiver_members() { ); } +/// Verifies expression static receivers support property write statement lowering. +#[test] +fn parse_fragment_accepts_expression_static_receiver_property_writes() { + let program = parse_fragment( + br#"(factory())::$count = 2; +(factory())::$count += 3; +(factory())::$items[] = 4; +(factory())::$items[0] = 5; +(factory())::$count++;"#, + ) + .expect("fragment should parse"); + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertySet { + class_name: factory_call(), + property: "count".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicStaticPropertySet { + class_name: factory_call(), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(factory_call()), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: factory_call(), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::Int(4)), + }, + EvalStmt::DynamicStaticPropertyArraySet { + class_name: factory_call(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::Int(5)), + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: true, + }, + ] + ); +} + /// Verifies runtime-valued static receivers support properties, constants, and `::class`. #[test] fn parse_fragment_accepts_dynamic_static_metadata_receiver() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 74cc016b85..b8ce3e2355 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and postfix increment/decrement, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 104dfcd40c..bb4d8417c3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10188,6 +10188,39 @@ echo (eval_expression_static_receiver())::$count;'); assert_eq!(out.stdout, "read:A|word|word|5"); } +/// Verifies eval supports expression static receivers for static property writes. +#[test] +fn test_eval_expression_static_receiver_property_writes() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 11:40:49 +0200 Subject: [PATCH 0747/1208] Support eval prefixed expression static property mutations --- .../elephc-magician/src/parser/statements.rs | 18 +++++++++++------- .../src/parser/tests/static_members.rs | 14 +++++++++++++- docs/php/eval.md | 2 +- tests/codegen/eval.rs | 2 ++ 4 files changed, 27 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index dc2af7a3ca..be5cba5afe 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2843,22 +2843,26 @@ impl Parser { }]) } - /// Parses prefix `++$name` and `--$name` as simple statement effects. + /// Parses prefix `++$name` / `--$name` and supported property-like prefix mutations. pub(super) fn parse_prefix_inc_dec_stmt( &mut self, require_semicolon: bool, ) -> Result, EvalParseError> { let increment = matches!(self.current(), TokenKind::PlusPlus); self.advance(); - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let name = name.clone(); - self.advance(); + if let TokenKind::DollarIdent(name) = self.current() { + let name = name.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![inc_dec_store(name, increment)]); + } + let target = self.parse_expr()?; if require_semicolon { self.expect_semicolon()?; } - Ok(vec![inc_dec_store(name, increment)]) + property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]) } /// Parses postfix `$name++` and `$name--` as simple statement effects. diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index dcb2900201..8ef84c40a5 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -225,7 +225,9 @@ fn parse_fragment_accepts_expression_static_receiver_property_writes() { (factory())::$count += 3; (factory())::$items[] = 4; (factory())::$items[0] = 5; -(factory())::$count++;"#, +(factory())::$count++; +++ (factory())::$count; +-- (factory())::$count;"#, ) .expect("fragment should parse"); let factory_call = || EvalExpr::Call { @@ -269,6 +271,16 @@ fn parse_fragment_accepts_expression_static_receiver_property_writes() { property: "count".to_string(), increment: true, }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: false, + }, ] ); } diff --git a/docs/php/eval.md b/docs/php/eval.md index b8ce3e2355..3a8e79d3c8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and postfix increment/decrement, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bb4d8417c3..992c5e4973 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10208,6 +10208,8 @@ function eval_expression_static_receiver_writes() { (eval_expression_static_receiver_writes())::$items[] = 4; (eval_expression_static_receiver_writes())::$items[0] = 5; (eval_expression_static_receiver_writes())::$count++; +++(eval_expression_static_receiver_writes())::$count; +--(eval_expression_static_receiver_writes())::$count; echo (eval_expression_static_receiver_writes())::$count . "|"; echo (eval_expression_static_receiver_writes())::$items[0] . ":"; echo (eval_expression_static_receiver_writes())::$items[1];'); From 667dd632577082c403ea3b7c1caeb946c1b3d0fe Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 11:53:54 +0200 Subject: [PATCH 0748/1208] Support eval dynamic static property names --- crates/elephc-magician/src/eval_ir.rs | 26 ++++ .../src/interpreter/dynamic_functions.rs | 4 + .../src/interpreter/expressions.rs | 9 ++ .../src/interpreter/statements.rs | 84 +++++++++++++ crates/elephc-magician/src/lexer/scan.rs | 4 + crates/elephc-magician/src/lexer/token.rs | 1 + .../elephc-magician/src/parser/expressions.rs | 18 +++ .../elephc-magician/src/parser/statements.rs | 111 +++++++++++++++++- .../src/parser/tests/static_members.rs | 77 ++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 40 +++++++ 11 files changed, 370 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 2d485f1bcb..0e4429cafc 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -241,6 +241,28 @@ pub enum EvalStmt { property: String, increment: bool, }, + DynamicStaticPropertyNameSet { + class_name: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicStaticPropertyNameArrayAppend { + class_name: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicStaticPropertyNameArraySet { + class_name: EvalExpr, + property: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + DynamicStaticPropertyNameIncDec { + class_name: EvalExpr, + property: EvalExpr, + increment: bool, + }, StaticVar { name: String, init: EvalExpr, @@ -2401,6 +2423,10 @@ pub enum EvalExpr { class_name: Box, property: String, }, + DynamicStaticPropertyNameGet { + class_name: Box, + property: Box, + }, DynamicClassConstantFetch { class_name: Box, constant: String, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index f868247ab1..fff97f87d7 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1137,6 +1137,10 @@ fn visit_static_var_declarations( | EvalStmt::DynamicStaticPropertyArrayAppend { .. } | EvalStmt::DynamicStaticPropertyArraySet { .. } | EvalStmt::DynamicStaticPropertyIncDec { .. } + | EvalStmt::DynamicStaticPropertyNameArrayAppend { .. } + | EvalStmt::DynamicStaticPropertyNameArraySet { .. } + | EvalStmt::DynamicStaticPropertyNameIncDec { .. } + | EvalStmt::DynamicStaticPropertyNameSet { .. } | EvalStmt::DynamicStaticPropertySet { .. } | EvalStmt::PropertyReferenceBind { .. } | EvalStmt::PropertyArrayAppend { .. } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index ac5c2a04df..cb43f4cc55 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -87,6 +87,15 @@ pub(in crate::interpreter) fn eval_expr( let class_name = eval_dynamic_class_name(class_name, context, values)?; eval_static_property_get_result(&class_name, property, context, values) } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_get_result(&class_name, &property, context, values) + } EvalExpr::DynamicClassConstantFetch { class_name, constant, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 21c85a5427..5011ad6b79 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -389,6 +389,75 @@ pub(in crate::interpreter) fn execute_stmt( )?; Ok(EvalControl::None) } + EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(&class_name, &property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_array_append_result( + &class_name, + &property, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameArraySet { + class_name, + property, + index, + op, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_array_set_result( + &class_name, + &property, + index, + *op, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name, + property, + increment, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_inc_dec_result( + &class_name, + &property, + *increment, + context, + values, + )?; + Ok(EvalControl::None) + } EvalStmt::StoreVar { name, value } => { let value = eval_expr(value, context, scope, values)?; for replaced in set_scope_cell( @@ -637,6 +706,21 @@ fn eval_array_unset_element_stmt( } return Ok(()); } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let array = eval_static_property_get_result(&class_name, &property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(&class_name, &property, array, context, values)?; + } + return Ok(()); + } _ => {} } let array = eval_expr(array, context, scope, values)?; diff --git a/crates/elephc-magician/src/lexer/scan.rs b/crates/elephc-magician/src/lexer/scan.rs index f006dc1e59..e10cf044a0 100644 --- a/crates/elephc-magician/src/lexer/scan.rs +++ b/crates/elephc-magician/src/lexer/scan.rs @@ -318,6 +318,10 @@ impl<'a> Lexer<'a> { /// Reads a `$name` token. fn lex_variable(&mut self) -> Result { self.bump_char(); + if self.peek_char() == Some('{') { + self.bump_char(); + return Ok(TokenKind::DollarLBrace); + } let name = self.lex_ident(); if name.is_empty() { return Err(EvalParseError::ExpectedVariable); diff --git a/crates/elephc-magician/src/lexer/token.rs b/crates/elephc-magician/src/lexer/token.rs index 739d653ff0..3c7726beaa 100644 --- a/crates/elephc-magician/src/lexer/token.rs +++ b/crates/elephc-magician/src/lexer/token.rs @@ -44,6 +44,7 @@ impl Token { /// Token kinds used by the initial eval fragment parser. #[derive(Debug, Clone, PartialEq)] pub(crate) enum TokenKind { + DollarLBrace, DollarIdent(String), Ident(String), Magic(EvalMagicConst), diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 46bd0dd24f..c3020508e6 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -832,6 +832,15 @@ impl Parser { property, }) } + TokenKind::DollarLBrace => { + self.advance(); + let property = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + Ok(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + property: Box::new(property), + }) + } TokenKind::Ident(name) if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => { @@ -899,6 +908,15 @@ impl Parser { property: member, }) } + TokenKind::DollarLBrace => { + self.advance(); + let property = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + Ok(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(class_name), + property: Box::new(property), + }) + } TokenKind::Ident(name) if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index be5cba5afe..5ccce5b16f 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2851,12 +2851,20 @@ impl Parser { let increment = matches!(self.current(), TokenKind::PlusPlus); self.advance(); if let TokenKind::DollarIdent(name) = self.current() { - let name = name.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; + if !matches!( + self.peek(), + TokenKind::DoubleColon + | TokenKind::Arrow + | TokenKind::QuestionArrow + | TokenKind::LBracket + ) { + let name = name.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![inc_dec_store(name, increment)]); } - return Ok(vec![inc_dec_store(name, increment)]); } let target = self.parse_expr()?; if require_semicolon { @@ -3006,6 +3014,32 @@ impl Parser { value, }]) } + ( + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + }, + op, + ) => { + let class_name = *class_name; + let property = *property; + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(class_name.clone()), + property: Box::new(property.clone()), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + }]) + } _ => Err(EvalParseError::UnexpectedToken), } } @@ -3352,6 +3386,14 @@ fn property_inc_dec_stmt(target: EvalExpr, increment: bool) -> Result Ok(EvalStmt::DynamicStaticPropertyNameIncDec { + class_name: *class_name, + property: *property, + increment, + }), _ => Err(EvalParseError::UnexpectedToken), } } @@ -3379,6 +3421,14 @@ fn property_array_append_stmt(target: EvalExpr, value: EvalExpr) -> Result Ok(EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name: *class_name, + property: *property, + value, + }), _ => Err(EvalParseError::UnexpectedToken), } } @@ -3415,6 +3465,16 @@ fn property_array_set_stmt( op, value, }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameArraySet { + class_name: *class_name, + property: *property, + index, + op, + value, + }), _ => Err(EvalParseError::UnexpectedToken), } } @@ -3664,6 +3724,40 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { EvalStmt::DynamicStaticPropertyIncDec { class_name, .. } => { eval_expr_uses_this_property(class_name, property_name) } + EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + } + | EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name, + property, + value, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyNameArraySet { + class_name, + property, + index, + value, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name, + property, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } EvalStmt::StaticPropertySet { value, .. } | EvalStmt::StaticPropertyArrayAppend { value, .. } | EvalStmt::StoreVar { value, .. } => { @@ -3751,6 +3845,13 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { | EvalExpr::DynamicClassNameFetch { class_name } => { eval_expr_uses_this_property(class_name, property_name) } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } EvalExpr::DynamicClassConstantNameFetch { class_name, constant, diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index 8ef84c40a5..f6e8684d2b 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -285,6 +285,83 @@ fn parse_fragment_accepts_expression_static_receiver_property_writes() { ); } +/// Verifies `${...}` static property names parse as runtime-name expressions. +#[test] +fn parse_fragment_accepts_dynamic_static_property_name_expressions() { + let program = parse_fragment( + br#"return [EvalStaticBox::${$property}, $class::${name_expr()}, (factory())::${"items"}];"#, + ) + .expect("fragment should parse"); + let name_expr_call = || EvalExpr::Call { + name: "name_expr".to_string(), + args: Vec::new(), + }; + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + property: Box::new(EvalExpr::LoadVar("property".to_string())), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: Box::new(name_expr_call()), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(factory_call()), + property: Box::new(EvalExpr::Const(EvalConst::String("items".to_string()))), + }), + ])))] + ); +} + +/// Verifies `${...}` static property names lower write-like statements. +#[test] +fn parse_fragment_accepts_dynamic_static_property_name_writes() { + let program = parse_fragment( + br#"EvalStaticBox::${$property} = 2; +++$class::${name_expr()}; +(factory())::${"items"}[] = 4;"#, + ) + .expect("fragment should parse"); + let name_expr_call = || EvalExpr::Call { + name: "name_expr".to_string(), + args: Vec::new(), + }; + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertyNameSet { + class_name: EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }, + property: EvalExpr::LoadVar("property".to_string()), + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name: EvalExpr::LoadVar("class".to_string()), + property: name_expr_call(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name: factory_call(), + property: EvalExpr::Const(EvalConst::String("items".to_string())), + value: EvalExpr::Const(EvalConst::Int(4)), + }, + ] + ); +} + /// Verifies runtime-valued static receivers support properties, constants, and `::class`. #[test] fn parse_fragment_accepts_dynamic_static_metadata_receiver() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 3a8e79d3c8..7b7fcf226c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,7 +71,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, array writes/appends, and increment/decrement, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 992c5e4973..6703aecbf5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10223,6 +10223,46 @@ echo (eval_expression_static_receiver_writes())::$items[1];'); assert_eq!(out.stdout, "6|5:4"); } +/// Verifies eval supports `${...}` static property names for reads and writes. +#[test] +fn test_eval_dynamic_static_property_name_expressions() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 12:18:16 +0200 Subject: [PATCH 0749/1208] Support eval object property reference binding --- crates/elephc-magician/src/eval_ir.rs | 5 ++ .../src/interpreter/dynamic_functions.rs | 1 + .../src/interpreter/statements.rs | 48 +++++++++++++++++-- .../elephc-magician/src/parser/statements.rs | 40 ++++++++++++++++ .../src/parser/tests/arrays_objects.rs | 23 +++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 30 ++++++++++++ 7 files changed, 145 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 0e4429cafc..ac279101c7 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -141,6 +141,11 @@ pub enum EvalStmt { property: String, source: String, }, + DynamicPropertyReferenceBind { + object: EvalExpr, + property: EvalExpr, + source: String, + }, DynamicPropertySet { object: EvalExpr, property: EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index fff97f87d7..38f2ce9874 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1133,6 +1133,7 @@ fn visit_static_var_declarations( | EvalStmt::DynamicPropertyArraySet { .. } | EvalStmt::DynamicPropertyCompoundAssign { .. } | EvalStmt::DynamicPropertyIncDec { .. } + | EvalStmt::DynamicPropertyReferenceBind { .. } | EvalStmt::DynamicPropertySet { .. } | EvalStmt::DynamicStaticPropertyArrayAppend { .. } | EvalStmt::DynamicStaticPropertyArraySet { .. } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 5011ad6b79..b2e9ef49cd 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -166,6 +166,23 @@ pub(in crate::interpreter) fn execute_stmt( eval_property_reference_bind_result(object, property, source, context, scope, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicPropertyReferenceBind { + object, + property, + source, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_reference_bind_result( + object, + &property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } EvalStmt::DynamicPropertySet { object, property, @@ -3796,7 +3813,14 @@ fn eval_property_reference_bind_result( return Err(EvalStatus::RuntimeFatal); } let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); - let target = eval_property_reference_target(source_name, context, scope, values)?; + let target = eval_property_reference_target( + identity, + &storage_property_name, + source_name, + context, + scope, + values, + )?; let value = eval_reference_target_value(&target, context, values)?; context.bind_dynamic_property_alias(identity, &storage_property_name, target); values.property_set(object, &storage_property_name, value)?; @@ -3806,6 +3830,8 @@ fn eval_property_reference_bind_result( /// Resolves a local by-reference source into a persistent property alias target. fn eval_property_reference_target( + object_identity: u64, + storage_property_name: &str, source_name: &str, context: &ElephcEvalContext, scope: &mut ElephcEvalScope, @@ -3814,8 +3840,24 @@ fn eval_property_reference_target( if let Some(target) = scope.reference_target(source_name).cloned() { return Ok(target); } - let cell = visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; - Ok(EvalReferenceTarget::Cell { cell }) + if context.current_function().is_some() { + let cell = + visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; + return Ok(EvalReferenceTarget::Cell { cell }); + } + let alias_name = eval_property_reference_alias_name(object_identity, storage_property_name); + for replaced in set_reference_alias(context, scope, &alias_name, source_name, values)? { + values.release(replaced)?; + } + Ok(EvalReferenceTarget::Variable { + scope: scope as *mut ElephcEvalScope, + name: alias_name, + }) +} + +/// Builds the hidden scope variable name that backs one property reference alias. +fn eval_property_reference_alias_name(object_identity: u64, storage_property_name: &str) -> String { + format!("\0elephc_property_ref:{object_identity}:{storage_property_name}") } /// Reads the current value from a persistent reference target. diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 5ccce5b16f..cc78bc8ab6 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2953,6 +2953,17 @@ impl Parser { return Ok(vec![EvalStmt::Expr(target)]); }; self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return property_reference_bind_stmt(target, source).map(|stmt| vec![stmt]); + } let value = self.parse_expr()?; if require_semicolon { self.expect_semicolon()?; @@ -3365,6 +3376,28 @@ fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { .eq_ignore_ascii_case("static") } +/// Builds a property by-reference binding statement from a parsed property target. +fn property_reference_bind_stmt( + target: EvalExpr, + source: String, +) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyReferenceBind { + object: *object, + property, + source, + }), + EvalExpr::DynamicPropertyGet { object, property } => { + Ok(EvalStmt::DynamicPropertyReferenceBind { + object: *object, + property: *property, + source, + }) + } + _ => Err(EvalParseError::UnexpectedToken), + } +} + /// Builds an object-property increment/decrement statement from a parsed property target. fn property_inc_dec_stmt(target: EvalExpr, increment: bool) -> Result { match target { @@ -3614,6 +3647,13 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { eval_is_this_property(object, property, property_name) || eval_expr_uses_this_property(object, property_name) } + EvalStmt::DynamicPropertyReferenceBind { + object, property, .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } EvalStmt::UnsetDynamicProperty { object, property } => { eval_is_this_dynamic_property(object, property, property_name) || eval_expr_uses_this_property(object, property_name) diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index 2277d533e0..6b5189394c 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -409,6 +409,29 @@ fn parse_fragment_accepts_property_write_source() { ); } +/// Verifies object property reference bindings parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_reference_bind_source() { + let program = + parse_fragment(br#"$this->x =& $source; $this->{$name} =& $other;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + source: "source".to_string(), + }, + EvalStmt::DynamicPropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + source: "other".to_string(), + }, + ] + ); +} + /// Verifies object property array writes and appends parse as dedicated EvalIR statements. #[test] fn parse_fragment_accepts_property_array_write_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 7b7fcf226c..21d5083fee 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -45,7 +45,7 @@ such a local alias removes the alias without unsetting the global value. | Comments | PHP comments are accepted inside fragments. | | Output | `echo` supports comma-separated arguments. `print` is an expression. | | Variables | Reads, writes, by-name assignment, by-reference assignment, `unset()`, `isset()`, and `empty()` are supported. | -| Assignment forms | Simple variable assignment, compound assignment for variables, object properties, and static properties (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | +| Assignment forms | Simple variable assignment, by-reference variable and eval object-property binding, compound assignment for variables, object properties, and static properties (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6703aecbf5..e7911ef2c8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3458,6 +3458,36 @@ echo empty($missing?->{eval_dynamic_write_bad()}) ? "nullempty" : "bad";'); ); } +/// Verifies eval-declared object properties can bind to local variables by reference. +#[test] +fn test_eval_declared_property_reference_binding() { + let out = compile_and_run( + r#"value =& $source; +$source = "B"; +echo $box->value . "|"; +$box->value = "C"; +echo $source . "|"; + +$name = "other"; +$dynamic = "D"; +$box->{$name} =& $dynamic; +$dynamic = "E"; +echo $box->other . "|"; +$box->{$name} = "F"; +echo $dynamic;'); +"#, + ); + assert_eq!(out, "B|C|E|F"); +} + /// Verifies eval object property increment/decrement works with named and dynamic properties. #[test] fn test_eval_object_property_inc_dec_statements() { From 6829348ab81e5a073405ecb27f9d9e781464f3be Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 12:30:07 +0200 Subject: [PATCH 0750/1208] Support eval static property reference binding --- crates/elephc-magician/src/context.rs | 24 +++ crates/elephc-magician/src/eval_ir.rs | 15 ++ .../src/interpreter/dynamic_functions.rs | 3 + .../src/interpreter/statements.rs | 199 +++++++++++++++++- .../elephc-magician/src/parser/statements.rs | 61 +++++- .../src/parser/tests/static_members.rs | 42 ++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 38 ++++ 8 files changed, 379 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index ebc407944c..dee9a863d5 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -422,6 +422,7 @@ pub struct ElephcEvalContext { native_property_attributes: HashMap<(String, String), Vec>, static_locals: HashMap<(String, String), RuntimeCellHandle>, static_properties: HashMap<(String, String), RuntimeCellHandle>, + static_property_aliases: HashMap<(String, String), EvalReferenceTarget>, class_constants: HashMap<(String, String), RuntimeCellHandle>, included_files: HashSet, dynamic_objects: HashMap, @@ -482,6 +483,7 @@ impl ElephcEvalContext { native_property_attributes: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), + static_property_aliases: HashMap::new(), class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), @@ -543,6 +545,7 @@ impl ElephcEvalContext { native_property_attributes: HashMap::new(), static_locals: HashMap::new(), static_properties: HashMap::new(), + static_property_aliases: HashMap::new(), class_constants: HashMap::new(), included_files: HashSet::new(), dynamic_objects: HashMap::new(), @@ -2539,6 +2542,27 @@ impl ElephcEvalContext { previous.filter(|previous| *previous != cell) } + /// Binds one eval static property slot to a persistent PHP reference target. + pub fn bind_static_property_alias( + &mut self, + class_name: &str, + name: &str, + target: EvalReferenceTarget, + ) -> Option { + self.static_property_aliases + .insert((normalize_class_name(class_name), name.to_string()), target) + } + + /// Returns the persistent reference target bound to one eval static property slot. + pub fn static_property_alias( + &self, + class_name: &str, + name: &str, + ) -> Option<&EvalReferenceTarget> { + self.static_property_aliases + .get(&(normalize_class_name(class_name), name.to_string())) + } + /// Returns a materialized eval class constant cell. pub fn class_constant_cell(&self, class_name: &str, name: &str) -> Option { self.class_constants diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index ac279101c7..c8a577fd2f 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -207,6 +207,11 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + StaticPropertyReferenceBind { + class_name: String, + property: String, + source: String, + }, StaticPropertyArrayAppend { class_name: String, property: String, @@ -229,6 +234,11 @@ pub enum EvalStmt { property: String, value: EvalExpr, }, + DynamicStaticPropertyReferenceBind { + class_name: EvalExpr, + property: String, + source: String, + }, DynamicStaticPropertyArrayAppend { class_name: EvalExpr, property: String, @@ -251,6 +261,11 @@ pub enum EvalStmt { property: EvalExpr, value: EvalExpr, }, + DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr, + property: EvalExpr, + source: String, + }, DynamicStaticPropertyNameArrayAppend { class_name: EvalExpr, property: EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 38f2ce9874..128a19c1c4 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1138,9 +1138,11 @@ fn visit_static_var_declarations( | EvalStmt::DynamicStaticPropertyArrayAppend { .. } | EvalStmt::DynamicStaticPropertyArraySet { .. } | EvalStmt::DynamicStaticPropertyIncDec { .. } + | EvalStmt::DynamicStaticPropertyReferenceBind { .. } | EvalStmt::DynamicStaticPropertyNameArrayAppend { .. } | EvalStmt::DynamicStaticPropertyNameArraySet { .. } | EvalStmt::DynamicStaticPropertyNameIncDec { .. } + | EvalStmt::DynamicStaticPropertyNameReferenceBind { .. } | EvalStmt::DynamicStaticPropertyNameSet { .. } | EvalStmt::DynamicStaticPropertySet { .. } | EvalStmt::PropertyReferenceBind { .. } @@ -1154,6 +1156,7 @@ fn visit_static_var_declarations( | EvalStmt::StaticPropertyArrayAppend { .. } | EvalStmt::StaticPropertyArraySet { .. } | EvalStmt::StaticPropertyIncDec { .. } + | EvalStmt::StaticPropertyReferenceBind { .. } | EvalStmt::StaticPropertySet { .. } | EvalStmt::StoreVar { .. } | EvalStmt::Throw(_) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b2e9ef49cd..14e86d22cd 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -309,6 +309,16 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_set_result(class_name, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::StaticPropertyReferenceBind { + class_name, + property, + source, + } => { + eval_static_property_reference_bind_result( + class_name, property, source, context, scope, values, + )?; + Ok(EvalControl::None) + } EvalStmt::StaticPropertyArrayAppend { class_name, property, @@ -352,6 +362,23 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_set_result(&class_name, property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicStaticPropertyReferenceBind { + class_name, + property, + source, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_reference_bind_result( + &class_name, + property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } EvalStmt::DynamicStaticPropertyArrayAppend { class_name, property, @@ -418,6 +445,24 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_set_result(&class_name, &property, value, context, values)?; Ok(EvalControl::None) } + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name, + property, + source, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_reference_bind_result( + &class_name, + &property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } EvalStmt::DynamicStaticPropertyNameArrayAppend { class_name, property, @@ -4640,6 +4685,12 @@ pub(in crate::interpreter) fn eval_static_property_get_result( values, ); } + if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } if let Some(value) = context.static_property(&declaring_class, property.name()) { return Ok(value); } @@ -4711,8 +4762,16 @@ pub(in crate::interpreter) fn eval_static_property_isset_result( if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { return Ok(false); } - let Some(value) = context.static_property(&declaring_class, property.name()) else { - return Ok(false); + let value = if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + eval_reference_target_value(&target, context, values)? + } else { + let Some(value) = context.static_property(&declaring_class, property.name()) else { + return Ok(false); + }; + value }; return Ok(!values.is_null(value)?); } @@ -4928,6 +4987,125 @@ pub(in crate::interpreter) fn eval_class_name_fetch_result( values.string(&class_name) } +/// Binds one eval-declared static property to a by-reference source variable. +fn eval_static_property_reference_bind_result( + class_name: &str, + property_name: &str, + source_name: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_write_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property.name(), + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property.name(), target); + if let Some(replaced) = + context.set_static_property(&declaring_class, property.name(), value) + { + values.release(replaced)?; + } + return Ok(()); + } + if eval_static_member_context_owns_class(&class_name, context) { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) + } else { + eval_throw_class_not_found_error(&class_name, context, values) + } +} + +/// Resolves a local by-reference source into a persistent static-property alias target. +fn eval_static_property_reference_target( + class_name: &str, + property_name: &str, + source_name: &str, + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(target) = scope.reference_target(source_name).cloned() { + return Ok(target); + } + if context.current_function().is_some() { + let cell = + visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; + return Ok(EvalReferenceTarget::Cell { cell }); + } + let alias_name = eval_static_property_reference_alias_name(class_name, property_name); + for replaced in set_reference_alias(context, scope, &alias_name, source_name, values)? { + values.release(replaced)?; + } + Ok(EvalReferenceTarget::Variable { + scope: scope as *mut ElephcEvalScope, + name: alias_name, + }) +} + +/// Builds the hidden scope variable name backing one static-property reference alias. +fn eval_static_property_reference_alias_name(class_name: &str, property_name: &str) -> String { + format!("\0elephc_static_property_ref:{class_name}:{property_name}") +} + +/// Writes one eval static-property assignment through its persistent reference target. +fn eval_static_reference_target_write( + class_name: &str, + property_name: &str, + target: EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if matches!(target, EvalReferenceTarget::Cell { .. }) { + context.bind_static_property_alias( + class_name, + property_name, + EvalReferenceTarget::Cell { cell: value }, + ); + return Ok(()); + } + write_back_method_ref_target(&target, value, context, values) +} + /// Writes one eval-declared static property after resolving the class-like receiver. pub(in crate::interpreter) fn eval_static_property_set_result( class_name: &str, @@ -4962,7 +5140,22 @@ pub(in crate::interpreter) fn eval_static_property_set_result( values, ); } - if let Some(replaced) = context.set_static_property(&declaring_class, property.name(), value) { + if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property.name(), + target, + value, + context, + values, + )?; + } + if let Some(replaced) = + context.set_static_property(&declaring_class, property.name(), value) + { values.release(replaced)?; } return Ok(()); diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index cc78bc8ab6..2e96a3b533 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -2669,6 +2669,21 @@ impl Parser { return Err(EvalParseError::UnexpectedToken); }; self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyReferenceBind { + class_name, + property, + source, + }]); + } let value = self.parse_expr()?; if require_semicolon { self.expect_semicolon()?; @@ -2742,6 +2757,21 @@ impl Parser { return Err(EvalParseError::UnexpectedToken); }; self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyReferenceBind { + class_name, + property, + source, + }]); + } let value = self.parse_expr()?; if require_semicolon { self.expect_semicolon()?; @@ -3394,6 +3424,22 @@ fn property_reference_bind_stmt( source, }) } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyReferenceBind { + class_name: *class_name, + property, + source, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: *class_name, + property: *property, + source, + }), _ => Err(EvalParseError::UnexpectedToken), } } @@ -3662,7 +3708,9 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { EvalStmt::UnsetDynamicStaticProperty { class_name, .. } => { eval_expr_uses_this_property(class_name, property_name) } - EvalStmt::StaticPropertyIncDec { .. } | EvalStmt::UnsetStaticProperty { .. } => false, + EvalStmt::StaticPropertyIncDec { .. } + | EvalStmt::StaticPropertyReferenceBind { .. } + | EvalStmt::UnsetStaticProperty { .. } => false, EvalStmt::DynamicPropertySet { object, property, @@ -3764,6 +3812,9 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { EvalStmt::DynamicStaticPropertyIncDec { class_name, .. } => { eval_expr_uses_this_property(class_name, property_name) } + EvalStmt::DynamicStaticPropertyReferenceBind { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } EvalStmt::DynamicStaticPropertyNameSet { class_name, property, @@ -3798,6 +3849,14 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { eval_expr_uses_this_property(class_name, property_name) || eval_expr_uses_this_property(property, property_name) } + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name, + property, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } EvalStmt::StaticPropertySet { value, .. } | EvalStmt::StaticPropertyArrayAppend { value, .. } | EvalStmt::StoreVar { value, .. } => { diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index f6e8684d2b..fbe217faf2 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -542,6 +542,48 @@ fn parse_fragment_accepts_static_property_compound_assignment() { ); } +/// Verifies static property reference bindings parse for named and dynamic targets. +#[test] +fn parse_fragment_accepts_static_property_reference_bind_source() { + let program = parse_fragment( + br#"EvalStaticBox::$count =& $source; +$class::$count =& $dynamic; +EvalStaticBox::${$name} =& $other; +(factory())::${$name} =& $third;"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertyReferenceBind { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + source: "source".to_string(), + }, + EvalStmt::DynamicStaticPropertyReferenceBind { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + source: "dynamic".to_string(), + }, + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }, + property: EvalExpr::LoadVar("name".to_string()), + source: "other".to_string(), + }, + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }, + property: EvalExpr::LoadVar("name".to_string()), + source: "third".to_string(), + }, + ] + ); +} + /// Verifies indexed static-property writes parse as dedicated EvalIR statements. #[test] fn parse_fragment_accepts_static_property_array_write_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 21d5083fee..8145f88e1a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -45,7 +45,7 @@ such a local alias removes the alias without unsetting the global value. | Comments | PHP comments are accepted inside fragments. | | Output | `echo` supports comma-separated arguments. `print` is an expression. | | Variables | Reads, writes, by-name assignment, by-reference assignment, `unset()`, `isset()`, and `empty()` are supported. | -| Assignment forms | Simple variable assignment, by-reference variable and eval object-property binding, compound assignment for variables, object properties, and static properties (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | +| Assignment forms | Simple variable assignment, by-reference variable, eval object-property, and eval static-property binding, compound assignment for variables, object properties, and static properties (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e7911ef2c8..d9112b9afa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10293,6 +10293,44 @@ echo $class::${eval_dynamic_static_property_items_expression()}[1];'); assert_eq!(out.stdout, "1|6|5:4"); } +/// Verifies eval-declared static properties can bind to local variables by reference. +#[test] +fn test_eval_declared_static_property_reference_binding() { + let out = compile_and_run( + r#" Date: Fri, 26 Jun 2026 12:41:08 +0200 Subject: [PATCH 0751/1208] Support eval static property by-ref call args --- crates/elephc-magician/src/context.rs | 5 ++ .../src/interpreter/dynamic_functions.rs | 64 +++++++++++++++++++ .../src/interpreter/statements.rs | 53 +++++++++++++++ .../src/interpreter/tests/method_arguments.rs | 47 ++++++++++++++ tests/codegen/eval.rs | 51 +++++++++++++++ 5 files changed, 220 insertions(+) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index dee9a863d5..5e3d6b22bb 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -53,6 +53,11 @@ pub enum EvalReferenceTarget { property: String, access_scope: ElephcEvalExecutionScope, }, + StaticProperty { + class_name: String, + property: String, + access_scope: ElephcEvalExecutionScope, + }, Cell { cell: RuntimeCellHandle, }, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 128a19c1c4..3eff4e293a 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -136,10 +136,74 @@ fn eval_call_arg_value( }), )) } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => { + let access_scope = context.execution_scope(); + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + eval_static_property_call_arg_value( + class_name, + property.clone(), + access_scope, + context, + values, + ) + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => { + let access_scope = context.execution_scope(); + let class_name = eval_expr(class_name, context, caller_scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_call_arg_value( + class_name, + property.clone(), + access_scope, + context, + values, + ) + } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + let access_scope = context.execution_scope(); + let class_name = eval_expr(class_name, context, caller_scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, caller_scope, values)?; + eval_static_property_call_arg_value( + class_name, + property, + access_scope, + context, + values, + ) + } _ => eval_expr(expr, context, caller_scope, values).map(|value| (value, None)), } } +/// Evaluates one static-property lvalue and records it as a by-reference call target. +fn eval_static_property_call_arg_value( + class_name: String, + property: String, + access_scope: ElephcEvalExecutionScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let value = eval_static_property_get_result(&class_name, &property, context, values)?; + Ok(( + value, + Some(EvalReferenceTarget::StaticProperty { + class_name, + property, + access_scope, + }), + )) +} + /// Converts a `call_user_func_array` argument array into ordered call arguments. pub(in crate::interpreter) fn eval_array_call_arg_values( arg_array: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 14e86d22cd..ba5f63eb69 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3940,6 +3940,16 @@ fn eval_reference_target_value( context.replace_execution_scope(previous_scope); result } + EvalReferenceTarget::StaticProperty { + class_name, + property, + access_scope, + } => { + let previous_scope = context.replace_execution_scope(access_scope.clone()); + let result = eval_static_property_get_result(class_name, property, context, values); + context.replace_execution_scope(previous_scope); + result + } EvalReferenceTarget::Cell { cell } => Ok(*cell), } } @@ -6933,6 +6943,22 @@ fn same_method_ref_target(left: &EvalReferenceTarget, right: &EvalReferenceTarge EvalReferenceTarget::Cell { cell: left_cell }, EvalReferenceTarget::Cell { cell: right_cell }, ) => left_cell == right_cell, + ( + EvalReferenceTarget::StaticProperty { + class_name: left_class_name, + property: left_property, + access_scope: left_access_scope, + }, + EvalReferenceTarget::StaticProperty { + class_name: right_class_name, + property: right_property, + access_scope: right_access_scope, + }, + ) => { + left_class_name == right_class_name + && left_property == right_property + && left_access_scope == right_access_scope + } _ => false, } } @@ -7037,6 +7063,18 @@ fn write_back_method_ref_target( context, values, ), + EvalReferenceTarget::StaticProperty { + class_name, + property, + access_scope, + } => write_back_method_static_property_ref_target( + class_name, + property, + access_scope.clone(), + value, + context, + values, + ), EvalReferenceTarget::Cell { .. } => Ok(()), } } @@ -7085,6 +7123,21 @@ fn write_back_method_object_property_ref_target( result } +/// Stores one by-reference method result in a caller-side static property. +fn write_back_method_static_property_ref_target( + class_name: &str, + property: &str, + access_scope: ElephcEvalExecutionScope, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let previous_scope = context.replace_execution_scope(access_scope); + let result = eval_static_property_set_result(class_name, property, value, context, values); + context.replace_execution_scope(previous_scope); + result +} + /// Creates an indexed or associative array according to the first write key. fn eval_new_array_for_index( index: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs index 7879cbe885..7753f45a07 100644 --- a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs @@ -328,6 +328,53 @@ return $private->update($changer);"#, assert_eq!(values.get(result), FakeValue::String("secret".to_string())); } +/// Verifies eval-declared by-reference method params write back static-property lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_static_properties() { + let program = parse_fragment( + br#"class EvalByRefStaticPropertyChanger { + public function set(&$value, $next) { + $value = $next; + } + public function pair(&$left, &$right) { + $left = "left"; + $right = "right"; + return $left; + } +} +class EvalByRefStaticPropertyBox { + public static string $value = "old"; + public static string $other = "second"; + public static string $third = "third"; + private static string $secret = "private"; + public static function updatePrivate($changer) { + $changer->set(self::$secret, "secret"); + return self::$secret; + } +} +$changer = new EvalByRefStaticPropertyChanger(); +$changer->set(EvalByRefStaticPropertyBox::$value, "changed"); +echo $changer->pair(EvalByRefStaticPropertyBox::$value, EvalByRefStaticPropertyBox::$value); +echo ":"; +echo EvalByRefStaticPropertyBox::$value; echo ":"; +$class = "EvalByRefStaticPropertyBox"; +$changer->set($class::$other, "dynamic"); +$name = "third"; +$changer->set($class::${$name}, "name"); +echo EvalByRefStaticPropertyBox::$other; echo ":"; +echo EvalByRefStaticPropertyBox::$third; echo ":"; +return EvalByRefStaticPropertyBox::updatePrivate($changer);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "right:right:dynamic:name:"); + assert_eq!(values.get(result), FakeValue::String("secret".to_string())); +} + /// Verifies eval-declared by-reference method params keep property access restrictions. #[test] fn execute_program_rejects_invalid_eval_method_by_ref_object_property_targets() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d9112b9afa..ebacb8fabe 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9409,6 +9409,57 @@ echo $ctor . ":" . $value . ":" . $named . ":" . $items["k"] . ":" . $prop->valu ); } +/// Verifies eval-declared methods can mutate eval static properties passed by reference. +#[test] +fn test_eval_declared_method_by_ref_static_property_arguments() { + let out = compile_and_run_capture( + r#"set(self::$secret, "secret"); + return self::$secret; + } +} +$changer = new EvalByRefStaticPropertyChanger(); +$changer->set(EvalByRefStaticPropertyBox::$value, "changed"); +echo $changer->pair(EvalByRefStaticPropertyBox::$value, EvalByRefStaticPropertyBox::$value) . ":"; +echo EvalByRefStaticPropertyBox::$value . ":"; +$class = "EvalByRefStaticPropertyBox"; +$changer->set($class::$other, "dynamic"); +$name = "third"; +$changer->set($class::${$name}, "name"); +echo EvalByRefStaticPropertyBox::$other . ":"; +echo EvalByRefStaticPropertyBox::$third . ":"; +echo EvalByRefStaticPropertyBox::updatePrivate($changer) . ":"; +$changer->set(EvalAotByRefStaticPropertyBox::$value, "aot-changed"); +echo EvalAotByRefStaticPropertyBox::$value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "right:right:dynamic:name:secret:aot-changed"); +} + /// Verifies eval dynamic static callables dispatch eval-declared static methods. #[test] fn test_eval_declared_static_method_dynamic_callables() { From 3f935dba01fd18e2d071183a8b842414b53a2330 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 12:46:22 +0200 Subject: [PATCH 0752/1208] Support eval dynamic property by-ref call args --- .../src/interpreter/dynamic_functions.rs | 15 +++++++++++++++ .../src/interpreter/tests/method_arguments.rs | 7 +++++-- tests/codegen/eval.rs | 4 +++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 3eff4e293a..863af0dfac 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -136,6 +136,21 @@ fn eval_call_arg_value( }), )) } + EvalExpr::DynamicPropertyGet { object, property } => { + let access_scope = context.execution_scope(); + let object = eval_expr(object, context, caller_scope, values)?; + let property = eval_dynamic_member_name(property, context, caller_scope, values)?; + let value = eval_property_get_result(object, &property, context, values)?; + validate_property_ref_target(object, &property, context, values)?; + Ok(( + value, + Some(EvalReferenceTarget::ObjectProperty { + object, + property, + access_scope, + }), + )) + } EvalExpr::StaticPropertyGet { class_name, property, diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs index 7753f45a07..b50584d3e0 100644 --- a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs @@ -306,7 +306,8 @@ class EvalByRefPublicPropertyBox { class EvalByRefPrivatePropertyBox { private string $value = "private"; public function update($changer) { - $changer->set($this->value, "secret"); + $name = "value"; + $changer->set($this->{$name}, "secret"); return $this->value; } } @@ -314,6 +315,8 @@ $changer = new EvalByRefPropertyChanger(); $public = new EvalByRefPublicPropertyBox(); $changer->set($public->value, "changed"); $changer->variadic($public->value); +$name = "value"; +$changer->set($public->{$name}, "dynamic"); echo $public->value; echo ":"; $private = new EvalByRefPrivatePropertyBox(); return $private->update($changer);"#, @@ -324,7 +327,7 @@ return $private->update($changer);"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "variadic:"); + assert_eq!(values.output, "dynamic:"); assert_eq!(values.get(result), FakeValue::String("secret".to_string())); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ebacb8fabe..a281a760ae 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9395,6 +9395,8 @@ $items = ["k" => "C"]; $box->change($items["k"]); $prop = new EvalByRefPropertyBox(); $box->change($prop->value); +$propName = "value"; +$box->change($prop->{$propName}); echo $ctor . ":" . $value . ":" . $named . ":" . $items["k"] . ":" . $prop->value;'); "#, ); @@ -9405,7 +9407,7 @@ echo $ctor . ":" . $value . ":" . $named . ":" . $items["k"] . ":" . $prop->valu ); assert_eq!( out.stdout, - "Z-ctor:A-method-static-variadic:B-named:C-method:D-method" + "Z-ctor:A-method-static-variadic:B-named:C-method:D-method-method" ); } From 96f70a9641d3cd94d9dd3d1217ac49866f336c51 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 12:51:17 +0200 Subject: [PATCH 0753/1208] Support eval dynamic static property probes --- .../src/interpreter/builtins/symbols.rs | 24 ++++++++++++++ .../src/interpreter/tests/static_members.rs | 32 +++++++++++++++++++ tests/codegen/eval.rs | 13 ++++++-- 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 10632654f1..f955191d4b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -709,6 +709,20 @@ pub(in crate::interpreter) fn eval_empty_arg( let value = eval_static_property_get_result(&class_name, property, context, values)?; return Ok(!values.truthy(value)?); } + if let EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_static_property_isset_result(&class_name, &property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(&class_name, &property, context, values)?; + return Ok(!values.truthy(value)?); + } if let EvalExpr::ArrayGet { array, index } = arg { let array = eval_expr(array, context, scope, values)?; let index = eval_expr(index, context, scope, values)?; @@ -775,6 +789,16 @@ pub(in crate::interpreter) fn eval_isset_arg( let class_name = eval_dynamic_class_name(class_name, context, values)?; return eval_static_property_isset_result(&class_name, property, context, values); } + if let EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_static_property_isset_result(&class_name, &property, context, values); + } if let EvalExpr::ArrayGet { array, index } = arg { let array = eval_expr(array, context, scope, values)?; let index = eval_expr(index, context, scope, values)?; diff --git a/crates/elephc-magician/src/interpreter/tests/static_members.rs b/crates/elephc-magician/src/interpreter/tests/static_members.rs index 6480550ae2..839b34b0a6 100644 --- a/crates/elephc-magician/src/interpreter/tests/static_members.rs +++ b/crates/elephc-magician/src/interpreter/tests/static_members.rs @@ -66,6 +66,38 @@ return EvalStaticBase::baseRead();"#, assert_eq!(values.get(result), FakeValue::Int(5)); } +/// Verifies `isset()` and `empty()` support dynamic static property-name expressions. +#[test] +fn execute_program_probes_dynamic_static_property_names() { + let program = parse_fragment( + br#"class EvalStaticNameProbe { + public static $nullish = null; + public static $empty = ""; + public static $value = "x"; +} +$class = "EvalStaticNameProbe"; +$valueName = "value"; +$nullName = "nullish"; +$emptyName = "empty"; +$missingName = "missing"; +echo isset($class::${$valueName}) ? "set" : "bad"; echo ":"; +echo isset($class::${$nullName}) ? "bad" : "null"; echo ":"; +echo empty($class::${$emptyName}) ? "empty" : "bad"; echo ":"; +echo empty($class::${$valueName}) ? "bad" : "value"; echo ":"; +echo isset($class::${$missingName}) ? "bad" : "missing"; echo ":"; +echo empty($class::${$missingName}) ? "missing-empty" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "set:null:empty:value:missing:missing-empty"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies private static property access from global eval scope throws Error. #[test] fn execute_program_private_eval_static_property_from_global_scope_throws_error() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a281a760ae..089260a5c7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9762,13 +9762,22 @@ echo empty($evalClass::$empty) ? "eval-empty" : "bad"; echo "|"; echo empty($evalClass::$value) ? "bad" : "eval-value"; echo "|"; echo isset($evalClass::$missing) ? "bad" : "eval-missing"; echo "|"; echo empty($evalClass::$missing) ? "eval-missing-empty" : "bad"; echo "|"; +$propName = "value"; +$emptyName = "empty"; +$missingName = "missing"; +echo isset($evalClass::${$propName}) ? "eval-name-set" : "bad"; echo "|"; +echo empty($evalClass::${$emptyName}) ? "eval-name-empty" : "bad"; echo "|"; +echo empty($evalClass::${$missingName}) ? "eval-name-missing-empty" : "bad"; echo "|"; $aotClass = "EvalAotStaticProbe"; echo isset($aotClass::$value) ? "aot-set" : "bad"; echo "|"; echo isset($aotClass::$nullish) ? "bad" : "aot-null"; echo "|"; echo empty($aotClass::$empty) ? "aot-empty" : "bad"; echo "|"; echo empty($aotClass::$value) ? "bad" : "aot-value"; echo "|"; echo isset($aotClass::$missing) ? "bad" : "aot-missing"; echo "|"; -echo empty($aotClass::$missing) ? "aot-missing-empty" : "bad";'); +echo empty($aotClass::$missing) ? "aot-missing-empty" : "bad"; echo "|"; +echo isset($aotClass::${$propName}) ? "aot-name-set" : "bad"; echo "|"; +echo empty($aotClass::${$emptyName}) ? "aot-name-empty" : "bad"; echo "|"; +echo empty($aotClass::${$missingName}) ? "aot-name-missing-empty" : "bad";'); "#, ); assert!( @@ -9778,7 +9787,7 @@ echo empty($aotClass::$missing) ? "aot-missing-empty" : "bad";'); ); assert_eq!( out.stdout, - "nominal|eval-set|eval-null|eval-empty|eval-value|eval-missing|eval-missing-empty|aot-set|aot-null|aot-empty|aot-value|aot-missing|aot-missing-empty" + "nominal|eval-set|eval-null|eval-empty|eval-value|eval-missing|eval-missing-empty|eval-name-set|eval-name-empty|eval-name-missing-empty|aot-set|aot-null|aot-empty|aot-value|aot-missing|aot-missing-empty|aot-name-set|aot-name-empty|aot-name-missing-empty" ); } From 47c61de71c3283fd99262553e972e8e3cdf49441 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 12:56:44 +0200 Subject: [PATCH 0754/1208] Support eval dynamic static property unset --- crates/elephc-magician/src/eval_ir.rs | 4 ++++ .../src/interpreter/dynamic_functions.rs | 1 + .../src/interpreter/statements.rs | 10 ++++++++++ crates/elephc-magician/src/parser/statements.rs | 14 ++++++++++++++ .../src/parser/tests/static_members.rs | 17 ++++++++++++----- tests/codegen/eval.rs | 15 ++++++++++++++- 6 files changed, 55 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index c8a577fd2f..a46aaada21 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -321,6 +321,10 @@ pub enum EvalStmt { class_name: EvalExpr, property: String, }, + UnsetDynamicStaticPropertyName { + class_name: EvalExpr, + property: EvalExpr, + }, UnsetVar { name: String, }, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 863af0dfac..ab5669c507 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1243,6 +1243,7 @@ fn visit_static_var_declarations( | EvalStmt::UnsetArrayElement { .. } | EvalStmt::UnsetDynamicProperty { .. } | EvalStmt::UnsetDynamicStaticProperty { .. } + | EvalStmt::UnsetDynamicStaticPropertyName { .. } | EvalStmt::UnsetProperty { .. } | EvalStmt::UnsetStaticProperty { .. } | EvalStmt::UnsetVar { .. } => {} diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index ba5f63eb69..e861491c8d 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -579,6 +579,16 @@ pub(in crate::interpreter) fn execute_stmt( eval_static_property_unset_result(&class_name, property, context, values)?; Ok(EvalControl::None) } + EvalStmt::UnsetDynamicStaticPropertyName { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_unset_result(&class_name, &property, context, values)?; + Ok(EvalControl::None) + } EvalStmt::UnsetVar { name } => { if let Some(replaced) = unset_scope_cell(scope, name.clone()) { eval_release_value(context, values, replaced)?; diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 2e96a3b533..18906bf465 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3208,6 +3208,13 @@ impl Parser { class_name: *class_name, property, }, + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => EvalStmt::UnsetDynamicStaticPropertyName { + class_name: *class_name, + property: *property, + }, _ => return Err(EvalParseError::ExpectedVariable), }; statements.push(stmt); @@ -3708,6 +3715,13 @@ fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { EvalStmt::UnsetDynamicStaticProperty { class_name, .. } => { eval_expr_uses_this_property(class_name, property_name) } + EvalStmt::UnsetDynamicStaticPropertyName { + class_name, + property, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } EvalStmt::StaticPropertyIncDec { .. } | EvalStmt::StaticPropertyReferenceBind { .. } | EvalStmt::UnsetStaticProperty { .. } => false, diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index fbe217faf2..a9718b54a7 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -441,13 +441,20 @@ fn parse_fragment_accepts_static_property_unset() { /// Verifies runtime-valued static property unsets preserve the class receiver expression. #[test] fn parse_fragment_accepts_dynamic_static_property_unset() { - let program = parse_fragment(br#"unset($class::$count);"#).expect("fragment should parse"); + let program = parse_fragment(br#"unset($class::$count, $class::${$name});"#) + .expect("fragment should parse"); assert_eq!( program.statements(), - &[EvalStmt::UnsetDynamicStaticProperty { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "count".to_string(), - }] + &[ + EvalStmt::UnsetDynamicStaticProperty { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + }, + EvalStmt::UnsetDynamicStaticPropertyName { + class_name: EvalExpr::LoadVar("class".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + }, + ] ); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 089260a5c7..8cf3ca86b5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9815,6 +9815,13 @@ try { echo get_class($e) . ":" . $e->getMessage(); } echo "|"; +$name = "value"; +try { + unset($class::${$name}); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; $aot = "EvalAotStaticUnset"; try { unset($aot::$value); @@ -9822,6 +9829,12 @@ try { echo get_class($e) . ":" . $e->getMessage(); } echo "|"; +try { + unset($aot::${$name}); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; try { unset(EvalMissingStaticUnset::$value); } catch (Error $e) { @@ -9836,7 +9849,7 @@ try { ); assert_eq!( out.stdout, - "Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalAotStaticUnset::$value|Error:Class \"EvalMissingStaticUnset\" not found" + "Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalDynamicStaticUnset::$value|Error:Attempt to unset static property EvalAotStaticUnset::$value|Error:Attempt to unset static property EvalAotStaticUnset::$value|Error:Class \"EvalMissingStaticUnset\" not found" ); } From 6d716dd8ba235ccb26c26f7723b427f38db0ad5a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 12:58:42 +0200 Subject: [PATCH 0755/1208] Document eval dynamic OOP property support --- docs/php/eval.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 8145f88e1a..0b0fa85bd7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -71,12 +71,12 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Expression area | Support | |---|---| | Scalars | `null`, booleans, integers, floats, and strings. | -| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, array writes/appends, and increment/decrement, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | +| Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, `isset()` / `empty()` probes, array writes/appends, array-element unsets, and increment/decrement, static-property unset attempts including dynamic names as PHP-compatible catchable errors, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, and object-property arguments. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, and static-property arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | @@ -471,9 +471,11 @@ arguments into a PHP array and are reported through `ReflectionParameter::isVariadic()` and `ReflectionParameter::isOptional()`. Constructor-promoted eval parameters are reported through `ReflectionParameter::isPromoted()`. By-reference eval method parameters accept -direct variable, array-element, and object-property arguments, write back fixed -parameters after method execution, write back mutated `&...$items` elements -when the variadic container itself is not rebound, and are reported through +direct variable, array-element, object-property including dynamic property +names, and static-property arguments including dynamic receivers and dynamic +property names, write back fixed parameters after method execution, write back +mutated `&...$items` elements when the variadic container itself is not +rebound, and are reported through `ReflectionParameter::isPassedByReference()` and `ReflectionParameter::canBePassedByValue()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, @@ -563,7 +565,8 @@ readonly classes. `self::`, `parent::`, and late-bound `static::` work for supported static members, class constants, and class-name literals. Attempts to `unset()` static properties parse normally and throw PHP's -catchable `Error`, including runtime-valued static receivers. +catchable `Error`, including runtime-valued static receivers and dynamic +static property names. Eval object construction can allocate eval-declared classes, `stdClass`, and emitted AOT classes visible through runtime class metadata. Missing class names From 75303658f3705f31b89347f1a2df6bf03c75b4d0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 13:04:39 +0200 Subject: [PATCH 0756/1208] Support eval AOT static property reference binding --- .../src/interpreter/statements.rs | 73 +++++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 28 +++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e861491c8d..bdf2fd0176 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -4747,6 +4747,12 @@ pub(in crate::interpreter) fn eval_static_property_get_result( values, ); } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } if !values.static_property_is_initialized(&declaring_class, property_name)? { return eval_throw_uninitialized_static_property_error( &declaring_class, @@ -4812,6 +4818,13 @@ pub(in crate::interpreter) fn eval_static_property_isset_result( if validate_eval_member_access(&declaring_class, visibility, context).is_err() { return Ok(false); } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + let value = eval_reference_target_value(&target, context, values)?; + return Ok(!values.is_null(value)?); + } if !values.static_property_is_initialized(&declaring_class, property_name)? { return Ok(false); } @@ -5067,6 +5080,51 @@ fn eval_static_property_reference_bind_result( values, ); } + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property_name, + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property_name, target); + if values.static_property_set(&class_name, property_name, value)? { + return Ok(()); + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } if eval_runtime_class_like_exists(&class_name, context, values)? { eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) } else { @@ -5207,6 +5265,21 @@ pub(in crate::interpreter) fn eval_static_property_set_result( values, ); } + if is_static { + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property_name, + target, + value, + context, + values, + )?; + } + } } if values.static_property_set(&class_name, property_name, value)? { return Ok(()); diff --git a/docs/php/eval.md b/docs/php/eval.md index 0b0fa85bd7..6756d2aa9b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -45,7 +45,7 @@ such a local alias removes the alias without unsetting the global value. | Comments | PHP comments are accepted inside fragments. | | Output | `echo` supports comma-separated arguments. `print` is an expression. | | Variables | Reads, writes, by-name assignment, by-reference assignment, `unset()`, `isset()`, and `empty()` are supported. | -| Assignment forms | Simple variable assignment, by-reference variable, eval object-property, and eval static-property binding, compound assignment for variables, object properties, and static properties (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | +| Assignment forms | Simple variable assignment, by-reference variable, eval object-property, eval static-property, and bridge-supported generated/AOT static-property binding, compound assignment for variables, object properties, and static properties (`+=`, `-=`, `*=`, `**=`, `/=`, `%=`, `&=`, `|=`, `^=`, `<<=`, `>>=`, `.=`), simple variable increment/decrement (`++$x`, `$x++`, `--$x`, `$x--`), object property increment/decrement, and static property increment/decrement are supported. | | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8cf3ca86b5..24d6345dd6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10406,6 +10406,34 @@ echo $third;'); assert_eq!(out, "B|C|E|F|H|I"); } +/// Verifies eval can bind generated/AOT static properties to eval variables by reference. +#[test] +fn test_eval_aot_static_property_reference_binding() { + let out = compile_and_run( + r#" Date: Fri, 26 Jun 2026 13:17:07 +0200 Subject: [PATCH 0757/1208] Support eval settype property lvalues --- .../interpreter/builtins/arrays/mutation.rs | 57 ++++++++++++------- .../src/interpreter/dynamic_functions.rs | 2 +- .../src/interpreter/statements.rs | 4 +- .../src/interpreter/tests/builtins_scalars.rs | 21 ++++++- docs/php/eval.md | 5 ++ tests/codegen/eval.rs | 24 +++++++- 6 files changed, 88 insertions(+), 25 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs index a645336a9b..23d17a7665 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs @@ -18,20 +18,11 @@ pub(in crate::interpreter) fn eval_builtin_settype_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let (var_name, type_name) = eval_settype_direct_args(args, context, scope, values)?; - let value = visible_scope_cell(context, scope, &var_name).map_or_else(|| values.null(), Ok)?; + let (value, target, type_name) = eval_settype_direct_args(args, context, scope, values)?; let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { return values.bool_value(false); }; - for replaced in set_scope_cell( - context, - scope, - var_name, - converted, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } + eval_settype_write_ref_target(&target, converted, context, values)?; values.bool_value(true) } @@ -41,8 +32,8 @@ pub(in crate::interpreter) fn eval_settype_direct_args( context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, -) -> Result<(String, RuntimeCellHandle), EvalStatus> { - let mut var_name = None; +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut var_target = None; let mut type_name = None; let mut positional_index = 0; let mut saw_named = false; @@ -69,13 +60,12 @@ pub(in crate::interpreter) fn eval_settype_direct_args( match parameter { "var" => { - if var_name.is_some() { + if var_target.is_some() { return Err(EvalStatus::RuntimeFatal); } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - var_name = Some(name.clone()); + let (value, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + let target = target.ok_or(EvalStatus::RuntimeFatal)?; + var_target = Some((value, target)); } "type" => { if type_name.is_some() { @@ -87,9 +77,36 @@ pub(in crate::interpreter) fn eval_settype_direct_args( } } - let var_name = var_name.ok_or(EvalStatus::RuntimeFatal)?; + let (value, target) = var_target.ok_or(EvalStatus::RuntimeFatal)?; let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; - Ok((var_name, type_name)) + Ok((value, target, type_name)) +} + +/// Writes a direct `settype()` result back to the captured caller lvalue. +fn eval_settype_write_ref_target( + target: &EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match target { + EvalReferenceTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + for replaced in set_scope_cell( + context, + scope, + name.clone(), + value, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + Ok(()) + } + _ => write_back_method_ref_target(target, value, context, values), + } } /// Applies the eval-supported `settype()` scalar target conversion. diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index ab5669c507..9ff21d519a 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -84,7 +84,7 @@ pub(in crate::interpreter) fn eval_call_arg_values( } /// Evaluates one call arg and captures caller-side storage for by-reference parameters. -fn eval_call_arg_value( +pub(in crate::interpreter) fn eval_call_arg_value( expr: &EvalExpr, context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index bdf2fd0176..fb5f8ef2f8 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7099,8 +7099,8 @@ fn write_back_method_variadic_ref_args( Ok(()) } -/// Stores one by-reference method result in the original caller-side variable. -fn write_back_method_ref_target( +/// Stores one by-reference result in the original caller-side target. +pub(in crate::interpreter) fn write_back_method_ref_target( target: &EvalReferenceTarget, value: RuntimeCellHandle, context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs index fd701128da..61ba6d9db7 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs @@ -151,6 +151,25 @@ echo ":"; $z = 3.8; echo call_user_func("settype", $z, "integer") ? gettype($z) . ":" . $z : "bad"; echo ":"; +$items = ["k" => "6"]; +echo settype($items["k"], "integer") ? gettype($items["k"]) . ":" . $items["k"] : "bad"; +echo ":"; +class EvalSettypePropertyBox { + public $value = 42; + public static $staticValue = "5"; +} +$box = new EvalSettypePropertyBox(); +echo settype($box->value, "string") ? gettype($box->value) . ":" . $box->value : "bad"; +echo ":"; +$name = "value"; +echo settype($box->{$name}, "integer") ? gettype($box->value) . ":" . $box->value : "bad"; +echo ":"; +echo settype(EvalSettypePropertyBox::$staticValue, "integer") ? gettype(EvalSettypePropertyBox::$staticValue) . ":" . EvalSettypePropertyBox::$staticValue : "bad"; +echo ":"; +$class = "EvalSettypePropertyBox"; +$staticName = "staticValue"; +echo settype($class::${$staticName}, "bool") ? gettype(EvalSettypePropertyBox::$staticValue) . ":" . (EvalSettypePropertyBox::$staticValue ? "true" : "false") : "bad"; +echo ":"; return function_exists("settype");"#, ) .expect("parse eval fragment"); @@ -161,7 +180,7 @@ return function_exists("settype");"#, assert_eq!( values.output, - "string:42:boolean:false:integer:0:double:3.8:" + "string:42:boolean:false:integer:0:double:3.8:integer:6:string:42:integer:42:integer:5:boolean:true:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); assert_eq!( diff --git a/docs/php/eval.md b/docs/php/eval.md index 6756d2aa9b..76fdfede05 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -673,6 +673,11 @@ where listed below unless a note says otherwise. ## Builtin notes +Eval `settype()` mutates direct variables, array elements, object properties +including dynamic property names, and static properties including dynamic +receivers or dynamic property names. Callable dispatch of `settype()` remains +by-value and emits PHP's by-reference warning. + Eval `array_map()` supports one or more source arrays with a string callback or `null` callback. One-array results preserve source keys, multi-array results are reindexed, missing source values are padded with `null`, and diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 24d6345dd6..98d110e3c3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3694,16 +3694,38 @@ try { fn test_eval_dispatches_settype_builtin_call() { let out = compile_and_run( r#" "6"]; +echo settype($items["k"], "integer") ? gettype($items["k"]) . ":" . $items["k"] : "bad"; +echo ":"; +$box = new EvalAotSettypeBox(); +echo settype($box->value, "string") ? gettype($box->value) . ":" . $box->value : "bad"; +echo ":"; +$name = "value"; +echo settype($box->{$name}, "integer") ? gettype($box->value) . ":" . $box->value : "bad"; +echo ":"; +echo settype(EvalAotSettypeBox::$staticValue, "string") ? gettype(EvalAotSettypeBox::$staticValue) . ":" . EvalAotSettypeBox::$staticValue : "bad"; +echo ":"; +$class = "EvalAotSettypeBox"; +$staticName = "staticValue"; +echo settype($class::${$staticName}, "bool") ? gettype(EvalAotSettypeBox::$staticValue) . ":" . (EvalAotSettypeBox::$staticValue ? "true" : "false") : "bad"; +echo ":"; echo function_exists("settype");'); "#, ); - assert_eq!(out, "string:42:boolean:false:1"); + assert_eq!( + out, + "string:42:boolean:false:integer:6:string:8:integer:8:string:9:boolean:true:1" + ); } /// Verifies eval SPL object identity builtins inspect AOT object cells. From 6ce4ef85e2a2eece5fa1fc4acd486200a5677ada Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 13:25:40 +0200 Subject: [PATCH 0758/1208] Support eval array mutator property lvalues --- .../interpreter/builtins/arrays/mutation.rs | 78 +++++++++---------- .../interpreter/tests/builtins_arrays_core.rs | 38 ++++++++- tests/codegen/eval.rs | 38 ++++++++- 3 files changed, 107 insertions(+), 47 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs index 23d17a7665..4372ba69a3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs @@ -22,7 +22,13 @@ pub(in crate::interpreter) fn eval_builtin_settype_call( let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { return values.bool_value(false); }; - eval_settype_write_ref_target(&target, converted, context, values)?; + eval_write_direct_ref_target( + &target, + converted, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; values.bool_value(true) } @@ -82,24 +88,31 @@ pub(in crate::interpreter) fn eval_settype_direct_args( Ok((value, target, type_name)) } -/// Writes a direct `settype()` result back to the captured caller lvalue. -fn eval_settype_write_ref_target( +/// Writes a direct by-reference builtin result back to the captured caller lvalue. +fn eval_write_direct_ref_target( target: &EvalReferenceTarget, value: RuntimeCellHandle, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, + variable_ownership: Option, ) -> Result<(), EvalStatus> { match target { EvalReferenceTarget::Variable { scope, name } => { let Some(scope) = (unsafe { scope.as_mut() }) else { return Err(EvalStatus::RuntimeFatal); }; + let ownership = variable_ownership.unwrap_or_else(|| { + scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| entry.flags().ownership) + .unwrap_or(ScopeCellOwnership::Owned) + }); for replaced in set_scope_cell( context, scope, name.clone(), value, - ScopeCellOwnership::Owned, + ownership, )? { values.release(replaced)?; } @@ -109,6 +122,24 @@ fn eval_settype_write_ref_target( } } +/// Captures the first by-reference array mutator argument as a writable lvalue. +fn eval_array_mutation_lvalue_arg( + arg: &EvalCallArg, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let (array, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + let target = target.ok_or(EvalStatus::RuntimeFatal)?; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + Ok((array, target)) +} + /// Applies the eval-supported `settype()` scalar target conversion. pub(in crate::interpreter) fn eval_settype_cast_value( value: RuntimeCellHandle, @@ -179,27 +210,10 @@ pub(in crate::interpreter) fn eval_builtin_array_pop_shift_call( let [arg] = args else { return Err(EvalStatus::RuntimeFatal); }; - if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(var_name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - let Some(entry) = - scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } + let (array, target) = eval_array_mutation_lvalue_arg(arg, context, scope, values)?; let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; - for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { - values.release(replaced)?; - } + eval_write_direct_ref_target(&target, replacement, context, values, None)?; Ok(result) } @@ -214,28 +228,14 @@ pub(in crate::interpreter) fn eval_builtin_array_push_unshift_call( if args.len() < 2 || !eval_call_args_are_plain_positional(args) { return Err(EvalStatus::RuntimeFatal); } - let EvalExpr::LoadVar(var_name) = args[0].value() else { - return Err(EvalStatus::RuntimeFatal); - }; + let (array, target) = eval_array_mutation_lvalue_arg(&args[0], context, scope, values)?; let mut inserted = Vec::with_capacity(args.len() - 1); for arg in &args[1..] { inserted.push(eval_expr(arg.value(), context, scope, values)?); } - let Some(entry) = - scope_entry(context, scope, var_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; - for replaced in set_scope_cell(context, scope, var_name.clone(), replacement, ownership)? { - values.release(replaced)?; - } + eval_write_direct_ref_target(&target, replacement, context, values, None)?; Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs index 49dff13451..46600591d2 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs @@ -131,7 +131,7 @@ return function_exists("array_walk");"#, assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval `array_pop()` and `array_shift()` write back only for direct variable calls. +/// Verifies eval `array_pop()` and `array_shift()` write back writable lvalue calls. #[test] fn execute_program_dispatches_array_pop_shift_builtins() { let program = parse_fragment( @@ -143,6 +143,18 @@ $c = [4, 5]; echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; $d = [6, 7]; echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; +class EvalArrayPopShiftPropertyBox { + public array $items = ["p", "q"]; + public static array $staticItems = ["s", "t"]; +} +$box = new EvalArrayPopShiftPropertyBox(); +echo array_pop($box->items) . ":" . count($box->items) . ":" . $box->items[0] . ":"; +$name = "items"; +echo array_push($box->{$name}, "r") . ":" . $box->items[1] . ":"; +echo array_shift(EvalArrayPopShiftPropertyBox::$staticItems) . ":" . EvalArrayPopShiftPropertyBox::$staticItems[0] . ":"; +$class = "EvalArrayPopShiftPropertyBox"; +$staticName = "staticItems"; +echo array_unshift($class::${$staticName}, "u") . ":" . EvalArrayPopShiftPropertyBox::$staticItems[0] . ":" . EvalArrayPopShiftPropertyBox::$staticItems[1] . ":"; return function_exists("array_pop") && function_exists("array_shift");"#, ) .expect("parse eval fragment"); @@ -151,7 +163,10 @@ return function_exists("array_pop") && function_exists("array_shift");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:2:2:1:2:3:4:5:2:5:6:2:6:"); + assert_eq!( + values.output, + "3:2:2:1:2:3:4:5:2:5:6:2:6:q:1:p:2:r:s:t:2:u:t:" + ); assert_eq!( values.warnings, vec![ @@ -161,7 +176,7 @@ return function_exists("array_pop") && function_exists("array_shift");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval `array_push()` and `array_unshift()` write back direct variable calls. +/// Verifies eval `array_push()` and `array_unshift()` write back writable lvalue calls. #[test] fn execute_program_dispatches_array_push_unshift_builtins() { let program = parse_fragment( @@ -177,6 +192,18 @@ $e = [5]; echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; $f = [7]; echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; +class EvalArrayPushUnshiftPropertyBox { + public array $items = ["p"]; + public static array $staticItems = ["s"]; +} +$box = new EvalArrayPushUnshiftPropertyBox(); +echo array_push($box->items, "q", "r") . ":" . $box->items[2] . ":"; +$name = "items"; +echo array_unshift($box->{$name}, "o") . ":" . $box->items[0] . ":" . $box->items[3] . ":"; +echo array_push(EvalArrayPushUnshiftPropertyBox::$staticItems, "t") . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[1] . ":"; +$class = "EvalArrayPushUnshiftPropertyBox"; +$staticName = "staticItems"; +echo array_unshift($class::${$staticName}, "r") . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[0] . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[2] . ":"; return function_exists("array_push") && function_exists("array_unshift");"#, ) .expect("parse eval fragment"); @@ -185,7 +212,10 @@ return function_exists("array_push") && function_exists("array_unshift");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:"); + assert_eq!( + values.output, + "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:3:r:4:o:r:2:t:3:r:t:" + ); assert_eq!( values.warnings, vec![ diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 98d110e3c3..d6385b0e32 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1261,7 +1261,7 @@ echo function_exists("array_walk");'); assert_eq!(out, "a=2;b=3;T:0=4;1=5;z=6;1"); } -/// Verifies eval `array_pop()` and `array_shift()` mutate direct variable arguments only. +/// Verifies eval `array_pop()` and `array_shift()` mutate writable lvalue arguments. #[test] fn test_eval_dispatches_array_pop_shift_builtin_calls() { let out = compile_and_run( @@ -1274,13 +1274,28 @@ $c = [4, 5]; echo call_user_func("array_pop", $c) . ":" . count($c) . ":" . $c[1] . ":"; $d = [6, 7]; echo call_user_func_array("array_shift", ["array" => $d]) . ":" . count($d) . ":" . $d[0] . ":"; +class EvalArrayPopShiftPropertyBox { + public array $items = ["p", "q"]; + public static array $staticItems = ["s", "t"]; +} +$box = new EvalArrayPopShiftPropertyBox(); +echo array_pop($box->items) . ":" . count($box->items) . ":" . $box->items[0] . ":"; +$name = "items"; +echo array_push($box->{$name}, "r") . ":" . $box->items[1] . ":"; +echo array_shift(EvalArrayPopShiftPropertyBox::$staticItems) . ":" . EvalArrayPopShiftPropertyBox::$staticItems[0] . ":"; +$class = "EvalArrayPopShiftPropertyBox"; +$staticName = "staticItems"; +echo array_unshift($class::${$staticName}, "u") . ":" . EvalArrayPopShiftPropertyBox::$staticItems[0] . ":" . EvalArrayPopShiftPropertyBox::$staticItems[1] . ":"; echo function_exists("array_pop") && function_exists("array_shift");'); "#, ); - assert_eq!(out, "3:2:2:1:2:3:4:5:2:5:6:2:6:1"); + assert_eq!( + out, + "3:2:2:1:2:3:4:5:2:5:6:2:6:q:1:p:2:r:s:t:2:u:t:1" + ); } -/// Verifies eval `array_push()` and `array_unshift()` mutate direct variable arguments only. +/// Verifies eval `array_push()` and `array_unshift()` mutate writable lvalue arguments. #[test] fn test_eval_dispatches_array_push_unshift_builtin_calls() { let out = compile_and_run( @@ -1297,10 +1312,25 @@ $e = [5]; echo call_user_func("array_push", $e, 6) . ":" . count($e) . ":" . $e[0] . ":"; $f = [7]; echo call_user_func_array("array_unshift", [$f, 6]) . ":" . count($f) . ":" . $f[0] . ":"; +class EvalArrayPushUnshiftPropertyBox { + public array $items = ["p"]; + public static array $staticItems = ["s"]; +} +$box = new EvalArrayPushUnshiftPropertyBox(); +echo array_push($box->items, "q", "r") . ":" . $box->items[2] . ":"; +$name = "items"; +echo array_unshift($box->{$name}, "o") . ":" . $box->items[0] . ":" . $box->items[3] . ":"; +echo array_push(EvalArrayPushUnshiftPropertyBox::$staticItems, "t") . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[1] . ":"; +$class = "EvalArrayPushUnshiftPropertyBox"; +$staticName = "staticItems"; +echo array_unshift($class::${$staticName}, "r") . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[0] . ":" . EvalArrayPushUnshiftPropertyBox::$staticItems[2] . ":"; echo function_exists("array_push") && function_exists("array_unshift");'); "#, ); - assert_eq!(out, "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:1"); + assert_eq!( + out, + "3:3:3:1:A:4:0:3:4:A:1:2:3:2:1:5:2:1:7:3:r:4:o:r:2:t:3:r:t:1" + ); } /// Verifies eval `array_splice()` mutates direct variable arguments only. From 37ade14a0c3500c9d5bb946852ae60c18e029722 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 13:35:01 +0200 Subject: [PATCH 0759/1208] Support eval array splice and sort property lvalues --- .../interpreter/builtins/arrays/mutation.rs | 4 +- .../builtins/arrays/sort/direct.rs | 63 +++------ .../src/interpreter/builtins/arrays/splice.rs | 27 +--- .../src/interpreter/control.rs | 3 +- .../interpreter/tests/builtins_arrays_core.rs | 114 +++++++++++++++-- tests/codegen/eval.rs | 120 ++++++++++++++++-- 6 files changed, 241 insertions(+), 90 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs index 4372ba69a3..b3346138dc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs @@ -89,7 +89,7 @@ pub(in crate::interpreter) fn eval_settype_direct_args( } /// Writes a direct by-reference builtin result back to the captured caller lvalue. -fn eval_write_direct_ref_target( +pub(in crate::interpreter) fn eval_write_direct_ref_target( target: &EvalReferenceTarget, value: RuntimeCellHandle, context: &mut ElephcEvalContext, @@ -123,7 +123,7 @@ fn eval_write_direct_ref_target( } /// Captures the first by-reference array mutator argument as a writable lvalue. -fn eval_array_mutation_lvalue_arg( +pub(in crate::interpreter) fn eval_array_mutation_lvalue_arg( arg: &EvalCallArg, context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs index a05af5a7fc..b1d9e1a547 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs @@ -5,10 +5,11 @@ //! - `crate::interpreter::builtins::arrays::sort` re-exports. //! //! Key details: -//! - Direct calls extract a writable variable cell and write back the sorted +//! - Direct calls extract a writable lvalue cell and write back the sorted //! replacement while preserving source-order evaluation of callback arguments. use super::super::super::super::*; +use super::super::{eval_array_mutation_lvalue_arg, eval_write_direct_ref_target}; use super::*; /// Evaluates direct by-reference array ordering calls and writes back the array. @@ -19,23 +20,11 @@ pub(in crate::interpreter) fn eval_builtin_array_sort_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let array_name = eval_array_sort_direct_arg(args)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } + let (array, target) = eval_array_sort_direct_arg(args, context, scope, values)?; let replacement = eval_array_sort_replacement(name, array, values)?; let result = values.bool_value(true)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } + eval_write_direct_ref_target(&target, replacement, context, values, None)?; Ok(result) } @@ -47,23 +36,11 @@ pub(in crate::interpreter) fn eval_builtin_user_sort_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let (array_name, callback) = eval_user_sort_direct_args(args, context, scope, values)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } + let (array, target, callback) = eval_user_sort_direct_args(args, context, scope, values)?; let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; let result = values.bool_value(true)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } + eval_write_direct_ref_target(&target, replacement, context, values, None)?; Ok(result) } @@ -73,7 +50,7 @@ pub(in crate::interpreter) fn eval_user_sort_direct_args( context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, -) -> Result<(String, RuntimeCellHandle), EvalStatus> { +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { let mut array = None; let mut callback = None; let mut positional_index = 0; @@ -104,10 +81,9 @@ pub(in crate::interpreter) fn eval_user_sort_direct_args( if array.is_some() { return Err(EvalStatus::RuntimeFatal); } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - array = Some(name.clone()); + array = Some(eval_array_mutation_lvalue_arg( + arg, context, scope, values, + )?); } "callback" => { if callback.is_some() { @@ -119,23 +95,20 @@ pub(in crate::interpreter) fn eval_user_sort_direct_args( } } - let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let (array, target) = array.ok_or(EvalStatus::RuntimeFatal)?; let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, callback)) + Ok((array, target, callback)) } -/// Extracts the direct variable argument accepted by eval array ordering builtins. +/// Extracts the writable array lvalue accepted by eval array ordering builtins. pub(in crate::interpreter) fn eval_array_sort_direct_arg( args: &[EvalCallArg], -) -> Result { + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { let [arg] = args else { return Err(EvalStatus::RuntimeFatal); }; - if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { - return Err(EvalStatus::RuntimeFatal); - } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - Ok(name.clone()) + eval_array_mutation_lvalue_arg(arg, context, scope, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs index dee9c6551a..b3424f72c1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs @@ -19,24 +19,12 @@ pub(in crate::interpreter) fn eval_builtin_array_splice_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let (array_name, offset, length, replacement_arg) = + let (array, target, offset, length, replacement_arg) = eval_array_splice_direct_args(args, context, scope, values)?; - let Some(entry) = - scope_entry(context, scope, &array_name).filter(|entry| entry.flags().is_visible()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = entry.cell(); - let ownership = entry.flags().ownership; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } let (removed, replacement) = eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; - for replaced in set_scope_cell(context, scope, array_name, replacement, ownership)? { - values.release(replaced)?; - } + eval_write_direct_ref_target(&target, replacement, context, values, None)?; Ok(removed) } @@ -81,10 +69,9 @@ pub(in crate::interpreter) fn eval_array_splice_direct_args( if array.is_some() { return Err(EvalStatus::RuntimeFatal); } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - array = Some(name.clone()); + array = Some(eval_array_mutation_lvalue_arg( + arg, context, scope, values, + )?); } "offset" => { if offset.is_some() { @@ -108,9 +95,9 @@ pub(in crate::interpreter) fn eval_array_splice_direct_args( } } - let array = array.ok_or(EvalStatus::RuntimeFatal)?; + let (array, target) = array.ok_or(EvalStatus::RuntimeFatal)?; let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, offset, length, replacement)) + Ok((array, target, offset, length, replacement)) } /// Returns the removed elements that `array_splice()` would produce without mutating the source. diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index bc4e2009a2..889284bb86 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -61,7 +61,8 @@ pub(super) enum EvaluatedCallable { /// Bound argument tuple for direct `array_splice()` calls. pub(super) type EvalArraySpliceDirectArgs = ( - String, + RuntimeCellHandle, + EvalReferenceTarget, RuntimeCellHandle, Option, Option, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs index 46600591d2..88bc8af8c0 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs @@ -225,7 +225,7 @@ return function_exists("array_push") && function_exists("array_unshift");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval `array_splice()` returns removed values and writes back direct variable calls. +/// Verifies eval `array_splice()` returns removed values and writes back writable lvalue calls. #[test] fn execute_program_dispatches_array_splice_builtin() { let program = parse_fragment( @@ -253,6 +253,22 @@ echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g $h = [1, 2, 3]; $removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; +class EvalArraySplicePropertyBox { + public array $items = ["a", "b", "c"]; + public static array $staticItems = ["x", "y", "z"]; +} +$box = new EvalArraySplicePropertyBox(); +$propRemoved = array_splice($box->items, 1, 1, ["B"]); +echo count($propRemoved) . ":" . $propRemoved[0] . ":" . $box->items[1] . ":" . $box->items[2] . ":"; +$name = "items"; +$dynRemoved = array_splice($box->{$name}, 0, 1); +echo $dynRemoved[0] . ":" . count($box->items) . ":" . $box->items[0] . ":"; +$staticRemoved = array_splice(EvalArraySplicePropertyBox::$staticItems, 1, 1); +echo $staticRemoved[0] . ":" . count(EvalArraySplicePropertyBox::$staticItems) . ":" . EvalArraySplicePropertyBox::$staticItems[1] . ":"; +$class = "EvalArraySplicePropertyBox"; +$staticName = "staticItems"; +$dynStaticRemoved = array_splice($class::${$staticName}, 0, 1, ["w"]); +echo $dynStaticRemoved[0] . ":" . EvalArraySplicePropertyBox::$staticItems[0] . ":" . EvalArraySplicePropertyBox::$staticItems[1] . ":"; return function_exists("array_splice");"#, ) .expect("parse eval fragment"); @@ -263,7 +279,7 @@ return function_exists("array_splice");"#, assert_eq!( values.output, - "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:" + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:1:b:B:c:a:2:B:y:2:z:x:w:z:" ); assert_eq!( values.warnings, @@ -275,7 +291,7 @@ return function_exists("array_splice");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval `sort()` and `rsort()` reindex direct variable arrays only. +/// Verifies eval `sort()` and `rsort()` reindex writable array lvalues. #[test] fn execute_program_dispatches_sort_builtins() { let program = parse_fragment( @@ -290,6 +306,18 @@ $d = [3, 1, 2]; echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; $e = [1, 2, 3]; echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; +class EvalSortPropertyBox { + public array $items = [3, 1, 2]; + public static array $staticItems = ["b", "a"]; +} +$box = new EvalSortPropertyBox(); +echo sort($box->items) . ":" . $box->items[0] . $box->items[1] . $box->items[2] . ":"; +$name = "items"; +echo rsort($box->{$name}) . ":" . $box->items[0] . ":" . $box->items[2] . ":"; +echo sort(EvalSortPropertyBox::$staticItems) . ":" . EvalSortPropertyBox::$staticItems[0] . EvalSortPropertyBox::$staticItems[1] . ":"; +$class = "EvalSortPropertyBox"; +$staticName = "staticItems"; +echo rsort($class::${$staticName}) . ":" . EvalSortPropertyBox::$staticItems[0] . EvalSortPropertyBox::$staticItems[1] . ":"; return function_exists("sort") && function_exists("rsort");"#, ) .expect("parse eval fragment"); @@ -298,7 +326,10 @@ return function_exists("sort") && function_exists("rsort");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1:123:1:cherry:apple:123:1:312:1:1:3:"); + assert_eq!( + values.output, + "1:123:1:cherry:apple:123:1:312:1:1:3:1:123:1:3:1:1:ab:1:ba:" + ); assert_eq!( values.warnings, vec![ @@ -308,7 +339,7 @@ return function_exists("sort") && function_exists("rsort");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval key-preserving array ordering builtins write back direct variable calls. +/// Verifies eval key-preserving array ordering builtins write back writable lvalue calls. #[test] fn execute_program_dispatches_key_preserving_sort_builtins() { let program = parse_fragment( @@ -332,6 +363,26 @@ $e = ["x" => 2, "y" => 1]; echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; $f = ["b" => 1, "a" => 2]; echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; +class EvalKeySortPropertyBox { + public array $items = ["x" => 2, "y" => 1]; + public static array $staticItems = ["b" => 1, "a" => 2]; +} +$box = new EvalKeySortPropertyBox(); +echo asort($box->items) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value; } +echo ":"; +$name = "items"; +echo arsort($box->{$name}) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value; } +echo ":"; +echo ksort(EvalKeySortPropertyBox::$staticItems) . ":"; +foreach (EvalKeySortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; +$class = "EvalKeySortPropertyBox"; +$staticName = "staticItems"; +echo krsort($class::${$staticName}) . ":"; +foreach (EvalKeySortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; return function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");"#, ) .expect("parse eval fragment"); @@ -342,7 +393,7 @@ return function_exists("asort") && function_exists("arsort") && function_exists( assert_eq!( values.output, - "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:" + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:1:y1x2:1:x2y1:1:a2b1:1:b1a2:" ); assert_eq!( values.warnings, @@ -367,6 +418,19 @@ foreach ($b as $key => $value) { echo $key . $value . ";"; } echo ":"; $c = ["x" => "b", "y" => "a"]; echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; +class EvalNaturalSortPropertyBox { + public array $items = ["img10", "img2", "img1"]; + public static array $staticItems = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +} +$box = new EvalNaturalSortPropertyBox(); +echo natsort($box->items) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$class = "EvalNaturalSortPropertyBox"; +$staticName = "staticItems"; +echo natcasesort($class::${$staticName}) . ":"; +foreach (EvalNaturalSortPropertyBox::$staticItems as $key => $value) { echo $key . $value . ";"; } +echo ":"; return function_exists("natsort") && function_exists("natcasesort");"#, ) .expect("parse eval fragment"); @@ -377,7 +441,7 @@ return function_exists("natsort") && function_exists("natcasesort");"#, assert_eq!( values.output, - "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:" + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:" ); assert_eq!( values.warnings, @@ -385,7 +449,7 @@ return function_exists("natsort") && function_exists("natcasesort");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval `shuffle()` reindexes direct variable arrays only. +/// Verifies eval `shuffle()` reindexes writable array lvalues. #[test] fn execute_program_dispatches_shuffle_builtin() { let program = parse_fragment( @@ -393,6 +457,15 @@ fn execute_program_dispatches_shuffle_builtin() { echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; $b = ["x" => 1, "y" => 2]; echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; +class EvalShufflePropertyBox { + public array $items = ["x" => 1, "y" => 2]; + public static array $staticItems = ["a" => 3, "b" => 4]; +} +$box = new EvalShufflePropertyBox(); +echo shuffle($box->items) . ":" . (isset($box->items["x"]) ? "bad" : "prop") . ":" . count($box->items) . ":" . array_sum($box->items) . ":"; +$class = "EvalShufflePropertyBox"; +$staticName = "staticItems"; +echo shuffle($class::${$staticName}) . ":" . (isset(EvalShufflePropertyBox::$staticItems["a"]) ? "bad" : "static") . ":" . count(EvalShufflePropertyBox::$staticItems) . ":" . array_sum(EvalShufflePropertyBox::$staticItems) . ":"; return function_exists("shuffle");"#, ) .expect("parse eval fragment"); @@ -401,19 +474,20 @@ return function_exists("shuffle");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "1:reindexed:2:3:1:12:"); + assert_eq!(values.output, "1:reindexed:2:3:1:12:1:prop:2:3:1:static:2:7:"); assert_eq!( values.warnings, vec!["shuffle(): Argument #1 ($array) must be passed by reference, value given"] ); assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies eval user-comparator sort builtins call callbacks and write back direct arrays. +/// Verifies eval user-comparator sort builtins call callbacks and write back writable lvalues. #[test] fn execute_program_dispatches_user_sort_builtins() { let program = parse_fragment( br#"function eval_sort_cmp($left, $right) { echo "c"; return $left <=> $right; } function eval_key_cmp($left, $right) { return strcmp($left, $right); } +function eval_sort_quiet_cmp($left, $right) { return $left <=> $right; } $a = [3, 1, 2]; echo usort($a, "eval_sort_cmp") . ":"; foreach ($a as $value) { echo $value; } @@ -428,6 +502,21 @@ foreach ($c as $key => $value) { echo $key . $value; } echo ":"; $d = [2, 1]; echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; +class EvalUserSortPropertyBox { + public array $items = [3, 1, 2]; + public static array $staticItems = ["b" => 1, "a" => 2]; +} +$box = new EvalUserSortPropertyBox(); +echo usort($box->items, "eval_sort_quiet_cmp") . ":"; +foreach ($box->items as $value) { echo $value; } +echo ":"; +$name = "items"; +echo usort($box->{$name}, "eval_sort_quiet_cmp") . ":" . $box->items[0] . $box->items[2] . ":"; +$class = "EvalUserSortPropertyBox"; +$staticName = "staticItems"; +echo uksort($class::${$staticName}, "eval_key_cmp") . ":"; +foreach (EvalUserSortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; return function_exists("usort") && function_exists("uasort") && function_exists("uksort");"#, ) .expect("parse eval fragment"); @@ -436,7 +525,10 @@ return function_exists("usort") && function_exists("uasort") && function_exists( let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:"); + assert_eq!( + values.output, + "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1:123:1:13:1:a2b1:" + ); assert_eq!( values.warnings, vec!["usort(): Argument #1 ($array) must be passed by reference, value given"] diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d6385b0e32..43198f64fa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1333,7 +1333,7 @@ echo function_exists("array_push") && function_exists("array_unshift");'); ); } -/// Verifies eval `array_splice()` mutates direct variable arguments only. +/// Verifies eval `array_splice()` mutates writable lvalue arguments. #[test] fn test_eval_dispatches_array_splice_builtin_call() { let out = compile_and_run( @@ -1362,16 +1362,32 @@ echo count($rep3) . ":" . $rep3[0] . ":" . $rep3[1] . ":" . count($g) . ":" . $g $h = [1, 2, 3]; $removed2 = call_user_func_array("array_splice", ["array" => $h, "offset" => 1, "replacement" => [9]]); echo count($removed2) . ":" . $removed2[0] . ":" . $removed2[1] . ":" . count($h) . ":" . $h[1] . ":"; +class EvalArraySplicePropertyBox { + public array $items = ["a", "b", "c"]; + public static array $staticItems = ["x", "y", "z"]; +} +$box = new EvalArraySplicePropertyBox(); +$propRemoved = array_splice($box->items, 1, 1, ["B"]); +echo count($propRemoved) . ":" . $propRemoved[0] . ":" . $box->items[1] . ":" . $box->items[2] . ":"; +$name = "items"; +$dynRemoved = array_splice($box->{$name}, 0, 1); +echo $dynRemoved[0] . ":" . count($box->items) . ":" . $box->items[0] . ":"; +$staticRemoved = array_splice(EvalArraySplicePropertyBox::$staticItems, 1, 1); +echo $staticRemoved[0] . ":" . count(EvalArraySplicePropertyBox::$staticItems) . ":" . EvalArraySplicePropertyBox::$staticItems[1] . ":"; +$class = "EvalArraySplicePropertyBox"; +$staticName = "staticItems"; +$dynStaticRemoved = array_splice($class::${$staticName}, 0, 1, ["w"]); +echo $dynStaticRemoved[0] . ":" . EvalArraySplicePropertyBox::$staticItems[0] . ":" . EvalArraySplicePropertyBox::$staticItems[1] . ":"; echo function_exists("array_splice");'); "#, ); assert_eq!( out, - "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:1" + "2:20:30:2:40:2:3:1:4:3:4:3:2:6:7:3:2:2:3:1:A:B:4:2:3:1:S:N:4:2:2:3:2:9:2:2:3:3:2:1:b:B:c:a:2:B:y:2:z:x:w:z:1" ); } -/// Verifies eval `sort()` and `rsort()` mutate direct variable arguments only. +/// Verifies eval `sort()` and `rsort()` mutate writable lvalue arguments. #[test] fn test_eval_dispatches_sort_builtin_calls() { let out = compile_and_run( @@ -1387,13 +1403,28 @@ $d = [3, 1, 2]; echo call_user_func("sort", $d) . ":" . $d[0] . $d[1] . $d[2] . ":"; $e = [1, 2, 3]; echo call_user_func_array("rsort", ["array" => $e]) . ":" . $e[0] . ":" . $e[2] . ":"; +class EvalSortPropertyBox { + public array $items = [3, 1, 2]; + public static array $staticItems = ["b", "a"]; +} +$box = new EvalSortPropertyBox(); +echo sort($box->items) . ":" . $box->items[0] . $box->items[1] . $box->items[2] . ":"; +$name = "items"; +echo rsort($box->{$name}) . ":" . $box->items[0] . ":" . $box->items[2] . ":"; +echo sort(EvalSortPropertyBox::$staticItems) . ":" . EvalSortPropertyBox::$staticItems[0] . EvalSortPropertyBox::$staticItems[1] . ":"; +$class = "EvalSortPropertyBox"; +$staticName = "staticItems"; +echo rsort($class::${$staticName}) . ":" . EvalSortPropertyBox::$staticItems[0] . EvalSortPropertyBox::$staticItems[1] . ":"; echo function_exists("sort") && function_exists("rsort");'); "#, ); - assert_eq!(out, "1:123:1:cherry:apple:123:1:312:1:1:3:1"); + assert_eq!( + out, + "1:123:1:cherry:apple:123:1:312:1:1:3:1:123:1:3:1:1:ab:1:ba:1" + ); } -/// Verifies eval key-preserving sort builtins mutate direct variable arguments only. +/// Verifies eval key-preserving sort builtins mutate writable lvalue arguments. #[test] fn test_eval_dispatches_key_preserving_sort_builtin_calls() { let out = compile_and_run( @@ -1418,10 +1449,33 @@ $e = ["x" => 2, "y" => 1]; echo call_user_func("asort", $e) . ":" . $e["x"] . $e["y"] . ":"; $f = ["b" => 1, "a" => 2]; echo call_user_func_array("krsort", ["array" => $f]) . ":" . $f["b"] . $f["a"] . ":"; +class EvalKeySortPropertyBox { + public array $items = ["x" => 2, "y" => 1]; + public static array $staticItems = ["b" => 1, "a" => 2]; +} +$box = new EvalKeySortPropertyBox(); +echo asort($box->items) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value; } +echo ":"; +$name = "items"; +echo arsort($box->{$name}) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value; } +echo ":"; +echo ksort(EvalKeySortPropertyBox::$staticItems) . ":"; +foreach (EvalKeySortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; +$class = "EvalKeySortPropertyBox"; +$staticName = "staticItems"; +echo krsort($class::${$staticName}) . ":"; +foreach (EvalKeySortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; echo function_exists("asort") && function_exists("arsort") && function_exists("ksort") && function_exists("krsort");'); "#, ); - assert_eq!(out, "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:1"); + assert_eq!( + out, + "1:y1z2x3:1:y3z2x1:1:34a2b1:1:b1a234:1:21:1:12:1:y1x2:1:x2y1:1:a2b1:1:b1a2:1" + ); } /// Verifies eval natural sort builtins preserve keys and use natural string order. @@ -1439,13 +1493,29 @@ foreach ($b as $key => $value) { echo $key . $value . ";"; } echo ":"; $c = ["x" => "b", "y" => "a"]; echo call_user_func("natsort", $c) . ":" . $c["x"] . $c["y"] . ":"; +class EvalNaturalSortPropertyBox { + public array $items = ["img10", "img2", "img1"]; + public static array $staticItems = ["b" => "Img10", "a" => "img2", "c" => "IMG1"]; +} +$box = new EvalNaturalSortPropertyBox(); +echo natsort($box->items) . ":"; +foreach ($box->items as $key => $value) { echo $key . $value . ";"; } +echo ":"; +$class = "EvalNaturalSortPropertyBox"; +$staticName = "staticItems"; +echo natcasesort($class::${$staticName}) . ":"; +foreach (EvalNaturalSortPropertyBox::$staticItems as $key => $value) { echo $key . $value . ";"; } +echo ":"; echo function_exists("natsort") && function_exists("natcasesort");'); "#, ); - assert_eq!(out, "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:1"); + assert_eq!( + out, + "1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1:ba:1:2img1;1img2;0img10;:1:cIMG1;aimg2;bImg10;:1" + ); } -/// Verifies eval `shuffle()` reindexes direct variable arrays only. +/// Verifies eval `shuffle()` reindexes writable array lvalues. #[test] fn test_eval_dispatches_shuffle_builtin_call() { let out = compile_and_run( @@ -1454,19 +1524,29 @@ eval('$a = ["x" => 1, "y" => 2]; echo shuffle($a) . ":" . (isset($a["x"]) ? "bad" : "reindexed") . ":" . count($a) . ":" . array_sum($a) . ":"; $b = ["x" => 1, "y" => 2]; echo call_user_func("shuffle", $b) . ":" . $b["x"] . $b["y"] . ":"; +class EvalShufflePropertyBox { + public array $items = ["x" => 1, "y" => 2]; + public static array $staticItems = ["a" => 3, "b" => 4]; +} +$box = new EvalShufflePropertyBox(); +echo shuffle($box->items) . ":" . (isset($box->items["x"]) ? "bad" : "prop") . ":" . count($box->items) . ":" . array_sum($box->items) . ":"; +$class = "EvalShufflePropertyBox"; +$staticName = "staticItems"; +echo shuffle($class::${$staticName}) . ":" . (isset(EvalShufflePropertyBox::$staticItems["a"]) ? "bad" : "static") . ":" . count(EvalShufflePropertyBox::$staticItems) . ":" . array_sum(EvalShufflePropertyBox::$staticItems) . ":"; echo function_exists("shuffle");'); "#, ); - assert_eq!(out, "1:reindexed:2:3:1:12:1"); + assert_eq!(out, "1:reindexed:2:3:1:12:1:prop:2:3:1:static:2:7:1"); } -/// Verifies eval user-comparator sort builtins call callbacks and mutate direct arrays. +/// Verifies eval user-comparator sort builtins call callbacks and mutate writable lvalues. #[test] fn test_eval_dispatches_user_sort_builtin_calls() { let out = compile_and_run( r#" $right; } function eval_key_cmp($left, $right) { return strcmp($left, $right); } +function eval_sort_quiet_cmp($left, $right) { return $left <=> $right; } $a = [3, 1, 2]; echo usort($a, "eval_sort_cmp") . ":"; foreach ($a as $value) { echo $value; } @@ -1481,10 +1561,28 @@ foreach ($c as $key => $value) { echo $key . $value; } echo ":"; $d = [2, 1]; echo call_user_func("usort", $d, "eval_sort_cmp") . ":" . $d[0] . $d[1] . ":"; +class EvalUserSortPropertyBox { + public array $items = [3, 1, 2]; + public static array $staticItems = ["b" => 1, "a" => 2]; +} +$box = new EvalUserSortPropertyBox(); +echo usort($box->items, "eval_sort_quiet_cmp") . ":"; +foreach ($box->items as $value) { echo $value; } +echo ":"; +$name = "items"; +echo usort($box->{$name}, "eval_sort_quiet_cmp") . ":" . $box->items[0] . $box->items[2] . ":"; +$class = "EvalUserSortPropertyBox"; +$staticName = "staticItems"; +echo uksort($class::${$staticName}, "eval_key_cmp") . ":"; +foreach (EvalUserSortPropertyBox::$staticItems as $key => $value) { echo $key . $value; } +echo ":"; echo function_exists("usort") && function_exists("uasort") && function_exists("uksort");'); "#, ); - assert_eq!(out, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1"); + assert_eq!( + out, + "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1:123:1:13:1:a2b1:1" + ); } /// Verifies eval iterator array helpers dispatch through direct and dynamic calls. From 5c08f03c13f61e9bdf3e0b1811ec4160ea21df7f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 13:46:14 +0200 Subject: [PATCH 0760/1208] Support eval regex and flock property lvalues --- .../interpreter/builtins/arrays/mutation.rs | 34 --------------- .../builtins/arrays/sort/direct.rs | 3 +- .../builtins/filesystem/streams.rs | 29 ++++++++----- .../src/interpreter/builtins/mod.rs | 2 + .../src/interpreter/builtins/ref_targets.rs | 41 +++++++++++++++++++ .../interpreter/builtins/regex/match_all.rs | 28 ++----------- .../interpreter/builtins/regex/match_one.rs | 32 +++------------ .../src/interpreter/builtins/regex/mod.rs | 2 + .../src/interpreter/builtins/regex/targets.rs | 40 ++++++++++++++++++ .../tests/builtins_file_streams.rs | 22 +++++++++- .../tests/builtins_strings_encoding.rs | 20 ++++++++- tests/codegen/eval.rs | 18 +++++++- 12 files changed, 172 insertions(+), 99 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/ref_targets.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/targets.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs index b3346138dc..e5b68377f0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs @@ -88,40 +88,6 @@ pub(in crate::interpreter) fn eval_settype_direct_args( Ok((value, target, type_name)) } -/// Writes a direct by-reference builtin result back to the captured caller lvalue. -pub(in crate::interpreter) fn eval_write_direct_ref_target( - target: &EvalReferenceTarget, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, - variable_ownership: Option, -) -> Result<(), EvalStatus> { - match target { - EvalReferenceTarget::Variable { scope, name } => { - let Some(scope) = (unsafe { scope.as_mut() }) else { - return Err(EvalStatus::RuntimeFatal); - }; - let ownership = variable_ownership.unwrap_or_else(|| { - scope_entry(context, scope, name) - .filter(|entry| entry.flags().is_visible()) - .map(|entry| entry.flags().ownership) - .unwrap_or(ScopeCellOwnership::Owned) - }); - for replaced in set_scope_cell( - context, - scope, - name.clone(), - value, - ownership, - )? { - values.release(replaced)?; - } - Ok(()) - } - _ => write_back_method_ref_target(target, value, context, values), - } -} - /// Captures the first by-reference array mutator argument as a writable lvalue. pub(in crate::interpreter) fn eval_array_mutation_lvalue_arg( arg: &EvalCallArg, diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs index b1d9e1a547..7222bbb80a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs @@ -9,7 +9,8 @@ //! replacement while preserving source-order evaluation of callback arguments. use super::super::super::super::*; -use super::super::{eval_array_mutation_lvalue_arg, eval_write_direct_ref_target}; +use super::super::super::eval_write_direct_ref_target; +use super::super::eval_array_mutation_lvalue_arg; use super::*; /// Evaluates direct by-reference array ordering calls and writes back the array. diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index d0e7dedc54..728a9d10e5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -375,14 +375,18 @@ pub(in crate::interpreter) fn eval_builtin_flock( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let (stream, operation, would_block_var) = + let (stream, operation, would_block_target) = eval_flock_direct_args(args, context, scope, values)?; let (success, would_block) = eval_flock_result(stream, operation, context, values)?; - if let Some(var_name) = would_block_var { + if let Some(target) = would_block_target { let value = values.bool_value(would_block)?; - for replaced in set_scope_cell(context, scope, var_name, value, ScopeCellOwnership::Owned)? { - values.release(replaced)?; - } + eval_write_direct_ref_target( + &target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; } values.bool_value(success) } @@ -408,7 +412,14 @@ fn eval_flock_direct_args( context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle, Option), EvalStatus> { +) -> Result< + ( + RuntimeCellHandle, + RuntimeCellHandle, + Option, + ), + EvalStatus, +> { let mut stream = None; let mut operation = None; let mut would_block = None; @@ -453,10 +464,8 @@ fn eval_flock_direct_args( if would_block.is_some() { return Err(EvalStatus::RuntimeFatal); } - let EvalExpr::LoadVar(name) = arg.value() else { - return Err(EvalStatus::RuntimeFatal); - }; - would_block = Some(name.clone()); + let (_, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + would_block = Some(target.ok_or(EvalStatus::RuntimeFatal)?); } _ => return Err(EvalStatus::RuntimeFatal), } diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 50fe1d8eaf..2d729b739f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -17,6 +17,7 @@ mod filesystem; mod formatting; mod network_env; mod process_control; +mod ref_targets; mod regex; mod registry; mod scalars; @@ -31,6 +32,7 @@ pub(super) use filesystem::*; pub(super) use formatting::*; pub(super) use network_env::*; pub(super) use process_control::*; +pub(super) use ref_targets::*; pub(super) use regex::*; pub(super) use registry::*; pub(super) use scalars::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/ref_targets.rs b/crates/elephc-magician/src/interpreter/builtins/ref_targets.rs new file mode 100644 index 0000000000..14c3e1fe99 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/ref_targets.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Shared caller-lvalue writeback helpers for direct by-reference eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` modules that implement PHP builtins with +//! direct by-reference output parameters. +//! +//! Key details: +//! - Variable writes can request a specific scope-cell ownership, while object, +//! static-property, and array-element targets reuse method by-reference +//! writeback semantics. + +use super::super::*; + +/// Writes a direct by-reference builtin result back to the captured caller lvalue. +pub(in crate::interpreter) fn eval_write_direct_ref_target( + target: &EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + variable_ownership: Option, +) -> Result<(), EvalStatus> { + match target { + EvalReferenceTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + let ownership = variable_ownership.unwrap_or_else(|| { + scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| entry.flags().ownership) + .unwrap_or(ScopeCellOwnership::Owned) + }); + for replaced in set_scope_cell(context, scope, name.clone(), value, ownership)? { + values.release(replaced)?; + } + Ok(()) + } + _ => write_back_method_ref_target(target, value, context, values), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs b/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs index 9a014ece3e..5c79967522 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs @@ -28,40 +28,20 @@ pub(in crate::interpreter) fn eval_builtin_preg_match_all( [pattern, subject, matches] => { let pattern = eval_expr(pattern, context, scope, values)?; let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; + let matches_target = eval_preg_matches_target(matches, context, scope, values)?; let (result, matches_array) = eval_preg_match_all_capture_result(pattern, subject, None, values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } + eval_write_preg_matches_target(&matches_target, matches_array, context, values)?; Ok(result) } [pattern, subject, matches, flags] => { let pattern = eval_expr(pattern, context, scope, values)?; let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; + let matches_target = eval_preg_matches_target(matches, context, scope, values)?; let flags = eval_expr(flags, context, scope, values)?; let (result, matches_array) = eval_preg_match_all_capture_result(pattern, subject, Some(flags), values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } + eval_write_preg_matches_target(&matches_target, matches_array, context, values)?; Ok(result) } _ => Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs b/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs index 33ed83e092..27bce4fbf5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs @@ -6,8 +6,8 @@ //! - `crate::interpreter::builtins::regex` re-exports. //! //! Key details: -//! - `$matches` assignment writes back through eval scope cells and preserves runtime -//! ownership by releasing replaced values. +//! - `$matches` assignment captures writable caller lvalues and writes back the +//! materialized capture array after regex execution. use super::super::super::*; use super::super::*; @@ -29,40 +29,20 @@ pub(in crate::interpreter) fn eval_builtin_preg_match( [pattern, subject, matches] => { let pattern = eval_expr(pattern, context, scope, values)?; let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; + let matches_target = eval_preg_matches_target(matches, context, scope, values)?; let (result, matches_array) = eval_preg_match_capture_result(pattern, subject, None, values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } + eval_write_preg_matches_target(&matches_target, matches_array, context, values)?; Ok(result) } [pattern, subject, matches, flags] => { let pattern = eval_expr(pattern, context, scope, values)?; let subject = eval_expr(subject, context, scope, values)?; - let EvalExpr::LoadVar(matches_name) = matches else { - return Err(EvalStatus::RuntimeFatal); - }; + let matches_target = eval_preg_matches_target(matches, context, scope, values)?; let flags = eval_expr(flags, context, scope, values)?; let (result, matches_array) = eval_preg_match_capture_result(pattern, subject, Some(flags), values)?; - for replaced in set_scope_cell( - context, - scope, - matches_name.clone(), - matches_array, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } + eval_write_preg_matches_target(&matches_target, matches_array, context, values)?; Ok(result) } _ => Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs index 988f1c48d9..45af8e94c2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs @@ -18,6 +18,7 @@ mod replace; mod replacement; mod split; mod split_helpers; +mod targets; pub(in crate::interpreter) use captures::*; pub(in crate::interpreter) use match_all::*; @@ -27,3 +28,4 @@ pub(in crate::interpreter) use replace::*; pub(in crate::interpreter) use replacement::*; pub(in crate::interpreter) use split::*; pub(in crate::interpreter) use split_helpers::*; +pub(in crate::interpreter) use targets::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/targets.rs b/crates/elephc-magician/src/interpreter/builtins/regex/targets.rs new file mode 100644 index 0000000000..9102e12602 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/targets.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Shared by-reference `$matches` target helpers for eval preg builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::match_one` +//! - `crate::interpreter::builtins::regex::match_all` +//! +//! Key details: +//! - Direct preg calls capture writable caller lvalues in source order, then +//! write the materialized matches array back after regex execution. + +use super::super::super::*; +use super::super::*; + +/// Captures a writable `$matches` argument target from a direct preg call. +pub(in crate::interpreter) fn eval_preg_matches_target( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, target) = eval_call_arg_value(expr, context, scope, values)?; + target.ok_or(EvalStatus::RuntimeFatal) +} + +/// Writes a preg `$matches` result back to the captured caller lvalue. +pub(in crate::interpreter) fn eval_write_preg_matches_target( + target: &EvalReferenceTarget, + matches_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_write_direct_ref_target( + target, + matches_array, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs index 4e4c653e0d..3b35caa727 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs @@ -100,6 +100,26 @@ echo flock($h, LOCK_EX, $would) ? "lock" : "bad"; echo ":"; echo $would === false ? "would0" : "bad"; echo ":"; echo flock(stream: $h, operation: LOCK_UN, would_block: $would) ? "unlock" : "bad"; echo ":"; echo $would === false ? "would1" : "bad"; echo ":"; +class EvalFlockWouldBlockBox {{ + public bool $value = true; + public static bool $staticValue = true; +}} +$box = new EvalFlockWouldBlockBox(); +echo flock($h, LOCK_SH, $box->value) ? "proplock" : "bad"; echo ":"; +echo $box->value === false ? "prop0" : "bad"; echo ":"; +flock($h, LOCK_UN); +$name = "value"; +echo flock($h, LOCK_EX, $box->{{$name}}) ? "dynlock" : "bad"; echo ":"; +echo $box->value === false ? "dyn0" : "bad"; echo ":"; +flock($h, LOCK_UN); +echo flock($h, LOCK_SH, EvalFlockWouldBlockBox::$staticValue) ? "staticlock" : "bad"; echo ":"; +echo EvalFlockWouldBlockBox::$staticValue === false ? "static0" : "bad"; echo ":"; +flock($h, LOCK_UN); +$class = "EvalFlockWouldBlockBox"; +$staticName = "staticValue"; +echo flock($h, LOCK_EX, $class::${{$staticName}}) ? "dynstaticlock" : "bad"; echo ":"; +echo EvalFlockWouldBlockBox::$staticValue === false ? "dynstatic0" : "bad"; echo ":"; +flock($h, LOCK_UN); echo call_user_func("flock", $h, LOCK_SH) ? "calllock" : "bad"; echo ":"; flock($h, LOCK_UN); echo flock($h, 99) === false ? "invalid" : "bad"; echo ":"; @@ -120,7 +140,7 @@ return true;"# let _ = std::fs::remove_file(&file); assert_eq!( values.output, - "lock:would0:unlock:would1:calllock:invalid:cleanup:11111:locks=1234" + "lock:would0:unlock:would1:proplock:prop0:dynlock:dyn0:staticlock:static0:dynstaticlock:dynstatic0:calllock:invalid:cleanup:11111:locks=1234" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs index 907e76af15..1b7485fc64 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs @@ -146,6 +146,22 @@ $splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; $splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; +class EvalPregMatchesBox { + public array $matches = []; + public static array $staticMatches = []; +} +$box = new EvalPregMatchesBox(); +preg_match("/([a-z]+)([0-9]+)/", "ab12", $box->matches); +echo $box->matches[0] . ":" . $box->matches[1] . ":" . $box->matches[2] . ":"; +$name = "matches"; +preg_match_all("/([a-z])([0-9])/", "a1 b2", $box->{$name}, PREG_SET_ORDER); +echo count($box->matches) . ":" . $box->matches[1][0] . ":" . $box->matches[1][2] . ":"; +preg_match("/([A-Z]+)/", "ID", EvalPregMatchesBox::$staticMatches); +echo EvalPregMatchesBox::$staticMatches[0] . ":"; +$class = "EvalPregMatchesBox"; +$staticName = "staticMatches"; +preg_match_all("/([a-z])/", "xy", $class::${$staticName}); +echo count(EvalPregMatchesBox::$staticMatches[0]) . ":" . EvalPregMatchesBox::$staticMatches[0][1] . ":"; return function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");"#, ) .expect("parse eval fragment"); @@ -154,9 +170,9 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!( + assert_eq!( values.output, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:ab12:ab:12:2:b2:2:ID:2:y:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 43198f64fa..f6e743e881 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2808,12 +2808,28 @@ $splitBoth = preg_split("/(,)/", "a,b", 0, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT echo count($splitBoth) . ":" . $splitBoth[1][0] . ":" . $splitBoth[1][1] . ":"; $splitNoEmpty = preg_split("/,/", "a,,b", 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE); echo $splitNoEmpty[1][0] . ":" . $splitNoEmpty[1][1] . ":"; +class EvalPregMatchesBox { + public array $matches = []; + public static array $staticMatches = []; +} +$box = new EvalPregMatchesBox(); +preg_match("/([a-z]+)([0-9]+)/", "ab12", $box->matches); +echo $box->matches[0] . ":" . $box->matches[1] . ":" . $box->matches[2] . ":"; +$name = "matches"; +preg_match_all("/([a-z])([0-9])/", "a1 b2", $box->{$name}, PREG_SET_ORDER); +echo count($box->matches) . ":" . $box->matches[1][0] . ":" . $box->matches[1][2] . ":"; +preg_match("/([A-Z]+)/", "ID", EvalPregMatchesBox::$staticMatches); +echo EvalPregMatchesBox::$staticMatches[0] . ":"; +$class = "EvalPregMatchesBox"; +$staticName = "staticMatches"; +preg_match_all("/([a-z])/", "xy", $class::${$staticName}); +echo count(EvalPregMatchesBox::$staticMatches[0]) . ":" . EvalPregMatchesBox::$staticMatches[0][1] . ":"; echo function_exists("preg_match") && function_exists("preg_match_all") && function_exists("preg_replace") && function_exists("preg_replace_callback") && function_exists("preg_split") && defined("PREG_SPLIT_NO_EMPTY") && defined("PREG_SET_ORDER") && defined("PREG_OFFSET_CAPTURE") && defined("PREG_SPLIT_OFFSET_CAPTURE") && defined("PREG_UNMATCHED_AS_NULL");'); "#, ); assert_eq!( out, - "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:1" + "1:3:id42:id:42:0:3:2:3:b22:a:22:2:2:a1:a:22:b:0::-1:b:0:b22:3:0:4:b22:3:1:4:n:b:n:n:-1:n:-1:n:b:n:n:-1:n:-1:3:0:0:0:1-a 2-b:[A][B]:2:a:b,c:2:b:1:aN:3:,:a:0:b,c:2:3:,:1:b:3:ab12:ab:12:2:b2:2:ID:2:y:1" ); } From 1c2ea718e472a4bb235efd853db8c0d46701a7fe Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 13:52:19 +0200 Subject: [PATCH 0761/1208] Support eval trait abstract property contracts --- .../src/interpreter/statements.rs | 68 +++++++++++++++++-- .../interpreter/tests/trait_adaptations.rs | 4 +- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index fb5f8ef2f8..87eb2559ce 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2179,7 +2179,16 @@ fn append_eval_trait_properties( continue; } if let Some(existing) = trait_properties.get(property.name()) { - validate_eval_trait_property_compatibility(existing, property)?; + let resolved = resolve_eval_trait_property_conflict(existing, property)?; + if &resolved != existing { + trait_properties.insert(property.name().to_string(), resolved.clone()); + if let Some(slot) = properties + .iter_mut() + .find(|candidate| candidate.name() == property.name()) + { + *slot = resolved; + } + } continue; } trait_properties.insert(property.name().to_string(), property.clone()); @@ -2193,7 +2202,42 @@ fn validate_eval_trait_property_compatibility( existing: &EvalClassProperty, incoming: &EvalClassProperty, ) -> Result<(), EvalStatus> { - if existing.visibility() == incoming.visibility() + resolve_eval_trait_property_conflict(existing, incoming).map(|_| ()) +} + +/// Resolves compatible same-name properties imported from classes and traits. +fn resolve_eval_trait_property_conflict( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> Result { + if existing.is_abstract() && !incoming.is_abstract() { + return class_property_satisfies_abstract_contract(incoming, existing) + .then(|| incoming.clone()) + .ok_or(EvalStatus::RuntimeFatal); + } + if incoming.is_abstract() && !existing.is_abstract() { + return class_property_satisfies_abstract_contract(existing, incoming) + .then(|| existing.clone()) + .ok_or(EvalStatus::RuntimeFatal); + } + if existing.is_abstract() && incoming.is_abstract() { + return eval_trait_abstract_properties_are_compatible(existing, incoming) + .then(|| merge_abstract_property_contracts(existing, incoming)) + .ok_or(EvalStatus::RuntimeFatal); + } + if eval_trait_concrete_properties_are_compatible(existing, incoming) { + Ok(existing.clone()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether two concrete same-name trait properties are identical enough to deduplicate. +fn eval_trait_concrete_properties_are_compatible( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> bool { + existing.visibility() == incoming.visibility() && existing.set_visibility() == incoming.set_visibility() && existing.is_static() == incoming.is_static() && existing.is_final() == incoming.is_final() @@ -2206,11 +2250,20 @@ fn validate_eval_trait_property_compatibility( && existing.is_virtual() == incoming.is_virtual() && existing.property_type() == incoming.property_type() && existing.default() == incoming.default() - { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } +} + +/// Returns whether two abstract trait property contracts can be merged. +fn eval_trait_abstract_properties_are_compatible( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> bool { + existing.visibility() == incoming.visibility() + && existing.set_visibility() == incoming.set_visibility() + && existing.is_static() == incoming.is_static() + && existing.is_final() == incoming.is_final() + && existing.is_readonly() == incoming.is_readonly() + && existing.property_type() == incoming.property_type() + && existing.default() == incoming.default() } /// Appends trait methods unless the class provides a same-name method. @@ -3213,6 +3266,7 @@ fn class_property_satisfies_abstract_contract( ) -> bool { if property.is_abstract() || property.is_static() + || property.property_type() != requirement.property_type() || !property_contract_visibility_allows(requirement, property) { return false; diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index f115c01adf..fcd8f99996 100644 --- a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -46,7 +46,7 @@ return $box->talk();"#, assert_eq!(values.get(result), FakeValue::String("A".to_string())); } -/// Verifies visibility-only `as private` hides the imported method from global calls. +/// Verifies visibility-only `as private` throws when called from global scope. #[test] fn execute_program_applies_eval_trait_visibility_adaptation() { let program = parse_fragment( @@ -68,7 +68,7 @@ return $box->hidden();"#, let err = execute_program(&program, &mut scope, &mut values) .expect_err("private adapted trait method should fail from global scope"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(err, EvalStatus::UncaughtThrowable); } /// Verifies trait aliases that collide with class methods or no-op names follow PHP rules. From d14b067dfc4218c420ac58ad81ac2198d26abed2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 14:03:06 +0200 Subject: [PATCH 0762/1208] Support eval by-ref property array elements --- crates/elephc-magician/src/context.rs | 4 ++ .../src/interpreter/dynamic_functions.rs | 34 ++++++++++- .../src/interpreter/statements.rs | 61 +++++++++++++++++++ .../src/interpreter/tests/method_arguments.rs | 51 ++++++++++++++++ docs/php/eval.md | 11 ++-- tests/codegen/eval.rs | 56 +++++++++++++++++ 6 files changed, 211 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 5e3d6b22bb..db2dc77404 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -48,6 +48,10 @@ pub enum EvalReferenceTarget { array_name: String, index: RuntimeCellHandle, }, + NestedArrayElement { + array_target: Box, + index: RuntimeCellHandle, + }, ObjectProperty { object: RuntimeCellHandle, property: String, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 9ff21d519a..5d4bd64a66 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -104,7 +104,13 @@ pub(in crate::interpreter) fn eval_call_arg_value( } EvalExpr::ArrayGet { array, index } => { let EvalExpr::LoadVar(array_name) = array.as_ref() else { - return eval_expr(expr, context, caller_scope, values).map(|value| (value, None)); + return eval_nested_array_element_call_arg_value( + array, + index, + context, + caller_scope, + values, + ); }; let array = visible_scope_cell(context, caller_scope, array_name) .map_or_else(|| values.null(), Ok)?; @@ -200,6 +206,32 @@ pub(in crate::interpreter) fn eval_call_arg_value( } } +/// Evaluates an array element whose array expression is itself a writable caller target. +fn eval_nested_array_element_call_arg_value( + array: &EvalExpr, + index: &EvalExpr, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let (array, array_target) = eval_call_arg_value(array, context, caller_scope, values)?; + let index = eval_expr(index, context, caller_scope, values)?; + let value = eval_array_get_result(array, index, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return Ok((value, None)); + } + let Some(array_target) = array_target else { + return Ok((value, None)); + }; + Ok(( + value, + Some(EvalReferenceTarget::NestedArrayElement { + array_target: Box::new(array_target), + index, + }), + )) +} + /// Evaluates one static-property lvalue and records it as a by-reference call target. fn eval_static_property_call_arg_value( class_name: String, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 87eb2559ce..ef878858c7 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3994,6 +3994,13 @@ fn eval_reference_target_value( visible_scope_cell(context, scope, array_name).map_or_else(|| values.null(), Ok)?; values.array_get(array, *index) } + EvalReferenceTarget::NestedArrayElement { + array_target, + index, + } => { + let array = eval_reference_target_value(array_target, context, values)?; + values.array_get(array, *index) + } EvalReferenceTarget::ObjectProperty { object, property, @@ -7076,6 +7083,32 @@ fn same_method_ref_target(left: &EvalReferenceTarget, right: &EvalReferenceTarge index: right_index, }, ) => left_scope == right_scope && left_name == right_name && left_index == right_index, + ( + EvalReferenceTarget::NestedArrayElement { + array_target: left_target, + index: left_index, + }, + EvalReferenceTarget::NestedArrayElement { + array_target: right_target, + index: right_index, + }, + ) => left_index == right_index && same_method_ref_target(left_target, right_target), + ( + EvalReferenceTarget::ObjectProperty { + object: left_object, + property: left_property, + access_scope: left_access_scope, + }, + EvalReferenceTarget::ObjectProperty { + object: right_object, + property: right_property, + access_scope: right_access_scope, + }, + ) => { + left_object == right_object + && left_property == right_property + && left_access_scope == right_access_scope + } ( EvalReferenceTarget::Cell { cell: left_cell }, EvalReferenceTarget::Cell { cell: right_cell }, @@ -7188,6 +7221,16 @@ pub(in crate::interpreter) fn write_back_method_ref_target( scope, array_name, *index, value, context, values, ) } + EvalReferenceTarget::NestedArrayElement { + array_target, + index, + } => write_back_method_nested_array_element_ref_target( + array_target, + *index, + value, + context, + values, + ), EvalReferenceTarget::ObjectProperty { object, property, @@ -7245,6 +7288,24 @@ fn write_back_method_array_element_ref_target( Ok(()) } +/// Stores one by-reference method result in an element of a nested caller-side array target. +fn write_back_method_nested_array_element_ref_target( + array_target: &EvalReferenceTarget, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_reference_target_value(array_target, context, values)?; + let array = if values.is_array_like(current)? { + current + } else { + eval_new_array_for_index(index, values)? + }; + let array = values.array_set(array, index, value)?; + write_back_method_ref_target(array_target, array, context, values) +} + /// Stores one by-reference method result in a caller-side object property. fn write_back_method_object_property_ref_target( object: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs index b50584d3e0..d43d6a400b 100644 --- a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs @@ -378,6 +378,57 @@ return EvalByRefStaticPropertyBox::updatePrivate($changer);"#, assert_eq!(values.get(result), FakeValue::String("secret".to_string())); } +/// Verifies by-reference method params write back array elements stored in properties. +#[test] +fn execute_program_writes_back_eval_method_by_ref_property_array_elements() { + let program = parse_fragment( + br#"class EvalByRefPropertyArrayElementChanger { + public function set(&$value, $next) { + $value = $next; + } + public function pair(&$left, &$right) { + $left = "left"; + $right = "right"; + return $left; + } +} +class EvalByRefPropertyArrayElementBox { + public $items = ["first" => "old", "same" => "same"]; + public $other = null; + public static $staticItems = ["first" => "static-old", "same" => "static-same"]; +} +$changer = new EvalByRefPropertyArrayElementChanger(); +$box = new EvalByRefPropertyArrayElementBox(); +$changer->set($box->items["first"], "changed"); +$name = "items"; +$changer->set($box->{$name}["dynamic"], "dynamic"); +$changer->set($box->other["created"], "created"); +echo $box->items["first"]; echo ":"; +echo $box->items["dynamic"]; echo ":"; +echo $box->other["created"]; echo ":"; +echo $changer->pair($box->items["same"], $box->items["same"]); echo ":"; +echo $box->items["same"]; echo ":"; +$changer->set(EvalByRefPropertyArrayElementBox::$staticItems["first"], "static"); +$class = "EvalByRefPropertyArrayElementBox"; +$staticName = "staticItems"; +$changer->set($class::${$staticName}["dynamic"], "static-dynamic"); +echo EvalByRefPropertyArrayElementBox::$staticItems["first"]; echo ":"; +echo EvalByRefPropertyArrayElementBox::$staticItems["dynamic"]; echo ":"; +return $changer->pair( + EvalByRefPropertyArrayElementBox::$staticItems["same"], + EvalByRefPropertyArrayElementBox::$staticItems["same"] +) . ":" . EvalByRefPropertyArrayElementBox::$staticItems["same"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "changed:dynamic:created:right:right:static:static-dynamic:"); + assert_eq!(values.get(result), FakeValue::String("right:right".to_string())); +} + /// Verifies eval-declared by-reference method params keep property access restrictions. #[test] fn execute_program_rejects_invalid_eval_method_by_ref_object_property_targets() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 76fdfede05..4dae81baeb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -76,7 +76,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, and static-property arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | @@ -472,10 +472,11 @@ arguments into a PHP array and are reported through Constructor-promoted eval parameters are reported through `ReflectionParameter::isPromoted()`. By-reference eval method parameters accept direct variable, array-element, object-property including dynamic property -names, and static-property arguments including dynamic receivers and dynamic -property names, write back fixed parameters after method execution, write back -mutated `&...$items` elements when the variadic container itself is not -rebound, and are reported through +names, object-property array-element, static-property, and static-property +array-element arguments including dynamic receivers and dynamic property names, +write back fixed parameters after method execution, write back mutated +`&...$items` elements when the variadic container itself is not rebound, and are +reported through `ReflectionParameter::isPassedByReference()` and `ReflectionParameter::canBePassedByValue()`. `ReflectionProperty::isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f6e743e881..ccc536b943 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9628,6 +9628,62 @@ echo EvalAotByRefStaticPropertyBox::$value;'); assert_eq!(out.stdout, "right:right:dynamic:name:secret:aot-changed"); } +/// Verifies eval methods mutate property array elements passed by reference. +#[test] +fn test_eval_declared_method_by_ref_property_array_element_arguments() { + let out = compile_and_run_capture( + r#" "old"]; +} +eval('class EvalByRefPropertyArrayElementChanger { + public function set(&$value, $next) { + $value = $next; + } + public function pair(&$left, &$right) { + $left = "left"; + $right = "right"; + return $left; + } +} +class EvalByRefPropertyArrayElementBox { + public $items = ["first" => "old", "same" => "same"]; + public $other = null; + public static $staticItems = ["first" => "static-old", "same" => "static-same"]; +} +$changer = new EvalByRefPropertyArrayElementChanger(); +$box = new EvalByRefPropertyArrayElementBox(); +$changer->set($box->items["first"], "changed"); +$name = "items"; +$changer->set($box->{$name}["dynamic"], "dynamic"); +$changer->set($box->other["created"], "created"); +echo $box->items["first"] . ":" . $box->items["dynamic"] . ":" . $box->other["created"] . ":"; +echo $changer->pair($box->items["same"], $box->items["same"]) . ":" . $box->items["same"] . ":"; +$changer->set(EvalByRefPropertyArrayElementBox::$staticItems["first"], "static"); +$class = "EvalByRefPropertyArrayElementBox"; +$staticName = "staticItems"; +$changer->set($class::${$staticName}["dynamic"], "static-dynamic"); +echo EvalByRefPropertyArrayElementBox::$staticItems["first"] . ":"; +echo EvalByRefPropertyArrayElementBox::$staticItems["dynamic"] . ":"; +echo $changer->pair( + EvalByRefPropertyArrayElementBox::$staticItems["same"], + EvalByRefPropertyArrayElementBox::$staticItems["same"] +) . ":" . EvalByRefPropertyArrayElementBox::$staticItems["same"] . ":"; +$changer->set(EvalAotByRefPropertyArrayElementBox::$items["aot"], "aot-changed"); +echo EvalAotByRefPropertyArrayElementBox::$items["aot"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "changed:dynamic:created:right:right:static:static-dynamic:right:right:aot-changed" + ); +} + /// Verifies eval dynamic static callables dispatch eval-declared static methods. #[test] fn test_eval_declared_static_method_dynamic_callables() { From 11a56288c03834f21a2ba07916e50f79c376d918 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 14:07:49 +0200 Subject: [PATCH 0763/1208] Document eval promoted property reference targets --- .../src/interpreter/tests/classes.rs | 39 +++++++++++++++++++ docs/php/eval.md | 5 ++- tests/codegen/eval.rs | 34 ++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 03b2f79460..bbf89fc3f0 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -204,6 +204,45 @@ return $box->value;"#, assert_eq!(values.get(result), FakeValue::Int(7)); } +/// Verifies by-reference promoted properties can alias static and nested property targets. +#[test] +fn execute_program_aliases_by_reference_promoted_static_and_nested_properties() { + let program = parse_fragment( + br#"class EvalPromotedStaticRefHolder { + public static $value = 1; + public $items = [1]; + public static $staticItems = [1]; +} +class EvalPromotedStaticRefBox { + public function __construct(public &$value) {} +} +$box = new EvalPromotedStaticRefBox(EvalPromotedStaticRefHolder::$value); +$box->value = 5; +echo EvalPromotedStaticRefHolder::$value; echo ":"; +EvalPromotedStaticRefHolder::$value = 7; +echo $box->value; echo ":"; +$holder = new EvalPromotedStaticRefHolder(); +$itemBox = new EvalPromotedStaticRefBox($holder->items[0]); +$itemBox->value = 11; +echo $holder->items[0]; echo ":"; +$holder->items[0] = 13; +echo $itemBox->value; echo ":"; +$staticItemBox = new EvalPromotedStaticRefBox(EvalPromotedStaticRefHolder::$staticItems[0]); +$staticItemBox->value = 17; +echo EvalPromotedStaticRefHolder::$staticItems[0]; echo ":"; +EvalPromotedStaticRefHolder::$staticItems[0] = 19; +return $staticItemBox->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7:11:13:17:"); + assert_eq!(values.get(result), FakeValue::Int(19)); +} + /// Verifies by-reference promoted defaults use internal property alias storage. #[test] fn execute_program_aliases_by_reference_promoted_default_properties() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 4dae81baeb..a51956f109 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -144,7 +144,8 @@ containers to eval-declared functions. Eval-declared classes support inheritance, public/protected/private properties and methods, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion -for variable, array-element, object-property, and default-value targets, +for variable, array-element, object-property, static-property, +property-array-element, static-property-array-element, and default-value targets, concrete property `get` / `set` hooks, interface property hook contract checks including asymmetric write visibility, abstract property hook contracts including asymmetric write visibility, property-level `readonly`, `readonly class`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ccc536b943..f0d76b1084 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -17056,6 +17056,40 @@ return $box->value;'); assert_eq!(out, "5:7"); } +/// Verifies eval promoted by-reference properties alias static and nested property targets. +#[test] +fn test_eval_declared_class_aliases_by_reference_promoted_static_and_nested_properties() { + let out = compile_and_run( + r#"value = 5; +echo DynEvalPromotedStaticRefHolder::$value . ":"; +DynEvalPromotedStaticRefHolder::$value = 7; +echo $box->value . ":"; +$holder = new DynEvalPromotedStaticRefHolder(); +$itemBox = new DynEvalPromotedStaticRefSupported($holder->items[0]); +$itemBox->value = 11; +echo $holder->items[0] . ":"; +$holder->items[0] = 13; +echo $itemBox->value . ":"; +$staticItemBox = new DynEvalPromotedStaticRefSupported(DynEvalPromotedStaticRefHolder::$staticItems[0]); +$staticItemBox->value = 17; +echo DynEvalPromotedStaticRefHolder::$staticItems[0] . ":"; +DynEvalPromotedStaticRefHolder::$staticItems[0] = 19; +return $staticItemBox->value;'); +"#, + ); + assert_eq!(out, "5:7:11:13:17:19"); +} + /// Verifies eval `class_alias()` supports class-like interface, trait, enum, and class targets. #[test] fn test_eval_class_alias_supports_class_like_targets() { From c7eab4f2f6ac2d0d3149dccc75bf9154121dad22 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 14:13:22 +0200 Subject: [PATCH 0764/1208] Cover eval readonly anonymous classes --- .../src/interpreter/tests/classes.rs | 26 +++++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 20 ++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index bbf89fc3f0..dfce6eaccf 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -972,6 +972,32 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies readonly anonymous eval classes initialize and reject property writes. +#[test] +fn execute_program_instantiates_readonly_anonymous_class_expressions() { + let program = parse_fragment( + br#"$box = new readonly class("frozen") { + public function __construct(public string $label) {} +}; +echo $box->label; echo ":"; +try { + $box->label = "bad"; + echo "bad"; +} catch (Error $e) { + echo get_class($e); +} +return $box->label;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "frozen:Error"); + assert_eq!(values.get(result), FakeValue::String("frozen".to_string())); +} + /// Verifies eval object cloning copies properties before running `__clone()`. #[test] fn execute_program_clones_eval_object_and_runs_clone_hook() { diff --git a/docs/php/eval.md b/docs/php/eval.md index a51956f109..560bef7540 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,7 +74,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, `isset()` / `empty()` probes, array writes/appends, array-element unsets, and increment/decrement, static-property unset attempts including dynamic names as PHP-compatible catchable errors, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | +| Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new [readonly] class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f0d76b1084..b4ebb804d2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11870,6 +11870,26 @@ echo $ref->implementsInterface("EvalRuntimeAnonLabel") ? "iface" : "bad";'); assert_eq!(out.stdout, "A:anon:B:anon:same:anonymous:iface"); } +/// Verifies eval readonly anonymous class expressions initialize and reject writes. +#[test] +fn test_eval_readonly_anonymous_class_expression_runtime() { + let out = compile_and_run( + r#"label . ":"; +try { + $box->label = "bad"; + echo "bad"; +} catch (Error $e) { + echo get_class($e); +}'); +"#, + ); + assert_eq!(out, "frozen:Error"); +} + /// Verifies eval ReflectionClass reports method, property, and constant membership through the bridge. #[test] fn test_eval_reflection_class_member_existence() { From 949ca126367cd43df277639e0d51c5b9b7eafa78 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 14:25:15 +0200 Subject: [PATCH 0765/1208] Align eval readonly class static properties --- crates/elephc-magician/src/eval_ir.rs | 6 ++++-- .../src/interpreter/tests/classes.rs | 14 ++++++++------ docs/php/eval.md | 6 ++++-- tests/codegen/eval.rs | 12 ++++++++---- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index a46aaada21..e936e2ce62 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -1588,11 +1588,13 @@ impl EvalClass { self } - /// Marks all properties readonly when this metadata represents a `readonly class`. + /// Marks instance properties readonly when this metadata represents a `readonly class`. pub fn with_readonly_properties(mut self) -> Self { if self.is_readonly_class { for property in &mut self.properties { - property.is_readonly = true; + if !property.is_static { + property.is_readonly = true; + } } } self diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index dfce6eaccf..c9179ab7cb 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -543,22 +543,24 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies readonly classes reject static properties because PHP makes them readonly. +/// Verifies readonly classes leave static properties mutable like ordinary classes. #[test] -fn execute_program_rejects_readonly_class_static_property() { +fn execute_program_allows_readonly_class_static_property() { let program = parse_fragment( br#"readonly class EvalReadonlyStaticBox { public static int $count = 1; -}"#, +} +EvalReadonlyStaticBox::$count = EvalReadonlyStaticBox::$count + 1; +echo EvalReadonlyStaticBox::$count;"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("readonly class static property should fail"); + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.output, "2"); + assert_eq!(values.get(result), FakeValue::Null); } /// Verifies readonly classes may extend readonly parents and use inherited constructors. diff --git a/docs/php/eval.md b/docs/php/eval.md index 560bef7540..84846669f7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -558,8 +558,10 @@ Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the raw backing slot. `readonly` eval properties require a declared type, may be assigned from the constructor of the declaring class, and later writes fail as -eval runtime fatals. A `readonly class` makes declared properties readonly -implicitly, so static and untyped properties are rejected like PHP. +eval runtime fatals. A `readonly class` makes declared instance properties +readonly implicitly, while declared static properties remain mutable and are not +converted to readonly slots. Untyped instance properties in readonly classes are +still rejected. Missing-property writes can still dispatch through `__set()`, but readonly classes reject actual dynamic property creation. PHP's global `#[AllowDynamicProperties]` marker is rejected on eval-declared diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b4ebb804d2..b03d2346c2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8748,17 +8748,21 @@ echo $box->id() . ":" . $child->id();'); ); assert_eq!(out.stdout, "7:9"); - let static_err = compile_and_run_expect_failure( + let static_out = compile_and_run_capture( r#" Date: Fri, 26 Jun 2026 14:31:09 +0200 Subject: [PATCH 0766/1208] Allow eval property redeclarations to add readonly --- .../src/interpreter/statements.rs | 2 +- .../src/interpreter/tests/classes.rs | 21 +++++++++++++++++-- docs/php/eval.md | 5 +++-- tests/codegen/eval.rs | 21 +++++++++++++++++-- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index ef878858c7..ea07dca720 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2832,7 +2832,7 @@ fn validate_property_parent_redeclaration( return Err(EvalStatus::RuntimeFatal); } if parent_property.is_static() != property.is_static() - || parent_property.is_readonly() != property.is_readonly() + || (parent_property.is_readonly() && !property.is_readonly()) || property_visibility_rank(property.visibility()) < property_visibility_rank(parent_property.visibility()) || property_visibility_rank(property.write_visibility()) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index c9179ab7cb..bbdf847733 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -814,9 +814,26 @@ class EvalPropertyRelativeChild extends EvalPropertyRelativeBase { public self $selfValue; public parent $parentValue; } +class EvalPropertyReadonlyAddBase { + public int $count = 0; +} +class EvalPropertyReadonlyAddChild extends EvalPropertyReadonlyAddBase { + public readonly int $count; + public function __construct() { $this->count = 7; } +} +class EvalPropertyReadonlyWidenBase { + protected int $count = 0; + public function count() { return $this->count; } +} +class EvalPropertyReadonlyWidenChild extends EvalPropertyReadonlyWidenBase { + public readonly int $count; + public function __construct() { $this->count = 9; } +} $box = new EvalPropertyRedeclareChild(); $box->value = "ok"; -return $box->value;"#, +$readonly = new EvalPropertyReadonlyAddChild(); +$widened = new EvalPropertyReadonlyWidenChild(); +return $box->value . ":" . $readonly->count . ":" . $widened->count . ":" . $widened->count();"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -824,7 +841,7 @@ return $box->value;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::String("ok".to_string())); + assert_eq!(values.get(result), FakeValue::String("ok:7:9:9".to_string())); } /// Verifies eval rejects inherited property redeclarations that violate PHP invariance. diff --git a/docs/php/eval.md b/docs/php/eval.md index 84846669f7..17ffe3fda7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -164,8 +164,9 @@ Abstract classes may defer missing interface methods and property contracts, but declared or inherited members that cover an interface contract are validated at declaration time. Eval validates inherited property redeclarations with PHP-style invariant -property types, matching static/readonly modifiers, and compatible read/write -visibility. +property types, matching static storage, compatible read/write visibility, and +readonly compatibility: child redeclarations may add `readonly`, but may not +remove inherited `readonly`. Eval validates inherited class/interface constant redeclarations with PHP-style visibility compatibility, and rejects non-public interface constants. Eval-declared method calls also enforce declared return values at runtime, with diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b03d2346c2..adbd7d76a0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12319,12 +12319,29 @@ class EvalPropertyRelativeChild extends EvalPropertyRelativeBase { public self $selfValue; public parent $parentValue; } +class EvalPropertyReadonlyAddBase { + public int $count = 0; +} +class EvalPropertyReadonlyAddChild extends EvalPropertyReadonlyAddBase { + public readonly int $count; + public function __construct() { $this->count = 7; } +} +class EvalPropertyReadonlyWidenBase { + protected int $count = 0; + public function count() { return $this->count; } +} +class EvalPropertyReadonlyWidenChild extends EvalPropertyReadonlyWidenBase { + public readonly int $count; + public function __construct() { $this->count = 9; } +} $box = new EvalPropertyRedeclareChild(); $box->value = "ok"; -echo $box->value;'); +$readonly = new EvalPropertyReadonlyAddChild(); +$widened = new EvalPropertyReadonlyWidenChild(); +echo $box->value . ":" . $readonly->count . ":" . $widened->count . ":" . $widened->count();'); "#, ); - assert_eq!(out, "ok"); + assert_eq!(out, "ok:7:9:9"); let err = compile_and_run_expect_failure( r#" Date: Fri, 26 Jun 2026 14:40:16 +0200 Subject: [PATCH 0767/1208] Validate eval override attributes --- .../src/interpreter/statements.rs | 62 +++++++++++++++++++ .../src/interpreter/tests/classes.rs | 42 +++++++++++++ docs/php/eval.md | 2 + tests/codegen/eval.rs | 37 +++++++++++ 4 files changed, 143 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index ea07dca720..88945c5705 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2473,6 +2473,7 @@ fn validate_eval_class_modifiers( return Err(EvalStatus::RuntimeFatal); } validate_method_parent_override(class, method, context)?; + validate_eval_override_attribute(class, method, context)?; } Ok(()) } @@ -2485,6 +2486,67 @@ fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool .any(|attribute| attribute.name().eq_ignore_ascii_case("AllowDynamicProperties")) } +/// Validates PHP's global `#[Override]` marker on one eval-declared method. +fn validate_eval_override_attribute( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !eval_method_has_global_builtin_attribute(method, "Override") { + return Ok(()); + } + if eval_method_overrides_parent(class, method, context) + || eval_method_implements_interface(class, method, context) + { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether a method has an unqualified global builtin marker attribute. +fn eval_method_has_global_builtin_attribute(method: &EvalClassMethod, builtin: &str) -> bool { + method + .attributes() + .iter() + .any(|attribute| attribute.name().eq_ignore_ascii_case(builtin)) +} + +/// Returns whether one method overrides a non-private parent method. +fn eval_method_overrides_parent( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, +) -> bool { + class + .parent() + .and_then(|parent| context.class_method(parent, method.name())) + .is_some_and(|(_, parent_method)| { + parent_method.visibility() != EvalVisibility::Private + && parent_method.is_static() == method.is_static() + }) +} + +/// Returns whether one method implements a direct or inherited interface method. +fn eval_method_implements_interface( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, +) -> bool { + pending_class_interface_names(class, context) + .iter() + .filter(|interface| context.has_interface(interface)) + .any(|interface| { + context + .interface_method_requirements_with_owners(interface) + .into_iter() + .any(|(_, requirement)| { + requirement.name().eq_ignore_ascii_case(method.name()) + && requirement.is_static() == method.is_static() + }) + }) +} + /// Validates PHP magic-method contracts for one eval class-like method list. fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalStatus> { for method in methods { diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index bbdf847733..077933a4bd 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -543,6 +543,48 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval validates PHP's global `#[Override]` method marker. +#[test] +fn execute_program_validates_override_attribute_targets() { + let valid = parse_fragment( + br#"interface EvalOverrideContract { + public function label(): string; +} +class EvalOverrideBase { + public function name(): string { return "base"; } +} +class EvalOverrideChild extends EvalOverrideBase implements EvalOverrideContract { + #[\Override] + public function name(): string { return "child"; } + #[Override] + public function label(): string { return "contract"; } +} +$box = new EvalOverrideChild(); +echo $box->name() . ":" . $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&valid, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child:contract"); + + let invalid = parse_fragment( + br#"class EvalOverrideMissing { + #[\Override] + public function missing(): string { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&invalid, &mut scope, &mut values) + .expect_err("override marker without target should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies readonly classes leave static properties mutable like ordinary classes. #[test] fn execute_program_allows_readonly_class_static_property() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 17ffe3fda7..1198d17570 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -156,6 +156,8 @@ properties, static methods, static interface method contracts, class, interface, trait, and enum constants including `final` constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and `__callStatic()`, and magic property fallback through `__get()` and `__set()`. +PHP's global `#[Override]` marker is validated on eval-declared methods against +non-private parent methods and eval interface method contracts. Eval validates method override and interface method parameter and return types with PHP-style parameter contravariance and return covariance for supported declared type metadata, including nullable, union, `mixed`, `self`, `parent`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index adbd7d76a0..92b2e414f3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10781,6 +10781,43 @@ fn test_eval_rejects_invalid_magic_method_contracts() { } } +/// Verifies eval-declared `#[Override]` methods require a parent or interface target. +#[test] +fn test_eval_declared_override_attribute_validation() { + let out = compile_and_run( + r#"name() . ":" . $box->label();'); +"#, + ); + assert_eq!(out, "child:contract"); + + let err = compile_and_run_expect_failure( + r#" Date: Fri, 26 Jun 2026 14:52:21 +0200 Subject: [PATCH 0768/1208] Support eval deprecated reflection metadata --- .../src/interpreter/reflection.rs | 64 +++++++++++++++++-- .../tests/builtins_class_metadata.rs | 25 ++++++++ .../tests/builtins_reflection_functions.rs | 23 +++++++ .../interpreter/tests/support/object_ops.rs | 4 ++ docs/php/eval.md | 6 +- src/codegen/eval_reflection_owner_helpers.rs | 23 +++++++ tests/codegen/eval.rs | 31 +++++++++ 7 files changed, 167 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 2ce778b5b8..9923dac487 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -49,6 +49,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; const EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC: u64 = 8192; +const EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED: u64 = 16384; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -201,6 +202,7 @@ enum EvalReflectionFunctionMethodTarget { source_location: Option, is_variadic: bool, is_static: bool, + is_deprecated: bool, return_type_metadata: Option, }, Method { @@ -215,6 +217,7 @@ enum EvalReflectionFunctionMethodTarget { is_static: bool, is_final: bool, is_abstract: bool, + is_deprecated: bool, return_type_metadata: Option, }, } @@ -1221,10 +1224,15 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( } "isinternal" | "isclosure" - | "isdeprecated" | "returnsreference" | "isgenerator" | "hastentativereturntype" => eval_reflection_false_metadata_result(evaluated_args, values), + "isdeprecated" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_deprecated(&target)) + .map(Some) + } "isanonymous" => match target { EvalReflectionFunctionMethodTarget::Function { .. } => { eval_reflection_false_metadata_result(evaluated_args, values) @@ -3113,7 +3121,7 @@ fn eval_reflection_function_object_result( parameters, None, None, - 0, + eval_reflection_callable_flags(attributes), required_parameter_count as u64, 0, None, @@ -5567,6 +5575,9 @@ fn eval_reflection_member_object_result( if member.is_dynamic { flags |= EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC; } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + flags |= eval_reflection_callable_flags(&member.attributes); + } let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { member.required_parameter_count as u64 } else { @@ -6895,13 +6906,14 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_variadic(), ); - let flags = eval_reflection_member_flags( + let mut flags = eval_reflection_member_flags( method.visibility(), method.is_static(), method.is_final(), method.is_abstract(), false, ); + flags |= eval_reflection_callable_flags(method.attributes()); let return_type_metadata = method .return_type() .and_then(eval_reflection_parameter_type_metadata); @@ -6965,13 +6977,14 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_variadic(), ); - let flags = eval_reflection_member_flags( + let mut flags = eval_reflection_member_flags( EvalVisibility::Public, method.is_static(), false, true, false, ); + flags |= eval_reflection_callable_flags(method.attributes()); let return_type_metadata = method .return_type() .and_then(eval_reflection_parameter_type_metadata); @@ -7029,13 +7042,14 @@ fn eval_reflection_method_metadata( method.parameter_defaults(), method.parameter_is_variadic(), ); - let flags = eval_reflection_member_flags( + let mut flags = eval_reflection_member_flags( method.visibility(), method.is_static(), method.is_final(), method.is_abstract(), false, ); + flags |= eval_reflection_callable_flags(method.attributes()); let return_type_metadata = method .return_type() .and_then(eval_reflection_parameter_type_metadata); @@ -8516,11 +8530,12 @@ fn eval_reflection_function_parameters( .iter() .map(Option::is_some) .collect::>(); + let flags = eval_reflection_callable_flags(&function_attributes); let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: function_name.to_string(), declaring_class_name: None, attributes: function_attributes, - flags: 0, + flags, required_parameter_count: eval_reflection_required_parameter_count( defaults, variadic_flags, @@ -8723,6 +8738,9 @@ fn eval_reflection_function_method_target( let static_variables = function .map(|function| static_var_initializers(function.body())) .unwrap_or_default(); + let is_deprecated = function + .map(EvalFunction::attributes) + .is_some_and(eval_reflection_attributes_include_deprecated); return Ok(Some(EvalReflectionFunctionMethodTarget::Function { name: name.to_string(), static_key, @@ -8731,6 +8749,7 @@ fn eval_reflection_function_method_target( source_location, is_variadic, is_static: false, + is_deprecated, return_type_metadata, })); } @@ -8757,6 +8776,7 @@ fn eval_reflection_function_method_target( is_static, is_final, is_abstract, + is_deprecated, return_type_metadata, ) = match method_metadata { Some(method) => { @@ -8764,6 +8784,8 @@ fn eval_reflection_function_method_target( .parameters .iter() .any(|parameter| parameter.is_variadic); + let is_deprecated = + eval_reflection_attributes_include_deprecated(&method.attributes); ( method.parameters, method.source_location, @@ -8772,10 +8794,11 @@ fn eval_reflection_function_method_target( method.is_static, method.is_final, method.is_abstract, + is_deprecated, method.return_type_metadata, ) } - None => (Vec::new(), None, None, false, false, false, false, None), + None => (Vec::new(), None, None, false, false, false, false, false, None), }; let static_method = eval_reflection_eval_method_static_target(declaring_class, method_name, context); @@ -8801,6 +8824,7 @@ fn eval_reflection_function_method_target( is_static, is_final, is_abstract, + is_deprecated, return_type_metadata, })) } @@ -9023,6 +9047,16 @@ fn eval_reflection_function_method_is_static(target: &EvalReflectionFunctionMeth } } +/// Returns whether the reflected function-like target carries `#[Deprecated]`. +fn eval_reflection_function_method_is_deprecated( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_deprecated, .. } + | EvalReflectionFunctionMethodTarget::Method { is_deprecated, .. } => *is_deprecated, + } +} + /// Returns the retained return type metadata for a reflected function or method. fn eval_reflection_function_method_return_type( target: &EvalReflectionFunctionMethodTarget, @@ -9164,6 +9198,22 @@ fn eval_reflection_member_flags( flags } +/// Packs callable-only ReflectionFunctionAbstract predicate flags. +fn eval_reflection_callable_flags(attributes: &[EvalAttribute]) -> u64 { + if eval_reflection_attributes_include_deprecated(attributes) { + EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED + } else { + 0 + } +} + +/// Returns whether an attribute list contains PHP's global `#[Deprecated]` marker. +fn eval_reflection_attributes_include_deprecated(attributes: &[EvalAttribute]) -> bool { + attributes + .iter() + .any(|attribute| attribute.name().eq_ignore_ascii_case("Deprecated")) +} + /// Packs ReflectionParameter predicate flags for the runtime parameter factory. fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) -> u64 { let mut flags = 0; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 2d038fb3d7..fe4d97d4fd 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -477,6 +477,31 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionMethod derives `isDeprecated()` from eval-retained attributes. +#[test] +fn execute_program_reflection_method_reports_deprecated_attribute() { + let program = parse_fragment( + br#"class EvalReflectDeprecatedMethodTarget { + #[\Deprecated] + public function old() {} + public function fresh() {} +} +$deprecated = new ReflectionMethod(EvalReflectDeprecatedMethodTarget::class, "old"); +$plain = new ReflectionMethod(EvalReflectDeprecatedMethodTarget::class, "fresh"); +echo $deprecated->isDeprecated() ? "D" : "d"; echo ":"; +echo $plain->isDeprecated() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "D:d"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionMethod exposes eval static locals using the declaring class key. #[test] fn execute_program_reflection_method_reports_static_variables() { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs index e5cd4e9a15..73f9ec32f3 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -172,6 +172,29 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionFunction derives `isDeprecated()` from eval-retained attributes. +#[test] +fn execute_program_reflection_function_reports_deprecated_attribute() { + let program = parse_fragment( + br#"#[Deprecated] +function eval_reflect_deprecated_function() {} +function eval_reflect_plain_function() {} +$deprecated = new ReflectionFunction("eval_reflect_deprecated_function"); +$plain = new ReflectionFunction("eval_reflect_plain_function"); +echo $deprecated->isDeprecated() ? "D" : "d"; echo ":"; +echo $plain->isDeprecated() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "D:d"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionFunction exposes PHP-compatible name and origin predicate metadata. #[test] fn execute_program_reflection_function_reports_name_and_origin_predicates() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 8479bab03c..3a19b1559e 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -24,6 +24,7 @@ const EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE: u64 = 256; const EVAL_REFLECTION_MEMBER_FLAG_PROMOTED: u64 = 512; const EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL: u64 = 1024; const EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC: u64 = 8192; +const EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED: u64 = 16384; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -763,8 +764,11 @@ impl FakeOps { owner_kind, EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION ) { + let is_deprecated = + self.bool_value((flags & EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED) != 0)?; properties.push(("__parameters".to_string(), method_objects)); properties.push(("__required_parameter_count".to_string(), modifiers_cell)); + properties.push(("__is_deprecated".to_string(), is_deprecated)); } if owner_kind == EVAL_REFLECTION_OWNER_METHOD { let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; diff --git a/docs/php/eval.md b/docs/php/eval.md index 1198d17570..41c4ba4ffb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -262,12 +262,14 @@ name. `ReflectionMethod::getShortName()` reports the reflected method name, while `ReflectionMethod::getNamespaceName()` reports an empty string and `inNamespace()` reports `false`, matching PHP's method reflection behavior. `ReflectionFunction` and `ReflectionMethod` report eval user-symbol defaults -through `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, -`returnsReference()`, `isGenerator()`, `isVariadic()`, `isStatic()`, +through `isInternal()`, `isUserDefined()`, `isClosure()`, `returnsReference()`, +`isGenerator()`, `isVariadic()`, `isStatic()`, `hasTentativeReturnType()`, and `getTentativeReturnType()`. `hasReturnType()` and `getReturnType()` expose retained eval return type metadata for supported named, nullable, union, and intersection declarations, including `void` and `never` as builtin non-nullable named types. +`isDeprecated()` reflects PHP's global `#[Deprecated]` attribute on +eval-declared functions and methods, and reports `false` otherwise. `ReflectionFunction::isAnonymous()` reports `false` for eval-declared named functions, and `getClosureThis()`, `getClosureScopeClass()`, and `getClosureCalledClass()` report `null` for eval-visible non-closure function diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 11571211a0..78c6555ae8 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -76,6 +76,8 @@ struct ReflectionOwnerLayout { is_internal_hi: Option, is_user_defined_lo: Option, is_user_defined_hi: Option, + is_deprecated_lo: Option, + is_deprecated_hi: Option, modifiers_lo: Option, modifiers_hi: Option, in_namespace_lo: Option, @@ -259,6 +261,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option OptionisStatic() ? "S" : "s");'); ); } +/// Verifies eval ReflectionFunction/Method derive deprecation predicates from `#[Deprecated]`. +#[test] +fn test_eval_reflection_function_and_method_deprecated_attributes() { + let out = compile_and_run_capture( + r#"isDeprecated() ? "D" : "d") . ":"; +echo ($plainFn->isDeprecated() ? "D" : "d") . ":"; +echo ($deprecatedMethod->isDeprecated() ? "D" : "d") . ":"; +echo ($plainMethod->isDeprecated() ? "D" : "d");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "D:d:D:d"); +} + /// Verifies eval ReflectionFunction/Method expose static local variables through the bridge. #[test] fn test_eval_reflection_function_and_method_static_variables() { From 0d7a6fa5af005ae9c05496a0bddb09ab4976bfb6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 15:18:22 +0200 Subject: [PATCH 0769/1208] Support eval first-class callable syntax --- crates/elephc-magician/src/eval_ir.rs | 4 + .../src/interpreter/builtins/arrays/core.rs | 27 ++--- .../builtins/arrays/filters/filter.rs | 12 ++- .../interpreter/builtins/arrays/sort/user.rs | 8 +- .../src/interpreter/expressions.rs | 26 +++++ .../src/interpreter/tests/dynamic_calls.rs | 76 +++++++++++++ .../elephc-magician/src/parser/expressions.rs | 102 ++++++++++++++++++ .../elephc-magician/src/parser/statements.rs | 1 + .../src/parser/tests/arrays_objects.rs | 12 +++ .../elephc-magician/src/parser/tests/calls.rs | 13 +++ .../src/parser/tests/static_members.rs | 16 +++ docs/php/eval.md | 24 +++-- tests/codegen/eval.rs | 73 +++++++++++++ 13 files changed, 365 insertions(+), 29 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index e936e2ce62..0528ad9445 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2423,6 +2423,10 @@ pub enum EvalExpr { }, Const(EvalConst), ConstFetch(String), + FunctionCallable { + name: String, + fallback_name: Option, + }, DynamicCall { callee: Box, args: Vec, diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs index 99eb6332cf..37971387e2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs @@ -219,7 +219,7 @@ pub(in crate::interpreter) fn eval_array_fill_keys_result( Ok(result) } -/// Evaluates PHP `array_map()` for one source array and a string or null callback. +/// Evaluates PHP `array_map()` for one source array and a callable or null callback. pub(in crate::interpreter) fn eval_builtin_array_map( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -250,15 +250,15 @@ pub(in crate::interpreter) fn eval_array_map_result( let callback = if values.is_null(callback)? { None } else { - Some(eval_callable_name(callback, values)?) + Some(eval_callable(callback, context, values)?) }; let len = values.array_len(*array)?; let mut result = values.assoc_new(len)?; for position in 0..len { let key = values.array_iter_key(*array, position)?; let value = values.array_get(*array, key)?; - let mapped = if let Some(callback) = callback.as_deref() { - eval_callable_with_values(callback, vec![value], context, values)? + let mapped = if let Some(callback) = callback.as_ref() { + eval_evaluated_callable_with_values(callback, vec![value], context, values)? } else { value }; @@ -280,7 +280,7 @@ pub(in crate::interpreter) fn eval_array_map_variadic_result( let callback = if values.is_null(callback)? { None } else { - Some(eval_callable_name(callback, values)?) + Some(eval_callable(callback, context, values)?) }; let mut lengths = Vec::with_capacity(arrays.len()); let mut max_len = 0; @@ -302,8 +302,8 @@ pub(in crate::interpreter) fn eval_array_map_variadic_result( }; callback_args.push(value); } - let mapped = if let Some(callback) = callback.as_deref() { - eval_callable_with_values(callback, callback_args, context, values)? + let mapped = if let Some(callback) = callback.as_ref() { + eval_evaluated_callable_with_values(callback, callback_args, context, values)? } else { eval_array_map_zipped_row(callback_args, values)? }; @@ -350,7 +350,7 @@ pub(in crate::interpreter) fn eval_builtin_array_reduce( eval_array_reduce_result(array, callback, initial, context, values) } -/// Reduces one eval array by invoking a string callback with carry and item cells. +/// Reduces one eval array by invoking a callable with carry and item cells. pub(in crate::interpreter) fn eval_array_reduce_result( array: RuntimeCellHandle, callback: RuntimeCellHandle, @@ -358,13 +358,14 @@ pub(in crate::interpreter) fn eval_array_reduce_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable_name(callback, values)?; + let callback = eval_callable(callback, context, values)?; let len = values.array_len(array)?; let mut carry = initial; for position in 0..len { let key = values.array_iter_key(array, position)?; let value = values.array_get(array, key)?; - carry = eval_callable_with_values(&callback, vec![carry, value], context, values)?; + carry = + eval_evaluated_callable_with_values(&callback, vec![carry, value], context, values)?; } Ok(carry) } @@ -384,19 +385,19 @@ pub(in crate::interpreter) fn eval_builtin_array_walk( eval_array_walk_result(array, callback, context, values) } -/// Walks one eval array by invoking a string callback with value and key cells. +/// Walks one eval array by invoking a callable with value and key cells. pub(in crate::interpreter) fn eval_array_walk_result( array: RuntimeCellHandle, callback: RuntimeCellHandle, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable_name(callback, values)?; + let callback = eval_callable(callback, context, values)?; let len = values.array_len(array)?; for position in 0..len { let key = values.array_iter_key(array, position)?; let value = values.array_get(array, key)?; - let _ = eval_callable_with_values(&callback, vec![value, key], context, values)?; + let _ = eval_evaluated_callable_with_values(&callback, vec![value, key], context, values)?; } values.bool_value(true) } diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs index 14471ebc47..54b410f198 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs @@ -11,7 +11,7 @@ use super::super::super::super::*; use super::super::super::*; -/// Evaluates PHP `array_filter()` for null and string-callback filtering modes. +/// Evaluates PHP `array_filter()` for null and callable filtering modes. pub(in crate::interpreter) fn eval_builtin_array_filter( args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -38,7 +38,7 @@ pub(in crate::interpreter) fn eval_builtin_array_filter( } } -/// Filters eval array entries through PHP truthiness or a string callback. +/// Filters eval array entries through PHP truthiness or a callable callback. pub(in crate::interpreter) fn eval_array_filter_result( array: RuntimeCellHandle, callback: Option, @@ -47,7 +47,9 @@ pub(in crate::interpreter) fn eval_array_filter_result( values: &mut impl RuntimeValueOps, ) -> Result { let callback = match callback { - Some(callback) if !values.is_null(callback)? => Some(eval_callable_name(callback, values)?), + Some(callback) if !values.is_null(callback)? => { + Some(eval_callable(callback, context, values)?) + } _ => None, }; let mode = match mode { @@ -60,9 +62,9 @@ pub(in crate::interpreter) fn eval_array_filter_result( for position in 0..len { let key = values.array_iter_key(array, position)?; let value = values.array_get(array, key)?; - let keep = if let Some(callback) = callback.as_deref() { + let keep = if let Some(callback) = callback.as_ref() { let args = eval_array_filter_callback_args(mode, key, value)?; - let result = eval_callable_with_values(callback, args, context, values)?; + let result = eval_evaluated_callable_with_values(callback, args, context, values)?; values.truthy(result)? } else { values.truthy(value)? diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs index 2fef10ae00..e7421e5429 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs @@ -41,7 +41,7 @@ pub(in crate::interpreter) fn eval_user_sort_replacement( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable_name(callback, values)?; + let callback = eval_callable(callback, context, values)?; let mut entries = eval_user_sort_entries(array, values)?; eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; if name == "usort" { @@ -68,7 +68,7 @@ pub(in crate::interpreter) fn eval_user_sort_entries( /// Sorts entries by repeatedly invoking the PHP comparator callback. pub(in crate::interpreter) fn eval_user_sort_entries_in_place( name: &str, - callback: &str, + callback: &EvaluatedCallable, entries: &mut [EvalUserSortEntry], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -95,7 +95,7 @@ pub(in crate::interpreter) fn eval_user_sort_entries_in_place( /// Invokes one user-sort comparator and returns its integer ordering result. pub(in crate::interpreter) fn eval_user_sort_compare( name: &str, - callback: &str, + callback: &EvaluatedCallable, left: &EvalUserSortEntry, right: &EvalUserSortEntry, context: &mut ElephcEvalContext, @@ -106,7 +106,7 @@ pub(in crate::interpreter) fn eval_user_sort_compare( } else { vec![left.value, right.value] }; - let result = eval_callable_with_values(callback, args, context, values)?; + let result = eval_evaluated_callable_with_values(callback, args, context, values)?; eval_int_value(result, values) } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index cb43f4cc55..8fc0e0ac88 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -38,6 +38,10 @@ pub(in crate::interpreter) fn eval_expr( EvalExpr::Cast { target, expr } => eval_cast_expr(target, expr, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), + EvalExpr::FunctionCallable { + name, + fallback_name, + } => eval_function_callable_expr(name, fallback_name.as_deref(), context, values), EvalExpr::DynamicCall { callee, args } => { eval_dynamic_call(callee, args, context, scope, values) } @@ -762,6 +766,28 @@ pub(in crate::interpreter) fn eval_namespaced_call( eval_call(fallback_name, args, context, scope, values) } +/// Resolves a first-class function callable name with PHP namespace fallback rules. +fn eval_function_callable_expr( + name: &str, + fallback_name: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_function_probe_exists(context, name) { + return values.string(name); + } + if let Some(fallback_name) = fallback_name { + if eval_function_probe_exists(context, fallback_name) { + return values.string(fallback_name); + } + } + eval_throw_error( + &format!("Call to undefined function {}()", name.trim_start_matches('\\')), + context, + values, + ) +} + /// Evaluates a variable or expression callable and dispatches it with source-order arguments. pub(in crate::interpreter) fn eval_dynamic_call( callee: &EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 28fd1b59cf..4b0cf18e48 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -201,6 +201,82 @@ return $named(right: "H", left: "G");"#, assert_eq!(values.get(result), FakeValue::String("GH".to_string())); } +/// Verifies first-class callable syntax dispatches through eval's callback paths. +#[test] +fn execute_program_first_class_callables_dispatch_functions_and_methods() { + let program = parse_fragment( + br#"function eval_fc_double($value) { + return $value * 2; +} +class EvalFirstClassCallableBase { + public function __construct($offset = 1) { + $this->offset = $offset; + } + public function add($value) { + return $value + $this->offset; + } + public function keep($value) { + return $value > 2; + } + public function sum($carry, $value) { + return $carry + $value + $this->offset; + } + public function show($value, $key) { + echo $key . $value; + } + public static function join($left, $right) { + return $left . $right; + } + public static function compareDesc($left, $right) { + return $right - $left; + } + public static function label($value) { + return "base:" . $value; + } + public static function relay($value) { + $fn = static::label(...); + return $fn($value); + } +} +class EvalFirstClassCallableChild extends EvalFirstClassCallableBase { + public static function label($value) { + return "child:" . $value; + } +} +$function = eval_fc_double(...); +echo $function(4); echo ":"; +echo (strlen(...))("abcd"); echo ":"; +$box = new EvalFirstClassCallableBase(3); +$method = $box->add(...); +echo $method(4); echo ":"; +echo call_user_func($box->add(...), 5); echo ":"; +$static = EvalFirstClassCallableBase::join(...); +echo $static(right: "B", left: "A"); echo ":"; +$mapped = array_map($box->add(...), [1, 2]); +echo $mapped[0] . $mapped[1] . ":"; +$filtered = array_filter([1, 2, 3, 4], $box->keep(...)); +echo count($filtered) . ":"; +echo array_reduce([1, 2], $box->sum(...), 0) . ":"; +array_walk(["a" => 1], $box->show(...)); +echo ":"; +$sorted = [3, 1, 2]; +usort($sorted, EvalFirstClassCallableBase::compareDesc(...)); +echo $sorted[0] . $sorted[1] . $sorted[2] . ":"; +return EvalFirstClassCallableChild::relay("ok");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:4:7:8:AB:45:2:9:a1:321:"); + assert_eq!( + values.get(result), + FakeValue::String("child:ok".to_string()) + ); +} + /// Verifies invokable eval objects dispatch through variable and callback call paths. #[test] fn execute_program_invokes_eval_object_callables() { diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index c3020508e6..db2644f38b 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -554,6 +554,15 @@ impl Parser { nullsafe: bool, ) -> Result { if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + if nullsafe { + return Err(EvalParseError::UnsupportedConstruct); + } + return Ok(Self::callable_array_expr( + object, + EvalExpr::Const(EvalConst::String(member)), + )); + } let args = self.parse_call_args()?; return Ok(if nullsafe { EvalExpr::NullsafeMethodCall { @@ -590,6 +599,12 @@ impl Parser { nullsafe: bool, ) -> Result { if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + if nullsafe { + return Err(EvalParseError::UnsupportedConstruct); + } + return Ok(Self::callable_array_expr(object, member)); + } let args = self.parse_call_args()?; return Ok(if nullsafe { EvalExpr::NullsafeDynamicMethodCall { @@ -788,6 +803,9 @@ impl Parser { /// Parses a function-like call expression and its source-order arguments. pub(super) fn parse_call_expr(&mut self, name: String) -> Result { self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(self.function_callable_expr(name)); + } let args = self.parse_call_args()?; Ok(self.call_expr(name, args)) } @@ -801,6 +819,9 @@ impl Parser { } let name = self.resolve_qualified_name(name); if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::function_callable_value(name.to_ascii_lowercase(), None)); + } let args = self.parse_call_args()?; return Ok(EvalExpr::Call { name: name.to_ascii_lowercase(), @@ -820,6 +841,12 @@ impl Parser { let property = property.clone(); self.advance(); if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::callable_array_expr( + EvalExpr::ClassNameFetch { class_name }, + EvalExpr::LoadVar(property), + )); + } let args = self.parse_call_args()?; return Ok(EvalExpr::DynamicStaticMethodCall { class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), @@ -850,6 +877,12 @@ impl Parser { TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { let method = method.clone(); self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(Self::callable_array_expr( + EvalExpr::ClassNameFetch { class_name }, + EvalExpr::Const(EvalConst::String(method)), + )); + } let args = self.parse_call_args()?; Ok(EvalExpr::StaticMethodCall { class_name, @@ -862,6 +895,12 @@ impl Parser { let member = self.parse_expr()?; self.expect(TokenKind::RBrace)?; if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::callable_array_expr( + EvalExpr::ClassNameFetch { class_name }, + member, + )); + } let args = self.parse_call_args()?; return Ok(EvalExpr::DynamicStaticMethodCall { class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), @@ -896,6 +935,12 @@ impl Parser { let member = member.clone(); self.advance(); if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::callable_array_expr( + class_name, + EvalExpr::LoadVar(member), + )); + } let args = self.parse_call_args()?; return Ok(EvalExpr::DynamicStaticMethodCall { class_name: Box::new(class_name), @@ -928,6 +973,12 @@ impl Parser { TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { let method = method.clone(); self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(Self::callable_array_expr( + class_name, + EvalExpr::Const(EvalConst::String(method)), + )); + } let args = self.parse_call_args()?; Ok(EvalExpr::DynamicStaticMethodCall { class_name: Box::new(class_name), @@ -940,6 +991,9 @@ impl Parser { let member = self.parse_expr()?; self.expect(TokenKind::RBrace)?; if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::callable_array_expr(class_name, member)); + } let args = self.parse_call_args()?; return Ok(EvalExpr::DynamicStaticMethodCall { class_name: Box::new(class_name), @@ -1145,6 +1199,54 @@ impl Parser { self.parse_expr().map(EvalCallArg::positional) } + /// Consumes PHP's `(...)` first-class callable marker when it is the whole argument list. + fn consume_first_class_callable_marker(&mut self) -> bool { + if matches!(self.current(), TokenKind::LParen) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::Ellipsis)) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::RParen)) + { + self.advance(); + self.advance(); + self.advance(); + true + } else { + false + } + } + + /// Builds an eval function-callable expression with namespace fallback metadata. + fn function_callable_expr(&self, name: String) -> EvalExpr { + if let Some(imported) = self.imports.resolve_function(&name) { + return Self::function_callable_value(imported.to_ascii_lowercase(), None); + } + let fallback_name = name.to_ascii_lowercase(); + if self.namespace.is_empty() { + Self::function_callable_value(fallback_name, None) + } else { + Self::function_callable_value( + self.qualify_name_in_current_namespace(&name) + .to_ascii_lowercase(), + Some(fallback_name), + ) + } + } + + /// Builds the EvalIR node that resolves a first-class function callable at runtime. + fn function_callable_value(name: String, fallback_name: Option) -> EvalExpr { + EvalExpr::FunctionCallable { + name, + fallback_name, + } + } + + /// Builds the PHP callable-array value used for method first-class callables. + fn callable_array_expr(receiver: EvalExpr, method: EvalExpr) -> EvalExpr { + EvalExpr::Array(vec![ + EvalArrayElement::Value(receiver), + EvalArrayElement::Value(method), + ]) + } + /// Parses an array literal with source-order optional key/value element expressions. pub(super) fn parse_array_literal(&mut self) -> Result { self.expect(TokenKind::LBracket)?; diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 18906bf465..120bf59fa4 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3990,6 +3990,7 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { } EvalExpr::Const(_) | EvalExpr::ConstFetch(_) + | EvalExpr::FunctionCallable { .. } | EvalExpr::ClassConstantFetch { .. } | EvalExpr::ClassNameFetch { .. } | EvalExpr::LoadVar(_) diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index 6b5189394c..2c95c6db1e 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -135,6 +135,18 @@ fn parse_fragment_accepts_method_call_source() { }))] ); } +/// Verifies first-class object method syntax lowers to a PHP callable array. +#[test] +fn parse_fragment_accepts_first_class_method_callable_source() { + let program = parse_fragment(br#"return $this->Answer(...);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::LoadVar("this".to_string())), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("Answer".to_string()))), + ])))] + ); +} /// Verifies braced dynamic object property reads parse as runtime-name EvalIR expressions. #[test] fn parse_fragment_accepts_dynamic_property_read_source() { diff --git a/crates/elephc-magician/src/parser/tests/calls.rs b/crates/elephc-magician/src/parser/tests/calls.rs index 8079d3e8d8..43b0747d9d 100644 --- a/crates/elephc-magician/src/parser/tests/calls.rs +++ b/crates/elephc-magician/src/parser/tests/calls.rs @@ -90,6 +90,19 @@ fn parse_fragment_accepts_qualified_call_expression_source() { }))] ); } +/// Verifies first-class function callable syntax lowers to runtime callable resolution metadata. +#[test] +fn parse_fragment_accepts_first_class_function_callable_source() { + let program = parse_fragment(br#"namespace App; return strlen(...);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::FunctionCallable { + name: "app\\strlen".to_string(), + fallback_name: Some("strlen".to_string()), + }))] + ); +} /// Verifies variable callable expressions lower to dynamic calls with source-order args. #[test] fn parse_fragment_accepts_dynamic_call_expression_source() { diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index a9718b54a7..b334e61b58 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -64,6 +64,22 @@ fn parse_fragment_accepts_static_method_call_expression() { ); } +/// Verifies first-class static method syntax lowers to a PHP callable array. +#[test] +fn parse_fragment_accepts_first_class_static_method_callable_source() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(...);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("Read".to_string()))), + ])))] + ); +} + /// Verifies static method calls preserve named arguments in source order. #[test] fn parse_fragment_accepts_named_static_method_call_expression() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 41c4ba4ffb..331e9ce1d8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -73,7 +73,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, `isset()` / `empty()` probes, array writes/appends, array-element unsets, and increment/decrement, static-property unset attempts including dynamic names as PHP-compatible catchable errors, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | -| Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | +| Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, first-class callable syntax for supported function and method targets, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new [readonly] class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | @@ -116,6 +116,13 @@ String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share the eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. +First-class callable syntax such as `strlen(...)`, `$object->method(...)`, and +`ClassName::method(...)` materializes eval callback values that can be invoked +through `$callback(...)`, `call_user_func()`, and `call_user_func_array()`. +Namespaced function callables follow PHP's global fallback rule when the +namespaced function is not visible. The eval bridge does not model these values +as full `Closure` objects yet. + Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, ...)`, `call_user_func_array($cb, [...])`, and `iterator_apply()`. Eval-declared @@ -687,10 +694,13 @@ including dynamic property names, and static properties including dynamic receivers or dynamic property names. Callable dispatch of `settype()` remains by-value and emits PHP's by-reference warning. -Eval `array_map()` supports one or more source arrays with a string callback or -`null` callback. One-array results preserve source keys, multi-array results -are reindexed, missing source values are padded with `null`, and -`array_map(null, ...)` returns zipped row arrays. +Eval `array_map()` supports one or more source arrays with supported callable +values or a `null` callback. `array_filter()`, `array_reduce()`, +`array_walk()`, `usort()`, `uasort()`, and `uksort()` share the same callback +dispatcher, including first-class function and method callback values. One-array +`array_map()` results preserve source keys, multi-array results are reindexed, +missing source values are padded with `null`, and `array_map(null, ...)` +returns zipped row arrays. Eval `count()` supports normal and recursive array counting and dispatches top-level eval-declared or generated/AOT objects implementing `Countable` @@ -752,8 +762,8 @@ Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In -particular, advanced native callable descriptors and closure callback values are -still outside eval fragments. Runtime/AOT object-method, static-method, and +particular, advanced native callable descriptors and full `Closure` object +semantics for callback values are still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch is limited to visibility-checked non-by-reference scalar/nullable scalar/Mixed/array/iterable/object diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c7e9156e76..c2dcd116eb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9714,6 +9714,79 @@ echo $named(right: "H", left: "G");'); assert_eq!(out.stdout, "AB:CD:EF:GH"); } +/// Verifies eval first-class callable syntax dispatches functions and methods. +#[test] +fn test_eval_first_class_callables_dispatch_functions_and_methods() { + let out = compile_and_run_capture( + r#"offset = $offset; + } + public function add($value) { + return $value + $this->offset; + } + public function keep($value) { + return $value > 2; + } + public function sum($carry, $value) { + return $carry + $value + $this->offset; + } + public function show($value, $key) { + echo $key . $value; + } + public static function join($left, $right) { + return $left . $right; + } + public static function compareDesc($left, $right) { + return $right - $left; + } + public static function label($value) { + return "base:" . $value; + } + public static function relay($value) { + $fn = static::label(...); + return $fn($value); + } +} +class EvalFirstClassCallableChild extends EvalFirstClassCallableBase { + public static function label($value) { + return "child:" . $value; + } +} +$function = eval_fc_double(...); +echo $function(4) . ":"; +echo (strlen(...))("abcd") . ":"; +$box = new EvalFirstClassCallableBase(3); +$method = $box->add(...); +echo $method(4) . ":"; +echo call_user_func($box->add(...), 5) . ":"; +$static = EvalFirstClassCallableBase::join(...); +echo $static(right: "B", left: "A") . ":"; +$mapped = array_map($box->add(...), [1, 2]); +echo $mapped[0] . $mapped[1] . ":"; +$filtered = array_filter([1, 2, 3, 4], $box->keep(...)); +echo count($filtered) . ":"; +echo array_reduce([1, 2], $box->sum(...), 0) . ":"; +array_walk(["a" => 1], $box->show(...)); +echo ":"; +$sorted = [3, 1, 2]; +usort($sorted, EvalFirstClassCallableBase::compareDesc(...)); +echo $sorted[0] . $sorted[1] . $sorted[2] . ":"; +echo EvalFirstClassCallableChild::relay("ok");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "8:4:7:8:AB:45:2:9:a1:321:child:ok"); +} + /// Verifies eval dynamic static receivers dispatch methods, properties, constants, and `::class`. #[test] fn test_eval_dynamic_static_receivers() { From 16a53efbe6b2db7a08f4d2ef596bb1c664560c41 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 15:53:26 +0200 Subject: [PATCH 0770/1208] Support eval SPL container dispatch --- src/codegen/eval_constructor_helpers.rs | 118 ++++++++++++++++-- src/codegen/eval_method_helpers.rs | 35 +++++- .../runtime/objects/new_by_name.rs | 63 ++++++++++ tests/codegen/eval.rs | 34 +++++ 4 files changed, 237 insertions(+), 13 deletions(-) diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index afa4ab3978..8a7ddb925e 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -20,9 +20,10 @@ use crate::codegen::abi; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; +use crate::intrinsics::IntrinsicCall; use crate::ir::{Function, LocalKind, Module}; use crate::names::{method_symbol, php_symbol_key}; -use crate::parser::ast::Visibility; +use crate::parser::ast::{ExprKind, Visibility}; use crate::types::{ClassInfo, FunctionSig, PhpType}; use super::eval_ref_arg_helpers::{ @@ -66,6 +67,8 @@ struct EvalConstructorSlot { params: Vec, ref_params: Vec, supported: bool, + runtime_helper: Option<&'static str>, + zero_default_first_arg: bool, } /// Emits eval constructor helpers when any lowered function owns an eval context. @@ -110,7 +113,7 @@ fn function_uses_eval(function: &Function) -> bool { }) } -/// Collects AOT constructors backed by emitted EIR symbols in stable class-id order. +/// Collects AOT and runtime-backed constructors in stable class-id order. fn collect_eval_constructor_slots(module: &Module) -> Vec { let emitted_methods = super::eir_class_method_keys(module); let mut slots = Vec::new(); @@ -134,7 +137,7 @@ fn collect_builtin_throwable_constructor_class_ids(module: &Module) -> Vec class_ids } -/// Adds one constructor slot for a class when the constructor has emitted code. +/// Adds one constructor slot for a class when the constructor has emitted code or a runtime helper. fn collect_class_constructor_slot( module: &Module, class_name: &str, @@ -151,7 +154,10 @@ fn collect_class_constructor_slot( .get(&method_key) .map(String::as_str) .unwrap_or(class_name); - if !emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), false)) { + let runtime_helper = eval_runtime_backed_constructor_helper(class_name, &method_key); + if runtime_helper.is_none() + && !emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), false)) + { return; } let visibility = constructor_visibility(class_info, &method_key); @@ -176,9 +182,44 @@ fn collect_class_constructor_slot( params, ref_params, supported, + runtime_helper, + zero_default_first_arg: constructor_uses_zero_default_first_arg( + class_name, + &method_key, + sig, + runtime_helper, + ), }); } +/// Returns a normal-ABI runtime helper for builtin constructors that eval can bridge. +fn eval_runtime_backed_constructor_helper( + class_name: &str, + method_key: &str, +) -> Option<&'static str> { + if class_name.trim_start_matches('\\') != "SplFixedArray" || method_key != "__construct" { + return None; + } + IntrinsicCall::instance_method(class_name, method_key)?.runtime_helper() +} + +/// Returns true when the first constructor parameter has PHP's builtin zero default. +fn constructor_uses_zero_default_first_arg( + class_name: &str, + method_key: &str, + sig: &FunctionSig, + runtime_helper: Option<&'static str>, +) -> bool { + runtime_helper.is_some() + && class_name.trim_start_matches('\\') == "SplFixedArray" + && method_key == "__construct" + && matches!(sig.params.first().map(|(_, ty)| ty.codegen_repr()), Some(PhpType::Int)) + && matches!( + sig.defaults.first().and_then(Option::as_ref).map(|expr| &expr.kind), + Some(ExprKind::IntLiteral(0)) + ) +} + /// Returns the declared constructor visibility, defaulting to public metadata. fn constructor_visibility<'a>(class_info: &'a ClassInfo, method_key: &str) -> &'a Visibility { class_info @@ -560,7 +601,11 @@ fn emit_aarch64_constructor_body( emit_aarch64_prepare_constructor_args(module, emitter, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); - abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); + let callee = slot + .runtime_helper + .map(str::to_string) + .unwrap_or_else(|| method_symbol(&slot.impl_class, "__construct")); + abi::emit_call_label(emitter, &callee); abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_aarch64_write_back_ref_args( @@ -596,7 +641,11 @@ fn emit_x86_64_constructor_body( emit_x86_64_prepare_constructor_args(module, emitter, slot, fail_label); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); - abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, "__construct")); + let callee = slot + .runtime_helper + .map(str::to_string) + .unwrap_or_else(|| method_symbol(&slot.impl_class, "__construct")); + abi::emit_call_label(emitter, &callee); abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_x86_64_write_back_ref_args( @@ -667,9 +716,19 @@ fn emit_aarch64_validate_constructor_arg_count( emitter.instruction("ldr x0, [sp, #24]"); // reload the eval argument array for arity validation let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("str x0, [sp, #32]"); // save the supplied constructor argument count abi::emit_load_int_immediate(emitter, "x9", slot.params.len() as i64); emitter.instruction("cmp x0, x9"); // compare supplied eval argument count with the constructor signature - emitter.instruction(&format!("b.ne {}", fail_label)); // reject constructor dispatch when arity differs + if slot.zero_default_first_arg { + let ok_label = format!("{}_argc_ok", constructor_body_label(module, slot)); + emitter.instruction(&format!("b.eq {}", ok_label)); // explicit argument count matches the constructor signature + emitter.instruction("cmp x0, #0"); // did eval omit the optional builtin constructor argument? + emitter.instruction(&format!("b.eq {}", ok_label)); // accept the builtin zero-default constructor call + emitter.instruction(&format!("b {}", fail_label)); // reject constructor dispatch when arity differs + emitter.label(&ok_label); + } else { + emitter.instruction(&format!("b.ne {}", fail_label)); // reject constructor dispatch when arity differs + } } /// Emits x86_64 arity validation for one constructor body. @@ -682,9 +741,19 @@ fn emit_x86_64_validate_constructor_arg_count( emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the eval argument array for arity validation let array_len_symbol = module.target.extern_symbol("__elephc_eval_value_array_len"); abi::emit_call_label(emitter, &array_len_symbol); + emitter.instruction("mov QWORD PTR [rbp - 8], rax"); // save the supplied constructor argument count abi::emit_load_int_immediate(emitter, "r10", slot.params.len() as i64); emitter.instruction("cmp rax, r10"); // compare supplied eval argument count with the constructor signature - emitter.instruction(&format!("jne {}", fail_label)); // reject constructor dispatch when arity differs + if slot.zero_default_first_arg { + let ok_label = format!("{}_argc_ok", constructor_body_label(module, slot)); + emitter.instruction(&format!("je {}", ok_label)); // explicit argument count matches the constructor signature + emitter.instruction("test rax, rax"); // did eval omit the optional builtin constructor argument? + emitter.instruction(&format!("je {}", ok_label)); // accept the builtin zero-default constructor call + emitter.instruction(&format!("jmp {}", fail_label)); // reject constructor dispatch when arity differs + emitter.label(&ok_label); + } else { + emitter.instruction(&format!("jne {}", fail_label)); // reject constructor dispatch when arity differs + } } /// Prepares ARM64 constructor ABI registers for the supported argument shapes. @@ -709,7 +778,21 @@ fn emit_aarch64_prepare_constructor_args( abi::emit_push_result_value(emitter, &receiver_ty); let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { - if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + if slot.zero_default_first_arg && index == 0 { + let default_label = format!("{}_arg_{}_default", body_label, index); + let done_label = format!("{}_arg_{}_done", body_label, index); + emitter.instruction("ldr x9, [sp, #32]"); // reload argc before selecting the optional constructor default + emitter.instruction(&format!("cbz x9, {}", default_label)); // omitted SplFixedArray size uses PHP's zero default + emit_aarch64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_aarch64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + emitter.instruction(&format!("b {}", done_label)); // skip default materialization after an explicit argument + emitter.label(&default_label); + abi::emit_load_int_immediate(emitter, "x0", 0); + abi::emit_push_result_value(emitter, &PhpType::Int); + emitter.label(&done_label); + } else if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { abi::emit_temporary_stack_address( emitter, abi::int_result_reg(emitter), @@ -758,7 +841,22 @@ fn emit_x86_64_prepare_constructor_args( abi::emit_push_result_value(emitter, &receiver_ty); let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { - if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { + if slot.zero_default_first_arg && index == 0 { + let default_label = format!("{}_arg_{}_default", body_label, index); + let done_label = format!("{}_arg_{}_done", body_label, index); + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload argc before selecting the optional constructor default + emitter.instruction("test r10, r10"); // did eval pass an explicit constructor argument? + emitter.instruction(&format!("jz {}", default_label)); // omitted SplFixedArray size uses PHP's zero default + emit_x86_64_load_eval_arg(module, emitter, index); + let label_prefix = format!("{}_arg_{}", body_label, index); + emit_x86_64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); + abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); + emitter.instruction(&format!("jmp {}", done_label)); // skip default materialization after an explicit argument + emitter.label(&default_label); + abi::emit_load_int_immediate(emitter, "rax", 0); + abi::emit_push_result_value(emitter, &PhpType::Int); + emitter.label(&done_label); + } else if let Some(ref_slot) = ref_slots.iter().find(|ref_slot| ref_slot.param_index == index) { abi::emit_temporary_stack_address( emitter, abi::int_result_reg(emitter), diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index a96c1e81d3..7f40e1d9be 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -18,6 +18,7 @@ use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::emit_box_current_value_as_mixed; use crate::codegen::platform::Arch; +use crate::intrinsics::IntrinsicCall; use crate::ir::{Function, LocalKind, Module}; use crate::names::{method_symbol, static_method_symbol}; use crate::parser::ast::Visibility; @@ -42,6 +43,7 @@ struct EvalMethodSlot { ref_params: Vec, return_ty: PhpType, is_hidden_shadow: bool, + runtime_helper: Option<&'static str>, } /// Static method metadata needed by eval static method-call bridge dispatch. @@ -194,7 +196,10 @@ fn collect_class_method_slots( .get(method) .map(String::as_str) .unwrap_or(class_name); - if !emitted_methods.contains(&(impl_class.to_string(), method.clone(), false)) { + let runtime_helper = eval_runtime_backed_instance_method_helper(class_name, method); + if runtime_helper.is_none() + && !emitted_methods.contains(&(impl_class.to_string(), method.clone(), false)) + { continue; } slots.push(EvalMethodSlot { @@ -208,6 +213,7 @@ fn collect_class_method_slots( ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), return_ty: sig.return_type.codegen_repr(), is_hidden_shadow: false, + runtime_helper, }); } } @@ -253,11 +259,26 @@ fn collect_hidden_private_ancestor_method_slots( ref_params: eval_normalized_ref_params(sig.params.len(), &sig.ref_params), return_ty: sig.return_type.codegen_repr(), is_hidden_shadow: true, + runtime_helper: None, }); } } } +/// Returns a normal-ABI runtime helper for builtin instance methods that eval can bridge. +fn eval_runtime_backed_instance_method_helper( + class_name: &str, + method_name: &str, +) -> Option<&'static str> { + if !matches!( + class_name.trim_start_matches('\\'), + "SplDoublyLinkedList" | "SplStack" | "SplQueue" | "SplFixedArray" + ) { + return None; + } + IntrinsicCall::instance_method(class_name, method_name)?.runtime_helper() +} + /// Adds bridge-supported static methods for one class. fn collect_class_static_method_slots( module: &Module, @@ -1078,7 +1099,11 @@ fn emit_aarch64_method_bodies( let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); - abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); + let callee = slot + .runtime_helper + .map(str::to_string) + .unwrap_or_else(|| method_symbol(&slot.impl_class, &slot.method)); + abi::emit_call_label(emitter, &callee); abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); @@ -1108,7 +1133,11 @@ fn emit_x86_64_method_bodies( let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); - abi::emit_call_label(emitter, &method_symbol(&slot.impl_class, &slot.method)); + let callee = slot + .runtime_helper + .map(str::to_string) + .unwrap_or_else(|| method_symbol(&slot.impl_class, &slot.method)); + abi::emit_call_label(emitter, &callee); abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); diff --git a/src/codegen_support/runtime/objects/new_by_name.rs b/src/codegen_support/runtime/objects/new_by_name.rs index 3f0dbcb38d..f1e688d283 100644 --- a/src/codegen_support/runtime/objects/new_by_name.rs +++ b/src/codegen_support/runtime/objects/new_by_name.rs @@ -90,8 +90,10 @@ pub fn emit_new_by_name(emitter: &mut Emitter) { emitter.instruction("ldr x13, [x10, #24]"); // obj_size emitter.instruction("str x12, [sp, #32]"); // save class_id across the heap call emitter.instruction("str x13, [sp, #40]"); // save obj_size across the heap call + emit_runtime_managed_match_aarch64(emitter); // -- allocate the object payload -- + emitter.label("__rt_nbn_generic_alloc"); emitter.instruction("mov x0, x13"); // allocation size emitter.instruction("bl __rt_heap_alloc"); // x0 = object pointer emitter.instruction("mov x9, #4"); // heap kind 4 = object instance @@ -119,6 +121,7 @@ pub fn emit_new_by_name(emitter: &mut Emitter) { emitter.instruction("blr x10"); // _class_propinit_(this = object in x0) emitter.instruction("ldr x0, [sp, #40]"); // restore the object pointer (the thunk may clobber x0) emitter.label("__rt_nbn_no_propinit"); + emitter.label("__rt_nbn_return_allocated"); emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address emitter.instruction("add sp, sp, #64"); // release the frame emitter.instruction("ret"); // return the object pointer @@ -184,8 +187,10 @@ fn emit_new_by_name_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("mov rdx, QWORD PTR [r10 + 24]"); // obj_size emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // stash class_id emitter.instruction("mov QWORD PTR [rbp - 40], rdx"); // stash obj_size + emit_runtime_managed_match_x86_64(emitter); // -- allocate the object payload -- + emitter.label("__rt_nbn_generic_alloc_x86"); emitter.instruction("mov rax, rdx"); // allocation size emitter.instruction("call __rt_heap_alloc"); // rax = object pointer emitter.instruction(&format!( @@ -218,6 +223,7 @@ fn emit_new_by_name_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("call r10"); // _class_propinit_(this) emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // restore the object pointer (the thunk may clobber rax) emitter.label("__rt_nbn_no_propinit_x86"); + emitter.label("__rt_nbn_return_allocated_x86"); emitter.instruction("add rsp, 48"); // release the frame emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return the object pointer @@ -228,3 +234,60 @@ fn emit_new_by_name_linux_x86_64(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the caller frame pointer emitter.instruction("ret"); // return null } + +/// Emits ARM64 class-id checks for runtime-managed builtin payload layouts. +fn emit_runtime_managed_match_aarch64(emitter: &mut Emitter) { + emitter.instruction("ldr x12, [sp, #32]"); // reload matched class id for runtime-managed allocation checks + abi::emit_symbol_address(emitter, "x10", "_spl_dll_class_id"); + emitter.instruction("ldr x10, [x10]"); // load SplDoublyLinkedList class id + emitter.instruction("cmp x12, x10"); // is the requested class SplDoublyLinkedList? + emitter.instruction("b.eq __rt_nbn_alloc_spl_dll"); // allocate the SPL list payload for SplDoublyLinkedList + abi::emit_symbol_address(emitter, "x10", "_spl_stack_class_id"); + emitter.instruction("ldr x10, [x10]"); // load SplStack class id + emitter.instruction("cmp x12, x10"); // is the requested class SplStack? + emitter.instruction("b.eq __rt_nbn_alloc_spl_dll"); // allocate the shared SPL list payload for SplStack + abi::emit_symbol_address(emitter, "x10", "_spl_queue_class_id"); + emitter.instruction("ldr x10, [x10]"); // load SplQueue class id + emitter.instruction("cmp x12, x10"); // is the requested class SplQueue? + emitter.instruction("b.eq __rt_nbn_alloc_spl_dll"); // allocate the shared SPL list payload for SplQueue + abi::emit_symbol_address(emitter, "x10", "_spl_fixed_array_class_id"); + emitter.instruction("ldr x10, [x10]"); // load SplFixedArray class id + emitter.instruction("cmp x12, x10"); // is the requested class SplFixedArray? + emitter.instruction("b.eq __rt_nbn_alloc_spl_fixed"); // allocate the SPL fixed-array payload with size zero + emitter.instruction("b __rt_nbn_generic_alloc"); // continue with the generic property-object allocator + emitter.label("__rt_nbn_alloc_spl_dll"); + emitter.instruction("mov x0, x12"); // pass the matched concrete SPL list class id + emitter.instruction("bl __rt_spl_dll_new"); // allocate the runtime-managed SPL list object layout + emitter.instruction("b __rt_nbn_return_allocated"); // return the initialized runtime-managed object + emitter.label("__rt_nbn_alloc_spl_fixed"); + emitter.instruction("mov x0, x12"); // pass the matched SplFixedArray class id + emitter.instruction("mov x1, xzr"); // default dynamic size is zero before constructor dispatch + emitter.instruction("bl __rt_spl_fixed_new"); // allocate the runtime-managed fixed-array object layout + emitter.instruction("b __rt_nbn_return_allocated"); // return the initialized runtime-managed object +} + +/// Emits x86_64 class-id checks for runtime-managed builtin payload layouts. +fn emit_runtime_managed_match_x86_64(emitter: &mut Emitter) { + abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_dll_class_id", 0); + emitter.instruction("cmp rcx, r10"); // is the requested class SplDoublyLinkedList? + emitter.instruction("je __rt_nbn_alloc_spl_dll_x86"); // allocate the SPL list payload for SplDoublyLinkedList + abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_stack_class_id", 0); + emitter.instruction("cmp rcx, r10"); // is the requested class SplStack? + emitter.instruction("je __rt_nbn_alloc_spl_dll_x86"); // allocate the shared SPL list payload for SplStack + abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_queue_class_id", 0); + emitter.instruction("cmp rcx, r10"); // is the requested class SplQueue? + emitter.instruction("je __rt_nbn_alloc_spl_dll_x86"); // allocate the shared SPL list payload for SplQueue + abi::emit_load_symbol_to_reg(emitter, "r10", "_spl_fixed_array_class_id", 0); + emitter.instruction("cmp rcx, r10"); // is the requested class SplFixedArray? + emitter.instruction("je __rt_nbn_alloc_spl_fixed_x86"); // allocate the SPL fixed-array payload with size zero + emitter.instruction("jmp __rt_nbn_generic_alloc_x86"); // continue with the generic property-object allocator + emitter.label("__rt_nbn_alloc_spl_dll_x86"); + emitter.instruction("mov rdi, rcx"); // pass the matched concrete SPL list class id + emitter.instruction("call __rt_spl_dll_new"); // allocate the runtime-managed SPL list object layout + emitter.instruction("jmp __rt_nbn_return_allocated_x86"); // return the initialized runtime-managed object + emitter.label("__rt_nbn_alloc_spl_fixed_x86"); + emitter.instruction("mov rdi, rcx"); // pass the matched SplFixedArray class id + emitter.instruction("xor esi, esi"); // default dynamic size is zero before constructor dispatch + emitter.instruction("call __rt_spl_fixed_new"); // allocate the runtime-managed fixed-array object layout + emitter.instruction("jmp __rt_nbn_return_allocated_x86"); // return the initialized runtime-managed object +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c2dcd116eb..de68670a32 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1923,6 +1923,40 @@ echo function_exists("spl_classes"); echo is_callable("spl_classes");'); ); } +/// Verifies eval fragments can construct and dispatch SPL container objects. +#[test] +fn test_eval_constructs_and_dispatches_spl_container_objects() { + let out = compile_and_run_capture( + r#"push("a"); +$list->push("b"); +echo count($list) . ":" . $list->bottom() . ":" . $list->top() . ":"; +foreach ($list as $value) { + echo $value; +} +$stack = new SplStack(); +$stack->push("s"); +echo ":" . count($stack) . $stack->pop(); +$queue = new SplQueue(); +$queue->enqueue("q"); +echo ":" . count($queue) . $queue->dequeue(); +$emptyFixed = new SplFixedArray(); +echo ":" . $emptyFixed->getSize(); +$fixed = new SplFixedArray(2); +$fixed->offsetSet(0, "x"); +echo ":" . $fixed->getSize() . $fixed->offsetGet(0); +'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "2:a:b:ab:1s:1q:0:2x"); +} + /// Verifies eval `array_fill()` and `array_fill_keys()` create arrays with PHP key rules. #[test] fn test_eval_dispatches_array_fill_builtin_calls() { From 01c653e688b5ec73e48d0a7436eb59abb87d6915 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 16:24:57 +0200 Subject: [PATCH 0771/1208] Support escaped eval object destructors --- crates/elephc-magician/src/context.rs | 20 +++ crates/elephc-magician/src/ffi/context.rs | 7 + .../src/ffi/dynamic_destructors.rs | 145 ++++++++++++++++++ crates/elephc-magician/src/ffi/mod.rs | 1 + crates/elephc-magician/src/interpreter/mod.rs | 2 + .../src/interpreter/statements.rs | 18 ++- .../src/runtime_hooks/externs.rs | 6 + .../elephc-magician/src/runtime_hooks/mod.rs | 20 ++- src/codegen_support/runtime/data/fixed.rs | 1 + src/codegen_support/runtime/eval_bridge.rs | 50 ++++++ .../runtime/objects/call_destructor.rs | 52 ++++++- src/ir_lower/context.rs | 1 + tests/codegen/eval.rs | 19 +++ 13 files changed, 333 insertions(+), 9 deletions(-) create mode 100644 crates/elephc-magician/src/ffi/dynamic_destructors.rs diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index db2dc77404..60467b0edd 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -966,12 +966,32 @@ impl ElephcEvalContext { .unwrap_or_else(|| class_name.to_string()); self.dynamic_objects .insert(identity, normalize_class_name(&class_name)); + crate::ffi::dynamic_destructors::register_dynamic_object_context(identity, self as *mut Self); self.dynamic_destructing_objects.remove(&identity); self.dynamic_destructed_objects.remove(&identity); self.dynamic_initialized_properties .retain(|(object, _)| *object != identity); } + /// Removes one dynamic object identity and all per-object eval metadata. + pub fn forget_dynamic_object(&mut self, identity: u64) { + self.dynamic_objects.remove(&identity); + self.dynamic_destructing_objects.remove(&identity); + self.dynamic_destructed_objects.remove(&identity); + self.dynamic_property_aliases + .retain(|(object, _), _| *object != identity); + self.dynamic_initialized_properties + .retain(|(object, _)| *object != identity); + crate::ffi::dynamic_destructors::unregister_dynamic_object(identity); + } + + /// Removes this context from the process-local dynamic object destructor registry. + pub fn unregister_dynamic_object_context(&self) { + crate::ffi::dynamic_destructors::unregister_dynamic_objects_for_context( + self as *const Self as *mut Self, + ); + } + /// Returns the dynamic eval class metadata associated with one object identity. pub fn dynamic_object_class(&self, identity: u64) -> Option<&EvalClass> { self.dynamic_objects diff --git a/crates/elephc-magician/src/ffi/context.rs b/crates/elephc-magician/src/ffi/context.rs index 8d4523a89f..b82714d95e 100644 --- a/crates/elephc-magician/src/ffi/context.rs +++ b/crates/elephc-magician/src/ffi/context.rs @@ -13,6 +13,8 @@ use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ElephcEvalScope, ABI_VERSION}; use crate::errors::EvalStatus; +#[cfg(not(test))] +use crate::ffi::dynamic_destructors::install_dynamic_object_destructor_hook; /// Returns the ABI version expected by generated elephc eval call sites. #[no_mangle] @@ -23,6 +25,8 @@ pub extern "C" fn __elephc_eval_abi_version() -> u32 { /// Allocates a process-level eval context handle for generated code. #[no_mangle] pub extern "C" fn __elephc_eval_context_new() -> *mut ElephcEvalContext { + #[cfg(not(test))] + install_dynamic_object_destructor_hook(); Box::into_raw(Box::new(ElephcEvalContext::new())) } @@ -34,6 +38,9 @@ pub extern "C" fn __elephc_eval_context_new() -> *mut ElephcEvalContext { #[no_mangle] pub unsafe extern "C" fn __elephc_eval_context_free(ctx: *mut ElephcEvalContext) { if !ctx.is_null() { + if let Some(context) = unsafe { ctx.as_ref() } { + context.unregister_dynamic_object_context(); + } drop(Box::from_raw(ctx)); } } diff --git a/crates/elephc-magician/src/ffi/dynamic_destructors.rs b/crates/elephc-magician/src/ffi/dynamic_destructors.rs new file mode 100644 index 0000000000..300bf2a125 --- /dev/null +++ b/crates/elephc-magician/src/ffi/dynamic_destructors.rs @@ -0,0 +1,145 @@ +//! Purpose: +//! Owns the process-local registry that lets native object release call back +//! into eval-declared `__destruct()` methods for dynamic classes. +//! +//! Called from: +//! - `crate::context::ElephcEvalContext` when dynamic objects are registered or freed. +//! - The generated runtime through the installed destructor hook function pointer. +//! +//! Key details: +//! - The runtime owns object storage and calls this hook while the object is still +//! intact but already in the final-release path. +//! - Registry values are stored as integer addresses so the global mutex remains +//! `Sync`; every use revalidates null pointers and ABI version. + +#[cfg(not(test))] +use crate::abi::ABI_VERSION; +use crate::abi::ElephcEvalContext; +#[cfg(not(test))] +use crate::errors::EvalStatus; +#[cfg(not(test))] +use crate::interpreter::eval_dynamic_destructor_for_object_cell; +#[cfg(not(test))] +use crate::interpreter::RuntimeValueOps; +#[cfg(not(test))] +use crate::runtime_hooks::{self, ElephcRuntimeOps}; +#[cfg(not(test))] +use crate::value::RuntimeCell; +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; + +static DYNAMIC_DESTRUCTOR_CONTEXTS: OnceLock>> = OnceLock::new(); + +/// Returns the process-local dynamic object to eval context registry. +fn dynamic_destructor_contexts() -> &'static Mutex> { + DYNAMIC_DESTRUCTOR_CONTEXTS.get_or_init(|| Mutex::new(HashMap::new())) +} + +/// Installs the eval dynamic object destructor callback into the generated runtime. +#[cfg(not(test))] +pub(crate) fn install_dynamic_object_destructor_hook() { + unsafe { + runtime_hooks::install_dynamic_object_destructor_hook( + __elephc_eval_dynamic_object_destruct as *const () as usize, + ); + } +} + +/// Records which eval context owns one dynamic object's eval class metadata. +pub(crate) fn register_dynamic_object_context(identity: u64, context: *mut ElephcEvalContext) { + if identity == 0 || context.is_null() { + return; + } + if let Ok(mut contexts) = dynamic_destructor_contexts().lock() { + contexts.insert(identity, context as usize); + } +} + +/// Removes one dynamic object from the process-local destructor registry. +pub(crate) fn unregister_dynamic_object(identity: u64) { + if identity == 0 { + return; + } + if let Ok(mut contexts) = dynamic_destructor_contexts().lock() { + contexts.remove(&identity); + } +} + +/// Removes every dynamic object currently associated with a soon-to-be-freed context. +pub(crate) fn unregister_dynamic_objects_for_context(context: *mut ElephcEvalContext) { + if context.is_null() { + return; + } + let context = context as usize; + if let Ok(mut contexts) = dynamic_destructor_contexts().lock() { + contexts.retain(|_, owner| *owner != context); + } +} + +/// Looks up the eval context that owns one dynamic object identity. +#[cfg(not(test))] +fn dynamic_destructor_context(identity: u64) -> Option<*mut ElephcEvalContext> { + let contexts = dynamic_destructor_contexts().lock().ok()?; + let context = *contexts.get(&identity)?; + Some(context as *mut ElephcEvalContext) +} + +/// Runs an eval dynamic object destructor from the native object free path. +/// +/// # Safety +/// `object` must be null or a live elephc runtime object pointer. The runtime +/// calls this only while its object destruction guard bit is set, so boxing the +/// borrowed object for `$this` cannot recursively free the same storage. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_dynamic_object_destruct( + object: *mut RuntimeCell, +) -> u64 { + std::panic::catch_unwind(|| unsafe { dynamic_object_destruct_inner(object) }).unwrap_or(0) +} + +/// Executes the callback body after the exported ABI shim has installed a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_dynamic_object_destruct`; callers must pass a live raw +/// object pointer whose refcount guard already marks destruction as active. +#[cfg(not(test))] +unsafe fn dynamic_object_destruct_inner(object: *mut RuntimeCell) -> u64 { + if object.is_null() { + return 0; + } + let identity = object as u64; + let Some(context) = dynamic_destructor_context(identity) else { + return 0; + }; + let Some(context) = (unsafe { context.as_mut() }) else { + unregister_dynamic_object(identity); + return 0; + }; + if context.abi_version() != ABI_VERSION { + unregister_dynamic_object(identity); + return 0; + } + if context.dynamic_object_class(identity).is_none() { + unregister_dynamic_object(identity); + return 0; + } + + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + let object_cell = match ElephcRuntimeOps::object_from_raw(object) { + Ok(object_cell) => object_cell, + Err(_) => { + context.forget_dynamic_object(identity); + return 1; + } + }; + let destruct_result = + eval_dynamic_destructor_for_object_cell(identity, object_cell, context, &mut values); + let release_result = values.release(object_cell); + context.forget_dynamic_object(identity); + match (destruct_result, release_result) { + (Ok(true), Ok(())) | (Ok(false), Ok(())) => 1, + (Err(EvalStatus::UnsupportedConstruct), _) => 1, + (Err(_), _) | (_, Err(_)) => 1, + } +} diff --git a/crates/elephc-magician/src/ffi/mod.rs b/crates/elephc-magician/src/ffi/mod.rs index 4c87feb3f5..d9f73bcc1c 100644 --- a/crates/elephc-magician/src/ffi/mod.rs +++ b/crates/elephc-magician/src/ffi/mod.rs @@ -10,6 +10,7 @@ //! - Helper routines stay private to the FFI layer unless shared across families. pub mod context; +pub(crate) mod dynamic_destructors; pub mod execute; #[cfg(not(test))] pub mod function_calls; diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index e3d5ad1011..1c53e8cbf9 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -71,6 +71,8 @@ use return_values::*; pub use runtime_ops::RuntimeValueOps; use runtime_ops::*; use scope_cells::*; +#[cfg(not(test))] +pub(crate) use statements::eval_dynamic_destructor_for_object_cell; use statements::*; use throwables::*; use std::ffi::{CStr, CString}; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 88945c5705..7d3f169003 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -677,17 +677,27 @@ fn eval_dynamic_destructor_for_release( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { + eval_dynamic_destructor_for_object_cell(identity, object, context, values).map(|_| ()) +} + +/// Calls a dynamic eval `__destruct()` hook for an already-boxed object cell. +pub(crate) fn eval_dynamic_destructor_for_object_cell( + identity: u64, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { let Some(class_name) = context .dynamic_object_class(identity) .map(|class| class.name().to_string()) else { - return Ok(()); + return Ok(false); }; let Some((declaring_class, method)) = context.class_method(&class_name, "__destruct") else { - return Ok(()); + return Ok(true); }; if !context.begin_dynamic_object_destructor(identity) { - return Ok(()); + return Ok(true); } let result = eval_dynamic_method_with_values( &declaring_class, @@ -703,7 +713,7 @@ fn eval_dynamic_destructor_for_release( Err(status) => Err(status), }; context.finish_dynamic_object_destructor(identity); - release_result + release_result.map(|_| true) } /// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index cc665f889e..5d22ff0b34 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -99,6 +99,10 @@ unsafe extern "C" { pub(super) fn __elephc_eval_value_object_clone_shallow( object: *mut RuntimeCell, ) -> *mut RuntimeCell; + /// Returns a boxed Mixed object cell for a borrowed raw object payload. + pub(super) fn __elephc_eval_value_object_from_raw( + object: *mut RuntimeCell, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_object_property_len(object: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_value_object_property_iter_key( object: *mut RuntimeCell, @@ -325,4 +329,6 @@ unsafe extern "C" { pub(super) fn __elephc_eval_value_final_object_identity(value: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_value_release(value: *mut RuntimeCell); pub(super) fn __elephc_eval_value_retain(value: *mut RuntimeCell) -> *mut RuntimeCell; + /// Installs the optional eval dynamic object destructor callback. + pub(super) fn __elephc_eval_install_dynamic_object_destructor_hook(callback: usize); } diff --git a/crates/elephc-magician/src/runtime_hooks/mod.rs b/crates/elephc-magician/src/runtime_hooks/mod.rs index c0c22487fc..f6b2447326 100644 --- a/crates/elephc-magician/src/runtime_hooks/mod.rs +++ b/crates/elephc-magician/src/runtime_hooks/mod.rs @@ -26,7 +26,8 @@ use crate::abi::ElephcEvalContext; use crate::value::{RuntimeCell, RuntimeCellHandle}; #[cfg(not(test))] use externs::{ - __elephc_eval_value_array_new, __elephc_eval_value_array_set, __elephc_eval_value_int, + __elephc_eval_install_dynamic_object_destructor_hook, __elephc_eval_value_array_new, + __elephc_eval_value_array_set, __elephc_eval_value_int, __elephc_eval_value_object_from_raw, }; /// Runtime hook adapter that produces and consumes boxed elephc Mixed cells. @@ -50,7 +51,7 @@ impl ElephcRuntimeOps { } /// Converts a runtime wrapper result into an interpreter handle. - fn handle(ptr: *mut RuntimeCell) -> Result { + pub(crate) fn handle(ptr: *mut RuntimeCell) -> Result { if ptr.is_null() { Err(EvalStatus::RuntimeFatal) } else { @@ -58,6 +59,13 @@ impl ElephcRuntimeOps { } } + /// Boxes one borrowed raw object payload for eval `$this` dispatch. + pub(crate) fn object_from_raw( + object: *mut RuntimeCell, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_object_from_raw(object) }) + } + /// Packs source-order argument cells into the boxed eval array ABI. fn arg_array(args: Vec) -> Result { let arg_array = unsafe { __elephc_eval_value_array_new(args.len() as u64) }; @@ -82,3 +90,11 @@ impl ElephcRuntimeOps { (class_scope.as_ptr(), class_scope.len() as u64) } } + +/// Installs the eval dynamic object destructor callback into runtime data. +#[cfg(not(test))] +pub(crate) unsafe fn install_dynamic_object_destructor_hook(callback: usize) { + unsafe { + __elephc_eval_install_dynamic_object_destructor_hook(callback); + } +} diff --git a/src/codegen_support/runtime/data/fixed.rs b/src/codegen_support/runtime/data/fixed.rs index a6736b9a29..281a4efc0f 100644 --- a/src/codegen_support/runtime/data/fixed.rs +++ b/src/codegen_support/runtime/data/fixed.rs @@ -99,6 +99,7 @@ pub(crate) fn emit_runtime_data_fixed(heap_size: usize, target: Target) -> Strin out.push_str(".comm _fiber_main_saved_sp, 8, 3\n"); out.push_str(".comm _fiber_main_saved_exc, 8, 3\n"); out.push_str(".comm _fiber_main_saved_call_frame, 8, 3\n"); + out.push_str(".comm _elephc_eval_dynamic_object_destruct_fn, 8, 3\n"); out.push_str(".comm _rt_diag_suppression, 8, 3\n"); // elephc_web_capture: per-request output-capture mode flag read by // __rt_stdout_write. Zero (the default) routes echo output to the plain diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 4e74db396e..dea4b3ec0a 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -106,6 +106,8 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #32"); // release the dynamic-object wrapper frame emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + emit_aarch64_object_from_raw_wrapper(emitter); + emit_aarch64_install_dynamic_object_destructor_hook(emitter); emit_aarch64_object_clone_shallow_wrapper(emitter); label_c_global(emitter, "__elephc_eval_class_exists"); @@ -1565,6 +1567,8 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the boxed object or null Mixed cell to Rust + emit_x86_64_object_from_raw_wrapper(emitter); + emit_x86_64_install_dynamic_object_destructor_hook(emitter); emit_x86_64_object_clone_shallow_wrapper(emitter); label_c_global(emitter, "__elephc_eval_class_exists"); @@ -3052,6 +3056,52 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("jmp __rt_decref_mixed"); // release one eval-owned boxed Mixed cell } +/// Emits the ARM64 wrapper that boxes a borrowed raw object pointer for Rust eval. +fn emit_aarch64_object_from_raw_wrapper(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_object_from_raw"); + emitter.instruction("cbz x0, __elephc_eval_value_object_from_raw_null"); // null raw object pointers become PHP null cells + emitter.instruction("mov x1, x0"); // move the raw object pointer into the Mixed payload + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("mov x2, xzr"); // object payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // box and retain the borrowed object for eval + emitter.label("__elephc_eval_value_object_from_raw_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 = null + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("b __rt_mixed_from_value"); // box the null payload and return to Rust +} + +/// Emits the ARM64 wrapper that installs the dynamic object destructor callback. +fn emit_aarch64_install_dynamic_object_destructor_hook(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_install_dynamic_object_destructor_hook"); + abi::emit_symbol_address(emitter, "x9", "_elephc_eval_dynamic_object_destruct_fn"); + emitter.instruction("str x0, [x9]"); // store the Rust callback pointer for object destruction + emitter.instruction("ret"); // return after installing the optional eval hook +} + +/// Emits the x86_64 wrapper that boxes a borrowed raw object pointer for Rust eval. +fn emit_x86_64_object_from_raw_wrapper(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_object_from_raw"); + emitter.instruction("test rdi, rdi"); // null raw object pointers become PHP null cells + emitter.instruction("jz __elephc_eval_value_object_from_raw_null_x86"); // branch to null boxing for missing object payloads + emitter.instruction("mov eax, 6"); // runtime tag 6 = object + emitter.instruction("xor esi, esi"); // object payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // box and retain the borrowed object for eval + emitter.label("__elephc_eval_value_object_from_raw_null_x86"); + emitter.instruction("mov eax, 8"); // runtime tag 8 = null + emitter.instruction("xor edi, edi"); // null has no low payload word + emitter.instruction("xor esi, esi"); // null has no high payload word + emitter.instruction("jmp __rt_mixed_from_value"); // box the null payload and return to Rust +} + +/// Emits the x86_64 wrapper that installs the dynamic object destructor callback. +fn emit_x86_64_install_dynamic_object_destructor_hook(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_install_dynamic_object_destructor_hook"); + abi::emit_symbol_address(emitter, "r10", "_elephc_eval_dynamic_object_destruct_fn"); + emitter.instruction("mov QWORD PTR [r10], rdi"); // store the Rust callback pointer for object destruction + emitter.instruction("ret"); // return after installing the optional eval hook +} + /// Emits the ARM64 eval hook that returns AOT ReflectionMethod names. fn emit_aarch64_eval_reflection_method_names(emitter: &mut Emitter) { emit_aarch64_eval_reflection_member_names( diff --git a/src/codegen_support/runtime/objects/call_destructor.rs b/src/codegen_support/runtime/objects/call_destructor.rs index bf3823e9cb..cd2049399a 100644 --- a/src/codegen_support/runtime/objects/call_destructor.rs +++ b/src/codegen_support/runtime/objects/call_destructor.rs @@ -14,6 +14,9 @@ //! - `$this` is passed in the first integer argument register and is borrowed by //! the callee (no incref/decref around the call), matching normal method ABI, so //! the call cannot double-free the receiver. +//! - An optional eval callback can claim runtime-generic objects that actually +//! belong to eval-declared classes; when no callback is installed, the helper +//! follows the original static destructor table path. //! - Re-entrancy guard: before calling the destructor, bit 31 of the 32-bit //! refcount is set. A balanced `$tmp = $this;`/scope-exit inside the body then //! decrements from `0x8000_0001` back to `0x8000_0000` instead of reaching zero, @@ -44,6 +47,29 @@ fn emit_call_object_destructor_aarch64(emitter: &mut Emitter) { emitter.label_global("__rt_call_object_destructor"); emitter.instruction("cbz x0, __rt_call_object_destructor_ret"); // null receiver → nothing to destruct + emitter.instruction("ldr w9, [x0, #-12]"); // w9 = object refcount (header offset -12) + emitter.instruction("tbnz w9, #31, __rt_call_object_destructor_ret"); // destruction already in progress → never run twice + abi::emit_symbol_address(emitter, "x10", "_elephc_eval_dynamic_object_destruct_fn"); + emitter.instruction("ldr x10, [x10]"); // x10 = optional eval dynamic destructor callback + emitter.instruction("cbz x10, __rt_call_object_destructor_static"); // no eval callback installed → use static class table + emitter.instruction("movz w12, #0x8000, lsl #16"); // w12 = 0x80000000, the destruction-in-progress flag bit + emitter.instruction("orr w9, w9, w12"); // mark destruction in progress before boxing borrowed $this + emitter.instruction("str w9, [x0, #-12]"); // persist the guard flag in the refcount field + emitter.instruction("sub sp, sp, #32"); // allocate an aligned frame for the eval callback + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address before the Rust call + emitter.instruction("add x29, sp, #16"); // establish the helper frame + emitter.instruction("str x0, [sp, #0]"); // save the object pointer across the callback + emitter.instruction("blr x10"); // ask eval whether it owns and destructed this object + emitter.instruction("mov x12, x0"); // preserve the eval callback handled flag + emitter.instruction("ldr x0, [sp, #0]"); // restore the object pointer after the callback + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the eval callback frame + emitter.instruction("cbnz x12, __rt_call_object_destructor_ret"); // eval handled the dynamic object → skip static lookup + emitter.instruction("ldr w9, [x0, #-12]"); // reload the refcount after an eval miss + emitter.instruction("movz w12, #0x8000, lsl #16"); // w12 = destruction-in-progress flag bit + emitter.instruction("bic w9, w9, w12"); // clear the temporary eval guard before static lookup + emitter.instruction("str w9, [x0, #-12]"); // persist the restored refcount guard state + emitter.label("__rt_call_object_destructor_static"); emitter.instruction("ldr x11, [x0]"); // x11 = runtime class_id (object payload offset 0) // emit_load_symbol_to_reg uses x9 as scratch, so class_id is kept in x11. crate::codegen_support::abi::emit_load_symbol_to_reg(emitter, "x10", "_class_destruct_count", 0); @@ -53,7 +79,6 @@ fn emit_call_object_destructor_aarch64(emitter: &mut Emitter) { emitter.instruction("ldr x10, [x10, x11, lsl #3]"); // x10 = destructor symbol for this class (or 0) emitter.instruction("cbz x10, __rt_call_object_destructor_ret"); // class defines no __destruct → done emitter.instruction("ldr w9, [x0, #-12]"); // w9 = object refcount (header offset -12) - emitter.instruction("tbnz w9, #31, __rt_call_object_destructor_ret"); // destruction already in progress → never run twice emitter.instruction("movz w12, #0x8000, lsl #16"); // w12 = 0x80000000, the destruction-in-progress flag bit emitter.instruction("orr w9, w9, w12"); // mark destruction in progress so a balanced self-ref cannot re-enter the free path emitter.instruction("str w9, [x0, #-12]"); // persist the guard flag in the refcount field @@ -74,6 +99,29 @@ fn emit_call_object_destructor_x86_64(emitter: &mut Emitter) { emitter.instruction("test rdi, rdi"); // null receiver → nothing to destruct emitter.instruction("jz __rt_call_object_destructor_ret"); // skip the lookup for a null object + emitter.instruction("mov eax, DWORD PTR [rdi - 12]"); // eax = object refcount (header offset -12) + emitter.instruction("test eax, 0x80000000"); // is destruction already in progress? + emitter.instruction("jnz __rt_call_object_destructor_ret"); // never run a destructor twice + abi::emit_symbol_address(emitter, "r10", "_elephc_eval_dynamic_object_destruct_fn"); + emitter.instruction("mov r10, QWORD PTR [r10]"); // r10 = optional eval dynamic destructor callback + emitter.instruction("test r10, r10"); // is the eval callback installed? + emitter.instruction("jz __rt_call_object_destructor_static_x86"); // no eval callback installed → use static class table + emitter.instruction("or eax, 0x80000000"); // mark destruction in progress before boxing borrowed $this + emitter.instruction("mov DWORD PTR [rdi - 12], eax"); // persist the guard flag in the refcount field + emitter.instruction("push rbp"); // align the stack and save the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish the eval callback frame + emitter.instruction("sub rsp, 16"); // reserve a spill slot for the object pointer + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the object pointer across the callback + emitter.instruction("call r10"); // ask eval whether it owns and destructed this object + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // restore the object pointer after the callback + emitter.instruction("add rsp, 16"); // release the eval callback spill slot + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("test rax, rax"); // did eval handle this dynamic object? + emitter.instruction("jnz __rt_call_object_destructor_ret"); // eval handled the dynamic object → skip static lookup + emitter.instruction("mov eax, DWORD PTR [rdi - 12]"); // reload the refcount after an eval miss + emitter.instruction("and eax, 0x7fffffff"); // clear the temporary eval guard before static lookup + emitter.instruction("mov DWORD PTR [rdi - 12], eax"); // persist the restored refcount guard state + emitter.label("__rt_call_object_destructor_static_x86"); emitter.instruction("mov rax, QWORD PTR [rdi]"); // rax = runtime class_id (object payload offset 0) abi::emit_cmp_reg_to_symbol(emitter, "rax", "_class_destruct_count"); // is class_id within the destructor table? emitter.instruction("jae __rt_call_object_destructor_ret"); // out-of-range class ids have no destructor @@ -82,8 +130,6 @@ fn emit_call_object_destructor_x86_64(emitter: &mut Emitter) { emitter.instruction("test r10, r10"); // class defines no __destruct? emitter.instruction("jz __rt_call_object_destructor_ret"); // nothing to call → done emitter.instruction("mov eax, DWORD PTR [rdi - 12]"); // eax = object refcount (header offset -12) - emitter.instruction("test eax, 0x80000000"); // is destruction already in progress? - emitter.instruction("jnz __rt_call_object_destructor_ret"); // never run a destructor twice emitter.instruction("or eax, 0x80000000"); // mark destruction in progress so a balanced self-ref cannot re-enter the free path emitter.instruction("mov DWORD PTR [rdi - 12], eax"); // persist the guard flag in the refcount field emitter.instruction("push rbp"); // align the stack and save the caller frame pointer diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 181234b51a..d3e0391f9d 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1622,6 +1622,7 @@ fn builtin_call_result_owns_storage_as_temporary(name: &str) -> bool { | "array_column" | "array_combine" | "array_diff" + | "eval" | "array_fill" | "array_fill_keys" | "array_intersect" diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index de68670a32..f48330e0fb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -15976,6 +15976,25 @@ echo "after";'); assert_eq!(out, "drop:A:drop:B:after"); } +/// Verifies eval-declared object destructors run when objects escape eval and native code releases them. +#[test] +fn test_eval_dynamic_object_runs_destructor_after_native_release() { + let out = compile_and_run( + r#"name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +return new EvalDestructEscapedBox("A");'); +echo "before:"; +unset($box); +echo "after"; +"#, + ); + assert_eq!(out, "before:drop:A:after"); +} + /// Verifies eval `clone` shallow-copies ordinary emitted AOT objects. #[test] fn test_eval_clone_aot_object_expression() { From 2aa2e97136174cdbfc412163ccc0eab2198208c5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 16:36:43 +0200 Subject: [PATCH 0772/1208] Preserve eval var_dump object class names --- .../builtins/registry/dispatch/core.rs | 2 +- .../src/interpreter/debug_output.rs | 50 +++++++++++++++---- .../src/interpreter/tests/expressions.rs | 22 ++++++++ docs/php/eval.md | 11 ++-- tests/codegen/eval.rs | 17 +++++++ 5 files changed, 87 insertions(+), 15 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs index eaa7308418..a7b68a7b4d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs @@ -30,7 +30,7 @@ pub(in crate::interpreter) fn eval_core_builtin_with_values( let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_var_dump_result(*value, values)? + eval_var_dump_result(*value, context, values)? } "call_user_func" => { return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) diff --git a/crates/elephc-magician/src/interpreter/debug_output.rs b/crates/elephc-magician/src/interpreter/debug_output.rs index ca7c1a5f46..410d70e483 100644 --- a/crates/elephc-magician/src/interpreter/debug_output.rs +++ b/crates/elephc-magician/src/interpreter/debug_output.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::eval_positional_expr_call()` for debug-output builtin dispatch. //! //! Key details: -//! - Output formatting walks runtime arrays and scalar values only through `RuntimeValueOps`. +//! - Output formatting walks runtime arrays, scalars, and AOT object names through `RuntimeValueOps`. +//! - Eval-declared object names come from the interpreter context's dynamic object registry. //! - The builtins either echo output directly or return captured string output according to PHP flags. use super::*; @@ -49,17 +50,18 @@ pub(super) fn eval_builtin_var_dump( return Err(EvalStatus::RuntimeFatal); }; let value = eval_expr(value, context, scope, values)?; - eval_var_dump_result(value, values) + eval_var_dump_result(value, context, values) } /// Emits one eval value using PHP-style `var_dump()` debug formatting. pub(in crate::interpreter) fn eval_var_dump_result( value: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let mut output = Vec::new(); let mut arrays_seen = Vec::new(); - eval_var_dump_append_value(value, values, 0, &mut arrays_seen, &mut output)?; + eval_var_dump_append_value(value, context, values, 0, &mut arrays_seen, &mut output)?; let output = values.string_bytes_value(&output)?; values.echo(output)?; values.null() @@ -68,6 +70,7 @@ pub(in crate::interpreter) fn eval_var_dump_result( /// Appends one value and its nested array entries to a `var_dump()` byte buffer. fn eval_var_dump_append_value( value: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, depth: usize, arrays_seen: &mut Vec, @@ -79,13 +82,9 @@ fn eval_var_dump_append_value( EVAL_TAG_FLOAT => eval_var_dump_append_scalar(b"float", value, values, depth, output), EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, output), EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { - eval_var_dump_append_array(value, values, depth, arrays_seen, output) - } - EVAL_TAG_OBJECT => { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"object(Object)\n"); - Ok(()) + eval_var_dump_append_array(value, context, values, depth, arrays_seen, output) } + EVAL_TAG_OBJECT => eval_var_dump_append_object(value, context, values, depth, output), EVAL_TAG_NULL => { eval_var_dump_append_indent(depth, output); output.extend_from_slice(b"NULL\n"); @@ -152,6 +151,7 @@ fn eval_var_dump_append_bool( /// Appends one array shell and recursively emits foreach-visible entries. fn eval_var_dump_append_array( value: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, depth: usize, arrays_seen: &mut Vec, @@ -174,7 +174,7 @@ fn eval_var_dump_append_array( let key = values.array_iter_key(value, position)?; let element = values.array_get(value, key)?; eval_var_dump_append_key(key, values, depth + 1, output)?; - eval_var_dump_append_value(element, values, depth + 1, arrays_seen, output)?; + eval_var_dump_append_value(element, context, values, depth + 1, arrays_seen, output)?; } eval_var_dump_append_indent(depth, output); output.extend_from_slice(b"}\n"); @@ -182,6 +182,36 @@ fn eval_var_dump_append_array( Ok(()) } +/// Appends one object line while preserving eval-declared and runtime class names. +fn eval_var_dump_append_object( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"object("); + if let Ok(identity) = values.object_identity(value) { + if let Some(class) = context.dynamic_object_class(identity) { + output.extend_from_slice(class.name().trim_start_matches('\\').as_bytes()); + output.extend_from_slice(b")\n"); + return Ok(()); + } + } + let class_name = values.object_class_name(value)?; + let bytes = values.string_bytes(class_name)?; + values.release(class_name)?; + output.extend_from_slice(trim_leading_namespace_separator(&bytes)); + output.extend_from_slice(b")\n"); + Ok(()) +} + +/// Removes a leading PHP namespace separator from a runtime class-name byte slice. +fn trim_leading_namespace_separator(bytes: &[u8]) -> &[u8] { + bytes.strip_prefix(b"\\").unwrap_or(bytes) +} + /// Appends one array key line for an indexed or associative `var_dump()` entry. fn eval_var_dump_append_key( key: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index 2e5b153ecf..2fa41fb2a0 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -199,6 +199,28 @@ return function_exists("var_dump");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies eval `var_dump()` keeps eval-declared and runtime object class names. +#[test] +fn execute_program_var_dump_prints_object_class_names() { + let program = parse_fragment( + br#"class EvalDumpBox {} +var_dump(new EvalDumpBox()); +var_dump(new KnownClass());"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "object(EvalDumpBox)\nobject(KnownClass)\n" + ); + assert_eq!(values.get(result), FakeValue::Null); +} + /// Verifies eval property reads and writes dispatch through runtime hooks. #[test] fn execute_program_reads_and_writes_object_property() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 331e9ce1d8..14c9c31c79 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -751,8 +751,9 @@ output path as `echo`, boolean false and null print nothing, and arrays print the same `Array\n` header shape as elephc's native `print_r()` subset. Eval `var_dump()` supports the one-argument form. Scalars print typed -diagnostic lines, and indexed or associative arrays print foreach-visible keys -and nested values through eval value hooks. +diagnostic lines, indexed or associative arrays print foreach-visible keys and +nested values through eval value hooks, and eval-declared or generated/AOT +objects print their PHP-visible class names. ## Current limitations @@ -775,8 +776,10 @@ those bridge paths. Eval class support is still smaller than the full static class system. Eval-declared `__destruct()` hooks run when an eval-owned dynamic object reaches final release through ordinary eval statement execution, such as `unset($obj)` or -a discarded temporary expression; destructor execution from runtime cycle -collection is still outside the bridge. The main remaining class-system gaps are +a discarded temporary expression, and through the native runtime final-release +path after an eval-owned object escapes back into compiled code; destructor +execution from runtime cycle collection is still outside the bridge. The main +remaining class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Object/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and Enum/attribute slice, Reflection type APIs beyond retained parameter, generated diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f48330e0fb..b2c55f43eb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -161,6 +161,23 @@ echo function_exists("var_dump");'); ); } +/// Verifies eval `var_dump()` prints eval-declared and generated object class names. +#[test] +fn test_eval_var_dump_prints_object_class_names() { + let out = compile_and_run( + r#" Date: Fri, 26 Jun 2026 16:42:48 +0200 Subject: [PATCH 0773/1208] Cover eval destructors during cycle collection --- docs/php/eval.md | 6 +++--- tests/codegen/eval.rs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 14c9c31c79..77a4ebf698 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -777,9 +777,9 @@ Eval class support is still smaller than the full static class system. Eval-declared `__destruct()` hooks run when an eval-owned dynamic object reaches final release through ordinary eval statement execution, such as `unset($obj)` or a discarded temporary expression, and through the native runtime final-release -path after an eval-owned object escapes back into compiled code; destructor -execution from runtime cycle collection is still outside the bridge. The main -remaining class-system gaps are +path after an eval-owned object escapes back into compiled code, including +runtime cycle collection of eval-owned object graphs. The main remaining +class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Object/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and Enum/attribute slice, Reflection type APIs beyond retained parameter, generated diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b2c55f43eb..eaa48885f8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -16012,6 +16012,24 @@ echo "after"; assert_eq!(out, "before:drop:A:after"); } +/// Verifies eval-declared object destructors run when cycle collection releases them. +#[test] +fn test_eval_dynamic_object_runs_destructor_after_cycle_collection() { + let out = compile_and_run( + r#"name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +$box = new EvalCycleDropBox("A"); +$box->self = $box; +unset($box); +echo "after";'); +"#, + ); + assert_eq!(out, "drop:A:after"); +} + /// Verifies eval `clone` shallow-copies ordinary emitted AOT objects. #[test] fn test_eval_clone_aot_object_expression() { From 25389eebd3604ae9322df1a0c80ffb8910ed3216 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 16:59:09 +0200 Subject: [PATCH 0774/1208] Reject uninitialized eval typed property reads --- .../src/interpreter/statements.rs | 28 ++++++++++ .../src/interpreter/tests/classes.rs | 54 +++++++++++++++++++ docs/php/eval.md | 4 ++ tests/codegen/eval.rs | 53 ++++++++++++++++++ 4 files changed, 139 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 7d3f169003..adedf3fac9 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3792,6 +3792,16 @@ pub(in crate::interpreter) fn eval_property_get_result( values, ); } + if property.property_type().is_some() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + return eval_throw_uninitialized_property_error( + &declaring_class, + property.name(), + context, + values, + ); + } } if !declared_property_found && eval_object_public_property_exists(object, property_name, values)? @@ -4651,6 +4661,24 @@ fn eval_throw_undeclared_static_property_error( ) } +/// Throws PHP's uninitialized typed instance property error. +fn eval_throw_uninitialized_property_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Typed property {}::${} must not be accessed before initialization", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + /// Throws PHP's uninitialized typed static property error. fn eval_throw_uninitialized_static_property_error( declaring_class: &str, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 077933a4bd..adb2520572 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -339,6 +339,60 @@ return $box->id();"#, assert_eq!(values.get(result), FakeValue::Int(7)); } +/// Verifies direct reads of uninitialized typed eval properties throw catchable PHP errors. +#[test] +fn execute_program_rejects_uninitialized_typed_property_reads() { + let program = parse_fragment( + br#"class EvalTypedReadBox { + public int $typed; + public ?int $nullable; + public ?int $defaultNull = null; + public $plain; +} +$box = new EvalTypedReadBox(); +try { + echo $box->typed; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + echo $box->nullable; +} catch (Error $e) { + echo $e->getMessage(); +} +echo "|"; +echo is_null($box->defaultNull) ? "default-null" : "bad"; +echo "|"; +echo is_null($box->plain) ? "plain-null" : "bad"; +echo "|"; +$box->typed = 0; +echo $box->typed; +echo "|"; +unset($box->typed); +try { + echo $box->typed; +} catch (Error $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Typed property EvalTypedReadBox::$typed must not be accessed before initialization|\ +Typed property EvalTypedReadBox::$nullable must not be accessed before initialization|\ +default-null|plain-null|0|\ +Typed property EvalTypedReadBox::$typed must not be accessed before initialization" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies readonly eval properties throw Error on writes outside the declaring constructor. #[test] fn execute_program_readonly_property_write_after_constructor_throws_error() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 77a4ebf698..7d64206e41 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -176,6 +176,10 @@ Eval validates inherited property redeclarations with PHP-style invariant property types, matching static storage, compatible read/write visibility, and readonly compatibility: child redeclarations may add `readonly`, but may not remove inherited `readonly`. +Typed eval-declared instance and static properties track PHP initialization +state: reads before initialization or after `unset()` throw the same catchable +`Error`, while untyped properties and explicit `= null` defaults are initialized +to `null`. Eval validates inherited class/interface constant redeclarations with PHP-style visibility compatibility, and rejects non-public interface constants. Eval-declared method calls also enforce declared return values at runtime, with diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eaa48885f8..a8c8c35534 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8584,6 +8584,59 @@ echo implode(",", array_keys($vars));'); assert_eq!(out.stdout, "PA:b:a,b:b"); } +/// Verifies direct reads of uninitialized eval-declared typed properties throw PHP errors. +#[test] +fn test_eval_uninitialized_typed_instance_property_reads_throw_error() { + let out = compile_and_run_capture( + r#"typed; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + echo $object->nullable; +} catch (Error $e) { + echo $e->getMessage(); +} +echo "|"; +echo is_null($object->defaultNull) ? "default-null" : "bad"; +echo "|"; +echo is_null($object->plain) ? "plain-null" : "bad"; +echo "|"; +$object->typed = 0; +echo $object->typed; +echo "|"; +unset($object->typed); +try { + echo $object->typed; +} catch (Error $e) { + echo $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Typed property EvalUninitializedTypedRead::$typed must not be accessed before initialization|\ +Typed property EvalUninitializedTypedRead::$nullable must not be accessed before initialization|\ +default-null|plain-null|0|\ +Typed property EvalUninitializedTypedRead::$typed must not be accessed before initialization" + ); +} + /// Verifies eval `get_object_vars()` exposes initialized generated/AOT properties by scope. #[test] fn test_eval_get_object_vars_exposes_initialized_aot_properties_by_scope() { From 6d90baee7352bc7d65054224bf68a6e07f309bcf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 17:07:57 +0200 Subject: [PATCH 0775/1208] Support eval short set property hooks --- .../src/interpreter/tests/classes.rs | 34 +++++++++++++++++++ .../elephc-magician/src/parser/statements.rs | 14 ++++++-- .../src/parser/tests/classes_errors.rs | 22 +++++++++--- docs/php/eval.md | 4 ++- tests/codegen/eval.rs | 33 ++++++++++++++++++ 5 files changed, 98 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index adb2520572..3f061e6032 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1271,6 +1271,40 @@ return $name->value;"#, assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); } +/// Verifies short set hooks assign their expression result into the raw backing slot. +#[test] +fn execute_program_routes_eval_short_set_property_hooks() { + let program = parse_fragment( + br#"class EvalShortSetHookName { + public string $value { + get => $this->value; + set => trim($value); + } +} +class EvalShortSetHookLabel { + public string $text { + get => $this->text; + set(string $raw) => strtoupper($raw); + } +} +$name = new EvalShortSetHookName(); +$name->value = " Ada "; +echo "[" . $name->value . "]:"; +$label = new EvalShortSetHookLabel(); +$label->text = "hi"; +echo $label->text; +return $label->text;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "[Ada]:HI"); + assert_eq!(values.get(result), FakeValue::String("HI".to_string())); +} + /// Verifies undefined eval property reads and writes dispatch through `__get` and `__set`. #[test] fn execute_program_dispatches_eval_magic_get_and_set() { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 120bf59fa4..4ff7ddc61d 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -1234,14 +1234,22 @@ impl Parser { }; let (body, source_end_line) = match self.current() { TokenKind::Semicolon => return Err(EvalParseError::UnsupportedConstruct), - TokenKind::FatArrow if is_get => { + TokenKind::FatArrow => { self.advance(); let expr = self.parse_expr()?; let source_end_line = self.current_line(); self.expect_semicolon()?; - (vec![EvalStmt::Return(Some(expr))], source_end_line) + let body = if is_get { + vec![EvalStmt::Return(Some(expr))] + } else { + vec![EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: property_name.to_string(), + value: expr, + }] + }; + (body, source_end_line) } - TokenKind::FatArrow => return Err(EvalParseError::UnsupportedConstruct), TokenKind::LBrace => self.parse_block_with_end_line()?, _ => return Err(EvalParseError::UnexpectedToken), }; diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index a4696f7fa3..45084be649 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -754,7 +754,7 @@ fn parse_fragment_accepts_concrete_class_property_hooks() { br#"class DynEvalHooked { public int $value { get => 7; - set { return; } + set => $value + 1; } }"#, ) @@ -774,20 +774,32 @@ fn parse_fragment_accepts_concrete_class_property_hooks() { vec![EvalParameterTypeVariant::Int], false ))) - .with_hooks(true, true)], + .with_hooks(true, true) + .with_virtual(false)], vec![ EvalClassMethod::new( "__propget_value", Vec::new(), vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(7))))] - ), + ) + .with_source_location(EvalSourceLocation::new(3, 3)), EvalClassMethod::new( "__propset_value", vec!["value".to_string()], - vec![EvalStmt::Return(None)] + vec![EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "value".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("value".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))) + } + }] ) + .with_source_location(EvalSourceLocation::new(4, 4)) ] - ))] + ) + .with_source_location(EvalSourceLocation::new(1, 6)))] ); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 7d64206e41..01770fe830 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -572,7 +572,9 @@ non-final constant metadata, and the enum-case reflection classes expose the same inherited `IS_*` constants as PHP. Concrete property hooks are lowered to eval accessor methods; reads and writes route through inherited hooks, while access from the accessor itself uses the -raw backing slot. `readonly` eval properties require a declared type, may be +raw backing slot. Both `get => expr;` and `set => expr;` short forms are +supported; short set hooks store the expression result in the raw backing slot. +`readonly` eval properties require a declared type, may be assigned from the constructor of the declaring class, and later writes fail as eval runtime fatals. A `readonly class` makes declared instance properties readonly implicitly, while declared static properties remain mutable and are not diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a8c8c35534..d22409a5f7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9021,6 +9021,39 @@ try { assert_eq!(err.stdout, "Error:Property EvalHookReadOnly::$answer is read-only"); } +/// Verifies eval-declared short set property hooks store their expression result. +#[test] +fn test_eval_declared_short_set_property_hooks() { + let out = compile_and_run_capture( + r#" $this->value; + set => trim($value); + } +} +class EvalShortSetHookLabel { + public string $text { + get => $this->text; + set(string $raw) => strtoupper($raw); + } +} +$name = new EvalShortSetHookName(); +$name->value = " Ada "; +echo "[" . $name->value . "]:"; +$label = new EvalShortSetHookLabel(); +$label->text = "hi"; +echo $label->text;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "[Ada]:HI"); +} + /// Verifies eval-declared magic property methods handle missing and inaccessible properties. #[test] fn test_eval_declared_magic_property_methods() { From ae0e77b0543dd2f43fb8e547ce4c43a5ab565158 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 17:15:29 +0200 Subject: [PATCH 0776/1208] Cover eval nullsafe property hook reads --- .../src/interpreter/tests/classes.rs | 37 +++++++++++++++++++ tests/codegen/eval.rs | 36 ++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 3f061e6032..a3c6781b55 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1305,6 +1305,43 @@ return $label->text;"#, assert_eq!(values.get(result), FakeValue::String("HI".to_string())); } +/// Verifies nullsafe reads and mixed-case names still route through eval property hooks. +#[test] +fn execute_program_routes_eval_nullsafe_and_mixed_case_property_hooks() { + let program = parse_fragment( + br#"class EvalNullsafeHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + get => $this->first . " " . $this->last; + } +} +class EvalMixedCaseHookBox { + private int $store = 0; + public int $Total { + get { return $this->store; } + } + public function set(int $value) { $this->store = $value; } +} +function eval_hook_describe($person) { + return $person?->full ?? "(none)"; +} +$person = new EvalNullsafeHookPerson(); +$box = new EvalMixedCaseHookBox(); +$box->set(5); +echo eval_hook_describe($person) . "|" . eval_hook_describe(null) . "|" . $box->Total; +return $box->Total;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace|(none)|5"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + /// Verifies undefined eval property reads and writes dispatch through `__get` and `__set`. #[test] fn execute_program_dispatches_eval_magic_get_and_set() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d22409a5f7..281ac3cf82 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9054,6 +9054,42 @@ echo $label->text;'); assert_eq!(out.stdout, "[Ada]:HI"); } +/// Verifies eval-declared nullsafe and mixed-case property hook reads stay routed. +#[test] +fn test_eval_declared_nullsafe_and_mixed_case_property_hooks() { + let out = compile_and_run_capture( + r#" $this->first . " " . $this->last; + } +} +class EvalMixedCaseHookBox { + private int $store = 0; + public int $Total { + get { return $this->store; } + } + public function set(int $value) { $this->store = $value; } +} +function eval_hook_describe($person) { + return $person?->full ?? "(none)"; +} +$person = new EvalNullsafeHookPerson(); +$box = new EvalMixedCaseHookBox(); +$box->set(5); +echo eval_hook_describe($person) . "|" . eval_hook_describe(null) . "|" . $box->Total;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada Lovelace|(none)|5"); +} + /// Verifies eval-declared magic property methods handle missing and inaccessible properties. #[test] fn test_eval_declared_magic_property_methods() { From 0d50f34f4f888d6a16ba5f1d51834acd38eaf493 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 17:22:16 +0200 Subject: [PATCH 0777/1208] Reject reserved eval class constant name --- .../elephc-magician/src/parser/statements.rs | 3 ++ .../src/parser/tests/classes_errors.rs | 16 +++++++++ tests/codegen/eval.rs | 33 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 4ff7ddc61d..b87d5cfdf8 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -763,6 +763,9 @@ impl Parser { let TokenKind::Ident(name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; + if ident_eq(name, "class") { + return Err(EvalParseError::UnsupportedConstruct); + } let name = name.clone(); self.advance(); self.expect(TokenKind::Equal)?; diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 45084be649..269954097c 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -128,6 +128,22 @@ fn parse_fragment_rejects_invalid_type_atom_forms() { } } +/// Verifies eval rejects PHP's reserved `class` class-constant name. +#[test] +fn parse_fragment_rejects_reserved_class_constant_name() { + for source in [ + b"class DynEvalBadConstName { const class = 1; }" as &[u8], + b"interface DynEvalBadIfaceConstName { const class = 1; }", + b"trait DynEvalBadTraitConstName { const class = 1; }", + b"enum DynEvalBadEnumConstName { const class = 1; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + /// Verifies class attributes lower to eval class metadata with supported literal args. #[test] fn parse_fragment_accepts_class_attribute_metadata() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 281ac3cf82..4b2aa023c9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11124,6 +11124,39 @@ echo EvalConstChild::hidden();'); assert_eq!(out.stdout, "2:7:9:2:5"); } +/// Verifies eval rejects PHP's reserved `class` class-constant name. +#[test] +fn test_eval_declared_reserved_class_constant_name_fails() { + for source in [ + r#" Date: Fri, 26 Jun 2026 17:31:10 +0200 Subject: [PATCH 0778/1208] Accept eval by-ref get property hooks --- .../src/interpreter/tests/classes.rs | 28 +++++++++++++++++++ .../elephc-magician/src/parser/statements.rs | 14 ++++++---- .../src/parser/tests/classes_errors.rs | 8 ++++-- docs/php/eval.md | 2 +- tests/codegen/eval.rs | 24 ++++++++++++++++ 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index a3c6781b55..99a2a0f48c 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1246,6 +1246,34 @@ return $person->full;"#, ); } +/// Verifies by-reference get hook syntax routes through the concrete eval get accessor. +#[test] +fn execute_program_reads_eval_by_ref_get_property_hook() { + let program = parse_fragment( + br#"class EvalByRefGetHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + &get => $this->first . " " . $this->last; + } +} +$person = new EvalByRefGetHookPerson(); +echo $person->full; +return $person->full;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace"); + assert_eq!( + values.get(result), + FakeValue::String("Ada Lovelace".to_string()) + ); +} + /// Verifies get/set property hooks can use the raw backing slot from inside accessors. #[test] fn execute_program_routes_eval_property_get_and_set_hooks() { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index b87d5cfdf8..31f92d4817 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -1217,9 +1217,7 @@ impl Parser { &mut self, property_name: &str, ) -> Result<(bool, EvalClassMethod), EvalParseError> { - if self.consume(TokenKind::Ampersand) { - return Err(EvalParseError::UnsupportedConstruct); - } + let returns_by_ref = self.consume(TokenKind::Ampersand); let TokenKind::Ident(hook_name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; @@ -1228,6 +1226,9 @@ impl Parser { if !is_get && !is_set { return Err(EvalParseError::UnsupportedConstruct); } + if returns_by_ref && !is_get { + return Err(EvalParseError::UnsupportedConstruct); + } let source_start_line = self.current_line(); self.advance(); let params = if is_set { @@ -1799,9 +1800,7 @@ impl Parser { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - if self.consume(TokenKind::Ampersand) { - return Err(EvalParseError::UnsupportedConstruct); - } + let returns_by_ref = self.consume(TokenKind::Ampersand); let TokenKind::Ident(hook_name) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; @@ -1810,6 +1809,9 @@ impl Parser { if !is_get && !is_set { return Err(EvalParseError::UnsupportedConstruct); } + if returns_by_ref && !is_get { + return Err(EvalParseError::UnsupportedConstruct); + } self.advance(); if matches!( self.current(), diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 269954097c..d77fc5f27c 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -372,7 +372,7 @@ fn parse_fragment_accepts_interface_property_hook_contracts() { let program = parse_fragment( br#"interface DynEvalHookIface { public string $value { get; set; } - public int $id { get; } + public int $id { &get; } }"#, ) .expect("fragment should parse"); @@ -769,7 +769,7 @@ fn parse_fragment_accepts_concrete_class_property_hooks() { let program = parse_fragment( br#"class DynEvalHooked { public int $value { - get => 7; + &get => 7; set => $value + 1; } }"#, @@ -901,6 +901,8 @@ fn parse_fragment_rejects_invalid_property_hooks() { .expect_err("hooked properties cannot have defaults in eval"); parse_fragment(b"class DynEvalHookStatic { public static int $id { get => 1; } }") .expect_err("static properties cannot have hooks in eval"); + parse_fragment(b"class DynEvalHookByRefSet { public int $id { &set => 1; } }") + .expect_err("set property hooks cannot return by reference"); parse_fragment( b"abstract class DynEvalHookAbstractDefault { abstract public int $id = 1 { get; } }", ) @@ -920,6 +922,8 @@ fn parse_fragment_rejects_invalid_interface_property_hooks() { .expect_err("interface property hooks cannot repeat contracts"); parse_fragment(b"interface DynEvalIfaceHookEmpty { public int $id { } }") .expect_err("interface property hooks require at least one contract"); + parse_fragment(b"interface DynEvalIfaceHookByRefSet { public int $id { &set; } }") + .expect_err("set interface property hooks cannot return by reference"); parse_fragment(b"interface DynEvalIfaceHookDefault { public int $id = 1 { get; } }") .expect_err("interface property hook contracts cannot have defaults"); parse_fragment( diff --git a/docs/php/eval.md b/docs/php/eval.md index 01770fe830..14a0db011a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4b2aa023c9..cf8f50898b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -9021,6 +9021,30 @@ try { assert_eq!(err.stdout, "Error:Property EvalHookReadOnly::$answer is read-only"); } +/// Verifies eval-declared by-reference get hook syntax reads through the accessor. +#[test] +fn test_eval_declared_by_ref_get_property_hook() { + let out = compile_and_run_capture( + r#" $this->first . " " . $this->last; + } +} +$person = new EvalByRefGetHookPerson(); +echo $person->full;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada Lovelace"); +} + /// Verifies eval-declared short set property hooks store their expression result. #[test] fn test_eval_declared_short_set_property_hooks() { From 16d65e16e846ed5e2167e874099e32daf22b47d7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 17:44:52 +0200 Subject: [PATCH 0779/1208] Reject eval classes implementing Throwable --- .../src/interpreter/statements.rs | 23 +++++++++++++++++ .../src/interpreter/tests/classes.rs | 24 ++++++++++++++++++ .../src/interpreter/tests/enums.rs | 7 ++++++ .../interpreter/tests/support/object_ops.rs | 1 + tests/codegen/eval.rs | 25 +++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index adedf3fac9..612b2ddf39 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1321,6 +1321,12 @@ fn eval_builtin_backed_enum_interface_name(name: &str) -> bool { .eq_ignore_ascii_case("BackedEnum") } +/// Returns whether one name is PHP's native Throwable interface. +fn eval_builtin_throwable_interface_name(name: &str) -> bool { + name.trim_start_matches('\\') + .eq_ignore_ascii_case("Throwable") +} + /// Returns whether one name is visible as a native/runtime interface to eval. pub(in crate::interpreter) fn eval_runtime_interface_exists( name: &str, @@ -1367,6 +1373,7 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( return Err(EvalStatus::RuntimeFatal); } } + validate_eval_class_does_not_implement_throwable_interfaces(class, context)?; validate_eval_class_does_not_implement_enum_interfaces(class, context)?; validate_declared_class_interface_members(class, context)?; if !class.is_abstract() { @@ -1549,6 +1556,7 @@ fn validate_eval_enum_interfaces( return Err(EvalStatus::RuntimeFatal); } } + validate_eval_class_does_not_implement_throwable_interfaces(enum_class, context)?; if enum_decl.backing_type().is_none() && pending_class_interface_names(enum_class, context) .iter() @@ -2449,6 +2457,21 @@ fn validate_eval_class_does_not_implement_enum_interfaces( } } +/// Rejects eval classes and enums that directly implement PHP's Throwable contract. +fn validate_eval_class_does_not_implement_throwable_interfaces( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if pending_class_interface_names(class, context) + .iter() + .any(|interface| eval_builtin_throwable_interface_name(interface)) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + /// Validates abstract/final modifiers on an eval-declared class and its methods. fn validate_eval_class_modifiers( class: &EvalClass, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 99a2a0f48c..7f039c04e6 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -73,6 +73,30 @@ return $self instanceof EvalRelativeFactoryBase assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval classes cannot directly implement PHP's special Throwable contract. +#[test] +fn execute_program_rejects_eval_class_implementing_throwable_contracts() { + for (source, label) in [ + ( + br#"class EvalInvalidThrowableClass implements Throwable {}"# as &[u8], + "direct Throwable implementation should fail", + ), + ( + br#"interface EvalThrowableMarker extends Throwable {} +class EvalInvalidThrowableMarkerClass implements EvalThrowableMarker {}"#, + "Throwable-derived interface implementation should fail", + ), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + /// Verifies eval-declared `ArrayAccess` objects dispatch reads, writes, append, probes, and unset. #[test] fn execute_program_dispatches_eval_array_access_objects() { diff --git a/crates/elephc-magician/src/interpreter/tests/enums.rs b/crates/elephc-magician/src/interpreter/tests/enums.rs index 33f8a5415c..ed6e19ae74 100644 --- a/crates/elephc-magician/src/interpreter/tests/enums.rs +++ b/crates/elephc-magician/src/interpreter/tests/enums.rs @@ -415,6 +415,13 @@ enum EvalInvalidPureBackedMarker implements EvalBackedMarker { class EvalInvalidUnitEnumClass implements EvalUnitMarker {}"#, "non-enum class cannot implement UnitEnum through a marker", ); + + assert_invalid_enum_fragment( + br#"enum EvalInvalidThrowableEnum implements Throwable { + case Ready; +}"#, + "enum cannot implement Throwable", + ); } /// Verifies eval allows the enum magic methods PHP permits. diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 3a19b1559e..572dea274f 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -1287,6 +1287,7 @@ impl FakeOps { "Countable", "Iterator", "IteratorAggregate", + "Throwable", "Traversable", ] .iter() diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cf8f50898b..58342a3021 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7485,6 +7485,31 @@ enum EvalDynPureBackedMarker implements EvalDynBackedMarkerBad { ); } +/// Verifies eval-declared classes cannot implement PHP's special Throwable contract. +#[test] +fn test_eval_declared_class_rejects_throwable_interfaces() { + let err = compile_and_run_expect_failure( + r#" Date: Fri, 26 Jun 2026 18:03:48 +0200 Subject: [PATCH 0780/1208] Validate eval builtin interface contracts --- .../src/interpreter/builtin_interfaces.rs | 223 ++++++++++++++++++ crates/elephc-magician/src/interpreter/mod.rs | 2 + .../src/interpreter/statements.rs | 61 +++++ .../src/interpreter/tests/classes.rs | 60 +++++ .../interpreter/tests/support/object_ops.rs | 7 + tests/codegen/eval.rs | 25 ++ 6 files changed, 378 insertions(+) create mode 100644 crates/elephc-magician/src/interpreter/builtin_interfaces.rs diff --git a/crates/elephc-magician/src/interpreter/builtin_interfaces.rs b/crates/elephc-magician/src/interpreter/builtin_interfaces.rs new file mode 100644 index 0000000000..4289e42667 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtin_interfaces.rs @@ -0,0 +1,223 @@ +//! Purpose: +//! Provides eval-side method contracts for PHP builtin interfaces. +//! This keeps runtime interface declaration checks aligned with the main checker catalog. +//! +//! Called from: +//! - `crate::interpreter::statements` while registering eval class-like declarations. +//! +//! Key details: +//! - `Traversable` remains marker-only, matching the current checker model. +//! - Child interfaces include their builtin parent method requirements. + +use super::*; + +/// Returns method requirements for one PHP builtin interface name. +pub(super) fn builtin_interface_method_requirements( + interface: &str, +) -> Vec<(String, EvalInterfaceMethod)> { + let interface = interface.trim_start_matches('\\'); + let mut requirements = Vec::new(); + if interface.eq_ignore_ascii_case("Iterator") { + append_iterator_interface_requirements(&mut requirements); + } else if interface.eq_ignore_ascii_case("IteratorAggregate") { + requirements.push(( + String::from("IteratorAggregate"), + builtin_interface_method( + "getIterator", + &[], + Some(builtin_class_type("Traversable")), + ), + )); + } else if interface.eq_ignore_ascii_case("ArrayAccess") { + append_array_access_interface_requirements(&mut requirements); + } else if interface.eq_ignore_ascii_case("Countable") { + requirements.push(( + String::from("Countable"), + builtin_interface_method( + "count", + &[], + Some(builtin_type(EvalParameterTypeVariant::Int)), + ), + )); + } else if interface.eq_ignore_ascii_case("OuterIterator") { + append_iterator_interface_requirements(&mut requirements); + requirements.push(( + String::from("OuterIterator"), + builtin_interface_method( + "getInnerIterator", + &[], + Some(nullable_builtin_class_type("Iterator")), + ), + )); + } else if interface.eq_ignore_ascii_case("RecursiveIterator") { + append_iterator_interface_requirements(&mut requirements); + requirements.push(( + String::from("RecursiveIterator"), + builtin_interface_method( + "getChildren", + &[], + Some(nullable_builtin_class_type("RecursiveIterator")), + ), + )); + requirements.push(( + String::from("RecursiveIterator"), + builtin_interface_method( + "hasChildren", + &[], + Some(builtin_type(EvalParameterTypeVariant::Bool)), + ), + )); + } else if interface.eq_ignore_ascii_case("SeekableIterator") { + append_iterator_interface_requirements(&mut requirements); + requirements.push(( + String::from("SeekableIterator"), + builtin_interface_method( + "seek", + &[("offset", builtin_type(EvalParameterTypeVariant::Int))], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + } else if interface.eq_ignore_ascii_case("SplObserver") { + requirements.push(( + String::from("SplObserver"), + builtin_interface_method( + "update", + &[("subject", builtin_class_type("SplSubject"))], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + } else if interface.eq_ignore_ascii_case("SplSubject") { + requirements.push(( + String::from("SplSubject"), + builtin_interface_method( + "attach", + &[("observer", builtin_class_type("SplObserver"))], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + requirements.push(( + String::from("SplSubject"), + builtin_interface_method( + "detach", + &[("observer", builtin_class_type("SplObserver"))], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + requirements.push(( + String::from("SplSubject"), + builtin_interface_method( + "notify", + &[], + Some(builtin_type(EvalParameterTypeVariant::Void)), + ), + )); + } else if interface.eq_ignore_ascii_case("Stringable") { + requirements.push(( + String::from("Stringable"), + builtin_interface_method( + "__toString", + &[], + Some(builtin_type(EvalParameterTypeVariant::String)), + ), + )); + } else if interface.eq_ignore_ascii_case("JsonSerializable") { + requirements.push(( + String::from("JsonSerializable"), + builtin_interface_method( + "jsonSerialize", + &[], + Some(builtin_type(EvalParameterTypeVariant::Mixed)), + ), + )); + } + requirements +} + +/// Appends the five methods required by PHP's `Iterator` interface. +fn append_iterator_interface_requirements(requirements: &mut Vec<(String, EvalInterfaceMethod)>) { + for (method, return_type) in [ + ("current", EvalParameterTypeVariant::Mixed), + ("key", EvalParameterTypeVariant::Mixed), + ("next", EvalParameterTypeVariant::Void), + ("valid", EvalParameterTypeVariant::Bool), + ("rewind", EvalParameterTypeVariant::Void), + ] { + requirements.push(( + String::from("Iterator"), + builtin_interface_method(method, &[], Some(builtin_type(return_type))), + )); + } +} + +/// Appends the four methods required by PHP's `ArrayAccess` interface. +fn append_array_access_interface_requirements( + requirements: &mut Vec<(String, EvalInterfaceMethod)>, +) { + for (method, params, return_type) in [ + ( + "offsetExists", + vec![("offset", builtin_type(EvalParameterTypeVariant::Mixed))], + EvalParameterTypeVariant::Bool, + ), + ( + "offsetGet", + vec![("offset", builtin_type(EvalParameterTypeVariant::Mixed))], + EvalParameterTypeVariant::Mixed, + ), + ( + "offsetSet", + vec![ + ("offset", builtin_type(EvalParameterTypeVariant::Mixed)), + ("value", builtin_type(EvalParameterTypeVariant::Mixed)), + ], + EvalParameterTypeVariant::Void, + ), + ( + "offsetUnset", + vec![("offset", builtin_type(EvalParameterTypeVariant::Mixed))], + EvalParameterTypeVariant::Void, + ), + ] { + requirements.push(( + String::from("ArrayAccess"), + builtin_interface_method(method, ¶ms, Some(builtin_type(return_type))), + )); + } +} + +/// Builds one synthetic eval interface method requirement for a PHP builtin interface. +fn builtin_interface_method( + name: &str, + params: &[(&str, EvalParameterType)], + return_type: Option, +) -> EvalInterfaceMethod { + EvalInterfaceMethod::new( + name, + params + .iter() + .map(|(param_name, _)| (*param_name).to_string()) + .collect(), + ) + .with_parameter_types( + params + .iter() + .map(|(_, param_type)| Some(param_type.clone())) + .collect(), + ) + .with_return_type(return_type) +} + +/// Returns one non-nullable scalar/object builtin interface type. +fn builtin_type(variant: EvalParameterTypeVariant) -> EvalParameterType { + EvalParameterType::new(vec![variant], false) +} + +/// Returns one non-nullable class/interface builtin interface type. +fn builtin_class_type(name: &str) -> EvalParameterType { + builtin_type(EvalParameterTypeVariant::Class(name.to_string())) +} + +/// Returns one nullable class/interface builtin interface type. +fn nullable_builtin_class_type(name: &str) -> EvalParameterType { + EvalParameterType::new(vec![EvalParameterTypeVariant::Class(name.to_string())], true) +} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 1c53e8cbf9..e8faf6e956 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -12,6 +12,7 @@ //! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. mod array_literals; +mod builtin_interfaces; mod builtins; mod constant_eval; mod constants; @@ -49,6 +50,7 @@ use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; use array_literals::*; +use builtin_interfaces::*; use builtins::*; use constant_eval::*; use constants::*; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 612b2ddf39..f4125ca4e0 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1376,8 +1376,10 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( validate_eval_class_does_not_implement_throwable_interfaces(class, context)?; validate_eval_class_does_not_implement_enum_interfaces(class, context)?; validate_declared_class_interface_members(class, context)?; + validate_declared_class_builtin_interface_members(class, context)?; if !class.is_abstract() { validate_concrete_class_requirements(class, context)?; + validate_concrete_class_builtin_interface_requirements(class, context)?; } if context.define_class(class.clone()) { initialize_eval_declared_constants( @@ -1538,6 +1540,8 @@ fn validate_eval_enum_decl( let enum_class = enum_decl.as_class_metadata(); validate_eval_class_modifiers(&enum_class, context)?; validate_eval_enum_interfaces(enum_decl, &enum_class, context, values)?; + validate_declared_class_builtin_interface_members(&enum_class, context)?; + validate_concrete_class_builtin_interface_requirements(&enum_class, context)?; validate_concrete_class_requirements(&enum_class, context) } @@ -3044,6 +3048,36 @@ fn validate_declared_class_interface_members( Ok(()) } +/// Validates declared class methods against PHP builtin runtime interface contracts. +fn validate_declared_class_builtin_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + pending_class_builtin_interface_method_requirements(class, context) + { + let Some((declaring_class, method)) = + pending_class_method(class, requirement.name(), context) + else { + continue; + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static() + || !class_method_satisfies_interface_signature( + &method, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + /// Validates class methods present for an eval interface, even on abstract classes. fn validate_declared_class_interface_methods( class: &EvalClass, @@ -3226,6 +3260,21 @@ fn validate_concrete_class_requirements( Ok(()) } +/// Validates concrete class methods required by PHP builtin runtime interfaces. +fn validate_concrete_class_builtin_interface_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + pending_class_builtin_interface_method_requirements(class, context) + { + if !class_has_interface_method(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + /// Returns inherited abstract methods that the pending class has not concretized. fn pending_class_abstract_method_requirements( class: &EvalClass, @@ -3423,6 +3472,18 @@ fn push_pending_class_interface_name( } } +/// Returns PHP builtin interface method requirements inherited by a pending class. +fn pending_class_builtin_interface_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Vec<(String, EvalInterfaceMethod)> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + requirements.extend(builtin_interface_method_requirements(&interface)); + } + requirements +} + /// Validates that one eval class provides methods required by one eval interface. fn validate_class_implements_eval_interface( class: &EvalClass, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 7f039c04e6..ad7e23a399 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -97,6 +97,66 @@ class EvalInvalidThrowableMarkerClass implements EvalThrowableMarker {}"#, } } +/// Verifies eval classes must satisfy methods required by PHP builtin interfaces. +#[test] +fn execute_program_rejects_invalid_builtin_interface_implementations() { + for (source, label) in [ + ( + br#"class EvalMissingCountable implements Countable {}"# as &[u8], + "missing Countable::count should fail", + ), + ( + br#"class EvalBadCountableReturn implements Countable { + public function count(): string { return "1"; } +}"#, + "incompatible Countable::count return type should fail", + ), + ( + br#"class EvalMissingStringable implements Stringable {}"#, + "missing Stringable::__toString should fail", + ), + ( + br#"class EvalBadIterator implements Iterator { + public function current(): mixed { return null; } + public function key(): mixed { return null; } + public function next(): void {} + public function valid(): bool { return false; } +}"#, + "missing Iterator::rewind should fail", + ), + ( + br#"class EvalBadJsonSerializable implements JsonSerializable { + public static function jsonSerialize(): mixed { return []; } +}"#, + "static JsonSerializable::jsonSerialize should fail", + ), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies abstract eval classes can defer PHP builtin interface methods. +#[test] +fn execute_program_allows_abstract_builtin_interface_implementations() { + let program = parse_fragment( + br#"abstract class EvalAbstractCountable implements Countable {} +return class_exists("EvalAbstractCountable");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval-declared `ArrayAccess` objects dispatch reads, writes, append, probes, and unset. #[test] fn execute_program_dispatches_eval_array_access_objects() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 572dea274f..3d53096148 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -1287,6 +1287,13 @@ impl FakeOps { "Countable", "Iterator", "IteratorAggregate", + "JsonSerializable", + "OuterIterator", + "RecursiveIterator", + "SeekableIterator", + "SplObserver", + "SplSubject", + "Stringable", "Throwable", "Traversable", ] diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 58342a3021..365406298d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7510,6 +7510,31 @@ class EvalDynInvalidThrowableMarker implements EvalDynThrowableMarker {}'); ); } +/// Verifies eval-declared classes must satisfy PHP builtin interface methods. +#[test] +fn test_eval_declared_class_rejects_missing_builtin_interface_methods() { + let out = compile_and_run( + r#" Date: Fri, 26 Jun 2026 18:38:22 +0200 Subject: [PATCH 0781/1208] Support eval classes extending AOT parents --- crates/elephc-magician/src/context.rs | 54 +++- .../src/interpreter/builtins/symbols.rs | 15 +- .../src/interpreter/reflection.rs | 30 ++- .../src/interpreter/statements.rs | 237 ++++++++++++++++-- .../src/interpreter/tests/classes.rs | 31 +++ .../interpreter/tests/support/object_ops.rs | 8 +- tests/codegen/eval.rs | 37 +++ 7 files changed, 370 insertions(+), 42 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 60467b0edd..086f733863 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -1428,22 +1428,62 @@ impl ElephcEvalContext { /// Returns direct and inherited parent class names for an eval-declared class. pub fn class_parent_names(&self, class_name: &str) -> Vec { let mut parents = Vec::new(); - let mut current = self.class(class_name).and_then(EvalClass::parent); + let mut current = self + .class(class_name) + .and_then(EvalClass::parent) + .map(str::to_string) + .or_else(|| self.native_class_parent(class_name).map(str::to_string)); let mut seen = HashSet::new(); while let Some(parent) = current { - let Some(parent_class) = self.class(parent) else { - break; - }; - let key = normalize_class_name(parent_class.name()); + let parent = self + .resolve_class_name(&parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); + let key = normalize_class_name(&parent); if !seen.insert(key) { break; } - parents.push(parent_class.name().trim_start_matches('\\').to_string()); - current = parent_class.parent(); + if let Some(parent_class) = self.class(&parent) { + parents.push(parent_class.name().trim_start_matches('\\').to_string()); + current = parent_class + .parent() + .map(str::to_string) + .or_else(|| self.native_class_parent(parent_class.name()).map(str::to_string)); + } else { + parents.push(parent); + current = parents + .last() + .and_then(|parent| self.native_class_parent(parent)) + .map(str::to_string); + } } parents } + /// Returns the nearest runtime/AOT parent backing an eval class hierarchy. + pub fn class_native_parent_name(&self, class_name: &str) -> Option { + let mut current = self + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let mut seen = HashSet::new(); + loop { + if !seen.insert(normalize_class_name(¤t)) { + return None; + } + let parent = self + .class(¤t) + .and_then(EvalClass::parent) + .map(str::to_string) + .or_else(|| self.native_class_parent(¤t).map(str::to_string))?; + let parent = self + .resolve_class_name(&parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); + if self.class(&parent).is_none() { + return Some(parent); + } + current = parent; + } + } + /// Returns direct and inherited interface names for an eval-declared class. pub fn class_interface_names(&self, class_name: &str) -> Vec { let mut interfaces = Vec::new(); diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index f955191d4b..5626bdb119 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -524,9 +524,18 @@ pub(in crate::interpreter) fn dynamic_object_is_a( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let identity = values.object_identity(object)?; - Ok(context - .dynamic_object_class(identity) - .map(|class| context.class_is_a(class.name(), target_class, exclude_self))) + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(None); + }; + if context.class_is_a(class.name(), target_class, exclude_self) { + return Ok(Some(true)); + } + if context.class_native_parent_name(class.name()).is_some() { + return values + .object_is_a(object, target_class, exclude_self) + .map(Some); + } + Ok(Some(false)) } /// Evaluates PHP's `isset(...)` language construct over eval-visible values. diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 9923dac487..1fe23f3c68 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -2931,6 +2931,28 @@ fn eval_reflection_aot_class_flags( Ok(Some((flags, modifiers))) } +/// Returns AOT class modifiers relevant to validating an eval `extends` clause. +pub(in crate::interpreter) fn eval_reflection_aot_class_inheritance_modifiers( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + if flags + & (EVAL_REFLECTION_CLASS_FLAG_INTERFACE + | EVAL_REFLECTION_CLASS_FLAG_TRAIT + | EVAL_REFLECTION_CLASS_FLAG_ENUM) + != 0 + { + return Ok(None); + } + Ok(Some(( + flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0, + ))) +} + /// Returns the catchable error for generated/AOT allocation without constructor, if any. pub(in crate::interpreter) fn eval_reflection_aot_class_without_constructor_error( class_name: &str, @@ -5882,16 +5904,12 @@ fn eval_reflection_class_like_attributes( }) } -/// Returns the canonical eval parent class name for ReflectionClass metadata. +/// Returns the PHP-visible parent class name for ReflectionClass metadata. fn eval_reflection_parent_class_name( class: &EvalClass, context: &ElephcEvalContext, ) -> Option { - let parent = class.parent()?; - context - .class(parent) - .map(|parent_class| parent_class.name().trim_start_matches('\\').to_string()) - .or_else(|| Some(parent.trim_start_matches('\\').to_string())) + context.class_parent_names(class.name()).into_iter().next() } /// Returns PHP's `ReflectionClass::isInstantiable()` value for eval class metadata. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index f4125ca4e0..657e048928 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1357,17 +1357,7 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( let class = expand_eval_class_traits(class, context)?.with_readonly_properties(); let class = &class; validate_eval_class_modifiers(class, context)?; - if let Some(parent) = class.parent() { - let Some(parent_class) = context.class(parent) else { - return Err(EvalStatus::RuntimeFatal); - }; - if parent_class.is_final() - || parent_class.is_readonly_class() != class.is_readonly_class() - || context.class_is_a(parent, name, false) - { - return Err(EvalStatus::RuntimeFatal); - } - } + let native_parent = validate_eval_class_parent(class, context, values)?; for interface in class.interfaces() { if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { return Err(EvalStatus::RuntimeFatal); @@ -1382,6 +1372,11 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( validate_concrete_class_builtin_interface_requirements(class, context)?; } if context.define_class(class.clone()) { + if let Some(parent) = native_parent.as_deref() { + if !context.define_native_class_parent(class.name(), parent) { + return Err(EvalStatus::RuntimeFatal); + } + } initialize_eval_declared_constants( class.name(), class.constants(), @@ -1395,6 +1390,41 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( } } +/// Validates an eval class parent and returns an AOT parent name when the parent is runtime-backed. +fn validate_eval_class_parent( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(None); + }; + let parent = context + .resolve_class_name(parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); + if let Some(parent_class) = context.class(&parent) { + if parent_class.is_final() + || parent_class.is_readonly_class() != class.is_readonly_class() + || context.class_is_a(&parent, class.name(), false) + { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(None); + } + let Some((parent_is_final, parent_is_readonly)) = + eval_reflection_aot_class_inheritance_modifiers(&parent, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if parent_is_final + || parent_is_readonly != class.is_readonly_class() + || native_class_is_a(&parent, class.name(), context) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(parent)) +} + /// Registers one eval anonymous class expression if this execution has not seen it yet. pub(in crate::interpreter) fn ensure_eval_anonymous_class_decl( class: &EvalClass, @@ -3063,7 +3093,7 @@ fn validate_declared_class_builtin_interface_members( }; if method.visibility() != EvalVisibility::Public || method.is_static() != requirement.is_static() - || !class_method_satisfies_interface_signature( + || !class_method_satisfies_builtin_interface_signature( &method, &declaring_class, &requirement, @@ -3268,7 +3298,7 @@ fn validate_concrete_class_builtin_interface_requirements( for (requirement_owner, requirement) in pending_class_builtin_interface_method_requirements(class, context) { - if !class_has_interface_method(class, &requirement_owner, &requirement, context) { + if !class_has_builtin_interface_method(class, &requirement_owner, &requirement, context) { return Err(EvalStatus::RuntimeFatal); } } @@ -3507,6 +3537,44 @@ fn validate_class_implements_eval_interface( Ok(()) } +/// Returns whether a class or its eval parents satisfy one builtin interface method signature. +fn class_has_builtin_interface_method( + class: &EvalClass, + requirement_owner: &str, + requirement: &EvalInterfaceMethod, + context: &ElephcEvalContext, +) -> bool { + if let Some(method) = class.method(requirement.name()) { + return method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_builtin_interface_signature( + method, + class.name(), + requirement, + requirement_owner, + Some(class), + context, + ); + } + class + .parent() + .and_then(|parent| context.class_method(parent, requirement.name())) + .is_some_and(|(declaring_class, method)| { + method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_builtin_interface_signature( + &method, + &declaring_class, + requirement, + requirement_owner, + Some(class), + context, + ) + }) +} + /// Returns whether a class or its eval parents satisfy one interface method signature. fn class_has_interface_method( class: &EvalClass, @@ -3553,6 +3621,47 @@ fn class_method_satisfies_interface_signature( requirement_owner: &str, pending_class: Option<&EvalClass>, context: &ElephcEvalContext, +) -> bool { + class_method_satisfies_interface_signature_with_return_mode( + method, + method_owner, + requirement, + requirement_owner, + pending_class, + context, + false, + ) +} + +/// Returns whether one class method can satisfy a PHP builtin interface method contract. +fn class_method_satisfies_builtin_interface_signature( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + class_method_satisfies_interface_signature_with_return_mode( + method, + method_owner, + requirement, + requirement_owner, + pending_class, + context, + true, + ) +} + +/// Returns whether one class method satisfies an interface signature with configurable return checks. +fn class_method_satisfies_interface_signature_with_return_mode( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + allow_missing_return_type: bool, ) -> bool { method_signature_accepts( method.params().len(), @@ -3573,14 +3682,15 @@ fn class_method_satisfies_interface_signature( requirement.params().len(), pending_class, context, - ) && method_return_type_signature_accepts( - method.return_type(), - method_owner, - requirement.return_type(), - requirement_owner, - pending_class, - context, - ) + ) && ((allow_missing_return_type && method.return_type().is_none()) + || method_return_type_signature_accepts( + method.return_type(), + method_owner, + requirement.return_type(), + requirement_owner, + pending_class, + context, + )) } /// Returns whether one class property is compatible with one interface property contract. @@ -4231,6 +4341,13 @@ pub(in crate::interpreter) fn eval_property_isset_result( eval_dynamic_property_for_access(&object_class_name, property_name, context) { if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { + let storage_property_name = + eval_instance_property_storage_name(&declaring_class, &property); + if property.property_type().is_some() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + return Ok(false); + } let value = eval_property_get_result(object, property_name, context, values)?; return Ok(!values.is_null(value)?); } @@ -5618,6 +5735,21 @@ pub(in crate::interpreter) fn eval_static_method_call_result( values, ); } + if let Some(parent) = context.class_native_parent_name(&class_name) { + let has_native_method = + eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values)? + .is_some() + || eval_native_static_magic_method_available(&parent, context, values)?; + if has_native_method { + return eval_native_static_method_with_evaluated_args( + &parent, + method_name, + evaluated_args, + context, + values, + ); + } + } if context.has_class(&class_name) || context.has_interface(&class_name) || context.has_trait(&class_name) @@ -5957,7 +6089,11 @@ pub(in crate::interpreter) fn resolve_eval_static_class_name( context .class(current) .and_then(EvalClass::parent) - .map(str::to_string) + .map(|parent| { + context + .resolve_class_name(parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()) + }) .or_else(|| context.native_class_parent(current).map(str::to_string)) .ok_or(EvalStatus::RuntimeFatal) } @@ -6057,7 +6193,34 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( )?; eval_release_value(context, values, result)?; } else if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); + if let Some(parent) = context.class_native_parent_name(class.name()) { + eval_native_constructor_with_evaluated_args( + &parent, + object, + evaluated_args, + context, + values, + )?; + } else { + return Err(EvalStatus::RuntimeFatal); + } + } else if let Some(parent) = context.class_native_parent_name(class.name()) { + if eval_aot_method_dispatch_metadata_in_hierarchy( + &parent, + "__construct", + context, + values, + )? + .is_some() + { + eval_native_constructor_with_evaluated_args( + &parent, + object, + Vec::new(), + context, + values, + )?; + } } Ok(object) } @@ -6157,7 +6320,10 @@ fn eval_dynamic_class_allocate_object( if class.is_abstract() || context.has_enum(class.name()) { return Err(EvalStatus::RuntimeFatal); } - let object = values.new_object("stdClass")?; + let backing_class = context + .class_native_parent_name(class.name()) + .unwrap_or_else(|| String::from("stdClass")); + let object = values.new_object(&backing_class)?; let identity = values.object_identity(object)?; context.register_dynamic_object(identity, class.name()); let mut class_chain = context.class_chain(class.name()); @@ -6685,6 +6851,29 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( } inaccessible_method = Some((class_name, method)); } + if inaccessible_method.is_none() { + if let Some(parent) = context.class_native_parent_name(&called_class_name) { + let has_native_method = + eval_aot_method_dispatch_metadata_in_hierarchy( + &parent, + method_name, + context, + values, + )? + .is_some() + || eval_native_instance_magic_method_available(&parent, context, values)?; + if has_native_method { + return eval_native_method_with_evaluated_args( + object, + &parent, + method_name, + evaluated_args, + context, + values, + ); + } + } + } if let Some(result) = eval_magic_instance_method_call( object, &called_class_name, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index ad7e23a399..f82e91dd7e 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -73,6 +73,37 @@ return $self instanceof EvalRelativeFactoryBase assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval classes can extend runtime/AOT classes through the dynamic backing object. +#[test] +fn execute_program_extends_runtime_class_from_eval_declaration() { + let program = parse_fragment( + br#"class EvalRuntimeParentChild extends KnownClass { + public function own() { return $this->read_x() + 1; } +} +$box = new EvalRuntimeParentChild(9); +echo get_class($box); echo ":"; +echo get_parent_class($box); echo ":"; +echo is_a($box, "EvalRuntimeParentChild") ? "D" : "d"; echo ":"; +echo is_a($box, "KnownClass") ? "K" : "k"; echo ":"; +echo is_a($box, "KnownInterface") ? "I" : "i"; echo ":"; +echo is_subclass_of($box, "KnownClass") ? "S" : "s"; echo ":"; +echo is_subclass_of("EvalRuntimeParentChild", "KnownClass") ? "N" : "n"; echo ":"; +echo $box->read_x(); echo ":"; +return $box->own();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRuntimeParentChild:KnownClass:D:K:I:S:N:9:" + ); + assert_eq!(values.get(result), FakeValue::Int(10)); +} + /// Verifies eval classes cannot directly implement PHP's special Throwable contract. #[test] fn execute_program_rejects_eval_class_implementing_throwable_contracts() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 3d53096148..8ae30c4aaf 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -1193,7 +1193,9 @@ impl FakeOps { return Ok(None); } match method_name.to_ascii_lowercase().as_str() { - "run" => Ok(Some(EVAL_REFLECTION_MEMBER_FLAG_PUBLIC)), + "answer" | "add_x" | "add2_x" | "read_x" | "run" => { + Ok(Some(EVAL_REFLECTION_MEMBER_FLAG_PUBLIC)) + } "helper" => Ok(Some( EVAL_REFLECTION_MEMBER_FLAG_STATIC | EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, )), @@ -1213,7 +1215,9 @@ impl FakeOps { return Ok(None); } match method_name.to_ascii_lowercase().as_str() { - "run" | "helper" | "locked" => Ok(Some("KnownClass".to_string())), + "answer" | "add_x" | "add2_x" | "read_x" | "run" | "helper" | "locked" => { + Ok(Some("KnownClass".to_string())) + } _ => Ok(None), } } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 365406298d..ab322a1169 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7639,6 +7639,43 @@ echo 7 instanceof MissingEvalInstance ? "bad" : "S";'); assert_eq!(out.stdout, "APICBFDTXOS"); } +/// Verifies eval-declared classes can extend generated/AOT classes at runtime. +#[test] +fn test_eval_declared_class_extends_aot_parent() { + let out = compile_and_run_capture( + r#"x = $x; } + public function read() { return $this->x; } +} +eval('class EvalRuntimeParentChild extends EvalRuntimeParentBase { + public function own() { return $this->read() + 1; } +} +$box = new EvalRuntimeParentChild(6); +echo get_class($box); echo ":"; +echo get_parent_class($box); echo ":"; +echo is_a($box, "EvalRuntimeParentChild") ? "D" : "d"; echo ":"; +echo is_a($box, "EvalRuntimeParentBase") ? "P" : "p"; echo ":"; +echo is_subclass_of($box, "EvalRuntimeParentBase") ? "S" : "s"; echo ":"; +echo is_subclass_of("EvalRuntimeParentChild", "EvalRuntimeParentBase") ? "N" : "n"; echo ":"; +echo $box->read(); echo ":"; +echo $box->own(); echo ":"; +$parent = (new ReflectionClass("EvalRuntimeParentChild"))->getParentClass(); +echo $parent ? $parent->getName() : "missing";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalRuntimeParentChild:EvalRuntimeParentBase:D:P:S:N:6:7:EvalRuntimeParentBase" + ); +} + /// Verifies eval-declared class inheritance uses dynamic methods and metadata. #[test] fn test_eval_declared_class_inherits_methods_and_metadata() { From fae2c3f9153c8a34bfbdab87c5a22bb616407b5c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 18:48:36 +0200 Subject: [PATCH 0782/1208] Preserve eval static forwarding semantics --- .../src/interpreter/statements.rs | 38 +++++++++++++++++-- .../src/interpreter/tests/classes.rs | 30 +++++++++++++++ tests/codegen/eval.rs | 30 +++++++++++++++ 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 657e048928..426ad6361b 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -5661,7 +5661,9 @@ pub(in crate::interpreter) fn eval_static_method_call_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + let class_name = receiver.dispatch_class; + let called_class_name = receiver.called_class; if let Some(result) = eval_builtin_property_hook_type_static_method_result( &class_name, method_name, @@ -5711,6 +5713,7 @@ pub(in crate::interpreter) fn eval_static_method_call_result( if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { if let Some(result) = eval_magic_static_method_call( &class_name, + &called_class_name, method_name, evaluated_args, context, @@ -5728,7 +5731,7 @@ pub(in crate::interpreter) fn eval_static_method_call_result( } return eval_dynamic_static_method_with_values( &declaring_class, - &class_name, + &called_class_name, &method, evaluated_args, context, @@ -5757,6 +5760,7 @@ pub(in crate::interpreter) fn eval_static_method_call_result( { if let Some(result) = eval_magic_static_method_call( &class_name, + &called_class_name, method_name, evaluated_args, context, @@ -6108,6 +6112,33 @@ pub(in crate::interpreter) fn resolve_eval_static_class_name( } } +/// Resolved static method dispatch metadata preserving PHP late-static forwarding. +struct EvalStaticMethodReceiver { + dispatch_class: String, + called_class: String, +} + +/// Resolves static method receivers into lookup and late-static called-class names. +fn resolve_eval_static_method_receiver( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + let dispatch_class = resolve_eval_static_member_class_name(class_name, context)?; + let called_class = match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" => context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal)?, + "static" => dispatch_class.clone(), + _ => dispatch_class.clone(), + }; + Ok(EvalStaticMethodReceiver { + dispatch_class, + called_class, + }) +} + /// Resolves static member receivers while allowing non-eval class names to reach AOT lookup. pub(in crate::interpreter) fn resolve_eval_static_member_class_name( class_name: &str, @@ -6983,6 +7014,7 @@ fn eval_magic_instance_method_call( /// Dispatches a missing or inaccessible eval static method through `__callStatic()`. fn eval_magic_static_method_call( class_name: &str, + called_class_name: &str, method_name: &str, evaluated_args: Vec, context: &mut ElephcEvalContext, @@ -6997,7 +7029,7 @@ fn eval_magic_static_method_call( let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; eval_dynamic_static_method_with_values( &declaring_class, - class_name, + called_class_name, &method, magic_args, context, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index f82e91dd7e..ccfe4b94f4 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -73,6 +73,36 @@ return $self instanceof EvalRelativeFactoryBase assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval static method calls preserve PHP late-static forwarding rules. +#[test] +fn execute_program_forwards_eval_static_method_called_class() { + let program = parse_fragment( + br#"class EvalForwardA { + public static function who() { return static::tag(); } + public static function relayNamed() { return EvalForwardA::who(); } + public static function relaySelf() { return self::who(); } + public static function tag() { return "A"; } +} +class EvalForwardB extends EvalForwardA { + public static function relayParent() { return parent::who(); } + public static function relayStatic() { return static::who(); } + public static function tag() { return "B"; } +} +echo EvalForwardB::relayNamed(); echo ":"; +echo EvalForwardB::relaySelf(); echo ":"; +echo EvalForwardB::relayParent(); echo ":"; +return EvalForwardB::relayStatic();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:B:B:"); + assert_eq!(values.get(result), FakeValue::String("B".to_string())); +} + /// Verifies eval classes can extend runtime/AOT classes through the dynamic backing object. #[test] fn execute_program_extends_runtime_class_from_eval_declaration() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ab322a1169..1a272f1f66 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7716,6 +7716,36 @@ echo count($implements) . ":" . $implements["EvalDynIface"];'); ); } +/// Verifies eval static method calls preserve PHP forwarding and late-static binding. +#[test] +fn test_eval_declared_static_method_calls_preserve_forwarding() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 19:03:51 +0200 Subject: [PATCH 0783/1208] Preserve eval first-class static forwarding --- crates/elephc-magician/src/context.rs | 43 ++++++++++++++ crates/elephc-magician/src/eval_ir.rs | 4 ++ .../interpreter/builtins/registry/callable.rs | 57 +++++++++++++++---- .../builtins/registry/callable_validation.rs | 4 +- .../src/interpreter/builtins/symbols.rs | 4 +- .../src/interpreter/control.rs | 1 + .../src/interpreter/expressions.rs | 29 ++++++++++ .../src/interpreter/statements.rs | 52 +++++++++++++++-- .../src/interpreter/tests/dynamic_calls.rs | 38 +++++++++++++ .../elephc-magician/src/parser/expressions.rs | 24 ++++---- .../elephc-magician/src/parser/statements.rs | 3 + .../src/parser/tests/static_members.rs | 12 ++-- tests/codegen/eval.rs | 38 +++++++++++++ 13 files changed, 272 insertions(+), 37 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 086f733863..4ea77021c2 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -67,6 +67,14 @@ pub enum EvalReferenceTarget { }, } +/// Late-static dispatch metadata attached to eval-created static callable arrays. +#[derive(Clone)] +struct EvalStaticCallableMetadata { + class_name: String, + method: String, + called_class: String, +} + /// Native AOT function callback metadata visible to runtime eval fragments. #[derive(Clone)] pub struct NativeFunction { @@ -446,6 +454,7 @@ pub struct ElephcEvalContext { eval_reflection_properties: HashMap, eval_dynamic_reflection_properties: HashSet, eval_reflection_class_constants: HashMap, + eval_static_callables: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, class_stack: Vec, @@ -507,6 +516,7 @@ impl ElephcEvalContext { eval_reflection_properties: HashMap::new(), eval_dynamic_reflection_properties: HashSet::new(), eval_reflection_class_constants: HashMap::new(), + eval_static_callables: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -569,6 +579,7 @@ impl ElephcEvalContext { eval_reflection_properties: HashMap::new(), eval_dynamic_reflection_properties: HashSet::new(), eval_reflection_class_constants: HashMap::new(), + eval_static_callables: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -649,6 +660,38 @@ impl ElephcEvalContext { }) } + /// Registers one eval-created static callable array with late-static dispatch metadata. + pub fn register_eval_static_callable( + &mut self, + callable: RuntimeCellHandle, + class_name: &str, + method: &str, + called_class: &str, + ) { + self.eval_static_callables.insert( + callable.as_ptr() as usize, + EvalStaticCallableMetadata { + class_name: class_name.trim_start_matches('\\').to_string(), + method: method.to_string(), + called_class: called_class.trim_start_matches('\\').to_string(), + }, + ); + } + + /// Returns the captured late-static called class for one matching static callable array. + pub fn eval_static_callable_called_class( + &self, + callable: RuntimeCellHandle, + class_name: &str, + method: &str, + ) -> Option<&str> { + let metadata = self.eval_static_callables.get(&(callable.as_ptr() as usize))?; + let class_name = class_name.trim_start_matches('\\'); + (metadata.class_name.eq_ignore_ascii_case(class_name) + && metadata.method.eq_ignore_ascii_case(method)) + .then_some(metadata.called_class.as_str()) + } + /// Resolves a PHP class-like name to eval class, interface, trait, or alias spelling. pub fn resolve_class_like_name(&self, name: &str) -> Option { let key = normalize_class_name(name); diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 0528ad9445..b87c4c67b9 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2427,6 +2427,10 @@ pub enum EvalExpr { name: String, fallback_name: Option, }, + StaticMethodCallable { + class_name: String, + method: Box, + }, DynamicCall { callee: Box, args: Vec, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index d4ce3518ba..10b3eddd05 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -105,7 +105,7 @@ pub(in crate::interpreter) fn eval_callable( return eval_object_callable(callback, context, values); } if values.is_array_like(callback)? { - return eval_array_callable(callback, values); + return eval_array_callable(callback, context, values); } eval_string_callable(callback, values) } @@ -132,6 +132,7 @@ pub(in crate::interpreter) fn eval_object_callable( /// Normalizes one two-element object-method or static-method callable array. pub(in crate::interpreter) fn eval_array_callable( callback: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if values.array_len(callback)? != 2 { @@ -151,7 +152,14 @@ pub(in crate::interpreter) fn eval_array_callable( EVAL_TAG_STRING => { let class_name = String::from_utf8(values.string_bytes(receiver)?) .map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(EvaluatedCallable::StaticMethod { class_name, method }) + let called_class = context + .eval_static_callable_called_class(callback, &class_name, &method) + .map(str::to_string); + Ok(EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + }) } _ => Err(EvalStatus::UnsupportedConstruct), } @@ -171,6 +179,7 @@ pub(in crate::interpreter) fn eval_string_callable( return Ok(EvaluatedCallable::StaticMethod { class_name: class_name.trim_start_matches('\\').to_string(), method: method.to_string(), + called_class: None, }); } Ok(EvaluatedCallable::Named( @@ -214,13 +223,27 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( EvaluatedCallable::ObjectMethod { object, method } => { eval_method_call_result(*object, method, evaluated_args, context, values) } - EvaluatedCallable::StaticMethod { class_name, method } => eval_static_method_call_result( + EvaluatedCallable::StaticMethod { class_name, method, - positional_args(evaluated_args), - context, - values, - ), + called_class, + } => match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method, + positional_args(evaluated_args), + context, + values, + ), + None => eval_static_method_call_result( + class_name, + method, + positional_args(evaluated_args), + context, + values, + ), + }, } } @@ -247,9 +270,23 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( values, ) } - EvaluatedCallable::StaticMethod { class_name, method } => { - eval_static_method_call_result(class_name, method, evaluated_args, context, values) - } + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + } => match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method, + evaluated_args, + context, + values, + ), + None => { + eval_static_method_call_result(class_name, method, evaluated_args, context, values) + } + }, } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index 49d0528cc3..20ac2c1fb6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -28,7 +28,9 @@ pub(in crate::interpreter) fn eval_validate_call_user_func_callback( values, ) } - EvaluatedCallable::StaticMethod { class_name, method } => { + EvaluatedCallable::StaticMethod { + class_name, method, .. + } => { eval_validate_call_user_func_static_method( class_name, method, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 5626bdb119..d219f9b7da 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -895,7 +895,9 @@ fn eval_callable_probe_exists( EvaluatedCallable::ObjectMethod { object, method } => { eval_object_method_callable_probe(*object, method, context, values) } - EvaluatedCallable::StaticMethod { class_name, method } => { + EvaluatedCallable::StaticMethod { + class_name, method, .. + } => { eval_static_method_callable_probe(class_name, method, context, values) } } diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index 889284bb86..4b0f5eeb00 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -56,6 +56,7 @@ pub(super) enum EvaluatedCallable { StaticMethod { class_name: String, method: String, + called_class: Option, }, } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 8fc0e0ac88..47febdb373 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -42,6 +42,9 @@ pub(in crate::interpreter) fn eval_expr( name, fallback_name, } => eval_function_callable_expr(name, fallback_name.as_deref(), context, values), + EvalExpr::StaticMethodCallable { class_name, method } => { + eval_static_method_callable_expr(class_name, method, context, scope, values) + } EvalExpr::DynamicCall { callee, args } => { eval_dynamic_call(callee, args, context, scope, values) } @@ -788,6 +791,32 @@ fn eval_function_callable_expr( ) } +/// Materializes a first-class static method callable while retaining late-static metadata. +fn eval_static_method_callable_expr( + class_name: &str, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let mut array = values.array_new(2)?; + let zero = values.int(0)?; + let one = values.int(1)?; + let class_value = values.string(&receiver.dispatch_class)?; + let method_value = values.string(&method)?; + array = values.array_set(array, zero, class_value)?; + array = values.array_set(array, one, method_value)?; + context.register_eval_static_callable( + array, + &receiver.dispatch_class, + &method, + &receiver.called_class, + ); + Ok(array) +} + /// Evaluates a variable or expression callable and dispatches it with source-order arguments. pub(in crate::interpreter) fn eval_dynamic_call( callee: &EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 426ad6361b..4e68444ed4 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -5662,8 +5662,48 @@ pub(in crate::interpreter) fn eval_static_method_call_result( values: &mut impl RuntimeValueOps, ) -> Result { let receiver = resolve_eval_static_method_receiver(class_name, context)?; - let class_name = receiver.dispatch_class; - let called_class_name = receiver.called_class; + eval_static_method_call_result_resolved( + receiver.dispatch_class, + receiver.called_class, + method_name, + evaluated_args, + context, + values, + ) +} + +/// Dispatches a static method call using a first-class callable's captured called class. +pub(in crate::interpreter) fn eval_static_method_call_result_with_called_class( + class_name: &str, + called_class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + let called_class_name = context + .resolve_class_name(called_class_name) + .unwrap_or_else(|| called_class_name.trim_start_matches('\\').to_string()); + eval_static_method_call_result_resolved( + class_name, + called_class_name, + method_name, + evaluated_args, + context, + values, + ) +} + +/// Dispatches a static method call after lookup and late-static names have been resolved. +fn eval_static_method_call_result_resolved( + class_name: String, + called_class_name: String, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { if let Some(result) = eval_builtin_property_hook_type_static_method_result( &class_name, method_name, @@ -6113,13 +6153,13 @@ pub(in crate::interpreter) fn resolve_eval_static_class_name( } /// Resolved static method dispatch metadata preserving PHP late-static forwarding. -struct EvalStaticMethodReceiver { - dispatch_class: String, - called_class: String, +pub(in crate::interpreter) struct EvalStaticMethodReceiver { + pub(in crate::interpreter) dispatch_class: String, + pub(in crate::interpreter) called_class: String, } /// Resolves static method receivers into lookup and late-static called-class names. -fn resolve_eval_static_method_receiver( +pub(in crate::interpreter) fn resolve_eval_static_method_receiver( class_name: &str, context: &ElephcEvalContext, ) -> Result { diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 4b0cf18e48..899194be66 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -277,6 +277,44 @@ return EvalFirstClassCallableChild::relay("ok");"#, ); } +/// Verifies first-class static callables preserve late-static forwarding metadata. +#[test] +fn execute_program_first_class_static_callables_preserve_called_class() { + let program = parse_fragment( + br#"class EvalFirstClassStaticForwardBase { + public static function who() { + return static::tag(); + } + public static function tag() { + return "base"; + } + public static function relaySelf() { + $fn = self::who(...); + return $fn(); + } +} +class EvalFirstClassStaticForwardChild extends EvalFirstClassStaticForwardBase { + public static function relayParent() { + $fn = parent::who(...); + return $fn(); + } + public static function tag() { + return "child"; + } +} +echo EvalFirstClassStaticForwardChild::relayParent(); echo ":"; +return EvalFirstClassStaticForwardChild::relaySelf();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child:"); + assert_eq!(values.get(result), FakeValue::String("child".to_string())); +} + /// Verifies invokable eval objects dispatch through variable and callback call paths. #[test] fn execute_program_invokes_eval_object_callables() { diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index db2644f38b..d35cc64f95 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -842,10 +842,10 @@ impl Parser { self.advance(); if matches!(self.current(), TokenKind::LParen) { if self.consume_first_class_callable_marker() { - return Ok(Self::callable_array_expr( - EvalExpr::ClassNameFetch { class_name }, - EvalExpr::LoadVar(property), - )); + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(EvalExpr::LoadVar(property)), + }); } let args = self.parse_call_args()?; return Ok(EvalExpr::DynamicStaticMethodCall { @@ -878,10 +878,10 @@ impl Parser { let method = method.clone(); self.advance(); if self.consume_first_class_callable_marker() { - return Ok(Self::callable_array_expr( - EvalExpr::ClassNameFetch { class_name }, - EvalExpr::Const(EvalConst::String(method)), - )); + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(EvalExpr::Const(EvalConst::String(method))), + }); } let args = self.parse_call_args()?; Ok(EvalExpr::StaticMethodCall { @@ -896,10 +896,10 @@ impl Parser { self.expect(TokenKind::RBrace)?; if matches!(self.current(), TokenKind::LParen) { if self.consume_first_class_callable_marker() { - return Ok(Self::callable_array_expr( - EvalExpr::ClassNameFetch { class_name }, - member, - )); + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(member), + }); } let args = self.parse_call_args()?; return Ok(EvalExpr::DynamicStaticMethodCall { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 31f92d4817..541ecf9afe 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -4010,6 +4010,9 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { | EvalExpr::Magic(_) | EvalExpr::NamespacedConstFetch { .. } | EvalExpr::StaticPropertyGet { .. } => false, + EvalExpr::StaticMethodCallable { method, .. } => { + eval_expr_uses_this_property(method, property_name) + } EvalExpr::Include { path, .. } | EvalExpr::Cast { expr: path, .. } | EvalExpr::Clone(path) diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs index b334e61b58..d6004d6b11 100644 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ b/crates/elephc-magician/src/parser/tests/static_members.rs @@ -64,19 +64,17 @@ fn parse_fragment_accepts_static_method_call_expression() { ); } -/// Verifies first-class static method syntax lowers to a PHP callable array. +/// Verifies first-class static method syntax retains static callable metadata. #[test] fn parse_fragment_accepts_first_class_static_method_callable_source() { let program = parse_fragment(br#"return EvalStaticBox::Read(...);"#).expect("fragment should parse"); assert_eq!( program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::ClassNameFetch { - class_name: "EvalStaticBox".to_string(), - }), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("Read".to_string()))), - ])))] + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCallable { + class_name: "EvalStaticBox".to_string(), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + }))] ); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1a272f1f66..723e038a12 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10101,6 +10101,44 @@ echo EvalFirstClassCallableChild::relay("ok");'); assert_eq!(out.stdout, "8:4:7:8:AB:45:2:9:a1:321:child:ok"); } +/// Verifies eval first-class static callables preserve late-static forwarding metadata. +#[test] +fn test_eval_first_class_static_callables_preserve_called_class() { + let out = compile_and_run_capture( + r#" Date: Fri, 26 Jun 2026 19:19:03 +0200 Subject: [PATCH 0784/1208] Support eval get_called_class builtin --- .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/symbols.rs | 6 +++ .../interpreter/builtins/registry/names.rs | 1 + .../src/interpreter/builtins/scalars/types.rs | 30 +++++++++++ .../src/interpreter/expressions.rs | 1 + .../src/interpreter/tests/classes.rs | 45 ++++++++++++++++ docs/internals/the-runtime.md | 2 + docs/php/eval.md | 2 +- tests/codegen/eval.rs | 54 +++++++++++++++++++ 9 files changed, 141 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 219ce74d0d..745836e3ce 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -137,6 +137,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), "settype" => Some(&["var", "type"]), + "get_called_class" => Some(&[]), "get_class" => Some(&["object"]), "get_class_methods" => Some(&["object_or_class"]), "get_object_vars" => Some(&["object"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs index 8ea0f7d57b..b94874f3f4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -70,6 +70,12 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( "is_a" | "is_subclass_of" => { eval_is_a_relation_result(name, evaluated_args, context, values)? } + "get_called_class" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_called_class_result(context, values)? + } "get_class" => { let [object] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 7ea77ea865..20c2fe7f4a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -151,6 +151,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "getprotobynumber" | "getservbyname" | "getservbyport" + | "get_called_class" | "get_class" | "get_class_methods" | "get_declared_classes" diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs index adac645226..2b0be08cdb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs @@ -67,6 +67,36 @@ pub(in crate::interpreter) fn eval_gettype_result( values.string(eval_gettype_name(tag)) } +/// Evaluates PHP's `get_called_class()` against the current eval method scope. +pub(in crate::interpreter) fn eval_builtin_get_called_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_called_class_result(context, values) +} + +/// Returns the current late-static-bound class name or throws PHP's class-scope error. +pub(in crate::interpreter) fn eval_get_called_class_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + else { + return eval_throw_error( + "get_called_class() must be called from within a class", + context, + values, + ); + }; + values.string(class_name.trim_start_matches('\\')) +} + /// Evaluates PHP's `get_class(...)` over one eval object expression. pub(in crate::interpreter) fn eval_builtin_get_class( args: &[EvalExpr], diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 47febdb373..7cb70ed51d 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1003,6 +1003,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), + "get_called_class" => eval_builtin_get_called_class(args, context, values), "get_class" => eval_builtin_get_class(args, context, scope, values), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { eval_builtin_get_declared_symbols(name, args, context, values) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index ccfe4b94f4..6c5c50a2eb 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -103,6 +103,51 @@ return EvalForwardB::relayStatic();"#, assert_eq!(values.get(result), FakeValue::String("B".to_string())); } +/// Verifies `get_called_class()` follows eval late-static method scopes. +#[test] +fn execute_program_dispatches_get_called_class_builtin() { + let program = parse_fragment( + br#"class EvalCalledClassBase { + public function instanceWho() { return get_called_class(); } + public function instanceCall() { return call_user_func("get_called_class"); } + public static function staticWho() { return get_called_class(); } + public static function staticCallArray() { return call_user_func_array("get_called_class", []); } + public static function makeCallable() { return get_called_class(...); } +} +class EvalCalledClassChild extends EvalCalledClassBase {} +$child = new EvalCalledClassChild(); +echo $child->instanceWho(); echo ":"; +echo $child->instanceCall(); echo ":"; +echo EvalCalledClassChild::staticWho(); echo ":"; +echo EvalCalledClassChild::staticCallArray(); echo ":"; +echo EvalCalledClassBase::staticWho(); echo ":"; +try { + get_called_class(); +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); echo ":"; +} +$fn = EvalCalledClassChild::makeCallable(); +try { + $fn(); +} catch (Error $e) { + echo "callable:"; +} +echo function_exists("get_called_class"); echo is_callable("get_called_class"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassBase:Error:get_called_class() must be called from within a class:callable:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval classes can extend runtime/AOT classes through the dynamic backing object. #[test] fn execute_program_extends_runtime_class_from_eval_declaration() { diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index 359d826d93..a91cda8bcf 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -23,6 +23,8 @@ The bridge crate parses the fragment at runtime into a small EvalIR and interpre Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. +Eval type-introspection builtin coverage includes `get_called_class()`. It reads the current late-static-bound eval method scope for direct calls, `call_user_func()`, and `call_user_func_array()`, and throws PHP's class-scope `Error` when invoked outside a class scope. + Eval `catch (Throwable)` may omit the variable binding; in that case the interpreter consumes the throwable and releases the unbound cell before executing the catch body. The eval callable dispatcher currently normalizes string callbacks and two-element object-method callable arrays such as `[$this, "method"]`. Object-method arrays are supported for direct `$cb(...)`, `call_user_func()`, `call_user_func_array()`, and `iterator_apply()` paths with positional arguments; static-method callable arrays and closure/descriptor callback values are still handled only by the native descriptor runtime outside eval fragments. diff --git a/docs/php/eval.md b/docs/php/eval.md index 14a0db011a..986444cfd8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -689,7 +689,7 @@ where listed below unless a note says otherwise. | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | | Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | -| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_resource()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_called_class()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_resource()` | | Debug output | `print_r()`, `var_dump()` | | Constants | `define()`, `defined()` | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 723e038a12..975b748b24 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10139,6 +10139,60 @@ echo EvalFirstClassStaticForwardChild::relaySelf();'); assert_eq!(out.stdout, "child:child"); } +/// Verifies eval `get_called_class()` follows late-static scopes and remains call-time scoped. +#[test] +fn test_eval_get_called_class_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceWho() . ":"; +echo $child->instanceCall() . ":"; +echo EvalGetCalledClassChild::staticWho() . ":"; +echo EvalGetCalledClassChild::staticCallArray() . ":"; +echo EvalGetCalledClassBase::staticWho() . ":"; +try { + get_called_class(); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +$fn = EvalGetCalledClassChild::makeCallable(); +try { + $fn(); +} catch (Error $e) { + echo "callable:"; +} +echo function_exists("get_called_class") . ":" . is_callable("get_called_class");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalGetCalledClassChild:EvalGetCalledClassChild:EvalGetCalledClassChild:EvalGetCalledClassChild:EvalGetCalledClassBase:Error:get_called_class() must be called from within a class:callable:1:1" + ); +} + /// Verifies eval dynamic static receivers dispatch methods, properties, constants, and `::class`. #[test] fn test_eval_dynamic_static_receivers() { From 4d1502adf025fe926cae3bc2d6ca49087346a5fd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 19:27:28 +0200 Subject: [PATCH 0785/1208] Support eval no-arg class name builtins --- .../builtins/registry/dispatch/symbols.rs | 20 +++++--- .../src/interpreter/builtins/scalars/types.rs | 49 ++++++++++++++++--- .../src/interpreter/tests/builtins_scalars.rs | 42 ++++++++++++++++ docs/php/eval.md | 6 +++ tests/codegen/eval.rs | 42 ++++++++++++++++ 5 files changed, 143 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs index b94874f3f4..c79edf19ce 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -77,10 +77,11 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( eval_get_called_class_result(context, values)? } "get_class" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_get_class_result(*object, context, values)? + match evaluated_args { + [] => eval_get_class_no_arg_result(context, values)?, + [object] => eval_get_class_result(*object, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + } } "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { if !evaluated_args.is_empty() { @@ -89,10 +90,13 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( eval_get_declared_symbols_result(name, context, values)? } "get_parent_class" => { - let [object_or_class] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_get_parent_class_result(*object_or_class, context, values)? + match evaluated_args { + [] => eval_get_parent_class_no_arg_result(context, values)?, + [object_or_class] => { + eval_get_parent_class_result(*object_or_class, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + } } "get_resource_id" | "get_resource_type" => { let [resource] = evaluated_args else { diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs index 2b0be08cdb..253f95f575 100644 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs @@ -104,11 +104,29 @@ pub(in crate::interpreter) fn eval_builtin_get_class( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); + match args { + [] => eval_get_class_no_arg_result(context, values), + [object] => { + let object = eval_expr(object, context, scope, values)?; + eval_get_class_result(object, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves PHP's deprecated no-arg `get_class()` form from the current class scope. +pub(in crate::interpreter) fn eval_get_class_no_arg_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context.current_class_scope() else { + return eval_throw_error( + "get_class() without arguments must be called from within a class", + context, + values, + ); }; - let object = eval_expr(object, context, scope, values)?; - eval_get_class_result(object, context, values) + values.string(class_name.trim_start_matches('\\')) } /// Resolves the PHP-visible class name for one already materialized object cell. @@ -164,11 +182,26 @@ pub(in crate::interpreter) fn eval_builtin_get_parent_class( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [object_or_class] = args else { - return Err(EvalStatus::RuntimeFatal); + match args { + [] => eval_get_parent_class_no_arg_result(context, values), + [object_or_class] => { + let object_or_class = eval_expr(object_or_class, context, scope, values)?; + eval_get_parent_class_result(object_or_class, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves PHP's deprecated no-arg `get_parent_class()` form from the current class scope. +pub(in crate::interpreter) fn eval_get_parent_class_no_arg_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context.current_class_scope() else { + return values.string(""); }; - let object_or_class = eval_expr(object_or_class, context, scope, values)?; - eval_get_parent_class_result(object_or_class, context, values) + let class_name = values.string(class_name.trim_start_matches('\\'))?; + eval_get_parent_class_result(class_name, context, values) } /// Resolves the PHP-visible parent class name for one object or class-name cell. diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs index 61ba6d9db7..957aa39569 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs @@ -237,6 +237,48 @@ return call_user_func_array("get_class", ["object" => $object]);"#, FakeValue::String("stdClass".to_string()) ); } + +/// Verifies eval no-arg `get_class()` and `get_parent_class()` read method class scope. +#[test] +fn execute_program_dispatches_no_arg_class_name_builtins() { + let program = parse_fragment( + br#"class EvalNoArgClassParent {} +class EvalNoArgClassBase extends EvalNoArgClassParent { + public function inherited() { + return get_class() . ":" . get_parent_class(); + } + public function inheritedCallable() { + return call_user_func("get_class") . ":" . call_user_func_array("get_parent_class", []); + } +} +class EvalNoArgClassChild extends EvalNoArgClassBase { + public function own() { + return get_class() . ":" . get_parent_class(); + } +} +$child = new EvalNoArgClassChild(); +echo $child->inherited(); echo ":"; +echo $child->inheritedCallable(); echo ":"; +echo $child->own(); echo ":"; +try { + get_class(); +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); echo ":"; +} +return get_parent_class();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalNoArgClassBase:EvalNoArgClassParent:EvalNoArgClassBase:EvalNoArgClassParent:EvalNoArgClassChild:EvalNoArgClassBase:Error:get_class() without arguments must be called from within a class:" + ); + assert_eq!(values.get(result), FakeValue::String(String::new())); +} /// Verifies eval `get_parent_class()` reads object and class-string parents by callable. #[test] fn execute_program_dispatches_get_parent_class_builtin() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 986444cfd8..cd084a10dd 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -700,6 +700,12 @@ including dynamic property names, and static properties including dynamic receivers or dynamic property names. Callable dispatch of `settype()` remains by-value and emits PHP's by-reference warning. +Eval supports the deprecated no-argument `get_class()` and `get_parent_class()` +forms inside class methods. They read the declaring method's class scope rather +than the late-static called class. Outside a class scope, no-argument +`get_class()` throws `Error`; no-argument `get_parent_class()` returns an empty +string, matching elephc's AOT lowering for parentless or scope-less lookups. + Eval `array_map()` supports one or more source arrays with supported callable values or a `null` callback. `array_filter()`, `array_reduce()`, `array_walk()`, `usort()`, `uasort()`, and `uksort()` share the same callback diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 975b748b24..b83e0031f3 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3993,6 +3993,48 @@ echo function_exists("get_class");'); ); } +/// Verifies eval no-arg `get_class()` and `get_parent_class()` use method class scope. +#[test] +fn test_eval_dispatches_no_arg_class_name_builtins() { + let out = compile_and_run_capture( + r#"inherited() . ":"; +echo $child->inheritedCallable() . ":"; +echo $child->own() . ":"; +try { + get_class(); +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +echo get_parent_class();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalNoArgRuntimeBase:EvalNoArgRuntimeParent:EvalNoArgRuntimeBase:EvalNoArgRuntimeParent:EvalNoArgRuntimeChild:EvalNoArgRuntimeBase:Error:get_class() without arguments must be called from within a class:" + ); +} + /// Verifies eval `get_parent_class()` resolves AOT object and class-string parents. #[test] fn test_eval_dispatches_get_parent_class_builtin_call() { From 5ab06d4efead72063eaebd41ebf47866a68349f6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 19:43:25 +0200 Subject: [PATCH 0786/1208] Support eval trait magic constants --- crates/elephc-magician/src/context.rs | 58 +++++++++++++++++++ crates/elephc-magician/src/eval_ir.rs | 35 +++++++++++ .../src/interpreter/constant_eval.rs | 20 +++++-- .../src/interpreter/statements.rs | 21 ++++++- .../interpreter/tests/trait_adaptations.rs | 48 +++++++++++++++ tests/codegen/eval.rs | 42 ++++++++++++++ 6 files changed, 216 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 4ea77021c2..149b2c8313 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -36,6 +36,15 @@ pub struct ElephcEvalExecutionScope { called_class_stack: Vec, } +/// PHP-visible magic-constant names for the current eval execution frame. +#[derive(Debug, Clone, PartialEq, Eq)] +struct EvalMagicScope { + function_name: String, + method_name: String, + class_name: String, + trait_name: String, +} + /// Caller-side storage target that can remain linked to an eval object property. #[derive(Clone)] pub enum EvalReferenceTarget { @@ -459,6 +468,7 @@ pub struct ElephcEvalContext { function_stack: Vec, class_stack: Vec, called_class_stack: Vec, + magic_stack: Vec, pending_throw: Option, spl_autoload_extensions: String, streams: EvalStreamResources, @@ -521,6 +531,7 @@ impl ElephcEvalContext { function_stack: Vec::new(), class_stack: Vec::new(), called_class_stack: Vec::new(), + magic_stack: Vec::new(), pending_throw: None, spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), @@ -584,6 +595,7 @@ impl ElephcEvalContext { function_stack: Vec::new(), class_stack: Vec::new(), called_class_stack: Vec::new(), + magic_stack: Vec::new(), pending_throw: None, spl_autoload_extensions: String::from(".inc,.php"), streams: EvalStreamResources::default(), @@ -2766,6 +2778,52 @@ impl ElephcEvalContext { self.called_class_stack.last().map(String::as_str) } + /// Pushes PHP-visible method magic constants for the current eval method frame. + pub fn push_method_magic_scope(&mut self, class_name: &str, method: &EvalClassMethod) { + self.magic_stack.push(EvalMagicScope { + function_name: method.magic_function_name().to_string(), + method_name: method.magic_method_name(class_name), + class_name: class_name.trim_start_matches('\\').to_string(), + trait_name: method + .trait_origin() + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + + /// Pops the current PHP-visible eval magic-constant scope. + pub fn pop_magic_scope(&mut self) { + self.magic_stack.pop(); + } + + /// Returns the PHP `__FUNCTION__` value for the current eval frame. + pub fn current_magic_function(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.function_name.as_str()) + } + + /// Returns the PHP `__METHOD__` value for the current eval method frame. + pub fn current_magic_method(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.method_name.as_str()) + } + + /// Returns the PHP `__CLASS__` value for the current eval method frame. + pub fn current_magic_class(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.class_name.as_str()) + } + + /// Returns the PHP `__TRAIT__` value for the current eval method frame. + pub fn current_magic_trait(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.trait_name.as_str()) + } + /// Captures the current eval execution stacks for later caller-context-sensitive work. pub fn execution_scope(&self) -> ElephcEvalExecutionScope { ElephcEvalExecutionScope { diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index b87c4c67b9..7245ff8aad 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2153,6 +2153,8 @@ pub enum EvalVisibility { #[derive(Debug, Clone)] pub struct EvalClassMethod { name: String, + trait_origin: Option, + trait_origin_method: Option, source_location: Option, attributes: Vec, visibility: EvalVisibility, @@ -2174,6 +2176,8 @@ impl PartialEq for EvalClassMethod { /// Compares class method metadata while ignoring retained source-location decoration. fn eq(&self, other: &Self) -> bool { self.name == other.name + && self.trait_origin == other.trait_origin + && self.trait_origin_method == other.trait_origin_method && self.attributes == other.attributes && self.visibility == other.visibility && self.is_static == other.is_static @@ -2234,6 +2238,8 @@ impl EvalClassMethod { let parameter_is_variadic = vec![false; params.len()]; Self { name: name.into(), + trait_origin: None, + trait_origin_method: None, source_location: None, attributes: Vec::new(), visibility, @@ -2263,6 +2269,35 @@ impl EvalClassMethod { &self.name } + /// Returns a copy of this method with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + self.trait_origin_method = Some(self.name.clone()); + } + self + } + + /// Returns the trait that originally declared this imported method, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + + /// Returns the PHP `__FUNCTION__` value for this method body. + pub fn magic_function_name(&self) -> &str { + self.trait_origin_method.as_deref().unwrap_or(&self.name) + } + + /// Returns the PHP `__METHOD__` value for this method body. + pub fn magic_method_name(&self, class_name: &str) -> String { + let owner = self.trait_origin().unwrap_or(class_name); + format!( + "{}::{}", + owner.trim_start_matches('\\'), + self.magic_function_name() + ) + } + /// Returns eval-fragment source-location metadata, when retained. pub const fn source_location(&self) -> Option { self.source_location diff --git a/crates/elephc-magician/src/interpreter/constant_eval.rs b/crates/elephc-magician/src/interpreter/constant_eval.rs index 61263e8d39..47bf66993c 100644 --- a/crates/elephc-magician/src/interpreter/constant_eval.rs +++ b/crates/elephc-magician/src/interpreter/constant_eval.rs @@ -173,10 +173,20 @@ pub(super) fn eval_magic_const( EvalMagicConst::File => values.string(&context.eval_file_magic()), EvalMagicConst::Dir => values.string(context.call_dir()), EvalMagicConst::Line(line) => values.int(*line), - EvalMagicConst::Function => values.string(context.current_function().unwrap_or("")), - EvalMagicConst::Method => values.string(context.current_function().unwrap_or("")), - EvalMagicConst::Class | EvalMagicConst::Namespace | EvalMagicConst::Trait => { - values.string("") - } + EvalMagicConst::Function => values.string( + context + .current_magic_function() + .or_else(|| context.current_function()) + .unwrap_or(""), + ), + EvalMagicConst::Method => values.string( + context + .current_magic_method() + .or_else(|| context.current_function()) + .unwrap_or(""), + ), + EvalMagicConst::Class => values.string(context.current_magic_class().unwrap_or("")), + EvalMagicConst::Namespace => values.string(""), + EvalMagicConst::Trait => values.string(context.current_magic_trait().unwrap_or("")), } } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 4e68444ed4..a5c0eb7ad2 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2335,8 +2335,14 @@ fn append_eval_trait_methods( if class_method_names.contains(&key) { continue; } - let method = - apply_trait_visibility_adaptations(trait_decl.name(), method, trait_adaptations); + let method = method + .clone() + .with_trait_origin(trait_decl.name().to_string()); + let method = apply_trait_visibility_adaptations( + trait_decl.name(), + &method, + trait_adaptations, + ); if !trait_method_names.insert(key) { return Err(EvalStatus::RuntimeFatal); } @@ -2387,7 +2393,10 @@ fn append_eval_trait_method_aliases( } continue; }; - let mut alias_method = source_method.renamed(alias.clone()); + let mut alias_method = source_method + .clone() + .with_trait_origin(trait_decl.name().to_string()) + .renamed(alias.clone()); if let Some(visibility) = visibility { alias_method = alias_method.with_visibility_override(*visibility); } @@ -7832,6 +7841,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( context.push_function(qualified_method_name.clone()); context.push_class_scope(class_name.to_string()); context.push_called_class_scope(called_class_name.to_string()); + context.push_method_magic_scope(class_name, method); let evaluated_args = match bind_evaluated_method_args( method.params(), method.parameter_types(), @@ -7844,6 +7854,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( ) { Ok(args) => args, Err(status) => { + context.pop_magic_scope(); context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); @@ -7884,6 +7895,7 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( values, ), }; + context.pop_magic_scope(); context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); @@ -7926,6 +7938,7 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_fla context.push_function(qualified_method_name.clone()); context.push_class_scope(class_name.to_string()); context.push_called_class_scope(called_class_name.to_string()); + context.push_method_magic_scope(class_name, method); let evaluated_args = match bind_evaluated_method_args( method.params(), method.parameter_types(), @@ -7938,6 +7951,7 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_fla ) { Ok(args) => args, Err(status) => { + context.pop_magic_scope(); context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); @@ -7977,6 +7991,7 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_fla values, ), }; + context.pop_magic_scope(); context.pop_called_class_scope(); context.pop_class_scope(); context.pop_function(); diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index fcd8f99996..3b80914389 100644 --- a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -145,6 +145,54 @@ return $box->word();"#, assert_eq!(values.get(result), FakeValue::String("in".to_string())); } +/// Verifies nested trait aliases preserve PHP magic constants from the declaring trait. +#[test] +fn execute_program_resolves_eval_trait_method_magic_constants() { + let program = parse_fragment( + br#"namespace EvalMagicTraitNs; +trait EvalMagicInner { + public function report() { + return __NAMESPACE__ . "|" . __CLASS__ . "|" . __TRAIT__ . "|" . __METHOD__ . "|" . __FUNCTION__; + } + public static function stat() { + return __NAMESPACE__ . "|" . __CLASS__ . "|" . __TRAIT__ . "|" . __METHOD__ . "|" . __FUNCTION__; + } +} +trait EvalMagicOuter { + use EvalMagicInner { + report as aliasReport; + stat as aliasStat; + } +} +class EvalMagicBox { + use EvalMagicOuter; +} +echo (new EvalMagicBox())->aliasReport(); echo ":"; +return EvalMagicBox::aliasStat();"#, + ) + .expect("parse eval trait magic fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let expected_instance = concat!( + "EvalMagicTraitNs|EvalMagicTraitNs\\EvalMagicBox|", + "EvalMagicTraitNs\\EvalMagicInner|", + "EvalMagicTraitNs\\EvalMagicInner::report|report:" + ); + let expected_static = concat!( + "EvalMagicTraitNs|EvalMagicTraitNs\\EvalMagicBox|", + "EvalMagicTraitNs\\EvalMagicInner|", + "EvalMagicTraitNs\\EvalMagicInner::stat|stat" + ); + + assert_eq!(values.output, expected_instance); + assert_eq!( + values.get(result), + FakeValue::String(expected_static.to_string()) + ); +} + /// Verifies trait-to-trait composition rejects missing inner traits. #[test] fn execute_program_rejects_missing_eval_trait_used_by_trait() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b83e0031f3..e323667612 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4593,6 +4593,48 @@ class Box { assert_eq!(out, "[||]"); } +/// Verifies eval trait methods expose PHP magic constants from the declaring trait. +#[test] +fn test_eval_trait_method_magic_constants_execute_through_bridge() { + let out = compile_and_run( + r#"aliasReport(); echo ":"; +echo Box::aliasStat();'); +"#, + ); + let expected = concat!( + "EvalBridgeTraitMagic|EvalBridgeTraitMagic\\Box|", + "EvalBridgeTraitMagic\\Inner|", + "EvalBridgeTraitMagic\\Inner::report|report:", + "EvalBridgeTraitMagic|EvalBridgeTraitMagic\\Box|", + "EvalBridgeTraitMagic\\Inner|", + "EvalBridgeTraitMagic\\Inner::stat|stat" + ); + + assert_eq!( + out, + expected + ); +} + /// Verifies eval-declared functions persist across eval calls in the same generated context. #[test] fn test_eval_declared_function_persists_across_eval_calls() { From 2262833bbd6e4acb70bd0eb43e77ae462d615c1e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 19:44:11 +0200 Subject: [PATCH 0787/1208] Document eval method magic constants --- docs/php/eval.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index cd084a10dd..daeeb8229b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -78,7 +78,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | -| Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`. | +| Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty top-level eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, eval-declared-function `__FUNCTION__` / `__METHOD__`, and eval-declared-method `__FUNCTION__` / `__METHOD__` / `__CLASS__` / `__TRAIT__`, including trait-origin names for imported trait methods. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | | Ternaries | Full ternary and short ternary (`?:`). | | Match | Strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default`. A miss without `default` is reported as an eval runtime fatal. | From 387c55ecf2cd6c22522e5caea9718bc1054df849 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 20:00:49 +0200 Subject: [PATCH 0788/1208] Support eval trait member magic defaults --- crates/elephc-magician/src/context.rs | 16 ++++ crates/elephc-magician/src/eval_ir.rs | 30 +++++++ .../src/interpreter/reflection.rs | 78 ++++++++++++++++++- .../src/interpreter/statements.rs | 53 +++++++++++-- .../interpreter/tests/trait_adaptations.rs | 68 ++++++++++++++++ docs/php/eval.md | 2 +- tests/codegen/eval.rs | 54 +++++++++++++ 7 files changed, 292 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 149b2c8313..5672f7764b 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -2791,6 +2791,22 @@ impl ElephcEvalContext { }); } + /// Pushes PHP-visible class-like member magic constants for default expressions. + pub fn push_class_like_member_magic_scope( + &mut self, + class_name: &str, + trait_name: Option<&str>, + ) { + self.magic_stack.push(EvalMagicScope { + function_name: String::new(), + method_name: String::new(), + class_name: class_name.trim_start_matches('\\').to_string(), + trait_name: trait_name + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + /// Pops the current PHP-visible eval magic-constant scope. pub fn pop_magic_scope(&mut self) { self.magic_stack.pop(); diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 7245ff8aad..5952da47da 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -1705,6 +1705,7 @@ pub enum EvalTraitAdaptation { #[derive(Debug, Clone, PartialEq)] pub struct EvalClassConstant { name: String, + trait_origin: Option, attributes: Vec, visibility: EvalVisibility, is_final: bool, @@ -1735,6 +1736,7 @@ impl EvalClassConstant { ) -> Self { Self { name: name.into(), + trait_origin: None, attributes: Vec::new(), visibility, is_final, @@ -1748,11 +1750,24 @@ impl EvalClassConstant { self } + /// Returns a copy of this constant with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + } + self + } + /// Returns the PHP-visible class constant name. pub fn name(&self) -> &str { &self.name } + /// Returns the trait that originally declared this imported constant, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + /// Returns attributes declared directly on this class constant. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes @@ -1912,6 +1927,7 @@ impl EvalTrait { #[derive(Debug, Clone, PartialEq)] pub struct EvalClassProperty { name: String, + trait_origin: Option, attributes: Vec, property_type: Option, visibility: EvalVisibility, @@ -1983,6 +1999,7 @@ impl EvalClassProperty { ) -> Self { Self { name: name.into(), + trait_origin: None, attributes: Vec::new(), property_type: None, visibility, @@ -2034,6 +2051,14 @@ impl EvalClassProperty { self } + /// Returns a copy of this property with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + } + self + } + /// Returns a copy of this property with retained type metadata. pub fn with_type(mut self, property_type: Option) -> Self { self.property_type = property_type; @@ -2057,6 +2082,11 @@ impl EvalClassProperty { &self.name } + /// Returns the trait that originally declared this imported property, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + /// Returns attributes declared directly on this class property. pub fn attributes(&self) -> &[EvalAttribute] { &self.attributes diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 1fe23f3c68..465f05f51e 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -99,6 +99,7 @@ struct EvalReflectionMemberMetadata { type_metadata: Option, return_type_metadata: Option, default_value: Option, + default_value_trait_origin: Option, required_parameter_count: usize, parameters: Vec, } @@ -967,7 +968,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_default_properties_resul continue; }; let key = values.string(&name)?; - let value = eval_method_parameter_default(default, context, values)?; + let value = eval_reflection_member_default_value(&member, default, context, values)?; result = values.array_set(result, key, value)?; } Ok(Some(result)) @@ -2597,6 +2598,7 @@ fn eval_reflection_class_constant_object_result( &[], None, None, + None, flags, modifiers, 0, @@ -2753,6 +2755,7 @@ fn eval_reflection_class_owner_object_result( &[], None, None, + None, flags, modifiers, 0, @@ -2775,6 +2778,7 @@ fn eval_reflection_class_owner_object_result( &[], None, None, + None, metadata.flags, metadata.modifiers, 0, @@ -2845,6 +2849,7 @@ fn eval_reflection_enum_object_result( &[], None, None, + None, metadata.flags, metadata.modifiers, 0, @@ -3143,6 +3148,7 @@ fn eval_reflection_function_object_result( parameters, None, None, + None, eval_reflection_callable_flags(attributes), required_parameter_count as u64, 0, @@ -3922,6 +3928,7 @@ fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflection type_metadata: None, return_type_metadata: None, default_value: None, + default_value_trait_origin: None, required_parameter_count: 0, parameters: Vec::new(), } @@ -4044,6 +4051,7 @@ fn eval_reflection_aot_method_metadata( type_metadata: None, return_type_metadata, default_value: None, + default_value_trait_origin: None, required_parameter_count, parameters, } @@ -4416,6 +4424,7 @@ fn eval_reflection_aot_property_metadata( type_metadata, return_type_metadata: None, default_value, + default_value_trait_origin: None, required_parameter_count: 0, parameters: Vec::new(), } @@ -4477,6 +4486,7 @@ fn eval_reflection_class_constant_object_result_or_throw( &[], None, None, + None, flags, modifiers, 0, @@ -4649,6 +4659,7 @@ fn eval_reflection_enum_case_object_result( &[], None, None, + None, flags, modifiers, 0, @@ -4700,6 +4711,7 @@ fn eval_reflection_owner_object( parameter_metadata: &[EvalReflectionParameterMetadata], type_metadata: Option<&EvalReflectionParameterTypeMetadata>, default_value: Option<&EvalExpr>, + default_value_trait_origin: Option<&str>, flags: u64, modifiers: u64, method_modifiers: u64, @@ -4720,6 +4732,7 @@ fn eval_reflection_owner_object( parameter_metadata, type_metadata, default_value, + default_value_trait_origin, flags, modifiers, method_modifiers, @@ -4744,6 +4757,7 @@ fn eval_reflection_owner_object_with_members( parameter_metadata: &[EvalReflectionParameterMetadata], type_metadata: Option<&EvalReflectionParameterTypeMetadata>, default_value: Option<&EvalExpr>, + default_value_trait_origin: Option<&str>, flags: u64, modifiers: u64, method_modifiers: u64, @@ -4821,7 +4835,13 @@ fn eval_reflection_owner_object_with_members( } } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { match default_value { - Some(default) => eval_method_parameter_default(default, context, values)?, + Some(default) => eval_reflection_class_like_default_value( + parent_class_name, + default_value_trait_origin, + default, + context, + values, + )?, None => values.null()?, } } else { @@ -4997,6 +5017,7 @@ fn eval_reflection_full_class_object_result( &[], None, None, + None, flags, modifiers, 0, @@ -5018,6 +5039,7 @@ fn eval_reflection_full_class_object_result( &[], None, None, + None, metadata.flags, metadata.modifiers, 0, @@ -5052,6 +5074,7 @@ fn eval_reflection_shallow_class_object_result( &[], None, None, + None, flags, modifiers, 0, @@ -5074,6 +5097,7 @@ fn eval_reflection_shallow_class_object_result( &[], None, None, + None, metadata.flags, metadata.modifiers, 0, @@ -5313,6 +5337,41 @@ fn eval_reflection_parameter_default_value( result } +/// Evaluates one reflected property default with its declaring class-like magic scope. +fn eval_reflection_member_default_value( + member: &EvalReflectionMemberMetadata, + default: &EvalExpr, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_class_like_default_value( + member.declaring_class_name.as_deref(), + member.default_value_trait_origin.as_deref(), + default, + context, + values, + ) +} + +/// Evaluates one class-like default expression with PHP `__CLASS__` and `__TRAIT__`. +fn eval_reflection_class_like_default_value( + declaring_class: Option<&str>, + trait_origin: Option<&str>, + default: &EvalExpr, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(declaring_class) = declaring_class else { + return eval_method_parameter_default(default, context, values); + }; + let trait_name = + trait_origin.or_else(|| context.has_trait(declaring_class).then_some(declaring_class)); + context.push_class_like_member_magic_scope(declaring_class, trait_name); + let result = eval_method_parameter_default(default, context, values); + context.pop_magic_scope(); + result +} + /// Builds a shallow ReflectionMethod object for a parameter's declaring function metadata. fn eval_reflection_declaring_function_object_result( metadata: &EvalReflectionDeclaringFunctionMetadata, @@ -5341,6 +5400,7 @@ fn eval_reflection_declaring_function_object_result( &[], None, None, + None, metadata.flags, metadata.required_parameter_count as u64, method_modifiers, @@ -5622,6 +5682,7 @@ fn eval_reflection_member_object_result( &member.parameters, member.type_metadata.as_ref(), member.default_value.as_ref(), + member.default_value_trait_origin.as_deref(), flags, owner_modifiers, method_modifiers, @@ -6979,6 +7040,7 @@ fn eval_reflection_method_metadata( type_metadata: None, return_type_metadata, default_value: None, + default_value_trait_origin: None, required_parameter_count, parameters, }); @@ -7045,6 +7107,7 @@ fn eval_reflection_method_metadata( type_metadata: None, return_type_metadata, default_value: None, + default_value_trait_origin: None, required_parameter_count, parameters, } @@ -7112,6 +7175,7 @@ fn eval_reflection_method_metadata( type_metadata: None, return_type_metadata, default_value: None, + default_value_trait_origin: None, required_parameter_count, parameters, } @@ -7199,6 +7263,7 @@ fn eval_reflection_enum_synthetic_method_metadata( type_metadata: None, return_type_metadata, default_value: None, + default_value_trait_origin: None, required_parameter_count, parameters, }) @@ -7239,6 +7304,7 @@ fn eval_reflection_property_metadata( .and_then(eval_reflection_parameter_type_metadata), return_type_metadata: None, default_value, + default_value_trait_origin: property.trait_origin().map(str::to_string), required_parameter_count: 0, parameters: Vec::new(), } @@ -7275,6 +7341,7 @@ fn eval_reflection_property_metadata( .and_then(eval_reflection_parameter_type_metadata), return_type_metadata: None, default_value: None, + default_value_trait_origin: None, required_parameter_count: 0, parameters: Vec::new(), }); @@ -7311,6 +7378,10 @@ fn eval_reflection_property_metadata( .and_then(eval_reflection_parameter_type_metadata), return_type_metadata: None, default_value, + default_value_trait_origin: property + .trait_origin() + .map(str::to_string) + .or_else(|| Some(trait_decl.name().to_string())), required_parameter_count: 0, parameters: Vec::new(), } @@ -7438,7 +7509,7 @@ fn eval_reflection_static_property_value( return member .default_value .as_ref() - .map(|default| eval_method_parameter_default(default, context, values)) + .map(|default| eval_reflection_member_default_value(&member, default, context, values)) .transpose(); } let declaring_class = member @@ -7713,6 +7784,7 @@ fn eval_reflection_property_hook_method_metadata( type_metadata: None, return_type_metadata: eval_reflection_property_hook_return_type(property, hook), default_value: None, + default_value_trait_origin: None, required_parameter_count, parameters, } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index a5c0eb7ad2..b8921893cd 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1754,7 +1754,14 @@ fn initialize_eval_declared_constants( values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { for constant in constants { - let value = eval_expr(constant.value(), context, scope, values)?; + let value = eval_class_like_member_default( + owner_name, + constant.trait_origin(), + constant.value(), + context, + scope, + values, + )?; if let Some(replaced) = context.set_class_constant_cell(owner_name, constant.name(), value) { values.release(replaced)?; @@ -1763,6 +1770,22 @@ fn initialize_eval_declared_constants( Ok(()) } +/// Evaluates a class-like constant or property initializer with PHP magic scope. +fn eval_class_like_member_default( + owner_name: &str, + trait_origin: Option<&str>, + default: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let trait_name = trait_origin.or_else(|| context.has_trait(owner_name).then_some(owner_name)); + context.push_class_like_member_magic_scope(owner_name, trait_name); + let result = eval_expr(default, context, scope, values); + context.pop_magic_scope(); + result +} + /// Initializes static property cells for a newly declared eval class. fn initialize_eval_static_properties( class: &EvalClass, @@ -1776,7 +1799,14 @@ fn initialize_eval_static_properties( .filter(|property| property.is_static()) { let value = if let Some(default) = property.default() { - Some(eval_expr(default, context, scope, values)?) + Some(eval_class_like_member_default( + class.name(), + property.trait_origin(), + default, + context, + scope, + values, + )?) } else if property.property_type().is_none() { Some(values.null()?) } else { @@ -2194,8 +2224,11 @@ fn append_eval_trait_constants( validate_eval_trait_constant_compatibility(existing, constant)?; continue; } + let constant = constant + .clone() + .with_trait_origin(trait_decl.name().to_string()); trait_constants.insert(constant.name().to_string(), constant.clone()); - constants.push(constant.clone()); + constants.push(constant); } Ok(()) } @@ -2243,8 +2276,11 @@ fn append_eval_trait_properties( } continue; } + let property = property + .clone() + .with_trait_origin(trait_decl.name().to_string()); trait_properties.insert(property.name().to_string(), property.clone()); - properties.push(property.clone()); + properties.push(property); } Ok(()) } @@ -6417,7 +6453,14 @@ fn eval_dynamic_class_allocate_object( .filter(|property| !property.is_static() && !property.is_abstract()) { let value = if let Some(default) = property.default() { - Some(eval_expr(default, context, caller_scope, values)?) + Some(eval_class_like_member_default( + class.name(), + property.trait_origin(), + default, + context, + caller_scope, + values, + )?) } else if property.property_type().is_none() { Some(values.null()?) } else { diff --git a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs index 3b80914389..6a5058e31e 100644 --- a/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs +++ b/crates/elephc-magician/src/interpreter/tests/trait_adaptations.rs @@ -193,6 +193,74 @@ return EvalMagicBox::aliasStat();"#, ); } +/// Verifies trait-imported constants and property defaults use PHP magic scopes. +#[test] +fn execute_program_resolves_eval_trait_member_default_magic_constants() { + let program = parse_fragment( + br#"namespace EvalMagicDefaultNs; +trait EvalMagicDefaultInner { + public const C = __CLASS__; + public const T = __TRAIT__; + public string $p = __CLASS__; + public string $pt = __TRAIT__; + public static string $sp = __CLASS__; + public static string $st = __TRAIT__; +} +trait EvalMagicDefaultOuter { + use EvalMagicDefaultInner; +} +class EvalMagicDefaultBase { + use EvalMagicDefaultOuter; +} +class EvalMagicDefaultChild extends EvalMagicDefaultBase {} +class EvalMagicDirect { + public const C = __CLASS__; + public const T = __TRAIT__; + public string $p = __CLASS__; + public string $pt = __TRAIT__; + public static string $sp = __CLASS__; + public static string $st = __TRAIT__; +} +$object = new EvalMagicDefaultChild(); +$traitProps = (new \ReflectionClass(EvalMagicDefaultInner::class))->getDefaultProperties(); +echo EvalMagicDefaultBase::C . "|" . EvalMagicDefaultBase::T . "|"; +echo EvalMagicDefaultChild::C . "|" . EvalMagicDefaultChild::T . "|"; +echo $object->p . "|" . $object->pt . "|"; +echo EvalMagicDefaultChild::$sp . "|" . EvalMagicDefaultChild::$st . "|"; +echo $traitProps["p"] . "|" . $traitProps["pt"] . ":"; +$direct = new EvalMagicDirect(); +return EvalMagicDirect::C . "|" . EvalMagicDirect::T . "|" . $direct->p . "|" . $direct->pt . "|" . EvalMagicDirect::$sp . "|" . EvalMagicDirect::$st;"#, + ) + .expect("parse eval trait member magic fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let expected_trait = concat!( + "EvalMagicDefaultNs\\EvalMagicDefaultBase|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultBase|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultBase|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultBase|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner|", + "EvalMagicDefaultNs\\EvalMagicDefaultInner:" + ); + let expected_direct = concat!( + "EvalMagicDefaultNs\\EvalMagicDirect||", + "EvalMagicDefaultNs\\EvalMagicDirect||", + "EvalMagicDefaultNs\\EvalMagicDirect|" + ); + + assert_eq!(values.output, expected_trait); + assert_eq!( + values.get(result), + FakeValue::String(expected_direct.to_string()) + ); +} + /// Verifies trait-to-trait composition rejects missing inner traits. #[test] fn execute_program_rejects_missing_eval_trait_used_by_trait() { diff --git a/docs/php/eval.md b/docs/php/eval.md index daeeb8229b..5adefcc8a5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -78,7 +78,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | -| Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty top-level eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, eval-declared-function `__FUNCTION__` / `__METHOD__`, and eval-declared-method `__FUNCTION__` / `__METHOD__` / `__CLASS__` / `__TRAIT__`, including trait-origin names for imported trait methods. | +| Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty top-level eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, eval-declared-function `__FUNCTION__` / `__METHOD__`, eval-declared-method `__FUNCTION__` / `__METHOD__` / `__CLASS__` / `__TRAIT__`, and eval class-like constant/property initializers for `__CLASS__` / `__TRAIT__`, including trait-origin names for imported trait members. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | | Ternaries | Full ternary and short ternary (`?:`). | | Match | Strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default`. A miss without `default` is reported as an eval runtime fatal. | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e323667612..108797651d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4635,6 +4635,60 @@ echo Box::aliasStat();'); ); } +/// Verifies eval trait member defaults expose PHP magic constants through the bridge. +#[test] +fn test_eval_trait_member_default_magic_constants_execute_through_bridge() { + let out = compile_and_run( + r#"getDefaultProperties(); +echo Base::C . "|" . Base::T . "|"; +echo Child::C . "|" . Child::T . "|"; +echo $object->p . "|" . $object->pt . "|"; +echo Child::$sp . "|" . Child::$st . "|"; +echo $traitProps["p"] . "|" . $traitProps["pt"] . ":"; +$direct = new Direct(); +echo Direct::C . "|" . Direct::T . "|" . $direct->p . "|" . $direct->pt . "|" . Direct::$sp . "|" . Direct::$st;'); +"#, + ); + let expected = concat!( + "EvalBridgeDefaultMagic\\Base|EvalBridgeDefaultMagic\\Inner|", + "EvalBridgeDefaultMagic\\Base|EvalBridgeDefaultMagic\\Inner|", + "EvalBridgeDefaultMagic\\Base|EvalBridgeDefaultMagic\\Inner|", + "EvalBridgeDefaultMagic\\Base|EvalBridgeDefaultMagic\\Inner|", + "EvalBridgeDefaultMagic\\Inner|EvalBridgeDefaultMagic\\Inner:", + "EvalBridgeDefaultMagic\\Direct||", + "EvalBridgeDefaultMagic\\Direct||", + "EvalBridgeDefaultMagic\\Direct|" + ); + + assert_eq!(out, expected); +} + /// Verifies eval-declared functions persist across eval calls in the same generated context. #[test] fn test_eval_declared_function_persists_across_eval_calls() { From 0931e7ed8f3a68d53aaee04b251dad91b3b6dc2d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 20:17:37 +0200 Subject: [PATCH 0789/1208] Support eval reflected parameter magic defaults --- crates/elephc-magician/src/context.rs | 20 ++++ .../src/interpreter/reflection.rs | 105 ++++++++++++++++-- .../tests/builtins_class_metadata.rs | 60 ++++++++++ docs/php/eval.md | 8 +- tests/codegen/eval.rs | 59 ++++++++++ 5 files changed, 242 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 5672f7764b..3fc01b8b7b 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -2807,6 +2807,26 @@ impl ElephcEvalContext { }); } + /// Pushes PHP-visible callable magic constants for reflected parameter defaults. + pub fn push_callable_magic_scope( + &mut self, + function_name: &str, + method_name: &str, + class_name: Option<&str>, + trait_name: Option<&str>, + ) { + self.magic_stack.push(EvalMagicScope { + function_name: function_name.to_string(), + method_name: method_name.to_string(), + class_name: class_name + .map(|class_name| class_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + trait_name: trait_name + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + /// Pops the current PHP-visible eval magic-constant scope. pub fn pop_magic_scope(&mut self) { self.magic_stack.pop(); diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 465f05f51e..af14a4786b 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -124,11 +124,21 @@ struct EvalReflectionParameterMetadata { default_value_constant_name: Option, } +/// PHP-visible magic constant scope for one reflected parameter default. +#[derive(Clone)] +struct EvalReflectionParameterMagicScope { + function_name: String, + method_name: String, + class_name: Option, + trait_name: Option, +} + /// Eval metadata needed for `ReflectionParameter::getDeclaringFunction()`. #[derive(Clone)] struct EvalReflectionDeclaringFunctionMetadata { name: String, declaring_class_name: Option, + magic_scope: Option, attributes: Vec, flags: u64, required_parameter_count: usize, @@ -4113,6 +4123,7 @@ fn eval_reflection_native_callable_parameters( let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method_name.to_ascii_lowercase(), declaring_class_name: Some(declaring_class_name.trim_start_matches('\\').to_string()), + magic_scope: None, attributes: Vec::new(), flags, required_parameter_count: signature.required_param_count(), @@ -5317,7 +5328,7 @@ fn eval_reflection_parameter_class_name( } } -/// Materializes one ReflectionParameter default using the declaring class scope when present. +/// Materializes one ReflectionParameter default using declaring class and magic scopes. fn eval_reflection_parameter_default_value( parameter: &EvalReflectionParameterMetadata, context: &mut ElephcEvalContext, @@ -5326,14 +5337,30 @@ fn eval_reflection_parameter_default_value( let Some(default) = parameter.default_value.as_ref() else { return values.null(); }; - let Some(class_name) = parameter.declaring_class_name.as_deref() else { - return eval_method_parameter_default(default, context, values); - }; - context.push_class_scope(class_name.to_string()); - context.push_called_class_scope(class_name.to_string()); + if let Some(class_name) = parameter.declaring_class_name.as_deref() { + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(class_name.to_string()); + } + let magic_scope = parameter + .declaring_function + .as_ref() + .and_then(|function| function.magic_scope.as_ref()); + if let Some(magic_scope) = magic_scope { + context.push_callable_magic_scope( + &magic_scope.function_name, + &magic_scope.method_name, + magic_scope.class_name.as_deref(), + magic_scope.trait_name.as_deref(), + ); + } let result = eval_method_parameter_default(default, context, values); - context.pop_called_class_scope(); - context.pop_class_scope(); + if magic_scope.is_some() { + context.pop_magic_scope(); + } + if parameter.declaring_class_name.is_some() { + context.pop_called_class_scope(); + context.pop_class_scope(); + } result } @@ -6999,6 +7026,11 @@ fn eval_reflection_method_metadata( let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), declaring_class_name: Some(declaring_class.clone()), + magic_scope: Some(eval_reflection_eval_method_parameter_magic_scope( + &declaring_class, + &method, + None, + )), attributes: method.attributes().to_vec(), flags, required_parameter_count, @@ -7071,6 +7103,12 @@ fn eval_reflection_method_metadata( let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), declaring_class_name: Some(class_name.to_string()), + magic_scope: Some(eval_reflection_method_parameter_magic_scope( + class_name, + method.name(), + &format!("{}::{}", class_name.trim_start_matches('\\'), method.name()), + None, + )), attributes: method.attributes().to_vec(), flags, required_parameter_count, @@ -7137,6 +7175,11 @@ fn eval_reflection_method_metadata( let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: method.name().to_string(), declaring_class_name: Some(trait_decl.name().to_string()), + magic_scope: Some(eval_reflection_eval_method_parameter_magic_scope( + trait_decl.name(), + method, + Some(trait_decl.name()), + )), attributes: method.attributes().to_vec(), flags, required_parameter_count, @@ -7232,6 +7275,7 @@ fn eval_reflection_enum_synthetic_method_metadata( let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: synthetic_name.to_string(), declaring_class_name: Some(declaring_class_name.clone()), + magic_scope: None, attributes: Vec::new(), flags, required_parameter_count, @@ -7809,6 +7853,7 @@ fn eval_reflection_property_hook_parameters( let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: hook.reflected_method_name(property.name()), declaring_class_name: Some(declaring_class.to_string()), + magic_scope: None, attributes: Vec::new(), flags: eval_reflection_member_flags(property.visibility(), false, false, false, false), required_parameter_count: 1, @@ -8516,6 +8561,49 @@ fn eval_reflection_required_parameter_count( .map_or(0, |position| position + 1) } +/// Builds PHP magic scope metadata for a reflected function parameter default. +fn eval_reflection_function_parameter_magic_scope( + function_name: &str, +) -> EvalReflectionParameterMagicScope { + EvalReflectionParameterMagicScope { + function_name: function_name.to_string(), + method_name: function_name.to_string(), + class_name: None, + trait_name: None, + } +} + +/// Builds PHP magic scope metadata for a reflected method parameter default. +fn eval_reflection_method_parameter_magic_scope( + class_name: &str, + function_name: &str, + method_name: &str, + trait_name: Option<&str>, +) -> EvalReflectionParameterMagicScope { + EvalReflectionParameterMagicScope { + function_name: function_name.to_string(), + method_name: method_name.to_string(), + class_name: Some(class_name.trim_start_matches('\\').to_string()), + trait_name: trait_name.map(|trait_name| trait_name.trim_start_matches('\\').to_string()), + } +} + +/// Builds PHP magic scope metadata for an eval method's reflected parameter default. +fn eval_reflection_eval_method_parameter_magic_scope( + class_name: &str, + method: &EvalClassMethod, + fallback_trait_name: Option<&str>, +) -> EvalReflectionParameterMagicScope { + eval_reflection_method_parameter_magic_scope( + class_name, + method.magic_function_name(), + &method.magic_method_name(class_name), + method + .trait_origin() + .or(fallback_trait_name), + ) +} + /// Builds parameter reflection metadata from eval parameter names and type flags. fn eval_reflection_parameters_from_names_and_type_flags( declaring_class_name: Option<&str>, @@ -8624,6 +8712,7 @@ fn eval_reflection_function_parameters( let declaring_function = EvalReflectionDeclaringFunctionMetadata { name: function_name.to_string(), declaring_class_name: None, + magic_scope: Some(eval_reflection_function_parameter_magic_scope(function_name)), attributes: function_attributes, flags, required_parameter_count: eval_reflection_required_parameter_count( diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index fe4d97d4fd..de10eafdba 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -2476,6 +2476,66 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionParameter default magic constants use declaring callable scopes. +#[test] +fn execute_program_reflection_parameter_resolves_default_magic_constants() { + let program = parse_fragment( + br##"namespace EvalReflectParamMagicNs; +function eval_reflect_param_magic($fn = __FUNCTION__, $m = __METHOD__, $c = __CLASS__, $t = __TRAIT__, $n = __NAMESPACE__) {} +interface EvalReflectParamMagicIface { + public function read($c = __CLASS__, $m = __METHOD__, $fn = __FUNCTION__, $t = __TRAIT__, $n = __NAMESPACE__); +} +trait EvalReflectParamMagicTrait { + public function source($c = __CLASS__, $t = __TRAIT__, $m = __METHOD__, $fn = __FUNCTION__, $n = __NAMESPACE__) {} +} +class EvalReflectParamMagicBox { + use EvalReflectParamMagicTrait { source as aliasSource; } + public function own($c = __CLASS__, $t = __TRAIT__, $m = __METHOD__, $fn = __FUNCTION__, $n = __NAMESPACE__) {} +} +function eval_param_magic_dump($ref) { + foreach ($ref->getParameters() as $param) { + echo "[" . $param->getDefaultValue() . "]"; + } + echo ":"; +} +eval_param_magic_dump(new \ReflectionFunction(__NAMESPACE__ . "\\eval_reflect_param_magic")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "own")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "aliasSource")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicIface::class, "read")); +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[][][EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox::own]", + "[own]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait::source]", + "[source]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface::read]", + "[read]", + "[]", + "[EvalReflectParamMagicNs]:" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionClass exposes eval property default metadata as an associative map. #[test] fn execute_program_reflection_class_get_default_properties_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 5adefcc8a5..93bff3d782 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -78,7 +78,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | -| Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty top-level eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, eval-declared-function `__FUNCTION__` / `__METHOD__`, eval-declared-method `__FUNCTION__` / `__METHOD__` / `__CLASS__` / `__TRAIT__`, and eval class-like constant/property initializers for `__CLASS__` / `__TRAIT__`, including trait-origin names for imported trait members. | +| Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty top-level eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, eval-declared-function `__FUNCTION__` / `__METHOD__`, eval-declared-method `__FUNCTION__` / `__METHOD__` / `__CLASS__` / `__TRAIT__`, eval class-like constant/property initializers for `__CLASS__` / `__TRAIT__`, and reflected parameter defaults using the declaring callable scope, including trait-origin names for imported trait members. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | | Ternaries | Full ternary and short ternary (`?:`). | | Match | Strict pattern comparison, comma-separated patterns, lazy result-arm evaluation, and `default`. A miss without `default` is reported as an eval runtime fatal. | @@ -481,7 +481,11 @@ ternary and null-coalescing expressions, predefined or eval-defined constant fetches, namespaced constant fallback, class/interface/trait/enum constant fetches, `self::class` / `parent::class` / named class-like `::class` literals, and `new ClassName(...)` / `new self(...)` / `new parent(...)` with supported -non-spread constructor arguments. Late-bound `static::` defaults and unpacked +non-spread constructor arguments. Magic constants in reflected parameter +defaults are evaluated in the declaring function or method scope; methods +imported from traits retain PHP's trait-origin `__TRAIT__`, `__METHOD__`, and +`__FUNCTION__` values while `__CLASS__` follows the using class. Late-bound +`static::` defaults and unpacked constructor arguments in defaults are rejected like PHP constant expressions. Variadic eval method parameters bind extra positional and unknown named arguments into a PHP array and are reported through diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 108797651d..73ce37737b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -15139,6 +15139,65 @@ foreach ($params as $param) { ); } +/// Verifies eval ReflectionParameter default magic constants use callable scopes through the bridge. +#[test] +fn test_eval_reflection_parameter_resolves_default_magic_constants() { + let out = compile_and_run_capture( + r#"getParameters() as $param) { + echo "[" . $param->getDefaultValue() . "]"; + } + echo ":"; +} +eval_param_magic_dump(new \ReflectionFunction(__NAMESPACE__ . "\\\\eval_reflect_param_magic")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "own")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "aliasSource")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicIface::class, "read"));'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + concat!( + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[][][EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox::own]", + "[own]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait::source]", + "[source]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface::read]", + "[read]", + "[]", + "[EvalReflectParamMagicNs]:" + ) + ); +} + /// Verifies eval ReflectionMethod exposes eval-declared return type metadata. #[test] fn test_eval_reflection_method_reports_return_type_metadata() { From 758f1444e5296dd1cf540b2187a8419a240affd4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 20:25:47 +0200 Subject: [PATCH 0790/1208] Support eval legacy var properties --- .../src/interpreter/tests/classes.rs | 31 +++++++++++++ .../elephc-magician/src/parser/statements.rs | 25 +++++++++++ .../src/parser/tests/classes_errors.rs | 45 +++++++++++++++++++ docs/php/eval.md | 5 ++- tests/codegen/eval.rs | 25 +++++++++++ 5 files changed, 129 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 6c5c50a2eb..5bcb8e57f1 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -73,6 +73,37 @@ return $self instanceof EvalRelativeFactoryBase assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies PHP legacy `var` properties behave as public eval properties. +#[test] +fn execute_program_supports_legacy_var_properties() { + let program = parse_fragment( + br#"class EvalLegacyVarProperty { + var $plain = "p"; + var ?int $count = null; +} +$object = new EvalLegacyVarProperty(); +$plain = new ReflectionProperty("EvalLegacyVarProperty", "plain"); +$count = new ReflectionProperty("EvalLegacyVarProperty", "count"); +$defaults = (new ReflectionClass("EvalLegacyVarProperty"))->getDefaultProperties(); +echo $object->plain; echo ":"; +echo $plain->isPublic() ? "P" : "p"; echo ":"; +echo $plain->hasType() ? "T" : "t"; echo ":"; +echo $count->isPublic() ? "C" : "c"; echo ":"; +echo $count->hasType() ? $count->getType()->getName() : "none"; echo ":"; +echo $count->getType()->allowsNull() ? "N" : "n"; echo ":"; +echo is_null($defaults["count"]) ? "null" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "p:P:t:C:int:N:null"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval static method calls preserve PHP late-static forwarding rules. #[test] fn execute_program_forwards_eval_static_method_called_class() { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 541ecf9afe..918c7ee04e 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -698,6 +698,31 @@ impl Parser { return Ok(()); } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { + if visibility.is_some() + || is_static + || is_abstract + || is_final + || is_readonly + || set_visibility.is_some() + { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let (property, mut hook_methods) = self.parse_class_property_decl( + EvalVisibility::Public, + None, + false, + false, + false, + is_readonly_class, + false, + )?; + properties.push(property.with_attributes(attributes)); + methods.append(&mut hook_methods); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { if is_static || is_abstract || is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index d77fc5f27c..fb1dafabc7 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -268,6 +268,51 @@ fn parse_fragment_accepts_class_member_attribute_metadata() { ); } +/// Verifies PHP's legacy `var` property marker parses as public property syntax. +#[test] +fn parse_fragment_accepts_legacy_var_properties() { + let program = parse_fragment( + br#"class DynEvalVarProps { + var $plain = "p"; + var ?int $count = null; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalVarProps", + vec![ + EvalClassProperty::new( + "plain", + Some(EvalExpr::Const(EvalConst::String("p".to_string()))) + ), + EvalClassProperty::new("count", Some(EvalExpr::Const(EvalConst::Null))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + true + ))) + ], + Vec::new(), + ))] + ); +} + +/// Verifies legacy `var` cannot be combined with other property modifiers. +#[test] +fn parse_fragment_rejects_legacy_var_modifier_combinations() { + for source in [ + b"class DynEvalBadPublicVar { public var $value; }" as &[u8], + b"class DynEvalBadStaticVar { static var $value; }", + b"class DynEvalBadReadonlyVar { readonly var $value; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + /// Verifies interface, trait, and enum member attributes are retained. #[test] fn parse_fragment_accepts_class_like_member_attribute_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 93bff3d782..8437ad338a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes with properties including PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -148,7 +148,8 @@ containers to eval-declared functions. ## Classes and objects -Eval-declared classes support inheritance, public/protected/private properties +Eval-declared classes support inheritance, public/protected/private properties, +PHP's legacy `var` marker for public properties, and methods, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 73ce37737b..3a8dde44dd 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7304,6 +7304,31 @@ echo get_class($parent); echo ":"; echo $parent->label;'); ); } +/// Verifies eval supports PHP's legacy `var` public property marker through the bridge. +#[test] +fn test_eval_declared_legacy_var_properties() { + let out = compile_and_run( + r#"getDefaultProperties(); +echo $object->plain; echo ":"; +echo $plain->isPublic() ? "P" : "p"; echo ":"; +echo $plain->hasType() ? "T" : "t"; echo ":"; +echo $count->isPublic() ? "C" : "c"; echo ":"; +echo $count->hasType() ? $count->getType()->getName() : "none"; echo ":"; +echo $count->getType()->allowsNull() ? "N" : "n"; echo ":"; +echo is_null($defaults["count"]) ? "null" : "bad";'); +"#, + ); + assert_eq!(out, "p:P:t:C:int:N:null"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From 119a26458191c64bbce1e2471e07d77345c92f9e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 20:31:45 +0200 Subject: [PATCH 0791/1208] Support eval trait legacy var properties --- .../src/interpreter/tests/classes.rs | 14 +++- .../elephc-magician/src/parser/statements.rs | 71 ++++++++++++++----- .../src/parser/tests/classes_errors.rs | 25 +++++++ docs/php/eval.md | 7 +- tests/codegen/eval.rs | 14 +++- 5 files changed, 103 insertions(+), 28 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 5bcb8e57f1..75b48a6d4b 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -77,13 +77,18 @@ return $self instanceof EvalRelativeFactoryBase #[test] fn execute_program_supports_legacy_var_properties() { let program = parse_fragment( - br#"class EvalLegacyVarProperty { + br#"trait EvalLegacyVarTrait { + var ?string $label = "trait"; +} +class EvalLegacyVarProperty { + use EvalLegacyVarTrait; var $plain = "p"; var ?int $count = null; } $object = new EvalLegacyVarProperty(); $plain = new ReflectionProperty("EvalLegacyVarProperty", "plain"); $count = new ReflectionProperty("EvalLegacyVarProperty", "count"); +$label = new ReflectionProperty("EvalLegacyVarProperty", "label"); $defaults = (new ReflectionClass("EvalLegacyVarProperty"))->getDefaultProperties(); echo $object->plain; echo ":"; echo $plain->isPublic() ? "P" : "p"; echo ":"; @@ -91,7 +96,10 @@ echo $plain->hasType() ? "T" : "t"; echo ":"; echo $count->isPublic() ? "C" : "c"; echo ":"; echo $count->hasType() ? $count->getType()->getName() : "none"; echo ":"; echo $count->getType()->allowsNull() ? "N" : "n"; echo ":"; -echo is_null($defaults["count"]) ? "null" : "bad"; +echo is_null($defaults["count"]) ? "null" : "bad"; echo ":"; +echo $object->label; echo ":"; +echo $label->isPublic() ? "L" : "l"; echo ":"; +echo $label->getType()->getName(); return true;"#, ) .expect("parse eval fragment"); @@ -100,7 +108,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "p:P:t:C:int:N:null"); + assert_eq!(values.output, "p:P:t:C:int:N:null:trait:L:string"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 918c7ee04e..022e8a6b82 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -699,27 +699,18 @@ impl Parser { } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { - if visibility.is_some() - || is_static - || is_abstract - || is_final - || is_readonly - || set_visibility.is_some() - { - return Err(EvalParseError::UnsupportedConstruct); - } - self.advance(); - let (property, mut hook_methods) = self.parse_class_property_decl( - EvalVisibility::Public, - None, - false, - false, - false, + self.parse_legacy_var_property_member( + attributes, + visibility.is_some() + || is_static + || is_abstract + || is_final + || is_readonly + || set_visibility.is_some(), is_readonly_class, - false, + properties, + methods, )?; - properties.push(property.with_attributes(attributes)); - methods.append(&mut hook_methods); return Ok(()); } @@ -767,6 +758,33 @@ impl Parser { Ok(()) } + /// Parses PHP's legacy `var` public-property marker after the keyword is recognized. + fn parse_legacy_var_property_member( + &mut self, + attributes: Vec, + has_other_modifiers: bool, + is_readonly_class: bool, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + if has_other_modifiers { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let (property, mut hook_methods) = self.parse_class_property_decl( + EvalVisibility::Public, + None, + false, + false, + false, + is_readonly_class, + false, + )?; + properties.push(property.with_attributes(attributes)); + methods.append(&mut hook_methods); + Ok(()) + } + /// Parses optional attributes that decorate one class-like member. pub(super) fn parse_optional_member_attributes( &mut self, @@ -1428,6 +1446,21 @@ impl Parser { methods.push(method.with_attributes(attributes)); return Ok(()); } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { + self.parse_legacy_var_property_member( + attributes, + visibility.is_some() + || is_static + || is_abstract + || is_final + || is_readonly + || set_visibility.is_some(), + false, + properties, + methods, + )?; + return Ok(()); + } let visibility = visibility.unwrap_or(EvalVisibility::Public); let (property, mut hook_methods) = self.parse_class_property_decl( visibility, diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index fb1dafabc7..de3216fc45 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -298,6 +298,31 @@ fn parse_fragment_accepts_legacy_var_properties() { ); } +/// Verifies legacy `var` property syntax also works inside eval traits. +#[test] +fn parse_fragment_accepts_legacy_var_trait_properties() { + let program = parse_fragment( + br#"trait DynEvalVarTrait { + var ?int $count = null; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalVarTrait", + vec![ + EvalClassProperty::new("count", Some(EvalExpr::Const(EvalConst::Null))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + true + ))) + ], + Vec::new(), + ))] + ); +} + /// Verifies legacy `var` cannot be combined with other property modifiers. #[test] fn parse_fragment_rejects_legacy_var_modifier_combinations() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 8437ad338a..8fda61eea7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes with properties including PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | +| Classes | Eval fragments can declare classes and traits with properties including PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -149,8 +149,8 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties, -PHP's legacy `var` marker for public properties, -and methods, asymmetric property write visibility (`private(set)` / +PHP's legacy `var` marker for public properties, and methods, asymmetric +property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, @@ -164,6 +164,7 @@ properties, static methods, static interface method contracts, class, interface, trait, and enum constants including `final` constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and `__callStatic()`, and magic property fallback through `__get()` and `__set()`. +Eval traits also accept the legacy `var` marker for public properties. PHP's global `#[Override]` marker is validated on eval-declared methods against non-private parent methods and eval interface method contracts. Eval validates method override and interface method parameter and return types diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3a8dde44dd..ca590f86a5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7309,13 +7309,18 @@ echo get_class($parent); echo ":"; echo $parent->label;'); fn test_eval_declared_legacy_var_properties() { let out = compile_and_run( r#"getDefaultProperties(); echo $object->plain; echo ":"; echo $plain->isPublic() ? "P" : "p"; echo ":"; @@ -7323,10 +7328,13 @@ echo $plain->hasType() ? "T" : "t"; echo ":"; echo $count->isPublic() ? "C" : "c"; echo ":"; echo $count->hasType() ? $count->getType()->getName() : "none"; echo ":"; echo $count->getType()->allowsNull() ? "N" : "n"; echo ":"; -echo is_null($defaults["count"]) ? "null" : "bad";'); +echo is_null($defaults["count"]) ? "null" : "bad"; echo ":"; +echo $object->label; echo ":"; +echo $label->isPublic() ? "L" : "l"; echo ":"; +echo $label->getType()->getName();'); "#, ); - assert_eq!(out, "p:P:t:C:int:N:null"); + assert_eq!(out, "p:P:t:C:int:N:null:trait:L:string"); } /// Verifies native callable probes can see functions declared by eval after the barrier. From a98b2199ffbc700e2f471dc42d27434f86ee2be6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 20:53:21 +0200 Subject: [PATCH 0792/1208] Support eval get_class_vars builtin --- .../class_metadata/oop_introspection.rs | 187 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 1 + .../builtins/registry/dispatch/symbols.rs | 1 + .../interpreter/builtins/registry/names.rs | 1 + .../src/interpreter/expressions.rs | 1 + .../src/interpreter/statements.rs | 2 +- .../tests/builtins_class_metadata.rs | 68 +++++++ docs/php/eval.md | 11 +- tests/codegen/eval.rs | 67 +++++++ 9 files changed, 333 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 6680815b72..06b061d252 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -8,6 +8,8 @@ //! Key details: //! - `method_exists()` distinguishes object targets from class-string targets //! because PHP exposes inherited private methods only on object targets. +//! - `get_class_vars()` materializes declarative defaults, not current runtime +//! static property state. //! - `get_object_vars()` filters declared storage slots so inaccessible //! protected/private eval properties do not leak as dynamic properties. @@ -86,6 +88,45 @@ pub(in crate::interpreter) fn eval_get_class_methods_result( eval_indexed_string_array_result(&names, values) } +/// Evaluates `get_class_vars()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_class_vars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + eval_get_class_vars_result(&[target], context, values) +} + +/// Evaluates materialized `get_class_vars()` arguments. +pub(in crate::interpreter) fn eval_get_class_vars_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let class_name = eval_resolved_class_metadata_name(*target, context, values)?; + if context.has_class(&class_name) || context.has_enum(&class_name) { + return eval_dynamic_class_vars_result(&class_name, context, values); + } + if context.has_trait(&class_name) { + return eval_dynamic_trait_vars_result(&class_name, context, values); + } + if context.has_interface(&class_name) { + return values.assoc_new(0); + } + if eval_class_relation_name_exists(&class_name, context, values)? { + return eval_runtime_class_vars_result(&class_name, context, values); + } + Err(EvalStatus::RuntimeFatal) +} + /// Evaluates `get_object_vars()` from eval expressions. pub(in crate::interpreter) fn eval_builtin_get_object_vars( args: &[EvalExpr], @@ -365,6 +406,152 @@ fn eval_public_runtime_method_names( Ok(result) } +/// Builds `get_class_vars()` for an eval-declared class or enum. +fn eval_dynamic_class_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(0)?; + let mut emitted_keys = HashSet::new(); + if let Some(enum_decl) = context.enum_decl(class_name) { + let name_value = values.null()?; + result = eval_add_class_var_entry(result, "name", name_value, values)?; + emitted_keys.insert(String::from("name")); + if enum_decl.backing_type().is_some() { + let value_value = values.null()?; + result = eval_add_class_var_entry(result, "value", value_value, values)?; + emitted_keys.insert(String::from("value")); + } + } + for class in context.class_chain(class_name).into_iter().rev() { + for property in class.properties() { + if emitted_keys.contains(property.name()) + || validate_eval_member_access(class.name(), property.visibility(), context) + .is_err() + { + continue; + } + let value = + eval_class_vars_property_default_value(class.name(), property, context, values)?; + result = eval_add_class_var_entry(result, property.name(), value, values)?; + emitted_keys.insert(property.name().to_string()); + } + } + Ok(result) +} + +/// Builds `get_class_vars()` for an eval-declared trait. +fn eval_dynamic_trait_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(trait_decl) = context.trait_decl(class_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + let trait_name = trait_decl.name().to_string(); + let properties = trait_decl.properties().to_vec(); + let mut result = values.assoc_new(properties.len())?; + let mut emitted_keys = HashSet::new(); + for property in properties { + if emitted_keys.contains(property.name()) + || validate_eval_member_access(&trait_name, property.visibility(), context).is_err() + { + continue; + } + let value = + eval_class_vars_property_default_value(&trait_name, &property, context, values)?; + result = eval_add_class_var_entry(result, property.name(), value, values)?; + emitted_keys.insert(property.name().to_string()); + } + Ok(result) +} + +/// Builds `get_class_vars()` data for generated/AOT class metadata. +fn eval_runtime_class_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_names = values.reflection_property_names(class_name)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + let mut result = values.assoc_new(declared_names.len())?; + let mut emitted_keys = HashSet::new(); + for property_name in declared_names { + if emitted_keys.contains(&property_name) { + continue; + } + let Some((declaring_class, visibility, _is_static)) = + eval_runtime_property_access_metadata(class_name, &property_name, values)? + else { + continue; + }; + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + continue; + } + let value = eval_runtime_class_var_default_value( + class_name, + &declaring_class, + &property_name, + context, + values, + )?; + result = eval_add_class_var_entry(result, &property_name, value, values)?; + emitted_keys.insert(property_name); + } + Ok(result) +} + +/// Materializes one eval-declared property default for `get_class_vars()`. +fn eval_class_vars_property_default_value( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(default) = property.default() else { + return values.null(); + }; + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + context.push_class_like_member_magic_scope(declaring_class, property.trait_origin()); + let result = eval_method_parameter_default(default, context, values); + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + result +} + +/// Materializes one generated/AOT property default for `get_class_vars()`. +fn eval_runtime_class_var_default_value( + runtime_class: &str, + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(default) = context + .native_property_default(declaring_class, property_name) + .or_else(|| context.native_property_default(runtime_class, property_name)) + { + return materialize_native_callable_default(&default, context, values); + } + values.null() +} + +/// Adds one string-keyed class variable value to an associative result array. +fn eval_add_class_var_entry( + result: RuntimeCellHandle, + property_name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(property_name)?; + values.array_set(result, key, value) +} + /// Builds `get_object_vars()` for an eval-declared object. fn eval_dynamic_object_vars_result( object: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 745836e3ce..e52b35d630 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -140,6 +140,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "get_called_class" => Some(&[]), "get_class" => Some(&["object"]), "get_class_methods" => Some(&["object_or_class"]), + "get_class_vars" => Some(&["class"]), "get_object_vars" => Some(&["object"]), "get_parent_class" => Some(&["object_or_class"]), "call_user_func" => Some(&["callback"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs index c79edf19ce..67db18b8c6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -62,6 +62,7 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( eval_member_exists_result(name, evaluated_args, context, values)? } "get_class_methods" => eval_get_class_methods_result(evaluated_args, context, values)?, + "get_class_vars" => eval_get_class_vars_result(evaluated_args, context, values)?, "get_object_vars" => eval_get_object_vars_result(evaluated_args, context, values)?, "enum_exists" | "trait_exists" => { eval_class_like_exists_result(name, evaluated_args, context, values)? diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 20c2fe7f4a..263760eedc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -154,6 +154,7 @@ pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> boo | "get_called_class" | "get_class" | "get_class_methods" + | "get_class_vars" | "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 7cb70ed51d..03ea80eb8c 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -924,6 +924,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_member_exists(name, args, context, scope, values) } "get_class_methods" => eval_builtin_get_class_methods(args, context, scope, values), + "get_class_vars" => eval_builtin_get_class_vars(args, context, scope, values), "get_object_vars" => eval_builtin_get_object_vars(args, context, scope, values), "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), "trait_exists" | "enum_exists" => { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b8921893cd..ecf264423c 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -8683,7 +8683,7 @@ fn eval_visibility_label(visibility: EvalVisibility) -> &'static str { } /// Allocates a fresh runtime cell for one invocation-safe native AOT default. -fn materialize_native_callable_default( +pub(in crate::interpreter) fn materialize_native_callable_default( default: &NativeCallableDefault, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index de10eafdba..9bb47c0f3f 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -142,6 +142,74 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies `get_class_vars()` materializes visible defaults for eval class-like metadata. +#[test] +fn execute_program_dispatches_get_class_vars_builtin() { + let program = parse_fragment( + br#"trait EvalClassVarsTrait { + public $traitPublic = "tp"; + protected $traitProtected = "tq"; +} +enum EvalClassVarsBacked: int { case Ready = 1; } +class EvalClassVarsBase { + public $basePublic = "bp"; + protected $baseProtected = "bq"; + private $basePrivate = "bs"; + public static $baseStatic = "static"; + public int $typed; +} +class EvalClassVarsChild extends EvalClassVarsBase { + use EvalClassVarsTrait; + public $childPublic = "cp"; + protected $childProtected = "cq"; + private $childPrivate = "cs"; + public function childView() { + $vars = get_class_vars(self::class); + ksort($vars); + foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } + public function baseView() { + $vars = get_class_vars(EvalClassVarsBase::class); + ksort($vars); + foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } +} +$outside = get_class_vars("EvalClassVarsChild"); +ksort($outside); +foreach ($outside as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +(new EvalClassVarsChild())->childView(); +echo ":"; +(new EvalClassVarsChild())->baseView(); +echo ":"; +$trait = call_user_func("get_class_vars", "EvalClassVarsTrait"); +ksort($trait); +foreach ($trait as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +$enum = call_user_func_array("get_class_vars", ["class" => "EvalClassVarsBacked"]); +ksort($enum); +foreach ($enum as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +echo function_exists("get_class_vars") ? "F" : "f"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "basePublic=bp|baseStatic=static|childPublic=cp|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|childPrivate=cs|childProtected=cq|childPublic=cp|traitProtected=tq|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|typed=null|:traitPublic=tp|:name=null|value=null|:F" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies class attribute helpers expose eval class-level metadata. #[test] fn execute_program_dispatches_class_attribute_metadata_builtins() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 8fda61eea7..26ffb809c5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -619,10 +619,11 @@ visible inside eval through `enum_exists()` and through class-like probes such as `class_exists()`. `method_exists()` and `property_exists()` inspect eval-declared class/interface/ trait/enum metadata and generated runtime metadata. Object targets also see -dynamic public properties. `get_class_methods()` and `get_object_vars()` follow -PHP visibility from the current eval class scope for eval-declared objects; -generated/AOT objects expose the public bridge-visible metadata and object -properties available through the runtime hook slice. +dynamic public properties. `get_class_methods()`, `get_class_vars()`, and +`get_object_vars()` follow PHP visibility from the current eval class scope for +eval-declared metadata and objects; generated/AOT objects expose the public +bridge-visible metadata and object properties available through the runtime +hook slice. Eval-declared enums share the dynamic class-like metadata path used by eval-declared classes. Pure and backed enum cases are singleton objects, @@ -695,7 +696,7 @@ where listed below unless a note says otherwise. | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | | Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | -| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_called_class()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_resource()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_called_class()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_class_vars()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_resource()` | | Debug output | `print_r()`, `var_dump()` | | Constants | `define()`, `defined()` | diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ca590f86a5..129a3ae8f6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8798,6 +8798,73 @@ echo function_exists("get_class_methods"); echo function_exists("get_object_vars ); } +/// Verifies eval `get_class_vars()` exposes visible class-like defaults like PHP. +#[test] +fn test_eval_declared_get_class_vars_builtin() { + let out = compile_and_run_capture( + r#" $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } + public function baseView() { + $vars = get_class_vars(EvalClassVarsBase::class); + ksort($vars); + foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } +} +$outside = get_class_vars("EvalClassVarsChild"); +ksort($outside); +foreach ($outside as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +(new EvalClassVarsChild())->childView(); +echo ":"; +(new EvalClassVarsChild())->baseView(); +echo ":"; +$trait = call_user_func("get_class_vars", "EvalClassVarsTrait"); +ksort($trait); +foreach ($trait as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +$enum = call_user_func_array("get_class_vars", ["class" => "EvalClassVarsBacked"]); +ksort($enum); +foreach ($enum as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +echo function_exists("get_class_vars") ? "F" : "f";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "basePublic=bp|baseStatic=static|childPublic=cp|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|childPrivate=cs|childProtected=cq|childPublic=cp|traitProtected=tq|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|typed=null|:traitPublic=tp|:name=null|value=null|:F" + ); +} + /// Verifies eval OOP introspection builtins honor AOT inherited private-member rules. #[test] fn test_eval_oop_introspection_builtins_for_aot_inherited_private_members() { From 84c8ff5cff25a07e22ab7dfd74b185ea751f8dcb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 20:59:41 +0200 Subject: [PATCH 0793/1208] Cover eval get_class_vars for AOT classes --- tests/codegen/eval.rs | 55 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 129a3ae8f6..c7f76ff918 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8865,6 +8865,61 @@ echo function_exists("get_class_vars") ? "F" : "f";'); ); } +/// Verifies eval `get_class_vars()` exposes generated/AOT class defaults by scope. +#[test] +fn test_eval_get_class_vars_exposes_aot_defaults_by_scope() { + let out = compile_and_run_capture( + r#" $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; +}'); + } +} +class EvalAotClassVarsChild extends EvalAotClassVarsBase { + public $childPublic = "cp"; + protected $childProtected = "cq"; + private $childPrivate = "cs"; + public static $childStatic = "childStatic"; + public function childView() { + return eval('$vars = get_class_vars(self::class); +ksort($vars); +foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; +}'); + } +} +eval('$outside = get_class_vars(EvalAotClassVarsChild::class); +ksort($outside); +foreach ($outside as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; +} +echo ":";'); +(new EvalAotClassVarsChild())->childView(); +echo ":"; +(new EvalAotClassVarsChild())->parentView(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "baseNullable=null|basePublic=bp|baseStatic=static|baseTyped=null|childPublic=cp|childStatic=childStatic|:baseNullable=null|baseProtected=bq|basePublic=bp|baseStatic=static|baseTyped=null|childPrivate=cs|childProtected=cq|childPublic=cp|childStatic=childStatic|:baseNullable=null|basePrivate=bs|baseProtected=bq|basePublic=bp|baseStatic=static|baseTyped=null|childProtected=cq|childPublic=cp|childStatic=childStatic|" + ); +} + /// Verifies eval OOP introspection builtins honor AOT inherited private-member rules. #[test] fn test_eval_oop_introspection_builtins_for_aot_inherited_private_members() { From 13dd4800896fba4fb0092bd9c0c568375193d4fc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 21:15:44 +0200 Subject: [PATCH 0794/1208] Support associative array property defaults --- src/codegen/lower_inst.rs | 11 ++++++++ src/codegen/lower_inst/objects.rs | 44 ++++++++++++++++++++++++++++--- tests/codegen/eval.rs | 12 ++++++--- tests/ir_backend_smoke_test.rs | 26 ++++++++++++++++++ 4 files changed, 86 insertions(+), 7 deletions(-) diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index b5b35121ba..fc8ee2acbe 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -5493,6 +5493,17 @@ fn emit_loaded_indexed_array_to_mixed(ctx: &mut FunctionContext<'_>, source_elem abi::emit_call_label(ctx.emitter, "__rt_array_to_mixed"); } +/// Converts the currently loaded associative-array argument into boxed Mixed values. +fn emit_loaded_assoc_array_to_mixed(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => {} + Arch::X86_64 => { + ctx.emitter.instruction("mov rdi, rax"); // pass the loaded associative-array argument to the Mixed conversion helper + } + } + abi::emit_call_label(ctx.emitter, "__rt_hash_to_mixed"); +} + /// Loads method call arguments for lexical `self::`/`parent::` instance calls using local `this`. fn materialize_method_call_args_with_receiver_local_and_refs( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 31b058c306..5fb5c6ad79 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -30,9 +30,9 @@ use crate::types::{ClassInfo, InterfaceInfo, PhpType}; use super::super::context::FunctionContext; use super::{ callables, cast_loaded_mixed_pointer_to_result, direct_call_stack_pad_bytes, expect_data, - coerce_loaded_value_to_tagged_scalar, emit_loaded_indexed_array_to_mixed, - emit_mixed_string_for_persistent_store, emit_ref_arg_writebacks, expect_operand, iterators, - load_value_to_first_int_arg, + coerce_loaded_value_to_tagged_scalar, emit_loaded_assoc_array_to_mixed, + emit_loaded_indexed_array_to_mixed, emit_mixed_string_for_persistent_store, + emit_ref_arg_writebacks, expect_operand, iterators, load_value_to_first_int_arg, materialize_direct_call_args_with_refs, materialize_method_call_args_with_receiver_reg_and_refs, resolve_method_call_target, store_if_result, store_method_call_result, @@ -4827,6 +4827,9 @@ fn ensure_property_value_supported( if can_convert_indexed_array_to_mixed_property(value_ty, &slot.php_type) { return Ok(()); } + if can_store_assoc_array_as_mixed_property(value_ty, &slot.php_type) { + return Ok(()); + } if can_store_value_as_tagged_scalar_property(value_ty, &slot.php_type) { return Ok(()); } @@ -5039,6 +5042,18 @@ fn can_convert_indexed_array_to_mixed_property(value_ty: &PhpType, slot_ty: &Php slot_elem.codegen_repr() == PhpType::Mixed && value_elem.codegen_repr() != PhpType::Mixed } +/// Returns true when associative-array storage can satisfy a generic `array` property. +fn can_store_assoc_array_as_mixed_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { + let PhpType::AssocArray { .. } = value_ty.codegen_repr() else { + return false; + }; + match slot_ty.codegen_repr() { + PhpType::Array(slot_elem) => slot_elem.codegen_repr() == PhpType::Mixed, + PhpType::AssocArray { value, .. } => value.codegen_repr() == PhpType::Mixed, + _ => false, + } +} + /// Returns true when a value can initialize a pointer-sized slot as null. fn is_pointer_slot_null_sentinel( ctx: &FunctionContext<'_>, @@ -5593,6 +5608,29 @@ fn load_property_store_value_to_result( abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Array(Box::new(PhpType::Mixed))); return Ok(()); } + if can_store_assoc_array_as_mixed_property(&value_ty, slot_ty) { + let PhpType::AssocArray { + key: source_key, + value: source_value, + } = ctx.load_value_to_result(value)?.codegen_repr() + else { + return Err(CodegenIrError::unsupported(format!( + "property associative-array widening from PHP type {:?}", + value_ty + ))); + }; + if source_value.codegen_repr() != PhpType::Mixed { + emit_loaded_assoc_array_to_mixed(ctx); + } + abi::emit_incref_if_refcounted( + ctx.emitter, + &PhpType::AssocArray { + key: source_key, + value: Box::new(PhpType::Mixed), + }, + ); + return Ok(()); + } if can_store_value_as_tagged_scalar_property(&value_ty, slot_ty) { match value_ty.codegen_repr() { PhpType::Void | PhpType::Never => { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c7f76ff918..593b3dd3bd 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8875,13 +8875,15 @@ class EvalAotClassVarsBase { protected $baseProtected = "bq"; private $basePrivate = "bs"; public static $baseStatic = "static"; + public array $baseArray = ["a" => 1]; public int $baseTyped; public ?int $baseNullable = null; public function parentView() { return eval('$vars = get_class_vars(EvalAotClassVarsChild::class); ksort($vars); foreach ($vars as $name => $value) { - echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + $rendered = is_array($value) ? $value["a"] : (is_null($value) ? "null" : $value); + echo $name . "=" . $rendered . "|"; }'); } } @@ -8894,14 +8896,16 @@ class EvalAotClassVarsChild extends EvalAotClassVarsBase { return eval('$vars = get_class_vars(self::class); ksort($vars); foreach ($vars as $name => $value) { - echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + $rendered = is_array($value) ? $value["a"] : (is_null($value) ? "null" : $value); + echo $name . "=" . $rendered . "|"; }'); } } eval('$outside = get_class_vars(EvalAotClassVarsChild::class); ksort($outside); foreach ($outside as $name => $value) { - echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + $rendered = is_array($value) ? $value["a"] : (is_null($value) ? "null" : $value); + echo $name . "=" . $rendered . "|"; } echo ":";'); (new EvalAotClassVarsChild())->childView(); @@ -8916,7 +8920,7 @@ echo ":"; ); assert_eq!( out.stdout, - "baseNullable=null|basePublic=bp|baseStatic=static|baseTyped=null|childPublic=cp|childStatic=childStatic|:baseNullable=null|baseProtected=bq|basePublic=bp|baseStatic=static|baseTyped=null|childPrivate=cs|childProtected=cq|childPublic=cp|childStatic=childStatic|:baseNullable=null|basePrivate=bs|baseProtected=bq|basePublic=bp|baseStatic=static|baseTyped=null|childProtected=cq|childPublic=cp|childStatic=childStatic|" + "baseArray=1|baseNullable=null|basePublic=bp|baseStatic=static|baseTyped=null|childPublic=cp|childStatic=childStatic|:baseArray=1|baseNullable=null|baseProtected=bq|basePublic=bp|baseStatic=static|baseTyped=null|childPrivate=cs|childProtected=cq|childPublic=cp|childStatic=childStatic|:baseArray=1|baseNullable=null|basePrivate=bs|baseProtected=bq|basePublic=bp|baseStatic=static|baseTyped=null|childProtected=cq|childPublic=cp|childStatic=childStatic|" ); } diff --git a/tests/ir_backend_smoke_test.rs b/tests/ir_backend_smoke_test.rs index c8ec9f46b4..c3712cb72c 100644 --- a/tests/ir_backend_smoke_test.rs +++ b/tests/ir_backend_smoke_test.rs @@ -2674,6 +2674,32 @@ echo is_null($box->a[5]) ? "N" : "bad"; "3:1:N:ok:6:7:G:N" ); + let assoc_object_source = r#" "Ada", "1" => "one", false => "zero"]; +} +$box = new AssocBox(); +echo count($box->a); +echo ":"; +echo $box->a["name"]; +echo ":"; +echo $box->a[1]; +echo ":"; +echo $box->a[0]; +$box->a["extra"] = "E"; +echo ":"; +echo count($box->a); +echo ":"; +echo $box->a["extra"]; +"#; + assert_eq!( + compile_and_run_ir_backend( + "assoc_array_object_property_defaults", + assoc_object_source + ), + "3:Ada:one:zero:4:E" + ); + let static_source = r#" Date: Fri, 26 Jun 2026 21:33:33 +0200 Subject: [PATCH 0795/1208] Honor eval scope for get_class_methods --- .../class_metadata/oop_introspection.rs | 153 ++++++++++++++++-- docs/php/eval.md | 5 +- tests/codegen/eval.rs | 91 +++++++++++ 3 files changed, 233 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 06b061d252..98e6412ffe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -18,7 +18,6 @@ use super::{eval_class_metadata_name, eval_class_relation_name_exists}; use std::collections::HashSet; const EVAL_CLASS_METADATA_FLAG_STATIC: u64 = 1; -const EVAL_CLASS_METADATA_FLAG_PUBLIC: u64 = 2; const EVAL_CLASS_METADATA_FLAG_PROTECTED: u64 = 4; const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; @@ -360,15 +359,19 @@ fn eval_class_method_names_for_scope( ) -> Result, EvalStatus> { if context.has_class(class_name) || context.has_enum(class_name) { let mut names = Vec::new(); + let mut seen = HashSet::new(); for name in context.class_method_names(class_name) { let Some((declaring_class, method)) = context.class_method(class_name, &name) else { - names.push(name); + eval_push_unique_method_name(&mut names, &mut seen, name); continue; }; if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() { - names.push(name); + eval_push_unique_method_name(&mut names, &mut seen, name); } } + eval_add_current_scope_private_method_names( + &mut names, &mut seen, class_name, context, values, + )?; return Ok(names); } if context.has_interface(class_name) { @@ -385,27 +388,151 @@ fn eval_class_method_names_for_scope( let method_names = values.reflection_method_names(class_name)?; let names = eval_runtime_string_array_to_vec(method_names, values)?; values.release(method_names)?; - eval_public_runtime_method_names(class_name, names, values) + let mut names = eval_visible_runtime_method_names(class_name, names, context, values)?; + let mut seen = names + .iter() + .map(|name| name.to_ascii_lowercase()) + .collect::>(); + eval_add_current_scope_private_method_names(&mut names, &mut seen, class_name, context, values)?; + Ok(names) } -/// Filters generated runtime methods to the public surface visible to eval. -fn eval_public_runtime_method_names( +/// Filters generated runtime methods to the surface visible from the current eval scope. +fn eval_visible_runtime_method_names( class_name: &str, names: Vec, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let mut result = Vec::new(); for name in names { - if values - .reflection_method_flags(class_name, &name)? - .is_some_and(|flags| flags & EVAL_CLASS_METADATA_FLAG_PUBLIC != 0) - { + let Some((declaring_class, visibility)) = + eval_runtime_method_access_metadata(class_name, &name, values)? + else { + continue; + }; + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() { result.push(name); } } Ok(result) } +/// Adds private methods declared by the current eval scope when PHP would expose them. +fn eval_add_current_scope_private_method_names( + names: &mut Vec, + seen: &mut HashSet, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(current_class) = context.current_class_scope() else { + return Ok(()); + }; + if !eval_class_metadata_is_a(class_name, current_class, context) { + return Ok(()); + } + if let Some(class) = context.class(current_class) { + for method in class.methods() { + if method.visibility() == EvalVisibility::Private { + eval_push_unique_method_name(names, seen, method.name().to_string()); + } + } + return Ok(()); + } + if context.has_interface(current_class) || context.has_trait(current_class) { + return Ok(()); + } + if !eval_class_relation_name_exists(current_class, context, values)? { + return Ok(()); + } + let method_names = values.reflection_method_names(current_class)?; + let current_names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + for name in current_names { + let Some((declaring_class, visibility)) = + eval_runtime_method_access_metadata(current_class, &name, values)? + else { + continue; + }; + if visibility == EvalVisibility::Private + && eval_same_class_metadata_name(&declaring_class, current_class) + { + eval_push_unique_method_name(names, seen, name); + } + } + Ok(()) +} + +/// Returns whether one eval or generated/AOT class name is the same as or extends another. +fn eval_class_metadata_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + eval_same_class_metadata_name(class_name, target) + || context.class_is_a(class_name, target, false) + || eval_native_class_metadata_is_a(class_name, target, context) +} + +/// Returns whether generated/AOT parent metadata proves one class extends another. +fn eval_native_class_metadata_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + let target = target.trim_start_matches('\\'); + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return false; + } + if eval_same_class_metadata_name(¤t, target) { + return true; + } + let Some(parent) = context.native_class_parent(¤t) else { + return false; + }; + current = parent.to_string(); + } +} + +/// Returns whether two PHP class names refer to the same normalized metadata name. +fn eval_same_class_metadata_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Appends one method name while preserving PHP's case-insensitive uniqueness rule. +fn eval_push_unique_method_name( + names: &mut Vec, + seen: &mut HashSet, + name: String, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + +/// Returns access metadata for one generated/AOT method name, if reflection exposes it. +fn eval_runtime_method_access_metadata( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_method_declaring_class(class_name, method_name)? + .unwrap_or_else(|| class_name.to_string()); + Ok(Some(( + declaring_class, + eval_runtime_member_visibility(flags), + ))) +} + /// Builds `get_class_vars()` for an eval-declared class or enum. fn eval_dynamic_class_vars_result( class_name: &str, @@ -697,13 +824,13 @@ fn eval_runtime_property_access_metadata( .unwrap_or_else(|| class_name.to_string()); Ok(Some(( declaring_class, - eval_runtime_property_visibility(flags), + eval_runtime_member_visibility(flags), flags & EVAL_CLASS_METADATA_FLAG_STATIC != 0, ))) } -/// Converts generated/AOT reflection property flags into eval visibility metadata. -fn eval_runtime_property_visibility(flags: u64) -> EvalVisibility { +/// Converts generated/AOT reflection member flags into eval visibility metadata. +fn eval_runtime_member_visibility(flags: u64) -> EvalVisibility { if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE != 0 { EvalVisibility::Private } else if flags & EVAL_CLASS_METADATA_FLAG_PROTECTED != 0 { diff --git a/docs/php/eval.md b/docs/php/eval.md index 26ffb809c5..125c4be47c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -621,9 +621,8 @@ as `class_exists()`. trait/enum metadata and generated runtime metadata. Object targets also see dynamic public properties. `get_class_methods()`, `get_class_vars()`, and `get_object_vars()` follow PHP visibility from the current eval class scope for -eval-declared metadata and objects; generated/AOT objects expose the public -bridge-visible metadata and object properties available through the runtime -hook slice. +eval-declared metadata and objects, and use generated/AOT runtime metadata when +available through the bridge-visible hook slice. Eval-declared enums share the dynamic class-like metadata path used by eval-declared classes. Pure and backed enum cases are singleton objects, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 593b3dd3bd..d24046491b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8966,6 +8966,97 @@ echo property_exists($object, "childSecret") ? "objectChildPrivateProperty" : "b ); } +/// Verifies eval `get_class_methods()` follows PHP scope visibility for eval-declared metadata. +#[test] +fn test_eval_get_class_methods_exposes_declared_methods_by_scope() { + let out = compile_and_run_capture( + r#"childView(); +echo ":"; +(new EvalClassMethodsChild())->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "basePublic,childPublic,childView,parentView:baseProtected,basePublic,childPrivate,childProtectedStatic,childPublic,childView,parentView:basePrivate,baseProtected,basePublic,childProtectedStatic,childPublic,childView,parentView" + ); +} + +/// Verifies eval `get_class_methods()` follows PHP scope visibility for generated/AOT metadata. +#[test] +fn test_eval_get_class_methods_exposes_aot_methods_by_scope() { + let out = compile_and_run_capture( + r#"childView(); +echo ":"; +(new EvalAotClassMethodsChild())->parentView(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "basepublic,basestaticpublic,childpublic,childview,parentview:baseprotected,basepublic,basestaticpublic,childprivate,childprotectedstatic,childpublic,childview,parentview:baseprivate,baseprotected,basepublic,basestaticpublic,childprotectedstatic,childpublic,childview,parentview" + ); +} + /// Verifies eval `get_object_vars()` skips uninitialized typed properties like PHP. #[test] fn test_eval_get_object_vars_skips_uninitialized_declared_properties() { From 6900e07c6823e84bbddbf05da6069e1ef5491671 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 21:41:08 +0200 Subject: [PATCH 0796/1208] Honor parent scope for property_exists --- .../class_metadata/oop_introspection.rs | 44 ++++++++++ docs/php/eval.md | 10 ++- tests/codegen/eval.rs | 82 +++++++++++++++++++ 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 98e6412ffe..800a6f5bff 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -196,6 +196,14 @@ fn eval_property_exists_target( if eval_property_exists_on_class(&class_name, property_name, context, values)? { return Ok(true); } + if eval_current_scope_private_property_exists_on_object( + &class_name, + property_name, + context, + values, + )? { + return Ok(true); + } eval_object_public_property_exists(target, property_name, values) } EVAL_TAG_STRING => { @@ -305,6 +313,42 @@ fn eval_property_exists_on_class( .eq_ignore_ascii_case(class_name.trim_start_matches('\\'))) } +/// Checks private instance properties declared by the current scope for object targets. +fn eval_current_scope_private_property_exists_on_object( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(current_class) = context.current_class_scope() else { + return Ok(false); + }; + if !eval_class_metadata_is_a(class_name, current_class, context) { + return Ok(false); + } + if let Some(class) = context.class(current_class) { + return Ok(class.properties().iter().any(|property| { + property.name() == property_name + && property.visibility() == EvalVisibility::Private + && !property.is_static() + })); + } + if context.has_interface(current_class) || context.has_trait(current_class) { + return Ok(false); + } + if !eval_class_relation_name_exists(current_class, context, values)? { + return Ok(false); + } + let Some((declaring_class, visibility, is_static)) = + eval_runtime_property_access_metadata(current_class, property_name, values)? + else { + return Ok(false); + }; + Ok(visibility == EvalVisibility::Private + && !is_static + && eval_same_class_metadata_name(&declaring_class, current_class)) +} + /// Resolves an object-or-class argument to a PHP class name and records whether it was an object. fn eval_class_metadata_target_name( target: RuntimeCellHandle, diff --git a/docs/php/eval.md b/docs/php/eval.md index 125c4be47c..d2aa414d13 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -619,10 +619,12 @@ visible inside eval through `enum_exists()` and through class-like probes such as `class_exists()`. `method_exists()` and `property_exists()` inspect eval-declared class/interface/ trait/enum metadata and generated runtime metadata. Object targets also see -dynamic public properties. `get_class_methods()`, `get_class_vars()`, and -`get_object_vars()` follow PHP visibility from the current eval class scope for -eval-declared metadata and objects, and use generated/AOT runtime metadata when -available through the bridge-visible hook slice. +dynamic public properties, and `property_exists()` follows PHP's current-scope +visibility for inherited private instance properties on object targets while +leaving class-string targets hidden. `get_class_methods()`, `get_class_vars()`, +and `get_object_vars()` follow PHP visibility from the current eval class scope +for eval-declared metadata and objects, and use generated/AOT runtime metadata +when available through the bridge-visible hook slice. Eval-declared enums share the dynamic class-like metadata path used by eval-declared classes. Pure and backed enum cases are singleton objects, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d24046491b..0b0d06ec4f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8966,6 +8966,88 @@ echo property_exists($object, "childSecret") ? "objectChildPrivateProperty" : "b ); } +/// Verifies eval `property_exists()` exposes parent private object properties only from parent scope. +#[test] +fn test_eval_property_exists_exposes_declared_parent_private_object_property_by_scope() { + let out = compile_and_run_capture( + r#"childView(); +echo ":"; +$object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "noOutsideObject:noOutsideClass:noChildParentPrivate:parentPrivate,noParentStatic,noClassPrivate" + ); +} + +/// Verifies eval `property_exists()` applies parent private object-property scope to AOT metadata. +#[test] +fn test_eval_property_exists_exposes_aot_parent_private_object_property_by_scope() { + let out = compile_and_run_capture( + r#"childView(); +echo ":"; +echo $object->parentView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "noOutsideObject:noOutsideClass:noChildParentPrivate:parentPrivate,noParentStatic,noClassPrivate" + ); +} + /// Verifies eval `get_class_methods()` follows PHP scope visibility for eval-declared metadata. #[test] fn test_eval_get_class_methods_exposes_declared_methods_by_scope() { From 394be795caeeb88fd77f92ab8b46a0c602634554 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 22:05:05 +0200 Subject: [PATCH 0797/1208] Support AOT class relation metadata in eval --- .../interpreter/builtins/class_metadata.rs | 58 ++++++++++++++++++- .../tests/builtins_class_metadata.rs | 40 +++++++++++++ docs/php/eval.md | 9 ++- tests/codegen/eval.rs | 51 ++++++++++++++++ 4 files changed, 155 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index d0d02eec30..bb74c81f6e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -87,7 +87,62 @@ pub(in crate::interpreter) fn eval_class_relation_target_result( _ => Err(EvalStatus::RuntimeFatal), }; } - values.assoc_new(0) + match name { + "class_implements" => eval_runtime_class_interface_names_result(&target, values), + "class_parents" => { + eval_class_relation_names_result( + eval_runtime_class_parent_names(&target, context), + values, + ) + } + "class_uses" => values.assoc_new(0), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds `class_implements()` data for generated/AOT class metadata. +fn eval_runtime_class_interface_names_result( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let names_array = values.reflection_class_interface_names(class_name)?; + let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + eval_class_relation_names_result(names, values) +} + +/// Returns generated/AOT parent names in PHP's nearest-parent-first order. +fn eval_runtime_class_parent_names( + class_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let mut names = Vec::new(); + let mut current = context.native_class_parent(class_name).map(str::to_string); + let mut seen = std::collections::HashSet::new(); + while let Some(parent) = current { + let parent = parent.trim_start_matches('\\').to_string(); + if !seen.insert(parent.to_ascii_lowercase()) { + break; + } + current = context.native_class_parent(&parent).map(str::to_string); + names.push(parent); + } + names +} + +/// Copies a runtime string array into Rust-owned class/interface names. +fn eval_class_relation_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_class_metadata_name(value, values)?); + } + Ok(result) } /// Evaluates class attribute metadata helpers. @@ -295,6 +350,7 @@ fn eval_class_relation_target_name( return Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)); } let name = eval_class_metadata_name(target, values)?; + let name = context.resolve_class_like_name(&name).unwrap_or(name); Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)) } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 9bb47c0f3f..535ee20db8 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -74,6 +74,46 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies generated/AOT parent and interface metadata is exposed to relation builtins. +#[test] +fn execute_program_reports_aot_class_relation_metadata() { + let program = parse_fragment( + br#"$implements = class_implements("KnownClass"); +echo count($implements); echo ":"; +echo $implements["KnownInterface"]; echo ":"; +$parents = class_parents("KnownClass"); +echo count($parents); echo ":"; +echo $parents["ParentClass"]; echo ":"; +$call = call_user_func("class_implements", "KnownClass"); +echo $call["KnownInterface"]; echo ":"; +$interfaceParents = class_implements("KnownInterface"); +echo $interfaceParents["Traversable"]; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => "KnownClass"]); +echo $named["ParentClass"]; echo ":"; +class_alias("KnownClass", "KnownAlias"); +$aliasImplements = class_implements("KnownAlias"); +echo $aliasImplements["KnownInterface"]; echo ":"; +$aliasParents = class_parents("KnownAlias"); +echo $aliasParents["ParentClass"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + assert!(context.define_native_class_parent("KnownClass", "ParentClass")); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.output, + "1:KnownInterface:1:ParentClass:KnownInterface:Traversable:ParentClass:KnownInterface:ParentClass" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies PHP OOP introspection builtins follow eval visibility and scope rules. #[test] fn execute_program_dispatches_oop_introspection_builtins() { diff --git a/docs/php/eval.md b/docs/php/eval.md index d2aa414d13..00e58f9d02 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -610,9 +610,14 @@ AOT and eval-declared class-name probes are visible through `class_exists()`. Eval object relation probes through `instanceof`, `is_a()`, and `is_subclass_of()` use generated AOT class/interface metadata and eval-created object metadata. `interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated -AOT metadata. `class_alias()` can alias eval-declared and generated/AOT +AOT metadata. `class_implements()` and `class_parents()` materialize +generated/AOT interface and parent metadata when the bridge exposes it, while +`class_uses()` reports eval-declared direct trait uses. `class_alias()` can +alias eval-declared and generated/AOT classes, interfaces, traits, and enums, preserving the target class-like kind -for the corresponding metadata probes. Aliases are usable for class-like +for the corresponding metadata probes. Top-level compiled `class_alias()` +calls still use elephc's generated subclass model, so eval sees the same AOT +parent metadata as the compiled program. Aliases are usable for class-like lookups but are not added to `get_declared_classes()`, `get_declared_interfaces()`, or `get_declared_traits()`. Eval-declared enums are visible inside eval through `enum_exists()` and through class-like probes such diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0b0d06ec4f..6c15acd864 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7770,6 +7770,57 @@ echo function_exists("is_a"); echo function_exists("is_subclass_of");'); assert_eq!(out, "YYYNYYYYN11"); } +/// Verifies eval class-relation builtins materialize generated/AOT metadata. +#[test] +fn test_eval_class_relation_builtins_expose_aot_metadata() { + let out = compile_and_run_capture( + r#" "EvalAotRelationChild"]); +foreach ($stringParents as $name) { echo $name . ","; } +echo ":"; +echo function_exists("class_implements"); echo function_exists("class_parents");'); +class_alias("EvalAotRelationChild", "EvalAotRelationAlias"); +eval('echo ":"; +$aliasImplements = class_implements("EvalAotRelationAlias"); +ksort($aliasImplements); +foreach ($aliasImplements as $name) { echo $name . ","; } +echo ":"; +$aliasParents = class_parents("EvalAotRelationAlias"); +foreach ($aliasParents as $name) { echo $name . ","; }'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotRelationBaseIface,EvalAotRelationChildIface,:EvalAotRelationBaseIface,EvalAotRelationChildIface,:EvalAotRelationBaseIface,:EvalAotRelationMid,EvalAotRelationBase,:EvalAotRelationMid,EvalAotRelationBase,:11:EvalAotRelationBaseIface,EvalAotRelationChildIface,:EvalAotRelationChild,EvalAotRelationMid,EvalAotRelationBase," + ); +} + /// Verifies eval `instanceof` probes AOT and eval-declared class metadata. #[test] fn test_eval_fragment_instanceof_probes_class_metadata() { From 8fe11ea498ad3f39c48fd6e9c989ec1bc6a4b686 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 22:12:07 +0200 Subject: [PATCH 0798/1208] Support eval trait targets in class_uses --- .../interpreter/builtins/class_metadata.rs | 9 +++++++ .../tests/builtins_class_metadata.rs | 16 +++++++++++-- docs/php/eval.md | 3 ++- tests/codegen/eval.rs | 24 +++++++++++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index bb74c81f6e..79408bfb56 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -87,6 +87,15 @@ pub(in crate::interpreter) fn eval_class_relation_target_result( _ => Err(EvalStatus::RuntimeFatal), }; } + if context.trait_decl(&target).is_some() { + return match name { + "class_uses" => { + eval_class_relation_names_result(context.trait_trait_names(&target), values) + } + "class_implements" | "class_parents" => values.assoc_new(0), + _ => Err(EvalStatus::RuntimeFatal), + }; + } match name { "class_implements" => eval_runtime_class_interface_names_result(&target, values), "class_parents" => { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 535ee20db8..46a1fe0c88 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -12,11 +12,15 @@ use super::super::*; use super::support::*; -/// Verifies class-relation helpers return empty arrays for known eval classes. +/// Verifies class-relation helpers handle eval class and trait targets. #[test] fn execute_program_dispatches_class_relation_builtins() { let program = parse_fragment( br#"class EvalMeta {} +trait EvalMetaInnerTrait {} +trait EvalMetaOuterTrait { + use EvalMetaInnerTrait; +} $object = new EvalMeta(); $implements = class_implements("EvalMeta"); echo is_array($implements) && count($implements) === 0 ? "impl" : "bad"; echo ":"; @@ -29,6 +33,11 @@ $call = call_user_func("class_implements", "EvalMeta"); echo is_array($call) && count($call) === 0 ? "call" : "bad"; echo ":"; $named = call_user_func_array("class_parents", ["object_or_class" => "EvalMeta"]); echo is_array($named) && count($named) === 0 ? "named" : "bad"; echo ":"; +$trait_uses = class_uses("EvalMetaOuterTrait"); +echo $trait_uses["EvalMetaInnerTrait"]; echo ":"; +class_alias("EvalMetaOuterTrait", "EvalMetaOuterTraitAlias"); +$alias_uses = class_uses("EvalMetaOuterTraitAlias"); +echo $alias_uses["EvalMetaInnerTrait"]; echo ":"; echo function_exists("class_implements"); echo function_exists("class_parents"); echo function_exists("class_uses"); return true;"#, @@ -39,7 +48,10 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "impl:parents:uses:missing:call:named:111"); + assert_eq!( + values.output, + "impl:parents:uses:missing:call:named:EvalMetaInnerTrait:EvalMetaInnerTrait:111" + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval-declared parent and interface metadata is exposed to relation builtins. diff --git a/docs/php/eval.md b/docs/php/eval.md index 00e58f9d02..2d891ce742 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -612,7 +612,8 @@ generated AOT class/interface metadata and eval-created object metadata. `interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated AOT metadata. `class_implements()` and `class_parents()` materialize generated/AOT interface and parent metadata when the bridge exposes it, while -`class_uses()` reports eval-declared direct trait uses. `class_alias()` can +`class_uses()` reports direct trait uses for eval-declared classes and traits. +`class_alias()` can alias eval-declared and generated/AOT classes, interfaces, traits, and enums, preserving the target class-like kind for the corresponding metadata probes. Top-level compiled `class_alias()` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6c15acd864..190ceec3aa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7821,6 +7821,30 @@ foreach ($aliasParents as $name) { echo $name . ","; }'); ); } +/// Verifies eval `class_uses()` accepts eval-declared trait targets and aliases. +#[test] +fn test_eval_class_uses_exposes_eval_trait_targets() { + let out = compile_and_run( + r#" Date: Fri, 26 Jun 2026 22:44:29 +0200 Subject: [PATCH 0799/1208] Expose AOT class_uses metadata to eval --- .../interpreter/builtins/class_metadata.rs | 13 ++- .../src/interpreter/runtime_ops.rs | 6 ++ .../tests/builtins_class_metadata.rs | 10 ++- .../interpreter/tests/support/object_ops.rs | 17 +++- .../interpreter/tests/support/runtime_ops.rs | 7 ++ .../src/runtime_hooks/externs.rs | 4 + .../elephc-magician/src/runtime_hooks/ops.rs | 13 +++ docs/php/eval.md | 6 +- src/codegen/mod.rs | 1 + src/codegen_support/runtime/data/user.rs | 81 +++++++++++++++++++ src/codegen_support/runtime/eval_bridge.rs | 28 +++++++ tests/codegen/eval.rs | 16 +++- 12 files changed, 193 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index 79408bfb56..b46262187e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -104,7 +104,7 @@ pub(in crate::interpreter) fn eval_class_relation_target_result( values, ) } - "class_uses" => values.assoc_new(0), + "class_uses" => eval_runtime_class_trait_names_result(&target, values), _ => Err(EvalStatus::RuntimeFatal), } } @@ -120,6 +120,17 @@ fn eval_runtime_class_interface_names_result( eval_class_relation_names_result(names, values) } +/// Builds `class_uses()` data for generated/AOT direct trait-use metadata. +fn eval_runtime_class_trait_names_result( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let names_array = values.reflection_class_trait_names(class_name)?; + let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + eval_class_relation_names_result(names, values) +} + /// Returns generated/AOT parent names in PHP's nearest-parent-first order. fn eval_runtime_class_parent_names( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 7851005733..c4f91e9d25 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -253,6 +253,12 @@ pub trait RuntimeValueOps { class_name: &str, ) -> Result; + /// Returns generated/AOT trait names visible for one reflected class-like symbol. + fn reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result; + /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 46a1fe0c88..19f2a505f4 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -100,13 +100,19 @@ $call = call_user_func("class_implements", "KnownClass"); echo $call["KnownInterface"]; echo ":"; $interfaceParents = class_implements("KnownInterface"); echo $interfaceParents["Traversable"]; echo ":"; +$uses = class_uses("KnownClass"); +echo count($uses); echo ":"; echo $uses["KnownTrait"]; echo ":"; +$traitUses = class_uses("KnownTrait"); +echo $traitUses["KnownInnerTrait"]; echo ":"; $named = call_user_func_array("class_parents", ["object_or_class" => "KnownClass"]); echo $named["ParentClass"]; echo ":"; class_alias("KnownClass", "KnownAlias"); $aliasImplements = class_implements("KnownAlias"); echo $aliasImplements["KnownInterface"]; echo ":"; $aliasParents = class_parents("KnownAlias"); -echo $aliasParents["ParentClass"]; +echo $aliasParents["ParentClass"]; echo ":"; +$aliasUses = class_uses("KnownAlias"); +echo $aliasUses["KnownTrait"]; return true;"#, ) .expect("parse eval fragment"); @@ -121,7 +127,7 @@ return true;"#, assert_eq!( values.output, - "1:KnownInterface:1:ParentClass:KnownInterface:Traversable:ParentClass:KnownInterface:ParentClass" + "1:KnownInterface:1:ParentClass:KnownInterface:Traversable:1:KnownTrait:KnownInnerTrait:ParentClass:KnownInterface:ParentClass:KnownTrait" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 8ae30c4aaf..0ebbb779e4 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -1283,6 +1283,19 @@ impl FakeOps { } Ok(array) } + /// Reports fake generated/AOT ReflectionClass trait names for metadata unit tests. + pub(super) fn runtime_reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownTrait")?; + } else if class_name.eq_ignore_ascii_case("KnownTrait") { + array = self.runtime_string_array_push(array, "KnownInnerTrait")?; + } + Ok(array) + } /// Reports one fake AOT interface for eval `interface_exists` unit tests. pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { Ok([ @@ -1306,7 +1319,9 @@ impl FakeOps { } /// Reports one fake AOT trait for eval `trait_exists` unit tests. pub(super) fn runtime_trait_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownTrait")) + Ok(["KnownTrait", "KnownInnerTrait"] + .iter() + .any(|known| name.eq_ignore_ascii_case(known))) } /// Reports one fake AOT enum for eval `enum_exists` unit tests. pub(super) fn runtime_enum_exists(&mut self, name: &str) -> Result { diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index 56fafa8bc2..6096984aa1 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -298,6 +298,13 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_reflection_class_interface_names(class_name) } + /// Reports fake generated/AOT ReflectionClass trait names for metadata bridge tests. + fn reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_names(class_name) + } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { self.runtime_new_object(_class_name) diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 5d22ff0b34..945e23311d 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -213,6 +213,10 @@ unsafe extern "C" { class_ptr: *const u8, class_len: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_trait_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, name_len: u64, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index baa63a57da..0722138ac5 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -576,6 +576,19 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns generated AOT trait names visible for one reflected class-like symbol. + fn reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { let object = Self::handle(unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index 2d891ce742..ae5a19f749 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -611,9 +611,9 @@ Eval object relation probes through `instanceof`, `is_a()`, and `is_subclass_of( generated AOT class/interface metadata and eval-created object metadata. `interface_exists()`, `trait_exists()`, and `enum_exists()` can probe generated AOT metadata. `class_implements()` and `class_parents()` materialize -generated/AOT interface and parent metadata when the bridge exposes it, while -`class_uses()` reports direct trait uses for eval-declared classes and traits. -`class_alias()` can +generated/AOT interface and parent metadata when the bridge exposes it. +`class_uses()` reports direct trait uses for eval-declared and generated/AOT +classes and traits. `class_alias()` can alias eval-declared and generated/AOT classes, interfaces, traits, and enums, preserving the target class-like kind for the corresponding metadata probes. Top-level compiled `class_alias()` diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 3bd5a39005..1f3083bb9f 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -241,6 +241,7 @@ fn finalize_user_asm( &runtime_interfaces, &module.declared_interface_names, &module.trait_table.names, + &module.declared_trait_uses, &runtime_classes, &module.enum_infos, Some(&allowed_class_names), diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index c64635b181..9634db7aaa 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -50,6 +50,7 @@ pub(crate) fn emit_runtime_data_user( interfaces: &HashMap, interface_names: &[String], trait_names: &[String], + declared_trait_uses: &HashMap>, classes: &HashMap, enums: &HashMap, allowed_class_names: Option<&HashSet>, @@ -482,6 +483,12 @@ pub(crate) fn emit_runtime_data_user( emit_eval_reflection_class_lookup_data(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); emit_eval_reflection_class_interface_lookup_data(&mut out, &sorted_classes, interfaces); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_trait_lookup_data( + &mut out, + &sorted_classes, + declared_trait_uses, + ); // -- class-level PHP 8 attribute metadata table -- // Per-class layout: count followed by (name_ptr, name_len) pairs. @@ -1387,6 +1394,78 @@ fn push_eval_reflection_class_interface_row( *index += 1; } +/// Emits class-like/trait-name rows consumed by eval `class_uses()` metadata probes. +fn emit_eval_reflection_class_trait_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], + declared_trait_uses: &HashMap>, +) { + let mut entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + for trait_name in &class_info.used_traits { + push_eval_reflection_class_trait_row( + out, + &mut entries, + &mut index, + class_name, + trait_name, + ); + } + } + + let mut sorted_traits: Vec<&String> = declared_trait_uses.keys().collect(); + sorted_traits.sort(); + for trait_name in sorted_traits { + if let Some(used_traits) = declared_trait_uses.get(trait_name) { + for used_trait in used_traits { + push_eval_reflection_class_trait_row( + out, + &mut entries, + &mut index, + trait_name, + used_trait, + ); + } + } + } + + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_class_trait_count\n_eval_reflection_class_trait_count:\n"); + out.push_str(&format!(" .quad {}\n", entries.len())); + out.push_str(".globl _eval_reflection_class_traits\n_eval_reflection_class_traits:\n"); + for (class_label, class_len, trait_label, trait_len) in entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", trait_label)); + out.push_str(&format!(" .quad {}\n", trait_len)); + } +} + +/// Adds one class-like/trait-name row and its backing string labels. +fn push_eval_reflection_class_trait_row( + out: &mut String, + entries: &mut Vec<(String, usize, String, usize)>, + index: &mut usize, + class_name: &str, + trait_name: &str, +) { + let class_label = format!("_eval_reflection_class_trait_class_{}", *index); + let trait_label = format!("_eval_reflection_class_trait_name_{}", *index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + trait_label, + escaped_ascii(trait_name) + )); + entries.push((class_label, class_name.len(), trait_label, trait_name.len())); + *index += 1; +} + /// Returns direct and inherited parent interface names for one generated interface. fn eval_reflection_interface_parent_names( interface_name: &str, @@ -2041,6 +2120,7 @@ mod tests { &HashMap::new(), &[], &[], + &HashMap::new(), &classes, &HashMap::new(), Some(&allowed_class_names), @@ -2068,6 +2148,7 @@ mod tests { &HashMap::new(), &[], &[], + &HashMap::new(), &classes, &HashMap::new(), None, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index dea4b3ec0a..4f6b5bd02c 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -183,6 +183,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emit_aarch64_eval_reflection_method_names(emitter); emit_aarch64_eval_reflection_property_names(emitter); emit_aarch64_eval_reflection_class_interface_names(emitter); + emit_aarch64_eval_reflection_class_trait_names(emitter); emit_aarch64_eval_reflection_class_flags(emitter); emit_aarch64_eval_reflection_method_flags(emitter); emit_aarch64_eval_reflection_method_declaring_class(emitter); @@ -1643,6 +1644,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emit_x86_64_eval_reflection_method_names(emitter); emit_x86_64_eval_reflection_property_names(emitter); emit_x86_64_eval_reflection_class_interface_names(emitter); + emit_x86_64_eval_reflection_class_trait_names(emitter); emit_x86_64_eval_reflection_class_flags(emitter); emit_x86_64_eval_reflection_method_flags(emitter); emit_x86_64_eval_reflection_method_declaring_class(emitter); @@ -3141,6 +3143,19 @@ fn emit_aarch64_eval_reflection_class_interface_names(emitter: &mut Emitter) { ); } +/// Emits the ARM64 eval hook that returns AOT `class_uses()` trait names. +fn emit_aarch64_eval_reflection_class_trait_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_names", + "_eval_reflection_class_trait_count", + "_eval_reflection_class_traits", + "__elephc_eval_reflection_class_trait_names", + "class trait", + 32, + ); +} + /// Emits an ARM64 class-filtered AOT reflection member-name scanner. fn emit_aarch64_eval_reflection_member_names( emitter: &mut Emitter, @@ -3531,6 +3546,19 @@ fn emit_x86_64_eval_reflection_class_interface_names(emitter: &mut Emitter) { ); } +/// Emits the x86_64 eval hook that returns AOT `class_uses()` trait names. +fn emit_x86_64_eval_reflection_class_trait_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_names", + "_eval_reflection_class_trait_count", + "_eval_reflection_class_traits", + "__elephc_eval_reflection_class_trait_names_x86", + "class trait", + 32, + ); +} + /// Emits an x86_64 class-filtered AOT reflection member-name scanner. fn emit_x86_64_eval_reflection_member_names( emitter: &mut Emitter, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 190ceec3aa..62036663e4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7777,9 +7777,15 @@ fn test_eval_class_relation_builtins_expose_aot_metadata() { r#" Date: Fri, 26 Jun 2026 22:54:38 +0200 Subject: [PATCH 0800/1208] Expose AOT trait metadata through ReflectionClass --- .../src/interpreter/reflection.rs | 26 ++++++++++++--- .../tests/builtins_class_metadata.rs | 14 +++++--- docs/php/eval.md | 5 +-- tests/codegen/eval.rs | 32 +++++++++++++++++++ 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index af14a4786b..62dc49c705 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -853,7 +853,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( } else if relation_kind == "interfaces" { eval_reflection_aot_class_interface_names(&reflected_name, values)? } else { - Vec::new() + eval_reflection_aot_class_trait_names(&reflected_name, values)? }; eval_reflection_class_object_map_result(&names, context, values).map(Some) } @@ -2751,6 +2751,7 @@ fn eval_reflection_class_owner_object_result( values, )?; let interface_names = eval_reflection_aot_class_interface_names(reflected_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(reflected_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(reflected_name, values)?; let attributes = context.native_class_attributes(reflected_name); return eval_reflection_owner_object( @@ -2758,7 +2759,7 @@ fn eval_reflection_class_owner_object_result( reflected_name, &attributes, &interface_names, - &[], + &trait_names, &method_names, &property_names, parent_class_name.as_deref(), @@ -5014,6 +5015,7 @@ fn eval_reflection_full_class_object_result( )?; let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; let attributes = context.native_class_attributes(runtime_class_name); return eval_reflection_owner_object( @@ -5021,7 +5023,7 @@ fn eval_reflection_full_class_object_result( runtime_class_name, &attributes, &interface_names, - &[], + &trait_names, &method_names, &property_names, parent_class_name.as_deref(), @@ -5072,13 +5074,14 @@ fn eval_reflection_shallow_class_object_result( return values.bool_value(false); }; let interface_names = eval_reflection_aot_class_interface_names(class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(class_name, values)?; let attributes = context.native_class_attributes(class_name); return eval_reflection_owner_object_with_members( EVAL_REFLECTION_OWNER_CLASS, class_name.trim_start_matches('\\'), &attributes, &interface_names, - &[], + &trait_names, &[], &[], None, @@ -5864,6 +5867,18 @@ fn eval_reflection_aot_class_interface_names( Ok(names) } +/// Returns generated AOT trait names for one reflected class-like symbol. +fn eval_reflection_aot_class_trait_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = values.reflection_class_trait_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + /// Copies a runtime string array into Rust-owned strings for reflection metadata assembly. fn eval_reflection_string_array_to_vec( array: RuntimeCellHandle, @@ -6280,6 +6295,7 @@ fn eval_reflection_class_to_string_metadata( values, )?; let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; Ok(Some(EvalReflectionClassMetadata { resolved_name: runtime_class_name.to_string(), @@ -6288,7 +6304,7 @@ fn eval_reflection_class_to_string_metadata( flags, modifiers, interface_names, - trait_names: Vec::new(), + trait_names, method_names, property_names, parent_class_name, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 19f2a505f4..bdf7b1440a 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -796,9 +796,9 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } -/// Verifies ReflectionClass::getInterfaceNames reads fake generated/AOT metadata. +/// Verifies ReflectionClass relation-name helpers read fake generated/AOT metadata. #[test] -fn execute_program_reflects_aot_class_interface_names() { +fn execute_program_reflects_aot_class_relation_names() { let program = parse_fragment( br#"$class_names = (new ReflectionClass("KnownClass"))->getInterfaceNames(); echo count($class_names); echo ":"; echo $class_names[0]; echo ":"; @@ -807,7 +807,13 @@ echo count($interface_names); echo ":"; echo $interface_names[0]; echo ":"; $class_objects = (new ReflectionClass("KnownClass"))->getInterfaces(); echo count($class_objects); echo ":"; echo $class_objects["KnownInterface"]->getName(); echo ":"; $interface_objects = (new ReflectionClass("KnownInterface"))->getInterfaces(); -echo count($interface_objects); echo ":"; echo $interface_objects["Traversable"]->getName(); +echo count($interface_objects); echo ":"; echo $interface_objects["Traversable"]->getName(); echo ":"; +$trait_names = (new ReflectionClass("KnownClass"))->getTraitNames(); +echo count($trait_names); echo ":"; echo $trait_names[0]; echo ":"; +$trait_objects = (new ReflectionClass("KnownClass"))->getTraits(); +echo count($trait_objects); echo ":"; echo $trait_objects["KnownTrait"]->getName(); echo ":"; +$nested_trait_names = (new ReflectionClass("KnownTrait"))->getTraitNames(); +echo count($nested_trait_names); echo ":"; echo $nested_trait_names[0]; return true;"#, ) .expect("parse eval fragment"); @@ -818,7 +824,7 @@ return true;"#, assert_eq!( values.output, - "1:KnownInterface:1:Traversable:1:KnownInterface:1:Traversable" + "1:KnownInterface:1:Traversable:1:KnownInterface:1:Traversable:1:KnownTrait:1:KnownTrait:1:KnownInnerTrait" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index ae5a19f749..0af37f3c35 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -320,8 +320,9 @@ modifier bitmask for eval class-like metadata. and generated/AOT classes, plus parent interfaces for eval and generated/AOT interfaces. `ReflectionClass::getInterfaces()` materializes those names as a name-keyed array of `ReflectionClass` objects. `ReflectionClass::getTraitNames()` -returns traits used directly by eval classes, `ReflectionClass::getTraits()` -materializes those direct trait names as `ReflectionClass` objects, and +returns traits used directly by eval and generated/AOT classes and traits, +`ReflectionClass::getTraits()` materializes those direct trait names as +`ReflectionClass` objects, and `ReflectionClass::getTraitAliases()` exposes direct eval trait `as` aliases as PHP's alias-name to `Trait::method` map. `ReflectionClass::implementsInterface()` checks those eval relations diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 62036663e4..3b611e2950 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12398,6 +12398,38 @@ echo $interfaceObjects["EvalAotReflectIfaceBase"]->getName();'); ); } +/// Verifies eval ReflectionClass exposes generated/AOT direct trait-use metadata. +#[test] +fn test_eval_reflection_class_get_trait_names_for_aot_class() { + let out = compile_and_run_capture( + r#"getTraitNames(); +echo count($traits) . ":" . $traits[0] . ":"; +$traitObjects = $ref->getTraits(); +echo count($traitObjects) . ":" . $traitObjects["EvalAotReflectRelationOuterTrait"]->getName() . ":"; +$nestedTraits = (new ReflectionClass("EvalAotReflectRelationOuterTrait"))->getTraitNames(); +echo count($nestedTraits) . ":" . $nestedTraits[0];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalAotReflectRelationOuterTrait:1:EvalAotReflectRelationOuterTrait:1:EvalAotReflectRelationInnerTrait" + ); +} + /// Verifies eval ReflectionClass::implementsInterface reports class, enum, and /// interface metadata through the bridge. #[test] From f04e6c088a1c0e22c798bcb3e77072a0381ac096 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 23:14:22 +0200 Subject: [PATCH 0801/1208] Expose AOT trait aliases through ReflectionClass --- .../src/interpreter/reflection.rs | 22 ++++- .../src/interpreter/runtime_ops.rs | 12 +++ .../tests/builtins_class_metadata.rs | 6 +- .../interpreter/tests/support/object_ops.rs | 22 +++++ .../interpreter/tests/support/runtime_ops.rs | 14 ++++ .../src/runtime_hooks/externs.rs | 8 ++ .../elephc-magician/src/runtime_hooks/ops.rs | 26 ++++++ docs/php/eval.md | 4 +- src/codegen_support/runtime/data/user.rs | 80 +++++++++++++++++++ src/codegen_support/runtime/eval_bridge.rs | 56 +++++++++++++ tests/codegen/eval.rs | 28 +++++++ 11 files changed, 273 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 62dc49c705..831ca6cac2 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -878,8 +878,10 @@ pub(in crate::interpreter) fn eval_reflection_class_get_trait_aliases_result( }; let aliases = if context.trait_decl(&reflected_name).is_some() { context.trait_trait_aliases(&reflected_name) - } else { + } else if eval_reflection_class_like_exists(&reflected_name, context) { context.class_trait_aliases(&reflected_name) + } else { + eval_reflection_aot_class_trait_aliases(&reflected_name, values)? }; eval_reflection_string_assoc_result(aliases, values).map(Some) } @@ -5879,6 +5881,24 @@ fn eval_reflection_aot_class_trait_names( Ok(names) } +/// Returns generated AOT trait aliases for one reflected class-like symbol. +fn eval_reflection_aot_class_trait_aliases( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let alias_names_array = values.reflection_class_trait_alias_names(runtime_class_name)?; + let alias_names = eval_reflection_string_array_to_vec(alias_names_array, values)?; + values.release(alias_names_array)?; + let alias_sources_array = values.reflection_class_trait_alias_sources(runtime_class_name)?; + let alias_sources = eval_reflection_string_array_to_vec(alias_sources_array, values)?; + values.release(alias_sources_array)?; + if alias_names.len() != alias_sources.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(alias_names.into_iter().zip(alias_sources).collect()) +} + /// Copies a runtime string array into Rust-owned strings for reflection metadata assembly. fn eval_reflection_string_array_to_vec( array: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index c4f91e9d25..fa88d1816f 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -259,6 +259,18 @@ pub trait RuntimeValueOps { class_name: &str, ) -> Result; + /// Returns generated/AOT trait alias names visible for one reflected class-like symbol. + fn reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result; + + /// Returns generated/AOT trait alias sources visible for one reflected class-like symbol. + fn reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result; + /// Creates a named runtime object without constructor arguments. fn new_object(&mut self, class_name: &str) -> Result; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index bdf7b1440a..80c0c624f0 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -813,7 +813,9 @@ echo count($trait_names); echo ":"; echo $trait_names[0]; echo ":"; $trait_objects = (new ReflectionClass("KnownClass"))->getTraits(); echo count($trait_objects); echo ":"; echo $trait_objects["KnownTrait"]->getName(); echo ":"; $nested_trait_names = (new ReflectionClass("KnownTrait"))->getTraitNames(); -echo count($nested_trait_names); echo ":"; echo $nested_trait_names[0]; +echo count($nested_trait_names); echo ":"; echo $nested_trait_names[0]; echo ":"; +$aliases = (new ReflectionClass("KnownClass"))->getTraitAliases(); +echo count($aliases); echo ":"; echo $aliases["knownAlias"]; return true;"#, ) .expect("parse eval fragment"); @@ -824,7 +826,7 @@ return true;"#, assert_eq!( values.output, - "1:KnownInterface:1:Traversable:1:KnownInterface:1:Traversable:1:KnownTrait:1:KnownTrait:1:KnownInnerTrait" + "1:KnownInterface:1:Traversable:1:KnownInterface:1:Traversable:1:KnownTrait:1:KnownTrait:1:KnownInnerTrait:1:KnownTrait::source" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 0ebbb779e4..8a90462954 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -1296,6 +1296,28 @@ impl FakeOps { } Ok(array) } + /// Reports fake generated/AOT ReflectionClass trait alias names for metadata unit tests. + pub(super) fn runtime_reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "knownAlias")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass trait alias sources for metadata unit tests. + pub(super) fn runtime_reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownTrait::source")?; + } + Ok(array) + } /// Reports one fake AOT interface for eval `interface_exists` unit tests. pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { Ok([ diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index 6096984aa1..6e260a8771 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -305,6 +305,20 @@ impl RuntimeValueOps for FakeOps { ) -> Result { self.runtime_reflection_class_trait_names(class_name) } + /// Reports fake generated/AOT ReflectionClass trait alias names for metadata bridge tests. + fn reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_alias_names(class_name) + } + /// Reports fake generated/AOT ReflectionClass trait alias sources for metadata bridge tests. + fn reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_alias_sources(class_name) + } /// Creates one fake object for eval `new` unit tests. fn new_object(&mut self, _class_name: &str) -> Result { self.runtime_new_object(_class_name) diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 945e23311d..c471981bbe 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -217,6 +217,14 @@ unsafe extern "C" { class_ptr: *const u8, class_len: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_trait_alias_names( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_class_trait_alias_sources( + class_ptr: *const u8, + class_len: u64, + ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_new_object( name_ptr: *const u8, name_len: u64, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 0722138ac5..07c3bb887a 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -589,6 +589,32 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns generated AOT trait alias names visible for one reflected class-like symbol. + fn reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_alias_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + /// Returns generated AOT trait alias sources visible for one reflected class-like symbol. + fn reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_alias_sources( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. fn new_object(&mut self, class_name: &str) -> Result { let object = Self::handle(unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index 0af37f3c35..b460d01a52 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -323,8 +323,8 @@ name-keyed array of `ReflectionClass` objects. `ReflectionClass::getTraitNames() returns traits used directly by eval and generated/AOT classes and traits, `ReflectionClass::getTraits()` materializes those direct trait names as `ReflectionClass` objects, and -`ReflectionClass::getTraitAliases()` exposes direct eval trait `as` aliases as -PHP's alias-name to `Trait::method` map. +`ReflectionClass::getTraitAliases()` exposes direct eval and generated/AOT trait +`as` aliases as PHP's alias-name to `Trait::method` map. `ReflectionClass::implementsInterface()` checks those eval relations case-insensitively, returns true when reflecting the requested interface itself, and checks generated/AOT class-interface relations through runtime metadata. It diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 9634db7aaa..f533617591 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -489,6 +489,8 @@ pub(crate) fn emit_runtime_data_user( &sorted_classes, declared_trait_uses, ); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_trait_alias_lookup_data(&mut out, &sorted_classes); // -- class-level PHP 8 attribute metadata table -- // Per-class layout: count followed by (name_ptr, name_len) pairs. @@ -1466,6 +1468,84 @@ fn push_eval_reflection_class_trait_row( *index += 1; } +/// Emits class/alias and class/source rows consumed by eval `getTraitAliases()`. +fn emit_eval_reflection_class_trait_alias_lookup_data( + out: &mut String, + sorted_classes: &[(&String, &ClassInfo)], +) { + let mut alias_entries = Vec::new(); + let mut source_entries = Vec::new(); + let mut index = 0usize; + for (class_name, class_info) in sorted_classes { + for (alias, source) in &class_info.trait_aliases { + push_eval_reflection_class_trait_alias_row( + out, + &mut alias_entries, + &mut source_entries, + &mut index, + class_name, + alias, + source, + ); + } + } + + out.push_str(".p2align 3\n"); + out.push_str( + ".globl _eval_reflection_class_trait_alias_count\n_eval_reflection_class_trait_alias_count:\n", + ); + out.push_str(&format!(" .quad {}\n", alias_entries.len())); + out.push_str(".globl _eval_reflection_class_trait_aliases\n_eval_reflection_class_trait_aliases:\n"); + for (class_label, class_len, alias_label, alias_len) in alias_entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", alias_label)); + out.push_str(&format!(" .quad {}\n", alias_len)); + } + out.push_str( + ".globl _eval_reflection_class_trait_alias_sources\n_eval_reflection_class_trait_alias_sources:\n", + ); + for (class_label, class_len, source_label, source_len) in source_entries { + out.push_str(&format!(" .quad {}\n", class_label)); + out.push_str(&format!(" .quad {}\n", class_len)); + out.push_str(&format!(" .quad {}\n", source_label)); + out.push_str(&format!(" .quad {}\n", source_len)); + } +} + +/// Adds one class/trait-alias row and its backing string labels. +fn push_eval_reflection_class_trait_alias_row( + out: &mut String, + alias_entries: &mut Vec<(String, usize, String, usize)>, + source_entries: &mut Vec<(String, usize, String, usize)>, + index: &mut usize, + class_name: &str, + alias: &str, + source: &str, +) { + let class_label = format!("_eval_reflection_class_trait_alias_class_{}", *index); + let alias_label = format!("_eval_reflection_class_trait_alias_name_{}", *index); + let source_label = format!("_eval_reflection_class_trait_alias_source_{}", *index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + alias_label, + escaped_ascii(alias) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + source_label, + escaped_ascii(source) + )); + alias_entries.push((class_label.clone(), class_name.len(), alias_label, alias.len())); + source_entries.push((class_label, class_name.len(), source_label, source.len())); + *index += 1; +} + /// Returns direct and inherited parent interface names for one generated interface. fn eval_reflection_interface_parent_names( interface_name: &str, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 4f6b5bd02c..4eb4dbff5a 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -184,6 +184,8 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emit_aarch64_eval_reflection_property_names(emitter); emit_aarch64_eval_reflection_class_interface_names(emitter); emit_aarch64_eval_reflection_class_trait_names(emitter); + emit_aarch64_eval_reflection_class_trait_alias_names(emitter); + emit_aarch64_eval_reflection_class_trait_alias_sources(emitter); emit_aarch64_eval_reflection_class_flags(emitter); emit_aarch64_eval_reflection_method_flags(emitter); emit_aarch64_eval_reflection_method_declaring_class(emitter); @@ -1645,6 +1647,8 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emit_x86_64_eval_reflection_property_names(emitter); emit_x86_64_eval_reflection_class_interface_names(emitter); emit_x86_64_eval_reflection_class_trait_names(emitter); + emit_x86_64_eval_reflection_class_trait_alias_names(emitter); + emit_x86_64_eval_reflection_class_trait_alias_sources(emitter); emit_x86_64_eval_reflection_class_flags(emitter); emit_x86_64_eval_reflection_method_flags(emitter); emit_x86_64_eval_reflection_method_declaring_class(emitter); @@ -3156,6 +3160,32 @@ fn emit_aarch64_eval_reflection_class_trait_names(emitter: &mut Emitter) { ); } +/// Emits the ARM64 eval hook that returns AOT ReflectionClass trait alias names. +fn emit_aarch64_eval_reflection_class_trait_alias_names(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_alias_names", + "_eval_reflection_class_trait_alias_count", + "_eval_reflection_class_trait_aliases", + "__elephc_eval_reflection_class_trait_alias_names", + "class trait alias", + 32, + ); +} + +/// Emits the ARM64 eval hook that returns AOT ReflectionClass trait alias sources. +fn emit_aarch64_eval_reflection_class_trait_alias_sources(emitter: &mut Emitter) { + emit_aarch64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_alias_sources", + "_eval_reflection_class_trait_alias_count", + "_eval_reflection_class_trait_alias_sources", + "__elephc_eval_reflection_class_trait_alias_sources", + "class trait alias source", + 32, + ); +} + /// Emits an ARM64 class-filtered AOT reflection member-name scanner. fn emit_aarch64_eval_reflection_member_names( emitter: &mut Emitter, @@ -3559,6 +3589,32 @@ fn emit_x86_64_eval_reflection_class_trait_names(emitter: &mut Emitter) { ); } +/// Emits the x86_64 eval hook that returns AOT ReflectionClass trait alias names. +fn emit_x86_64_eval_reflection_class_trait_alias_names(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_alias_names", + "_eval_reflection_class_trait_alias_count", + "_eval_reflection_class_trait_aliases", + "__elephc_eval_reflection_class_trait_alias_names_x86", + "class trait alias", + 32, + ); +} + +/// Emits the x86_64 eval hook that returns AOT ReflectionClass trait alias sources. +fn emit_x86_64_eval_reflection_class_trait_alias_sources(emitter: &mut Emitter) { + emit_x86_64_eval_reflection_member_names( + emitter, + "__elephc_eval_reflection_class_trait_alias_sources", + "_eval_reflection_class_trait_alias_count", + "_eval_reflection_class_trait_alias_sources", + "__elephc_eval_reflection_class_trait_alias_sources_x86", + "class trait alias source", + 32, + ); +} + /// Emits an x86_64 class-filtered AOT reflection member-name scanner. fn emit_x86_64_eval_reflection_member_names( emitter: &mut Emitter, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3b611e2950..35991ec042 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12430,6 +12430,34 @@ echo count($nestedTraits) . ":" . $nestedTraits[0];'); ); } +/// Verifies eval ReflectionClass exposes generated/AOT direct trait aliases. +#[test] +fn test_eval_reflection_class_get_trait_aliases_for_aot_class() { + let out = compile_and_run_capture( + r#"getTraitAliases(); +echo count($aliases) . ":" . $aliases["aliasOriginal"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalAotReflectAliasTrait::original" + ); +} + /// Verifies eval ReflectionClass::implementsInterface reports class, enum, and /// interface metadata through the bridge. #[test] From e38a1f0c48546e61a0dd6202a315ed837b951202 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 23:53:03 +0200 Subject: [PATCH 0802/1208] Expose AOT enum trait metadata to eval --- docs/php/eval.md | 11 ++-- src/ir_lower/program.rs | 5 +- src/types/checker/builtin_enums.rs | 4 ++ src/types/checker/driver/mod.rs | 10 ++++ src/types/checker/schema/enums.rs | 16 ++++-- src/types/result.rs | 33 +++++++++++ tests/codegen/eval.rs | 91 ++++++++++++++++++++++++++++++ 7 files changed, 157 insertions(+), 13 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index b460d01a52..732204b130 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -320,11 +320,12 @@ modifier bitmask for eval class-like metadata. and generated/AOT classes, plus parent interfaces for eval and generated/AOT interfaces. `ReflectionClass::getInterfaces()` materializes those names as a name-keyed array of `ReflectionClass` objects. `ReflectionClass::getTraitNames()` -returns traits used directly by eval and generated/AOT classes and traits, -`ReflectionClass::getTraits()` materializes those direct trait names as -`ReflectionClass` objects, and -`ReflectionClass::getTraitAliases()` exposes direct eval and generated/AOT trait -`as` aliases as PHP's alias-name to `Trait::method` map. +returns traits used directly by eval class-like symbols and generated/AOT +classes, traits, and enums. `ReflectionClass::getTraits()` materializes those +direct trait names as `ReflectionClass` objects, and +`ReflectionClass::getTraitAliases()` exposes direct eval class-like and +generated/AOT class/enum trait `as` aliases as PHP's alias-name to +`Trait::method` map. `ReflectionClass::implementsInterface()` checks those eval relations case-insensitively, returns true when reflecting the requested interface itself, and checks generated/AOT class-interface relations through runtime metadata. It diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 9eaa3a8829..22d8456a04 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -585,16 +585,13 @@ fn collect_declared_trait_names(program: &Program) -> Vec { names } -/// Collects direct trait-use declarations keyed by the declaring trait or enum name. +/// Collects direct trait-to-trait use declarations keyed by declaring trait name. fn collect_declared_trait_uses(program: &Program) -> HashMap> { let mut uses = HashMap::new(); for stmt in program { match &stmt.kind { StmtKind::TraitDecl { name, trait_uses, .. - } - | StmtKind::EnumDecl { - name, trait_uses, .. } => { uses.insert( name.clone(), diff --git a/src/types/checker/builtin_enums.rs b/src/types/checker/builtin_enums.rs index 6617aa0392..1ae00c6377 100644 --- a/src/types/checker/builtin_enums.rs +++ b/src/types/checker/builtin_enums.rs @@ -49,6 +49,8 @@ pub(crate) fn inject_builtin_enums( &[], &[], &[], + &[], + &[], checker, next_class_id, )?; @@ -74,6 +76,8 @@ pub(crate) fn inject_builtin_enums( &[], &[], &[], + &[], + &[], checker, next_class_id, ) diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 5dbd18e30b..7e68fb082b 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -244,6 +244,14 @@ pub(super) fn check_types_impl( .get(name) .map(|flattened| flattened.methods.as_slice()) .unwrap_or(methods.as_slice()); + let enum_used_traits = flattened_enums + .get(name) + .map(|flattened| flattened.used_traits.as_slice()) + .unwrap_or(&[]); + let enum_trait_aliases = flattened_enums + .get(name) + .map(|flattened| flattened.trait_aliases.as_slice()) + .unwrap_or(&[]); if let Err(error) = build_enum_info( name, backing_type.as_ref(), @@ -251,6 +259,8 @@ pub(super) fn check_types_impl( implements, enum_methods, constants, + enum_used_traits, + enum_trait_aliases, stmt.span, &mut checker, &mut next_class_id, diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index 0f9ae4ccf4..a94dd5da93 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -102,6 +102,7 @@ pub(crate) fn propagate_abstract_return_types(checker: &mut Checker) { /// - `backing_type`: optional `TypeExpr` for backed enums /// - `cases`: parsed enum case declarations /// - `span`: source location for error reporting +/// - `used_traits` / `trait_aliases`: flattened direct enum trait-use metadata /// - `checker`: type checker state (classes, interfaces, enums, resolve_type_expr) /// - `next_class_id`: incrementing class ID counter /// @@ -119,6 +120,8 @@ pub(crate) fn build_enum_info( implements: &[crate::names::Name], user_methods: &[crate::parser::ast::ClassMethod], user_constants: &[crate::parser::ast::ClassConst], + used_traits: &[String], + trait_aliases: &[(String, String)], span: crate::span::Span, checker: &mut Checker, next_class_id: &mut u64, @@ -237,6 +240,8 @@ pub(crate) fn build_enum_info( implements, user_methods, user_constants, + used_traits, + trait_aliases, checker, next_class_id, ) @@ -246,8 +251,9 @@ pub(crate) fn build_enum_info( /// /// Used by parsed enum declarations and builtin enum injection after case/backing /// validation has already happened. Synthesizes the static enum methods exposed -/// by PHP: all enums get `cases()`, while backed enums also get `from()` and -/// `tryFrom()`. +/// by PHP, and preserves flattened trait-use metadata for reflection/runtime +/// class-like queries. All enums get `cases()`, while backed enums also get +/// `from()` and `tryFrom()`. #[allow(clippy::too_many_arguments)] pub(crate) fn insert_enum_metadata( name: &str, @@ -256,6 +262,8 @@ pub(crate) fn insert_enum_metadata( implements: &[crate::names::Name], user_methods: &[ClassMethod], user_constants: &[crate::parser::ast::ClassConst], + used_traits: &[String], + trait_aliases: &[(String, String)], checker: &mut Checker, next_class_id: &mut u64, ) -> Result<(), CompileError> { @@ -443,8 +451,8 @@ pub(crate) fn insert_enum_metadata( property_attribute_args: HashMap::new(), constant_attribute_names, constant_attribute_args, - used_traits: Vec::new(), - trait_aliases: Vec::new(), + used_traits: used_traits.to_vec(), + trait_aliases: trait_aliases.to_vec(), properties, property_offsets, property_declaring_classes, diff --git a/src/types/result.rs b/src/types/result.rs index 9d6969e526..71dae9c32c 100644 --- a/src/types/result.rs +++ b/src/types/result.rs @@ -123,4 +123,37 @@ mod tests { .expect("mac type check failed"); assert_eq!(mac.required_libraries, vec!["elephc_crypto"]); } + + /// Verifies enum class metadata preserves flattened trait relation data for runtime reflection. + #[test] + fn test_enum_class_info_preserves_trait_metadata() { + let program = parse_program( + r#"getTraitNames(); +$aliases = $ref->getTraitAliases(); +echo count($traits) . ":" . implode(",", $traits) . ":"; +echo count($aliases) . ":" . $aliases["aliasOriginal"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "1:EvalAotReflectEnumTrait:1:EvalAotReflectEnumTrait::original" + ); +} + +/// Verifies generated enum trait metadata emits one runtime reflection row per direct trait. +#[test] +fn test_eval_reflection_class_trait_metadata_for_aot_enum_is_not_duplicated() { + if !codegen_fixture_uses_ir_backend() { + return; + } + let dir = make_cli_test_dir("elephc_eval_aot_enum_trait_metadata"); + let (user_asm, _runtime_asm, _required_libraries) = compile_source_to_asm_with_options( + r#"getTraitNames(); +$aliases = $ref->getTraitAliases(); +echo count($traits) . ":" . implode(",", $traits) . ":"; +echo count($aliases) . ":" . $aliases["aliasOriginal"];'); +"#, + &dir, + 8_388_608, + false, + false, + ); + let start = user_asm + .find("_eval_reflection_class_traits:\n") + .expect("missing trait metadata table"); + let count_start = user_asm + .find("_eval_reflection_class_trait_count:\n") + .expect("missing trait metadata count"); + let count_tail = &user_asm[count_start..start]; + let tail = &user_asm[start..]; + let end = tail + .find(".globl _eval_reflection_class_trait_alias_count") + .expect("missing trait alias metadata table"); + let trait_table = &tail[..end]; + + assert!( + count_tail.contains(" .quad 1\n"), + "unexpected trait table count:\n{count_tail}\n{trait_table}" + ); + + assert_eq!( + trait_table + .matches(".ascii \"EvalAotReflectTraitEnum\"") + .count(), + 1, + "{trait_table}" + ); +} + /// Verifies eval ReflectionClass::implementsInterface reports class, enum, and /// interface metadata through the bridge. #[test] From c94358f1449523ffe4d22a81a8421d44b0305b02 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 26 Jun 2026 23:59:43 +0200 Subject: [PATCH 0803/1208] Initialize eval-visible AOT enum cases --- src/codegen/mod.rs | 23 +++++++++++++++++++++++ tests/codegen/eval.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 1f3083bb9f..0a51e5e705 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -548,6 +548,7 @@ fn runtime_referenced_enum_singleton_names(module: &Module) -> HashSet { collect_reflection_enum_singleton_name(module, function, inst, &mut names); } } + seed_eval_visible_enum_singleton_names(module, &mut names); names } @@ -564,6 +565,28 @@ fn all_runtime_scanned_functions(module: &Module) -> impl Iterator) { + if !module_contains_eval_state(module) { + return; + } + names.extend(module.enum_infos.keys().cloned()); +} + +/// Returns true when any scanned function owns persistent eval runtime state. +fn module_contains_eval_state(module: &Module) -> bool { + all_runtime_scanned_functions(module).any(|function| { + function.locals.iter().any(|local| { + matches!( + local.kind, + crate::ir::LocalKind::EvalContext + | crate::ir::LocalKind::EvalScope + | crate::ir::LocalKind::EvalGlobalScope + ) + }) + }) +} + /// Adds enum names referenced by `Enum::Case` scoped constant reads. fn collect_scoped_constant_enum_singleton_name( module: &Module, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 600dfa1994..8d75cab384 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12493,6 +12493,36 @@ echo count($aliases) . ":" . $aliases["aliasOriginal"];'); ); } +/// Verifies eval can fetch a generated/AOT enum case only referenced inside eval source. +#[test] +fn test_eval_fetches_aot_enum_case_object_from_eval_only_reference() { + if !codegen_fixture_uses_ir_backend() { + return; + } + let out = compile_and_run_capture( + r#" Date: Sat, 27 Jun 2026 00:01:51 +0200 Subject: [PATCH 0804/1208] Document eval class_uses enum metadata --- docs/php/eval.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 732204b130..358d7b1fd6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -615,7 +615,7 @@ generated AOT class/interface metadata and eval-created object metadata. AOT metadata. `class_implements()` and `class_parents()` materialize generated/AOT interface and parent metadata when the bridge exposes it. `class_uses()` reports direct trait uses for eval-declared and generated/AOT -classes and traits. `class_alias()` can +classes, traits, and enums. `class_alias()` can alias eval-declared and generated/AOT classes, interfaces, traits, and enums, preserving the target class-like kind for the corresponding metadata probes. Top-level compiled `class_alias()` From 4596511c747fc7c3c0b7a4d88a76e6a2718a120e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 00:04:05 +0200 Subject: [PATCH 0805/1208] Cover eval class_uses for AOT enum names --- tests/codegen/eval.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8d75cab384..94e974558d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12512,7 +12512,10 @@ eval('$case = EvalAotCaseEnum::Ready; echo get_class($case) . ":"; $uses = class_uses($case); ksort($uses); -echo count($uses) . ":" . $uses["EvalAotCaseTrait"];'); +echo count($uses) . ":" . $uses["EvalAotCaseTrait"] . ":"; +$stringUses = class_uses("EvalAotCaseEnum"); +ksort($stringUses); +echo count($stringUses) . ":" . $stringUses["EvalAotCaseTrait"];'); "#, ); assert!( @@ -12520,7 +12523,10 @@ echo count($uses) . ":" . $uses["EvalAotCaseTrait"];'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "EvalAotCaseEnum:1:EvalAotCaseTrait"); + assert_eq!( + out.stdout, + "EvalAotCaseEnum:1:EvalAotCaseTrait:1:EvalAotCaseTrait" + ); } /// Verifies generated enum trait metadata emits one runtime reflection row per direct trait. From 8614f4247cd264e6afb2fa51c14505a41e2d3ba3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 00:16:26 +0200 Subject: [PATCH 0806/1208] Update eval reflection class docs --- docs/php/classes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 2f6ef657de..53e6b73896 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1374,7 +1374,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper materializes supported literal and array values directly, but symbolic references are resolved through `ReflectionClass::getAttributes()->getArguments()` / `ReflectionAttribute::newInstance()` instead. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` forwards direct positional arguments and statically known indexed unpacking to the reflected constructor. `ReflectionClass::newInstanceArgs()` forwards indexed and string-keyed argument arrays to the reflected constructor. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation is limited to reflected user functions whose parameters have declared types, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` constructs eval-declared and bridge-supported generated/AOT reflected classes with public constructors and forwards positional, named, unpacked, and defaulted constructor arguments through eval's call binding. `ReflectionClass::newInstanceArgs()` constructs those reflected classes from indexed or string-keyed argument arrays, including arrays built at eval runtime; string keys become named constructor arguments. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation supports reflected user functions with declared or inferred/untyped parameter contracts, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionObject` supports the inherited class-metadata methods for object arguments and materializes the same metadata through a `ReflectionObject` instance. - `ReflectionEnum` additionally supports backing metadata (`isBacked()`, `getBackingType()`); inside eval fragments it also supports enum-case lookup (`hasCase()`, `getCase()`, `getCases()`). Enum-case reflectors expose `getEnum()`. - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. From da5b7a6405159b585c6571d1dffaf0bfe1d49913 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 00:19:23 +0200 Subject: [PATCH 0807/1208] Document eval enum case reflection APIs --- docs/php/classes.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/php/classes.md b/docs/php/classes.md index 53e6b73896..8d51a33b98 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1295,6 +1295,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionClassConstant::isProtected()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant is protected; enum cases report `false` | | `ReflectionClassConstant::isPrivate()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class constant is private; enum cases report `false` | | `ReflectionClassConstant::isFinal()` | Same as `ReflectionClassConstant::getName()` | Return whether the reflected class/interface/trait/enum constant is final; enum cases report `false` | +| `ReflectionClassConstant::isDeprecated()` | Same as `ReflectionClassConstant::getName()` | Return PHP's current non-deprecated default for retained class-constant metadata | +| `ReflectionClassConstant::hasType()` | Same as `ReflectionClassConstant::getName()` | Return `false` because elephc's current class-constant metadata is untyped | +| `ReflectionClassConstant::getType()` | Same as `ReflectionClassConstant::getName()` | Return `null` for the current untyped class-constant metadata | | `ReflectionClassConstant::getModifiers()` | Same as `ReflectionClassConstant::getName()` | Return PHP's `ReflectionClassConstant::IS_*` visibility/finality bitmask | | `ReflectionEnumUnitCase::getName()` / `ReflectionEnumBackedCase::getName()` | `new ReflectionEnumUnitCase($enum_name, $case_name)` or `new ReflectionEnumBackedCase($enum_name, $case_name)` | Return the reflected enum-case name | | `ReflectionEnumUnitCase::getValue()` / `ReflectionEnumBackedCase::getValue()` | Same as enum-case `getName()` | Return the reflected enum-case object | @@ -1302,6 +1305,9 @@ echo ($instance instanceof Route) ? "yes" : "no"; | `ReflectionEnumUnitCase::getAttributes()` / `ReflectionEnumBackedCase::getAttributes()` | Same as enum-case `getName()` | Return `ReflectionAttribute` objects for enum-case attributes | | `ReflectionEnumUnitCase::getDeclaringClass()` / `ReflectionEnumBackedCase::getDeclaringClass()` | Same as enum-case `getName()` | Return a `ReflectionClass` object for the enum that declares the reflected case | | `ReflectionEnumUnitCase::getEnum()` / `ReflectionEnumBackedCase::getEnum()` | Same as enum-case `getName()` | Return a `ReflectionEnum` object for the enum that declares the reflected case | +| `ReflectionEnumUnitCase::isDeprecated()` / `ReflectionEnumBackedCase::isDeprecated()` | Same as enum-case `getName()` | Return PHP's current non-deprecated default for retained enum-case metadata | +| `ReflectionEnumUnitCase::hasType()` / `ReflectionEnumBackedCase::hasType()` | Same as enum-case `getName()` | Return `false` because enum cases expose no class-constant type metadata | +| `ReflectionEnumUnitCase::getType()` / `ReflectionEnumBackedCase::getType()` | Same as enum-case `getName()` | Return `null` for enum-case type metadata | | `ReflectionAttribute::newInstance()` | Internal only | Instantiate the attribute class from captured literal positional/named args | Functions and their parameters can also be reflected. `ReflectionFunction` reads @@ -1374,7 +1380,7 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper materializes supported literal and array values directly, but symbolic references are resolved through `ReflectionClass::getAttributes()->getArguments()` / `ReflectionAttribute::newInstance()` instead. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` constructs eval-declared and bridge-supported generated/AOT reflected classes with public constructors and forwards positional, named, unpacked, and defaulted constructor arguments through eval's call binding. `ReflectionClass::newInstanceArgs()` constructs those reflected classes from indexed or string-keyed argument arrays, including arrays built at eval runtime; string keys become named constructor arguments. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation supports reflected user functions with declared or inferred/untyped parameter contracts, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` constructs eval-declared and bridge-supported generated/AOT reflected classes with public constructors and forwards positional, named, unpacked, and defaulted constructor arguments through eval's call binding. `ReflectionClass::newInstanceArgs()` constructs those reflected classes from indexed or string-keyed argument arrays, including arrays built at eval runtime; string keys become named constructor arguments. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation supports reflected user functions with declared or inferred/untyped parameter contracts, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isDeprecated()`, `hasType()`, `getType()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. The enum-case reflectors expose the same `isDeprecated()`, `hasType()`, and `getType()` defaults as `ReflectionClassConstant`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionObject` supports the inherited class-metadata methods for object arguments and materializes the same metadata through a `ReflectionObject` instance. - `ReflectionEnum` additionally supports backing metadata (`isBacked()`, `getBackingType()`); inside eval fragments it also supports enum-case lookup (`hasCase()`, `getCase()`, `getCases()`). Enum-case reflectors expose `getEnum()`. - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. From 0d0f21235cd7ba5400610869aba9ba2a04c68712 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 00:37:51 +0200 Subject: [PATCH 0808/1208] Support eval ReflectionFunction AOT defaults --- crates/elephc-magician/src/context.rs | 38 +++ .../src/ffi/native_functions.rs | 246 +++++++++++++++++- .../elephc-magician/src/ffi/native_methods.rs | 6 +- crates/elephc-magician/src/ffi/tests.rs | 14 +- .../interpreter/builtins/registry/callable.rs | 9 +- .../src/interpreter/dynamic_functions.rs | 56 +++- .../src/interpreter/expressions.rs | 20 -- docs/php/classes.md | 2 +- docs/php/eval.md | 8 +- src/codegen/lower_inst/builtins/eval.rs | 108 ++++++++ tests/codegen/eval.rs | 24 ++ 11 files changed, 489 insertions(+), 42 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 3fc01b8b7b..c2e9149eb1 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -91,6 +91,7 @@ pub struct NativeFunction { invoker: NativeFunctionInvoker, param_count: usize, param_names: Vec, + param_defaults: Vec>, } impl NativeFunction { @@ -105,6 +106,7 @@ impl NativeFunction { invoker, param_count, param_names: Vec::new(), + param_defaults: Vec::new(), } } @@ -130,6 +132,30 @@ impl NativeFunction { &self.param_names } + /// Records a PHP default value for one positional callback slot. + pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { + if index >= self.param_count { + return false; + } + if self.param_defaults.len() < self.param_count { + self.param_defaults.resize(self.param_count, None); + } + self.param_defaults[index] = Some(default); + true + } + + /// Returns the registered default for one parameter slot, if any. + pub fn param_default(&self, index: usize) -> Option<&NativeCallableDefault> { + self.param_defaults.get(index).and_then(Option::as_ref) + } + + /// Returns the minimum number of required parameters implied by defaults. + pub fn required_param_count(&self) -> usize { + (0..self.param_count) + .rfind(|index| self.param_default(*index).is_none()) + .map_or(0, |index| index + 1) + } + /// Invokes the descriptor-compatible callback with a boxed Mixed arg array. /// /// # Safety @@ -2107,6 +2133,18 @@ impl ElephcEvalContext { .is_some_and(|function| function.set_param_name(index, param_name)) } + /// Records one parameter default for an already registered native AOT callback. + pub fn define_native_function_param_default( + &mut self, + function_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_param_default(index, default)) + } + /// Defines native AOT instance-method signature metadata for eval named-argument binding. pub fn define_native_method_signature( &mut self, diff --git a/crates/elephc-magician/src/ffi/native_functions.rs b/crates/elephc-magician/src/ffi/native_functions.rs index ce81e919b6..3bfeb465e8 100644 --- a/crates/elephc-magician/src/ffi/native_functions.rs +++ b/crates/elephc-magician/src/ffi/native_functions.rs @@ -1,7 +1,7 @@ //! Purpose: //! Exports registration of generated native PHP callbacks into an eval context. //! Eval fragments use this metadata to call AOT functions through descriptor -//! invokers while preserving PHP-visible parameter names. +//! invokers while preserving PHP-visible parameter names and defaults. //! //! Called from: //! - Generated EIR backend assembly before fragments can call AOT functions. @@ -10,9 +10,12 @@ //! - Invalid names, handles, descriptors, or indexes fail closed as `false`. //! - Function names are stored under their PHP case-insensitive folded key. +use super::native_methods::{ + native_callable_array_default, native_callable_object_default, native_callable_scalar_default, +}; use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ABI_VERSION}; -use crate::context::{NativeFunction, NativeFunctionInvoker}; +use crate::context::{NativeCallableDefault, NativeFunction, NativeFunctionInvoker}; use std::ffi::c_void; /// Registers a generated native PHP function callback in an eval context. @@ -63,6 +66,114 @@ pub unsafe extern "C" fn __elephc_eval_register_native_function_param( .unwrap_or(0) } +/// Registers one generated native PHP function scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_scalar( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_scalar_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_string( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_string_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_object( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_object_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_array( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_array_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + /// Runs the native registration ABI body after installing a panic boundary. /// /// # Safety @@ -133,3 +244,134 @@ unsafe fn register_native_function_param_inner( param_name, )) } + +/// Runs native function scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_scalar`; invalid +/// handles, names, indexes, or default kinds fail closed as `false`. +unsafe fn register_native_function_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Runs native function string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_string`; invalid +/// handles, names, or indexes fail closed as `false`. +unsafe fn register_native_function_param_default_string_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Runs native function object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_object`; invalid +/// handles, names, indexes, or object specs fail closed as `false`. +unsafe fn register_native_function_param_default_object_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Runs native function array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_array`; invalid +/// handles, names, indexes, or array specs fail closed as `false`. +unsafe fn register_native_function_param_default_array_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Records a native function parameter default by folded function name. +/// +/// # Safety +/// `ctx` and `function_name_ptr` must be valid for their declared use; callers +/// are the exported ABI wrappers above. +unsafe fn register_native_function_param_default_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_function_param_default( + &function_name.to_ascii_lowercase(), + param_index, + default, + )) +} diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index 2e70c35f47..f6e79f5855 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -1978,7 +1978,7 @@ fn native_attribute_take_bytes<'a>( } /// Decodes tagged default kind/payload ABI fields into native callable metadata. -fn native_callable_scalar_default( +pub(super) fn native_callable_scalar_default( default_kind: u64, default_payload: u64, ) -> Option { @@ -1998,7 +1998,7 @@ fn native_callable_scalar_default( /// /// # Safety /// `spec_ptr` must be readable for `spec_len` bytes when non-null. -unsafe fn native_callable_object_default( +pub(super) unsafe fn native_callable_object_default( spec_ptr: *const u8, spec_len: u64, ) -> Option { @@ -2013,7 +2013,7 @@ unsafe fn native_callable_object_default( /// /// # Safety /// `spec_ptr` must be readable for `spec_len` bytes when non-null. -unsafe fn native_callable_array_default( +pub(super) unsafe fn native_callable_array_default( spec_ptr: *const u8, spec_len: u64, ) -> Option { diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 772c4c7cee..3ab759a5bb 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -396,7 +396,7 @@ fn dynamic_class_exists_reports_declared_eval_class() { assert_eq!(missing_result, 0); } -/// Verifies native AOT registration records function and parameter metadata. +/// Verifies native AOT registration records function parameter metadata and defaults. #[test] fn register_native_function_reports_function_exists() { let mut ctx = ElephcEvalContext::new(); @@ -424,6 +424,16 @@ fn register_native_function_reports_function_exists() { param.len() as u64, ) }; + let param_default_registered = unsafe { + __elephc_eval_register_native_function_param_default_scalar( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + TEST_NATIVE_DEFAULT_INT, + 42, + ) + }; let exists = unsafe { __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) }; assert_eq!(registered, 1); @@ -432,8 +442,10 @@ fn register_native_function_reports_function_exists() { .expect("native function should be registered"); assert_eq!(param_registered, 1); + assert_eq!(param_default_registered, 1); assert_eq!(exists, 1); assert_eq!(native.param_names(), &["value".to_string()]); + assert_eq!(native.param_default(0), Some(&NativeCallableDefault::Int(42))); } /// Verifies native AOT method registration records instance/static/constructor parameters. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 10b3eddd05..2ac07b15c2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -304,6 +304,9 @@ pub(in crate::interpreter) fn eval_callable_with_values( return eval_dynamic_function_with_values(&function, evaluated_args, context, values); } if let Some(function) = context.native_function(name) { + let evaluated_args = positional_args(evaluated_args); + let evaluated_args = + bind_evaluated_native_function_args(&function, evaluated_args, context, values)?; return eval_native_function_with_values(function, evaluated_args, values); } Err(EvalStatus::UnsupportedConstruct) @@ -336,10 +339,8 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( ); } if let Some(function) = context.native_function(name) { - if function.param_names().len() != function.param_count() { - return Err(EvalStatus::RuntimeFatal); - } - let evaluated_args = bind_evaluated_function_args(function.param_names(), evaluated_args)?; + let evaluated_args = + bind_evaluated_native_function_args(&function, evaluated_args, context, values)?; return eval_native_function_with_values(function, evaluated_args, values); } Err(EvalStatus::UnsupportedConstruct) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 5d4bd64a66..9c997ea96d 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -22,16 +22,16 @@ pub(in crate::interpreter) fn eval_dynamic_function( eval_dynamic_function_with_evaluated_args(function, evaluated_args, context, values) } -/// Evaluates and binds function-like arguments to parameter order. -pub(in crate::interpreter) fn eval_function_call_args( - params: &[String], +/// Evaluates and binds native AOT function arguments, filling registered defaults. +pub(in crate::interpreter) fn eval_native_function_call_args( + function: &NativeFunction, args: &[EvalCallArg], context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let evaluated_args = eval_call_arg_values(args, context, caller_scope, values)?; - bind_evaluated_function_args(params, evaluated_args) + bind_evaluated_native_function_args(function, evaluated_args, context, values) } /// Evaluates source-order call arguments while preserving named-argument metadata. @@ -323,6 +323,47 @@ pub(in crate::interpreter) fn bind_evaluated_function_args( .ok_or(EvalStatus::RuntimeFatal) } +/// Binds already evaluated native AOT function args and fills omitted defaults. +pub(in crate::interpreter) fn bind_evaluated_native_function_args( + function: &NativeFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; function.param_count()]; + let has_param_names = function.param_names().len() == function.param_count(); + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + if !has_param_names { + return Err(EvalStatus::RuntimeFatal); + } + bind_dynamic_named_arg(function.param_names(), &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + for (position, value) in bound_args.iter_mut().enumerate() { + if value.is_some() { + continue; + } + if position < function.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = function.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *value = Some(materialize_native_callable_default(default, context, values)?); + } + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + /// Binds evaluated method arguments and fills omitted parameters from defaults. pub(in crate::interpreter) fn bind_evaluated_method_args( params: &[String], @@ -1291,11 +1332,8 @@ pub(super) fn eval_native_function( caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let evaluated_args = if function.param_names().len() == function.param_count() { - eval_function_call_args(function.param_names(), args, context, caller_scope, values)? - } else { - eval_positional_call_arg_values(args, context, caller_scope, values)? - }; + let evaluated_args = + eval_native_function_call_args(&function, args, context, caller_scope, values)?; eval_native_function_with_values(function, evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 03ea80eb8c..ac7740356e 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -666,26 +666,6 @@ pub(in crate::interpreter) fn positional_call_arg_exprs( Ok(args.iter().map(|arg| arg.value().clone()).collect()) } -/// Evaluates a positional-only call argument list in source order. -pub(in crate::interpreter) fn eval_positional_call_arg_values( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if args - .iter() - .any(|arg| arg.name().is_some() || arg.is_spread()) - { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg.value(), context, scope, values)?); - } - Ok(evaluated_args) -} - /// Evaluates method-call arguments, preserving named metadata for eval method binding. pub(in crate::interpreter) fn eval_method_call_arg_values( args: &[EvalCallArg], diff --git a/docs/php/classes.md b/docs/php/classes.md index 8d51a33b98..4327a933bb 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1386,7 +1386,7 @@ Limitations today: - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Runtime-held static `ReflectionProperty` objects can read `getValue()` and `isInitialized()` from the materialized declaring-class static-property snapshot. Live refresh after reflector construction and dynamic static `setValue()` still require richer runtime metadata. -- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Runtime-only or non-literal argument arrays still require richer runtime/typechecker support. +- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Inside eval fragments, runtime-held generated/AOT function reflectors can also invoke registered generated functions through the native bridge with parameter names, supported defaults, named arguments, and indexed or string-keyed runtime argument arrays. Runtime-only generated/AOT function invocation outside eval still requires richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index 358d7b1fd6..65b7e4f13a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -469,8 +469,12 @@ accepts eval free-function names, class/interface/trait method arrays, and object-method arrays resolved from the evaluated runtime object, including inline `new` expressions. `ReflectionFunction::invoke()` and `invokeArgs()` dispatch eval-declared functions with the same named/default/variadic argument -binding used by direct eval function calls; generated/AOT function invocation and supported callable-builtin invocation are -covered by the general Reflection support documented in `docs/php/classes.md`. +binding used by direct eval function calls. Runtime-held generated/AOT +`ReflectionFunction` objects can invoke registered generated functions through +the native bridge with parameter names, supported defaults, named arguments, and +indexed or string-keyed runtime argument arrays. Supported callable-builtin +invocation is covered by the general Reflection support documented in +`docs/php/classes.md`. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, `isDefaultValueConstant()`, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 9b1e3f37c3..f72c8e6620 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -1555,6 +1555,19 @@ fn register_eval_native_function( param_name, ); } + for (index, default) in registration.signature.defaults.iter().enumerate() { + let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { + continue; + }; + register_eval_native_function_param_default( + ctx, + context_offset, + &name_label, + name_len, + index, + &default, + ); + } Ok(()) } @@ -2647,6 +2660,101 @@ fn register_eval_native_function_param( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native function parameter-default registration call. +fn register_eval_native_function_param_default( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + param_index: usize, + default: &EvalNativeCallableDefault, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let symbol = match default { + EvalNativeCallableDefault::Scalar { kind, payload } => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + *kind, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + *payload, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_scalar") + } + EvalNativeCallableDefault::String(value) => { + let (default_label, default_len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_string") + } + EvalNativeCallableDefault::Object { .. } => { + let spec = encode_eval_native_object_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_object") + } + EvalNativeCallableDefault::Array(_) => { + let spec = encode_eval_native_array_default(default); + let (default_label, default_len) = ctx.data.add_string(&spec); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &default_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + default_len as i64, + ); + ctx.emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_default_array") + } + }; + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Loads the persistent eval context local into the selected integer argument register. fn load_eval_context_local_to_arg( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 94e974558d..cb75150044 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -17115,6 +17115,30 @@ echo $mutate->invoke($value) . ":" . $value;'); assert_eq!(out.stdout, "121X:341Y:Q!:Q"); } +/// Verifies eval ReflectionFunction::invokeArgs accepts runtime-built AOT argument arrays. +#[test] +fn test_eval_reflection_function_invoke_args_accepts_runtime_aot_arg_arrays() { + let out = compile_and_run_capture( + r#"invokeArgs($args) . ":"; +return $ref->invoke(left: "Q");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "XY:QB"); +} + /// Verifies eval ReflectionClass::isCloneable uses eval class metadata through the bridge. #[test] fn test_eval_reflection_class_cloneable_predicate() { From 276cfb8c17e37b2c6a6f8c1aa1219e4b3b287911 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 00:48:37 +0200 Subject: [PATCH 0809/1208] Reflect eval AOT function defaults --- .../src/interpreter/reflection.rs | 74 ++++++++++--------- docs/php/classes.md | 4 +- docs/php/eval.md | 7 +- tests/codegen/eval.rs | 31 ++++++++ 4 files changed, 77 insertions(+), 39 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 831ca6cac2..f3491006dd 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3095,24 +3095,8 @@ fn eval_reflection_function_new( } if let Some(function) = context.native_function(&lookup_name) { let reflected_name = requested_name.trim_start_matches('\\'); - let parameter_names = eval_reflection_native_function_parameter_names(&function); - let parameter_attributes = vec![Vec::new(); parameter_names.len()]; - let parameter_types: Vec> = vec![None; parameter_names.len()]; - let parameter_defaults = vec![None; parameter_names.len()]; - let parameter_is_by_ref = vec![false; parameter_names.len()]; - let parameter_is_variadic = vec![false; parameter_names.len()]; - let required_parameter_count = - eval_reflection_required_parameter_count(¶meter_defaults, ¶meter_is_variadic); - let parameters = eval_reflection_function_parameters( - reflected_name, - ¶meter_names, - Vec::new(), - ¶meter_attributes, - ¶meter_types, - ¶meter_defaults, - ¶meter_is_by_ref, - ¶meter_is_variadic, - ); + let required_parameter_count = function.required_param_count(); + let parameters = eval_reflection_native_function_parameters(reflected_name, &function); return eval_reflection_function_object_result( reflected_name, &[], @@ -3140,6 +3124,43 @@ fn eval_reflection_native_function_parameter_names(function: &NativeFunction) -> .collect() } +/// Builds ReflectionParameter metadata for one registered native AOT function. +fn eval_reflection_native_function_parameters( + function_name: &str, + function: &NativeFunction, +) -> Vec { + let parameter_names = eval_reflection_native_function_parameter_names(function); + let parameter_count = parameter_names.len(); + let parameter_attributes = vec![Vec::new(); parameter_count]; + let parameter_types: Vec> = vec![None; parameter_count]; + let parameter_defaults = eval_reflection_native_function_parameter_defaults(function); + let parameter_is_by_ref = vec![false; parameter_count]; + let parameter_is_variadic = vec![false; parameter_count]; + eval_reflection_function_parameters( + function_name, + ¶meter_names, + Vec::new(), + ¶meter_attributes, + ¶meter_types, + ¶meter_defaults, + ¶meter_is_by_ref, + ¶meter_is_variadic, + ) +} + +/// Converts registered native function defaults into eval constant expressions. +fn eval_reflection_native_function_parameter_defaults( + function: &NativeFunction, +) -> Vec> { + (0..function.param_count()) + .map(|index| { + function + .param_default(index) + .map(eval_reflection_native_callable_default_expr) + }) + .collect() +} + /// Builds one `ReflectionFunction` object from retained eval function metadata. fn eval_reflection_function_object_result( function_name: &str, @@ -3347,22 +3368,7 @@ fn eval_reflection_function_parameter_metadata( } if let Some(function) = context.native_function(&lookup_name) { let reflected_name = requested_name.trim_start_matches('\\'); - let parameter_names = eval_reflection_native_function_parameter_names(&function); - let parameter_attributes = vec![Vec::new(); parameter_names.len()]; - let parameter_types: Vec> = vec![None; parameter_names.len()]; - let parameter_defaults = vec![None; parameter_names.len()]; - let parameter_is_by_ref = vec![false; parameter_names.len()]; - let parameter_is_variadic = vec![false; parameter_names.len()]; - let parameters = eval_reflection_function_parameters( - reflected_name, - ¶meter_names, - Vec::new(), - ¶meter_attributes, - ¶meter_types, - ¶meter_defaults, - ¶meter_is_by_ref, - ¶meter_is_variadic, - ); + let parameters = eval_reflection_native_function_parameters(reflected_name, &function); return Ok(eval_reflection_parameter_for_selector(parameters, selector)); } Ok(None) diff --git a/docs/php/classes.md b/docs/php/classes.md index 4327a933bb..4f12786a7c 100644 --- a/docs/php/classes.md +++ b/docs/php/classes.md @@ -1380,13 +1380,13 @@ Limitations today: - A symbolic reference that elephc cannot resolve - for example a built-in class constant such as `Attribute::TARGET_CLASS`, which is not registered - is treated as unsupported metadata: the attribute still parses and compiles and `class_attribute_names()` still lists it, but its arguments are not reflectable through `getAttributes()`/`class_get_attributes()`/`class_attribute_args()`. - The flat `class_attribute_args()` helper materializes supported literal and array values directly, but symbolic references are resolved through `ReflectionClass::getAttributes()->getArguments()` / `ReflectionAttribute::newInstance()` instead. - When several attributes share a name on the same class, `class_attribute_args()` returns the args of the first match; `class_get_attributes()` does expose every occurrence as a separate `ReflectionAttribute` in source order. -- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` constructs eval-declared and bridge-supported generated/AOT reflected classes with public constructors and forwards positional, named, unpacked, and defaulted constructor arguments through eval's call binding. `ReflectionClass::newInstanceArgs()` constructs those reflected classes from indexed or string-keyed argument arrays, including arrays built at eval runtime; string keys become named constructor arguments. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation supports reflected user functions with declared or inferred/untyped parameter contracts, plus supported callable builtins. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isDeprecated()`, `hasType()`, `getType()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. The enum-case reflectors expose the same `isDeprecated()`, `hasType()`, and `getType()` defaults as `ReflectionClassConstant`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. +- `ReflectionClass` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `getAttributes()`, `isFinal()`, `isAbstract()`, `isInterface()`, `isTrait()`, `isEnum()`, `isReadOnly()`, `isAnonymous()`, `isInstantiable()`, `isCloneable()`, `isIterable()`, `isIterateable()`, `isInternal()`, `isUserDefined()`, `getModifiers()`, `hasMethod()`, `hasProperty()`, `hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getReflectionConstant()`, `getReflectionConstants()`, `implementsInterface()`, `isSubclassOf()`, `isInstance()`, `getInterfaceNames()`, `getInterfaces()`, `getTraitNames()`, `getTraits()`, `getTraitAliases()`, `getMethods()`, `getConstructor()`, `getParentClass()`, `getProperties()`, `newInstance()`, `newInstanceArgs()`, and `newInstanceWithoutConstructor()`. Its constructor accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. It also exposes PHP-compatible `ReflectionClass::IS_IMPLICIT_ABSTRACT`, `ReflectionClass::IS_FINAL`, `ReflectionClass::IS_EXPLICIT_ABSTRACT`, and `ReflectionClass::IS_READONLY` constants. `ReflectionClass::implementsInterface()` returns the metadata predicate for known interface names and throws PHP-compatible `ReflectionException` messages when the argument is missing or names a non-interface class-like symbol. `ReflectionClass::isSubclassOf()` returns the metadata predicate for known class/interface targets, excludes the reflected symbol itself, returns `false` for trait and enum targets, and throws PHP-compatible `ReflectionException` messages for missing names. `ReflectionClass::isInstance()` checks the provided object against the reflected class/interface/enum metadata and returns `false` for trait targets. `ReflectionClass::getDefaultProperties()` returns supported materialized property defaults, including static and instance properties, omits typed properties without an initializer, and reports implicit `null` for untyped concrete properties without an explicit initializer. `ReflectionClass::newInstance()` constructs eval-declared and bridge-supported generated/AOT reflected classes with public constructors and forwards positional, named, unpacked, and defaulted constructor arguments through eval's call binding. `ReflectionClass::newInstanceArgs()` constructs those reflected classes from indexed or string-keyed argument arrays, including arrays built at eval runtime; string keys become named constructor arguments. When the receiver is an inline `new ReflectionClass(Known::class)` expression, named arguments, static string-keyed unpacking, and constructor defaults are normalized through the reflected constructor signature. `ReflectionClass::newInstanceWithoutConstructor()` allocates concrete non-enum reflected classes with supported property defaults while skipping `__construct()`; specialized internal classes with runtime-owned storage still require dedicated support. Dynamic unpacking and named-argument forwarding from a `ReflectionClass` object held only in runtime storage still require richer runtime constructor binding. `ReflectionFunction` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `getClosureUsedVariables()`, `invoke()`, `invokeArgs()`, and `isDisabled()` for statically known user functions and supported first-class-callable builtins. AOT invocation supports reflected user functions with declared or inferred/untyped parameter contracts, plus supported callable builtins. Inside eval fragments, registered generated/AOT free-function defaults are reflected through `ReflectionParameter` metadata. `ReflectionMethod` construction accepts class-name strings and object arguments; object arguments resolve to the runtime class before method lookup. `ReflectionMethod` supports `getName()`, `getShortName()`, `getNamespaceName()`, `inNamespace()`, `isInternal()`, `isUserDefined()`, `isClosure()`, `isDeprecated()`, `returnsReference()`, `isGenerator()`, `hasPrototype()`, `getPrototype()`, `getDeclaringClass()`, `getAttributes()`, `getParameters()`, `getNumberOfParameters()`, `getNumberOfRequiredParameters()`, `hasReturnType()`, `getReturnType()`, `isVariadic()`, `hasTentativeReturnType()`, `getTentativeReturnType()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isConstructor()`, `isDestructor()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionMethod::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, and `IS_ABSTRACT` constants. `ReflectionParameter` supports direct construction for statically known user function names or supported callable-builtin names (`"function"`), class/interface/trait method arrays (`[ClassLike::class, "method"]`), and statically typed object-expression method arrays (`[$object_expr, "method"]`) outside eval; those object expressions are still evaluated before reflection metadata is materialized. Inside eval fragments it can also resolve object-method arrays from evaluated runtime objects, including inline `new` expressions. It also supports `getName()`, `getPosition()`, `isOptional()`, `isVariadic()`, `isPassedByReference()`, `canBePassedByValue()`, `isPromoted()`, `hasType()`, `getType()` for simple named, union, and intersection parameter types, `allowsNull()`, `isArray()`, `isCallable()`, `getAttributes()` for function and method parameter attributes, `getDeclaringClass()`, `getDeclaringFunction()`, `isDefaultValueAvailable()`, `getDefaultValue()`, `isDefaultValueConstant()`, and `getDefaultValueConstantName()` for supported scalar/null/class-constant/default-array metadata and object defaults with up to eight supported scalar/null constructor arguments from literals or constants, including omitted constructor defaults. `ReflectionNamedType` supports `getName()`, `allowsNull()`, and `isBuiltin()`. `ReflectionUnionType` and `ReflectionIntersectionType` support `getTypes()` and `allowsNull()`. `ReflectionProperty` supports `getName()`, `getDeclaringClass()`, `getAttributes()`, `isStatic()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isAbstract()`, `isReadOnly()`, `isPromoted()`, `isProtectedSet()`, `isPrivateSet()`, `getModifiers()`, `isDefault()`, `isDynamic()`, `isLazy()`, `skipLazyInitialization()`, `__toString()`, `hasHooks()`, `getHooks()`, `hasHook()`, `getHook()`, `hasType()`, `getType()`, `getSettableType()`, `hasDefaultValue()`, `getDefaultValue()` for supported property defaults, and `isInitialized()` for supported known property slots. It also exposes PHP-compatible `ReflectionProperty::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_STATIC`, `IS_FINAL`, `IS_ABSTRACT`, `IS_READONLY`, `IS_VIRTUAL`, `IS_PROTECTED_SET`, and `IS_PRIVATE_SET` constants. `ReflectionClassConstant` supports `getName()`, `getValue()`, `getAttributes()`, `getDeclaringClass()`, `isEnumCase()`, `isPublic()`, `isProtected()`, `isPrivate()`, `isFinal()`, `isDeprecated()`, `hasType()`, `getType()`, and `getModifiers()`. It also exposes PHP-compatible `ReflectionClassConstant::IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, and `IS_FINAL` constants. `ReflectionEnumUnitCase` and `ReflectionEnumBackedCase` support `getName()`, `getValue()`, `getAttributes()`, and `getDeclaringClass()`, and `ReflectionEnumBackedCase` also supports `getBackingValue()`. The enum-case reflectors expose the same `isDeprecated()`, `hasType()`, and `getType()` defaults as `ReflectionClassConstant`. Broader APIs such as object parameter defaults beyond that limited literal constructor-argument slice, broader internal-function metadata beyond supported first-class-callable builtin signatures, and runtime-only/dynamically typed object-expression `new ReflectionParameter(...)` construction outside eval fragments are not yet available. `ReflectionClassConstant::getValue()` returns the reflected class-constant value, or the enum-case object when the reflected constant is an enum case. An explicit `mixed` hint is reported as untyped. - `ReflectionObject` supports the inherited class-metadata methods for object arguments and materializes the same metadata through a `ReflectionObject` instance. - `ReflectionEnum` additionally supports backing metadata (`isBacked()`, `getBackingType()`); inside eval fragments it also supports enum-case lookup (`hasCase()`, `getCase()`, `getCases()`). Enum-case reflectors expose `getEnum()`. - `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` also support `__toString()` for retained named, nullable, union, and intersection metadata. - For object parameter defaults, that limited constructor-argument slice accepts supported scalar/null literals and class constants; omitted constructor defaults are expanded when they stay within the same subset. - `ReflectionProperty::hasHooks()`, `getHooks()`, `hasHook(PropertyHookType $type)`, and `getHook(PropertyHookType $type)` expose retained concrete generated/AOT property get/set hooks as string-keyed or single `ReflectionMethod` objects. `PropertyHookType::Get`, `PropertyHookType::Set`, `cases()`, `from()`, and `tryFrom()` are available for those hook APIs. `ReflectionProperty::getValue()` and `setValue()` are supported for instance-property reflectors with explicit positional or named object/value arguments, including private/protected properties, reflector objects held in variables, and entries returned from `ReflectionClass::getProperties()`. `ReflectionProperty::isInitialized()` is supported for inline or straight-line tracked instance/static property reflectors and checks the typed-property initialization sentinel without reading the value, including private/protected slots and initialized `null` values. Static-property value access is supported for inline `new ReflectionProperty(Known::class, "property")` receivers, inline `ReflectionClass::getProperty("property")` receivers, `ReflectionClass::getProperties()[N]` and `ReflectionClass::getProperties()[N]` receivers with a known numeric index, and straight-line locals assigned from those forms with omitted/ignored object arguments, including private/protected static properties. Runtime-held static `ReflectionProperty` objects can read `getValue()` and `isInitialized()` from the materialized declaring-class static-property snapshot. Live refresh after reflector construction and dynamic static `setValue()` still require richer runtime metadata. -- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Inside eval fragments, runtime-held generated/AOT function reflectors can also invoke registered generated functions through the native bridge with parameter names, supported defaults, named arguments, and indexed or string-keyed runtime argument arrays. Runtime-only generated/AOT function invocation outside eval still requires richer runtime/typechecker support. +- `ReflectionFunction::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts, and for supported callable-builtin reflectors. The lowered path supports function-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Inside eval fragments, runtime-held generated/AOT function reflectors can also invoke registered generated functions through the native bridge with parameter names, supported defaults, named arguments, indexed or string-keyed runtime argument arrays, and reflected default metadata for supported parameter defaults. Runtime-only generated/AOT function invocation outside eval still requires richer runtime/typechecker support. - `ReflectionMethod::invoke()` and `invokeArgs()` are supported for inline or straight-line tracked generated/AOT reflectors with declared or inferred/untyped parameter contracts. The lowered path supports instance and static methods, constructors returned by `ReflectionClass::getConstructor()`, method-name case-insensitivity, defaults, named arguments, inline static `invokeArgs()` argument arrays, and straight-line locals assigned from literal argument arrays; untyped parameters are forwarded through the boxed `Mixed` ABI used by EIR. Runtime-only or non-literal argument arrays still require richer runtime/typechecker support. - `ReflectionClass::getMethods()` and `getProperties()` modifier filters are supported for materialized member lists, including runtime-held `ReflectionClass` objects. Inline or straight-line tracked `ReflectionClass(Known::class)` receivers with known integer or `ReflectionMethod::IS_*` / `ReflectionProperty::IS_*` filters can also be statically narrowed before materialization. - `ReflectionClass::getStaticProperties()`, `getStaticPropertyValue()`, and `setStaticPropertyValue()` expose current static-property values for eval-declared classes. For generated/AOT classes, these methods are lowered to live static-property storage, including private/protected static properties, when the receiver is an inline `new ReflectionClass(Known::class)` expression, or a straight-line local assigned from one; single-property get/set calls additionally require a literal property name. Runtime-held generated/AOT `ReflectionClass` objects also carry a materialized static-property map captured when the reflector is constructed; it includes initialized public/protected/private static properties, preserves initialized `null` values, omits uninitialized typed static properties, and lets `getStaticProperties()` plus map-backed `getStaticPropertyValue()` keep working after the reflector is passed through runtime storage. Missing literal properties can return the explicit default argument on both the tracked live path and the materialized-map path; the tracked live path also throws a catchable `ReflectionException` when no default is provided. The same tracked receiver metadata lets `newInstance()` and `newInstanceArgs()` normalize named constructor arguments through the reflected constructor signature, including straight-line locals assigned from literal argument arrays. Fully dynamic class/property lookup, live refresh after reflector construction, and dynamic `setStaticPropertyValue()` still require richer runtime metadata. diff --git a/docs/php/eval.md b/docs/php/eval.md index 65b7e4f13a..08e703893b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -472,9 +472,10 @@ dispatch eval-declared functions with the same named/default/variadic argument binding used by direct eval function calls. Runtime-held generated/AOT `ReflectionFunction` objects can invoke registered generated functions through the native bridge with parameter names, supported defaults, named arguments, and -indexed or string-keyed runtime argument arrays. Supported callable-builtin -invocation is covered by the general Reflection support documented in -`docs/php/classes.md`. +indexed or string-keyed runtime argument arrays. Registered generated/AOT +free-function parameter defaults are also exposed through `ReflectionParameter` +metadata. Supported callable-builtin invocation is covered by the general +Reflection support documented in `docs/php/classes.md`. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, `isDefaultValueConstant()`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cb75150044..f0e62b6725 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -17139,6 +17139,37 @@ return $ref->invoke(left: "Q");'); assert_eq!(out.stdout, "XY:QB"); } +/// Verifies eval ReflectionParameter exposes generated/AOT function defaults. +#[test] +fn test_eval_reflection_parameter_exposes_aot_function_defaults() { + let out = compile_and_run_capture( + r#"getParameters(); +echo $params[0]->getName() . ":"; +echo ($params[0]->isDefaultValueAvailable() ? "bad" : "required") . ":"; +echo $params[1]->getName() . ":"; +echo ($params[1]->isOptional() ? "O" : "r") . ":"; +echo ($params[1]->isDefaultValueAvailable() ? "D" : "d") . ":"; +echo $params[1]->getDefaultValue() . ":"; +$direct = new ReflectionParameter("eval_aot_reflect_default_function", "items"); +echo $direct->getName() . ":"; +echo ($direct->isOptional() ? "O" : "r") . ":"; +$default = $direct->getDefaultValue(); +return count($default) . ":" . $default[0] . ":" . $default[1];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "left:required:right:O:D:B:items:O:2:1:2"); +} + /// Verifies eval ReflectionClass::isCloneable uses eval class metadata through the bridge. #[test] fn test_eval_reflection_class_cloneable_predicate() { From fd7fc28c704486fbc28cca32074308b8b8e729da Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 01:13:38 +0200 Subject: [PATCH 0810/1208] Reflect eval AOT function signatures --- crates/elephc-magician/src/context.rs | 151 +++++++++++ .../src/ffi/native_functions.rs | 240 ++++++++++++++++++ .../elephc-magician/src/ffi/native_methods.rs | 4 +- crates/elephc-magician/src/ffi/tests.rs | 85 ++++++- .../src/interpreter/dynamic_functions.rs | 3 + .../src/interpreter/reflection.rs | 116 ++++++--- docs/php/eval.md | 31 ++- src/codegen/lower_inst/builtins/eval.rs | 218 +++++++++++++++- tests/codegen/eval.rs | 36 +++ 9 files changed, 820 insertions(+), 64 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index c2e9149eb1..68c264f881 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -91,7 +91,12 @@ pub struct NativeFunction { invoker: NativeFunctionInvoker, param_count: usize, param_names: Vec, + param_types: Vec>, param_defaults: Vec>, + param_by_ref: Vec, + variadic_index: Option, + return_type: Option, + bridge_supported: bool, } impl NativeFunction { @@ -106,7 +111,12 @@ impl NativeFunction { invoker, param_count, param_names: Vec::new(), + param_types: Vec::new(), param_defaults: Vec::new(), + param_by_ref: Vec::new(), + variadic_index: None, + return_type: None, + bridge_supported: true, } } @@ -132,6 +142,28 @@ impl NativeFunction { &self.param_names } + /// Records the PHP declared type metadata for one positional callback slot. + pub fn set_param_type(&mut self, index: usize, param_type: EvalParameterType) -> bool { + if index >= self.param_count { + return false; + } + if self.param_types.len() < self.param_count { + self.param_types.resize(self.param_count, None); + } + self.param_types[index] = Some(param_type); + true + } + + /// Returns PHP declared parameter types registered for this callback. + pub fn param_types(&self) -> &[Option] { + &self.param_types + } + + /// Returns the registered declared type for one parameter slot, if any. + pub fn param_type(&self, index: usize) -> Option<&EvalParameterType> { + self.param_types.get(index).and_then(Option::as_ref) + } + /// Records a PHP default value for one positional callback slot. pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { if index >= self.param_count { @@ -149,8 +181,64 @@ impl NativeFunction { self.param_defaults.get(index).and_then(Option::as_ref) } + /// Records whether one positional callback parameter is by-reference. + pub fn set_param_by_ref(&mut self, index: usize, by_ref: bool) -> bool { + if index >= self.param_count { + return false; + } + if self.param_by_ref.len() < self.param_count { + self.param_by_ref.resize(self.param_count, false); + } + self.param_by_ref[index] = by_ref; + true + } + + /// Records which positional callback parameter is variadic. + pub fn set_variadic_index(&mut self, index: usize) -> bool { + if index >= self.param_count { + return false; + } + self.variadic_index = Some(index); + true + } + + /// Records the PHP declared return type metadata for this callback. + pub fn set_return_type(&mut self, return_type: EvalParameterType) { + self.return_type = Some(return_type); + } + + /// Records whether eval may dispatch this callback through the generated bridge. + pub fn set_bridge_supported(&mut self, supported: bool) { + self.bridge_supported = supported; + } + + /// Returns whether one registered parameter is by-reference. + pub fn param_by_ref(&self, index: usize) -> bool { + self.param_by_ref.get(index).copied().unwrap_or(false) + } + + /// Returns whether one registered parameter is the variadic parameter. + pub fn param_variadic(&self, index: usize) -> bool { + self.variadic_index == Some(index) + } + + /// Returns the registered declared return type, if any. + pub fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + + /// Returns whether eval may dispatch this callback through the generated bridge. + pub const fn bridge_supported(&self) -> bool { + self.bridge_supported + } + /// Returns the minimum number of required parameters implied by defaults. pub fn required_param_count(&self) -> usize { + if let Some(index) = self.variadic_index { + return (0..index) + .rfind(|position| self.param_default(*position).is_none()) + .map_or(0, |position| position + 1); + } (0..self.param_count) .rfind(|index| self.param_default(*index).is_none()) .map_or(0, |index| index + 1) @@ -2133,6 +2221,18 @@ impl ElephcEvalContext { .is_some_and(|function| function.set_param_name(index, param_name)) } + /// Records one parameter type for an already registered native AOT callback. + pub fn define_native_function_param_type( + &mut self, + function_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_param_type(index, param_type)) + } + /// Records one parameter default for an already registered native AOT callback. pub fn define_native_function_param_default( &mut self, @@ -2145,6 +2245,57 @@ impl ElephcEvalContext { .is_some_and(|function| function.set_param_default(index, default)) } + /// Records whether one native AOT callback parameter is by-reference. + pub fn define_native_function_param_by_ref( + &mut self, + function_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT callback parameter is variadic. + pub fn define_native_function_variadic_param( + &mut self, + function_name: &str, + index: usize, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_variadic_index(index)) + } + + /// Records one native AOT callback return type. + pub fn define_native_function_return_type( + &mut self, + function_name: &str, + return_type: EvalParameterType, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| { + function.set_return_type(return_type); + true + }) + } + + /// Records whether eval may dispatch a native AOT callback through its bridge. + pub fn define_native_function_bridge_supported( + &mut self, + function_name: &str, + supported: bool, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| { + function.set_bridge_supported(supported); + true + }) + } + /// Defines native AOT instance-method signature metadata for eval named-argument binding. pub fn define_native_method_signature( &mut self, diff --git a/crates/elephc-magician/src/ffi/native_functions.rs b/crates/elephc-magician/src/ffi/native_functions.rs index 3bfeb465e8..5b14d47895 100644 --- a/crates/elephc-magician/src/ffi/native_functions.rs +++ b/crates/elephc-magician/src/ffi/native_functions.rs @@ -12,6 +12,7 @@ use super::native_methods::{ native_callable_array_default, native_callable_object_default, native_callable_scalar_default, + native_callable_type_from_abi, NativeCallableTypePosition, }; use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ABI_VERSION}; @@ -66,6 +67,108 @@ pub unsafe extern "C" fn __elephc_eval_register_native_function_param( .unwrap_or(0) } +/// Registers whether a generated native PHP function can be invoked through its bridge. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_bridge_support( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_bridge_support_inner( + ctx, + function_name_ptr, + function_name_len, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter's by-ref and variadic flags. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_flags( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_flags_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and type-spec +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_type( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_type_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function return type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and type-spec +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_return_type( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_return_type_inner( + ctx, + function_name_ptr, + function_name_len, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + /// Registers one generated native PHP function scalar parameter default in an eval context. /// /// # Safety @@ -245,6 +348,143 @@ unsafe fn register_native_function_param_inner( )) } +/// Runs native function bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_bridge_support`; invalid +/// handles or names fail closed as `false`. +unsafe fn register_native_function_bridge_support_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + i32::from(context.define_native_function_bridge_supported( + &function_name.to_ascii_lowercase(), + supported != 0, + )) +} + +/// Runs native function parameter-flags registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_flags`; invalid +/// handles, names, or indexes fail closed as `false`. +unsafe fn register_native_function_param_flags_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let function_name = function_name.to_ascii_lowercase(); + if !context.define_native_function_param_by_ref(&function_name, param_index, is_by_ref != 0) { + return 0; + } + if is_variadic != 0 { + return i32::from(context.define_native_function_variadic_param( + &function_name, + param_index, + )); + } + 1 +} + +/// Runs native function parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_type`; invalid handles, +/// names, indexes, or type specs fail closed as `false`. +unsafe fn register_native_function_param_type_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_function_param_type( + &function_name.to_ascii_lowercase(), + param_index, + param_type, + )) +} + +/// Runs native function return-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_return_type`; invalid handles, +/// names, or type specs fail closed as `false`. +unsafe fn register_native_function_return_type_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Some(return_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Return, + ) else { + return 0; + }; + i32::from(context.define_native_function_return_type( + &function_name.to_ascii_lowercase(), + return_type, + )) +} + /// Runs native function scalar-default registration after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index f6e79f5855..594e48b865 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -49,7 +49,7 @@ const NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; #[derive(Clone, Copy)] -enum NativeCallableTypePosition { +pub(super) enum NativeCallableTypePosition { Parameter, Return, } @@ -2127,7 +2127,7 @@ fn native_attribute_take_u64(bytes: &[u8], offset: &mut usize) -> Option { } /// Decodes one generated type-spec string into eval Reflection type metadata. -fn native_callable_type_from_abi( +pub(super) fn native_callable_type_from_abi( type_spec_ptr: *const u8, type_spec_len: u64, position: NativeCallableTypePosition, diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 3ab759a5bb..6d8e163c25 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -402,6 +402,9 @@ fn register_native_function_reports_function_exists() { let mut ctx = ElephcEvalContext::new(); let name = b"NATIVE_PROBE"; let param = b"value"; + let variadic = b"items"; + let param_type = b"?string"; + let return_type = b"int"; let descriptor = 1usize as *mut c_void; let registered = unsafe { @@ -411,7 +414,7 @@ fn register_native_function_reports_function_exists() { name.len() as u64, descriptor, Some(fake_native_invoker), - 1, + 2, ) }; let param_registered = unsafe { @@ -424,6 +427,63 @@ fn register_native_function_reports_function_exists() { param.len() as u64, ) }; + let variadic_param_registered = unsafe { + __elephc_eval_register_native_function_param( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 1, + variadic.as_ptr(), + variadic.len() as u64, + ) + }; + let bridge_support_registered = unsafe { + __elephc_eval_register_native_function_bridge_support( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + ) + }; + let param_flags_registered = unsafe { + __elephc_eval_register_native_function_param_flags( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + 1, + 0, + ) + }; + let variadic_flags_registered = unsafe { + __elephc_eval_register_native_function_param_flags( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 1, + 0, + 1, + ) + }; + let param_type_registered = unsafe { + __elephc_eval_register_native_function_param_type( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + param_type.as_ptr(), + param_type.len() as u64, + ) + }; + let return_type_registered = unsafe { + __elephc_eval_register_native_function_return_type( + &mut ctx, + name.as_ptr(), + name.len() as u64, + return_type.as_ptr(), + return_type.len() as u64, + ) + }; let param_default_registered = unsafe { __elephc_eval_register_native_function_param_default_scalar( &mut ctx, @@ -442,9 +502,30 @@ fn register_native_function_reports_function_exists() { .expect("native function should be registered"); assert_eq!(param_registered, 1); + assert_eq!(variadic_param_registered, 1); + assert_eq!(bridge_support_registered, 1); + assert_eq!(param_flags_registered, 1); + assert_eq!(variadic_flags_registered, 1); + assert_eq!(param_type_registered, 1); + assert_eq!(return_type_registered, 1); assert_eq!(param_default_registered, 1); assert_eq!(exists, 1); - assert_eq!(native.param_names(), &["value".to_string()]); + assert_eq!( + native.param_names(), + &["value".to_string(), "items".to_string()] + ); + let native_type = native.param_type(0).expect("native parameter type"); + assert!(native_type.allows_null()); + assert_eq!(native_type.variants(), &[EvalParameterTypeVariant::String]); + assert!(native.param_by_ref(0)); + assert!(!native.param_variadic(0)); + assert!(native.param_variadic(1)); + assert!(!native.bridge_supported()); + assert_eq!(native.required_param_count(), 0); + assert_eq!( + native.return_type().expect("native return type").variants(), + &[EvalParameterTypeVariant::Int] + ); assert_eq!(native.param_default(0), Some(&NativeCallableDefault::Int(42))); } diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 9c997ea96d..c10588258a 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1343,6 +1343,9 @@ pub(super) fn eval_native_function_with_values( evaluated_args: Vec, values: &mut impl RuntimeValueOps, ) -> Result { + if !function.bridge_supported() { + return Err(EvalStatus::RuntimeFatal); + } if evaluated_args.len() != function.param_count() { return Err(EvalStatus::RuntimeFatal); } diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index f3491006dd..c0f9b1436e 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3132,10 +3132,14 @@ fn eval_reflection_native_function_parameters( let parameter_names = eval_reflection_native_function_parameter_names(function); let parameter_count = parameter_names.len(); let parameter_attributes = vec![Vec::new(); parameter_count]; - let parameter_types: Vec> = vec![None; parameter_count]; + let parameter_types = eval_reflection_native_function_parameter_types(function); let parameter_defaults = eval_reflection_native_function_parameter_defaults(function); - let parameter_is_by_ref = vec![false; parameter_count]; - let parameter_is_variadic = vec![false; parameter_count]; + let parameter_is_by_ref = (0..parameter_count) + .map(|index| function.param_by_ref(index)) + .collect::>(); + let parameter_is_variadic = (0..parameter_count) + .map(|index| function.param_variadic(index)) + .collect::>(); eval_reflection_function_parameters( function_name, ¶meter_names, @@ -3148,6 +3152,15 @@ fn eval_reflection_native_function_parameters( ) } +/// Converts registered native function parameter types into reflection metadata input. +fn eval_reflection_native_function_parameter_types( + function: &NativeFunction, +) -> Vec> { + (0..function.param_count()) + .map(|index| function.param_type(index).cloned()) + .collect() +} + /// Converts registered native function defaults into eval constant expressions. fn eval_reflection_native_function_parameter_defaults( function: &NativeFunction, @@ -8934,44 +8947,71 @@ fn eval_reflection_function_method_target( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { if let Some(name) = context.eval_reflection_function_name(identity) { - let function = context.function(&name.to_ascii_lowercase()); - let is_variadic = function - .is_some_and(|function| function.parameter_is_variadic().iter().any(|flag| *flag)); - let parameters = function - .map(|function| { - eval_reflection_function_parameters( - function.name(), - function.params(), - function.attributes().to_vec(), - function.parameter_attributes(), - function.parameter_types(), - function.parameter_defaults(), - function.parameter_is_by_ref(), - function.parameter_is_variadic(), - ) - }) - .unwrap_or_default(); - let source_location = function.and_then(EvalFunction::source_location); - let return_type_metadata = function - .and_then(EvalFunction::return_type) - .and_then(eval_reflection_parameter_type_metadata); - let static_key = function.map(|function| function.name().to_string()); - let static_variables = function - .map(|function| static_var_initializers(function.body())) - .unwrap_or_default(); - let is_deprecated = function - .map(EvalFunction::attributes) - .is_some_and(eval_reflection_attributes_include_deprecated); + let lookup_name = name.to_ascii_lowercase(); + if let Some(function) = context.function(&lookup_name) { + let is_variadic = function + .parameter_is_variadic() + .iter() + .any(|flag| *flag); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + let source_location = function.source_location(); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let static_key = Some(function.name().to_string()); + let static_variables = static_var_initializers(function.body()); + let is_deprecated = + eval_reflection_attributes_include_deprecated(function.attributes()); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key, + static_variables, + parameters, + source_location, + is_variadic, + is_static: false, + is_deprecated, + return_type_metadata, + })); + } + if let Some(function) = context.native_function(&lookup_name) { + let parameters = eval_reflection_native_function_parameters(name, &function); + let is_variadic = + (0..function.param_count()).any(|index| function.param_variadic(index)); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key: None, + static_variables: Vec::new(), + parameters, + source_location: None, + is_variadic, + is_static: false, + is_deprecated: false, + return_type_metadata, + })); + } return Ok(Some(EvalReflectionFunctionMethodTarget::Function { name: name.to_string(), - static_key, - static_variables, - parameters, - source_location, - is_variadic, + static_key: None, + static_variables: Vec::new(), + parameters: Vec::new(), + source_location: None, + is_variadic: false, is_static: false, - is_deprecated, - return_type_metadata, + is_deprecated: false, + return_type_metadata: None, })); } let Some((declaring_class, method_name)) = context.eval_reflection_method(identity) else { diff --git a/docs/php/eval.md b/docs/php/eval.md index 08e703893b..e5d45a4760 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -435,12 +435,13 @@ reflected method is `__construct` or `__destruct`. `ReflectionFunction::getName()`, `ReflectionFunction::getParameters()`, `ReflectionMethod::getParameters()`, `getNumberOfParameters()`, and `getNumberOfRequiredParameters()` report retained eval-declared function and -method metadata, plus registered generated/AOT method parameter names, declared -parameter and return types, required/optional counts, by-reference and variadic -flags, and scalar, null, empty-array, supported array-valued, or supported object-valued default values when native -method/static-method or constructor signatures are registered. Eval code can -also reflect supported callable-builtin signatures, including -internal origin, parameter names, parameter types, and return type metadata. +method metadata, plus registered generated/AOT free-function, method, +static-method, and constructor parameter names, declared parameter and return +types, required/optional counts, by-reference and variadic flags, and scalar, +null, empty-array, supported array-valued, or supported object-valued default +values when native signatures are registered. Eval code can also reflect +supported callable-builtin signatures, including internal origin, parameter +names, parameter types, and return type metadata. Eval-declared functions and methods expose declared-type presence for parameters and return types, simple named type metadata through @@ -473,9 +474,13 @@ binding used by direct eval function calls. Runtime-held generated/AOT `ReflectionFunction` objects can invoke registered generated functions through the native bridge with parameter names, supported defaults, named arguments, and indexed or string-keyed runtime argument arrays. Registered generated/AOT -free-function parameter defaults are also exposed through `ReflectionParameter` -metadata. Supported callable-builtin invocation is covered by the general -Reflection support documented in `docs/php/classes.md`. +free-function parameter names, declared types, return types, by-reference and +variadic flags, required/optional counts, and supported defaults are also +exposed through `ReflectionFunction` / `ReflectionParameter` metadata. +Unsupported generated/AOT free-function bridge shapes remain reflectable as +metadata but are not invocable through eval. Supported callable-builtin +invocation is covered by the general Reflection support documented in +`docs/php/classes.md`. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, `isDefaultValueAvailable()`, `isDefaultValueConstant()`, @@ -821,10 +826,10 @@ parameter and generated property default-value materialization beyond scalar, null, empty-array, supported array-valued defaults, and supported object-valued parameter defaults during generated/AOT invocation, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked scalar/nullable scalar/Mixed/array/iterable/object parameter slice plus scalar/nullable scalar/Mixed/array/iterable/object returns and -`__clone()` hooks. Generated/AOT method type -metadata, by-reference and variadic parameter flags, and generated/AOT -class/method/property/class-constant attributes are exposed for registered metadata slices, while -other unsupported bridge shapes remain metadata-only +`__clone()` hooks. Generated/AOT free-function and method type metadata, +return metadata, by-reference and variadic parameter flags, and generated/AOT +class/method/property/class-constant attributes are exposed for registered +metadata slices, while other unsupported bridge shapes remain metadata-only rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index f72c8e6620..159b255f89 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -106,6 +106,7 @@ struct EvalGlobalAlias { struct EvalNativeFunctionRegistration { name: String, signature: FunctionSig, + bridge_supported: bool, } /// A module-local method signature that can be registered with the eval context. @@ -559,10 +560,11 @@ fn eval_native_function_registrations( ctx.module .functions .iter() - .filter(|function| function_can_register_with_eval(function)) + .filter(|function| function_has_eval_metadata(function)) .map(|function| EvalNativeFunctionRegistration { name: function.name.clone(), signature: function_signature_from_eir(function), + bridge_supported: function_signature_can_bridge_with_eval(function), }) .collect() } @@ -1026,14 +1028,17 @@ fn collect_eval_native_static_methods( } } -/// Returns true when a module function is a PHP-visible AOT function supported by this bridge. -fn function_can_register_with_eval(function: &Function) -> bool { - !function.flags.is_main - && !function.name.starts_with('_') - && function - .params - .iter() - .all(|param| !param.by_ref && !param.variadic) +/// Returns true when a module function should expose metadata to eval fragments. +fn function_has_eval_metadata(function: &Function) -> bool { + !function.flags.is_main && !function.name.starts_with('_') +} + +/// Returns true when eval can dispatch a native function through the generated bridge. +fn function_signature_can_bridge_with_eval(function: &Function) -> bool { + function + .params + .iter() + .all(|param| !param.by_ref && !param.variadic) } /// Returns true when eval can dispatch a native method through the generated bridge. @@ -1545,6 +1550,14 @@ fn register_eval_native_function( .target .extern_symbol("__elephc_eval_register_native_function"); abi::emit_call_label(ctx.emitter, &symbol); + register_eval_native_function_bridge_support( + ctx, + context_offset, + &name_label, + name_len, + registration.bridge_supported, + ); + let param_type_specs = eval_native_callable_param_type_specs(®istration.signature); for (index, (param_name, _)) in registration.signature.params.iter().enumerate() { register_eval_native_function_param( ctx, @@ -1554,6 +1567,30 @@ fn register_eval_native_function( index, param_name, ); + register_eval_native_function_param_flags( + ctx, + context_offset, + &name_label, + name_len, + index, + registration + .signature + .ref_params + .get(index) + .copied() + .unwrap_or(false), + signature_param_is_variadic(®istration.signature, index, param_name), + ); + if let Some(type_spec) = param_type_specs.get(index).and_then(Option::as_deref) { + register_eval_native_function_param_type( + ctx, + context_offset, + &name_label, + name_len, + index, + type_spec, + ); + } } for (index, default) in registration.signature.defaults.iter().enumerate() { let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { @@ -1568,6 +1605,15 @@ fn register_eval_native_function( &default, ); } + if let Some(type_spec) = eval_native_callable_return_type_spec(®istration.signature) { + register_eval_native_function_return_type( + ctx, + context_offset, + &name_label, + name_len, + &type_spec, + ); + } Ok(()) } @@ -2660,6 +2706,160 @@ fn register_eval_native_function_param( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native-function bridge-support registration call. +fn register_eval_native_function_bridge_support( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + bridge_supported: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + if bridge_supported { 1 } else { 0 }, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_bridge_support"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native-function parameter-flags registration call. +fn register_eval_native_function_param_flags( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + param_index: usize, + is_by_ref: bool, + is_variadic: bool, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + if is_by_ref { 1 } else { 0 }, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + if is_variadic { 1 } else { 0 }, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_flags"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native-function parameter-type registration call. +fn register_eval_native_function_param_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + param_index: usize, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + param_index as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + type_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_param_type"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Emits one native-function return-type registration call. +fn register_eval_native_function_return_type( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + function_name_label: &str, + function_name_len: usize, + type_spec: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + function_name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + function_name_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_function_return_type"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native function parameter-default registration call. fn register_eval_native_function_param_default( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f0e62b6725..59e28c290d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -17170,6 +17170,42 @@ return count($default) . ":" . $default[0] . ":" . $default[1];'); assert_eq!(out.stdout, "left:required:right:O:D:B:items:O:2:1:2"); } +/// Verifies eval ReflectionParameter exposes generated/AOT function type flags. +#[test] +fn test_eval_reflection_parameter_exposes_aot_function_type_flags() { + let out = compile_and_run_capture( + r#"getParameters(); +foreach ($params as $param) { + echo $param->getName() . ":"; + echo ($param->hasType() ? "T" : "t") . ":"; + $type = $param->getType(); + echo ($type ? $type->getName() : "none") . ":"; + echo ($type && $type->allowsNull() ? "N" : "n") . ":"; + echo ($param->isVariadic() ? "V" : "v") . ":"; + echo ($param->isPassedByReference() ? "R" : "r") . "|"; +} +echo ":"; +echo ($ref->hasReturnType() ? "R" : "r") . ":"; +$return = $ref->getReturnType(); +return $return->getName() . ":" . ($return->allowsNull() ? "N" : "n");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "id:T:int:n:v:R|name:T:string:N:v:r|items:T:array:n:V:r|:R:string:N" + ); +} + /// Verifies eval ReflectionClass::isCloneable uses eval class metadata through the bridge. #[test] fn test_eval_reflection_class_cloneable_predicate() { From a986e60b7564c1366f5443f48c1d9f08c41cd534 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 01:29:39 +0200 Subject: [PATCH 0811/1208] Expose AOT declarations to eval --- crates/elephc-magician/src/context.rs | 37 ++++++- .../src/ffi/declared_symbols.rs | 101 ++++++++++++++++++ crates/elephc-magician/src/ffi/mod.rs | 2 + crates/elephc-magician/src/ffi/tests.rs | 60 +++++++++++ docs/php/eval.md | 11 +- src/codegen/lower_inst/builtins/eval.rs | 55 ++++++++++ tests/codegen/eval.rs | 79 ++++++++++++-- 7 files changed, 331 insertions(+), 14 deletions(-) create mode 100644 crates/elephc-magician/src/ffi/declared_symbols.rs diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 68c264f881..02bb353b06 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -916,11 +916,16 @@ impl ElephcEvalContext { true } - /// Returns class names declared or aliased through eval in PHP-visible order. + /// Returns class names declared through eval or registered from generated metadata. pub fn declared_class_names(&self) -> &[String] { &self.declared_class_names } + /// Registers a runtime-visible class or enum declaration name for `get_declared_classes()`. + pub fn define_external_declared_class_name(&mut self, name: &str) -> bool { + push_external_declared_name(&mut self.declared_class_names, name) + } + /// Defines an eval-declared interface, failing if this context already has the name. pub fn define_interface(&mut self, interface: EvalInterface) -> bool { let key = normalize_class_name(interface.name()); @@ -960,11 +965,16 @@ impl ElephcEvalContext { .flatten() } - /// Returns interface names declared through eval in PHP-visible order. + /// Returns interface names declared through eval or registered from generated metadata. pub fn declared_interface_names(&self) -> &[String] { &self.declared_interface_names } + /// Registers a runtime-visible interface declaration name for `get_declared_interfaces()`. + pub fn define_external_declared_interface_name(&mut self, name: &str) -> bool { + push_external_declared_name(&mut self.declared_interface_names, name) + } + /// Defines an eval-declared trait, failing if this context already has the name. pub fn define_trait(&mut self, trait_decl: EvalTrait) -> bool { let key = normalize_class_name(trait_decl.name()); @@ -1004,11 +1014,16 @@ impl ElephcEvalContext { .flatten() } - /// Returns trait names declared through eval in PHP-visible order. + /// Returns trait names declared through eval or registered from generated metadata. pub fn declared_trait_names(&self) -> &[String] { &self.declared_trait_names } + /// Registers a runtime-visible trait declaration name for `get_declared_traits()`. + pub fn define_external_declared_trait_name(&mut self, name: &str) -> bool { + push_external_declared_name(&mut self.declared_trait_names, name) + } + /// Defines an eval-declared enum plus class-shaped metadata for dispatch. pub fn define_enum(&mut self, enum_decl: EvalEnum) -> bool { let key = normalize_class_name(enum_decl.name()); @@ -3178,6 +3193,22 @@ fn normalize_class_name(name: &str) -> String { name.trim_start_matches('\\').to_ascii_lowercase() } +/// Adds an external declaration name once while preserving PHP-visible spelling. +fn push_external_declared_name(names: &mut Vec, name: &str) -> bool { + let visible_name = name.trim_start_matches('\\'); + let key = normalize_class_name(visible_name); + if key.is_empty() { + return false; + } + if !names + .iter() + .any(|existing| normalize_class_name(existing) == key) + { + names.push(visible_name.to_string()); + } + true +} + /// Normalizes PHP enum case names for case-sensitive eval enum lookup. fn normalize_enum_case_name(name: &str) -> String { name.to_string() diff --git a/crates/elephc-magician/src/ffi/declared_symbols.rs b/crates/elephc-magician/src/ffi/declared_symbols.rs new file mode 100644 index 0000000000..267e5700d3 --- /dev/null +++ b/crates/elephc-magician/src/ffi/declared_symbols.rs @@ -0,0 +1,101 @@ +//! Purpose: +//! Exports registration of generated declaration-name metadata into eval. +//! The interpreter uses these lists to answer `get_declared_*()` calls without +//! treating AOT class-like symbols as eval-owned declarations. +//! +//! Called from: +//! - Generated EIR backend assembly when a persistent eval context is created. +//! +//! Key details: +//! - Invalid context handles, ABI versions, or names fail closed as `false`. +//! - Duplicate names are ignored case-insensitively while preserving first spelling. + +use super::util::abi_name_to_string; +use crate::abi::{ElephcEvalContext, ABI_VERSION}; + +/// Class-like declaration list selected by one registration entry point. +#[derive(Clone, Copy)] +enum DeclaredSymbolKind { + Class, + Interface, + Trait, +} + +/// Registers one generated class or enum name for eval `get_declared_classes()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_declared_class_name( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_declared_symbol_inner(ctx, name_ptr, name_len, DeclaredSymbolKind::Class) + }) + .unwrap_or(0) +} + +/// Registers one generated interface name for eval `get_declared_interfaces()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_declared_interface_name( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_declared_symbol_inner(ctx, name_ptr, name_len, DeclaredSymbolKind::Interface) + }) + .unwrap_or(0) +} + +/// Registers one generated trait name for eval `get_declared_traits()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_declared_trait_name( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_declared_symbol_inner(ctx, name_ptr, name_len, DeclaredSymbolKind::Trait) + }) + .unwrap_or(0) +} + +/// Runs declared-name registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors the exported registration functions; invalid handles or unreadable +/// name storage fail closed as `false`. +unsafe fn register_declared_symbol_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + kind: DeclaredSymbolKind, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + let registered = match kind { + DeclaredSymbolKind::Class => context.define_external_declared_class_name(&name), + DeclaredSymbolKind::Interface => context.define_external_declared_interface_name(&name), + DeclaredSymbolKind::Trait => context.define_external_declared_trait_name(&name), + }; + i32::from(registered) +} diff --git a/crates/elephc-magician/src/ffi/mod.rs b/crates/elephc-magician/src/ffi/mod.rs index d9f73bcc1c..801afa5eff 100644 --- a/crates/elephc-magician/src/ffi/mod.rs +++ b/crates/elephc-magician/src/ffi/mod.rs @@ -10,6 +10,7 @@ //! - Helper routines stay private to the FFI layer unless shared across families. pub mod context; +pub mod declared_symbols; pub(crate) mod dynamic_destructors; pub mod execute; #[cfg(not(test))] @@ -21,6 +22,7 @@ pub mod symbols; pub(crate) mod util; pub use context::*; +pub use declared_symbols::*; pub use execute::*; #[cfg(not(test))] pub use function_calls::*; diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 6d8e163c25..6b1e881600 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -11,6 +11,7 @@ //! - Fake runtime-cell pointers are never dereferenced by these tests. use super::context::*; +use super::declared_symbols::*; use super::execute::*; use super::native_functions::*; use super::native_methods::*; @@ -317,6 +318,65 @@ fn context_push_class_scope_records_self_and_called_class() { assert_eq!(ctx.current_called_class_scope(), None); } +/// Verifies generated declaration-name metadata is exposed through eval lists. +#[test] +fn register_declared_symbol_names_records_visible_metadata() { + let mut ctx = ElephcEvalContext::new(); + let class_name = b"\\AotDeclaredClass"; + let class_duplicate = b"aotdeclaredclass"; + let interface_name = b"AotDeclaredInterface"; + let trait_name = b"AotDeclaredTrait"; + let empty_name = b""; + + let class_registered = unsafe { + __elephc_eval_register_declared_class_name( + &mut ctx, + class_name.as_ptr(), + class_name.len() as u64, + ) + }; + let duplicate_registered = unsafe { + __elephc_eval_register_declared_class_name( + &mut ctx, + class_duplicate.as_ptr(), + class_duplicate.len() as u64, + ) + }; + let interface_registered = unsafe { + __elephc_eval_register_declared_interface_name( + &mut ctx, + interface_name.as_ptr(), + interface_name.len() as u64, + ) + }; + let trait_registered = unsafe { + __elephc_eval_register_declared_trait_name( + &mut ctx, + trait_name.as_ptr(), + trait_name.len() as u64, + ) + }; + let empty_rejected = unsafe { + __elephc_eval_register_declared_trait_name( + &mut ctx, + empty_name.as_ptr(), + empty_name.len() as u64, + ) + }; + + assert_eq!(class_registered, 1); + assert_eq!(duplicate_registered, 1); + assert_eq!(interface_registered, 1); + assert_eq!(trait_registered, 1); + assert_eq!(empty_rejected, 0); + assert_eq!(ctx.declared_class_names(), &["AotDeclaredClass".to_string()]); + assert_eq!( + ctx.declared_interface_names(), + &["AotDeclaredInterface".to_string()] + ); + assert_eq!(ctx.declared_trait_names(), &["AotDeclaredTrait".to_string()]); +} + /// Verifies the function-exists ABI probes eval-declared functions by folded name. #[test] fn function_exists_reports_declared_eval_function() { diff --git a/docs/php/eval.md b/docs/php/eval.md index e5d45a4760..3715a9091b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -630,11 +630,12 @@ alias eval-declared and generated/AOT classes, interfaces, traits, and enums, preserving the target class-like kind for the corresponding metadata probes. Top-level compiled `class_alias()` calls still use elephc's generated subclass model, so eval sees the same AOT -parent metadata as the compiled program. Aliases are usable for class-like -lookups but are not added to `get_declared_classes()`, -`get_declared_interfaces()`, or `get_declared_traits()`. Eval-declared enums are -visible inside eval through `enum_exists()` and through class-like probes such -as `class_exists()`. +parent metadata as the compiled program. `get_declared_classes()`, +`get_declared_interfaces()`, and `get_declared_traits()` expose eval-declared +names plus generated/AOT declaration names; enum names are included in the class +list like PHP. Aliases are usable for class-like lookups but are not added to +the declared-name lists. Eval-declared enums are visible inside eval through +`enum_exists()` and through class-like probes such as `class_exists()`. `method_exists()` and `property_exists()` inspect eval-declared class/interface/ trait/enum metadata and generated runtime metadata. Object targets also see dynamic public properties, and `property_exists()` follows PHP's current-scope diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 159b255f89..dcaf9b739a 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -503,6 +503,7 @@ fn ensure_eval_context(ctx: &mut FunctionContext<'_>) -> Result<()> { .extern_symbol("__elephc_eval_context_new"); abi::emit_call_label(ctx.emitter, &symbol); abi::store_at_offset(ctx.emitter, result_reg, offset); + register_eval_declared_symbols(ctx, offset); register_eval_native_functions(ctx, offset)?; register_eval_native_method_signatures(ctx, offset); ctx.emitter.label(&ready); @@ -553,6 +554,60 @@ fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context register_eval_native_class_parents(ctx, context_offset); } +/// Registers generated declared-name metadata with a newly allocated eval context. +fn register_eval_declared_symbols(ctx: &mut FunctionContext<'_>, context_offset: usize) { + let class_names = ctx.module.declared_class_names.clone(); + let interface_names = ctx.module.declared_interface_names.clone(); + let trait_names = ctx.module.declared_trait_names.clone(); + for name in class_names { + register_eval_declared_symbol_name( + ctx, + context_offset, + "__elephc_eval_register_declared_class_name", + &name, + ); + } + for name in interface_names { + register_eval_declared_symbol_name( + ctx, + context_offset, + "__elephc_eval_register_declared_interface_name", + &name, + ); + } + for name in trait_names { + register_eval_declared_symbol_name( + ctx, + context_offset, + "__elephc_eval_register_declared_trait_name", + &name, + ); + } +} + +/// Emits one declared-name metadata registration call into the eval context. +fn register_eval_declared_symbol_name( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + symbol_name: &str, + name: &str, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &name_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let symbol = ctx.emitter.target.extern_symbol(symbol_name); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Collects global PHP functions that can use the descriptor-invoker bridge. fn eval_native_function_registrations( ctx: &FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 59e28c290d..d577d4c0b2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7384,6 +7384,41 @@ echo class_exists(class: "MissingEvalClassExistsProbe", autoload: false) ? "Y" : assert_eq!(out, "YYYYYN"); } +/// Verifies eval `get_declared_*()` exposes generated AOT class-like names. +#[test] +fn test_eval_get_declared_symbols_exposes_aot_metadata() { + let out = compile_and_run( + r#"read(4) . ":"; echo $box->label() . ":"; echo is_a($box, "EvalDynNamedReader") ? "isa" : "bad"; echo ":"; @@ -8038,7 +8080,7 @@ echo count($implements) . ":" . $implements["EvalDynNamedReader"] . ":" . $imple ); assert_eq!( out.stdout, - "iface:notclass:2:5:box:isa:str:2:EvalDynNamedReader:EvalDynReader" + "iface:notclass:declared:5:box:isa:str:2:EvalDynNamedReader:EvalDynReader" ); } @@ -18670,14 +18712,39 @@ echo (new ReflectionClass("EvalAliasEnumCopy"))->getName(); echo ":"; echo EvalAliasEnumCopy::Ready->value; echo ":"; echo class_alias("EvalAliasClass", "EvalAliasClassCopy") ? "C" : "c"; echo ":"; echo class_exists("EvalAliasClassCopy") ? "CE" : "ce"; echo ":"; -echo count(get_declared_classes()); echo ":"; -echo count(get_declared_interfaces()); echo ":"; -return count(get_declared_traits());'); +$declaredClasses = get_declared_classes(); +$classDeclared = false; +$enumDeclared = false; +$classAliasDeclared = false; +$enumAliasDeclared = false; +foreach ($declaredClasses as $name) { + if ($name === "EvalAliasClass") { $classDeclared = true; } + if ($name === "EvalAliasEnum") { $enumDeclared = true; } + if ($name === "EvalAliasClassCopy") { $classAliasDeclared = true; } + if ($name === "EvalAliasEnumCopy") { $enumAliasDeclared = true; } +} +echo ($classDeclared && $enumDeclared && !$classAliasDeclared && !$enumAliasDeclared) ? "DC" : "dc"; echo ":"; +$declaredInterfaces = get_declared_interfaces(); +$ifaceDeclared = false; +$ifaceAliasDeclared = false; +foreach ($declaredInterfaces as $name) { + if ($name === "EvalAliasIface") { $ifaceDeclared = true; } + if ($name === "EvalAliasIfaceCopy") { $ifaceAliasDeclared = true; } +} +echo ($ifaceDeclared && !$ifaceAliasDeclared) ? "DI" : "di"; echo ":"; +$declaredTraits = get_declared_traits(); +$traitDeclared = false; +$traitAliasDeclared = false; +foreach ($declaredTraits as $name) { + if ($name === "EvalAliasTrait") { $traitDeclared = true; } + if ($name === "EvalAliasTraitCopy") { $traitAliasDeclared = true; } +} +return ($traitDeclared && !$traitAliasDeclared) ? "DT" : "dt";'); "#, ); assert_eq!( out, - "I:IE:IC:II:IR:U:UE:UC:T:TE:TC:TI:E:EE:EC:EvalAliasEnum:ready:C:CE:2:1:1" + "I:IE:IC:II:IR:U:UE:UC:T:TE:TC:TI:E:EE:EC:EvalAliasEnum:ready:C:CE:DC:DI:DT" ); } From dafdad3aab973fbcd441386ad01cb31e300d3afa Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 01:42:10 +0200 Subject: [PATCH 0812/1208] Lift eval AOT method arity cap --- docs/php/eval.md | 16 +++++++++----- src/codegen/eval_constructor_helpers.rs | 4 +--- src/codegen/eval_method_helpers.rs | 4 +--- src/codegen/lower_inst/builtins/eval.rs | 7 ++---- tests/codegen/eval.rs | 29 +++++++++++++++++++++++++ 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 3715a9091b..45a74e0bf5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -806,11 +806,14 @@ particular, advanced native callable descriptors and full `Closure` object semantics for callback values are still outside eval fragments. Runtime/AOT object-method, static-method, and constructor fallback from eval can bind registered names, defaults, positional variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch -is limited to visibility-checked non-by-reference scalar/nullable scalar/Mixed/array/iterable/object -parameter signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference -method and constructor parameters, including the generated positional variadic -array slot when present. Broader parameter/return ABI shapes are still outside -those bridge paths. +is limited to visibility-checked +non-by-reference scalar/nullable scalar/Mixed/array/iterable/object parameter +signatures plus `mixed`/untyped, scalar including string, nullable scalar, +array, iterable, and object by-reference method and constructor parameters, +including the generated positional variadic array slot when present. Those +supported method and constructor shapes use normal target ABI materialization +for arguments beyond the register set instead of a fixed eval arity cap. Broader +parameter/return ABI shapes are still outside those bridge paths. Eval class support is still smaller than the full static class system. Eval-declared `__destruct()` hooks run when an eval-owned dynamic object reaches @@ -827,7 +830,8 @@ parameter and generated property default-value materialization beyond scalar, null, empty-array, supported array-valued defaults, and supported object-valued parameter defaults during generated/AOT invocation, and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked scalar/nullable scalar/Mixed/array/iterable/object parameter slice plus scalar/nullable scalar/Mixed/array/iterable/object returns and -`__clone()` hooks. Generated/AOT free-function and method type metadata, +`__clone()` hooks; the remaining limit there is the supported type slice rather +than a fixed parameter count. Generated/AOT free-function and method type metadata, return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while other unsupported bridge shapes remain metadata-only diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 8a7ddb925e..b3b9a673d3 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -32,7 +32,6 @@ use super::eval_ref_arg_helpers::{ emit_aarch64_write_back_ref_args, emit_x86_64_write_back_ref_args, }; -const MAX_EVAL_CONSTRUCTOR_ARGS: usize = 8; const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ "Error", "TypeError", @@ -238,8 +237,7 @@ fn constructor_visibility_supported(visibility: &Visibility) -> bool { /// Returns true for constructor signatures supported by this eval bridge slice. fn constructor_signature_supported(sig: &FunctionSig) -> bool { - sig.params.len() <= MAX_EVAL_CONSTRUCTOR_ARGS - && eval_signature_ref_params_supported(sig) + eval_signature_ref_params_supported(sig) && sig .params .iter() diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 7f40e1d9be..5bff70b9a9 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -60,7 +60,6 @@ struct EvalStaticMethodSlot { return_ty: PhpType, } -const MAX_EVAL_METHOD_ARGS: usize = 8; const BUILTIN_THROWABLE_METHOD_CLASSES: &[&str] = &[ "Error", "TypeError", @@ -367,8 +366,7 @@ fn method_visibility_supported(visibility: &Visibility) -> bool { /// Returns true for method signatures supported by the eval bridge. fn method_signature_supported(sig: &crate::types::FunctionSig) -> bool { - sig.params.len() <= MAX_EVAL_METHOD_ARGS - && eval_signature_ref_params_supported(sig) + eval_signature_ref_params_supported(sig) && sig.params.iter().all(|(_, ty)| method_param_supported(ty)) } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index dcaf9b739a..de5f67b316 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -50,7 +50,6 @@ const EVAL_CALLED_CLASS_PTR_OFFSET: usize = 72; const EVAL_CALLED_CLASS_LEN_OFFSET: usize = 80; const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; -const MAX_EVAL_NATIVE_METHOD_PARAMS: usize = 8; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; const NATIVE_DEFAULT_NULL: i64 = 0; const NATIVE_DEFAULT_BOOL: i64 = 1; @@ -1098,8 +1097,7 @@ fn function_signature_can_bridge_with_eval(function: &Function) -> bool { /// Returns true when eval can dispatch a native method through the generated bridge. fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { - signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS - && eval_signature_ref_params_supported(signature) + eval_signature_ref_params_supported(signature) && signature .params .iter() @@ -1109,8 +1107,7 @@ fn method_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { /// Returns true when eval can dispatch a native constructor through the generated bridge. fn constructor_signature_can_bridge_with_eval(signature: &FunctionSig) -> bool { - signature.params.len() <= MAX_EVAL_NATIVE_METHOD_PARAMS - && eval_signature_ref_params_supported(signature) + eval_signature_ref_params_supported(signature) && signature .params .iter() diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d577d4c0b2..2a585fa7c6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14772,6 +14772,35 @@ return $target->collect("H", "A", "B") . "|" . EvalAotVariadicBridgeTarget::coll assert_eq!(out, "H:2:A:B|2:S:T|C:2:D:E"); } +/// Verifies eval can dispatch generated/AOT methods and constructors beyond register-only arity. +#[test] +fn test_eval_aot_wide_method_and_constructor_bridge() { + let out = compile_and_run( + r#"label = $a . $b . $c . $d . $e . $f . $g . $h . $i; + } + + public function join($a, $b, $c, $d, $e, $f, $g, $h, $i): string { + return $a . $b . $c . $d . $e . $f . $g . $h . $i; + } + + public static function joinStatic($a, $b, $c, $d, $e, $f, $g, $h, $i): string { + return $a . $b . $c . $d . $e . $f . $g . $h . $i; + } +} +echo eval('$target = new EvalAotWideMethodBridgeTarget("A", "B", "C", "D", "E", "F", "G", "H", "I"); +return $target->join("1", "2", "3", "4", "5", "6", "7", "8", "9") + . "|" . EvalAotWideMethodBridgeTarget::joinStatic("a", "b", "c", "d", "e", "f", "g", "h", "i") + . "|" . $target->label;'); +"#, + ); + assert_eq!(out, "123456789|abcdefghi|ABCDEFGHI"); +} + /// Verifies generated/AOT variadic bridge rejects named arguments captured by the tail. #[test] fn test_eval_aot_variadic_method_rejects_named_tail() { From 90cbf3767fb02102910f63cbbdda0e19967656e5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 01:54:31 +0200 Subject: [PATCH 0813/1208] Support string-keyed eval attribute arrays --- .../interpreter/builtins/class_metadata.rs | 13 ++++++-- .../src/interpreter/statements.rs | 13 ++++++-- .../tests/builtins_class_metadata.rs | 24 ++++++++------ .../elephc-magician/src/parser/statements.rs | 13 ++++++-- .../src/parser/tests/classes_errors.rs | 7 +++-- docs/php/eval.md | 10 +++--- tests/codegen/eval.rs | 31 +++++++++++++++++++ 7 files changed, 88 insertions(+), 23 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index b46262187e..db665a3c53 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -337,14 +337,21 @@ fn eval_class_attribute_arg_value( } } -/// Materializes one retained positional attribute array literal as a runtime array cell. +/// Materializes one retained attribute array literal as a runtime array cell. fn eval_class_attribute_array_arg_value( elements: &[EvalAttributeArg], values: &mut impl RuntimeValueOps, ) -> Result { - let mut result = values.array_new(elements.len())?; + let mut result = if elements.iter().any(|element| element.name().is_some()) { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; for (index, element) in elements.iter().enumerate() { - let key = values.int(index as i64)?; + let key = match element.name() { + Some(name) => values.string(name)?, + None => values.int(index as i64)?, + }; let value = eval_class_attribute_arg_value(element.value(), values)?; result = values.array_set(result, key, value)?; } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index ecf264423c..0fce913218 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7404,14 +7404,21 @@ fn eval_reflection_attribute_arg_value( } } -/// Materializes one retained positional attribute array literal for constructor calls. +/// Materializes one retained attribute array literal for constructor calls. fn eval_reflection_attribute_array_arg_value( elements: &[EvalAttributeArg], values: &mut impl RuntimeValueOps, ) -> Result { - let mut result = values.array_new(elements.len())?; + let mut result = if elements.iter().any(|element| element.name().is_some()) { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; for (index, element) in elements.iter().enumerate() { - let key = values.int(index as i64)?; + let key = match element.name() { + Some(name) => values.string(name)?, + None => values.int(index as i64)?, + }; let value = eval_reflection_attribute_arg_value(element.value(), values)?; result = values.array_set(result, key, value)?; } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 80c0c624f0..e4bb6f96c5 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -3999,20 +3999,26 @@ return true;"#, /// Verifies unsupported attribute argument metadata remains name-visible but not materializable. #[test] fn execute_program_rejects_unsupported_class_attribute_args_metadata() { - let program = parse_fragment( + for source in [ br#"#[Tag($dynamic)] class EvalUnsupportedAttr {} $names = class_attribute_names("EvalUnsupportedAttr"); echo count($names); echo ":"; echo $names[0]; echo ":"; +class_attribute_args("EvalUnsupportedAttr", "Tag");"# as &[u8], + br#"#[Tag(["fixed" => "ok", $dynamic => "bad"])] +class EvalUnsupportedAttr {} +$names = class_attribute_names("EvalUnsupportedAttr"); +echo count($names); echo ":"; echo $names[0]; echo ":"; class_attribute_args("EvalUnsupportedAttr", "Tag");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("unsupported attribute metadata should fail"); + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unsupported attribute metadata should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - assert_eq!(values.output, "1:Tag:"); + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.output, "1:Tag:"); + } } diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 022e8a6b82..90abbc3367 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3664,7 +3664,7 @@ fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { } } -/// Converts a positional eval array literal into retained attribute metadata. +/// Converts an eval array literal into retained attribute metadata. fn eval_attribute_array_arg_from_elements( elements: &[EvalArrayElement], ) -> Option { @@ -3672,7 +3672,16 @@ fn eval_attribute_array_arg_from_elements( .iter() .map(|element| match element { EvalArrayElement::Value(value) => eval_attribute_arg_from_expr(value), - EvalArrayElement::KeyValue { .. } => None, + EvalArrayElement::KeyValue { key, value } => { + let value = eval_attribute_arg_from_expr(value)?; + match key { + EvalExpr::Const(EvalConst::String(name)) => Some(EvalAttributeArg::Named { + name: name.clone(), + value: Box::new(value), + }), + _ => None, + } + } }) .collect::>>() .map(EvalAttributeArg::Array) diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index de3216fc45..325b5ceb37 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -148,7 +148,7 @@ fn parse_fragment_rejects_reserved_class_constant_name() { #[test] fn parse_fragment_accepts_class_attribute_metadata() { let program = parse_fragment( - br#"#[Route("/home", -1, 1.5, -2.5, true, null, EvalAttrDep::class, ["nested", 2])] + br#"#[Route("/home", -1, 1.5, -2.5, true, null, EvalAttrDep::class, ["nested", "key" => 2])] #[Tag(name: "named")] class DynEvalAttributed {}"#, ) @@ -169,7 +169,10 @@ class DynEvalAttributed {}"#, EvalAttributeArg::String("EvalAttrDep".to_string()), EvalAttributeArg::Array(vec![ EvalAttributeArg::String("nested".to_string()), - EvalAttributeArg::Int(2), + EvalAttributeArg::Named { + name: "key".to_string(), + value: Box::new(EvalAttributeArg::Int(2)), + }, ]), ]), ), diff --git a/docs/php/eval.md b/docs/php/eval.md index 45a74e0bf5..ad0a85cdc5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -211,9 +211,10 @@ attributes, are visible through `class_attribute_names()`, `class_attribute_args()`, and `class_get_attributes()` when their arguments fit the supported literal positional/named subset (`string`, `int`, `float`, `bool`, `null`, negated numeric literals, `ClassName::class` strings, or positional -array literals containing the same supported values). Positional arguments keep integer keys in -`class_attribute_args()` / `ReflectionAttribute::getArguments()`, and named -arguments keep their PHP names as string keys. +or string-keyed array literals containing the same supported values). Positional +arguments keep integer keys in `class_attribute_args()` / +`ReflectionAttribute::getArguments()`, and named arguments keep their PHP names +as string keys. `ReflectionAttribute::newInstance()` instantiates eval-declared or bridge-supported generated/AOT attribute classes from those materialized attributes, and `ReflectionAttribute::getTarget()` / @@ -231,7 +232,8 @@ child methods and public access see the child property. property, class-constant, and method-parameter attributes for eval-declared class-like symbols and bridge-registered generated/AOT class-level, method, property, and class-constant attributes when their arguments fit the same -literal positional/named subset. Associative or dynamic attribute arrays are +literal positional/named subset. String-keyed attribute array literals keep +their PHP string keys; dynamic or otherwise unsupported attribute array keys are still unsupported metadata. `getName()` returns the reflected class, member, or parameter name for those owners. `ReflectionClass`, `ReflectionObject`, `ReflectionFunction`, `ReflectionMethod`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2a585fa7c6..03bfb97d1b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12166,6 +12166,37 @@ echo is_null($attrs[0]->newInstance()) ? "N" : "bad";'); ); } +/// Verifies eval attribute array arguments preserve string keys. +#[test] +fn test_eval_declared_class_attribute_string_keyed_array_args() { + let out = compile_and_run_capture( + r#"items = $items; + } +} +#[EvalArrayAttribute(["plain", "name" => "Ada", "nested" => ["inner" => "value"]])] +class EvalArrayAttributeTarget {} +$args = class_attribute_args("EvalArrayAttributeTarget", "EvalArrayAttribute"); +$items = $args[0]; +echo count($items) . ":" . $items[0] . ":" . $items["name"] . ":" . $items["nested"]["inner"] . ":"; +$attr = class_get_attributes("EvalArrayAttributeTarget")[0]; +$attrItems = $attr->getArguments()[0]; +echo count($attrItems) . ":" . $attrItems[0] . ":" . $attrItems["name"] . ":" . $attrItems["nested"]["inner"] . ":"; +$instanceItems = $attr->newInstance()->items; +echo count($instanceItems) . ":" . $instanceItems[0] . ":" . $instanceItems["name"] . ":" . $instanceItems["nested"]["inner"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "3:plain:Ada:value:3:plain:Ada:value:3:plain:Ada:value"); +} + /// Verifies eval can read generated/AOT float attribute arguments. #[test] fn test_eval_reflection_class_exposes_aot_float_attribute_args() { From 1bf64379eae60c420523d0138dd5704f7bb2c3cb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 02:15:43 +0200 Subject: [PATCH 0814/1208] Expose eval AOT method source locations --- .../src/interpreter/reflection.rs | 97 +++++++++++++++++-- .../src/interpreter/runtime_ops.rs | 5 + .../src/runtime_hooks/externs.rs | 1 + .../elephc-magician/src/runtime_hooks/ops.rs | 14 +++ docs/php/eval.md | 8 +- src/codegen/mod.rs | 1 + src/codegen_support/runtime/data/user.rs | 71 ++++++++++++-- src/codegen_support/runtime/eval_bridge.rs | 31 ++++++ tests/codegen/eval.rs | 21 ++++ 9 files changed, 234 insertions(+), 15 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index c0f9b1436e..d5ddf51bb5 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -50,6 +50,9 @@ const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; const EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC: u64 = 8192; const EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED: u64 = 16384; +const EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK: u64 = 0x00ff_ffff; +const EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT: u64 = 16; +const EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT: u64 = 40; const EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL: u64 = 1; const EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC: u64 = 2; const EVAL_REFLECTION_PARAMETER_FLAG_BY_REF: u64 = 4; @@ -86,6 +89,7 @@ struct EvalReflectionClassMetadata { /// Eval metadata needed to materialize one `ReflectionMethod` or `ReflectionProperty` owner object. struct EvalReflectionMemberMetadata { declaring_class_name: Option, + source_file: Option, source_location: Option, attributes: Vec, visibility: EvalVisibility, @@ -221,6 +225,7 @@ enum EvalReflectionFunctionMethodTarget { name: String, static_key: Option, static_variables: Vec, + source_file: Option, parameters: Vec, source_location: Option, visibility: Option, @@ -432,6 +437,7 @@ pub(in crate::interpreter) fn eval_reflection_class_source_location_result( .and_then(|metadata| metadata.source_location); eval_reflection_source_location_result( method_key.as_str(), + None, source_location, evaluated_args, context, @@ -1227,9 +1233,12 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( .map(Some) } "getfilename" | "getstartline" | "getendline" => { + let (source_file, source_location) = + eval_reflection_function_method_source_location(&target); eval_reflection_source_location_result( method_key.as_str(), - eval_reflection_function_method_source_location(&target), + source_file, + source_location, evaluated_args, context, values, @@ -3939,6 +3948,7 @@ fn eval_reflection_dynamic_property_name_is_visible( fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflectionMemberMetadata { EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_file: None, source_location: None, attributes: Vec::new(), visibility: EvalVisibility::Public, @@ -3985,6 +3995,8 @@ fn eval_reflection_aot_method_metadata_if_exists( flags, Vec::new(), None, + None, + None, ))) } @@ -4014,15 +4026,47 @@ fn eval_reflection_aot_method_metadata_with_signature_if_exists( method_name, context, ); + let (source_file, source_location) = + eval_reflection_aot_method_source_metadata(flags, values)?; Ok(Some(eval_reflection_aot_method_metadata( &declaring_class_name, method_name, flags, attributes, + source_file, + source_location, signature.as_ref(), ))) } +/// Returns AOT source-file and line metadata encoded in ReflectionMethod flags. +fn eval_reflection_aot_method_source_metadata( + flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, Option), EvalStatus> { + let Some(source_location) = eval_reflection_aot_method_source_location_from_flags(flags) else { + return Ok((None, None)); + }; + let Some(source_file) = values.reflection_source_file()? else { + return Ok((None, None)); + }; + Ok((Some(source_file), Some(source_location))) +} + +/// Decodes AOT ReflectionMethod source lines packed into high flag bits. +fn eval_reflection_aot_method_source_location_from_flags( + flags: u64, +) -> Option { + let start_line = + ((flags >> EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT) + & EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK) as i64; + let end_line = + ((flags >> EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT) + & EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK) as i64; + (start_line > 0 && end_line >= start_line) + .then(|| EvalSourceLocation::new(start_line, end_line)) +} + /// Returns generated/AOT method dispatch metadata for interpreter-only runtime decisions. pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata( class_name: &str, @@ -4051,6 +4095,8 @@ fn eval_reflection_aot_method_metadata( method_name: &str, flags: u64, attributes: Vec, + source_file: Option, + source_location: Option, signature: Option<&NativeCallableSignature>, ) -> EvalReflectionMemberMetadata { let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { @@ -4070,7 +4116,8 @@ fn eval_reflection_aot_method_metadata( .and_then(eval_reflection_parameter_type_metadata); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), - source_location: None, + source_file, + source_location, attributes, visibility, is_static: flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, @@ -4444,6 +4491,7 @@ fn eval_reflection_aot_property_metadata( } EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_file: None, source_location: None, attributes, visibility, @@ -7109,6 +7157,7 @@ fn eval_reflection_method_metadata( ); return Some(EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class), + source_file: None, source_location: method.source_location(), attributes: method.attributes().to_vec(), visibility: method.visibility(), @@ -7182,6 +7231,7 @@ fn eval_reflection_method_metadata( ); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.to_string()), + source_file: None, source_location: method.source_location(), attributes: method.attributes().to_vec(), visibility: EvalVisibility::Public, @@ -7255,6 +7305,7 @@ fn eval_reflection_method_metadata( ); EvalReflectionMemberMetadata { declaring_class_name: Some(trait_decl.name().to_string()), + source_file: None, source_location: method.source_location(), attributes: method.attributes().to_vec(), visibility: method.visibility(), @@ -7349,6 +7400,7 @@ fn eval_reflection_enum_synthetic_method_metadata( ); Some(EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class_name), + source_file: None, source_location: None, attributes: Vec::new(), visibility: EvalVisibility::Public, @@ -7380,6 +7432,7 @@ fn eval_reflection_property_metadata( let default_value = eval_reflection_property_default_value(&property); EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class), + source_file: None, source_location: None, attributes: property.attributes().to_vec(), visibility: property.visibility(), @@ -7417,6 +7470,7 @@ fn eval_reflection_property_metadata( .find(|property| property.name() == property_name) .map(|property| EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.to_string()), + source_file: None, source_location: None, attributes: property.attributes().to_vec(), visibility: EvalVisibility::Public, @@ -7454,6 +7508,7 @@ fn eval_reflection_property_metadata( let default_value = eval_reflection_property_default_value(property); EvalReflectionMemberMetadata { declaring_class_name: Some(trait_decl.name().to_string()), + source_file: None, source_location: None, attributes: property.attributes().to_vec(), visibility: property.visibility(), @@ -7865,6 +7920,7 @@ fn eval_reflection_property_hook_method_metadata( let required_parameter_count = parameters.len(); EvalReflectionMemberMetadata { declaring_class_name: Some(declaring_class.to_string()), + source_file: None, source_location: None, attributes: Vec::new(), visibility: property.visibility(), @@ -9031,6 +9087,7 @@ fn eval_reflection_function_method_target( }; let ( parameters, + source_file, source_location, visibility, is_variadic, @@ -9049,6 +9106,7 @@ fn eval_reflection_function_method_target( eval_reflection_attributes_include_deprecated(&method.attributes); ( method.parameters, + method.source_file, method.source_location, Some(method.visibility), is_variadic, @@ -9059,7 +9117,18 @@ fn eval_reflection_function_method_target( method.return_type_metadata, ) } - None => (Vec::new(), None, None, false, false, false, false, false, None), + None => ( + Vec::new(), + None, + None, + None, + false, + false, + false, + false, + false, + None, + ), }; let static_method = eval_reflection_eval_method_static_target(declaring_class, method_name, context); @@ -9079,6 +9148,7 @@ fn eval_reflection_function_method_target( static_key, static_variables, parameters, + source_file, source_location, visibility, is_variadic, @@ -9235,6 +9305,7 @@ fn eval_reflection_false_metadata_result( /// Returns source file or line metadata for eval-backed reflection objects. fn eval_reflection_source_location_result( method_key: &str, + source_file: Option<&str>, source_location: Option, evaluated_args: Vec, context: &ElephcEvalContext, @@ -9245,7 +9316,16 @@ fn eval_reflection_source_location_result( return values.bool_value(false).map(Some); }; match method_key { - "getfilename" => values.string(&context.eval_file_magic()).map(Some), + "getfilename" => { + let eval_file; + let file = if let Some(source_file) = source_file { + source_file + } else { + eval_file = context.eval_file_magic(); + &eval_file + }; + values.string(file).map(Some) + } "getstartline" => values.int(source_location.start_line()).map(Some), "getendline" => values.int(source_location.end_line()).map(Some), _ => Ok(None), @@ -9267,14 +9347,15 @@ fn eval_reflection_function_method_short_name( /// Returns eval-fragment source metadata for a ReflectionFunction or ReflectionMethod target. fn eval_reflection_function_method_source_location( target: &EvalReflectionFunctionMethodTarget, -) -> Option { +) -> (Option<&str>, Option) { match target { EvalReflectionFunctionMethodTarget::Function { source_location, .. - } - | EvalReflectionFunctionMethodTarget::Method { + } => (None, *source_location), + EvalReflectionFunctionMethodTarget::Method { + source_file, source_location, .. - } => *source_location, + } => (source_file.as_deref(), *source_location), } } diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index fa88d1816f..067a5c149d 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -197,6 +197,11 @@ pub trait RuntimeValueOps { class_name: &str, ) -> Result; + /// Returns the generated program source file used for AOT reflection metadata. + fn reflection_source_file(&mut self) -> Result, EvalStatus> { + Ok(None) + } + /// Returns generated AOT ReflectionClass modifier flags for one class. fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus>; diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index c471981bbe..425d03c39b 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -167,6 +167,7 @@ unsafe extern "C" { class_ptr: *const u8, class_len: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_reflection_source_file() -> *mut RuntimeCell; pub(super) fn __elephc_eval_reflection_class_flags( class_ptr: *const u8, class_len: u64, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 07c3bb887a..08aeffe765 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -430,6 +430,20 @@ impl RuntimeValueOps for ElephcRuntimeOps { }) } + /// Returns generated AOT source-file metadata for reflection source-location calls. + fn reflection_source_file(&mut self) -> Result, EvalStatus> { + let ptr = unsafe { __elephc_eval_reflection_source_file() }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + /// Returns generated AOT ReflectionClass modifier flags, or `None` when no row matches. fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { let flags = unsafe { diff --git a/docs/php/eval.md b/docs/php/eval.md index ad0a85cdc5..bd3235545b 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -246,6 +246,10 @@ report `false` / `null` for eval-declared user symbols. `getFileName()`, `getStartLine()`, and `getEndLine()` for parser-backed eval declarations. File names use PHP's synthetic eval file format from the current eval call site, and line numbers are one-based inside the evaluated fragment. +Generated/AOT `ReflectionMethod` metadata exposes the original source file and +method declaration line when EIR source metadata is available. AOT method +`getEndLine()` currently reports the declaration line because the bridge keeps +the method declaration span, not the full method body span. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. `ReflectionObject` construction accepts object arguments and exposes the @@ -837,7 +841,9 @@ than a fixed parameter count. Generated/AOT free-function and method type metada return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while other unsupported bridge shapes remain metadata-only -rather than invocable through eval. +rather than invocable through eval. Generated/AOT `ReflectionClass` +source-location metadata is still unavailable because compiler class metadata +does not yet retain the class declaration span. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 0a51e5e705..69b85e862d 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -245,6 +245,7 @@ fn finalize_user_asm( &runtime_classes, &module.enum_infos, Some(&allowed_class_names), + module.source_path.as_deref(), ); let mut user_asm = emitter.output(); diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index f533617591..a94e9df5fd 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -39,6 +39,9 @@ const EVAL_REFLECTION_METHOD_FLAG_PROTECTED: u64 = 4; const EVAL_REFLECTION_METHOD_FLAG_PRIVATE: u64 = 8; const EVAL_REFLECTION_METHOD_FLAG_FINAL: u64 = 16; const EVAL_REFLECTION_METHOD_FLAG_ABSTRACT: u64 = 32; +const EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK: u64 = 0x00ff_ffff; +const EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT: u64 = 16; +const EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT: u64 = 40; /// Emit the user-dependent data section — globals, statics, class metadata. /// This changes per program and cannot be cached. @@ -54,6 +57,7 @@ pub(crate) fn emit_runtime_data_user( classes: &HashMap, enums: &HashMap, allowed_class_names: Option<&HashSet>, + source_path: Option<&str>, ) -> String { let mut out = String::new(); @@ -474,6 +478,8 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); emit_static_callable_method_data(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); + emit_eval_reflection_source_file_data(&mut out, source_path); + out.push_str(".p2align 3\n"); emit_classes_by_name_table(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); emit_eval_reflection_method_lookup_data(&mut out, &sorted_classes); @@ -975,25 +981,45 @@ fn emit_name_lookup_data( } } +/// Emits the source filename used by eval Reflection source-location hooks. +fn emit_eval_reflection_source_file_data(out: &mut String, source_path: Option<&str>) { + let source_path = source_path.unwrap_or(""); + out.push_str(".globl _eval_reflection_source_file\n_eval_reflection_source_file:\n"); + out.push_str(&format!(" .ascii \"{}\"\n", escaped_ascii(source_path))); + out.push_str(".p2align 3\n"); + out.push_str(".globl _eval_reflection_source_file_len\n_eval_reflection_source_file_len:\n"); + out.push_str(&format!(" .quad {}\n", source_path.len())); +} + /// Emits AOT method flag rows consumed by eval ReflectionMethod metadata probes. fn emit_eval_reflection_method_lookup_data( out: &mut String, sorted_classes: &[(&String, &ClassInfo)], ) { let mut entries = Vec::new(); + let class_infos = sorted_classes + .iter() + .map(|(name, info)| (name.as_str(), *info)) + .collect::>(); let mut index = 0usize; for (class_name, class_info) in sorted_classes { let mut methods = class_info.methods.keys().collect::>(); methods.sort(); for method_name in methods { - let flags = eval_reflection_instance_method_flags(class_info, method_name); - let class_label = format!("_eval_reflection_method_class_{}", index); - let method_label = format!("_eval_reflection_method_name_{}", index); let declaring_class = eval_reflection_instance_method_declaring_class( class_name, class_info, method_name, ); + let declaring_info = class_infos.get(declaring_class).copied().unwrap_or(class_info); + let flags = eval_reflection_method_flags_with_source_lines( + eval_reflection_instance_method_flags(class_info, method_name), + declaring_info, + method_name, + false, + ); + let class_label = format!("_eval_reflection_method_class_{}", index); + let method_label = format!("_eval_reflection_method_name_{}", index); let declaring_label = format!("_eval_reflection_method_declaring_class_{}", index); out.push_str(&format!( ".globl {0}\n{0}:\n .ascii \"{1}\"\n", @@ -1025,11 +1051,17 @@ fn emit_eval_reflection_method_lookup_data( let mut static_methods = class_info.static_methods.keys().collect::>(); static_methods.sort(); for method_name in static_methods { - let flags = eval_reflection_static_method_flags(class_info, method_name); - let class_label = format!("_eval_reflection_method_class_{}", index); - let method_label = format!("_eval_reflection_method_name_{}", index); let declaring_class = eval_reflection_static_method_declaring_class(class_name, class_info, method_name); + let declaring_info = class_infos.get(declaring_class).copied().unwrap_or(class_info); + let flags = eval_reflection_method_flags_with_source_lines( + eval_reflection_static_method_flags(class_info, method_name), + declaring_info, + method_name, + true, + ); + let class_label = format!("_eval_reflection_method_class_{}", index); + let method_label = format!("_eval_reflection_method_name_{}", index); let declaring_label = format!("_eval_reflection_method_declaring_class_{}", index); out.push_str(&format!( ".globl {0}\n{0}:\n .ascii \"{1}\"\n", @@ -1076,6 +1108,31 @@ fn emit_eval_reflection_method_lookup_data( } } +/// Adds source start/end line bits to an AOT ReflectionMethod flag word when available. +fn eval_reflection_method_flags_with_source_lines( + flags: u64, + class_info: &ClassInfo, + method_name: &str, + is_static: bool, +) -> u64 { + let Some(method) = class_info + .method_decls + .iter() + .find(|method| method.is_static == is_static && php_symbol_key(&method.name) == method_name) + else { + return flags; + }; + let Ok(start_line) = u64::try_from(method.span.line) else { + return flags; + }; + if start_line == 0 || start_line > EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK { + return flags; + } + flags + | (start_line << EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT) + | (start_line << EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT) +} + /// Returns the class name that declares one visible instance method. fn eval_reflection_instance_method_declaring_class<'a>( reflected_class: &'a str, @@ -2204,6 +2261,7 @@ mod tests { &classes, &HashMap::new(), Some(&allowed_class_names), + None, ); assert!(asm.contains("_class_vtable_1")); @@ -2232,6 +2290,7 @@ mod tests { &classes, &HashMap::new(), None, + None, ); assert!(asm.contains("_class_gc_desc_count:\n .quad 4\n")); diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 4eb4dbff5a..1a27959c15 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -186,6 +186,7 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emit_aarch64_eval_reflection_class_trait_names(emitter); emit_aarch64_eval_reflection_class_trait_alias_names(emitter); emit_aarch64_eval_reflection_class_trait_alias_sources(emitter); + emit_aarch64_eval_reflection_source_file(emitter); emit_aarch64_eval_reflection_class_flags(emitter); emit_aarch64_eval_reflection_method_flags(emitter); emit_aarch64_eval_reflection_method_declaring_class(emitter); @@ -1649,6 +1650,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emit_x86_64_eval_reflection_class_trait_names(emitter); emit_x86_64_eval_reflection_class_trait_alias_names(emitter); emit_x86_64_eval_reflection_class_trait_alias_sources(emitter); + emit_x86_64_eval_reflection_source_file(emitter); emit_x86_64_eval_reflection_class_flags(emitter); emit_x86_64_eval_reflection_method_flags(emitter); emit_x86_64_eval_reflection_method_declaring_class(emitter); @@ -3186,6 +3188,20 @@ fn emit_aarch64_eval_reflection_class_trait_alias_sources(emitter: &mut Emitter) ); } +/// Emits the ARM64 eval hook that returns the AOT reflection source file. +fn emit_aarch64_eval_reflection_source_file(emitter: &mut Emitter) { + let string_symbol = emitter.target.extern_symbol("__elephc_eval_value_string"); + label_c_global(emitter, "__elephc_eval_reflection_source_file"); + abi::emit_symbol_address(emitter, "x9", "_eval_reflection_source_file_len"); + emitter.instruction("ldr x1, [x9]"); // load the generated source-file length + emitter.instruction("cbz x1, __elephc_eval_reflection_source_file_miss"); // report no source file when EIR metadata is absent + abi::emit_symbol_address(emitter, "x0", "_eval_reflection_source_file"); + emitter.instruction(&format!("b {string_symbol}")); // box the generated source-file path for Rust + emitter.label("__elephc_eval_reflection_source_file_miss"); + emitter.instruction("mov x0, xzr"); // return null when no source file is available + emitter.instruction("ret"); // finish the source-file metadata lookup +} + /// Emits an ARM64 class-filtered AOT reflection member-name scanner. fn emit_aarch64_eval_reflection_member_names( emitter: &mut Emitter, @@ -3615,6 +3631,21 @@ fn emit_x86_64_eval_reflection_class_trait_alias_sources(emitter: &mut Emitter) ); } +/// Emits the x86_64 eval hook that returns the AOT reflection source file. +fn emit_x86_64_eval_reflection_source_file(emitter: &mut Emitter) { + let string_symbol = emitter.target.extern_symbol("__elephc_eval_value_string"); + label_c_global(emitter, "__elephc_eval_reflection_source_file"); + abi::emit_symbol_address(emitter, "r10", "_eval_reflection_source_file_len"); + emitter.instruction("mov rsi, QWORD PTR [r10]"); // load the generated source-file length + emitter.instruction("test rsi, rsi"); // is source-file metadata available? + emitter.instruction("jz __elephc_eval_reflection_source_file_miss_x86"); // report no source file when EIR metadata is absent + abi::emit_symbol_address(emitter, "rdi", "_eval_reflection_source_file"); + emitter.instruction(&format!("jmp {string_symbol}")); // box the generated source-file path for Rust + emitter.label("__elephc_eval_reflection_source_file_miss_x86"); + emitter.instruction("xor eax, eax"); // return null when no source file is available + emitter.instruction("ret"); // finish the source-file metadata lookup +} + /// Emits an x86_64 class-filtered AOT reflection member-name scanner. fn emit_x86_64_eval_reflection_member_names( emitter: &mut Emitter, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 03bfb97d1b..63082afeae 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12322,6 +12322,27 @@ echo $function->getStartLine(); echo ":"; echo $function->getEndLine();'); assert_eq!(out, "F:1:5:2:4:6:8"); } +/// Verifies eval ReflectionMethod exposes generated/AOT source-location metadata. +#[test] +fn test_eval_reflection_aot_method_source_locations() { + let out = compile_and_run_capture( + r#"getFileName() === false ? "f" : "F"; echo ":"; +echo $method->getStartLine(); echo ":"; echo $method->getEndLine();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "F:3:3"); +} + /// Verifies eval ReflectionAttribute exposes owner target and repetition metadata. #[test] fn test_eval_reflection_attribute_target_and_repetition() { From 4e1ec1c5b92b953284ccc771146c271db7766fdc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 02:29:19 +0200 Subject: [PATCH 0815/1208] Expose eval AOT class source locations --- .../src/interpreter/reflection.rs | 40 +++++++++++++++++-- docs/php/eval.md | 12 +++--- src/codegen_support/runtime/data/user.rs | 12 ++++++ src/ir_lower/tests/exhaustive.rs | 1 + src/types/checker/builtin_enums.rs | 2 + src/types/checker/builtin_iterators.rs | 1 + .../checker/builtin_spl_classes/append.rs | 1 + .../append_array_iterator.rs | 1 + .../checker/builtin_spl_classes/caching.rs | 1 + .../checker/builtin_spl_classes/containers.rs | 5 +++ .../checker/builtin_spl_classes/filesystem.rs | 8 ++++ .../checker/builtin_spl_classes/filters.rs | 2 + .../checker/builtin_spl_classes/forwarding.rs | 4 ++ .../checker/builtin_spl_classes/heaps.rs | 4 ++ .../checker/builtin_spl_classes/multiple.rs | 1 + .../builtin_spl_classes/object_storage.rs | 1 + src/types/checker/builtin_spl_classes/phar.rs | 2 + .../checker/builtin_spl_classes/recursive.rs | 3 ++ .../builtin_spl_classes/recursive_array.rs | 1 + .../recursive_iterator_iterator.rs | 1 + .../checker/builtin_spl_classes/regex.rs | 2 + .../checker/builtin_spl_classes/storage.rs | 3 ++ src/types/checker/builtin_spl_exceptions.rs | 1 + src/types/checker/builtin_stdclass.rs | 1 + .../checker/builtin_types/declarations.rs | 9 +++++ src/types/checker/builtin_user_filter.rs | 1 + src/types/checker/driver/mod.rs | 1 + src/types/checker/schema/classes/state.rs | 1 + src/types/checker/schema/enums.rs | 3 ++ src/types/schema.rs | 2 + src/types/traits.rs | 3 ++ tests/codegen/eval.rs | 25 ++++++++++++ 32 files changed, 145 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index d5ddf51bb5..e41877f94e 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -29,6 +29,9 @@ const EVAL_REFLECTION_CLASS_FLAG_INTERNAL: u64 = 256; const EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED: u64 = 512; const EVAL_REFLECTION_CLASS_FLAG_ITERABLE: u64 = 1024; const EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS: u64 = 2048; +const EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK: u64 = 0x00ff_ffff; +const EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT: u64 = 16; +const EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT: u64 = 40; const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; @@ -433,11 +436,15 @@ pub(in crate::interpreter) fn eval_reflection_class_source_location_result( let Some(reflected_name) = context.eval_reflection_class_name(identity) else { return Ok(None); }; - let source_location = eval_reflection_class_like_attributes(reflected_name, context) - .and_then(|metadata| metadata.source_location); + let (source_file, source_location) = + if let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) { + (None, metadata.source_location) + } else { + eval_reflection_aot_class_source_metadata(reflected_name, values)? + }; eval_reflection_source_location_result( method_key.as_str(), - None, + source_file.as_deref(), source_location, evaluated_args, context, @@ -445,6 +452,33 @@ pub(in crate::interpreter) fn eval_reflection_class_source_location_result( ) } +/// Returns AOT source-file and line metadata for a generated ReflectionClass. +fn eval_reflection_aot_class_source_metadata( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, Option), EvalStatus> { + let Some(flags) = values.reflection_class_flags(class_name.trim_start_matches('\\'))? else { + return Ok((None, None)); + }; + let Some(source_location) = eval_reflection_aot_class_source_location_from_flags(flags) else { + return Ok((None, None)); + }; + let Some(source_file) = values.reflection_source_file()? else { + return Ok((None, None)); + }; + Ok((Some(source_file), Some(source_location))) +} + +/// Decodes AOT ReflectionClass source lines packed into high flag bits. +fn eval_reflection_aot_class_source_location_from_flags(flags: u64) -> Option { + let start_line = ((flags >> EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT) + & EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK) as i64; + let end_line = ((flags >> EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT) + & EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK) as i64; + (start_line > 0 && end_line >= start_line) + .then(|| EvalSourceLocation::new(start_line, end_line)) +} + /// Handles eval-backed `ReflectionClass` scalar metadata methods. pub(in crate::interpreter) fn eval_reflection_class_basic_metadata_result( identity: u64, diff --git a/docs/php/eval.md b/docs/php/eval.md index bd3235545b..3f41816ef4 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -246,10 +246,10 @@ report `false` / `null` for eval-declared user symbols. `getFileName()`, `getStartLine()`, and `getEndLine()` for parser-backed eval declarations. File names use PHP's synthetic eval file format from the current eval call site, and line numbers are one-based inside the evaluated fragment. -Generated/AOT `ReflectionMethod` metadata exposes the original source file and -method declaration line when EIR source metadata is available. AOT method -`getEndLine()` currently reports the declaration line because the bridge keeps -the method declaration span, not the full method body span. +Generated/AOT metadata for `ReflectionClass` and `ReflectionMethod` exposes +the original source file and declaration line when EIR source metadata is +available. AOT `getEndLine()` currently reports the declaration line because +the bridge keeps declaration spans, not full body spans. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. `ReflectionObject` construction accepts object arguments and exposes the @@ -841,9 +841,7 @@ than a fixed parameter count. Generated/AOT free-function and method type metada return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while other unsupported bridge shapes remain metadata-only -rather than invocable through eval. Generated/AOT `ReflectionClass` -source-location metadata is still unavailable because compiler class metadata -does not yet retain the class declaration span. +rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index a94e9df5fd..b22efe5872 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -22,6 +22,9 @@ use super::instanceof::{escaped_ascii, escaped_bytes}; const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; +const EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK: u64 = 0x00ff_ffff; +const EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT: u64 = 16; +const EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT: u64 = 40; const EVAL_REFLECTION_PROPERTY_FLAG_STATIC: u64 = 1; const EVAL_REFLECTION_PROPERTY_FLAG_PUBLIC: u64 = 2; const EVAL_REFLECTION_PROPERTY_FLAG_PROTECTED: u64 = 4; @@ -1373,6 +1376,14 @@ fn eval_reflection_class_flags(class_info: &ClassInfo) -> u64 { if class_info.is_readonly_class { flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; } + let Ok(start_line) = u64::try_from(class_info.declaration_span.line) else { + return flags; + }; + if start_line == 0 || start_line > EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK { + return flags; + } + flags |= (start_line << EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT) + | (start_line << EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT); flags } @@ -2171,6 +2182,7 @@ mod tests { ClassInfo { class_id, + declaration_span: crate::span::Span::dummy(), parent: None, is_abstract: false, is_final: false, diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 5e6aaba187..5bd4bd427f 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -161,6 +161,7 @@ fn class_info(_class_name: &str) -> ClassInfo { static_methods.insert("sm".to_string(), method_sig); ClassInfo { class_id: 1, + declaration_span: crate::span::Span::dummy(), parent: None, is_abstract: false, is_final: false, diff --git a/src/types/checker/builtin_enums.rs b/src/types/checker/builtin_enums.rs index 1ae00c6377..1015bb9370 100644 --- a/src/types/checker/builtin_enums.rs +++ b/src/types/checker/builtin_enums.rs @@ -51,6 +51,7 @@ pub(crate) fn inject_builtin_enums( &[], &[], &[], + crate::span::Span::dummy(), checker, next_class_id, )?; @@ -78,6 +79,7 @@ pub(crate) fn inject_builtin_enums( &[], &[], &[], + crate::span::Span::dummy(), checker, next_class_id, ) diff --git a/src/types/checker/builtin_iterators.rs b/src/types/checker/builtin_iterators.rs index 80c921270a..9dbc3188e2 100644 --- a/src/types/checker/builtin_iterators.rs +++ b/src/types/checker/builtin_iterators.rs @@ -53,6 +53,7 @@ pub(crate) fn inject_builtin_iterators( "Generator".to_string(), FlattenedClass { name: "Generator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string()], is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/append.rs b/src/types/checker/builtin_spl_classes/append.rs index d5236b5b0f..0c13ba0258 100644 --- a/src/types/checker/builtin_spl_classes/append.rs +++ b/src/types/checker/builtin_spl_classes/append.rs @@ -25,6 +25,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "AppendIterator".to_string(), FlattenedClass { name: "AppendIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/append_array_iterator.rs b/src/types/checker/builtin_spl_classes/append_array_iterator.rs index 7c68fffbf4..b97e2d14c4 100644 --- a/src/types/checker/builtin_spl_classes/append_array_iterator.rs +++ b/src/types/checker/builtin_spl_classes/append_array_iterator.rs @@ -22,6 +22,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "__ElephcAppendIteratorArrayIterator".to_string(), FlattenedClass { name: "__ElephcAppendIteratorArrayIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("ArrayIterator".to_string()), implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/caching.rs b/src/types/checker/builtin_spl_classes/caching.rs index bfba95afa7..857d3f2300 100644 --- a/src/types/checker/builtin_spl_classes/caching.rs +++ b/src/types/checker/builtin_spl_classes/caching.rs @@ -23,6 +23,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "CachingIterator".to_string(), FlattenedClass { name: "CachingIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: vec![ "ArrayAccess".to_string(), diff --git a/src/types/checker/builtin_spl_classes/containers.rs b/src/types/checker/builtin_spl_classes/containers.rs index a4d1c3088a..9ce2afc478 100644 --- a/src/types/checker/builtin_spl_classes/containers.rs +++ b/src/types/checker/builtin_spl_classes/containers.rs @@ -24,6 +24,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplDoublyLinkedList".to_string(), FlattenedClass { name: "SplDoublyLinkedList".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "Iterator".to_string(), @@ -46,6 +47,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplStack".to_string(), FlattenedClass { name: "SplStack".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplDoublyLinkedList".to_string()), implements: Vec::new(), is_abstract: false, @@ -64,6 +66,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplQueue".to_string(), FlattenedClass { name: "SplQueue".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplDoublyLinkedList".to_string()), implements: Vec::new(), is_abstract: false, @@ -85,6 +88,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplFixedArray".to_string(), FlattenedClass { name: "SplFixedArray".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "IteratorAggregate".to_string(), @@ -108,6 +112,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "InternalIterator".to_string(), FlattenedClass { name: "InternalIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string()], is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/filesystem.rs b/src/types/checker/builtin_spl_classes/filesystem.rs index 2d8ef6c613..b241589875 100644 --- a/src/types/checker/builtin_spl_classes/filesystem.rs +++ b/src/types/checker/builtin_spl_classes/filesystem.rs @@ -42,6 +42,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplFileInfo".to_string(), FlattenedClass { name: "SplFileInfo".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Stringable".to_string()], is_abstract: false, @@ -60,6 +61,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplFileObject".to_string(), FlattenedClass { name: "SplFileObject".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplFileInfo".to_string()), implements: vec!["RecursiveIterator".to_string(), "SeekableIterator".to_string()], is_abstract: false, @@ -78,6 +80,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplTempFileObject".to_string(), FlattenedClass { name: "SplTempFileObject".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplFileObject".to_string()), implements: Vec::new(), is_abstract: false, @@ -96,6 +99,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "DirectoryIterator".to_string(), FlattenedClass { name: "DirectoryIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplFileInfo".to_string()), implements: vec!["Iterator".to_string(), "SeekableIterator".to_string()], is_abstract: false, @@ -114,6 +118,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "FilesystemIterator".to_string(), FlattenedClass { name: "FilesystemIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("DirectoryIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -132,6 +137,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "GlobIterator".to_string(), FlattenedClass { name: "GlobIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilesystemIterator".to_string()), implements: vec!["Countable".to_string()], is_abstract: false, @@ -150,6 +156,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveDirectoryIterator".to_string(), FlattenedClass { name: "RecursiveDirectoryIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilesystemIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, @@ -168,6 +175,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveCachingIterator".to_string(), FlattenedClass { name: "RecursiveCachingIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("CachingIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/filters.rs b/src/types/checker/builtin_spl_classes/filters.rs index e5c9d71442..a0bdb2b76b 100644 --- a/src/types/checker/builtin_spl_classes/filters.rs +++ b/src/types/checker/builtin_spl_classes/filters.rs @@ -23,6 +23,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "FilterIterator".to_string(), FlattenedClass { name: "FilterIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: true, @@ -41,6 +42,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "CallbackFilterIterator".to_string(), FlattenedClass { name: "CallbackFilterIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilterIterator".to_string()), implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/forwarding.rs b/src/types/checker/builtin_spl_classes/forwarding.rs index 1c42433c58..d630888c6b 100644 --- a/src/types/checker/builtin_spl_classes/forwarding.rs +++ b/src/types/checker/builtin_spl_classes/forwarding.rs @@ -23,6 +23,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "IteratorIterator".to_string(), FlattenedClass { name: "IteratorIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["OuterIterator".to_string()], is_abstract: false, @@ -41,6 +42,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "LimitIterator".to_string(), FlattenedClass { name: "LimitIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -59,6 +61,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "NoRewindIterator".to_string(), FlattenedClass { name: "NoRewindIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -77,6 +80,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "InfiniteIterator".to_string(), FlattenedClass { name: "InfiniteIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("IteratorIterator".to_string()), implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/heaps.rs b/src/types/checker/builtin_spl_classes/heaps.rs index 66b09c7a6e..c52b7101ce 100644 --- a/src/types/checker/builtin_spl_classes/heaps.rs +++ b/src/types/checker/builtin_spl_classes/heaps.rs @@ -22,6 +22,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplHeap".to_string(), FlattenedClass { name: "SplHeap".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string(), "Countable".to_string()], is_abstract: true, @@ -40,6 +41,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplMaxHeap".to_string(), FlattenedClass { name: "SplMaxHeap".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplHeap".to_string()), implements: Vec::new(), is_abstract: false, @@ -58,6 +60,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplMinHeap".to_string(), FlattenedClass { name: "SplMinHeap".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplHeap".to_string()), implements: Vec::new(), is_abstract: false, @@ -76,6 +79,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "SplPriorityQueue".to_string(), FlattenedClass { name: "SplPriorityQueue".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string(), "Countable".to_string()], is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/multiple.rs b/src/types/checker/builtin_spl_classes/multiple.rs index 8de2d45b39..0950fbe81a 100644 --- a/src/types/checker/builtin_spl_classes/multiple.rs +++ b/src/types/checker/builtin_spl_classes/multiple.rs @@ -22,6 +22,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "MultipleIterator".to_string(), FlattenedClass { name: "MultipleIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string()], is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/object_storage.rs b/src/types/checker/builtin_spl_classes/object_storage.rs index 8177d726c4..d6e14c828c 100644 --- a/src/types/checker/builtin_spl_classes/object_storage.rs +++ b/src/types/checker/builtin_spl_classes/object_storage.rs @@ -22,6 +22,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "SplObjectStorage".to_string(), FlattenedClass { name: "SplObjectStorage".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "Iterator".to_string(), diff --git a/src/types/checker/builtin_spl_classes/phar.rs b/src/types/checker/builtin_spl_classes/phar.rs index be66d78183..29359a6bea 100644 --- a/src/types/checker/builtin_spl_classes/phar.rs +++ b/src/types/checker/builtin_spl_classes/phar.rs @@ -32,6 +32,7 @@ fn insert_phar_like_class(class_map: &mut HashMap, name: name.to_string(), FlattenedClass { name: name.to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "ArrayAccess".to_string(), @@ -57,6 +58,7 @@ fn insert_phar_file_info_class(class_map: &mut HashMap) "PharFileInfo".to_string(), FlattenedClass { name: "PharFileInfo".to_string(), + span: crate::span::Span::dummy(), extends: Some("SplFileInfo".to_string()), implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/recursive.rs b/src/types/checker/builtin_spl_classes/recursive.rs index c1f93b48e0..1b76375d8d 100644 --- a/src/types/checker/builtin_spl_classes/recursive.rs +++ b/src/types/checker/builtin_spl_classes/recursive.rs @@ -25,6 +25,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveFilterIterator".to_string(), FlattenedClass { name: "RecursiveFilterIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilterIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: true, @@ -43,6 +44,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveCallbackFilterIterator".to_string(), FlattenedClass { name: "RecursiveCallbackFilterIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("CallbackFilterIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, @@ -61,6 +63,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "ParentIterator".to_string(), FlattenedClass { name: "ParentIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("RecursiveFilterIterator".to_string()), implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/recursive_array.rs b/src/types/checker/builtin_spl_classes/recursive_array.rs index d307ae12f8..05bcc6bb25 100644 --- a/src/types/checker/builtin_spl_classes/recursive_array.rs +++ b/src/types/checker/builtin_spl_classes/recursive_array.rs @@ -22,6 +22,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "RecursiveArrayIterator".to_string(), FlattenedClass { name: "RecursiveArrayIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("ArrayIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs b/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs index c3ff9275b3..47847a2e8d 100644 --- a/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs +++ b/src/types/checker/builtin_spl_classes/recursive_iterator_iterator.rs @@ -23,6 +23,7 @@ pub(super) fn insert_class(class_map: &mut HashMap) { "RecursiveIteratorIterator".to_string(), FlattenedClass { name: "RecursiveIteratorIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["OuterIterator".to_string()], is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/regex.rs b/src/types/checker/builtin_spl_classes/regex.rs index fe656eb33e..1a5b4d7af2 100644 --- a/src/types/checker/builtin_spl_classes/regex.rs +++ b/src/types/checker/builtin_spl_classes/regex.rs @@ -37,6 +37,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RegexIterator".to_string(), FlattenedClass { name: "RegexIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("FilterIterator".to_string()), implements: Vec::new(), is_abstract: false, @@ -55,6 +56,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "RecursiveRegexIterator".to_string(), FlattenedClass { name: "RecursiveRegexIterator".to_string(), + span: crate::span::Span::dummy(), extends: Some("RegexIterator".to_string()), implements: vec!["RecursiveIterator".to_string()], is_abstract: false, diff --git a/src/types/checker/builtin_spl_classes/storage.rs b/src/types/checker/builtin_spl_classes/storage.rs index 68776d222e..b3669daf23 100644 --- a/src/types/checker/builtin_spl_classes/storage.rs +++ b/src/types/checker/builtin_spl_classes/storage.rs @@ -22,6 +22,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "EmptyIterator".to_string(), FlattenedClass { name: "EmptyIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Iterator".to_string()], is_abstract: false, @@ -40,6 +41,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "ArrayIterator".to_string(), FlattenedClass { name: "ArrayIterator".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "Iterator".to_string(), @@ -63,6 +65,7 @@ pub(super) fn insert_classes(class_map: &mut HashMap) { "ArrayObject".to_string(), FlattenedClass { name: "ArrayObject".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec![ "IteratorAggregate".to_string(), diff --git a/src/types/checker/builtin_spl_exceptions.rs b/src/types/checker/builtin_spl_exceptions.rs index a4f83c3d7b..e1364dd3de 100644 --- a/src/types/checker/builtin_spl_exceptions.rs +++ b/src/types/checker/builtin_spl_exceptions.rs @@ -75,6 +75,7 @@ pub(crate) fn inject_builtin_spl_exceptions( (*name).to_string(), FlattenedClass { name: (*name).to_string(), + span: crate::span::Span::dummy(), extends: Some((*parent).to_string()), implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_stdclass.rs b/src/types/checker/builtin_stdclass.rs index f83e927944..126febf1e7 100644 --- a/src/types/checker/builtin_stdclass.rs +++ b/src/types/checker/builtin_stdclass.rs @@ -42,6 +42,7 @@ pub(crate) fn inject_builtin_stdclass( "stdClass".to_string(), FlattenedClass { name: "stdClass".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_types/declarations.rs b/src/types/checker/builtin_types/declarations.rs index 93a959b739..f9fa8eda9a 100644 --- a/src/types/checker/builtin_types/declarations.rs +++ b/src/types/checker/builtin_types/declarations.rs @@ -109,6 +109,7 @@ pub(crate) fn inject_builtin_throwables( "Error".to_string(), FlattenedClass { name: "Error".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Throwable".to_string()], is_abstract: false, @@ -139,6 +140,7 @@ pub(crate) fn inject_builtin_throwables( "Exception".to_string(), FlattenedClass { name: "Exception".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: vec!["Throwable".to_string()], is_abstract: false, @@ -172,6 +174,7 @@ pub(crate) fn inject_builtin_throwables( "RuntimeException".to_string(), FlattenedClass { name: "RuntimeException".to_string(), + span: crate::span::Span::dummy(), extends: Some("Exception".to_string()), implements: Vec::new(), is_abstract: false, @@ -189,6 +192,7 @@ pub(crate) fn inject_builtin_throwables( "ReflectionException".to_string(), FlattenedClass { name: "ReflectionException".to_string(), + span: crate::span::Span::dummy(), extends: Some("Exception".to_string()), implements: Vec::new(), is_abstract: false, @@ -206,6 +210,7 @@ pub(crate) fn inject_builtin_throwables( "JsonException".to_string(), FlattenedClass { name: "JsonException".to_string(), + span: crate::span::Span::dummy(), extends: Some("RuntimeException".to_string()), implements: Vec::new(), is_abstract: false, @@ -224,6 +229,7 @@ pub(crate) fn inject_builtin_throwables( "TypeError".to_string(), FlattenedClass { name: "TypeError".to_string(), + span: crate::span::Span::dummy(), extends: Some("Error".to_string()), implements: Vec::new(), is_abstract: false, @@ -241,6 +247,7 @@ pub(crate) fn inject_builtin_throwables( "ValueError".to_string(), FlattenedClass { name: "ValueError".to_string(), + span: crate::span::Span::dummy(), extends: Some("Error".to_string()), implements: Vec::new(), is_abstract: false, @@ -280,6 +287,7 @@ pub(crate) fn inject_builtin_throwables( "Fiber".to_string(), FlattenedClass { name: "Fiber".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -299,6 +307,7 @@ pub(crate) fn inject_builtin_throwables( "FiberError".to_string(), FlattenedClass { name: "FiberError".to_string(), + span: crate::span::Span::dummy(), extends: Some("Error".to_string()), implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/builtin_user_filter.rs b/src/types/checker/builtin_user_filter.rs index 57f71ebe99..d3db6d3c32 100644 --- a/src/types/checker/builtin_user_filter.rs +++ b/src/types/checker/builtin_user_filter.rs @@ -37,6 +37,7 @@ pub(crate) fn inject_builtin_user_filter( "php_user_filter".to_string(), FlattenedClass { name: "php_user_filter".to_string(), + span: crate::span::Span::dummy(), extends: None, implements: Vec::new(), is_abstract: false, diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 7e68fb082b..528d02e18f 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -490,6 +490,7 @@ fn flatten_enum_methods( } let mut flattened = FlattenedClass { name: name.clone(), + span: stmt.span, extends: None, implements: implements .iter() diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index c33a21dcce..d43c00f0bf 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -125,6 +125,7 @@ impl ClassBuildState { .collect(); Ok(ClassInfo { class_id, + declaration_span: class.span, parent: class.extends.clone(), is_abstract: class.is_abstract, is_final: class.is_final, diff --git a/src/types/checker/schema/enums.rs b/src/types/checker/schema/enums.rs index a94dd5da93..6784f995f2 100644 --- a/src/types/checker/schema/enums.rs +++ b/src/types/checker/schema/enums.rs @@ -242,6 +242,7 @@ pub(crate) fn build_enum_info( user_constants, used_traits, trait_aliases, + span, checker, next_class_id, ) @@ -264,6 +265,7 @@ pub(crate) fn insert_enum_metadata( user_constants: &[crate::parser::ast::ClassConst], used_traits: &[String], trait_aliases: &[(String, String)], + declaration_span: crate::span::Span, checker: &mut Checker, next_class_id: &mut u64, ) -> Result<(), CompileError> { @@ -435,6 +437,7 @@ pub(crate) fn insert_enum_metadata( name.to_string(), ClassInfo { class_id: *next_class_id, + declaration_span, parent: None, is_abstract: false, is_final: true, diff --git a/src/types/schema.rs b/src/types/schema.rs index 35ce5e21c8..8ffc295562 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -240,6 +240,8 @@ pub struct InterfaceInfo { #[derive(Debug, Clone, PartialEq)] pub struct ClassInfo { pub class_id: u64, + /// Source span of the class-like declaration, or `Span::dummy()` for compiler-injected classes. + pub declaration_span: crate::span::Span, pub parent: Option, pub is_abstract: bool, pub is_final: bool, diff --git a/src/types/traits.rs b/src/types/traits.rs index db1af70f81..6cf479eef8 100644 --- a/src/types/traits.rs +++ b/src/types/traits.rs @@ -27,6 +27,7 @@ mod validation; /// schema building, type checking, and codegen. pub struct FlattenedClass { pub name: String, + pub span: Span, pub extends: Option, pub implements: Vec, pub is_abstract: bool, @@ -203,6 +204,7 @@ pub fn flatten_classes( ); flattened.push(FlattenedClass { name: name.clone(), + span: stmt.span, extends: extends.as_ref().map(|name| name.as_str().to_string()), implements: implements.iter().map(|name| name.as_str().to_string()).collect(), is_abstract: *is_abstract, @@ -273,6 +275,7 @@ pub fn flatten_classes( name.clone(), FlattenedClass { name: name.clone(), + span: stmt.span, extends: None, implements: implements .iter() diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 63082afeae..586b71b88e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12322,6 +12322,31 @@ echo $function->getStartLine(); echo ":"; echo $function->getEndLine();'); assert_eq!(out, "F:1:5:2:4:6:8"); } +/// Verifies eval ReflectionClass exposes generated/AOT source-location metadata. +#[test] +fn test_eval_reflection_aot_class_source_locations() { + let out = compile_and_run_capture( + r#"getFileName() === false ? "f" : "F"; echo ":"; +echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; +$enum = new ReflectionClass("EvalAotReflectEnumSourceE2E"); +echo $enum->getFileName() === false ? "f" : "F"; echo ":"; +echo $enum->getStartLine(); echo ":"; echo $enum->getEndLine();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "F:2:2:F:5:5"); +} + /// Verifies eval ReflectionMethod exposes generated/AOT source-location metadata. #[test] fn test_eval_reflection_aot_method_source_locations() { From d5f5be1abeb7393b63c984cc9797d96a17763607 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 02:36:14 +0200 Subject: [PATCH 0816/1208] Expose eval AOT interface source locations --- docs/php/eval.md | 13 +++++--- src/codegen_support/runtime/data/user.rs | 39 +++++++++++++++++++----- src/types/checker/schema/interfaces.rs | 1 + src/types/schema.rs | 2 ++ tests/codegen/eval.rs | 6 +++- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 3f41816ef4..f11b339e8f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -246,10 +246,11 @@ report `false` / `null` for eval-declared user symbols. `getFileName()`, `getStartLine()`, and `getEndLine()` for parser-backed eval declarations. File names use PHP's synthetic eval file format from the current eval call site, and line numbers are one-based inside the evaluated fragment. -Generated/AOT metadata for `ReflectionClass` and `ReflectionMethod` exposes -the original source file and declaration line when EIR source metadata is -available. AOT `getEndLine()` currently reports the declaration line because -the bridge keeps declaration spans, not full body spans. +Generated/AOT metadata for `ReflectionClass` over classes, interfaces, and +enums, plus `ReflectionMethod`, exposes the original source file and +declaration line when EIR source metadata is available. AOT `getEndLine()` +currently reports the declaration line because the bridge keeps declaration +spans, not full body spans. `ReflectionClass` construction accepts class-name strings and object arguments; object arguments reflect the runtime class of eval-created or generated/AOT objects. `ReflectionObject` construction accepts object arguments and exposes the @@ -841,7 +842,9 @@ than a fixed parameter count. Generated/AOT free-function and method type metada return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while other unsupported bridge shapes remain metadata-only -rather than invocable through eval. +rather than invocable through eval. Generated/AOT trait source-location +metadata is still unavailable because emitted trait metadata currently carries +names and direct trait-use relations, not declaration spans. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index b22efe5872..a40db2bc57 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -489,7 +489,7 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); - emit_eval_reflection_class_lookup_data(&mut out, &sorted_classes); + emit_eval_reflection_class_lookup_data(&mut out, &sorted_classes, &sorted_interfaces); out.push_str(".p2align 3\n"); emit_eval_reflection_class_interface_lookup_data(&mut out, &sorted_classes, interfaces); out.push_str(".p2align 3\n"); @@ -1335,6 +1335,7 @@ fn eval_reflection_static_property_declaring_class<'a>( fn emit_eval_reflection_class_lookup_data( out: &mut String, sorted_classes: &[(&String, &ClassInfo)], + sorted_interfaces: &[(&String, &InterfaceInfo)], ) { let mut entries = Vec::new(); let mut index = 0usize; @@ -1352,6 +1353,20 @@ fn emit_eval_reflection_class_lookup_data( entries.push((class_label, class_name.len(), flags)); index += 1; } + for (interface_name, interface_info) in sorted_interfaces { + let flags = eval_reflection_interface_flags(interface_info); + if flags == 0 { + continue; + } + let class_label = format!("_eval_reflection_class_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(interface_name) + )); + entries.push((class_label, interface_name.len(), flags)); + index += 1; + } out.push_str(".p2align 3\n"); out.push_str(".globl _eval_reflection_class_count\n_eval_reflection_class_count:\n"); @@ -1376,15 +1391,25 @@ fn eval_reflection_class_flags(class_info: &ClassInfo) -> u64 { if class_info.is_readonly_class { flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; } - let Ok(start_line) = u64::try_from(class_info.declaration_span.line) else { - return flags; + flags |= eval_reflection_source_line_flags(class_info.declaration_span.line); + flags +} + +/// Returns eval ReflectionClass source-location bits retained for one generated/AOT interface. +fn eval_reflection_interface_flags(interface_info: &InterfaceInfo) -> u64 { + eval_reflection_source_line_flags(interface_info.declaration_span.line) +} + +/// Encodes declaration line metadata into high ReflectionClass flag bits. +fn eval_reflection_source_line_flags(line: usize) -> u64 { + let Ok(start_line) = u64::try_from(line) else { + return 0; }; if start_line == 0 || start_line > EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK { - return flags; + return 0; } - flags |= (start_line << EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT) - | (start_line << EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT); - flags + (start_line << EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT) + | (start_line << EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT) } /// Emits class-like/interface-name rows consumed by eval ReflectionClass metadata probes. diff --git a/src/types/checker/schema/interfaces.rs b/src/types/checker/schema/interfaces.rs index 5f92bcb8ba..292646385b 100644 --- a/src/types/checker/schema/interfaces.rs +++ b/src/types/checker/schema/interfaces.rs @@ -365,6 +365,7 @@ pub(crate) fn build_interface_info_recursive( interface.name.clone(), InterfaceInfo { interface_id: *next_interface_id, + declaration_span: interface.span, parents: interface.extends.clone(), properties, property_order, diff --git a/src/types/schema.rs b/src/types/schema.rs index 8ffc295562..f10a018192 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -209,6 +209,8 @@ impl PartialEq for PropertyHookContract { #[derive(Debug, Clone)] pub struct InterfaceInfo { pub interface_id: u64, + /// Source span of the interface declaration, or `Span::dummy()` for compiler-injected interfaces. + pub declaration_span: crate::span::Span, pub parents: Vec, pub properties: HashMap, pub property_order: Vec, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 586b71b88e..af64be7d2f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12330,10 +12330,14 @@ fn test_eval_reflection_aot_class_source_locations() { class EvalAotReflectClassSourceE2E { public function run() { return 1; } } +interface EvalAotReflectInterfaceSourceE2E {} enum EvalAotReflectEnumSourceE2E { case One; } echo eval('$class = new ReflectionClass("EvalAotReflectClassSourceE2E"); echo $class->getFileName() === false ? "f" : "F"; echo ":"; echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; +$interface = new ReflectionClass("EvalAotReflectInterfaceSourceE2E"); +echo $interface->getFileName() === false ? "f" : "F"; echo ":"; +echo $interface->getStartLine(); echo ":"; echo $interface->getEndLine(); echo ":"; $enum = new ReflectionClass("EvalAotReflectEnumSourceE2E"); echo $enum->getFileName() === false ? "f" : "F"; echo ":"; echo $enum->getStartLine(); echo ":"; echo $enum->getEndLine();'); @@ -12344,7 +12348,7 @@ echo $enum->getStartLine(); echo ":"; echo $enum->getEndLine();'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "F:2:2:F:5:5"); + assert_eq!(out.stdout, "F:2:2:F:5:5:F:6:6"); } /// Verifies eval ReflectionMethod exposes generated/AOT source-location metadata. From 43ac845a423fbf2f3dcec5e1722cd73be9921eb1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 02:43:01 +0200 Subject: [PATCH 0817/1208] Expose eval AOT trait source locations --- docs/php/eval.md | 8 +++--- src/codegen/mod.rs | 1 + src/codegen_support/runtime/data/user.rs | 33 +++++++++++++++++++++++- src/ir/module.rs | 2 ++ src/ir_lower/program.rs | 18 +++++++++++++ tests/codegen/eval.rs | 6 ++++- 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index f11b339e8f..11d117cf05 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -246,8 +246,8 @@ report `false` / `null` for eval-declared user symbols. `getFileName()`, `getStartLine()`, and `getEndLine()` for parser-backed eval declarations. File names use PHP's synthetic eval file format from the current eval call site, and line numbers are one-based inside the evaluated fragment. -Generated/AOT metadata for `ReflectionClass` over classes, interfaces, and -enums, plus `ReflectionMethod`, exposes the original source file and +Generated/AOT metadata for `ReflectionClass` over classes, interfaces, traits, +and enums, plus `ReflectionMethod`, exposes the original source file and declaration line when EIR source metadata is available. AOT `getEndLine()` currently reports the declaration line because the bridge keeps declaration spans, not full body spans. @@ -842,9 +842,7 @@ than a fixed parameter count. Generated/AOT free-function and method type metada return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while other unsupported bridge shapes remain metadata-only -rather than invocable through eval. Generated/AOT trait source-location -metadata is still unavailable because emitted trait metadata currently carries -names and direct trait-use relations, not declaration spans. +rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 69b85e862d..e58f1b473d 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -242,6 +242,7 @@ fn finalize_user_asm( &module.declared_interface_names, &module.trait_table.names, &module.declared_trait_uses, + &module.declared_trait_source_lines, &runtime_classes, &module.enum_infos, Some(&allowed_class_names), diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index a40db2bc57..9138899a15 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -57,6 +57,7 @@ pub(crate) fn emit_runtime_data_user( interface_names: &[String], trait_names: &[String], declared_trait_uses: &HashMap>, + declared_trait_source_lines: &HashMap, classes: &HashMap, enums: &HashMap, allowed_class_names: Option<&HashSet>, @@ -489,7 +490,12 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); - emit_eval_reflection_class_lookup_data(&mut out, &sorted_classes, &sorted_interfaces); + emit_eval_reflection_class_lookup_data( + &mut out, + &sorted_classes, + &sorted_interfaces, + declared_trait_source_lines, + ); out.push_str(".p2align 3\n"); emit_eval_reflection_class_interface_lookup_data(&mut out, &sorted_classes, interfaces); out.push_str(".p2align 3\n"); @@ -1336,6 +1342,7 @@ fn emit_eval_reflection_class_lookup_data( out: &mut String, sorted_classes: &[(&String, &ClassInfo)], sorted_interfaces: &[(&String, &InterfaceInfo)], + declared_trait_source_lines: &HashMap, ) { let mut entries = Vec::new(); let mut index = 0usize; @@ -1367,6 +1374,23 @@ fn emit_eval_reflection_class_lookup_data( entries.push((class_label, interface_name.len(), flags)); index += 1; } + let mut sorted_trait_lines = declared_trait_source_lines.iter().collect::>(); + sorted_trait_lines + .sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name)); + for (trait_name, line) in sorted_trait_lines { + let flags = eval_reflection_trait_flags(*line); + if flags == 0 { + continue; + } + let class_label = format!("_eval_reflection_class_name_{}", index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(trait_name) + )); + entries.push((class_label, trait_name.len(), flags)); + index += 1; + } out.push_str(".p2align 3\n"); out.push_str(".globl _eval_reflection_class_count\n_eval_reflection_class_count:\n"); @@ -1400,6 +1424,11 @@ fn eval_reflection_interface_flags(interface_info: &InterfaceInfo) -> u64 { eval_reflection_source_line_flags(interface_info.declaration_span.line) } +/// Returns eval ReflectionClass source-location bits retained for one generated/AOT trait. +fn eval_reflection_trait_flags(line: usize) -> u64 { + eval_reflection_source_line_flags(line) +} + /// Encodes declaration line metadata into high ReflectionClass flag bits. fn eval_reflection_source_line_flags(line: usize) -> u64 { let Ok(start_line) = u64::try_from(line) else { @@ -2295,6 +2324,7 @@ mod tests { &[], &[], &HashMap::new(), + &HashMap::new(), &classes, &HashMap::new(), Some(&allowed_class_names), @@ -2324,6 +2354,7 @@ mod tests { &[], &[], &HashMap::new(), + &HashMap::new(), &classes, &HashMap::new(), None, diff --git a/src/ir/module.rs b/src/ir/module.rs index 0dcad19674..c9b08b71f6 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -68,6 +68,7 @@ pub struct Module { pub declared_class_names: Vec, pub declared_interface_names: Vec, pub declared_trait_names: Vec, + pub declared_trait_source_lines: HashMap, pub declared_trait_uses: HashMap>, pub declared_trait_method_names: HashMap>, pub declared_trait_methods: HashMap>, @@ -109,6 +110,7 @@ impl Module { declared_class_names: Vec::new(), declared_interface_names: Vec::new(), declared_trait_names: Vec::new(), + declared_trait_source_lines: HashMap::new(), declared_trait_uses: HashMap::new(), declared_trait_method_names: HashMap::new(), declared_trait_methods: HashMap::new(), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 22d8456a04..855a8aa049 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -72,6 +72,7 @@ fn populate_metadata(module: &mut Module, program: &Program, check_result: &Chec module.declared_interface_names = collect_declared_interface_names(program, &check_result.interfaces); module.declared_trait_names = collect_declared_trait_names(program); + module.declared_trait_source_lines = collect_declared_trait_source_lines(program); module.declared_trait_uses = collect_declared_trait_uses(program); module.declared_trait_method_names = collect_declared_trait_method_names(program); module.declared_trait_methods = collect_declared_trait_methods(program); @@ -585,6 +586,23 @@ fn collect_declared_trait_names(program: &Program) -> Vec { names } +/// Collects source line metadata for user-declared traits, keyed by trait name. +fn collect_declared_trait_source_lines(program: &Program) -> HashMap { + let mut lines = HashMap::new(); + for stmt in program { + match &stmt.kind { + StmtKind::TraitDecl { name, .. } => { + lines.insert(name.clone(), stmt.span.line); + } + StmtKind::NamespaceBlock { body, .. } => { + lines.extend(collect_declared_trait_source_lines(body)); + } + _ => {} + } + } + lines +} + /// Collects direct trait-to-trait use declarations keyed by declaring trait name. fn collect_declared_trait_uses(program: &Program) -> HashMap> { let mut uses = HashMap::new(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index af64be7d2f..f2ceaccac5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12331,6 +12331,7 @@ class EvalAotReflectClassSourceE2E { public function run() { return 1; } } interface EvalAotReflectInterfaceSourceE2E {} +trait EvalAotReflectTraitSourceE2E {} enum EvalAotReflectEnumSourceE2E { case One; } echo eval('$class = new ReflectionClass("EvalAotReflectClassSourceE2E"); echo $class->getFileName() === false ? "f" : "F"; echo ":"; @@ -12338,6 +12339,9 @@ echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; $interface = new ReflectionClass("EvalAotReflectInterfaceSourceE2E"); echo $interface->getFileName() === false ? "f" : "F"; echo ":"; echo $interface->getStartLine(); echo ":"; echo $interface->getEndLine(); echo ":"; +$trait = new ReflectionClass("EvalAotReflectTraitSourceE2E"); +echo $trait->getFileName() === false ? "f" : "F"; echo ":"; +echo $trait->getStartLine(); echo ":"; echo $trait->getEndLine(); echo ":"; $enum = new ReflectionClass("EvalAotReflectEnumSourceE2E"); echo $enum->getFileName() === false ? "f" : "F"; echo ":"; echo $enum->getStartLine(); echo ":"; echo $enum->getEndLine();'); @@ -12348,7 +12352,7 @@ echo $enum->getStartLine(); echo ":"; echo $enum->getEndLine();'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "F:2:2:F:5:5:F:6:6"); + assert_eq!(out.stdout, "F:2:2:F:5:5:F:6:6:F:7:7"); } /// Verifies eval ReflectionMethod exposes generated/AOT source-location metadata. From 40a3d9b66ef79edafd4f152361da9deaa8afbc2f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 02:52:16 +0200 Subject: [PATCH 0818/1208] Document AOT reflection parameter construction --- docs/php/eval.md | 7 ++++--- tests/codegen/eval.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 11d117cf05..9981dcb395 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -473,9 +473,10 @@ returns the declaring class-like symbol for eval method parameters, and `ReflectionParameter::getDeclaringFunction()` returns a `ReflectionFunction` object for eval free-function parameters or a `ReflectionMethod` object for the declaring eval method. Direct `new ReflectionParameter(...)` construction -accepts eval free-function names, class/interface/trait method arrays, and -object-method arrays resolved from the evaluated runtime object, including -inline `new` expressions. `ReflectionFunction::invoke()` and `invokeArgs()` +accepts eval and registered generated/AOT free-function names, eval-visible and +generated/AOT class/interface/trait method arrays, and object-method arrays +resolved from the evaluated runtime object, including inline `new` expressions. +`ReflectionFunction::invoke()` and `invokeArgs()` dispatch eval-declared functions with the same named/default/variadic argument binding used by direct eval function calls. Runtime-held generated/AOT `ReflectionFunction` objects can invoke registered generated functions through diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f2ceaccac5..e3981a4383 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14575,6 +14575,42 @@ echo ":" . $listed->getNumberOfParameters() . "/" . $listed->getParameters()[2]- ); } +/// Verifies direct ReflectionParameter construction accepts generated/AOT method targets. +#[test] +fn test_eval_reflection_parameter_accepts_aot_method_targets() { + let out = compile_and_run_capture( + r#"getName() . ":" . $direct->getPosition() . ":"; +echo $direct->getDeclaringClass()->getName() . ":"; +echo $direct->getDeclaringFunction()->getName() . ":"; +echo ($direct->isOptional() ? "O" : "r") . ":" . $direct->getDefaultValue() . ":"; +$objectParam = new ReflectionParameter([new EvalAotDirectParamTarget(), "join"], 0); +echo $objectParam->getName() . ":" . $objectParam->getType()->getName() . ":"; +$static = new ReflectionParameter(["EvalAotDirectParamTarget", "collect"], "items"); +echo $static->getName() . ":" . ($static->isVariadic() ? "V" : "v") . ":" . ($static->isOptional() ? "O" : "r");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "right:1:EvalAotDirectParamTarget:join:O:B:left:string:items:V:O" + ); +} + /// Verifies eval ReflectionMethod exposes generated/AOT empty-array defaults. #[test] fn test_eval_reflection_method_exposes_aot_empty_array_default() { From 07145372ddffc8f29d1f6328184650ed0cc23077 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 03:12:47 +0200 Subject: [PATCH 0819/1208] Support AOT string-keyed attribute arrays --- tests/codegen/eval.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e3981a4383..137ac9523f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14188,35 +14188,35 @@ fn test_eval_reflection_class_exposes_aot_attributes() { class EvalAotClassAttr { public string $name = ""; public int $value = 0; - public array $items = []; + public string $summary = ""; - public function __construct(string $name, int $value = 0, array $items = []) { + public function __construct(string $name, int $value = 0, $items = []) { $this->name = $name; $this->value = $value; - $this->items = $items; + $this->summary = "ok"; } public function label(): string { return $this->name . ":" . $this->value; } } -#[EvalAotClassAttr("class", 1, ["nested", 3]), EvalAotClassAttr("again", 2, ["later"])] +#[EvalAotClassAttr("class", 1, ["nested", "kind" => "class", "meta" => ["inner" => "value"]]), EvalAotClassAttr("again", 2, ["later", "kind" => "again"])] class EvalAotClassAttrTarget {} echo eval('$names = class_attribute_names("EvalAotClassAttrTarget"); echo count($names) . ":" . $names[0] . ":" . $names[1] . ":"; $args = class_attribute_args("evalaotclassattrtarget", "EvalAotClassAttr"); echo count($args) . ":" . $args[0] . ":" . $args[1] . ":"; -echo count($args[2]) . ":" . $args[2][0] . ":" . $args[2][1] . ":"; +echo count($args[2]) . ":" . $args[2][0] . ":" . $args[2]["kind"] . ":" . $args[2]["meta"]["inner"] . ":"; $attrs = class_get_attributes("EvalAotClassAttrTarget"); echo count($attrs) . ":" . $attrs[0]->getName() . ":"; echo ($attrs[0]->isRepeated() ? "R" : "r") . ":"; echo $attrs[0]->getArguments()[0] . ":" . $attrs[0]->getArguments()[1] . ":"; -echo count($attrs[0]->getArguments()[2]) . ":" . $attrs[0]->getArguments()[2][0] . ":"; +echo count($attrs[0]->getArguments()[2]) . ":" . $attrs[0]->getArguments()[2][0] . ":" . $attrs[0]->getArguments()[2]["kind"] . ":"; $firstInstance = $attrs[0]->newInstance(); -echo $attrs[0]->getTarget() . ":" . $firstInstance->label() . ":" . count($firstInstance->items) . ":" . $firstInstance->items[0] . ":"; +echo $attrs[0]->getTarget() . ":" . $firstInstance->label() . ":" . $firstInstance->summary . ":"; $refAttrs = (new ReflectionClass("EvalAotClassAttrTarget"))->getAttributes(); echo count($refAttrs) . ":" . $refAttrs[1]->getArguments()[0] . ":"; -echo count($refAttrs[1]->getArguments()[2]) . ":" . $refAttrs[1]->getArguments()[2][0] . ":"; +echo count($refAttrs[1]->getArguments()[2]) . ":" . $refAttrs[1]->getArguments()[2][0] . ":" . $refAttrs[1]->getArguments()[2]["kind"] . ":"; echo $refAttrs[1]->newInstance()->label();'); "#, ); @@ -14227,7 +14227,7 @@ echo $refAttrs[1]->newInstance()->label();'); ); assert_eq!( out.stdout, - "2:EvalAotClassAttr:EvalAotClassAttr:3:class:1:2:nested:3:2:EvalAotClassAttr:R:class:1:2:nested:1:class:1:2:nested:2:again:1:later:again:2" + "2:EvalAotClassAttr:EvalAotClassAttr:3:class:1:3:nested:class:value:2:EvalAotClassAttr:R:class:1:3:nested:class:1:class:1:ok:2:again:2:later:again:again:2" ); } From 39453caea2b860bcb2a88512d654453da8c1c4df Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 03:27:35 +0200 Subject: [PATCH 0820/1208] Support eval reflection float constant arithmetic --- src/codegen/eval_class_constant_helpers.rs | 78 +++++++++++++++--- src/codegen/lower_inst/objects/reflection.rs | 86 ++++++++++++++++---- tests/codegen/eval.rs | 40 +++++++++ 3 files changed, 177 insertions(+), 27 deletions(-) diff --git a/src/codegen/eval_class_constant_helpers.rs b/src/codegen/eval_class_constant_helpers.rs index 97f9f7ef9b..109b93a235 100644 --- a/src/codegen/eval_class_constant_helpers.rs +++ b/src/codegen/eval_class_constant_helpers.rs @@ -470,30 +470,82 @@ fn eval_binary_class_constant_value( ) -> Option { let left = eval_class_constant_value(module, current_class, current_info, left, depth)?; let right = eval_class_constant_value(module, current_class, current_info, right, depth)?; - match (left, op, right) { - (EvalClassConstantValue::Int(left), BinOp::Add, EvalClassConstantValue::Int(right)) => { - left.checked_add(right).map(EvalClassConstantValue::Int) + match (&left, op, &right) { + ( + EvalClassConstantValue::Int(left), + BinOp::Add, + EvalClassConstantValue::Int(right), + ) => { + (*left).checked_add(*right).map(EvalClassConstantValue::Int) } - (EvalClassConstantValue::Int(left), BinOp::Sub, EvalClassConstantValue::Int(right)) => { - left.checked_sub(right).map(EvalClassConstantValue::Int) + ( + EvalClassConstantValue::Int(left), + BinOp::Sub, + EvalClassConstantValue::Int(right), + ) => { + (*left).checked_sub(*right).map(EvalClassConstantValue::Int) } - (EvalClassConstantValue::Int(left), BinOp::Mul, EvalClassConstantValue::Int(right)) => { - left.checked_mul(right).map(EvalClassConstantValue::Int) + ( + EvalClassConstantValue::Int(left), + BinOp::Mul, + EvalClassConstantValue::Int(right), + ) => { + (*left).checked_mul(*right).map(EvalClassConstantValue::Int) } - (EvalClassConstantValue::Int(left), BinOp::Mod, EvalClassConstantValue::Int(right)) => { - left.checked_rem(right).map(EvalClassConstantValue::Int) + ( + EvalClassConstantValue::Int(left), + BinOp::Mod, + EvalClassConstantValue::Int(right), + ) => { + (*left).checked_rem(*right).map(EvalClassConstantValue::Int) } - (EvalClassConstantValue::Int(left), BinOp::Pow, EvalClassConstantValue::Int(right)) - if right >= 0 => + ( + EvalClassConstantValue::Int(left), + BinOp::Pow, + EvalClassConstantValue::Int(right), + ) if *right >= 0 => { - let exponent = u32::try_from(right).ok()?; - left.checked_pow(exponent).map(EvalClassConstantValue::Int) + let exponent = u32::try_from(*right).ok()?; + (*left).checked_pow(exponent).map(EvalClassConstantValue::Int) } ( EvalClassConstantValue::Str(left), BinOp::Concat, EvalClassConstantValue::Str(right), ) => Some(EvalClassConstantValue::Str(format!("{}{}", left, right))), + ( + left, + BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow, + right, + ) => eval_float_binary_class_constant_value(left, op, right), + _ => None, + } +} + +/// Evaluates a numeric class-constant expression that must produce a float. +fn eval_float_binary_class_constant_value( + left: &EvalClassConstantValue, + op: &BinOp, + right: &EvalClassConstantValue, +) -> Option { + let left = eval_class_constant_value_as_float(left)?; + let right = eval_class_constant_value_as_float(right)?; + let value = match op { + BinOp::Add => left + right, + BinOp::Sub => left - right, + BinOp::Mul => left * right, + BinOp::Div if right != 0.0 => left / right, + BinOp::Pow => left.powf(right), + _ => return None, + }; + Some(EvalClassConstantValue::Float(value)) +} + +/// Returns the float representation of numeric eval class-constant metadata. +fn eval_class_constant_value_as_float(value: &EvalClassConstantValue) -> Option { + match value { + EvalClassConstantValue::Int(value) => Some(*value as f64), + EvalClassConstantValue::Float(value) => Some(*value), _ => None, } } diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 8c14add7b0..06a039658c 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -4622,37 +4622,95 @@ fn reflection_binary_constant_value( ) -> Result { let left = reflection_constant_value(ctx, current_class, current_info, left, depth)?; let right = reflection_constant_value(ctx, current_class, current_info, right, depth)?; - match (left, op, right) { - (ReflectionConstantValue::Int(left), BinOp::Add, ReflectionConstantValue::Int(right)) => { - Ok(ReflectionConstantValue::Int(left + right)) + match (&left, op, &right) { + ( + ReflectionConstantValue::Int(left), + BinOp::Add, + ReflectionConstantValue::Int(right), + ) => { + Ok(ReflectionConstantValue::Int(*left + *right)) } - (ReflectionConstantValue::Int(left), BinOp::Sub, ReflectionConstantValue::Int(right)) => { - Ok(ReflectionConstantValue::Int(left - right)) + ( + ReflectionConstantValue::Int(left), + BinOp::Sub, + ReflectionConstantValue::Int(right), + ) => { + Ok(ReflectionConstantValue::Int(*left - *right)) } - (ReflectionConstantValue::Int(left), BinOp::Mul, ReflectionConstantValue::Int(right)) => { - Ok(ReflectionConstantValue::Int(left * right)) + ( + ReflectionConstantValue::Int(left), + BinOp::Mul, + ReflectionConstantValue::Int(right), + ) => { + Ok(ReflectionConstantValue::Int(*left * *right)) } - (ReflectionConstantValue::Int(left), BinOp::Mod, ReflectionConstantValue::Int(right)) => { - Ok(ReflectionConstantValue::Int(left % right)) + ( + ReflectionConstantValue::Int(left), + BinOp::Mod, + ReflectionConstantValue::Int(right), + ) if *right != 0 => { + Ok(ReflectionConstantValue::Int(*left % *right)) } - (ReflectionConstantValue::Int(left), BinOp::Pow, ReflectionConstantValue::Int(right)) - if right >= 0 => + ( + ReflectionConstantValue::Int(left), + BinOp::Pow, + ReflectionConstantValue::Int(right), + ) if *right >= 0 => { - Ok(ReflectionConstantValue::Int(left.pow(right as u32))) + Ok(ReflectionConstantValue::Int((*left).pow(*right as u32))) } ( ReflectionConstantValue::Str(left), BinOp::Concat, ReflectionConstantValue::Str(right), ) => Ok(ReflectionConstantValue::Str(format!("{}{}", left, right))), + ( + left, + BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow, + right, + ) => reflection_float_binary_constant_value(left, op, right).ok_or_else(|| { + CodegenIrError::unsupported(format!( + "ReflectionClass constant metadata binary value {:?} {:?}", + reflection_constant_value_kind(left), + reflection_constant_value_kind(right) + )) + }), (left, _, right) => Err(CodegenIrError::unsupported(format!( "ReflectionClass constant metadata binary value {:?} {:?}", - reflection_constant_value_kind(&left), - reflection_constant_value_kind(&right) + reflection_constant_value_kind(left), + reflection_constant_value_kind(right) ))), } } +/// Evaluates a numeric binary operator that must produce a float Reflection value. +fn reflection_float_binary_constant_value( + left: &ReflectionConstantValue, + op: &BinOp, + right: &ReflectionConstantValue, +) -> Option { + let left = reflection_constant_value_as_float(left)?; + let right = reflection_constant_value_as_float(right)?; + let value = match op { + BinOp::Add => left + right, + BinOp::Sub => left - right, + BinOp::Mul => left * right, + BinOp::Div if right != 0.0 => left / right, + BinOp::Pow => left.powf(right), + _ => return None, + }; + Some(ReflectionConstantValue::Float(value)) +} + +/// Returns the float representation of numeric Reflection constant metadata. +fn reflection_constant_value_as_float(value: &ReflectionConstantValue) -> Option { + match value { + ReflectionConstantValue::Int(value) => Some(*value as f64), + ReflectionConstantValue::Float(value) => Some(*value), + _ => None, + } +} + /// Resolves and evaluates one scoped class/interface/trait constant value. fn reflection_scoped_constant_value( ctx: &FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 137ac9523f..0b83572347 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6504,6 +6504,46 @@ return "";'); assert_eq!(out, "OBIz:10:s:4:8:1:s:1:OWN:10:F:EvalAotReflectConstChild:s:case:7"); } +/// Verifies eval ReflectionClass materializes generated/AOT float constant arithmetic. +#[test] +fn test_eval_reflection_aot_float_constant_arithmetic() { + let out = compile_and_run_capture( + r#"getConstants(); +$power = $ref->getReflectionConstant("POWER"); +$negativePower = new ReflectionClassConstant("EvalAotReflectFloatConstTarget", "NEG_POWER"); +echo $ref->getConstant("SUM") . ":"; +echo $ref->getConstant("DIFF") . ":"; +echo $all["PRODUCT"] . ":"; +echo $ref->getConstant("QUOTIENT") . ":"; +echo $power->getValue() . ":"; +echo $negativePower->getValue(); +} catch (Throwable $e) { + echo "ERR:" . get_class($e) . ":" . $e->getMessage(); +} +return "";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "3.5:3.5:6:3.5:2.25:0.5"); +} + /// Verifies eval fragments can unpack numeric arrays into public AOT method calls. #[test] fn test_eval_fragment_can_call_this_public_method_with_spread_args() { From 73e937e23dd404a5f24d112ced2533c7c417783c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 03:37:06 +0200 Subject: [PATCH 0821/1208] Normalize eval AOT array default keys --- src/codegen/lower_inst/builtins/eval.rs | 26 ++++++++++++++-- tests/codegen/eval.rs | 41 +++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index de5f67b316..5c61ce8b96 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -22,7 +22,7 @@ use crate::codegen::{ use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op}; use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; use crate::parser::ast::{Expr, ExprKind, TypeExpr, Visibility}; -use crate::types::{AttrArgValue, ClassInfo, FunctionSig, PhpType}; +use crate::types::{is_php_integer_array_key, AttrArgValue, ClassInfo, FunctionSig, PhpType}; use super::super::super::context::FunctionContext; use super::super::{ @@ -1382,19 +1382,39 @@ fn eval_native_array_default(expr: &Expr) -> Option { fn eval_native_array_default_key(expr: &Expr) -> Option { match &expr.kind { ExprKind::IntLiteral(value) => Some(EvalNativeCallableArrayDefaultKey::Int(*value)), - ExprKind::StringLiteral(value) => { - Some(EvalNativeCallableArrayDefaultKey::String(value.clone())) + ExprKind::BoolLiteral(value) => { + Some(EvalNativeCallableArrayDefaultKey::Int(i64::from(*value))) } + ExprKind::FloatLiteral(value) => { + Some(EvalNativeCallableArrayDefaultKey::Int(*value as i64)) + } + ExprKind::StringLiteral(value) => eval_native_string_array_default_key(value), + ExprKind::Null => Some(EvalNativeCallableArrayDefaultKey::String(String::new())), ExprKind::Negate(inner) => match &inner.kind { ExprKind::IntLiteral(value) => value .checked_neg() .map(EvalNativeCallableArrayDefaultKey::Int), + ExprKind::FloatLiteral(value) => { + Some(EvalNativeCallableArrayDefaultKey::Int((-*value) as i64)) + } _ => None, }, _ => None, } } +/// Normalizes one string default-array key to PHP's integer-key rules. +fn eval_native_string_array_default_key(value: &str) -> Option { + if is_php_integer_array_key(value) { + value + .parse::() + .ok() + .map(EvalNativeCallableArrayDefaultKey::Int) + } else { + Some(EvalNativeCallableArrayDefaultKey::String(value.to_string())) + } +} + /// Converts supported property defaults into the compact eval bridge default ABI. fn eval_native_property_default( default: Option<&Expr>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0b83572347..e1d0039c23 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14723,6 +14723,47 @@ return $obj->describe() . ":" . EvalAotArrayValueDefaultMethodTarget::describeSt assert_eq!(out.stdout, "4:5:6:7:8:9:456"); } +/// Verifies eval normalizes generated/AOT array-default keys with PHP rules. +#[test] +fn test_eval_aot_method_array_default_normalizes_keys() { + let out = compile_and_run_capture( + r#" "yes", + false => "no", + 2.8 => "float", + "3" => "numeric", + ]): string { + return $items[1] . ":" . $items[0] . ":" . $items[2] . ":" . $items[3]; + } + + public function reflected(array $items = [ + null => "nil", + "03" => "string", + -1.2 => "negative", + ]): void { + } +} + +echo eval('$obj = new EvalAotArrayKeyDefaultMethodTarget(); +$method = new ReflectionMethod("EvalAotArrayKeyDefaultMethodTarget", "describe"); +$default = $method->getParameters()[0]->getDefaultValue(); +$reflected = (new ReflectionMethod("EvalAotArrayKeyDefaultMethodTarget", "reflected"))->getParameters()[0]->getDefaultValue(); +return $obj->describe() . "|" . $default[1] . ":" . $default[0] . ":" . $default[2] . ":" . $default[3] . "|" . $reflected[""] . ":" . $reflected["03"] . ":" . $reflected[-1];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "yes:no:float:numeric|yes:no:float:numeric|nil:string:negative" + ); +} + /// Verifies eval materializes generated/AOT object defaults during method dispatch. #[test] fn test_eval_aot_method_call_uses_object_default() { From 02499b282cbda892769a5ea251d0eaf5afc3f4ff Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 03:57:36 +0200 Subject: [PATCH 0822/1208] Support eval AOT method prototypes --- .../src/interpreter/reflection.rs | 133 ++++++++++++- src/codegen/lower_inst/builtins/eval.rs | 92 ++++++++- src/codegen_support/runtime/data/user.rs | 188 +++++++++++++----- tests/codegen/eval.rs | 63 ++++++ 4 files changed, 411 insertions(+), 65 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index e41877f94e..a08f9c5090 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1712,8 +1712,12 @@ pub(in crate::interpreter) fn eval_reflection_method_prototype_result( return Ok(None); }; eval_reflection_bind_no_args(evaluated_args)?; - let Some((prototype_class, prototype_method)) = - eval_reflection_method_prototype_target(&declaring_class, &reflected_method, context) + let Some((prototype_class, prototype_method)) = eval_reflection_method_prototype_target( + &declaring_class, + &reflected_method, + context, + values, + )? else { if is_has_prototype { return values.bool_value(false).map(Some); @@ -1731,7 +1735,7 @@ pub(in crate::interpreter) fn eval_reflection_method_prototype_result( return values.bool_value(true).map(Some); } let Some(metadata) = - eval_reflection_method_metadata(&prototype_class, &prototype_method, context) + eval_reflection_prototype_method_metadata(&prototype_class, &prototype_method, context, values)? else { return Err(EvalStatus::RuntimeFatal); }; @@ -9467,20 +9471,131 @@ fn eval_reflection_namespace_name(name: &str) -> String { }) } -/// Finds the PHP ReflectionMethod prototype target for an eval-declared method. +/// Builds ReflectionMethod metadata for a resolved eval or AOT prototype target. +fn eval_reflection_prototype_method_metadata( + prototype_class: &str, + prototype_method: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(metadata) = + eval_reflection_method_metadata(prototype_class, prototype_method, context) + { + return Ok(Some(metadata)); + } + eval_reflection_aot_method_metadata_with_signature_if_exists( + prototype_class, + prototype_method, + context, + values, + ) +} + +/// Finds the PHP ReflectionMethod prototype target for an eval or AOT method. fn eval_reflection_method_prototype_target( declaring_class: &str, method_name: &str, context: &ElephcEvalContext, -) -> Option<(String, String)> { - if !(context.has_class(declaring_class) || context.has_enum(declaring_class)) { - return None; + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + return Ok(eval_reflection_parent_method_prototype_target( + declaring_class, + method_name, + context, + ) + .or_else(|| { + eval_reflection_interface_method_prototype_target(declaring_class, method_name, context) + })); + } + eval_reflection_aot_method_prototype_target(declaring_class, method_name, values) +} + +/// Finds the PHP ReflectionMethod prototype target for a generated/AOT method. +fn eval_reflection_aot_method_prototype_target( + declaring_class: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(declaring_class, method_name)? else { + return Ok(None); + }; + if let Some(prototype) = + eval_reflection_aot_parent_method_prototype_target(declaring_class, method_name, flags, values)? + { + return Ok(Some(prototype)); } - eval_reflection_parent_method_prototype_target(declaring_class, method_name, context).or_else( - || eval_reflection_interface_method_prototype_target(declaring_class, method_name, context), + eval_reflection_aot_interface_method_prototype_target( + declaring_class, + method_name, + flags, + values, ) } +/// Finds the nearest generated/AOT parent-class method that can act as prototype. +fn eval_reflection_aot_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + method_flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let want_static = method_flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let mut current = eval_reflection_aot_parent_class_name(declaring_class, values)?; + let mut seen = std::collections::HashSet::new(); + while let Some(parent_class) = current { + if !seen.insert(parent_class.to_ascii_lowercase()) { + break; + } + if let Some(prototype) = + eval_reflection_aot_method_candidate(&parent_class, method_name, want_static, values)? + { + return Ok(Some(prototype)); + } + current = eval_reflection_aot_parent_class_name(&parent_class, values)?; + } + Ok(None) +} + +/// Finds the first generated/AOT interface method that can act as prototype. +fn eval_reflection_aot_interface_method_prototype_target( + declaring_class: &str, + method_name: &str, + method_flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let want_static = method_flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + for interface_name in eval_reflection_aot_class_interface_names(declaring_class, values)? { + if let Some(prototype) = + eval_reflection_aot_method_candidate(&interface_name, method_name, want_static, values)? + { + return Ok(Some(prototype)); + } + } + Ok(None) +} + +/// Returns one generated/AOT method prototype candidate if staticness and visibility match. +fn eval_reflection_aot_method_candidate( + class_name: &str, + method_name: &str, + want_static: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(None); + }; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let is_private = flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0; + if is_static != want_static || is_private { + return Ok(None); + } + let declaring_class = values + .reflection_method_declaring_class(class_name, method_name)? + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + Ok(Some((declaring_class, method_name.to_ascii_lowercase()))) +} + /// Finds the nearest parent-class method prototype for an eval-declared override. fn eval_reflection_parent_method_prototype_target( declaring_class: &str, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 5c61ce8b96..1b6eb4074d 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -22,7 +22,9 @@ use crate::codegen::{ use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op}; use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; use crate::parser::ast::{Expr, ExprKind, TypeExpr, Visibility}; -use crate::types::{is_php_integer_array_key, AttrArgValue, ClassInfo, FunctionSig, PhpType}; +use crate::types::{ + is_php_integer_array_key, AttrArgValue, ClassInfo, FunctionSig, InterfaceInfo, PhpType, +}; use super::super::super::context::FunctionContext; use super::super::{ @@ -634,6 +636,20 @@ fn eval_native_method_registrations( collect_eval_native_instance_methods(class_name, class_info, &mut registrations); collect_eval_native_static_methods(class_name, class_info, &mut registrations); } + let mut interfaces = ctx.module.interface_infos.iter().collect::>(); + interfaces.sort_by_key(|(_, interface_info)| interface_info.interface_id); + for (interface_name, interface_info) in interfaces { + collect_eval_native_interface_instance_methods( + interface_name, + interface_info, + &mut registrations, + ); + collect_eval_native_interface_static_methods( + interface_name, + interface_info, + &mut registrations, + ); + } registrations } @@ -1082,6 +1098,80 @@ fn collect_eval_native_static_methods( } } +/// Adds interface instance-method metadata to eval signature registration. +fn collect_eval_native_interface_instance_methods( + interface_name: &str, + interface_info: &InterfaceInfo, + registrations: &mut Vec, +) { + let mut methods = interface_info.methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method_name, signature) in methods { + registrations.push(EvalNativeMethodRegistration { + class_name: eval_native_interface_method_declaring_interface( + interface_name, + interface_info, + method_name, + ) + .to_string(), + method_name: method_name.clone(), + is_static: false, + signature: signature.clone(), + bridge_supported: false, + }); + } +} + +/// Adds interface static-method metadata to eval signature registration. +fn collect_eval_native_interface_static_methods( + interface_name: &str, + interface_info: &InterfaceInfo, + registrations: &mut Vec, +) { + let mut methods = interface_info.static_methods.iter().collect::>(); + methods.sort_by_key(|(method, _)| method.as_str()); + for (method_name, signature) in methods { + registrations.push(EvalNativeMethodRegistration { + class_name: eval_native_interface_static_method_declaring_interface( + interface_name, + interface_info, + method_name, + ) + .to_string(), + method_name: method_name.clone(), + is_static: true, + signature: signature.clone(), + bridge_supported: false, + }); + } +} + +/// Returns the interface name that declares one AOT interface instance method row. +fn eval_native_interface_method_declaring_interface<'a>( + reflected_interface: &'a str, + interface_info: &'a InterfaceInfo, + method_name: &str, +) -> &'a str { + interface_info + .method_declaring_interfaces + .get(method_name) + .map(String::as_str) + .unwrap_or(reflected_interface) +} + +/// Returns the interface name that declares one AOT interface static method row. +fn eval_native_interface_static_method_declaring_interface<'a>( + reflected_interface: &'a str, + interface_info: &'a InterfaceInfo, + method_name: &str, +) -> &'a str { + interface_info + .static_method_declaring_interfaces + .get(method_name) + .map(String::as_str) + .unwrap_or(reflected_interface) +} + /// Returns true when a module function should expose metadata to eval fragments. fn function_has_eval_metadata(function: &Function) -> bool { !function.flags.is_main && !function.name.starts_with('_') diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 9138899a15..031457ccaa 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -486,7 +486,7 @@ pub(crate) fn emit_runtime_data_user( out.push_str(".p2align 3\n"); emit_classes_by_name_table(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); - emit_eval_reflection_method_lookup_data(&mut out, &sorted_classes); + emit_eval_reflection_method_lookup_data(&mut out, &sorted_classes, &sorted_interfaces); out.push_str(".p2align 3\n"); emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); out.push_str(".p2align 3\n"); @@ -1004,6 +1004,7 @@ fn emit_eval_reflection_source_file_data(out: &mut String, source_path: Option<& fn emit_eval_reflection_method_lookup_data( out: &mut String, sorted_classes: &[(&String, &ClassInfo)], + sorted_interfaces: &[(&String, &InterfaceInfo)], ) { let mut entries = Vec::new(); let class_infos = sorted_classes @@ -1027,34 +1028,15 @@ fn emit_eval_reflection_method_lookup_data( method_name, false, ); - let class_label = format!("_eval_reflection_method_class_{}", index); - let method_label = format!("_eval_reflection_method_name_{}", index); - let declaring_label = format!("_eval_reflection_method_declaring_class_{}", index); - out.push_str(&format!( - ".globl {0}\n{0}:\n .ascii \"{1}\"\n", - class_label, - escaped_ascii(class_name) - )); - out.push_str(&format!( - ".globl {0}\n{0}:\n .ascii \"{1}\"\n", - method_label, - escaped_ascii(method_name) - )); - out.push_str(&format!( - ".globl {0}\n{0}:\n .ascii \"{1}\"\n", - declaring_label, - escaped_ascii(declaring_class) - )); - entries.push(( - class_label, - class_name.len(), - method_label, - method_name.len(), + push_eval_reflection_method_lookup_row( + out, + &mut entries, + &mut index, + class_name, + method_name, flags, - declaring_label, - declaring_class.len(), - )); - index += 1; + declaring_class, + ); } let mut static_methods = class_info.static_methods.keys().collect::>(); @@ -1069,34 +1051,55 @@ fn emit_eval_reflection_method_lookup_data( method_name, true, ); - let class_label = format!("_eval_reflection_method_class_{}", index); - let method_label = format!("_eval_reflection_method_name_{}", index); - let declaring_label = format!("_eval_reflection_method_declaring_class_{}", index); - out.push_str(&format!( - ".globl {0}\n{0}:\n .ascii \"{1}\"\n", - class_label, - escaped_ascii(class_name) - )); - out.push_str(&format!( - ".globl {0}\n{0}:\n .ascii \"{1}\"\n", - method_label, - escaped_ascii(method_name) - )); - out.push_str(&format!( - ".globl {0}\n{0}:\n .ascii \"{1}\"\n", - declaring_label, - escaped_ascii(declaring_class) - )); - entries.push(( - class_label, - class_name.len(), - method_label, - method_name.len(), + push_eval_reflection_method_lookup_row( + out, + &mut entries, + &mut index, + class_name, + method_name, flags, - declaring_label, - declaring_class.len(), - )); - index += 1; + declaring_class, + ); + } + } + + for (interface_name, interface_info) in sorted_interfaces { + let mut methods = interface_info.methods.keys().collect::>(); + methods.sort(); + for method_name in methods { + let declaring_interface = eval_reflection_interface_method_declaring_interface( + interface_name, + interface_info, + method_name, + ); + push_eval_reflection_method_lookup_row( + out, + &mut entries, + &mut index, + interface_name, + method_name, + eval_reflection_interface_method_flags(false), + declaring_interface, + ); + } + + let mut static_methods = interface_info.static_methods.keys().collect::>(); + static_methods.sort(); + for method_name in static_methods { + let declaring_interface = eval_reflection_interface_static_method_declaring_interface( + interface_name, + interface_info, + method_name, + ); + push_eval_reflection_method_lookup_row( + out, + &mut entries, + &mut index, + interface_name, + method_name, + eval_reflection_interface_method_flags(true), + declaring_interface, + ); } } @@ -1117,6 +1120,46 @@ fn emit_eval_reflection_method_lookup_data( } } +/// Adds one eval ReflectionMethod lookup row and its backing string labels. +fn push_eval_reflection_method_lookup_row( + out: &mut String, + entries: &mut Vec<(String, usize, String, usize, u64, String, usize)>, + index: &mut usize, + class_name: &str, + method_name: &str, + flags: u64, + declaring_class: &str, +) { + let class_label = format!("_eval_reflection_method_class_{}", *index); + let method_label = format!("_eval_reflection_method_name_{}", *index); + let declaring_label = format!("_eval_reflection_method_declaring_class_{}", *index); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + class_label, + escaped_ascii(class_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + method_label, + escaped_ascii(method_name) + )); + out.push_str(&format!( + ".globl {0}\n{0}:\n .ascii \"{1}\"\n", + declaring_label, + escaped_ascii(declaring_class) + )); + entries.push(( + class_label, + class_name.len(), + method_label, + method_name.len(), + flags, + declaring_label, + declaring_class.len(), + )); + *index += 1; +} + /// Adds source start/end line bits to an AOT ReflectionMethod flag word when available. fn eval_reflection_method_flags_with_source_lines( flags: u64, @@ -1170,6 +1213,32 @@ fn eval_reflection_static_method_declaring_class<'a>( .unwrap_or(reflected_class) } +/// Returns the interface name that declares one visible instance method. +fn eval_reflection_interface_method_declaring_interface<'a>( + reflected_interface: &'a str, + interface_info: &'a InterfaceInfo, + method_name: &str, +) -> &'a str { + interface_info + .method_declaring_interfaces + .get(method_name) + .map(String::as_str) + .unwrap_or(reflected_interface) +} + +/// Returns the interface name that declares one visible static method. +fn eval_reflection_interface_static_method_declaring_interface<'a>( + reflected_interface: &'a str, + interface_info: &'a InterfaceInfo, + method_name: &str, +) -> &'a str { + interface_info + .static_method_declaring_interfaces + .get(method_name) + .map(String::as_str) + .unwrap_or(reflected_interface) +} + /// Returns eval ReflectionMethod bitflags for one instance method entry. fn eval_reflection_instance_method_flags(class_info: &ClassInfo, method_name: &str) -> u64 { let visibility = class_info @@ -1203,6 +1272,15 @@ fn eval_reflection_static_method_flags(class_info: &ClassInfo, method_name: &str flags } +/// Returns eval ReflectionMethod bitflags for one interface method entry. +fn eval_reflection_interface_method_flags(is_static: bool) -> u64 { + let mut flags = EVAL_REFLECTION_METHOD_FLAG_PUBLIC | EVAL_REFLECTION_METHOD_FLAG_ABSTRACT; + if is_static { + flags |= EVAL_REFLECTION_METHOD_FLAG_STATIC; + } + flags +} + /// Converts method visibility metadata into eval ReflectionMethod flag bits. fn eval_reflection_method_visibility_flags(visibility: &Visibility) -> u64 { match visibility { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e1d0039c23..7e7f3de156 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -17329,6 +17329,69 @@ echo $inherited->hasPrototype() ? "Y" : "N";'); ); } +/// Verifies eval ReflectionMethod hasPrototype/getPrototype expose generated/AOT prototypes. +#[test] +fn test_eval_reflection_method_reports_aot_prototypes() { + let out = compile_and_run_capture( + r#"getPrototype(); +echo ($override->hasPrototype() ? "Y" : "N") . ":"; +echo $overrideProto->getDeclaringClass()->getName() . "::"; +echo $overrideProto->getName() . ":"; +$iface = new ReflectionMethod("EvalAotProtoChild", "iface"); +$ifaceProto = $iface->getPrototype(); +echo ($iface->hasPrototype() ? "Y" : "N") . ":"; +echo $ifaceProto->getDeclaringClass()->getName() . "::"; +echo $ifaceProto->getName() . ":"; +$parentIface = new ReflectionMethod("EvalAotProtoChild", "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName() . "::"; +echo $parentIfaceProto->getName() . ":"; +$own = new ReflectionMethod("EvalAotProtoChild", "own"); +echo ($own->hasPrototype() ? "Y" : "N") . ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod("EvalAotProtoChild", "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N"; +} catch (Throwable $e) { + echo "ERR:" . get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Y:EvalAotProtoBase::run:Y:EvalAotProtoIface::iface:EvalAotProtoParentIface::parented:N:E:N" + ); +} + /// Verifies eval-declared functions share method-style named/default/ref/variadic binding. #[test] fn test_eval_declared_function_rich_argument_binding() { From 890e24e221b5d1354b0a73563b871d04ba444750 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 04:02:45 +0200 Subject: [PATCH 0823/1208] Cover eval AOT static method prototypes --- tests/codegen/eval.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7e7f3de156..6a7dd4ecbc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -17340,14 +17340,18 @@ interface EvalAotProtoParentIface { interface EvalAotProtoChildIface extends EvalAotProtoParentIface {} interface EvalAotProtoIface { public function iface(); + public static function staticIface(); } class EvalAotProtoBase { public function run() {} + public static function staticRun() {} public function inherited() {} } class EvalAotProtoChild extends EvalAotProtoBase implements EvalAotProtoIface, EvalAotProtoChildIface { public function run() {} + public static function staticRun() {} public function iface() {} + public static function staticIface() {} public function parented() {} public function own() {} } @@ -17366,6 +17370,16 @@ $parentIface = new ReflectionMethod("EvalAotProtoChild", "parented"); $parentIfaceProto = $parentIface->getPrototype(); echo $parentIfaceProto->getDeclaringClass()->getName() . "::"; echo $parentIfaceProto->getName() . ":"; +$staticOverride = new ReflectionMethod("EvalAotProtoChild", "staticRun"); +$staticOverrideProto = $staticOverride->getPrototype(); +echo ($staticOverride->hasPrototype() ? "Y" : "N") . ":"; +echo $staticOverrideProto->getDeclaringClass()->getName() . "::"; +echo $staticOverrideProto->getName() . ":"; +$staticIface = new ReflectionMethod("EvalAotProtoChild", "staticIface"); +$staticIfaceProto = $staticIface->getPrototype(); +echo ($staticIface->hasPrototype() ? "Y" : "N") . ":"; +echo $staticIfaceProto->getDeclaringClass()->getName() . "::"; +echo $staticIfaceProto->getName() . ":"; $own = new ReflectionMethod("EvalAotProtoChild", "own"); echo ($own->hasPrototype() ? "Y" : "N") . ":"; try { @@ -17388,7 +17402,7 @@ echo $inherited->hasPrototype() ? "Y" : "N"; ); assert_eq!( out.stdout, - "Y:EvalAotProtoBase::run:Y:EvalAotProtoIface::iface:EvalAotProtoParentIface::parented:N:E:N" + "Y:EvalAotProtoBase::run:Y:EvalAotProtoIface::iface:EvalAotProtoParentIface::parented:Y:EvalAotProtoBase::staticrun:Y:EvalAotProtoIface::staticiface:N:E:N" ); } From d8e90151fd27a40ee71f854cda1f2898e4c50b54 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 04:33:35 +0200 Subject: [PATCH 0824/1208] Support native eval-declared object construction --- crates/elephc-magician/src/ffi/mod.rs | 4 + .../src/ffi/object_construction.rs | 161 ++++++++++++++++++ crates/elephc-magician/src/interpreter/mod.rs | 47 +++++ src/codegen/lower_inst.rs | 15 +- src/codegen/lower_inst/builtins.rs | 23 +++ src/codegen/lower_inst/builtins/eval.rs | 118 ++++++++++++- src/ir/instr.rs | 6 +- src/ir_lower/expr/mod.rs | 15 ++ .../checker/inference/objects/constructors.rs | 25 +++ tests/codegen/eval.rs | 38 +++++ 10 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 crates/elephc-magician/src/ffi/object_construction.rs diff --git a/crates/elephc-magician/src/ffi/mod.rs b/crates/elephc-magician/src/ffi/mod.rs index 801afa5eff..f6d21df4ef 100644 --- a/crates/elephc-magician/src/ffi/mod.rs +++ b/crates/elephc-magician/src/ffi/mod.rs @@ -17,6 +17,8 @@ pub mod execute; pub mod function_calls; pub mod native_functions; pub mod native_methods; +#[cfg(not(test))] +pub mod object_construction; pub mod scope; pub mod symbols; pub(crate) mod util; @@ -28,6 +30,8 @@ pub use execute::*; pub use function_calls::*; pub use native_functions::*; pub use native_methods::*; +#[cfg(not(test))] +pub use object_construction::*; pub use scope::*; pub use symbols::*; diff --git a/crates/elephc-magician/src/ffi/object_construction.rs b/crates/elephc-magician/src/ffi/object_construction.rs new file mode 100644 index 0000000000..fdb31f8ae1 --- /dev/null +++ b/crates/elephc-magician/src/ffi/object_construction.rs @@ -0,0 +1,161 @@ +//! Purpose: +//! Exports post-barrier construction and method calls for classes declared by +//! eval fragments. Generated code uses this ABI when native object operations +//! may target a dynamic eval class instead of an AOT class. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_new_object`. +//! - Generated EIR backend assembly through `__elephc_eval_method_call`. +//! +//! Key details: +//! - The ABI currently accepts positional arguments already boxed as Mixed cells. +//! - Calls return either a value cell or an uncaught throwable cell through +//! `ElephcEvalResult`. + +use super::util::{abi_name_to_string, clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::interpreter; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; +use std::slice; + +/// Constructs a class previously declared through `eval()` with positional cells. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `args` must be readable for +/// `arg_count` runtime-cell pointers when `arg_count > 0`, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_new_object( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_new_object_inner(ctx, name_ptr, name_len, args, arg_count, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Calls a method on a value that may be an eval-created object. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `object` must point at a boxed +/// runtime cell. `method_ptr` must be readable for `method_len` bytes when +/// `method_len > 0`. `arg_pack` points at a native-word count followed by that +/// many runtime-cell pointers, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_method_call( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + method_ptr: *const u8, + method_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_method_call_inner(ctx, object, method_ptr, method_len, arg_pack, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the dynamic object-construction ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_new_object`; callers must provide a valid context, +/// readable class-name bytes, and readable argument pointer storage. +#[cfg(not(test))] +unsafe fn eval_new_object_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(arg_count) = usize::try_from(arg_count) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_count > 0 && args.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(args, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_new_object_outcome(context, &name, args, &mut values) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the dynamic method-call ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_method_call`; callers must provide a valid context, +/// boxed object cell, readable method-name bytes, and a readable argument pack. +#[cfg(not(test))] +unsafe fn eval_method_call_inner( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + method_ptr: *const u8, + method_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if object.is_null() || arg_pack.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(method) = abi_name_to_string(method_ptr, method_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let arg_count = *arg_pack; + let arg_ptrs = arg_pack.add(1) as *const *mut RuntimeCell; + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(arg_ptrs, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_method_call_outcome( + context, + RuntimeCellHandle::from_raw(object), + &method, + args, + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index e8faf6e956..25613bda3f 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -214,6 +214,53 @@ pub fn execute_context_function_call_array_outcome( } } +/// Constructs a class declared in the shared eval context with prepared positional arguments. +pub fn execute_context_new_object_outcome( + context: &mut ElephcEvalContext, + name: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class) = context.class(name).cloned() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let evaluated_args = args + .into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + let mut scope = ElephcEvalScope::new(); + match eval_dynamic_class_new_object(&class, evaluated_args, context, &mut scope, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Calls a method on a value that may be an eval-created object. +pub fn execute_context_method_call_outcome( + context: &mut ElephcEvalContext, + object: RuntimeCellHandle, + method: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_method_call_result(object, method, args, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + /// Returns the current interpreter availability status for the ABI stub. pub fn current_stub_status() -> EvalStatus { EvalStatus::UnsupportedConstruct diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index fc8ee2acbe..aecf19fc52 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -213,6 +213,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::BuiltinCall => builtins::lower_builtin_call(ctx, &inst), Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), Op::EvalFunctionCallArray => builtins::lower_eval_function_call_array(ctx, &inst), + Op::EvalObjectNew => builtins::lower_eval_object_new(ctx, &inst), Op::EvalFunctionExists => builtins::lower_eval_function_exists(ctx, &inst), Op::EvalClassExists => builtins::lower_eval_class_exists(ctx, &inst), Op::EvalConstantExists => builtins::lower_eval_constant_exists(ctx, &inst), @@ -2902,11 +2903,15 @@ fn lower_mixed_method_call( ) -> Result<()> { let candidates = mixed_method_candidates(ctx, method_name, inst.operands.len())?; if candidates.is_empty() { + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_method_call(ctx, inst, object, method_name); + } emit_method_call_on_null_fatal(ctx, method_name); return Ok(()); } let receiver_reg = abi::nested_call_reg(ctx.emitter); + let non_object_label = ctx.next_label("mixed_method_non_object"); let no_match_label = ctx.next_label("mixed_method_no_match"); let done_label = ctx.next_label("mixed_method_done"); let match_labels = candidates @@ -2921,7 +2926,7 @@ fn lower_mixed_method_call( ctx.load_value_to_result(object)?; abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); - emit_mixed_method_object_payload_or_fatal(ctx, receiver_reg, &no_match_label); + emit_mixed_method_object_payload_or_fatal(ctx, receiver_reg, &non_object_label); emit_mixed_method_class_dispatch( ctx, receiver_reg, @@ -2937,6 +2942,14 @@ fn lower_mixed_method_call( } ctx.emitter.label(&no_match_label); + if builtins::has_eval_context(ctx) { + builtins::lower_eval_method_call(ctx, inst, object, method_name)?; + abi::emit_jump(ctx.emitter, &done_label); + } else { + emit_method_call_on_null_fatal(ctx, method_name); + } + + ctx.emitter.label(&non_object_label); emit_method_call_on_null_fatal(ctx, method_name); ctx.emitter.label(&done_label); diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index fe8fee8950..20761c746c 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -105,6 +105,29 @@ pub(super) fn lower_eval_function_call_array( eval::lower_eval_function_call_array(ctx, inst) } +/// Lowers post-eval object construction for classes declared by eval fragments. +pub(super) fn lower_eval_object_new( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_object_new(ctx, inst) +} + +/// Lowers a post-eval method call that may target an eval-created object. +pub(super) fn lower_eval_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + method_name: &str, +) -> Result<()> { + eval::lower_eval_method_call(ctx, inst, object, method_name) +} + +/// Returns true when this lowered function has a persistent eval context local. +pub(super) fn has_eval_context(ctx: &FunctionContext<'_>) -> bool { + eval::has_eval_context(ctx) +} + /// Lowers post-eval dynamic function existence probes to the optional eval bridge. pub(super) fn lower_eval_function_exists( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 1b6eb4074d..b1c3a76a7d 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -19,7 +19,7 @@ use crate::codegen::platform::Arch; use crate::codegen::{ abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, }; -use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op}; +use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op, ValueId}; use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; use crate::parser::ast::{Expr, ExprKind, TypeExpr, Visibility}; use crate::types::{ @@ -352,6 +352,95 @@ pub(super) fn lower_eval_function_call_array( store_if_result(ctx, inst) } +/// Lowers native construction of a class declared by a prior eval fragment. +pub(super) fn lower_eval_object_new( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + let (name_label, name_len) = ctx.intern_class_name_data(expect_data(inst)?)?; + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_function_call_stack_bytes(inst.operands.len()); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_context(ctx)?; + store_eval_function_call_args(ctx, inst, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + if inst.operands.is_empty() { + abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + } else { + abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + inst.operands.len() as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_new_object"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers a method call that may dispatch to an eval-created dynamic object. +pub(super) fn lower_eval_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + method_name: &str, +) -> Result<()> { + let arg_count = inst.operands.len().saturating_sub(1); + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_method_call_stack_bytes(arg_count); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_context(ctx)?; + let object_ty = ctx.load_value_to_result(object)?.codegen_repr(); + if !matches!(object_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &object_ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + store_eval_method_call_arg_pack(ctx, inst, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); + let (method_label, method_len) = ctx.data.add_string(method_name.as_bytes()); + let method_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_symbol_address(ctx.emitter, method_ptr_arg, &method_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + method_len as i64, + ); + let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_method_call"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Returns true when the current function owns an eval context local. +pub(super) fn has_eval_context(ctx: &FunctionContext<'_>) -> bool { + eval_context_slot(ctx).is_ok() +} + /// Lowers a post-eval dynamic function existence probe to the eval bridge ABI. pub(super) fn lower_eval_function_exists( ctx: &mut FunctionContext<'_>, @@ -466,6 +555,12 @@ fn eval_function_call_stack_bytes(arg_count: usize) -> usize { (bytes + 15) & !15 } +/// Returns the aligned scratch size for an eval dynamic method-call argument pack. +fn eval_method_call_stack_bytes(arg_count: usize) -> usize { + let bytes = EVAL_STACK_BYTES + 8 + arg_count * 8; + (bytes + 15) & !15 +} + /// Stores positional operands as boxed Mixed cells for the eval function-call ABI. fn store_eval_function_call_args( ctx: &mut FunctionContext<'_>, @@ -483,6 +578,27 @@ fn store_eval_function_call_args( Ok(()) } +/// Stores a count-prefixed positional argument pack for the eval method-call ABI. +fn store_eval_method_call_arg_pack( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + args_offset: usize, +) -> Result<()> { + let arg_count = inst.operands.len().saturating_sub(1); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, result_reg, arg_count as i64); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset); + for (index, operand) in inst.operands.iter().skip(1).enumerate() { + let ty = ctx.load_value_to_result(*operand)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset + 8 + index * 8); + } + Ok(()) +} + /// Saves the loaded eval source string while scope setup calls use argument registers. fn save_eval_code_string(ctx: &mut FunctionContext<'_>) { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); diff --git a/src/ir/instr.rs b/src/ir/instr.rs index 49cd41459f..b1b40c34fe 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -313,6 +313,7 @@ pub enum Op { IteratorMethodCall, SplRuntimeCall, ObjectNew, + EvalObjectNew, ObjectCloneShallow, DynamicObjectNew, DynamicObjectNewMixed, @@ -511,7 +512,8 @@ impl Op { EvalConstantFetch => { E::READS_GLOBAL | E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL } - Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | EvalFunctionCallArray | RuntimeCall + Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | EvalFunctionCallArray + | EvalObjectNew | RuntimeCall | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { E::all().difference(E::REFCOUNT_OP) } @@ -537,6 +539,7 @@ impl Op { | Op::BuiltinCall | Op::EvalFunctionCall | Op::EvalFunctionCallArray + | Op::EvalObjectNew | Op::RuntimeCall | Op::ExternCall | Op::MethodCall @@ -689,6 +692,7 @@ impl Op { IteratorMethodCall => "iterator_method_call", SplRuntimeCall => "spl_runtime_call", ObjectNew => "object_new", + EvalObjectNew => "eval_object_new", ObjectCloneShallow => "object_clone_shallow", DynamicObjectNew => "dynamic_object_new", DynamicObjectNewMixed => "dynamic_object_new_mixed", diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 7b2f62d366..6c1ad6ac02 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9250,6 +9250,21 @@ fn lower_new_object( ); } } + if ctx.has_eval_barrier() + && !ctx.classes.contains_key(class_name.as_str()) + && plain_positional_call_args(args) + { + let operands = lower_args_with_signature(ctx, None, args); + let data = ctx.intern_class_name(class_name.as_str()); + return ctx.emit_value( + Op::EvalObjectNew, + operands, + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalObjectNew.default_effects(), + Some(expr.span), + ); + } let sig = constructor_signature(ctx, class_name).cloned(); let operands = lower_args_with_signature(ctx, sig.as_ref(), args); let php_type = PhpType::Object(class_name.as_str().to_string()); diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 51e4e2cc28..691b34de62 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -62,6 +62,10 @@ impl Checker { )); } if !self.classes.contains_key(class_name.as_str()) { + if self.eval_barrier_active { + self.infer_eval_barrier_dynamic_constructor_args(args, expr, env)?; + return Ok(PhpType::Mixed); + } return Err(CompileError::new( expr.span, &format!("Undefined class: {}", class_name), @@ -180,6 +184,27 @@ impl Checker { Ok(PhpType::Object(class_name)) } + /// Infers constructor arguments for a class that may have been declared by a prior eval call. + fn infer_eval_barrier_dynamic_constructor_args( + &mut self, + args: &[Expr], + expr: &Expr, + env: &TypeEnv, + ) -> Result<(), CompileError> { + if crate::types::call_args::has_named_args(args) + || args.iter().any(|arg| matches!(arg.kind, ExprKind::Spread(_))) + { + return Err(CompileError::new( + expr.span, + "Cannot use named or spread arguments for eval-declared class construction", + )); + } + for arg in args { + self.infer_type(arg, env)?; + } + Ok(()) + } + /// Records the PHAR bridge and decompression libraries needed by PHAR archive helpers. pub(crate) fn require_phar_archive_libraries(&mut self) { self.require_builtin_library("elephc_phar"); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6a7dd4ecbc..356509462b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18939,6 +18939,44 @@ return $box->x;'); assert_eq!(out, "DynEvalSupported:9:Y10:12:12"); } +/// Verifies native object construction can use eval-declared classes after the barrier. +#[test] +fn test_eval_declared_class_constructs_object_natively_after_barrier() { + let out = compile_and_run( + r#"x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +}'); +$box = new DynEvalNativeSupported(5); +echo $box->bump(4) . ":"; +echo $box->x; +"#, + ); + assert_eq!(out, "9:9"); +} + +/// Verifies eval class declarations from a namespace are registered globally. +#[test] +fn test_eval_declared_class_in_namespace_is_global() { + let out = compile_and_run( + r#"label(); +"#, + ); + assert_eq!(out, "0:1:global"); +} + /// Verifies eval-declared by-reference promoted properties remain aliased after construction. #[test] fn test_eval_declared_class_aliases_by_reference_promoted_property() { From de179ee5f64858454fc1c188aa00f18463b3cbe1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 04:47:24 +0200 Subject: [PATCH 0825/1208] Support eval object introspection after barrier --- crates/elephc-magician/src/ffi/mod.rs | 4 + .../src/ffi/object_introspection.rs | 137 +++++++++++++++++ crates/elephc-magician/src/interpreter/mod.rs | 42 +++++ src/codegen/lower_inst/builtins.rs | 21 +++ src/codegen/lower_inst/builtins/eval.rs | 144 ++++++++++++++++++ src/codegen/lower_inst/builtins/types.rs | 10 ++ tests/codegen/eval.rs | 22 +++ 7 files changed, 380 insertions(+) create mode 100644 crates/elephc-magician/src/ffi/object_introspection.rs diff --git a/crates/elephc-magician/src/ffi/mod.rs b/crates/elephc-magician/src/ffi/mod.rs index f6d21df4ef..6ecbf4acf0 100644 --- a/crates/elephc-magician/src/ffi/mod.rs +++ b/crates/elephc-magician/src/ffi/mod.rs @@ -19,6 +19,8 @@ pub mod native_functions; pub mod native_methods; #[cfg(not(test))] pub mod object_construction; +#[cfg(not(test))] +pub mod object_introspection; pub mod scope; pub mod symbols; pub(crate) mod util; @@ -32,6 +34,8 @@ pub use native_functions::*; pub use native_methods::*; #[cfg(not(test))] pub use object_construction::*; +#[cfg(not(test))] +pub use object_introspection::*; pub use scope::*; pub use symbols::*; diff --git a/crates/elephc-magician/src/ffi/object_introspection.rs b/crates/elephc-magician/src/ffi/object_introspection.rs new file mode 100644 index 0000000000..70e85ffa5b --- /dev/null +++ b/crates/elephc-magician/src/ffi/object_introspection.rs @@ -0,0 +1,137 @@ +//! Purpose: +//! Exports post-barrier object introspection for values that may have been +//! created from eval-declared classes. Generated code uses this ABI when native +//! type metadata must consult the persistent eval context. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_object_class_name`. +//! - Generated EIR backend assembly through `__elephc_eval_object_is_a`. +//! +//! Key details: +//! - Class-name lookups return boxed Mixed string cells through `ElephcEvalResult`. +//! - Predicate probes fail closed as false when ABI inputs are invalid. + +use super::util::{abi_name_to_string, clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::interpreter::{self, EvalOutcome}; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +const CLASS_LOOKUP_GET_CLASS: u64 = 0; +const CLASS_LOOKUP_GET_PARENT_CLASS: u64 = 1; + +/// Resolves `get_class()` or `get_parent_class()` against eval dynamic objects. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `object_or_class` must point at a +/// boxed runtime cell, `lookup_kind` must be a supported lookup discriminator, +/// and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_object_class_name( + ctx: *mut ElephcEvalContext, + object_or_class: *mut RuntimeCell, + lookup_kind: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_object_class_name_inner(ctx, object_or_class, lookup_kind, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Tests whether an object satisfies a class/interface relation in the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `object` must point at a boxed +/// runtime cell. `target_ptr` must be readable for `target_len` bytes when +/// `target_len > 0`. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_object_is_a( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + target_ptr: *const u8, + target_len: u64, + exclude_self: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_object_is_a_inner(ctx, object, target_ptr, target_len, exclude_self) + }) + .unwrap_or(0) +} + +/// Runs the eval object class-name ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_object_class_name`; callers must provide a valid +/// context, a boxed runtime cell, and optional writable result storage. +#[cfg(not(test))] +unsafe fn eval_object_class_name_inner( + ctx: *mut ElephcEvalContext, + object_or_class: *mut RuntimeCell, + lookup_kind: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if object_or_class.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let lookup = match lookup_kind { + CLASS_LOOKUP_GET_CLASS => "get_class", + CLASS_LOOKUP_GET_PARENT_CLASS => "get_parent_class", + _ => return EvalStatus::RuntimeFatal.code(), + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_object_class_name( + context, + lookup, + RuntimeCellHandle::from_raw(object_or_class), + &mut values, + ) { + Ok(result) => write_outcome(EvalOutcome::Value(result), out).code(), + Err(status) => status.code(), + } +} + +/// Runs the eval object relation ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_object_is_a`; invalid handles or unreadable target +/// storage fail closed as false. +#[cfg(not(test))] +unsafe fn eval_object_is_a_inner( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + target_ptr: *const u8, + target_len: u64, + exclude_self: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || object.is_null() { + return 0; + } + let Ok(target) = abi_name_to_string(target_ptr, target_len) else { + return 0; + }; + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_object_is_a( + context, + RuntimeCellHandle::from_raw(object), + &target, + exclude_self != 0, + &mut values, + ) { + Ok(result) => i32::from(result), + Err(_) => 0, + } +} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 25613bda3f..7702c29b93 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -261,6 +261,48 @@ pub fn execute_context_method_call_outcome( } } +/// Resolves object class-name builtins against eval dynamic-object metadata first. +pub fn execute_context_object_class_name( + context: &mut ElephcEvalContext, + lookup: &str, + object_or_class: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match lookup { + "get_class" => eval_get_class_result(object_or_class, context, values), + "get_parent_class" => eval_get_parent_class_result(object_or_class, context, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Tests an object relation against eval dynamic-object metadata before AOT metadata. +pub fn execute_context_object_is_a( + context: &mut ElephcEvalContext, + object: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Ok(false); + } + let target_class = target_class.trim_start_matches('\\'); + let resolved_target_class = context + .resolve_class_like_name(target_class) + .unwrap_or_else(|| target_class.to_string()); + dynamic_object_is_a( + object, + &resolved_target_class, + exclude_self, + context, + values, + )? + .map_or_else( + || values.object_is_a(object, &resolved_target_class, exclude_self), + Ok, + ) +} + /// Returns the current interpreter availability status for the ABI stub. pub fn current_stub_status() -> EvalStatus { EvalStatus::UnsupportedConstruct diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 20761c746c..5a18bd7568 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -123,6 +123,27 @@ pub(super) fn lower_eval_method_call( eval::lower_eval_method_call(ctx, inst, object, method_name) } +/// Lowers post-eval object class-name introspection against eval dynamic metadata. +pub(super) fn lower_eval_object_class_name( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + name: &str, +) -> Result<()> { + eval::lower_eval_object_class_name(ctx, inst, object, name) +} + +/// Lowers post-eval object/class relation predicates against eval dynamic metadata. +pub(super) fn lower_eval_object_is_a( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + target_class: &str, + exclude_self: bool, +) -> Result<()> { + eval::lower_eval_object_is_a(ctx, inst, object, target_class, exclude_self) +} + /// Returns true when this lowered function has a persistent eval context local. pub(super) fn has_eval_context(ctx: &FunctionContext<'_>) -> bool { eval::has_eval_context(ctx) diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index b1c3a76a7d..609613471b 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -52,6 +52,8 @@ const EVAL_CALLED_CLASS_PTR_OFFSET: usize = 72; const EVAL_CALLED_CLASS_LEN_OFFSET: usize = 80; const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; +const EVAL_CLASS_LOOKUP_GET_CLASS: i64 = 0; +const EVAL_CLASS_LOOKUP_GET_PARENT_CLASS: i64 = 1; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; const NATIVE_DEFAULT_NULL: i64 = 0; const NATIVE_DEFAULT_BOOL: i64 = 1; @@ -436,6 +438,94 @@ pub(super) fn lower_eval_method_call( store_if_result(ctx, inst) } +/// Lowers object class-name introspection through the eval bridge. +pub(super) fn lower_eval_object_class_name( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + name: &str, +) -> Result<()> { + let lookup_kind = eval_class_lookup_kind(name)?; + let non_object_label = ctx.next_label("eval_object_class_non_object"); + let done_label = ctx.next_label("eval_object_class_done"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_object_operand(ctx, object)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_eval_unboxed_not_object(ctx, &non_object_label); + load_eval_context_to_arg(ctx, 0); + let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + lookup_kind, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_object_class_name"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_eval_unboxed_string_result(ctx); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&non_object_label); + emit_eval_string_result(ctx, b""); + + ctx.emitter.label(&done_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers object/class relation predicates through the eval bridge. +pub(super) fn lower_eval_object_is_a( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + target_class: &str, + exclude_self: bool, +) -> Result<()> { + let false_label = ctx.next_label("eval_object_is_a_false"); + let done_label = ctx.next_label("eval_object_is_a_done"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_object_operand(ctx, object)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_eval_unboxed_not_object(ctx, &false_label); + load_eval_context_to_arg(ctx, 0); + let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); + let (target_label, target_len) = ctx.data.add_string(target_class.as_bytes()); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_symbol_address(ctx.emitter, target_arg, &target_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + target_len as i64, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + i64::from(exclude_self), + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_object_is_a"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + + ctx.emitter.label(&done_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Returns true when the current function owns an eval context local. pub(super) fn has_eval_context(ctx: &FunctionContext<'_>) -> bool { eval_context_slot(ctx).is_ok() @@ -599,6 +689,60 @@ fn store_eval_method_call_arg_pack( Ok(()) } +/// Stores an object operand as a boxed Mixed cell in eval scratch storage. +fn store_eval_object_operand(ctx: &mut FunctionContext<'_>, object: ValueId) -> Result<()> { + let object_ty = ctx.load_value_to_result(object)?.codegen_repr(); + if !matches!(object_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &object_ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + Ok(()) +} + +/// Returns the eval ABI discriminator for a class-name builtin. +fn eval_class_lookup_kind(name: &str) -> Result { + match name { + "get_class" => Ok(EVAL_CLASS_LOOKUP_GET_CLASS), + "get_parent_class" => Ok(EVAL_CLASS_LOOKUP_GET_PARENT_CLASS), + _ => Err(CodegenIrError::unsupported(format!( + "eval object class-name lookup {}", + name + ))), + } +} + +/// Branches when `__rt_mixed_unbox` did not expose an object payload. +fn emit_branch_if_eval_unboxed_not_object(ctx: &mut FunctionContext<'_>, label: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed value contains an object + ctx.emitter + .instruction(&format!("b.ne {}", label)); // non-object values use the native false/empty fallback + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed value contains an object + ctx.emitter + .instruction(&format!("jne {}", label)); // non-object values use the native false/empty fallback + } + } +} + +/// Reorders an unboxed eval string cell into the target string result registers. +fn emit_eval_unboxed_string_result(ctx: &mut FunctionContext<'_>) { + if ctx.emitter.target.arch == Arch::X86_64 { + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the x86_64 string-result register + } +} + +/// Emits a borrowed string literal as the current native string result. +fn emit_eval_string_result(ctx: &mut FunctionContext<'_>, bytes: &[u8]) { + let (label, len) = ctx.data.add_string(bytes); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); +} + /// Saves the loaded eval source string while scope setup calls use argument registers. fn save_eval_code_string(ctx: &mut FunctionContext<'_>) { let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); diff --git a/src/codegen/lower_inst/builtins/types.rs b/src/codegen/lower_inst/builtins/types.rs index bf2b081e10..ff24d0fce3 100644 --- a/src/codegen/lower_inst/builtins/types.rs +++ b/src/codegen/lower_inst/builtins/types.rs @@ -345,6 +345,9 @@ pub(crate) fn lower_class_name_lookup( ctx.load_value_to_result(value)?; emit_dynamic_object_class_name(ctx, name); } + PhpType::Mixed | PhpType::Union(_) if super::has_eval_context(ctx) => { + return super::lower_eval_object_class_name(ctx, inst, value, name); + } PhpType::Mixed | PhpType::Union(_) => { ctx.load_value_to_result(value)?; emit_mixed_object_class_name(ctx, name); @@ -376,6 +379,13 @@ pub(crate) fn lower_is_a_relation( let object = expect_operand(inst, 0)?; let target = expect_operand(inst, 1)?; let exclude_self = name == "is_subclass_of"; + if matches!(ctx.value_php_type(object)?, PhpType::Mixed | PhpType::Union(_)) + && super::has_eval_context(ctx) + { + if let Some(target_class) = optional_const_string_operand(ctx, target)? { + return super::lower_eval_object_is_a(ctx, inst, object, &target_class, exclude_self); + } + } let result = static_relation_holds(ctx, object, target, exclude_self)?; emit_bool_result(ctx, result); store_if_result(ctx, inst) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 356509462b..1f1f418903 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18957,6 +18957,28 @@ echo $box->x; assert_eq!(out, "9:9"); } +/// Verifies native introspection sees eval-declared object metadata after the barrier. +#[test] +fn test_eval_declared_class_native_introspection_after_barrier() { + let out = compile_and_run( + r#" Date: Sat, 27 Jun 2026 04:57:46 +0200 Subject: [PATCH 0826/1208] Support eval callable array fallback --- crates/elephc-magician/src/ffi/callables.rs | 71 +++++++++++++++++++ crates/elephc-magician/src/ffi/mod.rs | 4 ++ crates/elephc-magician/src/interpreter/mod.rs | 17 +++++ src/codegen/lower_inst/builtins.rs | 10 +++ src/codegen/lower_inst/builtins/eval.rs | 48 +++++++++++-- src/codegen/lower_inst/callables.rs | 7 +- tests/codegen/eval.rs | 5 +- 7 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 crates/elephc-magician/src/ffi/callables.rs diff --git a/crates/elephc-magician/src/ffi/callables.rs b/crates/elephc-magician/src/ffi/callables.rs new file mode 100644 index 0000000000..72f7131b58 --- /dev/null +++ b/crates/elephc-magician/src/ffi/callables.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Exports post-barrier callable dispatch for callback values that may reference +//! eval-declared functions, methods, or objects. Generated code uses this ABI +//! after native descriptor dispatch cannot resolve a dynamic callable array. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_callable_call_array`. +//! +//! Key details: +//! - Callback and argument containers are boxed Mixed cells owned by generated +//! code. Results and uncaught throwables are returned through `ElephcEvalResult`. + +use super::util::{clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::interpreter; +use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; + +/// Dispatches a callback value with a PHP argument array through the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `callback` and `arg_array` must +/// point at boxed runtime cells, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_callable_call_array( + ctx: *mut ElephcEvalContext, + callback: *mut RuntimeCell, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_callable_call_array_inner(ctx, callback, arg_array, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the eval callable-array ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_callable_call_array`; callers must provide a valid +/// context and boxed callback/argument-array cells. +#[cfg(not(test))] +unsafe fn eval_callable_call_array_inner( + ctx: *mut ElephcEvalContext, + callback: *mut RuntimeCell, + arg_array: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if callback.is_null() || arg_array.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_callable_call_array_outcome( + context, + RuntimeCellHandle::from_raw(callback), + RuntimeCellHandle::from_raw(arg_array), + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} diff --git a/crates/elephc-magician/src/ffi/mod.rs b/crates/elephc-magician/src/ffi/mod.rs index 6ecbf4acf0..979c27473d 100644 --- a/crates/elephc-magician/src/ffi/mod.rs +++ b/crates/elephc-magician/src/ffi/mod.rs @@ -9,6 +9,8 @@ //! - Every exported function installs a panic boundary before touching bridge state. //! - Helper routines stay private to the FFI layer unless shared across families. +#[cfg(not(test))] +pub mod callables; pub mod context; pub mod declared_symbols; pub(crate) mod dynamic_destructors; @@ -25,6 +27,8 @@ pub mod scope; pub mod symbols; pub(crate) mod util; +#[cfg(not(test))] +pub use callables::*; pub use context::*; pub use declared_symbols::*; pub use execute::*; diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 7702c29b93..5f55d61453 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -214,6 +214,23 @@ pub fn execute_context_function_call_array_outcome( } } +/// Executes a callback value with a prepared argument array in the shared eval context. +pub fn execute_context_callable_call_array_outcome( + context: &mut ElephcEvalContext, + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_call_user_func_array_with_values(callback, arg_array, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + /// Constructs a class declared in the shared eval context with prepared positional arguments. pub fn execute_context_new_object_outcome( context: &mut ElephcEvalContext, diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 5a18bd7568..039cbd6194 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -123,6 +123,16 @@ pub(super) fn lower_eval_method_call( eval::lower_eval_method_call(ctx, inst, object, method_name) } +/// Lowers post-eval callable-array dispatch against eval dynamic callables. +pub(super) fn lower_eval_callable_call_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + callback: ValueId, + arg_array: ValueId, +) -> Result<()> { + eval::lower_eval_callable_call_array(ctx, inst, callback, arg_array) +} + /// Lowers post-eval object class-name introspection against eval dynamic metadata. pub(super) fn lower_eval_object_class_name( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 609613471b..45f61cca9d 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -54,6 +54,7 @@ const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; const EVAL_CLASS_LOOKUP_GET_CLASS: i64 = 0; const EVAL_CLASS_LOOKUP_GET_PARENT_CLASS: i64 = 1; +const EVAL_CALLABLE_ARG_ARRAY_OFFSET: usize = EVAL_CODE_PTR_OFFSET; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; const NATIVE_DEFAULT_NULL: i64 = 0; const NATIVE_DEFAULT_BOOL: i64 = 1; @@ -438,6 +439,36 @@ pub(super) fn lower_eval_method_call( store_if_result(ctx, inst) } +/// Lowers a callable-array dispatch through the eval bridge. +pub(super) fn lower_eval_callable_call_array( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + callback: ValueId, + arg_array: ValueId, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, callback, EVAL_TEMP_CELL_OFFSET)?; + store_eval_mixed_operand_at(ctx, arg_array, EVAL_CALLABLE_ARG_ARRAY_OFFSET)?; + load_eval_context_to_arg(ctx, 0); + let callback_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, callback_arg, EVAL_TEMP_CELL_OFFSET); + let arg_array_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, arg_array_arg, EVAL_CALLABLE_ARG_ARRAY_OFFSET); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_callable_call_array"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Lowers object class-name introspection through the eval bridge. pub(super) fn lower_eval_object_class_name( ctx: &mut FunctionContext<'_>, @@ -691,12 +722,21 @@ fn store_eval_method_call_arg_pack( /// Stores an object operand as a boxed Mixed cell in eval scratch storage. fn store_eval_object_operand(ctx: &mut FunctionContext<'_>, object: ValueId) -> Result<()> { - let object_ty = ctx.load_value_to_result(object)?.codegen_repr(); - if !matches!(object_ty, PhpType::Mixed | PhpType::Union(_)) { - emit_box_current_value_as_mixed(ctx.emitter, &object_ty); + store_eval_mixed_operand_at(ctx, object, EVAL_TEMP_CELL_OFFSET) +} + +/// Stores one operand as a boxed Mixed cell at an eval scratch offset. +fn store_eval_mixed_operand_at( + ctx: &mut FunctionContext<'_>, + value: ValueId, + offset: usize, +) -> Result<()> { + let value_ty = ctx.load_value_to_result(value)?.codegen_repr(); + if !matches!(value_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &value_ty); } let result_reg = abi::int_result_reg(ctx.emitter); - abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + abi::emit_store_to_sp(ctx.emitter, result_reg, offset); Ok(()) } diff --git a/src/codegen/lower_inst/callables.rs b/src/codegen/lower_inst/callables.rs index e62846c61e..bc9534209f 100644 --- a/src/codegen/lower_inst/callables.rs +++ b/src/codegen/lower_inst/callables.rs @@ -680,7 +680,12 @@ fn lower_runtime_mixed_callable_array_descriptor_invoke( abi::emit_jump(ctx.emitter, &miss_label); ctx.emitter.label(&miss_label); - emit_runtime_callable_array_no_match_abort(ctx); + if super::builtins::has_eval_context(ctx) { + super::builtins::lower_eval_callable_call_array(ctx, inst, callable, arg_mixed)?; + abi::emit_jump(ctx.emitter, &done_label); + } else { + emit_runtime_callable_array_no_match_abort(ctx); + } ctx.emitter.label(&done_label); abi::emit_release_temporary_stack(ctx.emitter, MIXED_SELECTOR_BYTES); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1f1f418903..3dbab67a01 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18951,10 +18951,13 @@ eval('class DynEvalNativeSupported { }'); $box = new DynEvalNativeSupported(5); echo $box->bump(4) . ":"; +$call = [$box, "bump"]; +echo call_user_func($call, 1) . ":"; +echo call_user_func_array($call, [2]) . ":"; echo $box->x; "#, ); - assert_eq!(out, "9:9"); + assert_eq!(out, "9:10:12:12"); } /// Verifies native introspection sees eval-declared object metadata after the barrier. From 0a9aee7a09597aadd3b9de5037c1a54c78345468 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 05:12:44 +0200 Subject: [PATCH 0827/1208] Support eval callable probes after barrier --- crates/elephc-magician/src/ffi/callables.rs | 50 +++++++++++++++++-- crates/elephc-magician/src/interpreter/mod.rs | 12 +++++ src/codegen/lower_inst/builtins.rs | 15 +++++- src/codegen/lower_inst/builtins/eval.rs | 18 +++++++ src/ir_lower/expr/mod.rs | 4 ++ tests/codegen/eval.rs | 26 ++++++++++ 6 files changed, 120 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/ffi/callables.rs b/crates/elephc-magician/src/ffi/callables.rs index 72f7131b58..77235b2e14 100644 --- a/crates/elephc-magician/src/ffi/callables.rs +++ b/crates/elephc-magician/src/ffi/callables.rs @@ -1,14 +1,16 @@ //! Purpose: -//! Exports post-barrier callable dispatch for callback values that may reference -//! eval-declared functions, methods, or objects. Generated code uses this ABI -//! after native descriptor dispatch cannot resolve a dynamic callable array. +//! Exports post-barrier callable dispatch and probes for callback values that +//! may reference eval-declared functions, methods, or objects. Generated code +//! uses this ABI when native descriptor metadata cannot answer dynamically. //! //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_callable_call_array`. +//! - Generated EIR backend assembly through `__elephc_eval_is_callable`. //! //! Key details: //! - Callback and argument containers are boxed Mixed cells owned by generated -//! code. Results and uncaught throwables are returned through `ElephcEvalResult`. +//! code. Dispatch results and uncaught throwables are returned through +//! `ElephcEvalResult`; probe failures fail closed as `false`. use super::util::{clear_result, write_outcome}; use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; @@ -17,6 +19,20 @@ use crate::interpreter; use crate::runtime_hooks::ElephcRuntimeOps; use crate::value::{RuntimeCell, RuntimeCellHandle}; +/// Checks whether a callback value is callable in the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle and `callback` must point at a +/// boxed runtime cell. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_is_callable( + ctx: *mut ElephcEvalContext, + callback: *mut RuntimeCell, +) -> i32 { + std::panic::catch_unwind(|| unsafe { eval_is_callable_inner(ctx, callback) }).unwrap_or(0) +} + /// Dispatches a callback value with a PHP argument array through the eval context. /// /// # Safety @@ -36,6 +52,32 @@ pub unsafe extern "C" fn __elephc_eval_callable_call_array( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Runs the eval callable-probe ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_is_callable`; invalid handles fail closed as false. +#[cfg(not(test))] +unsafe fn eval_is_callable_inner( + ctx: *mut ElephcEvalContext, + callback: *mut RuntimeCell, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || callback.is_null() { + return 0; + } + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_is_callable( + context, + RuntimeCellHandle::from_raw(callback), + &mut values, + ) { + Ok(callable) => i32::from(callable), + Err(_) => 0, + } +} + /// Runs the eval callable-array ABI body after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 5f55d61453..cb5913f0aa 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -231,6 +231,18 @@ pub fn execute_context_callable_call_array_outcome( } } +/// Probes whether a callback value is callable in the shared eval context. +pub fn execute_context_is_callable( + context: &ElephcEvalContext, + callback: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_function_probe_result("is_callable", callback, context, values)?; + let callable = values.truthy(result)?; + values.release(result)?; + Ok(callable) +} + /// Constructs a class declared in the shared eval context with prepared positional arguments. pub fn execute_context_new_object_outcome( context: &mut ElephcEvalContext, diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 039cbd6194..6d5ad88205 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -133,6 +133,15 @@ pub(super) fn lower_eval_callable_call_array( eval::lower_eval_callable_call_array(ctx, inst, callback, arg_array) } +/// Lowers post-eval callable probes against eval dynamic callables. +pub(super) fn lower_eval_is_callable( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + callback: ValueId, +) -> Result<()> { + eval::lower_eval_is_callable(ctx, inst, callback) +} + /// Lowers post-eval object class-name introspection against eval dynamic metadata. pub(super) fn lower_eval_object_class_name( ctx: &mut FunctionContext<'_>, @@ -535,7 +544,11 @@ fn emit_dynamic_class_like_exists_compare( pub(crate) fn lower_is_callable(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "is_callable", 1)?; let value = expect_operand(inst, 0)?; - match ctx.value_php_type(value)?.codegen_repr() { + let value_ty = ctx.value_php_type(value)?.codegen_repr(); + if has_eval_context(ctx) && value_ty != PhpType::Callable { + return lower_eval_is_callable(ctx, inst, value); + } + match value_ty { PhpType::Callable => emit_static_bool(ctx, true), PhpType::Str => { if let Ok(function_name) = const_string_operand(ctx, value) { diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 45f61cca9d..9a2f67c520 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -469,6 +469,24 @@ pub(super) fn lower_eval_callable_call_array( store_if_result(ctx, inst) } +/// Lowers an `is_callable()` probe through eval dynamic callable metadata. +pub(super) fn lower_eval_is_callable( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + callback: ValueId, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, callback, EVAL_TEMP_CELL_OFFSET)?; + load_eval_context_to_arg(ctx, 0); + let callback_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, callback_arg, EVAL_TEMP_CELL_OFFSET); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_is_callable"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Lowers object class-name introspection through the eval bridge. pub(super) fn lower_eval_object_class_name( ctx: &mut FunctionContext<'_>, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 6c1ad6ac02..e11d7661ae 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -2589,6 +2589,10 @@ fn lower_static_is_callable( if crate::types::call_args::has_named_args(args) || args.iter().any(is_spread_arg) { return None; } + // Eval can declare callable targets after static metadata has been built. + if ctx.has_eval_barrier() { + return None; + } match &args[0].kind { ExprKind::FirstClassCallable( CallableTarget::Function(_) | CallableTarget::StaticMethod { .. }, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3dbab67a01..431eaa2223 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18982,6 +18982,32 @@ echo is_subclass_of($box, "DynEvalNativeIntrospectBase") ? "P" : "p"; assert_eq!(out, "DynEvalNativeIntrospectChild:DynEvalNativeIntrospectBase:C:B:s:P"); } +/// Verifies native callable probes see eval-declared dynamic callable targets. +#[test] +fn test_eval_declared_class_native_callable_probe_after_barrier() { + let out = compile_and_run( + r#" Date: Sat, 27 Jun 2026 05:44:39 +0200 Subject: [PATCH 0828/1208] Support eval member probes after barrier --- .../src/ffi/object_introspection.rs | 57 +++++ crates/elephc-magician/src/interpreter/mod.rs | 14 ++ src/codegen/lower_inst/builtins.rs | 200 +++++++++++++++++- src/codegen/lower_inst/builtins/eval.rs | 56 +++++ src/ir_lower/expr/mod.rs | 5 +- src/types/checker/builtins/catalog.rs | 2 + src/types/checker/builtins/numeric.rs | 11 + src/types/signatures.rs | 18 ++ tests/codegen/eval.rs | 31 +++ tests/codegen/objects/classes.rs | 30 +++ 10 files changed, 421 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/ffi/object_introspection.rs b/crates/elephc-magician/src/ffi/object_introspection.rs index 70e85ffa5b..5ea3f9f956 100644 --- a/crates/elephc-magician/src/ffi/object_introspection.rs +++ b/crates/elephc-magician/src/ffi/object_introspection.rs @@ -6,6 +6,7 @@ //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_object_class_name`. //! - Generated EIR backend assembly through `__elephc_eval_object_is_a`. +//! - Generated EIR backend assembly through `__elephc_eval_member_exists`. //! //! Key details: //! - Class-name lookups return boxed Mixed string cells through `ElephcEvalResult`. @@ -20,6 +21,8 @@ use crate::value::{RuntimeCell, RuntimeCellHandle}; const CLASS_LOOKUP_GET_CLASS: u64 = 0; const CLASS_LOOKUP_GET_PARENT_CLASS: u64 = 1; +const MEMBER_LOOKUP_METHOD_EXISTS: u64 = 0; +const MEMBER_LOOKUP_PROPERTY_EXISTS: u64 = 1; /// Resolves `get_class()` or `get_parent_class()` against eval dynamic objects. /// @@ -62,6 +65,25 @@ pub unsafe extern "C" fn __elephc_eval_object_is_a( .unwrap_or(0) } +/// Tests whether a method or property exists in the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `target` and `member` must point +/// at boxed runtime cells. `lookup_kind` must be a supported discriminator. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_member_exists( + ctx: *mut ElephcEvalContext, + target: *mut RuntimeCell, + member: *mut RuntimeCell, + lookup_kind: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_member_exists_inner(ctx, target, member, lookup_kind) + }) + .unwrap_or(0) +} + /// Runs the eval object class-name ABI body after installing a panic boundary. /// /// # Safety @@ -135,3 +157,38 @@ unsafe fn eval_object_is_a_inner( Err(_) => 0, } } + +/// Runs the eval member-exists ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_member_exists`; invalid handles fail closed as false. +#[cfg(not(test))] +unsafe fn eval_member_exists_inner( + ctx: *mut ElephcEvalContext, + target: *mut RuntimeCell, + member: *mut RuntimeCell, + lookup_kind: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || target.is_null() || member.is_null() { + return 0; + } + let lookup = match lookup_kind { + MEMBER_LOOKUP_METHOD_EXISTS => "method_exists", + MEMBER_LOOKUP_PROPERTY_EXISTS => "property_exists", + _ => return 0, + }; + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_member_exists( + context, + lookup, + RuntimeCellHandle::from_raw(target), + RuntimeCellHandle::from_raw(member), + &mut values, + ) { + Ok(result) => i32::from(result), + Err(_) => 0, + } +} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index cb5913f0aa..973be5eb67 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -332,6 +332,20 @@ pub fn execute_context_object_is_a( ) } +/// Tests whether a method or property exists through eval dynamic metadata. +pub fn execute_context_member_exists( + context: &mut ElephcEvalContext, + name: &str, + target: RuntimeCellHandle, + member: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_member_exists_result(name, &[target, member], context, values)?; + let exists = values.truthy(result)?; + values.release(result)?; + Ok(exists) +} + /// Returns the current interpreter availability status for the ABI stub. pub fn current_stub_status() -> EvalStatus { EvalStatus::UnsupportedConstruct diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 6d5ad88205..3af6ce205b 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -9,13 +9,15 @@ //! - Runtime conversions reuse existing target-aware helpers instead of duplicating parsing logic. //! - Selected Mixed predicates inspect the boxed runtime tag through shared predicate lowering. +use std::collections::BTreeSet; + use crate::codegen::abi; use crate::codegen::platform::Arch; use crate::ir::{Immediate, Instruction, Op, ValueDef, ValueId}; use crate::names::{define_seen_symbol, ir_global_symbol, php_symbol_key}; use crate::parser::ast::Visibility; use crate::types::checker::builtins::is_php_visible_builtin_function; -use crate::types::PhpType; +use crate::types::{ClassInfo, PhpType}; use super::super::context::FunctionContext; use super::{ @@ -65,6 +67,7 @@ pub(super) fn lower_builtin_call(ctx: &mut FunctionContext<'_>, inst: &Instructi "buffer_len" => buffers::lower_buffer_len(ctx, inst), "buffer_free" => buffers::lower_buffer_free(ctx, inst), "strval" => lower_strval(ctx, inst), + "method_exists" | "property_exists" => lower_member_exists(ctx, inst, key.as_str()), "is_integer" | "is_long" => { lower_static_type_predicate(ctx, inst, key.as_str(), PhpType::Int) } @@ -142,6 +145,17 @@ pub(super) fn lower_eval_is_callable( eval::lower_eval_is_callable(ctx, inst, callback) } +/// Lowers post-eval member-existence probes against eval dynamic metadata. +pub(super) fn lower_eval_member_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + target: ValueId, + member: ValueId, + name: &str, +) -> Result<()> { + eval::lower_eval_member_exists(ctx, inst, target, member, name) +} + /// Lowers post-eval object class-name introspection against eval dynamic metadata. pub(super) fn lower_eval_object_class_name( ctx: &mut FunctionContext<'_>, @@ -622,6 +636,190 @@ fn emit_is_callable_dynamic_string_lookup(ctx: &mut FunctionContext<'_>) { abi::emit_call_label(ctx.emitter, "__rt_is_callable_string"); } +/// Lowers `method_exists()` and `property_exists()` through eval or static metadata. +fn lower_member_exists(ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str) -> Result<()> { + ensure_arg_count(inst, name, 2)?; + let target = expect_operand(inst, 0)?; + let member = expect_operand(inst, 1)?; + if has_eval_context(ctx) { + return lower_eval_member_exists(ctx, inst, target, member, name); + } + let member_name = const_string_operand(ctx, member)?; + let exists = match ctx.value_php_type(target)?.codegen_repr() { + PhpType::Object(class_name) => { + static_member_exists_on_class(ctx, &class_name, &member_name, name, true) + } + PhpType::Str => { + let class_name = const_string_operand(ctx, target)?; + static_member_exists_on_class(ctx, &class_name, &member_name, name, false) + } + other => { + return Err(CodegenIrError::unsupported(format!( + "{} target PHP type {:?}", + name, other + ))) + } + }; + emit_static_bool(ctx, exists); + store_if_result(ctx, inst) +} + +/// Checks one static class-like target for `method_exists()` or `property_exists()`. +fn static_member_exists_on_class( + ctx: &FunctionContext<'_>, + class_name: &str, + member_name: &str, + name: &str, + target_is_object: bool, +) -> bool { + let Some((resolved_class, class_info)) = lookup_class_info(ctx, class_name) else { + return false; + }; + match name { + "method_exists" => static_method_exists_on_class_info( + ctx, + &resolved_class, + class_info, + member_name, + target_is_object, + ), + "property_exists" => static_property_exists_on_class_info( + &resolved_class, + class_info, + member_name, + target_is_object, + ), + _ => false, + } +} + +/// Looks up class metadata using PHP's case-insensitive class-name semantics. +fn lookup_class_info<'a>( + ctx: &'a FunctionContext<'_>, + class_name: &str, +) -> Option<(String, &'a ClassInfo)> { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + ctx.module + .class_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == class_key) + .map(|(candidate, class_info)| (candidate.clone(), class_info)) +} + +/// Checks static method metadata while hiding inherited private methods on class-string targets. +fn static_method_exists_on_class_info( + ctx: &FunctionContext<'_>, + resolved_class: &str, + class_info: &ClassInfo, + method_name: &str, + target_is_object: bool, +) -> bool { + let method_key = php_symbol_key(method_name); + if class_info.methods.contains_key(&method_key) { + return target_is_object + || method_visible_from_class_string( + resolved_class, + &method_key, + &class_info.method_visibilities, + &class_info.method_declaring_classes, + ); + } + if class_info.static_methods.contains_key(&method_key) { + return target_is_object + || method_visible_from_class_string( + resolved_class, + &method_key, + &class_info.static_method_visibilities, + &class_info.static_method_declaring_classes, + ); + } + if target_is_object { + return static_parent_chain_method_exists(ctx, class_info, &method_key); + } + false +} + +/// Checks parent class metadata for private methods visible to object-target `method_exists()`. +fn static_parent_chain_method_exists( + ctx: &FunctionContext<'_>, + class_info: &ClassInfo, + method_key: &str, +) -> bool { + let mut visited = BTreeSet::new(); + let mut parent_name = class_info.parent.as_deref(); + while let Some(candidate) = parent_name { + let parent_key = php_symbol_key(candidate.trim_start_matches('\\')); + if !visited.insert(parent_key) { + return false; + } + let Some((_resolved_class, parent_info)) = lookup_class_info(ctx, candidate) else { + return false; + }; + if parent_info.methods.contains_key(method_key) + || parent_info.static_methods.contains_key(method_key) + { + return true; + } + parent_name = parent_info.parent.as_deref(); + } + false +} + +/// Returns whether a method should be visible for a class-string member probe. +fn method_visible_from_class_string( + resolved_class: &str, + method_key: &str, + visibilities: &std::collections::HashMap, + declaring_classes: &std::collections::HashMap, +) -> bool { + visibilities.get(method_key) != Some(&Visibility::Private) + || declaring_classes + .get(method_key) + .is_none_or(|declaring_class| { + php_symbol_key(declaring_class.trim_start_matches('\\')) + == php_symbol_key(resolved_class.trim_start_matches('\\')) + }) +} + +/// Checks static property metadata while hiding inherited private properties. +fn static_property_exists_on_class_info( + resolved_class: &str, + class_info: &ClassInfo, + property_name: &str, + _target_is_object: bool, +) -> bool { + property_visible_from_class_string( + resolved_class, + property_name, + &class_info.property_visibilities, + &class_info.property_declaring_classes, + ) || property_visible_from_class_string( + resolved_class, + property_name, + &class_info.static_property_visibilities, + &class_info.static_property_declaring_classes, + ) +} + +/// Returns whether a property exists for a class-string or ordinary object probe. +fn property_visible_from_class_string( + resolved_class: &str, + property_name: &str, + visibilities: &std::collections::HashMap, + declaring_classes: &std::collections::HashMap, +) -> bool { + let Some(visibility) = visibilities.get(property_name) else { + return false; + }; + visibility != &Visibility::Private + || declaring_classes + .get(property_name) + .is_none_or(|declaring_class| { + php_symbol_key(declaring_class.trim_start_matches('\\')) + == php_symbol_key(resolved_class.trim_start_matches('\\')) + }) +} + /// Returns true when a static `Class::method` string names a public static method. fn static_method_string_is_callable( ctx: &FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 9a2f67c520..2cc2befa4f 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -54,6 +54,8 @@ const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; const EVAL_CLASS_LOOKUP_GET_CLASS: i64 = 0; const EVAL_CLASS_LOOKUP_GET_PARENT_CLASS: i64 = 1; +const EVAL_MEMBER_LOOKUP_METHOD_EXISTS: i64 = 0; +const EVAL_MEMBER_LOOKUP_PROPERTY_EXISTS: i64 = 1; const EVAL_CALLABLE_ARG_ARRAY_OFFSET: usize = EVAL_CODE_PTR_OFFSET; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; const NATIVE_DEFAULT_NULL: i64 = 0; @@ -484,6 +486,37 @@ pub(super) fn lower_eval_is_callable( let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_is_callable"); abi::emit_call_label(ctx.emitter, &symbol); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); + store_if_result(ctx, inst) +} + +/// Lowers member-existence introspection through eval dynamic metadata. +pub(super) fn lower_eval_member_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + target: ValueId, + member: ValueId, + name: &str, +) -> Result<()> { + let lookup_kind = eval_member_lookup_kind(name)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, target, EVAL_TEMP_CELL_OFFSET)?; + store_eval_mixed_operand_at(ctx, member, EVAL_CODE_PTR_OFFSET)?; + load_eval_context_to_arg(ctx, 0); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, target_arg, EVAL_TEMP_CELL_OFFSET); + let member_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, member_arg, EVAL_CODE_PTR_OFFSET); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + lookup_kind, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_member_exists"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); store_if_result(ctx, inst) } @@ -529,6 +562,7 @@ pub(super) fn lower_eval_object_class_name( ctx.emitter.label(&done_label); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); store_if_result(ctx, inst) } @@ -603,6 +637,7 @@ pub(super) fn lower_eval_function_exists( .extern_symbol("__elephc_eval_function_exists"); abi::emit_call_label(ctx.emitter, &symbol); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); store_if_result(ctx, inst) } @@ -628,6 +663,7 @@ pub(super) fn lower_eval_class_exists( .extern_symbol("__elephc_eval_dynamic_class_exists"); abi::emit_call_label(ctx.emitter, &symbol); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); store_if_result(ctx, inst) } @@ -654,6 +690,7 @@ pub(super) fn lower_eval_constant_exists( .extern_symbol("__elephc_eval_constant_exists"); abi::emit_call_label(ctx.emitter, &symbol); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + box_eval_bool_result_if_mixed(ctx, inst); store_if_result(ctx, inst) } @@ -758,6 +795,13 @@ fn store_eval_mixed_operand_at( Ok(()) } +/// Boxes a raw eval predicate result when the enclosing IR value expects Mixed storage. +fn box_eval_bool_result_if_mixed(ctx: &mut FunctionContext<'_>, inst: &Instruction) { + if inst.result.is_some() && inst.result_php_type.codegen_repr() == PhpType::Mixed { + emit_box_current_value_as_mixed(ctx.emitter, &PhpType::Bool); + } +} + /// Returns the eval ABI discriminator for a class-name builtin. fn eval_class_lookup_kind(name: &str) -> Result { match name { @@ -770,6 +814,18 @@ fn eval_class_lookup_kind(name: &str) -> Result { } } +/// Returns the eval ABI discriminator for member-existence builtins. +fn eval_member_lookup_kind(name: &str) -> Result { + match name { + "method_exists" => Ok(EVAL_MEMBER_LOOKUP_METHOD_EXISTS), + "property_exists" => Ok(EVAL_MEMBER_LOOKUP_PROPERTY_EXISTS), + _ => Err(CodegenIrError::unsupported(format!( + "eval member-exists lookup {}", + name + ))), + } +} + /// Branches when `__rt_mixed_unbox` did not expose an object payload. fn emit_branch_if_eval_unboxed_not_object(ctx: &mut FunctionContext<'_>, label: &str) { match ctx.emitter.target.arch { diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index e11d7661ae..e15f7d00a6 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -7022,8 +7022,9 @@ fn builtin_return_type_override(name: &str) -> Option { | "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_object" | "is_real" | "is_scalar" | "is_string" - | "fdatasync" | "fflush" | "flock" | "fsync" | "ftruncate" | "interface_exists" | "is_dir" - | "is_executable" | "is_file" | "is_link" | "is_numeric" | "link" | "mkdir" | "rename" + | "fdatasync" | "fflush" | "flock" | "fsync" | "ftruncate" | "interface_exists" + | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_numeric" | "link" + | "method_exists" | "mkdir" | "property_exists" | "rename" | "enum_exists" | "trait_exists" | "putenv" | "rmdir" | "is_readable" | "is_subclass_of" | "is_writeable" | "is_writable" | "settype" | "is_resource" | "hash_equals" | "hash_update" | "spl_autoload_register" diff --git a/src/types/checker/builtins/catalog.rs b/src/types/checker/builtins/catalog.rs index d4bcba6c94..de98768dc2 100644 --- a/src/types/checker/builtins/catalog.rs +++ b/src/types/checker/builtins/catalog.rs @@ -25,6 +25,8 @@ const SUPPORTED_BUILTIN_FUNCTIONS: &[&str] = &[ "is_long", "is_real", "isset", + "method_exists", + "property_exists", "strval", "unset", ]; diff --git a/src/types/checker/builtins/numeric.rs b/src/types/checker/builtins/numeric.rs index 42dcc33cd1..cb70cfac8f 100644 --- a/src/types/checker/builtins/numeric.rs +++ b/src/types/checker/builtins/numeric.rs @@ -77,6 +77,17 @@ pub(super) fn check_builtin( checker.infer_type(&args[0], env)?; Ok(Some(PhpType::Bool)) } + "method_exists" | "property_exists" => { + if args.len() != 2 { + return Err(CompileError::new( + span, + &format!("{}() takes exactly 2 arguments", name), + )); + } + checker.infer_type(&args[0], env)?; + checker.infer_type(&args[1], env)?; + Ok(Some(PhpType::Bool)) + } "empty" => { if args.len() != 1 { return Err(CompileError::new(span, "empty() takes exactly 1 argument")); diff --git a/src/types/signatures.rs b/src/types/signatures.rs index b56813a21f..4dc16e7f23 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -180,6 +180,14 @@ pub(crate) fn legacy_builtin_call_sig(name: &str) -> Option { 1, vec![bool_lit(true)], )), + "method_exists" => Some(with_return( + fixed(&["object_or_class", "method"]), + PhpType::Bool, + )), + "property_exists" => Some(with_return( + fixed(&["object_or_class", "property"]), + PhpType::Bool, + )), "iterator_to_array" => Some(optional( &["iterator", "preserve_keys"], 1, @@ -813,6 +821,9 @@ fn general_first_class_callable_builtin_sig(name: &str) -> Option { &[PhpType::Str], PhpType::Bool, )), + "method_exists" | "property_exists" => { + return_typed_first_class_builtin_sig(name, PhpType::Bool) + } "gettype" => Some(typed_first_class_builtin_sig( name, &[PhpType::Mixed], @@ -1137,6 +1148,13 @@ fn variadic(regular_params: &[&str], variadic_name: &str) -> FunctionSig { make_sig(¶ms, defaults, Some(variadic_name)) } +/// Sets the declared return type for a signature while preserving its parameter contract. +fn with_return(mut sig: FunctionSig, return_type: PhpType) -> FunctionSig { + sig.return_type = return_type; + sig.declared_return = true; + sig +} + /// Marks the first parameter of a signature as by-reference. fn first_param_ref(mut sig: FunctionSig) -> FunctionSig { if let Some(first_ref) = sig.ref_params.first_mut() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 431eaa2223..57e23aa04a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19008,6 +19008,37 @@ echo is_callable([$box, "missing"]) ? "M" : "m"; assert_eq!(out, "Y:5:F:S:m"); } +/// Verifies native member-existence probes see eval-declared class metadata after the barrier. +#[test] +fn test_eval_declared_class_native_member_exists_after_barrier() { + let out = compile_and_run( + r#"value + 1; } + private function hidden() { return $this->secret; } +}'); +$box = new DynEvalNativeMemberProbe(); +echo method_exists($box, "bump") ? "M" : "m"; +echo ":"; +echo method_exists("DynEvalNativeMemberProbe", "bump") ? "C" : "c"; +echo ":"; +echo method_exists($box, "missing") ? "x" : "X"; +echo ":"; +echo property_exists($box, "value") ? "P" : "p"; +echo ":"; +echo property_exists("DynEvalNativeMemberProbe", "value") ? "S" : "s"; +echo ":"; +echo property_exists($box, "missing") ? "y" : "Y"; +echo ":"; +echo function_exists("method_exists") ? "F" : "f"; +echo function_exists("property_exists") ? "G" : "g"; +"#, + ); + assert_eq!(out, "M:C:X:P:S:Y:FG"); +} + /// Verifies eval class declarations from a namespace are registered globally. #[test] fn test_eval_declared_class_in_namespace_is_global() { diff --git a/tests/codegen/objects/classes.rs b/tests/codegen/objects/classes.rs index 8fc6d5d203..c399671f73 100644 --- a/tests/codegen/objects/classes.rs +++ b/tests/codegen/objects/classes.rs @@ -541,6 +541,36 @@ echo $s->reveal(); assert_eq!(out, "ok"); } +/// Verifies native `method_exists()` and `property_exists()` use AOT class metadata. +#[test] +fn test_class_member_exists_builtin_uses_native_metadata() { + let out = compile_and_run( + r#" Date: Sat, 27 Jun 2026 06:07:47 +0200 Subject: [PATCH 0829/1208] Support eval class relations after barrier --- .../src/ffi/object_introspection.rs | 66 ++++++++++++++++++- crates/elephc-magician/src/interpreter/mod.rs | 10 +++ src/builtins/callables/support.rs | 3 + src/codegen/lower_inst/builtins.rs | 10 +++ .../lower_inst/builtins/class_relations.rs | 9 ++- src/codegen/lower_inst/builtins/eval.rs | 49 ++++++++++++++ tests/codegen/eval.rs | 30 +++++++++ 7 files changed, 174 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/ffi/object_introspection.rs b/crates/elephc-magician/src/ffi/object_introspection.rs index 5ea3f9f956..71f10848ff 100644 --- a/crates/elephc-magician/src/ffi/object_introspection.rs +++ b/crates/elephc-magician/src/ffi/object_introspection.rs @@ -7,9 +7,10 @@ //! - Generated EIR backend assembly through `__elephc_eval_object_class_name`. //! - Generated EIR backend assembly through `__elephc_eval_object_is_a`. //! - Generated EIR backend assembly through `__elephc_eval_member_exists`. +//! - Generated EIR backend assembly through `__elephc_eval_class_relation`. //! //! Key details: -//! - Class-name lookups return boxed Mixed string cells through `ElephcEvalResult`. +//! - Class-name and relation lookups return boxed Mixed cells through `ElephcEvalResult`. //! - Predicate probes fail closed as false when ABI inputs are invalid. use super::util::{abi_name_to_string, clear_result, write_outcome}; @@ -23,6 +24,9 @@ const CLASS_LOOKUP_GET_CLASS: u64 = 0; const CLASS_LOOKUP_GET_PARENT_CLASS: u64 = 1; const MEMBER_LOOKUP_METHOD_EXISTS: u64 = 0; const MEMBER_LOOKUP_PROPERTY_EXISTS: u64 = 1; +const CLASS_RELATION_IMPLEMENTS: u64 = 0; +const CLASS_RELATION_PARENTS: u64 = 1; +const CLASS_RELATION_USES: u64 = 2; /// Resolves `get_class()` or `get_parent_class()` against eval dynamic objects. /// @@ -84,6 +88,26 @@ pub unsafe extern "C" fn __elephc_eval_member_exists( .unwrap_or(0) } +/// Resolves class/interface/trait relation metadata in the eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `target` must point at a boxed +/// runtime cell. `relation_kind` must be a supported discriminator, and `out` +/// may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_class_relation( + ctx: *mut ElephcEvalContext, + target: *mut RuntimeCell, + relation_kind: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_class_relation_inner(ctx, target, relation_kind, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Runs the eval object class-name ABI body after installing a panic boundary. /// /// # Safety @@ -123,6 +147,46 @@ unsafe fn eval_object_class_name_inner( } } +/// Runs the eval class-relation ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_class_relation`; callers must provide a valid context, +/// a boxed runtime cell target, and optional writable result storage. +#[cfg(not(test))] +unsafe fn eval_class_relation_inner( + ctx: *mut ElephcEvalContext, + target: *mut RuntimeCell, + relation_kind: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if target.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let lookup = match relation_kind { + CLASS_RELATION_IMPLEMENTS => "class_implements", + CLASS_RELATION_PARENTS => "class_parents", + CLASS_RELATION_USES => "class_uses", + _ => return EvalStatus::RuntimeFatal.code(), + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_class_relation( + context, + lookup, + RuntimeCellHandle::from_raw(target), + &mut values, + ) { + Ok(result) => write_outcome(EvalOutcome::Value(result), out).code(), + Err(status) => status.code(), + } +} + /// Runs the eval object relation ABI body after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 973be5eb67..257c6fffec 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -304,6 +304,16 @@ pub fn execute_context_object_class_name( } } +/// Resolves class/interface/trait relation metadata through eval dynamic metadata. +pub fn execute_context_class_relation( + context: &mut ElephcEvalContext, + name: &str, + target: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_class_relation_result(name, &[target], context, values) +} + /// Tests an object relation against eval dynamic-object metadata before AOT metadata. pub fn execute_context_object_is_a( context: &mut ElephcEvalContext, diff --git a/src/builtins/callables/support.rs b/src/builtins/callables/support.rs index 52655f991f..8056c26392 100644 --- a/src/builtins/callables/support.rs +++ b/src/builtins/callables/support.rs @@ -53,8 +53,11 @@ pub(crate) fn check_class_like_exists(cx: &mut BuiltinCheckCtx) -> Result Result { let first_ty = cx.checker.infer_type(&cx.args[0], cx.env)?; + let dynamic_eval_target = cx.checker.eval_barrier_active + && matches!(first_ty.codegen_repr(), PhpType::Mixed | PhpType::Str); if !matches!(first_ty, PhpType::Object(_)) && !matches!(cx.args[0].kind, ExprKind::StringLiteral(_)) + && !dynamic_eval_target { return Err(CompileError::new( cx.span, diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 3af6ce205b..9569aa44f7 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -156,6 +156,16 @@ pub(super) fn lower_eval_member_exists( eval::lower_eval_member_exists(ctx, inst, target, member, name) } +/// Lowers post-eval class-relation probes against eval dynamic metadata. +pub(super) fn lower_eval_class_relation( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + target: ValueId, + name: &str, +) -> Result<()> { + eval::lower_eval_class_relation(ctx, inst, target, name) +} + /// Lowers post-eval object class-name introspection against eval dynamic metadata. pub(super) fn lower_eval_object_class_name( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/class_relations.rs b/src/codegen/lower_inst/builtins/class_relations.rs index ef19152104..b68571bb7e 100644 --- a/src/codegen/lower_inst/builtins/class_relations.rs +++ b/src/codegen/lower_inst/builtins/class_relations.rs @@ -19,7 +19,7 @@ use crate::names::php_symbol_key; use crate::types::{ClassInfo, InterfaceInfo, PhpType}; use super::super::super::context::FunctionContext; -use super::{expect_operand, store_if_result}; +use super::{expect_operand, has_eval_context, lower_eval_class_relation, store_if_result}; enum ClassLikeTarget { Class(String), @@ -35,7 +35,12 @@ pub(crate) fn lower_class_relation( name: &str, ) -> Result<()> { super::ensure_arg_count_between(inst, name, 1, 2)?; - let target = resolve_relation_target(ctx, expect_operand(inst, 0)?)?; + let target_value = expect_operand(inst, 0)?; + if has_eval_context(ctx) { + return lower_eval_class_relation(ctx, inst, target_value, name); + } + + let target = resolve_relation_target(ctx, target_value)?; if matches!(target, ClassLikeTarget::Unknown) { emit_boxed_bool(ctx, false); return store_if_result(ctx, inst); diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 2cc2befa4f..d9ed898539 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -56,6 +56,9 @@ const EVAL_CLASS_LOOKUP_GET_CLASS: i64 = 0; const EVAL_CLASS_LOOKUP_GET_PARENT_CLASS: i64 = 1; const EVAL_MEMBER_LOOKUP_METHOD_EXISTS: i64 = 0; const EVAL_MEMBER_LOOKUP_PROPERTY_EXISTS: i64 = 1; +const EVAL_CLASS_RELATION_IMPLEMENTS: i64 = 0; +const EVAL_CLASS_RELATION_PARENTS: i64 = 1; +const EVAL_CLASS_RELATION_USES: i64 = 2; const EVAL_CALLABLE_ARG_ARRAY_OFFSET: usize = EVAL_CODE_PTR_OFFSET; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; const NATIVE_DEFAULT_NULL: i64 = 0; @@ -520,6 +523,39 @@ pub(super) fn lower_eval_member_exists( store_if_result(ctx, inst) } +/// Lowers class/interface/trait relation introspection through eval dynamic metadata. +pub(super) fn lower_eval_class_relation( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + target: ValueId, + name: &str, +) -> Result<()> { + let relation_kind = eval_class_relation_kind(name)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, target, EVAL_TEMP_CELL_OFFSET)?; + load_eval_context_to_arg(ctx, 0); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, target_arg, EVAL_TEMP_CELL_OFFSET); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + relation_kind, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_class_relation"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Lowers object class-name introspection through the eval bridge. pub(super) fn lower_eval_object_class_name( ctx: &mut FunctionContext<'_>, @@ -826,6 +862,19 @@ fn eval_member_lookup_kind(name: &str) -> Result { } } +/// Returns the eval ABI discriminator for class/interface/trait relation builtins. +fn eval_class_relation_kind(name: &str) -> Result { + match name { + "class_implements" => Ok(EVAL_CLASS_RELATION_IMPLEMENTS), + "class_parents" => Ok(EVAL_CLASS_RELATION_PARENTS), + "class_uses" => Ok(EVAL_CLASS_RELATION_USES), + _ => Err(CodegenIrError::unsupported(format!( + "eval class-relation lookup {}", + name + ))), + } +} + /// Branches when `__rt_mixed_unbox` did not expose an object payload. fn emit_branch_if_eval_unboxed_not_object(ctx: &mut FunctionContext<'_>, label: &str) { match ctx.emitter.target.arch { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 57e23aa04a..f1832be077 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19039,6 +19039,36 @@ echo function_exists("property_exists") ? "G" : "g"; assert_eq!(out, "M:C:X:P:S:Y:FG"); } +/// Verifies native class-relation probes see eval-declared metadata after the barrier. +#[test] +fn test_eval_declared_class_native_relations_after_barrier() { + let out = compile_and_run( + r#" Date: Sat, 27 Jun 2026 06:31:51 +0200 Subject: [PATCH 0830/1208] Support eval static member reads after barrier --- crates/elephc-magician/src/ffi/mod.rs | 4 + .../elephc-magician/src/ffi/static_members.rs | 151 ++++++++++++++++++ crates/elephc-magician/src/interpreter/mod.rs | 34 ++++ src/codegen/lower_inst/builtins.rs | 20 +++ src/codegen/lower_inst/builtins/eval.rs | 92 +++++++++++ src/codegen/lower_inst/scoped_constants.rs | 45 ++++-- src/codegen/lower_inst/static_properties.rs | 32 +++- src/ir_lower/expr/mod.rs | 10 +- .../checker/inference/expr/class_refs.rs | 11 ++ src/types/checker/inference/objects/access.rs | 12 +- tests/codegen/eval.rs | 22 +++ 11 files changed, 406 insertions(+), 27 deletions(-) create mode 100644 crates/elephc-magician/src/ffi/static_members.rs diff --git a/crates/elephc-magician/src/ffi/mod.rs b/crates/elephc-magician/src/ffi/mod.rs index 979c27473d..7f7e834c56 100644 --- a/crates/elephc-magician/src/ffi/mod.rs +++ b/crates/elephc-magician/src/ffi/mod.rs @@ -24,6 +24,8 @@ pub mod object_construction; #[cfg(not(test))] pub mod object_introspection; pub mod scope; +#[cfg(not(test))] +pub mod static_members; pub mod symbols; pub(crate) mod util; @@ -41,6 +43,8 @@ pub use object_construction::*; #[cfg(not(test))] pub use object_introspection::*; pub use scope::*; +#[cfg(not(test))] +pub use static_members::*; pub use symbols::*; #[cfg(test)] diff --git a/crates/elephc-magician/src/ffi/static_members.rs b/crates/elephc-magician/src/ffi/static_members.rs new file mode 100644 index 0000000000..1f966884b0 --- /dev/null +++ b/crates/elephc-magician/src/ffi/static_members.rs @@ -0,0 +1,151 @@ +//! Purpose: +//! Exports post-barrier static member reads for classes declared by eval +//! fragments. Generated code uses this ABI when native scoped-constant or +//! static-property reads target dynamic eval metadata. +//! +//! Called from: +//! - Generated EIR backend assembly through `__elephc_eval_class_constant_fetch`. +//! - Generated EIR backend assembly through `__elephc_eval_static_property_get`. +//! +//! Key details: +//! - Returned value cells are retained before crossing back to generated code. +//! - Errors and thrown PHP objects use the shared `ElephcEvalResult` contract. + +use super::util::{abi_name_to_string, clear_result, write_outcome}; +use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::errors::EvalStatus; +use crate::interpreter::{self, EvalOutcome, RuntimeValueOps}; +use crate::runtime_hooks::ElephcRuntimeOps; + +/// Fetches a class-like constant through eval dynamic metadata. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class and constant name +/// byte ranges must be readable when their lengths are non-zero, and `out` may +/// be null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_class_constant_fetch( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_class_constant_fetch_inner(ctx, class_ptr, class_len, constant_ptr, constant_len, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Reads a static property through eval dynamic metadata. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class and property name byte +/// ranges must be readable when their lengths are non-zero, and `out` may be +/// null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_static_property_get( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + property_ptr: *const u8, + property_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_static_property_get_inner(ctx, class_ptr, class_len, property_ptr, property_len, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + +/// Runs the class-constant fetch ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_class_constant_fetch`; callers must provide a valid +/// context, readable class and constant name bytes, and optional result storage. +unsafe fn eval_class_constant_fetch_inner( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + constant_ptr: *const u8, + constant_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(class_name) = abi_name_to_string(class_ptr, class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(constant_name) = abi_name_to_string(constant_ptr, constant_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_class_constant_fetch( + context, + &class_name, + &constant_name, + &mut values, + ) + .and_then(|outcome| retain_value_outcome(outcome, &mut values)) + { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Runs the static-property get ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_static_property_get`; callers must provide a valid +/// context, readable class and property name bytes, and optional result storage. +unsafe fn eval_static_property_get_inner( + ctx: *mut ElephcEvalContext, + class_ptr: *const u8, + class_len: u64, + property_ptr: *const u8, + property_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(class_name) = abi_name_to_string(class_ptr, class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(property_name) = abi_name_to_string(property_ptr, property_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_property_get( + context, + &class_name, + &property_name, + &mut values, + ) + .and_then(|outcome| retain_value_outcome(outcome, &mut values)) + { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + +/// Retains value outcomes so generated code receives an owned boxed cell. +fn retain_value_outcome( + outcome: EvalOutcome, + values: &mut impl RuntimeValueOps, +) -> Result { + match outcome { + EvalOutcome::Value(value) => values.retain(value).map(EvalOutcome::Value), + EvalOutcome::Throwable(error) => Ok(EvalOutcome::Throwable(error)), + } +} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 257c6fffec..e49d372965 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -314,6 +314,40 @@ pub fn execute_context_class_relation( eval_class_relation_result(name, &[target], context, values) } +/// Fetches a class-like constant through eval dynamic metadata and runtime fallback hooks. +pub fn execute_context_class_constant_fetch( + context: &mut ElephcEvalContext, + class_name: &str, + constant_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_class_constant_fetch_result(class_name, constant_name, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + +/// Reads a static property through eval dynamic metadata and runtime fallback hooks. +pub fn execute_context_static_property_get( + context: &mut ElephcEvalContext, + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_static_property_get_result(class_name, property_name, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + /// Tests an object relation against eval dynamic-object metadata before AOT metadata. pub fn execute_context_object_is_a( context: &mut ElephcEvalContext, diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 9569aa44f7..732f09e7b6 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -224,6 +224,26 @@ pub(super) fn lower_eval_constant_fetch( eval::lower_eval_constant_fetch(ctx, inst) } +/// Lowers post-eval class-like constant fetches to the optional eval bridge. +pub(super) fn lower_eval_class_constant_fetch( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + constant_name: &str, +) -> Result<()> { + eval::lower_eval_class_constant_fetch(ctx, inst, class_name, constant_name) +} + +/// Lowers post-eval static-property reads to the optional eval bridge. +pub(super) fn lower_eval_static_property_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + property_name: &str, +) -> Result<()> { + eval::lower_eval_static_property_get(ctx, inst, class_name, property_name) +} + /// Lowers `define("NAME", value)` with the duplicate-name runtime guard. pub(crate) fn lower_define(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "define", 2)?; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index d9ed898539..0608222eac 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -761,6 +761,98 @@ pub(super) fn lower_eval_constant_fetch( store_if_result(ctx, inst) } +/// Lowers a post-eval dynamic class-like constant fetch to the eval bridge ABI. +pub(super) fn lower_eval_class_constant_fetch( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + constant_name: &str, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_len as i64, + ); + let (constant_label, constant_len) = ctx.data.add_string(constant_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &constant_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + constant_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_class_constant_fetch"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers a post-eval dynamic static-property read to the eval bridge ABI. +pub(super) fn lower_eval_static_property_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + property_name: &str, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let (class_label, class_len) = ctx.data.add_string(class_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &class_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + class_len as i64, + ); + let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &property_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + property_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_static_property_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Returns the aligned scratch size for an eval-declared function call. fn eval_function_call_stack_bytes(arg_count: usize) -> usize { let bytes = EVAL_STACK_BYTES + arg_count * 8; diff --git a/src/codegen/lower_inst/scoped_constants.rs b/src/codegen/lower_inst/scoped_constants.rs index 7cf14b16b3..49be7bfd5e 100644 --- a/src/codegen/lower_inst/scoped_constants.rs +++ b/src/codegen/lower_inst/scoped_constants.rs @@ -14,27 +14,40 @@ use crate::ir::Instruction; use crate::names::enum_case_symbol; use super::super::context::FunctionContext; -use super::{expect_data, store_if_result}; +use super::{builtins, expect_data, store_if_result}; use crate::codegen::{CodegenIrError, Result}; /// Lowers a scoped enum-case read into the current object pointer result register. -pub(super) fn lower_scoped_constant_get(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_scoped_constant_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { let (enum_name, case_name) = scoped_constant_label(ctx, inst)?; - let enum_info = ctx - .module - .enum_infos - .get(enum_name) - .ok_or_else(|| CodegenIrError::unsupported(format!("scoped constant {}::{}", enum_name, case_name)))?; - if !enum_info.cases.iter().any(|case| case.name == case_name) { - return Err(CodegenIrError::unsupported(format!( - "scoped enum constant {}::{}", - enum_name, - case_name - ))); + let class_name = enum_name.to_string(); + let constant_name = case_name.to_string(); + if let Some(enum_info) = ctx.module.enum_infos.get(class_name.as_str()) { + if enum_info + .cases + .iter() + .any(|case| case.name == constant_name.as_str()) + { + let symbol = enum_case_symbol(&class_name, &constant_name); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + &symbol, + 0, + ); + return store_if_result(ctx, inst); + } + } + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_class_constant_fetch(ctx, inst, &class_name, &constant_name); } - let symbol = enum_case_symbol(enum_name, case_name); - abi::emit_load_symbol_to_reg(ctx.emitter, abi::int_result_reg(ctx.emitter), &symbol, 0); - store_if_result(ctx, inst) + Err(CodegenIrError::unsupported(format!( + "scoped constant {}::{}", + class_name, constant_name + ))) } /// Resolves the string immediate `Enum::Case` attached to a scoped constant read. diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index 45342f0195..fc198698ab 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -21,7 +21,7 @@ use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; use super::super::context::FunctionContext; -use super::{expect_data, expect_operand, store_if_result}; +use super::{builtins, expect_data, expect_operand, store_if_result}; use crate::codegen::{CodegenIrError, Result}; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; @@ -45,7 +45,13 @@ struct StaticPropertyBranch { } /// Lowers a direct static property read into the current result register(s). -pub(super) fn lower_load_static_property(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_load_static_property( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + if let Some((class_name, property)) = eval_dynamic_static_property_target(ctx, inst)? { + return builtins::lower_eval_static_property_get(ctx, inst, &class_name, &property); + } let slot = resolve_static_property_slot(ctx, inst, true)?; ensure_static_property_type_supported(&slot.php_type, inst)?; if slot.late_bound && !slot.branches.is_empty() { @@ -59,8 +65,28 @@ pub(super) fn lower_load_static_property(ctx: &mut FunctionContext<'_>, inst: &I store_if_result(ctx, inst) } +/// Returns an eval dynamic static-property target when no AOT class owns the receiver. +fn eval_dynamic_static_property_target( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result> { + if !builtins::has_eval_context(ctx) { + return Ok(None); + } + let label = static_property_label(ctx, inst)?; + let (receiver, property) = parse_static_property_label(label)?; + let receiver = resolve_static_property_receiver(ctx, receiver, inst)?; + if ctx.module.class_infos.contains_key(receiver.as_str()) { + return Ok(None); + } + Ok(Some((receiver, property.to_string()))) +} + /// Lowers a direct static property write from one SSA operand into symbol-backed storage. -pub(super) fn lower_store_static_property(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { +pub(super) fn lower_store_static_property( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { let value = expect_operand(inst, 0)?; let slot = resolve_static_property_slot(ctx, inst, true)?; ensure_static_property_type_supported(&slot.php_type, inst)?; diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index e15f7d00a6..872a1128b2 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -10016,20 +10016,20 @@ fn static_property_result_type( ctx: &LoweringContext<'_, '_>, receiver: &StaticReceiver, property: &str, - expr: &Expr, + _expr: &Expr, ) -> PhpType { let Some(class_name) = static_receiver_class_name(ctx, receiver) else { - return fallback_expr_type(expr); + return PhpType::Mixed; }; let Some(class_info) = ctx.classes.get(class_name.as_str()) else { - return fallback_expr_type(expr); + return PhpType::Mixed; }; let Some((_, property_ty)) = class_info .static_properties .iter() .find(|(name, _)| name == property) else { - return fallback_expr_type(expr); + return PhpType::Mixed; }; normalize_value_php_type(property_ty.codegen_repr()) } @@ -13339,7 +13339,7 @@ fn lower_scoped_constant(ctx: &mut LoweringContext<'_, '_>, receiver: &StaticRec Op::ScopedConstantGet, Vec::new(), Some(Immediate::Data(data)), - fallback_expr_type(expr), + PhpType::Mixed, Op::ScopedConstantGet.default_effects(), Some(expr.span), ) diff --git a/src/types/checker/inference/expr/class_refs.rs b/src/types/checker/inference/expr/class_refs.rs index dc95c9ce78..e11f86da3b 100644 --- a/src/types/checker/inference/expr/class_refs.rs +++ b/src/types/checker/inference/expr/class_refs.rs @@ -72,6 +72,9 @@ impl Checker { expr: &Expr, ) -> Result { let class_name = self.resolve_static_receiver_class(receiver, expr.span)?; + if !self.scoped_constant_receiver_is_known(&class_name) && self.eval_barrier_active { + return Ok(PhpType::Mixed); + } // First: enum case access (`Color::Red`). Enums shadow classes for // this syntax in PHP since 8.1. A name that is not a declared case is an enum *constant* // (`Scale::FACTOR`), which is resolved through the class-constant table below. @@ -116,6 +119,14 @@ impl Checker { )) } + /// Returns whether a scoped-constant receiver is known in static class-like metadata. + fn scoped_constant_receiver_is_known(&self, class_name: &str) -> bool { + self.classes.contains_key(class_name) + || self.interfaces.contains_key(class_name) + || self.declared_traits.contains(class_name) + || self.enums.contains_key(class_name) + } + /// Looks up a constant by name on an interface, traversing parent interfaces breadth-first /// to find it. Returns the constant's value expression if found. fn lookup_interface_constant( diff --git a/src/types/checker/inference/objects/access.rs b/src/types/checker/inference/objects/access.rs index 51367821f4..0db858d2a3 100644 --- a/src/types/checker/inference/objects/access.rs +++ b/src/types/checker/inference/objects/access.rs @@ -421,9 +421,15 @@ impl Checker { expr: &Expr, ) -> Result { let class_name = self.resolve_static_property_receiver(receiver, expr)?; - let class_info = self.classes.get(&class_name).ok_or_else(|| { - CompileError::new(expr.span, &format!("Undefined class: {}", class_name)) - })?; + let Some(class_info) = self.classes.get(&class_name) else { + if self.eval_barrier_active && matches!(receiver, StaticReceiver::Named(_)) { + return Ok(PhpType::Mixed); + } + return Err(CompileError::new( + expr.span, + &format!("Undefined class: {}", class_name), + )); + }; if let Some(visibility) = class_info.static_property_visibilities.get(property) { let declaring_class = class_info .static_property_declaring_classes diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f1832be077..b83e92d204 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19069,6 +19069,28 @@ echo class_implements("MissingDynEvalNativeRel") === false ? "F" : "f"; ); } +/// Verifies native static member reads see eval-declared metadata after the barrier. +#[test] +fn test_eval_declared_class_native_static_members_after_barrier() { + let out = compile_and_run( + r#" Date: Sat, 27 Jun 2026 06:52:21 +0200 Subject: [PATCH 0831/1208] Support eval static method calls after barrier --- .../src/ffi/object_construction.rs | 71 +++++++++++++++++++ crates/elephc-magician/src/interpreter/mod.rs | 26 +++++++ src/codegen/lower_inst.rs | 30 ++++++-- src/codegen/lower_inst/builtins.rs | 10 +++ src/codegen/lower_inst/builtins/eval.rs | 64 +++++++++++++++++ src/ir/instr.rs | 5 +- src/ir/validator.rs | 2 + src/ir_lower/context.rs | 1 + src/ir_lower/expr/mod.rs | 28 +++++++- src/ir_lower/program.rs | 3 +- .../checker/inference/objects/methods.rs | 5 ++ tests/codegen/eval.rs | 18 +++++ 12 files changed, 253 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/ffi/object_construction.rs b/crates/elephc-magician/src/ffi/object_construction.rs index fdb31f8ae1..55cfa12956 100644 --- a/crates/elephc-magician/src/ffi/object_construction.rs +++ b/crates/elephc-magician/src/ffi/object_construction.rs @@ -6,6 +6,7 @@ //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_new_object`. //! - Generated EIR backend assembly through `__elephc_eval_method_call`. +//! - Generated EIR backend assembly through `__elephc_eval_static_method_call`. //! //! Key details: //! - The ABI currently accepts positional arguments already boxed as Mixed cells. @@ -65,6 +66,28 @@ pub unsafe extern "C" fn __elephc_eval_method_call( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Calls a static method on a class previously declared through `eval()`. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `target_ptr` must be readable +/// for `target_len` bytes and contain `ClassName::method`. `arg_pack` points at +/// a native-word count followed by that many runtime-cell pointers, and `out` +/// may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_static_method_call( + ctx: *mut ElephcEvalContext, + target_ptr: *const u8, + target_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_static_method_call_inner(ctx, target_ptr, target_len, arg_pack, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Runs the dynamic object-construction ABI body after installing a panic boundary. /// /// # Safety @@ -110,6 +133,54 @@ unsafe fn eval_new_object_inner( } } +/// Runs the dynamic static-method call ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_static_method_call`; callers must provide a valid +/// context, readable `ClassName::method` bytes, and a readable argument pack. +#[cfg(not(test))] +unsafe fn eval_static_method_call_inner( + ctx: *mut ElephcEvalContext, + target_ptr: *const u8, + target_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if arg_pack.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(target) = abi_name_to_string(target_ptr, target_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Some((class_name, method)) = target.rsplit_once("::") else { + return EvalStatus::RuntimeFatal.code(); + }; + let arg_count = *arg_pack; + let arg_ptrs = arg_pack.add(1) as *const *mut RuntimeCell; + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(arg_ptrs, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_method_call_outcome( + context, class_name, method, args, &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + /// Runs the dynamic method-call ABI body after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index e49d372965..b91c0e22f4 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -290,6 +290,32 @@ pub fn execute_context_method_call_outcome( } } +/// Calls a static method on a class-like symbol known to the shared eval context. +pub fn execute_context_static_method_call_outcome( + context: &mut ElephcEvalContext, + class_name: &str, + method: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = args + .into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + match eval_static_method_call_result(class_name, method, evaluated_args, context, values) { + Ok(result) => Ok(EvalOutcome::Value(result)), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + /// Resolves object class-name builtins against eval dynamic-object metadata first. pub fn execute_context_object_class_name( context: &mut ElephcEvalContext, diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index aecf19fc52..1438ebc1bf 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -207,6 +207,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::MethodCall => lower_method_call(ctx, &inst), Op::NullsafeMethodCall => lower_nullsafe_method_call(ctx, &inst), Op::StaticMethodCall => lower_static_method_call(ctx, &inst), + Op::EvalStaticMethodCall => lower_eval_static_method_call(ctx, &inst), Op::EnumBackingStringToInt => enums::lower_enum_backing_string_to_int(ctx, &inst), Op::EnumBackingMixedToInt => enums::lower_enum_backing_mixed_to_int(ctx, &inst), Op::ExternCall => externs::lower_extern_call(ctx, &inst), @@ -4563,13 +4564,20 @@ fn lower_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) - ); } let late_bound_static = is_late_bound_static_receiver(receiver_label); - let receiver_info = ctx - .module - .class_infos - .get(receiver.as_str()) - .ok_or_else(|| { - CodegenIrError::unsupported(format!("static method call on unknown class {}", receiver)) - })?; + let Some(receiver_info) = ctx.module.class_infos.get(receiver.as_str()) else { + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_static_method_call( + ctx, + inst, + receiver.as_str(), + method_name, + ); + } + return Err(CodegenIrError::unsupported(format!( + "static method call on unknown class {}", + receiver + ))); + }; let method_key = php_symbol_key(method_name); let impl_class = receiver_info .static_method_impl_classes @@ -4645,6 +4653,14 @@ fn lower_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) - emit_ref_arg_writebacks(ctx, &call_args.ref_writebacks) } +/// Lowers a direct static-method call against a class declared by a previous eval fragment. +fn lower_eval_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let target = method_name_data(ctx, inst)?.to_string(); + let (receiver_label, method_name) = parse_static_method_target(&target)?; + let receiver = resolve_static_method_receiver(ctx, receiver_label)?; + builtins::lower_eval_static_method_call(ctx, inst, receiver.as_str(), method_name) +} + /// Lowers `self::method()` or `parent::method()` when it targets an instance method. fn lower_lexical_instance_static_method_call( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 732f09e7b6..ddbe2b712b 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -126,6 +126,16 @@ pub(super) fn lower_eval_method_call( eval::lower_eval_method_call(ctx, inst, object, method_name) } +/// Lowers a post-eval static method call to an eval-declared class. +pub(super) fn lower_eval_static_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + method_name: &str, +) -> Result<()> { + eval::lower_eval_static_method_call(ctx, inst, class_name, method_name) +} + /// Lowers post-eval callable-array dispatch against eval dynamic callables. pub(super) fn lower_eval_callable_call_array( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 0608222eac..3e877541f5 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -444,6 +444,44 @@ pub(super) fn lower_eval_method_call( store_if_result(ctx, inst) } +/// Lowers a native static-method call to an eval-declared dynamic class. +pub(super) fn lower_eval_static_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + class_name: &str, + method_name: &str, +) -> Result<()> { + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_static_method_call_stack_bytes(inst.operands.len()); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_context(ctx)?; + store_eval_static_method_call_arg_pack(ctx, inst, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let target = format!("{}::{}", class_name, method_name); + let (target_label, target_len) = ctx.data.add_string(target.as_bytes()); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, target_arg, &target_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + target_len as i64, + ); + let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_static_method_call"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + /// Lowers a callable-array dispatch through the eval bridge. pub(super) fn lower_eval_callable_call_array( ctx: &mut FunctionContext<'_>, @@ -865,6 +903,12 @@ fn eval_method_call_stack_bytes(arg_count: usize) -> usize { (bytes + 15) & !15 } +/// Returns the aligned scratch size for an eval dynamic static-method call. +fn eval_static_method_call_stack_bytes(arg_count: usize) -> usize { + let bytes = EVAL_STACK_BYTES + 8 + arg_count * 8; + (bytes + 15) & !15 +} + /// Stores positional operands as boxed Mixed cells for the eval function-call ABI. fn store_eval_function_call_args( ctx: &mut FunctionContext<'_>, @@ -903,6 +947,26 @@ fn store_eval_method_call_arg_pack( Ok(()) } +/// Stores all positional operands as a count-prefixed static-method argument pack. +fn store_eval_static_method_call_arg_pack( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + args_offset: usize, +) -> Result<()> { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, result_reg, inst.operands.len() as i64); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset); + for (index, operand) in inst.operands.iter().enumerate() { + let ty = ctx.load_value_to_result(*operand)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, args_offset + 8 + index * 8); + } + Ok(()) +} + /// Stores an object operand as a boxed Mixed cell in eval scratch storage. fn store_eval_object_operand(ctx: &mut FunctionContext<'_>, object: ValueId) -> Result<()> { store_eval_mixed_operand_at(ctx, object, EVAL_TEMP_CELL_OFFSET) diff --git a/src/ir/instr.rs b/src/ir/instr.rs index b1b40c34fe..edeff9097c 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -341,6 +341,7 @@ pub enum Op { MethodLookup, MethodCall, StaticMethodCall, + EvalStaticMethodCall, /// Coerces a PHP numeric string operand to its integer value for an int-backed enum /// `from()`/`tryFrom()` call. Operand: the string. Immediate: data id of the PHP /// `TypeError` message thrown when the string is not numeric. Result: `I64`. @@ -513,7 +514,7 @@ impl Op { E::READS_GLOBAL | E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL } Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | EvalFunctionCallArray - | EvalObjectNew | RuntimeCall + | EvalObjectNew | EvalStaticMethodCall | RuntimeCall | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { E::all().difference(E::REFCOUNT_OP) } @@ -540,6 +541,7 @@ impl Op { | Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalObjectNew + | Op::EvalStaticMethodCall | Op::RuntimeCall | Op::ExternCall | Op::MethodCall @@ -712,6 +714,7 @@ impl Op { MethodLookup => "method_lookup", MethodCall => "method_call", StaticMethodCall => "static_method_call", + EvalStaticMethodCall => "eval_static_method_call", EnumBackingStringToInt => "enum_backing_string_to_int", EnumBackingMixedToInt => "enum_backing_mixed_to_int", ClassConstant => "class_constant", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index 487223e93a..cb0fae88f0 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -275,6 +275,7 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionCallArray | EvalFunctionExists | EvalClassExists | EvalConstantExists | EvalConstantFetch + | EvalStaticMethodCall | EnumBackingStringToInt | EnumBackingMixedToInt | PropInitialized @@ -365,6 +366,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio ClosureNew => Ok(()), FirstClassCallableNew => check_count_at_most(inst_id, inst, 1, "0 or 1"), ObjectNew => Ok(()), + EvalStaticMethodCall => Ok(()), IAdd | ISub | IMul | IDiv | ISDiv | ISMod | IPow | IBitAnd | IBitOr | IBitXor | IShl | IShrA => check_binary(function, inst_id, inst, IrType::I64, "I64"), ICheckedAdd | ICheckedSub | ICheckedMul => { diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index d3e0391f9d..2d7ad795d1 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1210,6 +1210,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalConstantFetch + | Op::EvalStaticMethodCall | Op::RuntimeCall | Op::ExternCall | Op::MethodCall diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 872a1128b2..d9d474dce2 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -13015,6 +13015,26 @@ fn lower_static_method_call( } else { (method, args) }; + if ctx.has_eval_barrier() + && matches!(receiver, StaticReceiver::Named(_)) + && plain_positional_call_args(args) + { + if let Some(class_name) = static_receiver_class_name(ctx, receiver) { + if !ctx.classes.contains_key(class_name.as_str()) { + let operands = lower_args_with_signature(ctx, None, args); + let name = format!("{}::{}", class_name, dispatch_method); + let data = ctx.intern_string(&name); + return ctx.emit_value( + Op::EvalStaticMethodCall, + operands, + Some(Immediate::Data(data)), + PhpType::Mixed, + Op::EvalStaticMethodCall.default_effects(), + Some(expr.span), + ); + } + } + } let sig = static_method_implementation_signature(ctx, receiver, dispatch_method) .or_else(|| lexical_instance_static_call_signature(ctx, receiver, dispatch_method)) .cloned(); @@ -13026,7 +13046,13 @@ fn lower_static_method_call( let result_type = sig .as_ref() .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) - .unwrap_or_else(|| fallback_expr_type(expr)); + .unwrap_or_else(|| { + if ctx.has_eval_barrier() && matches!(receiver, StaticReceiver::Named(_)) { + PhpType::Mixed + } else { + fallback_expr_type(expr) + } + }); ctx.emit_value( Op::StaticMethodCall, operands, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 855a8aa049..9252ba4cd5 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -348,7 +348,8 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { | Op::EvalFunctionExists | Op::EvalClassExists | Op::EvalConstantExists - | Op::EvalConstantFetch => { + | Op::EvalConstantFetch + | Op::EvalStaticMethodCall => { features.eval = true; } Op::ExprCall | Op::CallableDescriptorInvoke => { diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index 175082626f..e1ef521d49 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -920,6 +920,11 @@ impl Checker { &format!("Undefined method: {}::{}", class_name, method), )); } + } else if self.eval_barrier_active && matches!(receiver, StaticReceiver::Named(_)) { + for arg in args { + self.infer_type(arg, env)?; + } + return Ok(PhpType::Mixed); } else { return Err(CompileError::new( expr.span, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b83e92d204..c2fea051a6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19091,6 +19091,24 @@ echo DynEvalNativeStaticChild::$count; assert_eq!(out, "4:3:6:5"); } +/// Verifies native static method calls can dispatch to eval-declared classes after the barrier. +#[test] +fn test_eval_declared_class_native_static_method_call_after_barrier() { + let out = compile_and_run( + r#" Date: Sat, 27 Jun 2026 07:05:06 +0200 Subject: [PATCH 0832/1208] Support eval static property writes after barrier --- .../elephc-magician/src/ffi/static_members.rs | 80 ++++++++++++++++++- crates/elephc-magician/src/interpreter/mod.rs | 19 +++++ src/codegen/lower_inst/builtins.rs | 11 +++ src/codegen/lower_inst/builtins/eval.rs | 38 +++++++++ src/codegen/lower_inst/static_properties.rs | 9 +++ .../assignments/static_properties.rs | 12 +++ tests/codegen/eval.rs | 20 +++++ 7 files changed, 187 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/ffi/static_members.rs b/crates/elephc-magician/src/ffi/static_members.rs index 1f966884b0..914b669242 100644 --- a/crates/elephc-magician/src/ffi/static_members.rs +++ b/crates/elephc-magician/src/ffi/static_members.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Exports post-barrier static member reads for classes declared by eval +//! Exports post-barrier static member operations for classes declared by eval //! fragments. Generated code uses this ABI when native scoped-constant or -//! static-property reads target dynamic eval metadata. +//! static-property access targets dynamic eval metadata. //! //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_class_constant_fetch`. //! - Generated EIR backend assembly through `__elephc_eval_static_property_get`. +//! - Generated EIR backend assembly through `__elephc_eval_static_property_set`. //! //! Key details: //! - Returned value cells are retained before crossing back to generated code. @@ -16,6 +17,7 @@ use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; use crate::errors::EvalStatus; use crate::interpreter::{self, EvalOutcome, RuntimeValueOps}; use crate::runtime_hooks::ElephcRuntimeOps; +use crate::value::{RuntimeCell, RuntimeCellHandle}; /// Fetches a class-like constant through eval dynamic metadata. /// @@ -59,6 +61,26 @@ pub unsafe extern "C" fn __elephc_eval_static_property_get( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Writes a static property through eval dynamic metadata. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The target byte range must be a +/// readable `Class::property` name when non-empty, `value` must be a valid +/// runtime cell, and `out` may be null. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_static_property_set( + ctx: *mut ElephcEvalContext, + target_ptr: *const u8, + target_len: u64, + value: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_static_property_set_inner(ctx, target_ptr, target_len, value, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Runs the class-constant fetch ABI body after installing a panic boundary. /// /// # Safety @@ -139,6 +161,60 @@ unsafe fn eval_static_property_get_inner( } } +/// Runs the static-property set ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_static_property_set`; callers must provide a valid +/// context, readable `Class::property` target bytes, a valid value cell, and +/// optional result storage for thrown PHP objects. +unsafe fn eval_static_property_set_inner( + ctx: *mut ElephcEvalContext, + target_ptr: *const u8, + target_len: u64, + value: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + if value.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(target_name) = abi_name_to_string(target_ptr, target_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok((class_name, property_name)) = split_static_property_target(&target_name) else { + return EvalStatus::RuntimeFatal.code(); + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_property_set( + context, + class_name, + property_name, + RuntimeCellHandle::from_raw(value), + &mut values, + ) { + Ok(Some(outcome)) => write_outcome(outcome, out).code(), + Ok(None) => EvalStatus::Ok.code(), + Err(status) => status.code(), + } +} + +/// Splits the packed static-property target used by the compact setter ABI. +fn split_static_property_target(target: &str) -> Result<(&str, &str), EvalStatus> { + let Some((class_name, property_name)) = target.rsplit_once("::") else { + return Err(EvalStatus::RuntimeFatal); + }; + if class_name.is_empty() || property_name.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + Ok((class_name, property_name)) +} + /// Retains value outcomes so generated code receives an owned boxed cell. fn retain_value_outcome( outcome: EvalOutcome, diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index b91c0e22f4..cacb9c505a 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -374,6 +374,25 @@ pub fn execute_context_static_property_get( } } +/// Writes a static property through eval dynamic metadata and runtime fallback hooks. +pub fn execute_context_static_property_set( + context: &mut ElephcEvalContext, + class_name: &str, + property_name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match eval_static_property_set_result(class_name, property_name, value, context, values) { + Ok(()) => Ok(None), + Err(EvalStatus::UncaughtThrowable) => context + .take_pending_throw() + .map(EvalOutcome::Throwable) + .map(Some) + .ok_or(EvalStatus::UncaughtThrowable), + Err(status) => Err(status), + } +} + /// Tests an object relation against eval dynamic-object metadata before AOT metadata. pub fn execute_context_object_is_a( context: &mut ElephcEvalContext, diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index ddbe2b712b..e419c6445f 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -254,6 +254,17 @@ pub(super) fn lower_eval_static_property_get( eval::lower_eval_static_property_get(ctx, inst, class_name, property_name) } +/// Lowers post-eval static-property writes to the optional eval bridge. +pub(super) fn lower_eval_static_property_set( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + value: ValueId, + class_name: &str, + property_name: &str, +) -> Result<()> { + eval::lower_eval_static_property_set(ctx, inst, value, class_name, property_name) +} + /// Lowers `define("NAME", value)` with the duplicate-name runtime guard. pub(crate) fn lower_define(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { ensure_arg_count(inst, "define", 2)?; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 3e877541f5..434a44ec1d 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -891,6 +891,44 @@ pub(super) fn lower_eval_static_property_get( store_if_result(ctx, inst) } +/// Lowers a post-eval dynamic static-property write to the eval bridge ABI. +pub(super) fn lower_eval_static_property_set( + ctx: &mut FunctionContext<'_>, + _inst: &Instruction, + value: ValueId, + class_name: &str, + property_name: &str, +) -> Result<()> { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_eval_mixed_operand_at(ctx, value, EVAL_TEMP_CELL_OFFSET)?; + ensure_eval_context(ctx)?; + load_eval_context_to_arg(ctx, 0); + let target = format!("{}::{}", class_name, property_name); + let (target_label, target_len) = ctx.data.add_string(target.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &target_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + target_len as i64, + ); + let value_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_load_temporary_stack_slot(ctx.emitter, value_arg, EVAL_TEMP_CELL_OFFSET); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_static_property_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + Ok(()) +} + /// Returns the aligned scratch size for an eval-declared function call. fn eval_function_call_stack_bytes(arg_count: usize) -> usize { let bytes = EVAL_STACK_BYTES + arg_count * 8; diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index fc198698ab..80269d1363 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -88,6 +88,15 @@ pub(super) fn lower_store_static_property( inst: &Instruction, ) -> Result<()> { let value = expect_operand(inst, 0)?; + if let Some((class_name, property)) = eval_dynamic_static_property_target(ctx, inst)? { + return builtins::lower_eval_static_property_set( + ctx, + inst, + value, + &class_name, + &property, + ); + } let slot = resolve_static_property_slot(ctx, inst, true)?; ensure_static_property_type_supported(&slot.php_type, inst)?; let value_ty = ctx.value_php_type(value)?; diff --git a/src/types/checker/stmt_check/assignments/static_properties.rs b/src/types/checker/stmt_check/assignments/static_properties.rs index 0ea6566129..491fe708ed 100644 --- a/src/types/checker/stmt_check/assignments/static_properties.rs +++ b/src/types/checker/stmt_check/assignments/static_properties.rs @@ -231,6 +231,18 @@ fn resolve_static_property_assignment_target( } }; + if checker.eval_barrier_active + && matches!(receiver, StaticReceiver::Named(_)) + && !checker.classes.contains_key(&class_name) + { + return Ok(StaticPropertyAssignmentTarget { + class_name: class_name.clone(), + declaring_class: class_name, + property_has_declared_type: false, + prop_ty: PhpType::Mixed, + }); + } + let class_info = checker .classes .get(&class_name) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c2fea051a6..6676f22cc7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19109,6 +19109,26 @@ echo DynEvalNativeStaticCallChild::base(4); assert_eq!(out, "14:14"); } +/// Verifies native static property writes can update eval-declared classes after the barrier. +#[test] +fn test_eval_declared_class_native_static_property_write_after_barrier() { + let out = compile_and_run( + r#" Date: Sat, 27 Jun 2026 07:08:37 +0200 Subject: [PATCH 0833/1208] Cover eval object property writes after barrier --- tests/codegen/eval.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6676f22cc7..5914789fe9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18960,6 +18960,25 @@ echo $box->x; assert_eq!(out, "9:10:12:12"); } +/// Verifies native property writes can update eval-created objects after the barrier. +#[test] +fn test_eval_declared_class_native_property_write_after_barrier() { + let out = compile_and_run( + r#"x = 8; +$box->label = "new"; +echo $box->label . ":"; +echo $box->x; +"#, + ); + assert_eq!(out, "new:8"); +} + /// Verifies native introspection sees eval-declared object metadata after the barrier. #[test] fn test_eval_declared_class_native_introspection_after_barrier() { From 358ade49b3310df7301fbc3e20afd1be2830db8a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 07:16:18 +0200 Subject: [PATCH 0834/1208] Support eval static property array writes after barrier --- src/ir_lower/stmt/mod.rs | 36 +++++++++++++++++++ .../assignments/static_properties.rs | 20 ++++++++++- tests/codegen/eval.rs | 22 ++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index 3a61365e7e..60c8a956f9 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -2476,6 +2476,17 @@ fn lower_static_property_array_push( let property_value = load_static_property(ctx, receiver, property, span); let value = lower_expr(ctx, value); + if static_property_may_be_eval_dynamic(ctx, receiver) { + ctx.emit_void( + Op::MixedArrayAppend, + vec![property_value.value, value.value], + None, + Op::MixedArrayAppend.default_effects(), + Some(span), + ); + store_static_property(ctx, receiver, property, property_value.value, span); + return; + } ctx.emit_void( Op::RuntimeCall, vec![property_value.value, value.value], @@ -2523,6 +2534,17 @@ fn lower_static_property_array_assign( }; let index = lower_expr(ctx, index); let value = lower_expr(ctx, value); + if static_property_may_be_eval_dynamic(ctx, receiver) { + ctx.emit_void( + Op::RuntimeCall, + vec![property_value.value, index.value, value.value], + None, + effects_lookup::runtime_effects(), + Some(span), + ); + store_static_property(ctx, receiver, property, property_value.value, span); + return; + } ctx.emit_void( Op::RuntimeCall, vec![property_value.value, index.value, value.value], @@ -2532,6 +2554,20 @@ fn lower_static_property_array_assign( ); } +/// Returns true when a named static-property receiver may resolve through eval metadata. +fn static_property_may_be_eval_dynamic( + ctx: &LoweringContext<'_, '_>, + receiver: &StaticReceiver, +) -> bool { + let StaticReceiver::Named(class_name) = receiver else { + return false; + }; + ctx.has_eval_barrier() + && !ctx + .classes + .contains_key(class_name.as_str().trim_start_matches('\\')) +} + /// Lowers `$object->prop[] = value`. fn lower_property_array_push( ctx: &mut LoweringContext<'_, '_>, diff --git a/src/types/checker/stmt_check/assignments/static_properties.rs b/src/types/checker/stmt_check/assignments/static_properties.rs index 491fe708ed..5928d2002e 100644 --- a/src/types/checker/stmt_check/assignments/static_properties.rs +++ b/src/types/checker/stmt_check/assignments/static_properties.rs @@ -11,7 +11,7 @@ use crate::errors::CompileError; use crate::parser::ast::{Expr, StaticReceiver}; use crate::span::Span; -use crate::types::{PhpType, TypeEnv}; +use crate::types::{normalized_array_key_type, PhpType, TypeEnv}; use super::super::super::Checker; @@ -22,6 +22,7 @@ struct StaticPropertyAssignmentTarget { declaring_class: String, property_has_declared_type: bool, prop_ty: PhpType, + dynamic_eval_target: bool, } /// Type-checks a direct static property assignment `Class::$prop = value`. @@ -75,6 +76,9 @@ pub(super) fn check_static_property_array_push( ) -> Result<(), CompileError> { let val_ty = checker.infer_type_with_assignment_effects(value, env)?; let target = resolve_static_property_assignment_target(checker, receiver, property, span)?; + if target.dynamic_eval_target { + return Ok(()); + } let updated_prop_ty = match target.prop_ty { PhpType::Array(elem_ty) => { if target.property_has_declared_type { @@ -139,6 +143,13 @@ pub(super) fn check_static_property_array_assign( let idx_ty = checker.infer_type_with_assignment_effects(index, env)?; let val_ty = checker.infer_type_with_assignment_effects(value, env)?; let target = resolve_static_property_assignment_target(checker, receiver, property, span)?; + if target.dynamic_eval_target { + let normalized_idx_ty = normalized_array_key_type(index, idx_ty); + if !is_dynamic_eval_static_property_array_key_type(&normalized_idx_ty) { + return Err(CompileError::new(span, "Array index must be integer")); + } + return Ok(()); + } if let PhpType::Object(class_name) = &target.prop_ty { if checker.object_type_implements_interface(class_name, "ArrayAccess") { return Ok(()); @@ -240,6 +251,7 @@ fn resolve_static_property_assignment_target( declaring_class: class_name, property_has_declared_type: false, prop_ty: PhpType::Mixed, + dynamic_eval_target: true, }); } @@ -293,9 +305,15 @@ fn resolve_static_property_assignment_target( declaring_class, property_has_declared_type, prop_ty, + dynamic_eval_target: false, }) } +/// Returns true when a dynamic eval static-property key can be handled by runtime array writes. +fn is_dynamic_eval_static_property_array_key_type(ty: &PhpType) -> bool { + matches!(ty, PhpType::Int | PhpType::Str | PhpType::Mixed) +} + /// Updates the type of a static property in all classes that declare it. /// /// Iterates over all classes and mutates the type entry for `property` on the diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5914789fe9..5ae88e5928 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19148,6 +19148,28 @@ echo DynEvalNativeStaticWriteChild::$count; assert_eq!(out, "new:7"); } +/// Verifies native static property array writes update eval-declared classes after the barrier. +#[test] +fn test_eval_declared_class_native_static_property_array_write_after_barrier() { + let out = compile_and_run( + r#" "old"]; +}'); +DynEvalNativeStaticArrayWrite::$items[] = 4; +DynEvalNativeStaticArrayWrite::$items[0] = 5; +DynEvalNativeStaticArrayWrite::$map["a"] = "new"; +DynEvalNativeStaticArrayWrite::$map["b"] = "bee"; +echo DynEvalNativeStaticArrayWrite::$items[0] . ":"; +echo DynEvalNativeStaticArrayWrite::$items[1] . ":"; +echo DynEvalNativeStaticArrayWrite::$map["a"] . ":"; +echo DynEvalNativeStaticArrayWrite::$map["b"]; +"#, + ); + assert_eq!(out, "5:4:new:bee"); +} + /// Verifies eval class declarations from a namespace are registered globally. #[test] fn test_eval_declared_class_in_namespace_is_global() { From 98c765e13078d499c12a045cb2353e73a0bec266 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 07:20:56 +0200 Subject: [PATCH 0835/1208] Cover eval static property compound writes after barrier --- tests/codegen/eval.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5ae88e5928..67b7689d7c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19141,11 +19141,12 @@ class DynEvalNativeStaticWriteChild extends DynEvalNativeStaticWriteBase { }'); DynEvalNativeStaticWriteChild::$label = "new"; DynEvalNativeStaticWriteChild::$count = 7; +DynEvalNativeStaticWriteChild::$count += 2; echo DynEvalNativeStaticWriteChild::$label . ":"; echo DynEvalNativeStaticWriteChild::$count; "#, ); - assert_eq!(out, "new:7"); + assert_eq!(out, "new:9"); } /// Verifies native static property array writes update eval-declared classes after the barrier. From 9d147dc3337a3997705e431a094787b9c150292e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 07:29:32 +0200 Subject: [PATCH 0836/1208] Support eval instanceof after barrier --- src/codegen/lower_inst/objects.rs | 10 +++++++--- tests/codegen/eval.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 5fb5c6ad79..eed994cdf1 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -29,7 +29,8 @@ use crate::types::{ClassInfo, InterfaceInfo, PhpType}; use super::super::context::FunctionContext; use super::{ - callables, cast_loaded_mixed_pointer_to_result, direct_call_stack_pad_bytes, expect_data, + builtins, callables, cast_loaded_mixed_pointer_to_result, direct_call_stack_pad_bytes, + expect_data, coerce_loaded_value_to_tagged_scalar, emit_loaded_assoc_array_to_mixed, emit_loaded_indexed_array_to_mixed, emit_mixed_string_for_persistent_store, emit_ref_arg_writebacks, expect_operand, iterators, load_value_to_first_int_arg, @@ -4065,8 +4066,11 @@ pub(super) fn lower_instanceof(ctx: &mut FunctionContext<'_>, inst: &Instruction emit_false(ctx); return store_if_result(ctx, inst); } - let class_name = class_name_immediate(ctx, inst)?; - let Some((target_id, target_kind)) = classify_named_target(ctx, class_name) else { + let class_name = class_name_immediate(ctx, inst)?.to_string(); + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_object_is_a(ctx, inst, value, &class_name, false); + } + let Some((target_id, target_kind)) = classify_named_target(ctx, &class_name) else { emit_false(ctx); return store_if_result(ctx, inst); }; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 67b7689d7c..0b110342ab 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19001,6 +19001,35 @@ echo is_subclass_of($box, "DynEvalNativeIntrospectBase") ? "P" : "p"; assert_eq!(out, "DynEvalNativeIntrospectChild:DynEvalNativeIntrospectBase:C:B:s:P"); } +/// Verifies native `instanceof` sees eval-declared class metadata after the barrier. +#[test] +fn test_eval_declared_class_native_instanceof_after_barrier() { + let out = compile_and_run( + r#" Date: Sat, 27 Jun 2026 07:51:26 +0200 Subject: [PATCH 0837/1208] Support eval dynamic instanceof after barrier --- .../src/ffi/object_introspection.rs | 55 +++++++++- crates/elephc-magician/src/interpreter/mod.rs | 31 ++++++ src/codegen/lower_inst/builtins.rs | 11 ++ src/codegen/lower_inst/builtins/eval.rs | 102 ++++++++++++++++++ src/codegen/lower_inst/objects.rs | 3 + tests/codegen/eval.rs | 42 +++++++- 6 files changed, 242 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/ffi/object_introspection.rs b/crates/elephc-magician/src/ffi/object_introspection.rs index 71f10848ff..02cb1e83ea 100644 --- a/crates/elephc-magician/src/ffi/object_introspection.rs +++ b/crates/elephc-magician/src/ffi/object_introspection.rs @@ -6,12 +6,14 @@ //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_object_class_name`. //! - Generated EIR backend assembly through `__elephc_eval_object_is_a`. +//! - Generated EIR backend assembly through `__elephc_eval_object_is_a_dynamic`. //! - Generated EIR backend assembly through `__elephc_eval_member_exists`. //! - Generated EIR backend assembly through `__elephc_eval_class_relation`. //! //! Key details: //! - Class-name and relation lookups return boxed Mixed cells through `ElephcEvalResult`. -//! - Predicate probes fail closed as false when ABI inputs are invalid. +//! - Named predicate probes fail closed as false; dynamic `instanceof` invalid +//! targets return -1 so generated code can raise PHP's fatal diagnostic. use super::util::{abi_name_to_string, clear_result, write_outcome}; use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; @@ -69,6 +71,26 @@ pub unsafe extern "C" fn __elephc_eval_object_is_a( .unwrap_or(0) } +/// Tests whether an object satisfies a runtime string/object class target. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `object` and `target` must point +/// at boxed runtime cells. Returns -1 when the target is invalid for +/// `instanceof`. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_object_is_a_dynamic( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + target: *mut RuntimeCell, + exclude_self: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_object_is_a_dynamic_inner(ctx, object, target, exclude_self) + }) + .unwrap_or(-1) +} + /// Tests whether a method or property exists in the eval context. /// /// # Safety @@ -222,6 +244,37 @@ unsafe fn eval_object_is_a_inner( } } +/// Runs the eval dynamic object relation ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_object_is_a_dynamic`; invalid target cells report -1 +/// so generated code can use the normal dynamic-`instanceof` fatal path. +#[cfg(not(test))] +unsafe fn eval_object_is_a_dynamic_inner( + ctx: *mut ElephcEvalContext, + object: *mut RuntimeCell, + target: *mut RuntimeCell, + exclude_self: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return -1; + }; + if context.abi_version() != ABI_VERSION || object.is_null() || target.is_null() { + return -1; + } + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_object_is_a_dynamic( + context, + RuntimeCellHandle::from_raw(object), + RuntimeCellHandle::from_raw(target), + exclude_self != 0, + &mut values, + ) { + Ok(result) => i32::from(result), + Err(_) => -1, + } +} + /// Runs the eval member-exists ABI body after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index cacb9c505a..a48c21e944 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -421,6 +421,37 @@ pub fn execute_context_object_is_a( ) } +/// Tests an object relation when the target is a runtime string or object cell. +pub fn execute_context_object_is_a_dynamic( + context: &mut ElephcEvalContext, + object: RuntimeCellHandle, + target: RuntimeCellHandle, + exclude_self: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let target_class = match values.type_tag(target)? { + EVAL_TAG_STRING => { + let bytes = values.string_bytes(target)?; + let target = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + target.trim_start_matches('\\').to_string() + } + EVAL_TAG_OBJECT => { + let identity = values.object_identity(target)?; + if let Some(class) = context.dynamic_object_class(identity) { + class.name().to_string() + } else { + let class_name = values.object_class_name(target)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + class_name.trim_start_matches('\\').to_string() + } + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + execute_context_object_is_a(context, object, &target_class, exclude_self, values) +} + /// Tests whether a method or property exists through eval dynamic metadata. pub fn execute_context_member_exists( context: &mut ElephcEvalContext, diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index e419c6445f..0f4007d0d0 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -197,6 +197,17 @@ pub(super) fn lower_eval_object_is_a( eval::lower_eval_object_is_a(ctx, inst, object, target_class, exclude_self) } +/// Lowers post-eval object/class relation predicates with runtime target cells. +pub(super) fn lower_eval_object_is_a_dynamic( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + target: ValueId, + exclude_self: bool, +) -> Result<()> { + eval::lower_eval_object_is_a_dynamic(ctx, inst, object, target, exclude_self) +} + /// Returns true when this lowered function has a persistent eval context local. pub(super) fn has_eval_context(ctx: &FunctionContext<'_>) -> bool { eval::has_eval_context(ctx) diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 434a44ec1d..4d1ae33863 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -683,6 +683,66 @@ pub(super) fn lower_eval_object_is_a( store_if_result(ctx, inst) } +/// Lowers object/class relation predicates whose target is a runtime string or object cell. +pub(super) fn lower_eval_object_is_a_dynamic( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + object: ValueId, + target: ValueId, + exclude_self: bool, +) -> Result<()> { + let false_label = ctx.next_label("eval_object_is_a_dynamic_false"); + let invalid_label = ctx.next_label("eval_object_is_a_dynamic_invalid"); + let done_label = ctx.next_label("eval_object_is_a_dynamic_done"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_context(ctx)?; + store_eval_mixed_operand_at(ctx, object, EVAL_TEMP_CELL_OFFSET)?; + store_eval_mixed_operand_at(ctx, target, EVAL_CODE_PTR_OFFSET)?; + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + EVAL_CODE_PTR_OFFSET, + ); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_validate_eval_dynamic_instanceof_target(ctx, &invalid_label); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_branch_if_eval_unboxed_not_object(ctx, &false_label); + load_eval_context_to_arg(ctx, 0); + let object_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, object_arg, EVAL_TEMP_CELL_OFFSET); + let target_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, target_arg, EVAL_CODE_PTR_OFFSET); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + i64::from(exclude_self), + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_object_is_a_dynamic"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_predicate_negative(ctx, &invalid_label); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&invalid_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_call_label(ctx.emitter, "__rt_instanceof_invalid_target"); + + ctx.emitter.label(&done_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + /// Returns true when the current function owns an eval context local. pub(super) fn has_eval_context(ctx: &FunctionContext<'_>) -> bool { eval_context_slot(ctx).is_ok() @@ -1085,6 +1145,48 @@ fn emit_branch_if_eval_unboxed_not_object(ctx: &mut FunctionContext<'_>, label: } } +/// Branches to the invalid-target fatal unless an eval dynamic target is string or object. +fn emit_validate_eval_dynamic_instanceof_target(ctx: &mut FunctionContext<'_>, label: &str) { + let ok_label = ctx.next_label("eval_object_is_a_dynamic_target_ok"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter + .instruction(&format!("b.eq {}", ok_label)); // accept string targets for dynamic instanceof + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter + .instruction(&format!("b.eq {}", ok_label)); // accept object targets for dynamic instanceof + ctx.emitter + .instruction(&format!("b {}", label)); // reject every other dynamic instanceof target kind + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter + .instruction(&format!("je {}", ok_label)); // accept string targets for dynamic instanceof + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter + .instruction(&format!("je {}", ok_label)); // accept object targets for dynamic instanceof + ctx.emitter + .instruction(&format!("jmp {}", label)); // reject every other dynamic instanceof target kind + } + } + ctx.emitter.label(&ok_label); +} + +/// Branches when an eval predicate returned the negative invalid-target sentinel. +fn emit_branch_if_eval_predicate_negative(ctx: &mut FunctionContext<'_>, label: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + let branch = format!("tbnz w0, #31, {}", label); + ctx.emitter.instruction(&branch); // branch when the C int result is the invalid-target sentinel + } + Arch::X86_64 => { + ctx.emitter.instruction("test eax, eax"); // set flags from the C int result + ctx.emitter.instruction(&format!("js {}", label)); // branch when the C int result is the invalid-target sentinel + } + } +} + /// Reorders an unboxed eval string cell into the target string result registers. fn emit_eval_unboxed_string_result(ctx: &mut FunctionContext<'_>) { if ctx.emitter.target.arch == Arch::X86_64 { diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index eed994cdf1..c95e04d774 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -4095,6 +4095,9 @@ pub(super) fn lower_instanceof_dynamic( ) -> Result<()> { let value = expect_operand(inst, 0)?; let target = expect_operand(inst, 1)?; + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_object_is_a_dynamic(ctx, inst, value, target, false); + } let value_ty = ctx.value_php_type(value)?; let target_ty = ctx.value_php_type(target)?; let target_false = ctx.next_label("instanceof_dynamic_target_false"); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0b110342ab..3bdb2b591d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19024,10 +19024,50 @@ echo $aotBox instanceof DynEvalNativeInstanceAotBase ? "A" : "a"; echo ":"; echo $aotBox instanceof DynEvalNativeInstanceAotIface ? "F" : "f"; echo ":"; +$target = "DynEvalNativeInstanceChild"; +echo $box instanceof $target ? "D" : "d"; +echo ":"; +$iface = "DynEvalNativeInstanceIface"; +echo $box instanceof $iface ? "T" : "t"; +echo ":"; echo 7 instanceof DynEvalNativeInstanceChild ? "bad" : "S"; "#, ); - assert_eq!(out, "C:B:I:A:F:S"); + assert_eq!(out, "C:B:I:A:F:D:T:S"); +} + +/// Verifies dynamic `instanceof` keeps invalid-target fatals after the eval barrier. +#[test] +fn test_eval_declared_class_native_dynamic_instanceof_invalid_target_after_barrier() { + let err = compile_and_run_expect_failure( + r#" Date: Sat, 27 Jun 2026 08:15:01 +0200 Subject: [PATCH 0838/1208] Support eval dynamic new after barrier --- .../src/ffi/object_construction.rs | 70 +++++++++++++++++ crates/elephc-magician/src/interpreter/mod.rs | 16 +++- src/codegen/lower_inst/builtins.rs | 9 +++ src/codegen/lower_inst/builtins/eval.rs | 77 ++++++++++++++++++- src/codegen/lower_inst/objects.rs | 15 +++- tests/codegen/eval.rs | 26 +++++++ 6 files changed, 204 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/ffi/object_construction.rs b/crates/elephc-magician/src/ffi/object_construction.rs index 55cfa12956..9236fe6941 100644 --- a/crates/elephc-magician/src/ffi/object_construction.rs +++ b/crates/elephc-magician/src/ffi/object_construction.rs @@ -5,6 +5,7 @@ //! //! Called from: //! - Generated EIR backend assembly through `__elephc_eval_new_object`. +//! - Generated EIR backend assembly through `__elephc_eval_try_new_object`. //! - Generated EIR backend assembly through `__elephc_eval_method_call`. //! - Generated EIR backend assembly through `__elephc_eval_static_method_call`. //! @@ -43,6 +44,29 @@ pub unsafe extern "C" fn __elephc_eval_new_object( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Attempts to construct an eval-declared class with positional cells. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `args` must be readable for +/// `arg_count` runtime-cell pointers when `arg_count > 0`, and `out` may be null. +/// Returns -1 when the class name is not declared in the eval context. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_try_new_object( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_try_new_object_inner(ctx, name_ptr, name_len, args, arg_count, out) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Calls a method on a value that may be an eval-created object. /// /// # Safety @@ -133,6 +157,52 @@ unsafe fn eval_new_object_inner( } } +/// Runs the dynamic object-construction probe ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_try_new_object`; callers must provide a valid context, +/// readable class-name bytes, and readable argument pointer storage. +#[cfg(not(test))] +unsafe fn eval_try_new_object_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + args: *const *mut RuntimeCell, + arg_count: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(arg_count) = usize::try_from(arg_count) else { + return EvalStatus::RuntimeFatal.code(); + }; + if arg_count > 0 && args.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(args, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_try_new_object_outcome(context, &name, args, &mut values) { + Ok(Some(outcome)) => write_outcome(outcome, out).code(), + Ok(None) => -1, + Err(status) => status.code(), + } +} + /// Runs the dynamic static-method call ABI body after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index a48c21e944..2f582c794f 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -250,8 +250,19 @@ pub fn execute_context_new_object_outcome( args: Vec, values: &mut impl RuntimeValueOps, ) -> Result { + execute_context_try_new_object_outcome(context, name, args, values)? + .ok_or(EvalStatus::UnsupportedConstruct) +} + +/// Attempts to construct an eval-declared class, returning `None` when it is absent. +pub fn execute_context_try_new_object_outcome( + context: &mut ElephcEvalContext, + name: &str, + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { let Some(class) = context.class(name).cloned() else { - return Err(EvalStatus::UnsupportedConstruct); + return Ok(None); }; let evaluated_args = args .into_iter() @@ -263,10 +274,11 @@ pub fn execute_context_new_object_outcome( .collect(); let mut scope = ElephcEvalScope::new(); match eval_dynamic_class_new_object(&class, evaluated_args, context, &mut scope, values) { - Ok(result) => Ok(EvalOutcome::Value(result)), + Ok(result) => Ok(Some(EvalOutcome::Value(result))), Err(EvalStatus::UncaughtThrowable) => context .take_pending_throw() .map(EvalOutcome::Throwable) + .map(Some) .ok_or(EvalStatus::UncaughtThrowable), Err(status) => Err(status), } diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 0f4007d0d0..320b5d6eb6 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -116,6 +116,15 @@ pub(super) fn lower_eval_object_new( eval::lower_eval_object_new(ctx, inst) } +/// Lowers fallback construction of a runtime class name through eval dynamic metadata. +pub(super) fn lower_eval_object_new_dynamic_fallback( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + miss_label: &str, +) -> Result<()> { + eval::lower_eval_object_new_dynamic_fallback(ctx, inst, miss_label) +} + /// Lowers a post-eval method call that may target an eval-created object. pub(super) fn lower_eval_method_call( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 4d1ae33863..ff13a70250 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -401,6 +401,66 @@ pub(super) fn lower_eval_object_new( store_if_result(ctx, inst) } +/// Lowers fallback `new $class` construction through eval dynamic metadata. +pub(super) fn lower_eval_object_new_dynamic_fallback( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + miss_label: &str, +) -> Result<()> { + let constructor_args = inst.operands.get(1..).ok_or_else(|| { + CodegenIrError::invalid_module("eval dynamic object new missing class operand") + })?; + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_function_call_stack_bytes(constructor_args.len()); + let eval_miss_label = ctx.next_label("eval_dynamic_new_missing_class"); + let done_label = ctx.next_label("eval_dynamic_new_done"); + let name_ptr_reg = abi::int_arg_reg_name(ctx.emitter.target, 1); + let name_len_reg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, name_ptr_reg, 0); + abi::emit_load_temporary_stack_slot(ctx.emitter, name_len_reg, 8); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + abi::emit_store_to_sp(ctx.emitter, name_ptr_reg, EVAL_CODE_PTR_OFFSET); + abi::emit_store_to_sp(ctx.emitter, name_len_reg, EVAL_CODE_LEN_OFFSET); + ensure_eval_context(ctx)?; + store_eval_function_call_operands(ctx, constructor_args, args_offset)?; + load_eval_context_to_arg(ctx, 0); + let name_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_load_temporary_stack_slot(ctx.emitter, name_ptr_arg, EVAL_CODE_PTR_OFFSET); + let name_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_load_temporary_stack_slot(ctx.emitter, name_len_arg, EVAL_CODE_LEN_OFFSET); + let args_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + if constructor_args.is_empty() { + abi::emit_load_int_immediate(ctx.emitter, args_arg, 0); + } else { + abi::emit_temporary_stack_address(ctx.emitter, args_arg, args_offset); + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + constructor_args.len() as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_try_new_object"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &eval_miss_label); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + abi::emit_release_temporary_stack(ctx.emitter, 16); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&eval_miss_label); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + abi::emit_jump(ctx.emitter, miss_label); + ctx.emitter.label(&done_label); + Ok(()) +} + /// Lowers a method call that may dispatch to an eval-created dynamic object. pub(super) fn lower_eval_method_call( ctx: &mut FunctionContext<'_>, @@ -727,7 +787,7 @@ pub(super) fn lower_eval_object_is_a_dynamic( .target .extern_symbol("__elephc_eval_object_is_a_dynamic"); abi::emit_call_label(ctx.emitter, &symbol); - emit_branch_if_eval_predicate_negative(ctx, &invalid_label); + emit_branch_if_eval_c_int_negative(ctx, &invalid_label); abi::emit_jump(ctx.emitter, &done_label); ctx.emitter.label(&false_label); @@ -1013,7 +1073,16 @@ fn store_eval_function_call_args( inst: &Instruction, args_offset: usize, ) -> Result<()> { - for (index, operand) in inst.operands.iter().enumerate() { + store_eval_function_call_operands(ctx, &inst.operands, args_offset) +} + +/// Stores one operand slice as boxed Mixed cells for eval positional-call ABIs. +fn store_eval_function_call_operands( + ctx: &mut FunctionContext<'_>, + operands: &[ValueId], + args_offset: usize, +) -> Result<()> { + for (index, operand) in operands.iter().enumerate() { let ty = ctx.load_value_to_result(*operand)?.codegen_repr(); if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { emit_box_current_value_as_mixed(ctx.emitter, &ty); @@ -1173,8 +1242,8 @@ fn emit_validate_eval_dynamic_instanceof_target(ctx: &mut FunctionContext<'_>, l ctx.emitter.label(&ok_label); } -/// Branches when an eval predicate returned the negative invalid-target sentinel. -fn emit_branch_if_eval_predicate_negative(ctx: &mut FunctionContext<'_>, label: &str) { +/// Branches when an eval C-ABI call returned a negative `int` sentinel. +fn emit_branch_if_eval_c_int_negative(ctx: &mut FunctionContext<'_>, label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { let branch = format!("tbnz w0, #31, {}", label); diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index c95e04d774..91d805a406 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -1273,9 +1273,18 @@ pub(super) fn lower_dynamic_object_new_mixed( } ctx.emitter.label(&fallback_label); - emit_dynamic_new_mixed_fallback(ctx); - ctx.store_result_value(result)?; - abi::emit_jump(ctx.emitter, &done_label); + if builtins::has_eval_context(ctx) { + let eval_miss_label = ctx.next_label("dynamic_new_mixed_eval_miss"); + builtins::lower_eval_object_new_dynamic_fallback(ctx, inst, &eval_miss_label)?; + ctx.store_result_value(result)?; + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&eval_miss_label); + emit_dynamic_new_class_not_found_fatal(ctx); + } else { + emit_dynamic_new_mixed_fallback(ctx); + ctx.store_result_value(result)?; + abi::emit_jump(ctx.emitter, &done_label); + } ctx.emitter.label(&non_string_label); emit_dynamic_new_invalid_class_name_fatal(ctx); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3bdb2b591d..4755ae9fa8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18960,6 +18960,32 @@ echo $box->x; assert_eq!(out, "9:10:12:12"); } +/// Verifies native dynamic `new $class` can instantiate eval-declared classes after the barrier. +#[test] +fn test_eval_declared_class_dynamic_new_natively_after_barrier() { + let out = compile_and_run( + r#"label = $label; } + public function readAot() { return $this->label; } +} +eval('class DynEvalNativeDynamicNew { + public int $label; + public function __construct($label) { $this->label = $label; } + public function read() { return $this->label; } +}'); +$class = "DynEvalNativeDynamicNew"; +$box = new $class(5); +echo $box->read() . ":"; +$aotClass = "DynEvalNativeDynamicNewAot"; +$aotBox = new $aotClass(7); +echo $aotBox->readAot(); +"#, + ); + assert_eq!(out, "5:7"); +} + /// Verifies native property writes can update eval-created objects after the barrier. #[test] fn test_eval_declared_class_native_property_write_after_barrier() { From ed1564c9722084b011e54bdfecbd491e9b255653 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 08:22:25 +0200 Subject: [PATCH 0839/1208] Route eval mixed method calls through bridge --- src/codegen/lower_inst.rs | 13 ++++--------- src/ir_lower/expr/mod.rs | 3 +++ tests/codegen/eval.rs | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 1438ebc1bf..64045b6167 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -2902,11 +2902,11 @@ fn lower_mixed_method_call( object: ValueId, method_name: &str, ) -> Result<()> { + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_method_call(ctx, inst, object, method_name); + } let candidates = mixed_method_candidates(ctx, method_name, inst.operands.len())?; if candidates.is_empty() { - if builtins::has_eval_context(ctx) { - return builtins::lower_eval_method_call(ctx, inst, object, method_name); - } emit_method_call_on_null_fatal(ctx, method_name); return Ok(()); } @@ -2943,12 +2943,7 @@ fn lower_mixed_method_call( } ctx.emitter.label(&no_match_label); - if builtins::has_eval_context(ctx) { - builtins::lower_eval_method_call(ctx, inst, object, method_name)?; - abi::emit_jump(ctx.emitter, &done_label); - } else { - emit_method_call_on_null_fatal(ctx, method_name); - } + emit_method_call_on_null_fatal(ctx, method_name); ctx.emitter.label(&non_object_label); emit_method_call_on_null_fatal(ctx, method_name); diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index d9d474dce2..fe2e00792f 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -12897,6 +12897,9 @@ fn method_signature( return class_method_signature(ctx, normalized, &key).cloned(); } if dynamic_method_receiver_needs_mixed_fallback(&object_ty) { + if ctx.has_eval_barrier() { + return None; + } return common_dynamic_method_signature(ctx, &key); } None diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4755ae9fa8..b22a00da85 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18968,7 +18968,7 @@ fn test_eval_declared_class_dynamic_new_natively_after_barrier() { class DynEvalNativeDynamicNewAot { public int $label; public function __construct($label) { $this->label = $label; } - public function readAot() { return $this->label; } + public function read() { return $this->label; } } eval('class DynEvalNativeDynamicNew { public int $label; @@ -18980,7 +18980,7 @@ $box = new $class(5); echo $box->read() . ":"; $aotClass = "DynEvalNativeDynamicNewAot"; $aotBox = new $aotClass(7); -echo $aotBox->readAot(); +echo $aotBox->read(); "#, ); assert_eq!(out, "5:7"); From 685887f0a82bd838c8526347cf4e7756cc20e6e8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 08:31:40 +0200 Subject: [PATCH 0840/1208] Update eval trait failure expectation --- tests/codegen/eval.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b22a00da85..aca766a94a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8712,8 +8712,8 @@ echo $box->hidden();'); "#, ); assert!( - err.contains("Fatal error: eval() runtime failed"), - "stderr did not contain eval runtime fatal diagnostic: {err}" + err.contains("Fatal error: uncaught exception"), + "stderr did not contain uncaught throwable diagnostic: {err}" ); } From d84cb4461357aba3994f9aa1aafecf33789fec6b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 09:01:12 +0200 Subject: [PATCH 0841/1208] Fix eval reflection mixed dispatch --- .../src/interpreter/reflection.rs | 17 ------ src/codegen/lower_inst.rs | 13 +++-- src/codegen/lower_inst/objects/reflection.rs | 12 ++++- src/types/checker/builtin_types/reflection.rs | 52 ++++++++++++++++++- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index a08f9c5090..e6e121274f 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1386,12 +1386,6 @@ pub(in crate::interpreter) fn eval_reflection_parameter_legacy_type_predicate_re return Ok(None); } eval_reflection_bind_no_args(evaluated_args)?; - if let Some(flag_property) = eval_reflection_parameter_legacy_type_flag_property(method_name) { - let flag = values.property_get(object, flag_property)?; - if values.type_tag(flag)? == EVAL_TAG_BOOL { - return Ok(Some(flag)); - } - } let type_value = values.method_call(object, "getType", Vec::new())?; if values.is_null(type_value)? { return values.bool_value(false).map(Some); @@ -1660,17 +1654,6 @@ fn eval_reflection_parameter_legacy_type_name(method_name: &str) -> Option<&'sta } } -/// Maps a legacy ReflectionParameter predicate method to its precomputed flag slot. -fn eval_reflection_parameter_legacy_type_flag_property(method_name: &str) -> Option<&'static str> { - if method_name.eq_ignore_ascii_case("isArray") { - Some("__is_array_type") - } else if method_name.eq_ignore_ascii_case("isCallable") { - Some("__is_callable_type") - } else { - None - } -} - /// Returns whether one runtime object cell has the requested PHP class name. fn eval_reflection_object_has_class( object: RuntimeCellHandle, diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 64045b6167..1438ebc1bf 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -2902,11 +2902,11 @@ fn lower_mixed_method_call( object: ValueId, method_name: &str, ) -> Result<()> { - if builtins::has_eval_context(ctx) { - return builtins::lower_eval_method_call(ctx, inst, object, method_name); - } let candidates = mixed_method_candidates(ctx, method_name, inst.operands.len())?; if candidates.is_empty() { + if builtins::has_eval_context(ctx) { + return builtins::lower_eval_method_call(ctx, inst, object, method_name); + } emit_method_call_on_null_fatal(ctx, method_name); return Ok(()); } @@ -2943,7 +2943,12 @@ fn lower_mixed_method_call( } ctx.emitter.label(&no_match_label); - emit_method_call_on_null_fatal(ctx, method_name); + if builtins::has_eval_context(ctx) { + builtins::lower_eval_method_call(ctx, inst, object, method_name)?; + abi::emit_jump(ctx.emitter, &done_label); + } else { + emit_method_call_on_null_fatal(ctx, method_name); + } ctx.emitter.label(&non_object_label); emit_method_call_on_null_fatal(ctx, method_name); diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 06a039658c..72de275dc9 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -2894,12 +2894,21 @@ fn reflection_property_visible_from_class( /// Returns the class that declares one reflected instance or static method. fn reflection_method_declaring_class_name( info: &crate::types::ClassInfo, + reflected_class_name: &str, method_key: &str, ) -> Option { info.method_declaring_classes .get(method_key) .or_else(|| info.static_method_declaring_classes.get(method_key)) .cloned() + .or_else(|| { + reflection_class_has_method_kind(info, method_key, false) + .then(|| reflected_class_name.to_string()) + }) + .or_else(|| { + reflection_class_has_method_kind(info, method_key, true) + .then(|| reflected_class_name.to_string()) + }) } /// Returns a prototype method for a reflected generated/AOT class method, if PHP exposes one. @@ -3143,7 +3152,8 @@ fn reflection_class_method_member( let Some(sig) = sig else { return Ok(None); }; - let declaring_class_name = reflection_method_declaring_class_name(info, &method_key); + let declaring_class_name = + reflection_method_declaring_class_name(info, class_name, &method_key); let attr_names = info .method_attribute_names .get(&method_key) diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 3865fe0f88..f8fb0386e4 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -3059,6 +3059,40 @@ fn builtin_reflection_class_nullable_object_method( } } +/// Returns a public object-valued `ReflectionClass` method backed by one private slot. +fn builtin_reflection_class_object_method( + method_name: &str, + property: &str, + class_name: &str, +) -> ClassMethod { + let dummy_span = crate::span::Span::dummy(); + ClassMethod { + name: method_name.to_string(), + visibility: Visibility::Public, + is_static: false, + is_abstract: false, + is_final: false, + has_body: true, + params: Vec::new(), + param_attributes: Vec::new(), + variadic: None, + variadic_type: None, + return_type: Some(TypeExpr::Named(Name::unqualified(class_name))), + body: vec![Stmt::new( + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, dummy_span)), + property: property.to_string(), + }, + dummy_span, + ))), + dummy_span, + )], + span: dummy_span, + attributes: Vec::new(), + } +} + /// Returns a public mixed `ReflectionClass` method backed by one private slot. fn builtin_reflection_class_mixed_method(method_name: &str, property: &str) -> ClassMethod { let dummy_span = crate::span::Span::dummy(); @@ -3841,9 +3875,10 @@ fn builtin_reflection_owner_class( Some(mixed_type()), false_bool(), )); - methods.push(builtin_reflection_class_mixed_method( + methods.push(builtin_reflection_class_object_method( "getDeclaringClass", "__declaring_class", + "ReflectionClass", )); } if matches!( @@ -4676,6 +4711,21 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("getExtension")) { sig.return_type = PhpType::Mixed; } + if matches!( + class_name, + "ReflectionMethod" + | "ReflectionProperty" + | "ReflectionClassConstant" + | "ReflectionEnumUnitCase" + | "ReflectionEnumBackedCase" + ) { + if let Some(sig) = class_info + .methods + .get_mut(&php_symbol_key("getDeclaringClass")) + { + sig.return_type = PhpType::Object("ReflectionClass".to_string()); + } + } if matches!(class_name, "ReflectionClass" | "ReflectionObject") { for (property_name, property_type) in &mut class_info.properties { match property_name.as_str() { From 98393743262e9bd185f3f0b260ae08093c81e733 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 09:09:02 +0200 Subject: [PATCH 0842/1208] Cover eval abstract property concretization --- tests/codegen/eval.rs | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index aca766a94a..d7a2c216aa 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10124,6 +10124,50 @@ eval('class EvalBadStaticPromoted { } } +/// Verifies eval-declared plain abstract properties can be concretized by child storage. +#[test] +fn test_eval_declared_plain_abstract_property_concretization() { + let out = compile_and_run( + r#"sides; + } +} +abstract class EvalPlainAbstractPolygon extends EvalPlainAbstractShape {} +class EvalPlainAbstractSquare extends EvalPlainAbstractPolygon { + public int $sides = 4; +} + +abstract class EvalPlainAbstractEntity { + abstract public int $id { get; set; } +} +class EvalPlainAbstractUser extends EvalPlainAbstractEntity { + public function __construct(public int $id) {} +} + +abstract class EvalPlainAbstractReadonlyBase { + abstract public int $value { get; } +} +class EvalPlainAbstractReadonlyBox extends EvalPlainAbstractReadonlyBase { + public readonly int $value; + + public function __construct(int $value) { + $this->value = $value; + } +} + +$shape = new EvalPlainAbstractSquare(); +$user = new EvalPlainAbstractUser(7); +$box = new EvalPlainAbstractReadonlyBox(42); +echo $shape->show() . ":" . $user->id . ":" . $box->value;'); +"#, + ); + assert_eq!(out, "4:7:42"); +} + /// Verifies eval-declared abstract property hook contracts validate concrete subclasses. #[test] fn test_eval_declared_abstract_property_hook_contracts() { From e3b891268af21374c1a33d44a180c35535b34814 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 09:19:59 +0200 Subject: [PATCH 0843/1208] Cover eval AOT callable named args --- tests/codegen/eval.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index d7a2c216aa..15c7f5f779 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6597,6 +6597,40 @@ $box->run(); assert_eq!(out, "41a:42b:43c"); } +/// Verifies eval callable arrays bind named arguments for generated/AOT methods. +#[test] +fn test_eval_fragment_callable_array_dispatches_aot_method_with_named_args() { + let out = compile_and_run_capture( + r#" "D", "left" => "C"]) . ":" . + call_user_func_array($static, ["right" => "F", "left" => "E"]);'); + } +} + +echo (new EvalAotCallableArrayNamedBox())->run(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "AB:CD:EF"); +} + /// Verifies eval static calls and static callables dispatch public AOT static methods. #[test] fn test_eval_fragment_dispatches_aot_static_methods() { From 906e6593343e7037fad4f9e3e03a7dee30e3dd68 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 09:44:54 +0200 Subject: [PATCH 0844/1208] Support eval callable args for AOT methods --- src/codegen/eval_callable_helpers.rs | 272 ++++++++++++++++++++++++ src/codegen/eval_constructor_helpers.rs | 229 ++++++++++++++++++-- src/codegen/eval_method_helpers.rs | 201 +++++++++++++++-- src/codegen/lower_inst.rs | 51 ++++- src/codegen/lower_inst/builtins/eval.rs | 9 +- src/codegen/mod.rs | 23 +- tests/codegen/eval.rs | 39 ++++ 7 files changed, 783 insertions(+), 41 deletions(-) create mode 100644 src/codegen/eval_callable_helpers.rs diff --git a/src/codegen/eval_callable_helpers.rs b/src/codegen/eval_callable_helpers.rs new file mode 100644 index 0000000000..c887d959f5 --- /dev/null +++ b/src/codegen/eval_callable_helpers.rs @@ -0,0 +1,272 @@ +//! Purpose: +//! Builds callable descriptor support reused by eval-to-native constructor and +//! method bridges. +//! +//! Called from: +//! - `crate::codegen::finalize_user_asm()`. +//! - `crate::codegen::eval_constructor_helpers` and +//! `crate::codegen::eval_method_helpers`. +//! +//! Key details: +//! - Eval stores PHP callbacks as boxed runtime values, while AOT callable +//! parameters expect descriptor pointers. +//! - This bridge slice currently accepts string callables and selects static +//! descriptors with uniform invokers generated by the existing callable +//! dispatch machinery. + +use crate::codegen::abi; +use crate::codegen::callable_descriptor::{ + self, CallableDescriptorInvocation, CallableDescriptorShape, +}; +use crate::codegen::callable_dispatch::{self, RuntimeCallableCase, RuntimeCallableSelector}; +use crate::codegen::data_section::DataSection; +use crate::codegen::emit::Emitter; +use crate::ir::{Function, LocalKind, Module}; +use crate::names::function_symbol; +use crate::types::{callable_wrapper_sig, PhpType}; + +/// Callable descriptors available to eval constructor and method bridges. +pub(super) struct EvalCallableDescriptorSupport { + cases: Vec, +} + +impl EvalCallableDescriptorSupport { + /// Returns true when no callable descriptor case is available. + pub(super) fn is_empty(&self) -> bool { + self.cases.is_empty() + } +} + +/// Returns true when eval bridges may need to turn PHP callbacks into descriptors. +pub(super) fn module_needs_eval_callable_descriptor_support(module: &Module) -> bool { + module_uses_eval(module) && module_has_callable_aot_method_param(module) +} + +/// Returns true when the EIR module contains a function that can call eval. +fn module_uses_eval(module: &Module) -> bool { + all_module_functions(module).any(function_uses_eval) +} + +/// Iterates every EIR function body emitted or inspected by the backend. +fn all_module_functions(module: &Module) -> impl Iterator { + module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) +} + +/// Returns true when a function has hidden eval state locals. +fn function_uses_eval(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Returns true when any generated class callable exposed to eval accepts `callable`. +fn module_has_callable_aot_method_param(module: &Module) -> bool { + module.class_infos.values().any(|class_info| { + class_info.methods.values().any(signature_has_callable_param) + || class_info + .static_methods + .values() + .any(signature_has_callable_param) + }) +} + +/// Returns true when one signature contains a callable ABI parameter. +fn signature_has_callable_param(signature: &crate::types::FunctionSig) -> bool { + signature + .params + .iter() + .any(|(_, ty)| ty.codegen_repr() == PhpType::Callable) +} + +/// Emits shared runtime callable wrappers/invokers and returns descriptor cases. +pub(super) fn emit_eval_callable_descriptor_support( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + needed: bool, +) -> EvalCallableDescriptorSupport { + if !needed { + return EvalCallableDescriptorSupport { cases: Vec::new() }; + } + let mut legacy_ctx = super::lower_inst::legacy_context_from_eir_module(module); + legacy_ctx.functions.retain(|name, _| { + module + .functions + .iter() + .any(|function| !function.flags.is_main && function.name == *name) + }); + let cases = eval_user_function_callable_cases(&mut legacy_ctx, data); + emit_deferred_callable_support(emitter, data, &mut legacy_ctx); + EvalCallableDescriptorSupport { cases } +} + +/// Builds descriptor cases for user functions visible as eval string callables. +fn eval_user_function_callable_cases( + legacy_ctx: &mut crate::codegen::context::Context, + data: &mut DataSection, +) -> Vec { + let mut functions = legacy_ctx + .functions + .iter() + .map(|(name, sig)| (name.clone(), sig.clone())) + .collect::>(); + functions.sort_by(|left, right| left.0.cmp(&right.0)); + let mut cases = Vec::with_capacity(functions.len()); + for (name, sig) in functions { + let case_sig = callable_wrapper_sig(&sig); + let invoker_label = + callable_dispatch::ensure_runtime_descriptor_invoker(legacy_ctx, &[], &case_sig); + let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( + data, + &function_symbol(&name), + Some(&name), + callable_descriptor::CALLABLE_DESC_KIND_FUNCTION, + Some(&case_sig), + &[], + &[], + CallableDescriptorInvocation::named(CallableDescriptorShape::Function, &name), + invoker_label.as_deref(), + ); + cases.push(RuntimeCallableCase { + label: function_symbol(&name), + descriptor_label, + php_name: Some(name), + sig: case_sig, + captures: Vec::new(), + has_invoker: invoker_label.is_some(), + invoker_label, + }); + } + cases +} + +/// Emits deferred callable support bodies behind one jump. +fn emit_deferred_callable_support( + emitter: &mut Emitter, + data: &mut DataSection, + legacy_ctx: &mut crate::codegen::context::Context, +) { + if legacy_ctx.deferred_closures.is_empty() + && legacy_ctx.deferred_fiber_wrappers.is_empty() + && legacy_ctx.deferred_callback_wrappers.is_empty() + && legacy_ctx.deferred_extern_callback_trampolines.is_empty() + && legacy_ctx.deferred_runtime_callable_invokers.is_empty() + { + return; + } + let done_label = legacy_ctx.next_label("eval_callable_support_done"); + abi::emit_jump(emitter, &done_label); + crate::codegen::emit_deferred_closures(emitter, data, legacy_ctx); + emitter.label(&done_label); +} + +/// Converts the ARM64 boxed eval argument slot into a callable descriptor pointer. +pub(super) fn emit_aarch64_cast_eval_callable_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, +) { + let string_label = format!("{}_callable_string", label_prefix); + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for callable validation + emitter.instruction("bl __rt_mixed_unbox"); // expose the eval callable tag and payload words + emitter.instruction("cmp x0, #1"); // runtime tag 1 means a string callable name + emitter.instruction(&format!("b.eq {}", string_label)); // resolve string callables through descriptor metadata + abi::emit_jump(emitter, fail_label); + emitter.label(&string_label); + abi::emit_push_reg_pair(emitter, "x1", "x2"); + emit_eval_string_callable_descriptor_lookup( + module, + emitter, + data, + support, + label_prefix, + fail_label, + "x0", + ); +} + +/// Converts the x86_64 boxed eval argument slot into a callable descriptor pointer. +pub(super) fn emit_x86_64_cast_eval_callable_arg( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, +) { + let string_label = format!("{}_callable_string", label_prefix); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for callable validation + emitter.instruction("call __rt_mixed_unbox"); // expose the eval callable tag and payload words + emitter.instruction("cmp rax, 1"); // runtime tag 1 means a string callable name + emitter.instruction(&format!("je {}", string_label)); // resolve string callables through descriptor metadata + abi::emit_jump(emitter, fail_label); + emitter.label(&string_label); + abi::emit_push_reg_pair(emitter, "rdi", "rdx"); + emit_eval_string_callable_descriptor_lookup( + module, + emitter, + data, + support, + label_prefix, + fail_label, + "rax", + ); +} + +/// Looks up a descriptor by the string callable currently saved on the temp stack. +fn emit_eval_string_callable_descriptor_lookup( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, + result_reg: &str, +) { + let done_label = format!("{}_callable_done", label_prefix); + let miss_label = format!("{}_callable_missing", label_prefix); + if support.is_empty() { + abi::emit_release_temporary_stack(emitter, 16); + abi::emit_jump(emitter, fail_label); + return; + } + let selector = RuntimeCallableSelector::StringNameStack { + ptr_offset: 0, + len_offset: 8, + call_reg: result_reg, + }; + let mut legacy_ctx = super::lower_inst::legacy_context_from_eir_module(module); + for case in &support.cases { + let next_case = legacy_ctx.next_label("eval_callable_next"); + callable_dispatch::emit_branch_if_callable_case_mismatch( + &selector, + case, + &next_case, + emitter, + &mut legacy_ctx, + data, + ); + abi::emit_jump(emitter, &done_label); + emitter.label(&next_case); + } + abi::emit_jump(emitter, &miss_label); + emitter.label(&miss_label); + abi::emit_release_temporary_stack(emitter, 16); + abi::emit_jump(emitter, fail_label); + emitter.label(&done_label); + abi::emit_release_temporary_stack(emitter, 16); +} diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index b3b9a673d3..69d957bf09 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -31,6 +31,7 @@ use super::eval_ref_arg_helpers::{ eval_normalized_ref_params, eval_ref_arg_slots, eval_signature_ref_params_supported, emit_aarch64_write_back_ref_args, emit_x86_64_write_back_ref_args, }; +use super::eval_callable_helpers::EvalCallableDescriptorSupport; const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ "Error", @@ -75,13 +76,21 @@ pub(super) fn emit_eval_constructor_helpers( module: &Module, emitter: &mut Emitter, data: &mut DataSection, + callable_support: &EvalCallableDescriptorSupport, ) { if !module_uses_eval(module) { return; } let slots = collect_eval_constructor_slots(module); let builtin_throwable_class_ids = collect_builtin_throwable_constructor_class_ids(module); - emit_constructor_helper(module, emitter, data, &slots, &builtin_throwable_class_ids); + emit_constructor_helper( + module, + emitter, + data, + &slots, + &builtin_throwable_class_ids, + callable_support, + ); } /// Returns true when the EIR module contains a function that can call eval. @@ -252,6 +261,7 @@ fn constructor_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Callable | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Iterable @@ -268,16 +278,31 @@ fn emit_constructor_helper( data: &mut DataSection, slots: &[EvalConstructorSlot], builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, ) { emitter.blank(); emitter.comment("--- eval bridge: user constructor call ---"); label_c_global(module, emitter, "__elephc_eval_value_construct_object"); match module.target.arch { Arch::AArch64 => { - emit_constructor_aarch64(module, emitter, data, slots, builtin_throwable_class_ids) + emit_constructor_aarch64( + module, + emitter, + data, + slots, + builtin_throwable_class_ids, + callable_support, + ) } Arch::X86_64 => { - emit_constructor_x86_64(module, emitter, data, slots, builtin_throwable_class_ids) + emit_constructor_x86_64( + module, + emitter, + data, + slots, + builtin_throwable_class_ids, + callable_support, + ) } } } @@ -289,6 +314,7 @@ fn emit_constructor_aarch64( data: &mut DataSection, slots: &[EvalConstructorSlot], builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, ) { let success_label = "__elephc_eval_value_construct_success"; let fail_label = "__elephc_eval_value_construct_fail"; @@ -307,11 +333,21 @@ fn emit_constructor_aarch64( emit_aarch64_builtin_throwable_constructor_dispatch( module, emitter, + data, builtin_throwable_class_ids, fail_label, success_label, + callable_support, + ); + emit_aarch64_constructor_dispatch( + module, + emitter, + data, + slots, + fail_label, + success_label, + callable_support, ); - emit_aarch64_constructor_dispatch(module, emitter, data, slots, fail_label, success_label); emitter.instruction(&format!("b {}", success_label)); // no constructor metadata matched this class id emitter.label(fail_label); emitter.instruction("mov x0, #0"); // report constructor dispatch failure to Rust @@ -331,6 +367,7 @@ fn emit_constructor_x86_64( data: &mut DataSection, slots: &[EvalConstructorSlot], builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, ) { let success_label = "__elephc_eval_value_construct_success_x"; let fail_label = "__elephc_eval_value_construct_fail_x"; @@ -351,11 +388,21 @@ fn emit_constructor_x86_64( emit_x86_64_builtin_throwable_constructor_dispatch( module, emitter, + data, builtin_throwable_class_ids, fail_label, success_label, + callable_support, + ); + emit_x86_64_constructor_dispatch( + module, + emitter, + data, + slots, + fail_label, + success_label, + callable_support, ); - emit_x86_64_constructor_dispatch(module, emitter, data, slots, fail_label, success_label); emitter.instruction(&format!("jmp {}", success_label)); // no constructor metadata matched this class id emitter.label(fail_label); emitter.instruction("xor eax, eax"); // report constructor dispatch failure to Rust @@ -372,9 +419,11 @@ fn emit_constructor_x86_64( fn emit_aarch64_builtin_throwable_constructor_dispatch( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, class_ids: &[u64], fail_label: &str, success_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { for class_id in class_ids { let next_label = format!("__elephc_eval_builtin_throwable_next_{}", class_id); @@ -383,7 +432,14 @@ fn emit_aarch64_builtin_throwable_constructor_dispatch( abi::emit_load_int_immediate(emitter, "x10", *class_id as i64); emitter.instruction("cmp x9, x10"); // compare receiver class id against this builtin Throwable class emitter.instruction(&format!("b.ne {}", next_label)); // try the next builtin Throwable class when ids differ - emit_aarch64_builtin_throwable_constructor_body(module, emitter, fail_label, success_label); + emit_aarch64_builtin_throwable_constructor_body( + module, + emitter, + data, + fail_label, + success_label, + callable_support, + ); emitter.label(&next_label); } } @@ -392,9 +448,11 @@ fn emit_aarch64_builtin_throwable_constructor_dispatch( fn emit_x86_64_builtin_throwable_constructor_dispatch( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, class_ids: &[u64], fail_label: &str, success_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { for class_id in class_ids { let next_label = format!("__elephc_eval_builtin_throwable_next_{}_x", class_id); @@ -403,7 +461,14 @@ fn emit_x86_64_builtin_throwable_constructor_dispatch( abi::emit_load_int_immediate(emitter, "r10", *class_id as i64); emitter.instruction("cmp r11, r10"); // compare receiver class id against this builtin Throwable class emitter.instruction(&format!("jne {}", next_label)); // try the next builtin Throwable class when ids differ - emit_x86_64_builtin_throwable_constructor_body(module, emitter, fail_label, success_label); + emit_x86_64_builtin_throwable_constructor_body( + module, + emitter, + data, + fail_label, + success_label, + callable_support, + ); emitter.label(&next_label); } } @@ -412,8 +477,10 @@ fn emit_x86_64_builtin_throwable_constructor_dispatch( fn emit_aarch64_builtin_throwable_constructor_body( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, fail_label: &str, success_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { emit_aarch64_validate_builtin_throwable_arg_count(module, emitter, fail_label); emit_aarch64_default_builtin_throwable_fields(emitter); @@ -427,6 +494,8 @@ fn emit_aarch64_builtin_throwable_constructor_body( &PhpType::Str, "__elephc_eval_builtin_throwable_message", fail_label, + data, + callable_support, ); emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for message initialization emitter.instruction("str x1, [x9, #8]"); // store the message pointer in the compact Throwable payload @@ -441,6 +510,8 @@ fn emit_aarch64_builtin_throwable_constructor_body( &PhpType::Int, "__elephc_eval_builtin_throwable_code", fail_label, + data, + callable_support, ); emitter.instruction("ldr x9, [sp, #16]"); // reload the compact Throwable object for code initialization emitter.instruction("str x0, [x9, #24]"); // store the integer exception code @@ -451,8 +522,10 @@ fn emit_aarch64_builtin_throwable_constructor_body( fn emit_x86_64_builtin_throwable_constructor_body( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, fail_label: &str, success_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { emit_x86_64_validate_builtin_throwable_arg_count(module, emitter, fail_label); emit_x86_64_default_builtin_throwable_fields(emitter); @@ -466,6 +539,8 @@ fn emit_x86_64_builtin_throwable_constructor_body( &PhpType::Str, "__elephc_eval_builtin_throwable_message_x", fail_label, + data, + callable_support, ); emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for message initialization emitter.instruction("mov QWORD PTR [r11 + 8], rax"); // store the message pointer in the compact Throwable payload @@ -480,6 +555,8 @@ fn emit_x86_64_builtin_throwable_constructor_body( &PhpType::Int, "__elephc_eval_builtin_throwable_code_x", fail_label, + data, + callable_support, ); emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the compact Throwable object for code initialization emitter.instruction("mov QWORD PTR [r11 + 24], rax"); // store the integer exception code @@ -538,6 +615,7 @@ fn emit_aarch64_constructor_dispatch( slots: &[EvalConstructorSlot], fail_label: &str, success_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { for (class_id, class_slots) in grouped_slots(slots) { let next_label = format!("__elephc_eval_constructor_next_{}", class_id); @@ -547,7 +625,15 @@ fn emit_aarch64_constructor_dispatch( emitter.instruction("cmp x9, x10"); // compare receiver class id against this constructor class emitter.instruction(&format!("b.ne {}", next_label)); // try the next constructor class when ids differ for slot in class_slots { - emit_aarch64_constructor_body(module, emitter, data, slot, fail_label, success_label); + emit_aarch64_constructor_body( + module, + emitter, + data, + slot, + fail_label, + success_label, + callable_support, + ); } emitter.label(&next_label); } @@ -561,6 +647,7 @@ fn emit_x86_64_constructor_dispatch( slots: &[EvalConstructorSlot], fail_label: &str, success_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { for (class_id, class_slots) in grouped_slots(slots) { let next_label = format!("__elephc_eval_constructor_next_{}_x", class_id); @@ -570,7 +657,15 @@ fn emit_x86_64_constructor_dispatch( emitter.instruction("cmp r11, r10"); // compare receiver class id against this constructor class emitter.instruction(&format!("jne {}", next_label)); // try the next constructor class when ids differ for slot in class_slots { - emit_x86_64_constructor_body(module, emitter, data, slot, fail_label, success_label); + emit_x86_64_constructor_body( + module, + emitter, + data, + slot, + fail_label, + success_label, + callable_support, + ); } emitter.label(&next_label); } @@ -584,6 +679,7 @@ fn emit_aarch64_constructor_body( slot: &EvalConstructorSlot, fail_label: &str, success_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { if !slot.supported { emitter.instruction(&format!("b {}", fail_label)); // reject constructors outside this bridge's supported ABI slice @@ -596,7 +692,14 @@ fn emit_aarch64_constructor_body( } emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); let (overflow_bytes, ref_slots) = - emit_aarch64_prepare_constructor_args(module, emitter, slot, fail_label); + emit_aarch64_prepare_constructor_args( + module, + emitter, + data, + slot, + fail_label, + callable_support, + ); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); let callee = slot @@ -624,6 +727,7 @@ fn emit_x86_64_constructor_body( slot: &EvalConstructorSlot, fail_label: &str, success_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { if !slot.supported { emitter.instruction(&format!("jmp {}", fail_label)); // reject constructors outside this bridge's supported ABI slice @@ -636,7 +740,14 @@ fn emit_x86_64_constructor_body( } emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); let (overflow_bytes, ref_slots) = - emit_x86_64_prepare_constructor_args(module, emitter, slot, fail_label); + emit_x86_64_prepare_constructor_args( + module, + emitter, + data, + slot, + fail_label, + callable_support, + ); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); let callee = slot @@ -758,17 +869,21 @@ fn emit_x86_64_validate_constructor_arg_count( fn emit_aarch64_prepare_constructor_args( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slot: &EvalConstructorSlot, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> (usize, Vec) { let body_label = constructor_body_label(module, slot); let ref_slots = emit_aarch64_constructor_ref_arg_cells( module, emitter, + data, &slot.params, &slot.ref_params, &body_label, fail_label, + callable_support, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); @@ -783,7 +898,15 @@ fn emit_aarch64_prepare_constructor_args( emitter.instruction(&format!("cbz x9, {}", default_label)); // omitted SplFixedArray size uses PHP's zero default emit_aarch64_load_eval_arg(module, emitter, index); let label_prefix = format!("{}_arg_{}", body_label, index); - emit_aarch64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); + emit_aarch64_cast_eval_arg( + module, + emitter, + param_ty, + &label_prefix, + fail_label, + data, + callable_support, + ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); emitter.instruction(&format!("b {}", done_label)); // skip default materialization after an explicit argument emitter.label(&default_label); @@ -800,7 +923,15 @@ fn emit_aarch64_prepare_constructor_args( } else { emit_aarch64_load_eval_arg(module, emitter, index); let label_prefix = format!("{}_arg_{}", body_label, index); - emit_aarch64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); + emit_aarch64_cast_eval_arg( + module, + emitter, + param_ty, + &label_prefix, + fail_label, + data, + callable_support, + ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); @@ -821,17 +952,21 @@ fn emit_aarch64_prepare_constructor_args( fn emit_x86_64_prepare_constructor_args( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, slot: &EvalConstructorSlot, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> (usize, Vec) { let body_label = constructor_body_label(module, slot); let ref_slots = emit_x86_64_constructor_ref_arg_cells( module, emitter, + data, &slot.params, &slot.ref_params, &body_label, fail_label, + callable_support, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); @@ -847,7 +982,15 @@ fn emit_x86_64_prepare_constructor_args( emitter.instruction(&format!("jz {}", default_label)); // omitted SplFixedArray size uses PHP's zero default emit_x86_64_load_eval_arg(module, emitter, index); let label_prefix = format!("{}_arg_{}", body_label, index); - emit_x86_64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); + emit_x86_64_cast_eval_arg( + module, + emitter, + param_ty, + &label_prefix, + fail_label, + data, + callable_support, + ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); emitter.instruction(&format!("jmp {}", done_label)); // skip default materialization after an explicit argument emitter.label(&default_label); @@ -864,7 +1007,15 @@ fn emit_x86_64_prepare_constructor_args( } else { emit_x86_64_load_eval_arg(module, emitter, index); let label_prefix = format!("{}_arg_{}", body_label, index); - emit_x86_64_cast_eval_arg(module, emitter, param_ty, &label_prefix, fail_label); + emit_x86_64_cast_eval_arg( + module, + emitter, + param_ty, + &label_prefix, + fail_label, + data, + callable_support, + ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); @@ -900,10 +1051,12 @@ fn materialize_constructor_args( fn emit_aarch64_constructor_ref_arg_cells( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, param_types: &[PhpType], ref_params: &[bool], label_prefix: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> Vec { let ref_slots = eval_ref_arg_slots(param_types, ref_params, true); for slot in &ref_slots { @@ -915,7 +1068,15 @@ fn emit_aarch64_constructor_ref_arg_cells( abi::emit_push_result_value(emitter, &PhpType::Mixed); } else { let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); - emit_aarch64_cast_eval_arg(module, emitter, &slot.param_ty, &arg_label, fail_label); + emit_aarch64_cast_eval_arg( + module, + emitter, + &slot.param_ty, + &arg_label, + fail_label, + data, + callable_support, + ); abi::emit_push_result_value(emitter, &slot.param_ty); } } @@ -926,10 +1087,12 @@ fn emit_aarch64_constructor_ref_arg_cells( fn emit_x86_64_constructor_ref_arg_cells( module: &Module, emitter: &mut Emitter, + data: &mut DataSection, param_types: &[PhpType], ref_params: &[bool], label_prefix: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> Vec { let ref_slots = eval_ref_arg_slots(param_types, ref_params, true); for slot in &ref_slots { @@ -941,7 +1104,15 @@ fn emit_x86_64_constructor_ref_arg_cells( abi::emit_push_result_value(emitter, &PhpType::Mixed); } else { let arg_label = format!("{}_ref_arg_{}", label_prefix, slot.param_index); - emit_x86_64_cast_eval_arg(module, emitter, &slot.param_ty, &arg_label, fail_label); + emit_x86_64_cast_eval_arg( + module, + emitter, + &slot.param_ty, + &arg_label, + fail_label, + data, + callable_support, + ); abi::emit_push_result_value(emitter, &slot.param_ty); } } @@ -981,6 +1152,8 @@ fn emit_aarch64_cast_eval_arg( param_ty: &PhpType, label_prefix: &str, fail_label: &str, + data: &mut DataSection, + callable_support: &EvalCallableDescriptorSupport, ) { match param_ty.codegen_repr() { PhpType::Int => { @@ -999,6 +1172,16 @@ fn emit_aarch64_cast_eval_arg( emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 } + PhpType::Callable => { + super::eval_callable_helpers::emit_aarch64_cast_eval_callable_arg( + module, + emitter, + data, + callable_support, + label_prefix, + fail_label, + ); + } PhpType::TaggedScalar => { emit_aarch64_cast_eval_tagged_scalar_arg(emitter, label_prefix); } @@ -1125,6 +1308,8 @@ fn emit_x86_64_cast_eval_arg( param_ty: &PhpType, label_prefix: &str, fail_label: &str, + data: &mut DataSection, + callable_support: &EvalCallableDescriptorSupport, ) { match param_ty.codegen_repr() { PhpType::Int => { @@ -1143,6 +1328,16 @@ fn emit_x86_64_cast_eval_arg( emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair } + PhpType::Callable => { + super::eval_callable_helpers::emit_x86_64_cast_eval_callable_arg( + module, + emitter, + data, + callable_support, + label_prefix, + fail_label, + ); + } PhpType::TaggedScalar => { emit_x86_64_cast_eval_tagged_scalar_arg(emitter, label_prefix); } diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 5bff70b9a9..2eb92c205d 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -29,6 +29,7 @@ use super::eval_ref_arg_helpers::{ eval_normalized_ref_params, eval_ref_arg_slots, eval_signature_ref_params_supported, emit_aarch64_write_back_ref_args, emit_x86_64_write_back_ref_args, }; +use super::eval_callable_helpers::EvalCallableDescriptorSupport; /// Method metadata needed by eval method-call bridge dispatch. #[derive(Clone)] @@ -90,6 +91,7 @@ pub(super) fn emit_eval_method_helpers( module: &Module, emitter: &mut Emitter, data: &mut DataSection, + callable_support: &EvalCallableDescriptorSupport, ) { if !module_uses_eval(module) { return; @@ -97,8 +99,15 @@ pub(super) fn emit_eval_method_helpers( let slots = collect_eval_method_slots(module); let static_slots = collect_eval_static_method_slots(module); let builtin_throwable_class_ids = collect_builtin_throwable_method_class_ids(module); - emit_method_call_helper(module, emitter, data, &slots, &builtin_throwable_class_ids); - emit_static_method_call_helper(module, emitter, data, &static_slots); + emit_method_call_helper( + module, + emitter, + data, + &slots, + &builtin_throwable_class_ids, + callable_support, + ); + emit_static_method_call_helper(module, emitter, data, &static_slots, callable_support); } /// Returns true when the EIR module contains a function that can call eval. @@ -378,6 +387,7 @@ fn method_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Callable | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Iterable @@ -396,6 +406,7 @@ fn method_return_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Callable | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Union(_) @@ -413,16 +424,31 @@ fn emit_method_call_helper( data: &mut DataSection, slots: &[EvalMethodSlot], builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, ) { emitter.blank(); emitter.comment("--- eval bridge: user method call ---"); label_c_global(module, emitter, "__elephc_eval_value_method_call"); match module.target.arch { Arch::AArch64 => { - emit_method_call_aarch64(module, emitter, data, slots, builtin_throwable_class_ids) + emit_method_call_aarch64( + module, + emitter, + data, + slots, + builtin_throwable_class_ids, + callable_support, + ) } Arch::X86_64 => { - emit_method_call_x86_64(module, emitter, data, slots, builtin_throwable_class_ids) + emit_method_call_x86_64( + module, + emitter, + data, + slots, + builtin_throwable_class_ids, + callable_support, + ) } } } @@ -433,13 +459,18 @@ fn emit_static_method_call_helper( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalStaticMethodSlot], + callable_support: &EvalCallableDescriptorSupport, ) { emitter.blank(); emitter.comment("--- eval bridge: user static method call ---"); label_c_global(module, emitter, "__elephc_eval_value_static_method_call"); match module.target.arch { - Arch::AArch64 => emit_static_method_call_aarch64(module, emitter, data, slots), - Arch::X86_64 => emit_static_method_call_x86_64(module, emitter, data, slots), + Arch::AArch64 => { + emit_static_method_call_aarch64(module, emitter, data, slots, callable_support) + } + Arch::X86_64 => { + emit_static_method_call_x86_64(module, emitter, data, slots, callable_support) + } } } @@ -449,6 +480,7 @@ fn emit_static_method_call_aarch64( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalStaticMethodSlot], + callable_support: &EvalCallableDescriptorSupport, ) { let fail_label = "__elephc_eval_value_static_method_call_fail"; let done_label = "__elephc_eval_value_static_method_call_done"; @@ -464,7 +496,15 @@ fn emit_static_method_call_aarch64( emitter.instruction("str x6, [sp, #48]"); // save the active eval class-scope length emit_aarch64_static_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("b {}", fail_label)); // no supported static method matched the request - emit_aarch64_static_method_bodies(module, emitter, data, slots, done_label, fail_label); + emit_aarch64_static_method_bodies( + module, + emitter, + data, + slots, + done_label, + fail_label, + callable_support, + ); emitter.label(fail_label); emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -479,6 +519,7 @@ fn emit_static_method_call_x86_64( emitter: &mut Emitter, data: &mut DataSection, slots: &[EvalStaticMethodSlot], + callable_support: &EvalCallableDescriptorSupport, ) { let fail_label = "__elephc_eval_value_static_method_call_fail_x"; let done_label = "__elephc_eval_value_static_method_call_done_x"; @@ -495,7 +536,15 @@ fn emit_static_method_call_x86_64( emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the active eval class-scope length emit_x86_64_static_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("jmp {}", fail_label)); // no supported static method matched the request - emit_x86_64_static_method_bodies(module, emitter, data, slots, done_label, fail_label); + emit_x86_64_static_method_bodies( + module, + emitter, + data, + slots, + done_label, + fail_label, + callable_support, + ); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -511,6 +560,7 @@ fn emit_method_call_aarch64( data: &mut DataSection, slots: &[EvalMethodSlot], builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, ) { let fail_label = "__elephc_eval_value_method_call_fail"; let done_label = "__elephc_eval_value_method_call_done"; @@ -536,7 +586,15 @@ fn emit_method_call_aarch64( emit_aarch64_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("b {}", fail_label)); // no supported method matched the request emit_aarch64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); - emit_aarch64_method_bodies(module, emitter, data, slots, done_label, fail_label); + emit_aarch64_method_bodies( + module, + emitter, + data, + slots, + done_label, + fail_label, + callable_support, + ); emitter.label(fail_label); emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -552,6 +610,7 @@ fn emit_method_call_x86_64( data: &mut DataSection, slots: &[EvalMethodSlot], builtin_throwable_class_ids: &[u64], + callable_support: &EvalCallableDescriptorSupport, ) { let fail_label = "__elephc_eval_value_method_call_fail_x"; let done_label = "__elephc_eval_value_method_call_done_x"; @@ -579,7 +638,15 @@ fn emit_method_call_x86_64( emit_x86_64_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("jmp {}", fail_label)); // no supported method matched the request emit_x86_64_builtin_throwable_method_bodies(module, emitter, done_label, fail_label); - emit_x86_64_method_bodies(module, emitter, data, slots, done_label, fail_label); + emit_x86_64_method_bodies( + module, + emitter, + data, + slots, + done_label, + fail_label, + callable_support, + ); emitter.label(fail_label); emitter.instruction("xor eax, eax"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); @@ -1088,12 +1155,20 @@ fn emit_aarch64_method_bodies( slots: &[EvalMethodSlot], done_label: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { for slot in slots { emitter.label(&method_body_label(module, slot)); emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); let (overflow_bytes, ref_slots) = - emit_aarch64_prepare_method_args(module, emitter, data, slot, fail_label); + emit_aarch64_prepare_method_args( + module, + emitter, + data, + slot, + fail_label, + callable_support, + ); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1122,12 +1197,20 @@ fn emit_x86_64_method_bodies( slots: &[EvalMethodSlot], done_label: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { for slot in slots { emitter.label(&method_body_label(module, slot)); emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); let (overflow_bytes, ref_slots) = - emit_x86_64_prepare_method_args(module, emitter, data, slot, fail_label); + emit_x86_64_prepare_method_args( + module, + emitter, + data, + slot, + fail_label, + callable_support, + ); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1156,12 +1239,20 @@ fn emit_aarch64_static_method_bodies( slots: &[EvalStaticMethodSlot], done_label: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { for slot in slots { emitter.label(&static_method_body_label(module, slot)); emit_aarch64_validate_static_method_arg_count(module, emitter, slot, fail_label); let (overflow_bytes, ref_slots) = - emit_aarch64_prepare_static_method_args(module, emitter, data, slot, fail_label); + emit_aarch64_prepare_static_method_args( + module, + emitter, + data, + slot, + fail_label, + callable_support, + ); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1189,12 +1280,20 @@ fn emit_x86_64_static_method_bodies( slots: &[EvalStaticMethodSlot], done_label: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { for slot in slots { emitter.label(&static_method_body_label(module, slot)); emit_x86_64_validate_static_method_arg_count(module, emitter, slot, fail_label); let (overflow_bytes, ref_slots) = - emit_x86_64_prepare_static_method_args(module, emitter, data, slot, fail_label); + emit_x86_64_prepare_static_method_args( + module, + emitter, + data, + slot, + fail_label, + callable_support, + ); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1281,6 +1380,7 @@ fn emit_aarch64_prepare_method_args( data: &mut DataSection, slot: &EvalMethodSlot, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> (usize, Vec) { let body_label = method_body_label(module, slot); let ref_slots = emit_aarch64_ref_arg_cells( @@ -1292,6 +1392,7 @@ fn emit_aarch64_prepare_method_args( 24, &body_label, fail_label, + callable_support, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); @@ -1309,7 +1410,15 @@ fn emit_aarch64_prepare_method_args( } else { emit_aarch64_load_eval_arg(module, emitter, index, 24); let label_prefix = format!("{}_arg_{}", body_label, index); - emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); + emit_aarch64_cast_eval_arg( + module, + emitter, + data, + param_ty, + &label_prefix, + fail_label, + callable_support, + ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); @@ -1333,6 +1442,7 @@ fn emit_aarch64_prepare_static_method_args( data: &mut DataSection, slot: &EvalStaticMethodSlot, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> (usize, Vec) { let body_label = static_method_body_label(module, slot); let ref_slots = emit_aarch64_ref_arg_cells( @@ -1344,6 +1454,7 @@ fn emit_aarch64_prepare_static_method_args( 40, &body_label, fail_label, + callable_support, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); abi::emit_load_int_immediate(emitter, "x0", slot.class_id as i64); @@ -1360,7 +1471,15 @@ fn emit_aarch64_prepare_static_method_args( } else { emit_aarch64_load_eval_arg(module, emitter, index, 40); let label_prefix = format!("{}_arg_{}", body_label, index); - emit_aarch64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); + emit_aarch64_cast_eval_arg( + module, + emitter, + data, + param_ty, + &label_prefix, + fail_label, + callable_support, + ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); @@ -1375,6 +1494,7 @@ fn emit_x86_64_prepare_method_args( data: &mut DataSection, slot: &EvalMethodSlot, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> (usize, Vec) { let body_label = method_body_label(module, slot); let ref_slots = emit_x86_64_ref_arg_cells( @@ -1385,6 +1505,7 @@ fn emit_x86_64_prepare_method_args( &slot.ref_params, &body_label, fail_label, + callable_support, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); @@ -1402,7 +1523,15 @@ fn emit_x86_64_prepare_method_args( } else { emit_x86_64_load_eval_arg(module, emitter, index); let label_prefix = format!("{}_arg_{}", body_label, index); - emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); + emit_x86_64_cast_eval_arg( + module, + emitter, + data, + param_ty, + &label_prefix, + fail_label, + callable_support, + ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); @@ -1426,6 +1555,7 @@ fn emit_x86_64_prepare_static_method_args( data: &mut DataSection, slot: &EvalStaticMethodSlot, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> (usize, Vec) { let body_label = static_method_body_label(module, slot); let ref_slots = emit_x86_64_ref_arg_cells( @@ -1436,6 +1566,7 @@ fn emit_x86_64_prepare_static_method_args( &slot.ref_params, &body_label, fail_label, + callable_support, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); abi::emit_load_int_immediate(emitter, "rax", slot.class_id as i64); @@ -1452,7 +1583,15 @@ fn emit_x86_64_prepare_static_method_args( } else { emit_x86_64_load_eval_arg(module, emitter, index); let label_prefix = format!("{}_arg_{}", body_label, index); - emit_x86_64_cast_eval_arg(module, emitter, data, param_ty, &label_prefix, fail_label); + emit_x86_64_cast_eval_arg( + module, + emitter, + data, + param_ty, + &label_prefix, + fail_label, + callable_support, + ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); @@ -1498,6 +1637,7 @@ fn emit_aarch64_ref_arg_cells( arg_array_frame_offset: usize, label_prefix: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> Vec { let ref_slots = eval_ref_arg_slots(param_types, ref_params, false); for slot in &ref_slots { @@ -1516,6 +1656,7 @@ fn emit_aarch64_ref_arg_cells( &slot.param_ty, &arg_label, fail_label, + callable_support, ); abi::emit_push_result_value(emitter, &slot.param_ty); } @@ -1532,6 +1673,7 @@ fn emit_x86_64_ref_arg_cells( ref_params: &[bool], label_prefix: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) -> Vec { let ref_slots = eval_ref_arg_slots(param_types, ref_params, false); for slot in &ref_slots { @@ -1550,6 +1692,7 @@ fn emit_x86_64_ref_arg_cells( &slot.param_ty, &arg_label, fail_label, + callable_support, ); abi::emit_push_result_value(emitter, &slot.param_ty); } @@ -1626,6 +1769,7 @@ fn emit_aarch64_cast_eval_arg( param_ty: &PhpType, label_prefix: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { match param_ty.codegen_repr() { PhpType::Int => { @@ -1644,6 +1788,16 @@ fn emit_aarch64_cast_eval_arg( emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for string coercion emitter.instruction("bl __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair in x1/x2 } + PhpType::Callable => { + super::eval_callable_helpers::emit_aarch64_cast_eval_callable_arg( + module, + emitter, + data, + callable_support, + label_prefix, + fail_label, + ); + } PhpType::TaggedScalar => { emit_aarch64_cast_eval_tagged_scalar_arg(emitter, label_prefix); } @@ -1781,6 +1935,7 @@ fn emit_x86_64_cast_eval_arg( param_ty: &PhpType, label_prefix: &str, fail_label: &str, + callable_support: &EvalCallableDescriptorSupport, ) { match param_ty.codegen_repr() { PhpType::Int => { @@ -1799,6 +1954,16 @@ fn emit_x86_64_cast_eval_arg( emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for string coercion emitter.instruction("call __rt_mixed_cast_string"); // coerce the eval argument to a PHP string pair } + PhpType::Callable => { + super::eval_callable_helpers::emit_x86_64_cast_eval_callable_arg( + module, + emitter, + data, + callable_support, + label_prefix, + fail_label, + ); + } PhpType::TaggedScalar => { emit_x86_64_cast_eval_tagged_scalar_arg(emitter, label_prefix); } diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 1438ebc1bf..fad08b38c6 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -15,6 +15,7 @@ use crate::codegen::{ emit_box_current_value_as_mixed, emit_box_runtime_payload_as_mixed, runtime, runtime_value_tag, }; +use crate::codegen::context::Context as LegacyContext; use crate::codegen_support::try_handlers::{ TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, }; @@ -26,7 +27,9 @@ use crate::ir::{ use crate::names::{ function_symbol, ir_global_symbol, method_symbol, php_symbol_key, static_method_symbol, }; -use crate::types::{callable_wrapper_sig, first_class_callable_builtin_sig, FunctionSig, PhpType}; +use crate::types::{ + callable_wrapper_sig, first_class_callable_builtin_sig, ExternFunctionSig, FunctionSig, PhpType, +}; use super::context::FunctionContext; use super::function_variants; @@ -1579,6 +1582,52 @@ fn emit_runtime_call_wrapper_inline( Ok(label) } +/// Builds the legacy metadata context needed by reused descriptor-invoker emitters. +pub(crate) fn legacy_context_from_eir_module(module: &crate::ir::Module) -> LegacyContext { + let mut ctx = LegacyContext::new(); + for function in module + .functions + .iter() + .filter(|function| !is_property_init_thunk_function(function)) + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + { + ctx.functions + .insert(function.name.clone(), function_signature_from_eir(function)); + } + ctx.function_variant_groups = super::function_variants::collect_dispatch_groups(module) + .into_iter() + .map(|group| group.name) + .collect(); + ctx.callable_param_sigs = module.callable_param_sigs.clone(); + for decl in &module.extern_decls { + ctx.extern_functions.insert( + decl.name.clone(), + ExternFunctionSig { + name: decl.name.clone(), + params: decl + .params + .iter() + .map(|param| (param.name.clone(), param.php_type.clone())) + .collect(), + return_type: decl.return_php_type.clone(), + library: decl.link_libs.first().cloned(), + }, + ); + } + ctx.classes = module.class_infos.clone(); + ctx.interfaces = module.interface_infos.clone(); + ctx.enums = module.enum_infos.clone(); + ctx.packed_classes = module.packed_class_infos.clone(); + ctx.extern_classes = module.extern_class_infos.clone(); + ctx +} + +/// Returns true for synthetic property-default init thunks, which are not PHP callables. +fn is_property_init_thunk_function(function: &crate::ir::Function) -> bool { + function.name.starts_with("_class_propinit_") +} + /// Builds the EIR body for a PHP-ABI wrapper around a builtin or extern call. fn build_runtime_call_wrapper_function( module: &mut Module, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index ff13a70250..a4915127bd 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -1247,11 +1247,11 @@ fn emit_branch_if_eval_c_int_negative(ctx: &mut FunctionContext<'_>, label: &str match ctx.emitter.target.arch { Arch::AArch64 => { let branch = format!("tbnz w0, #31, {}", label); - ctx.emitter.instruction(&branch); // branch when the C int result is the invalid-target sentinel + ctx.emitter.instruction(&branch); // branch when the C int result is the invalid-target sentinel } Arch::X86_64 => { - ctx.emitter.instruction("test eax, eax"); // set flags from the C int result - ctx.emitter.instruction(&format!("js {}", label)); // branch when the C int result is the invalid-target sentinel + ctx.emitter.instruction("test eax, eax"); // set flags from the C int result + ctx.emitter.instruction(&format!("js {}", label)); // branch when the C int result is the invalid-target sentinel } } } @@ -2000,6 +2000,7 @@ fn eval_native_method_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Callable | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Iterable @@ -2017,6 +2018,7 @@ fn eval_native_constructor_param_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Callable | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Iterable @@ -2035,6 +2037,7 @@ fn eval_native_method_return_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Callable | PhpType::TaggedScalar | PhpType::Mixed | PhpType::Union(_) diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index e58f1b473d..8c21f1fd73 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -11,6 +11,7 @@ mod block_emit; pub(crate) mod context; +mod eval_callable_helpers; mod eval_class_constant_helpers; mod eval_constructor_helpers; mod eval_method_helpers; @@ -206,8 +207,26 @@ fn finalize_user_asm( &mut emitter, &mut data, ); - eval_constructor_helpers::emit_eval_constructor_helpers(module, &mut emitter, &mut data); - eval_method_helpers::emit_eval_method_helpers(module, &mut emitter, &mut data); + let eval_callable_support_needed = + eval_callable_helpers::module_needs_eval_callable_descriptor_support(module); + let eval_callable_support = eval_callable_helpers::emit_eval_callable_descriptor_support( + module, + &mut emitter, + &mut data, + eval_callable_support_needed, + ); + eval_constructor_helpers::emit_eval_constructor_helpers( + module, + &mut emitter, + &mut data, + &eval_callable_support, + ); + eval_method_helpers::emit_eval_method_helpers( + module, + &mut emitter, + &mut data, + &eval_callable_support, + ); eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); eval_reflection_owner_helpers::emit_eval_reflection_owner_helpers(module, &mut emitter); let data_output = data.emit(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 15c7f5f779..86eea2ddf0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6631,6 +6631,45 @@ echo (new EvalAotCallableArrayNamedBox())->run(); assert_eq!(out.stdout, "AB:CD:EF"); } +/// Verifies eval can pass runtime PHP callbacks to generated/AOT callable-typed methods. +#[test] +fn test_eval_fragment_passes_callable_args_to_aot_methods() { + let out = compile_and_run_capture( + r#"value = $callback("C"); + } + + public function apply(callable $callback) { + return $callback("M"); + } + + public static function applyStatic(callable $callback) { + return $callback("S"); + } +} + +echo eval('$box = new EvalAotCallableArgBox("eval_aot_callable_arg_suffix"); +return $box->value . ":" . + $box->apply("eval_aot_callable_arg_suffix") . ":" . + EvalAotCallableArgBox::applyStatic("eval_aot_callable_arg_suffix");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "C!:M!:S!"); +} + /// Verifies eval static calls and static callables dispatch public AOT static methods. #[test] fn test_eval_fragment_dispatches_aot_static_methods() { From 95cf2a7cfc87778ef50214eaea2fd9cbf3cf1eef Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 09:56:10 +0200 Subject: [PATCH 0845/1208] Support eval static callable array args --- src/codegen/eval_callable_helpers.rs | 345 ++++++++++++++++++++++++++- tests/codegen/eval.rs | 14 +- 2 files changed, 348 insertions(+), 11 deletions(-) diff --git a/src/codegen/eval_callable_helpers.rs b/src/codegen/eval_callable_helpers.rs index c887d959f5..15d5c5d5ec 100644 --- a/src/codegen/eval_callable_helpers.rs +++ b/src/codegen/eval_callable_helpers.rs @@ -10,30 +10,45 @@ //! Key details: //! - Eval stores PHP callbacks as boxed runtime values, while AOT callable //! parameters expect descriptor pointers. -//! - This bridge slice currently accepts string callables and selects static -//! descriptors with uniform invokers generated by the existing callable -//! dispatch machinery. +//! - String callables and static callable arrays select static descriptors with +//! uniform invokers generated by the existing callable dispatch machinery. use crate::codegen::abi; use crate::codegen::callable_descriptor::{ self, CallableDescriptorInvocation, CallableDescriptorShape, }; -use crate::codegen::callable_dispatch::{self, RuntimeCallableCase, RuntimeCallableSelector}; +use crate::codegen::callable_dispatch::{ + self, RuntimeCallableCase, RuntimeCallableSelector, RuntimeStaticMethodCallableCase, +}; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::ir::{Function, LocalKind, Module}; use crate::names::function_symbol; +use crate::parser::ast::Visibility; use crate::types::{callable_wrapper_sig, PhpType}; +const MIXED_METHOD_TAG_OFFSET: usize = 0; +const MIXED_METHOD_PAYLOAD_OFFSET: usize = 16; +const MIXED_RECEIVER_TAG_OFFSET: usize = 32; +const MIXED_RECEIVER_PAYLOAD_OFFSET: usize = 48; +const MIXED_SELECTOR_BYTES: usize = 64; +const MIXED_TAG_STRING: i64 = 1; + /// Callable descriptors available to eval constructor and method bridges. pub(super) struct EvalCallableDescriptorSupport { - cases: Vec, + string_cases: Vec, + static_array_cases: Vec, } impl EvalCallableDescriptorSupport { /// Returns true when no callable descriptor case is available. pub(super) fn is_empty(&self) -> bool { - self.cases.is_empty() + self.string_cases.is_empty() && self.static_array_cases.is_empty() + } + + /// Returns true when no static callable-array descriptor case is available. + fn static_array_cases_empty(&self) -> bool { + self.static_array_cases.is_empty() } } @@ -97,7 +112,10 @@ pub(super) fn emit_eval_callable_descriptor_support( needed: bool, ) -> EvalCallableDescriptorSupport { if !needed { - return EvalCallableDescriptorSupport { cases: Vec::new() }; + return EvalCallableDescriptorSupport { + string_cases: Vec::new(), + static_array_cases: Vec::new(), + }; } let mut legacy_ctx = super::lower_inst::legacy_context_from_eir_module(module); legacy_ctx.functions.retain(|name, _| { @@ -106,9 +124,13 @@ pub(super) fn emit_eval_callable_descriptor_support( .iter() .any(|function| !function.flags.is_main && function.name == *name) }); - let cases = eval_user_function_callable_cases(&mut legacy_ctx, data); + let string_cases = eval_user_function_callable_cases(&mut legacy_ctx, data); + let static_array_cases = eval_static_method_callable_cases(module, &mut legacy_ctx, data); emit_deferred_callable_support(emitter, data, &mut legacy_ctx); - EvalCallableDescriptorSupport { cases } + EvalCallableDescriptorSupport { + string_cases, + static_array_cases, + } } /// Builds descriptor cases for user functions visible as eval string callables. @@ -151,6 +173,53 @@ fn eval_user_function_callable_cases( cases } +/// Builds descriptor cases for emitted public static methods visible to eval. +fn eval_static_method_callable_cases( + module: &Module, + legacy_ctx: &mut crate::codegen::context::Context, + data: &mut DataSection, +) -> Vec { + let emitted_methods = super::eir_class_method_keys(module); + let mut candidates = Vec::new(); + for (class_name, class_info) in &module.class_infos { + for method_name in class_info.static_methods.keys() { + if !class_info + .static_method_visibilities + .get(method_name) + .is_some_and(|visibility| matches!(visibility, Visibility::Public)) + { + continue; + } + let method_key = crate::names::php_symbol_key(method_name); + let impl_class = class_info + .static_method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + if emitted_methods.contains(&(impl_class.to_string(), method_key, true)) { + candidates.push((class_name.clone(), method_name.clone())); + } + } + } + candidates.sort_by(|left, right| (&left.0, &left.1).cmp(&(&right.0, &right.1))); + candidates + .into_iter() + .filter_map(|(class_name, method_name)| { + callable_dispatch::runtime_static_method_case( + legacy_ctx, + data, + &class_name, + &method_name, + ) + .map(|case| RuntimeStaticMethodCallableCase { + class_name, + method_name, + case, + }) + }) + .collect() +} + /// Emits deferred callable support bodies behind one jump. fn emit_deferred_callable_support( emitter: &mut Emitter, @@ -181,10 +250,15 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( fail_label: &str, ) { let string_label = format!("{}_callable_string", label_prefix); + let array_label = format!("{}_callable_array", label_prefix); emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for callable validation emitter.instruction("bl __rt_mixed_unbox"); // expose the eval callable tag and payload words emitter.instruction("cmp x0, #1"); // runtime tag 1 means a string callable name emitter.instruction(&format!("b.eq {}", string_label)); // resolve string callables through descriptor metadata + emitter.instruction("cmp x0, #4"); // runtime tag 4 means an indexed callable array + emitter.instruction(&format!("b.eq {}", array_label)); // resolve static callable arrays through descriptor metadata + emitter.instruction("cmp x0, #5"); // runtime tag 5 means an associative callable array + emitter.instruction(&format!("b.eq {}", array_label)); // resolve static callable arrays with numeric keys abi::emit_jump(emitter, fail_label); emitter.label(&string_label); abi::emit_push_reg_pair(emitter, "x1", "x2"); @@ -197,6 +271,18 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( fail_label, "x0", ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + emitter.label(&array_label); + emit_aarch64_eval_callable_array_selector_slots(module, emitter); + emit_eval_static_callable_array_descriptor_lookup( + emitter, + data, + support, + label_prefix, + fail_label, + "x0", + ); + emitter.label(&format!("{}_callable_cast_done", label_prefix)); } /// Converts the x86_64 boxed eval argument slot into a callable descriptor pointer. @@ -209,10 +295,15 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( fail_label: &str, ) { let string_label = format!("{}_callable_string", label_prefix); + let array_label = format!("{}_callable_array", label_prefix); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for callable validation emitter.instruction("call __rt_mixed_unbox"); // expose the eval callable tag and payload words emitter.instruction("cmp rax, 1"); // runtime tag 1 means a string callable name emitter.instruction(&format!("je {}", string_label)); // resolve string callables through descriptor metadata + emitter.instruction("cmp rax, 4"); // runtime tag 4 means an indexed callable array + emitter.instruction(&format!("je {}", array_label)); // resolve static callable arrays through descriptor metadata + emitter.instruction("cmp rax, 5"); // runtime tag 5 means an associative callable array + emitter.instruction(&format!("je {}", array_label)); // resolve static callable arrays with numeric keys abi::emit_jump(emitter, fail_label); emitter.label(&string_label); abi::emit_push_reg_pair(emitter, "rdi", "rdx"); @@ -225,6 +316,18 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( fail_label, "rax", ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + emitter.label(&array_label); + emit_x86_64_eval_callable_array_selector_slots(module, emitter); + emit_eval_static_callable_array_descriptor_lookup( + emitter, + data, + support, + label_prefix, + fail_label, + "rax", + ); + emitter.label(&format!("{}_callable_cast_done", label_prefix)); } /// Looks up a descriptor by the string callable currently saved on the temp stack. @@ -250,7 +353,11 @@ fn emit_eval_string_callable_descriptor_lookup( call_reg: result_reg, }; let mut legacy_ctx = super::lower_inst::legacy_context_from_eir_module(module); - for case in &support.cases { + for case in support + .string_cases + .iter() + .chain(support.static_array_cases.iter().map(|case| &case.case)) + { let next_case = legacy_ctx.next_label("eval_callable_next"); callable_dispatch::emit_branch_if_callable_case_mismatch( &selector, @@ -270,3 +377,221 @@ fn emit_eval_string_callable_descriptor_lookup( emitter.label(&done_label); abi::emit_release_temporary_stack(emitter, 16); } + +/// Saves the receiver and method slots from an ARM64 boxed eval callable array. +fn emit_aarch64_eval_callable_array_selector_slots(module: &Module, emitter: &mut Emitter) { + emit_aarch64_eval_callable_array_slot(module, emitter, 0); + emit_aarch64_push_mixed_unbox_payload(emitter); + emit_aarch64_eval_callable_array_slot(module, emitter, 1); + emit_aarch64_push_mixed_unbox_payload(emitter); +} + +/// Saves the receiver and method slots from an x86_64 boxed eval callable array. +fn emit_x86_64_eval_callable_array_selector_slots(module: &Module, emitter: &mut Emitter) { + emit_x86_64_eval_callable_array_slot(module, emitter, 0); + emit_x86_64_push_mixed_unbox_payload(emitter); + emit_x86_64_eval_callable_array_slot(module, emitter, 1); + emit_x86_64_push_mixed_unbox_payload(emitter); +} + +/// Loads and unboxes one ARM64 callable-array slot through eval's array reader. +fn emit_aarch64_eval_callable_array_slot(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "x0", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + emitter.instruction("str x0, [sp, #-16]!"); // preserve the boxed callable-array index + emitter.instruction("ldr x1, [sp]"); // pass the boxed selector index to eval's array reader + emitter.instruction("ldr x0, [x29, #-16]"); // pass the boxed callable array to eval's array reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("add sp, sp, #16"); // discard the temporary boxed selector index + emitter.instruction("bl __rt_mixed_unbox"); // expose the callable-array selector slot tag and payload +} + +/// Loads and unboxes one x86_64 callable-array slot through eval's array reader. +fn emit_x86_64_eval_callable_array_slot(module: &Module, emitter: &mut Emitter, index: usize) { + let value_int_symbol = module.target.extern_symbol("__elephc_eval_value_int"); + let array_get_symbol = module.target.extern_symbol("__elephc_eval_value_array_get"); + abi::emit_load_int_immediate(emitter, "rdi", index as i64); + abi::emit_call_label(emitter, &value_int_symbol); + abi::emit_push_reg(emitter, "rax"); + emitter.instruction("mov rsi, QWORD PTR [rsp]"); // pass the boxed selector index to eval's array reader + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // pass the boxed callable array to eval's array reader + abi::emit_call_label(emitter, &array_get_symbol); + emitter.instruction("add rsp, 16"); // discard the temporary boxed selector index + emitter.instruction("call __rt_mixed_unbox"); // expose the callable-array selector slot tag and payload +} + +/// Preserves the current ARM64 mixed-unbox tag and payload on the temp stack. +fn emit_aarch64_push_mixed_unbox_payload(emitter: &mut Emitter) { + abi::emit_push_reg_pair(emitter, "x1", "x2"); + abi::emit_push_reg(emitter, "x0"); +} + +/// Preserves the current x86_64 mixed-unbox tag and payload on the temp stack. +fn emit_x86_64_push_mixed_unbox_payload(emitter: &mut Emitter) { + abi::emit_push_reg_pair(emitter, "rdi", "rdx"); + abi::emit_push_reg(emitter, "rax"); +} + +/// Looks up a static-method descriptor from saved callable-array selector slots. +fn emit_eval_static_callable_array_descriptor_lookup( + emitter: &mut Emitter, + data: &mut DataSection, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, + result_reg: &str, +) { + let done_label = format!("{}_callable_array_done", label_prefix); + let miss_label = format!("{}_callable_array_missing", label_prefix); + if support.static_array_cases_empty() { + abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); + abi::emit_jump(emitter, fail_label); + return; + } + for (index, case) in support.static_array_cases.iter().enumerate() { + let next_case = format!("{}_callable_array_next_{}", label_prefix, index); + emit_branch_if_static_callable_array_case_mismatch( + emitter, + data, + case, + &next_case, + ); + abi::emit_symbol_address(emitter, result_reg, &case.case.descriptor_label); + abi::emit_jump(emitter, &done_label); + emitter.label(&next_case); + } + abi::emit_jump(emitter, &miss_label); + emitter.label(&miss_label); + abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); + abi::emit_jump(emitter, fail_label); + emitter.label(&done_label); + abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); +} + +/// Branches unless saved selector slots match one static callable-array case. +fn emit_branch_if_static_callable_array_case_mismatch( + emitter: &mut Emitter, + data: &mut DataSection, + case: &RuntimeStaticMethodCallableCase, + next_label: &str, +) { + emit_branch_if_stack_tag_mismatch(emitter, MIXED_RECEIVER_TAG_OFFSET, MIXED_TAG_STRING, next_label); + emit_branch_if_stack_tag_mismatch(emitter, MIXED_METHOD_TAG_OFFSET, MIXED_TAG_STRING, next_label); + emit_branch_if_static_class_string_mismatch( + emitter, + data, + MIXED_RECEIVER_PAYLOAD_OFFSET, + MIXED_RECEIVER_PAYLOAD_OFFSET + 8, + &case.class_name, + next_label, + ); + emit_branch_if_stack_string_mismatch( + emitter, + data, + MIXED_METHOD_PAYLOAD_OFFSET, + MIXED_METHOD_PAYLOAD_OFFSET + 8, + case.method_name.as_bytes(), + next_label, + ); +} + +/// Branches when a saved Mixed tag stack slot does not equal `expected_tag`. +fn emit_branch_if_stack_tag_mismatch( + emitter: &mut Emitter, + tag_offset: usize, + expected_tag: i64, + next_label: &str, +) { + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(emitter, "x9", tag_offset); + emitter.instruction(&format!("cmp x9, #{}", expected_tag)); // compare the callable-array selector runtime tag + emitter.instruction(&format!("b.ne {}", next_label)); // try the next callable-array target when the tag differs + } + crate::codegen::platform::Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(emitter, "r10", tag_offset); + emitter.instruction(&format!("cmp r10, {}", expected_tag)); // compare the callable-array selector runtime tag + emitter.instruction(&format!("jne {}", next_label)); // try the next callable-array target when the tag differs + } + } +} + +/// Branches when a saved stack string does not match the expected PHP name. +fn emit_branch_if_stack_string_mismatch( + emitter: &mut Emitter, + data: &mut DataSection, + ptr_offset: usize, + len_offset: usize, + expected: &[u8], + next_label: &str, +) { + let matched_label = format!("{}_matched", next_label); + emit_stack_string_compare_branch(emitter, data, ptr_offset, len_offset, expected, &matched_label); + abi::emit_jump(emitter, next_label); + emitter.label(&matched_label); +} + +/// Branches when a saved class string does not match bare or leading-slash forms. +fn emit_branch_if_static_class_string_mismatch( + emitter: &mut Emitter, + data: &mut DataSection, + ptr_offset: usize, + len_offset: usize, + class_name: &str, + next_label: &str, +) { + let matched_label = format!("{}_class_matched", next_label); + emit_stack_string_compare_branch( + emitter, + data, + ptr_offset, + len_offset, + class_name.as_bytes(), + &matched_label, + ); + let leading_slash = format!("\\{}", class_name); + emit_stack_string_compare_branch( + emitter, + data, + ptr_offset, + len_offset, + leading_slash.as_bytes(), + &matched_label, + ); + abi::emit_jump(emitter, next_label); + emitter.label(&matched_label); +} + +/// Compares a saved stack string with `expected` and branches on equality. +fn emit_stack_string_compare_branch( + emitter: &mut Emitter, + data: &mut DataSection, + ptr_offset: usize, + len_offset: usize, + expected: &[u8], + matched_label: &str, +) { + let (expected_label, expected_len) = data.add_string(expected); + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(emitter, "x1", ptr_offset); + abi::emit_load_temporary_stack_slot(emitter, "x2", len_offset); + abi::emit_symbol_address(emitter, "x3", &expected_label); + abi::emit_load_int_immediate(emitter, "x4", expected_len as i64); + abi::emit_call_label(emitter, "__rt_strcasecmp"); + emitter.instruction("cmp x0, #0"); // check whether the runtime method string matched + emitter.instruction(&format!("b.eq {}", matched_label)); // select this callable-array target when names match + } + crate::codegen::platform::Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(emitter, "rdi", ptr_offset); + abi::emit_load_temporary_stack_slot(emitter, "rsi", len_offset); + abi::emit_symbol_address(emitter, "rdx", &expected_label); + abi::emit_load_int_immediate(emitter, "rcx", expected_len as i64); + abi::emit_call_label(emitter, "__rt_strcasecmp"); + emitter.instruction("test rax, rax"); // check whether the runtime method string matched + emitter.instruction(&format!("je {}", matched_label)); // select this callable-array target when names match + } + } +} diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 86eea2ddf0..32ab7b3a58 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6640,6 +6640,12 @@ function eval_aot_callable_arg_suffix(string $value): string { return $value . "!"; } +class EvalAotCallableArgTarget { + public static function suffix(string $value): string { + return $value . "?"; + } +} + class EvalAotCallableArgBox { public $value = ""; @@ -6660,6 +6666,12 @@ echo eval('$box = new EvalAotCallableArgBox("eval_aot_callable_arg_suffix"); return $box->value . ":" . $box->apply("eval_aot_callable_arg_suffix") . ":" . EvalAotCallableArgBox::applyStatic("eval_aot_callable_arg_suffix");'); +echo ":"; +echo eval('$static = [EvalAotCallableArgTarget::class, "suffix"]; +$box = new EvalAotCallableArgBox($static); +return $box->value . ":" . + $box->apply("EvalAotCallableArgTarget::suffix") . ":" . + EvalAotCallableArgBox::applyStatic($static);'); "#, ); assert!( @@ -6667,7 +6679,7 @@ return $box->value . ":" . "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "C!:M!:S!"); + assert_eq!(out.stdout, "C!:M!:S!:C?:M?:S?"); } /// Verifies eval static calls and static callables dispatch public AOT static methods. From 33330a4441fa1d795c00334c6cae7336286b590a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 10:15:59 +0200 Subject: [PATCH 0846/1208] Support eval instance callable args --- src/codegen/eval_callable_helpers.rs | 508 ++++++++++++++++++++++++++- tests/codegen/eval.rs | 27 +- 2 files changed, 516 insertions(+), 19 deletions(-) diff --git a/src/codegen/eval_callable_helpers.rs b/src/codegen/eval_callable_helpers.rs index 15d5c5d5ec..4b30f7bf1f 100644 --- a/src/codegen/eval_callable_helpers.rs +++ b/src/codegen/eval_callable_helpers.rs @@ -18,37 +18,53 @@ use crate::codegen::callable_descriptor::{ self, CallableDescriptorInvocation, CallableDescriptorShape, }; use crate::codegen::callable_dispatch::{ - self, RuntimeCallableCase, RuntimeCallableSelector, RuntimeStaticMethodCallableCase, + self, RuntimeCallableCase, RuntimeCallableSelector, RuntimeInstanceCallableShape, + RuntimeInstanceMethodCallableCase, RuntimeStaticMethodCallableCase, }; +use crate::codegen::context::DeferredClosure; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::ir::{Function, LocalKind, Module}; -use crate::names::function_symbol; -use crate::parser::ast::Visibility; -use crate::types::{callable_wrapper_sig, PhpType}; +use crate::names::{function_symbol, php_symbol_key}; +use crate::parser::ast::{Expr, ExprKind, Stmt, StmtKind, Visibility}; +use crate::span::Span; +use crate::types::{callable_wrapper_sig, FunctionSig, PhpType}; +const EVAL_RECEIVER_CAPTURE_PARAM: &str = "__elephc_eval_callable_receiver"; const MIXED_METHOD_TAG_OFFSET: usize = 0; const MIXED_METHOD_PAYLOAD_OFFSET: usize = 16; const MIXED_RECEIVER_TAG_OFFSET: usize = 32; const MIXED_RECEIVER_PAYLOAD_OFFSET: usize = 48; const MIXED_SELECTOR_BYTES: usize = 64; +const SAVED_OBJECT_RECEIVER_BYTES: usize = 16; const MIXED_TAG_STRING: i64 = 1; +const MIXED_TAG_OBJECT: i64 = 6; /// Callable descriptors available to eval constructor and method bridges. pub(super) struct EvalCallableDescriptorSupport { string_cases: Vec, + instance_array_cases: Vec, static_array_cases: Vec, + object_cases: Vec, } impl EvalCallableDescriptorSupport { /// Returns true when no callable descriptor case is available. pub(super) fn is_empty(&self) -> bool { - self.string_cases.is_empty() && self.static_array_cases.is_empty() + self.string_cases.is_empty() + && self.instance_array_cases.is_empty() + && self.static_array_cases.is_empty() + && self.object_cases.is_empty() } - /// Returns true when no static callable-array descriptor case is available. - fn static_array_cases_empty(&self) -> bool { - self.static_array_cases.is_empty() + /// Returns true when no callable-array descriptor case is available. + fn array_cases_empty(&self) -> bool { + self.instance_array_cases.is_empty() && self.static_array_cases.is_empty() + } + + /// Returns true when no invokable-object descriptor case is available. + fn object_cases_empty(&self) -> bool { + self.object_cases.is_empty() } } @@ -114,7 +130,9 @@ pub(super) fn emit_eval_callable_descriptor_support( if !needed { return EvalCallableDescriptorSupport { string_cases: Vec::new(), + instance_array_cases: Vec::new(), static_array_cases: Vec::new(), + object_cases: Vec::new(), }; } let mut legacy_ctx = super::lower_inst::legacy_context_from_eir_module(module); @@ -125,11 +143,15 @@ pub(super) fn emit_eval_callable_descriptor_support( .any(|function| !function.flags.is_main && function.name == *name) }); let string_cases = eval_user_function_callable_cases(&mut legacy_ctx, data); + let instance_array_cases = eval_instance_method_callable_cases(module, &mut legacy_ctx, data); let static_array_cases = eval_static_method_callable_cases(module, &mut legacy_ctx, data); + let object_cases = eval_invokable_object_callable_cases(module, &mut legacy_ctx, data); emit_deferred_callable_support(emitter, data, &mut legacy_ctx); EvalCallableDescriptorSupport { string_cases, + instance_array_cases, static_array_cases, + object_cases, } } @@ -173,6 +195,93 @@ fn eval_user_function_callable_cases( cases } +/// Builds descriptor cases for emitted public instance methods visible to eval. +fn eval_instance_method_callable_cases( + module: &Module, + legacy_ctx: &mut crate::codegen::context::Context, + data: &mut DataSection, +) -> Vec { + eval_instance_callable_cases( + module, + legacy_ctx, + data, + RuntimeInstanceCallableShape::InstanceMethod, + |_| true, + ) +} + +/// Builds descriptor cases for emitted public `__invoke` methods visible to eval. +fn eval_invokable_object_callable_cases( + module: &Module, + legacy_ctx: &mut crate::codegen::context::Context, + data: &mut DataSection, +) -> Vec { + eval_instance_callable_cases( + module, + legacy_ctx, + data, + RuntimeInstanceCallableShape::ObjectInvoke, + |method_name| php_symbol_key(method_name) == "__invoke", + ) +} + +/// Builds receiver-bound descriptor cases for public emitted instance methods. +fn eval_instance_callable_cases( + module: &Module, + legacy_ctx: &mut crate::codegen::context::Context, + data: &mut DataSection, + shape: RuntimeInstanceCallableShape, + include_method: impl Fn(&str) -> bool, +) -> Vec { + let emitted_methods = super::eir_class_method_keys(module); + let mut candidates = Vec::new(); + for (class_name, class_info) in &module.class_infos { + for method_name in class_info.methods.keys() { + if !include_method(method_name) { + continue; + } + if !class_info + .method_visibilities + .get(method_name) + .is_some_and(|visibility| matches!(visibility, Visibility::Public)) + { + continue; + } + let method_key = php_symbol_key(method_name); + let impl_class = class_info + .method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + if emitted_methods.contains(&(impl_class.to_string(), method_key, false)) { + candidates.push(( + class_name.clone(), + class_info.class_id, + method_name.clone(), + )); + } + } + } + candidates.sort_by(|left, right| (&left.0, &left.2).cmp(&(&right.0, &right.2))); + candidates + .into_iter() + .filter_map(|(class_name, class_id, method_name)| { + receiver_bound_instance_method_case( + legacy_ctx, + data, + &class_name, + &method_name, + shape, + ) + .map(|case| RuntimeInstanceMethodCallableCase { + class_id, + method_name, + case, + }) + }) + .collect() +} + /// Builds descriptor cases for emitted public static methods visible to eval. fn eval_static_method_callable_cases( module: &Module, @@ -220,6 +329,152 @@ fn eval_static_method_callable_cases( .collect() } +/// Builds a receiver-bound descriptor case for one public instance method. +fn receiver_bound_instance_method_case( + legacy_ctx: &mut crate::codegen::context::Context, + data: &mut DataSection, + class_name: &str, + method_name: &str, + shape: RuntimeInstanceCallableShape, +) -> Option { + let (resolved_method_name, sig) = { + let class_info = legacy_ctx.classes.get(class_name)?; + let method_key = php_symbol_key(method_name); + let (resolved_method_name, sig) = class_info + .methods + .iter() + .find(|(candidate, _)| php_symbol_key(candidate) == method_key)?; + if !class_info + .method_visibilities + .get(resolved_method_name) + .is_some_and(|visibility| matches!(visibility, Visibility::Public)) + { + return None; + } + (resolved_method_name.clone(), sig.clone()) + }; + + let case_sig = callable_wrapper_sig(&sig); + let hidden_name = unique_hidden_param(EVAL_RECEIVER_CAPTURE_PARAM, &case_sig); + let capture_ty = PhpType::Object(class_name.to_string()); + let captures = vec![(hidden_name.clone(), capture_ty.clone(), false)]; + let hidden_params = vec![(hidden_name.clone(), capture_ty, false)]; + let wrapper_label = legacy_ctx.next_label("eval_callable_instance_method"); + let params = case_sig + .params + .iter() + .map(|(name, _)| name.clone()) + .collect::>(); + legacy_ctx.deferred_closures.push(DeferredClosure { + label: wrapper_label.clone(), + params, + body: receiver_bound_instance_method_wrapper_body( + &hidden_name, + &resolved_method_name, + &case_sig, + ), + sig: case_sig.clone(), + captures: captures.clone(), + hidden_params: hidden_params.clone(), + current_class: Some(class_name.to_string()), + needed: true, + }); + + let invoker_label = + callable_dispatch::ensure_runtime_descriptor_invoker(legacy_ctx, &hidden_params, &case_sig); + let (kind, invocation_shape) = match shape { + RuntimeInstanceCallableShape::ObjectInvoke => ( + callable_descriptor::CALLABLE_DESC_KIND_OBJECT_INVOKE, + CallableDescriptorShape::ObjectInvoke, + ), + RuntimeInstanceCallableShape::InstanceMethod => ( + callable_descriptor::CALLABLE_DESC_KIND_INSTANCE_METHOD, + CallableDescriptorShape::InstanceMethod, + ), + }; + let php_name = format!("{}::{}", class_name, resolved_method_name); + let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( + data, + &wrapper_label, + Some(&php_name), + kind, + Some(&case_sig), + &captures, + &hidden_params, + CallableDescriptorInvocation::method( + invocation_shape, + Some(class_name.to_string()), + resolved_method_name, + ), + invoker_label.as_deref(), + ); + Some(RuntimeCallableCase { + label: wrapper_label, + descriptor_label, + php_name: Some(php_name), + sig: case_sig, + captures, + has_invoker: invoker_label.is_some(), + invoker_label, + }) +} + +/// Builds the synthetic wrapper body for a receiver-bound eval callable descriptor. +fn receiver_bound_instance_method_wrapper_body( + receiver_param: &str, + method_name: &str, + sig: &FunctionSig, +) -> Vec { + let last_param_idx = sig.params.len().saturating_sub(1); + let args = sig + .params + .iter() + .enumerate() + .map(|(idx, (name, _))| { + let var = Expr::new(ExprKind::Variable(name.clone()), Span::dummy()); + if sig.variadic.is_some() && idx == last_param_idx { + Expr::new(ExprKind::Spread(Box::new(var)), Span::dummy()) + } else { + var + } + }) + .collect(); + let call = Expr::new( + ExprKind::MethodCall { + object: Box::new(Expr::new( + ExprKind::Variable(receiver_param.to_string()), + Span::dummy(), + )), + method: method_name.to_string(), + args, + }, + Span::dummy(), + ); + if sig.return_type == PhpType::Void { + vec![ + Stmt::new(StmtKind::ExprStmt(call), Span::dummy()), + Stmt::new(StmtKind::Return(None), Span::dummy()), + ] + } else { + vec![Stmt::new(StmtKind::Return(Some(call)), Span::dummy())] + } +} + +/// Returns a hidden receiver parameter name that cannot collide with visible parameters. +fn unique_hidden_param(base: &str, sig: &FunctionSig) -> String { + if !sig.params.iter().any(|(name, _)| name == base) { + return base.to_string(); + } + let mut index = 0usize; + loop { + let candidate = format!("{}_{}", base, index); + if !sig.params.iter().any(|(name, _)| name == &candidate) { + return candidate; + } + index += 1; + } +} + /// Emits deferred callable support bodies behind one jump. fn emit_deferred_callable_support( emitter: &mut Emitter, @@ -251,6 +506,7 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( ) { let string_label = format!("{}_callable_string", label_prefix); let array_label = format!("{}_callable_array", label_prefix); + let object_label = format!("{}_callable_object", label_prefix); emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for callable validation emitter.instruction("bl __rt_mixed_unbox"); // expose the eval callable tag and payload words emitter.instruction("cmp x0, #1"); // runtime tag 1 means a string callable name @@ -259,6 +515,8 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( emitter.instruction(&format!("b.eq {}", array_label)); // resolve static callable arrays through descriptor metadata emitter.instruction("cmp x0, #5"); // runtime tag 5 means an associative callable array emitter.instruction(&format!("b.eq {}", array_label)); // resolve static callable arrays with numeric keys + emitter.instruction("cmp x0, #6"); // runtime tag 6 means an invokable object candidate + emitter.instruction(&format!("b.eq {}", object_label)); // resolve invokable objects through descriptor metadata abi::emit_jump(emitter, fail_label); emitter.label(&string_label); abi::emit_push_reg_pair(emitter, "x1", "x2"); @@ -274,7 +532,7 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); emitter.label(&array_label); emit_aarch64_eval_callable_array_selector_slots(module, emitter); - emit_eval_static_callable_array_descriptor_lookup( + emit_eval_callable_array_descriptor_lookup( emitter, data, support, @@ -282,6 +540,16 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( fail_label, "x0", ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + emitter.label(&object_label); + abi::emit_push_reg(emitter, "x1"); + emit_eval_invokable_object_descriptor_lookup( + emitter, + support, + label_prefix, + fail_label, + "x0", + ); emitter.label(&format!("{}_callable_cast_done", label_prefix)); } @@ -296,6 +564,7 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( ) { let string_label = format!("{}_callable_string", label_prefix); let array_label = format!("{}_callable_array", label_prefix); + let object_label = format!("{}_callable_object", label_prefix); emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for callable validation emitter.instruction("call __rt_mixed_unbox"); // expose the eval callable tag and payload words emitter.instruction("cmp rax, 1"); // runtime tag 1 means a string callable name @@ -304,6 +573,8 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( emitter.instruction(&format!("je {}", array_label)); // resolve static callable arrays through descriptor metadata emitter.instruction("cmp rax, 5"); // runtime tag 5 means an associative callable array emitter.instruction(&format!("je {}", array_label)); // resolve static callable arrays with numeric keys + emitter.instruction("cmp rax, 6"); // runtime tag 6 means an invokable object candidate + emitter.instruction(&format!("je {}", object_label)); // resolve invokable objects through descriptor metadata abi::emit_jump(emitter, fail_label); emitter.label(&string_label); abi::emit_push_reg_pair(emitter, "rdi", "rdx"); @@ -319,7 +590,7 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); emitter.label(&array_label); emit_x86_64_eval_callable_array_selector_slots(module, emitter); - emit_eval_static_callable_array_descriptor_lookup( + emit_eval_callable_array_descriptor_lookup( emitter, data, support, @@ -327,6 +598,16 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( fail_label, "rax", ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + emitter.label(&object_label); + abi::emit_push_reg(emitter, "rdi"); + emit_eval_invokable_object_descriptor_lookup( + emitter, + support, + label_prefix, + fail_label, + "rax", + ); emitter.label(&format!("{}_callable_cast_done", label_prefix)); } @@ -434,8 +715,8 @@ fn emit_x86_64_push_mixed_unbox_payload(emitter: &mut Emitter) { abi::emit_push_reg(emitter, "rax"); } -/// Looks up a static-method descriptor from saved callable-array selector slots. -fn emit_eval_static_callable_array_descriptor_lookup( +/// Looks up a descriptor from saved callable-array selector slots. +fn emit_eval_callable_array_descriptor_lookup( emitter: &mut Emitter, data: &mut DataSection, support: &EvalCallableDescriptorSupport, @@ -445,11 +726,28 @@ fn emit_eval_static_callable_array_descriptor_lookup( ) { let done_label = format!("{}_callable_array_done", label_prefix); let miss_label = format!("{}_callable_array_missing", label_prefix); - if support.static_array_cases_empty() { + if support.array_cases_empty() { abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); abi::emit_jump(emitter, fail_label); return; } + for (index, case) in support.instance_array_cases.iter().enumerate() { + let next_case = format!("{}_callable_array_instance_next_{}", label_prefix, index); + emit_branch_if_instance_callable_array_case_mismatch( + emitter, + data, + case, + &next_case, + ); + emit_runtime_descriptor_with_saved_receiver_capture( + emitter, + &case.case.descriptor_label, + MIXED_RECEIVER_PAYLOAD_OFFSET, + result_reg, + ); + abi::emit_jump(emitter, &done_label); + emitter.label(&next_case); + } for (index, case) in support.static_array_cases.iter().enumerate() { let next_case = format!("{}_callable_array_next_{}", label_prefix, index); emit_branch_if_static_callable_array_case_mismatch( @@ -470,6 +768,81 @@ fn emit_eval_static_callable_array_descriptor_lookup( abi::emit_release_temporary_stack(emitter, MIXED_SELECTOR_BYTES); } +/// Looks up a descriptor from a saved invokable-object receiver. +fn emit_eval_invokable_object_descriptor_lookup( + emitter: &mut Emitter, + support: &EvalCallableDescriptorSupport, + label_prefix: &str, + fail_label: &str, + result_reg: &str, +) { + let done_label = format!("{}_callable_object_done", label_prefix); + let miss_label = format!("{}_callable_object_missing", label_prefix); + if support.object_cases_empty() { + abi::emit_release_temporary_stack(emitter, SAVED_OBJECT_RECEIVER_BYTES); + abi::emit_jump(emitter, fail_label); + return; + } + for (index, case) in support.object_cases.iter().enumerate() { + let next_case = format!("{}_callable_object_next_{}", label_prefix, index); + emit_branch_if_receiver_class_id_mismatch( + emitter, + case.class_id, + 0, + &next_case, + ); + emit_runtime_descriptor_with_saved_receiver_capture( + emitter, + &case.case.descriptor_label, + 0, + result_reg, + ); + abi::emit_jump(emitter, &done_label); + emitter.label(&next_case); + } + abi::emit_jump(emitter, &miss_label); + emitter.label(&miss_label); + abi::emit_release_temporary_stack(emitter, SAVED_OBJECT_RECEIVER_BYTES); + abi::emit_jump(emitter, fail_label); + emitter.label(&done_label); + abi::emit_release_temporary_stack(emitter, SAVED_OBJECT_RECEIVER_BYTES); +} + +/// Branches unless saved selector slots match one instance callable-array case. +fn emit_branch_if_instance_callable_array_case_mismatch( + emitter: &mut Emitter, + data: &mut DataSection, + case: &RuntimeInstanceMethodCallableCase, + next_label: &str, +) { + emit_branch_if_stack_tag_mismatch( + emitter, + MIXED_RECEIVER_TAG_OFFSET, + MIXED_TAG_OBJECT, + next_label, + ); + emit_branch_if_stack_tag_mismatch( + emitter, + MIXED_METHOD_TAG_OFFSET, + MIXED_TAG_STRING, + next_label, + ); + emit_branch_if_receiver_class_id_mismatch( + emitter, + case.class_id, + MIXED_RECEIVER_PAYLOAD_OFFSET, + next_label, + ); + emit_branch_if_stack_string_mismatch( + emitter, + data, + MIXED_METHOD_PAYLOAD_OFFSET, + MIXED_METHOD_PAYLOAD_OFFSET + 8, + case.method_name.as_bytes(), + next_label, + ); +} + /// Branches unless saved selector slots match one static callable-array case. fn emit_branch_if_static_callable_array_case_mismatch( emitter: &mut Emitter, @@ -477,8 +850,18 @@ fn emit_branch_if_static_callable_array_case_mismatch( case: &RuntimeStaticMethodCallableCase, next_label: &str, ) { - emit_branch_if_stack_tag_mismatch(emitter, MIXED_RECEIVER_TAG_OFFSET, MIXED_TAG_STRING, next_label); - emit_branch_if_stack_tag_mismatch(emitter, MIXED_METHOD_TAG_OFFSET, MIXED_TAG_STRING, next_label); + emit_branch_if_stack_tag_mismatch( + emitter, + MIXED_RECEIVER_TAG_OFFSET, + MIXED_TAG_STRING, + next_label, + ); + emit_branch_if_stack_tag_mismatch( + emitter, + MIXED_METHOD_TAG_OFFSET, + MIXED_TAG_STRING, + next_label, + ); emit_branch_if_static_class_string_mismatch( emitter, data, @@ -497,6 +880,92 @@ fn emit_branch_if_static_callable_array_case_mismatch( ); } +/// Branches when the saved receiver object's class id does not match `class_id`. +fn emit_branch_if_receiver_class_id_mismatch( + emitter: &mut Emitter, + class_id: u64, + receiver_offset: usize, + next_label: &str, +) { + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(emitter, "x9", receiver_offset); + emitter.instruction(&format!("cbz x9, {}", next_label)); // reject null receiver pointers before reading their class id + emitter.instruction("ldr x10, [x9]"); // load the receiver runtime class id from the object header + abi::emit_load_int_immediate(emitter, "x11", class_id as i64); + emitter.instruction("cmp x10, x11"); // compare receiver class id against this callable case + emitter.instruction(&format!("b.ne {}", next_label)); // try the next callable target when the receiver class differs + } + crate::codegen::platform::Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(emitter, "r10", receiver_offset); + emitter.instruction("test r10, r10"); // reject null receiver pointers before reading their class id + emitter.instruction(&format!("je {}", next_label)); // try the next callable target when the receiver pointer is null + emitter.instruction("mov r11, QWORD PTR [r10]"); // load the receiver runtime class id from the object header + abi::emit_load_int_immediate(emitter, "r10", class_id as i64); + emitter.instruction("cmp r11, r10"); // compare receiver class id against this callable case + emitter.instruction(&format!("jne {}", next_label)); // try the next callable target when the receiver class differs + } + } +} + +/// Allocates a runtime descriptor and captures the saved receiver object. +fn emit_runtime_descriptor_with_saved_receiver_capture( + emitter: &mut Emitter, + descriptor_label: &str, + receiver_offset: usize, + result_reg: &str, +) { + let total_bytes = callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + 16; + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => { + abi::emit_load_temporary_stack_slot(emitter, "x0", receiver_offset); + emitter.instruction("bl __rt_incref"); // retain the receiver for the runtime callable descriptor + abi::emit_push_reg(emitter, "x0"); + abi::emit_load_int_immediate(emitter, "x0", total_bytes as i64); + emitter.instruction("bl __rt_heap_alloc"); // allocate receiver-bound callable descriptor storage + callable_descriptor::emit_copy_static_descriptor_to_runtime( + emitter, + "x0", + descriptor_label, + ); + abi::emit_push_reg(emitter, "x0"); + abi::emit_load_temporary_stack_slot(emitter, "x11", 0); + abi::emit_load_temporary_stack_slot(emitter, "x0", 16); + callable_descriptor::emit_store_current_result_to_runtime_capture( + emitter, + "x11", + 0, + &PhpType::Object(String::new()), + ); + abi::emit_pop_reg(emitter, result_reg); + abi::emit_release_temporary_stack(emitter, 16); + } + crate::codegen::platform::Arch::X86_64 => { + abi::emit_load_temporary_stack_slot(emitter, "rax", receiver_offset); + emitter.instruction("call __rt_incref"); // retain the receiver for the runtime callable descriptor + abi::emit_push_reg(emitter, "rax"); + abi::emit_load_int_immediate(emitter, "rax", total_bytes as i64); + emitter.instruction("call __rt_heap_alloc"); // allocate receiver-bound callable descriptor storage + callable_descriptor::emit_copy_static_descriptor_to_runtime( + emitter, + "rax", + descriptor_label, + ); + abi::emit_push_reg(emitter, "rax"); + abi::emit_load_temporary_stack_slot(emitter, "rcx", 0); + abi::emit_load_temporary_stack_slot(emitter, "rax", 16); + callable_descriptor::emit_store_current_result_to_runtime_capture( + emitter, + "rcx", + 0, + &PhpType::Object(String::new()), + ); + abi::emit_pop_reg(emitter, result_reg); + abi::emit_release_temporary_stack(emitter, 16); + } + } +} + /// Branches when a saved Mixed tag stack slot does not equal `expected_tag`. fn emit_branch_if_stack_tag_mismatch( emitter: &mut Emitter, @@ -528,7 +997,14 @@ fn emit_branch_if_stack_string_mismatch( next_label: &str, ) { let matched_label = format!("{}_matched", next_label); - emit_stack_string_compare_branch(emitter, data, ptr_offset, len_offset, expected, &matched_label); + emit_stack_string_compare_branch( + emitter, + data, + ptr_offset, + len_offset, + expected, + &matched_label, + ); abi::emit_jump(emitter, next_label); emitter.label(&matched_label); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 32ab7b3a58..0c5dc68b9b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6635,7 +6635,7 @@ echo (new EvalAotCallableArrayNamedBox())->run(); #[test] fn test_eval_fragment_passes_callable_args_to_aot_methods() { let out = compile_and_run_capture( - r#"value . ":" . $box->apply("EvalAotCallableArgTarget::suffix") . ":" . EvalAotCallableArgBox::applyStatic($static);'); -"#, +echo ":"; +echo eval('$target = new EvalAotCallableArgTarget(); +$instance = [$target, "instanceSuffix"]; +$box = new EvalAotCallableArgBox($instance); +return $box->value . ":" . + $box->apply($instance) . ":" . + EvalAotCallableArgBox::applyStatic($instance);'); +echo ":"; +echo eval('$invokable = new EvalAotCallableArgTarget(); +$box = new EvalAotCallableArgBox($invokable); +return $box->value . ":" . + $box->apply($invokable) . ":" . + EvalAotCallableArgBox::applyStatic($invokable);'); +"##, ); assert!( out.success, "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "C!:M!:S!:C?:M?:S?"); + assert_eq!(out.stdout, "C!:M!:S!:C?:M?:S?:C~:M~:S~:C#:M#:S#"); } /// Verifies eval static calls and static callables dispatch public AOT static methods. From 59fb3690d6465c9efb90df560b33c0d911795173 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 10:27:25 +0200 Subject: [PATCH 0847/1208] Support eval AOT invokable object calls --- .../interpreter/builtins/registry/callable.rs | 14 +++++ .../src/interpreter/statements.rs | 38 ++++++++++++- tests/codegen/eval.rs | 56 ++++++++++++++++++- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 2ac07b15c2..bc26a7af32 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -118,6 +118,20 @@ pub(in crate::interpreter) fn eval_object_callable( ) -> Result { let identity = values.object_identity(callback)?; let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(callback, values)?; + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__invoke", + context, + values, + )? + else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if is_static || is_abstract { + return Err(EvalStatus::UnsupportedConstruct); + } return Ok(EvaluatedCallable::InvokableObject { object: callback }); }; let Some((_, method)) = context.class_method(class.name(), "__invoke") else { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 0fce913218..63cb46cdc7 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7031,8 +7031,28 @@ pub(in crate::interpreter) fn eval_invokable_object_call_result( return values.method_call(object, "__invoke", evaluated_args); }; let Some(class) = context.dynamic_object_class(identity) else { - let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; - return values.method_call(object, "__invoke", evaluated_args); + let class_name = runtime_object_class_name(object, values)?; + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__invoke", + context, + values, + )? + else { + return eval_throw_object_not_callable_error(&class_name, context, values); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + return eval_native_method_with_evaluated_args_unchecked( + object, + &class_name, + "__invoke", + evaluated_args, + context, + values, + ); }; let called_class_name = class.name().to_string(); let Some((declaring_class, method)) = context.class_method(&called_class_name, "__invoke") @@ -7063,6 +7083,20 @@ pub(in crate::interpreter) fn eval_invokable_object_precheck( return Ok(()); }; let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__invoke", + context, + values, + )? + else { + return eval_throw_object_not_callable_error(&class_name, context, values); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } return Ok(()); }; let called_class_name = class.name().to_string(); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0c5dc68b9b..65931c76b5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11393,11 +11393,56 @@ echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); ); } +/// Verifies eval AOT invokable objects dispatch through variable and callback call paths. +#[test] +fn test_eval_aot_invokable_object_dynamic_callables() { + let out = compile_and_run_capture( + r#" "I", "left" => "H"]) . ":"; +try { + (new EvalAotPlainInvokableProbe())(eval_aot_invokable_side_effect()); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Y:CD:EB:FG:HI:Error:Object of type EvalAotPlainInvokableProbe is not callable" + ); +} + /// Verifies eval call_user_func rejects non-invokable objects with PHP's TypeError. #[test] fn test_eval_call_user_func_rejects_non_invokable_object() { let out = compile_and_run_capture( r#"getMessage(); +} +echo "|"; +$aotPlain = new EvalAotPlainCallbackError(); +try { + call_user_func($aotPlain); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); }'); "#, ); @@ -11423,7 +11476,8 @@ try { assert_eq!( out.stdout, "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given|\ -TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, no array or string given" +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, no array or string given|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given" ); } From 4bbd831adf6ab446c47b02e80cfb0602dbb28685 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 10:28:30 +0200 Subject: [PATCH 0848/1208] Document eval AOT invokable support --- docs/php/eval.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 9981dcb395..0240244a51 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -130,7 +130,11 @@ object methods support string-keyed named arguments through `call_user_func_array()`. Eval-declared objects with `__invoke()` can be called through `$object(...)`, `call_user_func($object, ...)`, and `call_user_func_array($object, [...])`, and `is_callable($object)` reports them -as callable. Static method callables can use `["ClassName", "method"]` or +as callable. Generated/AOT objects with bridge-supported `__invoke()` metadata +use the same direct and callback call paths, including named/defaulted argument +binding, and non-invokable generated/AOT objects report PHP-compatible +direct-call or callback errors. Static method callables can use +`["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method From cf5955d65d72b5d66511fad80e2ad3e8e54b6e76 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 10:37:53 +0200 Subject: [PATCH 0849/1208] Support eval first-class invokable objects --- .../src/interpreter/tests/dynamic_calls.rs | 4 +++- crates/elephc-magician/src/parser/expressions.rs | 3 +++ crates/elephc-magician/src/parser/tests/calls.rs | 11 +++++++++++ docs/php/eval.md | 11 ++++++----- tests/codegen/eval.rs | 8 ++++++-- 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 899194be66..7fba9f82fb 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -346,6 +346,8 @@ try { echo ":"; echo (new EvalInvokableBox("new"))("E", "F"); echo ":"; echo call_user_func($box, "G", "H"); echo ":"; +$first = $box(...); +echo $first("K", "L"); echo ":"; return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, ) .expect("parse eval fragment"); @@ -356,7 +358,7 @@ return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, assert_eq!( values.output, - "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:" + "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:box:KL:" ); assert_eq!(values.get(result), FakeValue::String("box:IJ".to_string())); } diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index d35cc64f95..d9b0d11a15 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -481,6 +481,9 @@ impl Parser { let mut expr = self.parse_primary()?; loop { if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + continue; + } let args = self.parse_call_args()?; expr = EvalExpr::DynamicCall { callee: Box::new(expr), diff --git a/crates/elephc-magician/src/parser/tests/calls.rs b/crates/elephc-magician/src/parser/tests/calls.rs index 43b0747d9d..d0c7c1839a 100644 --- a/crates/elephc-magician/src/parser/tests/calls.rs +++ b/crates/elephc-magician/src/parser/tests/calls.rs @@ -119,6 +119,17 @@ fn parse_fragment_accepts_dynamic_call_expression_source() { }))] ); } + +/// Verifies first-class invokable object syntax leaves the object value as the eval callback. +#[test] +fn parse_fragment_accepts_first_class_invokable_object_callable_source() { + let program = parse_fragment(br#"return $box(...);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::LoadVar("box".to_string())))] + ); +} + /// Verifies dynamic calls can be applied after another postfix expression. #[test] fn parse_fragment_accepts_postfix_dynamic_call_source() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 0240244a51..1967df7362 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -73,7 +73,7 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Scalars | `null`, booleans, integers, floats, and strings. | | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, `isset()` / `empty()` probes, array writes/appends, array-element unsets, and increment/decrement, static-property unset attempts including dynamic names as PHP-compatible catchable errors, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | -| Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, first-class callable syntax for supported function and method targets, invokable eval objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | +| Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, first-class callable syntax for supported function, method, and invokable-object targets, invokable eval and generated/AOT objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | | Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new [readonly] class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | | Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | @@ -116,9 +116,10 @@ String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share the eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. -First-class callable syntax such as `strlen(...)`, `$object->method(...)`, and -`ClassName::method(...)` materializes eval callback values that can be invoked -through `$callback(...)`, `call_user_func()`, and `call_user_func_array()`. +First-class callable syntax such as `strlen(...)`, `$object->method(...)`, +`ClassName::method(...)`, and `$invokable(...)` materializes eval callback +values that can be invoked through `$callback(...)`, `call_user_func()`, and +`call_user_func_array()`. Namespaced function callables follow PHP's global fallback rule when the namespaced function is not visible. The eval bridge does not model these values as full `Closure` objects yet. @@ -748,7 +749,7 @@ string, matching elephc's AOT lowering for parentless or scope-less lookups. Eval `array_map()` supports one or more source arrays with supported callable values or a `null` callback. `array_filter()`, `array_reduce()`, `array_walk()`, `usort()`, `uasort()`, and `uksort()` share the same callback -dispatcher, including first-class function and method callback values. One-array +dispatcher, including first-class function, method, and invokable-object callback values. One-array `array_map()` results preserve source keys, multi-array results are reindexed, missing source values are padded with `null`, and `array_map(null, ...)` returns zipped row arrays. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 65931c76b5..b0df7da10f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -11379,6 +11379,8 @@ try { echo ":"; echo (new EvalInvokableBox("new"))("E", "F") . ":"; echo call_user_func($box, "G", "H") . ":"; +$first = $box(...); +echo $first("K", "L") . ":"; echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); "#, ); @@ -11389,7 +11391,7 @@ echo call_user_func_array($box, ["right" => "J", "left" => "I"]);'); ); assert_eq!( out.stdout, - "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:box:IJ" + "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:box:KL:box:IJ" ); } @@ -11416,6 +11418,8 @@ echo is_callable($box) ? "Y:" : "N:"; echo $box(right: "D", left: "C") . ":"; echo $box("E") . ":"; echo call_user_func($box, "F", "G") . ":"; +$first = $box(...); +echo $first("J", "K") . ":"; echo call_user_func_array($box, ["right" => "I", "left" => "H"]) . ":"; try { (new EvalAotPlainInvokableProbe())(eval_aot_invokable_side_effect()); @@ -11432,7 +11436,7 @@ try { ); assert_eq!( out.stdout, - "Y:CD:EB:FG:HI:Error:Object of type EvalAotPlainInvokableProbe is not callable" + "Y:CD:EB:FG:JK:HI:Error:Object of type EvalAotPlainInvokableProbe is not callable" ); } From a3c5e0eb6657c7a0264b5e7f80053c75e077834f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 10:55:10 +0200 Subject: [PATCH 0850/1208] Support inherited AOT invokable eval classes --- .../interpreter/builtins/registry/callable.rs | 5 ++ .../builtins/registry/callable_validation.rs | 52 ++++++++++++++++++- .../src/interpreter/builtins/symbols.rs | 31 +++++++++++ .../src/interpreter/statements.rs | 47 +++++++++++++++++ docs/php/eval.md | 4 +- tests/codegen/eval.rs | 38 ++++++++++++++ 6 files changed, 174 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index bc26a7af32..64c6e0c171 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -135,6 +135,11 @@ pub(in crate::interpreter) fn eval_object_callable( return Ok(EvaluatedCallable::InvokableObject { object: callback }); }; let Some((_, method)) = context.class_method(class.name(), "__invoke") else { + if eval_dynamic_class_native_invokable_method_class(class.name(), context, values)? + .is_some() + { + return Ok(EvaluatedCallable::InvokableObject { object: callback }); + } return Err(EvalStatus::UnsupportedConstruct); }; if method.is_static() || method.is_abstract() { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index 20ac2c1fb6..c1da696422 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -67,6 +67,35 @@ fn eval_validate_call_user_func_object_method( let Some((declaring_class, method)) = eval_dynamic_method_for_call(&called_class_name, method_name, context) else { + if let Some(parent) = context.class_native_parent_name(&called_class_name) { + let has_native_metadata = eval_dynamic_class_native_method_metadata( + &called_class_name, + method_name, + context, + values, + )? + .is_some(); + let has_native_magic = + eval_call_user_func_native_instance_magic_callable(&parent, context, values)?; + let has_native_signature = context + .native_method_signature(&parent, method_name) + .is_some(); + let missing_native_class = !values.class_exists(&parent)?; + let has_native_target = has_native_metadata + || has_native_magic + || has_native_signature + || missing_native_class; + if has_native_target { + return eval_validate_call_user_func_native_object_method_for_class( + &parent, + &called_class_name, + method_name, + function_name, + context, + values, + ); + } + } if eval_call_user_func_instance_magic_callable(&called_class_name, context) { return Ok(()); } @@ -105,6 +134,25 @@ fn eval_validate_call_user_func_native_object_method( values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let class_name = runtime_object_class_name(object, values)?; + eval_validate_call_user_func_native_object_method_for_class( + &class_name, + &class_name, + method_name, + function_name, + context, + values, + ) +} + +/// Validates generated/AOT object-method callbacks by class metadata. +fn eval_validate_call_user_func_native_object_method_for_class( + class_name: &str, + error_class_name: &str, + method_name: &str, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { let Some((declaring_class, visibility, _, is_abstract)) = eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? else { @@ -112,13 +160,13 @@ fn eval_validate_call_user_func_native_object_method( || context .native_method_signature(&class_name, method_name) .is_some() - || !values.class_exists(&class_name)? + || !values.class_exists(class_name)? { return Ok(()); } return eval_call_user_func_missing_method_type_error( function_name, - &class_name, + error_class_name, method_name, context, values, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index d219f9b7da..a13c4d4d76 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -922,6 +922,14 @@ fn eval_object_method_callable_probe( let Some((declaring_class, method)) = eval_dynamic_method_for_call(class.name(), method_name, context) else { + if eval_dynamic_class_native_method_callable_probe( + class.name(), + method_name, + context, + values, + )? { + return Ok(true); + } return Ok(eval_instance_magic_method_callable_probe( class.name(), context, @@ -974,6 +982,29 @@ fn eval_aot_object_method_callable_probe( values: &mut impl RuntimeValueOps, ) -> Result { let class_name = runtime_object_class_name(object, values)?; + eval_aot_class_method_callable_probe(&class_name, method_name, context, values) +} + +/// Returns whether an eval class can call a generated/AOT parent instance method. +fn eval_dynamic_class_native_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_aot_class_method_callable_probe(&parent, method_name, context, values) +} + +/// Returns whether one generated/AOT class instance method can be called from eval. +fn eval_aot_class_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { let Some((declaring_class, visibility, _, is_abstract)) = eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? else { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 63cb46cdc7..73308420c2 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7057,6 +7057,18 @@ pub(in crate::interpreter) fn eval_invokable_object_call_result( let called_class_name = class.name().to_string(); let Some((declaring_class, method)) = context.class_method(&called_class_name, "__invoke") else { + if let Some(native_class_name) = + eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? + { + return eval_native_method_with_evaluated_args_unchecked( + object, + &native_class_name, + "__invoke", + evaluated_args, + context, + values, + ); + } return eval_throw_object_not_callable_error(&called_class_name, context, values); }; if method.is_static() || method.is_abstract() { @@ -7101,6 +7113,11 @@ pub(in crate::interpreter) fn eval_invokable_object_precheck( }; let called_class_name = class.name().to_string(); let Some((_, method)) = context.class_method(&called_class_name, "__invoke") else { + if eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? + .is_some() + { + return Ok(()); + } return eval_throw_object_not_callable_error(&called_class_name, context, values); }; if method.is_static() || method.is_abstract() { @@ -7109,6 +7126,36 @@ pub(in crate::interpreter) fn eval_invokable_object_precheck( Ok(()) } +/// Returns the generated/AOT class that can dispatch an inherited `__invoke()` hook. +pub(in crate::interpreter) fn eval_dynamic_class_native_invokable_method_class( + called_class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, _, is_static, is_abstract)) = + eval_dynamic_class_native_method_metadata(called_class_name, "__invoke", context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + Ok(Some(declaring_class)) +} + +/// Returns generated/AOT method metadata inherited by an eval-declared class. +pub(in crate::interpreter) fn eval_dynamic_class_native_method_metadata( + called_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values) +} + /// Dispatches a missing or inaccessible eval instance method through `__call()`. fn eval_magic_instance_method_call( object: RuntimeCellHandle, diff --git a/docs/php/eval.md b/docs/php/eval.md index 1967df7362..649e726cb5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -134,7 +134,9 @@ through `$object(...)`, `call_user_func($object, ...)`, and as callable. Generated/AOT objects with bridge-supported `__invoke()` metadata use the same direct and callback call paths, including named/defaulted argument binding, and non-invokable generated/AOT objects report PHP-compatible -direct-call or callback errors. Static method callables can use +direct-call or callback errors. Eval-declared classes that extend generated/AOT +parents inherit bridge-supported parent instance-method callable probes and +`__invoke()` object-call dispatch. Static method callables can use `["ClassName", "method"]` or `"ClassName::method"` through `$cb(...)`, `call_user_func()`, and `call_user_func_array()`. Eval-declared static methods also support string-keyed diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b0df7da10f..7ce250339c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8115,6 +8115,44 @@ echo $parent ? $parent->getName() : "missing";'); ); } +/// Verifies eval-declared classes inherit AOT callable object and method behavior. +#[test] +fn test_eval_declared_class_inherits_aot_invokable_parent_callables() { + let out = compile_and_run_capture( + r#"prefix = $prefix; } + public function read(string $value = "R"): string { return $this->prefix . ":" . $value; } + public function __invoke(string $left = "A", string $right = "B"): string { + return $this->prefix . ":" . $left . $right; + } +} + +eval('class EvalRuntimeCallableChild extends EvalAotCallableParent {} +$box = new EvalRuntimeCallableChild("box"); +echo is_callable($box) ? "I:" : "bad:"; +echo $box(right: "D", left: "C") . ":"; +$first = $box(...); +echo $first("E", "F") . ":"; +echo call_user_func($box, "G", "H") . ":"; +echo call_user_func_array($box, ["right" => "J", "left" => "I"]) . ":"; +echo is_callable([$box, "read"]) ? "M:" : "bad:"; +echo call_user_func([$box, "read"], "K") . ":"; +echo call_user_func_array([$box, "read"], ["value" => "L"]);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "I:box:CD:box:EF:box:GH:box:IJ:M:box:K:box:L" + ); +} + /// Verifies eval-declared class inheritance uses dynamic methods and metadata. #[test] fn test_eval_declared_class_inherits_methods_and_metadata() { From d4b8606f679aa1d2323904e36ce41c13d416f85a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 11:14:53 +0200 Subject: [PATCH 0851/1208] Expose AOT parent members to eval introspection --- .../class_metadata/oop_introspection.rs | 165 +++++++++++++++--- docs/php/eval.md | 4 + tests/codegen/eval.rs | 42 +++++ 3 files changed, 190 insertions(+), 21 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 800a6f5bff..d9b3fcc54b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -224,10 +224,20 @@ fn eval_method_exists_on_class( ) -> Result { if context.has_class(class_name) || context.has_enum(class_name) { if target_is_object { - return Ok(context + if context .class_method_names(class_name) .iter() - .any(|name| name.eq_ignore_ascii_case(method_name))); + .any(|name| name.eq_ignore_ascii_case(method_name)) + { + return Ok(true); + } + return eval_native_parent_method_exists_on_class( + class_name, + method_name, + target_is_object, + context, + values, + ); } if context .class_method_names(class_name) @@ -243,7 +253,13 @@ fn eval_method_exists_on_class( .trim_start_matches('\\') .eq_ignore_ascii_case(class_name.trim_start_matches('\\'))); } - return Ok(false); + return eval_native_parent_method_exists_on_class( + class_name, + method_name, + target_is_object, + context, + values, + ); } if context.has_interface(class_name) { return Ok(context @@ -258,17 +274,71 @@ fn eval_method_exists_on_class( .any(|name| name.eq_ignore_ascii_case(method_name))); } if target_is_object { - return Ok(eval_aot_method_dispatch_metadata_in_hierarchy( + return eval_native_method_exists_on_class( + class_name, class_name, method_name, + target_is_object, + context, + values, + ); + } + eval_native_method_exists_on_class( + class_name, + class_name, + method_name, + target_is_object, + context, + values, + ) +} + +/// Checks generated/AOT parent method metadata inherited by one eval class. +fn eval_native_parent_method_exists_on_class( + class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_native_method_exists_on_class( + class_name, + &parent, + method_name, + target_is_object, + context, + values, + ) +} + +/// Checks generated/AOT method metadata for method_exists() semantics. +fn eval_native_method_exists_on_class( + reflected_class_name: &str, + lookup_class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, _)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + lookup_class_name, + method_name, context, values, )? - .is_some()); + else { + return Ok(false); + }; + if target_is_object || visibility != EvalVisibility::Private { + return Ok(true); } - values - .reflection_method_flags(class_name, method_name) - .map(|flags| flags.is_some()) + Ok(declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(reflected_class_name.trim_start_matches('\\'))) } /// Checks property metadata for one resolved class-like name. @@ -279,10 +349,19 @@ fn eval_property_exists_on_class( values: &mut impl RuntimeValueOps, ) -> Result { if context.has_class(class_name) || context.has_enum(class_name) { - return Ok(context + if context .class_property_names(class_name) .iter() - .any(|name| name == property_name)); + .any(|name| name == property_name) + { + return Ok(true); + } + return eval_native_parent_property_exists_on_class( + class_name, + property_name, + context, + values, + ); } if context.has_interface(class_name) { return Ok(context @@ -296,21 +375,40 @@ fn eval_property_exists_on_class( .iter() .any(|name| name == property_name)); } - let Some(flags) = values.reflection_property_flags(class_name, property_name)? else { + eval_native_property_exists_on_class(class_name, class_name, property_name, values) +} + +/// Checks generated/AOT parent property metadata inherited by one eval class. +fn eval_native_parent_property_exists_on_class( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_native_property_exists_on_class(class_name, &parent, property_name, values) +} + +/// Checks generated/AOT property metadata for property_exists() semantics. +fn eval_native_property_exists_on_class( + reflected_class_name: &str, + lookup_class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _)) = + eval_runtime_property_access_metadata(lookup_class_name, property_name, values)? + else { return Ok(false); }; - if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE == 0 { + if visibility != EvalVisibility::Private { return Ok(true); } - let Some(declaring_class) = values.reflection_property_declaring_class( - class_name, - property_name, - )? else { - return Ok(true); - }; Ok(declaring_class .trim_start_matches('\\') - .eq_ignore_ascii_case(class_name.trim_start_matches('\\'))) + .eq_ignore_ascii_case(reflected_class_name.trim_start_matches('\\'))) } /// Checks private instance properties declared by the current scope for object targets. @@ -416,6 +514,9 @@ fn eval_class_method_names_for_scope( eval_add_current_scope_private_method_names( &mut names, &mut seen, class_name, context, values, )?; + eval_add_native_parent_method_names( + &mut names, &mut seen, class_name, context, values, + )?; return Ok(names); } if context.has_interface(class_name) { @@ -443,7 +544,7 @@ fn eval_class_method_names_for_scope( /// Filters generated runtime methods to the surface visible from the current eval scope. fn eval_visible_runtime_method_names( - class_name: &str, + lookup_class_name: &str, names: Vec, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -451,7 +552,7 @@ fn eval_visible_runtime_method_names( let mut result = Vec::new(); for name in names { let Some((declaring_class, visibility)) = - eval_runtime_method_access_metadata(class_name, &name, values)? + eval_runtime_method_access_metadata(lookup_class_name, &name, values)? else { continue; }; @@ -462,6 +563,28 @@ fn eval_visible_runtime_method_names( Ok(result) } +/// Adds generated/AOT parent method names inherited by one eval class. +fn eval_add_native_parent_method_names( + names: &mut Vec, + seen: &mut HashSet, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(()); + }; + let method_names = values.reflection_method_names(&parent)?; + let parent_names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + let parent_names = + eval_visible_runtime_method_names(&parent, parent_names, context, values)?; + for name in parent_names { + eval_push_unique_method_name(names, seen, name); + } + Ok(()) +} + /// Adds private methods declared by the current eval scope when PHP would expose them. fn eval_add_current_scope_private_method_names( names: &mut Vec, diff --git a/docs/php/eval.md b/docs/php/eval.md index 649e726cb5..0501483fd3 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -358,6 +358,10 @@ method lookup is case-insensitive, while property lookup is case-sensitive. For generated/AOT classes, `ReflectionClass::hasMethod()` and `hasProperty()` can also probe emitted method/property metadata without requiring the full member lists to be materialized on the `ReflectionClass` object. +Eval-declared classes that extend generated/AOT parents expose inherited +bridge-supported public/protected parent members to `method_exists()`, +`property_exists()`, and `get_class_methods()` with PHP-compatible scope +filtering. `ReflectionClass::hasConstant()`, `getConstant()`, `getConstants()`, `getDefaultProperties()`, `getStaticProperties()`, `getStaticPropertyValue()`, `setStaticPropertyValue()`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7ce250339c..2bb3f0c752 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8153,6 +8153,48 @@ echo call_user_func_array([$box, "read"], ["value" => "L"]);'); ); } +/// Verifies eval-declared classes expose inherited AOT members to OOP introspection. +#[test] +fn test_eval_declared_class_inherits_aot_parent_member_introspection() { + let out = compile_and_run_capture( + r#"childView();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "classProtected:objectProtected:objectPublicProp:classProtectedProp:outsideParent:outsideNoProtected:outsideChild:P" + ); +} + /// Verifies eval-declared class inheritance uses dynamic methods and metadata. #[test] fn test_eval_declared_class_inherits_methods_and_metadata() { From 12b8a256e8410f87bb32f948a6516cbeebdc3c4b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 11:25:37 +0200 Subject: [PATCH 0852/1208] Sync eval classes across generated eval contexts --- crates/elephc-magician/src/context.rs | 56 +++++++++++++++++++++++ crates/elephc-magician/src/ffi/execute.rs | 1 + docs/php/eval.md | 3 ++ tests/codegen/eval.rs | 10 +++- 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 02bb353b06..f8215a31ca 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -13,6 +13,8 @@ use std::collections::{HashMap, HashSet}; use std::ffi::c_void; +#[cfg(not(test))] +use std::sync::{Mutex, OnceLock}; use crate::abi::ABI_VERSION; use crate::eval_ir::{ @@ -24,6 +26,34 @@ use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; use crate::value::{RuntimeCell, RuntimeCellHandle}; +#[cfg(not(test))] +static GLOBAL_EVAL_CLASSES: OnceLock> = OnceLock::new(); + +#[cfg(not(test))] +#[derive(Default)] +struct GlobalEvalClassRegistry { + classes: HashMap, + declared_names: Vec, +} + +/// Returns the process-local eval class registry for generated-code eval contexts. +#[cfg(not(test))] +fn global_eval_classes() -> &'static Mutex { + GLOBAL_EVAL_CLASSES.get_or_init(|| Mutex::new(GlobalEvalClassRegistry::default())) +} + +/// Records one eval-declared class so later eval contexts can see PHP-global metadata. +#[cfg(not(test))] +fn register_global_eval_class(class: &EvalClass) { + let key = normalize_class_name(class.name()); + if let Ok(mut registry) = global_eval_classes().lock() { + if !registry.classes.contains_key(&key) { + registry.declared_names.push(class.name().to_string()); + } + registry.classes.insert(key, class.clone()); + } +} + /// Native descriptor-invoker ABI registered by generated code for AOT functions. pub type NativeFunctionInvoker = unsafe extern "C" fn(*mut c_void, *mut RuntimeCell) -> *mut RuntimeCell; @@ -739,10 +769,36 @@ impl ElephcEvalContext { return false; } self.declared_class_names.push(class.name().to_string()); + #[cfg(not(test))] + register_global_eval_class(&class); self.classes.insert(key, class); true } + /// Imports eval-declared process-global classes not yet known by this context. + #[cfg(not(test))] + pub fn sync_global_eval_classes(&mut self) { + let Ok(registry) = global_eval_classes().lock() else { + return; + }; + for name in ®istry.declared_names { + let key = normalize_class_name(name); + if self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.traits.contains_key(&key) + || self.enums.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + continue; + } + let Some(class) = registry.classes.get(&key).cloned() else { + continue; + }; + self.declared_class_names.push(class.name().to_string()); + self.classes.insert(key, class); + } + } + /// Returns true when this eval context has a dynamic class or alias with the requested name. pub fn has_class(&self, name: &str) -> bool { let key = normalize_class_name(name); diff --git a/crates/elephc-magician/src/ffi/execute.rs b/crates/elephc-magician/src/ffi/execute.rs index e1e603d7cb..de746a5bf0 100644 --- a/crates/elephc-magician/src/ffi/execute.rs +++ b/crates/elephc-magician/src/ffi/execute.rs @@ -105,6 +105,7 @@ unsafe fn execute_parsed_eval( fallback_scope = ElephcEvalScope::new(); &mut fallback_scope }; + context.sync_global_eval_classes(); let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); match interpreter::execute_program_outcome_with_context(context, program, scope, &mut values) { Ok(outcome) => write_outcome(outcome, out).code(), diff --git a/docs/php/eval.md b/docs/php/eval.md index 0501483fd3..af02d4077d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -210,6 +210,9 @@ parameter bans, and relevant declared parameter/return-type contracts for `__unserialize()`, `__debugInfo()`, `__set_state()`, `__invoke()`, `__clone()`, `__destruct()`, and `__construct()` when dynamic classes or traits are declared. +Eval-declared class metadata is synchronized across generated eval contexts, so +later eval fragments inside AOT methods can introspect classes declared by an +earlier eval call in the same process. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2bb3f0c752..45a4a4fa7f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8163,6 +8163,10 @@ class EvalAotIntrospectionParent { protected int $prot = 2; protected function guarded() {} public function read() {} + public function parentView() { + return eval('$methods = get_class_methods("EvalRuntimeIntrospectionChild"); +return in_array("guarded", $methods) ? "parentProtected" : "bad";'); + } } eval('class EvalRuntimeIntrospectionChild extends EvalAotIntrospectionParent { @@ -8181,7 +8185,9 @@ $outside = get_class_methods("EvalRuntimeIntrospectionChild"); echo in_array("read", $outside) ? "outsideParent:" : "bad:"; echo in_array("guarded", $outside) ? "bad:" : "outsideNoProtected:"; echo in_array("childRead", $outside) ? "outsideChild:" : "bad:"; -$box->childView();'); +$box->childView(); +echo ":"; +echo $box->parentView();'); "#, ); assert!( @@ -8191,7 +8197,7 @@ $box->childView();'); ); assert_eq!( out.stdout, - "classProtected:objectProtected:objectPublicProp:classProtectedProp:outsideParent:outsideNoProtected:outsideChild:P" + "classProtected:objectProtected:objectPublicProp:classProtectedProp:outsideParent:outsideNoProtected:outsideChild:P:parentProtected" ); } From ccf31370318faca7444bbb1c988fa450a22fa405 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 11:47:19 +0200 Subject: [PATCH 0853/1208] Preserve eval object identity across eval contexts --- crates/elephc-magician/src/context.rs | 73 +++++++++++++++++-- .../src/ffi/dynamic_destructors.rs | 4 +- .../class_metadata/oop_introspection.rs | 4 +- .../src/interpreter/builtins/scalars/types.rs | 8 +- .../src/interpreter/reflection.rs | 4 +- tests/codegen/eval.rs | 10 ++- 6 files changed, 86 insertions(+), 17 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index f8215a31ca..c065b9db39 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -1234,9 +1234,45 @@ impl ElephcEvalContext { /// Returns the dynamic eval class metadata associated with one object identity. pub fn dynamic_object_class(&self, identity: u64) -> Option<&EvalClass> { - self.dynamic_objects - .get(&identity) - .and_then(|class_key| self.classes.get(class_key)) + if let Some(class_key) = self.dynamic_objects.get(&identity) { + return self.classes.get(class_key); + } + #[cfg(not(test))] + { + let owner = crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity)?; + let owner = unsafe { owner.as_ref()? }; + if owner.abi_version() != ABI_VERSION { + return None; + } + let class_key = owner.dynamic_objects.get(&identity)?; + self.classes.get(class_key) + } + #[cfg(test)] + { + None + } + } + + /// Returns the PHP-visible eval class name associated with one dynamic object identity. + pub fn dynamic_object_class_name(&self, identity: u64) -> Option { + if let Some(class) = self.dynamic_object_class(identity) { + return Some(class.name().trim_start_matches('\\').to_string()); + } + #[cfg(not(test))] + { + let owner = crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity)?; + let owner = unsafe { owner.as_ref()? }; + if owner.abi_version() != ABI_VERSION { + return None; + } + owner + .dynamic_object_class(identity) + .map(|class| class.name().trim_start_matches('\\').to_string()) + } + #[cfg(test)] + { + None + } } /// Marks one dynamic object's destructor as active if it has not already run. @@ -1258,9 +1294,36 @@ impl ElephcEvalContext { /// Returns whether one dynamic object identity was registered with a class-like name. pub fn dynamic_object_is_class(&self, identity: u64, class_name: &str) -> bool { - self.dynamic_objects + let class_name = normalize_class_name(class_name); + if self + .dynamic_objects .get(&identity) - .is_some_and(|class_key| class_key == &normalize_class_name(class_name)) + .is_some_and(|class_key| class_key == &class_name) + { + return true; + } + #[cfg(not(test))] + { + let Some(owner) = + crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity) + else { + return false; + }; + let Some(owner) = (unsafe { owner.as_ref() }) else { + return false; + }; + if owner.abi_version() != ABI_VERSION { + return false; + } + owner + .dynamic_objects + .get(&identity) + .is_some_and(|class_key| class_key == &class_name) + } + #[cfg(test)] + { + false + } } /// Binds one eval object property slot to a persistent PHP reference target. diff --git a/crates/elephc-magician/src/ffi/dynamic_destructors.rs b/crates/elephc-magician/src/ffi/dynamic_destructors.rs index 300bf2a125..c7d5576179 100644 --- a/crates/elephc-magician/src/ffi/dynamic_destructors.rs +++ b/crates/elephc-magician/src/ffi/dynamic_destructors.rs @@ -78,7 +78,7 @@ pub(crate) fn unregister_dynamic_objects_for_context(context: *mut ElephcEvalCon /// Looks up the eval context that owns one dynamic object identity. #[cfg(not(test))] -fn dynamic_destructor_context(identity: u64) -> Option<*mut ElephcEvalContext> { +pub(crate) fn dynamic_object_owner_context(identity: u64) -> Option<*mut ElephcEvalContext> { let contexts = dynamic_destructor_contexts().lock().ok()?; let context = *contexts.get(&identity)?; Some(context as *mut ElephcEvalContext) @@ -109,7 +109,7 @@ unsafe fn dynamic_object_destruct_inner(object: *mut RuntimeCell) -> u64 { return 0; } let identity = object as u64; - let Some(context) = dynamic_destructor_context(identity) else { + let Some(context) = dynamic_object_owner_context(identity) else { return 0; }; let Some(context) = (unsafe { context.as_mut() }) else { diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index d9b3fcc54b..a35d013480 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -473,8 +473,8 @@ fn eval_object_class_metadata_name( values: &mut impl RuntimeValueOps, ) -> Result { let identity = values.object_identity(object)?; - if let Some(class) = context.dynamic_object_class(identity) { - return Ok(class.name().trim_start_matches('\\').to_string()); + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); } let class_name = values.object_class_name(object)?; let class_name_bytes = values.string_bytes(class_name); diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs index 253f95f575..3f9e0c009b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs @@ -136,8 +136,8 @@ pub(in crate::interpreter) fn eval_get_class_result( values: &mut impl RuntimeValueOps, ) -> Result { if let Ok(identity) = values.object_identity(object) { - if let Some(class) = context.dynamic_object_class(identity) { - return values.string(class.name().trim_start_matches('\\')); + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return values.string(&class_name); } } values.object_class_name(object) @@ -211,8 +211,8 @@ pub(in crate::interpreter) fn eval_get_parent_class_result( values: &mut impl RuntimeValueOps, ) -> Result { if let Ok(identity) = values.object_identity(object_or_class) { - if let Some(class) = context.dynamic_object_class(identity) { - if let Some(parent) = context.class_parent_names(class.name()).into_iter().next() { + if let Some(class_name) = context.dynamic_object_class_name(identity) { + if let Some(parent) = context.class_parent_names(&class_name).into_iter().next() { return values.string(&parent); } return values.string(""); diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index e6e121274f..319960cab6 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3830,8 +3830,8 @@ fn eval_reflection_object_class_name( values: &mut impl RuntimeValueOps, ) -> Result { let identity = values.object_identity(object)?; - if let Some(class) = context.dynamic_object_class(identity) { - return Ok(class.name().trim_start_matches('\\').to_string()); + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); } let class_name = values.object_class_name(object)?; let bytes = values.string_bytes(class_name); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 45a4a4fa7f..7409e46fe7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8165,7 +8165,13 @@ class EvalAotIntrospectionParent { public function read() {} public function parentView() { return eval('$methods = get_class_methods("EvalRuntimeIntrospectionChild"); -return in_array("guarded", $methods) ? "parentProtected" : "bad";'); +$objectMethods = get_class_methods($this); +$objectRef = new ReflectionObject($this); +return (in_array("guarded", $methods) ? "parentProtected" : "bad") . ":" . + get_class($this) . ":" . + get_parent_class($this) . ":" . + $objectRef->getName() . ":" . + (in_array("childRead", $objectMethods) ? "objectChild" : "bad");'); } } @@ -8197,7 +8203,7 @@ echo $box->parentView();'); ); assert_eq!( out.stdout, - "classProtected:objectProtected:objectPublicProp:classProtectedProp:outsideParent:outsideNoProtected:outsideChild:P:parentProtected" + "classProtected:objectProtected:objectPublicProp:classProtectedProp:outsideParent:outsideNoProtected:outsideChild:P:parentProtected:EvalRuntimeIntrospectionChild:EvalAotIntrospectionParent:EvalRuntimeIntrospectionChild:objectChild" ); } From 5851d20acbc5815836da10ab7e487b2df15d899d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 11:57:53 +0200 Subject: [PATCH 0854/1208] Sync eval class-like metadata across contexts --- crates/elephc-magician/src/context.rs | 152 ++++++++++++++++++++++++-- docs/php/eval.md | 7 +- tests/codegen/eval.rs | 41 +++++++ 3 files changed, 186 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index c065b9db39..d1f8998956 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -33,7 +33,14 @@ static GLOBAL_EVAL_CLASSES: OnceLock> = OnceLock: #[derive(Default)] struct GlobalEvalClassRegistry { classes: HashMap, - declared_names: Vec, + declared_class_names: Vec, + interfaces: HashMap, + declared_interface_names: Vec, + traits: HashMap, + declared_trait_names: Vec, + enums: HashMap, + declared_enum_names: Vec, + aliases: HashMap, } /// Returns the process-local eval class registry for generated-code eval contexts. @@ -48,12 +55,63 @@ fn register_global_eval_class(class: &EvalClass) { let key = normalize_class_name(class.name()); if let Ok(mut registry) = global_eval_classes().lock() { if !registry.classes.contains_key(&key) { - registry.declared_names.push(class.name().to_string()); + registry.declared_class_names.push(class.name().to_string()); } registry.classes.insert(key, class.clone()); } } +/// Records one eval-declared interface so later eval contexts can see PHP-global metadata. +#[cfg(not(test))] +fn register_global_eval_interface(interface: &EvalInterface) { + let key = normalize_class_name(interface.name()); + if let Ok(mut registry) = global_eval_classes().lock() { + if !registry.interfaces.contains_key(&key) { + registry + .declared_interface_names + .push(interface.name().to_string()); + } + registry.interfaces.insert(key, interface.clone()); + } +} + +/// Records one eval-declared trait so later eval contexts can see PHP-global metadata. +#[cfg(not(test))] +fn register_global_eval_trait(trait_decl: &EvalTrait) { + let key = normalize_class_name(trait_decl.name()); + if let Ok(mut registry) = global_eval_classes().lock() { + if !registry.traits.contains_key(&key) { + registry + .declared_trait_names + .push(trait_decl.name().to_string()); + } + registry.traits.insert(key, trait_decl.clone()); + } +} + +/// Records one eval-declared enum so later eval contexts can see PHP-global metadata. +#[cfg(not(test))] +fn register_global_eval_enum(enum_decl: &EvalEnum) { + let key = normalize_class_name(enum_decl.name()); + if let Ok(mut registry) = global_eval_classes().lock() { + if !registry.enums.contains_key(&key) { + registry + .declared_enum_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + } + registry.enums.insert(key, enum_decl.clone()); + } +} + +/// Records one eval-defined class-like alias for later generated eval contexts. +#[cfg(not(test))] +fn register_global_eval_alias(alias_name: &str, alias: &EvalClassAlias) { + let key = normalize_class_name(alias_name); + if let Ok(mut registry) = global_eval_classes().lock() { + registry.aliases.insert(key, alias.clone()); + } +} + /// Native descriptor-invoker ABI registered by generated code for AOT functions. pub type NativeFunctionInvoker = unsafe extern "C" fn(*mut c_void, *mut RuntimeCell) -> *mut RuntimeCell; @@ -775,13 +833,13 @@ impl ElephcEvalContext { true } - /// Imports eval-declared process-global classes not yet known by this context. + /// Imports eval-declared process-global class-like metadata not yet known by this context. #[cfg(not(test))] pub fn sync_global_eval_classes(&mut self) { let Ok(registry) = global_eval_classes().lock() else { return; }; - for name in ®istry.declared_names { + for name in ®istry.declared_class_names { let key = normalize_class_name(name); if self.classes.contains_key(&key) || self.interfaces.contains_key(&key) @@ -797,6 +855,72 @@ impl ElephcEvalContext { self.declared_class_names.push(class.name().to_string()); self.classes.insert(key, class); } + for name in ®istry.declared_interface_names { + let key = normalize_class_name(name); + if self.interfaces.contains_key(&key) + || self.classes.contains_key(&key) + || self.traits.contains_key(&key) + || self.enums.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + continue; + } + let Some(interface) = registry.interfaces.get(&key).cloned() else { + continue; + }; + self.declared_interface_names + .push(interface.name().to_string()); + self.interfaces.insert(key, interface); + } + for name in ®istry.declared_trait_names { + let key = normalize_class_name(name); + if self.traits.contains_key(&key) + || self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.enums.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + continue; + } + let Some(trait_decl) = registry.traits.get(&key).cloned() else { + continue; + }; + self.declared_trait_names + .push(trait_decl.name().to_string()); + self.traits.insert(key, trait_decl); + } + for name in ®istry.declared_enum_names { + let key = normalize_class_name(name); + if self.enums.contains_key(&key) + || self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.traits.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + continue; + } + let Some(enum_decl) = registry.enums.get(&key).cloned() else { + continue; + }; + self.declared_enum_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + self.declared_class_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + self.classes + .insert(key.clone(), enum_decl.as_class_metadata()); + self.enums.insert(key, enum_decl); + } + for (key, alias) in ®istry.aliases { + if self.classes.contains_key(key) + || self.interfaces.contains_key(key) + || self.traits.contains_key(key) + || self.enums.contains_key(key) + || self.class_aliases.contains_key(key) + { + continue; + } + self.class_aliases.insert(key.clone(), alias.clone()); + } } /// Returns true when this eval context has a dynamic class or alias with the requested name. @@ -962,13 +1086,13 @@ impl ElephcEvalContext { { return false; } - self.class_aliases.insert( - alias_key, - EvalClassAlias { - target: original.trim_start_matches('\\').to_string(), - kind, - }, - ); + let alias_record = EvalClassAlias { + target: original.trim_start_matches('\\').to_string(), + kind, + }; + #[cfg(not(test))] + register_global_eval_alias(alias, &alias_record); + self.class_aliases.insert(alias_key, alias_record); true } @@ -995,6 +1119,8 @@ impl ElephcEvalContext { } self.declared_interface_names .push(interface.name().to_string()); + #[cfg(not(test))] + register_global_eval_interface(&interface); self.interfaces.insert(key, interface); true } @@ -1044,6 +1170,8 @@ impl ElephcEvalContext { } self.declared_trait_names .push(trait_decl.name().to_string()); + #[cfg(not(test))] + register_global_eval_trait(&trait_decl); self.traits.insert(key, trait_decl); true } @@ -1095,6 +1223,8 @@ impl ElephcEvalContext { .push(enum_decl.name().trim_start_matches('\\').to_string()); self.declared_class_names .push(enum_decl.name().trim_start_matches('\\').to_string()); + #[cfg(not(test))] + register_global_eval_enum(&enum_decl); self.classes .insert(key.clone(), enum_decl.as_class_metadata()); self.enums.insert(key, enum_decl); diff --git a/docs/php/eval.md b/docs/php/eval.md index af02d4077d..8287642ff8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -210,9 +210,10 @@ parameter bans, and relevant declared parameter/return-type contracts for `__unserialize()`, `__debugInfo()`, `__set_state()`, `__invoke()`, `__clone()`, `__destruct()`, and `__construct()` when dynamic classes or traits are declared. -Eval-declared class metadata is synchronized across generated eval contexts, so -later eval fragments inside AOT methods can introspect classes declared by an -earlier eval call in the same process. +Eval-declared class-like metadata and aliases are synchronized across generated +eval contexts, so later eval fragments inside AOT methods can introspect +classes, interfaces, traits, enums, and aliases declared by an earlier eval call +in the same process. Member visibility is checked at runtime for eval-declared objects and static/class-constant accesses. Class-level attributes declared on eval classes, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7409e46fe7..1997d4acfc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8207,6 +8207,47 @@ echo $box->parentView();'); ); } +/// Verifies eval-declared class-like symbols remain visible in generated eval contexts. +#[test] +fn test_eval_declared_class_likes_are_visible_in_aot_nested_eval_context() { + let out = compile_and_run_capture( + r#"view(); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "CA:IA:TA:EACA:R"); +} + /// Verifies eval-declared class inheritance uses dynamic methods and metadata. #[test] fn test_eval_declared_class_inherits_methods_and_metadata() { From dbfd4b53b04e00f3f12867790c818ba71a92e8ef Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 12:19:53 +0200 Subject: [PATCH 0855/1208] Support scalar-keyed eval attribute arrays --- crates/elephc-magician/src/eval_ir.rs | 18 ++++- crates/elephc-magician/src/ffi/tests.rs | 3 + .../interpreter/builtins/class_metadata.rs | 11 +++- .../src/interpreter/statements.rs | 11 +++- .../elephc-magician/src/parser/statements.rs | 66 +++++++++++++++++-- docs/php/eval.md | 15 +++-- tests/codegen/eval.rs | 34 ++++++++++ 7 files changed, 136 insertions(+), 22 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 5952da47da..081855b973 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -585,6 +585,10 @@ pub enum EvalAttributeArg { name: String, value: Box, }, + IntKeyed { + key: i64, + value: Box, + }, } impl EvalAttributeArg { @@ -596,10 +600,20 @@ impl EvalAttributeArg { } } - /// Returns the scalar payload, unwrapping a named-argument wrapper. + /// Returns the PHP integer array key when this attribute arg is int-keyed. + pub fn int_key(&self) -> Option { + match self { + EvalAttributeArg::IntKeyed { key, .. } => Some(*key), + _ => None, + } + } + + /// Returns the scalar payload, unwrapping a named or int-keyed wrapper. pub fn value(&self) -> &EvalAttributeArg { match self { - EvalAttributeArg::Named { value, .. } => value, + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + value + } _ => self, } } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 6b1e881600..8f4e7e159c 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -102,6 +102,9 @@ fn native_member_attribute_push_arg(record: &mut Vec, arg: &EvalAttributeArg native_member_attribute_push_string(record, name); native_member_attribute_push_arg(record, value); } + EvalAttributeArg::IntKeyed { .. } => { + panic!("native attribute test ABI does not encode int-keyed array arguments") + } EvalAttributeArg::Array(elements) => { record.push(6); record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index db665a3c53..83c10b2e0e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -333,7 +333,9 @@ fn eval_class_attribute_arg_value( EvalAttributeArg::Bool(value) => values.bool_value(*value), EvalAttributeArg::Null => values.null(), EvalAttributeArg::Array(elements) => eval_class_attribute_array_arg_value(elements, values), - EvalAttributeArg::Named { value, .. } => eval_class_attribute_arg_value(value, values), + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + eval_class_attribute_arg_value(value, values) + } } } @@ -342,7 +344,10 @@ fn eval_class_attribute_array_arg_value( elements: &[EvalAttributeArg], values: &mut impl RuntimeValueOps, ) -> Result { - let mut result = if elements.iter().any(|element| element.name().is_some()) { + let mut result = if elements + .iter() + .any(|element| element.name().is_some() || element.int_key().is_some()) + { values.assoc_new(elements.len())? } else { values.array_new(elements.len())? @@ -350,7 +355,7 @@ fn eval_class_attribute_array_arg_value( for (index, element) in elements.iter().enumerate() { let key = match element.name() { Some(name) => values.string(name)?, - None => values.int(index as i64)?, + None => values.int(element.int_key().unwrap_or(index as i64))?, }; let value = eval_class_attribute_arg_value(element.value(), values)?; result = values.array_set(result, key, value)?; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 73308420c2..4c084b496f 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7481,7 +7481,9 @@ fn eval_reflection_attribute_arg_value( EvalAttributeArg::Array(elements) => { eval_reflection_attribute_array_arg_value(elements, values) } - EvalAttributeArg::Named { value, .. } => eval_reflection_attribute_arg_value(value, values), + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + eval_reflection_attribute_arg_value(value, values) + } } } @@ -7490,7 +7492,10 @@ fn eval_reflection_attribute_array_arg_value( elements: &[EvalAttributeArg], values: &mut impl RuntimeValueOps, ) -> Result { - let mut result = if elements.iter().any(|element| element.name().is_some()) { + let mut result = if elements + .iter() + .any(|element| element.name().is_some() || element.int_key().is_some()) + { values.assoc_new(elements.len())? } else { values.array_new(elements.len())? @@ -7498,7 +7503,7 @@ fn eval_reflection_attribute_array_arg_value( for (index, element) in elements.iter().enumerate() { let key = match element.name() { Some(name) => values.string(name)?, - None => values.int(index as i64)?, + None => values.int(element.int_key().unwrap_or(index as i64))?, }; let value = eval_reflection_attribute_arg_value(element.value(), values)?; result = values.array_set(result, key, value)?; diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 90abbc3367..339122ec4f 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3674,19 +3674,71 @@ fn eval_attribute_array_arg_from_elements( EvalArrayElement::Value(value) => eval_attribute_arg_from_expr(value), EvalArrayElement::KeyValue { key, value } => { let value = eval_attribute_arg_from_expr(value)?; - match key { - EvalExpr::Const(EvalConst::String(name)) => Some(EvalAttributeArg::Named { - name: name.clone(), - value: Box::new(value), - }), - _ => None, - } + eval_attribute_array_keyed_arg(key, value) } }) .collect::>>() .map(EvalAttributeArg::Array) } +/// Wraps an attribute array value with the PHP-normalized literal key metadata. +fn eval_attribute_array_keyed_arg( + key: &EvalExpr, + value: EvalAttributeArg, +) -> Option { + match key { + EvalExpr::Const(EvalConst::String(name)) => Some(EvalAttributeArg::Named { + name: name.clone(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Int(key)) => Some(EvalAttributeArg::IntKeyed { + key: *key, + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Bool(key)) => Some(EvalAttributeArg::IntKeyed { + key: i64::from(*key), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Named { + name: String::new(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Float(key)) => Some(EvalAttributeArg::IntKeyed { + key: *key as i64, + value: Box::new(value), + }), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => eval_attribute_array_negated_keyed_arg(expr, value), + EvalExpr::ClassNameFetch { class_name } => { + eval_attribute_class_name_arg(class_name).map(|name| EvalAttributeArg::Named { + name, + value: Box::new(value), + }) + } + _ => None, + } +} + +/// Wraps an attribute array value with a normalized negative numeric literal key. +fn eval_attribute_array_negated_keyed_arg( + key: &EvalExpr, + value: EvalAttributeArg, +) -> Option { + match key { + EvalExpr::Const(EvalConst::Int(key)) => Some(EvalAttributeArg::IntKeyed { + key: key.wrapping_neg(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Float(key)) => Some(EvalAttributeArg::IntKeyed { + key: (-*key) as i64, + value: Box::new(value), + }), + _ => None, + } +} + /// Returns a compile-time class-name string for named `ClassName::class` attribute args. fn eval_attribute_class_name_arg(class_name: &str) -> Option { let class_name = class_name.trim_start_matches('\\'); diff --git a/docs/php/eval.md b/docs/php/eval.md index 8287642ff8..ac6638c8fb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -222,10 +222,11 @@ attributes, are visible through `class_attribute_names()`, `class_attribute_args()`, and `class_get_attributes()` when their arguments fit the supported literal positional/named subset (`string`, `int`, `float`, `bool`, `null`, negated numeric literals, `ClassName::class` strings, or positional -or string-keyed array literals containing the same supported values). Positional +or scalar-keyed array literals containing the same supported values). Positional arguments keep integer keys in `class_attribute_args()` / -`ReflectionAttribute::getArguments()`, and named arguments keep their PHP names -as string keys. +`ReflectionAttribute::getArguments()`, named arguments keep their PHP names as +string keys, and array literal keys support string keys plus PHP-normalized +integer, boolean, null, and float keys. `ReflectionAttribute::newInstance()` instantiates eval-declared or bridge-supported generated/AOT attribute classes from those materialized attributes, and `ReflectionAttribute::getTarget()` / @@ -243,10 +244,10 @@ child methods and public access see the child property. property, class-constant, and method-parameter attributes for eval-declared class-like symbols and bridge-registered generated/AOT class-level, method, property, and class-constant attributes when their arguments fit the same -literal positional/named subset. String-keyed attribute array literals keep -their PHP string keys; dynamic or otherwise unsupported attribute array keys are -still unsupported metadata. `getName()` returns the reflected class, member, or -parameter name +literal positional/named subset. Attribute array literal keys support string +keys plus PHP-normalized integer, boolean, null, and float keys; dynamic or +otherwise unsupported attribute array keys are still unsupported metadata. +`getName()` returns the reflected class, member, or parameter name for those owners. `ReflectionClass`, `ReflectionObject`, `ReflectionFunction`, `ReflectionMethod`, `ReflectionProperty`, `ReflectionClassConstant`, `ReflectionEnumUnitCase`, and `ReflectionEnumBackedCase` expose `getDocComment()` and report `false` because diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 1997d4acfc..39e0187030 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12578,6 +12578,40 @@ echo count($instanceItems) . ":" . $instanceItems[0] . ":" . $instanceItems["nam assert_eq!(out.stdout, "3:plain:Ada:value:3:plain:Ada:value:3:plain:Ada:value"); } +/// Verifies eval attribute array arguments preserve PHP-normalized scalar keys. +#[test] +fn test_eval_declared_class_attribute_scalar_keyed_array_args() { + let out = compile_and_run_capture( + r#"items = $items; + } +} +#[EvalScalarKeyAttribute([2 => "two", true => "bool", null => "null", 1.8 => "float", "name" => "Ada"])] +class EvalScalarKeyAttributeTarget {} +$args = class_attribute_args("EvalScalarKeyAttributeTarget", "EvalScalarKeyAttribute"); +$items = $args[0]; +echo count($items) . ":" . $items[2] . ":" . $items[1] . ":" . $items[""] . ":" . $items["name"] . ":"; +$attr = class_get_attributes("EvalScalarKeyAttributeTarget")[0]; +$attrItems = $attr->getArguments()[0]; +echo count($attrItems) . ":" . $attrItems[2] . ":" . $attrItems[1] . ":" . $attrItems[""] . ":" . $attrItems["name"] . ":"; +$instanceItems = $attr->newInstance()->items; +echo count($instanceItems) . ":" . $instanceItems[2] . ":" . $instanceItems[1] . ":" . $instanceItems[""] . ":" . $instanceItems["name"];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "4:two:float:null:Ada:4:two:float:null:Ada:4:two:float:null:Ada" + ); +} + /// Verifies eval can read generated/AOT float attribute arguments. #[test] fn test_eval_reflection_class_exposes_aot_float_attribute_args() { From 5c5fb446c5e06fbeaa60ea7c1a6280dca48cb341 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 13:04:03 +0200 Subject: [PATCH 0856/1208] Support eval settable property hook types --- crates/elephc-magician/src/eval_ir.rs | 18 +++++ .../src/interpreter/reflection.rs | 55 +++++++++++++- .../src/interpreter/statements.rs | 6 +- .../tests/builtins_class_metadata.rs | 37 +++++++++ .../interpreter/tests/support/object_ops.rs | 7 +- .../elephc-magician/src/parser/statements.rs | 75 ++++++++++++------- .../src/parser/tests/classes_errors.rs | 48 ++++++++++++ docs/php/eval.md | 6 +- src/codegen/eval_reflection_owner_helpers.rs | 48 ++++++++++++ src/codegen/lower_inst/objects/reflection.rs | 12 +++ src/types/checker/builtin_types/reflection.rs | 8 +- tests/codegen/eval.rs | 36 +++++++++ 12 files changed, 319 insertions(+), 37 deletions(-) diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 081855b973..7e70ec3da2 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -1944,6 +1944,7 @@ pub struct EvalClassProperty { trait_origin: Option, attributes: Vec, property_type: Option, + set_hook_type: Option, visibility: EvalVisibility, set_visibility: Option, is_static: bool, @@ -2016,6 +2017,7 @@ impl EvalClassProperty { trait_origin: None, attributes: Vec::new(), property_type: None, + set_hook_type: None, visibility, set_visibility: None, is_static, @@ -2079,6 +2081,12 @@ impl EvalClassProperty { self } + /// Returns a copy of this property with retained explicit set-hook parameter type metadata. + pub fn with_set_hook_type(mut self, set_hook_type: Option) -> Self { + self.set_hook_type = set_hook_type; + self + } + /// Returns a copy of this property with PHP asymmetric write visibility metadata. pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { self.set_visibility = set_visibility; @@ -2111,6 +2119,16 @@ impl EvalClassProperty { self.property_type.as_ref() } + /// Returns retained PHP type metadata for an explicit set-hook parameter. + pub fn set_hook_type(&self) -> Option<&EvalParameterType> { + self.set_hook_type.as_ref() + } + + /// Returns the PHP-visible type accepted by property writes. + pub fn settable_type(&self) -> Option<&EvalParameterType> { + self.set_hook_type().or_else(|| self.property_type()) + } + /// Returns the PHP visibility declared for this property. pub const fn visibility(&self) -> EvalVisibility { self.visibility diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 319960cab6..6449fb49d8 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -104,6 +104,7 @@ struct EvalReflectionMemberMetadata { is_dynamic: bool, modifiers: u64, type_metadata: Option, + settable_type_metadata: Option, return_type_metadata: Option, default_value: Option, default_value_trait_origin: Option, @@ -2641,6 +2642,7 @@ fn eval_reflection_class_constant_object_result( None, None, None, + None, flags, modifiers, 0, @@ -2799,6 +2801,7 @@ fn eval_reflection_class_owner_object_result( None, None, None, + None, flags, modifiers, 0, @@ -2822,6 +2825,7 @@ fn eval_reflection_class_owner_object_result( None, None, None, + None, metadata.flags, metadata.modifiers, 0, @@ -2893,6 +2897,7 @@ fn eval_reflection_enum_object_result( None, None, None, + None, metadata.flags, metadata.modifiers, 0, @@ -3226,6 +3231,7 @@ fn eval_reflection_function_object_result( None, None, None, + None, eval_reflection_callable_flags(attributes), required_parameter_count as u64, 0, @@ -3989,6 +3995,7 @@ fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflection false, ), type_metadata: None, + settable_type_metadata: None, return_type_metadata: None, default_value: None, default_value_trait_origin: None, @@ -4149,6 +4156,7 @@ fn eval_reflection_aot_method_metadata( is_dynamic: false, modifiers: eval_reflection_method_modifiers_from_flags(flags), type_metadata: None, + settable_type_metadata: None, return_type_metadata, default_value: None, default_value_trait_origin: None, @@ -4510,6 +4518,7 @@ fn eval_reflection_aot_property_metadata( } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { modifiers |= 2048; } + let settable_type_metadata = type_metadata.clone(); EvalReflectionMemberMetadata { declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), source_file: None, @@ -4524,6 +4533,7 @@ fn eval_reflection_aot_property_metadata( is_dynamic: false, modifiers, type_metadata, + settable_type_metadata, return_type_metadata: None, default_value, default_value_trait_origin: None, @@ -4589,6 +4599,7 @@ fn eval_reflection_class_constant_object_result_or_throw( None, None, None, + None, flags, modifiers, 0, @@ -4762,6 +4773,7 @@ fn eval_reflection_enum_case_object_result( None, None, None, + None, flags, modifiers, 0, @@ -4812,6 +4824,7 @@ fn eval_reflection_owner_object( parent_class_name: Option<&str>, parameter_metadata: &[EvalReflectionParameterMetadata], type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + settable_type_metadata: Option<&EvalReflectionParameterTypeMetadata>, default_value: Option<&EvalExpr>, default_value_trait_origin: Option<&str>, flags: u64, @@ -4833,6 +4846,7 @@ fn eval_reflection_owner_object( parent_class_name, parameter_metadata, type_metadata, + settable_type_metadata, default_value, default_value_trait_origin, flags, @@ -4858,6 +4872,7 @@ fn eval_reflection_owner_object_with_members( parent_class_name: Option<&str>, parameter_metadata: &[EvalReflectionParameterMetadata], type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + settable_type_metadata: Option<&EvalReflectionParameterTypeMetadata>, default_value: Option<&EvalExpr>, default_value_trait_origin: Option<&str>, flags: u64, @@ -4963,9 +4978,21 @@ fn eval_reflection_owner_object_with_members( context, values, )?; - let (constant_value_cell, release_constant_value) = match constant_value { - Some(value) => (value, false), - None => (values.null()?, true), + let (constant_value_cell, release_constant_value) = if owner_kind + == EVAL_REFLECTION_OWNER_PROPERTY + { + match settable_type_metadata { + Some(type_metadata) => ( + eval_reflection_type_object_result(type_metadata, values)?, + true, + ), + None => (values.null()?, true), + } + } else { + match constant_value { + Some(value) => (value, false), + None => (values.null()?, true), + } }; let (backing_value_cell, release_backing_value) = match backing_value { Some(value) => (value, false), @@ -5121,6 +5148,7 @@ fn eval_reflection_full_class_object_result( None, None, None, + None, flags, modifiers, 0, @@ -5143,6 +5171,7 @@ fn eval_reflection_full_class_object_result( None, None, None, + None, metadata.flags, metadata.modifiers, 0, @@ -5179,6 +5208,7 @@ fn eval_reflection_shallow_class_object_result( None, None, None, + None, flags, modifiers, 0, @@ -5202,6 +5232,7 @@ fn eval_reflection_shallow_class_object_result( None, None, None, + None, metadata.flags, metadata.modifiers, 0, @@ -5521,6 +5552,7 @@ fn eval_reflection_declaring_function_object_result( None, None, None, + None, metadata.flags, metadata.required_parameter_count as u64, method_modifiers, @@ -5801,6 +5833,7 @@ fn eval_reflection_member_object_result( member.declaring_class_name.as_deref(), &member.parameters, member.type_metadata.as_ref(), + member.settable_type_metadata.as_ref(), member.default_value.as_ref(), member.default_value_trait_origin.as_deref(), flags, @@ -7195,6 +7228,7 @@ fn eval_reflection_method_metadata( method.is_abstract(), ), type_metadata: None, + settable_type_metadata: None, return_type_metadata, default_value: None, default_value_trait_origin: None, @@ -7269,6 +7303,7 @@ fn eval_reflection_method_metadata( true, ), type_metadata: None, + settable_type_metadata: None, return_type_metadata, default_value: None, default_value_trait_origin: None, @@ -7343,6 +7378,7 @@ fn eval_reflection_method_metadata( method.is_abstract(), ), type_metadata: None, + settable_type_metadata: None, return_type_metadata, default_value: None, default_value_trait_origin: None, @@ -7433,6 +7469,7 @@ fn eval_reflection_enum_synthetic_method_metadata( is_dynamic: false, modifiers: eval_reflection_method_modifiers(EvalVisibility::Public, true, false, false), type_metadata: None, + settable_type_metadata: None, return_type_metadata, default_value: None, default_value_trait_origin: None, @@ -7475,6 +7512,9 @@ fn eval_reflection_property_metadata( type_metadata: property .property_type() .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .settable_type() + .and_then(eval_reflection_parameter_type_metadata), return_type_metadata: None, default_value, default_value_trait_origin: property.trait_origin().map(str::to_string), @@ -7513,6 +7553,9 @@ fn eval_reflection_property_metadata( type_metadata: property .property_type() .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), return_type_metadata: None, default_value: None, default_value_trait_origin: None, @@ -7551,6 +7594,9 @@ fn eval_reflection_property_metadata( type_metadata: property .property_type() .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .settable_type() + .and_then(eval_reflection_parameter_type_metadata), return_type_metadata: None, default_value, default_value_trait_origin: property @@ -7958,6 +8004,7 @@ fn eval_reflection_property_hook_method_metadata( property.is_abstract(), ), type_metadata: None, + settable_type_metadata: None, return_type_metadata: eval_reflection_property_hook_return_type(property, hook), default_value: None, default_value_trait_origin: None, @@ -7976,7 +8023,7 @@ fn eval_reflection_property_hook_parameters( return Vec::new(); } let type_metadata = property - .property_type() + .settable_type() .and_then(eval_reflection_parameter_type_metadata); let has_type = type_metadata.is_some(); let is_array_type = eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 4c084b496f..26ec386f52 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2337,6 +2337,7 @@ fn eval_trait_concrete_properties_are_compatible( && existing.requires_set_hook() == incoming.requires_set_hook() && existing.is_virtual() == incoming.is_virtual() && existing.property_type() == incoming.property_type() + && existing.set_hook_type() == incoming.set_hook_type() && existing.default() == incoming.default() } @@ -2351,6 +2352,7 @@ fn eval_trait_abstract_properties_are_compatible( && existing.is_final() == incoming.is_final() && existing.is_readonly() == incoming.is_readonly() && existing.property_type() == incoming.property_type() + && existing.set_hook_type() == incoming.set_hook_type() && existing.default() == incoming.default() } @@ -3752,6 +3754,7 @@ fn class_property_can_cover_interface_contract( } if !class_property_type_satisfies_interface_contract( property.property_type(), + property.settable_type(), property_owner, requirement, requirement_owner, @@ -3775,6 +3778,7 @@ fn class_property_can_cover_interface_contract( /// Returns whether one property type is compatible with interface get/set hook signatures. fn class_property_type_satisfies_interface_contract( property_type: Option<&EvalParameterType>, + settable_type: Option<&EvalParameterType>, property_owner: &str, requirement: &EvalInterfaceProperty, requirement_owner: &str, @@ -3794,7 +3798,7 @@ fn class_property_type_satisfies_interface_contract( return false; } if requirement.requires_set() { - let property_types = vec![property_type.cloned()]; + let property_types = vec![settable_type.cloned()]; let requirement_types = vec![requirement.property_type().cloned()]; return method_parameter_type_signature_accepts( &property_types, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index e4bb6f96c5..8f04c3d7df 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -2494,6 +2494,43 @@ return true;"##, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionProperty retains explicit set-hook parameter metadata as the settable type. +#[test] +fn execute_program_reflection_property_get_settable_type_uses_set_hook_parameter() { + let program = parse_fragment( + br##"class EvalReflectSettableTypeTarget { + public string $value { + get => $this->value; + set(int|string $raw) => (string) $raw; + } +} +$property = new ReflectionProperty("EvalReflectSettableTypeTarget", "value"); +$type = $property->getType(); +$settable = $property->getSettableType(); +echo $type->getName(); echo ":"; +echo count($settable->getTypes()); +foreach ($settable->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; +} +$setHook = $property->getHook(PropertyHookType::Set); +$paramType = $setHook->getParameters()[0]->getType(); +echo ":"; echo count($paramType->getTypes()); +$box = new EvalReflectSettableTypeTarget(); +$box->value = 7; +echo ":"; echo $box->value; +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "string:2:intB:stringB:2:7"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionProperty exposes eval property default metadata. #[test] fn execute_program_reflection_property_get_default_value_metadata() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 8a90462954..41bf075e27 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -405,9 +405,13 @@ impl FakeOps { (FakeValue::Object(properties), "getposition") if args.is_empty() => { Self::object_property(&properties, "__position").map_or_else(|| self.int(0), Ok) } - (FakeValue::Object(properties), "gettype" | "getsettabletype") if args.is_empty() => { + (FakeValue::Object(properties), "gettype") if args.is_empty() => { Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) } + (FakeValue::Object(properties), "getsettabletype") if args.is_empty() => { + Self::object_property(&properties, "__settable_type") + .map_or_else(|| self.null(), Ok) + } (FakeValue::Object(properties), "getclass") if args.is_empty() => { Self::object_property(&properties, "__class").map_or_else(|| self.null(), Ok) } @@ -742,6 +746,7 @@ impl FakeOps { properties.push(("__is_readonly".to_string(), is_readonly)); properties.push(("__modifiers".to_string(), modifiers_cell)); properties.push(("__type".to_string(), method_objects)); + properties.push(("__settable_type".to_string(), constant_value)); properties.push(("__has_default_value".to_string(), has_default_value)); properties.push(("__is_promoted".to_string(), is_promoted)); properties.push(("__is_virtual".to_string(), is_virtual)); diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 339122ec4f..28f403dada 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -1191,8 +1191,17 @@ impl Parser { return Ok((property, Vec::new())); } let default_is_some = default.is_some(); - let (has_get_hook, has_set_hook, hook_methods) = - self.parse_property_hook_tail(&name, is_static, effective_readonly, default_is_some)?; + let (has_get_hook, has_set_hook, set_hook_type, hook_methods) = self + .parse_property_hook_tail( + &name, + property_type.as_ref(), + is_static, + effective_readonly, + default_is_some, + )?; + if set_hook_type.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } let is_virtual = (has_get_hook || has_set_hook) && !property_hook_methods_use_backing_slot(&hook_methods, &name); let property = EvalClassProperty::with_visibility_static_final_and_readonly( @@ -1204,6 +1213,7 @@ impl Parser { default, ) .with_type(property_type) + .with_set_hook_type(set_hook_type) .with_set_visibility(set_visibility) .with_hooks(has_get_hook, has_set_hook) .with_virtual(is_virtual); @@ -1214,12 +1224,13 @@ impl Parser { pub(super) fn parse_property_hook_tail( &mut self, property_name: &str, + property_type: Option<&EvalParameterType>, is_static: bool, is_readonly: bool, has_default: bool, - ) -> Result<(bool, bool, Vec), EvalParseError> { + ) -> Result<(bool, bool, Option, Vec), EvalParseError> { if self.consume(TokenKind::Semicolon) { - return Ok((false, false, Vec::new())); + return Ok((false, false, None, Vec::new())); } if !matches!(self.current(), TokenKind::LBrace) { return Err(EvalParseError::UnexpectedToken); @@ -1230,12 +1241,14 @@ impl Parser { self.advance(); let mut has_get_hook = false; let mut has_set_hook = false; + let mut set_hook_type = None; let mut methods = Vec::new(); while !self.consume(TokenKind::RBrace) { if matches!(self.current(), TokenKind::Eof) { return Err(EvalParseError::UnexpectedEof); } - let (is_get, method) = self.parse_property_hook_decl(property_name)?; + let (is_get, hook_set_type, method) = + self.parse_property_hook_decl(property_name, property_type)?; if is_get { if has_get_hook { return Err(EvalParseError::UnsupportedConstruct); @@ -1246,20 +1259,22 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } has_set_hook = true; + set_hook_type = hook_set_type; } methods.push(method); } if !has_get_hook && !has_set_hook { return Err(EvalParseError::UnsupportedConstruct); } - Ok((has_get_hook, has_set_hook, methods)) + Ok((has_get_hook, has_set_hook, set_hook_type, methods)) } /// Parses one concrete `get` or `set` property hook declaration. pub(super) fn parse_property_hook_decl( &mut self, property_name: &str, - ) -> Result<(bool, EvalClassMethod), EvalParseError> { + property_type: Option<&EvalParameterType>, + ) -> Result<(bool, Option, EvalClassMethod), EvalParseError> { let returns_by_ref = self.consume(TokenKind::Ampersand); let TokenKind::Ident(hook_name) = self.current() else { return Err(EvalParseError::UnexpectedToken); @@ -1274,10 +1289,11 @@ impl Parser { } let source_start_line = self.current_line(); self.advance(); - let params = if is_set { - vec![self.parse_property_set_hook_param()?] + let (params, set_hook_type) = if is_set { + let (param, set_hook_type) = self.parse_property_set_hook_param()?; + (vec![param], set_hook_type) } else { - Vec::new() + (Vec::new(), None) }; let (body, source_end_line) = match self.current() { TokenKind::Semicolon => return Err(EvalParseError::UnsupportedConstruct), @@ -1305,34 +1321,39 @@ impl Parser { } else { property_hook_set_method(property_name) }; - Ok(( - is_get, - EvalClassMethod::with_visibility_and_modifiers( - method_name, - EvalVisibility::Public, - false, - false, - false, - params, - body, - ) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)), - )) + let mut method = EvalClassMethod::with_visibility_and_modifiers( + method_name, + EvalVisibility::Public, + false, + false, + false, + params, + body, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)); + if is_set { + method = method.with_parameter_types(vec![ + set_hook_type.clone().or_else(|| property_type.cloned()), + ]); + } + Ok((is_get, set_hook_type, method)) } /// Parses an optional set-hook parameter list and returns the hook value variable. - pub(super) fn parse_property_set_hook_param(&mut self) -> Result { + pub(super) fn parse_property_set_hook_param( + &mut self, + ) -> Result<(String, Option), EvalParseError> { if !self.consume(TokenKind::LParen) { - return Ok("value".to_string()); + return Ok(("value".to_string(), None)); } - let _ = self.parse_optional_property_type()?; + let set_hook_type = self.parse_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { return Err(EvalParseError::ExpectedVariable); }; let name = name.clone(); self.advance(); self.expect(TokenKind::RParen)?; - Ok(name) + Ok((name, set_hook_type)) } /// Parses `trait Name { ... }` declarations into dynamic trait metadata. diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 325b5ceb37..7d9c13977e 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -885,6 +885,10 @@ fn parse_fragment_accepts_concrete_class_property_hooks() { } }] ) + .with_parameter_types(vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))]) .with_source_location(EvalSourceLocation::new(4, 4)) ] ) @@ -892,6 +896,50 @@ fn parse_fragment_accepts_concrete_class_property_hooks() { ); } +/// Verifies typed set-hook parameters are retained separately from the property type. +#[test] +fn parse_fragment_retains_property_set_hook_parameter_type() { + let program = parse_fragment( + br#"class DynEvalTypedSetHooked { + public string $value { + set(int|string $raw) => $raw; + } +}"#, + ) + .expect("fragment should parse"); + let EvalStmt::ClassDecl(class) = &program.statements()[0] else { + panic!("expected class declaration"); + }; + let property = &class.properties()[0]; + assert_eq!( + property.property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + )) + ); + assert_eq!( + property.set_hook_type(), + Some(&EvalParameterType::new( + vec![ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ], + false + )) + ); + assert_eq!( + class.methods()[0].parameter_types(), + &[Some(EvalParameterType::new( + vec![ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ], + false + ))] + ); +} + /// Verifies abstract property hook contracts lower without concrete accessors. #[test] fn parse_fragment_accepts_abstract_class_property_hook_contracts() { diff --git a/docs/php/eval.md b/docs/php/eval.md index ac6638c8fb..1662cc3edb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -558,9 +558,9 @@ dynamic properties on the inspected object. `ReflectionProperty::hasType()`, `getType()`, and `getSettableType()` expose retained property type metadata through `ReflectionNamedType`, `ReflectionUnionType`, and `ReflectionIntersectionType` where eval has retained -a supported declared type. For the supported property surface, -`getSettableType()` currently returns the same retained type metadata as -`getType()`. +a supported declared type. `getSettableType()` follows an explicit property +`set(Type $value)` hook parameter when one is retained; otherwise it falls back +to the declared property type. `ReflectionProperty::hasDefaultValue()` and `getDefaultValue()` expose materialized property default metadata, including PHP's implicit `null` default for untyped concrete properties without an explicit initializer. diff --git a/src/codegen/eval_reflection_owner_helpers.rs b/src/codegen/eval_reflection_owner_helpers.rs index 78c6555ae8..cc3eeef895 100644 --- a/src/codegen/eval_reflection_owner_helpers.rs +++ b/src/codegen/eval_reflection_owner_helpers.rs @@ -106,6 +106,8 @@ struct ReflectionOwnerLayout { has_type_hi: Option, parameter_type_lo: Option, parameter_type_hi: Option, + settable_type_lo: Option, + settable_type_hi: Option, parameter_class_lo: Option, parameter_class_hi: Option, has_default_value_lo: Option, @@ -279,6 +281,7 @@ fn reflection_owner_layout(info: &ClassInfo, has_name: bool) -> Option OptiongetSettableType()->getTypes());'); ); } +/// Verifies eval ReflectionProperty uses explicit set-hook parameter metadata for settable type. +#[test] +fn test_eval_reflection_property_get_settable_type_uses_set_hook_parameter() { + let out = compile_and_run_capture( + r#" $this->value; + set(int|string $raw) => (string) $raw; + } +} +$property = new ReflectionProperty("EvalReflectSettableTypeTarget", "value"); +$type = $property->getType(); +$settable = $property->getSettableType(); +echo $type->getName() . ":"; +echo count($settable->getTypes()); +foreach ($settable->getTypes() as $memberType) { + echo ":" . $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; +} +$setHook = $property->getHook(PropertyHookType::Set); +$paramType = $setHook->getParameters()[0]->getType(); +echo ":" . count($paramType->getTypes()); +$box = new EvalReflectSettableTypeTarget(); +$box->value = 7; +echo ":" . $box->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "string:2:intB:stringB:2:7"); +} + /// Verifies eval ReflectionProperty materializes property default metadata through the bridge. #[test] fn test_eval_reflection_property_get_default_value_metadata() { From d10855f8d36b6eb03d8540c2aa29c838c3531aa1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 13:14:46 +0200 Subject: [PATCH 0857/1208] Support eval comma-separated class constants --- .../src/interpreter/tests/class_constants.rs | 35 ++++++++ .../elephc-magician/src/parser/statements.rs | 64 ++++++++------- .../src/parser/tests/classes_errors.rs | 80 +++++++++++++++++++ docs/php/eval.md | 5 +- tests/codegen/eval.rs | 35 ++++++++ 5 files changed, 190 insertions(+), 29 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/class_constants.rs b/crates/elephc-magician/src/interpreter/tests/class_constants.rs index 34bfc98b48..05d8299ab7 100644 --- a/crates/elephc-magician/src/interpreter/tests/class_constants.rs +++ b/crates/elephc-magician/src/interpreter/tests/class_constants.rs @@ -46,6 +46,41 @@ return EvalConstChild::hidden();"#, assert_eq!(values.get(result), FakeValue::Int(5)); } +/// Verifies comma-separated class-like constants are registered and fetchable. +#[test] +fn execute_program_reads_comma_separated_eval_class_like_constants() { + let program = parse_fragment( + br#"class EvalMultiConstClass { + public const A = 1, B = 2; +} +interface EvalMultiConstIface { + public const C = 3, D = 4; +} +trait EvalMultiConstTrait { + public const E = 5, F = 6; +} +class EvalMultiConstTraitBox { + use EvalMultiConstTrait; +} +enum EvalMultiConstEnum { + public const G = 7, H = 8; + case Ready; +} +echo EvalMultiConstClass::A; echo EvalMultiConstClass::B; echo ":"; +echo EvalMultiConstIface::C; echo EvalMultiConstIface::D; echo ":"; +echo EvalMultiConstTraitBox::E; echo EvalMultiConstTraitBox::F; echo ":"; +return EvalMultiConstEnum::G + EvalMultiConstEnum::H;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12:34:56:"); + assert_eq!(values.get(result), FakeValue::Int(15)); +} + /// Verifies protected class constant access from global eval scope throws Error. #[test] fn execute_program_protected_eval_class_constant_from_global_scope_throws_error() { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 28f403dada..7f7a8516c4 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -718,12 +718,12 @@ impl Parser { if is_static || is_abstract || is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } - constants.push( + constants.extend( self.parse_class_const_decl( visibility.unwrap_or(EvalVisibility::Public), is_final, - )? - .with_attributes(attributes), + &attributes, + )?, ); return Ok(()); } @@ -796,27 +796,36 @@ impl Parser { } } - /// Parses one eval class constant declaration. + /// Parses one eval class constant declaration, including comma-separated constants. pub(super) fn parse_class_const_decl( &mut self, visibility: EvalVisibility, is_final: bool, - ) -> Result { + attributes: &[EvalAttribute], + ) -> Result, EvalParseError> { self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - if ident_eq(name, "class") { - return Err(EvalParseError::UnsupportedConstruct); + let mut constants = Vec::new(); + loop { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + if ident_eq(name, "class") { + return Err(EvalParseError::UnsupportedConstruct); + } + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + constants.push( + EvalClassConstant::with_visibility_and_final(name, visibility, is_final, value) + .with_attributes(attributes.to_vec()), + ); + if !self.consume(TokenKind::Comma) { + break; + } } - let name = name.clone(); - self.advance(); - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; self.expect_semicolon()?; - Ok(EvalClassConstant::with_visibility_and_final( - name, visibility, is_final, value, - )) + Ok(constants) } /// Parses `use TraitName, OtherTrait;` or an adaptation block inside an eval class body. @@ -1444,12 +1453,12 @@ impl Parser { if is_static || is_abstract || is_readonly || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } - constants.push( + constants.extend( self.parse_class_const_decl( visibility.unwrap_or(EvalVisibility::Public), is_final, - )? - .with_attributes(attributes), + &attributes, + )?, ); return Ok(()); } @@ -1611,12 +1620,12 @@ impl Parser { if is_static { return Err(EvalParseError::UnsupportedConstruct); } - constants.push( + constants.extend( self.parse_class_const_decl( visibility.unwrap_or(EvalVisibility::Public), is_final, - )? - .with_attributes(attributes), + &attributes, + )?, ); return Ok(()); } @@ -1778,10 +1787,11 @@ impl Parser { if is_static || set_visibility.is_some() { return Err(EvalParseError::UnsupportedConstruct); } - constants.push( - self.parse_class_const_decl(EvalVisibility::Public, is_final)? - .with_attributes(attributes), - ); + constants.extend(self.parse_class_const_decl( + EvalVisibility::Public, + is_final, + &attributes, + )?); return Ok(()); } if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 7d9c13977e..ceeccd75c5 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -144,6 +144,86 @@ fn parse_fragment_rejects_reserved_class_constant_name() { } } +/// Verifies comma-separated class-like constants lower to individual eval constants. +#[test] +fn parse_fragment_accepts_comma_separated_class_like_constants() { + let program = parse_fragment( + br#"class DynEvalMultiConstClass { + public const A = 1, B = 2; +} +interface DynEvalMultiConstIface { + final public const C = 3, D = 4; +} +trait DynEvalMultiConstTrait { + protected const E = 5, F = 6; +} +enum DynEvalMultiConstEnum { + public const G = 7, H = 8; + case Ready; +}"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::ClassDecl(class) = &statements[0] else { + panic!("expected class declaration"); + }; + assert_eq!( + class.constants(), + &[ + EvalClassConstant::new("A", EvalExpr::Const(EvalConst::Int(1))), + EvalClassConstant::new("B", EvalExpr::Const(EvalConst::Int(2))), + ], + ); + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + assert_eq!( + interface.constants(), + &[ + EvalClassConstant::with_visibility_and_final( + "C", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(3)), + ), + EvalClassConstant::with_visibility_and_final( + "D", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(4)), + ), + ], + ); + let EvalStmt::TraitDecl(trait_decl) = &statements[2] else { + panic!("expected trait declaration"); + }; + assert_eq!( + trait_decl.constants(), + &[ + EvalClassConstant::with_visibility( + "E", + EvalVisibility::Protected, + EvalExpr::Const(EvalConst::Int(5)), + ), + EvalClassConstant::with_visibility( + "F", + EvalVisibility::Protected, + EvalExpr::Const(EvalConst::Int(6)), + ), + ], + ); + let EvalStmt::EnumDecl(enum_decl) = &statements[3] else { + panic!("expected enum declaration"); + }; + assert_eq!( + enum_decl.constants(), + &[ + EvalClassConstant::new("G", EvalExpr::Const(EvalConst::Int(7))), + EvalClassConstant::new("H", EvalExpr::Const(EvalConst::Int(8))), + ], + ); +} + /// Verifies class attributes lower to eval class metadata with supported literal args. #[test] fn parse_fragment_accepts_class_attribute_metadata() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 1662cc3edb..ff9fa6b8d5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -167,8 +167,9 @@ including asymmetric write visibility, property-level `readonly`, `readonly clas `__construct()`, abstract classes and methods, final classes, methods, and properties, trait composition with `insteadof` conflict resolution and `as` aliases/visibility adaptations, interface implementation checks, static -properties, static methods, static interface method contracts, class, interface, -trait, and enum constants including `final` constants, class-level attributes, +properties, static methods, static interface method contracts, single and +comma-separated class, interface, trait, and enum constants including `final` +constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and `__callStatic()`, and magic property fallback through `__get()` and `__set()`. Eval traits also accept the legacy `var` marker for public properties. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0247da1421..9721a9a86c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12337,6 +12337,41 @@ echo EvalConstChild::hidden();'); assert_eq!(out.stdout, "2:7:9:2:5"); } +/// Verifies eval-declared class-like constants support PHP comma-separated declarations. +#[test] +fn test_eval_declared_comma_separated_class_like_constants() { + let out = compile_and_run_capture( + r#" Date: Sat, 27 Jun 2026 13:26:23 +0200 Subject: [PATCH 0858/1208] Support eval comma-separated properties --- .../src/interpreter/tests/classes.rs | 33 ++++ .../elephc-magician/src/parser/statements.rs | 178 +++++++++++------- .../src/parser/tests/classes_errors.rs | 58 ++++++ docs/php/eval.md | 4 +- tests/codegen/eval.rs | 28 +++ 5 files changed, 236 insertions(+), 65 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 75b48a6d4b..f6e127e8af 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -112,6 +112,39 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies comma-separated eval properties initialize instance, static, and trait storage. +#[test] +fn execute_program_reads_comma_separated_eval_properties() { + let program = parse_fragment( + br#"class EvalMultiPropertyBox { + public int $a = 1, $b = 2; + public static int $s = 3, $t = 4; + public function sum() { return $this->a + $this->b + self::$s + self::$t; } +} +trait EvalMultiPropertyTrait { + public int $x = 5, $y = 6; +} +class EvalMultiPropertyTraitBox { + use EvalMultiPropertyTrait; + public function sum() { return $this->x + $this->y; } +} +$box = new EvalMultiPropertyBox(); +$traitBox = new EvalMultiPropertyTraitBox(); +echo $box->a; echo $box->b; echo ":"; +echo EvalMultiPropertyBox::$s; echo EvalMultiPropertyBox::$t; echo ":"; +echo $traitBox->x; echo $traitBox->y; echo ":"; +return $box->sum() + $traitBox->sum();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12:34:56:"); + assert_eq!(values.get(result), FakeValue::Int(21)); +} + /// Verifies eval static method calls preserve PHP late-static forwarding rules. #[test] fn execute_program_forwards_eval_static_method_called_class() { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 7f7a8516c4..5404a361b1 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -744,7 +744,7 @@ impl Parser { } let visibility = visibility.unwrap_or(EvalVisibility::Public); - let (property, mut hook_methods) = self.parse_class_property_decl( + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( visibility, set_visibility, is_static, @@ -753,7 +753,11 @@ impl Parser { is_readonly_class, is_abstract, )?; - properties.push(property.with_attributes(attributes)); + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); methods.append(&mut hook_methods); Ok(()) } @@ -771,7 +775,7 @@ impl Parser { return Err(EvalParseError::UnsupportedConstruct); } self.advance(); - let (property, mut hook_methods) = self.parse_class_property_decl( + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( EvalVisibility::Public, None, false, @@ -780,7 +784,11 @@ impl Parser { is_readonly_class, false, )?; - properties.push(property.with_attributes(attributes)); + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); methods.append(&mut hook_methods); Ok(()) } @@ -1141,7 +1149,7 @@ impl Parser { )) } - /// Parses one public property declaration with an optional initializer. + /// Parses one property declaration, including comma-separated simple properties. pub(super) fn parse_class_property_decl( &mut self, visibility: EvalVisibility, @@ -1151,7 +1159,7 @@ impl Parser { is_readonly: bool, is_readonly_class: bool, is_abstract: bool, - ) -> Result<(EvalClassProperty, Vec), EvalParseError> { + ) -> Result<(Vec, Vec), EvalParseError> { if is_static && is_readonly { return Err(EvalParseError::UnsupportedConstruct); } @@ -1168,65 +1176,105 @@ impl Parser { if set_visibility.is_some() && property_type.is_none() { return Err(EvalParseError::UnsupportedConstruct); } - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - let default = if self.consume(TokenKind::Equal) { - if is_abstract || effective_readonly { - return Err(EvalParseError::UnsupportedConstruct); + let mut properties = Vec::new(); + let mut hook_methods = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let default = if self.consume(TokenKind::Equal) { + if is_abstract || effective_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + Some(self.parse_expr()?) + } else { + None + }; + if is_abstract { + if is_static || effective_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + let (requires_get_hook, requires_set_hook) = + self.parse_property_hook_contracts()?; + let property = EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + None, + ) + .with_type(property_type) + .with_set_visibility(set_visibility) + .with_abstract_hook_contract(requires_get_hook, requires_set_hook); + return Ok((vec![property], Vec::new())); + } + let default_is_some = default.is_some(); + if self.consume(TokenKind::Comma) { + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_visibility(set_visibility), + ); + continue; } - Some(self.parse_expr()?) - } else { - None - }; - if is_abstract { - if is_static || effective_readonly { + if !properties.is_empty() { + self.expect_semicolon()?; + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_visibility(set_visibility), + ); + break; + } + let (has_get_hook, has_set_hook, set_hook_type, parsed_hook_methods) = self + .parse_property_hook_tail( + &name, + property_type.as_ref(), + is_static, + effective_readonly, + default_is_some, + )?; + if set_hook_type.is_some() && property_type.is_none() { return Err(EvalParseError::UnsupportedConstruct); } - let (requires_get_hook, requires_set_hook) = self.parse_property_hook_contracts()?; - let property = EvalClassProperty::with_visibility_static_final_and_readonly( - name, - visibility, - is_static, - is_final, - effective_readonly, - None, - ) - .with_type(property_type) - .with_set_visibility(set_visibility) - .with_abstract_hook_contract(requires_get_hook, requires_set_hook); - return Ok((property, Vec::new())); - } - let default_is_some = default.is_some(); - let (has_get_hook, has_set_hook, set_hook_type, hook_methods) = self - .parse_property_hook_tail( - &name, - property_type.as_ref(), - is_static, - effective_readonly, - default_is_some, - )?; - if set_hook_type.is_some() && property_type.is_none() { - return Err(EvalParseError::UnsupportedConstruct); + let is_virtual = (has_get_hook || has_set_hook) + && !property_hook_methods_use_backing_slot(&parsed_hook_methods, &name); + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_hook_type(set_hook_type) + .with_set_visibility(set_visibility) + .with_hooks(has_get_hook, has_set_hook) + .with_virtual(is_virtual), + ); + hook_methods.extend(parsed_hook_methods); + break; } - let is_virtual = (has_get_hook || has_set_hook) - && !property_hook_methods_use_backing_slot(&hook_methods, &name); - let property = EvalClassProperty::with_visibility_static_final_and_readonly( - name, - visibility, - is_static, - is_final, - effective_readonly, - default, - ) - .with_type(property_type) - .with_set_hook_type(set_hook_type) - .with_set_visibility(set_visibility) - .with_hooks(has_get_hook, has_set_hook) - .with_virtual(is_virtual); - Ok((property, hook_methods)) + Ok((properties, hook_methods)) } /// Parses `;` or a concrete eval property hook block after one property declaration. @@ -1492,7 +1540,7 @@ impl Parser { return Ok(()); } let visibility = visibility.unwrap_or(EvalVisibility::Public); - let (property, mut hook_methods) = self.parse_class_property_decl( + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( visibility, set_visibility, is_static, @@ -1501,7 +1549,11 @@ impl Parser { false, is_abstract, )?; - properties.push(property.with_attributes(attributes)); + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); methods.append(&mut hook_methods); Ok(()) } diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index ceeccd75c5..92062d6fd7 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -690,6 +690,64 @@ fn parse_fragment_accepts_constructor_promoted_properties() { ); } +/// Verifies comma-separated simple properties lower to individual eval properties. +#[test] +fn parse_fragment_accepts_comma_separated_class_properties() { + let program = parse_fragment( + br#"class DynEvalMultiProperties { + public private(set) int $id = 1, $nextId; + public static string $first = "a", $second = "b"; + var $legacy, $legacyDefault = 3; +} +trait DynEvalMultiPropertyTrait { + protected int $left = 4, $right = 5; +}"#, + ) + .expect("fragment should parse"); + let EvalStmt::ClassDecl(class) = &program.statements()[0] else { + panic!("expected class declaration"); + }; + let properties = class.properties(); + assert_eq!(properties.len(), 6); + assert_eq!(properties[0].name(), "id"); + assert_eq!(properties[0].set_visibility(), Some(EvalVisibility::Private)); + assert_eq!( + properties[0].property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false, + )), + ); + assert_eq!(properties[0].default(), Some(&EvalExpr::Const(EvalConst::Int(1)))); + assert_eq!(properties[1].name(), "nextId"); + assert_eq!(properties[1].set_visibility(), Some(EvalVisibility::Private)); + assert_eq!(properties[1].default(), None); + assert_eq!(properties[2].name(), "first"); + assert!(properties[2].is_static()); + assert_eq!( + properties[2].property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false, + )), + ); + assert_eq!(properties[3].name(), "second"); + assert!(properties[3].is_static()); + assert_eq!(properties[4].name(), "legacy"); + assert_eq!(properties[4].property_type(), None); + assert_eq!(properties[5].name(), "legacyDefault"); + assert_eq!(properties[5].default(), Some(&EvalExpr::Const(EvalConst::Int(3)))); + + let EvalStmt::TraitDecl(trait_decl) = &program.statements()[1] else { + panic!("expected trait declaration"); + }; + assert_eq!(trait_decl.properties().len(), 2); + assert_eq!(trait_decl.properties()[0].name(), "left"); + assert_eq!(trait_decl.properties()[0].visibility(), EvalVisibility::Protected); + assert_eq!(trait_decl.properties()[1].name(), "right"); + assert_eq!(trait_decl.properties()[1].visibility(), EvalVisibility::Protected); +} + /// Verifies by-reference promoted constructor parameters lower to property aliases. #[test] fn parse_fragment_accepts_by_reference_constructor_promoted_properties() { diff --git a/docs/php/eval.md b/docs/php/eval.md index ff9fa6b8d5..469d683ffd 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -156,8 +156,8 @@ containers to eval-declared functions. ## Classes and objects Eval-declared classes support inheritance, public/protected/private properties, -PHP's legacy `var` marker for public properties, and methods, asymmetric -property write visibility (`private(set)` / +comma-separated simple property declarations, PHP's legacy `var` marker for +public properties, and methods, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 9721a9a86c..642b1b5a57 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7483,6 +7483,34 @@ echo $label->getType()->getName();'); assert_eq!(out, "p:P:t:C:int:N:null:trait:L:string"); } +/// Verifies eval supports PHP comma-separated instance, static, and trait properties. +#[test] +fn test_eval_declared_comma_separated_properties() { + let out = compile_and_run( + r#"a + $this->b + self::$s + self::$t; } +} +trait EvalMultiPropertyTrait { + public int $x = 5, $y = 6; +} +class EvalMultiPropertyTraitBox { + use EvalMultiPropertyTrait; + public function sum() { return $this->x + $this->y; } +} +$box = new EvalMultiPropertyBox(); +$traitBox = new EvalMultiPropertyTraitBox(); +echo $box->a . $box->b . ":"; +echo EvalMultiPropertyBox::$s . EvalMultiPropertyBox::$t . ":"; +echo $traitBox->x . $traitBox->y . ":"; +return $box->sum() + $traitBox->sum();'); +"#, + ); + assert_eq!(out, "12:34:56:21"); +} + /// Verifies native callable probes can see functions declared by eval after the barrier. #[test] fn test_eval_declared_function_is_visible_to_callable_probes() { From 05d3091d26068e9e32960914bb5ad22a7f35942e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 13:40:25 +0200 Subject: [PATCH 0859/1208] Reject eval reserved class-like names --- crates/elephc-magician/src/parser/cursor.rs | 92 +++++++++++++++++++ .../elephc-magician/src/parser/statements.rs | 37 ++++---- .../src/parser/tests/classes_errors.rs | 50 ++++++++++ docs/php/eval.md | 6 +- tests/codegen/eval.rs | 28 ++++++ 5 files changed, 191 insertions(+), 22 deletions(-) diff --git a/crates/elephc-magician/src/parser/cursor.rs b/crates/elephc-magician/src/parser/cursor.rs index 960b09581c..ff11bc100d 100644 --- a/crates/elephc-magician/src/parser/cursor.rs +++ b/crates/elephc-magician/src/parser/cursor.rs @@ -129,6 +129,98 @@ pub(super) fn ident_eq(actual: &str, expected: &str) -> bool { actual.eq_ignore_ascii_case(expected) } +/// Returns true when PHP forbids a name for class, interface, trait, or enum declarations. +pub(super) fn is_reserved_class_like_name(name: &str) -> bool { + [ + "__halt_compiler", + "abstract", + "and", + "array", + "as", + "break", + "callable", + "case", + "catch", + "class", + "clone", + "const", + "continue", + "declare", + "default", + "die", + "do", + "echo", + "else", + "elseif", + "empty", + "enddeclare", + "endfor", + "endforeach", + "endif", + "endswitch", + "endwhile", + "eval", + "exit", + "extends", + "false", + "final", + "finally", + "fn", + "for", + "foreach", + "function", + "global", + "goto", + "if", + "implements", + "include", + "include_once", + "instanceof", + "insteadof", + "interface", + "isset", + "list", + "match", + "namespace", + "new", + "null", + "or", + "print", + "private", + "protected", + "public", + "readonly", + "require", + "require_once", + "return", + "static", + "switch", + "throw", + "trait", + "true", + "try", + "unset", + "use", + "var", + "while", + "xor", + "yield", + "bool", + "int", + "float", + "string", + "object", + "mixed", + "never", + "void", + "iterable", + "self", + "parent", + ] + .iter() + .any(|keyword| ident_eq(name, keyword)) +} + /// Returns true for PHP statement forms that the eval subset intentionally does not parse yet. pub(super) fn is_unsupported_statement_keyword(name: &str) -> bool { let _ = name; diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 5404a361b1..ef1e5bd788 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -498,11 +498,7 @@ impl Parser { } let source_start_line = self.current_line(); self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); + let name = self.parse_class_like_decl_name()?; let parent = self.parse_class_parent_clause()?; let interfaces = self.parse_class_interface_clause()?; let body = self.parse_class_body_members(is_readonly_class)?; @@ -565,6 +561,19 @@ impl Parser { Ok(EvalExpr::NewAnonymousClass { class, args }) } + /// Parses and namespace-qualifies a declared class, interface, trait, or enum name. + fn parse_class_like_decl_name(&mut self) -> Result { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + if is_reserved_class_like_name(name) { + return Err(EvalParseError::UnsupportedConstruct); + } + let qualified_name = self.qualify_name_in_current_namespace(name); + self.advance(); + Ok(qualified_name) + } + /// Parses members inside a class body after relation clauses. fn parse_class_body_members( &mut self, @@ -1425,11 +1434,7 @@ impl Parser { ) -> Result, EvalParseError> { let source_start_line = self.current_line(); self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); + let name = self.parse_class_like_decl_name()?; self.expect(TokenKind::LBrace)?; let mut traits = Vec::new(); let mut trait_adaptations = Vec::new(); @@ -1570,11 +1575,7 @@ impl Parser { ) -> Result, EvalParseError> { let source_start_line = self.current_line(); self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); + let name = self.parse_class_like_decl_name()?; let backing_type = self.parse_enum_backing_type()?; let interfaces = self.parse_class_interface_clause()?; self.expect(TokenKind::LBrace)?; @@ -1726,11 +1727,7 @@ impl Parser { ) -> Result, EvalParseError> { let source_start_line = self.current_line(); self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); + let name = self.parse_class_like_decl_name()?; let parents = self.parse_interface_parent_clause()?; self.expect(TokenKind::LBrace)?; let mut constants = Vec::new(); diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 92062d6fd7..e9f1ae6c3f 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -144,6 +144,56 @@ fn parse_fragment_rejects_reserved_class_constant_name() { } } +/// Verifies eval rejects PHP-reserved class-like declaration names. +#[test] +fn parse_fragment_rejects_reserved_class_like_declaration_names() { + for source in [ + b"class match {}" as &[u8], + b"class string {}", + b"class CLASS {}", + b"interface interface {}", + b"trait readonly {}", + b"enum bool { case Ready; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval accepts PHP semi-reserved class-like declaration names. +#[test] +fn parse_fragment_accepts_semi_reserved_class_like_declaration_names() { + let program = parse_fragment( + br#"class enum {} +interface from {} +trait resource {} +enum integer { case Ready; }"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::ClassDecl(class) = &statements[0] else { + panic!("expected class declaration"); + }; + assert_eq!(class.name(), "enum"); + + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + assert_eq!(interface.name(), "from"); + + let EvalStmt::TraitDecl(trait_decl) = &statements[2] else { + panic!("expected trait declaration"); + }; + assert_eq!(trait_decl.name(), "resource"); + + let EvalStmt::EnumDecl(enum_decl) = &statements[3] else { + panic!("expected enum declaration"); + }; + assert_eq!(enum_decl.name(), "integer"); +} + /// Verifies comma-separated class-like constants lower to individual eval constants. #[test] fn parse_fragment_accepts_comma_separated_class_like_constants() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 469d683ffd..b00def35c6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,8 +49,8 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes and traits with properties including PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait constants including `final` constants, and class-level attributes. Duplicate eval class-like names are rejected. | -| Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | +| Classes | Eval fragments can declare classes and traits with properties including comma-separated simple property declarations, PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait comma-separated constants including `final` constants, and class-level attributes. Duplicate eval class-like names and PHP-reserved class-like declaration names are rejected. | +| Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, comma-separated constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -172,6 +172,8 @@ comma-separated class, interface, trait, and enum constants including `final` constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and `__callStatic()`, and magic property fallback through `__get()` and `__set()`. +Reserved PHP class-like declaration names are rejected, while semi-reserved +names that PHP allows, such as `enum`, `from`, and `resource`, remain valid. Eval traits also accept the legacy `var` marker for public properties. PHP's global `#[Override]` marker is validated on eval-declared methods against non-private parent methods and eval interface method contracts. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 642b1b5a57..2dd1dc8f32 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12433,6 +12433,34 @@ eval('enum EvalBadEnumConstName { } } +/// Verifies eval rejects PHP-reserved class-like declaration names. +#[test] +fn test_eval_declared_reserved_class_like_name_fails() { + for source in [ + r#" Date: Sat, 27 Jun 2026 13:51:46 +0200 Subject: [PATCH 0860/1208] Validate eval property set hook parameter types --- .../src/interpreter/statements.rs | 37 ++++++++++++++- .../src/interpreter/tests/classes.rs | 45 ++++++++++++++++++ .../elephc-magician/src/parser/statements.rs | 14 ++++-- .../src/parser/tests/classes_errors.rs | 24 ++++++++++ docs/php/eval.md | 4 +- tests/codegen/eval.rs | 47 +++++++++++++++++++ 6 files changed, 163 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 26ec386f52..9bcb291f5f 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2568,7 +2568,7 @@ fn validate_eval_class_modifiers( for constant in class.constants() { validate_constant_parent_redeclaration(class, constant, context)?; } - validate_eval_declared_properties(class)?; + validate_eval_declared_properties(class, context)?; for property in class.properties() { validate_property_parent_redeclaration(class, property, context)?; } @@ -2938,7 +2938,10 @@ fn magic_return_type_matches( } /// Validates property declarations that can be checked before class registration. -fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus> { +fn validate_eval_declared_properties( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); for property in class.properties() { if !names.insert(property.name().to_string()) { @@ -2981,10 +2984,40 @@ fn validate_eval_declared_properties(class: &EvalClass) -> Result<(), EvalStatus { return Err(EvalStatus::RuntimeFatal); } + validate_eval_property_set_hook_parameter_type(class, property, context)?; } Ok(()) } +/// Validates that an explicit set-hook parameter type can accept every property value. +fn validate_eval_property_set_hook_parameter_type( + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let Some(set_hook_type) = property.set_hook_type() else { + return Ok(()); + }; + let Some(property_type) = property.property_type() else { + return Err(EvalStatus::RuntimeFatal); + }; + let set_hook_types = vec![Some(set_hook_type.clone())]; + let property_types = vec![Some(property_type.clone())]; + method_parameter_type_signature_accepts( + &set_hook_types, + &[], + class.name(), + &property_types, + &[], + class.name(), + 1, + Some(class), + context, + ) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + /// Validates one property declaration against inherited eval property metadata. fn validate_property_parent_redeclaration( class: &EvalClass, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index f6e127e8af..3f713bcd3d 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -1595,6 +1595,51 @@ return $label->text;"#, assert_eq!(values.get(result), FakeValue::String("HI".to_string())); } +/// Verifies explicit set-hook parameter types are contravariant with the property type. +#[test] +fn execute_program_validates_eval_property_set_hook_parameter_types() { + let valid_program = parse_fragment( + br#"class EvalWideSetHookParam { + public string $value { + get => $this->value; + set(mixed $raw) => $raw; + } +} +$box = new EvalWideSetHookParam(); +$box->value = "Ada"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&valid_program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); + + for source in [ + br#"class EvalNarrowSetHookParam { + public mixed $value { + set(string $raw) => $raw; + } +}"# + .as_slice(), + br#"class EvalNullableSetHookParam { + public ?string $value { + set(string $raw) => $raw; + } +}"# + .as_slice(), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("incompatible set-hook parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + /// Verifies nullsafe reads and mixed-case names still route through eval property hooks. #[test] fn execute_program_routes_eval_nullsafe_and_mixed_case_property_hooks() { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index ef1e5bd788..8763f02119 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -1356,7 +1356,11 @@ impl Parser { let source_start_line = self.current_line(); self.advance(); let (params, set_hook_type) = if is_set { - let (param, set_hook_type) = self.parse_property_set_hook_param()?; + let (param, set_hook_type, has_explicit_param) = + self.parse_property_set_hook_param()?; + if has_explicit_param && set_hook_type.is_none() && property_type.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } (vec![param], set_hook_type) } else { (Vec::new(), None) @@ -1405,12 +1409,12 @@ impl Parser { Ok((is_get, set_hook_type, method)) } - /// Parses an optional set-hook parameter list and returns the hook value variable. + /// Parses an optional set-hook parameter list and returns the value variable metadata. pub(super) fn parse_property_set_hook_param( &mut self, - ) -> Result<(String, Option), EvalParseError> { + ) -> Result<(String, Option, bool), EvalParseError> { if !self.consume(TokenKind::LParen) { - return Ok(("value".to_string(), None)); + return Ok(("value".to_string(), None, false)); } let set_hook_type = self.parse_optional_property_type()?; let TokenKind::DollarIdent(name) = self.current() else { @@ -1419,7 +1423,7 @@ impl Parser { let name = name.clone(); self.advance(); self.expect(TokenKind::RParen)?; - Ok((name, set_hook_type)) + Ok((name, set_hook_type, true)) } /// Parses `trait Name { ... }` declarations into dynamic trait metadata. diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index e9f1ae6c3f..8b079f350c 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -1128,6 +1128,30 @@ fn parse_fragment_retains_property_set_hook_parameter_type() { ); } +/// Verifies eval rejects explicit untyped set-hook parameters for typed properties. +#[test] +fn parse_fragment_rejects_untyped_explicit_property_set_hook_parameter_for_typed_property() { + assert_eq!( + parse_fragment( + br#"class DynEvalUntypedSetParamHooked { + public string $value { + set($raw) => $raw; + } +}"# + ), + Err(EvalParseError::UnsupportedConstruct) + ); + + parse_fragment( + br#"class DynEvalUntypedSetParamUntypedProperty { + public $value { + set($raw) => $raw; + } +}"#, + ) + .expect("untyped properties may use an untyped explicit set-hook parameter"); +} + /// Verifies abstract property hook contracts lower without concrete accessors. #[test] fn parse_fragment_accepts_abstract_class_property_hook_contracts() { diff --git a/docs/php/eval.md b/docs/php/eval.md index b00def35c6..f01e2f54d5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes and traits with properties including comma-separated simple property declarations, PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait comma-separated constants including `final` constants, and class-level attributes. Duplicate eval class-like names and PHP-reserved class-like declaration names are rejected. | +| Classes | Eval fragments can declare classes and traits with properties including comma-separated simple property declarations, PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax and PHP-compatible explicit set-hook parameter typing, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait comma-separated constants including `final` constants, and class-level attributes. Duplicate eval class-like names and PHP-reserved class-like declaration names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, comma-separated constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -174,6 +174,8 @@ constants, class-level attributes, `__callStatic()`, and magic property fallback through `__get()` and `__set()`. Reserved PHP class-like declaration names are rejected, while semi-reserved names that PHP allows, such as `enum`, `from`, and `resource`, remain valid. +Explicit concrete `set` hook parameter types are retained as settable-property +metadata and must be PHP-compatible supertypes of the property type. Eval traits also accept the legacy `var` marker for public properties. PHP's global `#[Override]` marker is validated on eval-declared methods against non-private parent methods and eval interface method contracts. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2dd1dc8f32..c5509bc30b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -10101,6 +10101,53 @@ echo $label->text;'); assert_eq!(out.stdout, "[Ada]:HI"); } +/// Verifies eval-declared set-hook parameter types stay compatible with property writes. +#[test] +fn test_eval_declared_property_set_hook_parameter_type_compatibility() { + let out = compile_and_run_capture( + r#" $this->value; + set(mixed $raw) => $raw; + } +} +$box = new EvalWideSetHookParam(); +$box->value = "Ada"; +echo $box->value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "Ada"); + + for source in [ + r#" $raw; + } +}'); +"#, + r#" $raw; + } +}'); +"#, + ] { + let err = compile_and_run_expect_failure(source); + assert!( + err.contains("Fatal error: eval()"), + "stderr did not contain eval fatal diagnostic: {err}" + ); + } +} + /// Verifies eval-declared nullsafe and mixed-case property hook reads stay routed. #[test] fn test_eval_declared_nullsafe_and_mixed_case_property_hooks() { From 15930d0787db1ac9670b37f1e57a89e7018db8e3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 14:03:10 +0200 Subject: [PATCH 0861/1208] Reject eval reserved class-like references --- .../elephc-magician/src/parser/expressions.rs | 37 +++++++++++++++++-- .../elephc-magician/src/parser/statements.rs | 17 +++++---- .../src/parser/tests/classes_errors.rs | 35 ++++++++++++++++++ docs/php/eval.md | 7 ++-- tests/codegen/eval.rs | 28 ++++++++++++++ 5 files changed, 111 insertions(+), 13 deletions(-) diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index d9b0d11a15..550f6d3be5 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -402,7 +402,7 @@ impl Parser { let target = self.parse_instanceof_variable_target()?; return Ok(EvalInstanceOfTarget::Expr(Box::new(target))); } - let name = self.parse_qualified_name()?; + let name = self.parse_class_reference_name(true)?; let class_name = self.resolve_static_class_name(name); if self.consume(TokenKind::DoubleColon) { let TokenKind::DollarIdent(property) = self.current() else { @@ -1050,8 +1050,8 @@ impl Parser { args, }); } - let class_name = self.parse_qualified_name()?; - let class_name = self.resolve_class_name(class_name); + let class_name = self.parse_class_reference_name(true)?; + let class_name = self.resolve_static_class_name(class_name); let args = self.parse_optional_constructor_args()?; Ok(EvalExpr::NewObject { class_name, args }) } @@ -1084,6 +1084,37 @@ impl Parser { Ok(ParsedQualifiedName { name, absolute }) } + /// Parses a class-like reference name while rejecting PHP-reserved unqualified names. + pub(super) fn parse_class_reference_name( + &mut self, + allow_relative_keywords: bool, + ) -> Result { + let name = self.parse_qualified_name()?; + if self.class_reference_name_is_reserved(&name, allow_relative_keywords) { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(name) + } + + /// Returns whether a parsed class-like reference uses a PHP-reserved bare name. + pub(super) fn class_reference_name_is_reserved( + &self, + name: &ParsedQualifiedName, + allow_relative_keywords: bool, + ) -> bool { + if name.absolute || name.name.contains('\\') { + return false; + } + if allow_relative_keywords + && ["self", "parent", "static"] + .iter() + .any(|keyword| ident_eq(&name.name, keyword)) + { + return false; + } + is_reserved_class_like_name(&name.name) + } + /// Resolves a class name used before `::`, preserving PHP relative class keywords. pub(super) fn resolve_static_class_name(&self, name: ParsedQualifiedName) -> String { if !name.absolute diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 8763f02119..215f267ce7 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -653,7 +653,7 @@ impl Parser { return Ok(None); } self.advance(); - let parent = self.parse_qualified_name()?; + let parent = self.parse_class_reference_name(false)?; Ok(Some(self.resolve_class_name(parent))) } @@ -665,7 +665,7 @@ impl Parser { self.advance(); let mut interfaces = Vec::new(); loop { - let interface = self.parse_qualified_name()?; + let interface = self.parse_class_reference_name(false)?; interfaces.push(self.resolve_class_name(interface)); if !self.consume(TokenKind::Comma) { break; @@ -853,7 +853,7 @@ impl Parser { ) -> Result<(), EvalParseError> { self.advance(); loop { - let trait_name = self.parse_qualified_name()?; + let trait_name = self.parse_class_reference_name(false)?; traits.push(self.resolve_class_name(trait_name)); if !self.consume(TokenKind::Comma) { break; @@ -902,7 +902,7 @@ impl Parser { self.advance(); let mut instead_of = Vec::new(); loop { - let trait_name = self.parse_qualified_name()?; + let trait_name = self.parse_class_reference_name(false)?; instead_of.push(self.resolve_class_name(trait_name)); if !self.consume(TokenKind::Comma) { break; @@ -927,6 +927,9 @@ impl Parser { ) -> Result<(Option, String), EvalParseError> { let first = self.parse_qualified_name()?; if self.consume(TokenKind::DoubleColon) { + if self.class_reference_name_is_reserved(&first, false) { + return Err(EvalParseError::UnsupportedConstruct); + } let TokenKind::Ident(method) = self.current() else { return Err(EvalParseError::UnexpectedToken); }; @@ -1766,7 +1769,7 @@ impl Parser { self.advance(); let mut parents = Vec::new(); loop { - let parent = self.parse_qualified_name()?; + let parent = self.parse_class_reference_name(false)?; parents.push(self.resolve_class_name(parent)); if !self.consume(TokenKind::Comma) { break; @@ -2322,10 +2325,10 @@ impl Parser { /// Parses one or more unioned catch types in source order. pub(super) fn parse_catch_types(&mut self) -> Result, EvalParseError> { - let class_name = self.parse_qualified_name()?; + let class_name = self.parse_class_reference_name(false)?; let mut class_names = vec![self.resolve_class_name(class_name)]; while self.consume(TokenKind::Pipe) { - let class_name = self.parse_qualified_name()?; + let class_name = self.parse_class_reference_name(false)?; class_names.push(self.resolve_class_name(class_name)); } Ok(class_names) diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs index 8b079f350c..43f3ddf43b 100644 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ b/crates/elephc-magician/src/parser/tests/classes_errors.rs @@ -194,6 +194,41 @@ enum integer { case Ready; }"#, assert_eq!(enum_decl.name(), "integer"); } +/// Verifies eval rejects PHP-reserved bare class-like reference names. +#[test] +fn parse_fragment_rejects_reserved_unqualified_class_like_reference_names() { + for source in [ + b"class DynEvalBadExtends extends match {}" as &[u8], + b"class DynEvalBadImplements implements match {}", + b"interface DynEvalBadIfaceExtends extends match {}", + b"class DynEvalBadTraitUse { use match; }", + b"class DynEvalBadTraitAdapt { use SomeTrait { match::run insteadof SomeTrait; } }", + b"$box = new match();", + b"$ok = $box instanceof match;", + b"try {} catch (match $e) {}", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval accepts semi-reserved or qualified class-like reference names PHP parses. +#[test] +fn parse_fragment_accepts_semi_reserved_and_qualified_class_like_reference_names() { + parse_fragment( + br#"class enum {} +class DynEvalExtendsSemiReserved extends enum {} +class DynEvalExtendsQualifiedReserved extends \match {} +class DynEvalNewSelf { + public function make() { return new self(); } +} +$ok = $box instanceof \match;"#, + ) + .expect("fragment should parse"); +} + /// Verifies comma-separated class-like constants lower to individual eval constants. #[test] fn parse_fragment_accepts_comma_separated_class_like_constants() { diff --git a/docs/php/eval.md b/docs/php/eval.md index f01e2f54d5..fd605a47c9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -49,7 +49,7 @@ such a local alias removes the alias without unsetting the global value. | Control flow | Braced and single-statement `if`/`elseif`/`else`, `else if`, `while`, `do/while`, `for`, `foreach`, `switch`, `break`, and `continue` are supported. | | Exceptions | `throw`, `try`, `catch`, union catches, class-specific catches, optional catch variables, and `finally` are supported. `finally` runs before a fragment returns or propagates a `Throwable`; a control action from `finally` replaces the pending action from the protected body or catch. | | Functions | Eval fragments can declare functions. Static locals inside eval-declared functions are initialized once per eval context and persist across later calls through that context. Top-level `static` declarations in separate eval fragments are initialized for each eval execution. | -| Classes | Eval fragments can declare classes and traits with properties including comma-separated simple property declarations, PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax and PHP-compatible explicit set-hook parameter typing, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait comma-separated constants including `final` constants, and class-level attributes. Duplicate eval class-like names and PHP-reserved class-like declaration names are rejected. | +| Classes | Eval fragments can declare classes and traits with properties including comma-separated simple property declarations, PHP's legacy `var` public-property marker, asymmetric property write visibility (`private(set)` / `protected(set)`), constructor property promotion including by-reference promotion for variable, array-element, object-property, static-property, property-array-element, static-property-array-element, and default-value targets, concrete property get/set hooks including by-reference get-hook syntax and PHP-compatible explicit set-hook parameter typing, methods, `__construct()`, inheritance, visibility, readonly properties/classes, abstract/final modifiers, trait uses with `insteadof` / `as` adaptations and PHP-compatible property/constant conflict checks, interface implementations, static members, class/interface/trait comma-separated constants including `final` constants, and class-level attributes. Duplicate eval class-like names and PHP-reserved bare class-like declaration/reference names are rejected. | | Enums | Eval fragments can declare pure and `int` / `string` backed enums with cases, comma-separated constants including `final` constants, methods, interface implementations, `::cases()`, `::from()`, `::tryFrom()`, `->name`, and backed `->value`. | | Includes | `include`, `include_once`, `require`, and `require_once` execute local filesystem paths from inside fragments. | | Namespaces | Both `namespace Name;` and `namespace Name { ... }` forms are supported, including simple and grouped `use`, `use function`, and `use const` declarations. | @@ -172,8 +172,9 @@ comma-separated class, interface, trait, and enum constants including `final` constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and `__callStatic()`, and magic property fallback through `__get()` and `__set()`. -Reserved PHP class-like declaration names are rejected, while semi-reserved -names that PHP allows, such as `enum`, `from`, and `resource`, remain valid. +Reserved PHP bare class-like declaration and reference names are rejected, while +semi-reserved names that PHP allows, such as `enum`, `from`, and `resource`, +remain valid. Explicit concrete `set` hook parameter types are retained as settable-property metadata and must be PHP-compatible supertypes of the property type. Eval traits also accept the legacy `var` marker for public properties. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c5509bc30b..95c844917c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12508,6 +12508,34 @@ eval('enum bool { case Ready; }'); } } +/// Verifies eval rejects PHP-reserved bare class-like reference names. +#[test] +fn test_eval_reserved_class_like_reference_name_fails() { + for source in [ + r#" Date: Sat, 27 Jun 2026 14:08:31 +0200 Subject: [PATCH 0862/1208] Cover namespaced eval relative construction --- .../src/interpreter/tests/classes.rs | 40 +++++++++++++++++++ tests/codegen/eval.rs | 30 ++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index 3f713bcd3d..ff48cf0b6d 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -73,6 +73,46 @@ return $self instanceof EvalRelativeFactoryBase assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies `new self/static/parent` stay relative inside eval namespaces. +#[test] +fn execute_program_constructs_namespaced_relative_class_names_from_eval_methods() { + let program = parse_fragment( + br#"namespace EvalRelativeNs; +class Base { + public string $label; + public function __construct($label = "base") { $this->label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class Child extends Base { + public function parentFactory() { return new parent("parent"); } +} +$child = new Child("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label; +return $self instanceof Base + && !($self instanceof Child) + && $static instanceof Child + && $parent instanceof Base + && !($parent instanceof Child);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRelativeNs\\Base:self:EvalRelativeNs\\Child:static:EvalRelativeNs\\Base:parent" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies PHP legacy `var` properties behave as public eval properties. #[test] fn execute_program_supports_legacy_var_properties() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 95c844917c..e5bf23d20b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7450,6 +7450,36 @@ echo get_class($parent); echo ":"; echo $parent->label;'); ); } +/// Verifies eval-declared methods resolve `new self/static/parent` inside namespaces. +#[test] +fn test_eval_declared_methods_construct_namespaced_relative_class_names() { + let out = compile_and_run( + r#"label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class Child extends Base { + public function parentFactory() { return new parent("parent"); } +} +$child = new Child("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label;'); +"#, + ); + assert_eq!( + out, + "EvalRelativeNs\\Base:self:EvalRelativeNs\\Child:static:EvalRelativeNs\\Base:parent" + ); +} + /// Verifies eval supports PHP's legacy `var` public property marker through the bridge. #[test] fn test_eval_declared_legacy_var_properties() { From ccfda9d8a47bc16a95584fbadac80e6400f43364 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 14:30:16 +0200 Subject: [PATCH 0863/1208] Validate eval builtin attribute targets --- .../src/interpreter/statements.rs | 115 ++++++++++++++++-- .../src/interpreter/tests/classes.rs | 48 ++++++++ docs/php/eval.md | 8 +- tests/codegen/eval.rs | 19 +++ 4 files changed, 179 insertions(+), 11 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 9bcb291f5f..b07c48e439 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1564,6 +1564,7 @@ fn validate_eval_enum_decl( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { + validate_eval_enum_attribute_targets(enum_decl)?; validate_eval_declared_constants(enum_decl.constants())?; validate_eval_enum_case_declarations(enum_decl)?; validate_eval_enum_forbidden_magic_methods(enum_decl)?; @@ -1610,6 +1611,7 @@ fn validate_eval_enum_case_declarations(enum_decl: &EvalEnum) -> Result<(), Eval .map(|constant| constant.name().to_string()) .collect::>(); for case in enum_decl.cases() { + validate_eval_non_method_attribute_targets(case.attributes())?; if !case_names.insert(case.name().to_string()) { return Err(EvalStatus::RuntimeFatal); } @@ -1852,6 +1854,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( return Err(EvalStatus::RuntimeFatal); } } + validate_eval_interface_attribute_targets(interface)?; validate_eval_declared_constants(interface.constants())?; validate_eval_interface_constants(interface.constants())?; validate_interface_constant_parent_redeclarations(interface, context)?; @@ -1888,6 +1891,7 @@ pub(in crate::interpreter) fn execute_trait_decl_stmt( return Err(EvalStatus::RuntimeFatal); } let trait_decl = expand_eval_trait_traits(trait_decl, context)?; + validate_eval_trait_attribute_targets(&trait_decl)?; validate_eval_declared_constants(trait_decl.constants())?; validate_eval_magic_methods(trait_decl.methods())?; if context.define_trait(trait_decl.clone()) { @@ -2561,6 +2565,7 @@ fn validate_eval_class_modifiers( if class.is_abstract() && class.is_final() { return Err(EvalStatus::RuntimeFatal); } + validate_eval_class_attribute_targets(class.attributes())?; if class.is_readonly_class() && eval_class_has_allow_dynamic_properties_attribute(class) { return Err(EvalStatus::RuntimeFatal); } @@ -2573,6 +2578,7 @@ fn validate_eval_class_modifiers( validate_property_parent_redeclaration(class, property, context)?; } for method in class.methods() { + validate_eval_method_attribute_targets(method.attributes())?; validate_eval_magic_method(method)?; if method.is_abstract() && method.is_final() { return Err(EvalStatus::RuntimeFatal); @@ -2594,10 +2600,104 @@ fn validate_eval_class_modifiers( /// Returns whether a class carries PHP's global `#[AllowDynamicProperties]` attribute. fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool { - class - .attributes() + eval_attributes_have_global_builtin_attribute(class.attributes(), "AllowDynamicProperties") +} + +/// Rejects builtin attributes that cannot target an eval-declared class. +fn validate_eval_class_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "Override") { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects builtin attributes that cannot target eval-declared interfaces. +fn validate_eval_interface_attribute_targets( + interface: &EvalInterface, +) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(interface.attributes())?; + for property in interface.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; + } + for method in interface.methods() { + validate_eval_method_attribute_targets(method.attributes())?; + } + Ok(()) +} + +/// Rejects builtin attributes that cannot target eval-declared traits. +fn validate_eval_trait_attribute_targets(trait_decl: &EvalTrait) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(trait_decl.attributes())?; + for property in trait_decl.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; + } + for method in trait_decl.methods() { + validate_eval_method_attribute_targets(method.attributes())?; + } + Ok(()) +} + +/// Rejects builtin attributes that cannot target eval-declared enums. +fn validate_eval_enum_attribute_targets(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(enum_decl.attributes()) +} + +/// Rejects class-only or method-only builtin attributes on non-class declarations. +fn validate_eval_non_class_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") + || eval_attributes_have_global_builtin_attribute(attributes, "Override") + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects class-only or method-only builtin attributes on non-method members. +fn validate_eval_non_method_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") + || eval_attributes_have_global_builtin_attribute(attributes, "Override") + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects class-only builtin attributes on method declarations. +fn validate_eval_method_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Returns whether the attribute list contains one global builtin attribute. +fn eval_attributes_have_global_builtin_attribute( + attributes: &[EvalAttribute], + builtin: &str, +) -> bool { + attributes .iter() - .any(|attribute| attribute.name().eq_ignore_ascii_case("AllowDynamicProperties")) + .any(|attribute| eval_attribute_is_global_builtin(attribute, builtin)) +} + +/// Returns whether one attribute names a global builtin attribute class. +fn eval_attribute_is_global_builtin(attribute: &EvalAttribute, builtin: &str) -> bool { + attribute + .name() + .trim_start_matches('\\') + .eq_ignore_ascii_case(builtin) } /// Validates PHP's global `#[Override]` marker on one eval-declared method. @@ -2618,12 +2718,9 @@ fn validate_eval_override_attribute( } } -/// Returns whether a method has an unqualified global builtin marker attribute. +/// Returns whether a method has a global builtin marker attribute. fn eval_method_has_global_builtin_attribute(method: &EvalClassMethod, builtin: &str) -> bool { - method - .attributes() - .iter() - .any(|attribute| attribute.name().eq_ignore_ascii_case(builtin)) + eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) } /// Returns whether one method overrides a non-private parent method. @@ -2944,6 +3041,7 @@ fn validate_eval_declared_properties( ) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); for property in class.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; if !names.insert(property.name().to_string()) { return Err(EvalStatus::RuntimeFatal); } @@ -3064,6 +3162,7 @@ fn validate_property_parent_redeclaration( fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); for constant in constants { + validate_eval_non_method_attribute_targets(constant.attributes())?; if !names.insert(constant.name().to_string()) { return Err(EvalStatus::RuntimeFatal); } diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index ff48cf0b6d..b2c8f2b870 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -941,6 +941,54 @@ echo $box->name() . ":" . $box->label();"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies eval rejects global builtin attributes on unsupported OOP targets. +#[test] +fn execute_program_rejects_invalid_builtin_attribute_targets() { + let cases: &[(&[u8], &str)] = &[ + ( + br#"#[\AllowDynamicProperties] interface EvalInvalidAttrInterface {}"#, + "AllowDynamicProperties interface", + ), + ( + br#"#[\AllowDynamicProperties] trait EvalInvalidAttrTrait {}"#, + "AllowDynamicProperties trait", + ), + ( + br#"#[\AllowDynamicProperties] enum EvalInvalidAttrEnum { case Ready; }"#, + "AllowDynamicProperties enum", + ), + ( + br#"#[\Override] class EvalInvalidAttrClass {}"#, + "Override class", + ), + ( + br#"class EvalInvalidAttrProperty { #[\Override] public int $value; }"#, + "Override property", + ), + ( + br#"class EvalInvalidAttrConstant { #[\AllowDynamicProperties] public const VALUE = 1; }"#, + "AllowDynamicProperties constant", + ), + ( + br#"class EvalInvalidAttrMethod { #[\AllowDynamicProperties] public function run() {} }"#, + "AllowDynamicProperties method", + ), + ( + br#"enum EvalInvalidAttrCase { #[\AllowDynamicProperties] case Ready; }"#, + "AllowDynamicProperties enum case", + ), + ]; + + for &(source, label) in cases { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + /// Verifies readonly classes leave static properties mutable like ordinary classes. #[test] fn execute_program_allows_readonly_class_static_property() { diff --git a/docs/php/eval.md b/docs/php/eval.md index fd605a47c9..54a6022a75 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -179,7 +179,8 @@ Explicit concrete `set` hook parameter types are retained as settable-property metadata and must be PHP-compatible supertypes of the property type. Eval traits also accept the legacy `var` marker for public properties. PHP's global `#[Override]` marker is validated on eval-declared methods against -non-private parent methods and eval interface method contracts. +non-private parent methods and eval interface method contracts, and rejected on +non-method OOP declaration targets. Eval validates method override and interface method parameter and return types with PHP-style parameter contravariance and return covariance for supported declared type metadata, including nullable, union, `mixed`, `self`, `parent`, @@ -630,8 +631,9 @@ converted to readonly slots. Untyped instance properties in readonly classes are still rejected. Missing-property writes can still dispatch through `__set()`, but readonly classes reject actual dynamic property creation. -PHP's global `#[AllowDynamicProperties]` marker is rejected on eval-declared -readonly classes. +PHP's global `#[AllowDynamicProperties]` marker is accepted only on +non-readonly eval-declared classes; eval rejects it on readonly classes, +interfaces, traits, enums, members, and enum cases. `self::`, `parent::`, and late-bound `static::` work for supported static members, class constants, and class-name literals. Attempts to `unset()` static properties parse normally and throw PHP's diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e5bf23d20b..fba4b4d654 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12382,6 +12382,25 @@ eval('class EvalOverrideMissing { ); } +/// Verifies eval rejects global builtin attributes on unsupported OOP targets. +#[test] +fn test_eval_declared_builtin_attribute_target_validation() { + let cases = [ + r#"eval('#[\AllowDynamicProperties] interface EvalInvalidAttrInterface {}');"#, + r#"eval('class EvalInvalidAttrProperty { #[\Override] public int $value; }');"#, + r#"eval('class EvalInvalidAttrMethod { #[\AllowDynamicProperties] public function run() {} }');"#, + ]; + + for source in cases { + let err = compile_and_run_expect_failure(&format!(" Date: Sat, 27 Jun 2026 14:38:33 +0200 Subject: [PATCH 0864/1208] Validate eval interface override attributes --- .../src/interpreter/statements.rs | 44 +++++++++++++++++++ .../src/interpreter/tests/classes.rs | 40 +++++++++++++++++ docs/php/eval.md | 4 +- tests/codegen/eval.rs | 35 +++++++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b07c48e439..e4efe12c89 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1855,6 +1855,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( } } validate_eval_interface_attribute_targets(interface)?; + validate_eval_interface_override_attributes(interface, context)?; validate_eval_declared_constants(interface.constants())?; validate_eval_interface_constants(interface.constants())?; validate_interface_constant_parent_redeclarations(interface, context)?; @@ -2628,6 +2629,41 @@ fn validate_eval_interface_attribute_targets( Ok(()) } +/// Validates PHP's global `#[Override]` marker on eval-declared interface methods. +fn validate_eval_interface_override_attributes( + interface: &EvalInterface, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let parent_requirements = eval_interface_parent_method_requirements(interface, context); + for method in interface.methods() { + if !eval_interface_method_has_global_builtin_attribute(method, "Override") { + continue; + } + if !parent_requirements.iter().any(|(_, requirement)| { + requirement.name().eq_ignore_ascii_case(method.name()) + && requirement.is_static() == method.is_static() + }) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns method requirements inherited by one eval interface declaration. +fn eval_interface_parent_method_requirements( + interface: &EvalInterface, + context: &ElephcEvalContext, +) -> Vec<(String, EvalInterfaceMethod)> { + let mut requirements = Vec::new(); + for parent in interface.parents() { + if context.has_interface(parent) { + requirements.extend(context.interface_method_requirements_with_owners(parent)); + } + requirements.extend(builtin_interface_method_requirements(parent)); + } + requirements +} + /// Rejects builtin attributes that cannot target eval-declared traits. fn validate_eval_trait_attribute_targets(trait_decl: &EvalTrait) -> Result<(), EvalStatus> { validate_eval_non_class_attribute_targets(trait_decl.attributes())?; @@ -2723,6 +2759,14 @@ fn eval_method_has_global_builtin_attribute(method: &EvalClassMethod, builtin: & eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) } +/// Returns whether an interface method has a global builtin marker attribute. +fn eval_interface_method_has_global_builtin_attribute( + method: &EvalInterfaceMethod, + builtin: &str, +) -> bool { + eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) +} + /// Returns whether one method overrides a non-private parent method. fn eval_method_overrides_parent( class: &EvalClass, diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index b2c8f2b870..b2bfdc1bf8 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -941,6 +941,46 @@ echo $box->name() . ":" . $box->label();"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies interface `#[Override]` methods require an inherited interface method. +#[test] +fn execute_program_validates_interface_override_attribute_targets() { + let valid = parse_fragment( + br#"interface EvalIfaceOverrideParent { + public function label(): string; +} +interface EvalIfaceOverrideChild extends EvalIfaceOverrideParent { + #[\Override] + public function label(): string; +} +class EvalIfaceOverrideImpl implements EvalIfaceOverrideChild { + public function label(): string { return "child"; } +} +$box = new EvalIfaceOverrideImpl(); +echo $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&valid, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child"); + + let invalid = parse_fragment( + br#"interface EvalIfaceOverrideMissing { + #[\Override] + public function missing(): string; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&invalid, &mut scope, &mut values) + .expect_err("interface override marker without parent method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies eval rejects global builtin attributes on unsupported OOP targets. #[test] fn execute_program_rejects_invalid_builtin_attribute_targets() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 54a6022a75..327540a5f0 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -180,7 +180,9 @@ metadata and must be PHP-compatible supertypes of the property type. Eval traits also accept the legacy `var` marker for public properties. PHP's global `#[Override]` marker is validated on eval-declared methods against non-private parent methods and eval interface method contracts, and rejected on -non-method OOP declaration targets. +non-method OOP declaration targets. On eval-declared interface methods, the +marker requires a matching method inherited from an eval or builtin parent +interface. Eval validates method override and interface method parameter and return types with PHP-style parameter contravariance and return covariance for supported declared type metadata, including nullable, union, `mixed`, `self`, `parent`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fba4b4d654..466b9b5431 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12382,6 +12382,41 @@ eval('class EvalOverrideMissing { ); } +/// Verifies eval-declared interface `#[Override]` methods require parent methods. +#[test] +fn test_eval_declared_interface_override_attribute_validation() { + let out = compile_and_run( + r#"label();'); +"#, + ); + assert_eq!(out, "child"); + + let err = compile_and_run_expect_failure( + r#" Date: Sat, 27 Jun 2026 14:56:29 +0200 Subject: [PATCH 0865/1208] Cover builtin interface override attributes --- .../src/interpreter/statements.rs | 17 +++++++++++++---- .../src/interpreter/tests/classes.rs | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e4efe12c89..e7125d625d 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2639,10 +2639,10 @@ fn validate_eval_interface_override_attributes( if !eval_interface_method_has_global_builtin_attribute(method, "Override") { continue; } - if !parent_requirements.iter().any(|(_, requirement)| { - requirement.name().eq_ignore_ascii_case(method.name()) - && requirement.is_static() == method.is_static() - }) { + if !parent_requirements + .iter() + .any(|(_, requirement)| eval_interface_method_matches_requirement(method, requirement)) + { return Err(EvalStatus::RuntimeFatal); } } @@ -2664,6 +2664,15 @@ fn eval_interface_parent_method_requirements( requirements } +/// Returns whether an interface method matches one inherited requirement signature kind. +fn eval_interface_method_matches_requirement( + method: &EvalInterfaceMethod, + requirement: &EvalInterfaceMethod, +) -> bool { + requirement.name().eq_ignore_ascii_case(method.name()) + && requirement.is_static() == method.is_static() +} + /// Rejects builtin attributes that cannot target eval-declared traits. fn validate_eval_trait_attribute_targets(trait_decl: &EvalTrait) -> Result<(), EvalStatus> { validate_eval_non_class_attribute_targets(trait_decl.attributes())?; diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs index b2bfdc1bf8..3d39977960 100644 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ b/crates/elephc-magician/src/interpreter/tests/classes.rs @@ -966,6 +966,25 @@ echo $box->label();"#, assert_eq!(values.output, "child"); + let builtin_parent = parse_fragment( + br#"interface EvalIfaceOverrideStringable extends Stringable { + #[\Override] + public function __toString(): string; +} +class EvalIfaceOverrideStringableImpl implements EvalIfaceOverrideStringable { + public function __toString(): string { return "stringable"; } +} +$box = new EvalIfaceOverrideStringableImpl(); +echo $box;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&builtin_parent, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stringable"); + let invalid = parse_fragment( br#"interface EvalIfaceOverrideMissing { #[\Override] From 071ddb592d5b93850dedc4952a52f2417f33d6f3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 15:08:46 +0200 Subject: [PATCH 0866/1208] Expose AOT interface methods to eval --- .../src/interpreter/statements.rs | 33 ++++++++++++++-- docs/php/eval.md | 4 +- src/codegen/mod.rs | 3 ++ tests/codegen/eval.rs | 39 +++++++++++++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e7125d625d..eec93cebf1 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1855,7 +1855,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( } } validate_eval_interface_attribute_targets(interface)?; - validate_eval_interface_override_attributes(interface, context)?; + validate_eval_interface_override_attributes(interface, context, values)?; validate_eval_declared_constants(interface.constants())?; validate_eval_interface_constants(interface.constants())?; validate_interface_constant_parent_redeclarations(interface, context)?; @@ -2604,6 +2604,9 @@ fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool eval_attributes_have_global_builtin_attribute(class.attributes(), "AllowDynamicProperties") } +/// Bridge ReflectionMethod flag for static generated/AOT methods. +const EVAL_REFLECTION_METHOD_FLAG_STATIC: u64 = 1; + /// Rejects builtin attributes that cannot target an eval-declared class. fn validate_eval_class_attribute_targets( attributes: &[EvalAttribute], @@ -2633,18 +2636,23 @@ fn validate_eval_interface_attribute_targets( fn validate_eval_interface_override_attributes( interface: &EvalInterface, context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let parent_requirements = eval_interface_parent_method_requirements(interface, context); for method in interface.methods() { if !eval_interface_method_has_global_builtin_attribute(method, "Override") { continue; } - if !parent_requirements + if parent_requirements .iter() .any(|(_, requirement)| eval_interface_method_matches_requirement(method, requirement)) { - return Err(EvalStatus::RuntimeFatal); + continue; + } + if eval_aot_interface_parent_method_matches(interface, method, values)? { + continue; } + return Err(EvalStatus::RuntimeFatal); } Ok(()) } @@ -2664,6 +2672,25 @@ fn eval_interface_parent_method_requirements( requirements } +/// Returns whether a generated/AOT parent interface exposes a matching method. +fn eval_aot_interface_parent_method_matches( + interface: &EvalInterface, + method: &EvalInterfaceMethod, + values: &mut impl RuntimeValueOps, +) -> Result { + for parent in interface.parents() { + if !values.interface_exists(parent)? { + continue; + } + let parent = parent.trim_start_matches('\\'); + if let Some(flags) = values.reflection_method_flags(parent, method.name())? { + let parent_method_is_static = flags & EVAL_REFLECTION_METHOD_FLAG_STATIC != 0; + return Ok(parent_method_is_static == method.is_static()); + } + } + Ok(false) +} + /// Returns whether an interface method matches one inherited requirement signature kind. fn eval_interface_method_matches_requirement( method: &EvalInterfaceMethod, diff --git a/docs/php/eval.md b/docs/php/eval.md index 327540a5f0..5f0df237a9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -181,8 +181,8 @@ Eval traits also accept the legacy `var` marker for public properties. PHP's global `#[Override]` marker is validated on eval-declared methods against non-private parent methods and eval interface method contracts, and rejected on non-method OOP declaration targets. On eval-declared interface methods, the -marker requires a matching method inherited from an eval or builtin parent -interface. +marker requires a matching method inherited from an eval, builtin, or +generated/AOT parent interface. Eval validates method override and interface method parameter and return types with PHP-style parameter contravariance and return covariance for supported declared type metadata, including nullable, union, `mixed`, `self`, `parent`, diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 8c21f1fd73..e6dee45844 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -843,6 +843,9 @@ fn runtime_referenced_interfaces( class_names: &HashSet, ) -> HashMap { let mut names = HashSet::new(); + if module.required_runtime_features.eval { + names.extend(module.interface_infos.keys().cloned()); + } if module_uses_dynamic_instanceof(module) { names.extend(dynamic_instanceof_interface_names(module)); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 466b9b5431..f628adbe3d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12417,6 +12417,45 @@ eval('interface EvalIfaceOverrideMissing { ); } +/// Verifies eval reflection exposes methods for AOT interfaces that only eval uses. +#[test] +fn test_eval_reflects_unimplemented_aot_interface_methods() { + let out = compile_and_run( + r#"hasMethod("aotLabel") ? "H" : "h"; echo ":"; +echo count($r->getMethods()); echo ":"; +echo get_class_methods("EvalAotOnlyReflectableContract")[0] ?? "none";'); +"#, + ); + assert_eq!(out, "H:1:aotlabel"); +} + +/// Verifies eval interface `#[Override]` can target a generated/AOT parent interface. +#[test] +fn test_eval_declared_interface_override_attribute_accepts_aot_parent() { + let out = compile_and_run( + r#"aotLabel();'); +"#, + ); + assert_eq!(out, "aot"); +} + /// Verifies eval rejects global builtin attributes on unsupported OOP targets. #[test] fn test_eval_declared_builtin_attribute_target_validation() { From e78da079978340862c3bb4e47e816b1852163f8f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 15:24:32 +0200 Subject: [PATCH 0867/1208] Validate eval classes against AOT interfaces --- .../src/interpreter/statements.rs | 299 +++++++++++++++++- docs/php/eval.md | 3 +- tests/codegen/eval.rs | 76 +++++ 3 files changed, 370 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index eec93cebf1..617650e9db 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -11,7 +11,7 @@ use super::*; use crate::context::{ NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, - NativeCallableObjectDefaultArg, + NativeCallableObjectDefaultArg, NativeCallableSignature, }; /// Executes statements in source order and propagates the first eval `return`. @@ -1356,7 +1356,7 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( } let class = expand_eval_class_traits(class, context)?.with_readonly_properties(); let class = &class; - validate_eval_class_modifiers(class, context)?; + validate_eval_class_modifiers(class, context, values)?; let native_parent = validate_eval_class_parent(class, context, values)?; for interface in class.interfaces() { if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { @@ -1367,9 +1367,11 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( validate_eval_class_does_not_implement_enum_interfaces(class, context)?; validate_declared_class_interface_members(class, context)?; validate_declared_class_builtin_interface_members(class, context)?; + validate_declared_class_aot_interface_members(class, context, values)?; if !class.is_abstract() { validate_concrete_class_requirements(class, context)?; validate_concrete_class_builtin_interface_requirements(class, context)?; + validate_concrete_class_aot_interface_requirements(class, context, values)?; } if context.define_class(class.clone()) { if let Some(parent) = native_parent.as_deref() { @@ -1569,10 +1571,12 @@ fn validate_eval_enum_decl( validate_eval_enum_case_declarations(enum_decl)?; validate_eval_enum_forbidden_magic_methods(enum_decl)?; let enum_class = enum_decl.as_class_metadata(); - validate_eval_class_modifiers(&enum_class, context)?; + validate_eval_class_modifiers(&enum_class, context, values)?; validate_eval_enum_interfaces(enum_decl, &enum_class, context, values)?; validate_declared_class_builtin_interface_members(&enum_class, context)?; + validate_declared_class_aot_interface_members(&enum_class, context, values)?; validate_concrete_class_builtin_interface_requirements(&enum_class, context)?; + validate_concrete_class_aot_interface_requirements(&enum_class, context, values)?; validate_concrete_class_requirements(&enum_class, context) } @@ -2562,6 +2566,7 @@ fn validate_eval_class_does_not_implement_throwable_interfaces( fn validate_eval_class_modifiers( class: &EvalClass, context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if class.is_abstract() && class.is_final() { return Err(EvalStatus::RuntimeFatal); @@ -2594,7 +2599,7 @@ fn validate_eval_class_modifiers( return Err(EvalStatus::RuntimeFatal); } validate_method_parent_override(class, method, context)?; - validate_eval_override_attribute(class, method, context)?; + validate_eval_override_attribute(class, method, context, values)?; } Ok(()) } @@ -2607,6 +2612,17 @@ fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool /// Bridge ReflectionMethod flag for static generated/AOT methods. const EVAL_REFLECTION_METHOD_FLAG_STATIC: u64 = 1; +/// Bridge ReflectionMethod flag for private generated/AOT methods. +const EVAL_REFLECTION_METHOD_FLAG_PRIVATE: u64 = 8; + +/// Method requirement discovered from generated/AOT interface metadata. +struct EvalAotInterfaceMethodRequirement { + owner: String, + name: String, + is_static: bool, + signature: Option, +} + /// Rejects builtin attributes that cannot target an eval-declared class. fn validate_eval_class_attribute_targets( attributes: &[EvalAttribute], @@ -2777,12 +2793,14 @@ fn validate_eval_override_attribute( class: &EvalClass, method: &EvalClassMethod, context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if !eval_method_has_global_builtin_attribute(method, "Override") { return Ok(()); } if eval_method_overrides_parent(class, method, context) - || eval_method_implements_interface(class, method, context) + || eval_method_overrides_aot_parent(class, method, context, values)? + || eval_method_implements_interface(class, method, context, values)? { Ok(()) } else { @@ -2818,13 +2836,36 @@ fn eval_method_overrides_parent( }) } +/// Returns whether one method overrides a visible generated/AOT parent method. +fn eval_method_overrides_aot_parent( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = class.parent() else { + return Ok(false); + }; + if context.has_class(parent) || !values.class_exists(parent)? { + return Ok(false); + } + let parent = parent.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(parent, method.name())? else { + return Ok(false); + }; + let parent_method_is_static = flags & EVAL_REFLECTION_METHOD_FLAG_STATIC != 0; + let parent_method_is_private = flags & EVAL_REFLECTION_METHOD_FLAG_PRIVATE != 0; + Ok(!parent_method_is_private && parent_method_is_static == method.is_static()) +} + /// Returns whether one method implements a direct or inherited interface method. fn eval_method_implements_interface( class: &EvalClass, method: &EvalClassMethod, context: &ElephcEvalContext, -) -> bool { - pending_class_interface_names(class, context) + values: &mut impl RuntimeValueOps, +) -> Result { + if pending_class_interface_names(class, context) .iter() .filter(|interface| context.has_interface(interface)) .any(|interface| { @@ -2836,6 +2877,15 @@ fn eval_method_implements_interface( && requirement.is_static() == method.is_static() }) }) + { + return Ok(true); + } + Ok(pending_class_aot_interface_method_requirements(class, context, values)? + .iter() + .any(|requirement| { + requirement.name.eq_ignore_ascii_case(method.name()) + && requirement.is_static == method.is_static() + })) } /// Validates PHP magic-method contracts for one eval class-like method list. @@ -3367,6 +3417,31 @@ fn validate_declared_class_builtin_interface_members( Ok(()) } +/// Validates declared class methods against generated/AOT interface contracts. +fn validate_declared_class_aot_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for requirement in pending_class_aot_interface_method_requirements(class, context, values)? { + let Some((declaring_class, method)) = pending_class_method(class, &requirement.name, context) + else { + continue; + }; + if !class_method_satisfies_aot_interface_requirement( + &method, + &declaring_class, + &requirement, + Some(class), + context, + false, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + /// Validates class methods present for an eval interface, even on abstract classes. fn validate_declared_class_interface_methods( class: &EvalClass, @@ -3564,6 +3639,20 @@ fn validate_concrete_class_builtin_interface_requirements( Ok(()) } +/// Validates concrete class methods required by generated/AOT runtime interfaces. +fn validate_concrete_class_aot_interface_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for requirement in pending_class_aot_interface_method_requirements(class, context, values)? { + if !class_has_aot_interface_method(class, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + /// Returns inherited abstract methods that the pending class has not concretized. fn pending_class_abstract_method_requirements( class: &EvalClass, @@ -3773,6 +3862,155 @@ fn pending_class_builtin_interface_method_requirements( requirements } +/// Returns generated/AOT interface method requirements inherited by a pending class. +fn pending_class_aot_interface_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) || !values.interface_exists(&interface)? { + continue; + } + requirements.extend(eval_aot_interface_method_requirements( + &interface, context, values, + )?); + } + Ok(requirements) +} + +/// Returns generated/AOT method requirements for one runtime interface. +fn eval_aot_interface_method_requirements( + interface: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let interface = interface.trim_start_matches('\\'); + let method_names = eval_aot_interface_method_names(interface, values)?; + let mut requirements = Vec::new(); + for method_name in method_names { + if let Some(requirement) = + eval_aot_interface_method_requirement(interface, &method_name, context, values)? + { + requirements.push(requirement); + } + } + Ok(requirements) +} + +/// Returns generated/AOT method names for one runtime interface. +fn eval_aot_interface_method_names( + interface: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method_names = values.reflection_method_names(interface)?; + let names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + Ok(names) +} + +/// Builds one generated/AOT interface method requirement from reflection and signature metadata. +fn eval_aot_interface_method_requirement( + interface: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(interface, method_name)? else { + return Ok(None); + }; + let is_static = flags & EVAL_REFLECTION_METHOD_FLAG_STATIC != 0; + let owner = values + .reflection_method_declaring_class(interface, method_name)? + .unwrap_or_else(|| interface.to_string()); + let signature = if is_static { + context.native_static_method_signature(&owner, method_name) + } else { + context.native_method_signature(&owner, method_name) + }; + Ok(Some(EvalAotInterfaceMethodRequirement { + owner: owner.clone(), + name: method_name.to_string(), + is_static, + signature: signature.map(|signature| { + eval_native_signature_interface_method(method_name, is_static, &signature) + }), + })) +} + +/// Converts generated/AOT callable metadata into an eval interface method requirement. +fn eval_native_signature_interface_method( + method_name: &str, + is_static: bool, + signature: &NativeCallableSignature, +) -> EvalInterfaceMethod { + let param_count = signature.param_count(); + EvalInterfaceMethod::new( + method_name, + (0..param_count) + .map(|index| { + signature + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{index}")) + }) + .collect(), + ) + .with_static(is_static) + .with_parameter_types( + (0..param_count) + .map(|index| signature.param_type(index).cloned()) + .collect(), + ) + .with_parameter_defaults( + (0..param_count) + .map(|index| { + signature + .param_default(index) + .map(|_| EvalExpr::Const(EvalConst::Null)) + }) + .collect(), + ) + .with_parameter_by_ref_flags( + (0..param_count) + .map(|index| signature.param_by_ref(index)) + .collect(), + ) + .with_parameter_variadic_flags( + (0..param_count) + .map(|index| signature.param_variadic(index)) + .collect(), + ) + .with_return_type(signature.return_type().cloned()) +} + +/// Copies a runtime string array into Rust-owned strings for declaration validation. +fn eval_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_runtime_string_value(value, values)?); + } + Ok(result) +} + +/// Reads one runtime string cell as UTF-8 metadata. +fn eval_runtime_string_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + /// Validates that one eval class provides methods required by one eval interface. fn validate_class_implements_eval_interface( class: &EvalClass, @@ -3834,6 +4072,26 @@ fn class_has_builtin_interface_method( }) } +/// Returns whether a class or its eval parents satisfy one generated/AOT interface method. +fn class_has_aot_interface_method( + class: &EvalClass, + requirement: &EvalAotInterfaceMethodRequirement, + context: &ElephcEvalContext, +) -> bool { + if let Some((declaring_class, method)) = pending_class_method(class, &requirement.name, context) + { + return class_method_satisfies_aot_interface_requirement( + &method, + &declaring_class, + requirement, + Some(class), + context, + true, + ); + } + false +} + /// Returns whether a class or its eval parents satisfy one interface method signature. fn class_has_interface_method( class: &EvalClass, @@ -3872,6 +4130,33 @@ fn class_has_interface_method( }) } +/// Returns whether one method satisfies a generated/AOT interface requirement. +fn class_method_satisfies_aot_interface_requirement( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalAotInterfaceMethodRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + require_concrete: bool, +) -> bool { + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static + || (require_concrete && method.is_abstract()) + { + return false; + } + requirement.signature.as_ref().is_none_or(|signature| { + class_method_satisfies_interface_signature( + method, + method_owner, + signature, + &requirement.owner, + pending_class, + context, + ) + }) +} + /// Returns whether one class method can accept every call required by an interface method. fn class_method_satisfies_interface_signature( method: &EvalClassMethod, diff --git a/docs/php/eval.md b/docs/php/eval.md index 5f0df237a9..cd751734a6 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -167,7 +167,8 @@ including asymmetric write visibility, property-level `readonly`, `readonly clas `__construct()`, abstract classes and methods, final classes, methods, and properties, trait composition with `insteadof` conflict resolution and `as` aliases/visibility adaptations, interface implementation checks, static -properties, static methods, static interface method contracts, single and +properties, static methods, static interface method contracts, generated/AOT +interface method contract checks, single and comma-separated class, interface, trait, and enum constants including `final` constants, class-level attributes, `ClassName::class` literals, magic method fallback through `__call()` and diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f628adbe3d..c035283898 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12456,6 +12456,82 @@ echo $box->aotLabel();'); assert_eq!(out, "aot"); } +/// Verifies eval classes can implement generated/AOT interface method contracts. +#[test] +fn test_eval_declared_class_implements_aot_interface_methods() { + let out = compile_and_run( + r#"label("Ada") . ":" . EvalAotImplementedBox::staticLabel("Bob");'); +"#, + ); + assert_eq!(out, "I:Ada:S:Bob"); +} + +/// Verifies eval rejects concrete classes missing generated/AOT interface methods. +#[test] +fn test_eval_declared_class_rejects_missing_aot_interface_methods() { + let err = compile_and_run_expect_failure( + r#"label();'); +"#, + ); + assert_eq!(out, "child"); +} + /// Verifies eval rejects global builtin attributes on unsupported OOP targets. #[test] fn test_eval_declared_builtin_attribute_target_validation() { From 44299cc5e035f2cdc1aeb46763c0e736efbe8205 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 15:30:10 +0200 Subject: [PATCH 0868/1208] Validate eval overrides against AOT parents --- .../src/interpreter/statements.rs | 112 ++++++++++++++++++ docs/php/eval.md | 3 +- tests/codegen/eval.rs | 38 ++++++ 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 617650e9db..e285a47b5f 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2599,6 +2599,7 @@ fn validate_eval_class_modifiers( return Err(EvalStatus::RuntimeFatal); } validate_method_parent_override(class, method, context)?; + validate_method_aot_parent_override(class, method, context, values)?; validate_eval_override_attribute(class, method, context, values)?; } Ok(()) @@ -2612,9 +2613,21 @@ fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool /// Bridge ReflectionMethod flag for static generated/AOT methods. const EVAL_REFLECTION_METHOD_FLAG_STATIC: u64 = 1; +/// Bridge ReflectionMethod flag for public generated/AOT methods. +const EVAL_REFLECTION_METHOD_FLAG_PUBLIC: u64 = 2; + +/// Bridge ReflectionMethod flag for protected generated/AOT methods. +const EVAL_REFLECTION_METHOD_FLAG_PROTECTED: u64 = 4; + /// Bridge ReflectionMethod flag for private generated/AOT methods. const EVAL_REFLECTION_METHOD_FLAG_PRIVATE: u64 = 8; +/// Bridge ReflectionMethod flag for final generated/AOT methods. +const EVAL_REFLECTION_METHOD_FLAG_FINAL: u64 = 16; + +/// Bridge ReflectionMethod flag for abstract generated/AOT methods. +const EVAL_REFLECTION_METHOD_FLAG_ABSTRACT: u64 = 32; + /// Method requirement discovered from generated/AOT interface metadata. struct EvalAotInterfaceMethodRequirement { owner: String, @@ -3558,6 +3571,105 @@ fn validate_method_parent_override( Ok(()) } +/// Validates one method declaration against inherited generated/AOT method metadata. +fn validate_method_aot_parent_override( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(()); + }; + if context.has_class(parent) || !values.class_exists(parent)? { + return Ok(()); + } + let parent = parent.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(parent, method.name())? else { + return Ok(()); + }; + if flags & EVAL_REFLECTION_METHOD_FLAG_PRIVATE != 0 { + return Ok(()); + } + let parent_is_static = flags & EVAL_REFLECTION_METHOD_FLAG_STATIC != 0; + if parent_is_static != method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + let parent_visibility = eval_aot_method_visibility(flags); + if method_visibility_rank(method.visibility()) < method_visibility_rank(parent_visibility) { + return Err(EvalStatus::RuntimeFatal); + } + if flags & EVAL_REFLECTION_METHOD_FLAG_FINAL != 0 { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && flags & EVAL_REFLECTION_METHOD_FLAG_ABSTRACT == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let Some(required) = eval_aot_method_signature_requirement( + parent, + method.name(), + parent_is_static, + context, + values, + )? else { + return Ok(()); + }; + if !class_method_satisfies_interface_signature( + method, + class.name(), + &required, + &eval_aot_method_declaring_class(parent, method.name(), values)?, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the eval visibility represented by generated/AOT reflection flags. +fn eval_aot_method_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_METHOD_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_METHOD_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else if flags & EVAL_REFLECTION_METHOD_FLAG_PUBLIC != 0 { + EvalVisibility::Public + } else { + EvalVisibility::Public + } +} + +/// Returns the generated/AOT declaring class for one reflected method. +fn eval_aot_method_declaring_class( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values + .reflection_method_declaring_class(class_name, method_name) + .map(|declaring_class| declaring_class.unwrap_or_else(|| class_name.to_string())) +} + +/// Returns a generated/AOT parent method signature as an eval method requirement. +fn eval_aot_method_signature_requirement( + class_name: &str, + method_name: &str, + is_static: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let declaring_class = eval_aot_method_declaring_class(class_name, method_name, values)?; + let signature = if is_static { + context.native_static_method_signature(&declaring_class, method_name) + } else { + context.native_method_signature(&declaring_class, method_name) + }; + Ok(signature.map(|signature| { + eval_native_signature_interface_method(method_name, is_static, &signature) + })) +} + /// Returns whether one eval class method can accept every call accepted by its parent method. fn class_method_signature_accepts( method: &EvalClassMethod, diff --git a/docs/php/eval.md b/docs/php/eval.md index cd751734a6..a1e9590e0a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -187,7 +187,8 @@ generated/AOT parent interface. Eval validates method override and interface method parameter and return types with PHP-style parameter contravariance and return covariance for supported declared type metadata, including nullable, union, `mixed`, `self`, `parent`, -`static`, class, and interface types. +`static`, class, and interface types, including generated/AOT parent method +metadata retained by the eval bridge. Abstract classes may defer missing interface methods and property contracts, but declared or inherited members that cover an interface contract are validated at declaration time. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c035283898..b475bf8d35 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12532,6 +12532,44 @@ echo (new EvalAotOverrideChild())->label();'); assert_eq!(out, "child"); } +/// Verifies eval rejects overriding final generated/AOT parent methods. +#[test] +fn test_eval_declared_class_rejects_final_aot_parent_method_override() { + let err = compile_and_run_expect_failure( + r#" Date: Sat, 27 Jun 2026 15:38:54 +0200 Subject: [PATCH 0869/1208] Validate eval constants against AOT metadata --- .../src/interpreter/statements.rs | 118 ++++++++++++++---- docs/php/eval.md | 4 +- tests/codegen/eval.rs | 76 +++++++++++ 3 files changed, 170 insertions(+), 28 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e285a47b5f..db3f54a12a 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1862,7 +1862,7 @@ pub(in crate::interpreter) fn execute_interface_decl_stmt( validate_eval_interface_override_attributes(interface, context, values)?; validate_eval_declared_constants(interface.constants())?; validate_eval_interface_constants(interface.constants())?; - validate_interface_constant_parent_redeclarations(interface, context)?; + validate_interface_constant_parent_redeclarations(interface, context, values)?; if context.define_interface(interface.clone()) { initialize_eval_declared_constants( interface.name(), @@ -2577,7 +2577,7 @@ fn validate_eval_class_modifiers( } validate_eval_declared_constants(class.constants())?; for constant in class.constants() { - validate_constant_parent_redeclaration(class, constant, context)?; + validate_constant_parent_redeclaration(class, constant, context, values)?; } validate_eval_declared_properties(class, context)?; for property in class.properties() { @@ -2610,23 +2610,23 @@ fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool eval_attributes_have_global_builtin_attribute(class.attributes(), "AllowDynamicProperties") } -/// Bridge ReflectionMethod flag for static generated/AOT methods. -const EVAL_REFLECTION_METHOD_FLAG_STATIC: u64 = 1; +/// Bridge reflection flag for static generated/AOT members. +const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; -/// Bridge ReflectionMethod flag for public generated/AOT methods. -const EVAL_REFLECTION_METHOD_FLAG_PUBLIC: u64 = 2; +/// Bridge reflection flag for public generated/AOT members. +const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; -/// Bridge ReflectionMethod flag for protected generated/AOT methods. -const EVAL_REFLECTION_METHOD_FLAG_PROTECTED: u64 = 4; +/// Bridge reflection flag for protected generated/AOT members. +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; -/// Bridge ReflectionMethod flag for private generated/AOT methods. -const EVAL_REFLECTION_METHOD_FLAG_PRIVATE: u64 = 8; +/// Bridge reflection flag for private generated/AOT members. +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; -/// Bridge ReflectionMethod flag for final generated/AOT methods. -const EVAL_REFLECTION_METHOD_FLAG_FINAL: u64 = 16; +/// Bridge reflection flag for final generated/AOT members. +const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; -/// Bridge ReflectionMethod flag for abstract generated/AOT methods. -const EVAL_REFLECTION_METHOD_FLAG_ABSTRACT: u64 = 32; +/// Bridge reflection flag for abstract generated/AOT members. +const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; /// Method requirement discovered from generated/AOT interface metadata. struct EvalAotInterfaceMethodRequirement { @@ -2713,7 +2713,7 @@ fn eval_aot_interface_parent_method_matches( } let parent = parent.trim_start_matches('\\'); if let Some(flags) = values.reflection_method_flags(parent, method.name())? { - let parent_method_is_static = flags & EVAL_REFLECTION_METHOD_FLAG_STATIC != 0; + let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; return Ok(parent_method_is_static == method.is_static()); } } @@ -2866,8 +2866,8 @@ fn eval_method_overrides_aot_parent( let Some(flags) = values.reflection_method_flags(parent, method.name())? else { return Ok(false); }; - let parent_method_is_static = flags & EVAL_REFLECTION_METHOD_FLAG_STATIC != 0; - let parent_method_is_private = flags & EVAL_REFLECTION_METHOD_FLAG_PRIVATE != 0; + let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let parent_method_is_private = flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0; Ok(!parent_method_is_private && parent_method_is_static == method.is_static()) } @@ -3330,6 +3330,7 @@ fn validate_eval_interface_constants(constants: &[EvalClassConstant]) -> Result< fn validate_interface_constant_parent_redeclarations( interface: &EvalInterface, context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { for constant in interface.constants() { for parent in interface.parents() { @@ -3339,6 +3340,7 @@ fn validate_interface_constant_parent_redeclarations( return Err(EvalStatus::RuntimeFatal); } } + validate_aot_interface_constant_redeclaration(parent, constant, values)?; } } Ok(()) @@ -3349,6 +3351,7 @@ fn validate_constant_parent_redeclaration( class: &EvalClass, constant: &EvalClassConstant, context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if let Some(parent) = class.parent() { if let Some((_, parent_constant)) = context.class_constant(parent, constant.name()) { @@ -3360,10 +3363,11 @@ fn validate_constant_parent_redeclaration( return Err(EvalStatus::RuntimeFatal); } } + validate_aot_class_constant_redeclaration(parent, constant, values)?; } - for interface in class.interfaces() { + for interface in pending_class_interface_names(class, context) { if let Some((_, interface_constant)) = - context.interface_constant(interface, constant.name()) + context.interface_constant(&interface, constant.name()) { if interface_constant.is_final() || constant_visibility_rank(constant.visibility()) @@ -3372,10 +3376,70 @@ fn validate_constant_parent_redeclaration( return Err(EvalStatus::RuntimeFatal); } } + validate_aot_interface_constant_redeclaration(&interface, constant, values)?; } Ok(()) } +/// Validates a class constant redeclaration against a generated/AOT parent class constant. +fn validate_aot_class_constant_redeclaration( + parent: &str, + constant: &EvalClassConstant, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.class_exists(parent)? { + return Ok(()); + } + validate_aot_constant_redeclaration(parent, constant, false, values) +} + +/// Validates a class/interface constant redeclaration against a generated/AOT interface constant. +fn validate_aot_interface_constant_redeclaration( + interface: &str, + constant: &EvalClassConstant, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.interface_exists(interface)? { + return Ok(()); + } + validate_aot_constant_redeclaration(interface, constant, true, values) +} + +/// Applies PHP redeclaration rules to one generated/AOT constant metadata row. +fn validate_aot_constant_redeclaration( + class_like: &str, + constant: &EvalClassConstant, + interface_context: bool, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_like = class_like.trim_start_matches('\\'); + let Some(flags) = values.reflection_constant_flags(class_like, constant.name())? else { + return Ok(()); + }; + let inherited_visibility = eval_aot_constant_visibility(flags); + if !interface_context && inherited_visibility == EvalVisibility::Private { + return Ok(()); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(inherited_visibility) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the eval visibility represented by generated/AOT constant reflection flags. +fn eval_aot_constant_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + /// Returns a comparable rank where larger means less restrictive constant visibility. fn constant_visibility_rank(visibility: EvalVisibility) -> u8 { match visibility { @@ -3588,10 +3652,10 @@ fn validate_method_aot_parent_override( let Some(flags) = values.reflection_method_flags(parent, method.name())? else { return Ok(()); }; - if flags & EVAL_REFLECTION_METHOD_FLAG_PRIVATE != 0 { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { return Ok(()); } - let parent_is_static = flags & EVAL_REFLECTION_METHOD_FLAG_STATIC != 0; + let parent_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; if parent_is_static != method.is_static() { return Err(EvalStatus::RuntimeFatal); } @@ -3599,10 +3663,10 @@ fn validate_method_aot_parent_override( if method_visibility_rank(method.visibility()) < method_visibility_rank(parent_visibility) { return Err(EvalStatus::RuntimeFatal); } - if flags & EVAL_REFLECTION_METHOD_FLAG_FINAL != 0 { + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 { return Err(EvalStatus::RuntimeFatal); } - if method.is_abstract() && flags & EVAL_REFLECTION_METHOD_FLAG_ABSTRACT == 0 { + if method.is_abstract() && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 { return Err(EvalStatus::RuntimeFatal); } let Some(required) = eval_aot_method_signature_requirement( @@ -3629,11 +3693,11 @@ fn validate_method_aot_parent_override( /// Returns the eval visibility represented by generated/AOT reflection flags. fn eval_aot_method_visibility(flags: u64) -> EvalVisibility { - if flags & EVAL_REFLECTION_METHOD_FLAG_PRIVATE != 0 { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { EvalVisibility::Private - } else if flags & EVAL_REFLECTION_METHOD_FLAG_PROTECTED != 0 { + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { EvalVisibility::Protected - } else if flags & EVAL_REFLECTION_METHOD_FLAG_PUBLIC != 0 { + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC != 0 { EvalVisibility::Public } else { EvalVisibility::Public @@ -4032,7 +4096,7 @@ fn eval_aot_interface_method_requirement( let Some(flags) = values.reflection_method_flags(interface, method_name)? else { return Ok(None); }; - let is_static = flags & EVAL_REFLECTION_METHOD_FLAG_STATIC != 0; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; let owner = values .reflection_method_declaring_class(interface, method_name)? .unwrap_or_else(|| interface.to_string()); diff --git a/docs/php/eval.md b/docs/php/eval.md index a1e9590e0a..263083d815 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -201,7 +201,9 @@ state: reads before initialization or after `unset()` throw the same catchable `Error`, while untyped properties and explicit `= null` defaults are initialized to `null`. Eval validates inherited class/interface constant redeclarations with PHP-style -visibility compatibility, and rejects non-public interface constants. +visibility compatibility, including generated/AOT parent and interface constants +when their reflection metadata is available, and rejects non-public interface +constants. Eval-declared method calls also enforce declared return values at runtime, with weak scalar coercions and PHP-style handling for `void`, `never`, `self`, `parent`, and late-bound `static`. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index b475bf8d35..e1dc726f05 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12792,6 +12792,25 @@ class EvalFinalConstChild extends EvalFinalConstBase { ); } +/// Verifies eval-declared classes cannot redeclare final generated/AOT class constants. +#[test] +fn test_eval_declared_class_rejects_final_aot_parent_constant_override() { + let err = compile_and_run_expect_failure( + r#" Date: Sat, 27 Jun 2026 15:50:21 +0200 Subject: [PATCH 0870/1208] Validate eval properties against AOT parents --- .../src/interpreter/statements.rs | 120 ++++++++++++++++-- docs/php/eval.md | 4 +- tests/codegen/eval.rs | 83 ++++++++++++ 3 files changed, 195 insertions(+), 12 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index db3f54a12a..da689c0951 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2581,7 +2581,7 @@ fn validate_eval_class_modifiers( } validate_eval_declared_properties(class, context)?; for property in class.properties() { - validate_property_parent_redeclaration(class, property, context)?; + validate_property_parent_redeclaration(class, property, context, values)?; } for method in class.methods() { validate_eval_method_attribute_targets(method.attributes())?; @@ -2628,6 +2628,15 @@ const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; /// Bridge reflection flag for abstract generated/AOT members. const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; +/// Bridge reflection flag for readonly generated/AOT properties. +const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; + +/// Bridge reflection flag for protected-set generated/AOT properties. +const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; + +/// Bridge reflection flag for private-set generated/AOT properties. +const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; + /// Method requirement discovered from generated/AOT interface metadata. struct EvalAotInterfaceMethodRequirement { owner: String, @@ -3264,33 +3273,86 @@ fn validate_property_parent_redeclaration( class: &EvalClass, property: &EvalClassProperty, context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let Some(parent) = class.parent() else { return Ok(()); }; - let Some((parent_declaring_class, parent_property)) = + if let Some((parent_declaring_class, parent_property)) = context.class_property(parent, property.name()) - else { + { + if parent_property.visibility() == EvalVisibility::Private { + return Ok(()); + } + if parent_property.is_final() + || parent_property.set_visibility() == Some(EvalVisibility::Private) + { + return Err(EvalStatus::RuntimeFatal); + } + if parent_property.is_static() != property.is_static() + || (parent_property.is_readonly() && !property.is_readonly()) + || property_visibility_rank(property.visibility()) + < property_visibility_rank(parent_property.visibility()) + || property_visibility_rank(property.write_visibility()) + < property_visibility_rank(parent_property.write_visibility()) + || !property_type_signature_matches( + property.property_type(), + class.name(), + parent_property.property_type(), + &parent_declaring_class, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(()); + } + validate_property_aot_parent_redeclaration(parent, class, property, context, values) +} + +/// Validates one property declaration against inherited generated/AOT property metadata. +fn validate_property_aot_parent_redeclaration( + parent: &str, + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if context.has_class(parent) || !values.class_exists(parent)? { + return Ok(()); + } + let parent = parent.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(parent, property.name())? else { return Ok(()); }; - if parent_property.visibility() == EvalVisibility::Private { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { return Ok(()); } - if parent_property.is_final() - || parent_property.set_visibility() == Some(EvalVisibility::Private) + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 + || flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { return Err(EvalStatus::RuntimeFatal); } - if parent_property.is_static() != property.is_static() - || (parent_property.is_readonly() && !property.is_readonly()) + let parent_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let parent_visibility = eval_aot_property_visibility(flags); + let parent_write_visibility = eval_aot_property_write_visibility(flags, parent_visibility); + let parent_is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; + let parent_declaring_class = + eval_aot_property_declaring_class(parent, property.name(), values)?; + let parent_property_type = context + .native_property_type(&parent_declaring_class, property.name()) + .or_else(|| context.native_property_type(parent, property.name())); + if parent_is_static != property.is_static() + || (parent_is_readonly && !property.is_readonly()) || property_visibility_rank(property.visibility()) - < property_visibility_rank(parent_property.visibility()) + < property_visibility_rank(parent_visibility) || property_visibility_rank(property.write_visibility()) - < property_visibility_rank(parent_property.write_visibility()) + < property_visibility_rank(parent_write_visibility) || !property_type_signature_matches( property.property_type(), class.name(), - parent_property.property_type(), + parent_property_type.as_ref(), &parent_declaring_class, Some(class), context, @@ -3301,6 +3363,42 @@ fn validate_property_parent_redeclaration( Ok(()) } +/// Returns the eval visibility represented by generated/AOT property reflection flags. +fn eval_aot_property_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + +/// Returns the eval write visibility represented by generated/AOT property flags. +fn eval_aot_property_write_visibility( + flags: u64, + read_visibility: EvalVisibility, +) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + EvalVisibility::Protected + } else { + read_visibility + } +} + +/// Returns the generated/AOT declaring class for one reflected property. +fn eval_aot_property_declaring_class( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values + .reflection_property_declaring_class(class_name, property_name) + .map(|declaring_class| declaring_class.unwrap_or_else(|| class_name.to_string())) +} + /// Validates constant declarations that can be checked before registration. fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { let mut names = std::collections::HashSet::new(); diff --git a/docs/php/eval.md b/docs/php/eval.md index 263083d815..e6eb6af20d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -195,7 +195,9 @@ declaration time. Eval validates inherited property redeclarations with PHP-style invariant property types, matching static storage, compatible read/write visibility, and readonly compatibility: child redeclarations may add `readonly`, but may not -remove inherited `readonly`. +remove inherited `readonly`. The same checks apply to generated/AOT parent +property metadata when the eval bridge exposes reflection flags and declared +property types. Typed eval-declared instance and static properties track PHP initialization state: reads before initialization or after `unset()` throw the same catchable `Error`, while untyped properties and explicit `= null` defaults are initialized diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e1dc726f05..30b4546ac4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12570,6 +12570,89 @@ eval('class EvalAotParentSignatureChild extends EvalAotParentSignatureBase { ); } +/// Verifies eval rejects overriding final generated/AOT parent properties. +#[test] +fn test_eval_declared_class_rejects_final_aot_parent_property_override() { + let err = compile_and_run_expect_failure( + r#" Date: Sat, 27 Jun 2026 16:03:47 +0200 Subject: [PATCH 0871/1208] Validate eval properties against AOT interfaces --- crates/elephc-magician/src/context.rs | 33 +++++ .../elephc-magician/src/ffi/native_methods.rs | 83 +++++++++++- crates/elephc-magician/src/ffi/tests.rs | 51 +++++++ .../src/interpreter/statements.rs | 42 ++++++ docs/php/eval.md | 3 +- src/codegen/lower_inst/builtins/eval.rs | 128 ++++++++++++++++++ tests/codegen/eval.rs | 74 ++++++++++ 7 files changed, 410 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index d1f8998956..24294e3beb 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -645,6 +645,7 @@ pub struct ElephcEvalContext { native_class_attributes: HashMap>, native_method_attributes: HashMap<(String, String), Vec>, native_constant_attributes: HashMap<(String, String), Vec>, + native_interface_properties: HashMap>, native_property_types: HashMap<(String, String), EvalParameterType>, native_property_defaults: HashMap<(String, String), NativeCallableDefault>, native_property_attributes: HashMap<(String, String), Vec>, @@ -708,6 +709,7 @@ impl ElephcEvalContext { native_class_attributes: HashMap::new(), native_method_attributes: HashMap::new(), native_constant_attributes: HashMap::new(), + native_interface_properties: HashMap::new(), native_property_types: HashMap::new(), native_property_defaults: HashMap::new(), native_property_attributes: HashMap::new(), @@ -772,6 +774,7 @@ impl ElephcEvalContext { native_class_attributes: HashMap::new(), native_method_attributes: HashMap::new(), native_constant_attributes: HashMap::new(), + native_interface_properties: HashMap::new(), native_property_types: HashMap::new(), native_property_defaults: HashMap::new(), native_property_attributes: HashMap::new(), @@ -2992,6 +2995,36 @@ impl ElephcEvalContext { .unwrap_or_default() } + /// Defines generated AOT interface property-hook metadata for eval validation. + pub fn define_native_interface_property_requirement( + &mut self, + interface_name: &str, + property: EvalInterfaceProperty, + ) -> bool { + let key = normalize_class_name(interface_name); + if key.is_empty() || property.name().is_empty() { + return false; + } + let owner = interface_name.trim_start_matches('\\').to_string(); + let requirements = self.native_interface_properties.entry(key).or_default(); + if requirements.iter().any(|(_, existing)| existing.name() == property.name()) { + return false; + } + requirements.push((owner, property)); + true + } + + /// Returns generated AOT interface property-hook metadata by interface name. + pub fn native_interface_property_requirements( + &self, + interface_name: &str, + ) -> Vec<(String, EvalInterfaceProperty)> { + self.native_interface_properties + .get(&normalize_class_name(interface_name)) + .cloned() + .unwrap_or_default() + } + /// Defines generated AOT property type metadata for eval reflection. pub fn define_native_property_type( &mut self, diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index 594e48b865..de8a9774f2 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -1,6 +1,7 @@ //! Purpose: -//! Exports registration of generated native PHP method signatures into an eval -//! context so runtime fragments can bind AOT method named arguments. +//! Exports registration of generated native PHP method signatures and related +//! metadata into an eval context so runtime fragments can bind AOT calls and +//! validate generated interface contracts. //! //! Called from: //! - Generated EIR backend assembly before fragments can call AOT methods. @@ -17,7 +18,8 @@ use crate::context::{ NativeCallableObjectDefaultArg, NativeCallableSignature, }; use crate::eval_ir::{ - EvalAttribute, EvalAttributeArg, EvalParameterType, EvalParameterTypeVariant, + EvalAttribute, EvalAttributeArg, EvalInterfaceProperty, EvalParameterType, + EvalParameterTypeVariant, }; const NATIVE_DEFAULT_NULL: u64 = 0; @@ -46,6 +48,8 @@ const NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; const NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; const NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; const NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; +const NATIVE_INTERFACE_PROPERTY_REQUIRES_GET: u64 = 1; +const NATIVE_INTERFACE_PROPERTY_REQUIRES_SET: u64 = 2; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; #[derive(Clone, Copy)] @@ -90,6 +94,34 @@ pub unsafe extern "C" fn __elephc_eval_register_native_static_method( .unwrap_or(0) } +/// Registers one generated native PHP interface property contract in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a +/// readable `InterfaceName::propertyName` byte string, and `type_spec_ptr` must +/// point to a readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_interface_property( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_interface_property_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + flags, + ) + }) + .unwrap_or(0) +} + /// Registers one generated native PHP method parameter name in an eval context. /// /// # Safety @@ -990,6 +1022,51 @@ unsafe fn register_native_method_inner( } } +/// Runs native interface property registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_interface_property`; invalid handles, +/// names, flags, or type specs fail closed as `false`. +unsafe fn register_native_interface_property_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((interface_name, property_name)) = split_property_key(&property_key) else { + return 0; + }; + let requires_get = flags & NATIVE_INTERFACE_PROPERTY_REQUIRES_GET != 0; + let requires_set = flags & NATIVE_INTERFACE_PROPERTY_REQUIRES_SET != 0; + if !requires_get && !requires_set { + return 0; + } + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + let property = EvalInterfaceProperty::new(property_name, requires_get, requires_set) + .with_type(Some(property_type)); + i32::from(context.define_native_interface_property_requirement( + interface_name, + property, + )) +} + /// Runs native method parameter registration after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 8f4e7e159c..a774256fff 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -35,6 +35,8 @@ const TEST_NATIVE_DEFAULT_BOOL: u64 = 1; const TEST_NATIVE_DEFAULT_INT: u64 = 2; const TEST_NATIVE_DEFAULT_FLOAT: u64 = 3; const TEST_NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; +const TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_GET: u64 = 1; +const TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_SET: u64 = 2; const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; @@ -1156,6 +1158,55 @@ fn register_native_property_type_records_metadata() { assert!(ctx.native_property_type("KnownClass", "bad").is_none()); } +/// Verifies native AOT interface property contracts are available to eval validation. +#[test] +fn register_native_interface_property_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownContract::name"; + let property_type = b"string"; + let invalid_property = b"KnownContract::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_interface_property( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_GET + | TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_SET, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_interface_property( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_GET, + ) + }; + + let requirements = ctx.native_interface_property_requirements("knowncontract"); + assert_eq!(registered, 1); + assert_eq!(requirements.len(), 1); + assert_eq!(requirements[0].0, "KnownContract"); + assert_eq!(requirements[0].1.name(), "name"); + assert!(requirements[0].1.requires_get()); + assert!(requirements[0].1.requires_set()); + assert_eq!( + requirements[0].1.property_type().map(|ty| ty.variants()), + Some(&[EvalParameterTypeVariant::String][..]) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_interface_property_requirements("KnownContract") + .iter() + .all(|(_, property)| property.name() != "bad")); +} + /// Verifies native AOT property default metadata is available to eval reflection. #[test] fn register_native_property_default_records_metadata() { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index da689c0951..5bf0a057aa 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -3614,6 +3614,25 @@ fn validate_declared_class_aot_interface_members( return Err(EvalStatus::RuntimeFatal); } } + for (requirement_owner, requirement) in + pending_class_aot_interface_property_requirements(class, context, values)? + { + let Some((declaring_class, property)) = + pending_class_property_with_owner(class, requirement.name(), context) + else { + continue; + }; + if !class_property_can_cover_interface_contract( + &property, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } Ok(()) } @@ -3924,6 +3943,13 @@ fn validate_concrete_class_aot_interface_requirements( return Err(EvalStatus::RuntimeFatal); } } + for (requirement_owner, requirement) in + pending_class_aot_interface_property_requirements(class, context, values)? + { + if !class_has_interface_property(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } Ok(()) } @@ -4154,6 +4180,22 @@ fn pending_class_aot_interface_method_requirements( Ok(requirements) } +/// Returns generated/AOT interface property requirements inherited by a pending class. +fn pending_class_aot_interface_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) || !values.interface_exists(&interface)? { + continue; + } + requirements.extend(context.native_interface_property_requirements(&interface)); + } + Ok(requirements) +} + /// Returns generated/AOT method requirements for one runtime interface. fn eval_aot_interface_method_requirements( interface: &str, diff --git a/docs/php/eval.md b/docs/php/eval.md index e6eb6af20d..c9a30fe78a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -191,7 +191,8 @@ declared type metadata, including nullable, union, `mixed`, `self`, `parent`, metadata retained by the eval bridge. Abstract classes may defer missing interface methods and property contracts, but declared or inherited members that cover an interface contract are validated at -declaration time. +declaration time. Generated/AOT interface method and property-hook contracts are +validated when their bridge metadata is available. Eval validates inherited property redeclarations with PHP-style invariant property types, matching static storage, compatible read/write visibility, and readonly compatibility: child redeclarations may add `readonly`, but may not diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index a4915127bd..2583ba9947 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -24,6 +24,7 @@ use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; use crate::parser::ast::{Expr, ExprKind, TypeExpr, Visibility}; use crate::types::{ is_php_integer_array_key, AttrArgValue, ClassInfo, FunctionSig, InterfaceInfo, PhpType, + PropertyHookContract, }; use super::super::super::context::FunctionContext; @@ -66,6 +67,8 @@ const NATIVE_DEFAULT_BOOL: i64 = 1; const NATIVE_DEFAULT_INT: i64 = 2; const NATIVE_DEFAULT_FLOAT: i64 = 3; const NATIVE_DEFAULT_EMPTY_ARRAY: i64 = 4; +const NATIVE_INTERFACE_PROPERTY_REQUIRES_GET: i64 = 1; +const NATIVE_INTERFACE_PROPERTY_REQUIRES_SET: i64 = 2; const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; const NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT: u8 = 2; @@ -141,6 +144,15 @@ struct EvalNativePropertyTypeRegistration { type_spec: String, } +/// A module-local interface property contract that can be registered with the eval context. +struct EvalNativeInterfacePropertyRegistration { + interface_name: String, + property_name: String, + type_spec: String, + requires_get: bool, + requires_set: bool, +} + /// A module-local property default that can be registered with the eval context. struct EvalNativePropertyDefaultRegistration { class_name: String, @@ -1334,6 +1346,9 @@ fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context for registration in eval_native_property_type_registrations(ctx) { register_eval_native_property_type(ctx, context_offset, ®istration); } + for registration in eval_native_interface_property_registrations(ctx) { + register_eval_native_interface_property(ctx, context_offset, ®istration); + } for registration in eval_native_property_default_registrations(ctx) { register_eval_native_property_default(ctx, context_offset, ®istration); } @@ -1478,6 +1493,67 @@ fn eval_native_property_type_registrations( registrations } +/// Collects AOT interface property contracts that eval can validate at declaration time. +fn eval_native_interface_property_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut interfaces = ctx.module.interface_infos.iter().collect::>(); + interfaces.sort_by_key(|(_, interface_info)| interface_info.interface_id); + for (interface_name, interface_info) in interfaces { + let mut property_names = interface_info.property_order.iter().collect::>(); + if property_names.is_empty() { + property_names = interface_info.properties.keys().collect(); + property_names.sort(); + } + for property_name in property_names { + let Some(contract) = interface_info.properties.get(property_name) else { + continue; + }; + let Some(registration) = + eval_native_interface_property_registration(interface_name, property_name, contract) + else { + continue; + }; + registrations.push(registration); + } + } + registrations +} + +/// Converts one static interface property contract into eval-native metadata. +fn eval_native_interface_property_registration( + interface_name: &str, + property_name: &str, + contract: &PropertyHookContract, +) -> Option { + let requires_get = contract.get_type.is_some(); + let requires_set = contract.set_type.is_some(); + if !requires_get && !requires_set { + return None; + } + let type_spec = eval_native_interface_property_type_spec(contract)?; + Some(EvalNativeInterfacePropertyRegistration { + interface_name: interface_name.to_string(), + property_name: property_name.to_string(), + type_spec, + requires_get, + requires_set, + }) +} + +/// Returns the single property type representation accepted by EvalIR metadata. +fn eval_native_interface_property_type_spec(contract: &PropertyHookContract) -> Option { + match (contract.get_type.as_ref(), contract.set_type.as_ref()) { + (Some(get_type), Some(set_type)) if get_type == set_type => { + eval_native_php_type_spec(get_type, false) + } + (Some(get_type), None) => eval_native_php_type_spec(get_type, false), + (None, Some(set_type)) => eval_native_php_type_spec(set_type, false), + _ => None, + } +} + /// Collects AOT property defaults whose value can be exposed to eval reflection. fn eval_native_property_default_registrations( ctx: &FunctionContext<'_>, @@ -3217,6 +3293,58 @@ fn register_eval_native_property_type( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native interface-property metadata registration call into the eval context. +fn register_eval_native_interface_property( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeInterfacePropertyRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let property_key = format!( + "{}::{}", + registration.interface_name, registration.property_name + ); + let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &property_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + property_key_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let mut flags = 0; + if registration.requires_get { + flags |= NATIVE_INTERFACE_PROPERTY_REQUIRES_GET; + } + if registration.requires_set { + flags |= NATIVE_INTERFACE_PROPERTY_REQUIRES_SET; + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + flags, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_interface_property"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native property-default metadata registration call into the eval context. fn register_eval_native_property_default( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 30b4546ac4..db605f8d7e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12514,6 +12514,80 @@ eval('class EvalAotSignatureBox implements EvalAotSignatureContract { ); } +/// Verifies eval classes can implement generated/AOT interface property contracts. +#[test] +fn test_eval_declared_class_implements_aot_interface_properties() { + let out = compile_and_run( + r#"name = "Grace"; +echo $box->name;'); +"#, + ); + assert_eq!(out, "Grace"); +} + +/// Verifies eval rejects concrete classes missing generated/AOT interface properties. +#[test] +fn test_eval_declared_class_rejects_missing_aot_interface_property() { + let err = compile_and_run_expect_failure( + r#" Date: Sat, 27 Jun 2026 16:17:19 +0200 Subject: [PATCH 0872/1208] Reflect AOT interface property hooks in eval --- crates/elephc-magician/src/context.rs | 5 +- .../elephc-magician/src/ffi/native_methods.rs | 19 +- crates/elephc-magician/src/ffi/tests.rs | 6 +- .../src/interpreter/reflection.rs | 238 ++++++++++++++---- docs/php/eval.md | 5 +- src/codegen/lower_inst/builtins/eval.rs | 8 +- tests/codegen/eval.rs | 44 ++++ 7 files changed, 260 insertions(+), 65 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 24294e3beb..cf503537f0 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -2999,13 +2999,14 @@ impl ElephcEvalContext { pub fn define_native_interface_property_requirement( &mut self, interface_name: &str, + declaring_interface_name: &str, property: EvalInterfaceProperty, ) -> bool { let key = normalize_class_name(interface_name); - if key.is_empty() || property.name().is_empty() { + let owner = declaring_interface_name.trim_start_matches('\\').to_string(); + if key.is_empty() || owner.is_empty() || property.name().is_empty() { return false; } - let owner = interface_name.trim_start_matches('\\').to_string(); let requirements = self.native_interface_properties.entry(key).or_default(); if requirements.iter().any(|(_, existing)| existing.name() == property.name()) { return false; diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index de8a9774f2..eae79b9403 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -98,8 +98,8 @@ pub unsafe extern "C" fn __elephc_eval_register_native_static_method( /// /// # Safety /// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a -/// readable `InterfaceName::propertyName` byte string, and `type_spec_ptr` must -/// point to a readable generated type-spec byte string. +/// readable `InterfaceName::DeclaringInterface::propertyName` byte string, and +/// `type_spec_ptr` must point to a readable generated type-spec byte string. #[no_mangle] pub unsafe extern "C" fn __elephc_eval_register_native_interface_property( ctx: *mut ElephcEvalContext, @@ -1044,7 +1044,9 @@ unsafe fn register_native_interface_property_inner( let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { return 0; }; - let Some((interface_name, property_name)) = split_property_key(&property_key) else { + let Some((interface_name, declaring_interface_name, property_name)) = + split_interface_property_key(&property_key) + else { return 0; }; let requires_get = flags & NATIVE_INTERFACE_PROPERTY_REQUIRES_GET != 0; @@ -1063,6 +1065,7 @@ unsafe fn register_native_interface_property_inner( .with_type(Some(property_type)); i32::from(context.define_native_interface_property_requirement( interface_name, + declaring_interface_name, property, )) } @@ -2299,3 +2302,13 @@ fn split_method_key(method_key: &str) -> Option<(&str, &str)> { fn split_property_key(property_key: &str) -> Option<(&str, &str)> { split_method_key(property_key) } + +/// Splits `InterfaceName::DeclaringInterface::propertyName` interface-property metadata keys. +fn split_interface_property_key(property_key: &str) -> Option<(&str, &str, &str)> { + let (owner_key, property_name) = property_key.rsplit_once("::")?; + let (interface_name, declaring_interface_name) = owner_key.rsplit_once("::")?; + (!interface_name.is_empty() + && !declaring_interface_name.is_empty() + && !property_name.is_empty()) + .then_some((interface_name, declaring_interface_name, property_name)) +} diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index a774256fff..bdffcce00d 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -1162,9 +1162,9 @@ fn register_native_property_type_records_metadata() { #[test] fn register_native_interface_property_records_metadata() { let mut ctx = ElephcEvalContext::new(); - let property = b"KnownContract::name"; + let property = b"KnownContract::KnownParentContract::name"; let property_type = b"string"; - let invalid_property = b"KnownContract::bad"; + let invalid_property = b"KnownContract::KnownParentContract::bad"; let invalid_type = b"void"; let registered = unsafe { @@ -1192,7 +1192,7 @@ fn register_native_interface_property_records_metadata() { let requirements = ctx.native_interface_property_requirements("knowncontract"); assert_eq!(registered, 1); assert_eq!(requirements.len(), 1); - assert_eq!(requirements[0].0, "KnownContract"); + assert_eq!(requirements[0].0, "KnownParentContract"); assert_eq!(requirements[0].1.name(), "name"); assert!(requirements[0].1.requires_get()); assert!(requirements[0].1.requires_set()); diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 6449fb49d8..467134245b 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -730,6 +730,12 @@ pub(in crate::interpreter) fn eval_reflection_class_has_property_result( values, )? .is_some() + || eval_reflection_native_interface_property_requirement( + &reflected_name, + &property_name, + context, + ) + .is_some() }; if !exists { if let Some(dynamic_object) = @@ -2145,19 +2151,9 @@ pub(in crate::interpreter) fn eval_reflection_property_to_string_result( let text = eval_reflection_property_to_string(&property_name, &member); return values.string(&text).map(Some); } - let member = if let Some(member) = - eval_reflection_property_metadata(&declaring_class, &property_name, context) - { - member - } else { - eval_reflection_aot_property_metadata_if_exists( - &declaring_class, - &property_name, - context, - values, - )? - .ok_or(EvalStatus::RuntimeFatal)? - }; + let member = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; let text = eval_reflection_property_to_string(&property_name, &member); values.string(&text).map(Some) } @@ -2425,7 +2421,17 @@ pub(in crate::interpreter) fn eval_reflection_class_get_members_result( }) .map(Some); } - let names = eval_reflection_aot_member_names(owner_kind, &reflected_name, values)?; + let native_interface_property_names = + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + eval_reflection_native_interface_property_names(&reflected_name, context) + } else { + Vec::new() + }; + let names = if native_interface_property_names.is_empty() { + eval_reflection_aot_member_names(owner_kind, &reflected_name, values)? + } else { + native_interface_property_names + }; eval_reflection_aot_member_object_array_result( owner_kind, &reflected_name, @@ -2498,6 +2504,23 @@ pub(in crate::interpreter) fn eval_reflection_class_get_member_result( if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && !eval_reflection_class_like_exists(&reflected_name, context) { + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + &reflected_name, + &requested_name, + context, + ) + { + let member = eval_reflection_interface_property_metadata(declaring_class, &property); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } if let Some(member) = eval_reflection_aot_property_metadata_if_exists( &reflected_name, &requested_name, @@ -3727,6 +3750,23 @@ fn eval_reflection_property_object_result_if_exists( .resolve_class_like_name(class_name) .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); if !eval_reflection_class_like_exists(&reflected_name, context) { + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + &reflected_name, + property_name, + context, + ) + { + let property = eval_reflection_interface_property_metadata(declaring_class, &property); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); + } let Some(property) = eval_reflection_aot_property_metadata_if_exists( &reflected_name, property_name, @@ -5891,7 +5931,13 @@ fn eval_reflection_aot_member_object_array_result( class_name, name, context, values, )? } else { - eval_reflection_aot_property_metadata_if_exists(class_name, name, context, values)? + eval_reflection_native_interface_property_requirement(class_name, name, context) + .map(|(declaring_class, property)| { + eval_reflection_interface_property_metadata(declaring_class, &property) + }) + .or(eval_reflection_aot_property_metadata_if_exists( + class_name, name, context, values, + )?) }; let Some(member) = member else { continue; @@ -6430,11 +6476,17 @@ fn eval_reflection_class_to_string_metadata( runtime_class_name, values, )?; - let property_names = eval_reflection_aot_member_names( - EVAL_REFLECTION_OWNER_PROPERTY, - runtime_class_name, - values, - )?; + let native_interface_property_names = + eval_reflection_native_interface_property_names(runtime_class_name, context); + let property_names = if native_interface_property_names.is_empty() { + eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + runtime_class_name, + values, + )? + } else { + native_interface_property_names + }; let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; @@ -7529,40 +7581,18 @@ fn eval_reflection_property_metadata( .interface_property_requirements(class_name) .into_iter() .find(|property| property.name() == property_name) - .map(|property| EvalReflectionMemberMetadata { - declaring_class_name: Some(class_name.to_string()), - source_file: None, - source_location: None, - attributes: property.attributes().to_vec(), - visibility: EvalVisibility::Public, - is_static: false, - is_final: false, - is_abstract: true, - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_property_modifiers( - EvalVisibility::Public, - property.set_visibility(), - false, - false, - true, - false, - true, - ), - type_metadata: property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - settable_type_metadata: property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - return_type_metadata: None, - default_value: None, - default_value_trait_origin: None, - required_parameter_count: 0, - parameters: Vec::new(), + .map(|property| { + eval_reflection_interface_property_metadata(class_name.to_string(), &property) }); } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement(class_name, property_name, context) + { + return Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + )); + } context.trait_decl(class_name).and_then(|trait_decl| { trait_decl .properties() @@ -7606,10 +7636,50 @@ fn eval_reflection_property_metadata( required_parameter_count: 0, parameters: Vec::new(), } - }) + }) }) } +/// Returns property metadata for a property contract declared on an interface. +fn eval_reflection_interface_property_metadata( + declaring_class: String, + property: &EvalInterfaceProperty, +) -> EvalReflectionMemberMetadata { + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + source_file: None, + source_location: None, + attributes: property.attributes().to_vec(), + visibility: EvalVisibility::Public, + is_static: false, + is_final: false, + is_abstract: true, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_property_modifiers( + EvalVisibility::Public, + property.set_visibility(), + false, + false, + true, + false, + true, + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, + default_value: None, + default_value_trait_origin: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} + /// Returns property names that can contribute to `ReflectionClass::getDefaultProperties()`. fn eval_reflection_default_property_names( reflected_name: &str, @@ -7636,6 +7706,18 @@ fn eval_reflection_default_property_metadata( if let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) { return Ok(Some(member)); } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + reflected_name, + property_name, + context, + ) + { + return Ok(Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + ))); + } eval_reflection_aot_property_metadata_if_exists(reflected_name, property_name, context, values) } @@ -7649,6 +7731,18 @@ fn eval_reflection_reflected_property_metadata( if let Some(member) = eval_reflection_property_metadata(declaring_class, property_name, context) { return Ok(Some(member)); } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + declaring_class, + property_name, + context, + ) + { + return Ok(Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + ))); + } eval_reflection_aot_property_metadata_if_exists(declaring_class, property_name, context, values) } @@ -7878,6 +7972,44 @@ fn eval_reflection_property_for_hooks( (declaring_class.to_string(), property) }) }) + .or_else(|| { + eval_reflection_native_interface_property_requirement( + declaring_class, + property_name, + context, + ) + .map(|(owner, property)| { + let property = EvalClassProperty::new(property.name(), None) + .with_type(property.property_type().cloned()) + .with_attributes(property.attributes().to_vec()) + .with_abstract_hook_contract(property.requires_get(), property.requires_set()); + (owner, property) + }) + }) +} + +/// Returns one generated/AOT interface property contract registered for eval reflection. +fn eval_reflection_native_interface_property_requirement( + interface_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalInterfaceProperty)> { + context + .native_interface_property_requirements(interface_name) + .into_iter() + .find(|(_, property)| property.name() == property_name) +} + +/// Returns generated/AOT interface property names registered for eval reflection. +fn eval_reflection_native_interface_property_names( + interface_name: &str, + context: &ElephcEvalContext, +) -> Vec { + context + .native_interface_property_requirements(interface_name) + .into_iter() + .map(|(_, property)| property.name().to_string()) + .collect() } /// Binds the `PropertyHookType $type` argument used by ReflectionProperty hook APIs. diff --git a/docs/php/eval.md b/docs/php/eval.md index c9a30fe78a..decdd2dbcd 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -585,8 +585,9 @@ metadata as a PHP-style `Property [ ... ]` descriptor for the supported visibility, static, type, default, and virtual-property surface. `ReflectionProperty::hasHooks()`, `hasHook()`, `getHooks()`, and `getHook()` expose eval-declared concrete, abstract, and interface property get/set hook -metadata and return hook `ReflectionMethod` objects using PHP's -`$property::get` / `$property::set` names. Eval also exposes +metadata plus registered generated/AOT interface property-hook metadata, and +return hook `ReflectionMethod` objects using PHP's `$property::get` / +`$property::set` names. Eval also exposes `PropertyHookType::Get` and `PropertyHookType::Set` inside evaluated fragments for those APIs, including `PropertyHookType::cases()`, `from()`, and `tryFrom()`. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 2583ba9947..195968f8ef 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -147,6 +147,7 @@ struct EvalNativePropertyTypeRegistration { /// A module-local interface property contract that can be registered with the eval context. struct EvalNativeInterfacePropertyRegistration { interface_name: String, + declaring_interface_name: String, property_name: String, type_spec: String, requires_get: bool, @@ -1535,6 +1536,7 @@ fn eval_native_interface_property_registration( let type_spec = eval_native_interface_property_type_spec(contract)?; Some(EvalNativeInterfacePropertyRegistration { interface_name: interface_name.to_string(), + declaring_interface_name: contract.declaring_type.clone(), property_name: property_name.to_string(), type_spec, requires_get, @@ -3301,8 +3303,10 @@ fn register_eval_native_interface_property( ) { load_eval_context_local_to_arg(ctx, context_offset, 0); let property_key = format!( - "{}::{}", - registration.interface_name, registration.property_name + "{}::{}::{}", + registration.interface_name, + registration.declaring_interface_name, + registration.property_name ); let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); abi::emit_symbol_address( diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index db605f8d7e..7a0a661430 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12588,6 +12588,50 @@ eval('class EvalAotReadonlySetPropertyBox implements EvalAotSetPropertyContract ); } +/// Verifies eval reflection exposes generated/AOT interface property-hook metadata. +#[test] +fn test_eval_reflection_exposes_aot_interface_property_hooks() { + let out = compile_and_run( + r#"hasProperty("name") ? "H:" : "h:"; +echo $ref->hasProperty("parentName") ? "P:" : "p:"; +$properties = $ref->getProperties(); +echo count($properties) . ":"; +$property = $ref->getProperty("name"); +$parent = $ref->getProperty("parentName"); +$direct = new ReflectionProperty("EvalAotReflectPropertyContract", "name"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +echo $property->getName() . ":"; +echo $property->getDeclaringClass()->getName() . ":"; +echo $property->getType()->getName() . ":"; +echo ($property->hasHooks() ? "hooks" : "plain") . ":"; +echo ($property->hasHook($getCase) ? "G" : "g") . ":"; +echo ($property->hasHook($setCase) ? "S" : "s") . ":"; +$hooks = $property->getHooks(); +echo count($hooks) . ":"; +echo $hooks["get"]->getName() . ":"; +echo $hooks["set"]->getName() . ":"; +echo ($property->getHook($getCase)->isAbstract() ? "A" : "a") . ":"; +echo ($direct->hasHook($setCase) ? "D" : "d") . ":"; +echo $parent->getDeclaringClass()->getName() . ":"; +echo ($parent->hasHook($getCase) ? "PG" : "pg") . ":"; +echo ($parent->hasHook($setCase) ? "bad" : "ps");'); +"#, + ); + assert_eq!( + out, + "H:P:2:name:EvalAotReflectPropertyContract:string:hooks:G:S:2:$name::get:$name::set:A:D:EvalAotReflectPropertyParent:PG:ps" + ); +} + /// Verifies eval `#[Override]` can target generated/AOT parent class methods. #[test] fn test_eval_declared_class_override_attribute_accepts_aot_parent_method() { From 5e2fc01de26da46bf1bbfc35a162fd3d4f5d66f5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 16:34:24 +0200 Subject: [PATCH 0873/1208] Validate eval classes against AOT abstract methods --- .../src/interpreter/statements.rs | 188 +++++++++++++++++- docs/php/eval.md | 3 + tests/codegen/eval.rs | 72 +++++++ 3 files changed, 262 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 5bf0a057aa..2ad119f0b6 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1371,6 +1371,7 @@ pub(in crate::interpreter) fn execute_class_decl_stmt( if !class.is_abstract() { validate_concrete_class_requirements(class, context)?; validate_concrete_class_builtin_interface_requirements(class, context)?; + validate_concrete_class_aot_parent_requirements(class, context, values)?; validate_concrete_class_aot_interface_requirements(class, context, values)?; } if context.define_class(class.clone()) { @@ -2645,6 +2646,14 @@ struct EvalAotInterfaceMethodRequirement { signature: Option, } +/// Abstract method requirement discovered from generated/AOT parent metadata. +struct EvalAotAbstractMethodRequirement { + owner: String, + is_static: bool, + visibility: EvalVisibility, + signature: Option, +} + /// Rejects builtin attributes that cannot target an eval-declared class. fn validate_eval_class_attribute_targets( attributes: &[EvalAttribute], @@ -3932,6 +3941,18 @@ fn validate_concrete_class_builtin_interface_requirements( Ok(()) } +/// Validates concrete class methods required by generated/AOT abstract parents. +fn validate_concrete_class_aot_parent_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !pending_class_aot_parent_abstract_method_requirements(class, context, values)?.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + /// Validates concrete class methods required by generated/AOT runtime interfaces. fn validate_concrete_class_aot_interface_requirements( class: &EvalClass, @@ -3979,6 +4000,25 @@ fn pending_class_abstract_property_requirements( Ok(requirements.into_values().collect()) } +/// Returns generated/AOT abstract parent methods the pending class has not concretized. +fn pending_class_aot_parent_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_method_requirements( + parent, + context, + values, + &mut requirements, + )?; + } + apply_class_aot_parent_abstract_method_requirements(class, context, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + /// Collects abstract method requirements from one declared eval class ancestry chain. fn collect_class_abstract_method_requirements( class_name: &str, @@ -3994,6 +4034,63 @@ fn collect_class_abstract_method_requirements( apply_class_abstract_method_requirements(class, requirements); } +/// Collects generated/AOT abstract method requirements through eval and AOT parents. +fn collect_aot_parent_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + if let Some(class) = context.class(class_name) { + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_method_requirements( + parent, + context, + values, + requirements, + )?; + } + apply_class_aot_parent_abstract_method_requirements(class, context, requirements)?; + return Ok(()); + } + if values.class_exists(class_name)? { + collect_native_aot_abstract_method_requirements( + class_name, + context, + values, + requirements, + )?; + } + Ok(()) +} + +/// Collects abstract methods exposed by one generated/AOT class reflection row. +fn collect_native_aot_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for method_name in eval_aot_method_names(class_name, values)? { + let Some(flags) = values.reflection_method_flags(class_name, &method_name)? else { + continue; + }; + let key = method_name.to_ascii_lowercase(); + if flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 { + requirements.remove(&key); + continue; + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + continue; + } + let requirement = + eval_aot_abstract_method_requirement(class_name, &method_name, flags, context, values)?; + requirements.insert(key, requirement); + } + Ok(()) +} + /// Collects abstract property requirements from one declared eval class ancestry chain. fn collect_class_abstract_property_requirements( class_name: &str, @@ -4024,6 +4121,34 @@ fn apply_class_abstract_method_requirements( } } +/// Applies one eval class's methods to the open AOT abstract-method requirement set. +fn apply_class_aot_parent_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for method in class.methods() { + let key = method.name().to_ascii_lowercase(); + let Some(requirement) = requirements.get(&key) else { + continue; + }; + if !class_method_satisfies_aot_abstract_parent_requirement( + method, + class.name(), + requirement, + Some(class), + context, + false, + ) { + return Err(EvalStatus::RuntimeFatal); + } + if !method.is_abstract() { + requirements.remove(&key); + } + } + Ok(()) +} + /// Applies one class's properties to the open abstract-property requirement set. fn apply_class_abstract_property_requirements( class: &EvalClass, @@ -4220,12 +4345,45 @@ fn eval_aot_interface_method_names( interface: &str, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let method_names = values.reflection_method_names(interface)?; + eval_aot_method_names(interface, values) +} + +/// Returns generated/AOT method names for one runtime class-like symbol. +fn eval_aot_method_names( + class_like: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method_names = values.reflection_method_names(class_like)?; let names = eval_runtime_string_array_to_vec(method_names, values)?; values.release(method_names)?; Ok(names) } +/// Builds one generated/AOT abstract parent method requirement from metadata. +fn eval_aot_abstract_method_requirement( + class_name: &str, + method_name: &str, + flags: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let owner = eval_aot_method_declaring_class(class_name, method_name, values)?; + let signature = eval_aot_method_signature_requirement( + class_name, + method_name, + is_static, + context, + values, + )?; + Ok(EvalAotAbstractMethodRequirement { + owner, + is_static, + visibility: eval_aot_method_visibility(flags), + signature, + }) +} + /// Builds one generated/AOT interface method requirement from reflection and signature metadata. fn eval_aot_interface_method_requirement( interface: &str, @@ -4473,6 +4631,34 @@ fn class_method_satisfies_aot_interface_requirement( }) } +/// Returns whether one method satisfies a generated/AOT abstract parent requirement. +fn class_method_satisfies_aot_abstract_parent_requirement( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalAotAbstractMethodRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + require_concrete: bool, +) -> bool { + if method.is_static() != requirement.is_static + || method_visibility_rank(method.visibility()) + < method_visibility_rank(requirement.visibility) + || (require_concrete && method.is_abstract()) + { + return false; + } + requirement.signature.as_ref().is_none_or(|signature| { + class_method_satisfies_interface_signature( + method, + method_owner, + signature, + &requirement.owner, + pending_class, + context, + ) + }) +} + /// Returns whether one class method can accept every call required by an interface method. fn class_method_satisfies_interface_signature( method: &EvalClassMethod, diff --git a/docs/php/eval.md b/docs/php/eval.md index decdd2dbcd..a429934947 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -189,6 +189,9 @@ with PHP-style parameter contravariance and return covariance for supported declared type metadata, including nullable, union, `mixed`, `self`, `parent`, `static`, class, and interface types, including generated/AOT parent method metadata retained by the eval bridge. +Concrete eval classes also enforce inherited generated/AOT abstract parent +method requirements when reflection/signature metadata is available, including +requirements carried through intermediate eval abstract classes. Abstract classes may defer missing interface methods and property contracts, but declared or inherited members that cover an interface contract are validated at declaration time. Generated/AOT interface method and property-hook contracts are diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7a0a661430..a2952bcdfc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12688,6 +12688,78 @@ eval('class EvalAotParentSignatureChild extends EvalAotParentSignatureBase { ); } +/// Verifies eval concrete classes can implement generated/AOT abstract parent methods. +#[test] +fn test_eval_declared_class_implements_aot_abstract_parent_method() { + let out = compile_and_run( + r#"label("ok");'); +"#, + ); + assert_eq!(out, "ok"); +} + +/// Verifies eval concrete classes must implement generated/AOT abstract parent methods. +#[test] +fn test_eval_declared_class_rejects_missing_aot_abstract_parent_method() { + let err = compile_and_run_expect_failure( + r#" Date: Sat, 27 Jun 2026 16:51:18 +0200 Subject: [PATCH 0874/1208] Validate eval classes against AOT abstract properties --- crates/elephc-magician/src/context.rs | 37 ++++ .../elephc-magician/src/ffi/native_methods.rs | 98 ++++++++-- crates/elephc-magician/src/ffi/tests.rs | 57 +++++- .../src/interpreter/statements.rs | 176 ++++++++++++++++++ docs/php/eval.md | 2 + src/codegen/lower_inst/builtins/eval.rs | 120 +++++++++++- tests/codegen/eval.rs | 106 +++++++++++ 7 files changed, 576 insertions(+), 20 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index cf503537f0..af3b49edd1 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -646,6 +646,7 @@ pub struct ElephcEvalContext { native_method_attributes: HashMap<(String, String), Vec>, native_constant_attributes: HashMap<(String, String), Vec>, native_interface_properties: HashMap>, + native_abstract_properties: HashMap>, native_property_types: HashMap<(String, String), EvalParameterType>, native_property_defaults: HashMap<(String, String), NativeCallableDefault>, native_property_attributes: HashMap<(String, String), Vec>, @@ -710,6 +711,7 @@ impl ElephcEvalContext { native_method_attributes: HashMap::new(), native_constant_attributes: HashMap::new(), native_interface_properties: HashMap::new(), + native_abstract_properties: HashMap::new(), native_property_types: HashMap::new(), native_property_defaults: HashMap::new(), native_property_attributes: HashMap::new(), @@ -775,6 +777,7 @@ impl ElephcEvalContext { native_method_attributes: HashMap::new(), native_constant_attributes: HashMap::new(), native_interface_properties: HashMap::new(), + native_abstract_properties: HashMap::new(), native_property_types: HashMap::new(), native_property_defaults: HashMap::new(), native_property_attributes: HashMap::new(), @@ -3026,6 +3029,40 @@ impl ElephcEvalContext { .unwrap_or_default() } + /// Defines generated AOT abstract class property-hook metadata for eval validation. + pub fn define_native_abstract_property_requirement( + &mut self, + class_name: &str, + declaring_class_name: &str, + property: EvalInterfaceProperty, + ) -> bool { + let key = normalize_class_name(class_name); + let owner = declaring_class_name.trim_start_matches('\\').to_string(); + if key.is_empty() || owner.is_empty() || property.name().is_empty() { + return false; + } + let requirements = self.native_abstract_properties.entry(key).or_default(); + if requirements + .iter() + .any(|(_, existing)| existing.name() == property.name()) + { + return false; + } + requirements.push((owner, property)); + true + } + + /// Returns generated AOT abstract class property-hook metadata by class name. + pub fn native_abstract_property_requirements( + &self, + class_name: &str, + ) -> Vec<(String, EvalInterfaceProperty)> { + self.native_abstract_properties + .get(&normalize_class_name(class_name)) + .cloned() + .unwrap_or_default() + } + /// Defines generated AOT property type metadata for eval reflection. pub fn define_native_property_type( &mut self, diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index eae79b9403..c62360bbc0 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -48,8 +48,8 @@ const NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; const NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; const NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; const NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; -const NATIVE_INTERFACE_PROPERTY_REQUIRES_GET: u64 = 1; -const NATIVE_INTERFACE_PROPERTY_REQUIRES_SET: u64 = 2; +const NATIVE_PROPERTY_REQUIRES_GET: u64 = 1; +const NATIVE_PROPERTY_REQUIRES_SET: u64 = 2; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; #[derive(Clone, Copy)] @@ -122,6 +122,34 @@ pub unsafe extern "C" fn __elephc_eval_register_native_interface_property( .unwrap_or(0) } +/// Registers one generated native PHP abstract class property contract in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a +/// readable `ClassName::DeclaringClass::propertyName` byte string, and +/// `type_spec_ptr` must point to a readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_abstract_property( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_abstract_property_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + flags, + ) + }) + .unwrap_or(0) +} + /// Registers one generated native PHP method parameter name in an eval context. /// /// # Safety @@ -1045,12 +1073,12 @@ unsafe fn register_native_interface_property_inner( return 0; }; let Some((interface_name, declaring_interface_name, property_name)) = - split_interface_property_key(&property_key) + split_three_part_property_key(&property_key) else { return 0; }; - let requires_get = flags & NATIVE_INTERFACE_PROPERTY_REQUIRES_GET != 0; - let requires_set = flags & NATIVE_INTERFACE_PROPERTY_REQUIRES_SET != 0; + let requires_get = flags & NATIVE_PROPERTY_REQUIRES_GET != 0; + let requires_set = flags & NATIVE_PROPERTY_REQUIRES_SET != 0; if !requires_get && !requires_set { return 0; } @@ -1070,6 +1098,54 @@ unsafe fn register_native_interface_property_inner( )) } +/// Runs native abstract property registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_abstract_property`; invalid handles, +/// names, flags, or type specs fail closed as `false`. +unsafe fn register_native_abstract_property_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, declaring_class_name, property_name)) = + split_three_part_property_key(&property_key) + else { + return 0; + }; + let requires_get = flags & NATIVE_PROPERTY_REQUIRES_GET != 0; + let requires_set = flags & NATIVE_PROPERTY_REQUIRES_SET != 0; + if !requires_get && !requires_set { + return 0; + } + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + let property = EvalInterfaceProperty::new(property_name, requires_get, requires_set) + .with_type(Some(property_type)); + i32::from(context.define_native_abstract_property_requirement( + class_name, + declaring_class_name, + property, + )) +} + /// Runs native method parameter registration after installing a panic boundary. /// /// # Safety @@ -2303,12 +2379,12 @@ fn split_property_key(property_key: &str) -> Option<(&str, &str)> { split_method_key(property_key) } -/// Splits `InterfaceName::DeclaringInterface::propertyName` interface-property metadata keys. -fn split_interface_property_key(property_key: &str) -> Option<(&str, &str, &str)> { +/// Splits `ClassLike::DeclaringClassLike::propertyName` property metadata keys. +fn split_three_part_property_key(property_key: &str) -> Option<(&str, &str, &str)> { let (owner_key, property_name) = property_key.rsplit_once("::")?; - let (interface_name, declaring_interface_name) = owner_key.rsplit_once("::")?; - (!interface_name.is_empty() - && !declaring_interface_name.is_empty() + let (class_like_name, declaring_class_like_name) = owner_key.rsplit_once("::")?; + (!class_like_name.is_empty() + && !declaring_class_like_name.is_empty() && !property_name.is_empty()) - .then_some((interface_name, declaring_interface_name, property_name)) + .then_some((class_like_name, declaring_class_like_name, property_name)) } diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index bdffcce00d..940d3726cf 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -35,8 +35,8 @@ const TEST_NATIVE_DEFAULT_BOOL: u64 = 1; const TEST_NATIVE_DEFAULT_INT: u64 = 2; const TEST_NATIVE_DEFAULT_FLOAT: u64 = 3; const TEST_NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; -const TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_GET: u64 = 1; -const TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_SET: u64 = 2; +const TEST_NATIVE_PROPERTY_REQUIRES_GET: u64 = 1; +const TEST_NATIVE_PROPERTY_REQUIRES_SET: u64 = 2; const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; @@ -1174,8 +1174,7 @@ fn register_native_interface_property_records_metadata() { property.len() as u64, property_type.as_ptr(), property_type.len() as u64, - TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_GET - | TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_SET, + TEST_NATIVE_PROPERTY_REQUIRES_GET | TEST_NATIVE_PROPERTY_REQUIRES_SET, ) }; let invalid_registered = unsafe { @@ -1185,7 +1184,7 @@ fn register_native_interface_property_records_metadata() { invalid_property.len() as u64, invalid_type.as_ptr(), invalid_type.len() as u64, - TEST_NATIVE_INTERFACE_PROPERTY_REQUIRES_GET, + TEST_NATIVE_PROPERTY_REQUIRES_GET, ) }; @@ -1207,6 +1206,54 @@ fn register_native_interface_property_records_metadata() { .all(|(_, property)| property.name() != "bad")); } +/// Verifies native AOT abstract class property contracts are available to eval validation. +#[test] +fn register_native_abstract_property_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownClass::KnownParent::name"; + let property_type = b"string"; + let invalid_property = b"KnownClass::KnownParent::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_abstract_property( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET | TEST_NATIVE_PROPERTY_REQUIRES_SET, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_abstract_property( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET, + ) + }; + + let requirements = ctx.native_abstract_property_requirements("knownclass"); + assert_eq!(registered, 1); + assert_eq!(requirements.len(), 1); + assert_eq!(requirements[0].0, "KnownParent"); + assert_eq!(requirements[0].1.name(), "name"); + assert!(requirements[0].1.requires_get()); + assert!(requirements[0].1.requires_set()); + assert_eq!( + requirements[0].1.property_type().map(|ty| ty.variants()), + Some(&[EvalParameterTypeVariant::String][..]) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_abstract_property_requirements("KnownClass") + .iter() + .all(|(_, property)| property.name() != "bad")); +} + /// Verifies native AOT property default metadata is available to eval reflection. #[test] fn register_native_property_default_records_metadata() { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 2ad119f0b6..03452e584d 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2654,6 +2654,12 @@ struct EvalAotAbstractMethodRequirement { signature: Option, } +/// Abstract property requirement discovered from generated/AOT parent metadata. +struct EvalAotAbstractPropertyRequirement { + owner: String, + property: EvalClassProperty, +} + /// Rejects builtin attributes that cannot target an eval-declared class. fn validate_eval_class_attribute_targets( attributes: &[EvalAttribute], @@ -3950,6 +3956,10 @@ fn validate_concrete_class_aot_parent_requirements( if !pending_class_aot_parent_abstract_method_requirements(class, context, values)?.is_empty() { return Err(EvalStatus::RuntimeFatal); } + if !pending_class_aot_parent_abstract_property_requirements(class, context, values)?.is_empty() + { + return Err(EvalStatus::RuntimeFatal); + } Ok(()) } @@ -4019,6 +4029,25 @@ fn pending_class_aot_parent_abstract_method_requirements( Ok(requirements.into_values().collect()) } +/// Returns generated/AOT abstract parent properties the pending class has not concretized. +fn pending_class_aot_parent_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_property_requirements( + parent, + context, + values, + &mut requirements, + )?; + } + apply_class_aot_parent_abstract_property_requirements(class, context, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + /// Collects abstract method requirements from one declared eval class ancestry chain. fn collect_class_abstract_method_requirements( class_name: &str, @@ -4091,6 +4120,71 @@ fn collect_native_aot_abstract_method_requirements( Ok(()) } +/// Collects generated/AOT abstract property requirements through eval and AOT parents. +fn collect_aot_parent_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + if let Some(class) = context.class(class_name) { + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_property_requirements( + parent, + context, + values, + requirements, + )?; + } + apply_class_aot_parent_abstract_property_requirements(class, context, requirements)?; + return Ok(()); + } + if values.class_exists(class_name)? { + collect_native_aot_abstract_property_requirements( + class_name, + context, + values, + requirements, + )?; + } + Ok(()) +} + +/// Collects abstract properties exposed by one generated/AOT class metadata row. +fn collect_native_aot_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for (owner, property) in context.native_abstract_property_requirements(class_name) { + let Some(flags) = values.reflection_property_flags(class_name, property.name())? else { + continue; + }; + if flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 + || flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 + { + continue; + } + let visibility = eval_aot_property_visibility(flags); + let write_visibility = eval_aot_property_write_visibility(flags, visibility); + let set_visibility = (write_visibility != visibility).then_some(write_visibility); + let requirement = EvalClassProperty::with_visibility(property.name(), visibility, None) + .with_type(property.property_type().cloned()) + .with_set_visibility(set_visibility) + .with_abstract_hook_contract(property.requires_get(), property.requires_set()); + requirements.insert( + property.name().to_string(), + EvalAotAbstractPropertyRequirement { + owner, + property: requirement, + }, + ); + } + Ok(()) +} + /// Collects abstract property requirements from one declared eval class ancestry chain. fn collect_class_abstract_property_requirements( class_name: &str, @@ -4149,6 +4243,49 @@ fn apply_class_aot_parent_abstract_method_requirements( Ok(()) } +/// Applies one eval class's properties to the open AOT abstract-property requirement set. +fn apply_class_aot_parent_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for property in class.properties() { + let key = property.name().to_string(); + let Some(requirement) = requirements.get(&key).map(|requirement| { + EvalAotAbstractPropertyRequirement { + owner: requirement.owner.clone(), + property: requirement.property.clone(), + } + }) else { + continue; + }; + if !class_property_satisfies_aot_abstract_parent_requirement( + property, + class.name(), + &requirement, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_abstract() { + requirements.insert( + key, + EvalAotAbstractPropertyRequirement { + owner: class.name().to_string(), + property: merge_abstract_property_contracts( + &requirement.property, + property, + ), + }, + ); + } else { + requirements.remove(&key); + } + } + Ok(()) +} + /// Applies one class's properties to the open abstract-property requirement set. fn apply_class_abstract_property_requirements( class: &EvalClass, @@ -4226,6 +4363,45 @@ fn class_property_satisfies_abstract_contract( requirement.requires_get_hook() } +/// Returns whether one property satisfies a generated/AOT abstract parent contract. +fn class_property_satisfies_aot_abstract_parent_requirement( + property: &EvalClassProperty, + property_owner: &str, + requirement: &EvalAotAbstractPropertyRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + let required = &requirement.property; + if property.is_static() != required.is_static() + || !property_contract_visibility_allows(required, property) + || !property_type_signature_matches( + property.property_type(), + property_owner, + required.property_type(), + &requirement.owner, + pending_class, + context, + ) + { + return false; + } + if property.is_abstract() { + return (!required.requires_get_hook() || property.requires_get_hook()) + && (!required.requires_set_hook() + || (property.requires_set_hook() + && property_contract_write_visibility_allows(required, property))); + } + if required.requires_get_hook() && !class_property_supports_interface_get(property) { + return false; + } + if required.requires_set_hook() { + return required.set_visibility() != Some(EvalVisibility::Private) + && property_contract_write_visibility_allows(required, property) + && class_property_supports_interface_set(property); + } + required.requires_get_hook() +} + /// Returns a comparable rank where larger means less restrictive property visibility. fn property_visibility_rank(visibility: EvalVisibility) -> u8 { match visibility { diff --git a/docs/php/eval.md b/docs/php/eval.md index a429934947..c9c8ee74f7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -192,6 +192,8 @@ metadata retained by the eval bridge. Concrete eval classes also enforce inherited generated/AOT abstract parent method requirements when reflection/signature metadata is available, including requirements carried through intermediate eval abstract classes. +The same applies to generated/AOT abstract parent property-hook contracts when +the bridge retains the class property contract metadata. Abstract classes may defer missing interface methods and property contracts, but declared or inherited members that cover an interface contract are validated at declaration time. Generated/AOT interface method and property-hook contracts are diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 195968f8ef..96b292fa1f 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -67,8 +67,8 @@ const NATIVE_DEFAULT_BOOL: i64 = 1; const NATIVE_DEFAULT_INT: i64 = 2; const NATIVE_DEFAULT_FLOAT: i64 = 3; const NATIVE_DEFAULT_EMPTY_ARRAY: i64 = 4; -const NATIVE_INTERFACE_PROPERTY_REQUIRES_GET: i64 = 1; -const NATIVE_INTERFACE_PROPERTY_REQUIRES_SET: i64 = 2; +const NATIVE_PROPERTY_REQUIRES_GET: i64 = 1; +const NATIVE_PROPERTY_REQUIRES_SET: i64 = 2; const NATIVE_MEMBER_ATTRIBUTE_METHOD: u8 = 0; const NATIVE_MEMBER_ATTRIBUTE_PROPERTY: u8 = 1; const NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT: u8 = 2; @@ -154,6 +154,16 @@ struct EvalNativeInterfacePropertyRegistration { requires_set: bool, } +/// A module-local abstract class property contract that can be registered with the eval context. +struct EvalNativeAbstractPropertyRegistration { + class_name: String, + declaring_class_name: String, + property_name: String, + type_spec: String, + requires_get: bool, + requires_set: bool, +} + /// A module-local property default that can be registered with the eval context. struct EvalNativePropertyDefaultRegistration { class_name: String, @@ -1347,6 +1357,9 @@ fn register_eval_native_method_signatures(ctx: &mut FunctionContext<'_>, context for registration in eval_native_property_type_registrations(ctx) { register_eval_native_property_type(ctx, context_offset, ®istration); } + for registration in eval_native_abstract_property_registrations(ctx) { + register_eval_native_abstract_property(ctx, context_offset, ®istration); + } for registration in eval_native_interface_property_registrations(ctx) { register_eval_native_interface_property(ctx, context_offset, ®istration); } @@ -1522,6 +1535,53 @@ fn eval_native_interface_property_registrations( registrations } +/// Collects AOT abstract class property contracts that eval can validate at declaration time. +fn eval_native_abstract_property_registrations( + ctx: &FunctionContext<'_>, +) -> Vec { + let mut registrations = Vec::new(); + let mut classes = ctx.module.class_infos.iter().collect::>(); + classes.sort_by_key(|(_, class_info)| class_info.class_id); + for (class_name, class_info) in classes { + let mut property_names = class_info.abstract_property_hooks.keys().collect::>(); + property_names.sort(); + for property_name in property_names { + let Some(contract) = class_info.abstract_property_hooks.get(property_name) else { + continue; + }; + let Some(registration) = + eval_native_abstract_property_registration(class_name, property_name, contract) + else { + continue; + }; + registrations.push(registration); + } + } + registrations +} + +/// Converts one static abstract class property contract into eval-native metadata. +fn eval_native_abstract_property_registration( + class_name: &str, + property_name: &str, + contract: &PropertyHookContract, +) -> Option { + let requires_get = contract.get_type.is_some(); + let requires_set = contract.set_type.is_some(); + if !requires_get && !requires_set { + return None; + } + let type_spec = eval_native_interface_property_type_spec(contract)?; + Some(EvalNativeAbstractPropertyRegistration { + class_name: class_name.to_string(), + declaring_class_name: contract.declaring_type.clone(), + property_name: property_name.to_string(), + type_spec, + requires_get, + requires_set, + }) +} + /// Converts one static interface property contract into eval-native metadata. fn eval_native_interface_property_registration( interface_name: &str, @@ -3332,10 +3392,10 @@ fn register_eval_native_interface_property( ); let mut flags = 0; if registration.requires_get { - flags |= NATIVE_INTERFACE_PROPERTY_REQUIRES_GET; + flags |= NATIVE_PROPERTY_REQUIRES_GET; } if registration.requires_set { - flags |= NATIVE_INTERFACE_PROPERTY_REQUIRES_SET; + flags |= NATIVE_PROPERTY_REQUIRES_SET; } abi::emit_load_int_immediate( ctx.emitter, @@ -3349,6 +3409,58 @@ fn register_eval_native_interface_property( abi::emit_call_label(ctx.emitter, &symbol); } +/// Emits one native abstract-property metadata registration call into the eval context. +fn register_eval_native_abstract_property( + ctx: &mut FunctionContext<'_>, + context_offset: usize, + registration: &EvalNativeAbstractPropertyRegistration, +) { + load_eval_context_local_to_arg(ctx, context_offset, 0); + let property_key = format!( + "{}::{}::{}", + registration.class_name, registration.declaring_class_name, registration.property_name + ); + let (property_key_label, property_key_len) = ctx.data.add_string(property_key.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + &property_key_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + property_key_len as i64, + ); + let (type_label, type_len) = ctx.data.add_string(registration.type_spec.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + &type_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + type_len as i64, + ); + let mut flags = 0; + if registration.requires_get { + flags |= NATIVE_PROPERTY_REQUIRES_GET; + } + if registration.requires_set { + flags |= NATIVE_PROPERTY_REQUIRES_SET; + } + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 5), + flags, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_register_native_abstract_property"); + abi::emit_call_label(ctx.emitter, &symbol); +} + /// Emits one native property-default metadata registration call into the eval context. fn register_eval_native_property_default( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a2952bcdfc..cfb31b6f8f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12843,6 +12843,112 @@ eval('class EvalAotTypedPropertyChild extends EvalAotTypedPropertyBase { ); } +/// Verifies eval concrete classes can implement generated/AOT abstract parent properties. +#[test] +fn test_eval_declared_class_implements_aot_abstract_parent_property() { + let out = compile_and_run( + r#"id;'); +"#, + ); + assert_eq!(out, "7"); +} + +/// Verifies eval concrete classes must implement generated/AOT abstract parent properties. +#[test] +fn test_eval_declared_class_rejects_missing_aot_abstract_parent_property() { + let err = compile_and_run_expect_failure( + r#"id = $id; } +} +echo (new EvalAotAbstractPropertyReadonlyChild(9))->id;'); +"#, + ); + assert_eq!(out, "9"); + + let err = compile_and_run_expect_failure( + r#"id = $id; } +}'); +"#, + ); + assert!( + err.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {err}" + ); +} + /// Verifies eval rejects global builtin attributes on unsupported OOP targets. #[test] fn test_eval_declared_builtin_attribute_target_validation() { From f56a20fdab501946a99c8da4a171732061311eab Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 17:19:16 +0200 Subject: [PATCH 0875/1208] Expose inherited AOT interfaces in eval relations --- .../interpreter/builtins/class_metadata.rs | 73 ++++++++- .../src/interpreter/builtins/symbols.rs | 120 +++++++++++++- .../src/interpreter/reflection.rs | 151 +++++++++++++++--- .../tests/builtins_class_metadata.rs | 4 +- tests/codegen/eval.rs | 91 ++++++++++- 5 files changed, 397 insertions(+), 42 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index 83c10b2e0e..3289ff3b3f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -67,7 +67,9 @@ pub(in crate::interpreter) fn eval_class_relation_target_result( if context.class(&target).is_some() { return match name { "class_implements" => { - eval_class_relation_names_result(context.class_interface_names(&target), values) + let names = + eval_class_relation_eval_class_interface_names(&target, context, values)?; + eval_class_relation_names_result(names, values) } "class_parents" => { eval_class_relation_names_result(context.class_parent_names(&target), values) @@ -81,7 +83,9 @@ pub(in crate::interpreter) fn eval_class_relation_target_result( if context.interface(&target).is_some() { return match name { "class_implements" => { - eval_class_relation_names_result(context.interface_parent_names(&target), values) + let names = + eval_class_relation_eval_interface_parent_names(&target, context, values)?; + eval_class_relation_names_result(names, values) } "class_parents" | "class_uses" => values.assoc_new(0), _ => Err(EvalStatus::RuntimeFatal), @@ -114,10 +118,19 @@ fn eval_runtime_class_interface_names_result( class_name: &str, values: &mut impl RuntimeValueOps, ) -> Result { + let names = eval_runtime_class_interface_names(class_name, values)?; + eval_class_relation_names_result(names, values) +} + +/// Returns generated/AOT interface names visible for one class-like symbol. +fn eval_runtime_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { let names_array = values.reflection_class_interface_names(class_name)?; let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; values.release(names_array)?; - eval_class_relation_names_result(names, values) + Ok(names) } /// Builds `class_uses()` data for generated/AOT direct trait-use metadata. @@ -150,6 +163,60 @@ fn eval_runtime_class_parent_names( names } +/// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. +fn eval_class_relation_eval_class_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_runtime_class_interface_names(&parent, values)? { + eval_class_relation_push_unique_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_class_relation_push_unique_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_class_relation_push_unique_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus inherited generated/AOT interface parents. +fn eval_class_relation_eval_interface_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_class_relation_push_unique_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_class_relation_push_unique_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +fn eval_class_relation_push_unique_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + /// Copies a runtime string array into Rust-owned class/interface names. fn eval_class_relation_runtime_string_array_to_vec( array: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index a13c4d4d76..f9d880d6fa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -471,14 +471,21 @@ pub(in crate::interpreter) fn eval_is_a_relation_result( .resolve_class_like_name(&source_class) .unwrap_or_else(|| source_class.trim_start_matches('\\').to_string()); if context.class(&resolved_source_class).is_some() { - context.class_is_a(&resolved_source_class, &resolved_target_class, exclude_self) + eval_class_string_is_a( + &resolved_source_class, + &resolved_target_class, + exclude_self, + context, + values, + )? } else if context.interface(&resolved_source_class).is_some() { eval_interface_string_is_a( &resolved_source_class, &resolved_target_class, exclude_self, context, - ) + values, + )? } else if context.trait_decl(&resolved_source_class).is_some() { !exclude_self && eval_class_like_name_matches(&resolved_source_class, &resolved_target_class) @@ -499,14 +506,111 @@ fn eval_interface_string_is_a( target_class: &str, exclude_self: bool, context: &ElephcEvalContext, -) -> bool { + values: &mut impl RuntimeValueOps, +) -> Result { if !exclude_self && eval_class_like_name_matches(source_class, target_class) { - return true; + return Ok(true); } - context - .interface_parent_names(source_class) + Ok(eval_interface_runtime_parent_names(source_class, context, values)? .iter() - .any(|parent| eval_class_like_name_matches(parent, target_class)) + .any(|parent| eval_class_like_name_matches(parent, target_class))) +} + +/// Returns whether an eval class-string source satisfies a class-like target. +fn eval_class_string_is_a( + source_class: &str, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.class_is_a(source_class, target_class, exclude_self) { + return Ok(true); + } + Ok(eval_class_runtime_interface_names(source_class, context, values)? + .iter() + .any(|interface| eval_class_like_name_matches(interface, target_class))) +} + +/// Returns eval class interfaces plus generated/AOT inherited interface names. +fn eval_class_runtime_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_runtime_class_interface_names(&parent, values)? { + eval_push_unique_class_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus generated/AOT inherited interface names. +fn eval_interface_runtime_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns generated/AOT interface names visible for one class-like symbol. +fn eval_runtime_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let names_array = values.reflection_class_interface_names(class_name)?; + let names = eval_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Copies a runtime string array into Rust-owned names. +fn eval_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + let bytes = values.string_bytes(value)?; + result.push(String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?); + } + Ok(result) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +fn eval_push_unique_class_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } } /// Returns whether two class-like names match PHP's case-insensitive class-name rules. @@ -527,7 +631,7 @@ pub(in crate::interpreter) fn dynamic_object_is_a( let Some(class) = context.dynamic_object_class(identity) else { return Ok(None); }; - if context.class_is_a(class.name(), target_class, exclude_self) { + if eval_class_string_is_a(class.name(), target_class, exclude_self, context, values)? { return Ok(Some(true)); } if context.class_native_parent_name(class.name()).is_some() { diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 467134245b..2de4e26b63 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -338,7 +338,12 @@ pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( ); } let result = if eval_reflection_class_like_exists(&reflected_name, context) { - eval_reflection_class_implements_interface_name(&reflected_name, &interface_name, context) + eval_reflection_class_implements_interface_name( + &reflected_name, + &interface_name, + context, + values, + )? } else if eval_runtime_interface_exists(&reflected_name, values)? { eval_reflection_same_class_like_name(&reflected_name, &interface_name) } else { @@ -382,7 +387,12 @@ pub(in crate::interpreter) fn eval_reflection_class_is_subclass_of_result( ); } let result = if eval_reflection_class_like_exists(&reflected_name, context) { - eval_reflection_class_is_subclass_of_name(&reflected_name, &target_name, context) + eval_reflection_class_is_subclass_of_name( + &reflected_name, + &target_name, + context, + values, + )? } else { let reflected_class = values.string(&reflected_name)?; let result = values.object_is_a(reflected_class, &target_name, true)?; @@ -523,7 +533,9 @@ pub(in crate::interpreter) fn eval_reflection_class_basic_metadata_result( } "getinterfacenames" => { eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_string_array_result(&metadata.interface_names, values).map(Some) + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + eval_reflection_string_array_result(&interface_names, values).map(Some) } "gettraitnames" => { eval_reflection_bind_no_args(evaluated_args)?; @@ -893,7 +905,7 @@ pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( let names = if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { if relation_kind == "interfaces" { - metadata.interface_names + eval_reflection_eval_metadata_interface_names(&metadata, context, values)? } else { metadata.trait_names } @@ -2835,11 +2847,13 @@ fn eval_reflection_class_owner_object_result( ) .map(Some); }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; eval_reflection_owner_object( owner_kind, &metadata.resolved_name, &metadata.attributes, - &metadata.interface_names, + &interface_names, &metadata.trait_names, &metadata.method_names, &metadata.property_names, @@ -5198,11 +5212,13 @@ fn eval_reflection_full_class_object_result( values, ); }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, &metadata.resolved_name, &metadata.attributes, - &metadata.interface_names, + &interface_names, &metadata.trait_names, &metadata.method_names, &metadata.property_names, @@ -5259,11 +5275,13 @@ fn eval_reflection_shallow_class_object_result( values, ); }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; eval_reflection_owner_object_with_members( EVAL_REFLECTION_OWNER_CLASS, &metadata.resolved_name, &metadata.attributes, - &metadata.interface_names, + &interface_names, &metadata.trait_names, &[], &[], @@ -6036,6 +6054,75 @@ fn eval_reflection_aot_class_interface_names( Ok(names) } +/// Returns eval metadata interface names expanded with generated/AOT ancestors. +fn eval_reflection_eval_metadata_interface_names( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(&metadata.resolved_name) || context.has_enum(&metadata.resolved_name) { + eval_reflection_eval_class_interface_names(&metadata.resolved_name, context, values) + } else if context.has_interface(&metadata.resolved_name) { + eval_reflection_eval_interface_parent_names(&metadata.resolved_name, context, values) + } else { + Ok(metadata.interface_names.clone()) + } +} + +/// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. +fn eval_reflection_eval_class_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_reflection_aot_class_interface_names(&parent, values)? { + eval_reflection_push_unique_class_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_reflection_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_reflection_aot_class_interface_names(&name, values)? { + eval_reflection_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus inherited generated/AOT interface parents. +fn eval_reflection_eval_interface_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_reflection_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_reflection_aot_class_interface_names(&name, values)? { + eval_reflection_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +fn eval_reflection_push_unique_class_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + /// Returns generated AOT trait names for one reflected class-like symbol. fn eval_reflection_aot_class_trait_names( class_name: &str, @@ -6463,7 +6550,9 @@ fn eval_reflection_class_to_string_metadata( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - if let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) { + if let Some(mut metadata) = eval_reflection_class_like_attributes(reflected_name, context) { + metadata.interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; return Ok(Some(metadata)); } let runtime_class_name = reflected_name.trim_start_matches('\\'); @@ -7111,21 +7200,26 @@ fn eval_reflection_class_implements_interface_name( reflected_name: &str, interface_name: &str, context: &ElephcEvalContext, -) -> bool { + values: &mut impl RuntimeValueOps, +) -> Result { if context.has_interface(reflected_name) { - return eval_reflection_same_class_like_name(reflected_name, interface_name) - || context - .interface_parent_names(reflected_name) - .iter() - .any(|parent| eval_reflection_same_class_like_name(parent, interface_name)); + if eval_reflection_same_class_like_name(reflected_name, interface_name) { + return Ok(true); + } + return Ok(eval_reflection_eval_interface_parent_names( + reflected_name, + context, + values, + )? + .iter() + .any(|parent| eval_reflection_same_class_like_name(parent, interface_name))); } if context.has_class(reflected_name) || context.has_enum(reflected_name) { - return context - .class_interface_names(reflected_name) + return Ok(eval_reflection_eval_class_interface_names(reflected_name, context, values)? .iter() - .any(|interface| eval_reflection_same_class_like_name(interface, interface_name)); + .any(|interface| eval_reflection_same_class_like_name(interface, interface_name))); } - false + Ok(false) } /// Returns true when reflected eval metadata is a subclass or subinterface of a target. @@ -7133,17 +7227,26 @@ fn eval_reflection_class_is_subclass_of_name( reflected_name: &str, target_name: &str, context: &ElephcEvalContext, -) -> bool { + values: &mut impl RuntimeValueOps, +) -> Result { if context.has_interface(reflected_name) { - return context - .interface_parent_names(reflected_name) + return Ok(eval_reflection_eval_interface_parent_names( + reflected_name, + context, + values, + )? .iter() - .any(|parent| eval_reflection_same_class_like_name(parent, target_name)); + .any(|parent| eval_reflection_same_class_like_name(parent, target_name))); } if context.has_class(reflected_name) || context.has_enum(reflected_name) { - return context.class_is_a(reflected_name, target_name, true); + if context.class_is_a(reflected_name, target_name, true) { + return Ok(true); + } + return Ok(eval_reflection_eval_class_interface_names(reflected_name, context, values)? + .iter() + .any(|interface| eval_reflection_same_class_like_name(interface, target_name))); } - false + Ok(false) } /// Returns true when two PHP class-like names match case-insensitively. diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs index 8f04c3d7df..b969288960 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs @@ -64,11 +64,13 @@ $object = new EvalMetaChild(); $implements = class_implements($object); echo count($implements); echo ":"; echo $implements["KnownInterface"]; echo ":"; +echo $implements["Traversable"]; echo ":"; $parents = class_parents("EvalMetaChild"); echo count($parents); echo ":"; echo $parents["EvalMetaBase"]; echo ":"; $call = call_user_func("class_implements", "EvalMetaChild"); echo $call["KnownInterface"]; echo ":"; +echo $call["Traversable"]; echo ":"; $named = call_user_func_array("class_parents", ["object_or_class" => $object]); echo $named["EvalMetaBase"]; return true;"#, @@ -81,7 +83,7 @@ return true;"#, assert_eq!( values.output, - "1:KnownInterface:1:EvalMetaBase:KnownInterface:EvalMetaBase" + "2:KnownInterface:Traversable:1:EvalMetaBase:KnownInterface:Traversable:EvalMetaBase" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cfb31b6f8f..eb8444bf37 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -14034,6 +14034,85 @@ echo $ref->implementsInterface("Iterator") ? "I" : "i";'); assert_eq!(out.stdout, "CBi"); } +/// Verifies eval-declared children expose interfaces inherited from AOT parents. +#[test] +fn test_eval_declared_class_reflects_aot_parent_interfaces() { + let out = compile_and_run_capture( + r#"getInterfaceNames(); +sort($names); +echo implode(",", $names) . ":"; +$objects = $ref->getInterfaces(); +ksort($objects); +echo implode(",", array_keys($objects)) . ":"; +echo $ref->implementsInterface("EvalAotParentMarkerChild") ? "C" : "c"; +echo $ref->implementsInterface("evalaotparentmarkerbase") ? "B" : "b"; +echo $ref->isSubclassOf("EvalAotParentMarkerChild") ? "S" : "s"; +echo is_subclass_of("EvalAotParentMarkerLeaf", "EvalAotParentMarkerChild") ? "U" : "u"; +$box = new EvalAotParentMarkerLeaf(); +echo $box instanceof EvalAotParentMarkerChild ? "I" : "i";'); +$box = new EvalAotParentMarkerLeaf(); +echo ":"; +echo $box instanceof EvalAotParentMarkerChild ? "N" : "n"; +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotParentMarkerBase,EvalAotParentMarkerChild:EvalAotParentMarkerBase,EvalAotParentMarkerChild:EvalAotParentMarkerBase,EvalAotParentMarkerChild:CBSUI:N" + ); +} + +/// Verifies eval-declared interfaces expose inherited AOT parent interfaces. +#[test] +fn test_eval_declared_interface_reflects_aot_parent_interfaces() { + let out = compile_and_run_capture( + r#"getInterfaceNames(); +sort($names); +echo implode(",", $names) . ":"; +echo (new ReflectionClass("EvalAotInterfaceMarkerLeaf"))->implementsInterface("EvalAotInterfaceMarkerBase") ? "I" : "i"; +echo (new ReflectionClass("EvalAotInterfaceMarkerBox"))->implementsInterface("evalaotinterfacemarkerbase") ? "C" : "c"; +echo (new ReflectionClass("EvalAotInterfaceMarkerLeaf"))->isSubclassOf("EvalAotInterfaceMarkerBase") ? "S" : "s"; +echo is_subclass_of("EvalAotInterfaceMarkerLeaf", "EvalAotInterfaceMarkerBase") ? "U" : "u"; +$box = new EvalAotInterfaceMarkerBox(); +echo $box instanceof EvalAotInterfaceMarkerBase ? "N" : "n"; +echo is_a($box, "EvalAotInterfaceMarkerBase") ? "A" : "a";'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotInterfaceMarkerBase,EvalAotInterfaceMarkerChild:EvalAotInterfaceMarkerBase,EvalAotInterfaceMarkerChild,EvalAotInterfaceMarkerLeaf:EvalAotInterfaceMarkerBase,EvalAotInterfaceMarkerChild,EvalAotInterfaceMarkerLeaf:ICSUNA" + ); +} + /// Verifies eval `ReflectionClass::implementsInterface()` throws ReflectionException /// for missing or non-interface argument names. #[test] @@ -19138,14 +19217,14 @@ interface EvalIterableIface extends Iterator {} trait EvalIterableTrait {} enum EvalIterableEnum { case Ready; } class EvalIterableIterator implements Iterator { - public function current() { return null; } - public function key() { return null; } - public function next() {} - public function valid() { return false; } - public function rewind() {} + public function current(): mixed { return null; } + public function key(): mixed { return null; } + public function next(): void {} + public function valid(): bool { return false; } + public function rewind(): void {} } class EvalIterableAggregate implements IteratorAggregate { - public function getIterator() { return $this; } + public function getIterator(): Traversable { return new ArrayIterator([]); } } echo (new ReflectionClass("EvalIterablePlain"))->isIterable() ? "P" : "p"; $iter = new ReflectionClass("EvalIterableIterator"); From b3b4e071279d08ddb82b6807716dde796b4b22e3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 17:45:51 +0200 Subject: [PATCH 0876/1208] Reflect eval iterable status through AOT parents --- .../src/interpreter/reflection.rs | 51 +++++++++++++++---- tests/codegen/eval.rs | 17 +++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 2de4e26b63..7d62081499 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -621,12 +621,15 @@ pub(in crate::interpreter) fn eval_reflection_class_basic_metadata_result( evaluated_args, values, ), - "isiterable" | "isiterateable" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_ITERABLE, - evaluated_args, - values, - ), + "isiterable" | "isiterateable" => { + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_class_flag_result( + flags, + EVAL_REFLECTION_CLASS_FLAG_ITERABLE, + evaluated_args, + values, + ) + } "isinternal" => eval_reflection_class_flag_result( metadata.flags, EVAL_REFLECTION_CLASS_FLAG_INTERNAL, @@ -2849,6 +2852,7 @@ fn eval_reflection_class_owner_object_result( }; let interface_names = eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; eval_reflection_owner_object( owner_kind, &metadata.resolved_name, @@ -2863,7 +2867,7 @@ fn eval_reflection_class_owner_object_result( None, None, None, - metadata.flags, + flags, metadata.modifiers, 0, None, @@ -5214,6 +5218,7 @@ fn eval_reflection_full_class_object_result( }; let interface_names = eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; eval_reflection_owner_object( EVAL_REFLECTION_OWNER_CLASS, &metadata.resolved_name, @@ -5228,7 +5233,7 @@ fn eval_reflection_full_class_object_result( None, None, None, - metadata.flags, + flags, metadata.modifiers, 0, None, @@ -5277,6 +5282,7 @@ fn eval_reflection_shallow_class_object_result( }; let interface_names = eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; eval_reflection_owner_object_with_members( EVAL_REFLECTION_OWNER_CLASS, &metadata.resolved_name, @@ -5291,7 +5297,7 @@ fn eval_reflection_shallow_class_object_result( None, None, None, - metadata.flags, + flags, metadata.modifiers, 0, None, @@ -6069,6 +6075,25 @@ fn eval_reflection_eval_metadata_interface_names( } } +/// Returns eval metadata flags corrected for generated/AOT inherited interfaces. +fn eval_reflection_eval_metadata_flags( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut flags = metadata.flags; + if flags & EVAL_REFLECTION_CLASS_FLAG_ITERABLE == 0 + && flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT == 0 + && context.has_class(&metadata.resolved_name) + && eval_reflection_interface_names_include_iterable( + &eval_reflection_eval_class_interface_names(&metadata.resolved_name, context, values)?, + ) + { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } + Ok(flags) +} + /// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. fn eval_reflection_eval_class_interface_names( class_name: &str, @@ -6112,6 +6137,13 @@ fn eval_reflection_eval_interface_parent_names( Ok(names) } +/// Returns whether one interface list includes PHP iterable marker interfaces. +fn eval_reflection_interface_names_include_iterable(interface_names: &[String]) -> bool { + interface_names.iter().any(|name| { + name.eq_ignore_ascii_case("Iterator") || name.eq_ignore_ascii_case("IteratorAggregate") + }) +} + /// Appends one class-like name while preserving PHP's case-insensitive uniqueness. fn eval_reflection_push_unique_class_name( name: String, @@ -6553,6 +6585,7 @@ fn eval_reflection_class_to_string_metadata( if let Some(mut metadata) = eval_reflection_class_like_attributes(reflected_name, context) { metadata.interface_names = eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + metadata.flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; return Ok(Some(metadata)); } let runtime_class_name = reflected_name.trim_start_matches('\\'); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eb8444bf37..0eef4350af 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19243,6 +19243,23 @@ echo (new ReflectionClass("EvalIterableTrait"))->isIterable() ? "H" : "h";'); assert_eq!(out, "pIAGbftRseh"); } +/// Verifies eval ReflectionClass::isIterable sees interfaces inherited from AOT parents. +#[test] +fn test_eval_reflection_class_iterable_inherits_aot_parent_interface() { + let out = compile_and_run( + r#"isIterable() ? "R" : "r"; +$box = new EvalAotIterableChild(); +echo is_iterable($box) ? "I" : "i";'); +"#, + ); + assert_eq!(out, "RI"); +} + /// Verifies eval ReflectionClass origin predicates distinguish eval symbols from built-ins. #[test] fn test_eval_reflection_class_internal_user_defined_predicates() { From 6c27b592244927796144db4ee6607f06d67ff685 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 17:56:08 +0200 Subject: [PATCH 0877/1208] Reflect eval method prototypes through AOT interfaces --- .../src/interpreter/reflection.rs | 61 +++++++++++++++++-- tests/codegen/eval.rs | 39 ++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 7d62081499..e483570082 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -9797,14 +9797,30 @@ fn eval_reflection_method_prototype_target( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { if context.has_class(declaring_class) || context.has_enum(declaring_class) { - return Ok(eval_reflection_parent_method_prototype_target( + if let Some(prototype) = + eval_reflection_parent_method_prototype_target(declaring_class, method_name, context) + { + return Ok(Some(prototype)); + } + if let Some(prototype) = eval_reflection_eval_aot_parent_method_prototype_target( declaring_class, method_name, context, - ) - .or_else(|| { + values, + )? { + return Ok(Some(prototype)); + } + if let Some(prototype) = eval_reflection_interface_method_prototype_target(declaring_class, method_name, context) - })); + { + return Ok(Some(prototype)); + } + return eval_reflection_aot_interface_method_prototype_target_for_eval( + declaring_class, + method_name, + context, + values, + ); } eval_reflection_aot_method_prototype_target(declaring_class, method_name, values) } @@ -9910,6 +9926,19 @@ fn eval_reflection_parent_method_prototype_target( None } +/// Finds the nearest generated/AOT parent-class method prototype for an eval method. +fn eval_reflection_eval_aot_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent_class) = context.class_native_parent_name(declaring_class) else { + return Ok(None); + }; + eval_reflection_aot_method_candidate(&parent_class, method_name, false, values) +} + /// Finds the interface method prototype for an eval-declared class method. fn eval_reflection_interface_method_prototype_target( declaring_class: &str, @@ -9930,6 +9959,30 @@ fn eval_reflection_interface_method_prototype_target( None } +/// Finds an AOT interface method prototype for an eval-declared class method. +fn eval_reflection_aot_interface_method_prototype_target_for_eval( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + for interface_name in eval_reflection_eval_class_interface_names( + declaring_class, + context, + values, + )? { + if context.has_interface(&interface_name) { + continue; + } + if let Some(prototype) = + eval_reflection_aot_method_candidate(&interface_name, method_name, false, values)? + { + return Ok(Some(prototype)); + } + } + Ok(None) +} + /// Finds the interface that actually declares a method in an interface hierarchy. fn eval_reflection_interface_declared_method_target( interface_name: &str, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0eef4350af..e3b161addb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18754,6 +18754,45 @@ echo $inherited->hasPrototype() ? "Y" : "N"; ); } +/// Verifies eval ReflectionMethod prototypes include inherited AOT interfaces. +#[test] +fn test_eval_reflection_method_reports_inherited_aot_interface_prototypes() { + let out = compile_and_run_capture( + r#"getPrototype(); +echo ($leaf->hasPrototype() ? "L" : "l") . ":"; +echo $leafProto->getDeclaringClass()->getName() . "::" . $leafProto->getName() . ":"; +$child = new ReflectionMethod("EvalProtoAotParentChild", "inheritedIface"); +$childProto = $child->getPrototype(); +echo ($child->hasPrototype() ? "C" : "c") . ":"; +echo $childProto->getDeclaringClass()->getName() . "::" . $childProto->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "L:EvalAotProtoRootIface::inheritediface:C:EvalAotProtoParentWithIface::inheritediface" + ); +} + /// Verifies eval-declared functions share method-style named/default/ref/variadic binding. #[test] fn test_eval_declared_function_rich_argument_binding() { From faa3dd7bb0c1b8bcb2a2ef625be35c9d4911febb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 18:05:29 +0200 Subject: [PATCH 0878/1208] Preserve eval static prototypes for AOT reflection --- .../src/interpreter/reflection.rs | 10 ++++- tests/codegen/eval.rs | 37 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index e483570082..390a16c567 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -9797,6 +9797,8 @@ fn eval_reflection_method_prototype_target( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { if context.has_class(declaring_class) || context.has_enum(declaring_class) { + let want_static = eval_reflection_method_metadata(declaring_class, method_name, context) + .map_or(false, |metadata| metadata.is_static); if let Some(prototype) = eval_reflection_parent_method_prototype_target(declaring_class, method_name, context) { @@ -9806,6 +9808,7 @@ fn eval_reflection_method_prototype_target( declaring_class, method_name, context, + want_static, values, )? { return Ok(Some(prototype)); @@ -9819,6 +9822,7 @@ fn eval_reflection_method_prototype_target( declaring_class, method_name, context, + want_static, values, ); } @@ -9931,12 +9935,13 @@ fn eval_reflection_eval_aot_parent_method_prototype_target( declaring_class: &str, method_name: &str, context: &ElephcEvalContext, + want_static: bool, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(parent_class) = context.class_native_parent_name(declaring_class) else { return Ok(None); }; - eval_reflection_aot_method_candidate(&parent_class, method_name, false, values) + eval_reflection_aot_method_candidate(&parent_class, method_name, want_static, values) } /// Finds the interface method prototype for an eval-declared class method. @@ -9964,6 +9969,7 @@ fn eval_reflection_aot_interface_method_prototype_target_for_eval( declaring_class: &str, method_name: &str, context: &ElephcEvalContext, + want_static: bool, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { for interface_name in eval_reflection_eval_class_interface_names( @@ -9975,7 +9981,7 @@ fn eval_reflection_aot_interface_method_prototype_target_for_eval( continue; } if let Some(prototype) = - eval_reflection_aot_method_candidate(&interface_name, method_name, false, values)? + eval_reflection_aot_method_candidate(&interface_name, method_name, want_static, values)? { return Ok(Some(prototype)); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e3b161addb..7a0b075a11 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -18793,6 +18793,43 @@ echo $childProto->getDeclaringClass()->getName() . "::" . $childProto->getName() ); } +/// Verifies eval ReflectionMethod prototypes preserve staticness for AOT targets. +#[test] +fn test_eval_reflection_method_reports_static_aot_prototypes() { + let out = compile_and_run_capture( + r#"getPrototype(); +echo ($parent->hasPrototype() ? "P" : "p") . ":"; +echo $parentProto->getDeclaringClass()->getName() . "::" . $parentProto->getName() . ":"; +$iface = new ReflectionMethod("EvalStaticProtoImpl", "staticIface"); +$ifaceProto = $iface->getPrototype(); +echo ($iface->hasPrototype() ? "I" : "i") . ":"; +echo $ifaceProto->getDeclaringClass()->getName() . "::" . $ifaceProto->getName();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "P:EvalAotStaticProtoParent::staticrun:I:EvalAotStaticProtoIface::staticiface" + ); +} + /// Verifies eval-declared functions share method-style named/default/ref/variadic binding. #[test] fn test_eval_declared_function_rich_argument_binding() { From 521184667bba3da2c8bbb558c1be4bd35442fcc5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 18:06:03 +0200 Subject: [PATCH 0879/1208] Document AOT eval method prototypes --- docs/php/eval.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index c9c8ee74f7..d1bfb958e9 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -429,8 +429,9 @@ constants, properties, and methods. `ReflectionProperty::getDeclaringClass()` return a materialized `ReflectionClass` for the symbol that declares the reflected member. `ReflectionMethod::hasPrototype()` and `getPrototype()` expose -eval parent-class overrides and interface implementation prototypes; inherited -methods that are not overridden report no prototype, matching PHP reflection. +eval and generated/AOT parent-class overrides and interface implementation +prototypes, including static method prototypes; inherited methods that are not +overridden report no prototype, matching PHP reflection. `ReflectionClass::getConstructor()` returns a materialized `ReflectionMethod` for direct, inherited, interface, trait, and generated/AOT constructors, including registered generated/AOT constructor parameter names, From 832a487470d7750c33daf1c79fbd3fcd05e77283 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 18:16:21 +0200 Subject: [PATCH 0880/1208] Validate eval overrides through AOT ancestors --- .../src/interpreter/statements.rs | 39 ++++++++++++++----- tests/codegen/eval.rs | 39 +++++++++++++++++++ 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 03452e584d..d3d831dbb7 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -2880,14 +2880,13 @@ fn eval_method_overrides_aot_parent( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let Some(parent) = class.parent() else { + let Some(parent) = pending_class_native_parent_name(class, context) else { return Ok(false); }; - if context.has_class(parent) || !values.class_exists(parent)? { + if !values.class_exists(&parent)? { return Ok(false); } - let parent = parent.trim_start_matches('\\'); - let Some(flags) = values.reflection_method_flags(parent, method.name())? else { + let Some(flags) = values.reflection_method_flags(&parent, method.name())? else { return Ok(false); }; let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; @@ -2895,6 +2894,27 @@ fn eval_method_overrides_aot_parent( Ok(!parent_method_is_private && parent_method_is_static == method.is_static()) } +/// Returns the nearest generated/AOT parent for a class not yet registered in context. +fn pending_class_native_parent_name( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Option { + let mut current = class.parent()?.to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + let resolved = context + .resolve_class_name(¤t) + .unwrap_or_else(|| current.trim_start_matches('\\').to_string()); + if !seen.insert(resolved.to_ascii_lowercase()) { + return None; + } + let Some(parent_class) = context.class(&resolved) else { + return Some(resolved.trim_start_matches('\\').to_string()); + }; + current = parent_class.parent()?.to_string(); + } +} + /// Returns whether one method implements a direct or inherited interface method. fn eval_method_implements_interface( class: &EvalClass, @@ -3774,14 +3794,13 @@ fn validate_method_aot_parent_override( context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { - let Some(parent) = class.parent() else { + let Some(parent) = pending_class_native_parent_name(class, context) else { return Ok(()); }; - if context.has_class(parent) || !values.class_exists(parent)? { + if !values.class_exists(&parent)? { return Ok(()); } - let parent = parent.trim_start_matches('\\'); - let Some(flags) = values.reflection_method_flags(parent, method.name())? else { + let Some(flags) = values.reflection_method_flags(&parent, method.name())? else { return Ok(()); }; if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { @@ -3802,7 +3821,7 @@ fn validate_method_aot_parent_override( return Err(EvalStatus::RuntimeFatal); } let Some(required) = eval_aot_method_signature_requirement( - parent, + &parent, method.name(), parent_is_static, context, @@ -3814,7 +3833,7 @@ fn validate_method_aot_parent_override( method, class.name(), &required, - &eval_aot_method_declaring_class(parent, method.name(), values)?, + &eval_aot_method_declaring_class(&parent, method.name(), values)?, Some(class), context, ) { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7a0b075a11..afeb3d837b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12650,6 +12650,25 @@ echo (new EvalAotOverrideChild())->label();'); assert_eq!(out, "child"); } +/// Verifies eval `#[Override]` can target AOT methods through eval parents. +#[test] +fn test_eval_declared_class_override_attribute_accepts_inherited_aot_parent_method() { + let out = compile_and_run( + r#"label();'); +"#, + ); + assert_eq!(out, "child"); +} + /// Verifies eval rejects overriding final generated/AOT parent methods. #[test] fn test_eval_declared_class_rejects_final_aot_parent_method_override() { @@ -12669,6 +12688,26 @@ eval('class EvalAotFinalOverrideChild extends EvalAotFinalOverrideParent { ); } +/// Verifies eval rejects final AOT method overrides through eval parents. +#[test] +fn test_eval_declared_class_rejects_final_inherited_aot_parent_method_override() { + let err = compile_and_run_expect_failure( + r#" Date: Sat, 27 Jun 2026 18:41:09 +0200 Subject: [PATCH 0881/1208] Run inherited AOT clone hooks for eval subclasses --- .../src/interpreter/statements.rs | 32 ++++++++ tests/codegen/eval.rs | 74 +++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index d3d831dbb7..68ea3033f6 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7562,6 +7562,15 @@ pub(in crate::interpreter) fn eval_object_clone_result( ); } } + let should_call_dynamic_native_clone_hook = if clone_method.is_none() { + if let Some(class_name) = dynamic_class_name.as_deref() { + eval_dynamic_native_clone_hook_is_callable(class_name, context, values)? + } else { + false + } + } else { + false + }; let should_call_aot_clone_hook = if dynamic_class_name.is_none() { eval_aot_clone_hook_is_callable(object, context, values)? } else { @@ -7584,6 +7593,9 @@ pub(in crate::interpreter) fn eval_object_clone_result( values, )?; eval_release_value(context, values, result)?; + } else if should_call_dynamic_native_clone_hook { + let result = values.method_call(clone, "__clone", Vec::new())?; + values.release(result)?; } } else if should_call_aot_clone_hook { let result = values.method_call(clone, "__clone", Vec::new())?; @@ -7592,6 +7604,26 @@ pub(in crate::interpreter) fn eval_object_clone_result( Ok(clone) } +/// Returns whether an inherited generated/AOT `__clone()` hook can run for a dynamic eval class. +fn eval_dynamic_native_clone_hook_is_callable( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_dynamic_class_native_method_metadata(class_name, "__clone", context, values)? + else { + return Ok(false); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_clone_access_error(&declaring_class, visibility, context, values); + } + Ok(true) +} + /// Returns whether an accessible instance AOT `__clone()` hook should run. fn eval_aot_clone_hook_is_callable( object: RuntimeCellHandle, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index afeb3d837b..fdb46068e4 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19191,6 +19191,36 @@ echo $copy->name;'); assert_eq!(out, "A:A:hook"); } +/// Verifies eval `clone` invokes inherited AOT `__clone()` hooks for dynamic subclasses. +#[test] +fn test_eval_clone_dynamic_subclass_runs_inherited_aot_clone_hook() { + let out = compile_and_run_capture( + r#"name = $name; + } + + public function __clone(): void { + $this->name = $this->name . ":aot"; + } +} +eval('class EvalCloneAotInheritedHookChild extends EvalCloneAotInheritedHookParent {} +$child = new EvalCloneAotInheritedHookChild("A"); +$childCopy = clone $child; +echo $child->name . ":" . $childCopy->name;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "A:A:aot"); +} + /// Verifies eval `clone` invokes private AOT `__clone()` hooks from the declaring scope. #[test] fn test_eval_clone_aot_object_runs_private_clone_hook_in_scope() { @@ -19220,6 +19250,50 @@ echo $copy->name;'); assert_eq!(out, "A:A:private"); } +/// Verifies eval `clone` applies inherited private AOT clone visibility. +#[test] +fn test_eval_clone_dynamic_subclass_respects_private_aot_clone_hook() { + let out = compile_and_run_capture( + r#"name = $name; + } + + private function __clone(): void { + $this->name = $this->name . ":private"; + } + + public function copyInParent(): void { + eval('$copy = clone $this; +echo $copy->name;'); + } +} +eval('class EvalCloneAotInheritedPrivateChild extends EvalCloneAotInheritedPrivateParent {} +$child = new EvalCloneAotInheritedPrivateChild("A"); +try { + $copy = clone $child; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo ":"; +$child->copyInParent();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Error:Call to private EvalCloneAotInheritedPrivateParent::__clone() from global scope:A:private" + ); +} + /// Verifies eval `clone` invokes protected AOT `__clone()` hooks from child scopes. #[test] fn test_eval_clone_aot_object_runs_protected_clone_hook_in_child_scope() { From 42c0096212839c2d79f0978a0eb5c7826dacaa0b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 19:12:08 +0200 Subject: [PATCH 0882/1208] Use class layout metadata for eval object clone --- src/codegen_support/runtime/data/user.rs | 50 +++++++++++++++++++--- src/codegen_support/runtime/eval_bridge.rs | 47 +++++++++++++------- tests/codegen/eval.rs | 34 +++++++++++++++ 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 031457ccaa..91694c707b 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -282,6 +282,33 @@ pub(crate) fn emit_runtime_data_user( } } + out.push_str(".globl _class_object_payload_sizes\n_class_object_payload_sizes:\n"); + if let Some(max_class_id) = max_class_id { + for class_id in 0..=max_class_id { + let payload_size = + match (class_info_by_id.get(&class_id), class_name_by_id.get(&class_id)) { + (Some(class_info), Some(class_name)) => { + class_object_payload_size(class_name, class_info) + } + _ => 0, + }; + out.push_str(&format!(" .quad {}\n", payload_size)); + } + } + + out.push_str(".globl _class_object_dynamic_prop_flags\n_class_object_dynamic_prop_flags:\n"); + if let Some(max_class_id) = max_class_id { + for class_id in 0..=max_class_id { + let flag = match (class_info_by_id.get(&class_id), class_name_by_id.get(&class_id)) { + (Some(class_info), Some(class_name)) => { + u8::from(class_uses_dynamic_property_tail(class_name, class_info)) + } + _ => 0, + }; + out.push_str(&format!(" .quad {}\n", flag)); + } + } + out.push_str(".globl _class_gc_desc_count\n_class_gc_desc_count:\n"); out.push_str(&format!( " .quad {}\n", @@ -1928,13 +1955,7 @@ fn emit_classes_by_name_table( out.push_str(&format!(" .quad {}\n", sorted_classes.len())); out.push_str(".globl _classes_by_name\n_classes_by_name:\n"); for (class_name, class_info) in sorted_classes { - let num_props = class_info.properties.len(); - let dyn_props_slot = if class_info.allow_dynamic_properties { - 8 - } else { - 0 - }; - let obj_size = 8 + num_props * 16 + dyn_props_slot; + let obj_size = class_object_payload_size(class_name, class_info); out.push_str(&format!( " .quad _class_by_name_str_{}\n", class_info.class_id @@ -1945,6 +1966,21 @@ fn emit_classes_by_name_table( } } +/// Returns the PHP object payload bytes required by one class layout. +fn class_object_payload_size(class_name: &str, class_info: &ClassInfo) -> usize { + let dyn_props_slot = if class_uses_dynamic_property_tail(class_name, class_info) { + 8 + } else { + 0 + }; + 8 + class_info.properties.len() * 16 + dyn_props_slot +} + +/// Returns whether this class layout stores a dynamic-property hash tail. +fn class_uses_dynamic_property_tail(class_name: &str, class_info: &ClassInfo) -> bool { + class_name == "stdClass" || class_info.allow_dynamic_properties +} + /// The number of fixed-slot stream-wrapper methods recorded per class in /// `_user_wrapper_vtable_`. Slot order matches the runtime fopen /// dispatch (Phase 10): 0 stream_open, 1 stream_close, 2 stream_read, diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 1a27959c15..4e876c43ea 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -4219,8 +4219,9 @@ fn emit_aarch64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("lsl x12, x11, #3"); // scale class id to an 8-byte descriptor pointer slot emitter.instruction("ldr x10, [x10, x12]"); // load the class property-tag descriptor pointer emitter.instruction("str x10, [sp, #16]"); // save descriptor pointer for the property-copy loop - emitter.instruction("ldr w12, [x9, #-16]"); // load the object payload size from the heap header - emitter.instruction("str x12, [sp, #48]"); // save payload size for allocation and dyn-prop detection + abi::emit_symbol_address(emitter, "x10", "_class_object_payload_sizes"); + emitter.instruction("ldr x12, [x10, x12]"); // load the class-declared object payload size + emitter.instruction("str x12, [sp, #48]"); // save payload size for allocation and dyn-prop offsets emitter.instruction("mov x0, x12"); // pass the source payload size to the heap allocator emitter.instruction("bl __rt_heap_alloc"); // allocate a clone object payload with the same byte size emitter.instruction("mov x9, #4"); // heap kind 4 marks object instances for ownership helpers @@ -4230,6 +4231,13 @@ fn emit_aarch64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("str x0, [sp, #8]"); // save the clone object payload pointer emitter.instruction("ldr x12, [sp, #48]"); // reload the payload size emitter.instruction("sub x12, x12, #8"); // remove the leading class id field from the clone layout + emitter.instruction("ldr x13, [sp, #56]"); // reload the source class id for dynamic-property metadata + emitter.instruction("lsl x13, x13, #3"); // scale class id to an 8-byte dynamic-flag slot + abi::emit_symbol_address(emitter, "x10", "_class_object_dynamic_prop_flags"); + emitter.instruction("ldr x13, [x10, x13]"); // load whether this class layout has a dyn-props tail + emitter.instruction("cbz x13, __elephc_eval_value_object_clone_shallow_count_ready"); // no dyn-props tail contributes to property count + emitter.instruction("sub x12, x12, #8"); // remove the dyn-props tail before counting declared slots + emitter.label("__elephc_eval_value_object_clone_shallow_count_ready"); emitter.instruction("lsr x12, x12, #4"); // derive declared-property slot count from the payload size emitter.instruction("str x12, [sp, #24]"); // save property count for the copy loop emitter.instruction("str xzr, [sp, #32]"); // initialize property-copy index to zero @@ -4290,12 +4298,13 @@ fn emit_aarch64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("b __elephc_eval_value_object_clone_shallow_prop_loop"); // continue copying declared properties emitter.label("__elephc_eval_value_object_clone_shallow_dyn"); - emitter.instruction("ldr x12, [sp, #48]"); // reload clone payload size for dynamic-property detection - emitter.instruction("sub x13, x12, #8"); // isolate bytes after the leading class id field - emitter.instruction("and x14, x13, #15"); // check whether an 8-byte dynamic-property tail exists - emitter.instruction("cmp x14, #8"); // remainder 8 means the layout has a dyn-props hash slot - emitter.instruction("b.ne __elephc_eval_value_object_clone_shallow_box"); // no dynamic hash slot: box the copied clone - emitter.instruction("sub x13, x12, #8"); // compute dyn-props slot offset as payload_size - 8 + emitter.instruction("ldr x12, [sp, #56]"); // reload the source class id for dynamic-property metadata + emitter.instruction("lsl x12, x12, #3"); // scale class id to an 8-byte dynamic-flag slot + abi::emit_symbol_address(emitter, "x10", "_class_object_dynamic_prop_flags"); + emitter.instruction("ldr x12, [x10, x12]"); // load whether this class layout has a dyn-props tail + emitter.instruction("cbz x12, __elephc_eval_value_object_clone_shallow_box"); // no dynamic hash slot: box the copied clone + emitter.instruction("ldr x13, [sp, #48]"); // reload the class-declared payload size + emitter.instruction("sub x13, x13, #8"); // compute dyn-props slot offset as payload_size - 8 emitter.instruction("str x13, [sp, #40]"); // save dyn-props slot offset across hash cloning emitter.instruction("ldr x9, [sp, #0]"); // reload the source object pointer emitter.instruction("ldr x10, [x9, x13]"); // load the source dynamic-property hash pointer @@ -4350,7 +4359,8 @@ fn emit_x86_64_object_clone_shallow_wrapper(emitter: &mut Emitter) { abi::emit_symbol_address(emitter, "r11", "_class_gc_desc_ptrs"); emitter.instruction("mov r11, QWORD PTR [r11 + rax * 8]"); // load the class property-tag descriptor pointer emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // save descriptor pointer for the property-copy loop - emitter.instruction("mov ecx, DWORD PTR [r10 - 16]"); // load the object payload size from the heap header + abi::emit_symbol_address(emitter, "r11", "_class_object_payload_sizes"); + emitter.instruction("mov rcx, QWORD PTR [r11 + rax * 8]"); // load the class-declared object payload size emitter.instruction("mov QWORD PTR [rbp - 48], rcx"); // save payload size for allocation and dyn-prop detection emitter.instruction("mov rax, rcx"); // pass the source payload size to the heap allocator emitter.instruction("call __rt_heap_alloc"); // allocate a clone object payload with the same byte size @@ -4361,6 +4371,13 @@ fn emit_x86_64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("mov QWORD PTR [rbp - 16], rax"); // save the clone object payload pointer emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the payload size emitter.instruction("sub r10, 8"); // remove the leading class id field from the clone layout + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the source class id for dynamic-property metadata + abi::emit_symbol_address(emitter, "r11", "_class_object_dynamic_prop_flags"); + emitter.instruction("mov r11, QWORD PTR [r11 + rax * 8]"); // load whether this class layout has a dyn-props tail + emitter.instruction("test r11, r11"); // check whether dyn-props bytes should be excluded + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_count_ready_x86"); // no dyn-props tail contributes to property count + emitter.instruction("sub r10, 8"); // remove the dyn-props tail before counting declared slots + emitter.label("__elephc_eval_value_object_clone_shallow_count_ready_x86"); emitter.instruction("shr r10, 4"); // derive declared-property slot count from the payload size emitter.instruction("mov QWORD PTR [rbp - 32], r10"); // save property count for the copy loop emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // initialize property-copy index to zero @@ -4412,12 +4429,12 @@ fn emit_x86_64_object_clone_shallow_wrapper(emitter: &mut Emitter) { emitter.instruction("jmp __elephc_eval_value_object_clone_shallow_prop_loop_x86"); // continue copying declared properties emitter.label("__elephc_eval_value_object_clone_shallow_dyn_x86"); - emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload clone payload size for dynamic-property detection - emitter.instruction("mov r11, r10"); // copy payload size before deriving the property region size - emitter.instruction("sub r11, 8"); // isolate bytes after the leading class id field - emitter.instruction("and r11, 15"); // check whether an 8-byte dynamic-property tail exists - emitter.instruction("cmp r11, 8"); // remainder 8 means the layout has a dyn-props hash slot - emitter.instruction("jne __elephc_eval_value_object_clone_shallow_box_x86"); // no dynamic hash slot: box the copied clone + emitter.instruction("mov rax, QWORD PTR [rbp - 56]"); // reload the source class id for dynamic-property metadata + abi::emit_symbol_address(emitter, "r11", "_class_object_dynamic_prop_flags"); + emitter.instruction("mov r11, QWORD PTR [r11 + rax * 8]"); // load whether this class layout has a dyn-props tail + emitter.instruction("test r11, r11"); // check whether a dyn-props hash slot exists + emitter.instruction("jz __elephc_eval_value_object_clone_shallow_box_x86"); // no dynamic hash slot: box the copied clone + emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the class-declared payload size emitter.instruction("sub r10, 8"); // compute dyn-props slot offset as payload_size - 8 emitter.instruction("mov QWORD PTR [rbp - 40], r10"); // save dyn-props slot offset across hash cloning emitter.instruction("mov r11, QWORD PTR [rbp - 8]"); // reload the source object pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fdb46068e4..5e76424479 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19221,6 +19221,40 @@ echo $child->name . ":" . $childCopy->name;'); assert_eq!(out.stdout, "A:A:aot"); } +/// Verifies an eval `__clone()` override on an AOT-backed subclass owns clone-hook dispatch. +#[test] +fn test_eval_clone_dynamic_subclass_override_aot_clone_hook() { + let out = compile_and_run_capture( + r#"name = $name; + } + + public function __clone(): void { + $this->name = $this->name . ":aot"; + } +} +eval('class EvalCloneAotOverrideHookChild extends EvalCloneAotOverrideHookParent { + public function __clone(): void { + $this->name = $this->name . ":eval"; + } +} +$child = new EvalCloneAotOverrideHookChild("B"); +$childCopy = clone $child; +echo $child->name . ":" . $childCopy->name;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "B:B:eval"); +} + /// Verifies eval `clone` invokes private AOT `__clone()` hooks from the declaring scope. #[test] fn test_eval_clone_aot_object_runs_private_clone_hook_in_scope() { From 1160baf4a3dc479ea0c7a496f444e65bc11184ce Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 19:19:35 +0200 Subject: [PATCH 0883/1208] Dispatch inherited protected clone hooks for eval subclasses --- .../src/interpreter/statements.rs | 34 ++++++++++++++----- tests/codegen/eval.rs | 34 +++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 68ea3033f6..780a9d85bb 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7562,14 +7562,14 @@ pub(in crate::interpreter) fn eval_object_clone_result( ); } } - let should_call_dynamic_native_clone_hook = if clone_method.is_none() { + let dynamic_native_clone_hook_scope = if clone_method.is_none() { if let Some(class_name) = dynamic_class_name.as_deref() { eval_dynamic_native_clone_hook_is_callable(class_name, context, values)? } else { - false + None } } else { - false + None }; let should_call_aot_clone_hook = if dynamic_class_name.is_none() { eval_aot_clone_hook_is_callable(object, context, values)? @@ -7593,8 +7593,9 @@ pub(in crate::interpreter) fn eval_object_clone_result( values, )?; eval_release_value(context, values, result)?; - } else if should_call_dynamic_native_clone_hook { - let result = values.method_call(clone, "__clone", Vec::new())?; + } else if let Some(scope) = dynamic_native_clone_hook_scope { + let result = + eval_native_method_call_with_scope(&scope, clone, "__clone", Vec::new(), context, values)?; values.release(result)?; } } else if should_call_aot_clone_hook { @@ -7604,16 +7605,16 @@ pub(in crate::interpreter) fn eval_object_clone_result( Ok(clone) } -/// Returns whether an inherited generated/AOT `__clone()` hook can run for a dynamic eval class. +/// Returns the declaring scope for an inherited generated/AOT `__clone()` hook. fn eval_dynamic_native_clone_hook_is_callable( class_name: &str, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result { +) -> Result, EvalStatus> { let Some((declaring_class, visibility, is_static, is_abstract)) = eval_dynamic_class_native_method_metadata(class_name, "__clone", context, values)? else { - return Ok(false); + return Ok(None); }; if is_static || is_abstract { return Err(EvalStatus::RuntimeFatal); @@ -7621,7 +7622,22 @@ fn eval_dynamic_native_clone_hook_is_callable( if validate_eval_member_access(&declaring_class, visibility, context).is_err() { return eval_throw_clone_access_error(&declaring_class, visibility, context, values); } - Ok(true) + Ok(Some(declaring_class)) +} + +/// Calls one generated/AOT method while presenting an explicit PHP class scope to the bridge. +fn eval_native_method_call_with_scope( + scope: &str, + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.push_class_scope(scope.to_string()); + let result = values.method_call(object, method_name, evaluated_args); + context.pop_class_scope(); + result } /// Returns whether an accessible instance AOT `__clone()` hook should run. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5e76424479..603989734b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19328,6 +19328,40 @@ $child->copyInParent();'); ); } +/// Verifies eval subclasses can invoke inherited protected AOT `__clone()` hooks in child scope. +#[test] +fn test_eval_clone_dynamic_subclass_runs_protected_aot_clone_hook_in_child_scope() { + let out = compile_and_run_capture( + r#"name = $name; + } + + protected function __clone(): void { + $this->name = $this->name . ":protected"; + } +} +eval('class EvalCloneAotInheritedProtectedChild extends EvalCloneAotInheritedProtectedParent { + public function copySelf(): void { + $copy = clone $this; + echo $this->name . ":" . $copy->name; + } +} +$child = new EvalCloneAotInheritedProtectedChild("B"); +$child->copySelf();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "B:B:protected"); +} + /// Verifies eval `clone` invokes protected AOT `__clone()` hooks from child scopes. #[test] fn test_eval_clone_aot_object_runs_protected_clone_hook_in_child_scope() { From 0185e09cf1344ad1306e4c728e4282ff34789983 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 19:30:04 +0200 Subject: [PATCH 0884/1208] Bridge inherited protected AOT methods for eval subclasses --- .../src/interpreter/statements.rs | 164 ++++++++++++++++-- tests/codegen/eval.rs | 56 ++++++ 2 files changed, 206 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 780a9d85bb..396bd3718a 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7022,11 +7022,19 @@ fn eval_static_method_call_result_resolved( ); } if let Some(parent) = context.class_native_parent_name(&class_name) { - let has_native_method = + if let Some((declaring_class, _, _, _)) = eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values)? - .is_some() - || eval_native_static_magic_method_available(&parent, context, values)?; - if has_native_method { + { + return eval_native_static_method_with_evaluated_args_bridge_scope( + &parent, + method_name, + evaluated_args, + Some(&declaring_class), + context, + values, + ); + } + if eval_native_static_magic_method_available(&parent, context, values)? { return eval_native_static_method_with_evaluated_args( &parent, method_name, @@ -7640,6 +7648,21 @@ fn eval_native_method_call_with_scope( result } +/// Calls one generated/AOT static method while presenting an explicit PHP class scope. +fn eval_native_static_method_call_with_scope( + scope: &str, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.push_class_scope(scope.to_string()); + let result = values.static_method_call(class_name, method_name, evaluated_args); + context.pop_class_scope(); + result +} + /// Returns whether an accessible instance AOT `__clone()` hook should run. fn eval_aot_clone_hook_is_callable( object: RuntimeCellHandle, @@ -8222,16 +8245,25 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( } if inaccessible_method.is_none() { if let Some(parent) = context.class_native_parent_name(&called_class_name) { - let has_native_method = + if let Some((declaring_class, _, _, _)) = eval_aot_method_dispatch_metadata_in_hierarchy( &parent, method_name, context, values, )? - .is_some() - || eval_native_instance_magic_method_available(&parent, context, values)?; - if has_native_method { + { + return eval_native_method_with_evaluated_args_bridge_scope( + object, + &parent, + method_name, + evaluated_args, + Some(&declaring_class), + context, + values, + ); + } + if eval_native_instance_magic_method_available(&parent, context, values)? { return eval_native_method_with_evaluated_args( object, &parent, @@ -8306,11 +8338,12 @@ pub(in crate::interpreter) fn eval_invokable_object_call_result( if let Some(native_class_name) = eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? { - return eval_native_method_with_evaluated_args_unchecked( + return eval_native_method_with_evaluated_args_unchecked_bridge_scope( object, &native_class_name, "__invoke", evaluated_args, + Some(&native_class_name), context, values, ); @@ -9708,6 +9741,27 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + None, + context, + values, + ) +} + +/// Calls one generated/AOT instance method after validation with an optional bridge scope. +fn eval_native_method_with_evaluated_args_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let metadata = eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; @@ -9743,11 +9797,12 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( values, ); } - eval_native_method_with_evaluated_args_unchecked( + eval_native_method_with_evaluated_args_unchecked_bridge_scope( object, class_name, method_name, evaluated_args, + bridge_scope, context, values, ) @@ -9761,6 +9816,27 @@ fn eval_native_method_with_evaluated_args_unchecked( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + None, + context, + values, + ) +} + +/// Calls one generated/AOT instance method without visibility checks using an optional bridge scope. +fn eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let signature = context.native_method_signature(class_name, method_name); let bound_args = bind_native_callable_bound_args( @@ -9769,7 +9845,18 @@ fn eval_native_method_with_evaluated_args_unchecked( context, values, )?; - let result = values.method_call(object, method_name, native_bound_arg_values(&bound_args)); + let result = if let Some(scope) = bridge_scope { + eval_native_method_call_with_scope( + scope, + object, + method_name, + native_bound_arg_values(&bound_args), + context, + values, + ) + } else { + values.method_call(object, method_name, native_bound_arg_values(&bound_args)) + }; let writeback = write_back_native_callable_ref_args(&bound_args, context, values); match (result, writeback) { (Err(status), _) | (_, Err(status)) => Err(status), @@ -9784,6 +9871,25 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_bridge_scope( + class_name, + method_name, + evaluated_args, + None, + context, + values, + ) +} + +/// Calls one generated/AOT static method after validation with an optional bridge scope. +fn eval_native_static_method_with_evaluated_args_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let metadata = eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; @@ -9818,10 +9924,11 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( values, ); } - eval_native_static_method_with_evaluated_args_unchecked( + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( class_name, method_name, evaluated_args, + bridge_scope, context, values, ) @@ -9834,6 +9941,25 @@ fn eval_native_static_method_with_evaluated_args_unchecked( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + class_name, + method_name, + evaluated_args, + None, + context, + values, + ) +} + +/// Calls one generated/AOT static method without visibility checks using an optional bridge scope. +fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let signature = context.native_static_method_signature(class_name, method_name); let bound_args = bind_native_callable_bound_args( @@ -9842,8 +9968,18 @@ fn eval_native_static_method_with_evaluated_args_unchecked( context, values, )?; - let result = - values.static_method_call(class_name, method_name, native_bound_arg_values(&bound_args)); + let result = if let Some(scope) = bridge_scope { + eval_native_static_method_call_with_scope( + scope, + class_name, + method_name, + native_bound_arg_values(&bound_args), + context, + values, + ) + } else { + values.static_method_call(class_name, method_name, native_bound_arg_values(&bound_args)) + }; let writeback = write_back_native_callable_ref_args(&bound_args, context, values); match (result, writeback) { (Err(status), _) | (_, Err(status)) => Err(status), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 603989734b..0d1aef2bbb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8173,6 +8173,62 @@ echo $parent ? $parent->getName() : "missing";'); ); } +/// Verifies eval-declared children can call inherited protected AOT instance methods. +#[test] +fn test_eval_declared_child_calls_inherited_protected_aot_instance_method() { + let out = compile_and_run_capture( + r#"add(3); + } +} +$box = new EvalRuntimeProtectedMethodChild(); +$box->run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5"); +} + +/// Verifies eval-declared children can call inherited protected AOT static methods. +#[test] +fn test_eval_declared_child_calls_inherited_protected_aot_static_method() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "6"); +} + /// Verifies eval-declared classes inherit AOT callable object and method behavior. #[test] fn test_eval_declared_class_inherits_aot_invokable_parent_callables() { From 791d324f8332de924eef1b827933065235a8f8ec Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 19:37:59 +0200 Subject: [PATCH 0885/1208] Bridge inherited protected AOT properties for eval subclasses --- .../src/interpreter/statements.rs | 306 ++++++++++++++++++ tests/codegen/eval.rs | 56 ++++ 2 files changed, 362 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 396bd3718a..6fa5d5a3ed 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -5245,6 +5245,40 @@ pub(in crate::interpreter) fn eval_property_get_result( { return values.property_get(object, property_name); } + if !declared_property_found { + if let Some((declaring_class, visibility, _, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if let Some(result) = eval_magic_property_get( + object, + &object_class_name, + property_name, + context, + values, + )? { + return Ok(result); + } + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + return eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_get(object, property_name) + }); + } + } + } if !declared_property_found { if let Some(result) = eval_magic_property_get(object, &object_class_name, property_name, context, values)? @@ -5376,6 +5410,43 @@ pub(in crate::interpreter) fn eval_property_set_result( ); } } + if !declared_property_found { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + if eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? { + return Ok(()); + } + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + return eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_set(object, property_name, value) + }); + } + } + } if !declared_property_found && eval_magic_property_set( object, @@ -5607,6 +5678,36 @@ pub(in crate::interpreter) fn eval_property_isset_result( let value = values.property_get(object, property_name)?; return Ok(!values.is_null(value)?); } + if let Some((declaring_class, visibility, _, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_magic_property_isset( + object, + &object_class_name, + property_name, + context, + values, + ) + .map(|result| result.unwrap_or(false)); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_is_initialized(object, property_name) + })? { + return Ok(false); + } + let value = eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_get(object, property_name) + })?; + return Ok(!values.is_null(value)?); + } + } eval_magic_property_isset(object, &object_class_name, property_name, context, values) .map(|result| result.unwrap_or(false)) } @@ -6327,6 +6428,57 @@ pub(in crate::interpreter) fn eval_static_property_get_result( ); } if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_is_initialized(&declaring_class, property_name) + })? { + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property_name, + context, + values, + ); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.static_property_get(&declaring_class, property_name), + )? { + return Ok(value); + } + } + } return eval_throw_undeclared_static_property_error( &class_name, property_name, @@ -6407,6 +6559,42 @@ pub(in crate::interpreter) fn eval_static_property_isset_result( return Ok(!values.is_null(value)?); } if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return Ok(false); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + let value = eval_reference_target_value(&target, context, values)?; + return Ok(!values.is_null(value)?); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_is_initialized(&declaring_class, property_name) + })? { + return Ok(false); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.static_property_get(&declaring_class, property_name), + )? { + return Ok(!values.is_null(value)?); + } + } + } return Ok(false); } if let Some((declaring_class, visibility, _, is_static)) = @@ -6678,6 +6866,51 @@ fn eval_static_property_reference_bind_result( return Ok(()); } if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property_name, + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property_name, target); + if eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_set(&declaring_class, property_name, value) + })? { + return Ok(()); + } + } + } return eval_throw_undeclared_static_property_error( &class_name, property_name, @@ -6844,6 +7077,54 @@ pub(in crate::interpreter) fn eval_static_property_set_result( return Ok(()); } if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property_name, + target, + value, + context, + values, + )?; + } + if eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_set(&declaring_class, property_name, value) + })? { + return Ok(()); + } + } + } return eval_throw_undeclared_static_property_error( &class_name, property_name, @@ -7663,6 +7944,31 @@ fn eval_native_static_method_call_with_scope( result } +/// Runs one generated/AOT bridge operation while exposing an explicit PHP class scope. +fn eval_with_native_bridge_scope( + scope: &str, + context: &mut ElephcEvalContext, + call: impl FnOnce() -> Result, +) -> Result { + context.push_class_scope(scope.to_string()); + let result = call(); + context.pop_class_scope(); + result +} + +/// Returns generated/AOT property metadata inherited by an eval-declared class. +fn eval_dynamic_class_native_property_metadata( + called_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + eval_reflection_aot_property_access_metadata(&parent, property_name, values) +} + /// Returns whether an accessible instance AOT `__clone()` hook should run. fn eval_aot_clone_hook_is_callable( object: RuntimeCellHandle, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0d1aef2bbb..3914135048 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8229,6 +8229,62 @@ $box->run();'); assert_eq!(out.stdout, "6"); } +/// Verifies eval-declared children can read and write inherited protected AOT properties. +#[test] +fn test_eval_declared_child_accesses_inherited_protected_aot_property() { + let out = compile_and_run_capture( + r#"x) ? "I:" : "i:"; + $this->x = 7; + echo $this->x; + } +} +$box = new EvalRuntimeProtectedPropertyChild(); +$box->run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "I:7"); +} + +/// Verifies eval-declared children can read and write inherited protected AOT static properties. +#[test] +fn test_eval_declared_child_accesses_inherited_protected_aot_static_property() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "S:8"); +} + /// Verifies eval-declared classes inherit AOT callable object and method behavior. #[test] fn test_eval_declared_class_inherits_aot_invokable_parent_callables() { From 80643542dc459e6aabd8307ca9f9bf1c18388cf0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 19:42:55 +0200 Subject: [PATCH 0886/1208] Bridge inherited AOT class constants for eval subclasses --- .../src/interpreter/statements.rs | 51 +++++++++++++++++++ tests/codegen/eval.rs | 26 ++++++++++ 2 files changed, 77 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 6fa5d5a3ed..c44a630a9a 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -6687,6 +6687,31 @@ pub(in crate::interpreter) fn eval_class_constant_fetch_result( .ok_or(EvalStatus::RuntimeFatal); } if eval_static_member_context_owns_class(&class_name, context) { + if let Some((declaring_class, visibility)) = + eval_dynamic_class_native_constant_metadata( + &class_name, + constant_name, + context, + values, + )? + { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_constant_access_error( + &declaring_class, + constant_name, + visibility, + context, + values, + ); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.class_constant_get(&declaring_class, constant_name), + )? { + return Ok(value); + } + } return Err(EvalStatus::RuntimeFatal); } if let Some(value) = values.class_constant_get(&class_name, constant_name)? { @@ -7969,6 +7994,32 @@ fn eval_dynamic_class_native_property_metadata( eval_reflection_aot_property_access_metadata(&parent, property_name, values) } +/// Returns generated/AOT class-constant metadata inherited by an eval-declared class. +fn eval_dynamic_class_native_constant_metadata( + called_class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + let Some(flags) = values.reflection_constant_flags(&parent, constant_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_constant_declaring_class(&parent, constant_name)? + .unwrap_or(parent); + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + Ok(Some((declaring_class, visibility))) +} + /// Returns whether an accessible instance AOT `__clone()` hook should run. fn eval_aot_clone_hook_is_callable( object: RuntimeCellHandle, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3914135048..46301f62a0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8285,6 +8285,32 @@ $box->run();'); assert_eq!(out.stdout, "S:8"); } +/// Verifies eval-declared children can read inherited protected AOT class constants. +#[test] +fn test_eval_declared_child_reads_inherited_protected_aot_class_constant() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "9"); +} + /// Verifies eval-declared classes inherit AOT callable object and method behavior. #[test] fn test_eval_declared_class_inherits_aot_invokable_parent_callables() { From 31fa996f25801c88297882bf45c3c372f8d68f43 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 19:47:38 +0200 Subject: [PATCH 0887/1208] Bridge inherited AOT constructors for eval subclasses --- .../src/interpreter/statements.rs | 28 +++++++++++++++++- tests/codegen/eval.rs | 29 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index c44a630a9a..0a4a00059a 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -10443,6 +10443,8 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( if let Some(message) = eval_native_constructor_access_error(class_name, context, values)? { return eval_throw_error(&message, context, values); } + let bridge_scope = + eval_native_constructor_bridge_scope(class_name, context, values)?; let signature = context.native_constructor_signature(class_name); let bound_args = bind_native_callable_bound_args( signature, @@ -10450,7 +10452,13 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( context, values, )?; - let result = values.construct_object(object, native_bound_arg_values(&bound_args)); + let result = if let Some(scope) = bridge_scope.as_deref() { + eval_with_native_bridge_scope(scope, context, || { + values.construct_object(object, native_bound_arg_values(&bound_args)) + }) + } else { + values.construct_object(object, native_bound_arg_values(&bound_args)) + }; let writeback = write_back_native_callable_ref_args(&bound_args, context, values); match (result, writeback) { (Err(status), _) | (_, Err(status)) => Err(status), @@ -10458,6 +10466,24 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( } } +/// Returns the generated/AOT constructor scope that the runtime bridge can recognize. +fn eval_native_constructor_bridge_scope( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility)) = + eval_reflection_aot_non_public_constructor(class_name, values)? + else { + return Ok(None); + }; + if eval_native_constructor_access_allowed(&declaring_class, visibility, context) { + Ok(Some(declaring_class)) + } else { + Ok(None) + } +} + /// Returns PHP's constructor access error for generated/AOT constructors, if inaccessible. fn eval_native_constructor_access_error( class_name: &str, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 46301f62a0..822a9fb08d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8311,6 +8311,35 @@ $box->run();'); assert_eq!(out.stdout, "9"); } +/// Verifies eval-declared children can run inherited protected AOT constructors. +#[test] +fn test_eval_declared_child_runs_inherited_protected_aot_constructor() { + let out = compile_and_run_capture( + r#"x = $x + 2; + } +} + +eval('class EvalRuntimeProtectedConstructorChild extends EvalRuntimeProtectedConstructorParent { + public static function make() { + return new self(3); + } +} +$box = EvalRuntimeProtectedConstructorChild::make(); +echo $box->x;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5"); +} + /// Verifies eval-declared classes inherit AOT callable object and method behavior. #[test] fn test_eval_declared_class_inherits_aot_invokable_parent_callables() { From 1e68a65b0ac94c333cd2b6ce4fa6aaedb932e97f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 19:59:11 +0200 Subject: [PATCH 0888/1208] Run inherited AOT destructors for eval subclasses --- .../src/ffi/dynamic_destructors.rs | 3 ++- .../src/interpreter/statements.rs | 2 +- tests/codegen/eval.rs | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/ffi/dynamic_destructors.rs b/crates/elephc-magician/src/ffi/dynamic_destructors.rs index c7d5576179..862fbad004 100644 --- a/crates/elephc-magician/src/ffi/dynamic_destructors.rs +++ b/crates/elephc-magician/src/ffi/dynamic_destructors.rs @@ -138,7 +138,8 @@ unsafe fn dynamic_object_destruct_inner(object: *mut RuntimeCell) -> u64 { let release_result = values.release(object_cell); context.forget_dynamic_object(identity); match (destruct_result, release_result) { - (Ok(true), Ok(())) | (Ok(false), Ok(())) => 1, + (Ok(true), Ok(())) => 1, + (Ok(false), Ok(())) => 0, (Err(EvalStatus::UnsupportedConstruct), _) => 1, (Err(_), _) | (_, Err(_)) => 1, } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 0a4a00059a..445963662d 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -694,7 +694,7 @@ pub(crate) fn eval_dynamic_destructor_for_object_cell( return Ok(false); }; let Some((declaring_class, method)) = context.class_method(&class_name, "__destruct") else { - return Ok(true); + return Ok(false); }; if !context.begin_dynamic_object_destructor(identity) { return Ok(true); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 822a9fb08d..6efa0fa0c0 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19292,6 +19292,27 @@ echo "after";'); assert_eq!(out, "drop:A:after"); } +/// Verifies eval-declared subclasses inherit generated/AOT destructors. +#[test] +fn test_eval_dynamic_subclass_runs_inherited_aot_destructor() { + let out = compile_and_run( + r#"name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} + +eval('class EvalDestructAotChild extends EvalDestructAotParent {} +$box = new EvalDestructAotChild("C"); +echo "body:"; +unset($box); +echo "after";'); +"#, + ); + assert_eq!(out, "body:drop:C:after"); +} + /// Verifies eval `clone` shallow-copies ordinary emitted AOT objects. #[test] fn test_eval_clone_aot_object_expression() { From 452cd146e7aa1ab0a6f5e413a066358af69124ab Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 20:12:39 +0200 Subject: [PATCH 0889/1208] Support parent static syntax for eval AOT parents --- .../src/interpreter/expressions.rs | 18 +- .../src/interpreter/statements.rs | 224 +++++++++++++++--- tests/codegen/eval.rs | 56 +++++ 3 files changed, 269 insertions(+), 29 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index ac7740356e..86b9f6cf43 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -84,7 +84,14 @@ pub(in crate::interpreter) fn eval_expr( let class_name = eval_dynamic_class_name(class_name, context, values)?; let method = eval_dynamic_member_name(method, context, scope, values)?; let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; - eval_static_method_call_result(&class_name, &method, evaluated_args, context, values) + eval_static_method_call_result_from_scope( + &class_name, + &method, + evaluated_args, + scope, + context, + values, + ) } EvalExpr::DynamicStaticPropertyGet { class_name, @@ -174,7 +181,14 @@ pub(in crate::interpreter) fn eval_expr( args, } => { let evaluated_args = eval_method_call_arg_values(args, context, scope, values)?; - eval_static_method_call_result(class_name, method, evaluated_args, context, values) + eval_static_method_call_result_from_scope( + class_name, + method, + evaluated_args, + scope, + context, + values, + ) } EvalExpr::StaticPropertyGet { class_name, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 445963662d..ca4e1daa71 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7216,6 +7216,28 @@ pub(in crate::interpreter) fn eval_static_method_call_result( receiver.called_class, method_name, evaluated_args, + None, + context, + values, + ) +} + +/// Dispatches a static-syntax method call from an expression scope that may hold `$this`. +pub(in crate::interpreter) fn eval_static_method_call_result_from_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + eval_static_method_call_result_resolved( + receiver.dispatch_class, + receiver.called_class, + method_name, + evaluated_args, + Some(scope), context, values, ) @@ -7239,6 +7261,7 @@ pub(in crate::interpreter) fn eval_static_method_call_result_with_called_class( called_class_name, method_name, evaluated_args, + None, context, values, ) @@ -7250,6 +7273,7 @@ fn eval_static_method_call_result_resolved( called_class_name: String, method_name: &str, evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -7283,14 +7307,6 @@ fn eval_static_method_call_result_resolved( if let Some((declaring_class, method)) = eval_dynamic_static_method_for_call(&class_name, method_name, context) { - if !method.is_static() { - return eval_throw_non_static_method_call_error( - &declaring_class, - method.name(), - context, - values, - ); - } if method.is_abstract() { return eval_throw_abstract_method_call_error( &declaring_class, @@ -7318,6 +7334,27 @@ fn eval_static_method_call_result_resolved( values, ); } + if !method.is_static() { + if let Some(object) = + eval_static_syntax_instance_receiver(&class_name, lexical_scope, context, values)? + { + return eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ); + } + return eval_throw_non_static_method_call_error( + &declaring_class, + method.name(), + context, + values, + ); + } return eval_dynamic_static_method_with_values( &declaring_class, &called_class_name, @@ -7328,26 +7365,16 @@ fn eval_static_method_call_result_resolved( ); } if let Some(parent) = context.class_native_parent_name(&class_name) { - if let Some((declaring_class, _, _, _)) = - eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values)? + if let Some(result) = eval_native_static_syntax_method_result( + &parent, + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? { - return eval_native_static_method_with_evaluated_args_bridge_scope( - &parent, - method_name, - evaluated_args, - Some(&declaring_class), - context, - values, - ); - } - if eval_native_static_magic_method_available(&parent, context, values)? { - return eval_native_static_method_with_evaluated_args( - &parent, - method_name, - evaluated_args, - context, - values, - ); + return Ok(result); } } if context.has_class(&class_name) @@ -7372,6 +7399,16 @@ fn eval_static_method_call_result_resolved( values, ); } + if let Some(result) = eval_native_static_syntax_method_result( + &class_name, + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? { + return Ok(result); + } eval_native_static_method_with_evaluated_args( &class_name, method_name, @@ -7381,6 +7418,139 @@ fn eval_static_method_call_result_resolved( ) } +/// Dispatches one generated/AOT method reached through PHP static-call syntax. +fn eval_native_static_syntax_method_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_static_method_with_evaluated_args( + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + return Ok(None); + }; + if is_abstract { + return eval_throw_abstract_method_call_error( + &declaring_class, + method_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + if !is_static { + if let Some(object) = + eval_static_syntax_instance_receiver(class_name, lexical_scope, context, values)? + { + return eval_native_method_with_evaluated_args_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + context, + values, + ) + .map(Some); + } + return eval_throw_non_static_method_call_error( + &declaring_class, + method_name, + context, + values, + ); + } + eval_native_static_method_with_evaluated_args_bridge_scope( + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + context, + values, + ) + .map(Some) +} + +/// Returns `$this` when PHP permits static-call syntax to target an instance method. +fn eval_static_syntax_instance_receiver( + class_name: &str, + lexical_scope: Option<&ElephcEvalScope>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(scope) = lexical_scope else { + return Ok(None); + }; + let Some(object) = visible_scope_cell(context, scope, "this") else { + return Ok(None); + }; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Ok(None); + } + let object_class_name = eval_static_syntax_object_class_name(object, context, values)?; + if eval_static_syntax_object_matches_class(&object_class_name, class_name, context) { + Ok(Some(object)) + } else { + Ok(None) + } +} + +/// Resolves the PHP-visible class name for the current static-syntax `$this` object. +fn eval_static_syntax_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + } + runtime_object_class_name(object, values) +} + +/// Returns whether `$this` is an instance of the class named by static-call syntax. +fn eval_static_syntax_object_matches_class( + object_class_name: &str, + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + same_eval_class_name(object_class_name, class_name) + || context.class_is_a(object_class_name, class_name, false) + || native_class_is_a(object_class_name, class_name, context) +} + /// Dispatches static methods for eval's builtin `PropertyHookType` enum slice. fn eval_builtin_property_hook_type_static_method_result( class_name: &str, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 6efa0fa0c0..8a236bed2a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8229,6 +8229,62 @@ $box->run();'); assert_eq!(out.stdout, "6"); } +/// Verifies `parent::` in eval children can call inherited non-static AOT methods on `$this`. +#[test] +fn test_eval_declared_child_parent_static_syntax_calls_aot_instance_method() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "parent"); +} + +/// Verifies `parent::` in eval children bridges protected static AOT method scope. +#[test] +fn test_eval_declared_child_parent_static_syntax_calls_aot_static_method() { + let out = compile_and_run_capture( + r#"run();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "parent-static"); +} + /// Verifies eval-declared children can read and write inherited protected AOT properties. #[test] fn test_eval_declared_child_accesses_inherited_protected_aot_property() { From 8b29ce1c80f6f169e0748e7c6a89ba53d5fe173c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 20:16:42 +0200 Subject: [PATCH 0890/1208] Cover eval static syntax instance calls --- tests/codegen/eval.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8a236bed2a..23ec41cf30 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -20805,6 +20805,27 @@ return $box->x;'); assert_eq!(out, "DynEvalSupported:9:Y10:12:12"); } +/// Verifies eval-declared methods support PHP static syntax for `$this` instance calls. +#[test] +fn test_eval_declared_class_static_syntax_calls_instance_methods() { + let out = compile_and_run( + r#"run();'); +"#, + ); + assert_eq!(out, "base:child"); +} + /// Verifies native object construction can use eval-declared classes after the barrier. #[test] fn test_eval_declared_class_constructs_object_natively_after_barrier() { From 25df41b8862f3dfc3b99a300cad107e8af8d882a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 20:19:59 +0200 Subject: [PATCH 0891/1208] Cover AOT non-static static syntax errors --- tests/codegen/eval.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 23ec41cf30..bcdcc37a5d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6739,6 +6739,30 @@ echo EvalAotStaticBox::sum4(1, 2, 3, 4);'); assert_eq!(out.stdout, "AB:CD:EF:7:10"); } +/// Verifies eval reports PHP's catchable Error for static syntax on non-static AOT methods. +#[test] +fn test_eval_fragment_rejects_aot_non_static_method_called_statically() { + let out = compile_and_run( + r#"getMessage(); +} +"#, + ); + assert_eq!( + out, + "Non-static method EvalAotNonStaticStaticSyntaxBox::run() cannot be called statically" + ); +} + /// Verifies eval fragments can call private AOT static methods from the declaring scope. #[test] fn test_eval_fragment_can_call_private_aot_static_method_from_declaring_scope() { From c64ed884483a194bba79895dbee6a09f385c0796 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 20:43:39 +0200 Subject: [PATCH 0892/1208] Capture AOT method scope for eval callables --- crates/elephc-magician/src/context.rs | 47 +++++++++++++ crates/elephc-magician/src/eval_ir.rs | 4 ++ .../interpreter/builtins/registry/callable.rs | 62 ++++++++++++++--- .../builtins/registry/callable_validation.rs | 14 ++-- .../src/interpreter/builtins/symbols.rs | 13 +++- .../src/interpreter/control.rs | 2 + .../src/interpreter/expressions.rs | 68 +++++++++++++++++++ .../src/interpreter/statements.rs | 2 +- .../elephc-magician/src/parser/expressions.rs | 14 +++- .../elephc-magician/src/parser/statements.rs | 4 ++ .../src/parser/tests/arrays_objects.rs | 10 +-- tests/codegen/eval.rs | 36 ++++++++++ 12 files changed, 250 insertions(+), 26 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index af3b49edd1..3a7fb3e717 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -172,6 +172,15 @@ struct EvalStaticCallableMetadata { called_class: String, } +/// Native instance-method dispatch metadata attached to eval-created method callables. +#[derive(Clone)] +struct EvalObjectCallableMetadata { + object: usize, + method: String, + native_class: String, + bridge_scope: String, +} + /// Native AOT function callback metadata visible to runtime eval fragments. #[derive(Clone)] pub struct NativeFunction { @@ -668,6 +677,7 @@ pub struct ElephcEvalContext { eval_dynamic_reflection_properties: HashSet, eval_reflection_class_constants: HashMap, eval_static_callables: HashMap, + eval_object_callables: HashMap, global_scope: Option<*mut ElephcEvalScope>, function_stack: Vec, class_stack: Vec, @@ -733,6 +743,7 @@ impl ElephcEvalContext { eval_dynamic_reflection_properties: HashSet::new(), eval_reflection_class_constants: HashMap::new(), eval_static_callables: HashMap::new(), + eval_object_callables: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -799,6 +810,7 @@ impl ElephcEvalContext { eval_dynamic_reflection_properties: HashSet::new(), eval_reflection_class_constants: HashMap::new(), eval_static_callables: HashMap::new(), + eval_object_callables: HashMap::new(), global_scope: None, function_stack: Vec::new(), class_stack: Vec::new(), @@ -1004,6 +1016,41 @@ impl ElephcEvalContext { .then_some(metadata.called_class.as_str()) } + /// Registers one eval-created object method callable with native bridge metadata. + pub fn register_eval_object_callable( + &mut self, + callable: RuntimeCellHandle, + object: RuntimeCellHandle, + method: &str, + native_class: &str, + bridge_scope: &str, + ) { + self.eval_object_callables.insert( + callable.as_ptr() as usize, + EvalObjectCallableMetadata { + object: object.as_ptr() as usize, + method: method.to_string(), + native_class: native_class.trim_start_matches('\\').to_string(), + bridge_scope: bridge_scope.trim_start_matches('\\').to_string(), + }, + ); + } + + /// Returns native method bridge metadata captured for one object callable array. + pub fn eval_object_callable_native_dispatch( + &self, + callable: RuntimeCellHandle, + object: RuntimeCellHandle, + method: &str, + ) -> Option<(&str, &str)> { + let metadata = self + .eval_object_callables + .get(&(callable.as_ptr() as usize))?; + (metadata.object == object.as_ptr() as usize + && metadata.method.eq_ignore_ascii_case(method)) + .then_some((metadata.native_class.as_str(), metadata.bridge_scope.as_str())) + } + /// Resolves a PHP class-like name to eval class, interface, trait, or alias spelling. pub fn resolve_class_like_name(&self, name: &str) -> Option { let key = normalize_class_name(name); diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 7e70ec3da2..71fa92b2b7 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2524,6 +2524,10 @@ pub enum EvalExpr { name: String, fallback_name: Option, }, + MethodCallable { + object: Box, + method: Box, + }, StaticMethodCallable { class_name: String, method: Box, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 64c6e0c171..ae7f96dad4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -164,10 +164,22 @@ pub(in crate::interpreter) fn eval_array_callable( let method = String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; match values.type_tag(receiver)? { - EVAL_TAG_OBJECT => Ok(EvaluatedCallable::ObjectMethod { - object: receiver, - method, - }), + EVAL_TAG_OBJECT => { + let native_dispatch = context + .eval_object_callable_native_dispatch(callback, receiver, &method) + .map(|(native_class, bridge_scope)| { + (native_class.to_string(), bridge_scope.to_string()) + }); + let (native_class, bridge_scope) = native_dispatch + .map(|(native_class, bridge_scope)| (Some(native_class), Some(bridge_scope))) + .unwrap_or((None, None)); + Ok(EvaluatedCallable::ObjectMethod { + object: receiver, + method, + native_class, + bridge_scope, + }) + } EVAL_TAG_STRING => { let class_name = String::from_utf8(values.string_bytes(receiver)?) .map_err(|_| EvalStatus::RuntimeFatal)?; @@ -239,9 +251,23 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( values, ) } - EvaluatedCallable::ObjectMethod { object, method } => { - eval_method_call_result(*object, method, evaluated_args, context, values) - } + EvaluatedCallable::ObjectMethod { + object, + method, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => eval_native_method_with_evaluated_args_unchecked_bridge_scope( + *object, + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + context, + values, + ), + None => eval_method_call_result(*object, method, evaluated_args, context, values), + }, EvaluatedCallable::StaticMethod { class_name, method, @@ -280,15 +306,29 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( EvaluatedCallable::InvokableObject { object } => { eval_invokable_object_call_result(*object, evaluated_args, context, values) } - EvaluatedCallable::ObjectMethod { object, method } => { - eval_method_call_result_with_evaluated_args( + EvaluatedCallable::ObjectMethod { + object, + method, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => eval_native_method_with_evaluated_args_unchecked_bridge_scope( *object, + native_class, method, evaluated_args, + bridge_scope.as_deref(), context, values, - ) - } + ), + None => eval_method_call_result_with_evaluated_args( + *object, + method, + evaluated_args, + context, + values, + ), + }, EvaluatedCallable::StaticMethod { class_name, method, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index c1da696422..ea7ac7fec5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -19,15 +19,21 @@ pub(in crate::interpreter) fn eval_validate_call_user_func_callback( values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { match callback { - EvaluatedCallable::ObjectMethod { object, method } => { - eval_validate_call_user_func_object_method( + EvaluatedCallable::ObjectMethod { + object, + method, + native_class, + .. + } => match native_class { + Some(_) => Ok(()), + None => eval_validate_call_user_func_object_method( *object, method, function_name, context, values, - ) - } + ), + }, EvaluatedCallable::StaticMethod { class_name, method, .. } => { diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index f9d880d6fa..5f6b392de7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -996,8 +996,17 @@ fn eval_callable_probe_exists( EvaluatedCallable::InvokableObject { object } => { eval_object_method_callable_probe(*object, "__invoke", context, values) } - EvaluatedCallable::ObjectMethod { object, method } => { - eval_object_method_callable_probe(*object, method, context, values) + EvaluatedCallable::ObjectMethod { + object, + method, + native_class, + .. + } => { + if native_class.is_some() { + Ok(true) + } else { + eval_object_method_callable_probe(*object, method, context, values) + } } EvaluatedCallable::StaticMethod { class_name, method, .. diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index 4b0f5eeb00..1e76f807a3 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -52,6 +52,8 @@ pub(super) enum EvaluatedCallable { ObjectMethod { object: RuntimeCellHandle, method: String, + native_class: Option, + bridge_scope: Option, }, StaticMethod { class_name: String, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 86b9f6cf43..6df0477d6b 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -42,6 +42,9 @@ pub(in crate::interpreter) fn eval_expr( name, fallback_name, } => eval_function_callable_expr(name, fallback_name.as_deref(), context, values), + EvalExpr::MethodCallable { object, method } => { + eval_method_callable_expr(object, method, context, scope, values) + } EvalExpr::StaticMethodCallable { class_name, method } => { eval_static_method_callable_expr(class_name, method, context, scope, values) } @@ -785,6 +788,71 @@ fn eval_function_callable_expr( ) } +/// Materializes an object method first-class callable and records captured AOT bridge scope. +fn eval_method_callable_expr( + object: &EvalExpr, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_expr(object, context, scope, values)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let mut array = values.array_new(2)?; + let zero = values.int(0)?; + let one = values.int(1)?; + let method_value = values.string(&method)?; + array = values.array_set(array, zero, object)?; + array = values.array_set(array, one, method_value)?; + if let Some((native_class, bridge_scope)) = + eval_method_callable_native_dispatch(object, &method, context, values)? + { + context.register_eval_object_callable( + array, + object, + &method, + &native_class, + &bridge_scope, + ); + } + Ok(array) +} + +/// Finds inherited AOT method dispatch metadata captured by an object method callable. +fn eval_method_callable_native_dispatch( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(None); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(None); + }; + let called_class_name = class.name(); + if eval_dynamic_method_for_call(called_class_name, method_name, context).is_some() { + return Ok(None); + } + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + // First-class callables may outlive this scope, so capture only after access is valid now. + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return Ok(None); + } + Ok(Some((parent, declaring_class))) +} + /// Materializes a first-class static method callable while retaining late-static metadata. fn eval_static_method_callable_expr( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index ca4e1daa71..292e5d27aa 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -10356,7 +10356,7 @@ fn eval_native_method_with_evaluated_args_unchecked( } /// Calls one generated/AOT instance method without visibility checks using an optional bridge scope. -fn eval_native_method_with_evaluated_args_unchecked_bridge_scope( +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_bridge_scope( object: RuntimeCellHandle, class_name: &str, method_name: &str, diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 550f6d3be5..78b0c88244 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -561,7 +561,7 @@ impl Parser { if nullsafe { return Err(EvalParseError::UnsupportedConstruct); } - return Ok(Self::callable_array_expr( + return Ok(Self::method_callable_expr( object, EvalExpr::Const(EvalConst::String(member)), )); @@ -606,7 +606,7 @@ impl Parser { if nullsafe { return Err(EvalParseError::UnsupportedConstruct); } - return Ok(Self::callable_array_expr(object, member)); + return Ok(Self::method_callable_expr(object, member)); } let args = self.parse_call_args()?; return Ok(if nullsafe { @@ -1273,7 +1273,15 @@ impl Parser { } } - /// Builds the PHP callable-array value used for method first-class callables. + /// Builds the EvalIR node used for object method first-class callables. + fn method_callable_expr(object: EvalExpr, method: EvalExpr) -> EvalExpr { + EvalExpr::MethodCallable { + object: Box::new(object), + method: Box::new(method), + } + } + + /// Builds a plain PHP callable-array value for dynamic static callable forms. fn callable_array_expr(receiver: EvalExpr, method: EvalExpr) -> EvalExpr { EvalExpr::Array(vec![ EvalArrayElement::Value(receiver), diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 215f267ce7..62072ffd60 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -4216,6 +4216,10 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { | EvalExpr::Magic(_) | EvalExpr::NamespacedConstFetch { .. } | EvalExpr::StaticPropertyGet { .. } => false, + EvalExpr::MethodCallable { object, method } => { + eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(method, property_name) + } EvalExpr::StaticMethodCallable { method, .. } => { eval_expr_uses_this_property(method, property_name) } diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index 2c95c6db1e..3abee204ae 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -135,16 +135,16 @@ fn parse_fragment_accepts_method_call_source() { }))] ); } -/// Verifies first-class object method syntax lowers to a PHP callable array. +/// Verifies first-class object method syntax keeps callable-capture semantics in EvalIR. #[test] fn parse_fragment_accepts_first_class_method_callable_source() { let program = parse_fragment(br#"return $this->Answer(...);"#).expect("fragment should parse"); assert_eq!( program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::LoadVar("this".to_string())), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::String("Answer".to_string()))), - ])))] + &[EvalStmt::Return(Some(EvalExpr::MethodCallable { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Answer".to_string()))), + }))] ); } /// Verifies braced dynamic object property reads parse as runtime-name EvalIR expressions. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index bcdcc37a5d..fb725db16c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8458,6 +8458,42 @@ echo call_user_func_array([$box, "read"], ["value" => "L"]);'); ); } +/// Verifies eval first-class callables retain access to inherited protected AOT methods. +#[test] +fn test_eval_declared_child_first_class_callable_inherited_protected_aot_method() { + let out = compile_and_run_capture( + r#"hidden(...); + } +} +$box = new EvalProtectedCallableChild(); +$public = $box->read(...); +echo $public("A") . ":"; +$hidden = $box->makeHidden(); +echo is_callable($hidden) ? "callable:" : "bad:"; +echo call_user_func($hidden, "B") . ":"; +echo $hidden("C");'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "P:A:callable:H:B:H:C"); +} + /// Verifies eval-declared classes expose inherited AOT members to OOP introspection. #[test] fn test_eval_declared_class_inherits_aot_parent_member_introspection() { From 713c185aa8953ea4fe4256badf5655c19e888fb2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 20:54:56 +0200 Subject: [PATCH 0893/1208] Expose inherited AOT __callStatic to eval callbacks --- .../builtins/registry/callable_validation.rs | 5 ++++ .../src/interpreter/builtins/symbols.rs | 8 ++++- tests/codegen/eval.rs | 30 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index ea7ac7fec5..f26745da51 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -243,6 +243,11 @@ fn eval_validate_call_user_func_static_method( if eval_call_user_func_static_magic_callable(&class_name, context) { return Ok(()); } + if let Some(parent) = context.class_native_parent_name(&class_name) { + if eval_call_user_func_native_static_magic_callable(&parent, context, values)? { + return Ok(()); + } + } return eval_call_user_func_missing_method_type_error( function_name, &class_name, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 5f6b392de7..f0acc53d87 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -1082,7 +1082,13 @@ fn eval_static_method_callable_probe( || context.has_trait(&class_name) || context.has_enum(&class_name) { - return Ok(eval_static_magic_method_callable_probe(&class_name, context)); + if eval_static_magic_method_callable_probe(&class_name, context) { + return Ok(true); + } + if let Some(parent) = context.class_native_parent_name(&class_name) { + return eval_aot_static_magic_method_callable_probe(&parent, context, values); + } + return Ok(false); } eval_aot_static_method_callable_probe(&class_name, method_name, context, values) } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index fb725db16c..37111eac79 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12546,6 +12546,36 @@ echo call_user_func(["EvalAotMagicStaticBox", "Hidden"], "K", "L");'); ); } +/// Verifies eval-declared subclasses expose inherited AOT `__callStatic` to callback probes. +#[test] +fn test_eval_declared_child_inherits_aot_magic_call_static_callbacks() { + let out = compile_and_run_capture( + r#" Date: Sat, 27 Jun 2026 21:15:34 +0200 Subject: [PATCH 0894/1208] Bridge inherited AOT static callbacks for eval subclasses --- crates/elephc-magician/src/context.rs | 33 +++++++ .../interpreter/builtins/registry/callable.rs | 98 ++++++++++++++----- .../builtins/registry/callable_validation.rs | 80 +++++++++++++-- .../src/interpreter/builtins/symbols.rs | 18 +++- .../src/interpreter/control.rs | 2 + .../src/interpreter/expressions.rs | 52 ++++++++++ .../src/interpreter/statements.rs | 2 +- tests/codegen/eval.rs | 91 +++++++++++++++++ 8 files changed, 337 insertions(+), 39 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 3a7fb3e717..9d8743d376 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -170,6 +170,8 @@ struct EvalStaticCallableMetadata { class_name: String, method: String, called_class: String, + native_class: Option, + bridge_scope: Option, } /// Native instance-method dispatch metadata attached to eval-created method callables. @@ -991,13 +993,24 @@ impl ElephcEvalContext { class_name: &str, method: &str, called_class: &str, + native_dispatch: Option<(&str, &str)>, ) { + let (native_class, bridge_scope) = native_dispatch + .map(|(native_class, bridge_scope)| { + ( + Some(native_class.trim_start_matches('\\').to_string()), + Some(bridge_scope.trim_start_matches('\\').to_string()), + ) + }) + .unwrap_or((None, None)); self.eval_static_callables.insert( callable.as_ptr() as usize, EvalStaticCallableMetadata { class_name: class_name.trim_start_matches('\\').to_string(), method: method.to_string(), called_class: called_class.trim_start_matches('\\').to_string(), + native_class, + bridge_scope, }, ); } @@ -1016,6 +1029,26 @@ impl ElephcEvalContext { .then_some(metadata.called_class.as_str()) } + /// Returns native method bridge metadata captured for one static callable array. + pub fn eval_static_callable_native_dispatch( + &self, + callable: RuntimeCellHandle, + class_name: &str, + method: &str, + ) -> Option<(&str, &str)> { + let metadata = self.eval_static_callables.get(&(callable.as_ptr() as usize))?; + let class_name = class_name.trim_start_matches('\\'); + if !metadata.class_name.eq_ignore_ascii_case(class_name) + || !metadata.method.eq_ignore_ascii_case(method) + { + return None; + } + Some(( + metadata.native_class.as_deref()?, + metadata.bridge_scope.as_deref()?, + )) + } + /// Registers one eval-created object method callable with native bridge metadata. pub fn register_eval_object_callable( &mut self, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index ae7f96dad4..ef2d2f5538 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -186,10 +186,20 @@ pub(in crate::interpreter) fn eval_array_callable( let called_class = context .eval_static_callable_called_class(callback, &class_name, &method) .map(str::to_string); + let native_dispatch = context + .eval_static_callable_native_dispatch(callback, &class_name, &method) + .map(|(native_class, bridge_scope)| { + (native_class.to_string(), bridge_scope.to_string()) + }); + let (native_class, bridge_scope) = native_dispatch + .map(|(native_class, bridge_scope)| (Some(native_class), Some(bridge_scope))) + .unwrap_or((None, None)); Ok(EvaluatedCallable::StaticMethod { class_name, method, called_class, + native_class, + bridge_scope, }) } _ => Err(EvalStatus::UnsupportedConstruct), @@ -211,6 +221,8 @@ pub(in crate::interpreter) fn eval_string_callable( class_name: class_name.trim_start_matches('\\').to_string(), method: method.to_string(), called_class: None, + native_class: None, + bridge_scope: None, }); } Ok(EvaluatedCallable::Named( @@ -272,22 +284,36 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( class_name, method, called_class, - } => match called_class { - Some(called_class) => eval_static_method_call_result_with_called_class( - class_name, - called_class, - method, - positional_args(evaluated_args), - context, - values, - ), - None => eval_static_method_call_result( - class_name, - method, - positional_args(evaluated_args), - context, - values, - ), + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + context, + values, + ) + } + None => match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method, + positional_args(evaluated_args), + context, + values, + ), + None => eval_static_method_call_result( + class_name, + method, + positional_args(evaluated_args), + context, + values, + ), + }, }, } } @@ -333,18 +359,36 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( class_name, method, called_class, - } => match called_class { - Some(called_class) => eval_static_method_call_result_with_called_class( - class_name, - called_class, - method, - evaluated_args, - context, - values, - ), - None => { - eval_static_method_call_result(class_name, method, evaluated_args, context, values) + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + context, + values, + ) } + None => match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method, + evaluated_args, + context, + values, + ), + None => eval_static_method_call_result( + class_name, + method, + evaluated_args, + context, + values, + ), + }, }, } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index f26745da51..b17e8e3209 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -35,16 +35,20 @@ pub(in crate::interpreter) fn eval_validate_call_user_func_callback( ), }, EvaluatedCallable::StaticMethod { - class_name, method, .. - } => { - eval_validate_call_user_func_static_method( + class_name, + method, + native_class, + .. + } => match native_class { + Some(_) => Ok(()), + None => eval_validate_call_user_func_static_method( class_name, method, function_name, context, values, - ) - } + ), + }, EvaluatedCallable::Named(_) | EvaluatedCallable::InvokableObject { .. } => Ok(()), } } @@ -244,9 +248,14 @@ fn eval_validate_call_user_func_static_method( return Ok(()); } if let Some(parent) = context.class_native_parent_name(&class_name) { - if eval_call_user_func_native_static_magic_callable(&parent, context, values)? { - return Ok(()); - } + return eval_validate_call_user_func_native_static_method_for_class( + &parent, + &class_name, + method_name, + function_name, + context, + values, + ); } return eval_call_user_func_missing_method_type_error( function_name, @@ -319,6 +328,61 @@ fn eval_validate_call_user_func_native_static_method( ) } +/// Validates generated/AOT static-method callbacks while preserving eval-class error names. +fn eval_validate_call_user_func_native_static_method_for_class( + class_name: &str, + error_class_name: &str, + method_name: &str, + function_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + if eval_call_user_func_native_static_magic_callable(class_name, context, values)? + || context + .native_static_method_signature(class_name, method_name) + .is_some() + || !values.class_exists(class_name)? + { + return Ok(()); + } + return eval_call_user_func_missing_method_type_error( + function_name, + error_class_name, + method_name, + context, + values, + ); + }; + if !is_static { + return eval_call_user_func_non_static_method_type_error( + function_name, + &declaring_class, + method_name, + context, + values, + ); + } + if is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_call_user_func_native_static_magic_callable(class_name, context, values)? + { + return Ok(()); + } + eval_call_user_func_method_access_type_error( + function_name, + &declaring_class, + method_name, + visibility, + context, + values, + ) +} + /// Returns whether an eval class has an instance magic-call fallback. fn eval_call_user_func_instance_magic_callable( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index f0acc53d87..13f0bcb6ea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -1009,9 +1009,16 @@ fn eval_callable_probe_exists( } } EvaluatedCallable::StaticMethod { - class_name, method, .. + class_name, + method, + native_class, + .. } => { - eval_static_method_callable_probe(class_name, method, context, values) + if native_class.is_some() { + Ok(true) + } else { + eval_static_method_callable_probe(class_name, method, context, values) + } } } } @@ -1086,7 +1093,12 @@ fn eval_static_method_callable_probe( return Ok(true); } if let Some(parent) = context.class_native_parent_name(&class_name) { - return eval_aot_static_magic_method_callable_probe(&parent, context, values); + return eval_aot_static_method_callable_probe( + &parent, + method_name, + context, + values, + ); } return Ok(false); } diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index 1e76f807a3..f4d0c6b6f5 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -59,6 +59,8 @@ pub(super) enum EvaluatedCallable { class_name: String, method: String, called_class: Option, + native_class: Option, + bridge_scope: Option, }, } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 6df0477d6b..fbfb5ee4b2 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -870,15 +870,67 @@ fn eval_static_method_callable_expr( let method_value = values.string(&method)?; array = values.array_set(array, zero, class_value)?; array = values.array_set(array, one, method_value)?; + let native_dispatch = eval_static_method_callable_native_dispatch( + &receiver.dispatch_class, + &method, + context, + values, + )?; context.register_eval_static_callable( array, &receiver.dispatch_class, &method, &receiver.called_class, + native_dispatch + .as_ref() + .map(|(native_class, bridge_scope)| (native_class.as_str(), bridge_scope.as_str())), ); Ok(array) } +/// Finds inherited or direct AOT static dispatch metadata captured by a static callable. +fn eval_static_method_callable_native_dispatch( + dispatch_class: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.class_method(dispatch_class, method_name).is_some() { + return Ok(None); + } + let native_class = if context.has_class(dispatch_class) { + let Some(parent) = context.class_native_parent_name(dispatch_class) else { + return Ok(None); + }; + parent + } else if context.has_interface(dispatch_class) + || context.has_trait(dispatch_class) + || context.has_enum(dispatch_class) + { + return Ok(None); + } else { + dispatch_class.trim_start_matches('\\').to_string() + }; + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + method_name, + context, + values, + )? + else { + return Ok(None); + }; + if !is_static || is_abstract { + return Ok(None); + } + // First-class callables may outlive this scope, so capture only after access is valid now. + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return Ok(None); + } + Ok(Some((native_class, declaring_class))) +} + /// Evaluates a variable or expression callable and dispatches it with source-order arguments. pub(in crate::interpreter) fn eval_dynamic_call( callee: &EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 292e5d27aa..184126b8ca 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -10480,7 +10480,7 @@ fn eval_native_static_method_with_evaluated_args_unchecked( } /// Calls one generated/AOT static method without visibility checks using an optional bridge scope. -fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( class_name: &str, method_name: &str, evaluated_args: Vec, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 37111eac79..3c6aaf8fe2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12576,6 +12576,97 @@ echo call_user_func_array($callback, ["D"]);'); ); } +/// Verifies inherited native `__callStatic` does not mask inherited non-static AOT methods. +#[test] +fn test_eval_declared_child_aot_magic_call_static_does_not_mask_non_static_callbacks() { + let out = compile_and_run_capture( + r#" Date: Sat, 27 Jun 2026 21:47:04 +0200 Subject: [PATCH 0895/1208] Preserve eval child late-static scope across AOT eval frames --- crates/elephc-magician/src/context.rs | 101 +++++++++++++++++- crates/elephc-magician/src/ffi/context.rs | 3 + .../interpreter/builtins/registry/callable.rs | 23 +++- .../src/interpreter/control.rs | 1 + .../src/interpreter/expressions.rs | 7 +- .../src/interpreter/statements.rs | 73 ++++++++++--- tests/codegen/eval.rs | 66 ++++++++++++ 7 files changed, 251 insertions(+), 23 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 9d8743d376..ffaed8f46d 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -11,6 +11,7 @@ //! - The handle is intentionally opaque to generated code. //! - No Rust-owned layout is promised across the C ABI. +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::ffi::c_void; #[cfg(not(test))] @@ -29,6 +30,66 @@ use crate::value::{RuntimeCell, RuntimeCellHandle}; #[cfg(not(test))] static GLOBAL_EVAL_CLASSES: OnceLock> = OnceLock::new(); +thread_local! { + static NATIVE_FRAME_CALLED_CLASS_OVERRIDES: RefCell> = + RefCell::new(Vec::new()); +} + +/// Late-static override installed while eval dispatches into a generated/AOT frame. +#[derive(Clone)] +struct NativeFrameCalledClassOverride { + frame_class: String, + called_class: String, +} + +/// Scoped guard that removes one native-frame called-class override on drop. +pub(crate) struct NativeFrameCalledClassOverrideGuard; + +/// Installs a late-static called-class override for a generated/AOT frame call. +pub(crate) fn push_native_frame_called_class_override( + frame_class: &str, + called_class: &str, +) -> NativeFrameCalledClassOverrideGuard { + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides + .borrow_mut() + .push(NativeFrameCalledClassOverride { + frame_class: frame_class.trim_start_matches('\\').to_string(), + called_class: called_class.trim_start_matches('\\').to_string(), + }); + }); + NativeFrameCalledClassOverrideGuard +} + +impl Drop for NativeFrameCalledClassOverrideGuard { + /// Removes the most recent native-frame called-class override. + fn drop(&mut self) { + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides.borrow_mut().pop(); + }); + } +} + +/// Returns the active thread-local late-static override for one generated/AOT frame. +fn native_frame_called_class_override( + frame_class: &str, + called_class: &str, +) -> Option { + let frame_class = frame_class.trim_start_matches('\\'); + let called_class = called_class.trim_start_matches('\\'); + if frame_class.is_empty() || !called_class.eq_ignore_ascii_case(frame_class) { + return None; + } + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides + .borrow() + .iter() + .rev() + .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) + .map(|entry| entry.called_class.clone()) + }) +} + #[cfg(not(test))] #[derive(Default)] struct GlobalEvalClassRegistry { @@ -179,6 +240,7 @@ struct EvalStaticCallableMetadata { struct EvalObjectCallableMetadata { object: usize, method: String, + called_class: String, native_class: String, bridge_scope: String, } @@ -1055,6 +1117,7 @@ impl ElephcEvalContext { callable: RuntimeCellHandle, object: RuntimeCellHandle, method: &str, + called_class: &str, native_class: &str, bridge_scope: &str, ) { @@ -1063,6 +1126,7 @@ impl ElephcEvalContext { EvalObjectCallableMetadata { object: object.as_ptr() as usize, method: method.to_string(), + called_class: called_class.trim_start_matches('\\').to_string(), native_class: native_class.trim_start_matches('\\').to_string(), bridge_scope: bridge_scope.trim_start_matches('\\').to_string(), }, @@ -1075,13 +1139,17 @@ impl ElephcEvalContext { callable: RuntimeCellHandle, object: RuntimeCellHandle, method: &str, - ) -> Option<(&str, &str)> { + ) -> Option<(&str, &str, &str)> { let metadata = self .eval_object_callables .get(&(callable.as_ptr() as usize))?; (metadata.object == object.as_ptr() as usize && metadata.method.eq_ignore_ascii_case(method)) - .then_some((metadata.native_class.as_str(), metadata.bridge_scope.as_str())) + .then_some(( + metadata.native_class.as_str(), + metadata.bridge_scope.as_str(), + metadata.called_class.as_str(), + )) } /// Resolves a PHP class-like name to eval class, interface, trait, or alias spelling. @@ -3382,6 +3450,35 @@ impl ElephcEvalContext { self.called_class_stack.last().map(String::as_str) } + /// Returns a dynamic called-class override for a generated/AOT frame entering eval. + pub fn native_frame_called_class_override( + &self, + class_name: &str, + called_class_name: &str, + ) -> Option { + let class_name = class_name.trim_start_matches('\\'); + let called_class_name = called_class_name.trim_start_matches('\\'); + if class_name.is_empty() || !called_class_name.eq_ignore_ascii_case(class_name) { + return None; + } + if let Some(called_class) = + native_frame_called_class_override(class_name, called_class_name) + { + return Some(called_class); + } + let active = self.current_called_class_scope()?.trim_start_matches('\\'); + if active.is_empty() || active.eq_ignore_ascii_case(class_name) { + return None; + } + let active = self + .resolve_class_name(active) + .unwrap_or_else(|| active.to_string()); + self.class_parent_names(&active) + .iter() + .any(|parent| parent.eq_ignore_ascii_case(class_name)) + .then_some(active) + } + /// Pushes PHP-visible method magic constants for the current eval method frame. pub fn push_method_magic_scope(&mut self, class_name: &str, method: &EvalClassMethod) { self.magic_stack.push(EvalMagicScope { diff --git a/crates/elephc-magician/src/ffi/context.rs b/crates/elephc-magician/src/ffi/context.rs index b82714d95e..c4c029765c 100644 --- a/crates/elephc-magician/src/ffi/context.rs +++ b/crates/elephc-magician/src/ffi/context.rs @@ -203,6 +203,9 @@ unsafe fn eval_context_push_class_scope_inner( } else { called_class_name.to_string() }; + let called_class_name = context + .native_frame_called_class_override(&class_name, &called_class_name) + .unwrap_or(called_class_name); context.push_class_scope(class_name); context.push_called_class_scope(called_class_name); EvalStatus::Ok.code() diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index ef2d2f5538..3bb4ddf0e6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -167,15 +167,22 @@ pub(in crate::interpreter) fn eval_array_callable( EVAL_TAG_OBJECT => { let native_dispatch = context .eval_object_callable_native_dispatch(callback, receiver, &method) - .map(|(native_class, bridge_scope)| { - (native_class.to_string(), bridge_scope.to_string()) + .map(|(native_class, bridge_scope, called_class)| { + ( + native_class.to_string(), + bridge_scope.to_string(), + called_class.to_string(), + ) }); - let (native_class, bridge_scope) = native_dispatch - .map(|(native_class, bridge_scope)| (Some(native_class), Some(bridge_scope))) - .unwrap_or((None, None)); + let (native_class, bridge_scope, called_class) = native_dispatch + .map(|(native_class, bridge_scope, called_class)| { + (Some(native_class), Some(bridge_scope), Some(called_class)) + }) + .unwrap_or((None, None, None)); Ok(EvaluatedCallable::ObjectMethod { object: receiver, method, + called_class, native_class, bridge_scope, }) @@ -266,6 +273,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( EvaluatedCallable::ObjectMethod { object, method, + called_class, native_class, bridge_scope, } => match native_class { @@ -275,6 +283,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( method, positional_args(evaluated_args), bridge_scope.as_deref(), + called_class.as_deref(), context, values, ), @@ -293,6 +302,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( method, positional_args(evaluated_args), bridge_scope.as_deref(), + called_class.as_deref(), context, values, ) @@ -335,6 +345,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( EvaluatedCallable::ObjectMethod { object, method, + called_class, native_class, bridge_scope, } => match native_class { @@ -344,6 +355,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( method, evaluated_args, bridge_scope.as_deref(), + called_class.as_deref(), context, values, ), @@ -368,6 +380,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( method, evaluated_args, bridge_scope.as_deref(), + called_class.as_deref(), context, values, ) diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index f4d0c6b6f5..cadbb2a877 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -52,6 +52,7 @@ pub(super) enum EvaluatedCallable { ObjectMethod { object: RuntimeCellHandle, method: String, + called_class: Option, native_class: Option, bridge_scope: Option, }, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index fbfb5ee4b2..1f41f7c846 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -804,13 +804,14 @@ fn eval_method_callable_expr( let method_value = values.string(&method)?; array = values.array_set(array, zero, object)?; array = values.array_set(array, one, method_value)?; - if let Some((native_class, bridge_scope)) = + if let Some((native_class, bridge_scope, called_class)) = eval_method_callable_native_dispatch(object, &method, context, values)? { context.register_eval_object_callable( array, object, &method, + &called_class, &native_class, &bridge_scope, ); @@ -824,7 +825,7 @@ fn eval_method_callable_native_dispatch( method_name: &str, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let Ok(identity) = values.object_identity(object) else { return Ok(None); }; @@ -850,7 +851,7 @@ fn eval_method_callable_native_dispatch( if validate_eval_member_access(&declaring_class, visibility, context).is_err() { return Ok(None); } - Ok(Some((parent, declaring_class))) + Ok(Some((parent, declaring_class, called_class_name.to_string()))) } /// Materializes a first-class static method callable while retaining late-static metadata. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 184126b8ca..2c6f91ac26 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -12,6 +12,7 @@ use super::*; use crate::context::{ NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, NativeCallableObjectDefaultArg, NativeCallableSignature, + push_native_frame_called_class_override, }; /// Executes statements in source order and propagates the first eval `return`. @@ -7364,17 +7365,20 @@ fn eval_static_method_call_result_resolved( values, ); } - if let Some(parent) = context.class_native_parent_name(&class_name) { - if let Some(result) = eval_native_static_syntax_method_result( - &parent, - method_name, - evaluated_args.clone(), - lexical_scope, - context, - values, - )? - { - return Ok(result); + if context.has_class(&class_name) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some(result) = eval_native_static_syntax_method_result( + &parent, + Some(&called_class_name), + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? + { + return Ok(result); + } } } if context.has_class(&class_name) @@ -7401,6 +7405,7 @@ fn eval_static_method_call_result_resolved( } if let Some(result) = eval_native_static_syntax_method_result( &class_name, + None, method_name, evaluated_args.clone(), lexical_scope, @@ -7421,6 +7426,7 @@ fn eval_static_method_call_result_resolved( /// Dispatches one generated/AOT method reached through PHP static-call syntax. fn eval_native_static_syntax_method_result( class_name: &str, + called_class_scope: Option<&str>, method_name: &str, evaluated_args: Vec, lexical_scope: Option<&ElephcEvalScope>, @@ -7479,6 +7485,7 @@ fn eval_native_static_syntax_method_result( method_name, evaluated_args, Some(&declaring_class), + called_class_scope, context, values, ) @@ -7496,6 +7503,7 @@ fn eval_native_static_syntax_method_result( method_name, evaluated_args, Some(&declaring_class), + called_class_scope, context, values, ) @@ -8078,8 +8086,15 @@ pub(in crate::interpreter) fn eval_object_clone_result( )?; eval_release_value(context, values, result)?; } else if let Some(scope) = dynamic_native_clone_hook_scope { - let result = - eval_native_method_call_with_scope(&scope, clone, "__clone", Vec::new(), context, values)?; + let result = eval_native_method_call_with_scope( + &scope, + None, + clone, + "__clone", + Vec::new(), + context, + values, + )?; values.release(result)?; } } else if should_call_aot_clone_hook { @@ -8112,6 +8127,7 @@ fn eval_dynamic_native_clone_hook_is_callable( /// Calls one generated/AOT method while presenting an explicit PHP class scope to the bridge. fn eval_native_method_call_with_scope( scope: &str, + called_class_scope: Option<&str>, object: RuntimeCellHandle, method_name: &str, evaluated_args: Vec, @@ -8119,7 +8135,15 @@ fn eval_native_method_call_with_scope( values: &mut impl RuntimeValueOps, ) -> Result { context.push_class_scope(scope.to_string()); + if let Some(called_class) = called_class_scope { + context.push_called_class_scope(called_class.to_string()); + } + let _called_class_override = called_class_scope + .map(|called_class| push_native_frame_called_class_override(scope, called_class)); let result = values.method_call(object, method_name, evaluated_args); + if called_class_scope.is_some() { + context.pop_called_class_scope(); + } context.pop_class_scope(); result } @@ -8127,6 +8151,7 @@ fn eval_native_method_call_with_scope( /// Calls one generated/AOT static method while presenting an explicit PHP class scope. fn eval_native_static_method_call_with_scope( scope: &str, + called_class_scope: Option<&str>, class_name: &str, method_name: &str, evaluated_args: Vec, @@ -8134,7 +8159,15 @@ fn eval_native_static_method_call_with_scope( values: &mut impl RuntimeValueOps, ) -> Result { context.push_class_scope(scope.to_string()); + if let Some(called_class) = called_class_scope { + context.push_called_class_scope(called_class.to_string()); + } + let _called_class_override = called_class_scope + .map(|called_class| push_native_frame_called_class_override(scope, called_class)); let result = values.static_method_call(class_name, method_name, evaluated_args); + if called_class_scope.is_some() { + context.pop_called_class_scope(); + } context.pop_class_scope(); result } @@ -8786,6 +8819,7 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( method_name, evaluated_args, Some(&declaring_class), + Some(&called_class_name), context, values, ); @@ -8871,6 +8905,7 @@ pub(in crate::interpreter) fn eval_invokable_object_call_result( "__invoke", evaluated_args, Some(&native_class_name), + Some(&called_class_name), context, values, ); @@ -10275,6 +10310,7 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( method_name, evaluated_args, None, + None, context, values, ) @@ -10287,6 +10323,7 @@ fn eval_native_method_with_evaluated_args_bridge_scope( method_name: &str, evaluated_args: Vec, bridge_scope: Option<&str>, + called_class_scope: Option<&str>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -10330,6 +10367,7 @@ fn eval_native_method_with_evaluated_args_bridge_scope( method_name, evaluated_args, bridge_scope, + called_class_scope, context, values, ) @@ -10350,6 +10388,7 @@ fn eval_native_method_with_evaluated_args_unchecked( method_name, evaluated_args, None, + None, context, values, ) @@ -10362,6 +10401,7 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_b method_name: &str, evaluated_args: Vec, bridge_scope: Option<&str>, + called_class_scope: Option<&str>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -10375,6 +10415,7 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_b let result = if let Some(scope) = bridge_scope { eval_native_method_call_with_scope( scope, + called_class_scope, object, method_name, native_bound_arg_values(&bound_args), @@ -10404,6 +10445,7 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( method_name, evaluated_args, None, + None, context, values, ) @@ -10415,6 +10457,7 @@ fn eval_native_static_method_with_evaluated_args_bridge_scope( method_name: &str, evaluated_args: Vec, bridge_scope: Option<&str>, + called_class_scope: Option<&str>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -10456,6 +10499,7 @@ fn eval_native_static_method_with_evaluated_args_bridge_scope( method_name, evaluated_args, bridge_scope, + called_class_scope, context, values, ) @@ -10474,6 +10518,7 @@ fn eval_native_static_method_with_evaluated_args_unchecked( method_name, evaluated_args, None, + None, context, values, ) @@ -10485,6 +10530,7 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unch method_name: &str, evaluated_args: Vec, bridge_scope: Option<&str>, + called_class_scope: Option<&str>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -10498,6 +10544,7 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unch let result = if let Some(scope) = bridge_scope { eval_native_static_method_call_with_scope( scope, + called_class_scope, class_name, method_name, native_bound_arg_values(&bound_args), diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3c6aaf8fe2..516cd145de 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6356,6 +6356,38 @@ class EvalAotScopeStaticChild extends EvalAotScopeStaticBase { assert_eq!(out, "EvalAotScopeStaticBase:EvalAotScopeStaticChild:child"); } +/// Verifies eval classes keep late-static scope inside inherited AOT eval fragments. +#[test] +fn test_eval_declared_child_inherited_aot_eval_fragment_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceProbe() . "|"; +echo EvalAotEvalScopeChild::staticProbe();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotEvalScopeParent:EvalAotEvalScopeChild:EvalAotEvalScopeChild|\ +EvalAotEvalScopeParent:EvalAotEvalScopeChild:EvalAotEvalScopeChild" + ); +} + /// Verifies eval fragments resolve `parent::` through AOT parent metadata. #[test] fn test_eval_fragment_in_aot_method_resolves_parent_scope() { @@ -8494,6 +8526,40 @@ echo $hidden("C");'); assert_eq!(out.stdout, "P:A:callable:H:B:H:C"); } +/// Verifies first-class AOT object callables keep eval-child late-static scope. +#[test] +fn test_eval_declared_child_first_class_aot_method_callable_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"hiddenScope(...); + } +} +$box = new EvalAotCallableScopeChild(); +$hidden = $box->makeHidden(); +echo $hidden() . "|"; +echo call_user_func($hidden);'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotCallableScopeParent:EvalAotCallableScopeChild:EvalAotCallableScopeChild|\ +EvalAotCallableScopeParent:EvalAotCallableScopeChild:EvalAotCallableScopeChild" + ); +} + /// Verifies eval-declared classes expose inherited AOT members to OOP introspection. #[test] fn test_eval_declared_class_inherits_aot_parent_member_introspection() { From 76117602bfc5aafecc7ad96560bd70baffee74e6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 22:35:42 +0200 Subject: [PATCH 0896/1208] Preserve eval late-static scope in inherited AOT methods --- crates/elephc-magician/src/context.rs | 50 +++++ crates/elephc-magician/src/ffi/context.rs | 57 ++++++ .../src/ffi/object_construction.rs | 87 +++++++++ crates/elephc-magician/src/ffi/tests.rs | 57 +++++- .../src/interpreter/statements.rs | 4 +- src/codegen/lower_inst.rs | 22 ++- src/codegen/lower_inst/builtins.rs | 19 ++ src/codegen/lower_inst/builtins/eval.rs | 173 ++++++++++++++++++ src/codegen/lower_inst/strings.rs | 88 +++++++++ tests/codegen/eval.rs | 67 +++++++ 10 files changed, 620 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index ffaed8f46d..07595076d2 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -38,6 +38,8 @@ thread_local! { /// Late-static override installed while eval dispatches into a generated/AOT frame. #[derive(Clone)] struct NativeFrameCalledClassOverride { + #[cfg_attr(test, allow(dead_code))] + context: *mut ElephcEvalContext, frame_class: String, called_class: String, } @@ -47,6 +49,7 @@ pub(crate) struct NativeFrameCalledClassOverrideGuard; /// Installs a late-static called-class override for a generated/AOT frame call. pub(crate) fn push_native_frame_called_class_override( + context: *mut ElephcEvalContext, frame_class: &str, called_class: &str, ) -> NativeFrameCalledClassOverrideGuard { @@ -54,6 +57,7 @@ pub(crate) fn push_native_frame_called_class_override( overrides .borrow_mut() .push(NativeFrameCalledClassOverride { + context, frame_class: frame_class.trim_start_matches('\\').to_string(), called_class: called_class.trim_start_matches('\\').to_string(), }); @@ -80,6 +84,15 @@ fn native_frame_called_class_override( if frame_class.is_empty() || !called_class.eq_ignore_ascii_case(frame_class) { return None; } + native_frame_called_class_override_for_frame(frame_class) +} + +/// Returns the active called-class override for one generated/AOT frame class. +pub(crate) fn native_frame_called_class_override_for_frame(frame_class: &str) -> Option { + let frame_class = frame_class.trim_start_matches('\\'); + if frame_class.is_empty() { + return None; + } NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { overrides .borrow() @@ -90,6 +103,43 @@ fn native_frame_called_class_override( }) } +/// Returns the active called-class override bytes for one generated/AOT frame class. +pub(crate) fn native_frame_called_class_override_bytes( + frame_class: &str, +) -> Option<(*const u8, usize)> { + let frame_class = frame_class.trim_start_matches('\\'); + if frame_class.is_empty() { + return None; + } + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides + .borrow() + .iter() + .rev() + .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) + .map(|entry| (entry.called_class.as_ptr(), entry.called_class.len())) + }) +} + +/// Returns the active eval context and called class for one generated/AOT frame. +#[cfg_attr(test, allow(dead_code))] +pub(crate) fn native_frame_called_class_override_context( + frame_class: &str, +) -> Option<(*mut ElephcEvalContext, String)> { + let frame_class = frame_class.trim_start_matches('\\'); + if frame_class.is_empty() { + return None; + } + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides + .borrow() + .iter() + .rev() + .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) + .map(|entry| (entry.context, entry.called_class.clone())) + }) +} + #[cfg(not(test))] #[derive(Default)] struct GlobalEvalClassRegistry { diff --git a/crates/elephc-magician/src/ffi/context.rs b/crates/elephc-magician/src/ffi/context.rs index c4c029765c..77ee8ef37d 100644 --- a/crates/elephc-magician/src/ffi/context.rs +++ b/crates/elephc-magician/src/ffi/context.rs @@ -12,9 +12,11 @@ use super::util::abi_name_to_string; use crate::abi::{ElephcEvalContext, ElephcEvalScope, ABI_VERSION}; +use crate::context::native_frame_called_class_override_bytes; use crate::errors::EvalStatus; #[cfg(not(test))] use crate::ffi::dynamic_destructors::install_dynamic_object_destructor_hook; +use std::ptr; /// Returns the ABI version expected by generated elephc eval call sites. #[no_mangle] @@ -116,6 +118,26 @@ pub unsafe extern "C" fn __elephc_eval_context_pop_class_scope(ctx: *mut ElephcE .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Reads the late-static override currently installed for a generated/AOT frame. +/// +/// # Safety +/// `class_ptr` must be a readable UTF-8 slice for `class_len` bytes. `out_ptr` +/// and `out_len` must be valid writable out-parameters. Returned bytes are +/// owned by eval thread-local state and remain valid until the native frame +/// override guard is dropped. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_native_frame_called_class_override( + class_ptr: *const u8, + class_len: u64, + out_ptr: *mut *const u8, + out_len: *mut u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_native_frame_called_class_override_inner(class_ptr, class_len, out_ptr, out_len) + }) + .unwrap_or(0) +} + /// Runs the call-site metadata setter ABI body after installing a panic boundary. /// /// # Safety @@ -227,3 +249,38 @@ unsafe fn eval_context_pop_class_scope_inner(ctx: *mut ElephcEvalContext) -> i32 context.pop_class_scope(); EvalStatus::Ok.code() } + +/// Runs the native-frame called-class lookup after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_native_frame_called_class_override`; generated code +/// passes writable stack slots for both out-parameters. +unsafe fn eval_native_frame_called_class_override_inner( + class_ptr: *const u8, + class_len: u64, + out_ptr: *mut *const u8, + out_len: *mut u64, +) -> i32 { + if out_ptr.is_null() || out_len.is_null() { + return 0; + } + unsafe { + *out_ptr = ptr::null(); + *out_len = 0; + } + let Ok(class_name) = abi_name_to_string(class_ptr, class_len) else { + return 0; + }; + let Some((called_ptr, called_len)) = native_frame_called_class_override_bytes(&class_name) + else { + return 0; + }; + let Ok(called_len) = u64::try_from(called_len) else { + return 0; + }; + unsafe { + *out_ptr = called_ptr; + *out_len = called_len; + } + 1 +} diff --git a/crates/elephc-magician/src/ffi/object_construction.rs b/crates/elephc-magician/src/ffi/object_construction.rs index 9236fe6941..05a2f2c924 100644 --- a/crates/elephc-magician/src/ffi/object_construction.rs +++ b/crates/elephc-magician/src/ffi/object_construction.rs @@ -16,6 +16,7 @@ use super::util::{abi_name_to_string, clear_result, write_outcome}; use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::context::native_frame_called_class_override_context; use crate::errors::EvalStatus; use crate::interpreter; use crate::runtime_hooks::ElephcRuntimeOps; @@ -112,6 +113,35 @@ pub unsafe extern "C" fn __elephc_eval_static_method_call( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Calls a static method through the current eval late-static AOT-frame override. +/// +/// # Safety +/// `frame_class_ptr` and `method_ptr` must be readable UTF-8 byte ranges. +/// `arg_pack` points at a native-word count followed by that many runtime-cell +/// pointers, and `out` may be null. +#[cfg(not(test))] +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_native_frame_static_method_call( + frame_class_ptr: *const u8, + frame_class_len: u64, + method_ptr: *const u8, + method_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_native_frame_static_method_call_inner( + frame_class_ptr, + frame_class_len, + method_ptr, + method_len, + arg_pack, + out, + ) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Runs the dynamic object-construction ABI body after installing a panic boundary. /// /// # Safety @@ -251,6 +281,63 @@ unsafe fn eval_static_method_call_inner( } } +/// Runs a late-static eval override call from a generated/AOT frame. +/// +/// # Safety +/// Mirrors `__elephc_eval_native_frame_static_method_call`; callers must +/// provide readable frame/method names and a readable argument pack. +#[cfg(not(test))] +unsafe fn eval_native_frame_static_method_call_inner( + frame_class_ptr: *const u8, + frame_class_len: u64, + method_ptr: *const u8, + method_len: u64, + arg_pack: *const usize, + out: *mut ElephcEvalResult, +) -> i32 { + if arg_pack.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(frame_class) = abi_name_to_string(frame_class_ptr, frame_class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(method) = abi_name_to_string(method_ptr, method_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Some((context, called_class)) = native_frame_called_class_override_context(&frame_class) + else { + return -1; + }; + let Some(context) = context.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + let arg_count = *arg_pack; + let arg_ptrs = arg_pack.add(1) as *const *mut RuntimeCell; + let args = if arg_count == 0 { + Vec::new() + } else { + slice::from_raw_parts(arg_ptrs, arg_count) + .iter() + .map(|arg| RuntimeCellHandle::from_raw(*arg)) + .collect() + }; + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_method_call_outcome( + context, + &called_class, + &method, + args, + &mut values, + ) { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + /// Runs the dynamic method-call ABI body after installing a panic boundary. /// /// # Safety diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index 940d3726cf..c6a565bfe5 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -23,7 +23,7 @@ use crate::abi::{ }; use crate::context::{ NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, NativeCallableDefault, - NativeCallableObjectDefaultArg, + NativeCallableObjectDefaultArg, push_native_frame_called_class_override, }; use crate::errors::EvalStatus; use crate::eval_ir::{EvalAttributeArg, EvalParameterTypeVariant}; @@ -323,6 +323,61 @@ fn context_push_class_scope_records_self_and_called_class() { assert_eq!(ctx.current_called_class_scope(), None); } +/// Verifies generated frames can query eval late-static overrides without a context handle. +#[test] +fn native_frame_called_class_override_reports_thread_local_scope() { + let class_name = b"AotBase"; + let mut out_ptr = std::ptr::null(); + let mut out_len = 0; + + let missing = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(missing, 0); + assert!(out_ptr.is_null()); + assert_eq!(out_len, 0); + + { + let _guard = push_native_frame_called_class_override( + std::ptr::null_mut(), + "AotBase", + "EvalChild", + ); + + let found = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(found, 1); + let bytes = unsafe { std::slice::from_raw_parts(out_ptr, out_len as usize) }; + assert_eq!(bytes, b"EvalChild"); + } + + let after_drop = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(after_drop, 0); + assert!(out_ptr.is_null()); + assert_eq!(out_len, 0); +} + /// Verifies generated declaration-name metadata is exposed through eval lists. #[test] fn register_declared_symbol_names_records_visible_metadata() { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 2c6f91ac26..3f8c9b2e64 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -8139,7 +8139,7 @@ fn eval_native_method_call_with_scope( context.push_called_class_scope(called_class.to_string()); } let _called_class_override = called_class_scope - .map(|called_class| push_native_frame_called_class_override(scope, called_class)); + .map(|called_class| push_native_frame_called_class_override(context, scope, called_class)); let result = values.method_call(object, method_name, evaluated_args); if called_class_scope.is_some() { context.pop_called_class_scope(); @@ -8163,7 +8163,7 @@ fn eval_native_static_method_call_with_scope( context.push_called_class_scope(called_class.to_string()); } let _called_class_override = called_class_scope - .map(|called_class| push_native_frame_called_class_override(scope, called_class)); + .map(|called_class| push_native_frame_called_class_override(context, scope, called_class)); let result = values.static_method_call(class_name, method_name, evaluated_args); if called_class_scope.is_some() { context.pop_called_class_scope(); diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index fad08b38c6..6954793a7d 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -4673,6 +4673,22 @@ fn lower_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) - } else { None }; + let eval_done_label = if late_bound_static && ctx.module.required_runtime_features.eval { + let no_override_label = ctx.next_label("eval_late_static_no_override"); + let done_label = ctx.next_label("eval_late_static_done"); + builtins::lower_eval_native_frame_static_method_call( + ctx, + inst, + receiver.as_str(), + method_name, + &no_override_label, + &done_label, + )?; + ctx.emitter.label(&no_override_label); + Some(done_label) + } else { + None + }; let call_args = materialize_static_method_call_args_with_refs( ctx, &called_class_id, @@ -4699,7 +4715,11 @@ fn lower_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) - } ctx.store_result_value(result)?; } - emit_ref_arg_writebacks(ctx, &call_args.ref_writebacks) + emit_ref_arg_writebacks(ctx, &call_args.ref_writebacks)?; + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } + Ok(()) } /// Lowers a direct static-method call against a class declared by a previous eval fragment. diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 320b5d6eb6..27deea885b 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -145,6 +145,25 @@ pub(super) fn lower_eval_static_method_call( eval::lower_eval_static_method_call(ctx, inst, class_name, method_name) } +/// Lowers a late-static AOT-frame static method call through an active eval override. +pub(super) fn lower_eval_native_frame_static_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + frame_class: &str, + method_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + eval::lower_eval_native_frame_static_method_call( + ctx, + inst, + frame_class, + method_name, + no_override_label, + done_label, + ) +} + /// Lowers post-eval callable-array dispatch against eval dynamic callables. pub(super) fn lower_eval_callable_call_array( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 96b292fa1f..9823103a3d 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -565,6 +565,67 @@ pub(super) fn lower_eval_static_method_call( store_if_result(ctx, inst) } +/// Lowers a late-static AOT-frame static method call through an active eval override. +pub(super) fn lower_eval_native_frame_static_method_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + frame_class: &str, + method_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + let args_offset = EVAL_STACK_BYTES; + let stack_bytes = eval_static_method_call_stack_bytes(inst.operands.len()); + let miss_stack_label = ctx.next_label("eval_native_frame_static_method_miss"); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); + store_eval_static_method_call_arg_pack(ctx, inst, args_offset)?; + let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &frame_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + frame_len as i64, + ); + let (method_label, method_len) = ctx.data.add_string(method_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + &method_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + method_len as i64, + ); + let pack_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_static_method_call"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + emit_eval_result_as_type(ctx, &inst.result_php_type)?; + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst)?; + abi::emit_jump(ctx.emitter, done_label); + + ctx.emitter.label(&miss_stack_label); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + abi::emit_jump(ctx.emitter, no_override_label); + Ok(()) +} + /// Lowers a callable-array dispatch through the eval bridge. pub(super) fn lower_eval_callable_call_array( ctx: &mut FunctionContext<'_>, @@ -1177,6 +1238,118 @@ fn store_eval_mixed_operand_at( Ok(()) } +/// Probes whether eval has a late-static called-class override for an AOT frame. +fn emit_eval_native_frame_override_probe( + ctx: &mut FunctionContext<'_>, + frame_class: &str, + no_override_label: &str, +) { + let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &frame_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + frame_len as i64, + ); + let out_ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 2); + abi::emit_temporary_stack_address(ctx.emitter, out_ptr_arg, EVAL_CALLED_CLASS_PTR_OFFSET); + let out_len_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_len_arg, EVAL_CALLED_CLASS_LEN_OFFSET); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_called_class_override"); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_branch_if_int_result_zero(ctx.emitter, no_override_label); +} + +/// Converts an eval Mixed result cell to the concrete EIR type expected here. +fn emit_eval_result_as_type(ctx: &mut FunctionContext<'_>, result_ty: &PhpType) -> Result<()> { + match result_ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => Ok(()), + PhpType::Str => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + Ok(()) + } + PhpType::Float => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + Ok(()) + } + PhpType::Int => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + Ok(()) + } + PhpType::Bool => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); + Ok(()) + } + PhpType::TaggedScalar => { + emit_eval_mixed_result_as_tagged_scalar(ctx); + Ok(()) + } + PhpType::Void | PhpType::Never => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + 0x7fff_ffff_ffff_fffe, + ); + Ok(()) + } + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Iterable + | PhpType::Object(_) + | PhpType::Buffer(_) + | PhpType::Callable + | PhpType::Packed(_) + | PhpType::Pointer(_) + | PhpType::Resource(_) => { + emit_eval_unbox_mixed_to_owned_result(ctx, &result_ty.codegen_repr()); + Ok(()) + } + } +} + +/// Reorders an eval Mixed result cell into inline tagged-scalar result registers. +fn emit_eval_mixed_result_as_tagged_scalar(ctx: &mut FunctionContext<'_>) { + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x9, x0"); // preserve the unboxed eval result tag before moving the payload + ctx.emitter.instruction("mov x0, x1"); // place the unboxed eval payload into the tagged-scalar payload register + ctx.emitter.instruction("mov x1, x9"); // place the unboxed eval tag into the tagged-scalar tag register + } + Arch::X86_64 => { + ctx.emitter.instruction("mov r10, rax"); // preserve the unboxed eval result tag before moving the payload + ctx.emitter.instruction("mov rax, rdi"); // place the unboxed eval payload into the tagged-scalar payload register + ctx.emitter.instruction("mov rdx, r10"); // place the unboxed eval tag into the tagged-scalar tag register + } + } +} + +/// Unboxes an eval Mixed result cell and retains concrete refcounted payloads. +fn emit_eval_unbox_mixed_to_owned_result(ctx: &mut FunctionContext<'_>, result_ty: &PhpType) { + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + emit_eval_move_unboxed_low_payload_to_result(ctx); + abi::emit_incref_if_refcounted(ctx.emitter, result_ty); +} + +/// Moves the low payload from `__rt_mixed_unbox` into the integer result register. +fn emit_eval_move_unboxed_low_payload_to_result(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, x1"); // return the unboxed eval low payload as the concrete result + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rax, rdi"); // return the unboxed eval low payload as the concrete result + } + } +} + /// Boxes a raw eval predicate result when the enclosing IR value expects Mixed storage. fn box_eval_bool_result_if_mixed(ctx: &mut FunctionContext<'_>, inst: &Instruction) { if inst.result.is_some() && inst.result_php_type.codegen_repr() == PhpType::Mixed { diff --git a/src/codegen/lower_inst/strings.rs b/src/codegen/lower_inst/strings.rs index 0725c7caab..6a33d687f0 100644 --- a/src/codegen/lower_inst/strings.rs +++ b/src/codegen/lower_inst/strings.rs @@ -66,6 +66,7 @@ fn lower_late_static_class_name(ctx: &mut FunctionContext<'_>) -> Result<()> { fn lower_late_static_class_name_arm64(ctx: &mut FunctionContext<'_>) -> Result<()> { let missing = ctx.next_label("static_class_missing"); let done = ctx.next_label("static_class_done"); + emit_eval_native_frame_called_class_override_probe(ctx, &done); emit_late_static_class_id_to_reg(ctx, "x12")?; abi::emit_load_symbol_to_reg(ctx.emitter, "x10", "_class_name_count", 0); ctx.emitter.instruction("cmp x12, x10"); // reject called-class ids outside the emitted class-name table @@ -87,6 +88,7 @@ fn lower_late_static_class_name_arm64(ctx: &mut FunctionContext<'_>) -> Result<( fn lower_late_static_class_name_x86_64(ctx: &mut FunctionContext<'_>) -> Result<()> { let missing = ctx.next_label("static_class_missing"); let done = ctx.next_label("static_class_done"); + emit_eval_native_frame_called_class_override_probe(ctx, &done); emit_late_static_class_id_to_reg(ctx, "r8")?; abi::emit_load_symbol_to_reg(ctx.emitter, "r9", "_class_name_count", 0); ctx.emitter.instruction("cmp r8, r9"); // reject called-class ids outside the emitted class-name table @@ -104,6 +106,92 @@ fn lower_late_static_class_name_x86_64(ctx: &mut FunctionContext<'_>) -> Result< Ok(()) } +/// Probes eval's thread-local called-class override before falling back to AOT class ids. +fn emit_eval_native_frame_called_class_override_probe( + ctx: &mut FunctionContext<'_>, + done_label: &str, +) { + if !ctx.module.required_runtime_features.eval { + return; + } + let Some(frame_class) = current_late_static_frame_class(ctx).map(str::to_string) else { + return; + }; + match ctx.emitter.target.arch { + Arch::AArch64 => { + emit_eval_native_frame_called_class_override_probe_aarch64(ctx, &frame_class, done_label); + } + Arch::X86_64 => { + emit_eval_native_frame_called_class_override_probe_x86_64(ctx, &frame_class, done_label); + } + } +} + +/// Emits the AArch64 eval override probe for one generated/AOT method frame. +fn emit_eval_native_frame_called_class_override_probe_aarch64( + ctx: &mut FunctionContext<'_>, + frame_class: &str, + done_label: &str, +) { + let no_override = ctx.next_label("static_class_no_eval_override"); + let (class_label, class_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_reserve_temporary_stack(ctx.emitter, 32); + abi::emit_symbol_address(ctx.emitter, "x0", &class_label); + abi::emit_load_int_immediate(ctx.emitter, "x1", class_len as i64); + abi::emit_temporary_stack_address(ctx.emitter, "x2", 0); + abi::emit_temporary_stack_address(ctx.emitter, "x3", 8); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_called_class_override"); + abi::emit_call_label(ctx.emitter, &symbol); + ctx.emitter.instruction("cmp x0, #0"); // check whether eval has a late-static class override for this AOT frame + ctx.emitter.instruction(&format!("b.eq {}", no_override)); // fall back to the emitted class-id metadata when no override is active + ctx.emitter.instruction("ldr x1, [sp]"); // load the eval called-class name pointer into the string result + ctx.emitter.instruction("ldr x2, [sp, #8]"); // load the eval called-class name length into the string result + abi::emit_release_temporary_stack(ctx.emitter, 32); + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the AOT class-id metadata path after using the eval override + ctx.emitter.label(&no_override); + abi::emit_release_temporary_stack(ctx.emitter, 32); +} + +/// Emits the x86_64 eval override probe for one generated/AOT method frame. +fn emit_eval_native_frame_called_class_override_probe_x86_64( + ctx: &mut FunctionContext<'_>, + frame_class: &str, + done_label: &str, +) { + let no_override = ctx.next_label("static_class_no_eval_override"); + let (class_label, class_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_reserve_temporary_stack(ctx.emitter, 32); + abi::emit_symbol_address(ctx.emitter, "rdi", &class_label); + abi::emit_load_int_immediate(ctx.emitter, "rsi", class_len as i64); + abi::emit_temporary_stack_address(ctx.emitter, "rdx", 0); + abi::emit_temporary_stack_address(ctx.emitter, "rcx", 8); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_called_class_override"); + abi::emit_call_label(ctx.emitter, &symbol); + ctx.emitter.instruction("cmp rax, 0"); // check whether eval has a late-static class override for this AOT frame + ctx.emitter.instruction(&format!("je {}", no_override)); // fall back to the emitted class-id metadata when no override is active + ctx.emitter.instruction("mov rax, QWORD PTR [rsp]"); // load the eval called-class name pointer into the string result + ctx.emitter.instruction("mov rdx, QWORD PTR [rsp + 8]"); // load the eval called-class name length into the string result + abi::emit_release_temporary_stack(ctx.emitter, 32); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the AOT class-id metadata path after using the eval override + ctx.emitter.label(&no_override); + abi::emit_release_temporary_stack(ctx.emitter, 32); +} + +/// Returns the generated/AOT class encoded in the current method frame name. +fn current_late_static_frame_class<'a>(ctx: &'a FunctionContext<'_>) -> Option<&'a str> { + ctx.function + .flags + .is_method + .then(|| ctx.function.name.rsplit_once("::").map(|(class_name, _)| class_name)) + .flatten() +} + /// Loads the late-static class id from the hidden static frame slot or `$this`. fn emit_late_static_class_id_to_reg(ctx: &mut FunctionContext<'_>, reg: &str) -> Result<()> { if let Some(slot) = ctx.local_slot_by_name(CALLED_CLASS_ID_PARAM) { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 516cd145de..2c813a8756 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6388,6 +6388,73 @@ EvalAotEvalScopeParent:EvalAotEvalScopeChild:EvalAotEvalScopeChild" ); } +/// Verifies eval classes keep late-static `static::class` in inherited AOT methods. +#[test] +fn test_eval_declared_child_inherited_aot_method_static_class_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceClass() . "|"; +echo EvalAotDirectScopeChild::staticClass();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "EvalAotDirectScopeChild|EvalAotDirectScopeChild" + ); +} + +/// Verifies eval classes keep late-static `static::method()` in inherited AOT methods. +#[test] +fn test_eval_declared_child_inherited_aot_method_static_call_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceCall() . "|"; +echo EvalAotDirectStaticCallChild::staticCall();'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "child|child"); +} + /// Verifies eval fragments resolve `parent::` through AOT parent metadata. #[test] fn test_eval_fragment_in_aot_method_resolves_parent_scope() { From f3d0dbc6a4f9c05d4790a3a8f891f2b8ed6c3ed6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 22:56:36 +0200 Subject: [PATCH 0897/1208] Preserve eval late-static scope for AOT static properties --- .../elephc-magician/src/ffi/static_members.rs | 147 ++++++++++++++++++ src/codegen/lower_inst/builtins.rs | 40 +++++ src/codegen/lower_inst/builtins/eval.rs | 112 +++++++++++++ src/codegen/lower_inst/static_properties.rs | 77 ++++++++- tests/codegen/eval.rs | 44 ++++++ 5 files changed, 416 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/ffi/static_members.rs b/crates/elephc-magician/src/ffi/static_members.rs index 914b669242..6b2504deff 100644 --- a/crates/elephc-magician/src/ffi/static_members.rs +++ b/crates/elephc-magician/src/ffi/static_members.rs @@ -7,6 +7,7 @@ //! - Generated EIR backend assembly through `__elephc_eval_class_constant_fetch`. //! - Generated EIR backend assembly through `__elephc_eval_static_property_get`. //! - Generated EIR backend assembly through `__elephc_eval_static_property_set`. +//! - Generated AOT frames through the native-frame late-static property hooks. //! //! Key details: //! - Returned value cells are retained before crossing back to generated code. @@ -14,6 +15,7 @@ use super::util::{abi_name_to_string, clear_result, write_outcome}; use crate::abi::{ElephcEvalContext, ElephcEvalResult, ABI_VERSION}; +use crate::context::native_frame_called_class_override_context; use crate::errors::EvalStatus; use crate::interpreter::{self, EvalOutcome, RuntimeValueOps}; use crate::runtime_hooks::ElephcRuntimeOps; @@ -61,6 +63,32 @@ pub unsafe extern "C" fn __elephc_eval_static_property_get( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Reads a static property through an active eval late-static native-frame override. +/// +/// # Safety +/// The frame class and property name byte ranges must be readable when their +/// lengths are non-zero, and `out` may be null. Returns `-1` when no matching +/// native-frame override is active. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_native_frame_static_property_get( + frame_class_ptr: *const u8, + frame_class_len: u64, + property_ptr: *const u8, + property_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_native_frame_static_property_get_inner( + frame_class_ptr, + frame_class_len, + property_ptr, + property_len, + out, + ) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Writes a static property through eval dynamic metadata. /// /// # Safety @@ -81,6 +109,34 @@ pub unsafe extern "C" fn __elephc_eval_static_property_set( .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) } +/// Writes a static property through an active eval late-static native-frame override. +/// +/// # Safety +/// The frame class and property name byte ranges must be readable when their +/// lengths are non-zero, `value` must be a valid runtime cell, and `out` may be +/// null. Returns `-1` when no matching native-frame override is active. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_native_frame_static_property_set( + frame_class_ptr: *const u8, + frame_class_len: u64, + property_ptr: *const u8, + property_len: u64, + value: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + eval_native_frame_static_property_set_inner( + frame_class_ptr, + frame_class_len, + property_ptr, + property_len, + value, + out, + ) + }) + .unwrap_or_else(|_| EvalStatus::RuntimeFatal.code()) +} + /// Runs the class-constant fetch ABI body after installing a panic boundary. /// /// # Safety @@ -161,6 +217,49 @@ unsafe fn eval_static_property_get_inner( } } +/// Runs the native-frame static-property get ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_native_frame_static_property_get`; callers must +/// provide readable frame/property name bytes and optional result storage. +unsafe fn eval_native_frame_static_property_get_inner( + frame_class_ptr: *const u8, + frame_class_len: u64, + property_ptr: *const u8, + property_len: u64, + out: *mut ElephcEvalResult, +) -> i32 { + let Ok(frame_class) = abi_name_to_string(frame_class_ptr, frame_class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(property_name) = abi_name_to_string(property_ptr, property_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Some((context, called_class)) = native_frame_called_class_override_context(&frame_class) + else { + return -1; + }; + let Some(context) = context.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_property_get( + context, + &called_class, + &property_name, + &mut values, + ) + .and_then(|outcome| retain_value_outcome(outcome, &mut values)) + { + Ok(outcome) => write_outcome(outcome, out).code(), + Err(status) => status.code(), + } +} + /// Runs the static-property set ABI body after installing a panic boundary. /// /// # Safety @@ -204,6 +303,54 @@ unsafe fn eval_static_property_set_inner( } } +/// Runs the native-frame static-property set ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_native_frame_static_property_set`; callers must +/// provide readable frame/property name bytes, a valid value cell, and optional +/// result storage for thrown PHP objects. +unsafe fn eval_native_frame_static_property_set_inner( + frame_class_ptr: *const u8, + frame_class_len: u64, + property_ptr: *const u8, + property_len: u64, + value: *mut RuntimeCell, + out: *mut ElephcEvalResult, +) -> i32 { + if value.is_null() { + return EvalStatus::RuntimeFatal.code(); + } + let Ok(frame_class) = abi_name_to_string(frame_class_ptr, frame_class_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Ok(property_name) = abi_name_to_string(property_ptr, property_len) else { + return EvalStatus::RuntimeFatal.code(); + }; + let Some((context, called_class)) = native_frame_called_class_override_context(&frame_class) + else { + return -1; + }; + let Some(context) = context.as_mut() else { + return EvalStatus::RuntimeFatal.code(); + }; + if context.abi_version() != ABI_VERSION { + return EvalStatus::AbiMismatch.code(); + } + clear_result(out); + let mut values = ElephcRuntimeOps::with_context(context as *const ElephcEvalContext); + match interpreter::execute_context_static_property_set( + context, + &called_class, + &property_name, + RuntimeCellHandle::from_raw(value), + &mut values, + ) { + Ok(Some(outcome)) => write_outcome(outcome, out).code(), + Ok(None) => EvalStatus::Ok.code(), + Err(status) => status.code(), + } +} + /// Splits the packed static-property target used by the compact setter ABI. fn split_static_property_target(target: &str) -> Result<(&str, &str), EvalStatus> { let Some((class_name, property_name)) = target.rsplit_once("::") else { diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 27deea885b..883c3ab0b9 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -164,6 +164,46 @@ pub(super) fn lower_eval_native_frame_static_method_call( ) } +/// Lowers a late-static AOT-frame static-property read through an active eval override. +pub(super) fn lower_eval_native_frame_static_property_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + frame_class: &str, + property_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + eval::lower_eval_native_frame_static_property_get( + ctx, + inst, + frame_class, + property_name, + no_override_label, + done_label, + ) +} + +/// Lowers a late-static AOT-frame static-property write through an active eval override. +pub(super) fn lower_eval_native_frame_static_property_set( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + value: ValueId, + frame_class: &str, + property_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + eval::lower_eval_native_frame_static_property_set( + ctx, + inst, + value, + frame_class, + property_name, + no_override_label, + done_label, + ) +} + /// Lowers post-eval callable-array dispatch against eval dynamic callables. pub(super) fn lower_eval_callable_call_array( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 9823103a3d..d257d066a6 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -626,6 +626,118 @@ pub(super) fn lower_eval_native_frame_static_method_call( Ok(()) } +/// Lowers a late-static AOT-frame static-property read through an active eval override. +pub(super) fn lower_eval_native_frame_static_property_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + frame_class: &str, + property_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + let miss_stack_label = ctx.next_label("eval_native_frame_static_prop_get_miss"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); + let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &frame_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + frame_len as i64, + ); + let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + &property_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + property_len as i64, + ); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_static_property_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + emit_eval_result_as_type(ctx, &inst.result_php_type)?; + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst)?; + abi::emit_jump(ctx.emitter, done_label); + + ctx.emitter.label(&miss_stack_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_jump(ctx.emitter, no_override_label); + Ok(()) +} + +/// Lowers a late-static AOT-frame static-property write through an active eval override. +pub(super) fn lower_eval_native_frame_static_property_set( + ctx: &mut FunctionContext<'_>, + _inst: &Instruction, + value: ValueId, + frame_class: &str, + property_name: &str, + no_override_label: &str, + done_label: &str, +) -> Result<()> { + let miss_stack_label = ctx.next_label("eval_native_frame_static_prop_set_miss"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + emit_eval_native_frame_override_probe(ctx, frame_class, &miss_stack_label); + store_eval_mixed_operand_at(ctx, value, EVAL_TEMP_CELL_OFFSET)?; + let (frame_label, frame_len) = ctx.data.add_string(frame_class.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &frame_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + frame_len as i64, + ); + let (property_label, property_len) = ctx.data.add_string(property_name.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + &property_label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + property_len as i64, + ); + let value_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_load_temporary_stack_slot(ctx.emitter, value_arg, EVAL_TEMP_CELL_OFFSET); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_native_frame_static_property_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_branch_if_eval_c_int_negative(ctx, &miss_stack_label); + emit_eval_status_check(ctx); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_jump(ctx.emitter, done_label); + + ctx.emitter.label(&miss_stack_label); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + abi::emit_jump(ctx.emitter, no_override_label); + Ok(()) +} + /// Lowers a callable-array dispatch through the eval bridge. pub(super) fn lower_eval_callable_call_array( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index 80269d1363..903c42f775 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -8,7 +8,9 @@ //! Key details: //! - This slice supports public scalar/string/array/object static properties with //! named, lexical `self`, and lexical `parent` receivers, but not late static -//! binding, references, or non-indexed array mutation. +//! references or non-indexed array mutation. +//! - `static::` receivers use native class-id branches for generated classes and +//! the eval native-frame override when late static scope points at an eval class. //! - Typed static properties use the same high-word uninitialized sentinel as //! the emitted code before reads. @@ -54,15 +56,24 @@ pub(super) fn lower_load_static_property( } let slot = resolve_static_property_slot(ctx, inst, true)?; ensure_static_property_type_supported(&slot.php_type, inst)?; + let eval_done_label = emit_eval_native_frame_static_property_get_if_needed(ctx, inst, &slot)?; if slot.late_bound && !slot.branches.is_empty() { let class_id_reg = class_id_work_reg(ctx.emitter); if emit_called_class_id_to_reg(ctx, class_id_reg)? { emit_dynamic_load_static_property_result(ctx, &slot, class_id_reg)?; - return store_if_result(ctx, inst); + store_if_result(ctx, inst)?; + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } + return Ok(()); } } emit_direct_load_static_property_result(ctx, &slot); - store_if_result(ctx, inst) + store_if_result(ctx, inst)?; + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } + Ok(()) } /// Returns an eval dynamic static-property target when no AOT class owns the receiver. @@ -101,16 +112,24 @@ pub(super) fn lower_store_static_property( ensure_static_property_type_supported(&slot.php_type, inst)?; let value_ty = ctx.value_php_type(value)?; ensure_static_property_value_supported(&slot, &value_ty, inst)?; - load_static_property_store_value_to_result(ctx, value, &slot.php_type)?; let release_previous = !value_is_same_static_property_load(ctx, value, &slot)?; + let eval_done_label = + emit_eval_native_frame_static_property_set_if_needed(ctx, inst, value, &slot)?; + load_static_property_store_value_to_result(ctx, value, &slot.php_type)?; if slot.late_bound && !slot.branches.is_empty() { let class_id_reg = class_id_work_reg(ctx.emitter); if emit_called_class_id_to_reg(ctx, class_id_reg)? { emit_dynamic_store_static_property_result(ctx, &slot, class_id_reg, release_previous); + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } return Ok(()); } } emit_direct_store_static_property_result(ctx, &slot, release_previous); + if let Some(done_label) = eval_done_label { + ctx.emitter.label(&done_label); + } Ok(()) } @@ -323,6 +342,56 @@ fn emit_called_class_id_to_reg( Ok(true) } +/// Emits an eval late-static override read before falling back to native slots. +fn emit_eval_native_frame_static_property_get_if_needed( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + slot: &StaticPropertySlot, +) -> Result> { + if !slot.late_bound || !ctx.module.required_runtime_features.eval { + return Ok(None); + } + let frame_class = super::current_method_class(ctx)?.to_string(); + let no_override_label = ctx.next_label("eval_late_static_prop_get_no_override"); + let done_label = ctx.next_label("eval_late_static_prop_get_done"); + builtins::lower_eval_native_frame_static_property_get( + ctx, + inst, + &frame_class, + &slot.property, + &no_override_label, + &done_label, + )?; + ctx.emitter.label(&no_override_label); + Ok(Some(done_label)) +} + +/// Emits an eval late-static override write before falling back to native slots. +fn emit_eval_native_frame_static_property_set_if_needed( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + value: ValueId, + slot: &StaticPropertySlot, +) -> Result> { + if !slot.late_bound || !ctx.module.required_runtime_features.eval { + return Ok(None); + } + let frame_class = super::current_method_class(ctx)?.to_string(); + let no_override_label = ctx.next_label("eval_late_static_prop_set_no_override"); + let done_label = ctx.next_label("eval_late_static_prop_set_done"); + builtins::lower_eval_native_frame_static_property_set( + ctx, + inst, + value, + &frame_class, + &slot.property, + &no_override_label, + &done_label, + )?; + ctx.emitter.label(&no_override_label); + Ok(Some(done_label)) +} + /// Emits a late-bound static property read selected by the runtime called-class id. fn emit_dynamic_load_static_property_result( ctx: &mut FunctionContext<'_>, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2c813a8756..80203dabe5 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6455,6 +6455,50 @@ echo EvalAotDirectStaticCallChild::staticCall();'); assert_eq!(out.stdout, "child|child"); } +/// Verifies eval classes keep late-static `static::$property` access in inherited AOT methods. +#[test] +fn test_eval_declared_child_inherited_aot_method_static_property_preserves_late_static_scope() { + let out = compile_and_run_capture( + r#"instanceRead() . "|"; +echo EvalAotDirectStaticPropChild::staticRead() . "|"; +(new EvalAotDirectStaticPropChild())->instanceWrite("one"); +echo EvalAotDirectStaticPropChild::$value . ":" . EvalAotDirectStaticPropParent::$value . "|"; +EvalAotDirectStaticPropChild::staticWrite("two"); +echo EvalAotDirectStaticPropChild::$value . ":" . EvalAotDirectStaticPropParent::$value;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "child|child|one:parent|two:parent"); +} + /// Verifies eval fragments resolve `parent::` through AOT parent metadata. #[test] fn test_eval_fragment_in_aot_method_resolves_parent_scope() { From f962d76baae8329062242da995cbb6a985d3aeb5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 23:16:00 +0200 Subject: [PATCH 0898/1208] Coerce mixed values for typed static property stores --- src/codegen/lower_inst/static_properties.rs | 28 ++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index 903c42f775..9b121930f1 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -23,7 +23,7 @@ use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; use super::super::context::FunctionContext; -use super::{builtins, expect_data, expect_operand, store_if_result}; +use super::{builtins, expect_data, expect_operand, load_value_to_first_int_arg, store_if_result}; use crate::codegen::{CodegenIrError, Result}; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; @@ -708,6 +708,9 @@ fn ensure_static_property_value_supported( if is_empty_array_for_array_static_property(value_ty, &slot.php_type) { return Ok(()); } + if can_coerce_mixed_to_scalar_static_property(value_ty, &slot.php_type) { + return Ok(()); + } Err(CodegenIrError::unsupported(format!( "{} assigning PHP type {:?} to {}::${} with PHP type {:?}", inst.op.name(), @@ -729,6 +732,15 @@ fn is_empty_array_for_array_static_property(value_ty: &PhpType, slot_ty: &PhpTyp matches!(value_elem.codegen_repr(), PhpType::Never | PhpType::Void) } +/// Returns true when a boxed Mixed value can be coerced before a scalar static-property store. +fn can_coerce_mixed_to_scalar_static_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { + matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) + && matches!( + slot_ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str + ) +} + /// Returns true when a value can materialize the inline nullable-int static-property shape. fn can_store_value_as_tagged_scalar_static_property( value_ty: &PhpType, @@ -782,6 +794,20 @@ fn load_static_property_store_value_to_result( } return Ok(()); } + if matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + load_value_to_first_int_arg(ctx, value)?; + match slot_ty.codegen_repr() { + PhpType::Str => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); + abi::emit_call_label(ctx.emitter, "__rt_str_persist"); + } + PhpType::Int => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"), + PhpType::Bool => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"), + PhpType::Float => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"), + _ => {} + } + return Ok(()); + } ctx.load_value_to_result(value)?; if matches!(slot_ty.codegen_repr(), PhpType::Int) && matches!(value_ty.codegen_repr(), PhpType::Mixed) From 00ecfb30a58bf9e5a14e6e972b2711b4d6d2c37d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 27 Jun 2026 23:41:11 +0200 Subject: [PATCH 0899/1208] Support variadic instance first-class callables --- .../lower_inst/callables/instance_expr.rs | 19 +-------- tests/codegen/oop/callables/methods.rs | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/codegen/lower_inst/callables/instance_expr.rs b/src/codegen/lower_inst/callables/instance_expr.rs index 4203a5f07b..90b2ed11d6 100644 --- a/src/codegen/lower_inst/callables/instance_expr.rs +++ b/src/codegen/lower_inst/callables/instance_expr.rs @@ -13,7 +13,7 @@ use crate::codegen::abi; use crate::ir::{BlockId, Immediate, Instruction, Op, ValueDef, ValueId}; use crate::names::{method_symbol, php_symbol_key}; -use crate::types::{FunctionSig, PhpType}; +use crate::types::PhpType; use super::super::super::context::FunctionContext; use super::super::{ @@ -159,7 +159,6 @@ fn instance_method_expr_call_target( target )) })?; - require_instance_expr_call_signature(owner, target, sig)?; let impl_class = class_info .method_impl_classes .get(&method_key) @@ -188,22 +187,6 @@ fn instance_method_expr_call_target( }) } -/// Rejects callable method signatures whose EIR expr-call lowering cannot yet forward safely. -fn require_instance_expr_call_signature( - owner: &str, - target: &str, - sig: &FunctionSig, -) -> Result<()> { - if sig.variadic.is_some() { - return Err(CodegenIrError::unsupported(format!( - "{} receiver-bound callable '{}' with variadic method signature", - owner, - target - ))); - } - Ok(()) -} - /// Returns the first-class callable producer for an expr-call operand. fn expr_callable_source_instruction<'a>( ctx: &'a FunctionContext<'_>, diff --git a/tests/codegen/oop/callables/methods.rs b/tests/codegen/oop/callables/methods.rs index e256f218f6..47220f9f77 100644 --- a/tests/codegen/oop/callables/methods.rs +++ b/tests/codegen/oop/callables/methods.rs @@ -1068,6 +1068,45 @@ echo ($fn)(5); assert_eq!(out, "12"); } +/// Verifies a direct instance method call with a typed variadic tail keeps all +/// trailing arguments before the first-class callable regression below. +#[test] +fn test_direct_instance_method_string_variadic_count() { + let out = compile_and_run( + r#"join("A", "B", "C"); +"#, + ); + assert_eq!(out, "A:2"); +} + +/// Tests a captured first-class callable for an instance method with a variadic +/// signature, verifying the variadic tail is packed before the bound method call. +#[test] +fn test_captured_first_class_callable_instance_method_variadic_call() { + let out = compile_and_run( + r#"join(...); +echo $fn("A", "B", "C"); +"#, + ); + assert_eq!(out, "A:2"); +} + /// Tests a first-class callable where the method's receiver captures a private property /// from the outer scope, verifying the non-local state is correctly preserved across /// callable invocation. From abf3704048f594c95e13e0f3da35ddc4a5f04830 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 00:07:16 +0200 Subject: [PATCH 0900/1208] Box mixed params for method sort callbacks --- src/codegen/lower_inst/builtins/arrays.rs | 364 ++++++++++++++++++---- 1 file changed, 296 insertions(+), 68 deletions(-) diff --git a/src/codegen/lower_inst/builtins/arrays.rs b/src/codegen/lower_inst/builtins/arrays.rs index f3cd48f277..b56e7aa1fa 100644 --- a/src/codegen/lower_inst/builtins/arrays.rs +++ b/src/codegen/lower_inst/builtins/arrays.rs @@ -3186,6 +3186,7 @@ struct StaticMethodCallbackTarget { called_class: StaticCallbackCalledClass, dynamic_slot: Option, env_source: Option, + param_types: Vec, return_ty: PhpType, } @@ -3193,6 +3194,7 @@ struct StaticMethodCallbackTarget { struct InstanceMethodCallbackTarget { entry_label: String, receiver: ValueId, + param_types: Vec, return_ty: PhpType, } @@ -3287,6 +3289,7 @@ fn static_method_callback_target_inner( called_class: StaticCallbackCalledClass::Immediate(receiver_info.class_id), dynamic_slot: None, env_source: None, + param_types: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), })) } @@ -3346,6 +3349,7 @@ fn static_late_bound_method_callback_target( called_class: StaticCallbackCalledClass::Env, dynamic_slot: receiver_info.static_vtable_slots.get(&method_key).copied(), env_source: Some(static_callback_env_source(ctx)?), + param_types: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), })) } @@ -3449,6 +3453,7 @@ fn instance_method_sort_callback_target( Ok(Some(InstanceMethodCallbackTarget { entry_label: method_symbol(impl_class, &method_key), receiver, + param_types: sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect(), return_ty: sig.return_type.codegen_repr(), })) } @@ -3511,23 +3516,25 @@ fn require_static_method_callback_arg_types( Ok(()) } -/// Verifies the target static method can consume the wrapper's unboxed integer ABI values. +/// Verifies the target method can consume the wrapper's runtime callback ABI values. fn require_static_method_callback_param_types( owner: &str, callback_name: &str, sig: &crate::types::FunctionSig, visible_arg_types: &[PhpType], ) -> Result<()> { + let mut needs_mixed_boxes = false; for ((_, param_ty), visible_ty) in sig.params.iter().zip(visible_arg_types.iter()) { let param_ty = param_ty.codegen_repr(); let visible_ty = visible_ty.codegen_repr(); if matches!(visible_ty, PhpType::Void | PhpType::Never) { continue; } - if matches!( - (¶m_ty, &visible_ty), - (PhpType::Int | PhpType::Bool, PhpType::Int | PhpType::Bool) - ) { + if param_ty == PhpType::Mixed { + needs_mixed_boxes = true; + continue; + } + if matches!((¶m_ty, &visible_ty), (PhpType::Int | PhpType::Bool, PhpType::Int | PhpType::Bool)) { continue; } if matches!((¶m_ty, &visible_ty), (PhpType::Str, PhpType::Str)) { @@ -3538,9 +3545,94 @@ fn require_static_method_callback_param_types( owner, callback_name, param_ty, visible_ty ))); } + if needs_mixed_boxes + && matches!( + sig.return_type.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) | PhpType::TaggedScalar + ) + { + return Err(CodegenIrError::unsupported(format!( + "{} '{}' with boxed callback args and alias-sensitive return type", + owner, + callback_name + ))); + } Ok(()) } +const NO_CALLBACK_BOX_OFFSET: usize = usize::MAX; + +/// Stack frame layout used by generated callback ABI adapters. +struct CallbackWrapperFrame { + hidden_offset: usize, + raw_offsets: Vec, + boxed_offsets: Vec, + return_offset: usize, + return_address_offset: usize, + total_bytes: usize, +} + +/// Builds the stack layout for one generated callback wrapper. +fn callback_wrapper_frame(param_types: &[PhpType], visible_arg_types: &[PhpType]) -> CallbackWrapperFrame { + let mut offset = 0usize; + let hidden_offset = offset; + offset += 8; + let mut raw_offsets = Vec::with_capacity(visible_arg_types.len()); + for ty in visible_arg_types { + raw_offsets.push(offset); + offset += callback_visible_arg_slot_size(ty); + } + let mut boxed_offsets = Vec::with_capacity(visible_arg_types.len()); + for (param_ty, visible_ty) in param_types.iter().zip(visible_arg_types.iter()) { + if callback_arg_needs_mixed_box(param_ty, visible_ty) { + boxed_offsets.push(offset); + offset += 8; + } else { + boxed_offsets.push(NO_CALLBACK_BOX_OFFSET); + } + } + let return_offset = offset; + offset += 16; + let return_address_offset = offset; + offset += 8; + CallbackWrapperFrame { + hidden_offset, + raw_offsets, + boxed_offsets, + return_offset, + return_address_offset, + total_bytes: align_callback_frame_bytes(offset), + } +} + +/// Rounds a wrapper frame size up to the stack alignment required before calls. +fn align_callback_frame_bytes(bytes: usize) -> usize { + (bytes + 15) & !15 +} + +/// Returns the stack bytes needed to preserve one incoming runtime callback argument. +fn callback_visible_arg_slot_size(ty: &PhpType) -> usize { + if matches!(ty.codegen_repr(), PhpType::Str) { + 16 + } else { + 8 + } +} + +/// Returns true when the target method parameter needs a boxed Mixed argument. +fn callback_arg_needs_mixed_box(param_ty: &PhpType, visible_ty: &PhpType) -> bool { + param_ty.codegen_repr() == PhpType::Mixed + && !matches!(visible_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) +} + +/// Returns true when any callback argument must be boxed for the target signature. +fn callback_wrapper_has_boxed_args(frame: &CallbackWrapperFrame) -> bool { + frame + .boxed_offsets + .iter() + .any(|offset| *offset != NO_CALLBACK_BOX_OFFSET) +} + /// Counts integer ABI registers consumed by the visible callback argument list. fn callback_arg_abi_slots(visible_arg_types: &[PhpType]) -> usize { visible_arg_types @@ -3555,48 +3647,178 @@ fn callback_arg_abi_slots(visible_arg_types: &[PhpType]) -> usize { .sum() } -/// Shifts AArch64 callback arguments right by one slot for a hidden receiver/class id. -fn shift_callback_args_after_hidden_aarch64( +/// Saves the incoming runtime callback arguments before nested boxing calls can clobber them. +fn save_callback_visible_args( ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, visible_arg_types: &[PhpType], ) { - match visible_arg_types { - [ty] if matches!(ty.codegen_repr(), PhpType::Str) => { - ctx.emitter.instruction("mov x2, x1"); // shift the callback string length after the hidden receiver/class id - ctx.emitter.instruction("mov x1, x0"); // shift the callback string pointer after the hidden receiver/class id - } - [_] => { - ctx.emitter.instruction("mov x1, x0"); // shift the scalar callback argument after the hidden receiver/class id + let mut reg_index = 0usize; + for (index, ty) in visible_arg_types.iter().enumerate() { + let offset = frame.raw_offsets[index]; + if matches!(ty.codegen_repr(), PhpType::Str) { + let ptr_reg = abi::int_arg_reg_name(ctx.emitter.target, reg_index); + let len_reg = abi::int_arg_reg_name(ctx.emitter.target, reg_index + 1); + abi::emit_store_to_sp(ctx.emitter, ptr_reg, offset); + abi::emit_store_to_sp(ctx.emitter, len_reg, offset + 8); + reg_index += 2; + } else { + let reg = abi::int_arg_reg_name(ctx.emitter.target, reg_index); + abi::emit_store_to_sp(ctx.emitter, reg, offset); + reg_index += 1; } - [_, _] => { - ctx.emitter.instruction("mov x2, x1"); // shift the second scalar callback argument after the hidden receiver/class id - ctx.emitter.instruction("mov x1, x0"); // shift the first scalar callback argument after the hidden receiver/class id + } +} + +/// Saves an already materialized hidden receiver or called-class id into the wrapper frame. +fn save_callback_hidden_arg(ctx: &mut FunctionContext<'_>, frame: &CallbackWrapperFrame, reg: &str) { + abi::emit_store_to_sp(ctx.emitter, reg, frame.hidden_offset); +} + +/// Boxes visible runtime arguments whose target method parameters are `Mixed`. +fn box_callback_mixed_args( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, + param_types: &[PhpType], + visible_arg_types: &[PhpType], +) { + for (index, (param_ty, visible_ty)) in param_types.iter().zip(visible_arg_types.iter()).enumerate() { + let box_offset = frame.boxed_offsets[index]; + if box_offset == NO_CALLBACK_BOX_OFFSET || !callback_arg_needs_mixed_box(param_ty, visible_ty) { + continue; } - _ => {} + load_callback_raw_arg_to_result(ctx, frame.raw_offsets[index], visible_ty); + emit_box_current_value_as_mixed(ctx.emitter, &visible_ty.codegen_repr()); + abi::emit_store_to_sp(ctx.emitter, abi::int_result_reg(ctx.emitter), box_offset); + } +} + +/// Loads a saved runtime callback argument into the normal result registers. +fn load_callback_raw_arg_to_result( + ctx: &mut FunctionContext<'_>, + raw_offset: usize, + visible_ty: &PhpType, +) { + if matches!(visible_ty.codegen_repr(), PhpType::Str) { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, ptr_reg, raw_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, len_reg, raw_offset + 8); + } else { + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), raw_offset); } } -/// Shifts x86_64 callback arguments right by one slot for a hidden receiver/class id. -fn shift_callback_args_after_hidden_x86_64( +/// Loads the hidden receiver/class id and visible method parameters into callee ABI registers. +fn load_callback_target_args( ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, visible_arg_types: &[PhpType], ) { - match visible_arg_types { - [ty] if matches!(ty.codegen_repr(), PhpType::Str) => { - ctx.emitter.instruction("mov rdx, rsi"); // shift the callback string length after the hidden receiver/class id - ctx.emitter.instruction("mov rsi, rdi"); // shift the callback string pointer after the hidden receiver/class id + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + frame.hidden_offset, + ); + let mut reg_index = 1usize; + for (index, visible_ty) in visible_arg_types.iter().enumerate() { + let box_offset = frame.boxed_offsets[index]; + if box_offset != NO_CALLBACK_BOX_OFFSET { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, reg_index), + box_offset, + ); + reg_index += 1; + } else if matches!(visible_ty.codegen_repr(), PhpType::Str) { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, reg_index), + frame.raw_offsets[index], + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, reg_index + 1), + frame.raw_offsets[index] + 8, + ); + reg_index += 2; + } else { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, reg_index), + frame.raw_offsets[index], + ); + reg_index += 1; } - [_] => { - ctx.emitter.instruction("mov rsi, rdi"); // shift the scalar callback argument after the hidden receiver/class id + } +} + +/// Saves a callback return value while boxed argument temporaries are released. +fn save_callback_return_value( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, + return_ty: &PhpType, +) { + match return_ty.codegen_repr() { + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, ptr_reg, frame.return_offset); + abi::emit_store_to_sp(ctx.emitter, len_reg, frame.return_offset + 8); } - [_, _] => { - ctx.emitter.instruction("mov rdx, rsi"); // shift the second scalar callback argument after the hidden receiver/class id - ctx.emitter.instruction("mov rsi, rdi"); // shift the first scalar callback argument after the hidden receiver/class id + PhpType::Float => { + abi::emit_store_to_sp(ctx.emitter, abi::float_result_reg(ctx.emitter), frame.return_offset); + } + _ => { + abi::emit_store_to_sp(ctx.emitter, abi::int_result_reg(ctx.emitter), frame.return_offset); } - _ => {} } } +/// Restores a callback return value after boxed argument temporaries are released. +fn restore_callback_return_value( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, + return_ty: &PhpType, +) { + match return_ty.codegen_repr() { + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, ptr_reg, frame.return_offset); + abi::emit_load_temporary_stack_slot(ctx.emitter, len_reg, frame.return_offset + 8); + } + PhpType::Float => { + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::float_result_reg(ctx.emitter), frame.return_offset); + } + _ => { + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), frame.return_offset); + } + } +} + +/// Releases boxed Mixed arguments allocated by the wrapper for target `Mixed` parameters. +fn release_callback_boxed_args(ctx: &mut FunctionContext<'_>, frame: &CallbackWrapperFrame) { + for offset in &frame.boxed_offsets { + if *offset == NO_CALLBACK_BOX_OFFSET { + continue; + } + abi::emit_load_temporary_stack_slot(ctx.emitter, abi::int_result_reg(ctx.emitter), *offset); + abi::emit_decref_if_refcounted(ctx.emitter, &PhpType::Mixed); + } +} + +/// Cleans up boxed callback arguments without losing the callback return value. +fn cleanup_callback_boxed_args( + ctx: &mut FunctionContext<'_>, + frame: &CallbackWrapperFrame, + return_ty: &PhpType, +) { + if !callback_wrapper_has_boxed_args(frame) { + return; + } + save_callback_return_value(ctx, frame, return_ty); + release_callback_boxed_args(ctx, frame); + restore_callback_return_value(ctx, frame, return_ty); +} + /// Emits a local wrapper that prepends the hidden static called-class id. fn emit_static_method_callback_wrapper( ctx: &mut FunctionContext<'_>, @@ -3623,25 +3845,26 @@ fn emit_static_method_callback_wrapper_aarch64( target: &StaticMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name( - ctx.emitter.target, - callback_arg_abi_slots(visible_arg_types), - ); - ctx.emitter.instruction("sub sp, sp, #16"); // reserve wrapper spill space for the runtime callback return address - ctx.emitter.instruction("str x30, [sp, #8]"); // preserve the runtime helper return address across the static method call + let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); + abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); + abi::emit_store_to_sp(ctx.emitter, "x30", frame.return_address_offset); + save_callback_visible_args(ctx, &frame, visible_arg_types); match target.called_class { StaticCallbackCalledClass::Immediate(class_id) => { abi::emit_load_int_immediate(ctx.emitter, "x3", class_id as i64); } StaticCallbackCalledClass::Env => { - ctx.emitter.instruction(&format!("ldr x3, [{}]", env_reg)); // load the late-static called-class id from the callback environment + abi::emit_load_from_address(ctx.emitter, "x3", env_reg, 0); } } - shift_callback_args_after_hidden_aarch64(ctx, visible_arg_types); - ctx.emitter.instruction("mov x0, x3"); // pass the called-class id as the hidden static method argument + save_callback_hidden_arg(ctx, &frame, "x3"); + box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); + load_callback_target_args(ctx, &frame, visible_arg_types); emit_static_callback_dispatch(ctx, target); - ctx.emitter.instruction("ldr x30, [sp, #8]"); // restore the runtime helper return address after the static method call - ctx.emitter.instruction("add sp, sp, #16"); // release the wrapper spill space before returning to the runtime helper + cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x30", frame.return_address_offset); + abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("ret"); // return the static method result to the runtime callback helper } @@ -3651,24 +3874,26 @@ fn emit_static_method_callback_wrapper_x86_64( target: &StaticMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name( - ctx.emitter.target, - callback_arg_abi_slots(visible_arg_types), - ); + let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); ctx.emitter.instruction("push rbp"); // preserve the runtime helper frame pointer for the nested static method call ctx.emitter.instruction("mov rbp, rsp"); // establish a wrapper frame while shifting callback arguments + abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); + save_callback_visible_args(ctx, &frame, visible_arg_types); match target.called_class { StaticCallbackCalledClass::Immediate(class_id) => { abi::emit_load_int_immediate(ctx.emitter, "rcx", class_id as i64); } StaticCallbackCalledClass::Env => { - ctx.emitter - .instruction(&format!("mov rcx, QWORD PTR [{}]", env_reg)); // load the late-static called-class id from the callback environment + abi::emit_load_from_address(ctx.emitter, "rcx", env_reg, 0); } } - shift_callback_args_after_hidden_x86_64(ctx, visible_arg_types); - ctx.emitter.instruction("mov rdi, rcx"); // pass the called-class id as the hidden static method argument + save_callback_hidden_arg(ctx, &frame, "rcx"); + box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); + load_callback_target_args(ctx, &frame, visible_arg_types); emit_static_callback_dispatch(ctx, target); + cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); + abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("pop rbp"); // restore the runtime helper frame pointer before returning ctx.emitter.instruction("ret"); // return the static method result to the runtime callback helper } @@ -3701,18 +3926,19 @@ fn emit_instance_method_callback_wrapper_aarch64( target: &InstanceMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name( - ctx.emitter.target, - callback_arg_abi_slots(visible_arg_types), - ); - ctx.emitter.instruction("sub sp, sp, #16"); // reserve wrapper spill space for the runtime callback return address - ctx.emitter.instruction("str x30, [sp, #8]"); // preserve the runtime helper return address across the instance method call - ctx.emitter.instruction(&format!("ldr x3, [{}]", env_reg)); // load the captured object receiver from the callback environment - shift_callback_args_after_hidden_aarch64(ctx, visible_arg_types); - ctx.emitter.instruction("mov x0, x3"); // pass the captured object receiver as the method receiver + let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); + abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); + abi::emit_store_to_sp(ctx.emitter, "x30", frame.return_address_offset); + save_callback_visible_args(ctx, &frame, visible_arg_types); + abi::emit_load_from_address(ctx.emitter, "x3", env_reg, 0); + save_callback_hidden_arg(ctx, &frame, "x3"); + box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); + load_callback_target_args(ctx, &frame, visible_arg_types); abi::emit_call_label(ctx.emitter, &target.entry_label); - ctx.emitter.instruction("ldr x30, [sp, #8]"); // restore the runtime helper return address after the instance method call - ctx.emitter.instruction("add sp, sp, #16"); // release the wrapper spill space before returning to the runtime helper + cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); + abi::emit_load_temporary_stack_slot(ctx.emitter, "x30", frame.return_address_offset); + abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("ret"); // return the instance method result to the runtime callback helper } @@ -3722,17 +3948,19 @@ fn emit_instance_method_callback_wrapper_x86_64( target: &InstanceMethodCallbackTarget, visible_arg_types: &[PhpType], ) { - let env_reg = abi::int_arg_reg_name( - ctx.emitter.target, - callback_arg_abi_slots(visible_arg_types), - ); + let env_reg = abi::int_arg_reg_name(ctx.emitter.target, callback_arg_abi_slots(visible_arg_types)); + let frame = callback_wrapper_frame(&target.param_types, visible_arg_types); ctx.emitter.instruction("push rbp"); // preserve the runtime helper frame pointer for the nested instance method call ctx.emitter.instruction("mov rbp, rsp"); // establish a wrapper frame while shifting callback arguments - ctx.emitter - .instruction(&format!("mov rcx, QWORD PTR [{}]", env_reg)); // load the captured object receiver from the callback environment - shift_callback_args_after_hidden_x86_64(ctx, visible_arg_types); - ctx.emitter.instruction("mov rdi, rcx"); // pass the captured object receiver as the method receiver + abi::emit_reserve_temporary_stack(ctx.emitter, frame.total_bytes); + save_callback_visible_args(ctx, &frame, visible_arg_types); + abi::emit_load_from_address(ctx.emitter, "rcx", env_reg, 0); + save_callback_hidden_arg(ctx, &frame, "rcx"); + box_callback_mixed_args(ctx, &frame, &target.param_types, visible_arg_types); + load_callback_target_args(ctx, &frame, visible_arg_types); abi::emit_call_label(ctx.emitter, &target.entry_label); + cleanup_callback_boxed_args(ctx, &frame, &target.return_ty); + abi::emit_release_temporary_stack(ctx.emitter, frame.total_bytes); ctx.emitter.instruction("pop rbp"); // restore the runtime helper frame pointer before returning ctx.emitter.instruction("ret"); // return the instance method result to the runtime callback helper } From 36836dcf80c554eec9b2a9dbc8e2708bc582f223 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 00:36:30 +0200 Subject: [PATCH 0901/1208] Track ReflectionClass targets in checker --- src/types/checker/driver/init.rs | 1 + .../checker/inference/objects/methods.rs | 29 ++++++- src/types/checker/mod.rs | 2 + .../checker/stmt_check/assignments/locals.rs | 82 +++++++++++++++++++ tests/codegen/spl/autoload.rs | 44 +++++----- 5 files changed, 135 insertions(+), 23 deletions(-) diff --git a/src/types/checker/driver/init.rs b/src/types/checker/driver/init.rs index eacfde53a3..f8eb8f2b82 100644 --- a/src/types/checker/driver/init.rs +++ b/src/types/checker/driver/init.rs @@ -91,6 +91,7 @@ impl Checker { callable_captures: HashMap::new(), callable_array_targets: HashMap::new(), first_class_callable_targets: HashMap::new(), + reflection_class_targets: HashMap::new(), interfaces: HashMap::new(), classes: HashMap::new(), declared_classes: HashSet::new(), diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index e1ef521d49..5b4c2711b8 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -40,7 +40,10 @@ impl Checker { return self .infer_method_call_on_interface_type(class_name, method, args, expr, env); } - return self.infer_method_call_on_class_type(class_name, method, args, expr, env); + let return_ty = self.infer_method_call_on_class_type(class_name, method, args, expr, env)?; + return Ok(self + .tracked_reflection_class_method_return_type(object, method) + .unwrap_or(return_ty)); } // Method calls on a union object type are allowed when the union has a // single object class. `?Foo` / `Foo|null` faults on a null receiver as in @@ -64,7 +67,11 @@ impl Checker { env, ); } - return self.infer_method_call_on_class_type(&class_name, method, args, expr, env); + let return_ty = + self.infer_method_call_on_class_type(&class_name, method, args, expr, env)?; + return Ok(self + .tracked_reflection_class_method_return_type(object, method) + .unwrap_or(return_ty)); } // Union of two or more distinct object classes (`A|B`, `A|B|false`): // the method must exist on every object member; codegen dispatches on @@ -165,6 +172,24 @@ impl Checker { } } + /// Returns a concrete reflected object type for tracked `ReflectionClass` construction helpers. + fn tracked_reflection_class_method_return_type( + &self, + object: &Expr, + method: &str, + ) -> Option { + let ExprKind::Variable(name) = &object.kind else { + return None; + }; + let reflected_class = self.reflection_class_targets.get(name)?; + match php_symbol_key(method).as_str() { + "newinstance" | "newinstanceargs" | "newinstancewithoutconstructor" => { + Some(PhpType::Object(reflected_class.clone())) + } + _ => None, + } + } + /// Infers the type of a nullsafe method call expression (`$obj?->method(...)`). /// /// Returns `PhpType::Void` for invalid receivers. For valid nullable object diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index 9b61dfe047..eb0cf7f3c3 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -98,6 +98,8 @@ pub(crate) struct Checker { pub callable_array_targets: HashMap, /// Tracks first-class callable targets assigned to variables, keyed by variable name. pub first_class_callable_targets: HashMap, + /// Tracks `ReflectionClass` locals whose reflected class is statically known. + pub reflection_class_targets: HashMap, /// Interface definitions collected during the first pass, keyed by canonical name. pub interfaces: HashMap, /// Class definitions collected during the first pass, keyed by canonical name. diff --git a/src/types/checker/stmt_check/assignments/locals.rs b/src/types/checker/stmt_check/assignments/locals.rs index 173e1ef988..d97c40e368 100644 --- a/src/types/checker/stmt_check/assignments/locals.rs +++ b/src/types/checker/stmt_check/assignments/locals.rs @@ -147,6 +147,7 @@ pub(super) fn check_assign( } else { value }; + let reflection_class_target = reflection_class_assignment_target(checker, callable_source, env); let ty_result: Result = (|| { if let Some(default) = null_coalesce_default { if let Some(existing) = env.get(name).cloned() { @@ -181,6 +182,7 @@ pub(super) fn check_assign( } let ty = ty_result?; metadata_result?; + update_reflection_class_assignment_metadata(checker, name, reflection_class_target); merge_local_assignment_type(checker, name, &ty, span, env) } @@ -277,6 +279,7 @@ fn check_ref_assign_variable( } else { clear_callable_metadata(checker, target); } + copy_reflection_class_metadata(checker, target, source); Ok(()) } @@ -457,6 +460,82 @@ pub(super) fn update_callable_assignment_metadata( Ok(()) } +/// Updates static `ReflectionClass` metadata for one assigned local. +fn update_reflection_class_assignment_metadata( + checker: &mut Checker, + name: &str, + reflected_class: Option, +) { + if let Some(reflected_class) = reflected_class { + checker + .reflection_class_targets + .insert(name.to_string(), reflected_class); + } else { + checker.reflection_class_targets.remove(name); + } +} + +/// Mirrors static `ReflectionClass` metadata across a reference alias assignment. +fn copy_reflection_class_metadata(checker: &mut Checker, target: &str, source: &str) { + if let Some(reflected_class) = checker.reflection_class_targets.get(source).cloned() { + checker + .reflection_class_targets + .insert(target.to_string(), reflected_class); + } else { + checker.reflection_class_targets.remove(target); + } +} + +/// Resolves the statically-known class reflected by a `new ReflectionClass(...)` expression. +fn reflection_class_assignment_target( + checker: &Checker, + source: &Expr, + env: &TypeEnv, +) -> Option { + let ExprKind::NewObject { class_name, args } = &source.kind else { + return None; + }; + if php_symbol_key(class_name.as_str().trim_start_matches('\\')) != "reflectionclass" { + return None; + } + let class_arg = reflection_class_constructor_arg(args)?; + match &class_arg.kind { + ExprKind::StringLiteral(class_name) => { + resolve_class_name(checker, class_name).map(str::to_string) + } + ExprKind::ClassConstant { receiver } => { + resolve_static_receiver_class(checker, receiver, class_arg.span).ok() + } + _ => match env + .get(variable_name(class_arg).unwrap_or_default()) + .cloned() + .unwrap_or_else(|| crate::types::checker::infer_expr_type_syntactic(class_arg)) + .codegen_repr() + { + PhpType::Object(class_name) if !class_name.is_empty() => Some(class_name), + _ => None, + }, + } +} + +/// Returns the ReflectionClass constructor class argument after simple named-argument matching. +fn reflection_class_constructor_arg(args: &[Expr]) -> Option<&Expr> { + if let Some(positional) = args.iter().find(|arg| !matches!(arg.kind, ExprKind::NamedArg { .. })) { + return Some(positional); + } + args.iter().find_map(|arg| { + let ExprKind::NamedArg { name, value } = &arg.kind else { + return None; + }; + match php_symbol_key(name).as_str() { + "class" | "classname" | "class_name" | "objectorclass" | "object_or_class" => { + Some(value.as_ref()) + } + _ => None, + } + }) +} + /// Returns true when a local assignment stores an array whose elements are callable descriptors. fn is_callable_array_type(ty: &PhpType) -> bool { match ty { @@ -698,6 +777,8 @@ pub(super) fn check_typed_assign( )); } env.insert(name.to_string(), declared_ty); + let reflected_class = reflection_class_assignment_target(checker, value, env); + update_reflection_class_assignment_metadata(checker, name, reflected_class); Ok(()) } @@ -736,6 +817,7 @@ pub(super) fn check_list_unpack( let unpack_ty = *elem_ty.clone(); env.insert(var.clone(), unpack_ty.clone()); update_list_unpack_callable_metadata(checker, var, value, &unpack_ty); + checker.reflection_class_targets.remove(var); } } _ => { diff --git a/tests/codegen/spl/autoload.rs b/tests/codegen/spl/autoload.rs index 2a0ff05016..65de9f23c4 100644 --- a/tests/codegen/spl/autoload.rs +++ b/tests/codegen/spl/autoload.rs @@ -908,27 +908,29 @@ fn test_class_exists_with_int_nonzero_triggers_autoload() { /// Verifies class exists dynamic autoload arg does not trigger aot autoload. #[test] fn test_class_exists_dynamic_autoload_arg_does_not_trigger_aot_autoload() { - // class_exists with a variable (non-literal) second arg must not trigger AOT autoload; panics. - let result = std::panic::catch_unwind(|| { - compile_and_run_files( - &[ - ( - "composer.json", - r#"{"autoload":{"psr-4":{"App\\":"src/"}}}"#, - ), - ( - "src/DynamicFlag.php", - " Date: Sun, 28 Jun 2026 15:44:40 +0200 Subject: [PATCH 0902/1208] test(eval): lock bridge invariant regressions --- crates/elephc-magician/src/ffi/tests.rs | 16 ++ tests/codegen/eval.rs | 188 ++++++++++++++++++++++++ 2 files changed, 204 insertions(+) diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs index c6a565bfe5..5848d7bd4a 100644 --- a/crates/elephc-magician/src/ffi/tests.rs +++ b/crates/elephc-magician/src/ffi/tests.rs @@ -1532,6 +1532,22 @@ fn execute_rejects_php_opening_tags_as_parse_errors() { assert_eq!(status, EvalStatus::ParseError.code()); } +/// Verifies execute maps invalid ABI code storage to runtime fatal instead of panicking. +#[test] +fn execute_rejects_null_code_pointer_with_nonzero_length() { + let status = unsafe { + __elephc_eval_execute( + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null(), + 1, + std::ptr::null_mut(), + ) + }; + + assert_eq!(status, EvalStatus::RuntimeFatal.code()); +} + /// Verifies scope set/get expose runtime-cell handles and dirty flags through the ABI. #[test] fn scope_set_get_round_trips_cell_and_flags() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 80203dabe5..ecd3446454 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -13,6 +13,14 @@ use crate::support::*; +/// Asserts an eval failure stayed inside the C ABI/error-status contract. +fn assert_no_rust_panic_leaked(stderr: &str) { + assert!( + !stderr.contains("panicked at") && !stderr.contains("thread '"), + "eval failure leaked Rust panic diagnostics: {stderr}" + ); +} + /// Verifies `eval` is resolved as a language construct, not a PHP-visible callable function. #[test] fn test_eval_is_not_function_exists_or_callable() { @@ -75,6 +83,75 @@ fn test_non_eval_program_does_not_request_eval_bridge() { let _ = fs::remove_dir_all(&dir); } +/// Verifies non-eval builtin probes stay static and do not request the eval bridge. +#[test] +fn test_non_eval_builtin_probes_remain_static_without_eval_bridge() { + let dir = make_cli_test_dir("elephc_no_eval_builtin_static_contract"); + let source = r#" Date: Sun, 28 Jun 2026 16:12:50 +0200 Subject: [PATCH 0903/1208] test(eval): add builtin parity metadata checks --- .../src/interpreter/builtin_metadata.rs | 41 ++++++ .../interpreter/builtins/registry/binding.rs | 44 +++--- crates/elephc-magician/src/interpreter/mod.rs | 1 + crates/elephc-magician/src/lib.rs | 1 + src/builtin_metadata.rs | 62 +++++++++ src/lib.rs | 2 + tests/builtin_parity_tests.rs | 128 ++++++++++++++++++ tests/codegen/eval_builtin_parity.rs | 71 ++++++++++ tests/codegen/mod.rs | 1 + 9 files changed, 332 insertions(+), 19 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtin_metadata.rs create mode 100644 src/builtin_metadata.rs create mode 100644 tests/builtin_parity_tests.rs create mode 100644 tests/codegen/eval_builtin_parity.rs diff --git a/crates/elephc-magician/src/interpreter/builtin_metadata.rs b/crates/elephc-magician/src/interpreter/builtin_metadata.rs new file mode 100644 index 0000000000..094491f5b3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtin_metadata.rs @@ -0,0 +1,41 @@ +//! Purpose: +//! Public metadata view for eval-interpreter builtin support. +//! Gives parity tests a stable API for builtin existence and named-argument +//! parameter lists without duplicating the interpreter registry. +//! +//! Called from: +//! - `elephc-magician::builtin_metadata` re-export. +//! +//! Key details: +//! - Lookup normalizes names with PHP-style case-insensitivity. +//! - Parameter names are the same registry data used by eval named-argument binding. + +use super::builtins::{eval_builtin_param_names, eval_php_visible_builtin_exists}; + +/// A compact, comparison-friendly view of an eval builtin call signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuiltinSignatureMetadata { + /// Parameter names in PHP call order. + pub params: Vec, +} + +/// Returns whether the eval interpreter exposes a PHP-visible builtin name. +pub fn php_visible_builtin_exists(name: &str) -> bool { + let canonical = php_symbol_key(name); + eval_php_visible_builtin_exists(&canonical) +} + +/// Returns comparison metadata for one eval builtin signature, when named calls are tracked. +pub fn builtin_signature_metadata(name: &str) -> Option { + let canonical = php_symbol_key(name); + let params = eval_builtin_param_names(&canonical)? + .iter() + .map(|param| (*param).to_string()) + .collect::>(); + Some(BuiltinSignatureMetadata { params }) +} + +/// Normalizes a PHP symbol name for case-insensitive builtin lookup. +fn php_symbol_key(name: &str) -> String { + name.trim_start_matches('\\').to_ascii_lowercase() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index e52b35d630..197ffeea2e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -109,7 +109,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "array_fill" => Some(&["start_index", "count", "value"]), "array_fill_keys" => Some(&["keys", "value"]), "array_filter" => Some(&["array", "callback", "mode"]), - "array_map" => Some(&["callback", "array"]), + "array_map" => Some(&["callback", "array", "arrays"]), "array_reduce" => Some(&["array", "callback", "initial"]), "array_walk" => Some(&["array", "callback"]), "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), @@ -118,13 +118,17 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { Some(&["array"]) } + "array_merge" => Some(&["arrays"]), + "array_diff" | "array_intersect" | "array_diff_key" | "array_intersect_key" => { + Some(&["array", "arrays"]) + } "array_push" | "array_unshift" => Some(&["array", "values"]), "array_key_exists" => Some(&["key", "array"]), "array_pad" => Some(&["array", "length", "value"]), - "array_reverse" => Some(&["array", "preserve_keys"]), + "array_reverse" => Some(&["array"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), "array_slice" => Some(&["array", "offset", "length"]), - "array_splice" => Some(&["array", "offset", "length", "replacement"]), + "array_splice" => Some(&["array", "offset", "length"]), "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), "atan2" => Some(&["y", "x"]), @@ -133,9 +137,10 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "hex2bin" | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => Some(&["string"]), "boolval" | "empty" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" - | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" - | "is_iterable" | "is_long" | "is_nan" | "is_null" | "is_numeric" | "is_object" - | "is_real" | "is_resource" | "is_string" | "is_callable" | "strval" => Some(&["value"]), + | "is_double" | "is_float" | "is_int" | "is_integer" | "is_iterable" | "is_long" + | "is_null" | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" + | "is_callable" | "strval" => Some(&["value"]), + "is_finite" | "is_infinite" | "is_nan" => Some(&["num"]), "settype" => Some(&["var", "type"]), "get_called_class" => Some(&[]), "get_class" => Some(&["object"]), @@ -143,7 +148,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "get_class_vars" => Some(&["class"]), "get_object_vars" => Some(&["object"]), "get_parent_class" => Some(&["object_or_class"]), - "call_user_func" => Some(&["callback"]), + "call_user_func" => Some(&["callback", "args"]), "call_user_func_array" => Some(&["callback", "args"]), "class_alias" => Some(&["class", "alias", "autoload"]), "class_attribute_args" => Some(&["class_name", "attribute_name"]), @@ -176,7 +181,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "dirname" => Some(&["path", "levels"]), "disk_free_space" | "disk_total_space" => Some(&["directory"]), "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), - "explode" => Some(&["separator", "string"]), + "explode" => Some(&["separator", "string", "limit"]), "fdiv" | "fmod" => Some(&["num1", "num2"]), "fclose" | "fgetc" @@ -229,7 +234,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "hash_file" => Some(&["algo", "filename", "binary"]), "hash_final" => Some(&["context", "binary"]), "hash_hmac" => Some(&["algo", "data", "key", "binary"]), - "hash_init" => Some(&["algo"]), + "hash_init" => Some(&["algo", "flags", "key"]), "hash_update" => Some(&["context", "data"]), "gzcompress" | "gzdeflate" => Some(&["data", "level"]), "gzinflate" | "gzuncompress" => Some(&["data", "max_length"]), @@ -251,11 +256,11 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), "log" => Some(&["num", "base"]), - "max" | "min" => Some(&["value"]), + "max" | "min" => Some(&["value", "values"]), "md5" | "sha1" => Some(&["string", "binary"]), "microtime" => Some(&["as_float"]), "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), - "nl2br" => Some(&["string", "use_xhtml"]), + "nl2br" => Some(&["string"]), "number_format" => Some(&[ "num", "decimals", @@ -270,17 +275,18 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "pclose" => Some(&["handle"]), "popen" => Some(&["command", "mode"]), "pow" => Some(&["num", "exponent"]), - "preg_match" => Some(&["pattern", "subject", "matches", "flags", "offset"]), - "preg_match_all" => Some(&["pattern", "subject", "matches", "flags", "offset"]), - "preg_replace" => Some(&["pattern", "replacement", "subject", "limit", "count"]), - "preg_replace_callback" => Some(&["pattern", "callback", "subject", "limit", "count"]), + "preg_match" => Some(&["pattern", "subject", "matches"]), + "preg_match_all" => Some(&["pattern", "subject"]), + "preg_replace" => Some(&["pattern", "replacement", "subject"]), + "preg_replace_callback" => Some(&["pattern", "callback", "subject"]), "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), "print_r" | "var_dump" => Some(&["value"]), "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "range" => Some(&["start", "end"]), "readline" => Some(&["prompt"]), - "realpath" | "stream_resolve_include_path" => Some(&["path"]), + "realpath" => Some(&["path"]), + "stream_resolve_include_path" => Some(&["filename"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), @@ -327,8 +333,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } "stream_socket_get_name" => Some(&["socket", "remote"]), "stream_socket_pair" => Some(&["domain", "type", "protocol"]), - "stream_socket_recvfrom" => Some(&["stream", "length", "flags", "address"]), - "stream_socket_sendto" => Some(&["stream", "data", "flags", "address"]), + "stream_socket_recvfrom" => Some(&["socket", "length", "flags", "address"]), + "stream_socket_sendto" => Some(&["socket", "data", "flags", "address"]), "stream_socket_shutdown" => Some(&["stream", "mode"]), "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), @@ -337,7 +343,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "strtotime" => Some(&["datetime"]), "strstr" => Some(&["haystack", "needle", "before_needle"]), "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), - "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject"]), + "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject", "count"]), "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), "str_repeat" => Some(&["string", "times"]), "str_split" => Some(&["string", "length"]), diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 2f582c794f..b480c5c4b6 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -12,6 +12,7 @@ //! to `RuntimeValueOps`, which will be backed by elephc runtime hooks. mod array_literals; +pub mod builtin_metadata; mod builtin_interfaces; mod builtins; mod constant_eval; diff --git a/crates/elephc-magician/src/lib.rs b/crates/elephc-magician/src/lib.rs index 315237b116..11c9306369 100644 --- a/crates/elephc-magician/src/lib.rs +++ b/crates/elephc-magician/src/lib.rs @@ -26,4 +26,5 @@ pub mod scope; mod stream_resources; pub mod value; +pub use interpreter::builtin_metadata; pub use ffi::*; diff --git a/src/builtin_metadata.rs b/src/builtin_metadata.rs new file mode 100644 index 0000000000..32bcbac18c --- /dev/null +++ b/src/builtin_metadata.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Public builtin metadata snapshots used by parity tests and external audits. +//! Keeps catalog and call-signature details observable without duplicating +//! builtin lists outside the compiler-owned sources of truth. +//! +//! Called from: +//! - Rust integration tests that compare `elephc` and `elephc-magician` builtin support. +//! +//! Key details: +//! - Names come from the checker builtin catalog. +//! - Signature shapes are derived from `FunctionSig`, not maintained by hand. + +/// A compact, comparison-friendly view of a builtin call signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuiltinSignatureMetadata { + /// Parameter names in PHP call order. + pub params: Vec, + /// Number of leading parameters that must be supplied positionally or by name. + pub required_param_count: usize, + /// Number of parameters that carry explicit default values. + pub default_param_count: usize, + /// Name of the variadic parameter, when the builtin accepts one. + pub variadic: Option, + /// Parameter names that must be passed by reference. + pub by_ref_params: Vec, +} + +/// Returns the compiler's PHP-visible builtin names. +pub fn php_visible_builtin_names() -> &'static [&'static str] { + crate::types::checker::builtins::supported_builtin_function_names() +} + +/// Returns comparison metadata for one builtin signature, when the compiler tracks it. +pub fn builtin_signature_metadata(name: &str) -> Option { + let canonical = crate::names::php_symbol_key(name.trim_start_matches('\\')); + let sig = crate::types::builtin_call_sig(&canonical)?; + let params = sig + .params + .iter() + .map(|(name, _)| name.clone()) + .collect::>(); + let required_param_count = sig + .defaults + .iter() + .position(Option::is_some) + .unwrap_or(sig.params.len()); + let default_param_count = sig.defaults.iter().filter(|default| default.is_some()).count(); + let by_ref_params = sig + .params + .iter() + .zip(sig.ref_params.iter()) + .filter_map(|((name, _), is_ref)| is_ref.then(|| name.clone())) + .collect::>(); + + Some(BuiltinSignatureMetadata { + params, + required_param_count, + default_param_count, + variadic: sig.variadic, + by_ref_params, + }) +} diff --git a/src/lib.rs b/src/lib.rs index 4630ece030..26e1d782c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ //! - Public module boundaries here are part of the crate-facing compiler API. pub mod autoload; +/// Builtin catalog and signature metadata snapshots. +pub mod builtin_metadata; /// Single-source builtin registry: catalog, signatures, type-check, and lowering dispatch. pub mod builtins; /// Canonical EIR-consuming assembly backend and public codegen helpers. diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs new file mode 100644 index 0000000000..a34ff25646 --- /dev/null +++ b/tests/builtin_parity_tests.rs @@ -0,0 +1,128 @@ +//! Purpose: +//! Integration tests for builtin catalog parity between static elephc and +//! elephc-magician's eval interpreter. +//! +//! Called from: +//! - `cargo test --test builtin_parity_tests` through Rust's test harness. +//! +//! Key details: +//! - Static builtin names and signatures are read from compiler metadata APIs. +//! - Eval builtin existence and named-argument parameters are read from magician metadata APIs. + +use std::collections::BTreeSet; + +/// Static-only raw memory helpers are elephc extensions tied to AOT FFI values. +const STATIC_ONLY_RAW_MEMORY_BUILTINS: &[&str] = &[ + "buffer_free", + "buffer_len", + "buffer_new", + "ptr", + "ptr_get", + "ptr_is_null", + "ptr_null", + "ptr_offset", + "ptr_read16", + "ptr_read32", + "ptr_read8", + "ptr_read_string", + "ptr_set", + "ptr_sizeof", + "ptr_write16", + "ptr_write32", + "ptr_write8", + "ptr_write_string", +]; + +/// Eval-only reflection probes exist because magician can inspect dynamic eval metadata before the AOT catalog exposes them. +const EVAL_ONLY_REFLECTION_BUILTINS: &[&str] = &[ + "get_called_class", + "get_class_methods", + "get_class_vars", + "get_object_vars", +]; + +/// Verifies every non-raw-memory static builtin is visible through eval's function lookup. +#[test] +fn static_php_visible_builtins_are_visible_to_eval() { + let static_only = STATIC_ONLY_RAW_MEMORY_BUILTINS + .iter() + .copied() + .collect::>(); + let missing = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .filter(|name| !static_only.contains(name)) + .filter(|name| !elephc_magician::builtin_metadata::php_visible_builtin_exists(name)) + .collect::>(); + + assert!( + missing.is_empty(), + "static builtins missing from eval function lookup: {missing:?}" + ); +} + +/// Verifies eval has named-argument parameter metadata for each shared static builtin. +#[test] +fn shared_builtin_parameter_names_match_static_signatures() { + let static_only = STATIC_ONLY_RAW_MEMORY_BUILTINS + .iter() + .copied() + .collect::>(); + let mut missing_static_signature = Vec::new(); + let mut missing_eval_signature = Vec::new(); + let mut mismatched_params = Vec::new(); + + for name in elephc::builtin_metadata::php_visible_builtin_names() { + if static_only.contains(name) { + continue; + } + let Some(static_meta) = elephc::builtin_metadata::builtin_signature_metadata(name) else { + missing_static_signature.push(*name); + continue; + }; + let Some(eval_meta) = elephc_magician::builtin_metadata::builtin_signature_metadata(name) else { + missing_eval_signature.push(*name); + continue; + }; + if static_meta.params != eval_meta.params { + mismatched_params.push(( + *name, + static_meta.params.clone(), + eval_meta.params.clone(), + )); + } + } + + assert!( + missing_static_signature.is_empty(), + "static catalog entries without signature metadata: {missing_static_signature:?}" + ); + assert!( + missing_eval_signature.is_empty(), + "shared builtins without eval parameter metadata: {missing_eval_signature:?}" + ); + assert!( + mismatched_params.is_empty(), + "shared builtin parameter-name mismatches: {mismatched_params:#?}" + ); +} + +/// Documents the current eval-only reflection builtins so the drift is explicit. +#[test] +fn eval_only_reflection_builtins_remain_visible_until_static_catalog_catches_up() { + let static_names = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .collect::>(); + + for name in EVAL_ONLY_REFLECTION_BUILTINS { + assert!( + !static_names.contains(name), + "{name} moved into the static catalog; remove it from the eval-only allowlist" + ); + assert!( + elephc_magician::builtin_metadata::php_visible_builtin_exists(name), + "{name} should stay visible to eval while it is documented as eval-only" + ); + } +} diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs new file mode 100644 index 0000000000..135a30fda9 --- /dev/null +++ b/tests/codegen/eval_builtin_parity.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! End-to-end regressions for builtin lookup parity across AOT code and eval. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_builtin_parity` through Rust's test harness. +//! +//! Key details: +//! - Fixtures verify `function_exists()` and namespaced builtin fallback before +//! and after eval has introduced dynamic symbols. + +use crate::support::compile_and_run; + +/// Verifies AOT builtin lookup stays case-insensitive without eval being present. +#[test] +fn test_aot_function_exists_builtin_case_insensitive_without_eval() { + let out = compile_and_run( + r#" Date: Sun, 28 Jun 2026 16:17:43 +0200 Subject: [PATCH 0904/1208] refactor(eval): expose builtin registry names --- .../src/interpreter/builtin_metadata.rs | 10 +- .../interpreter/builtins/registry/names.rs | 801 +++++++++--------- tests/builtin_parity_tests.rs | 23 + 3 files changed, 436 insertions(+), 398 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtin_metadata.rs b/crates/elephc-magician/src/interpreter/builtin_metadata.rs index 094491f5b3..90aca40767 100644 --- a/crates/elephc-magician/src/interpreter/builtin_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtin_metadata.rs @@ -10,7 +10,10 @@ //! - Lookup normalizes names with PHP-style case-insensitivity. //! - Parameter names are the same registry data used by eval named-argument binding. -use super::builtins::{eval_builtin_param_names, eval_php_visible_builtin_exists}; +use super::builtins::{ + eval_builtin_param_names, eval_php_visible_builtin_exists, + eval_php_visible_builtin_function_names, +}; /// A compact, comparison-friendly view of an eval builtin call signature. #[derive(Debug, Clone, PartialEq, Eq)] @@ -25,6 +28,11 @@ pub fn php_visible_builtin_exists(name: &str) -> bool { eval_php_visible_builtin_exists(&canonical) } +/// Returns the eval interpreter's PHP-visible builtin names. +pub fn php_visible_builtin_names() -> &'static [&'static str] { + eval_php_visible_builtin_function_names() +} + /// Returns comparison metadata for one eval builtin signature, when named calls are tracked. pub fn builtin_signature_metadata(name: &str) -> Option { let canonical = php_symbol_key(name); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 263760eedc..539781a6ee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -5,404 +5,411 @@ //! - `crate::interpreter::builtins::registry` re-exports. //! //! Key details: -//! - Helpers are scoped to the eval interpreter and operate on already parsed -//! EvalIR call metadata or evaluated runtime-cell handles. +//! - The slice is the source of truth for PHP-visible eval builtin names. +//! - Lookup callers pass canonical lowercase PHP symbol names. + +/// PHP-visible builtin names implemented by the eval interpreter. +pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[ + "abs", + "acos", + "addslashes", + "array_chunk", + "array_column", + "array_combine", + "array_diff", + "array_diff_key", + "array_fill", + "array_fill_keys", + "array_filter", + "array_flip", + "array_intersect", + "array_intersect_key", + "array_key_exists", + "array_keys", + "array_map", + "array_merge", + "array_pad", + "array_pop", + "array_product", + "array_push", + "array_rand", + "array_reduce", + "array_reverse", + "array_search", + "array_shift", + "array_slice", + "array_splice", + "array_sum", + "array_unique", + "array_unshift", + "array_values", + "array_walk", + "arsort", + "asin", + "asort", + "atan", + "atan2", + "base64_decode", + "base64_encode", + "basename", + "bin2hex", + "boolval", + "call_user_func", + "call_user_func_array", + "ceil", + "chdir", + "chgrp", + "chmod", + "chop", + "chown", + "chr", + "clamp", + "class_alias", + "class_attribute_args", + "class_attribute_names", + "class_exists", + "class_get_attributes", + "class_implements", + "class_parents", + "class_uses", + "clearstatcache", + "closedir", + "copy", + "cos", + "cosh", + "count", + "crc32", + "ctype_alnum", + "ctype_alpha", + "ctype_digit", + "ctype_space", + "date", + "define", + "defined", + "deg2rad", + "die", + "dirname", + "disk_free_space", + "disk_total_space", + "empty", + "enum_exists", + "exec", + "exit", + "exp", + "explode", + "fclose", + "fdatasync", + "fdiv", + "feof", + "fflush", + "fgetc", + "fgetcsv", + "fgets", + "file", + "file_exists", + "file_get_contents", + "file_put_contents", + "fileatime", + "filectime", + "filegroup", + "fileinode", + "filemtime", + "fileowner", + "fileperms", + "filesize", + "filetype", + "floatval", + "flock", + "floor", + "fmod", + "fnmatch", + "fopen", + "fpassthru", + "fprintf", + "fputcsv", + "fread", + "fscanf", + "fseek", + "fsockopen", + "fstat", + "fsync", + "ftell", + "ftruncate", + "function_exists", + "fwrite", + "get_called_class", + "get_class", + "get_class_methods", + "get_class_vars", + "get_declared_classes", + "get_declared_interfaces", + "get_declared_traits", + "get_object_vars", + "get_parent_class", + "get_resource_id", + "get_resource_type", + "getcwd", + "getenv", + "gethostbyaddr", + "gethostbyname", + "gethostname", + "getprotobyname", + "getprotobynumber", + "getservbyname", + "getservbyport", + "gettype", + "glob", + "grapheme_strrev", + "gzcompress", + "gzdeflate", + "gzinflate", + "gzuncompress", + "hash", + "hash_algos", + "hash_copy", + "hash_equals", + "hash_file", + "hash_final", + "hash_hmac", + "hash_init", + "hash_update", + "hex2bin", + "html_entity_decode", + "htmlentities", + "htmlspecialchars", + "hypot", + "implode", + "in_array", + "inet_ntop", + "inet_pton", + "intdiv", + "interface_exists", + "intval", + "ip2long", + "is_a", + "is_array", + "is_bool", + "is_callable", + "is_dir", + "is_double", + "is_executable", + "is_file", + "is_finite", + "is_float", + "is_infinite", + "is_int", + "is_integer", + "is_iterable", + "is_link", + "is_long", + "is_nan", + "is_null", + "is_numeric", + "is_object", + "is_readable", + "is_real", + "is_resource", + "is_string", + "is_subclass_of", + "is_writable", + "is_writeable", + "isset", + "iterator_apply", + "iterator_count", + "iterator_to_array", + "json_decode", + "json_encode", + "json_last_error", + "json_last_error_msg", + "json_validate", + "krsort", + "ksort", + "lcfirst", + "lchgrp", + "lchown", + "link", + "linkinfo", + "log", + "log10", + "log2", + "long2ip", + "lstat", + "ltrim", + "max", + "md5", + "method_exists", + "microtime", + "min", + "mkdir", + "mktime", + "mt_rand", + "natcasesort", + "natsort", + "nl2br", + "number_format", + "opendir", + "ord", + "passthru", + "pathinfo", + "pclose", + "pfsockopen", + "php_uname", + "phpversion", + "pi", + "popen", + "pow", + "preg_match", + "preg_match_all", + "preg_replace", + "preg_replace_callback", + "preg_split", + "print_r", + "printf", + "property_exists", + "putenv", + "rad2deg", + "rand", + "random_int", + "range", + "rawurldecode", + "rawurlencode", + "readdir", + "readfile", + "readline", + "readlink", + "realpath", + "realpath_cache_get", + "realpath_cache_size", + "rename", + "rewind", + "rewinddir", + "rmdir", + "round", + "rsort", + "rtrim", + "scandir", + "settype", + "sha1", + "shell_exec", + "shuffle", + "sin", + "sinh", + "sleep", + "sort", + "spl_autoload", + "spl_autoload_call", + "spl_autoload_extensions", + "spl_autoload_functions", + "spl_autoload_register", + "spl_autoload_unregister", + "spl_classes", + "spl_object_hash", + "spl_object_id", + "sprintf", + "sqrt", + "sscanf", + "stat", + "str_contains", + "str_ends_with", + "str_ireplace", + "str_pad", + "str_repeat", + "str_replace", + "str_split", + "str_starts_with", + "strcasecmp", + "strcmp", + "stream_bucket_append", + "stream_bucket_make_writeable", + "stream_bucket_new", + "stream_bucket_prepend", + "stream_context_create", + "stream_context_get_default", + "stream_context_get_options", + "stream_context_get_params", + "stream_context_set_default", + "stream_context_set_option", + "stream_context_set_params", + "stream_copy_to_stream", + "stream_filter_append", + "stream_filter_prepend", + "stream_filter_register", + "stream_filter_remove", + "stream_get_contents", + "stream_get_filters", + "stream_get_line", + "stream_get_meta_data", + "stream_get_transports", + "stream_get_wrappers", + "stream_is_local", + "stream_isatty", + "stream_resolve_include_path", + "stream_select", + "stream_set_blocking", + "stream_set_chunk_size", + "stream_set_read_buffer", + "stream_set_timeout", + "stream_set_write_buffer", + "stream_socket_accept", + "stream_socket_client", + "stream_socket_enable_crypto", + "stream_socket_get_name", + "stream_socket_pair", + "stream_socket_recvfrom", + "stream_socket_sendto", + "stream_socket_server", + "stream_socket_shutdown", + "stream_supports_lock", + "stream_wrapper_register", + "stream_wrapper_restore", + "stream_wrapper_unregister", + "stripslashes", + "strlen", + "strpos", + "strrev", + "strrpos", + "strstr", + "strtolower", + "strtotime", + "strtoupper", + "strval", + "substr", + "substr_replace", + "symlink", + "sys_get_temp_dir", + "system", + "tan", + "tanh", + "tempnam", + "time", + "tmpfile", + "touch", + "trait_exists", + "trim", + "uasort", + "ucfirst", + "ucwords", + "uksort", + "umask", + "unlink", + "unset", + "urldecode", + "urlencode", + "usleep", + "usort", + "var_dump", + "vfprintf", + "vprintf", + "vsprintf", + "wordwrap", +]; + +/// Returns the eval interpreter's PHP-visible builtin names. +pub(in crate::interpreter) fn eval_php_visible_builtin_function_names() -> &'static [&'static str] { + EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS +} /// Returns true for PHP-visible builtin names implemented by the eval interpreter. pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> bool { - matches!( - name, - "abs" - | "addslashes" - | "array_chunk" - | "array_column" - | "array_combine" - | "array_fill" - | "array_fill_keys" - | "array_filter" - | "array_flip" - | "array_map" - | "array_reduce" - | "array_walk" - | "array_key_exists" - | "array_keys" - | "array_diff" - | "array_intersect" - | "array_diff_key" - | "array_intersect_key" - | "array_merge" - | "array_pad" - | "array_pop" - | "array_product" - | "array_push" - | "array_rand" - | "array_reverse" - | "array_search" - | "array_shift" - | "array_slice" - | "array_splice" - | "array_sum" - | "array_unique" - | "array_unshift" - | "array_values" - | "arsort" - | "asort" - | "acos" - | "asin" - | "atan" - | "atan2" - | "basename" - | "base64_decode" - | "base64_encode" - | "bin2hex" - | "ceil" - | "chgrp" - | "chdir" - | "chmod" - | "chown" - | "class_alias" - | "class_attribute_args" - | "class_attribute_names" - | "closedir" - | "call_user_func" - | "call_user_func_array" - | "class_exists" - | "class_get_attributes" - | "class_implements" - | "class_parents" - | "class_uses" - | "method_exists" - | "property_exists" - | "enum_exists" - | "interface_exists" - | "is_a" - | "is_subclass_of" - | "boolval" - | "chop" - | "chr" - | "clamp" - | "clearstatcache" - | "count" - | "copy" - | "cos" - | "cosh" - | "crc32" - | "ctype_alnum" - | "ctype_alpha" - | "ctype_digit" - | "ctype_space" - | "date" - | "define" - | "defined" - | "deg2rad" - | "die" - | "dirname" - | "disk_free_space" - | "disk_total_space" - | "empty" - | "exec" - | "exp" - | "exit" - | "explode" - | "fclose" - | "fdatasync" - | "fgetc" - | "fgetcsv" - | "fgets" - | "feof" - | "fdiv" - | "fflush" - | "file" - | "file_exists" - | "fileatime" - | "filectime" - | "filegroup" - | "file_get_contents" - | "fileinode" - | "filemtime" - | "fileowner" - | "fileperms" - | "file_put_contents" - | "filesize" - | "filetype" - | "fnmatch" - | "flock" - | "floor" - | "floatval" - | "fmod" - | "fopen" - | "fpassthru" - | "fputcsv" - | "fprintf" - | "fread" - | "fscanf" - | "fseek" - | "fstat" - | "fsync" - | "fsockopen" - | "ftell" - | "ftruncate" - | "function_exists" - | "fwrite" - | "gethostbyaddr" - | "gethostbyname" - | "gethostname" - | "getprotobyname" - | "getprotobynumber" - | "getservbyname" - | "getservbyport" - | "get_called_class" - | "get_class" - | "get_class_methods" - | "get_class_vars" - | "get_declared_classes" - | "get_declared_interfaces" - | "get_declared_traits" - | "get_object_vars" - | "get_parent_class" - | "get_resource_id" - | "get_resource_type" - | "getcwd" - | "getenv" - | "gettype" - | "glob" - | "grapheme_strrev" - | "gzcompress" - | "gzdeflate" - | "gzinflate" - | "gzuncompress" - | "hash" - | "hash_algos" - | "hash_copy" - | "hash_equals" - | "hash_file" - | "hash_final" - | "hash_hmac" - | "hash_init" - | "hash_update" - | "hex2bin" - | "html_entity_decode" - | "htmlentities" - | "htmlspecialchars" - | "hypot" - | "implode" - | "in_array" - | "inet_ntop" - | "inet_pton" - | "intdiv" - | "ip2long" - | "is_dir" - | "is_executable" - | "is_file" - | "is_link" - | "is_readable" - | "is_writable" - | "is_writeable" - | "intval" - | "link" - | "linkinfo" - | "lchgrp" - | "lchown" - | "ltrim" - | "is_callable" - | "is_array" - | "is_bool" - | "is_double" - | "is_finite" - | "is_float" - | "is_infinite" - | "is_int" - | "is_integer" - | "is_iterable" - | "isset" - | "is_long" - | "is_nan" - | "is_null" - | "is_numeric" - | "is_object" - | "is_real" - | "is_resource" - | "is_string" - | "iterator_apply" - | "iterator_count" - | "iterator_to_array" - | "json_decode" - | "json_encode" - | "json_last_error" - | "json_last_error_msg" - | "json_validate" - | "krsort" - | "ksort" - | "lcfirst" - | "log" - | "log2" - | "log10" - | "long2ip" - | "max" - | "md5" - | "microtime" - | "min" - | "mkdir" - | "mktime" - | "mt_rand" - | "natcasesort" - | "natsort" - | "nl2br" - | "number_format" - | "ord" - | "opendir" - | "passthru" - | "pathinfo" - | "pi" - | "pow" - | "php_uname" - | "phpversion" - | "pclose" - | "pfsockopen" - | "popen" - | "preg_match" - | "preg_match_all" - | "preg_replace" - | "preg_replace_callback" - | "preg_split" - | "putenv" - | "print_r" - | "rand" - | "random_int" - | "range" - | "rad2deg" - | "rawurldecode" - | "rawurlencode" - | "readfile" - | "readline" - | "readdir" - | "readlink" - | "realpath" - | "realpath_cache_get" - | "realpath_cache_size" - | "rename" - | "rewind" - | "rewinddir" - | "rsort" - | "rtrim" - | "round" - | "rmdir" - | "scandir" - | "settype" - | "shell_exec" - | "sleep" - | "sha1" - | "shuffle" - | "sin" - | "sinh" - | "sort" - | "sqrt" - | "spl_autoload" - | "spl_autoload_call" - | "spl_autoload_extensions" - | "spl_autoload_functions" - | "spl_autoload_register" - | "spl_autoload_unregister" - | "spl_classes" - | "spl_object_hash" - | "spl_object_id" - | "sscanf" - | "sprintf" - | "strcasecmp" - | "stream_bucket_append" - | "stream_bucket_make_writeable" - | "stream_bucket_new" - | "stream_bucket_prepend" - | "stream_copy_to_stream" - | "stream_context_create" - | "stream_context_get_default" - | "stream_context_get_options" - | "stream_context_get_params" - | "stream_context_set_default" - | "stream_context_set_option" - | "stream_context_set_params" - | "stream_filter_append" - | "stream_filter_prepend" - | "stream_filter_register" - | "stream_filter_remove" - | "stream_get_contents" - | "stream_get_filters" - | "stream_get_line" - | "stream_get_meta_data" - | "stream_get_transports" - | "stream_get_wrappers" - | "stream_isatty" - | "stream_is_local" - | "stream_set_blocking" - | "stream_set_chunk_size" - | "stream_set_read_buffer" - | "stream_set_timeout" - | "stream_set_write_buffer" - | "stream_supports_lock" - | "stream_resolve_include_path" - | "stream_wrapper_register" - | "stream_wrapper_restore" - | "stream_wrapper_unregister" - | "stream_select" - | "stream_socket_accept" - | "stream_socket_client" - | "stream_socket_enable_crypto" - | "stream_socket_get_name" - | "stream_socket_pair" - | "stream_socket_recvfrom" - | "stream_socket_sendto" - | "stream_socket_server" - | "stream_socket_shutdown" - | "str_contains" - | "str_ends_with" - | "str_ireplace" - | "str_repeat" - | "str_replace" - | "str_starts_with" - | "strcmp" - | "stat" - | "strlen" - | "strpos" - | "strrpos" - | "strrev" - | "str_pad" - | "str_split" - | "strstr" - | "strtotime" - | "substr" - | "stripslashes" - | "strtolower" - | "strtoupper" - | "strval" - | "symlink" - | "system" - | "sys_get_temp_dir" - | "tempnam" - | "tan" - | "tanh" - | "time" - | "touch" - | "trait_exists" - | "trim" - | "tmpfile" - | "substr_replace" - | "ucfirst" - | "ucwords" - | "uasort" - | "uksort" - | "unlink" - | "unset" - | "umask" - | "urldecode" - | "urlencode" - | "usort" - | "usleep" - | "var_dump" - | "vfprintf" - | "printf" - | "vprintf" - | "vsprintf" - | "wordwrap" - | "lstat" - ) + EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS.contains(&name) } diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index a34ff25646..3ca013dd8b 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -126,3 +126,26 @@ fn eval_only_reflection_builtins_remain_visible_until_static_catalog_catches_up( ); } } + +/// Verifies magician does not expose unexpected builtin names outside the static catalog. +#[test] +fn eval_php_visible_builtins_are_static_or_documented_eval_only() { + let static_names = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .collect::>(); + let eval_only = EVAL_ONLY_REFLECTION_BUILTINS + .iter() + .copied() + .collect::>(); + let unexpected = elephc_magician::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .filter(|name| !static_names.contains(name) && !eval_only.contains(name)) + .collect::>(); + + assert!( + unexpected.is_empty(), + "eval exposes builtins outside the static catalog and eval-only allowlist: {unexpected:?}" + ); +} From 0295f5b02284886f3184bf04831bd907750423b9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 16:38:14 +0200 Subject: [PATCH 0905/1208] refactor(eval): use pcre2 for preg builtins --- Cargo.lock | 1 - crates/elephc-magician/Cargo.toml | 1 - crates/elephc-magician/build.rs | 34 +++ .../src/interpreter/builtins/regex/engine.rs | 254 ++++++++++++++++++ .../src/interpreter/builtins/regex/mod.rs | 2 + .../src/interpreter/builtins/regex/pattern.rs | 24 +- .../interpreter/builtins/regex/replacement.rs | 1 - crates/elephc-magician/src/interpreter/mod.rs | 1 - src/codegen_support/runtime_features.rs | 25 +- tests/codegen/eval_builtin_parity.rs | 14 + 10 files changed, 328 insertions(+), 29 deletions(-) create mode 100644 crates/elephc-magician/build.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/engine.rs diff --git a/Cargo.lock b/Cargo.lock index cd55dee8ca..ba0012321f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -623,7 +623,6 @@ dependencies = [ "elephc-crypto", "flate2", "libc", - "regex", "unicode-segmentation", ] diff --git a/crates/elephc-magician/Cargo.toml b/crates/elephc-magician/Cargo.toml index e25a41e1fa..810213caa6 100644 --- a/crates/elephc-magician/Cargo.toml +++ b/crates/elephc-magician/Cargo.toml @@ -13,5 +13,4 @@ crate-type = ["staticlib", "rlib"] elephc-crypto = { path = "../elephc-crypto" } flate2 = "1" libc = "0.2" -regex = "1" unicode-segmentation = "1" diff --git a/crates/elephc-magician/build.rs b/crates/elephc-magician/build.rs new file mode 100644 index 0000000000..d2e5073e69 --- /dev/null +++ b/crates/elephc-magician/build.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Build script for native libraries used by the eval bridge crate. +//! +//! Called from: +//! - Cargo while compiling `elephc-magician`. +//! +//! Key details: +//! - Eval regex support calls PCRE2's POSIX wrapper directly, so Rust test +//! binaries that link the rlib need the same native libraries as generated +//! elephc binaries. + +use std::path::Path; + +/// Emits native PCRE2 link directives for cargo-built test binaries and rlibs. +fn main() { + for path in pcre2_library_search_paths() { + println!("cargo:rustc-link-search=native={path}"); + } + println!("cargo:rustc-link-lib=pcre2-posix"); + println!("cargo:rustc-link-lib=pcre2-8"); +} + +/// Returns common PCRE2 library directories for local macOS/Homebrew builds. +fn pcre2_library_search_paths() -> Vec<&'static str> { + [ + "/opt/homebrew/opt/pcre2/lib", + "/opt/homebrew/lib", + "/usr/local/opt/pcre2/lib", + "/usr/local/lib", + ] + .into_iter() + .filter(|path| Path::new(path).exists()) + .collect() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/engine.rs b/crates/elephc-magician/src/interpreter/builtins/regex/engine.rs new file mode 100644 index 0000000000..334c69d04b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/engine.rs @@ -0,0 +1,254 @@ +//! Purpose: +//! PCRE2 POSIX-wrapper regex engine used by eval `preg_*` builtins. +//! Provides the small capture API the eval regex modules need while sharing +//! the same native regex family as the AOT runtime path. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::pattern`. +//! - `crate::interpreter::builtins::regex` match, replace, and split helpers. +//! +//! Key details: +//! - Subject and pattern bytes are passed through PCRE2's POSIX wrapper as C strings. +//! - Match offsets are byte offsets into the original subject, matching PHP capture arrays. + +use std::ffi::CString; +use std::marker::PhantomData; + +use libc::{c_char, c_int, c_void, size_t}; + +use super::super::super::EvalStatus; + +const REG_ICASE: c_int = 0x0001; +const REG_NEWLINE: c_int = 0x0002; +const REG_DOTALL: c_int = 0x0010; +const REG_STARTEND: c_int = 0x0080; +const REG_UNGREEDY: c_int = 0x0200; +const REG_UCP: c_int = 0x0400; +const REG_UTF: c_int = 0x0040; +const REG_NOMATCH: c_int = 17; + +/// PCRE2 POSIX `regex_t` layout for the supported PCRE2 wrapper ABI. +#[repr(C)] +struct Pcre2Regex { + re_pcre2_code: *mut c_void, + re_match_data: *mut c_void, + re_endp: *const c_char, + re_nsub: size_t, + re_erroffset: size_t, + re_cflags: c_int, +} + +/// PCRE2 POSIX `regmatch_t` capture offset pair. +#[repr(C)] +#[derive(Clone, Copy)] +struct Pcre2Regmatch { + rm_so: c_int, + rm_eo: c_int, +} + +unsafe extern "C" { + /// Compiles a PCRE2 pattern through the POSIX wrapper. + fn pcre2_regcomp(regex: *mut Pcre2Regex, pattern: *const c_char, flags: c_int) -> c_int; + + /// Executes a compiled PCRE2 regex and fills capture offsets. + fn pcre2_regexec( + regex: *const Pcre2Regex, + subject: *const c_char, + nmatch: size_t, + matches: *mut Pcre2Regmatch, + flags: c_int, + ) -> c_int; + + /// Releases resources owned by a compiled PCRE2 regex. + fn pcre2_regfree(regex: *mut Pcre2Regex); +} + +/// Supported PHP regex modifiers after delimiter stripping. +#[derive(Default)] +pub(in crate::interpreter) struct EvalPregModifiers { + pub(in crate::interpreter) case_insensitive: bool, + pub(in crate::interpreter) multi_line: bool, + pub(in crate::interpreter) dot_matches_new_line: bool, + pub(in crate::interpreter) swap_greed: bool, + pub(in crate::interpreter) unicode: bool, +} + +/// A compiled PCRE2 regex plus POSIX wrapper metadata. +pub(in crate::interpreter) struct Regex { + raw: Pcre2Regex, +} + +impl Regex { + /// Compiles a delimiter-stripped pattern with PHP regex modifiers. + pub(in crate::interpreter) fn compile( + body: &[u8], + modifiers: EvalPregModifiers, + ) -> Result { + let pattern = CString::new(body).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut raw = Pcre2Regex { + re_pcre2_code: std::ptr::null_mut(), + re_match_data: std::ptr::null_mut(), + re_endp: std::ptr::null(), + re_nsub: 0, + re_erroffset: 0, + re_cflags: 0, + }; + let status = unsafe { pcre2_regcomp(&mut raw, pattern.as_ptr(), modifiers.flags()) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Self { raw }) + } + + /// Returns the number of capture slots including the full match at index 0. + pub(in crate::interpreter) fn captures_len(&self) -> usize { + self.raw.re_nsub.saturating_add(1) + } + + /// Returns whether this regex matches the subject. + pub(in crate::interpreter) fn is_match(&self, subject: &[u8]) -> bool { + self.captures(subject).is_some() + } + + /// Returns the first capture set for this regex and subject. + pub(in crate::interpreter) fn captures<'a>(&self, subject: &'a [u8]) -> Option> { + self.exec_at(subject, 0) + } + + /// Returns every non-overlapping capture set for this regex and subject. + pub(in crate::interpreter) fn captures_iter<'a>( + &self, + subject: &'a [u8], + ) -> std::vec::IntoIter> { + let mut captures = Vec::new(); + let mut cursor = 0; + while cursor <= subject.len() { + let Some(next) = self.exec_at(subject, cursor) else { + break; + }; + let Some(full_match) = next.get(0) else { + break; + }; + let end = full_match.end(); + let start = full_match.start(); + captures.push(next); + if end > cursor { + cursor = end; + } else if start < subject.len() { + cursor = start + 1; + } else { + break; + } + } + captures.into_iter() + } + + /// Executes this regex from a byte offset, returning capture offsets on match. + fn exec_at<'a>(&self, subject: &'a [u8], start: usize) -> Option> { + let subject_c = CString::new(subject).ok()?; + let mut matches = vec![Pcre2Regmatch::unmatched(); self.captures_len().max(1)]; + matches[0].rm_so = c_int::try_from(start).ok()?; + matches[0].rm_eo = c_int::try_from(subject.len()).ok()?; + let status = unsafe { + pcre2_regexec( + &self.raw, + subject_c.as_ptr(), + matches.len(), + matches.as_mut_ptr(), + REG_STARTEND, + ) + }; + if status == REG_NOMATCH || status != 0 { + return None; + } + Some(Captures { + matches: matches + .into_iter() + .map(|matched| matched.to_offsets()) + .collect(), + _subject: PhantomData, + }) + } +} + +impl Drop for Regex { + /// Releases the compiled PCRE2 regex when the wrapper is dropped. + fn drop(&mut self) { + unsafe { pcre2_regfree(&mut self.raw) }; + } +} + +impl EvalPregModifiers { + /// Converts parsed PHP modifiers into PCRE2 POSIX compile flags. + fn flags(&self) -> c_int { + let mut flags = 0; + if self.case_insensitive { + flags |= REG_ICASE; + } + if self.multi_line { + flags |= REG_NEWLINE; + } + if self.dot_matches_new_line { + flags |= REG_DOTALL; + } + if self.swap_greed { + flags |= REG_UNGREEDY; + } + if self.unicode { + flags |= REG_UTF | REG_UCP; + } + flags + } +} + +impl Pcre2Regmatch { + /// Returns an unmatched capture sentinel. + fn unmatched() -> Self { + Self { rm_so: -1, rm_eo: -1 } + } + + /// Converts a PCRE2 offset pair to an optional Rust byte range. + fn to_offsets(self) -> Option<(usize, usize)> { + let start = usize::try_from(self.rm_so).ok()?; + let end = usize::try_from(self.rm_eo).ok()?; + Some((start, end)) + } +} + +/// One regex match span. +#[derive(Clone, Copy)] +pub(in crate::interpreter) struct Match { + start: usize, + end: usize, +} + +impl Match { + /// Returns the match start byte offset. + pub(in crate::interpreter) fn start(&self) -> usize { + self.start + } + + /// Returns the match end byte offset. + pub(in crate::interpreter) fn end(&self) -> usize { + self.end + } +} + +/// Capture offsets for one regex match. +pub(in crate::interpreter) struct Captures<'a> { + matches: Vec>, + _subject: PhantomData<&'a [u8]>, +} + +impl Captures<'_> { + /// Returns the number of capture slots including the full match. + pub(in crate::interpreter) fn len(&self) -> usize { + self.matches.len() + } + + /// Returns the match span for one capture slot. + pub(in crate::interpreter) fn get(&self, index: usize) -> Option { + let (start, end) = self.matches.get(index).copied().flatten()?; + Some(Match { start, end }) + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs index 45af8e94c2..ade026f965 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs @@ -11,6 +11,7 @@ //! behavior through `RuntimeValueOps`. mod captures; +mod engine; mod match_all; mod match_one; mod pattern; @@ -21,6 +22,7 @@ mod split_helpers; mod targets; pub(in crate::interpreter) use captures::*; +pub(in crate::interpreter) use engine::*; pub(in crate::interpreter) use match_all::*; pub(in crate::interpreter) use match_one::*; pub(in crate::interpreter) use pattern::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs b/crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs index 596b45a9b6..f50830e1e1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/pattern.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Parses PHP delimited preg patterns and compiles them into Rust byte regexes. +//! Parses PHP delimited preg patterns and compiles them into PCRE2 regexes. //! //! Called from: //! - `crate::interpreter::builtins::regex` entrypoint modules. @@ -10,30 +10,14 @@ use super::super::super::*; -/// Compiles one eval PCRE-style delimited pattern into a Rust regex. +/// Compiles one eval PCRE-style delimited pattern into a PCRE2 regex. pub(in crate::interpreter) fn eval_preg_regex( pattern: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { let pattern = values.string_bytes(pattern)?; let (body, modifiers) = eval_preg_pattern_parts(&pattern)?; - let body = String::from_utf8(body).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut builder = RegexBuilder::new(&body); - builder - .case_insensitive(modifiers.case_insensitive) - .multi_line(modifiers.multi_line) - .dot_matches_new_line(modifiers.dot_matches_new_line) - .swap_greed(modifiers.swap_greed); - builder.build().map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Regex modifiers supported by eval `preg_*` pattern stripping. -#[derive(Default)] -pub(in crate::interpreter) struct EvalPregModifiers { - case_insensitive: bool, - multi_line: bool, - dot_matches_new_line: bool, - swap_greed: bool, + Regex::compile(&body, modifiers) } /// Splits a PHP delimited regex into body bytes and supported modifiers. @@ -122,7 +106,7 @@ pub(in crate::interpreter) fn eval_preg_modifiers( b'm' => parsed.multi_line = true, b's' => parsed.dot_matches_new_line = true, b'U' => parsed.swap_greed = true, - b'u' => {} + b'u' => parsed.unicode = true, _ => return Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs b/crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs index c236a9d1a5..3e901f145c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/replacement.rs @@ -8,7 +8,6 @@ //! - Supports `$n`, `${n}`, and `\n` capture references without allocating //! intermediate runtime cells. -use super::super::super::*; use super::*; /// Appends one replacement string after expanding `$n`, `${n}`, and `\n` captures. diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index b480c5c4b6..68de8d8cc3 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -68,7 +68,6 @@ use include_exec::*; use json::*; use libc_shims::*; use reflection::*; -use regex::bytes::{Captures, Regex, RegexBuilder}; use return_type_compat::*; use return_values::*; pub use runtime_ops::RuntimeValueOps; diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index cdc9ebd130..5d6bc5f4bd 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -87,8 +87,8 @@ pub fn runtime_features_for_program_and_classes( pub fn required_libraries_for_runtime_features(features: RuntimeFeatures) -> Vec { let mut libs = Vec::new(); if features.regex { - libs.push("pcre2-posix".to_string()); - libs.push("pcre2-8".to_string()); + push_required_library(&mut libs, "pcre2-posix"); + push_required_library(&mut libs, "pcre2-8"); } if features.phar_archive { libs.push("elephc_phar".to_string()); @@ -101,11 +101,22 @@ pub fn required_libraries_for_runtime_features(features: RuntimeFeatures) -> Vec libs.push("elephc_crypto".to_string()); } if features.eval { - libs.push("elephc_magician".to_string()); + // Eval code can call preg_* from dynamically parsed source, so PCRE2 is + // required even when no static preg call appears in the AOT AST. + push_required_library(&mut libs, "pcre2-posix"); + push_required_library(&mut libs, "pcre2-8"); + push_required_library(&mut libs, "elephc_magician"); } libs } +/// Appends a required link library once while preserving feature order. +fn push_required_library(libs: &mut Vec, library: &str) { + if !libs.iter().any(|existing| existing == library) { + libs.push(library.to_string()); + } +} + /// Builds the optional runtime feature set, using class metadata when codegen has it. fn runtime_features_for_program_and_classes_opt( program: &Program, @@ -928,7 +939,7 @@ mod tests { assert!(required_libraries_for_runtime_features(RuntimeFeatures::none()).is_empty()); } - /// Verifies eval runtime features request the magician bridge staticlib for final linking. + /// Verifies eval runtime features request PCRE2 plus the magician bridge staticlib for final linking. #[test] fn test_eval_runtime_features_require_elephc_magician_library() { assert_eq!( @@ -936,7 +947,11 @@ mod tests { eval: true, ..RuntimeFeatures::none() }), - vec!["elephc_magician".to_string()] + vec![ + "pcre2-posix".to_string(), + "pcre2-8".to_string(), + "elephc_magician".to_string() + ] ); } diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 135a30fda9..983a4ef78a 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -69,3 +69,17 @@ echo STRLEN("fghi");'); assert_eq!(out, "324"); } + +/// Verifies eval preg builtins use PCRE2 features that Rust regex did not support. +#[test] +fn test_eval_preg_uses_pcre2_lookaround_semantics() { + let out = compile_and_run( + r#" Date: Sun, 28 Jun 2026 16:53:03 +0200 Subject: [PATCH 0906/1208] test(eval): compare builtin signature shapes --- .../src/interpreter/builtin_metadata.rs | 25 +- .../interpreter/builtins/registry/binding.rs | 55 ++- .../src/interpreter/builtins/registry/mod.rs | 2 + .../builtins/registry/signature.rs | 325 ++++++++++++++++++ tests/builtin_parity_tests.rs | 25 +- tests/codegen/eval_builtin_parity.rs | 30 ++ 6 files changed, 429 insertions(+), 33 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/signature.rs diff --git a/crates/elephc-magician/src/interpreter/builtin_metadata.rs b/crates/elephc-magician/src/interpreter/builtin_metadata.rs index 90aca40767..589d58763d 100644 --- a/crates/elephc-magician/src/interpreter/builtin_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtin_metadata.rs @@ -8,10 +8,10 @@ //! //! Key details: //! - Lookup normalizes names with PHP-style case-insensitivity. -//! - Parameter names are the same registry data used by eval named-argument binding. +//! - Signature shape is the same registry data used by eval named-argument binding. use super::builtins::{ - eval_builtin_param_names, eval_php_visible_builtin_exists, + eval_builtin_param_names, eval_builtin_signature_shape, eval_php_visible_builtin_exists, eval_php_visible_builtin_function_names, }; @@ -20,6 +20,14 @@ use super::builtins::{ pub struct BuiltinSignatureMetadata { /// Parameter names in PHP call order. pub params: Vec, + /// Number of leading parameters that must be supplied positionally or by name. + pub required_param_count: usize, + /// Number of parameters that carry explicit default values. + pub default_param_count: usize, + /// Name of the variadic parameter, when the builtin accepts one. + pub variadic: Option, + /// Parameter names that must be passed by reference. + pub by_ref_params: Vec, } /// Returns whether the eval interpreter exposes a PHP-visible builtin name. @@ -40,7 +48,18 @@ pub fn builtin_signature_metadata(name: &str) -> Option>(); - Some(BuiltinSignatureMetadata { params }) + let shape = eval_builtin_signature_shape(&canonical)?; + Some(BuiltinSignatureMetadata { + params, + required_param_count: shape.required_param_count, + default_param_count: shape.default_param_count, + variadic: shape.variadic.map(str::to_string), + by_ref_params: shape + .by_ref_params + .iter() + .map(|param| (*param).to_string()) + .collect(), + }) } /// Normalizes a PHP symbol name for case-insensitive builtin lookup. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 197ffeea2e..9a26cd58ee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -69,32 +69,51 @@ pub(in crate::interpreter) fn bind_builtin_named_arg( Ok(()) } -/// Collects ordered bound arguments, rejecting gaps where defaults would be needed. -pub(in crate::interpreter) fn collect_contiguous_bound_args( +/// Collects ordered builtin arguments, applying PHP defaults for named-call gaps. +pub(in crate::interpreter) fn collect_bound_builtin_args( + name: &str, bound_args: Vec>, + values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let Some(last_index) = bound_args.iter().rposition(Option::is_some) else { + if !bound_args.iter().any(Option::is_some) { return Ok(Vec::new()); - }; - bound_args - .into_iter() - .take(last_index + 1) - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal) + } + + let shape = eval_builtin_signature_shape(name).ok_or(EvalStatus::RuntimeFatal)?; + let last_index = bound_args + .iter() + .rposition(Option::is_some) + .expect("non-empty bound args has a last supplied arg"); + let mut args = Vec::with_capacity(last_index + 1); + + for (index, arg) in bound_args.into_iter().take(last_index + 1).enumerate() { + if let Some(value) = arg { + args.push(value); + } else if index >= shape.required_param_count { + args.push(eval_builtin_default_arg(name, index, values)?); + } else { + return Err(EvalStatus::RuntimeFatal); + } + } + + Ok(args) } -/// Collects ordered builtin arguments, applying PHP defaults for special named-call gaps. -pub(in crate::interpreter) fn collect_bound_builtin_args( +/// Materializes one builtin default argument as a runtime cell. +fn eval_builtin_default_arg( name: &str, - mut bound_args: Vec>, + index: usize, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if name == "array_splice" && bound_args.get(3).is_some_and(Option::is_some) { - if bound_args.get(2).is_some_and(Option::is_none) { - bound_args[2] = Some(values.null()?); - } +) -> Result { + match eval_builtin_default_value(name, index).ok_or(EvalStatus::RuntimeFatal)? { + EvalBuiltinDefaultValue::Null => values.null(), + EvalBuiltinDefaultValue::Bool(value) => values.bool_value(value), + EvalBuiltinDefaultValue::Int(value) => values.int(value), + EvalBuiltinDefaultValue::Float(value) => values.float(value), + EvalBuiltinDefaultValue::String(value) => values.string(value), + EvalBuiltinDefaultValue::Bytes(value) => values.string_bytes_value(value), + EvalBuiltinDefaultValue::EmptyArray => values.array_new(0), } - collect_contiguous_bound_args(bound_args) } /// Returns PHP parameter names for builtin calls implemented by eval. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 887213ab4c..6e9d95915b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -14,9 +14,11 @@ mod callable; mod callable_validation; mod dispatch; mod names; +mod signature; pub(in crate::interpreter) use binding::*; pub(in crate::interpreter) use callable::*; pub(in crate::interpreter) use callable_validation::*; pub(in crate::interpreter) use dispatch::*; pub(in crate::interpreter) use names::*; +pub(in crate::interpreter) use signature::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs new file mode 100644 index 0000000000..f79805e64c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -0,0 +1,325 @@ +//! Purpose: +//! Signature-shape metadata for PHP-visible eval builtin calls. +//! Keeps named/default/variadic/by-reference shape visible to parity tests +//! without duplicating runtime dispatch behavior. +//! +//! Called from: +//! - `crate::interpreter::builtin_metadata` +//! - builtin registry tests and argument binding audits. +//! +//! Key details: +//! - Parameter names come from `eval_builtin_param_names()`. +//! - Default values mirror the dispatcher defaults so named calls can skip +//! optional parameters without changing positional semantics. + +use super::eval_builtin_param_names; + +/// Comparison-friendly shape for one eval builtin signature. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(in crate::interpreter) struct EvalBuiltinSignatureShape { + /// Number of leading parameters that must be supplied. + pub(in crate::interpreter) required_param_count: usize, + /// Number of parameters that have defaults. + pub(in crate::interpreter) default_param_count: usize, + /// Variadic parameter name when this builtin accepts extra arguments. + pub(in crate::interpreter) variadic: Option<&'static str>, + /// Parameter names that are passed by reference. + pub(in crate::interpreter) by_ref_params: &'static [&'static str], +} + +/// Runtime-materializable default value for one eval builtin parameter. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(in crate::interpreter) enum EvalBuiltinDefaultValue { + /// PHP null default. + Null, + /// PHP boolean default. + Bool(bool), + /// PHP integer default. + Int(i64), + /// PHP float default. + Float(f64), + /// PHP string default represented as UTF-8 text. + String(&'static str), + /// PHP string default represented as raw bytes. + Bytes(&'static [u8]), + /// PHP empty indexed array default. + EmptyArray, +} + +/// Returns signature-shape metadata for one PHP-visible eval builtin. +pub(in crate::interpreter) fn eval_builtin_signature_shape( + name: &str, +) -> Option { + let params = eval_builtin_param_names(name)?; + Some(match name { + "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => optional(params, 1), + + "isset" | "unset" => variadic(params, &[]), + "settype" => fixed_by_ref(params, &["var"]), + + "class_alias" => optional(params, 2), + "class_exists" | "interface_exists" | "trait_exists" | "enum_exists" + | "class_implements" | "class_parents" | "class_uses" => optional(params, 1), + "iterator_to_array" => optional(params, 1), + "iterator_apply" => optional(params, 2), + "get_class" | "get_parent_class" => optional(params, 0), + "is_a" | "is_subclass_of" => optional(params, 2), + + "count" => optional(params, 1), + "microtime" | "php_uname" | "readline" | "umask" | "exit" | "die" => { + optional(params, 0) + } + + "trim" | "ltrim" | "rtrim" | "chop" | "ucwords" | "str_split" | "wordwrap" => { + optional(params, 1) + } + "substr" | "strpos" | "strrpos" | "strstr" | "explode" | "str_pad" => { + optional(params, 2) + } + "str_replace" | "str_ireplace" => optional(params, 3), + "implode" => optional(params, 1), + "substr_replace" => optional(params, 3), + "sprintf" | "printf" | "sscanf" => variadic(params, &[]), + "fprintf" | "fscanf" => variadic(params, &[]), + + "hash" | "hash_file" => optional(params, 2), + "hash_hmac" => optional(params, 3), + "hash_init" => optional(params, 1), + "hash_final" | "md5" | "sha1" => optional(params, 1), + "number_format" => optional(params, 1), + + "array_pop" | "array_shift" => fixed_by_ref(params, &["array"]), + "sort" | "rsort" | "shuffle" | "natsort" | "natcasesort" | "asort" | "arsort" + | "ksort" | "krsort" => fixed_by_ref(params, &["array"]), + "in_array" | "array_search" => optional(params, 2), + "array_push" | "array_unshift" => variadic(params, &["array"]), + "array_merge" => variadic(params, &[]), + "array_diff" | "array_intersect" | "array_diff_key" | "array_intersect_key" => { + variadic(params, &[]) + } + "array_slice" => optional(params, 2), + "array_splice" => optional_by_ref(params, 2, &["array"]), + "array_map" => variadic(params, &[]), + "array_filter" => optional(params, 1), + "array_reduce" => optional(params, 2), + "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), + "call_user_func" => variadic(params, &[]), + + "log" | "round" | "date" => optional(params, 1), + "min" | "max" => variadic(params, &[]), + "json_encode" | "json_decode" | "json_validate" => optional(params, 1), + + "preg_match" => optional_by_ref(params, 2, &["matches"]), + "preg_split" => optional(params, 2), + + "touch" | "basename" | "dirname" | "pathinfo" => optional(params, 1), + "fnmatch" | "fopen" | "fseek" | "fputcsv" => optional(params, 2), + "flock" => optional_by_ref(params, 2, &["would_block"]), + "fgetcsv" => optional(params, 1), + "clearstatcache" => optional(params, 0), + "stream_get_contents" => optional(params, 1), + "stream_copy_to_stream" => optional(params, 2), + "stream_socket_accept" => optional_by_ref(params, 1, &["peer_name"]), + "fsockopen" | "pfsockopen" => { + optional_by_ref(params, 2, &["error_code", "error_message"]) + } + "stream_wrapper_register" | "stream_socket_enable_crypto" => optional(params, 2), + "stream_context_create" | "stream_context_get_default" => optional(params, 0), + "stream_context_set_option" => optional(params, 2), + "stream_get_line" | "stream_set_timeout" | "stream_socket_sendto" + | "stream_filter_append" | "stream_filter_prepend" => optional(params, 2), + "stream_select" => optional_by_ref(params, 4, &["read", "write", "except"]), + "stream_socket_recvfrom" => optional_by_ref(params, 2, &["address"]), + + "spl_autoload_register" | "spl_autoload_extensions" => optional(params, 0), + "spl_autoload" => optional(params, 1), + + _ => fixed(params), + }) +} + +/// Returns the concrete default value for one optional builtin parameter. +pub(in crate::interpreter) fn eval_builtin_default_value( + name: &str, + param_index: usize, +) -> Option { + use EvalBuiltinDefaultValue::*; + + Some(match (name, param_index) { + ("gzcompress" | "gzdeflate", 1) => Int(-1), + ("gzinflate" | "gzuncompress", 1) => Int(0), + + ("class_alias", 2) => Bool(true), + ( + "class_exists" | "interface_exists" | "trait_exists" | "enum_exists" + | "class_implements" | "class_parents" | "class_uses", + 1, + ) => Bool(true), + ("iterator_to_array", 1) => Bool(true), + ("iterator_apply", 2) => Null, + ("get_class" | "get_parent_class", 0) => Null, + ("is_a", 2) => Bool(false), + ("is_subclass_of", 2) => Bool(true), + + ("count", 1) => Int(0), + ("microtime", 0) => Bool(false), + ("php_uname", 0) => String("a"), + ("readline" | "umask", 0) => Null, + ("exit" | "die", 0) => Int(0), + + ("trim" | "ltrim" | "rtrim" | "chop", 1) => Bytes(b" \n\r\t\x0b\x0c\0"), + ("ucwords", 1) => Bytes(b" \t\r\n\x0c\x0b"), + ("substr", 2) => Null, + ("strpos" | "strrpos", 2) => Int(0), + ("strstr", 2) => Bool(false), + ("str_replace" | "str_ireplace", 3) => Null, + ("explode", 2) => Int(i64::MAX), + ("implode", 0) => Null, + ("substr_replace", 3) => Null, + ("str_pad", 2) => String(" "), + ("str_pad", 3) => Int(1), + ("str_split", 1) => Int(1), + ("wordwrap", 1) => Int(75), + ("wordwrap", 2) => String("\n"), + ("wordwrap", 3) => Bool(false), + + ("hash" | "hash_file", 2) => Bool(false), + ("hash_hmac", 3) => Bool(false), + ("hash_init", 1) => Int(0), + ("hash_init", 2) => String(""), + ("hash_final" | "md5" | "sha1", 1) => Bool(false), + ("number_format", 1) => Int(0), + ("number_format", 2) => String("."), + ("number_format", 3) => String(","), + + ("in_array" | "array_search", 2) => Bool(false), + ("array_slice" | "array_splice", 2) => Null, + ("array_filter", 1) => Null, + ("array_filter", 2) => Int(0), + ("array_reduce", 2) => Null, + + ("log", 1) => Float(std::f64::consts::E), + ("round", 1) => Int(0), + ("date", 1) => Null, + ("json_encode", 1) => Int(0), + ("json_encode", 2) => Int(512), + ("json_decode", 1) => Null, + ("json_decode", 2) => Int(512), + ("json_decode", 3) => Int(0), + ("json_validate", 1) => Int(512), + ("json_validate", 2) => Int(0), + + ("preg_match", 2) => EmptyArray, + ("preg_split", 2) => Int(-1), + ("preg_split", 3) => Int(0), + + ("touch", 1 | 2) => Null, + ("basename", 1) => String(""), + ("dirname", 1) => Int(1), + ("fnmatch", 2) => Int(0), + ("pathinfo", 1) => Int(15), + ("fopen", 2) => Bool(false), + ("fopen", 3) => Null, + ("flock", 2) => Null, + ("fseek", 2) => Int(0), + ("fgetcsv", 1) => Null, + ("fgetcsv", 2) => String(","), + ("fputcsv", 2) => String(","), + ("fputcsv", 3) => String("\""), + ("clearstatcache", 0) => Bool(false), + ("clearstatcache", 1) => String(""), + ("stream_get_contents", 1) => Null, + ("stream_get_contents", 2) => Int(-1), + ("stream_copy_to_stream", 2) => Null, + ("stream_copy_to_stream", 3) => Int(-1), + ("stream_socket_accept", 1 | 2) => Null, + ("fsockopen" | "pfsockopen", 2 | 3 | 4) => Null, + ("stream_wrapper_register", 2) => Int(0), + ("stream_socket_enable_crypto", 2 | 3) => Null, + ("stream_context_create", 0 | 1) => Null, + ("stream_context_get_default", 0) => Null, + ("stream_context_set_option", 2 | 3) => Null, + ("stream_get_line", 2) => String(""), + ("stream_select", 4) => Int(0), + ("stream_set_timeout", 2) => Int(0), + ("stream_socket_sendto", 2) => Int(0), + ("stream_socket_sendto", 3) => String(""), + ("stream_socket_recvfrom", 2) => Int(0), + ("stream_socket_recvfrom", 3) => String(""), + ("stream_filter_append" | "stream_filter_prepend", 2) => Int(3), + ("stream_filter_append" | "stream_filter_prepend", 3) => Null, + + ("spl_autoload_register", 0) => Null, + ("spl_autoload_register", 1) => Bool(true), + ("spl_autoload_register", 2) => Bool(false), + ("spl_autoload_extensions", 0) => Null, + ("spl_autoload", 1) => Null, + + _ => return None, + }) +} + +/// Builds fixed-arity signature shape. +fn fixed(params: &[&'static str]) -> EvalBuiltinSignatureShape { + shape(params.len(), 0, None, &[]) +} + +/// Builds fixed-arity signature shape with by-reference parameters. +fn fixed_by_ref( + params: &[&'static str], + by_ref_params: &'static [&'static str], +) -> EvalBuiltinSignatureShape { + shape(params.len(), 0, None, by_ref_params) +} + +/// Builds trailing-default signature shape. +fn optional(params: &[&'static str], required_param_count: usize) -> EvalBuiltinSignatureShape { + shape( + required_param_count, + params.len().saturating_sub(required_param_count), + None, + &[], + ) +} + +/// Builds trailing-default signature shape with by-reference parameters. +fn optional_by_ref( + params: &[&'static str], + required_param_count: usize, + by_ref_params: &'static [&'static str], +) -> EvalBuiltinSignatureShape { + shape( + required_param_count, + params.len().saturating_sub(required_param_count), + None, + by_ref_params, + ) +} + +/// Builds variadic signature shape. +fn variadic( + params: &[&'static str], + by_ref_params: &'static [&'static str], +) -> EvalBuiltinSignatureShape { + shape( + params.len().saturating_sub(1), + 1, + params.last().copied(), + by_ref_params, + ) +} + +/// Builds the raw signature-shape value. +fn shape( + required_param_count: usize, + default_param_count: usize, + variadic: Option<&'static str>, + by_ref_params: &'static [&'static str], +) -> EvalBuiltinSignatureShape { + EvalBuiltinSignatureShape { + required_param_count, + default_param_count, + variadic, + by_ref_params, + } +} diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 3ca013dd8b..c81e14b7e9 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -7,7 +7,7 @@ //! //! Key details: //! - Static builtin names and signatures are read from compiler metadata APIs. -//! - Eval builtin existence and named-argument parameters are read from magician metadata APIs. +//! - Eval builtin existence and signature shape are read from magician metadata APIs. use std::collections::BTreeSet; @@ -61,16 +61,16 @@ fn static_php_visible_builtins_are_visible_to_eval() { ); } -/// Verifies eval has named-argument parameter metadata for each shared static builtin. +/// Verifies eval has signature metadata for each shared static builtin. #[test] -fn shared_builtin_parameter_names_match_static_signatures() { +fn shared_builtin_signature_shape_matches_static_signatures() { let static_only = STATIC_ONLY_RAW_MEMORY_BUILTINS .iter() .copied() .collect::>(); let mut missing_static_signature = Vec::new(); let mut missing_eval_signature = Vec::new(); - let mut mismatched_params = Vec::new(); + let mut mismatched_signatures = Vec::new(); for name in elephc::builtin_metadata::php_visible_builtin_names() { if static_only.contains(name) { @@ -84,12 +84,13 @@ fn shared_builtin_parameter_names_match_static_signatures() { missing_eval_signature.push(*name); continue; }; - if static_meta.params != eval_meta.params { - mismatched_params.push(( - *name, - static_meta.params.clone(), - eval_meta.params.clone(), - )); + if static_meta.params != eval_meta.params + || static_meta.required_param_count != eval_meta.required_param_count + || static_meta.default_param_count != eval_meta.default_param_count + || static_meta.variadic != eval_meta.variadic + || static_meta.by_ref_params != eval_meta.by_ref_params + { + mismatched_signatures.push((*name, static_meta, eval_meta)); } } @@ -102,8 +103,8 @@ fn shared_builtin_parameter_names_match_static_signatures() { "shared builtins without eval parameter metadata: {missing_eval_signature:?}" ); assert!( - mismatched_params.is_empty(), - "shared builtin parameter-name mismatches: {mismatched_params:#?}" + mismatched_signatures.is_empty(), + "shared builtin signature-shape mismatches: {mismatched_signatures:#?}" ); } diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 983a4ef78a..3db6ce44c5 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -83,3 +83,33 @@ echo preg_match("/(?<=foo)bar/", "foobar");'); assert_eq!(out, "1:1"); } + +/// Verifies eval named builtin calls can skip optional parameters with defaults. +#[test] +fn test_eval_named_builtin_arguments_fill_default_gaps() { + let out = compile_and_run( + r#" 1], depth: 512);'); +"#, + ); + + assert_eq!(out, " x:{\"a\":1}"); +} + +/// Verifies eval named builtin calls preserve variadic and by-reference behavior. +#[test] +fn test_eval_named_builtin_arguments_support_variadic_and_by_ref() { + let out = compile_and_run( + r#" Date: Sun, 28 Jun 2026 17:07:08 +0200 Subject: [PATCH 0907/1208] fix(eval): document supported builtin signature extensions --- .../interpreter/builtins/registry/binding.rs | 6 ++-- .../builtins/registry/signature.rs | 6 +++- .../tests/builtins_filesystem_ops.rs | 4 +-- tests/builtin_parity_tests.rs | 36 +++++++++++++++++++ 4 files changed, 46 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 9a26cd58ee..fe4ac8159c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -144,10 +144,10 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "array_push" | "array_unshift" => Some(&["array", "values"]), "array_key_exists" => Some(&["key", "array"]), "array_pad" => Some(&["array", "length", "value"]), - "array_reverse" => Some(&["array"]), + "array_reverse" => Some(&["array", "preserve_keys"]), "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), "array_slice" => Some(&["array", "offset", "length"]), - "array_splice" => Some(&["array", "offset", "length"]), + "array_splice" => Some(&["array", "offset", "length", "replacement"]), "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), "atan2" => Some(&["y", "x"]), @@ -279,7 +279,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "md5" | "sha1" => Some(&["string", "binary"]), "microtime" => Some(&["as_float"]), "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), - "nl2br" => Some(&["string"]), + "nl2br" => Some(&["string", "use_xhtml"]), "number_format" => Some(&[ "num", "decimals", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index f79805e64c..5d17c2c297 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -89,6 +89,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "number_format" => optional(params, 1), "array_pop" | "array_shift" => fixed_by_ref(params, &["array"]), + "array_reverse" => optional(params, 1), "sort" | "rsort" | "shuffle" | "natsort" | "natcasesort" | "asort" | "arsort" | "ksort" | "krsort" => fixed_by_ref(params, &["array"]), "in_array" | "array_search" => optional(params, 2), @@ -105,7 +106,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "call_user_func" => variadic(params, &[]), - "log" | "round" | "date" => optional(params, 1), + "log" | "round" | "date" | "nl2br" => optional(params, 1), "min" | "max" => variadic(params, &[]), "json_encode" | "json_decode" | "json_validate" => optional(params, 1), @@ -192,8 +193,10 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("number_format", 2) => String("."), ("number_format", 3) => String(","), + ("array_reverse", 1) => Bool(false), ("in_array" | "array_search", 2) => Bool(false), ("array_slice" | "array_splice", 2) => Null, + ("array_splice", 3) => EmptyArray, ("array_filter", 1) => Null, ("array_filter", 2) => Int(0), ("array_reduce", 2) => Null, @@ -201,6 +204,7 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("log", 1) => Float(std::f64::consts::E), ("round", 1) => Int(0), ("date", 1) => Null, + ("nl2br", 1) => Bool(true), ("json_encode", 1) => Int(0), ("json_encode", 2) => Int(512), ("json_decode", 1) => Null, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs index 1774ddc2d2..fc25eb689d 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_filesystem_ops.rs @@ -78,11 +78,11 @@ fn execute_program_dispatches_stream_resolve_include_path_builtin() { $resolved = stream_resolve_include_path("{file}"); echo is_string($resolved) && basename($resolved) === "{file}" && file_get_contents($resolved) === "payload" ? "resolved" : "bad"; echo ":"; echo stream_resolve_include_path("{missing}") === false ? "missing" : "bad"; echo ":"; -$named = stream_resolve_include_path(path: "{file}"); +$named = stream_resolve_include_path(filename: "{file}"); echo is_string($named) && basename($named) === "{file}" ? "named" : "bad"; echo ":"; $call = call_user_func("stream_resolve_include_path", "{file}"); echo is_string($call) && basename($call) === "{file}" ? "call" : "bad"; echo ":"; -$spread = call_user_func_array("stream_resolve_include_path", ["path" => "{file}"]); +$spread = call_user_func_array("stream_resolve_include_path", ["filename" => "{file}"]); echo is_string($spread) && basename($spread) === "{file}" ? "spread" : "bad"; echo ":"; echo unlink("{file}") ? "cleanup" : "bad"; echo ":"; return function_exists("stream_resolve_include_path");"#, diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index c81e14b7e9..27b642f6ed 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -41,6 +41,9 @@ const EVAL_ONLY_REFLECTION_BUILTINS: &[&str] = &[ "get_object_vars", ]; +/// Eval supports these PHP optional parameters before the static backend does. +const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["array_reverse", "array_splice", "nl2br"]; + /// Verifies every non-raw-memory static builtin is visible through eval's function lookup. #[test] fn static_php_visible_builtins_are_visible_to_eval() { @@ -84,6 +87,11 @@ fn shared_builtin_signature_shape_matches_static_signatures() { missing_eval_signature.push(*name); continue; }; + if EVAL_SIGNATURE_EXTENSION_BUILTINS.contains(name) { + assert_eval_signature_extends_static_signature(name, &static_meta, &eval_meta); + continue; + } + if static_meta.params != eval_meta.params || static_meta.required_param_count != eval_meta.required_param_count || static_meta.default_param_count != eval_meta.default_param_count @@ -108,6 +116,34 @@ fn shared_builtin_signature_shape_matches_static_signatures() { ); } +/// Verifies a documented eval signature extension keeps the static prefix contract. +fn assert_eval_signature_extends_static_signature( + name: &str, + static_meta: &elephc::builtin_metadata::BuiltinSignatureMetadata, + eval_meta: &elephc_magician::builtin_metadata::BuiltinSignatureMetadata, +) { + assert!( + eval_meta.params.starts_with(&static_meta.params), + "{name} eval extension must preserve static parameter prefix: static={static_meta:#?} eval={eval_meta:#?}" + ); + assert_eq!( + static_meta.required_param_count, eval_meta.required_param_count, + "{name} eval extension must preserve required parameter count" + ); + assert_eq!( + static_meta.variadic, eval_meta.variadic, + "{name} eval extension must not change variadic behavior" + ); + assert_eq!( + static_meta.by_ref_params, eval_meta.by_ref_params, + "{name} eval extension must preserve by-reference parameters" + ); + assert!( + eval_meta.default_param_count >= static_meta.default_param_count, + "{name} eval extension must not remove defaults" + ); +} + /// Documents the current eval-only reflection builtins so the drift is explicit. #[test] fn eval_only_reflection_builtins_remain_visible_until_static_catalog_catches_up() { From e3ba3ac7ca59e915ee3bfb637ffbe4eb3a46b9ea Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 20:02:24 +0200 Subject: [PATCH 0908/1208] Extend magician stream wrapper support --- Cargo.lock | 1 + crates/elephc-magician/Cargo.toml | 1 + .../builtins/filesystem/file_io.rs | 81 ++++++- .../builtins/filesystem/ops/links.rs | 13 ++ .../builtins/filesystem/ops/path_bool.rs | 16 ++ .../builtins/filesystem/stream_extensions.rs | 38 +++- .../builtins/registry/dispatch/filesystem.rs | 2 +- .../builtins/registry/dispatch/network_env.rs | 4 +- .../builtins/strings/introspection.rs | 29 ++- .../src/interpreter/expressions.rs | 2 +- .../tests/builtins_file_streams.rs | 34 +++ .../tests/builtins_stream_wrappers.rs | 101 +++++++++ .../src/interpreter/tests/mod.rs | 1 + crates/elephc-magician/src/lib.rs | 1 + .../elephc-magician/src/stream_resources.rs | 206 +++++++++++++++++- crates/elephc-magician/src/stream_wrappers.rs | 141 ++++++++++++ 16 files changed, 637 insertions(+), 34 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs create mode 100644 crates/elephc-magician/src/stream_wrappers.rs diff --git a/Cargo.lock b/Cargo.lock index ba0012321f..848f81d690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -621,6 +621,7 @@ name = "elephc-magician" version = "0.1.0" dependencies = [ "elephc-crypto", + "elephc-phar", "flate2", "libc", "unicode-segmentation", diff --git a/crates/elephc-magician/Cargo.toml b/crates/elephc-magician/Cargo.toml index 810213caa6..e93bcae2ab 100644 --- a/crates/elephc-magician/Cargo.toml +++ b/crates/elephc-magician/Cargo.toml @@ -11,6 +11,7 @@ crate-type = ["staticlib", "rlib"] [dependencies] elephc-crypto = { path = "../elephc-crypto" } +elephc-phar = { path = "../elephc-phar" } flate2 = "1" libc = "0.2" unicode-segmentation = "1" diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs index 07805bc15c..872f436cfe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs @@ -9,6 +9,7 @@ use super::super::super::*; use super::*; +use crate::stream_wrappers; /// Evaluates PHP `getcwd()` with no arguments. pub(in crate::interpreter) fn eval_builtin_getcwd( @@ -51,6 +52,14 @@ pub(in crate::interpreter) fn eval_file_probe_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if stream_wrappers::is_phar_stream(&path) { + let exists = elephc_phar::extract_url_bytes(path.as_bytes()).is_some(); + let supported = matches!(name, "file_exists" | "is_file" | "is_readable"); + return values.bool_value(supported && exists); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; let path = std::path::Path::new(&path); let result = match name { "file_exists" => path.exists(), @@ -89,6 +98,12 @@ pub(in crate::interpreter) fn eval_file_stat_scalar_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return match name { + "filemtime" => values.int(0), + _ => values.bool_value(false), + }; + }; let metadata = match std::fs::metadata(path) { Ok(metadata) => metadata, Err(_) if name == "filemtime" => return values.int(0), @@ -122,13 +137,13 @@ pub(in crate::interpreter) fn eval_builtin_file_get_contents( eval_file_get_contents_result(filename, values) } -/// Reads a local file into a PHP string, or returns false when it cannot be opened. +/// Reads a local file or supported wrapper into a PHP string, or returns false on failure. pub(in crate::interpreter) fn eval_file_get_contents_result( filename: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; - match std::fs::read(path) { + match eval_read_path_or_wrapper_bytes(&path) { Ok(bytes) => values.string_bytes_value(&bytes), Err(_) => { values.warning("Warning: file_get_contents(): Failed to open stream\n")?; @@ -151,13 +166,13 @@ pub(in crate::interpreter) fn eval_builtin_file( eval_file_result(filename, values) } -/// Reads one local file and returns an indexed array of line byte strings. +/// Reads one local file or supported wrapper and returns indexed line byte strings. pub(in crate::interpreter) fn eval_file_result( filename: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; - let bytes = match std::fs::read(path) { + let bytes = match eval_read_path_or_wrapper_bytes(&path) { Ok(bytes) => bytes, Err(_) => { values.warning("Warning: file_get_contents(): Failed to open stream\n")?; @@ -204,17 +219,19 @@ pub(in crate::interpreter) fn eval_builtin_readfile( eval_readfile_result(filename, values) } -/// Streams one local file to eval output and returns a byte count, false, or -1. +/// Streams one local file or supported wrapper to eval output. pub(in crate::interpreter) fn eval_readfile_result( filename: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; - let path = std::path::Path::new(&path); - if path.is_dir() { - return values.int(-1); + if let Some(local_path) = stream_wrappers::local_filesystem_path(&path) { + let path = std::path::Path::new(&local_path); + if path.is_dir() { + return values.int(-1); + } } - let bytes = match std::fs::read(path) { + let bytes = match eval_read_path_or_wrapper_bytes(&path) { Ok(bytes) => bytes, Err(_) => return values.bool_value(false), }; @@ -238,7 +255,7 @@ pub(in crate::interpreter) fn eval_builtin_file_put_contents( eval_file_put_contents_result(filename, data, values) } -/// Writes a PHP string to a local file and returns the written byte count or false. +/// Writes a PHP string to a local file or supported wrapper and returns a byte count. pub(in crate::interpreter) fn eval_file_put_contents_result( filename: RuntimeCellHandle, data: RuntimeCellHandle, @@ -246,6 +263,15 @@ pub(in crate::interpreter) fn eval_file_put_contents_result( ) -> Result { let path = eval_path_string(filename, values)?; let data = values.string_bytes(data)?; + if stream_wrappers::is_phar_stream(&path) { + return match elephc_phar::put_url_bytes(path.as_bytes(), &data) { + Some(len) => values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + }; + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; match std::fs::write(path, &data) { Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), Err(_) => values.bool_value(false), @@ -266,12 +292,18 @@ pub(in crate::interpreter) fn eval_builtin_filesize( eval_filesize_result(filename, values) } -/// Returns one local file size in bytes, or zero when stat fails. +/// Returns one local file or supported wrapper size in bytes, or zero on failure. pub(in crate::interpreter) fn eval_filesize_result( filename: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if let Ok(bytes) = eval_read_path_or_wrapper_bytes(&path) { + return values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.int(0); + }; let len = std::fs::metadata(path) .map(|metadata| metadata.len()) .unwrap_or(0); @@ -298,6 +330,16 @@ pub(in crate::interpreter) fn eval_filetype_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if stream_wrappers::is_phar_stream(&path) { + return if elephc_phar::extract_url_bytes(path.as_bytes()).is_some() { + values.string("file") + } else { + values.bool_value(false) + }; + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; let file_type = match std::fs::symlink_metadata(path) { Ok(metadata) => metadata.file_type(), Err(_) => return values.bool_value(false), @@ -344,6 +386,9 @@ pub(in crate::interpreter) fn eval_stat_array_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; let metadata = match name { "stat" => std::fs::metadata(path), "lstat" => std::fs::symlink_metadata(path), @@ -412,3 +457,17 @@ pub(in crate::interpreter) fn eval_stat_array_set_string_key( pub(in crate::interpreter) fn eval_u64_to_i64(value: u64) -> Result { i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) } + +/// Reads bytes from supported direct path or stream-wrapper URLs. +fn eval_read_path_or_wrapper_bytes(path: &str) -> Result, ()> { + if stream_wrappers::is_data_stream(path) { + return stream_wrappers::decode_data_uri(path).ok_or(()); + } + if stream_wrappers::is_phar_stream(path) { + return elephc_phar::extract_url_bytes(path.as_bytes()).ok_or(()); + } + let Some(path) = stream_wrappers::local_filesystem_path(path) else { + return Err(()); + }; + std::fs::read(path).map_err(|_| ()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs index 42925a2f9c..cd1e9d0ffb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs @@ -10,6 +10,7 @@ use super::super::super::super::*; use super::super::*; +use crate::stream_wrappers; /// Evaluates PHP `readlink($path)` over one eval expression. pub(in crate::interpreter) fn eval_builtin_readlink( @@ -31,6 +32,9 @@ pub(in crate::interpreter) fn eval_readlink_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(path, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; match std::fs::read_link(path) { Ok(target) => values.string(target.to_string_lossy().as_ref()), Err(_) => values.bool_value(false), @@ -57,6 +61,9 @@ pub(in crate::interpreter) fn eval_linkinfo_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(path, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.int(-1); + }; let dev = match std::fs::symlink_metadata(path) { Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, Err(_) => -1, @@ -100,5 +107,11 @@ pub(in crate::interpreter) fn eval_unlink_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if stream_wrappers::is_phar_stream(&path) { + return values.bool_value(elephc_phar::delete_url_bytes(path.as_bytes()).is_some()); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; values.bool_value(std::fs::remove_file(path).is_ok()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs index de95f3b4c9..d8b3abc68e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs @@ -13,6 +13,7 @@ use std::ffi::CString; use super::super::super::super::*; use super::super::super::*; use super::super::*; +use crate::stream_wrappers; /// Evaluates a one-path filesystem operation that returns a PHP boolean. pub(in crate::interpreter) fn eval_builtin_unary_path_bool( @@ -36,6 +37,9 @@ pub(in crate::interpreter) fn eval_unary_path_bool_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(path, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; let ok = match name { "chdir" => std::env::set_current_dir(path).is_ok(), "mkdir" => std::fs::create_dir(path).is_ok(), @@ -70,6 +74,12 @@ pub(in crate::interpreter) fn eval_binary_path_bool_result( ) -> Result { let from = eval_path_string(from, values)?; let to = eval_path_string(to, values)?; + let Some(from) = stream_wrappers::local_filesystem_path(&from) else { + return values.bool_value(false); + }; + let Some(to) = stream_wrappers::local_filesystem_path(&to) else { + return values.bool_value(false); + }; let ok = match name { "copy" => std::fs::copy(from, to).is_ok(), "link" => std::fs::hard_link(from, to).is_ok(), @@ -102,6 +112,9 @@ pub(in crate::interpreter) fn eval_chmod_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; let mode = eval_int_value(permissions, values)? as u32; let permissions = std::fs::Permissions::from_mode(mode); values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) @@ -131,6 +144,9 @@ pub(in crate::interpreter) fn eval_chown_like_result( values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; let Some(path) = eval_c_string(&path) else { return values.bool_value(false); }; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs index cabc2a2936..ec0e308cc0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs @@ -26,19 +26,19 @@ pub(in crate::interpreter) fn eval_builtin_stream_wrapper_registry( "stream_wrapper_unregister" | "stream_wrapper_restore" if args.len() == 1 => {} _ => return Err(EvalStatus::RuntimeFatal), } + let mut evaluated_args = Vec::with_capacity(args.len()); for arg in args { let value = eval_expr(arg, context, scope, values)?; - if matches!(name, "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore") { - let _ = values.string_bytes(value).ok(); - } + evaluated_args.push(value); } - values.bool_value(true) + eval_stream_wrapper_registry_result(name, &evaluated_args, context, values) } /// Evaluates stream wrapper registration builtins from materialized arguments. pub(in crate::interpreter) fn eval_stream_wrapper_registry_result( name: &str, evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match name { @@ -46,10 +46,32 @@ pub(in crate::interpreter) fn eval_stream_wrapper_registry_result( "stream_wrapper_unregister" | "stream_wrapper_restore" if evaluated_args.len() == 1 => {} _ => return Err(EvalStatus::RuntimeFatal), } - for value in evaluated_args { - let _ = values.string_bytes(*value).ok(); - } - values.bool_value(true) + let protocol = eval_stream_wrapper_protocol(evaluated_args[0], values)?; + let ok = match name { + "stream_wrapper_register" => { + let _ = values.string_bytes(evaluated_args[1])?; + context + .stream_resources_mut() + .register_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS) + } + "stream_wrapper_unregister" => context + .stream_resources_mut() + .unregister_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS), + "stream_wrapper_restore" => context + .stream_resources_mut() + .restore_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) +} + +/// Coerces one stream wrapper protocol argument into an owned string. +fn eval_stream_wrapper_protocol( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(protocol)?; + Ok(String::from_utf8_lossy(&bytes).into_owned()) } /// Evaluates `stream_filter_register(filter_name, class)`. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index bf59e697c5..cf84eabc97 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -382,7 +382,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( values.bool_value(true)? } "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { - eval_stream_wrapper_registry_result(name, evaluated_args, values)? + eval_stream_wrapper_registry_result(name, evaluated_args, context, values)? } "stream_filter_register" => { let [filter_name, class] = evaluated_args else { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs index fd9194b929..029f0bca0e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs @@ -15,7 +15,7 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_network_env_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { @@ -112,7 +112,7 @@ pub(in crate::interpreter) fn eval_network_env_builtin_with_values( if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - eval_stream_introspection_result(name, values)? + eval_stream_introspection_result(name, context, values)? } "long2ip" => { let [value] = evaluated_args else { diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs b/crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs index c093683ce4..3a6cffadc8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs @@ -64,26 +64,45 @@ pub(in crate::interpreter) fn eval_spl_classes_result( pub(in crate::interpreter) fn eval_builtin_stream_introspection( name: &str, args: &[EvalExpr], + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if !args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - eval_stream_introspection_result(name, values) + eval_stream_introspection_result(name, context, values) } /// Builds the static list returned by one eval stream introspection builtin. pub(in crate::interpreter) fn eval_stream_introspection_result( name: &str, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let items = match name { - "stream_get_filters" => EVAL_STREAM_FILTERS, - "stream_get_transports" => EVAL_STREAM_TRANSPORTS, - "stream_get_wrappers" => EVAL_STREAM_WRAPPERS, + "stream_get_filters" => return eval_static_string_array_result(EVAL_STREAM_FILTERS, values), + "stream_get_transports" => { + return eval_static_string_array_result(EVAL_STREAM_TRANSPORTS, values); + } + "stream_get_wrappers" => context.stream_resources().stream_wrappers(EVAL_STREAM_WRAPPERS), _ => return Err(EvalStatus::RuntimeFatal), }; - eval_static_string_array_result(items, values) + eval_owned_string_array_result(&items, values) +} + +/// Builds one indexed PHP array from an owned string slice. +fn eval_owned_string_array_result( + items: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) } /// Evaluates PHP stream boolean-introspection builtins over one eval expression. diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 1f41f7c846..cd707b78b4 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1239,7 +1239,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_stream_bool_predicate(name, args, context, scope, values) } "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { - eval_builtin_stream_introspection(name, args, values) + eval_builtin_stream_introspection(name, args, context, values) } "stream_resolve_include_path" => { eval_builtin_stream_resolve_include_path(args, context, scope, values) diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs index 3b35caa727..398036641f 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs @@ -87,6 +87,40 @@ return true;"# assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval `fstat()` returns PHP's complete numeric and string-keyed array. +#[test] +fn execute_program_dispatches_complete_fstat_array() { + let pid = std::process::id(); + let file = format!("elephc_magician_complete_fstat_{pid}.txt"); + let source = format!( + r#"file_put_contents("{file}", "abcdef"); +$h = fopen("{file}", "r"); +$stat = fstat($h); +echo count($stat) === 26 ? "count" : "bad"; echo ":"; +echo $stat[0] === $stat["dev"] && $stat[1] === $stat["ino"] && $stat[2] === $stat["mode"] ? "head" : "bad"; echo ":"; +echo $stat[3] === $stat["nlink"] && $stat[4] === $stat["uid"] && $stat[5] === $stat["gid"] ? "owner" : "bad"; echo ":"; +echo $stat[6] === $stat["rdev"] && $stat[7] === $stat["size"] && $stat["size"] === 6 ? "size" : "bad"; echo ":"; +echo $stat[8] === $stat["atime"] && $stat[9] === $stat["mtime"] && $stat[10] === $stat["ctime"] ? "time" : "bad"; echo ":"; +echo $stat[11] === $stat["blksize"] && $stat[12] === $stat["blocks"] ? "blocks" : "bad"; echo ":"; +fclose($h); +echo unlink("{file}") ? "cleanup" : "bad"; +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let _ = std::fs::remove_file(&file); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&file); + assert_eq!( + values.output, + "count:head:owner:size:time:blocks:cleanup" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval `flock()` applies advisory locks to local file stream resources. #[test] fn execute_program_dispatches_file_stream_flock_builtin() { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs new file mode 100644 index 0000000000..8d502c5beb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -0,0 +1,101 @@ +//! Purpose: +//! Interpreter tests for eval-supported stream wrapper URL handling. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - PHAR fixtures are written through `elephc-phar` so tests exercise the same +//! archive bridge used by generated-runtime paths. + +use super::super::*; +use super::support::*; + +/// Verifies eval `fopen()` and one-shot file builtins handle supported wrappers. +#[test] +fn execute_program_dispatches_supported_stream_wrapper_urls() { + let pid = std::process::id(); + let local = format!("elephc_magician_wrapper_local_{pid}.txt"); + let archive = format!("elephc_magician_wrapper_{pid}.phar"); + let read_url = format!("phar://{archive}/dir/read.txt"); + let put_url = format!("phar://{archive}/dir/put.txt"); + let stream_url = format!("phar://{archive}/dir/stream.txt"); + let _ = std::fs::remove_file(&local); + let _ = std::fs::remove_file(&archive); + std::fs::write(&local, b"local").expect("write local wrapper fixture"); + elephc_phar::put_url_bytes(read_url.as_bytes(), b"from-phar") + .expect("write phar wrapper fixture"); + let source = format!( + r#"echo file_get_contents("file://{local}") === "local" ? "fileurl" : "bad"; echo ":"; +$memory = fopen("php://memory", "w+"); +fwrite($memory, "mem"); +rewind($memory); +echo fread($memory, 3) === "mem" ? "memory" : "bad"; echo ":"; +fclose($memory); +$data = fopen("data://text/plain;base64,SGVsbG8=", "r"); +echo fread($data, 5) === "Hello" ? "data" : "bad"; echo ":"; +fclose($data); +$phar = fopen("{read_url}", "r"); +echo fread($phar, 32) === "from-phar" ? "pharopen" : "bad"; echo ":"; +fclose($phar); +echo file_get_contents("{read_url}") === "from-phar" ? "pharget" : "bad"; echo ":"; +echo file_exists("{read_url}") && is_file("{read_url}") && is_readable("{read_url}") ? "pharprobe" : "bad"; echo ":"; +echo filetype("{read_url}") === "file" ? "phartype" : "bad"; echo ":"; +echo filesize("{read_url}") === 9 ? "pharsize" : "bad"; echo ":"; +echo file_put_contents("{put_url}", "put") === 3 ? "pharput" : "bad"; echo ":"; +echo file_get_contents("{put_url}") === "put" ? "putread" : "bad"; echo ":"; +$out = fopen("{stream_url}", "w"); +fwrite($out, "stream"); +echo fclose($out) ? "streamclose" : "bad"; echo ":"; +echo file_get_contents("{stream_url}") === "stream" ? "streamread" : "bad"; echo ":"; +echo unlink("{stream_url}") ? "unlink" : "bad"; echo ":"; +echo file_get_contents("{stream_url}") === false ? "deleted" : "bad"; +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let _ = std::fs::remove_file(&local); + let _ = std::fs::remove_file(&archive); + assert_eq!( + values.output, + "fileurl:memory:data:pharopen:pharget:pharprobe:phartype:pharsize:pharput:putread:streamclose:streamread:unlink:deleted" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval stream wrapper registration changes the visible wrapper list. +#[test] +fn execute_program_tracks_stream_wrapper_registry_state() { + let program = parse_fragment( + br#"$before = stream_get_wrappers(); +echo in_array("evaltest", $before) ? "bad" : "missing"; echo ":"; +echo stream_wrapper_register("evaltest", "stdClass") ? "reg" : "bad"; echo ":"; +$after = stream_get_wrappers(); +echo in_array("evaltest", $after) ? "listed" : "bad"; echo ":"; +echo stream_wrapper_unregister("evaltest") ? "unreg" : "bad"; echo ":"; +$removed = call_user_func("stream_get_wrappers"); +echo in_array("evaltest", $removed) ? "bad" : "removed"; echo ":"; +echo stream_wrapper_unregister("file") ? "unfile" : "bad"; echo ":"; +$without_file = stream_get_wrappers(); +echo in_array("file", $without_file) ? "bad" : "nofile"; echo ":"; +echo stream_wrapper_restore("file") ? "restore" : "bad"; echo ":"; +$restored = call_user_func_array("stream_get_wrappers", []); +echo in_array("file", $restored) ? "fileback" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "missing:reg:listed:unreg:removed:unfile:nofile:restore:fileback" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index 9b86ee2874..fa0b88e06f 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -31,6 +31,7 @@ mod builtins_stream_contexts; mod builtins_stream_extensions; mod builtins_stream_settings; mod builtins_stream_sockets; +mod builtins_stream_wrappers; mod builtins_strings_binary; mod builtins_strings_encoding; mod builtins_strings_text; diff --git a/crates/elephc-magician/src/lib.rs b/crates/elephc-magician/src/lib.rs index 11c9306369..a09d04e525 100644 --- a/crates/elephc-magician/src/lib.rs +++ b/crates/elephc-magician/src/lib.rs @@ -24,6 +24,7 @@ pub mod parser; pub mod runtime_hooks; pub mod scope; mod stream_resources; +mod stream_wrappers; pub mod value; pub use interpreter::builtin_metadata; diff --git a/crates/elephc-magician/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs index 0181766897..ff3710307e 100644 --- a/crates/elephc-magician/src/stream_resources.rs +++ b/crates/elephc-magician/src/stream_resources.rs @@ -22,6 +22,7 @@ use std::os::unix::net::UnixStream; use std::path::PathBuf; use std::process::{Child, Command, Stdio}; +use crate::stream_wrappers; use crate::value::RuntimeCellHandle; /// Eval-owned table of local file streams keyed by runtime resource payload. @@ -29,6 +30,7 @@ use crate::value::RuntimeCellHandle; pub(crate) struct EvalStreamResources { chunk_sizes: HashMap, default_stream_context: Option, + disabled_builtin_stream_wrappers: HashSet, next_id: i64, directories: HashMap, filter_resources: HashSet, @@ -38,14 +40,26 @@ pub(crate) struct EvalStreamResources { socket_names: HashMap, stream_contexts: HashMap, streams: HashMap, + user_stream_wrappers: Vec, } impl EvalStreamResources { /// Opens a local path using PHP's common `fopen()` mode strings. pub(crate) fn open_path(&mut self, path: &str, mode: &str) -> Option { let mode = EvalOpenMode::parse(mode)?; - let file = mode.open(path).ok()?; - Some(self.insert(EvalFileStream::new(file, path.to_string(), mode.label))) + if stream_wrappers::is_php_memory_stream(path) { + return self.open_ephemeral_stream(path, &mode, &[], None, false); + } + if stream_wrappers::is_data_stream(path) { + let bytes = stream_wrappers::decode_data_uri(path)?; + return self.open_ephemeral_stream(path, &mode, &bytes, None, false); + } + if stream_wrappers::is_phar_stream(path) { + return self.open_phar_stream(path, &mode); + } + let path = stream_wrappers::local_filesystem_path(path)?; + let file = mode.open(&path).ok()?; + Some(self.insert(EvalFileStream::new(file, path, mode.label))) } /// Opens an anonymous temporary file and returns its resource id. @@ -220,6 +234,69 @@ impl EvalStreamResources { self.insert_stream_context(EvalStreamContext { options }) } + /// Registers a user stream wrapper protocol in eval-local state. + pub(crate) fn register_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if self + .user_stream_wrappers + .iter() + .any(|current| current.eq_ignore_ascii_case(&protocol)) + { + return false; + } + if eval_builtin_stream_wrapper_exists(builtins, &protocol) + && !self.disabled_builtin_stream_wrappers.contains(&protocol) + { + return false; + } + self.user_stream_wrappers.push(protocol); + true + } + + /// Unregisters a user or built-in stream wrapper protocol. + pub(crate) fn unregister_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if let Some(index) = self + .user_stream_wrappers + .iter() + .position(|current| current.eq_ignore_ascii_case(&protocol)) + { + self.user_stream_wrappers.remove(index); + return true; + } + if eval_builtin_stream_wrapper_exists(builtins, &protocol) { + return self.disabled_builtin_stream_wrappers.insert(protocol); + } + false + } + + /// Restores a built-in stream wrapper protocol or accepts no-op user restores. + pub(crate) fn restore_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if eval_builtin_stream_wrapper_exists(builtins, &protocol) { + self.disabled_builtin_stream_wrappers.remove(&protocol); + } + true + } + + /// Returns the currently visible stream wrapper protocol list. + pub(crate) fn stream_wrappers(&self, builtins: &[&str]) -> Vec { + let mut wrappers = Vec::with_capacity(builtins.len() + self.user_stream_wrappers.len()); + for builtin in builtins { + if !self.disabled_builtin_stream_wrappers.contains(*builtin) { + wrappers.push((*builtin).to_string()); + } + } + wrappers.extend(self.user_stream_wrappers.iter().cloned()); + wrappers + } + /// Returns the default stream context resource id, creating it if needed. pub(crate) fn default_stream_context(&mut self) -> i64 { if let Some(id) = self.default_stream_context { @@ -232,14 +309,18 @@ impl EvalStreamResources { /// Removes a stream resource from the table, closing its file handle. pub(crate) fn close(&mut self, id: i64) -> bool { - let closed = self.streams.remove(&id).is_some() - || self.filter_resources.remove(&id) - || self.socket_listeners.remove(&id).is_some(); + let mut closed = false; + let mut ok = true; + if let Some(stream) = self.streams.remove(&id) { + closed = true; + ok = stream.finalize_on_close(); + } + closed = closed || self.filter_resources.remove(&id) || self.socket_listeners.remove(&id).is_some(); self.socket_names.remove(&id); if let Some(mut child) = self.process_children.remove(&id) { let _ = child.wait(); } - closed + closed && ok } /// Returns whether a file-like stream resource exists. @@ -602,6 +683,62 @@ impl EvalStreamResources { id } + /// Opens one unlinked temporary file as the backing storage for wrapper streams. + fn open_ephemeral_stream( + &mut self, + uri: &str, + mode: &EvalOpenMode, + initial: &[u8], + flush_target: Option, + append: bool, + ) -> Option { + let path = eval_tmpfile_path(); + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + .ok()?; + let _ = std::fs::remove_file(&path); + file.write_all(initial).ok()?; + if append { + file.seek(SeekFrom::End(0)).ok()?; + } else { + file.seek(SeekFrom::Start(0)).ok()?; + } + Some(self.insert(EvalFileStream::new_with_flush_target( + file, + uri.to_string(), + mode.label.clone(), + flush_target, + ))) + } + + /// Opens a `phar://` entry for reading or buffered write-back on close. + fn open_phar_stream(&mut self, path: &str, mode: &EvalOpenMode) -> Option { + let url = path.as_bytes(); + if mode.write { + let initial = if mode.truncate { + Vec::new() + } else { + match elephc_phar::extract_url_bytes(url) { + Some(bytes) => bytes, + None if mode.create => Vec::new(), + None => return None, + } + }; + return self.open_ephemeral_stream( + path, + mode, + &initial, + Some(EvalStreamFlushTarget::PharUrl(url.to_vec())), + mode.append, + ); + } + let bytes = elephc_phar::extract_url_bytes(url)?; + self.open_ephemeral_stream(path, mode, &bytes, None, false) + } + /// Inserts a TCP stream as a File-backed eval stream and records endpoint names. fn insert_tcp_stream(&mut self, stream: TcpStream) -> Option { let local = stream.local_addr().ok()?.to_string(); @@ -712,22 +849,79 @@ fn eval_hash_digest_bytes(len: isize, output: &[u8; 64]) -> Option> { Some(output[..len].to_vec()) } +/// Normalizes a PHP stream wrapper protocol name for eval registry storage. +fn eval_normalize_stream_wrapper_protocol(protocol: &str) -> Option { + let protocol = protocol.trim().trim_end_matches("://"); + if protocol.is_empty() { + return None; + } + Some(protocol.to_ascii_lowercase()) +} + +/// Returns whether the protocol is one of elephc's built-in stream wrappers. +fn eval_builtin_stream_wrapper_exists(builtins: &[&str], protocol: &str) -> bool { + builtins + .iter() + .any(|builtin| builtin.eq_ignore_ascii_case(protocol)) +} + /// File stream stored behind one eval resource id. struct EvalFileStream { file: File, uri: String, mode: String, eof: bool, + flush_target: Option, } impl EvalFileStream { /// Creates a tracked stream around a host file handle. fn new(file: File, uri: String, mode: String) -> Self { + Self::new_with_flush_target(file, uri, mode, None) + } + + /// Creates a tracked stream that may write back to a wrapper target on close. + fn new_with_flush_target( + file: File, + uri: String, + mode: String, + flush_target: Option, + ) -> Self { Self { file, uri, mode, eof: false, + flush_target, + } + } + + /// Flushes any buffered wrapper target before the stream resource disappears. + fn finalize_on_close(mut self) -> bool { + let Some(flush_target) = self.flush_target.take() else { + return true; + }; + let mut bytes = Vec::new(); + if self.file.flush().is_err() || self.file.seek(SeekFrom::Start(0)).is_err() { + return false; + } + if self.file.read_to_end(&mut bytes).is_err() { + return false; + } + flush_target.write_back(&bytes) + } +} + +/// Wrapper targets that need a write-back step when their stream closes. +enum EvalStreamFlushTarget { + PharUrl(Vec), +} + +impl EvalStreamFlushTarget { + /// Writes buffered stream bytes back to the target URL. + fn write_back(&self, bytes: &[u8]) -> bool { + match self { + Self::PharUrl(url) => elephc_phar::put_url_bytes(url, bytes).is_some(), } } } diff --git a/crates/elephc-magician/src/stream_wrappers.rs b/crates/elephc-magician/src/stream_wrappers.rs new file mode 100644 index 0000000000..e1698bf00b --- /dev/null +++ b/crates/elephc-magician/src/stream_wrappers.rs @@ -0,0 +1,141 @@ +//! Purpose: +//! Parses eval-supported PHP stream wrapper URLs that can be handled entirely +//! inside the magician process. +//! +//! Called from: +//! - `crate::stream_resources::EvalStreamResources` for `fopen()` wrapper routing. +//! - Filesystem builtins that need direct `file_get_contents()` / `file_put_contents()` handling. +//! +//! Key details: +//! - Network wrappers are deliberately not implemented here; callers receive +//! `None` for unsupported schemes and surface ordinary PHP false paths. + +/// Returns true when the path names a `php://memory` or `php://temp` stream. +pub(crate) fn is_php_memory_stream(path: &str) -> bool { + path == "php://memory" || path == "php://temp" || path.starts_with("php://temp/") +} + +/// Returns true when the path names a `data://` stream. +pub(crate) fn is_data_stream(path: &str) -> bool { + path.starts_with("data://") +} + +/// Returns true when the path names a `phar://` stream. +pub(crate) fn is_phar_stream(path: &str) -> bool { + path.starts_with("phar://") +} + +/// Maps plain local paths and `file://` URLs onto host filesystem paths. +pub(crate) fn local_filesystem_path(path: &str) -> Option { + if let Some(rest) = path.strip_prefix("file://") { + return Some(file_url_local_path(rest)); + } + if path_has_scheme(path) { + None + } else { + Some(path.to_string()) + } +} + +/// Decodes a `data://[][;base64],` URI into raw bytes. +pub(crate) fn decode_data_uri(path: &str) -> Option> { + let rest = path.strip_prefix("data://")?; + let comma = rest.find(',')?; + let meta = &rest[..comma]; + let payload = &rest[comma + 1..]; + if meta.to_ascii_lowercase().ends_with(";base64") { + base64_decode(payload) + } else { + Some(percent_decode(payload)) + } +} + +/// Converts the part after `file://` into a path understood by the host OS. +fn file_url_local_path(rest: &str) -> String { + if let Some(localhost_path) = rest.strip_prefix("localhost/") { + format!("/{localhost_path}") + } else if rest == "localhost" { + "/".to_string() + } else { + rest.to_string() + } +} + +/// Reports whether a string starts with a PHP-style `scheme://` prefix. +fn path_has_scheme(path: &str) -> bool { + let Some(separator) = path.find("://") else { + return false; + }; + separator > 0 + && path[..separator] + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.')) +} + +/// Decodes a base64 payload into raw bytes, rejecting invalid alphabet bytes. +fn base64_decode(input: &str) -> Option> { + /// Converts one base64 byte into its six-bit value. + fn sextet(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(u32::from(byte - b'A')), + b'a'..=b'z' => Some(u32::from(byte - b'a') + 26), + b'0'..=b'9' => Some(u32::from(byte - b'0') + 52), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } + } + + let mut output = Vec::new(); + let mut accumulator = 0_u32; + let mut bits = 0_u32; + for byte in input.bytes() { + if byte == b'=' { + break; + } + if byte.is_ascii_whitespace() { + continue; + } + accumulator = (accumulator << 6) | sextet(byte)?; + bits += 6; + if bits >= 8 { + bits -= 8; + output.push((accumulator >> bits) as u8); + } + } + Some(output) +} + +/// Percent-decodes the non-base64 data URI payload form. +fn percent_decode(input: &str) -> Vec { + let bytes = input.as_bytes(); + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'%' if index + 2 < bytes.len() => { + let high = (bytes[index + 1] as char).to_digit(16); + let low = (bytes[index + 2] as char).to_digit(16); + match (high, low) { + (Some(high), Some(low)) => { + output.push((high * 16 + low) as u8); + index += 3; + } + _ => { + output.push(b'%'); + index += 1; + } + } + } + b'+' => { + output.push(b' '); + index += 1; + } + byte => { + output.push(byte); + index += 1; + } + } + } + output +} From 4cd86da047bf67b60e3eb60b3de813c4eb21820d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 20:09:53 +0200 Subject: [PATCH 0909/1208] Add magician http stream wrapper reads --- .../builtins/filesystem/file_io.rs | 3 + .../tests/builtins_stream_wrappers.rs | 53 +++++++ .../elephc-magician/src/stream_resources.rs | 4 + crates/elephc-magician/src/stream_wrappers.rs | 134 +++++++++++++++++- 4 files changed, 192 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs index 872f436cfe..35baa5a67d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs @@ -466,6 +466,9 @@ fn eval_read_path_or_wrapper_bytes(path: &str) -> Result, ()> { if stream_wrappers::is_phar_stream(path) { return elephc_phar::extract_url_bytes(path.as_bytes()).ok_or(()); } + if stream_wrappers::is_http_stream(path) { + return stream_wrappers::read_http_url(path).ok_or(()); + } let Some(path) = stream_wrappers::local_filesystem_path(path) else { return Err(()); }; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs index 8d502c5beb..bb241ef1f4 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -7,9 +7,13 @@ //! Key details: //! - PHAR fixtures are written through `elephc-phar` so tests exercise the same //! archive bridge used by generated-runtime paths. +//! - HTTP tests use a one-shot localhost server to avoid external network dependencies. use super::super::*; use super::support::*; +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::thread::JoinHandle; /// Verifies eval `fopen()` and one-shot file builtins handle supported wrappers. #[test] @@ -67,6 +71,32 @@ return true;"# assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval `http://` URLs feed `file_get_contents()` and `fopen()`. +#[test] +fn execute_program_dispatches_http_stream_wrapper_urls() { + let (fgc_port, fgc_server) = spawn_http_once("fgc-body"); + let (fopen_port, fopen_server) = spawn_http_once("stream-body"); + let source = format!( + r#"echo file_get_contents("http://127.0.0.1:{fgc_port}/body?x=1") === "fgc-body" ? "fgc" : "bad"; echo ":"; +$h = fopen("http://127.0.0.1:{fopen_port}/stream", "r"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo fread($h, 64) === "stream-body" ? "read" : "bad"; echo ":"; +echo fclose($h) ? "close" : "bad"; echo ":"; +echo file_get_contents("http://") === false ? "invalid" : "bad"; +return true;"# + ); + let program = parse_fragment(source.as_bytes()).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + fgc_server.join().expect("join file_get_contents HTTP fixture"); + fopen_server.join().expect("join fopen HTTP fixture"); + assert_eq!(values.output, "fgc:open:read:close:invalid"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies eval stream wrapper registration changes the visible wrapper list. #[test] fn execute_program_tracks_stream_wrapper_registry_state() { @@ -99,3 +129,26 @@ return true;"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Starts a localhost HTTP server that returns one fixed body and then exits. +fn spawn_http_once(body: &'static str) -> (u16, JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind HTTP wrapper fixture"); + let port = listener + .local_addr() + .expect("read HTTP wrapper fixture address") + .port(); + let handle = std::thread::spawn(move || { + let (mut socket, _) = listener.accept().expect("accept HTTP wrapper request"); + let mut request = [0_u8; 1024]; + let _ = socket.read(&mut request).expect("read HTTP wrapper request"); + let response = format!( + "HTTP/1.0 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + socket + .write_all(response.as_bytes()) + .expect("write HTTP wrapper response"); + }); + (port, handle) +} diff --git a/crates/elephc-magician/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs index ff3710307e..98acdde0eb 100644 --- a/crates/elephc-magician/src/stream_resources.rs +++ b/crates/elephc-magician/src/stream_resources.rs @@ -57,6 +57,10 @@ impl EvalStreamResources { if stream_wrappers::is_phar_stream(path) { return self.open_phar_stream(path, &mode); } + if stream_wrappers::is_http_stream(path) && mode.read && !mode.write { + let bytes = stream_wrappers::read_http_url(path)?; + return self.open_ephemeral_stream(path, &mode, &bytes, None, false); + } let path = stream_wrappers::local_filesystem_path(path)?; let file = mode.open(&path).ok()?; Some(self.insert(EvalFileStream::new(file, path, mode.label))) diff --git a/crates/elephc-magician/src/stream_wrappers.rs b/crates/elephc-magician/src/stream_wrappers.rs index e1698bf00b..ec67392455 100644 --- a/crates/elephc-magician/src/stream_wrappers.rs +++ b/crates/elephc-magician/src/stream_wrappers.rs @@ -7,8 +7,14 @@ //! - Filesystem builtins that need direct `file_get_contents()` / `file_put_contents()` handling. //! //! Key details: -//! - Network wrappers are deliberately not implemented here; callers receive -//! `None` for unsupported schemes and surface ordinary PHP false paths. +//! - Plain `http://` uses a small blocking HTTP/1.0 client. TLS-backed +//! `https://` remains outside magician's implemented wrapper paths for now. + +use std::io::{Read, Write}; +use std::net::TcpStream; +use std::time::Duration; + +const HTTP_TIMEOUT: Duration = Duration::from_secs(5); /// Returns true when the path names a `php://memory` or `php://temp` stream. pub(crate) fn is_php_memory_stream(path: &str) -> bool { @@ -25,6 +31,11 @@ pub(crate) fn is_phar_stream(path: &str) -> bool { path.starts_with("phar://") } +/// Returns true when the path names a plain `http://` stream. +pub(crate) fn is_http_stream(path: &str) -> bool { + path.starts_with("http://") +} + /// Maps plain local paths and `file://` URLs onto host filesystem paths. pub(crate) fn local_filesystem_path(path: &str) -> Option { if let Some(rest) = path.strip_prefix("file://") { @@ -50,6 +61,125 @@ pub(crate) fn decode_data_uri(path: &str) -> Option> { } } +/// Reads a plain HTTP URL into response-body bytes. +pub(crate) fn read_http_url(path: &str) -> Option> { + let request = parse_http_url(path)?; + let mut stream = TcpStream::connect((request.host.as_str(), request.port)).ok()?; + let _ = stream.set_read_timeout(Some(HTTP_TIMEOUT)); + let _ = stream.set_write_timeout(Some(HTTP_TIMEOUT)); + let wire_request = format!( + "GET {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n", + request.path_and_query, + request.host_header() + ); + stream.write_all(wire_request.as_bytes()).ok()?; + let mut response = Vec::new(); + stream.read_to_end(&mut response).ok()?; + parse_http_response_body(&response) +} + +/// Parsed `http://` URL pieces needed for a minimal HTTP request. +struct EvalHttpRequest { + host: String, + port: u16, + path_and_query: String, +} + +impl EvalHttpRequest { + /// Returns the Host header value, preserving non-default ports. + fn host_header(&self) -> String { + if self.port == 80 { + self.host.clone() + } else { + format!("{}:{}", self.host, self.port) + } + } +} + +/// Parses a plain `http://host[:port][/path][?query]` URL. +fn parse_http_url(path: &str) -> Option { + let rest = path.strip_prefix("http://")?; + let (authority, suffix) = match rest.find(['/', '?', '#']) { + Some(index) => (&rest[..index], &rest[index..]), + None => (rest, "/"), + }; + if authority.is_empty() || authority.contains('@') { + return None; + } + let (host, port) = parse_http_authority(authority)?; + let mut path_and_query = match suffix.chars().next() { + Some('/') => suffix.to_string(), + Some('?') => format!("/{suffix}"), + Some('#') | None => "/".to_string(), + _ => return None, + }; + if let Some(fragment) = path_and_query.find('#') { + path_and_query.truncate(fragment); + } + if path_and_query.is_empty() { + path_and_query.push('/'); + } + Some(EvalHttpRequest { + host, + port, + path_and_query, + }) +} + +/// Parses the authority portion of an HTTP URL. +fn parse_http_authority(authority: &str) -> Option<(String, u16)> { + if let Some(rest) = authority.strip_prefix('[') { + let end = rest.find(']')?; + let host = rest[..end].to_string(); + let after = &rest[end + 1..]; + let port = if let Some(port) = after.strip_prefix(':') { + port.parse::().ok()? + } else if after.is_empty() { + 80 + } else { + return None; + }; + return Some((host, port)); + } + let (host, port) = match authority.rsplit_once(':') { + Some((host, port)) if !port.is_empty() && port.bytes().all(|byte| byte.is_ascii_digit()) => { + (host, port.parse::().ok()?) + } + _ => (authority, 80), + }; + if host.is_empty() { + None + } else { + Some((host.to_string(), port)) + } +} + +/// Extracts the body from an HTTP response with a successful status code. +fn parse_http_response_body(response: &[u8]) -> Option> { + let header_end = response + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|index| index + 4) + .or_else(|| { + response + .windows(2) + .position(|window| window == b"\n\n") + .map(|index| index + 2) + })?; + let status_line_end = response + .windows(2) + .position(|window| window == b"\r\n") + .or_else(|| response.iter().position(|byte| *byte == b'\n'))?; + let status_line = std::str::from_utf8(&response[..status_line_end]).ok()?; + let mut pieces = status_line.split_whitespace(); + let _version = pieces.next()?; + let status = pieces.next()?.parse::().ok()?; + if !(200..300).contains(&status) { + return None; + } + Some(response[header_end..].to_vec()) +} + /// Converts the part after `file://` into a path understood by the host OS. fn file_url_local_path(rest: &str) -> String { if let Some(localhost_path) = rest.strip_prefix("localhost/") { From 88c3a9491e342b275c41442dfd5a927540de250a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 20:24:50 +0200 Subject: [PATCH 0910/1208] Add magician userspace stream wrapper dispatch --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/stream_extensions.rs | 17 +- .../builtins/filesystem/streams.rs | 61 ++++- .../filesystem/user_wrapper_streams.rs | 228 ++++++++++++++++++ .../tests/builtins_stream_wrappers.rs | 78 ++++++ .../elephc-magician/src/stream_resources.rs | 112 ++++++++- crates/elephc-magician/src/stream_wrappers.rs | 9 + 7 files changed, 488 insertions(+), 19 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 8eebfbf1bf..fc18c2ca58 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -20,6 +20,7 @@ mod stream_context; mod stream_settings; mod stream_sockets; mod streams; +mod user_wrapper_streams; pub(in crate::interpreter) use directories::*; pub(in crate::interpreter) use file_io::*; @@ -33,3 +34,4 @@ pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; +pub(in crate::interpreter) use user_wrapper_streams::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs index ec0e308cc0..66e9faf4df 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs @@ -49,10 +49,10 @@ pub(in crate::interpreter) fn eval_stream_wrapper_registry_result( let protocol = eval_stream_wrapper_protocol(evaluated_args[0], values)?; let ok = match name { "stream_wrapper_register" => { - let _ = values.string_bytes(evaluated_args[1])?; + let class_name = eval_stream_wrapper_class(evaluated_args[1], context, values)?; context .stream_resources_mut() - .register_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS) + .register_stream_wrapper(&protocol, &class_name, EVAL_STREAM_WRAPPERS) } "stream_wrapper_unregister" => context .stream_resources_mut() @@ -74,6 +74,19 @@ fn eval_stream_wrapper_protocol( Ok(String::from_utf8_lossy(&bytes).into_owned()) } +/// Coerces one stream wrapper class argument into a resolved class-name string. +fn eval_stream_wrapper_class( + class_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(class_name)?; + let class_name = String::from_utf8_lossy(&bytes).into_owned(); + Ok(context + .resolve_class_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())) +} + /// Evaluates `stream_filter_register(filter_name, class)`. pub(in crate::interpreter) fn eval_builtin_stream_filter_register( args: &[EvalExpr], diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index 728a9d10e5..8f380a5be1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -9,7 +9,8 @@ //! //! Key details: //! - Runtime resource payloads are zero-based; `get_resource_id()` exposes payload + 1. -//! - This module supports local file resources, not sockets, pipes, wrappers, or filters. +//! - File-backed streams stay in `EvalStreamResources`; userspace wrapper calls +//! delegate to the focused wrapper-dispatch helper module. use super::super::super::*; use super::*; @@ -29,7 +30,9 @@ pub(in crate::interpreter) fn eval_builtin_fopen( for arg in &args[2..] { eval_expr(arg, context, scope, values)?; } - eval_fopen_result(filename, mode, context, values) + let filename = eval_path_string(filename, values)?; + let mode = eval_stream_string(mode, values)?; + eval_fopen_path_result(&filename, &mode, context, scope, values) } /// Opens a local file stream and returns a resource cell or PHP false. @@ -41,7 +44,24 @@ pub(in crate::interpreter) fn eval_fopen_result( ) -> Result { let filename = eval_path_string(filename, values)?; let mode = eval_stream_string(mode, values)?; - match context.stream_resources_mut().open_path(&filename, &mode) { + let mut scope = ElephcEvalScope::new(); + eval_fopen_path_result(&filename, &mode, context, &mut scope, values) +} + +/// Opens a stream by already-coerced path and mode strings. +fn eval_fopen_path_result( + filename: &str, + mode: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = + eval_user_wrapper_fopen_result(filename, mode, context, scope, values)? + { + return Ok(result); + } + match context.stream_resources_mut().open_path(filename, mode) { Some(id) => values.resource(id), None => { values.warning("Warning: fopen(): Failed to open stream\n")?; @@ -97,12 +117,22 @@ pub(in crate::interpreter) fn eval_unary_stream_result( ) -> Result { let id = eval_stream_resource_id(stream, values)?; match name { - "fclose" => values.bool_value(context.stream_resources_mut().close(id)), - "fgetc" => match context.stream_resources_mut().read(id, 1) { - Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), - Some(_) => values.bool_value(false), - None => values.bool_value(false), - }, + "fclose" => { + if let Some(result) = eval_user_wrapper_fclose_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources_mut().close(id)) + } + "fgetc" => { + if let Some(result) = eval_user_wrapper_fread_result(id, 1, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().read(id, 1) { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } + } "fgets" => match context .stream_resources_mut() .read_line(id, usize::MAX, None, true, true) @@ -111,7 +141,12 @@ pub(in crate::interpreter) fn eval_unary_stream_result( Some(_) => values.bool_value(false), None => values.bool_value(false), }, - "feof" => values.bool_value(context.stream_resources().eof(id).unwrap_or(false)), + "feof" => { + if let Some(result) = eval_user_wrapper_feof_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources().eof(id).unwrap_or(false)) + } "fflush" => values.bool_value(context.stream_resources_mut().flush(id)), "fpassthru" => eval_fpassthru_result(id, context, values), "fsync" => values.bool_value(context.stream_resources_mut().sync_all(id)), @@ -218,6 +253,9 @@ pub(in crate::interpreter) fn eval_fread_result( ) -> Result { let id = eval_stream_resource_id(stream, values)?; let length = eval_nonnegative_usize(length, values)?; + if let Some(result) = eval_user_wrapper_fread_result(id, length, context, values)? { + return Ok(result); + } match context.stream_resources_mut().read(id, length) { Some(bytes) => values.string_bytes_value(&bytes), None => values.bool_value(false), @@ -248,6 +286,9 @@ pub(in crate::interpreter) fn eval_fwrite_result( ) -> Result { let id = eval_stream_resource_id(stream, values)?; let data = values.string_bytes(data)?; + if let Some(result) = eval_user_wrapper_fwrite_result(id, &data, context, values)? { + return Ok(result); + } match context.stream_resources_mut().write(id, &data) { Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), None => values.bool_value(false), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs new file mode 100644 index 0000000000..dab0a5c84c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs @@ -0,0 +1,228 @@ +//! Purpose: +//! Dispatches eval userspace stream wrapper resources into wrapper class methods. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams` for `fopen()`, +//! `fread()`, `fwrite()`, `feof()`, and `fclose()`. +//! +//! Key details: +//! - Registered wrapper resources keep the wrapper object in eval-local resource +//! state; file-backed streams continue to use the normal host-file path. +//! - `stream_open()` receives a synthetic by-ref `opened_path` cell. Magician +//! accepts writes to it, but currently keeps the original URL as the stream URI. + +use super::super::super::*; + +/// Opens a registered eval userspace stream wrapper, or reports no match. +pub(in crate::interpreter) fn eval_user_wrapper_fopen_result( + filename: &str, + mode: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(filename) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, stream_open)) = + eval_user_wrapper_method(class.name(), "stream_open", context) + else { + return values.bool_value(false).map(Some); + }; + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, scope, values)?; + let open_args = eval_user_wrapper_stream_open_args(filename, mode, values)?; + let open_result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &stream_open, + object, + open_args, + context, + values, + )?; + let opened = values.truthy(open_result)?; + values.release(open_result)?; + if !opened { + values.release(object)?; + return values.bool_value(false).map(Some); + } + let id = + context + .stream_resources_mut() + .open_user_wrapper_stream(object, class.name(), filename, mode); + values.resource(id).map(Some) +} + +/// Dispatches `fclose()` to `stream_close()` for a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_fclose_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + if let Some((declaring_class, stream_close)) = + eval_user_wrapper_method(&info.class_name, "stream_close", context) + { + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_close, + info.object, + Vec::new(), + context, + values, + )?; + values.release(result)?; + } + let closed = context.stream_resources_mut().close(id); + values.release(info.object)?; + values.bool_value(closed).map(Some) +} + +/// Dispatches `fread()` or `fgetc()` to a wrapper object's `stream_read()`. +pub(in crate::interpreter) fn eval_user_wrapper_fread_result( + id: i64, + length: usize, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_read)) = + eval_user_wrapper_method(&info.class_name, "stream_read", context) + else { + return values.bool_value(false).map(Some); + }; + let length = values.int(i64::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_read, + info.object, + positional_args(vec![length]), + context, + values, + )?; + let bytes = values.string_bytes(result)?; + values.release(result)?; + context + .stream_resources_mut() + .set_user_wrapper_eof(id, bytes.is_empty()); + values.string_bytes_value(&bytes).map(Some) +} + +/// Dispatches `fwrite()` to a wrapper object's `stream_write()`. +pub(in crate::interpreter) fn eval_user_wrapper_fwrite_result( + id: i64, + data: &[u8], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_write)) = + eval_user_wrapper_method(&info.class_name, "stream_write", context) + else { + return values.bool_value(false).map(Some); + }; + let data = values.string_bytes_value(data)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_write, + info.object, + positional_args(vec![data]), + context, + values, + )?; + let written = eval_int_value(result, values)?; + values.release(result)?; + context.stream_resources_mut().set_user_wrapper_eof(id, false); + values.int(written).map(Some) +} + +/// Dispatches `feof()` to a wrapper object's `stream_eof()`. +pub(in crate::interpreter) fn eval_user_wrapper_feof_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_eof)) = + eval_user_wrapper_method(&info.class_name, "stream_eof", context) + else { + return values.bool_value(info.eof).map(Some); + }; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_eof, + info.object, + Vec::new(), + context, + values, + )?; + let eof = values.truthy(result)?; + values.release(result)?; + context.stream_resources_mut().set_user_wrapper_eof(id, eof); + values.bool_value(eof).map(Some) +} + +/// Builds the four PHP arguments passed to a wrapper `stream_open()` method. +fn eval_user_wrapper_stream_open_args( + filename: &str, + mode: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let path = values.string(filename)?; + let mode = values.string(mode)?; + let options = values.int(0)?; + let opened_path = values.null()?; + Ok(vec![ + EvaluatedCallArg { + name: None, + value: path, + ref_target: None, + }, + EvaluatedCallArg { + name: None, + value: mode, + ref_target: None, + }, + EvaluatedCallArg { + name: None, + value: options, + ref_target: None, + }, + EvaluatedCallArg { + name: None, + value: opened_path, + ref_target: Some(EvalReferenceTarget::Cell { cell: opened_path }), + }, + ]) +} + +/// Returns a callable eval-declared wrapper method. +fn eval_user_wrapper_method( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + let (declaring_class, method) = context.class_method(class_name, method_name)?; + if method.visibility() != EvalVisibility::Public || method.is_static() || method.is_abstract() { + return None; + } + Some((declaring_class, method)) +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs index bb241ef1f4..19680fe0f7 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -130,6 +130,84 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval userspace stream wrappers dispatch core stream methods. +#[test] +fn execute_program_dispatches_user_stream_wrapper_methods() { + let program = parse_fragment( + br#"class EvalUserWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + $this->data = "abcdef"; + $this->pos = 0; + $opened_path = "ignored"; + return true; + } + public function stream_read($count): string { + $chunk = substr($this->data, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_write($data): int { + echo "[" . $data . "]"; + return strlen($data); + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + public function stream_close(): void { + echo "C"; + } +} +echo stream_wrapper_register("uw", "EvalUserWrapperW") ? "reg" : "bad"; echo ":"; +$h = fopen("uw://read", "r"); +echo is_resource($h) ? "open" : "bad"; echo ":"; +echo fread($h, 2); echo ":"; +echo feof($h) ? "bad" : "not"; echo ":"; +echo fread($h, 4); echo ":"; +echo feof($h) ? "eof" : "bad"; echo ":"; +echo fclose($h) ? "closed" : "bad"; echo ":"; +$w = fopen("uw://write", "w"); +echo fwrite($w, "xyz") === 3 ? "wrote" : "bad"; echo ":"; +echo fclose($w) ? "closed2" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "reg:open:ab:not:cdef:eof:Cclosed:[xyz]wrote:Cclosed2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies a wrapper whose `stream_open()` returns false makes `fopen()` false. +#[test] +fn execute_program_user_stream_wrapper_open_false_returns_false() { + let program = parse_fragment( + br#"class EvalRejectWrapperW { + public function stream_open($path, $mode, $options, &$opened_path): bool { + return false; + } +} +stream_wrapper_register("rejectw", "EvalRejectWrapperW"); +echo fopen("rejectw://x", "r") === false ? "false" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "false"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Starts a localhost HTTP server that returns one fixed body and then exits. fn spawn_http_once(body: &'static str) -> (u16, JoinHandle<()>) { let listener = TcpListener::bind("127.0.0.1:0").expect("bind HTTP wrapper fixture"); diff --git a/crates/elephc-magician/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs index 98acdde0eb..78b78dd4a8 100644 --- a/crates/elephc-magician/src/stream_resources.rs +++ b/crates/elephc-magician/src/stream_resources.rs @@ -40,7 +40,9 @@ pub(crate) struct EvalStreamResources { socket_names: HashMap, stream_contexts: HashMap, streams: HashMap, + user_stream_wrapper_classes: HashMap, user_stream_wrappers: Vec, + user_wrapper_streams: HashMap, } impl EvalStreamResources { @@ -238,8 +240,13 @@ impl EvalStreamResources { self.insert_stream_context(EvalStreamContext { options }) } - /// Registers a user stream wrapper protocol in eval-local state. - pub(crate) fn register_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { + /// Registers a user stream wrapper protocol and class in eval-local state. + pub(crate) fn register_stream_wrapper( + &mut self, + protocol: &str, + class_name: &str, + builtins: &[&str], + ) -> bool { let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { return false; }; @@ -255,6 +262,8 @@ impl EvalStreamResources { { return false; } + self.user_stream_wrapper_classes + .insert(protocol.clone(), class_name.to_string()); self.user_stream_wrappers.push(protocol); true } @@ -269,7 +278,8 @@ impl EvalStreamResources { .iter() .position(|current| current.eq_ignore_ascii_case(&protocol)) { - self.user_stream_wrappers.remove(index); + let protocol = self.user_stream_wrappers.remove(index); + self.user_stream_wrapper_classes.remove(&protocol); return true; } if eval_builtin_stream_wrapper_exists(builtins, &protocol) { @@ -301,6 +311,54 @@ impl EvalStreamResources { wrappers } + /// Returns the registered userspace wrapper class for a URL scheme. + pub(crate) fn user_stream_wrapper_class_for_path(&self, path: &str) -> Option { + let scheme = stream_wrappers::stream_scheme(path)?; + self.user_stream_wrapper_classes.get(&scheme).cloned() + } + + /// Opens a userspace wrapper stream around an eval-created wrapper object. + pub(crate) fn open_user_wrapper_stream( + &mut self, + object: RuntimeCellHandle, + class_name: &str, + uri: &str, + mode: &str, + ) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.user_wrapper_streams.insert( + id, + EvalUserWrapperStream { + object, + class_name: class_name.to_string(), + uri: uri.to_string(), + mode: mode.to_string(), + eof: false, + }, + ); + id + } + + /// Returns a copied userspace-wrapper stream descriptor for dispatch. + pub(crate) fn user_wrapper_stream_info( + &self, + id: i64, + ) -> Option { + self.user_wrapper_streams + .get(&id) + .map(EvalUserWrapperStream::info) + } + + /// Updates the cached EOF state for a userspace-wrapper stream. + pub(crate) fn set_user_wrapper_eof(&mut self, id: i64, eof: bool) -> bool { + let Some(stream) = self.user_wrapper_streams.get_mut(&id) else { + return false; + }; + stream.eof = eof; + true + } + /// Returns the default stream context resource id, creating it if needed. pub(crate) fn default_stream_context(&mut self) -> i64 { if let Some(id) = self.default_stream_context { @@ -319,7 +377,10 @@ impl EvalStreamResources { closed = true; ok = stream.finalize_on_close(); } - closed = closed || self.filter_resources.remove(&id) || self.socket_listeners.remove(&id).is_some(); + closed = closed + || self.user_wrapper_streams.remove(&id).is_some() + || self.filter_resources.remove(&id) + || self.socket_listeners.remove(&id).is_some(); self.socket_names.remove(&id); if let Some(mut child) = self.process_children.remove(&id) { let _ = child.wait(); @@ -329,7 +390,7 @@ impl EvalStreamResources { /// Returns whether a file-like stream resource exists. pub(crate) fn has_stream(&self, id: i64) -> bool { - self.streams.contains_key(&id) + self.streams.contains_key(&id) || self.user_wrapper_streams.contains_key(&id) } /// Returns a local or remote socket name for a socket resource. @@ -560,7 +621,10 @@ impl EvalStreamResources { /// Returns whether the stream has reached EOF after the last read attempt. pub(crate) fn eof(&self, id: i64) -> Option { - self.streams.get(&id).map(|stream| stream.eof) + self.streams + .get(&id) + .map(|stream| stream.eof) + .or_else(|| self.user_wrapper_streams.get(&id).map(|stream| stream.eof)) } /// Returns the current stream cursor offset. @@ -671,7 +735,14 @@ impl EvalStreamResources { /// Returns metadata fields used by PHP `stream_get_meta_data()`. pub(crate) fn meta_data(&self, id: i64) -> Option { - let stream = self.streams.get(&id)?; + if let Some(stream) = self.streams.get(&id) { + return Some(EvalStreamMetaData { + eof: stream.eof, + mode: stream.mode.clone(), + uri: stream.uri.clone(), + }); + } + let stream = self.user_wrapper_streams.get(&id)?; Some(EvalStreamMetaData { eof: stream.eof, mode: stream.mode.clone(), @@ -916,6 +987,33 @@ impl EvalFileStream { } } +/// Userspace wrapper stream stored behind one eval resource id. +struct EvalUserWrapperStream { + object: RuntimeCellHandle, + class_name: String, + uri: String, + mode: String, + eof: bool, +} + +impl EvalUserWrapperStream { + /// Copies the dispatch-relevant wrapper fields out of the resource table. + fn info(&self) -> EvalUserWrapperStreamInfo { + EvalUserWrapperStreamInfo { + object: self.object, + class_name: self.class_name.clone(), + eof: self.eof, + } + } +} + +/// Copied userspace-wrapper stream fields used while dispatching PHP methods. +pub(crate) struct EvalUserWrapperStreamInfo { + pub(crate) object: RuntimeCellHandle, + pub(crate) class_name: String, + pub(crate) eof: bool, +} + /// Wrapper targets that need a write-back step when their stream closes. enum EvalStreamFlushTarget { PharUrl(Vec), diff --git a/crates/elephc-magician/src/stream_wrappers.rs b/crates/elephc-magician/src/stream_wrappers.rs index ec67392455..b86073a715 100644 --- a/crates/elephc-magician/src/stream_wrappers.rs +++ b/crates/elephc-magician/src/stream_wrappers.rs @@ -36,6 +36,15 @@ pub(crate) fn is_http_stream(path: &str) -> bool { path.starts_with("http://") } +/// Extracts and normalizes the PHP stream-wrapper scheme from a URL-like path. +pub(crate) fn stream_scheme(path: &str) -> Option { + let separator = path.find("://")?; + if separator == 0 || !path_has_scheme(path) { + return None; + } + Some(path[..separator].to_ascii_lowercase()) +} + /// Maps plain local paths and `file://` URLs onto host filesystem paths. pub(crate) fn local_filesystem_path(path: &str) -> Option { if let Some(rest) = path.strip_prefix("file://") { From e63f60b3ecc71b7dfbde02c0d20c92f8c4fd5409 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 20:30:38 +0200 Subject: [PATCH 0911/1208] Extend magician wrapper stream helpers --- .../builtins/filesystem/streams.rs | 23 +- .../filesystem/user_wrapper_streams.rs | 235 +++++++++++++++++- .../tests/builtins_stream_wrappers.rs | 69 +++++ 3 files changed, 313 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index 8f380a5be1..2211eda3c9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -157,7 +157,12 @@ pub(in crate::interpreter) fn eval_unary_stream_result( } None => values.bool_value(false), }, - "rewind" => values.bool_value(context.stream_resources_mut().rewind(id)), + "rewind" => { + if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, 0, 0, context, values)? { + return values.bool_value(seek_ok); + } + values.bool_value(context.stream_resources_mut().rewind(id)) + } "fstat" => match context.stream_resources().metadata(id) { Some(metadata) => eval_stat_metadata_array(&metadata, values), None => values.bool_value(false), @@ -173,6 +178,9 @@ fn eval_fpassthru_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if let Some(result) = eval_user_wrapper_fpassthru_result(id, context, values)? { + return Ok(result); + } let Some(bytes) = context.stream_resources_mut().get_contents(id, None, None) else { return values.bool_value(false); }; @@ -578,6 +586,9 @@ pub(in crate::interpreter) fn eval_fseek_result( Some(whence) => eval_int_value(whence, values)?, None => 0, }; + if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, offset, whence, context, values)? { + return values.int(if seek_ok { 0 } else { -1 }); + } let status = if context.stream_resources_mut().seek(id, offset, whence) { 0 } else { @@ -649,6 +660,11 @@ pub(in crate::interpreter) fn eval_stream_get_contents_result( let id = eval_stream_resource_id(stream, values)?; let length = eval_optional_stream_length(length, values)?; let offset = eval_optional_stream_offset(offset, values)?; + if let Some(result) = + eval_user_wrapper_stream_get_contents_result(id, length, offset, context, values)? + { + return Ok(result); + } match context .stream_resources_mut() .get_contents(id, length, offset) @@ -739,6 +755,11 @@ pub(in crate::interpreter) fn eval_stream_copy_to_stream_result( let to = eval_stream_resource_id(to, values)?; let length = eval_optional_stream_length(length, values)?; let offset = eval_optional_stream_offset(offset, values)?; + if let Some(result) = + eval_user_wrapper_stream_copy_to_stream_result(from, to, length, offset, context, values)? + { + return Ok(result); + } match context .stream_resources_mut() .copy_to_stream(from, to, length, offset) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs index dab0a5c84c..574798375f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs @@ -94,13 +94,147 @@ pub(in crate::interpreter) fn eval_user_wrapper_fread_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { + let Some(bytes) = eval_user_wrapper_read_bytes(id, length, context, values)? else { + return Ok(None); + }; + values.string_bytes_value(&bytes).map(Some) +} + +/// Dispatches `fwrite()` to a wrapper object's `stream_write()`. +pub(in crate::interpreter) fn eval_user_wrapper_fwrite_result( + id: i64, + data: &[u8], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(written) = eval_user_wrapper_write_bytes(id, data, context, values)? else { + return Ok(None); + }; + values.int(written).map(Some) +} + +/// Dispatches `feof()` to a wrapper object's `stream_eof()`. +pub(in crate::interpreter) fn eval_user_wrapper_feof_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let eof = eval_user_wrapper_eof_bool(id, context, values)?; + values.bool_value(eof).map(Some) +} + +/// Dispatches `fseek()` or `rewind()` to a wrapper object's `stream_seek()`. +pub(in crate::interpreter) fn eval_user_wrapper_fseek_result( + id: i64, + offset: i64, + whence: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + eval_user_wrapper_seek_bool(id, offset, whence, context, values).map(Some) +} + +/// Reads the remaining or bounded contents from a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_stream_get_contents_result( + id: i64, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(bytes) = + eval_user_wrapper_contents_bytes(id, length, offset, context, values)? + else { + return values.bool_value(false).map(Some); + }; + values.string_bytes_value(&bytes).map(Some) +} + +/// Copies bytes between streams when either endpoint is a userspace wrapper. +pub(in crate::interpreter) fn eval_user_wrapper_stream_copy_to_stream_result( + from: i64, + to: i64, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let from_is_wrapper = context.stream_resources().user_wrapper_stream_info(from).is_some(); + let to_is_wrapper = context.stream_resources().user_wrapper_stream_info(to).is_some(); + if !from_is_wrapper && !to_is_wrapper { + return Ok(None); + } + let bytes = if from_is_wrapper { + let Some(bytes) = + eval_user_wrapper_contents_bytes(from, length, offset, context, values)? + else { + return values.bool_value(false).map(Some); + }; + bytes + } else { + let Some(bytes) = context + .stream_resources_mut() + .get_contents(from, length, offset) + else { + return values.bool_value(false).map(Some); + }; + bytes + }; + let written = if to_is_wrapper { + let Some(written) = eval_user_wrapper_write_bytes(to, &bytes, context, values)? else { + return values.bool_value(false).map(Some); + }; + written + } else { + let Some(written) = context.stream_resources_mut().write(to, &bytes) else { + return values.bool_value(false).map(Some); + }; + i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)? + }; + values.int(written).map(Some) +} + +/// Streams remaining wrapper bytes to eval output and returns the byte count. +pub(in crate::interpreter) fn eval_user_wrapper_fpassthru_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(bytes) = eval_user_wrapper_contents_bytes(id, None, None, context, values)? else { + return values.bool_value(false).map(Some); + }; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(len).map(Some) +} + +/// Reads one chunk from a userspace-wrapper stream. +fn eval_user_wrapper_read_bytes( + id: i64, + length: usize, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result>, EvalStatus> { let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { return Ok(None); }; let Some((declaring_class, stream_read)) = eval_user_wrapper_method(&info.class_name, "stream_read", context) else { - return values.bool_value(false).map(Some); + return Ok(None); }; let length = values.int(i64::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?)?; let result = eval_dynamic_method_with_values( @@ -117,23 +251,23 @@ pub(in crate::interpreter) fn eval_user_wrapper_fread_result( context .stream_resources_mut() .set_user_wrapper_eof(id, bytes.is_empty()); - values.string_bytes_value(&bytes).map(Some) + Ok(Some(bytes)) } -/// Dispatches `fwrite()` to a wrapper object's `stream_write()`. -pub(in crate::interpreter) fn eval_user_wrapper_fwrite_result( +/// Writes one byte slice to a userspace-wrapper stream. +fn eval_user_wrapper_write_bytes( id: i64, data: &[u8], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result, EvalStatus> { let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { return Ok(None); }; let Some((declaring_class, stream_write)) = eval_user_wrapper_method(&info.class_name, "stream_write", context) else { - return values.bool_value(false).map(Some); + return Ok(None); }; let data = values.string_bytes_value(data)?; let result = eval_dynamic_method_with_values( @@ -148,22 +282,62 @@ pub(in crate::interpreter) fn eval_user_wrapper_fwrite_result( let written = eval_int_value(result, values)?; values.release(result)?; context.stream_resources_mut().set_user_wrapper_eof(id, false); - values.int(written).map(Some) + Ok(Some(written)) } -/// Dispatches `feof()` to a wrapper object's `stream_eof()`. -pub(in crate::interpreter) fn eval_user_wrapper_feof_result( +/// Reads wrapper bytes until EOF or a finite length cap. +fn eval_user_wrapper_contents_bytes( id: i64, + length: Option, + offset: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result>, EvalStatus> { + if let Some(offset) = offset { + if !eval_user_wrapper_seek_bool(id, offset, 0, context, values)? { + return Ok(None); + } + } + let mut bytes = Vec::new(); + loop { + if length.is_some_and(|limit| bytes.len() >= limit) { + break; + } + if eval_user_wrapper_eof_bool(id, context, values)? { + break; + } + let remaining = length + .map(|limit| limit.saturating_sub(bytes.len())) + .unwrap_or(8192); + let Some(mut chunk) = + eval_user_wrapper_read_bytes(id, remaining, context, values)? + else { + return Ok(None); + }; + if chunk.is_empty() { + break; + } + if let Some(limit) = length { + chunk.truncate(limit.saturating_sub(bytes.len())); + } + bytes.extend_from_slice(&chunk); + } + Ok(Some(bytes)) +} + +/// Returns a userspace-wrapper EOF result, falling back to cached EOF state. +fn eval_user_wrapper_eof_bool( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { - return Ok(None); + return Ok(false); }; let Some((declaring_class, stream_eof)) = eval_user_wrapper_method(&info.class_name, "stream_eof", context) else { - return values.bool_value(info.eof).map(Some); + return Ok(info.eof); }; let result = eval_dynamic_method_with_values( &declaring_class, @@ -177,7 +351,42 @@ pub(in crate::interpreter) fn eval_user_wrapper_feof_result( let eof = values.truthy(result)?; values.release(result)?; context.stream_resources_mut().set_user_wrapper_eof(id, eof); - values.bool_value(eof).map(Some) + Ok(eof) +} + +/// Returns a userspace-wrapper seek result, or false when the method is absent. +fn eval_user_wrapper_seek_bool( + id: i64, + offset: i64, + whence: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(false); + }; + let Some((declaring_class, stream_seek)) = + eval_user_wrapper_method(&info.class_name, "stream_seek", context) + else { + return Ok(false); + }; + let offset = values.int(offset)?; + let whence = values.int(whence)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_seek, + info.object, + positional_args(vec![offset, whence]), + context, + values, + )?; + let ok = values.truthy(result)?; + values.release(result)?; + if ok { + context.stream_resources_mut().set_user_wrapper_eof(id, false); + } + Ok(ok) } /// Builds the four PHP arguments passed to a wrapper `stream_open()` method. diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs index 19680fe0f7..14a7f2c5e7 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -208,6 +208,75 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies aggregate stream helpers drain and copy userspace wrapper streams. +#[test] +fn execute_program_dispatches_user_stream_wrapper_aggregate_reads() { + let program = parse_fragment( + br#"class EvalSlowWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + $this->data = "abcdefghi"; + $this->pos = 0; + return true; + } + public function stream_read($count): string { + $limit = min(2, $count); + $chunk = substr($this->data, $this->pos, $limit); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + public function stream_seek($offset, $whence): bool { + if ($whence !== 0) { return false; } + $this->pos = $offset; + return true; + } +} +class EvalSinkWrapperW { + public function stream_open($path, $mode, $options, &$opened_path): bool { + return true; + } + public function stream_write($data): int { + echo "<" . $data . ">"; + return strlen($data); + } +} +stream_wrapper_register("sloww", "EvalSlowWrapperW"); +stream_wrapper_register("sinkw", "EvalSinkWrapperW"); +$h = fopen("sloww://read", "r"); +echo stream_get_contents($h, 5) === "abcde" ? "bounded" : "bad"; echo ":"; +echo stream_get_contents($h) === "fghi" ? "rest" : "bad"; echo ":"; +echo stream_get_contents($h, 3, 2) === "cde" ? "offset" : "bad"; echo ":"; +$src = fopen("sloww://copy", "r"); +$dst = fopen("php://memory", "w+"); +echo stream_copy_to_stream($src, $dst, 5) === 5 ? "copy" : "bad"; echo ":"; +rewind($dst); +echo stream_get_contents($dst) === "abcde" ? "copied" : "bad"; echo ":"; +$raw = fopen("php://memory", "w+"); +fwrite($raw, "sinkdata"); +rewind($raw); +$sink = fopen("sinkw://out", "w"); +echo stream_copy_to_stream($raw, $sink) === 8 ? "sinkcopy" : "bad"; echo ":"; +$pass = fopen("sloww://pass", "r"); +echo fpassthru($pass) === 9 ? "pass" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "bounded:rest:offset:copy:copied:sinkcopy:abcdefghipass" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Starts a localhost HTTP server that returns one fixed body and then exits. fn spawn_http_once(body: &'static str) -> (u16, JoinHandle<()>) { let listener = TcpListener::bind("127.0.0.1:0").expect("bind HTTP wrapper fixture"); From 6756e5af826de557f47eb4c49bc14200b3fac386 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 20:41:11 +0200 Subject: [PATCH 0912/1208] Add magician wrapper url_stat dispatch --- .../builtins/filesystem/file_io.rs | 34 ++++++- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/user_wrapper_stat.rs | 91 +++++++++++++++++++ .../filesystem/user_wrapper_streams.rs | 38 ++++++++ .../builtins/registry/dispatch/filesystem.rs | 10 +- .../tests/builtins_stream_wrappers.rs | 52 +++++++++++ 6 files changed, 217 insertions(+), 10 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs index 35baa5a67d..152686e8c8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs @@ -42,16 +42,20 @@ pub(in crate::interpreter) fn eval_builtin_file_probe( return Err(EvalStatus::RuntimeFatal); }; let filename = eval_expr(filename, context, scope, values)?; - eval_file_probe_result(name, filename, values) + eval_file_probe_result(name, filename, context, values) } /// Computes one local filesystem predicate and returns a PHP boolean. pub(in crate::interpreter) fn eval_file_probe_result( name: &str, filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return eval_user_wrapper_file_probe_from_stat(name, stat, values); + } if stream_wrappers::is_phar_stream(&path) { let exists = elephc_phar::extract_url_bytes(path.as_bytes()).is_some(); let supported = matches!(name, "file_exists" | "is_file" | "is_readable"); @@ -88,16 +92,20 @@ pub(in crate::interpreter) fn eval_builtin_file_stat_scalar( return Err(EvalStatus::RuntimeFatal); }; let filename = eval_expr(filename, context, scope, values)?; - eval_file_stat_scalar_result(name, filename, values) + eval_file_stat_scalar_result(name, filename, context, values) } /// Returns scalar stat metadata, using PHP false for failure where native elephc does. pub(in crate::interpreter) fn eval_file_stat_scalar_result( name: &str, filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return eval_user_wrapper_file_stat_scalar_from_stat(name, stat, values); + } let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return match name { "filemtime" => values.int(0), @@ -289,15 +297,20 @@ pub(in crate::interpreter) fn eval_builtin_filesize( return Err(EvalStatus::RuntimeFatal); }; let filename = eval_expr(filename, context, scope, values)?; - eval_filesize_result(filename, values) + eval_filesize_result(filename, context, values) } /// Returns one local file or supported wrapper size in bytes, or zero on failure. pub(in crate::interpreter) fn eval_filesize_result( filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + let size = eval_user_wrapper_stat_int_field(stat, "size", values)?.unwrap_or(0); + return values.int(size); + } if let Ok(bytes) = eval_read_path_or_wrapper_bytes(&path) { return values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?); } @@ -321,15 +334,22 @@ pub(in crate::interpreter) fn eval_builtin_filetype( return Err(EvalStatus::RuntimeFatal); }; let filename = eval_expr(filename, context, scope, values)?; - eval_filetype_result(filename, values) + eval_filetype_result(filename, context, values) } /// Returns the PHP filetype string for one path, or false when lstat fails. pub(in crate::interpreter) fn eval_filetype_result( filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return match eval_user_wrapper_stat_int_field(stat, "mode", values)? { + Some(mode) => values.string(eval_filetype_label_from_mode(mode)), + None => values.bool_value(false), + }; + } if stream_wrappers::is_phar_stream(&path) { return if elephc_phar::extract_url_bytes(path.as_bytes()).is_some() { values.string("file") @@ -376,16 +396,20 @@ pub(in crate::interpreter) fn eval_builtin_stat_array( return Err(EvalStatus::RuntimeFatal); }; let filename = eval_expr(filename, context, scope, values)?; - eval_stat_array_result(name, filename, values) + eval_stat_array_result(name, filename, context, values) } /// Builds PHP's stat array for one local path, or returns false on stat failure. pub(in crate::interpreter) fn eval_stat_array_result( name: &str, filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return Ok(stat); + } let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.bool_value(false); }; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index fc18c2ca58..cc99b14ed4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -20,6 +20,7 @@ mod stream_context; mod stream_settings; mod stream_sockets; mod streams; +mod user_wrapper_stat; mod user_wrapper_streams; pub(in crate::interpreter) use directories::*; @@ -34,4 +35,5 @@ pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; +pub(in crate::interpreter) use user_wrapper_stat::*; pub(in crate::interpreter) use user_wrapper_streams::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs new file mode 100644 index 0000000000..80c36ef07d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs @@ -0,0 +1,91 @@ +//! Purpose: +//! Interprets userspace stream-wrapper `url_stat()` arrays for path builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::file_io` when a path belongs +//! to a registered eval userspace stream wrapper. +//! +//! Key details: +//! - The wrapper owns the stat array shape. These helpers read the PHP-standard +//! string keys used by file probes, scalar stat builtins, and `filetype()`. + +use super::super::super::*; + +/// Computes one filesystem predicate from a userspace wrapper `url_stat()` result. +pub(in crate::interpreter) fn eval_user_wrapper_file_probe_from_stat( + name: &str, + stat: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.truthy(stat)? { + return values.bool_value(false); + } + let mode = eval_user_wrapper_stat_int_field(stat, "mode", values)?.unwrap_or(0); + let result = match name { + "file_exists" => true, + "is_dir" => eval_mode_kind(mode) == libc::S_IFDIR as i64, + "is_executable" => mode & 0o111 != 0, + "is_file" => eval_mode_kind(mode) == libc::S_IFREG as i64, + "is_link" => eval_mode_kind(mode) == libc::S_IFLNK as i64, + "is_readable" => mode & 0o444 != 0, + "is_writable" | "is_writeable" => mode & 0o222 != 0, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(result) +} + +/// Returns one scalar stat builtin value from a userspace wrapper stat array. +pub(in crate::interpreter) fn eval_user_wrapper_file_stat_scalar_from_stat( + name: &str, + stat: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let field = match name { + "fileatime" => "atime", + "filectime" => "ctime", + "filegroup" => "gid", + "fileinode" => "ino", + "filemtime" => "mtime", + "fileowner" => "uid", + "fileperms" => "mode", + _ => return Err(EvalStatus::RuntimeFatal), + }; + match eval_user_wrapper_stat_int_field(stat, field, values)? { + Some(value) => values.int(value), + None if name == "filemtime" => values.int(0), + None => values.bool_value(false), + } +} + +/// Extracts one integer field from a userspace wrapper stat result. +pub(in crate::interpreter) fn eval_user_wrapper_stat_int_field( + stat: RuntimeCellHandle, + field: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.truthy(stat)? { + return Ok(None); + } + let key = values.string(field)?; + let value = values.array_get(stat, key)?; + Ok(Some(eval_int_value(value, values)?)) +} + +/// Maps one POSIX mode value to PHP's `filetype()` label. +pub(in crate::interpreter) fn eval_filetype_label_from_mode(mode: i64) -> &'static str { + match eval_mode_kind(mode) { + kind if kind == libc::S_IFREG as i64 => "file", + kind if kind == libc::S_IFDIR as i64 => "dir", + kind if kind == libc::S_IFLNK as i64 => "link", + kind if kind == libc::S_IFCHR as i64 => "char", + kind if kind == libc::S_IFBLK as i64 => "block", + kind if kind == libc::S_IFIFO as i64 => "fifo", + kind if kind == libc::S_IFSOCK as i64 => "socket", + _ => "unknown", + } +} + +/// Masks one POSIX mode value down to its file-kind bits. +fn eval_mode_kind(mode: i64) -> i64 { + mode & (libc::S_IFMT as i64) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs index 574798375f..1568c74f93 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs @@ -221,6 +221,44 @@ pub(in crate::interpreter) fn eval_user_wrapper_fpassthru_result( values.int(len).map(Some) } +/// Dispatches path-based filesystem probes to a wrapper object's `url_stat()`. +pub(in crate::interpreter) fn eval_user_wrapper_url_stat_result( + path: &str, + flags: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(path) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, url_stat)) = + eval_user_wrapper_method(class.name(), "url_stat", context) + else { + return values.bool_value(false).map(Some); + }; + let mut scope = ElephcEvalScope::new(); + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, &mut scope, values)?; + let path = values.string(path)?; + let flags = values.int(flags)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &url_stat, + object, + positional_args(vec![path, flags]), + context, + values, + )?; + values.release(object)?; + Ok(Some(result)) +} + /// Reads one chunk from a userspace-wrapper stream. fn eval_user_wrapper_read_bytes( id: i64, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index cf84eabc97..f487ff457b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -66,14 +66,14 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_file_probe_result(name, *filename, values)? + eval_file_probe_result(name, *filename, context, values)? } "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_file_stat_scalar_result(name, *filename, values)? + eval_file_stat_scalar_result(name, *filename, context, values)? } "file_get_contents" => { let [filename] = evaluated_args else { @@ -108,13 +108,13 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_filesize_result(*filename, values)? + eval_filesize_result(*filename, context, values)? } "filetype" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_filetype_result(*filename, values)? + eval_filetype_result(*filename, context, values)? } "fnmatch" => match evaluated_args { [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, @@ -216,7 +216,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_stat_array_result(name, *filename, values)? + eval_stat_array_result(name, *filename, context, values)? } "linkinfo" => { let [path] = evaluated_args else { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs index 14a7f2c5e7..ca1f8201ce 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -277,6 +277,58 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies path-based filesystem builtins dispatch to wrapper `url_stat()`. +#[test] +fn execute_program_dispatches_user_stream_wrapper_url_stat() { + let program = parse_fragment( + br#"class EvalUrlStatWrapperW { + public function url_stat($path, $flags) { + if (str_contains($path, "missing")) { + return false; + } + if (str_contains($path, "dir")) { + return ["mode" => 16877, "size" => 0, "mtime" => 5, "uid" => 10]; + } + return [ + "mode" => 33188, + "size" => 123, + "mtime" => 77, + "atime" => 66, + "ctime" => 88, + "uid" => 501, + "gid" => 20, + "ino" => 9 + ]; + } +} +stream_wrapper_register("ustat", "EvalUrlStatWrapperW"); +echo file_exists("ustat://file") ? "exists" : "bad"; echo ":"; +echo is_file("ustat://file") ? "file" : "bad"; echo ":"; +echo is_dir("ustat://file") ? "bad" : "notdir"; echo ":"; +echo filetype("ustat://file") === "file" ? "type" : "bad"; echo ":"; +echo filesize("ustat://file") === 123 ? "size" : "bad"; echo ":"; +echo filemtime("ustat://file") === 77 ? "mtime" : "bad"; echo ":"; +echo fileowner("ustat://file") === 501 ? "owner" : "bad"; echo ":"; +$stat = stat("ustat://file"); +echo $stat["size"] === 123 && $stat["mode"] === 33188 ? "stat" : "bad"; echo ":"; +echo call_user_func("filesize", "ustat://file") === 123 ? "callsize" : "bad"; echo ":"; +echo file_exists("ustat://missing") ? "bad" : "missing"; echo ":"; +echo filetype("ustat://dir") === "dir" ? "dirtype" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "exists:file:notdir:type:size:mtime:owner:stat:callsize:missing:dirtype" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Starts a localhost HTTP server that returns one fixed body and then exits. fn spawn_http_once(body: &'static str) -> (u16, JoinHandle<()>) { let listener = TcpListener::bind("127.0.0.1:0").expect("bind HTTP wrapper fixture"); From fdd3471d72a54e5f1e13ab1afd34d4d5903e60a2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 20:51:27 +0200 Subject: [PATCH 0913/1208] Add magician wrapper stream_stat dispatch --- .../builtins/filesystem/streams.rs | 13 +++++--- .../builtins/filesystem/user_wrapper_stat.rs | 30 ++++++++++++++++++- .../filesystem/user_wrapper_streams.rs | 2 +- .../tests/builtins_stream_wrappers.rs | 11 ++++++- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index 2211eda3c9..fd85723e36 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -163,10 +163,15 @@ pub(in crate::interpreter) fn eval_unary_stream_result( } values.bool_value(context.stream_resources_mut().rewind(id)) } - "fstat" => match context.stream_resources().metadata(id) { - Some(metadata) => eval_stat_metadata_array(&metadata, values), - None => values.bool_value(false), - }, + "fstat" => { + if let Some(result) = eval_user_wrapper_fstat_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources().metadata(id) { + Some(metadata) => eval_stat_metadata_array(&metadata, values), + None => values.bool_value(false), + } + } "stream_get_meta_data" => eval_stream_get_meta_data_result(id, context, values), _ => Err(EvalStatus::RuntimeFatal), } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs index 80c36ef07d..7d629c8ee1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs @@ -1,9 +1,11 @@ //! Purpose: -//! Interprets userspace stream-wrapper `url_stat()` arrays for path builtins. +//! Interprets userspace stream-wrapper stat results for path and stream builtins. //! //! Called from: //! - `crate::interpreter::builtins::filesystem::file_io` when a path belongs //! to a registered eval userspace stream wrapper. +//! - `crate::interpreter::builtins::filesystem::streams` when `fstat()` sees a +//! userspace-wrapper stream resource. //! //! Key details: //! - The wrapper owns the stat array shape. These helpers read the PHP-standard @@ -11,6 +13,32 @@ use super::super::super::*; +/// Dispatches `fstat()` to a wrapper object's `stream_stat()`. +pub(in crate::interpreter) fn eval_user_wrapper_fstat_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_stat)) = + eval_user_wrapper_method(&info.class_name, "stream_stat", context) + else { + return values.bool_value(false).map(Some); + }; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_stat, + info.object, + Vec::new(), + context, + values, + )?; + Ok(Some(result)) +} + /// Computes one filesystem predicate from a userspace wrapper `url_stat()` result. pub(in crate::interpreter) fn eval_user_wrapper_file_probe_from_stat( name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs index 1568c74f93..bd9145ec97 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs @@ -462,7 +462,7 @@ fn eval_user_wrapper_stream_open_args( } /// Returns a callable eval-declared wrapper method. -fn eval_user_wrapper_method( +pub(in crate::interpreter) fn eval_user_wrapper_method( class_name: &str, method_name: &str, context: &ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs index ca1f8201ce..08134e29a0 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -300,6 +300,12 @@ fn execute_program_dispatches_user_stream_wrapper_url_stat() { "ino" => 9 ]; } + public function stream_open($path, $mode, $options, &$opened_path): bool { + return true; + } + public function stream_stat() { + return ["mode" => 33188, "size" => 321]; + } } stream_wrapper_register("ustat", "EvalUrlStatWrapperW"); echo file_exists("ustat://file") ? "exists" : "bad"; echo ":"; @@ -311,6 +317,9 @@ echo filemtime("ustat://file") === 77 ? "mtime" : "bad"; echo ":"; echo fileowner("ustat://file") === 501 ? "owner" : "bad"; echo ":"; $stat = stat("ustat://file"); echo $stat["size"] === 123 && $stat["mode"] === 33188 ? "stat" : "bad"; echo ":"; +$h = fopen("ustat://file", "r"); +$fstat = fstat($h); +echo $fstat["size"] === 321 && $fstat["mode"] === 33188 ? "fstat" : "bad"; echo ":"; echo call_user_func("filesize", "ustat://file") === 123 ? "callsize" : "bad"; echo ":"; echo file_exists("ustat://missing") ? "bad" : "missing"; echo ":"; echo filetype("ustat://dir") === "dir" ? "dirtype" : "bad"; @@ -324,7 +333,7 @@ return true;"#, assert_eq!( values.output, - "exists:file:notdir:type:size:mtime:owner:stat:callsize:missing:dirtype" + "exists:file:notdir:type:size:mtime:owner:stat:fstat:callsize:missing:dirtype" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } From 75eb10e9dfe67e8d18fb550f5c15ece956e47a3d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 20:58:04 +0200 Subject: [PATCH 0914/1208] Add magician wrapper stream control dispatch --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/streams.rs | 23 +++- .../filesystem/user_wrapper_controls.rs | 110 ++++++++++++++++++ .../tests/builtins_stream_wrappers.rs | 66 +++++++++++ 4 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index cc99b14ed4..f11be5e3af 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -20,6 +20,7 @@ mod stream_context; mod stream_settings; mod stream_sockets; mod streams; +mod user_wrapper_controls; mod user_wrapper_stat; mod user_wrapper_streams; @@ -35,5 +36,6 @@ pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; +pub(in crate::interpreter) use user_wrapper_controls::*; pub(in crate::interpreter) use user_wrapper_stat::*; pub(in crate::interpreter) use user_wrapper_streams::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index fd85723e36..2ff0c6a3ba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -147,15 +147,25 @@ pub(in crate::interpreter) fn eval_unary_stream_result( } values.bool_value(context.stream_resources().eof(id).unwrap_or(false)) } - "fflush" => values.bool_value(context.stream_resources_mut().flush(id)), + "fflush" => { + if let Some(result) = eval_user_wrapper_fflush_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources_mut().flush(id)) + } "fpassthru" => eval_fpassthru_result(id, context, values), "fsync" => values.bool_value(context.stream_resources_mut().sync_all(id)), "fdatasync" => values.bool_value(context.stream_resources_mut().sync_data(id)), - "ftell" => match context.stream_resources_mut().tell(id) { - Some(position) => { - values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?) + "ftell" => { + if let Some(result) = eval_user_wrapper_ftell_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().tell(id) { + Some(position) => { + values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?) + } + None => values.bool_value(false), } - None => values.bool_value(false), }, "rewind" => { if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, 0, 0, context, values)? { @@ -629,6 +639,9 @@ pub(in crate::interpreter) fn eval_ftruncate_result( let Ok(size) = u64::try_from(size) else { return values.bool_value(false); }; + if let Some(result) = eval_user_wrapper_ftruncate_result(id, size, context, values)? { + return Ok(result); + } values.bool_value(context.stream_resources_mut().truncate(id, size)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs new file mode 100644 index 0000000000..648add1761 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs @@ -0,0 +1,110 @@ +//! Purpose: +//! Dispatches control-style userspace stream wrapper methods for eval streams. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams` for flush, tell, and +//! truncate builtins on userspace wrapper resources. +//! +//! Key details: +//! - File-backed resources keep using `EvalStreamResources`; these helpers only +//! intercept resources created by `stream_wrapper_register()` + `fopen()`. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +/// Dispatches `fflush()` to a wrapper object's `stream_flush()`. +pub(in crate::interpreter) fn eval_user_wrapper_fflush_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(result) = eval_user_wrapper_call_no_arg_method(id, "stream_flush", context, values)? + else { + return values.bool_value(false).map(Some); + }; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} + +/// Dispatches `ftell()` to a wrapper object's `stream_tell()`. +pub(in crate::interpreter) fn eval_user_wrapper_ftell_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(result) = eval_user_wrapper_call_no_arg_method(id, "stream_tell", context, values)? + else { + return values.bool_value(false).map(Some); + }; + if values.type_tag(result)? == EVAL_TAG_BOOL && !values.truthy(result)? { + values.release(result)?; + return values.bool_value(false).map(Some); + } + let position = eval_int_value(result, values)?; + values.release(result)?; + values.int(position).map(Some) +} + +/// Dispatches `ftruncate()` to a wrapper object's `stream_truncate()`. +pub(in crate::interpreter) fn eval_user_wrapper_ftruncate_result( + id: i64, + size: u64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_truncate)) = + eval_user_wrapper_method(&info.class_name, "stream_truncate", context) + else { + return values.bool_value(false).map(Some); + }; + let size = values.int(i64::try_from(size).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_truncate, + info.object, + positional_args(vec![size]), + context, + values, + )?; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} + +/// Calls one no-argument userspace wrapper method on a stream resource. +fn eval_user_wrapper_call_no_arg_method( + id: i64, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, method)) = + eval_user_wrapper_method(&info.class_name, method_name, context) + else { + return Ok(None); + }; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &method, + info.object, + Vec::new(), + context, + values, + )?; + Ok(Some(result)) +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs index 08134e29a0..cd7707537e 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -185,6 +185,72 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval userspace stream wrappers dispatch stream control methods. +#[test] +fn execute_program_dispatches_user_stream_wrapper_control_methods() { + let program = parse_fragment( + br#"class EvalControlWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + $this->data = "abcdef"; + $this->pos = 0; + return true; + } + public function stream_read($count): string { + $chunk = substr($this->data, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + public function stream_tell(): int { + echo "T"; + return $this->pos; + } + public function stream_seek($offset, $whence): bool { + echo "S" . $whence; + if ($whence !== 0) { return false; } + $this->pos = $offset; + return true; + } + public function stream_truncate($new_size): bool { + echo "R" . $new_size; + $this->data = substr($this->data, 0, $new_size); + if ($this->pos > $new_size) { $this->pos = $new_size; } + return true; + } + public function stream_flush(): bool { + echo "F"; + return true; + } +} +stream_wrapper_register("ctrlw", "EvalControlWrapperW"); +$h = fopen("ctrlw://one", "r+"); +echo ftell($h) === 0 ? "tell0" : "bad"; echo ":"; +echo fread($h, 2) === "ab" ? "read" : "bad"; echo ":"; +echo ftell($h) === 2 ? "tell2" : "bad"; echo ":"; +echo fseek($h, 1) === 0 ? "seek" : "bad"; echo ":"; +echo ftell($h) === 1 ? "tell1" : "bad"; echo ":"; +echo ftruncate($h, 3) ? "trunc" : "bad"; echo ":"; +echo stream_get_contents($h) === "bc" ? "contents" : "bad"; echo ":"; +echo fflush($h) ? "flush" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Ttell0:read:Ttell2:S0seek:Ttell1:R3trunc:contents:Fflush" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies a wrapper whose `stream_open()` returns false makes `fopen()` false. #[test] fn execute_program_user_stream_wrapper_open_false_returns_false() { From 5235bc2f08b1d706706e185bc98eef1d14111824 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 21:02:29 +0200 Subject: [PATCH 0915/1208] Add magician wrapper line read dispatch --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/streams.rs | 26 ++++-- .../builtins/filesystem/user_wrapper_lines.rs | 93 +++++++++++++++++++ .../filesystem/user_wrapper_streams.rs | 4 +- .../tests/builtins_stream_wrappers.rs | 38 ++++++++ 5 files changed, 153 insertions(+), 10 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_lines.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index f11be5e3af..25e5e3d838 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -21,6 +21,7 @@ mod stream_settings; mod stream_sockets; mod streams; mod user_wrapper_controls; +mod user_wrapper_lines; mod user_wrapper_stat; mod user_wrapper_streams; @@ -37,5 +38,6 @@ pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_controls::*; +pub(in crate::interpreter) use user_wrapper_lines::*; pub(in crate::interpreter) use user_wrapper_stat::*; pub(in crate::interpreter) use user_wrapper_streams::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index 2ff0c6a3ba..bc8e18c33d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -133,14 +133,19 @@ pub(in crate::interpreter) fn eval_unary_stream_result( None => values.bool_value(false), } } - "fgets" => match context - .stream_resources_mut() - .read_line(id, usize::MAX, None, true, true) - { - Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), - Some(_) => values.bool_value(false), - None => values.bool_value(false), - }, + "fgets" => { + if let Some(result) = eval_user_wrapper_fgets_result(id, context, values)? { + return Ok(result); + } + match context + .stream_resources_mut() + .read_line(id, usize::MAX, None, true, true) + { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } + } "feof" => { if let Some(result) = eval_user_wrapper_feof_result(id, context, values)? { return Ok(result); @@ -750,6 +755,11 @@ pub(in crate::interpreter) fn eval_stream_get_line_result( } _ => None, }; + if let Some(result) = + eval_user_wrapper_stream_get_line_result(id, length, ending.as_deref(), context, values)? + { + return Ok(result); + } match context .stream_resources_mut() .read_line(id, length, ending.as_deref(), false, false) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_lines.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_lines.rs new file mode 100644 index 0000000000..186bf31100 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_lines.rs @@ -0,0 +1,93 @@ +//! Purpose: +//! Implements line-oriented reads for eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams` for `fgets()` and +//! `stream_get_line()` on userspace wrapper resources. +//! +//! Key details: +//! - Reads flow through the wrapper object's `stream_read()`/`stream_eof()` +//! methods, preserving the same wrapper state used by aggregate reads. + +use super::super::super::*; +use super::user_wrapper_streams::{ + eval_user_wrapper_eof_bool, eval_user_wrapper_read_bytes, +}; + +/// Dispatches `fgets()` to a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_fgets_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(bytes) = + eval_user_wrapper_line_bytes(id, usize::MAX, None, true, true, context, values)? + else { + return values.bool_value(false).map(Some); + }; + if bytes.is_empty() { + return values.bool_value(false).map(Some); + } + values.string_bytes_value(&bytes).map(Some) +} + +/// Dispatches `stream_get_line()` to a userspace-wrapper stream. +pub(in crate::interpreter) fn eval_user_wrapper_stream_get_line_result( + id: i64, + length: usize, + ending: Option<&[u8]>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.stream_resources().user_wrapper_stream_info(id).is_none() { + return Ok(None); + } + let Some(bytes) = + eval_user_wrapper_line_bytes(id, length, ending, false, false, context, values)? + else { + return values.bool_value(false).map(Some); + }; + if bytes.is_empty() { + return values.bool_value(false).map(Some); + } + values.string_bytes_value(&bytes).map(Some) +} + +/// Reads one wrapper line up to a limit, newline, or custom delimiter. +fn eval_user_wrapper_line_bytes( + id: i64, + length: usize, + ending: Option<&[u8]>, + include_ending: bool, + stop_at_newline: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result>, EvalStatus> { + let mut output = Vec::new(); + while output.len() < length { + if eval_user_wrapper_eof_bool(id, context, values)? { + break; + } + let Some(chunk) = eval_user_wrapper_read_bytes(id, 1, context, values)? else { + return Ok(None); + }; + if chunk.is_empty() { + break; + } + output.push(chunk[0]); + if let Some(ending) = ending { + if !ending.is_empty() && output.ends_with(ending) { + if !include_ending { + output.truncate(output.len().saturating_sub(ending.len())); + } + break; + } + } else if stop_at_newline && chunk[0] == b'\n' { + break; + } + } + Ok(Some(output)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs index bd9145ec97..7cb4969cdf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_streams.rs @@ -260,7 +260,7 @@ pub(in crate::interpreter) fn eval_user_wrapper_url_stat_result( } /// Reads one chunk from a userspace-wrapper stream. -fn eval_user_wrapper_read_bytes( +pub(in crate::interpreter) fn eval_user_wrapper_read_bytes( id: i64, length: usize, context: &mut ElephcEvalContext, @@ -364,7 +364,7 @@ fn eval_user_wrapper_contents_bytes( } /// Returns a userspace-wrapper EOF result, falling back to cached EOF state. -fn eval_user_wrapper_eof_bool( +pub(in crate::interpreter) fn eval_user_wrapper_eof_bool( id: i64, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs index cd7707537e..c7372032a9 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -343,6 +343,44 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies line-oriented reads dispatch through userspace stream wrappers. +#[test] +fn execute_program_dispatches_user_stream_wrapper_line_reads() { + let program = parse_fragment( + br#"class EvalLineWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + $this->data = "aa\nbbENDcc"; + $this->pos = 0; + return true; + } + public function stream_read($count): string { + $chunk = substr($this->data, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } +} +stream_wrapper_register("linew", "EvalLineWrapperW"); +$h = fopen("linew://one", "r"); +echo fgets($h) === "aa\n" ? "fgets" : "bad"; echo ":"; +echo stream_get_line($h, 10, "END") === "bb" ? "getline" : "bad"; echo ":"; +echo stream_get_contents($h) === "cc" ? "rest" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "fgets:getline:rest"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies path-based filesystem builtins dispatch to wrapper `url_stat()`. #[test] fn execute_program_dispatches_user_stream_wrapper_url_stat() { From 9167e505a3a458ce5e2db11af86d102bcbcde13f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 21:06:06 +0200 Subject: [PATCH 0916/1208] Add magician wrapper stream_lock dispatch --- .../builtins/filesystem/streams.rs | 3 ++ .../filesystem/user_wrapper_controls.rs | 30 +++++++++++++++++++ .../tests/builtins_stream_wrappers.rs | 11 +++++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index bc8e18c33d..61cfe52340 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -469,6 +469,9 @@ pub(in crate::interpreter) fn eval_flock_result( ) -> Result<(bool, bool), EvalStatus> { let id = eval_stream_resource_id(stream, values)?; let operation = eval_int_value(operation, values)?; + if let Some(success) = eval_user_wrapper_flock_result(id, operation, context, values)? { + return Ok((success, false)); + } Ok(context .stream_resources() .flock(id, operation) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs index 648add1761..592cbc9d0f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_controls.rs @@ -82,6 +82,36 @@ pub(in crate::interpreter) fn eval_user_wrapper_ftruncate_result( values.bool_value(ok).map(Some) } +/// Dispatches `flock()` to a wrapper object's `stream_lock()`. +pub(in crate::interpreter) fn eval_user_wrapper_flock_result( + id: i64, + operation: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_lock)) = + eval_user_wrapper_method(&info.class_name, "stream_lock", context) + else { + return Ok(Some(false)); + }; + let operation = values.int(operation)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_lock, + info.object, + positional_args(vec![operation]), + context, + values, + )?; + let ok = values.truthy(result)?; + values.release(result)?; + Ok(Some(ok)) +} + /// Calls one no-argument userspace wrapper method on a stream resource. fn eval_user_wrapper_call_no_arg_method( id: i64, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs index c7372032a9..3ae271dc6f 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrappers.rs @@ -225,6 +225,10 @@ fn execute_program_dispatches_user_stream_wrapper_control_methods() { echo "F"; return true; } + public function stream_lock($operation): bool { + echo "L" . $operation; + return $operation !== 99; + } } stream_wrapper_register("ctrlw", "EvalControlWrapperW"); $h = fopen("ctrlw://one", "r+"); @@ -235,7 +239,10 @@ echo fseek($h, 1) === 0 ? "seek" : "bad"; echo ":"; echo ftell($h) === 1 ? "tell1" : "bad"; echo ":"; echo ftruncate($h, 3) ? "trunc" : "bad"; echo ":"; echo stream_get_contents($h) === "bc" ? "contents" : "bad"; echo ":"; -echo fflush($h) ? "flush" : "bad"; +echo fflush($h) ? "flush" : "bad"; echo ":"; +$lock_ok = flock($h, LOCK_EX, $would); +echo $lock_ok && $would === false ? "lock" : "bad"; echo ":"; +echo flock($h, 99) === false ? "lockfalse" : "bad"; return true;"#, ) .expect("parse eval fragment"); @@ -246,7 +253,7 @@ return true;"#, assert_eq!( values.output, - "Ttell0:read:Ttell2:S0seek:Ttell1:R3trunc:contents:Fflush" + "Ttell0:read:Ttell2:S0seek:Ttell1:R3trunc:contents:Fflush:L2lock:L99lockfalse" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } From fe7bc367dfbeda4d9c87bb0134e541f0013552b3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 21:16:58 +0200 Subject: [PATCH 0917/1208] Add magician wrapper stream_metadata dispatch --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/ops/path_bool.rs | 49 ++++++++++++++- .../builtins/filesystem/ops/touch.rs | 46 +++++++++++++- .../filesystem/user_wrapper_metadata.rs | 61 +++++++++++++++++++ .../builtins/registry/dispatch/filesystem.rs | 12 ++-- .../tests/builtins_stream_wrapper_metadata.rs | 48 +++++++++++++++ .../src/interpreter/tests/mod.rs | 1 + 7 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_metadata.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 25e5e3d838..6645ffbe58 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -22,6 +22,7 @@ mod stream_sockets; mod streams; mod user_wrapper_controls; mod user_wrapper_lines; +mod user_wrapper_metadata; mod user_wrapper_stat; mod user_wrapper_streams; @@ -39,5 +40,6 @@ pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_controls::*; pub(in crate::interpreter) use user_wrapper_lines::*; +pub(in crate::interpreter) use user_wrapper_metadata::*; pub(in crate::interpreter) use user_wrapper_stat::*; pub(in crate::interpreter) use user_wrapper_streams::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs index d8b3abc68e..9c6babda1d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs @@ -102,20 +102,31 @@ pub(in crate::interpreter) fn eval_builtin_chmod( }; let filename = eval_expr(filename, context, scope, values)?; let permissions = eval_expr(permissions, context, scope, values)?; - eval_chmod_result(filename, permissions, values) + eval_chmod_result(filename, permissions, context, values) } /// Changes one local file's mode and returns whether the operation succeeded. pub(in crate::interpreter) fn eval_chmod_result( filename: RuntimeCellHandle, permissions: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + let mode = eval_int_value(permissions, values)? as u32; + let metadata_value = values.int(i64::from(mode))?; + if let Some(result) = eval_user_wrapper_stream_metadata_result( + &path, + EVAL_STREAM_META_ACCESS, + metadata_value, + context, + values, + )? { + return Ok(result); + } let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.bool_value(false); }; - let mode = eval_int_value(permissions, values)? as u32; let permissions = std::fs::Permissions::from_mode(mode); values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) } @@ -133,7 +144,7 @@ pub(in crate::interpreter) fn eval_builtin_chown_like( }; let filename = eval_expr(filename, context, scope, values)?; let principal = eval_expr(principal, context, scope, values)?; - eval_chown_like_result(name, filename, principal, values) + eval_chown_like_result(name, filename, principal, context, values) } /// Changes one local path owner or group and returns whether the operation succeeded. @@ -141,9 +152,19 @@ pub(in crate::interpreter) fn eval_chown_like_result( name: &str, filename: RuntimeCellHandle, principal: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; + if matches!(name, "chown" | "chgrp") { + let (option, metadata_value) = + eval_chown_metadata_arg(name, principal, values)?; + if let Some(result) = + eval_user_wrapper_stream_metadata_result(&path, option, metadata_value, context, values)? + { + return Ok(result); + } + } let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.bool_value(false); }; @@ -163,6 +184,28 @@ pub(in crate::interpreter) fn eval_chown_like_result( values.bool_value(status == 0) } +/// Builds the wrapper metadata option and value for `chown()` or `chgrp()`. +fn eval_chown_metadata_arg( + name: &str, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(i64, RuntimeCellHandle), EvalStatus> { + match (name, values.type_tag(principal)?) { + ("chown", EVAL_TAG_INT) => { + let principal = eval_int_value(principal, values)?; + Ok((EVAL_STREAM_META_OWNER, values.int(principal)?)) + } + ("chgrp", EVAL_TAG_INT) => { + let principal = eval_int_value(principal, values)?; + Ok((EVAL_STREAM_META_GROUP, values.int(principal)?)) + } + ("chown", EVAL_TAG_STRING) => Ok((EVAL_STREAM_META_OWNER_NAME, principal)), + ("chgrp", EVAL_TAG_STRING) => Ok((EVAL_STREAM_META_GROUP_NAME, principal)), + ("chown" | "chgrp", _) => Err(EvalStatus::RuntimeFatal), + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Resolves one PHP owner/group argument into libc uid/gid slots. fn eval_chown_principal_ids( name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs index ab55f1a3f4..ca3a1cfde4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs @@ -11,6 +11,7 @@ use super::super::super::super::*; use super::super::super::*; use super::super::*; +use crate::stream_wrappers; /// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. pub(in crate::interpreter) fn eval_builtin_touch( @@ -22,18 +23,18 @@ pub(in crate::interpreter) fn eval_builtin_touch( match args { [filename] => { let filename = eval_expr(filename, context, scope, values)?; - eval_touch_result(filename, None, None, values) + eval_touch_result(filename, None, None, context, values) } [filename, mtime] => { let filename = eval_expr(filename, context, scope, values)?; let mtime = eval_expr(mtime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), None, values) + eval_touch_result(filename, Some(mtime), None, context, values) } [filename, mtime, atime] => { let filename = eval_expr(filename, context, scope, values)?; let mtime = eval_expr(mtime, context, scope, values)?; let atime = eval_expr(atime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), Some(atime), values) + eval_touch_result(filename, Some(mtime), Some(atime), context, values) } _ => Err(EvalStatus::RuntimeFatal), } @@ -44,10 +45,24 @@ pub(in crate::interpreter) fn eval_touch_result( filename: RuntimeCellHandle, mtime: Option, atime: Option, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; let (mtime, atime) = eval_touch_times(mtime, atime, values)?; + let metadata_value = eval_touch_metadata_value(mtime, atime, values)?; + if let Some(result) = eval_user_wrapper_stream_metadata_result( + &path, + EVAL_STREAM_META_TOUCH, + metadata_value, + context, + values, + )? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; let file = match std::fs::OpenOptions::new() .write(true) .create(true) @@ -62,6 +77,21 @@ pub(in crate::interpreter) fn eval_touch_result( values.bool_value(file.set_times(times).is_ok()) } +/// Builds the `[mtime, atime]` array passed to wrapper `stream_metadata()`. +fn eval_touch_metadata_value( + mtime: std::time::SystemTime, + atime: std::time::SystemTime, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(2)?; + let key = values.int(0)?; + let value = values.int(eval_system_time_to_unix(mtime).ok_or(EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + let key = values.int(1)?; + let value = values.int(eval_system_time_to_unix(atime).ok_or(EvalStatus::RuntimeFatal)?)?; + values.array_set(result, key, value) +} + /// Resolves PHP touch timestamp defaults into concrete system times. pub(in crate::interpreter) fn eval_touch_times( mtime: Option, @@ -103,3 +133,13 @@ pub(in crate::interpreter) fn eval_system_time_from_unix( std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) } } + +/// Converts a `SystemTime` back to whole Unix seconds for wrapper metadata. +fn eval_system_time_to_unix(time: std::time::SystemTime) -> Option { + match time.duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => i64::try_from(duration.as_secs()).ok(), + Err(error) => i64::try_from(error.duration().as_secs()) + .ok() + .map(|seconds| -seconds), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs new file mode 100644 index 0000000000..7034869bdf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Dispatches path-based metadata changes to eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` for `touch()`, `chmod()`, +//! `chown()`, and `chgrp()` when the path scheme is a registered wrapper. +//! +//! Key details: +//! - Mirrors the AOT stream-wrapper metadata options used by filesystem +//! builtins; non-wrapper paths return `None` so local host operations continue. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +pub(in crate::interpreter) const EVAL_STREAM_META_TOUCH: i64 = 1; +pub(in crate::interpreter) const EVAL_STREAM_META_OWNER_NAME: i64 = 2; +pub(in crate::interpreter) const EVAL_STREAM_META_OWNER: i64 = 3; +pub(in crate::interpreter) const EVAL_STREAM_META_GROUP_NAME: i64 = 4; +pub(in crate::interpreter) const EVAL_STREAM_META_GROUP: i64 = 5; +pub(in crate::interpreter) const EVAL_STREAM_META_ACCESS: i64 = 6; + +/// Dispatches one `stream_metadata($path, $option, $value)` wrapper call. +pub(in crate::interpreter) fn eval_user_wrapper_stream_metadata_result( + path: &str, + option: i64, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(path) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, stream_metadata)) = + eval_user_wrapper_method(class.name(), "stream_metadata", context) + else { + return values.bool_value(false).map(Some); + }; + let mut scope = ElephcEvalScope::new(); + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, &mut scope, values)?; + let path = values.string(path)?; + let option = values.int(option)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &stream_metadata, + object, + positional_args(vec![path, option, value]), + context, + values, + )?; + values.release(object)?; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index f487ff457b..38e3d7f960 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -29,13 +29,13 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [filename, permissions] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_chmod_result(*filename, *permissions, values)? + eval_chmod_result(*filename, *permissions, context, values)? } "chown" | "chgrp" | "lchown" | "lchgrp" => { let [filename, principal] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_chown_like_result(name, *filename, *principal, values)? + eval_chown_like_result(name, *filename, *principal, context, values)? } "closedir" | "readdir" | "rewinddir" => { let [dir_handle] = evaluated_args else { @@ -582,10 +582,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( eval_vfprintf_result(*stream, *format, *array, context, values)? } "touch" => match evaluated_args { - [filename] => eval_touch_result(*filename, None, None, values)?, - [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, values)?, + [filename] => eval_touch_result(*filename, None, None, context, values)?, + [filename, mtime] => { + eval_touch_result(*filename, Some(*mtime), None, context, values)? + } [filename, mtime, atime] => { - eval_touch_result(*filename, Some(*mtime), Some(*atime), values)? + eval_touch_result(*filename, Some(*mtime), Some(*atime), context, values)? } _ => return Err(EvalStatus::RuntimeFatal), }, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_metadata.rs new file mode 100644 index 0000000000..c92ba28df2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_metadata.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Interpreter tests for path metadata operations on eval userspace stream wrappers. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `stream_metadata()` option/value mapping for path-based +//! filesystem mutation builtins. + +use super::super::*; +use super::support::*; + +/// Verifies path metadata builtins dispatch to wrapper `stream_metadata()`. +#[test] +fn execute_program_dispatches_user_stream_wrapper_metadata() { + let program = parse_fragment( + br##"class EvalMetadataWrapperW { + public function stream_metadata($path, $option, $value): bool { + echo $path . "#" . $option . "#"; + if ($option === 1) { + echo $value[0] . "/" . $value[1]; + return true; + } + echo $value; + return true; + } +} +stream_wrapper_register("metaw", "EvalMetadataWrapperW"); +echo chmod("metaw://file", 384) ? "chmod" : "bad"; echo ":"; +echo touch("metaw://file", 100, 200) ? "touch" : "bad"; echo ":"; +echo chown("metaw://file", 501) ? "chown" : "bad"; echo ":"; +echo chgrp("metaw://file", "staff") ? "chgrp" : "bad"; echo ":"; +echo lchown("metaw://file", 501) === false ? "lskip" : "bad"; +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "metaw://file#6#384chmod:metaw://file#1#100/200touch:metaw://file#3#501chown:metaw://file#4#staffchgrp:lskip" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index fa0b88e06f..40635ab101 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -31,6 +31,7 @@ mod builtins_stream_contexts; mod builtins_stream_extensions; mod builtins_stream_settings; mod builtins_stream_sockets; +mod builtins_stream_wrapper_metadata; mod builtins_stream_wrappers; mod builtins_strings_binary; mod builtins_strings_encoding; From 7f1a77b436439662fefa75b2753a4647bea26019 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 21:25:27 +0200 Subject: [PATCH 0918/1208] Add magician wrapper path operations --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/ops/links.rs | 8 +- .../builtins/filesystem/ops/path_bool.rs | 18 ++- .../filesystem/user_wrapper_path_ops.rs | 106 ++++++++++++++++++ .../builtins/registry/dispatch/filesystem.rs | 6 +- .../tests/builtins_stream_wrapper_path_ops.rs | 56 +++++++++ .../src/interpreter/tests/mod.rs | 1 + 7 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_path_ops.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 6645ffbe58..3a59f745ba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -23,6 +23,7 @@ mod streams; mod user_wrapper_controls; mod user_wrapper_lines; mod user_wrapper_metadata; +mod user_wrapper_path_ops; mod user_wrapper_stat; mod user_wrapper_streams; @@ -41,5 +42,6 @@ pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_controls::*; pub(in crate::interpreter) use user_wrapper_lines::*; pub(in crate::interpreter) use user_wrapper_metadata::*; +pub(in crate::interpreter) use user_wrapper_path_ops::*; pub(in crate::interpreter) use user_wrapper_stat::*; pub(in crate::interpreter) use user_wrapper_streams::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs index cd1e9d0ffb..cf1e43088a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs @@ -98,18 +98,22 @@ pub(in crate::interpreter) fn eval_builtin_unlink( return Err(EvalStatus::RuntimeFatal); }; let filename = eval_expr(filename, context, scope, values)?; - eval_unlink_result(filename, values) + eval_unlink_result(filename, context, values) } -/// Deletes one local file and returns whether it succeeded. +/// Deletes one path and returns whether it succeeded. pub(in crate::interpreter) fn eval_unlink_result( filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(filename, values)?; if stream_wrappers::is_phar_stream(&path) { return values.bool_value(elephc_phar::delete_url_bytes(path.as_bytes()).is_some()); } + if let Some(result) = eval_user_wrapper_unlink_result(&path, context, values)? { + return Ok(result); + } let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.bool_value(false); }; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs index 9c6babda1d..83c0e29a94 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs @@ -27,16 +27,20 @@ pub(in crate::interpreter) fn eval_builtin_unary_path_bool( return Err(EvalStatus::RuntimeFatal); }; let path = eval_expr(path, context, scope, values)?; - eval_unary_path_bool_result(name, path, values) + eval_unary_path_bool_result(name, path, context, values) } -/// Executes a one-path local filesystem operation and returns whether it succeeded. +/// Executes a one-path filesystem operation and returns whether it succeeded. pub(in crate::interpreter) fn eval_unary_path_bool_result( name: &str, path: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let path = eval_path_string(path, values)?; + if let Some(result) = eval_user_wrapper_single_path_op_result(name, &path, context, values)? { + return Ok(result); + } let Some(path) = stream_wrappers::local_filesystem_path(&path) else { return values.bool_value(false); }; @@ -62,18 +66,24 @@ pub(in crate::interpreter) fn eval_builtin_binary_path_bool( }; let from = eval_expr(from, context, scope, values)?; let to = eval_expr(to, context, scope, values)?; - eval_binary_path_bool_result(name, from, to, values) + eval_binary_path_bool_result(name, from, to, context, values) } -/// Executes a two-path local filesystem operation and returns whether it succeeded. +/// Executes a two-path filesystem operation and returns whether it succeeded. pub(in crate::interpreter) fn eval_binary_path_bool_result( name: &str, from: RuntimeCellHandle, to: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let from = eval_path_string(from, values)?; let to = eval_path_string(to, values)?; + if name == "rename" { + if let Some(result) = eval_user_wrapper_rename_result(&from, &to, context, values)? { + return Ok(result); + } + } let Some(from) = stream_wrappers::local_filesystem_path(&from) else { return values.bool_value(false); }; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs new file mode 100644 index 0000000000..1d56282d41 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs @@ -0,0 +1,106 @@ +//! Purpose: +//! Dispatches path mutation builtins to eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::ops` for `unlink()`, `rename()`, +//! `mkdir()`, and `rmdir()` when the source path scheme is registered. +//! +//! Key details: +//! - Path methods use a throwaway wrapper instance, matching the generated +//! runtime's path-op helpers instead of reusing open stream resources. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +/// Dispatches `unlink($path)` to a wrapper object's `unlink()` method. +pub(in crate::interpreter) fn eval_user_wrapper_unlink_result( + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_user_wrapper_path_method_result(path, "unlink", context, values, |_| Ok(Vec::new())) +} + +/// Dispatches `mkdir($path)` or `rmdir($path)` to the registered wrapper. +pub(in crate::interpreter) fn eval_user_wrapper_single_path_op_result( + name: &str, + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match name { + "mkdir" => eval_user_wrapper_path_method_result(path, name, context, values, |values| { + Ok(vec![values.int(0)?, values.int(0)?]) + }), + "rmdir" => eval_user_wrapper_path_method_result(path, name, context, values, |values| { + Ok(vec![values.int(0)?]) + }), + _ => Ok(None), + } +} + +/// Dispatches `rename($from, $to)` using the source path's wrapper scheme. +pub(in crate::interpreter) fn eval_user_wrapper_rename_result( + from: &str, + to: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_user_wrapper_path_method_result(from, "rename", context, values, |values| { + Ok(vec![values.string(to)?]) + }) +} + +/// Instantiates the wrapper for one path and invokes a boolean path method. +fn eval_user_wrapper_path_method_result( + path: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut V, + extra_args: impl FnOnce(&mut V) -> Result, EvalStatus>, +) -> Result, EvalStatus> +where + V: RuntimeValueOps, +{ + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(path) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, method)) = + eval_user_wrapper_method(class.name(), method_name, context) + else { + return values.bool_value(false).map(Some); + }; + let mut scope = ElephcEvalScope::new(); + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, &mut scope, values)?; + let path = values.string(path)?; + let extra_args = match extra_args(values) { + Ok(args) => args, + Err(status) => { + values.release(object)?; + values.release(path)?; + return Err(status); + } + }; + let mut args = Vec::with_capacity(extra_args.len() + 1); + args.push(path); + args.extend(extra_args); + let result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &method, + object, + positional_args(args), + context, + values, + )?; + values.release(object)?; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 38e3d7f960..769c7136f3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -23,7 +23,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [path] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_unary_path_bool_result(name, *path, values)? + eval_unary_path_bool_result(name, *path, context, values)? } "chmod" => { let [filename, permissions] = evaluated_args else { @@ -53,7 +53,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [from, to] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_binary_path_bool_result(name, *from, *to, values)? + eval_binary_path_bool_result(name, *from, *to, context, values)? } "file" => { let [filename] = evaluated_args else { @@ -606,7 +606,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_unlink_result(*filename, values)? + eval_unlink_result(*filename, context, values)? } _ => return Ok(None), }; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_path_ops.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_path_ops.rs new file mode 100644 index 0000000000..58daf971cb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_path_ops.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Interpreter tests for userspace stream-wrapper filesystem path operations. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Path operations instantiate wrapper objects per operation, matching the +//! generated runtime path-op dispatch instead of open stream resource dispatch. + +use super::super::*; +use super::support::*; + +/// Verifies path mutation builtins dispatch to userspace stream-wrapper methods. +#[test] +fn execute_program_dispatches_user_stream_wrapper_path_ops() { + let program = parse_fragment( + br#"class EvalPathOpWrapperW { + public function unlink($path): bool { + echo "U(" . $path . ")"; + return $path === "pathop://delete-ok"; + } + public function mkdir($path, $mode, $options): bool { + echo "M(" . $path . "," . $mode . "," . $options . ")"; + return true; + } + public function rmdir($path, $options): bool { + echo "R(" . $path . "," . $options . ")"; + return false; + } + public function rename($from, $to): bool { + echo "N(" . $from . "," . $to . ")"; + return $to === "pathop://dest"; + } +} +stream_wrapper_register("pathop", "EvalPathOpWrapperW"); +echo unlink("pathop://delete-ok") ? "unlink" : "bad"; echo ":"; +echo call_user_func("unlink", "pathop://delete-no") === false ? "unlinkfalse" : "bad"; echo ":"; +echo mkdir("pathop://dir") ? "mkdir" : "bad"; echo ":"; +echo rmdir("pathop://dir") === false ? "rmdirfalse" : "bad"; echo ":"; +echo rename("pathop://source", "pathop://dest") ? "rename" : "bad"; echo ":"; +echo call_user_func("rename", "pathop://source2", "pathop://dest") ? "callrename" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "U(pathop://delete-ok)unlink:U(pathop://delete-no)unlinkfalse:M(pathop://dir,0,0)mkdir:R(pathop://dir,0)rmdirfalse:N(pathop://source,pathop://dest)rename:N(pathop://source2,pathop://dest)callrename" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index 40635ab101..78226a4ade 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -32,6 +32,7 @@ mod builtins_stream_extensions; mod builtins_stream_settings; mod builtins_stream_sockets; mod builtins_stream_wrapper_metadata; +mod builtins_stream_wrapper_path_ops; mod builtins_stream_wrappers; mod builtins_strings_binary; mod builtins_strings_encoding; From 4a1ede40aca231d8e93cda5d7d4ef9cc1230dba0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 21:31:30 +0200 Subject: [PATCH 0919/1208] Add magician wrapper directory dispatch --- .../builtins/filesystem/directories.rs | 22 ++- .../interpreter/builtins/filesystem/mod.rs | 2 + .../filesystem/user_wrapper_directories.rs | 146 ++++++++++++++++++ .../builtins_stream_wrapper_directories.rs | 71 +++++++++ .../src/interpreter/tests/mod.rs | 1 + .../elephc-magician/src/stream_resources.rs | 61 ++++++++ 6 files changed, 299 insertions(+), 4 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_directories.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs index 38522cfe95..d880b6fa06 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs @@ -33,6 +33,9 @@ pub(in crate::interpreter) fn eval_opendir_result( values: &mut impl RuntimeValueOps, ) -> Result { let directory = eval_path_string(directory, values)?; + if let Some(result) = eval_user_wrapper_opendir_result(&directory, context, values)? { + return Ok(result); + } match context.stream_resources_mut().open_directory(&directory) { Some(id) => values.resource(id), None => values.bool_value(false), @@ -64,14 +67,25 @@ pub(in crate::interpreter) fn eval_unary_directory_result( let id = eval_directory_resource_id(dir_handle, values)?; match name { "closedir" => { + if let Some(result) = eval_user_wrapper_closedir_result(id, context, values)? { + return Ok(result); + } context.stream_resources_mut().close_directory(id); values.null() } - "readdir" => match context.stream_resources_mut().read_directory(id) { - Some(name) => values.string(&name), - None => values.bool_value(false), - }, + "readdir" => { + if let Some(result) = eval_user_wrapper_readdir_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().read_directory(id) { + Some(name) => values.string(&name), + None => values.bool_value(false), + } + } "rewinddir" => { + if let Some(result) = eval_user_wrapper_rewinddir_result(id, context, values)? { + return Ok(result); + } context.stream_resources_mut().rewind_directory(id); values.null() } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 3a59f745ba..570848957e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -21,6 +21,7 @@ mod stream_settings; mod stream_sockets; mod streams; mod user_wrapper_controls; +mod user_wrapper_directories; mod user_wrapper_lines; mod user_wrapper_metadata; mod user_wrapper_path_ops; @@ -40,6 +41,7 @@ pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_controls::*; +pub(in crate::interpreter) use user_wrapper_directories::*; pub(in crate::interpreter) use user_wrapper_lines::*; pub(in crate::interpreter) use user_wrapper_metadata::*; pub(in crate::interpreter) use user_wrapper_path_ops::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs new file mode 100644 index 0000000000..bd4e2f05d6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs @@ -0,0 +1,146 @@ +//! Purpose: +//! Dispatches eval directory builtins to userspace stream-wrapper directory methods. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::directories` for `opendir()`, +//! `readdir()`, `rewinddir()`, and `closedir()`. +//! +//! Key details: +//! - `dir_opendir()` owns the wrapper object for the directory resource lifetime; +//! `dir_closedir()` releases it, while `readdir()`/`rewinddir()` reuse it. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +/// Dispatches `opendir($path)` to a wrapper object's `dir_opendir()` method. +pub(in crate::interpreter) fn eval_user_wrapper_opendir_result( + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(class_name) = context + .stream_resources() + .user_stream_wrapper_class_for_path(path) + else { + return Ok(None); + }; + let Some(class) = context.class(&class_name).cloned() else { + return values.bool_value(false).map(Some); + }; + let Some((declaring_class, dir_opendir)) = + eval_user_wrapper_method(class.name(), "dir_opendir", context) + else { + return values.bool_value(false).map(Some); + }; + let mut scope = ElephcEvalScope::new(); + let object = eval_dynamic_class_new_object(&class, Vec::new(), context, &mut scope, values)?; + let path_arg = values.string(path)?; + let options = values.int(0)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + class.name(), + &dir_opendir, + object, + positional_args(vec![path_arg, options]), + context, + values, + )?; + let opened = values.truthy(result)?; + values.release(result)?; + if !opened { + values.release(object)?; + return values.bool_value(false).map(Some); + } + let id = context + .stream_resources_mut() + .open_user_wrapper_directory(object, class.name()); + values.resource(id).map(Some) +} + +/// Dispatches `closedir($handle)` to a wrapper object's `dir_closedir()` method. +pub(in crate::interpreter) fn eval_user_wrapper_closedir_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_directory_info(id) else { + return Ok(None); + }; + if let Some((declaring_class, dir_closedir)) = + eval_user_wrapper_method(&info.class_name, "dir_closedir", context) + { + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &dir_closedir, + info.object, + Vec::new(), + context, + values, + )?; + values.release(result)?; + } + if let Some(info) = context + .stream_resources_mut() + .close_user_wrapper_directory(id) + { + values.release(info.object)?; + } + values.null().map(Some) +} + +/// Dispatches `readdir($handle)` to a wrapper object's `dir_readdir()` method. +pub(in crate::interpreter) fn eval_user_wrapper_readdir_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_directory_info(id) else { + return Ok(None); + }; + let Some((declaring_class, dir_readdir)) = + eval_user_wrapper_method(&info.class_name, "dir_readdir", context) + else { + return values.bool_value(false).map(Some); + }; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &dir_readdir, + info.object, + Vec::new(), + context, + values, + )?; + if values.type_tag(result)? == EVAL_TAG_STRING && values.string_bytes(result)?.is_empty() { + values.release(result)?; + return values.bool_value(false).map(Some); + } + Ok(Some(result)) +} + +/// Dispatches `rewinddir($handle)` to a wrapper object's `dir_rewinddir()` method. +pub(in crate::interpreter) fn eval_user_wrapper_rewinddir_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_directory_info(id) else { + return Ok(None); + }; + if let Some((declaring_class, dir_rewinddir)) = + eval_user_wrapper_method(&info.class_name, "dir_rewinddir", context) + { + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &dir_rewinddir, + info.object, + Vec::new(), + context, + values, + )?; + values.release(result)?; + } + values.null().map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_directories.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_directories.rs new file mode 100644 index 0000000000..21c0b710d5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_directories.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Interpreter tests for userspace stream-wrapper directory handles. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Wrapper directory resources hold the wrapper object across read, rewind, +//! and close calls so cursor state stays on the userspace instance. + +use super::super::*; +use super::support::*; + +/// Verifies directory stream builtins dispatch to userspace wrapper methods. +#[test] +fn execute_program_dispatches_user_stream_wrapper_directories() { + let program = parse_fragment( + br#"class EvalDirWrapperW { + public $entries; + public $pos; + public function dir_opendir($path, $options): bool { + echo "O(" . $path . "," . $options . ")"; + $this->entries = ["one", "two"]; + $this->pos = 0; + return $path === "dirw://ok"; + } + public function dir_readdir(): string { + if ($this->pos >= count($this->entries)) { + return ""; + } + $entry = $this->entries[$this->pos]; + $this->pos += 1; + return $entry; + } + public function dir_rewinddir(): bool { + echo "W"; + $this->pos = 0; + return true; + } + public function dir_closedir(): bool { + echo "C"; + return true; + } +} +stream_wrapper_register("dirw", "EvalDirWrapperW"); +$dh = opendir("dirw://ok"); +echo is_resource($dh) ? "open" : "bad"; echo ":"; +echo readdir($dh) === "one" ? "one" : "bad"; echo ":"; +echo readdir($dh) === "two" ? "two" : "bad"; echo ":"; +echo readdir($dh) === false ? "eof" : "bad"; echo ":"; +rewinddir($dh); +echo readdir($dh) === "one" ? "rewind" : "bad"; echo ":"; +call_user_func("rewinddir", $dh); +echo call_user_func("readdir", $dh) === "one" ? "callread" : "bad"; echo ":"; +call_user_func("closedir", $dh); +echo readdir($dh) === false ? "closed" : "bad"; echo ":"; +echo opendir("dirw://bad") === false ? "openfalse" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "O(dirw://ok,0)open:one:two:eof:Wrewind:Wcallread:Cclosed:O(dirw://bad,0)openfalse" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index 78226a4ade..6c094d4148 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -31,6 +31,7 @@ mod builtins_stream_contexts; mod builtins_stream_extensions; mod builtins_stream_settings; mod builtins_stream_sockets; +mod builtins_stream_wrapper_directories; mod builtins_stream_wrapper_metadata; mod builtins_stream_wrapper_path_ops; mod builtins_stream_wrappers; diff --git a/crates/elephc-magician/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs index 78b78dd4a8..8a4e2dc2d9 100644 --- a/crates/elephc-magician/src/stream_resources.rs +++ b/crates/elephc-magician/src/stream_resources.rs @@ -42,6 +42,7 @@ pub(crate) struct EvalStreamResources { streams: HashMap, user_stream_wrapper_classes: HashMap, user_stream_wrappers: Vec, + user_wrapper_directories: HashMap, user_wrapper_streams: HashMap, } @@ -340,6 +341,24 @@ impl EvalStreamResources { id } + /// Opens a userspace wrapper directory around an eval-created wrapper object. + pub(crate) fn open_user_wrapper_directory( + &mut self, + object: RuntimeCellHandle, + class_name: &str, + ) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.user_wrapper_directories.insert( + id, + EvalUserWrapperDirectory { + object, + class_name: class_name.to_string(), + }, + ); + id + } + /// Returns a copied userspace-wrapper stream descriptor for dispatch. pub(crate) fn user_wrapper_stream_info( &self, @@ -350,6 +369,26 @@ impl EvalStreamResources { .map(EvalUserWrapperStream::info) } + /// Returns a copied userspace-wrapper directory descriptor for dispatch. + pub(crate) fn user_wrapper_directory_info( + &self, + id: i64, + ) -> Option { + self.user_wrapper_directories + .get(&id) + .map(EvalUserWrapperDirectory::info) + } + + /// Removes a userspace-wrapper directory and returns its descriptor. + pub(crate) fn close_user_wrapper_directory( + &mut self, + id: i64, + ) -> Option { + self.user_wrapper_directories + .remove(&id) + .map(|directory| directory.info()) + } + /// Updates the cached EOF state for a userspace-wrapper stream. pub(crate) fn set_user_wrapper_eof(&mut self, id: i64, eof: bool) -> bool { let Some(stream) = self.user_wrapper_streams.get_mut(&id) else { @@ -1014,6 +1053,28 @@ pub(crate) struct EvalUserWrapperStreamInfo { pub(crate) eof: bool, } +/// Userspace-wrapper directory stored behind one eval resource id. +struct EvalUserWrapperDirectory { + object: RuntimeCellHandle, + class_name: String, +} + +impl EvalUserWrapperDirectory { + /// Copies the dispatch fields needed while invoking wrapper directory methods. + fn info(&self) -> EvalUserWrapperDirectoryInfo { + EvalUserWrapperDirectoryInfo { + object: self.object, + class_name: self.class_name.clone(), + } + } +} + +/// Copied userspace-wrapper directory fields used while dispatching PHP methods. +pub(crate) struct EvalUserWrapperDirectoryInfo { + pub(crate) object: RuntimeCellHandle, + pub(crate) class_name: String, +} + /// Wrapper targets that need a write-back step when their stream closes. enum EvalStreamFlushTarget { PharUrl(Vec), From 95f0f439959e4abeae72491d88ed01a5b8c63f73 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 21:35:45 +0200 Subject: [PATCH 0920/1208] Add magician wrapper stream option dispatch --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/stream_settings.rs | 20 +++++++ .../filesystem/user_wrapper_options.rs | 50 ++++++++++++++++++ .../tests/builtins_stream_wrapper_options.rs | 52 +++++++++++++++++++ .../src/interpreter/tests/mod.rs | 1 + 5 files changed, 125 insertions(+) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_options.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 570848957e..6362f539b9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -24,6 +24,7 @@ mod user_wrapper_controls; mod user_wrapper_directories; mod user_wrapper_lines; mod user_wrapper_metadata; +mod user_wrapper_options; mod user_wrapper_path_ops; mod user_wrapper_stat; mod user_wrapper_streams; @@ -44,6 +45,7 @@ pub(in crate::interpreter) use user_wrapper_controls::*; pub(in crate::interpreter) use user_wrapper_directories::*; pub(in crate::interpreter) use user_wrapper_lines::*; pub(in crate::interpreter) use user_wrapper_metadata::*; +pub(in crate::interpreter) use user_wrapper_options::*; pub(in crate::interpreter) use user_wrapper_path_ops::*; pub(in crate::interpreter) use user_wrapper_stat::*; pub(in crate::interpreter) use user_wrapper_streams::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs index 693645a600..756745314e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs @@ -60,6 +60,16 @@ pub(in crate::interpreter) fn eval_stream_set_blocking_result( ) -> Result { let id = eval_settings_stream_resource_id(stream, values)?; let enable = values.truthy(enable)?; + if let Some(result) = eval_user_wrapper_stream_set_option_result( + id, + EVAL_STREAM_OPTION_BLOCKING, + if enable { 1 } else { 0 }, + 0, + context, + values, + )? { + return Ok(result); + } values.bool_value(context.stream_resources().set_blocking(id, enable).unwrap_or(false)) } @@ -96,6 +106,16 @@ pub(in crate::interpreter) fn eval_stream_set_timeout_result( Some(microseconds) => eval_int_value(microseconds, values)?, None => 0, }; + if let Some(result) = eval_user_wrapper_stream_set_option_result( + id, + EVAL_STREAM_OPTION_READ_TIMEOUT, + seconds, + microseconds, + context, + values, + )? { + return Ok(result); + } values.bool_value( context .stream_resources() diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs new file mode 100644 index 0000000000..d6706b0218 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Dispatches stream option builtins to eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_settings` for +//! `stream_set_blocking()` and `stream_set_timeout()`. +//! +//! Key details: +//! - Mirrors the generated runtime's `stream_set_option($option, $arg1, $arg2)` +//! dispatch for synthetic wrapper descriptors. + +use super::super::super::*; +use super::user_wrapper_streams::eval_user_wrapper_method; + +pub(in crate::interpreter) const EVAL_STREAM_OPTION_BLOCKING: i64 = 1; +pub(in crate::interpreter) const EVAL_STREAM_OPTION_READ_TIMEOUT: i64 = 4; + +/// Dispatches a stream option update to a wrapper object's `stream_set_option()`. +pub(in crate::interpreter) fn eval_user_wrapper_stream_set_option_result( + id: i64, + option: i64, + arg1: i64, + arg2: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_set_option)) = + eval_user_wrapper_method(&info.class_name, "stream_set_option", context) + else { + return values.bool_value(false).map(Some); + }; + let option = values.int(option)?; + let arg1 = values.int(arg1)?; + let arg2 = values.int(arg2)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_set_option, + info.object, + positional_args(vec![option, arg1, arg2]), + context, + values, + )?; + let ok = values.truthy(result)?; + values.release(result)?; + values.bool_value(ok).map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_options.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_options.rs new file mode 100644 index 0000000000..ffecc8396c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_options.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Interpreter tests for userspace stream-wrapper option dispatch. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - `stream_set_blocking()` and `stream_set_timeout()` map to +//! `stream_set_option($option, $arg1, $arg2)` on wrapper streams. + +use super::super::*; +use super::support::*; + +/// Verifies stream setting builtins dispatch to wrapper `stream_set_option()`. +#[test] +fn execute_program_dispatches_user_stream_wrapper_options() { + let program = parse_fragment( + br#"class EvalOptionWrapperW { + public function stream_open($path, $mode, $options, &$opened_path): bool { + return true; + } + public function stream_set_option($option, $arg1, $arg2): bool { + echo "O(" . $option . "," . $arg1 . "," . $arg2 . ")"; + if ($option === 1) { + return $arg1 === 1; + } + if ($option === 4) { + return $arg2 === 7; + } + return false; + } +} +stream_wrapper_register("optw", "EvalOptionWrapperW"); +$h = fopen("optw://one", "r"); +echo stream_set_blocking($h, true) ? "block" : "bad"; echo ":"; +echo stream_set_blocking($h, false) === false ? "nonblockfalse" : "bad"; echo ":"; +echo stream_set_timeout($h, 3, 7) ? "timeout" : "bad"; echo ":"; +echo call_user_func("stream_set_timeout", $h, 5) === false ? "calltimeoutfalse" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "O(1,1,0)block:O(1,0,0)nonblockfalse:O(4,3,7)timeout:O(4,5,0)calltimeoutfalse" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index 6c094d4148..a47f7b6b8e 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -33,6 +33,7 @@ mod builtins_stream_settings; mod builtins_stream_sockets; mod builtins_stream_wrapper_directories; mod builtins_stream_wrapper_metadata; +mod builtins_stream_wrapper_options; mod builtins_stream_wrapper_path_ops; mod builtins_stream_wrappers; mod builtins_strings_binary; From 21c7ff300ba8740addbbcb8fe94056e22ace2528 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 21:46:10 +0200 Subject: [PATCH 0921/1208] Add magician wrapper one-shot file IO --- .../builtins/filesystem/file_contents.rs | 211 ++++++++++++++++++ .../builtins/filesystem/file_io.rs | 172 -------------- .../interpreter/builtins/filesystem/mod.rs | 4 + .../filesystem/user_wrapper_file_io.rs | 129 +++++++++++ .../builtins/registry/dispatch/filesystem.rs | 8 +- .../tests/builtins_stream_wrapper_file_io.rs | 62 +++++ .../src/interpreter/tests/mod.rs | 1 + 7 files changed, 411 insertions(+), 176 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/file_contents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_file_io.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_contents.rs new file mode 100644 index 0000000000..31d29a12dd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_contents.rs @@ -0,0 +1,211 @@ +//! Purpose: +//! Implements one-shot file content builtins for eval. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem` re-exports. +//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! +//! Key details: +//! - Local paths, built-in URL wrappers, PHAR URLs, and userspace stream wrappers +//! share these entry points for content reads and writes. + +use super::super::super::*; +use super::*; +use crate::stream_wrappers; + +/// Evaluates PHP `file_get_contents($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_get_contents_result(filename, context, values) +} + +/// Reads a local file or supported wrapper into a PHP string, or returns false on failure. +pub(in crate::interpreter) fn eval_file_get_contents_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { + return Ok(result); + } + match eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => values.string_bytes_value(&bytes), + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} + +/// Evaluates PHP `file($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_result(filename, context, values) +} + +/// Reads one local file or supported wrapper and returns indexed line byte strings. +pub(in crate::interpreter) fn eval_file_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { + if values.type_tag(result)? == EVAL_TAG_STRING { + let bytes = values.string_bytes(result)?; + return eval_file_lines_array(&bytes, values); + } + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + let bytes = match eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => bytes, + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + }; + eval_file_lines_array(&bytes, values) +} + +/// Splits file payload bytes into runtime array entries, preserving trailing newlines. +fn eval_file_lines_array( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(0)?; + let mut line_start = 0; + let mut line_index = 0; + for (index, byte) in bytes.iter().enumerate() { + if *byte != b'\n' { + continue; + } + result = + eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; + line_start = index + 1; + line_index += 1; + } + if line_start < bytes.len() { + result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; + } + Ok(result) +} + +/// Evaluates PHP `readfile($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_readfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_readfile_result(filename, context, values) +} + +/// Streams one local file or supported wrapper to eval output. +pub(in crate::interpreter) fn eval_readfile_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_readfile_result(&path, context, values)? { + return Ok(result); + } + if let Some(local_path) = stream_wrappers::local_filesystem_path(&path) { + let path = std::path::Path::new(&local_path); + if path.is_dir() { + return values.int(-1); + } + } + let bytes = match eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => bytes, + Err(_) => return values.bool_value(false), + }; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file_put_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_file_put_contents_result(filename, data, context, values) +} + +/// Writes a PHP string to a local file or supported wrapper and returns a byte count. +pub(in crate::interpreter) fn eval_file_put_contents_result( + filename: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let data = values.string_bytes(data)?; + if stream_wrappers::is_phar_stream(&path) { + return match elephc_phar::put_url_bytes(path.as_bytes(), &data) { + Some(len) => values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + }; + } + if let Some(result) = + eval_user_wrapper_file_put_contents_result(&path, &data, context, values)? + { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + match std::fs::write(path, &data) { + Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), + Err(_) => values.bool_value(false), + } +} + +/// Reads bytes from supported direct path or stream-wrapper URLs. +pub(in crate::interpreter) fn eval_read_path_or_wrapper_bytes( + path: &str, +) -> Result, ()> { + if stream_wrappers::is_data_stream(path) { + return stream_wrappers::decode_data_uri(path).ok_or(()); + } + if stream_wrappers::is_phar_stream(path) { + return elephc_phar::extract_url_bytes(path.as_bytes()).ok_or(()); + } + if stream_wrappers::is_http_stream(path) { + return stream_wrappers::read_http_url(path).ok_or(()); + } + let Some(path) = stream_wrappers::local_filesystem_path(path) else { + return Err(()); + }; + std::fs::read(path).map_err(|_| ()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs index 152686e8c8..9d04ab78b9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs @@ -131,161 +131,6 @@ pub(in crate::interpreter) fn eval_file_stat_scalar_result( } } -/// Evaluates PHP `file_get_contents($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_file_get_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_get_contents_result(filename, values) -} - -/// Reads a local file or supported wrapper into a PHP string, or returns false on failure. -pub(in crate::interpreter) fn eval_file_get_contents_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - match eval_read_path_or_wrapper_bytes(&path) { - Ok(bytes) => values.string_bytes_value(&bytes), - Err(_) => { - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - values.bool_value(false) - } - } -} - -/// Evaluates PHP `file($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_file( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_result(filename, values) -} - -/// Reads one local file or supported wrapper and returns indexed line byte strings. -pub(in crate::interpreter) fn eval_file_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let bytes = match eval_read_path_or_wrapper_bytes(&path) { - Ok(bytes) => bytes, - Err(_) => { - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - return values.array_new(0); - } - }; - eval_file_lines_array(&bytes, values) -} - -/// Splits file payload bytes into runtime array entries, preserving trailing newlines. -pub(in crate::interpreter) fn eval_file_lines_array( - bytes: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(0)?; - let mut line_start = 0; - let mut line_index = 0; - for (index, byte) in bytes.iter().enumerate() { - if *byte != b'\n' { - continue; - } - result = - eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; - line_start = index + 1; - line_index += 1; - } - if line_start < bytes.len() { - result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; - } - Ok(result) -} - -/// Evaluates PHP `readfile($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_readfile( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_readfile_result(filename, values) -} - -/// Streams one local file or supported wrapper to eval output. -pub(in crate::interpreter) fn eval_readfile_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(local_path) = stream_wrappers::local_filesystem_path(&path) { - let path = std::path::Path::new(&local_path); - if path.is_dir() { - return values.int(-1); - } - } - let bytes = match eval_read_path_or_wrapper_bytes(&path) { - Ok(bytes) => bytes, - Err(_) => return values.bool_value(false), - }; - let output = values.string_bytes_value(&bytes)?; - values.echo(output)?; - values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_file_put_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, data] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let data = eval_expr(data, context, scope, values)?; - eval_file_put_contents_result(filename, data, values) -} - -/// Writes a PHP string to a local file or supported wrapper and returns a byte count. -pub(in crate::interpreter) fn eval_file_put_contents_result( - filename: RuntimeCellHandle, - data: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let data = values.string_bytes(data)?; - if stream_wrappers::is_phar_stream(&path) { - return match elephc_phar::put_url_bytes(path.as_bytes(), &data) { - Some(len) => values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?), - None => values.bool_value(false), - }; - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - match std::fs::write(path, &data) { - Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), - Err(_) => values.bool_value(false), - } -} - /// Evaluates PHP `filesize($filename)` over one eval expression. pub(in crate::interpreter) fn eval_builtin_filesize( args: &[EvalExpr], @@ -481,20 +326,3 @@ pub(in crate::interpreter) fn eval_stat_array_set_string_key( pub(in crate::interpreter) fn eval_u64_to_i64(value: u64) -> Result { i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) } - -/// Reads bytes from supported direct path or stream-wrapper URLs. -fn eval_read_path_or_wrapper_bytes(path: &str) -> Result, ()> { - if stream_wrappers::is_data_stream(path) { - return stream_wrappers::decode_data_uri(path).ok_or(()); - } - if stream_wrappers::is_phar_stream(path) { - return elephc_phar::extract_url_bytes(path.as_bytes()).ok_or(()); - } - if stream_wrappers::is_http_stream(path) { - return stream_wrappers::read_http_url(path).ok_or(()); - } - let Some(path) = stream_wrappers::local_filesystem_path(path) else { - return Err(()); - }; - std::fs::read(path).map_err(|_| ()) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 6362f539b9..ff4f6984b8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -9,6 +9,7 @@ //! host filesystem. mod directories; +mod file_contents; mod file_io; mod fnmatch; mod ops; @@ -22,6 +23,7 @@ mod stream_sockets; mod streams; mod user_wrapper_controls; mod user_wrapper_directories; +mod user_wrapper_file_io; mod user_wrapper_lines; mod user_wrapper_metadata; mod user_wrapper_options; @@ -30,6 +32,7 @@ mod user_wrapper_stat; mod user_wrapper_streams; pub(in crate::interpreter) use directories::*; +pub(in crate::interpreter) use file_contents::*; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use ops::*; @@ -43,6 +46,7 @@ pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_controls::*; pub(in crate::interpreter) use user_wrapper_directories::*; +pub(in crate::interpreter) use user_wrapper_file_io::*; pub(in crate::interpreter) use user_wrapper_lines::*; pub(in crate::interpreter) use user_wrapper_metadata::*; pub(in crate::interpreter) use user_wrapper_options::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs new file mode 100644 index 0000000000..72aa4fcfe0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs @@ -0,0 +1,129 @@ +//! Purpose: +//! Dispatches one-shot file I/O builtins through eval userspace stream wrappers. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::file_io` for `file()`, +//! `file_get_contents()`, `readfile()`, and `file_put_contents()`. +//! +//! Key details: +//! - These helpers open a temporary wrapper stream, perform the requested read or +//! write through stream methods, and close the wrapper resource before returning. + +use super::super::super::*; + +/// Reads a full userspace-wrapper path into a PHP string cell. +pub(in crate::interpreter) fn eval_user_wrapper_file_get_contents_result( + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((id, opened)) = eval_user_wrapper_open_file_path(path, "r", context, values)? else { + return Ok(None); + }; + if values.type_tag(opened)? != EVAL_TAG_RESOURCE { + return Ok(Some(opened)); + } + let result = match eval_user_wrapper_stream_get_contents_result(id, None, None, context, values)? + { + Some(result) => result, + None => values.bool_value(false)?, + }; + eval_user_wrapper_close_one_shot(id, context, values)?; + Ok(Some(result)) +} + +/// Streams one userspace-wrapper path to eval output and returns the byte count. +pub(in crate::interpreter) fn eval_user_wrapper_readfile_result( + path: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((id, opened)) = eval_user_wrapper_open_file_path(path, "r", context, values)? else { + return Ok(None); + }; + if values.type_tag(opened)? != EVAL_TAG_RESOURCE { + return Ok(Some(opened)); + } + let result = match eval_user_wrapper_stream_get_contents_result(id, None, None, context, values)? + { + Some(result) => result, + None => values.bool_value(false)?, + }; + if values.type_tag(result)? != EVAL_TAG_STRING { + eval_user_wrapper_close_one_shot(id, context, values)?; + return Ok(Some(result)); + } + let bytes = values.string_bytes(result)?; + values.echo(result)?; + eval_user_wrapper_close_one_shot(id, context, values)?; + values + .int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) + .map(Some) +} + +/// Writes bytes to one userspace-wrapper path and returns the wrapper byte count. +pub(in crate::interpreter) fn eval_user_wrapper_file_put_contents_result( + path: &str, + data: &[u8], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((id, opened)) = eval_user_wrapper_open_file_path(path, "w", context, values)? else { + return Ok(None); + }; + if values.type_tag(opened)? != EVAL_TAG_RESOURCE { + return Ok(Some(opened)); + } + let result = match eval_user_wrapper_fwrite_result(id, data, context, values)? { + Some(result) => result, + None => values.bool_value(false)?, + }; + eval_user_wrapper_close_one_shot(id, context, values)?; + Ok(Some(result)) +} + +/// Opens one userspace-wrapper file path and returns its resource id and cell. +fn eval_user_wrapper_open_file_path( + path: &str, + mode: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut scope = ElephcEvalScope::new(); + let Some(opened) = eval_user_wrapper_fopen_result(path, mode, context, &mut scope, values)? + else { + return Ok(None); + }; + if values.type_tag(opened)? != EVAL_TAG_RESOURCE { + return Ok(Some((-1, opened))); + } + let id = eval_user_wrapper_file_resource_id(opened, values)?; + Ok(Some((id, opened))) +} + +/// Closes a one-shot wrapper stream and releases the close result cell. +fn eval_user_wrapper_close_one_shot( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if id < 0 { + return Ok(()); + } + if let Some(closed) = eval_user_wrapper_fclose_result(id, context, values)? { + values.release(closed)?; + } + Ok(()) +} + +/// Converts a PHP resource cell into eval's zero-based stream resource id. +fn eval_user_wrapper_file_resource_id( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 769c7136f3..ff87d3ffee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -59,7 +59,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_file_result(*filename, values)? + eval_file_result(*filename, context, values)? } "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => { @@ -79,13 +79,13 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_file_get_contents_result(*filename, values)? + eval_file_get_contents_result(*filename, context, values)? } "file_put_contents" => { let [filename, data] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_file_put_contents_result(*filename, *data, values)? + eval_file_put_contents_result(*filename, *data, context, values)? } "fclose" | "fgetc" @@ -228,7 +228,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_readfile_result(*filename, values)? + eval_readfile_result(*filename, context, values)? } "readline" => { if evaluated_args.len() > 1 { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_file_io.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_file_io.rs new file mode 100644 index 0000000000..d1b6d1555f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_file_io.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Interpreter tests for one-shot file I/O through userspace stream wrappers. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - `file_get_contents()`, `file()`, `readfile()`, and `file_put_contents()` +//! should use `stream_open()` plus stream read/write/close methods. + +use super::super::*; +use super::support::*; + +/// Verifies one-shot file I/O builtins dispatch through userspace stream wrappers. +#[test] +fn execute_program_dispatches_user_stream_wrapper_file_io() { + let program = parse_fragment( + br#"class EvalFileIoWrapperW { + public $data; + public $pos; + public function stream_open($path, $mode, $options, &$opened_path): bool { + echo "O(" . $mode . ")"; + $this->data = "aa\nbb"; + $this->pos = 0; + return true; + } + public function stream_read($count): string { + $chunk = substr($this->data, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_write($data): int { + echo "W(" . $data . ")"; + return strlen($data); + } + public function stream_eof(): bool { + return $this->pos >= strlen($this->data); + } + public function stream_close(): void { + echo "C"; + } +} +stream_wrapper_register("fio", "EvalFileIoWrapperW"); +echo file_get_contents("fio://read") === "aa\nbb" ? "fgc" : "bad"; echo ":"; +$lines = file("fio://read"); +echo count($lines) === 2 && $lines[0] === "aa\n" && $lines[1] === "bb" ? "file" : "bad"; echo ":"; +echo readfile("fio://read") === 5 ? "readfile" : "bad"; echo ":"; +echo file_put_contents("fio://write", "xyz") === 3 ? "put" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "O(r)Cfgc:O(r)Cfile:O(r)aa\nbbCreadfile:O(w)W(xyz)Cput" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index a47f7b6b8e..8566b00ef6 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -32,6 +32,7 @@ mod builtins_stream_extensions; mod builtins_stream_settings; mod builtins_stream_sockets; mod builtins_stream_wrapper_directories; +mod builtins_stream_wrapper_file_io; mod builtins_stream_wrapper_metadata; mod builtins_stream_wrapper_options; mod builtins_stream_wrapper_path_ops; From c1d6121c9bbc6c56cf01730f045201aee626cf13 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 21:56:22 +0200 Subject: [PATCH 0922/1208] Add magician wrapper stream cast dispatch --- .../interpreter/builtins/filesystem/mod.rs | 2 + .../builtins/filesystem/stream_sockets.rs | 48 ++++++++++++++++++- .../builtins/filesystem/user_wrapper_cast.rs | 42 ++++++++++++++++ .../builtins/registry/dispatch/filesystem.rs | 2 +- .../tests/builtins_stream_wrapper_cast.rs | 46 ++++++++++++++++++ .../src/interpreter/tests/mod.rs | 1 + 6 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_cast.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index ff4f6984b8..a18ff9477d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -21,6 +21,7 @@ mod stream_context; mod stream_settings; mod stream_sockets; mod streams; +mod user_wrapper_cast; mod user_wrapper_controls; mod user_wrapper_directories; mod user_wrapper_file_io; @@ -44,6 +45,7 @@ pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; +pub(in crate::interpreter) use user_wrapper_cast::*; pub(in crate::interpreter) use user_wrapper_controls::*; pub(in crate::interpreter) use user_wrapper_directories::*; pub(in crate::interpreter) use user_wrapper_file_io::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs index f969c8edc3..81e20eaef0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs @@ -328,23 +328,67 @@ pub(in crate::interpreter) fn eval_builtin_stream_select( if !(4..=5).contains(&args.len()) { return Err(EvalStatus::RuntimeFatal); } + let mut evaluated_args = Vec::with_capacity(args.len()); for arg in args { - let _ = eval_expr(arg, context, scope, values)?; + evaluated_args.push(eval_expr(arg, context, scope, values)?); } - values.int(0) + eval_stream_select_result(&evaluated_args, context, values) } /// Evaluates materialized `stream_select(...)` arguments. pub(in crate::interpreter) fn eval_stream_select_result( evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if !(4..=5).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); } + for array in evaluated_args.iter().take(3) { + eval_stream_select_cast_array(*array, context, values)?; + } values.int(0) } +/// Invokes `stream_cast(STREAM_CAST_FOR_SELECT)` for wrapper resources in an array. +fn eval_stream_select_cast_array( + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.is_array_like(array)? { + return Ok(()); + } + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + eval_stream_select_cast_value(value, context, values)?; + } + Ok(()) +} + +/// Invokes `stream_cast()` for one userspace-wrapper stream resource value. +fn eval_stream_select_cast_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_RESOURCE { + return Ok(()); + } + let display_id = eval_int_value(value, values)?; + let Some(id) = display_id.checked_sub(1) else { + return Ok(()); + }; + let Some(result) = + eval_user_wrapper_stream_cast_result(id, EVAL_STREAM_CAST_FOR_SELECT, context, values)? + else { + return Ok(()); + }; + values.release(result) +} + /// Converts a runtime resource cell into eval's zero-based socket id. fn eval_socket_resource_id( resource: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs new file mode 100644 index 0000000000..c34ef5d1ed --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Dispatches eval userspace stream wrappers to `stream_cast()`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_sockets`. +//! +//! Key details: +//! - Magician keeps `stream_select()` conservative and returns no ready streams, +//! but wrapper `stream_cast(STREAM_CAST_FOR_SELECT)` is still PHP-observable. + +use super::super::super::*; + +/// PHP's `STREAM_CAST_FOR_SELECT` value passed to wrapper `stream_cast()`. +pub(in crate::interpreter) const EVAL_STREAM_CAST_FOR_SELECT: i64 = 3; + +/// Invokes `stream_cast($cast_as)` for a userspace-wrapper stream resource. +pub(in crate::interpreter) fn eval_user_wrapper_stream_cast_result( + id: i64, + cast_as: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(info) = context.stream_resources().user_wrapper_stream_info(id) else { + return Ok(None); + }; + let Some((declaring_class, stream_cast)) = + eval_user_wrapper_method(&info.class_name, "stream_cast", context) + else { + return Ok(None); + }; + let cast_as = values.int(cast_as)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + &info.class_name, + &stream_cast, + info.object, + positional_args(vec![cast_as]), + context, + values, + )?; + Ok(Some(result)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index ff87d3ffee..a350d01b4f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -426,7 +426,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_stream_bucket_push_result(name, *brigade, *bucket, values)? } - "stream_select" => eval_stream_select_result(evaluated_args, values)?, + "stream_select" => eval_stream_select_result(evaluated_args, context, values)?, "stream_socket_server" => { let [address] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_cast.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_cast.rs new file mode 100644 index 0000000000..741b72f78c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_wrapper_cast.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Interpreter tests for userspace stream-wrapper `stream_cast()` dispatch. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Magician keeps `stream_select()` conservative, but still invokes +//! `stream_cast(STREAM_CAST_FOR_SELECT)` for PHP-observable wrapper effects. + +use super::super::*; +use super::support::*; + +/// Verifies `stream_select()` invokes wrapper `stream_cast()` for each stream array. +#[test] +fn execute_program_dispatches_user_stream_wrapper_cast_for_select() { + let program = parse_fragment( + br#"class EvalCastWrapperW { + public function stream_open($path, $mode, $options, &$opened_path): bool { + return true; + } + public function stream_cast($cast_as) { + echo "cast(" . $cast_as . ")"; + return false; + } +} +stream_wrapper_register("castw", "EvalCastWrapperW"); +$h = fopen("castw://one", "r"); +$read = [$h]; $write = []; $except = []; +echo stream_select($read, $write, $except, 0) === 0 ? "select" : "bad"; echo ":"; +$read = []; $write = [$h]; $except = [$h]; +echo stream_select($read, $write, $except, 0, 0) === 0 ? "select2" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "cast(3)select:cast(3)cast(3)select2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index 8566b00ef6..7723b50491 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -31,6 +31,7 @@ mod builtins_stream_contexts; mod builtins_stream_extensions; mod builtins_stream_settings; mod builtins_stream_sockets; +mod builtins_stream_wrapper_cast; mod builtins_stream_wrapper_directories; mod builtins_stream_wrapper_file_io; mod builtins_stream_wrapper_metadata; From 5d3ecfe8bbc0d5b91f61c33da9baf09396f9d7ea Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 28 Jun 2026 23:28:33 +0200 Subject: [PATCH 0923/1208] Improve magician debug output builtins --- .../interpreter/builtins/registry/binding.rs | 3 +- .../builtins/registry/dispatch/core.rs | 14 +- .../builtins/registry/signature.rs | 3 + .../src/interpreter/debug_output.rs | 597 ++++++++++++++++-- .../src/interpreter/statements.rs | 2 +- .../tests/builtins_debug_output.rs | 194 ++++++ .../src/interpreter/tests/expressions.rs | 88 --- .../src/interpreter/tests/mod.rs | 1 + docs/internals/the-runtime.md | 15 +- docs/php/eval.md | 28 +- 10 files changed, 766 insertions(+), 179 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_debug_output.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index fe4ac8159c..26dd3d112e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -299,7 +299,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "preg_replace" => Some(&["pattern", "replacement", "subject"]), "preg_replace_callback" => Some(&["pattern", "callback", "subject"]), "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), - "print_r" | "var_dump" => Some(&["value"]), + "print_r" => Some(&["value", "return"]), + "var_dump" => Some(&["value", "values"]), "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "range" => Some(&["start", "end"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs index a7b68a7b4d..1c07bd2c0a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs @@ -20,18 +20,8 @@ pub(in crate::interpreter) fn eval_core_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "print_r" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_print_r_result(*value, values)? - } - "var_dump" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_var_dump_result(*value, context, values)? - } + "print_r" => eval_print_r_result(evaluated_args, context, values)?, + "var_dump" => eval_var_dump_result(evaluated_args, context, values)?, "call_user_func" => { return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) .map(Some); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 5d17c2c297..60830d9249 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -112,6 +112,8 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "preg_match" => optional_by_ref(params, 2, &["matches"]), "preg_split" => optional(params, 2), + "print_r" => optional(params, 1), + "var_dump" => variadic(params, &[]), "touch" | "basename" | "dirname" | "pathinfo" => optional(params, 1), "fnmatch" | "fopen" | "fseek" | "fputcsv" => optional(params, 2), @@ -216,6 +218,7 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("preg_match", 2) => EmptyArray, ("preg_split", 2) => Int(-1), ("preg_split", 3) => Int(0), + ("print_r", 1) => Bool(false), ("touch", 1 | 2) => Null, ("basename", 1) => String(""), diff --git a/crates/elephc-magician/src/interpreter/debug_output.rs b/crates/elephc-magician/src/interpreter/debug_output.rs index 410d70e483..636c616972 100644 --- a/crates/elephc-magician/src/interpreter/debug_output.rs +++ b/crates/elephc-magician/src/interpreter/debug_output.rs @@ -5,93 +5,196 @@ //! - `crate::interpreter::eval_positional_expr_call()` for debug-output builtin dispatch. //! //! Key details: -//! - Output formatting walks runtime arrays, scalars, and AOT object names through `RuntimeValueOps`. -//! - Eval-declared object names come from the interpreter context's dynamic object registry. -//! - The builtins either echo output directly or return captured string output according to PHP flags. +//! - Output formatting walks runtime arrays, scalars, object names, and public or +//! eval-declared object properties through `RuntimeValueOps` and eval context metadata. +//! - Eval property aliases are rendered as references when dumping eval-declared objects. +//! - `print_r($value, true)` returns captured output instead of echoing it. + +use std::collections::HashSet; use super::*; -/// Evaluates PHP `print_r()` over one eval expression. +/// Property visibility rendered by `var_dump()` and `print_r()` object output. +#[derive(Clone)] +struct EvalDebugPropertyVisibility { + kind: EvalDebugPropertyVisibilityKind, +} + +/// Concrete PHP visibility shape for one object property key. +#[derive(Clone)] +enum EvalDebugPropertyVisibilityKind { + Public, + Protected, + Private(String), +} + +/// Object property entry collected before rendering object headers. +#[derive(Clone)] +struct EvalDebugObjectProperty { + name: String, + visibility: EvalDebugPropertyVisibility, + value: RuntimeCellHandle, + is_reference: bool, +} + +/// Evaluates PHP `print_r()` over one value and an optional return flag. pub(super) fn eval_builtin_print_r( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [value] = args else { + if !(1..=2).contains(&args.len()) { return Err(EvalStatus::RuntimeFatal); + } + let value = eval_expr(&args[0], context, scope, values)?; + let return_output = match args.get(1) { + Some(arg) => { + let flag = eval_expr(arg, context, scope, values)?; + values.truthy(flag)? + } + None => false, }; - let value = eval_expr(value, context, scope, values)?; - eval_print_r_result(value, values) + eval_print_r_value_result(value, return_output, context, values) } -/// Emits one eval value using elephc's supported `print_r()` output shape. +/// Evaluates already materialized `print_r()` arguments. pub(in crate::interpreter) fn eval_print_r_result( + args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let return_output = match args.get(1) { + Some(flag) => values.truthy(*flag)?, + None => false, + }; + eval_print_r_value_result(args[0], return_output, context, values) +} + +/// Renders, echoes, or returns one `print_r()` output string. +fn eval_print_r_value_result( value: RuntimeCellHandle, + return_output: bool, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if matches!(values.type_tag(value)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - let output = values.string_bytes_value(b"Array\n")?; - values.echo(output)?; + let mut output = Vec::new(); + let mut arrays_seen = Vec::new(); + let mut objects_seen = Vec::new(); + eval_print_r_append_value( + value, + context, + values, + 0, + &mut arrays_seen, + &mut objects_seen, + &mut output, + )?; + let output = values.string_bytes_value(&output)?; + if return_output { + Ok(output) } else { - values.echo(value)?; + values.echo(output)?; + values.bool_value(true) } - values.bool_value(true) } -/// Evaluates PHP `var_dump()` over one eval expression and returns null. +/// Evaluates PHP `var_dump()` over one or more eval expressions and returns null. pub(super) fn eval_builtin_var_dump( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [value] = args else { + if args.is_empty() { return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_var_dump_result(value, context, values) + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_var_dump_result(&evaluated_args, context, values) } -/// Emits one eval value using PHP-style `var_dump()` debug formatting. +/// Emits already materialized values using PHP-style `var_dump()` debug formatting. pub(in crate::interpreter) fn eval_var_dump_result( - value: RuntimeCellHandle, - context: &ElephcEvalContext, + values_to_dump: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if values_to_dump.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } let mut output = Vec::new(); let mut arrays_seen = Vec::new(); - eval_var_dump_append_value(value, context, values, 0, &mut arrays_seen, &mut output)?; + let mut objects_seen = Vec::new(); + for value in values_to_dump { + eval_var_dump_append_value( + *value, + context, + values, + 0, + false, + &mut arrays_seen, + &mut objects_seen, + &mut output, + )?; + } let output = values.string_bytes_value(&output)?; values.echo(output)?; values.null() } -/// Appends one value and its nested array entries to a `var_dump()` byte buffer. +/// Appends one value and its nested entries to a `var_dump()` byte buffer. fn eval_var_dump_append_value( value: RuntimeCellHandle, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, depth: usize, + is_reference: bool, arrays_seen: &mut Vec, + objects_seen: &mut Vec, output: &mut Vec, ) -> Result<(), EvalStatus> { match values.type_tag(value)? { - EVAL_TAG_INT => eval_var_dump_append_scalar(b"int", value, values, depth, output), - EVAL_TAG_STRING => eval_var_dump_append_string(value, values, depth, output), - EVAL_TAG_FLOAT => eval_var_dump_append_scalar(b"float", value, values, depth, output), - EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, output), - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { - eval_var_dump_append_array(value, context, values, depth, arrays_seen, output) + EVAL_TAG_INT => { + eval_var_dump_append_scalar(b"int", value, values, depth, is_reference, output) } - EVAL_TAG_OBJECT => eval_var_dump_append_object(value, context, values, depth, output), + EVAL_TAG_STRING => eval_var_dump_append_string(value, values, depth, is_reference, output), + EVAL_TAG_FLOAT => { + eval_var_dump_append_scalar(b"float", value, values, depth, is_reference, output) + } + EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, is_reference, output), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => eval_var_dump_append_array( + value, + context, + values, + depth, + is_reference, + arrays_seen, + objects_seen, + output, + ), + EVAL_TAG_OBJECT => eval_var_dump_append_object( + value, + context, + values, + depth, + is_reference, + arrays_seen, + objects_seen, + output, + ), EVAL_TAG_NULL => { - eval_var_dump_append_indent(depth, output); + eval_var_dump_append_prefix(depth, is_reference, output); output.extend_from_slice(b"NULL\n"); Ok(()) } EVAL_TAG_RESOURCE => { - eval_var_dump_append_indent(depth, output); + eval_var_dump_append_prefix(depth, is_reference, output); output.extend_from_slice(b"resource(0) of type (stream)\n"); Ok(()) } @@ -105,9 +208,10 @@ fn eval_var_dump_append_scalar( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, depth: usize, + is_reference: bool, output: &mut Vec, ) -> Result<(), EvalStatus> { - eval_var_dump_append_indent(depth, output); + eval_var_dump_append_prefix(depth, is_reference, output); output.extend_from_slice(label); output.extend_from_slice(b"("); output.extend_from_slice(&values.string_bytes(value)?); @@ -120,10 +224,11 @@ fn eval_var_dump_append_string( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, depth: usize, + is_reference: bool, output: &mut Vec, ) -> Result<(), EvalStatus> { let bytes = values.string_bytes(value)?; - eval_var_dump_append_indent(depth, output); + eval_var_dump_append_prefix(depth, is_reference, output); output.extend_from_slice(b"string("); output.extend_from_slice(bytes.len().to_string().as_bytes()); output.extend_from_slice(b") \""); @@ -137,9 +242,10 @@ fn eval_var_dump_append_bool( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, depth: usize, + is_reference: bool, output: &mut Vec, ) -> Result<(), EvalStatus> { - eval_var_dump_append_indent(depth, output); + eval_var_dump_append_prefix(depth, is_reference, output); if values.truthy(value)? { output.extend_from_slice(b"bool(true)\n"); } else { @@ -151,30 +257,41 @@ fn eval_var_dump_append_bool( /// Appends one array shell and recursively emits foreach-visible entries. fn eval_var_dump_append_array( value: RuntimeCellHandle, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, depth: usize, + is_reference: bool, arrays_seen: &mut Vec, + objects_seen: &mut Vec, output: &mut Vec, ) -> Result<(), EvalStatus> { let address = value.as_ptr() as usize; if arrays_seen.contains(&address) { - eval_var_dump_append_indent(depth, output); + eval_var_dump_append_prefix(depth, is_reference, output); output.extend_from_slice(b"*RECURSION*\n"); return Ok(()); } arrays_seen.push(address); let len = values.array_len(value)?; - eval_var_dump_append_indent(depth, output); + eval_var_dump_append_prefix(depth, is_reference, output); output.extend_from_slice(b"array("); output.extend_from_slice(len.to_string().as_bytes()); output.extend_from_slice(b") {\n"); for position in 0..len { let key = values.array_iter_key(value, position)?; let element = values.array_get(value, key)?; - eval_var_dump_append_key(key, values, depth + 1, output)?; - eval_var_dump_append_value(element, context, values, depth + 1, arrays_seen, output)?; + eval_var_dump_append_array_key(key, values, depth + 1, output)?; + eval_var_dump_append_value( + element, + context, + values, + depth + 1, + false, + arrays_seen, + objects_seen, + output, + )?; } eval_var_dump_append_indent(depth, output); output.extend_from_slice(b"}\n"); @@ -182,38 +299,57 @@ fn eval_var_dump_append_array( Ok(()) } -/// Appends one object line while preserving eval-declared and runtime class names. +/// Appends one object shell and its collected properties. fn eval_var_dump_append_object( value: RuntimeCellHandle, - context: &ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, depth: usize, + is_reference: bool, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, output: &mut Vec, ) -> Result<(), EvalStatus> { - eval_var_dump_append_indent(depth, output); + let identity = eval_debug_object_identity(value, values); + let object_key = identity.unwrap_or(value.as_ptr() as usize as u64) as usize; + if objects_seen.contains(&object_key) { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"*RECURSION*\n"); + return Ok(()); + } + + objects_seen.push(object_key); + let class_name = eval_debug_object_class_name(value, identity, context, values)?; + let properties = eval_debug_object_properties(value, identity, &class_name, context, values)?; + eval_var_dump_append_prefix(depth, is_reference, output); output.extend_from_slice(b"object("); - if let Ok(identity) = values.object_identity(value) { - if let Some(class) = context.dynamic_object_class(identity) { - output.extend_from_slice(class.name().trim_start_matches('\\').as_bytes()); - output.extend_from_slice(b")\n"); - return Ok(()); - } + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b")#"); + output.extend_from_slice(object_key.to_string().as_bytes()); + output.extend_from_slice(b" ("); + output.extend_from_slice(properties.len().to_string().as_bytes()); + output.extend_from_slice(b") {\n"); + for property in &properties { + eval_var_dump_append_object_key(property, depth + 1, output); + eval_var_dump_append_value( + property.value, + context, + values, + depth + 1, + property.is_reference, + arrays_seen, + objects_seen, + output, + )?; } - let class_name = values.object_class_name(value)?; - let bytes = values.string_bytes(class_name)?; - values.release(class_name)?; - output.extend_from_slice(trim_leading_namespace_separator(&bytes)); - output.extend_from_slice(b")\n"); + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"}\n"); + objects_seen.pop(); Ok(()) } -/// Removes a leading PHP namespace separator from a runtime class-name byte slice. -fn trim_leading_namespace_separator(bytes: &[u8]) -> &[u8] { - bytes.strip_prefix(b"\\").unwrap_or(bytes) -} - /// Appends one array key line for an indexed or associative `var_dump()` entry. -fn eval_var_dump_append_key( +fn eval_var_dump_append_array_key( key: RuntimeCellHandle, values: &mut impl RuntimeValueOps, depth: usize, @@ -233,9 +369,346 @@ fn eval_var_dump_append_key( Ok(()) } -/// Appends the two-space indentation used by PHP `var_dump()` arrays. +/// Appends one object property key line for `var_dump()`. +fn eval_var_dump_append_object_key( + property: &EvalDebugObjectProperty, + depth: usize, + output: &mut Vec, +) { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"[\""); + output.extend_from_slice(property.name.as_bytes()); + output.extend_from_slice(b"\""); + match &property.visibility.kind { + EvalDebugPropertyVisibilityKind::Public => {} + EvalDebugPropertyVisibilityKind::Protected => output.extend_from_slice(b":protected"), + EvalDebugPropertyVisibilityKind::Private(class_name) => { + output.extend_from_slice(b":\""); + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b"\":private"); + } + } + output.extend_from_slice(b"]=>\n"); +} + +/// Appends one `var_dump()` line prefix, including a reference marker when needed. +fn eval_var_dump_append_prefix(depth: usize, is_reference: bool, output: &mut Vec) { + eval_var_dump_append_indent(depth, output); + if is_reference { + output.extend_from_slice(b"&"); + } +} + +/// Appends the two-space indentation used by PHP `var_dump()` arrays and objects. fn eval_var_dump_append_indent(depth: usize, output: &mut Vec) { for _ in 0..depth { output.extend_from_slice(b" "); } } + +/// Appends one value to a `print_r()` byte buffer. +fn eval_print_r_append_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { + eval_print_r_append_array(value, context, values, depth, arrays_seen, objects_seen, output) + } + EVAL_TAG_OBJECT => { + eval_print_r_append_object(value, context, values, depth, arrays_seen, objects_seen, output) + } + _ => { + output.extend_from_slice(&values.string_bytes(value)?); + Ok(()) + } + } +} + +/// Appends one array in PHP `print_r()` style. +fn eval_print_r_append_array( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + output.extend_from_slice(b"*RECURSION*"); + return Ok(()); + } + arrays_seen.push(address); + output.extend_from_slice(b"Array\n"); + eval_print_r_append_indent(depth, output); + output.extend_from_slice(b"(\n"); + let len = values.array_len(value)?; + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + eval_print_r_append_indent(depth + 1, output); + output.extend_from_slice(b"["); + output.extend_from_slice(&values.string_bytes(key)?); + output.extend_from_slice(b"] => "); + eval_print_r_append_value( + element, + context, + values, + depth + 1, + arrays_seen, + objects_seen, + output, + )?; + output.extend_from_slice(b"\n"); + } + eval_print_r_append_indent(depth, output); + output.extend_from_slice(b")\n"); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one object in PHP `print_r()` style. +fn eval_print_r_append_object( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let identity = eval_debug_object_identity(value, values); + let object_key = identity.unwrap_or(value.as_ptr() as usize as u64) as usize; + if objects_seen.contains(&object_key) { + output.extend_from_slice(b"*RECURSION*"); + return Ok(()); + } + objects_seen.push(object_key); + let class_name = eval_debug_object_class_name(value, identity, context, values)?; + let properties = eval_debug_object_properties(value, identity, &class_name, context, values)?; + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b" Object\n"); + eval_print_r_append_indent(depth, output); + output.extend_from_slice(b"(\n"); + for property in &properties { + eval_print_r_append_indent(depth + 1, output); + eval_print_r_append_object_key(property, output); + output.extend_from_slice(b" => "); + eval_print_r_append_value( + property.value, + context, + values, + depth + 1, + arrays_seen, + objects_seen, + output, + )?; + output.extend_from_slice(b"\n"); + } + eval_print_r_append_indent(depth, output); + output.extend_from_slice(b")\n"); + objects_seen.pop(); + Ok(()) +} + +/// Appends one object property key for `print_r()`. +fn eval_print_r_append_object_key(property: &EvalDebugObjectProperty, output: &mut Vec) { + output.extend_from_slice(b"["); + output.extend_from_slice(property.name.as_bytes()); + match &property.visibility.kind { + EvalDebugPropertyVisibilityKind::Public => {} + EvalDebugPropertyVisibilityKind::Protected => output.extend_from_slice(b":protected"), + EvalDebugPropertyVisibilityKind::Private(class_name) => { + output.extend_from_slice(b":"); + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b":private"); + } + } + output.extend_from_slice(b"]"); +} + +/// Appends the four-space indentation used by PHP `print_r()`. +fn eval_print_r_append_indent(depth: usize, output: &mut Vec) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} + +/// Returns an object identity without turning non-object-like values into fatals. +fn eval_debug_object_identity( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Option { + values.object_identity(value).ok() +} + +/// Resolves the PHP-visible class name for one object value. +fn eval_debug_object_class_name( + value: RuntimeCellHandle, + identity: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(identity) = identity { + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().trim_start_matches('\\').to_string()); + } + } + let class_name = values.object_class_name(value)?; + let bytes = values.string_bytes(class_name)?; + values.release(class_name)?; + String::from_utf8(trim_leading_namespace_separator(&bytes).to_vec()) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Collects object properties visible to debug-output rendering. +fn eval_debug_object_properties( + object: RuntimeCellHandle, + identity: Option, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(identity) = identity { + if context.dynamic_object_class(identity).is_some() { + return eval_debug_dynamic_object_properties(object, identity, class_name, context, values); + } + } + eval_debug_public_object_properties(object, values) +} + +/// Collects eval-declared object properties plus public dynamic properties. +fn eval_debug_dynamic_object_properties( + object: RuntimeCellHandle, + identity: u64, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut properties = Vec::new(); + let mut storage_keys = HashSet::new(); + let mut emitted_public_names = HashSet::new(); + + for class in context.class_chain(class_name) { + for property in class.properties() { + if property.is_static() { + continue; + } + let storage_name = eval_instance_property_storage_name(class.name(), property); + storage_keys.insert(storage_name.clone()); + if !property.is_virtual() + && !context.dynamic_property_is_initialized(identity, &storage_name) + { + continue; + } + let alias = context.dynamic_property_alias(identity, &storage_name).cloned(); + let value = match &alias { + Some(target) => eval_reference_target_value(target, context, values)?, + None => values.property_get(object, &storage_name)?, + }; + if property.visibility() == EvalVisibility::Public { + emitted_public_names.insert(property.name().to_string()); + } + properties.push(EvalDebugObjectProperty { + name: property.name().to_string(), + visibility: eval_debug_property_visibility(class.name(), property.visibility()), + value, + is_reference: alias.is_some(), + }); + } + } + + eval_debug_append_dynamic_public_properties( + object, + &storage_keys, + &emitted_public_names, + &mut properties, + values, + )?; + Ok(properties) +} + +/// Appends dynamic public properties stored directly on an eval object. +fn eval_debug_append_dynamic_public_properties( + object: RuntimeCellHandle, + storage_keys: &HashSet, + emitted_public_names: &HashSet, + properties: &mut Vec, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key)?; + values.release(key)?; + let key_name = String::from_utf8(key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + if key_name.contains('\0') + || storage_keys.contains(&key_name) + || emitted_public_names.contains(&key_name) + { + continue; + } + let value = values.property_get(object, &key_name)?; + properties.push(EvalDebugObjectProperty { + name: key_name, + visibility: EvalDebugPropertyVisibility { + kind: EvalDebugPropertyVisibilityKind::Public, + }, + value, + is_reference: false, + }); + } + Ok(()) +} + +/// Collects public bridge-visible properties for non-eval runtime objects. +fn eval_debug_public_object_properties( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let property_count = values.object_property_len(object)?; + let mut properties = Vec::with_capacity(property_count); + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key)?; + values.release(key)?; + let key_name = String::from_utf8(key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let value = values.property_get(object, &key_name)?; + properties.push(EvalDebugObjectProperty { + name: key_name, + visibility: EvalDebugPropertyVisibility { + kind: EvalDebugPropertyVisibilityKind::Public, + }, + value, + is_reference: false, + }); + } + Ok(properties) +} + +/// Converts eval visibility metadata into debug-output key metadata. +fn eval_debug_property_visibility( + declaring_class: &str, + visibility: EvalVisibility, +) -> EvalDebugPropertyVisibility { + let kind = match visibility { + EvalVisibility::Public => EvalDebugPropertyVisibilityKind::Public, + EvalVisibility::Protected => EvalDebugPropertyVisibilityKind::Protected, + EvalVisibility::Private => { + EvalDebugPropertyVisibilityKind::Private(declaring_class.trim_start_matches('\\').to_string()) + } + }; + EvalDebugPropertyVisibility { kind } +} + +/// Removes a leading PHP namespace separator from a runtime class-name byte slice. +fn trim_leading_namespace_separator(bytes: &[u8]) -> &[u8] { + bytes.strip_prefix(b"\\").unwrap_or(bytes) +} diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 3f8c9b2e64..aacee1a834 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -5561,7 +5561,7 @@ fn eval_property_reference_alias_name(object_identity: u64, storage_property_nam } /// Reads the current value from a persistent reference target. -fn eval_reference_target_value( +pub(in crate::interpreter) fn eval_reference_target_value( target: &EvalReferenceTarget, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_debug_output.rs b/crates/elephc-magician/src/interpreter/tests/builtins_debug_output.rs new file mode 100644 index 0000000000..73cb0be50a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_debug_output.rs @@ -0,0 +1,194 @@ +//! Purpose: +//! Interpreter tests for eval debug-output builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These tests cover `print_r()` capture mode, variadic `var_dump()`, and +//! object property/reference rendering without growing the generic expression suite. + +use super::super::*; +use super::support::*; + +/// Verifies eval `print_r()` emits values, returns true, and captures output when requested. +#[test] +fn execute_program_dispatches_print_r_builtin() { + let program = parse_fragment( + br#"print_r("x"); echo ":"; +print_r(value: false); echo ":"; +print_r([1, 2]); echo ":"; +$call = call_user_func("print_r", true); +$spread = call_user_func_array("print_r", ["value" => "z"]); +$captured = print_r(["k" => 1], true); +$captured_call = call_user_func_array("print_r", ["value" => ["q" => 2], "return" => true]); +echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; +echo $captured === "Array\n(\n [k] => 1\n)\n" ? "captured" : "bad"; echo ":"; +echo $captured_call === "Array\n(\n [q] => 2\n)\n" ? "callcaptured" : "bad"; echo ":"; +return function_exists("print_r");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "x::", + "Array\n", + "(\n", + " [0] => 1\n", + " [1] => 2\n", + ")\n", + ":1z:call:spread:captured:callcaptured:", + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies captured `print_r()` output includes eval object properties. +#[test] +fn execute_program_print_r_captures_object_properties() { + let program = parse_fragment( + br#"class EvalPrintRProps { + public $a = 1; + protected $b = "p"; + private $c = false; + public function __construct(&$ref) { + $this->a =& $ref; + $this->dyn = [2]; + } +} +$x = 9; +$o = new EvalPrintRProps($x); +$x = 10; +return print_r($o, true);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + let FakeValue::String(output) = values.get(result) else { + panic!("expected captured print_r object output"); + }; + assert_eq!(values.output, ""); + assert!(output.contains("EvalPrintRProps Object\n")); + assert!(output.contains(" [a] => 10\n")); + assert!(output.contains(" [b:protected] => p\n")); + assert!(output.contains(" [c:EvalPrintRProps:private] => \n")); + assert!(output.contains(" [dyn] => Array\n")); +} + +/// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. +#[test] +fn execute_program_dispatches_var_dump_builtin() { + let program = parse_fragment( + br#"var_dump(42); +var_dump("hi"); +var_dump(false); +var_dump(null); +var_dump([10, 20]); +var_dump(["x" => true]); +var_dump(1, "m", [2]); +$call = call_user_func("var_dump", 3.5, false); +$spread = call_user_func_array("var_dump", ["value" => "z", "values" => true]); +echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; +return function_exists("var_dump");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "int(42)\n", + "string(2) \"hi\"\n", + "bool(false)\n", + "NULL\n", + "array(2) {\n", + " [0]=>\n", + " int(10)\n", + " [1]=>\n", + " int(20)\n", + "}\n", + "array(1) {\n", + " [\"x\"]=>\n", + " bool(true)\n", + "}\n", + "int(1)\n", + "string(1) \"m\"\n", + "array(1) {\n", + " [0]=>\n", + " int(2)\n", + "}\n", + "float(3.5)\n", + "bool(false)\n", + "string(1) \"z\"\n", + "bool(true)\n", + "call-null:spread-null:", + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval `var_dump()` keeps eval-declared and runtime object class names. +#[test] +fn execute_program_var_dump_prints_object_class_names() { + let program = parse_fragment( + br#"class EvalDumpBox {} +var_dump(new EvalDumpBox()); +var_dump(new KnownClass());"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert!(values.output.contains("object(EvalDumpBox)#")); + assert!(values.output.contains(" (0) {\n}\n")); + assert!(values.output.contains("object(KnownClass)#")); + assert_eq!(values.get(result), FakeValue::Null); +} + +/// Verifies object dumps include eval-declared properties, dynamic properties, and refs. +#[test] +fn execute_program_var_dump_prints_object_properties_and_references() { + let program = parse_fragment( + br#"class EvalDumpProps { + public $a = 1; + protected $b = "p"; + private $c = false; + public function __construct(&$ref) { + $this->a =& $ref; + $this->dyn = [2]; + } +} +$x = 9; +$o = new EvalDumpProps($x); +var_dump($o); +$x = 10; +var_dump($o);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert!(values.output.contains("object(EvalDumpProps)#")); + assert!(values.output.contains(" (4) {\n")); + assert!(values.output.contains(" [\"a\"]=>\n &int(9)\n")); + assert!(values.output.contains(" [\"b\":protected]=>\n string(1) \"p\"\n")); + assert!(values.output.contains(" [\"c\":\"EvalDumpProps\":private]=>\n bool(false)\n")); + assert!(values.output.contains(" [\"dyn\"]=>\n array(1) {\n")); + assert!(values.output.contains(" [\"a\"]=>\n &int(10)\n")); + assert_eq!(values.get(result), FakeValue::Null); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index 2fa41fb2a0..41d67646b6 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -133,94 +133,6 @@ fn execute_program_print_returns_one() { assert_eq!(values.output, "p"); assert_eq!(values.get(result), FakeValue::Int(1)); } -/// Verifies eval `print_r()` emits supported values and returns true. -#[test] -fn execute_program_dispatches_print_r_builtin() { - let program = parse_fragment( - br#"print_r("x"); echo ":"; -print_r(value: false); echo ":"; -print_r([1, 2]); echo ":"; -$call = call_user_func("print_r", true); -$spread = call_user_func_array("print_r", ["value" => "z"]); -echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; -return function_exists("print_r");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "x::Array\n:1z:call:spread:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} -/// Verifies eval `var_dump()` emits scalar and array diagnostics and returns null. -#[test] -fn execute_program_dispatches_var_dump_builtin() { - let program = parse_fragment( - br#"var_dump(42); -var_dump("hi"); -var_dump(false); -var_dump(null); -var_dump([10, 20]); -var_dump(["x" => true]); -$call = call_user_func("var_dump", 3.5); -$spread = call_user_func_array("var_dump", ["value" => "z"]); -echo ($call === null ? "call-null" : "bad") . ":" . ($spread === null ? "spread-null" : "bad") . ":"; -return function_exists("var_dump");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - concat!( - "int(42)\n", - "string(2) \"hi\"\n", - "bool(false)\n", - "NULL\n", - "array(2) {\n", - " [0]=>\n", - " int(10)\n", - " [1]=>\n", - " int(20)\n", - "}\n", - "array(1) {\n", - " [\"x\"]=>\n", - " bool(true)\n", - "}\n", - "float(3.5)\n", - "string(1) \"z\"\n", - "call-null:spread-null:", - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval `var_dump()` keeps eval-declared and runtime object class names. -#[test] -fn execute_program_var_dump_prints_object_class_names() { - let program = parse_fragment( - br#"class EvalDumpBox {} -var_dump(new EvalDumpBox()); -var_dump(new KnownClass());"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "object(EvalDumpBox)\nobject(KnownClass)\n" - ); - assert_eq!(values.get(result), FakeValue::Null); -} - /// Verifies eval property reads and writes dispatch through runtime hooks. #[test] fn execute_program_reads_and_writes_object_property() { diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index 7723b50491..3bd2d5b0b8 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -15,6 +15,7 @@ mod builtins_arrays_core; mod builtins_arrays_iterators; mod builtins_arrays_sets; mod builtins_class_metadata; +mod builtins_debug_output; mod builtins_directory_streams; mod builtins_file_streams; mod builtins_filesystem_metadata; diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index a91cda8bcf..e7ce2ad22d 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -37,9 +37,18 @@ Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. -Eval `print_r()` writes scalars through the eval output hook and emits `Array\n` for indexed or associative arrays, matching the native runtime's currently supported `print_r()` shape. - -Eval `var_dump()` formats scalar tags and array contents inside the interpreter, then emits one byte string through the eval output hook. Array dumping reads foreach-visible keys and values through the same eval hooks used by `foreach`, `array_keys()`, and `array_values()`. +Eval `print_r()` formats scalars, recursive arrays, and bridge-visible object +properties inside the interpreter. With the second argument truthy it returns +the captured string; otherwise it emits that string through the eval output +hook and returns `true`. + +Eval `var_dump()` formats one or more materialized arguments inside the +interpreter, then emits one byte string through the eval output hook. Array +dumping reads foreach-visible keys and values through the same eval hooks used +by `foreach`, `array_keys()`, and `array_values()`. Object dumping uses eval +class metadata for eval-declared property visibility labels, public runtime +property hooks for generated/AOT objects, and eval property-alias metadata for +reference markers. Eval SPL class introspection dispatch includes `spl_classes()` through direct calls, callable dispatch, and `function_exists()` probes. The interpreter builds diff --git a/docs/php/eval.md b/docs/php/eval.md index d1bfb958e9..fa8398c746 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -828,19 +828,23 @@ ignore/substitute modes where applicable, `JSON_BIGINT_AS_STRING` for overflowing integer tokens in `json_decode()`, and `JsonException` through `JSON_THROW_ON_ERROR`. -Eval local filesystem calls operate on host filesystem paths. Stream wrappers, -PHAR URLs, network URLs, ownership/group modification, and `fstat()` array -results remain outside the eval filesystem subset. Stream wrapper functionality -for native code is documented in [Streams](streams.md). - -Eval `print_r()` supports the one-argument form. Scalars print through the same -output path as `echo`, boolean false and null print nothing, and arrays print -the same `Array\n` header shape as elephc's native `print_r()` subset. - -Eval `var_dump()` supports the one-argument form. Scalars print typed +Eval local filesystem calls operate on host filesystem paths and support the +implemented stream-wrapper paths, including `file://`, `php://memory`, +`data://`, `phar://`, plain `http://`, and eval-registered userspace wrappers. +Eval also supports complete `fstat()` arrays and portable ownership/group +metadata calls where the host platform exposes them. TLS-backed `https://` +URLs remain outside magician's implemented wrapper paths. + +Eval `print_r()` supports the normal echoing form and `print_r($value, true)`. +Scalars print through the same output path as `echo`, boolean false and null +print nothing, arrays use PHP's recursive `Array\n(\n [key] => value\n)\n` +shape, and objects render class names plus bridge-visible properties. + +Eval `var_dump()` supports one or more arguments. Scalars print typed diagnostic lines, indexed or associative arrays print foreach-visible keys and -nested values through eval value hooks, and eval-declared or generated/AOT -objects print their PHP-visible class names. +nested values through eval value hooks, and objects print class names, object +ids, bridge-visible properties, eval-declared private/protected/public property +labels, and eval property references when alias metadata is available. ## Current limitations From 74c7052c9aa50c12fb828f3bc48ade209c7b0b3d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 09:35:32 +0200 Subject: [PATCH 0924/1208] Fix magician by-ref array mutators --- .../src/interpreter/builtins/arrays/core.rs | 103 +++++++++++++++++- .../interpreter/builtins/arrays/mutation.rs | 3 + .../interpreter/builtins/registry/callable.rs | 5 +- .../builtins/registry/dispatch/arrays.rs | 3 + .../src/interpreter/expressions.rs | 1 + .../src/interpreter/runtime_ops.rs | 19 ++++ .../src/interpreter/statements.rs | 4 +- .../interpreter/tests/builtins_arrays_core.rs | 63 ++++++++++- .../src/interpreter/tests/dynamic_calls.rs | 3 +- 9 files changed, 197 insertions(+), 7 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs index 37971387e2..8e8d1afff5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs @@ -370,7 +370,108 @@ pub(in crate::interpreter) fn eval_array_reduce_result( Ok(carry) } -/// Evaluates PHP `array_walk()` for side-effect callbacks over value/key pairs. +/// Evaluates direct PHP `array_walk()` calls and preserves element by-ref targets. +pub(in crate::interpreter) fn eval_builtin_array_walk_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, array_target, callback) = + eval_array_walk_direct_args(args, context, scope, values)?; + eval_array_walk_ref_result(array, array_target, callback, context, values) +} + +/// Evaluates and binds direct `array_walk()` arguments in PHP source order. +fn eval_array_walk_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut array_target = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array_target.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + array_target = Some(eval_array_mutation_lvalue_arg(arg, context, scope, values)?); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (array, array_target) = array_target.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, array_target, callback)) +} + +/// Walks one writable eval array by invoking a callable with element ref targets. +pub(in crate::interpreter) fn eval_array_walk_ref_result( + array: RuntimeCellHandle, + array_target: EvalReferenceTarget, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable(callback, context, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let current_array = eval_reference_target_value(&array_target, context, values)?; + let key = values.array_iter_key(current_array, position)?; + let value = values.array_get(current_array, key)?; + let ref_target = EvalReferenceTarget::NestedArrayElement { + array_target: Box::new(array_target.clone()), + index: key, + }; + let args = vec![ + EvaluatedCallArg { + name: None, + value, + ref_target: Some(ref_target), + }, + EvaluatedCallArg { + name: None, + value: key, + ref_target: None, + }, + ]; + let _ = eval_evaluated_callable_with_call_array_args(&callback, args, context, values)?; + } + values.bool_value(true) +} + +/// Evaluates PHP `array_walk()` for by-value callable dispatch. pub(in crate::interpreter) fn eval_builtin_array_walk( args: &[EvalExpr], context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs index e5b68377f0..7e26cbafb8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs @@ -155,6 +155,9 @@ pub(in crate::interpreter) fn eval_builtin_array_pop_shift_call( if name == "array_splice" { return eval_builtin_array_splice_call(args, context, scope, values); } + if name == "array_walk" { + return eval_builtin_array_walk_call(args, context, scope, values); + } if matches!( name, "arsort" diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 3bb4ddf0e6..aee4c893d7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -435,7 +435,10 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if evaluated_args.iter().all(|arg| arg.name.is_none()) { + if evaluated_args + .iter() + .all(|arg| arg.name.is_none() && arg.ref_target.is_none()) + { let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); return eval_callable_with_values(name, evaluated_args, context, values); } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs index 71df18a0b8..37dd0a09ca 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs @@ -80,6 +80,9 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + values.warning( + "array_walk(): Argument #1 ($array) must be passed by reference, value given", + )?; eval_array_walk_result(*array, *callback, context, values)? } "array_pop" | "array_shift" => { diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index cd707b78b4..c76234d34b 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -715,6 +715,7 @@ pub(in crate::interpreter) fn eval_call( | "array_shift" | "array_splice" | "array_unshift" + | "array_walk" | "arsort" | "asort" | "krsort" diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 067a5c149d..a3dcf5f401 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -64,6 +64,25 @@ pub trait RuntimeValueOps { value: RuntimeCellHandle, ) -> Result; + /// Creates a shallow array copy before writeback paths perform PHP COW writes. + fn array_clone_shallow( + &mut self, + array: RuntimeCellHandle, + ) -> Result { + let len = self.array_len(array)?; + let mut result = match self.type_tag(array)? { + EVAL_TAG_ARRAY => self.array_new(len)?, + EVAL_TAG_ASSOC => self.assoc_new(len)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + for position in 0..len { + let key = self.array_iter_key(array, position)?; + let value = self.array_get(array, key)?; + result = self.array_set(result, key, value)?; + } + Ok(result) + } + /// Reads a named property from a runtime object held in a boxed Mixed cell. fn property_get( &mut self, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index aacee1a834..1c61a16afe 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -9705,7 +9705,7 @@ fn write_back_method_array_element_ref_target( { if values.is_array_like(existing.cell())? { ownership = existing.flags().ownership; - existing.cell() + values.array_clone_shallow(existing.cell())? } else { eval_new_array_for_index(index, values)? } @@ -9729,7 +9729,7 @@ fn write_back_method_nested_array_element_ref_target( ) -> Result<(), EvalStatus> { let current = eval_reference_target_value(array_target, context, values)?; let array = if values.is_array_like(current)? { - current + values.array_clone_shallow(current)? } else { eval_new_array_for_index(index, values)? }; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs index 88bc8af8c0..d61a17e9f4 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs @@ -117,9 +117,31 @@ return function_exists("array_reduce");"#, fn execute_program_dispatches_array_walk_builtin() { let program = parse_fragment( br#"function eval_walk_show($value, $key) { echo $key . "=" . $value . ";"; } -echo array_walk(["a" => 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; +$show = ["a" => 2, "b" => 3]; +echo array_walk($show, "eval_walk_show") ? "T:" : "F:"; $call = call_user_func("array_walk", [4, 5], "eval_walk_show"); $spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); +function eval_walk_add_key(&$value, $key) { $value = $value + $key + 1; } +$items = [10, 20]; +array_walk($items, "eval_walk_add_key"); +echo $items[0] . ":" . $items[1] . ":"; +$aliased = [3, 4]; +$alias =& $aliased; +array_walk($alias, "eval_walk_add_key"); +echo $aliased[0] . ":" . $alias[1] . ":"; +$original = [5, 6]; +$copy = $original; +array_walk($copy, "eval_walk_add_key"); +echo $original[0] . ":" . $original[1] . ":" . $copy[0] . ":" . $copy[1] . ":"; +class EvalArrayWalkByRefCallbackBox { + public function tag(&$value, $key) { $value = $value . $key; } +} +$letters = ["a" => "X", "b" => "Y"]; +array_walk($letters, [new EvalArrayWalkByRefCallbackBox(), "tag"]); +echo $letters["a"] . ":" . $letters["b"] . ":"; +$named = [1]; +array_walk(callback: "eval_walk_add_key", array: $named); +echo $named[0] . ":"; return function_exists("array_walk");"#, ) .expect("parse eval fragment"); @@ -128,7 +150,17 @@ return function_exists("array_walk");"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "a=2;b=3;T:0=4;1=5;z=6;"); + assert_eq!( + values.output, + "a=2;b=3;T:0=4;1=5;z=6;11:22:4:6:5:6:6:8:Xa:Yb:2:" + ); + assert_eq!( + values.warnings, + vec![ + "array_walk(): Argument #1 ($array) must be passed by reference, value given", + "array_walk(): Argument #1 ($array) must be passed by reference, value given", + ] + ); assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval `array_pop()` and `array_shift()` write back writable lvalue calls. @@ -225,6 +257,33 @@ return function_exists("array_push") && function_exists("array_unshift");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval array mutators write aliases and preserve copied array sources. +#[test] +fn execute_program_array_mutators_preserve_aliases_and_cow() { + let program = parse_fragment( + br#"$original = [1, 2, 3]; +$copy = $original; +array_pop($copy); +echo count($original) . ":" . $original[2] . ":" . count($copy) . ":"; +$aliased = [4, 5]; +$alias =& $aliased; +array_push($alias, 6); +echo count($aliased) . ":" . $aliased[2] . ":"; +$sortOriginal = [3, 1]; +$sortCopy = $sortOriginal; +sort($sortCopy); +echo $sortOriginal[0] . ":" . $sortCopy[0] . ":"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:3:2:3:6:3:1:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval `array_splice()` returns removed values and writes back writable lvalue calls. #[test] fn execute_program_dispatches_array_splice_builtin() { diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 7fba9f82fb..46e5957a5f 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -257,7 +257,8 @@ echo $mapped[0] . $mapped[1] . ":"; $filtered = array_filter([1, 2, 3, 4], $box->keep(...)); echo count($filtered) . ":"; echo array_reduce([1, 2], $box->sum(...), 0) . ":"; -array_walk(["a" => 1], $box->show(...)); +$walked = ["a" => 1]; +array_walk($walked, $box->show(...)); echo ":"; $sorted = [3, 1, 2]; usort($sorted, EvalFirstClassCallableBase::compareDesc(...)); From f88d43b9282ad011311d40654de5c71d0b91d106 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 09:59:27 +0200 Subject: [PATCH 0925/1208] Preserve by-ref targets for dynamic mutators --- .../interpreter/builtins/registry/callable.rs | 5 + .../builtins/registry/dynamic_mutation.rs | 263 ++++++++++++++++++ .../src/interpreter/builtins/registry/mod.rs | 2 + .../interpreter/tests/builtins_arrays_core.rs | 17 +- 4 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index aee4c893d7..50bb08f8f3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -442,6 +442,11 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); return eval_callable_with_values(name, evaluated_args, context, values); } + if let Some(result) = + eval_mutating_builtin_with_call_array_args(name, &evaluated_args, context, values)? + { + return Ok(result); + } if eval_php_visible_builtin_exists(name) { let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs new file mode 100644 index 0000000000..20f24edfca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -0,0 +1,263 @@ +//! Purpose: +//! Dispatches dynamic callable invocations of builtin mutators while preserving +//! caller-side by-reference targets captured during argument evaluation. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::callable`. +//! +//! Key details: +//! - This module only handles builtin calls whose direct PHP semantics can write +//! to caller storage. Other builtins continue through the by-value dispatcher. + +use super::super::super::*; +use super::super::*; + +/// Dispatches dynamic builtin calls that must preserve by-reference caller targets. +pub(in crate::interpreter) fn eval_mutating_builtin_with_call_array_args( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "settype" => eval_dynamic_settype_call(evaluated_args, context, values)?, + "array_walk" => eval_dynamic_array_walk_call(evaluated_args, context, values)?, + "array_pop" | "array_shift" => { + eval_dynamic_array_pop_shift_call(name, evaluated_args, context, values)? + } + "array_push" | "array_unshift" => { + eval_dynamic_array_push_unshift_call(name, evaluated_args, context, values)? + } + "array_splice" => eval_dynamic_array_splice_call(evaluated_args, context, values)?, + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" => { + eval_dynamic_array_sort_call(name, evaluated_args, context, values)? + } + "uasort" | "uksort" | "usort" => { + eval_dynamic_user_sort_call(name, evaluated_args, context, values)? + } + _ => return Ok(None), + }; + Ok(result) +} + +/// Evaluates a dynamic `settype()` call when the first argument is writable. +fn eval_dynamic_settype_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args(&["var", "type"], evaluated_args, false)?; + let var = required_evaluated_ref_arg(&bound, 0)?; + let type_name = required_evaluated_ref_arg(&bound, 1)?; + let Some(target) = var.ref_target.as_ref() else { + return Ok(None); + }; + let Some(converted) = eval_settype_cast_value(var.value, type_name.value, values)? else { + return values.bool_value(false).map(Some); + }; + eval_write_direct_ref_target( + target, + converted, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + values.bool_value(true).map(Some) +} + +/// Evaluates a dynamic `array_walk()` call when the array argument is writable. +fn eval_dynamic_array_walk_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = + bind_evaluated_ref_builtin_args(&["array", "callback"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let callback = required_evaluated_ref_arg(&bound, 1)?; + let Some(target) = array.ref_target.clone() else { + return Ok(None); + }; + eval_array_walk_ref_result(array.value, target, callback.value, context, values).map(Some) +} + +/// Evaluates a dynamic `array_pop()` or `array_shift()` call against a writable array. +fn eval_dynamic_array_pop_shift_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args(&["array"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + let (result, replacement) = eval_array_pop_shift_replacement(name, array.value, values)?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(result)) +} + +/// Evaluates dynamic `array_push()` or `array_unshift()` calls against a writable array. +fn eval_dynamic_array_push_unshift_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, inserted) = + bind_evaluated_ref_builtin_args(&["array", "values"], evaluated_args, true)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + if inserted.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let inserted_values = inserted.iter().map(|arg| arg.value).collect::>(); + let replacement = + eval_array_push_unshift_replacement(name, array.value, &inserted_values, values)?; + let result = eval_array_push_unshift_count_result(array.value, inserted_values.len(), values)?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(result)) +} + +/// Evaluates a dynamic `array_splice()` call against a writable array. +fn eval_dynamic_array_splice_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["array", "offset", "length", "replacement"], + evaluated_args, + false, + )?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let offset = required_evaluated_ref_arg(&bound, 1)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + let length = optional_evaluated_ref_arg(&bound, 2).map(|arg| arg.value); + let replacement_arg = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let (removed, replacement) = eval_array_splice_removed_and_replacement( + array.value, + offset.value, + length, + replacement_arg, + values, + )?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(removed)) +} + +/// Evaluates a dynamic standard array sort call against a writable array. +fn eval_dynamic_array_sort_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args(&["array"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + let replacement = eval_array_sort_replacement(name, array.value, values)?; + let result = values.bool_value(true)?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(result)) +} + +/// Evaluates a dynamic user-comparator sort call against a writable array. +fn eval_dynamic_user_sort_call( + name: &str, + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = + bind_evaluated_ref_builtin_args(&["array", "callback"], evaluated_args, false)?; + let array = required_evaluated_ref_arg(&bound, 0)?; + let callback = required_evaluated_ref_arg(&bound, 1)?; + let Some(target) = array.ref_target.as_ref() else { + return Ok(None); + }; + let replacement = + eval_user_sort_replacement(name, array.value, callback.value, context, values)?; + let result = values.bool_value(true)?; + eval_write_direct_ref_target(target, replacement, context, values, None)?; + Ok(Some(result)) +} + +/// Binds already evaluated arguments while preserving by-reference target metadata. +fn bind_evaluated_ref_builtin_args( + params: &[&str], + evaluated_args: &[EvaluatedCallArg], + variadic_last: bool, +) -> Result<(Vec>, Vec), EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut variadic_args = Vec::new(); + let mut next_positional = 0; + let mut saw_named = false; + + for arg in evaluated_args { + if let Some(name) = arg.name.as_deref() { + saw_named = true; + let Some(index) = params.iter().position(|param| *param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[index] = Some(arg.clone()); + continue; + } + + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + if variadic_last && next_positional >= params.len().saturating_sub(1) { + variadic_args.push(arg.clone()); + next_positional += 1; + continue; + } + if next_positional >= params.len() { + return Err(EvalStatus::RuntimeFatal); + } + if bound_args[next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg.clone()); + next_positional += 1; + } + + if variadic_last { + let variadic_index = params.len().saturating_sub(1); + if let Some(named_variadic) = bound_args[variadic_index].take() { + variadic_args.insert(0, named_variadic); + } + } + + Ok((bound_args, variadic_args)) +} + +/// Returns a required already evaluated argument by bound parameter index. +fn required_evaluated_ref_arg( + bound_args: &[Option], + index: usize, +) -> Result<&EvaluatedCallArg, EvalStatus> { + bound_args + .get(index) + .and_then(Option::as_ref) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns an optional already evaluated argument by bound parameter index. +fn optional_evaluated_ref_arg( + bound_args: &[Option], + index: usize, +) -> Option<&EvaluatedCallArg> { + bound_args.get(index).and_then(Option::as_ref) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 6e9d95915b..4906fc56bf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -13,6 +13,7 @@ mod binding; mod callable; mod callable_validation; mod dispatch; +mod dynamic_mutation; mod names; mod signature; @@ -20,5 +21,6 @@ pub(in crate::interpreter) use binding::*; pub(in crate::interpreter) use callable::*; pub(in crate::interpreter) use callable_validation::*; pub(in crate::interpreter) use dispatch::*; +pub(in crate::interpreter) use dynamic_mutation::*; pub(in crate::interpreter) use names::*; pub(in crate::interpreter) use signature::*; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs index d61a17e9f4..a4a7f6b772 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_arrays_core.rs @@ -273,6 +273,21 @@ $sortOriginal = [3, 1]; $sortCopy = $sortOriginal; sort($sortCopy); echo $sortOriginal[0] . ":" . $sortCopy[0] . ":"; +$popFn = array_pop(...); +$callableCopy = [7, 8]; +echo $popFn($callableCopy) . ":" . count($callableCopy) . ":"; +$sortFn = sort(...); +$callableSort = [9, 7]; +$sortFn($callableSort); +echo $callableSort[0] . ":"; +function eval_mutator_walk(&$value, $key) { $value = $value + $key + 1; } +$walkFn = array_walk(...); +$walked = [2]; +$walkFn($walked, "eval_mutator_walk"); +echo $walked[0] . ":"; +$setFn = settype(...); +$typed = 10; +echo $setFn($typed, "string") ? gettype($typed) . ":" . $typed . ":" : "bad:"; return true;"#, ) .expect("parse eval fragment"); @@ -281,7 +296,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "3:3:2:3:6:3:1:"); + assert_eq!(values.output, "3:3:2:3:6:3:1:8:1:7:3:string:10:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } /// Verifies eval `array_splice()` returns removed values and writes back writable lvalue calls. From 8b5d5a2813c7b6b0fbefa532335a59a67a180c96 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 10:57:39 +0200 Subject: [PATCH 0926/1208] Expand eval AOT bridge signature support --- .../interpreter/builtins/registry/callable.rs | 4 +- .../src/interpreter/dynamic_functions.rs | 5 +- .../src/interpreter/return_values.rs | 32 ++++++++ .../src/interpreter/statements.rs | 52 ++++++++---- .../src/interpreter/tests/native_scope.rs | 24 ++++++ docs/php/eval.md | 82 +++++++++++-------- tests/codegen/eval.rs | 32 ++++++++ 7 files changed, 173 insertions(+), 58 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 50bb08f8f3..21a7f5ad17 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -423,7 +423,7 @@ pub(in crate::interpreter) fn eval_callable_with_values( let evaluated_args = positional_args(evaluated_args); let evaluated_args = bind_evaluated_native_function_args(&function, evaluated_args, context, values)?; - return eval_native_function_with_values(function, evaluated_args, values); + return eval_native_function_with_values(function, evaluated_args, context, values); } Err(EvalStatus::UnsupportedConstruct) } @@ -465,7 +465,7 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( if let Some(function) = context.native_function(name) { let evaluated_args = bind_evaluated_native_function_args(&function, evaluated_args, context, values)?; - return eval_native_function_with_values(function, evaluated_args, values); + return eval_native_function_with_values(function, evaluated_args, context, values); } Err(EvalStatus::UnsupportedConstruct) } diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index c10588258a..e59462d498 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1334,13 +1334,14 @@ pub(super) fn eval_native_function( ) -> Result { let evaluated_args = eval_native_function_call_args(&function, args, context, caller_scope, values)?; - eval_native_function_with_values(function, evaluated_args, values) + eval_native_function_with_values(function, evaluated_args, context, values) } /// Invokes a registered AOT function after its positional arguments are prepared. pub(super) fn eval_native_function_with_values( function: NativeFunction, evaluated_args: Vec, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { if !function.bridge_supported() { @@ -1359,5 +1360,5 @@ pub(super) fn eval_native_function_with_values( if result.is_null() { return Err(EvalStatus::RuntimeFatal); } - Ok(result) + eval_declared_native_return_value(function.return_type(), None, None, result, context, values) } diff --git a/crates/elephc-magician/src/interpreter/return_values.rs b/crates/elephc-magician/src/interpreter/return_values.rs index 258c3ce427..c9b1a447b4 100644 --- a/crates/elephc-magician/src/interpreter/return_values.rs +++ b/crates/elephc-magician/src/interpreter/return_values.rs @@ -40,6 +40,38 @@ pub(in crate::interpreter) fn eval_declared_return_control_value( } } +/// Applies a registered native/AOT return type to an already materialized result. +pub(in crate::interpreter) fn eval_declared_native_return_value( + return_type: Option<&EvalParameterType>, + return_owner: Option<&str>, + called_class_name: Option<&str>, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(return_type) = return_type else { + return Ok(value); + }; + if eval_declared_return_type_is_void(return_type) { + return if values.type_tag(value)? == EVAL_TAG_NULL { + Ok(value) + } else { + Err(EvalStatus::RuntimeFatal) + }; + } + if eval_declared_return_type_is_never(return_type) { + return Err(EvalStatus::RuntimeFatal); + } + eval_declared_return_value( + return_type, + return_owner, + called_class_name, + value, + context, + values, + ) +} + /// Materializes an implicit return according to the declared return type. fn eval_declared_implicit_return_value( return_type: Option<&EvalParameterType>, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 1c61a16afe..8cc6bb9f19 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -10327,9 +10327,13 @@ fn eval_native_method_with_evaluated_args_bridge_scope( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + let mut resolved_bridge_scope = bridge_scope.map(str::to_string); let metadata = eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; if let Some((declaring_class, visibility, _, is_abstract)) = metadata { + if resolved_bridge_scope.is_none() { + resolved_bridge_scope = Some(declaring_class.clone()); + } if !is_abstract && validate_eval_member_access(&declaring_class, visibility, context).is_err() { @@ -10366,7 +10370,7 @@ fn eval_native_method_with_evaluated_args_bridge_scope( class_name, method_name, evaluated_args, - bridge_scope, + resolved_bridge_scope.as_deref(), called_class_scope, context, values, @@ -10405,13 +10409,10 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_b context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let signature = context.native_method_signature(class_name, method_name); - let bound_args = bind_native_callable_bound_args( - signature, - evaluated_args, - context, - values, - )?; + let signature_owner = bridge_scope.unwrap_or(class_name); + let signature = context.native_method_signature(signature_owner, method_name); + let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); + let bound_args = bind_native_callable_bound_args(signature, evaluated_args, context, values)?; let result = if let Some(scope) = bridge_scope { eval_native_method_call_with_scope( scope, @@ -10428,7 +10429,14 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_b let writeback = write_back_native_callable_ref_args(&bound_args, context, values); match (result, writeback) { (Err(status), _) | (_, Err(status)) => Err(status), - (Ok(result), Ok(())) => Ok(result), + (Ok(result), Ok(())) => eval_declared_native_return_value( + return_type.as_ref(), + Some(signature_owner), + called_class_scope.or(Some(class_name)), + result, + context, + values, + ), } } @@ -10461,9 +10469,13 @@ fn eval_native_static_method_with_evaluated_args_bridge_scope( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + let mut resolved_bridge_scope = bridge_scope.map(str::to_string); let metadata = eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; if let Some((declaring_class, visibility, is_static, is_abstract)) = metadata { + if resolved_bridge_scope.is_none() { + resolved_bridge_scope = Some(declaring_class.clone()); + } if is_static && !is_abstract && validate_eval_member_access(&declaring_class, visibility, context).is_err() @@ -10498,7 +10510,7 @@ fn eval_native_static_method_with_evaluated_args_bridge_scope( class_name, method_name, evaluated_args, - bridge_scope, + resolved_bridge_scope.as_deref(), called_class_scope, context, values, @@ -10534,13 +10546,10 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unch context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let signature = context.native_static_method_signature(class_name, method_name); - let bound_args = bind_native_callable_bound_args( - signature, - evaluated_args, - context, - values, - )?; + let signature_owner = bridge_scope.unwrap_or(class_name); + let signature = context.native_static_method_signature(signature_owner, method_name); + let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); + let bound_args = bind_native_callable_bound_args(signature, evaluated_args, context, values)?; let result = if let Some(scope) = bridge_scope { eval_native_static_method_call_with_scope( scope, @@ -10557,7 +10566,14 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unch let writeback = write_back_native_callable_ref_args(&bound_args, context, values); match (result, writeback) { (Err(status), _) | (_, Err(status)) => Err(status), - (Ok(result), Ok(())) => Ok(result), + (Ok(result), Ok(())) => eval_declared_native_return_value( + return_type.as_ref(), + Some(signature_owner), + called_class_scope.or(Some(class_name)), + result, + context, + values, + ), } } diff --git a/crates/elephc-magician/src/interpreter/tests/native_scope.rs b/crates/elephc-magician/src/interpreter/tests/native_scope.rs index a2525e9a91..977b2996e2 100644 --- a/crates/elephc-magician/src/interpreter/tests/native_scope.rs +++ b/crates/elephc-magician/src/interpreter/tests/native_scope.rs @@ -28,6 +28,30 @@ fn execute_program_calls_registered_native_function() { assert_eq!(result, expected); } + +/// Verifies registered native AOT function return types are enforced after dispatch. +#[test] +fn execute_program_checks_registered_native_function_return_type() { + let program = parse_fragment(br#"return native_answer();"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.string("not-an-int").expect("allocate fake result"); + let mut native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + native.set_return_type(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false, + )); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("native return type mismatch should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + /// Verifies direct eval calls can bind registered native parameters by name. #[test] fn execute_program_calls_registered_native_function_with_named_args() { diff --git a/docs/php/eval.md b/docs/php/eval.md index fa8398c746..c08c74e89f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, `isset()` / `empty()` probes, array writes/appends, array-element unsets, and increment/decrement, static-property unset attempts including dynamic names as PHP-compatible catchable errors, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, first-class callable syntax for supported function, method, and invokable-object targets, invokable eval and generated/AOT objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new [readonly] class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object constructor signatures when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference constructor parameters. | +| Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new [readonly] class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, union/nullable registered type validation, intersection object type validation, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, object, and supported union/intersection object shapes, with by-reference constructor parameters limited to the staged scalar/nullable scalar/Mixed/array/iterable/object slice. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object parameters when the current eval class scope satisfies PHP visibility; scalar/nullable scalar/Mixed/array/iterable/object return values are boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference parameters. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private parameters when the current eval class scope satisfies PHP visibility; scalar, nullable, callable, Mixed, array, iterable, object, union, and intersection-object return values are checked and boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, object, and supported union/intersection object shapes, with by-reference parameters limited to the staged scalar/nullable scalar/Mixed/array/iterable/object slice. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty top-level eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, eval-declared-function `__FUNCTION__` / `__METHOD__`, eval-declared-method `__FUNCTION__` / `__METHOD__` / `__CLASS__` / `__TRAIT__`, eval class-like constant/property initializers for `__CLASS__` / `__TRAIT__`, and reflected parameter defaults using the declaring callable scope, including trait-origin names for imported trait members. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | @@ -142,11 +142,14 @@ parents inherit bridge-supported parent instance-method callable probes and `call_user_func_array()`. Eval-declared static methods also support string-keyed named arguments through `call_user_func_array()`; generated/AOT static method fallback supports the same named-argument and positional variadic-tail binding -for public/protected/private scalar/nullable scalar/Mixed/array/iterable/object -parameter signatures, including registered by-reference lvalue validation, when -the current eval class scope satisfies PHP visibility. Generated AOT bridge -dispatch can invoke `mixed`/untyped, scalar including string, nullable scalar, array, iterable, and object by-reference method -and constructor parameters. +for public/protected/private scalar, nullable, callable, Mixed, array, iterable, +object, union, and intersection-object parameter signatures, including +registered by-reference lvalue validation, when the current eval class scope +satisfies PHP visibility. Generated AOT bridge dispatch can invoke +`mixed`/untyped, scalar including string, nullable scalar, array, iterable, +object, and supported union/intersection object shapes, with by-reference method +and constructor parameters limited to the staged scalar/nullable +scalar/Mixed/array/iterable/object slice. Post-barrier native direct calls and string-literal `call_user_func()` callbacks currently accept simple positional arguments. Post-barrier @@ -716,7 +719,8 @@ method parameters through `$this->method(...)` and `Class::method(...)` are supported by the native method bridge when the eval fragment runs inside a native class scope that satisfies PHP visibility for the declaring class, including registered named arguments and string-keyed unpacking; method returns -may be scalar/nullable scalar/Mixed/array/iterable/object values. +may be scalar, nullable, callable, Mixed, array, iterable, object, union, and +intersection-object values. ## Namespaces and constants @@ -808,13 +812,14 @@ PHP's by-value callback behavior: the return value is computed from the supplied array, a by-reference warning is emitted where PHP would emit one, and the caller's original array is not mutated. -Eval regex dispatch uses Rust's `regex` engine for common PCRE-style delimited -patterns. It strips PHP delimiters, supports the `i`, `m`, `s`, `u`, and `U` -modifiers, supports common capture array shapes and replacement references, and -supports `PREG_SPLIT_NO_EMPTY`, `PREG_SPLIT_DELIM_CAPTURE`, and -`PREG_SPLIT_OFFSET_CAPTURE`. PCRE constructs unsupported by Rust `regex` fail -as eval runtime fatals. Native non-eval regex codegen remains PCRE2-backed as -documented in [Regex](regex.md). +Eval regex dispatch uses PCRE2 through the POSIX wrapper for common PCRE-style +delimited patterns. It strips PHP delimiters, supports the `i`, `m`, `s`, `u`, +and `U` modifiers, supports common capture array shapes and replacement +references, and supports `PREG_SPLIT_NO_EMPTY`, `PREG_SPLIT_DELIM_CAPTURE`, and +`PREG_SPLIT_OFFSET_CAPTURE`. Patterns, delimiters, modifiers, or subject bytes +that the eval bridge cannot pass through this wrapper fail as eval runtime +fatals. Native non-eval regex codegen remains PCRE2-backed as documented in +[Regex](regex.md). Eval JSON support covers null, booleans, integers, floats, strings, indexed arrays, associative arrays, and `stdClass` dynamic properties. `json_encode()` @@ -855,17 +860,21 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and full `Closure` object -semantics for callback values are still outside eval fragments. Runtime/AOT object-method, static-method, and -constructor fallback from eval can bind registered names, defaults, positional -variadic tails, and by-reference lvalue metadata. Generated AOT bridge dispatch -is limited to visibility-checked -non-by-reference scalar/nullable scalar/Mixed/array/iterable/object parameter -signatures plus `mixed`/untyped, scalar including string, nullable scalar, -array, iterable, and object by-reference method and constructor parameters, -including the generated positional variadic array slot when present. Those -supported method and constructor shapes use normal target ABI materialization -for arguments beyond the register set instead of a fixed eval arity cap. Broader -parameter/return ABI shapes are still outside those bridge paths. +semantics for callback values are still outside eval fragments. Runtime/AOT +object-method, static-method, and constructor fallback from eval can bind +registered names, defaults, positional variadic tails, and by-reference lvalue +metadata. Generated AOT bridge dispatch supports visibility-checked +non-by-reference signatures whose generated ABI storage is scalar/string, +callable descriptor, boxed Mixed/union, array/hash, iterable, or object. +Registered type specs validate nullable and union members, intersection object +parameters, and intersection object returns before or after the generated AOT +call. By-reference method and constructor parameters remain limited to the +staged scalar/nullable scalar/Mixed/array/iterable/object slice, including the +generated positional variadic array slot when present. These supported method +and constructor shapes use normal target ABI materialization for arguments +beyond the register set instead of a fixed eval arity cap. Layouts such as +pointer, buffer, packed, resource, and other specialized native-only ABI shapes +remain outside those bridge paths. Eval class support is still smaller than the full static class system. Eval-declared `__destruct()` hooks run when an eval-owned dynamic object reaches @@ -877,17 +886,18 @@ class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Object/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and Enum/attribute slice, Reflection type APIs beyond retained parameter, generated -property, and function/method return metadata, broader -parameter and generated property default-value materialization beyond scalar, -null, empty-array, supported array-valued defaults, and supported object-valued parameter defaults during generated/AOT invocation, -and broader generated/AOT method and constructor bridge signatures beyond the current visibility-checked -scalar/nullable scalar/Mixed/array/iterable/object parameter slice plus scalar/nullable scalar/Mixed/array/iterable/object returns and -`__clone()` hooks; the remaining limit there is the supported type slice rather -than a fixed parameter count. Generated/AOT free-function and method type metadata, -return metadata, by-reference and variadic parameter flags, and generated/AOT +property, and function/method return metadata, broader parameter and generated +property default-value materialization beyond scalar, null, empty-array, +supported array-valued defaults, and supported object-valued parameter defaults +during generated/AOT invocation, and generated/AOT method and constructor +bridge signatures beyond the current visibility-checked by-reference parameter +slice, specialized native-only ABI shapes, and `__clone()` hooks; the remaining +limit there is the supported type slice rather than a fixed parameter count. +Generated/AOT free-function and method type metadata, return metadata, +by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered -metadata slices, while other unsupported bridge shapes remain metadata-only -rather than invocable through eval. +metadata slices, while unsupported native-only bridge shapes remain +metadata-only rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ecd3446454..171b7ca1ac 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -6426,6 +6426,38 @@ echo (new EvalAotNullableScalarReturnBox())->run(); assert_eq!(out, "S:SN:BF:BN:F15:FN:SS:SSN:SBT:SBN:SF25:SFN"); } +/// Verifies eval dispatch uses inherited AOT metadata for complex signatures. +#[test] +fn test_eval_fragment_dispatches_inherited_aot_complex_signatures() { + let out = compile_and_run( + r#"choose(suffix: "X") . ":" . $child->choose(value: 2) . ":" . EvalAotComplexChild::chooseStatic(suffix: "Y") . ":" . EvalAotComplexChild::chooseStatic(value: 3) . ":" . $child->both(new EvalAotComplexBoth());'); +"#, + ); + assert_eq!(out, "DX:12:DY:13:both"); +} + /// Verifies eval fragments can read iterable return values from AOT methods. #[test] fn test_eval_fragment_dispatches_aot_method_with_iterable_return() { From 02ee2aa6e9e861a8523261bc3ab3ee16c356b46e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 12:56:43 +0200 Subject: [PATCH 0927/1208] Make AOT variadic functions invocable from eval --- .../src/interpreter/dynamic_functions.rs | 124 +++++++++++++++++- docs/php/eval.md | 30 +++-- src/codegen/lower_inst/builtins/eval.rs | 2 +- tests/codegen/eval.rs | 24 ++++ 4 files changed, 165 insertions(+), 15 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index e59462d498..1c8b49aec9 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -330,6 +330,14 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { + if native_function_variadic_index(function).is_some() { + return bind_evaluated_native_variadic_function_args( + function, + evaluated_args, + context, + values, + ); + } let mut bound_args = vec![None; function.param_count()]; let has_param_names = function.param_names().len() == function.param_count(); let mut next_positional = 0; @@ -358,10 +366,114 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( *value = Some(materialize_native_callable_default(default, context, values)?); } - bound_args + let mut bound_args = bound_args .into_iter() .collect::>>() - .ok_or(EvalStatus::RuntimeFatal) + .ok_or(EvalStatus::RuntimeFatal)?; + apply_native_function_arg_types(function, None, &mut bound_args, context, values)?; + Ok(bound_args) +} + +/// Binds a native AOT variadic function while keeping the raw invoker argument layout. +fn bind_evaluated_native_variadic_function_args( + function: &NativeFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let variadic_index = native_function_variadic_index(function).ok_or(EvalStatus::RuntimeFatal)?; + let has_param_names = function.param_names().len() == function.param_count(); + let mut regular_args = vec![None; variadic_index]; + let mut variadic_args = Vec::new(); + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + if !has_param_names { + return Err(EvalStatus::RuntimeFatal); + } + let Some(param_index) = + native_function_regular_param_index(function, variadic_index, &name) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if regular_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + regular_args[param_index] = Some(arg.value); + } else if next_positional < variadic_index { + bind_dynamic_positional_arg(&mut regular_args, &mut next_positional, arg.value)?; + } else { + variadic_args.push(arg.value); + } + } + + for (position, value) in regular_args.iter_mut().enumerate() { + if value.is_some() { + continue; + } + if position < function.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = function.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *value = Some(materialize_native_callable_default(default, context, values)?); + } + + let mut bound_args = regular_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal)?; + bound_args.extend(variadic_args); + apply_native_function_arg_types( + function, + Some(variadic_index), + &mut bound_args, + context, + values, + )?; + Ok(bound_args) +} + +/// Applies registered native AOT function parameter types after argument binding. +fn apply_native_function_arg_types( + function: &NativeFunction, + variadic_index: Option, + bound_args: &mut [RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, value) in bound_args.iter_mut().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + let Some(param_type) = function.param_type(param_index) else { + continue; + }; + *value = eval_method_parameter_value(param_type, *value, context, values)?; + } + Ok(()) +} + +/// Returns the variadic parameter index for a native AOT function, if registered. +fn native_function_variadic_index(function: &NativeFunction) -> Option { + (0..function.param_count()).find(|index| function.param_variadic(*index)) +} + +/// Returns the non-variadic native function parameter index for one named argument. +fn native_function_regular_param_index( + function: &NativeFunction, + variadic_index: usize, + name: &str, +) -> Option { + function + .param_names() + .iter() + .enumerate() + .position(|(index, param)| index < variadic_index && param == name) } /// Binds evaluated method arguments and fills omitted parameters from defaults. @@ -1347,9 +1459,15 @@ pub(super) fn eval_native_function_with_values( if !function.bridge_supported() { return Err(EvalStatus::RuntimeFatal); } - if evaluated_args.len() != function.param_count() { + let variadic_index = native_function_variadic_index(&function); + if variadic_index.is_none() && evaluated_args.len() != function.param_count() { return Err(EvalStatus::RuntimeFatal); } + if let Some(variadic_index) = variadic_index { + if evaluated_args.len() < function.required_param_count().min(variadic_index) { + return Err(EvalStatus::RuntimeFatal); + } + } let arg_array = values.array_new(evaluated_args.len())?; for (index, value) in evaluated_args.into_iter().enumerate() { let index = values.int(index as i64)?; diff --git a/docs/php/eval.md b/docs/php/eval.md index c08c74e89f..83d47ae382 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -522,13 +522,17 @@ dispatch eval-declared functions with the same named/default/variadic argument binding used by direct eval function calls. Runtime-held generated/AOT `ReflectionFunction` objects can invoke registered generated functions through the native bridge with parameter names, supported defaults, named arguments, and -indexed or string-keyed runtime argument arrays. Registered generated/AOT +indexed or string-keyed runtime argument arrays. Direct eval calls, variable +function calls, and `call_user_func()` paths can also invoke registered +generated/AOT free functions with positional variadic tails when the generated +signature has no by-reference parameters. Registered generated/AOT free-function parameter names, declared types, return types, by-reference and variadic flags, required/optional counts, and supported defaults are also exposed through `ReflectionFunction` / `ReflectionParameter` metadata. -Unsupported generated/AOT free-function bridge shapes remain reflectable as -metadata but are not invocable through eval. Supported callable-builtin -invocation is covered by the general Reflection support documented in +Unsupported generated/AOT free-function bridge shapes, such as by-reference +function parameters and native-only ABI layouts, remain reflectable as metadata +but are not invocable through eval. Supported callable-builtin invocation is +covered by the general Reflection support documented in `docs/php/classes.md`. Defaulted eval method parameters are bound when omitted and reported through `ReflectionParameter::isOptional()`, @@ -861,11 +865,14 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and full `Closure` object semantics for callback values are still outside eval fragments. Runtime/AOT -object-method, static-method, and constructor fallback from eval can bind -registered names, defaults, positional variadic tails, and by-reference lvalue -metadata. Generated AOT bridge dispatch supports visibility-checked -non-by-reference signatures whose generated ABI storage is scalar/string, -callable descriptor, boxed Mixed/union, array/hash, iterable, or object. +free-function, object-method, static-method, and constructor fallback from eval +can bind registered names, defaults, and positional variadic tails; method, +static-method, and constructor fallback can also bind by-reference lvalue +metadata. Generated AOT bridge dispatch supports visibility-checked method, +static-method, and constructor signatures whose generated ABI storage is +scalar/string, callable descriptor, boxed Mixed/union, array/hash, iterable, or +object, plus non-by-reference free-function signatures using the descriptor +invoker ABI. Registered type specs validate nullable and union members, intersection object parameters, and intersection object returns before or after the generated AOT call. By-reference method and constructor parameters remain limited to the @@ -896,8 +903,9 @@ limit there is the supported type slice rather than a fixed parameter count. Generated/AOT free-function and method type metadata, return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered -metadata slices, while unsupported native-only bridge shapes remain -metadata-only rather than invocable through eval. +metadata slices, while unsupported native-only bridge shapes and by-reference +free-function bridge shapes remain metadata-only rather than invocable through +eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index d257d066a6..407009e0ff 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -2393,7 +2393,7 @@ fn function_signature_can_bridge_with_eval(function: &Function) -> bool { function .params .iter() - .all(|param| !param.by_ref && !param.variadic) + .all(|param| !param.by_ref) } /// Returns true when eval can dispatch a native method through the generated bridge. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 171b7ca1ac..ed51484589 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5274,6 +5274,30 @@ eval('echo native_eval_spread(...["L", "R"]);'); assert_eq!(out, "L:R"); } +/// Verifies eval can dispatch generated/AOT variadic functions through the native bridge. +#[test] +fn test_eval_fragment_can_call_native_variadic_user_function() { + let out = compile_and_run( + r#" 0 ? $items[0] : "-"); +} + +echo eval('$fn = "native_eval_variadic_collect"; +return native_eval_variadic_collect("H", "A", "B") . "|" + . native_eval_variadic_default(head: "N") . "|" + . native_eval_variadic_default("P", "Q") . "|" + . $fn("V", "X", "Y") . "|" + . call_user_func("native_eval_variadic_collect", "C", "M", "N");'); +"#, + ); + assert_eq!(out, "H:2:A:B|N:0:-|P:1:Q|V:2:X:Y|C:2:M:N"); +} + /// Verifies eval fragments called from methods can mutate public properties through `$this`. #[test] fn test_eval_fragment_can_mutate_this_public_property() { From 1260330b651b2aad05af7f2549fe147f92af95d0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 13:24:59 +0200 Subject: [PATCH 0928/1208] Invoke AOT mixed by-ref functions from eval --- .../src/interpreter/control.rs | 13 + .../src/interpreter/dynamic_functions.rs | 237 +++++++++++++++--- crates/elephc-magician/src/interpreter/mod.rs | 5 +- .../src/interpreter/runtime_ops.rs | 6 + .../src/interpreter/tests/support/cell_ops.rs | 2 + .../interpreter/tests/support/conversions.rs | 4 + .../src/interpreter/tests/support/mod.rs | 1 + .../interpreter/tests/support/runtime_ops.rs | 7 + .../src/runtime_hooks/externs.rs | 5 +- .../elephc-magician/src/runtime_hooks/ops.rs | 8 + crates/elephc-magician/src/value.rs | 1 + docs/php/eval.md | 29 ++- src/codegen/lower_inst/builtins/eval.rs | 12 +- src/codegen_support/runtime/eval_bridge.rs | 13 + tests/codegen/eval.rs | 21 ++ 15 files changed, 316 insertions(+), 48 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index cadbb2a877..2af0745bac 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -43,6 +43,19 @@ pub(super) struct BoundMethodArg { pub(super) variadic_ref_targets: Vec<(RuntimeCellHandle, EvalReferenceTarget)>, } +/// One native function argument list prepared for the descriptor invoker ABI. +pub(super) struct BoundNativeFunctionArgs { + pub(super) values: Vec, + pub(super) ref_slots: Vec, +} + +/// One staged Mixed-slot reference passed to a native function invoker. +pub(super) struct BoundNativeFunctionRefSlot { + pub(super) original: RuntimeCellHandle, + pub(super) slot: Box, + pub(super) target: EvalReferenceTarget, +} + /// One already evaluated PHP callback supported by the eval dispatcher. pub(super) enum EvaluatedCallable { Named(String), diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 1c8b49aec9..9ea5ca6879 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -29,7 +29,7 @@ pub(in crate::interpreter) fn eval_native_function_call_args( context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result { let evaluated_args = eval_call_arg_values(args, context, caller_scope, values)?; bind_evaluated_native_function_args(function, evaluated_args, context, values) } @@ -329,7 +329,7 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result { if native_function_variadic_index(function).is_some() { return bind_evaluated_native_variadic_function_args( function, @@ -347,14 +347,28 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( if !has_param_names { return Err(EvalStatus::RuntimeFatal); } - bind_dynamic_named_arg(function.param_names(), &mut bound_args, &name, arg.value)?; + bind_native_function_named_arg( + function, + None, + &mut bound_args, + &name, + arg.value, + arg.ref_target, + )?; } else { - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + bind_native_function_positional_arg( + function, + &mut bound_args, + None, + &mut next_positional, + arg.value, + arg.ref_target, + )?; } } - for (position, value) in bound_args.iter_mut().enumerate() { - if value.is_some() { + for (position, bound) in bound_args.iter_mut().enumerate() { + if bound.is_some() { continue; } if position < function.required_param_count() { @@ -363,7 +377,11 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( let Some(default) = function.param_default(position) else { return Err(EvalStatus::RuntimeFatal); }; - *value = Some(materialize_native_callable_default(default, context, values)?); + *bound = Some(BoundMethodArg { + value: materialize_native_callable_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); } let mut bound_args = bound_args @@ -371,7 +389,7 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( .collect::>>() .ok_or(EvalStatus::RuntimeFatal)?; apply_native_function_arg_types(function, None, &mut bound_args, context, values)?; - Ok(bound_args) + stage_native_function_invoker_args(function, None, bound_args, values) } /// Binds a native AOT variadic function while keeping the raw invoker argument layout. @@ -380,7 +398,7 @@ fn bind_evaluated_native_variadic_function_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result { let variadic_index = native_function_variadic_index(function).ok_or(EvalStatus::RuntimeFatal)?; let has_param_names = function.param_names().len() == function.param_count(); let mut regular_args = vec![None; variadic_index]; @@ -392,24 +410,42 @@ fn bind_evaluated_native_variadic_function_args( if !has_param_names { return Err(EvalStatus::RuntimeFatal); } - let Some(param_index) = - native_function_regular_param_index(function, variadic_index, &name) - else { - return Err(EvalStatus::RuntimeFatal); - }; - if regular_args[param_index].is_some() { + if native_function_regular_param_index(function, variadic_index, &name).is_none() { return Err(EvalStatus::RuntimeFatal); } - regular_args[param_index] = Some(arg.value); + bind_native_function_named_arg( + function, + Some(variadic_index), + &mut regular_args, + &name, + arg.value, + arg.ref_target, + )?; } else if next_positional < variadic_index { - bind_dynamic_positional_arg(&mut regular_args, &mut next_positional, arg.value)?; + bind_native_function_positional_arg( + function, + &mut regular_args, + Some(variadic_index), + &mut next_positional, + arg.value, + arg.ref_target, + )?; } else { - variadic_args.push(arg.value); + let ref_target = native_function_parameter_ref_target( + function, + Some(variadic_index), + arg.ref_target, + )?; + variadic_args.push(BoundMethodArg { + value: arg.value, + ref_target, + variadic_ref_targets: Vec::new(), + }); } } - for (position, value) in regular_args.iter_mut().enumerate() { - if value.is_some() { + for (position, bound) in regular_args.iter_mut().enumerate() { + if bound.is_some() { continue; } if position < function.required_param_count() { @@ -418,7 +454,11 @@ fn bind_evaluated_native_variadic_function_args( let Some(default) = function.param_default(position) else { return Err(EvalStatus::RuntimeFatal); }; - *value = Some(materialize_native_callable_default(default, context, values)?); + *bound = Some(BoundMethodArg { + value: materialize_native_callable_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); } let mut bound_args = regular_args @@ -433,18 +473,18 @@ fn bind_evaluated_native_variadic_function_args( context, values, )?; - Ok(bound_args) + stage_native_function_invoker_args(function, Some(variadic_index), bound_args, values) } /// Applies registered native AOT function parameter types after argument binding. fn apply_native_function_arg_types( function: &NativeFunction, variadic_index: Option, - bound_args: &mut [RuntimeCellHandle], + bound_args: &mut [BoundMethodArg], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { - for (position, value) in bound_args.iter_mut().enumerate() { + for (position, bound_arg) in bound_args.iter_mut().enumerate() { let param_index = if variadic_index.is_some_and(|index| position >= index) { variadic_index.ok_or(EvalStatus::RuntimeFatal)? } else { @@ -453,16 +493,130 @@ fn apply_native_function_arg_types( let Some(param_type) = function.param_type(param_index) else { continue; }; - *value = eval_method_parameter_value(param_type, *value, context, values)?; + bound_arg.value = eval_method_parameter_value(param_type, bound_arg.value, context, values)?; + } + Ok(()) +} + +/// Binds one named native AOT function argument to a non-variadic parameter slot. +fn bind_native_function_named_arg( + function: &NativeFunction, + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, + ref_target: Option, +) -> Result<(), EvalStatus> { + let Some(param_index) = native_function_named_param_index(function, variadic_index, name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); } + let ref_target = native_function_parameter_ref_target(function, Some(param_index), ref_target)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); Ok(()) } +/// Binds one positional native AOT function argument to the next fixed parameter. +fn bind_native_function_positional_arg( + function: &NativeFunction, + bound_args: &mut [Option], + variadic_index: Option, + next_positional: &mut usize, + value: RuntimeCellHandle, + ref_target: Option, +) -> Result<(), EvalStatus> { + let param_index = *next_positional; + if variadic_index.is_some_and(|index| param_index >= index) + || param_index >= bound_args.len() + || bound_args[param_index].is_some() + { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = native_function_parameter_ref_target(function, Some(param_index), ref_target)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) +} + +/// Returns the caller writeback target required by a native function by-reference parameter. +fn native_function_parameter_ref_target( + function: &NativeFunction, + param_index: Option, + ref_target: Option, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !function.param_by_ref(param_index) { + return Ok(None); + } + ref_target.map(Some).ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts bound values into descriptor-invoker arguments, staging Mixed by-reference slots. +fn stage_native_function_invoker_args( + function: &NativeFunction, + variadic_index: Option, + bound_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut invoker_values = Vec::with_capacity(bound_args.len()); + let mut ref_slots = Vec::new(); + for (position, bound_arg) in bound_args.into_iter().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + if !function.param_by_ref(param_index) { + invoker_values.push(bound_arg.value); + continue; + } + let target = bound_arg.ref_target.ok_or(EvalStatus::RuntimeFatal)?; + let original = bound_arg.value; + let mut slot = Box::new(original); + let marker = values.invoker_ref_cell(slot.as_mut() as *mut RuntimeCellHandle)?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot { + original, + slot, + target, + }); + } + Ok(BoundNativeFunctionArgs { + values: invoker_values, + ref_slots, + }) +} + /// Returns the variadic parameter index for a native AOT function, if registered. fn native_function_variadic_index(function: &NativeFunction) -> Option { (0..function.param_count()).find(|index| function.param_variadic(*index)) } +/// Returns the native function parameter index for one named argument. +fn native_function_named_param_index( + function: &NativeFunction, + variadic_index: Option, + name: &str, +) -> Option { + function + .param_names() + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + /// Returns the non-variadic native function parameter index for one named argument. fn native_function_regular_param_index( function: &NativeFunction, @@ -1449,10 +1603,10 @@ pub(super) fn eval_native_function( eval_native_function_with_values(function, evaluated_args, context, values) } -/// Invokes a registered AOT function after its positional arguments are prepared. +/// Invokes a registered AOT function after its arguments have been bound and staged. pub(super) fn eval_native_function_with_values( function: NativeFunction, - evaluated_args: Vec, + bound_args: BoundNativeFunctionArgs, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -1460,23 +1614,44 @@ pub(super) fn eval_native_function_with_values( return Err(EvalStatus::RuntimeFatal); } let variadic_index = native_function_variadic_index(&function); - if variadic_index.is_none() && evaluated_args.len() != function.param_count() { + if variadic_index.is_none() && bound_args.values.len() != function.param_count() { return Err(EvalStatus::RuntimeFatal); } if let Some(variadic_index) = variadic_index { - if evaluated_args.len() < function.required_param_count().min(variadic_index) { + if bound_args.values.len() < function.required_param_count().min(variadic_index) { return Err(EvalStatus::RuntimeFatal); } } - let arg_array = values.array_new(evaluated_args.len())?; - for (index, value) in evaluated_args.into_iter().enumerate() { + let arg_array = values.array_new(bound_args.values.len())?; + for (index, value) in bound_args.values.iter().copied().enumerate() { let index = values.int(index as i64)?; let _ = values.array_set(arg_array, index, value)?; } let result = unsafe { function.call(arg_array) }; values.release(arg_array)?; + write_back_native_function_ref_args(&bound_args, context, values)?; if result.is_null() { return Err(EvalStatus::RuntimeFatal); } eval_declared_native_return_value(function.return_type(), None, None, result, context, values) } + +/// Writes changed staged native-function by-reference slots back to eval caller targets. +fn write_back_native_function_ref_args( + bound_args: &BoundNativeFunctionArgs, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for ref_slot in &bound_args.ref_slots { + let value = *ref_slot.slot; + if value == ref_slot.original { + continue; + } + let current = eval_reference_target_value(&ref_slot.target, context, values)?; + if current == value { + continue; + } + write_back_method_ref_target(&ref_slot.target, value, context, values)?; + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 68de8d8cc3..9cf8789699 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -57,8 +57,9 @@ use constant_eval::*; use constants::*; pub use control::EvalOutcome; use control::{ - BoundMethodArg, EvalArraySpliceDirectArgs, EvalControl, EvalPredefinedConstant, - EvalSprintfSpec, EvaluatedCallArg, EvaluatedCallable, + BoundMethodArg, BoundNativeFunctionArgs, BoundNativeFunctionRefSlot, + EvalArraySpliceDirectArgs, EvalControl, EvalPredefinedConstant, EvalSprintfSpec, + EvaluatedCallArg, EvaluatedCallable, }; use core_builtins::*; use debug_output::*; diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index a3dcf5f401..a49ebc5b61 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -349,6 +349,12 @@ pub trait RuntimeValueOps { /// Returns the concrete boxed Mixed runtime tag after unwrapping nested Mixed cells. fn type_tag(&mut self, value: RuntimeCellHandle) -> Result; + /// Creates an invoker-only by-reference marker for a staged Mixed slot. + fn invoker_ref_cell( + &mut self, + slot: *mut RuntimeCellHandle, + ) -> Result; + /// Returns the unboxed object payload pointer used for PHP object identity. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result; diff --git a/crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs index a69340e6a2..77e03dab4e 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/cell_ops.rs @@ -25,6 +25,7 @@ impl FakeOps { FakeValue::Assoc(_) => EVAL_TAG_ASSOC, FakeValue::Object(_) | FakeValue::Iterator { .. } => EVAL_TAG_OBJECT, FakeValue::Resource(_) => EVAL_TAG_RESOURCE, + FakeValue::InvokerRefCell(_) => 11, FakeValue::Null => EVAL_TAG_NULL, }) } @@ -85,6 +86,7 @@ impl FakeOps { FakeValue::Assoc(value) => !value.is_empty(), FakeValue::Object(_) | FakeValue::Iterator { .. } => true, FakeValue::Resource(_) => true, + FakeValue::InvokerRefCell(_) => true, }) } } diff --git a/crates/elephc-magician/src/interpreter/tests/support/conversions.rs b/crates/elephc-magician/src/interpreter/tests/support/conversions.rs index 538f762311..426ed91ebe 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/conversions.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/conversions.rs @@ -95,6 +95,7 @@ impl FakeOps { FakeValue::Assoc(value) => value.len() as f64, FakeValue::Object(_) | FakeValue::Iterator { .. } => 1.0, FakeValue::Resource(value) => (*value + 1) as f64, + FakeValue::InvokerRefCell(_) => 0.0, } } @@ -116,6 +117,7 @@ impl FakeOps { FakeValue::Assoc(value) => !value.is_empty(), FakeValue::Object(_) | FakeValue::Iterator { .. } => true, FakeValue::Resource(_) => true, + FakeValue::InvokerRefCell(_) => true, } } @@ -133,6 +135,7 @@ impl FakeOps { FakeValue::Assoc(_) => "Array".to_string(), FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + FakeValue::InvokerRefCell(_) => "[invoker-ref]".to_string(), } } @@ -158,6 +161,7 @@ impl FakeOps { FakeValue::Array(_) | FakeValue::Assoc(_) => "Array".to_string(), FakeValue::Object(_) | FakeValue::Iterator { .. } => "Object".to_string(), FakeValue::Resource(value) => format!("Resource id #{}", value + 1), + FakeValue::InvokerRefCell(_) => "[invoker-ref]".to_string(), } } } diff --git a/crates/elephc-magician/src/interpreter/tests/support/mod.rs b/crates/elephc-magician/src/interpreter/tests/support/mod.rs index b0b59d733f..1daee7982b 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/mod.rs @@ -46,6 +46,7 @@ pub(super) enum FakeValue { Object(Vec<(String, RuntimeCellHandle)>), Iterator { len: i64, position: i64 }, Resource(i64), + InvokerRefCell(usize), } /// Test runtime hooks that allocate stable fake handles and record echo output. diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index 6e260a8771..bcdeffc65f 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -386,6 +386,13 @@ impl RuntimeValueOps for FakeOps { fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { self.runtime_type_tag(value) } + /// Creates a fake invoker-only by-reference marker. + fn invoker_ref_cell( + &mut self, + slot: *mut RuntimeCellHandle, + ) -> Result { + Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) + } /// Returns the fake object handle as a stable object identity. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { self.runtime_object_identity(object) diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 425d03c39b..f26038c5b3 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -11,7 +11,7 @@ //! - Symbols are provided by the main elephc runtime object when eval is enabled. //! - Null return pointers are translated to `EvalStatus::RuntimeFatal` by callers. -use crate::value::RuntimeCell; +use crate::value::{RuntimeCell, RuntimeCellHandle}; #[cfg(not(test))] unsafe extern "C" { @@ -258,6 +258,9 @@ unsafe extern "C" { pub(super) fn __elephc_eval_value_is_array_like(value: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_value_is_null(value: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_value_type_tag(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_invoker_ref_cell( + slot: *mut RuntimeCellHandle, + ) -> *mut RuntimeCell; /// Returns the unboxed object payload pointer for object-tagged eval values. pub(super) fn __elephc_eval_value_object_identity(value: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_warning(message_ptr: *const u8, message_len: u64); diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 08aeffe765..499cfcd6b5 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -747,6 +747,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_type_tag(value.as_ptr()) }) } + /// Creates an invoker-only by-reference marker for a staged Mixed slot. + fn invoker_ref_cell( + &mut self, + slot: *mut RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_invoker_ref_cell(slot) }) + } + /// Returns the unboxed object payload pointer for SPL object identity builtins. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { let identity = unsafe { __elephc_eval_value_object_identity(object.as_ptr()) }; diff --git a/crates/elephc-magician/src/value.rs b/crates/elephc-magician/src/value.rs index 52c39f6d8c..8d3e736c2a 100644 --- a/crates/elephc-magician/src/value.rs +++ b/crates/elephc-magician/src/value.rs @@ -15,6 +15,7 @@ use std::ffi::c_void; pub type RuntimeCell = c_void; /// Wraps an opaque runtime cell pointer without taking ownership by itself. +#[repr(transparent)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct RuntimeCellHandle { ptr: *mut RuntimeCell, diff --git a/docs/php/eval.md b/docs/php/eval.md index 83d47ae382..a0bfd6880c 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -110,7 +110,13 @@ after the eval barrier, and from string-literal `call_user_func()` / `call_user_func_array()` paths. Eval-declared functions and registered AOT global user functions support positional, named, and spread arguments inside eval fragments. String keys in unpacked argument arrays bind as named -parameters. +parameters. Direct calls and variable-function calls can also invoke registered +AOT global user functions when their by-reference parameters use generated +`mixed`/union-style boxed storage; typed scalar, string, array, iterable, +object, and other raw-storage by-reference free-function parameters remain +metadata-only until the function bridge has typed staging/writeback for those +ABI layouts. `call_user_func()` remains by-value for registered AOT +free-function by-reference parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share the eval callable dispatcher for supported builtins, @@ -525,13 +531,16 @@ the native bridge with parameter names, supported defaults, named arguments, and indexed or string-keyed runtime argument arrays. Direct eval calls, variable function calls, and `call_user_func()` paths can also invoke registered generated/AOT free functions with positional variadic tails when the generated -signature has no by-reference parameters. Registered generated/AOT +signature has no by-reference parameters. Direct and variable-function calls +can additionally invoke generated/AOT free functions whose by-reference +parameters use boxed Mixed/union storage. Registered generated/AOT free-function parameter names, declared types, return types, by-reference and variadic flags, required/optional counts, and supported defaults are also exposed through `ReflectionFunction` / `ReflectionParameter` metadata. -Unsupported generated/AOT free-function bridge shapes, such as by-reference -function parameters and native-only ABI layouts, remain reflectable as metadata -but are not invocable through eval. Supported callable-builtin invocation is +Unsupported generated/AOT free-function bridge shapes, such as typed +raw-storage by-reference parameters and native-only ABI layouts, remain +reflectable as metadata but are not invocable through eval. Supported +callable-builtin invocation is covered by the general Reflection support documented in `docs/php/classes.md`. Defaulted eval method parameters are @@ -871,8 +880,8 @@ static-method, and constructor fallback can also bind by-reference lvalue metadata. Generated AOT bridge dispatch supports visibility-checked method, static-method, and constructor signatures whose generated ABI storage is scalar/string, callable descriptor, boxed Mixed/union, array/hash, iterable, or -object, plus non-by-reference free-function signatures using the descriptor -invoker ABI. +object, plus free-function signatures using the descriptor invoker ABI when +by-reference parameters, if any, use boxed Mixed/union storage. Registered type specs validate nullable and union members, intersection object parameters, and intersection object returns before or after the generated AOT call. By-reference method and constructor parameters remain limited to the @@ -903,9 +912,9 @@ limit there is the supported type slice rather than a fixed parameter count. Generated/AOT free-function and method type metadata, return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered -metadata slices, while unsupported native-only bridge shapes and by-reference -free-function bridge shapes remain metadata-only rather than invocable through -eval. +metadata slices, while unsupported native-only bridge shapes and typed +raw-storage by-reference free-function bridge shapes remain metadata-only +rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 407009e0ff..ee7958e6aa 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -2390,10 +2390,14 @@ fn function_has_eval_metadata(function: &Function) -> bool { /// Returns true when eval can dispatch a native function through the generated bridge. fn function_signature_can_bridge_with_eval(function: &Function) -> bool { - function - .params - .iter() - .all(|param| !param.by_ref) + function.params.iter().all(|param| { + !param.by_ref || eval_native_function_ref_param_supported(¶m.php_type) + }) +} + +/// Returns true when a native function by-reference parameter can use a Mixed staging slot. +fn eval_native_function_ref_param_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Mixed) } /// Returns true when eval can dispatch a native method through the generated bridge. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 4e876c43ea..4eced99a09 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -15,6 +15,8 @@ use crate::codegen_support::emit::Emitter; use crate::codegen_support::platform::Arch; const X86_64_HEAP_MAGIC_HI32: u64 = 0x454C5048; +const EVAL_RUNTIME_TAG_MIXED: i64 = 7; +const INVOKER_ARG_REF_CELL_TAG: i64 = 11; /// Builds the x86_64 instruction that installs the Mixed heap-kind marker. fn x86_64_mixed_heap_kind_instruction() -> String { @@ -760,6 +762,12 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the type-tag wrapper frame emitter.instruction("ret"); // return the unboxed runtime tag to Rust + label_c_global(emitter, "__elephc_eval_value_invoker_ref_cell"); + emitter.instruction("mov x1, x0"); // pass the staged Mixed slot address as marker payload + emitter.instruction(&format!("mov x0, #{}", INVOKER_ARG_REF_CELL_TAG)); // runtime tag 11 marks descriptor-invoker by-reference args + emitter.instruction(&format!("mov x2, #{}", EVAL_RUNTIME_TAG_MIXED)); // source tag 7 tells invoker fallback paths the slot stores Mixed + emitter.instruction("b __rt_mixed_from_value"); // box the marker cell and return it to Rust + label_c_global(emitter, "__elephc_eval_value_object_identity"); emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the object cell emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox @@ -2243,6 +2251,11 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the unboxed runtime tag to Rust + label_c_global(emitter, "__elephc_eval_value_invoker_ref_cell"); + emitter.instruction(&format!("mov rax, {}", INVOKER_ARG_REF_CELL_TAG)); // runtime tag 11 marks descriptor-invoker by-reference args + emitter.instruction(&format!("mov rsi, {}", EVAL_RUNTIME_TAG_MIXED)); // source tag 7 tells invoker fallback paths the slot stores Mixed + emitter.instruction("jmp __rt_mixed_from_value"); // box the marker cell and return it to Rust + label_c_global(emitter, "__elephc_eval_value_object_identity"); emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable object-identity wrapper frame diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ed51484589..eca627e0cc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5274,6 +5274,27 @@ eval('echo native_eval_spread(...["L", "R"]);'); assert_eq!(out, "L:R"); } +/// Verifies eval can dispatch AOT user functions with untyped by-reference params. +#[test] +fn test_eval_fragment_can_call_native_user_function_with_mixed_by_ref_arg() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 13:31:58 +0200 Subject: [PATCH 0929/1208] Preserve ownership for eval function ref writeback --- .../elephc-magician/src/interpreter/dynamic_functions.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 9ea5ca6879..6830b36f97 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1651,7 +1651,13 @@ fn write_back_native_function_ref_args( if current == value { continue; } - write_back_method_ref_target(&ref_slot.target, value, context, values)?; + eval_write_direct_ref_target( + &ref_slot.target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; } Ok(()) } From ccc19f2359023ec0003da041a580098dc1f46273 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 14:00:20 +0200 Subject: [PATCH 0930/1208] Invoke AOT scalar by-ref functions from eval --- .../src/interpreter/control.rs | 18 +++- .../src/interpreter/dynamic_functions.rs | 91 +++++++++++++++---- .../src/interpreter/runtime_ops.rs | 19 ++++ .../interpreter/tests/support/runtime_ops.rs | 30 ++++++ .../src/runtime_hooks/externs.rs | 11 +++ .../elephc-magician/src/runtime_hooks/ops.rs | 23 +++++ docs/php/eval.md | 37 ++++---- src/codegen/lower_inst/builtins/eval.rs | 5 +- src/codegen_support/runtime/eval_bridge.rs | 39 ++++++++ tests/codegen/eval.rs | 25 +++++ 10 files changed, 259 insertions(+), 39 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index 2af0745bac..6eba86d9d9 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -49,11 +49,19 @@ pub(super) struct BoundNativeFunctionArgs { pub(super) ref_slots: Vec, } -/// One staged Mixed-slot reference passed to a native function invoker. -pub(super) struct BoundNativeFunctionRefSlot { - pub(super) original: RuntimeCellHandle, - pub(super) slot: Box, - pub(super) target: EvalReferenceTarget, +/// One staged by-reference slot passed to a native function invoker. +pub(super) enum BoundNativeFunctionRefSlot { + Mixed { + original: RuntimeCellHandle, + slot: Box, + target: EvalReferenceTarget, + }, + RawWord { + tag: u64, + original: u64, + slot: Box, + target: EvalReferenceTarget, + }, } /// One already evaluated PHP callback supported by the eval dispatcher. diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 6830b36f97..af9b805e95 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -9,6 +9,7 @@ //! - Static locals are persisted through `ElephcEvalContext` after function execution. use super::*; +use std::ffi::c_void; /// Evaluates an eval-declared user function with PHP-style argument binding. pub(in crate::interpreter) fn eval_dynamic_function( @@ -563,7 +564,7 @@ fn native_function_parameter_ref_target( ref_target.map(Some).ok_or(EvalStatus::RuntimeFatal) } -/// Converts bound values into descriptor-invoker arguments, staging Mixed by-reference slots. +/// Converts bound values into descriptor-invoker arguments, staging by-reference slots. fn stage_native_function_invoker_args( function: &NativeFunction, variadic_index: Option, @@ -583,11 +584,25 @@ fn stage_native_function_invoker_args( continue; } let target = bound_arg.ref_target.ok_or(EvalStatus::RuntimeFatal)?; + if let Some(tag) = native_function_raw_word_ref_tag(function.param_type(param_index)) { + let original = values.raw_value_word(bound_arg.value)?; + let mut slot = Box::new(original); + let marker = + values.invoker_raw_ref_cell(slot.as_mut() as *mut u64 as *mut c_void, tag)?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::RawWord { + tag, + original, + slot, + target, + }); + continue; + } let original = bound_arg.value; let mut slot = Box::new(original); let marker = values.invoker_ref_cell(slot.as_mut() as *mut RuntimeCellHandle)?; invoker_values.push(marker); - ref_slots.push(BoundNativeFunctionRefSlot { + ref_slots.push(BoundNativeFunctionRefSlot::Mixed { original, slot, target, @@ -599,6 +614,23 @@ fn stage_native_function_invoker_args( }) } +/// Returns the runtime tag for a by-reference parameter that can use one raw word. +fn native_function_raw_word_ref_tag(param_type: Option<&EvalParameterType>) -> Option { + let param_type = param_type?; + if param_type.allows_null() + || param_type.is_intersection() + || param_type.variants().len() != 1 + { + return None; + } + match param_type.variants().first()? { + EvalParameterTypeVariant::Bool => Some(EVAL_TAG_BOOL), + EvalParameterTypeVariant::Float => Some(EVAL_TAG_FLOAT), + EvalParameterTypeVariant::Int => Some(EVAL_TAG_INT), + _ => None, + } +} + /// Returns the variadic parameter index for a native AOT function, if registered. fn native_function_variadic_index(function: &NativeFunction) -> Option { (0..function.param_count()).find(|index| function.param_variadic(*index)) @@ -1643,21 +1675,48 @@ fn write_back_native_function_ref_args( values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { for ref_slot in &bound_args.ref_slots { - let value = *ref_slot.slot; - if value == ref_slot.original { - continue; - } - let current = eval_reference_target_value(&ref_slot.target, context, values)?; - if current == value { - continue; + match ref_slot { + BoundNativeFunctionRefSlot::Mixed { + original, + slot, + target, + } => { + let value = **slot; + if value == *original { + continue; + } + let current = eval_reference_target_value(target, context, values)?; + if current == value { + continue; + } + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + BoundNativeFunctionRefSlot::RawWord { + tag, + original, + slot, + target, + } => { + let word = **slot; + if word == *original { + continue; + } + let value = values.raw_word_value(*tag, word)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } } - eval_write_direct_ref_target( - &ref_slot.target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; } Ok(()) } diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index a49ebc5b61..b5ec3c223f 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -10,6 +10,8 @@ //! - Implementors own allocation, retain/release, casting, arithmetic, and target runtime calls. //! - Tag constants mirror boxed Mixed runtime tags consumed by eval-only helpers. +use std::ffi::c_void; + use crate::errors::EvalStatus; use crate::eval_ir::EvalBinOp; use crate::value::RuntimeCellHandle; @@ -355,6 +357,23 @@ pub trait RuntimeValueOps { slot: *mut RuntimeCellHandle, ) -> Result; + /// Creates an invoker-only by-reference marker for a staged raw one-word slot. + fn invoker_raw_ref_cell( + &mut self, + slot: *mut c_void, + source_tag: u64, + ) -> Result; + + /// Extracts the low raw payload word from a boxed scalar Mixed cell. + fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result; + + /// Boxes one raw scalar payload word as a Mixed cell with the provided runtime tag. + fn raw_word_value( + &mut self, + source_tag: u64, + word: u64, + ) -> Result; + /// Returns the unboxed object payload pointer used for PHP object identity. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index bcdeffc65f..381f804996 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -393,6 +393,36 @@ impl RuntimeValueOps for FakeOps { ) -> Result { Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) } + /// Creates a fake invoker-only raw by-reference marker. + fn invoker_raw_ref_cell( + &mut self, + slot: *mut std::ffi::c_void, + _source_tag: u64, + ) -> Result { + Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) + } + /// Extracts one fake scalar payload word for raw by-reference staging. + fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Bool(value) => u64::from(value), + FakeValue::Float(value) => value.to_bits(), + FakeValue::Int(value) => value as u64, + _ => 0, + }) + } + /// Boxes one fake scalar raw payload word with the provided runtime tag. + fn raw_word_value( + &mut self, + source_tag: u64, + word: u64, + ) -> Result { + match source_tag { + EVAL_TAG_INT => self.runtime_int(word as i64), + EVAL_TAG_FLOAT => self.runtime_float(f64::from_bits(word)), + EVAL_TAG_BOOL => self.runtime_bool_value(word != 0), + _ => Err(EvalStatus::RuntimeFatal), + } + } /// Returns the fake object handle as a stable object identity. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { self.runtime_object_identity(object) diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index f26038c5b3..11e249ab92 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -11,6 +11,8 @@ //! - Symbols are provided by the main elephc runtime object when eval is enabled. //! - Null return pointers are translated to `EvalStatus::RuntimeFatal` by callers. +use std::ffi::c_void; + use crate::value::{RuntimeCell, RuntimeCellHandle}; #[cfg(not(test))] @@ -261,6 +263,15 @@ unsafe extern "C" { pub(super) fn __elephc_eval_value_invoker_ref_cell( slot: *mut RuntimeCellHandle, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_invoker_raw_ref_cell( + slot: *mut c_void, + source_tag: u64, + ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_raw_word(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_from_raw_word( + source_tag: u64, + word: u64, + ) -> *mut RuntimeCell; /// Returns the unboxed object payload pointer for object-tagged eval values. pub(super) fn __elephc_eval_value_object_identity(value: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_warning(message_ptr: *const u8, message_len: u64); diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 499cfcd6b5..e9e9f029ae 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -755,6 +755,29 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_invoker_ref_cell(slot) }) } + /// Creates an invoker-only by-reference marker for a staged raw one-word slot. + fn invoker_raw_ref_cell( + &mut self, + slot: *mut std::ffi::c_void, + source_tag: u64, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_invoker_raw_ref_cell(slot, source_tag) }) + } + + /// Extracts the low raw payload word from a boxed scalar Mixed cell. + fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_raw_word(value.as_ptr()) }) + } + + /// Boxes one raw scalar payload word as a Mixed cell with the provided runtime tag. + fn raw_word_value( + &mut self, + source_tag: u64, + word: u64, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_word(source_tag, word) }) + } + /// Returns the unboxed object payload pointer for SPL object identity builtins. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { let identity = unsafe { __elephc_eval_value_object_identity(object.as_ptr()) }; diff --git a/docs/php/eval.md b/docs/php/eval.md index a0bfd6880c..5711870b01 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -112,11 +112,12 @@ global user functions support positional, named, and spread arguments inside eval fragments. String keys in unpacked argument arrays bind as named parameters. Direct calls and variable-function calls can also invoke registered AOT global user functions when their by-reference parameters use generated -`mixed`/union-style boxed storage; typed scalar, string, array, iterable, -object, and other raw-storage by-reference free-function parameters remain -metadata-only until the function bridge has typed staging/writeback for those -ABI layouts. `call_user_func()` remains by-value for registered AOT -free-function by-reference parameters. +`mixed`/union-style boxed storage or one-word scalar raw storage (`int`, `bool`, +or `float`). Nullable scalar, string, array, iterable, object, and other +raw-storage by-reference free-function parameters remain metadata-only until +the function bridge has typed staging/writeback for those ABI layouts. +`call_user_func()` remains by-value for registered AOT free-function +by-reference parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share the eval callable dispatcher for supported builtins, @@ -533,14 +534,15 @@ function calls, and `call_user_func()` paths can also invoke registered generated/AOT free functions with positional variadic tails when the generated signature has no by-reference parameters. Direct and variable-function calls can additionally invoke generated/AOT free functions whose by-reference -parameters use boxed Mixed/union storage. Registered generated/AOT -free-function parameter names, declared types, return types, by-reference and -variadic flags, required/optional counts, and supported defaults are also -exposed through `ReflectionFunction` / `ReflectionParameter` metadata. -Unsupported generated/AOT free-function bridge shapes, such as typed -raw-storage by-reference parameters and native-only ABI layouts, remain -reflectable as metadata but are not invocable through eval. Supported -callable-builtin invocation is +parameters use boxed Mixed/union storage or one-word scalar raw storage +(`int`, `bool`, or `float`). Registered generated/AOT free-function parameter +names, declared types, return types, by-reference and variadic flags, +required/optional counts, and supported defaults are also exposed through +`ReflectionFunction` / `ReflectionParameter` metadata. Unsupported +generated/AOT free-function bridge shapes, such as nullable tagged-scalar, +string, array, iterable, object, native-only ABI layouts, and other raw-storage +by-reference parameters, remain reflectable as metadata but are not invocable +through eval. Supported callable-builtin invocation is covered by the general Reflection support documented in `docs/php/classes.md`. Defaulted eval method parameters are @@ -881,7 +883,8 @@ metadata. Generated AOT bridge dispatch supports visibility-checked method, static-method, and constructor signatures whose generated ABI storage is scalar/string, callable descriptor, boxed Mixed/union, array/hash, iterable, or object, plus free-function signatures using the descriptor invoker ABI when -by-reference parameters, if any, use boxed Mixed/union storage. +by-reference parameters, if any, use boxed Mixed/union storage or raw one-word +scalar storage (`int`, `bool`, or `float`). Registered type specs validate nullable and union members, intersection object parameters, and intersection object returns before or after the generated AOT call. By-reference method and constructor parameters remain limited to the @@ -912,9 +915,9 @@ limit there is the supported type slice rather than a fixed parameter count. Generated/AOT free-function and method type metadata, return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered -metadata slices, while unsupported native-only bridge shapes and typed -raw-storage by-reference free-function bridge shapes remain metadata-only -rather than invocable through eval. +metadata slices, while unsupported native-only bridge shapes and raw-storage +by-reference free-function bridge shapes beyond `int`, `bool`, and `float` +remain metadata-only rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index ee7958e6aa..6606e6bff6 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -2397,7 +2397,10 @@ fn function_signature_can_bridge_with_eval(function: &Function) -> bool { /// Returns true when a native function by-reference parameter can use a Mixed staging slot. fn eval_native_function_ref_param_supported(ty: &PhpType) -> bool { - matches!(ty.codegen_repr(), PhpType::Mixed) + matches!( + ty.codegen_repr(), + PhpType::Mixed | PhpType::Int | PhpType::Bool | PhpType::Float + ) } /// Returns true when eval can dispatch a native method through the generated bridge. diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index 4eced99a09..c457a97a32 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -768,6 +768,26 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction(&format!("mov x2, #{}", EVAL_RUNTIME_TAG_MIXED)); // source tag 7 tells invoker fallback paths the slot stores Mixed emitter.instruction("b __rt_mixed_from_value"); // box the marker cell and return it to Rust + label_c_global(emitter, "__elephc_eval_value_invoker_raw_ref_cell"); + emitter.instruction("mov x2, x1"); // pass the staged raw slot source tag as marker metadata + emitter.instruction("mov x1, x0"); // pass the staged raw slot address as marker payload + emitter.instruction(&format!("mov x0, #{}", INVOKER_ARG_REF_CELL_TAG)); // runtime tag 11 marks descriptor-invoker by-reference args + emitter.instruction("b __rt_mixed_from_value"); // box the marker cell and return it to Rust + + label_c_global(emitter, "__elephc_eval_value_raw_word"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the scalar cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a frame pointer for the wrapper call + emitter.instruction("bl __rt_mixed_unbox"); // expose the boxed scalar payload words + emitter.instruction("mov x0, x1"); // return the scalar low payload word to Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore caller frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the wrapper frame + emitter.instruction("ret"); // return the raw payload word + + label_c_global(emitter, "__elephc_eval_value_from_raw_word"); + emitter.instruction("mov x2, xzr"); // raw one-word scalar payloads have no high payload word + emitter.instruction("b __rt_mixed_from_value"); // box the raw scalar payload and return it to Rust + label_c_global(emitter, "__elephc_eval_value_object_identity"); emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the object cell emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox @@ -2256,6 +2276,25 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction(&format!("mov rsi, {}", EVAL_RUNTIME_TAG_MIXED)); // source tag 7 tells invoker fallback paths the slot stores Mixed emitter.instruction("jmp __rt_mixed_from_value"); // box the marker cell and return it to Rust + label_c_global(emitter, "__elephc_eval_value_invoker_raw_ref_cell"); + emitter.instruction(&format!("mov rax, {}", INVOKER_ARG_REF_CELL_TAG)); // runtime tag 11 marks descriptor-invoker by-reference args + emitter.instruction("jmp __rt_mixed_from_value"); // box the marker cell and return it to Rust + + label_c_global(emitter, "__elephc_eval_value_raw_word"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed scalar cell into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // expose the boxed scalar payload words + emitter.instruction("mov rax, rdi"); // return the scalar low payload word to Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the raw payload word + + label_c_global(emitter, "__elephc_eval_value_from_raw_word"); + emitter.instruction("mov rax, rdi"); // move the runtime tag into the mixed boxing tag register + emitter.instruction("mov rdi, rsi"); // move the raw scalar word into the low payload register + emitter.instruction("xor esi, esi"); // raw one-word scalar payloads have no high payload word + emitter.instruction("jmp __rt_mixed_from_value"); // box the raw scalar payload and return it to Rust + label_c_global(emitter, "__elephc_eval_value_object_identity"); emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable object-identity wrapper frame diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eca627e0cc..cc76e1ea5f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5295,6 +5295,31 @@ return $first . ":" . $value;'); assert_eq!(out, "ret:AB:ABCD"); } +/// Verifies eval can dispatch AOT user functions with raw scalar by-reference params. +#[test] +fn test_eval_fragment_can_call_native_user_function_with_scalar_by_ref_args() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 14:29:18 +0200 Subject: [PATCH 0931/1208] Fix eval AOT refcounted ref writeback --- src/codegen/eval_ref_arg_helpers.rs | 262 +++++++++++++++++++++++++++- 1 file changed, 261 insertions(+), 1 deletion(-) diff --git a/src/codegen/eval_ref_arg_helpers.rs b/src/codegen/eval_ref_arg_helpers.rs index 7feab0aed3..d864f1e73d 100644 --- a/src/codegen/eval_ref_arg_helpers.rs +++ b/src/codegen/eval_ref_arg_helpers.rs @@ -15,7 +15,7 @@ //! boxed again during writeback. use crate::codegen::emit::Emitter; -use crate::codegen::{abi, emit_box_current_value_as_mixed}; +use crate::codegen::{abi, emit_box_current_value_as_mixed, runtime_value_tag}; use crate::types::{FunctionSig, PhpType}; /// Describes the stack storage for one eval-supplied by-reference argument. @@ -187,12 +187,24 @@ fn emit_aarch64_write_back_typed_ref_arg( stack_offset: usize, label_prefix: &str, ) { + if typed_ref_arg_is_refcounted_heap(&slot.param_ty) { + emit_aarch64_write_back_refcounted_typed_ref_arg( + emitter, + slot, + stack_offset, + label_prefix, + ); + return; + } + let done_label = format!("{}_ref_{}_typed_done", label_prefix, slot.param_index); + emit_aarch64_skip_unchanged_typed_ref_arg(emitter, slot, stack_offset, &done_label); emit_aarch64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); emit_box_current_value_as_mixed(emitter, &slot.param_ty); emitter.instruction("mov x10, x0"); // keep the newly boxed ref value available for cell replacement abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); emit_aarch64_release_typed_ref_slot(emitter, slot, stack_offset, label_prefix); + emitter.label(&done_label); } /// Boxes one x86_64 typed raw ref slot and replaces the original eval Mixed cell. @@ -202,12 +214,220 @@ fn emit_x86_64_write_back_typed_ref_arg( stack_offset: usize, label_prefix: &str, ) { + if typed_ref_arg_is_refcounted_heap(&slot.param_ty) { + emit_x86_64_write_back_refcounted_typed_ref_arg( + emitter, + slot, + stack_offset, + label_prefix, + ); + return; + } + let done_label = format!("{}_ref_{}_typed_done_x", label_prefix, slot.param_index); + emit_x86_64_skip_unchanged_typed_ref_arg(emitter, slot, stack_offset, &done_label); emit_x86_64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); emit_box_current_value_as_mixed(emitter, &slot.param_ty); emitter.instruction("mov r11, rax"); // keep the newly boxed ref value available for cell replacement abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); emit_x86_64_release_typed_ref_slot(emitter, slot, stack_offset, label_prefix); + emitter.label(&done_label); +} + +/// Writes one ARM64 refcounted raw ref slot back with borrowed/owned slot semantics. +fn emit_aarch64_write_back_refcounted_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let changed_label = format!( + "{}_ref_{}_typed_changed", + label_prefix, slot.param_index + ); + let unchanged_label = format!( + "{}_ref_{}_typed_unchanged", + label_prefix, slot.param_index + ); + let done_label = format!("{}_ref_{}_typed_done", label_prefix, slot.param_index); + emit_aarch64_branch_on_refcounted_raw_slot_change( + emitter, + slot, + stack_offset, + &changed_label, + &unchanged_label, + ); + emitter.label(&changed_label); + emit_aarch64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_box_current_value_as_mixed(emitter, &slot.param_ty); + emitter.instruction("mov x10, x0"); // keep the boxed replacement while updating the eval ref cell + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + if slot.raw_refcounted_owned { + emit_aarch64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "x9", "x10"); + } else { + emit_aarch64_replace_mixed_cell_without_releasing_old(emitter, "x9", "x10"); + } + emit_aarch64_release_refcounted_raw_slot_value(emitter, slot, stack_offset); + emitter.instruction(&format!("b {}", done_label)); // finish after transferring the changed raw ref payload + emitter.label(&unchanged_label); + if slot.raw_refcounted_owned { + emit_aarch64_release_refcounted_raw_slot_value(emitter, slot, stack_offset); + } + emitter.label(&done_label); +} + +/// Writes one x86_64 refcounted raw ref slot back with borrowed/owned slot semantics. +fn emit_x86_64_write_back_refcounted_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + label_prefix: &str, +) { + let changed_label = format!( + "{}_ref_{}_typed_changed_x", + label_prefix, slot.param_index + ); + let unchanged_label = format!( + "{}_ref_{}_typed_unchanged_x", + label_prefix, slot.param_index + ); + let done_label = format!("{}_ref_{}_typed_done_x", label_prefix, slot.param_index); + emit_x86_64_branch_on_refcounted_raw_slot_change( + emitter, + slot, + stack_offset, + &changed_label, + &unchanged_label, + ); + emitter.label(&changed_label); + emit_x86_64_load_typed_ref_slot(emitter, &slot.param_ty, stack_offset + slot.raw_offset); + emit_box_current_value_as_mixed(emitter, &slot.param_ty); + emitter.instruction("mov r11, rax"); // keep the boxed replacement while updating the eval ref cell + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + if slot.raw_refcounted_owned { + emit_x86_64_replace_mixed_cell(emitter, label_prefix, slot.param_index, "r10", "r11"); + } else { + emit_x86_64_replace_mixed_cell_without_releasing_old(emitter, "r10", "r11"); + } + emit_x86_64_release_refcounted_raw_slot_value(emitter, slot, stack_offset); + emitter.instruction(&format!("jmp {}", done_label)); // finish after transferring the changed raw ref payload + emitter.label(&unchanged_label); + if slot.raw_refcounted_owned { + emit_x86_64_release_refcounted_raw_slot_value(emitter, slot, stack_offset); + } + emitter.label(&done_label); +} + +/// Branches on whether an ARM64 refcounted raw ref slot differs from the eval cell. +fn emit_aarch64_branch_on_refcounted_raw_slot_change( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + changed_label: &str, + unchanged_label: &str, +) { + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("ldr x11, [x9, #8]"); // load the eval cell's current refcounted payload pointer + emitter.instruction("cmp x10, x11"); // did the native by-ref call replace the payload pointer? + emitter.instruction(&format!("b.ne {}", changed_label)); // changed raw slots need boxing and eval-cell replacement + emitter.instruction(&format!("b {}", unchanged_label)); // unchanged raw slots keep the existing eval cell payload +} + +/// Branches on whether an x86_64 refcounted raw ref slot differs from the eval cell. +fn emit_x86_64_branch_on_refcounted_raw_slot_change( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + changed_label: &str, + unchanged_label: &str, +) { + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("mov r9, QWORD PTR [r10 + 8]"); // load the eval cell's current refcounted payload pointer + emitter.instruction("cmp r11, r9"); // did the native by-ref call replace the payload pointer? + emitter.instruction(&format!("jne {}", changed_label)); // changed raw slots need boxing and eval-cell replacement + emitter.instruction(&format!("jmp {}", unchanged_label)); // unchanged raw slots keep the existing eval cell payload +} + +/// Releases the current ARM64 refcounted raw slot value. +fn emit_aarch64_release_refcounted_raw_slot_value( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, +) { + abi::emit_load_temporary_stack_slot(emitter, "x0", stack_offset + slot.raw_offset); + abi::emit_decref_if_refcounted(emitter, &slot.param_ty.codegen_repr()); +} + +/// Releases the current x86_64 refcounted raw slot value. +fn emit_x86_64_release_refcounted_raw_slot_value( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, +) { + abi::emit_load_temporary_stack_slot(emitter, "rax", stack_offset + slot.raw_offset); + abi::emit_decref_if_refcounted(emitter, &slot.param_ty.codegen_repr()); +} + +/// Skips ARM64 typed writeback when the raw slot still matches the original Mixed payload. +fn emit_aarch64_skip_unchanged_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + done_label: &str, +) { + let Some(expected_tag) = typed_ref_arg_unchanged_runtime_tag(&slot.param_ty) else { + return; + }; + let changed_label = format!("{}_changed", done_label); + abi::emit_load_temporary_stack_slot(emitter, "x9", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "x10", stack_offset + slot.raw_offset); + emitter.instruction("ldr x11, [x9]"); // load the original eval cell runtime tag before skipping writeback + emitter.instruction(&format!("cmp x11, #{}", expected_tag)); // only skip when the eval cell already has the target scalar tag + emitter.instruction(&format!("b.ne {}", changed_label)); // coerce or rewrite cells whose original tag differs + emitter.instruction("ldr x11, [x9, #8]"); // load the original eval cell scalar payload word + emitter.instruction("cmp x10, x11"); // did the native call leave the raw ref slot unchanged? + emitter.instruction(&format!("b.eq {}", done_label)); // keep the existing Mixed cell when no replacement is needed + emitter.label(&changed_label); +} + +/// Skips x86_64 typed writeback when the raw slot still matches the original Mixed payload. +fn emit_x86_64_skip_unchanged_typed_ref_arg( + emitter: &mut Emitter, + slot: &EvalRefArgSlot, + stack_offset: usize, + done_label: &str, +) { + let Some(expected_tag) = typed_ref_arg_unchanged_runtime_tag(&slot.param_ty) else { + return; + }; + let changed_label = format!("{}_changed", done_label); + abi::emit_load_temporary_stack_slot(emitter, "r10", stack_offset + slot.original_offset); + abi::emit_load_temporary_stack_slot(emitter, "r11", stack_offset + slot.raw_offset); + emitter.instruction("mov r9, QWORD PTR [r10]"); // load the original eval cell runtime tag before skipping writeback + emitter.instruction(&format!("cmp r9, {}", expected_tag)); // only skip when the eval cell already has the target scalar tag + emitter.instruction(&format!("jne {}", changed_label)); // coerce or rewrite cells whose original tag differs + emitter.instruction("mov r9, QWORD PTR [r10 + 8]"); // load the original eval cell scalar payload word + emitter.instruction("cmp r11, r9"); // did the native call leave the raw ref slot unchanged? + emitter.instruction(&format!("je {}", done_label)); // keep the existing Mixed cell when no replacement is needed + emitter.label(&changed_label); +} + +/// Returns the runtime scalar tag that can skip writeback on exact tag and payload match. +fn typed_ref_arg_unchanged_runtime_tag(ty: &PhpType) -> Option { + match ty.codegen_repr() { + ty @ (PhpType::Int | PhpType::Bool | PhpType::Float) => Some(runtime_value_tag(&ty)), + _ => None, + } +} + +/// Returns true when a typed raw ref slot stores a refcounted heap payload pointer. +fn typed_ref_arg_is_refcounted_heap(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Iterable | PhpType::Object(_) + ) } /// Loads one ARM64 typed raw ref slot into the canonical result registers. @@ -358,6 +578,46 @@ fn emit_x86_64_release_refcounted_raw_slot( emitter.label(&done_label); } +/// Copies a replacement ARM64 Mixed cell into an existing target cell without releasing old payload. +fn emit_aarch64_replace_mixed_cell_without_releasing_old( + emitter: &mut Emitter, + target_reg: &str, + replacement_reg: &str, +) { + abi::emit_push_reg_pair(emitter, target_reg, replacement_reg); + emitter.instruction("ldr x9, [sp]"); // reload the original Mixed cell pointer for direct replacement + emitter.instruction("ldr x10, [sp, #8]"); // reload the replacement Mixed cell pointer + emitter.instruction("ldr x11, [x10]"); // copy the replacement runtime tag + emitter.instruction("str x11, [x9]"); // overwrite the target cell tag + emitter.instruction("ldr x11, [x10, #8]"); // copy the replacement low payload word + emitter.instruction("str x11, [x9, #8]"); // overwrite the target cell low payload word + emitter.instruction("ldr x11, [x10, #16]"); // copy the replacement high payload word + emitter.instruction("str x11, [x9, #16]"); // overwrite the target cell high payload word + emitter.instruction("mov x0, x10"); // pass the now-empty replacement cell storage to heap_free + abi::emit_call_label(emitter, "__rt_heap_free"); + abi::emit_release_temporary_stack(emitter, 16); +} + +/// Copies a replacement x86_64 Mixed cell into an existing target cell without releasing old payload. +fn emit_x86_64_replace_mixed_cell_without_releasing_old( + emitter: &mut Emitter, + target_reg: &str, + replacement_reg: &str, +) { + abi::emit_push_reg_pair(emitter, target_reg, replacement_reg); + emitter.instruction("mov r10, QWORD PTR [rsp]"); // reload the original Mixed cell pointer for direct replacement + emitter.instruction("mov r11, QWORD PTR [rsp + 8]"); // reload the replacement Mixed cell pointer + emitter.instruction("mov r9, QWORD PTR [r11]"); // copy the replacement runtime tag + emitter.instruction("mov QWORD PTR [r10], r9"); // overwrite the target cell tag + emitter.instruction("mov r9, QWORD PTR [r11 + 8]"); // copy the replacement low payload word + emitter.instruction("mov QWORD PTR [r10 + 8], r9"); // overwrite the target cell low payload word + emitter.instruction("mov r9, QWORD PTR [r11 + 16]"); // copy the replacement high payload word + emitter.instruction("mov QWORD PTR [r10 + 16], r9"); // overwrite the target cell high payload word + emitter.instruction("mov rax, r11"); // pass the now-empty replacement cell storage to heap_free + abi::emit_call_label(emitter, "__rt_heap_free"); + abi::emit_release_temporary_stack(emitter, 16); +} + /// Copies one replacement ARM64 Mixed cell payload into an existing target cell. fn emit_aarch64_replace_mixed_cell( emitter: &mut Emitter, From 460b362e4d673d3600b71795b2b39b0075e90cd1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 14:55:19 +0200 Subject: [PATCH 0932/1208] Invoke AOT heap by-ref functions from eval --- .../src/interpreter/control.rs | 5 + .../src/interpreter/dynamic_functions.rs | 87 +++++++++++++---- .../src/interpreter/runtime_ops.rs | 11 ++- .../interpreter/tests/support/runtime_ops.rs | 19 +++- .../src/runtime_hooks/externs.rs | 3 + .../elephc-magician/src/runtime_hooks/ops.rs | 20 +++- docs/php/eval.md | 17 ++-- src/codegen/lower_inst/builtins/eval.rs | 11 ++- src/codegen_support/runtime/eval_bridge.rs | 95 +++++++++++++++++++ tests/codegen/eval.rs | 44 +++++++++ 10 files changed, 283 insertions(+), 29 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index 6eba86d9d9..f3c3822235 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -62,6 +62,11 @@ pub(super) enum BoundNativeFunctionRefSlot { slot: Box, target: EvalReferenceTarget, }, + OwnedRawWord { + original: u64, + slot: Box, + target: EvalReferenceTarget, + }, } /// One already evaluated PHP callback supported by the eval dispatcher. diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index af9b805e95..a09ee49ad9 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -584,18 +584,38 @@ fn stage_native_function_invoker_args( continue; } let target = bound_arg.ref_target.ok_or(EvalStatus::RuntimeFatal)?; - if let Some(tag) = native_function_raw_word_ref_tag(function.param_type(param_index)) { - let original = values.raw_value_word(bound_arg.value)?; - let mut slot = Box::new(original); - let marker = - values.invoker_raw_ref_cell(slot.as_mut() as *mut u64 as *mut c_void, tag)?; - invoker_values.push(marker); - ref_slots.push(BoundNativeFunctionRefSlot::RawWord { - tag, - original, - slot, - target, - }); + if let Some(raw_ref_kind) = native_function_raw_ref_kind(function.param_type(param_index)) { + match raw_ref_kind { + NativeFunctionRawRefKind::Scalar { tag } => { + let original = values.raw_value_word(bound_arg.value)?; + let mut slot = Box::new(original); + let marker = + values.invoker_raw_ref_cell(slot.as_mut() as *mut u64 as *mut c_void, tag)?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::RawWord { + tag, + original, + slot, + target, + }); + } + NativeFunctionRawRefKind::OwnedHeap => { + let source_tag = values.type_tag(bound_arg.value)?; + let original = values.raw_value_word(bound_arg.value)?; + let retained = values.retain_raw_heap_word(original)?; + let mut slot = Box::new(retained); + let marker = values.invoker_raw_ref_cell( + slot.as_mut() as *mut u64 as *mut c_void, + source_tag, + )?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::OwnedRawWord { + original, + slot, + target, + }); + } + } continue; } let original = bound_arg.value; @@ -614,8 +634,14 @@ fn stage_native_function_invoker_args( }) } -/// Returns the runtime tag for a by-reference parameter that can use one raw word. -fn native_function_raw_word_ref_tag(param_type: Option<&EvalParameterType>) -> Option { +/// Describes a native function by-reference parameter that can use one raw word. +enum NativeFunctionRawRefKind { + Scalar { tag: u64 }, + OwnedHeap, +} + +/// Returns the raw-slot strategy for a by-reference parameter that can use one raw word. +fn native_function_raw_ref_kind(param_type: Option<&EvalParameterType>) -> Option { let param_type = param_type?; if param_type.allows_null() || param_type.is_intersection() @@ -624,9 +650,15 @@ fn native_function_raw_word_ref_tag(param_type: Option<&EvalParameterType>) -> O return None; } match param_type.variants().first()? { - EvalParameterTypeVariant::Bool => Some(EVAL_TAG_BOOL), - EvalParameterTypeVariant::Float => Some(EVAL_TAG_FLOAT), - EvalParameterTypeVariant::Int => Some(EVAL_TAG_INT), + EvalParameterTypeVariant::Array + | EvalParameterTypeVariant::Class(_) + | EvalParameterTypeVariant::Iterable + | EvalParameterTypeVariant::Object => Some(NativeFunctionRawRefKind::OwnedHeap), + EvalParameterTypeVariant::Bool => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_BOOL }), + EvalParameterTypeVariant::Float => { + Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_FLOAT }) + } + EvalParameterTypeVariant::Int => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_INT }), _ => None, } } @@ -1716,6 +1748,27 @@ fn write_back_native_function_ref_args( Some(ScopeCellOwnership::Owned), )?; } + BoundNativeFunctionRefSlot::OwnedRawWord { + original, + slot, + target, + } => { + let word = **slot; + if word == *original { + values.release_raw_heap_word(word)?; + continue; + } + let value = values.raw_heap_word_value(word); + values.release_raw_heap_word(word)?; + let value = value?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } } } Ok(()) diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index b5ec3c223f..b9f25c9c06 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -364,9 +364,12 @@ pub trait RuntimeValueOps { source_tag: u64, ) -> Result; - /// Extracts the low raw payload word from a boxed scalar Mixed cell. + /// Extracts the low raw payload word from a boxed Mixed cell. fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result; + /// Retains one raw heap payload word so a staged native by-ref slot owns it. + fn retain_raw_heap_word(&mut self, word: u64) -> Result; + /// Boxes one raw scalar payload word as a Mixed cell with the provided runtime tag. fn raw_word_value( &mut self, @@ -374,6 +377,12 @@ pub trait RuntimeValueOps { word: u64, ) -> Result; + /// Boxes one raw heap payload word as a Mixed cell after inspecting its heap kind. + fn raw_heap_word_value(&mut self, word: u64) -> Result; + + /// Releases one raw heap payload word owned by a staged native by-ref slot. + fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus>; + /// Returns the unboxed object payload pointer used for PHP object identity. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index 381f804996..656980ab4f 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -401,15 +401,24 @@ impl RuntimeValueOps for FakeOps { ) -> Result { Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) } - /// Extracts one fake scalar payload word for raw by-reference staging. + /// Extracts one fake low payload word for raw by-reference staging. fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { Ok(match self.get(value) { FakeValue::Bool(value) => u64::from(value), FakeValue::Float(value) => value.to_bits(), FakeValue::Int(value) => value as u64, + FakeValue::Array(_) + | FakeValue::Assoc(_) + | FakeValue::Object(_) + | FakeValue::Iterator { .. } => value.as_ptr() as u64, _ => 0, }) } + /// Retains a fake raw heap word for native by-reference staging. + fn retain_raw_heap_word(&mut self, word: u64) -> Result { + self.runtime_retain(RuntimeCellHandle::from_raw(word as *mut RuntimeCell))?; + Ok(word) + } /// Boxes one fake scalar raw payload word with the provided runtime tag. fn raw_word_value( &mut self, @@ -423,6 +432,14 @@ impl RuntimeValueOps for FakeOps { _ => Err(EvalStatus::RuntimeFatal), } } + /// Converts a fake raw heap word back to its stable fake handle. + fn raw_heap_word_value(&mut self, word: u64) -> Result { + Ok(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } + /// Records release of a fake raw heap word owned by a staged slot. + fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus> { + self.runtime_release(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } /// Returns the fake object handle as a stable object identity. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { self.runtime_object_identity(object) diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 11e249ab92..6b4d4b8c78 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -268,10 +268,13 @@ unsafe extern "C" { source_tag: u64, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_raw_word(value: *mut RuntimeCell) -> u64; + pub(super) fn __elephc_eval_value_retain_raw_heap_word(word: u64) -> u64; pub(super) fn __elephc_eval_value_from_raw_word( source_tag: u64, word: u64, ) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_from_raw_heap_word(word: u64) -> *mut RuntimeCell; + pub(super) fn __elephc_eval_value_release_raw_heap_word(word: u64); /// Returns the unboxed object payload pointer for object-tagged eval values. pub(super) fn __elephc_eval_value_object_identity(value: *mut RuntimeCell) -> u64; pub(super) fn __elephc_eval_warning(message_ptr: *const u8, message_len: u64); diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index e9e9f029ae..8212563760 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -764,11 +764,16 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_invoker_raw_ref_cell(slot, source_tag) }) } - /// Extracts the low raw payload word from a boxed scalar Mixed cell. + /// Extracts the low raw payload word from a boxed Mixed cell. fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { Ok(unsafe { __elephc_eval_value_raw_word(value.as_ptr()) }) } + /// Retains one raw heap payload word for owned native by-reference staging. + fn retain_raw_heap_word(&mut self, word: u64) -> Result { + Ok(unsafe { __elephc_eval_value_retain_raw_heap_word(word) }) + } + /// Boxes one raw scalar payload word as a Mixed cell with the provided runtime tag. fn raw_word_value( &mut self, @@ -778,6 +783,19 @@ impl RuntimeValueOps for ElephcRuntimeOps { Self::handle(unsafe { __elephc_eval_value_from_raw_word(source_tag, word) }) } + /// Boxes one raw heap payload word as a Mixed cell using its runtime heap kind. + fn raw_heap_word_value(&mut self, word: u64) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_heap_word(word) }) + } + + /// Releases one raw heap payload word owned by native by-reference staging. + fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release_raw_heap_word(word); + } + Ok(()) + } + /// Returns the unboxed object payload pointer for SPL object identity builtins. fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { let identity = unsafe { __elephc_eval_value_object_identity(object.as_ptr()) }; diff --git a/docs/php/eval.md b/docs/php/eval.md index 5711870b01..46f006eb52 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -113,9 +113,11 @@ eval fragments. String keys in unpacked argument arrays bind as named parameters. Direct calls and variable-function calls can also invoke registered AOT global user functions when their by-reference parameters use generated `mixed`/union-style boxed storage or one-word scalar raw storage (`int`, `bool`, -or `float`). Nullable scalar, string, array, iterable, object, and other -raw-storage by-reference free-function parameters remain metadata-only until -the function bridge has typed staging/writeback for those ABI layouts. +or `float`) or one-word heap raw storage (`array`, `iterable`, and +object/class parameters). Nullable scalar, string, native-only ABI layouts, and +other multi-word or unsupported raw-storage by-reference free-function +parameters remain metadata-only until the function bridge has typed +staging/writeback for those ABI layouts. `call_user_func()` remains by-value for registered AOT free-function by-reference parameters. @@ -535,12 +537,13 @@ generated/AOT free functions with positional variadic tails when the generated signature has no by-reference parameters. Direct and variable-function calls can additionally invoke generated/AOT free functions whose by-reference parameters use boxed Mixed/union storage or one-word scalar raw storage -(`int`, `bool`, or `float`). Registered generated/AOT free-function parameter +(`int`, `bool`, or `float`) or one-word heap raw storage (`array`, `iterable`, +and object/class parameters). Registered generated/AOT free-function parameter names, declared types, return types, by-reference and variadic flags, required/optional counts, and supported defaults are also exposed through `ReflectionFunction` / `ReflectionParameter` metadata. Unsupported generated/AOT free-function bridge shapes, such as nullable tagged-scalar, -string, array, iterable, object, native-only ABI layouts, and other raw-storage +string, native-only ABI layouts, and other multi-word or unsupported raw-storage by-reference parameters, remain reflectable as metadata but are not invocable through eval. Supported callable-builtin invocation is covered by the general Reflection support documented in @@ -916,8 +919,8 @@ Generated/AOT free-function and method type metadata, return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while unsupported native-only bridge shapes and raw-storage -by-reference free-function bridge shapes beyond `int`, `bool`, and `float` -remain metadata-only rather than invocable through eval. +by-reference free-function bridge shapes beyond the current Mixed/scalar/one-word +heap slice remain metadata-only rather than invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 6606e6bff6..9991326596 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -2395,11 +2395,18 @@ fn function_signature_can_bridge_with_eval(function: &Function) -> bool { }) } -/// Returns true when a native function by-reference parameter can use a Mixed staging slot. +/// Returns true when a native function by-reference parameter can use eval bridge staging. fn eval_native_function_ref_param_supported(ty: &PhpType) -> bool { matches!( ty.codegen_repr(), - PhpType::Mixed | PhpType::Int | PhpType::Bool | PhpType::Float + PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Bool + | PhpType::Float + | PhpType::Int + | PhpType::Iterable + | PhpType::Mixed + | PhpType::Object(_) ) } diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index c457a97a32..b6e0fbc56b 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -784,10 +784,58 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the wrapper frame emitter.instruction("ret"); // return the raw payload word + label_c_global(emitter, "__elephc_eval_value_retain_raw_heap_word"); + emitter.instruction("sub sp, sp, #32"); // reserve a wrapper frame while retaining the raw heap word + emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across incref + emitter.instruction("add x29, sp, #16"); // establish a stable frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the raw heap word for the C return value + emitter.instruction("bl __rt_incref"); // retain the heap payload for the staged by-ref slot + emitter.instruction("ldr x0, [sp, #0]"); // return the original raw heap word to Rust + emitter.instruction("ldp x29, x30, [sp, #16]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #32"); // release the retain wrapper frame + emitter.instruction("ret"); // return the retained raw heap word + label_c_global(emitter, "__elephc_eval_value_from_raw_word"); emitter.instruction("mov x2, xzr"); // raw one-word scalar payloads have no high payload word emitter.instruction("b __rt_mixed_from_value"); // box the raw scalar payload and return it to Rust + label_c_global(emitter, "__elephc_eval_value_from_raw_heap_word"); + emitter.instruction("cbz x0, __elephc_eval_value_from_raw_heap_word_miss"); // null raw heap words cannot be boxed + emitter.instruction("ldr x9, [x0, #-8]"); // load the uniform heap kind word for tag recovery + emitter.instruction("and x9, x9, #0xff"); // isolate the low-byte heap kind tag + emitter.instruction("cmp x9, #2"); // heap kind 2 stores indexed-array payloads + emitter.instruction("b.eq __elephc_eval_value_from_raw_heap_word_array"); // box indexed arrays with runtime tag 4 + emitter.instruction("cmp x9, #3"); // heap kind 3 stores associative hash payloads + emitter.instruction("b.eq __elephc_eval_value_from_raw_heap_word_hash"); // box hashes with runtime tag 5 + emitter.instruction("cmp x9, #4"); // heap kind 4 stores object payloads + emitter.instruction("b.eq __elephc_eval_value_from_raw_heap_word_object"); // box objects with runtime tag 6 + emitter.instruction("cmp x9, #5"); // heap kind 5 stores boxed Mixed payloads + emitter.instruction("b.eq __elephc_eval_value_from_raw_heap_word_mixed"); // box nested Mixed cells with runtime tag 7 + emitter.label("__elephc_eval_value_from_raw_heap_word_miss"); + emitter.instruction("mov x0, xzr"); // report malformed raw heap words as a null Rust handle + emitter.instruction("ret"); // return the failed boxing sentinel + emitter.label("__elephc_eval_value_from_raw_heap_word_array"); + emitter.instruction("mov x1, x0"); // move the indexed-array payload into the boxing low word + emitter.instruction("mov x0, #4"); // runtime tag 4 = indexed array + emitter.instruction("b __elephc_eval_value_from_raw_heap_word_box"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_hash"); + emitter.instruction("mov x1, x0"); // move the hash payload into the boxing low word + emitter.instruction("mov x0, #5"); // runtime tag 5 = associative array + emitter.instruction("b __elephc_eval_value_from_raw_heap_word_box"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_object"); + emitter.instruction("mov x1, x0"); // move the object payload into the boxing low word + emitter.instruction("mov x0, #6"); // runtime tag 6 = object + emitter.instruction("b __elephc_eval_value_from_raw_heap_word_box"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_mixed"); + emitter.instruction("mov x1, x0"); // move the boxed Mixed payload into the boxing low word + emitter.instruction("mov x0, #7"); // runtime tag 7 = Mixed + emitter.label("__elephc_eval_value_from_raw_heap_word_box"); + emitter.instruction("mov x2, xzr"); // one-word heap payloads do not use a high word + emitter.instruction("b __rt_mixed_from_value"); // retain and box the raw heap payload for eval + + label_c_global(emitter, "__elephc_eval_value_release_raw_heap_word"); + emitter.instruction("b __rt_decref_any"); // release the staged raw heap slot owner + label_c_global(emitter, "__elephc_eval_value_object_identity"); emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the object cell emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox @@ -2289,12 +2337,59 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the raw payload word + label_c_global(emitter, "__elephc_eval_value_retain_raw_heap_word"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer while retaining a raw heap word + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve space for the raw heap word return value + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the raw heap word across incref + emitter.instruction("mov rax, rdi"); // move the raw heap word into the runtime incref input register + emitter.instruction("call __rt_incref"); // retain the heap payload for the staged by-ref slot + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // return the original raw heap word to Rust + emitter.instruction("add rsp, 16"); // release the retain wrapper spill slot + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the retained raw heap word + label_c_global(emitter, "__elephc_eval_value_from_raw_word"); emitter.instruction("mov rax, rdi"); // move the runtime tag into the mixed boxing tag register emitter.instruction("mov rdi, rsi"); // move the raw scalar word into the low payload register emitter.instruction("xor esi, esi"); // raw one-word scalar payloads have no high payload word emitter.instruction("jmp __rt_mixed_from_value"); // box the raw scalar payload and return it to Rust + label_c_global(emitter, "__elephc_eval_value_from_raw_heap_word"); + emitter.instruction("test rdi, rdi"); // reject null raw heap words before reading their header + emitter.instruction("jz __elephc_eval_value_from_raw_heap_word_miss_x86"); // null raw heap words cannot be boxed + emitter.instruction("mov r10, QWORD PTR [rdi - 8]"); // load the uniform x86_64 heap kind word for tag recovery + emitter.instruction("and r10, 0xff"); // isolate the low-byte heap kind tag + emitter.instruction("cmp r10, 2"); // heap kind 2 stores indexed-array payloads + emitter.instruction("je __elephc_eval_value_from_raw_heap_word_array_x86"); // box indexed arrays with runtime tag 4 + emitter.instruction("cmp r10, 3"); // heap kind 3 stores associative hash payloads + emitter.instruction("je __elephc_eval_value_from_raw_heap_word_hash_x86"); // box hashes with runtime tag 5 + emitter.instruction("cmp r10, 4"); // heap kind 4 stores object payloads + emitter.instruction("je __elephc_eval_value_from_raw_heap_word_object_x86"); // box objects with runtime tag 6 + emitter.instruction("cmp r10, 5"); // heap kind 5 stores boxed Mixed payloads + emitter.instruction("je __elephc_eval_value_from_raw_heap_word_mixed_x86"); // box nested Mixed cells with runtime tag 7 + emitter.label("__elephc_eval_value_from_raw_heap_word_miss_x86"); + emitter.instruction("xor eax, eax"); // report malformed raw heap words as a null Rust handle + emitter.instruction("ret"); // return the failed boxing sentinel + emitter.label("__elephc_eval_value_from_raw_heap_word_array_x86"); + emitter.instruction("mov rax, 4"); // runtime tag 4 = indexed array + emitter.instruction("jmp __elephc_eval_value_from_raw_heap_word_box_x86"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_hash_x86"); + emitter.instruction("mov rax, 5"); // runtime tag 5 = associative array + emitter.instruction("jmp __elephc_eval_value_from_raw_heap_word_box_x86"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_object_x86"); + emitter.instruction("mov rax, 6"); // runtime tag 6 = object + emitter.instruction("jmp __elephc_eval_value_from_raw_heap_word_box_x86"); // share the one-word heap boxing tail + emitter.label("__elephc_eval_value_from_raw_heap_word_mixed_x86"); + emitter.instruction("mov rax, 7"); // runtime tag 7 = Mixed + emitter.label("__elephc_eval_value_from_raw_heap_word_box_x86"); + emitter.instruction("xor esi, esi"); // one-word heap payloads do not use a high word + emitter.instruction("jmp __rt_mixed_from_value"); // retain and box the raw heap payload for eval + + label_c_global(emitter, "__elephc_eval_value_release_raw_heap_word"); + emitter.instruction("mov rax, rdi"); // move the raw heap word into the runtime release input register + emitter.instruction("jmp __rt_decref_any"); // release the staged raw heap slot owner + label_c_global(emitter, "__elephc_eval_value_object_identity"); emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable object-identity wrapper frame diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cc76e1ea5f..008dc79bbb 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5320,6 +5320,50 @@ return $first . ":" . $i . ":" . ($b ? "T" : "F") . ":" . ($f == 3.0 ? "F3" : "b assert_eq!(out, "done:9:F:F3"); } +/// Verifies eval can dispatch AOT user functions with one-word heap by-reference params. +#[test] +fn test_eval_fragment_can_call_native_user_function_with_heap_by_ref_args() { + let out = compile_and_run( + r#"name = $name; + } +} + +function native_eval_ref_array_heap(array &$items): string { + $items = ["A", "B"]; + return "array"; +} + +function native_eval_ref_iterable_heap(iterable &$items): string { + $items = ["left" => "L", "right" => "R"]; + return "iter"; +} + +function native_eval_ref_object_heap(NativeEvalRefBox &$box): string { + $box = new NativeEvalRefBox($box->name . "!"); + return $box->name; +} + +echo eval('$items = [1]; +$first = native_eval_ref_array_heap($items); +$fn = "native_eval_ref_array_heap"; +$fn($items); +native_eval_ref_array_heap(items: $items); +$iter = [0]; +$iterFirst = native_eval_ref_iterable_heap($iter); +$box = new NativeEvalRefBox("start"); +$objectFirst = native_eval_ref_object_heap($box); +native_eval_ref_object_heap(box: $box); +return $first . ":" . $items[0] . ":" . $items[1] . ":" . $iterFirst . ":" . $iter["right"] . ":" . $objectFirst . ":" . $box->name;'); +"#, + ); + assert_eq!(out, "array:A:B:iter:R:start!:start!!"); +} + /// Verifies eval can dispatch generated/AOT variadic functions through the native bridge. #[test] fn test_eval_fragment_can_call_native_variadic_user_function() { From 42d7f91b182d70eeae39752ee5b74b3723ace379 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 15:11:13 +0200 Subject: [PATCH 0933/1208] Invoke AOT string by-ref functions from eval --- .../src/interpreter/control.rs | 5 ++ .../src/interpreter/dynamic_functions.rs | 43 +++++++++++- .../src/interpreter/runtime_ops.rs | 12 ++++ .../interpreter/tests/support/runtime_ops.rs | 22 +++++++ .../src/runtime_hooks/externs.rs | 17 +++++ .../elephc-magician/src/runtime_hooks/ops.rs | 25 +++++++ docs/php/eval.md | 21 +++--- src/codegen/lower_inst/builtins/eval.rs | 1 + src/codegen_support/runtime/eval_bridge.rs | 66 +++++++++++++++++++ tests/codegen/eval.rs | 21 ++++++ 10 files changed, 221 insertions(+), 12 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index f3c3822235..40993ff549 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -62,6 +62,11 @@ pub(super) enum BoundNativeFunctionRefSlot { slot: Box, target: EvalReferenceTarget, }, + RawString { + original: [u64; 2], + slot: Box<[u64; 2]>, + target: EvalReferenceTarget, + }, OwnedRawWord { original: u64, slot: Box, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index a09ee49ad9..794834b0c1 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -599,6 +599,22 @@ fn stage_native_function_invoker_args( target, }); } + NativeFunctionRawRefKind::String => { + let original_ptr = values.raw_value_word(bound_arg.value)?; + let original_len = values.raw_value_high_word(bound_arg.value)?; + let retained = values.retain_raw_string_words(original_ptr, original_len)?; + let mut slot = Box::new([retained.0, retained.1]); + let marker = values.invoker_raw_ref_cell( + slot.as_mut() as *mut [u64; 2] as *mut c_void, + EVAL_TAG_STRING, + )?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::RawString { + original: [retained.0, retained.1], + slot, + target, + }); + } NativeFunctionRawRefKind::OwnedHeap => { let source_tag = values.type_tag(bound_arg.value)?; let original = values.raw_value_word(bound_arg.value)?; @@ -634,13 +650,14 @@ fn stage_native_function_invoker_args( }) } -/// Describes a native function by-reference parameter that can use one raw word. +/// Describes native function by-reference parameters that can use typed raw slots. enum NativeFunctionRawRefKind { Scalar { tag: u64 }, + String, OwnedHeap, } -/// Returns the raw-slot strategy for a by-reference parameter that can use one raw word. +/// Returns the raw-slot strategy for one supported by-reference parameter. fn native_function_raw_ref_kind(param_type: Option<&EvalParameterType>) -> Option { let param_type = param_type?; if param_type.allows_null() @@ -659,6 +676,7 @@ fn native_function_raw_ref_kind(param_type: Option<&EvalParameterType>) -> Optio Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_FLOAT }) } EvalParameterTypeVariant::Int => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_INT }), + EvalParameterTypeVariant::String => Some(NativeFunctionRawRefKind::String), _ => None, } } @@ -1748,6 +1766,27 @@ fn write_back_native_function_ref_args( Some(ScopeCellOwnership::Owned), )?; } + BoundNativeFunctionRefSlot::RawString { + original, + slot, + target, + } => { + let words = **slot; + if words == *original { + values.release_raw_string_words(words[0], words[1])?; + continue; + } + let value = values.raw_string_value(words[0], words[1]); + values.release_raw_string_words(words[0], words[1])?; + let value = value?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } BoundNativeFunctionRefSlot::OwnedRawWord { original, slot, diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index b9f25c9c06..f47b640764 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -367,6 +367,18 @@ pub trait RuntimeValueOps { /// Extracts the low raw payload word from a boxed Mixed cell. fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result; + /// Extracts the high raw payload word from a boxed Mixed cell. + fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result; + + /// Duplicates one raw string payload so a staged native by-ref slot owns it. + fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus>; + + /// Boxes one raw string payload as a Mixed string cell. + fn raw_string_value(&mut self, ptr: u64, len: u64) -> Result; + + /// Releases one raw string payload owned by a staged native by-ref slot. + fn release_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(), EvalStatus>; + /// Retains one raw heap payload word so a staged native by-ref slot owns it. fn retain_raw_heap_word(&mut self, word: u64) -> Result; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index 656980ab4f..64e3e87249 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -407,6 +407,7 @@ impl RuntimeValueOps for FakeOps { FakeValue::Bool(value) => u64::from(value), FakeValue::Float(value) => value.to_bits(), FakeValue::Int(value) => value as u64, + FakeValue::String(_) | FakeValue::Bytes(_) => value.as_ptr() as u64, FakeValue::Array(_) | FakeValue::Assoc(_) | FakeValue::Object(_) @@ -414,6 +415,27 @@ impl RuntimeValueOps for FakeOps { _ => 0, }) } + /// Extracts one fake high payload word for raw by-reference staging. + fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::String(value) => value.len() as u64, + FakeValue::Bytes(value) => value.len() as u64, + _ => 0, + }) + } + /// Retains a fake raw string payload for native by-reference staging. + fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus> { + self.runtime_retain(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell))?; + Ok((ptr, len)) + } + /// Converts a fake raw string payload back to its stable fake handle. + fn raw_string_value(&mut self, ptr: u64, _len: u64) -> Result { + Ok(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell)) + } + /// Records release of a fake raw string payload owned by a staged slot. + fn release_raw_string_words(&mut self, ptr: u64, _len: u64) -> Result<(), EvalStatus> { + self.runtime_release(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell)) + } /// Retains a fake raw heap word for native by-reference staging. fn retain_raw_heap_word(&mut self, word: u64) -> Result { self.runtime_retain(RuntimeCellHandle::from_raw(word as *mut RuntimeCell))?; diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 6b4d4b8c78..e8e51830bc 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -267,13 +267,30 @@ unsafe extern "C" { slot: *mut c_void, source_tag: u64, ) -> *mut RuntimeCell; + /// Extracts the low raw payload word from a boxed runtime value. pub(super) fn __elephc_eval_value_raw_word(value: *mut RuntimeCell) -> u64; + /// Extracts the high raw payload word from a boxed runtime value. + pub(super) fn __elephc_eval_value_raw_high_word(value: *mut RuntimeCell) -> u64; + /// Duplicates raw string storage for a staged native by-reference slot. + pub(super) fn __elephc_eval_value_retain_raw_string( + ptr: u64, + len: u64, + out_len: *mut u64, + ) -> u64; + /// Boxes raw string storage back into a runtime value for eval writeback. + pub(super) fn __elephc_eval_value_from_raw_string(ptr: u64, len: u64) -> *mut RuntimeCell; + /// Releases raw string storage owned by a staged native by-reference slot. + pub(super) fn __elephc_eval_value_release_raw_string(ptr: u64, len: u64); + /// Retains one raw heap payload word for a staged native by-reference slot. pub(super) fn __elephc_eval_value_retain_raw_heap_word(word: u64) -> u64; + /// Boxes one raw scalar payload word back into a runtime value. pub(super) fn __elephc_eval_value_from_raw_word( source_tag: u64, word: u64, ) -> *mut RuntimeCell; + /// Boxes one raw heap payload word back into a runtime value. pub(super) fn __elephc_eval_value_from_raw_heap_word(word: u64) -> *mut RuntimeCell; + /// Releases one raw heap payload word owned by a staged by-reference slot. pub(super) fn __elephc_eval_value_release_raw_heap_word(word: u64); /// Returns the unboxed object payload pointer for object-tagged eval values. pub(super) fn __elephc_eval_value_object_identity(value: *mut RuntimeCell) -> u64; diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 8212563760..58e77498c9 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -769,6 +769,31 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_raw_word(value.as_ptr()) }) } + /// Extracts the high raw payload word from a boxed Mixed cell. + fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_raw_high_word(value.as_ptr()) }) + } + + /// Duplicates one raw string payload for owned native by-reference staging. + fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus> { + let mut out_len = 0; + let out_ptr = unsafe { __elephc_eval_value_retain_raw_string(ptr, len, &mut out_len) }; + Ok((out_ptr, out_len)) + } + + /// Boxes one raw string payload as a Mixed string cell. + fn raw_string_value(&mut self, ptr: u64, len: u64) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_string(ptr, len) }) + } + + /// Releases one raw string payload owned by native by-reference staging. + fn release_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release_raw_string(ptr, len); + } + Ok(()) + } + /// Retains one raw heap payload word for owned native by-reference staging. fn retain_raw_heap_word(&mut self, word: u64) -> Result { Ok(unsafe { __elephc_eval_value_retain_raw_heap_word(word) }) diff --git a/docs/php/eval.md b/docs/php/eval.md index 46f006eb52..c80cf7ffdb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -113,9 +113,9 @@ eval fragments. String keys in unpacked argument arrays bind as named parameters. Direct calls and variable-function calls can also invoke registered AOT global user functions when their by-reference parameters use generated `mixed`/union-style boxed storage or one-word scalar raw storage (`int`, `bool`, -or `float`) or one-word heap raw storage (`array`, `iterable`, and -object/class parameters). Nullable scalar, string, native-only ABI layouts, and -other multi-word or unsupported raw-storage by-reference free-function +or `float`), string raw storage, or one-word heap raw storage (`array`, +`iterable`, and object/class parameters). Nullable scalar, native-only ABI +layouts, and other unsupported raw-storage by-reference free-function parameters remain metadata-only until the function bridge has typed staging/writeback for those ABI layouts. `call_user_func()` remains by-value for registered AOT free-function @@ -537,15 +537,15 @@ generated/AOT free functions with positional variadic tails when the generated signature has no by-reference parameters. Direct and variable-function calls can additionally invoke generated/AOT free functions whose by-reference parameters use boxed Mixed/union storage or one-word scalar raw storage -(`int`, `bool`, or `float`) or one-word heap raw storage (`array`, `iterable`, -and object/class parameters). Registered generated/AOT free-function parameter +(`int`, `bool`, or `float`), string raw storage, or one-word heap raw storage +(`array`, `iterable`, and object/class parameters). Registered generated/AOT free-function parameter names, declared types, return types, by-reference and variadic flags, required/optional counts, and supported defaults are also exposed through `ReflectionFunction` / `ReflectionParameter` metadata. Unsupported generated/AOT free-function bridge shapes, such as nullable tagged-scalar, -string, native-only ABI layouts, and other multi-word or unsupported raw-storage -by-reference parameters, remain reflectable as metadata but are not invocable -through eval. Supported callable-builtin invocation is +native-only ABI layouts, and other unsupported raw-storage by-reference +parameters, remain reflectable as metadata but are not invocable through eval. +Supported callable-builtin invocation is covered by the general Reflection support documented in `docs/php/classes.md`. Defaulted eval method parameters are @@ -919,8 +919,9 @@ Generated/AOT free-function and method type metadata, return metadata, by-reference and variadic parameter flags, and generated/AOT class/method/property/class-constant attributes are exposed for registered metadata slices, while unsupported native-only bridge shapes and raw-storage -by-reference free-function bridge shapes beyond the current Mixed/scalar/one-word -heap slice remain metadata-only rather than invocable through eval. +by-reference free-function bridge shapes beyond the current +Mixed/scalar/string/one-word heap slice remain metadata-only rather than +invocable through eval. Because `eval()` is a dynamic barrier, the compiler must be conservative after an eval call. Values that cross the barrier may be widened to boxed `Mixed` diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 9991326596..f341b834c5 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -2407,6 +2407,7 @@ fn eval_native_function_ref_param_supported(ty: &PhpType) -> bool { | PhpType::Iterable | PhpType::Mixed | PhpType::Object(_) + | PhpType::Str ) } diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index b6e0fbc56b..e6c23c13eb 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -784,6 +784,40 @@ fn emit_aarch64_wrappers(emitter: &mut Emitter) { emitter.instruction("add sp, sp, #16"); // release the wrapper frame emitter.instruction("ret"); // return the raw payload word + label_c_global(emitter, "__elephc_eval_value_raw_high_word"); + emitter.instruction("sub sp, sp, #16"); // allocate a wrapper frame while unboxing the string cell + emitter.instruction("stp x29, x30, [sp]"); // save frame pointer and return address across mixed_unbox + emitter.instruction("mov x29, sp"); // establish a frame pointer for the wrapper call + emitter.instruction("bl __rt_mixed_unbox"); // expose the boxed payload words + emitter.instruction("mov x0, x2"); // return the high payload word to Rust + emitter.instruction("ldp x29, x30, [sp]"); // restore caller frame pointer and return address + emitter.instruction("add sp, sp, #16"); // release the wrapper frame + emitter.instruction("ret"); // return the raw high payload word + + label_c_global(emitter, "__elephc_eval_value_retain_raw_string"); + emitter.instruction("sub sp, sp, #48"); // reserve a wrapper frame while persisting the raw string + emitter.instruction("stp x29, x30, [sp, #32]"); // save frame pointer and return address across str_persist + emitter.instruction("add x29, sp, #32"); // establish a stable frame pointer + emitter.instruction("str x2, [sp, #0]"); // save the Rust out-len pointer across string persistence + emitter.instruction("mov x2, x1"); // move the raw string length into str_persist input + emitter.instruction("mov x1, x0"); // move the raw string pointer into str_persist input + emitter.instruction("bl __rt_str_persist"); // duplicate the string for staged by-ref ownership + emitter.instruction("ldr x9, [sp, #0]"); // reload the Rust out-len pointer + emitter.instruction("str x2, [x9]"); // report the persisted string length to Rust + emitter.instruction("mov x0, x1"); // return the persisted string pointer to Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the string retain wrapper frame + emitter.instruction("ret"); // return the retained raw string pointer + + label_c_global(emitter, "__elephc_eval_value_from_raw_string"); + emitter.instruction("mov x2, x1"); // move the raw string length into the Mixed high word + emitter.instruction("mov x1, x0"); // move the raw string pointer into the Mixed low word + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("b __rt_mixed_from_value"); // persist and box the raw string payload for eval + + label_c_global(emitter, "__elephc_eval_value_release_raw_string"); + emitter.instruction("b __rt_heap_free_safe"); // release the staged raw string owner + label_c_global(emitter, "__elephc_eval_value_retain_raw_heap_word"); emitter.instruction("sub sp, sp, #32"); // reserve a wrapper frame while retaining the raw heap word emitter.instruction("stp x29, x30, [sp, #16]"); // save frame pointer and return address across incref @@ -2337,6 +2371,38 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the raw payload word + label_c_global(emitter, "__elephc_eval_value_raw_high_word"); + emitter.instruction("push rbp"); // align the stack and preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("mov rax, rdi"); // move the boxed cell into mixed_unbox input + emitter.instruction("call __rt_mixed_unbox"); // expose the boxed payload words + emitter.instruction("mov rax, rsi"); // return the high payload word to Rust + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the raw high payload word + + label_c_global(emitter, "__elephc_eval_value_retain_raw_string"); + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer while persisting a raw string + emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer + emitter.instruction("sub rsp, 16"); // reserve space for the Rust out-len pointer + emitter.instruction("mov QWORD PTR [rbp - 8], rdx"); // save the Rust out-len pointer across str_persist + emitter.instruction("mov rax, rdi"); // move the raw string pointer into str_persist input + emitter.instruction("mov rdx, rsi"); // move the raw string length into str_persist input + emitter.instruction("call __rt_str_persist"); // duplicate the string for staged by-ref ownership + emitter.instruction("mov r10, QWORD PTR [rbp - 8]"); // reload the Rust out-len pointer + emitter.instruction("mov QWORD PTR [r10], rdx"); // report the persisted string length to Rust + emitter.instruction("add rsp, 16"); // release the string retain wrapper spill slot + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the retained raw string pointer + + label_c_global(emitter, "__elephc_eval_value_from_raw_string"); + emitter.instruction("mov rax, 1"); // runtime tag 1 = string + emitter.instruction("mov rdx, rsi"); // move the raw string length into the Mixed high word + emitter.instruction("jmp __rt_mixed_from_value"); // persist and box the raw string payload for eval + + label_c_global(emitter, "__elephc_eval_value_release_raw_string"); + emitter.instruction("mov rax, rdi"); // move the raw string pointer into the runtime release input register + emitter.instruction("jmp __rt_heap_free_safe"); // release the staged raw string owner + label_c_global(emitter, "__elephc_eval_value_retain_raw_heap_word"); emitter.instruction("push rbp"); // preserve the Rust caller frame pointer while retaining a raw heap word emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 008dc79bbb..caf0be9bb2 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5320,6 +5320,27 @@ return $first . ":" . $i . ":" . ($b ? "T" : "F") . ":" . ($f == 3.0 ? "F3" : "b assert_eq!(out, "done:9:F:F3"); } +/// Verifies eval can dispatch AOT user functions with string by-reference params. +#[test] +fn test_eval_fragment_can_call_native_user_function_with_string_by_ref_arg() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 16:11:00 +0200 Subject: [PATCH 0934/1208] Materialize richer AOT defaults in eval --- docs/php/eval.md | 14 +- src/codegen/lower_inst/builtins/eval.rs | 641 +++++++++++++++++++++++- src/ir/module.rs | 5 +- src/ir_lower/program.rs | 1 + tests/codegen/eval.rs | 91 ++++ 5 files changed, 722 insertions(+), 30 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index c80cf7ffdb..01793a4a4d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -74,9 +74,9 @@ repeated `*_once` includes evaluate to `true`, missing `include` returns | Variables and properties | Variable reads, `$this->property` reads/writes from native methods including public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT property slots when eval is executing in a PHP-visible native class scope, dynamic `stdClass` properties, eval object property access including `__get()` / `__set()` fallback for missing or inaccessible eval properties, `isset()`, `empty()`, and `unset()` magic property dispatch through `__isset()` / `__unset()`, `instanceof` over static and dynamic class/interface targets, eval-declared static property access, runtime-valued static receivers for `$class::$property` reads/writes, increment/decrement, and `isset()` / `empty()` probes, expression-valued static receivers for `($expr)::$property` reads/writes, array writes/appends, and increment/decrement, dynamic static property names (`ClassName::${expr}` / `$class::${expr}` / `($expr)::${expr}`) for reads/writes, `isset()` / `empty()` probes, array writes/appends, array-element unsets, and increment/decrement, static-property unset attempts including dynamic names as PHP-compatible catchable errors, public/protected/private scalar including string, nullable scalar, Mixed, array, and object AOT static property access from PHP-visible native class scopes, and public/protected/private generated/AOT class-like constant fetches through the bridge. | | Arrays | Indexed and associative literals, modern `[...]` and legacy `array(...)`, keyed elements, append writes (`$array[] = value`), numeric-index reads/writes, string-key reads/writes, object-property and static-property array writes/appends/unsets, and eval-declared or generated/AOT `ArrayAccess` object reads, writes, appends, `isset()`, `empty()`, and `unset()` through `offsetGet()`, `offsetSet()`, `offsetExists()`, and `offsetUnset()`. | | Function-like calls | Direct calls, named arguments, argument unpacking (`...`), dynamic string/expression calls, first-class callable syntax for supported function, method, and invokable-object targets, invokable eval and generated/AOT objects, `call_user_func()`, and `call_user_func_array()` for supported call targets. | -| Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new [readonly] class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, union/nullable registered type validation, intersection object type validation, registered by-reference lvalue validation, and registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, object, and supported union/intersection object shapes, with by-reference constructor parameters limited to the staged scalar/nullable scalar/Mixed/array/iterable/object slice. | +| Object construction | `new ClassName`, `new ClassName(...)`, `new $className`, `new $className(...)`, and parenthesized class-name expressions (`new ($expr)` / `new ($expr)(...)`) for eval-declared classes, including constructor named arguments and unpacking; runtime class-name expressions may hold strings or objects whose runtime class is used as the construction target. `new self()`, `new static()`, and `new parent()` work inside eval-declared methods; anonymous `new [readonly] class [(args)] [extends Parent] [implements Iface, ...] { ... }` expressions are supported. `stdClass` and emitted AOT classes visible through runtime metadata support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, positional variadic tails, array-typed arguments, iterable-typed arguments, object-typed arguments, union/nullable registered type validation, intersection object type validation, registered by-reference lvalue validation, and representable scalar/string constant expressions, resolved global/named class-like constants, null, empty-array, supported array-valued, or supported object-valued default arguments when the current eval class scope satisfies PHP visibility. Generated AOT constructor bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, object, and supported union/intersection object shapes, with by-reference constructor parameters limited to the staged scalar/nullable scalar/Mixed/array/iterable/object slice. | | Object cloning | `clone $object` shallow-copies eval-declared objects, `stdClass` storage, and ordinary emitted/AOT object storage. Eval-declared and emitted/AOT `__clone()` hooks run after the copy and obey public/protected/private visibility. | -| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus registered scalar, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private parameters when the current eval class scope satisfies PHP visibility; scalar, nullable, callable, Mixed, array, iterable, object, union, and intersection-object return values are checked and boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, object, and supported union/intersection object shapes, with by-reference parameters limited to the staged scalar/nullable scalar/Mixed/array/iterable/object slice. | +| Method calls | Eval-declared object and static method calls support positional arguments, named arguments, numeric unpacking, string-keyed named unpacking, variadic tails, dynamic static receivers (`$class::method()`, `$class::$method()`, and `($expr)::method()`), variable static method names on named receivers (`ClassName::$method()`), braced dynamic static method names (`ClassName::{$method}()` / `$class::{$method}()` / `($expr)::{$method}()`), and by-reference parameters for direct variable, array-element, object-property including dynamic property names, object-property array-element, static-property, and static-property array-element arguments including dynamic receivers and dynamic property names. Missing or inaccessible eval methods dispatch through `__call()` / `__callStatic()` when those hooks are available. Runtime/AOT object-method and static-method fallback supports the same fixed-parameter binding plus positional variadic tails, registered by-reference lvalue validation and writeback target preservation, plus representable scalar/string constant expressions, resolved global/named class-like constants, null, empty-array, supported array-valued, or supported object-valued default arguments for public/protected/private parameters when the current eval class scope satisfies PHP visibility; scalar, nullable, callable, Mixed, array, iterable, object, union, and intersection-object return values are checked and boxed back to eval. Generated AOT method bridge dispatch can invoke visibility-checked non-by-reference signatures plus `mixed`/untyped, scalar including string, nullable scalar, array, iterable, object, and supported union/intersection object shapes, with by-reference parameters limited to the staged scalar/nullable scalar/Mixed/array/iterable/object slice. | | Includes | `include`, `include_once`, `require`, and `require_once` are expressions. | | Magic constants | `__LINE__`, call-site `__FILE__` / `__DIR__`, empty top-level eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, eval-declared-function `__FUNCTION__` / `__METHOD__`, eval-declared-method `__FUNCTION__` / `__METHOD__` / `__CLASS__` / `__TRAIT__`, eval class-like constant/property initializers for `__CLASS__` / `__TRAIT__`, and reflected parameter defaults using the declaring callable scope, including trait-origin names for imported trait members. | | Constants | Predefined eval-visible constants, dynamic constants from `define()`, namespaced constant fallback, bare constant fetches, `$class::CONST`, expression-valued static receivers for `($expr)::CONST`, braced dynamic class constant names (`ClassName::{$constant}` / `$class::{$constant}` / `($expr)::{$constant}`), and `$object::class` are supported. | @@ -908,10 +908,12 @@ class-system gaps are broader reflection APIs beyond the supported ReflectionClass/Object/Function/Method/Parameter/Property/NamedType/UnionType/IntersectionType and Enum/attribute slice, Reflection type APIs beyond retained parameter, generated -property, and function/method return metadata, broader parameter and generated -property default-value materialization beyond scalar, null, empty-array, -supported array-valued defaults, and supported object-valued parameter defaults -during generated/AOT invocation, and generated/AOT method and constructor +property, and function/method return metadata, generated property default-value +materialization beyond scalar, null, empty-array, and supported array-valued +defaults, parameter defaults beyond representable scalar/string constant +expressions, resolved global/named class-like constants, supported array-valued +defaults, and supported object-valued defaults during generated/AOT invocation, +and generated/AOT method and constructor bridge signatures beyond the current visibility-checked by-reference parameter slice, specialized native-only ABI shapes, and `__clone()` hooks; the remaining limit there is the supported type slice rather than a fixed parameter count. diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index f341b834c5..8481c6907d 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -19,9 +19,9 @@ use crate::codegen::platform::Arch; use crate::codegen::{ abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, }; -use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Op, ValueId}; +use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Module, Op, ValueId}; use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; -use crate::parser::ast::{Expr, ExprKind, TypeExpr, Visibility}; +use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, TypeExpr, Visibility}; use crate::types::{ is_php_integer_array_key, AttrArgValue, ClassInfo, FunctionSig, InterfaceInfo, PhpType, PropertyHookContract, @@ -91,6 +91,7 @@ const NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; const NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; const NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; const MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; +const MAX_NATIVE_DEFAULT_CONSTANT_DEPTH: usize = 16; /// Local slot metadata needed for conservative eval scope synchronization. #[derive(Clone)] @@ -137,6 +138,30 @@ struct EvalNativeConstructorRegistration { bridge_supported: bool, } +/// Static metadata used while converting AOT defaults into eval bridge values. +struct EvalNativeDefaultContext<'a> { + module: &'a Module, + current_class: Option<&'a str>, +} + +impl<'a> EvalNativeDefaultContext<'a> { + /// Builds a default-materialization context for global function defaults. + fn global(module: &'a Module) -> Self { + Self { + module, + current_class: None, + } + } + + /// Builds a default-materialization context for class-like member defaults. + fn for_class(module: &'a Module, class_name: &'a str) -> Self { + Self { + module, + current_class: Some(class_name), + } + } +} + /// A module-local property type that can be registered with the eval context. struct EvalNativePropertyTypeRegistration { class_name: String, @@ -1909,8 +1934,19 @@ fn eval_native_property_default_registrations( let mut classes = ctx.module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - collect_eval_native_instance_property_defaults(class_name, class_info, &mut registrations); - collect_eval_native_static_property_defaults(class_name, class_info, &mut registrations); + let default_context = EvalNativeDefaultContext::for_class(ctx.module, class_name); + collect_eval_native_instance_property_defaults( + class_name, + class_info, + &default_context, + &mut registrations, + ); + collect_eval_native_static_property_defaults( + class_name, + class_info, + &default_context, + &mut registrations, + ); } registrations } @@ -2097,13 +2133,16 @@ fn collect_eval_native_member_attributes( fn collect_eval_native_instance_property_defaults( class_name: &str, class_info: &ClassInfo, + default_context: &EvalNativeDefaultContext<'_>, registrations: &mut Vec, ) { for (slot, (property_name, _)) in class_info.properties.iter().enumerate() { let default = class_info.defaults.get(slot).and_then(Option::as_ref); let is_declared = class_info.property_slot_is_declared(slot, property_name); let is_abstract = class_info.abstract_properties.contains(property_name); - let Some(default) = eval_native_property_default(default, is_declared, is_abstract) else { + let Some(default) = + eval_native_property_default(default, is_declared, is_abstract, default_context) + else { continue; }; registrations.push(EvalNativePropertyDefaultRegistration { @@ -2123,6 +2162,7 @@ fn collect_eval_native_instance_property_defaults( fn collect_eval_native_static_property_defaults( class_name: &str, class_info: &ClassInfo, + default_context: &EvalNativeDefaultContext<'_>, registrations: &mut Vec, ) { for (slot, (property_name, _)) in class_info.static_properties.iter().enumerate() { @@ -2133,7 +2173,9 @@ fn collect_eval_native_static_property_defaults( let is_declared = class_info .declared_static_properties .contains(property_name); - let Some(default) = eval_native_property_default(default, is_declared, false) else { + let Some(default) = + eval_native_property_default(default, is_declared, false, default_context) + else { continue; }; registrations.push(EvalNativePropertyDefaultRegistration { @@ -2597,10 +2639,318 @@ fn eval_native_php_type_member_specs(members: &[PhpType]) -> Option { } /// Converts a PHP signature default into the compact eval bridge default ABI. -fn eval_native_callable_default(expr: &Expr) -> Option { +fn eval_native_callable_default( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, +) -> Option { + eval_native_callable_default_at(expr, default_context, 0) +} + +/// Converts a PHP default expression while preserving a recursion limit for constants. +fn eval_native_callable_default_at( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + if depth > MAX_NATIVE_DEFAULT_CONSTANT_DEPTH { + return None; + } eval_native_literal_default(expr) - .or_else(|| eval_native_object_default(expr)) - .or_else(|| eval_native_array_default(expr)) + .or_else(|| eval_native_object_default(expr, default_context, depth)) + .or_else(|| eval_native_array_default(expr, default_context, depth)) + .or_else(|| eval_native_constant_expression_default(expr, default_context, depth)) +} + +/// Converts representable pure constant expressions into native eval defaults. +fn eval_native_constant_expression_default( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match &expr.kind { + ExprKind::ConstRef(name) => { + eval_native_global_constant_default(default_context, name, depth + 1) + } + ExprKind::ClassConstant { receiver } => { + eval_native_static_receiver_name(default_context, receiver) + .map(EvalNativeCallableDefault::String) + } + ExprKind::ScopedConstantAccess { receiver, name } => { + eval_native_scoped_constant_default(default_context, receiver, name, depth + 1) + } + ExprKind::BinaryOp { left, op, right } => { + eval_native_binary_expression_default(left, op, right, default_context, depth + 1) + } + ExprKind::Not(inner) => { + eval_native_default_truthy(&eval_native_callable_default_at( + inner, + default_context, + depth + 1, + )?) + .map(|value| eval_native_bool_default(!value)) + } + ExprKind::BitNot(inner) => eval_native_default_int(inner, default_context, depth + 1) + .map(|value| eval_native_int_default(!value)), + ExprKind::NullCoalesce { value, default } => { + let value = eval_native_callable_default_at(value, default_context, depth + 1)?; + if eval_native_default_is_null(&value) { + eval_native_callable_default_at(default, default_context, depth + 1) + } else { + Some(value) + } + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + if eval_native_default_truthy(&eval_native_callable_default_at( + condition, + default_context, + depth + 1, + )?)? { + eval_native_callable_default_at(then_expr, default_context, depth + 1) + } else { + eval_native_callable_default_at(else_expr, default_context, depth + 1) + } + } + ExprKind::ShortTernary { value, default } => { + let value = eval_native_callable_default_at(value, default_context, depth + 1)?; + if eval_native_default_truthy(&value)? { + Some(value) + } else { + eval_native_callable_default_at(default, default_context, depth + 1) + } + } + _ => None, + } +} + +/// Converts one supported binary constant expression into a native eval default. +fn eval_native_binary_expression_default( + left: &Expr, + op: &BinOp, + right: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match op { + BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow => { + eval_native_numeric_binary_default(left, op, right, default_context, depth + 1) + } + BinOp::Mod => { + let left = eval_native_default_int(left, default_context, depth + 1)?; + let right = eval_native_default_int(right, default_context, depth + 1)?; + (right != 0).then(|| eval_native_int_default(left % right)) + } + BinOp::Concat => { + let left = eval_native_default_string(left, default_context, depth + 1)?; + let right = eval_native_default_string(right, default_context, depth + 1)?; + Some(EvalNativeCallableDefault::String(format!("{left}{right}"))) + } + BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor => { + let left = eval_native_default_int(left, default_context, depth + 1)?; + let right = eval_native_default_int(right, default_context, depth + 1)?; + let value = match op { + BinOp::BitAnd => left & right, + BinOp::BitOr => left | right, + BinOp::BitXor => left ^ right, + _ => unreachable!("bitwise default operator was prefiltered"), + }; + Some(eval_native_int_default(value)) + } + BinOp::ShiftLeft | BinOp::ShiftRight => { + let left = eval_native_default_int(left, default_context, depth + 1)?; + let right = + u32::try_from(eval_native_default_int(right, default_context, depth + 1)?).ok()?; + let value = match op { + BinOp::ShiftLeft => left.checked_shl(right), + BinOp::ShiftRight => left.checked_shr(right), + _ => unreachable!("shift default operator was prefiltered"), + }?; + Some(eval_native_int_default(value)) + } + BinOp::And | BinOp::Or | BinOp::Xor => { + let left = eval_native_default_truthy(&eval_native_callable_default_at( + left, + default_context, + depth + 1, + )?)?; + let right = eval_native_default_truthy(&eval_native_callable_default_at( + right, + default_context, + depth + 1, + )?)?; + let value = match op { + BinOp::And => left && right, + BinOp::Or => left || right, + BinOp::Xor => left ^ right, + _ => unreachable!("logical default operator was prefiltered"), + }; + Some(eval_native_bool_default(value)) + } + BinOp::NullCoalesce => { + let left = eval_native_callable_default_at(left, default_context, depth + 1)?; + if eval_native_default_is_null(&left) { + eval_native_callable_default_at(right, default_context, depth + 1) + } else { + Some(left) + } + } + _ => None, + } +} + +/// Converts one supported arithmetic expression into a native eval default. +fn eval_native_numeric_binary_default( + left: &Expr, + op: &BinOp, + right: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + if let (Some(left), Some(right)) = ( + eval_native_default_int(left, default_context, depth + 1), + eval_native_default_int(right, default_context, depth + 1), + ) { + return match op { + BinOp::Add => left.checked_add(right).map(eval_native_int_default), + BinOp::Sub => left.checked_sub(right).map(eval_native_int_default), + BinOp::Mul => left.checked_mul(right).map(eval_native_int_default), + BinOp::Div if right != 0 => { + Some(eval_native_float_default(left as f64 / right as f64)) + } + BinOp::Pow => { + let value = (left as f64).powf(right as f64); + value.is_finite().then(|| eval_native_float_default(value)) + } + _ => None, + }; + } + + let left = eval_native_default_numeric(left, default_context, depth + 1)?; + let right = eval_native_default_numeric(right, default_context, depth + 1)?; + let value = match op { + BinOp::Add => left + right, + BinOp::Sub => left - right, + BinOp::Mul => left * right, + BinOp::Div if right != 0.0 => left / right, + BinOp::Pow => left.powf(right), + _ => return None, + }; + value.is_finite().then(|| eval_native_float_default(value)) +} + +/// Builds one bool default metadata value. +fn eval_native_bool_default(value: bool) -> EvalNativeCallableDefault { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_BOOL, + payload: i64::from(value), + } +} + +/// Builds one int default metadata value. +fn eval_native_int_default(value: i64) -> EvalNativeCallableDefault { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload: value, + } +} + +/// Builds one float default metadata value. +fn eval_native_float_default(value: f64) -> EvalNativeCallableDefault { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload: value.to_bits() as i64, + } +} + +/// Returns true when one default metadata value is PHP `null`. +fn eval_native_default_is_null(default: &EvalNativeCallableDefault) -> bool { + matches!( + default, + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + .. + } + ) +} + +/// Returns PHP truthiness for one representable native eval default. +fn eval_native_default_truthy(default: &EvalNativeCallableDefault) -> Option { + match default { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + .. + } => Some(false), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_BOOL, + payload, + } => Some(*payload != 0), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + } => Some(*payload != 0), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload, + } => Some(f64::from_bits(*payload as u64) != 0.0), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_EMPTY_ARRAY, + .. + } => Some(false), + EvalNativeCallableDefault::String(value) => { + Some(!value.is_empty() && value != "0") + } + EvalNativeCallableDefault::Array(_) | EvalNativeCallableDefault::Object { .. } => None, + EvalNativeCallableDefault::Scalar { .. } => None, + } +} + +/// Extracts an int value from one representable default expression. +fn eval_native_default_int( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match eval_native_callable_default_at(expr, default_context, depth)? { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + } => Some(payload), + _ => None, + } +} + +/// Extracts a numeric value from one representable default expression. +fn eval_native_default_numeric( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match eval_native_callable_default_at(expr, default_context, depth)? { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + } => Some(payload as f64), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload, + } => Some(f64::from_bits(payload as u64)), + _ => None, + } +} + +/// Extracts a string value from one representable default expression. +fn eval_native_default_string( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + match eval_native_callable_default_at(expr, default_context, depth)? { + EvalNativeCallableDefault::String(value) => Some(value), + _ => None, + } } /// Converts scalar/string/empty-array defaults into the compact eval bridge default ABI. @@ -2635,7 +2985,11 @@ fn eval_native_literal_default(expr: &Expr) -> Option } /// Converts supported object-valued defaults into compact eval bridge metadata. -fn eval_native_object_default(expr: &Expr) -> Option { +fn eval_native_object_default( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { let ExprKind::NewObject { class_name, args } = &expr.kind else { return None; }; @@ -2644,7 +2998,11 @@ fn eval_native_object_default(expr: &Expr) -> Option } let mut default_args = Vec::with_capacity(args.len()); for arg in args { - default_args.push(eval_native_object_default_arg(arg)?); + default_args.push(eval_native_object_default_arg( + arg, + default_context, + depth + 1, + )?); } Some(EvalNativeCallableDefault::Object { class_name: class_name.as_canonical(), @@ -2653,22 +3011,30 @@ fn eval_native_object_default(expr: &Expr) -> Option } /// Converts one object-valued default constructor argument into bridge metadata. -fn eval_native_object_default_arg(expr: &Expr) -> Option { +fn eval_native_object_default_arg( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { match &expr.kind { ExprKind::NamedArg { name, value } => Some(EvalNativeCallableObjectDefaultArg { name: Some(name.clone()), - default: eval_native_callable_default(value)?, + default: eval_native_callable_default_at(value, default_context, depth + 1)?, }), ExprKind::Spread(_) => None, _ => Some(EvalNativeCallableObjectDefaultArg { name: None, - default: eval_native_callable_default(expr)?, + default: eval_native_callable_default_at(expr, default_context, depth + 1)?, }), } } /// Converts supported array-valued defaults into compact eval bridge metadata. -fn eval_native_array_default(expr: &Expr) -> Option { +fn eval_native_array_default( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { match &expr.kind { ExprKind::ArrayLiteral(elements) => { let mut default_elements = Vec::with_capacity(elements.len()); @@ -2678,7 +3044,11 @@ fn eval_native_array_default(expr: &Expr) -> Option { } default_elements.push(EvalNativeCallableArrayDefaultElement { key: None, - default: eval_native_callable_default(element)?, + default: eval_native_callable_default_at( + element, + default_context, + depth + 1, + )?, }); } Some(EvalNativeCallableDefault::Array(default_elements)) @@ -2687,8 +3057,16 @@ fn eval_native_array_default(expr: &Expr) -> Option { let mut default_elements = Vec::with_capacity(elements.len()); for (key, value) in elements { default_elements.push(EvalNativeCallableArrayDefaultElement { - key: Some(eval_native_array_default_key(key)?), - default: eval_native_callable_default(value)?, + key: Some(eval_native_array_default_key( + key, + default_context, + depth + 1, + )?), + default: eval_native_callable_default_at( + value, + default_context, + depth + 1, + )?, }); } Some(EvalNativeCallableDefault::Array(default_elements)) @@ -2698,7 +3076,208 @@ fn eval_native_array_default(expr: &Expr) -> Option { } /// Converts one supported static array key into bridge metadata. -fn eval_native_array_default_key(expr: &Expr) -> Option { +fn eval_native_array_default_key( + expr: &Expr, + default_context: &EvalNativeDefaultContext<'_>, + depth: usize, +) -> Option { + if let Some(key) = eval_native_literal_array_default_key(expr) { + return Some(key); + } + match eval_native_callable_default_at(expr, default_context, depth + 1)? { + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_NULL, + .. + } => Some(EvalNativeCallableArrayDefaultKey::String(String::new())), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_BOOL, + payload, + } => Some(EvalNativeCallableArrayDefaultKey::Int((payload != 0) as i64)), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_INT, + payload, + } => Some(EvalNativeCallableArrayDefaultKey::Int(payload)), + EvalNativeCallableDefault::Scalar { + kind: NATIVE_DEFAULT_FLOAT, + payload, + } => Some(EvalNativeCallableArrayDefaultKey::Int( + f64::from_bits(payload as u64) as i64, + )), + EvalNativeCallableDefault::String(value) => { + eval_native_string_array_default_key(&value) + } + _ => None, + } +} + +/// Resolves and materializes one global constant default expression. +fn eval_native_global_constant_default( + default_context: &EvalNativeDefaultContext<'_>, + name: &str, + depth: usize, +) -> Option { + let expr_kind = default_context + .module + .global_constants + .get(name) + .or_else(|| { + default_context + .module + .global_constants + .get(name.trim_start_matches('\\')) + }) + .map(|(expr_kind, _)| expr_kind.clone())?; + let expr = Expr::new(expr_kind, crate::span::Span::dummy()); + eval_native_callable_default_at(&expr, default_context, depth + 1) +} + +/// Resolves and materializes one class-like constant default expression. +fn eval_native_scoped_constant_default( + default_context: &EvalNativeDefaultContext<'_>, + receiver: &StaticReceiver, + constant_name: &str, + depth: usize, +) -> Option { + let class_name = eval_native_static_receiver_name(default_context, receiver)?; + if let Some((declaring_name, value)) = + eval_native_class_constant_expr(default_context.module, &class_name, constant_name) + { + let nested_context = + EvalNativeDefaultContext::for_class(default_context.module, declaring_name); + return eval_native_callable_default_at(value, &nested_context, depth + 1); + } + if let Some((declaring_name, value)) = + eval_native_interface_constant_expr(default_context.module, &class_name, constant_name) + { + let nested_context = + EvalNativeDefaultContext::for_class(default_context.module, declaring_name); + return eval_native_callable_default_at(value, &nested_context, depth + 1); + } + if let Some((declaring_name, value)) = + eval_native_trait_constant_expr(default_context.module, &class_name, constant_name) + { + let nested_context = + EvalNativeDefaultContext::for_class(default_context.module, declaring_name); + return eval_native_callable_default_at(value, &nested_context, depth + 1); + } + None +} + +/// Resolves `self`, `static`, `parent`, or a named receiver for default constants. +fn eval_native_static_receiver_name( + default_context: &EvalNativeDefaultContext<'_>, + receiver: &StaticReceiver, +) -> Option { + match receiver { + StaticReceiver::Named(name) => { + Some(name.as_canonical().trim_start_matches('\\').to_string()) + } + StaticReceiver::Self_ | StaticReceiver::Static => { + default_context.current_class.map(str::to_string) + } + StaticReceiver::Parent => { + let current = default_context.current_class?; + resolve_eval_native_default_class(default_context.module, current) + .and_then(|(_, class_info)| class_info.parent.clone()) + } + } +} + +/// Looks up a class constant expression, including inherited parent classes. +fn eval_native_class_constant_expr<'a>( + module: &'a Module, + class_name: &str, + constant_name: &str, +) -> Option<(&'a str, &'a Expr)> { + let (resolved_name, class_info) = resolve_eval_native_default_class(module, class_name)?; + if let Some(value) = class_info.constants.get(constant_name) { + return Some((resolved_name, value)); + } + for interface_name in &class_info.interfaces { + if let Some(value) = + eval_native_interface_constant_expr(module, interface_name, constant_name) + { + return Some(value); + } + } + if let Some(parent_name) = class_info.parent.as_deref() { + return eval_native_class_constant_expr(module, parent_name, constant_name); + } + None +} + +/// Looks up an interface constant expression, including inherited interfaces. +fn eval_native_interface_constant_expr<'a>( + module: &'a Module, + interface_name: &str, + constant_name: &str, +) -> Option<(&'a str, &'a Expr)> { + let mut visited = std::collections::HashSet::new(); + let mut queue = vec![interface_name.to_string()]; + while let Some(name) = queue.pop() { + let Some((resolved_name, interface_info)) = + resolve_eval_native_default_interface(module, &name) + else { + continue; + }; + if !visited.insert(php_symbol_key(resolved_name.trim_start_matches('\\'))) { + continue; + } + if let Some(value) = interface_info.constants.get(constant_name) { + return Some((resolved_name, value)); + } + queue.extend(interface_info.parents.iter().cloned()); + } + None +} + +/// Looks up a direct trait constant expression by PHP-style trait name. +fn eval_native_trait_constant_expr<'a>( + module: &'a Module, + trait_name: &str, + constant_name: &str, +) -> Option<(&'a str, &'a Expr)> { + let trait_key = php_symbol_key(trait_name.trim_start_matches('\\')); + let resolved_name = module + .trait_table + .names + .iter() + .find(|candidate| php_symbol_key(candidate.trim_start_matches('\\')) == trait_key)?; + let value = module + .declared_trait_constants + .get(resolved_name) + .and_then(|constants| constants.get(constant_name))?; + Some((resolved_name.as_str(), value)) +} + +/// Looks up class metadata by PHP-style case-insensitive name. +fn resolve_eval_native_default_class<'a>( + module: &'a Module, + class_name: &str, +) -> Option<(&'a str, &'a ClassInfo)> { + let class_key = php_symbol_key(class_name.trim_start_matches('\\')); + module + .class_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == class_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Looks up interface metadata by PHP-style case-insensitive name. +fn resolve_eval_native_default_interface<'a>( + module: &'a Module, + interface_name: &str, +) -> Option<(&'a str, &'a InterfaceInfo)> { + let interface_key = php_symbol_key(interface_name.trim_start_matches('\\')); + module + .interface_infos + .iter() + .find(|(candidate, _)| php_symbol_key(candidate.trim_start_matches('\\')) == interface_key) + .map(|(name, info)| (name.as_str(), info)) +} + +/// Converts one literal static array key into bridge metadata. +fn eval_native_literal_array_default_key(expr: &Expr) -> Option { match &expr.kind { ExprKind::IntLiteral(value) => Some(EvalNativeCallableArrayDefaultKey::Int(*value)), ExprKind::BoolLiteral(value) => { @@ -2739,9 +3318,11 @@ fn eval_native_property_default( default: Option<&Expr>, is_declared: bool, is_abstract: bool, + default_context: &EvalNativeDefaultContext<'_>, ) -> Option { if let Some(default) = default { - return eval_native_literal_default(default).or_else(|| eval_native_array_default(default)); + return eval_native_literal_default(default) + .or_else(|| eval_native_array_default(default, default_context, 0)); } (!is_declared && !is_abstract).then_some(EvalNativeCallableDefault::Scalar { kind: NATIVE_DEFAULT_NULL, @@ -2983,8 +3564,12 @@ fn register_eval_native_function( ); } } + let default_context = EvalNativeDefaultContext::global(ctx.module); for (index, default) in registration.signature.defaults.iter().enumerate() { - let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { + let Some(default) = default + .as_ref() + .and_then(|expr| eval_native_callable_default(expr, &default_context)) + else { continue; }; register_eval_native_function_param_default( @@ -3088,8 +3673,13 @@ fn register_eval_native_method( ); } } + let default_context = + EvalNativeDefaultContext::for_class(ctx.module, ®istration.class_name); for (index, default) in registration.signature.defaults.iter().enumerate() { - let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { + let Some(default) = default + .as_ref() + .and_then(|expr| eval_native_callable_default(expr, &default_context)) + else { continue; }; register_eval_native_method_param_default( @@ -3532,8 +4122,13 @@ fn register_eval_native_constructor( ); } } + let default_context = + EvalNativeDefaultContext::for_class(ctx.module, ®istration.class_name); for (index, default) in registration.signature.defaults.iter().enumerate() { - let Some(default) = default.as_ref().and_then(eval_native_callable_default) else { + let Some(default) = default + .as_ref() + .and_then(|expr| eval_native_callable_default(expr, &default_context)) + else { continue; }; register_eval_native_constructor_param_default( diff --git a/src/ir/module.rs b/src/ir/module.rs index c9b08b71f6..b4b603529f 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -15,7 +15,7 @@ use crate::codegen::platform::Target; use crate::codegen::RuntimeFeatures; use crate::ir::function::{Function, FunctionId}; use crate::ir::types::IrType; -use crate::parser::ast::Visibility; +use crate::parser::ast::{ExprKind, Visibility}; use crate::types::{ ClassInfo, EnumInfo, ExternClassInfo, FunctionSig, InterfaceInfo, PackedClassInfo, PhpType, }; @@ -77,6 +77,8 @@ pub struct Module { pub declared_trait_constants: HashMap>, pub declared_trait_constant_visibilities: HashMap>, pub declared_trait_final_constants: HashMap>, + /// Prescanned global constant values used by EIR lowering and eval metadata registration. + pub global_constants: HashMap, pub class_infos: HashMap, pub interface_infos: HashMap, pub enum_infos: HashMap, @@ -119,6 +121,7 @@ impl Module { declared_trait_constants: HashMap::new(), declared_trait_constant_visibilities: HashMap::new(), declared_trait_final_constants: HashMap::new(), + global_constants: HashMap::new(), class_infos: HashMap::new(), interface_infos: HashMap::new(), enum_infos: HashMap::new(), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 9252ba4cd5..a810f73fea 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -34,6 +34,7 @@ pub(crate) fn lower( let mut module = Module::new(target); module.source_path = source_path.map(canonical_source_path); let constants = crate::codegen::collect_constants(program, target.platform); + module.global_constants = constants.clone(); let fiber_return_sigs = crate::ir_lower::fibers::collect_fiber_return_sigs(program); populate_metadata(&mut module, program, check_result); lower_function_declarations(program, &mut module, check_result, &constants, &fiber_return_sigs); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index caf0be9bb2..c7ca84fa32 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19919,6 +19919,97 @@ return count($default) . ":" . $default[0] . ":" . $default[1];'); assert_eq!(out.stdout, "left:required:right:O:D:B:items:O:2:1:2"); } +/// Verifies eval materializes generated/AOT constant-expression defaults. +#[test] +fn test_eval_aot_callable_constant_expression_defaults() { + let out = compile_and_run_capture( + r#" "two"], + int $global = EVAL_AOT_DEFAULT_GLOBAL_SUM + 1, + string $globalWord = EVAL_AOT_DEFAULT_GLOBAL_WORD . "H", + int $pathFlag = PATHINFO_EXTENSION, + ?string $maybe = null, + float $scale = 1.5 +): string { + return $sum . ":" . $word . ":" . ($flag ? "T" : "F") . ":" . + $items[0] . ":" . $items[1] . ":" . $items[2] . ":" . + $global . ":" . $globalWord . ":" . $pathFlag . ":" . + ($maybe === null ? "null" : "set") . ":" . ($scale > 1.0 ? "float" : "bad"); +} + +class EvalAotDefaultExpressionDep { + public string $label; + + public function __construct(string $label = "base") { + $this->label = $label; + } +} + +interface EvalAotDefaultExpressionIface { + public const IFACE_TAG = "I"; +} + +class EvalAotDefaultExpressionBase { + public const OFFSET = 4; + public const TAG = "B"; +} + +class EvalAotDefaultExpressionTarget extends EvalAotDefaultExpressionBase implements EvalAotDefaultExpressionIface { + public const LOCAL = 3; + public const LABEL = "L"; + + public string $label; + + public function __construct( + string $prefix = EvalAotDefaultExpressionTarget::LABEL . EvalAotDefaultExpressionBase::TAG, + int $count = EvalAotDefaultExpressionTarget::LOCAL + EvalAotDefaultExpressionBase::OFFSET + ) { + $this->label = $prefix . ":" . $count; + } + + public function describe( + EvalAotDefaultExpressionDep $dep = new EvalAotDefaultExpressionDep(EvalAotDefaultExpressionBase::TAG . "E"), + array $items = [ + EvalAotDefaultExpressionTarget::LOCAL + EvalAotDefaultExpressionBase::OFFSET, + EvalAotDefaultExpressionIface::IFACE_TAG + ] + ): string { + return $dep->label . ":" . $items[0] . ":" . $items[1]; + } + + public function className(string $name = EvalAotDefaultExpressionTarget::class): string { + return $name; + } +} + +echo eval('$target = new EvalAotDefaultExpressionTarget(); +$ref = new ReflectionFunction("eval_aot_default_expression_function"); +$items = $ref->getParameters()[3]->getDefaultValue(); +return eval_aot_default_expression_function() . "|" + . $target->describe() . "|" + . $target->label . "|" + . $target->className() . "|" + . $items[0] . ":" . $items[2];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "3:AB:T:6:xy:two:10:GH:4:null:float|BE:7:I|LB:7|EvalAotDefaultExpressionTarget|6:two" + ); +} + /// Verifies eval ReflectionParameter exposes generated/AOT function type flags. #[test] fn test_eval_reflection_parameter_exposes_aot_function_type_flags() { From 7c2887caafc2ae038fe82aa70eb8abc6a5cdcba3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 16:52:31 +0200 Subject: [PATCH 0935/1208] Fill eval builtin parity gaps --- crates/elephc-magician/src/context.rs | 30 ++ .../interpreter/builtins/registry/binding.rs | 16 +- .../builtins/registry/dispatch/scalars.rs | 3 +- .../builtins/registry/dispatch/time.rs | 65 ++++- .../interpreter/builtins/registry/names.rs | 11 + .../builtins/registry/signature.rs | 15 +- .../src/interpreter/builtins/scalars/types.rs | 4 + .../src/interpreter/builtins/time/calendar.rs | 274 ++++++++++++++++++ .../src/interpreter/builtins/time/date.rs | 92 +++++- .../src/interpreter/builtins/time/mktime.rs | 57 +++- .../src/interpreter/builtins/time/mod.rs | 2 + .../interpreter/builtins/time/strtotime.rs | 12 +- .../src/interpreter/builtins/time/system.rs | 122 ++++++++ .../src/interpreter/expressions.rs | 19 +- .../src/interpreter/tests/builtins_scalars.rs | 27 ++ .../tests/builtins_system_network.rs | 67 +++++ docs/php/eval.md | 4 +- 17 files changed, 788 insertions(+), 32 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/calendar.rs diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 07595076d2..21e5303857 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -802,6 +802,8 @@ pub struct ElephcEvalContext { streams: EvalStreamResources, json_last_error: i64, json_last_error_msg: String, + default_timezone: String, + http_response_code: i64, call_file: String, call_dir: String, call_line: i64, @@ -868,6 +870,8 @@ impl ElephcEvalContext { streams: EvalStreamResources::default(), json_last_error: 0, json_last_error_msg: String::from("No error"), + default_timezone: String::from("UTC"), + http_response_code: 200, call_file: String::new(), call_dir: String::new(), call_line: 0, @@ -935,6 +939,8 @@ impl ElephcEvalContext { streams: EvalStreamResources::default(), json_last_error: 0, json_last_error_msg: String::from("No error"), + default_timezone: String::from("UTC"), + http_response_code: 200, call_file: String::new(), call_dir: String::new(), call_line: 0, @@ -3688,6 +3694,30 @@ impl ElephcEvalContext { &self.json_last_error_msg } + /// Returns the eval-local PHP default timezone identifier. + pub fn default_timezone(&self) -> &str { + &self.default_timezone + } + + /// Replaces the eval-local PHP default timezone identifier. + pub fn set_default_timezone(&mut self, timezone: impl Into) { + self.default_timezone = timezone.into(); + } + + /// Returns the eval-local HTTP response code used by web-facing builtins. + pub const fn http_response_code(&self) -> i64 { + self.http_response_code + } + + /// Applies a new eval-local HTTP response code and returns the previous one. + pub fn replace_http_response_code(&mut self, response_code: i64) -> i64 { + let previous = self.http_response_code; + if response_code > 0 { + self.http_response_code = response_code; + } + previous + } + /// Updates the source file, directory, and line for the current eval call site. pub fn set_call_site(&mut self, file: impl Into, dir: impl Into, line: i64) { self.call_file = file.into(); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 26dd3d112e..609022d30b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -158,7 +158,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "boolval" | "empty" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_null" | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" - | "is_callable" | "strval" => Some(&["value"]), + | "is_scalar" | "is_callable" | "strval" => Some(&["value"]), "is_finite" | "is_infinite" | "is_nan" => Some(&["num"]), "settype" => Some(&["var", "type"]), "get_called_class" => Some(&[]), @@ -193,7 +193,10 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "copy" | "rename" => Some(&["from", "to"]), "crc32" => Some(&["string"]), "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), - "date" => Some(&["format", "timestamp"]), + "checkdate" => Some(&["month", "day", "year"]), + "date" | "gmdate" => Some(&["format", "timestamp"]), + "date_default_timezone_get" => Some(&[]), + "date_default_timezone_set" => Some(&["timezoneId"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "die" | "exit" => Some(&["status"]), @@ -234,6 +237,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "ftruncate" => Some(&["stream", "size"]), "fwrite" => Some(&["stream", "data"]), "function_exists" => Some(&["function"]), + "getdate" => Some(&["timestamp"]), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), "gethostbyaddr" => Some(&["ip"]), "gethostbyname" => Some(&["hostname"]), @@ -255,6 +259,9 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "hash_hmac" => Some(&["algo", "data", "key", "binary"]), "hash_init" => Some(&["algo", "flags", "key"]), "hash_update" => Some(&["context", "data"]), + "header" => Some(&["header", "replace", "response_code"]), + "hrtime" => Some(&["as_number"]), + "http_response_code" => Some(&["response_code"]), "gzcompress" | "gzdeflate" => Some(&["data", "level"]), "gzinflate" | "gzuncompress" => Some(&["data", "max_length"]), "hypot" => Some(&["x", "y"]), @@ -277,8 +284,11 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "log" => Some(&["num", "base"]), "max" | "min" => Some(&["value", "values"]), "md5" | "sha1" => Some(&["string", "binary"]), + "localtime" => Some(&["timestamp", "associative"]), "microtime" => Some(&["as_float"]), - "mktime" => Some(&["hour", "minute", "second", "month", "day", "year"]), + "mktime" | "gmmktime" => { + Some(&["hour", "minute", "second", "month", "day", "year"]) + } "nl2br" => Some(&["string", "use_xhtml"]), "number_format" => Some(&[ "num", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs index 0afec7d3f9..8516925d4b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs @@ -99,7 +99,8 @@ pub(in crate::interpreter) fn eval_scalars_builtin_with_values( } "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { + | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_scalar" + | "is_string" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs index 54cbfd9aad..bb77cfb230 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs @@ -15,24 +15,75 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_time_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "date" => match evaluated_args { - [format] => eval_date_result(*format, None, values)?, - [format, timestamp] => eval_date_result(*format, Some(*timestamp), values)?, + "checkdate" => { + let [month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_checkdate_result(*month, *day, *year, values)? + } + "date" | "gmdate" => match evaluated_args { + [format] => eval_date_result(name, *format, None, context, values)?, + [format, timestamp] => eval_date_result(name, *format, Some(*timestamp), context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "date_default_timezone_get" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_date_default_timezone_get_result(context, values)? + } + "date_default_timezone_set" => { + let [timezone] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_date_default_timezone_set_result(*timezone, context, values)? + } + "getdate" => match evaluated_args { + [] => eval_getdate_result(None, context, values)?, + [timestamp] => eval_getdate_result(Some(*timestamp), context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "hrtime" => match evaluated_args { + [] => eval_hrtime_result(None, values)?, + [as_number] => eval_hrtime_result(Some(*as_number), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "header" => match evaluated_args { + [line] => eval_header_result(*line, None, None, context, values)?, + [line, replace] => eval_header_result(*line, Some(*replace), None, context, values)?, + [line, replace, response_code] => { + eval_header_result(*line, Some(*replace), Some(*response_code), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "http_response_code" => match evaluated_args { + [] => eval_http_response_code_result(None, context, values)?, + [response_code] => eval_http_response_code_result(Some(*response_code), context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "localtime" => match evaluated_args { + [] => eval_localtime_result(None, None, context, values)?, + [timestamp] => eval_localtime_result(Some(*timestamp), None, context, values)?, + [timestamp, associative] => { + eval_localtime_result(Some(*timestamp), Some(*associative), context, values)? + } _ => return Err(EvalStatus::RuntimeFatal), }, "microtime" => match evaluated_args { [] | [_] => eval_microtime_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "mktime" => { + "mktime" | "gmmktime" => { let [hour, minute, second, month, day, year] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_mktime_result(*hour, *minute, *second, *month, *day, *year, values)? + eval_mktime_result( + name, *hour, *minute, *second, *month, *day, *year, context, values, + )? } "sleep" => { let [seconds] = evaluated_args else { @@ -50,7 +101,7 @@ pub(in crate::interpreter) fn eval_time_builtin_with_values( let [datetime] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_strtotime_result(*datetime, values)? + eval_strtotime_result(*datetime, context, values)? } "usleep" => { let [microseconds] = evaluated_args else { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 539781a6ee..a616664167 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -83,7 +83,10 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ctype_alpha", "ctype_digit", "ctype_space", + "checkdate", "date", + "date_default_timezone_get", + "date_default_timezone_set", "define", "defined", "deg2rad", @@ -137,6 +140,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ftruncate", "function_exists", "fwrite", + "getdate", "get_called_class", "get_class", "get_class_methods", @@ -158,6 +162,8 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "getservbyname", "getservbyport", "gettype", + "gmdate", + "gmmktime", "glob", "grapheme_strrev", "gzcompress", @@ -173,10 +179,13 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "hash_hmac", "hash_init", "hash_update", + "header", "hex2bin", "html_entity_decode", "htmlentities", "htmlspecialchars", + "hrtime", + "http_response_code", "hypot", "implode", "in_array", @@ -209,6 +218,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "is_readable", "is_real", "is_resource", + "is_scalar", "is_string", "is_subclass_of", "is_writable", @@ -229,6 +239,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "lchown", "link", "linkinfo", + "localtime", "log", "log10", "log2", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 60830d9249..4294e51fb1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -66,6 +66,10 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "is_a" | "is_subclass_of" => optional(params, 2), "count" => optional(params, 1), + "getdate" | "hrtime" => optional(params, 0), + "header" => optional(params, 1), + "http_response_code" => optional(params, 0), + "localtime" => optional(params, 0), "microtime" | "php_uname" | "readline" | "umask" | "exit" | "die" => { optional(params, 0) } @@ -106,7 +110,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "call_user_func" => variadic(params, &[]), - "log" | "round" | "date" | "nl2br" => optional(params, 1), + "log" | "round" | "date" | "gmdate" | "nl2br" => optional(params, 1), "min" | "max" => variadic(params, &[]), "json_encode" | "json_decode" | "json_validate" => optional(params, 1), @@ -165,6 +169,13 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_subclass_of", 2) => Bool(true), ("count", 1) => Int(0), + ("getdate", 0) => Null, + ("header", 1) => Bool(true), + ("header", 2) => Int(0), + ("hrtime", 0) => Bool(false), + ("http_response_code", 0) => Int(0), + ("localtime", 0) => Null, + ("localtime", 1) => Bool(false), ("microtime", 0) => Bool(false), ("php_uname", 0) => String("a"), ("readline" | "umask", 0) => Null, @@ -205,7 +216,7 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("log", 1) => Float(std::f64::consts::E), ("round", 1) => Int(0), - ("date", 1) => Null, + ("date" | "gmdate", 1) => Null, ("nl2br", 1) => Bool(true), ("json_encode", 1) => Int(0), ("json_encode", 2) => Int(512), diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs index 3f9e0c009b..80e25a0492 100644 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs @@ -310,6 +310,10 @@ pub(in crate::interpreter) fn eval_type_predicate_result( "is_iterable" => eval_is_iterable_value(tag, value, context, values)?, "is_object" => tag == EVAL_TAG_OBJECT, "is_resource" => tag == EVAL_TAG_RESOURCE, + "is_scalar" => matches!( + tag, + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_STRING | EVAL_TAG_BOOL + ), "is_nan" => eval_float_value(value, values)?.is_nan(), "is_infinite" => eval_float_value(value, values)?.is_infinite(), "is_finite" => eval_float_value(value, values)?.is_finite(), diff --git a/crates/elephc-magician/src/interpreter/builtins/time/calendar.rs b/crates/elephc-magician/src/interpreter/builtins/time/calendar.rs new file mode 100644 index 0000000000..a36005ef6c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/calendar.rs @@ -0,0 +1,274 @@ +//! Purpose: +//! Implements calendar decomposition helpers such as `checkdate()`, `getdate()`, +//! `localtime()`, and `hrtime()` for eval builtin execution. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` re-exports. +//! +//! Key details: +//! - Local calendar decomposition uses the eval context timezone, which defaults +//! to UTC to match elephc's native runtime initialization. + +use super::super::super::*; +use super::super::*; +use super::*; + +const EVAL_LOCALTIME_KEYS: &[&str; 9] = &[ + "tm_sec", "tm_min", "tm_hour", "tm_mday", "tm_mon", "tm_year", "tm_wday", "tm_yday", + "tm_isdst", +]; + +/// Evaluates PHP `checkdate(month, day, year)` over three eval expressions. +pub(in crate::interpreter) fn eval_builtin_checkdate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [month, day, year] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let month = eval_expr(month, context, scope, values)?; + let day = eval_expr(day, context, scope, values)?; + let year = eval_expr(year, context, scope, values)?; + eval_checkdate_result(month, day, year, values) +} + +/// Returns whether the supplied month/day/year tuple is a valid Gregorian date. +pub(in crate::interpreter) fn eval_checkdate_result( + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let month = eval_int_value(month, values)?; + let day = eval_int_value(day, values)?; + let year = eval_int_value(year, values)?; + values.bool_value(eval_checkdate_parts(month, day, year)) +} + +/// Tests PHP `checkdate()` bounds and leap-year behavior for integer components. +fn eval_checkdate_parts(month: i64, day: i64, year: i64) -> bool { + if !(1..=12).contains(&month) || !(1..=32767).contains(&year) { + return false; + } + let days = match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if eval_is_leap_year(year) => 29, + 2 => 28, + _ => return false, + }; + (1..=days).contains(&day) +} + +/// Returns whether one Gregorian year is a leap year. +fn eval_is_leap_year(year: i64) -> bool { + (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} + +/// Evaluates PHP `getdate($timestamp = null)`. +pub(in crate::interpreter) fn eval_builtin_getdate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_getdate_result(None, context, values), + [timestamp] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_getdate_result(Some(timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds PHP's `getdate()` associative array for one optional timestamp. +pub(in crate::interpreter) fn eval_getdate_result( + timestamp: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_optional_timestamp(timestamp, values)?; + let tm = eval_context_localtime(timestamp, context)?; + let mut result = values.assoc_new(11)?; + result = eval_array_set_string_int(result, "seconds", i64::from(tm.tm_sec), values)?; + result = eval_array_set_string_int(result, "minutes", i64::from(tm.tm_min), values)?; + result = eval_array_set_string_int(result, "hours", i64::from(tm.tm_hour), values)?; + result = eval_array_set_string_int(result, "mday", i64::from(tm.tm_mday), values)?; + result = eval_array_set_string_int(result, "wday", i64::from(tm.tm_wday), values)?; + result = eval_array_set_string_int(result, "mon", i64::from(tm.tm_mon + 1), values)?; + result = eval_array_set_string_int(result, "year", i64::from(tm.tm_year + 1900), values)?; + result = eval_array_set_string_int(result, "yday", i64::from(tm.tm_yday), values)?; + result = eval_array_set_string_str( + result, + "weekday", + EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(&tm)?], + values, + )?; + result = eval_array_set_string_str( + result, + "month", + EVAL_MONTH_NAMES[eval_tm_month_index(&tm)?], + values, + )?; + eval_array_set_int_int(result, 0, timestamp, values) +} + +/// Evaluates PHP `localtime($timestamp = null, $associative = false)`. +pub(in crate::interpreter) fn eval_builtin_localtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_localtime_result(None, None, context, values), + [timestamp] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_localtime_result(Some(timestamp), None, context, values) + } + [timestamp, associative] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + eval_localtime_result(Some(timestamp), Some(associative), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds PHP's `localtime()` array for one optional timestamp and key mode. +pub(in crate::interpreter) fn eval_localtime_result( + timestamp: Option, + associative: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_optional_timestamp(timestamp, values)?; + let associative = associative + .map(|value| values.truthy(value)) + .transpose()? + .unwrap_or(false); + let tm = eval_context_localtime(timestamp, context)?; + let fields = [ + tm.tm_sec, + tm.tm_min, + tm.tm_hour, + tm.tm_mday, + tm.tm_mon, + tm.tm_year, + tm.tm_wday, + tm.tm_yday, + tm.tm_isdst, + ]; + if associative { + let mut result = values.assoc_new(fields.len())?; + for (key, value) in EVAL_LOCALTIME_KEYS.iter().zip(fields) { + result = eval_array_set_string_int(result, key, i64::from(value), values)?; + } + return Ok(result); + } + let mut result = values.array_new(fields.len())?; + for (index, value) in fields.into_iter().enumerate() { + result = eval_array_set_int_int(result, index as i64, i64::from(value), values)?; + } + Ok(result) +} + +/// Evaluates PHP `hrtime($as_number = false)`. +pub(in crate::interpreter) fn eval_builtin_hrtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_hrtime_result(None, values), + [as_number] => { + let as_number = eval_expr(as_number, context, scope, values)?; + eval_hrtime_result(Some(as_number), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns monotonic time as either nanoseconds or `[seconds, nanoseconds]`. +pub(in crate::interpreter) fn eval_hrtime_result( + as_number: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (seconds, nanoseconds) = eval_monotonic_time()?; + let as_number = as_number + .map(|value| values.truthy(value)) + .transpose()? + .unwrap_or(false); + if as_number { + let total = seconds + .checked_mul(1_000_000_000) + .and_then(|value| value.checked_add(nanoseconds)) + .ok_or(EvalStatus::RuntimeFatal)?; + return values.int(total); + } + let mut result = values.array_new(2)?; + result = eval_array_set_int_int(result, 0, seconds, values)?; + eval_array_set_int_int(result, 1, nanoseconds, values) +} + +/// Reads the monotonic clock in whole seconds and nanoseconds. +fn eval_monotonic_time() -> Result<(i64, i64), EvalStatus> { + let mut timespec = MaybeUninit::::uninit(); + let status = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, timespec.as_mut_ptr()) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + let timespec = unsafe { timespec.assume_init() }; + Ok((timespec.tv_sec, timespec.tv_nsec)) +} + +/// Coerces an optional timestamp argument, treating null/omitted as the current time. +fn eval_optional_timestamp( + timestamp: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + match timestamp { + Some(timestamp) if !values.is_null(timestamp)? => eval_int_value(timestamp, values), + _ => eval_current_unix_timestamp(), + } +} + +/// Writes one string-keyed integer entry into a PHP array. +fn eval_array_set_string_int( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Writes one string-keyed string entry into a PHP array. +fn eval_array_set_string_str( + array: RuntimeCellHandle, + key: &str, + value: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string(value)?; + values.array_set(array, key, value) +} + +/// Writes one integer-keyed integer entry into a PHP array. +fn eval_array_set_int_int( + array: RuntimeCellHandle, + key: i64, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/date.rs b/crates/elephc-magician/src/interpreter/builtins/time/date.rs index ba5636b052..b098c107f5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/date.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/date.rs @@ -11,9 +11,19 @@ use super::super::super::*; use super::super::*; use super::*; +use std::os::unix::ffi::OsStrExt; +use std::sync::Mutex; + +static EVAL_TZ_MUTEX: Mutex<()> = Mutex::new(()); + +unsafe extern "C" { + /// Re-reads libc's process-global timezone environment. + fn tzset(); +} /// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. -pub(in crate::interpreter) fn eval_builtin_date( +pub(in crate::interpreter) fn eval_builtin_date_like( + name: &str, args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, @@ -22,12 +32,12 @@ pub(in crate::interpreter) fn eval_builtin_date( match args { [format] => { let format = eval_expr(format, context, scope, values)?; - eval_date_result(format, None, values) + eval_date_result(name, format, None, context, values) } [format, timestamp] => { let format = eval_expr(format, context, scope, values)?; let timestamp = eval_expr(timestamp, context, scope, values)?; - eval_date_result(format, Some(timestamp), values) + eval_date_result(name, format, Some(timestamp), context, values) } _ => Err(EvalStatus::RuntimeFatal), } @@ -35,21 +45,36 @@ pub(in crate::interpreter) fn eval_builtin_date( /// Formats one Unix timestamp through PHP `date()` token rules supported by elephc. pub(in crate::interpreter) fn eval_date_result( + name: &str, format: RuntimeCellHandle, timestamp: Option, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let format = values.string_bytes(format)?; let timestamp = match timestamp { - Some(timestamp) => eval_int_value(timestamp, values)?, + Some(timestamp) if !values.is_null(timestamp)? => eval_int_value(timestamp, values)?, None => eval_current_unix_timestamp()?, + Some(_) => eval_current_unix_timestamp()?, + }; + let tm = match name { + "date" => eval_context_localtime(timestamp, context)?, + "gmdate" => eval_gmtime(timestamp)?, + _ => return Err(EvalStatus::UnsupportedConstruct), }; - let tm = eval_localtime(timestamp)?; let output = eval_format_date_bytes(&format, &tm, timestamp)?; values.string_bytes_value(&output) } -/// Converts one Unix timestamp to local broken-down time through libc. +/// Converts one Unix timestamp to eval-timezone broken-down time through libc. +pub(in crate::interpreter) fn eval_context_localtime( + timestamp: i64, + context: &ElephcEvalContext, +) -> Result { + eval_with_timezone(context.default_timezone(), || eval_localtime(timestamp)) +} + +/// Converts one Unix timestamp to process-local broken-down time through libc. pub(in crate::interpreter) fn eval_localtime(timestamp: i64) -> Result { let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; let mut tm = MaybeUninit::::uninit(); @@ -60,6 +85,61 @@ pub(in crate::interpreter) fn eval_localtime(timestamp: i64) -> Result Result { + let raw: libc::time_t = timestamp.try_into().map_err(|_| EvalStatus::RuntimeFatal)?; + let mut tm = MaybeUninit::::uninit(); + let result = unsafe { libc::gmtime_r(&raw, tm.as_mut_ptr()) }; + if result.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(unsafe { tm.assume_init() }) +} + +/// Runs one libc timezone-sensitive operation under the eval context timezone. +pub(in crate::interpreter) fn eval_with_timezone( + timezone: &str, + operation: impl FnOnce() -> Result, +) -> Result { + let _guard = EVAL_TZ_MUTEX + .lock() + .map_err(|_| EvalStatus::RuntimeFatal)?; + let previous = std::env::var_os("TZ") + .map(|value| CString::new(value.as_bytes()).map_err(|_| EvalStatus::RuntimeFatal)) + .transpose()?; + eval_apply_process_timezone(timezone)?; + let result = operation(); + eval_restore_process_timezone(previous.as_ref())?; + result +} + +/// Applies one timezone identifier to libc's process-global timezone state. +fn eval_apply_process_timezone(timezone: &str) -> Result<(), EvalStatus> { + let key = CString::new("TZ").map_err(|_| EvalStatus::RuntimeFatal)?; + let value = CString::new(timezone).map_err(|_| EvalStatus::RuntimeFatal)?; + let status = unsafe { libc::setenv(key.as_ptr(), value.as_ptr(), 1) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + unsafe { tzset() }; + Ok(()) +} + +/// Restores the process timezone that was active before an eval-local conversion. +fn eval_restore_process_timezone(previous: Option<&CString>) -> Result<(), EvalStatus> { + let key = CString::new("TZ").map_err(|_| EvalStatus::RuntimeFatal)?; + let status = if let Some(value) = previous { + unsafe { libc::setenv(key.as_ptr(), value.as_ptr(), 1) } + } else { + unsafe { libc::unsetenv(key.as_ptr()) } + }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + unsafe { tzset() }; + Ok(()) +} + /// Applies PHP `date()` tokens to one local broken-down timestamp. pub(in crate::interpreter) fn eval_format_date_bytes( format: &[u8], diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs index 9dec237852..47c39a8696 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs @@ -9,9 +9,11 @@ use super::super::super::*; use super::super::*; +use super::*; /// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. -pub(in crate::interpreter) fn eval_builtin_mktime( +pub(in crate::interpreter) fn eval_builtin_mktime_like( + name: &str, args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, @@ -26,30 +28,54 @@ pub(in crate::interpreter) fn eval_builtin_mktime( let month = eval_expr(month, context, scope, values)?; let day = eval_expr(day, context, scope, values)?; let year = eval_expr(year, context, scope, values)?; - eval_mktime_result(hour, minute, second, month, day, year, values) + eval_mktime_result(name, hour, minute, second, month, day, year, context, values) } /// Converts PHP date components to a local Unix timestamp through libc `mktime`. pub(in crate::interpreter) fn eval_mktime_result( + name: &str, hour: RuntimeCellHandle, minute: RuntimeCellHandle, second: RuntimeCellHandle, month: RuntimeCellHandle, day: RuntimeCellHandle, year: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let timestamp = eval_mktime_timestamp( + let args = ( eval_int_cell_as_c_int(hour, values)?, eval_int_cell_as_c_int(minute, values)?, eval_int_cell_as_c_int(second, values)?, eval_int_cell_as_c_int(month, values)?, eval_int_cell_as_c_int(day, values)?, eval_int_cell_as_c_int(year, values)?, - )?; + ); + let timestamp = match name { + "mktime" => eval_context_mktime_timestamp(args, context)?, + "gmmktime" => eval_gmmktime_timestamp(args)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; values.int(timestamp) } +/// Converts local date components into an eval-timezone Unix timestamp. +pub(in crate::interpreter) fn eval_context_mktime_timestamp( + args: ( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + ), + context: &ElephcEvalContext, +) -> Result { + eval_with_timezone(context.default_timezone(), || { + eval_mktime_timestamp(args.0, args.1, args.2, args.3, args.4, args.5) + }) +} + /// Converts local date components into a Unix timestamp through libc `mktime`. pub(in crate::interpreter) fn eval_mktime_timestamp( hour: libc::c_int, @@ -71,6 +97,29 @@ pub(in crate::interpreter) fn eval_mktime_timestamp( i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) } +/// Converts UTC date components into a Unix timestamp through libc `timegm`. +pub(in crate::interpreter) fn eval_gmmktime_timestamp( + args: ( + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + libc::c_int, + ), +) -> Result { + let mut tm = unsafe { MaybeUninit::::zeroed().assume_init() }; + tm.tm_hour = args.0; + tm.tm_min = args.1; + tm.tm_sec = args.2; + tm.tm_mon = args.3 - 1; + tm.tm_mday = args.4; + tm.tm_year = args.5 - 1900; + tm.tm_isdst = 0; + let timestamp = unsafe { libc::timegm(&mut tm) }; + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} + /// Casts one eval cell to a PHP int and checks it fits a libc `c_int`. pub(in crate::interpreter) fn eval_int_cell_as_c_int( value: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs index 137746f809..cf38c37b5f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs @@ -9,6 +9,7 @@ //! - Time conversion helpers stay scoped to the eval interpreter and use libc for //! local calendar conversions where PHP behavior depends on host locale/timezone. +mod calendar; mod clock; mod date; mod mktime; @@ -16,6 +17,7 @@ mod sleep; mod strtotime; mod system; +pub(in crate::interpreter) use calendar::*; pub(in crate::interpreter) use clock::*; pub(in crate::interpreter) use date::*; pub(in crate::interpreter) use mktime::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs index 7334ad6ceb..9a35b8fbc8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs @@ -22,21 +22,25 @@ pub(in crate::interpreter) fn eval_builtin_strtotime( return Err(EvalStatus::RuntimeFatal); }; let datetime = eval_expr(datetime, context, scope, values)?; - eval_strtotime_result(datetime, values) + eval_strtotime_result(datetime, context, values) } /// Parses one eval `strtotime()` input and boxes the resulting timestamp. pub(in crate::interpreter) fn eval_strtotime_result( datetime: RuntimeCellHandle, + context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let bytes = values.string_bytes(datetime)?; - let timestamp = eval_strtotime_bytes(&bytes)?; + let timestamp = eval_strtotime_bytes(&bytes, context)?; values.int(timestamp) } /// Parses eval's supported `strtotime()` strings into local Unix timestamps. -pub(in crate::interpreter) fn eval_strtotime_bytes(bytes: &[u8]) -> Result { +pub(in crate::interpreter) fn eval_strtotime_bytes( + bytes: &[u8], + context: &ElephcEvalContext, +) -> Result { let bytes = eval_trim_ascii_whitespace(bytes); if bytes.eq_ignore_ascii_case(b"now") { return eval_current_unix_timestamp(); @@ -44,7 +48,7 @@ pub(in crate::interpreter) fn eval_strtotime_bytes(bytes: &[u8]) -> Result Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_date_default_timezone_get_result(context, values) +} + +/// Returns the eval-local default timezone identifier. +pub(in crate::interpreter) fn eval_date_default_timezone_get_result( + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(context.default_timezone()) +} + +/// Evaluates PHP `date_default_timezone_set($timezoneId)`. +pub(in crate::interpreter) fn eval_builtin_date_default_timezone_set( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [timezone] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let timezone = eval_expr(timezone, context, scope, values)?; + eval_date_default_timezone_set_result(timezone, context, values) +} + +/// Stores one eval-local default timezone identifier and reports success. +pub(in crate::interpreter) fn eval_date_default_timezone_set_result( + timezone: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timezone = values.string_bytes(timezone)?; + let timezone = String::from_utf8_lossy(&timezone).into_owned(); + context.set_default_timezone(timezone); + values.bool_value(true) +} + +/// Evaluates PHP `http_response_code($response_code = 0)`. +pub(in crate::interpreter) fn eval_builtin_http_response_code( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_http_response_code_result(None, context, values), + [response_code] => { + let response_code = eval_expr(response_code, context, scope, values)?; + eval_http_response_code_result(Some(response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads or updates the eval-local HTTP response code. +pub(in crate::interpreter) fn eval_http_response_code_result( + response_code: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = match response_code { + Some(response_code) => context.replace_http_response_code(eval_int_value(response_code, values)?), + None => context.http_response_code(), + }; + values.int(result) +} + +/// Evaluates PHP `header($header, $replace = true, $response_code = 0)`. +pub(in crate::interpreter) fn eval_builtin_header( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [line] => { + let line = eval_expr(line, context, scope, values)?; + eval_header_result(line, None, None, context, values) + } + [line, replace] => { + let line = eval_expr(line, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + eval_header_result(line, Some(replace), None, context, values) + } + [line, replace, response_code] => { + let line = eval_expr(line, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let response_code = eval_expr(response_code, context, scope, values)?; + eval_header_result(line, Some(replace), Some(response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies eval-local `header()` side effects that are observable without a web bridge. +pub(in crate::interpreter) fn eval_header_result( + line: RuntimeCellHandle, + replace: Option, + response_code: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.string_bytes(line)?; + if let Some(replace) = replace { + let _ = values.truthy(replace)?; + } + if let Some(response_code) = response_code { + let response_code = eval_int_value(response_code, values)?; + let _ = context.replace_http_response_code(response_code); + } + values.null() +} + /// Evaluates PHP `phpversion()` with no arguments. pub(in crate::interpreter) fn eval_builtin_phpversion( args: &[EvalExpr], diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index c76234d34b..92c7bc0144 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1062,7 +1062,14 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { eval_builtin_ctype(name, args, context, scope, values) } - "date" => eval_builtin_date(args, context, scope, values), + "checkdate" => eval_builtin_checkdate(args, context, scope, values), + "date" | "gmdate" => eval_builtin_date_like(name, args, context, scope, values), + "date_default_timezone_get" => { + eval_builtin_date_default_timezone_get(args, context, values) + } + "date_default_timezone_set" => { + eval_builtin_date_default_timezone_set(args, context, scope, values) + } "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "dirname" => eval_builtin_dirname(args, context, scope, values), @@ -1113,6 +1120,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(name, args, context, scope, values) } + "getdate" => eval_builtin_getdate(args, context, scope, values), "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), "gethostname" => eval_builtin_gethostname(args, values), @@ -1140,6 +1148,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { eval_builtin_hash_one_shot(name, args, context, scope, values) } + "header" => eval_builtin_header(args, context, scope, values), "chown" | "chgrp" | "lchown" | "lchgrp" => { eval_builtin_chown_like(name, args, context, scope, values) } @@ -1162,9 +1171,12 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), + "hrtime" => eval_builtin_hrtime(args, context, scope, values), + "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" => { + | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_scalar" + | "is_string" => { eval_builtin_type_predicate(name, args, context, scope, values) } "ip2long" => eval_builtin_ip2long(args, context, scope, values), @@ -1177,8 +1189,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "log" => eval_builtin_log(args, context, scope, values), "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), + "localtime" => eval_builtin_localtime(args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), - "mktime" => eval_builtin_mktime(args, context, scope, values), + "mktime" | "gmmktime" => eval_builtin_mktime_like(name, args, context, scope, values), "nl2br" => eval_builtin_nl2br(args, context, scope, values), "number_format" => eval_builtin_number_format(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs index 957aa39569..64d7ecf487 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_scalars.rs @@ -71,6 +71,33 @@ return function_exists("is_infinite");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval `is_scalar()` matches PHP scalar-tag membership directly and by callable. +#[test] +fn execute_program_dispatches_is_scalar_builtin() { + let program = parse_fragment( + br#"echo is_scalar(1) ? "i" : "bad"; +echo is_scalar(1.5) ? "f" : "bad"; +echo is_scalar("x") ? "s" : "bad"; +echo is_scalar(false) ? "b" : "bad"; +echo is_scalar(null) ? "bad" : "n"; +echo is_scalar([1]) ? "bad" : "a"; +echo is_scalar($object) ? "bad" : "o"; +echo ":"; +echo call_user_func("is_scalar", "x") ? "call" : "bad"; +echo ":"; +return call_user_func_array("is_scalar", ["value" => [1]]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("object".to_string(), object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ifsbnao:call:"); + assert_eq!(values.get(result), FakeValue::Bool(false)); +} /// Verifies eval `is_resource()` recognizes resource-tagged runtime cells from scope. #[test] fn execute_program_dispatches_is_resource_true() { diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs index 9242320ac3..093d19e15f 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs @@ -67,6 +67,41 @@ return function_exists("mktime");"#, ); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval UTC calendar builtins and timezone probes are callable-visible. +#[test] +fn execute_program_dispatches_extended_calendar_builtins() { + let program = parse_fragment( + br#"echo date_default_timezone_get(); echo ":"; +echo date_default_timezone_set("UTC") ? "set" : "bad"; echo ":"; +$ts = gmmktime(0, 0, 0, 1, 2, 2024); +echo gmdate("Y-m-d H:i:s", $ts); echo ":"; +echo call_user_func("gmdate", "Y", $ts); echo ":"; +echo checkdate(2, 29, 2024) ? "leap" : "bad"; echo ":"; +echo checkdate(2, 29, 2023) ? "bad" : "common"; echo ":"; +$g = getdate(0); +echo $g["year"] . "-" . $g["mon"] . "-" . $g["mday"] . " " . $g["hours"] . ":" . $g["minutes"] . ":" . $g["seconds"]; +echo ":" . $g["weekday"] . ":" . $g["month"] . ":" . $g[0]; +$l = localtime(0, true); +echo ":" . ($l["tm_year"] + 1900) . "-" . $l["tm_mon"] . "-" . $l["tm_mday"] . " " . $l["tm_hour"]; +$n = localtime(0); +$callLocal = call_user_func_array("localtime", ["timestamp" => 0, "associative" => true]); +echo ":" . ($n[5] + 1900) . "-" . $n[4] . ":" . $callLocal["tm_sec"]; +echo ":" . function_exists("gmdate") . function_exists("gmmktime") . function_exists("checkdate"); +echo function_exists("getdate") . function_exists("localtime") . function_exists("date_default_timezone_get"); +return function_exists("date_default_timezone_set");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "UTC:set:2024-01-02 00:00:00:2024:leap:common:1970-1-1 0:0:0:Thursday:January:0:1970-0-1 0:1970-0:0:111111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval `strtotime()` parses supported ISO date strings and rejects others. #[test] fn execute_program_dispatches_strtotime_builtin() { @@ -116,6 +151,38 @@ return function_exists("microtime");"#, assert_eq!(values.output, "now:named:call:array:"); assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies eval `hrtime()`, `http_response_code()`, and `header()` dispatch paths. +#[test] +fn execute_program_dispatches_hrtime_and_http_header_builtins() { + let program = parse_fragment( + br#"$parts = hrtime(); +echo count($parts) === 2 ? "parts" : "bad"; echo ":"; +echo is_int($parts[0]) && is_int($parts[1]) ? "ints" : "bad"; echo ":"; +echo hrtime(true) > 0 ? "number" : "bad"; echo ":"; +echo http_response_code(); echo ":"; +echo http_response_code(404); echo ":"; +echo http_response_code(); echo ":"; +header("X-Test: 1", true, 201); +echo http_response_code(); echo ":"; +echo call_user_func("http_response_code", 202); echo ":"; +echo http_response_code(); echo ":"; +echo call_user_func_array("header", ["header" => "X-Test: 2", "replace" => false, "response_code" => 204]); +echo http_response_code(); echo ":"; +echo function_exists("hrtime") . function_exists("http_response_code") . function_exists("header"); +return is_callable("header");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "parts:ints:number:200:200:404:201:201:202:204:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} /// Verifies eval realpath-cache stubs match elephc's empty-cache runtime view. #[test] fn execute_program_dispatches_realpath_cache_builtins() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 01793a4a4d..10a9737243 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -775,7 +775,7 @@ where listed below unless a note says otherwise. | Area | Builtins | |---|---| -| System, time, and environment | `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()` | +| System, time, and environment | `time()`, `microtime()`, `hrtime()`, `date()`, `gmdate()`, `mktime()`, `gmmktime()`, `checkdate()`, `getdate()`, `localtime()`, `strtotime()`, `date_default_timezone_get()`, `date_default_timezone_set()`, `http_response_code()`, `header()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()` | | Filesystem and paths | `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()` | | Stream introspection | `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()` | | Network and protocol databases | `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()` | @@ -786,7 +786,7 @@ where listed below unless a note says otherwise. | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | | Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | -| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_called_class()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_class_vars()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_resource()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_called_class()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_class_vars()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_scalar()`, `is_resource()` | | Debug output | `print_r()`, `var_dump()` | | Constants | `define()`, `defined()` | From 8406735e59e4bee25845954e18e79538d904f550 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 17:37:23 +0200 Subject: [PATCH 0936/1208] Align eval builtin parity --- .../interpreter/builtins/registry/binding.rs | 2 +- .../builtins/registry/dispatch/time.rs | 13 ++-- .../builtins/registry/signature.rs | 3 +- .../interpreter/builtins/time/strtotime.rs | 34 +++++++--- .../tests/builtins_system_network.rs | 7 +- .../src/interpreter/tests/dynamic_calls.rs | 65 +++++++++++++++++++ .../src/interpreter/tests/expressions.rs | 29 +++++++++ .../src/interpreter/tests/method_arguments.rs | 30 +++++++++ src/codegen/eval_constructor_helpers.rs | 2 +- src/codegen/eval_method_helpers.rs | 2 +- tests/builtin_parity_tests.rs | 46 ++++++++++++- tests/codegen/eval.rs | 46 +++++++++++++ tests/codegen/eval_builtin_parity.rs | 17 +++++ 13 files changed, 275 insertions(+), 21 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 609022d30b..351d683977 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -370,7 +370,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), - "strtotime" => Some(&["datetime"]), + "strtotime" => Some(&["datetime", "baseTimestamp"]), "strstr" => Some(&["haystack", "needle", "before_needle"]), "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject", "count"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs index bb77cfb230..f31148efbc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs @@ -97,12 +97,13 @@ pub(in crate::interpreter) fn eval_time_builtin_with_values( } eval_time_result(values)? } - "strtotime" => { - let [datetime] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_strtotime_result(*datetime, context, values)? - } + "strtotime" => match evaluated_args { + [datetime] => eval_strtotime_result(*datetime, None, context, values)?, + [datetime, base_timestamp] => { + eval_strtotime_result(*datetime, Some(*base_timestamp), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "usleep" => { let [microseconds] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 4294e51fb1..f158fc3aaf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -110,7 +110,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "call_user_func" => variadic(params, &[]), - "log" | "round" | "date" | "gmdate" | "nl2br" => optional(params, 1), + "log" | "round" | "date" | "gmdate" | "nl2br" | "strtotime" => optional(params, 1), "min" | "max" => variadic(params, &[]), "json_encode" | "json_decode" | "json_validate" => optional(params, 1), @@ -217,6 +217,7 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("log", 1) => Float(std::f64::consts::E), ("round", 1) => Int(0), ("date" | "gmdate", 1) => Null, + ("strtotime", 1) => Null, ("nl2br", 1) => Bool(true), ("json_encode", 1) => Int(0), ("json_encode", 2) => Int(512), diff --git a/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs index 9a35b8fbc8..db87cdf172 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs @@ -11,39 +11,57 @@ use super::super::super::*; use super::*; -/// Evaluates PHP `strtotime(datetime)` for eval's supported date-string subset. +/// Evaluates PHP `strtotime(datetime, baseTimestamp = null)` for eval's supported subset. pub(in crate::interpreter) fn eval_builtin_strtotime( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [datetime] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let datetime = eval_expr(datetime, context, scope, values)?; - eval_strtotime_result(datetime, context, values) + match args { + [datetime] => { + let datetime = eval_expr(datetime, context, scope, values)?; + eval_strtotime_result(datetime, None, context, values) + } + [datetime, base_timestamp] => { + let datetime = eval_expr(datetime, context, scope, values)?; + let base_timestamp = eval_expr(base_timestamp, context, scope, values)?; + eval_strtotime_result(datetime, Some(base_timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } } /// Parses one eval `strtotime()` input and boxes the resulting timestamp. pub(in crate::interpreter) fn eval_strtotime_result( datetime: RuntimeCellHandle, + base_timestamp: Option, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let bytes = values.string_bytes(datetime)?; - let timestamp = eval_strtotime_bytes(&bytes, context)?; + let base_timestamp = match base_timestamp { + Some(base_timestamp) if !values.is_null(base_timestamp)? => { + Some(eval_int_value(base_timestamp, values)?) + } + _ => None, + }; + let timestamp = eval_strtotime_bytes(&bytes, base_timestamp, context)?; values.int(timestamp) } /// Parses eval's supported `strtotime()` strings into local Unix timestamps. pub(in crate::interpreter) fn eval_strtotime_bytes( bytes: &[u8], + base_timestamp: Option, context: &ElephcEvalContext, ) -> Result { let bytes = eval_trim_ascii_whitespace(bytes); if bytes.eq_ignore_ascii_case(b"now") { - return eval_current_unix_timestamp(); + return match base_timestamp { + Some(timestamp) => Ok(timestamp), + None => eval_current_unix_timestamp(), + }; } let Some((year, month, day, hour, minute, second)) = eval_parse_iso_datetime(bytes) else { return Ok(-1); diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs index 093d19e15f..f4fd060253 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_system_network.rs @@ -116,7 +116,10 @@ echo ":" . (strtotime("2024/06/15") === -1 ? "bad" : "wrong"); $call = call_user_func("strtotime", "2024-01-02 03:04:05"); echo ":" . date("Y-m-d H:i:s", $call); $spread = call_user_func_array("strtotime", ["datetime" => "2024-01-02"]); -echo ":" . date("Y-m-d", $spread) . ":"; +echo ":" . date("Y-m-d", $spread); +echo ":" . strtotime("now", 1700000000); +$base = call_user_func_array("strtotime", ["datetime" => "now", "baseTimestamp" => 1700000001]); +echo ":" . $base . ":"; return function_exists("strtotime");"#, ) .expect("parse eval fragment"); @@ -127,7 +130,7 @@ return function_exists("strtotime");"#, assert_eq!( values.output, - "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:" + "2024-06-15 00:00:00:2024-06-15 12:30:45:2024-06-15 12:30:00:bad:2024-01-02 03:04:05:2024-01-02:1700000000:1700000001:" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 46e5957a5f..9ace6d45fd 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -620,6 +620,71 @@ fn execute_program_static_runtime_method_hook_rejects_by_ref_temporary_arg() { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies callable-array AOT method dispatch preserves by-reference writeback. +#[test] +fn execute_program_callable_array_runtime_method_writes_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$cb = [$box, "add2_x"]; +$value = "3"; +echo $cb($value, 2); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("callable-array runtime method should preserve by-ref target"); + + assert_eq!(values.output, "15"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies string and first-class AOT static callables preserve by-reference writeback. +#[test] +fn execute_program_static_runtime_callables_write_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$string = "KnownClass::sum"; +$left = "3"; +echo $string($left, 2); echo ":"; +echo $left + 1; echo ":"; +$first = KnownClass::sum(...); +$next = "4"; +echo $first($next, 5); echo ":"; +return $next;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("runtime static callables should preserve by-ref target"); + + assert_eq!(values.output, "5:4:9:"); + assert_eq!(values.get(result), FakeValue::Int(4)); +} + /// Verifies runtime AOT static method fallback rejects named arguments without metadata. #[test] fn execute_program_static_runtime_method_hook_rejects_unregistered_named_args() { diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index 41d67646b6..1b0d9b7eae 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -279,6 +279,35 @@ fn execute_program_rejects_runtime_constructor_by_ref_temporary_arg() { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies runtime/AOT constructor fallback writes coerced by-reference args back. +#[test] +fn execute_program_writes_back_runtime_constructor_by_ref_type_coercion() { + let program = parse_fragment( + br#"$value = "9"; +$box = new KnownClass($value); +echo $box->read_x(); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered constructor by-ref coercion should bind"); + + assert_eq!(values.output, "9"); + assert_eq!(values.get(result), FakeValue::Int(9)); +} + /// Verifies eval-declared classes create objects with properties and methods. #[test] fn execute_program_constructs_eval_declared_class_with_method() { diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs index d43d6a400b..75f969f1a2 100644 --- a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs @@ -679,6 +679,36 @@ return $box->add2_x(1, 2);"#, assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies runtime/AOT method fallback writes coerced by-reference args back. +#[test] +fn execute_program_writes_back_runtime_method_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$value = "3"; +echo $box->add2_x($value, 2); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered runtime method by-ref coercion should bind"); + + assert_eq!(values.output, "15"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + /// Verifies runtime/AOT method fallback rejects named arguments without metadata. #[test] fn execute_program_rejects_unregistered_named_args_for_runtime_method_fallback() { diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 69d957bf09..e061879574 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -887,7 +887,7 @@ fn emit_aarch64_prepare_constructor_args( ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); - emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first constructor argument + emitter.instruction("ldr x0, [x29, #-32]"); // load the unboxed receiver as the first constructor argument abi::emit_push_result_value(emitter, &receiver_ty); let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 2eb92c205d..9606b1f22a 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -1396,7 +1396,7 @@ fn emit_aarch64_prepare_method_args( ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); - emitter.instruction("ldr x0, [sp, #16]"); // load the unboxed receiver as the first method argument + emitter.instruction("ldr x0, [x29, #-32]"); // load the unboxed receiver as the first method argument abi::emit_push_result_value(emitter, &receiver_ty); let mut arg_temp_bytes = eval_arg_temp_slot_size(&receiver_ty); for (index, param_ty) in slot.params.iter().enumerate() { diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 27b642f6ed..f222e65dfa 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -42,7 +42,15 @@ const EVAL_ONLY_REFLECTION_BUILTINS: &[&str] = &[ ]; /// Eval supports these PHP optional parameters before the static backend does. -const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["array_reverse", "array_splice", "nl2br"]; +const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[ + "array_reverse", + "array_splice", + "nl2br", + "print_r", +]; + +/// Eval supports variadic debug output before the static backend does. +const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; /// Verifies every non-raw-memory static builtin is visible through eval's function lookup. #[test] @@ -91,6 +99,14 @@ fn shared_builtin_signature_shape_matches_static_signatures() { assert_eval_signature_extends_static_signature(name, &static_meta, &eval_meta); continue; } + if EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS.contains(name) { + assert_eval_variadic_signature_extends_static_signature( + name, + &static_meta, + &eval_meta, + ); + continue; + } if static_meta.params != eval_meta.params || static_meta.required_param_count != eval_meta.required_param_count @@ -144,6 +160,34 @@ fn assert_eval_signature_extends_static_signature( ); } +/// Verifies a documented eval variadic extension keeps the static prefix contract. +fn assert_eval_variadic_signature_extends_static_signature( + name: &str, + static_meta: &elephc::builtin_metadata::BuiltinSignatureMetadata, + eval_meta: &elephc_magician::builtin_metadata::BuiltinSignatureMetadata, +) { + assert!( + eval_meta.params.starts_with(&static_meta.params), + "{name} eval variadic extension must preserve static parameter prefix: static={static_meta:#?} eval={eval_meta:#?}" + ); + assert_eq!( + static_meta.required_param_count, eval_meta.required_param_count, + "{name} eval variadic extension must preserve required parameter count" + ); + assert!( + static_meta.variadic.is_none() && eval_meta.variadic.is_some(), + "{name} eval variadic extension must add, not remove, variadic behavior" + ); + assert_eq!( + static_meta.by_ref_params, eval_meta.by_ref_params, + "{name} eval variadic extension must preserve by-reference parameters" + ); + assert!( + eval_meta.default_param_count >= static_meta.default_param_count, + "{name} eval variadic extension must not remove defaults" + ); +} + /// Documents the current eval-only reflection builtins so the drift is explicit. #[test] fn eval_only_reflection_builtins_remain_visible_until_static_catalog_catches_up() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c7ca84fa32..dddb8488af 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7086,6 +7086,52 @@ echo (new EvalAotCallableArrayNamedBox())->run(); assert_eq!(out.stdout, "AB:CD:EF"); } +/// Verifies eval AOT callables preserve typed by-reference argument writeback. +#[test] +fn test_eval_fragment_aot_callables_write_back_typed_by_ref_args() { + let out = compile_and_run_capture( + r#"offset = $offset; + } + + public function bump(int &$value): int { + $value = $value + $this->offset; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallableRefBox(5); +$method = [$box, "bump"]; +$value = "7"; +echo $method($value) . ":" . gettype($value) . ":" . $value . "|"; +$string = "EvalAotCallableRefBox::add"; +$staticValue = "3"; +echo $string($staticValue, 4) . ":" . gettype($staticValue) . ":" . $staticValue . "|"; +$first = EvalAotCallableRefBox::add(...); +$next = "2"; +return $first($next, 6) . ":" . gettype($next) . ":" . $next;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "12:integer:12|7:integer:7|8:integer:8" + ); +} + /// Verifies eval can pass runtime PHP callbacks to generated/AOT callable-typed methods. #[test] fn test_eval_fragment_passes_callable_args_to_aot_methods() { diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 3db6ce44c5..cdd8f6c2b0 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -40,6 +40,23 @@ echo function_exists("eval_declared_lookup") ? eval_declared_lookup() : "d"; assert_eq!(out, "BSCD"); } +/// Verifies compiler-internal raw time helpers stay hidden from PHP function lookup. +#[test] +fn test_internal_raw_time_helpers_are_not_php_visible_before_or_after_eval() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 18:20:19 +0200 Subject: [PATCH 0937/1208] Test eval callable bridge parity --- Dockerfile.test-linux-arm64 | 2 +- Dockerfile.test-linux-x86_64 | 2 +- crates/elephc-magician/build.rs | 5 ++- tests/codegen/eval.rs | 59 +++++++++++++++++++++++++++++++-- 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/Dockerfile.test-linux-arm64 b/Dockerfile.test-linux-arm64 index 868bbe1b7b..3f6ee1c01d 100644 --- a/Dockerfile.test-linux-arm64 +++ b/Dockerfile.test-linux-arm64 @@ -1,6 +1,6 @@ FROM rust:1.95-alpine -RUN apk add --no-cache gcc musl-dev openssl-dev zlib-dev bzip2-dev pcre2-dev gdb strace binutils file tzdata +RUN apk add --no-cache gcc musl-dev openssl-dev zlib-dev bzip2-dev pcre2-dev pcre2-static gdb strace binutils file tzdata ENV PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin diff --git a/Dockerfile.test-linux-x86_64 b/Dockerfile.test-linux-x86_64 index 868bbe1b7b..3f6ee1c01d 100644 --- a/Dockerfile.test-linux-x86_64 +++ b/Dockerfile.test-linux-x86_64 @@ -1,6 +1,6 @@ FROM rust:1.95-alpine -RUN apk add --no-cache gcc musl-dev openssl-dev zlib-dev bzip2-dev pcre2-dev gdb strace binutils file tzdata +RUN apk add --no-cache gcc musl-dev openssl-dev zlib-dev bzip2-dev pcre2-dev pcre2-static gdb strace binutils file tzdata ENV PATH=/usr/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin diff --git a/crates/elephc-magician/build.rs b/crates/elephc-magician/build.rs index d2e5073e69..f50825d9fe 100644 --- a/crates/elephc-magician/build.rs +++ b/crates/elephc-magician/build.rs @@ -9,7 +9,7 @@ //! binaries that link the rlib need the same native libraries as generated //! elephc binaries. -use std::path::Path; +use std::{env, path::Path}; /// Emits native PCRE2 link directives for cargo-built test binaries and rlibs. fn main() { @@ -18,6 +18,9 @@ fn main() { } println!("cargo:rustc-link-lib=pcre2-posix"); println!("cargo:rustc-link-lib=pcre2-8"); + if env::var("TARGET").as_deref() == Ok("aarch64-unknown-linux-musl") { + println!("cargo:rustc-link-lib=gcc"); + } } /// Returns common PCRE2 library directories for local macOS/Homebrew builds. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index dddb8488af..7fdf651936 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7107,6 +7107,17 @@ class EvalAotCallableRefBox { $value = $value + $delta; return $value; } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->offset + $delta; + return $value; + } +} + +class EvalAotCallableRefDriver { + public static function run(callable $callback, int &$value, int $delta) { + return $callback($value, $delta); + } } echo eval('$box = new EvalAotCallableRefBox(5); @@ -7118,7 +7129,20 @@ $staticValue = "3"; echo $string($staticValue, 4) . ":" . gettype($staticValue) . ":" . $staticValue . "|"; $first = EvalAotCallableRefBox::add(...); $next = "2"; -return $first($next, 6) . ":" . gettype($next) . ":" . $next;'); +echo $first($next, 6) . ":" . gettype($next) . ":" . $next . "|"; +$instanceFirst = $box->bump(...); +$instanceValue = "4"; +echo $instanceFirst($instanceValue) . ":" . gettype($instanceValue) . ":" . $instanceValue . "|"; +$invokable = new EvalAotCallableRefBox(10); +$invokableValue = "1"; +echo $invokable($invokableValue, 2) . ":" . gettype($invokableValue) . ":" . $invokableValue . "|"; +$invokableFirst = $invokable(...); +$firstValue = "2"; +echo $invokableFirst($firstValue, 3) . ":" . gettype($firstValue) . ":" . $firstValue . "|"; +$bridge = new EvalAotCallableRefBox(20); +$bridgeFirst = $bridge(...); +$bridgeValue = "5"; +return EvalAotCallableRefDriver::run($bridgeFirst, $bridgeValue, 1) . ":" . gettype($bridgeValue) . ":" . $bridgeValue;'); "#, ); assert!( @@ -7128,7 +7152,7 @@ return $first($next, 6) . ":" . gettype($next) . ":" . $next;'); ); assert_eq!( out.stdout, - "12:integer:12|7:integer:7|8:integer:8" + "12:integer:12|7:integer:7|8:integer:8|9:integer:9|13:integer:13|15:integer:15|26:integer:26" ); } @@ -7191,6 +7215,32 @@ return $box->value . ":" . echo ":"; echo eval('$invokable = new EvalAotCallableArgTarget(); $box = new EvalAotCallableArgBox($invokable); +return $box->value . ":" . + $box->apply($invokable) . ":" . + EvalAotCallableArgBox::applyStatic($invokable);'); +echo ":"; +echo eval('$function = eval_aot_callable_arg_suffix(...); +$box = new EvalAotCallableArgBox($function); +return $box->value . ":" . + $box->apply($function) . ":" . + EvalAotCallableArgBox::applyStatic($function);'); +echo ":"; +echo eval('$static = EvalAotCallableArgTarget::suffix(...); +$box = new EvalAotCallableArgBox($static); +return $box->value . ":" . + $box->apply($static) . ":" . + EvalAotCallableArgBox::applyStatic($static);'); +echo ":"; +echo eval('$target = new EvalAotCallableArgTarget(); +$instance = $target->instanceSuffix(...); +$box = new EvalAotCallableArgBox($instance); +return $box->value . ":" . + $box->apply($instance) . ":" . + EvalAotCallableArgBox::applyStatic($instance);'); +echo ":"; +echo eval('$target = new EvalAotCallableArgTarget(); +$invokable = $target(...); +$box = new EvalAotCallableArgBox($invokable); return $box->value . ":" . $box->apply($invokable) . ":" . EvalAotCallableArgBox::applyStatic($invokable);'); @@ -7201,7 +7251,10 @@ return $box->value . ":" . "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "C!:M!:S!:C?:M?:S?:C~:M~:S~:C#:M#:S#"); + assert_eq!( + out.stdout, + "C!:M!:S!:C?:M?:S?:C~:M~:S~:C#:M#:S#:C!:M!:S!:C?:M?:S?:C~:M~:S~:C#:M#:S#" + ); } /// Verifies eval static calls and static callables dispatch public AOT static methods. From 34481d77350de8e1672a4aee90226db1f1b4baad Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 19:01:29 +0200 Subject: [PATCH 0938/1208] Add eval raw memory builtins --- .../src/interpreter/builtins/mod.rs | 2 + .../src/interpreter/builtins/raw_memory.rs | 441 ++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 14 + .../builtins/registry/dispatch/mod.rs | 5 + .../interpreter/builtins/registry/names.rs | 18 + .../src/interpreter/expressions.rs | 6 + .../interpreter/tests/builtins_raw_memory.rs | 89 ++++ .../src/interpreter/tests/mod.rs | 1 + tests/builtin_parity_tests.rs | 36 +- tests/codegen/eval.rs | 31 ++ 10 files changed, 608 insertions(+), 35 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_raw_memory.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 2d729b739f..7f9cb9435b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -17,6 +17,7 @@ mod filesystem; mod formatting; mod network_env; mod process_control; +mod raw_memory; mod ref_targets; mod regex; mod registry; @@ -32,6 +33,7 @@ pub(super) use filesystem::*; pub(super) use formatting::*; pub(super) use network_env::*; pub(super) use process_control::*; +pub(super) use raw_memory::*; pub(super) use ref_targets::*; pub(super) use regex::*; pub(super) use registry::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs new file mode 100644 index 0000000000..7823feda7b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs @@ -0,0 +1,441 @@ +//! Purpose: +//! Implements eval-side raw pointer and buffer extension builtins. +//! These helpers expose the AOT-visible names while preserving the raw-address +//! representation as integer runtime cells inside eval. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_positional_expr_call()`. +//! - `crate::interpreter::builtins::registry::dispatch::eval_builtin_with_values()`. +//! +//! Key details: +//! - `buffer_new()` returns the same header pointer shape used by AOT buffers: +//! length word, stride word, then zeroed payload. +//! - `ptr($var)` still requires lvalue storage that the by-value eval call path +//! does not expose, so it fails instead of inventing an unsafe fake address. + +use std::mem; +use std::ptr; +use std::slice; + +use super::super::*; + +const BUFFER_HEADER_WORDS: usize = 2; +const BUFFER_DEFAULT_STRIDE: usize = 8; + +/// Evaluates raw-memory builtins whose arguments are still source expressions. +pub(in crate::interpreter) fn eval_builtin_raw_memory( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "ptr" { + let [_value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + return Err(EvalStatus::UnsupportedConstruct); + } + if name == "buffer_free" { + if let [EvalExpr::LoadVar(variable)] = args { + return eval_buffer_free_direct_variable(variable, context, scope, values); + } + } + let evaluated_args = args + .iter() + .map(|arg| eval_expr(arg, context, scope, values)) + .collect::, _>>()?; + eval_raw_memory_builtin_result(name, &evaluated_args, context, values) +} + +/// Dispatches already evaluated raw-memory builtin arguments for dynamic calls. +pub(in crate::interpreter) fn eval_raw_memory_builtin_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match name { + "buffer_free" | "buffer_len" | "buffer_new" | "ptr" | "ptr_get" | "ptr_is_null" + | "ptr_null" | "ptr_offset" | "ptr_read8" | "ptr_read16" | "ptr_read32" + | "ptr_read_string" | "ptr_set" | "ptr_sizeof" | "ptr_write8" | "ptr_write16" + | "ptr_write32" | "ptr_write_string" => { + eval_raw_memory_builtin_result(name, evaluated_args, context, values).map(Some) + } + _ => Ok(None), + } +} + +/// Applies one raw-memory builtin to already evaluated runtime cells. +fn eval_raw_memory_builtin_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "buffer_new" => { + let [length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_new_result(*length, values) + } + "buffer_len" => { + let [buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_len_result(*buffer, values) + } + "buffer_free" => { + let [buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_free_result(*buffer, values) + } + "ptr" => { + let [_value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + Err(EvalStatus::UnsupportedConstruct) + } + "ptr_null" => { + let [] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.int(0) + } + "ptr_is_null" => { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_pointer_address(*pointer, values)?; + values.bool_value(address == 0) + } + "ptr_offset" => { + let [pointer, offset] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_offset_result(*pointer, *offset, values) + } + "ptr_get" => { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pointer_read_result(*pointer, PointerReadWidth::Word64, values) + } + "ptr_set" => { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pointer_write_result(*pointer, *value, PointerWriteWidth::Word64, values) + } + "ptr_read8" => { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pointer_read_result(*pointer, PointerReadWidth::Byte, values) + } + "ptr_read16" => { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pointer_read_result(*pointer, PointerReadWidth::Half, values) + } + "ptr_read32" => { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pointer_read_result(*pointer, PointerReadWidth::Word32, values) + } + "ptr_write8" => { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pointer_write_result(*pointer, *value, PointerWriteWidth::Byte, values) + } + "ptr_write16" => { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pointer_write_result(*pointer, *value, PointerWriteWidth::Half, values) + } + "ptr_write32" => { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_pointer_write_result(*pointer, *value, PointerWriteWidth::Word32, values) + } + "ptr_read_string" => { + let [pointer, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read_string_result(*pointer, *length, values) + } + "ptr_write_string" => { + let [pointer, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write_string_result(*pointer, *string, values) + } + "ptr_sizeof" => { + let [type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_sizeof_result(*type_name, context, values) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Allocates a zero-filled AOT-shaped buffer and returns its header address. +fn eval_buffer_new_result( + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let length = eval_int_value(length, values)?; + if length < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let header_bytes = BUFFER_HEADER_WORDS + .checked_mul(mem::size_of::()) + .ok_or(EvalStatus::RuntimeFatal)?; + let payload_bytes = length + .checked_mul(BUFFER_DEFAULT_STRIDE) + .ok_or(EvalStatus::RuntimeFatal)?; + let total_bytes = header_bytes + .checked_add(payload_bytes) + .ok_or(EvalStatus::RuntimeFatal)?; + let allocation = unsafe { libc::calloc(total_bytes.max(1), 1) }; + if allocation.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + unsafe { + let header = allocation.cast::(); + ptr::write(header, length); + ptr::write(header.add(1), BUFFER_DEFAULT_STRIDE); + } + eval_address_value(allocation as usize, values) +} + +/// Reads the logical element count from an AOT-shaped buffer header. +fn eval_buffer_len_result( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let header = eval_non_null_pointer(buffer, values)?.cast::(); + let length = unsafe { ptr::read(header) }; + values.int(i64::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Frees an AOT-shaped buffer header and returns PHP null. +fn eval_buffer_free_result( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_buffer_free_address(buffer, values)?; + values.null() +} + +/// Frees a local buffer variable and replaces the source variable with null. +fn eval_buffer_free_direct_variable( + variable: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(&EvalExpr::LoadVar(variable.to_string()), context, scope, values)?; + eval_buffer_free_address(value, values)?; + let null = values.null()?; + for replaced in scope.set_respecting_references( + variable.to_string(), + null, + ScopeCellOwnership::Owned, + ) { + values.release(replaced)?; + } + values.null() +} + +/// Frees the raw allocation addressed by an AOT-shaped buffer header pointer. +fn eval_buffer_free_address( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let address = eval_non_null_pointer(buffer, values)?; + unsafe { + libc::free(address.cast::()); + } + Ok(()) +} + +/// Computes a derived raw pointer address by adding a signed byte offset. +fn eval_ptr_offset_result( + pointer: RuntimeCellHandle, + offset: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_pointer_address(pointer, values)?; + let offset = eval_int_value(offset, values)?; + let address = if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + address.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)? + } else { + let offset = usize::try_from(offset.unsigned_abs()).map_err(|_| EvalStatus::RuntimeFatal)?; + address.checked_sub(offset).ok_or(EvalStatus::RuntimeFatal)? + }; + eval_address_value(address, values) +} + +/// Reads one unsigned or machine-word value from raw memory. +fn eval_pointer_read_result( + pointer: RuntimeCellHandle, + width: PointerReadWidth, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_non_null_pointer(pointer, values)?; + let value = unsafe { + match width { + PointerReadWidth::Byte => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Half => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Word32 => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Word64 => { + let word = ptr::read_unaligned(address.cast::()); + i64::from_ne_bytes(word.to_ne_bytes()) + } + } + }; + values.int(value) +} + +/// Writes one integer payload to raw memory and returns PHP null. +fn eval_pointer_write_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + width: PointerWriteWidth, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_non_null_pointer(pointer, values)?; + let value = eval_int_value(value, values)?; + unsafe { + match width { + PointerWriteWidth::Byte => ptr::write_unaligned(address.cast::(), value as u8), + PointerWriteWidth::Half => ptr::write_unaligned(address.cast::(), value as u16), + PointerWriteWidth::Word32 => ptr::write_unaligned(address.cast::(), value as u32), + PointerWriteWidth::Word64 => { + ptr::write_unaligned(address.cast::(), u64::from_ne_bytes(value.to_ne_bytes())) + } + } + } + values.null() +} + +/// Copies raw memory bytes into a PHP byte string. +fn eval_ptr_read_string_result( + pointer: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_non_null_pointer(pointer, values)?; + let length = eval_int_value(length, values)?; + if length < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = unsafe { slice::from_raw_parts(address.cast::(), length) }; + values.string_bytes_value(bytes) +} + +/// Copies PHP string bytes into raw memory and returns the byte count written. +fn eval_ptr_write_string_result( + pointer: RuntimeCellHandle, + string: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_non_null_pointer(pointer, values)?; + let bytes = values.string_bytes(string)?; + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), address.cast::(), bytes.len()); + } + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Computes the checked byte size for a low-level type name. +fn eval_ptr_sizeof_result( + type_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(type_name)?; + let type_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let size = eval_pointer_target_size(type_name.trim_start_matches('\\'), context) + .ok_or(EvalStatus::RuntimeFatal)?; + values.int(i64::try_from(size).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Returns the eval-side byte size for one low-level pointer target name. +fn eval_pointer_target_size(type_name: &str, context: &ElephcEvalContext) -> Option { + match type_name.to_ascii_lowercase().as_str() { + "int" | "integer" => Some(8), + "float" | "double" | "real" => Some(8), + "bool" | "boolean" => Some(8), + "string" => Some(16), + "ptr" | "pointer" => Some(8), + _ => context.class(type_name).map(eval_boxed_class_size), + } +} + +/// Returns the boxed object storage size used by AOT class metadata. +fn eval_boxed_class_size(class: &EvalClass) -> usize { + let instance_properties = class + .properties() + .iter() + .filter(|property| !property.is_static()) + .count(); + 8 + instance_properties * 16 +} + +/// Converts a runtime cell to a raw pointer address encoded as a PHP integer. +fn eval_pointer_address( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_int_value(value, values)?; + usize::try_from(address).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts a runtime cell to a non-null raw pointer. +fn eval_non_null_pointer( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<*mut u8, EvalStatus> { + let address = eval_pointer_address(value, values)?; + if address == 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(address as *mut u8) +} + +/// Boxes a raw pointer address as a PHP integer cell. +fn eval_address_value( + address: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(i64::try_from(address).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Widths supported by pointer read helpers. +enum PointerReadWidth { + Byte, + Half, + Word32, + Word64, +} + +/// Widths supported by pointer write helpers. +enum PointerWriteWidth { + Byte, + Half, + Word32, + Word64, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 351d683977..af59ee454d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -159,6 +159,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "is_double" | "is_float" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_null" | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" | "is_scalar" | "is_callable" | "strval" => Some(&["value"]), + "buffer_new" => Some(&["length"]), + "buffer_len" | "buffer_free" => Some(&["buffer"]), "is_finite" | "is_infinite" | "is_nan" => Some(&["num"]), "settype" => Some(&["var", "type"]), "get_called_class" => Some(&[]), @@ -304,6 +306,18 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "pclose" => Some(&["handle"]), "popen" => Some(&["command", "mode"]), "pow" => Some(&["num", "exponent"]), + "ptr" => Some(&["value"]), + "ptr_null" => Some(&[]), + "ptr_is_null" | "ptr_get" | "ptr_read8" | "ptr_read16" | "ptr_read32" => { + Some(&["pointer"]) + } + "ptr_offset" => Some(&["pointer", "offset"]), + "ptr_read_string" => Some(&["pointer", "length"]), + "ptr_set" | "ptr_write8" | "ptr_write16" | "ptr_write32" => { + Some(&["pointer", "value"]) + } + "ptr_write_string" => Some(&["pointer", "string"]), + "ptr_sizeof" => Some(&["type"]), "preg_match" => Some(&["pattern", "subject", "matches"]), "preg_match_all" => Some(&["pattern", "subject"]), "preg_replace" => Some(&["pattern", "replacement", "subject"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 14b90293de..e4435201b7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -57,6 +57,11 @@ pub(in crate::interpreter) fn eval_builtin_with_values( if let Some(result) = eval_regex_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } + if let Some(result) = + eval_raw_memory_builtin_with_values(name, evaluated_args, context, values)? + { + return Ok(Some(result)); + } if let Some(result) = eval_strings_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index a616664167..8463c26a5e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -54,6 +54,9 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "basename", "bin2hex", "boolval", + "buffer_free", + "buffer_len", + "buffer_new", "call_user_func", "call_user_func_array", "ceil", @@ -269,6 +272,21 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "pi", "popen", "pow", + "ptr", + "ptr_get", + "ptr_is_null", + "ptr_null", + "ptr_offset", + "ptr_read8", + "ptr_read16", + "ptr_read32", + "ptr_read_string", + "ptr_set", + "ptr_sizeof", + "ptr_write8", + "ptr_write16", + "ptr_write32", + "ptr_write_string", "preg_match", "preg_match_all", "preg_replace", diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 92c7bc0144..1b6b39a50a 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1208,6 +1208,12 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), "preg_replace_callback" => eval_builtin_preg_replace_callback(args, context, scope, values), "preg_split" => eval_builtin_preg_split(args, context, scope, values), + "buffer_free" | "buffer_len" | "buffer_new" | "ptr" | "ptr_get" | "ptr_is_null" + | "ptr_null" | "ptr_offset" | "ptr_read8" | "ptr_read16" | "ptr_read32" + | "ptr_read_string" | "ptr_set" | "ptr_sizeof" | "ptr_write8" | "ptr_write16" + | "ptr_write32" | "ptr_write_string" => { + eval_builtin_raw_memory(name, args, context, scope, values) + } "print_r" => eval_builtin_print_r(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_raw_memory.rs b/crates/elephc-magician/src/interpreter/tests/builtins_raw_memory.rs new file mode 100644 index 0000000000..ccd469c797 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_raw_memory.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Interpreter tests for eval raw pointer and buffer extension builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Buffer tests use the AOT-shaped header pointer and offset by 16 bytes for +//! payload pointer reads and writes. + +use super::super::*; +use super::support::*; + +/// Verifies pointer probes, type sizes, and callable metadata for raw-memory builtins. +#[test] +fn execute_program_dispatches_pointer_null_offset_and_sizeof_builtins() { + let program = parse_fragment( + br#"class EvalPtrSizeBox { + public $x; + public $y; + public static $z; +} +$p = ptr_null(); +echo ptr_is_null($p) ? "N" : "bad"; +echo ":" . ptr_is_null(ptr_offset($p, 0)); +echo ":" . ptr_sizeof("int"); +echo ":" . ptr_sizeof("string"); +echo ":" . ptr_sizeof("ptr"); +echo ":" . ptr_sizeof("EvalPtrSizeBox"); +echo ":" . function_exists("ptr_read16") . function_exists("buffer_new"); +return is_callable("ptr_write_string");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "N:1:8:16:8:40:11"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval can allocate buffers and use pointer memory helpers on their payload. +#[test] +fn execute_program_dispatches_buffer_and_pointer_memory_builtins() { + let program = parse_fragment( + br#"$buf = buffer_new(4); +$payload = ptr_offset($buf, 16); +echo buffer_len($buf) . ":"; +ptr_set($payload, 123456789); +echo ptr_get($payload) . ":"; +ptr_write8($payload, 255); +ptr_write8(ptr_offset($payload, 1), 1); +echo ptr_read8($payload) . "," . ptr_read8(ptr_offset($payload, 1)) . ":"; +call_user_func_array("ptr_write16", ["pointer" => $payload, "value" => 4660]); +echo ptr_read16($payload) . ":"; +ptr_write32($payload, 305419896); +echo ptr_read32($payload) . ":"; +$written = ptr_write_string($payload, "GET /"); +echo $written . ":" . ptr_read_string($payload, $written) . ":"; +echo strlen(ptr_read_string($payload, 0)); +buffer_free($buf); +echo ":" . (ptr_is_null($buf) ? "freed" : "live"); +return function_exists("buffer_free");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "4:123456789:255,1:4660:305419896:5:GET /:0:freed" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies `ptr($var)` remains rejected until eval has an lvalue-aware pointer path. +#[test] +fn execute_program_rejects_ptr_lvalue_builtin_without_storage_address() { + let program = parse_fragment(br#"$value = 1; return ptr($value);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("reject ptr lvalue"); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index 3bd2d5b0b8..6f5ea657d8 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -24,6 +24,7 @@ mod builtins_json; mod builtins_language_constructs; mod builtins_math_formatting; mod builtins_process_pipes; +mod builtins_raw_memory; mod builtins_readline; mod builtins_reflection_functions; mod builtins_scalars; diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index f222e65dfa..06c630a33f 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -11,28 +11,6 @@ use std::collections::BTreeSet; -/// Static-only raw memory helpers are elephc extensions tied to AOT FFI values. -const STATIC_ONLY_RAW_MEMORY_BUILTINS: &[&str] = &[ - "buffer_free", - "buffer_len", - "buffer_new", - "ptr", - "ptr_get", - "ptr_is_null", - "ptr_null", - "ptr_offset", - "ptr_read16", - "ptr_read32", - "ptr_read8", - "ptr_read_string", - "ptr_set", - "ptr_sizeof", - "ptr_write16", - "ptr_write32", - "ptr_write8", - "ptr_write_string", -]; - /// Eval-only reflection probes exist because magician can inspect dynamic eval metadata before the AOT catalog exposes them. const EVAL_ONLY_REFLECTION_BUILTINS: &[&str] = &[ "get_called_class", @@ -52,17 +30,12 @@ const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[ /// Eval supports variadic debug output before the static backend does. const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; -/// Verifies every non-raw-memory static builtin is visible through eval's function lookup. +/// Verifies every static builtin is visible through eval's function lookup. #[test] fn static_php_visible_builtins_are_visible_to_eval() { - let static_only = STATIC_ONLY_RAW_MEMORY_BUILTINS - .iter() - .copied() - .collect::>(); let missing = elephc::builtin_metadata::php_visible_builtin_names() .iter() .copied() - .filter(|name| !static_only.contains(name)) .filter(|name| !elephc_magician::builtin_metadata::php_visible_builtin_exists(name)) .collect::>(); @@ -75,18 +48,11 @@ fn static_php_visible_builtins_are_visible_to_eval() { /// Verifies eval has signature metadata for each shared static builtin. #[test] fn shared_builtin_signature_shape_matches_static_signatures() { - let static_only = STATIC_ONLY_RAW_MEMORY_BUILTINS - .iter() - .copied() - .collect::>(); let mut missing_static_signature = Vec::new(); let mut missing_eval_signature = Vec::new(); let mut mismatched_signatures = Vec::new(); for name in elephc::builtin_metadata::php_visible_builtin_names() { - if static_only.contains(name) { - continue; - } let Some(static_meta) = elephc::builtin_metadata::builtin_signature_metadata(name) else { missing_static_signature.push(*name); continue; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 7fdf651936..e0e2362000 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -960,6 +960,37 @@ echo defined("COUNT_RECURSIVE") ? "C" : "bad";'); assert_eq!(out, "4:2:3:6:2:3:C"); } +/// Verifies eval can dispatch raw pointer and buffer extension builtins through the bridge. +#[test] +fn test_eval_dispatches_raw_memory_builtins() { + let out = compile_and_run( + r#" $payload, "value" => 4660]); +echo ptr_read16($payload) . ":"; +ptr_write32($payload, 305419896); +echo ptr_read32($payload) . ":"; +$written = ptr_write_string($payload, "GET /"); +echo $written . ":" . ptr_read_string($payload, $written) . ":"; +echo strlen(ptr_read_string($payload, 0)); +buffer_free($buf); +echo ":" . (ptr_is_null($buf) ? "freed" : "live"); +return ":" . function_exists("ptr_read16") . is_callable("ptr_write_string") . function_exists("buffer_new");'); +"#, + ); + assert_eq!( + out, + "4:123456789:255,1:4660:305419896:5:GET /:0:freed:111" + ); +} + /// Verifies eval `count()` dispatches through `Countable` for generated/AOT objects. #[test] fn test_eval_counts_aot_countable_objects() { From bfd7a5fe20528942586171618ee245f2eb05a38c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 19:28:09 +0200 Subject: [PATCH 0939/1208] Support dynamic eval preg matches writeback --- .../builtins/registry/dynamic_mutation.rs | 54 +++++++++++++++++++ .../tests/builtins_strings_encoding.rs | 25 +++++++++ tests/codegen/eval.rs | 19 +++++++ 3 files changed, 98 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs index 20f24edfca..3321ad0b7f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -36,6 +36,8 @@ pub(in crate::interpreter) fn eval_mutating_builtin_with_call_array_args( "uasort" | "uksort" | "usort" => { eval_dynamic_user_sort_call(name, evaluated_args, context, values)? } + "preg_match" => eval_dynamic_preg_match_call(evaluated_args, context, values)?, + "preg_match_all" => eval_dynamic_preg_match_all_call(evaluated_args, context, values)?, _ => return Ok(None), }; Ok(result) @@ -191,6 +193,58 @@ fn eval_dynamic_user_sort_call( Ok(Some(result)) } +/// Evaluates a dynamic `preg_match()` call when `$matches` is a writable lvalue. +fn eval_dynamic_preg_match_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["pattern", "subject", "matches", "flags"], + evaluated_args, + false, + )?; + let pattern = required_evaluated_ref_arg(&bound, 0)?; + let subject = required_evaluated_ref_arg(&bound, 1)?; + let Some(matches) = optional_evaluated_ref_arg(&bound, 2) else { + return Ok(None); + }; + let Some(target) = matches.ref_target.as_ref() else { + return Ok(None); + }; + let flags = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let (result, matches_array) = + eval_preg_match_capture_result(pattern.value, subject.value, flags, values)?; + eval_write_preg_matches_target(target, matches_array, context, values)?; + Ok(Some(result)) +} + +/// Evaluates a dynamic `preg_match_all()` call when `$matches` is a writable lvalue. +fn eval_dynamic_preg_match_all_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["pattern", "subject", "matches", "flags"], + evaluated_args, + false, + )?; + let pattern = required_evaluated_ref_arg(&bound, 0)?; + let subject = required_evaluated_ref_arg(&bound, 1)?; + let Some(matches) = optional_evaluated_ref_arg(&bound, 2) else { + return Ok(None); + }; + let Some(target) = matches.ref_target.as_ref() else { + return Ok(None); + }; + let flags = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern.value, subject.value, flags, values)?; + eval_write_preg_matches_target(target, matches_array, context, values)?; + Ok(Some(result)) +} + /// Binds already evaluated arguments while preserving by-reference target metadata. fn bind_evaluated_ref_builtin_args( params: &[&str], diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs index 1b7485fc64..945de45f1c 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs @@ -176,6 +176,31 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun ); assert_eq!(values.get(result), FakeValue::Bool(true)); } + +/// Verifies dynamic preg callables write by-reference `$matches` targets. +#[test] +fn execute_program_dispatches_dynamic_preg_match_ref_targets() { + let program = parse_fragment( + br#"$match = "preg_match"; +$ok = $match("/([a-z]+)([0-9]+)/", "id42", $matches); +echo $ok . ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . ":"; +$matchAll = "preg_match_all"; +$count = $matchAll("/([a-z])([0-9])/", "a1 b2", $all, PREG_SET_ORDER); +echo $count . ":" . $all[1][0] . ":" . $all[1][2] . ":"; +$firstClass = preg_match(...); +$okAgain = $firstClass("/([A-Z]+)/", "ID", $firstClassMatches); +return $okAgain . ":" . $firstClassMatches[0];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:id42:id:42:2:b2:2:"); + assert_eq!(values.get(result), FakeValue::String("1:ID".to_string())); +} + /// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. #[test] fn execute_program_dispatches_html_entity_builtins() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e0e2362000..f44f3c7841 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2992,6 +2992,25 @@ echo function_exists("preg_match") && function_exists("preg_match_all") && funct ); } +/// Verifies dynamic eval preg callables write by-reference `$matches` arrays. +#[test] +fn test_eval_dynamic_preg_callables_write_matches_by_ref() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 19:50:07 +0200 Subject: [PATCH 0940/1208] Support dynamic eval flock writeback --- .../builtins/registry/dynamic_mutation.rs | 33 +++++++++++++++++++ .../tests/builtins_file_streams.rs | 12 ++++++- tests/codegen/eval.rs | 24 ++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs index 3321ad0b7f..b777f88b3d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -38,6 +38,7 @@ pub(in crate::interpreter) fn eval_mutating_builtin_with_call_array_args( } "preg_match" => eval_dynamic_preg_match_call(evaluated_args, context, values)?, "preg_match_all" => eval_dynamic_preg_match_all_call(evaluated_args, context, values)?, + "flock" => eval_dynamic_flock_call(evaluated_args, context, values)?, _ => return Ok(None), }; Ok(result) @@ -245,6 +246,38 @@ fn eval_dynamic_preg_match_all_call( Ok(Some(result)) } +/// Evaluates a dynamic `flock()` call when `$would_block` is a writable lvalue. +fn eval_dynamic_flock_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["stream", "operation", "would_block"], + evaluated_args, + false, + )?; + let stream = required_evaluated_ref_arg(&bound, 0)?; + let operation = required_evaluated_ref_arg(&bound, 1)?; + let Some(would_block) = optional_evaluated_ref_arg(&bound, 2) else { + return Ok(None); + }; + let Some(target) = would_block.ref_target.as_ref() else { + return Ok(None); + }; + let (success, would_block) = + eval_flock_result(stream.value, operation.value, context, values)?; + let would_block = values.bool_value(would_block)?; + eval_write_direct_ref_target( + target, + would_block, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + values.bool_value(success).map(Some) +} + /// Binds already evaluated arguments while preserving by-reference target metadata. fn bind_evaluated_ref_builtin_args( params: &[&str], diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs index 398036641f..0a49862ef9 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_file_streams.rs @@ -154,6 +154,16 @@ $staticName = "staticValue"; echo flock($h, LOCK_EX, $class::${{$staticName}}) ? "dynstaticlock" : "bad"; echo ":"; echo EvalFlockWouldBlockBox::$staticValue === false ? "dynstatic0" : "bad"; echo ":"; flock($h, LOCK_UN); +$lock = "flock"; +$dynamicWould = true; +echo $lock($h, LOCK_SH, $dynamicWould) ? "dynfnlock" : "bad"; echo ":"; +echo $dynamicWould === false ? "dynfn0" : "bad"; echo ":"; +flock($h, LOCK_UN); +$firstClassLock = flock(...); +$firstClassWould = true; +echo $firstClassLock($h, LOCK_EX, $firstClassWould) ? "fcclock" : "bad"; echo ":"; +echo $firstClassWould === false ? "fcc0" : "bad"; echo ":"; +flock($h, LOCK_UN); echo call_user_func("flock", $h, LOCK_SH) ? "calllock" : "bad"; echo ":"; flock($h, LOCK_UN); echo flock($h, 99) === false ? "invalid" : "bad"; echo ":"; @@ -174,7 +184,7 @@ return true;"# let _ = std::fs::remove_file(&file); assert_eq!( values.output, - "lock:would0:unlock:would1:proplock:prop0:dynlock:dyn0:staticlock:static0:dynstaticlock:dynstatic0:calllock:invalid:cleanup:11111:locks=1234" + "lock:would0:unlock:would1:proplock:prop0:dynlock:dyn0:staticlock:static0:dynstaticlock:dynstatic0:dynfnlock:dynfn0:fcclock:fcc0:calllock:invalid:cleanup:11111:locks=1234" ); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index f44f3c7841..a128bccf27 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3092,6 +3092,30 @@ echo function_exists("filesize"); echo function_exists("unlink");'); ); } +/// Verifies dynamic eval `flock()` callables write the by-reference `$would_block` output. +#[test] +fn test_eval_dynamic_flock_callables_write_would_block_by_ref() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 20:13:24 +0200 Subject: [PATCH 0941/1208] Support eval stream socket ref outputs --- .../builtins/filesystem/stream_sockets.rs | 131 +++++++++++++++++- .../builtins/registry/dynamic_mutation.rs | 79 ++++++++++- .../src/interpreter/expressions.rs | 6 + .../tests/builtins_stream_sockets.rs | 25 +++- tests/codegen/eval.rs | 32 +++++ 5 files changed, 260 insertions(+), 13 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs index 81e20eaef0..d1cd2e13b4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs @@ -115,10 +115,39 @@ pub(in crate::interpreter) fn eval_builtin_stream_socket_accept( return Err(EvalStatus::RuntimeFatal); } let socket = eval_expr(&args[0], context, scope, values)?; - for arg in &args[1..] { - let _ = eval_expr(arg, context, scope, values)?; + if let Some(timeout) = args.get(1) { + let _ = eval_expr(timeout, context, scope, values)?; } - eval_stream_socket_accept_result(socket, context, values) + let peer_name_target = args + .get(2) + .map(|peer_name| eval_socket_output_ref_target(peer_name, context, scope, values)) + .transpose()?; + let (result, peer_name) = eval_stream_socket_accept_with_peer_result(socket, context, values)?; + eval_write_socket_output_ref_target(peer_name_target.as_ref(), peer_name, context, values)?; + Ok(result) +} + +/// Evaluates `stream_socket_accept()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_socket_accept_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "timeout", "peer_name"], + &evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let peer_name_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, peer_name) = + eval_stream_socket_accept_with_peer_result(socket.value, context, values)?; + eval_write_socket_output_ref_target(peer_name_target.as_ref(), peer_name, context, values)?; + Ok(result) } /// Accepts one pending TCP connection from a listener resource. @@ -134,6 +163,21 @@ pub(in crate::interpreter) fn eval_stream_socket_accept_result( } } +/// Accepts one TCP connection and returns the accepted resource plus peer endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_accept_with_peer_result( + socket: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let id = eval_socket_resource_id(socket, values)?; + let Some(accepted_id) = context.stream_resources_mut().accept_tcp(id) else { + return values.bool_value(false).map(|result| (result, None)); + }; + let peer_name = context.stream_resources().socket_name(accepted_id, true); + let result = values.resource(accepted_id)?; + Ok((result, peer_name)) +} + /// Evaluates `stream_socket_get_name(socket, remote)`. pub(in crate::interpreter) fn eval_builtin_stream_socket_get_name( args: &[EvalExpr], @@ -269,10 +313,41 @@ pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom( } let stream = eval_expr(&args[0], context, scope, values)?; let length = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - let _ = eval_expr(arg, context, scope, values)?; + if let Some(flags) = args.get(2) { + let _ = eval_expr(flags, context, scope, values)?; } - eval_stream_socket_recvfrom_result(stream, length, context, values) + let address_target = args + .get(3) + .map(|address| eval_socket_output_ref_target(address, context, scope, values)) + .transpose()?; + let (result, address) = + eval_stream_socket_recvfrom_with_address_result(stream, length, context, values)?; + eval_write_socket_output_ref_target(address_target.as_ref(), address, context, values)?; + Ok(result) +} + +/// Evaluates `stream_socket_recvfrom()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "length", "flags", "address"], + &evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let length = required_evaluated_ref_arg(&bound, 1)?; + let address_target = optional_evaluated_ref_arg(&bound, 3) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, address) = + eval_stream_socket_recvfrom_with_address_result(socket.value, length.value, context, values)?; + eval_write_socket_output_ref_target(address_target.as_ref(), address, context, values)?; + Ok(result) } /// Reads bytes from a connected socket stream. @@ -285,6 +360,50 @@ pub(in crate::interpreter) fn eval_stream_socket_recvfrom_result( eval_fread_result(stream, length, context, values) } +/// Reads bytes from a connected socket stream and returns the tracked remote endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_with_address_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let id = eval_socket_resource_id(stream, values)?; + let address = context.stream_resources().socket_name(id, true); + let result = eval_fread_result(stream, length, context, values)?; + Ok((result, address)) +} + +/// Captures a writable output argument target for socket by-reference parameters. +fn eval_socket_output_ref_target( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, target) = eval_call_arg_value(expr, context, scope, values)?; + target.ok_or(EvalStatus::RuntimeFatal) +} + +/// Writes a socket output string to a captured by-reference target when available. +fn eval_write_socket_output_ref_target( + target: Option<&EvalReferenceTarget>, + value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((target, value)) = target.zip(value) else { + return Ok(()); + }; + let value = values.string(&value)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} + /// Evaluates `stream_socket_pair(domain, type, protocol)`. pub(in crate::interpreter) fn eval_builtin_stream_socket_pair( args: &[EvalExpr], diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs index b777f88b3d..71c18d4fb6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -39,6 +39,12 @@ pub(in crate::interpreter) fn eval_mutating_builtin_with_call_array_args( "preg_match" => eval_dynamic_preg_match_call(evaluated_args, context, values)?, "preg_match_all" => eval_dynamic_preg_match_all_call(evaluated_args, context, values)?, "flock" => eval_dynamic_flock_call(evaluated_args, context, values)?, + "stream_socket_accept" => { + eval_dynamic_stream_socket_accept_call(evaluated_args, context, values)? + } + "stream_socket_recvfrom" => { + eval_dynamic_stream_socket_recvfrom_call(evaluated_args, context, values)? + } _ => return Ok(None), }; Ok(result) @@ -278,8 +284,75 @@ fn eval_dynamic_flock_call( values.bool_value(success).map(Some) } +/// Evaluates a dynamic `stream_socket_accept()` call when `$peer_name` is writable. +fn eval_dynamic_stream_socket_accept_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "timeout", "peer_name"], + evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let Some(peer_name) = optional_evaluated_ref_arg(&bound, 2) else { + return Ok(None); + }; + let Some(target) = peer_name.ref_target.as_ref() else { + return Ok(None); + }; + let (result, peer_name) = + eval_stream_socket_accept_with_peer_result(socket.value, context, values)?; + if let Some(peer_name) = peer_name { + let peer_name = values.string(&peer_name)?; + eval_write_direct_ref_target( + target, + peer_name, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(Some(result)) +} + +/// Evaluates a dynamic `stream_socket_recvfrom()` call when `$address` is writable. +fn eval_dynamic_stream_socket_recvfrom_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "length", "flags", "address"], + evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let length = required_evaluated_ref_arg(&bound, 1)?; + let Some(address) = optional_evaluated_ref_arg(&bound, 3) else { + return Ok(None); + }; + let Some(target) = address.ref_target.as_ref() else { + return Ok(None); + }; + let (result, address) = + eval_stream_socket_recvfrom_with_address_result(socket.value, length.value, context, values)?; + if let Some(address) = address { + let address = values.string(&address)?; + eval_write_direct_ref_target( + target, + address, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(Some(result)) +} + /// Binds already evaluated arguments while preserving by-reference target metadata. -fn bind_evaluated_ref_builtin_args( +pub(in crate::interpreter) fn bind_evaluated_ref_builtin_args( params: &[&str], evaluated_args: &[EvaluatedCallArg], variadic_last: bool, @@ -331,7 +404,7 @@ fn bind_evaluated_ref_builtin_args( } /// Returns a required already evaluated argument by bound parameter index. -fn required_evaluated_ref_arg( +pub(in crate::interpreter) fn required_evaluated_ref_arg( bound_args: &[Option], index: usize, ) -> Result<&EvaluatedCallArg, EvalStatus> { @@ -342,7 +415,7 @@ fn required_evaluated_ref_arg( } /// Returns an optional already evaluated argument by bound parameter index. -fn optional_evaluated_ref_arg( +pub(in crate::interpreter) fn optional_evaluated_ref_arg( bound_args: &[Option], index: usize, ) -> Option<&EvaluatedCallArg> { diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 1b6b39a50a..9610bd2fc9 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -708,6 +708,12 @@ pub(in crate::interpreter) fn eval_call( if name == "flock" { return eval_builtin_flock(args, context, scope, values); } + if name == "stream_socket_accept" { + return eval_builtin_stream_socket_accept_call(args, context, scope, values); + } + if name == "stream_socket_recvfrom" { + return eval_builtin_stream_socket_recvfrom_call(args, context, scope, values); + } if matches!( name, "array_pop" diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs index df446b7d7a..4056213f24 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs @@ -21,11 +21,15 @@ $addr = stream_socket_get_name($server, false); echo $addr !== false ? "name" : "bad"; echo ":"; $client = stream_socket_client("tcp://" . $addr); echo is_resource($client) ? "client" : "bad"; echo ":"; -$peer = stream_socket_accept($server); +$peerName = ""; +$peer = stream_socket_accept($server, null, $peerName); echo is_resource($peer) ? "accept" : "bad"; echo ":"; +echo $peerName !== "" ? "peerout" : "bad"; echo ":"; echo stream_socket_get_name($client, true) !== false ? "peername" : "bad"; echo ":"; echo stream_socket_sendto($client, "ping") === 4 ? "send" : "bad"; echo ":"; -echo stream_socket_recvfrom($peer, 4) === "ping" ? "recv" : "bad"; echo ":"; +$remoteAddr = ""; +echo stream_socket_recvfrom($peer, 4, 0, $remoteAddr) === "ping" ? "recv" : "bad"; echo ":"; +echo $remoteAddr !== "" ? "addrout" : "bad"; echo ":"; fwrite($peer, "pong"); echo fread($client, 4) === "pong" ? "roundtrip" : "bad"; echo ":"; echo stream_socket_enable_crypto($client, false) ? "cryptooff" : "bad"; echo ":"; @@ -45,6 +49,18 @@ $pfs = pfsockopen("127.0.0.1", intval($parts[1])); $peer = stream_socket_accept($server); echo is_resource($pfs) && is_resource($peer) ? "pfsock" : "bad"; echo ":"; fclose($pfs); fclose($peer); fclose($server); +$server = stream_socket_server("tcp://127.0.0.1:0"); +$addr = stream_socket_get_name($server, false); +$accept = "stream_socket_accept"; +$client = stream_socket_client("tcp://" . $addr); +$dynPeer = ""; +$peer = $accept($server, null, $dynPeer); +echo is_resource($peer) && $dynPeer !== "" ? "dynaccept" : "bad"; echo ":"; +fwrite($client, "call"); +$recv = stream_socket_recvfrom(...); +$dynAddr = ""; +echo $recv($peer, 4, 0, $dynAddr) === "call" && $dynAddr !== "" ? "dynrecv" : "bad"; echo ":"; +fclose($client); fclose($peer); fclose($server); $pair = stream_socket_pair(1, 1, 0); echo is_array($pair) && is_resource($pair[0]) && is_resource($pair[1]) ? "pair" : "bad"; echo ":"; fwrite($pair[0], "xy"); @@ -69,8 +85,9 @@ return true;"#, assert_eq!( values.output, concat!( - "server:name:client:accept:peername:send:recv:roundtrip:cryptooff:", - "shutdown:fsock:pfsock:pair:pairio:select:111111111111" + "server:name:client:accept:peerout:peername:send:recv:addrout:", + "roundtrip:cryptooff:shutdown:fsock:pfsock:dynaccept:dynrecv:", + "pair:pairio:select:111111111111" ) ); assert_eq!(values.get(result), FakeValue::Bool(true)); diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a128bccf27..dfc084434e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3116,6 +3116,38 @@ echo unlink("eval-lock.txt") ? "cleanup" : "bad";'); assert_eq!(out, "dynlock:dyn0:fcclock:fcc0:cleanup"); } +/// Verifies dynamic eval stream-socket callables write by-reference output parameters. +#[test] +fn test_eval_dynamic_stream_socket_callables_write_ref_outputs() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 20:31:22 +0200 Subject: [PATCH 0942/1208] Support eval socket stream ref outputs --- .../builtins/filesystem/stream_sockets.rs | 169 +++++++++++++++++- .../builtins/registry/dynamic_mutation.rs | 97 ++++++++++ .../src/interpreter/expressions.rs | 6 + .../tests/builtins_stream_sockets.rs | 28 ++- .../elephc-magician/src/stream_resources.rs | 24 ++- tests/codegen/eval.rs | 34 ++++ 6 files changed, 343 insertions(+), 15 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs index d1cd2e13b4..293b52bcc0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs @@ -80,10 +80,70 @@ pub(in crate::interpreter) fn eval_builtin_fsockopen( } let host = eval_expr(&args[0], context, scope, values)?; let port = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - let _ = eval_expr(arg, context, scope, values)?; + let error_code_target = args + .get(2) + .map(|error_code| eval_socket_output_ref_target(error_code, context, scope, values)) + .transpose()?; + let error_message_target = args + .get(3) + .map(|error_message| eval_socket_output_ref_target(error_message, context, scope, values)) + .transpose()?; + if let Some(timeout) = args.get(4) { + let _ = eval_expr(timeout, context, scope, values)?; } - eval_fsockopen_result(host, port, context, values) + let (result, error_code, error_message) = + eval_fsockopen_with_error_result(host, port, context, values)?; + eval_write_socket_int_output_ref_target( + error_code_target.as_ref(), + error_code, + context, + values, + )?; + eval_write_socket_output_ref_target( + error_message_target.as_ref(), + Some(error_message), + context, + values, + )?; + Ok(result) +} + +/// Evaluates `fsockopen()` or `pfsockopen()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_fsockopen_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["hostname", "port", "error_code", "error_message", "timeout"], + &evaluated_args, + false, + )?; + let host = required_evaluated_ref_arg(&bound, 0)?; + let port = required_evaluated_ref_arg(&bound, 1)?; + let error_code_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let error_message_target = optional_evaluated_ref_arg(&bound, 3) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, error_code, error_message) = + eval_fsockopen_with_error_result(host.value, port.value, context, values)?; + eval_write_socket_int_output_ref_target( + error_code_target.as_ref(), + error_code, + context, + values, + )?; + eval_write_socket_output_ref_target( + error_message_target.as_ref(), + Some(error_message), + context, + values, + )?; + Ok(result) } /// Opens a connected TCP stream from host and port cells. @@ -104,6 +164,27 @@ pub(in crate::interpreter) fn eval_fsockopen_result( } } +/// Opens a host/port TCP stream and returns PHP `fsockopen()` error outputs. +pub(in crate::interpreter) fn eval_fsockopen_with_error_result( + host: RuntimeCellHandle, + port: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, i64, String), EvalStatus> { + let host = eval_path_string(host, values)?; + let port = eval_int_value(port, values)?; + match context + .stream_resources_mut() + .open_tcp_stream_host_port_result(&host, port) + { + Ok(id) => Ok((values.resource(id)?, 0, String::new())), + Err(error) => { + let error_code = i64::from(error.raw_os_error().unwrap_or(0)); + Ok((values.bool_value(false)?, error_code, error.to_string())) + } + } +} + /// Evaluates `stream_socket_accept(socket, timeout = null, peer_name = null)`. pub(in crate::interpreter) fn eval_builtin_stream_socket_accept( args: &[EvalExpr], @@ -404,6 +485,26 @@ fn eval_write_socket_output_ref_target( ) } +/// Writes a socket output integer to a captured by-reference target when available. +fn eval_write_socket_int_output_ref_target( + target: Option<&EvalReferenceTarget>, + value: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(target) = target else { + return Ok(()); + }; + let value = values.int(value)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} + /// Evaluates `stream_socket_pair(domain, type, protocol)`. pub(in crate::interpreter) fn eval_builtin_stream_socket_pair( args: &[EvalExpr], @@ -448,10 +549,49 @@ pub(in crate::interpreter) fn eval_builtin_stream_select( return Err(EvalStatus::RuntimeFatal); } let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { + let mut targets = Vec::with_capacity(3); + for arg in args.iter().take(3) { + let (value, target) = eval_call_arg_value(arg, context, scope, values)?; + evaluated_args.push(value); + targets.push(target.ok_or(EvalStatus::RuntimeFatal)?); + } + for arg in args.iter().skip(3) { evaluated_args.push(eval_expr(arg, context, scope, values)?); } - eval_stream_select_result(&evaluated_args, context, values) + let result = eval_stream_select_result(&evaluated_args, context, values)?; + eval_write_stream_select_empty_arrays(&targets, context, values)?; + Ok(result) +} + +/// Evaluates `stream_select()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_select_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["read", "write", "except", "seconds", "microseconds"], + &evaluated_args, + false, + )?; + let read = required_evaluated_ref_arg(&bound, 0)?; + let write = required_evaluated_ref_arg(&bound, 1)?; + let except = required_evaluated_ref_arg(&bound, 2)?; + let seconds = required_evaluated_ref_arg(&bound, 3)?; + let targets = vec![ + read.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + write.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + except.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + ]; + let mut selected_args = vec![read.value, write.value, except.value, seconds.value]; + if let Some(microseconds) = optional_evaluated_ref_arg(&bound, 4) { + selected_args.push(microseconds.value); + } + let result = eval_stream_select_result(&selected_args, context, values)?; + eval_write_stream_select_empty_arrays(&targets, context, values)?; + Ok(result) } /// Evaluates materialized `stream_select(...)` arguments. @@ -469,6 +609,25 @@ pub(in crate::interpreter) fn eval_stream_select_result( values.int(0) } +/// Writes conservative empty readiness arrays back to `stream_select()` lvalues. +fn eval_write_stream_select_empty_arrays( + targets: &[EvalReferenceTarget], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for target in targets { + let value = values.array_new(0)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(()) +} + /// Invokes `stream_cast(STREAM_CAST_FOR_SELECT)` for wrapper resources in an array. fn eval_stream_select_cast_array( array: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs index 71c18d4fb6..281a4d2dec 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -39,6 +39,10 @@ pub(in crate::interpreter) fn eval_mutating_builtin_with_call_array_args( "preg_match" => eval_dynamic_preg_match_call(evaluated_args, context, values)?, "preg_match_all" => eval_dynamic_preg_match_all_call(evaluated_args, context, values)?, "flock" => eval_dynamic_flock_call(evaluated_args, context, values)?, + "fsockopen" | "pfsockopen" => { + eval_dynamic_fsockopen_call(evaluated_args, context, values)? + } + "stream_select" => eval_dynamic_stream_select_call(evaluated_args, context, values)?, "stream_socket_accept" => { eval_dynamic_stream_socket_accept_call(evaluated_args, context, values)? } @@ -284,6 +288,89 @@ fn eval_dynamic_flock_call( values.bool_value(success).map(Some) } +/// Evaluates a dynamic `fsockopen()`/`pfsockopen()` call when error outputs are writable. +fn eval_dynamic_fsockopen_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["hostname", "port", "error_code", "error_message", "timeout"], + evaluated_args, + false, + )?; + let host = required_evaluated_ref_arg(&bound, 0)?; + let port = required_evaluated_ref_arg(&bound, 1)?; + let error_code = optional_evaluated_ref_arg(&bound, 2); + let error_message = optional_evaluated_ref_arg(&bound, 3); + if error_code.is_none() && error_message.is_none() { + return Ok(None); + } + let error_code_target = optional_dynamic_ref_target(error_code)?; + let error_message_target = optional_dynamic_ref_target(error_message)?; + let (result, error_code, error_message) = + eval_fsockopen_with_error_result(host.value, port.value, context, values)?; + if let Some(target) = error_code_target { + let error_code = values.int(error_code)?; + eval_write_direct_ref_target( + target, + error_code, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + if let Some(target) = error_message_target { + let error_message = values.string(&error_message)?; + eval_write_direct_ref_target( + target, + error_message, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(Some(result)) +} + +/// Evaluates a dynamic `stream_select()` call and writes conservative empty arrays. +fn eval_dynamic_stream_select_call( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["read", "write", "except", "seconds", "microseconds"], + evaluated_args, + false, + )?; + let read = required_evaluated_ref_arg(&bound, 0)?; + let write = required_evaluated_ref_arg(&bound, 1)?; + let except = required_evaluated_ref_arg(&bound, 2)?; + let seconds = required_evaluated_ref_arg(&bound, 3)?; + let targets = [ + read.ref_target.as_ref().ok_or(EvalStatus::RuntimeFatal)?, + write.ref_target.as_ref().ok_or(EvalStatus::RuntimeFatal)?, + except.ref_target.as_ref().ok_or(EvalStatus::RuntimeFatal)?, + ]; + let mut selected_args = vec![read.value, write.value, except.value, seconds.value]; + if let Some(microseconds) = optional_evaluated_ref_arg(&bound, 4) { + selected_args.push(microseconds.value); + } + let result = eval_stream_select_result(&selected_args, context, values)?; + for target in targets { + let value = values.array_new(0)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(Some(result)) +} + /// Evaluates a dynamic `stream_socket_accept()` call when `$peer_name` is writable. fn eval_dynamic_stream_socket_accept_call( evaluated_args: &[EvaluatedCallArg], @@ -351,6 +438,16 @@ fn eval_dynamic_stream_socket_recvfrom_call( Ok(Some(result)) } +/// Returns a writable target for an optional dynamic by-reference argument. +fn optional_dynamic_ref_target( + arg: Option<&EvaluatedCallArg>, +) -> Result, EvalStatus> { + match arg { + Some(arg) => arg.ref_target.as_ref().map(Some).ok_or(EvalStatus::RuntimeFatal), + None => Ok(None), + } +} + /// Binds already evaluated arguments while preserving by-reference target metadata. pub(in crate::interpreter) fn bind_evaluated_ref_builtin_args( params: &[&str], diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 9610bd2fc9..58147d24f6 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -708,6 +708,12 @@ pub(in crate::interpreter) fn eval_call( if name == "flock" { return eval_builtin_flock(args, context, scope, values); } + if matches!(name, "fsockopen" | "pfsockopen") { + return eval_builtin_fsockopen_call(args, context, scope, values); + } + if name == "stream_select" { + return eval_builtin_stream_select_call(args, context, scope, values); + } if name == "stream_socket_accept" { return eval_builtin_stream_socket_accept_call(args, context, scope, values); } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs b/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs index 4056213f24..f8b9a11e58 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_stream_sockets.rs @@ -38,19 +38,30 @@ fclose($peer); fclose($client); fclose($server); $server = stream_socket_server("tcp://127.0.0.1:0"); $addr = stream_socket_get_name($server, false); $parts = explode(":", $addr); -$fs = fsockopen("127.0.0.1", intval($parts[1])); +$errno = 9; $errstr = "x"; +$fs = fsockopen("127.0.0.1", intval($parts[1]), $errno, $errstr); $peer = stream_socket_accept($server); -echo is_resource($fs) && is_resource($peer) ? "fsock" : "bad"; echo ":"; +echo is_resource($fs) && is_resource($peer) && $errno === 0 && $errstr === "" ? "fsock" : "bad"; echo ":"; fclose($fs); fclose($peer); fclose($server); $server = stream_socket_server("tcp://127.0.0.1:0"); $addr = call_user_func("stream_socket_get_name", $server, false); $parts = explode(":", $addr); -$pfs = pfsockopen("127.0.0.1", intval($parts[1])); +$perrno = 9; $perrstr = "x"; +$pfs = pfsockopen("127.0.0.1", intval($parts[1]), $perrno, $perrstr); $peer = stream_socket_accept($server); -echo is_resource($pfs) && is_resource($peer) ? "pfsock" : "bad"; echo ":"; +echo is_resource($pfs) && is_resource($peer) && $perrno === 0 && $perrstr === "" ? "pfsock" : "bad"; echo ":"; fclose($pfs); fclose($peer); fclose($server); $server = stream_socket_server("tcp://127.0.0.1:0"); $addr = stream_socket_get_name($server, false); +$parts = explode(":", $addr); +$open = "fsockopen"; +$dynErrno = 9; $dynErrstr = "x"; +$fs = $open("127.0.0.1", intval($parts[1]), $dynErrno, $dynErrstr); +$peer = stream_socket_accept($server); +echo is_resource($fs) && is_resource($peer) && $dynErrno === 0 && $dynErrstr === "" ? "dynfsock" : "bad"; echo ":"; +fclose($fs); fclose($peer); fclose($server); +$server = stream_socket_server("tcp://127.0.0.1:0"); +$addr = stream_socket_get_name($server, false); $accept = "stream_socket_accept"; $client = stream_socket_client("tcp://" . $addr); $dynPeer = ""; @@ -65,6 +76,11 @@ $pair = stream_socket_pair(1, 1, 0); echo is_array($pair) && is_resource($pair[0]) && is_resource($pair[1]) ? "pair" : "bad"; echo ":"; fwrite($pair[0], "xy"); echo fread($pair[1], 2) === "xy" ? "pairio" : "bad"; echo ":"; +$read = [$pair[1]]; $write = [$pair[0]]; $except = [$pair[0]]; +echo stream_select($read, $write, $except, 0) === 0 && count($read) === 0 && count($write) === 0 && count($except) === 0 ? "selectclear" : "bad"; echo ":"; +$read = [$pair[1]]; $write = []; $except = []; +$select = "stream_select"; +echo $select($read, $write, $except, 0) === 0 && count($read) === 0 ? "dynselect" : "bad"; echo ":"; fclose($pair[0]); fclose($pair[1]); $read = []; $write = []; $except = []; echo stream_select($read, $write, $except, 0) === 0 ? "select" : "bad"; echo ":"; @@ -86,8 +102,8 @@ return true;"#, values.output, concat!( "server:name:client:accept:peerout:peername:send:recv:addrout:", - "roundtrip:cryptooff:shutdown:fsock:pfsock:dynaccept:dynrecv:", - "pair:pairio:select:111111111111" + "roundtrip:cryptooff:shutdown:fsock:pfsock:dynfsock:dynaccept:dynrecv:", + "pair:pairio:selectclear:dynselect:select:111111111111" ) ); assert_eq!(values.get(result), FakeValue::Bool(true)); diff --git a/crates/elephc-magician/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs index 8a4e2dc2d9..4c2d2c9e98 100644 --- a/crates/elephc-magician/src/stream_resources.rs +++ b/crates/elephc-magician/src/stream_resources.rs @@ -14,7 +14,7 @@ use std::collections::{HashMap, HashSet}; use std::ffi::c_void; use std::fs::{File, Metadata, OpenOptions}; -use std::io::{Read, Seek, SeekFrom, Write}; +use std::io::{self, Read, Seek, SeekFrom, Write}; use std::net::{Shutdown, TcpListener, TcpStream}; use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd}; #[cfg(unix)] @@ -151,18 +151,34 @@ impl EvalStreamResources { /// Opens a connected TCP stream resource. pub(crate) fn open_tcp_stream(&mut self, address: &str) -> Option { - let stream = TcpStream::connect(eval_tcp_address(address)).ok()?; - self.insert_tcp_stream(stream) + self.open_tcp_stream_result(address).ok() + } + + /// Opens a connected TCP stream resource and preserves the host I/O error on failure. + pub(crate) fn open_tcp_stream_result(&mut self, address: &str) -> io::Result { + let stream = TcpStream::connect(eval_tcp_address(address))?; + self.insert_tcp_stream(stream).ok_or_else(|| { + io::Error::new(io::ErrorKind::Other, "failed to track eval TCP stream") + }) } /// Opens a connected TCP stream from separate host and port arguments. pub(crate) fn open_tcp_stream_host_port(&mut self, host: &str, port: i64) -> Option { + self.open_tcp_stream_host_port_result(host, port).ok() + } + + /// Opens a connected TCP stream from host and port while preserving I/O errors. + pub(crate) fn open_tcp_stream_host_port_result( + &mut self, + host: &str, + port: i64, + ) -> io::Result { let host = host .strip_prefix("tcp://") .or_else(|| host.strip_prefix("ssl://")) .or_else(|| host.strip_prefix("tls://")) .unwrap_or(host); - self.open_tcp_stream(&format!("{host}:{port}")) + self.open_tcp_stream_result(&format!("{host}:{port}")) } /// Accepts one TCP connection from a listener resource. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index dfc084434e..399ad62ed6 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3148,6 +3148,40 @@ fclose($client); fclose($peer); fclose($server);'); assert_eq!(out, "dynaccept:dynrecv:namedaccept:namedrecv"); } +/// Verifies eval fsockopen and stream_select callables write by-reference outputs. +#[test] +fn test_eval_dynamic_fsockopen_and_stream_select_write_ref_outputs() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 20:53:45 +0200 Subject: [PATCH 0943/1208] Support named eval preg ref outputs --- .../interpreter/builtins/regex/match_all.rs | 29 +++++++++++++++++++ .../interpreter/builtins/regex/match_one.rs | 29 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 3 +- .../builtins/registry/signature.rs | 5 ++-- .../src/interpreter/expressions.rs | 6 ++++ .../tests/builtins_strings_encoding.rs | 22 ++++++++++++++ tests/codegen/eval.rs | 17 +++++++++++ 7 files changed, 107 insertions(+), 4 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs b/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs index 5c79967522..33e570f506 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs @@ -48,6 +48,35 @@ pub(in crate::interpreter) fn eval_builtin_preg_match_all( } } +/// Evaluates PHP `preg_match_all()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_preg_match_all_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["pattern", "subject", "matches", "flags"], + &evaluated_args, + false, + )?; + let pattern = required_evaluated_ref_arg(&bound, 0)?; + let subject = required_evaluated_ref_arg(&bound, 1)?; + let flags = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let Some(matches) = optional_evaluated_ref_arg(&bound, 2) else { + return eval_preg_match_all_result(pattern.value, subject.value, values); + }; + let target = matches + .ref_target + .clone() + .ok_or(EvalStatus::RuntimeFatal)?; + let (result, matches_array) = + eval_preg_match_all_capture_result(pattern.value, subject.value, flags, values)?; + eval_write_preg_matches_target(&target, matches_array, context, values)?; + Ok(result) +} + /// Counts all non-overlapping regex matches in one subject string. pub(in crate::interpreter) fn eval_preg_match_all_result( pattern: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs b/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs index 27bce4fbf5..cd15e93e27 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs @@ -49,6 +49,35 @@ pub(in crate::interpreter) fn eval_builtin_preg_match( } } +/// Evaluates PHP `preg_match()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_preg_match_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["pattern", "subject", "matches", "flags"], + &evaluated_args, + false, + )?; + let pattern = required_evaluated_ref_arg(&bound, 0)?; + let subject = required_evaluated_ref_arg(&bound, 1)?; + let flags = optional_evaluated_ref_arg(&bound, 3).map(|arg| arg.value); + let Some(matches) = optional_evaluated_ref_arg(&bound, 2) else { + return eval_preg_match_result(pattern.value, subject.value, values); + }; + let target = matches + .ref_target + .clone() + .ok_or(EvalStatus::RuntimeFatal)?; + let (result, matches_array) = + eval_preg_match_capture_result(pattern.value, subject.value, flags, values)?; + eval_write_preg_matches_target(&target, matches_array, context, values)?; + Ok(result) +} + /// Returns whether one regex matches the subject string. pub(in crate::interpreter) fn eval_preg_match_result( pattern: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index af59ee454d..e2648d27a0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -318,8 +318,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } "ptr_write_string" => Some(&["pointer", "string"]), "ptr_sizeof" => Some(&["type"]), - "preg_match" => Some(&["pattern", "subject", "matches"]), - "preg_match_all" => Some(&["pattern", "subject"]), + "preg_match" | "preg_match_all" => Some(&["pattern", "subject", "matches", "flags"]), "preg_replace" => Some(&["pattern", "replacement", "subject"]), "preg_replace_callback" => Some(&["pattern", "callback", "subject"]), "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index f158fc3aaf..30bb6514aa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -114,7 +114,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "min" | "max" => variadic(params, &[]), "json_encode" | "json_decode" | "json_validate" => optional(params, 1), - "preg_match" => optional_by_ref(params, 2, &["matches"]), + "preg_match" | "preg_match_all" => optional_by_ref(params, 2, &["matches"]), "preg_split" => optional(params, 2), "print_r" => optional(params, 1), "var_dump" => variadic(params, &[]), @@ -227,7 +227,8 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("json_validate", 1) => Int(512), ("json_validate", 2) => Int(0), - ("preg_match", 2) => EmptyArray, + ("preg_match" | "preg_match_all", 2) => EmptyArray, + ("preg_match" | "preg_match_all", 3) => Int(0), ("preg_split", 2) => Int(-1), ("preg_split", 3) => Int(0), ("print_r", 1) => Bool(false), diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 58147d24f6..5ddb328ef1 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -708,6 +708,12 @@ pub(in crate::interpreter) fn eval_call( if name == "flock" { return eval_builtin_flock(args, context, scope, values); } + if name == "preg_match" { + return eval_builtin_preg_match_call(args, context, scope, values); + } + if name == "preg_match_all" { + return eval_builtin_preg_match_all_call(args, context, scope, values); + } if matches!(name, "fsockopen" | "pfsockopen") { return eval_builtin_fsockopen_call(args, context, scope, values); } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs index 945de45f1c..2ff5103dc6 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs @@ -201,6 +201,28 @@ return $okAgain . ":" . $firstClassMatches[0];"#, assert_eq!(values.get(result), FakeValue::String("1:ID".to_string())); } +/// Verifies named direct preg calls write by-reference `$matches` targets. +#[test] +fn execute_program_dispatches_named_preg_match_ref_targets() { + let program = parse_fragment( + br#"$named = []; +$ok = preg_match(pattern: "/([a-z]+)([0-9]+)/", subject: "id42", matches: $named); +echo $ok . ":" . $named[0] . ":" . $named[1] . ":" . $named[2] . ":"; +$all = []; +$count = preg_match_all(pattern: "/([a-z])([0-9])/", subject: "a1 b2", matches: $all, flags: PREG_SET_ORDER); +echo $count . ":" . $all[1][0] . ":" . $all[1][2] . ":"; +return preg_match(pattern: "/x/", subject: "x", flags: PREG_OFFSET_CAPTURE);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:id42:id:42:2:b2:2:"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} + /// Verifies eval HTML entity builtins encode, decode, and dispatch as callables. #[test] fn execute_program_dispatches_html_entity_builtins() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 399ad62ed6..46e08ad9a8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3011,6 +3011,23 @@ echo $okAgain . ":" . $firstClassMatches[0];'); assert_eq!(out, "1:id42:id:42:2:b2:2:1:ID"); } +/// Verifies named eval preg calls write by-reference `$matches` arrays. +#[test] +fn test_eval_named_preg_calls_write_matches_by_ref() { + let out = compile_and_run( + r#" Date: Mon, 29 Jun 2026 21:04:00 +0200 Subject: [PATCH 0944/1208] Document eval preg signature extensions --- tests/builtin_parity_tests.rs | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 06c630a33f..4365b9f2f9 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -24,9 +24,13 @@ const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[ "array_reverse", "array_splice", "nl2br", + "preg_match", "print_r", ]; +/// Eval supports extra optional by-reference parameters before the static backend does. +const EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["preg_match_all"]; + /// Eval supports variadic debug output before the static backend does. const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; @@ -65,6 +69,10 @@ fn shared_builtin_signature_shape_matches_static_signatures() { assert_eval_signature_extends_static_signature(name, &static_meta, &eval_meta); continue; } + if EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS.contains(name) { + assert_eval_by_ref_signature_extends_static_signature(name, &static_meta, &eval_meta); + continue; + } if EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS.contains(name) { assert_eval_variadic_signature_extends_static_signature( name, @@ -126,6 +134,34 @@ fn assert_eval_signature_extends_static_signature( ); } +/// Verifies a documented eval by-reference extension keeps the static prefix contract. +fn assert_eval_by_ref_signature_extends_static_signature( + name: &str, + static_meta: &elephc::builtin_metadata::BuiltinSignatureMetadata, + eval_meta: &elephc_magician::builtin_metadata::BuiltinSignatureMetadata, +) { + assert!( + eval_meta.params.starts_with(&static_meta.params), + "{name} eval by-ref extension must preserve static parameter prefix: static={static_meta:#?} eval={eval_meta:#?}" + ); + assert_eq!( + static_meta.required_param_count, eval_meta.required_param_count, + "{name} eval by-ref extension must preserve required parameter count" + ); + assert_eq!( + static_meta.variadic, eval_meta.variadic, + "{name} eval by-ref extension must not change variadic behavior" + ); + assert!( + eval_meta.by_ref_params.starts_with(&static_meta.by_ref_params), + "{name} eval by-ref extension must preserve static by-reference prefix" + ); + assert!( + eval_meta.default_param_count >= static_meta.default_param_count, + "{name} eval by-ref extension must not remove defaults" + ); +} + /// Verifies a documented eval variadic extension keeps the static prefix contract. fn assert_eval_variadic_signature_extends_static_signature( name: &str, From a9c8e90cdda380772c287689efc4d678c9023c9a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 21:51:55 +0200 Subject: [PATCH 0945/1208] Support eval by-ref call arrays --- crates/elephc-magician/src/context.rs | 31 ++++++ crates/elephc-magician/src/eval_ir.rs | 2 + .../src/interpreter/array_literals.rs | 103 ++++++++++++++++-- .../builtins/arrays/filters/iterator.rs | 5 +- .../interpreter/builtins/registry/callable.rs | 2 +- .../builtins/registry/dispatch/arrays.rs | 2 +- .../src/interpreter/dynamic_functions.rs | 26 ++++- .../src/interpreter/expressions.rs | 8 +- crates/elephc-magician/src/interpreter/mod.rs | 6 +- .../src/interpreter/reflection.rs | 10 +- .../src/interpreter/statements.rs | 5 +- .../src/interpreter/tests/dynamic_calls.rs | 32 ++++++ .../elephc-magician/src/parser/expressions.rs | 23 +++- .../elephc-magician/src/parser/statements.rs | 11 ++ .../src/parser/tests/arrays_objects.rs | 18 +++ tests/codegen/eval.rs | 59 ++++++++++ 16 files changed, 311 insertions(+), 32 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 21e5303857..9149e65bdc 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -275,6 +275,13 @@ pub enum EvalReferenceTarget { }, } +/// Normalized PHP array key used for eval-side reference metadata. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum EvalArrayReferenceKey { + Int(i64), + String(Vec), +} + /// Late-static dispatch metadata attached to eval-created static callable arrays. #[derive(Clone)] struct EvalStaticCallableMetadata { @@ -782,6 +789,7 @@ pub struct ElephcEvalContext { dynamic_destructing_objects: HashSet, dynamic_destructed_objects: HashSet, dynamic_property_aliases: HashMap<(u64, String), EvalReferenceTarget>, + array_element_aliases: HashMap<(usize, EvalArrayReferenceKey), EvalReferenceTarget>, dynamic_initialized_properties: HashSet<(u64, String)>, eval_reflection_attributes: HashMap, eval_reflection_classes: HashMap, @@ -850,6 +858,7 @@ impl ElephcEvalContext { dynamic_destructing_objects: HashSet::new(), dynamic_destructed_objects: HashSet::new(), dynamic_property_aliases: HashMap::new(), + array_element_aliases: HashMap::new(), dynamic_initialized_properties: HashSet::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), @@ -919,6 +928,7 @@ impl ElephcEvalContext { dynamic_destructing_objects: HashSet::new(), dynamic_destructed_objects: HashSet::new(), dynamic_property_aliases: HashMap::new(), + array_element_aliases: HashMap::new(), dynamic_initialized_properties: HashSet::new(), eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), @@ -1697,6 +1707,27 @@ impl ElephcEvalContext { .remove(&(identity, storage_property_name.to_string())) } + /// Binds one runtime array element slot to a PHP reference target. + pub fn bind_array_element_alias( + &mut self, + array: RuntimeCellHandle, + key: EvalArrayReferenceKey, + target: EvalReferenceTarget, + ) -> Option { + self.array_element_aliases + .insert((array.as_ptr() as usize, key), target) + } + + /// Returns the persistent reference target bound to one runtime array element slot. + pub fn array_element_alias( + &self, + array: RuntimeCellHandle, + key: &EvalArrayReferenceKey, + ) -> Option<&EvalReferenceTarget> { + self.array_element_aliases + .get(&(array.as_ptr() as usize, key.clone())) + } + /// Marks one eval object storage slot as initialized. pub fn mark_dynamic_property_initialized( &mut self, diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 71fa92b2b7..e2bc4457c5 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2734,7 +2734,9 @@ impl EvalCallArg { #[derive(Debug, Clone, PartialEq)] pub enum EvalArrayElement { Value(EvalExpr), + Reference(EvalExpr), KeyValue { key: EvalExpr, value: EvalExpr }, + KeyReference { key: EvalExpr, value: EvalExpr }, } /// One ordered arm in a PHP `match` expression parsed from an eval fragment. diff --git a/crates/elephc-magician/src/interpreter/array_literals.rs b/crates/elephc-magician/src/interpreter/array_literals.rs index d0673c6ff7..8674032516 100644 --- a/crates/elephc-magician/src/interpreter/array_literals.rs +++ b/crates/elephc-magician/src/interpreter/array_literals.rs @@ -17,14 +17,24 @@ pub(super) fn eval_indexed_array( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let array = values.array_new(elements.len())?; + let mut array = values.array_new(elements.len())?; for (index, element) in elements.iter().enumerate() { - let EvalArrayElement::Value(element) = element else { - return Err(EvalStatus::UnsupportedConstruct); - }; let index = values.int(index as i64)?; - let value = eval_expr(element, context, scope, values)?; - let _ = values.array_set(array, index, value)?; + let (value, target) = match element { + EvalArrayElement::Value(element) => (eval_expr(element, context, scope, values)?, None), + EvalArrayElement::Reference(element) => { + let (value, target) = + eval_reference_array_element_value(element, context, scope, values)?; + (value, Some(target)) + } + EvalArrayElement::KeyValue { .. } | EvalArrayElement::KeyReference { .. } => { + return Err(EvalStatus::UnsupportedConstruct); + } + }; + array = values.array_set(array, index, value)?; + if let Some(target) = target { + bind_array_element_reference(context, array, index, target, values)?; + } } Ok(array) } @@ -36,10 +46,10 @@ pub(super) fn eval_assoc_array( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let array = values.assoc_new(elements.len())?; + let mut array = values.assoc_new(elements.len())?; let mut next_key = None; for element in elements { - let (key, value) = match element { + let (key, value, target) = match element { EvalArrayElement::Value(value) => { let key = match next_key { Some(next_key) => next_key, @@ -47,20 +57,89 @@ pub(super) fn eval_assoc_array( }; let one = values.int(1)?; next_key = Some(values.add(key, one)?); - (key, value) + let value = eval_expr(value, context, scope, values)?; + (key, value, None) + } + EvalArrayElement::Reference(value) => { + let key = match next_key { + Some(next_key) => next_key, + None => values.int(0)?, + }; + let one = values.int(1)?; + next_key = Some(values.add(key, one)?); + let (value, target) = + eval_reference_array_element_value(value, context, scope, values)?; + (key, value, Some(target)) } EvalArrayElement::KeyValue { key, value } => { let key = eval_expr(key, context, scope, values)?; next_key = eval_array_next_key_after_explicit_key(key, next_key, values)?; - (key, value) + let value = eval_expr(value, context, scope, values)?; + (key, value, None) + } + EvalArrayElement::KeyReference { key, value } => { + let key = eval_expr(key, context, scope, values)?; + next_key = eval_array_next_key_after_explicit_key(key, next_key, values)?; + let (value, target) = + eval_reference_array_element_value(value, context, scope, values)?; + (key, value, Some(target)) } }; - let value = eval_expr(value, context, scope, values)?; - let _ = values.array_set(array, key, value)?; + array = values.array_set(array, key, value)?; + if let Some(target) = target { + bind_array_element_reference(context, array, key, target, values)?; + } } Ok(array) } +/// Evaluates a by-reference array literal element and captures its writable source target. +fn eval_reference_array_element_value( + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { + let (value, target) = eval_call_arg_value(value, context, scope, values)?; + target + .map(|target| (value, target)) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Records one by-reference array element on the eval context side table. +fn bind_array_element_reference( + context: &mut ElephcEvalContext, + array: RuntimeCellHandle, + key: RuntimeCellHandle, + target: EvalReferenceTarget, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let key = eval_array_reference_key(key, values)?.ok_or(EvalStatus::RuntimeFatal)?; + context.bind_array_element_alias(array, key, target); + Ok(()) +} + +/// Normalizes a PHP array key for eval reference metadata lookups. +pub(in crate::interpreter) fn eval_array_reference_key( + key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + Ok(Some(match values.type_tag(key)? { + EVAL_TAG_INT => EvalArrayReferenceKey::Int(eval_int_value(key, values)?), + EVAL_TAG_STRING => { + let bytes = values.string_bytes(key)?; + if let Some(key) = eval_numeric_string_array_key(&bytes) { + EvalArrayReferenceKey::Int(key) + } else { + EvalArrayReferenceKey::String(bytes) + } + } + EVAL_TAG_NULL => EvalArrayReferenceKey::String(Vec::new()), + EVAL_TAG_BOOL | EVAL_TAG_FLOAT => EvalArrayReferenceKey::Int(eval_int_value(key, values)?), + _ => return Ok(None), + })) +} + /// Advances an array literal's automatic key after an integer-normalized explicit key. fn eval_array_next_key_after_explicit_key( key: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs index b1bebd4e75..11c9d4cefe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs @@ -31,7 +31,7 @@ pub(in crate::interpreter) fn eval_builtin_iterator_apply( let callback = eval_expr(callback, context, scope, values)?; let callback = eval_callable(callback, context, values)?; let callback_args = eval_expr(callback_args, context, scope, values)?; - let callback_args = eval_iterator_apply_arg_values(callback_args, values)?; + let callback_args = eval_iterator_apply_arg_values(callback_args, context, values)?; eval_iterator_apply_result(iterator, &callback, callback_args, context, values) } _ => Err(EvalStatus::RuntimeFatal), @@ -41,6 +41,7 @@ pub(in crate::interpreter) fn eval_builtin_iterator_apply( /// Converts the optional `iterator_apply()` callback-args value into call arguments. pub(in crate::interpreter) fn eval_iterator_apply_arg_values( args: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { if values.is_null(args)? { @@ -49,7 +50,7 @@ pub(in crate::interpreter) fn eval_iterator_apply_arg_values( if !values.is_array_like(args)? { return Err(EvalStatus::RuntimeFatal); } - eval_array_call_arg_values(args, values) + eval_array_call_arg_values(args, context, values) } /// Applies a callback to each valid position of an eval-supported Traversable object. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 21a7f5ad17..633e2a557f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -54,7 +54,7 @@ pub(in crate::interpreter) fn eval_call_user_func_array_with_values( if !values.is_array_like(arg_array)? { return Err(EvalStatus::RuntimeFatal); } - let evaluated_args = eval_array_call_arg_values(arg_array, values)?; + let evaluated_args = eval_array_call_arg_values(arg_array, context, values)?; eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs index 37dd0a09ca..373b9f3d15 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs @@ -237,7 +237,7 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( } [iterator, callback, args] => { let callback = eval_callable(*callback, context, values)?; - let callback_args = eval_iterator_apply_arg_values(*args, values)?; + let callback_args = eval_iterator_apply_arg_values(*args, context, values)?; eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? } _ => return Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 794834b0c1..817d47649f 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -54,7 +54,13 @@ pub(in crate::interpreter) fn eval_call_arg_values( if !values.is_array_like(spread)? { return Err(EvalStatus::RuntimeFatal); } - append_unpacked_call_arg_values(spread, &mut evaluated_args, &mut saw_named, values)?; + append_unpacked_call_arg_values( + spread, + &mut evaluated_args, + &mut saw_named, + context, + values, + )?; continue; } @@ -255,12 +261,19 @@ fn eval_static_property_call_arg_value( /// Converts a `call_user_func_array` argument array into ordered call arguments. pub(in crate::interpreter) fn eval_array_call_arg_values( arg_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let len = values.array_len(arg_array)?; let mut evaluated_args = Vec::with_capacity(len); let mut saw_named = false; - append_unpacked_call_arg_values(arg_array, &mut evaluated_args, &mut saw_named, values)?; + append_unpacked_call_arg_values( + arg_array, + &mut evaluated_args, + &mut saw_named, + context, + values, + )?; Ok(evaluated_args) } @@ -269,12 +282,15 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( array: RuntimeCellHandle, evaluated_args: &mut Vec, saw_named: &mut bool, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let len = values.array_len(array)?; for position in 0..len { let key = values.array_iter_key(array, position)?; let value = values.array_get(array, key)?; + let ref_target = eval_array_reference_key(key, values)? + .and_then(|key| context.array_element_alias(array, &key).cloned()); match values.type_tag(key)? { EVAL_TAG_INT => { if *saw_named { @@ -283,7 +299,7 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( evaluated_args.push(EvaluatedCallArg { name: None, value, - ref_target: None, + ref_target, }); } EVAL_TAG_STRING => { @@ -293,7 +309,7 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( evaluated_args.push(EvaluatedCallArg { name: Some(name), value, - ref_target: None, + ref_target, }); } _ => return Err(EvalStatus::RuntimeFatal), @@ -1383,10 +1399,12 @@ fn eval_method_default_call_arg_is_supported(arg: &EvalCallArg) -> bool { fn eval_method_default_array_element_is_supported(element: &EvalArrayElement) -> bool { match element { EvalArrayElement::Value(value) => eval_method_default_expr_is_supported(value), + EvalArrayElement::Reference(_) => false, EvalArrayElement::KeyValue { key, value } => { eval_method_default_expr_is_supported(key) && eval_method_default_expr_is_supported(value) } + EvalArrayElement::KeyReference { .. } => false, } } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 5ddb328ef1..c9465ec5b0 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -22,7 +22,13 @@ pub(in crate::interpreter) fn eval_expr( EvalExpr::Array(elements) => { if elements .iter() - .any(|element| matches!(element, EvalArrayElement::KeyValue { .. })) + .any(|element| { + matches!( + element, + EvalArrayElement::KeyValue { .. } + | EvalArrayElement::KeyReference { .. } + ) + }) { eval_assoc_array(elements, context, scope, values) } else { diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 9cf8789699..73a814b6fb 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -34,8 +34,8 @@ mod statements; mod throwables; use crate::context::{ - ElephcEvalContext, ElephcEvalExecutionScope, EvalReferenceTarget, NativeCallableDefault, - NativeCallableSignature, NativeFunction, + ElephcEvalContext, ElephcEvalExecutionScope, EvalArrayReferenceKey, EvalReferenceTarget, + NativeCallableDefault, NativeCallableSignature, NativeFunction, }; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ @@ -204,7 +204,7 @@ pub fn execute_context_function_call_array_outcome( if !values.is_array_like(arg_array)? { return Err(EvalStatus::RuntimeFatal); } - let evaluated_args = eval_array_call_arg_values(arg_array, values)?; + let evaluated_args = eval_array_call_arg_values(arg_array, context, values)?; match eval_callable_with_call_array_args(name, evaluated_args, context, values) { Ok(result) => Ok(EvalOutcome::Value(result)), Err(EvalStatus::UncaughtThrowable) => context diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 390a16c567..b2b7867353 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1213,7 +1213,7 @@ pub(in crate::interpreter) fn eval_reflection_method_invoke_result( let (object, method_args) = if is_invoke { eval_reflection_method_invoke_args(evaluated_args)? } else { - eval_reflection_method_invoke_args_array(evaluated_args, values)? + eval_reflection_method_invoke_args_array(evaluated_args, context, values)? }; eval_reflection_method_invoke_dispatch( &declaring_class, @@ -1251,7 +1251,7 @@ pub(in crate::interpreter) fn eval_reflection_function_invoke_result( .map(eval_reflection_method_forwarded_value_arg) .collect() } else { - eval_reflection_function_invoke_args_array(evaluated_args, values)? + eval_reflection_function_invoke_args_array(evaluated_args, context, values)? }; eval_reflection_function_invoke_dispatch(&function_name, function_args, context, values) .map(Some) @@ -8390,23 +8390,25 @@ fn eval_reflection_method_forwarded_value_arg(arg: EvaluatedCallArg) -> Evaluate /// Binds `ReflectionMethod::invokeArgs()` and expands its PHP argument array. fn eval_reflection_method_invoke_args_array( evaluated_args: Vec, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { let args = bind_evaluated_function_args( &[String::from("object"), String::from("args")], evaluated_args, )?; - let method_args = eval_array_call_arg_values(args[1], values)?; + let method_args = eval_array_call_arg_values(args[1], context, values)?; Ok((args[0], method_args)) } /// Binds `ReflectionFunction::invokeArgs()` and expands its PHP argument array. fn eval_reflection_function_invoke_args_array( evaluated_args: Vec, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; - eval_array_call_arg_values(args[0], values) + eval_array_call_arg_values(args[0], context, values) } /// Dispatches one reflected function invocation through eval or registered native functions. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 8cc6bb9f19..766fada21f 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -9112,7 +9112,7 @@ fn eval_reflection_class_new_instance_result( let constructor_args = if method_name.eq_ignore_ascii_case("newInstance") { evaluated_args } else if method_name.eq_ignore_ascii_case("newInstanceArgs") { - eval_reflection_class_new_instance_args(evaluated_args, values)? + eval_reflection_class_new_instance_args(evaluated_args, context, values)? } else { return Ok(None); }; @@ -9169,10 +9169,11 @@ fn eval_reflection_class_new_instance_result( /// Expands the single `ReflectionClass::newInstanceArgs()` array argument. fn eval_reflection_class_new_instance_args( evaluated_args: Vec, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; - eval_array_call_arg_values(args[0], values) + eval_array_call_arg_values(args[0], context, values) } /// Runs ReflectionClass construction with only public constructor visibility. diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 9ace6d45fd..273efbcaa4 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -651,6 +651,38 @@ return $value;"#, assert_eq!(values.get(result), FakeValue::Int(3)); } +/// Verifies `call_user_func_array()` preserves by-reference array elements for AOT methods. +#[test] +fn execute_program_call_user_func_array_runtime_method_writes_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$cb = [$box, "add2_x"]; +$value = "3"; +echo call_user_func_array($cb, [&$value, 2]); +echo ":"; +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("call_user_func_array should preserve by-ref array element target"); + + assert_eq!(values.output, "15:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + /// Verifies string and first-class AOT static callables preserve by-reference writeback. #[test] fn execute_program_static_runtime_callables_write_back_by_ref_type_coercion() { diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 78b0c88244..6c682c3d57 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -1318,10 +1318,29 @@ impl Parser { return Ok(EvalExpr::Array(elements)); } loop { + if self.consume(TokenKind::Ampersand) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::Reference(value)); + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + continue; + } let first = self.parse_expr()?; if self.consume(TokenKind::FatArrow) { - let value = self.parse_expr()?; - elements.push(EvalArrayElement::KeyValue { key: first, value }); + if self.consume(TokenKind::Ampersand) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyReference { + key: first, + value, + }); + } else { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyValue { key: first, value }); + } } else { elements.push(EvalArrayElement::Value(first)); } diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 62072ffd60..0534c6f295 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -3529,10 +3529,12 @@ fn eval_call_arg_default_is_supported(arg: &EvalCallArg) -> bool { fn eval_array_element_default_is_supported(element: &EvalArrayElement) -> bool { match element { EvalArrayElement::Value(value) => eval_constant_expression_default_is_supported(value), + EvalArrayElement::Reference(_) => false, EvalArrayElement::KeyValue { key, value } => { eval_constant_expression_default_is_supported(key) && eval_constant_expression_default_is_supported(value) } + EvalArrayElement::KeyReference { .. } => false, } } @@ -3759,10 +3761,12 @@ fn eval_attribute_array_arg_from_elements( .iter() .map(|element| match element { EvalArrayElement::Value(value) => eval_attribute_arg_from_expr(value), + EvalArrayElement::Reference(_) => None, EvalArrayElement::KeyValue { key, value } => { let value = eval_attribute_arg_from_expr(value)?; eval_attribute_array_keyed_arg(key, value) } + EvalArrayElement::KeyReference { .. } => None, }) .collect::>>() .map(EvalAttributeArg::Array) @@ -4134,10 +4138,17 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { match expr { EvalExpr::Array(elements) => elements.iter().any(|element| match element { EvalArrayElement::Value(value) => eval_expr_uses_this_property(value, property_name), + EvalArrayElement::Reference(value) => { + eval_expr_uses_this_property(value, property_name) + } EvalArrayElement::KeyValue { key, value } => { eval_expr_uses_this_property(key, property_name) || eval_expr_uses_this_property(value, property_name) } + EvalArrayElement::KeyReference { key, value } => { + eval_expr_uses_this_property(key, property_name) + || eval_expr_uses_this_property(value, property_name) + } }), EvalExpr::ArrayGet { array, index } => { eval_expr_uses_this_property(array, property_name) diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs index 3abee204ae..fecf94929e 100644 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ b/crates/elephc-magician/src/parser/tests/arrays_objects.rs @@ -57,6 +57,24 @@ fn parse_fragment_accepts_assoc_array_literal_source() { ])))] ); } + +/// Verifies array literals preserve by-reference element syntax. +#[test] +fn parse_fragment_accepts_array_reference_elements_source() { + let program = parse_fragment(br#"return [&$value, "named" => &$other];"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Reference(EvalExpr::LoadVar("value".to_string())), + EvalArrayElement::KeyReference { + key: EvalExpr::Const(EvalConst::String("named".to_string())), + value: EvalExpr::LoadVar("other".to_string()), + }, + ])))] + ); +} + /// Verifies indexed array writes parse as variable-target array set statements. #[test] fn parse_fragment_accepts_indexed_array_write_source() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 46e08ad9a8..ec62caee8d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7313,6 +7313,65 @@ return EvalAotCallableRefDriver::run($bridgeFirst, $bridgeValue, 1) . ":" . gett ); } +/// Verifies eval `call_user_func_array()` preserves AOT callable by-reference writeback. +#[test] +fn test_eval_call_user_func_array_aot_callables_write_back_by_ref_args() { + let out = compile_and_run_capture( + r#"offset = $offset; + } + + public function bump(int &$value): int { + $value = $value + $this->offset; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->offset + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallArrayRefBox(5); +$method = [$box, "bump"]; +$value = "7"; +echo call_user_func_array($method, [&$value]) . ":" . gettype($value) . ":" . $value . "|"; +$string = "EvalAotCallArrayRefBox::add"; +$staticValue = "3"; +echo call_user_func_array($string, [&$staticValue, 4]) . ":" . gettype($staticValue) . ":" . $staticValue . "|"; +$namedValue = "5"; +echo call_user_func_array($string, ["delta" => 2, "value" => &$namedValue]) . ":" . gettype($namedValue) . ":" . $namedValue . "|"; +$first = EvalAotCallArrayRefBox::add(...); +$next = "2"; +echo call_user_func_array($first, [&$next, 6]) . ":" . gettype($next) . ":" . $next . "|"; +$invokable = new EvalAotCallArrayRefBox(10); +$invokableValue = "1"; +echo call_user_func_array($invokable, [&$invokableValue, 2]) . ":" . gettype($invokableValue) . ":" . $invokableValue . "|"; +$invokableFirst = $invokable(...); +$firstValue = "2"; +echo call_user_func_array($invokableFirst, [&$firstValue, 3]) . ":" . gettype($firstValue) . ":" . $firstValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "12:integer:12|7:integer:7|7:integer:7|8:integer:8|13:integer:13|15:integer:15" + ); +} + /// Verifies eval can pass runtime PHP callbacks to generated/AOT callable-typed methods. #[test] fn test_eval_fragment_passes_callable_args_to_aot_methods() { From eb8249342526e928a19d6145021bc6f2e25d7f68 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 22:10:21 +0200 Subject: [PATCH 0946/1208] Cover eval reflection constructor by-ref args --- tests/codegen/eval.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ec62caee8d..2e5e18ea5f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7372,6 +7372,37 @@ echo call_user_func_array($invokableFirst, [&$firstValue, 3]) . ":" . gettype($f ); } +/// Verifies eval `ReflectionClass::newInstanceArgs()` preserves AOT constructor by-reference writeback. +#[test] +fn test_eval_reflection_new_instance_args_aot_constructor_writes_back_by_ref_args() { + let out = compile_and_run_capture( + r#"seen = $value; + } +} + +echo eval('$ref = new ReflectionClass("EvalAotReflectCtorArrayRefBox"); +$value = "3"; +$box = $ref->newInstanceArgs([&$value, 4]); +echo $box->seen . ":" . gettype($value) . ":" . $value . "|"; +$named = "5"; +$box = $ref->newInstanceArgs(["delta" => 2, "value" => &$named]); +echo $box->seen . ":" . gettype($named) . ":" . $named;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "7:integer:7|7:integer:7"); +} + /// Verifies eval can pass runtime PHP callbacks to generated/AOT callable-typed methods. #[test] fn test_eval_fragment_passes_callable_args_to_aot_methods() { From f39d28ad56e0d6913b17eb6176b96f2e23519e56 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 22:38:31 +0200 Subject: [PATCH 0947/1208] Fix eval native by-ref raw slot cleanup --- crates/elephc-magician/src/interpreter/dynamic_functions.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 817d47649f..8f24f65915 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1796,6 +1796,9 @@ fn write_back_native_function_ref_args( } let value = values.raw_string_value(words[0], words[1]); values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } let value = value?; eval_write_direct_ref_target( target, @@ -1817,6 +1820,7 @@ fn write_back_native_function_ref_args( } let value = values.raw_heap_word_value(word); values.release_raw_heap_word(word)?; + values.release_raw_heap_word(*original)?; let value = value?; eval_write_direct_ref_target( target, From f8ab4beb9e80dc3cd695abe59bc75ebbeca45ada Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 23:01:07 +0200 Subject: [PATCH 0948/1208] Cover eval function_exists catalog parity --- tests/codegen/eval_builtin_parity.rs | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index cdd8f6c2b0..f492f7df93 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -8,6 +8,8 @@ //! - Fixtures verify `function_exists()` and namespaced builtin fallback before //! and after eval has introduced dynamic symbols. +use std::fmt::Write; + use crate::support::compile_and_run; /// Verifies AOT builtin lookup stays case-insensitive without eval being present. @@ -71,6 +73,40 @@ echo function_exists("StRlEn") ? "M" : "m";'); assert_eq!(out, "SCM"); } +/// Verifies eval `function_exists()` sees every compiler-catalog builtin name. +#[test] +fn test_eval_function_exists_covers_static_builtin_catalog() { + let mut fragment = String::new(); + for name in elephc::builtin_metadata::php_visible_builtin_names() { + writeln!( + &mut fragment, + "if (!function_exists(\"{name}\")) {{ echo \"{name},\"; }}" + ) + .expect("write eval builtin probe"); + } + fragment.push_str("return \"ok\";"); + + let source = format!(" String { + let mut literal = String::with_capacity(value.len() + 2); + literal.push('\''); + for ch in value.chars() { + match ch { + '\\' => literal.push_str("\\\\"), + '\'' => literal.push_str("\\'"), + _ => literal.push(ch), + } + } + literal.push('\''); + literal +} + /// Verifies namespaced function calls fall back to builtins in AOT and eval code. #[test] fn test_namespaced_calls_fall_back_to_builtin_before_and_after_eval() { From 6f116e121f1fabcd79f4cfa2bd51b6dd908f5161 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 23:13:44 +0200 Subject: [PATCH 0949/1208] Cover eval first-class ref-like builtin callables --- tests/codegen/eval.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 2e5e18ea5f..e42ff868a8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1458,6 +1458,34 @@ echo function_exists("array_push") && function_exists("array_unshift");'); ); } +/// Verifies first-class eval builtin callables preserve ref-like writeback targets. +#[test] +fn test_eval_first_class_ref_like_builtin_callables_write_back_lvalues() { + let out = compile_and_run( + r#"items, "b") . ":" . $box->items[1] . ":"; +$rsort = rsort(...); +echo $rsort(EvalFirstClassRefLikeBuiltinBox::$staticItems) . ":" . EvalFirstClassRefLikeBuiltinBox::$staticItems[0] . EvalFirstClassRefLikeBuiltinBox::$staticItems[1];'); +"#, + ); + assert_eq!(out, "3:2:2:1:1,2,3:1:integer:42:2:b:1:21"); +} + /// Verifies eval `array_splice()` mutates writable lvalue arguments. #[test] fn test_eval_dispatches_array_splice_builtin_call() { From 9c1c741d666ff102e995d358fe8bc959a14b315c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 23:32:40 +0200 Subject: [PATCH 0950/1208] Cover eval callable by-ref property lvalues --- tests/codegen/eval.rs | 57 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e42ff868a8..073d566d8f 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7341,6 +7341,63 @@ return EvalAotCallableRefDriver::run($bridgeFirst, $bridgeValue, 1) . ":" . gett ); } +/// Verifies eval AOT callable by-reference writeback updates property lvalues. +#[test] +fn test_eval_fragment_aot_callables_write_back_by_ref_property_lvalues() { + let out = compile_and_run_capture( + r#"offset = $offset; + } + + public function bump(int &$value): int { + $value = $value + $this->offset; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->offset + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallableRefLvalueBox(4); +$method = [$box, "bump"]; +echo $method($box->value) . ":" . $box->value . "|"; +$name = "value"; +echo $method($box->{$name}) . ":" . $box->value . "|"; +$string = "EvalAotCallableRefLvalueBox::add"; +echo $string(EvalAotCallableRefLvalueBox::$staticValue, 5) . ":" . EvalAotCallableRefLvalueBox::$staticValue . "|"; +$class = "EvalAotCallableRefLvalueBox"; +$staticName = "dynStaticValue"; +echo $string($class::${$staticName}, 6) . ":" . EvalAotCallableRefLvalueBox::$dynStaticValue . "|"; +$first = EvalAotCallableRefLvalueBox::add(...); +echo $first($box->value, 3) . ":" . $box->value . "|"; +$invokable = new EvalAotCallableRefLvalueBox(10); +echo $invokable($box->{$name}, 2) . ":" . $box->value . "|"; +$invokableFirst = $invokable(...); +echo $invokableFirst(EvalAotCallableRefLvalueBox::$staticValue, 1) . ":" . EvalAotCallableRefLvalueBox::$staticValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5:5|9:9|7:7|9:9|12:12|24:24|18:18"); +} + /// Verifies eval `call_user_func_array()` preserves AOT callable by-reference writeback. #[test] fn test_eval_call_user_func_array_aot_callables_write_back_by_ref_args() { From b0ad460dd250308a2cf20fd06795827ba293a78e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 29 Jun 2026 23:51:16 +0200 Subject: [PATCH 0951/1208] Cover eval ref-like builtin call array aliases --- tests/codegen/eval.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 073d566d8f..c5f7cbe301 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1486,6 +1486,33 @@ echo $rsort(EvalFirstClassRefLikeBuiltinBox::$staticItems) . ":" . EvalFirstClas assert_eq!(out, "3:2:2:1:1,2,3:1:integer:42:2:b:1:21"); } +/// Verifies eval `call_user_func_array()` preserves ref-like builtin writeback aliases. +#[test] +fn test_eval_call_user_func_array_ref_like_builtin_callables_write_back_lvalues() { + let out = compile_and_run( + r#"items]) . ":" . implode(",", $box->items) . ":"; +$rsort = rsort(...); +echo call_user_func_array($rsort, [&EvalCallArrayRefLikeBuiltinBox::$staticItems]) . ":" . implode(",", EvalCallArrayRefLikeBuiltinBox::$staticItems) . ":"; +$set = settype(...); +echo call_user_func_array($set, ["var" => &$box->value, "type" => "integer"]) . ":" . gettype($box->value) . ":" . $box->value . ":"; +$string = "settype"; +echo call_user_func_array($string, [&EvalCallArrayRefLikeBuiltinBox::$staticValue, "bool"]) . ":" . gettype(EvalCallArrayRefLikeBuiltinBox::$staticValue) . ":" . (EvalCallArrayRefLikeBuiltinBox::$staticValue ? "true" : "false") . ":"; +$push = array_push(...); +echo call_user_func_array($push, [&$box->items, 4]) . ":" . $box->items[3];'); +"#, + ); + assert_eq!(out, "1:1,2,3:1:2,1:1:integer:42:1:boolean:false:4:4"); +} + /// Verifies eval `array_splice()` mutates writable lvalue arguments. #[test] fn test_eval_dispatches_array_splice_builtin_call() { From 7b968c481d8bd3ba670fa26ab522bb58fcfb1f19 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 00:55:23 +0200 Subject: [PATCH 0952/1208] Handle eval call_user_func by-ref warnings --- .../interpreter/builtins/registry/callable.rs | 101 +++++++++++++- .../src/interpreter/control.rs | 17 ++- .../src/interpreter/dynamic_functions.rs | 126 +++++++++++++++++- crates/elephc-magician/src/interpreter/mod.rs | 2 +- tests/codegen/eval.rs | 57 ++++++++ 5 files changed, 291 insertions(+), 12 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 633e2a557f..daa9e0b218 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -68,7 +68,12 @@ pub(in crate::interpreter) fn eval_call_user_func_with_values( return Err(EvalStatus::RuntimeFatal); }; let callback = eval_call_user_func_callback(*callback, "call_user_func", context, values)?; - eval_evaluated_callable_with_values(&callback, callback_args.to_vec(), context, values) + eval_evaluated_callable_with_call_user_func_values( + &callback, + callback_args.to_vec(), + context, + values, + ) } /// Normalizes a `call_user_func*` callback and maps non-invokable objects to PHP's TypeError. @@ -328,6 +333,100 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( } } +/// Invokes a normalized callback through `call_user_func()` by-value argument semantics. +fn eval_evaluated_callable_with_call_user_func_values( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named(name) => { + eval_named_callable_with_call_user_func_values(name, evaluated_args, context, values) + } + _ => eval_evaluated_callable_with_values(callback, evaluated_args, context, values), + } +} + +/// Invokes a named callable through `call_user_func()` and warns for by-ref parameters. +fn eval_named_callable_with_call_user_func_values( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { + return Ok(result); + } + if let Some(function) = context.function(name).cloned() { + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + function.name(), + function.params(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_dynamic_function_with_evaluated_args_and_ref_flags( + &function, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = positional_args(evaluated_args); + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Builds by-value binding flags for `call_user_func()` and emits PHP by-ref warnings. +fn eval_call_user_func_by_value_ref_flags( + callable_name: &str, + params: &[String], + parameter_is_by_ref: &[bool], + parameter_is_variadic: &[bool], + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let variadic_index = parameter_is_variadic + .iter() + .position(|is_variadic| *is_variadic); + for arg_index in 0..supplied_count { + let param_index = if variadic_index.is_some_and(|index| arg_index >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + arg_index + }; + if !parameter_is_by_ref + .get(param_index) + .copied() + .unwrap_or(false) + { + continue; + } + let param_name = params + .get(param_index) + .map(String::as_str) + .unwrap_or("arg"); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + arg_index + 1 + ))?; + } + Ok(vec![false; params.len()]) +} + /// Invokes an already normalized callback with optional named-argument metadata. pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( callback: &EvaluatedCallable, diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index 40993ff549..7859ad5c9e 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -54,23 +54,32 @@ pub(super) enum BoundNativeFunctionRefSlot { Mixed { original: RuntimeCellHandle, slot: Box, - target: EvalReferenceTarget, + target: Option, }, RawWord { tag: u64, original: u64, slot: Box, - target: EvalReferenceTarget, + target: Option, }, RawString { original: [u64; 2], slot: Box<[u64; 2]>, - target: EvalReferenceTarget, + target: Option, }, OwnedRawWord { original: u64, slot: Box, - target: EvalReferenceTarget, + target: Option, + }, +} + +/// How a callable binder should handle by-reference parameters without caller storage. +#[derive(Clone, Copy)] +pub(super) enum EvalByRefBindingMode<'a> { + RequireTarget, + WarnByValue { + callable_name: &'a str, }, } diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 8f24f65915..6a1c2e6322 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -346,11 +346,46 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + bind_evaluated_native_function_args_with_mode( + function, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Binds native AOT function args for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn bind_evaluated_native_function_args_for_call_user_func( + callable_name: &str, + function: &NativeFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + bind_evaluated_native_function_args_with_mode( + function, + evaluated_args, + EvalByRefBindingMode::WarnByValue { callable_name }, + context, + values, + ) +} + +/// Binds already evaluated native AOT function args using the selected by-reference mode. +fn bind_evaluated_native_function_args_with_mode( + function: &NativeFunction, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { if native_function_variadic_index(function).is_some() { return bind_evaluated_native_variadic_function_args( function, evaluated_args, + by_ref_mode, context, values, ); @@ -371,6 +406,8 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( &name, arg.value, arg.ref_target, + by_ref_mode, + values, )?; } else { bind_native_function_positional_arg( @@ -380,6 +417,8 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( &mut next_positional, arg.value, arg.ref_target, + by_ref_mode, + values, )?; } } @@ -406,13 +445,14 @@ pub(in crate::interpreter) fn bind_evaluated_native_function_args( .collect::>>() .ok_or(EvalStatus::RuntimeFatal)?; apply_native_function_arg_types(function, None, &mut bound_args, context, values)?; - stage_native_function_invoker_args(function, None, bound_args, values) + stage_native_function_invoker_args(function, None, bound_args, by_ref_mode, values) } /// Binds a native AOT variadic function while keeping the raw invoker argument layout. fn bind_evaluated_native_variadic_function_args( function: &NativeFunction, evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -437,6 +477,8 @@ fn bind_evaluated_native_variadic_function_args( &name, arg.value, arg.ref_target, + by_ref_mode, + values, )?; } else if next_positional < variadic_index { bind_native_function_positional_arg( @@ -446,12 +488,16 @@ fn bind_evaluated_native_variadic_function_args( &mut next_positional, arg.value, arg.ref_target, + by_ref_mode, + values, )?; } else { let ref_target = native_function_parameter_ref_target( function, Some(variadic_index), arg.ref_target, + by_ref_mode, + values, )?; variadic_args.push(BoundMethodArg { value: arg.value, @@ -490,7 +536,13 @@ fn bind_evaluated_native_variadic_function_args( context, values, )?; - stage_native_function_invoker_args(function, Some(variadic_index), bound_args, values) + stage_native_function_invoker_args( + function, + Some(variadic_index), + bound_args, + by_ref_mode, + values, + ) } /// Applies registered native AOT function parameter types after argument binding. @@ -523,6 +575,8 @@ fn bind_native_function_named_arg( name: &str, value: RuntimeCellHandle, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let Some(param_index) = native_function_named_param_index(function, variadic_index, name) else { return Err(EvalStatus::RuntimeFatal); @@ -530,7 +584,8 @@ fn bind_native_function_named_arg( if bound_args[param_index].is_some() { return Err(EvalStatus::RuntimeFatal); } - let ref_target = native_function_parameter_ref_target(function, Some(param_index), ref_target)?; + let ref_target = + native_function_parameter_ref_target(function, Some(param_index), ref_target, by_ref_mode, values)?; bound_args[param_index] = Some(BoundMethodArg { value, ref_target, @@ -547,6 +602,8 @@ fn bind_native_function_positional_arg( next_positional: &mut usize, value: RuntimeCellHandle, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { let param_index = *next_positional; if variadic_index.is_some_and(|index| param_index >= index) @@ -555,7 +612,8 @@ fn bind_native_function_positional_arg( { return Err(EvalStatus::RuntimeFatal); } - let ref_target = native_function_parameter_ref_target(function, Some(param_index), ref_target)?; + let ref_target = + native_function_parameter_ref_target(function, Some(param_index), ref_target, by_ref_mode, values)?; bound_args[param_index] = Some(BoundMethodArg { value, ref_target, @@ -570,6 +628,8 @@ fn native_function_parameter_ref_target( function: &NativeFunction, param_index: Option, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(param_index) = param_index else { return Ok(None); @@ -577,7 +637,20 @@ fn native_function_parameter_ref_target( if !function.param_by_ref(param_index) { return Ok(None); } - ref_target.map(Some).ok_or(EvalStatus::RuntimeFatal) + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = native_function_param_warning_name(function, param_index); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + param_index + 1 + ))?; + Ok(None) + } + } } /// Converts bound values into descriptor-invoker arguments, staging by-reference slots. @@ -585,6 +658,7 @@ fn stage_native_function_invoker_args( function: &NativeFunction, variadic_index: Option, bound_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, values: &mut impl RuntimeValueOps, ) -> Result { let mut invoker_values = Vec::with_capacity(bound_args.len()); @@ -599,7 +673,11 @@ fn stage_native_function_invoker_args( invoker_values.push(bound_arg.value); continue; } - let target = bound_arg.ref_target.ok_or(EvalStatus::RuntimeFatal)?; + let target = match (bound_arg.ref_target, by_ref_mode) { + (Some(target), _) => Some(target), + (None, EvalByRefBindingMode::WarnByValue { .. }) => None, + (None, EvalByRefBindingMode::RequireTarget) => return Err(EvalStatus::RuntimeFatal), + }; if let Some(raw_ref_kind) = native_function_raw_ref_kind(function.param_type(param_index)) { match raw_ref_kind { NativeFunctionRawRefKind::Scalar { tag } => { @@ -666,6 +744,16 @@ fn stage_native_function_invoker_args( }) } +/// Returns the PHP parameter name used in by-reference warning diagnostics. +fn native_function_param_warning_name(function: &NativeFunction, param_index: usize) -> String { + function + .param_names() + .get(param_index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", param_index + 1)) +} + /// Describes native function by-reference parameters that can use typed raw slots. enum NativeFunctionRawRefKind { Scalar { tag: u64 }, @@ -1753,6 +1841,9 @@ fn write_back_native_function_ref_args( if value == *original { continue; } + let Some(target) = target else { + continue; + }; let current = eval_reference_target_value(target, context, values)?; if current == value { continue; @@ -1775,6 +1866,9 @@ fn write_back_native_function_ref_args( if word == *original { continue; } + let Some(target) = target else { + continue; + }; let value = values.raw_word_value(*tag, word)?; eval_write_direct_ref_target( target, @@ -1790,6 +1884,16 @@ fn write_back_native_function_ref_args( target, } => { let words = **slot; + if target.is_none() { + values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } + continue; + } + let Some(target) = target else { + return Err(EvalStatus::RuntimeFatal); + }; if words == *original { values.release_raw_string_words(words[0], words[1])?; continue; @@ -1814,6 +1918,16 @@ fn write_back_native_function_ref_args( target, } => { let word = **slot; + if target.is_none() { + values.release_raw_heap_word(word)?; + if word != *original { + values.release_raw_heap_word(*original)?; + } + continue; + } + let Some(target) = target else { + return Err(EvalStatus::RuntimeFatal); + }; if word == *original { values.release_raw_heap_word(word)?; continue; diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 73a814b6fb..78f0498e74 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -57,7 +57,7 @@ use constant_eval::*; use constants::*; pub use control::EvalOutcome; use control::{ - BoundMethodArg, BoundNativeFunctionArgs, BoundNativeFunctionRefSlot, + BoundMethodArg, BoundNativeFunctionArgs, BoundNativeFunctionRefSlot, EvalByRefBindingMode, EvalArraySpliceDirectArgs, EvalControl, EvalPredefinedConstant, EvalSprintfSpec, EvaluatedCallArg, EvaluatedCallable, }; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c5f7cbe301..060a3e7b68 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7484,6 +7484,63 @@ echo call_user_func_array($invokableFirst, [&$firstValue, 3]) . ":" . gettype($f ); } +/// Verifies eval `call_user_func()` warns and passes eval-declared by-ref params by value. +#[test] +fn test_eval_call_user_func_eval_function_by_ref_args_warn_and_use_value_copy() { + let out = compile_and_run_capture( + r#" Date: Tue, 30 Jun 2026 01:36:10 +0200 Subject: [PATCH 0953/1208] Handle eval call_user_func method by-ref semantics --- .../interpreter/builtins/registry/callable.rs | 331 +++++++++++++++++- .../src/interpreter/statements.rs | 261 +++++++++++++- tests/codegen/eval.rs | 115 ++++++ 3 files changed, 692 insertions(+), 15 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index daa9e0b218..c826002e94 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -344,7 +344,68 @@ fn eval_evaluated_callable_with_call_user_func_values( EvaluatedCallable::Named(name) => { eval_named_callable_with_call_user_func_values(name, evaluated_args, context, values) } - _ => eval_evaluated_callable_with_values(callback, evaluated_args, context, values), + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_with_call_user_func_values( + *object, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + *object, + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_object_method_with_call_user_func_values( + *object, + method, + evaluated_args, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_static_method_with_call_user_func_values( + class_name, + method, + called_class.as_deref(), + evaluated_args, + context, + values, + ), + }, } } @@ -390,6 +451,274 @@ fn eval_named_callable_with_call_user_func_values( Err(EvalStatus::UnsupportedConstruct) } +/// Invokes an invokable object through `call_user_func()` by-value argument semantics. +fn eval_invokable_object_with_call_user_func_values( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_object_method_with_call_user_func_values( + object, + "__invoke", + evaluated_args, + context, + values, + ) +} + +/// Invokes an object-method callable through `call_user_func()` by-value semantics. +fn eval_object_method_with_call_user_func_values( + object: RuntimeCellHandle, + method: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = positional_args(evaluated_args); + if let Some(result) = eval_object_method_call_user_func_result( + object, + method, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) +} + +/// Attempts call-user-func by-value dispatch for eval-declared or generated object methods. +fn eval_object_method_call_user_func_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return eval_native_object_method_call_user_func_result( + object, + method_name, + evaluated_args, + context, + values, + ); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return eval_native_object_method_call_user_func_result( + object, + method_name, + evaluated_args, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_method_for_call(&called_class_name, method_name, context) + { + if method.is_static() || method.is_abstract() { + return Ok(None); + } + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + &format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()), + method.params(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_dynamic_method_with_values_and_ref_flags( + &declaring_class, + &called_class_name, + &method, + object, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ) + .map(Some); + } + let Some(parent) = context.class_native_parent_name(&called_class_name) else { + return Ok(None); + }; + eval_native_object_method_call_user_func_result_for_class( + object, + &parent, + method_name, + Some(&called_class_name), + evaluated_args, + context, + values, + ) +} + +/// Attempts call-user-func by-value dispatch for a generated/AOT object method. +fn eval_native_object_method_call_user_func_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = runtime_object_class_name(object, values)?; + eval_native_object_method_call_user_func_result_for_class( + object, + &class_name, + method_name, + Some(&class_name), + evaluated_args, + context, + values, + ) +} + +/// Attempts generated/AOT object-method dispatch for one resolved receiver class. +fn eval_native_object_method_call_user_func_result_for_class( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + called_class_scope: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + called_class_scope, + context, + values, + ) + .map(Some) +} + +/// Invokes a static-method callable through `call_user_func()` by-value semantics. +fn eval_static_method_with_call_user_func_values( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = positional_args(evaluated_args); + if let Some(result) = eval_static_method_call_user_func_result( + class_name, + method_name, + called_class, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method_name, + evaluated_args, + context, + values, + ), + None => eval_static_method_call_result( + class_name, + method_name, + evaluated_args, + context, + values, + ), + } +} + +/// Attempts call-user-func by-value dispatch for eval-declared or generated static methods. +fn eval_static_method_call_user_func_result( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let dispatch_class = resolve_eval_static_member_class_name(class_name, context)?; + let called_class = called_class.unwrap_or(&dispatch_class).to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_static_method_for_call(&dispatch_class, method_name, context) + { + if !method.is_static() || method.is_abstract() { + return Ok(None); + } + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + &format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()), + method.params(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_dynamic_static_method_with_values_and_ref_flags( + &declaring_class, + &called_class, + &method, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ) + .map(Some); + } + let native_class = if context.has_class(&dispatch_class) { + let Some(parent) = context.class_native_parent_name(&dispatch_class) else { + return Ok(None); + }; + parent + } else if context.has_interface(&dispatch_class) + || context.has_trait(&dispatch_class) + || context.has_enum(&dispatch_class) + { + return Ok(None); + } else { + dispatch_class.clone() + }; + let Some((declaring_class, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + method_name, + context, + values, + )? + else { + return Ok(None); + }; + if !is_static || is_abstract { + return Ok(None); + } + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + &native_class, + method_name, + evaluated_args, + Some(&declaring_class), + Some(&called_class), + context, + values, + ) + .map(Some) +} + /// Builds by-value binding flags for `call_user_func()` and emits PHP by-ref warnings. fn eval_call_user_func_by_value_ref_flags( callable_name: &str, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 766fada21f..4dd4b3e86a 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7827,7 +7827,7 @@ fn eval_throw_reflection_instantiation_error( } /// Resolves a static method using private-method scope rules. -fn eval_dynamic_static_method_for_call( +pub(in crate::interpreter) fn eval_dynamic_static_method_for_call( class_name: &str, method_name: &str, context: &ElephcEvalContext, @@ -10005,17 +10005,34 @@ pub(in crate::interpreter) fn bind_native_callable_bound_args( args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + bind_native_callable_bound_args_with_mode( + signature, + args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Binds native AOT callable args using the selected by-reference degradation mode. +fn bind_native_callable_bound_args_with_mode( + signature: Option, + args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(signature) = signature else { - return positional_evaluated_bound_args(None, args, context, values); + return positional_evaluated_bound_args(None, args, by_ref_mode, context, values); }; if !signature.bridge_supported() { return Err(EvalStatus::RuntimeFatal); } if signature.param_names().len() == signature.param_count() { - bind_native_signature_args(&signature, args, context, values) + bind_native_signature_args(&signature, args, by_ref_mode, context, values) } else { - positional_evaluated_bound_args(Some(&signature), args, context, values) + positional_evaluated_bound_args(Some(&signature), args, by_ref_mode, context, values) } } @@ -10023,6 +10040,7 @@ pub(in crate::interpreter) fn bind_native_callable_bound_args( fn positional_evaluated_bound_args( signature: Option<&NativeCallableSignature>, args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { @@ -10033,10 +10051,15 @@ fn positional_evaluated_bound_args( .into_iter() .enumerate() .map(|(index, arg)| { - let ref_target = if signature.is_some_and(|signature| signature.param_by_ref(index)) { - Some(arg.ref_target.ok_or(EvalStatus::RuntimeFatal)?) - } else { - None + let ref_target = match signature { + Some(signature) => native_parameter_ref_target( + signature, + Some(index), + arg.ref_target, + by_ref_mode, + values, + )?, + None => None, }; Ok(BoundMethodArg { value: arg.value, @@ -10047,6 +10070,12 @@ fn positional_evaluated_bound_args( .collect::, _>>()?; if let Some(signature) = signature { apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; + copy_native_call_user_func_by_value_ref_args( + signature, + &mut bound_args, + by_ref_mode, + values, + )?; } Ok(bound_args) } @@ -10080,6 +10109,7 @@ pub(in crate::interpreter) fn write_back_native_callable_ref_args( fn bind_native_signature_args( signature: &NativeCallableSignature, args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { @@ -10106,6 +10136,8 @@ fn bind_native_signature_args( &name, arg.value, arg.ref_target, + by_ref_mode, + values, )?; } else { bind_native_positional_signature_arg( @@ -10116,6 +10148,7 @@ fn bind_native_signature_args( &mut next_variadic_index, arg.value, arg.ref_target, + by_ref_mode, values, )?; } @@ -10146,6 +10179,12 @@ fn bind_native_signature_args( .collect::>>() .ok_or(EvalStatus::RuntimeFatal)?; apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; + copy_native_call_user_func_by_value_ref_args( + signature, + &mut bound_args, + by_ref_mode, + values, + )?; Ok(bound_args) } @@ -10187,6 +10226,57 @@ fn apply_native_callable_variadic_arg_type( Ok(()) } +/// Copies by-value degraded by-ref native method args before the generated bridge mutates them. +fn copy_native_call_user_func_by_value_ref_args( + signature: &NativeCallableSignature, + bound_args: &mut [BoundMethodArg], + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !matches!(by_ref_mode, EvalByRefBindingMode::WarnByValue { .. }) { + return Ok(()); + } + let variadic_index = native_callable_variadic_index(signature); + for (position, bound_arg) in bound_args.iter_mut().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + if !signature.param_by_ref(param_index) || bound_arg.ref_target.is_some() { + continue; + } + bound_arg.value = copy_native_call_user_func_by_value_ref_arg(bound_arg.value, values)?; + } + Ok(()) +} + +/// Allocates a temporary runtime cell for one by-value degraded by-ref native method arg. +fn copy_native_call_user_func_by_value_ref_arg( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + match tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = values.raw_value_word(value)?; + values.raw_word_value(tag, word) + } + EVAL_TAG_STRING => { + let bytes = values.string_bytes(value)?; + values.string_bytes_value(&bytes) + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => values.array_clone_shallow(value), + EVAL_TAG_OBJECT => { + let word = values.raw_value_word(value)?; + let retained = values.retain_raw_heap_word(word)?; + values.raw_heap_word_value(retained) + } + EVAL_TAG_NULL => values.null(), + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Returns the native callable variadic slot, if metadata registered one. fn native_callable_variadic_index(signature: &NativeCallableSignature) -> Option { (0..signature.param_count()).find(|index| signature.param_variadic(*index)) @@ -10201,6 +10291,7 @@ fn bind_native_positional_signature_arg( next_variadic_index: &mut i64, value: RuntimeCellHandle, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if variadic_index.is_some_and(|index| *next_positional >= index) { @@ -10208,14 +10299,16 @@ fn bind_native_positional_signature_arg( *next_variadic_index = next_variadic_index .checked_add(1) .ok_or(EvalStatus::RuntimeFatal)?; - let ref_target = native_parameter_ref_target(signature, variadic_index, ref_target)?; + let ref_target = + native_parameter_ref_target(signature, variadic_index, ref_target, by_ref_mode, values)?; return bind_native_variadic_arg(bound_args, variadic_index, key, value, ref_target, values); } let param_index = *next_positional; if param_index >= bound_args.len() || bound_args[param_index].is_some() { return Err(EvalStatus::RuntimeFatal); } - let ref_target = native_parameter_ref_target(signature, Some(param_index), ref_target)?; + let ref_target = + native_parameter_ref_target(signature, Some(param_index), ref_target, by_ref_mode, values)?; bound_args[param_index] = Some(BoundMethodArg { value, ref_target, @@ -10233,12 +10326,20 @@ fn bind_native_named_signature_arg( name: &str, value: RuntimeCellHandle, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if let Some(param_index) = native_regular_param_index(signature, variadic_index, name) { if bound_args[param_index].is_some() { return Err(EvalStatus::RuntimeFatal); } - let ref_target = native_parameter_ref_target(signature, Some(param_index), ref_target)?; + let ref_target = native_parameter_ref_target( + signature, + Some(param_index), + ref_target, + by_ref_mode, + values, + )?; bound_args[param_index] = Some(BoundMethodArg { value, ref_target, @@ -10254,6 +10355,8 @@ fn native_parameter_ref_target( signature: &NativeCallableSignature, param_index: Option, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(param_index) = param_index else { return Ok(None); @@ -10261,7 +10364,33 @@ fn native_parameter_ref_target( if !signature.param_by_ref(param_index) { return Ok(None); } - ref_target.map(Some).ok_or(EvalStatus::RuntimeFatal) + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = native_callable_param_warning_name(signature, param_index); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + param_index + 1 + ))?; + Ok(None) + } + } +} + +/// Returns the PHP parameter name used in native method by-reference warnings. +fn native_callable_param_warning_name( + signature: &NativeCallableSignature, + param_index: usize, +) -> String { + signature + .param_names() + .get(param_index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", param_index + 1)) } /// Returns the matching non-variadic native parameter index for one named arg. @@ -10409,11 +10538,65 @@ pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_b called_class_scope: Option<&str>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object, + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Calls one generated/AOT instance method for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let callable_name = format!("{}::{}", signature_owner.trim_start_matches('\\'), method_name); + eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object, + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) +} + +/// Calls one generated/AOT instance method with a selected by-reference binding mode. +fn eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let signature_owner = bridge_scope.unwrap_or(class_name); let signature = context.native_method_signature(signature_owner, method_name); let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); - let bound_args = bind_native_callable_bound_args(signature, evaluated_args, context, values)?; + let bound_args = + bind_native_callable_bound_args_with_mode(signature, evaluated_args, by_ref_mode, context, values)?; let result = if let Some(scope) = bridge_scope { eval_native_method_call_with_scope( scope, @@ -10546,11 +10729,61 @@ pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unch called_class_scope: Option<&str>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Calls one generated/AOT static method for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let callable_name = format!("{}::{}", signature_owner.trim_start_matches('\\'), method_name); + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) +} + +/// Calls one generated/AOT static method with a selected by-reference binding mode. +fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let signature_owner = bridge_scope.unwrap_or(class_name); let signature = context.native_static_method_signature(signature_owner, method_name); let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); - let bound_args = bind_native_callable_bound_args(signature, evaluated_args, context, values)?; + let bound_args = + bind_native_callable_bound_args_with_mode(signature, evaluated_args, by_ref_mode, context, values)?; let result = if let Some(scope) = bridge_scope { eval_native_static_method_call_with_scope( scope, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 060a3e7b68..5be1eae472 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7541,6 +7541,121 @@ echo call_user_func("eval_aot_cuf_ref_int", $value) . ":" . $value;'); ); } +/// Verifies eval `call_user_func()` degrades eval-declared method by-ref params to by-value. +#[test] +fn test_eval_call_user_func_eval_method_by_ref_args_warn_and_use_value_copy() { + let out = compile_and_run_capture( + r#"append(...); +$firstValue = "b"; +echo call_user_func($first, $firstValue) . ":" . $firstValue . "|"; +$staticFirst = EvalCufMethodRefBox::add(...); +$staticValue = 4; +echo call_user_func($staticFirst, $staticValue) . ":" . $staticValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "ax:a|5:3|qi:q|bx:b|6:4"); + for warning in [ + "EvalCufMethodRefBox::append(): Argument #1 ($value) must be passed by reference, value given", + "EvalCufMethodRefBox::add(): Argument #1 ($value) must be passed by reference, value given", + "EvalCufMethodRefBox::__invoke(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + +/// Verifies eval `call_user_func()` degrades AOT method by-ref params to by-value. +#[test] +fn test_eval_call_user_func_aot_method_by_ref_args_warn_and_use_value_copy() { + let out = compile_and_run_capture( + r#"bump(...); +$firstValue = 5; +echo call_user_func($first, $firstValue) . ":" . $firstValue . "|"; +$staticFirst = EvalAotCufMethodRefBox::add(...); +$staticFirstValue = 9; +echo call_user_func($staticFirst, $staticFirstValue, 1) . ":" . $staticFirstValue . "|"; +$invokable = new EvalAotCufMethodRefBox(); +$invokableValue = 6; +echo call_user_func($invokable, $invokableValue, 4) . ":" . $invokableValue;'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "5:3|7:4|10:8|7:5|10:9|10:6"); + for warning in [ + "EvalAotCufMethodRefBox::bump(): Argument #1 ($value) must be passed by reference, value given", + "EvalAotCufMethodRefBox::add(): Argument #1 ($value) must be passed by reference, value given", + "EvalAotCufMethodRefBox::__invoke(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + /// Verifies eval `ReflectionClass::newInstanceArgs()` preserves AOT constructor by-reference writeback. #[test] fn test_eval_reflection_new_instance_args_aot_constructor_writes_back_by_ref_args() { From 099545e620049fe2ff8bb24f1899eedda2b51c53 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 01:57:50 +0200 Subject: [PATCH 0954/1208] Handle eval call_user_func builtin by-ref warnings --- .../builtins/registry/dispatch/filesystem.rs | 57 +++++++++++++- .../builtins/registry/dispatch/regex.rs | 36 +++++++++ tests/codegen/eval.rs | 74 +++++++++++++++++++ 3 files changed, 166 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index a350d01b4f..08b46c8d58 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -167,6 +167,11 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( if !(2..=3).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); } + if evaluated_args.len() >= 3 { + values.warning( + "flock(): Argument #3 ($would_block) must be passed by reference, value given", + )?; + } let (success, _) = eval_flock_result( evaluated_args[0], evaluated_args[1], @@ -185,6 +190,7 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( if !(2..=5).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); } + eval_fsockopen_by_value_ref_warnings(name, evaluated_args.len(), values)?; eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values)? } "fscanf" => { @@ -426,7 +432,10 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_stream_bucket_push_result(name, *brigade, *bucket, values)? } - "stream_select" => eval_stream_select_result(evaluated_args, context, values)?, + "stream_select" => { + eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; + eval_stream_select_result(evaluated_args, context, values)? + } "stream_socket_server" => { let [address] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -443,6 +452,11 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( if !(1..=3).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); } + if evaluated_args.len() >= 3 { + values.warning( + "stream_socket_accept(): Argument #3 ($peer_name) must be passed by reference, value given", + )?; + } eval_stream_socket_accept_result(evaluated_args[0], context, values)? } "stream_socket_get_name" => { @@ -478,6 +492,11 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( if !(2..=4).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); } + if evaluated_args.len() >= 4 { + values.warning( + "stream_socket_recvfrom(): Argument #4 ($address) must be passed by reference, value given", + )?; + } eval_stream_socket_recvfrom_result( evaluated_args[0], evaluated_args[1], @@ -612,3 +631,39 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; Ok(Some(result)) } + +/// Emits PHP by-reference warnings for by-value `fsockopen()` error outputs. +fn eval_fsockopen_by_value_ref_warnings( + name: &str, + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if supplied_count >= 3 { + values.warning(&format!( + "{name}(): Argument #3 ($error_code) must be passed by reference, value given" + ))?; + } + if supplied_count >= 4 { + values.warning(&format!( + "{name}(): Argument #4 ($error_message) must be passed by reference, value given" + ))?; + } + Ok(()) +} + +/// Emits PHP by-reference warnings for by-value `stream_select()` array outputs. +fn eval_stream_select_by_value_ref_warnings( + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (index, param_name) in ["read", "write", "except"].iter().enumerate() { + if supplied_count <= index { + continue; + } + values.warning(&format!( + "stream_select(): Argument #{} (${param_name}) must be passed by reference, value given", + index + 1 + ))?; + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs index c184358ea7..c1217bdaad 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs @@ -21,10 +21,46 @@ pub(in crate::interpreter) fn eval_regex_builtin_with_values( let result = match name { "preg_match" => match evaluated_args { [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, + [pattern, subject, _matches] => { + values.warning( + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (matched, matches) = + eval_preg_match_capture_result(*pattern, *subject, None, values)?; + values.release(matches)?; + matched + } + [pattern, subject, _matches, flags] => { + values.warning( + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (matched, matches) = + eval_preg_match_capture_result(*pattern, *subject, Some(*flags), values)?; + values.release(matches)?; + matched + } _ => return Err(EvalStatus::RuntimeFatal), }, "preg_match_all" => match evaluated_args { [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, + [pattern, subject, _matches] => { + values.warning( + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (count, matches) = + eval_preg_match_all_capture_result(*pattern, *subject, None, values)?; + values.release(matches)?; + count + } + [pattern, subject, _matches, flags] => { + values.warning( + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (count, matches) = + eval_preg_match_all_capture_result(*pattern, *subject, Some(*flags), values)?; + values.release(matches)?; + count + } _ => return Err(EvalStatus::RuntimeFatal), }, "preg_replace" => match evaluated_args { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5be1eae472..967eb85567 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3083,6 +3083,39 @@ echo preg_match(pattern: "/x/", subject: "x", flags: PREG_OFFSET_CAPTURE);'); assert_eq!(out, "1:id42:id:42:2:b2:2:1"); } +/// Verifies eval `call_user_func*()` warns for by-value regex `$matches` outputs. +#[test] +fn test_eval_call_user_func_regex_ref_like_builtin_args_warn_and_use_value_copy() { + let out = compile_and_run_capture( + r#" "/y/", "subject" => "y", "matches" => $named]) . ":" . $named[0] . "|"; +$flagged = ["old"]; +echo call_user_func("preg_match_all", "/([a-z])/", "ab", $flagged, PREG_SET_ORDER) . ":" . $flagged[0];'); +"#, + ); + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "1:old|2:old|1:old|2:old"); + for warning in [ + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + /// Verifies eval `fnmatch()` supports wildcards, classes, flags, constants, and callables. #[test] fn test_eval_dispatches_fnmatch_builtin_call() { @@ -3188,6 +3221,47 @@ echo unlink("eval-lock.txt") ? "cleanup" : "bad";'); assert_eq!(out, "dynlock:dyn0:fcclock:fcc0:cleanup"); } +/// Verifies eval `call_user_func()` warns for by-value filesystem ref-like outputs. +#[test] +fn test_eval_call_user_func_filesystem_ref_like_builtin_args_warn_and_use_value_copy() { + let out = compile_and_run_capture( + r#" Date: Tue, 30 Jun 2026 02:24:37 +0200 Subject: [PATCH 0955/1208] Clean native eval ref slot setup failures --- .../src/interpreter/dynamic_functions.rs | 67 +++++++++++++++++-- .../src/interpreter/tests/native_scope.rs | 34 ++++++++++ .../interpreter/tests/support/array_ops.rs | 5 ++ .../src/interpreter/tests/support/mod.rs | 8 +++ 4 files changed, 108 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 6a1c2e6322..b666c35790 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1810,13 +1810,18 @@ pub(super) fn eval_native_function_with_values( return Err(EvalStatus::RuntimeFatal); } } - let arg_array = values.array_new(bound_args.values.len())?; - for (index, value) in bound_args.values.iter().copied().enumerate() { - let index = values.int(index as i64)?; - let _ = values.array_set(arg_array, index, value)?; - } + let arg_array = match build_native_function_arg_array(&bound_args, values) { + Ok(arg_array) => arg_array, + Err(status) => { + cleanup_native_function_ref_args(&bound_args, values)?; + return Err(status); + } + }; let result = unsafe { function.call(arg_array) }; - values.release(arg_array)?; + if let Err(status) = values.release(arg_array) { + cleanup_native_function_ref_args(&bound_args, values)?; + return Err(status); + } write_back_native_function_ref_args(&bound_args, context, values)?; if result.is_null() { return Err(EvalStatus::RuntimeFatal); @@ -1824,6 +1829,56 @@ pub(super) fn eval_native_function_with_values( eval_declared_native_return_value(function.return_type(), None, None, result, context, values) } +/// Builds the positional runtime array passed to descriptor-compatible native invokers. +fn build_native_function_arg_array( + bound_args: &BoundNativeFunctionArgs, + values: &mut impl RuntimeValueOps, +) -> Result { + let arg_array = values.array_new(bound_args.values.len())?; + for (index, value) in bound_args.values.iter().copied().enumerate() { + let index = match values.int(index as i64) { + Ok(index) => index, + Err(status) => { + values.release(arg_array)?; + return Err(status); + } + }; + if let Err(status) = values.array_set(arg_array, index, value) { + values.release(arg_array)?; + return Err(status); + } + } + Ok(arg_array) +} + +/// Releases retained raw native-function by-reference staging slots without writeback. +fn cleanup_native_function_ref_args( + bound_args: &BoundNativeFunctionArgs, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for ref_slot in &bound_args.ref_slots { + match ref_slot { + BoundNativeFunctionRefSlot::RawString { original, slot, .. } => { + let words = **slot; + values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } + } + BoundNativeFunctionRefSlot::OwnedRawWord { original, slot, .. } => { + let word = **slot; + values.release_raw_heap_word(word)?; + if word != *original { + values.release_raw_heap_word(*original)?; + } + } + BoundNativeFunctionRefSlot::Mixed { .. } + | BoundNativeFunctionRefSlot::RawWord { .. } => {} + } + } + Ok(()) +} + /// Writes changed staged native-function by-reference slots back to eval caller targets. fn write_back_native_function_ref_args( bound_args: &BoundNativeFunctionArgs, diff --git a/crates/elephc-magician/src/interpreter/tests/native_scope.rs b/crates/elephc-magician/src/interpreter/tests/native_scope.rs index 977b2996e2..234017a39a 100644 --- a/crates/elephc-magician/src/interpreter/tests/native_scope.rs +++ b/crates/elephc-magician/src/interpreter/tests/native_scope.rs @@ -52,6 +52,40 @@ fn execute_program_checks_registered_native_function_return_type() { assert_eq!(err, EvalStatus::RuntimeFatal); } +/// Verifies raw native by-reference staging is released when invoker argument setup fails. +#[test] +fn execute_program_cleans_native_raw_ref_slots_when_arg_array_build_fails() { + let program = parse_fragment(br#"$value = "keep"; native_string_ref($value);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 1); + assert!(native.set_param_name(0, "value")); + assert!(native.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )); + assert!(native.set_param_by_ref(0, true)); + assert!(context + .define_native_function("native_string_ref", native) + .is_ok()); + values.fail_array_set_call(0); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("argument-array build failure should abort native dispatch"); + let value = scope + .entry("value") + .expect("scope should retain the original string") + .cell(); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(value), FakeValue::String("keep".to_string())); + assert!(values.releases.iter().any(|release| *release == value)); +} + /// Verifies direct eval calls can bind registered native parameters by name. #[test] fn execute_program_calls_registered_native_function_with_named_args() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs index 405e813bf3..f479b57b86 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/array_ops.rs @@ -116,6 +116,11 @@ impl FakeOps { index: RuntimeCellHandle, value: RuntimeCellHandle, ) -> Result { + let call_index = self.array_set_calls; + self.array_set_calls += 1; + if self.fail_array_set_call == Some(call_index) { + return Err(EvalStatus::UnsupportedConstruct); + } let key = self.key(index)?; let id = array.as_ptr() as usize; let Some(slot) = self.values.get_mut(&id) else { diff --git a/crates/elephc-magician/src/interpreter/tests/support/mod.rs b/crates/elephc-magician/src/interpreter/tests/support/mod.rs index 1daee7982b..799f2a0eb1 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/mod.rs @@ -58,6 +58,8 @@ pub(super) struct FakeOps { pub(super) output: String, pub(super) releases: Vec, pub(super) warnings: Vec, + pub(super) fail_array_set_call: Option, + pub(super) array_set_calls: usize, } impl FakeOps { @@ -115,6 +117,12 @@ impl FakeOps { .iter() .find_map(|(property, value)| (property == name).then_some(*value)) } + + /// Configures one fake array-set call to fail for cleanup-path tests. + pub(super) fn fail_array_set_call(&mut self, call_index: usize) { + self.fail_array_set_call = Some(call_index); + self.array_set_calls = 0; + } } /// Test native invoker that returns the descriptor pointer as a runtime cell. From d6a081a28ae3912a6b9bfbfffaf744965b42d720 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 02:46:35 +0200 Subject: [PATCH 0956/1208] Align eval preg callback callables --- .../src/interpreter/builtins/regex/replace.rs | 9 +++--- .../interpreter/builtins/registry/callable.rs | 14 --------- .../tests/builtins_strings_encoding.rs | 29 +++++++++++++++++++ tests/codegen/eval.rs | 24 +++++++++++++++ 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs b/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs index a5dbbe89db..e429fad8d0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs @@ -5,8 +5,8 @@ //! - `crate::interpreter::builtins::regex` re-exports. //! //! Key details: -//! - Callback replacement evaluates through the persistent eval context and casts -//! callback results with runtime string coercion. +//! - Callback replacement evaluates general eval callables and casts callback +//! results with runtime string coercion. use super::super::super::*; use super::super::*; @@ -77,7 +77,7 @@ pub(in crate::interpreter) fn eval_preg_replace_callback_result( values: &mut impl RuntimeValueOps, ) -> Result { let regex = eval_preg_regex(pattern, values)?; - let callback = eval_callable_name(callback, values)?; + let callback = eval_callable(callback, context, values)?; let subject = values.string_bytes(subject)?; let mut result = Vec::with_capacity(subject.len()); let mut cursor = 0; @@ -87,7 +87,8 @@ pub(in crate::interpreter) fn eval_preg_replace_callback_result( }; result.extend_from_slice(&subject[cursor..matched.start()]); let matches = eval_preg_capture_array(&subject, Some(&captures), false, false, values)?; - let callback_result = eval_callable_with_values(&callback, vec![matches], context, values)?; + let callback_result = + eval_evaluated_callable_with_values(&callback, vec![matches], context, values)?; let callback_result = values.cast_string(callback_result)?; let callback_bytes = values.string_bytes(callback_result)?; result.extend_from_slice(&callback_bytes); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index c826002e94..aee81fad64 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -242,20 +242,6 @@ pub(in crate::interpreter) fn eval_string_callable( )) } -/// Normalizes one string function callback name for builtin callback positions. -pub(in crate::interpreter) fn eval_callable_name( - callback: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = values.string_bytes(callback)?; - let callback = String::from_utf8(callback).map_err(|_| EvalStatus::RuntimeFatal)?; - let callback = callback.trim_start_matches('\\').to_ascii_lowercase(); - if callback.contains("::") { - return Err(EvalStatus::UnsupportedConstruct); - } - Ok(callback) -} - /// Invokes an already normalized callback with source-order positional values. pub(in crate::interpreter) fn eval_evaluated_callable_with_values( callback: &EvaluatedCallable, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs index 2ff5103dc6..9debe6556e 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs @@ -177,6 +177,35 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies `preg_replace_callback()` accepts the same callable forms as other callback builtins. +#[test] +fn execute_program_preg_replace_callback_accepts_general_callables() { + let program = parse_fragment( + br#"class EvalPregCallbackBox { + public $prefix = ""; + public function __construct($prefix) { $this->prefix = $prefix; } + public function wrap($matches) { return $this->prefix . $matches[0]; } + public static function wrapStatic($matches) { return "S" . $matches[0]; } +} +$box = new EvalPregCallbackBox("O"); +echo preg_replace_callback("/[A-Z]/", [$box, "wrap"], "AB") . ":"; +echo preg_replace_callback("/[0-9]/", "EvalPregCallbackBox::wrapStatic", "12") . ":"; +echo preg_replace_callback("/[C]/", ["EvalPregCallbackBox", "wrapStatic"], "CC") . ":"; +$first = $box->wrap(...); +echo preg_replace_callback("/[a-z]/", $first, "xy") . ":"; +$static = EvalPregCallbackBox::wrapStatic(...); +return preg_replace_callback("/[m]/", $static, "mm");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "OAOB:S1S2:SCSC:OxOy:"); + assert_eq!(values.get(result), FakeValue::String("SmSm".to_string())); +} + /// Verifies dynamic preg callables write by-reference `$matches` targets. #[test] fn execute_program_dispatches_dynamic_preg_match_ref_targets() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 967eb85567..5abb38be5c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -3047,6 +3047,30 @@ echo function_exists("preg_match") && function_exists("preg_match_all") && funct ); } +/// Verifies eval `preg_replace_callback()` accepts general callable forms. +#[test] +fn test_eval_preg_replace_callback_accepts_general_callables() { + let out = compile_and_run( + r#"prefix = $prefix; } + public function wrap($matches) { return $this->prefix . $matches[0]; } + public static function wrapStatic($matches) { return "S" . $matches[0]; } +} +$box = new EvalPregCallbackBox("O"); +echo preg_replace_callback("/[A-Z]/", [$box, "wrap"], "AB") . ":"; +echo preg_replace_callback("/[0-9]/", "EvalPregCallbackBox::wrapStatic", "12") . ":"; +echo preg_replace_callback("/[C]/", ["EvalPregCallbackBox", "wrapStatic"], "CC") . ":"; +$first = $box->wrap(...); +echo preg_replace_callback("/[a-z]/", $first, "xy") . ":"; +$static = EvalPregCallbackBox::wrapStatic(...); +return preg_replace_callback("/[m]/", $static, "mm");'); +"#, + ); + assert_eq!(out, "OAOB:S1S2:SCSC:OxOy:SmSm"); +} + /// Verifies dynamic eval preg callables write by-reference `$matches` arrays. #[test] fn test_eval_dynamic_preg_callables_write_matches_by_ref() { From 9d218a3cff50fae3c5704340f2b19f2df1a3714f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 03:00:30 +0200 Subject: [PATCH 0957/1208] Cover native eval raw ref cleanup paths --- .../src/interpreter/tests/native_scope.rs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/tests/native_scope.rs b/crates/elephc-magician/src/interpreter/tests/native_scope.rs index 234017a39a..cab78a9071 100644 --- a/crates/elephc-magician/src/interpreter/tests/native_scope.rs +++ b/crates/elephc-magician/src/interpreter/tests/native_scope.rs @@ -86,6 +86,82 @@ fn execute_program_cleans_native_raw_ref_slots_when_arg_array_build_fails() { assert!(values.releases.iter().any(|release| *release == value)); } +/// Verifies raw native by-reference staging is released after a successful invoker call. +#[test] +fn execute_program_cleans_native_raw_ref_slots_after_normal_return() { + let program = parse_fragment(br#"$value = "keep"; return native_string_ref($value);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 1); + assert!(native.set_param_name(0, "value")); + assert!(native.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )); + assert!(native.set_param_by_ref(0, true)); + assert!(context + .define_native_function("native_string_ref", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("native dispatch should return normally"); + let value = scope + .entry("value") + .expect("scope should retain the original string") + .cell(); + + assert_eq!(result, expected); + assert_eq!(values.get(value), FakeValue::String("keep".to_string())); + assert!(values.releases.iter().any(|release| *release == value)); +} + +/// Verifies raw native by-reference staging is released when the invoker signals fatal. +#[test] +fn execute_program_cleans_native_raw_ref_slots_after_null_invoker_return() { + let program = parse_fragment(br#"$value = "keep"; native_string_ref($value);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let mut native = NativeFunction::new( + std::ptr::null_mut(), + fake_native_null_descriptor, + 1, + ); + assert!(native.set_param_name(0, "value")); + assert!(native.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )); + assert!(native.set_param_by_ref(0, true)); + assert!(context + .define_native_function("native_string_ref", native) + .is_ok()); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("null invoker result should become a runtime fatal"); + let value = scope + .entry("value") + .expect("scope should retain the original string") + .cell(); + + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.get(value), FakeValue::String("keep".to_string())); + assert!(values.releases.iter().any(|release| *release == value)); +} + +/// Test native invoker that returns NULL to model a bridge-level fatal result. +unsafe extern "C" fn fake_native_null_descriptor( + _descriptor: *mut std::ffi::c_void, + _args: *mut crate::value::RuntimeCell, +) -> *mut crate::value::RuntimeCell { + std::ptr::null_mut() +} + /// Verifies direct eval calls can bind registered native parameters by name. #[test] fn execute_program_calls_registered_native_function_with_named_args() { From f6947a081f000fb0bd035c75dc35ce6eb041cb45 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 03:10:51 +0200 Subject: [PATCH 0958/1208] Guard eval builtin dispatcher parity --- tests/builtin_parity_tests.rs | 119 ++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 4365b9f2f9..813d07f087 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -11,6 +11,47 @@ use std::collections::BTreeSet; +const EVAL_DIRECT_DISPATCH_SOURCES: &[&str] = &[include_str!( + "../crates/elephc-magician/src/interpreter/expressions.rs" +)]; + +const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ + include_str!("../crates/elephc-magician/src/interpreter/builtins/raw_memory.rs"), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/json.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs" + ), + include_str!( + "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs" + ), +]; + /// Eval-only reflection probes exist because magician can inspect dynamic eval metadata before the AOT catalog exposes them. const EVAL_ONLY_REFLECTION_BUILTINS: &[&str] = &[ "get_called_class", @@ -49,6 +90,84 @@ fn static_php_visible_builtins_are_visible_to_eval() { ); } +/// Verifies every static builtin appears in eval's direct and dynamic dispatch sources. +#[test] +fn static_php_visible_builtins_have_eval_dispatch_literals() { + let direct_dispatch_names = php_symbol_string_literals(EVAL_DIRECT_DISPATCH_SOURCES); + let dynamic_dispatch_names = php_symbol_string_literals(EVAL_DYNAMIC_DISPATCH_SOURCES); + + let missing_direct = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .filter(|name| !direct_dispatch_names.contains(*name)) + .collect::>(); + let missing_dynamic = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .filter(|name| !dynamic_dispatch_names.contains(*name)) + .collect::>(); + + assert!( + missing_direct.is_empty(), + "static builtins missing from eval direct dispatcher literals: {missing_direct:?}" + ); + assert!( + missing_dynamic.is_empty(), + "static builtins missing from eval dynamic dispatcher literals: {missing_dynamic:?}" + ); +} + +/// Extracts lowercase PHP-symbol string literals from Rust source snippets. +fn php_symbol_string_literals(sources: &[&str]) -> BTreeSet { + let mut literals = BTreeSet::new(); + for source in sources { + collect_php_symbol_string_literals(source, &mut literals); + } + literals +} + +/// Adds simple double-quoted PHP-symbol literals from one Rust source string. +fn collect_php_symbol_string_literals(source: &str, literals: &mut BTreeSet) { + let bytes = source.as_bytes(); + let mut index = 0; + while index < bytes.len() { + if bytes[index] != b'"' { + index += 1; + continue; + } + + index += 1; + let mut literal = String::new(); + let mut escaped = false; + while index < bytes.len() { + let byte = bytes[index]; + index += 1; + if escaped { + literal.push(byte as char); + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == b'"' { + break; + } else { + literal.push(byte as char); + } + } + + if is_php_symbol_literal(&literal) { + literals.insert(literal); + } + } +} + +/// Returns whether a string literal can be a lowercase PHP builtin symbol. +fn is_php_symbol_literal(literal: &str) -> bool { + !literal.is_empty() + && literal + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') +} + /// Verifies eval has signature metadata for each shared static builtin. #[test] fn shared_builtin_signature_shape_matches_static_signatures() { From 4f6f30805ee993421c0acf78f6a290a0beef5a71 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 03:24:20 +0200 Subject: [PATCH 0959/1208] Cover eval AOT by-ref fatal writeback --- .../src/interpreter/tests/expressions.rs | 31 +++++++++ .../src/interpreter/tests/method_arguments.rs | 63 +++++++++++++++++++ .../interpreter/tests/support/object_ops.rs | 9 ++- 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs index 1b0d9b7eae..d1aaf8aa6a 100644 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ b/crates/elephc-magician/src/interpreter/tests/expressions.rs @@ -308,6 +308,37 @@ return $value;"#, assert_eq!(values.get(result), FakeValue::Int(9)); } +/// Verifies AOT constructor by-reference writeback still runs when construction fatals. +#[test] +fn execute_program_writes_back_runtime_constructor_by_ref_before_fatal() { + let program = parse_fragment( + br#"$value = "9"; +new KnownFailingConstructor($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownFailingConstructor", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("failing constructor should abort after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.get(value), FakeValue::Int(9)); +} + /// Verifies eval-declared classes create objects with properties and methods. #[test] fn execute_program_constructs_eval_declared_class_with_method() { diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs index 75f969f1a2..0b2b504f0c 100644 --- a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs @@ -709,6 +709,69 @@ return $value;"#, assert_eq!(values.get(result), FakeValue::Int(3)); } +/// Verifies AOT instance method by-reference writeback still runs when the method fatals. +#[test] +fn execute_program_writes_back_runtime_method_by_ref_before_fatal() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$value = "3"; +$box->add2_x($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("runtime method should fail after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(value), FakeValue::Int(3)); +} + +/// Verifies AOT static method by-reference writeback still runs when the method fatals. +#[test] +fn execute_program_writes_back_runtime_static_method_by_ref_before_fatal() { + let program = parse_fragment( + br#"$value = "3"; +KnownClass::sum($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("runtime static method should fail after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(value), FakeValue::Int(3)); +} + /// Verifies runtime/AOT method fallback rejects named arguments without metadata. #[test] fn execute_program_rejects_unregistered_named_args_for_runtime_method_fallback() { diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index 41bf075e27..ec67b963af 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -1163,6 +1163,12 @@ impl FakeOps { } return Ok(()); } + if class_name + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case("KnownFailingConstructor")) + { + return Err(EvalStatus::RuntimeFatal); + } if let Some(first) = args.first().copied() { if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { *value = first; @@ -1174,7 +1180,8 @@ impl FakeOps { } /// Reports one fake AOT class for eval `class_exists` unit tests. pub(super) fn runtime_class_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownClass")) + Ok(name.eq_ignore_ascii_case("KnownClass") + || name.eq_ignore_ascii_case("KnownFailingConstructor")) } /// Reports fake generated AOT ReflectionClass flags for eval metadata unit tests. pub(super) fn runtime_reflection_class_flags( From 9b028362c1f3f6a25bf7bb18e4d13fbbdf4e5be9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 03:58:13 +0200 Subject: [PATCH 0960/1208] Support eval closure callables --- crates/elephc-magician/src/context.rs | 86 +++++++++++++ crates/elephc-magician/src/eval_ir.rs | 31 +++++ .../interpreter/builtins/registry/callable.rs | 27 ++++ .../src/interpreter/builtins/symbols.rs | 4 +- .../src/interpreter/dynamic_functions.rs | 120 ++++++++++++++++++ .../src/interpreter/expressions.rs | 44 +++++++ crates/elephc-magician/src/interpreter/mod.rs | 3 +- .../src/interpreter/tests/closures.rs | 113 +++++++++++++++++ .../src/interpreter/tests/mod.rs | 1 + .../elephc-magician/src/parser/expressions.rs | 87 ++++++++++++- crates/elephc-magician/src/parser/state.rs | 7 + .../elephc-magician/src/parser/statements.rs | 21 +-- tests/codegen/eval_closures.rs | 57 +++++++++ tests/codegen/mod.rs | 1 + 14 files changed, 588 insertions(+), 14 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/tests/closures.rs create mode 100644 tests/codegen/eval_closures.rs diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 9149e65bdc..3ce08811e9 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -302,6 +302,68 @@ struct EvalObjectCallableMetadata { bridge_scope: String, } +/// Runtime value captured by an eval closure literal. +#[derive(Clone)] +pub struct EvalClosureCaptureBinding { + name: String, + value: RuntimeCellHandle, + by_ref_target: Option, +} + +impl EvalClosureCaptureBinding { + /// Creates one captured runtime value with optional caller-side by-reference storage. + pub fn new( + name: impl Into, + value: RuntimeCellHandle, + by_ref_target: Option, + ) -> Self { + Self { + name: name.into(), + value, + by_ref_target, + } + } + + /// Returns the captured variable name without the leading `$`. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the runtime cell captured by the closure. + pub const fn value(&self) -> RuntimeCellHandle { + self.value + } + + /// Returns caller-side writeback metadata for by-reference captures. + pub fn by_ref_target(&self) -> Option<&EvalReferenceTarget> { + self.by_ref_target.as_ref() + } +} + +/// One eval closure instance retained by a synthetic callable name. +#[derive(Clone)] +pub struct EvalClosure { + function: EvalFunction, + captures: Vec, +} + +impl EvalClosure { + /// Creates one closure instance from its function body and captured values. + pub fn new(function: EvalFunction, captures: Vec) -> Self { + Self { function, captures } + } + + /// Returns the executable eval function payload for this closure. + pub fn function(&self) -> &EvalFunction { + &self.function + } + + /// Returns the captured runtime values attached to this closure instance. + pub fn captures(&self) -> &[EvalClosureCaptureBinding] { + &self.captures + } +} + /// Native AOT function callback metadata visible to runtime eval fragments. #[derive(Clone)] pub struct NativeFunction { @@ -767,6 +829,8 @@ pub struct ElephcEvalContext { enum_case_values: HashMap<(String, String), RuntimeCellHandle>, constants: HashMap, functions: HashMap, + closures: HashMap, + next_closure_id: usize, native_functions: HashMap, native_methods: HashMap<(String, String), NativeCallableSignature>, native_static_methods: HashMap<(String, String), NativeCallableSignature>, @@ -836,6 +900,8 @@ impl ElephcEvalContext { enum_case_values: HashMap::new(), constants: HashMap::new(), functions: HashMap::new(), + closures: HashMap::new(), + next_closure_id: 0, native_functions: HashMap::new(), native_methods: HashMap::new(), native_static_methods: HashMap::new(), @@ -906,6 +972,8 @@ impl ElephcEvalContext { enum_case_values: HashMap::new(), constants: HashMap::new(), functions: HashMap::new(), + closures: HashMap::new(), + next_closure_id: 0, native_functions: HashMap::new(), native_methods: HashMap::new(), native_static_methods: HashMap::new(), @@ -2690,6 +2758,14 @@ impl ElephcEvalContext { Ok(()) } + /// Stores one eval closure instance under a context-local synthetic callable name. + pub fn define_closure(&mut self, closure: EvalClosure) -> String { + let name = format!("{{closure:eval:{}}}", self.next_closure_id); + self.next_closure_id += 1; + self.closures.insert(name.clone(), closure); + name + } + /// Defines a generated native function callback, failing if the name already exists. pub fn define_native_function( &mut self, @@ -2709,6 +2785,11 @@ impl ElephcEvalContext { self.functions.get(name) } + /// Returns a dynamic eval closure by its synthetic callable name. + pub fn closure(&self, name: &str) -> Option<&EvalClosure> { + self.closures.get(name) + } + /// Returns a native AOT function callback by its lowercase PHP function name. pub fn native_function(&self, name: &str) -> Option { self.native_functions.get(name).cloned() @@ -3385,6 +3466,11 @@ impl ElephcEvalContext { self.functions.contains_key(name) || self.native_functions.contains_key(name) } + /// Returns true when the context has a closure registered under this synthetic name. + pub fn has_closure(&self, name: &str) -> bool { + self.closures.contains_key(name) + } + /// Returns a stored static local cell for an eval-declared function. pub fn static_local(&self, function_name: &str, name: &str) -> Option { self.static_locals diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index e2bc4457c5..4db19137d6 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -343,6 +343,33 @@ pub struct EvalCatch { pub body: Vec, } +/// One lexical variable captured by a runtime eval closure literal. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClosureCapture { + name: String, + by_ref: bool, +} + +impl EvalClosureCapture { + /// Creates one closure capture with its source variable name and reference mode. + pub fn new(name: impl Into, by_ref: bool) -> Self { + Self { + name: name.into(), + by_ref, + } + } + + /// Returns the captured variable name without the leading `$`. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns whether this capture was declared as `use (&$name)`. + pub const fn by_ref(&self) -> bool { + self.by_ref + } +} + /// Runtime user function declared by an eval fragment. #[derive(Debug, Clone)] pub struct EvalFunction { @@ -2520,6 +2547,10 @@ pub enum EvalExpr { }, Const(EvalConst), ConstFetch(String), + Closure { + function: EvalFunction, + captures: Vec, + }, FunctionCallable { name: String, fallback_name: Option, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index aee81fad64..b2ba4c1f58 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -251,6 +251,14 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( ) -> Result { match callback { EvaluatedCallable::Named(name) => { + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args( + &closure, + positional_args(evaluated_args), + context, + values, + ); + } eval_callable_with_values(name, evaluated_args, context, values) } EvaluatedCallable::InvokableObject { object } => { @@ -405,6 +413,14 @@ fn eval_named_callable_with_call_user_func_values( if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { return Ok(result); } + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args( + &closure, + positional_args(evaluated_args), + context, + values, + ); + } if let Some(function) = context.function(name).cloned() { let evaluated_args = positional_args(evaluated_args); let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( @@ -830,6 +846,14 @@ pub(in crate::interpreter) fn eval_callable_with_values( if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { return Ok(result); } + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args( + &closure, + positional_args(evaluated_args), + context, + values, + ); + } if let Some(function) = context.function(name).cloned() { return eval_dynamic_function_with_values(&function, evaluated_args, context, values); } @@ -868,6 +892,9 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( }; return Ok(result); } + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args(&closure, evaluated_args, context, values); + } if let Some(function) = context.function(name).cloned() { return eval_dynamic_function_with_evaluated_args( &function, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 13f0bcb6ea..650aa5831e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -992,7 +992,9 @@ fn eval_callable_probe_exists( values: &mut impl RuntimeValueOps, ) -> Result { match callback { - EvaluatedCallable::Named(name) => Ok(eval_function_probe_exists(context, name)), + EvaluatedCallable::Named(name) => { + Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) + } EvaluatedCallable::InvokableObject { object } => { eval_object_method_callable_probe(*object, "__invoke", context, values) } diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index b666c35790..4de011c69b 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1631,6 +1631,126 @@ pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_ return_result } +/// Evaluates one runtime eval closure after callback arguments preserve names and ref targets. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args( + closure: &EvalClosure, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let function = closure.function(); + let static_names = static_var_names(function.body()); + context.push_function(function.name()); + let evaluated_args = match bind_evaluated_method_args( + function.params(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + evaluated_args, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + context.pop_function(); + return Err(status); + } + }; + let mut function_scope = ElephcEvalScope::new(); + bind_closure_captures(&mut function_scope, closure.captures()); + bind_method_scope_args( + &mut function_scope, + function.params(), + function.parameter_is_by_ref(), + &evaluated_args, + ); + let result = execute_statements(function.body(), context, &mut function_scope, values); + let persist_result = persist_static_locals( + context, + function.name(), + &static_names, + &function_scope, + values, + ); + let capture_writeback_result = + write_back_closure_ref_captures(closure.captures(), &function_scope, context, values); + let arg_writeback_result = write_back_method_ref_args( + function.params(), + &evaluated_args, + &function_scope, + context, + values, + ); + let return_result = match ( + persist_result, + capture_writeback_result, + arg_writeback_result, + result, + ) { + (Err(status), _, _, _) + | (_, Err(status), _, _) + | (_, _, Err(status), _) + | (_, _, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + function.return_type(), + None, + None, + control, + context, + values, + ), + }; + context.pop_function(); + return_result +} + +/// Seeds one closure activation scope with values captured when the closure was created. +fn bind_closure_captures( + function_scope: &mut ElephcEvalScope, + captures: &[EvalClosureCaptureBinding], +) { + for capture in captures { + if let Some(target) = capture.by_ref_target().cloned() { + function_scope.set_reference( + capture.name().to_string(), + capture.name().to_string(), + capture.value(), + ScopeCellOwnership::Borrowed, + ); + function_scope.set_reference_target(capture.name().to_string(), target); + } else { + function_scope.set( + capture.name().to_string(), + capture.value(), + ScopeCellOwnership::Borrowed, + ); + } + } +} + +/// Writes modified by-reference closure captures back to their defining caller targets. +fn write_back_closure_ref_captures( + captures: &[EvalClosureCaptureBinding], + function_scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for capture in captures { + let Some(target) = capture.by_ref_target() else { + continue; + }; + let Some(entry) = function_scope + .entry(capture.name()) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + continue; + }; + write_back_method_ref_target(target, entry.cell(), context, values)?; + } + Ok(()) +} + /// Persists static local variables from one eval-declared function activation. pub(super) fn persist_static_locals( context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index c9465ec5b0..40cf10893d 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -44,6 +44,9 @@ pub(in crate::interpreter) fn eval_expr( EvalExpr::Cast { target, expr } => eval_cast_expr(target, expr, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), + EvalExpr::Closure { function, captures } => { + eval_closure_expr(function, captures, context, scope, values) + } EvalExpr::FunctionCallable { name, fallback_name, @@ -652,6 +655,47 @@ fn eval_instanceof_object_result( .map_or_else(|| values.object_is_a(value, target_class, false), Ok) } +/// Materializes one eval closure literal as a synthetic callable stored in the context. +fn eval_closure_expr( + function: &EvalFunction, + captures: &[crate::eval_ir::EvalClosureCapture], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bindings = Vec::with_capacity(captures.len()); + for capture in captures { + bindings.push(eval_closure_capture(capture, context, scope, values)?); + } + let closure = EvalClosure::new(function.clone(), bindings); + let name = context.define_closure(closure); + values.string(&name) +} + +/// Evaluates one closure capture from the defining scope. +fn eval_closure_capture( + capture: &crate::eval_ir::EvalClosureCapture, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if capture.by_ref() { + let expr = EvalExpr::LoadVar(capture.name().to_string()); + let (value, target) = eval_call_arg_value(&expr, context, scope, values)?; + return Ok(EvalClosureCaptureBinding::new( + capture.name(), + value, + target, + )); + } + let value = if let Some(value) = visible_scope_cell(context, scope, capture.name()) { + values.retain(value)? + } else { + values.null()? + }; + Ok(EvalClosureCaptureBinding::new(capture.name(), value, None)) +} + /// Evaluates a PHP `match` expression with strict comparison and lazy arm values. pub(in crate::interpreter) fn eval_match_expr( subject: &EvalExpr, diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 78f0498e74..663f46e3ea 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -35,7 +35,8 @@ mod throwables; use crate::context::{ ElephcEvalContext, ElephcEvalExecutionScope, EvalArrayReferenceKey, EvalReferenceTarget, - NativeCallableDefault, NativeCallableSignature, NativeFunction, + EvalClosure, EvalClosureCaptureBinding, NativeCallableDefault, NativeCallableSignature, + NativeFunction, }; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ diff --git a/crates/elephc-magician/src/interpreter/tests/closures.rs b/crates/elephc-magician/src/interpreter/tests/closures.rs new file mode 100644 index 0000000000..350b8df2c7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/closures.rs @@ -0,0 +1,113 @@ +//! Purpose: +//! Interpreter tests for eval closure literals, captures, and callable dispatch. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases exercise eval-only closure execution against fake runtime cells. +//! - Reflection and PHP `Closure` object identity remain covered by later bridge work. + +use super::super::*; +use super::support::*; + +/// Verifies eval closure literals dispatch through direct variable calls and call_user_func_array. +#[test] +fn execute_program_dispatches_eval_closure_literal() { + let program = parse_fragment( + br#"$fn = function($left, $right = 2) { return $left + $right; }; +echo $fn(3); echo ":"; +echo call_user_func_array($fn, ["right" => 6, "left" => 5]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:11"); +} + +/// Verifies by-value eval closure captures snapshot the defining value for each invocation. +#[test] +fn execute_program_closure_by_value_capture_uses_snapshot() { + let program = parse_fragment( + br#"$x = 1; +$fn = function() use ($x) { $x += 1; return $x; }; +$x = 5; +echo $fn(); echo ":"; +echo $fn(); echo ":"; +echo $x;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "2:2:5"); + assert_eq!(values.get(x), FakeValue::Int(5)); +} + +/// Verifies by-reference eval closure captures write back before a failing body escapes. +#[test] +fn execute_program_closure_by_ref_capture_writes_back_before_fatal() { + let program = parse_fragment( + br#"$x = 1; +$fn = function() use (&$x) { $x = 9; missing_eval_closure_function(); }; +$fn();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err("closure should fail"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(x), FakeValue::Int(9)); +} + +/// Verifies eval closure by-reference parameters mutate the caller variable. +#[test] +fn execute_program_closure_by_ref_parameter_writes_back() { + let program = parse_fragment( + br#"$fn = function(&$value) { $value += 2; }; +$value = 3; +$fn($value); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies eval closure values are callable but do not leak into function_exists. +#[test] +fn execute_program_closure_is_callable_without_function_exists_leak() { + let program = parse_fragment( + br#"$fn = function() { return "ok"; }; +echo is_callable($fn) ? "C" : "c"; +echo call_user_func($fn);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + let fn_cell = scope.visible_cell("fn").expect("scope should contain fn"); + let FakeValue::String(name) = values.get(fn_cell) else { + panic!("temporary closure representation should be a callable name"); + }; + + assert_eq!(values.output, "Cok"); + assert!(context.has_closure(&name)); + assert!(!context.has_function(&name)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/mod.rs b/crates/elephc-magician/src/interpreter/tests/mod.rs index 6f5ea657d8..482988cf96 100644 --- a/crates/elephc-magician/src/interpreter/tests/mod.rs +++ b/crates/elephc-magician/src/interpreter/tests/mod.rs @@ -47,6 +47,7 @@ mod builtins_symbols; mod builtins_system_network; mod class_constants; mod classes; +mod closures; mod control_flow; mod core; mod dynamic_calls; diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 6c682c3d57..4aa8901d49 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -10,10 +10,12 @@ use super::cursor::*; use super::state::*; +use super::statements::{EvalTypePosition, ParsedMethodParams}; use crate::errors::EvalParseError; use crate::eval_ir::{ - EvalArrayElement, EvalBinOp, EvalCallArg, EvalCastType, EvalConst, EvalExpr, - EvalInstanceOfTarget, EvalMagicConst, EvalMatchArm, EvalUnaryOp, + EvalArrayElement, EvalBinOp, EvalCallArg, EvalCastType, EvalClosureCapture, EvalConst, + EvalExpr, EvalFunction, EvalInstanceOfTarget, EvalMagicConst, EvalMatchArm, EvalSourceLocation, + EvalUnaryOp, }; use crate::lexer::TokenKind; @@ -691,6 +693,13 @@ impl Parser { let expr = self.parse_expr()?; Ok(EvalExpr::Print(Box::new(expr))) } + TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_closure_expr(false), + TokenKind::Ident(name) + if ident_eq(name, "static") + && matches!(self.peek(), TokenKind::Ident(next) if ident_eq(next, "function")) => + { + self.parse_closure_expr(true) + } TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { self.parse_legacy_array_literal() } @@ -1289,6 +1298,80 @@ impl Parser { ]) } + /// Parses an anonymous function expression into a runtime eval closure payload. + fn parse_closure_expr(&mut self, is_static: bool) -> Result { + let source_start_line = self.current_line(); + if is_static { + self.advance(); + } + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params("", false)?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + let captures = self.parse_optional_closure_use_captures(¶ms)?; + let return_type = self.parse_optional_return_type(EvalTypePosition::FunctionReturn)?; + let (body, source_end_line) = self.parse_block_with_end_line()?; + let function = EvalFunction::new(next_closure_function_name(), params, body) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_parameter_attributes(parameter_attributes) + .with_parameter_types(parameter_types) + .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type); + Ok(EvalExpr::Closure { function, captures }) + } + + /// Parses an optional closure `use (...)` capture list. + fn parse_optional_closure_use_captures( + &mut self, + params: &[String], + ) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) { + return Ok(Vec::new()); + } + self.advance(); + self.expect(TokenKind::LParen)?; + if self.consume(TokenKind::RParen) { + return Ok(Vec::new()); + } + let mut captures = Vec::new(); + loop { + let by_ref = self.consume(TokenKind::Ampersand); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + if params.iter().any(|param| param == name) + || captures + .iter() + .any(|capture: &EvalClosureCapture| capture.name() == name) + { + return Err(EvalParseError::UnsupportedConstruct); + } + captures.push(EvalClosureCapture::new(name.clone(), by_ref)); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok(captures) + } + /// Parses an array literal with source-order optional key/value element expressions. pub(super) fn parse_array_literal(&mut self) -> Result { self.expect(TokenKind::LBracket)?; diff --git a/crates/elephc-magician/src/parser/state.rs b/crates/elephc-magician/src/parser/state.rs index e407adfb9b..cf087d9b29 100644 --- a/crates/elephc-magician/src/parser/state.rs +++ b/crates/elephc-magician/src/parser/state.rs @@ -19,6 +19,7 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; static ANONYMOUS_CLASS_COUNTER: AtomicUsize = AtomicUsize::new(0); +static CLOSURE_FUNCTION_COUNTER: AtomicUsize = AtomicUsize::new(0); /// Parses tokenized eval fragments into EvalIR. pub(super) struct Parser { @@ -59,6 +60,12 @@ pub(super) fn next_anonymous_class_name() -> String { format!("class@anonymous#eval{id}") } +/// Returns a parser-global synthetic function name for one eval closure expression. +pub(super) fn next_closure_function_name() -> String { + let id = CLOSURE_FUNCTION_COUNTER.fetch_add(1, Ordering::Relaxed); + format!("{{closure:eval:function:{id}}}") +} + impl NamespaceImports { /// Stores one class import under PHP's case-insensitive class alias key. pub(super) fn insert_class(&mut self, alias: String, name: String) { diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 0534c6f295..1990d9a96b 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -23,14 +23,14 @@ use crate::lexer::TokenKind; /// Parsed method parameters plus constructor-promotion side products. pub(super) struct ParsedMethodParams { - params: Vec, - parameter_attributes: Vec>, - parameter_types: Vec>, - parameter_defaults: Vec>, - parameter_is_by_ref: Vec, - parameter_is_variadic: Vec, - promoted_properties: Vec, - promoted_assignments: Vec, + pub(super) params: Vec, + pub(super) parameter_attributes: Vec>, + pub(super) parameter_types: Vec>, + pub(super) parameter_defaults: Vec>, + pub(super) parameter_is_by_ref: Vec, + pub(super) parameter_is_variadic: Vec, + pub(super) promoted_properties: Vec, + pub(super) promoted_assignments: Vec, } /// Class-body members collected while parsing a named or anonymous eval class. @@ -45,7 +45,7 @@ struct ParsedClassBody { /// Type-declaration position controls PHP-only atoms such as `void`, `static`, and `callable`. #[derive(Clone, Copy)] -enum EvalTypePosition { +pub(super) enum EvalTypePosition { FunctionParameter, MethodParameter, Property, @@ -2524,7 +2524,7 @@ impl Parser { } /// Consumes a supported function or method return type after `:`. - fn parse_optional_return_type( + pub(super) fn parse_optional_return_type( &mut self, position: EvalTypePosition, ) -> Result, EvalParseError> { @@ -4220,6 +4220,7 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { } EvalExpr::Const(_) | EvalExpr::ConstFetch(_) + | EvalExpr::Closure { .. } | EvalExpr::FunctionCallable { .. } | EvalExpr::ClassConstantFetch { .. } | EvalExpr::ClassNameFetch { .. } diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs new file mode 100644 index 0000000000..c9f18d6534 --- /dev/null +++ b/tests/codegen/eval_closures.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! End-to-end regressions for closure literals executed inside runtime eval. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_closure` through Rust's test harness. +//! +//! Key details: +//! - Fixtures compile PHP to native code, enter the eval bridge, and execute +//! closure callable paths through elephc-magician. + +use crate::support::compile_and_run; + +/// Verifies eval closure literals dispatch through direct calls and call_user_func_array. +#[test] +fn test_eval_closure_literal_dispatches_direct_and_call_user_func_array() { + let out = compile_and_run( + r#" 6, "left" => 5]);'); +"#, + ); + + assert_eq!(out, "5:11"); +} + +/// Verifies eval closure by-value captures snapshot the defining value for each call. +#[test] +fn test_eval_closure_by_value_capture_uses_snapshot() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 04:25:32 +0200 Subject: [PATCH 0961/1208] Support eval closure reflection --- .../src/interpreter/reflection.rs | 125 +++++++++++++++++- .../tests/builtins_reflection_functions.rs | 29 ++++ docs/php/eval.md | 6 +- tests/codegen/eval_closures.rs | 21 +++ 4 files changed, 173 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index b2b7867353..d5622ae77f 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -217,10 +217,12 @@ enum EvalReflectionFunctionMethodTarget { name: String, static_key: Option, static_variables: Vec, + closure_captures: Vec, parameters: Vec, source_location: Option, is_variadic: bool, is_static: bool, + is_closure: bool, is_deprecated: bool, return_type_metadata: Option, }, @@ -1300,11 +1302,15 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( values, ) } - "isinternal" - | "isclosure" - | "returnsreference" - | "isgenerator" - | "hastentativereturntype" => eval_reflection_false_metadata_result(evaluated_args, values), + "isinternal" | "returnsreference" | "isgenerator" | "hastentativereturntype" => { + eval_reflection_false_metadata_result(evaluated_args, values) + } + "isclosure" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_closure(&target)) + .map(Some) + } "isdeprecated" => { eval_reflection_bind_no_args(evaluated_args)?; values @@ -1313,7 +1319,10 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( } "isanonymous" => match target { EvalReflectionFunctionMethodTarget::Function { .. } => { - eval_reflection_false_metadata_result(evaluated_args, values) + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_closure(&target)) + .map(Some) } EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), }, @@ -1365,7 +1374,7 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( } "getclosureusedvariables" => { eval_reflection_bind_no_args(evaluated_args)?; - values.array_new(0).map(Some) + eval_reflection_function_closure_used_variables_result(&target, values).map(Some) } "getclosurethis" | "getclosurescopeclass" | "getclosurecalledclass" => { eval_reflection_bind_no_args(evaluated_args)?; @@ -3144,6 +3153,32 @@ fn eval_reflection_function_new( let args = bind_evaluated_function_args(&[String::from("function")], evaluated_args)?; let requested_name = eval_reflection_string_arg(args[0], values)?; let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if let Some(closure) = context.closure(&requested_name).cloned() { + let function = closure.function(); + let required_parameter_count = eval_reflection_required_parameter_count( + function.parameter_defaults(), + function.parameter_is_variadic(), + ); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + return eval_reflection_function_object_result( + &requested_name, + function.attributes(), + ¶meters, + required_parameter_count, + context, + values, + ) + .map(Some); + } if let Some(function) = context.function(&lookup_name).cloned() { let required_parameter_count = eval_reflection_required_parameter_count( function.parameter_defaults(), @@ -8418,6 +8453,9 @@ fn eval_reflection_function_invoke_dispatch( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if let Some(closure) = context.closure(function_name).cloned() { + return eval_closure_with_evaluated_args(&closure, function_args, context, values); + } let function_key = function_name.to_ascii_lowercase(); if let Some(function) = context.function(&function_key).cloned() { let by_value_parameters = vec![false; function.params().len()]; @@ -9341,6 +9379,41 @@ fn eval_reflection_function_method_target( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { if let Some(name) = context.eval_reflection_function_name(identity) { + if let Some(closure) = context.closure(name) { + let function = closure.function(); + let is_variadic = function.parameter_is_variadic().iter().any(|flag| *flag); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + let source_location = function.source_location(); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let static_key = Some(function.name().to_string()); + let static_variables = static_var_initializers(function.body()); + let is_deprecated = + eval_reflection_attributes_include_deprecated(function.attributes()); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key, + static_variables, + closure_captures: closure.captures().to_vec(), + parameters, + source_location, + is_variadic, + is_static: false, + is_closure: true, + is_deprecated, + return_type_metadata, + })); + } let lookup_name = name.to_ascii_lowercase(); if let Some(function) = context.function(&lookup_name) { let is_variadic = function @@ -9369,10 +9442,12 @@ fn eval_reflection_function_method_target( name: name.to_string(), static_key, static_variables, + closure_captures: Vec::new(), parameters, source_location, is_variadic, is_static: false, + is_closure: false, is_deprecated, return_type_metadata, })); @@ -9388,10 +9463,12 @@ fn eval_reflection_function_method_target( name: name.to_string(), static_key: None, static_variables: Vec::new(), + closure_captures: Vec::new(), parameters, source_location: None, is_variadic, is_static: false, + is_closure: false, is_deprecated: false, return_type_metadata, })); @@ -9400,10 +9477,12 @@ fn eval_reflection_function_method_target( name: name.to_string(), static_key: None, static_variables: Vec::new(), + closure_captures: Vec::new(), parameters: Vec::new(), source_location: None, is_variadic: false, is_static: false, + is_closure: false, is_deprecated: false, return_type_metadata: None, })); @@ -9719,6 +9798,16 @@ fn eval_reflection_function_method_is_variadic( } } +/// Returns whether the reflected function-like target is an eval closure literal. +fn eval_reflection_function_method_is_closure( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_closure, .. } => *is_closure, + EvalReflectionFunctionMethodTarget::Method { .. } => false, + } +} + /// Returns whether the reflected function-like target is static. fn eval_reflection_function_method_is_static(target: &EvalReflectionFunctionMethodTarget) -> bool { match target { @@ -9737,6 +9826,28 @@ fn eval_reflection_function_method_is_deprecated( } } +/// Builds `ReflectionFunction::getClosureUsedVariables()` for eval closure targets. +fn eval_reflection_function_closure_used_variables_result( + target: &EvalReflectionFunctionMethodTarget, + values: &mut impl RuntimeValueOps, +) -> Result { + let EvalReflectionFunctionMethodTarget::Function { + is_closure: true, + closure_captures, + .. + } = target + else { + return values.array_new(0); + }; + let mut result = values.assoc_new(closure_captures.len())?; + for capture in closure_captures { + let key = values.string(capture.name())?; + let value = values.retain(capture.value())?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + /// Returns the retained return type metadata for a reflected function or method. fn eval_reflection_function_method_return_type( target: &EvalReflectionFunctionMethodTarget, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs index 73f9ec32f3..76f78c05f3 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -237,6 +237,35 @@ return true;"#, assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies ReflectionFunction recognizes eval closure literals and dispatches them. +#[test] +fn execute_program_reflection_function_supports_eval_closure_literals() { + let program = parse_fragment( + br#"$seed = 4; +$fn = function($delta = 1) use ($seed) { return $seed + $delta; }; +$ref = new ReflectionFunction($fn); +echo $ref->isClosure() ? "C" : "c"; echo ":"; +echo $ref->isAnonymous() ? "A" : "a"; echo ":"; +echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->getNumberOfParameters(); echo ":"; +echo $ref->getNumberOfRequiredParameters(); echo ":"; +$vars = $ref->getClosureUsedVariables(); +echo count($vars); echo ":"; +echo $vars["seed"]; echo ":"; +echo $ref->invoke(3); echo ":"; +echo $ref->invokeArgs(["delta" => 5]); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "C:A:U:1:0:1:4:7:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies ReflectionFunction reports eval source file and line metadata. #[test] fn execute_program_reflection_function_reports_source_location() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 10a9737243..5e280607e3 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -344,9 +344,13 @@ and method reflectors. `ReflectionMethod::getStaticVariables()` expose eval-declared static local variables, materializing initializer values before the first invocation and returning updated values after reflected or direct calls. +`ReflectionFunction` over an eval closure literal reports `isClosure()` and +`isAnonymous()` as `true`, exposes captured `use` variables through +`getClosureUsedVariables()`, and can invoke the closure through `invoke()` or +`invokeArgs()`. `ReflectionFunction::getClosureUsedVariables()` and `ReflectionMethod::getClosureUsedVariables()` report empty arrays for supported -non-closure reflectors. +non-closure function and method reflectors. `ReflectionClass::isFinal()`, `ReflectionClass::isAbstract()`, `ReflectionClass::isInterface()`, `ReflectionClass::isTrait()`, and `ReflectionClass::isEnum()` report eval and generated/AOT class-like metadata, diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index c9f18d6534..2f625771c9 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -55,3 +55,24 @@ echo $x;'); assert_eq!(out, "5"); } + +/// Verifies eval closure literals are visible through ReflectionFunction metadata and invocation. +#[test] +fn test_eval_closure_reflection_function_metadata_and_invoke() { + let out = compile_and_run( + r#"isClosure() ? "C" : "c"; echo ":"; +echo $ref->isAnonymous() ? "A" : "a"; echo ":"; +$vars = $ref->getClosureUsedVariables(); +echo count($vars); echo ":"; +echo $vars["seed"]; echo ":"; +echo $ref->invoke(3); echo ":"; +echo $ref->invokeArgs(["delta" => 5]);'); +"#, + ); + + assert_eq!(out, "C:A:1:4:7:9"); +} From 0fe3db7f612813e7c2b4cb091e962533cdc5cb35 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 04:42:18 +0200 Subject: [PATCH 0962/1208] Track static eval closure metadata --- crates/elephc-magician/src/context.rs | 18 ++++++++++++++++-- crates/elephc-magician/src/eval_ir.rs | 1 + .../src/interpreter/expressions.rs | 11 +++++++---- .../src/interpreter/reflection.rs | 2 +- .../tests/builtins_reflection_functions.rs | 7 ++++++- .../elephc-magician/src/parser/expressions.rs | 6 +++++- docs/php/eval.md | 3 ++- tests/codegen/eval_closures.rs | 7 ++++++- 8 files changed, 44 insertions(+), 11 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 3ce08811e9..723beb8787 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -345,12 +345,21 @@ impl EvalClosureCaptureBinding { pub struct EvalClosure { function: EvalFunction, captures: Vec, + is_static: bool, } impl EvalClosure { /// Creates one closure instance from its function body and captured values. - pub fn new(function: EvalFunction, captures: Vec) -> Self { - Self { function, captures } + pub fn new( + function: EvalFunction, + captures: Vec, + is_static: bool, + ) -> Self { + Self { + function, + captures, + is_static, + } } /// Returns the executable eval function payload for this closure. @@ -362,6 +371,11 @@ impl EvalClosure { pub fn captures(&self) -> &[EvalClosureCaptureBinding] { &self.captures } + + /// Returns whether this closure was declared with PHP's `static function` form. + pub const fn is_static(&self) -> bool { + self.is_static + } } /// Native AOT function callback metadata visible to runtime eval fragments. diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 4db19137d6..c3959e10c2 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2550,6 +2550,7 @@ pub enum EvalExpr { Closure { function: EvalFunction, captures: Vec, + is_static: bool, }, FunctionCallable { name: String, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 40cf10893d..968920ecb5 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -44,9 +44,11 @@ pub(in crate::interpreter) fn eval_expr( EvalExpr::Cast { target, expr } => eval_cast_expr(target, expr, context, scope, values), EvalExpr::Const(value) => eval_const(value, values), EvalExpr::ConstFetch(name) => eval_const_fetch(name, context, values), - EvalExpr::Closure { function, captures } => { - eval_closure_expr(function, captures, context, scope, values) - } + EvalExpr::Closure { + function, + captures, + is_static, + } => eval_closure_expr(function, captures, *is_static, context, scope, values), EvalExpr::FunctionCallable { name, fallback_name, @@ -659,6 +661,7 @@ fn eval_instanceof_object_result( fn eval_closure_expr( function: &EvalFunction, captures: &[crate::eval_ir::EvalClosureCapture], + is_static: bool, context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, @@ -667,7 +670,7 @@ fn eval_closure_expr( for capture in captures { bindings.push(eval_closure_capture(capture, context, scope, values)?); } - let closure = EvalClosure::new(function.clone(), bindings); + let closure = EvalClosure::new(function.clone(), bindings, is_static); let name = context.define_closure(closure); values.string(&name) } diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index d5622ae77f..76aaa832e4 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -9408,7 +9408,7 @@ fn eval_reflection_function_method_target( parameters, source_location, is_variadic, - is_static: false, + is_static: closure.is_static(), is_closure: true, is_deprecated, return_type_metadata, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs index 76f78c05f3..4e5721bf9e 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_reflection_functions.rs @@ -244,9 +244,14 @@ fn execute_program_reflection_function_supports_eval_closure_literals() { br#"$seed = 4; $fn = function($delta = 1) use ($seed) { return $seed + $delta; }; $ref = new ReflectionFunction($fn); +$staticFn = static function() {}; +$staticRef = new ReflectionFunction($staticFn); echo $ref->isClosure() ? "C" : "c"; echo ":"; echo $ref->isAnonymous() ? "A" : "a"; echo ":"; echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; +echo $staticRef->isClosure() ? "C" : "c"; echo ":"; +echo $staticRef->isStatic() ? "S" : "s"; echo ":"; echo $ref->getNumberOfParameters(); echo ":"; echo $ref->getNumberOfRequiredParameters(); echo ":"; $vars = $ref->getClosureUsedVariables(); @@ -262,7 +267,7 @@ return true;"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "C:A:U:1:0:1:4:7:9"); + assert_eq!(values.output, "C:A:U:s:C:S:1:0:1:4:7:9"); assert_eq!(values.get(result), FakeValue::Bool(true)); } diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index 4aa8901d49..fbfe4d8cb7 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -1330,7 +1330,11 @@ impl Parser { .with_parameter_by_ref_flags(parameter_is_by_ref) .with_parameter_variadic_flags(parameter_is_variadic) .with_return_type(return_type); - Ok(EvalExpr::Closure { function, captures }) + Ok(EvalExpr::Closure { + function, + captures, + is_static, + }) } /// Parses an optional closure `use (...)` capture list. diff --git a/docs/php/eval.md b/docs/php/eval.md index 5e280607e3..66dea2a8e8 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -345,7 +345,8 @@ and method reflectors. variables, materializing initializer values before the first invocation and returning updated values after reflected or direct calls. `ReflectionFunction` over an eval closure literal reports `isClosure()` and -`isAnonymous()` as `true`, exposes captured `use` variables through +`isAnonymous()` as `true`, reports `isStatic()` from the literal's +`static function` marker, exposes captured `use` variables through `getClosureUsedVariables()`, and can invoke the closure through `invoke()` or `invokeArgs()`. `ReflectionFunction::getClosureUsedVariables()` and diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index 2f625771c9..d8f27092b9 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -64,8 +64,13 @@ fn test_eval_closure_reflection_function_metadata_and_invoke() { eval('$seed = 4; $fn = function($delta = 1) use ($seed) { return $seed + $delta; }; $ref = new ReflectionFunction($fn); +$staticFn = static function() {}; +$staticRef = new ReflectionFunction($staticFn); echo $ref->isClosure() ? "C" : "c"; echo ":"; echo $ref->isAnonymous() ? "A" : "a"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; +echo $staticRef->isClosure() ? "C" : "c"; echo ":"; +echo $staticRef->isStatic() ? "S" : "s"; echo ":"; $vars = $ref->getClosureUsedVariables(); echo count($vars); echo ":"; echo $vars["seed"]; echo ":"; @@ -74,5 +79,5 @@ echo $ref->invokeArgs(["delta" => 5]);'); "#, ); - assert_eq!(out, "C:A:1:4:7:9"); + assert_eq!(out, "C:A:s:C:S:1:4:7:9"); } From 72d88ea7f600a63b52afa17fbb13717956e277ba Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 05:12:33 +0200 Subject: [PATCH 0963/1208] Test eval callable by-ref bridge --- .../interpreter/builtins/registry/callable.rs | 36 +++++- .../src/interpreter/statements.rs | 4 +- tests/codegen/eval_callables.rs | 115 ++++++++++++++++++ tests/codegen/mod.rs | 1 + 4 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 tests/codegen/eval_callables.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index b2ba4c1f58..1704f27bd0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -587,7 +587,7 @@ fn eval_native_object_method_call_user_func_result_for_class( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let Some((declaring_class, _, is_static, is_abstract)) = + let Some((declaring_class, visibility, is_static, is_abstract)) = eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? else { return Ok(None); @@ -595,6 +595,20 @@ fn eval_native_object_method_call_user_func_result_for_class( if is_static || is_abstract { return Ok(None); } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() + && eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract) + { + return eval_native_magic_instance_method_call( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( object, class_name, @@ -696,7 +710,7 @@ fn eval_static_method_call_user_func_result( } else { dispatch_class.clone() }; - let Some((declaring_class, _, is_static, is_abstract)) = + let Some((declaring_class, visibility, is_static, is_abstract)) = eval_aot_method_dispatch_metadata_in_hierarchy( &native_class, method_name, @@ -709,6 +723,24 @@ fn eval_static_method_call_user_func_result( if !is_static || is_abstract { return Ok(None); } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() + && eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract) + { + return eval_native_magic_static_method_call( + &native_class, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( &native_class, method_name, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 4dd4b3e86a..54f1a8acb3 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -10839,7 +10839,7 @@ fn eval_native_static_magic_method_available( } /// Dispatches a missing or inaccessible generated/AOT instance method through `__call()`. -fn eval_native_magic_instance_method_call( +pub(in crate::interpreter) fn eval_native_magic_instance_method_call( object: RuntimeCellHandle, class_name: &str, method_name: &str, @@ -10859,7 +10859,7 @@ fn eval_native_magic_instance_method_call( } /// Dispatches a missing or inaccessible generated/AOT static method through `__callStatic()`. -fn eval_native_magic_static_method_call( +pub(in crate::interpreter) fn eval_native_magic_static_method_call( class_name: &str, method_name: &str, evaluated_args: Vec, diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs new file mode 100644 index 0000000000..d57f0bb3e1 --- /dev/null +++ b/tests/codegen/eval_callables.rs @@ -0,0 +1,115 @@ +//! Purpose: +//! End-to-end regressions for eval callable bridge dispatch. +//! Covers callable values crossing eval into generated/AOT functions and methods. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_callable` through Rust's test harness. +//! +//! Key details: +//! - Fixtures verify by-reference writeback through string, callable-array, and +//! first-class callable forms instead of only direct method/function syntax. + +use crate::support::compile_and_run; + +/// Verifies eval string and first-class AOT function callables preserve by-ref writeback. +#[test] +fn test_eval_aot_function_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallableArrayRefBox(); +$instance = [$box, "bump"]; +$a = "2"; +echo $instance($a, 3) . ":" . gettype($a) . ":" . $a . "|"; +$b = "4"; +echo call_user_func_array($instance, [&$b, 5]) . ":" . gettype($b) . ":" . $b . "|"; +$static = ["EvalAotCallableArrayRefBox", "add"]; +$c = "7"; +echo $static($c, 6) . ":" . gettype($c) . ":" . $c . "|"; +$d = "8"; +return call_user_func_array($static, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!( + out, + "15:integer:15|19:integer:19|13:integer:13|17:integer:17" + ); +} + +/// Verifies eval first-class AOT method callables preserve by-ref writeback. +#[test] +fn test_eval_aot_first_class_method_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotFirstClassRefBox(); +$method = $box->bump(...); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; +$static = EvalAotFirstClassRefBox::add(...); +$b = "4"; +echo $static($b, 5) . ":" . gettype($b) . ":" . $b . "|"; +$name = "EvalAotFirstClassRefBox::add"; +$c = "6"; +echo $name($c, 7) . ":" . gettype($c) . ":" . $c . "|"; +$d = "8"; +return call_user_func_array($static, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!( + out, + "25:integer:25|9:integer:9|13:integer:13|17:integer:17" + ); +} diff --git a/tests/codegen/mod.rs b/tests/codegen/mod.rs index 88ec67a2e9..bad4e3a199 100644 --- a/tests/codegen/mod.rs +++ b/tests/codegen/mod.rs @@ -19,6 +19,7 @@ mod benchmarks; mod echo_vars; mod eval; mod eval_builtin_parity; +mod eval_callables; mod eval_closures; mod operators; mod control_flow; From 45629ed478ee08f36349683e91a6f056fa03d363 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 05:56:41 +0200 Subject: [PATCH 0964/1208] Support eval Closure objects --- crates/elephc-magician/src/context.rs | 21 +++++++++++++++++++ .../interpreter/builtins/registry/callable.rs | 3 +++ .../src/interpreter/builtins/symbols.rs | 8 +++++++ .../src/interpreter/expressions.rs | 20 +++++++++++++----- .../src/interpreter/reflection.rs | 17 ++++++++++++++- .../src/interpreter/tests/closures.rs | 16 +++++++++----- docs/php/eval.md | 16 +++++++++----- tests/codegen/eval_closures.rs | 18 ++++++++++++++++ 8 files changed, 103 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 723beb8787..24e9989a87 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -844,6 +844,7 @@ pub struct ElephcEvalContext { constants: HashMap, functions: HashMap, closures: HashMap, + closure_objects: HashMap, next_closure_id: usize, native_functions: HashMap, native_methods: HashMap<(String, String), NativeCallableSignature>, @@ -915,6 +916,7 @@ impl ElephcEvalContext { constants: HashMap::new(), functions: HashMap::new(), closures: HashMap::new(), + closure_objects: HashMap::new(), next_closure_id: 0, native_functions: HashMap::new(), native_methods: HashMap::new(), @@ -987,6 +989,7 @@ impl ElephcEvalContext { constants: HashMap::new(), functions: HashMap::new(), closures: HashMap::new(), + closure_objects: HashMap::new(), next_closure_id: 0, native_functions: HashMap::new(), native_methods: HashMap::new(), @@ -1648,6 +1651,7 @@ impl ElephcEvalContext { /// Removes one dynamic object identity and all per-object eval metadata. pub fn forget_dynamic_object(&mut self, identity: u64) { self.dynamic_objects.remove(&identity); + self.closure_objects.remove(&identity); self.dynamic_destructing_objects.remove(&identity); self.dynamic_destructed_objects.remove(&identity); self.dynamic_property_aliases @@ -1687,6 +1691,9 @@ impl ElephcEvalContext { /// Returns the PHP-visible eval class name associated with one dynamic object identity. pub fn dynamic_object_class_name(&self, identity: u64) -> Option { + if self.closure_objects.contains_key(&identity) { + return Some(String::from("Closure")); + } if let Some(class) = self.dynamic_object_class(identity) { return Some(class.name().trim_start_matches('\\').to_string()); } @@ -1727,6 +1734,9 @@ impl ElephcEvalContext { /// Returns whether one dynamic object identity was registered with a class-like name. pub fn dynamic_object_is_class(&self, identity: u64, class_name: &str) -> bool { let class_name = normalize_class_name(class_name); + if class_name == "closure" && self.closure_objects.contains_key(&identity) { + return true; + } if self .dynamic_objects .get(&identity) @@ -2780,6 +2790,17 @@ impl ElephcEvalContext { name } + /// Associates a PHP `Closure` object identity with an eval closure callable name. + pub fn register_closure_object(&mut self, identity: u64, closure_name: &str) { + self.closure_objects + .insert(identity, closure_name.to_string()); + } + + /// Returns the eval closure callable name bound to a PHP `Closure` object. + pub fn closure_object_name(&self, identity: u64) -> Option<&str> { + self.closure_objects.get(&identity).map(String::as_str) + } + /// Defines a generated native function callback, failing if the name already exists. pub fn define_native_function( &mut self, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 1704f27bd0..1e3654b8f4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -122,6 +122,9 @@ pub(in crate::interpreter) fn eval_object_callable( values: &mut impl RuntimeValueOps, ) -> Result { let identity = values.object_identity(callback)?; + if let Some(name) = context.closure_object_name(identity) { + return Ok(EvaluatedCallable::Named(name.to_string())); + } let Some(class) = context.dynamic_object_class(identity) else { let class_name = runtime_object_class_name(callback, values)?; let Some((_, _, is_static, is_abstract)) = diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 650aa5831e..ad718f3c76 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -187,6 +187,9 @@ pub(in crate::interpreter) fn eval_class_exists_name( let name = values.string_bytes(name)?; let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; let name = name.trim_start_matches('\\'); + if name.eq_ignore_ascii_case("Closure") { + return Ok(true); + } if context.has_class(name) { return Ok(true); } @@ -628,6 +631,11 @@ pub(in crate::interpreter) fn dynamic_object_is_a( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let identity = values.object_identity(object)?; + if context.dynamic_object_is_class(identity, "Closure") { + return Ok(Some( + !exclude_self && eval_class_like_name_matches("Closure", target_class), + )); + } let Some(class) = context.dynamic_object_class(identity) else { return Ok(None); }; diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 968920ecb5..3edb36ecb3 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -657,7 +657,7 @@ fn eval_instanceof_object_result( .map_or_else(|| values.object_is_a(value, target_class, false), Ok) } -/// Materializes one eval closure literal as a synthetic callable stored in the context. +/// Materializes one eval closure literal as a PHP-visible `Closure` object. fn eval_closure_expr( function: &EvalFunction, captures: &[crate::eval_ir::EvalClosureCapture], @@ -672,7 +672,10 @@ fn eval_closure_expr( } let closure = EvalClosure::new(function.clone(), bindings, is_static); let name = context.define_closure(closure); - values.string(&name) + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_closure_object(identity, &name); + Ok(object) } /// Evaluates one closure capture from the defining scope. @@ -1014,9 +1017,16 @@ pub(in crate::interpreter) fn eval_dynamic_call( ) -> Result { let callback = eval_expr(callee, context, scope, values)?; if values.type_tag(callback)? == EVAL_TAG_OBJECT { - eval_invokable_object_precheck(callback, context, values)?; - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - return eval_invokable_object_call_result(callback, evaluated_args, context, values); + let is_closure_object = values + .object_identity(callback) + .ok() + .and_then(|identity| context.closure_object_name(identity)) + .is_some(); + if !is_closure_object { + eval_invokable_object_precheck(callback, context, values)?; + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + return eval_invokable_object_call_result(callback, evaluated_args, context, values); + } } let callback = eval_callable(callback, context, values)?; let evaluated_args = eval_call_arg_values(args, context, scope, values)?; diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 76aaa832e4..c29ffe792a 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -3151,7 +3151,7 @@ fn eval_reflection_function_new( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("function")], evaluated_args)?; - let requested_name = eval_reflection_string_arg(args[0], values)?; + let requested_name = eval_reflection_function_name_arg(args[0], context, values)?; let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); if let Some(closure) = context.closure(&requested_name).cloned() { let function = closure.function(); @@ -10232,6 +10232,21 @@ fn eval_reflection_union_type_flags(type_metadata: &EvalReflectionUnionTypeMetad } } +/// Converts a ReflectionFunction argument into a function or eval-closure name. +fn eval_reflection_function_name_arg( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_OBJECT { + let identity = values.object_identity(value)?; + if let Some(name) = context.closure_object_name(identity) { + return Ok(name.to_string()); + } + } + eval_reflection_string_arg(value, values) +} + /// Converts one reflection constructor argument to a Rust UTF-8 string. fn eval_reflection_string_arg( value: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/tests/closures.rs b/crates/elephc-magician/src/interpreter/tests/closures.rs index 350b8df2c7..70a923a1b7 100644 --- a/crates/elephc-magician/src/interpreter/tests/closures.rs +++ b/crates/elephc-magician/src/interpreter/tests/closures.rs @@ -6,7 +6,7 @@ //! //! Key details: //! - These cases exercise eval-only closure execution against fake runtime cells. -//! - Reflection and PHP `Closure` object identity remain covered by later bridge work. +//! - Closure values are PHP-visible `Closure` objects with eval-retained bodies. use super::super::*; use super::support::*; @@ -103,11 +103,17 @@ echo call_user_func($fn);"#, execute_program_with_context(&mut context, &program, &mut scope, &mut values) .expect("execute eval ir"); let fn_cell = scope.visible_cell("fn").expect("scope should contain fn"); - let FakeValue::String(name) = values.get(fn_cell) else { - panic!("temporary closure representation should be a callable name"); + let FakeValue::Object(_) = values.get(fn_cell) else { + panic!("closure representation should be a PHP object"); }; + let identity = values + .object_identity(fn_cell) + .expect("closure object should have identity"); + let name = context + .closure_object_name(identity) + .expect("closure object should map to callable name"); assert_eq!(values.output, "Cok"); - assert!(context.has_closure(&name)); - assert!(!context.has_function(&name)); + assert!(context.has_closure(name)); + assert!(!context.has_function(name)); } diff --git a/docs/php/eval.md b/docs/php/eval.md index 66dea2a8e8..c92cefafc7 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -130,8 +130,13 @@ First-class callable syntax such as `strlen(...)`, `$object->method(...)`, values that can be invoked through `$callback(...)`, `call_user_func()`, and `call_user_func_array()`. Namespaced function callables follow PHP's global fallback rule when the -namespaced function is not visible. The eval bridge does not model these values -as full `Closure` objects yet. +namespaced function is not visible. First-class callable descriptors remain eval +callback values rather than full PHP `Closure` objects. + +Closure literals created inside eval are PHP-visible `Closure` objects: they +report true for `is_object()`, `get_class($fn)` returns `Closure`, +`$fn instanceof Closure` works, and they remain callable through direct calls, +`call_user_func()`, `call_user_func_array()`, and `ReflectionFunction`. Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, @@ -882,9 +887,10 @@ Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In -particular, advanced native callable descriptors and full `Closure` object -semantics for callback values are still outside eval fragments. Runtime/AOT -free-function, object-method, static-method, and constructor fallback from eval +particular, advanced native callable descriptors and full PHP `Closure` object +semantics for first-class callable callback values are still outside eval +fragments. Runtime/AOT free-function, object-method, static-method, and +constructor fallback from eval can bind registered names, defaults, and positional variadic tails; method, static-method, and constructor fallback can also bind by-reference lvalue metadata. Generated AOT bridge dispatch supports visibility-checked method, diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index d8f27092b9..ef5eec25e8 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -24,6 +24,24 @@ echo call_user_func_array($fn, ["right" => 6, "left" => 5]);'); assert_eq!(out, "5:11"); } +/// Verifies eval closure literals are exposed as PHP `Closure` objects. +#[test] +fn test_eval_closure_literal_is_php_closure_object() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 06:24:28 +0200 Subject: [PATCH 0965/1208] Support eval first-class Closure objects --- crates/elephc-magician/src/context.rs | 54 +++++- crates/elephc-magician/src/eval_ir.rs | 7 + .../interpreter/builtins/registry/callable.rs | 40 ++++- .../src/interpreter/expressions.rs | 158 ++++++++++++++---- crates/elephc-magician/src/interpreter/mod.rs | 4 +- .../elephc-magician/src/parser/expressions.rs | 26 ++- .../elephc-magician/src/parser/statements.rs | 7 + .../elephc-magician/src/parser/tests/calls.rs | 6 +- docs/php/eval.md | 14 +- tests/codegen/eval_callables.rs | 47 ++++++ 10 files changed, 301 insertions(+), 62 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 24e9989a87..6e4c8f794c 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -302,6 +302,29 @@ struct EvalObjectCallableMetadata { bridge_scope: String, } +/// Callable target represented by a PHP-visible eval `Closure` object. +#[derive(Clone)] +pub enum EvalClosureObjectTarget { + Named(String), + InvokableObject { + object: RuntimeCellHandle, + }, + ObjectMethod { + object: RuntimeCellHandle, + method: String, + called_class: Option, + native_class: Option, + bridge_scope: Option, + }, + StaticMethod { + class_name: String, + method: String, + called_class: Option, + native_class: Option, + bridge_scope: Option, + }, +} + /// Runtime value captured by an eval closure literal. #[derive(Clone)] pub struct EvalClosureCaptureBinding { @@ -844,7 +867,7 @@ pub struct ElephcEvalContext { constants: HashMap, functions: HashMap, closures: HashMap, - closure_objects: HashMap, + closure_objects: HashMap, next_closure_id: usize, native_functions: HashMap, native_methods: HashMap<(String, String), NativeCallableSignature>, @@ -2792,13 +2815,34 @@ impl ElephcEvalContext { /// Associates a PHP `Closure` object identity with an eval closure callable name. pub fn register_closure_object(&mut self, identity: u64, closure_name: &str) { - self.closure_objects - .insert(identity, closure_name.to_string()); + self.register_closure_object_target( + identity, + EvalClosureObjectTarget::Named(closure_name.to_string()), + ); + } + + /// Associates a PHP `Closure` object identity with any eval callable target. + pub fn register_closure_object_target( + &mut self, + identity: u64, + target: EvalClosureObjectTarget, + ) { + self.closure_objects.insert(identity, target); } - /// Returns the eval closure callable name bound to a PHP `Closure` object. + /// Returns the callable target bound to a PHP `Closure` object. + pub fn closure_object_target(&self, identity: u64) -> Option<&EvalClosureObjectTarget> { + self.closure_objects.get(&identity) + } + + /// Returns the eval closure callable name bound to a literal PHP `Closure` object. pub fn closure_object_name(&self, identity: u64) -> Option<&str> { - self.closure_objects.get(&identity).map(String::as_str) + self.closure_objects + .get(&identity) + .and_then(|target| match target { + EvalClosureObjectTarget::Named(name) => Some(name.as_str()), + _ => None, + }) } /// Defines a generated native function callback, failing if the name already exists. diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index c3959e10c2..692da5c685 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -2556,6 +2556,9 @@ pub enum EvalExpr { name: String, fallback_name: Option, }, + InvokableCallable { + object: Box, + }, MethodCallable { object: Box, method: Box, @@ -2564,6 +2567,10 @@ pub enum EvalExpr { class_name: String, method: Box, }, + DynamicStaticMethodCallable { + class_name: Box, + method: Box, + }, DynamicCall { callee: Box, args: Vec, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 1e3654b8f4..56bb335b35 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -122,8 +122,8 @@ pub(in crate::interpreter) fn eval_object_callable( values: &mut impl RuntimeValueOps, ) -> Result { let identity = values.object_identity(callback)?; - if let Some(name) = context.closure_object_name(identity) { - return Ok(EvaluatedCallable::Named(name.to_string())); + if let Some(target) = context.closure_object_target(identity) { + return Ok(eval_closure_object_target_callable(target)); } let Some(class) = context.dynamic_object_class(identity) else { let class_name = runtime_object_class_name(callback, values)?; @@ -156,6 +156,42 @@ pub(in crate::interpreter) fn eval_object_callable( Ok(EvaluatedCallable::InvokableObject { object: callback }) } +/// Converts a PHP-visible eval `Closure` object target into the shared callback enum. +fn eval_closure_object_target_callable(target: &EvalClosureObjectTarget) -> EvaluatedCallable { + match target { + EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named(name.clone()), + EvalClosureObjectTarget::InvokableObject { object } => EvaluatedCallable::InvokableObject { + object: *object, + }, + EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::ObjectMethod { + object: *object, + method: method.clone(), + called_class: called_class.clone(), + native_class: native_class.clone(), + bridge_scope: bridge_scope.clone(), + }, + EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::StaticMethod { + class_name: class_name.clone(), + method: method.clone(), + called_class: called_class.clone(), + native_class: native_class.clone(), + bridge_scope: bridge_scope.clone(), + }, + } +} + /// Normalizes one two-element object-method or static-method callable array. pub(in crate::interpreter) fn eval_array_callable( callback: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 3edb36ecb3..ee410f8854 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -53,12 +53,18 @@ pub(in crate::interpreter) fn eval_expr( name, fallback_name, } => eval_function_callable_expr(name, fallback_name.as_deref(), context, values), + EvalExpr::InvokableCallable { object } => { + eval_invokable_callable_expr(object, context, scope, values) + } EvalExpr::MethodCallable { object, method } => { eval_method_callable_expr(object, method, context, scope, values) } EvalExpr::StaticMethodCallable { class_name, method } => { eval_static_method_callable_expr(class_name, method, context, scope, values) } + EvalExpr::DynamicStaticMethodCallable { class_name, method } => { + eval_dynamic_static_method_callable_expr(class_name, method, context, scope, values) + } EvalExpr::DynamicCall { callee, args } => { eval_dynamic_call(callee, args, context, scope, values) } @@ -672,9 +678,18 @@ fn eval_closure_expr( } let closure = EvalClosure::new(function.clone(), bindings, is_static); let name = context.define_closure(closure); + eval_closure_object_expr(EvalClosureObjectTarget::Named(name), context, values) +} + +/// Materializes one PHP-visible `Closure` object for an eval callable target. +fn eval_closure_object_expr( + target: EvalClosureObjectTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { let object = values.new_object("stdClass")?; let identity = values.object_identity(object)?; - context.register_closure_object(identity, &name); + context.register_closure_object_target(identity, target); Ok(object) } @@ -849,11 +864,21 @@ fn eval_function_callable_expr( values: &mut impl RuntimeValueOps, ) -> Result { if eval_function_probe_exists(context, name) { - return values.string(name); + return eval_closure_object_expr( + EvalClosureObjectTarget::Named(name.trim_start_matches('\\').to_ascii_lowercase()), + context, + values, + ); } if let Some(fallback_name) = fallback_name { if eval_function_probe_exists(context, fallback_name) { - return values.string(fallback_name); + return eval_closure_object_expr( + EvalClosureObjectTarget::Named( + fallback_name.trim_start_matches('\\').to_ascii_lowercase(), + ), + context, + values, + ); } } eval_throw_error( @@ -863,6 +888,22 @@ fn eval_function_callable_expr( ) } +/// Materializes an invokable-object first-class callable as a PHP `Closure` object. +fn eval_invokable_callable_expr( + object: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_expr(object, context, scope, values)?; + eval_invokable_object_precheck(object, context, values)?; + eval_closure_object_expr( + EvalClosureObjectTarget::InvokableObject { object }, + context, + values, + ) +} + /// Materializes an object method first-class callable and records captured AOT bridge scope. fn eval_method_callable_expr( object: &EvalExpr, @@ -873,25 +914,28 @@ fn eval_method_callable_expr( ) -> Result { let object = eval_expr(object, context, scope, values)?; let method = eval_dynamic_member_name(method, context, scope, values)?; - let mut array = values.array_new(2)?; - let zero = values.int(0)?; - let one = values.int(1)?; - let method_value = values.string(&method)?; - array = values.array_set(array, zero, object)?; - array = values.array_set(array, one, method_value)?; - if let Some((native_class, bridge_scope, called_class)) = + let (native_class, bridge_scope, called_class) = if let Some(( + native_class, + bridge_scope, + called_class, + )) = eval_method_callable_native_dispatch(object, &method, context, values)? { - context.register_eval_object_callable( - array, + (Some(native_class), Some(bridge_scope), Some(called_class)) + } else { + (None, None, None) + }; + eval_closure_object_expr( + EvalClosureObjectTarget::ObjectMethod { object, - &method, - &called_class, - &native_class, - &bridge_scope, - ); - } - Ok(array) + method, + called_class, + native_class, + bridge_scope, + }, + context, + values, + ) } /// Finds inherited AOT method dispatch metadata captured by an object method callable. @@ -939,29 +983,73 @@ fn eval_static_method_callable_expr( ) -> Result { let receiver = resolve_eval_static_method_receiver(class_name, context)?; let method = eval_dynamic_member_name(method, context, scope, values)?; - let mut array = values.array_new(2)?; - let zero = values.int(0)?; - let one = values.int(1)?; - let class_value = values.string(&receiver.dispatch_class)?; - let method_value = values.string(&method)?; - array = values.array_set(array, zero, class_value)?; - array = values.array_set(array, one, method_value)?; let native_dispatch = eval_static_method_callable_native_dispatch( &receiver.dispatch_class, &method, context, values, )?; - context.register_eval_static_callable( - array, + eval_static_method_closure_object( + receiver.dispatch_class, + method, + Some(receiver.called_class), + native_dispatch, + context, + values, + ) +} + +/// Materializes a runtime-class static first-class callable as a PHP `Closure` object. +fn eval_dynamic_static_method_callable_expr( + class_name: &EvalExpr, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let receiver = resolve_eval_static_method_receiver(&class_name, context)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let native_dispatch = eval_static_method_callable_native_dispatch( &receiver.dispatch_class, &method, - &receiver.called_class, - native_dispatch - .as_ref() - .map(|(native_class, bridge_scope)| (native_class.as_str(), bridge_scope.as_str())), - ); - Ok(array) + context, + values, + )?; + eval_static_method_closure_object( + receiver.dispatch_class, + method, + Some(receiver.called_class), + native_dispatch, + context, + values, + ) +} + +/// Materializes a static-method callable target as a PHP-visible `Closure` object. +fn eval_static_method_closure_object( + class_name: String, + method: String, + called_class: Option, + native_dispatch: Option<(String, String)>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (native_class, bridge_scope) = native_dispatch + .map(|(native_class, bridge_scope)| (Some(native_class), Some(bridge_scope))) + .unwrap_or((None, None)); + eval_closure_object_expr( + EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + }, + context, + values, + ) } /// Finds inherited or direct AOT static dispatch metadata captured by a static callable. @@ -1020,7 +1108,7 @@ pub(in crate::interpreter) fn eval_dynamic_call( let is_closure_object = values .object_identity(callback) .ok() - .and_then(|identity| context.closure_object_name(identity)) + .and_then(|identity| context.closure_object_target(identity)) .is_some(); if !is_closure_object { eval_invokable_object_precheck(callback, context, values)?; diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 663f46e3ea..abc6ef1e18 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -35,8 +35,8 @@ mod throwables; use crate::context::{ ElephcEvalContext, ElephcEvalExecutionScope, EvalArrayReferenceKey, EvalReferenceTarget, - EvalClosure, EvalClosureCaptureBinding, NativeCallableDefault, NativeCallableSignature, - NativeFunction, + EvalClosure, EvalClosureCaptureBinding, EvalClosureObjectTarget, NativeCallableDefault, + NativeCallableSignature, NativeFunction, }; use crate::errors::{EvalParseError, EvalStatus}; use crate::eval_ir::{ diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index fbfe4d8cb7..fa6336d53b 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -484,6 +484,7 @@ impl Parser { loop { if matches!(self.current(), TokenKind::LParen) { if self.consume_first_class_callable_marker() { + expr = Self::invokable_callable_expr(expr); continue; } let args = self.parse_call_args()?; @@ -948,7 +949,7 @@ impl Parser { self.advance(); if matches!(self.current(), TokenKind::LParen) { if self.consume_first_class_callable_marker() { - return Ok(Self::callable_array_expr( + return Ok(Self::dynamic_static_method_callable_expr( class_name, EvalExpr::LoadVar(member), )); @@ -986,7 +987,7 @@ impl Parser { let method = method.clone(); self.advance(); if self.consume_first_class_callable_marker() { - return Ok(Self::callable_array_expr( + return Ok(Self::dynamic_static_method_callable_expr( class_name, EvalExpr::Const(EvalConst::String(method)), )); @@ -1004,7 +1005,7 @@ impl Parser { self.expect(TokenKind::RBrace)?; if matches!(self.current(), TokenKind::LParen) { if self.consume_first_class_callable_marker() { - return Ok(Self::callable_array_expr(class_name, member)); + return Ok(Self::dynamic_static_method_callable_expr(class_name, member)); } let args = self.parse_call_args()?; return Ok(EvalExpr::DynamicStaticMethodCall { @@ -1290,12 +1291,19 @@ impl Parser { } } - /// Builds a plain PHP callable-array value for dynamic static callable forms. - fn callable_array_expr(receiver: EvalExpr, method: EvalExpr) -> EvalExpr { - EvalExpr::Array(vec![ - EvalArrayElement::Value(receiver), - EvalArrayElement::Value(method), - ]) + /// Builds the EvalIR node used for invokable-object first-class callables. + fn invokable_callable_expr(object: EvalExpr) -> EvalExpr { + EvalExpr::InvokableCallable { + object: Box::new(object), + } + } + + /// Builds the EvalIR node used for runtime-class static first-class callables. + fn dynamic_static_method_callable_expr(class_name: EvalExpr, method: EvalExpr) -> EvalExpr { + EvalExpr::DynamicStaticMethodCallable { + class_name: Box::new(class_name), + method: Box::new(method), + } } /// Parses an anonymous function expression into a runtime eval closure payload. diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index 1990d9a96b..cf1bd85210 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -4235,6 +4235,13 @@ fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { EvalExpr::StaticMethodCallable { method, .. } => { eval_expr_uses_this_property(method, property_name) } + EvalExpr::InvokableCallable { object } => { + eval_expr_uses_this_property(object, property_name) + } + EvalExpr::DynamicStaticMethodCallable { class_name, method } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(method, property_name) + } EvalExpr::Include { path, .. } | EvalExpr::Cast { expr: path, .. } | EvalExpr::Clone(path) diff --git a/crates/elephc-magician/src/parser/tests/calls.rs b/crates/elephc-magician/src/parser/tests/calls.rs index d0c7c1839a..85f9391599 100644 --- a/crates/elephc-magician/src/parser/tests/calls.rs +++ b/crates/elephc-magician/src/parser/tests/calls.rs @@ -120,13 +120,15 @@ fn parse_fragment_accepts_dynamic_call_expression_source() { ); } -/// Verifies first-class invokable object syntax leaves the object value as the eval callback. +/// Verifies first-class invokable object syntax parses as a callable value. #[test] fn parse_fragment_accepts_first_class_invokable_object_callable_source() { let program = parse_fragment(br#"return $box(...);"#).expect("fragment should parse"); assert_eq!( program.statements(), - &[EvalStmt::Return(Some(EvalExpr::LoadVar("box".to_string())))] + &[EvalStmt::Return(Some(EvalExpr::InvokableCallable { + object: Box::new(EvalExpr::LoadVar("box".to_string())) + }))] ); } diff --git a/docs/php/eval.md b/docs/php/eval.md index c92cefafc7..70401ccea5 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -127,11 +127,10 @@ eval-declared functions, and registered AOT functions. First-class callable syntax such as `strlen(...)`, `$object->method(...)`, `ClassName::method(...)`, and `$invokable(...)` materializes eval callback -values that can be invoked through `$callback(...)`, `call_user_func()`, and -`call_user_func_array()`. +values as PHP-visible `Closure` objects that can be invoked through +`$callback(...)`, `call_user_func()`, and `call_user_func_array()`. Namespaced function callables follow PHP's global fallback rule when the -namespaced function is not visible. First-class callable descriptors remain eval -callback values rather than full PHP `Closure` objects. +namespaced function is not visible. Closure literals created inside eval are PHP-visible `Closure` objects: they report true for `is_object()`, `get_class($fn)` returns `Closure`, @@ -887,9 +886,10 @@ Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In -particular, advanced native callable descriptors and full PHP `Closure` object -semantics for first-class callable callback values are still outside eval -fragments. Runtime/AOT free-function, object-method, static-method, and +particular, advanced native callable descriptors and full PHP `Closure` APIs +such as binding/scope mutation and reflection over every first-class callable +shape are still outside eval fragments. Runtime/AOT free-function, +object-method, static-method, and constructor fallback from eval can bind registered names, defaults, and positional variadic tails; method, static-method, and constructor fallback can also bind by-reference lvalue diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index d57f0bb3e1..700b17942a 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -113,3 +113,50 @@ return call_user_func_array($static, [&$d, 9]) . ":" . gettype($d) . ":" . $d;') "25:integer:25|9:integer:9|13:integer:13|17:integer:17" ); } + +/// Verifies eval first-class callables are PHP-visible `Closure` objects and remain invokable. +#[test] +fn test_eval_first_class_callables_are_php_closure_objects() { + let out = compile_and_run( + r#"m(...); +$s = EvalAotFirstClassObjectBox::s(...); +$class = "EvalAotFirstClassObjectBox"; +$ds = $class::s(...); +$i = $box(...); +foreach ([$f, $m, $s, $ds, $i] as $cb) { + echo is_object($cb) ? "O" : "o"; + echo get_class($cb); + echo $cb instanceof Closure ? "I" : "i"; + echo is_callable($cb) ? "C" : "c"; + echo "|"; +} +return $f("1") . ":" . $m("2") . ":" . $s("3") . ":" . $ds("4") . ":" . $i("5");'); +"#, + ); + + assert_eq!( + out, + "OClosureIC|OClosureIC|OClosureIC|OClosureIC|OClosureIC|F1:M2:S3:S4:I5" + ); +} From eb21bc7293be81386443894cc18bd7f3cf24fbad Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 07:05:31 +0200 Subject: [PATCH 0966/1208] Propagate eval constructor throws --- .../src/runtime_hooks/externs.rs | 1 + .../elephc-magician/src/runtime_hooks/ops.rs | 30 +++- src/codegen/eval_constructor_helpers.rs | 168 +++++++++++++++--- tests/codegen/eval.rs | 25 +++ 4 files changed, 194 insertions(+), 30 deletions(-) diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index e8e51830bc..e61f52afce 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -238,6 +238,7 @@ unsafe extern "C" { scope_ptr: *const u8, scope_len: u64, ) -> u64; + pub(super) fn __elephc_eval_value_take_pending_throwable() -> *mut RuntimeCell; pub(super) fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; pub(super) fn __elephc_eval_interface_exists(name_ptr: *const u8, name_len: u64) -> u64; pub(super) fn __elephc_eval_value_is_a( diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 58e77498c9..b5eed9b6b4 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -667,7 +667,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { __elephc_eval_value_release(arg_array.as_ptr()); } if ok == 0 { - Err(EvalStatus::RuntimeFatal) + self.take_pending_native_throwable() + .map_or(Err(EvalStatus::RuntimeFatal), |thrown| { + self.schedule_pending_throw(thrown)?; + Err(EvalStatus::UncaughtThrowable) + }) } else { Ok(()) } @@ -1106,3 +1110,27 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_truthy(value.as_ptr()) != 0 }) } } + +#[cfg(not(test))] +impl ElephcRuntimeOps { + /// Takes a native Throwable that escaped through the generated constructor bridge. + fn take_pending_native_throwable(&self) -> Option { + let thrown = unsafe { __elephc_eval_value_take_pending_throwable() }; + if thrown.is_null() { + None + } else { + Self::object_from_raw(thrown).ok() + } + } + + /// Schedules a native Throwable so eval's ordinary catch machinery can handle it. + fn schedule_pending_throw(&self, thrown: RuntimeCellHandle) -> Result<(), EvalStatus> { + let Some(context) = + (unsafe { (self.context as *mut crate::abi::ElephcEvalContext).as_mut() }) + else { + return Err(EvalStatus::RuntimeFatal); + }; + context.set_pending_throw(thrown); + Ok(()) + } +} diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index e061879574..1db25da362 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -17,6 +17,9 @@ use std::collections::BTreeMap; use crate::codegen::abi; +use crate::codegen::context::{ + TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, TRY_HANDLER_SLOT_SIZE, +}; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; @@ -55,6 +58,10 @@ const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ "JsonException", "FiberError", ]; +const CONSTRUCTOR_HELPER_BASE_FRAME_SIZE: usize = 64; +const CONSTRUCTOR_HELPER_HANDLER_OFFSET: usize = CONSTRUCTOR_HELPER_BASE_FRAME_SIZE; +const CONSTRUCTOR_HELPER_FRAME_SIZE: usize = + CONSTRUCTOR_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; /// Constructor metadata needed by the eval constructor bridge. #[derive(Clone)] @@ -305,6 +312,7 @@ fn emit_constructor_helper( ) } } + emit_take_pending_throwable_helper(module, emitter); } /// Emits the ARM64 constructor helper body. @@ -319,7 +327,7 @@ fn emit_constructor_aarch64( let success_label = "__elephc_eval_value_construct_success"; let fail_label = "__elephc_eval_value_construct_fail"; let done_label = "__elephc_eval_value_construct_done"; - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for scope, args, object, and fp/lr + emitter.instruction(&format!("sub sp, sp, #{}", CONSTRUCTOR_HELPER_FRAME_SIZE)); //reserve helper frame plus a boundary exception handler emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer emitter.instruction("str x2, [sp, #0]"); // save the active eval class-scope pointer @@ -356,7 +364,7 @@ fn emit_constructor_aarch64( emitter.instruction("mov x0, #1"); // report successful construction or no-op emitter.label(done_label); emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #64"); // release the constructor helper frame + emitter.instruction(&format!("add sp, sp, #{}", CONSTRUCTOR_HELPER_FRAME_SIZE)); //release the constructor helper frame and boundary handler emitter.instruction("ret"); // return the constructor status flag to Rust } @@ -374,7 +382,7 @@ fn emit_constructor_x86_64( let done_label = "__elephc_eval_value_construct_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 64"); // reserve aligned slots for object, args, scope, and temp values + emitter.instruction(&format!("sub rsp, {}", CONSTRUCTOR_HELPER_FRAME_SIZE)); //reserve aligned slots plus a boundary exception handler emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the active eval class-scope pointer emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // save the active eval class-scope length emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the boxed eval argument array @@ -415,6 +423,98 @@ fn emit_constructor_x86_64( emitter.instruction("ret"); // return the constructor status flag to Rust } +/// Emits an ARM64 boundary handler so native constructor throws return to magician. +fn emit_aarch64_constructor_exception_boundary_push(emitter: &mut Emitter, escape_label: &str) { + let handler_offset = CONSTRUCTOR_HELPER_HANDLER_OFFSET - 48; + emitter.comment("push eval constructor exception boundary"); + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset + 8)); // preserve the caller activation frame across constructor unwinding + abi::emit_load_symbol_to_reg(emitter, "x10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "str x10, [x29, #{}]", + handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("add x10, x29, #{}", handler_offset)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "add x0, x29, #{}", + handler_offset + TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native constructors + emitter.instruction(&format!("cbnz x0, {}", escape_label)); // non-zero setjmp result means a constructor Throwable escaped +} + +/// Emits an ARM64 boundary pop that preserves the constructor status in x0. +fn emit_aarch64_constructor_exception_boundary_pop(emitter: &mut Emitter) { + let handler_offset = CONSTRUCTOR_HELPER_HANDLER_OFFSET - 48; + emitter.comment("pop eval constructor exception boundary"); + emitter.instruction(&format!("ldr x10, [x29, #{}]", handler_offset)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "ldr x10, [x29, #{}]", + handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "x10", "_rt_diag_suppression", 0); +} + +/// Emits an x86_64 boundary handler so native constructor throws return to magician. +fn emit_x86_64_constructor_exception_boundary_push(emitter: &mut Emitter, escape_label: &str) { + let handler_base = CONSTRUCTOR_HELPER_FRAME_SIZE; + emitter.comment("push eval constructor exception boundary"); + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base)); //save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base - 8)); //preserve the caller activation frame across constructor unwinding + abi::emit_load_symbol_to_reg(emitter, "r10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "mov QWORD PTR [rbp - {}], r10", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("lea r10, [rbp - {}]", handler_base)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "lea rdi, [rbp - {}]", + handler_base - TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native constructors + emitter.instruction("test eax, eax"); // did control arrive through longjmp? + emitter.instruction(&format!("jne {}", escape_label)); // non-zero setjmp result means a constructor Throwable escaped +} + +/// Emits an x86_64 boundary pop that preserves the constructor status in rax. +fn emit_x86_64_constructor_exception_boundary_pop(emitter: &mut Emitter) { + let handler_base = CONSTRUCTOR_HELPER_FRAME_SIZE; + emitter.comment("pop eval constructor exception boundary"); + emitter.instruction(&format!("mov r10, QWORD PTR [rbp - {}]", handler_base)); //reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "mov r10, QWORD PTR [rbp - {}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "r10", "_rt_diag_suppression", 0); +} + +/// Emits a C helper that transfers `_exc_value` ownership to magician. +fn emit_take_pending_throwable_helper(module: &Module, emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- eval bridge: take pending throwable ---"); + label_c_global(module, emitter, "__elephc_eval_value_take_pending_throwable"); + match module.target.arch { + Arch::AArch64 => { + abi::emit_load_symbol_to_reg(emitter, "x0", "_exc_value", 0); + abi::emit_store_zero_to_symbol(emitter, "_exc_value", 0); + emitter.instruction("ret"); // return the pending Throwable pointer to magician + } + Arch::X86_64 => { + abi::emit_load_symbol_to_reg(emitter, "rax", "_exc_value", 0); + abi::emit_store_zero_to_symbol(emitter, "_exc_value", 0); + emitter.instruction("ret"); // return the pending Throwable pointer to magician + } + } +} + /// Emits ARM64 dispatch for compact builtin Throwable constructors. fn emit_aarch64_builtin_throwable_constructor_dispatch( module: &Module, @@ -691,7 +791,8 @@ fn emit_aarch64_constructor_body( emitter.label(&scope_ok_label); } emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); - let (overflow_bytes, ref_slots) = + let body_label = constructor_body_label(module, slot); + let (arg_temp_bytes, ref_slots) = emit_aarch64_prepare_constructor_args( module, emitter, @@ -700,6 +801,11 @@ fn emit_aarch64_constructor_body( fail_label, callable_support, ); + let escape_label = format!("{}_escape", body_label); + emit_aarch64_constructor_exception_boundary_push(emitter, &escape_label); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + let overflow_bytes = + materialize_constructor_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); let callee = slot @@ -713,10 +819,18 @@ fn emit_aarch64_constructor_body( emitter, &ref_slots, 0, - &constructor_body_label(module, slot), + &body_label, ); abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_aarch64_constructor_exception_boundary_pop(emitter); emitter.instruction(&format!("b {}", success_label)); // constructor returned normally + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", body_label); + emit_aarch64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_aarch64_constructor_exception_boundary_pop(emitter); + emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes } /// Emits one x86_64 constructor body or failure branch for an unsupported constructor. @@ -739,7 +853,8 @@ fn emit_x86_64_constructor_body( emitter.label(&scope_ok_label); } emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); - let (overflow_bytes, ref_slots) = + let body_label = constructor_body_label(module, slot); + let (arg_temp_bytes, ref_slots) = emit_x86_64_prepare_constructor_args( module, emitter, @@ -748,6 +863,11 @@ fn emit_x86_64_constructor_body( fail_label, callable_support, ); + let escape_label = format!("{}_escape_x", body_label); + emit_x86_64_constructor_exception_boundary_push(emitter, &escape_label); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + let overflow_bytes = + materialize_constructor_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); let callee = slot @@ -761,10 +881,18 @@ fn emit_x86_64_constructor_body( emitter, &ref_slots, 0, - &constructor_body_label(module, slot), + &body_label, ); abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_x86_64_constructor_exception_boundary_pop(emitter); emitter.instruction(&format!("jmp {}", success_label)); // constructor returned normally + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", body_label); + emit_x86_64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_x86_64_constructor_exception_boundary_pop(emitter); + emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes } /// Emits ARM64 visibility checks for a protected/private constructor bridge hit. @@ -865,7 +993,7 @@ fn emit_x86_64_validate_constructor_arg_count( } } -/// Prepares ARM64 constructor ABI registers for the supported argument shapes. +/// Prepares ARM64 constructor argument temporaries for the supported argument shapes. fn emit_aarch64_prepare_constructor_args( module: &Module, emitter: &mut Emitter, @@ -936,19 +1064,10 @@ fn emit_aarch64_prepare_constructor_args( } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - ( - materialize_constructor_args( - module, - emitter, - &receiver_ty, - &slot.params, - &slot.ref_params, - ), - ref_slots, - ) + (arg_temp_bytes, ref_slots) } -/// Prepares x86_64 constructor ABI registers for the supported argument shapes. +/// Prepares x86_64 constructor argument temporaries for the supported argument shapes. fn emit_x86_64_prepare_constructor_args( module: &Module, emitter: &mut Emitter, @@ -1020,16 +1139,7 @@ fn emit_x86_64_prepare_constructor_args( } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - ( - materialize_constructor_args( - module, - emitter, - &receiver_ty, - &slot.params, - &slot.ref_params, - ), - ref_slots, - ) + (arg_temp_bytes, ref_slots) } /// Materializes the pushed receiver and eval arguments into the target method ABI. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 5abb38be5c..a8fdbe440e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -22904,6 +22904,31 @@ return $value;'); assert_eq!(out, "51"); } +/// Verifies eval writes AOT constructor by-reference args back before a thrown fatal path. +#[test] +fn test_eval_dynamic_new_writes_back_constructor_by_ref_before_throw() { + let out = compile_and_run( + r#"getMessage() . ":"; +} +return gettype($value) . ":" . $value;'); +"#, + ); + assert_eq!(out, "Exception:ctor-fail:integer:20"); +} + /// Verifies eval writes nullable scalar by-reference AOT constructor results back to eval variables. #[test] fn test_eval_dynamic_new_runs_constructor_with_nullable_scalar_by_ref_args() { From a7a168a51bd86d98962a36d19c7a0a57380171d2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 07:30:54 +0200 Subject: [PATCH 0967/1208] Support eval Closure::fromCallable --- .../src/interpreter/statements.rs | 93 +++++++++++++++++++ docs/php/eval.md | 3 + tests/codegen/eval_callables.rs | 89 ++++++++++++++++++ 3 files changed, 185 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 54f1a8acb3..d1323f7093 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7278,6 +7278,15 @@ fn eval_static_method_call_result_resolved( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if let Some(result) = eval_closure_static_method_result( + &class_name, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } if let Some(result) = eval_builtin_property_hook_type_static_method_result( &class_name, method_name, @@ -7423,6 +7432,90 @@ fn eval_static_method_call_result_resolved( ) } +/// Dispatches static methods for eval's builtin `Closure` class slice. +fn eval_closure_static_method_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return Ok(None); + } + if !method_name.eq_ignore_ascii_case("fromCallable") { + return Ok(None); + } + eval_closure_from_callable(evaluated_args, context, values).map(Some) +} + +/// Materializes `Closure::fromCallable()` from one normalized eval callback. +fn eval_closure_from_callable( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut args = bind_evaluated_function_args(&[String::from("callback")], evaluated_args)?; + let callback = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + let callable = eval_callable(callback, context, values)?; + eval_validate_call_user_func_callback(&callable, "Closure::fromCallable", context, values)?; + let target = eval_closure_object_target_from_callable(callable); + eval_closure_object_from_target(target, context, values) +} + +/// Converts a normalized callable target into the storage used by eval Closure objects. +fn eval_closure_object_target_from_callable( + callable: EvaluatedCallable, +) -> EvalClosureObjectTarget { + match callable { + EvaluatedCallable::Named(name) => EvalClosureObjectTarget::Named(name), + EvaluatedCallable::InvokableObject { object } => { + EvalClosureObjectTarget::InvokableObject { object } + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + }, + } +} + +/// Allocates a PHP-visible eval Closure object for one retained callable target. +fn eval_closure_object_from_target( + target: EvalClosureObjectTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_closure_object_target(identity, target); + Ok(object) +} + /// Dispatches one generated/AOT method reached through PHP static-call syntax. fn eval_native_static_syntax_method_result( class_name: &str, diff --git a/docs/php/eval.md b/docs/php/eval.md index 70401ccea5..48386c293f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -131,6 +131,9 @@ values as PHP-visible `Closure` objects that can be invoked through `$callback(...)`, `call_user_func()`, and `call_user_func_array()`. Namespaced function callables follow PHP's global fallback rule when the namespaced function is not visible. +`Closure::fromCallable()` accepts the same supported string, callable-array, +object, and existing `Closure` callback values and materializes a PHP-visible +`Closure` object backed by the normalized eval callable target. Closure literals created inside eval are PHP-visible `Closure` objects: they report true for `is_object()`, `get_class($fn)` returns `Closure`, diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 700b17942a..05069e4034 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -160,3 +160,92 @@ return $f("1") . ":" . $m("2") . ":" . $s("3") . ":" . $ds("4") . ":" . $i("5"); "OClosureIC|OClosureIC|OClosureIC|OClosureIC|OClosureIC|F1:M2:S3:S4:I5" ); } + +/// Verifies `Closure::fromCallable()` normalizes eval string and array callables to Closure objects. +#[test] +fn test_eval_closure_from_callable_normalizes_string_and_array_callables() { + let out = compile_and_run( + r#" $cb) { + echo is_object($cb) ? "O" : "o"; + echo get_class($cb); + echo $cb instanceof Closure ? "I" : "i"; + echo is_callable($cb) ? "C" : "c"; + echo $cb($index + 1); + echo "|"; +}'); +"#, + ); + + assert_eq!( + out, + "OClosureICF1|OClosureICM2|OClosureICS3|OClosureICS4|" + ); +} + +/// Verifies `Closure::fromCallable()` preserves by-ref writeback for AOT call targets. +#[test] +fn test_eval_closure_from_callable_preserves_aot_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$box = new EvalFromCallableRefBox(); +$function = Closure::fromCallable("eval_from_callable_ref_add"); +$a = "2"; +echo $function($a, 3) . ":" . gettype($a) . ":" . $a . "|"; +$instance = Closure::fromCallable([$box, "bump"]); +$b = "4"; +echo $instance($b, 5) . ":" . gettype($b) . ":" . $b . "|"; +$static = Closure::fromCallable(["EvalFromCallableRefBox", "add"]); +$c = "7"; +echo $static($c, 6) . ":" . gettype($c) . ":" . $c . "|"; +$named = Closure::fromCallable("EvalFromCallableRefBox::add"); +$d = "8"; +return call_user_func_array($named, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!( + out, + "5:integer:5|19:integer:19|13:integer:13|17:integer:17" + ); +} From a12919162a7aff1f6b6171585e54e708b4ad0bbe Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 08:00:30 +0200 Subject: [PATCH 0968/1208] Support eval Closure::call --- .../src/interpreter/dynamic_functions.rs | 66 +++++++++ .../src/interpreter/statements.rs | 135 ++++++++++++++++++ docs/php/eval.md | 9 +- tests/codegen/eval_callables.rs | 51 +++++++ tests/codegen/eval_closures.rs | 25 ++++ 5 files changed, 284 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 4de011c69b..1a6fbf2bf9 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1637,10 +1637,48 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_closure_with_optional_bound_this(closure, None, evaluated_args, context, values) +} + +/// Evaluates one runtime eval closure with `$this` bound by `Closure::call()`. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let class_name = eval_closure_bound_object_class_name(bound_this, context, values)?; + eval_closure_with_optional_bound_this( + closure, + Some((bound_this, class_name)), + evaluated_args, + context, + values, + ) +} + +/// Evaluates one runtime eval closure with optional `$this` binding metadata. +fn eval_closure_with_optional_bound_this( + closure: &EvalClosure, + bound_this: Option<(RuntimeCellHandle, String)>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let function = closure.function(); let static_names = static_var_names(function.body()); + let bound_class_pushed = bound_this.is_some(); context.push_function(function.name()); + if let Some((_, class_name)) = &bound_this { + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(class_name.to_string()); + } let evaluated_args = match bind_evaluated_method_args( function.params(), function.parameter_types(), @@ -1653,12 +1691,19 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args( ) { Ok(args) => args, Err(status) => { + if bound_class_pushed { + context.pop_called_class_scope(); + context.pop_class_scope(); + } context.pop_function(); return Err(status); } }; let mut function_scope = ElephcEvalScope::new(); bind_closure_captures(&mut function_scope, closure.captures()); + if let Some((object, _)) = bound_this { + function_scope.set("this", object, ScopeCellOwnership::Borrowed); + } bind_method_scope_args( &mut function_scope, function.params(), @@ -1701,10 +1746,31 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args( values, ), }; + if bound_class_pushed { + context.pop_called_class_scope(); + context.pop_class_scope(); + } context.pop_function(); return_result } +/// Returns the PHP class name used as the bound scope for `Closure::call()`. +pub(in crate::interpreter) fn eval_closure_bound_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + } + runtime_object_class_name(object, values) +} + /// Seeds one closure activation scope with values captured when the closure was created. fn bind_closure_captures( function_scope: &mut ElephcEvalScope, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index d1323f7093..141311f546 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -8427,6 +8427,13 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; return values.method_call(object, method_name, evaluated_args); }; + if let Some(target) = context.closure_object_target(identity).cloned() { + if let Some(result) = + eval_closure_object_method_result(target, method_name, evaluated_args.clone(), context, values)? + { + return Ok(result); + } + } if let Some(attribute_metadata) = context.eval_reflection_attribute(identity).cloned() { if method_name.eq_ignore_ascii_case("newInstance") { if !evaluated_args.is_empty() { @@ -8951,6 +8958,134 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( eval_throw_undefined_method_call_error(&called_class_name, method_name, context, values) } +/// Dispatches PHP-visible methods on eval-backed `Closure` objects. +fn eval_closure_object_method_result( + target: EvalClosureObjectTarget, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("call") { + return Ok(None); + } + let (bound_this, call_args) = eval_closure_call_split_args(evaluated_args)?; + match target { + EvalClosureObjectTarget::Named(name) => { + if let Some(closure) = context.closure(&name).cloned() { + return eval_closure_with_evaluated_args_and_bound_this( + &closure, + bound_this, + call_args, + context, + values, + ) + .map(Some); + } + eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ) + .map(Some) + } + EvalClosureObjectTarget::InvokableObject { object } => { + if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ) + .map(Some); + } + eval_invokable_object_call_result(bound_this, call_args, context, values).map(Some) + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class: _, + native_class, + bridge_scope, + } => { + if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ) + .map(Some); + } + let called_class = Some(eval_closure_bound_object_class_name( + bound_this, context, values, + )?); + let callable = EvaluatedCallable::ObjectMethod { + object: bound_this, + method, + called_class, + native_class, + bridge_scope, + }; + eval_evaluated_callable_with_call_array_args(&callable, call_args, context, values) + .map(Some) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ) + .map(Some), + } +} + +/// Splits `Closure::call()` arguments into the bound object and forwarded closure args. +fn eval_closure_call_split_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let mut bound_this = None; + let mut consumed_positional_receiver = false; + let mut call_args = Vec::with_capacity(evaluated_args.len().saturating_sub(1)); + + for arg in evaluated_args { + if arg + .name + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case("newThis")) + { + if bound_this.replace(arg.value).is_some() { + return Err(EvalStatus::RuntimeFatal); + } + continue; + } + if arg.name.is_none() && !consumed_positional_receiver && bound_this.is_none() { + consumed_positional_receiver = true; + bound_this = Some(arg.value); + continue; + } + call_args.push(arg); + } + + bound_this + .map(|receiver| (receiver, call_args)) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns whether `Closure::call()` may bind a method closure to the new object. +fn eval_closure_call_bound_class_matches( + original_object: RuntimeCellHandle, + bound_this: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let original_class = eval_closure_bound_object_class_name(original_object, context, values)?; + let bound_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + Ok(original_class.eq_ignore_ascii_case(&bound_class)) +} + +/// Emits PHP's `Closure::call()` warning and returns `null`. +fn eval_closure_call_warning_null( + message: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values.warning(message)?; + values.null() +} + /// Dispatches an invokable object through `__invoke()` without enforcing hook visibility. pub(in crate::interpreter) fn eval_invokable_object_call_result( object: RuntimeCellHandle, diff --git a/docs/php/eval.md b/docs/php/eval.md index 48386c293f..d8195cbb4e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -133,12 +133,17 @@ Namespaced function callables follow PHP's global fallback rule when the namespaced function is not visible. `Closure::fromCallable()` accepts the same supported string, callable-array, object, and existing `Closure` callback values and materializes a PHP-visible -`Closure` object backed by the normalized eval callable target. +`Closure` object backed by the normalized eval callable target. `Closure::call()` +on those closure objects supports same-class method and invokable-object +rebinding and reports PHP-compatible warning/null results for function and +static-method callables. Closure literals created inside eval are PHP-visible `Closure` objects: they report true for `is_object()`, `get_class($fn)` returns `Closure`, `$fn instanceof Closure` works, and they remain callable through direct calls, -`call_user_func()`, `call_user_func_array()`, and `ReflectionFunction`. +`call_user_func()`, `call_user_func_array()`, `Closure::call()` for binding +`$this` to an object when invoking the eval-created closure, and +`ReflectionFunction`. Inside eval fragments, two-element object-method callable arrays such as `[$this, "method"]` can be invoked through `$cb(...)`, `call_user_func($cb, diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 05069e4034..b9ff112b09 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -249,3 +249,54 @@ return call_user_func_array($named, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); "5:integer:5|19:integer:19|13:integer:13|17:integer:17" ); } + +/// Verifies `Closure::call()` rebinds `fromCallable()` method closures to a same-class receiver. +#[test] +fn test_eval_closure_from_callable_call_rebinds_same_class_method_and_invokable_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } + + public static function add(int $value): int { + return $value + 1; + } +} + +echo eval('$original = new EvalFromCallableCallBox(); +$original->base = 10; +$bound = new EvalFromCallableCallBox(); +$bound->base = 20; + +$method = Closure::fromCallable([$original, "bump"]); +$a = "2"; +echo $method->call($bound, $a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invoke = Closure::fromCallable($original); +$b = "4"; +echo $invoke->call($bound, $b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$function = Closure::fromCallable("eval_from_callable_call_fn"); +echo is_null($function->call($bound)) ? "F" : "f"; echo "|"; + +$static = Closure::fromCallable(["EvalFromCallableCallBox", "add"]); +echo is_null($static->call($bound, 1)) ? "S" : "s";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|F|S"); +} diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index ef5eec25e8..4b53ab38bf 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -99,3 +99,28 @@ echo $ref->invokeArgs(["delta" => 5]);'); assert_eq!(out, "C:A:s:C:S:1:4:7:9"); } + +/// Verifies eval `Closure::call()` binds `$this` and preserves by-ref argument writeback. +#[test] +fn test_eval_closure_call_binds_this_and_writes_back_by_ref_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; +}; +$seed = "2"; +echo $fn->call($box, $seed, 3); +echo ":"; +echo gettype($seed); +echo ":"; +echo $seed;'); +"#, + ); + + assert_eq!(out, "15:integer:15"); +} From 71ee161ac93b76f5efaeafdc8fa1613bb617081e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 08:14:20 +0200 Subject: [PATCH 0969/1208] Test eval constructor by-ref lvalue targets --- tests/codegen/eval_constructors.rs | 93 ++++++++++++++++++++++++++++++ tests/codegen/mod.rs | 1 + 2 files changed, 94 insertions(+) create mode 100644 tests/codegen/eval_constructors.rs diff --git a/tests/codegen/eval_constructors.rs b/tests/codegen/eval_constructors.rs new file mode 100644 index 0000000000..245cfe40e8 --- /dev/null +++ b/tests/codegen/eval_constructors.rs @@ -0,0 +1,93 @@ +//! Purpose: +//! End-to-end regressions for runtime eval object construction through AOT classes. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_constructor` through Rust's test harness. +//! +//! Key details: +//! - Fixtures focus on constructor bridge argument binding and by-reference +//! writeback for non-variable eval caller targets. + +use crate::support::compile_and_run; + +/// Verifies AOT constructor by-reference args write back to eval lvalue targets. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_writes_back_to_lvalue_targets() { + let out = compile_and_run( + r#" "1"]; +new EvalCtorRefTargetBridge($items["x"]); +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$nested = ["outer" => ["inner" => "2"]]; +new EvalCtorRefTargetBridge($nested["outer"]["inner"]); +echo gettype($nested["outer"]["inner"]) . ":" . $nested["outer"]["inner"] . "|"; + +$box = new EvalCtorRefTargetBox(); +new EvalCtorRefTargetBridge($box->value); +echo gettype($box->value) . ":" . $box->value . "|"; + +EvalCtorRefTargetStatic::$value = "4"; +new EvalCtorRefTargetBridge(EvalCtorRefTargetStatic::$value); +return gettype(EvalCtorRefTargetStatic::$value) . ":" . EvalCtorRefTargetStatic::$value;'); +"#, + ); + + assert_eq!(out, "integer:6|integer:7|integer:8|integer:9"); +} + +/// Verifies AOT constructor by-reference writeback happens before a catchable throw. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_lvalue_writeback_before_throw() { + let out = compile_and_run( + r#" "1"]; +try { + new EvalCtorThrowRefTargetBridge($items["x"]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$box = new EvalCtorThrowRefTargetBox(); +try { + new EvalCtorThrowRefTargetBridge($box->value); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +return gettype($box->value) . ":" . $box->value;'); +"#, + ); + + assert_eq!( + out, + "Exception:ctor-lvalue:integer:12|Exception:ctor-lvalue:integer:16" + ); +} diff --git a/tests/codegen/mod.rs b/tests/codegen/mod.rs index bad4e3a199..b19393d05a 100644 --- a/tests/codegen/mod.rs +++ b/tests/codegen/mod.rs @@ -21,6 +21,7 @@ mod eval; mod eval_builtin_parity; mod eval_callables; mod eval_closures; +mod eval_constructors; mod operators; mod control_flow; mod scalar_strings; From 472101451eecdf7f473b9163122301a1baacdcc7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 08:35:09 +0200 Subject: [PATCH 0970/1208] Test eval ref-like builtin call arrays --- tests/codegen/eval_builtin_parity.rs | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index f492f7df93..18ad6f168f 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -166,3 +166,56 @@ echo max(value: 3, values: 8);'); assert_eq!(out, "1,2,3:8"); } + +/// Verifies eval `call_user_func_array()` preserves positional ref-like builtin targets. +#[test] +fn test_eval_call_user_func_array_ref_like_builtins_write_back_positional_aliases() { + let out = compile_and_run( + r#"items]) . ":"; +echo implode(",", $box->items) . "|"; + +echo call_user_func_array("settype", [&EvalBuiltinRefBridgeBox::$typed, "integer"]) ? "P" : "p"; +echo gettype(EvalBuiltinRefBridgeBox::$typed) . ":" . EvalBuiltinRefBridgeBox::$typed;'); +"#, + ); + + assert_eq!(out, "S1,2,3|Tinteger:42|2:3,1|Pinteger:123"); +} + +/// Verifies eval `call_user_func_array()` preserves named ref-like builtin targets. +#[test] +fn test_eval_call_user_func_array_ref_like_builtins_write_back_named_aliases() { + let out = compile_and_run( + r#" "/(a)(b)/", "subject" => "ab", "matches" => &$matches] +); +echo ":" . $matches[0] . ":" . $matches[1] . ":" . $matches[2] . "|"; + +$items = ["b" => 2, "a" => 1]; +echo call_user_func_array("ksort", ["array" => &$items]) ? "K" : "k"; +foreach ($items as $key => $value) { + echo $key . $value; +}'); +"#, + ); + + assert_eq!(out, "1:ab:a:b|Ka1b2"); +} From c1d85e6357cb31617b90f5bd25c443f5cc7ea57f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 09:00:50 +0200 Subject: [PATCH 0971/1208] Support eval Closure binding --- crates/elephc-magician/src/context.rs | 7 +- .../interpreter/builtins/registry/callable.rs | 45 ++++ .../builtins/registry/callable_validation.rs | 4 +- .../src/interpreter/builtins/symbols.rs | 1 + .../src/interpreter/control.rs | 4 + .../src/interpreter/statements.rs | 251 +++++++++++++++++- docs/php/eval.md | 13 +- tests/codegen/eval_callables.rs | 46 ++++ tests/codegen/eval_closures.rs | 34 +++ 9 files changed, 396 insertions(+), 9 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 6e4c8f794c..826f7ca6a9 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -306,6 +306,10 @@ struct EvalObjectCallableMetadata { #[derive(Clone)] pub enum EvalClosureObjectTarget { Named(String), + BoundNamed { + name: String, + bound_this: RuntimeCellHandle, + }, InvokableObject { object: RuntimeCellHandle, }, @@ -2840,7 +2844,8 @@ impl ElephcEvalContext { self.closure_objects .get(&identity) .and_then(|target| match target { - EvalClosureObjectTarget::Named(name) => Some(name.as_str()), + EvalClosureObjectTarget::Named(name) + | EvalClosureObjectTarget::BoundNamed { name, .. } => Some(name.as_str()), _ => None, }) } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 56bb335b35..1ab1bdfbac 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -160,6 +160,12 @@ pub(in crate::interpreter) fn eval_object_callable( fn eval_closure_object_target_callable(target: &EvalClosureObjectTarget) -> EvaluatedCallable { match target { EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named(name.clone()), + EvalClosureObjectTarget::BoundNamed { name, bound_this } => { + EvaluatedCallable::BoundClosure { + name: name.clone(), + bound_this: *bound_this, + } + } EvalClosureObjectTarget::InvokableObject { object } => EvaluatedCallable::InvokableObject { object: *object, }, @@ -300,6 +306,19 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( } eval_callable_with_values(name, evaluated_args, context, values) } + EvaluatedCallable::BoundClosure { name, bound_this } => { + let closure = context + .closure(name) + .cloned() + .ok_or(EvalStatus::UnsupportedConstruct)?; + eval_closure_with_evaluated_args_and_bound_this( + &closure, + *bound_this, + positional_args(evaluated_args), + context, + values, + ) + } EvaluatedCallable::InvokableObject { object } => { eval_invokable_object_call_result( *object, @@ -377,6 +396,19 @@ fn eval_evaluated_callable_with_call_user_func_values( EvaluatedCallable::Named(name) => { eval_named_callable_with_call_user_func_values(name, evaluated_args, context, values) } + EvaluatedCallable::BoundClosure { name, bound_this } => { + let closure = context + .closure(name) + .cloned() + .ok_or(EvalStatus::UnsupportedConstruct)?; + eval_closure_with_evaluated_args_and_bound_this( + &closure, + *bound_this, + positional_args(evaluated_args), + context, + values, + ) + } EvaluatedCallable::InvokableObject { object } => { eval_invokable_object_with_call_user_func_values( *object, @@ -840,6 +872,19 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( EvaluatedCallable::Named(name) => { eval_callable_with_call_array_args(name, evaluated_args, context, values) } + EvaluatedCallable::BoundClosure { name, bound_this } => { + let closure = context + .closure(name) + .cloned() + .ok_or(EvalStatus::UnsupportedConstruct)?; + eval_closure_with_evaluated_args_and_bound_this( + &closure, + *bound_this, + evaluated_args, + context, + values, + ) + } EvaluatedCallable::InvokableObject { object } => { eval_invokable_object_call_result(*object, evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index b17e8e3209..27c2a1957c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -49,7 +49,9 @@ pub(in crate::interpreter) fn eval_validate_call_user_func_callback( values, ), }, - EvaluatedCallable::Named(_) | EvaluatedCallable::InvokableObject { .. } => Ok(()), + EvaluatedCallable::Named(_) + | EvaluatedCallable::BoundClosure { .. } + | EvaluatedCallable::InvokableObject { .. } => Ok(()), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index ad718f3c76..1b8af26e13 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -1003,6 +1003,7 @@ fn eval_callable_probe_exists( EvaluatedCallable::Named(name) => { Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) } + EvaluatedCallable::BoundClosure { name, .. } => Ok(context.has_closure(name)), EvaluatedCallable::InvokableObject { object } => { eval_object_method_callable_probe(*object, "__invoke", context, values) } diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index 7859ad5c9e..b181d4ee0c 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -86,6 +86,10 @@ pub(super) enum EvalByRefBindingMode<'a> { /// One already evaluated PHP callback supported by the eval dispatcher. pub(super) enum EvaluatedCallable { Named(String), + BoundClosure { + name: String, + bound_this: RuntimeCellHandle, + }, InvokableObject { object: RuntimeCellHandle, }, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 141311f546..9ed1ce3fdb 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7446,10 +7446,13 @@ fn eval_closure_static_method_result( { return Ok(None); } - if !method_name.eq_ignore_ascii_case("fromCallable") { - return Ok(None); + if method_name.eq_ignore_ascii_case("fromCallable") { + return eval_closure_from_callable(evaluated_args, context, values).map(Some); + } + if method_name.eq_ignore_ascii_case("bind") { + return eval_closure_bind_static(evaluated_args, context, values).map(Some); } - eval_closure_from_callable(evaluated_args, context, values).map(Some) + Ok(None) } /// Materializes `Closure::fromCallable()` from one normalized eval callback. @@ -7472,6 +7475,9 @@ fn eval_closure_object_target_from_callable( ) -> EvalClosureObjectTarget { match callable { EvaluatedCallable::Named(name) => EvalClosureObjectTarget::Named(name), + EvaluatedCallable::BoundClosure { name, bound_this } => { + EvalClosureObjectTarget::BoundNamed { name, bound_this } + } EvaluatedCallable::InvokableObject { object } => { EvalClosureObjectTarget::InvokableObject { object } } @@ -7516,6 +7522,224 @@ fn eval_closure_object_from_target( Ok(object) } +/// Materializes `Closure::bind()` from a closure object and a persistent receiver. +fn eval_closure_bind_static( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (target, bound_this) = eval_closure_bind_static_args(evaluated_args, context, values)?; + eval_closure_bind_target(target, bound_this, context, values) +} + +/// Binds static `Closure::bind()` arguments to their PHP parameter slots. +fn eval_closure_bind_static_args( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(EvalClosureObjectTarget, Option), EvalStatus> { + let bound = eval_closure_bind_args( + &["closure", "newThis", "newScope"], + 2, + evaluated_args, + )?; + let closure = required_closure_bind_arg(&bound, 0)?; + let new_this = required_closure_bind_arg(&bound, 1)?; + let target = eval_closure_target_arg(closure.value, context, values)?; + let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; + Ok((target, bound_this)) +} + +/// Binds `Closure::bindTo()` arguments to their PHP parameter slots. +fn eval_closure_bind_to_args( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let bound = eval_closure_bind_args(&["newThis", "newScope"], 1, evaluated_args)?; + let new_this = required_closure_bind_arg(&bound, 0)?; + eval_closure_bind_receiver_arg(new_this.value, values) +} + +/// Binds positional and named Closure binding arguments while accepting optional scope. +fn eval_closure_bind_args( + params: &[&str], + required_count: usize, + evaluated_args: Vec, +) -> Result>, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + let mut saw_named = false; + + for arg in evaluated_args { + if let Some(name) = arg.name.as_deref() { + saw_named = true; + let Some(index) = params + .iter() + .position(|param| param.eq_ignore_ascii_case(name)) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[index] = Some(arg); + continue; + } + + if saw_named || next_positional >= params.len() || bound_args[next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg); + next_positional += 1; + } + + if bound_args + .iter() + .take(required_count) + .any(Option::is_none) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(bound_args) +} + +/// Returns one required Closure binding argument. +fn required_closure_bind_arg( + bound_args: &[Option], + index: usize, +) -> Result<&EvaluatedCallArg, EvalStatus> { + bound_args + .get(index) + .and_then(Option::as_ref) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Extracts a stored eval Closure object target from a runtime object. +fn eval_closure_target_arg( + closure: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(closure)?; + context + .closure_object_target(identity) + .cloned() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts the `newThis` binding argument to an optional object receiver. +fn eval_closure_bind_receiver_arg( + new_this: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(new_this)? { + return Ok(None); + } + if values.type_tag(new_this)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(new_this)) +} + +/// Creates a new Closure object with persistent binding metadata when supported. +fn eval_closure_bind_target( + target: EvalClosureObjectTarget, + bound_this: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(bound_this) = bound_this else { + return eval_closure_unbind_target(target, context, values); + }; + match target { + EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { + let Some(closure) = context.closure(&name) else { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + }; + if closure.is_static() { + return eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ); + } + eval_closure_object_from_target( + EvalClosureObjectTarget::BoundNamed { name, bound_this }, + context, + values, + ) + } + EvalClosureObjectTarget::InvokableObject { object } => { + if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ); + } + eval_closure_object_from_target( + EvalClosureObjectTarget::InvokableObject { object: bound_this }, + context, + values, + ) + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + native_class, + bridge_scope, + .. + } => { + if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ); + } + let called_class = Some(eval_closure_bound_object_class_name( + bound_this, context, values, + )?); + eval_closure_object_from_target( + EvalClosureObjectTarget::ObjectMethod { + object: bound_this, + method, + called_class, + native_class, + bridge_scope, + }, + context, + values, + ) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ), + } +} + +/// Creates an unbound Closure object for targets that can drop `$this`. +fn eval_closure_unbind_target( + target: EvalClosureObjectTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match target { + EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { + eval_closure_object_from_target(EvalClosureObjectTarget::Named(name), context, values) + } + EvalClosureObjectTarget::InvokableObject { .. } + | EvalClosureObjectTarget::ObjectMethod { .. } => { + eval_closure_call_warning_null("Cannot unbind $this of method", values) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot unbind $this of static method", + values, + ), + } +} + /// Dispatches one generated/AOT method reached through PHP static-call syntax. fn eval_native_static_syntax_method_result( class_name: &str, @@ -8966,6 +9190,10 @@ fn eval_closure_object_method_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { + if method_name.eq_ignore_ascii_case("bindTo") { + let bound_this = eval_closure_bind_to_args(evaluated_args, values)?; + return eval_closure_bind_target(target, bound_this, context, values).map(Some); + } if !method_name.eq_ignore_ascii_case("call") { return Ok(None); } @@ -8988,6 +9216,23 @@ fn eval_closure_object_method_result( ) .map(Some) } + EvalClosureObjectTarget::BoundNamed { name, .. } => { + if let Some(closure) = context.closure(&name).cloned() { + return eval_closure_with_evaluated_args_and_bound_this( + &closure, + bound_this, + call_args, + context, + values, + ) + .map(Some); + } + eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ) + .map(Some) + } EvalClosureObjectTarget::InvokableObject { object } => { if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { return eval_closure_call_warning_null( diff --git a/docs/php/eval.md b/docs/php/eval.md index d8195cbb4e..45081ecd64 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -136,13 +136,18 @@ object, and existing `Closure` callback values and materializes a PHP-visible `Closure` object backed by the normalized eval callable target. `Closure::call()` on those closure objects supports same-class method and invokable-object rebinding and reports PHP-compatible warning/null results for function and -static-method callables. +static-method callables. `Closure::bind()` and `Closure::bindTo()` persistently +bind eval closure literals plus same-class method and invokable-object closure +targets to a new receiver. The optional scope argument is accepted, but eval's +current binding model derives method scope from the bound receiver rather than +exposing the full PHP scope-mutation surface. Closure literals created inside eval are PHP-visible `Closure` objects: they report true for `is_object()`, `get_class($fn)` returns `Closure`, `$fn instanceof Closure` works, and they remain callable through direct calls, -`call_user_func()`, `call_user_func_array()`, `Closure::call()` for binding -`$this` to an object when invoking the eval-created closure, and +`call_user_func()`, `call_user_func_array()`, `Closure::call()` for transiently +binding `$this` to an object when invoking the eval-created closure, +`Closure::bind()` / `Closure::bindTo()` for persistent receiver binding, and `ReflectionFunction`. Inside eval fragments, two-element object-method callable arrays such as @@ -895,7 +900,7 @@ fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. In particular, advanced native callable descriptors and full PHP `Closure` APIs -such as binding/scope mutation and reflection over every first-class callable +such as arbitrary scope mutation and reflection over every first-class callable shape are still outside eval fragments. Runtime/AOT free-function, object-method, static-method, and constructor fallback from eval diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index b9ff112b09..02b5289cf9 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -300,3 +300,49 @@ echo is_null($static->call($bound, 1)) ? "S" : "s";'); assert_eq!(out, "25:integer:25|29:integer:29|F|S"); } + +/// Verifies `Closure::bindTo()` persists rebinding for method and invokable callable targets. +#[test] +fn test_eval_closure_bind_from_callable_persists_method_and_invokable_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } + + public static function add(int $value): int { + return $value + 1; + } +} + +echo eval('$original = new EvalFromCallableBindBox(); +$original->base = 10; +$bound = new EvalFromCallableBindBox(); +$bound->base = 20; + +$rawMethod = Closure::fromCallable([$original, "bump"]); +$method = $rawMethod->bindTo($bound); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$rawInvoke = Closure::fromCallable($original); +$invoke = $rawInvoke->bindTo($bound); +$b = "4"; +echo call_user_func_array($invoke, [&$b, 5]) . ":" . gettype($b) . ":" . $b . "|"; + +$static = Closure::fromCallable(["EvalFromCallableBindBox", "add"]); +echo is_null($static->bindTo($bound)) ? "S" : "s";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|S"); +} diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index 4b53ab38bf..386059a8ed 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -124,3 +124,37 @@ echo $seed;'); assert_eq!(out, "15:integer:15"); } + +/// Verifies eval `Closure::bind()` and `bindTo()` persist `$this` across later calls. +#[test] +fn test_eval_closure_bind_persists_this_and_by_ref_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; +}; + +$bound = $fn->bindTo($box); +$seed = "2"; +echo is_object($bound) ? "O:" : "o:"; +echo $bound($seed, 3) . ":" . gettype($seed) . ":" . $seed . "|"; + +$other = "4"; +echo call_user_func_array($bound, [&$other, 5]) . ":" . gettype($other) . ":" . $other . "|"; + +$staticBound = Closure::bind($fn, $box); +$third = "6"; +echo $staticBound($third, 7) . ":" . gettype($third) . ":" . $third . "|"; + +$static = static function() { return "bad"; }; +echo is_null($static->bindTo($box)) ? "N" : "n";'); +"#, + ); + + assert_eq!(out, "O:15:integer:15|19:integer:19|23:integer:23|N"); +} From 999463be2f5aa5f5d4c418634214777dc90c32de Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 09:24:09 +0200 Subject: [PATCH 0972/1208] Validate eval callable targets --- .../interpreter/builtins/registry/callable.rs | 19 +- .../builtins/registry/callable_validation.rs | 185 +++++++++++++----- .../src/interpreter/builtins/symbols.rs | 2 +- .../src/interpreter/control.rs | 5 +- .../src/interpreter/statements.rs | 16 +- tests/codegen/eval_callables.rs | 93 +++++++++ 6 files changed, 262 insertions(+), 58 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 1ab1bdfbac..5fa77c4866 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -159,7 +159,10 @@ pub(in crate::interpreter) fn eval_object_callable( /// Converts a PHP-visible eval `Closure` object target into the shared callback enum. fn eval_closure_object_target_callable(target: &EvalClosureObjectTarget) -> EvaluatedCallable { match target { - EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named(name.clone()), + EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named { + name: name.clone(), + display_name: name.clone(), + }, EvalClosureObjectTarget::BoundNamed { name, bound_this } => { EvaluatedCallable::BoundClosure { name: name.clone(), @@ -282,9 +285,11 @@ pub(in crate::interpreter) fn eval_string_callable( bridge_scope: None, }); } - Ok(EvaluatedCallable::Named( - callback.trim_start_matches('\\').to_ascii_lowercase(), - )) + let display_name = callback.trim_start_matches('\\').to_string(); + Ok(EvaluatedCallable::Named { + name: display_name.to_ascii_lowercase(), + display_name, + }) } /// Invokes an already normalized callback with source-order positional values. @@ -295,7 +300,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( values: &mut impl RuntimeValueOps, ) -> Result { match callback { - EvaluatedCallable::Named(name) => { + EvaluatedCallable::Named { name, .. } => { if let Some(closure) = context.closure(name).cloned() { return eval_closure_with_evaluated_args( &closure, @@ -393,7 +398,7 @@ fn eval_evaluated_callable_with_call_user_func_values( values: &mut impl RuntimeValueOps, ) -> Result { match callback { - EvaluatedCallable::Named(name) => { + EvaluatedCallable::Named { name, .. } => { eval_named_callable_with_call_user_func_values(name, evaluated_args, context, values) } EvaluatedCallable::BoundClosure { name, bound_this } => { @@ -869,7 +874,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( values: &mut impl RuntimeValueOps, ) -> Result { match callback { - EvaluatedCallable::Named(name) => { + EvaluatedCallable::Named { name, .. } => { eval_callable_with_call_array_args(name, evaluated_args, context, values) } EvaluatedCallable::BoundClosure { name, bound_this } => { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index 27c2a1957c..a706b2d9a3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -11,14 +11,66 @@ use super::super::super::*; +#[derive(Clone, Copy)] +enum EvalCallableValidationError<'a> { + CallUserFunc(&'a str), + ClosureFromCallable, +} + /// Validates callback targets whose PHP errors depend on method metadata. pub(in crate::interpreter) fn eval_validate_call_user_func_callback( callback: &EvaluatedCallable, function_name: &str, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_validate_callback( + callback, + EvalCallableValidationError::CallUserFunc(function_name), + context, + values, + ) +} + +/// Validates `Closure::fromCallable()` callback targets before materializing a closure. +pub(in crate::interpreter) fn eval_validate_closure_from_callable_callback( + callback: &EvaluatedCallable, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_validate_callback( + callback, + EvalCallableValidationError::ClosureFromCallable, + context, + values, + ) +} + +/// Throws the PHP TypeError used when `Closure::fromCallable()` cannot normalize a value. +pub(in crate::interpreter) fn eval_closure_from_callable_type_error( + reason: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_invalid_callback_type_error( + EvalCallableValidationError::ClosureFromCallable, + reason, + context, + values, + ) +} + +/// Validates one normalized callback for a PHP API with invalid-callback TypeErrors. +fn eval_validate_callback( + callback: &EvaluatedCallable, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { match callback { + EvaluatedCallable::Named { name, display_name } => { + eval_validate_named_callable(name, display_name, error, context, values) + } EvaluatedCallable::ObjectMethod { object, method, @@ -29,7 +81,7 @@ pub(in crate::interpreter) fn eval_validate_call_user_func_callback( None => eval_validate_call_user_func_object_method( *object, method, - function_name, + error, context, values, ), @@ -44,22 +96,44 @@ pub(in crate::interpreter) fn eval_validate_call_user_func_callback( None => eval_validate_call_user_func_static_method( class_name, method, - function_name, + error, context, values, ), }, - EvaluatedCallable::Named(_) - | EvaluatedCallable::BoundClosure { .. } - | EvaluatedCallable::InvokableObject { .. } => Ok(()), + EvaluatedCallable::BoundClosure { .. } | EvaluatedCallable::InvokableObject { .. } => { + Ok(()) + } } } +/// Validates string function callables before APIs materialize or dispatch them. +fn eval_validate_named_callable( + name: &str, + display_name: &str, + error: EvalCallableValidationError<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if context.has_closure(name) + || context.has_function(name) + || eval_php_visible_builtin_exists(name) + { + return Ok(()); + } + eval_invalid_callback_type_error( + error, + &format!("function \"{display_name}\" not found or invalid function name"), + context, + values, + ) +} + /// Validates `[$object, "method"]` callbacks before call_user_func dispatch. fn eval_validate_call_user_func_object_method( object: RuntimeCellHandle, method_name: &str, - function_name: &str, + error: EvalCallableValidationError<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { @@ -70,7 +144,7 @@ fn eval_validate_call_user_func_object_method( return eval_validate_call_user_func_native_object_method( object, method_name, - function_name, + error, context, values, ); @@ -102,7 +176,7 @@ fn eval_validate_call_user_func_object_method( &parent, &called_class_name, method_name, - function_name, + error, context, values, ); @@ -112,7 +186,7 @@ fn eval_validate_call_user_func_object_method( return Ok(()); } return eval_call_user_func_missing_method_type_error( - function_name, + error, &called_class_name, method_name, context, @@ -128,7 +202,7 @@ fn eval_validate_call_user_func_object_method( return Ok(()); } eval_call_user_func_method_access_type_error( - function_name, + error, &declaring_class, method.name(), method.visibility(), @@ -141,7 +215,7 @@ fn eval_validate_call_user_func_object_method( fn eval_validate_call_user_func_native_object_method( object: RuntimeCellHandle, method_name: &str, - function_name: &str, + error: EvalCallableValidationError<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { @@ -150,7 +224,7 @@ fn eval_validate_call_user_func_native_object_method( &class_name, &class_name, method_name, - function_name, + error, context, values, ) @@ -161,7 +235,7 @@ fn eval_validate_call_user_func_native_object_method_for_class( class_name: &str, error_class_name: &str, method_name: &str, - function_name: &str, + error: EvalCallableValidationError<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { @@ -177,7 +251,7 @@ fn eval_validate_call_user_func_native_object_method_for_class( return Ok(()); } return eval_call_user_func_missing_method_type_error( - function_name, + error, error_class_name, method_name, context, @@ -193,7 +267,7 @@ fn eval_validate_call_user_func_native_object_method_for_class( return Ok(()); } eval_call_user_func_method_access_type_error( - function_name, + error, &declaring_class, method_name, visibility, @@ -206,7 +280,7 @@ fn eval_validate_call_user_func_native_object_method_for_class( fn eval_validate_call_user_func_static_method( class_name: &str, method_name: &str, - function_name: &str, + error: EvalCallableValidationError<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { @@ -217,7 +291,7 @@ fn eval_validate_call_user_func_static_method( if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { if !method.is_static() { return eval_call_user_func_non_static_method_type_error( - function_name, + error, &declaring_class, method.name(), context, @@ -233,7 +307,7 @@ fn eval_validate_call_user_func_static_method( return Ok(()); } return eval_call_user_func_method_access_type_error( - function_name, + error, &declaring_class, method.name(), method.visibility(), @@ -254,13 +328,13 @@ fn eval_validate_call_user_func_static_method( &parent, &class_name, method_name, - function_name, + error, context, values, ); } return eval_call_user_func_missing_method_type_error( - function_name, + error, &class_name, method_name, context, @@ -270,7 +344,7 @@ fn eval_validate_call_user_func_static_method( eval_validate_call_user_func_native_static_method( &class_name, method_name, - function_name, + error, context, values, ) @@ -280,7 +354,7 @@ fn eval_validate_call_user_func_static_method( fn eval_validate_call_user_func_native_static_method( class_name: &str, method_name: &str, - function_name: &str, + error: EvalCallableValidationError<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { @@ -296,7 +370,7 @@ fn eval_validate_call_user_func_native_static_method( return Ok(()); } return eval_call_user_func_missing_method_type_error( - function_name, + error, class_name, method_name, context, @@ -305,7 +379,7 @@ fn eval_validate_call_user_func_native_static_method( }; if !is_static { return eval_call_user_func_non_static_method_type_error( - function_name, + error, &declaring_class, method_name, context, @@ -321,7 +395,7 @@ fn eval_validate_call_user_func_native_static_method( return Ok(()); } eval_call_user_func_method_access_type_error( - function_name, + error, &declaring_class, method_name, visibility, @@ -335,7 +409,7 @@ fn eval_validate_call_user_func_native_static_method_for_class( class_name: &str, error_class_name: &str, method_name: &str, - function_name: &str, + error: EvalCallableValidationError<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { @@ -351,7 +425,7 @@ fn eval_validate_call_user_func_native_static_method_for_class( return Ok(()); } return eval_call_user_func_missing_method_type_error( - function_name, + error, error_class_name, method_name, context, @@ -360,7 +434,7 @@ fn eval_validate_call_user_func_native_static_method_for_class( }; if !is_static { return eval_call_user_func_non_static_method_type_error( - function_name, + error, &declaring_class, method_name, context, @@ -376,7 +450,7 @@ fn eval_validate_call_user_func_native_static_method_for_class( return Ok(()); } eval_call_user_func_method_access_type_error( - function_name, + error, &declaring_class, method_name, visibility, @@ -432,16 +506,16 @@ fn eval_call_user_func_native_static_magic_callable( ) } -/// Throws call_user_func's TypeError for a missing object or static method callback. +/// Throws the API-specific TypeError for a missing object or static method callback. fn eval_call_user_func_missing_method_type_error( - function_name: &str, + error: EvalCallableValidationError<'_>, class_name: &str, method_name: &str, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_call_user_func_type_error( - function_name, + eval_invalid_callback_type_error( + error, &format!( "class {} does not have a method \"{}\"", class_name.trim_start_matches('\\'), @@ -452,17 +526,17 @@ fn eval_call_user_func_missing_method_type_error( ) } -/// Throws call_user_func's TypeError for an inaccessible method callback. +/// Throws the API-specific TypeError for an inaccessible method callback. fn eval_call_user_func_method_access_type_error( - function_name: &str, + error: EvalCallableValidationError<'_>, class_name: &str, method_name: &str, visibility: EvalVisibility, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_call_user_func_type_error( - function_name, + eval_invalid_callback_type_error( + error, &format!( "cannot access {} method {}::{}()", eval_call_user_func_visibility_label(visibility), @@ -474,16 +548,16 @@ fn eval_call_user_func_method_access_type_error( ) } -/// Throws call_user_func's TypeError for non-static static-method callbacks. +/// Throws the API-specific TypeError for non-static static-method callbacks. fn eval_call_user_func_non_static_method_type_error( - function_name: &str, + error: EvalCallableValidationError<'_>, class_name: &str, method_name: &str, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_call_user_func_type_error( - function_name, + eval_invalid_callback_type_error( + error, &format!( "non-static method {}::{}() cannot be called statically", class_name.trim_start_matches('\\'), @@ -501,16 +575,35 @@ pub(in crate::interpreter) fn eval_call_user_func_type_error( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_throw_type_error( - &format!( - "{}(): Argument #1 ($callback) must be a valid callback, {}", - function_name, reason - ), + eval_invalid_callback_type_error( + EvalCallableValidationError::CallUserFunc(function_name), + reason, context, values, ) } +/// Throws the invalid-callback TypeError for the PHP API currently validating a callback. +fn eval_invalid_callback_type_error( + error: EvalCallableValidationError<'_>, + reason: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let message = match error { + EvalCallableValidationError::CallUserFunc(function_name) => { + format!( + "{}(): Argument #1 ($callback) must be a valid callback, {}", + function_name, reason + ) + } + EvalCallableValidationError::ClosureFromCallable => { + format!("Failed to create closure from callable: {reason}") + } + }; + eval_throw_type_error(&message, context, values) +} + /// Returns PHP's lowercase visibility label used in callback TypeError messages. fn eval_call_user_func_visibility_label(visibility: EvalVisibility) -> &'static str { match visibility { diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 1b8af26e13..d8cbdd4772 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -1000,7 +1000,7 @@ fn eval_callable_probe_exists( values: &mut impl RuntimeValueOps, ) -> Result { match callback { - EvaluatedCallable::Named(name) => { + EvaluatedCallable::Named { name, .. } => { Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) } EvaluatedCallable::BoundClosure { name, .. } => Ok(context.has_closure(name)), diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index b181d4ee0c..fdc9769a26 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -85,7 +85,10 @@ pub(super) enum EvalByRefBindingMode<'a> { /// One already evaluated PHP callback supported by the eval dispatcher. pub(super) enum EvaluatedCallable { - Named(String), + Named { + name: String, + display_name: String, + }, BoundClosure { name: String, bound_this: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 9ed1ce3fdb..47ea271ad1 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7463,8 +7463,18 @@ fn eval_closure_from_callable( ) -> Result { let mut args = bind_evaluated_function_args(&[String::from("callback")], evaluated_args)?; let callback = args.pop().ok_or(EvalStatus::RuntimeFatal)?; - let callable = eval_callable(callback, context, values)?; - eval_validate_call_user_func_callback(&callable, "Closure::fromCallable", context, values)?; + let callable = match eval_callable(callback, context, values) { + Ok(callable) => callable, + Err(EvalStatus::UnsupportedConstruct) if values.type_tag(callback)? == EVAL_TAG_OBJECT => { + return eval_closure_from_callable_type_error( + "no array or string given", + context, + values, + ); + } + Err(status) => return Err(status), + }; + eval_validate_closure_from_callable_callback(&callable, context, values)?; let target = eval_closure_object_target_from_callable(callable); eval_closure_object_from_target(target, context, values) } @@ -7474,7 +7484,7 @@ fn eval_closure_object_target_from_callable( callable: EvaluatedCallable, ) -> EvalClosureObjectTarget { match callable { - EvaluatedCallable::Named(name) => EvalClosureObjectTarget::Named(name), + EvaluatedCallable::Named { name, .. } => EvalClosureObjectTarget::Named(name), EvaluatedCallable::BoundClosure { name, bound_this } => { EvalClosureObjectTarget::BoundNamed { name, bound_this } } diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 02b5289cf9..de6827308f 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -204,6 +204,99 @@ foreach ($callbacks as $index => $cb) { ); } +/// Verifies eval string callback APIs reject missing functions with PHP TypeErrors. +#[test] +fn test_eval_callable_validation_missing_string_callables_raise_type_errors() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|"; +try { + call_user_func_array("OtherMissingEvalCallback", []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + Closure::fromCallable("ThirdMissingEvalCallback"); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + + assert_eq!( + out, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, function \"MiSsInG_Eval_Callback\" not found or invalid function name|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, function \"OtherMissingEvalCallback\" not found or invalid function name|\ +TypeError:Failed to create closure from callable: function \"ThirdMissingEvalCallback\" not found or invalid function name" + ); +} + +/// Verifies `Closure::fromCallable()` rejects invalid object and method targets. +#[test] +fn test_eval_callable_validation_closure_from_callable_rejects_invalid_targets() { + let out = compile_and_run( + r#"getMessage(); +} +echo "|"; +try { + Closure::fromCallable([new EvalFromCallableMissing(), "MiSsInG"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + Closure::fromCallable([new EvalFromCallablePrivate(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + Closure::fromCallable(["EvalFromCallableInstance", "inst"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +}'); +"#, + ); + + assert_eq!( + out, + "TypeError:Failed to create closure from callable: no array or string given|\ +TypeError:Failed to create closure from callable: class EvalFromCallableMissing does not have a method \"MiSsInG\"|\ +TypeError:Failed to create closure from callable: cannot access private method EvalFromCallablePrivate::hidden()|\ +TypeError:Failed to create closure from callable: non-static method EvalFromCallableInstance::inst() cannot be called statically" + ); +} + /// Verifies `Closure::fromCallable()` preserves by-ref writeback for AOT call targets. #[test] fn test_eval_closure_from_callable_preserves_aot_by_ref_writeback() { From 13f4156f55a51d2e8b4b169fbbb7a1ed845979b2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 09:58:02 +0200 Subject: [PATCH 0973/1208] Validate eval first-class callable targets --- .../src/interpreter/expressions.rs | 576 ++++++++++++++---- .../src/interpreter/statements.rs | 2 +- docs/php/eval.md | 5 +- tests/codegen/eval.rs | 3 +- tests/codegen/eval_callables.rs | 151 +++++ 5 files changed, 628 insertions(+), 109 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index ee410f8854..5d46d84948 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -914,63 +914,153 @@ fn eval_method_callable_expr( ) -> Result { let object = eval_expr(object, context, scope, values)?; let method = eval_dynamic_member_name(method, context, scope, values)?; - let (native_class, bridge_scope, called_class) = if let Some(( - native_class, - bridge_scope, - called_class, - )) = - eval_method_callable_native_dispatch(object, &method, context, values)? - { - (Some(native_class), Some(bridge_scope), Some(called_class)) - } else { - (None, None, None) - }; - eval_closure_object_expr( - EvalClosureObjectTarget::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - }, - context, - values, - ) + let target = eval_method_callable_target(object, method, context, values)?; + eval_closure_object_expr(target, context, values) } -/// Finds inherited AOT method dispatch metadata captured by an object method callable. -fn eval_method_callable_native_dispatch( +/// Validates and builds the retained target for an object method first-class callable. +fn eval_method_callable_target( object: RuntimeCellHandle, - method_name: &str, - context: &ElephcEvalContext, + method_name: String, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result { let Ok(identity) = values.object_identity(object) else { - return Ok(None); + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: None, + native_class: None, + bridge_scope: None, + }); }; let Some(class) = context.dynamic_object_class(identity) else { - return Ok(None); + let class_name = runtime_object_class_name(object, values)?; + return eval_native_object_method_callable_target( + object, + &class_name, + &class_name, + method_name, + context, + values, + ); }; - let called_class_name = class.name(); - if eval_dynamic_method_for_call(called_class_name, method_name, context).is_some() { - return Ok(None); + let called_class_name = class.name().to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_method_for_call(&called_class_name, &method_name, context) + { + if method.is_abstract() { + return eval_first_class_abstract_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + && !eval_instance_magic_callable_for_class(&called_class_name, context) + { + return eval_first_class_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name), + native_class: None, + bridge_scope: None, + }); } - let Some(parent) = context.class_native_parent_name(called_class_name) else { - return Ok(None); - }; - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values)? + if let Some(parent) = context.class_native_parent_name(&called_class_name) { + return eval_native_object_method_callable_target( + object, + &parent, + &called_class_name, + method_name, + context, + values, + ); + } + if eval_instance_magic_callable_for_class(&called_class_name, context) { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name), + native_class: None, + bridge_scope: None, + }); + } + eval_first_class_undefined_method_error(&called_class_name, &method_name, context, values) +} + +/// Validates generated/AOT object-method first-class callable metadata. +fn eval_native_object_method_callable_target( + object: RuntimeCellHandle, + native_class: &str, + called_class_name: &str, + method_name: String, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(native_class, &method_name, context, values)? else { - return Ok(None); + if eval_native_instance_magic_callable_for_class(native_class, context, values)? + || !values.class_exists(native_class)? + { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + called_class_name, + &method_name, + context, + values, + ); }; - if is_static || is_abstract { - return Ok(None); + if is_abstract { + return eval_first_class_abstract_method_error( + &declaring_class, + &method_name, + context, + values, + ); } - // First-class callables may outlive this scope, so capture only after access is valid now. if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return Ok(None); + if eval_native_instance_magic_callable_for_class(native_class, context, values)? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_method_access_error( + &declaring_class, + &method_name, + visibility, + context, + values, + ); } - Ok(Some((parent, declaring_class, called_class_name.to_string()))) + Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: Some(native_class.to_string()), + bridge_scope: Some(declaring_class), + }) } /// Materializes a first-class static method callable while retaining late-static metadata. @@ -983,20 +1073,15 @@ fn eval_static_method_callable_expr( ) -> Result { let receiver = resolve_eval_static_method_receiver(class_name, context)?; let method = eval_dynamic_member_name(method, context, scope, values)?; - let native_dispatch = eval_static_method_callable_native_dispatch( - &receiver.dispatch_class, - &method, - context, - values, - )?; - eval_static_method_closure_object( + let target = eval_static_method_callable_target( receiver.dispatch_class, method, Some(receiver.called_class), - native_dispatch, + Some(scope), context, values, - ) + )?; + eval_closure_object_expr(target, context, values) } /// Materializes a runtime-class static first-class callable as a PHP `Closure` object. @@ -1011,88 +1096,367 @@ fn eval_dynamic_static_method_callable_expr( let class_name = eval_dynamic_class_name(class_name, context, values)?; let receiver = resolve_eval_static_method_receiver(&class_name, context)?; let method = eval_dynamic_member_name(method, context, scope, values)?; - let native_dispatch = eval_static_method_callable_native_dispatch( - &receiver.dispatch_class, - &method, - context, - values, - )?; - eval_static_method_closure_object( + let target = eval_static_method_callable_target( receiver.dispatch_class, method, Some(receiver.called_class), - native_dispatch, + Some(scope), context, values, - ) + )?; + eval_closure_object_expr(target, context, values) } -/// Materializes a static-method callable target as a PHP-visible `Closure` object. -fn eval_static_method_closure_object( - class_name: String, - method: String, +/// Validates and builds the retained target for a static-method first-class callable. +fn eval_static_method_callable_target( + dispatch_class: String, + method_name: String, called_class: Option, - native_dispatch: Option<(String, String)>, + lexical_scope: Option<&ElephcEvalScope>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result { - let (native_class, bridge_scope) = native_dispatch - .map(|(native_class, bridge_scope)| (Some(native_class), Some(bridge_scope))) - .unwrap_or((None, None)); - eval_closure_object_expr( - EvalClosureObjectTarget::StaticMethod { - class_name, - method, +) -> Result { + if let Some((declaring_class, method)) = + eval_dynamic_static_method_for_call(&dispatch_class, &method_name, context) + { + if method.is_abstract() { + return eval_first_class_abstract_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + && !eval_static_magic_callable_for_class(&dispatch_class, context) + { + return eval_first_class_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + if !method.is_static() { + if let Some(object) = eval_static_syntax_instance_receiver( + &dispatch_class, + lexical_scope, + context, + values, + )? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_non_static_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, called_class, - native_class, - bridge_scope, - }, + native_class: None, + bridge_scope: None, + }); + } + if context.has_class(&dispatch_class) { + if let Some(parent) = context.class_native_parent_name(&dispatch_class) { + return eval_native_static_method_callable_target( + dispatch_class, + parent, + method_name, + called_class, + lexical_scope, + context, + values, + ); + } + if eval_static_magic_callable_for_class(&dispatch_class, context) { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); + } + if context.has_interface(&dispatch_class) + || context.has_trait(&dispatch_class) + || context.has_enum(&dispatch_class) + { + if eval_static_magic_callable_for_class(&dispatch_class, context) { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); + } + eval_native_static_method_callable_target( + dispatch_class.clone(), + dispatch_class, + method_name, + called_class, + lexical_scope, context, values, ) } -/// Finds inherited or direct AOT static dispatch metadata captured by a static callable. -fn eval_static_method_callable_native_dispatch( - dispatch_class: &str, - method_name: &str, - context: &ElephcEvalContext, +/// Validates generated/AOT static-method first-class callable metadata. +fn eval_native_static_method_callable_target( + dispatch_class: String, + native_class: String, + method_name: String, + called_class: Option, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if context.class_method(dispatch_class, method_name).is_some() { - return Ok(None); - } - let native_class = if context.has_class(dispatch_class) { - let Some(parent) = context.class_native_parent_name(dispatch_class) else { - return Ok(None); - }; - parent - } else if context.has_interface(dispatch_class) - || context.has_trait(dispatch_class) - || context.has_enum(dispatch_class) - { - return Ok(None); - } else { - dispatch_class.trim_start_matches('\\').to_string() - }; +) -> Result { let Some((declaring_class, visibility, is_static, is_abstract)) = eval_aot_method_dispatch_metadata_in_hierarchy( &native_class, - method_name, + &method_name, context, values, )? else { - return Ok(None); + if eval_native_static_magic_callable_for_class(&native_class, context, values)? + || !values.class_exists(&native_class)? + { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); }; - if !is_static || is_abstract { - return Ok(None); + if is_abstract { + return eval_first_class_abstract_method_error( + &declaring_class, + &method_name, + context, + values, + ); } - // First-class callables may outlive this scope, so capture only after access is valid now. if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return Ok(None); + if eval_native_static_magic_callable_for_class(&native_class, context, values)? { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_method_access_error( + &declaring_class, + &method_name, + visibility, + context, + values, + ); + } + if !is_static { + if let Some(object) = eval_static_syntax_instance_receiver( + &dispatch_class, + lexical_scope, + context, + values, + )? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class, + native_class: Some(native_class), + bridge_scope: Some(declaring_class), + }); + } + return eval_first_class_non_static_method_error( + &declaring_class, + &method_name, + context, + values, + ); + } + Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: Some(native_class), + bridge_scope: Some(declaring_class), + }) +} + +/// Returns whether an eval class has an instance magic-call fallback for a callable. +fn eval_instance_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a static magic-call fallback for a callable. +fn eval_static_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) +} + +/// Returns whether an AOT class has an instance magic-call fallback for a callable. +fn eval_native_instance_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether an AOT class has a static magic-call fallback for a callable. +fn eval_native_static_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Throws PHP's first-class callable error for an inaccessible method. +fn eval_first_class_method_access_error( + declaring_class: &str, + method_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} method {}::{}() from {}", + eval_callable_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + method_name, + eval_callable_scope_label(context) + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for an instance method used statically. +fn eval_first_class_non_static_method_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Non-static method {}::{}() cannot be called statically", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for an abstract method target. +fn eval_first_class_abstract_method_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot call abstract method {}::{}()", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for a missing method target. +fn eval_first_class_undefined_method_error( + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to undefined method {}::{}()", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Returns the current PHP scope label used in callable access errors. +fn eval_callable_scope_label(context: &ElephcEvalContext) -> String { + context.current_class_scope().map_or_else( + || String::from("global scope"), + |class_name| format!("scope {}", class_name.trim_start_matches('\\')), + ) +} + +/// Returns PHP's lowercase visibility label for callable access errors. +fn eval_callable_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", } - Ok(Some((native_class, declaring_class))) } /// Evaluates a variable or expression callable and dispatches it with source-order arguments. diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 47ea271ad1..e684a377cc 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7838,7 +7838,7 @@ fn eval_native_static_syntax_method_result( } /// Returns `$this` when PHP permits static-call syntax to target an instance method. -fn eval_static_syntax_instance_receiver( +pub(in crate::interpreter) fn eval_static_syntax_instance_receiver( class_name: &str, lexical_scope: Option<&ElephcEvalScope>, context: &ElephcEvalContext, diff --git a/docs/php/eval.md b/docs/php/eval.md index 45081ecd64..ca0e369815 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -128,7 +128,10 @@ eval-declared functions, and registered AOT functions. First-class callable syntax such as `strlen(...)`, `$object->method(...)`, `ClassName::method(...)`, and `$invokable(...)` materializes eval callback values as PHP-visible `Closure` objects that can be invoked through -`$callback(...)`, `call_user_func()`, and `call_user_func_array()`. +`$callback(...)`, `call_user_func()`, and `call_user_func_array()`. Method +targets are validated when the first-class callable is created, including +missing, inaccessible, non-static static-syntax, and magic-call fallback cases; +static-syntax instance methods capture `$this` when PHP permits that form. Namespaced function callables follow PHP's global fallback rule when the namespaced function is not visible. `Closure::fromCallable()` accepts the same supported string, callable-array, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index a8fdbe440e..51c1fba934 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -12655,7 +12655,8 @@ echo $mapped[0] . $mapped[1] . ":"; $filtered = array_filter([1, 2, 3, 4], $box->keep(...)); echo count($filtered) . ":"; echo array_reduce([1, 2], $box->sum(...), 0) . ":"; -array_walk(["a" => 1], $box->show(...)); + $walkedForWalk = ["a" => 1]; + array_walk($walkedForWalk, $box->show(...)); echo ":"; $sorted = [3, 1, 2]; usort($sorted, EvalFirstClassCallableBase::compareDesc(...)); diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index de6827308f..b0498e3243 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -161,6 +161,157 @@ return $f("1") . ":" . $m("2") . ":" . $s("3") . ":" . $ds("4") . ":" . $i("5"); ); } +/// Verifies eval first-class callables reject invalid method targets at creation time. +#[test] +fn test_eval_first_class_callable_validation_rejects_invalid_method_targets() { + let out = compile_and_run( + r#"missing(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + $box->hidden(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalFirstClassInvalidTargets::missing(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalFirstClassInvalidTargets::inst(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + EvalFirstClassInvalidTargets::secret(...); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +$ok = EvalFirstClassInvalidTargets::ok(...); +echo $ok();'); +"#, + ); + + assert_eq!( + out, + "Error:Call to undefined method EvalFirstClassInvalidTargets::missing()|\ +Error:Call to private method EvalFirstClassInvalidTargets::hidden() from global scope|\ +Error:Call to undefined method EvalFirstClassInvalidTargets::missing()|\ +Error:Non-static method EvalFirstClassInvalidTargets::inst() cannot be called statically|\ +Error:Call to private method EvalFirstClassInvalidTargets::secret() from global scope|OK" + ); +} + +/// Verifies eval first-class callables preserve PHP magic-method fallback. +#[test] +fn test_eval_first_class_callable_validation_preserves_magic_fallback() { + let out = compile_and_run( + r#"hidden(...); +echo $hidden("a") . "|"; +$missing = $box->missing(...); +echo $missing("b") . "|"; +$secret = EvalFirstClassMagicTargets::secret(...); +echo $secret("c") . "|"; +$staticMissing = EvalFirstClassMagicTargets::missingStatic(...); +echo $staticMissing("d");'); +"#, + ); + + assert_eq!(out, "I:hidden:a|I:missing:b|S:secret:c|S:missingStatic:d"); +} + +/// Verifies `self::method(...)` inside an instance eval method captures `$this`. +#[test] +fn test_eval_first_class_callable_validation_static_syntax_instance_method_captures_this() { + let out = compile_and_run( + r#"base + $value; + } + + public static function makeStatic() { + try { + self::add(...); + return "bad"; + } catch (Error $e) { + return get_class($e) . ":" . $e->getMessage(); + } + } +} + +$box = new EvalFirstClassStaticSyntaxThis(); +echo $box->make() . "|"; +echo EvalFirstClassStaticSyntaxThis::makeStatic();'); +"#, + ); + + assert_eq!( + out, + "12|Error:Non-static method EvalFirstClassStaticSyntaxThis::add() cannot be called statically" + ); +} + /// Verifies `Closure::fromCallable()` normalizes eval string and array callables to Closure objects. #[test] fn test_eval_closure_from_callable_normalizes_string_and_array_callables() { From 1a3a54225b1120b4e283c78b91a84266eab17f9c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 10:29:37 +0200 Subject: [PATCH 0974/1208] Support eval is_callable callable names --- .../interpreter/builtins/registry/binding.rs | 3 +- .../builtins/registry/dispatch/symbols.rs | 3 +- .../builtins/registry/dynamic_mutation.rs | 7 + .../builtins/registry/signature.rs | 3 + .../src/interpreter/builtins/symbols.rs | 226 +++++++++++++++++- .../src/interpreter/expressions.rs | 3 + docs/php/eval.md | 3 + tests/builtin_parity_tests.rs | 2 +- tests/codegen/eval_callables.rs | 78 ++++++ 9 files changed, 320 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index e2648d27a0..aefe64a42e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -158,7 +158,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "boolval" | "empty" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_null" | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" - | "is_scalar" | "is_callable" | "strval" => Some(&["value"]), + | "is_scalar" | "strval" => Some(&["value"]), + "is_callable" => Some(&["value", "syntax_only", "callable_name"]), "buffer_new" => Some(&["length"]), "buffer_len" | "buffer_free" => Some(&["buffer"]), "is_finite" | "is_infinite" | "is_nan" => Some(&["num"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs index 67db18b8c6..882c38e504 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs @@ -41,12 +41,13 @@ pub(in crate::interpreter) fn eval_symbols_builtin_with_values( }; eval_spl_object_identity_result(name, *object, values)? } - "function_exists" | "is_callable" => { + "function_exists" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_function_probe_result(name, *value, context, values)? } + "is_callable" => eval_is_callable_with_values(evaluated_args, context, values)?, "empty" => eval_empty_result(evaluated_args, values)?, "isset" => eval_isset_result(evaluated_args, values)?, "unset" => eval_unset_result(evaluated_args, values)?, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs index 281a4d2dec..f17750ca6c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -38,6 +38,13 @@ pub(in crate::interpreter) fn eval_mutating_builtin_with_call_array_args( } "preg_match" => eval_dynamic_preg_match_call(evaluated_args, context, values)?, "preg_match_all" => eval_dynamic_preg_match_all_call(evaluated_args, context, values)?, + "is_callable" => { + Some(eval_is_callable_call_with_evaluated_args( + evaluated_args, + context, + values, + )?) + } "flock" => eval_dynamic_flock_call(evaluated_args, context, values)?, "fsockopen" | "pfsockopen" => { eval_dynamic_fsockopen_call(evaluated_args, context, values)? diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 30bb6514aa..6f03cccc3b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -69,6 +69,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "getdate" | "hrtime" => optional(params, 0), "header" => optional(params, 1), "http_response_code" => optional(params, 0), + "is_callable" => optional_by_ref(params, 1, &["callable_name"]), "localtime" => optional(params, 0), "microtime" | "php_uname" | "readline" | "umask" | "exit" | "die" => { optional(params, 0) @@ -174,6 +175,8 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("header", 2) => Int(0), ("hrtime", 0) => Bool(false), ("http_response_code", 0) => Int(0), + ("is_callable", 1) => Bool(false), + ("is_callable", 2) => Null, ("localtime", 0) => Null, ("localtime", 1) => Bool(false), ("microtime", 0) => Bool(false), diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index d8cbdd4772..7103b973d1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -19,11 +19,76 @@ pub(in crate::interpreter) fn eval_builtin_function_probe( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_function_probe_result(name, value, context, values) + match name { + "function_exists" => { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_function_probe_result(name, value, context, values) + } + "is_callable" => eval_builtin_is_callable(args, context, scope, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates `is_callable()` over full eval call metadata so `$callable_name` stays writable. +pub(in crate::interpreter) fn eval_builtin_is_callable_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_is_callable_call_with_evaluated_args(&evaluated_args, context, values) +} + +/// Evaluates `is_callable()` from already evaluated arguments that may retain ref targets. +pub(in crate::interpreter) fn eval_is_callable_call_with_evaluated_args( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["value", "syntax_only", "callable_name"], + evaluated_args, + false, + )?; + let value = required_evaluated_ref_arg(&bound, 0)?; + let syntax_only = optional_evaluated_ref_arg(&bound, 1) + .map(|arg| values.truthy(arg.value)) + .transpose()? + .unwrap_or(false); + let callable_name_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + eval_is_callable_result( + value.value, + syntax_only, + callable_name_target.as_ref(), + context, + values, + ) +} + +/// Evaluates by-value dynamic `is_callable()` arguments without `$callable_name` writeback. +pub(in crate::interpreter) fn eval_is_callable_with_values( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_is_callable_result(*value, false, None, context, values), + [value, syntax_only] => { + let syntax_only = values.truthy(*syntax_only)?; + eval_is_callable_result(*value, syntax_only, None, context, values) + } + [value, syntax_only, _callable_name] => { + let syntax_only = values.truthy(*syntax_only)?; + eval_is_callable_result(*value, syntax_only, None, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } } /// Evaluates `function_exists()` and `is_callable()` from materialized arguments. @@ -44,6 +109,43 @@ pub(in crate::interpreter) fn eval_function_probe_result( values.bool_value(exists) } +/// Evaluates positional `is_callable()` arguments inside an eval fragment. +fn eval_builtin_is_callable( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_is_callable_result(value, false, None, context, values) + } + [value, syntax_only] => { + let value = eval_expr(value, context, scope, values)?; + let syntax_only = eval_expr(syntax_only, context, scope, values)?; + let syntax_only = values.truthy(syntax_only)?; + eval_is_callable_result(value, syntax_only, None, context, values) + } + [value, syntax_only, callable_name] => { + let value = eval_expr(value, context, scope, values)?; + let syntax_only = eval_expr(syntax_only, context, scope, values)?; + let syntax_only = values.truthy(syntax_only)?; + let (_, callable_name_target) = + eval_call_arg_value(callable_name, context, scope, values)?; + let callable_name_target = callable_name_target.ok_or(EvalStatus::RuntimeFatal)?; + eval_is_callable_result( + value, + syntax_only, + Some(&callable_name_target), + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Evaluates `define(name, value)` for eval dynamic constant-name registration. pub(in crate::interpreter) fn eval_builtin_define( args: &[EvalExpr], @@ -993,6 +1095,120 @@ fn eval_is_callable_value( eval_callable_probe_exists(&callback, context, values) } +/// Evaluates `is_callable()` and writes PHP's display callable name when requested. +fn eval_is_callable_result( + value: RuntimeCellHandle, + syntax_only: bool, + callable_name_target: Option<&EvalReferenceTarget>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callable_name = callable_name_target + .map(|_| eval_callable_display_name(value, context, values)) + .transpose()?; + let callable = if syntax_only { + eval_is_callable_syntax_only(value, context, values)? + } else { + eval_is_callable_value(value, context, values)? + }; + if let Some((target, name)) = callable_name_target.zip(callable_name.as_deref()) { + let name = values.string(name)?; + eval_write_direct_ref_target( + target, + name, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + values.bool_value(callable) +} + +/// Returns PHP's syntax-only callable result without requiring the target to exist. +fn eval_is_callable_syntax_only( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_STRING { + return Ok(true); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT { + return eval_is_callable_value(value, context, values); + } + if values.is_array_like(value)? { + return eval_callable_array_display_name(value, context, values).map(|name| name.is_some()); + } + Ok(false) +} + +/// Builds PHP's `$callable_name` output for one probed callable value. +fn eval_callable_display_name( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_STRING { + let bytes = values.string_bytes(value)?; + return String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT { + let class_name = eval_callable_object_class_name(value, context, values)?; + return Ok(format!("{class_name}::__invoke")); + } + if values.is_array_like(value)? { + return Ok(eval_callable_array_display_name(value, context, values)? + .unwrap_or_else(|| String::from("Array"))); + } + let string = values.cast_string(value)?; + let bytes = values.string_bytes(string); + values.release(string)?; + String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Builds PHP's `$callable_name` output for a syntactically valid callable array. +fn eval_callable_array_display_name( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(value)? != 2 { + return Ok(None); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(value, zero)?; + let method = values.array_get(value, one)?; + if values.type_tag(method)? != EVAL_TAG_STRING { + return Ok(None); + } + let method = + String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; + let receiver_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_callable_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => String::from_utf8(values.string_bytes(receiver)?) + .map_err(|_| EvalStatus::RuntimeFatal)?, + _ => return Ok(None), + }; + Ok(Some(format!("{receiver_name}::{method}"))) +} + +/// Returns the PHP-visible class name used when formatting callable object probes. +fn eval_callable_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if context.closure_object_target(identity).is_some() { + return Ok(String::from("Closure")); + } + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + runtime_object_class_name(object, values) +} + /// Returns whether a normalized eval callback has an invokable target. fn eval_callable_probe_exists( callback: &EvaluatedCallable, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 5d46d84948..b1664ba088 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -785,6 +785,9 @@ pub(in crate::interpreter) fn eval_call( if name == "preg_match_all" { return eval_builtin_preg_match_all_call(args, context, scope, values); } + if name == "is_callable" { + return eval_builtin_is_callable_call(args, context, scope, values); + } if matches!(name, "fsockopen" | "pfsockopen") { return eval_builtin_fsockopen_call(args, context, scope, values); } diff --git a/docs/php/eval.md b/docs/php/eval.md index ca0e369815..0b9b7cc93e 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -124,6 +124,9 @@ by-reference parameters. String-variable and expression callable calls such as `$fn(...)` and `$callbacks[0](...)` share the eval callable dispatcher for supported builtins, eval-declared functions, and registered AOT functions. +Inside eval, `is_callable()` also supports PHP's optional `$syntax_only` probe +and by-reference `$callable_name` output for string, array, object, and +`Closure` callable forms. First-class callable syntax such as `strlen(...)`, `$object->method(...)`, `ClassName::method(...)`, and `$invokable(...)` materializes eval callback diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 813d07f087..8299b90dc2 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -70,7 +70,7 @@ const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[ ]; /// Eval supports extra optional by-reference parameters before the static backend does. -const EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["preg_match_all"]; +const EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["is_callable", "preg_match_all"]; /// Eval supports variadic debug output before the static backend does. const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index b0498e3243..7439c6ef76 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -312,6 +312,84 @@ echo EvalFirstClassStaticSyntaxThis::makeStatic();'); ); } +/// Verifies eval `is_callable()` supports syntax-only probes and callable-name writeback. +#[test] +fn test_eval_is_callable_supports_syntax_only_and_callable_name_writeback() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 11:49:26 +0200 Subject: [PATCH 0975/1208] Support eval scoped special callable arrays --- .../src/interpreter/builtins/arrays/core.rs | 87 ++++++++-- .../builtins/arrays/filters/filter.rs | 39 ++++- .../builtins/arrays/filters/iterator.rs | 4 +- .../builtins/arrays/sort/direct.rs | 9 +- .../interpreter/builtins/arrays/sort/user.rs | 14 +- .../src/interpreter/builtins/regex/replace.rs | 16 +- .../interpreter/builtins/registry/callable.rs | 154 +++++++++++++++++- .../src/interpreter/builtins/symbols.rs | 43 +++-- .../src/interpreter/statements.rs | 14 +- tests/codegen/eval_callables.rs | 130 +++++++++++++++ 10 files changed, 469 insertions(+), 41 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs index 8e8d1afff5..db291431ec 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs @@ -234,7 +234,7 @@ pub(in crate::interpreter) fn eval_builtin_array_map( for array in arrays { evaluated_arrays.push(eval_expr(array, context, scope, values)?); } - eval_array_map_result(callback, &evaluated_arrays, context, values) + eval_array_map_result_from_scope(callback, &evaluated_arrays, Some(scope), context, values) } /// Maps one eval array with PHP key preservation for the one-array form. @@ -243,14 +243,36 @@ pub(in crate::interpreter) fn eval_array_map_result( arrays: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_map_result_from_scope(callback, arrays, None, context, values) +} + +/// Maps one or more eval arrays with optional lexical scope for callback names. +fn eval_array_map_result_from_scope( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let [array] = arrays else { - return eval_array_map_variadic_result(callback, arrays, context, values); + return eval_array_map_variadic_result_from_scope( + callback, + arrays, + lexical_scope, + context, + values, + ); }; let callback = if values.is_null(callback)? { None } else { - Some(eval_callable(callback, context, values)?) + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) }; let len = values.array_len(*array)?; let mut result = values.assoc_new(len)?; @@ -267,10 +289,11 @@ pub(in crate::interpreter) fn eval_array_map_result( Ok(result) } -/// Maps multiple eval arrays with PHP's reindexed and null-padded variadic behavior. -pub(in crate::interpreter) fn eval_array_map_variadic_result( +/// Maps multiple eval arrays with optional lexical scope for callback names. +fn eval_array_map_variadic_result_from_scope( callback: RuntimeCellHandle, arrays: &[RuntimeCellHandle], + lexical_scope: Option<&ElephcEvalScope>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -280,7 +303,12 @@ pub(in crate::interpreter) fn eval_array_map_variadic_result( let callback = if values.is_null(callback)? { None } else { - Some(eval_callable(callback, context, values)?) + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) }; let mut lengths = Vec::with_capacity(arrays.len()); let mut max_len = 0; @@ -347,7 +375,7 @@ pub(in crate::interpreter) fn eval_builtin_array_reduce( } _ => return Err(EvalStatus::RuntimeFatal), }; - eval_array_reduce_result(array, callback, initial, context, values) + eval_array_reduce_result_from_scope(array, callback, initial, Some(scope), context, values) } /// Reduces one eval array by invoking a callable with carry and item cells. @@ -358,7 +386,19 @@ pub(in crate::interpreter) fn eval_array_reduce_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable(callback, context, values)?; + eval_array_reduce_result_from_scope(array, callback, initial, None, context, values) +} + +/// Reduces one eval array with optional lexical scope for callback names. +fn eval_array_reduce_result_from_scope( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; let len = values.array_len(array)?; let mut carry = initial; for position in 0..len { @@ -379,7 +419,7 @@ pub(in crate::interpreter) fn eval_builtin_array_walk_call( ) -> Result { let (array, array_target, callback) = eval_array_walk_direct_args(args, context, scope, values)?; - eval_array_walk_ref_result(array, array_target, callback, context, values) + eval_array_walk_ref_result_from_scope(array, array_target, callback, Some(scope), context, values) } /// Evaluates and binds direct `array_walk()` arguments in PHP source order. @@ -444,7 +484,19 @@ pub(in crate::interpreter) fn eval_array_walk_ref_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable(callback, context, values)?; + eval_array_walk_ref_result_from_scope(array, array_target, callback, None, context, values) +} + +/// Walks one writable eval array with optional lexical scope for callback names. +fn eval_array_walk_ref_result_from_scope( + array: RuntimeCellHandle, + array_target: EvalReferenceTarget, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; let len = values.array_len(array)?; for position in 0..len { let current_array = eval_reference_target_value(&array_target, context, values)?; @@ -483,7 +535,7 @@ pub(in crate::interpreter) fn eval_builtin_array_walk( }; let array = eval_expr(array, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - eval_array_walk_result(array, callback, context, values) + eval_array_walk_result_from_scope(array, callback, Some(scope), context, values) } /// Walks one eval array by invoking a callable with value and key cells. @@ -493,7 +545,18 @@ pub(in crate::interpreter) fn eval_array_walk_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable(callback, context, values)?; + eval_array_walk_result_from_scope(array, callback, None, context, values) +} + +/// Walks one eval array with optional lexical scope for callback names. +fn eval_array_walk_result_from_scope( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; let len = values.array_len(array)?; for position in 0..len { let key = values.array_iter_key(array, position)?; diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs index 54b410f198..c90dcd2736 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs @@ -21,18 +21,32 @@ pub(in crate::interpreter) fn eval_builtin_array_filter( match args { [array] => { let array = eval_expr(array, context, scope, values)?; - eval_array_filter_result(array, None, None, context, values) + eval_array_filter_result_from_scope(array, None, None, Some(scope), context, values) } [array, callback] => { let array = eval_expr(array, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - eval_array_filter_result(array, Some(callback), None, context, values) + eval_array_filter_result_from_scope( + array, + Some(callback), + None, + Some(scope), + context, + values, + ) } [array, callback, mode] => { let array = eval_expr(array, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; let mode = eval_expr(mode, context, scope, values)?; - eval_array_filter_result(array, Some(callback), Some(mode), context, values) + eval_array_filter_result_from_scope( + array, + Some(callback), + Some(mode), + Some(scope), + context, + values, + ) } _ => Err(EvalStatus::RuntimeFatal), } @@ -45,10 +59,27 @@ pub(in crate::interpreter) fn eval_array_filter_result( mode: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_filter_result_from_scope(array, callback, mode, None, context, values) +} + +/// Filters eval array entries with optional lexical scope for callback names. +fn eval_array_filter_result_from_scope( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let callback = match callback { Some(callback) if !values.is_null(callback)? => { - Some(eval_callable(callback, context, values)?) + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) } _ => None, }; diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs index 11c9d4cefe..c13a75a8ea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs @@ -23,13 +23,13 @@ pub(in crate::interpreter) fn eval_builtin_iterator_apply( [iterator, callback] => { let iterator = eval_expr(iterator, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, context, values)?; + let callback = eval_callable_from_scope(callback, context, scope, values)?; eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) } [iterator, callback, callback_args] => { let iterator = eval_expr(iterator, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable(callback, context, values)?; + let callback = eval_callable_from_scope(callback, context, scope, values)?; let callback_args = eval_expr(callback_args, context, scope, values)?; let callback_args = eval_iterator_apply_arg_values(callback_args, context, values)?; eval_iterator_apply_result(iterator, &callback, callback_args, context, values) diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs index 7222bbb80a..cdf0ca81ba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs @@ -39,7 +39,14 @@ pub(in crate::interpreter) fn eval_builtin_user_sort_call( ) -> Result { let (array, target, callback) = eval_user_sort_direct_args(args, context, scope, values)?; - let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + let replacement = eval_user_sort_replacement_from_scope( + name, + array, + callback, + Some(scope), + context, + values, + )?; let result = values.bool_value(true)?; eval_write_direct_ref_target(&target, replacement, context, values, None)?; Ok(result) diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs index e7421e5429..7fd4d32ce9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs @@ -41,7 +41,19 @@ pub(in crate::interpreter) fn eval_user_sort_replacement( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_callable(callback, context, values)?; + eval_user_sort_replacement_from_scope(name, array, callback, None, context, values) +} + +/// Builds the sorted replacement array with optional lexical scope for callback names. +pub(in crate::interpreter) fn eval_user_sort_replacement_from_scope( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; let mut entries = eval_user_sort_entries(array, values)?; eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; if name == "usort" { diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs b/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs index e429fad8d0..66ae6b6e50 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs @@ -65,7 +65,7 @@ pub(in crate::interpreter) fn eval_builtin_preg_replace_callback( let pattern = eval_expr(pattern, context, scope, values)?; let callback = eval_expr(callback, context, scope, values)?; let subject = eval_expr(subject, context, scope, values)?; - eval_preg_replace_callback_result(pattern, callback, subject, context, values) + eval_preg_replace_callback_result_from_scope(pattern, callback, subject, Some(scope), context, values) } /// Replaces every regex match by invoking an eval-supported callback with `$matches`. @@ -75,9 +75,21 @@ pub(in crate::interpreter) fn eval_preg_replace_callback_result( subject: RuntimeCellHandle, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_preg_replace_callback_result_from_scope(pattern, callback, subject, None, context, values) +} + +/// Replaces regex matches with optional lexical scope for callback names. +fn eval_preg_replace_callback_result_from_scope( + pattern: RuntimeCellHandle, + callback: RuntimeCellHandle, + subject: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let regex = eval_preg_regex(pattern, values)?; - let callback = eval_callable(callback, context, values)?; + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; let subject = values.string_bytes(subject)?; let mut result = Vec::with_capacity(subject.len()); let mut cursor = 0; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 5fa77c4866..d10f745347 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -25,7 +25,7 @@ pub(in crate::interpreter) fn eval_builtin_call_user_func( for arg in args { evaluated_args.push(eval_expr(arg, context, scope, values)?); } - eval_call_user_func_with_values(evaluated_args, context, values) + eval_call_user_func_with_values_from_scope(evaluated_args, Some(scope), context, values) } /// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. @@ -40,7 +40,7 @@ pub(in crate::interpreter) fn eval_builtin_call_user_func_array( }; let callback = eval_expr(callback, context, scope, values)?; let arg_array = eval_expr(arg_array, context, scope, values)?; - eval_call_user_func_array_with_values(callback, arg_array, context, values) + eval_call_user_func_array_with_values_from_scope(callback, arg_array, Some(scope), context, values) } /// Dispatches `call_user_func_array` after callback and array arguments are evaluated. @@ -50,7 +50,24 @@ pub(in crate::interpreter) fn eval_call_user_func_array_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let callback = eval_call_user_func_callback(callback, "call_user_func_array", context, values)?; + eval_call_user_func_array_with_values_from_scope(callback, arg_array, None, context, values) +} + +/// Dispatches `call_user_func_array` with optional lexical scope for special class receivers. +fn eval_call_user_func_array_with_values_from_scope( + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_call_user_func_callback( + callback, + "call_user_func_array", + lexical_scope, + context, + values, + )?; if !values.is_array_like(arg_array)? { return Err(EvalStatus::RuntimeFatal); } @@ -63,11 +80,22 @@ pub(in crate::interpreter) fn eval_call_user_func_with_values( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_call_user_func_with_values_from_scope(evaluated_args, None, context, values) +} + +/// Dispatches `call_user_func` with optional lexical scope for special class receivers. +fn eval_call_user_func_with_values_from_scope( + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let Some((callback, callback_args)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; - let callback = eval_call_user_func_callback(*callback, "call_user_func", context, values)?; + let callback = + eval_call_user_func_callback(*callback, "call_user_func", lexical_scope, context, values)?; eval_evaluated_callable_with_call_user_func_values( &callback, callback_args.to_vec(), @@ -80,10 +108,11 @@ pub(in crate::interpreter) fn eval_call_user_func_with_values( fn eval_call_user_func_callback( callback: RuntimeCellHandle, function_name: &str, + lexical_scope: Option<&ElephcEvalScope>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match eval_callable(callback, context, values) { + match eval_callable_with_optional_scope(callback, context, lexical_scope, values) { Ok(callback) => { eval_validate_call_user_func_callback(&callback, function_name, context, values)?; Ok(callback) @@ -105,12 +134,32 @@ pub(in crate::interpreter) fn eval_callable( callback: RuntimeCellHandle, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_callable_with_optional_scope(callback, context, None, values) +} + +/// Normalizes one PHP callback while retaining the current method scope when available. +pub(in crate::interpreter) fn eval_callable_from_scope( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + scope: &ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_callable_with_optional_scope(callback, context, Some(scope), values) +} + +/// Normalizes one PHP callback with optional scope-sensitive special class receivers. +pub(in crate::interpreter) fn eval_callable_with_optional_scope( + callback: RuntimeCellHandle, + context: &ElephcEvalContext, + lexical_scope: Option<&ElephcEvalScope>, + values: &mut impl RuntimeValueOps, ) -> Result { if values.type_tag(callback)? == EVAL_TAG_OBJECT { return eval_object_callable(callback, context, values); } if values.is_array_like(callback)? { - return eval_array_callable(callback, context, values); + return eval_array_callable(callback, context, lexical_scope, values); } eval_string_callable(callback, values) } @@ -205,6 +254,7 @@ fn eval_closure_object_target_callable(target: &EvalClosureObjectTarget) -> Eval pub(in crate::interpreter) fn eval_array_callable( callback: RuntimeCellHandle, context: &ElephcEvalContext, + lexical_scope: Option<&ElephcEvalScope>, values: &mut impl RuntimeValueOps, ) -> Result { if values.array_len(callback)? != 2 { @@ -243,6 +293,15 @@ pub(in crate::interpreter) fn eval_array_callable( EVAL_TAG_STRING => { let class_name = String::from_utf8(values.string_bytes(receiver)?) .map_err(|_| EvalStatus::RuntimeFatal)?; + if let Some(callable) = eval_special_class_array_callable( + &class_name, + &method, + lexical_scope, + context, + values, + )? { + return Ok(callable); + } let called_class = context .eval_static_callable_called_class(callback, &class_name, &method) .map(str::to_string); @@ -266,6 +325,80 @@ pub(in crate::interpreter) fn eval_array_callable( } } +/// Resolves deprecated `self`/`static`/`parent` callable arrays inside method scope. +fn eval_special_class_array_callable( + class_name: &str, + method: &str, + lexical_scope: Option<&ElephcEvalScope>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_callable_array_receiver_is_special_class_name(class_name) { + return Ok(None); + } + let Some(scope) = lexical_scope else { + return Ok(None); + }; + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + let use_instance_receiver = + !eval_special_class_array_static_method_exists(&receiver.dispatch_class, method, context, values)?; + if use_instance_receiver { + if let Some(object) = + eval_static_syntax_instance_receiver(&receiver.dispatch_class, Some(scope), context, values)? + { + return Ok(Some(EvaluatedCallable::ObjectMethod { + object, + method: method.to_string(), + called_class: Some(receiver.called_class), + native_class: None, + bridge_scope: None, + })); + } + } + Ok(Some(EvaluatedCallable::StaticMethod { + class_name: class_name.to_string(), + method: method.to_string(), + called_class: Some(receiver.called_class), + native_class: None, + bridge_scope: None, + })) +} + +/// Returns whether a callable-array receiver is PHP's deprecated special class string. +fn eval_callable_array_receiver_is_special_class_name(class_name: &str) -> bool { + matches!( + class_name.trim_start_matches('\\').to_ascii_lowercase().as_str(), + "self" | "static" | "parent" + ) +} + +/// Returns whether a special class callable array names a real static method. +fn eval_special_class_array_static_method_exists( + class_name: &str, + method: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some((_, method)) = eval_dynamic_static_method_for_call(class_name, method, context) { + return Ok(method.is_static()); + } + let native_class = if context.has_class(class_name) { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + parent + } else if context.has_interface(class_name) + || context.has_trait(class_name) + || context.has_enum(class_name) + { + return Ok(false); + } else { + class_name.to_string() + }; + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(&native_class, method, context, values)? + .is_some_and(|(_, _, is_static, _)| is_static)) +} + /// Normalizes one string callback name for eval dynamic callable dispatch. pub(in crate::interpreter) fn eval_string_callable( callback: RuntimeCellHandle, @@ -378,6 +511,9 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( context, values, ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } None => eval_static_method_call_result( class_name, method, @@ -727,6 +863,9 @@ fn eval_static_method_with_call_user_func_values( context, values, ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } None => eval_static_method_call_result( class_name, method_name, @@ -945,6 +1084,9 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( context, values, ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } None => eval_static_method_call_result( class_name, method, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 7103b973d1..4a2c808cc3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -40,7 +40,12 @@ pub(in crate::interpreter) fn eval_builtin_is_callable_call( values: &mut impl RuntimeValueOps, ) -> Result { let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - eval_is_callable_call_with_evaluated_args(&evaluated_args, context, values) + eval_is_callable_call_with_evaluated_args_from_scope( + &evaluated_args, + Some(scope), + context, + values, + ) } /// Evaluates `is_callable()` from already evaluated arguments that may retain ref targets. @@ -48,6 +53,16 @@ pub(in crate::interpreter) fn eval_is_callable_call_with_evaluated_args( evaluated_args: &[EvaluatedCallArg], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_is_callable_call_with_evaluated_args_from_scope(evaluated_args, None, context, values) +} + +/// Evaluates materialized `is_callable()` args with optional special-class callable scope. +fn eval_is_callable_call_with_evaluated_args_from_scope( + evaluated_args: &[EvaluatedCallArg], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let (bound, _) = bind_evaluated_ref_builtin_args( &["value", "syntax_only", "callable_name"], @@ -66,6 +81,7 @@ pub(in crate::interpreter) fn eval_is_callable_call_with_evaluated_args( value.value, syntax_only, callable_name_target.as_ref(), + lexical_scope, context, values, ) @@ -78,14 +94,14 @@ pub(in crate::interpreter) fn eval_is_callable_with_values( values: &mut impl RuntimeValueOps, ) -> Result { match evaluated_args { - [value] => eval_is_callable_result(*value, false, None, context, values), + [value] => eval_is_callable_result(*value, false, None, None, context, values), [value, syntax_only] => { let syntax_only = values.truthy(*syntax_only)?; - eval_is_callable_result(*value, syntax_only, None, context, values) + eval_is_callable_result(*value, syntax_only, None, None, context, values) } [value, syntax_only, _callable_name] => { let syntax_only = values.truthy(*syntax_only)?; - eval_is_callable_result(*value, syntax_only, None, context, values) + eval_is_callable_result(*value, syntax_only, None, None, context, values) } _ => Err(EvalStatus::RuntimeFatal), } @@ -103,7 +119,7 @@ pub(in crate::interpreter) fn eval_function_probe_result( let name = eval_function_probe_name(value, values)?; eval_function_probe_exists(context, &name) } - "is_callable" => eval_is_callable_value(value, context, values)?, + "is_callable" => eval_is_callable_value(value, None, context, values)?, _ => return Err(EvalStatus::UnsupportedConstruct), }; values.bool_value(exists) @@ -119,13 +135,13 @@ fn eval_builtin_is_callable( match args { [value] => { let value = eval_expr(value, context, scope, values)?; - eval_is_callable_result(value, false, None, context, values) + eval_is_callable_result(value, false, None, Some(scope), context, values) } [value, syntax_only] => { let value = eval_expr(value, context, scope, values)?; let syntax_only = eval_expr(syntax_only, context, scope, values)?; let syntax_only = values.truthy(syntax_only)?; - eval_is_callable_result(value, syntax_only, None, context, values) + eval_is_callable_result(value, syntax_only, None, Some(scope), context, values) } [value, syntax_only, callable_name] => { let value = eval_expr(value, context, scope, values)?; @@ -138,6 +154,7 @@ fn eval_builtin_is_callable( value, syntax_only, Some(&callable_name_target), + Some(scope), context, values, ) @@ -1086,10 +1103,15 @@ fn eval_function_probe_name( /// Returns whether one runtime value is callable from the current eval scope. fn eval_is_callable_value( value: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let Ok(callback) = eval_callable(value, context, values) else { + let callback = match lexical_scope { + Some(scope) => eval_callable_from_scope(value, context, scope, values), + None => eval_callable(value, context, values), + }; + let Ok(callback) = callback else { return Ok(false); }; eval_callable_probe_exists(&callback, context, values) @@ -1100,6 +1122,7 @@ fn eval_is_callable_result( value: RuntimeCellHandle, syntax_only: bool, callable_name_target: Option<&EvalReferenceTarget>, + lexical_scope: Option<&ElephcEvalScope>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -1109,7 +1132,7 @@ fn eval_is_callable_result( let callable = if syntax_only { eval_is_callable_syntax_only(value, context, values)? } else { - eval_is_callable_value(value, context, values)? + eval_is_callable_value(value, lexical_scope, context, values)? }; if let Some((target, name)) = callable_name_target.zip(callable_name.as_deref()) { let name = values.string(name)?; @@ -1134,7 +1157,7 @@ fn eval_is_callable_syntax_only( return Ok(true); } if values.type_tag(value)? == EVAL_TAG_OBJECT { - return eval_is_callable_value(value, context, values); + return eval_is_callable_value(value, None, context, values); } if values.is_array_like(value)? { return eval_callable_array_display_name(value, context, values).map(|name| name.is_some()); diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index e684a377cc..915a881775 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -6244,7 +6244,7 @@ fn eval_throw_uninitialized_static_property_error( } /// Throws PHP's class-not-found error for unresolved static member receivers. -fn eval_throw_class_not_found_error( +pub(in crate::interpreter) fn eval_throw_class_not_found_error( class_name: &str, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -7282,6 +7282,7 @@ fn eval_static_method_call_result_resolved( &class_name, method_name, evaluated_args.clone(), + lexical_scope, context, values, )? { @@ -7437,6 +7438,7 @@ fn eval_closure_static_method_result( class_name: &str, method_name: &str, evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { @@ -7447,7 +7449,8 @@ fn eval_closure_static_method_result( return Ok(None); } if method_name.eq_ignore_ascii_case("fromCallable") { - return eval_closure_from_callable(evaluated_args, context, values).map(Some); + return eval_closure_from_callable(evaluated_args, lexical_scope, context, values) + .map(Some); } if method_name.eq_ignore_ascii_case("bind") { return eval_closure_bind_static(evaluated_args, context, values).map(Some); @@ -7458,12 +7461,17 @@ fn eval_closure_static_method_result( /// Materializes `Closure::fromCallable()` from one normalized eval callback. fn eval_closure_from_callable( evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let mut args = bind_evaluated_function_args(&[String::from("callback")], evaluated_args)?; let callback = args.pop().ok_or(EvalStatus::RuntimeFatal)?; - let callable = match eval_callable(callback, context, values) { + let callable = match lexical_scope { + Some(scope) => eval_callable_from_scope(callback, context, scope, values), + None => eval_callable(callback, context, values), + }; + let callable = match callable { Ok(callable) => callable, Err(EvalStatus::UnsupportedConstruct) if values.type_tag(callback)? == EVAL_TAG_OBJECT => { return eval_closure_from_callable_type_error( diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 7439c6ef76..087dac4321 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -390,6 +390,136 @@ D:NoSuchDynamicCallable::missing" ); } +/// Verifies eval callable arrays resolve `self`, `static`, and `parent` in method scope. +#[test] +fn test_eval_special_class_callable_arrays_follow_method_scope() { + let out = compile_and_run( + r#" 1; + } + + public function reduceValue(string $carry, int $value): string { + return $carry . $value . ":" . get_class($this) . ";"; + } + + public function walkValue(string &$value, int $key) { + $value = $value . $key . ":" . get_class($this); + } + + public function compareDesc(int $left, int $right): int { + return $right - $left; + } + + public function replaceMatch(array $matches): string { + return "R" . $matches[0] . ":" . get_class($this); + } + + public function run(): string { + $out = ""; + $name = "seed"; + $out .= is_callable(["self", "selfStatic"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["self", "selfStatic"]) . "|"; + + $name = "seed"; + $out .= is_callable(["static", "selfStatic"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["static", "selfStatic"]) . "|"; + + $name = "seed"; + $out .= is_callable(["parent", "parentStatic"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["parent", "parentStatic"]) . "|"; + + $name = "seed"; + $out .= is_callable(["self", "selfInstance"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["self", "selfInstance"]) . "|"; + + $name = "seed"; + $out .= is_callable(["parent", "parentInstance"], false, $name) ? "Y:" . $name . ":" : "N:"; + $out .= call_user_func(["parent", "parentInstance"]) . "|"; + + $fromInstance = Closure::fromCallable(["self", "selfInstance"]); + $out .= $fromInstance() . "|"; + + $fromStatic = Closure::fromCallable(["parent", "parentStatic"]); + $out .= $fromStatic(); + + $out .= "|" . implode(",", array_map(["static", "mapStatic"], [3])); + $out .= "|" . implode(",", array_map(["self", "mapInstance"], [4])); + $out .= "|" . implode(",", array_filter([1, 2], ["self", "keepValue"])); + $out .= "|" . array_reduce([1, 2], ["self", "reduceValue"], ""); + + $walk = ["x"]; + array_walk($walk, ["self", "walkValue"]); + $out .= "|" . $walk[0]; + + $sort = [1, 3, 2]; + usort($sort, ["self", "compareDesc"]); + $out .= "|" . implode(",", $sort); + + $out .= "|" . preg_replace_callback("/a/", ["self", "replaceMatch"], "a"); + + try { + $direct = ["self", "selfStatic"]; + $out .= "|" . $direct(); + } catch (Error $e) { + $out .= "|" . get_class($e) . ":" . $e->getMessage(); + } + + return $out; + } +} + +return (new EvalSpecialCallableArrayChild())->run();'); +"#, + ); + + assert_eq!( + out, + "Y:self::selfStatic:S:EvalSpecialCallableArrayChild|\ +Y:static::selfStatic:S:EvalSpecialCallableArrayChild|\ +Y:parent::parentStatic:P:EvalSpecialCallableArrayChild|\ +Y:self::selfInstance:I:EvalSpecialCallableArrayChild|\ +Y:parent::parentInstance:PI:EvalSpecialCallableArrayChild|\ +I:EvalSpecialCallableArrayChild|\ +P:EvalSpecialCallableArrayChild|\ +MS3:EvalSpecialCallableArrayChild|\ +MI4:EvalSpecialCallableArrayChild|\ +2|\ +1:EvalSpecialCallableArrayChild;2:EvalSpecialCallableArrayChild;|\ +x0:EvalSpecialCallableArrayChild|\ +3,2,1|\ +Ra:EvalSpecialCallableArrayChild|\ +Error:Class \"self\" not found" + ); +} + /// Verifies `Closure::fromCallable()` normalizes eval string and array callables to Closure objects. #[test] fn test_eval_closure_from_callable_normalizes_string_and_array_callables() { From cd9511ba9619c40b355107d748d5c2cb2d815d42 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 12:16:52 +0200 Subject: [PATCH 0976/1208] Test eval namespaced first-class callable fallback --- tests/codegen/eval_callables.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 087dac4321..7862c3fe50 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -161,6 +161,30 @@ return $f("1") . ":" . $m("2") . ":" . $s("3") . ":" . $ds("4") . ":" . $i("5"); ); } +/// Verifies namespaced eval first-class function callables use PHP builtin fallback rules. +#[test] +fn test_eval_first_class_function_callables_follow_namespace_fallback() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 12:30:02 +0200 Subject: [PATCH 0977/1208] Test eval imported first-class callables --- tests/codegen/eval_callables.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 7862c3fe50..ba094b029f 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -185,6 +185,32 @@ echo call_user_func_array($local, ["cd"]);'); assert_eq!(out, "4|local:ab|3|local:cd"); } +/// Verifies eval first-class function callables resolve namespace function imports. +#[test] +fn test_eval_first_class_function_callables_follow_namespace_imports() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 12:43:50 +0200 Subject: [PATCH 0978/1208] Test eval leading slash callable arrays --- tests/codegen/eval_callables.rs | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index ba094b029f..cf6db5660d 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -211,6 +211,46 @@ echo call_user_func_array($target, ["q"]);'); assert_eq!(out, "4|target:x|2|target:q"); } +/// Verifies callable-array class receivers accept PHP's leading namespace separator. +#[test] +fn test_eval_callable_array_class_receivers_allow_leading_namespace_separator() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 12:56:19 +0200 Subject: [PATCH 0979/1208] Test eval leading slash string callables --- tests/codegen/eval_callables.rs | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index cf6db5660d..96094eac80 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -251,6 +251,48 @@ echo Closure::fromCallable($aot)("q");'); ); } +/// Verifies string callables accept PHP's leading namespace separator. +#[test] +fn test_eval_string_callables_allow_leading_namespace_separator() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 13:10:28 +0200 Subject: [PATCH 0980/1208] Test eval declared by-ref callables --- tests/codegen/eval_callables.rs | 72 +++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 96094eac80..ff5463add5 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -114,6 +114,78 @@ return call_user_func_array($static, [&$d, 9]) . ":" . gettype($d) . ":" . $d;') ); } +/// Verifies eval-declared callable forms preserve by-ref writeback. +#[test] +fn test_eval_declared_callable_forms_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +$string = "eval_declared_ref_add"; +$a = "2"; +echo $string($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$first = eval_declared_ref_add(...); +$b = "4"; +echo $first($b, 5) . ":" . gettype($b) . ":" . $b . "|"; +$c = "6"; +echo call_user_func_array($first, [&$c, 7]) . ":" . gettype($c) . ":" . $c . "|"; + +$box = new EvalDeclaredRefCallableBox(); +$instance = [$box, "bump"]; +$d = "8"; +echo $instance($d, 4) . ":" . gettype($d) . ":" . $d . "|"; +$e = "1"; +echo call_user_func_array($instance, [&$e, 5]) . ":" . gettype($e) . ":" . $e . "|"; + +$static = ["EvalDeclaredRefCallableBox", "add"]; +$f = "7"; +echo $static($f, 6) . ":" . gettype($f) . ":" . $f . "|"; + +$closureFunction = Closure::fromCallable("eval_declared_ref_add"); +$g = "3"; +echo $closureFunction($g, 4) . ":" . gettype($g) . ":" . $g . "|"; + +$closureInstance = Closure::fromCallable([$box, "bump"]); +$h = "2"; +echo $closureInstance($h, 6) . ":" . gettype($h) . ":" . $h . "|"; + +$closureStatic = Closure::fromCallable(["EvalDeclaredRefCallableBox", "add"]); +$i = "5"; +echo $closureStatic($i, 8) . ":" . gettype($i) . ":" . $i . "|"; + +$closureNamedStatic = Closure::fromCallable("EvalDeclaredRefCallableBox::add"); +$j = "6"; +return call_user_func_array($closureNamedStatic, [&$j, 9]) . ":" . gettype($j) . ":" . $j;'); +"#, + ); + + assert_eq!( + out, + concat!( + "5:integer:5|9:integer:9|13:integer:13|22:integer:22|16:integer:16|", + "13:integer:13|7:integer:7|18:integer:18|13:integer:13|15:integer:15" + ) + ); +} + /// Verifies eval first-class callables are PHP-visible `Closure` objects and remain invokable. #[test] fn test_eval_first_class_callables_are_php_closure_objects() { From 2b604904fb83ca5e8781740bbced585fe8303b86 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 13:23:49 +0200 Subject: [PATCH 0981/1208] Test eval declared by-ref constructors --- tests/codegen/eval_constructors.rs | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/codegen/eval_constructors.rs b/tests/codegen/eval_constructors.rs index 245cfe40e8..39a82e0208 100644 --- a/tests/codegen/eval_constructors.rs +++ b/tests/codegen/eval_constructors.rs @@ -91,3 +91,92 @@ return gettype($box->value) . ":" . $box->value;'); "Exception:ctor-lvalue:integer:12|Exception:ctor-lvalue:integer:16" ); } + +/// Verifies eval-declared constructor by-reference args write back to lvalue targets. +#[test] +fn test_eval_declared_constructor_by_ref_writes_back_to_lvalue_targets() { + let out = compile_and_run( + r#" "2"]; +new EvalDeclaredCtorRefTargetBridge($items["x"]); +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$nested = ["outer" => ["inner" => "3"]]; +new EvalDeclaredCtorRefTargetBridge($nested["outer"]["inner"]); +echo gettype($nested["outer"]["inner"]) . ":" . $nested["outer"]["inner"] . "|"; + +$box = new EvalDeclaredCtorRefTargetBox(); +new EvalDeclaredCtorRefTargetBridge($box->value); +echo gettype($box->value) . ":" . $box->value . "|"; + +EvalDeclaredCtorRefTargetStatic::$value = "5"; +new EvalDeclaredCtorRefTargetBridge(EvalDeclaredCtorRefTargetStatic::$value); +return gettype(EvalDeclaredCtorRefTargetStatic::$value) . ":" . EvalDeclaredCtorRefTargetStatic::$value;'); +"#, + ); + + assert_eq!( + out, + "integer:6|integer:7|integer:8|integer:8|integer:10" + ); +} + +/// Verifies eval-declared constructor by-reference writeback happens before catchable throw. +#[test] +fn test_eval_declared_constructor_by_ref_lvalue_writeback_before_throw() { + let out = compile_and_run( + r#" "1"]; +try { + new EvalDeclaredCtorThrowRefTargetBridge($items["x"]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$box = new EvalDeclaredCtorThrowRefTargetBox(); +try { + new EvalDeclaredCtorThrowRefTargetBridge($box->value); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":"; +} +return gettype($box->value) . ":" . $box->value;'); +"#, + ); + + assert_eq!( + out, + "Exception:eval-ctor-lvalue:integer:12|Exception:eval-ctor-lvalue:integer:16" + ); +} From d03691a20a80cc109bfdbf73e6db9ad4537d614e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 13:37:32 +0200 Subject: [PATCH 0982/1208] Test eval ref-like builtin closures --- tests/codegen/eval_builtin_parity.rs | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 18ad6f168f..5bd540dc07 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -219,3 +219,35 @@ foreach ($items as $key => $value) { assert_eq!(out, "1:ab:a:b|Ka1b2"); } + +/// Verifies eval first-class and Closure builtin callables preserve ref-like parameters. +#[test] +fn test_eval_ref_like_builtin_closures_write_back_aliases() { + let out = compile_and_run( + r#" 2, "a" => 1]; +echo call_user_func_array($ksort, ["array" => &$assoc]) ? "K" : "k"; +foreach ($assoc as $key => $entry) { + echo $key . $entry; +}'); +"#, + ); + + assert_eq!(out, "S1,2,3|Tinteger:42|1:ab:a:b|Ka1b2"); +} From 17ea29a691f40d8a2d86e1447c7f6aaf555cb28a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 14:36:11 +0200 Subject: [PATCH 0983/1208] Clean up eval call_user_func_array args --- .../interpreter/builtins/registry/callable.rs | 13 +++++- .../src/interpreter/dynamic_functions.rs | 43 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index d10f745347..0ee4e61c8d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -38,9 +38,20 @@ pub(in crate::interpreter) fn eval_builtin_call_user_func_array( let [callback, arg_array] = args else { return Err(EvalStatus::RuntimeFatal); }; + let release_arg_array = matches!(arg_array, EvalExpr::Array(_)); let callback = eval_expr(callback, context, scope, values)?; let arg_array = eval_expr(arg_array, context, scope, values)?; - eval_call_user_func_array_with_values_from_scope(callback, arg_array, Some(scope), context, values) + let result = eval_call_user_func_array_with_values_from_scope( + callback, + arg_array, + Some(scope), + context, + values, + ); + if release_arg_array { + values.release(arg_array)?; + } + result } /// Dispatches `call_user_func_array` after callback and array arguments are evaluated. diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 1a6fbf2bf9..9592106496 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -288,32 +288,57 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( let len = values.array_len(array)?; for position in 0..len { let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; let ref_target = eval_array_reference_key(key, values)? .and_then(|key| context.array_element_alias(array, &key).cloned()); - match values.type_tag(key)? { + let arg = match values.type_tag(key)? { EVAL_TAG_INT => { if *saw_named { + values.release(key)?; return Err(EvalStatus::RuntimeFatal); } - evaluated_args.push(EvaluatedCallArg { + let value = match values.array_get(array, key) { + Ok(value) => value, + Err(status) => { + values.release(key)?; + return Err(status); + } + }; + EvaluatedCallArg { name: None, value, ref_target, - }); + } } EVAL_TAG_STRING => { *saw_named = true; let name = values.string_bytes(key)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - evaluated_args.push(EvaluatedCallArg { + let name = match String::from_utf8(name) { + Ok(name) => name, + Err(_) => { + values.release(key)?; + return Err(EvalStatus::RuntimeFatal); + } + }; + let value = match values.array_get(array, key) { + Ok(value) => value, + Err(status) => { + values.release(key)?; + return Err(status); + } + }; + EvaluatedCallArg { name: Some(name), value, ref_target, - }); + } } - _ => return Err(EvalStatus::RuntimeFatal), - } + _ => { + values.release(key)?; + return Err(EvalStatus::RuntimeFatal); + } + }; + values.release(key)?; + evaluated_args.push(arg); } Ok(()) } From cb3dc979b4a5724f79ecc611870c9ec4568fb053 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 15:11:09 +0200 Subject: [PATCH 0984/1208] Clean up eval call_user_func_array callback temps --- .../interpreter/builtins/registry/callable.rs | 19 ++++- .../src/interpreter/tests/dynamic_calls.rs | 72 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 0ee4e61c8d..e4e5d159ad 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -38,9 +38,18 @@ pub(in crate::interpreter) fn eval_builtin_call_user_func_array( let [callback, arg_array] = args else { return Err(EvalStatus::RuntimeFatal); }; + let release_callback = eval_call_user_func_array_callback_is_temporary(callback); let release_arg_array = matches!(arg_array, EvalExpr::Array(_)); let callback = eval_expr(callback, context, scope, values)?; - let arg_array = eval_expr(arg_array, context, scope, values)?; + let arg_array = match eval_expr(arg_array, context, scope, values) { + Ok(arg_array) => arg_array, + Err(status) => { + if release_callback { + values.release(callback)?; + } + return Err(status); + } + }; let result = eval_call_user_func_array_with_values_from_scope( callback, arg_array, @@ -51,9 +60,17 @@ pub(in crate::interpreter) fn eval_builtin_call_user_func_array( if release_arg_array { values.release(arg_array)?; } + if release_callback { + values.release(callback)?; + } result } +/// Returns whether a `call_user_func_array` callback expression allocates a temporary cell. +fn eval_call_user_func_array_callback_is_temporary(callback: &EvalExpr) -> bool { + matches!(callback, EvalExpr::Const(_)) +} + /// Dispatches `call_user_func_array` after callback and array arguments are evaluated. pub(in crate::interpreter) fn eval_call_user_func_array_with_values( callback: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 273efbcaa4..aa3d6dfc59 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -817,6 +817,78 @@ fn execute_program_call_user_func_array_dispatches_builtin() { assert_eq!(values.get(result), FakeValue::Int(4)); } +/// Verifies `call_user_func_array` releases literal callback and argument-array temporaries. +#[test] +fn execute_program_call_user_func_array_releases_literal_callback_and_arg_array() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after dispatch" + ); + assert!( + values + .releases + .iter() + .any(|release| matches!(values.get(*release), FakeValue::Array(_))), + "literal call argument array should be released after dispatch" + ); +} +/// Verifies `call_user_func_array` releases literal temporaries after a fatal dispatch. +#[test] +fn execute_program_call_user_func_array_releases_literal_temporaries_after_fatal() { + let program = + parse_fragment(br#"return call_user_func_array("strlen", ["unknown" => "abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after fatal dispatch" + ); + assert!( + values + .releases + .iter() + .any(|release| matches!(values.get(*release), FakeValue::Assoc(_))), + "literal call argument hash should be released after fatal dispatch" + ); +} +/// Verifies `call_user_func_array` releases literal callback when arg-array evaluation fails. +#[test] +fn execute_program_call_user_func_array_releases_literal_callback_after_arg_array_eval_fatal() { + let program = parse_fragment(br#"return call_user_func_array("strlen", MISSING_ARG_ARRAY);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released when argument-array evaluation fails" + ); +} /// Verifies `call_user_func_array` inside eval can dispatch a registered native function. #[test] fn execute_program_call_user_func_array_dispatches_registered_native_function() { From d494ef62e9779f3457ddb8f2e527b6f7c018265b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 15:33:02 +0200 Subject: [PATCH 0985/1208] Clean up eval call_user_func callback temps --- .../interpreter/builtins/registry/callable.rs | 28 +++++++-- .../src/interpreter/tests/dynamic_calls.rs | 60 +++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index e4e5d159ad..b0662da5bd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -21,11 +21,27 @@ pub(in crate::interpreter) fn eval_builtin_call_user_func( if args.is_empty() { return Err(EvalStatus::RuntimeFatal); } + let release_callback = eval_call_user_func_callback_expr_is_temporary(&args[0]); let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); + for (index, arg) in args.iter().enumerate() { + let value = match eval_expr(arg, context, scope, values) { + Ok(value) => value, + Err(status) => { + if index > 0 && release_callback { + values.release(evaluated_args[0])?; + } + return Err(status); + } + }; + evaluated_args.push(value); } - eval_call_user_func_with_values_from_scope(evaluated_args, Some(scope), context, values) + let callback = evaluated_args[0]; + let result = + eval_call_user_func_with_values_from_scope(evaluated_args, Some(scope), context, values); + if release_callback { + values.release(callback)?; + } + result } /// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. @@ -38,7 +54,7 @@ pub(in crate::interpreter) fn eval_builtin_call_user_func_array( let [callback, arg_array] = args else { return Err(EvalStatus::RuntimeFatal); }; - let release_callback = eval_call_user_func_array_callback_is_temporary(callback); + let release_callback = eval_call_user_func_callback_expr_is_temporary(callback); let release_arg_array = matches!(arg_array, EvalExpr::Array(_)); let callback = eval_expr(callback, context, scope, values)?; let arg_array = match eval_expr(arg_array, context, scope, values) { @@ -66,8 +82,8 @@ pub(in crate::interpreter) fn eval_builtin_call_user_func_array( result } -/// Returns whether a `call_user_func_array` callback expression allocates a temporary cell. -fn eval_call_user_func_array_callback_is_temporary(callback: &EvalExpr) -> bool { +/// Returns whether a `call_user_func*` callback expression allocates a temporary cell. +fn eval_call_user_func_callback_expr_is_temporary(callback: &EvalExpr) -> bool { matches!(callback, EvalExpr::Const(_)) } diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index aa3d6dfc59..6e4a3f2dec 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -37,6 +37,66 @@ fn execute_program_call_user_func_dispatches_builtin() { assert_eq!(values.get(result), FakeValue::Int(4)); } +/// Verifies `call_user_func` releases literal callback temporaries after dispatch. +#[test] +fn execute_program_call_user_func_releases_literal_callback_after_dispatch() { + let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after dispatch" + ); +} + +/// Verifies `call_user_func` releases literal callback temporaries after dispatch fatal. +#[test] +fn execute_program_call_user_func_releases_literal_callback_after_dispatch_fatal() { + let program = + parse_fragment(br#"return call_user_func("strlen");"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after fatal dispatch" + ); +} + +/// Verifies `call_user_func` releases literal callback when later arg evaluation fails. +#[test] +fn execute_program_call_user_func_releases_literal_callback_after_arg_eval_fatal() { + let program = parse_fragment(br#"return call_user_func("strlen", MISSING_ARG);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released when argument evaluation fails" + ); +} + /// Verifies `call_user_func` inside eval can dispatch a registered native function. #[test] fn execute_program_call_user_func_dispatches_registered_native_function() { From 87cd772c74ec74ac4e66fb6e8dd6fe6e443e8eb6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 15:46:47 +0200 Subject: [PATCH 0986/1208] Clean up eval array callable index temps --- .../interpreter/builtins/registry/callable.rs | 28 ++++++++++-- .../src/interpreter/tests/dynamic_calls.rs | 43 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index b0662da5bd..6bff182c7d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -305,9 +305,31 @@ pub(in crate::interpreter) fn eval_array_callable( return Err(EvalStatus::RuntimeFatal); } let zero = values.int(0)?; - let one = values.int(1)?; - let receiver = values.array_get(callback, zero)?; - let method = values.array_get(callback, one)?; + let one = match values.int(1) { + Ok(one) => one, + Err(status) => { + values.release(zero)?; + return Err(status); + } + }; + let receiver = match values.array_get(callback, zero) { + Ok(receiver) => receiver, + Err(status) => { + values.release(zero)?; + values.release(one)?; + return Err(status); + } + }; + let method = match values.array_get(callback, one) { + Ok(method) => method, + Err(status) => { + values.release(zero)?; + values.release(one)?; + return Err(status); + } + }; + values.release(zero)?; + values.release(one)?; let method = String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; match values.type_tag(receiver)? { diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index 6e4a3f2dec..e74f85e1ab 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -202,6 +202,7 @@ return $cb(1);"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); assert_eq!(values.get(result), FakeValue::Int(42)); + assert_released_array_callable_index_temps(&values); } /// Verifies `call_user_func` dispatches callable arrays with object receivers. #[test] @@ -218,6 +219,30 @@ return call_user_func($cb);"#, let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); assert_eq!(values.get(result), FakeValue::Int(42)); + assert_released_array_callable_index_temps(&values); +} + +/// Verifies callable-array index temporaries are released when validation rejects the callback. +#[test] +fn execute_program_call_user_func_releases_array_callable_index_temps_after_validation_fatal() { + let program = parse_fragment( + br#"class EvalMissingArrayCallableMethod {} +$missing = new EvalMissingArrayCallableMethod(); +try { + call_user_func([$missing, "MiSsInG"]); + return false; +} catch (TypeError $e) { + return true; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_released_array_callable_index_temps(&values); } /// Verifies `call_user_func_array` dispatches callable arrays with positional args. #[test] @@ -261,6 +286,24 @@ return $named(right: "H", left: "G");"#, assert_eq!(values.get(result), FakeValue::String("GH".to_string())); } +/// Verifies fake runtime releases include both temporary callable-array index cells. +fn assert_released_array_callable_index_temps(values: &FakeOps) { + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::Int(0)), + "array callable index 0 temporary should be released" + ); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::Int(1)), + "array callable index 1 temporary should be released" + ); +} + /// Verifies first-class callable syntax dispatches through eval's callback paths. #[test] fn execute_program_first_class_callables_dispatch_functions_and_methods() { From c393ca5337096dba8e8c22c6795c37001f5e1ff8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 15:59:56 +0200 Subject: [PATCH 0987/1208] Recognize eval date alias probes --- .../src/interpreter/builtins/symbols.rs | 73 ++++++++++++++++++- .../src/interpreter/tests/builtins_symbols.rs | 42 +++++++++++ tests/codegen/eval.rs | 38 ++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 4a2c808cc3..253f64a538 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -11,6 +11,68 @@ use super::super::*; use super::*; +const EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS: &[&str] = &[ + "cal_days_in_month", + "cal_from_jd", + "cal_info", + "cal_to_jd", + "date_add", + "date_create", + "date_create_from_format", + "date_create_immutable", + "date_create_immutable_from_format", + "date_date_set", + "date_diff", + "date_format", + "date_get_last_errors", + "date_interval_create_from_date_string", + "date_interval_format", + "date_isodate_set", + "date_modify", + "date_offset_get", + "date_parse", + "date_parse_from_format", + "date_sub", + "date_sun_info", + "date_sunrise", + "date_sunset", + "date_time_set", + "date_timestamp_get", + "date_timestamp_set", + "date_timezone_get", + "date_timezone_set", + "easter_date", + "easter_days", + "frenchtojd", + "gettimeofday", + "gmstrftime", + "gregoriantojd", + "idate", + "jddayofweek", + "jdmonthname", + "jdtofrench", + "jdtogregorian", + "jdtojewish", + "jdtojulian", + "jdtounix", + "jewishtojd", + "juliantojd", + "mktime", + "gmmktime", + "strftime", + "strptime", + "timezone_abbreviations_list", + "timezone_identifiers_list", + "timezone_location_get", + "timezone_name_from_abbr", + "timezone_name_get", + "timezone_offset_get", + "timezone_open", + "timezone_transitions_get", + "timezone_version_get", + "unixtojd", +]; + /// Evaluates `function_exists()` and `is_callable()` inside an eval fragment. pub(in crate::interpreter) fn eval_builtin_function_probe( name: &str, @@ -1087,7 +1149,16 @@ pub(in crate::interpreter) fn eval_function_probe_exists( context: &ElephcEvalContext, name: &str, ) -> bool { - !name.contains("::") && (context.has_function(name) || eval_php_visible_builtin_exists(name)) + !name.contains("::") + && (context.has_function(name) + || eval_php_visible_builtin_exists(name) + || eval_date_procedural_alias_exists(name)) +} + +/// Returns true for DateTime/calendar/timezone aliases that static elephc desugars. +fn eval_date_procedural_alias_exists(name: &str) -> bool { + let bare = name.rsplit('\\').next().unwrap_or(""); + EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS.contains(&bare) } /// Reads and normalizes a function-probe string argument. diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs index d444423297..a03ec19506 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs @@ -130,6 +130,48 @@ echo function_exists("missing_probe") . "x";"#, assert_eq!(values.output, "1x1x1x1xxx"); } + +/// Verifies eval function probes recognize static DateTime/calendar procedural aliases. +#[test] +fn execute_program_function_probes_recognize_date_procedural_aliases() { + let program = parse_fragment( + br#"$aliases = [ + "idate", "mktime", "gmmktime", "date_create", "date_create_immutable", + "date_create_from_format", "date_create_immutable_from_format", + "date_parse_from_format", "date_parse", "date_sun_info", "date_sunrise", + "date_sunset", "strptime", "timezone_name_from_abbr", "cal_to_jd", + "cal_from_jd", "cal_days_in_month", "cal_info", "gregoriantojd", + "jdtogregorian", "juliantojd", "jdtojulian", "frenchtojd", + "jdtofrench", "jewishtojd", "jdtojewish", "jddayofweek", "jdmonthname", + "jdtounix", "unixtojd", "easter_days", "easter_date", "gettimeofday", + "date_get_last_errors", "strftime", "gmstrftime", "timezone_open", + "timezone_identifiers_list", "timezone_location_get", + "timezone_transitions_get", "timezone_abbreviations_list", + "timezone_version_get", "date_interval_create_from_date_string", + "date_diff", "date_format", "date_add", "date_sub", "date_modify", + "date_timestamp_get", "date_timestamp_set", "date_timezone_get", + "date_timezone_set", "date_offset_get", "date_date_set", + "date_isodate_set", "date_time_set", "date_interval_format", + "timezone_name_get", "timezone_offset_get" +]; +foreach ($aliases as $alias) { + echo function_exists($alias) ? "1" : "0"; +} +echo ":"; +echo function_exists("Date_Create") ? "C" : "c"; +echo function_exists("EvalAlias\\idate") ? "N" : "n"; +echo is_callable("timezone_version_get") ? "I" : "i"; +echo function_exists("does_not_exist_alias_xyz") ? "x" : "X";"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1".repeat(59) + ":CNIX"); +} + /// Verifies eval `interface_exists()` probes generated interface metadata by callable. #[test] fn execute_program_interface_exists_uses_runtime_probe() { diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 51c1fba934..578997120d 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2697,6 +2697,44 @@ echo ":"; echo function_exists("date"); echo function_exists("mktime");'); ); } +/// Verifies eval function probes recognize the DateTime/calendar aliases that static elephc +/// desugars before codegen. +#[test] +fn test_eval_function_probes_date_procedural_aliases() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 16:27:59 +0200 Subject: [PATCH 0988/1208] Execute eval date procedural aliases --- .../interpreter/builtins/registry/callable.rs | 5 + .../builtins/registry/callable_validation.rs | 2 +- .../builtins/registry/dispatch/mod.rs | 5 + .../src/interpreter/builtins/time/aliases.rs | 518 ++++++++++++++++++ .../src/interpreter/builtins/time/mod.rs | 2 + .../src/interpreter/expressions.rs | 3 + .../src/interpreter/tests/builtins_symbols.rs | 20 + src/ir_lower/builtin_datetime.rs | 142 ++++- tests/codegen/eval.rs | 20 + 9 files changed, 715 insertions(+), 2 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/aliases.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 6bff182c7d..c55f4a507e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -1209,6 +1209,11 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); return eval_callable_with_values(name, evaluated_args, context, values); } + if let Some(result) = + eval_date_procedural_alias_with_evaluated_args(name, evaluated_args.clone(), context, values)? + { + return Ok(result); + } if let Some(result) = eval_mutating_builtin_with_call_array_args(name, &evaluated_args, context, values)? { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index a706b2d9a3..b9678a9110 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -117,7 +117,7 @@ fn eval_validate_named_callable( ) -> Result<(), EvalStatus> { if context.has_closure(name) || context.has_function(name) - || eval_php_visible_builtin_exists(name) + || eval_function_probe_exists(context, name) { return Ok(()); } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index e4435201b7..c02cf5e018 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -68,6 +68,11 @@ pub(in crate::interpreter) fn eval_builtin_with_values( if let Some(result) = eval_scalars_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } + if let Some(result) = + eval_date_procedural_alias_with_values(name, evaluated_args, context, values)? + { + return Ok(Some(result)); + } if let Some(result) = eval_time_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } diff --git a/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs new file mode 100644 index 0000000000..1b3adbc254 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs @@ -0,0 +1,518 @@ +//! Purpose: +//! Executes procedural DateTime, DateInterval, DateTimeZone, and calendar aliases +//! that static elephc normally rewrites before type checking. +//! +//! Called from: +//! - `crate::interpreter::eval_call()` +//! - `crate::interpreter::builtins::registry::dispatch` +//! +//! Key details: +//! - The eval parser cannot run the static name-resolver rewrite, so this module +//! mirrors the alias dispatch at runtime and delegates to native AOT bridges. + +use super::super::super::*; +use super::*; + +const EVAL_TZ_VERSION: &str = + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../elephc-tz/data/version.data")); + +/// Attempts to execute one direct procedural date/time alias call. +pub(in crate::interpreter) fn eval_date_procedural_alias_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if eval_date_alias_key(name).is_none() { + return Ok(None); + } + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_date_procedural_alias_with_evaluated_args(name, evaluated_args, context, values) +} + +/// Attempts to execute one procedural date/time alias from positional runtime values. +pub(in crate::interpreter) fn eval_date_procedural_alias_with_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let evaluated_args = evaluated_args + .iter() + .copied() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + eval_date_procedural_alias_with_evaluated_args(name, evaluated_args, context, values) +} + +/// Attempts to execute one procedural date/time alias from evaluated call args. +pub(in crate::interpreter) fn eval_date_procedural_alias_with_evaluated_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(name) = eval_date_alias_key(name) else { + return Ok(None); + }; + if eval_date_alias_should_fall_back_to_builtin(&name, &evaluated_args) { + return Ok(None); + } + let args = positional_evaluated_arg_values(evaluated_args)?; + let result = eval_date_alias_result(&name, args, context, values)?; + Ok(Some(result)) +} + +/// Dispatches a normalized alias name to the equivalent runtime operation. +fn eval_date_alias_result( + name: &str, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "idate" => eval_idate_alias(args, context, values), + "mktime" | "gmmktime" => eval_mktime_alias(name, args, context, values), + "date_create" => eval_new_datetime_alias("DateTime", args, context, values), + "date_create_immutable" => { + eval_new_datetime_alias("DateTimeImmutable", args, context, values) + } + "date_create_from_format" => { + eval_static_alias("DateTime", "createFromFormat", args, context, values) + } + "date_create_immutable_from_format" => { + eval_static_alias("DateTimeImmutable", "createFromFormat", args, context, values) + } + "date_parse_from_format" => { + eval_static_alias("DateTime", "__elephc_date_parse_from_format", args, context, values) + } + "date_parse" => eval_static_alias("DateTime", "__elephc_date_parse", args, context, values), + "date_sun_info" => { + eval_static_alias("DateTime", "__elephc_date_sun_info", args, context, values) + } + "date_sunrise" => eval_date_sunfunc_alias(false, args, context, values), + "date_sunset" => eval_date_sunfunc_alias(true, args, context, values), + "strptime" => eval_static_alias("DateTime", "__elephc_strptime", args, context, values), + "timezone_name_from_abbr" => eval_static_alias( + "DateTime", + "__elephc_timezone_name_from_abbr", + args, + context, + values, + ), + "cal_to_jd" => eval_static_alias("DateTime", "__elephc_cal_to_jd", args, context, values), + "cal_from_jd" => { + eval_static_alias("DateTime", "__elephc_cal_from_jd", args, context, values) + } + "cal_days_in_month" => eval_static_alias( + "DateTime", + "__elephc_cal_days_in_month", + args, + context, + values, + ), + "cal_info" => eval_static_alias("DateTime", "__elephc_cal_info", args, context, values), + "gregoriantojd" => { + eval_static_alias("DateTime", "__elephc_gregoriantojd", args, context, values) + } + "jdtogregorian" => { + eval_static_alias("DateTime", "__elephc_jdtogregorian", args, context, values) + } + "juliantojd" => { + eval_static_alias("DateTime", "__elephc_juliantojd", args, context, values) + } + "jdtojulian" => eval_static_alias("DateTime", "__elephc_jdtojulian", args, context, values), + "frenchtojd" => { + eval_static_alias("DateTime", "__elephc_frenchtojd", args, context, values) + } + "jdtofrench" => eval_static_alias("DateTime", "__elephc_jdtofrench", args, context, values), + "jewishtojd" => { + eval_static_alias("DateTime", "__elephc_jewishtojd", args, context, values) + } + "jdtojewish" => eval_static_alias("DateTime", "__elephc_jdtojewish", args, context, values), + "jddayofweek" => { + eval_static_alias("DateTime", "__elephc_jddayofweek", args, context, values) + } + "jdmonthname" => { + eval_static_alias("DateTime", "__elephc_jdmonthname", args, context, values) + } + "jdtounix" => eval_static_alias("DateTime", "__elephc_jdtounix", args, context, values), + "unixtojd" => eval_unixtojd_alias(args, context, values), + "easter_days" => eval_easter_alias("__elephc_easter_days", args, context, values), + "easter_date" => eval_easter_alias("__elephc_easter_date", args, context, values), + "gettimeofday" => { + eval_static_alias("DateTime", "__elephc_gettimeofday", args, context, values) + } + "date_get_last_errors" => { + eval_static_alias("DateTime", "getLastErrors", args, context, values) + } + "strftime" => eval_strftime_alias(false, args, context, values), + "gmstrftime" => eval_strftime_alias(true, args, context, values), + "timezone_open" => eval_new_datetime_alias("DateTimeZone", args, context, values), + "timezone_identifiers_list" => { + eval_static_alias("DateTimeZone", "listIdentifiers", args, context, values) + } + "timezone_location_get" => eval_method_alias(args, 0, "getLocation", &[], context, values), + "timezone_transitions_get" => { + eval_method_alias_tail(args, 0, "getTransitions", context, values) + } + "timezone_abbreviations_list" => { + eval_static_alias("DateTimeZone", "listAbbreviations", args, context, values) + } + "timezone_version_get" => eval_timezone_version_alias(args, values), + "date_interval_create_from_date_string" => { + eval_static_alias("DateInterval", "createFromDateString", args, context, values) + } + "date_diff" => eval_method_alias_tail(args, 0, "diff", context, values), + "date_format" => eval_method_alias(args, 0, "format", &[1], context, values), + "date_add" => eval_method_alias(args, 0, "add", &[1], context, values), + "date_sub" => eval_method_alias(args, 0, "sub", &[1], context, values), + "date_modify" => eval_method_alias(args, 0, "modify", &[1], context, values), + "date_timestamp_get" => eval_method_alias(args, 0, "getTimestamp", &[], context, values), + "date_timestamp_set" => { + eval_method_alias(args, 0, "setTimestamp", &[1], context, values) + } + "date_timezone_get" => eval_method_alias(args, 0, "getTimezone", &[], context, values), + "date_timezone_set" => { + eval_method_alias(args, 0, "setTimezone", &[1], context, values) + } + "date_offset_get" => eval_method_alias(args, 0, "getOffset", &[], context, values), + "date_date_set" => eval_method_alias(args, 0, "setDate", &[1, 2, 3], context, values), + "date_isodate_set" => eval_method_alias_tail(args, 0, "setISODate", context, values), + "date_time_set" => eval_method_alias_tail(args, 0, "setTime", context, values), + "date_interval_format" => eval_method_alias(args, 0, "format", &[1], context, values), + "timezone_name_get" => eval_method_alias(args, 0, "getName", &[], context, values), + "timezone_offset_get" => eval_method_alias(args, 0, "getOffset", &[1], context, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Implements `idate()` as `intval(date(...))`. +fn eval_idate_alias( + args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = match args.as_slice() { + [format] => eval_date_result("date", *format, None, context, values), + [format, timestamp] => eval_date_result("date", *format, Some(*timestamp), context, values), + _ => return Err(EvalStatus::RuntimeFatal), + }?; + let cast = values.cast_int(result); + values.release(result)?; + cast +} + +/// Implements `mktime()` and `gmmktime()` optional argument filling. +fn eval_mktime_alias( + name: &str, + args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 6 { + return Err(EvalStatus::RuntimeFatal); + } + let mut full = Vec::with_capacity(6); + let mut temps = Vec::new(); + let date_name = if name == "gmmktime" { "gmdate" } else { "date" }; + for (index, spec) in ["G", "i", "s", "n", "j", "Y"].into_iter().enumerate() { + if let Some(arg) = args.get(index) { + full.push(*arg); + } else { + let default = eval_current_date_part_int(date_name, spec, context, values)?; + temps.push(default); + full.push(default); + } + } + let result = eval_mktime_result( + name, full[0], full[1], full[2], full[3], full[4], full[5], context, values, + ); + for temp in temps { + values.release(temp)?; + } + result +} + +/// Constructs one native DateTime-family object and runs its constructor. +fn eval_new_datetime_alias( + class_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object(class_name)?; + if let Err(status) = + eval_native_constructor_with_evaluated_args(class_name, object, positional_args(args), context, values) + { + let _ = values.release(object); + return Err(status); + } + Ok(object) +} + +/// Calls one native/static method alias with positional arguments. +fn eval_static_alias( + class_name: &str, + method_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_method_call_result(class_name, method_name, positional_args(args), context, values) +} + +/// Calls one object-method alias with selected argument indices. +fn eval_method_alias( + args: Vec, + object_index: usize, + method_name: &str, + arg_indices: &[usize], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(object) = args.get(object_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + let mut method_args = Vec::with_capacity(arg_indices.len()); + for index in arg_indices { + let Some(arg) = args.get(*index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + method_args.push(arg); + } + eval_method_call_result(object, method_name, method_args, context, values) +} + +/// Calls one object-method alias with every argument after the receiver. +fn eval_method_alias_tail( + args: Vec, + object_index: usize, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(object) = args.get(object_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + let method_args = args + .iter() + .enumerate() + .filter_map(|(index, arg)| (index != object_index).then_some(*arg)) + .collect(); + eval_method_call_result(object, method_name, method_args, context, values) +} + +/// Implements `unixtojd($timestamp = time())`. +fn eval_unixtojd_alias( + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (args, temp) = match args.as_slice() { + [] => { + let now = eval_time_result(values)?; + (vec![now], Some(now)) + } + [_] => (args, None), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let result = eval_static_alias("DateTime", "__elephc_unixtojd", args, context, values); + if let Some(temp) = temp { + values.release(temp)?; + } + result +} + +/// Implements `easter_days()` and `easter_date()` default-year filling. +fn eval_easter_alias( + method_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + let (args, temp) = if args.is_empty() { + let year = eval_current_date_part_int("date", "Y", context, values)?; + (vec![year], Some(year)) + } else { + (args, None) + }; + let result = eval_static_alias("DateTime", method_name, args, context, values); + if let Some(temp) = temp { + values.release(temp)?; + } + result +} + +/// Implements `date_sunrise()` and `date_sunset()` by prepending the synthetic flag. +fn eval_date_sunfunc_alias( + sunset: bool, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=6).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let which = values.int(if sunset { 1 } else { 0 })?; + let mut call_args = Vec::with_capacity(args.len() + 1); + call_args.push(which); + call_args.extend(args); + let result = eval_static_alias("DateTime", "__elephc_date_sunfunc", call_args, context, values); + values.release(which)?; + result +} + +/// Implements `strftime()` and `gmstrftime()` by adding timestamp and UTC flag. +fn eval_strftime_alias( + utc: bool, + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() != 1 && args.len() != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut call_args = Vec::with_capacity(3); + call_args.push(args[0]); + let mut temps = Vec::new(); + if let Some(timestamp) = args.get(1) { + call_args.push(*timestamp); + } else { + let timestamp = eval_time_result(values)?; + temps.push(timestamp); + call_args.push(timestamp); + } + let utc = values.bool_value(utc)?; + temps.push(utc); + call_args.push(utc); + let result = eval_static_alias("DateTime", "__elephc_strftime", call_args, context, values); + for temp in temps { + values.release(temp)?; + } + result +} + +/// Returns the bundled timezone database version. +fn eval_timezone_version_alias( + args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string(EVAL_TZ_VERSION.trim()) +} + +/// Evaluates one current date part as an integer runtime cell. +fn eval_current_date_part_int( + date_name: &str, + spec: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let format = values.string(spec)?; + let date = match eval_date_result(date_name, format, None, context, values) { + Ok(date) => date, + Err(status) => { + values.release(format)?; + return Err(status); + } + }; + values.release(format)?; + let value = values.cast_int(date); + values.release(date)?; + value +} + +/// Normalizes a possible alias name to its lowercase bare name. +fn eval_date_alias_key(name: &str) -> Option { + let bare = name + .rsplit('\\') + .next() + .unwrap_or("") + .to_ascii_lowercase(); + eval_date_alias_is_supported(&bare).then_some(bare) +} + +/// Returns true when a real builtin should keep handling this named call shape. +fn eval_date_alias_should_fall_back_to_builtin( + name: &str, + args: &[EvaluatedCallArg], +) -> bool { + matches!(name, "mktime" | "gmmktime") && args.iter().any(|arg| arg.name.is_some()) +} + +/// Returns whether eval has runtime dispatch for one procedural date/time alias. +fn eval_date_alias_is_supported(name: &str) -> bool { + matches!( + name, + "idate" + | "mktime" + | "gmmktime" + | "date_create" + | "date_create_immutable" + | "date_create_from_format" + | "date_create_immutable_from_format" + | "date_parse_from_format" + | "date_parse" + | "date_sun_info" + | "date_sunrise" + | "date_sunset" + | "strptime" + | "timezone_name_from_abbr" + | "cal_to_jd" + | "cal_from_jd" + | "cal_days_in_month" + | "cal_info" + | "gregoriantojd" + | "jdtogregorian" + | "juliantojd" + | "jdtojulian" + | "frenchtojd" + | "jdtofrench" + | "jewishtojd" + | "jdtojewish" + | "jddayofweek" + | "jdmonthname" + | "jdtounix" + | "unixtojd" + | "easter_days" + | "easter_date" + | "gettimeofday" + | "date_get_last_errors" + | "strftime" + | "gmstrftime" + | "timezone_open" + | "timezone_identifiers_list" + | "timezone_location_get" + | "timezone_transitions_get" + | "timezone_abbreviations_list" + | "timezone_version_get" + | "date_interval_create_from_date_string" + | "date_diff" + | "date_format" + | "date_add" + | "date_sub" + | "date_modify" + | "date_timestamp_get" + | "date_timestamp_set" + | "date_timezone_get" + | "date_timezone_set" + | "date_offset_get" + | "date_date_set" + | "date_isodate_set" + | "date_time_set" + | "date_interval_format" + | "timezone_name_get" + | "timezone_offset_get" + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs index cf38c37b5f..ee2aa91489 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs @@ -10,6 +10,7 @@ //! local calendar conversions where PHP behavior depends on host locale/timezone. mod calendar; +mod aliases; mod clock; mod date; mod mktime; @@ -17,6 +18,7 @@ mod sleep; mod strtotime; mod system; +pub(in crate::interpreter) use aliases::*; pub(in crate::interpreter) use calendar::*; pub(in crate::interpreter) use clock::*; pub(in crate::interpreter) use date::*; diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index b1664ba088..333ffa8b30 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -791,6 +791,9 @@ pub(in crate::interpreter) fn eval_call( if matches!(name, "fsockopen" | "pfsockopen") { return eval_builtin_fsockopen_call(args, context, scope, values); } + if let Some(result) = eval_date_procedural_alias_call(name, args, context, scope, values)? { + return Ok(result); + } if name == "stream_select" { return eval_builtin_stream_select_call(args, context, scope, values); } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs index a03ec19506..47c170b86d 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs @@ -172,6 +172,26 @@ echo function_exists("does_not_exist_alias_xyz") ? "x" : "X";"#, assert_eq!(values.output, "1".repeat(59) + ":CNIX"); } +/// Verifies simple procedural date/time aliases execute directly and by callable. +#[test] +fn execute_program_dispatches_simple_date_procedural_aliases() { + let program = parse_fragment( + br#"echo timezone_version_get() === "" ? "v" : "V"; +echo call_user_func("timezone_version_get") === timezone_version_get() ? "C" : "c"; +echo ":"; +echo idate("Y", 0); +echo ":"; +echo call_user_func("idate", "Y", 0);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "VC:1970:1970"); +} + /// Verifies eval `interface_exists()` probes generated interface metadata by callable. #[test] fn execute_program_interface_exists_uses_runtime_probe() { diff --git a/src/ir_lower/builtin_datetime.rs b/src/ir_lower/builtin_datetime.rs index 3cd226009d..2e0eee1541 100644 --- a/src/ir_lower/builtin_datetime.rs +++ b/src/ir_lower/builtin_datetime.rs @@ -21,7 +21,7 @@ use std::collections::HashSet; -use crate::ir::{Immediate, Module, Op}; +use crate::ir::{Function, Immediate, Module, Op}; use crate::ir_lower::function; use crate::parser::ast::ExprKind; use crate::types::{CheckResult, PhpType}; @@ -71,6 +71,7 @@ pub(crate) fn lower_referenced_builtin_datetime_methods( constants: &std::collections::HashMap, fiber_return_sigs: &std::collections::HashMap, ) { + lower_eval_date_alias_methods_if_needed(module, check_result, constants, fiber_return_sigs); loop { let mut methods = referenced_builtin_datetime_methods(module); methods.sort(); @@ -99,6 +100,145 @@ pub(crate) fn lower_referenced_builtin_datetime_methods( } } +/// Lowers DateTime-family methods that runtime eval aliases may call dynamically. +fn lower_eval_date_alias_methods_if_needed( + module: &mut Module, + check_result: &CheckResult, + constants: &std::collections::HashMap, + fiber_return_sigs: &std::collections::HashMap, +) { + if !module_uses_eval(module) { + return; + } + let mut methods = eval_date_alias_builtin_datetime_methods(module); + methods.sort(); + methods.dedup(); + for (class_name, method_key) in methods { + lower_builtin_datetime_method( + &class_name, + &method_key, + module, + check_result, + constants, + fiber_return_sigs, + ); + } +} + +/// Returns the builtin DateTime-family methods reachable from eval alias dispatch. +fn eval_date_alias_builtin_datetime_methods(module: &Module) -> Vec<(String, String)> { + let mut methods = Vec::new(); + for class_name in ["DateTime", "DateTimeImmutable", "DateTimeZone", "DateInterval"] { + push_constructor_and_interface_methods(&mut methods, module, class_name); + } + for method_name in [ + "createFromFormat", + "getLastErrors", + "__elephc_date_parse_from_format", + "__elephc_date_parse", + "__elephc_date_sun_info", + "__elephc_date_sunfunc", + "__elephc_strptime", + "__elephc_timezone_name_from_abbr", + "__elephc_cal_to_jd", + "__elephc_cal_from_jd", + "__elephc_cal_days_in_month", + "__elephc_cal_info", + "__elephc_gregoriantojd", + "__elephc_jdtogregorian", + "__elephc_juliantojd", + "__elephc_jdtojulian", + "__elephc_frenchtojd", + "__elephc_jdtofrench", + "__elephc_jewishtojd", + "__elephc_jdtojewish", + "__elephc_jddayofweek", + "__elephc_jdmonthname", + "__elephc_jdtounix", + "__elephc_unixtojd", + "__elephc_easter_days", + "__elephc_easter_date", + "__elephc_gettimeofday", + "__elephc_strftime", + "diff", + "format", + "add", + "sub", + "modify", + "getTimestamp", + "setTimestamp", + "getTimezone", + "setTimezone", + "getOffset", + "setDate", + "setISODate", + "setTime", + ] { + methods.push(("DateTime".to_string(), php_method_key(method_name))); + methods.push(("DateTimeImmutable".to_string(), php_method_key(method_name))); + } + for method_name in ["createFromDateString", "format"] { + methods.push(("DateInterval".to_string(), php_method_key(method_name))); + } + for method_name in ["getName", "getOffset", "listIdentifiers"] { + methods.push(("DateTimeZone".to_string(), php_method_key(method_name))); + } + methods +} + +/// Returns true when the lowered module has any dependency on the eval bridge. +fn module_uses_eval(module: &Module) -> bool { + module.required_runtime_features.eval + || module + .functions + .iter() + .chain(module.class_methods.iter()) + .chain(module.closures.iter()) + .chain(module.fiber_wrappers.iter()) + .chain(module.callback_wrappers.iter()) + .chain(module.extern_callback_trampolines.iter()) + .chain(module.runtime_callable_invokers.iter()) + .any(|function| function_uses_eval(module, function)) +} + +/// Returns true when one lowered function contains an eval bridge instruction. +fn function_uses_eval(module: &Module, function: &Function) -> bool { + function + .instructions + .iter() + .any(|inst| instruction_uses_eval(module, inst)) +} + +/// Returns true when one instruction requires eval runtime support. +fn instruction_uses_eval(module: &Module, inst: &crate::ir::Instruction) -> bool { + matches!( + inst.op, + Op::EvalFunctionCall + | Op::EvalFunctionCallArray + | Op::EvalObjectNew + | Op::EvalStaticMethodCall + | Op::EvalFunctionExists + | Op::EvalClassExists + | Op::EvalConstantExists + | Op::EvalConstantFetch + ) || builtin_call_is_eval(module, inst) +} + +/// Returns true when one lowered builtin call is PHP's `eval` construct. +fn builtin_call_is_eval(module: &Module, inst: &crate::ir::Instruction) -> bool { + if inst.op != Op::BuiltinCall { + return false; + } + let Some(Immediate::Data(data)) = inst.immediate else { + return false; + }; + module + .data + .function_names + .get(data.as_raw() as usize) + .is_some_and(|name| crate::names::php_symbol_key(name.trim_start_matches('\\')) == "eval") +} + /// Finds builtin date/time methods whose symbols are required by already-lowered EIR. /// /// Returns `(class_name, method_key)` pairs for every `ObjectNew`, diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 578997120d..a16f3637e8 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -2735,6 +2735,26 @@ echo function_exists("does_not_exist_alias_xyz") ? "x" : "X";'); assert_eq!(out, "1".repeat(59) + ":CNIX"); } +/// Verifies eval can execute DateTime-family procedural aliases without static DateTime references. +#[test] +fn test_eval_dispatches_datetime_procedural_aliases_without_static_references() { + let out = compile_and_run( + r#" Date: Tue, 30 Jun 2026 17:09:30 +0200 Subject: [PATCH 0989/1208] Complete eval timezone alias dispatch --- .../src/interpreter/builtins/time/aliases.rs | 112 +++++++++++++++++- .../src/interpreter/tests/builtins_symbols.rs | 7 +- src/ir_lower/builtin_datetime.rs | 9 +- src/list_id_prelude/detect.rs | 21 +++- src/tz_prelude/detect.rs | 21 +++- tests/codegen/eval.rs | 22 ++++ 6 files changed, 184 insertions(+), 8 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs index 1b3adbc254..f1895e3b77 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs @@ -13,8 +13,18 @@ use super::super::super::*; use super::*; +#[path = "../../../../../../src/list_id_prelude/table.rs"] +mod timezone_identifier_table; + const EVAL_TZ_VERSION: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../elephc-tz/data/version.data")); +const EVAL_DATETIMEZONE_ALL: i64 = 2047; +const EVAL_DATETIMEZONE_PER_COUNTRY: i64 = 4096; +const EVAL_DATETIMEZONE_PER_COUNTRY_ERROR: &str = concat!( + "DateTimeZone::listIdentifiers(): Argument #2 ($countryCode) must be a two-letter ", + "ISO 3166-1 compatible country code when argument #1 ($timezoneGroup) is ", + "DateTimeZone::PER_COUNTRY", +); /// Attempts to execute one direct procedural date/time alias call. pub(in crate::interpreter) fn eval_date_procedural_alias_call( @@ -154,9 +164,7 @@ fn eval_date_alias_result( "strftime" => eval_strftime_alias(false, args, context, values), "gmstrftime" => eval_strftime_alias(true, args, context, values), "timezone_open" => eval_new_datetime_alias("DateTimeZone", args, context, values), - "timezone_identifiers_list" => { - eval_static_alias("DateTimeZone", "listIdentifiers", args, context, values) - } + "timezone_identifiers_list" => eval_timezone_identifiers_alias(args, context, values), "timezone_location_get" => eval_method_alias(args, 0, "getLocation", &[], context, values), "timezone_transitions_get" => { eval_method_alias_tail(args, 0, "getTransitions", context, values) @@ -267,6 +275,104 @@ fn eval_static_alias( eval_static_method_call_result(class_name, method_name, positional_args(args), context, values) } +/// Calls the injected list-identifiers prelude function, falling back to the +/// synthetic method if the native prelude function is unavailable. +fn eval_timezone_identifiers_alias( + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(function) = context.native_function("__elephc_list_identifiers") { + let bound_args = + bind_evaluated_native_function_args(&function, positional_args(args), context, values)?; + return eval_native_function_with_values(function, bound_args, context, values); + } + eval_timezone_identifiers_filtered_alias(args, context, values) +} + +/// Implements `timezone_identifiers_list()` filtering for eval-only programs. +fn eval_timezone_identifiers_filtered_alias( + args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + let group = eval_timezone_identifier_group(args.first().copied(), values)?; + let country = eval_timezone_identifier_country(args.get(1).copied(), values)?; + if group & EVAL_DATETIMEZONE_PER_COUNTRY != 0 && country.is_empty() { + return eval_timezone_identifiers_country_error(context, values); + } + let rows = eval_timezone_identifier_rows(group, &country); + let mut result = values.array_new(rows.len())?; + for (index, name) in rows.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string(name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns the requested DateTimeZone group mask, defaulting to `DateTimeZone::ALL`. +fn eval_timezone_identifier_group( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + value + .map(|value| eval_int_value(value, values)) + .unwrap_or(Ok(EVAL_DATETIMEZONE_ALL)) +} + +/// Returns the requested PER_COUNTRY country code, defaulting to the empty marker. +fn eval_timezone_identifier_country( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(value) = value else { + return Ok(String::new()); + }; + String::from_utf8(values.string_bytes(value)?).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns the identifiers matching one DateTimeZone group/country selector. +fn eval_timezone_identifier_rows(group: i64, country: &str) -> Vec<&'static str> { + timezone_identifier_table::TIMEZONE_GROUPS_TABLE + .split(';') + .filter_map(|row| eval_timezone_identifier_row(row, group, country)) + .collect() +} + +/// Returns one table row's identifier when it matches the selector. +fn eval_timezone_identifier_row( + row: &'static str, + group: i64, + country: &str, +) -> Option<&'static str> { + let mut fields = row.split(','); + let name = fields.next()?; + let mask = fields.next()?.parse::().ok()?; + let row_country = fields.next()?; + if group & EVAL_DATETIMEZONE_PER_COUNTRY != 0 { + (row_country == country).then_some(name) + } else { + (mask & group != 0).then_some(name) + } +} + +/// Throws PHP's `ValueError` for PER_COUNTRY calls without a country code. +fn eval_timezone_identifiers_country_error( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("ValueError")?; + let message = values.string(EVAL_DATETIMEZONE_PER_COUNTRY_ERROR)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + /// Calls one object-method alias with selected argument indices. fn eval_method_alias( args: Vec, diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs index 47c170b86d..72168913c5 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_symbols.rs @@ -181,7 +181,10 @@ echo call_user_func("timezone_version_get") === timezone_version_get() ? "C" : " echo ":"; echo idate("Y", 0); echo ":"; -echo call_user_func("idate", "Y", 0);"#, +echo call_user_func("idate", "Y", 0); +echo ":"; +$ids = timezone_identifiers_list(512); +echo count($ids) . ":" . $ids[0];"#, ) .expect("parse eval fragment"); let mut scope = ElephcEvalScope::new(); @@ -189,7 +192,7 @@ echo call_user_func("idate", "Y", 0);"#, let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.output, "VC:1970:1970"); + assert_eq!(values.output, "VC:1970:1970:38:Pacific/Apia"); } /// Verifies eval `interface_exists()` probes generated interface metadata by callable. diff --git a/src/ir_lower/builtin_datetime.rs b/src/ir_lower/builtin_datetime.rs index 2e0eee1541..105c35f837 100644 --- a/src/ir_lower/builtin_datetime.rs +++ b/src/ir_lower/builtin_datetime.rs @@ -180,7 +180,14 @@ fn eval_date_alias_builtin_datetime_methods(module: &Module) -> Vec<(String, Str for method_name in ["createFromDateString", "format"] { methods.push(("DateInterval".to_string(), php_method_key(method_name))); } - for method_name in ["getName", "getOffset", "listIdentifiers"] { + for method_name in [ + "getName", + "getOffset", + "listIdentifiers", + "getLocation", + "getTransitions", + "listAbbreviations", + ] { methods.push(("DateTimeZone".to_string(), php_method_key(method_name))); } methods diff --git a/src/list_id_prelude/detect.rs b/src/list_id_prelude/detect.rs index 20175e71da..54bbcf0dd9 100644 --- a/src/list_id_prelude/detect.rs +++ b/src/list_id_prelude/detect.rs @@ -36,6 +36,16 @@ fn name_is_listid_fn(name: &Name) -> bool { .is_some_and(|segment| segment.eq_ignore_ascii_case("timezone_identifiers_list")) } +/// Returns whether a function name is PHP's `eval` construct. +/// +/// Runtime eval can call `timezone_identifiers_list()` from a string that this +/// prelude detector cannot inspect statically, so an `eval(...)` call is treated +/// as a potential identifier-listing use. +fn name_is_eval_fn(name: &Name) -> bool { + name.last_segment() + .is_some_and(|segment| segment.eq_ignore_ascii_case("eval")) +} + /// Returns whether a method name is `listIdentifiers`, compared case-insensitively /// as PHP method names are. fn method_is_listid(method: &str) -> bool { @@ -128,7 +138,7 @@ fn expr_refs_listid(expr: &Expr) -> bool { | ExprKind::MagicConstant(_) => false, ExprKind::FunctionCall { name, args } => { - name_is_listid_fn(name) || args.iter().any(expr_refs_listid) + name_is_eval_fn(name) || name_is_listid_fn(name) || args.iter().any(expr_refs_listid) } ExprKind::MethodCall { object, @@ -460,6 +470,15 @@ mod tests { ))); } + /// A runtime `eval(...)` call is detected because the eval fragment can call + /// `timezone_identifiers_list()` dynamically. + #[test] + fn detects_eval_call() { + assert!(program_uses_list_identifiers(&parse( + r#" bool { }) } +/// Returns whether a function name is PHP's `eval` construct. +/// +/// Runtime eval can call the introspection aliases from a string that this +/// prelude detector cannot inspect statically, so an `eval(...)` call is treated +/// as a potential introspection use. +fn name_is_eval_fn(name: &Name) -> bool { + name.last_segment() + .is_some_and(|segment| segment.eq_ignore_ascii_case("eval")) +} + /// Returns whether a method name is one of the three introspection methods, /// compared case-insensitively as PHP method names are. fn method_is_tz(method: &str) -> bool { @@ -131,7 +141,7 @@ fn expr_refs_tz(expr: &Expr) -> bool { | ExprKind::MagicConstant(_) => false, ExprKind::FunctionCall { name, args } => { - name_is_tz_fn(name) || args.iter().any(expr_refs_tz) + name_is_eval_fn(name) || name_is_tz_fn(name) || args.iter().any(expr_refs_tz) } ExprKind::MethodCall { object, @@ -455,6 +465,15 @@ mod tests { ))); } + /// A runtime `eval(...)` call is detected because the eval fragment can call + /// the introspection aliases dynamically. + #[test] + fn detects_eval_call() { + assert!(program_uses_tz_introspection(&parse( + r#" Date: Tue, 30 Jun 2026 18:11:15 +0200 Subject: [PATCH 0990/1208] Fix eval method throw bridge writeback --- .../elephc-magician/src/runtime_hooks/ops.rs | 29 ++- src/codegen/eval_method_helpers.rs | 199 +++++++++++++++--- tests/codegen/eval_callable_ref_errors.rs | 147 +++++++++++++ tests/codegen/mod.rs | 1 + 4 files changed, 337 insertions(+), 39 deletions(-) create mode 100644 tests/codegen/eval_callable_ref_errors.rs diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index b5eed9b6b4..4a4c20e54c 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -15,7 +15,7 @@ use super::ElephcRuntimeOps; use crate::errors::EvalStatus; use crate::eval_ir::EvalBinOp; use crate::interpreter::RuntimeValueOps; -use crate::value::RuntimeCellHandle; +use crate::value::{RuntimeCell, RuntimeCellHandle}; #[cfg(not(test))] impl RuntimeValueOps for ElephcRuntimeOps { @@ -274,7 +274,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { ) -> Result { let (scope_ptr, scope_len) = self.current_class_scope_abi(); let arg_array = Self::arg_array(args)?; - let result = Self::handle(unsafe { + let result = unsafe { __elephc_eval_value_method_call( object.as_ptr(), method.as_ptr(), @@ -283,11 +283,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { scope_ptr, scope_len, ) - }); + }; unsafe { __elephc_eval_value_release(arg_array.as_ptr()); } - result + self.handle_native_call_result(result) } /// Calls an AOT static method through the generated user helper. @@ -299,7 +299,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { ) -> Result { let (scope_ptr, scope_len) = self.current_class_scope_abi(); let arg_array = Self::arg_array(args)?; - let result = Self::handle(unsafe { + let result = unsafe { __elephc_eval_value_static_method_call( class_name.as_ptr(), class_name.len() as u64, @@ -309,11 +309,11 @@ impl RuntimeValueOps for ElephcRuntimeOps { scope_ptr, scope_len, ) - }); + }; unsafe { __elephc_eval_value_release(arg_array.as_ptr()); } - result + self.handle_native_call_result(result) } /// Materializes a populated synthetic `ReflectionAttribute` object for eval metadata. @@ -1113,6 +1113,21 @@ impl RuntimeValueOps for ElephcRuntimeOps { #[cfg(not(test))] impl ElephcRuntimeOps { + /// Converts a generated native method-call result into an eval result status. + fn handle_native_call_result( + &self, + result: *mut RuntimeCell, + ) -> Result { + if !result.is_null() { + return Ok(RuntimeCellHandle::from_raw(result)); + } + self.take_pending_native_throwable() + .map_or(Err(EvalStatus::RuntimeFatal), |thrown| { + self.schedule_pending_throw(thrown)?; + Err(EvalStatus::UncaughtThrowable) + }) + } + /// Takes a native Throwable that escaped through the generated constructor bridge. fn take_pending_native_throwable(&self) -> Option { let thrown = unsafe { __elephc_eval_value_take_pending_throwable() }; diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 9606b1f22a..f96db2b866 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -14,6 +14,9 @@ use std::collections::BTreeMap; use crate::codegen::abi; +use crate::codegen::context::{ + TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, TRY_HANDLER_SLOT_SIZE, +}; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::emit_box_current_value_as_mixed; @@ -85,6 +88,13 @@ const BUILTIN_THROWABLE_METHOD_CLASSES: &[&str] = &[ ]; const BUILTIN_THROWABLE_GET_MESSAGE_LABEL: &str = "__elephc_eval_builtin_throwable_getmessage"; const BUILTIN_THROWABLE_GET_CODE_LABEL: &str = "__elephc_eval_builtin_throwable_getcode"; +const METHOD_HELPER_BASE_FRAME_SIZE: usize = 64; +const METHOD_HELPER_HANDLER_OFFSET: usize = METHOD_HELPER_BASE_FRAME_SIZE; +const METHOD_HELPER_FRAME_SIZE: usize = METHOD_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; +const STATIC_METHOD_HELPER_BASE_FRAME_SIZE: usize = 80; +const STATIC_METHOD_HELPER_HANDLER_OFFSET: usize = STATIC_METHOD_HELPER_BASE_FRAME_SIZE; +const STATIC_METHOD_HELPER_FRAME_SIZE: usize = + STATIC_METHOD_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; /// Emits eval method-call helpers when any lowered function owns an eval context. pub(super) fn emit_eval_method_helpers( @@ -484,7 +494,7 @@ fn emit_static_method_call_aarch64( ) { let fail_label = "__elephc_eval_value_static_method_call_fail"; let done_label = "__elephc_eval_value_static_method_call_done"; - emitter.instruction("sub sp, sp, #80"); // reserve helper frame for class, method, args, scope, and fp/lr + emitter.instruction(&format!("sub sp, sp, #{}", STATIC_METHOD_HELPER_FRAME_SIZE)); // reserve helper frame plus a boundary exception handler emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer @@ -509,7 +519,7 @@ fn emit_static_method_call_aarch64( emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction(&format!("add sp, sp, #{}", STATIC_METHOD_HELPER_FRAME_SIZE)); // release the helper frame and boundary handler emitter.instruction("ret"); // return the boxed static method result to Rust } @@ -525,7 +535,7 @@ fn emit_static_method_call_x86_64( let done_label = "__elephc_eval_value_static_method_call_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 80"); // reserve aligned slots for class, method, args, scope, and one argument + emitter.instruction(&format!("sub rsp, {}", STATIC_METHOD_HELPER_FRAME_SIZE)); // reserve aligned slots plus a boundary exception handler emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested method-name pointer @@ -564,7 +574,7 @@ fn emit_method_call_aarch64( ) { let fail_label = "__elephc_eval_value_method_call_fail"; let done_label = "__elephc_eval_value_method_call_done"; - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for inputs, object, scope, and fp/lr + emitter.instruction(&format!("sub sp, sp, #{}", METHOD_HELPER_FRAME_SIZE)); // reserve helper frame plus a boundary exception handler emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer emitter.instruction("str x1, [sp, #0]"); // save the requested method-name pointer @@ -599,7 +609,7 @@ fn emit_method_call_aarch64( emitter.instruction("mov x0, xzr"); // return a null pointer so Rust reports runtime failure emitter.label(done_label); emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction(&format!("add sp, sp, #{}", METHOD_HELPER_FRAME_SIZE)); // release the helper frame and boundary handler emitter.instruction("ret"); // return the boxed method result to Rust } @@ -616,7 +626,7 @@ fn emit_method_call_x86_64( let done_label = "__elephc_eval_value_method_call_done_x"; emitter.instruction("push rbp"); // preserve the Rust caller frame pointer emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 64"); // reserve aligned slots for name, length, object, args, scope, and first argument + emitter.instruction(&format!("sub rsp, {}", METHOD_HELPER_FRAME_SIZE)); // reserve aligned slots plus a boundary exception handler emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested method-name pointer emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested method-name length emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed eval argument array @@ -655,6 +665,83 @@ fn emit_method_call_x86_64( emitter.instruction("ret"); // return the boxed method result to Rust } +/// Emits an ARM64 boundary handler so native method throws return to magician. +fn emit_aarch64_method_exception_boundary_push( + emitter: &mut Emitter, + handler_offset: usize, + escape_label: &str, +) { + emitter.comment("push eval method exception boundary"); + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("str x10, [x29, #{}]", handler_offset + 8)); // preserve the caller activation frame across method unwinding + abi::emit_load_symbol_to_reg(emitter, "x10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "str x10, [x29, #{}]", + handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("add x10, x29, #{}", handler_offset)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "add x0, x29, #{}", + handler_offset + TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native methods + emitter.instruction(&format!("cbnz x0, {}", escape_label)); // non-zero setjmp result means a method Throwable escaped +} + +/// Emits an ARM64 boundary pop after a native method call returns to magician. +fn emit_aarch64_method_exception_boundary_pop(emitter: &mut Emitter, handler_offset: usize) { + emitter.comment("pop eval method exception boundary"); + emitter.instruction(&format!("ldr x10, [x29, #{}]", handler_offset)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "ldr x10, [x29, #{}]", + handler_offset + TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "x10", "_rt_diag_suppression", 0); +} + +/// Emits an x86_64 boundary handler so native method throws return to magician. +fn emit_x86_64_method_exception_boundary_push( + emitter: &mut Emitter, + handler_base: usize, + escape_label: &str, +) { + emitter.comment("push eval method exception boundary"); + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base - 8)); // preserve the caller activation frame across method unwinding + abi::emit_load_symbol_to_reg(emitter, "r10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "mov QWORD PTR [rbp - {}], r10", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("lea r10, [rbp - {}]", handler_base)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "lea rdi, [rbp - {}]", + handler_base - TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering native methods + emitter.instruction("test eax, eax"); // did control arrive through longjmp? + emitter.instruction(&format!("jne {}", escape_label)); // non-zero setjmp result means a method Throwable escaped +} + +/// Emits an x86_64 boundary pop after a native method call returns to magician. +fn emit_x86_64_method_exception_boundary_pop(emitter: &mut Emitter, handler_base: usize) { + emitter.comment("pop eval method exception boundary"); + emitter.instruction(&format!("mov r10, QWORD PTR [rbp - {}]", handler_base)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "mov r10, QWORD PTR [rbp - {}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "r10", "_rt_diag_suppression", 0); +} + /// Emits ARM64 class-id and method-name dispatch for helper method bodies. fn emit_aarch64_method_dispatch( module: &Module, @@ -1160,7 +1247,7 @@ fn emit_aarch64_method_bodies( for slot in slots { emitter.label(&method_body_label(module, slot)); emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); - let (overflow_bytes, ref_slots) = + let (arg_temp_bytes, ref_slots) = emit_aarch64_prepare_method_args( module, emitter, @@ -1169,6 +1256,15 @@ fn emit_aarch64_method_bodies( fail_label, callable_support, ); + let escape_label = format!("{}_escape", method_body_label(module, slot)); + emit_aarch64_method_exception_boundary_push( + emitter, + METHOD_HELPER_HANDLER_OFFSET - 48, + &escape_label, + ); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + let overflow_bytes = + materialize_method_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1185,7 +1281,15 @@ fn emit_aarch64_method_bodies( &ref_slots, &method_body_label(module, slot), ); + emit_aarch64_method_exception_boundary_pop(emitter, METHOD_HELPER_HANDLER_OFFSET - 48); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", method_body_label(module, slot)); + emit_aarch64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_aarch64_method_exception_boundary_pop(emitter, METHOD_HELPER_HANDLER_OFFSET - 48); + emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes } } @@ -1202,7 +1306,7 @@ fn emit_x86_64_method_bodies( for slot in slots { emitter.label(&method_body_label(module, slot)); emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); - let (overflow_bytes, ref_slots) = + let (arg_temp_bytes, ref_slots) = emit_x86_64_prepare_method_args( module, emitter, @@ -1211,6 +1315,11 @@ fn emit_x86_64_method_bodies( fail_label, callable_support, ); + let escape_label = format!("{}_escape_x", method_body_label(module, slot)); + emit_x86_64_method_exception_boundary_push(emitter, METHOD_HELPER_FRAME_SIZE, &escape_label); + let receiver_ty = PhpType::Object(slot.class_name.clone()); + let overflow_bytes = + materialize_method_args(module, emitter, &receiver_ty, &slot.params, &slot.ref_params); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1227,7 +1336,15 @@ fn emit_x86_64_method_bodies( &ref_slots, &method_body_label(module, slot), ); + emit_x86_64_method_exception_boundary_pop(emitter, METHOD_HELPER_FRAME_SIZE); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", method_body_label(module, slot)); + emit_x86_64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_x86_64_method_exception_boundary_pop(emitter, METHOD_HELPER_FRAME_SIZE); + emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes } } @@ -1244,7 +1361,7 @@ fn emit_aarch64_static_method_bodies( for slot in slots { emitter.label(&static_method_body_label(module, slot)); emit_aarch64_validate_static_method_arg_count(module, emitter, slot, fail_label); - let (overflow_bytes, ref_slots) = + let (arg_temp_bytes, ref_slots) = emit_aarch64_prepare_static_method_args( module, emitter, @@ -1253,6 +1370,13 @@ fn emit_aarch64_static_method_bodies( fail_label, callable_support, ); + let escape_label = format!("{}_escape", static_method_body_label(module, slot)); + emit_aarch64_method_exception_boundary_push( + emitter, + STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, + &escape_label, + ); + let overflow_bytes = materialize_static_method_args(module, emitter, slot); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1268,7 +1392,21 @@ fn emit_aarch64_static_method_bodies( &ref_slots, &static_method_body_label(module, slot), ); + emit_aarch64_method_exception_boundary_pop( + emitter, + STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, + ); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native static method result + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", static_method_body_label(module, slot)); + emit_aarch64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_aarch64_method_exception_boundary_pop( + emitter, + STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, + ); + emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes } } @@ -1285,7 +1423,7 @@ fn emit_x86_64_static_method_bodies( for slot in slots { emitter.label(&static_method_body_label(module, slot)); emit_x86_64_validate_static_method_arg_count(module, emitter, slot, fail_label); - let (overflow_bytes, ref_slots) = + let (arg_temp_bytes, ref_slots) = emit_x86_64_prepare_static_method_args( module, emitter, @@ -1294,6 +1432,13 @@ fn emit_x86_64_static_method_bodies( fail_label, callable_support, ); + let escape_label = format!("{}_escape_x", static_method_body_label(module, slot)); + emit_x86_64_method_exception_boundary_push( + emitter, + STATIC_METHOD_HELPER_FRAME_SIZE, + &escape_label, + ); + let overflow_bytes = materialize_static_method_args(module, emitter, slot); let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(module.target, overflow_bytes); abi::emit_reserve_temporary_stack(emitter, caller_stack_pad_bytes); @@ -1309,7 +1454,15 @@ fn emit_x86_64_static_method_bodies( &ref_slots, &static_method_body_label(module, slot), ); + emit_x86_64_method_exception_boundary_pop(emitter, STATIC_METHOD_HELPER_FRAME_SIZE); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native static method result + emitter.label(&escape_label); + abi::emit_release_temporary_stack(emitter, arg_temp_bytes); + let escape_writeback_label = format!("{}_throw", static_method_body_label(module, slot)); + emit_x86_64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); + abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); + emit_x86_64_method_exception_boundary_pop(emitter, STATIC_METHOD_HELPER_FRAME_SIZE); + emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes } } @@ -1423,16 +1576,7 @@ fn emit_aarch64_prepare_method_args( } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - ( - materialize_method_args( - module, - emitter, - &receiver_ty, - &slot.params, - &slot.ref_params, - ), - ref_slots, - ) + (arg_temp_bytes, ref_slots) } /// Prepares ARM64 static method ABI registers for the supported argument shapes. @@ -1484,7 +1628,7 @@ fn emit_aarch64_prepare_static_method_args( } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - (materialize_static_method_args(module, emitter, slot), ref_slots) + (arg_temp_bytes, ref_slots) } /// Prepares x86_64 method ABI registers for the supported argument shapes. @@ -1536,16 +1680,7 @@ fn emit_x86_64_prepare_method_args( } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - ( - materialize_method_args( - module, - emitter, - &receiver_ty, - &slot.params, - &slot.ref_params, - ), - ref_slots, - ) + (arg_temp_bytes, ref_slots) } /// Prepares x86_64 static method ABI registers for the supported argument shapes. @@ -1596,7 +1731,7 @@ fn emit_x86_64_prepare_static_method_args( } arg_temp_bytes += eval_arg_temp_slot_size(&visible_abi_params[index]); } - (materialize_static_method_args(module, emitter, slot), ref_slots) + (arg_temp_bytes, ref_slots) } /// Materializes the pushed receiver and eval arguments into the target method ABI. diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs new file mode 100644 index 0000000000..5c3c76a1f9 --- /dev/null +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -0,0 +1,147 @@ +//! Purpose: +//! End-to-end regressions for eval callable by-reference writeback on error paths. +//! Covers callable values crossing eval into generated/AOT and eval-declared methods. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_callable_ref_errors` through Rust's test harness. +//! +//! Key details: +//! - Fixtures verify caller-side by-reference values are written back before a +//! method callable's catchable throw is returned through the eval bridge. + +use crate::support::{compile_and_run, compile_and_run_capture}; + +/// Verifies AOT method callable by-reference args write back before catchable throws. +#[test] +fn test_eval_aot_method_callables_write_back_by_ref_args_before_throw() { + let out = compile_and_run_capture( + r#"base + $delta; + throw new Exception("aot-instance"); + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + throw new Exception("aot-static"); + } +} + +echo eval('$box = new EvalAotThrowCallableBridge(); + +$array = [$box, "bump"]; +$a = "2"; +try { + $array($a, 3); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($a) . ":" . $a . "|"; +} + +$string = "EvalAotThrowCallableBridge::add"; +$b = "4"; +try { + $string($b, 5); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($b) . ":" . $b . "|"; +} + +$first = $box->bump(...); +$c = "6"; +try { + $first($c, 7); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($c) . ":" . $c . "|"; +} + +$closure = Closure::fromCallable(["EvalAotThrowCallableBridge", "add"]); +$d = "8"; +try { + call_user_func_array($closure, [&$d, 9]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($d) . ":" . $d; +}'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Exception:aot-instance:integer:15|Exception:aot-static:integer:9|Exception:aot-instance:integer:23|Exception:aot-static:integer:17" + ); +} + +/// Verifies eval-declared method callable by-reference args write back before catchable throws. +#[test] +fn test_eval_declared_method_callables_write_back_by_ref_args_before_throw() { + let out = compile_and_run( + r#"base + $delta; + throw new Exception("eval-instance"); + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + throw new Exception("eval-static"); + } +} + +$box = new EvalDeclaredThrowCallableBridge(); + +$array = [$box, "bump"]; +$a = "2"; +try { + $array($a, 3); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($a) . ":" . $a . "|"; +} + +$string = "EvalDeclaredThrowCallableBridge::add"; +$b = "4"; +try { + $string($b, 5); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($b) . ":" . $b . "|"; +} + +$first = $box->bump(...); +$c = "6"; +try { + $first($c, 7); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($c) . ":" . $c . "|"; +} + +$closure = Closure::fromCallable(["EvalDeclaredThrowCallableBridge", "add"]); +$d = "8"; +try { + call_user_func_array($closure, [&$d, 9]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($d) . ":" . $d; +}'); +"#, + ); + + assert_eq!( + out, + "Exception:eval-instance:integer:25|Exception:eval-static:integer:9|Exception:eval-instance:integer:33|Exception:eval-static:integer:17" + ); +} diff --git a/tests/codegen/mod.rs b/tests/codegen/mod.rs index b19393d05a..f7d420e2ea 100644 --- a/tests/codegen/mod.rs +++ b/tests/codegen/mod.rs @@ -19,6 +19,7 @@ mod benchmarks; mod echo_vars; mod eval; mod eval_builtin_parity; +mod eval_callable_ref_errors; mod eval_callables; mod eval_closures; mod eval_constructors; From 06aa10ec6e9e7eeda856634d3dada4fae82151b2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 18:54:43 +0200 Subject: [PATCH 0991/1208] Fix eval bridge arg-prep fatal cleanup --- src/codegen/eval_constructor_helpers.rs | 22 +++++- src/codegen/eval_method_helpers.rs | 88 +++++++++++++---------- tests/codegen/eval_callable_ref_errors.rs | 77 ++++++++++++++++++++ tests/codegen/eval_constructors.rs | 38 +++++++++- 4 files changed, 186 insertions(+), 39 deletions(-) diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 1db25da362..39e77614b0 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -792,13 +792,14 @@ fn emit_aarch64_constructor_body( } emit_aarch64_validate_constructor_arg_count(module, emitter, slot, fail_label); let body_label = constructor_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail", body_label); let (arg_temp_bytes, ref_slots) = emit_aarch64_prepare_constructor_args( module, emitter, data, slot, - fail_label, + &prep_fail_label, callable_support, ); let escape_label = format!("{}_escape", body_label); @@ -831,6 +832,8 @@ fn emit_aarch64_constructor_body( abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); emit_aarch64_constructor_exception_boundary_pop(emitter); emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_aarch64_constructor_prep_fail_cleanup(emitter, fail_label); } /// Emits one x86_64 constructor body or failure branch for an unsupported constructor. @@ -854,13 +857,14 @@ fn emit_x86_64_constructor_body( } emit_x86_64_validate_constructor_arg_count(module, emitter, slot, fail_label); let body_label = constructor_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail_x", body_label); let (arg_temp_bytes, ref_slots) = emit_x86_64_prepare_constructor_args( module, emitter, data, slot, - fail_label, + &prep_fail_label, callable_support, ); let escape_label = format!("{}_escape_x", body_label); @@ -893,6 +897,20 @@ fn emit_x86_64_constructor_body( abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); emit_x86_64_constructor_exception_boundary_pop(emitter); emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_x86_64_constructor_prep_fail_cleanup(emitter, fail_label); +} + +/// Restores an ARM64 constructor-helper frame before reporting an argument-prep fatal. +fn emit_aarch64_constructor_prep_fail_cleanup(emitter: &mut Emitter, fail_label: &str) { + emitter.instruction("sub sp, x29, #48"); // restore the helper frame base after argument staging failed + emitter.instruction(&format!("b {}", fail_label)); // report the argument-prep failure through the shared fail path +} + +/// Restores an x86_64 constructor-helper frame before reporting an argument-prep fatal. +fn emit_x86_64_constructor_prep_fail_cleanup(emitter: &mut Emitter, fail_label: &str) { + emitter.instruction("mov rsp, rbp"); // restore the helper frame base after argument staging failed + emitter.instruction(&format!("jmp {}", fail_label)); // report the argument-prep failure through the shared fail path } /// Emits ARM64 visibility checks for a protected/private constructor bridge hit. diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index f96db2b866..5a85f9d9ee 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -1245,7 +1245,9 @@ fn emit_aarch64_method_bodies( callable_support: &EvalCallableDescriptorSupport, ) { for slot in slots { - emitter.label(&method_body_label(module, slot)); + let body_label = method_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail", body_label); + emitter.label(&body_label); emit_aarch64_validate_method_arg_count(module, emitter, slot, fail_label); let (arg_temp_bytes, ref_slots) = emit_aarch64_prepare_method_args( @@ -1253,10 +1255,10 @@ fn emit_aarch64_method_bodies( emitter, data, slot, - fail_label, + &prep_fail_label, callable_support, ); - let escape_label = format!("{}_escape", method_body_label(module, slot)); + let escape_label = format!("{}_escape", body_label); emit_aarch64_method_exception_boundary_push( emitter, METHOD_HELPER_HANDLER_OFFSET - 48, @@ -1276,20 +1278,18 @@ fn emit_aarch64_method_bodies( abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); - preserve_result_and_write_back_aarch64_ref_args( - emitter, - &ref_slots, - &method_body_label(module, slot), - ); + preserve_result_and_write_back_aarch64_ref_args(emitter, &ref_slots, &body_label); emit_aarch64_method_exception_boundary_pop(emitter, METHOD_HELPER_HANDLER_OFFSET - 48); emitter.instruction(&format!("b {}", done_label)); // return after boxing the native method result emitter.label(&escape_label); abi::emit_release_temporary_stack(emitter, arg_temp_bytes); - let escape_writeback_label = format!("{}_throw", method_body_label(module, slot)); + let escape_writeback_label = format!("{}_throw", body_label); emit_aarch64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); emit_aarch64_method_exception_boundary_pop(emitter, METHOD_HELPER_HANDLER_OFFSET - 48); emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_aarch64_method_prep_fail_cleanup(emitter, 48, fail_label); } } @@ -1304,7 +1304,9 @@ fn emit_x86_64_method_bodies( callable_support: &EvalCallableDescriptorSupport, ) { for slot in slots { - emitter.label(&method_body_label(module, slot)); + let body_label = method_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail_x", body_label); + emitter.label(&body_label); emit_x86_64_validate_method_arg_count(module, emitter, slot, fail_label); let (arg_temp_bytes, ref_slots) = emit_x86_64_prepare_method_args( @@ -1312,10 +1314,10 @@ fn emit_x86_64_method_bodies( emitter, data, slot, - fail_label, + &prep_fail_label, callable_support, ); - let escape_label = format!("{}_escape_x", method_body_label(module, slot)); + let escape_label = format!("{}_escape_x", body_label); emit_x86_64_method_exception_boundary_push(emitter, METHOD_HELPER_FRAME_SIZE, &escape_label); let receiver_ty = PhpType::Object(slot.class_name.clone()); let overflow_bytes = @@ -1331,20 +1333,18 @@ fn emit_x86_64_method_bodies( abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); - preserve_result_and_write_back_x86_64_ref_args( - emitter, - &ref_slots, - &method_body_label(module, slot), - ); + preserve_result_and_write_back_x86_64_ref_args(emitter, &ref_slots, &body_label); emit_x86_64_method_exception_boundary_pop(emitter, METHOD_HELPER_FRAME_SIZE); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native method result emitter.label(&escape_label); abi::emit_release_temporary_stack(emitter, arg_temp_bytes); - let escape_writeback_label = format!("{}_throw", method_body_label(module, slot)); + let escape_writeback_label = format!("{}_throw", body_label); emit_x86_64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); emit_x86_64_method_exception_boundary_pop(emitter, METHOD_HELPER_FRAME_SIZE); emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_x86_64_method_prep_fail_cleanup(emitter, fail_label); } } @@ -1359,7 +1359,9 @@ fn emit_aarch64_static_method_bodies( callable_support: &EvalCallableDescriptorSupport, ) { for slot in slots { - emitter.label(&static_method_body_label(module, slot)); + let body_label = static_method_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail", body_label); + emitter.label(&body_label); emit_aarch64_validate_static_method_arg_count(module, emitter, slot, fail_label); let (arg_temp_bytes, ref_slots) = emit_aarch64_prepare_static_method_args( @@ -1367,10 +1369,10 @@ fn emit_aarch64_static_method_bodies( emitter, data, slot, - fail_label, + &prep_fail_label, callable_support, ); - let escape_label = format!("{}_escape", static_method_body_label(module, slot)); + let escape_label = format!("{}_escape", body_label); emit_aarch64_method_exception_boundary_push( emitter, STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, @@ -1387,11 +1389,7 @@ fn emit_aarch64_static_method_bodies( abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); - preserve_result_and_write_back_aarch64_ref_args( - emitter, - &ref_slots, - &static_method_body_label(module, slot), - ); + preserve_result_and_write_back_aarch64_ref_args(emitter, &ref_slots, &body_label); emit_aarch64_method_exception_boundary_pop( emitter, STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, @@ -1399,7 +1397,7 @@ fn emit_aarch64_static_method_bodies( emitter.instruction(&format!("b {}", done_label)); // return after boxing the native static method result emitter.label(&escape_label); abi::emit_release_temporary_stack(emitter, arg_temp_bytes); - let escape_writeback_label = format!("{}_throw", static_method_body_label(module, slot)); + let escape_writeback_label = format!("{}_throw", body_label); emit_aarch64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); emit_aarch64_method_exception_boundary_pop( @@ -1407,6 +1405,8 @@ fn emit_aarch64_static_method_bodies( STATIC_METHOD_HELPER_HANDLER_OFFSET - 64, ); emitter.instruction(&format!("b {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_aarch64_method_prep_fail_cleanup(emitter, 64, fail_label); } } @@ -1421,7 +1421,9 @@ fn emit_x86_64_static_method_bodies( callable_support: &EvalCallableDescriptorSupport, ) { for slot in slots { - emitter.label(&static_method_body_label(module, slot)); + let body_label = static_method_body_label(module, slot); + let prep_fail_label = format!("{}_prep_fail_x", body_label); + emitter.label(&body_label); emit_x86_64_validate_static_method_arg_count(module, emitter, slot, fail_label); let (arg_temp_bytes, ref_slots) = emit_x86_64_prepare_static_method_args( @@ -1429,10 +1431,10 @@ fn emit_x86_64_static_method_bodies( emitter, data, slot, - fail_label, + &prep_fail_label, callable_support, ); - let escape_label = format!("{}_escape_x", static_method_body_label(module, slot)); + let escape_label = format!("{}_escape_x", body_label); emit_x86_64_method_exception_boundary_push( emitter, STATIC_METHOD_HELPER_FRAME_SIZE, @@ -1449,23 +1451,37 @@ fn emit_x86_64_static_method_bodies( abi::emit_release_temporary_stack(emitter, caller_stack_pad_bytes); abi::emit_release_temporary_stack(emitter, overflow_bytes); emit_box_method_result(module, emitter, &slot.return_ty); - preserve_result_and_write_back_x86_64_ref_args( - emitter, - &ref_slots, - &static_method_body_label(module, slot), - ); + preserve_result_and_write_back_x86_64_ref_args(emitter, &ref_slots, &body_label); emit_x86_64_method_exception_boundary_pop(emitter, STATIC_METHOD_HELPER_FRAME_SIZE); emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the native static method result emitter.label(&escape_label); abi::emit_release_temporary_stack(emitter, arg_temp_bytes); - let escape_writeback_label = format!("{}_throw", static_method_body_label(module, slot)); + let escape_writeback_label = format!("{}_throw", body_label); emit_x86_64_write_back_ref_args(emitter, &ref_slots, 0, &escape_writeback_label); abi::emit_release_temporary_stack(emitter, ref_slots.len() * 32); emit_x86_64_method_exception_boundary_pop(emitter, STATIC_METHOD_HELPER_FRAME_SIZE); emitter.instruction(&format!("jmp {}", fail_label)); // return failure after preserving by-reference writes + emitter.label(&prep_fail_label); + emit_x86_64_method_prep_fail_cleanup(emitter, fail_label); } } +/// Restores an ARM64 method-helper frame before reporting an argument-prep fatal. +fn emit_aarch64_method_prep_fail_cleanup( + emitter: &mut Emitter, + frame_pointer_offset: usize, + fail_label: &str, +) { + emitter.instruction(&format!("sub sp, x29, #{}", frame_pointer_offset)); // restore the helper frame base after argument staging failed + emitter.instruction(&format!("b {}", fail_label)); // report the argument-prep failure through the shared fail path +} + +/// Restores an x86_64 method-helper frame before reporting an argument-prep fatal. +fn emit_x86_64_method_prep_fail_cleanup(emitter: &mut Emitter, fail_label: &str) { + emitter.instruction("mov rsp, rbp"); // restore the helper frame base after argument staging failed + emitter.instruction(&format!("jmp {}", fail_label)); // report the argument-prep failure through the shared fail path +} + /// Emits ARM64 arity validation for one method body. fn emit_aarch64_validate_method_arg_count( module: &Module, diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs index 5c3c76a1f9..aceefd9e5d 100644 --- a/tests/codegen/eval_callable_ref_errors.rs +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -145,3 +145,80 @@ try { "Exception:eval-instance:integer:25|Exception:eval-static:integer:9|Exception:eval-instance:integer:33|Exception:eval-static:integer:17" ); } + +/// Verifies AOT instance method argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_aot_instance_method_by_ref_arg_prep_fatal_cleans_up_stack() { + let out = compile_and_run_capture( + r#"value) . ":" . $box->value;'); ); } +/// Verifies AOT constructor argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_arg_prep_fatal_cleans_up_stack() { + let out = compile_and_run_capture( + r#" Date: Tue, 30 Jun 2026 19:40:13 +0200 Subject: [PATCH 0992/1208] Fix eval dynamic callable bridge --- .../src/runtime_hooks/externs.rs | 3 + .../elephc-magician/src/runtime_hooks/ops.rs | 3 + src/codegen/eval_callable_helpers.rs | 359 +++++++++++++++++- src/codegen/eval_constructor_helpers.rs | 8 +- src/codegen/eval_method_helpers.rs | 24 +- tests/codegen/eval_callables.rs | 63 +++ 6 files changed, 446 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index e61f52afce..388925d4c8 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -117,6 +117,7 @@ unsafe extern "C" { args: *mut RuntimeCell, scope_ptr: *const u8, scope_len: u64, + context: *const c_void, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_value_static_method_call( class_ptr: *const u8, @@ -126,6 +127,7 @@ unsafe extern "C" { args: *mut RuntimeCell, scope_ptr: *const u8, scope_len: u64, + context: *const c_void, ) -> *mut RuntimeCell; pub(super) fn __elephc_eval_reflection_attribute_new( name_ptr: *const u8, @@ -237,6 +239,7 @@ unsafe extern "C" { args: *mut RuntimeCell, scope_ptr: *const u8, scope_len: u64, + context: *const c_void, ) -> u64; pub(super) fn __elephc_eval_value_take_pending_throwable() -> *mut RuntimeCell; pub(super) fn __elephc_eval_class_exists(name_ptr: *const u8, name_len: u64) -> u64; diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 4a4c20e54c..9d342f7628 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -282,6 +282,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { arg_array.as_ptr(), scope_ptr, scope_len, + self.context.cast(), ) }; unsafe { @@ -308,6 +309,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { arg_array.as_ptr(), scope_ptr, scope_len, + self.context.cast(), ) }; unsafe { @@ -661,6 +663,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { arg_array.as_ptr(), scope_ptr, scope_len, + self.context.cast(), ) }; unsafe { diff --git a/src/codegen/eval_callable_helpers.rs b/src/codegen/eval_callable_helpers.rs index 4b30f7bf1f..28b6aebec2 100644 --- a/src/codegen/eval_callable_helpers.rs +++ b/src/codegen/eval_callable_helpers.rs @@ -39,6 +39,12 @@ const MIXED_SELECTOR_BYTES: usize = 64; const SAVED_OBJECT_RECEIVER_BYTES: usize = 16; const MIXED_TAG_STRING: i64 = 1; const MIXED_TAG_OBJECT: i64 = 6; +const EVAL_DYNAMIC_CALLABLE_INVOKER_LABEL: &str = "__elephc_eval_dynamic_callable_invoker"; +const EVAL_DYNAMIC_CALLABLE_ENTRY_LABEL: &str = "__elephc_eval_dynamic_callable_entry"; +const EVAL_DYNAMIC_CONTEXT_CAPTURE: usize = 0; +const EVAL_DYNAMIC_CALLBACK_CAPTURE: usize = 1; +const EVAL_DYNAMIC_CALLABLE_CAPTURE_BYTES: usize = 32; +const AARCH64_EVAL_CONTEXT_FROM_FP_OFFSET: i64 = 16; /// Callable descriptors available to eval constructor and method bridges. pub(super) struct EvalCallableDescriptorSupport { @@ -46,6 +52,7 @@ pub(super) struct EvalCallableDescriptorSupport { instance_array_cases: Vec, static_array_cases: Vec, object_cases: Vec, + dynamic_descriptor_label: Option, } impl EvalCallableDescriptorSupport { @@ -66,6 +73,11 @@ impl EvalCallableDescriptorSupport { fn object_cases_empty(&self) -> bool { self.object_cases.is_empty() } + + /// Returns true when eval callback fallback descriptors can be materialized. + fn has_dynamic_descriptor(&self) -> bool { + self.dynamic_descriptor_label.is_some() + } } /// Returns true when eval bridges may need to turn PHP callbacks into descriptors. @@ -133,6 +145,7 @@ pub(super) fn emit_eval_callable_descriptor_support( instance_array_cases: Vec::new(), static_array_cases: Vec::new(), object_cases: Vec::new(), + dynamic_descriptor_label: None, }; } let mut legacy_ctx = super::lower_inst::legacy_context_from_eir_module(module); @@ -146,15 +159,195 @@ pub(super) fn emit_eval_callable_descriptor_support( let instance_array_cases = eval_instance_method_callable_cases(module, &mut legacy_ctx, data); let static_array_cases = eval_static_method_callable_cases(module, &mut legacy_ctx, data); let object_cases = eval_invokable_object_callable_cases(module, &mut legacy_ctx, data); + let dynamic_descriptor_label = Some(eval_dynamic_callable_descriptor(data)); + emit_eval_dynamic_callable_invoker(module, emitter, data); emit_deferred_callable_support(emitter, data, &mut legacy_ctx); EvalCallableDescriptorSupport { string_cases, instance_array_cases, static_array_cases, object_cases, + dynamic_descriptor_label, } } +/// Emits the static descriptor template for eval-owned callback values. +fn eval_dynamic_callable_descriptor(data: &mut DataSection) -> String { + let captures = vec![ + ( + "__elephc_eval_callable_context".to_string(), + PhpType::Int, + false, + ), + ( + "__elephc_eval_callable_value".to_string(), + PhpType::Mixed, + false, + ), + ]; + callable_descriptor::static_descriptor_with_optional_invoker_meta( + data, + EVAL_DYNAMIC_CALLABLE_ENTRY_LABEL, + Some("eval callback"), + callable_descriptor::CALLABLE_DESC_KIND_CALLBACK_ADAPTER, + None, + &captures, + &[], + CallableDescriptorInvocation::new(CallableDescriptorShape::CallbackAdapter), + Some(EVAL_DYNAMIC_CALLABLE_INVOKER_LABEL), + ) +} + +/// Emits the uniform invoker for descriptors that capture an eval callback value. +fn emit_eval_dynamic_callable_invoker( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + emitter.blank(); + emitter.comment("--- eval bridge: dynamic callable descriptor invoker ---"); + emitter.label(EVAL_DYNAMIC_CALLABLE_ENTRY_LABEL); + emitter.instruction("ret"); // direct entry calls are never used for eval dynamic descriptors + emitter.label_global(EVAL_DYNAMIC_CALLABLE_INVOKER_LABEL); + match module.target.arch { + crate::codegen::platform::Arch::AArch64 => { + emit_aarch64_eval_dynamic_callable_invoker(module, emitter, data); + } + crate::codegen::platform::Arch::X86_64 => { + emit_x86_64_eval_dynamic_callable_invoker(module, emitter, data); + } + } +} + +/// Emits the ARM64 body for eval dynamic callable descriptor invocation. +fn emit_aarch64_eval_dynamic_callable_invoker( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + let throwable_label = "__elephc_eval_dynamic_callable_throwable"; + let fatal_label = "__elephc_eval_dynamic_callable_fatal"; + let done_label = "__elephc_eval_dynamic_callable_done"; + let symbol = module + .target + .extern_symbol("__elephc_eval_callable_call_array"); + emitter.instruction("sub sp, sp, #64"); // reserve descriptor, arg-array, eval result, and saved frame slots + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the caller frame around the Rust callback + emitter.instruction("add x29, sp, #48"); // establish a stable invoker frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the runtime callable descriptor pointer + emitter.instruction("str x1, [sp, #8]"); // save the boxed Mixed invoker argument array + emitter.instruction("str xzr, [sp, #16]"); // clear result kind and padding before the FFI call + emitter.instruction("str xzr, [sp, #24]"); // clear result value pointer before the FFI call + emitter.instruction("str xzr, [sp, #32]"); // clear result error pointer before the FFI call + emitter.instruction("ldr x9, [sp, #0]"); // reload descriptor before reading eval captures + emitter.instruction(&format!( + "ldr x0, [x9, #{}]", + dynamic_capture_offset(EVAL_DYNAMIC_CONTEXT_CAPTURE) + )); // pass the captured eval context as FFI argument 1 + emitter.instruction(&format!( + "ldr x1, [x9, #{}]", + dynamic_capture_offset(EVAL_DYNAMIC_CALLBACK_CAPTURE) + )); // pass the captured eval callback as FFI argument 2 + emitter.instruction("ldr x2, [sp, #8]"); // pass the boxed Mixed invoker argument array + emitter.instruction("add x3, sp, #16"); // pass writable eval result storage + abi::emit_call_label(emitter, &symbol); + emitter.instruction("cmp w0, #0"); // did magician complete callback dispatch successfully? + emitter.instruction(&format!("b.eq {}", done_label)); // return the boxed result on success + emitter.instruction("cmp w0, #3"); // status 3 means an eval Throwable escaped + emitter.instruction(&format!("b.eq {}", throwable_label)); // publish the Throwable through native unwinding + emitter.label(fatal_label); + emit_aarch64_eval_dynamic_callable_fatal(emitter, data); + emitter.label(throwable_label); + emitter.instruction("ldr x0, [sp, #32]"); // load the boxed Throwable returned by magician + emitter.instruction("bl __rt_mixed_unbox"); // expose the Throwable object payload + abi::emit_store_reg_to_symbol(emitter, "x1", "_exc_value", 0); + emitter.instruction("bl __rt_throw_current"); // enter the native Throwable unwinder + emitter.label(done_label); + emitter.instruction("ldr x0, [sp, #24]"); // return the boxed Mixed callback result + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the caller frame + emitter.instruction("add sp, sp, #64"); // release invoker scratch storage + emitter.instruction("ret"); // return to the descriptor invoker caller +} + +/// Emits the x86_64 body for eval dynamic callable descriptor invocation. +fn emit_x86_64_eval_dynamic_callable_invoker( + module: &Module, + emitter: &mut Emitter, + data: &mut DataSection, +) { + let throwable_label = "__elephc_eval_dynamic_callable_throwable_x"; + let fatal_label = "__elephc_eval_dynamic_callable_fatal_x"; + let done_label = "__elephc_eval_dynamic_callable_done_x"; + let symbol = module + .target + .extern_symbol("__elephc_eval_callable_call_array"); + emitter.instruction("push rbp"); // preserve the caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable invoker frame pointer + emitter.instruction("sub rsp, 64"); // reserve descriptor, arg-array, and eval result slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the runtime callable descriptor pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the boxed Mixed invoker argument array + emitter.instruction("mov QWORD PTR [rbp - 48], 0"); // clear result kind and padding before the FFI call + emitter.instruction("mov QWORD PTR [rbp - 40], 0"); // clear result value pointer before the FFI call + emitter.instruction("mov QWORD PTR [rbp - 32], 0"); // clear result error pointer before the FFI call + emitter.instruction("mov r9, QWORD PTR [rbp - 8]"); // reload descriptor before reading eval captures + emitter.instruction(&format!( + "mov rdi, QWORD PTR [r9 + {}]", + dynamic_capture_offset(EVAL_DYNAMIC_CONTEXT_CAPTURE) + )); // pass the captured eval context as FFI argument 1 + emitter.instruction(&format!( + "mov rsi, QWORD PTR [r9 + {}]", + dynamic_capture_offset(EVAL_DYNAMIC_CALLBACK_CAPTURE) + )); // pass the captured eval callback as FFI argument 2 + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // pass the boxed Mixed invoker argument array + emitter.instruction("lea rcx, [rbp - 48]"); // pass writable eval result storage + abi::emit_call_label(emitter, &symbol); + emitter.instruction("test eax, eax"); // did magician complete callback dispatch successfully? + emitter.instruction(&format!("je {}", done_label)); // return the boxed result on success + emitter.instruction("cmp eax, 3"); // status 3 means an eval Throwable escaped + emitter.instruction(&format!("je {}", throwable_label)); // publish the Throwable through native unwinding + emitter.label(fatal_label); + emit_x86_64_eval_dynamic_callable_fatal(emitter, data); + emitter.label(throwable_label); + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // load the boxed Throwable returned by magician + emitter.instruction("call __rt_mixed_unbox"); // expose the Throwable object payload + abi::emit_store_reg_to_symbol(emitter, "rdi", "_exc_value", 0); + emitter.instruction("call __rt_throw_current"); // enter the native Throwable unwinder + emitter.label(done_label); + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // return the boxed Mixed callback result + emitter.instruction("mov rsp, rbp"); // discard invoker scratch storage + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return to the descriptor invoker caller +} + +/// Returns the byte offset for one dynamic eval callable descriptor capture. +fn dynamic_capture_offset(index: usize) -> usize { + callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + index * 16 +} + +/// Emits an ARM64 fatal diagnostic for failed eval callback dispatch. +fn emit_aarch64_eval_dynamic_callable_fatal(emitter: &mut Emitter, data: &mut DataSection) { + let (message_label, message_len) = + data.add_string(b"Fatal error: eval callback dispatch failed\n"); + emitter.instruction("mov x0, #2"); // write the eval callback diagnostic to stderr + emitter.adrp("x1", &message_label); + emitter.add_lo12("x1", "x1", &message_label); + emitter.instruction(&format!("mov x2, #{}", message_len)); // pass the diagnostic byte length to write + emitter.syscall(4); + abi::emit_exit(emitter, 1); +} + +/// Emits an x86_64 fatal diagnostic for failed eval callback dispatch. +fn emit_x86_64_eval_dynamic_callable_fatal(emitter: &mut Emitter, data: &mut DataSection) { + let (message_label, message_len) = + data.add_string(b"Fatal error: eval callback dispatch failed\n"); + emitter.instruction("mov edi, 2"); // write the eval callback diagnostic to stderr + abi::emit_symbol_address(emitter, "rsi", &message_label); + emitter.instruction(&format!("mov edx, {}", message_len)); // pass the diagnostic byte length to write + emitter.instruction("mov eax, 1"); // Linux x86_64 syscall 1 = write + emitter.instruction("syscall"); // emit the fatal diagnostic before terminating + abi::emit_exit(emitter, 1); +} + /// Builds descriptor cases for user functions visible as eval string callables. fn eval_user_function_callable_cases( legacy_ctx: &mut crate::codegen::context::Context, @@ -507,6 +700,12 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( let string_label = format!("{}_callable_string", label_prefix); let array_label = format!("{}_callable_array", label_prefix); let object_label = format!("{}_callable_object", label_prefix); + let dynamic_label = format!("{}_callable_dynamic", label_prefix); + let miss_label = if support.has_dynamic_descriptor() { + dynamic_label.as_str() + } else { + fail_label + }; emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed eval argument for callable validation emitter.instruction("bl __rt_mixed_unbox"); // expose the eval callable tag and payload words emitter.instruction("cmp x0, #1"); // runtime tag 1 means a string callable name @@ -517,7 +716,7 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( emitter.instruction(&format!("b.eq {}", array_label)); // resolve static callable arrays with numeric keys emitter.instruction("cmp x0, #6"); // runtime tag 6 means an invokable object candidate emitter.instruction(&format!("b.eq {}", object_label)); // resolve invokable objects through descriptor metadata - abi::emit_jump(emitter, fail_label); + abi::emit_jump(emitter, miss_label); emitter.label(&string_label); abi::emit_push_reg_pair(emitter, "x1", "x2"); emit_eval_string_callable_descriptor_lookup( @@ -526,7 +725,7 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( data, support, label_prefix, - fail_label, + miss_label, "x0", ); abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); @@ -537,7 +736,7 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( data, support, label_prefix, - fail_label, + miss_label, "x0", ); abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); @@ -547,9 +746,20 @@ pub(super) fn emit_aarch64_cast_eval_callable_arg( emitter, support, label_prefix, - fail_label, + miss_label, "x0", ); + if support.has_dynamic_descriptor() { + emitter.label(&dynamic_label); + emit_aarch64_eval_dynamic_callable_descriptor( + module, + emitter, + support, + fail_label, + "x0", + ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + } emitter.label(&format!("{}_callable_cast_done", label_prefix)); } @@ -561,10 +771,17 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( support: &EvalCallableDescriptorSupport, label_prefix: &str, fail_label: &str, + context_frame_offset: usize, ) { let string_label = format!("{}_callable_string", label_prefix); let array_label = format!("{}_callable_array", label_prefix); let object_label = format!("{}_callable_object", label_prefix); + let dynamic_label = format!("{}_callable_dynamic", label_prefix); + let miss_label = if support.has_dynamic_descriptor() { + dynamic_label.as_str() + } else { + fail_label + }; emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed eval argument for callable validation emitter.instruction("call __rt_mixed_unbox"); // expose the eval callable tag and payload words emitter.instruction("cmp rax, 1"); // runtime tag 1 means a string callable name @@ -575,7 +792,7 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( emitter.instruction(&format!("je {}", array_label)); // resolve static callable arrays with numeric keys emitter.instruction("cmp rax, 6"); // runtime tag 6 means an invokable object candidate emitter.instruction(&format!("je {}", object_label)); // resolve invokable objects through descriptor metadata - abi::emit_jump(emitter, fail_label); + abi::emit_jump(emitter, miss_label); emitter.label(&string_label); abi::emit_push_reg_pair(emitter, "rdi", "rdx"); emit_eval_string_callable_descriptor_lookup( @@ -584,7 +801,7 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( data, support, label_prefix, - fail_label, + miss_label, "rax", ); abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); @@ -595,7 +812,7 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( data, support, label_prefix, - fail_label, + miss_label, "rax", ); abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); @@ -605,12 +822,138 @@ pub(super) fn emit_x86_64_cast_eval_callable_arg( emitter, support, label_prefix, - fail_label, + miss_label, "rax", ); + if support.has_dynamic_descriptor() { + emitter.label(&dynamic_label); + emit_x86_64_eval_dynamic_callable_descriptor( + module, + emitter, + support, + fail_label, + "rax", + context_frame_offset, + ); + abi::emit_jump(emitter, &format!("{}_callable_cast_done", label_prefix)); + } emitter.label(&format!("{}_callable_cast_done", label_prefix)); } +/// Materializes a runtime descriptor that dispatches a callable through magician on ARM64. +fn emit_aarch64_eval_dynamic_callable_descriptor( + module: &Module, + emitter: &mut Emitter, + support: &EvalCallableDescriptorSupport, + fail_label: &str, + result_reg: &str, +) { + let descriptor_label = support + .dynamic_descriptor_label + .as_deref() + .expect("dynamic eval callable descriptor must exist"); + let is_callable_symbol = module.target.extern_symbol("__elephc_eval_is_callable"); + emitter.instruction(&format!( + "ldr x0, [x29, #{}]", + AARCH64_EVAL_CONTEXT_FROM_FP_OFFSET + )); // load the active eval context for dynamic callable validation + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject dynamic eval callables when no context is active + emitter.instruction("ldr x1, [x29, #-16]"); // pass the original boxed eval callback value + abi::emit_call_label(emitter, &is_callable_symbol); + emitter.instruction("cmp w0, #0"); // check whether magician accepts the callback value + emitter.instruction(&format!("b.eq {}", fail_label)); // reject non-callable eval values + emitter.instruction("ldr x0, [x29, #-16]"); // reload the boxed callback value to retain it + emitter.instruction("bl __rt_incref"); // retain the callback for descriptor capture ownership + abi::emit_push_reg(emitter, "x0"); + abi::emit_load_int_immediate( + emitter, + "x0", + (callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + + EVAL_DYNAMIC_CALLABLE_CAPTURE_BYTES) as i64, + ); + emitter.instruction("bl __rt_heap_alloc"); // allocate runtime descriptor storage with eval captures + callable_descriptor::emit_copy_static_descriptor_to_runtime(emitter, "x0", descriptor_label); + emitter.instruction(&format!( + "ldr x10, [x29, #{}]", + AARCH64_EVAL_CONTEXT_FROM_FP_OFFSET + )); // reload the active eval context for descriptor capture 0 + abi::emit_store_to_address( + emitter, + "x10", + "x0", + dynamic_capture_offset(EVAL_DYNAMIC_CONTEXT_CAPTURE), + ); + abi::emit_load_temporary_stack_slot(emitter, "x10", 0); + abi::emit_store_to_address( + emitter, + "x10", + "x0", + dynamic_capture_offset(EVAL_DYNAMIC_CALLBACK_CAPTURE), + ); + abi::emit_release_temporary_stack(emitter, 16); + if result_reg != "x0" { + emitter.instruction(&format!("mov {}, x0", result_reg)); // return the dynamic eval callable descriptor + } +} + +/// Materializes a runtime descriptor that dispatches a callable through magician on x86_64. +fn emit_x86_64_eval_dynamic_callable_descriptor( + module: &Module, + emitter: &mut Emitter, + support: &EvalCallableDescriptorSupport, + fail_label: &str, + result_reg: &str, + context_frame_offset: usize, +) { + let descriptor_label = support + .dynamic_descriptor_label + .as_deref() + .expect("dynamic eval callable descriptor must exist"); + let is_callable_symbol = module.target.extern_symbol("__elephc_eval_is_callable"); + emitter.instruction(&format!( + "mov rdi, QWORD PTR [rbp - {}]", + context_frame_offset + )); // load the active eval context for dynamic callable validation + emitter.instruction("test rdi, rdi"); // check whether a context was passed by magician + emitter.instruction(&format!("jz {}", fail_label)); // reject dynamic eval callables when no context is active + emitter.instruction("mov rsi, QWORD PTR [rbp - 40]"); // pass the original boxed eval callback value + abi::emit_call_label(emitter, &is_callable_symbol); + emitter.instruction("test eax, eax"); // check whether magician accepts the callback value + emitter.instruction(&format!("jz {}", fail_label)); // reject non-callable eval values + emitter.instruction("mov rax, QWORD PTR [rbp - 40]"); // reload the boxed callback value to retain it + emitter.instruction("call __rt_incref"); // retain the callback for descriptor capture ownership + abi::emit_push_reg(emitter, "rax"); + abi::emit_load_int_immediate( + emitter, + "rax", + (callable_descriptor::CALLABLE_DESC_RUNTIME_CAPTURE_OFFSET + + EVAL_DYNAMIC_CALLABLE_CAPTURE_BYTES) as i64, + ); + emitter.instruction("call __rt_heap_alloc"); // allocate runtime descriptor storage with eval captures + callable_descriptor::emit_copy_static_descriptor_to_runtime(emitter, "rax", descriptor_label); + emitter.instruction(&format!( + "mov r10, QWORD PTR [rbp - {}]", + context_frame_offset + )); // reload the active eval context for descriptor capture 0 + abi::emit_store_to_address( + emitter, + "r10", + "rax", + dynamic_capture_offset(EVAL_DYNAMIC_CONTEXT_CAPTURE), + ); + abi::emit_load_temporary_stack_slot(emitter, "r10", 0); + abi::emit_store_to_address( + emitter, + "r10", + "rax", + dynamic_capture_offset(EVAL_DYNAMIC_CALLBACK_CAPTURE), + ); + abi::emit_release_temporary_stack(emitter, 16); + if result_reg != "rax" { + emitter.instruction(&format!("mov {}, rax", result_reg)); // return the dynamic eval callable descriptor + } +} + /// Looks up a descriptor by the string callable currently saved on the temp stack. fn emit_eval_string_callable_descriptor_lookup( module: &Module, diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 39e77614b0..2c38306254 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -58,10 +58,11 @@ const BUILTIN_THROWABLE_CONSTRUCTOR_CLASSES: &[&str] = &[ "JsonException", "FiberError", ]; -const CONSTRUCTOR_HELPER_BASE_FRAME_SIZE: usize = 64; +const CONSTRUCTOR_HELPER_BASE_FRAME_SIZE: usize = 80; const CONSTRUCTOR_HELPER_HANDLER_OFFSET: usize = CONSTRUCTOR_HELPER_BASE_FRAME_SIZE; const CONSTRUCTOR_HELPER_FRAME_SIZE: usize = CONSTRUCTOR_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; +const X86_64_CONSTRUCTOR_CONTEXT_FRAME_OFFSET: usize = 64; /// Constructor metadata needed by the eval constructor bridge. #[derive(Clone)] @@ -278,7 +279,7 @@ fn constructor_param_supported(ty: &PhpType) -> bool { ) } -/// Emits `__elephc_eval_value_construct_object(Mixed*, MixedArray*, scope, scope_len) -> bool`. +/// Emits `__elephc_eval_value_construct_object(Mixed*, MixedArray*, scope, scope_len, ctx) -> bool`. fn emit_constructor_helper( module: &Module, emitter: &mut Emitter, @@ -333,6 +334,7 @@ fn emit_constructor_aarch64( emitter.instruction("str x2, [sp, #0]"); // save the active eval class-scope pointer emitter.instruction("str x3, [sp, #8]"); // save the active eval class-scope length emitter.instruction("str x1, [sp, #24]"); // save the boxed eval argument array + emitter.instruction("str x4, [sp, #64]"); // save the active eval context for callable descriptors emitter.instruction(&format!("cbz x0, {}", success_label)); // a null object pointer means there is nothing to construct emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object @@ -386,6 +388,7 @@ fn emit_constructor_x86_64( emitter.instruction("mov QWORD PTR [rbp - 48], rdx"); // save the active eval class-scope pointer emitter.instruction("mov QWORD PTR [rbp - 56], rcx"); // save the active eval class-scope length emitter.instruction("mov QWORD PTR [rbp - 32], rsi"); // save the boxed eval argument array + emitter.instruction("mov QWORD PTR [rbp - 64], r8"); // save the active eval context for callable descriptors emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", success_label)); // a null object pointer means there is nothing to construct emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register @@ -1464,6 +1467,7 @@ fn emit_x86_64_cast_eval_arg( callable_support, label_prefix, fail_label, + X86_64_CONSTRUCTOR_CONTEXT_FRAME_OFFSET, ); } PhpType::TaggedScalar => { diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 5a85f9d9ee..76d1f1f44a 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -88,13 +88,15 @@ const BUILTIN_THROWABLE_METHOD_CLASSES: &[&str] = &[ ]; const BUILTIN_THROWABLE_GET_MESSAGE_LABEL: &str = "__elephc_eval_builtin_throwable_getmessage"; const BUILTIN_THROWABLE_GET_CODE_LABEL: &str = "__elephc_eval_builtin_throwable_getcode"; -const METHOD_HELPER_BASE_FRAME_SIZE: usize = 64; +const METHOD_HELPER_BASE_FRAME_SIZE: usize = 80; const METHOD_HELPER_HANDLER_OFFSET: usize = METHOD_HELPER_BASE_FRAME_SIZE; const METHOD_HELPER_FRAME_SIZE: usize = METHOD_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; -const STATIC_METHOD_HELPER_BASE_FRAME_SIZE: usize = 80; +const STATIC_METHOD_HELPER_BASE_FRAME_SIZE: usize = 96; const STATIC_METHOD_HELPER_HANDLER_OFFSET: usize = STATIC_METHOD_HELPER_BASE_FRAME_SIZE; const STATIC_METHOD_HELPER_FRAME_SIZE: usize = STATIC_METHOD_HELPER_BASE_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE; +const X86_64_METHOD_CONTEXT_FRAME_OFFSET: usize = 64; +const X86_64_STATIC_METHOD_CONTEXT_FRAME_OFFSET: usize = 72; /// Emits eval method-call helpers when any lowered function owns an eval context. pub(super) fn emit_eval_method_helpers( @@ -427,7 +429,7 @@ fn method_return_supported(ty: &PhpType) -> bool { ) } -/// Emits `__elephc_eval_value_method_call(Mixed*, name, len, MixedArray*, scope, scope_len) -> Mixed*`. +/// Emits `__elephc_eval_value_method_call(Mixed*, name, len, MixedArray*, scope, scope_len, ctx) -> Mixed*`. fn emit_method_call_helper( module: &Module, emitter: &mut Emitter, @@ -463,7 +465,7 @@ fn emit_method_call_helper( } } -/// Emits `__elephc_eval_value_static_method_call(class, method, MixedArray*, scope, scope_len) -> Mixed*`. +/// Emits `__elephc_eval_value_static_method_call(class, method, MixedArray*, scope, scope_len, ctx) -> Mixed*`. fn emit_static_method_call_helper( module: &Module, emitter: &mut Emitter, @@ -504,6 +506,7 @@ fn emit_static_method_call_aarch64( emitter.instruction("str x5, [sp, #32]"); // save the active eval class-scope pointer emitter.instruction("str x3, [sp, #40]"); // save the requested method-name length emitter.instruction("str x6, [sp, #48]"); // save the active eval class-scope length + emitter.instruction("str x7, [sp, #80]"); // save the active eval context for callable descriptors emit_aarch64_static_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("b {}", fail_label)); // no supported static method matched the request emit_aarch64_static_method_bodies( @@ -544,6 +547,8 @@ fn emit_static_method_call_x86_64( emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope pointer emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval class-scope length stack argument emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the active eval class-scope length + emitter.instruction("mov rax, QWORD PTR [rbp + 24]"); // load the active eval context stack argument + emitter.instruction("mov QWORD PTR [rbp - 72], rax"); // save the active eval context for callable descriptors emit_x86_64_static_method_dispatch(module, emitter, data, slots, fail_label); emitter.instruction(&format!("jmp {}", fail_label)); // no supported static method matched the request emit_x86_64_static_method_bodies( @@ -582,6 +587,7 @@ fn emit_method_call_aarch64( emitter.instruction("str x3, [sp, #24]"); // save the boxed eval argument array emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length + emitter.instruction("str x6, [sp, #64]"); // save the active eval context for callable descriptors emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot dispatch a method emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object @@ -632,6 +638,8 @@ fn emit_method_call_x86_64( emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed eval argument array emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope pointer emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope length + emitter.instruction("mov rax, QWORD PTR [rbp + 16]"); // load the active eval context stack argument + emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the active eval context for callable descriptors emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot dispatch a method emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register @@ -1666,6 +1674,7 @@ fn emit_x86_64_prepare_method_args( &body_label, fail_label, callable_support, + X86_64_METHOD_CONTEXT_FRAME_OFFSET, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); let receiver_ty = PhpType::Object(slot.class_name.clone()); @@ -1691,6 +1700,7 @@ fn emit_x86_64_prepare_method_args( &label_prefix, fail_label, callable_support, + X86_64_METHOD_CONTEXT_FRAME_OFFSET, ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } @@ -1718,6 +1728,7 @@ fn emit_x86_64_prepare_static_method_args( &body_label, fail_label, callable_support, + X86_64_STATIC_METHOD_CONTEXT_FRAME_OFFSET, ); let visible_abi_params = eval_abi_param_types_for_refs(&slot.params, &slot.ref_params); abi::emit_load_int_immediate(emitter, "rax", slot.class_id as i64); @@ -1742,6 +1753,7 @@ fn emit_x86_64_prepare_static_method_args( &label_prefix, fail_label, callable_support, + X86_64_STATIC_METHOD_CONTEXT_FRAME_OFFSET, ); abi::emit_push_result_value(emitter, ¶m_ty.codegen_repr()); } @@ -1825,6 +1837,7 @@ fn emit_x86_64_ref_arg_cells( label_prefix: &str, fail_label: &str, callable_support: &EvalCallableDescriptorSupport, + context_frame_offset: usize, ) -> Vec { let ref_slots = eval_ref_arg_slots(param_types, ref_params, false); for slot in &ref_slots { @@ -1844,6 +1857,7 @@ fn emit_x86_64_ref_arg_cells( &arg_label, fail_label, callable_support, + context_frame_offset, ); abi::emit_push_result_value(emitter, &slot.param_ty); } @@ -2087,6 +2101,7 @@ fn emit_x86_64_cast_eval_arg( label_prefix: &str, fail_label: &str, callable_support: &EvalCallableDescriptorSupport, + context_frame_offset: usize, ) { match param_ty.codegen_repr() { PhpType::Int => { @@ -2113,6 +2128,7 @@ fn emit_x86_64_cast_eval_arg( callable_support, label_prefix, fail_label, + context_frame_offset, ); } PhpType::TaggedScalar => { diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index ff5463add5..4d24e66fc1 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -906,6 +906,69 @@ return call_user_func_array($named, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); ); } +/// Verifies `Closure::fromCallable()` values can cross eval into AOT callable parameters. +#[test] +fn test_eval_closure_from_callable_values_pass_to_aot_callable_params() { + let out = compile_and_run( + r##"value = $callback("C"); + } + + public function apply(callable $callback) { + return $callback("M"); + } + + public static function applyStatic(callable $callback) { + return $callback("S"); + } +} + +echo eval('$target = new EvalClosureBridgeTarget(); +$cases = [ + Closure::fromCallable("eval_closure_bridge_suffix"), + Closure::fromCallable([EvalClosureBridgeTarget::class, "suffix"]), + Closure::fromCallable("EvalClosureBridgeTarget::suffix"), + Closure::fromCallable([$target, "instanceSuffix"]), + Closure::fromCallable($target), +]; +$out = []; +foreach ($cases as $callback) { + $box = new EvalClosureBridgeBox($callback); + $out[] = $box->value . ":" . $box->apply($callback) . ":" . + EvalClosureBridgeBox::applyStatic($callback); +} +return implode("|", $out);'); +"##, + ); + + assert_eq!( + out, + "C!:M!:S!|C?:M?:S?|C?:M?:S?|C~:M~:S~|C#:M#:S#" + ); +} + /// Verifies `Closure::call()` rebinds `fromCallable()` method closures to a same-class receiver. #[test] fn test_eval_closure_from_callable_call_rebinds_same_class_method_and_invokable_targets() { From bbf9b55c5a5fd9ec975d2de3aceee6eeac0cc90d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 20:22:45 +0200 Subject: [PATCH 0993/1208] Preserve eval dynamic callable ref slots --- crates/elephc-magician/src/context.rs | 4 ++ .../src/interpreter/dynamic_functions.rs | 41 ++++++++++++ .../src/interpreter/runtime_ops.rs | 2 + .../src/interpreter/statements.rs | 64 +++++++++++++++++++ tests/codegen/eval_callables.rs | 39 +++++++++++ 5 files changed, 150 insertions(+) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 826f7ca6a9..be451e44e6 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -273,6 +273,10 @@ pub enum EvalReferenceTarget { Cell { cell: RuntimeCellHandle, }, + InvokerSlot { + slot: usize, + source_tag: u64, + }, } /// Normalized PHP array key used for eval-side reference metadata. diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 9592106496..baebe07b86 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -303,6 +303,8 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( return Err(status); } }; + let (value, ref_target) = + eval_invoker_ref_arg_value_and_target(value, ref_target, values)?; EvaluatedCallArg { name: None, value, @@ -326,6 +328,8 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( return Err(status); } }; + let (value, ref_target) = + eval_invoker_ref_arg_value_and_target(value, ref_target, values)?; EvaluatedCallArg { name: Some(name), value, @@ -343,6 +347,43 @@ pub(in crate::interpreter) fn append_unpacked_call_arg_values( Ok(()) } +/// Converts a descriptor-invoker ref marker into an eval-visible value and writeback target. +fn eval_invoker_ref_arg_value_and_target( + value: RuntimeCellHandle, + ref_target: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_INVOKER_REF_CELL { + return Ok((value, ref_target)); + } + let slot = values.raw_value_word(value)? as usize; + let source_tag = values.raw_value_high_word(value)?; + let value = eval_invoker_ref_slot_value(slot, source_tag, values)?; + Ok(( + value, + ref_target.or(Some(EvalReferenceTarget::InvokerSlot { slot, source_tag })), + )) +} + +/// Reads the current PHP value from a native descriptor-invoker by-reference slot. +fn eval_invoker_ref_slot_value( + slot: usize, + source_tag: u64, + values: &mut impl RuntimeValueOps, +) -> Result { + match source_tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } + EVAL_TAG_MIXED => { + let value = unsafe { *(slot as *const RuntimeCellHandle) }; + values.retain(value) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Binds evaluated positional and named values to declared parameter order. pub(in crate::interpreter) fn bind_evaluated_function_args( params: &[String], diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index f47b640764..5413f80574 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -574,8 +574,10 @@ pub(super) const EVAL_TAG_BOOL: u64 = 3; pub(super) const EVAL_TAG_ARRAY: u64 = 4; pub(super) const EVAL_TAG_ASSOC: u64 = 5; pub(super) const EVAL_TAG_OBJECT: u64 = 6; +pub(super) const EVAL_TAG_MIXED: u64 = 7; pub(super) const EVAL_TAG_NULL: u64 = 8; pub(super) const EVAL_TAG_RESOURCE: u64 = 9; +pub(super) const EVAL_TAG_INVOKER_REF_CELL: u64 = 11; pub(super) const EVAL_REFLECTION_OWNER_CLASS: u64 = 0; pub(super) const EVAL_REFLECTION_OWNER_METHOD: u64 = 1; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 915a881775..6fabd839e3 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -5613,6 +5613,9 @@ pub(in crate::interpreter) fn eval_reference_target_value( result } EvalReferenceTarget::Cell { cell } => Ok(*cell), + EvalReferenceTarget::InvokerSlot { slot, source_tag } => { + eval_invoker_slot_ref_target_value(*slot, *source_tag, values) + } } } @@ -10036,6 +10039,16 @@ fn same_method_ref_target(left: &EvalReferenceTarget, right: &EvalReferenceTarge EvalReferenceTarget::Cell { cell: left_cell }, EvalReferenceTarget::Cell { cell: right_cell }, ) => left_cell == right_cell, + ( + EvalReferenceTarget::InvokerSlot { + slot: left_slot, + source_tag: left_source_tag, + }, + EvalReferenceTarget::InvokerSlot { + slot: right_slot, + source_tag: right_source_tag, + }, + ) => left_slot == right_slot && left_source_tag == right_source_tag, ( EvalReferenceTarget::StaticProperty { class_name: left_class_name, @@ -10179,6 +10192,57 @@ pub(in crate::interpreter) fn write_back_method_ref_target( values, ), EvalReferenceTarget::Cell { .. } => Ok(()), + EvalReferenceTarget::InvokerSlot { slot, source_tag } => { + write_back_invoker_slot_ref_target(*slot, *source_tag, value, values) + } + } +} + +/// Reads a value from a native descriptor-invoker by-reference slot. +fn eval_invoker_slot_ref_target_value( + slot: usize, + source_tag: u64, + values: &mut impl RuntimeValueOps, +) -> Result { + match source_tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } + EVAL_TAG_MIXED => { + let value = unsafe { *(slot as *const RuntimeCellHandle) }; + values.retain(value) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Writes a value back into a native descriptor-invoker by-reference slot. +fn write_back_invoker_slot_ref_target( + slot: usize, + source_tag: u64, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match source_tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = values.raw_value_word(value)?; + unsafe { + *(slot as *mut u64) = word; + } + Ok(()) + } + EVAL_TAG_MIXED => { + let retained = values.retain(value)?; + let replaced = unsafe { + let slot = slot as *mut RuntimeCellHandle; + let replaced = *slot; + *slot = retained; + replaced + }; + values.release(replaced) + } + _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 4d24e66fc1..6283eb772c 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -969,6 +969,45 @@ return implode("|", $out);'); ); } +/// Verifies eval dynamic callable descriptors preserve AOT caller by-ref variables. +#[test] +fn test_eval_dynamic_callable_params_write_back_aot_by_ref_args() { + let out = compile_and_run( + r#"value = $callback($value, 3) . ":" . gettype($value) . ":" . $value; + } + + public function apply(callable $callback): string { + $value = 4; + return $callback($value, 5) . ":" . gettype($value) . ":" . $value; + } + + public static function applyStatic(callable $callback): string { + $value = 6; + return $callback($value, 7) . ":" . gettype($value) . ":" . $value; + } +} + +echo eval('$callback = Closure::fromCallable("eval_dynamic_callable_ref_add"); +$box = new EvalDynamicCallableRefBridgeBox($callback); +return $box->value . "|" . $box->apply($callback) . "|" . + EvalDynamicCallableRefBridgeBox::applyStatic($callback);'); +"#, + ); + + assert_eq!(out, "5:integer:5|9:integer:9|13:integer:13"); +} + /// Verifies `Closure::call()` rebinds `fromCallable()` method closures to a same-class receiver. #[test] fn test_eval_closure_from_callable_call_rebinds_same_class_method_and_invokable_targets() { From 8883e1483d1242deb45eb93f2a763277b88a1d8d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 21:14:32 +0200 Subject: [PATCH 0994/1208] Fix eval callable ref high word on x86_64 --- src/codegen_support/runtime/eval_bridge.rs | 2 +- tests/codegen/eval_callables.rs | 42 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/codegen_support/runtime/eval_bridge.rs b/src/codegen_support/runtime/eval_bridge.rs index e6c23c13eb..ac3b9c2bdd 100644 --- a/src/codegen_support/runtime/eval_bridge.rs +++ b/src/codegen_support/runtime/eval_bridge.rs @@ -2376,7 +2376,7 @@ fn emit_x86_64_wrappers(emitter: &mut Emitter) { emitter.instruction("mov rbp, rsp"); // establish a stable wrapper frame pointer emitter.instruction("mov rax, rdi"); // move the boxed cell into mixed_unbox input emitter.instruction("call __rt_mixed_unbox"); // expose the boxed payload words - emitter.instruction("mov rax, rsi"); // return the high payload word to Rust + emitter.instruction("mov rax, rdx"); // return the high payload word to Rust emitter.instruction("pop rbp"); // restore the Rust caller frame pointer emitter.instruction("ret"); // return the raw high payload word diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 6283eb772c..b78a3c2947 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -1008,6 +1008,48 @@ return $box->value . "|" . $box->apply($callback) . "|" . assert_eq!(out, "5:integer:5|9:integer:9|13:integer:13"); } +/// Verifies eval dynamic callable descriptors preserve AOT mixed by-ref slots. +#[test] +fn test_eval_dynamic_callable_params_write_back_aot_mixed_by_ref_args() { + let out = compile_and_run( + r#"value = $callback($value, "ctor") . ":" . gettype($value) . ":" . $value; + } + + public function apply(callable $callback, mixed $seed): string { + $value = $seed; + return $callback($value, "method") . ":" . gettype($value) . ":" . $value; + } + + public static function applyStatic(callable $callback, mixed $seed): string { + $value = $seed; + return $callback($value, "static") . ":" . gettype($value) . ":" . $value; + } +} + +echo eval('$callback = Closure::fromCallable("eval_dynamic_callable_mixed_replace"); +$box = new EvalDynamicCallableMixedRefBridgeBox($callback, 2); +return $box->value . "|" . $box->apply($callback, 4) . "|" . + EvalDynamicCallableMixedRefBridgeBox::applyStatic($callback, 6);'); +"#, + ); + + assert_eq!( + out, + "string:ctor:string:ctor|string:method:string:method|string:static:string:static" + ); +} + /// Verifies `Closure::call()` rebinds `fromCallable()` method closures to a same-class receiver. #[test] fn test_eval_closure_from_callable_call_rebinds_same_class_method_and_invokable_targets() { From f9fad15e6a0d0b43f3ed11619d413045574c8901 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 21:24:55 +0200 Subject: [PATCH 0995/1208] Cover eval ref-like builtin Closure aliases --- tests/codegen/eval_builtin_parity.rs | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 5bd540dc07..3c6f3feec1 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -251,3 +251,40 @@ foreach ($assoc as $key => $entry) { assert_eq!(out, "S1,2,3|Tinteger:42|1:ab:a:b|Ka1b2"); } + +/// Verifies additional eval ref-like builtin callables write back through Closure dispatch. +#[test] +fn test_eval_ref_like_builtin_closures_write_back_extended_aliases() { + let out = compile_and_run( + r#" &$letters, "offset" => 1, "length" => 2, "replacement" => ["x", "y"]] +); +echo implode(",", $removed) . ":" . implode(",", $letters) . "|"; + +$walk = Closure::fromCallable("array_walk"); +$walked = [1, 2]; +$callback = function (&$value, $key) { $value = ($value * 10) + $key; }; +echo $walk($walked, $callback) ? "W:" : "w:"; +echo implode(",", $walked) . "|"; + +$pregAll = preg_match_all(...); +$matches = []; +echo $pregAll("/a(.)/", "ab ac", $matches); +echo ":" . implode(",", $matches[0]) . ":" . implode(",", $matches[1]);'); +"#, + ); + + assert_eq!(out, "3:1,2,3|2:a,b|b,c:a,x,y,d|W:10,21|2:ab,ac:b,c"); +} From bf4729316e46350f4953107f43a74009ce9fb38a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 21:58:20 +0200 Subject: [PATCH 0996/1208] Support eval Closure invoke callables --- .../builtins/registry/callable_validation.rs | 5 ++ .../src/interpreter/builtins/symbols.rs | 5 ++ .../src/interpreter/statements.rs | 52 +++++++++++++++++++ tests/codegen/eval_closures.rs | 36 ++++++++++++- 4 files changed, 97 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index b9678a9110..3a4bca6649 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -140,6 +140,11 @@ fn eval_validate_call_user_func_object_method( let Ok(identity) = values.object_identity(object) else { return Ok(()); }; + if context.closure_object_target(identity).is_some() + && method_name.eq_ignore_ascii_case("__invoke") + { + return Ok(()); + } let Some(class) = context.dynamic_object_class(identity) else { return eval_validate_call_user_func_native_object_method( object, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 253f64a538..dddfbc657e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -1354,6 +1354,11 @@ fn eval_object_method_callable_probe( let Ok(identity) = values.object_identity(object) else { return Ok(false); }; + if context.closure_object_target(identity).is_some() + && method_name.eq_ignore_ascii_case("__invoke") + { + return Ok(true); + } let Some(class) = context.dynamic_object_class(identity) else { return eval_aot_object_method_callable_probe(object, method_name, context, values); }; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 6fabd839e3..37992904e6 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -9211,6 +9211,10 @@ fn eval_closure_object_method_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { + if method_name.eq_ignore_ascii_case("__invoke") { + return eval_closure_object_invoke_result(target, evaluated_args, context, values) + .map(Some); + } if method_name.eq_ignore_ascii_case("bindTo") { let bound_this = eval_closure_bind_to_args(evaluated_args, values)?; return eval_closure_bind_target(target, bound_this, context, values).map(Some); @@ -9299,6 +9303,54 @@ fn eval_closure_object_method_result( } } +/// Invokes the callable target retained behind a PHP-visible eval `Closure` object. +fn eval_closure_object_invoke_result( + target: EvalClosureObjectTarget, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callable = match target { + EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named { + display_name: name.clone(), + name, + }, + EvalClosureObjectTarget::BoundNamed { name, bound_this } => { + EvaluatedCallable::BoundClosure { name, bound_this } + } + EvalClosureObjectTarget::InvokableObject { object } => { + EvaluatedCallable::InvokableObject { object } + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + }, + EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + }, + }; + eval_evaluated_callable_with_call_array_args(&callable, evaluated_args, context, values) +} + /// Splits `Closure::call()` arguments into the bound object and forwarded closure args. fn eval_closure_call_split_args( evaluated_args: Vec, diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index 386059a8ed..790225a037 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -8,7 +8,7 @@ //! - Fixtures compile PHP to native code, enter the eval bridge, and execute //! closure callable paths through elephc-magician. -use crate::support::compile_and_run; +use crate::support::{compile_and_run, compile_and_run_capture}; /// Verifies eval closure literals dispatch through direct calls and call_user_func_array. #[test] @@ -158,3 +158,37 @@ echo is_null($static->bindTo($box)) ? "N" : "n";'); assert_eq!(out, "O:15:integer:15|19:integer:19|23:integer:23|N"); } + +/// Verifies eval Closure `__invoke` works as an array callable and preserves by-ref args. +#[test] +fn test_eval_closure_invoke_array_callable_preserves_by_ref_args() { + let out = compile_and_run_capture( + r#"__invoke($fourth, 9) . ":" . gettype($fourth) . ":" . $fourth;'); +"#, + ); + + assert!(out.success, "stdout={} stderr={}", out.stdout, out.stderr); + assert_eq!( + out.stdout, + "C:5:integer:5|9:integer:9|13:integer:13|17:integer:17" + ); +} From 645d82fa5ea75f7115bed9808fdf52aae632fbac Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 22:09:29 +0200 Subject: [PATCH 0997/1208] Resolve eval special string callables --- .../interpreter/builtins/registry/callable.rs | 15 ++++++- tests/codegen/eval_callables.rs | 44 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index c55f4a507e..7fc07489ad 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -205,7 +205,7 @@ pub(in crate::interpreter) fn eval_callable_with_optional_scope( if values.is_array_like(callback)? { return eval_array_callable(callback, context, lexical_scope, values); } - eval_string_callable(callback, values) + eval_string_callable(callback, context, lexical_scope, values) } /// Normalizes one invokable eval object for dynamic callable dispatch. @@ -466,8 +466,12 @@ fn eval_special_class_array_static_method_exists( } /// Normalizes one string callback name for eval dynamic callable dispatch. +/// Uses method lexical scope only for PHP APIs that resolve deprecated `self::`, +/// `static::`, and `parent::` string callbacks through the current method. pub(in crate::interpreter) fn eval_string_callable( callback: RuntimeCellHandle, + context: &ElephcEvalContext, + lexical_scope: Option<&ElephcEvalScope>, values: &mut impl RuntimeValueOps, ) -> Result { let callback = values.string_bytes(callback)?; @@ -476,6 +480,15 @@ pub(in crate::interpreter) fn eval_string_callable( if class_name.is_empty() || method.is_empty() { return Err(EvalStatus::RuntimeFatal); } + if let Some(callable) = eval_special_class_array_callable( + class_name, + method, + lexical_scope, + context, + values, + )? { + return Ok(callable); + } return Ok(EvaluatedCallable::StaticMethod { class_name: class_name.trim_start_matches('\\').to_string(), method: method.to_string(), diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index b78a3c2947..82b6e58241 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -516,6 +516,50 @@ echo EvalFirstClassStaticSyntaxThis::makeStatic();'); ); } +/// Verifies eval string callbacks resolve special class names through method scope. +#[test] +fn test_eval_string_special_class_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function run() { + $self = "self::add"; + $first = "2"; + echo is_callable($self) ? "C:" : "c:"; + echo call_user_func_array($self, [&$first, 3]) . ":" . gettype($first) . ":" . $first . "|"; + + $static = "static::add"; + $second = "4"; + echo call_user_func_array($static, [&$second, 5]) . ":" . gettype($second) . ":" . $second . "|"; + + $parent = "parent::bump"; + $third = "6"; + echo call_user_func_array($parent, [&$third, 7]) . ":" . gettype($third) . ":" . $third; + } +} + +$box = new EvalStringSpecialCallableChild(); +$box->run();'); +"#, + ); + + assert_eq!(out, "C:15:integer:15|19:integer:19|13:integer:13"); +} + /// Verifies eval `is_callable()` supports syntax-only probes and callable-name writeback. #[test] fn test_eval_is_callable_supports_syntax_only_and_callable_name_writeback() { From 284c9a8dfae356c6d68ab6f5a1ab31e7a20bf068 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 22:26:14 +0200 Subject: [PATCH 0998/1208] Honor eval Closure bind scope --- crates/elephc-magician/src/context.rs | 1 + .../interpreter/builtins/registry/callable.rs | 42 ++++++--- .../src/interpreter/control.rs | 1 + .../src/interpreter/dynamic_functions.rs | 34 +++++-- .../src/interpreter/statements.rs | 92 +++++++++++++++---- tests/codegen/eval_closures.rs | 39 ++++++++ 6 files changed, 173 insertions(+), 36 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index be451e44e6..de1315c639 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -313,6 +313,7 @@ pub enum EvalClosureObjectTarget { BoundNamed { name: String, bound_this: RuntimeCellHandle, + bound_scope: Option, }, InvokableObject { object: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 7fc07489ad..9ab58c2549 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -256,12 +256,15 @@ fn eval_closure_object_target_callable(target: &EvalClosureObjectTarget) -> Eval name: name.clone(), display_name: name.clone(), }, - EvalClosureObjectTarget::BoundNamed { name, bound_this } => { - EvaluatedCallable::BoundClosure { - name: name.clone(), - bound_this: *bound_this, - } - } + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + } => EvaluatedCallable::BoundClosure { + name: name.clone(), + bound_this: *bound_this, + bound_scope: bound_scope.clone(), + }, EvalClosureObjectTarget::InvokableObject { object } => EvaluatedCallable::InvokableObject { object: *object, }, @@ -523,14 +526,19 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( } eval_callable_with_values(name, evaluated_args, context, values) } - EvaluatedCallable::BoundClosure { name, bound_this } => { + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { let closure = context .closure(name) .cloned() .ok_or(EvalStatus::UnsupportedConstruct)?; - eval_closure_with_evaluated_args_and_bound_this( + eval_closure_with_evaluated_args_and_bound_this_scope( &closure, *bound_this, + bound_scope.clone(), positional_args(evaluated_args), context, values, @@ -616,14 +624,19 @@ fn eval_evaluated_callable_with_call_user_func_values( EvaluatedCallable::Named { name, .. } => { eval_named_callable_with_call_user_func_values(name, evaluated_args, context, values) } - EvaluatedCallable::BoundClosure { name, bound_this } => { + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { let closure = context .closure(name) .cloned() .ok_or(EvalStatus::UnsupportedConstruct)?; - eval_closure_with_evaluated_args_and_bound_this( + eval_closure_with_evaluated_args_and_bound_this_scope( &closure, *bound_this, + bound_scope.clone(), positional_args(evaluated_args), context, values, @@ -1095,14 +1108,19 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( EvaluatedCallable::Named { name, .. } => { eval_callable_with_call_array_args(name, evaluated_args, context, values) } - EvaluatedCallable::BoundClosure { name, bound_this } => { + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { let closure = context .closure(name) .cloned() .ok_or(EvalStatus::UnsupportedConstruct)?; - eval_closure_with_evaluated_args_and_bound_this( + eval_closure_with_evaluated_args_and_bound_this_scope( &closure, *bound_this, + bound_scope.clone(), evaluated_args, context, values, diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index fdc9769a26..0b749e3f53 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -92,6 +92,7 @@ pub(super) enum EvaluatedCallable { BoundClosure { name: String, bound_this: RuntimeCellHandle, + bound_scope: Option, }, InvokableObject { object: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index baebe07b86..2c4d33e53a 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1714,15 +1714,35 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_closure_with_evaluated_args_and_bound_this_scope( + closure, + bound_this, + None, + evaluated_args, + context, + values, + ) +} + +/// Evaluates one runtime eval closure with `$this` and an optional binding scope. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { if closure.is_static() { values.warning("Cannot bind an instance to a static closure")?; return values.null(); } - let class_name = eval_closure_bound_object_class_name(bound_this, context, values)?; + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); eval_closure_with_optional_bound_this( closure, - Some((bound_this, class_name)), + Some((bound_this, class_scope, called_class)), evaluated_args, context, values, @@ -1732,7 +1752,7 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this( /// Evaluates one runtime eval closure with optional `$this` binding metadata. fn eval_closure_with_optional_bound_this( closure: &EvalClosure, - bound_this: Option<(RuntimeCellHandle, String)>, + bound_this: Option<(RuntimeCellHandle, String, String)>, evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -1741,9 +1761,9 @@ fn eval_closure_with_optional_bound_this( let static_names = static_var_names(function.body()); let bound_class_pushed = bound_this.is_some(); context.push_function(function.name()); - if let Some((_, class_name)) = &bound_this { - context.push_class_scope(class_name.to_string()); - context.push_called_class_scope(class_name.to_string()); + if let Some((_, class_scope, called_class)) = &bound_this { + context.push_class_scope(class_scope.to_string()); + context.push_called_class_scope(called_class.to_string()); } let evaluated_args = match bind_evaluated_method_args( function.params(), @@ -1767,7 +1787,7 @@ fn eval_closure_with_optional_bound_this( }; let mut function_scope = ElephcEvalScope::new(); bind_closure_captures(&mut function_scope, closure.captures()); - if let Some((object, _)) = bound_this { + if let Some((object, _, _)) = bound_this { function_scope.set("this", object, ScopeCellOwnership::Borrowed); } bind_method_scope_args( diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 37992904e6..3b90fded85 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7496,9 +7496,15 @@ fn eval_closure_object_target_from_callable( ) -> EvalClosureObjectTarget { match callable { EvaluatedCallable::Named { name, .. } => EvalClosureObjectTarget::Named(name), - EvaluatedCallable::BoundClosure { name, bound_this } => { - EvalClosureObjectTarget::BoundNamed { name, bound_this } - } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + }, EvaluatedCallable::InvokableObject { object } => { EvalClosureObjectTarget::InvokableObject { object } } @@ -7549,8 +7555,9 @@ fn eval_closure_bind_static( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let (target, bound_this) = eval_closure_bind_static_args(evaluated_args, context, values)?; - eval_closure_bind_target(target, bound_this, context, values) + let (target, bound_this, bound_scope) = + eval_closure_bind_static_args(evaluated_args, context, values)?; + eval_closure_bind_target(target, bound_this, bound_scope, context, values) } /// Binds static `Closure::bind()` arguments to their PHP parameter slots. @@ -7558,7 +7565,7 @@ fn eval_closure_bind_static_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result<(EvalClosureObjectTarget, Option), EvalStatus> { +) -> Result<(EvalClosureObjectTarget, Option, Option), EvalStatus> { let bound = eval_closure_bind_args( &["closure", "newThis", "newScope"], 2, @@ -7568,17 +7575,21 @@ fn eval_closure_bind_static_args( let new_this = required_closure_bind_arg(&bound, 1)?; let target = eval_closure_target_arg(closure.value, context, values)?; let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; - Ok((target, bound_this)) + let bound_scope = eval_closure_bind_scope_arg(bound.get(2), bound_this, context, values)?; + Ok((target, bound_this, bound_scope)) } /// Binds `Closure::bindTo()` arguments to their PHP parameter slots. fn eval_closure_bind_to_args( evaluated_args: Vec, + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result<(Option, Option), EvalStatus> { let bound = eval_closure_bind_args(&["newThis", "newScope"], 1, evaluated_args)?; let new_this = required_closure_bind_arg(&bound, 0)?; - eval_closure_bind_receiver_arg(new_this.value, values) + let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; + let bound_scope = eval_closure_bind_scope_arg(bound.get(1), bound_this, context, values)?; + Ok((bound_this, bound_scope)) } /// Binds positional and named Closure binding arguments while accepting optional scope. @@ -7662,10 +7673,42 @@ fn eval_closure_bind_receiver_arg( Ok(Some(new_this)) } +/// Converts the optional `newScope` binding argument into a PHP class scope name. +fn eval_closure_bind_scope_arg( + new_scope: Option<&Option>, + bound_this: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(new_scope) = new_scope.and_then(Option::as_ref) else { + return Ok(None); + }; + if values.is_null(new_scope.value)? { + return Ok(None); + } + if values.type_tag(new_scope.value)? == EVAL_TAG_OBJECT { + return eval_closure_bound_object_class_name(new_scope.value, context, values).map(Some); + } + let bytes = values.string_bytes(new_scope.value)?; + let scope = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + if scope.eq_ignore_ascii_case("static") { + let Some(bound_this) = bound_this else { + return Ok(None); + }; + return eval_closure_bound_object_class_name(bound_this, context, values).map(Some); + } + Ok(Some( + context + .resolve_class_name(&scope) + .unwrap_or_else(|| scope.trim_start_matches('\\').to_string()), + )) +} + /// Creates a new Closure object with persistent binding metadata when supported. fn eval_closure_bind_target( target: EvalClosureObjectTarget, bound_this: Option, + bound_scope: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { @@ -7687,7 +7730,11 @@ fn eval_closure_bind_target( ); } eval_closure_object_from_target( - EvalClosureObjectTarget::BoundNamed { name, bound_this }, + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + }, context, values, ) @@ -9216,8 +9263,10 @@ fn eval_closure_object_method_result( .map(Some); } if method_name.eq_ignore_ascii_case("bindTo") { - let bound_this = eval_closure_bind_to_args(evaluated_args, values)?; - return eval_closure_bind_target(target, bound_this, context, values).map(Some); + let (bound_this, bound_scope) = + eval_closure_bind_to_args(evaluated_args, context, values)?; + return eval_closure_bind_target(target, bound_this, bound_scope, context, values) + .map(Some); } if !method_name.eq_ignore_ascii_case("call") { return Ok(None); @@ -9241,11 +9290,14 @@ fn eval_closure_object_method_result( ) .map(Some) } - EvalClosureObjectTarget::BoundNamed { name, .. } => { + EvalClosureObjectTarget::BoundNamed { + name, bound_scope, .. + } => { if let Some(closure) = context.closure(&name).cloned() { - return eval_closure_with_evaluated_args_and_bound_this( + return eval_closure_with_evaluated_args_and_bound_this_scope( &closure, bound_this, + bound_scope, call_args, context, values, @@ -9315,9 +9367,15 @@ fn eval_closure_object_invoke_result( display_name: name.clone(), name, }, - EvalClosureObjectTarget::BoundNamed { name, bound_this } => { - EvaluatedCallable::BoundClosure { name, bound_this } - } + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + } => EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + }, EvalClosureObjectTarget::InvokableObject { object } => { EvaluatedCallable::InvokableObject { object } } diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index 790225a037..9a3ba3c916 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -159,6 +159,45 @@ echo is_null($static->bindTo($box)) ? "N" : "n";'); assert_eq!(out, "O:15:integer:15|19:integer:19|23:integer:23|N"); } +/// Verifies eval `Closure::bind()` and `bindTo()` honor explicit private-access scope. +#[test] +fn test_eval_closure_bind_honors_explicit_scope_and_by_ref_args() { + let out = compile_and_run( + r#"secret + $delta; + return $value . ":" . get_called_class(); + }; + } +} + +$fn = (new EvalClosureScopeFactory())->make(); +$child = new EvalClosureScopeChild(); + +$bound = $fn->bindTo($child, "EvalClosureScopeBase"); +$first = "1"; +echo $bound($first, 1) . ":" . gettype($first) . ":" . $first . "|"; + +$staticBound = Closure::bind($fn, $child, "EvalClosureScopeBase"); +$second = "2"; +echo call_user_func_array($staticBound, [&$second, 2]) . ":" . gettype($second) . ":" . $second;'); +"#, + ); + + assert_eq!( + out, + "42:EvalClosureScopeChild:integer:42|44:EvalClosureScopeChild:integer:44" + ); +} + /// Verifies eval Closure `__invoke` works as an array callable and preserves by-ref args. #[test] fn test_eval_closure_invoke_array_callable_preserves_by_ref_args() { From 941cfafa18c0fddc4414237e07d232a0dcde0d9f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 22:41:40 +0200 Subject: [PATCH 0999/1208] Cover eval first-class special refs --- tests/codegen/eval_callables.rs | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 82b6e58241..e9a64e1922 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -560,6 +560,49 @@ $box->run();'); assert_eq!(out, "C:15:integer:15|19:integer:19|13:integer:13"); } +/// Verifies eval first-class callbacks resolve special class names and preserve by-ref writeback. +#[test] +fn test_eval_first_class_special_class_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function run() { + $self = self::add(...); + $first = "2"; + echo $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + + $static = static::add(...); + $second = "4"; + echo call_user_func_array($static, [&$second, 5]) . ":" . gettype($second) . ":" . $second . "|"; + + $parent = parent::bump(...); + $third = "6"; + echo $parent($third, 7) . ":" . gettype($third) . ":" . $third; + } +} + +$box = new EvalFirstClassSpecialCallableChild(); +$box->run();'); +"#, + ); + + assert_eq!(out, "15:integer:15|19:integer:19|13:integer:13"); +} + /// Verifies eval `is_callable()` supports syntax-only probes and callable-name writeback. #[test] fn test_eval_is_callable_supports_syntax_only_and_callable_name_writeback() { From 59b2d9687fc8cc841bd93e0e85c157486e88b1da Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 30 Jun 2026 22:54:18 +0200 Subject: [PATCH 1000/1208] Cover eval callable fatal cleanup forms --- tests/codegen/eval_callable_ref_errors.rs | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs index aceefd9e5d..7236adba88 100644 --- a/tests/codegen/eval_callable_ref_errors.rs +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -222,3 +222,81 @@ echo "bad";'); out.stderr ); } + +/// Verifies AOT first-class method argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_aot_first_class_method_by_ref_arg_prep_fatal_cleans_up_stack() { + let out = compile_and_run_capture( + r#"bump(...); +$value = "2"; +$callback($value, 123); +echo "bad";'); +"#, + ); + + assert!( + !out.success, + "expected eval runtime fatal, stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, ""); + assert!( + out.stderr.contains("Fatal error: eval() runtime failed"), + "stderr did not contain eval runtime fatal diagnostic: {}", + out.stderr + ); + assert!( + !out.stderr.contains("panicked at") && !out.stderr.contains("thread '"), + "stderr leaked a Rust panic: {}", + out.stderr + ); +} + +/// Verifies AOT `Closure::fromCallable()` method argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_aot_closure_from_callable_method_by_ref_arg_prep_fatal_cleans_up_stack() { + let out = compile_and_run_capture( + r#" Date: Tue, 30 Jun 2026 23:07:20 +0200 Subject: [PATCH 1001/1208] Cover eval builtin callable fatal cleanup --- tests/codegen/eval_callable_ref_errors.rs | 60 +++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs index 7236adba88..e16a4cc18c 100644 --- a/tests/codegen/eval_callable_ref_errors.rs +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -300,3 +300,63 @@ echo "bad";'); out.stderr ); } + +/// Verifies first-class ref-like builtin fatals restore the eval bridge frame. +#[test] +fn test_eval_first_class_ref_like_builtin_fatal_cleans_up_stack() { + let out = compile_and_run_capture( + r#" Date: Tue, 30 Jun 2026 23:39:12 +0200 Subject: [PATCH 1002/1208] Cover eval special callable refs --- src/ir/validator.rs | 144 +++++++++++++++++++++++--------- tests/codegen/eval_callables.rs | 51 +++++++++++ 2 files changed, 155 insertions(+), 40 deletions(-) diff --git a/src/ir/validator.rs b/src/ir/validator.rs index cb0fae88f0..b925f66916 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -893,42 +893,125 @@ fn definition_dominates_use( /// resolve to `{self}` (no reachable predecessor), so genuine uses inside dead /// code remain flagged until they are neutralized. fn compute_dominators(function: &Function) -> HashMap> { - let all_blocks: HashSet = function.blocks.iter().map(|block| block.id).collect(); let predecessors = compute_predecessors(function); let reachable = reachable_from_entry(function, &predecessors); - let mut dominators = HashMap::new(); - for block in &function.blocks { - if block.id == function.entry { - dominators.insert(block.id, HashSet::from([block.id])); - } else { - dominators.insert(block.id, all_blocks.clone()); - } - } + let block_count = function.blocks.len(); + let all_blocks = full_block_bitset(block_count); + let mut dominators = vec![all_blocks.clone(); block_count]; + let entry_index = function.entry.as_raw() as usize; + dominators[entry_index] = empty_block_bitset(block_count); + set_block_bit(&mut dominators[entry_index], entry_index); + let predecessor_indices = reachable_predecessor_indices(function, &predecessors, &reachable); let mut changed = true; while changed { changed = false; - for block in &function.blocks { + for (block_index, block) in function.blocks.iter().enumerate() { if block.id == function.entry { continue; } - let preds: Vec = predecessors - .get(&block.id) - .map(|preds| preds.iter().copied().filter(|p| reachable.contains(p)).collect()) - .unwrap_or_default(); - let mut next = if preds.is_empty() { - HashSet::new() + let mut next = if predecessor_indices[block_index].is_empty() { + empty_block_bitset(block_count) } else { - intersection_of_predecessors(&preds, &dominators, &all_blocks) + all_blocks.clone() }; - next.insert(block.id); - if dominators.get(&block.id) != Some(&next) { - dominators.insert(block.id, next); + for pred_index in &predecessor_indices[block_index] { + bitset_and_assign(&mut next, &dominators[*pred_index]); + } + set_block_bit(&mut next, block_index); + if dominators[block_index] != next { + dominators[block_index] = next; changed = true; } } } - dominators + dominator_bitsets_to_map(function, &dominators) +} + +/// Converts reachable predecessor block ids into dense block indices for bitset dominator scans. +fn reachable_predecessor_indices( + function: &Function, + predecessors: &HashMap>, + reachable: &HashSet, +) -> Vec> { + function + .blocks + .iter() + .map(|block| { + predecessors + .get(&block.id) + .map(|preds| { + preds + .iter() + .filter(|pred| reachable.contains(pred)) + .map(|pred| pred.as_raw() as usize) + .collect() + }) + .unwrap_or_default() + }) + .collect() +} + +/// Builds a bitset with every block bit set, masking unused bits in the last word. +fn full_block_bitset(block_count: usize) -> Vec { + let mut bits = vec![u64::MAX; block_bitset_word_count(block_count)]; + let trailing_bits = block_count % u64::BITS as usize; + if trailing_bits != 0 { + if let Some(last) = bits.last_mut() { + *last = (1_u64 << trailing_bits) - 1; + } + } + bits +} + +/// Builds an empty block bitset with enough words for the function block count. +fn empty_block_bitset(block_count: usize) -> Vec { + vec![0; block_bitset_word_count(block_count)] +} + +/// Returns the number of machine words needed to represent one block bitset. +fn block_bitset_word_count(block_count: usize) -> usize { + block_count.div_ceil(u64::BITS as usize) +} + +/// Marks one block index as present in a dominator bitset. +fn set_block_bit(bits: &mut [u64], block_index: usize) { + let word = block_index / u64::BITS as usize; + let bit = block_index % u64::BITS as usize; + bits[word] |= 1_u64 << bit; +} + +/// Intersects a dominator bitset in place with another predecessor bitset. +fn bitset_and_assign(left: &mut [u64], right: &[u64]) { + for (left_word, right_word) in left.iter_mut().zip(right.iter()) { + *left_word &= *right_word; + } +} + +/// Converts dense dominator bitsets back to the validator's public block-id map. +fn dominator_bitsets_to_map( + function: &Function, + dominators: &[Vec], +) -> HashMap> { + let mut map = HashMap::with_capacity(function.blocks.len()); + for (block_index, block) in function.blocks.iter().enumerate() { + let mut set = HashSet::new(); + for (candidate_index, candidate) in function.blocks.iter().enumerate() { + if block_bit_is_set(&dominators[block_index], candidate_index) { + set.insert(candidate.id); + } + } + map.insert(block.id, set); + } + map +} + +/// Returns whether a block index is present in a dominator bitset. +fn block_bit_is_set(bits: &[u64], block_index: usize) -> bool { + let word = block_index / u64::BITS as usize; + let bit = block_index % u64::BITS as usize; + bits.get(word) + .is_some_and(|word_bits| (word_bits & (1_u64 << bit)) != 0) } /// Computes the set of blocks reachable from the entry over the same edge set as @@ -1013,25 +1096,6 @@ fn successors(term: &Terminator) -> Vec { } } -/// Intersects dominator sets for all predecessors. -fn intersection_of_predecessors( - predecessors: &[BlockId], - dominators: &HashMap>, - fallback: &HashSet, -) -> HashSet { - let mut iter = predecessors.iter(); - let Some(first) = iter.next() else { - return HashSet::new(); - }; - let mut result = dominators.get(first).cloned().unwrap_or_else(|| fallback.clone()); - for pred in iter { - if let Some(set) = dominators.get(pred) { - result = result.intersection(set).copied().collect(); - } - } - result -} - /// Returns true when PHP type metadata can use the given EIR storage type. fn php_type_compatible(ir_type: IrType, php_type: &PhpType) -> bool { let php_type = php_type.codegen_repr(); diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index e9a64e1922..a58fc1204c 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -811,6 +811,57 @@ Error:Class \"self\" not found" ); } +/// Verifies eval special-class callable arrays preserve by-reference writeback. +#[test] +fn test_eval_special_class_callable_arrays_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function run(): string { + $out = ""; + + $first = "2"; + $out .= call_user_func_array(["self", "add"], [&$first, 3]) . ":" . gettype($first) . ":" . $first . "|"; + + $second = "4"; + $static = Closure::fromCallable(["static", "add"]); + $out .= $static($second, 5) . ":" . gettype($second) . ":" . $second . "|"; + + $third = "6"; + $out .= call_user_func_array(["parent", "bump"], [&$third, 7]) . ":" . gettype($third) . ":" . $third . "|"; + + $fourth = "8"; + $parent = Closure::fromCallable(["parent", "bump"]); + $out .= $parent($fourth, 9) . ":" . gettype($fourth) . ":" . $fourth; + + return $out; + } +} + +return (new EvalSpecialArrayRefChild())->run();'); +"#, + ); + + assert_eq!( + out, + "15:integer:15|19:integer:19|13:integer:13|17:integer:17" + ); +} + /// Verifies `Closure::fromCallable()` normalizes eval string and array callables to Closure objects. #[test] fn test_eval_closure_from_callable_normalizes_string_and_array_callables() { From b8f64df97caca4c973988ab001f00060b2d388d8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 00:01:09 +0200 Subject: [PATCH 1003/1208] Preserve eval Closure null bind scope --- crates/elephc-magician/src/context.rs | 2 +- .../interpreter/builtins/registry/callable.rs | 34 ++++++++++- .../src/interpreter/control.rs | 2 +- .../src/interpreter/dynamic_functions.rs | 57 +++++++++++++++---- .../src/interpreter/statements.rs | 15 ++++- tests/codegen/eval_closures.rs | 27 +++++++++ 6 files changed, 118 insertions(+), 19 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index de1315c639..0fdf4b467e 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -312,7 +312,7 @@ pub enum EvalClosureObjectTarget { Named(String), BoundNamed { name: String, - bound_this: RuntimeCellHandle, + bound_this: Option, bound_scope: Option, }, InvokableObject { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 9ab58c2549..332e2cf9fe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -535,7 +535,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( .closure(name) .cloned() .ok_or(EvalStatus::UnsupportedConstruct)?; - eval_closure_with_evaluated_args_and_bound_this_scope( + eval_bound_closure_with_call_args( &closure, *bound_this, bound_scope.clone(), @@ -613,6 +613,34 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( } } +/// Invokes a bound eval closure with either `$this` or only an explicit class scope. +fn eval_bound_closure_with_call_args( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope( + closure, + this_object, + bound_scope, + evaluated_args, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope( + closure, + bound_scope, + evaluated_args, + context, + values, + ), + } +} + /// Invokes a normalized callback through `call_user_func()` by-value argument semantics. fn eval_evaluated_callable_with_call_user_func_values( callback: &EvaluatedCallable, @@ -633,7 +661,7 @@ fn eval_evaluated_callable_with_call_user_func_values( .closure(name) .cloned() .ok_or(EvalStatus::UnsupportedConstruct)?; - eval_closure_with_evaluated_args_and_bound_this_scope( + eval_bound_closure_with_call_args( &closure, *bound_this, bound_scope.clone(), @@ -1117,7 +1145,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( .closure(name) .cloned() .ok_or(EvalStatus::UnsupportedConstruct)?; - eval_closure_with_evaluated_args_and_bound_this_scope( + eval_bound_closure_with_call_args( &closure, *bound_this, bound_scope.clone(), diff --git a/crates/elephc-magician/src/interpreter/control.rs b/crates/elephc-magician/src/interpreter/control.rs index 0b749e3f53..d8dac6481f 100644 --- a/crates/elephc-magician/src/interpreter/control.rs +++ b/crates/elephc-magician/src/interpreter/control.rs @@ -91,7 +91,7 @@ pub(super) enum EvaluatedCallable { }, BoundClosure { name: String, - bound_this: RuntimeCellHandle, + bound_this: Option, bound_scope: Option, }, InvokableObject { diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 2c4d33e53a..76dd4a4b6b 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1704,7 +1704,7 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_closure_with_optional_bound_this(closure, None, evaluated_args, context, values) + eval_closure_with_optional_binding(closure, None, evaluated_args, context, values) } /// Evaluates one runtime eval closure with `$this` bound by `Closure::call()`. @@ -1740,30 +1740,65 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_sc } let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); - eval_closure_with_optional_bound_this( + eval_closure_with_optional_binding( closure, - Some((bound_this, class_scope, called_class)), + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates one runtime eval closure with a class scope but no `$this` binding. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope( + closure: &EvalClosure, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_evaluated_args(closure, evaluated_args, context, values); + }; + eval_closure_with_optional_binding( + closure, + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), evaluated_args, context, values, ) } -/// Evaluates one runtime eval closure with optional `$this` binding metadata. -fn eval_closure_with_optional_bound_this( +/// Class binding metadata for a runtime eval closure invocation. +struct EvalClosureBinding { + this_object: Option, + class_scope: String, + called_class: String, +} + +/// Evaluates one runtime eval closure with optional class and `$this` binding metadata. +fn eval_closure_with_optional_binding( closure: &EvalClosure, - bound_this: Option<(RuntimeCellHandle, String, String)>, + binding: Option, evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let function = closure.function(); let static_names = static_var_names(function.body()); - let bound_class_pushed = bound_this.is_some(); + let bound_class_pushed = binding.is_some(); context.push_function(function.name()); - if let Some((_, class_scope, called_class)) = &bound_this { - context.push_class_scope(class_scope.to_string()); - context.push_called_class_scope(called_class.to_string()); + if let Some(binding) = &binding { + context.push_class_scope(binding.class_scope.clone()); + context.push_called_class_scope(binding.called_class.clone()); } let evaluated_args = match bind_evaluated_method_args( function.params(), @@ -1787,7 +1822,7 @@ fn eval_closure_with_optional_bound_this( }; let mut function_scope = ElephcEvalScope::new(); bind_closure_captures(&mut function_scope, closure.captures()); - if let Some((object, _, _)) = bound_this { + if let Some(object) = binding.and_then(|binding| binding.this_object) { function_scope.set("this", object, ScopeCellOwnership::Borrowed); } bind_method_scope_args( diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 3b90fded85..b0ef294988 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7713,7 +7713,7 @@ fn eval_closure_bind_target( values: &mut impl RuntimeValueOps, ) -> Result { let Some(bound_this) = bound_this else { - return eval_closure_unbind_target(target, context, values); + return eval_closure_unbind_target(target, bound_scope, context, values); }; match target { EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { @@ -7732,7 +7732,7 @@ fn eval_closure_bind_target( eval_closure_object_from_target( EvalClosureObjectTarget::BoundNamed { name, - bound_this, + bound_this: Some(bound_this), bound_scope, }, context, @@ -7790,12 +7790,21 @@ fn eval_closure_bind_target( /// Creates an unbound Closure object for targets that can drop `$this`. fn eval_closure_unbind_target( target: EvalClosureObjectTarget, + bound_scope: Option, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match target { EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { - eval_closure_object_from_target(EvalClosureObjectTarget::Named(name), context, values) + let target = match bound_scope { + Some(bound_scope) => EvalClosureObjectTarget::BoundNamed { + name, + bound_this: None, + bound_scope: Some(bound_scope), + }, + None => EvalClosureObjectTarget::Named(name), + }; + eval_closure_object_from_target(target, context, values) } EvalClosureObjectTarget::InvokableObject { .. } | EvalClosureObjectTarget::ObjectMethod { .. } => { diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index 9a3ba3c916..d2e666c15f 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -198,6 +198,33 @@ echo call_user_func_array($staticBound, [&$second, 2]) . ":" . gettype($second) ); } +/// Verifies eval Closure binding to `null` preserves explicit class scope and by-ref args. +#[test] +fn test_eval_closure_bind_null_receiver_preserves_explicit_scope_and_by_ref_args() { + let out = compile_and_run( + r#"bindTo(null, "EvalClosureNullScopeBox"); +$second = "3"; +echo call_user_func_array($boundTo, [&$second, 4]) . ":" . gettype($second) . ":" . $second;'); +"#, + ); + + assert_eq!(out, "43:integer:43|47:integer:47"); +} + /// Verifies eval Closure `__invoke` works as an array callable and preserves by-ref args. #[test] fn test_eval_closure_invoke_array_callable_preserves_by_ref_args() { From 8fc24b25f64fe2a13898e25f535e19cbe005bd5e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 00:13:39 +0200 Subject: [PATCH 1004/1208] Cover eval Closure special string callables --- tests/codegen/eval_callables.rs | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index a58fc1204c..da0f755e37 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -603,6 +603,50 @@ $box->run();'); assert_eq!(out, "15:integer:15|19:integer:19|13:integer:13"); } +/// Verifies `Closure::fromCallable()` resolves special string callables through method scope. +#[test] +fn test_eval_closure_from_callable_special_string_callables_preserve_by_ref_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function run(): string { + $self = Closure::fromCallable("self::add"); + $first = "2"; + $out = $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + + $static = Closure::fromCallable("static::add"); + $second = "4"; + $out .= call_user_func_array($static, [&$second, 5]) . ":" . gettype($second) . ":" . $second . "|"; + + $parent = Closure::fromCallable("parent::bump"); + $third = "6"; + $out .= $parent($third, 7) . ":" . gettype($third) . ":" . $third; + + return $out; + } +} + +return (new EvalFromCallableSpecialStringChild())->run();'); +"#, + ); + + assert_eq!(out, "15:integer:15|19:integer:19|13:integer:13"); +} + /// Verifies eval `is_callable()` supports syntax-only probes and callable-name writeback. #[test] fn test_eval_is_callable_supports_syntax_only_and_callable_name_writeback() { From 83299d8ac43430353e4b4410c5f5421f3d08fcec Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 00:22:25 +0200 Subject: [PATCH 1005/1208] Cover eval static callable fatal cleanup --- tests/codegen/eval_callable_ref_errors.rs | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs index e16a4cc18c..794acd5175 100644 --- a/tests/codegen/eval_callable_ref_errors.rs +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -223,6 +223,84 @@ echo "bad";'); ); } +/// Verifies AOT static method callable variants restore the eval bridge frame on prep fatal. +#[test] +fn test_eval_aot_static_method_callable_variants_by_ref_arg_prep_fatal_clean_up_stack() { + let cases = [ + ( + "array callable", + r#" Date: Wed, 1 Jul 2026 00:38:10 +0200 Subject: [PATCH 1006/1208] Persist eval special static string closures --- .../interpreter/builtins/registry/callable.rs | 2 +- tests/codegen/eval_callables.rs | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 332e2cf9fe..c9cd854773 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -425,7 +425,7 @@ fn eval_special_class_array_callable( } } Ok(Some(EvaluatedCallable::StaticMethod { - class_name: class_name.to_string(), + class_name: receiver.dispatch_class, method: method.to_string(), called_class: Some(receiver.called_class), native_class: None, diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index da0f755e37..6e530b1d62 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -647,6 +647,59 @@ return (new EvalFromCallableSpecialStringChild())->run();'); assert_eq!(out, "15:integer:15|19:integer:19|13:integer:13"); } +/// Verifies special static string closures remain callable after leaving method scope. +#[test] +fn test_eval_closure_from_callable_special_static_string_callables_persist_resolved_scope() { + let out = compile_and_run( + r#"make(); +$self = $closures[0]; +$static = $closures[1]; +$parent = $closures[2]; + +$first = "2"; +$out = $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + +$second = "4"; +$out .= call_user_func_array($static, [&$second, 5]) . ":" . gettype($second) . ":" . $second . "|"; + +$third = "6"; +$out .= $parent($third, 7) . ":" . gettype($third) . ":" . $third; + +return $out;'); +"#, + ); + + assert_eq!( + out, + "C:EvalFromCallablePersistentSpecialChild:5:integer:5|\ +C:EvalFromCallablePersistentSpecialChild:9:integer:9|\ +B:EvalFromCallablePersistentSpecialChild:13:integer:13" + ); +} + /// Verifies eval `is_callable()` supports syntax-only probes and callable-name writeback. #[test] fn test_eval_is_callable_supports_syntax_only_and_callable_name_writeback() { From b1b62217e0221169d2f4e0c3eb21d21a45acb153 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 00:46:19 +0200 Subject: [PATCH 1007/1208] Cover eval special array closures after scope --- tests/codegen/eval_callables.rs | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 6e530b1d62..2b2cc17293 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -700,6 +700,59 @@ B:EvalFromCallablePersistentSpecialChild:13:integer:13" ); } +/// Verifies special static array-callable closures remain callable after method scope exits. +#[test] +fn test_eval_closure_from_callable_special_static_array_callables_persist_resolved_scope() { + let out = compile_and_run( + r#" Date: Wed, 1 Jul 2026 01:08:24 +0200 Subject: [PATCH 1008/1208] Cover eval persistent instance special closures --- tests/codegen/eval_callables.rs | 116 ++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 2b2cc17293..d86f47eb33 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -647,6 +647,61 @@ return (new EvalFromCallableSpecialStringChild())->run();'); assert_eq!(out, "15:integer:15|19:integer:19|13:integer:13"); } +/// Verifies special first-class instance closures retain `$this` after method scope exits. +#[test] +fn test_eval_first_class_special_instance_callables_persist_bound_receiver() { + let out = compile_and_run( + r#"base + $delta; + return "C:" . get_called_class() . ":" . get_class($this) . ":" . $value; + } + + public function make(): array { + return [ + self::add(...), + static::add(...), + parent::base(...), + ]; + } +} + +$closures = (new EvalFirstClassPersistentInstanceChild())->make(); +$self = $closures[0]; +$static = $closures[1]; +$parent = $closures[2]; + +$first = "2"; +$out = $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + +$second = "3"; +$out .= call_user_func_array($static, [&$second, 3]) . ":" . gettype($second) . ":" . $second . "|"; + +$third = "4"; +$out .= $parent($third, 3) . ":" . gettype($third) . ":" . $third; + +return $out;'); +"#, + ); + + assert_eq!( + out, + "C:EvalFirstClassPersistentInstanceChild:EvalFirstClassPersistentInstanceChild:15:integer:15|\ +C:EvalFirstClassPersistentInstanceChild:EvalFirstClassPersistentInstanceChild:16:integer:16|\ +B:EvalFirstClassPersistentInstanceChild:EvalFirstClassPersistentInstanceChild:7:integer:7" + ); +} + /// Verifies special static string closures remain callable after leaving method scope. #[test] fn test_eval_closure_from_callable_special_static_string_callables_persist_resolved_scope() { @@ -753,6 +808,67 @@ B:EvalFromCallablePersistentSpecialArrayChild:13:integer:13" ); } +/// Verifies special instance closures retain `$this` after their method scope exits. +#[test] +fn test_eval_closure_from_callable_special_instance_callables_persist_bound_receiver() { + let out = compile_and_run( + r#"base + $delta; + return "C:" . get_called_class() . ":" . get_class($this) . ":" . $value; + } + + public function make(): array { + return [ + Closure::fromCallable("self::add"), + Closure::fromCallable(["static", "add"]), + Closure::fromCallable("parent::base"), + Closure::fromCallable(["parent", "base"]), + ]; + } +} + +$closures = (new EvalFromCallablePersistentInstanceChild())->make(); +$self = $closures[0]; +$static = $closures[1]; +$parentString = $closures[2]; +$parentArray = $closures[3]; + +$first = "2"; +$out = $self($first, 3) . ":" . gettype($first) . ":" . $first . "|"; + +$second = "3"; +$out .= call_user_func_array($static, [&$second, 3]) . ":" . gettype($second) . ":" . $second . "|"; + +$third = "4"; +$out .= $parentString($third, 3) . ":" . gettype($third) . ":" . $third . "|"; + +$fourth = "5"; +$out .= call_user_func_array($parentArray, [&$fourth, 3]) . ":" . gettype($fourth) . ":" . $fourth; + +return $out;'); +"#, + ); + + assert_eq!( + out, + "C:EvalFromCallablePersistentInstanceChild:EvalFromCallablePersistentInstanceChild:15:integer:15|\ +C:EvalFromCallablePersistentInstanceChild:EvalFromCallablePersistentInstanceChild:16:integer:16|\ +B:EvalFromCallablePersistentInstanceChild:EvalFromCallablePersistentInstanceChild:7:integer:7|\ +B:EvalFromCallablePersistentInstanceChild:EvalFromCallablePersistentInstanceChild:8:integer:8" + ); +} + /// Verifies eval `is_callable()` supports syntax-only probes and callable-name writeback. #[test] fn test_eval_is_callable_supports_syntax_only_and_callable_name_writeback() { From a7088aa64d0dba1c125d3dd2915560f1b9e1bd1b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 01:27:42 +0200 Subject: [PATCH 1009/1208] Cover eval named constructor ref bridge --- tests/codegen/eval_constructors.rs | 101 +++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/codegen/eval_constructors.rs b/tests/codegen/eval_constructors.rs index c5d4dddd54..a49fcbd605 100644 --- a/tests/codegen/eval_constructors.rs +++ b/tests/codegen/eval_constructors.rs @@ -50,6 +50,48 @@ return gettype(EvalCtorRefTargetStatic::$value) . ":" . EvalCtorRefTargetStatic: assert_eq!(out, "integer:6|integer:7|integer:8|integer:9"); } +/// Verifies AOT constructor by-reference args write back through named and unpacked calls. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_named_and_spread_writeback() { + let out = compile_and_run( + r#" "4"]; +new $class(...["value" => &$items["x"], "delta" => 5]); +echo gettype($items["x"]) . ":" . $items["x"] . "|"; + +$box = new EvalCtorNamedRefTargetBox(); +new $class(delta: 6, value: $box->value); +echo gettype($box->value) . ":" . $box->value . "|"; + +EvalCtorNamedRefTargetStatic::$value = "8"; +new $class(delta: 7, value: EvalCtorNamedRefTargetStatic::$value); +return gettype(EvalCtorNamedRefTargetStatic::$value) . ":" . EvalCtorNamedRefTargetStatic::$value;'); +"#, + ); + + assert_eq!(out, "integer:5|integer:9|integer:10|integer:15"); +} + /// Verifies AOT constructor by-reference writeback happens before a catchable throw. #[test] fn test_eval_dynamic_new_constructor_by_ref_lvalue_writeback_before_throw() { @@ -128,6 +170,65 @@ echo "bad";'); ); } +/// Verifies named and unpacked AOT constructor arg-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_named_spread_arg_prep_fatal_cleans_up_stack() { + let cases = [ + ( + "named", + r#" &$value, "need" => 123]); +echo "bad";'); +"#, + ), + ]; + + for (label, source) in cases { + let out = compile_and_run_capture(source); + assert!( + !out.success, + "{label}: expected eval runtime fatal, stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "", "{label}: unexpected stdout"); + assert!( + out.stderr.contains("Fatal error: eval() runtime failed"), + "{label}: stderr did not contain eval runtime fatal diagnostic: {}", + out.stderr + ); + assert!( + !out.stderr.contains("panicked at") && !out.stderr.contains("thread '"), + "{label}: stderr leaked a Rust panic: {}", + out.stderr + ); + } +} + /// Verifies eval-declared constructor by-reference args write back to lvalue targets. #[test] fn test_eval_declared_constructor_by_ref_writes_back_to_lvalue_targets() { From 8dda9d17bc415a33e4d6d04f8fa3761cbf07a961 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 01:51:52 +0200 Subject: [PATCH 1010/1208] Cover eval named callable ref bridge --- tests/codegen/eval_callable_ref_errors.rs | 132 ++++++++++++++++++++++ tests/codegen/eval_callables.rs | 58 ++++++++++ 2 files changed, 190 insertions(+) diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs index 794acd5175..b203dcd2a2 100644 --- a/tests/codegen/eval_callable_ref_errors.rs +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -301,6 +301,138 @@ echo "bad";'); } } +/// Verifies named AOT callable by-ref argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_aot_callable_named_ref_arg_prep_fatal_cleans_up_stack() { + let cases = [ + ( + "instance array callable", + r#"bump(...); +$value = "2"; +$callback(need: 123, value: $value); +echo "bad";'); +"#, + ), + ( + "Closure::fromCallable instance", + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +echo eval('$box = new EvalAotCallableNamedRefBox(); + +$array = [$box, "bump"]; +$a = "2"; +echo $array(value: $a, delta: 3) . ":" . gettype($a) . ":" . $a . "|"; + +$first = $box->bump(...); +$b = "4"; +echo $first(delta: 5, value: $b) . ":" . gettype($b) . ":" . $b . "|"; + +$closure = Closure::fromCallable([$box, "bump"]); +$c = "6"; +echo $closure(delta: 7, value: $c) . ":" . gettype($c) . ":" . $c . "|"; + +$string = "EvalAotCallableNamedRefBox::add"; +$d = "8"; +echo $string(delta: 9, value: $d) . ":" . gettype($d) . ":" . $d . "|"; + +$static = EvalAotCallableNamedRefBox::add(...); +$e = "10"; +echo $static(value: $e, delta: 11) . ":" . gettype($e) . ":" . $e . "|"; + +$invokable = new EvalAotCallableNamedRefBox(); +$f = "12"; +return $invokable(delta: 13, value: $f) . ":" . gettype($f) . ":" . $f;'); +"#, + ); + + assert_eq!( + out, + "15:integer:15|19:integer:19|23:integer:23|17:integer:17|21:integer:21|35:integer:35" + ); +} + /// Verifies eval-declared callable forms preserve by-ref writeback. #[test] fn test_eval_declared_callable_forms_preserve_by_ref_writeback() { From 381b470d91e958d5a02ff6e54b58ca6ac3f4453b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 02:11:29 +0200 Subject: [PATCH 1011/1208] Align eval Closure::call value args --- .../interpreter/builtins/registry/callable.rs | 287 ++++++++++++++++++ .../src/interpreter/dynamic_functions.rs | 78 ++++- .../src/interpreter/statements.rs | 37 ++- tests/codegen/eval_callables.rs | 43 ++- tests/codegen/eval_closures.rs | 6 +- 5 files changed, 420 insertions(+), 31 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index c9cd854773..7d5433a4c2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -641,6 +641,37 @@ fn eval_bound_closure_with_call_args( } } +/// Invokes a bound eval closure with caller-selected by-reference binding flags. +fn eval_bound_closure_with_call_args_ref_flags( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope_ref_flags( + closure, + this_object, + bound_scope, + parameter_is_by_ref, + evaluated_args, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + closure, + bound_scope, + parameter_is_by_ref, + evaluated_args, + context, + values, + ), + } +} + /// Invokes a normalized callback through `call_user_func()` by-value argument semantics. fn eval_evaluated_callable_with_call_user_func_values( callback: &EvaluatedCallable, @@ -735,6 +766,124 @@ fn eval_evaluated_callable_with_call_user_func_values( } } +/// Invokes a normalized callback with by-value semantics while preserving named args. +pub(in crate::interpreter) fn eval_evaluated_callable_with_by_value_call_args( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_clear_evaluated_arg_ref_targets(evaluated_args); + match callback { + EvaluatedCallable::Named { name, .. } => { + eval_named_callable_with_call_user_func_args(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let closure = context + .closure(name) + .cloned() + .ok_or(EvalStatus::UnsupportedConstruct)?; + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + eval_bound_closure_with_call_args_ref_flags( + &closure, + *bound_this, + bound_scope.clone(), + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_with_call_user_func_args( + *object, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + *object, + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_object_method_with_call_user_func_args( + *object, + method, + evaluated_args, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_static_method_with_call_user_func_args( + class_name, + method, + called_class.as_deref(), + evaluated_args, + context, + values, + ), + }, + } +} + +/// Removes caller writeback targets before a by-value callable API dispatch. +fn eval_clear_evaluated_arg_ref_targets( + evaluated_args: Vec, +) -> Vec { + evaluated_args + .into_iter() + .map(|arg| EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + }) + .collect() +} + /// Invokes a named callable through `call_user_func()` and warns for by-ref parameters. fn eval_named_callable_with_call_user_func_values( name: &str, @@ -785,6 +934,73 @@ fn eval_named_callable_with_call_user_func_values( Err(EvalStatus::UnsupportedConstruct) } +/// Invokes a named callable through by-value callable semantics with named args. +fn eval_named_callable_with_call_user_func_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args + .iter() + .all(|arg| arg.name.is_none() && arg.ref_target.is_none()) + { + let evaluated_values = evaluated_args.into_iter().map(|arg| arg.value).collect(); + return eval_named_callable_with_call_user_func_values( + name, + evaluated_values, + context, + values, + ); + } + if let Some(closure) = context.closure(name).cloned() { + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + &closure, + None, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + function.name(), + function.params(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_dynamic_function_with_evaluated_args_and_ref_flags( + &function, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + /// Invokes an invokable object through `call_user_func()` by-value argument semantics. fn eval_invokable_object_with_call_user_func_values( object: RuntimeCellHandle, @@ -801,6 +1017,16 @@ fn eval_invokable_object_with_call_user_func_values( ) } +/// Invokes an invokable object through by-value callable semantics with named args. +fn eval_invokable_object_with_call_user_func_args( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_object_method_with_call_user_func_args(object, "__invoke", evaluated_args, context, values) +} + /// Invokes an object-method callable through `call_user_func()` by-value semantics. fn eval_object_method_with_call_user_func_values( object: RuntimeCellHandle, @@ -822,6 +1048,26 @@ fn eval_object_method_with_call_user_func_values( eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) } +/// Invokes an object-method callable through by-value callable semantics with named args. +fn eval_object_method_with_call_user_func_args( + object: RuntimeCellHandle, + method: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_object_method_call_user_func_result( + object, + method, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) +} + /// Attempts call-user-func by-value dispatch for eval-declared or generated object methods. fn eval_object_method_call_user_func_result( object: RuntimeCellHandle, @@ -996,6 +1242,47 @@ fn eval_static_method_with_call_user_func_values( } } +/// Invokes a static-method callable through by-value callable semantics with named args. +fn eval_static_method_with_call_user_func_args( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_static_method_call_user_func_result( + class_name, + method_name, + called_class, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method_name, + evaluated_args, + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_call_result( + class_name, + method_name, + evaluated_args, + context, + values, + ), + } +} + /// Attempts call-user-func by-value dispatch for eval-declared or generated static methods. fn eval_static_method_call_user_func_result( class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 76dd4a4b6b..b3ef9205ff 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1704,32 +1704,51 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_closure_with_optional_binding(closure, None, evaluated_args, context, values) + eval_closure_with_optional_binding( + closure, + function_ref_flags(closure), + None, + evaluated_args, + context, + values, + ) } -/// Evaluates one runtime eval closure with `$this` bound by `Closure::call()`. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this( +/// Evaluates one runtime eval closure with `$this` and an optional binding scope. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope( closure: &EvalClosure, bound_this: RuntimeCellHandle, + bound_scope: Option, evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_closure_with_evaluated_args_and_bound_this_scope( + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); + eval_closure_with_optional_binding( closure, - bound_this, - None, + function_ref_flags(closure), + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), evaluated_args, context, values, ) } -/// Evaluates one runtime eval closure with `$this` and an optional binding scope. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope( +/// Evaluates a runtime eval closure with `$this`, scope, and caller-selected by-ref flags. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope_ref_flags( closure: &EvalClosure, bound_this: RuntimeCellHandle, bound_scope: Option, + parameter_is_by_ref: &[bool], evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -1742,6 +1761,7 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_sc let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); eval_closure_with_optional_binding( closure, + parameter_is_by_ref, Some(EvalClosureBinding { this_object: Some(bound_this), class_scope, @@ -1766,6 +1786,40 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope( }; eval_closure_with_optional_binding( closure, + function_ref_flags(closure), + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a runtime eval closure with scope-only binding and caller-selected by-ref flags. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + closure: &EvalClosure, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + None, + evaluated_args, + context, + values, + ); + }; + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, Some(EvalClosureBinding { this_object: None, called_class: class_scope.clone(), @@ -1784,9 +1838,15 @@ struct EvalClosureBinding { called_class: String, } +/// Returns the closure function's declared by-reference parameter flags. +fn function_ref_flags(closure: &EvalClosure) -> &[bool] { + closure.function().parameter_is_by_ref() +} + /// Evaluates one runtime eval closure with optional class and `$this` binding metadata. fn eval_closure_with_optional_binding( closure: &EvalClosure, + parameter_is_by_ref: &[bool], binding: Option, evaluated_args: Vec, context: &mut ElephcEvalContext, @@ -1804,7 +1864,7 @@ fn eval_closure_with_optional_binding( function.params(), function.parameter_types(), function.parameter_defaults(), - function.parameter_is_by_ref(), + parameter_is_by_ref, function.parameter_is_variadic(), evaluated_args, context, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index b0ef294988..bac334cf25 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -9283,13 +9283,14 @@ fn eval_closure_object_method_result( let (bound_this, call_args) = eval_closure_call_split_args(evaluated_args)?; match target { EvalClosureObjectTarget::Named(name) => { - if let Some(closure) = context.closure(&name).cloned() { - return eval_closure_with_evaluated_args_and_bound_this( - &closure, - bound_this, - call_args, - context, - values, + if context.closure(&name).is_some() { + let callable = EvaluatedCallable::BoundClosure { + name, + bound_this: Some(bound_this), + bound_scope: None, + }; + return eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, ) .map(Some); } @@ -9302,14 +9303,14 @@ fn eval_closure_object_method_result( EvalClosureObjectTarget::BoundNamed { name, bound_scope, .. } => { - if let Some(closure) = context.closure(&name).cloned() { - return eval_closure_with_evaluated_args_and_bound_this_scope( - &closure, - bound_this, + if context.closure(&name).is_some() { + let callable = EvaluatedCallable::BoundClosure { + name, + bound_this: Some(bound_this), bound_scope, - call_args, - context, - values, + }; + return eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, ) .map(Some); } @@ -9327,7 +9328,9 @@ fn eval_closure_object_method_result( ) .map(Some); } - eval_invokable_object_call_result(bound_this, call_args, context, values).map(Some) + let callable = EvaluatedCallable::InvokableObject { object: bound_this }; + eval_evaluated_callable_with_by_value_call_args(&callable, call_args, context, values) + .map(Some) } EvalClosureObjectTarget::ObjectMethod { object, @@ -9353,7 +9356,9 @@ fn eval_closure_object_method_result( native_class, bridge_scope, }; - eval_evaluated_callable_with_call_array_args(&callable, call_args, context, values) + eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, + ) .map(Some) } EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index a788e79fbd..23368811d8 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -1512,9 +1512,9 @@ return $box->value . "|" . $box->apply($callback, 4) . "|" . ); } -/// Verifies `Closure::call()` rebinds `fromCallable()` method closures to a same-class receiver. +/// Verifies `Closure::call()` rebinds method closures but passes later args by value. #[test] -fn test_eval_closure_from_callable_call_rebinds_same_class_method_and_invokable_targets() { +fn test_eval_closure_from_callable_call_rebinds_targets_and_uses_by_value_args() { let out = compile_and_run( r#"call($bound, 1)) ? "S" : "s";'); "#, ); - assert_eq!(out, "25:integer:25|29:integer:29|F|S"); + assert_eq!(out, "25:string:2|29:string:4|F|S"); +} + +/// Verifies `Closure::call()` preserves named argument mapping while using by-value args. +#[test] +fn test_eval_closure_from_callable_call_named_args_use_by_value_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +echo eval('$original = new EvalFromCallableCallNamedBox(); +$original->base = 10; +$bound = new EvalFromCallableCallNamedBox(); +$bound->base = 20; + +$method = Closure::fromCallable([$original, "bump"]); +$a = "2"; +echo $method->call(newThis: $bound, delta: 3, value: $a) . ":" . gettype($a) . ":" . $a . "|"; + +$invoke = Closure::fromCallable($original); +$b = "4"; +return $invoke->call(newThis: $bound, value: $b, delta: 5) . ":" . gettype($b) . ":" . $b;'); +"#, + ); + + assert_eq!(out, "25:string:2|29:string:4"); } /// Verifies `Closure::bindTo()` persists rebinding for method and invokable callable targets. diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index d2e666c15f..760f7e8fc3 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -100,9 +100,9 @@ echo $ref->invokeArgs(["delta" => 5]);'); assert_eq!(out, "C:A:s:C:S:1:4:7:9"); } -/// Verifies eval `Closure::call()` binds `$this` and preserves by-ref argument writeback. +/// Verifies eval `Closure::call()` binds `$this` and passes later args by value. #[test] -fn test_eval_closure_call_binds_this_and_writes_back_by_ref_args() { +fn test_eval_closure_call_binds_this_and_uses_by_value_args() { let out = compile_and_run( r#" Date: Wed, 1 Jul 2026 02:17:32 +0200 Subject: [PATCH 1012/1208] Document complete eval builtin surface --- docs/php/eval.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index 0b9b7cc93e..7455367abb 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -141,12 +141,13 @@ namespaced function is not visible. object, and existing `Closure` callback values and materializes a PHP-visible `Closure` object backed by the normalized eval callable target. `Closure::call()` on those closure objects supports same-class method and invokable-object -rebinding and reports PHP-compatible warning/null results for function and -static-method callables. `Closure::bind()` and `Closure::bindTo()` persistently -bind eval closure literals plus same-class method and invokable-object closure -targets to a new receiver. The optional scope argument is accepted, but eval's -current binding model derives method scope from the bound receiver rather than -exposing the full PHP scope-mutation surface. +rebinding, passes the call arguments after the receiver by value like PHP, and +reports PHP-compatible warning/null results for function and static-method +callables. `Closure::bind()` and `Closure::bindTo()` persistently bind eval +closure literals plus same-class method and invokable-object closure targets to +a new receiver. The optional scope argument is accepted, but eval's current +binding model derives method scope from the bound receiver rather than exposing +the full PHP scope-mutation surface. Closure literals created inside eval are PHP-visible `Closure` objects: they report true for `is_object()`, `get_class($fn)` returns `Closure`, @@ -804,17 +805,20 @@ where listed below unless a note says otherwise. | Area | Builtins | |---|---| | System, time, and environment | `time()`, `microtime()`, `hrtime()`, `date()`, `gmdate()`, `mktime()`, `gmmktime()`, `checkdate()`, `getdate()`, `localtime()`, `strtotime()`, `date_default_timezone_get()`, `date_default_timezone_set()`, `http_response_code()`, `header()`, `phpversion()`, `php_uname()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()` | -| Filesystem and paths | `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()` | -| Stream introspection | `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()` | -| Network and protocol databases | `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()` | -| Strings, bytes, and formatting | `strlen()`, `ord()`, `chr()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `ucwords()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `number_format()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()` | -| Hashing | `crc32()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()` | +| Process execution | `exec()`, `shell_exec()`, `system()`, `passthru()`, `popen()`, `pclose()`, `readline()`, `die()`, `exit()` | +| Filesystem and paths | `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `disk_free_space()`, `disk_total_space()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `chgrp()`, `chown()`, `lchgrp()`, `lchown()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `tmpfile()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()` | +| File and directory streams | `fopen()`, `fclose()`, `feof()`, `fflush()`, `fgetc()`, `fgets()`, `fgetcsv()`, `fpassthru()`, `fprintf()`, `fputcsv()`, `fread()`, `fscanf()`, `flock()`, `fseek()`, `fstat()`, `fsync()`, `fdatasync()`, `ftell()`, `ftruncate()`, `fwrite()`, `rewind()`, `vfprintf()`, `opendir()`, `readdir()`, `closedir()`, `rewinddir()` | +| Streams and stream contexts | `stream_get_filters()`, `stream_get_transports()`, `stream_get_wrappers()`, `stream_isatty()`, `stream_is_local()`, `stream_supports_lock()`, `stream_get_contents()`, `stream_get_line()`, `stream_get_meta_data()`, `stream_copy_to_stream()`, `stream_resolve_include_path()`, `stream_select()`, `stream_set_blocking()`, `stream_set_chunk_size()`, `stream_set_read_buffer()`, `stream_set_timeout()`, `stream_set_write_buffer()`, `stream_context_create()`, `stream_context_get_default()`, `stream_context_get_options()`, `stream_context_get_params()`, `stream_context_set_default()`, `stream_context_set_option()`, `stream_context_set_params()`, `stream_wrapper_register()`, `stream_wrapper_unregister()`, `stream_wrapper_restore()`, `stream_filter_register()`, `stream_filter_append()`, `stream_filter_prepend()`, `stream_filter_remove()`, `stream_bucket_new()`, `stream_bucket_make_writeable()`, `stream_bucket_append()`, `stream_bucket_prepend()` | +| Stream sockets and network databases | `stream_socket_server()`, `stream_socket_client()`, `stream_socket_accept()`, `stream_socket_enable_crypto()`, `stream_socket_get_name()`, `stream_socket_pair()`, `stream_socket_recvfrom()`, `stream_socket_sendto()`, `stream_socket_shutdown()`, `fsockopen()`, `pfsockopen()`, `gethostname()`, `gethostbyname()`, `gethostbyaddr()`, `getprotobyname()`, `getprotobynumber()`, `getservbyname()`, `getservbyport()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()` | +| Strings, bytes, and formatting | `strlen()`, `ord()`, `chr()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `ucwords()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `strrev()`, `grapheme_strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `gzcompress()`, `gzdeflate()`, `gzinflate()`, `gzuncompress()`, `number_format()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()` | +| Hashing | `crc32()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `hash_init()`, `hash_update()`, `hash_final()`, `hash_copy()` | | JSON | `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()` | | Regex | `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()` | | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | -| Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()` | +| Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()`, `spl_autoload()`, `spl_autoload_call()`, `spl_autoload_extensions()`, `spl_autoload_functions()`, `spl_autoload_register()`, `spl_autoload_unregister()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | -| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_called_class()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_class_vars()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_scalar()`, `is_resource()` | +| Raw memory and buffers | `buffer_new()`, `buffer_len()`, `buffer_free()`, `ptr()`, `ptr_null()`, `ptr_is_null()`, `ptr_offset()`, `ptr_get()`, `ptr_set()`, `ptr_read8()`, `ptr_read16()`, `ptr_read32()`, `ptr_read_string()`, `ptr_write8()`, `ptr_write16()`, `ptr_write32()`, `ptr_write_string()`, `ptr_sizeof()` | +| Types, metadata, and dynamic calls | `intval()`, `floatval()`, `strval()`, `boolval()`, `settype()`, `gettype()`, `get_called_class()`, `get_class()`, `get_parent_class()`, `get_class_methods()`, `get_class_vars()`, `get_object_vars()`, `get_resource_type()`, `get_resource_id()`, `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `trait_exists()`, `enum_exists()`, `class_alias()`, `class_implements()`, `class_parents()`, `class_uses()`, `get_declared_classes()`, `get_declared_interfaces()`, `get_declared_traits()`, `method_exists()`, `property_exists()`, `is_a()`, `is_subclass_of()`, `class_attribute_names()`, `class_attribute_args()`, `class_get_attributes()`, `call_user_func()`, `call_user_func_array()`, `empty()`, `isset()`, `unset()`, `is_int()`, `is_integer()`, `is_long()`, `is_float()`, `is_double()`, `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()` for arrays and `Traversable` objects, `is_numeric()`, `is_scalar()`, `is_resource()` | | Debug output | `print_r()`, `var_dump()` | | Constants | `define()`, `defined()` | From 61ca32bf4f4bcde1ab6cfebcc1617297ab9f7a1b Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 02:55:18 +0200 Subject: [PATCH 1013/1208] Align eval call_user_func closure refs --- .../interpreter/builtins/registry/callable.rs | 29 +++++++++-- .../src/interpreter/dynamic_functions.rs | 2 +- tests/codegen/eval_callables.rs | 51 +++++++++++++++++++ tests/codegen/eval_closures.rs | 32 ++++++++++++ 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 7d5433a4c2..c5d3dba4df 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -692,11 +692,21 @@ fn eval_evaluated_callable_with_call_user_func_values( .closure(name) .cloned() .ok_or(EvalStatus::UnsupportedConstruct)?; - eval_bound_closure_with_call_args( + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + eval_bound_closure_with_call_args_ref_flags( &closure, *bound_this, bound_scope.clone(), - positional_args(evaluated_args), + ¶meter_is_by_ref, + evaluated_args, context, values, ) @@ -895,9 +905,20 @@ fn eval_named_callable_with_call_user_func_values( return Ok(result); } if let Some(closure) = context.closure(name).cloned() { - return eval_closure_with_evaluated_args( + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_closure_with_evaluated_args_and_bound_scope_ref_flags( &closure, - positional_args(evaluated_args), + None, + ¶meter_is_by_ref, + evaluated_args, context, values, ); diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index b3ef9205ff..47093f3af4 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1888,7 +1888,7 @@ fn eval_closure_with_optional_binding( bind_method_scope_args( &mut function_scope, function.params(), - function.parameter_is_by_ref(), + parameter_is_by_ref, &evaluated_args, ); let result = execute_statements(function.body(), context, &mut function_scope, values); diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 23368811d8..1e992655b9 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -1368,6 +1368,57 @@ return call_user_func_array($named, [&$d, 9]) . ":" . gettype($d) . ":" . $d;'); ); } +/// Verifies `call_user_func()` invokes `Closure::fromCallable()` targets by value. +#[test] +fn test_eval_closure_from_callable_call_user_func_uses_by_value_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$function = Closure::fromCallable("eval_from_callable_call_user_func_ref_add"); +$a = "2"; +echo call_user_func($function, $a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$box = new EvalFromCallableCallUserFuncRefBox(); +$method = Closure::fromCallable([$box, "bump"]); +$b = "4"; +echo call_user_func($method, $b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$invoke = Closure::fromCallable($box); +$c = "6"; +echo call_user_func($invoke, $c, 7) . ":" . gettype($c) . ":" . $c . "|"; + +$static = Closure::fromCallable(["EvalFromCallableCallUserFuncRefBox", "add"]); +$d = "8"; +return call_user_func($static, $d, 9) . ":" . gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!(out, "5:string:2|19:string:4|23:string:6|17:string:8"); +} + /// Verifies `Closure::fromCallable()` values can cross eval into AOT callable parameters. #[test] fn test_eval_closure_from_callable_values_pass_to_aot_callable_params() { diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index 760f7e8fc3..cbc0823f14 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -125,6 +125,38 @@ echo $seed;'); assert_eq!(out, "15:string:2"); } +/// Verifies eval `call_user_func()` invokes closures with by-value argument semantics. +#[test] +fn test_eval_closure_call_user_func_uses_by_value_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; +}; +$bound = $method->bindTo($box); +$second = "4"; +echo call_user_func($bound, $second, 5); +echo ":" . gettype($second) . ":" . $second;'); +"#, + ); + + assert_eq!(out, "5:string:2|19:string:4"); +} + /// Verifies eval `Closure::bind()` and `bindTo()` persist `$this` across later calls. #[test] fn test_eval_closure_bind_persists_this_and_by_ref_args() { From 887da40dad443a3238add735de82effcca504619 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 03:25:32 +0200 Subject: [PATCH 1014/1208] Align eval call_user_func_array refs --- .../interpreter/builtins/registry/callable.rs | 121 ++++++---- .../src/interpreter/dynamic_functions.rs | 216 ++++++++++++++++-- .../src/interpreter/statements.rs | 62 ++++- tests/codegen/eval_callables.rs | 56 +++++ tests/codegen/eval_closures.rs | 49 ++++ 5 files changed, 447 insertions(+), 57 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index c5d3dba4df..f78c960768 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -672,6 +672,40 @@ fn eval_bound_closure_with_call_args_ref_flags( } } +/// Invokes a bound eval closure with caller-selected by-reference binding mode. +fn eval_bound_closure_with_call_args_ref_mode( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope_ref_mode( + closure, + this_object, + bound_scope, + parameter_is_by_ref, + evaluated_args, + by_ref_mode, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + closure, + bound_scope, + parameter_is_by_ref, + evaluated_args, + by_ref_mode, + context, + values, + ), + } +} + /// Invokes a normalized callback through `call_user_func()` by-value argument semantics. fn eval_evaluated_callable_with_call_user_func_values( callback: &EvaluatedCallable, @@ -1122,21 +1156,17 @@ fn eval_object_method_call_user_func_result( if method.is_static() || method.is_abstract() { return Ok(None); } - let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( - &format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()), - method.params(), - method.parameter_is_by_ref(), - method.parameter_is_variadic(), - evaluated_args.len(), - values, - )?; - return eval_dynamic_method_with_values_and_ref_flags( + let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); + return eval_dynamic_method_with_values_and_ref_mode( &declaring_class, &called_class_name, &method, object, - ¶meter_is_by_ref, + method.parameter_is_by_ref(), evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, context, values, ) @@ -1321,20 +1351,16 @@ fn eval_static_method_call_user_func_result( if !method.is_static() || method.is_abstract() { return Ok(None); } - let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( - &format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()), - method.params(), - method.parameter_is_by_ref(), - method.parameter_is_variadic(), - evaluated_args.len(), - values, - )?; - return eval_dynamic_static_method_with_values_and_ref_flags( + let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); + return eval_dynamic_static_method_with_values_and_ref_mode( &declaring_class, &called_class, &method, - ¶meter_is_by_ref, + method.parameter_is_by_ref(), evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, context, values, ) @@ -1453,17 +1479,21 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( .closure(name) .cloned() .ok_or(EvalStatus::UnsupportedConstruct)?; - eval_bound_closure_with_call_args( + eval_bound_closure_with_call_args_ref_mode( &closure, *bound_this, bound_scope.clone(), + closure.function().parameter_is_by_ref(), evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, context, values, ) } EvaluatedCallable::InvokableObject { object } => { - eval_invokable_object_call_result(*object, evaluated_args, context, values) + eval_invokable_object_with_call_user_func_args(*object, evaluated_args, context, values) } EvaluatedCallable::ObjectMethod { object, @@ -1472,7 +1502,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( native_class, bridge_scope, } => match native_class { - Some(native_class) => eval_native_method_with_evaluated_args_unchecked_bridge_scope( + Some(native_class) => eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( *object, native_class, method, @@ -1482,7 +1512,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( context, values, ), - None => eval_method_call_result_with_evaluated_args( + None => eval_object_method_with_call_user_func_args( *object, method, evaluated_args, @@ -1498,7 +1528,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( bridge_scope, } => match native_class { Some(native_class) => { - eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( native_class, method, evaluated_args, @@ -1509,10 +1539,10 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( ) } None => match called_class { - Some(called_class) => eval_static_method_call_result_with_called_class( + Some(called_class) => eval_static_method_with_call_user_func_args( class_name, - called_class, method, + Some(called_class), evaluated_args, context, values, @@ -1520,9 +1550,10 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( None if eval_callable_array_receiver_is_special_class_name(class_name) => { eval_throw_class_not_found_error(class_name, context, values) } - None => eval_static_method_call_result( + None => eval_static_method_with_call_user_func_args( class_name, method, + None, evaluated_args, context, values, @@ -1569,13 +1600,6 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if evaluated_args - .iter() - .all(|arg| arg.name.is_none() && arg.ref_target.is_none()) - { - let evaluated_args = evaluated_args.into_iter().map(|arg| arg.value).collect(); - return eval_callable_with_values(name, evaluated_args, context, values); - } if let Some(result) = eval_date_procedural_alias_with_evaluated_args(name, evaluated_args.clone(), context, values)? { @@ -1594,19 +1618,38 @@ pub(in crate::interpreter) fn eval_callable_with_call_array_args( return Ok(result); } if let Some(closure) = context.closure(name).cloned() { - return eval_closure_with_evaluated_args(&closure, evaluated_args, context, values); + return eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + &closure, + None, + closure.function().parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, + context, + values, + ); } if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function_with_evaluated_args( + return eval_dynamic_function_with_evaluated_args_and_ref_mode( &function, + function.parameter_is_by_ref(), evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: function.name(), + }, context, values, ); } if let Some(function) = context.native_function(name) { - let evaluated_args = - bind_evaluated_native_function_args(&function, evaluated_args, context, values)?; + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; return eval_native_function_with_values(function, evaluated_args, context, values); } Err(EvalStatus::UnsupportedConstruct) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 47093f3af4..5784e8cd8d 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -882,14 +882,15 @@ fn native_function_regular_param_index( .position(|(index, param)| index < variadic_index && param == name) } -/// Binds evaluated method arguments and fills omitted parameters from defaults. -pub(in crate::interpreter) fn bind_evaluated_method_args( +/// Binds evaluated method arguments using a selected by-reference target policy. +pub(in crate::interpreter) fn bind_evaluated_method_args_with_ref_mode( params: &[String], parameter_types: &[Option], parameter_defaults: &[Option], parameter_is_by_ref: &[bool], parameter_is_variadic: &[bool], evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { @@ -931,12 +932,14 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( &name, arg.value, arg.ref_target, + by_ref_mode, &mut variadic_named_args, context, values, )?; } else { bind_dynamic_positional_method_arg( + params, &mut bound_args, parameter_types, parameter_is_by_ref, @@ -945,6 +948,7 @@ pub(in crate::interpreter) fn bind_evaluated_method_args( &mut next_variadic_index, arg.value, arg.ref_target, + by_ref_mode, context, values, )?; @@ -1013,6 +1017,7 @@ fn evaluated_args_contain_named_variadic_values( /// Binds one positional method argument to a fixed parameter or variadic array. fn bind_dynamic_positional_method_arg( + params: &[String], bound_args: &mut [Option], parameter_types: &[Option], parameter_is_by_ref: &[bool], @@ -1021,10 +1026,19 @@ fn bind_dynamic_positional_method_arg( next_variadic_index: &mut i64, value: RuntimeCellHandle, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if variadic_index.is_some_and(|index| *next_positional >= index) { + let argument_number = variadic_index + .and_then(|index| { + usize::try_from(*next_variadic_index) + .ok() + .and_then(|offset| index.checked_add(offset)) + }) + .and_then(|index| index.checked_add(1)) + .ok_or(EvalStatus::RuntimeFatal)?; let key = values.int(*next_variadic_index)?; *next_variadic_index = next_variadic_index .checked_add(1) @@ -1036,8 +1050,15 @@ fn bind_dynamic_positional_method_arg( context, values, )?; - let ref_target = - method_parameter_ref_target(parameter_is_by_ref, variadic_index, ref_target)?; + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + variadic_index, + argument_number, + ref_target, + by_ref_mode, + values, + )?; return bind_dynamic_variadic_arg( bound_args, variadic_index, @@ -1051,8 +1072,15 @@ fn bind_dynamic_positional_method_arg( if param_index >= bound_args.len() || bound_args[param_index].is_some() { return Err(EvalStatus::RuntimeFatal); } - let ref_target = - method_parameter_ref_target(parameter_is_by_ref, Some(param_index), ref_target)?; + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + Some(param_index), + param_index + 1, + ref_target, + by_ref_mode, + values, + )?; bound_args[param_index] = Some(BoundMethodArg { value, ref_target, @@ -1072,6 +1100,7 @@ fn bind_dynamic_named_method_arg( name: &str, value: RuntimeCellHandle, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, variadic_named_args: &mut std::collections::HashSet, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -1080,8 +1109,15 @@ fn bind_dynamic_named_method_arg( if bound_args[param_index].is_some() { return Err(EvalStatus::RuntimeFatal); } - let ref_target = - method_parameter_ref_target(parameter_is_by_ref, Some(param_index), ref_target)?; + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + Some(param_index), + param_index + 1, + ref_target, + by_ref_mode, + values, + )?; bound_args[param_index] = Some(BoundMethodArg { value, ref_target, @@ -1100,15 +1136,30 @@ fn bind_dynamic_named_method_arg( context, values, )?; - let ref_target = method_parameter_ref_target(parameter_is_by_ref, variadic_index, ref_target)?; + let argument_number = variadic_index + .and_then(|index| index.checked_add(1)) + .ok_or(EvalStatus::RuntimeFatal)?; + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + variadic_index, + argument_number, + ref_target, + by_ref_mode, + values, + )?; bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, ref_target, values) } /// Returns the caller writeback target required by a by-reference method parameter. fn method_parameter_ref_target( + params: &[String], parameter_is_by_ref: &[bool], param_index: Option, + argument_number: usize, ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(param_index) = param_index else { return Ok(None); @@ -1120,7 +1171,44 @@ fn method_parameter_ref_target( { return Ok(None); } - ref_target.map(Some).ok_or(EvalStatus::RuntimeFatal) + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = params + .get(param_index) + .map(String::as_str) + .unwrap_or("arg"); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + argument_number + ))?; + Ok(None) + } + } +} + +/// Returns the by-reference flags that should be installed into the callee scope. +pub(in crate::interpreter) fn method_scope_parameter_ref_flags( + parameter_is_by_ref: &[bool], + bound_args: &[BoundMethodArg], + by_ref_mode: EvalByRefBindingMode<'_>, +) -> Vec { + if matches!(by_ref_mode, EvalByRefBindingMode::RequireTarget) { + return parameter_is_by_ref.to_vec(); + } + parameter_is_by_ref + .iter() + .enumerate() + .map(|(position, is_by_ref)| { + *is_by_ref + && bound_args.get(position).is_some_and(|arg| { + arg.ref_target.is_some() || !arg.variadic_ref_targets.is_empty() + }) + }) + .collect() } /// Applies a variadic parameter type to one captured argument value. @@ -1641,16 +1729,36 @@ pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_ evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_function_with_evaluated_args_and_ref_mode( + function, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Evaluates an eval-declared function with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_mode( + function: &EvalFunction, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let static_names = static_var_names(function.body()); context.push_function(function.name()); - let evaluated_args = match bind_evaluated_method_args( + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( function.params(), function.parameter_types(), function.parameter_defaults(), parameter_is_by_ref, function.parameter_is_variadic(), evaluated_args, + by_ref_mode, context, values, ) { @@ -1660,11 +1768,13 @@ pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_ return Err(status); } }; + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); let mut function_scope = ElephcEvalScope::new(); bind_method_scope_args( &mut function_scope, function.params(), - parameter_is_by_ref, + &scope_parameter_is_by_ref, &evaluated_args, ); let result = execute_statements(function.body(), context, &mut function_scope, values); @@ -1707,6 +1817,7 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args( eval_closure_with_optional_binding( closure, function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, None, evaluated_args, context, @@ -1732,6 +1843,7 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_sc eval_closure_with_optional_binding( closure, function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, Some(EvalClosureBinding { this_object: Some(bound_this), class_scope, @@ -1762,6 +1874,39 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_sc eval_closure_with_optional_binding( closure, parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a runtime eval closure with `$this`, scope, and caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope_ref_mode( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, Some(EvalClosureBinding { this_object: Some(bound_this), class_scope, @@ -1787,6 +1932,7 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope( eval_closure_with_optional_binding( closure, function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, Some(EvalClosureBinding { this_object: None, called_class: class_scope.clone(), @@ -1811,6 +1957,7 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_r return eval_closure_with_optional_binding( closure, parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, None, evaluated_args, context, @@ -1820,6 +1967,43 @@ pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_r eval_closure_with_optional_binding( closure, parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a scope-only runtime eval closure with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + closure: &EvalClosure, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, + None, + evaluated_args, + context, + values, + ); + }; + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, Some(EvalClosureBinding { this_object: None, called_class: class_scope.clone(), @@ -1847,6 +2031,7 @@ fn function_ref_flags(closure: &EvalClosure) -> &[bool] { fn eval_closure_with_optional_binding( closure: &EvalClosure, parameter_is_by_ref: &[bool], + by_ref_mode: EvalByRefBindingMode<'_>, binding: Option, evaluated_args: Vec, context: &mut ElephcEvalContext, @@ -1860,13 +2045,14 @@ fn eval_closure_with_optional_binding( context.push_class_scope(binding.class_scope.clone()); context.push_called_class_scope(binding.called_class.clone()); } - let evaluated_args = match bind_evaluated_method_args( + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( function.params(), function.parameter_types(), function.parameter_defaults(), parameter_is_by_ref, function.parameter_is_variadic(), evaluated_args, + by_ref_mode, context, values, ) { @@ -1885,10 +2071,12 @@ fn eval_closure_with_optional_binding( if let Some(object) = binding.and_then(|binding| binding.this_object) { function_scope.set("this", object, ScopeCellOwnership::Borrowed); } + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); bind_method_scope_args( &mut function_scope, function.params(), - parameter_is_by_ref, + &scope_parameter_is_by_ref, &evaluated_args, ); let result = execute_statements(function.body(), context, &mut function_scope, values); diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index bac334cf25..fded50e3ef 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -10491,6 +10491,31 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_method_with_values_and_ref_mode( + class_name, + called_class_name, + method, + object, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Executes one eval-declared class method with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_mode( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let qualified_method_name = format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); @@ -10499,13 +10524,14 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( context.push_class_scope(class_name.to_string()); context.push_called_class_scope(called_class_name.to_string()); context.push_method_magic_scope(class_name, method); - let evaluated_args = match bind_evaluated_method_args( + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( method.params(), method.parameter_types(), method.parameter_defaults(), parameter_is_by_ref, method.parameter_is_variadic(), evaluated_args, + by_ref_mode, context, values, ) { @@ -10520,10 +10546,12 @@ pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( }; let mut method_scope = ElephcEvalScope::new(); method_scope.set("this", object, ScopeCellOwnership::Borrowed); + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); bind_method_scope_args( &mut method_scope, method.params(), - parameter_is_by_ref, + &scope_parameter_is_by_ref, &evaluated_args, ); let result = execute_statements(method.body(), context, &mut method_scope, values); @@ -10588,6 +10616,29 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_fla evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_static_method_with_values_and_ref_mode( + class_name, + called_class_name, + method, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Executes one eval-declared static method with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_mode( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result { let qualified_method_name = format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); @@ -10596,13 +10647,14 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_fla context.push_class_scope(class_name.to_string()); context.push_called_class_scope(called_class_name.to_string()); context.push_method_magic_scope(class_name, method); - let evaluated_args = match bind_evaluated_method_args( + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( method.params(), method.parameter_types(), method.parameter_defaults(), parameter_is_by_ref, method.parameter_is_variadic(), evaluated_args, + by_ref_mode, context, values, ) { @@ -10616,10 +10668,12 @@ pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_fla } }; let mut method_scope = ElephcEvalScope::new(); + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); bind_method_scope_args( &mut method_scope, method.params(), - parameter_is_by_ref, + &scope_parameter_is_by_ref, &evaluated_args, ); let result = execute_statements(method.body(), context, &mut method_scope, values); diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 1e992655b9..d91a4786f1 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -1419,6 +1419,62 @@ return call_user_func($static, $d, 9) . ":" . gettype($d) . ":" . $d;'); assert_eq!(out, "5:string:2|19:string:4|23:string:6|17:string:8"); } +/// Verifies `call_user_func_array()` degrades non-reference `Closure::fromCallable()` args by value. +#[test] +fn test_eval_closure_from_callable_call_user_func_array_degrades_non_ref_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } +} + +echo eval('$function = Closure::fromCallable("eval_from_callable_call_user_func_array_ref_add"); +$a = "2"; +$aArgs = [$a, 3]; +echo call_user_func_array($function, $aArgs) . ":" . gettype($a) . ":" . $a . ":" . gettype($aArgs[0]) . ":" . $aArgs[0] . "|"; + +$b = "4"; +$bArgs = [&$b, 5]; +echo call_user_func_array($function, $bArgs) . ":" . gettype($b) . ":" . $b . "|"; + +$box = new EvalFromCallableCallUserFuncArrayRefBox(); +$method = Closure::fromCallable([$box, "bump"]); +$c = "6"; +$cArgs = [$c, 7]; +echo call_user_func_array($method, $cArgs) . ":" . gettype($c) . ":" . $c . ":" . gettype($cArgs[0]) . ":" . $cArgs[0] . "|"; + +$d = "8"; +$dArgs = [&$d, 9]; +echo call_user_func_array($method, $dArgs) . ":" . gettype($d) . ":" . $d . "|"; + +$static = Closure::fromCallable(["EvalFromCallableCallUserFuncArrayRefBox", "add"]); +$e = "10"; +$eArgs = [$e, 11]; +return call_user_func_array($static, $eArgs) . ":" . gettype($e) . ":" . $e . ":" . gettype($eArgs[0]) . ":" . $eArgs[0];'); +"#, + ); + + assert_eq!( + out, + "5:string:2:string:2|9:integer:9|23:string:6:string:6|27:integer:27|21:string:10:string:10" + ); +} + /// Verifies `Closure::fromCallable()` values can cross eval into AOT callable parameters. #[test] fn test_eval_closure_from_callable_values_pass_to_aot_callable_params() { diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index cbc0823f14..961662306c 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -157,6 +157,55 @@ echo ":" . gettype($second) . ":" . $second;'); assert_eq!(out, "5:string:2|19:string:4"); } +/// Verifies eval `call_user_func_array()` degrades non-reference closure args by value. +#[test] +fn test_eval_closure_call_user_func_array_degrades_non_ref_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; +}; +$bound = $method->bindTo($box); +$third = "6"; +$thirdArgs = [$third, 7]; +echo call_user_func_array($bound, $thirdArgs); +echo ":" . gettype($third) . ":" . $third; +echo ":" . gettype($thirdArgs[0]) . ":" . $thirdArgs[0] . "|"; + +$fourth = "8"; +$fourthArgs = [&$fourth, 9]; +echo call_user_func_array($bound, $fourthArgs); +echo ":" . gettype($fourth) . ":" . $fourth;'); +"#, + ); + + assert_eq!( + out, + "5:string:2:string:2|9:integer:9|23:string:6:string:6|27:integer:27" + ); +} + /// Verifies eval `Closure::bind()` and `bindTo()` persist `$this` across later calls. #[test] fn test_eval_closure_bind_persists_this_and_by_ref_args() { From c5ff85be12efbbb6caaeb0e71e414b5697799812 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 03:50:58 +0200 Subject: [PATCH 1015/1208] Align eval array reference reads --- crates/elephc-magician/src/interpreter/expressions.rs | 5 +++++ tests/codegen/eval_callables.rs | 6 +++--- tests/codegen/eval_closures.rs | 8 +++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 333ffa8b30..d67f26d0e8 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -430,6 +430,11 @@ pub(in crate::interpreter) fn eval_array_get_result( values: &mut impl RuntimeValueOps, ) -> Result { if values.type_tag(array)? != EVAL_TAG_OBJECT { + if let Some(target) = eval_array_reference_key(index, values)? + .and_then(|key| context.array_element_alias(array, &key).cloned()) + { + return eval_reference_target_value(&target, context, values); + } return values.array_get(array, index); } if !eval_array_access_object_matches(array, context, values)? { diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index d91a4786f1..4d73783dc8 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -1450,7 +1450,7 @@ echo call_user_func_array($function, $aArgs) . ":" . gettype($a) . ":" . $a . ": $b = "4"; $bArgs = [&$b, 5]; -echo call_user_func_array($function, $bArgs) . ":" . gettype($b) . ":" . $b . "|"; +echo call_user_func_array($function, $bArgs) . ":" . gettype($b) . ":" . $b . ":" . gettype($bArgs[0]) . ":" . $bArgs[0] . "|"; $box = new EvalFromCallableCallUserFuncArrayRefBox(); $method = Closure::fromCallable([$box, "bump"]); @@ -1460,7 +1460,7 @@ echo call_user_func_array($method, $cArgs) . ":" . gettype($c) . ":" . $c . ":" $d = "8"; $dArgs = [&$d, 9]; -echo call_user_func_array($method, $dArgs) . ":" . gettype($d) . ":" . $d . "|"; +echo call_user_func_array($method, $dArgs) . ":" . gettype($d) . ":" . $d . ":" . gettype($dArgs[0]) . ":" . $dArgs[0] . "|"; $static = Closure::fromCallable(["EvalFromCallableCallUserFuncArrayRefBox", "add"]); $e = "10"; @@ -1471,7 +1471,7 @@ return call_user_func_array($static, $eArgs) . ":" . gettype($e) . ":" . $e . ": assert_eq!( out, - "5:string:2:string:2|9:integer:9|23:string:6:string:6|27:integer:27|21:string:10:string:10" + "5:string:2:string:2|9:integer:9:integer:9|23:string:6:string:6|27:integer:27:integer:27|21:string:10:string:10" ); } diff --git a/tests/codegen/eval_closures.rs b/tests/codegen/eval_closures.rs index 961662306c..9ac288d3c0 100644 --- a/tests/codegen/eval_closures.rs +++ b/tests/codegen/eval_closures.rs @@ -179,7 +179,8 @@ echo ":" . gettype($firstArgs[0]) . ":" . $firstArgs[0] . "|"; $second = "4"; $secondArgs = [&$second, 5]; echo call_user_func_array($fn, $secondArgs); -echo ":" . gettype($second) . ":" . $second . "|"; +echo ":" . gettype($second) . ":" . $second; +echo ":" . gettype($secondArgs[0]) . ":" . $secondArgs[0] . "|"; $box = new EvalClosureCallUserFuncArrayBox(); $method = function(int &$value, int $delta): int { @@ -196,13 +197,14 @@ echo ":" . gettype($thirdArgs[0]) . ":" . $thirdArgs[0] . "|"; $fourth = "8"; $fourthArgs = [&$fourth, 9]; echo call_user_func_array($bound, $fourthArgs); -echo ":" . gettype($fourth) . ":" . $fourth;'); +echo ":" . gettype($fourth) . ":" . $fourth; +echo ":" . gettype($fourthArgs[0]) . ":" . $fourthArgs[0];'); "#, ); assert_eq!( out, - "5:string:2:string:2|9:integer:9|23:string:6:string:6|27:integer:27" + "5:string:2:string:2|9:integer:9:integer:9|23:string:6:string:6|27:integer:27:integer:27" ); } From be8974709a4f5b560dba4abf79b3f9f2a3b82940 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 04:05:08 +0200 Subject: [PATCH 1016/1208] Cover eval variadic reference callables --- tests/codegen/eval_callables.rs | 58 +++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 4d73783dc8..f6f7ad7d3f 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -244,6 +244,64 @@ return call_user_func_array($closureNamedStatic, [&$j, 9]) . ":" . gettype($j) . ); } +/// Verifies eval-declared callable forms preserve by-ref variadic element writeback. +#[test] +fn test_eval_declared_callable_forms_preserve_by_ref_variadic_writeback() { + let out = compile_and_run( + r#"collect(...); +$c = "C"; +$d = "D"; +echo $first($c, named: $d) . ":" . $c . ":" . $d . "|"; + +$closure = Closure::fromCallable([$box, "collect"]); +$e = "E"; +$f = "F"; +echo $closure($e, named: $f) . ":" . $e . ":" . $f . "|"; + +$string = "EvalDeclaredVariadicRefCallableBox::collectStatic"; +$g = "G"; +$h = "H"; +echo $string($g, named: $h) . ":" . $g . ":" . $h . "|"; + +$i = "I"; +$j = "J"; +$args = [&$i, "named" => &$j]; +return call_user_func_array($closure, $args) . ":" . $i . ":" . $j . ":" . + $args[0] . ":" . $args["named"];'); +"#, + ); + + assert_eq!( + out, + concat!( + "A-i:B-n:A-i:B-n|C-i:D-n:C-i:D-n|E-i:F-n:E-i:F-n|", + "G-s:H-sn:G-s:H-sn|I-i:J-n:I-i:J-n:I-i:J-n" + ) + ); +} + /// Verifies eval first-class callables are PHP-visible `Closure` objects and remain invokable. #[test] fn test_eval_first_class_callables_are_php_closure_objects() { From 65c49864e568d4dcff9cd2a9df987e7e43cc8611 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 04:21:14 +0200 Subject: [PATCH 1017/1208] Document eval AOT variadic ref limitation --- docs/php/eval.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/php/eval.md b/docs/php/eval.md index 7455367abb..e18defc012 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -118,6 +118,10 @@ or `float`), string raw storage, or one-word heap raw storage (`array`, layouts, and other unsupported raw-storage by-reference free-function parameters remain metadata-only until the function bridge has typed staging/writeback for those ABI layouts. +Generated/AOT `&...$variadic` by-reference parameter tails are also +metadata-visible but not PHP-compatible through eval yet: the bridge currently +builds a temporary variadic array and does not propagate element-level writes +back to the caller variables. `call_user_func()` remains by-value for registered AOT free-function by-reference parameters. From 4361faacb2f4c9a5d9936f7428b8f6eb69f96857 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 04:27:35 +0200 Subject: [PATCH 1018/1208] Cover eval string callable ref builtins --- tests/codegen/eval_builtin_parity.rs | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 3c6f3feec1..2fb153c3c2 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -197,6 +197,39 @@ echo gettype(EvalBuiltinRefBridgeBox::$typed) . ":" . EvalBuiltinRefBridgeBox::$ assert_eq!(out, "S1,2,3|Tinteger:42|2:3,1|Pinteger:123"); } +/// Verifies eval string-callable ref-like builtins write back through lvalue targets. +#[test] +fn test_eval_string_callable_ref_like_builtins_write_back_aliases() { + let out = compile_and_run( + r#"items) . ":" . implode(",", $box->items) . "|"; + +$setter = "settype"; +echo $setter(EvalStringBuiltinRefBridgeBox::$typed, "integer") ? "P" : "p"; +echo gettype(EvalStringBuiltinRefBridgeBox::$typed) . ":" . EvalStringBuiltinRefBridgeBox::$typed;'); +"#, + ); + + assert_eq!(out, "S1,2,3|Tinteger:42|2:3,1|Pinteger:77"); +} + /// Verifies eval `call_user_func_array()` preserves named ref-like builtin targets. #[test] fn test_eval_call_user_func_array_ref_like_builtins_write_back_named_aliases() { From 209004bb6656fdb42b7cd6c8e8dbaf4017ce5ad4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 05:43:39 +0200 Subject: [PATCH 1019/1208] Support by-ref variadic eval parity --- src/builtins/array/uasort.rs | 2 + src/builtins/array/usort.rs | 2 + src/codegen/lower_inst.rs | 22 ++- src/codegen/lower_inst/arrays.rs | 174 ++++++++++++++++-- src/conditional/exprs.rs | 2 + src/conditional/stmts.rs | 2 + src/ir_lower/expr/mod.rs | 78 +++++++- src/ir_lower/fibers.rs | 25 ++- src/ir_lower/function.rs | 50 +++-- src/ir_lower/program.rs | 1 + src/ir_lower/tests/exhaustive.rs | 3 + src/magic_constants/walker/exprs.rs | 2 + src/magic_constants/walker/stmts.rs | 2 + src/name_resolver/declarations.rs | 2 + src/name_resolver/expressions.rs | 2 + src/optimize/control/dce.rs | 2 + src/optimize/control/fold.rs | 2 + src/optimize/control/prune/expr.rs | 2 + src/optimize/control/prune/statements.rs | 2 + src/optimize/fold/expr.rs | 3 + src/optimize/propagate/expr.rs | 2 + src/optimize/propagate/stmt.rs | 2 + src/optimize/tests/dce/basics.rs | 1 + .../composite_guards/composite_regions.rs | 5 + .../composite_guards/elseif_suffixes.rs | 4 + .../tests/dce/guards/excluded_guards.rs | 5 + .../dce/guards/outer_guards/boolean_guards.rs | 4 + .../dce/guards/outer_guards/scalar_guards.rs | 7 + src/optimize/tests/dce/switches/basics.rs | 1 + .../tests/dce/switches/case_shadowing.rs | 3 + .../tests/dce/switches/exhaustive_suffixes.rs | 5 + .../dce/switches/guarded_cases/cumulative.rs | 2 + .../dce/switches/guarded_cases/impossible.rs | 5 + .../dce/switches/guarded_cases/truthiness.rs | 3 + src/optimize/tests/dce/switches/tail_paths.rs | 3 + src/optimize/tests/dce/tail_sinking.rs | 5 + src/optimize/tests/dce/tries/catch_pruning.rs | 5 + src/optimize/tests/dce/tries/finally_paths.rs | 3 + src/optimize/tests/dce/tries/tail_paths.rs | 2 + src/optimize/tests/dce/tries/try_pruning.rs | 7 + src/optimize/tests/effects/basic_calls.rs | 3 + .../effects/callable_aliases/path_merges.rs | 4 + .../effects/callable_aliases/tracking.rs | 6 + src/optimize/tests/effects/methods.rs | 6 + src/optimize/tests/prune.rs | 4 + src/parser/ast/expr.rs | 2 + src/parser/ast/oop.rs | 2 + src/parser/ast/stmt.rs | 2 + src/parser/expr/prefix_complex.rs | 13 +- src/parser/stmt/oop/body.rs | 4 + src/parser/stmt/oop/method_params.rs | 4 + src/parser/stmt/params.rs | 15 +- src/resolver/engine.rs | 2 + src/resolver/exprs.rs | 2 + src/resolver/stmt_exprs.rs | 2 + src/types/checker/builtin_interfaces.rs | 2 + src/types/checker/builtin_iterators.rs | 4 + src/types/checker/builtin_json.rs | 1 + .../checker/builtin_spl_classes/common.rs | 1 + .../checker/builtin_spl_classes/regex.rs | 1 + src/types/checker/builtin_types/calendar.rs | 1 + .../checker/builtin_types/date_period.rs | 2 + src/types/checker/builtin_types/datetime.rs | 24 +++ src/types/checker/builtin_types/exception.rs | 3 + src/types/checker/builtin_types/fiber.rs | 8 + src/types/checker/builtin_types/reflection.rs | 50 +++++ .../callables/preg_replace_callback.rs | 3 +- src/types/checker/callables/closures.rs | 14 +- src/types/checker/driver/functions.rs | 7 +- src/types/checker/driver/mod.rs | 5 +- src/types/checker/functions/resolution/mod.rs | 13 +- .../functions/resolution/specialization.rs | 18 +- src/types/checker/inference/expr/mod.rs | 2 + .../checker/inference/objects/methods.rs | 34 +++- src/types/checker/inference/ops.rs | 9 + src/types/checker/method_pass.rs | 7 +- src/types/checker/mod.rs | 2 + src/types/checker/schema/classes/constants.rs | 2 + src/types/checker/schema/validation.rs | 29 +-- src/types/checker/type_compat/declarations.rs | 4 +- src/types/signatures.rs | 26 ++- .../codegen/callables/state_and_variadics.rs | 28 +++ tests/parser_tests/classes/declarations.rs | 15 ++ tests/parser_tests/functions.rs | 41 +++++ 84 files changed, 803 insertions(+), 78 deletions(-) diff --git a/src/builtins/array/uasort.rs b/src/builtins/array/uasort.rs index f509184416..a0063388f3 100644 --- a/src/builtins/array/uasort.rs +++ b/src/builtins/array/uasort.rs @@ -48,6 +48,7 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { if let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -58,6 +59,7 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { cx.checker.infer_closure_type_with_param_hints( params, variadic, + *variadic_by_ref, return_type, body, captures, diff --git a/src/builtins/array/usort.rs b/src/builtins/array/usort.rs index 9f45cd533f..5197ad5b9f 100644 --- a/src/builtins/array/usort.rs +++ b/src/builtins/array/usort.rs @@ -48,6 +48,7 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { if let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -58,6 +59,7 @@ fn check(cx: &mut BuiltinCheckCtx) -> Result { cx.checker.infer_closure_type_with_param_hints( params, variadic, + *variadic_by_ref, return_type, body, captures, diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 6954793a7d..ec847fab9f 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -672,13 +672,29 @@ fn ensure_variadic_param_slot(signature: &mut FunctionSig) { if signature.params.iter().any(|(name, _)| name == &variadic) { return; } + let variadic_index = signature.params.len(); + let variadic_type_expr = if signature.param_type_exprs.len() > variadic_index { + signature.param_type_exprs.remove(variadic_index) + } else { + None + }; + let variadic_ref = if signature.ref_params.len() > variadic_index { + signature.ref_params.remove(variadic_index) + } else { + false + }; + let variadic_declared = if signature.declared_params.len() > variadic_index { + signature.declared_params.remove(variadic_index) + } else { + false + }; signature .params .push((variadic, PhpType::Array(Box::new(PhpType::Mixed)))); signature.defaults.push(None); - signature.ref_params.push(false); - signature.declared_params.push(false); - signature.param_type_exprs.push(None); + signature.ref_params.push(variadic_ref); + signature.declared_params.push(variadic_declared); + signature.param_type_exprs.push(variadic_type_expr); } /// Lowers a concrete include-loaded function variant activation marker. diff --git a/src/codegen/lower_inst/arrays.rs b/src/codegen/lower_inst/arrays.rs index 4afaee788d..7ba03f261c 100644 --- a/src/codegen/lower_inst/arrays.rs +++ b/src/codegen/lower_inst/arrays.rs @@ -11,8 +11,10 @@ use crate::codegen::{ abi, emit_box_current_owned_value_as_mixed, emit_box_current_value_as_mixed, - emit_release_pushed_refcounted_temp_after_array_push, runtime_value_tag, + emit_box_runtime_payload_as_mixed, emit_release_pushed_refcounted_temp_after_array_push, + runtime_value_tag, }; +use crate::codegen::builtins::arrays::call_user_func_array::INVOKER_ARG_REF_CELL_TAG; use crate::codegen::platform::Arch; use crate::codegen::sentinels::TAGGED_SCALAR_ARRAY_VALUE_TYPE; use crate::ir::{Immediate, Instruction, LocalSlotId, Op, ValueDef, ValueId}; @@ -801,7 +803,7 @@ fn emit_array_get_in_bounds_aarch64( PhpType::Mixed => { ctx.emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip the indexed-array header to reach Mixed cell payloads ctx.emitter.instruction(&format!("ldr {}, [{}, {}, lsl #3]", index_reg, array_reg, index_reg)); // load the selected boxed Mixed cell - abi::emit_incref_if_refcounted(ctx.emitter, elem_ty); + emit_mixed_array_get_deref_invoker_ref_cell(ctx, index_reg); } other if other.is_refcounted() => { ctx.emitter.instruction(&format!("add {}, {}, #24", array_reg, array_reg)); // skip the indexed-array header to reach pointer payloads @@ -863,7 +865,7 @@ fn emit_array_get_in_bounds_x86_64( PhpType::Mixed => { ctx.emitter.instruction(&format!("lea {}, [{} + 24]", array_reg, array_reg)); // skip the indexed-array header to reach Mixed cell payloads ctx.emitter.instruction(&format!("mov {}, QWORD PTR [{} + {} * 8]", index_reg, array_reg, index_reg)); // load the selected boxed Mixed cell - abi::emit_incref_if_refcounted(ctx.emitter, elem_ty); + emit_mixed_array_get_deref_invoker_ref_cell(ctx, index_reg); } other if other.is_refcounted() => { ctx.emitter.instruction(&format!("lea {}, [{} + 24]", array_reg, array_reg)); // skip the indexed-array header to reach pointer payloads @@ -880,6 +882,80 @@ fn emit_array_get_in_bounds_x86_64( Ok(()) } +/// Dereferences descriptor-style ref-cell markers loaded from Mixed array slots. +fn emit_mixed_array_get_deref_invoker_ref_cell( + ctx: &mut FunctionContext<'_>, + mixed_reg: &str, +) { + let ref_label = ctx.next_label("array_get_mixed_ref_cell"); + let done_label = ctx.next_label("array_get_mixed_done"); + let tag_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_load_from_address(ctx.emitter, tag_reg, mixed_reg, 0); + emit_branch_if_invoker_ref_cell_tag(ctx, tag_reg, &ref_label); + abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Mixed); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&ref_label); + emit_box_loaded_invoker_ref_cell_value_as_mixed(ctx, mixed_reg); + ctx.emitter.label(&done_label); +} + +/// Boxes the current value referenced by a loaded invoker ref-cell marker. +fn emit_box_loaded_invoker_ref_cell_value_as_mixed( + ctx: &mut FunctionContext<'_>, + mixed_reg: &str, +) { + let ref_cell_reg = abi::symbol_scratch_reg(ctx.emitter); + let tag_reg = abi::secondary_scratch_reg(ctx.emitter); + let lo_reg = abi::tertiary_scratch_reg(ctx.emitter); + let hi_reg = match ctx.emitter.target.arch { + Arch::AArch64 => "x12", + Arch::X86_64 => "rdx", + }; + let string_hi_label = ctx.next_label("array_get_mixed_ref_string_hi"); + let box_label = ctx.next_label("array_get_mixed_ref_box"); + + abi::emit_load_from_address(ctx.emitter, ref_cell_reg, mixed_reg, 8); + abi::emit_load_from_address(ctx.emitter, tag_reg, mixed_reg, 16); + abi::emit_load_from_address(ctx.emitter, lo_reg, ref_cell_reg, 0); + abi::emit_load_int_immediate(ctx.emitter, hi_reg, 0); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #1", tag_reg)); // check whether the referenced value is a string slot + ctx.emitter.instruction(&format!("b.eq {}", string_hi_label)); // load string length only for string ref-cells + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, 1", tag_reg)); // check whether the referenced value is a string slot + ctx.emitter.instruction(&format!("je {}", string_hi_label)); // load string length only for string ref-cells + } + } + abi::emit_jump(ctx.emitter, &box_label); + + ctx.emitter.label(&string_hi_label); + abi::emit_load_from_address(ctx.emitter, hi_reg, ref_cell_reg, 8); + + ctx.emitter.label(&box_label); + emit_box_runtime_payload_as_mixed(ctx.emitter, tag_reg, lo_reg, hi_reg); +} + +/// Branches when a loaded Mixed tag is an invoker ref-cell marker. +fn emit_branch_if_invoker_ref_cell_tag( + ctx: &mut FunctionContext<'_>, + tag_reg: &str, + label: &str, +) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #{}", tag_reg, INVOKER_ARG_REF_CELL_TAG)); // check for a by-reference variadic marker + ctx.emitter.instruction(&format!("b.eq {}", label)); // dereference marker slots instead of returning the marker + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", tag_reg, INVOKER_ARG_REF_CELL_TAG)); // check for a by-reference variadic marker + ctx.emitter.instruction(&format!("je {}", label)); // dereference marker slots instead of returning the marker + } + } +} + /// Emits PHP's undefined integer array-key warning for the key in the result register. fn emit_undefined_array_key_warning(ctx: &mut FunctionContext<'_>) { abi::emit_call_label(ctx.emitter, "__rt_warn_undefined_array_key_int"); @@ -1387,16 +1463,22 @@ fn lower_mixed_array_set_aarch64( value: ValueId, value_ty: &PhpType, ) -> Result<()> { - if matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - ctx.load_value_to_result(value)?; - abi::emit_incref_if_refcounted(ctx.emitter, value_ty); + let value_ty = value_ty.codegen_repr(); + let fresh_boxed_value = !matches!(value_ty, PhpType::Mixed | PhpType::Union(_)); + if fresh_boxed_value { + box_value_for_mixed_container(ctx, value, &value_ty)?; } else { - box_value_for_mixed_container(ctx, value, value_ty)?; + ctx.load_value_to_result(value)?; + abi::emit_incref_if_refcounted(ctx.emitter, &value_ty); } abi::emit_push_reg(ctx.emitter, "x0"); ctx.load_value_to_reg(array, "x0")?; ctx.load_value_to_reg(index, "x1")?; abi::emit_pop_reg(ctx.emitter, "x2"); + if fresh_boxed_value { + emit_mixed_array_set_ref_marker_writeback_aarch64(ctx); + return Ok(()); + } abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); Ok(()) } @@ -1409,20 +1491,90 @@ fn lower_mixed_array_set_x86_64( value: ValueId, value_ty: &PhpType, ) -> Result<()> { - if matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - ctx.load_value_to_result(value)?; - abi::emit_incref_if_refcounted(ctx.emitter, value_ty); + let value_ty = value_ty.codegen_repr(); + let fresh_boxed_value = !matches!(value_ty, PhpType::Mixed | PhpType::Union(_)); + if fresh_boxed_value { + box_value_for_mixed_container(ctx, value, &value_ty)?; } else { - box_value_for_mixed_container(ctx, value, value_ty)?; + ctx.load_value_to_result(value)?; + abi::emit_incref_if_refcounted(ctx.emitter, &value_ty); } abi::emit_push_reg(ctx.emitter, "rax"); ctx.load_value_to_reg(array, "rdi")?; ctx.load_value_to_reg(index, "rsi")?; abi::emit_pop_reg(ctx.emitter, "rdx"); + if fresh_boxed_value { + emit_mixed_array_set_ref_marker_writeback_x86_64(ctx); + return Ok(()); + } abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); Ok(()) } +/// Stores a fresh boxed-Mixed value through an invoker ref-cell marker on AArch64. +fn emit_mixed_array_set_ref_marker_writeback_aarch64(ctx: &mut FunctionContext<'_>) { + let runtime_label = ctx.next_label("mixed_array_set_runtime"); + let done_label = ctx.next_label("mixed_array_set_done"); + + ctx.emitter.instruction("cmp x1, #0"); // reject negative indexes before checking for by-reference markers + ctx.emitter.instruction(&format!("b.lt {}", runtime_label)); // let the runtime setter drop ignored negative-index writes + ctx.emitter.instruction("ldr x9, [x0]"); // load the current logical length of the indexed array + ctx.emitter.instruction("cmp x1, x9"); // only existing slots can hold by-reference marker cells + ctx.emitter.instruction(&format!("b.hs {}", runtime_label)); // delegate appends and gap writes to the runtime setter + ctx.emitter.instruction("add x10, x0, #24"); // compute the boxed-Mixed payload base for indexed slots + ctx.emitter.instruction("ldr x11, [x10, x1, lsl #3]"); // load the existing boxed Mixed slot + ctx.emitter.instruction(&format!("cbz x11, {}", runtime_label)); // null gap slots are ordinary array writes + ctx.emitter.instruction("ldr x12, [x11]"); // load the existing Mixed tag for marker detection + ctx.emitter.instruction(&format!("cmp x12, #{}", INVOKER_ARG_REF_CELL_TAG)); // check whether the slot aliases caller storage + ctx.emitter.instruction(&format!("b.ne {}", runtime_label)); // ordinary boxed Mixed slots are replaced by the runtime setter + ctx.emitter.instruction("ldr x10, [x11, #8]"); // load the caller ref-cell address from the marker payload + ctx.emitter.instruction("ldr x12, [x2, #8]"); // load the replacement Mixed low payload word + ctx.emitter.instruction("str x12, [x10]"); // write the replacement low word through the caller ref-cell + ctx.emitter.instruction("ldr x12, [x2, #16]"); // load the replacement Mixed high payload word + ctx.emitter.instruction("str x12, [x10, #8]"); // write the replacement high word through the caller ref-cell + ctx.emitter.instruction("str x0, [sp, #-16]!"); // preserve the array result while freeing only the Mixed wrapper + ctx.emitter.instruction("mov x0, x2"); // pass the consumed fresh Mixed wrapper to heap_free + abi::emit_call_label(ctx.emitter, "__rt_heap_free"); + ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the array pointer as the ArraySet result + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the runtime setter after marker write-through + + ctx.emitter.label(&runtime_label); + abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); + ctx.emitter.label(&done_label); +} + +/// Stores a fresh boxed-Mixed value through an invoker ref-cell marker on x86_64. +fn emit_mixed_array_set_ref_marker_writeback_x86_64(ctx: &mut FunctionContext<'_>) { + let runtime_label = ctx.next_label("mixed_array_set_runtime"); + let done_label = ctx.next_label("mixed_array_set_done"); + + ctx.emitter.instruction("cmp rsi, 0"); // reject negative indexes before checking for by-reference markers + ctx.emitter.instruction(&format!("jl {}", runtime_label)); // let the runtime setter drop ignored negative-index writes + ctx.emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the current logical length of the indexed array + ctx.emitter.instruction("cmp rsi, r9"); // only existing slots can hold by-reference marker cells + ctx.emitter.instruction(&format!("jae {}", runtime_label)); // delegate appends and gap writes to the runtime setter + ctx.emitter.instruction("mov r10, QWORD PTR [rdi + 24 + rsi * 8]"); // load the existing boxed Mixed slot + ctx.emitter.instruction("test r10, r10"); // check whether the existing slot is a null gap + ctx.emitter.instruction(&format!("jz {}", runtime_label)); // null gap slots are ordinary array writes + ctx.emitter.instruction("mov r11, QWORD PTR [r10]"); // load the existing Mixed tag for marker detection + ctx.emitter.instruction(&format!("cmp r11, {}", INVOKER_ARG_REF_CELL_TAG)); // check whether the slot aliases caller storage + ctx.emitter.instruction(&format!("jne {}", runtime_label)); // ordinary boxed Mixed slots are replaced by the runtime setter + ctx.emitter.instruction("mov r10, QWORD PTR [r10 + 8]"); // load the caller ref-cell address from the marker payload + ctx.emitter.instruction("mov r11, QWORD PTR [rdx + 8]"); // load the replacement Mixed low payload word + ctx.emitter.instruction("mov QWORD PTR [r10], r11"); // write the replacement low word through the caller ref-cell + ctx.emitter.instruction("mov r11, QWORD PTR [rdx + 16]"); // load the replacement Mixed high payload word + ctx.emitter.instruction("mov QWORD PTR [r10 + 8], r11"); // write the replacement high word through the caller ref-cell + abi::emit_push_reg(ctx.emitter, "rdi"); + ctx.emitter.instruction("mov rax, rdx"); // pass the consumed fresh Mixed wrapper to heap_free + abi::emit_call_label(ctx.emitter, "__rt_heap_free"); + abi::emit_pop_reg(ctx.emitter, "rax"); + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the runtime setter after marker write-through + + ctx.emitter.label(&runtime_label); + abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); + ctx.emitter.label(&done_label); +} + /// Boxes a value for a Mixed array, consuming owned producers when possible. fn box_value_for_mixed_container( ctx: &mut FunctionContext<'_>, diff --git a/src/conditional/exprs.rs b/src/conditional/exprs.rs index 6de1e15c9c..5753c61057 100644 --- a/src/conditional/exprs.rs +++ b/src/conditional/exprs.rs @@ -122,6 +122,7 @@ pub(super) fn rewrite_expr(expr: Expr, defines: &HashSet) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -138,6 +139,7 @@ pub(super) fn rewrite_expr(expr: Expr, defines: &HashSet) -> Expr { }) .collect(), variadic, + variadic_by_ref, variadic_type, return_type, body: apply_stmts(body, defines), diff --git a/src/conditional/stmts.rs b/src/conditional/stmts.rs index be1a2528e7..e29cb4c3fb 100644 --- a/src/conditional/stmts.rs +++ b/src/conditional/stmts.rs @@ -237,6 +237,7 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -251,6 +252,7 @@ fn rewrite_stmt_kind(kind: StmtKind, defines: &HashSet) -> StmtKind { .collect(), param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: apply_stmts(body, defines), diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index fe2e00792f..b49c428882 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -100,6 +100,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -110,6 +111,7 @@ pub(crate) fn lower_expr(ctx: &mut LoweringContext<'_, '_>, expr: &Expr) -> Lowe ctx, params, variadic.as_deref(), + *variadic_by_ref, return_type.as_ref(), body, captures, @@ -4744,6 +4746,7 @@ fn lower_value_sort_comparator_closure( let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -4758,6 +4761,7 @@ fn lower_value_sort_comparator_closure( ctx, params, variadic.as_deref(), + *variadic_by_ref, return_type.as_ref(), body, captures, @@ -4914,6 +4918,7 @@ fn lower_preg_replace_callback_closure( let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -4928,6 +4933,7 @@ fn lower_preg_replace_callback_closure( ctx, params, variadic.as_deref(), + *variadic_by_ref, return_type.as_ref(), body, captures, @@ -6172,13 +6178,18 @@ fn lower_named_variadic_tail_array( Some(span), ); let elem_ty = indexed_array_literal_element_type(&array_ty); + let by_ref_variadic = variadic_param_is_by_ref(sig); for source in tail { if source.param_idx().is_some() { continue; } - let value = source_values[source.source_index()]; - let value = lowered_value_from_id(ctx, value); - let value = coerce_variadic_tail_value(ctx, value, &array_ty, source.expr().span); + let value = lower_variadic_tail_source_value( + ctx, + source.expr(), + by_ref_variadic, + Some(source_values[source.source_index()]), + &array_ty, + ); ctx.emit_void( Op::ArrayPush, vec![array.value, value.value], @@ -6222,6 +6233,7 @@ fn lower_named_variadic_tail_hash( Some(span), ); let mut next_positional_key = 0usize; + let by_ref_variadic = variadic_param_is_by_ref(sig); for source in tail { if source.param_idx().is_some() { continue; @@ -6233,10 +6245,13 @@ fn lower_named_variadic_tail_hash( next_positional_key += 1; key }; - let value = source_values[source.source_index()]; - let value = lowered_value_from_id(ctx, value); - let array_ty = PhpType::Array(Box::new(value_ty.clone())); - let value = coerce_variadic_tail_value(ctx, value, &array_ty, source.expr().span); + let value = lower_variadic_tail_source_value( + ctx, + source.expr(), + by_ref_variadic, + Some(source_values[source.source_index()]), + &PhpType::Array(Box::new(value_ty.clone())), + ); ctx.emit_void( Op::HashSet, vec![hash.value, key.value, value.value], @@ -6279,9 +6294,9 @@ fn lower_variadic_tail_array( Some(span), ); let elem_ty = indexed_array_literal_element_type(&array_ty); + let by_ref_variadic = variadic_param_is_by_ref(sig); for item in tail { - let value = lower_expr(ctx, item); - let value = coerce_variadic_tail_value(ctx, value, &array_ty, item.span); + let value = lower_variadic_tail_source_value(ctx, item, by_ref_variadic, None, &array_ty); ctx.emit_void( Op::ArrayPush, vec![array.value, value.value], @@ -6294,8 +6309,43 @@ fn lower_variadic_tail_array( array } +/// Lowers one value stored into a variadic tail container. +fn lower_variadic_tail_source_value( + ctx: &mut LoweringContext<'_, '_>, + expr: &Expr, + by_ref_variadic: bool, + prelowered: Option, + array_ty: &PhpType, +) -> LoweredValue { + if by_ref_variadic { + if let ExprKind::Variable(name) = &expr.kind { + return lower_invoker_ref_arg_marker(ctx, name, expr.span); + } + } + let value = prelowered + .map(|value| lowered_value_from_id(ctx, value)) + .unwrap_or_else(|| lower_expr(ctx, expr)); + coerce_variadic_tail_value(ctx, value, array_ty, expr.span) +} + +/// Returns whether the synthetic variadic parameter slot is by-reference. +fn variadic_param_is_by_ref(sig: &FunctionSig) -> bool { + let Some(variadic_name) = sig.variadic.as_ref() else { + return false; + }; + sig.params + .iter() + .position(|(name, _)| name == variadic_name) + .and_then(|index| sig.ref_params.get(index)) + .copied() + .unwrap_or(false) +} + /// Returns the element type expected inside a variadic tail container. fn variadic_tail_value_type(sig: &FunctionSig) -> PhpType { + if variadic_param_is_by_ref(sig) { + return PhpType::Mixed; + } let Some(variadic_name) = sig.variadic.as_ref() else { return PhpType::Mixed; }; @@ -6311,6 +6361,9 @@ fn variadic_tail_value_type(sig: &FunctionSig) -> PhpType { /// Returns the runtime array type used for a variadic parameter slot. fn variadic_array_type(sig: &FunctionSig) -> PhpType { + if variadic_param_is_by_ref(sig) { + return PhpType::Array(Box::new(PhpType::Mixed)); + } let Some(variadic_name) = sig.variadic.as_ref() else { return PhpType::Array(Box::new(PhpType::Mixed)); }; @@ -8287,6 +8340,7 @@ fn lower_closure( ctx: &mut LoweringContext<'_, '_>, params: &[(String, Option, Option, bool)], variadic: Option<&str>, + variadic_by_ref: bool, return_type: Option<&TypeExpr>, body: &[crate::parser::ast::Stmt], captures: &[String], @@ -8298,6 +8352,7 @@ fn lower_closure( ctx, params, variadic, + variadic_by_ref, return_type, body, captures, @@ -8318,6 +8373,7 @@ pub(crate) fn lower_closure_for_assignment( let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -8335,6 +8391,7 @@ pub(crate) fn lower_closure_for_assignment( ctx, params, variadic.as_deref(), + *variadic_by_ref, return_type.as_ref(), body, captures, @@ -8351,6 +8408,7 @@ fn lower_closure_with_context( ctx: &mut LoweringContext<'_, '_>, params: &[(String, Option, Option, bool)], variadic: Option<&str>, + variadic_by_ref: bool, return_type: Option<&TypeExpr>, body: &[crate::parser::ast::Stmt], captures: &[String], @@ -8426,6 +8484,7 @@ fn lower_closure_with_context( &name, params, variadic, + variadic_by_ref, return_type, body, &capture_params, @@ -8438,6 +8497,7 @@ fn lower_closure_with_context( &name, params, variadic, + variadic_by_ref, return_type, body, &capture_params, diff --git a/src/ir_lower/fibers.rs b/src/ir_lower/fibers.rs index 7d27d28e32..428715e708 100644 --- a/src/ir_lower/fibers.rs +++ b/src/ir_lower/fibers.rs @@ -159,8 +159,15 @@ fn direct_new_fiber_callback_sig(expr: &Expr) -> Option { let callback = args.first()?; match &callback.kind { ExprKind::Closure { - params, variadic, .. - } => Some(callback_sig_from_closure_params(params, variadic.as_deref())), + params, + variadic, + variadic_by_ref, + .. + } => Some(callback_sig_from_closure_params( + params, + variadic.as_deref(), + *variadic_by_ref, + )), _ => None, } } @@ -172,8 +179,15 @@ fn callback_sig_for_expr( ) -> Option { match &callback.kind { ExprKind::Closure { - params, variadic, .. - } => Some(callback_sig_from_closure_params(params, variadic.as_deref())), + params, + variadic, + variadic_by_ref, + .. + } => Some(callback_sig_from_closure_params( + params, + variadic.as_deref(), + *variadic_by_ref, + )), ExprKind::Variable(name) => ctx .callable_param_signature(name) .cloned() @@ -296,6 +310,7 @@ fn class_method_sig( fn callback_sig_from_closure_params( params: &[(String, Option, Option, bool)], variadic: Option<&str>, + variadic_by_ref: bool, ) -> FunctionSig { let mut sig = FunctionSig { params: params @@ -332,7 +347,7 @@ fn callback_sig_from_closure_params( .push((variadic_name.to_string(), PhpType::Array(Box::new(PhpType::Mixed)))); sig.param_type_exprs.push(None); sig.defaults.push(None); - sig.ref_params.push(false); + sig.ref_params.push(variadic_by_ref); sig.declared_params.push(false); } } diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index b04a1f9cdb..55a6f10970 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -348,13 +348,16 @@ pub(crate) fn method_signature_from_ast(method: &ClassMethod) -> FunctionSig { &method.params, method.return_type.as_ref(), method.variadic.as_deref(), + method.variadic_by_ref, ); - if let Some(variadic_type) = &method.variadic_type { - if let Some((_, php_type)) = signature.params.last_mut() { - *php_type = type_expr_to_php_type(variadic_type); - } - if let Some(declared) = signature.declared_params.last_mut() { - *declared = true; + if !method.variadic_by_ref { + if let Some(variadic_type) = &method.variadic_type { + if let Some((_, php_type)) = signature.params.last_mut() { + *php_type = type_expr_to_php_type(variadic_type); + } + if let Some(declared) = signature.declared_params.last_mut() { + *declared = true; + } } } signature @@ -469,13 +472,22 @@ pub(crate) fn lower_closure_function( name: &str, params: &AstParams, variadic: Option<&str>, + variadic_by_ref: bool, return_type: Option<&TypeExpr>, body: &[Stmt], captures: &[(String, PhpType, bool)], self_ref_callable_capture: Option<&str>, by_ref_return: bool, ) -> FunctionSig { - let mut signature = closure_signature_from_ast(params, variadic, return_type, body, captures, parent.classes); + let mut signature = closure_signature_from_ast( + params, + variadic, + variadic_by_ref, + return_type, + body, + captures, + parent.classes, + ); signature.by_ref_return = by_ref_return; lower_closure_function_with_signature( parent, @@ -493,6 +505,7 @@ pub(crate) fn lower_closure_function_with_context( name: &str, params: &AstParams, variadic: Option<&str>, + variadic_by_ref: bool, return_type: Option<&TypeExpr>, body: &[Stmt], captures: &[(String, PhpType, bool)], @@ -500,7 +513,15 @@ pub(crate) fn lower_closure_function_with_context( self_ref_callable_capture: Option<&str>, by_ref_return: bool, ) -> FunctionSig { - let mut signature = closure_signature_from_ast(params, variadic, return_type, body, captures, parent.classes); + let mut signature = closure_signature_from_ast( + params, + variadic, + variadic_by_ref, + return_type, + body, + captures, + parent.classes, + ); signature.by_ref_return = by_ref_return; for (idx, (_, type_ann, _, _)) in params.iter().enumerate() { if type_ann.is_none() { @@ -987,19 +1008,21 @@ fn params_with_closure_captures( /// Builds a fallback function signature from AST syntax when checker metadata is unavailable. fn signature_from_ast(params: &AstParams, return_type: Option<&TypeExpr>) -> FunctionSig { - signature_from_ast_with_variadic(params, return_type, None) + signature_from_ast_with_variadic(params, return_type, None, false) } /// Builds an EIR closure signature and infers fallthrough-only closures as `void`. fn closure_signature_from_ast( params: &AstParams, variadic: Option<&str>, + variadic_by_ref: bool, return_type: Option<&TypeExpr>, body: &[Stmt], captures: &[(String, PhpType, bool)], classes: &std::collections::HashMap, ) -> FunctionSig { - let mut signature = signature_from_ast_with_variadic(params, return_type, variadic); + let mut signature = + signature_from_ast_with_variadic(params, return_type, variadic, variadic_by_ref); if crate::types::checker::yield_validation::body_contains_yield(body) { signature.return_type = PhpType::Object("Generator".to_string()); return signature; @@ -1170,6 +1193,7 @@ fn signature_from_ast_with_variadic( params: &AstParams, return_type: Option<&TypeExpr>, variadic: Option<&str>, + variadic_by_ref: bool, ) -> FunctionSig { let mut signature = FunctionSig { params: params @@ -1197,12 +1221,12 @@ fn signature_from_ast_with_variadic( variadic: variadic.map(str::to_string), deprecation: None, }; - append_variadic_param_slot(&mut signature); + append_variadic_param_slot(&mut signature, variadic_by_ref); signature } /// Adds the variadic `array` parameter slot omitted from parsed parameter tuples. -fn append_variadic_param_slot(signature: &mut FunctionSig) { +fn append_variadic_param_slot(signature: &mut FunctionSig, variadic_by_ref: bool) { let Some(variadic) = signature.variadic.clone() else { return; }; @@ -1214,7 +1238,7 @@ fn append_variadic_param_slot(signature: &mut FunctionSig) { .push((variadic, PhpType::Array(Box::new(PhpType::Mixed)))); signature.param_type_exprs.push(None); signature.defaults.push(None); - signature.ref_params.push(false); + signature.ref_params.push(variadic_by_ref); signature.declared_params.push(false); } diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index a810f73fea..624eac592b 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -912,6 +912,7 @@ fn lower_function_declarations( name, params, variadic: _, + variadic_by_ref: _, variadic_type: _, return_type, body, diff --git a/src/ir_lower/tests/exhaustive.rs b/src/ir_lower/tests/exhaustive.rs index 5bd4bd427f..1ed84995a3 100644 --- a/src/ir_lower/tests/exhaustive.rs +++ b/src/ir_lower/tests/exhaustive.rs @@ -295,6 +295,7 @@ fn lowers_every_expr_variant_smoke() { expr(ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -444,6 +445,7 @@ fn lowers_every_stmt_variant_smoke() { params: vec![("x".to_string(), Some(TypeExpr::Int), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), by_ref_return: false, @@ -501,6 +503,7 @@ fn class_method(name: &str, is_static: bool) -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), by_ref_return: false, diff --git a/src/magic_constants/walker/exprs.rs b/src/magic_constants/walker/exprs.rs index f4717af9e5..c474800e19 100644 --- a/src/magic_constants/walker/exprs.rs +++ b/src/magic_constants/walker/exprs.rs @@ -130,6 +130,7 @@ pub(super) fn walk_expr(expr: Expr, pass: &mut P) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -151,6 +152,7 @@ pub(super) fn walk_expr(expr: Expr, pass: &mut P) -> Expr { ExprKind::Closure { params: new_params, variadic, + variadic_by_ref, variadic_type, return_type, body: new_body, diff --git a/src/magic_constants/walker/stmts.rs b/src/magic_constants/walker/stmts.rs index 06c72a5edf..2b7cafa755 100644 --- a/src/magic_constants/walker/stmts.rs +++ b/src/magic_constants/walker/stmts.rs @@ -243,6 +243,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -262,6 +263,7 @@ pub(super) fn walk_stmt(stmt: Stmt, pass: &mut P) -> Stmt { params: new_params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: new_body, diff --git a/src/name_resolver/declarations.rs b/src/name_resolver/declarations.rs index c2a5563d8e..4791332f54 100644 --- a/src/name_resolver/declarations.rs +++ b/src/name_resolver/declarations.rs @@ -42,6 +42,7 @@ pub(super) fn resolve_decl_stmt( params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -57,6 +58,7 @@ pub(super) fn resolve_decl_stmt( .map(|groups| resolve_attribute_groups(groups, namespace, imports, symbols)) .collect(), variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type .as_ref() diff --git a/src/name_resolver/expressions.rs b/src/name_resolver/expressions.rs index ef137555be..4f3f0ecd82 100644 --- a/src/name_resolver/expressions.rs +++ b/src/name_resolver/expressions.rs @@ -165,6 +165,7 @@ pub(super) fn resolve_expr( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -176,6 +177,7 @@ pub(super) fn resolve_expr( } => ExprKind::Closure { params: resolve_params(params, current_namespace, imports, symbols), variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type .as_ref() diff --git a/src/optimize/control/dce.rs b/src/optimize/control/dce.rs index cc0b6b6815..c5682fbff1 100644 --- a/src/optimize/control/dce.rs +++ b/src/optimize/control/dce.rs @@ -498,6 +498,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -508,6 +509,7 @@ fn dce_stmt_with_guards(stmt: Stmt, guards: &GuardState) -> Vec { params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: dce_block_with_guards(body, GuardState::default()), diff --git a/src/optimize/control/fold.rs b/src/optimize/control/fold.rs index 69328c2875..53f6d50330 100644 --- a/src/optimize/control/fold.rs +++ b/src/optimize/control/fold.rs @@ -175,6 +175,7 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -184,6 +185,7 @@ pub(crate) fn fold_stmt(stmt: Stmt) -> Stmt { params: fold_params(params), param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: fold_block(body), diff --git a/src/optimize/control/prune/expr.rs b/src/optimize/control/prune/expr.rs index 1a04955c0e..e93cd07a7c 100644 --- a/src/optimize/control/prune/expr.rs +++ b/src/optimize/control/prune/expr.rs @@ -124,6 +124,7 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -135,6 +136,7 @@ pub(crate) fn prune_expr(expr: Expr) -> Expr { } => ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body: prune_block(body), diff --git a/src/optimize/control/prune/statements.rs b/src/optimize/control/prune/statements.rs index ebaac31e7e..3ed1892269 100644 --- a/src/optimize/control/prune/statements.rs +++ b/src/optimize/control/prune/statements.rs @@ -246,6 +246,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -256,6 +257,7 @@ pub(crate) fn prune_stmt(stmt: Stmt) -> Vec { params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: prune_block(body), diff --git a/src/optimize/fold/expr.rs b/src/optimize/fold/expr.rs index 789a4c5a57..72d7704326 100644 --- a/src/optimize/fold/expr.rs +++ b/src/optimize/fold/expr.rs @@ -69,6 +69,7 @@ pub(in crate::optimize) fn fold_method(method: ClassMethod) -> ClassMethod { params: fold_params(method.params), param_attributes: method.param_attributes, variadic: method.variadic, + variadic_by_ref: method.variadic_by_ref, variadic_type: method.variadic_type, return_type: method.return_type, by_ref_return: method.by_ref_return, @@ -254,6 +255,7 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -265,6 +267,7 @@ pub(in crate::optimize) fn fold_expr(expr: Expr) -> Expr { } => ExprKind::Closure { params: fold_params(params), variadic, + variadic_by_ref, variadic_type, return_type, body: fold_block(body), diff --git a/src/optimize/propagate/expr.rs b/src/optimize/propagate/expr.rs index d47ad49efc..f2efa431a7 100644 --- a/src/optimize/propagate/expr.rs +++ b/src/optimize/propagate/expr.rs @@ -197,6 +197,7 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -214,6 +215,7 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { ExprKind::Closure { params: propagate_params(params), variadic, + variadic_by_ref, variadic_type, return_type, body: super::stmt::with_function_scope(|| { diff --git a/src/optimize/propagate/stmt.rs b/src/optimize/propagate/stmt.rs index 0b9d3a4cdc..614c286e8e 100644 --- a/src/optimize/propagate/stmt.rs +++ b/src/optimize/propagate/stmt.rs @@ -289,6 +289,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -300,6 +301,7 @@ pub(crate) fn propagate_stmt(stmt: Stmt, env: ConstantEnv) -> (Stmt, ConstantEnv params: propagate_params(params), param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body: with_function_scope(|| propagate_block(body, HashMap::new()).0), diff --git a/src/optimize/tests/dce/basics.rs b/src/optimize/tests/dce/basics.rs index 3ccb51bb75..c08f53d3e1 100644 --- a/src/optimize/tests/dce/basics.rs +++ b/src/optimize/tests/dce/basics.rs @@ -30,6 +30,7 @@ fn test_eliminate_dead_code_invalidates_outer_strict_bool_guard_after_local_writ params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs b/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs index 34a93eb9e0..595a10d302 100644 --- a/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs +++ b/src/optimize/tests/dce/guards/composite_guards/composite_regions.rs @@ -28,6 +28,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_demorgan_equivalent_gua params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -77,6 +78,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_loose_comparison_guard( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -125,6 +127,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_relational_guard() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -173,6 +176,7 @@ fn test_eliminate_dead_code_prunes_nested_elseif_from_composite_guard_refinement params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -237,6 +241,7 @@ fn test_eliminate_dead_code_prunes_nested_subexpr_from_composite_guard_refinemen params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs b/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs index dec1321d18..a264d83a5a 100644 --- a/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs +++ b/src/optimize/tests/dce/guards/composite_guards/elseif_suffixes.rs @@ -43,6 +43,7 @@ fn test_eliminate_dead_code_rebuilds_empty_elseif_tail_as_needed_guard() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -102,6 +103,7 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_cumulative_fal params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -169,6 +171,7 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_negated_compos params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -244,6 +247,7 @@ fn test_eliminate_dead_code_prunes_unreachable_elseif_suffix_from_demorgan_equiv params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/excluded_guards.rs b/src/optimize/tests/dce/guards/excluded_guards.rs index 893a80c021..fa1fa1b5c7 100644 --- a/src/optimize/tests/dce/guards/excluded_guards.rs +++ b/src/optimize/tests/dce/guards/excluded_guards.rs @@ -21,6 +21,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_zero_guard() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -71,6 +72,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_null_guard() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -126,6 +128,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_empty_string_g params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -184,6 +187,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_string_zero_gu params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -242,6 +246,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_excluded_float_guard() params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs b/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs index 3ed509e693..5d598ae4f7 100644 --- a/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs +++ b/src/optimize/tests/dce/guards/outer_guards/boolean_guards.rs @@ -37,6 +37,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_strict_bool_guard params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -89,6 +90,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_and_guard() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -138,6 +140,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_negated_and_guard params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -195,6 +198,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_or_false_branch() params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs b/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs index f487944507..f65bd4ae41 100644 --- a/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs +++ b/src/optimize/tests/dce/guards/outer_guards/scalar_guards.rs @@ -22,6 +22,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_guard() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -72,6 +73,7 @@ fn test_eliminate_dead_code_invalidates_outer_guard_after_local_write() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -124,6 +126,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_null_guard() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -180,6 +183,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_zero_guard() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -228,6 +232,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_empty_string_guar params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -280,6 +285,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_string_zero_guard params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -332,6 +338,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_outer_zero_float_guard( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/basics.rs b/src/optimize/tests/dce/switches/basics.rs index 95291edbd4..291f2af81e 100644 --- a/src/optimize/tests/dce/switches/basics.rs +++ b/src/optimize/tests/dce/switches/basics.rs @@ -33,6 +33,7 @@ fn test_eliminate_dead_code_drops_empty_switch_shell_created_by_branch_dce() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/case_shadowing.rs b/src/optimize/tests/dce/switches/case_shadowing.rs index 3b0709b80c..c4010ddcf7 100644 --- a/src/optimize/tests/dce/switches/case_shadowing.rs +++ b/src/optimize/tests/dce/switches/case_shadowing.rs @@ -19,6 +19,7 @@ fn test_eliminate_dead_code_drops_switch_case_shadowed_by_terminating_duplicate_ params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -78,6 +79,7 @@ fn test_eliminate_dead_code_merges_fallthrough_body_from_fully_shadowed_switch_c params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -132,6 +134,7 @@ fn test_eliminate_dead_code_prunes_dead_label_inside_live_mixed_switch_case() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/exhaustive_suffixes.rs b/src/optimize/tests/dce/switches/exhaustive_suffixes.rs index 4f551f959b..fe73f489b7 100644 --- a/src/optimize/tests/dce/switches/exhaustive_suffixes.rs +++ b/src/optimize/tests/dce/switches/exhaustive_suffixes.rs @@ -27,6 +27,7 @@ fn test_eliminate_dead_code_prunes_negated_strict_switch_true_case() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -83,6 +84,7 @@ fn test_eliminate_dead_code_prunes_exhaustive_negated_and_switch_true_default() params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -130,6 +132,7 @@ fn test_eliminate_dead_code_prunes_exhaustive_negated_or_switch_true_default() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -179,6 +182,7 @@ fn test_eliminate_dead_code_prunes_switch_true_suffix_after_exhaustive_multi_pat params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -227,6 +231,7 @@ fn test_eliminate_dead_code_prunes_scalar_switch_suffix_after_exhaustive_multi_p params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs b/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs index 6910504dd3..f8aa6bd98f 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/cumulative.rs @@ -25,6 +25,7 @@ fn test_eliminate_dead_code_prunes_exhaustive_switch_true_default_from_cumulativ params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -93,6 +94,7 @@ fn test_eliminate_dead_code_uses_cumulative_switch_true_guards_inside_case_body( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/guarded_cases/impossible.rs b/src/optimize/tests/dce/switches/guarded_cases/impossible.rs index b1106efd10..25a9e5f38f 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/impossible.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/impossible.rs @@ -35,6 +35,7 @@ fn test_eliminate_dead_code_prunes_nested_if_region_from_switch_bool_guard_case( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -88,6 +89,7 @@ fn test_eliminate_dead_code_drops_impossible_switch_cases_from_outer_exact_guard params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -140,6 +142,7 @@ fn test_eliminate_dead_code_drops_impossible_switch_cases_from_outer_excluded_gu params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -215,6 +218,7 @@ fn test_eliminate_dead_code_drops_impossible_switch_true_cases_from_outer_guard( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -273,6 +277,7 @@ fn test_eliminate_dead_code_invalidates_switch_bool_guard_after_local_write() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs b/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs index e424d0075a..01c87014c9 100644 --- a/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs +++ b/src/optimize/tests/dce/switches/guarded_cases/truthiness.rs @@ -19,6 +19,7 @@ fn test_eliminate_dead_code_prunes_truthy_switch_cases_and_default() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -85,6 +86,7 @@ fn test_eliminate_dead_code_prunes_falsy_scalar_labels_from_truthy_switch_subjec params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -149,6 +151,7 @@ fn test_eliminate_dead_code_combines_exclusion_and_truthy_switch_guards() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/switches/tail_paths.rs b/src/optimize/tests/dce/switches/tail_paths.rs index 34e19c12f8..cbf71eab77 100644 --- a/src/optimize/tests/dce/switches/tail_paths.rs +++ b/src/optimize/tests/dce/switches/tail_paths.rs @@ -33,6 +33,7 @@ fn test_eliminate_dead_code_drops_trailing_empty_switch_cases() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -93,6 +94,7 @@ fn test_eliminate_dead_code_sinks_tail_into_switch_exit_paths() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -144,6 +146,7 @@ fn test_eliminate_dead_code_sinks_tail_into_switch_break_paths() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tail_sinking.rs b/src/optimize/tests/dce/tail_sinking.rs index be86f665b6..3159a3b1f7 100644 --- a/src/optimize/tests/dce/tail_sinking.rs +++ b/src/optimize/tests/dce/tail_sinking.rs @@ -44,6 +44,7 @@ fn test_eliminate_dead_code_reduces_empty_if_chain_to_needed_condition_checks() params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -93,6 +94,7 @@ fn test_eliminate_dead_code_sinks_tail_into_if_fallthrough_branch() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -148,6 +150,7 @@ fn test_eliminate_dead_code_sinks_tail_into_implicit_else_path() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -204,6 +207,7 @@ fn test_eliminate_dead_code_sinks_tail_into_ifdef_fallthrough_paths() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -271,6 +275,7 @@ fn test_eliminate_dead_code_reduces_empty_if_to_effectful_condition_eval() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tries/catch_pruning.rs b/src/optimize/tests/dce/tries/catch_pruning.rs index 8695303a63..f2741af94e 100644 --- a/src/optimize/tests/dce/tries/catch_pruning.rs +++ b/src/optimize/tests/dce/tries/catch_pruning.rs @@ -22,6 +22,7 @@ fn test_eliminate_dead_code_drops_unreachable_catches_after_non_throwing_try() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -62,6 +63,7 @@ fn test_eliminate_dead_code_drops_unreachable_catches_before_finally() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -102,6 +104,7 @@ fn test_eliminate_dead_code_drops_catches_shadowed_by_throwable() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -158,6 +161,7 @@ fn test_eliminate_dead_code_drops_duplicate_shadowed_catch_types() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -216,6 +220,7 @@ fn test_eliminate_dead_code_merges_identical_catches_exposed_by_shadow_drop() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tries/finally_paths.rs b/src/optimize/tests/dce/tries/finally_paths.rs index f6ed69217b..63e6fd515d 100644 --- a/src/optimize/tests/dce/tries/finally_paths.rs +++ b/src/optimize/tests/dce/tries/finally_paths.rs @@ -24,6 +24,7 @@ fn test_eliminate_dead_code_drops_statements_after_try_finally_exit() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -73,6 +74,7 @@ fn test_eliminate_dead_code_preserves_outer_guard_for_finally_when_only_other_lo params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -133,6 +135,7 @@ fn test_eliminate_dead_code_sinks_tail_into_safe_finally_path() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tries/tail_paths.rs b/src/optimize/tests/dce/tries/tail_paths.rs index d3138b541c..72ae706335 100644 --- a/src/optimize/tests/dce/tries/tail_paths.rs +++ b/src/optimize/tests/dce/tries/tail_paths.rs @@ -21,6 +21,7 @@ fn test_eliminate_dead_code_keeps_statements_after_fallthrough_try() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -74,6 +75,7 @@ fn test_eliminate_dead_code_sinks_tail_into_try_fallthrough_paths() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/dce/tries/try_pruning.rs b/src/optimize/tests/dce/tries/try_pruning.rs index 97d0be27c7..f7c2a0e016 100644 --- a/src/optimize/tests/dce/tries/try_pruning.rs +++ b/src/optimize/tests/dce/tries/try_pruning.rs @@ -20,6 +20,7 @@ fn test_eliminate_dead_code_drops_statements_after_exhaustive_try_catch() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -85,6 +86,7 @@ fn test_eliminate_dead_code_drops_empty_try_shell_created_by_branch_dce() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -123,6 +125,7 @@ fn test_eliminate_dead_code_keeps_unknown_truthy_switch_entry_before_matching_ca params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -190,6 +193,7 @@ fn test_eliminate_dead_code_invalidates_outer_guard_before_catch_body() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -273,6 +277,7 @@ fn test_eliminate_dead_code_invalidates_outer_guard_before_catch_body_from_switc params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -361,6 +366,7 @@ fn test_eliminate_dead_code_ignores_unreachable_switch_throw_path_writes_before_ params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -453,6 +459,7 @@ fn test_eliminate_dead_code_preserves_outer_guard_for_catch_when_only_non_throw_ params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/basic_calls.rs b/src/optimize/tests/effects/basic_calls.rs index a49cb4c91a..aae2cfe4aa 100644 --- a/src/optimize/tests/effects/basic_calls.rs +++ b/src/optimize/tests/effects/basic_calls.rs @@ -80,6 +80,7 @@ fn test_program_function_effects_recognize_pure_user_functions() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -114,6 +115,7 @@ fn test_program_function_effects_propagate_throwing_calls() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -136,6 +138,7 @@ fn test_program_function_effects_propagate_throwing_calls() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/callable_aliases/path_merges.rs b/src/optimize/tests/effects/callable_aliases/path_merges.rs index 30c1fbb8f9..73e60341cf 100644 --- a/src/optimize/tests/effects/callable_aliases/path_merges.rs +++ b/src/optimize/tests/effects/callable_aliases/path_merges.rs @@ -21,6 +21,7 @@ fn test_effect_analysis_tracks_pure_iife_expr_calls() { ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -83,6 +84,7 @@ fn test_program_function_effects_merge_callable_aliases_across_if_paths() { params: vec![("flag".to_string(), None, None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -152,6 +154,7 @@ fn test_program_function_effects_merge_callable_aliases_across_try_paths() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -227,6 +230,7 @@ fn test_program_function_effects_merge_callable_aliases_across_switch_paths() { params: vec![("flag".to_string(), None, None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/callable_aliases/tracking.rs b/src/optimize/tests/effects/callable_aliases/tracking.rs index 944f0e0272..066115beb5 100644 --- a/src/optimize/tests/effects/callable_aliases/tracking.rs +++ b/src/optimize/tests/effects/callable_aliases/tracking.rs @@ -21,6 +21,7 @@ fn test_program_function_effects_track_closure_alias_locals() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -32,6 +33,7 @@ fn test_program_function_effects_track_closure_alias_locals() { ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -89,6 +91,7 @@ fn test_program_function_effects_track_callable_alias_through_ternary() { params: vec![("flag".to_string(), None, None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -151,6 +154,7 @@ fn test_program_function_effects_track_callable_alias_through_match() { params: vec![("flag".to_string(), None, None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -216,6 +220,7 @@ fn test_program_function_effects_track_callable_alias_through_null_coalesce() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -277,6 +282,7 @@ fn test_program_function_effects_track_callable_alias_locals() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/effects/methods.rs b/src/optimize/tests/effects/methods.rs index a1abf389a0..7f74f70daa 100644 --- a/src/optimize/tests/effects/methods.rs +++ b/src/optimize/tests/effects/methods.rs @@ -34,6 +34,7 @@ fn test_program_static_method_effects_recognize_pure_static_methods() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -88,6 +89,7 @@ fn test_program_static_method_effects_resolve_self_receiver() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -114,6 +116,7 @@ fn test_program_static_method_effects_resolve_self_receiver() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -171,6 +174,7 @@ fn test_program_static_method_effects_resolve_parent_receiver() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -211,6 +215,7 @@ fn test_program_static_method_effects_resolve_parent_receiver() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -266,6 +271,7 @@ fn test_program_private_instance_method_effects_recognize_private_methods() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/prune.rs b/src/optimize/tests/prune.rs index 4b49c76858..8b1134b106 100644 --- a/src/optimize/tests/prune.rs +++ b/src/optimize/tests/prune.rs @@ -129,6 +129,7 @@ fn test_prune_block_drops_statements_after_return() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -159,6 +160,7 @@ fn test_prune_drops_pure_expr_stmt() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -234,6 +236,7 @@ fn test_prune_block_drops_statements_after_exhaustive_if() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -280,6 +283,7 @@ fn test_prune_block_drops_statements_after_exhaustive_switch() { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/parser/ast/expr.rs b/src/parser/ast/expr.rs index 8812d05682..127c5e5449 100644 --- a/src/parser/ast/expr.rs +++ b/src/parser/ast/expr.rs @@ -99,6 +99,8 @@ pub enum ExprKind { Closure { params: Vec<(String, Option, Option, bool)>, variadic: Option, + /// Whether the variadic parameter was declared by reference (`&...$args`). + variadic_by_ref: bool, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. variadic_type: Option, return_type: Option, diff --git a/src/parser/ast/oop.rs b/src/parser/ast/oop.rs index 1f3134de7f..bc88e95fee 100644 --- a/src/parser/ast/oop.rs +++ b/src/parser/ast/oop.rs @@ -217,6 +217,8 @@ pub struct ClassMethod { /// This vector is parallel to `params`, plus one trailing entry when `variadic` is present. pub param_attributes: Vec>, pub variadic: Option, + /// Whether the variadic parameter was declared by reference (`&...$args`). + pub variadic_by_ref: bool, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. Each argument /// collected into the variadic is checked against this type. pub variadic_type: Option, diff --git a/src/parser/ast/stmt.rs b/src/parser/ast/stmt.rs index 0e8fee0209..7686ed501d 100644 --- a/src/parser/ast/stmt.rs +++ b/src/parser/ast/stmt.rs @@ -179,6 +179,8 @@ pub enum StmtKind { /// plus the variadic parameter when present. param_attributes: Vec>, variadic: Option, + /// Whether the variadic parameter was declared by reference (`&...$args`). + variadic_by_ref: bool, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. Each /// argument collected into the variadic is checked against this type. variadic_type: Option, diff --git a/src/parser/expr/prefix_complex.rs b/src/parser/expr/prefix_complex.rs index dd0eb0c799..7f32d923e6 100644 --- a/src/parser/expr/prefix_complex.rs +++ b/src/parser/expr/prefix_complex.rs @@ -163,7 +163,8 @@ pub(super) fn parse_closure( return Err(CompileError::new(span, "Unexpected token: Function")); } *pos = after_fn + 1; - let (params, variadic, variadic_type) = parse_closure_params(tokens, pos, span)?; + let (params, variadic, variadic_by_ref, variadic_type) = + parse_closure_params(tokens, pos, span)?; let mut captures = Vec::new(); let mut capture_refs = Vec::new(); if *pos < tokens.len() && tokens[*pos].0 == Token::Use { @@ -218,6 +219,7 @@ pub(super) fn parse_closure( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -247,7 +249,8 @@ pub(super) fn parse_arrow_closure( return Err(CompileError::new(span, "Expected '(' after 'fn'")); } *pos += 1; - let (params, variadic, variadic_type) = parse_closure_params(tokens, pos, span)?; + let (params, variadic, variadic_by_ref, variadic_type) = + parse_closure_params(tokens, pos, span)?; let return_type = parse_optional_closure_return_type(tokens, pos, span)?; if *pos >= tokens.len() || tokens[*pos].0 != Token::DoubleArrow { return Err(CompileError::new( @@ -263,6 +266,7 @@ pub(super) fn parse_arrow_closure( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -526,12 +530,14 @@ fn parse_closure_params( ( Vec<(String, Option, Option, bool)>, Option, + bool, Option, ), CompileError, > { let mut params = Vec::new(); let mut variadic = None; + let mut variadic_by_ref = false; let mut variadic_type = None; while *pos < tokens.len() && tokens[*pos].0 != Token::RParen { if !params.is_empty() || variadic.is_some() { @@ -573,6 +579,7 @@ fn parse_closure_params( match tokens.get(*pos).map(|(token, _)| token) { Some(Token::Variable(name)) => { variadic = Some(name.clone()); + variadic_by_ref = is_ref; variadic_type = type_ann; *pos += 1; } @@ -599,7 +606,7 @@ fn parse_closure_params( return Err(CompileError::new(span, "Expected ')' after parameters")); } *pos += 1; - Ok((params, variadic, variadic_type)) + Ok((params, variadic, variadic_by_ref, variadic_type)) } /// Parses a named expression that could be a constant reference, function call, buffer_new, ptr_cast, or static/class method access. diff --git a/src/parser/stmt/oop/body.rs b/src/parser/stmt/oop/body.rs index 8edc73770b..530cf16ed5 100644 --- a/src/parser/stmt/oop/body.rs +++ b/src/parser/stmt/oop/body.rs @@ -594,6 +594,7 @@ fn parse_class_like_method( params, param_attributes, variadic, + variadic_by_ref, variadic_type, promoted_properties, promoted_assignments, @@ -642,6 +643,7 @@ fn parse_class_like_method( params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, by_ref_return, @@ -965,6 +967,7 @@ fn parse_property_hooks( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: prop_type.cloned(), by_ref_return: get_by_ref, @@ -995,6 +998,7 @@ fn parse_property_hooks( params: vec![(set_param, prop_type.cloned(), None, false)], param_attributes: vec![Vec::new()], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), by_ref_return: false, diff --git a/src/parser/stmt/oop/method_params.rs b/src/parser/stmt/oop/method_params.rs index 058a96d512..34f07627d9 100644 --- a/src/parser/stmt/oop/method_params.rs +++ b/src/parser/stmt/oop/method_params.rs @@ -25,6 +25,7 @@ type ParsedMethodParams = ( Vec, Vec>, Option, + bool, Option, Vec, Vec, @@ -48,6 +49,7 @@ pub(super) fn parse_method_params( let mut params = Vec::new(); let mut param_attributes = Vec::new(); let mut variadic = None; + let mut variadic_by_ref = false; let mut variadic_type = None; let mut promoted_properties = Vec::new(); let mut promoted_assignments = Vec::new(); @@ -108,6 +110,7 @@ pub(super) fn parse_method_params( match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Variable(n)) => { variadic = Some(n.clone()); + variadic_by_ref = is_ref; variadic_type = type_ann; param_attributes.push(attributes); *pos += 1; @@ -167,6 +170,7 @@ pub(super) fn parse_method_params( params, param_attributes, variadic, + variadic_by_ref, variadic_type, promoted_properties, promoted_assignments, diff --git a/src/parser/stmt/params.rs b/src/parser/stmt/params.rs index 0c7f886c41..bcc32ade5c 100644 --- a/src/parser/stmt/params.rs +++ b/src/parser/stmt/params.rs @@ -45,7 +45,8 @@ pub(super) fn parse_function_decl( &Token::LParen, "Expected '(' after function name", )?; - let (params, param_attributes, variadic, variadic_type) = parse_params(tokens, pos, span)?; + let (params, param_attributes, variadic, variadic_by_ref, variadic_type) = + parse_params(tokens, pos, span)?; expect_token(tokens, pos, &Token::RParen, "Expected ')' after parameters")?; // Parse optional return type: `: TypeExpr` @@ -64,6 +65,7 @@ pub(super) fn parse_function_decl( params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, by_ref_return, @@ -349,6 +351,7 @@ pub(super) fn parse_params( Vec<(String, Option, Option, bool)>, Vec>, Option, + bool, Option, ), CompileError, @@ -356,6 +359,7 @@ pub(super) fn parse_params( let mut params = Vec::new(); let mut param_attributes = Vec::new(); let mut variadic = None; + let mut variadic_by_ref = false; let mut variadic_type = None; while *pos < tokens.len() && tokens[*pos].0 != Token::RParen { if !params.is_empty() || variadic.is_some() { @@ -397,6 +401,7 @@ pub(super) fn parse_params( match tokens.get(*pos).map(|(t, _)| t) { Some(Token::Variable(n)) => { variadic = Some(n.clone()); + variadic_by_ref = is_ref; variadic_type = type_ann; param_attributes.push(attributes); *pos += 1; @@ -421,7 +426,13 @@ pub(super) fn parse_params( _ => return Err(CompileError::new(span, "Expected parameter variable")), } } - Ok((params, param_attributes, variadic, variadic_type)) + Ok(( + params, + param_attributes, + variadic, + variadic_by_ref, + variadic_type, + )) } /// Parses a comma-separated list of `Name`s until a token that does not start a name is diff --git a/src/resolver/engine.rs b/src/resolver/engine.rs index 54f57fc39c..c3276b8491 100644 --- a/src/resolver/engine.rs +++ b/src/resolver/engine.rs @@ -403,6 +403,7 @@ pub(super) fn resolve_stmts( params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, by_ref_return, @@ -422,6 +423,7 @@ pub(super) fn resolve_stmts( params: params.clone(), param_attributes: param_attributes.clone(), variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type.clone(), by_ref_return: *by_ref_return, diff --git a/src/resolver/exprs.rs b/src/resolver/exprs.rs index dc02d2287c..204177a209 100644 --- a/src/resolver/exprs.rs +++ b/src/resolver/exprs.rs @@ -223,6 +223,7 @@ pub(super) fn resolve_expr( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -234,6 +235,7 @@ pub(super) fn resolve_expr( } => ExprKind::Closure { params: resolve_params(params, base_dir, declared_once, include_chain, state, function_variants)?, variadic, + variadic_by_ref, variadic_type, return_type, body: resolve_isolated( diff --git a/src/resolver/stmt_exprs.rs b/src/resolver/stmt_exprs.rs index a9e1fc7e53..61cfd37921 100644 --- a/src/resolver/stmt_exprs.rs +++ b/src/resolver/stmt_exprs.rs @@ -374,6 +374,7 @@ pub(super) fn resolve_stmt_exprs( params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -390,6 +391,7 @@ pub(super) fn resolve_stmt_exprs( )?, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, body, diff --git a/src/types/checker/builtin_interfaces.rs b/src/types/checker/builtin_interfaces.rs index 2ef293d2e3..ba6d4ca0a1 100644 --- a/src/types/checker/builtin_interfaces.rs +++ b/src/types/checker/builtin_interfaces.rs @@ -332,6 +332,7 @@ fn builtin_interface_method(name: &str, return_type: TypeExpr) -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, @@ -364,6 +365,7 @@ fn builtin_interface_method_with_params( params, param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, diff --git a/src/types/checker/builtin_iterators.rs b/src/types/checker/builtin_iterators.rs index 9dbc3188e2..e94e1a1189 100644 --- a/src/types/checker/builtin_iterators.rs +++ b/src/types/checker/builtin_iterators.rs @@ -95,6 +95,7 @@ fn stub_method_returning_null(name: &str) -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -122,6 +123,7 @@ fn stub_method_returning_null_with_param(name: &str, param: &str) -> ClassMethod params: vec![(param.to_string(), None, None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -147,6 +149,7 @@ fn stub_method_returning_false(name: &str) -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Bool), by_ref_return: false, @@ -175,6 +178,7 @@ fn stub_void_method(name: &str) -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), by_ref_return: false, diff --git a/src/types/checker/builtin_json.rs b/src/types/checker/builtin_json.rs index 2f52632ce5..a576504241 100644 --- a/src/types/checker/builtin_json.rs +++ b/src/types/checker/builtin_json.rs @@ -73,6 +73,7 @@ fn json_serialize_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, diff --git a/src/types/checker/builtin_spl_classes/common.rs b/src/types/checker/builtin_spl_classes/common.rs index 75fd60f84c..3ba9138b42 100644 --- a/src/types/checker/builtin_spl_classes/common.rs +++ b/src/types/checker/builtin_spl_classes/common.rs @@ -80,6 +80,7 @@ pub(super) fn class_method_with_body( params, param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type, by_ref_return: false, diff --git a/src/types/checker/builtin_spl_classes/regex.rs b/src/types/checker/builtin_spl_classes/regex.rs index 1a5b4d7af2..3121a8b868 100644 --- a/src/types/checker/builtin_spl_classes/regex.rs +++ b/src/types/checker/builtin_spl_classes/regex.rs @@ -313,6 +313,7 @@ fn regex_capture_closure_expr( expr(ExprKind::Closure { params: vec![("matches".to_string(), None, None, false)], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), body, diff --git a/src/types/checker/builtin_types/calendar.rs b/src/types/checker/builtin_types/calendar.rs index 82550bff70..1c0abe463d 100644 --- a/src/types/checker/builtin_types/calendar.rs +++ b/src/types/checker/builtin_types/calendar.rs @@ -43,6 +43,7 @@ fn cal_method( has_body: true, params, variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(ret), by_ref_return: false, diff --git a/src/types/checker/builtin_types/date_period.rs b/src/types/checker/builtin_types/date_period.rs index eb19ceb46e..b0fcde3d01 100644 --- a/src/types/checker/builtin_types/date_period.rs +++ b/src/types/checker/builtin_types/date_period.rs @@ -181,6 +181,7 @@ fn method_vis( has_body: true, params, variadic: None, + variadic_by_ref: false, variadic_type: None, return_type, by_ref_return: false, @@ -613,6 +614,7 @@ fn date_period_create_from_iso8601_string() -> ClassMethod { param("options", Some(TypeExpr::Int), Some(int_lit(0))), ], variadic: None, + variadic_by_ref: false, variadic_type: None, // PHP 8.3+: returns a `DatePeriod` or throws (never `false`). return_type: Some(TypeExpr::Named(Name::unqualified("DatePeriod"))), diff --git a/src/types/checker/builtin_types/datetime.rs b/src/types/checker/builtin_types/datetime.rs index 9a81402c3b..158ee332f1 100644 --- a/src/types/checker/builtin_types/datetime.rs +++ b/src/types/checker/builtin_types/datetime.rs @@ -141,6 +141,7 @@ fn method( has_body: true, params, variadic: None, + variadic_by_ref: false, variadic_type: None, return_type, by_ref_return: false, @@ -278,6 +279,7 @@ fn datetime_zone_list_identifiers() -> ClassMethod { ), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -435,6 +437,7 @@ fn datetime_zone_list_abbreviations() -> ClassMethod { has_body: true, params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -1549,6 +1552,7 @@ fn datetime_create_from_format(class_name: &str) -> ClassMethod { ), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Union(vec![ TypeExpr::Named(Name::unqualified(class_name)), @@ -1587,6 +1591,7 @@ fn datetime_get_last_errors(class_name: &str) -> ClassMethod { has_body: true, params: vec![], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -1632,6 +1637,7 @@ fn datetime_create_from_object(method_name: &str, target_class: &str) -> ClassMe false, )], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(target_class))), by_ref_return: false, @@ -1675,6 +1681,7 @@ fn datetime_create_from_timestamp(class_name: &str) -> ClassMethod { false, )], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(class_name))), by_ref_return: false, @@ -1725,6 +1732,7 @@ fn datetime_set_isodate(class_name: &str) -> ClassMethod { ), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(class_name))), by_ref_return: false, @@ -1935,6 +1943,7 @@ fn datetime_date_parse_from_format() -> ClassMethod { ("datetime".to_string(), Some(TypeExpr::Str), None, false), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2020,6 +2029,7 @@ fn datetime_gettimeofday() -> ClassMethod { false, )], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2218,6 +2228,7 @@ fn datetime_extract_micros() -> ClassMethod { has_body: true, params: vec![("s".to_string(), Some(TypeExpr::Str), None, false)], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), by_ref_return: false, @@ -2286,6 +2297,7 @@ fn datetime_extract_modify_micros() -> ClassMethod { has_body: true, params: vec![("m".to_string(), Some(TypeExpr::Str), None, false)], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), by_ref_return: false, @@ -2309,6 +2321,7 @@ fn datetime_strip_modify_micros() -> ClassMethod { has_body: true, params: vec![("m".to_string(), Some(TypeExpr::Str), None, false)], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -2332,6 +2345,7 @@ fn datetime_strip_micros() -> ClassMethod { has_body: true, params: vec![("s".to_string(), Some(TypeExpr::Str), None, false)], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -2362,6 +2376,7 @@ fn datetime_strftime() -> ClassMethod { ("utc".to_string(), Some(TypeExpr::Bool), None, false), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -2594,6 +2609,7 @@ fn datetime_tz_name_from_abbr() -> ClassMethod { ), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2756,6 +2772,7 @@ fn datetime_strptime() -> ClassMethod { ("format".to_string(), Some(TypeExpr::Str), None, false), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2785,6 +2802,7 @@ fn datetime_sun_rs() -> ClassMethod { ("limb".to_string(), Some(TypeExpr::Int), None, false), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2812,6 +2830,7 @@ fn datetime_sun_val() -> ClassMethod { ("tsval".to_string(), Some(TypeExpr::Int), None, false), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2839,6 +2858,7 @@ fn datetime_sun_info() -> ClassMethod { ("longitude".to_string(), Some(TypeExpr::Float), None, false), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2897,6 +2917,7 @@ fn datetime_sunfunc() -> ClassMethod { ), ], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2922,6 +2943,7 @@ fn datetime_date_parse() -> ClassMethod { has_body: true, params: vec![("datetime".to_string(), Some(TypeExpr::Str), None, false)], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("mixed"))), by_ref_return: false, @@ -2970,6 +2992,7 @@ fn abstract_method( has_body: false, params, variadic: None, + variadic_by_ref: false, variadic_type: None, return_type, by_ref_return: false, @@ -3278,6 +3301,7 @@ fn date_interval_create_from_date_string() -> ClassMethod { has_body: true, params: vec![("datetime".to_string(), Some(TypeExpr::Str), None, false)], variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("DateInterval"))), by_ref_return: false, diff --git a/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index eef802f3a5..b1ca606124 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -73,6 +73,7 @@ pub(super) fn builtin_exception_constructor_method() -> ClassMethod { ], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -263,6 +264,7 @@ fn concrete_throwable_method(name: &str, return_type: TypeExpr, value: Expr) -> params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, @@ -287,6 +289,7 @@ fn abstract_throwable_method(name: &str, return_type: TypeExpr) -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, diff --git a/src/types/checker/builtin_types/fiber.rs b/src/types/checker/builtin_types/fiber.rs index 52f3a9416b..550c9c3577 100644 --- a/src/types/checker/builtin_types/fiber.rs +++ b/src/types/checker/builtin_types/fiber.rs @@ -65,6 +65,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -85,6 +86,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { params: vec![("callback".to_string(), None, None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -106,6 +108,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -124,6 +127,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { params: vec![("value".to_string(), None, null_default(), false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -142,6 +146,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { params: vec![("exception".to_string(), None, None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -160,6 +165,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -183,6 +189,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { params: vec![("value".to_string(), None, null_default(), false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -201,6 +208,7 @@ pub(super) fn builtin_fiber_methods() -> Vec { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index f2c410d268..4470652164 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -370,6 +370,7 @@ fn builtin_reflection_private_constructor_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -393,6 +394,7 @@ fn builtin_reflection_attribute_get_name_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -425,6 +427,7 @@ fn builtin_reflection_attribute_get_arguments_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(crate::names::Name::unqualified("array"))), by_ref_return: false, @@ -457,6 +460,7 @@ fn builtin_reflection_attribute_new_instance_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), by_ref_return: false, @@ -487,6 +491,7 @@ fn builtin_reflection_class_new_instance_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: Some("args".to_string()), + variadic_by_ref: false, variadic_type: None, return_type: Some(object_type()), by_ref_return: false, @@ -528,6 +533,7 @@ fn builtin_reflection_method_invoke_method() -> ClassMethod { params: vec![("object".to_string(), Some(mixed_type()), None, false)], param_attributes: Vec::new(), variadic: Some("args".to_string()), + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![Stmt::new( @@ -558,6 +564,7 @@ fn builtin_reflection_method_invoke_args_method() -> ClassMethod { ], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![Stmt::new( @@ -582,6 +589,7 @@ fn builtin_reflection_method_create_from_method_name_method() -> ClassMethod { params: vec![("method".to_string(), Some(TypeExpr::Str), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("ReflectionMethod"))), body: vec![Stmt::new( @@ -615,6 +623,7 @@ fn builtin_reflection_set_accessible_method() -> ClassMethod { params: vec![("accessible".to_string(), Some(bool_type()), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), body: Vec::new(), @@ -639,6 +648,7 @@ fn builtin_reflection_function_invoke_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: Some("args".to_string()), + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![Stmt::new( @@ -666,6 +676,7 @@ fn builtin_reflection_function_invoke_args_method() -> ClassMethod { params: vec![("args".to_string(), Some(array_type()), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![Stmt::new( @@ -694,6 +705,7 @@ fn builtin_reflection_class_new_instance_args_method() -> ClassMethod { params: vec![("args".to_string(), Some(array_type()), empty_array(), false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), by_ref_return: false, @@ -1136,6 +1148,7 @@ fn builtin_reflection_class_new_instance_without_constructor_method() -> ClassMe params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), by_ref_return: false, @@ -2041,6 +2054,7 @@ fn builtin_reflection_class_string_method(method_name: &str, property: &str) -> params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -2072,6 +2086,7 @@ fn builtin_reflection_class_int_method(method_name: &str, property: &str) -> Cla params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), body: vec![Stmt::new( @@ -2132,6 +2147,7 @@ fn builtin_reflection_class_has_name_method( params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), body: vec![Stmt::new(StmtKind::Return(Some(contains)), dummy_span)], @@ -2170,6 +2186,7 @@ fn builtin_reflection_class_get_constant_method() -> ClassMethod { params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![ @@ -2247,6 +2264,7 @@ fn builtin_reflection_class_get_static_property_value_method() -> ClassMethod { ], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![Stmt::new( @@ -2274,6 +2292,7 @@ fn builtin_reflection_class_set_static_property_value_method() -> ClassMethod { ], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), body: Vec::new(), @@ -2310,6 +2329,7 @@ fn builtin_reflection_class_get_reflection_constant_method() -> ClassMethod { params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![ @@ -2441,6 +2461,7 @@ fn builtin_reflection_class_implements_interface_method() -> ClassMethod { params: vec![("interface".to_string(), Some(TypeExpr::Str), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![ @@ -2572,6 +2593,7 @@ fn builtin_reflection_class_is_subclass_of_method() -> ClassMethod { params: vec![("class".to_string(), Some(TypeExpr::Str), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![ @@ -2639,6 +2661,7 @@ fn builtin_reflection_class_is_instance_method() -> ClassMethod { params: vec![("object".to_string(), Some(object_type()), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new( @@ -2778,6 +2801,7 @@ fn builtin_reflection_class_array_method( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type.clone()), body: vec![Stmt::new( @@ -2844,6 +2868,7 @@ fn builtin_reflection_class_filtered_array_method( )], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type.clone()), body: vec![ @@ -2997,6 +3022,7 @@ fn builtin_reflection_class_get_member_method( params: vec![("name".to_string(), Some(TypeExpr::Str), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(return_class))), body: vec![ @@ -3042,6 +3068,7 @@ fn builtin_reflection_class_nullable_object_method( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(nullable_object_type(class_name)), body: vec![Stmt::new( @@ -3076,6 +3103,7 @@ fn builtin_reflection_class_object_method( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(class_name))), body: vec![Stmt::new( @@ -3106,6 +3134,7 @@ fn builtin_reflection_class_mixed_method(method_name: &str, property: &str) -> C params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![Stmt::new( @@ -3136,6 +3165,7 @@ fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> Cl params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new( @@ -3166,6 +3196,7 @@ fn builtin_reflection_method_get_prototype_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("ReflectionMethod"))), body: vec![ @@ -3213,6 +3244,7 @@ fn builtin_reflection_property_is_default_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new( @@ -3243,6 +3275,7 @@ fn builtin_reflection_constant_false_union_method(method_name: &str) -> ClassMet params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(string_or_bool_type()), body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], @@ -3264,6 +3297,7 @@ fn builtin_reflection_constant_false_bool_method(method_name: &str) -> ClassMeth params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], @@ -3285,6 +3319,7 @@ fn builtin_reflection_constant_empty_array_method(method_name: &str) -> ClassMet params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(array_type()), body: vec![Stmt::new(StmtKind::Return(empty_array()), dummy_span)], @@ -3306,6 +3341,7 @@ fn builtin_reflection_constant_null_mixed_method(method_name: &str) -> ClassMeth params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![Stmt::new(StmtKind::Return(null_expr()), dummy_span)], @@ -3337,6 +3373,7 @@ fn builtin_reflection_method_name_predicate_method( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new(StmtKind::Return(Some(comparison)), dummy_span)], @@ -3358,6 +3395,7 @@ fn builtin_reflection_property_has_type_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new( @@ -3411,6 +3449,7 @@ fn builtin_reflection_property_modifier_mask_method(method_name: &str, mask: i64 params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new(StmtKind::Return(Some(comparison)), dummy_span)], @@ -3441,6 +3480,7 @@ fn builtin_reflection_property_has_hook_method() -> ClassMethod { params: vec![("type".to_string(), Some(mixed_type()), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new(StmtKind::Return(Some(has_hook)), dummy_span)], @@ -3472,6 +3512,7 @@ fn builtin_reflection_property_get_hook_method() -> ClassMethod { params: vec![("type".to_string(), Some(mixed_type()), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(nullable_object_type("ReflectionMethod")), body: vec![ @@ -3522,6 +3563,7 @@ fn builtin_reflection_property_get_value_method() -> ClassMethod { params: vec![("object".to_string(), Some(mixed_type()), null_expr(), false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), body: vec![ @@ -3555,6 +3597,7 @@ fn builtin_reflection_property_set_value_method() -> ClassMethod { ], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), body: vec![ @@ -3611,6 +3654,7 @@ fn builtin_reflection_property_is_initialized_method() -> ClassMethod { params: vec![("object".to_string(), Some(mixed_type()), null_expr(), false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![ @@ -3753,6 +3797,7 @@ fn builtin_reflection_property_is_lazy_method() -> ClassMethod { params: vec![("object".to_string(), Some(object_type()), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], @@ -3785,6 +3830,7 @@ fn builtin_reflection_property_skip_lazy_initialization_method() -> ClassMethod params: vec![("object".to_string(), Some(object_type()), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), body: vec![ @@ -4113,6 +4159,7 @@ fn builtin_reflection_parameter_count_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), body: vec![Stmt::new( @@ -4164,6 +4211,7 @@ fn builtin_reflection_function_method_is_variadic_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), body: vec![ @@ -4585,6 +4633,7 @@ fn builtin_reflection_owner_constructor_method( .collect(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -4608,6 +4657,7 @@ fn builtin_reflection_owner_get_attributes_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(array_type()), by_ref_return: false, diff --git a/src/types/checker/builtins/callables/preg_replace_callback.rs b/src/types/checker/builtins/callables/preg_replace_callback.rs index 38cf72bf51..242a5a1ae9 100644 --- a/src/types/checker/builtins/callables/preg_replace_callback.rs +++ b/src/types/checker/builtins/callables/preg_replace_callback.rs @@ -58,6 +58,7 @@ fn contextual_closure_sig( let ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -131,7 +132,7 @@ fn contextual_closure_sig( param_types.push((name.clone(), PhpType::Array(Box::new(PhpType::Mixed)))); param_type_exprs.push(None); defaults.push(None); - ref_params.push(false); + ref_params.push(*variadic_by_ref); declared_params.push(false); } diff --git a/src/types/checker/callables/closures.rs b/src/types/checker/callables/closures.rs index 037ea28a14..3d27d1979b 100644 --- a/src/types/checker/callables/closures.rs +++ b/src/types/checker/callables/closures.rs @@ -39,12 +39,19 @@ impl Checker { &mut self, params: &[(String, Option, Option, bool)], variadic: &Option, + variadic_by_ref: bool, captures: &[String], span: Span, env: &TypeEnv, ) -> Result { self.prepare_closure_signature_context_with_param_hints( - params, variadic, captures, span, env, &[], + params, + variadic, + variadic_by_ref, + captures, + span, + env, + &[], ) } @@ -61,6 +68,7 @@ impl Checker { &mut self, params: &[(String, Option, Option, bool)], variadic: &Option, + variadic_by_ref: bool, captures: &[String], span: Span, env: &TypeEnv, @@ -117,7 +125,7 @@ impl Checker { param_types.push((name.clone(), PhpType::Array(Box::new(PhpType::Mixed)))); param_type_exprs.push(None); defaults.push(None); - ref_params.push(false); + ref_params.push(variadic_by_ref); declared_params.push(false); } @@ -222,6 +230,7 @@ impl Checker { ExprKind::Closure { params, variadic, + variadic_by_ref, return_type, body, captures, @@ -232,6 +241,7 @@ impl Checker { let closure_sig = self.prepare_closure_signature_context( params, variadic, + *variadic_by_ref, captures, expr.span, env, diff --git a/src/types/checker/driver/functions.rs b/src/types/checker/driver/functions.rs index 9e7a5714b0..a03bdc6e99 100644 --- a/src/types/checker/driver/functions.rs +++ b/src/types/checker/driver/functions.rs @@ -55,6 +55,7 @@ impl Checker { params, param_attributes, variadic, + variadic_by_ref, variadic_type, return_type, by_ref_return, @@ -84,7 +85,10 @@ impl Checker { params.iter().map(|(_, t, _, _)| t.clone()).collect(); let defaults: Vec> = params.iter().map(|(_, _, d, _)| d.clone()).collect(); - let ref_flags: Vec = params.iter().map(|(_, _, _, r)| *r).collect(); + let mut ref_flags: Vec = params.iter().map(|(_, _, _, r)| *r).collect(); + if variadic.is_some() { + ref_flags.push(*variadic_by_ref); + } self.fn_decls.insert( name.clone(), FnDecl { @@ -94,6 +98,7 @@ impl Checker { defaults, ref_params: ref_flags, variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type.clone(), by_ref_return: *by_ref_return, diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 528d02e18f..82cb724271 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -431,11 +431,14 @@ fn trait_method_reflection_sig(method: &ClassMethod) -> FunctionSig { .iter() .map(|(_, _, default, _)| default.clone()) .collect(); - let ref_params = method + let mut ref_params: Vec = method .params .iter() .map(|(_, _, _, by_ref)| *by_ref) .collect(); + if method.variadic.is_some() { + ref_params.push(method.variadic_by_ref); + } callable_wrapper_sig(&FunctionSig { params, param_type_exprs: method diff --git a/src/types/checker/functions/resolution/mod.rs b/src/types/checker/functions/resolution/mod.rs index 1f254cbfb4..c9481ad9df 100644 --- a/src/types/checker/functions/resolution/mod.rs +++ b/src/types/checker/functions/resolution/mod.rs @@ -188,7 +188,14 @@ impl Checker { decl.variadic .iter() .cloned() - .map(|name| (name, PhpType::Array(Box::new(PhpType::Int)))), + .map(|name| { + let elem_ty = if decl.variadic_by_ref { + PhpType::Mixed + } else { + PhpType::Int + }; + (name, PhpType::Array(Box::new(elem_ty))) + }), ) .collect(), param_type_exprs: decl @@ -402,7 +409,9 @@ impl Checker { } if let Some(ref vp) = decl.variadic { - if let Some(declared) = &decl.variadic_type { + if decl.variadic_by_ref { + param_types.push((vp.clone(), PhpType::Array(Box::new(PhpType::Mixed)))); + } else if let Some(declared) = &decl.variadic_type { // A declared element type (`int ...$xs`) constrains every collected argument; // it takes precedence over inference so call validation enforces the hint. let elem_ty = self.resolve_declared_param_type_hint( diff --git a/src/types/checker/functions/resolution/specialization.rs b/src/types/checker/functions/resolution/specialization.rs index 0ecae3fbd1..cc38c537b4 100644 --- a/src/types/checker/functions/resolution/specialization.rs +++ b/src/types/checker/functions/resolution/specialization.rs @@ -155,7 +155,10 @@ impl Checker { } seen_idx += 1; } - if stored_sig.variadic.is_some() && seen_idx > regular_param_count { + if stored_sig.variadic.is_some() + && seen_idx > regular_param_count + && !variadic_param_is_by_ref(stored_sig) + { let regular_names: Vec = stored_sig.params[..regular_param_count] .iter() .map(|(name, _)| name.clone()) @@ -278,6 +281,19 @@ fn is_callable_array_type(ty: &PhpType) -> bool { } } +/// Returns whether a stored function signature has a by-reference variadic slot. +fn variadic_param_is_by_ref(sig: &FunctionSig) -> bool { + let Some(variadic_name) = sig.variadic.as_ref() else { + return false; + }; + sig.params + .iter() + .position(|(name, _)| name == variadic_name) + .and_then(|index| sig.ref_params.get(index)) + .copied() + .unwrap_or(false) +} + /// Extracts parameter types from a generic `param_types` list, mapping them to the /// parameter names declared in `decl`. /// diff --git a/src/types/checker/inference/expr/mod.rs b/src/types/checker/inference/expr/mod.rs index abec45e2d3..1a1bf086d0 100644 --- a/src/types/checker/inference/expr/mod.rs +++ b/src/types/checker/inference/expr/mod.rs @@ -479,6 +479,7 @@ impl Checker { ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type: _, return_type, body, @@ -494,6 +495,7 @@ impl Checker { self.infer_closure_type( params, variadic, + *variadic_by_ref, return_type, body, captures, diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index 5b4c2711b8..155ce20a80 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -512,11 +512,15 @@ impl Checker { sig, regular_param_count, env, - ) { + ) && !method_variadic_param_is_by_ref(sig) + { if let Some((_, variadic_ty)) = sig.params.last_mut() { *variadic_ty = PhpType::Iterable; } - } else if sig.variadic.is_some() && arg_types.len() > regular_param_count { + } else if sig.variadic.is_some() + && arg_types.len() > regular_param_count + && !method_variadic_param_is_by_ref(sig) + { let mut elem_ty = arg_types[regular_param_count].clone(); for arg_ty in arg_types.iter().skip(regular_param_count + 1) { elem_ty = wider_type_syntactic(&elem_ty, arg_ty); @@ -1018,11 +1022,15 @@ impl Checker { sig, regular_param_count, env, - ) { + ) && !method_variadic_param_is_by_ref(sig) + { if let Some((_, variadic_ty)) = sig.params.last_mut() { *variadic_ty = PhpType::Iterable; } - } else if sig.variadic.is_some() && arg_types.len() > regular_param_count { + } else if sig.variadic.is_some() + && arg_types.len() > regular_param_count + && !method_variadic_param_is_by_ref(sig) + { let mut elem_ty = arg_types[regular_param_count].clone(); for arg_ty in arg_types.iter().skip(regular_param_count + 1) { elem_ty = wider_type_syntactic(&elem_ty, arg_ty); @@ -1076,7 +1084,10 @@ impl Checker { } } } - if sig.variadic.is_some() && arg_types.len() > regular_param_count { + if sig.variadic.is_some() + && arg_types.len() > regular_param_count + && !method_variadic_param_is_by_ref(sig) + { let mut elem_ty = arg_types[regular_param_count].clone(); for arg_ty in arg_types.iter().skip(regular_param_count + 1) { elem_ty = wider_type_syntactic(&elem_ty, arg_ty); @@ -1126,6 +1137,19 @@ fn method_variadic_tail_needs_iterable( }) } +/// Returns whether a method signature stores its variadic slot by reference. +fn method_variadic_param_is_by_ref(sig: &FunctionSig) -> bool { + let Some(variadic_name) = sig.variadic.as_ref() else { + return false; + }; + sig.params + .iter() + .position(|(name, _)| name == variadic_name) + .and_then(|index| sig.ref_params.get(index)) + .copied() + .unwrap_or(false) +} + /// Returns true when a spread source can carry string keys into a variadic method tail. fn spread_source_keeps_runtime_keys(expr: &Expr, env: &TypeEnv) -> bool { match &expr.kind { diff --git a/src/types/checker/inference/ops.rs b/src/types/checker/inference/ops.rs index 4563dedcfb..d157b6d706 100644 --- a/src/types/checker/inference/ops.rs +++ b/src/types/checker/inference/ops.rs @@ -310,6 +310,7 @@ impl Checker { &mut self, params: &[(String, Option, Option, bool)], variadic: &Option, + variadic_by_ref: bool, return_type: &Option, body: &[Stmt], captures: &[String], @@ -320,6 +321,7 @@ impl Checker { self.infer_closure_type_with_param_hints( params, variadic, + variadic_by_ref, return_type, body, captures, @@ -342,6 +344,7 @@ impl Checker { &mut self, params: &[(String, Option, Option, bool)], variadic: &Option, + variadic_by_ref: bool, return_type: &Option, body: &[Stmt], captures: &[String], @@ -353,6 +356,7 @@ impl Checker { let mut closure_sig = self.prepare_closure_signature_context_with_param_hints( params, variadic, + variadic_by_ref, captures, expr.span, env, @@ -363,6 +367,11 @@ impl Checker { .filter(|(_, _, _, is_ref)| *is_ref) .map(|(name, _, _, _)| name.clone()) .collect(); + if variadic_by_ref { + if let Some(name) = variadic { + closure_ref_params.push(name.clone()); + } + } closure_ref_params.extend(capture_refs.iter().cloned()); // Inside a closure body, `$this` is permitted even with no enclosing // class method: the closure may be bound to an object later. Track the diff --git a/src/types/checker/method_pass.rs b/src/types/checker/method_pass.rs index d4076e45e2..a5a7d26b88 100644 --- a/src/types/checker/method_pass.rs +++ b/src/types/checker/method_pass.rs @@ -115,11 +115,16 @@ impl Checker { method_env.insert(pname.clone(), ty); } if let Some(variadic_name) = &method.variadic { + let fallback_ty = if method.variadic_by_ref { + PhpType::Array(Box::new(PhpType::Mixed)) + } else { + PhpType::Array(Box::new(PhpType::Int)) + }; let ty = sig_params .as_ref() .and_then(|p| p.get(method.params.len())) .map(|(_, t)| t.clone()) - .unwrap_or(PhpType::Array(Box::new(PhpType::Int))); + .unwrap_or(fallback_ty); method_env.insert(variadic_name.clone(), ty); } if method_key == "__construct" { diff --git a/src/types/checker/mod.rs b/src/types/checker/mod.rs index eb0cf7f3c3..2152a710f7 100644 --- a/src/types/checker/mod.rs +++ b/src/types/checker/mod.rs @@ -196,6 +196,8 @@ pub(crate) struct FnDecl { pub defaults: Vec>, pub ref_params: Vec, pub variadic: Option, + /// Whether the variadic parameter was declared by reference (`&...$args`). + pub variadic_by_ref: bool, /// Declared element type hint on the variadic parameter (`int ...$xs`), if any. pub variadic_type: Option, pub return_type: Option, diff --git a/src/types/checker/schema/classes/constants.rs b/src/types/checker/schema/classes/constants.rs index ab75d897cf..54f1bf2b3b 100644 --- a/src/types/checker/schema/classes/constants.rs +++ b/src/types/checker/schema/classes/constants.rs @@ -162,6 +162,7 @@ fn rewrite_expr( ExprKind::Closure { params, variadic, + variadic_by_ref, variadic_type, return_type, body, @@ -186,6 +187,7 @@ fn rewrite_expr( }) .collect::, CompileError>>()?, variadic: variadic.clone(), + variadic_by_ref: *variadic_by_ref, variadic_type: variadic_type.clone(), return_type: return_type.clone(), body: body.clone(), diff --git a/src/types/checker/schema/validation.rs b/src/types/checker/schema/validation.rs index d58c7ca45d..d3ad647eea 100644 --- a/src/types/checker/schema/validation.rs +++ b/src/types/checker/schema/validation.rs @@ -57,7 +57,7 @@ pub(crate) fn build_method_sig( }) .collect::, CompileError>>()?; let defaults: Vec> = method.params.iter().map(|(_, _, d, _)| d.clone()).collect(); - let ref_params: Vec = method.params.iter().map(|(_, _, _, r)| *r).collect(); + let mut ref_params: Vec = method.params.iter().map(|(_, _, _, r)| *r).collect(); for ((param_name, type_ann, default, _), (_, resolved_ty)) in method.params.iter().zip(params.iter()) { @@ -78,6 +78,9 @@ pub(crate) fn build_method_sig( )?, None => super::super::infer_return_type_syntactic(&method.body), }; + if method.variadic.is_some() { + ref_params.push(method.variadic_by_ref); + } let mut sig = Checker::callable_wrapper_sig(&FunctionSig { params, param_type_exprs: method @@ -109,17 +112,19 @@ pub(crate) fn build_method_sig( // A declared element type on the variadic (`int ...$xs`) constrains every collected argument. // `callable_wrapper_sig` defaults the variadic container to `array`; refine it to the // declared element type so call validation enforces it. - if let Some(variadic_type) = &method.variadic_type { - let elem_ty = checker.resolve_declared_param_type_hint( - variadic_type, - method.span, - &format!( - "Method variadic parameter ${}", - method.variadic.as_deref().unwrap_or_default() - ), - )?; - if let Some((_, ty)) = sig.params.last_mut() { - *ty = PhpType::Array(Box::new(elem_ty)); + if !method.variadic_by_ref { + if let Some(variadic_type) = &method.variadic_type { + let elem_ty = checker.resolve_declared_param_type_hint( + variadic_type, + method.span, + &format!( + "Method variadic parameter ${}", + method.variadic.as_deref().unwrap_or_default() + ), + )?; + if let Some((_, ty)) = sig.params.last_mut() { + *ty = PhpType::Array(Box::new(elem_ty)); + } } } Ok(sig) diff --git a/src/types/checker/type_compat/declarations.rs b/src/types/checker/type_compat/declarations.rs index d15b961404..963ef1cd1b 100644 --- a/src/types/checker/type_compat/declarations.rs +++ b/src/types/checker/type_compat/declarations.rs @@ -240,7 +240,9 @@ impl Checker { } } if let Some(variadic_name) = decl.variadic.as_ref() { - let elem_ty = if let Some(type_ann) = decl.variadic_type.as_ref() { + let elem_ty = if decl.variadic_by_ref { + PhpType::Mixed + } else if let Some(type_ann) = decl.variadic_type.as_ref() { self.resolve_declared_param_type_hint( type_ann, decl.span, diff --git a/src/types/signatures.rs b/src/types/signatures.rs index 4dc16e7f23..2c75d1ed4c 100644 --- a/src/types/signatures.rs +++ b/src/types/signatures.rs @@ -61,20 +61,36 @@ pub(crate) fn callable_wrapper_sig(sig: &FunctionSig) -> FunctionSig { } } - let variadic_attributes = if wrapper_sig.param_attributes.len() > wrapper_sig.params.len() { - wrapper_sig.param_attributes.remove(wrapper_sig.params.len()) + let variadic_index = wrapper_sig.params.len(); + let variadic_type_expr = if wrapper_sig.param_type_exprs.len() > variadic_index { + wrapper_sig.param_type_exprs.remove(variadic_index) + } else { + None + }; + let variadic_attributes = if wrapper_sig.param_attributes.len() > variadic_index { + wrapper_sig.param_attributes.remove(variadic_index) } else { Vec::new() }; + let variadic_ref = if wrapper_sig.ref_params.len() > variadic_index { + wrapper_sig.ref_params.remove(variadic_index) + } else { + false + }; + let variadic_declared = if wrapper_sig.declared_params.len() > variadic_index { + wrapper_sig.declared_params.remove(variadic_index) + } else { + false + }; wrapper_sig.params.push(( variadic_name.clone(), PhpType::Array(Box::new(PhpType::Mixed)), )); wrapper_sig.defaults.push(None); - wrapper_sig.ref_params.push(false); - wrapper_sig.declared_params.push(false); - wrapper_sig.param_type_exprs.push(None); + wrapper_sig.ref_params.push(variadic_ref); + wrapper_sig.declared_params.push(variadic_declared); + wrapper_sig.param_type_exprs.push(variadic_type_expr); wrapper_sig.param_attributes.push(variadic_attributes); wrapper_sig } diff --git a/tests/codegen/callables/state_and_variadics.rs b/tests/codegen/callables/state_and_variadics.rs index aceea83b3b..124dc2c1f6 100644 --- a/tests/codegen/callables/state_and_variadics.rs +++ b/tests/codegen/callables/state_and_variadics.rs @@ -286,6 +286,34 @@ echo $x; assert_eq!(out, "15"); } +/// Verifies by-reference variadic function and method element assignments mutate caller variables. +#[test] +fn test_by_ref_variadic_function_and_method_element_writeback() { + let out = compile_and_run( + r#"m($c, $d); +echo $c . ":" . $d; +"#, + ); + assert_eq!(out, "A-f:B-g|C-m:D-n"); +} + // --- Variadic functions --- /// Verifies a variadic function collects exactly three positional arguments into the rest array. diff --git a/tests/parser_tests/classes/declarations.rs b/tests/parser_tests/classes/declarations.rs index 5e31c37f34..9af49c5ec3 100644 --- a/tests/parser_tests/classes/declarations.rs +++ b/tests/parser_tests/classes/declarations.rs @@ -152,6 +152,21 @@ fn test_parse_class_decl_with_extends() { } } +/// Verifies that class methods preserve `&...$items` as by-reference variadic metadata. +#[test] +fn test_parse_method_by_ref_variadic_param() { + let stmts = parse_source(" { + assert_eq!(methods.len(), 1); + assert_eq!(methods[0].name, "collect"); + assert_eq!(methods[0].variadic.as_deref(), Some("items")); + assert!(methods[0].variadic_by_ref); + } + _ => panic!("Expected ClassDecl"), + } +} + /// Verifies that ` { + assert_eq!(params.len(), 1); + assert_eq!(params[0].0, "first"); + assert!(params[0].3); + assert_eq!(variadic.as_deref(), Some("args")); + assert!(*variadic_by_ref); + } + _ => panic!("Expected FunctionDecl"), + } +} + #[test] // Verifies that ` match &value.kind { + ExprKind::Closure { + variadic, + variadic_by_ref, + .. + } => { + assert_eq!(variadic.as_deref(), Some("xs")); + assert!(*variadic_by_ref); + } + other => panic!("Expected Closure, got {:?}", other), + }, + other => panic!("Expected Assign, got {:?}", other), + } +} + #[test] // Verifies that ` Date: Wed, 1 Jul 2026 05:52:27 +0200 Subject: [PATCH 1020/1208] test: cover eval constructor refcounted by-ref writeback --- tests/codegen/eval_constructors.rs | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/codegen/eval_constructors.rs b/tests/codegen/eval_constructors.rs index a49fcbd605..fc28ac200a 100644 --- a/tests/codegen/eval_constructors.rs +++ b/tests/codegen/eval_constructors.rs @@ -92,6 +92,54 @@ return gettype(EvalCtorNamedRefTargetStatic::$value) . ":" . EvalCtorNamedRefTar assert_eq!(out, "integer:5|integer:9|integer:10|integer:15"); } +/// Verifies AOT constructor by-reference args write back refcounted string, array, and object values. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_refcounted_writeback() { + let out = compile_and_run( + r#"name = $name; + } +} + +class EvalCtorStringRefBridge { + public function __construct(string &$value) { + $value = $value . "-ctor"; + } +} + +class EvalCtorArrayRefBridge { + public function __construct(array &$items) { + $items[0] = $items[0] . "-head"; + $items[] = "tail"; + } +} + +class EvalCtorObjectRefBridge { + public function __construct(EvalCtorRefcountedPayload &$box) { + $box = new EvalCtorRefcountedPayload($box->name . "-ctor"); + } +} + +echo eval('$text = "A"; +new EvalCtorStringRefBridge($text); +echo $text . "|"; + +$items = ["B"]; +new EvalCtorArrayRefBridge($items); +echo $items[0] . ":" . $items[1] . "|"; + +$box = new EvalCtorRefcountedPayload("C"); +new EvalCtorObjectRefBridge($box); +return $box->name;'); +"#, + ); + + assert_eq!(out, "A-ctor|B-head:tail|C-ctor"); +} + /// Verifies AOT constructor by-reference writeback happens before a catchable throw. #[test] fn test_eval_dynamic_new_constructor_by_ref_lvalue_writeback_before_throw() { From 6842f1f507098bb4fbdecaa33ec8082ce4c061c2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 07:03:38 +0200 Subject: [PATCH 1021/1208] Support eval AOT by-ref variadic callables --- docs/php/eval.md | 23 ++++-- src/codegen/lower_inst/arrays.rs | 31 ++++++++ tests/codegen/eval_callables.rs | 113 ++++++++++++++++++++++++++++- tests/codegen/eval_constructors.rs | 26 +++++++ 4 files changed, 184 insertions(+), 9 deletions(-) diff --git a/docs/php/eval.md b/docs/php/eval.md index e18defc012..cf55010458 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -118,10 +118,12 @@ or `float`), string raw storage, or one-word heap raw storage (`array`, layouts, and other unsupported raw-storage by-reference free-function parameters remain metadata-only until the function bridge has typed staging/writeback for those ABI layouts. -Generated/AOT `&...$variadic` by-reference parameter tails are also -metadata-visible but not PHP-compatible through eval yet: the bridge currently -builds a temporary variadic array and does not propagate element-level writes -back to the caller variables. +Generated/AOT positional `&...$variadic` by-reference parameter tails are +supported for eval direct, variable-function, first-class callable, +`Closure::fromCallable()`, and `call_user_func_array()` dispatch when the +passed tail arguments have lvalue/ref-cell storage; element-level writes +propagate back to the caller variables, while rebinding the variadic container +itself remains local to the callee. `call_user_func()` remains by-value for registered AOT free-function by-reference parameters. @@ -567,11 +569,14 @@ the native bridge with parameter names, supported defaults, named arguments, and indexed or string-keyed runtime argument arrays. Direct eval calls, variable function calls, and `call_user_func()` paths can also invoke registered generated/AOT free functions with positional variadic tails when the generated -signature has no by-reference parameters. Direct and variable-function calls -can additionally invoke generated/AOT free functions whose by-reference +signature has no by-reference parameters. Direct, variable-function, +first-class callable, `Closure::fromCallable()`, and `call_user_func_array()` +paths can additionally invoke generated/AOT free functions whose by-reference parameters use boxed Mixed/union storage or one-word scalar raw storage (`int`, `bool`, or `float`), string raw storage, or one-word heap raw storage -(`array`, `iterable`, and object/class parameters). Registered generated/AOT free-function parameter +(`array`, `iterable`, and object/class parameters), including positional +`&...` variadic tails when the tail arguments have lvalue/ref-cell storage. +Registered generated/AOT free-function parameter names, declared types, return types, by-reference and variadic flags, required/optional counts, and supported defaults are also exposed through `ReflectionFunction` / `ReflectionParameter` metadata. Unsupported @@ -925,7 +930,9 @@ static-method, and constructor signatures whose generated ABI storage is scalar/string, callable descriptor, boxed Mixed/union, array/hash, iterable, or object, plus free-function signatures using the descriptor invoker ABI when by-reference parameters, if any, use boxed Mixed/union storage or raw one-word -scalar storage (`int`, `bool`, or `float`). +scalar storage (`int`, `bool`, or `float`), string raw storage, or one-word heap +storage (`array`, `iterable`, and object/class parameters), including +positional `&...` variadic tail element writeback. Registered type specs validate nullable and union members, intersection object parameters, and intersection object returns before or after the generated AOT call. By-reference method and constructor parameters remain limited to the diff --git a/src/codegen/lower_inst/arrays.rs b/src/codegen/lower_inst/arrays.rs index 7ba03f261c..93603be763 100644 --- a/src/codegen/lower_inst/arrays.rs +++ b/src/codegen/lower_inst/arrays.rs @@ -913,7 +913,9 @@ fn emit_box_loaded_invoker_ref_cell_value_as_mixed( Arch::X86_64 => "rdx", }; let string_hi_label = ctx.next_label("array_get_mixed_ref_string_hi"); + let mixed_cell_label = ctx.next_label("array_get_mixed_ref_cell"); let box_label = ctx.next_label("array_get_mixed_ref_box"); + let done_label = ctx.next_label("array_get_mixed_ref_done"); abi::emit_load_from_address(ctx.emitter, ref_cell_reg, mixed_reg, 8); abi::emit_load_from_address(ctx.emitter, tag_reg, mixed_reg, 16); @@ -921,10 +923,14 @@ fn emit_box_loaded_invoker_ref_cell_value_as_mixed( abi::emit_load_int_immediate(ctx.emitter, hi_reg, 0); match ctx.emitter.target.arch { Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #{}", tag_reg, runtime_value_tag(&PhpType::Mixed))); // check whether the ref-cell stores a boxed Mixed handle + ctx.emitter.instruction(&format!("b.eq {}", mixed_cell_label)); // retain and forward boxed Mixed values without reboxing their pointer ctx.emitter.instruction(&format!("cmp {}, #1", tag_reg)); // check whether the referenced value is a string slot ctx.emitter.instruction(&format!("b.eq {}", string_hi_label)); // load string length only for string ref-cells } Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, {}", tag_reg, runtime_value_tag(&PhpType::Mixed))); // check whether the ref-cell stores a boxed Mixed handle + ctx.emitter.instruction(&format!("je {}", mixed_cell_label)); // retain and forward boxed Mixed values without reboxing their pointer ctx.emitter.instruction(&format!("cmp {}, 1", tag_reg)); // check whether the referenced value is a string slot ctx.emitter.instruction(&format!("je {}", string_hi_label)); // load string length only for string ref-cells } @@ -936,6 +942,14 @@ fn emit_box_loaded_invoker_ref_cell_value_as_mixed( ctx.emitter.label(&box_label); emit_box_runtime_payload_as_mixed(ctx.emitter, tag_reg, lo_reg, hi_reg); + abi::emit_jump(ctx.emitter, &done_label); + + ctx.emitter.label(&mixed_cell_label); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, result_reg, lo_reg); + abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Mixed); + + ctx.emitter.label(&done_label); } /// Branches when a loaded Mixed tag is an invoker ref-cell marker. @@ -1514,6 +1528,7 @@ fn lower_mixed_array_set_x86_64( /// Stores a fresh boxed-Mixed value through an invoker ref-cell marker on AArch64. fn emit_mixed_array_set_ref_marker_writeback_aarch64(ctx: &mut FunctionContext<'_>) { let runtime_label = ctx.next_label("mixed_array_set_runtime"); + let mixed_cell_label = ctx.next_label("mixed_array_set_ref_mixed_cell"); let done_label = ctx.next_label("mixed_array_set_done"); ctx.emitter.instruction("cmp x1, #0"); // reject negative indexes before checking for by-reference markers @@ -1527,7 +1542,10 @@ fn emit_mixed_array_set_ref_marker_writeback_aarch64(ctx: &mut FunctionContext<' ctx.emitter.instruction("ldr x12, [x11]"); // load the existing Mixed tag for marker detection ctx.emitter.instruction(&format!("cmp x12, #{}", INVOKER_ARG_REF_CELL_TAG)); // check whether the slot aliases caller storage ctx.emitter.instruction(&format!("b.ne {}", runtime_label)); // ordinary boxed Mixed slots are replaced by the runtime setter + ctx.emitter.instruction("ldr x12, [x11, #16]"); // load the source runtime tag carried by the by-reference marker ctx.emitter.instruction("ldr x10, [x11, #8]"); // load the caller ref-cell address from the marker payload + ctx.emitter.instruction(&format!("cmp x12, #{}", runtime_value_tag(&PhpType::Mixed))); // check whether the caller ref-cell stores a boxed Mixed handle + ctx.emitter.instruction(&format!("b.eq {}", mixed_cell_label)); // transfer boxed Mixed replacements as handles rather than payload words ctx.emitter.instruction("ldr x12, [x2, #8]"); // load the replacement Mixed low payload word ctx.emitter.instruction("str x12, [x10]"); // write the replacement low word through the caller ref-cell ctx.emitter.instruction("ldr x12, [x2, #16]"); // load the replacement Mixed high payload word @@ -1538,6 +1556,10 @@ fn emit_mixed_array_set_ref_marker_writeback_aarch64(ctx: &mut FunctionContext<' ctx.emitter.instruction("ldr x0, [sp], #16"); // restore the array pointer as the ArraySet result ctx.emitter.instruction(&format!("b {}", done_label)); // skip the runtime setter after marker write-through + ctx.emitter.label(&mixed_cell_label); + ctx.emitter.instruction("str x2, [x10]"); // transfer the fresh boxed Mixed handle into the caller ref-cell + ctx.emitter.instruction(&format!("b {}", done_label)); // skip the runtime setter after handle transfer + ctx.emitter.label(&runtime_label); abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); ctx.emitter.label(&done_label); @@ -1546,6 +1568,7 @@ fn emit_mixed_array_set_ref_marker_writeback_aarch64(ctx: &mut FunctionContext<' /// Stores a fresh boxed-Mixed value through an invoker ref-cell marker on x86_64. fn emit_mixed_array_set_ref_marker_writeback_x86_64(ctx: &mut FunctionContext<'_>) { let runtime_label = ctx.next_label("mixed_array_set_runtime"); + let mixed_cell_label = ctx.next_label("mixed_array_set_ref_mixed_cell"); let done_label = ctx.next_label("mixed_array_set_done"); ctx.emitter.instruction("cmp rsi, 0"); // reject negative indexes before checking for by-reference markers @@ -1559,7 +1582,10 @@ fn emit_mixed_array_set_ref_marker_writeback_x86_64(ctx: &mut FunctionContext<'_ ctx.emitter.instruction("mov r11, QWORD PTR [r10]"); // load the existing Mixed tag for marker detection ctx.emitter.instruction(&format!("cmp r11, {}", INVOKER_ARG_REF_CELL_TAG)); // check whether the slot aliases caller storage ctx.emitter.instruction(&format!("jne {}", runtime_label)); // ordinary boxed Mixed slots are replaced by the runtime setter + ctx.emitter.instruction("mov r11, QWORD PTR [r10 + 16]"); // load the source runtime tag carried by the by-reference marker ctx.emitter.instruction("mov r10, QWORD PTR [r10 + 8]"); // load the caller ref-cell address from the marker payload + ctx.emitter.instruction(&format!("cmp r11, {}", runtime_value_tag(&PhpType::Mixed))); // check whether the caller ref-cell stores a boxed Mixed handle + ctx.emitter.instruction(&format!("je {}", mixed_cell_label)); // transfer boxed Mixed replacements as handles rather than payload words ctx.emitter.instruction("mov r11, QWORD PTR [rdx + 8]"); // load the replacement Mixed low payload word ctx.emitter.instruction("mov QWORD PTR [r10], r11"); // write the replacement low word through the caller ref-cell ctx.emitter.instruction("mov r11, QWORD PTR [rdx + 16]"); // load the replacement Mixed high payload word @@ -1570,6 +1596,11 @@ fn emit_mixed_array_set_ref_marker_writeback_x86_64(ctx: &mut FunctionContext<'_ abi::emit_pop_reg(ctx.emitter, "rax"); ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the runtime setter after marker write-through + ctx.emitter.label(&mixed_cell_label); + ctx.emitter.instruction("mov QWORD PTR [r10], rdx"); // transfer the fresh boxed Mixed handle into the caller ref-cell + ctx.emitter.instruction("mov rax, rdi"); // return the unchanged indexed array after marker handle transfer + ctx.emitter.instruction(&format!("jmp {}", done_label)); // skip the runtime setter after handle transfer + ctx.emitter.label(&runtime_label); abi::emit_call_label(ctx.emitter, "__rt_array_set_mixed"); ctx.emitter.label(&done_label); diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index f6f7ad7d3f..6f28cdb498 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -9,7 +9,7 @@ //! - Fixtures verify by-reference writeback through string, callable-array, and //! first-class callable forms instead of only direct method/function syntax. -use crate::support::compile_and_run; +use crate::support::{compile_and_run, compile_and_run_capture}; /// Verifies eval string and first-class AOT function callables preserve by-ref writeback. #[test] @@ -302,6 +302,117 @@ return call_user_func_array($closure, $args) . ":" . $i . ":" . $j . ":" . ); } +/// Verifies AOT function callable forms preserve by-ref variadic element writeback. +#[test] +fn test_eval_aot_function_callable_forms_preserve_by_ref_variadic_writeback() { + let out = compile_and_run_capture( + r#"collect(...); +$c = "C"; +$d = "D"; +echo $first($c, $d) . ":" . $c . ":" . $d . "|"; + +$closure = Closure::fromCallable([$box, "collect"]); +$e = "E"; +$f = "F"; +echo $closure($e, $f) . ":" . $e . ":" . $f . "|"; + +$string = "EvalAotVariadicRefCallableBox::collectStatic"; +$g = "G"; +$h = "H"; +echo $string($g, $h) . ":" . $g . ":" . $h . "|"; + +$static = EvalAotVariadicRefCallableBox::collectStatic(...); +$i = "I"; +$j = "J"; +echo $static($i, $j) . ":" . $i . ":" . $j . "|"; + +$k = "K"; +$l = "L"; +$args = [&$k, &$l]; +return call_user_func_array($closure, $args) . ":" . $k . ":" . $l . ":" . + $args[0] . ":" . $args[1];'); +"#, + ); + + assert_eq!( + out, + concat!( + "A-i:B-j:A-i:B-j|C-i:D-j:C-i:D-j|E-i:F-j:E-i:F-j|", + "G-s:H-t:G-s:H-t|I-s:J-t:I-s:J-t|K-i:L-j:K-i:L-j:K-i:L-j" + ) + ); +} + /// Verifies eval first-class callables are PHP-visible `Closure` objects and remain invokable. #[test] fn test_eval_first_class_callables_are_php_closure_objects() { diff --git a/tests/codegen/eval_constructors.rs b/tests/codegen/eval_constructors.rs index fc28ac200a..3910d44eb5 100644 --- a/tests/codegen/eval_constructors.rs +++ b/tests/codegen/eval_constructors.rs @@ -140,6 +140,32 @@ return $box->name;'); assert_eq!(out, "A-ctor|B-head:tail|C-ctor"); } +/// Verifies AOT constructor by-reference variadic args write back caller variables. +#[test] +fn test_eval_dynamic_new_constructor_by_ref_variadic_writeback() { + let out = compile_and_run( + r#"label = $items[0] . ":" . $items[1]; + } +} + +echo eval('$a = "A"; +$b = "B"; +$box = new EvalCtorVariadicRefBridge($a, $b); +echo $box->label . "|"; +return $a . ":" . $b;'); +"#, + ); + + assert_eq!(out, "A-ctor:B-tail|A-ctor:B-tail"); +} + /// Verifies AOT constructor by-reference writeback happens before a catchable throw. #[test] fn test_eval_dynamic_new_constructor_by_ref_lvalue_writeback_before_throw() { From 755d9894951b88f9d866caf72a3e309178a6740c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 07:50:10 +0200 Subject: [PATCH 1022/1208] Handle AOT function throws in eval callables --- .../src/interpreter/dynamic_functions.rs | 11 +- .../src/interpreter/runtime_ops.rs | 12 ++ .../elephc-magician/src/runtime_hooks/ops.rs | 8 + src/codegen/lower_inst/builtins/eval.rs | 29 +++- src/codegen/runtime_callable_invoker.rs | 159 +++++++++++++++++- tests/codegen/eval_callable_ref_errors.rs | 145 ++++++++++++++++ 6 files changed, 354 insertions(+), 10 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 5784e8cd8d..78c515c611 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -2377,11 +2377,14 @@ pub(super) fn eval_native_function_with_values( cleanup_native_function_ref_args(&bound_args, values)?; return Err(status); } - write_back_native_function_ref_args(&bound_args, context, values)?; - if result.is_null() { - return Err(EvalStatus::RuntimeFatal); + let result = values.native_call_result(result); + let writeback = write_back_native_function_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => { + eval_declared_native_return_value(function.return_type(), None, None, result, context, values) + } } - eval_declared_native_return_value(function.return_type(), None, None, result, context, values) } /// Builds the positional runtime array passed to descriptor-compatible native invokers. diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index 5413f80574..be5962b7a8 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -168,6 +168,18 @@ pub trait RuntimeValueOps { args: Vec, ) -> Result; + /// Converts a generated native call result into an eval result or pending throwable status. + fn native_call_result( + &mut self, + result: RuntimeCellHandle, + ) -> Result { + if result.is_null() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(result) + } + } + /// Materializes a synthetic `ReflectionAttribute` object through generated private-layout code. fn reflection_attribute_new( &mut self, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 9d342f7628..e8588f1f5b 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -318,6 +318,14 @@ impl RuntimeValueOps for ElephcRuntimeOps { self.handle_native_call_result(result) } + /// Converts a native free-function result into eval status, preserving pending throwables. + fn native_call_result( + &mut self, + result: RuntimeCellHandle, + ) -> Result { + self.handle_native_call_result(result.as_ptr()) + } + /// Materializes a populated synthetic `ReflectionAttribute` object for eval metadata. fn reflection_attribute_new( &mut self, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 8481c6907d..c3155a22a0 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -16,6 +16,7 @@ use std::path::Path; use crate::codegen::eval_ref_arg_helpers::eval_signature_ref_params_supported; use crate::codegen::platform::Arch; +use crate::codegen::runtime_callable_invoker::RuntimeCallableInvoker; use crate::codegen::{ abi, callable_descriptor, emit_box_current_value_as_mixed, CodegenIrError, Result, }; @@ -29,8 +30,7 @@ use crate::types::{ use super::super::super::context::FunctionContext; use super::super::{ - emit_runtime_callable_invoker_inline, expect_data, expect_operand, function_signature_from_eir, - store_if_result, + expect_data, expect_operand, function_signature_from_eir, store_if_result, }; const EVAL_STATUS_PARSE_ERROR: i64 = 1; @@ -3475,7 +3475,7 @@ fn register_eval_native_function( context_offset: usize, registration: &EvalNativeFunctionRegistration, ) -> Result<()> { - let invoker_label = emit_runtime_callable_invoker_inline(ctx, ®istration.signature, &[]); + let invoker_label = emit_eval_native_function_invoker_inline(ctx, ®istration.signature); let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( ctx.data, &function_symbol(®istration.name), @@ -3593,6 +3593,29 @@ fn register_eval_native_function( Ok(()) } +/// Emits an eval-safe descriptor invoker for a registered native free function. +fn emit_eval_native_function_invoker_inline( + ctx: &mut FunctionContext<'_>, + sig: &FunctionSig, +) -> String { + let label = ctx.next_label("eval_callable_invoker"); + let done_label = ctx.next_label("eval_callable_invoker_done"); + let captures: [(String, PhpType, bool); 0] = []; + let invoker = RuntimeCallableInvoker { + label: &label, + sig, + captures: &captures, + }; + abi::emit_jump(ctx.emitter, &done_label); + crate::codegen::runtime_callable_invoker::emit_runtime_callable_invoker_with_exception_boundary( + ctx.emitter, + ctx.data, + &invoker, + ); + ctx.emitter.label(&done_label); + label +} + /// Emits one native method signature registration call into the eval context. fn register_eval_native_method( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/runtime_callable_invoker.rs b/src/codegen/runtime_callable_invoker.rs index ab96d0baed..fac72f1b95 100644 --- a/src/codegen/runtime_callable_invoker.rs +++ b/src/codegen/runtime_callable_invoker.rs @@ -20,12 +20,18 @@ use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::codegen::{abi, emit_box_current_value_as_mixed, emit_box_runtime_payload_as_mixed}; +use crate::codegen_support::try_handlers::{ + TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, TRY_HANDLER_SLOT_SIZE, +}; use crate::parser::ast::{Expr, ExprKind}; use crate::types::{FunctionSig, PhpType}; const INVOKER_DESCRIPTOR_OFFSET: usize = 8; const INVOKER_CONCAT_OFFSET: usize = 16; const INVOKER_FRAME_SIZE: usize = 32; +const INVOKER_ARG_ARRAY_OFFSET: usize = 24; +const INVOKER_BOUNDARY_FRAME_SIZE: usize = INVOKER_FRAME_SIZE + TRY_HANDLER_SLOT_SIZE + 16; +const INVOKER_BOUNDARY_BASE_OFFSET: usize = INVOKER_BOUNDARY_FRAME_SIZE - 16; /// Runtime invoker metadata emitted beside callable descriptors. pub(super) struct RuntimeCallableInvoker<'a> { @@ -76,21 +82,65 @@ pub(super) fn emit_runtime_callable_invoker( emitter: &mut Emitter, data: &mut DataSection, invoker: &RuntimeCallableInvoker<'_>, +) { + emit_runtime_callable_invoker_impl(emitter, data, invoker, false); +} + +/// Emits a descriptor invoker wrapper that catches native throws for eval callbacks. +pub(crate) fn emit_runtime_callable_invoker_with_exception_boundary( + emitter: &mut Emitter, + data: &mut DataSection, + invoker: &RuntimeCallableInvoker<'_>, +) { + emit_runtime_callable_invoker_impl(emitter, data, invoker, true); +} + +/// Emits a descriptor invoker wrapper, optionally bounded by an exception handler. +fn emit_runtime_callable_invoker_impl( + emitter: &mut Emitter, + data: &mut DataSection, + invoker: &RuntimeCallableInvoker<'_>, + catch_native_throws: bool, ) { let mut ctx = InvokerEmitContext::new(invoker.label); let call_reg = abi::nested_call_reg(emitter); + let escape_label = format!("{}_eval_escape", invoker.label); + let frame_size = if catch_native_throws { + INVOKER_BOUNDARY_FRAME_SIZE + } else { + INVOKER_FRAME_SIZE + }; emitter.blank(); emitter.comment(&format!("runtime callable invoker {}", invoker.label)); emitter.raw(".align 2"); emitter.label_global(invoker.label); - abi::emit_frame_prologue(emitter, INVOKER_FRAME_SIZE); + abi::emit_frame_prologue(emitter, frame_size); abi::store_at_offset( emitter, abi::int_arg_reg_name(emitter.target, 0), INVOKER_DESCRIPTOR_OFFSET, ); - emit_descriptor_entry_to_call_reg(emitter, call_reg); + if catch_native_throws { + abi::store_at_offset( + emitter, + abi::int_arg_reg_name(emitter.target, 1), + INVOKER_ARG_ARRAY_OFFSET, + ); + emit_invoker_exception_boundary_push( + emitter, + INVOKER_BOUNDARY_BASE_OFFSET, + &escape_label, + ); + abi::load_at_offset( + emitter, + abi::int_arg_reg_name(emitter.target, 1), + INVOKER_ARG_ARRAY_OFFSET, + ); + emit_saved_descriptor_entry_to_call_reg(emitter, call_reg); + } else { + emit_descriptor_entry_to_call_reg(emitter, call_reg); + } let ret_ty = emit_loaded_array_callback_call( LoadedArraySource::ArgumentRegister(1), @@ -103,8 +153,18 @@ pub(super) fn emit_runtime_callable_invoker( data, ); emit_box_current_value_as_mixed(emitter, &ret_ty.codegen_repr()); - abi::emit_frame_restore(emitter, INVOKER_FRAME_SIZE); + if catch_native_throws { + emit_invoker_exception_boundary_pop(emitter, INVOKER_BOUNDARY_BASE_OFFSET); + } + abi::emit_frame_restore(emitter, frame_size); abi::emit_return(emitter); + if catch_native_throws { + emitter.label(&escape_label); + emit_invoker_exception_boundary_pop(emitter, INVOKER_BOUNDARY_BASE_OFFSET); + emit_null_invoker_result(emitter); + abi::emit_frame_restore(emitter, frame_size); + abi::emit_return(emitter); + } } /// Loads the descriptor entry slot from the first invoker argument into `call_reg`. @@ -120,6 +180,99 @@ fn emit_descriptor_entry_to_call_reg(emitter: &mut Emitter, call_reg: &str) { callable_descriptor::emit_load_entry_from_descriptor(emitter, call_reg, call_reg); } +/// Loads the saved descriptor entry slot into `call_reg` after a `setjmp` boundary. +fn emit_saved_descriptor_entry_to_call_reg(emitter: &mut Emitter, call_reg: &str) { + abi::load_at_offset(emitter, call_reg, INVOKER_DESCRIPTOR_OFFSET); + callable_descriptor::emit_load_entry_from_descriptor(emitter, call_reg, call_reg); +} + +/// Pushes a native exception boundary around an eval-owned descriptor invoker call. +fn emit_invoker_exception_boundary_push( + emitter: &mut Emitter, + handler_base: usize, + escape_label: &str, +) { + emitter.comment("push eval callable exception boundary"); + match emitter.target.arch { + Arch::AArch64 => { + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!("stur x10, [x29, #-{}]", handler_base)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "x10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("stur x10, [x29, #-{}]", handler_base - 8)); // preserve the caller activation frame across callable unwinding + abi::emit_load_symbol_to_reg(emitter, "x10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "stur x10, [x29, #-{}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("sub x10, x29, #{}", handler_base)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "sub x0, x29, #{}", + handler_base - TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering the callable + emitter.instruction(&format!("cbnz x0, {}", escape_label)); // non-zero setjmp result means a callable Throwable escaped + } + Arch::X86_64 => { + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base)); // save the previous native exception-handler head + abi::emit_load_symbol_to_reg(emitter, "r10", "_exc_call_frame_top", 0); + emitter.instruction(&format!("mov QWORD PTR [rbp - {}], r10", handler_base - 8)); // preserve the caller activation frame across callable unwinding + abi::emit_load_symbol_to_reg(emitter, "r10", "_rt_diag_suppression", 0); + emitter.instruction(&format!( + "mov QWORD PTR [rbp - {}], r10", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // save diagnostic suppression depth for restoration + emitter.instruction(&format!("lea r10, [rbp - {}]", handler_base)); // compute the boundary handler record address + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "lea rdi, [rbp - {}]", + handler_base - TRY_HANDLER_JMP_BUF_OFFSET + )); // pass the boundary jmp_buf to setjmp + emitter.bl_c("setjmp"); // snapshot the bridge stack before entering the callable + emitter.instruction("test eax, eax"); // did control arrive through longjmp? + emitter.instruction(&format!("jne {}", escape_label)); // non-zero setjmp result means a callable Throwable escaped + } + } +} + +/// Pops the native exception boundary around an eval-owned descriptor invoker call. +fn emit_invoker_exception_boundary_pop(emitter: &mut Emitter, handler_base: usize) { + emitter.comment("pop eval callable exception boundary"); + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction(&format!("ldur x10, [x29, #-{}]", handler_base)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "x10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "ldur x10, [x29, #-{}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "x10", "_rt_diag_suppression", 0); + } + Arch::X86_64 => { + emitter.instruction(&format!("mov r10, QWORD PTR [rbp - {}]", handler_base)); // reload the previous native exception-handler head + abi::emit_store_reg_to_symbol(emitter, "r10", "_exc_handler_top", 0); + emitter.instruction(&format!( + "mov r10, QWORD PTR [rbp - {}]", + handler_base - TRY_HANDLER_DIAG_DEPTH_OFFSET + )); // reload the saved diagnostic suppression depth + abi::emit_store_reg_to_symbol(emitter, "r10", "_rt_diag_suppression", 0); + } + } +} + +/// Leaves a null boxed-Mixed result for Rust to translate into a pending throwable. +fn emit_null_invoker_result(emitter: &mut Emitter) { + match emitter.target.arch { + Arch::AArch64 => { + emitter.instruction("mov x0, xzr"); // return null so magician takes the pending Throwable + } + Arch::X86_64 => { + emitter.instruction("xor eax, eax"); // return null so magician takes the pending Throwable + } + } +} + /// Source location for a callback argument array already materialized by caller code. #[derive(Clone, Copy)] enum LoadedArraySource { diff --git a/tests/codegen/eval_callable_ref_errors.rs b/tests/codegen/eval_callable_ref_errors.rs index b203dcd2a2..43430dc3aa 100644 --- a/tests/codegen/eval_callable_ref_errors.rs +++ b/tests/codegen/eval_callable_ref_errors.rs @@ -11,6 +11,151 @@ use crate::support::{compile_and_run, compile_and_run_capture}; +/// Verifies AOT function callables write back by-reference args before catchable throws. +#[test] +fn test_eval_aot_function_callables_write_back_by_ref_args_before_throw() { + let out = compile_and_run_capture( + r#"getMessage() . ":" . gettype($a) . ":" . $a . "|"; +} + +$first = eval_aot_throw_ref_add(...); +$b = "4"; +try { + $first($b, 5); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($b) . ":" . $b . "|"; +} + +$closure = Closure::fromCallable("eval_aot_throw_ref_add"); +$c = "6"; +try { + $closure($c, 7); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($c) . ":" . $c . "|"; +} + +$d = "8"; +try { + call_user_func_array($closure, [&$d, 9]); + echo "bad"; +} catch (Throwable $e) { + echo get_class($e) . ":" . $e->getMessage() . ":" . gettype($d) . ":" . $d; +}'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "Exception:aot-function:integer:5|Exception:aot-function:integer:9|Exception:aot-function:integer:13|Exception:aot-function:integer:17" + ); +} + +/// Verifies AOT function argument-prep fatals restore the eval bridge frame. +#[test] +fn test_eval_aot_function_by_ref_arg_prep_fatal_cleans_up_stack() { + let cases = [ + ( + "string callable", + r#" Date: Wed, 1 Jul 2026 08:04:30 +0200 Subject: [PATCH 1023/1208] Cover eval call_user_func AOT callable by-value args --- tests/codegen/eval_callables.rs | 62 +++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 6f28cdb498..a4ceafdc0e 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -172,6 +172,68 @@ return $invokable(delta: 13, value: $f) . ":" . gettype($f) . ":" . $f;'); ); } +/// Verifies eval `call_user_func()` keeps AOT callable by-reference args by value. +#[test] +fn test_eval_call_user_func_aot_callable_forms_use_by_value_args() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +echo eval('$string = "eval_call_user_func_aot_ref_add"; +$a = "2"; +echo call_user_func($string, $a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$first = eval_call_user_func_aot_ref_add(...); +$b = "4"; +echo call_user_func($first, $b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$box = new EvalCallUserFuncAotRefBox(); +$array = [$box, "bump"]; +$c = "6"; +echo call_user_func($array, $c, 7) . ":" . gettype($c) . ":" . $c . "|"; + +$staticArray = ["EvalCallUserFuncAotRefBox", "add"]; +$d = "8"; +echo call_user_func($staticArray, $d, 9) . ":" . gettype($d) . ":" . $d . "|"; + +$staticString = "EvalCallUserFuncAotRefBox::add"; +$e = "10"; +echo call_user_func($staticString, $e, 11) . ":" . gettype($e) . ":" . $e . "|"; + +$invokable = new EvalCallUserFuncAotRefBox(); +$f = "12"; +return call_user_func($invokable, $f, 13) . ":" . gettype($f) . ":" . $f;'); +"#, + ); + + assert_eq!( + out, + "5:string:2|9:string:4|23:string:6|17:string:8|21:string:10|35:string:12" + ); +} + /// Verifies eval-declared callable forms preserve by-ref writeback. #[test] fn test_eval_declared_callable_forms_preserve_by_ref_writeback() { From 6fcde58c7f8b4ac9f2c5542b8d83c15f99dafbc5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 08:16:53 +0200 Subject: [PATCH 1024/1208] Cover eval call_user_func ref-like builtin closures --- tests/codegen/eval_builtin_parity.rs | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 2fb153c3c2..2e673688a1 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -285,6 +285,35 @@ foreach ($assoc as $key => $entry) { assert_eq!(out, "S1,2,3|Tinteger:42|1:ab:a:b|Ka1b2"); } +/// Verifies eval `call_user_func()` keeps ref-like builtin Closure args by value. +#[test] +fn test_eval_call_user_func_ref_like_builtin_closures_use_by_value_args() { + let out = compile_and_run( + r#" Date: Wed, 1 Jul 2026 08:28:11 +0200 Subject: [PATCH 1025/1208] Cover eval call_user_func_array non-ref builtin closures --- tests/codegen/eval_builtin_parity.rs | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 2e673688a1..86c7e00b95 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -314,6 +314,40 @@ echo call_user_func($push, $front, "b") . ":" . implode(",", $front);'); assert_eq!(out, "S:3,1,2|T:string:42|1:0|2:a"); } +/// Verifies eval `call_user_func_array()` keeps non-reference builtin Closure args by value. +#[test] +fn test_eval_call_user_func_array_ref_like_builtin_closures_keep_non_ref_args_by_value() { + let out = compile_and_run( + r#" Date: Wed, 1 Jul 2026 08:47:05 +0200 Subject: [PATCH 1026/1208] Cover eval call_user_func_array named AOT refs --- tests/codegen/eval_callables.rs | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index a4ceafdc0e..46af6b0345 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -172,6 +172,79 @@ return $invokable(delta: 13, value: $f) . ":" . gettype($f) . ":" . $f;'); ); } +/// Verifies eval `call_user_func_array()` preserves named AOT by-ref argument aliases. +#[test] +fn test_eval_call_user_func_array_aot_callable_named_ref_args_preserve_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +echo eval('$function = "eval_call_array_aot_named_ref_add"; +$a = "2"; +echo call_user_func_array($function, ["delta" => 3, "value" => &$a]) . + ":" . gettype($a) . ":" . $a . "|"; + +$first = eval_call_array_aot_named_ref_add(...); +$b = "4"; +echo call_user_func_array($first, ["delta" => 5, "value" => &$b]) . + ":" . gettype($b) . ":" . $b . "|"; + +$box = new EvalCallArrayAotNamedRefBox(); +$array = [$box, "bump"]; +$c = "6"; +echo call_user_func_array($array, ["delta" => 7, "value" => &$c]) . + ":" . gettype($c) . ":" . $c . "|"; + +$closure = Closure::fromCallable([$box, "bump"]); +$d = "8"; +echo call_user_func_array($closure, ["value" => &$d, "delta" => 9]) . + ":" . gettype($d) . ":" . $d . "|"; + +$string = "EvalCallArrayAotNamedRefBox::add"; +$e = "10"; +echo call_user_func_array($string, ["delta" => 11, "value" => &$e]) . + ":" . gettype($e) . ":" . $e . "|"; + +$static = EvalCallArrayAotNamedRefBox::add(...); +$f = "12"; +echo call_user_func_array($static, ["value" => &$f, "delta" => 13]) . + ":" . gettype($f) . ":" . $f . "|"; + +$invokable = new EvalCallArrayAotNamedRefBox(); +$g = "14"; +return call_user_func_array($invokable, ["delta" => 15, "value" => &$g]) . + ":" . gettype($g) . ":" . $g;'); +"#, + ); + + assert_eq!( + out, + "5:integer:5|9:integer:9|23:integer:23|27:integer:27|21:integer:21|25:integer:25|39:integer:39" + ); +} + /// Verifies eval `call_user_func()` keeps AOT callable by-reference args by value. #[test] fn test_eval_call_user_func_aot_callable_forms_use_by_value_args() { From c31c59fc65812195990df2a1ca0d925f13e22770 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 09:17:10 +0200 Subject: [PATCH 1027/1208] Cover eval-declared call_user_func_array named refs --- tests/codegen/eval_callables.rs | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 46af6b0345..2e3aa7063f 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -245,6 +245,79 @@ return call_user_func_array($invokable, ["delta" => 15, "value" => &$g]) . ); } +/// Verifies eval `call_user_func_array()` preserves named eval-declared by-ref aliases. +#[test] +fn test_eval_call_user_func_array_declared_callable_named_ref_args_preserve_writeback() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public static function add(int &$value, int $delta): int { + $value = $value + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +$function = "eval_call_array_declared_named_ref_add"; +$a = "2"; +echo call_user_func_array($function, ["delta" => 3, "value" => &$a]) . + ":" . gettype($a) . ":" . $a . "|"; + +$first = eval_call_array_declared_named_ref_add(...); +$b = "4"; +echo call_user_func_array($first, ["delta" => 5, "value" => &$b]) . + ":" . gettype($b) . ":" . $b . "|"; + +$box = new EvalCallArrayDeclaredNamedRefBox(); +$array = [$box, "bump"]; +$c = "6"; +echo call_user_func_array($array, ["delta" => 7, "value" => &$c]) . + ":" . gettype($c) . ":" . $c . "|"; + +$closure = Closure::fromCallable([$box, "bump"]); +$d = "8"; +echo call_user_func_array($closure, ["value" => &$d, "delta" => 9]) . + ":" . gettype($d) . ":" . $d . "|"; + +$string = "EvalCallArrayDeclaredNamedRefBox::add"; +$e = "10"; +echo call_user_func_array($string, ["delta" => 11, "value" => &$e]) . + ":" . gettype($e) . ":" . $e . "|"; + +$static = EvalCallArrayDeclaredNamedRefBox::add(...); +$f = "12"; +echo call_user_func_array($static, ["value" => &$f, "delta" => 13]) . + ":" . gettype($f) . ":" . $f . "|"; + +$invokable = new EvalCallArrayDeclaredNamedRefBox(); +$g = "14"; +return call_user_func_array($invokable, ["delta" => 15, "value" => &$g]) . + ":" . gettype($g) . ":" . $g;'); +"#, + ); + + assert_eq!( + out, + "5:integer:5|9:integer:9|23:integer:23|27:integer:27|21:integer:21|25:integer:25|39:integer:39" + ); +} + /// Verifies eval `call_user_func()` keeps AOT callable by-reference args by value. #[test] fn test_eval_call_user_func_aot_callable_forms_use_by_value_args() { From 63e1008aa503a43f9bb89b794b29cfc254ad621e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 09:29:44 +0200 Subject: [PATCH 1028/1208] Cover eval static Closure bind fromCallable --- tests/codegen/eval_callables.rs | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 2e3aa7063f..b3591e3fd8 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -2129,3 +2129,50 @@ echo is_null($static->bindTo($bound)) ? "S" : "s";'); assert_eq!(out, "25:integer:25|29:integer:29|S"); } + +/// Verifies static `Closure::bind()` persists rebinding for `fromCallable()` targets. +#[test] +fn test_eval_static_closure_bind_from_callable_persists_method_and_invokable_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } + + public static function add(int $value): int { + return $value + 1; + } +} + +echo eval('$original = new EvalStaticFromCallableBindBox(); +$original->base = 10; +$bound = new EvalStaticFromCallableBindBox(); +$bound->base = 20; + +$rawMethod = Closure::fromCallable([$original, "bump"]); +$method = Closure::bind(closure: $rawMethod, newThis: $bound); +$a = "2"; +echo $method(delta: 3, value: $a) . ":" . gettype($a) . ":" . $a . "|"; + +$rawInvoke = Closure::fromCallable($original); +$invoke = Closure::bind($rawInvoke, $bound); +$b = "4"; +echo call_user_func_array($invoke, ["delta" => 5, "value" => &$b]) . + ":" . gettype($b) . ":" . $b . "|"; + +$static = Closure::fromCallable(["EvalStaticFromCallableBindBox", "add"]); +echo is_null(Closure::bind($static, $bound)) ? "S" : "s";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|S"); +} From 479f79c198834a1e802cd90d5041aca7dacb02cb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 09:52:07 +0200 Subject: [PATCH 1029/1208] Align eval Closure bind function callables --- .../interpreter/builtins/registry/callable.rs | 38 ++++++++++-------- .../src/interpreter/builtins/symbols.rs | 4 +- .../src/interpreter/statements.rs | 11 ++++++ tests/codegen/eval_callables.rs | 39 +++++++++++++++++++ 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index f78c960768..1f4e0ef7ef 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -531,10 +531,9 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_values( bound_this, bound_scope, } => { - let closure = context - .closure(name) - .cloned() - .ok_or(EvalStatus::UnsupportedConstruct)?; + let Some(closure) = context.closure(name).cloned() else { + return eval_callable_with_values(name, evaluated_args, context, values); + }; eval_bound_closure_with_call_args( &closure, *bound_this, @@ -722,10 +721,14 @@ fn eval_evaluated_callable_with_call_user_func_values( bound_this, bound_scope, } => { - let closure = context - .closure(name) - .cloned() - .ok_or(EvalStatus::UnsupportedConstruct)?; + let Some(closure) = context.closure(name).cloned() else { + return eval_named_callable_with_call_user_func_values( + name, + evaluated_args, + context, + values, + ); + }; let evaluated_args = positional_args(evaluated_args); let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( closure.function().name(), @@ -827,10 +830,14 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_by_value_call_args( bound_this, bound_scope, } => { - let closure = context - .closure(name) - .cloned() - .ok_or(EvalStatus::UnsupportedConstruct)?; + let Some(closure) = context.closure(name).cloned() else { + return eval_named_callable_with_call_user_func_args( + name, + evaluated_args, + context, + values, + ); + }; let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( closure.function().name(), closure.function().params(), @@ -1475,10 +1482,9 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( bound_this, bound_scope, } => { - let closure = context - .closure(name) - .cloned() - .ok_or(EvalStatus::UnsupportedConstruct)?; + let Some(closure) = context.closure(name).cloned() else { + return eval_callable_with_call_array_args(name, evaluated_args, context, values); + }; eval_bound_closure_with_call_args_ref_mode( &closure, *bound_this, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index dddfbc657e..62802b31f0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -1313,7 +1313,9 @@ fn eval_callable_probe_exists( EvaluatedCallable::Named { name, .. } => { Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) } - EvaluatedCallable::BoundClosure { name, .. } => Ok(context.has_closure(name)), + EvaluatedCallable::BoundClosure { name, .. } => { + Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) + } EvaluatedCallable::InvokableObject { object } => { eval_object_method_callable_probe(*object, "__invoke", context, values) } diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index fded50e3ef..37fd1eb6ab 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7718,6 +7718,17 @@ fn eval_closure_bind_target( match target { EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { let Some(closure) = context.closure(&name) else { + if eval_function_probe_exists(context, &name) { + return eval_closure_object_from_target( + EvalClosureObjectTarget::BoundNamed { + name, + bound_this: Some(bound_this), + bound_scope, + }, + context, + values, + ); + } return eval_closure_call_warning_null( "Cannot rebind scope of closure created from function", values, diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index b3591e3fd8..8746ade6c2 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -2176,3 +2176,42 @@ echo is_null(Closure::bind($static, $bound)) ? "S" : "s";'); assert_eq!(out, "25:integer:25|29:integer:29|S"); } + +/// Verifies binding function `fromCallable()` closures returns callable closures. +#[test] +fn test_eval_closure_bind_from_callable_function_targets_remain_callable() { + let out = compile_and_run( + r#"bindTo($box); +echo is_object($aotBoundTo) ? get_class($aotBoundTo) . ":" . $aotBoundTo("x") : "bad"; +echo "|"; + +$aotBound = Closure::bind(closure: $aot, newThis: $box); +echo is_object($aotBound) ? get_class($aotBound) . ":" . $aotBound("y") : "bad"; +echo "|"; + +$eval = Closure::fromCallable("eval_declared_bind_from_callable_function_target"); +$evalBoundTo = $eval->bindTo($box); +echo is_object($evalBoundTo) ? get_class($evalBoundTo) . ":" . $evalBoundTo("u") : "bad"; +echo "|"; + +$evalBound = Closure::bind($eval, $box); +return is_object($evalBound) ? get_class($evalBound) . ":" . $evalBound("v") : "bad";'); +"#, + ); + + assert_eq!(out, "Closure:A:x|Closure:A:y|Closure:E:u|Closure:E:v"); +} From 5832d688b6022faf24a1cfc0c3a93d4931ba6510 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 10:24:31 +0200 Subject: [PATCH 1030/1208] Reject eval Closure function scope rebinding --- .../src/interpreter/statements.rs | 98 ++++++++++++++----- docs/php/eval.md | 10 +- tests/codegen/eval_callables.rs | 45 +++++++++ 3 files changed, 126 insertions(+), 27 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 37fd1eb6ab..7584d12cf8 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7555,9 +7555,16 @@ fn eval_closure_bind_static( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - let (target, bound_this, bound_scope) = + let (target, bound_this, bound_scope, rebinds_function_scope) = eval_closure_bind_static_args(evaluated_args, context, values)?; - eval_closure_bind_target(target, bound_this, bound_scope, context, values) + eval_closure_bind_target( + target, + bound_this, + bound_scope, + rebinds_function_scope, + context, + values, + ) } /// Binds static `Closure::bind()` arguments to their PHP parameter slots. @@ -7565,7 +7572,15 @@ fn eval_closure_bind_static_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result<(EvalClosureObjectTarget, Option, Option), EvalStatus> { +) -> Result< + ( + EvalClosureObjectTarget, + Option, + Option, + bool, + ), + EvalStatus, +> { let bound = eval_closure_bind_args( &["closure", "newThis", "newScope"], 2, @@ -7575,8 +7590,9 @@ fn eval_closure_bind_static_args( let new_this = required_closure_bind_arg(&bound, 1)?; let target = eval_closure_target_arg(closure.value, context, values)?; let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; - let bound_scope = eval_closure_bind_scope_arg(bound.get(2), bound_this, context, values)?; - Ok((target, bound_this, bound_scope)) + let (bound_scope, rebinds_function_scope) = + eval_closure_bind_scope_arg(bound.get(2), bound_this, context, values)?; + Ok((target, bound_this, bound_scope, rebinds_function_scope)) } /// Binds `Closure::bindTo()` arguments to their PHP parameter slots. @@ -7584,12 +7600,13 @@ fn eval_closure_bind_to_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result<(Option, Option), EvalStatus> { +) -> Result<(Option, Option, bool), EvalStatus> { let bound = eval_closure_bind_args(&["newThis", "newScope"], 1, evaluated_args)?; let new_this = required_closure_bind_arg(&bound, 0)?; let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; - let bound_scope = eval_closure_bind_scope_arg(bound.get(1), bound_this, context, values)?; - Ok((bound_this, bound_scope)) + let (bound_scope, rebinds_function_scope) = + eval_closure_bind_scope_arg(bound.get(1), bound_this, context, values)?; + Ok((bound_this, bound_scope, rebinds_function_scope)) } /// Binds positional and named Closure binding arguments while accepting optional scope. @@ -7673,34 +7690,39 @@ fn eval_closure_bind_receiver_arg( Ok(Some(new_this)) } -/// Converts the optional `newScope` binding argument into a PHP class scope name. +/// Converts `newScope` into class scope plus whether function scope was rebound. fn eval_closure_bind_scope_arg( new_scope: Option<&Option>, bound_this: Option, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { +) -> Result<(Option, bool), EvalStatus> { let Some(new_scope) = new_scope.and_then(Option::as_ref) else { - return Ok(None); + return Ok((None, false)); }; if values.is_null(new_scope.value)? { - return Ok(None); + return Ok((None, false)); } if values.type_tag(new_scope.value)? == EVAL_TAG_OBJECT { - return eval_closure_bound_object_class_name(new_scope.value, context, values).map(Some); + return eval_closure_bound_object_class_name(new_scope.value, context, values) + .map(|scope| (Some(scope), true)); } let bytes = values.string_bytes(new_scope.value)?; let scope = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; if scope.eq_ignore_ascii_case("static") { let Some(bound_this) = bound_this else { - return Ok(None); + return Ok((None, false)); }; - return eval_closure_bound_object_class_name(bound_this, context, values).map(Some); + return eval_closure_bound_object_class_name(bound_this, context, values) + .map(|scope| (Some(scope), false)); } - Ok(Some( - context - .resolve_class_name(&scope) - .unwrap_or_else(|| scope.trim_start_matches('\\').to_string()), + Ok(( + Some( + context + .resolve_class_name(&scope) + .unwrap_or_else(|| scope.trim_start_matches('\\').to_string()), + ), + true, )) } @@ -7709,16 +7731,29 @@ fn eval_closure_bind_target( target: EvalClosureObjectTarget, bound_this: Option, bound_scope: Option, + rebinds_function_scope: bool, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { let Some(bound_this) = bound_this else { - return eval_closure_unbind_target(target, bound_scope, context, values); + return eval_closure_unbind_target( + target, + bound_scope, + rebinds_function_scope, + context, + values, + ); }; match target { EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { let Some(closure) = context.closure(&name) else { if eval_function_probe_exists(context, &name) { + if rebinds_function_scope { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + } return eval_closure_object_from_target( EvalClosureObjectTarget::BoundNamed { name, @@ -7802,11 +7837,21 @@ fn eval_closure_bind_target( fn eval_closure_unbind_target( target: EvalClosureObjectTarget, bound_scope: Option, + rebinds_function_scope: bool, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match target { EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { + if rebinds_function_scope + && context.closure(&name).is_none() + && eval_function_probe_exists(context, &name) + { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + } let target = match bound_scope { Some(bound_scope) => EvalClosureObjectTarget::BoundNamed { name, @@ -9283,10 +9328,17 @@ fn eval_closure_object_method_result( .map(Some); } if method_name.eq_ignore_ascii_case("bindTo") { - let (bound_this, bound_scope) = + let (bound_this, bound_scope, rebinds_function_scope) = eval_closure_bind_to_args(evaluated_args, context, values)?; - return eval_closure_bind_target(target, bound_this, bound_scope, context, values) - .map(Some); + return eval_closure_bind_target( + target, + bound_this, + bound_scope, + rebinds_function_scope, + context, + values, + ) + .map(Some); } if !method_name.eq_ignore_ascii_case("call") { return Ok(None); diff --git a/docs/php/eval.md b/docs/php/eval.md index cf55010458..5caed65fdc 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -150,10 +150,12 @@ on those closure objects supports same-class method and invokable-object rebinding, passes the call arguments after the receiver by value like PHP, and reports PHP-compatible warning/null results for function and static-method callables. `Closure::bind()` and `Closure::bindTo()` persistently bind eval -closure literals plus same-class method and invokable-object closure targets to -a new receiver. The optional scope argument is accepted, but eval's current -binding model derives method scope from the bound receiver rather than exposing -the full PHP scope-mutation surface. +closure literals, function callable targets, same-class method targets, and +invokable-object closure targets to a new receiver. Function-target closures +accept omitted, `null`, or `"static"` scope, but reject explicit object/class +scope rebinding like PHP. The optional scope argument is accepted for method +closures, but eval's current binding model derives method scope from the bound +receiver rather than exposing the full PHP scope-mutation surface. Closure literals created inside eval are PHP-visible `Closure` objects: they report true for `is_object()`, `get_class($fn)` returns `Closure`, diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 8746ade6c2..5c868782ae 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -2215,3 +2215,48 @@ return is_object($evalBound) ? get_class($evalBound) . ":" . $evalBound("v") : " assert_eq!(out, "Closure:A:x|Closure:A:y|Closure:E:u|Closure:E:v"); } + +/// Verifies function `fromCallable()` closures reject explicit scope rebinding. +#[test] +fn test_eval_closure_bind_from_callable_function_targets_reject_explicit_scope() { + let out = compile_and_run( + r#"bindTo($box, null); +echo is_object($aotNullScope) ? $aotNullScope("x") : "bad"; +echo "|"; +$aotStaticScope = Closure::bind($aot, $box, "static"); +echo is_object($aotStaticScope) ? $aotStaticScope("y") : "bad"; +echo "|"; +echo is_null($aot->bindTo($box, "EvalBindFromCallableScopeBox")) ? "a" : "A"; +echo "|"; +echo is_null(Closure::bind($aot, null, "EvalBindFromCallableScopeBox")) ? "b" : "B"; +echo "|"; + +$eval = Closure::fromCallable("eval_declared_bind_from_callable_scope_function_target"); +$evalNullScope = Closure::bind($eval, $box, null); +echo is_object($evalNullScope) ? $evalNullScope("u") : "bad"; +echo "|"; +$evalStaticScope = $eval->bindTo($box, "static"); +echo is_object($evalStaticScope) ? $evalStaticScope("v") : "bad"; +echo "|"; +echo is_null($eval->bindTo($box, "EvalBindFromCallableScopeBox")) ? "e" : "E"; +echo "|"; +return is_null(Closure::bind($eval, null, "EvalBindFromCallableScopeBox")) ? "f" : "F";'); +"#, + ); + + assert_eq!(out, "A:x|A:y|a|b|E:u|E:v|e|f"); +} From 98a7e1337bcb1a5990652e9f6b6a07ad0329f510 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 11:09:49 +0200 Subject: [PATCH 1031/1208] Retain eval Closure reflection metadata --- crates/elephc-magician/src/context.rs | 22 ++ .../src/interpreter/reflection.rs | 330 +++++++++++++++++- docs/php/eval.md | 12 +- tests/codegen/eval_callables.rs | 75 ++++ 4 files changed, 425 insertions(+), 14 deletions(-) diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index 0fdf4b467e..ab910f39d0 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -905,6 +905,7 @@ pub struct ElephcEvalContext { eval_reflection_attributes: HashMap, eval_reflection_classes: HashMap, eval_reflection_functions: HashMap, + eval_reflection_function_closure_targets: HashMap, eval_reflection_methods: HashMap, eval_reflection_properties: HashMap, eval_dynamic_reflection_properties: HashSet, @@ -977,6 +978,7 @@ impl ElephcEvalContext { eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), eval_reflection_functions: HashMap::new(), + eval_reflection_function_closure_targets: HashMap::new(), eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), eval_dynamic_reflection_properties: HashSet::new(), @@ -1050,6 +1052,7 @@ impl ElephcEvalContext { eval_reflection_attributes: HashMap::new(), eval_reflection_classes: HashMap::new(), eval_reflection_functions: HashMap::new(), + eval_reflection_function_closure_targets: HashMap::new(), eval_reflection_methods: HashMap::new(), eval_reflection_properties: HashMap::new(), eval_dynamic_reflection_properties: HashSet::new(), @@ -1954,6 +1957,25 @@ impl ElephcEvalContext { .map(String::as_str) } + /// Records the callable target behind a `Closure` reflected as a function. + pub fn register_eval_reflection_function_closure_target( + &mut self, + identity: u64, + target: EvalClosureObjectTarget, + ) { + self.eval_reflection_function_closure_targets + .insert(identity, target); + } + + /// Returns the callable target retained for a reflected `Closure` object. + pub fn eval_reflection_function_closure_target( + &self, + identity: u64, + ) -> Option<&EvalClosureObjectTarget> { + self.eval_reflection_function_closure_targets + .get(&identity) + } + /// Records reflected method metadata for one synthetic ReflectionMethod object. pub fn register_eval_reflection_method( &mut self, diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index c29ffe792a..7f58b8cff3 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -220,6 +220,7 @@ enum EvalReflectionFunctionMethodTarget { closure_captures: Vec, parameters: Vec, source_location: Option, + closure_target: Option, is_variadic: bool, is_static: bool, is_closure: bool, @@ -1376,9 +1377,19 @@ pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( eval_reflection_bind_no_args(evaluated_args)?; eval_reflection_function_closure_used_variables_result(&target, values).map(Some) } - "getclosurethis" | "getclosurescopeclass" | "getclosurecalledclass" => { + "getclosurethis" => { eval_reflection_bind_no_args(evaluated_args)?; - values.null().map(Some) + eval_reflection_function_closure_this_result(&target, values).map(Some) + } + "getclosurescopeclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_scope_class_result(&target, context, values) + .map(Some) + } + "getclosurecalledclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_called_class_result(&target, context, values) + .map(Some) } _ => Ok(None), } @@ -2817,6 +2828,15 @@ fn eval_reflection_class_owner_object_result( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) else { + if reflected_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return eval_reflection_builtin_closure_class_object_result( + owner_kind, context, values, + ) + .map(Some); + } let Some((flags, modifiers)) = eval_reflection_aot_class_flags(reflected_name, values)? else { return Ok(None); @@ -2887,6 +2907,38 @@ fn eval_reflection_class_owner_object_result( .map(Some) } +/// Builds the minimal ReflectionClass metadata object for PHP's builtin Closure class. +fn eval_reflection_builtin_closure_class_object_result( + owner_kind: u64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_INTERNAL; + let modifiers = eval_reflection_class_modifiers(true, false, false, false); + eval_reflection_owner_object( + owner_kind, + "Closure", + &[], + &[], + &[], + &[], + &[], + None, + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + context, + values, + ) +} + /// Builds an eval-backed `ReflectionEnum` object for a declared enum. fn eval_reflection_enum_new( evaluated_args: Vec, @@ -3151,7 +3203,11 @@ fn eval_reflection_function_new( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let args = bind_evaluated_function_args(&[String::from("function")], evaluated_args)?; - let requested_name = eval_reflection_function_name_arg(args[0], context, values)?; + let closure_target = eval_reflection_function_closure_target_arg(args[0], context, values)?; + let requested_name = match closure_target.as_ref() { + Some(target) => eval_reflection_function_closure_target_name(target), + None => eval_reflection_function_name_arg(args[0], context, values)?, + }; let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); if let Some(closure) = context.closure(&requested_name).cloned() { let function = closure.function(); @@ -3177,6 +3233,14 @@ fn eval_reflection_function_new( context, values, ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) .map(Some); } if let Some(function) = context.function(&lookup_name).cloned() { @@ -3202,6 +3266,14 @@ fn eval_reflection_function_new( context, values, ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) .map(Some); } if let Some(function) = context.native_function(&lookup_name) { @@ -3216,11 +3288,77 @@ fn eval_reflection_function_new( context, values, ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + if closure_target.is_some() { + return eval_reflection_function_object_result( + &requested_name, + &[], + &[], + 0, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) .map(Some); } Ok(None) } +/// Returns the retained callable target when a ReflectionFunction argument is a Closure object. +fn eval_reflection_function_closure_target_arg( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Ok(None); + } + let identity = values.object_identity(value)?; + Ok(context.closure_object_target(identity).cloned()) +} + +/// Returns the function-like name exposed for a Closure-backed ReflectionFunction. +fn eval_reflection_function_closure_target_name(target: &EvalClosureObjectTarget) -> String { + match target { + EvalClosureObjectTarget::Named(name) + | EvalClosureObjectTarget::BoundNamed { name, .. } => name.clone(), + EvalClosureObjectTarget::InvokableObject { .. } => String::from("__invoke"), + EvalClosureObjectTarget::ObjectMethod { method, .. } + | EvalClosureObjectTarget::StaticMethod { method, .. } => method.clone(), + } +} + +/// Attaches original Closure target metadata to a synthetic ReflectionFunction object. +fn eval_reflection_attach_function_closure_target( + object: RuntimeCellHandle, + closure_target: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(closure_target) = closure_target else { + return Ok(object); + }; + let identity = values.object_identity(object)?; + context.register_eval_reflection_function_closure_target(identity, closure_target); + Ok(object) +} + /// Returns parameter names for a registered native function, filling missing bridge names. fn eval_reflection_native_function_parameter_names(function: &NativeFunction) -> Vec { (0..function.param_count()) @@ -5208,6 +5346,16 @@ fn eval_reflection_full_class_object_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { + if class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return eval_reflection_builtin_closure_class_object_result( + EVAL_REFLECTION_OWNER_CLASS, + context, + values, + ); + } let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { return values.bool_value(false); @@ -9379,6 +9527,9 @@ fn eval_reflection_function_method_target( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { if let Some(name) = context.eval_reflection_function_name(identity) { + let closure_target = context + .eval_reflection_function_closure_target(identity) + .cloned(); if let Some(closure) = context.closure(name) { let function = closure.function(); let is_variadic = function.parameter_is_variadic().iter().any(|flag| *flag); @@ -9407,6 +9558,7 @@ fn eval_reflection_function_method_target( closure_captures: closure.captures().to_vec(), parameters, source_location, + closure_target, is_variadic, is_static: closure.is_static(), is_closure: true, @@ -9445,9 +9597,10 @@ fn eval_reflection_function_method_target( closure_captures: Vec::new(), parameters, source_location, + closure_target: closure_target.clone(), is_variadic, - is_static: false, - is_closure: false, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), is_deprecated, return_type_metadata, })); @@ -9466,9 +9619,10 @@ fn eval_reflection_function_method_target( closure_captures: Vec::new(), parameters, source_location: None, + closure_target: closure_target.clone(), is_variadic, - is_static: false, - is_closure: false, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), is_deprecated: false, return_type_metadata, })); @@ -9480,9 +9634,10 @@ fn eval_reflection_function_method_target( closure_captures: Vec::new(), parameters: Vec::new(), source_location: None, + closure_target: closure_target.clone(), is_variadic: false, - is_static: false, - is_closure: false, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), is_deprecated: false, return_type_metadata: None, })); @@ -9816,6 +9971,11 @@ fn eval_reflection_function_method_is_static(target: &EvalReflectionFunctionMeth } } +/// Returns whether retained Closure target metadata represents a static callable. +fn eval_reflection_closure_target_is_static(target: Option<&EvalClosureObjectTarget>) -> bool { + matches!(target, Some(EvalClosureObjectTarget::StaticMethod { .. })) +} + /// Returns whether the reflected function-like target carries `#[Deprecated]`. fn eval_reflection_function_method_is_deprecated( target: &EvalReflectionFunctionMethodTarget, @@ -9848,6 +10008,158 @@ fn eval_reflection_function_closure_used_variables_result( Ok(result) } +/// Builds `ReflectionFunction::getClosureThis()` from retained Closure target metadata. +fn eval_reflection_function_closure_this_result( + target: &EvalReflectionFunctionMethodTarget, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(target) = eval_reflection_function_closure_target(target) else { + return values.null(); + }; + match target { + EvalClosureObjectTarget::BoundNamed { + bound_this: Some(object), + .. + } + | EvalClosureObjectTarget::InvokableObject { object } + | EvalClosureObjectTarget::ObjectMethod { object, .. } => values.retain(*object), + EvalClosureObjectTarget::Named(_) + | EvalClosureObjectTarget::BoundNamed { + bound_this: None, .. + } + | EvalClosureObjectTarget::StaticMethod { .. } => values.null(), + } +} + +/// Builds `ReflectionFunction::getClosureScopeClass()` from retained Closure metadata. +fn eval_reflection_function_closure_scope_class_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = + eval_reflection_function_closure_scope_class_name(target, context, values)?; + eval_reflection_function_closure_class_object_result(class_name, context, values) +} + +/// Builds `ReflectionFunction::getClosureCalledClass()` from retained Closure metadata. +fn eval_reflection_function_closure_called_class_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = + eval_reflection_function_closure_called_class_name(target, context, values)?; + eval_reflection_function_closure_class_object_result(class_name, context, values) +} + +/// Returns the retained callable target for a Closure-backed ReflectionFunction. +fn eval_reflection_function_closure_target( + target: &EvalReflectionFunctionMethodTarget, +) -> Option<&EvalClosureObjectTarget> { + match target { + EvalReflectionFunctionMethodTarget::Function { closure_target, .. } => { + closure_target.as_ref() + } + EvalReflectionFunctionMethodTarget::Method { .. } => None, + } +} + +/// Resolves the PHP closure scope class name for retained Closure target metadata. +fn eval_reflection_function_closure_scope_class_name( + target: &EvalReflectionFunctionMethodTarget, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_closure_target(target) else { + return Ok(None); + }; + match target { + EvalClosureObjectTarget::Named(_) => Ok(None), + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + } => { + if context.closure(name).is_none() { + return Ok(bound_this.map(|_| String::from("Closure"))); + } + if let Some(bound_scope) = bound_scope { + return Ok(Some(bound_scope.clone())); + } + match bound_this { + Some(object) => eval_closure_bound_object_class_name(*object, context, values) + .map(Some), + None => Ok(None), + } + } + EvalClosureObjectTarget::InvokableObject { object } + | EvalClosureObjectTarget::ObjectMethod { object, .. } => { + eval_closure_bound_object_class_name(*object, context, values).map(Some) + } + EvalClosureObjectTarget::StaticMethod { class_name, .. } => { + Ok(Some(class_name.trim_start_matches('\\').to_string())) + } + } +} + +/// Resolves the PHP closure called class name for retained Closure target metadata. +fn eval_reflection_function_closure_called_class_name( + target: &EvalReflectionFunctionMethodTarget, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_closure_target(target) else { + return Ok(None); + }; + match target { + EvalClosureObjectTarget::Named(_) => Ok(None), + EvalClosureObjectTarget::BoundNamed { + bound_this, + bound_scope, + .. + } => match bound_this { + Some(object) => eval_closure_bound_object_class_name(*object, context, values) + .map(Some), + None => Ok(bound_scope.clone()), + }, + EvalClosureObjectTarget::InvokableObject { object } => { + eval_closure_bound_object_class_name(*object, context, values).map(Some) + } + EvalClosureObjectTarget::ObjectMethod { + object, + called_class, + .. + } => match called_class { + Some(called_class) => Ok(Some(called_class.clone())), + None => eval_closure_bound_object_class_name(*object, context, values).map(Some), + }, + EvalClosureObjectTarget::StaticMethod { + class_name, + called_class, + .. + } => Ok(Some( + called_class + .as_deref() + .unwrap_or(class_name) + .trim_start_matches('\\') + .to_string(), + )), + } +} + +/// Materializes a nullable ReflectionClass result for Closure scope metadata. +fn eval_reflection_function_closure_class_object_result( + class_name: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = class_name else { + return values.null(); + }; + eval_reflection_full_class_object_result(&class_name, context, values) +} + /// Returns the retained return type metadata for a reflected function or method. fn eval_reflection_function_method_return_type( target: &EvalReflectionFunctionMethodTarget, diff --git a/docs/php/eval.md b/docs/php/eval.md index 5caed65fdc..3c7c86647a 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -919,11 +919,13 @@ static AST -> EIR -> native codegen pipeline used for ordinary elephc source. Unsupported constructs and missing class names during eval object construction fail at runtime with an eval fatal diagnostic. -The fragment subset is broad but not the full elephc language surface. In -particular, advanced native callable descriptors and full PHP `Closure` APIs -such as arbitrary scope mutation and reflection over every first-class callable -shape are still outside eval fragments. Runtime/AOT free-function, -object-method, static-method, and +The fragment subset is broad but not the full elephc language surface. Eval +retains `ReflectionFunction` closure metadata for common +`Closure::fromCallable()` targets, including bound free functions, object +methods, static methods, and invokable objects. Advanced native callable +descriptors and full PHP `Closure` APIs, such as arbitrary scope mutation and +reflection over every first-class callable shape, are still outside eval +fragments. Runtime/AOT free-function, object-method, static-method, and constructor fallback from eval can bind registered names, defaults, and positional variadic tails; method, static-method, and constructor fallback can also bind by-reference lvalue diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 5c868782ae..c02402efdd 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -2260,3 +2260,78 @@ return is_null(Closure::bind($eval, null, "EvalBindFromCallableScopeBox")) ? "f" assert_eq!(out, "A:x|A:y|a|b|E:u|E:v|e|f"); } + +/// Verifies ReflectionFunction reports retained metadata for Closure callables. +#[test] +fn test_eval_reflection_function_reports_from_callable_closure_metadata() { + let out = compile_and_run( + r#"getClosureThis(); + $scope = $ref->getClosureScopeClass(); + $called = $ref->getClosureCalledClass(); + return $label . ":" . + (is_object($this) ? get_class($this) : "null") . ":" . + ($scope ? $scope->getName() : "null") . ":" . + ($called ? $called->getName() : "null") . ":" . + ($ref->isStatic() ? "S" : "s"); + } catch (Throwable $e) { + return $label . ":ERR:" . get_class($e) . ":" . $e->getMessage(); + } +} + +function eval_reflect_closure_meta_eval_function(): string { + return "eval"; +} + +class EvalReflectClosureMetaEvalBox { + public function method(): string { return "method"; } + public static function stat(): string { return "static"; } + public function __invoke(): string { return "invoke"; } +} + +$aot = new EvalReflectClosureMetaAotBox(); +$eval = new EvalReflectClosureMetaEvalBox(); + +$out = []; +$out[] = eval_reflect_closure_meta_dump( + "aot-fn", + Closure::bind(Closure::fromCallable("eval_reflect_closure_meta_aot_function"), $aot) +); +$out[] = eval_reflect_closure_meta_dump( + "eval-fn", + Closure::bind(Closure::fromCallable("eval_reflect_closure_meta_eval_function"), $eval) +); +$out[] = eval_reflect_closure_meta_dump("aot-method", Closure::fromCallable([$aot, "method"])); +$out[] = eval_reflect_closure_meta_dump("eval-method", Closure::fromCallable([$eval, "method"])); +$out[] = eval_reflect_closure_meta_dump("aot-invoke", Closure::fromCallable($aot)); +$out[] = eval_reflect_closure_meta_dump( + "eval-static", + Closure::fromCallable(["EvalReflectClosureMetaEvalBox", "stat"]) +); +return implode("|", $out);'); +"#, + ); + + assert_eq!( + out, + "aot-fn:EvalReflectClosureMetaAotBox:Closure:EvalReflectClosureMetaAotBox:s|\ +eval-fn:EvalReflectClosureMetaEvalBox:Closure:EvalReflectClosureMetaEvalBox:s|\ +aot-method:EvalReflectClosureMetaAotBox:EvalReflectClosureMetaAotBox:EvalReflectClosureMetaAotBox:s|\ +eval-method:EvalReflectClosureMetaEvalBox:EvalReflectClosureMetaEvalBox:EvalReflectClosureMetaEvalBox:s|\ +aot-invoke:EvalReflectClosureMetaAotBox:EvalReflectClosureMetaAotBox:EvalReflectClosureMetaAotBox:s|\ +eval-static:null:EvalReflectClosureMetaEvalBox:EvalReflectClosureMetaEvalBox:S" + ); +} From d3c65617e27fe908c0372a15bd5106d3c029abde Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 11:42:38 +0200 Subject: [PATCH 1032/1208] Align eval ReflectionClass constructor refs --- .../src/interpreter/statements.rs | 117 ++++++++++++++---- tests/codegen/eval_constructors.rs | 55 ++++++++ 2 files changed, 147 insertions(+), 25 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 7584d12cf8..27c9c419db 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -8421,6 +8421,25 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( context: &mut ElephcEvalContext, caller_scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_class_new_object_with_ref_mode( + class, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + caller_scope, + values, + ) +} + +/// Creates an eval-declared object while using the selected constructor by-ref mode. +fn eval_dynamic_class_new_object_with_ref_mode( + class: &EvalClass, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, ) -> Result { let object = eval_dynamic_class_allocate_object(class, context, caller_scope, values)?; if let Some((constructor_class, constructor)) = @@ -8438,22 +8457,25 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( values, ); } - let result = eval_dynamic_method_with_values( + let result = eval_dynamic_method_with_values_and_ref_mode( &constructor_class, class.name(), &constructor, object, + constructor.parameter_is_by_ref(), evaluated_args, + by_ref_mode, context, values, )?; eval_release_value(context, values, result)?; } else if !evaluated_args.is_empty() { if let Some(parent) = context.class_native_parent_name(class.name()) { - eval_native_constructor_with_evaluated_args( + eval_native_constructor_with_evaluated_args_and_ref_mode( &parent, object, evaluated_args, + by_ref_mode, context, values, )?; @@ -8469,10 +8491,11 @@ pub(in crate::interpreter) fn eval_dynamic_class_new_object( )? .is_some() { - eval_native_constructor_with_evaluated_args( + eval_native_constructor_with_evaluated_args_and_ref_mode( &parent, object, Vec::new(), + by_ref_mode, context, values, )?; @@ -9790,8 +9813,9 @@ fn eval_reflection_class_new_instance_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { - let constructor_args = if method_name.eq_ignore_ascii_case("newInstance") { - evaluated_args + let direct_new_instance = method_name.eq_ignore_ascii_case("newInstance"); + let constructor_args = if direct_new_instance { + eval_reflection_constructor_by_value_args(evaluated_args) } else if method_name.eq_ignore_ascii_case("newInstanceArgs") { eval_reflection_class_new_instance_args(evaluated_args, context, values)? } else { @@ -9822,9 +9846,25 @@ fn eval_reflection_class_new_instance_result( } } return eval_reflection_public_constructor_scope(context, values, |context, values| { + let constructor_name = + format!("{}::__construct", class.name().trim_start_matches('\\')); + let by_ref_mode = if direct_new_instance { + EvalByRefBindingMode::WarnByValue { + callable_name: &constructor_name, + } + } else { + EvalByRefBindingMode::RequireTarget + }; let mut scope = ElephcEvalScope::new(); - eval_dynamic_class_new_object(&class, constructor_args, context, &mut scope, values) - .map(Some) + eval_dynamic_class_new_object_with_ref_mode( + &class, + constructor_args, + by_ref_mode, + context, + &mut scope, + values, + ) + .map(Some) }); } let class_name = context @@ -9835,11 +9875,20 @@ fn eval_reflection_class_new_instance_result( return eval_throw_reflection_instantiation_error(error, context, values); } eval_reflection_public_constructor_scope(context, values, |context, values| { + let constructor_name = format!("{}::__construct", class_name.trim_start_matches('\\')); + let by_ref_mode = if direct_new_instance { + EvalByRefBindingMode::WarnByValue { + callable_name: &constructor_name, + } + } else { + EvalByRefBindingMode::RequireTarget + }; let instance = values.new_object(&class_name)?; - eval_native_constructor_with_evaluated_args( + eval_native_constructor_with_evaluated_args_and_ref_mode( &class_name, instance, constructor_args, + by_ref_mode, context, values, )?; @@ -9847,6 +9896,20 @@ fn eval_reflection_class_new_instance_result( }) } +/// Removes caller writeback targets for ReflectionClass::newInstance() by-value forwarding. +fn eval_reflection_constructor_by_value_args( + evaluated_args: Vec, +) -> Vec { + evaluated_args + .into_iter() + .map(|arg| EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + }) + .collect() +} + /// Expands the single `ReflectionClass::newInstanceArgs()` array argument. fn eval_reflection_class_new_instance_args( evaluated_args: Vec, @@ -10795,22 +10858,6 @@ pub(in crate::interpreter) fn positional_evaluated_arg_values( Ok(args.into_iter().map(|arg| arg.value).collect()) } -/// Binds native AOT callable args while preserving by-reference caller targets. -pub(in crate::interpreter) fn bind_native_callable_bound_args( - signature: Option, - args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - bind_native_callable_bound_args_with_mode( - signature, - args, - EvalByRefBindingMode::RequireTarget, - context, - values, - ) -} - /// Binds native AOT callable args using the selected by-reference degradation mode. fn bind_native_callable_bound_args_with_mode( signature: Option, @@ -11702,6 +11749,25 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_native_constructor_with_evaluated_args_and_ref_mode( + class_name, + object, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Runs one generated/AOT constructor with caller-selected by-ref binding behavior. +fn eval_native_constructor_with_evaluated_args_and_ref_mode( + class_name: &str, + object: RuntimeCellHandle, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, ) -> Result<(), EvalStatus> { if let Some(message) = eval_native_constructor_access_error(class_name, context, values)? { return eval_throw_error(&message, context, values); @@ -11709,9 +11775,10 @@ pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( let bridge_scope = eval_native_constructor_bridge_scope(class_name, context, values)?; let signature = context.native_constructor_signature(class_name); - let bound_args = bind_native_callable_bound_args( + let bound_args = bind_native_callable_bound_args_with_mode( signature, evaluated_args, + by_ref_mode, context, values, )?; diff --git a/tests/codegen/eval_constructors.rs b/tests/codegen/eval_constructors.rs index 3910d44eb5..cf9e03b405 100644 --- a/tests/codegen/eval_constructors.rs +++ b/tests/codegen/eval_constructors.rs @@ -92,6 +92,61 @@ return gettype(EvalCtorNamedRefTargetStatic::$value) . ":" . EvalCtorNamedRefTar assert_eq!(out, "integer:5|integer:9|integer:10|integer:15"); } +/// Verifies ReflectionClass construction uses PHP by-ref semantics for eval and AOT constructors. +#[test] +fn test_eval_reflection_class_constructor_by_ref_matches_php_ref_semantics() { + let out = compile_and_run_capture( + r#"newInstance($direct); +echo gettype($direct) . ":" . $direct . "|"; + +$argsValue = "2"; +$aotRef->newInstanceArgs([&$argsValue]); +echo gettype($argsValue) . ":" . $argsValue . "|"; + +class EvalReflectDeclaredCtorRefBridge { + public function __construct(int &$value) { + $value = $value + 7; + } +} + +$evalRef = new ReflectionClass("EvalReflectDeclaredCtorRefBridge"); +$evalDirect = "3"; +$evalRef->newInstance($evalDirect); +echo gettype($evalDirect) . ":" . $evalDirect . "|"; + +$evalArgsValue = "4"; +$evalRef->newInstanceArgs([&$evalArgsValue]); +return gettype($evalArgsValue) . ":" . $evalArgsValue;'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "string:1|integer:7|string:3|integer:11"); + for warning in [ + "EvalReflectAotCtorRefBridge::__construct(): Argument #1 ($value) must be passed by reference, value given", + "EvalReflectDeclaredCtorRefBridge::__construct(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + /// Verifies AOT constructor by-reference args write back refcounted string, array, and object values. #[test] fn test_eval_dynamic_new_constructor_by_ref_refcounted_writeback() { From 8d7a049a821475a60efce9260e197f3c4f54dda1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 12:21:17 +0200 Subject: [PATCH 1033/1208] Align eval Reflection invoke refs --- .../src/interpreter/reflection.rs | 49 ++++-- tests/codegen/eval_reflection_invocation.rs | 144 ++++++++++++++++++ tests/codegen/mod.rs | 1 + 3 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 tests/codegen/eval_reflection_invocation.rs diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 7f58b8cff3..5536687417 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -8602,15 +8602,27 @@ fn eval_reflection_function_invoke_dispatch( values: &mut impl RuntimeValueOps, ) -> Result { if let Some(closure) = context.closure(function_name).cloned() { - return eval_closure_with_evaluated_args(&closure, function_args, context, values); + return eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + &closure, + None, + closure.function().parameter_is_by_ref(), + function_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, + context, + values, + ); } let function_key = function_name.to_ascii_lowercase(); if let Some(function) = context.function(&function_key).cloned() { - let by_value_parameters = vec![false; function.params().len()]; - return eval_dynamic_function_with_evaluated_args_and_ref_flags( + return eval_dynamic_function_with_evaluated_args_and_ref_mode( &function, - &by_value_parameters, + function.parameter_is_by_ref(), function_args, + EvalByRefBindingMode::WarnByValue { + callable_name: function.name(), + }, context, values, ); @@ -8634,27 +8646,37 @@ fn eval_reflection_method_invoke_dispatch( if method.is_abstract() { return Err(EvalStatus::RuntimeFatal); } - let by_value_parameters = vec![false; method.params().len()]; + let callable_name = format!( + "{}::{}", + method_class.trim_start_matches('\\'), + method.name() + ); if method.is_static() { - return eval_dynamic_static_method_with_values_and_ref_flags( + return eval_dynamic_static_method_with_values_and_ref_mode( &method_class, &method_class, &method, - &by_value_parameters, + method.parameter_is_by_ref(), method_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, context, values, ); } let called_class = eval_reflection_method_instance_called_class(declaring_class, object, context, values)?; - return eval_dynamic_method_with_values_and_ref_flags( + return eval_dynamic_method_with_values_and_ref_mode( &method_class, &called_class, &method, object, - &by_value_parameters, + method.parameter_is_by_ref(), method_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, context, values, ); @@ -8723,10 +8745,12 @@ fn eval_reflection_aot_method_invoke_dispatch( } if member.is_static { return eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { - eval_native_static_method_with_evaluated_args( + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( declaring_class, method_name, method_args, + Some(declaring_class), + Some(declaring_class), context, values, ) @@ -8745,12 +8769,15 @@ fn eval_reflection_aot_method_invoke_dispatch( )?; return Err(EvalStatus::UncaughtThrowable); } + let called_class = eval_reflection_object_class_name(object, context, values)?; eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { - eval_native_method_with_evaluated_args( + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( object, declaring_class, method_name, method_args, + Some(declaring_class), + Some(&called_class), context, values, ) diff --git a/tests/codegen/eval_reflection_invocation.rs b/tests/codegen/eval_reflection_invocation.rs new file mode 100644 index 0000000000..cb263e0272 --- /dev/null +++ b/tests/codegen/eval_reflection_invocation.rs @@ -0,0 +1,144 @@ +//! Purpose: +//! End-to-end regressions for eval ReflectionFunction/ReflectionMethod invocation. +//! +//! Called from: +//! - `cargo test --test codegen_tests eval_reflection_invocation` through Rust's test harness. +//! +//! Key details: +//! - Fixtures distinguish PHP's by-value `invoke()` forwarding from by-reference +//! `invokeArgs([&$value])` forwarding for eval-declared and generated/AOT callables. + +use crate::support::compile_and_run_capture; + +/// Verifies ReflectionFunction preserves PHP by-ref semantics for invoke and invokeArgs. +#[test] +fn test_eval_reflection_function_invoke_by_ref_matches_php_ref_semantics() { + let out = compile_and_run_capture( + r#"invoke($direct) . ":" . gettype($direct) . ":" . $direct . "|"; + +$argsValue = "2"; +echo $evalRef->invokeArgs([&$argsValue]) . ":" . gettype($argsValue) . ":" . $argsValue . "|"; + +$aotRef = new ReflectionFunction("eval_reflect_aot_invoke_ref_fn"); +$aotDirect = "3"; +echo $aotRef->invoke($aotDirect) . ":" . gettype($aotDirect) . ":" . $aotDirect . "|"; + +$aotArgsValue = "4"; +return $aotRef->invokeArgs([&$aotArgsValue]) . ":" . gettype($aotArgsValue) . ":" . $aotArgsValue;'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!(out.stdout, "4:string:1|5:integer:5|10:string:3|11:integer:11"); + for warning in [ + "eval_reflect_invoke_ref_fn(): Argument #1 ($value) must be passed by reference, value given", + "eval_reflect_aot_invoke_ref_fn(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} + +/// Verifies ReflectionMethod preserves PHP by-ref semantics for invoke and invokeArgs. +#[test] +fn test_eval_reflection_method_invoke_by_ref_matches_php_ref_semantics() { + let out = compile_and_run_capture( + r#"invoke($evalBox, $evalDirect) . ":" . gettype($evalDirect) . ":" . $evalDirect . "|"; + +$evalArgsValue = "2"; +echo $evalMethod->invokeArgs($evalBox, [&$evalArgsValue]) . ":" . gettype($evalArgsValue) . ":" . $evalArgsValue . "|"; + +$evalStatic = new ReflectionMethod("EvalReflectInvokeRefMethodBox", "add"); +$evalStaticDirect = "3"; +echo $evalStatic->invoke(null, $evalStaticDirect) . ":" . gettype($evalStaticDirect) . ":" . $evalStaticDirect . "|"; + +$evalStaticArgsValue = "4"; +echo $evalStatic->invokeArgs(null, [&$evalStaticArgsValue]) . ":" . gettype($evalStaticArgsValue) . ":" . $evalStaticArgsValue . "|"; + +$aotBox = new EvalReflectAotInvokeRefMethodBox(); +$aotMethod = new ReflectionMethod("EvalReflectAotInvokeRefMethodBox", "bump"); +$aotDirect = "5"; +echo $aotMethod->invoke($aotBox, $aotDirect) . ":" . gettype($aotDirect) . ":" . $aotDirect . "|"; + +$aotArgsValue = "6"; +echo $aotMethod->invokeArgs($aotBox, [&$aotArgsValue]) . ":" . gettype($aotArgsValue) . ":" . $aotArgsValue . "|"; + +$aotStatic = new ReflectionMethod("EvalReflectAotInvokeRefMethodBox", "add"); +$aotStaticDirect = "7"; +echo $aotStatic->invoke(null, $aotStaticDirect) . ":" . gettype($aotStaticDirect) . ":" . $aotStaticDirect . "|"; + +$aotStaticArgsValue = "8"; +return $aotStatic->invokeArgs(null, [&$aotStaticArgsValue]) . ":" . gettype($aotStaticArgsValue) . ":" . $aotStaticArgsValue;'); +"#, + ); + + assert!( + out.success, + "program failed: stdout={:?} stderr={}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "6:string:1|7:integer:7|12:string:3|13:integer:13|16:string:5|17:integer:17|20:string:7|21:integer:21" + ); + for warning in [ + "EvalReflectInvokeRefMethodBox::bump(): Argument #1 ($value) must be passed by reference, value given", + "EvalReflectInvokeRefMethodBox::add(): Argument #1 ($value) must be passed by reference, value given", + "EvalReflectAotInvokeRefMethodBox::bump(): Argument #1 ($value) must be passed by reference, value given", + "EvalReflectAotInvokeRefMethodBox::add(): Argument #1 ($value) must be passed by reference, value given", + ] { + assert!( + out.stderr.contains(warning), + "missing by-ref warning {warning:?}: {}", + out.stderr + ); + } +} diff --git a/tests/codegen/mod.rs b/tests/codegen/mod.rs index f7d420e2ea..c537559f14 100644 --- a/tests/codegen/mod.rs +++ b/tests/codegen/mod.rs @@ -23,6 +23,7 @@ mod eval_callable_ref_errors; mod eval_callables; mod eval_closures; mod eval_constructors; +mod eval_reflection_invocation; mod operators; mod control_flow; mod scalar_strings; From 8a4a60a282f41764e2069f39b73790440d63be39 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 12:40:38 +0200 Subject: [PATCH 1034/1208] Align eval ReflectionClass newInstanceArgs refs --- .../src/interpreter/statements.rs | 16 ++++----------- tests/codegen/eval_constructors.rs | 20 +++++++++++++++---- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 27c9c419db..f5f46857ff 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -9848,12 +9848,8 @@ fn eval_reflection_class_new_instance_result( return eval_reflection_public_constructor_scope(context, values, |context, values| { let constructor_name = format!("{}::__construct", class.name().trim_start_matches('\\')); - let by_ref_mode = if direct_new_instance { - EvalByRefBindingMode::WarnByValue { - callable_name: &constructor_name, - } - } else { - EvalByRefBindingMode::RequireTarget + let by_ref_mode = EvalByRefBindingMode::WarnByValue { + callable_name: &constructor_name, }; let mut scope = ElephcEvalScope::new(); eval_dynamic_class_new_object_with_ref_mode( @@ -9876,12 +9872,8 @@ fn eval_reflection_class_new_instance_result( } eval_reflection_public_constructor_scope(context, values, |context, values| { let constructor_name = format!("{}::__construct", class_name.trim_start_matches('\\')); - let by_ref_mode = if direct_new_instance { - EvalByRefBindingMode::WarnByValue { - callable_name: &constructor_name, - } - } else { - EvalByRefBindingMode::RequireTarget + let by_ref_mode = EvalByRefBindingMode::WarnByValue { + callable_name: &constructor_name, }; let instance = values.new_object(&class_name)?; eval_native_constructor_with_evaluated_args_and_ref_mode( diff --git a/tests/codegen/eval_constructors.rs b/tests/codegen/eval_constructors.rs index cf9e03b405..1d7b056706 100644 --- a/tests/codegen/eval_constructors.rs +++ b/tests/codegen/eval_constructors.rs @@ -112,6 +112,10 @@ $argsValue = "2"; $aotRef->newInstanceArgs([&$argsValue]); echo gettype($argsValue) . ":" . $argsValue . "|"; +$argsCopy = "5"; +$aotRef->newInstanceArgs([$argsCopy]); +echo gettype($argsCopy) . ":" . $argsCopy . "|"; + class EvalReflectDeclaredCtorRefBridge { public function __construct(int &$value) { $value = $value + 7; @@ -125,7 +129,11 @@ echo gettype($evalDirect) . ":" . $evalDirect . "|"; $evalArgsValue = "4"; $evalRef->newInstanceArgs([&$evalArgsValue]); -return gettype($evalArgsValue) . ":" . $evalArgsValue;'); +echo gettype($evalArgsValue) . ":" . $evalArgsValue . "|"; + +$evalArgsCopy = "6"; +$evalRef->newInstanceArgs([$evalArgsCopy]); +return gettype($evalArgsCopy) . ":" . $evalArgsCopy;'); "#, ); @@ -134,14 +142,18 @@ return gettype($evalArgsValue) . ":" . $evalArgsValue;'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "string:1|integer:7|string:3|integer:11"); + assert_eq!( + out.stdout, + "string:1|integer:7|string:5|string:3|integer:11|string:6" + ); for warning in [ "EvalReflectAotCtorRefBridge::__construct(): Argument #1 ($value) must be passed by reference, value given", "EvalReflectDeclaredCtorRefBridge::__construct(): Argument #1 ($value) must be passed by reference, value given", ] { + let count = out.stderr.matches(warning).count(); assert!( - out.stderr.contains(warning), - "missing by-ref warning {warning:?}: {}", + count >= 2, + "expected at least two by-ref warnings {warning:?}, saw {count}: {}", out.stderr ); } From 9412fd1e4f8c118bed6c19274db5206f054979e4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 13:20:01 +0200 Subject: [PATCH 1035/1208] Align eval Closure bind inheritance --- .../src/interpreter/statements.rs | 68 +++++++++++++- tests/codegen/eval_callables.rs | 92 +++++++++++++++++++ 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index f5f46857ff..634227ccf9 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -7786,7 +7786,9 @@ fn eval_closure_bind_target( ) } EvalClosureObjectTarget::InvokableObject { object } => { - if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + if !eval_closure_bind_bound_class_matches_method( + object, "__invoke", bound_this, context, values, + )? { return eval_closure_call_warning_null( "Cannot rebind scope of closure created from method", values, @@ -7805,7 +7807,9 @@ fn eval_closure_bind_target( bridge_scope, .. } => { - if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + if !eval_closure_bind_bound_class_matches_method( + object, &method, bound_this, context, values, + )? { return eval_closure_call_warning_null( "Cannot rebind scope of closure created from method", values, @@ -9553,6 +9557,66 @@ fn eval_closure_call_bound_class_matches( Ok(original_class.eq_ignore_ascii_case(&bound_class)) } +/// Returns whether `Closure::bind()` may bind a method closure to the new object. +fn eval_closure_bind_bound_class_matches_method( + original_object: RuntimeCellHandle, + method_name: &str, + bound_this: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(declaring_class) = + eval_closure_bind_method_declaring_class(original_object, method_name, context, values)? + else { + return Ok(false); + }; + eval_closure_object_is_instance_of(bound_this, &declaring_class, context, values) +} + +/// Resolves the class that declares the method captured by a method Closure target. +fn eval_closure_bind_method_declaring_class( + original_object: RuntimeCellHandle, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let original_class = eval_closure_bound_object_class_name(original_object, context, values)?; + if let Some((declaring_class, method)) = context.class_method(&original_class, method_name) { + if method.is_static() || method.is_abstract() { + return Ok(None); + } + return Ok(Some(declaring_class)); + } + let native_class = context + .class_native_parent_name(&original_class) + .unwrap_or_else(|| original_class.clone()); + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&native_class, method_name, context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + let declaring_class = eval_aot_method_declaring_class(&native_class, method_name, values)?; + Ok(Some(declaring_class)) +} + +/// Returns whether an object is an instance of the requested eval or generated class name. +fn eval_closure_object_is_instance_of( + object: RuntimeCellHandle, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object_class = eval_closure_bound_object_class_name(object, context, values)?; + Ok(eval_static_syntax_object_matches_class( + &object_class, + class_name, + context, + )) +} + /// Emits PHP's `Closure::call()` warning and returns `null`. fn eval_closure_call_warning_null( message: &str, diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index c02402efdd..5c10fcf81d 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -2177,6 +2177,98 @@ echo is_null(Closure::bind($static, $bound)) ? "S" : "s";'); assert_eq!(out, "25:integer:25|29:integer:29|S"); } +/// Verifies eval `Closure::bind()` can bind inherited method closures to compatible subclasses. +#[test] +fn test_eval_closure_bind_from_callable_accepts_inherited_eval_method_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +class EvalInheritedBindChild extends EvalInheritedBindBase {} + +class EvalInheritedBindOverrideChild extends EvalInheritedBindBase { + public function bump(int &$value, int $delta): int { + $value = $value + $this->base + $delta + 100; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta + 100; + return $value; + } +} + +$base = new EvalInheritedBindBase(); +$child = new EvalInheritedBindChild(); +$child->base = 20; + +$method = Closure::fromCallable([$base, "bump"])->bindTo($child); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invoke = Closure::fromCallable($base)->bindTo($child); +$b = "4"; +echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$override = new EvalInheritedBindOverrideChild(); +echo is_null(Closure::fromCallable([$override, "bump"])->bindTo($base)) ? "M" : "m"; +echo is_null(Closure::fromCallable($override)->bindTo($base)) ? "I" : "i";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|MI"); +} + +/// Verifies eval `Closure::bind()` can bind inherited generated/AOT method closures to subclasses. +#[test] +fn test_eval_closure_bind_from_callable_accepts_inherited_aot_method_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +class EvalAotInheritedBindChild extends EvalAotInheritedBindBase {} + +echo eval('$base = new EvalAotInheritedBindBase(); +$child = new EvalAotInheritedBindChild(); +$child->base = 20; + +$method = Closure::fromCallable([$base, "bump"])->bindTo($child); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invoke = Closure::fromCallable($base)->bindTo($child); +$b = "4"; +echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b;'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29"); +} + /// Verifies binding function `fromCallable()` closures returns callable closures. #[test] fn test_eval_closure_bind_from_callable_function_targets_remain_callable() { From 2c8b43c2f71fafaa28ea89cd9a1fca94adbe4d71 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 13:37:11 +0200 Subject: [PATCH 1036/1208] Cover first-class Closure bind inheritance --- tests/codegen/eval_callables.rs | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/tests/codegen/eval_callables.rs b/tests/codegen/eval_callables.rs index 5c10fcf81d..e885233f9d 100644 --- a/tests/codegen/eval_callables.rs +++ b/tests/codegen/eval_callables.rs @@ -2269,6 +2269,124 @@ echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b;'); assert_eq!(out, "25:integer:25|29:integer:29"); } +/// Verifies first-class eval method closures bind inherited method targets to subclasses. +#[test] +fn test_eval_first_class_closure_bind_accepts_inherited_eval_method_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +class EvalFirstClassInheritedBindChild extends EvalFirstClassInheritedBindBase {} + +class EvalFirstClassInheritedBindOverrideChild extends EvalFirstClassInheritedBindBase { + public function bump(int &$value, int $delta): int { + $value = $value + $this->base + $delta + 100; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta + 100; + return $value; + } +} + +$base = new EvalFirstClassInheritedBindBase(); +$child = new EvalFirstClassInheritedBindChild(); +$child->base = 20; + +$methodSource = $base->bump(...); +$method = $methodSource->bindTo($child); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invokeSource = $base(...); +$invoke = $invokeSource->bindTo($child); +$b = "4"; +echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$callMethod = $base->bump(...); +$c = "6"; +echo is_null($callMethod->call($child, $c, 7)) ? "C:" : "c:"; +echo gettype($c) . ":" . $c . "|"; + +$callInvoke = $base(...); +$d = "8"; +echo is_null($callInvoke->call($child, $d, 9)) ? "I:" : "i:"; +echo gettype($d) . ":" . $d . "|"; + +$override = new EvalFirstClassInheritedBindOverrideChild(); +$overrideMethod = $override->bump(...); +echo is_null($overrideMethod->bindTo($base)) ? "M" : "m"; +$overrideInvoke = $override(...); +echo is_null($overrideInvoke->bindTo($base)) ? "I" : "i";'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|C:string:6|I:string:8|MI"); +} + +/// Verifies first-class AOT method closures bind inherited method targets to subclasses. +#[test] +fn test_eval_first_class_closure_bind_accepts_inherited_aot_method_targets() { + let out = compile_and_run( + r#"base + $delta; + return $value; + } + + public function __invoke(int &$value, int $delta): int { + $value = $value + $this->base + $delta; + return $value; + } +} + +class EvalAotFirstClassInheritedBindChild extends EvalAotFirstClassInheritedBindBase {} + +echo eval('$base = new EvalAotFirstClassInheritedBindBase(); +$child = new EvalAotFirstClassInheritedBindChild(); +$child->base = 20; + +$methodSource = $base->bump(...); +$method = $methodSource->bindTo($child); +$a = "2"; +echo $method($a, 3) . ":" . gettype($a) . ":" . $a . "|"; + +$invokeSource = $base(...); +$invoke = $invokeSource->bindTo($child); +$b = "4"; +echo $invoke($b, 5) . ":" . gettype($b) . ":" . $b . "|"; + +$callMethod = $base->bump(...); +$c = "6"; +echo is_null($callMethod->call($child, $c, 7)) ? "C:" : "c:"; +echo gettype($c) . ":" . $c . "|"; + +$callInvoke = $base(...); +$d = "8"; +echo is_null($callInvoke->call($child, $d, 9)) ? "I:" : "i:"; +echo gettype($d) . ":" . $d;'); +"#, + ); + + assert_eq!(out, "25:integer:25|29:integer:29|C:string:6|I:string:8"); +} + /// Verifies binding function `fromCallable()` closures returns callable closures. #[test] fn test_eval_closure_bind_from_callable_function_targets_remain_callable() { From 2f8589f721a184f3670c9bb05ab13bbe89ac5361 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 15:22:16 +0200 Subject: [PATCH 1037/1208] Support eval ref-like builtin callable bridge --- .../src/interpreter/dynamic_functions.rs | 8 ++ .../src/interpreter/runtime_ops.rs | 3 +- .../src/interpreter/statements.rs | 54 ++++++++++++++ .../interpreter/tests/support/runtime_ops.rs | 6 +- .../src/runtime_hooks/externs.rs | 2 +- .../elephc-magician/src/runtime_hooks/ops.rs | 2 +- tests/codegen/eval_builtin_parity.rs | 74 ++++++++++++++++++- 7 files changed, 144 insertions(+), 5 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 78c515c611..ffda7c1100 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -376,6 +376,14 @@ fn eval_invoker_ref_slot_value( let word = unsafe { *(slot as *const u64) }; values.raw_word_value(source_tag, word) } + EVAL_TAG_STRING => { + let words = unsafe { *(slot as *const [u64; 2]) }; + values.raw_string_value(words[0], words[1]) + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } EVAL_TAG_MIXED => { let value = unsafe { *(slot as *const RuntimeCellHandle) }; values.retain(value) diff --git a/crates/elephc-magician/src/interpreter/runtime_ops.rs b/crates/elephc-magician/src/interpreter/runtime_ops.rs index be5962b7a8..a7fd59c7d4 100644 --- a/crates/elephc-magician/src/interpreter/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/runtime_ops.rs @@ -394,7 +394,7 @@ pub trait RuntimeValueOps { /// Retains one raw heap payload word so a staged native by-ref slot owns it. fn retain_raw_heap_word(&mut self, word: u64) -> Result; - /// Boxes one raw scalar payload word as a Mixed cell with the provided runtime tag. + /// Boxes one one-word raw payload as a Mixed cell with the provided runtime tag. fn raw_word_value( &mut self, source_tag: u64, @@ -589,6 +589,7 @@ pub(super) const EVAL_TAG_OBJECT: u64 = 6; pub(super) const EVAL_TAG_MIXED: u64 = 7; pub(super) const EVAL_TAG_NULL: u64 = 8; pub(super) const EVAL_TAG_RESOURCE: u64 = 9; +pub(super) const EVAL_TAG_CALLABLE: u64 = 10; pub(super) const EVAL_TAG_INVOKER_REF_CELL: u64 = 11; pub(super) const EVAL_REFLECTION_OWNER_CLASS: u64 = 0; diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 634227ccf9..77705c72fc 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -10515,6 +10515,14 @@ fn eval_invoker_slot_ref_target_value( let word = unsafe { *(slot as *const u64) }; values.raw_word_value(source_tag, word) } + EVAL_TAG_STRING => { + let words = unsafe { *(slot as *const [u64; 2]) }; + values.raw_string_value(words[0], words[1]) + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } EVAL_TAG_MIXED => { let value = unsafe { *(slot as *const RuntimeCellHandle) }; values.retain(value) @@ -10538,6 +10546,10 @@ fn write_back_invoker_slot_ref_target( } Ok(()) } + EVAL_TAG_STRING => write_back_invoker_string_slot(slot, value, values), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + write_back_invoker_heap_slot(slot, source_tag, value, values) + } EVAL_TAG_MIXED => { let retained = values.retain(value)?; let replaced = unsafe { @@ -10552,6 +10564,48 @@ fn write_back_invoker_slot_ref_target( } } +/// Writes a boxed string value back into a native descriptor-invoker string slot. +fn write_back_invoker_string_slot( + slot: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_STRING { + return Err(EvalStatus::RuntimeFatal); + } + let ptr = values.raw_value_word(value)?; + let len = values.raw_value_high_word(value)?; + let retained = values.retain_raw_string_words(ptr, len)?; + let replaced = unsafe { + let slot = slot as *mut [u64; 2]; + let replaced = *slot; + *slot = [retained.0, retained.1]; + replaced + }; + values.release_raw_string_words(replaced[0], replaced[1]) +} + +/// Writes a boxed heap value back into a native descriptor-invoker raw heap slot. +fn write_back_invoker_heap_slot( + slot: usize, + source_tag: u64, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != source_tag { + return Err(EvalStatus::RuntimeFatal); + } + let word = values.raw_value_word(value)?; + let retained = values.retain_raw_heap_word(word)?; + let replaced = unsafe { + let slot = slot as *mut u64; + let replaced = *slot; + *slot = retained; + replaced + }; + values.release_raw_heap_word(replaced) +} + /// Stores one by-reference method result in a caller-side array element. fn write_back_method_array_element_ref_target( scope: &mut ElephcEvalScope, diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index 64e3e87249..54a5bcad2c 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -441,7 +441,7 @@ impl RuntimeValueOps for FakeOps { self.runtime_retain(RuntimeCellHandle::from_raw(word as *mut RuntimeCell))?; Ok(word) } - /// Boxes one fake scalar raw payload word with the provided runtime tag. + /// Boxes one fake one-word raw payload with the provided runtime tag. fn raw_word_value( &mut self, source_tag: u64, @@ -451,6 +451,10 @@ impl RuntimeValueOps for FakeOps { EVAL_TAG_INT => self.runtime_int(word as i64), EVAL_TAG_FLOAT => self.runtime_float(f64::from_bits(word)), EVAL_TAG_BOOL => self.runtime_bool_value(word != 0), + EVAL_TAG_RESOURCE => self.runtime_resource(word as i64), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + Ok(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/runtime_hooks/externs.rs b/crates/elephc-magician/src/runtime_hooks/externs.rs index 388925d4c8..80b7e88881 100644 --- a/crates/elephc-magician/src/runtime_hooks/externs.rs +++ b/crates/elephc-magician/src/runtime_hooks/externs.rs @@ -287,7 +287,7 @@ unsafe extern "C" { pub(super) fn __elephc_eval_value_release_raw_string(ptr: u64, len: u64); /// Retains one raw heap payload word for a staged native by-reference slot. pub(super) fn __elephc_eval_value_retain_raw_heap_word(word: u64) -> u64; - /// Boxes one raw scalar payload word back into a runtime value. + /// Boxes one one-word raw payload back into a runtime value using a known tag. pub(super) fn __elephc_eval_value_from_raw_word( source_tag: u64, word: u64, diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index e8588f1f5b..1c98d07c2d 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -814,7 +814,7 @@ impl RuntimeValueOps for ElephcRuntimeOps { Ok(unsafe { __elephc_eval_value_retain_raw_heap_word(word) }) } - /// Boxes one raw scalar payload word as a Mixed cell with the provided runtime tag. + /// Boxes one one-word raw payload as a Mixed cell with the provided runtime tag. fn raw_word_value( &mut self, source_tag: u64, diff --git a/tests/codegen/eval_builtin_parity.rs b/tests/codegen/eval_builtin_parity.rs index 86c7e00b95..a39c3bf1f1 100644 --- a/tests/codegen/eval_builtin_parity.rs +++ b/tests/codegen/eval_builtin_parity.rs @@ -10,7 +10,7 @@ use std::fmt::Write; -use crate::support::compile_and_run; +use crate::support::{compile_and_run, compile_and_run_capture}; /// Verifies AOT builtin lookup stays case-insensitive without eval being present. #[test] @@ -384,3 +384,75 @@ echo ":" . implode(",", $matches[0]) . ":" . implode(",", $matches[1]);'); assert_eq!(out, "3:1,2,3|2:a,b|b,c:a,x,y,d|W:10,21|2:ab,ac:b,c"); } + +/// Verifies ref-like builtin callbacks preserve writeback through AOT callable parameters. +#[test] +fn test_eval_ref_like_builtin_callables_pass_to_aot_callable_params() { + let out = compile_and_run_capture( + r#"value = $label . ":" . ($ok ? "T" : "F") . ":" . implode(",", $items); + } + + public function convert(callable $callback, string $label): string { + $value = $label === "never" ? "x" : 42; + $ok = $callback($value, "string"); + return $label . ":" . ($ok ? "T" : "F") . ":" . gettype($value) . ":" . $value; + } + + public static function match(callable $callback, string $label): string { + $matches = [0, ""]; + $count = $callback("/(a)(b)/", "ab", $matches); + return $label . ":" . $count . ":" . implode(":", $matches); + } + + public function push(callable $callback, string $label): string { + $items = $label === "never" ? [0] : ["a"]; + $count = $callback($items, "b"); + return $label . ":" . $count . ":" . implode(",", $items); + } +} + +echo eval('$out = []; +$box = new EvalRefLikeBuiltinCallableBridge("sort", "s"); +$out[] = $box->value; +$box = new EvalRefLikeBuiltinCallableBridge(sort(...), "f"); +$out[] = $box->value; +$box = new EvalRefLikeBuiltinCallableBridge(Closure::fromCallable("sort"), "c"); +$out[] = $box->value; + +$box = new EvalRefLikeBuiltinCallableBridge("sort", "seed"); +$out[] = $box->convert("settype", "s"); +$out[] = $box->convert(settype(...), "f"); +$out[] = $box->convert(Closure::fromCallable("settype"), "c"); + +$out[] = EvalRefLikeBuiltinCallableBridge::match("preg_match", "s"); +$out[] = EvalRefLikeBuiltinCallableBridge::match(preg_match(...), "f"); +$out[] = EvalRefLikeBuiltinCallableBridge::match(Closure::fromCallable("preg_match"), "c"); + +$out[] = $box->push("array_push", "s"); +$out[] = $box->push(array_push(...), "f"); +$out[] = $box->push(Closure::fromCallable("array_push"), "c"); + +return implode("|", $out);'); +"#, + ); + + assert!( + out.success, + "stdout:\n{}\nstderr:\n{}", + out.stdout, out.stderr + ); + assert_eq!( + out.stdout, + "s:T:1,2,3|f:T:1,2,3|c:T:1,2,3|\ +s:T:string:42|f:T:string:42|c:T:string:42|\ +s:1:ab:a:b|f:1:ab:a:b|c:1:ab:a:b|\ +s:2:a,b|f:2:a,b|c:2:a,b" + ); +} From eb02d7b46017f4739cfd87eeeedf43d3ce94dede Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 1 Jul 2026 16:24:13 +0200 Subject: [PATCH 1038/1208] Fix eval AOT static callable fallback --- .../src/interpreter/builtins/registry/callable.rs | 15 +++++++++++++++ .../src/interpreter/expressions.rs | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 1f4e0ef7ef..2e5472837d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -1394,6 +1394,21 @@ fn eval_static_method_call_user_func_result( values, )? else { + if context + .native_static_method_signature(&native_class, method_name) + .is_some() + { + return eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + &native_class, + method_name, + evaluated_args, + None, + Some(&called_class), + context, + values, + ) + .map(Some); + } return Ok(None); }; if !is_static || is_abstract { diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index d67f26d0e8..f737b3c0fa 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1256,6 +1256,18 @@ fn eval_native_static_method_callable_target( values, )? else { + if context + .native_static_method_signature(&native_class, &method_name) + .is_some() + { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } if eval_native_static_magic_callable_for_class(&native_class, context, values)? || !values.class_exists(&native_class)? { From e5f530650072e4f96b0f5d21d278152dece0b10a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 5 Jul 2026 13:40:51 +0200 Subject: [PATCH 1039/1208] Mark literal eval calls for AOT fallback --- src/codegen/lower_inst.rs | 1 + src/codegen/lower_inst/builtins.rs | 8 +++ src/codegen/lower_inst/builtins/eval.rs | 25 +++++++++ src/ir/instr.rs | 7 ++- src/ir/validator.rs | 7 +-- src/ir_lower/builtin_datetime.rs | 3 +- src/ir_lower/context.rs | 1 + src/ir_lower/expr/mod.rs | 70 +++++++++++++++++++++---- src/ir_lower/program.rs | 3 +- tests/codegen/eval.rs | 32 +++++++++++ 10 files changed, 140 insertions(+), 17 deletions(-) diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index ec847fab9f..8a7a5aa786 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -215,6 +215,7 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::EnumBackingMixedToInt => enums::lower_enum_backing_mixed_to_int(ctx, &inst), Op::ExternCall => externs::lower_extern_call(ctx, &inst), Op::BuiltinCall => builtins::lower_builtin_call(ctx, &inst), + Op::EvalLiteralCall => builtins::lower_eval_literal_call(ctx, &inst), Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), Op::EvalFunctionCallArray => builtins::lower_eval_function_call_array(ctx, &inst), Op::EvalObjectNew => builtins::lower_eval_object_new(ctx, &inst), diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index 883c3ab0b9..ebc9c91b4c 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -92,6 +92,14 @@ pub(super) fn lower_hash_isset(ctx: &mut FunctionContext<'_>, inst: &Instruction isset::lower_hash_isset(ctx, inst) } +/// Lowers a statically-known eval fragment through the current bridge fallback path. +pub(super) fn lower_eval_literal_call( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval(ctx, inst) +} + /// Lowers a native call to a zero-argument function declared through `eval()`. pub(super) fn lower_eval_function_call( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index c3155a22a0..9536f4abad 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -237,6 +237,7 @@ struct EvalNativeCallableObjectDefaultArg { /// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { super::ensure_arg_count(inst, "eval", 1)?; + emit_eval_literal_aot_marker(ctx, inst)?; let code = expect_operand(inst, 0)?; let ty = ctx.load_value_to_result(code)?.codegen_repr(); if ty != PhpType::Str { @@ -279,6 +280,30 @@ pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> R store_if_result(ctx, inst) } +/// Emits an assembly marker for literal eval fragments that still use the bridge fallback. +fn emit_eval_literal_aot_marker( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + if inst.op != Op::EvalLiteralCall { + return Ok(()); + } + let Some(Immediate::Data(data)) = inst.immediate else { + return Ok(()); + }; + let fragment = ctx + .module + .data + .strings + .get(data.as_raw() as usize) + .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw()))?; + ctx.emitter.comment(&format!( + "eval literal AOT candidate ({} bytes), using bridge fallback", + fragment.len() + )); + Ok(()) +} + /// Updates eval context source metadata for file, directory, and call-site line magic constants. fn set_eval_call_site(ctx: &mut FunctionContext<'_>, inst: &Instruction) { let Some(source_path) = ctx.module.source_path.as_deref() else { diff --git a/src/ir/instr.rs b/src/ir/instr.rs index edeff9097c..c975c818c6 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -363,6 +363,7 @@ pub enum Op { Call, FunctionVariantCall, BuiltinCall, + EvalLiteralCall, EvalFunctionCall, EvalFunctionCallArray, EvalFunctionExists, @@ -513,8 +514,8 @@ impl Op { EvalConstantFetch => { E::READS_GLOBAL | E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL } - Call | FunctionVariantCall | BuiltinCall | EvalFunctionCall | EvalFunctionCallArray - | EvalObjectNew | EvalStaticMethodCall | RuntimeCall + Call | FunctionVariantCall | BuiltinCall | EvalLiteralCall | EvalFunctionCall + | EvalFunctionCallArray | EvalObjectNew | EvalStaticMethodCall | RuntimeCall | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { E::all().difference(E::REFCOUNT_OP) } @@ -538,6 +539,7 @@ impl Op { Op::Call | Op::FunctionVariantCall | Op::BuiltinCall + | Op::EvalLiteralCall | Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalObjectNew @@ -726,6 +728,7 @@ impl Op { Call => "call", FunctionVariantCall => "function_variant_call", BuiltinCall => "builtin_call", + EvalLiteralCall => "eval_literal_call", EvalFunctionCall => "eval_function_call", EvalFunctionCallArray => "eval_function_call_array", EvalFunctionExists => "eval_function_exists", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index b925f66916..c5a6ce283d 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -273,8 +273,9 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result ConstF64 => require_immediate(inst_id, inst, "f64", |imm| matches!(imm, Imm::F64(_))), ConstBool => require_immediate(inst_id, inst, "bool", |imm| matches!(imm, Imm::Bool(_))), ConstStr | ConstClassName | DataAddr | Warn | IncludeOnceMark | IncludeOnceGuard - | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalFunctionCallArray - | EvalFunctionExists | EvalClassExists | EvalConstantExists | EvalConstantFetch + | FunctionVariantMark | FunctionVariantDispatch | LoadPropRefCell | EvalLiteralCall + | EvalFunctionCallArray | EvalFunctionExists | EvalClassExists | EvalConstantExists + | EvalConstantFetch | EvalStaticMethodCall | EnumBackingStringToInt | EnumBackingMixedToInt @@ -362,7 +363,7 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio | EvalClassExists | EvalConstantExists | EvalConstantFetch | ConcatReset | GcCollect | Nop => { check_count(inst_id, inst, 0, "0") } - EvalFunctionCallArray => check_count(inst_id, inst, 1, "1"), + EvalLiteralCall | EvalFunctionCallArray => check_count(inst_id, inst, 1, "1"), ClosureNew => Ok(()), FirstClassCallableNew => check_count_at_most(inst_id, inst, 1, "0 or 1"), ObjectNew => Ok(()), diff --git a/src/ir_lower/builtin_datetime.rs b/src/ir_lower/builtin_datetime.rs index 105c35f837..c8bb0c841c 100644 --- a/src/ir_lower/builtin_datetime.rs +++ b/src/ir_lower/builtin_datetime.rs @@ -220,7 +220,8 @@ fn function_uses_eval(module: &Module, function: &Function) -> bool { fn instruction_uses_eval(module: &Module, inst: &crate::ir::Instruction) -> bool { matches!( inst.op, - Op::EvalFunctionCall + Op::EvalLiteralCall + | Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalObjectNew | Op::EvalStaticMethodCall diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 2d7ad795d1..e34492b651 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -1207,6 +1207,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { | Op::GeneratorYieldFrom | Op::Call | Op::FunctionVariantCall + | Op::EvalLiteralCall | Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalConstantFetch diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index b49c428882..1c4353af57 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1892,7 +1892,8 @@ fn lower_function_call(ctx: &mut LoweringContext<'_, '_>, name: &Name, args: &[E Some(expr.span), ); } - emit_builtin_call_value(ctx, canonical, operands, php_type, expr.span) + let eval_literal = eval_literal_fragment(canonical, args); + emit_builtin_call_value(ctx, canonical, operands, php_type, expr.span, eval_literal) } /// Emits a builtin call and releases owned temporary arguments after the call consumes them. @@ -1902,14 +1903,27 @@ fn emit_builtin_call_value( operands: Vec, php_type: PhpType, span: Span, + eval_literal: Option<&str>, ) -> LoweredValue { - let data = ctx.intern_function_name(name); + let (op, immediate, effects) = if let Some(fragment) = eval_literal { + ( + Op::EvalLiteralCall, + Some(Immediate::Data(ctx.intern_string(fragment))), + Op::EvalLiteralCall.default_effects(), + ) + } else { + ( + Op::BuiltinCall, + Some(Immediate::Data(ctx.intern_function_name(name))), + effects_lookup::builtin_effects(name), + ) + }; let call = ctx.emit_value( - Op::BuiltinCall, + op, operands.clone(), - Some(Immediate::Data(data)), + immediate, php_type, - effects_lookup::builtin_effects(name), + effects, Some(span), ); release_owned_call_arg_temporaries(ctx, &operands, Some(call.value), span); @@ -1919,6 +1933,21 @@ fn emit_builtin_call_value( call } +/// Returns the literal eval fragment when the call is a simple `eval('...')`. +fn eval_literal_fragment<'a>(name: &str, args: &'a [Expr]) -> Option<&'a str> { + if php_symbol_key(name.trim_start_matches('\\')) != "eval" + || args.len() != 1 + || crate::types::call_args::has_named_args(args) + || args.iter().any(is_spread_arg) + { + return None; + } + match &args[0].kind { + ExprKind::StringLiteral(fragment) => Some(fragment.as_str()), + _ => None, + } +} + /// Returns true when a dynamic eval fallback can preserve simple positional call semantics. fn plain_positional_call_args(args: &[Expr]) -> bool { !crate::types::call_args::has_named_args(args) @@ -2033,7 +2062,7 @@ fn lower_lazy_isset( for (idx, arg) in args.iter().enumerate() { let checked = lower_lazy_isset_operand(ctx, arg).unwrap_or_else(|| { let value = lower_expr(ctx, arg); - emit_builtin_call_value(ctx, name, vec![value.value], PhpType::Int, arg.span) + emit_builtin_call_value(ctx, name, vec![value.value], PhpType::Int, arg.span, None) }); let then_target = if idx + 1 == args.len() { ctx.builder.create_named_block("isset.lazy_true", Vec::new()) @@ -2360,7 +2389,14 @@ fn lower_native_isset_offset_probe_from_value( } _ => { let read_value = lower_array_access_from_value(ctx, array_value, index, expr, false); - emit_builtin_call_value(ctx, "isset", vec![read_value.value], PhpType::Int, expr.span) + emit_builtin_call_value( + ctx, + "isset", + vec![read_value.value], + PhpType::Int, + expr.span, + None, + ) } } } @@ -3499,7 +3535,14 @@ fn lower_static_callable_value_call( } StaticCallableBinding::Builtin(function_name) => { let php_type = call_return_type(ctx, &function_name, &operands); - Some(emit_builtin_call_value(ctx, &function_name, operands, php_type, expr.span)) + Some(emit_builtin_call_value( + ctx, + &function_name, + operands, + php_type, + expr.span, + None, + )) } StaticCallableBinding::Closure { name, @@ -3930,7 +3973,14 @@ fn lower_static_callable_call( let sig = call_signature(ctx, &function_name, callback_args); let operands = lower_builtin_call_args(ctx, &function_name, sig.as_ref(), callback_args); let php_type = call_return_type(ctx, &function_name, &operands); - Some(emit_builtin_call_value(ctx, &function_name, operands, php_type, expr.span)) + Some(emit_builtin_call_value( + ctx, + &function_name, + operands, + php_type, + expr.span, + None, + )) } StaticCallableBinding::Closure { name, @@ -4817,7 +4867,7 @@ fn lower_static_settype( let target_ty = static_settype_target_type(&type_arg)?; let sig = call_signature(ctx, name, args); let operands = lower_builtin_call_args(ctx, name, sig.as_ref(), args); - let result = emit_builtin_call_value(ctx, name, operands, PhpType::Bool, expr.span); + let result = emit_builtin_call_value(ctx, name, operands, PhpType::Bool, expr.span, None); ctx.set_local_type(local_name, target_ty); Some(result) } diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 624eac592b..4e5e0fd5fc 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -344,7 +344,8 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { features.eval = true; } } - Op::EvalFunctionCall + Op::EvalLiteralCall + | Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalFunctionExists | Op::EvalClassExists diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 3764e15cc0..c964708389 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -43,6 +43,10 @@ fn test_eval_codegen_requires_eval_bridge() { user_asm.contains("__elephc_eval_execute"), "user assembly should call the eval bridge:\n{user_asm}" ); + assert!( + user_asm.contains("eval literal AOT candidate"), + "literal eval should be marked as an AOT candidate before falling back:\n{user_asm}" + ); assert!( user_asm.contains("__elephc_eval_context_new"), "user assembly should create a persistent eval context:\n{user_asm}" @@ -60,6 +64,34 @@ fn test_eval_codegen_requires_eval_bridge() { let _ = fs::remove_dir_all(&dir); } +/// Verifies dynamic eval code is not marked as a literal AOT candidate. +#[test] +fn test_dynamic_eval_does_not_emit_literal_aot_marker() { + let dir = make_cli_test_dir("elephc_dynamic_eval_no_aot_marker"); + let (user_asm, _runtime_asm, required_libraries) = compile_source_to_asm_with_options( + " Date: Sun, 5 Jul 2026 14:40:12 +0200 Subject: [PATCH 1040/1208] Cache parsed eval fragments --- crates/elephc-magician/src/ffi/execute.rs | 6 +- .../src/interpreter/include_exec.rs | 7 +- crates/elephc-magician/src/interpreter/mod.rs | 1 + crates/elephc-magician/src/lib.rs | 1 + crates/elephc-magician/src/parse_cache.rs | 167 ++++++++++++++++++ 5 files changed, 176 insertions(+), 6 deletions(-) create mode 100644 crates/elephc-magician/src/parse_cache.rs diff --git a/crates/elephc-magician/src/ffi/execute.rs b/crates/elephc-magician/src/ffi/execute.rs index de746a5bf0..c481a31505 100644 --- a/crates/elephc-magician/src/ffi/execute.rs +++ b/crates/elephc-magician/src/ffi/execute.rs @@ -18,7 +18,7 @@ use crate::errors::EvalStatus; use crate::eval_ir; #[cfg(not(test))] use crate::interpreter; -use crate::parser; +use crate::parse_cache; #[cfg(not(test))] use crate::runtime_hooks::ElephcRuntimeOps; use std::slice; @@ -72,12 +72,12 @@ unsafe fn execute_eval_inner( } else { slice::from_raw_parts(code_ptr, code_len) }; - let program = match parser::parse_fragment(code) { + let program = match parse_cache::parse_fragment_cached(code) { Ok(program) => program, Err(err) => return err.status().code(), }; clear_result(out); - execute_parsed_eval(ctx, scope, &program, out) + execute_parsed_eval(ctx, scope, program.as_ref(), out) } /// Executes a parsed eval program in production builds using elephc runtime hooks. diff --git a/crates/elephc-magician/src/interpreter/include_exec.rs b/crates/elephc-magician/src/interpreter/include_exec.rs index f2a0b9a25b..4de98746d0 100644 --- a/crates/elephc-magician/src/interpreter/include_exec.rs +++ b/crates/elephc-magician/src/interpreter/include_exec.rs @@ -11,6 +11,7 @@ //! - Missing include emits a warning and returns false; missing require is fatal. use super::*; +use crate::parse_cache::parse_fragment_cached; /// Evaluates nested `eval(...)` calls against the current materialized scope. pub(super) fn eval_nested_eval( @@ -24,8 +25,8 @@ pub(super) fn eval_nested_eval( }; let code = eval_expr(code, context, scope, values)?; let code = values.string_bytes(code)?; - let program = parse_fragment(&code).map_err(EvalParseError::status)?; - execute_program_with_context(context, &program, scope, values) + let program = parse_fragment_cached(&code).map_err(EvalParseError::status)?; + execute_program_with_context(context, program.as_ref(), scope, values) } /// Evaluates an eval-fragment include or require expression. @@ -140,7 +141,7 @@ fn eval_execute_include_code( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let program = parse_fragment(code).map_err(EvalParseError::status)?; + let program = parse_fragment_cached(code).map_err(EvalParseError::status)?; let previous = context.call_site(); let file = path.to_string_lossy().into_owned(); let dir = path diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index abc6ef1e18..d92650605a 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -48,6 +48,7 @@ use crate::eval_ir::{ EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; +#[cfg(test)] use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; use crate::value::RuntimeCellHandle; diff --git a/crates/elephc-magician/src/lib.rs b/crates/elephc-magician/src/lib.rs index a09d04e525..0fdac35156 100644 --- a/crates/elephc-magician/src/lib.rs +++ b/crates/elephc-magician/src/lib.rs @@ -20,6 +20,7 @@ pub mod interpreter; mod json_validate; mod lexer; pub mod lower; +mod parse_cache; pub mod parser; pub mod runtime_hooks; pub mod scope; diff --git a/crates/elephc-magician/src/parse_cache.rs b/crates/elephc-magician/src/parse_cache.rs new file mode 100644 index 0000000000..164ba6ca1c --- /dev/null +++ b/crates/elephc-magician/src/parse_cache.rs @@ -0,0 +1,167 @@ +//! Purpose: +//! Caches parsed eval fragments before interpreter execution. +//! This removes repeated tokenization and parsing for identical runtime source +//! bytes while keeping execution context and scope fully dynamic. +//! +//! Called from: +//! - `crate::ffi::execute::__elephc_eval_execute()` +//! - `crate::interpreter::include_exec` for nested eval/include parsing. +//! +//! Key details: +//! - The cache stores immutable EvalIR parse results only, never runtime cells, +//! declarations, scope entries, or context-derived magic-constant values. +//! - Large fragments bypass the cache to avoid pinning one-off source strings. + +use crate::errors::EvalParseError; +use crate::eval_ir::EvalProgram; +use crate::parser; +use std::collections::{HashMap, VecDeque}; +use std::sync::{Arc, Mutex, MutexGuard, OnceLock}; + +const EVAL_PARSE_CACHE_CAPACITY: usize = 256; +const MAX_CACHEABLE_FRAGMENT_BYTES: usize = 64 * 1024; + +type CachedParseResult = Result, EvalParseError>; + +static EVAL_PARSE_CACHE: OnceLock> = OnceLock::new(); + +/// Parses an eval fragment, reusing a cached immutable EvalIR program when available. +pub(crate) fn parse_fragment_cached(code: &[u8]) -> CachedParseResult { + if !is_cacheable_fragment(code) { + return parser::parse_fragment(code).map(Arc::new); + } + if let Some(result) = lock_eval_parse_cache().lookup(code) { + return result; + } + let result = parser::parse_fragment(code).map(Arc::new); + lock_eval_parse_cache().insert(code.to_vec(), result.clone()); + result +} + +/// Returns true when a fragment is small enough to retain in the parse cache. +fn is_cacheable_fragment(code: &[u8]) -> bool { + code.len() <= MAX_CACHEABLE_FRAGMENT_BYTES +} + +/// Returns the process-wide eval parse cache singleton. +fn eval_parse_cache() -> &'static Mutex { + EVAL_PARSE_CACHE.get_or_init(|| Mutex::new(EvalParseCache::new(EVAL_PARSE_CACHE_CAPACITY))) +} + +/// Locks the parse cache and recovers the inner cache if a previous panic poisoned it. +fn lock_eval_parse_cache() -> MutexGuard<'static, EvalParseCache> { + eval_parse_cache() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +/// Bounded FIFO cache for immutable eval parse results. +struct EvalParseCache { + capacity: usize, + entries: HashMap, CachedParseResult>, + order: VecDeque>, +} + +impl EvalParseCache { + /// Creates an empty cache with the requested maximum entry count. + fn new(capacity: usize) -> Self { + Self { + capacity, + entries: HashMap::new(), + order: VecDeque::new(), + } + } + + /// Returns a cloned cached parse result for the exact source bytes. + fn lookup(&self, code: &[u8]) -> Option { + self.entries.get(code).cloned() + } + + /// Inserts a parse result and evicts the oldest distinct source when full. + fn insert(&mut self, code: Vec, result: CachedParseResult) { + if self.capacity == 0 { + return; + } + if self.entries.contains_key(code.as_slice()) { + self.entries.insert(code, result); + return; + } + while self.entries.len() >= self.capacity { + let Some(oldest) = self.order.pop_front() else { + break; + }; + self.entries.remove(&oldest); + } + self.order.push_back(code.clone()); + self.entries.insert(code, result); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies repeated successful parses reuse the stored EvalIR allocation. + #[test] + fn cache_reuses_successful_parse_result() { + let mut cache = EvalParseCache::new(4); + let source = b"return 1;"; + let parsed = Arc::new(parser::parse_fragment(source).expect("fragment should parse")); + + cache.insert(source.to_vec(), Ok(parsed.clone())); + let hit = cache + .lookup(source) + .expect("source should be cached") + .expect("cached source should be successful"); + + assert!(Arc::ptr_eq(&parsed, &hit)); + } + + /// Verifies parse errors are cached too so repeated invalid fragments avoid reparsing. + #[test] + fn cache_reuses_parse_errors() { + let mut cache = EvalParseCache::new(4); + let source = b" Date: Sun, 5 Jul 2026 14:51:36 +0200 Subject: [PATCH 1041/1208] Plan magician performance follow-ups --- .plans/elephc-magician-performance-plan.md | 561 +++++++++++++++++++++ 1 file changed, 561 insertions(+) create mode 100644 .plans/elephc-magician-performance-plan.md diff --git a/.plans/elephc-magician-performance-plan.md b/.plans/elephc-magician-performance-plan.md new file mode 100644 index 0000000000..9c49730076 --- /dev/null +++ b/.plans/elephc-magician-performance-plan.md @@ -0,0 +1,561 @@ +# elephc-magician Performance Plan + +| Order | Work item | Objective | Status | Notes | +|---:|---|---|---|---| +| 1 | Dedicated magician benchmark suite | Measure real hot spots before larger interpreter or bridge refactors | Not started | Establishes a stable baseline for parse, dispatch, scalar arithmetic, builtin calls, callable calls, arrays, references, and include paths. | +| 2 | Eval fragment parse cache | Avoid repeated tokenization and parsing for identical runtime source bytes | Done | Implemented in commit `4ba0efb5c` through `crates/elephc-magician/src/parse_cache.rs`. | +| 3 | Parse-error cache | Avoid reparsing repeated invalid fragments | Done | Included in the same parse cache as successful `EvalProgram` results. | +| 4 | Dynamic context/scope preservation for cached parses | Ensure caching does not freeze magic constants, variables, declarations, or runtime state | Done | The cache stores only immutable parse results; execution still receives the current `ElephcEvalContext` and `ElephcEvalScope`. | +| 5 | Include parse cache | Avoid repeated parsing of identical PHP code blocks loaded through include/require | Partially done | The parse cache now covers parsed include code blocks, but file reads, stat/canonicalize work, and include-path resolution are not cached. | +| 6 | Eval symbol lookup cache | Speed up repeated function/class/method lookup for symbols declared by eval | Not started | Should come after parse caching and before deeper interpreter changes. | +| 7 | Direct builtin dispatch | Avoid repeated string matching and generic dispatch for common builtins | Not started | Needs benchmark data to prioritize the first builtin groups. | +| 8 | Callable resolution cache | Avoid reconstructing equivalent string/array/first-class/closure callables repeatedly | Not started | Best implemented after symbol and builtin lookup behavior is stable. | +| 9 | Reduce `RuntimeValueOps` calls on simple operations | Cut bridge overhead for arithmetic, comparisons, casts, and simple output paths | Not started | This starts touching the interpreter core and needs before/after benchmark evidence. | +| 10 | Unboxed scalar fast paths | Avoid boxing/unboxing for hot int/float/bool/string paths inside eval execution | Not started | Likely high impact for compute-heavy loops, but more invasive than dispatch caches. | +| 11 | Compact bytecode or linear EvalIR form | Reduce tree-walk overhead and branch-heavy dispatch in the current EvalIR interpreter | Not started | Should be guided by benchmarks after simpler caches and scalar fast paths are measured. | +| 12 | Array/reference/COW bridge optimizations | Reduce cost of array mutation and by-reference parameter handling | Not started | Semantically delicate; should happen after scalar and dispatch fast paths are stable. | +| 13 | AOT for literal `eval` | Compile `eval('...')` fragments ahead of time instead of interpreting them through magician | Partial | Commit `4bc962407` marks literal eval calls as AOT candidates, but still uses the bridge fallback. | + +## 1. Dedicated Magician Benchmark Suite + +### Goal + +Create repeatable benchmarks that isolate where `elephc-magician` spends time before making larger performance changes. This should prevent optimizing only the obvious parse path while missing the real cost of interpreter dispatch, value boxing, builtin lookup, callable resolution, or array/reference handling. + +### Scope + +Add a small benchmark harness that compares: + +- Native elephc code without eval. +- elephc code that enters magician through `eval`. +- PHP standard execution without eval. +- PHP execution through `eval`. + +The suite should include both microbenchmarks and a few mixed workloads: + +- Repeated identical small `eval` fragments. +- One large `eval` fragment with a compute-heavy loop. +- Arithmetic-only loops. +- String concatenation and output. +- Array reads/writes. +- Function calls declared inside eval. +- Builtin calls inside eval. +- Callable dispatch inside eval. +- Include/require with repeated code blocks. + +### Likely Files + +- `benches/` or `scripts/bench-eval-*` depending on existing project conventions. +- `tests/codegen/eval*.rs` only for correctness guards, not timing. +- `crates/elephc-magician/src/` only if benchmark-only hooks are required behind `#[cfg(test)]` or a feature gate. + +### Validation + +Each benchmark should record: + +- Runtime wall clock. +- Number of eval invocations. +- Fragment size. +- Whether the fragment is literal or dynamic. +- Whether parse cache should hit. +- Output correctness against PHP where practical. + +### Risks + +Benchmark results can be misleading if compile+assemble+link time is included for runtime comparisons. Runtime-only binaries should be generated once and executed repeatedly. + +## 2. Eval Fragment Parse Cache + +### Goal + +Avoid repeated tokenization and parsing for identical eval source bytes. + +### Current State + +Done in commit `4ba0efb5c`. + +The implementation adds `crates/elephc-magician/src/parse_cache.rs` and routes these call sites through it: + +- `crates/elephc-magician/src/ffi/execute.rs` +- `crates/elephc-magician/src/interpreter/include_exec.rs` + +### Design + +The cache is process-local, bounded, and keyed by exact fragment bytes. It stores immutable `EvalProgram` instances behind `Arc`, plus parse errors. + +Current policy: + +- FIFO capacity: 256 entries. +- Maximum cacheable source: 64 KiB. +- Larger fragments bypass the cache. +- Mutex poisoning is recovered by taking the inner cache. + +### Validation Already Run + +- `cargo test -p elephc-magician parse_cache` +- `cargo test -p elephc-magician execute_program_nested_eval_uses_same_scope` +- `cargo test -p elephc-magician execute_program_include_uses_call_site_and_returns_file_result` +- `cargo test --test codegen_tests test_eval_return_value` +- `git diff --check` + +### Follow-Up + +After benchmarks exist, revisit capacity and maximum source size. If workloads show many repeated fragments above 64 KiB, consider size-based memory budgeting instead of a hard source-length cutoff. + +## 3. Parse-Error Cache + +### Goal + +Avoid reparsing invalid fragments that are repeatedly passed to `eval`. + +### Current State + +Done as part of the parse cache. + +### Design + +The cached result type is: + +```rust +Result, EvalParseError> +``` + +This means both successful parses and parse errors are reusable. + +### Validation Already Run + +The unit test `parse_cache::tests::cache_reuses_parse_errors` verifies that parse errors are cached and returned without reparsing. + +### Follow-Up + +If user code frequently emits many distinct invalid fragments, error caching could retain noise. Benchmark and memory telemetry should decide whether invalid-fragment caching needs a lower capacity or should be disabled for very large invalid inputs. + +## 4. Dynamic Context And Scope Preservation + +### Goal + +Guarantee the parse cache does not alter PHP-observable runtime behavior. + +The cache must not freeze: + +- Variables from the caller scope. +- Variables created by eval. +- Function/class/interface/trait/enum declarations. +- Magic constants that depend on the current call site. +- Include file metadata. +- Pending throw state. +- Return values. + +### Current State + +Done for the parse cache. + +### Design + +The cache stores only the parsed `EvalProgram`. Execution still happens through the existing interpreter entry points and receives the current context and scope every time. + +Magic constants remain safe because EvalIR stores magic-constant nodes and runtime evaluation resolves context-dependent values through `ElephcEvalContext`. + +### Validation Already Run + +- Nested eval scope sharing: `execute_program_nested_eval_uses_same_scope`. +- Include file magic and scope sharing: `execute_program_include_uses_call_site_and_returns_file_result`. +- Native bridge return value: `test_eval_return_value`. + +### Follow-Up + +Add a focused regression test for the same cached fragment executed under two different call sites, verifying `__FILE__` and `__DIR__` remain context-sensitive. + +## 5. Include Parse Cache + +### Goal + +Avoid reparsing identical PHP code blocks loaded through include/require. + +### Current State + +Partially done. + +The current parse cache is used by `eval_execute_include_code()`, so the parsed PHP code block can be reused if the exact source bytes match. + +### Remaining Work + +The following costs are not cached yet: + +- File read bytes. +- `canonicalize()` for include-once keys. +- Include path resolution. +- Open/stat checks. +- Split scanning for multiple `` blocks in one file. + +### Likely Files + +- `crates/elephc-magician/src/interpreter/include_exec.rs` +- Possibly `crates/elephc-magician/src/context.rs` for include-once metadata if shared caching needs context-level state. + +### Implementation Plan + +1. Measure include-heavy workloads first. +2. Add a small include-file cache only if file I/O dominates. +3. Key file cache entries by canonical path plus file metadata where available. +4. Keep include_once semantics in `ElephcEvalContext`; do not move "already included" behavior into a global cache. +5. Ensure `__FILE__` and `__DIR__` still come from the current include path, not from cached source metadata. + +### Validation + +Run focused include tests: + +- `execute_program_include_uses_call_site_and_returns_file_result` +- `execute_program_include_once_skips_regularly_included_file` +- `execute_program_missing_include_warns_and_returns_false` +- `execute_program_missing_require_is_runtime_fatal` + +### Risks + +File caches can easily become stale. A conservative first version should cache parsed source bytes only within one process and avoid hiding file changes unless PHP-compatible behavior is explicitly defined. + +## 6. Eval Symbol Lookup Cache + +### Goal + +Speed up repeated lookup of functions, classes, methods, interfaces, traits, and enums declared dynamically through eval. + +### Motivation + +Once parsing is cached, repeated dynamic calls may still pay for name normalization, case-insensitive matching, namespace fallback, and symbol-table scans. + +### Likely Files + +- `crates/elephc-magician/src/context.rs` +- `crates/elephc-magician/src/interpreter/dynamic_functions.rs` +- `crates/elephc-magician/src/interpreter/reflection.rs` +- `crates/elephc-magician/src/interpreter/statements.rs` + +### Implementation Plan + +1. Inventory current lookup paths for function, class, method, and constant resolution. +2. Identify whether each lookup is already stored in a normalized map. +3. Add cache layers only where repeated lookup still performs normalization or scanning. +4. Invalidate or update caches when eval declares a new symbol. +5. Keep case-insensitive PHP behavior canonical. +6. Preserve namespace fallback for builtins and user symbols. + +### Validation + +Add tests for: + +- Repeated calls to an eval-declared function. +- Case-insensitive lookup. +- Namespaced calls with builtin fallback. +- `function_exists`, `class_exists`, and reflection seeing updated declarations after eval. + +### Risks + +Incorrect caching here can make declarations invisible or make duplicate declarations appear valid. This must be treated as a semantic change, not a pure optimization. + +## 7. Direct Builtin Dispatch + +### Goal + +Avoid repeated generic builtin lookup and string matching for common builtin calls inside eval. + +### Motivation + +Eval currently supports many builtins through interpreter dispatch. If a hot loop repeatedly calls the same builtin, the dispatch path should not repeatedly resolve the same function name from scratch. + +### Likely Files + +- `crates/elephc-magician/src/interpreter/builtins/` +- `crates/elephc-magician/src/interpreter/core_builtins.rs` +- `crates/elephc-magician/src/interpreter/builtin_metadata.rs` +- `crates/elephc-magician/src/interpreter/dynamic_functions.rs` + +### Implementation Plan + +1. Use benchmarks to rank builtin categories by runtime cost. +2. Add a compact builtin id or function pointer to parsed call expressions when safe. +3. Keep unknown/dynamic call paths generic. +4. Preserve PHP case-insensitivity and namespace fallback. +5. Ensure direct dispatch and generic dispatch share argument validation. + +### Validation + +For each optimized builtin group, add parity tests for: + +- Direct call. +- Case-insensitive call. +- Namespaced fallback. +- Named arguments when supported. +- First-class callable and callable aliases when relevant. + +### Risks + +A second builtin registry can drift from the canonical catalog. Any direct dispatch table must be generated from or tightly tied to the existing metadata source. + +## 8. Callable Resolution Cache + +### Goal + +Avoid rebuilding equivalent callable targets repeatedly. + +### Motivation + +Callable semantics now cover string callables, array callables, first-class callable syntax, and closures. Repeatedly resolving the same target can become expensive in callback-heavy eval programs. + +### Likely Files + +- `crates/elephc-magician/src/interpreter/dynamic_functions.rs` +- `crates/elephc-magician/src/interpreter/reflection.rs` +- `crates/elephc-magician/src/context.rs` +- `crates/elephc-magician/src/ffi/callables.rs` + +### Implementation Plan + +1. Define a canonical callable key for stable cases. +2. Cache only callables whose target is stable under current context rules. +3. Invalidate or bypass on symbol-table changes when needed. +4. Keep bound object callables separate from static function/class callables. +5. Do not cache by raw runtime-cell pointer unless ownership and lifetime are explicit. + +### Validation + +Add tests for repeated: + +- String function callables. +- Static string callables like `Class::method`. +- Array callables. +- First-class callables. +- `Closure::fromCallable`. +- By-reference callable arguments. + +### Risks + +Bound method callables can carry `$this`, visibility scope, or closure binding state. Caching must not reuse a callable across an incompatible object or visibility context. + +## 9. Reduce `RuntimeValueOps` Calls On Simple Operations + +### Goal + +Reduce the number of runtime bridge calls needed for simple operations inside eval. + +### Motivation + +Even after parse and dispatch are cached, simple arithmetic can still be slow if each operator repeatedly boxes, unboxes, allocates, or crosses through generic runtime hooks. + +### Likely Files + +- `crates/elephc-magician/src/interpreter/expressions.rs` +- `crates/elephc-magician/src/interpreter/constant_eval.rs` +- `crates/elephc-magician/src/interpreter/runtime_ops.rs` +- `crates/elephc-magician/src/runtime_hooks/ops.rs` + +### Implementation Plan + +1. Count `RuntimeValueOps` calls in arithmetic-heavy eval benchmarks. +2. Identify pure scalar operations where both operands are already scalar cells. +3. Add internal helper paths that perform combined operation + allocation where safe. +4. Avoid changing behavior for arrays, objects, strings with PHP coercion edge cases, refs, or `mixed` values until covered. +5. Keep fatal/error behavior identical to the current generic path. + +### Validation + +Add focused interpreter tests for: + +- Integer arithmetic. +- Float arithmetic. +- String numeric coercion. +- Division/modulo edge cases. +- Boolean comparisons. +- Error/fatal paths. + +### Risks + +PHP scalar coercion is subtle. Every fast path needs either exact compatibility or an explicit fallback to the current generic path. + +## 10. Unboxed Scalar Fast Paths + +### Goal + +Avoid boxing and unboxing hot scalar values inside eval loops. + +### Motivation + +Compute-heavy eval programs are likely dominated by repeated scalar loads, arithmetic, comparisons, and stores. Keeping scalars in a compact internal representation can reduce allocation and bridge overhead. + +### Likely Files + +- `crates/elephc-magician/src/value.rs` +- `crates/elephc-magician/src/interpreter/expressions.rs` +- `crates/elephc-magician/src/interpreter/statements.rs` +- `crates/elephc-magician/src/interpreter/scope_cells.rs` +- `crates/elephc-magician/src/runtime_hooks/` + +### Implementation Plan + +1. Introduce an interpreter-local value enum for hot temporaries, not persistent scope cells. +2. Keep scope-visible values as runtime cells unless ownership semantics are fully modeled. +3. Add unboxed paths for integer and boolean first. +4. Add float after edge cases are verified against PHP. +5. Add string only for immutable literals or clearly owned strings. +6. Box only when a value escapes to scope, output, by-ref parameters, arrays, objects, or runtime hooks. + +### Validation + +Create benchmark and correctness coverage for: + +- Integer loops. +- Float loops. +- Mixed scalar expressions. +- Assignments back into scope. +- Function calls that force boxing. +- Early return and throwable cleanup. + +### Risks + +The danger is splitting magician into two incompatible value systems. The unboxed layer should be a temporary execution optimization with explicit boxing boundaries. + +## 11. Compact Bytecode Or Linear EvalIR Form + +### Goal + +Reduce tree-walk and branch-heavy interpreter dispatch overhead. + +### Motivation + +The current EvalIR is a structured tree. A compact linear representation can improve cache locality and simplify dispatch, especially for loops. + +### Likely Files + +- `crates/elephc-magician/src/eval_ir.rs` +- New module such as `crates/elephc-magician/src/eval_bytecode.rs` +- `crates/elephc-magician/src/interpreter/` +- `crates/elephc-magician/src/parser/` + +### Implementation Plan + +1. Do not replace EvalIR immediately. +2. Add an optional lowering step from `EvalProgram` to a compact executable form. +3. Cache the lowered executable form alongside or inside the parse cache. +4. Start with expression-heavy straight-line code. +5. Add loops and control flow after linear basic blocks are proven. +6. Keep declarations and complex OOP constructs on the existing EvalIR path until needed. + +### Validation + +Run parity tests with both execution engines if a temporary dual path exists: + +- Existing magician interpreter unit tests. +- Focused codegen eval tests. +- PHP cross-checks for edge cases. + +### Risks + +This is a larger architectural change. It should not happen before benchmark data proves tree dispatch is a real bottleneck after parse caching and scalar fast paths. + +## 12. Array, Reference, And COW Bridge Optimizations + +### Goal + +Reduce overhead for array mutation, by-reference parameters, and copy-on-write behavior. + +### Motivation + +Array and reference-heavy eval code can be expensive because correctness requires preserving PHP aliasing, reference cells, and COW rules. + +### Likely Files + +- `crates/elephc-magician/src/interpreter/array_literals.rs` +- `crates/elephc-magician/src/interpreter/scope_cells.rs` +- `crates/elephc-magician/src/interpreter/statements.rs` +- `crates/elephc-magician/src/runtime_hooks/` +- `crates/elephc-magician/src/ffi/` + +### Implementation Plan + +1. Benchmark array mutation and by-ref workloads separately. +2. Identify repeated helper patterns that can be fused safely. +3. Optimize append/set/get paths before broad COW changes. +4. Keep reference binding and mutation behavior covered by regression tests. +5. Add cleanup tests for normal return, fatal, and throwable paths. + +### Validation + +Add tests for: + +- Array append and indexed set. +- Associative key set. +- By-ref function and method parameters. +- Ref-like builtin parameters. +- Aliasing across eval and native code. +- Cleanup after fatal and uncaught throwable. + +### Risks + +This area has high semantic risk. Incorrect optimization can create stale aliases, missed mutations, double frees, or leaks. + +## 13. AOT For Literal `eval` + +### Goal + +Compile literal eval fragments ahead of time so `eval('...')` can bypass the magician interpreter where possible. + +### Current State + +Partial. + +Commit `4bc962407` marks literal eval calls as AOT candidates through the EIR opcode `EvalLiteralCall`, but the backend still emits the bridge fallback to `__elephc_eval_execute`. + +### Motivation + +This is the largest potential speedup for literal eval because the code can become normal native elephc code instead of runtime-parsed and interpreted code. + +### Likely Files + +- `src/ir/` +- `src/ir_lower/expr/` +- `src/codegen_ir/lower_inst/` +- `src/types/` +- `src/name_resolver/` +- `src/resolver/` +- `src/optimize/` +- `crates/elephc-magician/` only for fallback and compatibility. + +### Implementation Plan + +1. Keep the existing fallback bridge as the compatibility path. +2. Parse literal fragments at compile time. +3. Run the same frontend passes needed for normal PHP code where possible. +4. Reject or fall back for constructs that require runtime-only context. +5. Lower eligible literal eval fragments into EIR. +6. Materialize eval-visible scope reads/writes through the same dynamic-scope bridge used by runtime eval. +7. Preserve eval return semantics: `return` exits eval, not the caller function. +8. Preserve declaration side effects for functions/classes declared by eval. +9. Add diagnostics or assembly markers showing whether a literal eval used AOT or fallback. +10. Expand eligibility gradually. + +### Validation + +Add parity tests for: + +- Literal eval assigning existing variables. +- Literal eval creating variables. +- Literal eval returning values. +- Literal eval with output. +- Literal eval declarations. +- Literal eval using builtins. +- Literal eval inside functions and methods. +- Fallback when unsupported syntax is present. +- No magician link for programs without eval. + +### Risks + +AOT eval crosses the static/dynamic boundary. The main risk is accidentally treating eval code as ordinary static code and losing PHP eval semantics for scope, declarations, magic constants, or returns. + +## Recommended Milestone Order + +1. Keep the completed parse cache as the first performance improvement. +2. Add benchmark coverage before changing interpreter internals. +3. Implement symbol lookup, builtin dispatch, and callable caches while they can still be verified as mostly semantic-preserving optimizations. +4. Use benchmark data to decide between reducing `RuntimeValueOps`, unboxed scalars, or compact bytecode first. +5. Treat array/reference/COW improvements as a separate correctness-heavy milestone. +6. Continue AOT literal eval as the strategic long-term path, using the bridge fallback for all unsupported cases. From dd0fad9f2818ae26a3b85db089b4295121b32a92 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 20:37:15 +0200 Subject: [PATCH 1042/1208] docs(eval): plan literal eval AOT completion --- .plans/elephc-eval-aot-complete-plan.md | 3339 +++++++++++++++++++++++ 1 file changed, 3339 insertions(+) create mode 100644 .plans/elephc-eval-aot-complete-plan.md diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md new file mode 100644 index 0000000000..fd0be58239 --- /dev/null +++ b/.plans/elephc-eval-aot-complete-plan.md @@ -0,0 +1,3339 @@ +# Piano: completare AOT per literal `eval` + +## Obiettivo + +Completare il percorso AOT per `eval('...')` quando il codice da eseguire e' +una stringa nota a compile time, evitando il passaggio attraverso +`__elephc_eval_execute` per tutti i frammenti che possono essere compilati in +modo staticamente sicuro. + +Il binario finale non deve incorporare il compilatore elephc, il parser, il +type checker o il codegen. Tutto il lavoro di parsing/lowering/codegen del +frammento literal deve avvenire a compile time. A runtime il programma deve +contenere solo: + +- codice nativo generato per il frammento AOT; +- ABI glue per leggere/scrivere lo scope eval quando necessario; +- runtime helpers gia' esistenti; +- `libelephc-magician` solo quando rimangono eval dinamici o fallback non-AOT. + +## Motivazione + +Il piano performance di magician ha introdotto un primo AOT conservativo per +literal eval, ma oggi il subset e' limitato a scalar return/output/store e a +scope read/write semplici. Frammenti reali come: + +```php +eval('$sum = 0; $n = 2; while ($n <= 100000) { ... } echo $sum;'); +``` + +sono ancora marcati come fallback e chiamano: + +```asm +bl ___elephc_eval_execute +``` + +Sul benchmark "somma dei numeri primi fino a 100000", questo mantiene il path +eval intorno a 800 ms e circa 642 MB RSS, contro circa 14 ms per Elephc +standard e circa 92 ms per PHP CLI. Il completamento dell'AOT deve trasformare +questo tipo di literal eval in codice nativo. + +## Principi + +1. Il backend attivo resta AST -> EIR -> `src/codegen_ir/`. Non estendere il + backend AST legacy. +2. Il fallback a `__elephc_eval_execute` resta obbligatorio per codice dinamico + o per costrutti non ancora supportati. +3. `eval` non ritorna implicitamente l'ultima espressione: senza `return`, + ritorna `null`. +4. `return` dentro eval esce dal frammento eval, non dalla funzione chiamante. +5. Il frammento AOT deve preservare la semantica di scope PHP: variabili del + caller visibili, scritture visibili dopo eval, variabili nuove create da + eval visibili dopo eval. +6. Nessun costrutto deve passare ad AOT se non e' semanticamente coperto da + test e fallback sicuro. +7. Ogni fase deve supportare `macos-aarch64`, `linux-aarch64` e + `linux-x86_64`, oppure essere esplicitamente target-gated con diagnostica, + test e documentazione. + +## Stato attuale + +Gia' implementato: + +- `EvalLiteralCall` conserva il frammento literal nel payload EIR. +- `src/codegen_ir/lower_inst/builtins/eval.rs` prova un parser AOT prima del + bridge. +- Il subset AOT attuale supporta scalar constants, scalar arithmetic, concat, + `echo` / `print`, `return`, scalar stores e read-modify-write scope access + tramite boxed Mixed helpers. +- I fallback literal emettono marker assembly `eval literal AOT fallback`. +- I path AOT emettono marker assembly `eval literal AOT compiled`. + +Mancante per "AOT completo" in senso utile: + +- `while`, `if`, `break`, `continue`; +- locals interni del frammento senza roundtrip continuo nello scope dinamico; +- operatori di confronto e truthiness di controllo; +- compound assignment nel subset AOT; +- chiamate statiche a builtins/funzioni gia' note; +- integrazione piu' diretta con la pipeline frontend/EIR per evitare un secondo + mini-codegen parallelo troppo grande; +- registrazione e test target-aware per frammenti AOT non banali; +- benchmark di accettazione sul caso "primi fino a 100000". + +## Stato implementazione - 2026-07-05 + +Implementato in questo ramo: + +- nuovo percorso AOT `local scalar` in + `src/codegen_ir/lower_inst/builtins/eval.rs`, separato dal percorso boxed + Mixed esistente; +- locals interni del frammento su stack temporaneo, con flag `defined` per + flushare nello scope solo le variabili effettivamente assegnate a runtime; +- supporto AOT per: + - assignment semplice e compound normalizzato dal parser; + - `echo`, `print`, `return`; + - `while`, `if`/`elseif`/`else`, `break`, `continue`; + - valori `int`/`bool` e stringhe solo in output; + - operatori `+`, `-`, `*`, `%`, `<`, `<=`, `>`, `>=`, `==`, `!=`, `&&`, + `||`, concat in `echo`; + - `strlen("literal")` foldato a compile time come primo builtin statico; + - chiamate a funzioni utente statiche gia' note con argomenti `int`/`bool` + in registri e ritorno `int`; +- fallback conservativo ancora attivo per costrutti non coperti, verificato con + `foreach`; +- test assembly/runtime per: + - loop `while` self-contained senza bridge; + - benchmark primi `100000` senza bridge, output `454396537`; + - `strlen("literal")` senza bridge; + - chiamata `inc(int $x): int` senza bridge; + - regressioni boxed scope read/write esistenti. + +Evidenza locale: + +- `cargo test --test codegen_tests literal_eval_ -- --nocapture` + - 5 test passati prima dell'aggiunta del test `strlen`; +- `cargo test --test codegen_tests test_literal_eval_static_strlen_uses_aot_without_execute_bridge -- --nocapture` + - passato; +- `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture` + - passato; +- benchmark temporaneo primi `100000`, 5 iterazioni: + - Elephc standard: mediana `13.451 ms`; + - Elephc via eval AOT: mediana `10.561 ms`; + - PHP standard: mediana `87.725 ms`; + - PHP via eval: mediana `85.066 ms`; + - assembly eval: `__elephc_eval_execute` assente, marker + `eval literal AOT compiled local scalar` presente; +- benchmark `benchmarks/magician/cases/algebra_heavy`, 5 iterazioni: + - Elephc standard: `3.52 ms`; + - Elephc via eval AOT: `4.43 ms`; + - PHP standard: `65.50 ms`; + - PHP via eval: `66.66 ms`. + +Ancora aperto: + +- il percorso `local scalar` e' ancora un mini-codegen manuale, non una + funzione EIR interna; +- il link a `elephc_magician` e' stato rimosso solo per literal eval + native-only senza locals/scope (`return 7`, `strlen("...")`, chiamata utente + statica scalar-register); resta richiesto per frammenti con locals creati + dentro eval per preservare la visibilita' PHP nello scope chiamante; +- gli helper `eval_scope_get/set` e `eval_value_*` usati per preservare scope + vivono ancora sotto la feature runtime eval; +- builtin statici foldabili supportati per `strlen("literal")`, + `intval()` e `abs()`; +- chiamate a funzioni statiche utente supportate solo per il subset + scalar-register (`int`/`bool` args, ritorno `int`, niente by-ref/variadic). + +Aggiornamento Milestone 6 - 2026-07-05: + +- `src/ir_lower/expr/mod.rs` evita `apply_eval_barrier()` per literal eval + classificati come native-only, quindi non dichiara hidden eval + context/scope quando non servono; +- `src/ir_lower/program.rs` distingue `EvalLiteralCall` native-only nello scan + `RuntimeFeatures`, includendo il sottoinsieme di chiamate utente statiche + note con argomenti letterali `int`/`bool` e ritorno `int`; +- `src/codegen_ir/lower_inst/builtins/eval.rs` usa il boxing core runtime per + i ritorni native-only e forza invece lo scope-sync per frammenti con locals + interni, evitando di perdere variabili create da eval; +- test aggiornati: `return 7`, `strlen("test")` e `inc(41)` ora verificano + assenza di `__elephc_eval_*` in user/runtime asm e assenza di + `elephc_magician` tra le librerie richieste; +- aggiunto test esplicito per `$a = 10; echo eval('return $a + 20;');`, che + verifica read dello scope caller via AOT e output `30`; +- `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 8/8; +- `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato. + +Aggiornamento Milestone 2 - 2026-07-05: + +- il parser AOT boxed ora conserva `scope_reads` e `scope_writes` per ogni + frammento literal; +- il path AOT boxed filtra la sync scope: + - flush solo dei nomi letti o scritti dal frammento; + - reload solo dei nomi scritti dal frammento; + - separazione local/global mantenuta tramite le tabelle sync esistenti; +- il test `$a = 10; $unused = 99; echo eval('return $a + 20;')` verifica che + venga emesso un solo `__elephc_eval_scope_set`, quindi `$unused` non viene + flushato nello scope eval; +- verifiche: + - `cargo test --test codegen_tests literal_eval_scope -- --nocapture`: 2/2; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 8/8; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato. + +Aggiornamento Milestone 4 - 2026-07-05: + +- il subset local-scalar AOT folda static builtins puri quando il risultato e' + completamente noto a compile time: + - `strlen("literal")`; + - `intval()` su `int`, `bool` o stringa intera parsabile; + - `abs()` su espressioni integer-literal rappresentabili come `int`; +- il classifier `RuntimeFeatures` riconosce gli stessi fold, quindi i casi + native-only non linkano `elephc_magician`; +- aggiunti test: + - builtin case-insensitive in caller namespaced: + `STRLEN("ab") + InTvAl("40") + ABS(-2)` -> `44`, senza helper eval e + senza `elephc_magician`; + - body local-scalar con `$x = intval("42"); echo $x + 1; return abs(-10);` + -> `4310`, senza bridge; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins -- --nocapture`: 2/2; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 10/10; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato. + +Aggiornamento Milestone 7 - 2026-07-05: + +- aggiunto `src/eval_aot.rs` come modulo target-independent per parsing dei + literal eval, classificazione del subset native-only e folding dei builtin + statici supportati; +- `src/ir_lower/program.rs` e `src/ir_lower/expr/mod.rs` ora usano lo stesso + classifier condiviso per decidere rispettivamente feature runtime e barriera + eval in lowering; +- `src/codegen_ir/lower_inst/builtins/eval.rs` riusa lo stesso parsing/folding + condiviso prima di emettere i path AOT, riducendo la duplicazione tra scan + feature e codegen; +- questo non e' ancora il target finale delle funzioni EIR interne, ma sposta + le decisioni di eleggibilita' fuori dal lowerer assembly e rende piu' + difficile divergere tra "non linkare magician" e "emettere davvero AOT"; +- aggiunto un primo path reale a funzione EIR interna per literal eval + self-contained senza variabili/scope: + - nome interno deterministico non esprimibile come nome PHP sorgente; + - generazione della `Function` EIR sintetica dopo il lowering dei body + ordinari e prima dello scan `RuntimeFeatures`; + - call-site `eval()` ancora rappresentato da `EvalLiteralCall`, ma il + lowerer assembly chiama la funzione EIR sintetica quando presente; + - il subset iniziale e' volutamente stretto (`echo`, `print`, `return`, + `if`/`elseif`/`else` senza scope, literal scalar, operatori scalar + no-scope, builtin statici foldabili e chiamate statiche a funzioni utente + gia' ammesse dal classifier) mentre assignment/local/scope restano sul path + AOT manuale esistente; + - i builtin statici foldabili vengono riscritti a literal integer nell'AST + del frammento prima di abbassare la funzione EIR, evitando dipendenze da + name-resolution runtime o case-sensitivity del frammento eval; +- lo scan `RuntimeFeatures` considera no-bridge anche i frammenti eleggibili + alla funzione EIR interna, non solo il vecchio subset native-only scalar; +- `src/ir_lower/expr/mod.rs` non applica piu' la barriera eval ai literal che + verranno gestiti da funzione EIR interna: questo evita di dichiarare + `EvalContext`/`EvalScope` nascosti e di trascinare helper user-side del bridge + quando il bridge non puo' essere chiamato; +- scelta semantica ancora aperta: i frammenti con assegnazioni o locals creati + dentro eval restano sul percorso `local scalar` manuale finche' non esistono + primitive EIR per sincronizzare correttamente lo scope PHP. Spostarli subito + in una funzione EIR no-param perderebbe la visibilita' delle variabili create + o modificate da eval; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_scalar_return_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_user_function_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins -- --nocapture`: 2/2; + - `cargo test --test codegen_tests test_literal_eval_if_without_scope_uses_eir_aot_function -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_scope -- --nocapture`: 2/2; + - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; + - riproduzione CLI manuale con `eval('if (true) { echo "yes"; } else { echo "no"; }')`: compilazione/link riusciti, output `yes`. + +Aggiornamento Milestone 7.1 - 2026-07-05: + +- `src/eval_aot.rs` ora materializza un `EvalAotPlan` condiviso invece di + esporre solo classifier booleani separati; +- il piano contiene: + - nome funzione EIR sintetica, quando il frammento e' eleggibile; + - AST gia' parsato/foldato per il lowering EIR; + - set conservativi di variabili lette e scritte; + - flag per variabili create dinamicamente, necessita' di eval context, + necessita' di global scope e motivo di fallback; +- `src/ir_lower/program.rs` usa `EvalAotPlan::requires_runtime_eval_bridge()` + per decidere se il modulo deve ancora linkare il runtime eval/magician; +- la raccolta dei candidati AOT EIR usa il nome e il body calcolati dal piano, + evitando di duplicare hashing/classificazione nel lowerer; +- `src/ir_lower/expr/mod.rs` continua a usare lo stesso piano per decidere se + applicare la barriera eval; +- nessuna nuova semantica di assignment/local e' stata spostata nella funzione + EIR no-param: i frammenti che leggono o scrivono scope restano sul percorso + AOT scope/local esistente finche' non vengono introdotte primitive EIR + esplicite per `eval_scope_get/set`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_if_without_scope_uses_eir_aot_function -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4. + +Aggiornamento Milestone 7.2 - 2026-07-05: + +- introdotte primitive EIR esplicite `EvalScopeGet` e `EvalScopeSet`, con + validazione immediato `GlobalName`, effetti conservativi heap/fatal/refcount + e dispatch nel backend EIR target-aware; +- `src/codegen_ir/lower_inst/builtins/eval.rs` abbassa le primitive verso gli + helper runtime esistenti `__elephc_eval_scope_get/set`, usando ABI helpers + invece di registri hardcoded; +- `src/eval_aot.rs` ora puo' produrre una seconda funzione EIR sintetica per + frammenti literal che leggono variabili note dallo scope caller ma non + scrivono scope e non creano nuove variabili; +- la funzione sintetica scope-read riceve un handle allo scope eval, e + `LoweringContext` trasforma le letture selezionate di variabili PHP in + `EvalScopeGet`; +- il call-site `eval()` sincronizza solo i nomi effettivamente letti dal + frammento prima di chiamare la funzione EIR scope-read. La selezione + local/global e' stata corretta per non trattare un nome internato dalla + funzione sintetica come storage globale reale; +- caso supportato e verificato: `$a = 10; echo eval('return $a + 20;')` + usa funzione EIR AOT con scope-read, emette un solo + `__elephc_eval_scope_set`, legge con `__elephc_eval_scope_get`, non chiama + `__elephc_eval_execute` e produce `30`; +- restano fuori da questa migrazione le scritture scope/local complete: + `EvalScopeSet` esiste come primitiva e backend, ma assignment/read-write + dentro eval resta sul percorso AOT manuale finche' non viene portato al + lowering EIR con ownership/reload completi; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_scope_read_return_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; + - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`; + - `git diff --check` focalizzato sui file AOT eval toccati: passato. + +Aggiornamento Milestone 7.3 - 2026-07-05: + +- estesa la funzione EIR scope-AOT ai frammenti read/write semplici, per + esempio `$x = $x + 1`, usando `EvalScopeGet` per leggere il valore caller e + `EvalScopeSet` per scrivere il risultato nello scope eval; +- `EvalAotPlan` distingue ora le letture che devono arrivare dal caller dalle + letture di variabili gia' inizializzate dentro il frammento. Questo evita di + spostare benchmark/local temporaries come prime-loop e while local-scalar nel + path scope EIR, che sarebbe corretto ma molto piu' costoso; +- `LoweringContext` supporta `enable_eval_scope_access(read_names, + write_names)`: le letture selezionate abbassano a `EvalScopeGet`, le + assegnazioni semplici selezionate abbassano a `EvalScopeSet` senza creare + slot locali interni; +- il call-site EIR scope-AOT: + - flusha nello scope solo i nomi letti dal caller; + - chiama la funzione EIR sintetica con l'handle dello scope; + - salva il valore di ritorno durante i reload; + - ricarica nel caller solo i nomi scritti dal frammento; + - per i global-backed writes legge dallo scope locale scritto dalla funzione + EIR, non dallo scope globale separato del bridge; +- il test read/write ora verifica output runtime (`2`) oltre a marker EIR, + `__elephc_eval_scope_get/set` e assenza di `__elephc_eval_execute`; +- restano aperti: + - creazione completa di nuove variabili via funzione EIR, quando non c'e' + una lettura caller iniziale; + - assegnazioni non semplici (`array`, property, list/unpack, by-ref, dynamic + variable); + - ownership/reload piu' ampi per shape array/object oltre al subset gia' + coperto dai sync helpers esistenti; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_scope_read_write_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; + - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. + +Aggiornamento Milestone 7.4 - 2026-07-05: + +- abilitata la funzione EIR scope-AOT anche per frammenti writes-only lineari + senza letture interne di variabili, come `eval('$created = "yes";')`; +- il gate e' volutamente stretto: non prende body con letture interne dopo + una scrittura (`$x = intval("42"); echo $x`) e non prende loop/local body + come il prime benchmark, che restano sul percorso local-scalar ottimizzato; +- il classifier conserva due viste distinte: + - letture raw del frammento, usate per capire se un writes-only e' davvero + una creazione/store lineare; + - letture che devono arrivare dal caller, usate per decidere il flush scope + prima della funzione EIR; +- il test `test_literal_eval_scalar_store_uses_aot_scope_write` ora verifica + esplicitamente il marker `eval literal AOT compiled EIR function`, la + scrittura `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute` e + output runtime `yes`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_scalar_store_uses_aot_scope_write -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins_in_local_body_use_aot -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; + - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. + +Aggiornamento Milestone 7.5 - 2026-07-05: + +- aggiunto un finalizer EIR per funzioni eval scope-aware: prima di ogni + `return` esplicito o implicito, `LoweringContext` puo' flushare un set + selezionato di local slot nel runtime eval scope tramite `EvalScopeSet`; +- `EvalAotPlan` distingue ora: + - `writes`: tutti i nomi scritti dal frammento, usati dal call-site per il + reload nel caller; + - `direct_writes`: assegnazioni che devono scrivere subito nello scope + durante il corpo EIR, usate per read-modify-write come `$x = $x + 1`; + - `flush_writes`: assegnazioni mantenute come local EIR interni e scritte + nello scope solo a fine funzione; +- i frammenti con write locali ma senza letture iniziali dal caller, inclusi + local-body con builtin foldabili, `while` self-contained e il prime-sum + benchmark, ora vengono abbassati a funzione EIR scope-aware invece che al + mini-codegen manuale `local scalar`; +- il vecchio percorso manuale `local scalar` resta come fallback di codegen per + frammenti non ancora pianificati come funzione EIR, ma non e' piu' il path + atteso per il benchmark dei primi coperto dai test; +- `requires_runtime_eval_bridge()` non tratta piu' read/write come motivo di + bridge quando il piano ha gia' una funzione EIR scope-aware: serve il runtime + eval-scope, ma non l'entrypoint interpretato `__elephc_eval_execute`; +- test aggiornati per aspettarsi il marker + `eval literal AOT compiled EIR function` nei casi local-body, while e + prime-loop; +- verifiche eseguite finora: + - `cargo test --test codegen_tests test_literal_eval_local_while_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins_in_local_body_use_aot -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_prime_loop_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; + - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`; + - `git diff --check` focalizzato sui file AOT eval toccati: passato. + +Aggiornamento Milestone 7.6 - 2026-07-05: + +- ampliato il folding compile-time condiviso dei builtin statici puri nel + planner AOT eval, in modo che scan runtime, lowering EIR sintetico e codegen + vedano lo stesso sottoinsieme; +- oltre a `strlen`, `intval` e `abs`, il planner folda ora: + - `boolval()` su literal `int`/`bool`/`string`/`null`; + - `ord("...")`; + - `chr()`; + - `min()` / `max()` su argomenti integer literal; + - `strtolower()`, `strtoupper()` e `strrev()` su stringhe ASCII literal; +- le trasformazioni stringa sono volutamente ASCII-only per non promettere + semantica byte-string non ancora rappresentabile in modo sicuro dall'AST + `StringLiteral`; +- aggiunto test namespaced/case-insensitive che verifica EIR AOT, assenza di + `__elephc_eval_execute`, assenza di helper eval in user/runtime assembly, + assenza di `elephc_magician` e output `AB:cd:cba77`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_more_static_builtins_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 12/12. + +Aggiornamento Milestone 7.7 - 2026-07-05: + +- il path EIR scope-read read-only puo' ora evitare completamente lo scope + runtime eval: la funzione sintetica riceve un parametro `mixed` per ogni + variabile letta dal caller; +- `src/ir_lower/function.rs` genera la signature diretta dei read params e + abbassa il body senza `EvalScopeGet` quando il frammento non scrive scope; +- `src/codegen_ir/lower_inst/builtins/eval.rs` materializza i parametri + leggendo i local slot del caller, boxandoli a `Mixed`, e chiama la funzione + EIR sintetica senza `__elephc_eval_scope_get/set` e senza + `__elephc_eval_execute`; +- `src/ir_lower/expr/mod.rs` evita la barriera eval per questo subset solo se + le variabili lette sono local PHP ordinari, non global/superglobal/by-ref, e + hanno tipi supportati; +- `src/ir_lower/program.rs` rileva comunque eventuale stato eval hidden nei + locals prima di decidere le runtime features, evitando link mancanti quando + altri path richiedono ancora magician; +- `src/ir_passes/dead_store.rs` esclude dal dead-store scalar slots letti + implicitamente da `EvalLiteralCall`: senza questa esclusione il DSE non vedeva + il load generato piu' tardi in codegen e trasformava `$a = 10` in `nop`; +- il test `$a = 10; $unused = 99; echo eval('return $a + 20;'); echo ':' . + $unused;` ora verifica: + - marker `eval literal AOT compiled EIR function with direct read params`; + - assenza di helper `__elephc_eval_*` in user/runtime assembly; + - assenza di `elephc_magician` tra le librerie richieste; + - output runtime `30:99`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_scope_read_return_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 12/12; + - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; + - riproduzione CLI manuale con `>` e l'unario `~`, riusando il normale lowering EIR invece di + introdurre codegen eval-specifico; +- gli assignment compound gia' normalizzati dal parser possono quindi usare il + path AOT anche per `/=`, `%=`, `&=`, `|=`, `^=`, `<<=` e `>>=`, con flush + finale nello scope eval quando il frammento crea o aggiorna variabili; +- il folding condiviso dei builtin statici ricorre anche dentro l'operando di + `~`, in linea con gli altri operatori unari supportati; +- aggiunti test per: + - divisione e potenza in frammenti no-scope, senza bridge, eval context o + link a `elephc_magician`; + - `/=` e `%=` con variabile locale creata dal frammento e visibile dopo + `eval`; + - bitwise, shift e operatori compound bitwise/shift con sync finale dello + scope; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_division_and_pow_use_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_bitwise_shift_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 29/29. + +Aggiornamento Milestone 3/7.23 - 2026-07-06: + +- il gate condiviso EIR AOT accetta ora anche l'operatore spaceship `<=>` + quando gli operandi appartengono al subset EIR sicuro; +- il supporto riusa `Op::Spaceship` e il normale lowering EIR, senza aggiungere + codegen specifico per eval; +- aggiunto test con: + - confronti no-scope interi e float; + - confronto su variabile caller `$a` tramite direct read params; + - assenza di `__elephc_eval_*`, runtime bridge e `elephc_magician`; +- rinominato il test runtime generico da `test_eval_spaceship_execute_through_bridge` + a `test_eval_spaceship_executes`, per non descrivere piu' come bridge un caso + che ora puo' passare dall'AOT; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_spaceship_uses_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_spaceship_executes -- --nocapture`: passato. + +Aggiornamento Milestone 4/7.24 - 2026-07-06: + +- il folder condiviso dei builtin statici pure per literal eval AOT copre ora + anche un sottoinsieme conservativo di builtin numerici con risultato `float`: + - `floor()` e `ceil()` su literal numerici finiti; + - `sqrt()` su literal numerici finiti non negativi; + - `round()` a un solo argomento su literal numerici finiti; +- il fold evita casi potenzialmente divergenti o non ancora rappresentati in + modo stabile nel subset eval AOT: precisione esplicita di `round`, `NaN`, + `INF`, `sqrt()` negativo, stringhe numeriche parziali e interi non + rappresentabili esattamente come `f64`; +- il programma foldato contiene `FloatLiteral`, quindi il normale gate EIR AOT + e il normale lowering EIR gestiscono il frammento senza codegen specifico per + eval; +- aggiunto test namespaced/case-insensitive con `FLOOR`, `ceil`, `sqrt` e + `round`, verificando funzione EIR AOT, assenza di helper eval, assenza di + runtime bridge e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_numeric_static_builtins_use_eir_aot_without_magician -- --nocapture`: passato. + +Aggiornamento Milestone 0/7.25 - 2026-07-06: + +- il planner condiviso `EvalAotPlan` espone ora una ragione conservativa di + fallback leggibile, invece di distinguere solo parse error e fallback + generico; +- il classifier target-independent riconosce le principali famiglie che devono + restare sul bridge finche' non sono modellate nell'AOT: + - include/require; + - declaration runtime; + - `global`/`static`; + - references/by-ref; + - dynamic calls; + - dynamic class/member access; + - object/member access; + - array/iterable; + - try/throw; + - control-flow o scope non supportato; +- il marker assembly del fallback literal mantiene il prefisso stabile + `eval literal AOT fallback`, ma ora include anche la ragione quando il + planner puo' classificarla; +- aggiornato il test fallback `foreach ([1] as $x)` per verificare il marker + `array/iterable semantics need bridge fallback`; +- verifiche: + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo check --tests`: passato. + +Aggiornamento Milestone 4/7.26 - 2026-07-06: + +- il folder condiviso dei builtin statici pure per literal eval AOT copre ora + `count()` su array literal completamente statici; +- il fold resta volutamente conservativo: + - supporta solo la forma a un argomento, senza `COUNT_RECURSIVE`; + - richiede che tutti i valori dell'array siano literal/fold statici senza + side effect; + - rifiuta spread e chiavi non letterali; + - per array associativi normalizza le chiavi stringa-intero e deduplica le + chiavi finali, cosi' `["1" => "a", 1 => "b"]` conta come PHP; +- il folder ricorre ora dentro array literal e assoc literal prima di tentare + i fold builtin, cosi' array statici con elementi gia' foldabili restano + side-effect-free; +- aggiunto test namespaced/case-insensitive con `COUNT([1, 2, 3])`, assoc + statici, chiavi duplicate normalizzate e array annidati top-level, verificando + funzione EIR AOT, assenza di helper eval, assenza di runtime bridge e assenza + di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_array_count_uses_eir_aot_without_magician -- --nocapture`: passato. + +Aggiornamento Milestone 4/7.27 - 2026-07-06: + +- il folder condiviso dei builtin statici pure per literal eval AOT copre ora + anche un sottoinsieme conservativo di builtin stringa: + - `substr()` su stringhe ASCII literal, offset non negativo e lunghezza + opzionale non negativa; + - `str_repeat()` su stringhe ASCII literal e repeat count non negativo, con + limite statico sul risultato foldato per evitare di gonfiare il binario in + compile time; +- i casi byte/locale sensibili restano volutamente fuori dal fold: stringhe + non ASCII, offset/lunghezze negative e risultati statici troppo grandi + continuano a usare il fallback disponibile; +- aggiunto test namespaced/case-insensitive con `SUBSTR`, `substr`, + `str_repeat` e `strlen(str_repeat(...))`, verificando funzione EIR AOT, + assenza di helper eval, assenza di runtime bridge e assenza di + `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_string_builtins_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 33/33. + +Aggiornamento Milestone 4/7.28 - 2026-07-06: + +- il folder condiviso dei builtin statici pure per literal eval AOT copre ora + anche trasformazioni ASCII monargomento: + - `ucfirst()` e `lcfirst()` su stringhe ASCII literal; + - `trim()`, `ltrim()`, `rtrim()` e alias `chop()` con maschera PHP default; +- il fold resta limitato alla forma senza `charlist` esplicito e rifiuta + stringhe non ASCII, cosi' non duplica la semantica completa delle maschere + custom o di stringhe byte non rappresentabili stabilmente nel subset; +- aggiunto test namespaced/case-insensitive con first-character case, trim + bilaterale e trim laterali, verificando funzione EIR AOT, assenza di helper + eval, assenza di runtime bridge e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_ascii_text_builtins_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 34/34. + +Aggiornamento Milestone 4/7.29 - 2026-07-06: + +- il folder condiviso dei builtin statici pure per literal eval AOT copre ora + anche predicate stringa binari su literal ASCII: + - `str_contains()`; + - `str_starts_with()`; + - `str_ends_with()`; +- il risultato viene riscritto a `BoolLiteral` prima del gate EIR, quindi + ternari/condizioni nel frammento possono restare sul percorso funzione EIR + senza emettere helper runtime di ricerca stringa; +- il fold rifiuta stringhe non ASCII per mantenere il sottoinsieme byte-stable + gia' usato dalle altre trasformazioni statiche stringa; +- aggiunto test namespaced/case-insensitive con match positivo, negativo e + needle vuoto, verificando funzione EIR AOT, assenza di helper eval, assenza + di runtime bridge e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_string_predicates_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 35/35. + +Aggiornamento Milestone 4/7.30 - 2026-07-06: + +- il folder condiviso dei builtin statici pure per literal eval AOT copre ora + `array_key_exists()` su array literal completamente statici; +- la normalizzazione delle chiavi statiche e' stata centralizzata in + `static_array_key_ids()`, riusata anche da `count()`: + - array numerici producono chiavi `0..len-1`; + - array associativi normalizzano stringhe-intero come PHP; + - duplicate key vengono deduplicate nella vista finale; +- il fold resta limitato a chiavi literal `int`/`string` e array literal senza + side effect, lasciando sul fallback chiavi dinamiche, spread o array non + statici; +- aggiunto test namespaced/case-insensitive con chiavi presenti/mancanti, + normalizzazione `"1"`/`1`, array numerici e associativi, verificando funzione + EIR AOT, assenza di helper eval, assenza di runtime bridge e assenza di + `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_array_count_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 36/36. + +Aggiornamento Milestone 4/7.31 - 2026-07-06: + +- il folder condiviso dei builtin statici pure per literal eval AOT normalizza + ora gli argomenti tramite `src/types/call_args::plan_call_args()` e le + signature canoniche `builtin_call_sig()` prima di provare il fold; +- questo abilita named arguments e static associative spread nei fold AOT senza + duplicare le regole PHP di matching parametri, duplicate detection, + ordinamento named/positional e default trailing; +- i fold restano conservativi: spread dinamici o materializzazioni non + statiche producono espressioni normalizzate non foldabili e quindi restano + fallback come prima; +- aggiunto test con: + - `strlen(string: "...")`; + - `strlen(...["string" => "..."])`; + - `count(value: [...])`; + - `array_key_exists(array: ..., key: ...)`; + - `str_contains(needle: ..., haystack: ...)`; + - `str_repeat(times: ..., string: ...)`; + - `substr(length: ..., string: ..., offset: ...)`; + verificando funzione EIR AOT, assenza di helper eval, assenza di runtime + bridge e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_builtin_named_args_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins_use_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 37/37. + +Aggiornamento Milestone 5/7.32 - 2026-07-06: + +- il gate condiviso per chiamate statiche a funzioni utente dentro literal eval + AOT normalizza ora gli argomenti con `plan_call_args()` prima di verificare + tipi scalar-register e arita'; +- questo abilita named arguments nelle chiamate utente statiche AOT senza + mantenere un controllo locale sugli `ExprKind::NamedArg` sorgenti; +- il controllo resta conservativo: + - niente by-ref; + - niente variadic user function; + - ritorni solo `int`/`bool`/`float`/`string`; + - argomenti normalizzati solo se restano literal scalar supportati; +- aggiunto test con funzione utente `join_named(string $left, string $right, + bool $bang): string` chiamata da eval con named args fuori ordine, + verificando funzione EIR AOT, assenza di helper eval, assenza di runtime + bridge e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_user_function_named_args_use_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 38/38. + +Aggiornamento Milestone 5/7.33 - 2026-07-06: + +- il gate condiviso per chiamate statiche a funzioni utente dentro literal eval + AOT accetta ora anche default scalar dei parametri quando il call-site omette + argomenti opzionali; +- per chiamate posizionali senza spread il gate estende gli argomenti con i + default trailing gia' presenti nella `FunctionSig`, e poi applica lo stesso + controllo scalar-register usato per gli argomenti espliciti; +- per chiamate named/static-spread il gate usa `plan_call_args(..., + trim_trailing_defaults = false)` cosi' i default intermedi vengono + materializzati dal planner condiviso; +- il supporto resta conservativo: default non scalar, by-ref, variadic e tipi + non `int`/`bool`/`float`/`string` restano fuori dal subset AOT; +- aggiunto test con funzione utente `greet_default(string $name, + string $suffix = "!", bool $loud = true): string` chiamata da eval sia con + argomenti posizionali omessi sia con named args fuori ordine e default + intermedio, verificando funzione EIR AOT, assenza di helper eval, assenza di + runtime bridge e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_user_function_defaults_use_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 39/39. + +Aggiornamento Milestone 3/7.34 - 2026-07-06: + +- il gate condiviso EIR AOT accetta ora incrementi/decrementi locali + (`$i++`, `++$i`, `$i--`, `--$i`) quando la variabile e' nota come local int + definita dal frammento eval; +- la classificazione AOT traccia ora `EirLocalFacts` invece del solo set di + variabili assegnate: + - `assigned` indica variabili definite nel path corrente; + - `int_locals` indica variabili sicuramente intere, usate per accettare + inc/dec senza aprire casi PHP string/float/bool non modellati; +- i facts vengono propagati conservativamente: + - i loop usano una copia dei facts per il body/update; + - gli `if` con `else` mantengono solo facts presenti in tutti i branch; + - i `switch` non promuovono nuovi facts oltre il blocco, come prima per le + assegnazioni; +- aggiunto test con post/pre increment e decrement, `for ($j++)` e + `for (--$k)` in literal eval, verificando funzione EIR AOT, flush scope + finale e assenza di `__elephc_eval_execute`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_local_inc_dec_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 40/40. + +Aggiornamento Milestone 3/7.35 - 2026-07-06: + +- il gate condiviso EIR AOT accetta ora i construct `isset()` ed `empty()` + quando restano nel subset scalare gia' modellato dal normale lowering EIR; +- `isset()` viene abilitato solo per variabili locali sicuramente assegnate + nel frammento eval: + - niente named args o spread; + - niente variabili mancanti del caller; + - niente offset, property, nullsafe o object probes finche' la semantica lazy + specifica non viene modellata nel piano AOT; +- `empty()` viene abilitato per un singolo argomento senza named/spread quando + l'argomento e' una variabile locale assegnata o un'espressione gia' accettata + dal gate EIR AOT; +- aggiunto test con `$zero`, `$blank`, `$value` e `$nullish` definiti dentro + literal eval, verificando `isset`, `empty`, flush finale dello scope e + assenza di `__elephc_eval_execute`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_local_isset_empty_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 41/41. + +Aggiornamento Milestone 3/7.36 - 2026-07-06: + +- il gate EIR AOT accetta ora `print` anche quando e' usato come espressione, + non solo come `ExprStmt(print ...)`; +- la classificazione mantiene la semantica side-effectful di `print` delegando + al normale lowering EIR, ma usa il fatto che `print` ritorna `1` per + mantenere il tracking `int_locals` su assegnamenti come `$x = print "A";`; +- aggiunto test con `$x = print "A";` ed `echo print "B";` dentro literal eval, + verificando output, flush finale dello scope e assenza di + `__elephc_eval_execute`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_print_expression_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 42/42. + +Aggiornamento Milestone 3/7.37 - 2026-07-06: + +- il gate EIR AOT accetta ora espressioni con soppressione errori (`@expr`) + quando l'espressione interna e' gia' nel subset AOT; +- il lowering resta quello EIR normale (`ErrorSuppressBegin` / + `ErrorSuppressEnd`) e quindi non introduce un percorso speciale nel bridge + eval; +- il tracking `int_locals` propaga il tipo intero attraverso `@expr` quando il + valore interno e' noto intero, ad esempio `$x = @intval("4");`; +- aggiunto test con `@strlen("ab")` e `$x = @intval("4")` dentro literal eval, + verificando output, flush finale dello scope e assenza di + `__elephc_eval_execute`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_error_suppress_expression_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 43/43. + +Aggiornamento Milestone 3/7.38 - 2026-07-06: + +- il gate EIR AOT per `isset()` ed `empty()` accetta ora anche variabili lette + dallo scope caller tramite direct read params, non solo variabili locali gia' + assegnate nel frammento eval; +- le variabili caller mancanti continuano a essere materializzate come + `Mixed null`, come gia' avveniva per `??`, quindi: + - `isset($missing)` resta `false`; + - `empty($missing)` resta `true`; + - non serve creare eval scope/context runtime; +- il supporto resta ristretto alle variabili semplici: offset, property, + nullsafe/object probes e named/spread args restano fuori dal subset AOT fino + a modellazione lazy dedicata; +- aggiunto test con `$missing`, `$nullish`, `$zero` e `$blank` dal caller, + verificando direct read params, assenza di helper `__elephc_eval_*`, assenza + di runtime bridge, assenza di `elephc_magician` e output PHP corretto; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_scope_isset_empty_uses_direct_params_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. + +Aggiornamento Milestone 6/7.40 - 2026-07-06: + +- aggiunto un sottoinsieme direct local-store per literal eval write-only con + assegnamenti statici a variabili semplici, ad esempio + `eval('$created = "yes";'); echo $created;`; +- il classifier condiviso in `src/eval_aot.rs` riconosce questi frammenti e + restituisce l'elenco ordinato delle write statiche con categoria scalare + (`null`, `bool`, `int`, `float`, `string`); +- `src/ir_lower/expr/mod.rs` salta la eval barrier per questi frammenti, quindi + non dichiara gli slot nascosti `EvalContext` / `EvalScope` / + `EvalGlobalScope` quando non servono; +- `src/ir_lower/program.rs` evita sia la funzione AOT sintetica con + `EvalScopeSet` sia il flag `features.eval` per lo stesso subset; +- `src/codegen_ir/lower_inst/builtins/eval.rs` emette store diretti nei local + slot del caller per tipi scalari supportati, incluso `Mixed` tramite boxing + core runtime, senza `__elephc_eval_scope_set` e senza + `__elephc_eval_execute`; +- aggiunto test `test_literal_eval_scalar_store_uses_direct_local_write_without_magician`, + con verifica di output `yes`, marker direct-local-store, assenza di + `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute` e assenza di + `elephc_magician` tra le librerie richieste; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_scalar_store_uses_direct_local_write_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. + +Gap residuo Milestone 6 - 2026-07-06: + +- i frammenti con scope-write EIR piu' ricchi che non rientrano nel subset + local-scalar direct-sync usano ancora il path `EvalScopeSet`; +- `src/ir_lower/program.rs` marca correttamente ogni `EvalScopeGet` / + `EvalScopeSet` come `features.eval = true`, quindi quei casi continuano a + linkare `elephc_magician` finche' lo scope glue AOT non verra' separato dal + bridge interpretato o sostituito da direct write/reload piu' generale. + +Aggiornamento Milestone 6/7.41 - 2026-07-06: + +- aggiunto un classifier condiviso per il sottoinsieme local-scalar AOT + (`int`/`bool`, assegnamenti semplici, `if`, `while`, `break`, `continue`, + `echo`, `print`, `return` e chiamate statiche gia' supportate dal vecchio + path local-scalar); +- quando il frammento rientra in questo subset, `src/ir_lower/program.rs` + evita di generare la funzione EIR scope-aware che avrebbe introdotto + `EvalScopeSet`, e lo scan runtime non abilita `features.eval`; +- `src/ir_lower/expr/mod.rs` evita la eval barrier per lo stesso subset solo + se le write statiche possono essere ignorate perche' non materializzate nel + caller oppure scritte in local PHP compatibili (`Mixed`/union, `int`, `bool`, + o tagged int); +- `src/codegen_ir/lower_inst/builtins/eval.rs` ha ora una finalizzazione + `local scalar with direct local sync`: il frammento gira sui suoi slot + temporanei e, a fine esecuzione, copia le variabili definite nei local slot + del caller senza `ElephcEvalScope`; +- il test `test_literal_eval_local_while_uses_aot_without_execute_bridge` + legge `$sum` dopo `eval`, verificando output `55:55`, marker direct-sync, + assenza di `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, + assenza di helper eval runtime e assenza di `elephc_magician`; +- il test `test_literal_eval_prime_loop_uses_aot_without_execute_bridge` + verifica ora il benchmark dei primi con path local-scalar direct-sync, + output `454396537`, assenza di `__elephc_eval_scope_set`, + assenza di `__elephc_eval_execute` e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_local_while_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_prime_loop_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44; + - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. + +Aggiornamento Milestone 6/7.42 - 2026-07-06: + +- aggiunto un sottoinsieme direct read/write per literal eval boxed con + assegnamenti interi che leggono la stessa variabile del caller, ad esempio + `eval('$x = $x + 1;')`; +- il classifier condiviso accetta solo espressioni intere strette + (`literal int`, `$target`, unary minus e `+`, `-`, `*`, `%`) e non copre + ancora `Mixed`, stringhe, concat, altri nomi caller o conversioni PHP + implicite; +- `src/ir_lower/program.rs` salta la funzione EIR scope-aware e non abilita + `features.eval` solo quando il target e' un local PHP `int` inizializzato + prima dell'eval; +- `src/ir_lower/expr/mod.rs` evita la eval barrier per lo stesso subset solo + se il local e' gia' nello snapshot di inizializzazione e ha tipo `int`; +- `src/codegen_ir/lower_inst/builtins/eval.rs` emette il marker + `eval literal AOT compiled direct local read/write stores`, carica il local + caller, calcola l'espressione intera target-aware e riscrive direttamente lo + slot locale senza `EvalScopeGet`/`EvalScopeSet`; +- il test `test_literal_eval_scope_read_write_uses_aot_without_execute_bridge` + ora verifica output `2`, assenza di ogni `__elephc_eval_*` in user/runtime + assembly e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_scope_read_write_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. + +Aggiornamento Milestone 6/7.43 - 2026-07-06: + +- esteso il subset local-scalar direct-sync a `do/while` e `for`, con + semantica di `continue` corretta: + - in `do/while`, `continue` salta al controllo finale della condizione; + - in `for`, `continue` salta alla clausola `update` prima di tornare alla + condizione; +- il classifier condiviso in `src/eval_aot.rs` riconosce ora `do/while`, + `for` e builtin statici interi foldabili, come `strlen("...")`, nello stesso + subset usato dal backend local-scalar; +- `src/codegen_ir/lower_inst/builtins/eval.rs` rappresenta ed emette + `DoWhile` e `For` nel mini-codegen local-scalar esistente, senza introdurre + `EvalScopeSet` o helper eval runtime; +- i test `test_literal_eval_do_while_uses_aot_without_execute_bridge`, + `test_literal_eval_for_loop_uses_aot_without_execute_bridge`, + `test_literal_eval_local_inc_dec_uses_aot_without_execute_bridge` e + `test_literal_eval_static_scalar_builtins_in_local_body_use_aot` verificano + marker `local scalar with direct local sync`, assenza di + `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, assenza di + helper `__elephc_eval_*` nel runtime e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_do_while_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_for_loop_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44; + - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. + +Aggiornamento Milestone 6/7.44 - 2026-07-06: + +- esteso il subset local-scalar direct-sync a `print` usato come espressione: + il backend emette l'output dell'operando, poi materializza il valore PHP + `1` nel registro risultato; +- `@expr` viene accettato nel subset local-scalar quando `expr` e' gia' + supportato dal subset stesso; questo copre i builtin statici foldabili nei + test senza introdurre nuova semantica di error reporting; +- `src/eval_aot.rs` e `src/codegen_ir/lower_inst/builtins/eval.rs` sono stati + allineati: il classifier vede `print` come side effect di echo + risultato + `int`, e il backend rappresenta `Print` come espressione local-scalar; +- i test `test_literal_eval_print_expression_uses_aot_without_execute_bridge` + e `test_literal_eval_error_suppress_expression_uses_aot_without_execute_bridge` + verificano ora il marker `local scalar with direct local sync`, assenza di + `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, assenza di + helper `__elephc_eval_*` nel runtime e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_print_expression_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_error_suppress_expression_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44; + - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. + +Aggiornamento Milestone 6/7.45 - 2026-07-06: + +- esteso il subset local-scalar direct-sync agli operatori interi bitwise e + shift: `~`, `&`, `|`, `^`, `<<`, `>>`; +- il classifier condiviso accetta solo operandi `int`, evitando conversioni + PHP implicite non modellate; +- il backend local-scalar emette lowering target-aware: + - AArch64: `mvn`, `and`, `orr`, `eor`, `lsl`, `asr`; + - x86_64: `not`, `and`, `or`, `xor`, `shl`, `sar`, con shift count in + `cl`; +- il test `test_literal_eval_bitwise_shift_uses_aot_without_execute_bridge` + verifica ora il marker `local scalar with direct local sync`, assenza di + `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, assenza di + helper `__elephc_eval_*` nel runtime e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_bitwise_shift_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44; + - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. + +Aggiornamento Milestone 4/7.39 - 2026-07-06: + +- il folder condiviso dei builtin statici pure per literal eval AOT copre ora: + - `gettype()` su literal `int`, `float`, `string`, `bool` e `null`, con le + spelling PHP stabili `integer`, `double`, `string`, `boolean`, `NULL`; + - `is_scalar()` sugli stessi literal scalari/null; +- il fold resta conservativo e non folda array/object/argomenti non literal, + cosi' non elimina side effect di valutazione non modellati; +- esteso il test dei builtin statici con `GETTYPE(1.25)`, `gettype(null)`, + `IS_SCALAR("x")` e `is_scalar(null)`, mantenendo funzione EIR AOT, assenza + di helper eval e assenza di `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_more_static_builtins_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. + +Aggiornamento Milestone 3/7.46 - 2026-07-06: + +- il percorso local-scalar direct-sync accetta ora `switch` quando subject, + case e body appartengono al subset `int`/`bool` gia' supportato; +- il lowering conserva il fallthrough PHP tra case consecutivi ed emette un + target `break` locale allo switch; +- `continue` dentro lo switch resta fallback conservativo per evitare semantiche + ambigue rispetto ai loop esterni; +- `default` prima di un `case` resta fallback perche' il lowerer local-scalar + non ricostruisce ancora quell'ordine di esecuzione PHP; +- il test `test_literal_eval_switch_uses_aot_without_execute_bridge` verifica + ora marker `eval literal AOT compiled local scalar with direct local sync`, + assenza di `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, + assenza di helper runtime `__elephc_eval_` e assenza di `elephc_magician`; +- il test `test_literal_eval_switch_default_before_case_uses_bridge_fallback` + copre il fallback obbligatorio per `default` prima di `case`; +- residui noti dopo questa tranche: + - `test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge` + e' AOT senza bridge ma usa ancora sync scope boxed, perche' `/=` produce + float PHP anche quando il divisore e' esatto; portarlo al direct-sync + richiede storage/boxing float o un modello valore piu' ricco; + - `test_literal_eval_local_isset_empty_uses_aot_without_execute_bridge` e' + AOT senza bridge ma usa ancora sync scope boxed, perche' il direct-sync + attuale non ha storage locale diretto per string/null usati da + `isset()`/`empty()`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_switch_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_switch_default_before_case_uses_bridge_fallback -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. + +Aggiornamento Milestone 6/7.47 - 2026-07-06: + +- esteso il percorso local-scalar direct-sync a variabili locali `string` e + `null` con tipo stabile, usando slot temporanei piu' larghi per conservare + pointer/length delle stringhe oltre al flag `defined`; +- aggiunto lowering diretto per `isset()` e `empty()` su variabili gia' + assegnate nel frammento eval: + - `isset()` controlla `defined` e rifiuta `null`; + - `empty()` implementa truthiness PHP per `int`, `bool`, `null`, stringa + vuota e stringa `"0"`; +- aggiunto supporto local-scalar per ternario con rami omogenei, necessario per + forme comuni come `echo empty($x) ? "Y" : "n";`; +- il flush diretto verso i local slot del caller ora puo' materializzare anche + stringhe e null senza passare da `__elephc_eval_scope_set`; +- il caso write-only `$created = "yes"` viene ora assorbito dallo stesso path + local-scalar direct-sync, restando senza scope eval, senza bridge e senza + `elephc_magician`; +- il test `test_literal_eval_local_isset_empty_uses_aot_without_execute_bridge` + verifica ora marker `eval literal AOT compiled local scalar with direct local + sync`, assenza di `__elephc_eval_scope_set`, assenza di + `__elephc_eval_execute`, assenza di helper runtime `__elephc_eval_`, assenza + di `elephc_magician` e output `InZBvL:x`; +- residuo noto dopo questa tranche: + - `test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge` + resta AOT senza bridge ma usa sync scope boxed, perche' `/=` richiede + rappresentazione float corretta nel direct-sync o una migrazione piu' + ampia al modello EIR multi-return/scope-write; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_local_isset_empty_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_scalar_store_uses_direct_local_write_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. + +Aggiornamento Milestone 6/7.48 - 2026-07-06: + +- chiuso il residuo `/=` + `%=` nel percorso local-scalar direct-sync: + `test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge` + ora usa marker `eval literal AOT compiled local scalar with direct local sync` + e non emette piu' `__elephc_eval_scope_set`; +- il classifier condiviso in `src/eval_aot.rs` accetta ora `float`, `/` come + risultato `float` e `%` su operandi numerici con risultato `int`; +- i cambi tipo local-scalar sono permessi solo nel flusso lineare del + frammento, mentre branch/loop/switch restano conservativi per evitare tipi + finali path-dependent; +- il lowering local-scalar in + `src/codegen_ir/lower_inst/builtins/eval.rs` conserva il tipo al punto di + lettura della variabile, quindi uno stesso slot puo' transitare correttamente + da `int` a `float` e tornare a `int`; +- aggiunti storage/load temporanei per `float`, divisione target-aware + `d0/d1` o `xmm0/xmm1`, modulo con coercizione numerica a int e boxing + eval-runtime/core-runtime dei float; +- il gating in `src/ir_lower/program.rs` considera supportato anche il + direct-sync verso slot caller `float`; +- dopo questa tranche non restano asserzioni positive su + `__elephc_eval_scope_set` nei test `literal_eval_`: i casi rimasti sono + asserzioni negative; +- verifiche: + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. + +Aggiornamento Milestone 6/7.49 - 2026-07-06: + +- esteso il path boxed direct read/write per literal eval a read-modify-write + numerici con risultato `float`, per esempio: + ` case successivo`; +- il gate `eval_aot` permette ora il caso no-scope quando gli span consentono + di ricostruire in modo sicuro la posizione del `default`; +- aggiunti: + - `test_switch_default_before_case_falls_through_in_source_order`; + - `test_literal_eval_switch_default_before_case_no_scope_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests switch_default_before_case -- --nocapture`: 3/3; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 47/47; + - `git diff --check`: passato. + +Aggiornamento Milestone 3/7.53 - 2026-07-06: + +- completata la stessa correzione `default` source-order anche nel CFG + optimizer dello `switch`, usato da reachability, DCE e analisi dei tail path; +- `build_switch_cfg` mantiene gli indici dei `case` invariati, ma calcola i + successori di fallthrough considerando la posizione sorgente del `default`: + un `case` prima del `default` cade nel `default`, e un `default` prima di un + `case` cade nel `case` successivo; +- aggiunto + `test_build_switch_cfg_tracks_default_before_later_case_successors`, che + verifica il grafo `case 1 -> default -> case 2` e la reachability entrando dal + blocco `default`; +- verifiche: + - `cargo test -p elephc --lib test_build_switch_cfg_tracks_default_before_later_case_successors -- --nocapture`: passato; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests switch_default_before_case -- --nocapture`: 3/3; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 47/47; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.54 - 2026-07-06: + +- promosso un sottoinsieme statico di array literal nel percorso eval EIR AOT: + accessi immediati come `[1, 2, 3][1]` e + `["name" => "Ada"]["name"]`; +- il gate resta conservativo: non abilita `foreach`, mutazioni array, append, + array letti dallo scope del caller o array literal restituiti direttamente; +- aggiunti helper nel classifier per riconoscere solo receiver `ArrayLiteral` / + `ArrayLiteralAssoc` con chiavi statiche `int|string` e valori gia' + EIR-safe; +- aggiunto + `test_literal_eval_static_array_literal_read_uses_eir_aot_without_magician`, + che verifica marker `eval literal AOT compiled EIR function`, assenza di + `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di + `elephc_magician` e output `2:Ada`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests array_literal_and -- --nocapture`: 3/3; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 48/48; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.55 - 2026-07-06: + +- promosso anche il return di array literal statici dal percorso eval EIR AOT, + per esempio `eval('return ["a", "b"];')` e + `eval('return ["left" => "L", "right" => "R"];')`; +- il return resta boxed `Mixed` attraverso il normale lowering EIR della + funzione eval AOT, quindi il caller puo' indicizzare il valore restituito + senza bridge eval; +- il gate e' stato ristretto per gli `ArrayLiteralAssoc`: accetta solo coppie + con chiavi esplicite stabili `int` o stringhe non intere PHP, e rifiuta le + chiavi sintetiche generate dal parser per entry associative non keyate; +- il vincolo evita di promuovere forme con semantica PHP del next automatic key, + come `["2" => "two", "tail"]`, che restano correttamente sul fallback bridge + finche' il lowering EIR non modella il cursore automatico PHP completo; +- aggiunto + `test_literal_eval_static_array_literal_return_uses_eir_aot_without_magician`, + che verifica marker `eval literal AOT compiled EIR function`, assenza di + `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di + `elephc_magician` e output `ab:LR`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_array_literal_return_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests array_literal -- --nocapture`: 35/35; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 49/49; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.56 - 2026-07-06: + +- promosso il vecchio test dei commenti eval da semplice verifica output + "through bridge" a guardrail esplicito del percorso EIR AOT; +- il caso coperto usa un frammento con commenti `//`, `#`, `/* ... */` e + `__LINE__`; e' sicuro per l'AOT attuale perche' `__LINE__` viene abbassato + dal parser a `IntLiteral`, senza richiedere il pass globale delle magic + constants; +- il test rinominato + `test_literal_eval_comments_and_line_magic_use_eir_aot_without_magician` + verifica marker `eval literal AOT compiled EIR function`, assenza di + `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di + `elephc_magician` e output `4`; +- le altre magic constants non abbassate dal parser richiedono una sostituzione + contestuale esplicita prima del lowering del frammento eval; +- verifica: + - `cargo test --test codegen_tests test_literal_eval_comments_and_line_magic_use_eir_aot_without_magician -- --nocapture`: passato. + +Aggiornamento Milestone 7/3.57 - 2026-07-06: + +- aggiunto un planner eval AOT contestuale: + `plan_literal_fragment_with_source_path_and_static_calls`; +- il planner contestuale parse-a il frammento literal e applica + `magic_constants::substitute_file_and_scope_constants` quando il modulo EIR + espone `source_path`, trasformando `__FILE__`, `__DIR__`, + `__NAMESPACE__`, `__CLASS__`, `__TRAIT__`, `__FUNCTION__` e `__METHOD__` + in literal prima di classificare il frammento; +- il `LoweringContext` porta ora il `source_path` canonico del modulo, cosi' + anche la decisione `eval_literal_needs_barrier` usa la stessa classificazione + contestuale dello scan modulo e del backend; +- aggiornati i call-site EIR contestuali in: + - `src/ir_lower/expr/mod.rs`, per evitare una barrier eval quando il + frammento con magic constants e' fully AOT; + - `src/ir_lower/program.rs`, per materializzare la funzione eval AOT e per + non marcare `RuntimeFeatures::eval` quando non serve; + - `src/codegen_ir/lower_inst/builtins/eval.rs`, per marker/fallback e + chiamate scope-read coerenti con il piano contestuale; +- promossi a EIR AOT senza bridge: + - `__FILE__` / `__DIR__` dentro eval literal, con metadata di call-site; + - magic constants di scope top-level eval (`__CLASS__`, `__NAMESPACE__`, + `__TRAIT__`, `__FUNCTION__`, `__METHOD__`) che restano stringhe vuote + anche se l'eval e' chiamato da un metodo namespaced; +- aggiunti/rinominati i test: + - `test_literal_eval_magic_file_and_dir_use_eir_aot_without_magician`; + - `test_literal_eval_scope_magic_constants_use_eir_aot_without_magician`; +- verifiche: + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests test_literal_eval_magic_file_and_dir_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_scope_magic_constants_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 52/52; + - `cargo test --test codegen_tests eval_magic -- --nocapture`: 2/2; + - `cargo test --test codegen_tests scope_magic -- --nocapture`: 1/1; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.58 - 2026-07-06: + +- promosso il caso PHP `echo` con lista separata da virgole nel percorso EIR + AOT per eval literal; +- il parser rappresenta `echo "a", "b", "c";` come + `StmtKind::Synthetic([...Echo...])`; il classifier eval AOT ora tratta + `Synthetic` come una sequenza di statement sia nel percorso no-scope EIR sia + nel percorso scope-write lineare; +- questo evita il fallback bridge per sintassi che non aggiunge semantica + dinamica, ma solo raggruppa statement gia' supportati; +- unificati i vecchi test output-only per comma-echo, `print` statement e + `return print` in + `test_literal_eval_echo_comma_and_print_use_eir_aot_without_magician`; +- il test verifica marker `eval literal AOT compiled EIR function`, assenza di + `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di + `elephc_magician` e output `abc:x:y1`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_echo_comma_and_print_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 53/53; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `git diff --check`: passato. + +Aggiornamento fallback policy - 2026-07-06: + +- separato il vecchio test legacy `array(...)` eval in due guardrail espliciti: + - `test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician`; + - `test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge`; +- i due casi ora hanno policy distinte: + - `array(...)` statico viene normalizzato dal parser nello stesso AST di `[]` + e puo' riusare il gate AOT esistente per literal array/read; + - l'assegnamento con chiave esplicita seguita da elemento non keyato dipende + dalla semantica PHP del next automatic key, che non va approssimata; +- verifiche: + - `cargo test --test parser_tests parse_legacy`: 3/3; + - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato. + +Aggiornamento Milestone 7/3.59 - 2026-07-06: + +- chiuso il fallback sintattico per la vecchia forma PHP `array(...)` quando il + contenuto e' staticamente supportato; +- estratto il parsing degli array in `src/parser/expr/arrays.rs`, usato sia da + `[...]` sia da `array(...)`, cosi' il resto della pipeline vede sempre + `ExprKind::ArrayLiteral` o `ExprKind::ArrayLiteralAssoc`; +- `parse_named_expr` intercetta `array(...)` non qualificato e + case-insensitive prima della normale logica di function-call, evitando che + `=>` venga trattato come errore degli argomenti; +- il test + `test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician` + verifica marker `eval literal AOT compiled EIR function`, assenza di + `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di + `elephc_magician` e output `b:Ada`; +- resta bridge il caso + `test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge`, + perche' scrive nello scope eval e dipende dalla semantica di next-key in un + assegnamento array non ancora modellato nel percorso AOT scope-write; +- verifiche: + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo check --tests`: passato; + - `cargo test --test parser_tests parse_legacy`: 3/3; + - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 54/54; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.60 - 2026-07-06: + +- promosso il sottoinsieme di nested array literal statici nel gate eval EIR + AOT no-scope; +- `expr_is_eir_static_array_value_safe` ora tratta `ArrayLiteral` e + `ArrayLiteralAssoc` come sorgenti array statiche ricorsive, continuando a + rifiutare `Spread` e chiavi associative sintetiche generate dal parser; +- questo consente forme come: + - `eval('return [[10, 20], ["name" => "Ada"]];')`; + - `eval('return ARRAY(array(10, 20), array("name" => "Ada"));')`; +- il test static-array-return ora verifica anche output nested `20:Ada`, + mentre il test legacy-array verifica la forma case-insensitive `ARRAY(...)` + e nested `array(...)` senza bridge; +- resta invariato il guardrail bridge per `array(2 => "two", "tail")` dentro + assegnamento scope-write, perche' quello dipende ancora dal cursore PHP + `next_auto_key` in un percorso di sincronizzazione scope piu' ampio; +- verifiche: + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_array_literal_return_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 54/54; + - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.61 - 2026-07-06: + +- promosso in eval EIR AOT il sottoinsieme statico di array associativi con + elementi non keyati dopo chiavi intere o stringhe-intere note; +- il parser degli array ora mantiene un cursore `next_auto_key` allineato a PHP + 8.4 per: + - prima chiave intera negativa, dove il prossimo indice diventa `key + 1`; + - chiavi stringa intere come `"2"`, normalizzate a chiave intera; + - chiavi stringa non intere come `"02"`, che non avanzano il cursore; + - chiavi negative successive che non abbassano un cursore gia' avanzato; +- il gate eval AOT ricostruisce lo stesso cursore prima di accettare chiavi + sintetiche generate dal parser; se una chiave sintetica non corrisponde al + cursore ricostruito, il frammento resta fallback; +- restano volutamente fuori da questa tranche chiavi `null`, `bool` e `float` + nel gate AOT, perche' ampliano la superficie di coercioni/diagnostiche + PHP-observable; il test runtime bridge esistente continua a coprire quei casi; +- aggiunto + `test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician`, + che verifica `[2 => ...]`, `[-2 => ...]`, `["2" => ...]`, `["02" => ...]` + e `array(2 => ..., ...)` senza `__elephc_eval_execute`, senza helper eval + bridge e senza `elephc_magician`; +- verifiche: + - `php -r` su PHP 8.4.19 per confermare i casi `-2`, `2.7`, `true`, + `false`, `null`, `"02"` e `"2"`; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test parser_tests next_key`: 2/2; + - `cargo test --test parser_tests negative_key_does_not_decrease -- --nocapture`: 1/1; + - `cargo test --test codegen_tests test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 55/55; + - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.62 - 2026-07-06: + +- esteso il gate eval EIR AOT anche alle chiavi booleane statiche negli array + associativi; +- il parser calcola gia' `true` come chiave intera `1` e `false` come chiave + intera `0` per il cursore `next_auto_key`; il gate ora accetta + `ExprKind::BoolLiteral` come chiave statica e aggiorna lo stesso cursore; +- il test parser + `test_parse_assoc_array_next_key_after_boolean_keys` verifica che + `[true => "yes", "tail", false => "no", "end"]` generi chiavi sintetiche + `2` e `3`; +- il test + `test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician` + copre ora anche `[true => "yes", "tail"][2]` e + `[false => "no", "tail"][1]` senza bridge; +- restano fuori dal gate AOT `float` e `null` keys: + - `float` puo' emettere diagnostiche PHP-observable di conversione implicita; + - `null` richiede chiave stringa vuota e non e' ancora nel subset di key + materialization ammesso dal gate; +- verifiche: + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test parser_tests boolean_keys -- --nocapture`: 1/1; + - `cargo test --test codegen_tests test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 55/55; + - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.63 - 2026-07-06: + +- esteso il gate eval EIR AOT alle chiavi `null` statiche negli array + associativi; +- `null` non avanza il cursore `next_auto_key`, coerentemente con PHP 8.4: + viene materializzato come chiave stringa vuota `""`, mentre l'entry non + keyata successiva usa ancora la chiave automatica corrente; +- il backend EIR `HashSet` accetta ora `PhpType::Void` come chiave hash, + materializzandola come stringa vuota persistente su entrambi i target + supportati dal lowerer (`aarch64` e `x86_64`); +- il test parser + `test_parse_assoc_array_null_key_does_not_advance_auto_key` verifica che + `[null => "empty", "tail"]` mantenga `null` come chiave esplicita e generi + la chiave sintetica `0` per `tail`; +- il test + `test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician` + copre ora anche `[null => "empty"][""]` e + `[null => "empty", "tail"][0]` senza bridge eval; +- resta fuori dal gate AOT la chiave `float`, perche' in PHP 8.4 puo' + produrre diagnostiche osservabili di conversione implicita quando perde + precisione; +- verifiche: + - `php -r` su PHP 8.4.19 per confermare che `null` diventa chiave `""` e non + avanza l'auto-key; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test parser_tests null_key -- --nocapture`: 1/1; + - `cargo test --test codegen_tests test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 55/55; + - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.64 - 2026-07-06: + +- aggiunto un guardrail esplicito per gli assegnamenti di array statici nello + scope eval tramite funzione EIR AOT; +- il percorso e' gia' nativo rispetto al bridge `execute`: frammenti come + `eval('$items = ["a", "b"];')`, `eval('$map = ["name" => "Ada"];')` e + `eval('$legacy = array("x", "y");')` vengono abbassati a funzione EIR e + sincronizzati nello scope con `__elephc_eval_scope_set`; +- il test + `test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers` + verifica: + - marker `eval literal AOT compiled EIR function`; + - presenza di `__elephc_eval_scope_set`; + - assenza di `__elephc_eval_execute`; + - output runtime `b:Ada:xy`; +- questa tranche non chiude ancora Milestone 6: gli helper di eval scope sono + ancora forniti da `elephc_magician`, quindi il test documenta esplicitamente + il link corrente alla libreria invece di dichiarare un percorso fully + magician-free; +- in questa tranche restava bridge il caso con lettura interna della variabile + array appena assegnata (`$items = array(...); echo $items[3];`), perche' il + gate EIR consentiva array access statici su literal ma non ancora su + variabili locali note come array; +- verifiche: + - compilazione temporanea del frammento scope-write array per ispezionare + marker assembly e output; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test codegen_tests test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 56/56; + - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.65 - 2026-07-06: + +- chiuso il gap della tranche precedente: il classificatore EIR AOT ora tiene + traccia anche delle variabili locali sicuramente assegnate da array literal + statici; +- `EirLocalFacts` conserva `array_locals` accanto a `assigned` e `int_locals`; + un assegnamento mantiene il fact solo se il valore e' un array literal + staticamente materializzabile dal gate AOT, altrimenti lo rimuove; +- il merge dei branch conserva un local array fact solo quando la variabile e' + presente in tutti i rami, quindi un array access successivo e' ammesso solo + se l'assegnamento e' definitivamente avvenuto; +- `expr_is_eir_static_array_source_safe` accetta ora anche variabili locali + tracciate come array statici, permettendo frammenti come + `eval('$items = array(2 => "two", "tail",); echo $items[3];')` senza + `__elephc_eval_execute`; +- il test bridge precedente e' stato promosso a regressione positiva: + `test_literal_eval_legacy_array_literal_next_key_scope_assignment_uses_eir_aot_scope_helpers` + verifica marker EIR AOT, presenza di `__elephc_eval_scope_set`, assenza del + bridge execute e output runtime `tail`; +- il percorso continua a linkare `elephc_magician` per gli helper eval-scope: + questa tranche elimina il bridge di esecuzione, non ancora la dipendenza + dagli helper di scope; +- verifiche: + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_next_key_scope_assignment_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 57/57; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.66 - 2026-07-06: + +- esteso il gate eval EIR AOT alle chiavi float statiche solo quando PHP le + converte a intero senza diagnostiche osservabili; +- sono accettati float literal finiti e integral-valued, inclusa la forma + negativa (`2.0 => ...`, `-2.0 => ...`), mentre float con parte frazionaria, + `NAN` e `INF` restano fallback bridge; +- il cursore `next_auto_key` del gate usa lo stesso intero normalizzato, quindi + `[2.0 => "two", "tail"][3]` e `[-2.0 => "minus", "tail"][-1]` passano da + funzione EIR AOT senza helper eval; +- `test_eval_static_array_fractional_float_key_uses_bridge_fallback` blocca il + caso `2.7 => ...` sul bridge, perche' PHP 8.4 emette + `Implicit conversion from float ... to int loses precision`; +- verifiche: + - `php -r` su PHP 8.4.19 per confermare conversioni silenziose di `2.0` e + `-2.0`, e deprecation per `2.7`, `-2.7`, `NAN`, `INF`; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test codegen_tests test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_static_array_fractional_float_key_uses_bridge_fallback -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 57/57; + - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.67 - 2026-07-06: + +- allineato il fold statico di `array_key_exists()` alla normalizzazione delle + chiavi gia' usata dal gate array AOT; +- `static_array_key_fold_id()` ora riusa `static_integer_array_key_value()` per + int, bool, stringhe intere, float integrali e forme negative supportate; +- `null` viene normalizzato come chiave stringa vuota `s:`, mentre stringhe non + intere restano chiavi stringa; +- il fold resta conservativo per float frazionari, `NAN` e `INF`, cosi' non + elimina diagnostiche PHP-observable; +- il test + `test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician` + copre ora anche `true`, `false`, `null`, `2.0` e `-2.0` in AOT senza helper + eval e senza `elephc_magician`; +- lo stesso normalizzatore alimenta `count()` sugli array statici: il test + `test_literal_eval_static_array_count_uses_eir_aot_without_magician` copre + ora collisioni `true`/`1`, `false`/`0`, `null`/`""`, `2.0`/`2` e + `-2.0`/`-2`; +- verifiche: + - `php -r` su PHP 8.4.19 per confermare `array_key_exists()` con `true`, + `false`, `null`, `2.0` e la deprecation di `2.7`; + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test codegen_tests test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_array_count_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 57/57; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.68 - 2026-07-06: + +- esteso il gate EIR AOT di `isset()` oltre le sole variabili semplici: + accetta ora anche `ArrayAccess` quando l'accesso e' gia' static-array safe + secondo `expr_is_eir_function_safe`; +- questo abilita `isset(["name" => "Ada"]["name"])` senza bridge, ma continua a + escludere array da scope caller, oggetti `ArrayAccess` e accessi dinamici non + gia' modellati dal subset statico; +- `empty()` non ha richiesto cambio perche' gia' delegava al controllo + espressione generico nel caso non-variable; +- il test + `test_literal_eval_static_array_literal_read_uses_eir_aot_without_magician` + copre ora: + - key presente con valore non-null -> `isset` true; + - key presente con valore `null` -> `isset` false; + - key mancante -> `isset` false; + - `empty()` true su stringa vuota letta da static-array access; + - `empty()` false su stringa non vuota letta da static-array access; + mantenendo assenza di helper eval, runtime bridge e `elephc_magician`; +- verifiche: + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test codegen_tests test_literal_eval_static_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato, poi ripassato dopo l'estensione `empty`; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 57/57; + - `git diff --check`: passato. + +Aggiornamento Milestone 7/3.69 - 2026-07-06: + +- abilitato `foreach` EIR AOT per sorgenti static-array literal non vuote, + sia indexed sia associative, quando il frammento usa lo scope-aware EIR AOT; +- il gate resta conservativo: + - `foreach` by-ref resta fallback; + - in questa tranche `foreach` su array letto dallo scope caller restava bridge + fallback, poi superato dal Milestone 4/7.89; + - array statici vuoti restano esclusi per non dichiarare key/value come + definite quando PHP non esegue il body; +- `collect_scope_reads_before_writes` ora modella `foreach` come assegnazione + di `value_var` e, se presente, `key_var` prima del body: le letture di + `$item`, `$key` e `$value` dentro il body non vengono piu' classificate come + reads dallo scope caller; +- dopo il loop, key/value vengono considerate definite solo per sorgenti literal + statiche non vuote, mantenendo conservativo il caso dinamico; +- corretto il lowering EIR di `EvalScopeSet`: quando il valore da pubblicare + nello scope e' gia' un `Mixed`/`Union`, il backend fa `__rt_incref` e passa + `EVAL_SCOPE_FLAG_OWNED`; cosi' lo scope possiede una cell separata e il + cleanup dei locals della funzione AOT non invalida il valore riletto dal + caller; +- questa correzione e' necessaria per casi come + `foreach (["a" => 1, "b" => 2] as $key => $value)`, dove `$key` e' un + `Mixed` string key che deve restare visibile dopo `eval`; +- test aggiunto: + `test_literal_eval_static_foreach_uses_eir_aot_scope_helpers`, che verifica + output corretto, assenza di `__elephc_eval_execute`, uso di + `__elephc_eval_scope_set`, e persistenza di `$item`, `$key`, `$value` dopo + eval; +- aggiornato `test_eval_codegen_requires_eval_bridge` per coprire il fallback + storico di `foreach` su array proveniente dallo scope caller, superato dal + Milestone 4/7.89; +- verifiche: + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 58/58. + +Gap residuo Milestone 6/7.70 - 2026-07-06: + +- il link a `elephc_magician` resta attivo per frammenti EIR AOT che usano + `EvalScopeGet`/`EvalScopeSet`, anche quando non chiamano + `__elephc_eval_execute`; +- la causa e' che `RuntimeFeatures::eval` oggi e' sovraccarico: + - abilita il bridge eval dinamico e quindi `pcre2-*` + `elephc_magician`; + - abilita anche hidden eval scope locals, scope helper calls, class metadata + extra, `$argv`/`$argc` storage e probe di late static binding legati a eval; +- rimuovere `features.eval = true` da `EvalScopeGet`/`EvalScopeSet` non e' + sufficiente: le chiamate generated continuerebbero a referenziare + `__elephc_eval_scope_new/free/get/set`, simboli oggi esportati da + `elephc_magician`; +- piano tecnico per chiudere Milestone 6 in modo pulito: + 1. dividere `RuntimeFeatures::eval` in almeno due feature, per esempio + `eval_bridge` e `eval_scope`; + 2. mantenere `eval_bridge` per `__elephc_eval_execute`, eval function/class + dynamic calls, callable/class/property helpers consumati da magician e + PCRE richiesto da codice dinamicamente parsato; + 3. introdurre un path `eval_scope` separato per gli helper scope-only usati da + EIR AOT; + 4. spostare o reimplementare in core runtime i soli helper scope-only + `__elephc_eval_scope_new`, `__elephc_eval_scope_free`, + `__elephc_eval_scope_get`, `__elephc_eval_scope_set` e la semantica + ownership/dirty/unset minima che serve al caller reload; + 5. lasciare in `elephc_magician` il bridge completo e gli helper che richiedono + parser/interprete dinamico; + 6. aggiornare `required_libraries_for_runtime_features()` affinche' + `eval_scope` non aggiunga `elephc_magician` ne' PCRE; + 7. aggiungere regressioni per scope-read/write EIR AOT e `foreach` statico che + verifichino assenza di `__elephc_eval_execute` e assenza di + `elephc_magician` quando nessun fallback dinamico rimane; +- alternativa parziale ma meno generale: aggiungere direct caller-local sync per + alcuni frammenti write-only oggi passati da `EvalScopeSet`; questo riduce + casi specifici ma non elimina il bisogno di helper scope-only per EIR AOT con + read/write dinamicamente visibili. + +Aggiornamento Milestone 6/7.71 - 2026-07-06: + +- introdotto lo split logico di `RuntimeFeatures::eval` in + `RuntimeFeatures::{eval_bridge, eval_scope}`; +- `EvalScopeGet`, `EvalScopeSet` e la presenza di hidden eval scope state ora + richiedono `eval_scope`, mentre `__elephc_eval_execute`, fallback literal, + eval dynamic calls e helper dinamici restano sotto `eval_bridge`; +- i consumer che servono solo al bridge dinamico, come metadata extra, + superglobal storage per `$argv`/`$argc`, probe late-static e override static + property/called-class, ora guardano `eval_bridge` invece della vecchia feature + unica; +- testato empiricamente il caso scope-only senza bridge libraries: il link + fallisce perche' lo staticlib Rust trascina ancora oggetti che referenziano + wrapper `__elephc_eval_*` e simboli PCRE; +- per questo motivo `eval_scope` oggi continua deliberatamente a linkare + `pcre2-posix`, `pcre2-8` ed `elephc_magician`, ed emette ancora i bridge + wrappers quando servono helper scope-only; +- questo non chiude Milestone 6, ma isola il debito residuo: il prossimo taglio + deve spostare `__elephc_eval_scope_new/free/get/set` e la semantica minima di + ownership/dirty scope nel core runtime, poi `eval_scope` potra' smettere di + richiedere `elephc_magician` e PCRE; +- test aggiornato in `runtime_features`: + `test_eval_scope_runtime_features_keep_bridge_libraries_until_core_scope_runtime`; +- verifiche: + - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile + `ignore`; + - `cargo check --tests`: passato; + - `cargo test --lib test_eval_scope_runtime_features_keep_bridge_libraries_until_core_scope_runtime -- --nocapture`: unit test passato prima di interrompere il resto del traversal workspace; + - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 58/58. + +Aggiornamento Milestone 6/7.72 - 2026-07-06: + +- implementato un provider core runtime per gli helper `eval_scope` usati dal + percorso AOT scope-only: + - `__elephc_eval_scope_new`; + - `__elephc_eval_scope_free`; + - `__elephc_eval_scope_get`; + - `__elephc_eval_scope_set`; + - `__elephc_eval_value_null`, necessario per reload Mixed mancanti; +- `eval_scope && !eval_bridge` ora emette questi helper dal core runtime e non + richiede piu' `pcre2-*` ne' `elephc_magician`; +- `eval_bridge` continua a usare il provider completo in `elephc_magician`, in + modo da evitare doppie definizioni di simboli e mantenere nel bridge dinamico + gli helper che dipendono da parser/interprete eval; +- aggiunta una barriera di lowering scope-only per literal eval AOT che hanno + bisogno di `EvalScopeGet`/`EvalScopeSet` ma non del contesto dinamico: viene + dichiarato solo l'hidden local `EvalScope`, evitando riferimenti a + `__elephc_eval_context_free`; +- la finalizzazione dell'assembly EIR ora emette helper metadata/reflection, + callable e dynamic eval solo quando `eval_bridge` e' davvero richiesto; +- aggiornate le regressioni scope-only per verificare assenza di + `elephc_magician` e presenza degli helper core `__elephc_eval_scope_*`; +- limite residuo: il provider core copre solo `new/free/get/set` e il valore + `null` minimo. Alias globali, `unset`, clear-dirty e semantiche piu' + dinamiche restano bridge-only finche' non vengono portate nel subset AOT; +- verifiche: + - `cargo test --lib test_eval_scope_runtime_features_omit_bridge_libraries -- --nocapture`: unit test passato prima di interrompere il resto del traversal workspace; + - `cargo test --test codegen_tests test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_next_key_scope_assignment_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 58/58. + +Aggiornamento Milestone 4/7.73 - 2026-07-06: + +- il classifier AOT EIR ora distingue una prima categoria di builtin statici + runtime-safe, separata dai fold compile-time su soli literal; +- abilitato `strlen()` dentro literal eval EIR AOT quando l'argomento e' + posizionale e arriva gia' al backend come `Str` o boxed `Mixed`, per esempio + una variabile del caller passata tramite direct read params; +- questo copre casi come: + + ```php + $s = "abcd"; + echo eval('return strlen($s);'); + ``` + + senza passare da `__elephc_eval_execute` e senza linkare `elephc_magician`; +- il gate resta conservativo: + - niente named/spread args in questo nuovo path runtime-safe; + - nessuna estensione implicita agli altri builtin; + - argomenti non string/Mixed restano fuori dal subset finche' non sono + modellati con la stessa semantica del type checker/backend ordinario; +- test aggiunto: + `test_literal_eval_strlen_scope_read_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_strlen_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 59/59; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.74 - 2026-07-06: + +- estesa la whitelist dei builtin statici runtime-safe con `count()` su array + concreti gia' modellabili dal subset EIR AOT; +- il nuovo caso coperto e' `count($items)` quando `$items` e' una local del + frammento assegnata da static array literal, quindi il backend ordinario puo' + leggere la lunghezza dell'array senza bridge; +- poiche' le variabili create dentro `eval` restano visibili nel caller, il + path corretto e' scope-only EIR AOT: usa `__elephc_eval_scope_set` core per + pubblicare `$items`/`$map`, ma non crea `EvalContext`, non chiama + `__elephc_eval_execute` e non linka `elephc_magician`; +- il gate resta conservativo: + - niente `count($callerVar)` su direct read param `Mixed`, finche' non viene + modellata la diagnostica per non-array; + - niente named/spread args in questo runtime-safe path; + - array provenienti da sorgenti dinamiche restano fuori dal subset; +- test aggiunto: + `test_literal_eval_local_array_count_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_local_array_count_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 60/60; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.75 - 2026-07-06: + +- estesa la whitelist dei builtin statici runtime-safe con `intval()` quando + l'argomento puo' raggiungere il backend ordinario come scalar, stringa, + null-like o boxed `Mixed`; +- il caso principale coperto e' una variabile del caller passata al frammento + eval tramite direct read params: + + ```php + $s = "42"; + echo eval('return intval($s) + 8;'); + ``` + +- questo path usa una funzione EIR AOT interna con parametri Mixed diretti, + quindi non crea scope runtime, non chiama `__elephc_eval_execute` e non + linka `elephc_magician`; +- il gate resta conservativo: + - niente named/spread args; + - array locali/statici non sono accettati come argomento `intval()`; + - altri builtin di cast restano esclusi finche' non vengono verificati + contro il lowerer EIR ordinario; +- test aggiunto: + `test_literal_eval_intval_scope_read_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_intval_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 61/61; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.76 - 2026-07-06: + +- aggiunto supporto EIR ordinario per `floatval()` su `Mixed`/`Union`, usando + il runtime helper gia' esistente `__rt_mixed_cast_float`; +- estesa la whitelist dei builtin statici runtime-safe con `floatval()` sugli + stessi argomenti scalar-safe gia' ammessi per `intval()`; +- il caso eval coperto e': + + ```php + $s = "1.5"; + echo eval('return floatval($s) + 2.25;'); + ``` + + che ora usa direct read params verso una funzione EIR AOT interna, senza + scope runtime, senza `__elephc_eval_execute` e senza `elephc_magician`; +- aggiunta anche una regressione non-eval per provare il backend ordinario: + `floatval(json_decode("2.5")) + 0.5`; +- il gate resta conservativo: + - niente named/spread args; + - array locali/statici restano esclusi dagli argomenti `floatval()`; + - `boolval()`/`strval()` non sono stati allargati in questa tranche; +- test aggiunti: + - `test_json_decode_float_value_can_be_floatval`; + - `test_literal_eval_floatval_scope_read_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_json_decode_float_value_can_be_floatval -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_floatval_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests json_decode_float -- --nocapture`: 3/3; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 62/62; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.77 - 2026-07-06: + +- aggiunto supporto EIR ordinario per `boolval()` su `Mixed`/`Union`, usando + il runtime helper gia' esistente `__rt_mixed_cast_bool`; +- estesa la whitelist dei builtin statici runtime-safe con `boolval()` sugli + stessi argomenti scalar-safe gia' ammessi per `intval()`/`floatval()`; +- il caso eval coperto e': + + ```php + $s = "0"; + echo eval('return boolval($s) ? "bad" : "ok";'); + ``` + + che ora usa direct read params verso una funzione EIR AOT interna, senza + scope runtime, senza `__elephc_eval_execute` e senza `elephc_magician`; +- aggiunta anche una regressione non-eval per provare il backend ordinario: + `boolval(json_decode("true"))`, `boolval(json_decode("false"))` e la + truthiness speciale PHP della stringa `"0"`; +- il gate resta conservativo: + - niente named/spread args; + - array locali/statici restano esclusi dagli argomenti `boolval()`; + - `strval()` non e' stato allargato in questa tranche; +- test aggiunti: + - `test_json_decode_bool_value_can_be_boolval`; + - `test_literal_eval_boolval_scope_read_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_json_decode_bool_value_can_be_boolval -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_boolval_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests json_decode_bool -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 63/63; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.78 - 2026-07-06: + +- verificato che `strval()` passa gia' dal cast a stringa EIR ordinario, che + supporta `Mixed`/`Union` tramite `__rt_mixed_cast_string` e la gestione + object-aware di `__toString()`; +- estesa la whitelist dei builtin statici runtime-safe con `strval()` sugli + stessi argomenti scalar-safe gia' ammessi per `intval()`/`floatval()`/ + `boolval()`; +- il caso eval coperto e': + + ```php + $s = false; + echo eval('return "[" . strval($s) . "]";'); + ``` + + che ora usa direct read params verso una funzione EIR AOT interna, senza + scope runtime, senza `__elephc_eval_execute` e senza `elephc_magician`; +- aggiunta anche una regressione non-eval per provare `strval()` su payload + `Mixed` da `json_decode()`: + - numero intero; + - `true`; + - `false` come stringa vuota; + - stringa JSON; +- il gate resta conservativo: + - niente named/spread args; + - array locali/statici restano esclusi dagli argomenti `strval()`; + - object/string-context dinamici restano affidati al normale lowerer EIR e + non vengono allargati nel classifier oltre i casi scalar-safe; +- test aggiunti: + - `test_json_decode_scalar_value_can_be_strval`; + - `test_literal_eval_strval_scope_read_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_json_decode_scalar_value_can_be_strval -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_strval_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests json_decode_str -- --nocapture`: 4/4; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 64/64; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.79 - 2026-07-06: + +- estesa la whitelist dei builtin statici runtime-safe con type probes scalari: + - `gettype()`; + - `is_int()` / `is_integer()` / `is_long()`; + - `is_float()` / `is_double()` / `is_real()`; + - `is_bool()`; + - `is_null()`; + - `is_scalar()`; + - `is_string()`; +- il caso eval coperto e': + + ```php + $i = 42; + $f = 1.5; + $b = false; + $n = null; + $s = "hi"; + echo eval('return gettype($i) . ":" . + (is_integer($i) ? "I" : "bad") . + (is_double($f) ? "D" : "bad") . + (is_bool($b) ? "B" : "bad") . + (is_null($n) ? "N" : "bad") . + (is_scalar($s) ? "S" : "bad") . + (is_string($s) ? "T" : "bad");'); + ``` + + che ora usa direct read params verso una funzione EIR AOT interna, senza + scope runtime, senza `__elephc_eval_execute` e senza `elephc_magician`; +- corretto un bug nel fold custom dei builtin statici di `eval_aot`: `is_*()` + su argomenti non literal veniva foldato a `false`; ora i non-literal + ritornano `None` e arrivano al lowerer EIR runtime-safe; +- aggiunta anche una regressione non-eval per provare `gettype()` e gli + `is_*()` su payload `Mixed` da `json_decode()`; +- il gate resta conservativo: + - niente named/spread args; + - argomenti limitati agli stessi casi scalar-safe usati dai cast runtime-safe; + - `is_array()`/`is_object()`/`is_iterable()` restano fuori da questa tranche; +- test aggiunti: + - `test_json_decode_scalar_type_predicates_accept_mixed`; + - `test_literal_eval_type_probes_scope_read_use_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_json_decode_scalar_type_predicates_accept_mixed -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_type_probes_scope_read_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 65/65; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.80 - 2026-07-06: + +- estesa la whitelist dei builtin statici runtime-safe con `is_array()` solo + per sorgenti array gia' materializzabili dal percorso EIR AOT: + - literal array statici; + - variabili locali del frammento assegnate da literal array; +- il caso eval coperto e': + + ```php + echo eval('$a = [1, 2]; return is_array($a) ? "A" : "bad";'); + ``` + + che ora passa dalla funzione EIR AOT interna e non chiama + `__elephc_eval_execute`; +- a differenza dei type probe scalari su read-only caller scope, questo caso + usa ancora `__elephc_eval_scope_set`: l'assegnazione `$a = [...]` dentro + `eval` deve creare/aggiornare `$a` nello scope del chiamante secondo + semantica PHP; +- aggiunta una regressione non-eval per provare `is_array()` su payload + `Mixed` da `json_decode()`; +- il gate resta conservativo: + - niente named/spread args; + - niente `is_array($callerVar)` direct-read finche' non esiste un path sicuro + per distinguere array/scalari dal valore scope letto; + - niente `is_object()`/`is_iterable()` in questa tranche; +- test aggiunti: + - `test_json_decode_array_value_can_be_is_array`; + - `test_literal_eval_is_array_local_array_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_json_decode_array_value_can_be_is_array -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_is_array_local_array_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 66/66; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.81 - 2026-07-06: + +- estesa la whitelist dei builtin statici runtime-safe con `is_iterable()` + solo per la parte array-safe gia' coperta da `is_array()`: + - literal array statici; + - variabili locali del frammento assegnate da literal array; +- il caso eval coperto e': + + ```php + echo eval('$a = [1, 2]; return is_iterable($a) ? "T" : "bad";'); + ``` + + che passa dalla funzione EIR AOT interna, non chiama + `__elephc_eval_execute` e non linka `elephc_magician`; +- resta intenzionalmente escluso `is_iterable($callerVar)` direct-read quando + il valore puo' essere oggetto/Iterator, perche' quel caso richiede ancora una + prova scope/object-safe separata; +- resta fuori anche `is_object()` in eval AOT runtime-safe; +- aggiunta una regressione non-eval per provare `is_iterable()` su payload + `Mixed` array da `json_decode()`; +- test aggiunti: + - `test_json_decode_array_value_can_be_is_iterable`; + - `test_literal_eval_is_iterable_local_array_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_json_decode_array_value_can_be_is_iterable -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_is_iterable_local_array_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 67/67; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.82 - 2026-07-06: + +- estesa la whitelist dei builtin statici runtime-safe con `is_object()` per: + - valori scalar/literal/array gia' gestiti dal subset EIR, con risultato + staticamente falso quando non sono oggetti; + - caller-scope reads passati come direct read params `Mixed`, cosi' il + lowerer ordinario puo' controllare il tag object a runtime senza + `eval_scope`; +- il caso eval coperto e': + + ```php + $o = json_decode("{}"); + $i = 42; + echo eval('return (is_object($o) ? "O" : "bad") . ":" . + (is_object($i) ? "bad" : "I");'); + ``` + + che passa dalla funzione EIR AOT interna con direct read params, senza + `__elephc_eval_execute`, senza `__elephc_eval_scope_*` e senza + `elephc_magician`; +- resta esclusa la creazione/lettura di proprieta' oggetto dentro eval AOT, + perche' quello e' un tema object/member semantics separato; +- aggiunta una regressione non-eval per provare `is_object()` su payload + `Mixed` object da `json_decode()`; +- test aggiunti: + - `test_json_decode_object_value_can_be_is_object`; + - `test_literal_eval_is_object_scope_read_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_json_decode_object_value_can_be_is_object -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_is_object_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 68/68; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.83 - 2026-07-06: + +- estesa la whitelist dei builtin statici runtime-safe con: + - `is_numeric()` su valori scalar-safe e caller-scope reads direct-param; + - `is_resource()` su valori scalar-safe e caller-scope reads direct-param; +- il caso eval coperto e': + + ```php + $n = "42"; + $s = "abc"; + $h = fopen("php://memory", "r+"); + echo eval('return (is_numeric($n) ? "N" : "bad") . + (is_numeric($s) ? "bad" : "S") . ":" . + (is_resource($h) ? "H" : "bad");'); + ``` + + che passa dalla funzione EIR AOT interna con direct read params, senza + `__elephc_eval_execute`, senza `__elephc_eval_scope_*` e senza + `elephc_magician`; +- il gate resta conservativo: + - array locali/statici non vengono aperti per `is_numeric()` anche se PHP + restituirebbe staticamente `false`; + - `is_nan()`/`is_finite()`/`is_infinite()` restano fuori da questa tranche + perche' richiedono coercione numerica/float, non solo type-probe; +- aggiunta una regressione non-eval per provare `is_numeric()` su payload + `Mixed` scalari da `json_decode()`; +- test aggiunti: + - `test_json_decode_scalar_value_can_be_is_numeric`; + - `test_literal_eval_numeric_resource_probes_scope_read_use_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_json_decode_scalar_value_can_be_is_numeric -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_numeric_resource_probes_scope_read_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 69/69; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.84 - 2026-07-06: + +- estesa la whitelist dei builtin statici runtime-safe con: + - `is_nan()`; + - `is_finite()`; + - `is_infinite()`; +- il supporto e' volutamente limitato ad argomenti numerici provati dentro il + frammento AOT: + - literal `int`/`float`/`bool`, inclusi `NAN`, `INF` e `-INF`; + - variabili locali del frammento assegnate da valori `int`/`float`; + - cast espliciti `(int)`, `(float)`, `(bool)` gia' abbassabili dal subset EIR; +- il caso eval coperto e': + + ```php + echo eval('$nan = NAN; $inf = INF; $num = 2.5; + return (is_nan($nan) ? "N" : "bad") . + (is_infinite($inf) ? "I" : "bad") . + (is_finite($num) ? "F" : "bad") . + (is_finite($inf) ? "bad" : "f");'); + ``` + + che passa dalla funzione EIR AOT interna, non chiama + `__elephc_eval_execute` e non linka `elephc_magician`; +- aggiunto un guardrail esplicito per non usare direct read params su stringhe + del caller: + + ```php + $s = "abc"; + echo eval('return is_finite($s) ? "bad" : "ok";'); + ``` + + resta bridge fallback, perche' PHP 8.4 lancia `TypeError` per stringhe non + numeriche in queste funzioni mentre il lowerer `Mixed` passerebbe da cast a + float; +- test aggiunti: + - `test_literal_eval_float_predicates_local_values_use_eir_aot_without_magician`; + - `test_literal_eval_float_predicates_scope_string_use_bridge_fallback`; +- verifiche: + - `php -r` su PHP 8.4 per confermare il comportamento TypeError delle + stringhe non numeriche con `is_nan()`/`is_finite()`/`is_infinite()`; + - `cargo test --test codegen_tests test_literal_eval_float_predicates_local_values_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_float_predicates_scope_string_use_bridge_fallback -- --nocapture`: passato; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 71/71; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.85 - 2026-07-06: + +- esteso il path EIR AOT direct-read per `is_array()` quando l'argomento e' + una variabile dello scope chiamante: + - il classifier `eval_aot` ora considera sicuro il probe `is_array($x)` anche + quando `$x` arriva come read-param `Mixed`; + - il lowering IR accetta array/hash tra i tipi che possono essere passati come + parametri diretti al frammento eval AOT; + - il codegen eval allinea la whitelist dei locals sincronizzabili, evitando il + disallineamento in cui il plan rimuoveva il barrier ma la funzione AOT non + trovava la sorgente `$items`; +- caso coperto: + + ```php + $items = [1, 2]; + $n = 42; + echo eval('return (is_array($items) ? "A" : "bad") . ":" . + (is_array($n) ? "bad" : "N");'); + ``` + + passa tramite funzione EIR AOT con direct read params, non chiama + `__elephc_eval_execute`, non alloca `__elephc_eval_context_new` e non linka + `elephc_magician`; +- `is_iterable()` su variabili caller-scope e' stato lasciato fuori da questa + tranche durante il primo giro; il passo successivo 4/7.86 completa quel caso + dopo aver allineato le whitelist array/object tra classifier, lowering e + codegen; +- test aggiunto: + - `test_literal_eval_is_array_scope_read_uses_eir_aot_without_magician`; +- regressione verificata: + - `test_literal_eval_is_iterable_local_array_uses_eir_aot_without_magician` + resta verde, quindi il supporto eval-local array per `is_iterable()` non e' + stato ridotto; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_is_array_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_is_iterable_local_array_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 72/72; + - `cargo check --tests`: passato; + - `git diff --check`: passato; + - scansione whitespace/conflitti sui file toccati: pulita. + +Aggiornamento Milestone 4/7.86 - 2026-07-06: + +- esteso anche `is_iterable()` sul path EIR AOT direct-read per valori dello + scope chiamante: + - array/list e hash arrivano come `Mixed` e passano dai tag runtime 4/5; + - oggetti arrivano come `Mixed` con tag runtime 6 e vengono verificati tramite + i metadati `Iterator`/`IteratorAggregate` gia' usati dal lowerer EIR; + - scalari caller-scope restano falsi senza fallback; +- il supporto ha richiesto di tenere coerenti tre predicati separati: + - `eval_aot` per decidere se generare la funzione scope-read AOT; + - `ir_lower::expr` per evitare il barrier eval completo quando i read params + sono davvero passabili; + - `codegen_ir::lower_inst::builtins::eval` per ritrovare le sorgenti locali + array/object al momento della chiamata direct-param; +- caso coperto: + + ```php + class EvalAotDirectIterator implements Iterator { /* metodi Iterator */ } + $items = [1, 2]; + $iterator = new EvalAotDirectIterator(); + $n = 42; + echo eval('return (is_iterable($items) ? "A" : "bad") . + (is_iterable($iterator) ? "I" : "bad") . + (is_iterable($n) ? "bad" : "N");'); + ``` + + produce `AIN`, usa la funzione EIR AOT con direct read params, non chiama + `__elephc_eval_execute`, non alloca `__elephc_eval_context_new` e non linka + `elephc_magician`; +- test aggiunto: + - `test_literal_eval_is_iterable_scope_read_uses_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_is_iterable_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 73/73; + - `cargo check --tests`: passato; + - `git diff --check`: passato; + - scansione whitespace/conflitti sui file toccati: pulita. + +Aggiornamento Milestone 4/7.87 - 2026-07-06: + +- consolidata la copertura dei type-probe read-only su valori dello scope + chiamante non scalari: + - `gettype($items)` con `$items` array caller-scope restituisce `array`; + - `gettype($o)` con `$o` object/Mixed caller-scope restituisce `object`; + - `is_scalar($items)` e `is_scalar($o)` restano falsi senza fallback; +- non e' stato necessario nuovo lowering: il lavoro 4/7.85 e 4/7.86 aveva gia' + allineato boxing direct-param, type whitelist e source lookup per array/object; + questa tranche rende esplicita la regressione nel test esistente; +- test esteso: + - `test_literal_eval_type_probes_scope_read_use_eir_aot_without_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_type_probes_scope_read_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 73/73; + - `cargo check --tests`: passato; + - `git diff --check`: passato; + - scansione whitespace/conflitti sui file toccati: pulita. + +Aggiornamento verifica target-aware - 2026-07-06: + +- aggiunta evidenza Linux x86_64 Docker per i nuovi casi direct-read su scope + caller non scalare: + - `./scripts/test-linux-x86_64.sh literal_eval_is_array_scope`: passato; + - `./scripts/test-linux-x86_64.sh literal_eval_is_iterable_scope`: passato; + - `./scripts/test-linux-x86_64.sh literal_eval_type_probes_scope`: passato; +- i runner Docker x86_64 sono usciti senza container residui; +- la verifica locale Linux ARM64 non e' stata rilanciata in questa tranche: + resta demandata alla matrice CI o a un filtro Docker ARM64 dedicato prima della + chiusura completa del piano. + +Aggiornamento Milestone 4/7.88 - 2026-07-06: + +- esteso il path EIR AOT direct-read per `count($callerArray)` senza aprire + `count($callerScalar)`: + - il plan `eval_aot` registra ora un set di read scope che devono essere + array-like quando il frammento contiene `count($nome)` su una variabile + proveniente dal caller; + - il classifier accetta `count($scopeRead)` solo come builtin `count()`, senza + promuovere genericamente `$scopeRead` a sorgente array per altri usi come + `$scopeRead[0]`; + - il lowering AST->EIR, la feature discovery e il backend EIR controllano il + vincolo contro i tipi locali del caller prima di scegliere il direct-param + AOT; + - se il caller ha uno scalare, il call-site resta sul bridge e continua a + linkare `elephc_magician`; +- test aggiunti: + - `test_literal_eval_count_scope_read_array_uses_eir_aot_without_magician`; + - `test_literal_eval_count_scope_read_scalar_keeps_bridge_fallback`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_count_scope_read_array_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_count_scope_read_scalar_keeps_bridge_fallback -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_count -- --nocapture`: 2/2; + - `cargo test --test codegen_tests array_count_uses_eir_aot_without_magician -- --nocapture`: 2/2; + - `cargo check --tests`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 75/75; + - `git diff --check`: passato; + - scansione whitespace/conflitti sui file toccati: pulita. + +Aggiornamento Milestone 4/7.89 - 2026-07-06: + +- abilitato `foreach ($callerArray as ...)` nel path EIR AOT scope-aware quando + la sorgente letta dallo scope caller soddisfa il vincolo array-like gia' + usato per `count($callerArray)`; +- il classifier registra il read array-constrained per la sorgente del + `foreach`, ma non marca key/value come definitely-assigned dopo il loop se la + sorgente non e' una static-array literal non vuota; +- il lowering/codegen controllano il vincolo array anche per il path con runtime + eval scope, cosi' `foreach ($callerScalar as ...)` resta fallback bridge; +- corretto il preambolo EIR di `foreach`: l'inizializzazione tecnica dei loop + locals a boxed `null` ora aggiorna solo lo slot locale e non pubblica nello + scope eval, altrimenti `foreach ([] as $kept)` sovrascriveva il caller con + `null`; +- il call site scope-aware pre-flusha anche i nomi scritti gia' presenti nel + caller, oltre ai nomi letti, in modo che una scrittura condizionale non + eseguita lasci invariato il valore dopo il reload; +- test aggiunti: + - `test_literal_eval_foreach_scope_array_uses_eir_aot_scope_helpers`, che + verifica output corretto, `__elephc_eval_scope_get/set`, assenza di + `__elephc_eval_execute`, assenza di `elephc_magician`, caso array vuoto e + key/value su array associativo del caller; + - `test_literal_eval_foreach_scope_scalar_keeps_bridge_fallback`, che mantiene + il bridge su sorgente scalare; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_foreach_scope_array_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests test_literal_eval_foreach_scope_scalar_keeps_bridge_fallback -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_foreach -- --nocapture`: 2/2; + - `cargo test --test codegen_tests eval_foreach -- --nocapture`: 9/9; + - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 77/77; + - `cargo check --tests`: passato; + - `./scripts/test-linux-x86_64.sh literal_eval_foreach`: 2/2 sui test + `codegen_tests`, ripassato dopo l'estensione key/value associativa; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.90 - 2026-07-06: + +- esteso il path EIR AOT direct-param per i predicati IEEE float + `is_nan()`, `is_infinite()` e `is_finite()` quando l'argomento letto dallo + scope caller e' un local inizializzato di tipo `int` o `float`; +- il planner registra ora `float_predicate_read_constraints`, separato dai + vincoli array usati da `count()`/`foreach`, cosi' il classifier puo' accettare + `$scopeRead` ma lowering, feature scan e backend verificano il tipo concreto + del caller prima di scegliere AOT; +- il vincolo resta conservativo: stringhe del caller, ref-bound locals, + superglobal/global alias e locals non inizializzati restano fallback bridge, + preservando i `TypeError` PHP-observable; +- test aggiunto: + - `test_literal_eval_float_predicates_scope_numeric_use_eir_aot_without_magician`, + che copre `NAN`, `INF`, float finite e int dal caller via direct read params + senza `__elephc_eval_execute`, senza eval-scope helper e senza + `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests literal_eval_float_predicates -- --nocapture`: 3/3, ripassato dopo `rustfmt`; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 78/78; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs src/ir_lower/expr/mod.rs src/ir_lower/program.rs src/codegen_ir/lower_inst/builtins/eval.rs tests/codegen/eval.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.91 - 2026-07-06: + +- abilitato `foreach ([])` / `foreach ([] as ...)` nel path EIR AOT no-scope; +- il planner ora distingue le sorgenti foreach staticamente vuote: + - la sorgente array viene ancora analizzata; + - il body resta richiesto nel subset EIR perche' viene comunque abbassato dal + backend; + - reads/writes del body non vengono registrati nello scope eval, perche' il + body e' runtime-irraggiungibile; + - key/value non vengono registrati come scritture e quindi non vengono flushati + nel caller; +- questo rimuove il vecchio fallback per array statici vuoti senza cambiare il + comportamento dei foreach statici non vuoti, che continuano a sincronizzare + key/value tramite eval-scope helper; +- test aggiunto: + - `test_literal_eval_static_empty_foreach_uses_eir_aot_without_scope_helpers`, + che verifica EIR AOT, assenza di `__elephc_eval_*`, assenza di + `elephc_magician` e preservazione della variabile caller `$kept`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_static_empty_foreach_uses_eir_aot_without_scope_helpers -- --nocapture`: passato, ripassato dopo `rustfmt`; + - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_foreach -- --nocapture`: 2/2; + - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 79/79; + - `cargo check --tests`: passato. + +Aggiornamento Milestone 4/7.92 - 2026-07-06: + +- esteso il subset EIR AOT per `array_key_exists()` su array letti dallo scope + caller: + - `array_key_exists(1, $callerIndexed)` puo' usare direct read params quando il + caller ha un local array inizializzato; + - `array_key_exists("name", $callerAssoc)` puo' usare direct read params quando + il caller ha un local associative-array inizializzato; +- il planner registra un vincolo separato `assoc_array_read_constraints`, oltre + al vincolo array-like gia' usato da `count()`/`foreach`, cosi' string-key probes + su indexed array restano bridge fallback invece di entrare in un path AOT con + semantica numeric-string incompleta; +- aggiunto lowering target-aware di `array_key_exists()` su receiver `Mixed`: + - unbox del receiver; + - tag `4` -> `__rt_array_key_exists` per chiavi `int`/`bool`; + - tag `5` -> `__rt_hash_get` per chiavi `int`/`bool`/`string`; + - altri tag -> `false`; + - il payload array/hash viene preservato su stack temporaneo mentre la chiave + viene materializzata, senza aggiungere un nuovo helper runtime globale; +- il gate resta conservativo: + - chiavi dinamiche (`array_key_exists($k, $items)`) restano fuori da questa + tranche; + - `null`/`float` keys sui caller-scope arrays restano fuori finche' il backend + runtime non modella tutta la normalizzazione PHP; + - caller scalari e string-key probes su indexed arrays restano fallback bridge; +- test aggiunti: + - `test_literal_eval_array_key_exists_scope_read_array_uses_eir_aot_without_magician`, + che copre indexed array, associative array, chiave presente con valore `null`, + miss, direct-read EIR AOT, assenza di `__elephc_eval_*` e assenza di + `elephc_magician`; + - `test_literal_eval_array_key_exists_scope_read_scalar_keeps_bridge_fallback`; + - `test_literal_eval_array_key_exists_string_key_indexed_scope_keeps_bridge_fallback`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_array_key_exists_scope_read_array_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_array_key_exists_ -- --nocapture`: 3/3; + - `cargo test --test codegen_tests test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 7/7; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs src/ir_lower/expr/mod.rs src/ir_lower/program.rs src/codegen_ir/lower_inst/builtins/eval.rs src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs tests/codegen/eval.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.93 - 2026-07-06: + +- chiuso un pezzo del gap lasciato in 4/7.92: `array_key_exists()` su array letti + dallo scope caller ora puo' restare in EIR AOT anche con: + - chiave statica `null`, limitata a receiver associativi per modellare la + normalizzazione PHP a chiave stringa vuota; + - chiavi statiche `float` integralmente rappresentabili, incluse forme negate + come `-2.0`, abbassate a integer key prima del probe indexed/hash; +- il lowering target-aware e' stato esteso su AArch64 e x86_64: + - indexed arrays convertono le chiavi float con `fcvtzs`/`cvttsd2si` prima di + chiamare `__rt_array_key_exists`; + - associative arrays materializzano `null` come stringa vuota e float integrali + come integer key prima di chiamare `__rt_hash_get`; + - receiver `Mixed` accetta ora chiavi `int`/`bool`/`float` su indexed/hash e + forza `string`/`null` solo sul path hash-only; +- il gate resta conservativo sui float frazionari (`2.7`, ecc.): restano bridge + fallback per preservare la deprecation PHP sulla conversione float->int con + perdita di precisione; +- test aggiunti: + - `test_literal_eval_array_key_exists_scope_read_null_and_float_keys_use_eir_aot`, + che copre `1.0` su indexed array, `null` su assoc array e `-2.0` su assoc + array via direct-read EIR AOT, senza `__elephc_eval_*` e senza + `elephc_magician`; + - `test_literal_eval_array_key_exists_fractional_float_key_keeps_bridge_fallback`, + che mantiene `array_key_exists(2.7, $items)` sul bridge; +- verifiche: + - `cargo test --test codegen_tests literal_eval_array_key_exists_ -- --nocapture`: 5/5; + - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 9/9; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs tests/codegen/eval.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.94 - 2026-07-06: + +- il gate EIR AOT per builtin runtime ora normalizza named arguments tramite + `builtin_call_sig()` + `plan_call_args()` prima di applicare i controlli gia' + esistenti sugli argomenti in ordine firma; +- la normalizzazione e' condivisa anche dal collector dei vincoli array, cosi' + `array_key_exists(array: $map, key: "name")` registra il vincolo associativo + sul parametro `array` corretto e non sulla posizione sorgente; +- questa tranche resta volutamente fixed-arity/no-spread: + - spread/unpack continuano a restare fallback; + - builtin con default opzionali restano da abilitare uno alla volta quando il + backend EIR accetta la forma materializzata; +- test aggiunto: + - `test_literal_eval_named_runtime_builtins_use_eir_aot_without_magician`, + che copre `boolval(value: $flag)`, + `array_key_exists(array: $map, key: "name")` e + `array_key_exists(key: "missing", array: $map)` via direct-read EIR AOT, + senza `__elephc_eval_*` e senza `elephc_magician`; +- verifiche: + - `cargo test --test codegen_tests test_literal_eval_named_runtime_builtins_use_eir_aot_without_magician -- --nocapture`: passato; + - `cargo test --test codegen_tests literal_eval_array_key_exists_ -- --nocapture`: 5/5; + - `cargo test --test codegen_tests literal_eval_named_runtime_builtins -- --nocapture`: 1/1; + - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 9/9; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs tests/codegen/eval.rs`: passato con i + warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.95 - 2026-07-06: + +- chiuso il caso opzionale piu' piccolo lasciato aperto in 4/7.94: + `count(value: $items)` e `count(value: $items, mode: 0)` possono ora restare + in EIR AOT quando `$items` e' un array/hash noto dal caller; +- il classifier accetta `count()` normalizzato con uno o due argomenti solo se + il `mode` opzionale e' il default literal `0`; il collector dei vincoli array + registra comunque il vincolo sul parametro `value`; +- il lowerer EIR `count` accetta ora uno o due operandi e ignora il secondo + solo dopo aver verificato che sia `ConstI64(0)`; +- il `mode` ricorsivo/non-zero resta fallback bridge finche' l'EIR non modella + `COUNT_RECURSIVE` per array annidati, oggetti `Countable` e `Mixed`; +- test aggiunti/aggiornati: + - `test_literal_eval_count_named_default_mode_uses_eir_aot_without_magician`, + che copre `count(value: $items)` e `count(value: $items, mode: 0)` via + direct-read EIR AOT senza helper eval e senza `elephc_magician`; + - `test_literal_eval_count_named_recursive_mode_keeps_bridge_fallback`, che + mantiene `count(value: $items, mode: 1)` sul bridge; + - `test_literal_eval_named_runtime_builtins_use_eir_aot_without_magician` ora + include anche `count(value: $items)`; +- verifiche: + - `cargo test --test codegen_tests literal_eval_count_named -- --nocapture`: 2/2; + - `cargo test --test codegen_tests literal_eval_count -- --nocapture`: 4/4; + - `cargo test --test codegen_tests literal_eval_named_runtime_builtins -- --nocapture`: 1/1; + - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 9/9; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs src/codegen_ir/lower_inst/builtins.rs tests/codegen/eval.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.96 - 2026-07-06: + +- il gate EIR AOT per builtin runtime non scarta piu' gli spread prima del + planner: ora passa sempre da `plan_call_args()` quando la chiamata contiene + named args o spread; +- gli spread statici espandibili dal planner, per esempio + `count(...["value" => $items])`, + `boolval(...["value" => $flag])` e + `array_key_exists(...["array" => $map, "key" => "name"])`, possono quindi + restare in direct-read EIR AOT senza bridge; +- gli spread dinamici o non espansi dal planner restano fallback bridge perche' + `normalize_eir_runtime_builtin_args()` rifiuta ancora ogni `Spread` residuo + dopo la normalizzazione; +- test aggiunti: + - `test_literal_eval_static_spread_runtime_builtins_use_eir_aot_without_magician`, + che copre `count`, `boolval` e `array_key_exists` con spread statico + associativo via direct-read EIR AOT, senza helper eval e senza + `elephc_magician`; + - `test_literal_eval_dynamic_spread_runtime_builtin_keeps_bridge_fallback`, + che mantiene `count(...$args)` sul bridge; +- verifiche: + - `cargo test --test codegen_tests literal_eval_static_spread_runtime_builtins -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_dynamic_spread_runtime_builtin -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_named_runtime_builtins -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_count_named -- --nocapture`: 2/2; + - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 9/9; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs tests/codegen/eval.rs`: passato con i + warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +Aggiornamento Milestone 5/7.97 - 2026-07-06: + +- il gate per chiamate statiche a funzioni utente dentro literal eval AOT ora + rifiuta esplicitamente gli spread rimasti dinamici dopo `plan_call_args()`, + invece di dipendere solo dal controllo finale sui literal scalar; +- gli spread statici espandibili dal planner, per esempio + `join_static_spread(...["right" => "B", "left" => "A"])`, restano nel subset + EIR AOT e possono materializzare anche default scalar opzionali; +- gli spread dinamici, per esempio `join_dynamic_spread(...$args)`, restano sul + bridge finche' il percorso EIR AOT non modella runtime unpack, evaluation + order e controlli di arita'/named args dinamici; +- test aggiunti: + - `test_literal_eval_static_user_function_static_spread_args_use_aot_without_magician`, + che copre named static spread e default scalar in una user function via EIR + AOT, senza helper eval e senza `elephc_magician`; + - `test_literal_eval_static_user_function_dynamic_spread_args_keep_bridge_fallback`, + che mantiene lo spread dinamico sul bridge; +- verifiche: + - `cargo test --test codegen_tests literal_eval_static_user_function_static_spread_args -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_static_user_function_dynamic_spread_args -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_static_user_function_named_args -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_static_user_function_defaults -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_static_scalar_user_functions -- --nocapture`: 1/1; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs tests/codegen/eval.rs`: passato con i + warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/7.98 - 2026-07-06: + +- chiuso il fallback per `array_key_exists("1", $items)` quando `$items` e' un + indexed array caller-scope: la string key ora puo' restare in direct-read EIR + AOT; +- il backend EIR condiviso di `array_key_exists()` normalizza le string key su + array indicizzati con `__rt_hash_normalize_key()`: + - stringhe intere canoniche, come `"1"`, diventano bounds-check tramite + `__rt_array_key_exists`; + - stringhe non intere o con leading zero non canonico, come `"x"` e `"01"`, + restituiscono `false` sugli indexed array senza provare un hash lookup; +- il dispatch Mixed indexed/assoc ora tratta `Str` come chiave valida anche sul + ramo indexed; `null` resta hash-only per la semantica della chiave `""`; +- test aggiunti/aggiornati: + - `test_literal_eval_array_key_exists_string_key_indexed_scope_uses_eir_aot`, + che copre `"1"`, `"x"` e `"01"` via direct-read EIR AOT senza helper eval e + senza `elephc_magician`; + - `test_array_key_exists_indexed_string_keys`, che copre lo stesso + comportamento nel backend EIR condiviso fuori da `eval`; +- verifiche: + - `cargo test --test codegen_tests literal_eval_array_key_exists_string_key_indexed_scope -- --nocapture`: 1/1; + - `cargo test --test codegen_tests test_array_key_exists_indexed_string_keys -- --nocapture`: 1/1; + - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 10/10; + - `./scripts/test-linux-x86_64.sh array_key_exists`: passato, inclusi 10/10 + codegen filtrati, 1/1 error test filtrato, 2/2 smoke test filtrati e 1/1 + test `elephc-magician` filtrato; warning preesistenti su `libc::time_t`; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs tests/codegen/eval.rs tests/codegen/arrays/indexed/search_merge_union.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +Aggiornamento Milestone 4/5/7.99 - 2026-07-06: + +- il gate EIR AOT di literal eval ora riconosce `call_user_func()` e + `call_user_func_array()` quando il callback e' una stringa literal risolta a + un builtin gia' sicuro per AOT oppure a una user function tipizzata gia' + supportata dal subset statico; +- `call_user_func_array()` con array literal viene convertito nello stesso + formato di argomenti usato dal lowering callable esistente: chiavi stringa + diventano named args, chiavi intere restano positional, e chiavi dinamiche + restano fallback; +- i `call_user_func*()` statici verso builtin puri foldabili vengono foldati + prima del controllo AOT, per esempio `call_user_func("strtoupper", "az")` + diventa direttamente una literal e non dipende dal bridge; +- le user function statiche AOT richiedono ora parametri e return type + dichiarati: le funzioni non tipizzate restano sul bridge, evitando il + mismatch in cui il planner module-level le accettava ma il lowerer del + frammento AOT generava un `EvalFunctionCall` senza eval context locale; +- fallback conservato: + - callback variabile, per esempio `call_user_func($fn, "abcd")`, resta sul + bridge; + - callback static method string o array callable non sono stati aperti in + questa tranche; + - user function non tipizzate continuano a usare il dispatch eval esistente; +- test aggiunti: + - `test_literal_eval_static_call_user_func_builtin_uses_aot_without_magician`, + che copre builtin statici via `call_user_func()` e + `call_user_func_array()` con direct-read EIR AOT, senza helper eval e senza + `elephc_magician`; + - `test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician`, + che copre callback a user function tipizzata con positional, named e + default args via EIR AOT; + - `test_literal_eval_dynamic_call_user_func_callback_keeps_bridge_fallback`, + che mantiene il callback variabile sul bridge; +- verifiche: + - `cargo test --test codegen_tests literal_eval_static_call_user_func -- --nocapture`: 2/2; + - `cargo test --test codegen_tests literal_eval_dynamic_call_user_func_callback -- --nocapture`: 1/1; + - `cargo test --test codegen_tests test_eval_fragment_call_user_func -- --nocapture`: 8/8; + - `cargo test --test codegen_tests literal_eval_static_user_function_uses_aot_without_execute_bridge -- --nocapture`: 1/1; + - `cargo test --test codegen_tests literal_eval_static_scalar_user_functions -- --nocapture`: 1/1; + - `cargo check --tests`: passato; + - `rustfmt --check src/eval_aot.rs tests/codegen/eval.rs`: passato con i + warning gia' noti sull'opzione stabile `ignore`; + - `git diff --check`: passato. + +## Architettura proposta + +### 1. Frammenti AOT come funzioni native interne + +Ogni `eval('...')` supportato deve generare una funzione interna univoca, ad +esempio: + +```text +__elephc_eval_aot__ +``` + +ABI logica: + +```text +eval_aot_fn(eval_context, eval_scope, eval_global_scope) -> boxed Mixed +``` + +Le implementazioni possono materializzare questi argomenti tramite helper ABI +target-aware esistenti. La funzione AOT restituisce sempre un boxed Mixed: + +- valore di `return expr;` se il frammento ritorna; +- boxed `null` se il frammento termina senza `return`; +- status/fatal gestito dagli helper esistenti se un helper runtime fallisce. + +Il call site di `eval()` deve: + +1. riconoscere che il frammento literal ha una funzione AOT; +2. preparare context/scope/global scope come il bridge, ma solo quanto serve; +3. flushare nello scope le variabili del caller lette o scritte dal frammento; +4. chiamare la funzione AOT; +5. ricaricare dal dynamic scope le variabili che il frammento puo' aver scritto; +6. saltare completamente `__elephc_eval_execute`. + +### 2. Analisi del frammento + +Introdurre una fase compile-time dedicata: + +```text +literal fragment string + -> tokenizzazione/parsing come PHP fragment + -> name/magic-constant handling compatibile + -> analisi AOT eligibility + -> raccolta read/write/declare/call effects + -> lowering AOT o fallback reason +``` + +Questa fase deve produrre un oggetto simile a: + +```rust +EvalAotPlan { + function_symbol, + reads: BTreeSet, + writes: BTreeSet, + creates_unknown_vars: bool, + needs_eval_context: bool, + needs_global_scope: bool, + fallback_reason: Option, +} +``` + +Motivo: il call site deve sapere quali locals sincronizzare senza trattare ogni +eval literal come barriera massima quando non serve. + +### 3. Non duplicare tutto il codegen a mano + +Il primo subset scalar AOT oggi e' in `eval.rs` con un lowering manuale. Per +completare davvero AOT, il piano deve spostarsi verso uno dei due approcci: + +Approccio preferito: + +- abbassare il frammento eval a una funzione EIR interna; +- riusare `src/ir_lower/`, `src/ir_passes/` e `src/codegen_ir/`; +- aggiungere primitive EIR esplicite per scope eval solo dove il normale + modello di locals statici non basta. + +Approccio temporaneo ammesso solo per sbloccare il benchmark dei primi: + +- estendere il lowering AOT manuale a un mini-subset int/bool/control-flow; +- trattarlo come ponte di breve durata; +- mantenere il piano di convergenza verso EIR-function AOT. + +Il completamento finale non deve lasciare un grande secondo compiler manuale +in `src/codegen_ir/lower_inst/builtins/eval.rs`. + +## Milestone + +### Milestone 0 - Baseline e guardrail + +Obiettivo: rendere misurabile il problema e impedire regressioni di fallback. + +Deliverable: + +- aggiungere benchmark temporaneo o fixture non-CI per "somma primi fino a + 100000" con quattro varianti: + - Elephc standard; + - Elephc literal eval; + - PHP standard; + - PHP eval; +- aggiungere test assembly che conferma lo stato pre-AOT per il frammento con + `while`/`if`/`break`: oggi deve contenere `__elephc_eval_execute`; +- aggiungere test fallback per un costrutto esplicitamente non supportato, cosi' + il fallback resta verificato quando AOT cresce. + +Comandi: + +- `cargo test --test codegen_tests test_eval_prime_loop_literal_fallback_before_full_aot` +- benchmark manuale con heap sufficiente per il fallback, solo come baseline. + +### Milestone 1 - Funzione AOT interna per frammenti self-contained + +Obiettivo: generare una funzione nativa interna per literal eval che non legge +ne' scrive variabili del caller. + +Subset: + +- scalar locals interni al frammento; +- assignment semplice e compound `+=`; +- `echo`; +- `return`; +- `while`; +- `if`; +- `break`; +- `continue`; +- int/bool values; +- operatori `+`, `-`, `*`, `%`, `<=`, `<`, `>=`, `>`, `==`, `!=`, `&&`, `||` + dove gia' supportati dal parser/typechecker o dove abbassabili senza + divergere da PHP. + +Esempio target: + +```php +eval('$sum = 0; $i = 1; while ($i <= 10000) { $sum += $i; $i += 1; } echo $sum;'); +``` + +Requisiti: + +- assembly deve contenere `eval literal AOT compiled`; +- assembly non deve contenere `__elephc_eval_execute`; +- output deve combaciare con PHP; +- se il frammento termina senza `return`, `eval()` deve restituire `null`. + +### Milestone 2 - Scope read/write efficiente + +Obiettivo: supportare frammenti AOT che leggono e scrivono variabili del caller +senza interpretazione runtime. + +Esempi: + +```php +$a = 10; +echo eval('return $a + 20;'); +``` + +```php +$a = 10; +eval('$a = $a + 20;'); +echo $a; +``` + +Requisiti: + +- analisi read/write del frammento; +- flush solo delle variabili lette/scritte; +- reload solo delle variabili scritte o potenzialmente create; +- `global` e alias globali restano fallback finche' non sono modellati; +- variable variables (`$$name`), `unset`, references e by-ref restano fallback + fino a supporto esplicito. + +### Milestone 3 - Controllo di flusso per benchmark dei primi + +Obiettivo: rendere AOT il benchmark: + +```php +eval('$sum = 0; $n = 2; while ($n <= 100000) { ... if (...) { break; } ... } echo $sum;'); +``` + +Requisiti: + +- loop annidati; +- `break` che esce dal loop interno corretto; +- `if` con branch; +- modulo e confronti interi; +- nessuna chiamata a `__elephc_eval_execute`; +- tempo runtime vicino al codice Elephc standard e sensibilmente sotto PHP. + +Acceptance iniziale: + +- output `454396537`; +- `Elephc via eval` non richiede heap da centinaia di MB; +- RSS indicativo vicino a Elephc standard, non al fallback magician; +- `Elephc via eval` non oltre 2x Elephc standard per questo benchmark come + primo target ragionevole. + +### Milestone 4 - Chiamate statiche a builtins + +Obiettivo: permettere a literal eval AOT di chiamare builtins noti. + +Esempi: + +```php +eval('echo strlen("abc");'); +eval('$x = intval("42"); echo $x + 1;'); +eval('echo abs(-10);'); +``` + +Regole: + +- chiamate con nome statico e risoluzione builtin/funzione gia' nota possono + essere AOT; +- chiamate dinamiche (`$f()`), `call_user_func`, method calls, static calls + late-bound restano fallback inizialmente; +- namespace fallback deve seguire le regole gia' usate dal resolver/typechecker; +- named/spread args devono usare `src/types/call_args/`, non una logica nuova. + +Test: + +- builtin case-insensitive dentro eval literal AOT; +- namespaced call con fallback builtin quando applicabile; +- arg count/type error uguale al path statico o fallback controllato. + +### Milestone 5 - Funzioni statiche gia' note + +Obiettivo: supportare chiamate a funzioni definite staticamente prima del punto +eval. + +Esempio: + +```php +function inc($x) { return $x + 1; } +echo eval('return inc(41);'); +``` + +Fallback iniziale: + +- funzioni dichiarate dentro eval; +- classi dichiarate dentro eval; +- duplicate declarations; +- dynamic function names; +- closure/callable dinamici. + +### Milestone 6 - Riduzione fallback magician + +Obiettivo: evitare di linkare `libelephc-magician` quando tutti gli eval del +programma sono literal e pienamente AOT. + +Requisiti: + +- il program usage scan distingue: + - eval dinamico; + - literal eval fallback; + - literal eval fully AOT; +- programmi con solo eval fully AOT non richiedono `elephc_magician`; +- test assembly/link metadata per assenza di `__elephc_eval_execute`, + `__elephc_eval_context_new` e libreria `elephc_magician` quando non servono. + +### Milestone 7 - Integrazione con pipeline EIR + +Obiettivo: sostituire il mini-AOT manuale con un vero lowering del frammento a +funzione EIR interna. + +Lavoro richiesto: + +- rappresentare eval fragment come `Function` EIR interna con ABI speciale; +- dichiarare locals del frammento separati dai locals del caller; +- introdurre istruzioni o builtins EIR per: + - `eval_scope_get`; + - `eval_scope_set`; + - `eval_return_null`; + - eventuale `eval_status_check`; +- far passare la funzione AOT attraverso validator, optimizer, regalloc e + backend target-aware; +- rimuovere o ridurre il lowering manuale in `eval.rs`. + +Questo milestone e' quello che rende "completo" il percorso AOT in modo +manutenibile. + +## Fallback policy + +Un frammento literal deve restare fallback se contiene: + +- codice non parseabile come fragment PHP; +- include/require; +- declaration di funzioni/classi/interfacce/trait/enum finche' non registrate + staticamente nel contesto eval; +- `global`, `static`, references/by-ref, `unset`, variable variables; +- dynamic calls, dynamic class names, object/method/property access non + supportati; +- eccezioni/throw/try finche' non modellati nel frammento AOT; +- costrutti non supportati dal normale EIR backend; +- qualunque comportamento per cui non esiste test PHP-parity. + +Il fallback deve essere esplicito in assembly con un marker che includa una +ragione leggibile quando possibile. + +## File probabili + +- `src/ir/` +- `src/ir_lower/` +- `src/ir_passes/` +- `src/codegen_ir/lower_inst/builtins/eval.rs` +- `src/codegen_ir/` +- `src/codegen/program_usage/` +- `src/types/call_args/` +- `src/types/checker/` +- `src/name_resolver/` +- `src/resolver/` +- `tests/codegen/eval.rs` +- `tests/codegen/optimizer/` +- `benchmarks/magician/cases/` solo se si decide di promuovere i benchmark di + accettazione nella suite permanente. + +## Test richiesti + +Assembly/codegen tests: + +- literal eval con `while` self-contained usa AOT e non bridge; +- literal eval con `if`/`break` usa AOT e produce output corretto; +- literal eval nested loops prime-sum usa AOT e non bridge; +- literal eval senza `return` ritorna `null`; +- literal eval con `return` ritorna il valore senza uscire dal caller; +- literal eval legge `$a` dal caller; +- literal eval scrive `$a` nel caller; +- literal eval crea `$created` visibile dopo eval; +- unsupported dynamic eval non emette marker AOT; +- unsupported literal eval emette fallback e chiama bridge; +- programma con soli eval fully AOT non linka magician, quando Milestone 6 e' + implementato. + +Target-sensitive checks: + +- focused macOS ARM64 codegen tests; +- focused Linux x86_64 Docker test per prime-loop AOT; +- focused Linux ARM64 Docker test per prime-loop AOT. + +Benchmark checks: + +- prime-sum `100000`: + - output `454396537`; + - no `__elephc_eval_execute`; + - RSS non vicino al fallback da centinaia di MB; + - runtime molto sotto PHP CLI e vicino a Elephc standard. + +## Criteri di completamento + +Il percorso AOT per eval puo' essere considerato completo solo quando: + +1. ogni literal eval supportato viene compilato a funzione nativa interna; +2. il benchmark dei primi fino a `100000` passa via AOT senza bridge; +3. scope read/write del caller funziona per variabili note a compile time; +4. `return`/`null` eval semantics sono PHP-compatible; +5. builtins statici comuni funzionano via AOT; +6. i fallback non supportati restano corretti e verificati; +7. programmi senza fallback eval non linkano magician; +8. la soluzione non embedda il compilatore nel binario finale; +9. i test focused passano sui tre target supportati o hanno CI equivalente; +10. `git diff --check` passa. + +## Rischi principali + +- Duplicare troppo codegen in `eval.rs` e creare un secondo backend difficile da + mantenere. +- Trattare eval come codice statico normale e perdere semantica di scope. +- Dimenticare che `eval('$x + 1;')` ritorna `null`, non l'ultima espressione. +- Saltare i fallback per costrutti dinamici e produrre miscompilazioni. +- Ottimizzare il benchmark dei primi con un percorso troppo speciale invece di + completare il meccanismo generale. +- Supportare solo ARM64 e lasciare x86_64/Linux indietro. + +## Ordine consigliato + +1. Stabilizzare baseline e test fallback. +2. Implementare funzione AOT interna self-contained con locals int/bool e + control-flow. +3. Far passare il benchmark dei primi via AOT. +4. Integrare scope read/write del caller nel nuovo modello a funzione AOT. +5. Aggiungere builtins statici. +6. Portare il lowering AOT verso funzione EIR interna riusando la pipeline. +7. Ridurre il link a magician quando tutti gli eval sono fully AOT. + +## Aggiornamento: metodi statici pubblici + +Tranche completata: + +- il planner AOT per literal eval accetta predicate separati per funzioni e + metodi statici; +- `ir_lower::program`, `ir_lower::expr` e il lowering codegen ricostruiscono lo + stesso piano con metadati di classe coerenti; +- entrano in AOT solo chiamate a metodi statici nominali, `public`, con + signature scalare completamente dichiarata e argomenti normalizzabili dal + planner condiviso; +- metodi statici non tipizzati o non coperti restano sul bridge magician. + +Verifiche locali: + +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_uses_aot_without_magician -- --exact --nocapture` +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_keeps_bridge_fallback -- --exact --nocapture` +- `cargo test --test codegen_tests codegen::eval::test_eval_fragment_dispatches_aot_static_methods -- --exact --nocapture` +- `cargo check --tests` +- `rustfmt --check src/eval_aot.rs src/ir_lower/program.rs src/ir_lower/expr/mod.rs src/codegen_ir/lower_inst/builtins/eval.rs tests/codegen/eval.rs` +- `git diff --check -- src/eval_aot.rs src/ir_lower/program.rs src/ir_lower/expr/mod.rs src/codegen_ir/lower_inst/builtins/eval.rs tests/codegen/eval.rs .plans/elephc-eval-aot-complete-plan.md` From 396277fb86bc9a007a14ed259b8dcc6e7dd0b45f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 20:38:19 +0200 Subject: [PATCH 1043/1208] feat(eval): add literal eval AOT scope path --- Cargo.lock | 20 +- src/codegen/eval_class_constant_helpers.rs | 482 +- src/codegen/eval_property_helpers.rs | 644 +-- src/codegen/lower_inst.rs | 12 +- src/codegen/lower_inst/builtins.rs | 172 +- src/codegen/lower_inst/builtins/eval.rs | 5036 ++++++++++++++++++- src/codegen_support/runtime/emitters.rs | 5 +- src/codegen_support/runtime/eval_scope.rs | 452 ++ src/codegen_support/runtime/mod.rs | 1 + src/codegen_support/runtime_features.rs | 179 +- src/eval_aot.rs | 5209 ++++++++++++++++++++ src/ir/instr.rs | 132 +- src/ir/validator.rs | 186 +- src/ir_lower/context.rs | 344 +- src/ir_lower/expr/mod.rs | 614 ++- src/ir_lower/function.rs | 253 +- src/ir_lower/program.rs | 860 +++- src/ir_lower/stmt/mod.rs | 461 +- src/ir_passes/dead_store.rs | 53 +- src/lib.rs | 9 +- src/main.rs | 5 +- tests/codegen/eval.rs | 4748 +++++++++++++++++- 22 files changed, 18510 insertions(+), 1367 deletions(-) create mode 100644 src/codegen_support/runtime/eval_scope.rs create mode 100644 src/eval_aot.rs diff --git a/Cargo.lock b/Cargo.lock index 848f81d690..3eadc75989 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,16 @@ dependencies = [ "whirlpool", ] +[[package]] +name = "elephc-image" +version = "0.1.0" +dependencies = [ + "font8x8", + "image", + "kamadak-exif", + "tiny-skia", +] + [[package]] name = "elephc-magician" version = "0.1.0" @@ -627,16 +637,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "elephc-image" -version = "0.1.0" -dependencies = [ - "font8x8", - "image", - "kamadak-exif", - "tiny-skia", -] - [[package]] name = "elephc-pdo" version = "0.1.0" diff --git a/src/codegen/eval_class_constant_helpers.rs b/src/codegen/eval_class_constant_helpers.rs index 109b93a235..9bbd90476c 100644 --- a/src/codegen/eval_class_constant_helpers.rs +++ b/src/codegen/eval_class_constant_helpers.rs @@ -48,7 +48,10 @@ enum EvalClassConstantValue { Float(f64), Str(String), Null, - EnumCase { enum_name: String, case_name: String }, + EnumCase { + enum_name: String, + case_name: String, + }, } /// Emits eval class-constant helpers when any lowered function owns an eval context. @@ -145,7 +148,9 @@ fn collect_reflected_interface_constant_slots( slots: &mut Vec, ) { for constant_name in interface_constant_names(module, interface_name) { - if let Some(mut slot) = resolve_interface_constant_slot(module, interface_name, &constant_name) { + if let Some(mut slot) = + resolve_interface_constant_slot(module, interface_name, &constant_name) + { slot.reflected_class = interface_name.to_string(); slots.push(slot); } @@ -305,7 +310,8 @@ fn class_constant_slot( constant_name: &str, value_expr: &Expr, ) -> Option { - let value = eval_class_constant_value(module, declaring_class, Some(class_info), value_expr, 0)?; + let value = + eval_class_constant_value(module, declaring_class, Some(class_info), value_expr, 0)?; let visibility = class_info .constant_visibilities .get(constant_name) @@ -388,8 +394,11 @@ fn enum_case_slot( case_name: &str, ) -> Option { let enum_info = module.enum_infos.get(enum_name)?; - enum_info.cases.iter().any(|case| case.name == case_name).then(|| { - EvalClassConstantSlot { + enum_info + .cases + .iter() + .any(|case| case.name == case_name) + .then(|| EvalClassConstantSlot { reflected_class: enum_name.to_string(), declaring_class: enum_name.to_string(), allowed_scopes: Vec::new(), @@ -401,8 +410,7 @@ fn enum_case_slot( enum_name: enum_name.to_string(), case_name: case_name.to_string(), }, - } - }) + }) } /// Returns the interface that originally declared one inherited constant. @@ -435,7 +443,8 @@ fn eval_class_constant_value( ExprKind::StringLiteral(value) => Some(EvalClassConstantValue::Str(value.clone())), ExprKind::Null => Some(EvalClassConstantValue::Null), ExprKind::Negate(inner) => { - match eval_class_constant_value(module, current_class, current_info, inner, depth + 1)? { + match eval_class_constant_value(module, current_class, current_info, inner, depth + 1)? + { EvalClassConstantValue::Int(value) => { value.checked_neg().map(EvalClassConstantValue::Int) } @@ -443,9 +452,15 @@ fn eval_class_constant_value( _ => None, } } - ExprKind::BinaryOp { left, op, right } => { - eval_binary_class_constant_value(module, current_class, current_info, left, op, right, depth + 1) - } + ExprKind::BinaryOp { left, op, right } => eval_binary_class_constant_value( + module, + current_class, + current_info, + left, + op, + right, + depth + 1, + ), ExprKind::ClassConstant { receiver } => { let class_name = static_receiver_name(current_class, current_info, receiver)?; Some(EvalClassConstantValue::Str(class_name)) @@ -471,53 +486,32 @@ fn eval_binary_class_constant_value( let left = eval_class_constant_value(module, current_class, current_info, left, depth)?; let right = eval_class_constant_value(module, current_class, current_info, right, depth)?; match (&left, op, &right) { - ( - EvalClassConstantValue::Int(left), - BinOp::Add, - EvalClassConstantValue::Int(right), - ) => { + (EvalClassConstantValue::Int(left), BinOp::Add, EvalClassConstantValue::Int(right)) => { (*left).checked_add(*right).map(EvalClassConstantValue::Int) } - ( - EvalClassConstantValue::Int(left), - BinOp::Sub, - EvalClassConstantValue::Int(right), - ) => { + (EvalClassConstantValue::Int(left), BinOp::Sub, EvalClassConstantValue::Int(right)) => { (*left).checked_sub(*right).map(EvalClassConstantValue::Int) } - ( - EvalClassConstantValue::Int(left), - BinOp::Mul, - EvalClassConstantValue::Int(right), - ) => { + (EvalClassConstantValue::Int(left), BinOp::Mul, EvalClassConstantValue::Int(right)) => { (*left).checked_mul(*right).map(EvalClassConstantValue::Int) } - ( - EvalClassConstantValue::Int(left), - BinOp::Mod, - EvalClassConstantValue::Int(right), - ) => { + (EvalClassConstantValue::Int(left), BinOp::Mod, EvalClassConstantValue::Int(right)) => { (*left).checked_rem(*right).map(EvalClassConstantValue::Int) } - ( - EvalClassConstantValue::Int(left), - BinOp::Pow, - EvalClassConstantValue::Int(right), - ) if *right >= 0 => + (EvalClassConstantValue::Int(left), BinOp::Pow, EvalClassConstantValue::Int(right)) + if *right >= 0 => { let exponent = u32::try_from(*right).ok()?; - (*left).checked_pow(exponent).map(EvalClassConstantValue::Int) + (*left) + .checked_pow(exponent) + .map(EvalClassConstantValue::Int) + } + (EvalClassConstantValue::Str(left), BinOp::Concat, EvalClassConstantValue::Str(right)) => { + Some(EvalClassConstantValue::Str(format!("{}{}", left, right))) + } + (left, BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow, right) => { + eval_float_binary_class_constant_value(left, op, right) } - ( - EvalClassConstantValue::Str(left), - BinOp::Concat, - EvalClassConstantValue::Str(right), - ) => Some(EvalClassConstantValue::Str(format!("{}{}", left, right))), - ( - left, - BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Pow, - right, - ) => eval_float_binary_class_constant_value(left, op, right), _ => None, } } @@ -562,12 +556,16 @@ fn eval_scoped_class_constant_value( return eval_class_constant_value(module, resolved_name, Some(info), value_expr, depth); } if let Some(parent) = info.parent.as_deref() { - if let Some(value) = eval_scoped_class_constant_value(module, parent, constant_name, depth) { + if let Some(value) = + eval_scoped_class_constant_value(module, parent, constant_name, depth) + { return Some(value); } } for interface_name in &info.interfaces { - if let Some(value) = eval_scoped_class_constant_value(module, interface_name, constant_name, depth) { + if let Some(value) = + eval_scoped_class_constant_value(module, interface_name, constant_name, depth) + { return Some(value); } } @@ -644,8 +642,12 @@ fn emit_class_constant_get_helper( emitter.comment("--- eval bridge: user class constant get ---"); label_c_global(module, emitter, "__elephc_eval_value_class_constant_get"); match module.target.arch { - Arch::AArch64 => emit_value_helper_aarch64(module, emitter, data, slots, ValueHelperMode::DirectGet), - Arch::X86_64 => emit_value_helper_x86_64(module, emitter, data, slots, ValueHelperMode::DirectGet), + Arch::AArch64 => { + emit_value_helper_aarch64(module, emitter, data, slots, ValueHelperMode::DirectGet) + } + Arch::X86_64 => { + emit_value_helper_x86_64(module, emitter, data, slots, ValueHelperMode::DirectGet) + } } } @@ -660,8 +662,20 @@ fn emit_reflection_constant_value_helper( emitter.comment("--- eval bridge: reflection class constant value ---"); label_c_global(module, emitter, "__elephc_eval_reflection_constant_value"); match module.target.arch { - Arch::AArch64 => emit_value_helper_aarch64(module, emitter, data, slots, ValueHelperMode::ReflectionValue), - Arch::X86_64 => emit_value_helper_x86_64(module, emitter, data, slots, ValueHelperMode::ReflectionValue), + Arch::AArch64 => emit_value_helper_aarch64( + module, + emitter, + data, + slots, + ValueHelperMode::ReflectionValue, + ), + Arch::X86_64 => emit_value_helper_x86_64( + module, + emitter, + data, + slots, + ValueHelperMode::ReflectionValue, + ), } } @@ -696,25 +710,25 @@ fn emit_value_helper_aarch64( mode: ValueHelperMode, ) { let done_label = format!("__elephc_eval_class_constant_{}_done", mode.suffix()); - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class, constant, scope, and fp/lr - emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer - emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length - emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer - emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class, constant, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length if mode.checks_visibility() { - emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer - emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length + emitter.instruction("str x4, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #40]"); // save the active eval class-scope length } emit_aarch64_constant_value_dispatch(module, emitter, data, slots, mode); - emitter.instruction("mov x0, xzr"); // report bridge miss with a null pointer - emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emitter.instruction("mov x0, xzr"); // report bridge miss with a null pointer + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss emit_aarch64_value_slot_bodies(module, emitter, data, slots, mode, &done_label); emitter.label(&done_label); - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #64"); // release the helper frame - emitter.instruction("ret"); // return the boxed class constant value to Rust + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed class constant value to Rust } /// Emits an x86_64 class-constant value helper body. @@ -726,25 +740,25 @@ fn emit_value_helper_x86_64( mode: ValueHelperMode, ) { let done_label = format!("__elephc_eval_class_constant_{}_done_x", mode.suffix()); - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, constant, and scope slices - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length - emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer - emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for class, constant, and scope slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length if mode.checks_visibility() { - emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the active eval class-scope pointer - emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope length + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // save the active eval class-scope length } emit_x86_64_constant_value_dispatch(module, emitter, data, slots, mode); - emitter.instruction("xor eax, eax"); // report bridge miss with a null pointer - emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emitter.instruction("xor eax, eax"); // report bridge miss with a null pointer + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss emit_x86_64_value_slot_bodies(module, emitter, data, slots, mode, &done_label); emitter.label(&done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the boxed class constant value to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed class constant value to Rust } /// Emits ARM64 class-name and constant-name dispatch for value helpers. @@ -787,12 +801,12 @@ fn emit_aarch64_class_name_compare( next_label: &str, ) { let (label, len) = data.add_string(class_name.as_bytes()); - emitter.instruction("ldr x1, [sp, #0]"); // reload requested class-name pointer - emitter.instruction("ldr x2, [sp, #8]"); // reload requested class-name length + emitter.instruction("ldr x1, [sp, #0]"); // reload requested class-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested class-name length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules - emitter.instruction(&format!("cbnz x0, {}", next_label)); // continue dispatch when class names differ + emitter.instruction("bl __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction(&format!("cbnz x0, {}", next_label)); // continue dispatch when class names differ } /// Emits one x86_64 case-insensitive class-name comparison. @@ -803,13 +817,13 @@ fn emit_x86_64_class_name_compare( next_label: &str, ) { let (label, len) = data.add_string(class_name.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested class-name pointer - emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested class-name length + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested class-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested class-name length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules - emitter.instruction("test rax, rax"); // check whether the class names matched - emitter.instruction(&format!("jne {}", next_label)); // continue dispatch when class names differ + emitter.instruction("call __rt_strcasecmp"); // compare class names with PHP case-insensitive rules + emitter.instruction("test rax, rax"); // check whether the class names matched + emitter.instruction(&format!("jne {}", next_label)); // continue dispatch when class names differ } /// Emits one ARM64 case-sensitive constant-name comparison. @@ -822,17 +836,17 @@ fn emit_aarch64_constant_name_compare( next_label: &str, ) { let (label, len) = data.add_string(slot.constant.as_bytes()); - emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer - emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules let target_label = slot_body_label(module, slot, mode.suffix()); if !mode.checks_visibility() || matches!(slot.visibility, Visibility::Public) { - emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the constant value body when names match + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the constant value body when names match return; } - emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ emit_aarch64_constant_scope_check(emitter, data, slot, &target_label, next_label); } @@ -846,18 +860,18 @@ fn emit_x86_64_constant_name_compare( next_label: &str, ) { let (label, len) = data.add_string(slot.constant.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer - emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules - emitter.instruction("test rax, rax"); // check whether the constant names matched + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched let target_label = slot_body_label(module, slot, mode.suffix()); if !mode.checks_visibility() || matches!(slot.visibility, Visibility::Public) { - emitter.instruction(&format!("jne {}", target_label)); // dispatch to the constant value body when names match + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the constant value body when names match return; } - emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ emit_x86_64_constant_scope_check(emitter, data, slot, &target_label, next_label); } @@ -869,19 +883,19 @@ fn emit_aarch64_constant_scope_check( target_label: &str, next_label: &str, ) { - emitter.instruction("ldr x1, [sp, #32]"); // reload the active eval class-scope pointer - emitter.instruction("ldr x2, [sp, #40]"); // reload the active eval class-scope length - emitter.instruction(&format!("cbz x1, {}", next_label)); // reject scoped constants outside a class scope + emitter.instruction("ldr x1, [sp, #32]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", next_label)); // reject scoped constants outside a class scope for scope_name in &slot.allowed_scopes { let (label, len) = data.add_string(scope_name.as_bytes()); - emitter.instruction("ldr x1, [sp, #32]"); // reload the active eval class-scope pointer - emitter.instruction("ldr x2, [sp, #40]"); // reload the active eval class-scope length + emitter.instruction("ldr x1, [sp, #32]"); // reload the active eval class-scope pointer + emitter.instruction("ldr x2, [sp, #40]"); // reload the active eval class-scope length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class - emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch when scoped visibility is satisfied + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", target_label)); // dispatch when scoped visibility is satisfied } - emitter.instruction(&format!("b {}", next_label)); // continue dispatch after a visibility miss + emitter.instruction(&format!("b {}", next_label)); // continue dispatch after a visibility miss } /// Emits x86_64 visibility checks for a protected/private constant bridge hit. @@ -892,21 +906,21 @@ fn emit_x86_64_constant_scope_check( target_label: &str, next_label: &str, ) { - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the active eval class-scope pointer - emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope length - emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope - emitter.instruction(&format!("jz {}", next_label)); // reject scoped constants outside a class scope + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", next_label)); // reject scoped constants outside a class scope for scope_name in &slot.allowed_scopes { let (label, len) = data.add_string(scope_name.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the active eval class-scope pointer - emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope length + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the active eval class-scope pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 48]"); // reload the active eval class-scope length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class - emitter.instruction("test rax, rax"); // check whether the current scope matched - emitter.instruction(&format!("je {}", target_label)); // dispatch when scoped visibility is satisfied + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", target_label)); // dispatch when scoped visibility is satisfied } - emitter.instruction(&format!("jmp {}", next_label)); // continue dispatch after a visibility miss + emitter.instruction(&format!("jmp {}", next_label)); // continue dispatch after a visibility miss } /// Emits ARM64 value bodies for all class-constant slots. @@ -921,7 +935,7 @@ fn emit_aarch64_value_slot_bodies( for slot in slots { emitter.label(&slot_body_label(module, slot, mode.suffix())); emit_aarch64_constant_value(emitter, data, &slot.value); - emitter.instruction(&format!("b {}", done_label)); // return after boxing the constant value + emitter.instruction(&format!("b {}", done_label)); // return after boxing the constant value } } @@ -937,7 +951,7 @@ fn emit_x86_64_value_slot_bodies( for slot in slots { emitter.label(&slot_body_label(module, slot, mode.suffix())); emit_x86_64_constant_value(emitter, data, &slot.value); - emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the constant value + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the constant value } } @@ -959,7 +973,7 @@ fn emit_aarch64_constant_value( EvalClassConstantValue::Float(value) => { let label = data.add_float(*value); abi::emit_symbol_address(emitter, "x9", &label); - emitter.instruction("ldr d0, [x9]"); // load the float constant through the data-section symbol + emitter.instruction("ldr d0, [x9]"); // load the float constant through the data-section symbol emit_box_current_value_as_mixed(emitter, &PhpType::Float); } EvalClassConstantValue::Str(value) => { @@ -1001,7 +1015,7 @@ fn emit_x86_64_constant_value( EvalClassConstantValue::Float(value) => { let label = data.add_float(*value); abi::emit_symbol_address(emitter, "r10", &label); - emitter.instruction("movsd xmm0, QWORD PTR [r10]"); // load the float constant through the data-section symbol + emitter.instruction("movsd xmm0, QWORD PTR [r10]"); // load the float constant through the data-section symbol emit_box_current_value_as_mixed(emitter, &PhpType::Float); } EvalClassConstantValue::Str(value) => { @@ -1052,26 +1066,26 @@ fn emit_reflection_constant_names_aarch64( let array_new = emitter .target .extern_symbol("__elephc_eval_value_string_array_new"); - emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class slice, array, and fp/lr - emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer - emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("sub sp, sp, #64"); // reserve helper frame for class slice, array, and fp/lr + emitter.instruction("stp x29, x30, [sp, #48]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #48"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length abi::emit_load_int_immediate(emitter, "x0", slots.len() as i64); - emitter.instruction(&format!("bl {}", array_new)); // allocate the boxed string-array result - emitter.instruction(&format!("cbz x0, {}", fail_label)); // fail if the runtime could not allocate the result array - emitter.instruction("str x0, [sp, #16]"); // save the accumulated boxed string array + emitter.instruction(&format!("bl {}", array_new)); // allocate the boxed string-array result + emitter.instruction(&format!("cbz x0, {}", fail_label)); // fail if the runtime could not allocate the result array + emitter.instruction("str x0, [sp, #16]"); // save the accumulated boxed string array for (index, slot) in slots.iter().enumerate() { emit_aarch64_constant_name_push(emitter, data, slot, index, fail_label); } - emitter.instruction("ldr x0, [sp, #16]"); // return the accumulated boxed string array - emitter.instruction(&format!("b {}", done_label)); // skip null failure after a successful scan + emitter.instruction("ldr x0, [sp, #16]"); // return the accumulated boxed string array + emitter.instruction(&format!("b {}", done_label)); // skip null failure after a successful scan emitter.label(fail_label); - emitter.instruction("mov x0, xzr"); // return null when allocation or append fails + emitter.instruction("mov x0, xzr"); // return null when allocation or append fails emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #64"); // release the helper frame - emitter.instruction("ret"); // return the boxed constant-name array to Rust + emitter.instruction("ldp x29, x30, [sp, #48]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #64"); // release the helper frame + emitter.instruction("ret"); // return the boxed constant-name array to Rust } /// Emits the x86_64 Reflection constant-name array helper. @@ -1085,27 +1099,27 @@ fn emit_reflection_constant_names_x86_64( let array_new = emitter .target .extern_symbol("__elephc_eval_value_string_array_new"); - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // reserve aligned slots for class slice and result array - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class slice and result array + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length abi::emit_load_int_immediate(emitter, "rdi", slots.len() as i64); - emitter.instruction(&format!("call {}", array_new)); // allocate the boxed string-array result - emitter.instruction("test rax, rax"); // check whether allocation returned a boxed array - emitter.instruction(&format!("jz {}", fail_label)); // fail if the runtime could not allocate the result array - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the accumulated boxed string array + emitter.instruction(&format!("call {}", array_new)); // allocate the boxed string-array result + emitter.instruction("test rax, rax"); // check whether allocation returned a boxed array + emitter.instruction(&format!("jz {}", fail_label)); // fail if the runtime could not allocate the result array + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the accumulated boxed string array for (index, slot) in slots.iter().enumerate() { emit_x86_64_constant_name_push(emitter, data, slot, index, fail_label); } - emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the accumulated boxed string array - emitter.instruction(&format!("jmp {}", done_label)); // skip null failure after a successful scan + emitter.instruction("mov rax, QWORD PTR [rbp - 24]"); // return the accumulated boxed string array + emitter.instruction(&format!("jmp {}", done_label)); // skip null failure after a successful scan emitter.label(fail_label); - emitter.instruction("xor eax, eax"); // return null when allocation or append fails + emitter.instruction("xor eax, eax"); // return null when allocation or append fails emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the boxed constant-name array to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed constant-name array to Rust } /// Emits one ARM64 conditional append for a reflected constant name. @@ -1122,12 +1136,12 @@ fn emit_aarch64_constant_name_push( .extern_symbol("__elephc_eval_value_string_array_push"); emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &skip_label); let (label, len) = data.add_string(slot.constant.as_bytes()); - emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed result string array + emitter.instruction("ldr x0, [sp, #16]"); // reload the boxed result string array abi::emit_symbol_address(emitter, "x1", &label); abi::emit_load_int_immediate(emitter, "x2", len as i64); - emitter.instruction(&format!("bl {}", push_symbol)); // append the matched constant name to the result array - emitter.instruction(&format!("cbz x0, {}", fail_label)); // fail if appending returned a null array pointer - emitter.instruction("str x0, [sp, #16]"); // save the updated boxed string array + emitter.instruction(&format!("bl {}", push_symbol)); // append the matched constant name to the result array + emitter.instruction(&format!("cbz x0, {}", fail_label)); // fail if appending returned a null array pointer + emitter.instruction("str x0, [sp, #16]"); // save the updated boxed string array emitter.label(&skip_label); } @@ -1145,13 +1159,13 @@ fn emit_x86_64_constant_name_push( .extern_symbol("__elephc_eval_value_string_array_push"); emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &skip_label); let (label, len) = data.add_string(slot.constant.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the boxed result string array + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload the boxed result string array abi::emit_symbol_address(emitter, "rsi", &label); abi::emit_load_int_immediate(emitter, "rdx", len as i64); - emitter.instruction(&format!("call {}", push_symbol)); // append the matched constant name to the result array - emitter.instruction("test rax, rax"); // check whether append returned an updated array - emitter.instruction(&format!("jz {}", fail_label)); // fail if appending returned a null array pointer - emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the updated boxed string array + emitter.instruction(&format!("call {}", push_symbol)); // append the matched constant name to the result array + emitter.instruction("test rax, rax"); // check whether append returned an updated array + emitter.instruction(&format!("jz {}", fail_label)); // fail if appending returned a null array pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rax"); // save the updated boxed string array emitter.label(&skip_label); } @@ -1179,24 +1193,24 @@ fn emit_reflection_constant_flags_aarch64( slots: &[EvalClassConstantSlot], ) { let done_label = "__elephc_eval_reflection_constant_flags_done"; - emitter.instruction("sub sp, sp, #48"); // reserve helper frame for class/constant slices and fp/lr - emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer - emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length - emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer - emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + emitter.instruction("sub sp, sp, #48"); // reserve helper frame for class/constant slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length for slot in slots { let next_label = slot_miss_label(module, slot, "flags"); emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); emit_aarch64_flags_constant_name_compare(module, emitter, data, slot, &next_label); emitter.label(&next_label); } - emitter.instruction("mov x0, #0"); // return zero when no constant metadata matched + emitter.instruction("mov x0, #0"); // return zero when no constant metadata matched emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #48"); // release the helper frame - emitter.instruction("ret"); // return the matched flags, or zero, to Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #48"); // release the helper frame + emitter.instruction("ret"); // return the matched flags, or zero, to Rust } /// Emits the x86_64 Reflection constant flags helper. @@ -1207,24 +1221,24 @@ fn emit_reflection_constant_flags_x86_64( slots: &[EvalClassConstantSlot], ) { let done_label = "__elephc_eval_reflection_constant_flags_done_x"; - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and constant slices - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length - emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer - emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and constant slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length for slot in slots { let next_label = slot_miss_label(module, slot, "flags_x"); emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); emit_x86_64_flags_constant_name_compare(module, emitter, data, slot, &next_label); emitter.label(&next_label); } - emitter.instruction("xor eax, eax"); // return zero when no constant metadata matched + emitter.instruction("xor eax, eax"); // return zero when no constant metadata matched emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the matched flags, or zero, to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the matched flags, or zero, to Rust } /// Emits an ARM64 constant-name comparison that returns flags on a match. @@ -1237,14 +1251,14 @@ fn emit_aarch64_flags_constant_name_compare( ) { let done_label = "__elephc_eval_reflection_constant_flags_done"; let (label, len) = data.add_string(slot.constant.as_bytes()); - emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer - emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules - emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ abi::emit_load_int_immediate(emitter, "x0", slot_flags(slot) as i64); - emitter.instruction(&format!("b {}", done_label)); // return the matched constant flags + emitter.instruction(&format!("b {}", done_label)); // return the matched constant flags let _ = module; } @@ -1258,15 +1272,15 @@ fn emit_x86_64_flags_constant_name_compare( ) { let done_label = "__elephc_eval_reflection_constant_flags_done_x"; let (label, len) = data.add_string(slot.constant.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer - emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules - emitter.instruction("test rax, rax"); // check whether the constant names matched - emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ abi::emit_load_int_immediate(emitter, "rax", slot_flags(slot) as i64); - emitter.instruction(&format!("jmp {}", done_label)); // return the matched constant flags + emitter.instruction(&format!("jmp {}", done_label)); // return the matched constant flags let _ = module; } @@ -1285,8 +1299,12 @@ fn emit_reflection_constant_declaring_class_helper( "__elephc_eval_reflection_constant_declaring_class", ); match module.target.arch { - Arch::AArch64 => emit_reflection_constant_declaring_class_aarch64(module, emitter, data, slots), - Arch::X86_64 => emit_reflection_constant_declaring_class_x86_64(module, emitter, data, slots), + Arch::AArch64 => { + emit_reflection_constant_declaring_class_aarch64(module, emitter, data, slots) + } + Arch::X86_64 => { + emit_reflection_constant_declaring_class_x86_64(module, emitter, data, slots) + } } } @@ -1298,24 +1316,24 @@ fn emit_reflection_constant_declaring_class_aarch64( slots: &[EvalClassConstantSlot], ) { let done_label = "__elephc_eval_reflection_constant_declaring_class_done"; - emitter.instruction("sub sp, sp, #48"); // reserve helper frame for class/constant slices and fp/lr - emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer - emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer - emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length - emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer - emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length + emitter.instruction("sub sp, sp, #48"); // reserve helper frame for class/constant slices and fp/lr + emitter.instruction("stp x29, x30, [sp, #32]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #32"); // establish a stable helper frame pointer + emitter.instruction("str x0, [sp, #0]"); // save the requested class-name pointer + emitter.instruction("str x1, [sp, #8]"); // save the requested class-name length + emitter.instruction("str x2, [sp, #16]"); // save the requested constant-name pointer + emitter.instruction("str x3, [sp, #24]"); // save the requested constant-name length for slot in slots { let next_label = slot_miss_label(module, slot, "declaring"); emit_aarch64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); emit_aarch64_declaring_constant_name_compare(emitter, data, slot, &next_label, done_label); emitter.label(&next_label); } - emitter.instruction("mov x0, xzr"); // return null when no constant metadata matched + emitter.instruction("mov x0, xzr"); // return null when no constant metadata matched emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #48"); // release the helper frame - emitter.instruction("ret"); // return the declaring class string, or null, to Rust + emitter.instruction("ldp x29, x30, [sp, #32]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #48"); // release the helper frame + emitter.instruction("ret"); // return the declaring class string, or null, to Rust } /// Emits the x86_64 Reflection constant declaring-class helper. @@ -1326,24 +1344,24 @@ fn emit_reflection_constant_declaring_class_x86_64( slots: &[EvalClassConstantSlot], ) { let done_label = "__elephc_eval_reflection_constant_declaring_class_done_x"; - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and constant slices - emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length - emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer - emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 32"); // reserve aligned slots for class and constant slices + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the requested class-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save the requested class-name length + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save the requested constant-name pointer + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the requested constant-name length for slot in slots { let next_label = slot_miss_label(module, slot, "declaring_x"); emit_x86_64_class_name_compare(emitter, data, &slot.reflected_class, &next_label); emit_x86_64_declaring_constant_name_compare(emitter, data, slot, &next_label, done_label); emitter.label(&next_label); } - emitter.instruction("xor eax, eax"); // return null when no constant metadata matched + emitter.instruction("xor eax, eax"); // return null when no constant metadata matched emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the declaring class string, or null, to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the declaring class string, or null, to Rust } /// Emits an ARM64 constant-name comparison that returns declaring class on a match. @@ -1355,18 +1373,18 @@ fn emit_aarch64_declaring_constant_name_compare( done_label: &str, ) { let (constant_label, constant_len) = data.add_string(slot.constant.as_bytes()); - emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer - emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length + emitter.instruction("ldr x1, [sp, #16]"); // reload requested constant-name pointer + emitter.instruction("ldr x2, [sp, #24]"); // reload requested constant-name length abi::emit_symbol_address(emitter, "x3", &constant_label); abi::emit_load_int_immediate(emitter, "x4", constant_len as i64); - emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules - emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ + emitter.instruction("bl __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction(&format!("cbz x0, {}", next_label)); // continue dispatch when constant names differ let (class_label, class_len) = data.add_string(slot.declaring_class.as_bytes()); abi::emit_symbol_address(emitter, "x1", &class_label); abi::emit_load_int_immediate(emitter, "x2", class_len as i64); - emitter.instruction("mov x0, #1"); // runtime tag 1 = string - emitter.instruction("bl __rt_mixed_from_value"); // box the declaring class name for Rust - emitter.instruction(&format!("b {}", done_label)); // return the matched declaring class name + emitter.instruction("mov x0, #1"); // runtime tag 1 = string + emitter.instruction("bl __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction(&format!("b {}", done_label)); // return the matched declaring class name } /// Emits an x86_64 constant-name comparison that returns declaring class on a match. @@ -1378,19 +1396,19 @@ fn emit_x86_64_declaring_constant_name_compare( done_label: &str, ) { let (constant_label, constant_len) = data.add_string(slot.constant.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer - emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length + emitter.instruction("mov rdi, QWORD PTR [rbp - 24]"); // reload requested constant-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 32]"); // reload requested constant-name length abi::emit_symbol_address(emitter, "rdx", &constant_label); abi::emit_load_int_immediate(emitter, "rcx", constant_len as i64); - emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules - emitter.instruction("test rax, rax"); // check whether the constant names matched - emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ + emitter.instruction("call __rt_str_eq"); // compare constant names with PHP case-sensitive rules + emitter.instruction("test rax, rax"); // check whether the constant names matched + emitter.instruction(&format!("je {}", next_label)); // continue dispatch when constant names differ let (class_label, class_len) = data.add_string(slot.declaring_class.as_bytes()); abi::emit_symbol_address(emitter, "rdi", &class_label); abi::emit_load_int_immediate(emitter, "rsi", class_len as i64); abi::emit_load_int_immediate(emitter, "rax", 1); - emitter.instruction("call __rt_mixed_from_value"); // box the declaring class name for Rust - emitter.instruction(&format!("jmp {}", done_label)); // return the matched declaring class name + emitter.instruction("call __rt_mixed_from_value"); // box the declaring class name for Rust + emitter.instruction(&format!("jmp {}", done_label)); // return the matched declaring class name } /// Returns ReflectionClassConstant-style member flags for one slot. diff --git a/src/codegen/eval_property_helpers.rs b/src/codegen/eval_property_helpers.rs index a6b3e2cbac..5364daccc6 100644 --- a/src/codegen/eval_property_helpers.rs +++ b/src/codegen/eval_property_helpers.rs @@ -13,12 +13,12 @@ use std::collections::BTreeMap; -use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; use crate::codegen::platform::Arch; use crate::codegen::runtime_value_tag; use crate::codegen::UNINITIALIZED_TYPED_PROPERTY_SENTINEL; +use crate::codegen::{abi, emit_box_current_value_as_mixed}; use crate::ir::{Function, LocalKind, Module}; use crate::parser::ast::Visibility; use crate::types::{ClassInfo, PhpType}; @@ -73,15 +73,12 @@ fn all_module_functions(module: &Module) -> impl Iterator { /// Returns true when a function has hidden eval state locals. fn function_uses_eval(function: &Function) -> bool { - function - .locals - .iter() - .any(|local| { - matches!( - local.kind, - LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope - ) - }) + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) } /// Collects declared properties with storage layouts and visibility rules the bridge can access. @@ -111,7 +108,11 @@ fn collect_class_property_slots( .map(String::as_str) .unwrap_or(class_name) .to_string(); - (declaring_class, property_visibility(class_info, property).clone(), false) + ( + declaring_class, + property_visibility(class_info, property).clone(), + false, + ) } else { let Some((declaring_class, visibility)) = hidden_private_property_slot_metadata(module, class_name, index, property) @@ -149,7 +150,8 @@ fn hidden_private_property_slot_metadata( let Some((ancestor_property, _)) = ancestor_info.properties.get(index) else { continue; }; - if ancestor_property != property || ancestor_info.visible_property_index(property) != Some(index) + if ancestor_property != property + || ancestor_info.visible_property_index(property) != Some(index) { continue; } @@ -247,7 +249,11 @@ fn emit_property_is_initialized_helper( ) { emitter.blank(); emitter.comment("--- eval bridge: user property initialization probe ---"); - label_c_global(module, emitter, "__elephc_eval_value_property_is_initialized"); + label_c_global( + module, + emitter, + "__elephc_eval_value_property_is_initialized", + ); match module.target.arch { Arch::AArch64 => emit_property_is_initialized_aarch64(module, emitter, data, slots), Arch::X86_64 => emit_property_is_initialized_x86_64(module, emitter, data, slots), @@ -280,34 +286,34 @@ fn emit_property_get_aarch64( let null_label = "__elephc_eval_value_property_get_null"; let fail_label = "__elephc_eval_value_property_get_fail"; let done_label = "__elephc_eval_value_property_get_done"; - emitter.instruction("sub sp, sp, #80"); // reserve helper frame for saved inputs, object, scope, and fp/lr - emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer - emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer - emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length - emitter.instruction("str x0, [sp, #24]"); // save the boxed receiver for stdClass fallback reads - emitter.instruction("str x3, [sp, #32]"); // save the active eval class-scope pointer - emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope length - emitter.instruction(&format!("cbz x0, {}", null_label)); // null Mixed receiver reads as PHP null - emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload - emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object - emitter.instruction(&format!("b.ne {}", null_label)); // non-object receivers read as PHP null - emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property loads - emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for saved inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x0, [sp, #24]"); // save the boxed receiver for stdClass fallback reads + emitter.instruction("str x3, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope length + emitter.instruction(&format!("cbz x0, {}", null_label)); // null Mixed receiver reads as PHP null + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", null_label)); // non-object receivers read as PHP null + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property loads + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id emit_aarch64_property_dispatch(module, emitter, data, slots, "get", fail_label); emit_aarch64_stdclass_property_get_fallback(emitter); - emitter.instruction(&format!("b {}", done_label)); // return after stdClass fallback get or null result + emitter.instruction(&format!("b {}", done_label)); // return after stdClass fallback get or null result emit_aarch64_get_slot_bodies(module, emitter, slots, done_label); emitter.label(fail_label); - emitter.instruction("mov x0, xzr"); // report an inaccessible declared property read to Rust - emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after access failure + emitter.instruction("mov x0, xzr"); // report an inaccessible declared property read to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after access failure emitter.label(null_label); let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); abi::emit_call_label(emitter, &null_symbol); emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #80"); // release the helper frame - emitter.instruction("ret"); // return the boxed property value to Rust + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the boxed property value to Rust } /// Emits the x86_64 property-get helper body. @@ -320,36 +326,36 @@ fn emit_property_get_x86_64( let null_label = "__elephc_eval_value_property_get_null_x"; let fail_label = "__elephc_eval_value_property_get_fail_x"; let done_label = "__elephc_eval_value_property_get_done_x"; - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, length, object, and scope - emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length - emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the boxed receiver for stdClass fallback reads - emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the active eval class-scope pointer - emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope length - emitter.instruction(&format!("test rdi, rdi")); // check whether the boxed receiver pointer is null - emitter.instruction(&format!("jz {}", null_label)); // null Mixed receiver reads as PHP null - emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register - emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload - emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object - emitter.instruction(&format!("jne {}", null_label)); // non-object receivers read as PHP null - emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property loads - emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, length, object, and scope + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rdi"); // save the boxed receiver for stdClass fallback reads + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope length + emitter.instruction(&format!("test rdi, rdi")); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", null_label)); // null Mixed receiver reads as PHP null + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", null_label)); // non-object receivers read as PHP null + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property loads + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id emit_x86_64_property_dispatch(module, emitter, data, slots, "get", fail_label); emit_x86_64_stdclass_property_get_fallback(emitter); - emitter.instruction(&format!("jmp {}", done_label)); // return after stdClass fallback get or null result + emitter.instruction(&format!("jmp {}", done_label)); // return after stdClass fallback get or null result emit_x86_64_get_slot_bodies(module, emitter, slots, done_label); emitter.label(fail_label); - emitter.instruction("xor eax, eax"); // report an inaccessible declared property read to Rust - emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after access failure + emitter.instruction("xor eax, eax"); // report an inaccessible declared property read to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after access failure emitter.label(null_label); let null_symbol = module.target.extern_symbol("__elephc_eval_value_null"); abi::emit_call_label(emitter, &null_symbol); emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the boxed property value to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the boxed property value to Rust } /// Emits the ARM64 property-initialization helper body. @@ -361,29 +367,29 @@ fn emit_property_is_initialized_aarch64( ) { let fail_label = "__elephc_eval_value_property_is_initialized_fail"; let done_label = "__elephc_eval_value_property_is_initialized_done"; - emitter.instruction("sub sp, sp, #80"); // reserve helper frame for saved inputs, object, scope, and fp/lr - emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer - emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer - emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length - emitter.instruction("str x3, [sp, #32]"); // save the active eval class-scope pointer - emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope length - emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot have an initialized declared property - emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload - emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object - emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers cannot have initialized declared properties - emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for marker loads - emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emitter.instruction("sub sp, sp, #80"); // reserve helper frame for saved inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #64]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #64"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x3, [sp, #32]"); // save the active eval class-scope pointer + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope length + emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot have an initialized declared property + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers cannot have initialized declared properties + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for marker loads + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id emit_aarch64_property_dispatch(module, emitter, data, slots, "is_initialized", fail_label); - emitter.instruction(&format!("b {}", fail_label)); // no supported declared property matched the request + emitter.instruction(&format!("b {}", fail_label)); // no supported declared property matched the request emit_aarch64_initialized_slot_bodies(module, emitter, slots, done_label); emitter.label(fail_label); - emitter.instruction("mov x0, #0"); // report an initialization miss to Rust - emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss + emitter.instruction("mov x0, #0"); // report an initialization miss to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after a miss emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #80"); // release the helper frame - emitter.instruction("ret"); // return the initialization flag to Rust + emitter.instruction("ldp x29, x30, [sp, #64]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #80"); // release the helper frame + emitter.instruction("ret"); // return the initialization flag to Rust } /// Emits the x86_64 property-initialization helper body. @@ -395,31 +401,31 @@ fn emit_property_is_initialized_x86_64( ) { let fail_label = "__elephc_eval_value_property_is_initialized_fail_x"; let done_label = "__elephc_eval_value_property_is_initialized_done_x"; - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, object, and scope - emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length - emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the active eval class-scope pointer - emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope length - emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null - emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot have an initialized declared property - emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register - emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload - emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object - emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers cannot have initialized declared properties - emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for marker loads - emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 48"); // reserve aligned slots for name, object, and scope + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 40], rcx"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot have an initialized declared property + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers cannot have initialized declared properties + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for marker loads + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id emit_x86_64_property_dispatch(module, emitter, data, slots, "is_initialized", fail_label); - emitter.instruction(&format!("jmp {}", fail_label)); // no supported declared property matched the request + emitter.instruction(&format!("jmp {}", fail_label)); // no supported declared property matched the request emit_x86_64_initialized_slot_bodies(module, emitter, slots, done_label); emitter.label(fail_label); - emitter.instruction("xor eax, eax"); // report an initialization miss to Rust - emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss + emitter.instruction("xor eax, eax"); // report an initialization miss to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after a miss emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the initialization flag to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the initialization flag to Rust } /// Emits the ARM64 property-set helper body. @@ -431,31 +437,31 @@ fn emit_property_set_aarch64( ) { let fail_label = "__elephc_eval_value_property_set_fail"; let done_label = "__elephc_eval_value_property_set_done"; - emitter.instruction("sub sp, sp, #96"); // reserve helper frame for inputs, object, scope, and fp/lr - emitter.instruction("stp x29, x30, [sp, #80]"); // preserve the Rust caller frame across runtime calls - emitter.instruction("add x29, sp, #80"); // establish a stable helper frame pointer - emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer - emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length - emitter.instruction("str x3, [sp, #24]"); // save the boxed value being assigned - emitter.instruction("str x0, [sp, #32]"); // save the boxed receiver for stdClass fallback writes - emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope pointer - emitter.instruction("str x5, [sp, #48]"); // save the active eval class-scope length - emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot accept a property write - emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload - emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object - emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers reject the property write - emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property stores - emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id + emitter.instruction("sub sp, sp, #96"); // reserve helper frame for inputs, object, scope, and fp/lr + emitter.instruction("stp x29, x30, [sp, #80]"); // preserve the Rust caller frame across runtime calls + emitter.instruction("add x29, sp, #80"); // establish a stable helper frame pointer + emitter.instruction("str x1, [sp, #0]"); // save the requested property-name pointer + emitter.instruction("str x2, [sp, #8]"); // save the requested property-name length + emitter.instruction("str x3, [sp, #24]"); // save the boxed value being assigned + emitter.instruction("str x0, [sp, #32]"); // save the boxed receiver for stdClass fallback writes + emitter.instruction("str x4, [sp, #40]"); // save the active eval class-scope pointer + emitter.instruction("str x5, [sp, #48]"); // save the active eval class-scope length + emitter.instruction(&format!("cbz x0, {}", fail_label)); // null Mixed receiver cannot accept a property write + emitter.instruction("bl __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("b.ne {}", fail_label)); // non-object receivers reject the property write + emitter.instruction("str x1, [sp, #16]"); // save the unboxed object pointer for property stores + emitter.instruction("ldr x9, [x1]"); // load the object's runtime class id emit_aarch64_property_dispatch(module, emitter, data, slots, "set", fail_label); emit_aarch64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); emit_aarch64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); - emitter.instruction("mov x0, #0"); // report a failed eval property write to Rust - emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure + emitter.instruction("mov x0, #0"); // report a failed eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after failure emitter.label(done_label); - emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the Rust caller frame - emitter.instruction("add sp, sp, #96"); // release the helper frame - emitter.instruction("ret"); // return the write-status flag to Rust + emitter.instruction("ldp x29, x30, [sp, #80]"); // restore the Rust caller frame + emitter.instruction("add sp, sp, #96"); // release the helper frame + emitter.instruction("ret"); // return the write-status flag to Rust } /// Emits the x86_64 property-set helper body. @@ -467,49 +473,49 @@ fn emit_property_set_x86_64( ) { let fail_label = "__elephc_eval_value_property_set_fail_x"; let done_label = "__elephc_eval_value_property_set_done_x"; - emitter.instruction("push rbp"); // preserve the Rust caller frame pointer - emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer - emitter.instruction("sub rsp, 64"); // reserve aligned slots for name, length, object, value, and scope - emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer - emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length - emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed value being assigned - emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the boxed receiver for stdClass fallback writes - emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope pointer - emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope length - emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null - emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot accept a property write - emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register - emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload - emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object - emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers reject the property write - emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property stores - emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id + emitter.instruction("push rbp"); // preserve the Rust caller frame pointer + emitter.instruction("mov rbp, rsp"); // establish a stable helper frame pointer + emitter.instruction("sub rsp, 64"); // reserve aligned slots for name, length, object, value, and scope + emitter.instruction("mov QWORD PTR [rbp - 8], rsi"); // save the requested property-name pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rdx"); // save the requested property-name length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save the boxed value being assigned + emitter.instruction("mov QWORD PTR [rbp - 40], rdi"); // save the boxed receiver for stdClass fallback writes + emitter.instruction("mov QWORD PTR [rbp - 48], r8"); // save the active eval class-scope pointer + emitter.instruction("mov QWORD PTR [rbp - 56], r9"); // save the active eval class-scope length + emitter.instruction("test rdi, rdi"); // check whether the boxed receiver pointer is null + emitter.instruction(&format!("jz {}", fail_label)); // null Mixed receiver cannot accept a property write + emitter.instruction("mov rax, rdi"); // move the receiver into the mixed-unbox input register + emitter.instruction("call __rt_mixed_unbox"); // expose receiver tag and object payload + emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed receiver is an object + emitter.instruction(&format!("jne {}", fail_label)); // non-object receivers reject the property write + emitter.instruction("mov QWORD PTR [rbp - 24], rdi"); // save the unboxed object pointer for property stores + emitter.instruction("mov r11, QWORD PTR [rdi]"); // load the object's runtime class id emit_x86_64_property_dispatch(module, emitter, data, slots, "set", fail_label); emit_x86_64_stdclass_property_set_fallback(module, emitter, fail_label, done_label); emit_x86_64_set_slot_bodies(module, emitter, data, slots, done_label, fail_label); emitter.label(fail_label); - emitter.instruction("xor eax, eax"); // report a failed eval property write to Rust - emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after failure + emitter.instruction("xor eax, eax"); // report a failed eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after failure emitter.label(done_label); - emitter.instruction("mov rsp, rbp"); // discard helper spill slots - emitter.instruction("pop rbp"); // restore the Rust caller frame pointer - emitter.instruction("ret"); // return the write-status flag to Rust + emitter.instruction("mov rsp, rbp"); // discard helper spill slots + emitter.instruction("pop rbp"); // restore the Rust caller frame pointer + emitter.instruction("ret"); // return the write-status flag to Rust } /// Emits an ARM64 fallback read for stdClass dynamic properties. fn emit_aarch64_stdclass_property_get_fallback(emitter: &mut Emitter) { - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed receiver for the Mixed stdClass getter - emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer - emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length - emitter.instruction("bl __rt_mixed_property_get"); // read stdClass dynamic property or return Mixed(null) + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed receiver for the Mixed stdClass getter + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + emitter.instruction("bl __rt_mixed_property_get"); // read stdClass dynamic property or return Mixed(null) } /// Emits an x86_64 fallback read for stdClass dynamic properties. fn emit_x86_64_stdclass_property_get_fallback(emitter: &mut Emitter) { - emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed receiver for the Mixed stdClass getter - emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer - emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload requested property-name length - emitter.instruction("call __rt_mixed_property_get"); // read stdClass dynamic property or return Mixed(null) + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed receiver for the Mixed stdClass getter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload requested property-name length + emitter.instruction("call __rt_mixed_property_get"); // read stdClass dynamic property or return Mixed(null) } /// Emits an ARM64 fallback write for stdClass dynamic properties. @@ -520,21 +526,21 @@ fn emit_aarch64_stdclass_property_set_fallback( done_label: &str, ) { let Some(class_id) = stdclass_class_id(module) else { - emitter.instruction(&format!("b {}", fail_label)); // reject writes when stdClass metadata is unavailable + emitter.instruction(&format!("b {}", fail_label)); // reject writes when stdClass metadata is unavailable return; }; - emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for stdClass class check - emitter.instruction("ldr x9, [x9]"); // load the object's runtime class id + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for stdClass class check + emitter.instruction("ldr x9, [x9]"); // load the object's runtime class id abi::emit_load_int_immediate(emitter, "x10", class_id as i64); - emitter.instruction("cmp x9, x10"); // check whether the receiver is stdClass - emitter.instruction(&format!("b.ne {}", fail_label)); // non-stdClass misses remain unsupported eval writes - emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed receiver for the Mixed stdClass setter - emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer - emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length - emitter.instruction("ldr x3, [sp, #24]"); // reload the boxed value being assigned - emitter.instruction("bl __rt_mixed_property_set"); // write the stdClass dynamic property - emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust - emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after stdClass write + emitter.instruction("cmp x9, x10"); // check whether the receiver is stdClass + emitter.instruction(&format!("b.ne {}", fail_label)); // non-stdClass misses remain unsupported eval writes + emitter.instruction("ldr x0, [sp, #32]"); // reload the boxed receiver for the Mixed stdClass setter + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + emitter.instruction("ldr x3, [sp, #24]"); // reload the boxed value being assigned + emitter.instruction("bl __rt_mixed_property_set"); // write the stdClass dynamic property + emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // join the helper epilogue after stdClass write } /// Emits an x86_64 fallback write for stdClass dynamic properties. @@ -545,21 +551,21 @@ fn emit_x86_64_stdclass_property_set_fallback( done_label: &str, ) { let Some(class_id) = stdclass_class_id(module) else { - emitter.instruction(&format!("jmp {}", fail_label)); // reject writes when stdClass metadata is unavailable + emitter.instruction(&format!("jmp {}", fail_label)); // reject writes when stdClass metadata is unavailable return; }; - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for stdClass class check - emitter.instruction("mov r11, QWORD PTR [r11]"); // load the object's runtime class id + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for stdClass class check + emitter.instruction("mov r11, QWORD PTR [r11]"); // load the object's runtime class id abi::emit_load_int_immediate(emitter, "r10", class_id as i64); - emitter.instruction("cmp r11, r10"); // check whether the receiver is stdClass - emitter.instruction(&format!("jne {}", fail_label)); // non-stdClass misses remain unsupported eval writes - emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed receiver for the Mixed stdClass setter - emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer - emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload requested property-name length - emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the boxed value being assigned - emitter.instruction("call __rt_mixed_property_set"); // write the stdClass dynamic property - emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust - emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after stdClass write + emitter.instruction("cmp r11, r10"); // check whether the receiver is stdClass + emitter.instruction(&format!("jne {}", fail_label)); // non-stdClass misses remain unsupported eval writes + emitter.instruction("mov rdi, QWORD PTR [rbp - 40]"); // reload the boxed receiver for the Mixed stdClass setter + emitter.instruction("mov rsi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rdx, QWORD PTR [rbp - 16]"); // reload requested property-name length + emitter.instruction("mov rcx, QWORD PTR [rbp - 32]"); // reload the boxed value being assigned + emitter.instruction("call __rt_mixed_property_set"); // write the stdClass dynamic property + emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // join the helper epilogue after stdClass write } /// Returns the runtime class id for builtin `stdClass` in this module. @@ -584,13 +590,13 @@ fn emit_aarch64_property_dispatch( let class_label = format!("__elephc_eval_property_{}_class_{}", mode, class_id); let next_label = format!("__elephc_eval_property_{}_next_{}", mode, class_id); abi::emit_load_int_immediate(emitter, "x10", class_id as i64); - emitter.instruction("cmp x9, x10"); // compare receiver class id against this eval bridge class - emitter.instruction(&format!("b.ne {}", next_label)); // try the next class when ids differ + emitter.instruction("cmp x9, x10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("b.ne {}", next_label)); // try the next class when ids differ for slot in class_slots { emit_aarch64_property_name_compare(module, emitter, data, slot, mode, fail_label); } emitter.label(&class_label); - emitter.instruction(&format!("b {}", next_label)); // fall through to the next class after a name miss + emitter.instruction(&format!("b {}", next_label)); // fall through to the next class after a name miss emitter.label(&next_label); } } @@ -608,13 +614,13 @@ fn emit_x86_64_property_dispatch( let class_label = format!("__elephc_eval_property_{}_class_{}_x", mode, class_id); let next_label = format!("__elephc_eval_property_{}_next_{}_x", mode, class_id); abi::emit_load_int_immediate(emitter, "r10", class_id as i64); - emitter.instruction("cmp r11, r10"); // compare receiver class id against this eval bridge class - emitter.instruction(&format!("jne {}", next_label)); // try the next class when ids differ + emitter.instruction("cmp r11, r10"); // compare receiver class id against this eval bridge class + emitter.instruction(&format!("jne {}", next_label)); // try the next class when ids differ for slot in class_slots { emit_x86_64_property_name_compare(module, emitter, data, slot, mode, fail_label); } emitter.label(&class_label); - emitter.instruction(&format!("jmp {}", next_label)); // fall through to the next class after a name miss + emitter.instruction(&format!("jmp {}", next_label)); // fall through to the next class after a name miss emitter.label(&next_label); } } @@ -629,18 +635,18 @@ fn emit_aarch64_property_name_compare( fail_label: &str, ) { let (label, len) = data.add_string(slot.property.as_bytes()); - emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer - emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length + emitter.instruction("ldr x1, [sp, #0]"); // reload requested property-name pointer + emitter.instruction("ldr x2, [sp, #8]"); // reload requested property-name length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_str_eq"); // compare requested property name with this declared property + emitter.instruction("bl __rt_str_eq"); // compare requested property name with this declared property let target_label = slot_body_label(module, slot, mode); if matches!(slot.visibility, Visibility::Public) { - emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the property body when the names match + emitter.instruction(&format!("cbnz x0, {}", target_label)); // dispatch to the property body when the names match return; } let miss_label = slot_access_miss_label(module, slot, mode); - emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue property dispatch when names differ + emitter.instruction(&format!("cbz x0, {}", miss_label)); // continue property dispatch when names differ let scope_ok_label = slot_scope_ok_label(module, slot, mode); let scope_fail_label = if slot.is_hidden_shadow { miss_label.as_str() @@ -649,7 +655,7 @@ fn emit_aarch64_property_name_compare( }; emit_aarch64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, scope_fail_label); emitter.label(&scope_ok_label); - emitter.instruction(&format!("b {}", target_label)); // dispatch after scoped visibility is satisfied + emitter.instruction(&format!("b {}", target_label)); // dispatch after scoped visibility is satisfied emitter.label(&miss_label); } @@ -663,19 +669,19 @@ fn emit_x86_64_property_name_compare( fail_label: &str, ) { let (label, len) = data.add_string(slot.property.as_bytes()); - emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer - emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested property-name length + emitter.instruction("mov rdi, QWORD PTR [rbp - 8]"); // reload requested property-name pointer + emitter.instruction("mov rsi, QWORD PTR [rbp - 16]"); // reload requested property-name length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_str_eq"); // compare requested property name with this declared property - emitter.instruction("test rax, rax"); // check whether the property names matched + emitter.instruction("call __rt_str_eq"); // compare requested property name with this declared property + emitter.instruction("test rax, rax"); // check whether the property names matched let target_label = slot_body_label(module, slot, mode); if matches!(slot.visibility, Visibility::Public) { - emitter.instruction(&format!("jne {}", target_label)); // dispatch to the property body when the names match + emitter.instruction(&format!("jne {}", target_label)); // dispatch to the property body when the names match return; } let miss_label = slot_access_miss_label(module, slot, mode); - emitter.instruction(&format!("je {}", miss_label)); // continue property dispatch when names differ + emitter.instruction(&format!("je {}", miss_label)); // continue property dispatch when names differ let scope_ok_label = slot_scope_ok_label(module, slot, mode); let scope_fail_label = if slot.is_hidden_shadow { miss_label.as_str() @@ -684,7 +690,7 @@ fn emit_x86_64_property_name_compare( }; emit_x86_64_property_scope_check(emitter, data, slot, mode, &scope_ok_label, scope_fail_label); emitter.label(&scope_ok_label); - emitter.instruction(&format!("jmp {}", target_label)); // dispatch after scoped visibility is satisfied + emitter.instruction(&format!("jmp {}", target_label)); // dispatch after scoped visibility is satisfied emitter.label(&miss_label); } @@ -698,19 +704,19 @@ fn emit_aarch64_property_scope_check( fail_label: &str, ) { let (scope_ptr_offset, scope_len_offset) = aarch64_scope_offsets(mode); - emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer - emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length - emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject scoped property access outside a class scope + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction(&format!("cbz x1, {}", fail_label)); // reject scoped property access outside a class scope for scope_name in &slot.allowed_scopes { let (label, len) = data.add_string(scope_name.as_bytes()); - emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer - emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length + emitter.instruction(&format!("ldr x1, [sp, #{}]", scope_ptr_offset)); // reload the active eval class-scope pointer + emitter.instruction(&format!("ldr x2, [sp, #{}]", scope_len_offset)); // reload the active eval class-scope length abi::emit_symbol_address(emitter, "x3", &label); abi::emit_load_int_immediate(emitter, "x4", len as i64); - emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class - emitter.instruction(&format!("cbz x0, {}", success_label)); // accept access when the current scope is allowed + emitter.instruction("bl __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction(&format!("cbz x0, {}", success_label)); // accept access when the current scope is allowed } - emitter.instruction(&format!("b {}", fail_label)); // reject scoped property access from unrelated classes + emitter.instruction(&format!("b {}", fail_label)); // reject scoped property access from unrelated classes } /// Emits x86_64 visibility checks for a protected/private property bridge hit. @@ -725,19 +731,19 @@ fn emit_x86_64_property_scope_check( let (scope_ptr_offset, scope_len_offset) = x86_64_scope_offsets(mode); emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length - emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope - emitter.instruction(&format!("jz {}", fail_label)); // reject scoped property access outside a class scope + emitter.instruction("test rdi, rdi"); // check whether eval is executing inside a class scope + emitter.instruction(&format!("jz {}", fail_label)); // reject scoped property access outside a class scope for scope_name in &slot.allowed_scopes { let (label, len) = data.add_string(scope_name.as_bytes()); emitter.instruction(&format!("mov rdi, QWORD PTR [rbp - {}]", scope_ptr_offset)); // reload the active eval class-scope pointer emitter.instruction(&format!("mov rsi, QWORD PTR [rbp - {}]", scope_len_offset)); // reload the active eval class-scope length abi::emit_symbol_address(emitter, "rdx", &label); abi::emit_load_int_immediate(emitter, "rcx", len as i64); - emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class - emitter.instruction("test rax, rax"); // check whether the current scope matched - emitter.instruction(&format!("je {}", success_label)); // accept access when the current scope is allowed + emitter.instruction("call __rt_strcasecmp"); // compare current eval scope with an allowed class + emitter.instruction("test rax, rax"); // check whether the current scope matched + emitter.instruction(&format!("je {}", success_label)); // accept access when the current scope is allowed } - emitter.instruction(&format!("jmp {}", fail_label)); // reject scoped property access from unrelated classes + emitter.instruction(&format!("jmp {}", fail_label)); // reject scoped property access from unrelated classes } /// Returns ARM64 stack offsets for the class-scope pointer and length. @@ -769,7 +775,7 @@ fn emit_aarch64_get_slot_bodies( emitter.label(&slot_body_label(module, slot, "get")); emit_aarch64_uninitialized_property_get_guard(emitter, slot, done_label); emit_aarch64_box_property_slot(emitter, slot); - emitter.instruction(&format!("b {}", done_label)); // return after boxing the declared property value + emitter.instruction(&format!("b {}", done_label)); // return after boxing the declared property value } } @@ -784,7 +790,7 @@ fn emit_x86_64_get_slot_bodies( emitter.label(&slot_body_label(module, slot, "get")); emit_x86_64_uninitialized_property_get_guard(emitter, slot, done_label); emit_x86_64_box_property_slot(emitter, slot); - emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the declared property value + emitter.instruction(&format!("jmp {}", done_label)); // return after boxing the declared property value } } @@ -798,7 +804,7 @@ fn emit_aarch64_initialized_slot_bodies( for slot in slots { emitter.label(&slot_body_label(module, slot, "is_initialized")); emit_aarch64_property_initialized_flag(emitter, slot); - emitter.instruction(&format!("b {}", done_label)); // return after materializing the initialization flag + emitter.instruction(&format!("b {}", done_label)); // return after materializing the initialization flag } } @@ -812,7 +818,7 @@ fn emit_x86_64_initialized_slot_bodies( for slot in slots { emitter.label(&slot_body_label(module, slot, "is_initialized")); emit_x86_64_property_initialized_flag(emitter, slot); - emitter.instruction(&format!("jmp {}", done_label)); // return after materializing the initialization flag + emitter.instruction(&format!("jmp {}", done_label)); // return after materializing the initialization flag } } @@ -828,8 +834,8 @@ fn emit_aarch64_set_slot_bodies( for slot in slots { emitter.label(&slot_body_label(module, slot, "set")); emit_aarch64_store_property_slot(module, emitter, data, slot, fail_label); - emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust - emitter.instruction(&format!("b {}", done_label)); // return after storing the declared property value + emitter.instruction("mov x0, #1"); // report a successful eval property write to Rust + emitter.instruction(&format!("b {}", done_label)); // return after storing the declared property value } } @@ -845,36 +851,36 @@ fn emit_x86_64_set_slot_bodies( for slot in slots { emitter.label(&slot_body_label(module, slot, "set")); emit_x86_64_store_property_slot(module, emitter, data, slot, fail_label); - emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust - emitter.instruction(&format!("jmp {}", done_label)); // return after storing the declared property value + emitter.instruction("mov rax, 1"); // report a successful eval property write to Rust + emitter.instruction(&format!("jmp {}", done_label)); // return after storing the declared property value } } /// Emits an ARM64 boolean for one declared property's initialized state. fn emit_aarch64_property_initialized_flag(emitter: &mut Emitter, slot: &EvalPropertySlot) { if !slot.is_declared { - emitter.instruction("mov x0, #1"); // non-typed declared properties are always initialized + emitter.instruction("mov x0, #1"); // non-typed declared properties are always initialized return; } - emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer - emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker + emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer + emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker abi::emit_load_int_immediate(emitter, "x12", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); - emitter.instruction("cmp x11, x12"); // compare the property marker against the uninitialized sentinel - emitter.instruction("cset x0, ne"); // materialize true when the instance property is initialized + emitter.instruction("cmp x11, x12"); // compare the property marker against the uninitialized sentinel + emitter.instruction("cset x0, ne"); // materialize true when the instance property is initialized } /// Emits an x86_64 boolean for one declared property's initialized state. fn emit_x86_64_property_initialized_flag(emitter: &mut Emitter, slot: &EvalPropertySlot) { if !slot.is_declared { - emitter.instruction("mov rax, 1"); // non-typed declared properties are always initialized + emitter.instruction("mov rax, 1"); // non-typed declared properties are always initialized return; } - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer emitter.instruction(&format!("mov rax, QWORD PTR [r11 + {}]", slot.offset + 8)); // load the typed-property initialization marker abi::emit_load_int_immediate(emitter, "r10", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); - emitter.instruction("cmp rax, r10"); // compare the property marker against the uninitialized sentinel - emitter.instruction("setne al"); // materialize true when the instance property is initialized - emitter.instruction("movzx rax, al"); // widen the initialization flag into the return register + emitter.instruction("cmp rax, r10"); // compare the property marker against the uninitialized sentinel + emitter.instruction("setne al"); // materialize true when the instance property is initialized + emitter.instruction("movzx rax, al"); // widen the initialization flag into the return register } /// Emits an ARM64 typed-property guard before boxing an eval bridge property read. @@ -890,13 +896,13 @@ fn emit_aarch64_uninitialized_property_get_guard( "{}_initialized", label_fragment(&slot_body_label_raw(slot, "get")) ); - emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer for marker inspection - emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker + emitter.instruction("ldr x10, [sp, #16]"); // reload the unboxed object pointer for marker inspection + emitter.instruction(&format!("ldr x11, [x10, #{}]", slot.offset + 8)); // load the typed-property initialization marker abi::emit_load_int_immediate(emitter, "x12", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); - emitter.instruction("cmp x11, x12"); // compare the property marker against the uninitialized sentinel - emitter.instruction(&format!("b.ne {}", initialized_label)); // continue boxing once the instance property is initialized - emitter.instruction("mov x0, xzr"); // report uninitialized property reads as bridge failures - emitter.instruction(&format!("b {}", done_label)); // return the failure to Rust without boxing storage + emitter.instruction("cmp x11, x12"); // compare the property marker against the uninitialized sentinel + emitter.instruction(&format!("b.ne {}", initialized_label)); // continue boxing once the instance property is initialized + emitter.instruction("mov x0, xzr"); // report uninitialized property reads as bridge failures + emitter.instruction(&format!("b {}", done_label)); // return the failure to Rust without boxing storage emitter.label(&initialized_label); } @@ -913,32 +919,36 @@ fn emit_x86_64_uninitialized_property_get_guard( "{}_initialized_x", label_fragment(&slot_body_label_raw(slot, "get")) ); - emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for marker inspection + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for marker inspection emitter.instruction(&format!("mov rax, QWORD PTR [r10 + {}]", slot.offset + 8)); // load the typed-property initialization marker abi::emit_load_int_immediate(emitter, "r11", UNINITIALIZED_TYPED_PROPERTY_SENTINEL); - emitter.instruction("cmp rax, r11"); // compare the property marker against the uninitialized sentinel - emitter.instruction(&format!("jne {}", initialized_label)); // continue boxing once the instance property is initialized - emitter.instruction("xor eax, eax"); // report uninitialized property reads as bridge failures - emitter.instruction(&format!("jmp {}", done_label)); // return the failure to Rust without boxing storage + emitter.instruction("cmp rax, r11"); // compare the property marker against the uninitialized sentinel + emitter.instruction(&format!("jne {}", initialized_label)); // continue boxing once the instance property is initialized + emitter.instruction("xor eax, eax"); // report uninitialized property reads as bridge failures + emitter.instruction(&format!("jmp {}", done_label)); // return the failure to Rust without boxing storage emitter.label(&initialized_label); } /// Boxes a property value loaded from an ARM64 object slot into a Mixed cell. fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { - emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer match slot.ty.codegen_repr() { - PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { - emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset)); // load the property payload low word - emitter.instruction("mov x2, xzr"); // heap/scalar property payloads do not use a high word here + PhpType::Int + | PhpType::Bool + | PhpType::Object(_) + | PhpType::Array(_) + | PhpType::AssocArray { .. } => { + emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset)); // load the property payload low word + emitter.instruction("mov x2, xzr"); // heap/scalar property payloads do not use a high word here abi::emit_load_int_immediate(emitter, "x0", runtime_value_tag(&slot.ty) as i64); - emitter.instruction("bl __rt_mixed_from_value"); // box the property payload as a Mixed cell + emitter.instruction("bl __rt_mixed_from_value"); // box the property payload as a Mixed cell } PhpType::Float => { - emitter.instruction(&format!("ldr d0, [x9, #{}]", slot.offset)); // load the floating property payload - emitter.instruction("fmov x1, d0"); // move float bits into the Mixed low payload word - emitter.instruction("mov x2, xzr"); // float payloads do not use a high word - emitter.instruction("mov x0, #2"); // runtime tag 2 = float - emitter.instruction("bl __rt_mixed_from_value"); // box the floating property payload as Mixed + emitter.instruction(&format!("ldr d0, [x9, #{}]", slot.offset)); // load the floating property payload + emitter.instruction("fmov x1, d0"); // move float bits into the Mixed low payload word + emitter.instruction("mov x2, xzr"); // float payloads do not use a high word + emitter.instruction("mov x0, #2"); // runtime tag 2 = float + emitter.instruction("bl __rt_mixed_from_value"); // box the floating property payload as Mixed } PhpType::Str => { emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset)); // load the string property pointer @@ -947,17 +957,23 @@ fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot emitter.instruction("bl __rt_mixed_from_value"); // persist and box the string property payload } PhpType::TaggedScalar => { - emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the nullable integer property payload + emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the nullable integer property payload emitter.instruction(&format!("ldr x1, [x9, #{}]", slot.offset + 8)); //load the nullable integer property tag emit_box_current_value_as_mixed(emitter, &PhpType::TaggedScalar); } PhpType::Mixed | PhpType::Union(_) => { - let null_label = format!("{}_mixed_null", label_fragment(&slot_body_label_raw(slot, "get"))); - let done_label = format!("{}_mixed_done", label_fragment(&slot_body_label_raw(slot, "get"))); - emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the stored Mixed property cell - emitter.instruction(&format!("cbz x0, {}", null_label)); // null property storage reads as PHP null - emitter.instruction("bl __rt_incref"); // retain the stored Mixed cell for the eval caller - emitter.instruction(&format!("b {}", done_label)); // skip null materialization after a retained hit + let null_label = format!( + "{}_mixed_null", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + let done_label = format!( + "{}_mixed_done", + label_fragment(&slot_body_label_raw(slot, "get")) + ); + emitter.instruction(&format!("ldr x0, [x9, #{}]", slot.offset)); // load the stored Mixed property cell + emitter.instruction(&format!("cbz x0, {}", null_label)); // null property storage reads as PHP null + emitter.instruction("bl __rt_incref"); // retain the stored Mixed cell for the eval caller + emitter.instruction(&format!("b {}", done_label)); // skip null materialization after a retained hit emitter.label(&null_label); let null_symbol = emitter.target.extern_symbol("__elephc_eval_value_null"); abi::emit_call_label(emitter, &null_symbol); @@ -972,13 +988,13 @@ fn emit_aarch64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot /// Boxes a property value loaded from an x86_64 object slot into a Mixed cell. fn emit_x86_64_box_property_slot(emitter: &mut Emitter, slot: &EvalPropertySlot) { - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer match slot.ty.codegen_repr() { PhpType::Int | PhpType::Bool | PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { emitter.instruction(&format!("mov rdi, QWORD PTR [r11 + {}]", slot.offset)); // load the property payload low word emitter.instruction("xor esi, esi"); // heap/scalar property payloads do not use a high word here abi::emit_load_int_immediate(emitter, "rax", runtime_value_tag(&slot.ty) as i64); - emitter.instruction("call __rt_mixed_from_value"); // box the property payload as a Mixed cell + emitter.instruction("call __rt_mixed_from_value"); // box the property payload as a Mixed cell } PhpType::Float => { emitter.instruction(&format!("movsd xmm0, QWORD PTR [r11 + {}]", slot.offset)); // load the floating property payload @@ -1028,12 +1044,14 @@ fn emit_aarch64_store_property_slot( ) { match slot.ty.codegen_repr() { PhpType::Int => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "x0"), - PhpType::Bool => emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "x0"), + PhpType::Bool => { + emit_aarch64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "x0") + } PhpType::Float => { - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for float coercion - emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval value to a PHP float - emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store - emitter.instruction(&format!("str d0, [x9, #{}]", slot.offset)); // store the coerced float into the property slot + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for float coercion + emitter.instruction("bl __rt_mixed_cast_float"); // coerce the eval value to a PHP float + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str d0, [x9, #{}]", slot.offset)); // store the coerced float into the property slot emit_aarch64_clear_scalar_property_marker(emitter, slot); } PhpType::Str => { @@ -1049,7 +1067,14 @@ fn emit_aarch64_store_property_slot( emit_aarch64_store_heap_property_slot(emitter, slot, 5, fail_label); } PhpType::Object(class_name) => { - emit_aarch64_store_object_property_slot(module, emitter, data, slot, &class_name, fail_label); + emit_aarch64_store_object_property_slot( + module, + emitter, + data, + slot, + &class_name, + fail_label, + ); } PhpType::Mixed | PhpType::Union(_) => { emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value being assigned @@ -1072,7 +1097,9 @@ fn emit_x86_64_store_property_slot( ) { match slot.ty.codegen_repr() { PhpType::Int => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_int", "rax"), - PhpType::Bool => emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "rax"), + PhpType::Bool => { + emit_x86_64_store_cast_scalar(emitter, slot, "__rt_mixed_cast_bool", "rax") + } PhpType::Float => { emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for float coercion emitter.instruction("call __rt_mixed_cast_float"); // coerce the eval value to a PHP float @@ -1093,7 +1120,14 @@ fn emit_x86_64_store_property_slot( emit_x86_64_store_heap_property_slot(emitter, slot, 5, fail_label); } PhpType::Object(class_name) => { - emit_x86_64_store_object_property_slot(module, emitter, data, slot, &class_name, fail_label); + emit_x86_64_store_object_property_slot( + module, + emitter, + data, + slot, + &class_name, + fail_label, + ); } PhpType::Mixed | PhpType::Union(_) => { emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value being assigned @@ -1123,7 +1157,7 @@ fn emit_aarch64_store_cast_scalar( /// Clears an ARM64 one-word scalar typed-property marker after a successful store. fn emit_aarch64_clear_scalar_property_marker(emitter: &mut Emitter, slot: &EvalPropertySlot) { if slot.is_declared { - emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the typed-property initialization marker + emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the typed-property initialization marker } } @@ -1137,20 +1171,20 @@ fn emit_aarch64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalP "{}_tagged_scalar_done", label_fragment(&slot_body_label_raw(slot, "set")) ); - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for nullable-int inspection - emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned value tag and payload words - emitter.instruction("cmp x0, #8"); // runtime tag 8 means the assigned value is null - emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null property writes - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for integer coercion - emitter.instruction("bl __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp x0, #8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("b.eq {}", null_label)); // materialize a tagged null for null property writes + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for integer coercion + emitter.instruction("bl __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - emitter.instruction(&format!("b {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.instruction(&format!("b {}", done_label)); // skip tagged-null materialization after integer coercion emitter.label(&null_label); crate::codegen::sentinels::emit_tagged_scalar_null(emitter); emitter.label(&done_label); - emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store - emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the nullable integer payload into the property slot - emitter.instruction(&format!("str x1, [x9, #{}]", slot.offset + 8)); // store the nullable integer tag into the property slot + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the store + emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the nullable integer payload into the property slot + emitter.instruction(&format!("str x1, [x9, #{}]", slot.offset + 8)); // store the nullable integer tag into the property slot } /// Emits an x86_64 scalar property store after Mixed coercion. @@ -1170,7 +1204,8 @@ fn emit_x86_64_store_cast_scalar( /// Clears an x86_64 one-word scalar typed-property marker after a successful store. fn emit_x86_64_clear_scalar_property_marker(emitter: &mut Emitter, slot: &EvalPropertySlot) { if slot.is_declared { - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); // clear the typed-property initialization marker + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); + // clear the typed-property initialization marker } } @@ -1181,16 +1216,16 @@ fn emit_aarch64_store_heap_property_slot( expected_tag: i64, fail_label: &str, ) { - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for heap payload inspection - emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("bl __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer abi::emit_load_int_immediate(emitter, "x10", expected_tag); - emitter.instruction("cmp x0, x10"); // compare the assigned value tag with the property storage ABI - emitter.instruction(&format!("b.ne {}", fail_label)); // reject heap values with an incompatible ABI shape - emitter.instruction("mov x0, x1"); // move the unboxed heap pointer into the retained-result register + emitter.instruction("cmp x0, x10"); // compare the assigned value tag with the property storage ABI + emitter.instruction(&format!("b.ne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov x0, x1"); // move the unboxed heap pointer into the retained-result register abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); - emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the heap store - emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the retained heap pointer into the property slot - emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the typed-property initialization marker + emitter.instruction("ldr x9, [sp, #16]"); // reload the unboxed object pointer for the heap store + emitter.instruction(&format!("str x0, [x9, #{}]", slot.offset)); // store the retained heap pointer into the property slot + emitter.instruction(&format!("str xzr, [x9, #{}]", slot.offset + 8)); // clear the typed-property initialization marker } /// Validates and stores a boxed ARM64 eval object into an object property slot. @@ -1205,12 +1240,12 @@ fn emit_aarch64_store_object_property_slot( if !class_name.is_empty() { let (label, len) = data.add_string(class_name.as_bytes()); let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); - emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for object type validation + emitter.instruction("ldr x0, [sp, #24]"); // reload the boxed eval value for object type validation abi::emit_symbol_address(emitter, "x1", &label); abi::emit_load_int_immediate(emitter, "x2", len as i64); - emitter.instruction("mov x3, xzr"); // allow exact class matches for object property type hints + emitter.instruction("mov x3, xzr"); // allow exact class matches for object property type hints abi::emit_call_label(emitter, &is_a_symbol); - emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject values that fail the object property type hint + emitter.instruction(&format!("cbz x0, {}", fail_label)); // reject values that fail the object property type hint } emit_aarch64_store_heap_property_slot(emitter, slot, 6, fail_label); } @@ -1222,16 +1257,17 @@ fn emit_x86_64_store_heap_property_slot( expected_tag: i64, fail_label: &str, ) { - emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for heap payload inspection - emitter.instruction("call __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for heap payload inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned heap value tag and payload pointer abi::emit_load_int_immediate(emitter, "r10", expected_tag); - emitter.instruction("cmp rax, r10"); // compare the assigned value tag with the property storage ABI - emitter.instruction(&format!("jne {}", fail_label)); // reject heap values with an incompatible ABI shape - emitter.instruction("mov rax, rdi"); // move the unboxed heap pointer into the retained-result register + emitter.instruction("cmp rax, r10"); // compare the assigned value tag with the property storage ABI + emitter.instruction(&format!("jne {}", fail_label)); // reject heap values with an incompatible ABI shape + emitter.instruction("mov rax, rdi"); // move the unboxed heap pointer into the retained-result register abi::emit_incref_if_refcounted(emitter, &slot.ty.codegen_repr()); - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the heap store + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the heap store emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); //store the retained heap pointer into the property slot - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); //clear the typed-property initialization marker + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], 0", slot.offset + 8)); + //clear the typed-property initialization marker } /// Validates and stores a boxed x86_64 eval object into an object property slot. @@ -1246,13 +1282,13 @@ fn emit_x86_64_store_object_property_slot( if !class_name.is_empty() { let (label, len) = data.add_string(class_name.as_bytes()); let is_a_symbol = module.target.extern_symbol("__elephc_eval_value_is_a"); - emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed eval value for object type validation + emitter.instruction("mov rdi, QWORD PTR [rbp - 32]"); // reload the boxed eval value for object type validation abi::emit_symbol_address(emitter, "rsi", &label); abi::emit_load_int_immediate(emitter, "rdx", len as i64); - emitter.instruction("xor ecx, ecx"); // allow exact class matches for object property type hints + emitter.instruction("xor ecx, ecx"); // allow exact class matches for object property type hints abi::emit_call_label(emitter, &is_a_symbol); - emitter.instruction("test rax, rax"); // check whether the value satisfied the object property type hint - emitter.instruction(&format!("je {}", fail_label)); // reject values that fail the object property type hint + emitter.instruction("test rax, rax"); // check whether the value satisfied the object property type hint + emitter.instruction(&format!("je {}", fail_label)); // reject values that fail the object property type hint } emit_x86_64_store_heap_property_slot(emitter, slot, 6, fail_label); } @@ -1267,27 +1303,31 @@ fn emit_x86_64_store_tagged_scalar_property(emitter: &mut Emitter, slot: &EvalPr "{}_tagged_scalar_done_x", label_fragment(&slot_body_label_raw(slot, "set")) ); - emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for nullable-int inspection - emitter.instruction("call __rt_mixed_unbox"); // expose the assigned value tag and payload words - emitter.instruction("cmp rax, 8"); // runtime tag 8 means the assigned value is null - emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null property writes - emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for integer coercion - emitter.instruction("call __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for nullable-int inspection + emitter.instruction("call __rt_mixed_unbox"); // expose the assigned value tag and payload words + emitter.instruction("cmp rax, 8"); // runtime tag 8 means the assigned value is null + emitter.instruction(&format!("je {}", null_label)); // materialize a tagged null for null property writes + emitter.instruction("mov rax, QWORD PTR [rbp - 32]"); // reload the boxed eval value for integer coercion + emitter.instruction("call __rt_mixed_cast_int"); // coerce non-null eval values to a PHP int payload crate::codegen::sentinels::emit_tagged_scalar_from_int_result(emitter); - emitter.instruction(&format!("jmp {}", done_label)); // skip tagged-null materialization after integer coercion + emitter.instruction(&format!("jmp {}", done_label)); // skip tagged-null materialization after integer coercion emitter.label(&null_label); crate::codegen::sentinels::emit_tagged_scalar_null(emitter); emitter.label(&done_label); - emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store + emitter.instruction("mov r11, QWORD PTR [rbp - 24]"); // reload the unboxed object pointer for the store emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rax", slot.offset)); //store the nullable integer payload into the property slot - emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rdx", slot.offset + 8)); //store the nullable integer tag into the property slot + emitter.instruction(&format!("mov QWORD PTR [r11 + {}], rdx", slot.offset + 8)); + //store the nullable integer tag into the property slot } /// Groups property slots by class id while preserving sorted class order. fn grouped_slots(slots: &[EvalPropertySlot]) -> BTreeMap> { let mut grouped = BTreeMap::new(); for slot in slots { - grouped.entry(slot.class_id).or_insert_with(Vec::new).push(slot); + grouped + .entry(slot.class_id) + .or_insert_with(Vec::new) + .push(slot); } grouped } diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index 8a7a5aa786..c875105afc 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -216,6 +216,8 @@ pub(super) fn lower_instruction(ctx: &mut FunctionContext<'_>, inst_id: InstId) Op::ExternCall => externs::lower_extern_call(ctx, &inst), Op::BuiltinCall => builtins::lower_builtin_call(ctx, &inst), Op::EvalLiteralCall => builtins::lower_eval_literal_call(ctx, &inst), + Op::EvalScopeGet => builtins::lower_eval_scope_get(ctx, &inst), + Op::EvalScopeSet => builtins::lower_eval_scope_set(ctx, &inst), Op::EvalFunctionCall => builtins::lower_eval_function_call(ctx, &inst), Op::EvalFunctionCallArray => builtins::lower_eval_function_call_array(ctx, &inst), Op::EvalObjectNew => builtins::lower_eval_object_new(ctx, &inst), @@ -4690,7 +4692,7 @@ fn lower_static_method_call(ctx: &mut FunctionContext<'_>, inst: &Instruction) - } else { None }; - let eval_done_label = if late_bound_static && ctx.module.required_runtime_features.eval { + let eval_done_label = if late_bound_static && ctx.module.required_runtime_features.eval_bridge { let no_override_label = ctx.next_label("eval_late_static_no_override"); let done_label = ctx.next_label("eval_late_static_done"); builtins::lower_eval_native_frame_static_method_call( @@ -5613,7 +5615,7 @@ fn emit_loaded_assoc_array_to_mixed(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => {} Arch::X86_64 => { - ctx.emitter.instruction("mov rdi, rax"); // pass the loaded associative-array argument to the Mixed conversion helper + ctx.emitter.instruction("mov rdi, rax"); // pass the loaded associative-array argument to the Mixed conversion helper } } abi::emit_call_label(ctx.emitter, "__rt_hash_to_mixed"); @@ -6735,9 +6737,9 @@ fn lower_load_global(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Resul let data = expect_global_name(inst)?; let name = ctx.global_name_data(data)?; let symbol = ir_global_symbol(name); - let result = inst.result.ok_or_else(|| { - CodegenIrError::invalid_module("load_global missing result value") - })?; + let result = inst + .result + .ok_or_else(|| CodegenIrError::invalid_module("load_global missing result value"))?; let ty = ctx.value_php_type(result)?; ctx.data .add_comm(symbol.clone(), ty.codegen_repr().stack_size().max(8)); diff --git a/src/codegen/lower_inst/builtins.rs b/src/codegen/lower_inst/builtins.rs index ebc9c91b4c..1a04552d64 100644 --- a/src/codegen/lower_inst/builtins.rs +++ b/src/codegen/lower_inst/builtins.rs @@ -100,6 +100,22 @@ pub(super) fn lower_eval_literal_call( eval::lower_eval(ctx, inst) } +/// Lowers a direct EIR eval-scope lookup by static variable name. +pub(super) fn lower_eval_scope_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_scope_get(ctx, inst) +} + +/// Lowers a direct EIR eval-scope write by static variable name. +pub(super) fn lower_eval_scope_set( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + eval::lower_eval_scope_set(ctx, inst) +} + /// Lowers a native call to a zero-argument function declared through `eval()`. pub(super) fn lower_eval_function_call( ctx: &mut FunctionContext<'_>, @@ -475,12 +491,12 @@ fn emit_mixed_gettype(ctx: &mut FunctionContext<'_>, value: ValueId) -> Result<( fn emit_branch_on_gettype_mixed_tag(ctx: &mut FunctionContext<'_>, tag: u8, label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp x0, #{}", tag)); // compare the unboxed Mixed tag against this gettype() case - ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching gettype() type-name case + ctx.emitter.instruction(&format!("cmp x0, #{}", tag)); // compare the unboxed Mixed tag against this gettype() case + ctx.emitter.instruction(&format!("b.eq {}", label)); // branch to the matching gettype() type-name case } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp rax, {}", tag)); // compare the unboxed Mixed tag against this gettype() case - ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching gettype() type-name case + ctx.emitter.instruction(&format!("cmp rax, {}", tag)); // compare the unboxed Mixed tag against this gettype() case + ctx.emitter.instruction(&format!("je {}", label)); // branch to the matching gettype() type-name case } } } @@ -677,8 +693,8 @@ fn emit_dynamic_class_like_exists_compare( abi::emit_symbol_address(ctx.emitter, "x3", &candidate_label); abi::emit_load_int_immediate(ctx.emitter, "x4", candidate_len as i64); abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); - ctx.emitter.instruction("cmp x0, #0"); // did the dynamic class-like name match this metadata entry? - ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // report existence when the runtime name matches case-insensitively + ctx.emitter.instruction("cmp x0, #0"); // did the dynamic class-like name match this metadata entry? + ctx.emitter.instruction(&format!("b.eq {}", matched_label)); // report existence when the runtime name matches case-insensitively } Arch::X86_64 => { abi::emit_load_temporary_stack_slot(ctx.emitter, "rdi", 0); @@ -686,8 +702,8 @@ fn emit_dynamic_class_like_exists_compare( abi::emit_symbol_address(ctx.emitter, "rdx", &candidate_label); abi::emit_load_int_immediate(ctx.emitter, "rcx", candidate_len as i64); abi::emit_call_label(ctx.emitter, "__rt_strcasecmp"); - ctx.emitter.instruction("test rax, rax"); // did the dynamic class-like name match this metadata entry? - ctx.emitter.instruction(&format!("je {}", matched_label)); // report existence when the runtime name matches case-insensitively + ctx.emitter.instruction("test rax, rax"); // did the dynamic class-like name match this metadata entry? + ctx.emitter.instruction(&format!("je {}", matched_label)); // report existence when the runtime name matches case-insensitively } } } @@ -754,7 +770,7 @@ pub(crate) fn lower_is_callable(ctx: &mut FunctionContext<'_>, inst: &Instructio /// Calls the runtime `is_callable` helper for pointer-shaped values already in result regs. fn emit_is_callable_pointer_lookup(ctx: &mut FunctionContext<'_>, label: &str) { if ctx.emitter.target.arch == Arch::X86_64 { - ctx.emitter.instruction("mov rdi, rax"); // move pointer-shaped value into helper argument 0 + ctx.emitter.instruction("mov rdi, rax"); // move pointer-shaped value into helper argument 0 } abi::emit_call_label(ctx.emitter, label); } @@ -763,19 +779,23 @@ fn emit_is_callable_pointer_lookup(ctx: &mut FunctionContext<'_>, label: &str) { fn emit_is_callable_dynamic_string_lookup(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, x1"); // move string pointer into helper argument 0 - ctx.emitter.instruction("mov x1, x2"); // move string length into helper argument 1 + ctx.emitter.instruction("mov x0, x1"); // move string pointer into helper argument 0 + ctx.emitter.instruction("mov x1, x2"); // move string length into helper argument 1 } Arch::X86_64 => { - ctx.emitter.instruction("mov rdi, rax"); // move string pointer into helper argument 0 - ctx.emitter.instruction("mov rsi, rdx"); // move string length into helper argument 1 + ctx.emitter.instruction("mov rdi, rax"); // move string pointer into helper argument 0 + ctx.emitter.instruction("mov rsi, rdx"); // move string length into helper argument 1 } } abi::emit_call_label(ctx.emitter, "__rt_is_callable_string"); } /// Lowers `method_exists()` and `property_exists()` through eval or static metadata. -fn lower_member_exists(ctx: &mut FunctionContext<'_>, inst: &Instruction, name: &str) -> Result<()> { +fn lower_member_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + name: &str, +) -> Result<()> { ensure_arg_count(inst, name, 2)?; let target = expect_operand(inst, 0)?; let member = expect_operand(inst, 1)?; @@ -985,8 +1005,8 @@ fn emit_variant_function_exists(ctx: &mut FunctionContext<'_>, function_name: &s abi::emit_load_symbol_to_reg(ctx.emitter, result_reg, &active_symbol, 0); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // test whether an include has activated this function variant - ctx.emitter.instruction(&format!("cset {}, ne", result_reg)); // return true only when a function variant is active + ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // test whether an include has activated this function variant + ctx.emitter.instruction(&format!("cset {}, ne", result_reg)); // return true only when a function variant is active } Arch::X86_64 => { ctx.emitter.instruction(&format!("test {}, {}", result_reg, result_reg)); // test whether an include has activated this function variant @@ -1138,6 +1158,10 @@ pub(crate) fn lower_floatval(ctx: &mut FunctionContext<'_>, inst: &Instruction) ctx.load_value_to_result(value)?; abi::emit_call_label(ctx.emitter, "__rt_str_to_number"); } + PhpType::Mixed | PhpType::Union(_) => { + load_value_to_first_int_arg(ctx, value)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + } other => { return Err(CodegenIrError::unsupported(format!( "floatval for PHP type {:?}", @@ -1170,6 +1194,10 @@ pub(crate) fn lower_boolval(ctx: &mut FunctionContext<'_>, inst: &Instruction) - PhpType::Array(_) | PhpType::AssocArray { .. } | PhpType::Iterable => { predicates::emit_array_truthiness(ctx, value)?; } + PhpType::Mixed | PhpType::Union(_) => { + load_value_to_first_int_arg(ctx, value)?; + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); + } other => { return Err(CodegenIrError::unsupported(format!( "boolval for PHP type {:?}", @@ -1249,13 +1277,13 @@ fn emit_int_result_zero_bool(ctx: &mut FunctionContext<'_>) { let result_reg = abi::int_result_reg(ctx.emitter); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // compare the empty() integer operand against zero - ctx.emitter.instruction(&format!("cset {}, eq", result_reg)); // return true when the integer operand is zero + ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // compare the empty() integer operand against zero + ctx.emitter.instruction(&format!("cset {}, eq", result_reg)); // return true when the integer operand is zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, 0", result_reg)); // compare the empty() integer operand against zero - ctx.emitter.instruction("sete al"); // materialize true when the integer operand is zero - ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register + ctx.emitter.instruction(&format!("cmp {}, 0", result_reg)); // compare the empty() integer operand against zero + ctx.emitter.instruction("sete al"); // materialize true when the integer operand is zero + ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register } } } @@ -1264,14 +1292,14 @@ fn emit_int_result_zero_bool(ctx: &mut FunctionContext<'_>) { fn emit_float_result_zero_bool(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("fcmp d0, #0.0"); // compare the empty() float operand against zero - ctx.emitter.instruction("cset x0, eq"); // return true when the float operand is zero + ctx.emitter.instruction("fcmp d0, #0.0"); // compare the empty() float operand against zero + ctx.emitter.instruction("cset x0, eq"); // return true when the float operand is zero } Arch::X86_64 => { - ctx.emitter.instruction("xorpd xmm1, xmm1"); // materialize a zero float register for empty() comparison - ctx.emitter.instruction("ucomisd xmm0, xmm1"); // compare the empty() float operand against zero - ctx.emitter.instruction("sete al"); // materialize true when the float operand is zero - ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register + ctx.emitter.instruction("xorpd xmm1, xmm1"); // materialize a zero float register for empty() comparison + ctx.emitter.instruction("ucomisd xmm0, xmm1"); // compare the empty() float operand against zero + ctx.emitter.instruction("sete al"); // materialize true when the float operand is zero + ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register } } } @@ -1281,13 +1309,13 @@ fn emit_string_length_zero_bool(ctx: &mut FunctionContext<'_>) { let len_reg = abi::string_result_regs(ctx.emitter).1; match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction(&format!("cmp {}, #0", len_reg)); // compare the empty() string length against zero - ctx.emitter.instruction("cset x0, eq"); // return true when the string length is zero + ctx.emitter.instruction(&format!("cmp {}, #0", len_reg)); // compare the empty() string length against zero + ctx.emitter.instruction("cset x0, eq"); // return true when the string length is zero } Arch::X86_64 => { - ctx.emitter.instruction(&format!("cmp {}, 0", len_reg)); // compare the empty() string length against zero - ctx.emitter.instruction("sete al"); // materialize true when the string length is zero - ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register + ctx.emitter.instruction(&format!("cmp {}, 0", len_reg)); // compare the empty() string length against zero + ctx.emitter.instruction("sete al"); // materialize true when the string length is zero + ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register } } } @@ -1296,10 +1324,10 @@ fn emit_string_length_zero_bool(ctx: &mut FunctionContext<'_>) { fn invert_bool_result(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("eor x0, x0, #1"); // invert the canonical boolean result for empty() + ctx.emitter.instruction("eor x0, x0, #1"); // invert the canonical boolean result for empty() } Arch::X86_64 => { - ctx.emitter.instruction("xor rax, 1"); // invert the canonical boolean result for empty() + ctx.emitter.instruction("xor rax, 1"); // invert the canonical boolean result for empty() } } } @@ -1346,17 +1374,17 @@ fn emit_tagged_scalar_int_predicate( "cmp x1, #{}", crate::codegen::sentinels::TAGGED_SCALAR_TAG_NULL ); - ctx.emitter.instruction(&cmp_inst); // does the tagged scalar carry the runtime null tag? - ctx.emitter.instruction("cset x0, ne"); // materialize true when the tagged scalar holds an integer + ctx.emitter.instruction(&cmp_inst); // does the tagged scalar carry the runtime null tag? + ctx.emitter.instruction("cset x0, ne"); // materialize true when the tagged scalar holds an integer } Arch::X86_64 => { let cmp_inst = format!( "cmp rdx, {}", crate::codegen::sentinels::TAGGED_SCALAR_TAG_NULL ); - ctx.emitter.instruction(&cmp_inst); // does the tagged scalar carry the runtime null tag? - ctx.emitter.instruction("setne al"); // materialize true when the tagged scalar holds an integer - ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register + ctx.emitter.instruction(&cmp_inst); // does the tagged scalar carry the runtime null tag? + ctx.emitter.instruction("setne al"); // materialize true when the tagged scalar holds an integer + ctx.emitter.instruction("movzx rax, al"); // widen the boolean byte into the integer result register } } Ok(()) @@ -1407,24 +1435,24 @@ fn emit_mixed_is_iterable(ctx: &mut FunctionContext<'_>, value: ValueId) -> Resu abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #4"); // check for a boxed indexed-array payload - ctx.emitter.instruction(&format!("b.eq {}", true_case)); // indexed arrays satisfy is_iterable - ctx.emitter.instruction("cmp x0, #5"); // check for a boxed associative-array payload - ctx.emitter.instruction(&format!("b.eq {}", true_case)); // associative arrays satisfy is_iterable - ctx.emitter.instruction("cmp x0, #6"); // check for a boxed object payload - ctx.emitter.instruction(&format!("b.eq {}", object_case)); // objects need a Traversable interface check - ctx.emitter.instruction("mov x0, #0"); // all other Mixed payloads are not iterable - ctx.emitter.instruction(&format!("b {}", done)); // skip the truthy result path + ctx.emitter.instruction("cmp x0, #4"); // check for a boxed indexed-array payload + ctx.emitter.instruction(&format!("b.eq {}", true_case)); // indexed arrays satisfy is_iterable + ctx.emitter.instruction("cmp x0, #5"); // check for a boxed associative-array payload + ctx.emitter.instruction(&format!("b.eq {}", true_case)); // associative arrays satisfy is_iterable + ctx.emitter.instruction("cmp x0, #6"); // check for a boxed object payload + ctx.emitter.instruction(&format!("b.eq {}", object_case)); // objects need a Traversable interface check + ctx.emitter.instruction("mov x0, #0"); // all other Mixed payloads are not iterable + ctx.emitter.instruction(&format!("b {}", done)); // skip the truthy result path } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 4"); // check for a boxed indexed-array payload - ctx.emitter.instruction(&format!("je {}", true_case)); // indexed arrays satisfy is_iterable - ctx.emitter.instruction("cmp rax, 5"); // check for a boxed associative-array payload - ctx.emitter.instruction(&format!("je {}", true_case)); // associative arrays satisfy is_iterable - ctx.emitter.instruction("cmp rax, 6"); // check for a boxed object payload - ctx.emitter.instruction(&format!("je {}", object_case)); // objects need a Traversable interface check - ctx.emitter.instruction("mov rax, 0"); // all other Mixed payloads are not iterable - ctx.emitter.instruction(&format!("jmp {}", done)); // skip the truthy result path + ctx.emitter.instruction("cmp rax, 4"); // check for a boxed indexed-array payload + ctx.emitter.instruction(&format!("je {}", true_case)); // indexed arrays satisfy is_iterable + ctx.emitter.instruction("cmp rax, 5"); // check for a boxed associative-array payload + ctx.emitter.instruction(&format!("je {}", true_case)); // associative arrays satisfy is_iterable + ctx.emitter.instruction("cmp rax, 6"); // check for a boxed object payload + ctx.emitter.instruction(&format!("je {}", object_case)); // objects need a Traversable interface check + ctx.emitter.instruction("mov rax, 0"); // all other Mixed payloads are not iterable + ctx.emitter.instruction(&format!("jmp {}", done)); // skip the truthy result path } } ctx.emitter.label(&object_case); @@ -1450,16 +1478,16 @@ fn emit_runtime_object_iterable_check( } match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("str x1, [sp, #-16]!"); // preserve the unboxed object pointer across Traversable checks + ctx.emitter.instruction("str x1, [sp, #-16]!"); // preserve the unboxed object pointer across Traversable checks for interface_id in interface_ids { emit_saved_object_interface_check(ctx, interface_id, &object_true); } - ctx.emitter.instruction("add sp, sp, #16"); // discard the saved object pointer after failed checks - ctx.emitter.instruction("mov x0, #0"); // non-Traversable objects are not iterable - ctx.emitter.instruction(&format!("b {}", done)); // skip the truthy result path + ctx.emitter.instruction("add sp, sp, #16"); // discard the saved object pointer after failed checks + ctx.emitter.instruction("mov x0, #0"); // non-Traversable objects are not iterable + ctx.emitter.instruction(&format!("b {}", done)); // skip the truthy result path ctx.emitter.label(&object_true); - ctx.emitter.instruction("add sp, sp, #16"); // discard the saved object pointer before returning true - ctx.emitter.instruction(&format!("b {}", true_case)); // continue through the shared truthy result path + ctx.emitter.instruction("add sp, sp, #16"); // discard the saved object pointer before returning true + ctx.emitter.instruction(&format!("b {}", true_case)); // continue through the shared truthy result path } Arch::X86_64 => { abi::emit_push_reg(ctx.emitter, "rdi"); @@ -1467,11 +1495,11 @@ fn emit_runtime_object_iterable_check( emit_saved_object_interface_check(ctx, interface_id, &object_true); } abi::emit_pop_reg(ctx.emitter, "r10"); - ctx.emitter.instruction("xor eax, eax"); // non-Traversable objects are not iterable - ctx.emitter.instruction(&format!("jmp {}", done)); // skip the truthy result path + ctx.emitter.instruction("xor eax, eax"); // non-Traversable objects are not iterable + ctx.emitter.instruction(&format!("jmp {}", done)); // skip the truthy result path ctx.emitter.label(&object_true); abi::emit_pop_reg(ctx.emitter, "r10"); - ctx.emitter.instruction(&format!("jmp {}", true_case)); // continue through the shared truthy result path + ctx.emitter.instruction(&format!("jmp {}", true_case)); // continue through the shared truthy result path } } } @@ -1484,20 +1512,20 @@ fn emit_saved_object_interface_check( ) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("ldr x0, [sp]"); // reload the object pointer as matcher argument 1 + ctx.emitter.instruction("ldr x0, [sp]"); // reload the object pointer as matcher argument 1 abi::emit_load_int_immediate(ctx.emitter, "x1", interface_id as i64); abi::emit_load_int_immediate(ctx.emitter, "x2", 1); - abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); // check whether the object implements the Traversable interface - ctx.emitter.instruction("cmp x0, #0"); // test whether the runtime matcher succeeded - ctx.emitter.instruction(&format!("b.ne {}", true_case)); // a matching interface makes the object iterable + abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); // check whether the object implements the Traversable interface + ctx.emitter.instruction("cmp x0, #0"); // test whether the runtime matcher succeeded + ctx.emitter.instruction(&format!("b.ne {}", true_case)); // a matching interface makes the object iterable } Arch::X86_64 => { - ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the object pointer as matcher argument 1 + ctx.emitter.instruction("mov rdi, QWORD PTR [rsp]"); // reload the object pointer as matcher argument 1 abi::emit_load_int_immediate(ctx.emitter, "rsi", interface_id as i64); abi::emit_load_int_immediate(ctx.emitter, "rdx", 1); - abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); // check whether the object implements the Traversable interface - ctx.emitter.instruction("test rax, rax"); // test whether the runtime matcher succeeded - ctx.emitter.instruction(&format!("jne {}", true_case)); // a matching interface makes the object iterable + abi::emit_call_label(ctx.emitter, "__rt_exception_matches"); // check whether the object implements the Traversable interface + ctx.emitter.instruction("test rax, rax"); // test whether the runtime matcher succeeded + ctx.emitter.instruction(&format!("jne {}", true_case)); // a matching interface makes the object iterable } } } diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 9536f4abad..1b00b73973 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -12,6 +12,7 @@ //! lowering; this module only materializes the bridge ABI call. //! - The bridge is target-mangled like other C staticlib symbols. +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use crate::codegen::eval_ref_arg_helpers::eval_signature_ref_params_supported; @@ -22,7 +23,9 @@ use crate::codegen::{ }; use crate::ir::{Function, Immediate, Instruction, LocalKind, LocalSlotId, Module, Op, ValueId}; use crate::names::{function_symbol, ir_global_symbol, php_symbol_key}; -use crate::parser::ast::{BinOp, Expr, ExprKind, StaticReceiver, TypeExpr, Visibility}; +use crate::parser::ast::{ + BinOp, Expr, ExprKind, StaticReceiver, Stmt, StmtKind, TypeExpr, Visibility, +}; use crate::types::{ is_php_integer_array_key, AttrArgValue, ClassInfo, FunctionSig, InterfaceInfo, PhpType, PropertyHookContract, @@ -30,7 +33,8 @@ use crate::types::{ use super::super::super::context::FunctionContext; use super::super::{ - expect_data, expect_operand, function_signature_from_eir, store_if_result, + expect_data, expect_global_name, expect_operand, function_signature_from_eir, predicates, + store_if_result, }; const EVAL_STATUS_PARSE_ERROR: i64 = 1; @@ -51,6 +55,7 @@ const EVAL_CODE_LEN_OFFSET: usize = 56; const EVAL_GLOBAL_SCOPE_HANDLE_OFFSET: usize = 64; const EVAL_CALLED_CLASS_PTR_OFFSET: usize = 72; const EVAL_CALLED_CLASS_LEN_OFFSET: usize = 80; +const EVAL_LOCAL_SCALAR_SLOT_BYTES: usize = 32; const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; const EVAL_CLASS_LOOKUP_GET_CLASS: i64 = 0; @@ -108,6 +113,12 @@ struct EvalSyncGlobal { ty: PhpType, } +/// Source location for one direct Mixed parameter passed into scope-read eval AOT. +enum EvalScopeReadParamSource { + Local(EvalSyncLocal), + Null, +} + /// Local-to-global alias metadata inherited by eval from the caller function scope. #[derive(Clone)] struct EvalGlobalAlias { @@ -115,6 +126,196 @@ struct EvalGlobalAlias { global_name: String, } +/// Straight-line literal eval instruction that can be emitted without the interpreter. +enum EvalLiteralAotInst { + Echo(EvalLiteralAotExpr), + Store { + name: String, + value: EvalLiteralAotExpr, + }, + Return(EvalLiteralAotExpr), +} + +/// Boxed-Mixed expression accepted by the literal eval AOT path. +enum EvalLiteralAotExpr { + Scalar(EvalLiteralAotScalar), + LoadVar(String), + Binary { + op: EvalLiteralAotBinaryOp, + left: Box, + right: Box, + }, +} + +/// Runtime-backed binary operation accepted by the literal eval AOT path. +enum EvalLiteralAotBinaryOp { + Add, + Sub, + Mul, + Div, + Mod, + Concat, +} + +/// Scalar value accepted by the first conservative literal-eval AOT subset. +#[derive(Clone)] +enum EvalLiteralAotScalar { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), +} + +/// Parsed and validated literal eval fragment ready for direct code emission. +enum EvalLiteralAotProgram { + Boxed(EvalLiteralBoxedAotProgram), + LocalScalar(EvalLocalScalarAotProgram), +} + +/// Parsed boxed-Mixed literal eval fragment for scope-oriented direct emission. +struct EvalLiteralBoxedAotProgram { + instructions: Vec, + scope_reads: BTreeSet, + scope_writes: BTreeSet, + has_scope_writes: bool, + has_scope_access: bool, +} + +/// Local scalar eval statement emitted as stack-slot native code before final scope flush. +enum EvalLocalScalarStmt { + Noop, + Echo(EvalLocalScalarExpr), + Store { + name: String, + value: EvalLocalScalarExpr, + }, + If { + branches: Vec<(EvalLocalScalarExpr, Vec)>, + else_body: Vec, + }, + While { + condition: EvalLocalScalarExpr, + body: Vec, + }, + DoWhile { + body: Vec, + condition: EvalLocalScalarExpr, + }, + For { + init: Option>, + condition: Option, + update: Option>, + body: Vec, + }, + Switch { + subject: EvalLocalScalarExpr, + cases: Vec<(Vec, Vec)>, + default: Vec, + default_index: Option, + }, + Break(usize), + Continue(usize), + Return(Option), +} + +/// Local scalar expression accepted by the control-flow AOT subset. +struct EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind, + ty: EvalLocalScalarType, +} + +/// Expression payload for the local scalar AOT subset. +enum EvalLocalScalarExprKind { + Null, + Int(i64), + Float(f64), + Bool(bool), + String(String), + LoadVar(String), + Isset(Vec), + EmptyVar(String), + Negate(Box), + BitNot(Box), + Not(Box), + Print(Box), + Ternary { + condition: Box, + then_expr: Box, + else_expr: Box, + }, + Binary { + op: EvalLocalScalarBinaryOp, + left: Box, + right: Box, + }, + StaticFunctionCall { + name: String, + args: Vec, + }, +} + +/// Runtime scalar type tracked by the local eval AOT subset. +#[derive(Clone, Copy, PartialEq, Eq)] +enum EvalLocalScalarType { + Null, + Int, + Float, + Bool, + String, +} + +/// Binary operations accepted by the local scalar AOT subset. +enum EvalLocalScalarBinaryOp { + Add, + Sub, + Mul, + Div, + Mod, + BitAnd, + BitOr, + BitXor, + ShiftLeft, + ShiftRight, + Lt, + Gt, + LtEq, + GtEq, + Eq, + NotEq, + And, + Or, + Concat, +} + +/// Parsed and analyzed local-scalar eval fragment ready for native control-flow emission. +struct EvalLocalScalarAotProgram { + statements: Vec, + locals: BTreeMap, + local_types: BTreeMap, + scratch_slots: usize, +} + +/// Mutable compile-time state for local scalar eval eligibility analysis. +struct EvalLocalScalarAnalysis { + locals: BTreeMap, + local_types: BTreeMap, + max_scratch_slots: usize, +} + +/// Break and continue targets for one nested local AOT loop. +struct EvalLocalLoopLabels { + break_label: String, + continue_label: String, +} + +/// Selects how local scalar AOT boxes eval return values. +#[derive(Clone, Copy)] +enum EvalLocalScalarBoxing { + EvalRuntime, + CoreRuntime, +} + /// A module-local function that can be registered with the eval context. struct EvalNativeFunctionRegistration { name: String, @@ -207,7 +408,10 @@ struct EvalNativeMemberAttributeRegistration { /// Native callable default that can be registered with libelephc-magician. enum EvalNativeCallableDefault { - Scalar { kind: i64, payload: i64 }, + Scalar { + kind: i64, + payload: i64, + }, String(String), Array(Vec), Object { @@ -216,90 +420,4541 @@ enum EvalNativeCallableDefault { }, } -/// Array element metadata for a native callable default registered with eval. -struct EvalNativeCallableArrayDefaultElement { - key: Option, - default: EvalNativeCallableDefault, +/// Array element metadata for a native callable default registered with eval. +struct EvalNativeCallableArrayDefaultElement { + key: Option, + default: EvalNativeCallableDefault, +} + +/// Static array key metadata for a native callable default registered with eval. +enum EvalNativeCallableArrayDefaultKey { + Int(i64), + String(String), +} + +/// Constructor argument metadata for an object-valued native callable default. +struct EvalNativeCallableObjectDefaultArg { + name: Option, + default: EvalNativeCallableDefault, +} + +/// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. +pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + super::ensure_arg_count(inst, "eval", 1)?; + if let Some(fragment) = eval_literal_fragment(ctx, inst)? { + if crate::eval_aot::literal_fragment_direct_local_read_write_writes(&fragment).is_none() + && lower_eval_literal_eir_function(ctx, inst, &fragment)? + { + return Ok(()); + } + } + if let Some(program) = eval_literal_aot_program(ctx, inst)? { + if lower_eval_literal_aot(ctx, inst, &program)? { + return Ok(()); + } + } + emit_eval_literal_aot_marker(ctx, inst)?; + let code = expect_operand(inst, 0)?; + let ty = ctx.load_value_to_result(code)?.codegen_repr(); + if ty != PhpType::Str { + return Err(CodegenIrError::unsupported(format!( + "eval() argument lowering for PHP type {:?}", + ty + ))); + } + + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + save_eval_code_string(ctx); + ensure_eval_context(ctx)?; + set_eval_call_site(ctx, inst); + ensure_eval_scope(ctx)?; + ensure_eval_global_scope(ctx)?; + let sync_locals = eval_sync_locals(ctx); + let sync_globals = eval_sync_globals(ctx); + let global_aliases = eval_global_aliases(ctx); + flush_eval_scope_locals(ctx, &sync_locals)?; + flush_eval_global_scope(ctx, &sync_globals)?; + mark_eval_scope_global_aliases(ctx, &global_aliases); + set_eval_context_global_scope(ctx); + let pushed_class_scope = push_eval_context_class_scope(ctx)?; + load_eval_context_to_arg(ctx, 0); + load_eval_scope_to_arg(ctx, 1); + move_saved_eval_code_to_eval_args(ctx); + let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_execute"); + abi::emit_call_label(ctx.emitter, &symbol); + pop_eval_context_class_scope(ctx, pushed_class_scope); + emit_eval_status_check(ctx); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + reload_eval_scope_locals(ctx, &sync_locals)?; + reload_eval_global_scope(ctx, &sync_globals)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Calls a pre-lowered internal EIR function for no-scope literal eval fragments. +fn lower_eval_literal_eir_function( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + fragment: &str, +) -> Result { + let function_name = crate::eval_aot::eir_function_name(fragment); + if let Some(callee) = ctx.callable_function_by_name(&function_name) { + if callee.params.is_empty() && callee.return_php_type.codegen_repr() == PhpType::Mixed { + ctx.emitter + .comment("eval literal AOT compiled EIR function"); + let caller_stack_pad_bytes = abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, 0); + abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + abi::emit_call_label(ctx.emitter, &function_symbol(&function_name)); + abi::emit_release_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + store_if_result(ctx, inst)?; + return Ok(true); + } + } + lower_eval_literal_scope_read_eir_function(ctx, inst, fragment) +} + +/// Calls a pre-lowered internal EIR function that reads from the eval scope. +fn lower_eval_literal_scope_read_eir_function( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + fragment: &str, +) -> Result { + let function_name = crate::eval_aot::eir_scope_read_function_name(fragment); + let Some(callee) = ctx.callable_function_by_name(&function_name) else { + return Ok(false); + }; + let param_types = callee + .params + .iter() + .map(|param| param.php_type.codegen_repr()) + .collect::>(); + let return_type = callee.return_php_type.codegen_repr(); + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment, + ctx.module.source_path.as_deref(), + |name, args| eval_literal_static_function_supported_by_codegen(ctx, name, args), + |receiver, method, args| { + eval_literal_static_method_supported_by_codegen(ctx, receiver, method, args) + }, + ); + if plan.uses_scope_read_params() { + return lower_eval_literal_scope_read_param_eir_function( + ctx, + inst, + &function_name, + ¶m_types, + &return_type, + plan.reads(), + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ); + } + if !eval_scope_read_constraints_supported( + ctx, + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ) { + return Ok(false); + } + if param_types.len() != 1 || return_type != PhpType::Mixed { + return Ok(false); + } + ctx.emitter + .comment("eval literal AOT compiled EIR function with scope reads"); + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_scope(ctx)?; + let read_names = plan.reads().clone(); + let write_names = plan.writes().clone(); + let mut flush_names = read_names.clone(); + flush_names.extend(write_names.iter().cloned()); + let sync_locals = eval_sync_locals(ctx); + let sync_globals = eval_sync_globals(ctx); + let flush_locals = filter_eval_sync_locals_by_name(sync_locals.clone(), &flush_names); + let flush_globals = filter_eval_sync_globals_by_name(sync_globals.clone(), &flush_names); + let reload_locals = filter_eval_sync_locals_by_name(sync_locals, &write_names); + let reload_globals = filter_eval_sync_globals_by_name(sync_globals, &write_names); + flush_eval_scope_locals(ctx, &flush_locals)?; + flush_eval_globals_to_local_scope(ctx, &flush_globals); + load_eval_scope_to_arg(ctx, 0); + abi::emit_call_label(ctx.emitter, &function_symbol(&function_name)); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + reload_eval_scope_locals(ctx, &reload_locals)?; + reload_eval_globals_from_local_scope(ctx, &reload_globals)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst)?; + Ok(true) +} + +/// Calls a read-only scope eval AOT function by passing direct boxed Mixed params. +fn lower_eval_literal_scope_read_param_eir_function( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + function_name: &str, + param_types: &[PhpType], + return_type: &PhpType, + read_names: &BTreeSet, + array_read_constraints: &BTreeSet, + assoc_array_read_constraints: &BTreeSet, + float_predicate_read_constraints: &BTreeSet, +) -> Result { + if param_types.len() != read_names.len() + || param_types + .iter() + .any(|ty| ty.codegen_repr() != PhpType::Mixed) + || return_type.codegen_repr() != PhpType::Mixed + { + return Ok(false); + } + if !eval_scope_read_constraints_supported( + ctx, + array_read_constraints, + assoc_array_read_constraints, + float_predicate_read_constraints, + ) { + return Ok(false); + } + let Some(param_sources) = eval_scope_read_param_sources(ctx, read_names) else { + return Ok(false); + }; + ctx.emitter + .comment("eval literal AOT compiled EIR function with direct read params"); + for source in ¶m_sources { + emit_eval_scope_read_param_source(ctx, source)?; + abi::emit_push_result_value(ctx.emitter, &PhpType::Mixed); + } + let assignments = + abi::build_outgoing_arg_assignments_for_target(ctx.emitter.target, param_types, 0); + let overflow_bytes = abi::materialize_outgoing_args(ctx.emitter, &assignments); + let caller_stack_pad_bytes = + abi::outgoing_call_stack_pad_bytes(ctx.emitter.target, overflow_bytes); + abi::emit_reserve_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + abi::emit_call_label(ctx.emitter, &function_symbol(function_name)); + abi::emit_release_temporary_stack(ctx.emitter, caller_stack_pad_bytes); + abi::emit_release_temporary_stack(ctx.emitter, overflow_bytes); + store_if_result(ctx, inst)?; + Ok(true) +} + +/// Resolves read-only eval variables to direct local values or undefined null. +fn eval_scope_read_param_sources( + ctx: &FunctionContext<'_>, + read_names: &BTreeSet, +) -> Option> { + let sync_locals = eval_sync_locals(ctx); + read_names + .iter() + .map(|name| { + if let Some(local) = sync_locals.iter().find(|local| local.name == *name) { + return Some(EvalScopeReadParamSource::Local(local.clone())); + } + if ctx.function.locals.iter().any(|local| { + local.name.as_deref() == Some(name.as_str()) + && local.kind == LocalKind::PhpLocal + && !local_uses_eval_global_sync(ctx, local.name.as_deref()) + && local.php_type.codegen_repr() == PhpType::Void + }) { + return Some(EvalScopeReadParamSource::Null); + } + let has_unsupported_local = ctx + .function + .locals + .iter() + .any(|local| local.name.as_deref() == Some(name.as_str())); + (!has_unsupported_local).then_some(EvalScopeReadParamSource::Null) + }) + .collect() +} + +/// Returns true when constrained direct read params have compatible local sources. +fn eval_scope_read_constraints_supported( + ctx: &FunctionContext<'_>, + array_read_constraints: &BTreeSet, + assoc_array_read_constraints: &BTreeSet, + float_predicate_read_constraints: &BTreeSet, +) -> bool { + let sync_locals = eval_sync_locals(ctx); + array_read_constraints.iter().all(|name| { + sync_locals + .iter() + .find(|local| local.name == *name) + .is_some_and(|local| eval_scope_read_array_param_type_supported(&local.ty)) + }) && assoc_array_read_constraints.iter().all(|name| { + sync_locals + .iter() + .find(|local| local.name == *name) + .is_some_and(|local| eval_scope_read_assoc_array_param_type_supported(&local.ty)) + }) && float_predicate_read_constraints.iter().all(|name| { + sync_locals + .iter() + .find(|local| local.name == *name) + .is_some_and(|local| eval_scope_read_float_predicate_param_type_supported(&local.ty)) + }) +} + +/// Returns true when a direct read-param source has array-only semantics. +fn eval_scope_read_array_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } + ) +} + +/// Returns true when a direct read-param source has associative-array-only semantics. +fn eval_scope_read_assoc_array_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::AssocArray { .. }) +} + +/// Returns true when a direct read-param source can feed IEEE float predicates. +fn eval_scope_read_float_predicate_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Int | PhpType::Float) +} + +/// Emits one direct read-param value as a boxed Mixed result. +fn emit_eval_scope_read_param_source( + ctx: &mut FunctionContext<'_>, + source: &EvalScopeReadParamSource, +) -> Result<()> { + match source { + EvalScopeReadParamSource::Local(local) => { + let ty = ctx.load_local_to_result(local.slot)?.codegen_repr(); + if !matches!(ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &ty); + } + } + EvalScopeReadParamSource::Null => emit_eval_local_scalar_core_null_cell(ctx), + } + Ok(()) +} + +/// Returns true when a static function call matches the EIR eval AOT codegen subset. +fn eval_literal_static_function_supported_by_codegen( + ctx: &FunctionContext<'_>, + name: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let key = php_symbol_key(name.trim_start_matches('\\')); + let Some(function) = ctx + .module + .functions + .iter() + .find(|function| php_symbol_key(function.name.trim_start_matches('\\')) == key) + else { + return false; + }; + let signature = function_signature_from_eir(function); + crate::eval_aot::static_function_signature_supported(&signature, args) +} + +/// Returns true when a static method call matches the EIR eval AOT codegen subset. +fn eval_literal_static_method_supported_by_codegen( + ctx: &FunctionContext<'_>, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let StaticReceiver::Named(class_name) = receiver else { + return false; + }; + let class_name = class_name.as_str().trim_start_matches('\\'); + let method_key = php_symbol_key(method); + let Some(receiver_info) = ctx.module.class_infos.get(class_name) else { + return false; + }; + if receiver_info + .static_method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public) + != &Visibility::Public + { + return false; + } + let impl_class = receiver_info + .static_method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + let Some(signature) = ctx + .module + .class_infos + .get(impl_class) + .and_then(|class_info| class_info.static_methods.get(&method_key)) + else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) +} + +/// Lowers an EIR eval-scope read for a static variable name. +pub(super) fn lower_eval_scope_get( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "eval scope get", 1)?; + let scope = expect_operand(inst, 0)?; + let name = eval_scope_instruction_name(ctx, inst)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + load_eval_scope_operand_to_arg(ctx, scope, 0)?; + emit_eval_scope_get_for_loaded_scope(ctx, &name, 0, 8); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 0); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + store_if_result(ctx, inst) +} + +/// Lowers an EIR eval-scope write for a static variable name. +pub(super) fn lower_eval_scope_set( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, +) -> Result<()> { + super::ensure_arg_count(inst, "eval scope set", 2)?; + let scope = expect_operand(inst, 0)?; + let value = expect_operand(inst, 1)?; + let name = eval_scope_instruction_name(ctx, inst)?; + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + let value_ty = ctx.load_value_to_result(value)?.codegen_repr(); + let flags = if matches!(value_ty, PhpType::Mixed | PhpType::Union(_)) { + abi::emit_call_label(ctx.emitter, "__rt_incref"); + EVAL_SCOPE_FLAG_OWNED + } else { + emit_box_current_value_as_mixed(ctx.emitter, &value_ty); + scope_set_flags_for_type(&value_ty) + }; + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + load_eval_scope_operand_to_arg(ctx, scope, 0)?; + emit_eval_scope_set_for_loaded_scope(ctx, &name, flags); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + Ok(()) +} + +/// Returns the static PHP variable name attached to an eval-scope instruction. +fn eval_scope_instruction_name(ctx: &FunctionContext<'_>, inst: &Instruction) -> Result { + let data = expect_global_name(inst)?; + ctx.module + .data + .global_names + .get(data.as_raw() as usize) + .cloned() + .ok_or_else(|| CodegenIrError::missing_entry("global name", data.as_raw())) +} + +/// Loads an eval-scope handle operand into the requested ABI argument register. +fn load_eval_scope_operand_to_arg( + ctx: &mut FunctionContext<'_>, + scope: ValueId, + arg_index: usize, +) -> Result<()> { + let arg = abi::int_arg_reg_name(ctx.emitter.target, arg_index); + let ty = ctx.load_value_to_reg(scope, arg)?.codegen_repr(); + if ty == PhpType::Int { + return Ok(()); + } + Err(CodegenIrError::unsupported(format!( + "eval scope handle operand for PHP type {:?}", + ty + ))) +} + +/// Calls `__elephc_eval_scope_get` using an already-loaded scope handle arg. +fn emit_eval_scope_get_for_loaded_scope( + ctx: &mut FunctionContext<'_>, + name: &str, + out_cell_offset: usize, + out_flags_offset: usize, +) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, out_cell_offset); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, out_flags_offset); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Calls `__elephc_eval_scope_set` using an already-loaded scope handle arg. +fn emit_eval_scope_set_for_loaded_scope(ctx: &mut FunctionContext<'_>, name: &str, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); +} + +/// Parses an `EvalLiteralCall` payload into the conservative scalar AOT subset. +fn eval_literal_aot_program( + ctx: &FunctionContext<'_>, + inst: &Instruction, +) -> Result> { + let Some(fragment) = eval_literal_fragment(ctx, inst)? else { + return Ok(None); + }; + Ok(parse_eval_literal_aot_program(&fragment)) +} + +/// Returns the literal fragment attached to an `EvalLiteralCall`, if this is one. +fn eval_literal_fragment(ctx: &FunctionContext<'_>, inst: &Instruction) -> Result> { + if inst.op != Op::EvalLiteralCall { + return Ok(None); + } + let Some(Immediate::Data(data)) = inst.immediate else { + return Ok(None); + }; + let fragment = ctx + .module + .data + .strings + .get(data.as_raw() as usize) + .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw()))?; + Ok(Some(fragment.clone())) +} + +/// Parses a PHP eval fragment and accepts only side-effect-free scalar statements plus scalar stores. +fn parse_eval_literal_aot_program(fragment: &str) -> Option { + let program = crate::eval_aot::parse_literal_fragment(fragment)?; + if let Some(local_scalar) = parse_eval_local_scalar_aot_program(&program) { + return Some(EvalLiteralAotProgram::LocalScalar(local_scalar)); + } + if let Some(boxed) = parse_eval_literal_boxed_aot_program(&program) { + return Some(EvalLiteralAotProgram::Boxed(boxed)); + } + None +} + +/// Parses a PHP eval fragment into the boxed-Mixed AOT subset. +fn parse_eval_literal_boxed_aot_program(program: &[Stmt]) -> Option { + let mut instructions = Vec::new(); + let mut terminated = false; + for stmt in program { + if terminated { + break; + } + terminated = push_eval_literal_aot_stmt(stmt, &mut instructions)?; + } + if !terminated { + instructions.push(EvalLiteralAotInst::Return(EvalLiteralAotExpr::Scalar( + EvalLiteralAotScalar::Null, + ))); + } + let has_scope_writes = instructions + .iter() + .any(|inst| matches!(inst, EvalLiteralAotInst::Store { .. })); + let has_scope_access = instructions + .iter() + .any(EvalLiteralAotInst::has_scope_access); + let scope_reads = instructions + .iter() + .flat_map(EvalLiteralAotInst::scope_reads) + .collect(); + let scope_writes = instructions + .iter() + .filter_map(EvalLiteralAotInst::scope_write) + .collect(); + Some(EvalLiteralBoxedAotProgram { + instructions, + scope_reads, + scope_writes, + has_scope_writes, + has_scope_access, + }) +} + +/// Parses a PHP eval fragment into the local int/bool control-flow AOT subset. +fn parse_eval_local_scalar_aot_program(program: &[Stmt]) -> Option { + let mut analysis = EvalLocalScalarAnalysis { + locals: BTreeMap::new(), + local_types: BTreeMap::new(), + max_scratch_slots: 0, + }; + let mut assigned = BTreeSet::new(); + let statements = parse_eval_local_scalar_block(program, &mut analysis, &mut assigned, 0, true)?; + Some(EvalLocalScalarAotProgram { + statements, + locals: analysis.locals, + local_types: analysis.local_types, + scratch_slots: analysis.max_scratch_slots.max(1), + }) +} + +/// Parses a statement block for the local scalar AOT subset. +fn parse_eval_local_scalar_block( + statements: &[Stmt], + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + loop_depth: usize, + allow_type_changes: bool, +) -> Option> { + let mut out = Vec::new(); + let mut terminated = false; + for stmt in statements { + if terminated { + break; + } + let (local_stmt, terminates_block) = + parse_eval_local_scalar_stmt(stmt, analysis, assigned, loop_depth, allow_type_changes)?; + out.push(local_stmt); + terminated = terminates_block; + } + Some(out) +} + +/// Parses one statement for the local scalar AOT subset. +fn parse_eval_local_scalar_stmt( + stmt: &Stmt, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + loop_depth: usize, + allow_type_changes: bool, +) -> Option<(EvalLocalScalarStmt, bool)> { + match &stmt.kind { + StmtKind::Echo(expr) => { + let expr = eval_local_scalar_echo_expr(expr, analysis, assigned)?; + Some((EvalLocalScalarStmt::Echo(expr), false)) + } + StmtKind::Assign { name, value } => { + let value = eval_local_scalar_value_expr(value, analysis, assigned)?; + analysis.ensure_local(name, value.ty, allow_type_changes)?; + assigned.insert(name.clone()); + Some(( + EvalLocalScalarStmt::Store { + name: name.clone(), + value, + }, + false, + )) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + let mut branches = Vec::new(); + let condition = eval_local_scalar_condition_expr(condition, analysis, assigned)?; + let mut then_assigned = assigned.clone(); + let then_body = parse_eval_local_scalar_block( + then_body, + analysis, + &mut then_assigned, + loop_depth, + false, + )?; + branches.push((condition, then_body)); + for (elseif_condition, elseif_body) in elseif_clauses { + let condition = + eval_local_scalar_condition_expr(elseif_condition, analysis, assigned)?; + let mut elseif_assigned = assigned.clone(); + let body = parse_eval_local_scalar_block( + elseif_body, + analysis, + &mut elseif_assigned, + loop_depth, + false, + )?; + branches.push((condition, body)); + } + let else_body = if let Some(else_body) = else_body { + let mut else_assigned = assigned.clone(); + parse_eval_local_scalar_block( + else_body, + analysis, + &mut else_assigned, + loop_depth, + false, + )? + } else { + Vec::new() + }; + Some(( + EvalLocalScalarStmt::If { + branches, + else_body, + }, + false, + )) + } + StmtKind::While { condition, body } => { + let condition = eval_local_scalar_condition_expr(condition, analysis, assigned)?; + let mut body_assigned = assigned.clone(); + let body = parse_eval_local_scalar_block( + body, + analysis, + &mut body_assigned, + loop_depth + 1, + false, + )?; + Some((EvalLocalScalarStmt::While { condition, body }, false)) + } + StmtKind::DoWhile { body, condition } => { + let mut body_assigned = assigned.clone(); + let body = parse_eval_local_scalar_block( + body, + analysis, + &mut body_assigned, + loop_depth + 1, + false, + )?; + let condition = eval_local_scalar_condition_expr(condition, analysis, &body_assigned)?; + Some((EvalLocalScalarStmt::DoWhile { body, condition }, false)) + } + StmtKind::For { + init, + condition, + update, + body, + } => { + let init = if let Some(init) = init.as_deref() { + Some(Box::new(parse_eval_local_scalar_for_clause( + init, + analysis, + assigned, + allow_type_changes, + )?)) + } else { + None + }; + let condition = match condition { + Some(condition) => Some(eval_local_scalar_condition_expr( + condition, analysis, assigned, + )?), + None => None, + }; + let mut body_assigned = assigned.clone(); + let body = parse_eval_local_scalar_block( + body, + analysis, + &mut body_assigned, + loop_depth + 1, + false, + )?; + let update = if let Some(update) = update.as_deref() { + Some(Box::new(parse_eval_local_scalar_for_clause( + update, + analysis, + &mut body_assigned, + false, + )?)) + } else { + None + }; + Some(( + EvalLocalScalarStmt::For { + init, + condition, + update, + body, + }, + false, + )) + } + StmtKind::Switch { + subject, + cases, + default, + } => parse_eval_local_scalar_switch_stmt( + subject, + cases, + default.as_deref(), + analysis, + assigned, + loop_depth, + ) + .map(|stmt| (stmt, false)), + StmtKind::Break(level) if *level > 0 && *level <= loop_depth => { + Some((EvalLocalScalarStmt::Break(*level), true)) + } + StmtKind::Continue(level) if *level > 0 && *level <= loop_depth => { + Some((EvalLocalScalarStmt::Continue(*level), true)) + } + StmtKind::Return(Some(expr)) => { + let expr = eval_local_scalar_value_expr(expr, analysis, assigned)?; + Some((EvalLocalScalarStmt::Return(Some(expr)), true)) + } + StmtKind::Return(None) => Some((EvalLocalScalarStmt::Return(None), true)), + StmtKind::ExprStmt(expr) => { + parse_eval_local_scalar_expr_stmt(expr, analysis, assigned, allow_type_changes) + } + _ => None, + } +} + +/// Parses a switch statement accepted by the local scalar AOT subset. +fn parse_eval_local_scalar_switch_stmt( + subject: &Expr, + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + loop_depth: usize, +) -> Option { + let default_index = eval_local_scalar_switch_default_index(cases, default)?; + let subject = eval_local_scalar_condition_expr(subject, analysis, assigned)?; + let mut parsed_cases = Vec::new(); + for (conditions, body) in cases { + let conditions = conditions + .iter() + .map(|condition| eval_local_scalar_condition_expr(condition, analysis, assigned)) + .collect::>>()?; + conditions + .iter() + .all(|condition| condition.ty == subject.ty) + .then_some(())?; + let mut case_assigned = assigned.clone(); + let body = parse_eval_local_scalar_switch_block( + body, + analysis, + &mut case_assigned, + loop_depth + 1, + false, + )?; + parsed_cases.push((conditions, body)); + } + let default = if let Some(default) = default { + let mut default_assigned = assigned.clone(); + parse_eval_local_scalar_switch_block( + default, + analysis, + &mut default_assigned, + loop_depth + 1, + false, + )? + } else { + Vec::new() + }; + let subject_slots = 1 + subject.scratch_slots(); + let condition_slots = parsed_cases + .iter() + .flat_map(|(conditions, _)| conditions) + .map(|condition| 1 + condition.scratch_slots()) + .max() + .unwrap_or(1); + analysis.max_scratch_slots = analysis + .max_scratch_slots + .max(subject_slots) + .max(condition_slots); + Some(EvalLocalScalarStmt::Switch { + subject, + cases: parsed_cases, + default, + default_index, + }) +} + +/// Parses one switch case/default body for the local scalar AOT subset. +fn parse_eval_local_scalar_switch_block( + statements: &[Stmt], + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + loop_depth: usize, + allow_type_changes: bool, +) -> Option> { + parse_eval_local_scalar_block( + statements, + analysis, + assigned, + loop_depth, + allow_type_changes, + ) +} + +/// Returns the source-order insertion point for a switch default body. +fn eval_local_scalar_switch_default_index( + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, +) -> Option> { + let Some(default) = default else { + return Some(None); + }; + if cases.is_empty() { + return Some(Some(0)); + } + let Some(default_start) = default.first().map(|stmt| stmt.span) else { + return None; + }; + if default_start == crate::span::Span::dummy() { + return None; + } + let mut default_index = 0; + for (conditions, _) in cases { + let case_start = conditions.first()?.span; + if case_start == crate::span::Span::dummy() { + return None; + } + if eval_local_scalar_span_is_before(case_start, default_start) { + default_index += 1; + } + } + Some(Some(default_index)) +} + +/// Returns true when `span` appears before `pivot` in the same eval fragment. +fn eval_local_scalar_span_is_before(span: crate::span::Span, pivot: crate::span::Span) -> bool { + span.line < pivot.line || (span.line == pivot.line && span.col < pivot.col) +} + +/// Parses one inline `for` init/update statement for local scalar AOT. +fn parse_eval_local_scalar_for_clause( + stmt: &Stmt, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + allow_type_changes: bool, +) -> Option { + match &stmt.kind { + StmtKind::Assign { .. } | StmtKind::ExprStmt(_) => { + let (stmt, terminates) = + parse_eval_local_scalar_stmt(stmt, analysis, assigned, 0, allow_type_changes)?; + (!terminates).then_some(stmt) + } + _ => None, + } +} + +/// Parses expression statements accepted by the local scalar AOT subset. +fn parse_eval_local_scalar_expr_stmt( + expr: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &mut BTreeSet, + allow_type_changes: bool, +) -> Option<(EvalLocalScalarStmt, bool)> { + match &expr.kind { + ExprKind::Print(inner) => { + let expr = eval_local_scalar_echo_expr(inner, analysis, assigned)?; + Some((EvalLocalScalarStmt::Echo(expr), false)) + } + ExprKind::Assignment { + target, + value, + prelude, + conditional_value_temp, + .. + } if prelude.is_empty() && conditional_value_temp.is_none() => { + let ExprKind::Variable(name) = &target.kind else { + return None; + }; + let value = eval_local_scalar_value_expr(value, analysis, assigned)?; + analysis.ensure_local(name, value.ty, allow_type_changes)?; + assigned.insert(name.clone()); + Some(( + EvalLocalScalarStmt::Store { + name: name.clone(), + value, + }, + false, + )) + } + ExprKind::PreIncrement(name) | ExprKind::PostIncrement(name) => { + let value = eval_local_scalar_inc_dec_expr(name, true, analysis, assigned)?; + Some(( + EvalLocalScalarStmt::Store { + name: name.clone(), + value, + }, + false, + )) + } + ExprKind::PreDecrement(name) | ExprKind::PostDecrement(name) => { + let value = eval_local_scalar_inc_dec_expr(name, false, analysis, assigned)?; + Some(( + EvalLocalScalarStmt::Store { + name: name.clone(), + value, + }, + false, + )) + } + _ => { + let _ = eval_local_scalar_value_expr(expr, analysis, assigned)?; + Some((EvalLocalScalarStmt::Noop, false)) + } + } +} + +/// Builds an increment/decrement assignment value for local scalar expression statements. +fn eval_local_scalar_inc_dec_expr( + name: &str, + increment: bool, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + if !assigned.contains(name) || analysis.local_types.get(name) != Some(&EvalLocalScalarType::Int) + { + return None; + } + let op = if increment { + EvalLocalScalarBinaryOp::Add + } else { + EvalLocalScalarBinaryOp::Sub + }; + let expr = EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Binary { + op, + left: Box::new(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::LoadVar(name.to_string()), + ty: EvalLocalScalarType::Int, + }), + right: Box::new(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Int(1), + ty: EvalLocalScalarType::Int, + }), + }, + ty: EvalLocalScalarType::Int, + }; + analysis.record_expr(&expr); + Some(expr) +} + +/// Parses a condition expression accepted by local scalar AOT control flow. +fn eval_local_scalar_condition_expr( + expr: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let expr = eval_local_scalar_value_expr(expr, analysis, assigned)?; + matches!( + expr.ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) + .then_some(expr) +} + +/// Parses an echo expression accepted by the local scalar AOT subset. +fn eval_local_scalar_echo_expr( + expr: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let parsed = match &expr.kind { + ExprKind::StringLiteral(value) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::String(value.clone()), + ty: EvalLocalScalarType::String, + }, + ExprKind::BinaryOp { left, op, right } if *op == BinOp::Concat => { + let left = eval_local_scalar_echo_expr(left, analysis, assigned)?; + let right = eval_local_scalar_echo_expr(right, analysis, assigned)?; + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Binary { + op: EvalLocalScalarBinaryOp::Concat, + left: Box::new(left), + right: Box::new(right), + }, + ty: EvalLocalScalarType::String, + } + } + _ => eval_local_scalar_value_expr(expr, analysis, assigned)?, + }; + analysis.record_expr(&parsed); + Some(parsed) +} + +/// Parses an int/bool value expression accepted by the local scalar AOT subset. +fn eval_local_scalar_value_expr( + expr: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let parsed = match &expr.kind { + ExprKind::Null => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Null, + ty: EvalLocalScalarType::Null, + }, + ExprKind::IntLiteral(value) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Int(*value), + ty: EvalLocalScalarType::Int, + }, + ExprKind::FloatLiteral(value) if value.is_finite() => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Float(*value), + ty: EvalLocalScalarType::Float, + }, + ExprKind::BoolLiteral(value) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Bool(*value), + ty: EvalLocalScalarType::Bool, + }, + ExprKind::StringLiteral(value) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::String(value.clone()), + ty: EvalLocalScalarType::String, + }, + ExprKind::Variable(name) if assigned.contains(name) => EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::LoadVar(name.clone()), + ty: *analysis.local_types.get(name)?, + }, + ExprKind::Negate(inner) => { + let inner = eval_local_scalar_value_expr(inner, analysis, assigned)?; + if !matches!( + inner.ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Float + ) { + return None; + } + let ty = inner.ty; + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Negate(Box::new(inner)), + ty, + } + } + ExprKind::BitNot(inner) => { + let inner = eval_local_scalar_value_expr(inner, analysis, assigned)?; + if inner.ty != EvalLocalScalarType::Int { + return None; + } + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::BitNot(Box::new(inner)), + ty: EvalLocalScalarType::Int, + } + } + ExprKind::Not(inner) => { + let inner = eval_local_scalar_condition_expr(inner, analysis, assigned)?; + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Not(Box::new(inner)), + ty: EvalLocalScalarType::Bool, + } + } + ExprKind::ErrorSuppress(inner) => eval_local_scalar_value_expr(inner, analysis, assigned)?, + ExprKind::Print(inner) => { + let inner = eval_local_scalar_echo_expr(inner, analysis, assigned)?; + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Print(Box::new(inner)), + ty: EvalLocalScalarType::Int, + } + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + let condition = eval_local_scalar_condition_expr(condition, analysis, assigned)?; + let then_expr = eval_local_scalar_value_expr(then_expr, analysis, assigned)?; + let else_expr = eval_local_scalar_value_expr(else_expr, analysis, assigned)?; + if then_expr.ty != else_expr.ty { + return None; + } + EvalLocalScalarExpr { + ty: then_expr.ty, + kind: EvalLocalScalarExprKind::Ternary { + condition: Box::new(condition), + then_expr: Box::new(then_expr), + else_expr: Box::new(else_expr), + }, + } + } + ExprKind::BinaryOp { left, op, right } => { + eval_local_scalar_binary_expr(left, op, right, analysis, assigned)? + } + ExprKind::FunctionCall { name, args } => { + eval_local_scalar_call_expr(name, args, analysis, assigned)? + } + _ => return None, + }; + analysis.record_expr(&parsed); + Some(parsed) +} + +/// Parses a static call accepted by the local scalar AOT subset. +fn eval_local_scalar_call_expr( + name: &crate::names::Name, + args: &[Expr], + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let short_name = name.as_str().trim_start_matches('\\'); + if let Some(expr) = eval_local_scalar_construct_call_expr(short_name, args, analysis, assigned) + { + return Some(expr); + } + if let Some(value) = crate::eval_aot::fold_static_builtin_int_call(short_name, args) { + return Some(eval_local_scalar_int_literal(value)); + } + let args = args + .iter() + .map(|arg| eval_local_scalar_value_expr(arg, analysis, assigned)) + .collect::>>()?; + Some(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::StaticFunctionCall { + name: name.as_str().to_string(), + args, + }, + ty: EvalLocalScalarType::Int, + }) +} + +/// Parses local-scalar language constructs that behave like expressions. +fn eval_local_scalar_construct_call_expr( + name: &str, + args: &[Expr], + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + if args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::NamedArg { .. } | ExprKind::Spread(_))) + { + return None; + } + match php_symbol_key(name).as_str() { + "isset" => { + if args.is_empty() { + return None; + } + let names = args + .iter() + .map(|arg| match &arg.kind { + ExprKind::Variable(name) if assigned.contains(name) => Some(name.clone()), + _ => None, + }) + .collect::>>()?; + Some(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Isset(names), + ty: EvalLocalScalarType::Bool, + }) + } + "empty" if args.len() == 1 => { + let ExprKind::Variable(name) = &args[0].kind else { + return None; + }; + if !assigned.contains(name) || !analysis.local_types.contains_key(name) { + return None; + } + Some(EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::EmptyVar(name.clone()), + ty: EvalLocalScalarType::Bool, + }) + } + _ => None, + } +} + +/// Constructs a local-scalar integer literal expression. +fn eval_local_scalar_int_literal(value: i64) -> EvalLocalScalarExpr { + EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Int(value), + ty: EvalLocalScalarType::Int, + } +} + +/// Parses a binary int/bool expression accepted by the local scalar AOT subset. +fn eval_local_scalar_binary_expr( + left: &Expr, + op: &BinOp, + right: &Expr, + analysis: &mut EvalLocalScalarAnalysis, + assigned: &BTreeSet, +) -> Option { + let left = eval_local_scalar_value_expr(left, analysis, assigned)?; + let right = eval_local_scalar_value_expr(right, analysis, assigned)?; + let (op, ty) = EvalLocalScalarBinaryOp::from_value_binop(op, left.ty, right.ty)?; + let expr = EvalLocalScalarExpr { + kind: EvalLocalScalarExprKind::Binary { + op, + left: Box::new(left), + right: Box::new(right), + }, + ty, + }; + analysis.record_expr(&expr); + Some(expr) +} + +/// Appends AOT instructions for one eligible eval-fragment statement. +fn push_eval_literal_aot_stmt( + stmt: &Stmt, + instructions: &mut Vec, +) -> Option { + match &stmt.kind { + StmtKind::Echo(expr) => { + instructions.push(EvalLiteralAotInst::Echo(eval_literal_aot_expr(expr)?)); + Some(false) + } + StmtKind::Assign { name, value } => { + instructions.push(EvalLiteralAotInst::Store { + name: name.clone(), + value: eval_literal_aot_expr(value)?, + }); + Some(false) + } + StmtKind::Return(Some(expr)) => { + instructions.push(EvalLiteralAotInst::Return(eval_literal_aot_expr(expr)?)); + Some(true) + } + StmtKind::Return(None) => { + instructions.push(EvalLiteralAotInst::Return(EvalLiteralAotExpr::Scalar( + EvalLiteralAotScalar::Null, + ))); + Some(true) + } + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::Print(inner) => { + instructions.push(EvalLiteralAotInst::Echo(eval_literal_aot_expr(inner)?)); + Some(false) + } + _ => { + let _ = eval_literal_aot_scalar_expr(expr)?; + Some(false) + } + }, + _ => None, + } +} + +/// Builds a boxed-Mixed AOT expression, folding pure scalar expressions when possible. +fn eval_literal_aot_expr(expr: &Expr) -> Option { + if let Some(value) = eval_literal_aot_scalar_expr(expr) { + return Some(EvalLiteralAotExpr::Scalar(value)); + } + match &expr.kind { + ExprKind::Variable(name) => Some(EvalLiteralAotExpr::LoadVar(name.clone())), + ExprKind::BinaryOp { left, op, right } => Some(EvalLiteralAotExpr::Binary { + op: EvalLiteralAotBinaryOp::from_binop(op)?, + left: Box::new(eval_literal_aot_expr(left)?), + right: Box::new(eval_literal_aot_expr(right)?), + }), + _ => None, + } +} + +/// Evaluates a scalar-only expression at compile time for the literal eval AOT subset. +fn eval_literal_aot_scalar_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::Null => Some(EvalLiteralAotScalar::Null), + ExprKind::BoolLiteral(value) => Some(EvalLiteralAotScalar::Bool(*value)), + ExprKind::IntLiteral(value) => Some(EvalLiteralAotScalar::Int(*value)), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(EvalLiteralAotScalar::Float(*value)) + } + ExprKind::StringLiteral(value) => Some(EvalLiteralAotScalar::String(value.clone())), + ExprKind::Negate(inner) => eval_literal_aot_negate(inner), + ExprKind::Not(inner) => { + let value = eval_literal_aot_scalar_expr(inner)?; + Some(EvalLiteralAotScalar::Bool(!value.truthy())) + } + ExprKind::BinaryOp { left, op, right } => { + let left = eval_literal_aot_scalar_expr(left)?; + let right = eval_literal_aot_scalar_expr(right)?; + eval_literal_aot_binary(&left, op, &right) + } + _ => None, + } +} + +/// Applies unary minus for scalar integer and float literals. +fn eval_literal_aot_negate(expr: &Expr) -> Option { + match eval_literal_aot_scalar_expr(expr)? { + EvalLiteralAotScalar::Int(value) => value.checked_neg().map(EvalLiteralAotScalar::Int), + EvalLiteralAotScalar::Float(value) => Some(EvalLiteralAotScalar::Float(-value)), + _ => None, + } +} + +/// Applies a safe scalar binary operation for the literal eval AOT subset. +fn eval_literal_aot_binary( + left: &EvalLiteralAotScalar, + op: &BinOp, + right: &EvalLiteralAotScalar, +) -> Option { + match op { + BinOp::Add => eval_literal_aot_int_binop(left, right, i64::checked_add), + BinOp::Sub => eval_literal_aot_int_binop(left, right, i64::checked_sub), + BinOp::Mul => eval_literal_aot_int_binop(left, right, i64::checked_mul), + BinOp::Mod => match (left, right) { + (EvalLiteralAotScalar::Int(_), EvalLiteralAotScalar::Int(0)) => None, + (EvalLiteralAotScalar::Int(left), EvalLiteralAotScalar::Int(right)) => { + left.checked_rem(*right).map(EvalLiteralAotScalar::Int) + } + _ => None, + }, + BinOp::Concat => Some(EvalLiteralAotScalar::String(format!( + "{}{}", + left.as_php_string()?, + right.as_php_string()? + ))), + _ => None, + } +} + +/// Applies an integer-only checked binary operation. +fn eval_literal_aot_int_binop( + left: &EvalLiteralAotScalar, + right: &EvalLiteralAotScalar, + op: fn(i64, i64) -> Option, +) -> Option { + match (left, right) { + (EvalLiteralAotScalar::Int(left), EvalLiteralAotScalar::Int(right)) => { + op(*left, *right).map(EvalLiteralAotScalar::Int) + } + _ => None, + } +} + +impl EvalLiteralAotInst { + /// Returns true when this AOT instruction needs eval scope setup. + fn has_scope_access(&self) -> bool { + match self { + Self::Echo(expr) | Self::Return(expr) => expr.has_scope_access(), + Self::Store { .. } => true, + } + } + + /// Returns variable names read from eval scope while executing this instruction. + fn scope_reads(&self) -> BTreeSet { + let mut reads = BTreeSet::new(); + match self { + Self::Echo(expr) | Self::Return(expr) => expr.collect_scope_reads(&mut reads), + Self::Store { value, .. } => value.collect_scope_reads(&mut reads), + } + reads + } + + /// Returns the eval scope variable written by this instruction, if any. + fn scope_write(&self) -> Option { + match self { + Self::Store { name, .. } => Some(name.clone()), + Self::Echo(_) | Self::Return(_) => None, + } + } +} + +impl EvalLiteralAotExpr { + /// Returns true when this expression reads a variable from the eval scope. + fn has_scope_access(&self) -> bool { + match self { + Self::Scalar(_) => false, + Self::LoadVar(_) => true, + Self::Binary { left, right, .. } => left.has_scope_access() || right.has_scope_access(), + } + } + + /// Adds all variable names read from eval scope by this expression. + fn collect_scope_reads(&self, reads: &mut BTreeSet) { + match self { + Self::Scalar(_) => {} + Self::LoadVar(name) => { + reads.insert(name.clone()); + } + Self::Binary { left, right, .. } => { + left.collect_scope_reads(reads); + right.collect_scope_reads(reads); + } + } + } +} + +impl EvalLiteralAotBinaryOp { + /// Maps an AST operator to the boxed-Mixed helper used by literal eval AOT. + fn from_binop(op: &BinOp) -> Option { + match op { + BinOp::Add => Some(Self::Add), + BinOp::Sub => Some(Self::Sub), + BinOp::Mul => Some(Self::Mul), + BinOp::Div => Some(Self::Div), + BinOp::Mod => Some(Self::Mod), + BinOp::Concat => Some(Self::Concat), + _ => None, + } + } + + /// Returns the runtime helper symbol for this boxed-Mixed binary operation. + fn helper(&self) -> &'static str { + match self { + Self::Add => "__elephc_eval_value_add", + Self::Sub => "__elephc_eval_value_sub", + Self::Mul => "__elephc_eval_value_mul", + Self::Div => "__elephc_eval_value_div", + Self::Mod => "__elephc_eval_value_mod", + Self::Concat => "__elephc_eval_value_concat", + } + } +} + +impl EvalLiteralAotScalar { + /// Returns PHP truthiness for the scalar forms accepted by the AOT subset. + fn truthy(&self) -> bool { + match self { + Self::Null => false, + Self::Bool(value) => *value, + Self::Int(value) => *value != 0, + Self::Float(value) => *value != 0.0, + Self::String(value) => !value.is_empty() && value != "0", + } + } + + /// Converts scalar forms to the PHP string form safe for compile-time concat. + fn as_php_string(&self) -> Option { + match self { + Self::Null => Some(String::new()), + Self::Bool(false) => Some(String::new()), + Self::Bool(true) => Some("1".to_string()), + Self::Int(value) => Some(value.to_string()), + Self::String(value) => Some(value.clone()), + Self::Float(_) => None, + } + } +} + +impl EvalLocalScalarAnalysis { + /// Registers or validates a local variable slot and its current scalar type. + fn ensure_local( + &mut self, + name: &str, + ty: EvalLocalScalarType, + allow_type_change: bool, + ) -> Option<()> { + if let Some(existing) = self.local_types.get(name) { + if *existing != ty { + allow_type_change.then_some(())?; + self.local_types.insert(name.to_string(), ty); + } + } else { + self.local_types.insert(name.to_string(), ty); + } + if !self.locals.contains_key(name) { + let slot = self.locals.len(); + self.locals.insert(name.to_string(), slot); + } + Some(()) + } + + /// Records scratch-slot demand for a parsed local scalar expression. + fn record_expr(&mut self, expr: &EvalLocalScalarExpr) { + self.max_scratch_slots = self.max_scratch_slots.max(expr.scratch_slots()); + } +} + +impl EvalLocalScalarAotProgram { + /// Returns the byte offset of a local scalar value slot from the reserved eval stack base. + fn value_offset(&self, name: &str) -> usize { + EVAL_STACK_BYTES + self.locals[name] * EVAL_LOCAL_SCALAR_SLOT_BYTES + } + + /// Returns the byte offset of a local scalar secondary payload word. + fn value_aux_offset(&self, name: &str) -> usize { + self.value_offset(name) + 8 + } + + /// Returns the byte offset of a local scalar defined-flag slot from the eval stack base. + fn defined_offset(&self, name: &str) -> usize { + self.value_offset(name) + 16 + } + + /// Returns the byte offset for a recursive expression scratch slot. + fn scratch_offset(&self, depth: usize) -> usize { + EVAL_STACK_BYTES + self.locals.len() * EVAL_LOCAL_SCALAR_SLOT_BYTES + depth * 8 + } + + /// Returns the byte offset used to preserve the eval return cell across scope flushes. + fn result_cell_offset(&self) -> usize { + self.scratch_offset(self.scratch_slots) + } + + /// Returns the total temporary stack bytes needed by this local AOT fragment. + fn stack_bytes(&self) -> usize { + align_to_16(self.result_cell_offset() + 8) + } + + /// Returns the scalar type tracked for a local name. + fn local_type(&self, name: &str) -> EvalLocalScalarType { + self.local_types[name] + } +} + +impl EvalLocalScalarExpr { + /// Returns how many fixed scratch slots this expression can need during lowering. + fn scratch_slots(&self) -> usize { + match &self.kind { + EvalLocalScalarExprKind::Null + | EvalLocalScalarExprKind::Int(_) + | EvalLocalScalarExprKind::Float(_) + | EvalLocalScalarExprKind::Bool(_) + | EvalLocalScalarExprKind::String(_) + | EvalLocalScalarExprKind::LoadVar(_) + | EvalLocalScalarExprKind::Isset(_) + | EvalLocalScalarExprKind::EmptyVar(_) => 0, + EvalLocalScalarExprKind::Negate(inner) + | EvalLocalScalarExprKind::BitNot(inner) + | EvalLocalScalarExprKind::Not(inner) + | EvalLocalScalarExprKind::Print(inner) => inner.scratch_slots(), + EvalLocalScalarExprKind::Ternary { + condition, + then_expr, + else_expr, + } => condition + .scratch_slots() + .max(then_expr.scratch_slots()) + .max(else_expr.scratch_slots()), + EvalLocalScalarExprKind::Binary { op, left, right } => { + if matches!( + op, + EvalLocalScalarBinaryOp::And | EvalLocalScalarBinaryOp::Or + ) { + left.scratch_slots().max(right.scratch_slots()) + } else if matches!(op, EvalLocalScalarBinaryOp::Concat) { + left.scratch_slots().max(right.scratch_slots()) + } else { + left.scratch_slots().max(1 + right.scratch_slots()).max(1) + } + } + EvalLocalScalarExprKind::StaticFunctionCall { args, .. } => { + let arg_slots = args.len(); + args.iter() + .map(EvalLocalScalarExpr::scratch_slots) + .max() + .unwrap_or(0) + + arg_slots + } + } + } +} + +impl EvalLocalScalarType { + /// Returns true when the type can participate in numeric local-scalar operations. + fn is_numeric(self) -> bool { + matches!(self, Self::Int | Self::Float) + } + + /// Maps the local scalar type to the nearest PHP codegen representation. + fn php_type(self) -> PhpType { + match self { + Self::Null => PhpType::Void, + Self::Int => PhpType::Int, + Self::Float => PhpType::Float, + Self::Bool => PhpType::Bool, + Self::String => PhpType::Str, + } + } +} + +impl EvalLocalScalarBinaryOp { + /// Maps an AST binary operator and operand types into the local scalar AOT subset. + fn from_value_binop( + op: &BinOp, + left_ty: EvalLocalScalarType, + right_ty: EvalLocalScalarType, + ) -> Option<(Self, EvalLocalScalarType)> { + match op { + BinOp::Add + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Add, EvalLocalScalarType::Int)) + } + BinOp::Sub + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Sub, EvalLocalScalarType::Int)) + } + BinOp::Mul + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Mul, EvalLocalScalarType::Int)) + } + BinOp::Div if left_ty.is_numeric() && right_ty.is_numeric() => { + Some((Self::Div, EvalLocalScalarType::Float)) + } + BinOp::Mod if left_ty.is_numeric() && right_ty.is_numeric() => { + Some((Self::Mod, EvalLocalScalarType::Int)) + } + BinOp::BitAnd + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::BitAnd, EvalLocalScalarType::Int)) + } + BinOp::BitOr + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::BitOr, EvalLocalScalarType::Int)) + } + BinOp::BitXor + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::BitXor, EvalLocalScalarType::Int)) + } + BinOp::ShiftLeft + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::ShiftLeft, EvalLocalScalarType::Int)) + } + BinOp::ShiftRight + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::ShiftRight, EvalLocalScalarType::Int)) + } + BinOp::Lt + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Lt, EvalLocalScalarType::Bool)) + } + BinOp::Gt + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::Gt, EvalLocalScalarType::Bool)) + } + BinOp::LtEq + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::LtEq, EvalLocalScalarType::Bool)) + } + BinOp::GtEq + if left_ty == EvalLocalScalarType::Int && right_ty == EvalLocalScalarType::Int => + { + Some((Self::GtEq, EvalLocalScalarType::Bool)) + } + BinOp::Eq + if left_ty == right_ty + && matches!( + left_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) => + { + Some((Self::Eq, EvalLocalScalarType::Bool)) + } + BinOp::NotEq + if left_ty == right_ty + && matches!( + left_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) => + { + Some((Self::NotEq, EvalLocalScalarType::Bool)) + } + BinOp::And + if matches!( + left_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) && matches!( + right_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) => + { + Some((Self::And, EvalLocalScalarType::Bool)) + } + BinOp::Or + if matches!( + left_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) && matches!( + right_ty, + EvalLocalScalarType::Int | EvalLocalScalarType::Bool + ) => + { + Some((Self::Or, EvalLocalScalarType::Bool)) + } + _ => None, + } + } +} + +/// Rounds a byte count up to the next 16-byte stack-aligned size. +fn align_to_16(value: usize) -> usize { + (value + 15) & !15 +} + +/// Lowers an eligible literal eval fragment directly, returning false when codegen must fall back. +fn lower_eval_literal_aot( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLiteralAotProgram, +) -> Result { + match program { + EvalLiteralAotProgram::Boxed(program) => lower_eval_literal_boxed_aot(ctx, inst, program), + EvalLiteralAotProgram::LocalScalar(program) => { + lower_eval_local_scalar_aot(ctx, inst, program) + } + } +} + +/// Lowers an eligible boxed-Mixed literal eval fragment directly. +fn lower_eval_literal_boxed_aot( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLiteralBoxedAotProgram, +) -> Result { + if let Some(targets) = eval_literal_boxed_direct_local_store_targets(ctx, program)? { + lower_eval_literal_boxed_aot_direct_local_stores(ctx, inst, program, &targets)?; + return Ok(true); + } + if let Some(targets) = eval_literal_boxed_direct_read_write_targets(ctx, program)? { + lower_eval_literal_boxed_aot_direct_read_writes(ctx, inst, program, &targets)?; + return Ok(true); + } + if program.has_scope_writes && !eval_global_aliases(ctx).is_empty() { + ctx.emitter.comment( + "eval literal AOT fallback: scope writes with global aliases need bridge semantics", + ); + return Ok(false); + } + + ctx.emitter.comment(&format!( + "eval literal AOT compiled ({} ops)", + program.instructions.len() + )); + if program.has_scope_access { + abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + ensure_eval_scope(ctx)?; + ensure_eval_global_scope(ctx)?; + let flush_names = program + .scope_reads + .union(&program.scope_writes) + .cloned() + .collect::>(); + let sync_locals = eval_sync_locals(ctx); + let sync_globals = eval_sync_globals(ctx); + let flush_locals = filter_eval_sync_locals_by_name(sync_locals.clone(), &flush_names); + let flush_globals = filter_eval_sync_globals_by_name(sync_globals.clone(), &flush_names); + let reload_locals = filter_eval_sync_locals_by_name(sync_locals, &program.scope_writes); + let reload_globals = filter_eval_sync_globals_by_name(sync_globals, &program.scope_writes); + flush_eval_scope_locals(ctx, &flush_locals)?; + flush_eval_global_scope(ctx, &flush_globals)?; + for aot_inst in &program.instructions { + match aot_inst { + EvalLiteralAotInst::Echo(value) => emit_eval_literal_aot_echo(ctx, value, 0), + EvalLiteralAotInst::Store { name, value } => { + emit_eval_literal_aot_scope_store(ctx, name, value, 0)?; + } + EvalLiteralAotInst::Return(value) => { + emit_eval_literal_aot_expr_cell(ctx, value, 0); + break; + } + } + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + reload_eval_scope_locals(ctx, &reload_locals)?; + reload_eval_global_scope(ctx, &reload_globals)?; + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); + } else { + for aot_inst in &program.instructions { + match aot_inst { + EvalLiteralAotInst::Echo(value) => emit_eval_literal_aot_echo(ctx, value, 0), + EvalLiteralAotInst::Store { .. } => unreachable!("scope writes are handled above"), + EvalLiteralAotInst::Return(value) => { + emit_eval_literal_aot_expr_cell(ctx, value, 0); + break; + } + } + } + } + store_if_result(ctx, inst)?; + Ok(true) +} + +/// Lowers write-only boxed AOT stores directly into caller local slots without eval scope. +fn lower_eval_literal_boxed_aot_direct_local_stores( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLiteralBoxedAotProgram, + targets: &BTreeMap>, +) -> Result<()> { + ctx.emitter.comment(&format!( + "eval literal AOT compiled direct local stores ({} ops)", + program.instructions.len() + )); + for aot_inst in &program.instructions { + match aot_inst { + EvalLiteralAotInst::Store { name, value } => { + let target = targets.get(name).ok_or_else(|| { + CodegenIrError::unsupported(format!( + "direct eval local store target ${} was not prepared", + name + )) + })?; + if let Some(slot) = target { + emit_eval_literal_aot_direct_local_store(ctx, *slot, value)?; + } + } + EvalLiteralAotInst::Return(value) => { + emit_eval_literal_aot_core_mixed_expr(ctx, value)?; + store_if_result(ctx, inst)?; + return Ok(()); + } + EvalLiteralAotInst::Echo(_) => { + return Err(CodegenIrError::unsupported( + "direct eval local store echo should be rejected before lowering".to_string(), + )); + } + } + } + emit_eval_local_scalar_core_null_cell(ctx); + store_if_result(ctx, inst) +} + +/// Lowers read/write eval stores by reading and writing caller integer locals directly. +fn lower_eval_literal_boxed_aot_direct_read_writes( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLiteralBoxedAotProgram, + targets: &BTreeMap, +) -> Result<()> { + ctx.emitter + .comment("eval literal AOT compiled direct local read/write stores"); + for aot_inst in &program.instructions { + match aot_inst { + EvalLiteralAotInst::Store { name, value } => { + let slot = targets.get(name).ok_or_else(|| { + CodegenIrError::unsupported(format!( + "direct eval read/write target ${} was not prepared", + name + )) + })?; + let source_ty = emit_eval_literal_aot_direct_read_write_expr(ctx, *slot, value)?; + emit_eval_literal_prepare_direct_read_write_store(ctx, *slot, &source_ty)?; + ctx.store_current_result_to_local(*slot)?; + } + EvalLiteralAotInst::Return(value) => { + let EvalLiteralAotExpr::Scalar(value) = value else { + return Err(CodegenIrError::unsupported( + "direct eval read/write return requires a static scalar".to_string(), + )); + }; + emit_eval_literal_aot_core_mixed_scalar(ctx, value); + store_if_result(ctx, inst)?; + return Ok(()); + } + EvalLiteralAotInst::Echo(_) => { + return Err(CodegenIrError::unsupported( + "direct eval read/write echo should be rejected before lowering".to_string(), + )); + } + } + } + emit_eval_local_scalar_core_null_cell(ctx); + store_if_result(ctx, inst) +} + +/// Resolves direct read/write eval targets, returning `None` when scope semantics are needed. +fn eval_literal_boxed_direct_read_write_targets( + ctx: &FunctionContext<'_>, + program: &EvalLiteralBoxedAotProgram, +) -> Result>> { + if !program.has_scope_writes || program.scope_reads.is_empty() { + return Ok(None); + } + let mut targets = BTreeMap::new(); + for inst in &program.instructions { + match inst { + EvalLiteralAotInst::Store { name, value } => { + let Some(slot) = eval_literal_direct_read_write_local_slot(ctx, name)? else { + return Ok(None); + }; + let target_ty = ctx.local_php_type(slot)?.codegen_repr(); + let Some(result_ty) = + eval_literal_direct_read_write_expr_type(value, name, &target_ty) + else { + return Ok(None); + }; + if !eval_literal_direct_read_write_result_supported(&target_ty, &result_ty) { + return Ok(None); + } + targets.insert(name.clone(), slot); + } + EvalLiteralAotInst::Return(value) => { + if !matches!(value, EvalLiteralAotExpr::Scalar(_)) { + return Ok(None); + } + } + EvalLiteralAotInst::Echo(_) => return Ok(None), + } + } + Ok(Some(targets)) +} + +/// Returns an initialized scalar caller local slot for direct read/write eval. +fn eval_literal_direct_read_write_local_slot( + ctx: &FunctionContext<'_>, + name: &str, +) -> Result> { + if main_name_uses_eval_global_scope(ctx, name) { + return Ok(None); + } + let Some(slot) = ctx.local_slot_by_name(name) else { + return Ok(None); + }; + if ctx.local_kind(slot)? != LocalKind::PhpLocal + || ctx.local_stores_ref_cell_pointer(slot) + || !matches!( + ctx.local_php_type(slot)?.codegen_repr(), + PhpType::Int | PhpType::Float | PhpType::Mixed | PhpType::Union(_) + ) + || !eval_literal_local_slot_has_eir_write(ctx, slot) + { + return Ok(None); + } + Ok(Some(slot)) +} + +/// Returns the native result type for a direct scalar read/write expression. +fn eval_literal_direct_read_write_expr_type( + value: &EvalLiteralAotExpr, + target_name: &str, + target_ty: &PhpType, +) -> Option { + match value { + EvalLiteralAotExpr::Scalar(EvalLiteralAotScalar::Int(_)) => Some(PhpType::Int), + EvalLiteralAotExpr::Scalar(EvalLiteralAotScalar::Float(_)) => Some(PhpType::Float), + EvalLiteralAotExpr::LoadVar(name) if name == target_name => Some(target_ty.clone()), + EvalLiteralAotExpr::Binary { op, left, right } => { + let left_ty = eval_literal_direct_read_write_expr_type(left, target_name, target_ty)?; + let right_ty = eval_literal_direct_read_write_expr_type(right, target_name, target_ty)?; + match op { + EvalLiteralAotBinaryOp::Add + | EvalLiteralAotBinaryOp::Sub + | EvalLiteralAotBinaryOp::Mul + if left_ty == PhpType::Int && right_ty == PhpType::Int => + { + Some(PhpType::Int) + } + EvalLiteralAotBinaryOp::Div + if eval_literal_direct_read_write_numeric_type(&left_ty) + && eval_literal_direct_read_write_numeric_type(&right_ty) => + { + Some(PhpType::Float) + } + EvalLiteralAotBinaryOp::Mod + if eval_literal_direct_read_write_numeric_type(&left_ty) + && eval_literal_direct_read_write_numeric_type(&right_ty) => + { + Some(PhpType::Int) + } + EvalLiteralAotBinaryOp::Concat + | EvalLiteralAotBinaryOp::Add + | EvalLiteralAotBinaryOp::Sub + | EvalLiteralAotBinaryOp::Mul + | EvalLiteralAotBinaryOp::Div + | EvalLiteralAotBinaryOp::Mod => None, + } + } + _ => None, + } +} + +/// Returns true when a direct read/write native result can be stored in the caller slot. +fn eval_literal_direct_read_write_result_supported( + target_ty: &PhpType, + result_ty: &PhpType, +) -> bool { + match target_ty.codegen_repr() { + PhpType::Int => result_ty.codegen_repr() == PhpType::Int, + PhpType::Float => result_ty.codegen_repr() == PhpType::Float, + PhpType::Mixed | PhpType::Union(_) => result_ty.codegen_repr() == PhpType::Float, + _ => false, + } +} + +/// Returns true for native numeric types accepted by direct read/write arithmetic. +fn eval_literal_direct_read_write_numeric_type(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Float | PhpType::Mixed | PhpType::Union(_) + ) +} + +/// Emits a direct scalar read/write expression into the matching result register. +fn emit_eval_literal_aot_direct_read_write_expr( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + value: &EvalLiteralAotExpr, +) -> Result { + match value { + EvalLiteralAotExpr::Scalar(EvalLiteralAotScalar::Int(value)) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), *value); + Ok(PhpType::Int) + } + EvalLiteralAotExpr::Scalar(EvalLiteralAotScalar::Float(value)) => { + emit_eval_literal_aot_scalar_native_result(ctx, &EvalLiteralAotScalar::Float(*value)); + Ok(PhpType::Float) + } + EvalLiteralAotExpr::LoadVar(_) => ctx.load_local_to_result(slot), + EvalLiteralAotExpr::Binary { op, left, right } => { + let left_ty = emit_eval_literal_aot_direct_read_write_expr(ctx, slot, left)?; + emit_eval_literal_direct_read_write_push_result(ctx, &left_ty)?; + let right_ty = emit_eval_literal_aot_direct_read_write_expr(ctx, slot, right)?; + match op { + EvalLiteralAotBinaryOp::Div => { + emit_eval_literal_direct_read_write_numeric_to_float(ctx, &right_ty)?; + let rhs_reg = abi::float_arg_reg_name(ctx.emitter.target, 1); + if matches!(left_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + abi::emit_push_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + 16, + ); + emit_eval_literal_direct_read_write_numeric_to_float(ctx, &left_ty)?; + abi::emit_pop_float_reg(ctx.emitter, rhs_reg); + abi::emit_release_temporary_stack(ctx.emitter, 16); + } else { + abi::emit_reg_move( + ctx.emitter, + rhs_reg, + abi::float_result_reg(ctx.emitter), + ); + emit_eval_literal_direct_read_write_pop_result(ctx, &left_ty)?; + emit_eval_literal_direct_read_write_numeric_to_float(ctx, &left_ty)?; + } + emit_eval_literal_direct_read_write_div(ctx, rhs_reg); + Ok(PhpType::Float) + } + EvalLiteralAotBinaryOp::Mod => { + emit_eval_literal_direct_read_write_numeric_to_int(ctx, &right_ty)?; + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_direct_read_write_pop_result(ctx, &left_ty)?; + emit_eval_literal_direct_read_write_numeric_to_int(ctx, &left_ty)?; + emit_eval_local_scalar_mod(ctx, rhs_reg); + Ok(PhpType::Int) + } + EvalLiteralAotBinaryOp::Add + | EvalLiteralAotBinaryOp::Sub + | EvalLiteralAotBinaryOp::Mul => { + if left_ty != PhpType::Int || right_ty != PhpType::Int { + return Err(CodegenIrError::unsupported( + "direct eval read/write float arithmetic".to_string(), + )); + } + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_direct_read_write_pop_result(ctx, &left_ty)?; + let local_op = match op { + EvalLiteralAotBinaryOp::Add => EvalLocalScalarBinaryOp::Add, + EvalLiteralAotBinaryOp::Sub => EvalLocalScalarBinaryOp::Sub, + EvalLiteralAotBinaryOp::Mul => EvalLocalScalarBinaryOp::Mul, + EvalLiteralAotBinaryOp::Div + | EvalLiteralAotBinaryOp::Mod + | EvalLiteralAotBinaryOp::Concat => unreachable!( + "direct read/write arithmetic op filtered before integer lowering" + ), + }; + emit_eval_local_scalar_binary_result(ctx, &local_op, rhs_reg); + Ok(PhpType::Int) + } + EvalLiteralAotBinaryOp::Concat => { + return Err(CodegenIrError::unsupported( + "direct eval read/write concat".to_string(), + )); + } + } + } + _ => Err(CodegenIrError::unsupported( + "direct eval read/write expression".to_string(), + )), + } +} + +/// Preserves the current direct read/write result on the temporary stack. +fn emit_eval_literal_direct_read_write_push_result( + ctx: &mut FunctionContext<'_>, + ty: &PhpType, +) -> Result<()> { + match ty.codegen_repr() { + PhpType::Int => { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + Ok(()) + } + PhpType::Float => { + abi::emit_push_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "direct eval read/write push for PHP type {:?}", + other + ))), + } +} + +/// Restores a preserved direct read/write result from the temporary stack. +fn emit_eval_literal_direct_read_write_pop_result( + ctx: &mut FunctionContext<'_>, + ty: &PhpType, +) -> Result<()> { + match ty.codegen_repr() { + PhpType::Int => { + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + Ok(()) + } + PhpType::Float => { + abi::emit_pop_float_reg(ctx.emitter, abi::float_result_reg(ctx.emitter)); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "direct eval read/write pop for PHP type {:?}", + other + ))), + } +} + +/// Coerces the current direct read/write numeric result into the float register. +fn emit_eval_literal_direct_read_write_numeric_to_float( + ctx: &mut FunctionContext<'_>, + ty: &PhpType, +) -> Result<()> { + match ty.codegen_repr() { + PhpType::Int => { + abi::emit_int_result_to_float_result(ctx.emitter); + Ok(()) + } + PhpType::Float => Ok(()), + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "direct eval read/write float coercion for PHP type {:?}", + other + ))), + } +} + +/// Coerces the current direct read/write numeric result into the integer register. +fn emit_eval_literal_direct_read_write_numeric_to_int( + ctx: &mut FunctionContext<'_>, + ty: &PhpType, +) -> Result<()> { + match ty.codegen_repr() { + PhpType::Int => Ok(()), + PhpType::Float => { + abi::emit_float_result_to_int_result(ctx.emitter); + Ok(()) + } + PhpType::Mixed | PhpType::Union(_) => { + abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); + Ok(()) + } + other => Err(CodegenIrError::unsupported(format!( + "direct eval read/write int coercion for PHP type {:?}", + other + ))), + } +} + +/// Emits the target-specific direct read/write floating division. +fn emit_eval_literal_direct_read_write_div(ctx: &mut FunctionContext<'_>, rhs_reg: &str) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!( + "fdiv {}, {}, {}", + abi::float_result_reg(ctx.emitter), + abi::float_result_reg(ctx.emitter), + rhs_reg + )); // compute the direct eval read/write floating-point quotient + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!( + "divsd {}, {}", + abi::float_result_reg(ctx.emitter), + rhs_reg + )); // compute the direct eval read/write floating-point quotient + } + } +} + +/// Converts a direct read/write result to the caller slot representation and releases old ownership. +fn emit_eval_literal_prepare_direct_read_write_store( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + source_ty: &PhpType, +) -> Result<()> { + let target_ty = ctx.local_php_type(slot)?.codegen_repr(); + if matches!(target_ty, PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &source_ty.codegen_repr()); + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_release_old_direct_local_value(ctx, slot, &target_ty)?; + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + } + Ok(()) +} + +/// Releases the previous refcounted value stored in a direct eval local target. +fn emit_eval_literal_release_old_direct_local_value( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + target_ty: &PhpType, +) -> Result<()> { + if target_ty.codegen_repr().is_refcounted() { + let offset = ctx.local_offset(slot)?; + abi::load_at_offset_scratch( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + offset, + abi::secondary_scratch_reg(ctx.emitter), + ); + abi::emit_decref_if_refcounted(ctx.emitter, &target_ty.codegen_repr()); + } + Ok(()) +} + +/// Resolves direct local-store targets, returning `None` when scope semantics are still needed. +fn eval_literal_boxed_direct_local_store_targets( + ctx: &FunctionContext<'_>, + program: &EvalLiteralBoxedAotProgram, +) -> Result>>> { + if !program.has_scope_writes || !program.scope_reads.is_empty() { + return Ok(None); + } + let mut targets = BTreeMap::new(); + for inst in &program.instructions { + match inst { + EvalLiteralAotInst::Store { name, value } => { + if !matches!(value, EvalLiteralAotExpr::Scalar(_)) { + return Ok(None); + } + let target = match eval_literal_direct_store_local_slot(ctx, name)? { + Some(slot) + if eval_literal_aot_expr_direct_store_supported(ctx, slot, value)? => + { + Some(slot) + } + _ => None, + }; + targets.insert(name.clone(), target); + } + EvalLiteralAotInst::Return(value) => { + if !matches!(value, EvalLiteralAotExpr::Scalar(_)) { + return Ok(None); + } + } + EvalLiteralAotInst::Echo(_) => return Ok(None), + } + } + Ok(Some(targets)) +} + +/// Returns the caller local slot for a direct eval store when overwriting is known safe. +fn eval_literal_direct_store_local_slot( + ctx: &FunctionContext<'_>, + name: &str, +) -> Result> { + if main_name_uses_eval_global_scope(ctx, name) { + return Ok(None); + } + let Some(slot) = ctx.local_slot_by_name(name) else { + return Ok(None); + }; + if ctx.local_kind(slot)? != LocalKind::PhpLocal + || ctx.local_stores_ref_cell_pointer(slot) + || eval_literal_local_slot_has_eir_write(ctx, slot) + { + return Ok(None); + } + Ok(Some(slot)) +} + +/// Returns true when existing EIR already writes a local slot and old-value release matters. +fn eval_literal_local_slot_has_eir_write(ctx: &FunctionContext<'_>, slot: LocalSlotId) -> bool { + ctx.function.instructions.iter().any(|inst| { + matches!( + inst.op, + Op::StoreLocal + | Op::StoreRefCell + | Op::UnsetLocal + | Op::PromoteLocalRefCell + | Op::AliasLocalRefCell + | Op::ReleaseLocalRefCell + ) && inst.immediate == Some(Immediate::LocalSlot(slot)) + }) +} + +/// Returns true when a boxed AOT expression can be stored into a local target type. +fn eval_literal_aot_expr_direct_store_supported( + ctx: &FunctionContext<'_>, + slot: LocalSlotId, + value: &EvalLiteralAotExpr, +) -> Result { + let EvalLiteralAotExpr::Scalar(value) = value else { + return Ok(false); + }; + let target_ty = ctx.local_php_type(slot)?.codegen_repr(); + Ok(eval_literal_aot_scalar_direct_store_supported( + value, &target_ty, + )) +} + +/// Returns true when a scalar value can be materialized in a direct local target format. +fn eval_literal_aot_scalar_direct_store_supported( + value: &EvalLiteralAotScalar, + target_ty: &PhpType, +) -> bool { + if matches!(target_ty, PhpType::Mixed | PhpType::Union(_)) { + return true; + } + match (value, target_ty) { + (EvalLiteralAotScalar::Int(_), PhpType::Int) => true, + (EvalLiteralAotScalar::Bool(_), PhpType::Bool) => true, + (EvalLiteralAotScalar::Float(_), PhpType::Float) => true, + (EvalLiteralAotScalar::String(_), PhpType::Str) => true, + (EvalLiteralAotScalar::Null | EvalLiteralAotScalar::Int(_), PhpType::TaggedScalar) => true, + _ => false, + } +} + +/// Stores one static scalar eval assignment directly into an already allocated local slot. +fn emit_eval_literal_aot_direct_local_store( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + value: &EvalLiteralAotExpr, +) -> Result<()> { + let EvalLiteralAotExpr::Scalar(value) = value else { + return Err(CodegenIrError::unsupported( + "direct eval local store requires a static scalar value".to_string(), + )); + }; + match ctx.local_php_type(slot)?.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => { + emit_eval_literal_aot_core_mixed_scalar(ctx, value); + ctx.store_current_result_to_local(slot) + } + PhpType::Str => { + emit_eval_literal_aot_scalar_native_result(ctx, value); + ctx.store_current_result_to_local(slot) + } + PhpType::TaggedScalar => { + emit_eval_literal_aot_tagged_scalar_result(ctx, value)?; + ctx.store_current_result_to_local(slot) + } + target_ty => { + emit_eval_literal_aot_scalar_native_result(ctx, value); + if eval_literal_aot_scalar_direct_store_supported(value, &target_ty) { + ctx.store_current_result_to_local(slot) + } else { + Err(CodegenIrError::unsupported(format!( + "direct eval local store to PHP type {:?}", + target_ty + ))) + } + } + } +} + +/// Emits a scalar eval expression as a core-runtime Mixed cell in the result register. +fn emit_eval_literal_aot_core_mixed_expr( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotExpr, +) -> Result<()> { + let EvalLiteralAotExpr::Scalar(value) = value else { + return Err(CodegenIrError::unsupported( + "direct eval local store return requires a static scalar value".to_string(), + )); + }; + emit_eval_literal_aot_core_mixed_scalar(ctx, value); + Ok(()) +} + +/// Emits a scalar value as a core-runtime Mixed cell without eval bridge helpers. +fn emit_eval_literal_aot_core_mixed_scalar( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotScalar, +) { + if matches!(value, EvalLiteralAotScalar::Null) { + emit_eval_local_scalar_core_null_cell(ctx); + return; + } + let source_ty = emit_eval_literal_aot_scalar_native_result(ctx, value); + emit_box_current_value_as_mixed(ctx.emitter, &source_ty); +} + +/// Emits a scalar value in its native result-register representation. +fn emit_eval_literal_aot_scalar_native_result( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotScalar, +) -> PhpType { + match value { + EvalLiteralAotScalar::Null => { + crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); + PhpType::TaggedScalar + } + EvalLiteralAotScalar::Bool(value) => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + i64::from(*value), + ); + PhpType::Bool + } + EvalLiteralAotScalar::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), *value); + PhpType::Int + } + EvalLiteralAotScalar::Float(value) => { + let label = ctx.data.add_float(*value); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::float_result_reg(ctx.emitter), + &label, + 0, + ); + PhpType::Float + } + EvalLiteralAotScalar::String(value) => { + let (label, len) = ctx.data.add_string(value.as_bytes()); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); + PhpType::Str + } + } +} + +/// Emits a scalar value as the tagged-scalar local representation. +fn emit_eval_literal_aot_tagged_scalar_result( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotScalar, +) -> Result<()> { + match value { + EvalLiteralAotScalar::Null => { + crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); + Ok(()) + } + EvalLiteralAotScalar::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), *value); + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(ctx.emitter); + Ok(()) + } + _ => Err(CodegenIrError::unsupported( + "direct eval local store tagged-scalar source".to_string(), + )), + } +} + +/// Lowers a self-contained int/bool literal eval fragment as native local control flow. +fn lower_eval_local_scalar_aot( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLocalScalarAotProgram, +) -> Result { + if !eval_global_aliases(ctx).is_empty() { + ctx.emitter.comment( + "eval literal AOT fallback: local scalar writes with global aliases need bridge semantics", + ); + return Ok(false); + } + if !eval_local_scalar_codegen_supported(ctx, program) { + ctx.emitter + .comment("eval literal AOT fallback: local scalar static call is not supported"); + return Ok(false); + } + + ctx.emitter.comment(&format!( + "eval literal AOT compiled local scalar ({} locals, {} stmts)", + program.locals.len(), + program.statements.len() + )); + if let Some(targets) = eval_local_scalar_direct_sync_targets(ctx, program)? { + lower_eval_local_scalar_aot_with_direct_sync(ctx, inst, program, &targets)?; + } else if eval_local_scalar_needs_scope_sync(ctx, program) { + lower_eval_local_scalar_aot_with_scope_sync(ctx, inst, program)?; + } else { + lower_eval_local_scalar_aot_native_only(ctx, inst, program)?; + } + Ok(true) +} + +/// Returns true when local scalar AOT must synchronize variables with eval scope. +fn eval_local_scalar_needs_scope_sync( + ctx: &FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) -> bool { + if !program.locals.is_empty() { + return true; + } + let sync_local_names = eval_sync_locals(ctx) + .into_iter() + .map(|local| local.name) + .collect::>(); + let sync_global_names = eval_sync_globals(ctx) + .into_iter() + .map(|global| global.name) + .collect::>(); + program + .locals + .keys() + .any(|name| sync_local_names.contains(name) || sync_global_names.contains(name)) +} + +/// Resolves caller local slots that can receive final local-scalar eval writes directly. +fn eval_local_scalar_direct_sync_targets( + ctx: &FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) -> Result>>> { + if program.locals.is_empty() { + return Ok(Some(BTreeMap::new())); + } + let mut targets = BTreeMap::new(); + for (name, local_type) in &program.local_types { + if main_name_uses_eval_global_scope(ctx, name) { + return Ok(None); + } + let Some(slot) = ctx.local_slot_by_name(name) else { + targets.insert(name.clone(), None); + continue; + }; + if ctx.local_kind(slot)? != LocalKind::PhpLocal + || ctx.local_stores_ref_cell_pointer(slot) + || eval_literal_local_slot_has_eir_write(ctx, slot) + || !eval_local_scalar_direct_sync_type_supported(ctx.local_php_type(slot)?, *local_type) + { + return Ok(None); + } + targets.insert(name.clone(), Some(slot)); + } + Ok(Some(targets)) +} + +/// Returns true when one local-scalar value type fits the caller local slot type. +fn eval_local_scalar_direct_sync_type_supported( + target_ty: PhpType, + local_type: EvalLocalScalarType, +) -> bool { + match target_ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => true, + PhpType::Int => local_type == EvalLocalScalarType::Int, + PhpType::Float => local_type == EvalLocalScalarType::Float, + PhpType::Bool => local_type == EvalLocalScalarType::Bool, + PhpType::Str => local_type == EvalLocalScalarType::String, + PhpType::TaggedScalar => matches!( + local_type, + EvalLocalScalarType::Null | EvalLocalScalarType::Int + ), + _ => false, + } +} + +/// Lowers local scalar AOT without any eval bridge or eval scope runtime dependency. +fn lower_eval_local_scalar_aot_native_only( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLocalScalarAotProgram, +) -> Result<()> { + let stack_bytes = program.stack_bytes(); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + emit_eval_local_scalar_init_slots(ctx, program); + + let return_label = ctx.next_label("eval_local_aot_return"); + let mut loop_stack = Vec::new(); + for stmt in &program.statements { + emit_eval_local_scalar_stmt( + ctx, + program, + stmt, + &mut loop_stack, + &return_label, + EvalLocalScalarBoxing::CoreRuntime, + ); + } + emit_eval_local_scalar_null_cell(ctx, EvalLocalScalarBoxing::CoreRuntime); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + + ctx.emitter.label(&return_label); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers local scalar AOT and writes supported final locals directly to caller slots. +fn lower_eval_local_scalar_aot_with_direct_sync( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLocalScalarAotProgram, + targets: &BTreeMap>, +) -> Result<()> { + ctx.emitter + .comment("eval literal AOT compiled local scalar with direct local sync"); + let stack_bytes = program.stack_bytes(); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + emit_eval_local_scalar_init_slots(ctx, program); + + let return_label = ctx.next_label("eval_local_aot_return"); + let mut loop_stack = Vec::new(); + for stmt in &program.statements { + emit_eval_local_scalar_stmt( + ctx, + program, + stmt, + &mut loop_stack, + &return_label, + EvalLocalScalarBoxing::CoreRuntime, + ); + } + emit_eval_local_scalar_null_cell(ctx, EvalLocalScalarBoxing::CoreRuntime); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + + ctx.emitter.label(&return_label); + emit_eval_local_scalar_flush_direct_locals(ctx, program, targets)?; + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Lowers local scalar AOT and syncs defined locals through eval scope. +fn lower_eval_local_scalar_aot_with_scope_sync( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + program: &EvalLocalScalarAotProgram, +) -> Result<()> { + let stack_bytes = program.stack_bytes(); + abi::emit_reserve_temporary_stack(ctx.emitter, stack_bytes); + ensure_eval_scope(ctx)?; + ensure_eval_global_scope(ctx)?; + emit_eval_local_scalar_init_slots(ctx, program); + + let return_label = ctx.next_label("eval_local_aot_return"); + let mut loop_stack = Vec::new(); + for stmt in &program.statements { + emit_eval_local_scalar_stmt( + ctx, + program, + stmt, + &mut loop_stack, + &return_label, + EvalLocalScalarBoxing::EvalRuntime, + ); + } + emit_eval_local_scalar_null_cell(ctx, EvalLocalScalarBoxing::EvalRuntime); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + + ctx.emitter.label(&return_label); + emit_eval_local_scalar_flush_defined_locals(ctx, program); + let sync_locals = eval_sync_locals(ctx) + .into_iter() + .filter(|local| program.locals.contains_key(&local.name)) + .collect::>(); + let sync_globals = eval_sync_globals(ctx) + .into_iter() + .filter(|global| program.locals.contains_key(&global.name)) + .collect::>(); + reload_eval_scope_locals(ctx, &sync_locals)?; + reload_eval_global_scope(ctx, &sync_globals)?; + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); + abi::emit_release_temporary_stack(ctx.emitter, stack_bytes); + store_if_result(ctx, inst) +} + +/// Returns true when all parsed local scalar statements are supported by codegen. +fn eval_local_scalar_codegen_supported( + ctx: &FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) -> bool { + eval_local_scalar_stmts_codegen_supported(ctx, &program.statements) +} + +/// Returns true when all statements in a local scalar block are supported by codegen. +fn eval_local_scalar_stmts_codegen_supported( + ctx: &FunctionContext<'_>, + statements: &[EvalLocalScalarStmt], +) -> bool { + statements.iter().all(|stmt| match stmt { + EvalLocalScalarStmt::Noop + | EvalLocalScalarStmt::Break(_) + | EvalLocalScalarStmt::Continue(_) + | EvalLocalScalarStmt::Return(None) => true, + EvalLocalScalarStmt::Echo(expr) | EvalLocalScalarStmt::Return(Some(expr)) => { + eval_local_scalar_expr_codegen_supported(ctx, expr) + } + EvalLocalScalarStmt::Store { value, .. } => { + eval_local_scalar_expr_codegen_supported(ctx, value) + } + EvalLocalScalarStmt::If { + branches, + else_body, + } => { + branches.iter().all(|(condition, body)| { + eval_local_scalar_expr_codegen_supported(ctx, condition) + && eval_local_scalar_stmts_codegen_supported(ctx, body) + }) && eval_local_scalar_stmts_codegen_supported(ctx, else_body) + } + EvalLocalScalarStmt::While { condition, body } => { + eval_local_scalar_expr_codegen_supported(ctx, condition) + && eval_local_scalar_stmts_codegen_supported(ctx, body) + } + EvalLocalScalarStmt::DoWhile { body, condition } => { + eval_local_scalar_stmts_codegen_supported(ctx, body) + && eval_local_scalar_expr_codegen_supported(ctx, condition) + } + EvalLocalScalarStmt::For { + init, + condition, + update, + body, + } => { + init.as_deref().is_none_or(|stmt| { + eval_local_scalar_stmts_codegen_supported(ctx, std::slice::from_ref(stmt)) + }) && condition + .as_ref() + .is_none_or(|condition| eval_local_scalar_expr_codegen_supported(ctx, condition)) + && update.as_deref().is_none_or(|stmt| { + eval_local_scalar_stmts_codegen_supported(ctx, std::slice::from_ref(stmt)) + }) + && eval_local_scalar_stmts_codegen_supported(ctx, body) + } + EvalLocalScalarStmt::Switch { + subject, + cases, + default, + default_index: _, + } => { + eval_local_scalar_expr_codegen_supported(ctx, subject) + && cases.iter().all(|(conditions, body)| { + conditions + .iter() + .all(|condition| eval_local_scalar_expr_codegen_supported(ctx, condition)) + && eval_local_scalar_stmts_codegen_supported(ctx, body) + }) + && eval_local_scalar_stmts_codegen_supported(ctx, default) + } + }) +} + +/// Returns true when a local scalar expression is supported by codegen. +fn eval_local_scalar_expr_codegen_supported( + ctx: &FunctionContext<'_>, + expr: &EvalLocalScalarExpr, +) -> bool { + match &expr.kind { + EvalLocalScalarExprKind::Null + | EvalLocalScalarExprKind::Int(_) + | EvalLocalScalarExprKind::Float(_) + | EvalLocalScalarExprKind::Bool(_) + | EvalLocalScalarExprKind::String(_) + | EvalLocalScalarExprKind::LoadVar(_) + | EvalLocalScalarExprKind::Isset(_) + | EvalLocalScalarExprKind::EmptyVar(_) => true, + EvalLocalScalarExprKind::Negate(inner) + | EvalLocalScalarExprKind::BitNot(inner) + | EvalLocalScalarExprKind::Not(inner) + | EvalLocalScalarExprKind::Print(inner) => { + eval_local_scalar_expr_codegen_supported(ctx, inner) + } + EvalLocalScalarExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + eval_local_scalar_expr_codegen_supported(ctx, condition) + && eval_local_scalar_expr_codegen_supported(ctx, then_expr) + && eval_local_scalar_expr_codegen_supported(ctx, else_expr) + } + EvalLocalScalarExprKind::Binary { left, right, .. } => { + eval_local_scalar_expr_codegen_supported(ctx, left) + && eval_local_scalar_expr_codegen_supported(ctx, right) + } + EvalLocalScalarExprKind::StaticFunctionCall { name, args } => { + eval_local_static_function_codegen_supported(ctx, name, args) + } + } +} + +/// Returns true when a static user-function call can be emitted by local scalar AOT. +fn eval_local_static_function_codegen_supported( + ctx: &FunctionContext<'_>, + name: &str, + args: &[EvalLocalScalarExpr], +) -> bool { + if args.len() > 6 { + return false; + } + let Some(callee) = ctx.callable_function_by_name(name) else { + return false; + }; + if callee.params.len() != args.len() || callee.return_php_type.codegen_repr() != PhpType::Int { + return false; + } + callee.params.iter().zip(args).all(|(param, arg)| { + !param.by_ref + && !param.variadic + && matches!( + (param.php_type.codegen_repr(), arg.ty), + (PhpType::Int, EvalLocalScalarType::Int) + | (PhpType::Bool, EvalLocalScalarType::Bool) + ) + }) +} + +/// Clears local value/defined slots before running the local scalar eval fragment. +fn emit_eval_local_scalar_init_slots( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + for name in program.locals.keys() { + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_offset(name)); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_aux_offset(name)); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.defined_offset(name)); + } +} + +/// Emits one local scalar AOT statement. +fn emit_eval_local_scalar_stmt( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + stmt: &EvalLocalScalarStmt, + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + match stmt { + EvalLocalScalarStmt::Noop => {} + EvalLocalScalarStmt::Echo(expr) => { + emit_eval_local_scalar_echo_expr(ctx, program, expr, 0); + } + EvalLocalScalarStmt::Store { name, value } => { + emit_eval_local_scalar_store(ctx, program, name, value); + } + EvalLocalScalarStmt::If { + branches, + else_body, + } => { + emit_eval_local_scalar_if( + ctx, + program, + branches, + else_body, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::While { condition, body } => { + emit_eval_local_scalar_while( + ctx, + program, + condition, + body, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::DoWhile { body, condition } => { + emit_eval_local_scalar_do_while( + ctx, + program, + body, + condition, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::For { + init, + condition, + update, + body, + } => { + emit_eval_local_scalar_for( + ctx, + program, + init.as_deref(), + condition.as_ref(), + update.as_deref(), + body, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::Switch { + subject, + cases, + default, + default_index, + } => { + emit_eval_local_scalar_switch( + ctx, + program, + subject, + cases, + default, + *default_index, + loop_stack, + return_label, + boxing, + ); + } + EvalLocalScalarStmt::Break(level) => { + let target_index = loop_stack.len() - level; + abi::emit_jump(ctx.emitter, &loop_stack[target_index].break_label); + } + EvalLocalScalarStmt::Continue(level) => { + let target_index = loop_stack.len() - level; + abi::emit_jump(ctx.emitter, &loop_stack[target_index].continue_label); + } + EvalLocalScalarStmt::Return(value) => { + emit_eval_local_scalar_return_cell(ctx, program, value.as_ref(), boxing); + abi::emit_jump(ctx.emitter, return_label); + } + } +} + +/// Stores one local scalar expression into its stack slot and marks the slot defined. +fn emit_eval_local_scalar_store( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + value: &EvalLocalScalarExpr, +) { + emit_eval_local_scalar_expr_value(ctx, program, value, 0); + emit_eval_local_scalar_store_current_value(ctx, program, name, value.ty); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_int_immediate(ctx.emitter, result_reg, 1); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.defined_offset(name)); +} + +/// Stores the current expression result registers into one local scalar slot. +fn emit_eval_local_scalar_store_current_value( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + ty: EvalLocalScalarType, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ty { + EvalLocalScalarType::String => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, ptr_reg, program.value_offset(name)); + abi::emit_store_to_sp(ctx.emitter, len_reg, program.value_aux_offset(name)); + } + EvalLocalScalarType::Null => { + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_offset(name)); + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_aux_offset(name)); + } + EvalLocalScalarType::Float => { + abi::emit_store_to_sp( + ctx.emitter, + abi::float_result_reg(ctx.emitter), + program.value_offset(name), + ); + } + EvalLocalScalarType::Int | EvalLocalScalarType::Bool => { + abi::emit_store_to_sp(ctx.emitter, result_reg, program.value_offset(name)); + } + } +} + +/// Emits a local scalar if/elseif/else chain. +fn emit_eval_local_scalar_if( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + branches: &[(EvalLocalScalarExpr, Vec)], + else_body: &[EvalLocalScalarStmt], + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + let done_label = ctx.next_label("eval_local_if_done"); + for (condition, body) in branches { + let next_label = ctx.next_label("eval_local_if_next"); + emit_eval_local_scalar_expr_value(ctx, program, condition, 0); + abi::emit_branch_if_int_result_zero(ctx.emitter, &next_label); + for stmt in body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&next_label); + } + for stmt in else_body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + ctx.emitter.label(&done_label); +} + +/// Emits a local scalar while loop. +fn emit_eval_local_scalar_while( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + condition: &EvalLocalScalarExpr, + body: &[EvalLocalScalarStmt], + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + let start_label = ctx.next_label("eval_local_while_start"); + let done_label = ctx.next_label("eval_local_while_done"); + ctx.emitter.label(&start_label); + emit_eval_local_scalar_expr_value(ctx, program, condition, 0); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done_label); + loop_stack.push(EvalLocalLoopLabels { + break_label: done_label.clone(), + continue_label: start_label.clone(), + }); + for stmt in body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + loop_stack.pop(); + abi::emit_jump(ctx.emitter, &start_label); + ctx.emitter.label(&done_label); +} + +/// Emits a local scalar do/while loop. +fn emit_eval_local_scalar_do_while( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + body: &[EvalLocalScalarStmt], + condition: &EvalLocalScalarExpr, + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + let start_label = ctx.next_label("eval_local_do_start"); + let condition_label = ctx.next_label("eval_local_do_condition"); + let done_label = ctx.next_label("eval_local_do_done"); + ctx.emitter.label(&start_label); + loop_stack.push(EvalLocalLoopLabels { + break_label: done_label.clone(), + continue_label: condition_label.clone(), + }); + for stmt in body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + loop_stack.pop(); + ctx.emitter.label(&condition_label); + emit_eval_local_scalar_expr_value(ctx, program, condition, 0); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &start_label); + ctx.emitter.label(&done_label); +} + +/// Emits a local scalar for loop with PHP continue-to-update behavior. +fn emit_eval_local_scalar_for( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + init: Option<&EvalLocalScalarStmt>, + condition: Option<&EvalLocalScalarExpr>, + update: Option<&EvalLocalScalarStmt>, + body: &[EvalLocalScalarStmt], + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + if let Some(init) = init { + emit_eval_local_scalar_stmt(ctx, program, init, loop_stack, return_label, boxing); + } + let start_label = ctx.next_label("eval_local_for_start"); + let update_label = ctx.next_label("eval_local_for_update"); + let done_label = ctx.next_label("eval_local_for_done"); + ctx.emitter.label(&start_label); + if let Some(condition) = condition { + emit_eval_local_scalar_expr_value(ctx, program, condition, 0); + abi::emit_branch_if_int_result_zero(ctx.emitter, &done_label); + } + loop_stack.push(EvalLocalLoopLabels { + break_label: done_label.clone(), + continue_label: update_label.clone(), + }); + for stmt in body { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + loop_stack.pop(); + ctx.emitter.label(&update_label); + if let Some(update) = update { + emit_eval_local_scalar_stmt(ctx, program, update, loop_stack, return_label, boxing); + } + abi::emit_jump(ctx.emitter, &start_label); + ctx.emitter.label(&done_label); +} + +/// Emits a local scalar switch with PHP-style fallthrough between case bodies. +fn emit_eval_local_scalar_switch( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + subject: &EvalLocalScalarExpr, + cases: &[(Vec, Vec)], + default: &[EvalLocalScalarStmt], + default_index: Option, + loop_stack: &mut Vec, + return_label: &str, + boxing: EvalLocalScalarBoxing, +) { + let done_label = ctx.next_label("eval_local_switch_done"); + let default_label = default_index.map(|_| ctx.next_label("eval_local_switch_default")); + let case_labels = cases + .iter() + .map(|_| ctx.next_label("eval_local_switch_case")) + .collect::>(); + emit_eval_local_scalar_expr_value(ctx, program, subject, 0); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.scratch_offset(0), + ); + for ((conditions, _), case_label) in cases.iter().zip(&case_labels) { + for condition in conditions { + emit_eval_local_scalar_expr_value(ctx, program, condition, 1); + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::int_result_reg(ctx.emitter)); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.scratch_offset(0), + ); + emit_eval_local_scalar_cmp(ctx, rhs_reg, "eq", "e"); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, case_label); + } + } + if let Some(default_label) = &default_label { + abi::emit_jump(ctx.emitter, default_label); + } else { + abi::emit_jump(ctx.emitter, &done_label); + } + loop_stack.push(EvalLocalLoopLabels { + break_label: done_label.clone(), + continue_label: done_label.clone(), + }); + for index in 0..=cases.len() { + if default_index == Some(index) { + if let Some(default_label) = &default_label { + ctx.emitter.label(default_label); + for stmt in default { + emit_eval_local_scalar_stmt( + ctx, + program, + stmt, + loop_stack, + return_label, + boxing, + ); + } + } + } + if index < cases.len() { + ctx.emitter.label(&case_labels[index]); + for stmt in &cases[index].1 { + emit_eval_local_scalar_stmt(ctx, program, stmt, loop_stack, return_label, boxing); + } + } + } + loop_stack.pop(); + ctx.emitter.label(&done_label); +} + +/// Emits an AOT echo expression, splitting concat into sequential writes. +fn emit_eval_local_scalar_echo_expr( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + expr: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + match &expr.kind { + EvalLocalScalarExprKind::String(value) => { + emit_eval_local_scalar_string_stdout(ctx, value.as_bytes()); + } + EvalLocalScalarExprKind::Binary { + op: EvalLocalScalarBinaryOp::Concat, + left, + right, + } => { + emit_eval_local_scalar_echo_expr(ctx, program, left, scratch_depth); + emit_eval_local_scalar_echo_expr(ctx, program, right, scratch_depth); + } + _ => { + emit_eval_local_scalar_expr_value(ctx, program, expr, scratch_depth); + match expr.ty { + EvalLocalScalarType::Null => {} + EvalLocalScalarType::Bool => { + let skip_label = ctx.next_label("eval_local_echo_skip_false"); + abi::emit_branch_if_int_result_zero(ctx.emitter, &skip_label); + abi::emit_write_stdout(ctx.emitter, &PhpType::Bool); + ctx.emitter.label(&skip_label); + } + EvalLocalScalarType::Int + | EvalLocalScalarType::Float + | EvalLocalScalarType::String => { + abi::emit_write_stdout(ctx.emitter, &expr.ty.php_type()); + } + } + } + } +} + +/// Emits a static string directly to stdout for local scalar AOT echo. +fn emit_eval_local_scalar_string_stdout(ctx: &mut FunctionContext<'_>, bytes: &[u8]) { + let (label, len) = ctx.data.add_string(bytes); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); + abi::emit_write_stdout(ctx.emitter, &PhpType::Str); +} + +/// Emits one local scalar expression value into the integer result register. +fn emit_eval_local_scalar_expr_value( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + expr: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + match &expr.kind { + EvalLocalScalarExprKind::Null => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + } + EvalLocalScalarExprKind::Int(value) => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), *value); + } + EvalLocalScalarExprKind::Float(value) => { + emit_eval_local_scalar_float_value(ctx, *value); + } + EvalLocalScalarExprKind::Bool(value) => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + i64::from(*value), + ); + } + EvalLocalScalarExprKind::String(value) => { + emit_eval_local_scalar_string_value(ctx, value); + } + EvalLocalScalarExprKind::LoadVar(name) => { + emit_eval_local_scalar_load_local_value_as(ctx, program, name, expr.ty); + } + EvalLocalScalarExprKind::Isset(names) => { + emit_eval_local_scalar_isset(ctx, program, names); + } + EvalLocalScalarExprKind::EmptyVar(name) => { + emit_eval_local_scalar_empty_var(ctx, program, name); + } + EvalLocalScalarExprKind::Negate(inner) => { + emit_eval_local_scalar_expr_value(ctx, program, inner, scratch_depth); + emit_eval_local_scalar_negate(ctx, inner.ty); + } + EvalLocalScalarExprKind::BitNot(inner) => { + emit_eval_local_scalar_expr_value(ctx, program, inner, scratch_depth); + emit_eval_local_scalar_bitnot(ctx); + } + EvalLocalScalarExprKind::Not(inner) => { + emit_eval_local_scalar_expr_value(ctx, program, inner, scratch_depth); + emit_eval_local_scalar_not(ctx); + } + EvalLocalScalarExprKind::Print(inner) => { + emit_eval_local_scalar_echo_expr(ctx, program, inner, scratch_depth); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + } + EvalLocalScalarExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + emit_eval_local_scalar_ternary_expr( + ctx, + program, + condition, + then_expr, + else_expr, + scratch_depth, + ); + } + EvalLocalScalarExprKind::Binary { op, left, right } => { + emit_eval_local_scalar_binary_expr(ctx, program, op, left, right, scratch_depth); + } + EvalLocalScalarExprKind::StaticFunctionCall { name, args } => { + emit_eval_local_scalar_static_function_call(ctx, program, name, args, scratch_depth); + } + } +} + +/// Emits a static string into the local-scalar string result registers. +fn emit_eval_local_scalar_string_value(ctx: &mut FunctionContext<'_>, value: &str) { + let (label, len) = ctx.data.add_string(value.as_bytes()); + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_symbol_address(ctx.emitter, ptr_reg, &label); + abi::emit_load_int_immediate(ctx.emitter, len_reg, len as i64); +} + +/// Emits a static float into the local-scalar float result register. +fn emit_eval_local_scalar_float_value(ctx: &mut FunctionContext<'_>, value: f64) { + let label = ctx.data.add_float(value); + abi::emit_load_symbol_to_reg(ctx.emitter, abi::float_result_reg(ctx.emitter), &label, 0); +} + +/// Loads one local scalar slot into the result registers for its tracked type. +fn emit_eval_local_scalar_load_local_value( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, +) { + emit_eval_local_scalar_load_local_value_as(ctx, program, name, program.local_type(name)); +} + +/// Loads one local scalar slot into result registers using the type at the read point. +fn emit_eval_local_scalar_load_local_value_as( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + ty: EvalLocalScalarType, +) { + match ty { + EvalLocalScalarType::String => { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, ptr_reg, program.value_offset(name)); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + len_reg, + program.value_aux_offset(name), + ); + } + EvalLocalScalarType::Null => { + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + } + EvalLocalScalarType::Float => { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::float_result_reg(ctx.emitter), + program.value_offset(name), + ); + } + EvalLocalScalarType::Int | EvalLocalScalarType::Bool => { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.value_offset(name), + ); + } + } +} + +/// Emits PHP `isset()` for definitely local scalar variables. +fn emit_eval_local_scalar_isset( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + names: &[String], +) { + let false_label = ctx.next_label("eval_local_isset_false"); + let done_label = ctx.next_label("eval_local_isset_done"); + for name in names { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.defined_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &false_label); + if program.local_type(name) == EvalLocalScalarType::Null { + abi::emit_jump(ctx.emitter, &false_label); + } + } + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.emitter.label(&done_label); +} + +/// Emits PHP `empty()` for a definitely local scalar variable. +fn emit_eval_local_scalar_empty_var( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, +) { + let true_label = ctx.next_label("eval_local_empty_true"); + let false_label = ctx.next_label("eval_local_empty_false"); + let done_label = ctx.next_label("eval_local_empty_done"); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.defined_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &true_label); + match program.local_type(name) { + EvalLocalScalarType::Null => { + abi::emit_jump(ctx.emitter, &true_label); + } + EvalLocalScalarType::Int | EvalLocalScalarType::Bool => { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.value_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &true_label); + abi::emit_jump(ctx.emitter, &false_label); + } + EvalLocalScalarType::Float => { + emit_eval_local_scalar_load_local_value_as( + ctx, + program, + name, + EvalLocalScalarType::Float, + ); + predicates::emit_float_result_nonzero_bool(ctx); + abi::emit_branch_if_int_result_zero(ctx.emitter, &true_label); + abi::emit_jump(ctx.emitter, &false_label); + } + EvalLocalScalarType::String => { + emit_eval_local_scalar_empty_string(ctx, program, name, &true_label, &false_label); + } + } + ctx.emitter.label(&true_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.emitter.label(&done_label); +} + +/// Emits the PHP string truthiness check used by `empty()`. +fn emit_eval_local_scalar_empty_string( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + true_label: &str, + false_label: &str, +) { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, ptr_reg, program.value_offset(name)); + abi::emit_load_temporary_stack_slot(ctx.emitter, len_reg, program.value_aux_offset(name)); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #0", len_reg)); // empty strings are falsey in PHP + ctx.emitter.instruction(&format!("b.eq {}", true_label)); // branch when length is zero + ctx.emitter.instruction(&format!("cmp {}, #1", len_reg)); // check for PHP's special string "0" empty case + ctx.emitter.instruction(&format!("b.ne {}", false_label)); // non-empty non-"0" strings are truthy + ctx.emitter.instruction(&format!("ldrb w11, [{}]", ptr_reg)); // load the only byte of the candidate "0" string + ctx.emitter.instruction("cmp w11, #48"); // compare the byte with ASCII '0' + ctx.emitter.instruction(&format!("b.eq {}", true_label)); // string "0" is empty in PHP truthiness + ctx.emitter.instruction(&format!("b {}", false_label)); // any other one-byte string is truthy + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("cmp {}, 0", len_reg)); // empty strings are falsey in PHP + ctx.emitter.instruction(&format!("je {}", true_label)); // branch when length is zero + ctx.emitter.instruction(&format!("cmp {}, 1", len_reg)); // check for PHP's special string "0" empty case + ctx.emitter.instruction(&format!("jne {}", false_label)); // non-empty non-"0" strings are truthy + ctx.emitter + .instruction(&format!("movzx ecx, BYTE PTR [{}]", ptr_reg)); // load the only byte of the candidate "0" string + ctx.emitter.instruction("cmp ecx, 48"); // compare the byte with ASCII '0' + ctx.emitter.instruction(&format!("je {}", true_label)); // string "0" is empty in PHP truthiness + ctx.emitter.instruction(&format!("jmp {}", false_label)); // any other one-byte string is truthy + } + } +} + +/// Emits a local scalar ternary expression with same-typed branches. +fn emit_eval_local_scalar_ternary_expr( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + condition: &EvalLocalScalarExpr, + then_expr: &EvalLocalScalarExpr, + else_expr: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + let else_label = ctx.next_label("eval_local_ternary_else"); + let done_label = ctx.next_label("eval_local_ternary_done"); + emit_eval_local_scalar_expr_value(ctx, program, condition, scratch_depth); + abi::emit_branch_if_int_result_zero(ctx.emitter, &else_label); + emit_eval_local_scalar_expr_value(ctx, program, then_expr, scratch_depth); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&else_label); + emit_eval_local_scalar_expr_value(ctx, program, else_expr, scratch_depth); + ctx.emitter.label(&done_label); +} + +/// Emits a validated static user-function call for local scalar AOT. +fn emit_eval_local_scalar_static_function_call( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + name: &str, + args: &[EvalLocalScalarExpr], + scratch_depth: usize, +) { + let arg_base_depth = scratch_depth; + let eval_depth = scratch_depth + args.len(); + for (index, arg) in args.iter().enumerate() { + emit_eval_local_scalar_expr_value(ctx, program, arg, eval_depth); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.scratch_offset(arg_base_depth + index), + ); + } + for index in 0..args.len() { + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, index), + program.scratch_offset(arg_base_depth + index), + ); + } + abi::emit_call_label(ctx.emitter, &function_symbol(name.trim_start_matches('\\'))); +} + +/// Emits a local scalar binary expression into the integer result register. +fn emit_eval_local_scalar_binary_expr( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + op: &EvalLocalScalarBinaryOp, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + match op { + EvalLocalScalarBinaryOp::And => { + emit_eval_local_scalar_and(ctx, program, left, right, scratch_depth); + } + EvalLocalScalarBinaryOp::Or => { + emit_eval_local_scalar_or(ctx, program, left, right, scratch_depth); + } + EvalLocalScalarBinaryOp::Div => { + emit_eval_local_scalar_div(ctx, program, left, right, scratch_depth); + } + EvalLocalScalarBinaryOp::Mod => { + emit_eval_local_scalar_mod_expr(ctx, program, left, right, scratch_depth); + } + EvalLocalScalarBinaryOp::Concat => unreachable!("concat is emitted as sequential echo"), + _ => { + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp( + ctx.emitter, + result_reg, + program.scratch_offset(scratch_depth), + ); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth + 1); + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, result_reg); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + result_reg, + program.scratch_offset(scratch_depth), + ); + emit_eval_local_scalar_binary_result(ctx, op, rhs_reg); + } + } +} + +/// Emits a numeric local-scalar division into the float result register. +fn emit_eval_local_scalar_div( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + emit_eval_local_scalar_store_numeric_scratch(ctx, program, scratch_depth, left.ty); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth + 1); + emit_eval_local_scalar_numeric_result_to_float(ctx, right.ty); + let rhs_reg = abi::float_arg_reg_name(ctx.emitter.target, 1); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::float_result_reg(ctx.emitter)); + emit_eval_local_scalar_load_numeric_scratch(ctx, program, scratch_depth, left.ty); + emit_eval_local_scalar_numeric_result_to_float(ctx, left.ty); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!( + "fdiv {}, {}, {}", + abi::float_result_reg(ctx.emitter), + abi::float_result_reg(ctx.emitter), + rhs_reg + )); // compute the local scalar floating-point quotient + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!( + "divsd {}, {}", + abi::float_result_reg(ctx.emitter), + rhs_reg + )); // compute the local scalar floating-point quotient + } + } +} + +/// Emits a numeric local-scalar modulo after PHP-style int coercion. +fn emit_eval_local_scalar_mod_expr( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + emit_eval_local_scalar_store_numeric_scratch(ctx, program, scratch_depth, left.ty); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth + 1); + emit_eval_local_scalar_numeric_result_to_int(ctx, right.ty); + let rhs_reg = abi::secondary_scratch_reg(ctx.emitter); + abi::emit_reg_move(ctx.emitter, rhs_reg, abi::int_result_reg(ctx.emitter)); + emit_eval_local_scalar_load_numeric_scratch(ctx, program, scratch_depth, left.ty); + emit_eval_local_scalar_numeric_result_to_int(ctx, left.ty); + emit_eval_local_scalar_mod(ctx, rhs_reg); +} + +/// Stores the current numeric result into a local-scalar scratch slot. +fn emit_eval_local_scalar_store_numeric_scratch( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + depth: usize, + ty: EvalLocalScalarType, +) { + let reg = match ty { + EvalLocalScalarType::Float => abi::float_result_reg(ctx.emitter), + EvalLocalScalarType::Int => abi::int_result_reg(ctx.emitter), + EvalLocalScalarType::Null | EvalLocalScalarType::Bool | EvalLocalScalarType::String => { + unreachable!("numeric scratch only accepts int/float local scalar values") + } + }; + abi::emit_store_to_sp(ctx.emitter, reg, program.scratch_offset(depth)); +} + +/// Loads a numeric scratch slot into the matching result register. +fn emit_eval_local_scalar_load_numeric_scratch( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + depth: usize, + ty: EvalLocalScalarType, +) { + let reg = match ty { + EvalLocalScalarType::Float => abi::float_result_reg(ctx.emitter), + EvalLocalScalarType::Int => abi::int_result_reg(ctx.emitter), + EvalLocalScalarType::Null | EvalLocalScalarType::Bool | EvalLocalScalarType::String => { + unreachable!("numeric scratch only accepts int/float local scalar values") + } + }; + abi::emit_load_temporary_stack_slot(ctx.emitter, reg, program.scratch_offset(depth)); +} + +/// Normalizes the current numeric result into the float result register. +fn emit_eval_local_scalar_numeric_result_to_float( + ctx: &mut FunctionContext<'_>, + ty: EvalLocalScalarType, +) { + match ty { + EvalLocalScalarType::Int => abi::emit_int_result_to_float_result(ctx.emitter), + EvalLocalScalarType::Float => {} + EvalLocalScalarType::Null | EvalLocalScalarType::Bool | EvalLocalScalarType::String => { + unreachable!("numeric float coercion only accepts int/float local scalar values") + } + } +} + +/// Normalizes the current numeric result into the integer result register. +fn emit_eval_local_scalar_numeric_result_to_int( + ctx: &mut FunctionContext<'_>, + ty: EvalLocalScalarType, +) { + match ty { + EvalLocalScalarType::Int => {} + EvalLocalScalarType::Float => abi::emit_float_result_to_int_result(ctx.emitter), + EvalLocalScalarType::Null | EvalLocalScalarType::Bool | EvalLocalScalarType::String => { + unreachable!("numeric int coercion only accepts int/float local scalar values") + } + } +} + +/// Emits a short-circuiting local scalar `&&` expression. +fn emit_eval_local_scalar_and( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + let false_label = ctx.next_label("eval_local_and_false"); + let done_label = ctx.next_label("eval_local_and_done"); + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + abi::emit_branch_if_int_result_zero(ctx.emitter, &false_label); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth); + abi::emit_branch_if_int_result_zero(ctx.emitter, &false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&false_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + ctx.emitter.label(&done_label); +} + +/// Emits a short-circuiting local scalar `||` expression. +fn emit_eval_local_scalar_or( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + left: &EvalLocalScalarExpr, + right: &EvalLocalScalarExpr, + scratch_depth: usize, +) { + let true_label = ctx.next_label("eval_local_or_true"); + let done_label = ctx.next_label("eval_local_or_done"); + emit_eval_local_scalar_expr_value(ctx, program, left, scratch_depth); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &true_label); + emit_eval_local_scalar_expr_value(ctx, program, right, scratch_depth); + abi::emit_branch_if_int_result_nonzero(ctx.emitter, &true_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&true_label); + abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 1); + ctx.emitter.label(&done_label); +} + +/// Emits unary numeric negation in the matching result register. +fn emit_eval_local_scalar_negate(ctx: &mut FunctionContext<'_>, ty: EvalLocalScalarType) { + if ty == EvalLocalScalarType::Float { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("fneg d0, d0"); // negate the local scalar floating-point result + } + Arch::X86_64 => { + ctx.emitter.instruction("xorpd xmm1, xmm1"); // materialize a zero float register for local scalar negation + ctx.emitter.instruction("subsd xmm1, xmm0"); // compute 0.0 minus the local scalar float + ctx.emitter.instruction("movsd xmm0, xmm1"); // move the negated local scalar float into the result register + } + } + return; + } + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("neg {}, {}", result_reg, result_reg)); //negate the local scalar integer result + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("neg {}", result_reg)); // negate the local scalar integer result + } + } +} + +/// Emits a unary integer bitwise-not in the result register. +fn emit_eval_local_scalar_bitnot(ctx: &mut FunctionContext<'_>) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("mvn {}, {}", result_reg, result_reg)); // invert every bit of the local scalar integer result + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("not {}", result_reg)); // invert every bit of the local scalar integer result + } + } +} + +/// Emits PHP boolean negation for an int/bool local scalar result. +fn emit_eval_local_scalar_not(ctx: &mut FunctionContext<'_>) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!("cmp {}, #0", result_reg)); // test local scalar truthiness against false + ctx.emitter.instruction(&format!("cset {}, eq", result_reg)); // materialize logical negation as 0 or 1 + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", result_reg, result_reg)); //test local scalar truthiness against false + ctx.emitter.instruction("sete al"); // materialize logical negation in the low byte + ctx.emitter + .instruction(&format!("movzx {}, al", result_reg)); // widen logical negation into the result register + } + } +} + +/// Emits the final arithmetic/comparison step for a local scalar binary operation. +fn emit_eval_local_scalar_binary_result( + ctx: &mut FunctionContext<'_>, + op: &EvalLocalScalarBinaryOp, + rhs_reg: &str, +) { + match op { + EvalLocalScalarBinaryOp::Add => emit_eval_local_scalar_arith(ctx, "add", "add", rhs_reg), + EvalLocalScalarBinaryOp::Sub => emit_eval_local_scalar_arith(ctx, "sub", "sub", rhs_reg), + EvalLocalScalarBinaryOp::Mul => emit_eval_local_scalar_arith(ctx, "mul", "imul", rhs_reg), + EvalLocalScalarBinaryOp::Mod => emit_eval_local_scalar_mod(ctx, rhs_reg), + EvalLocalScalarBinaryOp::BitAnd => emit_eval_local_scalar_arith(ctx, "and", "and", rhs_reg), + EvalLocalScalarBinaryOp::BitOr => emit_eval_local_scalar_arith(ctx, "orr", "or", rhs_reg), + EvalLocalScalarBinaryOp::BitXor => emit_eval_local_scalar_arith(ctx, "eor", "xor", rhs_reg), + EvalLocalScalarBinaryOp::ShiftLeft => { + emit_eval_local_scalar_shift(ctx, "lsl", "shl", rhs_reg) + } + EvalLocalScalarBinaryOp::ShiftRight => { + emit_eval_local_scalar_shift(ctx, "asr", "sar", rhs_reg) + } + EvalLocalScalarBinaryOp::Lt => emit_eval_local_scalar_cmp(ctx, rhs_reg, "lt", "l"), + EvalLocalScalarBinaryOp::Gt => emit_eval_local_scalar_cmp(ctx, rhs_reg, "gt", "g"), + EvalLocalScalarBinaryOp::LtEq => emit_eval_local_scalar_cmp(ctx, rhs_reg, "le", "le"), + EvalLocalScalarBinaryOp::GtEq => emit_eval_local_scalar_cmp(ctx, rhs_reg, "ge", "ge"), + EvalLocalScalarBinaryOp::Eq => emit_eval_local_scalar_cmp(ctx, rhs_reg, "eq", "e"), + EvalLocalScalarBinaryOp::NotEq => emit_eval_local_scalar_cmp(ctx, rhs_reg, "ne", "ne"), + EvalLocalScalarBinaryOp::And + | EvalLocalScalarBinaryOp::Div + | EvalLocalScalarBinaryOp::Or + | EvalLocalScalarBinaryOp::Concat => unreachable!("handled before final binary step"), + } +} + +/// Emits a target-aware integer arithmetic operation for local scalar AOT. +fn emit_eval_local_scalar_arith( + ctx: &mut FunctionContext<'_>, + aarch64_mnemonic: &str, + x86_64_mnemonic: &str, + rhs_reg: &str, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!( + "{} {}, {}, {}", + aarch64_mnemonic, result_reg, result_reg, rhs_reg + )); //compute the local scalar arithmetic result + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("{} {}, {}", x86_64_mnemonic, result_reg, rhs_reg)); + //update the local scalar arithmetic result + } + } +} + +/// Emits a target-aware variable-count integer shift for local scalar AOT. +fn emit_eval_local_scalar_shift( + ctx: &mut FunctionContext<'_>, + aarch64_mnemonic: &str, + x86_64_mnemonic: &str, + rhs_reg: &str, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction(&format!( + "{} {}, {}, {}", + aarch64_mnemonic, result_reg, result_reg, rhs_reg + )); // shift the local scalar integer by the evaluated count + } + Arch::X86_64 => { + ctx.emitter.instruction(&format!("mov rcx, {}", rhs_reg)); // move the local scalar shift count into x86_64's cl register + ctx.emitter + .instruction(&format!("{} {}, cl", x86_64_mnemonic, result_reg)); + // shift the local scalar integer by the low count byte + } + } +} + +/// Emits a target-aware signed modulo operation for local scalar AOT. +fn emit_eval_local_scalar_mod(ctx: &mut FunctionContext<'_>, rhs_reg: &str) { + let result_reg = abi::int_result_reg(ctx.emitter); + let zero_label = ctx.next_label("eval_local_mod_zero"); + let done_label = ctx.next_label("eval_local_mod_done"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + let quotient_reg = abi::tertiary_scratch_reg(ctx.emitter); + ctx.emitter + .instruction(&format!("cbz {}, {}", rhs_reg, zero_label)); //branch to the local scalar modulo zero guard + ctx.emitter.instruction(&format!( + "sdiv {}, {}, {}", + quotient_reg, result_reg, rhs_reg + )); //compute the local scalar signed quotient + ctx.emitter.instruction(&format!( + "msub {}, {}, {}, {}", + result_reg, quotient_reg, rhs_reg, result_reg + )); //compute the local scalar signed remainder + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&zero_label); + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + ctx.emitter.label(&done_label); + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", rhs_reg, rhs_reg)); // test whether the local scalar modulo divisor is zero + ctx.emitter.instruction(&format!("je {}", zero_label)); // branch to the local scalar modulo zero guard + ctx.emitter.instruction("cqo"); // sign-extend the local scalar dividend before division + ctx.emitter.instruction(&format!("idiv {}", rhs_reg)); // divide local scalar integers + ctx.emitter.instruction(&format!("mov {}, rdx", result_reg)); // move the local scalar remainder into the result register + abi::emit_jump(ctx.emitter, &done_label); + ctx.emitter.label(&zero_label); + abi::emit_load_int_immediate(ctx.emitter, result_reg, 0); + ctx.emitter.label(&done_label); + } + } +} + +/// Emits a target-aware integer comparison for local scalar AOT. +fn emit_eval_local_scalar_cmp( + ctx: &mut FunctionContext<'_>, + rhs_reg: &str, + aarch64_condition: &str, + x86_64_condition: &str, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cmp {}, {}", result_reg, rhs_reg)); //compare local scalar operands + ctx.emitter + .instruction(&format!("cset {}, {}", result_reg, aarch64_condition)); + //materialize the local scalar comparison result + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("cmp {}, {}", result_reg, rhs_reg)); //compare local scalar operands + ctx.emitter + .instruction(&format!("set{} al", x86_64_condition)); //materialize the local scalar comparison byte + ctx.emitter + .instruction(&format!("movzx {}, al", result_reg)); // widen the local scalar comparison result + } + } +} + +/// Boxes the return expression, or null when eval exits without `return`. +fn emit_eval_local_scalar_return_cell( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + value: Option<&EvalLocalScalarExpr>, + boxing: EvalLocalScalarBoxing, +) { + if let Some(value) = value { + emit_eval_local_scalar_expr_value(ctx, program, value, 0); + emit_eval_local_scalar_box_current_result(ctx, value.ty, boxing); + } else { + emit_eval_local_scalar_null_cell(ctx, boxing); + } + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.result_cell_offset(), + ); +} + +/// Emits a boxed eval null cell into the result register. +fn emit_eval_local_scalar_null_cell(ctx: &mut FunctionContext<'_>, boxing: EvalLocalScalarBoxing) { + match boxing { + EvalLocalScalarBoxing::EvalRuntime => { + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLocalScalarBoxing::CoreRuntime => emit_eval_local_scalar_core_null_cell(ctx), + } +} + +/// Boxes the current int/bool result register as an eval Mixed cell. +fn emit_eval_local_scalar_box_current_result( + ctx: &mut FunctionContext<'_>, + ty: EvalLocalScalarType, + boxing: EvalLocalScalarBoxing, +) { + if matches!(boxing, EvalLocalScalarBoxing::CoreRuntime) { + if ty == EvalLocalScalarType::Null { + emit_eval_local_scalar_core_null_cell(ctx); + return; + } + emit_box_current_value_as_mixed(ctx.emitter, &ty.php_type()); + return; + } + if ty == EvalLocalScalarType::Null { + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol); + return; + } + if ty == EvalLocalScalarType::String { + emit_eval_local_scalar_eval_runtime_string_cell(ctx); + return; + } + if ty == EvalLocalScalarType::Float { + emit_eval_local_scalar_eval_runtime_float_cell(ctx); + return; + } + let result_reg = abi::int_result_reg(ctx.emitter); + let arg_reg = abi::int_arg_reg_name(ctx.emitter.target, 0); + abi::emit_reg_move(ctx.emitter, arg_reg, result_reg); + let helper = match ty { + EvalLocalScalarType::Int => "__elephc_eval_value_int", + EvalLocalScalarType::Bool => "__elephc_eval_value_bool", + EvalLocalScalarType::Null | EvalLocalScalarType::Float | EvalLocalScalarType::String => { + unreachable!("non-integer eval cells are handled before integer helpers") + } + }; + let symbol = ctx.emitter.target.extern_symbol(helper); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Boxes the current float result register with the eval runtime float helper. +fn emit_eval_local_scalar_eval_runtime_float_cell(ctx: &mut FunctionContext<'_>) { + let float_arg = abi::float_arg_reg_name(ctx.emitter.target, 0); + abi::emit_reg_move(ctx.emitter, float_arg, abi::float_result_reg(ctx.emitter)); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_float"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Boxes the current string result registers with the eval runtime string helper. +fn emit_eval_local_scalar_eval_runtime_string_cell(ctx: &mut FunctionContext<'_>) { + let (ptr_reg, len_reg) = abi::string_result_regs(ctx.emitter); + let ptr_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + let len_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_reg_move(ctx.emitter, ptr_arg, ptr_reg); + abi::emit_reg_move(ctx.emitter, len_arg, len_reg); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_string"); + abi::emit_call_label(ctx.emitter, &symbol); +} + +/// Boxes PHP null using the core runtime Mixed helper, without eval bridge symbols. +fn emit_eval_local_scalar_core_null_cell(ctx: &mut FunctionContext<'_>) { + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("mov x0, #8"); // materialize the core Mixed null runtime tag + ctx.emitter.instruction("mov x1, #0"); // null has no low payload word + ctx.emitter.instruction("mov x2, #0"); // null has no high payload word + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + } + Arch::X86_64 => { + ctx.emitter.instruction("mov rax, 8"); // materialize the core Mixed null runtime tag + ctx.emitter.instruction("xor edi, edi"); // null has no low payload word + ctx.emitter.instruction("xor esi, esi"); // null has no high payload word + abi::emit_call_label(ctx.emitter, "__rt_mixed_from_value"); + } + } +} + +/// Flushes defined local scalar slots into eval scope once native execution completes. +fn emit_eval_local_scalar_flush_defined_locals( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, +) { + for name in program.locals.keys() { + let skip_label = ctx.next_label("eval_local_flush_skip_undefined"); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.defined_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &skip_label); + emit_eval_local_scalar_load_local_value(ctx, program, name); + emit_eval_local_scalar_box_current_result( + ctx, + program.local_type(name), + EvalLocalScalarBoxing::EvalRuntime, + ); + abi::emit_store_to_sp( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + EVAL_TEMP_CELL_OFFSET, + ); + if main_name_uses_eval_global_scope(ctx, name) { + emit_eval_global_scope_set_name(ctx, name, EVAL_SCOPE_FLAG_OWNED); + } else { + emit_eval_scope_set_name(ctx, name, EVAL_SCOPE_FLAG_OWNED); + } + ctx.emitter.label(&skip_label); + } +} + +/// Flushes defined local scalar slots directly into caller locals when no eval scope is needed. +fn emit_eval_local_scalar_flush_direct_locals( + ctx: &mut FunctionContext<'_>, + program: &EvalLocalScalarAotProgram, + targets: &BTreeMap>, +) -> Result<()> { + for name in program.locals.keys() { + let Some(target) = targets.get(name) else { + continue; + }; + let Some(slot) = target else { + continue; + }; + let skip_label = ctx.next_label("eval_local_direct_flush_skip_undefined"); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_result_reg(ctx.emitter), + program.defined_offset(name), + ); + abi::emit_branch_if_int_result_zero(ctx.emitter, &skip_label); + emit_eval_local_scalar_load_local_value(ctx, program, name); + emit_eval_local_scalar_store_current_result_to_direct_local( + ctx, + *slot, + program.local_type(name), + )?; + ctx.emitter.label(&skip_label); + } + Ok(()) +} + +/// Stores the current local-scalar result in one caller local slot. +fn emit_eval_local_scalar_store_current_result_to_direct_local( + ctx: &mut FunctionContext<'_>, + slot: LocalSlotId, + local_type: EvalLocalScalarType, +) -> Result<()> { + match ctx.local_php_type(slot)?.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => { + emit_eval_local_scalar_box_current_result( + ctx, + local_type, + EvalLocalScalarBoxing::CoreRuntime, + ); + ctx.store_current_result_to_local(slot) + } + PhpType::TaggedScalar => match local_type { + EvalLocalScalarType::Null => { + crate::codegen::sentinels::emit_tagged_scalar_null(ctx.emitter); + ctx.store_current_result_to_local(slot) + } + EvalLocalScalarType::Int => { + crate::codegen::sentinels::emit_tagged_scalar_from_int_result(ctx.emitter); + ctx.store_current_result_to_local(slot) + } + EvalLocalScalarType::Bool + | EvalLocalScalarType::Float + | EvalLocalScalarType::String => Err(CodegenIrError::unsupported( + "direct local scalar eval sync to tagged scalar".to_string(), + )), + }, + target_ty + if eval_local_scalar_direct_sync_type_supported(target_ty.clone(), local_type) => + { + ctx.store_current_result_to_local(slot) + } + target_ty => Err(CodegenIrError::unsupported(format!( + "direct local scalar eval sync to PHP type {:?}", + target_ty + ))), + } +} + +/// Boxes and echoes one AOT value, releasing the temporary box afterward. +fn emit_eval_literal_aot_echo( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotExpr, + stack_depth: usize, +) { + emit_eval_literal_aot_expr_cell(ctx, value, stack_depth); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_call_label(ctx.emitter, "__rt_mixed_write_stdout"); + abi::emit_pop_reg(ctx.emitter, result_reg); + abi::emit_call_label(ctx.emitter, "__rt_decref_mixed"); +} + +/// Stores one boxed scalar into the local or global eval scope used by bridge reloads. +fn emit_eval_literal_aot_scope_store( + ctx: &mut FunctionContext<'_>, + name: &str, + value: &EvalLiteralAotExpr, + stack_depth: usize, +) -> Result<()> { + emit_eval_literal_aot_expr_cell(ctx, value, stack_depth); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + if main_name_uses_eval_global_scope(ctx, name) { + emit_eval_global_scope_set_name(ctx, name, EVAL_SCOPE_FLAG_OWNED); + } else { + emit_eval_scope_set_name(ctx, name, EVAL_SCOPE_FLAG_OWNED); + } + Ok(()) +} + +/// Emits one AOT expression as an owned boxed Mixed value in the result register. +fn emit_eval_literal_aot_expr_cell( + ctx: &mut FunctionContext<'_>, + value: &EvalLiteralAotExpr, + stack_depth: usize, +) { + match value { + EvalLiteralAotExpr::Scalar(value) => emit_eval_literal_aot_scalar_cell(ctx, value), + EvalLiteralAotExpr::LoadVar(name) => { + emit_eval_literal_aot_scope_load(ctx, name, stack_depth); + } + EvalLiteralAotExpr::Binary { op, left, right } => { + emit_eval_literal_aot_binary_cell(ctx, op, left, right, stack_depth); + } + } +} + +/// Emits a boxed-Mixed binary operation and releases owned operand cells. +fn emit_eval_literal_aot_binary_cell( + ctx: &mut FunctionContext<'_>, + op: &EvalLiteralAotBinaryOp, + left: &EvalLiteralAotExpr, + right: &EvalLiteralAotExpr, + stack_depth: usize, +) { + let result_reg = abi::int_result_reg(ctx.emitter); + emit_eval_literal_aot_expr_cell(ctx, left, stack_depth); + abi::emit_push_reg(ctx.emitter, result_reg); + emit_eval_literal_aot_expr_cell(ctx, right, stack_depth + 1); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + 16, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + 0, + ); + let symbol = ctx.emitter.target.extern_symbol(op.helper()); + abi::emit_call_label(ctx.emitter, &symbol); + abi::emit_push_reg(ctx.emitter, result_reg); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 16); + abi::emit_call_label(ctx.emitter, "__rt_decref_mixed"); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 32); + abi::emit_call_label(ctx.emitter, "__rt_decref_mixed"); + abi::emit_pop_reg(ctx.emitter, result_reg); + abi::emit_release_temporary_stack(ctx.emitter, 32); +} + +/// Loads one variable from the eval scope and retains it as an owned Mixed cell. +fn emit_eval_literal_aot_scope_load(ctx: &mut FunctionContext<'_>, name: &str, stack_depth: usize) { + let out_cell_offset = stack_depth * 16; + let out_flags_offset = out_cell_offset + 8; + if main_name_uses_eval_global_scope(ctx, name) { + emit_eval_global_scope_get_name(ctx, name, out_cell_offset, out_flags_offset); + } else { + emit_eval_scope_get_name(ctx, name, out_cell_offset, out_flags_offset); + } + let missing = ctx.next_label("eval_literal_aot_load_missing"); + let done = ctx.next_label("eval_literal_aot_load_done"); + emit_branch_if_scope_entry_missing_at(ctx, out_flags_offset, &missing); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, out_cell_offset); + let retain = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_retain"); + abi::emit_call_label(ctx.emitter, &retain); + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&missing); + let null = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &null); + ctx.emitter.label(&done); +} + +/// Boxes one scalar AOT value into the standard eval `Mixed` return register. +fn emit_eval_literal_aot_scalar_cell(ctx: &mut FunctionContext<'_>, value: &EvalLiteralAotScalar) { + match value { + EvalLiteralAotScalar::Null => { + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_null"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLiteralAotScalar::Bool(value) => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + i64::from(*value), + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_bool"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLiteralAotScalar::Int(value) => { + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + *value, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_value_int"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLiteralAotScalar::Float(value) => { + let label = ctx.data.add_float(*value); + abi::emit_load_symbol_to_reg( + ctx.emitter, + abi::float_arg_reg_name(ctx.emitter.target, 0), + &label, + 0, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_float"); + abi::emit_call_label(ctx.emitter, &symbol); + } + EvalLiteralAotScalar::String(value) => { + let (label, len) = ctx.data.add_string(value.as_bytes()); + abi::emit_symbol_address( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 0), + &label, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 1), + len as i64, + ); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_value_string"); + abi::emit_call_label(ctx.emitter, &symbol); + } + } +} + +/// Calls `__elephc_eval_scope_get` for a direct AOT local-scope load. +fn emit_eval_scope_get_name( + ctx: &mut FunctionContext<'_>, + name: &str, + out_cell_offset: usize, + out_flags_offset: usize, +) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, out_cell_offset); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, out_flags_offset); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); } -/// Static array key metadata for a native callable default registered with eval. -enum EvalNativeCallableArrayDefaultKey { - Int(i64), - String(String), +/// Calls `__elephc_eval_scope_get` for a direct AOT global-scope load. +fn emit_eval_global_scope_get_name( + ctx: &mut FunctionContext<'_>, + name: &str, + out_cell_offset: usize, + out_flags_offset: usize, +) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + load_eval_global_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + let out_cell_arg = abi::int_arg_reg_name(ctx.emitter.target, 3); + abi::emit_temporary_stack_address(ctx.emitter, out_cell_arg, out_cell_offset); + let out_flags_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); + abi::emit_temporary_stack_address(ctx.emitter, out_flags_arg, out_flags_offset); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_get"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); } -/// Constructor argument metadata for an object-valued native callable default. -struct EvalNativeCallableObjectDefaultArg { - name: Option, - default: EvalNativeCallableDefault, +/// Calls `__elephc_eval_scope_set` for a direct AOT local-scope store. +fn emit_eval_scope_set_name(ctx: &mut FunctionContext<'_>, name: &str, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + load_eval_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); + abi::emit_call_label(ctx.emitter, &symbol); + emit_eval_status_check(ctx); } -/// Lowers `eval($code)` to the eval bridge ABI and leaves the eval return cell in result registers. -pub(super) fn lower_eval(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { - super::ensure_arg_count(inst, "eval", 1)?; - emit_eval_literal_aot_marker(ctx, inst)?; - let code = expect_operand(inst, 0)?; - let ty = ctx.load_value_to_result(code)?.codegen_repr(); - if ty != PhpType::Str { - return Err(CodegenIrError::unsupported(format!( - "eval() argument lowering for PHP type {:?}", - ty - ))); - } - - abi::emit_reserve_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); - save_eval_code_string(ctx); - ensure_eval_context(ctx)?; - set_eval_call_site(ctx, inst); - ensure_eval_scope(ctx)?; - ensure_eval_global_scope(ctx)?; - let sync_locals = eval_sync_locals(ctx); - let sync_globals = eval_sync_globals(ctx); - let global_aliases = eval_global_aliases(ctx); - flush_eval_scope_locals(ctx, &sync_locals)?; - flush_eval_global_scope(ctx, &sync_globals)?; - mark_eval_scope_global_aliases(ctx, &global_aliases); - set_eval_context_global_scope(ctx); - let pushed_class_scope = push_eval_context_class_scope(ctx)?; - load_eval_context_to_arg(ctx, 0); - load_eval_scope_to_arg(ctx, 1); - move_saved_eval_code_to_eval_args(ctx); - let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 4); - abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_execute"); +/// Calls `__elephc_eval_scope_set` for a direct AOT global-scope store. +fn emit_eval_global_scope_set_name(ctx: &mut FunctionContext<'_>, name: &str, flags: i64) { + let (name_label, name_len) = ctx.data.add_string(name.as_bytes()); + load_eval_global_scope_to_arg(ctx, 0); + let name_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); + abi::emit_symbol_address(ctx.emitter, name_arg, &name_label); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 2), + name_len as i64, + ); + abi::emit_load_temporary_stack_slot( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 3), + EVAL_TEMP_CELL_OFFSET, + ); + abi::emit_load_int_immediate( + ctx.emitter, + abi::int_arg_reg_name(ctx.emitter.target, 4), + flags, + ); + let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_scope_set"); abi::emit_call_label(ctx.emitter, &symbol); - pop_eval_context_class_scope(ctx, pushed_class_scope); emit_eval_status_check(ctx); - let result_reg = abi::int_result_reg(ctx.emitter); - abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_RESULT_VALUE_CELL_OFFSET); - abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); - reload_eval_scope_locals(ctx, &sync_locals)?; - reload_eval_global_scope(ctx, &sync_globals)?; - abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); - abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); - store_if_result(ctx, inst) } /// Emits an assembly marker for literal eval fragments that still use the bridge fallback. -fn emit_eval_literal_aot_marker( - ctx: &mut FunctionContext<'_>, - inst: &Instruction, -) -> Result<()> { - if inst.op != Op::EvalLiteralCall { - return Ok(()); - } - let Some(Immediate::Data(data)) = inst.immediate else { +fn emit_eval_literal_aot_marker(ctx: &mut FunctionContext<'_>, inst: &Instruction) -> Result<()> { + let Some(fragment) = eval_literal_fragment(ctx, inst)? else { return Ok(()); }; - let fragment = ctx - .module - .data - .strings - .get(data.as_raw() as usize) - .ok_or_else(|| CodegenIrError::missing_entry("data string", data.as_raw()))?; + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + &fragment, + ctx.module.source_path.as_deref(), + |name, args| eval_literal_static_function_supported_by_codegen(ctx, name, args), + |receiver, method, args| { + eval_literal_static_method_supported_by_codegen(ctx, receiver, method, args) + }, + ); + let reason = plan + .fallback_reason() + .map(crate::eval_aot::EvalAotFallbackReason::description) + .unwrap_or("bridge fallback required"); ctx.emitter.comment(&format!( - "eval literal AOT candidate ({} bytes), using bridge fallback", - fragment.len() + "eval literal AOT fallback: {} ({} bytes), using bridge fallback", + reason, + fragment.len(), )); Ok(()) } @@ -568,7 +5223,10 @@ pub(super) fn lower_eval_method_call( abi::emit_temporary_stack_address(ctx.emitter, pack_arg, args_offset); let out_arg = abi::int_arg_reg_name(ctx.emitter.target, 5); abi::emit_temporary_stack_address(ctx.emitter, out_arg, 0); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_method_call"); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_method_call"); abi::emit_call_label(ctx.emitter, &symbol); emit_eval_status_check(ctx); let result_reg = abi::int_result_reg(ctx.emitter); @@ -830,7 +5488,10 @@ pub(super) fn lower_eval_is_callable( load_eval_context_to_arg(ctx, 0); let callback_arg = abi::int_arg_reg_name(ctx.emitter.target, 1); abi::emit_load_temporary_stack_slot(ctx.emitter, callback_arg, EVAL_TEMP_CELL_OFFSET); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_is_callable"); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_is_callable"); abi::emit_call_label(ctx.emitter, &symbol); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); box_eval_bool_result_if_mixed(ctx, inst); @@ -860,7 +5521,10 @@ pub(super) fn lower_eval_member_exists( abi::int_arg_reg_name(ctx.emitter.target, 3), lookup_kind, ); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_member_exists"); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_member_exists"); abi::emit_call_label(ctx.emitter, &symbol); abi::emit_release_temporary_stack(ctx.emitter, EVAL_STACK_BYTES); box_eval_bool_result_if_mixed(ctx, inst); @@ -977,7 +5641,10 @@ pub(super) fn lower_eval_object_is_a( abi::int_arg_reg_name(ctx.emitter.target, 4), i64::from(exclude_self), ); - let symbol = ctx.emitter.target.extern_symbol("__elephc_eval_object_is_a"); + let symbol = ctx + .emitter + .target + .extern_symbol("__elephc_eval_object_is_a"); abi::emit_call_label(ctx.emitter, &symbol); abi::emit_jump(ctx.emitter, &done_label); @@ -1481,14 +6148,14 @@ fn emit_eval_mixed_result_as_tagged_scalar(ctx: &mut FunctionContext<'_>) { abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x9, x0"); // preserve the unboxed eval result tag before moving the payload - ctx.emitter.instruction("mov x0, x1"); // place the unboxed eval payload into the tagged-scalar payload register - ctx.emitter.instruction("mov x1, x9"); // place the unboxed eval tag into the tagged-scalar tag register + ctx.emitter.instruction("mov x9, x0"); // preserve the unboxed eval result tag before moving the payload + ctx.emitter.instruction("mov x0, x1"); // place the unboxed eval payload into the tagged-scalar payload register + ctx.emitter.instruction("mov x1, x9"); // place the unboxed eval tag into the tagged-scalar tag register } Arch::X86_64 => { - ctx.emitter.instruction("mov r10, rax"); // preserve the unboxed eval result tag before moving the payload - ctx.emitter.instruction("mov rax, rdi"); // place the unboxed eval payload into the tagged-scalar payload register - ctx.emitter.instruction("mov rdx, r10"); // place the unboxed eval tag into the tagged-scalar tag register + ctx.emitter.instruction("mov r10, rax"); // preserve the unboxed eval result tag before moving the payload + ctx.emitter.instruction("mov rax, rdi"); // place the unboxed eval payload into the tagged-scalar payload register + ctx.emitter.instruction("mov rdx, r10"); // place the unboxed eval tag into the tagged-scalar tag register } } } @@ -1504,10 +6171,10 @@ fn emit_eval_unbox_mixed_to_owned_result(ctx: &mut FunctionContext<'_>, result_t fn emit_eval_move_unboxed_low_payload_to_result(ctx: &mut FunctionContext<'_>) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("mov x0, x1"); // return the unboxed eval low payload as the concrete result + ctx.emitter.instruction("mov x0, x1"); // return the unboxed eval low payload as the concrete result } Arch::X86_64 => { - ctx.emitter.instruction("mov rax, rdi"); // return the unboxed eval low payload as the concrete result + ctx.emitter.instruction("mov rax, rdi"); // return the unboxed eval low payload as the concrete result } } } @@ -1560,14 +6227,12 @@ fn eval_class_relation_kind(name: &str) -> Result { fn emit_branch_if_eval_unboxed_not_object(ctx: &mut FunctionContext<'_>, label: &str) { match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed value contains an object - ctx.emitter - .instruction(&format!("b.ne {}", label)); // non-object values use the native false/empty fallback + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the Mixed value contains an object + ctx.emitter.instruction(&format!("b.ne {}", label)); // non-object values use the native false/empty fallback } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed value contains an object - ctx.emitter - .instruction(&format!("jne {}", label)); // non-object values use the native false/empty fallback + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the Mixed value contains an object + ctx.emitter.instruction(&format!("jne {}", label)); // non-object values use the native false/empty fallback } } } @@ -1577,24 +6242,18 @@ fn emit_validate_eval_dynamic_instanceof_target(ctx: &mut FunctionContext<'_>, l let ok_label = ctx.next_label("eval_object_is_a_dynamic_target_ok"); match ctx.emitter.target.arch { Arch::AArch64 => { - ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string - ctx.emitter - .instruction(&format!("b.eq {}", ok_label)); // accept string targets for dynamic instanceof - ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object - ctx.emitter - .instruction(&format!("b.eq {}", ok_label)); // accept object targets for dynamic instanceof - ctx.emitter - .instruction(&format!("b {}", label)); // reject every other dynamic instanceof target kind + ctx.emitter.instruction("cmp x0, #1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter.instruction(&format!("b.eq {}", ok_label)); // accept string targets for dynamic instanceof + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter.instruction(&format!("b.eq {}", ok_label)); // accept object targets for dynamic instanceof + ctx.emitter.instruction(&format!("b {}", label)); // reject every other dynamic instanceof target kind } Arch::X86_64 => { - ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string - ctx.emitter - .instruction(&format!("je {}", ok_label)); // accept string targets for dynamic instanceof - ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object - ctx.emitter - .instruction(&format!("je {}", ok_label)); // accept object targets for dynamic instanceof - ctx.emitter - .instruction(&format!("jmp {}", label)); // reject every other dynamic instanceof target kind + ctx.emitter.instruction("cmp rax, 1"); // runtime tag 1 means the dynamic target is a string + ctx.emitter.instruction(&format!("je {}", ok_label)); // accept string targets for dynamic instanceof + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the dynamic target is an object + ctx.emitter.instruction(&format!("je {}", ok_label)); // accept object targets for dynamic instanceof + ctx.emitter.instruction(&format!("jmp {}", label)); // reject every other dynamic instanceof target kind } } ctx.emitter.label(&ok_label); @@ -1605,11 +6264,11 @@ fn emit_branch_if_eval_c_int_negative(ctx: &mut FunctionContext<'_>, label: &str match ctx.emitter.target.arch { Arch::AArch64 => { let branch = format!("tbnz w0, #31, {}", label); - ctx.emitter.instruction(&branch); // branch when the C int result is the invalid-target sentinel + ctx.emitter.instruction(&branch); // branch when the C int result is the invalid-target sentinel } Arch::X86_64 => { - ctx.emitter.instruction("test eax, eax"); // set flags from the C int result - ctx.emitter.instruction(&format!("js {}", label)); // branch when the C int result is the invalid-target sentinel + ctx.emitter.instruction("test eax, eax"); // set flags from the C int result + ctx.emitter.instruction(&format!("js {}", label)); // branch when the C int result is the invalid-target sentinel } } } @@ -1617,7 +6276,7 @@ fn emit_branch_if_eval_c_int_negative(ctx: &mut FunctionContext<'_>, label: &str /// Reorders an unboxed eval string cell into the target string result registers. fn emit_eval_unboxed_string_result(ctx: &mut FunctionContext<'_>) { if ctx.emitter.target.arch == Arch::X86_64 { - ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the x86_64 string-result register + ctx.emitter.instruction("mov rax, rdi"); // move the unboxed string pointer into the x86_64 string-result register } } @@ -1859,9 +6518,11 @@ fn eval_native_interface_property_registrations( let Some(contract) = interface_info.properties.get(property_name) else { continue; }; - let Some(registration) = - eval_native_interface_property_registration(interface_name, property_name, contract) - else { + let Some(registration) = eval_native_interface_property_registration( + interface_name, + property_name, + contract, + ) else { continue; }; registrations.push(registration); @@ -1878,7 +6539,10 @@ fn eval_native_abstract_property_registrations( let mut classes = ctx.module.class_infos.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { - let mut property_names = class_info.abstract_property_hooks.keys().collect::>(); + let mut property_names = class_info + .abstract_property_hooks + .keys() + .collect::>(); property_names.sort(); for property_name in property_names { let Some(contract) = class_info.abstract_property_hooks.get(property_name) else { @@ -2364,8 +7028,9 @@ fn collect_eval_native_static_methods( let mut methods = class_info.static_methods.iter().collect::>(); methods.sort_by_key(|(method, _)| method.as_str()); for (method_name, signature) in methods { - let bridge_supported = class_static_method_visibility_bridge_supported(class_info, method_name) - && method_signature_can_bridge_with_eval(signature); + let bridge_supported = + class_static_method_visibility_bridge_supported(class_info, method_name) + && method_signature_can_bridge_with_eval(signature); registrations.push(EvalNativeMethodRegistration { class_name: class_name.to_string(), method_name: method_name.clone(), @@ -2457,9 +7122,10 @@ fn function_has_eval_metadata(function: &Function) -> bool { /// Returns true when eval can dispatch a native function through the generated bridge. fn function_signature_can_bridge_with_eval(function: &Function) -> bool { - function.params.iter().all(|param| { - !param.by_ref || eval_native_function_ref_param_supported(¶m.php_type) - }) + function + .params + .iter() + .all(|param| !param.by_ref || eval_native_function_ref_param_supported(¶m.php_type)) } /// Returns true when a native function by-reference parameter can use eval bridge staging. @@ -2706,14 +7372,12 @@ fn eval_native_constant_expression_default( ExprKind::BinaryOp { left, op, right } => { eval_native_binary_expression_default(left, op, right, default_context, depth + 1) } - ExprKind::Not(inner) => { - eval_native_default_truthy(&eval_native_callable_default_at( - inner, - default_context, - depth + 1, - )?) - .map(|value| eval_native_bool_default(!value)) - } + ExprKind::Not(inner) => eval_native_default_truthy(&eval_native_callable_default_at( + inner, + default_context, + depth + 1, + )?) + .map(|value| eval_native_bool_default(!value)), ExprKind::BitNot(inner) => eval_native_default_int(inner, default_context, depth + 1) .map(|value| eval_native_int_default(!value)), ExprKind::NullCoalesce { value, default } => { @@ -2842,9 +7506,7 @@ fn eval_native_numeric_binary_default( BinOp::Add => left.checked_add(right).map(eval_native_int_default), BinOp::Sub => left.checked_sub(right).map(eval_native_int_default), BinOp::Mul => left.checked_mul(right).map(eval_native_int_default), - BinOp::Div if right != 0 => { - Some(eval_native_float_default(left as f64 / right as f64)) - } + BinOp::Div if right != 0 => Some(eval_native_float_default(left as f64 / right as f64)), BinOp::Pow => { let value = (left as f64).powf(right as f64); value.is_finite().then(|| eval_native_float_default(value)) @@ -2924,9 +7586,7 @@ fn eval_native_default_truthy(default: &EvalNativeCallableDefault) -> Option Some(false), - EvalNativeCallableDefault::String(value) => { - Some(!value.is_empty() && value != "0") - } + EvalNativeCallableDefault::String(value) => Some(!value.is_empty() && value != "0"), EvalNativeCallableDefault::Array(_) | EvalNativeCallableDefault::Object { .. } => None, EvalNativeCallableDefault::Scalar { .. } => None, } @@ -3069,11 +7729,7 @@ fn eval_native_array_default( } default_elements.push(EvalNativeCallableArrayDefaultElement { key: None, - default: eval_native_callable_default_at( - element, - default_context, - depth + 1, - )?, + default: eval_native_callable_default_at(element, default_context, depth + 1)?, }); } Some(EvalNativeCallableDefault::Array(default_elements)) @@ -3087,11 +7743,7 @@ fn eval_native_array_default( default_context, depth + 1, )?), - default: eval_native_callable_default_at( - value, - default_context, - depth + 1, - )?, + default: eval_native_callable_default_at(value, default_context, depth + 1)?, }); } Some(EvalNativeCallableDefault::Array(default_elements)) @@ -3117,7 +7769,9 @@ fn eval_native_array_default_key( EvalNativeCallableDefault::Scalar { kind: NATIVE_DEFAULT_BOOL, payload, - } => Some(EvalNativeCallableArrayDefaultKey::Int((payload != 0) as i64)), + } => Some(EvalNativeCallableArrayDefaultKey::Int( + (payload != 0) as i64, + )), EvalNativeCallableDefault::Scalar { kind: NATIVE_DEFAULT_INT, payload, @@ -3128,9 +7782,7 @@ fn eval_native_array_default_key( } => Some(EvalNativeCallableArrayDefaultKey::Int( f64::from_bits(payload as u64) as i64, )), - EvalNativeCallableDefault::String(value) => { - eval_native_string_array_default_key(&value) - } + EvalNativeCallableDefault::String(value) => eval_native_string_array_default_key(&value), _ => None, } } @@ -3721,8 +8373,7 @@ fn register_eval_native_method( ); } } - let default_context = - EvalNativeDefaultContext::for_class(ctx.module, ®istration.class_name); + let default_context = EvalNativeDefaultContext::for_class(ctx.module, ®istration.class_name); for (index, default) in registration.signature.defaults.iter().enumerate() { let Some(default) = default .as_ref() @@ -4086,9 +8737,9 @@ fn register_eval_native_method_param_default( default_len as i64, ); if is_static { - ctx.emitter - .target - .extern_symbol("__elephc_eval_register_native_static_method_param_default_array") + ctx.emitter.target.extern_symbol( + "__elephc_eval_register_native_static_method_param_default_array", + ) } else { ctx.emitter .target @@ -4170,8 +8821,7 @@ fn register_eval_native_constructor( ); } } - let default_context = - EvalNativeDefaultContext::for_class(ctx.module, ®istration.class_name); + let default_context = EvalNativeDefaultContext::for_class(ctx.module, ®istration.class_name); for (index, default) in registration.signature.defaults.iter().enumerate() { let Some(default) = default .as_ref() @@ -5393,9 +10043,25 @@ fn eval_sync_locals(ctx: &FunctionContext<'_>) -> Vec { .collect() } +/// Keeps only eval-sync locals whose PHP name appears in `names`. +fn filter_eval_sync_locals_by_name( + locals: Vec, + names: &BTreeSet, +) -> Vec { + locals + .into_iter() + .filter(|local| names.contains(&local.name)) + .collect() +} + /// Returns true when a local name is backed by program-global storage during eval. fn local_uses_eval_global_sync(ctx: &FunctionContext<'_>, name: Option<&str>) -> bool { - ctx.is_main && name.is_some_and(|name| ctx.has_global_name(name)) + name.is_some_and(|name| main_name_uses_eval_global_scope(ctx, name)) +} + +/// Returns true when a main-scope name has actual EIR global storage to synchronize. +fn main_name_uses_eval_global_scope(ctx: &FunctionContext<'_>, name: &str) -> bool { + ctx.is_main && eval_sync_global_type(ctx, name).is_some() } /// Collects caller-scope `global` aliases that eval fragments inherit by name. @@ -5434,6 +10100,17 @@ fn eval_sync_globals(ctx: &FunctionContext<'_>) -> Vec { globals } +/// Keeps only eval-sync globals whose PHP name appears in `names`. +fn filter_eval_sync_globals_by_name( + globals: Vec, + names: &BTreeSet, +) -> Vec { + globals + .into_iter() + .filter(|global| names.contains(&global.name)) + .collect() +} + /// Adds a process superglobal to eval global sync unless normal globals already include it. fn push_eval_process_superglobal(globals: &mut Vec, name: &str, ty: PhpType) { if globals.iter().any(|global| global.name == name) { @@ -5526,6 +10203,8 @@ fn eval_sync_type_supported(ty: &PhpType) -> bool { | PhpType::Bool | PhpType::Float | PhpType::Str + | PhpType::Array(_) + | PhpType::AssocArray { .. } | PhpType::Object(_) | PhpType::Mixed | PhpType::Union(_) @@ -5563,6 +10242,19 @@ fn flush_eval_global_scope( Ok(()) } +/// Flushes global-backed variables into the local eval scope for scope-read EIR AOT. +fn flush_eval_globals_to_local_scope(ctx: &mut FunctionContext<'_>, globals: &[EvalSyncGlobal]) { + for global in globals { + load_global_to_result(ctx, global); + if !matches!(global.ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + emit_box_current_value_as_mixed(ctx.emitter, &global.ty); + } + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_store_to_sp(ctx.emitter, result_reg, EVAL_TEMP_CELL_OFFSET); + emit_eval_scope_set_name(ctx, &global.name, scope_set_flags_for_type(&global.ty)); + } +} + /// Loads a program-global symbol into result registers using its inferred type. fn load_global_to_result(ctx: &mut FunctionContext<'_>, global: &EvalSyncGlobal) { let symbol = ir_global_symbol(&global.name); @@ -5701,6 +10393,27 @@ fn reload_eval_global_scope( Ok(()) } +/// Reloads synchronized program globals from the local eval scope after EIR eval AOT. +fn reload_eval_globals_from_local_scope( + ctx: &mut FunctionContext<'_>, + globals: &[EvalSyncGlobal], +) -> Result<()> { + for global in globals { + emit_eval_scope_get_name(ctx, &global.name, 0, 8); + let missing = ctx.next_label("eval_global_reload_missing"); + let done = ctx.next_label("eval_global_reload_done"); + emit_branch_if_scope_entry_missing(ctx, &missing); + let result_reg = abi::int_result_reg(ctx.emitter); + abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, 0); + store_mixed_scope_cell_to_global(ctx, global)?; + abi::emit_jump(ctx.emitter, &done); + ctx.emitter.label(&missing); + store_missing_scope_entry_to_global(ctx, global)?; + ctx.emitter.label(&done); + } + Ok(()) +} + /// Calls `__elephc_eval_scope_get` and stores out cell/flags at the start of eval scratch. fn emit_eval_scope_get(ctx: &mut FunctionContext<'_>, local: &EvalSyncLocal) { let (name_label, name_len) = ctx.data.add_string(local.name.as_bytes()); @@ -5743,8 +10456,17 @@ fn emit_eval_global_scope_get(ctx: &mut FunctionContext<'_>, global: &EvalSyncGl /// Branches to `label` when the latest scope-get flags do not mark a visible value. fn emit_branch_if_scope_entry_missing(ctx: &mut FunctionContext<'_>, label: &str) { + emit_branch_if_scope_entry_missing_at(ctx, 8, label); +} + +/// Branches to `label` when the scope-get flags at `flags_offset` do not mark a visible value. +fn emit_branch_if_scope_entry_missing_at( + ctx: &mut FunctionContext<'_>, + flags_offset: usize, + label: &str, +) { let flags_reg = abi::secondary_scratch_reg(ctx.emitter); - abi::emit_load_temporary_stack_slot(ctx.emitter, flags_reg, 8); + abi::emit_load_temporary_stack_slot(ctx.emitter, flags_reg, flags_offset); match ctx.emitter.target.arch { Arch::AArch64 => { ctx.emitter diff --git a/src/codegen_support/runtime/emitters.rs b/src/codegen_support/runtime/emitters.rs index f74d121b23..677e210390 100644 --- a/src/codegen_support/runtime/emitters.rs +++ b/src/codegen_support/runtime/emitters.rs @@ -13,6 +13,7 @@ use super::buffers; use super::callables; use super::diagnostics; use super::eval_bridge; +use super::eval_scope; use super::exceptions; use super::fibers; use super::generators; @@ -321,8 +322,10 @@ pub(crate) fn emit_runtime(emitter: &mut Emitter, features: RuntimeFeatures) { arrays::emit_mixed_write_stdout(emitter); arrays::emit_object_free_deep(emitter); arrays::emit_refcount(emitter); - if features.eval { + if features.eval_bridge { eval_bridge::emit_eval_bridge_runtime(emitter); + } else if features.eval_scope { + eval_scope::emit_eval_scope_runtime(emitter); } // SPL runtime-managed containers diff --git a/src/codegen_support/runtime/eval_scope.rs b/src/codegen_support/runtime/eval_scope.rs new file mode 100644 index 0000000000..a4d0101f0d --- /dev/null +++ b/src/codegen_support/runtime/eval_scope.rs @@ -0,0 +1,452 @@ +//! Purpose: +//! Emits core runtime helpers for scope-only literal eval AOT fragments. +//! Provides a small materialized name-to-Mixed-cell map without linking the +//! optional Rust eval interpreter bridge. +//! +//! Called from: +//! - `crate::codegen::runtime::emitters::emit_runtime()` when `eval_scope` is +//! needed without `eval_bridge`. +//! +//! Key details: +//! - Entry names are generated static data labels, so the scope stores borrowed +//! name pointers instead of copying name bytes. +//! - Owned Mixed cells are released on overwrite and scope free. + +use crate::codegen::emit::Emitter; +use crate::codegen::platform::Arch; + +const EVAL_STATUS_OK: i64 = 0; +const EVAL_STATUS_RUNTIME_FATAL: i64 = 2; +const EVAL_SCOPE_FLAG_PRESENT: i64 = 1; +const EVAL_SCOPE_FLAG_DIRTY: i64 = 1 << 2; +const EVAL_SCOPE_FLAG_OWNED: i64 = 1 << 4; + +/// Emits the target-specific eval-scope core helper surface. +pub(crate) fn emit_eval_scope_runtime(emitter: &mut Emitter) { + emitter.blank(); + emitter.comment("--- runtime: eval scope core helpers ---"); + match emitter.target.arch { + Arch::AArch64 => emit_aarch64_eval_scope_runtime(emitter), + Arch::X86_64 => emit_x86_64_eval_scope_runtime(emitter), + } +} + +/// Emits ARM64 eval-scope helpers. +fn emit_aarch64_eval_scope_runtime(emitter: &mut Emitter) { + emit_aarch64_eval_scope_new(emitter); + emit_aarch64_eval_scope_free(emitter); + emit_aarch64_eval_scope_set(emitter); + emit_aarch64_eval_scope_get(emitter); + emit_aarch64_eval_value_null(emitter); +} + +/// Emits the ARM64 scope allocator. +fn emit_aarch64_eval_scope_new(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_new"); + emitter.instruction("stp x29, x30, [sp, #-16]!"); // preserve the caller frame while allocating the scope header + emitter.instruction("mov x29, sp"); // establish a stable frame for the nested allocator call + emitter.instruction("mov x0, #8"); // scope header stores one head pointer + emitter.instruction("bl __rt_heap_alloc"); // allocate the scope header from the core heap + emitter.instruction("str xzr, [x0]"); // initialize the entry-list head to null + emitter.instruction("ldp x29, x30, [sp], #16"); // restore the caller frame after allocation + emitter.instruction("ret"); // return the scope handle in x0 +} + +/// Emits the ARM64 scope destructor. +fn emit_aarch64_eval_scope_free(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_free"); + emitter.instruction("cbz x0, __elephc_eval_scope_free_done"); // null scope handles are already free + emitter.instruction("sub sp, sp, #48"); // reserve a frame for callee-saved scan state + emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address across release calls + emitter.instruction("stp x19, x20, [sp, #16]"); // preserve scope and current-entry registers + emitter.instruction("str x21, [sp, #32]"); // preserve the next-entry register + emitter.instruction("mov x29, sp"); // establish a stable frame for nested runtime calls + emitter.instruction("mov x19, x0"); // keep the scope header pointer across entry releases + emitter.instruction("ldr x20, [x19]"); // load the first entry in the scope list + emitter.label("__elephc_eval_scope_free_loop"); + emitter.instruction("cbz x20, __elephc_eval_scope_free_header"); // stop when every entry has been released + emitter.instruction("ldr x21, [x20]"); // save entry->next before freeing the current entry + emitter.instruction("ldr x9, [x20, #32]"); // load the entry ABI flags + emitter.instruction(&format!("tst x9, #{}", EVAL_SCOPE_FLAG_OWNED)); // check whether the scope owns this Mixed cell + emitter.instruction("b.eq __elephc_eval_scope_free_entry"); // borrowed cells are not released by the scope + emitter.instruction("ldr x0, [x20, #24]"); // load the owned Mixed cell pointer + emitter.instruction("cbz x0, __elephc_eval_scope_free_entry"); // tolerate null owned cells defensively + emitter.instruction("bl __rt_decref_mixed"); // release the scope-owned Mixed cell + emitter.label("__elephc_eval_scope_free_entry"); + emitter.instruction("mov x0, x20"); // pass the current entry allocation to the heap free helper + emitter.instruction("bl __rt_heap_free"); // release the entry record itself + emitter.instruction("mov x20, x21"); // advance to the saved next entry + emitter.instruction("b __elephc_eval_scope_free_loop"); // continue freeing entries + emitter.label("__elephc_eval_scope_free_header"); + emitter.instruction("mov x0, x19"); // pass the scope header allocation to the heap free helper + emitter.instruction("bl __rt_heap_free"); // release the scope header after all entries + emitter.instruction("ldr x21, [sp, #32]"); // restore the saved next-entry register + emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved scan registers + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #48"); // release the scope-free frame + emitter.label("__elephc_eval_scope_free_done"); + emitter.instruction("ret"); // return after the scope is fully released +} + +/// Emits the ARM64 scope setter. +fn emit_aarch64_eval_scope_set(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_set"); + emitter.instruction("cbz x0, __elephc_eval_scope_set_fatal"); // reject null scope handles + emitter.instruction("cbz x2, __elephc_eval_scope_set_frame"); // empty names do not need a readable pointer + emitter.instruction("cbz x1, __elephc_eval_scope_set_fatal"); // non-empty names must provide bytes + emitter.label("__elephc_eval_scope_set_frame"); + emitter.instruction("sub sp, sp, #96"); // reserve a frame for inputs and callee-saved scan state + emitter.instruction("stp x29, x30, [sp, #0]"); // save frame pointer and return address across runtime calls + emitter.instruction("stp x19, x20, [sp, #16]"); // preserve scope and name pointer + emitter.instruction("stp x21, x22, [sp, #32]"); // preserve name length and cell pointer + emitter.instruction("stp x23, x24, [sp, #48]"); // preserve flags and previous-next pointer + emitter.instruction("stp x25, x26, [sp, #64]"); // preserve current entry and allocated entry pointer + emitter.instruction("mov x29, sp"); // establish a stable frame for nested runtime calls + emitter.instruction("mov x19, x0"); // save scope header pointer + emitter.instruction("mov x20, x1"); // save name byte pointer + emitter.instruction("mov x21, x2"); // save name byte length + emitter.instruction("mov x22, x3"); // save Mixed cell pointer + emitter.instruction("mov x23, x4"); // save caller-provided ABI flags + emitter.instruction("mov x24, x19"); // previous-next pointer initially addresses scope->head + emitter.instruction("ldr x25, [x24]"); // load the first candidate entry + emitter.label("__elephc_eval_scope_set_probe"); + emitter.instruction("cbz x25, __elephc_eval_scope_set_insert"); // insert when the name is absent + emitter.instruction("ldr x9, [x25, #16]"); // load candidate name length + emitter.instruction("cmp x9, x21"); // compare candidate length with requested length + emitter.instruction("b.ne __elephc_eval_scope_set_next"); // different lengths cannot match + emitter.instruction("ldr x10, [x25, #8]"); // load candidate name bytes + emitter.instruction("mov x11, #0"); // byte index for the equality loop + emitter.label("__elephc_eval_scope_set_cmp"); + emitter.instruction("cmp x11, x21"); // have all bytes matched? + emitter.instruction("b.eq __elephc_eval_scope_set_update"); // equal length and bytes select this entry + emitter.instruction("ldrb w12, [x10, x11]"); // load one existing name byte + emitter.instruction("ldrb w13, [x20, x11]"); // load the corresponding requested name byte + emitter.instruction("cmp w12, w13"); // compare candidate and requested name bytes + emitter.instruction("b.ne __elephc_eval_scope_set_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add x11, x11, #1"); // advance to the next byte + emitter.instruction("b __elephc_eval_scope_set_cmp"); // continue comparing this candidate name + emitter.label("__elephc_eval_scope_set_next"); + emitter.instruction("mov x24, x25"); // previous-next pointer now addresses current->next + emitter.instruction("ldr x25, [x25]"); // advance to the next entry + emitter.instruction("b __elephc_eval_scope_set_probe"); // continue scanning the entry list + emitter.label("__elephc_eval_scope_set_update"); + emitter.instruction("ldr x9, [x25, #32]"); // load old entry flags + emitter.instruction(&format!("tst x9, #{}", EVAL_SCOPE_FLAG_OWNED)); // check whether the old cell is scope-owned + emitter.instruction("b.eq __elephc_eval_scope_set_store"); // borrowed old cells do not need release + emitter.instruction("ldr x0, [x25, #24]"); // load the old Mixed cell pointer + emitter.instruction("cmp x0, x22"); // compare old and replacement cells + emitter.instruction("b.eq __elephc_eval_scope_set_store"); // retaining the same cell must not decref it + emitter.instruction("cbz x0, __elephc_eval_scope_set_store"); // tolerate null old cells defensively + emitter.instruction("bl __rt_decref_mixed"); // release the overwritten owned Mixed cell + emitter.label("__elephc_eval_scope_set_store"); + emitter.instruction("str x22, [x25, #24]"); // store the replacement Mixed cell + emitter.instruction(&format!("mov x9, #{}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // materialize visible and dirty ABI bits + emitter.instruction("orr x9, x23, x9"); // merge caller ownership flags with core visibility flags + emitter.instruction("str x9, [x25, #32]"); // publish the updated ABI flags + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope write + emitter.instruction("b __elephc_eval_scope_set_done"); // restore the frame and return + emitter.label("__elephc_eval_scope_set_insert"); + emitter.instruction("mov x0, #40"); // entry records store next, name, length, cell, and flags + emitter.instruction("bl __rt_heap_alloc"); // allocate a new entry record + emitter.instruction("mov x26, x0"); // keep the new entry pointer while initializing fields + emitter.instruction("str x25, [x26]"); // new entry next points at the old successor, normally null + emitter.instruction("str x20, [x26, #8]"); // store the borrowed static name pointer + emitter.instruction("str x21, [x26, #16]"); // store the name byte length + emitter.instruction("str x22, [x26, #24]"); // store the Mixed cell pointer + emitter.instruction(&format!("mov x9, #{}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // materialize visible and dirty ABI bits + emitter.instruction("orr x9, x23, x9"); // merge caller ownership flags with core visibility flags + emitter.instruction("str x9, [x26, #32]"); // store the new entry flags + emitter.instruction("str x26, [x24]"); // link the new entry through the previous-next pointer + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope write + emitter.label("__elephc_eval_scope_set_done"); + emitter.instruction("ldp x25, x26, [sp, #64]"); // restore callee-saved entry registers + emitter.instruction("ldp x23, x24, [sp, #48]"); // restore callee-saved flag and link registers + emitter.instruction("ldp x21, x22, [sp, #32]"); // restore callee-saved name length and cell registers + emitter.instruction("ldp x19, x20, [sp, #16]"); // restore callee-saved scope and name registers + emitter.instruction("ldp x29, x30, [sp, #0]"); // restore frame pointer and return address + emitter.instruction("add sp, sp, #96"); // release the setter frame + emitter.instruction("ret"); // return the eval status in x0 + emitter.label("__elephc_eval_scope_set_fatal"); + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status without mutating scope +} + +/// Emits the ARM64 scope getter. +fn emit_aarch64_eval_scope_get(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_get"); + emitter.instruction("cbz x0, __elephc_eval_scope_get_fatal"); // reject null scope handles + emitter.instruction("cbz x2, __elephc_eval_scope_get_search"); // empty names do not need a readable pointer + emitter.instruction("cbz x1, __elephc_eval_scope_get_fatal"); // non-empty names must provide bytes + emitter.label("__elephc_eval_scope_get_search"); + emitter.instruction("ldr x9, [x0]"); // load the first entry from scope->head + emitter.label("__elephc_eval_scope_get_probe"); + emitter.instruction("cbz x9, __elephc_eval_scope_get_missing"); // missing names produce null cell and zero flags + emitter.instruction("ldr x10, [x9, #16]"); // load candidate name length + emitter.instruction("cmp x10, x2"); // compare candidate length with requested length + emitter.instruction("b.ne __elephc_eval_scope_get_next"); // different lengths cannot match + emitter.instruction("ldr x10, [x9, #8]"); // load candidate name bytes + emitter.instruction("mov x11, #0"); // byte index for the equality loop + emitter.label("__elephc_eval_scope_get_cmp"); + emitter.instruction("cmp x11, x2"); // have all bytes matched? + emitter.instruction("b.eq __elephc_eval_scope_get_found"); // equal length and bytes select this entry + emitter.instruction("ldrb w12, [x10, x11]"); // load one existing name byte + emitter.instruction("ldrb w13, [x1, x11]"); // load the corresponding requested name byte + emitter.instruction("cmp w12, w13"); // compare candidate and requested name bytes + emitter.instruction("b.ne __elephc_eval_scope_get_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add x11, x11, #1"); // advance to the next byte + emitter.instruction("b __elephc_eval_scope_get_cmp"); // continue comparing this candidate name + emitter.label("__elephc_eval_scope_get_next"); + emitter.instruction("ldr x9, [x9]"); // advance to the next entry + emitter.instruction("b __elephc_eval_scope_get_probe"); // continue scanning the scope list + emitter.label("__elephc_eval_scope_get_found"); + emitter.instruction("cbz x3, __elephc_eval_scope_get_found_flags"); // skip cell output when caller passed null + emitter.instruction("ldr x10, [x9, #24]"); // load the visible Mixed cell pointer + emitter.instruction("str x10, [x3]"); // write the output Mixed cell pointer + emitter.label("__elephc_eval_scope_get_found_flags"); + emitter.instruction("cbz x4, __elephc_eval_scope_get_ok"); // skip flags output when caller passed null + emitter.instruction("ldr w10, [x9, #32]"); // load the low ABI flag bits + emitter.instruction("str w10, [x4]"); // write the output ABI flags + emitter.instruction("b __elephc_eval_scope_get_ok"); // finish with success + emitter.label("__elephc_eval_scope_get_missing"); + emitter.instruction("cbz x3, __elephc_eval_scope_get_missing_flags"); // skip cell output when caller passed null + emitter.instruction("str xzr, [x3]"); // missing variables have no cell pointer + emitter.label("__elephc_eval_scope_get_missing_flags"); + emitter.instruction("cbz x4, __elephc_eval_scope_get_ok"); // skip flags output when caller passed null + emitter.instruction("str wzr, [x4]"); // missing variables have zero ABI flags + emitter.label("__elephc_eval_scope_get_ok"); + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_OK)); // report successful scope lookup + emitter.instruction("ret"); // return the eval status in x0 + emitter.label("__elephc_eval_scope_get_fatal"); + emitter.instruction(&format!("mov x0, #{}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status +} + +/// Emits the ARM64 helper used by missing Mixed reload fallbacks. +fn emit_aarch64_eval_value_null(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_null"); + emitter.instruction("mov x0, #8"); // runtime tag 8 represents PHP null + emitter.instruction("mov x1, xzr"); // null has no low payload word + emitter.instruction("mov x2, xzr"); // null has no high payload word + emitter.instruction("b __rt_mixed_from_value"); // box null in a Mixed cell for eval-scope reloads +} + +/// Emits x86_64 eval-scope helpers. +fn emit_x86_64_eval_scope_runtime(emitter: &mut Emitter) { + emit_x86_64_eval_scope_new(emitter); + emit_x86_64_eval_scope_free(emitter); + emit_x86_64_eval_scope_set(emitter); + emit_x86_64_eval_scope_get(emitter); + emit_x86_64_eval_value_null(emitter); +} + +/// Emits the x86_64 scope allocator. +fn emit_x86_64_eval_scope_new(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_new"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before allocating the scope header + emitter.instruction("mov rbp, rsp"); // establish a stable frame for the nested allocator call + emitter.instruction("mov rax, 8"); // scope header stores one head pointer + emitter.instruction("call __rt_heap_alloc"); // allocate the scope header from the core heap + emitter.instruction("mov QWORD PTR [rax], 0"); // initialize the entry-list head to null + emitter.instruction("pop rbp"); // restore the caller frame pointer after allocation + emitter.instruction("ret"); // return the scope handle in rax +} + +/// Emits the x86_64 scope destructor. +fn emit_x86_64_eval_scope_free(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_free"); + emitter.instruction("test rdi, rdi"); // null scope handles are already free + emitter.instruction("jz __elephc_eval_scope_free_done"); // skip all cleanup for null handles + emitter.instruction("push rbp"); // preserve the caller frame pointer before release work + emitter.instruction("mov rbp, rsp"); // establish a stable frame for local scan state + emitter.instruction("sub rsp, 32"); // reserve scope, current-entry, and next-entry slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save the scope header pointer + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first entry in the scope list + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // store the current entry pointer + emitter.label("__elephc_eval_scope_free_loop"); + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload the current entry pointer + emitter.instruction("test r10, r10"); // have all entries been released? + emitter.instruction("jz __elephc_eval_scope_free_header"); // stop when the entry list is exhausted + emitter.instruction("mov r11, QWORD PTR [r10]"); // save entry->next before freeing the current entry + emitter.instruction("mov QWORD PTR [rbp - 24], r11"); // keep the next entry across release calls + emitter.instruction("mov r11, QWORD PTR [r10 + 32]"); // load the entry ABI flags + emitter.instruction(&format!("test r11, {}", EVAL_SCOPE_FLAG_OWNED)); // check whether the scope owns this Mixed cell + emitter.instruction("jz __elephc_eval_scope_free_entry"); // borrowed cells are not released by the scope + emitter.instruction("mov rax, QWORD PTR [r10 + 24]"); // load the owned Mixed cell pointer + emitter.instruction("test rax, rax"); // tolerate null owned cells defensively + emitter.instruction("jz __elephc_eval_scope_free_entry"); // skip release when the owned cell is null + emitter.instruction("call __rt_decref_mixed"); // release the scope-owned Mixed cell + emitter.label("__elephc_eval_scope_free_entry"); + emitter.instruction("mov rax, QWORD PTR [rbp - 16]"); // pass the current entry allocation to the heap free helper + emitter.instruction("call __rt_heap_free"); // release the entry record itself + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload the saved next entry + emitter.instruction("mov QWORD PTR [rbp - 16], r10"); // make the saved next entry current + emitter.instruction("jmp __elephc_eval_scope_free_loop"); // continue freeing entries + emitter.label("__elephc_eval_scope_free_header"); + emitter.instruction("mov rax, QWORD PTR [rbp - 8]"); // pass the scope header allocation to the heap free helper + emitter.instruction("call __rt_heap_free"); // release the scope header after all entries + emitter.instruction("add rsp, 32"); // release local scan slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.label("__elephc_eval_scope_free_done"); + emitter.instruction("ret"); // return after the scope is fully released +} + +/// Emits the x86_64 scope setter. +fn emit_x86_64_eval_scope_set(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_set"); + emitter.instruction("test rdi, rdi"); // reject null scope handles + emitter.instruction("jz __elephc_eval_scope_set_fatal"); // return a runtime-fatal status for null scope + emitter.instruction("test rdx, rdx"); // empty names do not need a readable pointer + emitter.instruction("jz __elephc_eval_scope_set_frame"); // skip the pointer check for empty names + emitter.instruction("test rsi, rsi"); // non-empty names must provide bytes + emitter.instruction("jz __elephc_eval_scope_set_fatal"); // return a runtime-fatal status for invalid names + emitter.label("__elephc_eval_scope_set_frame"); + emitter.instruction("push rbp"); // preserve the caller frame pointer before scope mutation + emitter.instruction("mov rbp, rsp"); // establish a stable frame for saved inputs and scan state + emitter.instruction("sub rsp, 64"); // reserve scope, name, length, cell, flags, previous-next, current, and index slots + emitter.instruction("mov QWORD PTR [rbp - 8], rdi"); // save scope header pointer + emitter.instruction("mov QWORD PTR [rbp - 16], rsi"); // save name byte pointer + emitter.instruction("mov QWORD PTR [rbp - 24], rdx"); // save name byte length + emitter.instruction("mov QWORD PTR [rbp - 32], rcx"); // save Mixed cell pointer + emitter.instruction("mov QWORD PTR [rbp - 40], r8"); // save caller-provided ABI flags + emitter.instruction("mov QWORD PTR [rbp - 48], rdi"); // previous-next pointer initially addresses scope->head + emitter.instruction("mov r10, QWORD PTR [rdi]"); // load the first candidate entry + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the current candidate entry + emitter.label("__elephc_eval_scope_set_probe"); + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload the current candidate entry + emitter.instruction("test r9, r9"); // is the requested name absent? + emitter.instruction("jz __elephc_eval_scope_set_insert"); // insert when the scan reaches the end + emitter.instruction("mov r10, QWORD PTR [r9 + 16]"); // load candidate name length + emitter.instruction("cmp r10, QWORD PTR [rbp - 24]"); // compare candidate length with requested length + emitter.instruction("jne __elephc_eval_scope_set_next"); // different lengths cannot match + emitter.instruction("mov r10, QWORD PTR [r9 + 8]"); // load candidate name bytes + emitter.instruction("xor r11, r11"); // byte index for the equality loop + emitter.label("__elephc_eval_scope_set_cmp"); + emitter.instruction("cmp r11, QWORD PTR [rbp - 24]"); // have all bytes matched? + emitter.instruction("je __elephc_eval_scope_set_update"); // equal length and bytes select this entry + emitter.instruction("mov al, BYTE PTR [r10 + r11]"); // load one existing name byte + emitter.instruction("mov r8, QWORD PTR [rbp - 16]"); // reload requested name pointer + emitter.instruction("cmp al, BYTE PTR [r8 + r11]"); // compare candidate and requested name bytes + emitter.instruction("jne __elephc_eval_scope_set_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add r11, 1"); // advance to the next byte + emitter.instruction("jmp __elephc_eval_scope_set_cmp"); // continue comparing this candidate name + emitter.label("__elephc_eval_scope_set_next"); + emitter.instruction("mov QWORD PTR [rbp - 48], r9"); // previous-next pointer now addresses current->next + emitter.instruction("mov r10, QWORD PTR [r9]"); // advance to the next entry + emitter.instruction("mov QWORD PTR [rbp - 56], r10"); // save the next candidate as current + emitter.instruction("jmp __elephc_eval_scope_set_probe"); // continue scanning the entry list + emitter.label("__elephc_eval_scope_set_update"); + emitter.instruction("mov r10, QWORD PTR [r9 + 32]"); // load old entry flags + emitter.instruction(&format!("test r10, {}", EVAL_SCOPE_FLAG_OWNED)); // check whether the old cell is scope-owned + emitter.instruction("jz __elephc_eval_scope_set_store"); // borrowed old cells do not need release + emitter.instruction("mov rax, QWORD PTR [r9 + 24]"); // load the old Mixed cell pointer + emitter.instruction("cmp rax, QWORD PTR [rbp - 32]"); // compare old and replacement cells + emitter.instruction("je __elephc_eval_scope_set_store"); // retaining the same cell must not decref it + emitter.instruction("test rax, rax"); // tolerate null old cells defensively + emitter.instruction("jz __elephc_eval_scope_set_store"); // skip release when the old cell is null + emitter.instruction("call __rt_decref_mixed"); // release the overwritten owned Mixed cell + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload current entry after the runtime call + emitter.label("__elephc_eval_scope_set_store"); + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload the replacement Mixed cell + emitter.instruction("mov QWORD PTR [r9 + 24], r10"); // store the replacement Mixed cell + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload caller-provided ABI flags + emitter.instruction(&format!("or r10, {}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // mark the entry visible and dirty + emitter.instruction("mov QWORD PTR [r9 + 32], r10"); // publish the updated ABI flags + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope write + emitter.instruction("jmp __elephc_eval_scope_set_done"); // restore the frame and return + emitter.label("__elephc_eval_scope_set_insert"); + emitter.instruction("mov rax, 40"); // entry records store next, name, length, cell, and flags + emitter.instruction("call __rt_heap_alloc"); // allocate a new entry record + emitter.instruction("mov r9, QWORD PTR [rbp - 56]"); // reload old successor, normally null + emitter.instruction("mov QWORD PTR [rax], r9"); // new entry next points at the old successor + emitter.instruction("mov r10, QWORD PTR [rbp - 16]"); // reload borrowed static name pointer + emitter.instruction("mov QWORD PTR [rax + 8], r10"); // store the borrowed static name pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 24]"); // reload name byte length + emitter.instruction("mov QWORD PTR [rax + 16], r10"); // store the name byte length + emitter.instruction("mov r10, QWORD PTR [rbp - 32]"); // reload Mixed cell pointer + emitter.instruction("mov QWORD PTR [rax + 24], r10"); // store the Mixed cell pointer + emitter.instruction("mov r10, QWORD PTR [rbp - 40]"); // reload caller-provided ABI flags + emitter.instruction(&format!("or r10, {}", EVAL_SCOPE_FLAG_PRESENT | EVAL_SCOPE_FLAG_DIRTY)); // mark the new entry visible and dirty + emitter.instruction("mov QWORD PTR [rax + 32], r10"); // store the new entry flags + emitter.instruction("mov r11, QWORD PTR [rbp - 48]"); // reload the previous-next pointer + emitter.instruction("mov QWORD PTR [r11], rax"); // link the new entry through the previous-next pointer + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope write + emitter.label("__elephc_eval_scope_set_done"); + emitter.instruction("add rsp, 64"); // release saved input and scan slots + emitter.instruction("pop rbp"); // restore the caller frame pointer + emitter.instruction("ret"); // return the eval status in rax + emitter.label("__elephc_eval_scope_set_fatal"); + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status without mutating scope +} + +/// Emits the x86_64 scope getter. +fn emit_x86_64_eval_scope_get(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_scope_get"); + emitter.instruction("test rdi, rdi"); // reject null scope handles + emitter.instruction("jz __elephc_eval_scope_get_fatal"); // return a runtime-fatal status for null scope + emitter.instruction("test rdx, rdx"); // empty names do not need a readable pointer + emitter.instruction("jz __elephc_eval_scope_get_search"); // skip the pointer check for empty names + emitter.instruction("test rsi, rsi"); // non-empty names must provide bytes + emitter.instruction("jz __elephc_eval_scope_get_fatal"); // return a runtime-fatal status for invalid names + emitter.label("__elephc_eval_scope_get_search"); + emitter.instruction("mov r9, QWORD PTR [rdi]"); // load the first entry from scope->head + emitter.label("__elephc_eval_scope_get_probe"); + emitter.instruction("test r9, r9"); // has the scan reached the end of the entry list? + emitter.instruction("jz __elephc_eval_scope_get_missing"); // missing names produce null cell and zero flags + emitter.instruction("mov r10, QWORD PTR [r9 + 16]"); // load candidate name length + emitter.instruction("cmp r10, rdx"); // compare candidate length with requested length + emitter.instruction("jne __elephc_eval_scope_get_next"); // different lengths cannot match + emitter.instruction("mov r10, QWORD PTR [r9 + 8]"); // load candidate name bytes + emitter.instruction("xor r11, r11"); // byte index for the equality loop + emitter.label("__elephc_eval_scope_get_cmp"); + emitter.instruction("cmp r11, rdx"); // have all bytes matched? + emitter.instruction("je __elephc_eval_scope_get_found"); // equal length and bytes select this entry + emitter.instruction("mov al, BYTE PTR [r10 + r11]"); // load one existing name byte + emitter.instruction("cmp al, BYTE PTR [rsi + r11]"); // compare candidate and requested name bytes + emitter.instruction("jne __elephc_eval_scope_get_next"); // any byte mismatch means this entry is not the target + emitter.instruction("add r11, 1"); // advance to the next byte + emitter.instruction("jmp __elephc_eval_scope_get_cmp"); // continue comparing this candidate name + emitter.label("__elephc_eval_scope_get_next"); + emitter.instruction("mov r9, QWORD PTR [r9]"); // advance to the next entry + emitter.instruction("jmp __elephc_eval_scope_get_probe"); // continue scanning the scope list + emitter.label("__elephc_eval_scope_get_found"); + emitter.instruction("test rcx, rcx"); // did the caller request cell output? + emitter.instruction("jz __elephc_eval_scope_get_found_flags"); // skip cell output for null out_cell + emitter.instruction("mov r10, QWORD PTR [r9 + 24]"); // load the visible Mixed cell pointer + emitter.instruction("mov QWORD PTR [rcx], r10"); // write the output Mixed cell pointer + emitter.label("__elephc_eval_scope_get_found_flags"); + emitter.instruction("test r8, r8"); // did the caller request flag output? + emitter.instruction("jz __elephc_eval_scope_get_ok"); // skip flags output for null out_flags + emitter.instruction("mov r10d, DWORD PTR [r9 + 32]"); // load the low ABI flag bits + emitter.instruction("mov DWORD PTR [r8], r10d"); // write the output ABI flags + emitter.instruction("jmp __elephc_eval_scope_get_ok"); // finish with success + emitter.label("__elephc_eval_scope_get_missing"); + emitter.instruction("test rcx, rcx"); // did the caller request cell output? + emitter.instruction("jz __elephc_eval_scope_get_missing_flags"); // skip cell output for null out_cell + emitter.instruction("mov QWORD PTR [rcx], 0"); // missing variables have no cell pointer + emitter.label("__elephc_eval_scope_get_missing_flags"); + emitter.instruction("test r8, r8"); // did the caller request flag output? + emitter.instruction("jz __elephc_eval_scope_get_ok"); // skip flags output for null out_flags + emitter.instruction("mov DWORD PTR [r8], 0"); // missing variables have zero ABI flags + emitter.label("__elephc_eval_scope_get_ok"); + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_OK)); // report successful scope lookup + emitter.instruction("ret"); // return the eval status in rax + emitter.label("__elephc_eval_scope_get_fatal"); + emitter.instruction(&format!("mov eax, {}", EVAL_STATUS_RUNTIME_FATAL)); // report invalid scope/name inputs + emitter.instruction("ret"); // return the fatal eval status +} + +/// Emits the x86_64 helper used by missing Mixed reload fallbacks. +fn emit_x86_64_eval_value_null(emitter: &mut Emitter) { + label_c_global(emitter, "__elephc_eval_value_null"); + emitter.instruction("mov rax, 8"); // runtime tag 8 represents PHP null + emitter.instruction("xor rdi, rdi"); // null has no low payload word + emitter.instruction("xor rsi, rsi"); // null has no high payload word + emitter.instruction("jmp __rt_mixed_from_value"); // box null in a Mixed cell for eval-scope reloads +} + +/// Emits a global label with platform C-symbol mangling. +fn label_c_global(emitter: &mut Emitter, name: &str) { + let symbol = emitter.target.extern_symbol(name); + emitter.label_global(&symbol); +} diff --git a/src/codegen_support/runtime/mod.rs b/src/codegen_support/runtime/mod.rs index 514e0353d2..e07780b20e 100644 --- a/src/codegen_support/runtime/mod.rs +++ b/src/codegen_support/runtime/mod.rs @@ -16,6 +16,7 @@ mod data; mod diagnostics; mod emitters; mod eval_bridge; +mod eval_scope; mod exceptions; mod fibers; /// Runtime helpers for generator state management (yield, resume, stack frames). diff --git a/src/codegen_support/runtime_features.rs b/src/codegen_support/runtime_features.rs index 5d6bc5f4bd..eb54a3cd0d 100644 --- a/src/codegen_support/runtime_features.rs +++ b/src/codegen_support/runtime_features.rs @@ -14,8 +14,8 @@ //! - The dynamic builtin dispatcher (descriptor invoker) emits per-builtin //! wrappers — including md5/sha1/hash — that reference the `elephc_crypto` //! staticlib, so its detection forces that crate to link. -//! - `eval()` enables the optional libelephc-magician bridge only when lowered EIR -//! actually references the eval bridge call path. +//! - `eval()` keeps the dynamic bridge feature separate from scope-only helpers +//! so AOT fragments can shed bridge-only runtime/link dependencies incrementally. use std::collections::HashMap; @@ -35,8 +35,12 @@ pub struct RuntimeFeatures { /// True when codegen can emit the runtime callable dispatcher (descriptor /// invoker) that builds per-builtin wrappers referencing `elephc_crypto`. pub descriptor_invoker: bool, - /// True when codegen can call the optional eval bridge staticlib. - pub eval: bool, + /// True when codegen can call the optional eval interpreter bridge staticlib. + pub eval_bridge: bool, + /// True when codegen can use materialized eval-scope helper symbols without + /// the interpreter bridge. Scope-only helpers are emitted by the core runtime + /// when the dynamic eval bridge is not otherwise required. + pub eval_scope: bool, /// True when compiling a `--web` program. Selects the output-capture variant /// of `__rt_stdout_write`, which checks the `_elephc_web_capture` flag and may /// tail-call `elephc_web_write` (a symbol only linked into `--web` binaries). @@ -51,7 +55,8 @@ impl RuntimeFeatures { regex: false, phar_archive: false, descriptor_invoker: false, - eval: false, + eval_bridge: false, + eval_scope: false, web: false, } } @@ -63,7 +68,8 @@ impl RuntimeFeatures { regex: true, phar_archive: true, descriptor_invoker: true, - eval: true, + eval_bridge: true, + eval_scope: true, web: true, } } @@ -100,7 +106,7 @@ pub fn required_libraries_for_runtime_features(features: RuntimeFeatures) -> Vec // reference `elephc_crypto_hash`; force the crate to link on all targets. libs.push("elephc_crypto".to_string()); } - if features.eval { + if features.eval_bridge { // Eval code can call preg_* from dynamically parsed source, so PCRE2 is // required even when no static preg call appears in the AOT AST. push_required_library(&mut libs, "pcre2-posix"); @@ -190,7 +196,9 @@ fn emitted_classes_include_phar_archive_helpers( classes: &HashMap, ) -> bool { if program_has_dynamic_instanceof(program) { - return classes.keys().any(|name| is_phar_archive_helper_class_name(name)); + return classes + .keys() + .any(|name| is_phar_archive_helper_class_name(name)); } super::collect_emitted_class_names(program, classes) .iter() @@ -251,9 +259,9 @@ fn stmt_has_regex_call(stmt: &Stmt) -> bool { } => { expr_has_regex_call(condition) || body_has_regex_call(then_body) - || elseif_clauses - .iter() - .any(|(condition, body)| expr_has_regex_call(condition) || body_has_regex_call(body)) + || elseif_clauses.iter().any(|(condition, body)| { + expr_has_regex_call(condition) || body_has_regex_call(body) + }) || else_body.as_deref().is_some_and(body_has_regex_call) } StmtKind::IfDef { @@ -261,8 +269,7 @@ fn stmt_has_regex_call(stmt: &Stmt) -> bool { else_body, .. } => { - body_has_regex_call(then_body) - || else_body.as_deref().is_some_and(body_has_regex_call) + body_has_regex_call(then_body) || else_body.as_deref().is_some_and(body_has_regex_call) } StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { expr_has_regex_call(condition) || body_has_regex_call(body) @@ -298,9 +305,9 @@ fn stmt_has_regex_call(stmt: &Stmt) -> bool { | StmtKind::FunctionDecl { body, .. } => body_has_regex_call(body), StmtKind::ClassDecl { methods, .. } | StmtKind::TraitDecl { methods, .. } - | StmtKind::InterfaceDecl { methods, .. } => { - methods.iter().any(|method| body_has_regex_call(&method.body)) - } + | StmtKind::InterfaceDecl { methods, .. } => methods + .iter() + .any(|method| body_has_regex_call(&method.body)), StmtKind::Try { try_body, catches, @@ -333,9 +340,9 @@ fn expr_has_regex_call(expr: &Expr) -> bool { match &expr.kind { // `IncludeValue` is a transient parser node fully expanded by the resolver; // it can never reach this pass. - ExprKind::IncludeValue { .. } => unreachable!( - "ExprKind::IncludeValue must be expanded by the resolver" - ), + ExprKind::IncludeValue { .. } => { + unreachable!("ExprKind::IncludeValue must be expanded by the resolver") + } ExprKind::FunctionCall { name, args } => { is_regex_builtin_name(name.as_str()) || regex_callback_dispatch_call(name.as_str(), args) @@ -507,7 +514,10 @@ fn expr_is_regex_callback_string(expr: &Expr) -> bool { /// Returns true when a static receiver expression can contain a regex call. fn static_receiver_has_regex_call(receiver: &StaticReceiver) -> bool { match receiver { - StaticReceiver::Named(_) | StaticReceiver::Self_ | StaticReceiver::Static | StaticReceiver::Parent => false, + StaticReceiver::Named(_) + | StaticReceiver::Self_ + | StaticReceiver::Static + | StaticReceiver::Parent => false, } } @@ -574,7 +584,9 @@ fn stmt_needs_descriptor_invoker(stmt: &Stmt) -> bool { || elseif_clauses.iter().any(|(condition, body)| { expr_needs_descriptor_invoker(condition) || body_needs_descriptor_invoker(body) }) - || else_body.as_deref().is_some_and(body_needs_descriptor_invoker) + || else_body + .as_deref() + .is_some_and(body_needs_descriptor_invoker) } StmtKind::IfDef { then_body, @@ -582,7 +594,9 @@ fn stmt_needs_descriptor_invoker(stmt: &Stmt) -> bool { .. } => { body_needs_descriptor_invoker(then_body) - || else_body.as_deref().is_some_and(body_needs_descriptor_invoker) + || else_body + .as_deref() + .is_some_and(body_needs_descriptor_invoker) } StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { expr_needs_descriptor_invoker(condition) || body_needs_descriptor_invoker(body) @@ -594,7 +608,9 @@ fn stmt_needs_descriptor_invoker(stmt: &Stmt) -> bool { body, } => { init.as_deref().is_some_and(stmt_needs_descriptor_invoker) - || condition.as_ref().is_some_and(expr_needs_descriptor_invoker) + || condition + .as_ref() + .is_some_and(expr_needs_descriptor_invoker) || update.as_deref().is_some_and(stmt_needs_descriptor_invoker) || body_needs_descriptor_invoker(body) } @@ -611,7 +627,9 @@ fn stmt_needs_descriptor_invoker(stmt: &Stmt) -> bool { patterns.iter().any(expr_needs_descriptor_invoker) || body_needs_descriptor_invoker(body) }) - || default.as_deref().is_some_and(body_needs_descriptor_invoker) + || default + .as_deref() + .is_some_and(body_needs_descriptor_invoker) } StmtKind::Synthetic(body) | StmtKind::NamespaceBlock { body, .. } @@ -661,9 +679,9 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { match &expr.kind { // `IncludeValue` is a transient parser node fully expanded by the resolver; // it can never reach this pass. - ExprKind::IncludeValue { .. } => unreachable!( - "ExprKind::IncludeValue must be expanded by the resolver" - ), + ExprKind::IncludeValue { .. } => { + unreachable!("ExprKind::IncludeValue must be expanded by the resolver") + } // A direct dynamic call on an arbitrary callee (e.g. `$callback(...)`) lowers // through runtime callable dispatch when the callee resolves to a string name. ExprKind::ExprCall { callee, args } => { @@ -706,11 +724,9 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { .is_some_and(expr_needs_descriptor_invoker) } ExprKind::ArrayLiteral(items) => items.iter().any(expr_needs_descriptor_invoker), - ExprKind::ArrayLiteralAssoc(items) => items - .iter() - .any(|(key, value)| { - expr_needs_descriptor_invoker(key) || expr_needs_descriptor_invoker(value) - }), + ExprKind::ArrayLiteralAssoc(items) => items.iter().any(|(key, value)| { + expr_needs_descriptor_invoker(key) || expr_needs_descriptor_invoker(value) + }), ExprKind::Match { subject, arms, @@ -721,7 +737,9 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { patterns.iter().any(expr_needs_descriptor_invoker) || expr_needs_descriptor_invoker(value) }) - || default.as_deref().is_some_and(expr_needs_descriptor_invoker) + || default + .as_deref() + .is_some_and(expr_needs_descriptor_invoker) } ExprKind::ArrayAccess { array, index } => { expr_needs_descriptor_invoker(array) || expr_needs_descriptor_invoker(index) @@ -743,7 +761,10 @@ fn expr_needs_descriptor_invoker(expr: &Expr) -> bool { | ExprKind::NewScopedObject { args, .. } => args.iter().any(expr_needs_descriptor_invoker), ExprKind::NewDynamicObject { class_name, args, .. - } => expr_needs_descriptor_invoker(class_name) || args.iter().any(expr_needs_descriptor_invoker), + } => { + expr_needs_descriptor_invoker(class_name) + || args.iter().any(expr_needs_descriptor_invoker) + } ExprKind::PropertyAccess { object, .. } | ExprKind::NullsafePropertyAccess { object, .. } => expr_needs_descriptor_invoker(object), ExprKind::DynamicPropertyAccess { object, property } @@ -818,7 +839,9 @@ fn expr_is_descriptor_invoker_trigger(expr: &Expr) -> bool { // string literals — as runtime dispatch, so this uses the broader predicate. ExprKind::NewObject { class_name, args } => { is_fiber_class_name(class_name.as_str()) - && args.first().is_some_and(fiber_callback_may_be_runtime_dispatch) + && args + .first() + .is_some_and(fiber_callback_may_be_runtime_dispatch) } _ => false, } @@ -910,7 +933,10 @@ mod tests { /// Verifies ordinary programs do not require the optional regex runtime helpers. #[test] fn test_runtime_features_omit_regex_for_plain_program() { - assert_eq!(features_for(" bool; + + /// Returns true when a static method call can be lowered inside an EIR AOT fragment. + fn static_method_supported( + &self, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], + ) -> bool; +} + +/// Pair of caller-provided support predicates for eval EIR AOT static calls. +struct EirStaticCallPredicates<'a, F, M> { + function: &'a F, + static_method: &'a M, +} + +impl EirStaticCallSupport for EirStaticCallPredicates<'_, F, M> +where + F: Fn(&str, &[Expr]) -> bool, + M: Fn(&StaticReceiver, &str, &[Expr]) -> bool, +{ + /// Delegates function-call eligibility to the caller-provided predicate. + fn function_supported(&self, name: &str, args: &[Expr]) -> bool { + (self.function)(name, args) + } + + /// Delegates static-method eligibility to the caller-provided predicate. + fn static_method_supported( + &self, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], + ) -> bool { + (self.static_method)(receiver, method, args) + } +} + +/// Compile-time plan for one literal eval fragment. +pub(crate) struct EvalAotPlan { + function_name: Option, + eir_program: Option, + scope_read_function_name: Option, + scope_read_eir_program: Option, + reads: BTreeSet, + array_read_constraints: BTreeSet, + assoc_array_read_constraints: BTreeSet, + float_predicate_read_constraints: BTreeSet, + writes: BTreeSet, + scope_direct_writes: BTreeSet, + scope_flush_writes: BTreeSet, + creates_unknown_vars: bool, + needs_eval_context: bool, + needs_global_scope: bool, + fallback_reason: Option, + native_only_scalar: bool, +} + +impl EvalAotPlan { + /// Returns true when this fragment is fully native and cannot call the eval bridge. + pub(crate) fn is_fully_static_no_bridge(&self) -> bool { + !self.needs_eval_context + && self.fallback_reason.is_none() + && self.scope_read_eir_program.is_none() + && (self.eir_program.is_some() || self.native_only_scalar) + } + + /// Returns true when the scope-read EIR body can receive reads as direct parameters. + pub(crate) fn uses_scope_read_params(&self) -> bool { + self.scope_read_eir_program.is_some() + && !self.reads.is_empty() + && self.writes.is_empty() + && self.scope_direct_writes.is_empty() + && self.scope_flush_writes.is_empty() + } + + /// Returns true when the fragment still requires the magician eval bridge. + pub(crate) fn requires_runtime_eval_bridge(&self) -> bool { + if self.is_fully_static_no_bridge() { + return false; + } + if self.scope_read_eir_program.is_some() { + return false; + } + self.needs_eval_context + || self.needs_global_scope + || self.creates_unknown_vars + || !self.reads.is_empty() + || !self.writes.is_empty() + || self.fallback_reason.is_some() + } + + /// Returns true when the fragment needs only core eval-scope runtime state. + pub(crate) fn requires_runtime_eval_scope(&self) -> bool { + self.scope_read_eir_program.is_some() && !self.uses_scope_read_params() + } + + /// Takes the deterministic internal EIR function name, when one exists. + pub(crate) fn take_function_name(&mut self) -> Option { + self.function_name.take() + } + + /// Takes the parsed and folded EIR AOT body, when one exists. + pub(crate) fn take_eir_program(&mut self) -> Option { + self.eir_program.take() + } + + /// Takes the deterministic EIR function name for a scope-read AOT body. + pub(crate) fn take_scope_read_function_name(&mut self) -> Option { + self.scope_read_function_name.take() + } + + /// Takes the parsed and folded body for a scope-read EIR AOT function. + pub(crate) fn take_scope_read_eir_program(&mut self) -> Option { + self.scope_read_eir_program.take() + } + + /// Returns the statically known eval-scope reads for this fragment. + pub(crate) fn reads(&self) -> &BTreeSet { + &self.reads + } + + /// Returns scope reads that must be caller-side arrays for direct-param AOT. + pub(crate) fn array_read_constraints(&self) -> &BTreeSet { + &self.array_read_constraints + } + + /// Returns scope reads that must be caller-side associative arrays. + pub(crate) fn assoc_array_read_constraints(&self) -> &BTreeSet { + &self.assoc_array_read_constraints + } + + /// Returns scope reads that must be caller-side int/float values. + pub(crate) fn float_predicate_read_constraints(&self) -> &BTreeSet { + &self.float_predicate_read_constraints + } + + /// Returns the statically known eval-scope writes for this fragment. + pub(crate) fn writes(&self) -> &BTreeSet { + &self.writes + } + + /// Returns eval-scope writes that are stored immediately during EIR lowering. + pub(crate) fn direct_writes(&self) -> &BTreeSet { + &self.scope_direct_writes + } + + /// Returns local writes that are flushed to eval scope by the EIR finalizer. + pub(crate) fn flush_writes(&self) -> &BTreeSet { + &self.scope_flush_writes + } + + /// Returns the conservative bridge fallback reason, when this plan has one. + pub(crate) fn fallback_reason(&self) -> Option { + self.fallback_reason + } +} + +/// Conservative reason a literal eval fragment cannot be fully static today. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum EvalAotFallbackReason { + ParseError, + IncludeOrRequire, + Declaration, + GlobalOrStatic, + ReferenceOrByRef, + DynamicCall, + DynamicClassOrMember, + ObjectOrMemberAccess, + ArrayOrIterable, + TryOrThrow, + UnsupportedControlFlow, + UnsupportedScope, + UnsupportedStaticCall, + UnsupportedConstruct, +} + +/// Static scalar category for a direct eval local-store candidate. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum DirectLocalStoreScalarKind { + Null, + Bool, + Int, + Float, + String, +} + +impl EvalAotFallbackReason { + /// Returns a stable assembly-marker description for this fallback reason. + pub(crate) fn description(self) -> &'static str { + match self { + EvalAotFallbackReason::ParseError => "parse error", + EvalAotFallbackReason::IncludeOrRequire => "include/require needs bridge semantics", + EvalAotFallbackReason::Declaration => "runtime declarations need bridge semantics", + EvalAotFallbackReason::GlobalOrStatic => "global/static scope needs bridge semantics", + EvalAotFallbackReason::ReferenceOrByRef => "references/by-ref need bridge semantics", + EvalAotFallbackReason::DynamicCall => "dynamic call needs bridge semantics", + EvalAotFallbackReason::DynamicClassOrMember => { + "dynamic class/member access needs bridge semantics" + } + EvalAotFallbackReason::ObjectOrMemberAccess => { + "object/member access needs bridge semantics" + } + EvalAotFallbackReason::ArrayOrIterable => { + "array/iterable semantics need bridge fallback" + } + EvalAotFallbackReason::TryOrThrow => "try/throw needs bridge semantics", + EvalAotFallbackReason::UnsupportedControlFlow => "unsupported control flow", + EvalAotFallbackReason::UnsupportedScope => "unsupported scope synchronization", + EvalAotFallbackReason::UnsupportedStaticCall => "unsupported static call", + EvalAotFallbackReason::UnsupportedConstruct => "unsupported construct", + } + } +} + +/// Parses a literal eval fragment as a PHP statement fragment. +pub(crate) fn parse_literal_fragment(fragment: &str) -> Option { + let source = format!(", +) -> Option { + let program = parse_literal_fragment(fragment)?; + Some(match source_path { + Some(source_path) => crate::magic_constants::substitute_file_and_scope_constants( + program, + Path::new(source_path), + ), + None => program, + }) +} + +/// Returns the ordered static writes in a direct local-store eval candidate. +pub(crate) fn literal_fragment_direct_local_store_writes( + fragment: &str, +) -> Option> { + let program = parse_literal_fragment(fragment)?; + direct_local_store_candidate_writes(&program) +} + +/// Returns scalar self-read writes that can avoid eval scope with direct caller locals. +pub(crate) fn literal_fragment_direct_local_read_write_writes( + fragment: &str, +) -> Option> { + let program = parse_literal_fragment(fragment)?; + direct_local_read_write_candidate_writes(&program) +} + +/// Checks the narrow read/write shape `target = f(target, literals)`. +fn direct_local_read_write_candidate_writes( + program: &[Stmt], +) -> Option> { + let mut writes = BTreeMap::new(); + let mut saw_store = false; + let mut terminated = false; + for stmt in program { + if terminated { + return None; + } + match &stmt.kind { + StmtKind::Assign { name, value } => { + let kind = direct_local_read_write_expr(value, name)?; + writes.insert(name.clone(), kind); + saw_store = true; + } + StmtKind::Return(Some(expr)) => { + direct_local_store_static_scalar_expr(expr)?; + terminated = true; + } + StmtKind::Return(None) => { + terminated = true; + } + StmtKind::ExprStmt(expr) => { + direct_local_store_static_scalar_expr(expr)?; + } + _ => return None, + } + } + saw_store.then_some(writes) +} + +/// Checks scalar expressions whose only variable read is the target being assigned. +fn direct_local_read_write_expr( + expr: &Expr, + target_name: &str, +) -> Option { + match &expr.kind { + ExprKind::IntLiteral(_) => Some(DirectLocalStoreScalarKind::Int), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(DirectLocalStoreScalarKind::Float) + } + ExprKind::Variable(name) if name == target_name => Some(DirectLocalStoreScalarKind::Int), + ExprKind::Negate(inner) => (direct_local_read_write_expr(inner, target_name)? + == DirectLocalStoreScalarKind::Int) + .then_some(DirectLocalStoreScalarKind::Int), + ExprKind::BinaryOp { left, op, right } + if matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul) => + { + (direct_local_read_write_expr(left, target_name)? == DirectLocalStoreScalarKind::Int + && direct_local_read_write_expr(right, target_name)? + == DirectLocalStoreScalarKind::Int) + .then_some(DirectLocalStoreScalarKind::Int) + } + ExprKind::BinaryOp { left, op, right } if matches!(op, BinOp::Div | BinOp::Mod) => { + let left_kind = direct_local_read_write_expr(left, target_name)?; + let right_kind = direct_local_read_write_expr(right, target_name)?; + (local_scalar_numeric_kind(left_kind) && local_scalar_numeric_kind(right_kind)) + .then_some(if *op == BinOp::Div { + DirectLocalStoreScalarKind::Float + } else { + DirectLocalStoreScalarKind::Int + }) + } + _ => None, + } +} + +/// Returns int/bool local writes for the legacy local-scalar AOT subset. +pub(crate) fn literal_fragment_local_scalar_writes_with_static_calls( + fragment: &str, + static_call_supported: F, +) -> Option> +where + F: Fn(&str, &[Expr]) -> bool, +{ + let program = parse_literal_fragment(fragment)?; + let mut writes = BTreeMap::new(); + let mut assigned = BTreeMap::new(); + local_scalar_block_writes( + &program, + &mut writes, + &mut assigned, + 0, + true, + &static_call_supported, + )?; + (!writes.is_empty()).then_some(writes) +} + +/// Checks a statement block against the int/bool local-scalar AOT subset. +fn local_scalar_block_writes( + program: &[Stmt], + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + loop_depth: usize, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option<()> +where + F: Fn(&str, &[Expr]) -> bool, +{ + let mut terminated = false; + for stmt in program { + if terminated { + break; + } + terminated = local_scalar_stmt_writes( + stmt, + writes, + assigned, + loop_depth, + allow_type_changes, + static_call_supported, + )?; + } + Some(()) +} + +/// Checks one statement against the int/bool local-scalar AOT subset. +fn local_scalar_stmt_writes( + stmt: &Stmt, + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + loop_depth: usize, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &stmt.kind { + StmtKind::Echo(expr) => { + local_scalar_echo_expr(expr, assigned, static_call_supported)?; + Some(false) + } + StmtKind::Assign { name, value } => { + let kind = local_scalar_value_expr(value, assigned, static_call_supported)?; + local_scalar_record_write(writes, assigned, name, kind, allow_type_changes)?; + Some(false) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + let mut then_assigned = assigned.clone(); + local_scalar_block_writes( + then_body, + writes, + &mut then_assigned, + loop_depth, + false, + static_call_supported, + )?; + for (elseif_condition, elseif_body) in elseif_clauses { + local_scalar_condition_expr(elseif_condition, assigned, static_call_supported)?; + let mut elseif_assigned = assigned.clone(); + local_scalar_block_writes( + elseif_body, + writes, + &mut elseif_assigned, + loop_depth, + false, + static_call_supported, + )?; + } + if let Some(else_body) = else_body { + let mut else_assigned = assigned.clone(); + local_scalar_block_writes( + else_body, + writes, + &mut else_assigned, + loop_depth, + false, + static_call_supported, + )?; + } + Some(false) + } + StmtKind::While { condition, body } => { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + let mut body_assigned = assigned.clone(); + local_scalar_block_writes( + body, + writes, + &mut body_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + Some(false) + } + StmtKind::DoWhile { body, condition } => { + let mut body_assigned = assigned.clone(); + local_scalar_block_writes( + body, + writes, + &mut body_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + local_scalar_condition_expr(condition, &body_assigned, static_call_supported)?; + Some(false) + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init.as_deref() { + local_scalar_for_clause_writes( + init, + writes, + assigned, + allow_type_changes, + static_call_supported, + )?; + } + if let Some(condition) = condition { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + } + let mut body_assigned = assigned.clone(); + local_scalar_block_writes( + body, + writes, + &mut body_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + if let Some(update) = update.as_deref() { + local_scalar_for_clause_writes( + update, + writes, + &mut body_assigned, + false, + static_call_supported, + )?; + } + Some(false) + } + StmtKind::Switch { + subject, + cases, + default, + } => { + local_scalar_switch_stmt_writes( + subject, + cases, + default.as_deref(), + writes, + assigned, + loop_depth, + static_call_supported, + )?; + Some(false) + } + StmtKind::Break(level) if *level > 0 && *level <= loop_depth => Some(true), + StmtKind::Continue(level) if *level > 0 && *level <= loop_depth => Some(true), + StmtKind::Return(Some(expr)) => { + local_scalar_value_expr(expr, assigned, static_call_supported)?; + Some(true) + } + StmtKind::Return(None) => Some(true), + StmtKind::ExprStmt(expr) => local_scalar_expr_stmt_writes( + expr, + writes, + assigned, + allow_type_changes, + static_call_supported, + ), + _ => None, + } +} + +/// Checks a local-scalar switch while preserving conservative assignment facts. +fn local_scalar_switch_stmt_writes( + subject: &Expr, + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, + writes: &mut BTreeMap, + assigned: &BTreeMap, + loop_depth: usize, + static_call_supported: &F, +) -> Option<()> +where + F: Fn(&str, &[Expr]) -> bool, +{ + let subject_kind = local_scalar_condition_expr(subject, assigned, static_call_supported)?; + for (conditions, body) in cases { + for condition in conditions { + (local_scalar_condition_expr(condition, assigned, static_call_supported)? + == subject_kind) + .then_some(())?; + } + let mut case_assigned = assigned.clone(); + local_scalar_switch_block_writes( + body, + writes, + &mut case_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + } + if let Some(default) = default { + let mut default_assigned = assigned.clone(); + local_scalar_switch_block_writes( + default, + writes, + &mut default_assigned, + loop_depth + 1, + false, + static_call_supported, + )?; + } + Some(()) +} + +/// Checks a switch case/default body against the local-scalar subset. +fn local_scalar_switch_block_writes( + statements: &[Stmt], + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + loop_depth: usize, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option<()> +where + F: Fn(&str, &[Expr]) -> bool, +{ + local_scalar_block_writes( + statements, + writes, + assigned, + loop_depth, + allow_type_changes, + static_call_supported, + ) +} + +/// Checks an inline `for` init/update statement against the local-scalar subset. +fn local_scalar_for_clause_writes( + stmt: &Stmt, + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option<()> +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &stmt.kind { + StmtKind::Assign { .. } | StmtKind::ExprStmt(_) => (!local_scalar_stmt_writes( + stmt, + writes, + assigned, + 0, + allow_type_changes, + static_call_supported, + )?) + .then_some(()), + _ => None, + } +} + +/// Checks one expression statement accepted by the local-scalar AOT subset. +fn local_scalar_expr_stmt_writes( + expr: &Expr, + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + allow_type_changes: bool, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::Print(value) => { + local_scalar_echo_expr(value, assigned, static_call_supported)?; + Some(false) + } + ExprKind::Assignment { target, value, .. } => { + let ExprKind::Variable(name) = &target.kind else { + return None; + }; + let kind = local_scalar_value_expr(value, assigned, static_call_supported)?; + local_scalar_record_write(writes, assigned, name, kind, allow_type_changes)?; + Some(false) + } + ExprKind::PreIncrement(name) | ExprKind::PostIncrement(name) => { + local_scalar_inc_dec_write(writes, assigned, name)?; + Some(false) + } + ExprKind::PreDecrement(name) | ExprKind::PostDecrement(name) => { + local_scalar_inc_dec_write(writes, assigned, name)?; + Some(false) + } + _ => { + local_scalar_value_expr(expr, assigned, static_call_supported)?; + Some(false) + } + } +} + +/// Records an increment/decrement write for an assigned integer local. +fn local_scalar_inc_dec_write( + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + name: &str, +) -> Option<()> { + (assigned.get(name) == Some(&DirectLocalStoreScalarKind::Int)).then_some(())?; + local_scalar_record_write( + writes, + assigned, + name, + DirectLocalStoreScalarKind::Int, + true, + ) +} + +/// Records one local-scalar write, allowing type changes only in linear statement flow. +fn local_scalar_record_write( + writes: &mut BTreeMap, + assigned: &mut BTreeMap, + name: &str, + kind: DirectLocalStoreScalarKind, + allow_type_change: bool, +) -> Option<()> { + if crate::superglobals::is_superglobal(name) { + return None; + } + if let Some(existing) = assigned.get(name) { + (*existing == kind || allow_type_change).then_some(())?; + } + assigned.insert(name.to_string(), kind); + writes.insert(name.to_string(), kind); + Some(()) +} + +/// Checks a control-flow condition for the local-scalar AOT subset. +fn local_scalar_condition_expr( + expr: &Expr, + assigned: &BTreeMap, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + let kind = local_scalar_value_expr(expr, assigned, static_call_supported)?; + matches!( + kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Bool + ) + .then_some(kind) +} + +/// Checks an expression that can be emitted by local-scalar echo. +fn local_scalar_echo_expr( + expr: &Expr, + assigned: &BTreeMap, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::StringLiteral(_) => Some(DirectLocalStoreScalarKind::String), + ExprKind::BinaryOp { left, op, right } if *op == BinOp::Concat => { + local_scalar_echo_expr(left, assigned, static_call_supported)?; + local_scalar_echo_expr(right, assigned, static_call_supported)?; + Some(DirectLocalStoreScalarKind::String) + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + let then_kind = local_scalar_echo_expr(then_expr, assigned, static_call_supported)?; + let else_kind = local_scalar_echo_expr(else_expr, assigned, static_call_supported)?; + (then_kind == else_kind).then_some(then_kind) + } + _ => local_scalar_value_expr(expr, assigned, static_call_supported), + } +} + +/// Checks an int/bool expression for the local-scalar AOT subset. +fn local_scalar_value_expr( + expr: &Expr, + assigned: &BTreeMap, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::Null => Some(DirectLocalStoreScalarKind::Null), + ExprKind::IntLiteral(_) => Some(DirectLocalStoreScalarKind::Int), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(DirectLocalStoreScalarKind::Float) + } + ExprKind::BoolLiteral(_) => Some(DirectLocalStoreScalarKind::Bool), + ExprKind::StringLiteral(_) => Some(DirectLocalStoreScalarKind::String), + ExprKind::Variable(name) => assigned.get(name).copied(), + ExprKind::Negate(inner) => { + (local_scalar_value_expr(inner, assigned, static_call_supported)? + == DirectLocalStoreScalarKind::Int) + .then_some(DirectLocalStoreScalarKind::Int) + } + ExprKind::BitNot(inner) => { + (local_scalar_value_expr(inner, assigned, static_call_supported)? + == DirectLocalStoreScalarKind::Int) + .then_some(DirectLocalStoreScalarKind::Int) + } + ExprKind::Not(inner) => { + local_scalar_condition_expr(inner, assigned, static_call_supported)?; + Some(DirectLocalStoreScalarKind::Bool) + } + ExprKind::ErrorSuppress(inner) => { + local_scalar_value_expr(inner, assigned, static_call_supported) + } + ExprKind::Print(inner) => { + local_scalar_echo_expr(inner, assigned, static_call_supported)?; + Some(DirectLocalStoreScalarKind::Int) + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + local_scalar_condition_expr(condition, assigned, static_call_supported)?; + let then_kind = local_scalar_value_expr(then_expr, assigned, static_call_supported)?; + let else_kind = local_scalar_value_expr(else_expr, assigned, static_call_supported)?; + (then_kind == else_kind).then_some(then_kind) + } + ExprKind::BinaryOp { left, op, right } => { + local_scalar_binary_expr(left, op.clone(), right, assigned, static_call_supported) + } + ExprKind::FunctionCall { name, args } => { + let function_name = name.to_string(); + if let Some(kind) = local_scalar_construct_call_expr(&function_name, args, assigned) { + return Some(kind); + } + (fold_static_builtin_int_call(function_name.trim_start_matches('\\'), args).is_some() + || static_call_supported(&function_name, args)) + .then_some(DirectLocalStoreScalarKind::Int) + } + _ => None, + } +} + +/// Checks language-construct calls that local-scalar AOT can lower without scope reads. +fn local_scalar_construct_call_expr( + name: &str, + args: &[Expr], + assigned: &BTreeMap, +) -> Option { + if has_named_args(args) + || args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_))) + { + return None; + } + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "isset" => (!args.is_empty() + && args.iter().all( + |arg| matches!(&arg.kind, ExprKind::Variable(name) if assigned.contains_key(name)), + )) + .then_some(DirectLocalStoreScalarKind::Bool), + "empty" if args.len() == 1 => match &args[0].kind { + ExprKind::Variable(name) if assigned.contains_key(name) => { + Some(DirectLocalStoreScalarKind::Bool) + } + _ => { + local_scalar_value_expr(args.first()?, assigned, &|_, _| false)?; + Some(DirectLocalStoreScalarKind::Bool) + } + }, + _ => None, + } +} + +/// Checks a binary expression for the local-scalar AOT subset. +fn local_scalar_binary_expr( + left: &Expr, + op: BinOp, + right: &Expr, + assigned: &BTreeMap, + static_call_supported: &F, +) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + let left_kind = local_scalar_value_expr(left, assigned, static_call_supported)?; + let right_kind = local_scalar_value_expr(right, assigned, static_call_supported)?; + match op { + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::BitAnd + | BinOp::BitOr + | BinOp::BitXor + | BinOp::ShiftLeft + | BinOp::ShiftRight + if left_kind == DirectLocalStoreScalarKind::Int + && right_kind == DirectLocalStoreScalarKind::Int => + { + Some(DirectLocalStoreScalarKind::Int) + } + BinOp::Div + if local_scalar_numeric_kind(left_kind) && local_scalar_numeric_kind(right_kind) => + { + Some(DirectLocalStoreScalarKind::Float) + } + BinOp::Mod + if local_scalar_numeric_kind(left_kind) && local_scalar_numeric_kind(right_kind) => + { + Some(DirectLocalStoreScalarKind::Int) + } + BinOp::Lt | BinOp::Gt | BinOp::LtEq | BinOp::GtEq + if left_kind == DirectLocalStoreScalarKind::Int + && right_kind == DirectLocalStoreScalarKind::Int => + { + Some(DirectLocalStoreScalarKind::Bool) + } + BinOp::Eq | BinOp::NotEq + if left_kind == right_kind + && matches!( + left_kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Bool + ) => + { + Some(DirectLocalStoreScalarKind::Bool) + } + BinOp::And | BinOp::Or + if matches!( + left_kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Bool + ) && matches!( + right_kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Bool + ) => + { + Some(DirectLocalStoreScalarKind::Bool) + } + _ => None, + } +} + +/// Returns true when the scalar kind participates in numeric-only local AOT operations. +fn local_scalar_numeric_kind(kind: DirectLocalStoreScalarKind) -> bool { + matches!( + kind, + DirectLocalStoreScalarKind::Int | DirectLocalStoreScalarKind::Float + ) +} + +/// Checks the narrow write-only shape that codegen can store directly into caller locals. +fn direct_local_store_candidate_writes( + program: &[Stmt], +) -> Option> { + let mut writes = Vec::new(); + let mut saw_store = false; + let mut terminated = false; + for stmt in program { + if terminated { + return None; + } + match &stmt.kind { + StmtKind::Assign { name, value } => { + let kind = direct_local_store_static_scalar_expr(value)?; + writes.push((name.clone(), kind)); + saw_store = true; + } + StmtKind::Return(Some(expr)) => { + direct_local_store_static_scalar_expr(expr)?; + terminated = true; + } + StmtKind::Return(None) => { + terminated = true; + } + StmtKind::ExprStmt(expr) => { + direct_local_store_static_scalar_expr(expr)?; + } + _ => return None, + } + } + saw_store.then_some(writes) +} + +/// Returns the scalar kind when an expression can be materialized by direct local-store codegen. +fn direct_local_store_static_scalar_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::Null => Some(DirectLocalStoreScalarKind::Null), + ExprKind::BoolLiteral(_) => Some(DirectLocalStoreScalarKind::Bool), + ExprKind::IntLiteral(_) => Some(DirectLocalStoreScalarKind::Int), + ExprKind::StringLiteral(_) => Some(DirectLocalStoreScalarKind::String), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(DirectLocalStoreScalarKind::Float) + } + ExprKind::Negate(inner) => direct_local_store_static_numeric_expr(inner), + ExprKind::Not(inner) => { + direct_local_store_static_scalar_expr(inner).map(|_| DirectLocalStoreScalarKind::Bool) + } + ExprKind::BinaryOp { left, op, right } => match op { + BinOp::Concat => { + direct_local_store_static_concat_operand(left)?; + direct_local_store_static_concat_operand(right)?; + Some(DirectLocalStoreScalarKind::String) + } + _ => None, + }, + _ => None, + } +} + +/// Returns the scalar kind for a numeric expression accepted by unary minus. +fn direct_local_store_static_numeric_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(_) => Some(DirectLocalStoreScalarKind::Int), + ExprKind::FloatLiteral(value) if value.is_finite() => { + Some(DirectLocalStoreScalarKind::Float) + } + _ => None, + } +} + +/// Returns true when an expression is safe for compile-time PHP string concatenation. +fn direct_local_store_static_concat_operand(expr: &Expr) -> Option<()> { + match direct_local_store_static_scalar_expr(expr)? { + DirectLocalStoreScalarKind::Null + | DirectLocalStoreScalarKind::Bool + | DirectLocalStoreScalarKind::Int + | DirectLocalStoreScalarKind::String => Some(()), + DirectLocalStoreScalarKind::Float => None, + } +} + +/// Returns a deterministic internal function name for a literal eval fragment. +pub(crate) fn eir_function_name(fragment: &str) -> String { + format!( + "{}_{:016x}", + EIR_AOT_FUNCTION_PREFIX, + stable_fragment_hash(fragment) + ) +} + +/// Returns a deterministic internal function name for a scope-read eval fragment. +pub(crate) fn eir_scope_read_function_name(fragment: &str) -> String { + format!( + "{}_scope_{:016x}", + EIR_AOT_FUNCTION_PREFIX, + stable_fragment_hash(fragment) + ) +} + +/// Builds the shared literal eval AOT plan for scan, lowering, and codegen decisions. +pub(crate) fn plan_literal_fragment_with_static_calls( + fragment: &str, + static_call_supported: F, +) -> EvalAotPlan +where + F: Fn(&str, &[Expr]) -> bool, +{ + plan_literal_fragment_with_static_and_method_calls( + fragment, + static_call_supported, + |_, _, _| false, + ) +} + +/// Builds the shared literal eval AOT plan with function and static-method support. +pub(crate) fn plan_literal_fragment_with_static_and_method_calls( + fragment: &str, + static_call_supported: F, + static_method_supported: M, +) -> EvalAotPlan +where + F: Fn(&str, &[Expr]) -> bool, + M: Fn(&StaticReceiver, &str, &[Expr]) -> bool, +{ + let Some(program) = parse_literal_fragment(fragment) else { + return parse_error_plan(); + }; + plan_parsed_literal_fragment_with_static_and_method_calls( + fragment, + program, + static_call_supported, + static_method_supported, + ) +} + +/// Builds the literal eval AOT plan with call-site source and static-method metadata. +pub(crate) fn plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment: &str, + source_path: Option<&str>, + static_call_supported: F, + static_method_supported: M, +) -> EvalAotPlan +where + F: Fn(&str, &[Expr]) -> bool, + M: Fn(&StaticReceiver, &str, &[Expr]) -> bool, +{ + let Some(program) = parse_literal_fragment_with_source_path(fragment, source_path) else { + return parse_error_plan(); + }; + plan_parsed_literal_fragment_with_static_and_method_calls( + fragment, + program, + static_call_supported, + static_method_supported, + ) +} + +/// Returns a conservative plan for fragments that cannot be parsed statically. +fn parse_error_plan() -> EvalAotPlan { + EvalAotPlan { + function_name: None, + eir_program: None, + scope_read_function_name: None, + scope_read_eir_program: None, + reads: BTreeSet::new(), + array_read_constraints: BTreeSet::new(), + assoc_array_read_constraints: BTreeSet::new(), + float_predicate_read_constraints: BTreeSet::new(), + writes: BTreeSet::new(), + scope_direct_writes: BTreeSet::new(), + scope_flush_writes: BTreeSet::new(), + creates_unknown_vars: true, + needs_eval_context: true, + needs_global_scope: true, + fallback_reason: Some(EvalAotFallbackReason::ParseError), + native_only_scalar: false, + } +} + +/// Builds the shared literal eval AOT plan from an already parsed fragment program. +fn plan_parsed_literal_fragment_with_static_and_method_calls( + fragment: &str, + program: Program, + static_call_supported: F, + static_method_supported: M, +) -> EvalAotPlan +where + F: Fn(&str, &[Expr]) -> bool, + M: Fn(&StaticReceiver, &str, &[Expr]) -> bool, +{ + let mut scope_access = collect_scope_accesses(&program); + let fragment_scope_reads = scope_access.reads.clone(); + scope_access.reads = collect_scope_reads_before_writes(&program); + let folded_program = fold_static_builtin_calls_in_program(program.clone()); + let support = EirStaticCallPredicates { + function: &static_call_supported, + static_method: &static_method_supported, + }; + let eir_program = + program_is_eir_function_safe(&folded_program, &support).then_some(folded_program.clone()); + let scope_names = scope_access + .reads + .union(&scope_access.writes) + .cloned() + .collect::>(); + let scope_write_only_linear = scope_access.reads.is_empty() + && fragment_scope_reads.is_empty() + && !scope_access.writes.is_empty() + && program_is_eir_scope_write_linear_function_safe(&folded_program, &support, &scope_names); + let scope_eir_safe = eir_program.is_none() + && !scope_names.is_empty() + && !scope_access.creates_unknown_vars + && program_is_eir_scope_read_function_safe(&folded_program, &support, &scope_names); + let scope_flush_local = + scope_eir_safe && scope_access.reads.is_empty() && !scope_access.writes.is_empty(); + let scope_direct = scope_eir_safe + && !scope_flush_local + && (!scope_access.reads.is_empty() || scope_write_only_linear); + let scope_direct_writes = if scope_direct { + scope_access.writes.clone() + } else { + BTreeSet::new() + }; + let scope_flush_writes = if scope_flush_local { + scope_access.writes.clone() + } else { + BTreeSet::new() + }; + let array_read_constraint_sets = + collect_array_scope_read_constraint_sets(&folded_program, &scope_access.reads); + let array_read_constraints = array_read_constraint_sets.array_like; + let assoc_array_read_constraints = array_read_constraint_sets.assoc; + let float_predicate_read_constraints = + collect_float_predicate_scope_read_constraints(&folded_program, &scope_access.reads); + let scope_read_eir_program = (scope_direct || scope_flush_local).then_some(folded_program); + let native_only_scalar = program_is_native_only_scalar(&program, &static_call_supported); + let is_fully_static_no_bridge = eir_program.is_some() || native_only_scalar; + let has_scope_read_eir = scope_read_eir_program.is_some(); + let needs_global_scope = + !is_fully_static_no_bridge && !has_scope_read_eir && scope_access.has_scope_access(); + EvalAotPlan { + function_name: eir_program.as_ref().map(|_| eir_function_name(fragment)), + eir_program, + scope_read_function_name: scope_read_eir_program + .as_ref() + .map(|_| eir_scope_read_function_name(fragment)), + scope_read_eir_program, + reads: scope_access.reads, + array_read_constraints, + assoc_array_read_constraints, + float_predicate_read_constraints, + writes: scope_access.writes, + scope_direct_writes, + scope_flush_writes, + creates_unknown_vars: scope_access.creates_unknown_vars, + needs_eval_context: !is_fully_static_no_bridge && !has_scope_read_eir, + needs_global_scope, + fallback_reason: (!is_fully_static_no_bridge && !has_scope_read_eir) + .then(|| classify_fallback_reason(&program)), + native_only_scalar, + } +} + +/// Classifies the first visible reason this fragment cannot avoid the bridge. +fn classify_fallback_reason(program: &[Stmt]) -> EvalAotFallbackReason { + program + .iter() + .find_map(stmt_fallback_reason) + .unwrap_or(EvalAotFallbackReason::UnsupportedScope) +} + +/// Classifies one statement for a human-readable eval AOT fallback marker. +fn stmt_fallback_reason(stmt: &Stmt) -> Option { + match &stmt.kind { + StmtKind::Include { .. } | StmtKind::IncludeOnceMark { .. } => { + Some(EvalAotFallbackReason::IncludeOrRequire) + } + StmtKind::IncludeOnceGuard { body, .. } + | StmtKind::Synthetic(body) + | StmtKind::NamespaceBlock { body, .. } => body.iter().find_map(stmt_fallback_reason), + StmtKind::FunctionDecl { .. } + | StmtKind::FunctionVariantGroup { .. } + | StmtKind::FunctionVariantMark { .. } + | StmtKind::ConstDecl { .. } + | StmtKind::ClassDecl { .. } + | StmtKind::EnumDecl { .. } + | StmtKind::PackedClassDecl { .. } + | StmtKind::InterfaceDecl { .. } + | StmtKind::TraitDecl { .. } + | StmtKind::ExternFunctionDecl { .. } + | StmtKind::ExternClassDecl { .. } + | StmtKind::ExternGlobalDecl { .. } => Some(EvalAotFallbackReason::Declaration), + StmtKind::Global { .. } | StmtKind::StaticVar { .. } => { + Some(EvalAotFallbackReason::GlobalOrStatic) + } + StmtKind::RefAssign { .. } => Some(EvalAotFallbackReason::ReferenceOrByRef), + StmtKind::Foreach { + array, + value_by_ref, + body, + .. + } => { + if *value_by_ref { + return Some(EvalAotFallbackReason::ReferenceOrByRef); + } + expr_fallback_reason(array) + .or_else(|| body.iter().find_map(stmt_fallback_reason)) + .or(Some(EvalAotFallbackReason::ArrayOrIterable)) + } + StmtKind::Try { .. } | StmtKind::Throw(_) => Some(EvalAotFallbackReason::TryOrThrow), + StmtKind::ArrayAssign { .. } + | StmtKind::NestedArrayAssign { .. } + | StmtKind::ArrayPush { .. } + | StmtKind::ListUnpack { .. } => Some(EvalAotFallbackReason::ArrayOrIterable), + StmtKind::PropertyAssign { .. } + | StmtKind::StaticPropertyAssign { .. } + | StmtKind::StaticPropertyArrayPush { .. } + | StmtKind::StaticPropertyArrayAssign { .. } + | StmtKind::PropertyArrayPush { .. } + | StmtKind::PropertyArrayAssign { .. } => Some(EvalAotFallbackReason::ObjectOrMemberAccess), + StmtKind::Echo(expr) | StmtKind::ExprStmt(expr) | StmtKind::Return(Some(expr)) => { + expr_fallback_reason(expr) + } + StmtKind::Assign { value, .. } | StmtKind::TypedAssign { value, .. } => { + expr_fallback_reason(value) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => expr_fallback_reason(condition) + .or_else(|| then_body.iter().find_map(stmt_fallback_reason)) + .or_else(|| { + elseif_clauses.iter().find_map(|(condition, body)| { + expr_fallback_reason(condition) + .or_else(|| body.iter().find_map(stmt_fallback_reason)) + }) + }) + .or_else(|| { + else_body + .as_deref() + .and_then(|body| body.iter().find_map(stmt_fallback_reason)) + }), + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + expr_fallback_reason(condition).or_else(|| body.iter().find_map(stmt_fallback_reason)) + } + StmtKind::For { + init, + condition, + update, + body, + } => init + .as_deref() + .and_then(stmt_fallback_reason) + .or_else(|| condition.as_ref().and_then(expr_fallback_reason)) + .or_else(|| update.as_deref().and_then(stmt_fallback_reason)) + .or_else(|| body.iter().find_map(stmt_fallback_reason)), + StmtKind::Switch { + subject, + cases, + default, + } => expr_fallback_reason(subject) + .or_else(|| { + cases.iter().find_map(|(conditions, body)| { + conditions + .iter() + .find_map(expr_fallback_reason) + .or_else(|| body.iter().find_map(stmt_fallback_reason)) + }) + }) + .or_else(|| { + default + .as_deref() + .and_then(|body| body.iter().find_map(stmt_fallback_reason)) + }) + .or(Some(EvalAotFallbackReason::UnsupportedControlFlow)), + StmtKind::Break(_) | StmtKind::Continue(_) => { + Some(EvalAotFallbackReason::UnsupportedControlFlow) + } + StmtKind::Return(None) | StmtKind::NamespaceDecl { .. } | StmtKind::UseDecl { .. } => None, + StmtKind::IfDef { + then_body, + else_body, + .. + } => then_body.iter().find_map(stmt_fallback_reason).or_else(|| { + else_body + .as_deref() + .and_then(|body| body.iter().find_map(stmt_fallback_reason)) + }), + } +} + +/// Classifies one expression for a human-readable eval AOT fallback marker. +fn expr_fallback_reason(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::Variable(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null => None, + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Clone(inner) + | ExprKind::YieldFrom(inner) => expr_fallback_reason(inner), + ExprKind::Throw(_) => Some(EvalAotFallbackReason::TryOrThrow), + ExprKind::BinaryOp { left, right, .. } + | ExprKind::NullCoalesce { + value: left, + default: right, + } + | ExprKind::ShortTernary { + value: left, + default: right, + } + | ExprKind::ArrayAccess { + array: left, + index: right, + } => expr_fallback_reason(left) + .or_else(|| expr_fallback_reason(right)) + .or(Some(EvalAotFallbackReason::ArrayOrIterable)), + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => expr_fallback_reason(condition) + .or_else(|| expr_fallback_reason(then_expr)) + .or_else(|| expr_fallback_reason(else_expr)), + ExprKind::Cast { target, expr } => { + if matches!(target, CastType::Array) { + return Some(EvalAotFallbackReason::ArrayOrIterable); + } + expr_fallback_reason(expr) + } + ExprKind::Match { + subject, + arms, + default, + } => expr_fallback_reason(subject) + .or_else(|| { + arms.iter().find_map(|(conditions, result)| { + conditions + .iter() + .find_map(expr_fallback_reason) + .or_else(|| expr_fallback_reason(result)) + }) + }) + .or_else(|| default.as_deref().and_then(expr_fallback_reason)), + ExprKind::FunctionCall { args, .. } => args + .iter() + .find_map(expr_fallback_reason) + .or(Some(EvalAotFallbackReason::UnsupportedStaticCall)), + ExprKind::ClosureCall { .. } | ExprKind::ExprCall { .. } => { + Some(EvalAotFallbackReason::DynamicCall) + } + ExprKind::Pipe { value, callable } => expr_fallback_reason(value) + .or_else(|| expr_fallback_reason(callable)) + .or(Some(EvalAotFallbackReason::DynamicCall)), + ExprKind::NewDynamic { .. } | ExprKind::NewDynamicObject { .. } => { + Some(EvalAotFallbackReason::DynamicClassOrMember) + } + ExprKind::DynamicPropertyAccess { .. } + | ExprKind::NullsafeDynamicPropertyAccess { .. } + | ExprKind::NullsafeDynamicMethodCall { .. } => { + Some(EvalAotFallbackReason::DynamicClassOrMember) + } + ExprKind::NewObject { .. } + | ExprKind::NewScopedObject { .. } + | ExprKind::PropertyAccess { .. } + | ExprKind::NullsafePropertyAccess { .. } + | ExprKind::StaticPropertyAccess { .. } + | ExprKind::MethodCall { .. } + | ExprKind::NullsafeMethodCall { .. } + | ExprKind::StaticMethodCall { .. } + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } + | ExprKind::This => Some(EvalAotFallbackReason::ObjectOrMemberAccess), + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) | ExprKind::Spread(_) => { + Some(EvalAotFallbackReason::ArrayOrIterable) + } + ExprKind::Assignment { .. } + | ExprKind::PreIncrement(_) + | ExprKind::PostIncrement(_) + | ExprKind::PreDecrement(_) + | ExprKind::PostDecrement(_) + | ExprKind::NamedArg { .. } => Some(EvalAotFallbackReason::UnsupportedScope), + ExprKind::Closure { .. } => Some(EvalAotFallbackReason::Declaration), + ExprKind::IncludeValue { .. } => Some(EvalAotFallbackReason::IncludeOrRequire), + ExprKind::InstanceOf { value, target } => expr_fallback_reason(value) + .or_else(|| match target { + crate::parser::ast::InstanceOfTarget::Name(_) => None, + crate::parser::ast::InstanceOfTarget::Expr(expr) => expr_fallback_reason(expr), + }) + .or(Some(EvalAotFallbackReason::ObjectOrMemberAccess)), + ExprKind::FirstClassCallable(target) => callable_target_fallback_reason(target), + ExprKind::ConstRef(_) | ExprKind::MagicConstant(_) => { + Some(EvalAotFallbackReason::UnsupportedConstruct) + } + ExprKind::PtrCast { expr, .. } => { + expr_fallback_reason(expr).or(Some(EvalAotFallbackReason::UnsupportedConstruct)) + } + ExprKind::BufferNew { len, .. } => { + expr_fallback_reason(len).or(Some(EvalAotFallbackReason::UnsupportedConstruct)) + } + ExprKind::Yield { .. } => Some(EvalAotFallbackReason::UnsupportedControlFlow), + } +} + +/// Classifies first-class callable expressions for fallback markers. +fn callable_target_fallback_reason(target: &CallableTarget) -> Option { + match target { + CallableTarget::Function(_) => Some(EvalAotFallbackReason::DynamicCall), + CallableTarget::StaticMethod { .. } => Some(EvalAotFallbackReason::ObjectOrMemberAccess), + CallableTarget::Method { object, .. } => { + expr_fallback_reason(object).or(Some(EvalAotFallbackReason::ObjectOrMemberAccess)) + } + } +} + +/// Returns true for the native-only scalar eval subset in an already parsed program. +fn program_is_native_only_scalar(program: &[Stmt], static_call_supported: &F) -> bool +where + F: Fn(&str, &[Expr]) -> bool, +{ + let mut terminated = false; + for stmt in program { + if terminated { + return false; + } + let Some(done) = stmt_is_native_only_scalar(stmt, &static_call_supported) else { + return false; + }; + terminated = done; + } + true +} + +/// Variable read/write metadata collected from a parsed eval fragment. +struct EvalScopeAccess { + reads: BTreeSet, + writes: BTreeSet, + creates_unknown_vars: bool, +} + +impl EvalScopeAccess { + /// Creates an empty eval scope access accumulator. + fn new() -> Self { + Self { + reads: BTreeSet::new(), + writes: BTreeSet::new(), + creates_unknown_vars: false, + } + } + + /// Returns true when the fragment touches any eval-visible variable storage. + fn has_scope_access(&self) -> bool { + !self.reads.is_empty() || !self.writes.is_empty() || self.creates_unknown_vars + } + + /// Records a variable read. + fn read(&mut self, name: &str) { + self.reads.insert(name.to_string()); + } + + /// Records a variable write. + fn write(&mut self, name: &str) { + self.writes.insert(name.to_string()); + } + + /// Marks an access shape that cannot be mapped to a static variable name. + fn unknown_write(&mut self) { + self.creates_unknown_vars = true; + } +} + +/// Collects conservative eval-scope reads and writes from a parsed fragment. +fn collect_scope_accesses(program: &[Stmt]) -> EvalScopeAccess { + let mut access = EvalScopeAccess::new(); + for stmt in program { + collect_stmt_scope_access(stmt, &mut access); + } + access +} + +/// Collects variable reads that must come from the caller before local writes exist. +fn collect_scope_reads_before_writes(program: &[Stmt]) -> BTreeSet { + let mut reads = BTreeSet::new(); + let mut assigned = BTreeSet::new(); + collect_block_scope_reads_before_writes(program, &mut assigned, &mut reads); + reads +} + +/// Collects caller reads across a statement block and tracks definite local writes. +fn collect_block_scope_reads_before_writes( + body: &[Stmt], + assigned: &mut BTreeSet, + reads: &mut BTreeSet, +) { + for stmt in body { + collect_stmt_scope_reads_before_writes(stmt, assigned, reads); + } +} + +/// Collects caller reads for one statement before updating local assignment facts. +fn collect_stmt_scope_reads_before_writes( + stmt: &Stmt, + assigned: &mut BTreeSet, + reads: &mut BTreeSet, +) { + match &stmt.kind { + StmtKind::Assign { name, value } | StmtKind::TypedAssign { name, value, .. } => { + collect_expr_scope_reads_before_writes(value, assigned, reads); + assigned.insert(name.clone()); + } + StmtKind::Echo(expr) + | StmtKind::Throw(expr) + | StmtKind::ExprStmt(expr) + | StmtKind::Return(Some(expr)) => { + collect_expr_scope_reads_before_writes(expr, assigned, reads); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + collect_expr_scope_reads_before_writes(condition, assigned, reads); + let before = assigned.clone(); + let mut branch_outputs = Vec::new(); + let mut then_assigned = before.clone(); + collect_block_scope_reads_before_writes(then_body, &mut then_assigned, reads); + branch_outputs.push(then_assigned); + for (condition, body) in elseif_clauses { + collect_expr_scope_reads_before_writes(condition, &before, reads); + let mut branch_assigned = before.clone(); + collect_block_scope_reads_before_writes(body, &mut branch_assigned, reads); + branch_outputs.push(branch_assigned); + } + if let Some(else_body) = else_body { + let mut else_assigned = before.clone(); + collect_block_scope_reads_before_writes(else_body, &mut else_assigned, reads); + branch_outputs.push(else_assigned); + retain_definitely_assigned_after_branches(assigned, before, &branch_outputs); + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + collect_expr_scope_reads_before_writes(condition, assigned, reads); + let mut body_assigned = assigned.clone(); + collect_block_scope_reads_before_writes(body, &mut body_assigned, reads); + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + collect_stmt_scope_reads_before_writes(init, assigned, reads); + } + if let Some(condition) = condition { + collect_expr_scope_reads_before_writes(condition, assigned, reads); + } + let mut body_assigned = assigned.clone(); + collect_block_scope_reads_before_writes(body, &mut body_assigned, reads); + if let Some(update) = update { + collect_stmt_scope_reads_before_writes(update, &mut body_assigned, reads); + } + } + StmtKind::Foreach { + array, + key_var, + value_var, + body, + .. + } => { + collect_expr_scope_reads_before_writes(array, assigned, reads); + if expr_is_static_empty_array_literal_source(array) { + return; + } + let mut body_assigned = assigned.clone(); + body_assigned.insert(value_var.clone()); + if let Some(key_var) = key_var { + body_assigned.insert(key_var.clone()); + } + collect_block_scope_reads_before_writes(body, &mut body_assigned, reads); + if expr_is_non_empty_static_array_literal_source(array) { + assigned.insert(value_var.clone()); + if let Some(key_var) = key_var { + assigned.insert(key_var.clone()); + } + } + } + StmtKind::Switch { + subject, + cases, + default, + } => { + collect_expr_scope_reads_before_writes(subject, assigned, reads); + for (conditions, body) in cases { + for condition in conditions { + collect_expr_scope_reads_before_writes(condition, assigned, reads); + } + let mut case_assigned = assigned.clone(); + collect_block_scope_reads_before_writes(body, &mut case_assigned, reads); + } + if let Some(default) = default { + let mut default_assigned = assigned.clone(); + collect_block_scope_reads_before_writes(default, &mut default_assigned, reads); + } + } + StmtKind::Synthetic(body) | StmtKind::NamespaceBlock { body, .. } => { + collect_block_scope_reads_before_writes(body, assigned, reads); + } + _ => { + let mut access = EvalScopeAccess::new(); + collect_stmt_scope_access(stmt, &mut access); + extend_reads_not_assigned(reads, assigned, access.reads); + assigned.extend(access.writes); + } + } +} + +/// Keeps only names assigned on every branch after an if/elseif/else chain. +fn retain_definitely_assigned_after_branches( + assigned: &mut BTreeSet, + before: BTreeSet, + branch_outputs: &[BTreeSet], +) { + let mut definitely = before; + for name in branch_outputs + .first() + .into_iter() + .flat_map(|branch| branch.iter()) + { + if branch_outputs.iter().all(|branch| branch.contains(name)) { + definitely.insert(name.clone()); + } + } + *assigned = definitely; +} + +/// Collects caller reads from one expression using current assignment facts. +fn collect_expr_scope_reads_before_writes( + expr: &Expr, + assigned: &BTreeSet, + reads: &mut BTreeSet, +) { + match &expr.kind { + ExprKind::Variable(name) => { + if !assigned.contains(name) { + reads.insert(name.clone()); + } + } + ExprKind::Assignment { + prelude, + target, + value, + result_target, + .. + } => { + let mut expr_assigned = assigned.clone(); + for stmt in prelude { + collect_stmt_scope_reads_before_writes(stmt, &mut expr_assigned, reads); + } + collect_expr_scope_reads_before_writes(value, &expr_assigned, reads); + match &target.kind { + ExprKind::Variable(name) => { + expr_assigned.insert(name.clone()); + } + _ => collect_expr_scope_reads_before_writes(target, &expr_assigned, reads), + } + if let Some(result_target) = result_target { + collect_expr_scope_reads_before_writes(result_target, &expr_assigned, reads); + } + } + _ => { + let mut access = EvalScopeAccess::new(); + collect_expr_scope_access(expr, &mut access); + extend_reads_not_assigned(reads, assigned, access.reads); + } + } +} + +/// Adds collected reads that are not already definitely local to this fragment. +fn extend_reads_not_assigned( + reads: &mut BTreeSet, + assigned: &BTreeSet, + names: BTreeSet, +) { + reads.extend(names.into_iter().filter(|name| !assigned.contains(name))); +} + +/// Caller-scope variables that need array-specific call-site proof. +#[derive(Default)] +struct ArrayScopeReadConstraintSets { + array_like: BTreeSet, + assoc: BTreeSet, +} + +/// Collects caller-scope reads that must be array-like for accepted AOT calls. +fn collect_array_scope_read_constraint_sets( + program: &[Stmt], + scope_reads: &BTreeSet, +) -> ArrayScopeReadConstraintSets { + let mut constraints = ArrayScopeReadConstraintSets::default(); + for stmt in program { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, &mut constraints); + } + constraints +} + +/// Collects array constraints from one statement in the EIR AOT subset. +fn collect_stmt_array_scope_read_constraints( + stmt: &Stmt, + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + match &stmt.kind { + StmtKind::Synthetic(body) => { + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Echo(expr) | StmtKind::ExprStmt(expr) | StmtKind::Return(Some(expr)) => { + collect_expr_array_scope_read_constraints(expr, scope_reads, constraints); + } + StmtKind::Assign { value, .. } => { + collect_expr_array_scope_read_constraints(value, scope_reads, constraints); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + for stmt in then_body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + for (condition, body) in elseif_clauses { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + if let Some(else_body) = else_body { + for stmt in else_body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + collect_stmt_array_scope_read_constraints(init, scope_reads, constraints); + } + if let Some(condition) = condition { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + } + if let Some(update) = update { + collect_stmt_array_scope_read_constraints(update, scope_reads, constraints); + } + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Foreach { array, body, .. } => { + if let ExprKind::Variable(variable) = &array.kind { + if scope_reads.contains(variable) { + constraints.array_like.insert(variable.clone()); + } + } + collect_expr_array_scope_read_constraints(array, scope_reads, constraints); + if expr_is_static_empty_array_literal_source(array) { + return; + } + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Switch { + subject, + cases, + default, + } => { + collect_expr_array_scope_read_constraints(subject, scope_reads, constraints); + for (conditions, body) in cases { + for condition in conditions { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + } + for stmt in body { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + if let Some(default) = default { + for stmt in default { + collect_stmt_array_scope_read_constraints(stmt, scope_reads, constraints); + } + } + } + _ => {} + } +} + +/// Collects array constraints from one expression in the EIR AOT subset. +fn collect_expr_array_scope_read_constraints( + expr: &Expr, + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + match &expr.kind { + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::NamedArg { value: inner, .. } => { + collect_expr_array_scope_read_constraints(inner, scope_reads, constraints); + } + ExprKind::BinaryOp { left, right, .. } => { + collect_expr_array_scope_read_constraints(left, scope_reads, constraints); + collect_expr_array_scope_read_constraints(right, scope_reads, constraints); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + collect_expr_array_scope_read_constraints(then_expr, scope_reads, constraints); + collect_expr_array_scope_read_constraints(else_expr, scope_reads, constraints); + } + ExprKind::ShortTernary { value, default } | ExprKind::NullCoalesce { value, default } => { + collect_expr_array_scope_read_constraints(value, scope_reads, constraints); + collect_expr_array_scope_read_constraints(default, scope_reads, constraints); + } + ExprKind::Cast { expr, .. } => { + collect_expr_array_scope_read_constraints(expr, scope_reads, constraints); + } + ExprKind::Match { + subject, + arms, + default, + } => { + collect_expr_array_scope_read_constraints(subject, scope_reads, constraints); + for (conditions, result) in arms { + for condition in conditions { + collect_expr_array_scope_read_constraints(condition, scope_reads, constraints); + } + collect_expr_array_scope_read_constraints(result, scope_reads, constraints); + } + if let Some(default) = default { + collect_expr_array_scope_read_constraints(default, scope_reads, constraints); + } + } + ExprKind::ArrayAccess { array, index } => { + collect_expr_array_scope_read_constraints(array, scope_reads, constraints); + collect_expr_array_scope_read_constraints(index, scope_reads, constraints); + } + ExprKind::ArrayLiteral(items) => { + for item in items { + collect_expr_array_scope_read_constraints(item, scope_reads, constraints); + } + } + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + collect_expr_array_scope_read_constraints(key, scope_reads, constraints); + collect_expr_array_scope_read_constraints(value, scope_reads, constraints); + } + } + ExprKind::FunctionCall { name, args } => { + collect_builtin_array_scope_read_constraints( + name.as_str(), + args, + scope_reads, + constraints, + ); + for arg in args { + collect_expr_array_scope_read_constraints(arg, scope_reads, constraints); + } + } + _ => {} + } +} + +/// Collects array caller-type constraints from supported runtime builtin calls. +fn collect_builtin_array_scope_read_constraints( + name: &str, + args: &[Expr], + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + let short_name = php_symbol_key(name.trim_start_matches('\\')); + let Some(args) = normalize_eir_runtime_builtin_args(&short_name, args) else { + return; + }; + match short_name.as_str() { + "count" if (1..=2).contains(&args.len()) && eir_count_mode_is_default_zero(args.get(1)) => { + collect_scope_array_like_constraint(&args[0], scope_reads, constraints); + } + "array_key_exists" + if args.len() == 2 && eir_array_key_exists_static_key_is_safe(&args[0]) => + { + collect_scope_array_like_constraint(&args[1], scope_reads, constraints); + if eir_array_key_exists_static_key_needs_assoc_array(&args[0]) { + collect_scope_assoc_array_constraint(&args[1], scope_reads, constraints); + } + } + _ => {} + } +} + +/// Records that one expression must be a caller-side array when it reads scope. +fn collect_scope_array_like_constraint( + expr: &Expr, + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + if let ExprKind::Variable(variable) = &expr.kind { + if scope_reads.contains(variable) { + constraints.array_like.insert(variable.clone()); + } + } +} + +/// Records that one expression must be a caller-side associative array. +fn collect_scope_assoc_array_constraint( + expr: &Expr, + scope_reads: &BTreeSet, + constraints: &mut ArrayScopeReadConstraintSets, +) { + if let ExprKind::Variable(variable) = &expr.kind { + if scope_reads.contains(variable) { + constraints.assoc.insert(variable.clone()); + } + } +} + +/// Collects caller-scope reads that must be int/float for IEEE float predicates. +fn collect_float_predicate_scope_read_constraints( + program: &[Stmt], + scope_reads: &BTreeSet, +) -> BTreeSet { + let mut constraints = BTreeSet::new(); + for stmt in program { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, &mut constraints); + } + constraints +} + +/// Collects float-predicate constraints from one statement in the EIR AOT subset. +fn collect_stmt_float_predicate_scope_read_constraints( + stmt: &Stmt, + scope_reads: &BTreeSet, + constraints: &mut BTreeSet, +) { + match &stmt.kind { + StmtKind::Synthetic(body) => { + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Echo(expr) | StmtKind::ExprStmt(expr) | StmtKind::Return(Some(expr)) => { + collect_expr_float_predicate_scope_read_constraints(expr, scope_reads, constraints); + } + StmtKind::Assign { value, .. } => { + collect_expr_float_predicate_scope_read_constraints(value, scope_reads, constraints); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + for stmt in then_body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + for (condition, body) in elseif_clauses { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints( + stmt, + scope_reads, + constraints, + ); + } + } + if let Some(else_body) = else_body { + for stmt in else_body { + collect_stmt_float_predicate_scope_read_constraints( + stmt, + scope_reads, + constraints, + ); + } + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + collect_stmt_float_predicate_scope_read_constraints(init, scope_reads, constraints); + } + if let Some(condition) = condition { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + } + if let Some(update) = update { + collect_stmt_float_predicate_scope_read_constraints( + update, + scope_reads, + constraints, + ); + } + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Foreach { array, body, .. } => { + collect_expr_float_predicate_scope_read_constraints(array, scope_reads, constraints); + if expr_is_static_empty_array_literal_source(array) { + return; + } + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints(stmt, scope_reads, constraints); + } + } + StmtKind::Switch { + subject, + cases, + default, + } => { + collect_expr_float_predicate_scope_read_constraints(subject, scope_reads, constraints); + for (conditions, body) in cases { + for condition in conditions { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + } + for stmt in body { + collect_stmt_float_predicate_scope_read_constraints( + stmt, + scope_reads, + constraints, + ); + } + } + if let Some(default) = default { + for stmt in default { + collect_stmt_float_predicate_scope_read_constraints( + stmt, + scope_reads, + constraints, + ); + } + } + } + _ => {} + } +} + +/// Collects float-predicate constraints from one expression in the EIR AOT subset. +fn collect_expr_float_predicate_scope_read_constraints( + expr: &Expr, + scope_reads: &BTreeSet, + constraints: &mut BTreeSet, +) { + match &expr.kind { + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::NamedArg { value: inner, .. } => { + collect_expr_float_predicate_scope_read_constraints(inner, scope_reads, constraints); + } + ExprKind::BinaryOp { left, right, .. } => { + collect_expr_float_predicate_scope_read_constraints(left, scope_reads, constraints); + collect_expr_float_predicate_scope_read_constraints(right, scope_reads, constraints); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + collect_expr_float_predicate_scope_read_constraints( + then_expr, + scope_reads, + constraints, + ); + collect_expr_float_predicate_scope_read_constraints( + else_expr, + scope_reads, + constraints, + ); + } + ExprKind::ShortTernary { value, default } | ExprKind::NullCoalesce { value, default } => { + collect_expr_float_predicate_scope_read_constraints(value, scope_reads, constraints); + collect_expr_float_predicate_scope_read_constraints(default, scope_reads, constraints); + } + ExprKind::Cast { expr, .. } => { + collect_expr_float_predicate_scope_read_constraints(expr, scope_reads, constraints); + } + ExprKind::Match { + subject, + arms, + default, + } => { + collect_expr_float_predicate_scope_read_constraints(subject, scope_reads, constraints); + for (conditions, result) in arms { + for condition in conditions { + collect_expr_float_predicate_scope_read_constraints( + condition, + scope_reads, + constraints, + ); + } + collect_expr_float_predicate_scope_read_constraints( + result, + scope_reads, + constraints, + ); + } + if let Some(default) = default { + collect_expr_float_predicate_scope_read_constraints( + default, + scope_reads, + constraints, + ); + } + } + ExprKind::ArrayAccess { array, index } => { + collect_expr_float_predicate_scope_read_constraints(array, scope_reads, constraints); + collect_expr_float_predicate_scope_read_constraints(index, scope_reads, constraints); + } + ExprKind::ArrayLiteral(items) => { + for item in items { + collect_expr_float_predicate_scope_read_constraints(item, scope_reads, constraints); + } + } + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + collect_expr_float_predicate_scope_read_constraints(key, scope_reads, constraints); + collect_expr_float_predicate_scope_read_constraints( + value, + scope_reads, + constraints, + ); + } + } + ExprKind::FunctionCall { name, args } => { + let name = php_symbol_key(name.as_str().trim_start_matches('\\')); + if matches!(name.as_str(), "is_finite" | "is_infinite" | "is_nan") && args.len() == 1 { + collect_scope_read_variables_in_expr(&args[0], scope_reads, constraints); + } + for arg in args { + collect_expr_float_predicate_scope_read_constraints(arg, scope_reads, constraints); + } + } + _ => {} + } +} + +/// Collects scope-read variable names that occur anywhere inside an expression. +fn collect_scope_read_variables_in_expr( + expr: &Expr, + scope_reads: &BTreeSet, + variables: &mut BTreeSet, +) { + match &expr.kind { + ExprKind::Variable(name) => { + if scope_reads.contains(name) { + variables.insert(name.clone()); + } + } + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::NamedArg { value: inner, .. } + | ExprKind::Cast { expr: inner, .. } => { + collect_scope_read_variables_in_expr(inner, scope_reads, variables); + } + ExprKind::BinaryOp { left, right, .. } => { + collect_scope_read_variables_in_expr(left, scope_reads, variables); + collect_scope_read_variables_in_expr(right, scope_reads, variables); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + collect_scope_read_variables_in_expr(condition, scope_reads, variables); + collect_scope_read_variables_in_expr(then_expr, scope_reads, variables); + collect_scope_read_variables_in_expr(else_expr, scope_reads, variables); + } + ExprKind::ShortTernary { value, default } | ExprKind::NullCoalesce { value, default } => { + collect_scope_read_variables_in_expr(value, scope_reads, variables); + collect_scope_read_variables_in_expr(default, scope_reads, variables); + } + ExprKind::ArrayAccess { array, index } => { + collect_scope_read_variables_in_expr(array, scope_reads, variables); + collect_scope_read_variables_in_expr(index, scope_reads, variables); + } + ExprKind::ArrayLiteral(items) => { + for item in items { + collect_scope_read_variables_in_expr(item, scope_reads, variables); + } + } + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + collect_scope_read_variables_in_expr(key, scope_reads, variables); + collect_scope_read_variables_in_expr(value, scope_reads, variables); + } + } + ExprKind::FunctionCall { args, .. } => { + for arg in args { + collect_scope_read_variables_in_expr(arg, scope_reads, variables); + } + } + ExprKind::Match { + subject, + arms, + default, + } => { + collect_scope_read_variables_in_expr(subject, scope_reads, variables); + for (conditions, result) in arms { + for condition in conditions { + collect_scope_read_variables_in_expr(condition, scope_reads, variables); + } + collect_scope_read_variables_in_expr(result, scope_reads, variables); + } + if let Some(default) = default { + collect_scope_read_variables_in_expr(default, scope_reads, variables); + } + } + _ => {} + } +} + +/// Adds one statement's eval-scope reads and writes to the accumulator. +fn collect_stmt_scope_access(stmt: &Stmt, access: &mut EvalScopeAccess) { + match &stmt.kind { + StmtKind::Echo(expr) + | StmtKind::Throw(expr) + | StmtKind::ExprStmt(expr) + | StmtKind::Return(Some(expr)) => collect_expr_scope_access(expr, access), + StmtKind::Return(None) + | StmtKind::Break(_) + | StmtKind::Continue(_) + | StmtKind::NamespaceDecl { .. } + | StmtKind::UseDecl { .. } + | StmtKind::IncludeOnceMark { .. } + | StmtKind::FunctionVariantMark { .. } => {} + StmtKind::Assign { name, value } | StmtKind::TypedAssign { name, value, .. } => { + collect_expr_scope_access(value, access); + access.write(name); + } + StmtKind::RefAssign { target, source } => { + access.write(target); + access.read(source); + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + collect_expr_scope_access(condition, access); + collect_block_scope_access(then_body, access); + for (condition, body) in elseif_clauses { + collect_expr_scope_access(condition, access); + collect_block_scope_access(body, access); + } + if let Some(else_body) = else_body { + collect_block_scope_access(else_body, access); + } + } + StmtKind::IfDef { + then_body, + else_body, + .. + } => { + collect_block_scope_access(then_body, access); + if let Some(else_body) = else_body { + collect_block_scope_access(else_body, access); + } + } + StmtKind::While { condition, body } | StmtKind::DoWhile { condition, body } => { + collect_expr_scope_access(condition, access); + collect_block_scope_access(body, access); + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + collect_stmt_scope_access(init, access); + } + if let Some(condition) = condition { + collect_expr_scope_access(condition, access); + } + if let Some(update) = update { + collect_stmt_scope_access(update, access); + } + collect_block_scope_access(body, access); + } + StmtKind::ArrayAssign { + array, + index, + value, + } => { + access.read(array); + access.write(array); + collect_expr_scope_access(index, access); + collect_expr_scope_access(value, access); + } + StmtKind::NestedArrayAssign { target, value } => { + collect_assignment_target_scope_access(target, access); + collect_expr_scope_access(value, access); + } + StmtKind::ArrayPush { array, value } => { + access.read(array); + access.write(array); + collect_expr_scope_access(value, access); + } + StmtKind::Foreach { + array, + key_var, + value_var, + body, + .. + } => { + collect_expr_scope_access(array, access); + if expr_is_static_empty_array_literal_source(array) { + return; + } + if let Some(key_var) = key_var { + access.write(key_var); + } + access.write(value_var); + collect_block_scope_access(body, access); + } + StmtKind::Switch { + subject, + cases, + default, + } => { + collect_expr_scope_access(subject, access); + for (conditions, body) in cases { + for condition in conditions { + collect_expr_scope_access(condition, access); + } + collect_block_scope_access(body, access); + } + if let Some(default) = default { + collect_block_scope_access(default, access); + } + } + StmtKind::IncludeOnceGuard { body, .. } | StmtKind::Synthetic(body) => { + collect_block_scope_access(body, access); + } + StmtKind::Try { + try_body, + catches, + finally_body, + } => { + collect_block_scope_access(try_body, access); + for catch in catches { + if let Some(variable) = &catch.variable { + access.write(variable); + } + collect_block_scope_access(&catch.body, access); + } + if let Some(finally_body) = finally_body { + collect_block_scope_access(finally_body, access); + } + } + StmtKind::NamespaceBlock { body, .. } => collect_block_scope_access(body, access), + StmtKind::FunctionDecl { .. } + | StmtKind::FunctionVariantGroup { .. } + | StmtKind::ConstDecl { .. } + | StmtKind::ClassDecl { .. } + | StmtKind::EnumDecl { .. } + | StmtKind::PackedClassDecl { .. } + | StmtKind::InterfaceDecl { .. } + | StmtKind::TraitDecl { .. } + | StmtKind::ExternFunctionDecl { .. } + | StmtKind::ExternClassDecl { .. } + | StmtKind::ExternGlobalDecl { .. } => {} + StmtKind::ListUnpack { vars, value } => { + collect_expr_scope_access(value, access); + for var in vars { + access.write(var); + } + } + StmtKind::Global { vars } => { + for var in vars { + access.write(var); + } + access.creates_unknown_vars = true; + } + StmtKind::StaticVar { name, init } => { + collect_expr_scope_access(init, access); + access.write(name); + access.creates_unknown_vars = true; + } + StmtKind::PropertyAssign { object, value, .. } + | StmtKind::PropertyArrayPush { object, value, .. } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(value, access); + } + StmtKind::PropertyArrayAssign { + object, + index, + value, + .. + } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(index, access); + collect_expr_scope_access(value, access); + } + StmtKind::StaticPropertyAssign { value, .. } + | StmtKind::StaticPropertyArrayPush { value, .. } => { + collect_expr_scope_access(value, access); + } + StmtKind::StaticPropertyArrayAssign { index, value, .. } => { + collect_expr_scope_access(index, access); + collect_expr_scope_access(value, access); + } + StmtKind::Include { path, .. } => collect_expr_scope_access(path, access), + } +} + +/// Adds every statement in a block to the scope access accumulator. +fn collect_block_scope_access(body: &[Stmt], access: &mut EvalScopeAccess) { + for stmt in body { + collect_stmt_scope_access(stmt, access); + } +} + +/// Adds one expression's eval-scope reads and writes to the accumulator. +fn collect_expr_scope_access(expr: &Expr, access: &mut EvalScopeAccess) { + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null + | ExprKind::ConstRef(_) + | ExprKind::ClassConstant { .. } + | ExprKind::ScopedConstantAccess { .. } + | ExprKind::MagicConstant(_) => {} + ExprKind::Variable(name) + | ExprKind::PreIncrement(name) + | ExprKind::PostIncrement(name) + | ExprKind::PreDecrement(name) + | ExprKind::PostDecrement(name) => { + access.read(name); + if matches!( + &expr.kind, + ExprKind::PreIncrement(_) + | ExprKind::PostIncrement(_) + | ExprKind::PreDecrement(_) + | ExprKind::PostDecrement(_) + ) { + access.write(name); + } + } + ExprKind::BinaryOp { left, right, .. } => { + collect_expr_scope_access(left, access); + collect_expr_scope_access(right, access); + } + ExprKind::InstanceOf { value, target } => { + collect_expr_scope_access(value, access); + if let crate::parser::ast::InstanceOfTarget::Expr(target) = target { + collect_expr_scope_access(target, access); + } + } + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::Throw(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) + | ExprKind::Spread(inner) + | ExprKind::Clone(inner) + | ExprKind::YieldFrom(inner) + | ExprKind::Cast { expr: inner, .. } + | ExprKind::PtrCast { expr: inner, .. } => collect_expr_scope_access(inner, access), + ExprKind::NullCoalesce { value, default } + | ExprKind::ShortTernary { value, default } + | ExprKind::Pipe { + value, + callable: default, + } => { + collect_expr_scope_access(value, access); + collect_expr_scope_access(default, access); + } + ExprKind::Assignment { + target, + value, + result_target, + prelude, + .. + } => { + for stmt in prelude { + collect_stmt_scope_access(stmt, access); + } + collect_assignment_target_scope_access(target, access); + collect_expr_scope_access(value, access); + if let Some(result_target) = result_target { + collect_assignment_target_scope_access(result_target, access); + } + } + ExprKind::FunctionCall { args, .. } + | ExprKind::ClosureCall { args, .. } + | ExprKind::ExprCall { args, .. } + | ExprKind::NewObject { args, .. } + | ExprKind::StaticMethodCall { args, .. } + | ExprKind::NewScopedObject { args, .. } => { + if let ExprKind::ExprCall { callee, .. } = &expr.kind { + collect_expr_scope_access(callee, access); + } + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::ArrayLiteral(items) => { + for item in items { + collect_expr_scope_access(item, access); + } + } + ExprKind::ArrayLiteralAssoc(pairs) => { + for (key, value) in pairs { + collect_expr_scope_access(key, access); + collect_expr_scope_access(value, access); + } + } + ExprKind::Match { + subject, + arms, + default, + } => { + collect_expr_scope_access(subject, access); + for (conditions, value) in arms { + for condition in conditions { + collect_expr_scope_access(condition, access); + } + collect_expr_scope_access(value, access); + } + if let Some(default) = default { + collect_expr_scope_access(default, access); + } + } + ExprKind::ArrayAccess { array, index } => { + collect_expr_scope_access(array, access); + collect_expr_scope_access(index, access); + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + collect_expr_scope_access(condition, access); + collect_expr_scope_access(then_expr, access); + collect_expr_scope_access(else_expr, access); + } + ExprKind::Closure { + params, + body, + captures, + capture_refs, + .. + } => { + for (_, _, default, _) in params { + if let Some(default) = default { + collect_expr_scope_access(default, access); + } + } + for capture in captures.iter().chain(capture_refs.iter()) { + access.read(capture); + } + collect_block_scope_access(body, access); + } + ExprKind::NamedArg { value, .. } => collect_expr_scope_access(value, access), + ExprKind::IncludeValue { path, .. } => collect_expr_scope_access(path, access), + ExprKind::NewDynamic { name_expr, args } => { + collect_expr_scope_access(name_expr, access); + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::NewDynamicObject { + class_name, args, .. + } => { + collect_expr_scope_access(class_name, access); + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => { + collect_expr_scope_access(object, access); + } + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(property, access); + } + ExprKind::NullsafeMethodCall { object, args, .. } + | ExprKind::MethodCall { object, args, .. } => { + collect_expr_scope_access(object, access); + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(method, access); + for arg in args { + collect_expr_scope_access(arg, access); + } + } + ExprKind::StaticPropertyAccess { .. } | ExprKind::This => {} + ExprKind::BufferNew { len, .. } => collect_expr_scope_access(len, access), + ExprKind::FirstClassCallable(target) => { + collect_callable_target_scope_access(target, access) + } + ExprKind::Yield { key, value } => { + if let Some(key) = key { + collect_expr_scope_access(key, access); + } + if let Some(value) = value { + collect_expr_scope_access(value, access); + } + } + } +} + +/// Records the variable effects of an assignment target expression. +fn collect_assignment_target_scope_access(expr: &Expr, access: &mut EvalScopeAccess) { + match &expr.kind { + ExprKind::Variable(name) => access.write(name), + ExprKind::ArrayAccess { array, index } => { + collect_expr_scope_access(array, access); + collect_expr_scope_access(index, access); + } + ExprKind::PropertyAccess { object, .. } + | ExprKind::NullsafePropertyAccess { object, .. } => { + collect_expr_scope_access(object, access); + } + ExprKind::DynamicPropertyAccess { object, property } + | ExprKind::NullsafeDynamicPropertyAccess { object, property } => { + collect_expr_scope_access(object, access); + collect_expr_scope_access(property, access); + } + _ => { + collect_expr_scope_access(expr, access); + access.unknown_write(); + } + } +} + +/// Adds variable reads from a first-class callable target. +fn collect_callable_target_scope_access( + target: &crate::parser::ast::CallableTarget, + access: &mut EvalScopeAccess, +) { + match target { + crate::parser::ast::CallableTarget::Function(_) => {} + crate::parser::ast::CallableTarget::StaticMethod { .. } => {} + crate::parser::ast::CallableTarget::Method { object, .. } => { + collect_expr_scope_access(object, access); + } + } +} + +/// Hashes a fragment with a stable FNV-1a variant for deterministic symbol names. +fn stable_fragment_hash(fragment: &str) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325u64; + for byte in fragment.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + hash +} + +/// Definite local facts tracked while classifying an eval fragment for EIR AOT. +#[derive(Clone, Default)] +struct EirLocalFacts { + assigned: BTreeSet, + int_locals: BTreeSet, + float_locals: BTreeSet, + array_locals: BTreeSet, +} + +impl EirLocalFacts { + /// Creates empty local facts for a fresh eval fragment block. + fn new() -> Self { + Self::default() + } + + /// Returns true when a variable is definitely assigned in this control-flow path. + fn is_assigned(&self, name: &str) -> bool { + self.assigned.contains(name) + } + + /// Returns true when a variable is definitely assigned as an integer value. + fn is_int_local(&self, name: &str) -> bool { + self.int_locals.contains(name) + } + + /// Returns true when a variable is definitely assigned as a floating value. + fn is_float_local(&self, name: &str) -> bool { + self.float_locals.contains(name) + } + + /// Returns true when a variable is definitely assigned from a static array literal. + fn is_array_local(&self, name: &str) -> bool { + self.array_locals.contains(name) + } + + /// Records an assignment and updates scalar/array local facts for that variable. + fn assign( + &mut self, + name: &str, + value: &Expr, + support: &S, + scope_reads: Option<&BTreeSet>, + ) where + S: EirStaticCallSupport, + { + self.assigned.insert(name.to_string()); + if expr_is_eir_int_value_safe(value, support, self, scope_reads) { + self.int_locals.insert(name.to_string()); + } else { + self.int_locals.remove(name); + } + if expr_is_eir_float_value_safe(value, support, self, scope_reads) { + self.float_locals.insert(name.to_string()); + } else { + self.float_locals.remove(name); + } + if expr_is_eir_static_array_literal_source_safe(value, support, self, scope_reads) { + self.array_locals.insert(name.to_string()); + } else { + self.array_locals.remove(name); + } + } + + /// Records that a variable is definitely assigned, but with no narrower local fact. + fn assign_unknown(&mut self, name: &str) { + self.assigned.insert(name.to_string()); + self.int_locals.remove(name); + self.array_locals.remove(name); + } +} + +/// Returns true when the fragment can be lowered as a no-scope EIR function today. +fn program_is_eir_function_safe(program: &[Stmt], support: &S) -> bool +where + S: EirStaticCallSupport, +{ + let mut facts = EirLocalFacts::new(); + block_is_eir_function_safe(program, support, &mut facts, None, 0).is_some() +} + +/// Returns true when a fragment can be lowered as an EIR function reading eval scope. +fn program_is_eir_scope_read_function_safe( + program: &[Stmt], + support: &S, + scope_reads: &BTreeSet, +) -> bool +where + S: EirStaticCallSupport, +{ + let mut facts = EirLocalFacts::new(); + block_is_eir_function_safe(program, support, &mut facts, Some(scope_reads), 0).is_some() +} + +/// Returns true when a writes-only fragment can be an EIR eval-scope function. +fn program_is_eir_scope_write_linear_function_safe( + program: &[Stmt], + support: &S, + scope_names: &BTreeSet, +) -> bool +where + S: EirStaticCallSupport, +{ + let mut facts = EirLocalFacts::new(); + let mut terminated = false; + for stmt in program { + if terminated { + return false; + } + let Some(done) = + stmt_is_eir_scope_write_linear_function_safe(stmt, support, scope_names, &mut facts) + else { + return false; + }; + terminated = done; + } + true +} + +/// Checks one statement for the linear writes-only EIR eval-scope subset. +fn stmt_is_eir_scope_write_linear_function_safe( + stmt: &Stmt, + support: &S, + scope_names: &BTreeSet, + facts: &mut EirLocalFacts, +) -> Option +where + S: EirStaticCallSupport, +{ + match &stmt.kind { + StmtKind::Synthetic(body) => { + let mut terminated = false; + for stmt in body { + if terminated { + return None; + } + terminated = stmt_is_eir_scope_write_linear_function_safe( + stmt, + support, + scope_names, + facts, + )?; + } + Some(terminated) + } + StmtKind::Assign { name, value } if scope_names.contains(name) => { + expr_is_eir_function_safe(value, support, facts, Some(scope_names)).then_some(())?; + facts.assign(name, value, support, Some(scope_names)); + Some(false) + } + StmtKind::Echo(expr) => { + expr_is_eir_function_safe(expr, support, facts, Some(scope_names)).then_some(false) + } + StmtKind::Return(Some(expr)) => { + expr_is_eir_function_safe(expr, support, facts, Some(scope_names)).then_some(true) + } + StmtKind::Return(None) => Some(true), + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::Print(inner) => { + expr_is_eir_function_safe(inner, support, facts, Some(scope_names)).then_some(false) + } + _ => { + expr_is_eir_function_safe(expr, support, facts, Some(scope_names)).then_some(false) + } + }, + _ => None, + } +} + +/// Checks a statement block for the no-scope EIR-function eval subset. +fn block_is_eir_function_safe( + body: &[Stmt], + support: &S, + facts: &mut EirLocalFacts, + scope_reads: Option<&BTreeSet>, + loop_depth: usize, +) -> Option +where + S: EirStaticCallSupport, +{ + let mut terminated = false; + for stmt in body { + if terminated { + return None; + } + let done = stmt_is_eir_function_safe(stmt, support, facts, scope_reads, loop_depth)?; + terminated = done; + } + Some(terminated) +} + +/// Checks one statement for the initial no-scope EIR-function eval subset. +fn stmt_is_eir_function_safe( + stmt: &Stmt, + support: &S, + facts: &mut EirLocalFacts, + scope_reads: Option<&BTreeSet>, + loop_depth: usize, +) -> Option +where + S: EirStaticCallSupport, +{ + match &stmt.kind { + StmtKind::Synthetic(body) => { + block_is_eir_function_safe(body, support, facts, scope_reads, loop_depth) + } + StmtKind::Echo(expr) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads).then_some(false) + } + StmtKind::Assign { name, value } + if scope_reads.is_some_and(|names| names.contains(name)) => + { + expr_is_eir_function_safe(value, support, facts, scope_reads).then_some(())?; + facts.assign(name, value, support, scope_reads); + Some(false) + } + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => { + expr_is_eir_function_safe(condition, support, facts, scope_reads).then_some(())?; + if_stmt_is_eir_function_safe( + then_body, + elseif_clauses, + else_body.as_deref(), + support, + facts, + scope_reads, + loop_depth, + ) + .then_some(false) + } + StmtKind::While { condition, body } => { + expr_is_eir_function_safe(condition, support, facts, scope_reads).then_some(())?; + let mut body_facts = facts.clone(); + block_is_eir_function_safe(body, support, &mut body_facts, scope_reads, loop_depth + 1) + .map(|_| false) + } + StmtKind::DoWhile { condition, body } => { + let mut body_facts = facts.clone(); + block_is_eir_function_safe( + body, + support, + &mut body_facts, + scope_reads, + loop_depth + 1, + )?; + expr_is_eir_function_safe(condition, support, &body_facts, scope_reads).then_some(false) + } + StmtKind::For { + init, + condition, + update, + body, + } => { + if let Some(init) = init { + if stmt_is_eir_function_safe(init, support, facts, scope_reads, loop_depth)? { + return None; + } + } + if let Some(condition) = condition { + expr_is_eir_function_safe(condition, support, facts, scope_reads).then_some(())?; + } + let mut body_facts = facts.clone(); + block_is_eir_function_safe( + body, + support, + &mut body_facts, + scope_reads, + loop_depth + 1, + )?; + if let Some(update) = update { + if stmt_is_eir_function_safe( + update, + support, + &mut body_facts, + scope_reads, + loop_depth + 1, + )? { + return None; + } + } + Some(false) + } + StmtKind::Foreach { + array, + key_var, + value_var, + value_by_ref, + body, + } => { + let static_empty = expr_is_static_empty_array_literal_source(array); + if (scope_reads.is_none() && !static_empty) + || *value_by_ref + || !expr_is_eir_foreach_source_safe(array, scope_reads) + { + return None; + } + expr_is_eir_foreach_source_lowerable(array, support, facts, scope_reads) + .then_some(())?; + let mut body_facts = facts.clone(); + body_facts.assign_unknown(value_var); + if let Some(key_var) = key_var { + body_facts.assign_unknown(key_var); + } + block_is_eir_function_safe( + body, + support, + &mut body_facts, + scope_reads, + loop_depth + 1, + )?; + if expr_is_non_empty_static_array_literal_source(array) { + facts.assign_unknown(value_var); + if let Some(key_var) = key_var { + facts.assign_unknown(key_var); + } + } + Some(false) + } + StmtKind::Switch { + subject, + cases, + default, + } => switch_stmt_is_eir_function_safe( + subject, + cases, + default.as_deref(), + support, + facts, + scope_reads, + loop_depth, + ) + .then_some(false), + StmtKind::Break(level) => (*level > 0 && *level <= loop_depth).then_some(true), + StmtKind::Continue(level) => (*level > 0 && *level <= loop_depth).then_some(true), + StmtKind::Return(Some(expr)) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads).then_some(true) + } + StmtKind::Return(None) => Some(true), + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::Print(inner) => { + expr_is_eir_function_safe(inner, support, facts, scope_reads).then_some(false) + } + _ => expr_is_eir_function_safe(expr, support, facts, scope_reads).then_some(false), + }, + _ => None, + } +} + +/// Returns true when a foreach source has EIR-safe eval AOT semantics. +fn expr_is_eir_foreach_source_safe(expr: &Expr, scope_reads: Option<&BTreeSet>) -> bool { + if expr_is_static_array_literal_source(expr) { + return true; + } + matches!( + &expr.kind, + ExprKind::Variable(name) if scope_reads.is_some_and(|reads| reads.contains(name)) + ) +} + +/// Returns true when a foreach source can be lowered by the EIR backend. +fn expr_is_eir_foreach_source_lowerable( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::Variable(name) if scope_reads.is_some_and(|reads| reads.contains(name)) => true, + _ => expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads), + } +} + +/// Returns true when a static array source is a literal expression. +fn expr_is_static_array_literal_source(expr: &Expr) -> bool { + matches!( + &expr.kind, + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) + ) +} + +/// Returns true when a static array source is a literal known to skip its body. +fn expr_is_static_empty_array_literal_source(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::ArrayLiteral(items) => items.is_empty(), + ExprKind::ArrayLiteralAssoc(pairs) => pairs.is_empty(), + _ => false, + } +} + +/// Returns true when a static array source is a literal known to iterate at least once. +fn expr_is_non_empty_static_array_literal_source(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::ArrayLiteral(items) => !items.is_empty(), + ExprKind::ArrayLiteralAssoc(pairs) => !pairs.is_empty(), + _ => false, + } +} + +/// Checks a switch statement while preserving conservative assignment facts. +fn switch_stmt_is_eir_function_safe( + subject: &Expr, + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, + loop_depth: usize, +) -> bool +where + S: EirStaticCallSupport, +{ + if !expr_is_eir_function_safe(subject, support, facts, scope_reads) { + return false; + } + if !switch_default_position_is_eir_safe(cases, default) { + return false; + } + for (conditions, body) in cases { + for condition in conditions { + if !expr_is_eir_function_safe(condition, support, facts, scope_reads) { + return false; + } + } + let mut case_facts = facts.clone(); + if block_is_eir_function_safe(body, support, &mut case_facts, scope_reads, loop_depth + 1) + .is_none() + { + return false; + } + } + if let Some(default) = default { + let mut default_facts = facts.clone(); + if block_is_eir_function_safe( + default, + support, + &mut default_facts, + scope_reads, + loop_depth + 1, + ) + .is_none() + { + return false; + } + } + true +} + +/// Returns true when EIR switch lowering can reconstruct the default source position. +fn switch_default_position_is_eir_safe( + cases: &[(Vec, Vec)], + default: Option<&[Stmt]>, +) -> bool { + let Some(default) = default else { + return true; + }; + if cases.is_empty() { + return true; + } + let Some(default_start) = default.first().map(|stmt| stmt.span) else { + return false; + }; + if default_start == crate::span::Span::dummy() { + return false; + } + cases.iter().all(|(conditions, _)| { + conditions + .first() + .is_some_and(|condition| condition.span != crate::span::Span::dummy()) + }) +} + +/// Checks an if/elseif/else chain and propagates only definitely assigned locals. +fn if_stmt_is_eir_function_safe( + then_body: &[Stmt], + elseif_clauses: &[(Expr, Vec)], + else_body: Option<&[Stmt]>, + support: &S, + facts: &mut EirLocalFacts, + scope_reads: Option<&BTreeSet>, + loop_depth: usize, +) -> bool +where + S: EirStaticCallSupport, +{ + let before = facts.clone(); + let mut branch_outputs = Vec::new(); + let mut then_facts = before.clone(); + if block_is_eir_function_safe(then_body, support, &mut then_facts, scope_reads, loop_depth) + .is_none() + { + return false; + } + branch_outputs.push(then_facts); + + for (condition, body) in elseif_clauses { + if !expr_is_eir_function_safe(condition, support, &before, scope_reads) { + return false; + } + let mut branch_facts = before.clone(); + if block_is_eir_function_safe(body, support, &mut branch_facts, scope_reads, loop_depth) + .is_none() + { + return false; + } + branch_outputs.push(branch_facts); + } + + let Some(else_body) = else_body else { + return true; + }; + let mut else_facts = before.clone(); + if block_is_eir_function_safe(else_body, support, &mut else_facts, scope_reads, loop_depth) + .is_none() + { + return false; + } + branch_outputs.push(else_facts); + + *facts = definitely_assigned_after_eir_branches(before, &branch_outputs); + true +} + +/// Keeps only local facts that are true after every branch in an if/elseif/else chain. +fn definitely_assigned_after_eir_branches( + before: EirLocalFacts, + branch_outputs: &[EirLocalFacts], +) -> EirLocalFacts { + let mut definitely = before; + for name in branch_outputs + .first() + .into_iter() + .flat_map(|branch| branch.assigned.iter()) + { + if branch_outputs + .iter() + .all(|branch| branch.assigned.contains(name)) + { + definitely.assigned.insert(name.clone()); + } + } + for name in branch_outputs + .first() + .into_iter() + .flat_map(|branch| branch.int_locals.iter()) + { + if branch_outputs + .iter() + .all(|branch| branch.int_locals.contains(name)) + { + definitely.int_locals.insert(name.clone()); + } + } + for name in branch_outputs + .first() + .into_iter() + .flat_map(|branch| branch.array_locals.iter()) + { + if branch_outputs + .iter() + .all(|branch| branch.array_locals.contains(name)) + { + definitely.array_locals.insert(name.clone()); + } + } + definitely +} + +/// Checks one expression for the initial no-scope EIR-function eval subset. +fn expr_is_eir_function_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null => true, + ExprKind::Variable(name) => { + facts.is_assigned(name) || scope_reads.is_some_and(|reads| reads.contains(name)) + } + ExprKind::Negate(inner) + | ExprKind::Not(inner) + | ExprKind::BitNot(inner) + | ExprKind::ErrorSuppress(inner) + | ExprKind::Print(inner) => expr_is_eir_function_safe(inner, support, facts, scope_reads), + ExprKind::PreIncrement(name) + | ExprKind::PostIncrement(name) + | ExprKind::PreDecrement(name) + | ExprKind::PostDecrement(name) => facts.is_int_local(name), + ExprKind::BinaryOp { left, op, right } => { + matches!( + op, + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::Div + | BinOp::Mod + | BinOp::Pow + | BinOp::Lt + | BinOp::Gt + | BinOp::LtEq + | BinOp::GtEq + | BinOp::Eq + | BinOp::NotEq + | BinOp::StrictEq + | BinOp::StrictNotEq + | BinOp::And + | BinOp::Or + | BinOp::Xor + | BinOp::BitAnd + | BinOp::BitOr + | BinOp::BitXor + | BinOp::ShiftLeft + | BinOp::ShiftRight + | BinOp::Spaceship + | BinOp::Concat + ) && expr_is_eir_function_safe(left, support, facts, scope_reads) + && expr_is_eir_function_safe(right, support, facts, scope_reads) + } + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + expr_is_eir_function_safe(condition, support, facts, scope_reads) + && expr_is_eir_function_safe(then_expr, support, facts, scope_reads) + && expr_is_eir_function_safe(else_expr, support, facts, scope_reads) + } + ExprKind::ShortTernary { value, default } => { + expr_is_eir_function_safe(value, support, facts, scope_reads) + && expr_is_eir_function_safe(default, support, facts, scope_reads) + } + ExprKind::NullCoalesce { value, default } => { + expr_is_eir_function_safe(value, support, facts, scope_reads) + && expr_is_eir_function_safe(default, support, facts, scope_reads) + } + ExprKind::Cast { target, expr } => { + matches!( + target, + CastType::Int | CastType::Float | CastType::String | CastType::Bool + ) && expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::Match { + subject, + arms, + default, + } => { + expr_is_eir_function_safe(subject, support, facts, scope_reads) + && default.as_ref().is_some_and(|default| { + expr_is_eir_function_safe(default, support, facts, scope_reads) + }) + && arms.iter().all(|(conditions, result)| { + conditions.iter().all(|condition| { + expr_is_eir_function_safe(condition, support, facts, scope_reads) + }) && expr_is_eir_function_safe(result, support, facts, scope_reads) + }) + } + ExprKind::ArrayAccess { array, index } => { + expr_is_eir_static_array_source_safe(array, support, facts, scope_reads) + && expr_is_eir_function_safe(index, support, facts, scope_reads) + } + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) => { + expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) + } + ExprKind::FunctionCall { name, args } => { + eir_call_user_func_call_is_safe(name.as_str(), args, support, facts, scope_reads) + || eir_construct_call_is_safe(name.as_str(), args, support, facts, scope_reads) + || eir_runtime_builtin_call_is_safe( + name.as_str(), + args, + support, + facts, + scope_reads, + ) + || fold_static_builtin_int_call(name.as_str().trim_start_matches('\\'), args) + .is_some() + || support.function_supported(name.as_str(), args) + } + ExprKind::StaticMethodCall { + receiver, + method, + args, + } => support.static_method_supported(receiver, method, args), + _ => false, + } +} + +/// Returns true when a static `call_user_func*()` callback maps to an AOT-safe call. +fn eir_call_user_func_call_is_safe( + name: &str, + args: &[Expr], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "call_user_func" => { + let Some((callback, callback_args)) = args.split_first() else { + return false; + }; + static_callback_call_is_eir_safe(callback, callback_args, support, facts, scope_reads) + } + "call_user_func_array" => { + let [callback, arg_array] = args else { + return false; + }; + let Some(callback_args) = static_call_user_func_array_args(arg_array) else { + return false; + }; + static_callback_call_is_eir_safe(callback, &callback_args, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when a compile-time string callback names a safe builtin or user function. +fn static_callback_call_is_eir_safe( + callback: &Expr, + callback_args: &[Expr], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + let ExprKind::StringLiteral(callback_name) = &callback.kind else { + return false; + }; + if callback_name.contains("::") { + return false; + } + let short_callback = callback_name.trim_start_matches('\\'); + eir_runtime_builtin_call_is_safe(short_callback, callback_args, support, facts, scope_reads) + || fold_static_builtin_call(short_callback, callback_args).is_some() + || support.function_supported(short_callback, callback_args) +} + +/// Converts a static `call_user_func_array()` argument array into callback args. +fn static_call_user_func_array_args(arg_array: &Expr) -> Option> { + match &arg_array.kind { + ExprKind::ArrayLiteral(items) => Some(items.clone()), + ExprKind::ArrayLiteralAssoc(pairs) => { + static_call_user_func_array_assoc_args(pairs.as_slice()) + } + _ => None, + } +} + +/// Converts literal associative callback arrays into positional or named callback args. +fn static_call_user_func_array_assoc_args(pairs: &[(Expr, Expr)]) -> Option> { + let mut args = Vec::with_capacity(pairs.len()); + for (key, value) in pairs { + match &key.kind { + ExprKind::StringLiteral(name) => { + args.push(Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(value.clone()), + }, + value.span, + )); + } + ExprKind::IntLiteral(_) => args.push(value.clone()), + _ => return None, + } + } + Some(args) +} + +/// Returns true when an array source can be materialized inside eval EIR AOT. +fn expr_is_eir_static_array_source_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) => { + expr_is_eir_static_array_literal_source_safe(expr, support, facts, scope_reads) + } + ExprKind::Variable(name) => facts.is_array_local(name), + _ => false, + } +} + +/// Returns true when a literal array expression can be materialized inside eval EIR AOT. +fn expr_is_eir_static_array_literal_source_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::ArrayLiteral(items) => items + .iter() + .all(|item| expr_is_eir_static_array_value_safe(item, support, facts, scope_reads)), + ExprKind::ArrayLiteralAssoc(pairs) => { + expr_is_eir_static_assoc_array_source_safe(pairs, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when an associative array source has statically reconstructable key semantics. +fn expr_is_eir_static_assoc_array_source_safe( + pairs: &[(Expr, Expr)], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + let mut next_auto_key = 0i64; + let mut auto_key_initialized = false; + for (key, value) in pairs { + if static_assoc_key_is_parser_generated(key, value) { + let ExprKind::IntLiteral(generated) = &key.kind else { + return false; + }; + if *generated != next_auto_key { + return false; + } + if !expr_is_eir_static_array_value_safe(value, support, facts, scope_reads) { + return false; + } + advance_static_array_auto_key(&mut next_auto_key, &mut auto_key_initialized); + continue; + } + if !expr_is_eir_static_array_key_safe(key, support, facts, scope_reads) + || !expr_is_eir_static_array_value_safe(value, support, facts, scope_reads) + { + return false; + } + update_static_array_auto_key_from_explicit_key( + key, + &mut next_auto_key, + &mut auto_key_initialized, + ); + } + true +} + +/// Returns true when a static array key can be lowered without eval bridge state. +fn expr_is_eir_static_array_key_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::IntLiteral(_) | ExprKind::BoolLiteral(_) | ExprKind::Null => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::FloatLiteral(_) if static_integral_float_array_key_value(expr).is_some() => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::Negate(inner) + if matches!(inner.kind, ExprKind::IntLiteral(_)) + || static_integral_float_array_key_value(expr).is_some() => + { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::StringLiteral(_) => expr_is_eir_function_safe(expr, support, facts, scope_reads), + _ => false, + } +} + +/// Returns true for parser-synthesized integer keys from unkeyed assoc entries. +fn static_assoc_key_is_parser_generated(key: &Expr, value: &Expr) -> bool { + matches!(key.kind, ExprKind::IntLiteral(_)) && key.span == value.span +} + +/// Advances the static array auto-key cursor after an implicit generated key. +fn advance_static_array_auto_key(next_auto_key: &mut i64, auto_key_initialized: &mut bool) { + *next_auto_key = next_auto_key.saturating_add(1); + *auto_key_initialized = true; +} + +/// Updates the static array auto-key cursor from an explicit integer-like key. +fn update_static_array_auto_key_from_explicit_key( + key: &Expr, + next_auto_key: &mut i64, + auto_key_initialized: &mut bool, +) { + if let Some(value) = static_integer_array_key_value(key) { + let candidate = value.saturating_add(1); + if !*auto_key_initialized || candidate > *next_auto_key { + *next_auto_key = candidate; + } + *auto_key_initialized = true; + } +} + +/// Returns the integer value for static keys that affect PHP's next auto key. +fn static_integer_array_key_value(key: &Expr) -> Option { + match &key.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::BoolLiteral(value) => Some(i64::from(*value)), + ExprKind::FloatLiteral(_) => static_integral_float_array_key_value(key), + ExprKind::StringLiteral(value) if is_php_integer_array_key(value) => value.parse().ok(), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value.checked_neg(), + ExprKind::FloatLiteral(_) => static_integral_float_array_key_value(key), + _ => None, + }, + _ => None, + } +} + +/// Returns the integer key for a float literal that PHP casts without a precision warning. +fn static_integral_float_array_key_value(key: &Expr) -> Option { + let value = match &key.kind { + ExprKind::FloatLiteral(value) => *value, + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::FloatLiteral(value) => -*value, + _ => return None, + }, + _ => return None, + }; + if !value.is_finite() || value.fract() != 0.0 { + return None; + } + if value < i64::MIN as f64 || value >= i64::MAX as f64 { + return None; + } + Some(value as i64) +} + +/// Returns true when a static array value can be lowered without eval bridge state. +fn expr_is_eir_static_array_value_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::Spread(_) => false, + ExprKind::ArrayLiteral(_) | ExprKind::ArrayLiteralAssoc(_) => { + expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) + } + _ => expr_is_eir_function_safe(expr, support, facts, scope_reads), + } +} + +/// Returns true for language-construct calls that can safely lower through EIR AOT. +fn eir_construct_call_is_safe( + name: &str, + args: &[Expr], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + if has_named_args(args) + || args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_))) + { + return false; + } + match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "isset" => { + !args.is_empty() + && args + .iter() + .all(|arg| eir_isset_probe_is_safe(arg, support, facts, scope_reads)) + } + "empty" if args.len() == 1 => match &args[0].kind { + ExprKind::Variable(name) => eir_variable_probe_is_safe(name, facts, scope_reads), + _ => expr_is_eir_function_safe(&args[0], support, facts, scope_reads), + }, + _ => false, + } +} + +/// Returns true when an `isset()` operand can lower without evaluating dynamic scope state. +fn eir_isset_probe_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::Variable(name) => eir_variable_probe_is_safe(name, facts, scope_reads), + ExprKind::ArrayAccess { .. } => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when a variable probe can use local facts or direct eval read params. +fn eir_variable_probe_is_safe( + name: &str, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool { + facts.is_assigned(name) || scope_reads.is_some_and(|reads| reads.contains(name)) +} + +/// Returns true for builtin calls that the normal EIR backend can lower at runtime. +fn eir_runtime_builtin_call_is_safe( + name: &str, + args: &[Expr], + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + let short_name = php_symbol_key(name.trim_start_matches('\\')); + let Some(args) = normalize_eir_runtime_builtin_args(&short_name, args) else { + return false; + }; + match short_name.as_str() { + "boolval" if args.len() == 1 => { + eir_boolval_arg_is_safe(&args[0], support, facts, scope_reads) + } + "array_key_exists" if args.len() == 2 => { + eir_array_key_exists_args_are_safe(&args[0], &args[1], support, facts, scope_reads) + } + "count" if (1..=2).contains(&args.len()) => { + eir_count_mode_is_default_zero(args.get(1)) + && eir_count_arg_is_safe(&args[0], support, facts, scope_reads) + } + "floatval" if args.len() == 1 => { + eir_floatval_arg_is_safe(&args[0], support, facts, scope_reads) + } + "gettype" if args.len() == 1 => { + eir_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "intval" if args.len() == 1 => { + eir_intval_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_array" if args.len() == 1 => { + eir_array_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_iterable" if args.len() == 1 => { + eir_array_like_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_object" if args.len() == 1 => { + eir_object_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_numeric" | "is_resource" if args.len() == 1 => { + eir_scalar_cast_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_finite" | "is_infinite" | "is_nan" if args.len() == 1 => { + eir_float_predicate_arg_is_safe(&args[0], support, facts, scope_reads) + } + "is_bool" | "is_double" | "is_float" | "is_int" | "is_integer" | "is_long" | "is_null" + | "is_real" | "is_scalar" | "is_string" + if args.len() == 1 => + { + eir_type_probe_arg_is_safe(&args[0], support, facts, scope_reads) + } + "strval" if args.len() == 1 => { + eir_strval_arg_is_safe(&args[0], support, facts, scope_reads) + } + "strlen" if args.len() == 1 => { + eir_strlen_arg_is_safe(&args[0], support, facts, scope_reads) + } + _ => false, + } +} + +/// Normalizes EIR-safe builtin call arguments for eval AOT gating. +/// +/// Static spread arrays are expanded through the shared call planner; dynamic +/// spreads that remain after planning stay on the eval bridge fallback. +fn normalize_eir_runtime_builtin_args(short_name: &str, args: &[Expr]) -> Option> { + let has_spread = args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_))); + if !has_named_args(args) && !has_spread { + return Some(args.to_vec()); + } + let sig = builtin_call_sig(short_name)?; + let call_span = args.first().map(|arg| arg.span).unwrap_or_else(Span::dummy); + let plan = plan_call_args(&sig, args, call_span, false, false).ok()?; + if plan.has_spread_args() { + return None; + } + Some(plan.normalized_args()) +} + +/// Returns true when `array_key_exists()` can lower through EIR without eval bridge state. +fn eir_array_key_exists_args_are_safe( + key: &Expr, + array: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + if !eir_array_key_exists_static_key_is_safe(key) { + return false; + } + match &array.kind { + ExprKind::Variable(name) if scope_reads.is_some_and(|reads| reads.contains(name)) => true, + ExprKind::ArrayLiteralAssoc(_) => { + expr_is_eir_static_array_source_safe(array, support, facts, scope_reads) + } + ExprKind::ArrayLiteral(_) => { + !eir_array_key_exists_static_key_needs_assoc_array(key) + && expr_is_eir_static_array_source_safe(array, support, facts, scope_reads) + } + ExprKind::Variable(name) => { + !eir_array_key_exists_static_key_needs_assoc_array(key) && facts.is_array_local(name) + } + _ => false, + } +} + +/// Returns true when the key type has target-aware lowering for mixed array probes. +fn eir_array_key_exists_static_key_is_safe(key: &Expr) -> bool { + match &key.kind { + ExprKind::IntLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::StringLiteral(_) + | ExprKind::Null => true, + ExprKind::FloatLiteral(_) => static_integral_float_array_key_value(key).is_some(), + ExprKind::Negate(inner) => { + matches!(inner.kind, ExprKind::IntLiteral(_)) + || static_integral_float_array_key_value(key).is_some() + } + _ => false, + } +} + +/// Returns true when the static key only has safe mixed-array semantics for hashes. +/// +/// String keys can now probe indexed arrays too: numeric strings normalize to an +/// integer bounds check and non-integer strings return false on indexed arrays. +fn eir_array_key_exists_static_key_needs_assoc_array(key: &Expr) -> bool { + matches!(key.kind, ExprKind::Null) +} + +/// Returns true when `count()` uses PHP's default non-recursive mode. +fn eir_count_mode_is_default_zero(mode: Option<&Expr>) -> bool { + match mode { + None => true, + Some(expr) => matches!(expr.kind, ExprKind::IntLiteral(0)), + } +} + +/// Returns true when a value can reach `count()` as a concrete EIR array. +fn eir_count_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::Variable(name) if scope_reads.is_some_and(|reads| reads.contains(name)) => true, + _ => expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads), + } +} + +/// Returns true when a value can reach `boolval()` through an EIR-supported scalar path. +fn eir_boolval_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `floatval()` through an EIR-supported scalar path. +fn eir_floatval_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `intval()` through an EIR-supported scalar path. +fn eir_intval_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `gettype()`/`is_*()` through EIR-safe probes. +fn eir_type_probe_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach array-like type probes through safe EIR paths. +fn eir_array_type_probe_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) + || eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `is_iterable()` through currently safe EIR paths. +fn eir_array_like_type_probe_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) + || eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach `is_object()` through currently safe EIR paths. +fn eir_object_type_probe_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) + || expr_is_eir_static_array_source_safe(expr, support, facts, scope_reads) +} + +/// Returns true when a value can reach IEEE float predicates without PHP coercion surprises. +fn eir_float_predicate_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::IntLiteral(_) | ExprKind::FloatLiteral(_) | ExprKind::BoolLiteral(_) => true, + ExprKind::Variable(name) => { + scope_reads.is_some_and(|reads| reads.contains(name)) + || facts.is_int_local(name) + || facts.is_float_local(name) + } + ExprKind::Negate(inner) | ExprKind::ErrorSuppress(inner) => { + eir_float_predicate_arg_is_safe(inner, support, facts, scope_reads) + } + ExprKind::Cast { target, expr } + if matches!(target, CastType::Int | CastType::Float | CastType::Bool) => + { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when a value can reach `strval()` through an EIR-supported scalar path. +fn eir_strval_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + eir_scalar_cast_arg_is_safe(expr, support, facts, scope_reads) +} + +/// Returns true when an expression is scalar-like enough for EIR cast builtins. +fn eir_scalar_cast_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::StringLiteral(_) + | ExprKind::Null => true, + ExprKind::Variable(name) => { + scope_reads.is_some_and(|reads| reads.contains(name)) + || (facts.is_assigned(name) && !facts.is_array_local(name)) + } + ExprKind::Negate(inner) | ExprKind::ErrorSuppress(inner) => { + eir_scalar_cast_arg_is_safe(inner, support, facts, scope_reads) + } + ExprKind::Cast { target, expr } + if matches!( + target, + CastType::Int | CastType::Float | CastType::String | CastType::Bool + ) => + { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::ArrayAccess { .. } => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::FunctionCall { name, args } => { + eir_call_user_func_call_is_safe(name.as_str(), args, support, facts, scope_reads) + || eir_runtime_builtin_call_is_safe( + name.as_str(), + args, + support, + facts, + scope_reads, + ) + || fold_static_builtin_int_call(name.as_str().trim_start_matches('\\'), args) + .is_some() + || support.function_supported(name.as_str(), args) + } + ExprKind::StaticMethodCall { + receiver, + method, + args, + } => support.static_method_supported(receiver, method, args), + _ => false, + } +} + +/// Returns true when a value can reach `strlen()` as `Str` or boxed `Mixed`. +fn eir_strlen_arg_is_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::StringLiteral(_) => true, + ExprKind::Variable(name) => { + facts.is_assigned(name) || scope_reads.is_some_and(|reads| reads.contains(name)) + } + ExprKind::Cast { target, expr } if matches!(target, CastType::String) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + _ => false, + } +} + +/// Returns true when an expression is known to produce an integer in the EIR AOT subset. +fn expr_is_eir_int_value_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::IntLiteral(_) => true, + ExprKind::Variable(name) => facts.is_int_local(name), + ExprKind::Negate(inner) | ExprKind::BitNot(inner) | ExprKind::ErrorSuppress(inner) => { + expr_is_eir_int_value_safe(inner, support, facts, scope_reads) + } + ExprKind::Print(inner) => expr_is_eir_function_safe(inner, support, facts, scope_reads), + ExprKind::PreIncrement(name) + | ExprKind::PostIncrement(name) + | ExprKind::PreDecrement(name) + | ExprKind::PostDecrement(name) => facts.is_int_local(name), + ExprKind::Cast { target, expr } if matches!(target, CastType::Int) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + ExprKind::BinaryOp { left, op, right } => { + let int_operands = expr_is_eir_int_value_safe(left, support, facts, scope_reads) + && expr_is_eir_int_value_safe(right, support, facts, scope_reads); + match op { + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::Mod + | BinOp::BitAnd + | BinOp::BitOr + | BinOp::BitXor + | BinOp::ShiftLeft + | BinOp::ShiftRight => int_operands, + BinOp::Spaceship => { + expr_is_eir_function_safe(left, support, facts, scope_reads) + && expr_is_eir_function_safe(right, support, facts, scope_reads) + } + _ => false, + } + } + ExprKind::FunctionCall { name, args } => { + fold_static_builtin_int_call(name.as_str().trim_start_matches('\\'), args).is_some() + } + _ => false, + } +} + +/// Returns true when an expression is known to produce a float in the EIR AOT subset. +fn expr_is_eir_float_value_safe( + expr: &Expr, + support: &S, + facts: &EirLocalFacts, + scope_reads: Option<&BTreeSet>, +) -> bool +where + S: EirStaticCallSupport, +{ + match &expr.kind { + ExprKind::FloatLiteral(_) => true, + ExprKind::Variable(name) => facts.is_float_local(name), + ExprKind::Negate(inner) | ExprKind::ErrorSuppress(inner) => { + expr_is_eir_float_value_safe(inner, support, facts, scope_reads) + } + ExprKind::Cast { target, expr } if matches!(target, CastType::Float) => { + expr_is_eir_function_safe(expr, support, facts, scope_reads) + } + _ => false, + } +} + +/// Rewrites foldable static builtin calls in a program to integer literals. +fn fold_static_builtin_calls_in_program(program: Program) -> Program { + program + .into_iter() + .map(fold_static_builtin_calls_in_stmt) + .collect() +} + +/// Rewrites foldable static builtin calls inside one statement. +fn fold_static_builtin_calls_in_stmt(stmt: Stmt) -> Stmt { + let kind = match stmt.kind { + StmtKind::Echo(expr) => StmtKind::Echo(fold_static_builtin_calls_in_expr(expr)), + StmtKind::Assign { name, value } => StmtKind::Assign { + name, + value: fold_static_builtin_calls_in_expr(value), + }, + StmtKind::If { + condition, + then_body, + elseif_clauses, + else_body, + } => StmtKind::If { + condition: fold_static_builtin_calls_in_expr(condition), + then_body: fold_static_builtin_calls_in_program(then_body), + elseif_clauses: elseif_clauses + .into_iter() + .map(|(condition, body)| { + ( + fold_static_builtin_calls_in_expr(condition), + fold_static_builtin_calls_in_program(body), + ) + }) + .collect(), + else_body: else_body.map(fold_static_builtin_calls_in_program), + }, + StmtKind::While { condition, body } => StmtKind::While { + condition: fold_static_builtin_calls_in_expr(condition), + body: fold_static_builtin_calls_in_program(body), + }, + StmtKind::DoWhile { condition, body } => StmtKind::DoWhile { + condition: fold_static_builtin_calls_in_expr(condition), + body: fold_static_builtin_calls_in_program(body), + }, + StmtKind::For { + init, + condition, + update, + body, + } => StmtKind::For { + init: init.map(|stmt| Box::new(fold_static_builtin_calls_in_stmt(*stmt))), + condition: condition.map(fold_static_builtin_calls_in_expr), + update: update.map(|stmt| Box::new(fold_static_builtin_calls_in_stmt(*stmt))), + body: fold_static_builtin_calls_in_program(body), + }, + StmtKind::Switch { + subject, + cases, + default, + } => StmtKind::Switch { + subject: fold_static_builtin_calls_in_expr(subject), + cases: cases + .into_iter() + .map(|(conditions, body)| { + ( + conditions + .into_iter() + .map(fold_static_builtin_calls_in_expr) + .collect(), + fold_static_builtin_calls_in_program(body), + ) + }) + .collect(), + default: default.map(fold_static_builtin_calls_in_program), + }, + StmtKind::Return(Some(expr)) => { + StmtKind::Return(Some(fold_static_builtin_calls_in_expr(expr))) + } + StmtKind::ExprStmt(expr) => StmtKind::ExprStmt(fold_static_builtin_calls_in_expr(expr)), + other => other, + }; + Stmt { + kind, + span: stmt.span, + attributes: stmt.attributes, + } +} + +/// Rewrites foldable static builtin calls inside one expression. +fn fold_static_builtin_calls_in_expr(expr: Expr) -> Expr { + let span = expr.span; + let kind = match expr.kind { + ExprKind::Negate(inner) => { + ExprKind::Negate(Box::new(fold_static_builtin_calls_in_expr(*inner))) + } + ExprKind::Not(inner) => ExprKind::Not(Box::new(fold_static_builtin_calls_in_expr(*inner))), + ExprKind::BitNot(inner) => { + ExprKind::BitNot(Box::new(fold_static_builtin_calls_in_expr(*inner))) + } + ExprKind::Print(inner) => { + ExprKind::Print(Box::new(fold_static_builtin_calls_in_expr(*inner))) + } + ExprKind::BinaryOp { left, op, right } => ExprKind::BinaryOp { + left: Box::new(fold_static_builtin_calls_in_expr(*left)), + op, + right: Box::new(fold_static_builtin_calls_in_expr(*right)), + }, + ExprKind::Ternary { + condition, + then_expr, + else_expr, + } => ExprKind::Ternary { + condition: Box::new(fold_static_builtin_calls_in_expr(*condition)), + then_expr: Box::new(fold_static_builtin_calls_in_expr(*then_expr)), + else_expr: Box::new(fold_static_builtin_calls_in_expr(*else_expr)), + }, + ExprKind::ShortTernary { value, default } => ExprKind::ShortTernary { + value: Box::new(fold_static_builtin_calls_in_expr(*value)), + default: Box::new(fold_static_builtin_calls_in_expr(*default)), + }, + ExprKind::NullCoalesce { value, default } => ExprKind::NullCoalesce { + value: Box::new(fold_static_builtin_calls_in_expr(*value)), + default: Box::new(fold_static_builtin_calls_in_expr(*default)), + }, + ExprKind::Cast { target, expr } => ExprKind::Cast { + target, + expr: Box::new(fold_static_builtin_calls_in_expr(*expr)), + }, + ExprKind::Match { + subject, + arms, + default, + } => ExprKind::Match { + subject: Box::new(fold_static_builtin_calls_in_expr(*subject)), + arms: arms + .into_iter() + .map(|(conditions, result)| { + ( + conditions + .into_iter() + .map(fold_static_builtin_calls_in_expr) + .collect(), + fold_static_builtin_calls_in_expr(result), + ) + }) + .collect(), + default: default.map(|expr| Box::new(fold_static_builtin_calls_in_expr(*expr))), + }, + ExprKind::ArrayLiteral(items) => ExprKind::ArrayLiteral( + items + .into_iter() + .map(fold_static_builtin_calls_in_expr) + .collect(), + ), + ExprKind::ArrayLiteralAssoc(pairs) => ExprKind::ArrayLiteralAssoc( + pairs + .into_iter() + .map(|(key, value)| { + ( + fold_static_builtin_calls_in_expr(key), + fold_static_builtin_calls_in_expr(value), + ) + }) + .collect(), + ), + ExprKind::FunctionCall { name, args } => { + let folded_args = args + .into_iter() + .map(fold_static_builtin_calls_in_expr) + .collect::>(); + if let Some(kind) = fold_static_call_user_func_call( + name.as_str().trim_start_matches('\\'), + &folded_args, + ) { + kind + } else if let Some(kind) = + fold_static_builtin_call(name.as_str().trim_start_matches('\\'), &folded_args) + { + kind + } else { + ExprKind::FunctionCall { + name, + args: folded_args, + } + } + } + other => other, + }; + Expr { kind, span } +} + +/// Folds `call_user_func*()` when the callback is a pure foldable builtin. +fn fold_static_call_user_func_call(short_name: &str, args: &[Expr]) -> Option { + match php_symbol_key(short_name).as_str() { + "call_user_func" => { + let (callback, callback_args) = args.split_first()?; + fold_static_callback_call(callback, callback_args) + } + "call_user_func_array" => { + let [callback, arg_array] = args else { + return None; + }; + let callback_args = static_call_user_func_array_args(arg_array)?; + fold_static_callback_call(callback, &callback_args) + } + _ => None, + } +} + +/// Folds one static string callback when it names a pure foldable builtin. +fn fold_static_callback_call(callback: &Expr, callback_args: &[Expr]) -> Option { + let ExprKind::StringLiteral(callback_name) = &callback.kind else { + return None; + }; + if callback_name.contains("::") { + return None; + } + fold_static_builtin_call(callback_name.trim_start_matches('\\'), callback_args) +} + +/// Checks one statement for the native-only literal eval runtime-feature subset. +fn stmt_is_native_only_scalar(stmt: &Stmt, static_call_supported: &F) -> Option +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &stmt.kind { + StmtKind::Echo(expr) => { + echo_expr_is_native_only_scalar(expr, static_call_supported).then_some(false) + } + StmtKind::Return(Some(expr)) => { + value_expr_is_native_only_scalar(expr, static_call_supported).then_some(true) + } + StmtKind::Return(None) => Some(true), + StmtKind::ExprStmt(expr) => match &expr.kind { + ExprKind::Print(inner) => { + echo_expr_is_native_only_scalar(inner, static_call_supported).then_some(false) + } + _ => value_expr_is_native_only_scalar(expr, static_call_supported).then_some(false), + }, + _ => None, + } +} + +/// Checks an echo expression for the native-only literal eval runtime-feature subset. +fn echo_expr_is_native_only_scalar(expr: &Expr, static_call_supported: &F) -> bool +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::StringLiteral(_) => true, + ExprKind::BinaryOp { left, op, right } if *op == BinOp::Concat => { + echo_expr_is_native_only_scalar(left, static_call_supported) + && echo_expr_is_native_only_scalar(right, static_call_supported) + } + _ => value_expr_is_native_only_scalar(expr, static_call_supported), + } +} + +/// Checks a value expression for the native-only literal eval runtime-feature subset. +fn value_expr_is_native_only_scalar(expr: &Expr, static_call_supported: &F) -> bool +where + F: Fn(&str, &[Expr]) -> bool, +{ + match &expr.kind { + ExprKind::IntLiteral(_) | ExprKind::BoolLiteral(_) => true, + ExprKind::Negate(inner) => value_expr_is_native_only_scalar(inner, static_call_supported), + ExprKind::Not(inner) => value_expr_is_native_only_scalar(inner, static_call_supported), + ExprKind::BinaryOp { left, op, right } => { + matches!( + op, + BinOp::Add + | BinOp::Sub + | BinOp::Mul + | BinOp::Mod + | BinOp::Lt + | BinOp::Gt + | BinOp::LtEq + | BinOp::GtEq + | BinOp::Eq + | BinOp::NotEq + | BinOp::And + | BinOp::Or + ) && value_expr_is_native_only_scalar(left, static_call_supported) + && value_expr_is_native_only_scalar(right, static_call_supported) + } + ExprKind::FunctionCall { name, args } => { + let short_name = name.as_str().trim_start_matches('\\'); + fold_static_builtin_int_call(short_name, args).is_some() + || static_call_supported(name.as_str(), args) + } + _ => false, + } +} + +/// Folds pure static builtin calls whose integer result is fully known at compile time. +pub(crate) fn fold_static_builtin_int_call(short_name: &str, args: &[Expr]) -> Option { + let ExprKind::IntLiteral(value) = fold_static_builtin_call(short_name, args)? else { + return None; + }; + Some(value) +} + +/// Folds pure static builtin calls whose scalar result is fully known at compile time. +fn fold_static_builtin_call(short_name: &str, args: &[Expr]) -> Option { + let name = php_symbol_key(short_name); + let normalized_args = normalize_static_builtin_args(&name, args)?; + let args = normalized_args.as_slice(); + match name.as_str() { + name if name == "strlen" => fold_strlen(args).map(ExprKind::IntLiteral), + name if name == "intval" => fold_intval(args).map(ExprKind::IntLiteral), + name if name == "floatval" => fold_floatval(args).map(ExprKind::FloatLiteral), + name if name == "strval" => fold_strval(args).map(ExprKind::StringLiteral), + name if name == "boolval" => fold_boolval(args).map(ExprKind::BoolLiteral), + name if name == "is_int" || name == "is_integer" || name == "is_long" => { + fold_type_probe(args, LiteralTypeProbe::Int).map(ExprKind::BoolLiteral) + } + name if name == "is_string" => { + fold_type_probe(args, LiteralTypeProbe::String).map(ExprKind::BoolLiteral) + } + name if name == "is_bool" => { + fold_type_probe(args, LiteralTypeProbe::Bool).map(ExprKind::BoolLiteral) + } + name if name == "is_float" || name == "is_double" || name == "is_real" => { + fold_type_probe(args, LiteralTypeProbe::Float).map(ExprKind::BoolLiteral) + } + name if name == "is_null" => { + fold_type_probe(args, LiteralTypeProbe::Null).map(ExprKind::BoolLiteral) + } + name if name == "is_scalar" => fold_is_scalar(args).map(ExprKind::BoolLiteral), + name if name == "gettype" => fold_gettype(args).map(ExprKind::StringLiteral), + name if name == "abs" => fold_abs(args).map(ExprKind::IntLiteral), + name if name == "count" => fold_count(args).map(ExprKind::IntLiteral), + name if name == "array_key_exists" => { + fold_array_key_exists(args).map(ExprKind::BoolLiteral) + } + name if name == "floor" => fold_floor(args).map(ExprKind::FloatLiteral), + name if name == "ceil" => fold_ceil(args).map(ExprKind::FloatLiteral), + name if name == "sqrt" => fold_sqrt(args).map(ExprKind::FloatLiteral), + name if name == "round" => fold_round(args).map(ExprKind::FloatLiteral), + name if name == "ord" => fold_ord(args).map(ExprKind::IntLiteral), + name if name == "chr" => fold_chr(args).map(ExprKind::StringLiteral), + name if name == "min" => fold_min(args).map(ExprKind::IntLiteral), + name if name == "max" => fold_max(args).map(ExprKind::IntLiteral), + name if name == "strtolower" => { + fold_ascii_case(args, AsciiCaseFold::Lower).map(ExprKind::StringLiteral) + } + name if name == "strtoupper" => { + fold_ascii_case(args, AsciiCaseFold::Upper).map(ExprKind::StringLiteral) + } + name if name == "ucfirst" => { + fold_ascii_first_char_case(args, FirstCharCaseFold::Upper).map(ExprKind::StringLiteral) + } + name if name == "lcfirst" => { + fold_ascii_first_char_case(args, FirstCharCaseFold::Lower).map(ExprKind::StringLiteral) + } + name if name == "strrev" => fold_ascii_strrev(args).map(ExprKind::StringLiteral), + name if name == "substr" => fold_ascii_substr(args).map(ExprKind::StringLiteral), + name if name == "str_repeat" => fold_ascii_str_repeat(args).map(ExprKind::StringLiteral), + name if name == "trim" => { + fold_ascii_default_trim(args, TrimSide::Both).map(ExprKind::StringLiteral) + } + name if name == "ltrim" => { + fold_ascii_default_trim(args, TrimSide::Left).map(ExprKind::StringLiteral) + } + name if name == "rtrim" || name == "chop" => { + fold_ascii_default_trim(args, TrimSide::Right).map(ExprKind::StringLiteral) + } + name if name == "str_contains" => { + fold_ascii_string_predicate(args, StringPredicate::Contains).map(ExprKind::BoolLiteral) + } + name if name == "str_starts_with" => { + fold_ascii_string_predicate(args, StringPredicate::StartsWith) + .map(ExprKind::BoolLiteral) + } + name if name == "str_ends_with" => { + fold_ascii_string_predicate(args, StringPredicate::EndsWith).map(ExprKind::BoolLiteral) + } + _ => None, + } +} + +/// Normalizes named/static-spread builtin arguments before attempting a static fold. +fn normalize_static_builtin_args(short_name: &str, args: &[Expr]) -> Option> { + let sig = builtin_call_sig(short_name)?; + let call_span = args.first().map(|arg| arg.span).unwrap_or_else(Span::dummy); + let plan = plan_call_args(&sig, args, call_span, true, false).ok()?; + Some(plan.normalized_args()) +} + +/// Folds `strlen("literal")` to an integer result. +fn fold_strlen(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + i64::try_from(value.len()).ok() +} + +/// Folds `intval()` for literal scalar inputs whose PHP result is unambiguous here. +fn fold_intval(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::BoolLiteral(value) => Some(i64::from(*value)), + ExprKind::StringLiteral(value) => value.trim().parse::().ok(), + _ => None, + } +} + +/// Folds `floatval()` for literal scalar inputs whose PHP result is unambiguous here. +fn fold_floatval(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let value = match &args[0].kind { + ExprKind::IntLiteral(value) => *value as f64, + ExprKind::FloatLiteral(value) => *value, + ExprKind::BoolLiteral(value) => f64::from(u8::from(*value)), + ExprKind::StringLiteral(value) => value.trim().parse::().ok()?, + ExprKind::Null => 0.0, + _ => return None, + }; + value.is_finite().then_some(value) +} + +/// Folds `strval()` for literal scalar inputs with stable PHP string results. +fn fold_strval(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(value) => Some(value.to_string()), + ExprKind::BoolLiteral(true) => Some("1".to_string()), + ExprKind::BoolLiteral(false) | ExprKind::Null => Some(String::new()), + ExprKind::StringLiteral(value) => Some(value.clone()), + _ => None, + } +} + +/// Folds `boolval()` for literal scalar inputs whose PHP truthiness is clear. +fn fold_boolval(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(value) => Some(*value != 0), + ExprKind::BoolLiteral(value) => Some(*value), + ExprKind::StringLiteral(value) => Some(!(value.is_empty() || value == "0")), + ExprKind::Null => Some(false), + _ => None, + } +} + +/// Literal scalar type checked by pure `is_*` builtin folds. +enum LiteralTypeProbe { + Int, + String, + Bool, + Float, + Null, +} + +/// Folds pure `is_*` type probes for literal scalar inputs. +fn fold_type_probe(args: &[Expr], probe: LiteralTypeProbe) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(_) => Some(matches!(probe, LiteralTypeProbe::Int)), + ExprKind::StringLiteral(_) => Some(matches!(probe, LiteralTypeProbe::String)), + ExprKind::BoolLiteral(_) => Some(matches!(probe, LiteralTypeProbe::Bool)), + ExprKind::FloatLiteral(_) => Some(matches!(probe, LiteralTypeProbe::Float)), + ExprKind::Null => Some(matches!(probe, LiteralTypeProbe::Null)), + _ => None, + } +} + +/// Folds `is_scalar()` for literal scalar and null inputs. +fn fold_is_scalar(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + match &args[0].kind { + ExprKind::IntLiteral(_) + | ExprKind::StringLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::FloatLiteral(_) => Some(true), + ExprKind::Null => Some(false), + _ => None, + } +} + +/// Folds `gettype()` for literal scalar and null inputs with stable PHP spellings. +fn fold_gettype(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let ty = match &args[0].kind { + ExprKind::IntLiteral(_) => "integer", + ExprKind::FloatLiteral(_) => "double", + ExprKind::StringLiteral(_) => "string", + ExprKind::BoolLiteral(_) => "boolean", + ExprKind::Null => "NULL", + _ => return None, + }; + Some(ty.to_string()) +} + +/// Folds `abs()` for integer literals that stay representable as `int`. +fn fold_abs(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + const_int_expr(&args[0])?.checked_abs() +} + +/// Folds `count()` for static array literals whose element expressions have no side effects. +fn fold_count(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + i64::try_from(static_array_key_ids(&args[0])?.len()).ok() +} + +/// Folds `array_key_exists()` for static array literals and static scalar keys. +fn fold_array_key_exists(args: &[Expr]) -> Option { + if args.len() != 2 { + return None; + } + let key = static_array_key_fold_id(&args[0])?; + Some(static_array_key_ids(&args[1])?.contains(&key)) +} + +/// Returns normalized key identifiers for a static array literal. +fn static_array_key_ids(expr: &Expr) -> Option> { + match &expr.kind { + ExprKind::ArrayLiteral(items) => { + if !items.iter().all(static_array_value_is_fold_safe) { + return None; + } + (0..items.len()) + .map(|index| Some(format!("i:{index}"))) + .collect() + } + ExprKind::ArrayLiteralAssoc(pairs) => { + let mut keys = BTreeSet::new(); + for (key, value) in pairs { + if !static_array_value_is_fold_safe(value) { + return None; + } + keys.insert(static_array_key_fold_id(key)?); + } + Some(keys) + } + _ => None, + } +} + +/// Returns true when evaluating this expression while building a static array has no side effects. +fn static_array_value_is_fold_safe(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::StringLiteral(_) + | ExprKind::IntLiteral(_) + | ExprKind::FloatLiteral(_) + | ExprKind::BoolLiteral(_) + | ExprKind::Null => true, + ExprKind::Negate(inner) | ExprKind::Not(inner) | ExprKind::BitNot(inner) => { + static_array_value_is_fold_safe(inner) + } + ExprKind::ArrayLiteral(items) => items.iter().all(static_array_value_is_fold_safe), + ExprKind::ArrayLiteralAssoc(pairs) => pairs.iter().all(|(key, value)| { + static_array_key_fold_id(key).is_some() && static_array_value_is_fold_safe(value) + }), + _ => false, + } +} + +/// Returns a normalized key identifier for static array-literal count folding. +fn static_array_key_fold_id(expr: &Expr) -> Option { + if let Some(value) = static_integer_array_key_value(expr) { + return Some(format!("i:{value}")); + } + match &expr.kind { + ExprKind::Null => Some("s:".to_string()), + ExprKind::StringLiteral(value) => Some(format!("s:{value}")), + _ => None, + } +} + +/// Folds `floor()` for finite numeric literals. +fn fold_floor(args: &[Expr]) -> Option { + fold_finite_numeric_unary(args, f64::floor) +} + +/// Folds `ceil()` for finite numeric literals. +fn fold_ceil(args: &[Expr]) -> Option { + fold_finite_numeric_unary(args, f64::ceil) +} + +/// Folds `sqrt()` for non-negative finite numeric literals. +fn fold_sqrt(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let value = const_finite_numeric_expr(&args[0])?; + (value >= 0.0) + .then(|| value.sqrt()) + .filter(|sqrt| sqrt.is_finite()) +} + +/// Folds one-argument `round()` for finite numeric literals. +fn fold_round(args: &[Expr]) -> Option { + fold_finite_numeric_unary(args, f64::round) +} + +/// Applies a finite `f64` builtin fold to one numeric literal argument. +fn fold_finite_numeric_unary(args: &[Expr], fold: fn(f64) -> f64) -> Option { + if args.len() != 1 { + return None; + } + let value = fold(const_finite_numeric_expr(&args[0])?); + value.is_finite().then_some(value) +} + +/// Folds `ord()` for literal strings by returning the first byte value. +fn fold_ord(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + value.as_bytes().first().map(|byte| i64::from(*byte)) +} + +/// Folds `chr()` for ASCII byte values representable by the AST string type. +fn fold_chr(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let value = const_int_expr(&args[0])?; + let byte = u8::try_from(value).ok()?; + if !byte.is_ascii() { + return None; + } + Some(char::from(byte).to_string()) +} + +/// Folds `min()` over integer literal arguments. +fn fold_min(args: &[Expr]) -> Option { + fold_int_values(args)?.into_iter().min() +} + +/// Folds `max()` over integer literal arguments. +fn fold_max(args: &[Expr]) -> Option { + fold_int_values(args)?.into_iter().max() +} + +/// Collects integer literal arguments for variadic pure numeric folds. +fn fold_int_values(args: &[Expr]) -> Option> { + if args.is_empty() { + return None; + } + args.iter().map(const_int_expr).collect() +} + +/// ASCII-only case conversion supported by literal eval builtin folding. +enum AsciiCaseFold { + Lower, + Upper, +} + +/// First-byte ASCII case conversion supported by literal eval builtin folding. +enum FirstCharCaseFold { + Lower, + Upper, +} + +/// Side selected by default-mask ASCII trim folding. +enum TrimSide { + Left, + Right, + Both, +} + +/// Two-string ASCII predicates supported by literal eval builtin folding. +enum StringPredicate { + Contains, + StartsWith, + EndsWith, +} + +/// Folds ASCII-only `strtolower()` and `strtoupper()` literal calls. +fn fold_ascii_case(args: &[Expr], mode: AsciiCaseFold) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let folded = match mode { + AsciiCaseFold::Lower => value.to_ascii_lowercase(), + AsciiCaseFold::Upper => value.to_ascii_uppercase(), + }; + Some(folded) +} + +/// Folds ASCII-only `ucfirst()` and `lcfirst()` literal calls. +fn fold_ascii_first_char_case(args: &[Expr], mode: FirstCharCaseFold) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let mut bytes = value.as_bytes().to_vec(); + if let Some(first) = bytes.first_mut() { + match mode { + FirstCharCaseFold::Lower => first.make_ascii_lowercase(), + FirstCharCaseFold::Upper => first.make_ascii_uppercase(), + } + } + String::from_utf8(bytes).ok() +} + +/// Folds ASCII-only `strrev()` literal calls with PHP byte-order behavior. +fn fold_ascii_strrev(args: &[Expr]) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + Some(value.bytes().rev().map(char::from).collect()) +} + +/// Folds ASCII-only `substr()` literal calls with non-negative offset and length. +fn fold_ascii_substr(args: &[Expr]) -> Option { + if !(2..=3).contains(&args.len()) { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let offset = usize::try_from(const_int_expr(&args[1])?).ok()?; + let start = offset.min(value.len()); + let end = if let Some(length_arg) = args.get(2) { + let length = usize::try_from(const_int_expr(length_arg)?).ok()?; + start.saturating_add(length).min(value.len()) + } else { + value.len() + }; + Some(value[start..end].to_string()) +} + +/// Folds ASCII-only `str_repeat()` literal calls with a bounded static result. +fn fold_ascii_str_repeat(args: &[Expr]) -> Option { + if args.len() != 2 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let times = usize::try_from(const_int_expr(&args[1])?).ok()?; + let bytes = value.len().checked_mul(times)?; + if bytes > MAX_STATIC_STRING_FOLD_BYTES { + return None; + } + Some(value.repeat(times)) +} + +/// Folds one-argument ASCII `trim()`/`ltrim()`/`rtrim()` calls using PHP's default mask. +fn fold_ascii_default_trim(args: &[Expr], side: TrimSide) -> Option { + if args.len() != 1 { + return None; + } + let ExprKind::StringLiteral(value) = &args[0].kind else { + return None; + }; + if !value.is_ascii() { + return None; + } + let trimmed = match side { + TrimSide::Left => value.trim_start_matches(is_php_default_trim_char), + TrimSide::Right => value.trim_end_matches(is_php_default_trim_char), + TrimSide::Both => value.trim_matches(is_php_default_trim_char), + }; + Some(trimmed.to_string()) +} + +/// Returns true for characters removed by PHP's default trim character mask. +fn is_php_default_trim_char(ch: char) -> bool { + matches!(ch, '\0' | '\t' | '\n' | '\r' | '\x0b' | ' ') +} + +/// Folds ASCII-only two-string predicate calls to their boolean result. +fn fold_ascii_string_predicate(args: &[Expr], predicate: StringPredicate) -> Option { + if args.len() != 2 { + return None; + } + let (ExprKind::StringLiteral(haystack), ExprKind::StringLiteral(needle)) = + (&args[0].kind, &args[1].kind) + else { + return None; + }; + if !haystack.is_ascii() || !needle.is_ascii() { + return None; + } + Some(match predicate { + StringPredicate::Contains => haystack.contains(needle), + StringPredicate::StartsWith => haystack.starts_with(needle), + StringPredicate::EndsWith => haystack.ends_with(needle), + }) +} + +/// Evaluates integer-only literal expressions recognized by eval AOT analysis. +pub(crate) fn const_int_expr(expr: &Expr) -> Option { + match &expr.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::Negate(inner) => const_int_expr(inner)?.checked_neg(), + _ => None, + } +} + +/// Evaluates finite numeric literal expressions recognized by eval AOT analysis. +fn const_finite_numeric_expr(expr: &Expr) -> Option { + const MAX_EXACT_F64_INT: i64 = 9_007_199_254_740_992; + let value = match &expr.kind { + ExprKind::IntLiteral(value) if (-MAX_EXACT_F64_INT..=MAX_EXACT_F64_INT).contains(value) => { + *value as f64 + } + ExprKind::FloatLiteral(value) => *value, + ExprKind::Negate(inner) => -const_finite_numeric_expr(inner)?, + _ => return None, + }; + value.is_finite().then_some(value) +} + +/// Checks a user-function signature against the native-only eval call subset. +pub(crate) fn static_function_signature_supported(signature: &FunctionSig, args: &[Expr]) -> bool { + if !signature.declared_return + || signature.declared_params.iter().any(|declared| !declared) + || signature.ref_params.len() != signature.params.len() + || signature.variadic.is_some() + || !static_function_return_type_supported(&signature.return_type) + { + return false; + } + let Some(args) = normalize_static_function_args(signature, args) else { + return false; + }; + signature.params.len() == args.len() + && signature + .params + .iter() + .zip(signature.ref_params.iter().copied()) + .zip(args.iter()) + .all(|((param, by_ref), arg)| !by_ref && static_function_arg_supported(¶m.1, arg)) +} + +/// Normalizes user-function arguments for eval AOT eligibility checks. +/// +/// Static spread arrays are expanded through the shared call planner; dynamic +/// spreads that remain after planning stay on the eval bridge fallback. +fn normalize_static_function_args(signature: &FunctionSig, args: &[Expr]) -> Option> { + if !crate::types::call_args::has_named_args(args) + && !args + .iter() + .any(|arg| matches!(arg.kind, ExprKind::Spread(_))) + { + return normalize_positional_static_function_args(signature, args); + } + let call_span = args.first().map(|arg| arg.span).unwrap_or_else(Span::dummy); + let plan = plan_call_args(signature, args, call_span, false, false).ok()?; + if plan.has_spread_args() { + return None; + } + Some(plan.normalized_args()) +} + +/// Appends scalar default values for positional static user-function calls. +fn normalize_positional_static_function_args( + signature: &FunctionSig, + args: &[Expr], +) -> Option> { + if args.len() > signature.params.len() { + return None; + } + let mut normalized = args.to_vec(); + for idx in args.len()..signature.params.len() { + let default = signature.defaults.get(idx)?.clone()?; + normalized.push(default); + } + Some(normalized) +} + +/// Returns true when a user function return can be boxed by eval EIR AOT. +fn static_function_return_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Bool | PhpType::Float | PhpType::Str + ) +} + +/// Returns true when a literal argument matches the supported scalar parameter type. +fn static_function_arg_supported(param_ty: &PhpType, arg: &Expr) -> bool { + matches!( + (param_ty.codegen_repr(), &arg.kind), + (PhpType::Int, ExprKind::IntLiteral(_)) + | (PhpType::Bool, ExprKind::BoolLiteral(_)) + | (PhpType::Float, ExprKind::FloatLiteral(_)) + | (PhpType::Str, ExprKind::StringLiteral(_)) + ) +} diff --git a/src/ir/instr.rs b/src/ir/instr.rs index c975c818c6..c0ee10211b 100644 --- a/src/ir/instr.rs +++ b/src/ir/instr.rs @@ -112,18 +112,36 @@ pub enum Immediate { Bool(bool), Data(DataId), LocalSlot(LocalSlotId), - LocalSlotPair { first: LocalSlotId, second: LocalSlotId }, + LocalSlotPair { + first: LocalSlotId, + second: LocalSlotId, + }, GlobalName(DataId), FunctionRef(FunctionId), BuiltinRef(BuiltinId), RuntimeRef(RuntimeId), ExternRef(u32), ClassRef(u32), - EnumCaseRef { enum_id: u32, case_id: u32 }, - MethodRef { class: u32, method: u32 }, - PropertyRef { class: u32, property: u32 }, - FieldRef { layout: u32, field: u32 }, - FunctionVariantRef { group: u32, variant: u32 }, + EnumCaseRef { + enum_id: u32, + case_id: u32, + }, + MethodRef { + class: u32, + method: u32, + }, + PropertyRef { + class: u32, + property: u32, + }, + FieldRef { + layout: u32, + field: u32, + }, + FunctionVariantRef { + group: u32, + variant: u32, + }, HeapKind(IrHeapKind), MixedTag(u8), MixedNumericOp(MixedNumericOp), @@ -364,6 +382,8 @@ pub enum Op { FunctionVariantCall, BuiltinCall, EvalLiteralCall, + EvalScopeGet, + EvalScopeSet, EvalFunctionCall, EvalFunctionCallArray, EvalFunctionExists, @@ -435,12 +455,49 @@ impl Op { use Effects as E; use Op::*; match self { - ConstI64 | ConstF64 | ConstStr | ConstNull | ConstBool | ConstClassName - | DataAddr | IAdd | ISub | IMul | IPow | INeg | IBitAnd | IBitOr | IBitXor - | IBitNot | IShl | IShrA | FAdd | FSub | FMul | FDiv | FPow | FNeg | ICmp - | FCmp | StrLen | IToF | FToI | BoolToStr | StrToI | StrToF | StrToNumber - | MixedTagOf | IsNull | IsTruthy | IsEmpty | FunctionVariantDispatch | PtrCast - | PtrOffset | Move | Borrow | Nop => E::PURE, + ConstI64 + | ConstF64 + | ConstStr + | ConstNull + | ConstBool + | ConstClassName + | DataAddr + | IAdd + | ISub + | IMul + | IPow + | INeg + | IBitAnd + | IBitOr + | IBitXor + | IBitNot + | IShl + | IShrA + | FAdd + | FSub + | FMul + | FDiv + | FPow + | FNeg + | ICmp + | FCmp + | StrLen + | IToF + | FToI + | BoolToStr + | StrToI + | StrToF + | StrToNumber + | MixedTagOf + | IsNull + | IsTruthy + | IsEmpty + | FunctionVariantDispatch + | PtrCast + | PtrOffset + | Move + | Borrow + | Nop => E::PURE, IDiv | ISDiv | ISMod | PtrCheckNonnull => E::MAY_FATAL, ICheckedAdd | ICheckedSub | ICheckedMul => E::ALLOC_HEAP | E::READS_HEAP, ConstEnumCase => E::ALLOC_HEAP, @@ -450,14 +507,28 @@ impl Op { | FinallyExit => E::WRITES_LOCAL, PromoteLocalRefCell => { E::READS_LOCAL | E::WRITES_LOCAL | E::ALLOC_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP - }, + } AliasLocalRefCell => E::READS_LOCAL | E::WRITES_LOCAL, - ReleaseLocalRefCell => E::READS_LOCAL | E::WRITES_LOCAL | E::WRITES_HEAP | E::REFCOUNT_OP, - LoadGlobal | LoadStaticProperty | LoadReflectionStaticProperty - | ReflectionStaticPropertyInitialized | ScopedConstantGet | ClassAttrNames - | ClassAttrArgs | ClassGetAttributes | CatchCurrent => E::READS_GLOBAL, - StoreGlobal | StoreStaticLocal | StoreStaticProperty | StoreReflectionStaticProperty - | InitStaticLocal | IncludeOnceMark | FunctionVariantMark | TryPushHandler + ReleaseLocalRefCell => { + E::READS_LOCAL | E::WRITES_LOCAL | E::WRITES_HEAP | E::REFCOUNT_OP + } + LoadGlobal + | LoadStaticProperty + | LoadReflectionStaticProperty + | ReflectionStaticPropertyInitialized + | ScopedConstantGet + | ClassAttrNames + | ClassAttrArgs + | ClassGetAttributes + | CatchCurrent => E::READS_GLOBAL, + StoreGlobal + | StoreStaticLocal + | StoreStaticProperty + | StoreReflectionStaticProperty + | InitStaticLocal + | IncludeOnceMark + | FunctionVariantMark + | TryPushHandler | TryPopHandler => E::WRITES_GLOBAL, IncludeOnceGuard => E::READS_GLOBAL | E::WRITES_GLOBAL, IToStr | FToStr | ResourceToStr | StrConcat | StrCharAt | StrInterpolate @@ -511,14 +582,25 @@ impl Op { E::READS_HEAP | E::ALLOC_HEAP | E::MAY_THROW } EvalFunctionExists | EvalClassExists | EvalConstantExists => E::READS_GLOBAL, + EvalScopeGet => E::READS_HEAP | E::MAY_FATAL, + EvalScopeSet => E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL, EvalConstantFetch => { E::READS_GLOBAL | E::READS_HEAP | E::WRITES_HEAP | E::REFCOUNT_OP | E::MAY_FATAL } - Call | FunctionVariantCall | BuiltinCall | EvalLiteralCall | EvalFunctionCall - | EvalFunctionCallArray | EvalObjectNew | EvalStaticMethodCall | RuntimeCall - | ClosureCall | ExprCall | CallableDescriptorInvoke | PipeCall | FiberRuntimeCall => { - E::all().difference(E::REFCOUNT_OP) - } + Call + | FunctionVariantCall + | BuiltinCall + | EvalLiteralCall + | EvalFunctionCall + | EvalFunctionCallArray + | EvalObjectNew + | EvalStaticMethodCall + | RuntimeCall + | ClosureCall + | ExprCall + | CallableDescriptorInvoke + | PipeCall + | FiberRuntimeCall => E::all().difference(E::REFCOUNT_OP), ExternCall | ExternGlobalLoad | ExternGlobalStore => { E::READS_HEAP | E::WRITES_HEAP | E::READS_PROCESS | E::WRITES_PROCESS | E::MAY_THROW } @@ -729,6 +811,8 @@ impl Op { FunctionVariantCall => "function_variant_call", BuiltinCall => "builtin_call", EvalLiteralCall => "eval_literal_call", + EvalScopeGet => "eval_scope_get", + EvalScopeSet => "eval_scope_set", EvalFunctionCall => "eval_function_call", EvalFunctionCallArray => "eval_function_call_array", EvalFunctionExists => "eval_function_exists", diff --git a/src/ir/validator.rs b/src/ir/validator.rs index c5a6ce283d..b6f7302c7b 100644 --- a/src/ir/validator.rs +++ b/src/ir/validator.rs @@ -26,7 +26,10 @@ pub enum ValidationError { NoBlocks, NoEntryBlock, EntryBlockHasParams(BlockId), - BlockIdMismatch { expected: BlockId, actual: BlockId }, + BlockIdMismatch { + expected: BlockId, + actual: BlockId, + }, BlockMissingTerminator(BlockId), UnknownBlock(BlockId), UnknownInstruction(InstId), @@ -126,7 +129,10 @@ fn validate_function_shape(function: &Function) -> Result<(), ValidationError> { if function.block(function.entry).is_none() { return Err(ValidationError::NoEntryBlock); } - if !function.blocks[function.entry.as_raw() as usize].params.is_empty() { + if !function.blocks[function.entry.as_raw() as usize] + .params + .is_empty() + { return Err(ValidationError::EntryBlockHasParams(function.entry)); } for (index, block) in function.blocks.iter().enumerate() { @@ -211,7 +217,14 @@ fn validate_instructions( validate_instruction_result(function, block.id, index as u32, *inst_id, inst)?; validate_instruction_effects(*inst_id, inst)?; validate_instruction_immediate(*inst_id, inst)?; - validate_instruction_operands(function, block.id, index as u32, *inst_id, inst, dominators)?; + validate_instruction_operands( + function, + block.id, + index as u32, + *inst_id, + inst, + dominators, + )?; validate_opcode_rules(function, *inst_id, inst)?; } } @@ -243,7 +256,13 @@ fn validate_instruction_result( if value.ownership != inst.result_ownership { return Err(ValidationError::OwnershipTypeMismatch(value_id)); } - if value.def != (ValueDef::Instruction { block, index, inst: inst_id }) { + if value.def + != (ValueDef::Instruction { + block, + index, + inst: inst_id, + }) + { return Err(ValidationError::ValueDefMismatch(value_id)); } Ok(()) @@ -252,7 +271,10 @@ fn validate_instruction_result( } /// Validates that non-refinable opcodes carry their canonical effect set. -fn validate_instruction_effects(inst_id: InstId, inst: &Instruction) -> Result<(), ValidationError> { +fn validate_instruction_effects( + inst_id: InstId, + inst: &Instruction, +) -> Result<(), ValidationError> { let expected = inst.op.default_effects(); if !inst.op.allows_effect_refinement() && inst.effects != expected { return Err(ValidationError::EffectMismatch { @@ -265,7 +287,10 @@ fn validate_instruction_effects(inst_id: InstId, inst: &Instruction) -> Result<( } /// Validates immediate shape for opcodes whose immediate is structurally required. -fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result<(), ValidationError> { +fn validate_instruction_immediate( + inst_id: InstId, + inst: &Instruction, +) -> Result<(), ValidationError> { use Immediate as Imm; use Op::*; match inst.op { @@ -291,6 +316,9 @@ fn validate_instruction_immediate(inst_id: InstId, inst: &Instruction) -> Result PromoteLocalRefCell | AliasLocalRefCell => require_immediate(inst_id, inst, "local slot pair", |imm| { matches!(imm, Imm::LocalSlotPair { .. }) }), + EvalScopeGet | EvalScopeSet => require_immediate(inst_id, inst, "global name", |imm| { + matches!(imm, Imm::GlobalName(_)) + }), ICmp | FCmp => require_immediate(inst_id, inst, "comparison predicate", |imm| { matches!(imm, Imm::CmpPredicate(_)) }), @@ -326,12 +354,18 @@ fn require_immediate( matches_expected: impl FnOnce(&Immediate) -> bool, ) -> Result<(), ValidationError> { let Some(imm) = inst.immediate.as_ref() else { - return Err(ValidationError::MissingImmediate { inst: inst_id, expected }); + return Err(ValidationError::MissingImmediate { + inst: inst_id, + expected, + }); }; if matches_expected(imm) { Ok(()) } else { - Err(ValidationError::MissingImmediate { inst: inst_id, expected }) + Err(ValidationError::MissingImmediate { + inst: inst_id, + expected, + }) } } @@ -351,7 +385,11 @@ fn validate_instruction_operands( } /// Validates core opcode operand/result type rules. -fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instruction) -> Result<(), ValidationError> { +fn validate_opcode_rules( + function: &Function, + inst_id: InstId, + inst: &Instruction, +) -> Result<(), ValidationError> { use Op::*; match inst.op { ConstI64 | ConstBool | ConstNull => check_count(inst_id, inst, 0, "0"), @@ -363,7 +401,10 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio | EvalClassExists | EvalConstantExists | EvalConstantFetch | ConcatReset | GcCollect | Nop => { check_count(inst_id, inst, 0, "0") } - EvalLiteralCall | EvalFunctionCallArray => check_count(inst_id, inst, 1, "1"), + EvalLiteralCall | EvalFunctionCallArray | EvalScopeGet => { + check_count(inst_id, inst, 1, "1") + } + EvalScopeSet => check_count(inst_id, inst, 2, "2"), ClosureNew => Ok(()), FirstClassCallableNew => check_count_at_most(inst_id, inst, 1, "0 or 1"), ObjectNew => Ok(()), @@ -393,17 +434,23 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio StrToI | StrToF | StrToNumber | StrLen | StrPersist => { check_unary(function, inst_id, inst, IrType::Str, "Str") } - StrConcat | StrEq | StrCmp | StrLooseEq => check_binary(function, inst_id, inst, IrType::Str, "Str"), + StrConcat | StrEq | StrCmp | StrLooseEq => { + check_binary(function, inst_id, inst, IrType::Str, "Str") + } StrCharAt => { check_count(inst_id, inst, 2, "2")?; check_operand_type(function, inst_id, inst, 0, IrType::Str, "Str")?; check_operand_type(function, inst_id, inst, 1, IrType::I64, "I64") } BufferNew => check_unary(function, inst_id, inst, IrType::I64, "I64"), - LoadLocal | LoadRefCell | LoadGlobal | LoadStaticLocal | LoadStaticProperty - | LoadReflectionStaticProperty | ReflectionStaticPropertyInitialized | ExternGlobalLoad => { - check_count(inst_id, inst, 0, "0") - } + LoadLocal + | LoadRefCell + | LoadGlobal + | LoadStaticLocal + | LoadStaticProperty + | LoadReflectionStaticProperty + | ReflectionStaticPropertyInitialized + | ExternGlobalLoad => check_count(inst_id, inst, 0, "0"), UnsetLocal | PromoteLocalRefCell | AliasLocalRefCell | ReleaseLocalRefCell => { check_count(inst_id, inst, 0, "0") } @@ -415,9 +462,23 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio check_count(inst_id, inst, 1, "1") } MixedTagOf | MixedUnbox | MixedCastBool | MixedCastInt | MixedCastFloat - | MixedCastString => check_heap_unary(function, inst_id, inst, IrHeapKind::Mixed, "Heap(Mixed)"), - ArrayUnion => check_binary(function, inst_id, inst, IrType::Heap(IrHeapKind::Array), "Heap(Array)"), - HashUnion => check_binary(function, inst_id, inst, IrType::Heap(IrHeapKind::Hash), "Heap(Hash)"), + | MixedCastString => { + check_heap_unary(function, inst_id, inst, IrHeapKind::Mixed, "Heap(Mixed)") + } + ArrayUnion => check_binary( + function, + inst_id, + inst, + IrType::Heap(IrHeapKind::Array), + "Heap(Array)", + ), + HashUnion => check_binary( + function, + inst_id, + inst, + IrType::Heap(IrHeapKind::Hash), + "Heap(Hash)", + ), ArrayHashUnion => check_array_hash_union(function, inst_id, inst), HashArrayUnion => check_hash_array_union(function, inst_id, inst), HashSpread => check_binary(function, inst_id, inst, IrType::Heap(IrHeapKind::Hash), "Heap(Hash)"), @@ -433,9 +494,17 @@ fn validate_opcode_rules(function: &Function, inst_id: InstId, inst: &Instructio } MixedArrayAppend => { check_count(inst_id, inst, 2, "2")?; - check_operand_type(function, inst_id, inst, 0, IrType::Heap(IrHeapKind::Mixed), "Heap(Mixed)") + check_operand_type( + function, + inst_id, + inst, + 0, + IrType::Heap(IrHeapKind::Mixed), + "Heap(Mixed)", + ) } - HashLen | HashGet | HashIsset | HashSet | HashAppend | HashEnsureUnique | HashCloneShallow => { + HashLen | HashGet | HashIsset | HashSet | HashAppend | HashEnsureUnique + | HashCloneShallow => { check_first_heap(function, inst_id, inst, IrHeapKind::Hash, "Heap(Hash)") } IterCurrentValueRef => check_count(inst_id, inst, 1, "1"), @@ -469,8 +538,22 @@ fn check_array_hash_union( inst: &Instruction, ) -> Result<(), ValidationError> { check_count(inst_id, inst, 2, "2")?; - check_operand_type(function, inst_id, inst, 0, IrType::Heap(IrHeapKind::Array), "Heap(Array)")?; - check_operand_type(function, inst_id, inst, 1, IrType::Heap(IrHeapKind::Hash), "Heap(Hash)") + check_operand_type( + function, + inst_id, + inst, + 0, + IrType::Heap(IrHeapKind::Array), + "Heap(Array)", + )?; + check_operand_type( + function, + inst_id, + inst, + 1, + IrType::Heap(IrHeapKind::Hash), + "Heap(Hash)", + ) } /// Validates the operand shape for associative+indexed array union. @@ -480,12 +563,31 @@ fn check_hash_array_union( inst: &Instruction, ) -> Result<(), ValidationError> { check_count(inst_id, inst, 2, "2")?; - check_operand_type(function, inst_id, inst, 0, IrType::Heap(IrHeapKind::Hash), "Heap(Hash)")?; - check_operand_type(function, inst_id, inst, 1, IrType::Heap(IrHeapKind::Array), "Heap(Array)") + check_operand_type( + function, + inst_id, + inst, + 0, + IrType::Heap(IrHeapKind::Hash), + "Heap(Hash)", + )?; + check_operand_type( + function, + inst_id, + inst, + 1, + IrType::Heap(IrHeapKind::Array), + "Heap(Array)", + ) } /// Validates one exact operand count. -fn check_count(inst_id: InstId, inst: &Instruction, expected: usize, expected_label: &'static str) -> Result<(), ValidationError> { +fn check_count( + inst_id: InstId, + inst: &Instruction, + expected: usize, + expected_label: &'static str, +) -> Result<(), ValidationError> { if inst.operands.len() == expected { Ok(()) } else { @@ -498,7 +600,12 @@ fn check_count(inst_id: InstId, inst: &Instruction, expected: usize, expected_la } /// Validates a minimum operand count. -fn check_count_at_least(inst_id: InstId, inst: &Instruction, min: usize, expected_label: &'static str) -> Result<(), ValidationError> { +fn check_count_at_least( + inst_id: InstId, + inst: &Instruction, + min: usize, + expected_label: &'static str, +) -> Result<(), ValidationError> { if inst.operands.len() >= min { Ok(()) } else { @@ -511,7 +618,12 @@ fn check_count_at_least(inst_id: InstId, inst: &Instruction, min: usize, expecte } /// Validates a maximum operand count. -fn check_count_at_most(inst_id: InstId, inst: &Instruction, max: usize, expected_label: &'static str) -> Result<(), ValidationError> { +fn check_count_at_most( + inst_id: InstId, + inst: &Instruction, + max: usize, + expected_label: &'static str, +) -> Result<(), ValidationError> { if inst.operands.len() <= max { Ok(()) } else { @@ -685,7 +797,9 @@ fn validate_terminators( validate_branch_args(function, *default, default_args)?; validate_terminator_uses(function, block.id, default_args, dominators)?; } - Terminator::Return { value } => validate_return(function, block.id, *value, dominators)?, + Terminator::Return { value } => { + validate_return(function, block.id, *value, dominators)? + } Terminator::Throw { value } => { validate_use(function, *value, block.id, None, dominators)?; } @@ -774,7 +888,11 @@ fn validate_terminator_uses( } /// Validates destination block argument count and type compatibility. -fn validate_branch_args(function: &Function, target: BlockId, args: &[ValueId]) -> Result<(), ValidationError> { +fn validate_branch_args( + function: &Function, + target: BlockId, + args: &[ValueId], +) -> Result<(), ValidationError> { let Some(target_block) = function.block(target) else { return Err(ValidationError::UnknownBlock(target)); }; @@ -871,9 +989,9 @@ fn definition_dominates_use( .map(|set| set.contains(&block)) .unwrap_or(false) } - ValueDef::Instruction { block, index, .. } if block == use_block => { - use_inst_index.map(|use_index| index < use_index).unwrap_or(true) - } + ValueDef::Instruction { block, index, .. } if block == use_block => use_inst_index + .map(|use_index| index < use_index) + .unwrap_or(true), ValueDef::Instruction { block, .. } => dominators .get(&use_block) .map(|set| set.contains(&block)) @@ -1107,8 +1225,8 @@ fn php_type_compatible(ir_type: IrType, php_type: &PhpType) -> bool { /// Returns true when ownership is coherent with storage and PHP type metadata. fn ownership_compatible(ir_type: IrType, php_type: &PhpType, ownership: Ownership) -> bool { let php_type = php_type.codegen_repr(); - let tracks_lifetime = ir_type.is_refcounted_storage() - || Ownership::php_type_needs_lifetime_tracking(&php_type); + let tracks_lifetime = + ir_type.is_refcounted_storage() || Ownership::php_type_needs_lifetime_tracking(&php_type); if tracks_lifetime { !matches!(ownership, Ownership::NonHeap) } else { diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index e34492b651..a5f6fa938b 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -11,11 +11,11 @@ //! - Control-flow joins can reload locals from slots, so Phase 03 does not need //! to synthesize block-parameter phis for every PHP variable yet. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use crate::ir::{ - BlockId, Builder, DataId, DataPool, Effects, Immediate, IrType, LocalKind, LocalSlotId, Op, - Ownership, ValueId, Function, + BlockId, Builder, DataId, DataPool, Effects, Function, Immediate, IrType, LocalKind, + LocalSlotId, Op, Ownership, ValueId, }; use crate::names::{php_symbol_key, property_hook_get_method, property_hook_set_method}; use crate::parser::ast::{Expr, ExprKind, StaticReceiver, Stmt, TypeExpr}; @@ -149,6 +149,11 @@ pub(crate) struct LoweringContext<'m, 'f> { closure_counter: usize, hidden_temp_counter: usize, eval_barrier_active: bool, + eval_scope_read_param: Option, + eval_scope_read_names: HashSet, + eval_scope_write_names: HashSet, + eval_scope_flush_names: BTreeSet, + source_path: Option, } impl<'m, 'f> LoweringContext<'m, 'f> { @@ -174,6 +179,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { return_php_type: PhpType, in_main: bool, all_global_var_names: HashSet, + source_path: Option, ) -> Self { let return_type = return_ir_type(&return_php_type); Self { @@ -219,9 +225,19 @@ impl<'m, 'f> LoweringContext<'m, 'f> { closure_counter: 0, hidden_temp_counter: 0, eval_barrier_active: false, + eval_scope_read_param: None, + eval_scope_read_names: HashSet::new(), + eval_scope_write_names: HashSet::new(), + eval_scope_flush_names: BTreeSet::new(), + source_path, } } + /// Returns the canonical PHP source path associated with this lowered body, if known. + pub(crate) fn source_path(&self) -> Option<&str> { + self.source_path.as_deref() + } + /// Interns a string literal or metadata name in the module data pool. pub(crate) fn intern_string(&mut self, value: &str) -> DataId { self.data.intern_string(value) @@ -233,7 +249,8 @@ impl<'m, 'f> LoweringContext<'m, 'f> { TypeExpr::Named(name) => { let name = name.as_str().trim_start_matches('\\'); let php_type = named_type_expr_to_php_type(name); - if matches!(php_type, PhpType::Object(_)) && self.packed_classes.contains_key(name) { + if matches!(php_type, PhpType::Object(_)) && self.packed_classes.contains_key(name) + { PhpType::Packed(name.to_string()) } else { php_type @@ -245,9 +262,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { TypeExpr::Array(inner) => { PhpType::Array(Box::new(self.type_expr_to_php_type_for_value(inner))) } - TypeExpr::Nullable(inner) => { - PhpType::Union(vec![PhpType::Void, self.type_expr_to_php_type_for_value(inner)]) - } + TypeExpr::Nullable(inner) => PhpType::Union(vec![ + PhpType::Void, + self.type_expr_to_php_type_for_value(inner), + ]), TypeExpr::Union(members) => PhpType::Union( members .iter() @@ -275,7 +293,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Returns the current known PHP type for a local or `Mixed` when unknown. pub(crate) fn local_type(&self, name: &str) -> PhpType { - self.local_types.get(name).cloned().unwrap_or(PhpType::Mixed) + self.local_types + .get(name) + .cloned() + .unwrap_or(PhpType::Mixed) } /// Records a foreach loop-key local whose source is a concretely-indexed @@ -402,12 +423,9 @@ impl<'m, 'f> LoweringContext<'m, 'f> { return *slot; } let ir_type = value_ir_type(&php_type); - let slot = self.builder.add_local( - Some(name.to_string()), - ir_type, - php_type.clone(), - kind, - ); + let slot = self + .builder + .add_local(Some(name.to_string()), ir_type, php_type.clone(), kind); self.local_slots.insert(name.to_string(), slot); self.local_kinds.insert(name.to_string(), kind); self.local_types.entry(name.to_string()).or_insert(php_type); @@ -473,7 +491,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Ensures this function has a persistent eval context handle slot. pub(crate) fn declare_eval_context_local(&mut self) -> LocalSlotId { - self.declare_local_with_kind(EVAL_CONTEXT_LOCAL_NAME, PhpType::Int, LocalKind::EvalContext) + self.declare_local_with_kind( + EVAL_CONTEXT_LOCAL_NAME, + PhpType::Int, + LocalKind::EvalContext, + ) } /// Ensures this function has a persistent eval scope handle slot. @@ -526,6 +548,53 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } } + /// Enables direct eval-scope reads for selected variable names in an AOT eval body. + pub(crate) fn enable_eval_scope_access( + &mut self, + scope_param: String, + read_names: HashSet, + write_names: HashSet, + flush_names: BTreeSet, + ) { + self.eval_scope_read_param = Some(scope_param); + self.eval_scope_read_names = read_names; + self.eval_scope_write_names = write_names; + self.eval_scope_flush_names = flush_names; + } + + /// Flushes selected local slots back into the eval scope before function exit. + pub(crate) fn emit_eval_scope_finalizer(&mut self, span: Option) { + let Some(scope_param) = self.eval_scope_read_param.clone() else { + return; + }; + let names = self + .eval_scope_flush_names + .iter() + .cloned() + .collect::>(); + for name in names { + if !self.local_slots.contains_key(&name) { + continue; + } + let scope = self.load_local(&scope_param, span); + let value = self.load_local(&name, span); + let name_data = self.intern_global_name(&name); + self.emit_void( + Op::EvalScopeSet, + vec![scope.value, value.value], + Some(Immediate::GlobalName(name_data)), + Op::EvalScopeSet.default_effects(), + span, + ); + } + } + + /// Applies only the materialized local-scope part needed by EIR eval AOT. + pub(crate) fn apply_eval_scope_barrier(&mut self) { + self.eval_barrier_active = true; + self.declare_eval_scope_local(); + } + /// Ensures top-level eval fragments can see `$argc` and `$argv` by name. fn declare_eval_main_superglobals(&mut self) { if !self.in_main { @@ -547,7 +616,8 @@ impl<'m, 'f> LoweringContext<'m, 'f> { let name = format!("__eir_ref_owner{}_{}", self.hidden_temp_counter, variable); self.hidden_temp_counter += 1; let slot = self.declare_local_with_kind(&name, php_type, LocalKind::RefCell); - self.ref_cell_owner_locals.insert(variable.to_string(), slot); + self.ref_cell_owner_locals + .insert(variable.to_string(), slot); slot } @@ -609,6 +679,9 @@ impl<'m, 'f> LoweringContext<'m, 'f> { if let Some(php_type) = self.extern_global_type(name) { return self.load_extern_global(name, php_type, span); } + if self.should_load_from_eval_scope(name) { + return self.load_eval_scope_name(name, span); + } let kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, kind); // Superglobals carry a fixed `AssocArray{Str, Mixed}` type in every scope. @@ -650,6 +723,102 @@ impl<'m, 'f> LoweringContext<'m, 'f> { LoweredValue { value, ir_type } } + /// Returns true when a variable read should be sourced from the eval scope handle. + fn should_load_from_eval_scope(&self, name: &str) -> bool { + let Some(scope_param) = &self.eval_scope_read_param else { + return false; + }; + name != scope_param + && !self.local_slots.contains_key(name) + && (self.eval_scope_read_names.contains(name) + || self.eval_scope_write_names.contains(name)) + } + + /// Emits an `EvalScopeGet` for a selected eval-scope variable read. + fn load_eval_scope_name(&mut self, name: &str, span: Option) -> LoweredValue { + let scope_param = self + .eval_scope_read_param + .clone() + .expect("eval scope read mode has a scope parameter"); + let scope = self.load_local(&scope_param, span); + let name_data = self.intern_global_name(name); + let value = self + .builder + .emit_with_effects( + Op::EvalScopeGet, + vec![scope.value], + Some(Immediate::GlobalName(name_data)), + IrType::Heap(crate::ir::IrHeapKind::Mixed), + PhpType::Mixed, + Ownership::Borrowed, + Op::EvalScopeGet.default_effects(), + span, + ) + .expect("eval_scope_get produces a Mixed value"); + LoweredValue { + value, + ir_type: IrType::Heap(crate::ir::IrHeapKind::Mixed), + } + } + + /// Returns true when a variable write should be stored into the eval scope handle. + fn should_store_to_eval_scope(&self, name: &str) -> bool { + let Some(scope_param) = &self.eval_scope_read_param else { + return false; + }; + name != scope_param && self.eval_scope_write_names.contains(name) + } + + /// Emits an `EvalScopeSet` for a selected eval-scope variable write. + fn store_eval_scope_name( + &mut self, + name: &str, + value: LoweredValue, + span: Option, + ) -> LoweredValue { + let php_type = self.builder.value_php_type(value.value).codegen_repr(); + let previous_slot = self.local_slots.get(name).copied(); + let previous_kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); + let scope_param = self + .eval_scope_read_param + .clone() + .expect("eval scope write mode has a scope parameter"); + let scope = self.load_local(&scope_param, span); + let name_data = self.intern_global_name(name); + self.emit_void( + Op::EvalScopeSet, + vec![scope.value, value.value], + Some(Immediate::GlobalName(name_data)), + Op::EvalScopeSet.default_effects(), + span, + ); + let slot = self.declare_local(name, php_type.clone()); + self.builder + .widen_local_storage_type(slot, php_type.clone()); + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_some_and(|slot| self.initialized_slots.contains(&slot)) + { + self.release_stored_local_value(name, slot, span); + } + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_some_and(|slot| !self.initialized_slots.contains(&slot)) + && !self.loop_stack.is_empty() + { + self.release_stored_local_value(name, slot, span); + } + let stored = crate::ir_lower::ownership::acquire_if_refcounted(self, value, span); + self.store_slot_with_op(slot, stored, Op::StoreLocal, span); + self.set_local_type(name, php_type); + if self.value_needs_release_after_retaining_store(value) { + crate::ir_lower::ownership::release_if_owned(self, value, span); + } + stored + } + /// Emits a load using the local slot's concrete frame-storage type. /// /// This is for cleanup paths that must release the value already present in @@ -664,9 +833,14 @@ impl<'m, 'f> LoweringContext<'m, 'f> { ) -> LoweredValue { let ir_type = value_ir_type(&php_type); let ownership = Ownership::for_php_type(&php_type); - let kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); + let kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, kind); - let is_ref_bound = self.is_ref_bound_local(name) && !uses_global && kind == LocalKind::PhpLocal; + let is_ref_bound = + self.is_ref_bound_local(name) && !uses_global && kind == LocalKind::PhpLocal; let op = match (is_ref_bound, uses_global, kind) { (true, _, _) => Op::LoadRefCell, (false, true, _) => Op::LoadGlobal, @@ -695,7 +869,12 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } /// Releases the value currently stored in a local slot using frame-storage metadata. - pub(crate) fn release_stored_local_value(&mut self, name: &str, slot: LocalSlotId, span: Option) { + pub(crate) fn release_stored_local_value( + &mut self, + name: &str, + slot: LocalSlotId, + span: Option, + ) { let storage_type = self.builder.local_php_type(slot); if !Ownership::php_type_needs_lifetime_tracking(&storage_type) { return; @@ -705,7 +884,13 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } /// Emits a store to a PHP local slot, updates type facts, and returns the stored value. - pub(crate) fn store_local(&mut self, name: &str, value: LoweredValue, php_type: PhpType, span: Option) -> LoweredValue { + pub(crate) fn store_local( + &mut self, + name: &str, + value: LoweredValue, + php_type: PhpType, + span: Option, + ) -> LoweredValue { self.clear_static_callable_local(name); self.clear_reflection_class_local(name); self.clear_reflection_function_local(name); @@ -722,9 +907,16 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } return value; } + if self.should_store_to_eval_scope(name) { + return self.store_eval_scope_name(name, value, span); + } let previous_slot = self.local_slots.get(name).copied(); let previous_type = self.local_type(name); - let previous_kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); + let previous_kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, previous_kind); let php_type = if uses_global { self.global_alias_type(name) @@ -814,7 +1006,8 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } return value; } - let is_ref_bound = self.is_ref_bound_local(name) && !uses_global && previous_kind == LocalKind::PhpLocal; + let is_ref_bound = + self.is_ref_bound_local(name) && !uses_global && previous_kind == LocalKind::PhpLocal; let op = match (is_ref_bound, previous_kind) { (true, _) => Op::StoreRefCell, (false, LocalKind::StaticLocal) => Op::StoreStaticLocal, @@ -872,6 +1065,56 @@ impl<'m, 'f> LoweringContext<'m, 'f> { ) } + /// Stores a synthetic foreach initializer in the local frame without eval-scope sync. + /// + /// Fresh `foreach` key/value locals need a concrete frame slot before the first + /// iteration, but PHP must not observe that setup when the iterable is empty. + /// Runtime eval-scope writes therefore use this path for the pre-loop null seed + /// and keep normal `store_local` for values assigned inside the loop body. + pub(crate) fn store_foreach_initializer_local_only( + &mut self, + name: &str, + value: LoweredValue, + php_type: PhpType, + span: Option, + ) -> LoweredValue { + let previous_slot = self.local_slots.get(name).copied(); + let previous_kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); + let slot = self.declare_local(name, php_type.clone()); + self.builder + .widen_local_storage_type(slot, php_type.clone()); + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_some_and(|slot| self.initialized_slots.contains(&slot)) + { + self.release_stored_local_value(name, slot, span); + } + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_some_and(|slot| !self.initialized_slots.contains(&slot)) + && !self.loop_stack.is_empty() + { + self.release_stored_local_value(name, slot, span); + } + if local_kind_uses_plain_store_cleanup(previous_kind) + && previous_slot.is_none() + && !self.loop_stack.is_empty() + { + self.release_stored_local_value(name, slot, span); + } + let source = value; + let release_source_after_store = self.value_needs_release_after_retaining_store(value); + let stored = crate::ir_lower::ownership::acquire_if_refcounted(self, value, span); + self.store_slot_with_op(slot, stored, Op::StoreLocal, span); + self.set_local_type(name, php_type); + if release_source_after_store { + crate::ir_lower::ownership::release_if_owned(self, source, span); + } + stored + } + /// Returns the declared PHP type for an extern global visible as a variable. fn extern_global_type(&self, name: &str) -> Option { self.extern_globals.get(name).cloned() @@ -904,12 +1147,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } /// Emits a write to a C extern global symbol using the already-lowered source value. - fn store_extern_global_name( - &mut self, - name: &str, - value: LoweredValue, - span: Option, - ) { + fn store_extern_global_name(&mut self, name: &str, value: LoweredValue, span: Option) { let data = self.intern_global_name(name); self.builder.emit_with_effects( Op::ExternGlobalStore, @@ -938,7 +1176,11 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.clear_reflection_method_local(name); self.clear_reflection_arg_array_local(name); self.clear_fiber_start_sig(name); - let previous_kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); + let previous_kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); let uses_global = self.uses_global_storage(name, previous_kind); let slot = self.declare_local(name, php_type.clone()); if uses_global { @@ -946,8 +1188,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.set_local_type(name, php_type); return value; } - let is_ref_bound = - self.is_ref_bound_local(name) && previous_kind == LocalKind::PhpLocal; + let is_ref_bound = self.is_ref_bound_local(name) && previous_kind == LocalKind::PhpLocal; match (is_ref_bound, previous_kind) { (true, _) => self.store_ref_cell_slot(slot, value, php_type, span), (false, LocalKind::StaticLocal) => { @@ -963,7 +1204,12 @@ impl<'m, 'f> LoweringContext<'m, 'f> { } /// Emits `unset($local)`, breaking by-reference aliases without writing through them. - pub(crate) fn unset_local(&mut self, name: &str, null: LoweredValue, span: Option) -> LoweredValue { + pub(crate) fn unset_local( + &mut self, + name: &str, + null: LoweredValue, + span: Option, + ) -> LoweredValue { if !self.is_ref_bound_local(name) { return self.store_local(name, null, PhpType::Void, span); } @@ -1276,10 +1522,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { let op = self.builder.value_defining_op(value); (matches!(php_type, PhpType::Mixed | PhpType::Union(_)) || (php_type.is_refcounted() && php_type != PhpType::Str)) - && matches!( - op, - Some(Op::ArrayGet | Op::HashGet) - ) + && matches!(op, Some(Op::ArrayGet | Op::HashGet)) } /// Returns true for builtin calls whose return value is newly allocated for the caller. @@ -1301,16 +1544,16 @@ impl<'m, 'f> LoweringContext<'m, 'f> { /// Returns true when straight-line callable binding metadata is safe for a local. pub(crate) fn can_track_static_callable_local(&self, name: &str) -> bool { - let kind = self.local_kinds.get(name).copied().unwrap_or(LocalKind::PhpLocal); + let kind = self + .local_kinds + .get(name) + .copied() + .unwrap_or(LocalKind::PhpLocal); !self.uses_global_storage(name, kind) && kind == LocalKind::PhpLocal } /// Records that a PHP local currently holds a compile-time-known callable. - pub(crate) fn bind_static_callable_local( - &mut self, - name: &str, - target: StaticCallableBinding, - ) { + pub(crate) fn bind_static_callable_local(&mut self, name: &str, target: StaticCallableBinding) { if self.can_track_static_callable_local(name) { self.static_callable_locals.insert(name.to_string(), target); } @@ -1579,7 +1822,9 @@ impl<'m, 'f> LoweringContext<'m, 'f> { let ownership = Ownership::for_php_type(&php_type); let value = self .builder - .emit_with_effects(op, operands, immediate, ir_type, php_type, ownership, effects, span) + .emit_with_effects( + op, operands, immediate, ir_type, php_type, ownership, effects, span, + ) .expect("value opcode produces a value"); LoweredValue { value, ir_type } } @@ -1604,7 +1849,10 @@ impl<'m, 'f> LoweringContext<'m, 'f> { fn local_kind_uses_plain_store_cleanup(kind: LocalKind) -> bool { matches!( kind, - LocalKind::PhpLocal | LocalKind::HiddenTemp | LocalKind::OwnedTemp | LocalKind::NamedArgTemp + LocalKind::PhpLocal + | LocalKind::HiddenTemp + | LocalKind::OwnedTemp + | LocalKind::NamedArgTemp ) } @@ -1703,10 +1951,14 @@ pub(crate) fn type_expr_to_php_type(type_expr: &TypeExpr) -> PhpType { TypeExpr::Never => PhpType::Never, TypeExpr::Iterable => PhpType::Iterable, TypeExpr::Array(inner) => PhpType::Array(Box::new(type_expr_to_php_type(inner))), - TypeExpr::Ptr(name) => PhpType::Pointer(name.as_ref().map(|name| name.as_str().to_string())), + TypeExpr::Ptr(name) => { + PhpType::Pointer(name.as_ref().map(|name| name.as_str().to_string())) + } TypeExpr::Buffer(inner) => PhpType::Buffer(Box::new(type_expr_to_php_type(inner))), TypeExpr::Named(name) => named_type_expr_to_php_type(name.as_str()), - TypeExpr::Nullable(inner) => PhpType::Union(vec![PhpType::Void, type_expr_to_php_type(inner)]), + TypeExpr::Nullable(inner) => { + PhpType::Union(vec![PhpType::Void, type_expr_to_php_type(inner)]) + } TypeExpr::Union(members) => { PhpType::Union(members.iter().map(type_expr_to_php_type).collect()) } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 1c4353af57..4f12532726 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -11,8 +11,8 @@ //! conservative effects until Phase 04 gives them target-specific meaning. use crate::ir::{ - BlockId, CmpPredicate, Effects, Immediate, IrHeapKind, IrType, LocalSlotId, MixedNumericOp, Op, - Ownership, Terminator, ValueId, + BlockId, CmpPredicate, Effects, Immediate, IrHeapKind, IrType, LocalKind, LocalSlotId, + MixedNumericOp, Op, Ownership, Terminator, ValueId, }; use crate::ir_lower::context::{ value_ir_type, ClosureCapture, LoweredValue, LoweringContext, StaticCallableBinding, @@ -1186,7 +1186,9 @@ fn lower_null_coalesce( let result_type = null_coalesce_result_type(ctx, value.value, default); let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); let split_initialized = ctx.initialized_slots_snapshot(); - let default_block = ctx.builder.create_named_block("coalesce.default", Vec::new()); + let default_block = ctx + .builder + .create_named_block("coalesce.default", Vec::new()); let value_block = ctx.builder.create_named_block("coalesce.value", Vec::new()); let merge = ctx.builder.create_named_block("coalesce.merge", Vec::new()); ctx.builder.terminate(Terminator::CondBr { @@ -1301,9 +1303,15 @@ fn lower_short_ternary( let result_type = fallback_expr_type(expr); let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); let split_initialized = ctx.initialized_slots_snapshot(); - let value_block = ctx.builder.create_named_block("short_ternary.value", Vec::new()); - let default_block = ctx.builder.create_named_block("short_ternary.default", Vec::new()); - let merge = ctx.builder.create_named_block("short_ternary.merge", Vec::new()); + let value_block = ctx + .builder + .create_named_block("short_ternary.value", Vec::new()); + let default_block = ctx + .builder + .create_named_block("short_ternary.default", Vec::new()); + let merge = ctx + .builder + .create_named_block("short_ternary.merge", Vec::new()); ctx.builder.terminate(Terminator::CondBr { cond: cond.value, then_target: value_block, @@ -1520,8 +1528,7 @@ fn lower_assignment_expr( assigned_name.and_then(|_| reflection_property_binding_for_expr(ctx, value)); let reflected_method = assigned_name.and_then(|_| reflection_method_binding_for_expr(ctx, value)); - let reflected_args = - assigned_name.and_then(|_| reflection_arg_array_binding_for_expr(value)); + let reflected_args = assigned_name.and_then(|_| reflection_arg_array_binding_for_expr(value)); let fiber_start_sig = assigned_name.and_then(|_| crate::ir_lower::fibers::start_sig_for_expr(ctx, value)); let callable_array = assigned_name @@ -1927,12 +1934,392 @@ fn emit_builtin_call_value( Some(span), ); release_owned_call_arg_temporaries(ctx, &operands, Some(call.value), span); + let eval_needs_barrier = match eval_literal { + Some(fragment) => eval_literal_needs_barrier(ctx, fragment), + None => true, + }; if php_symbol_key(name.trim_start_matches('\\')) == "eval" { - ctx.apply_eval_barrier(); + if eval_needs_barrier { + ctx.apply_eval_barrier(); + } else if eval_literal + .is_some_and(|fragment| eval_literal_needs_scope_barrier(ctx, fragment)) + { + ctx.apply_eval_scope_barrier(); + } } call } +/// Returns true when a literal `eval` call may still need runtime scope/interpreter state. +fn eval_literal_needs_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { + if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { + return false; + } + if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { + return false; + } + if eval_literal_local_scalar_direct_sync_supported_by_lowering(ctx, fragment) { + return false; + } + let static_call_supported = |name: &str, args: &[Expr]| { + eval_literal_static_function_supported_by_lowering(ctx, name, args) + }; + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment, + ctx.source_path(), + static_call_supported, + |receiver, method, args| { + eval_literal_static_method_supported_by_lowering(ctx, receiver, method, args) + }, + ); + if plan.is_fully_static_no_bridge() { + return false; + } + if plan.uses_scope_read_params() + && eval_literal_scope_read_params_supported_by_lowering( + ctx, + plan.reads(), + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ) + { + return false; + } + if plan.requires_runtime_eval_scope() + && eval_literal_scope_constraints_supported_by_lowering( + ctx, + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ) + { + return false; + } + true +} + +/// Returns true when a literal `eval` only needs materialized eval-scope state. +fn eval_literal_needs_scope_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { + if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { + return false; + } + if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { + return false; + } + if eval_literal_local_scalar_direct_sync_supported_by_lowering(ctx, fragment) { + return false; + } + let static_call_supported = |name: &str, args: &[Expr]| { + eval_literal_static_function_supported_by_lowering(ctx, name, args) + }; + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment, + ctx.source_path(), + static_call_supported, + |receiver, method, args| { + eval_literal_static_method_supported_by_lowering(ctx, receiver, method, args) + }, + ); + plan.requires_runtime_eval_scope() + && eval_literal_scope_constraints_supported_by_lowering( + ctx, + plan.array_read_constraints(), + plan.assoc_array_read_constraints(), + plan.float_predicate_read_constraints(), + ) +} + +/// Returns true when a boxed read/write eval can use direct caller locals. +fn eval_literal_direct_read_write_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_direct_local_read_write_writes(fragment) + else { + return false; + }; + writes + .iter() + .all(|(name, kind)| eval_literal_direct_read_write_name_supported(ctx, name, *kind)) +} + +/// Returns true when one direct read/write target is an initialized scalar local. +fn eval_literal_direct_read_write_name_supported( + ctx: &LoweringContext<'_, '_>, + name: &str, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(slot) = ctx.local_slots.get(name) else { + return false; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + if !ctx.initialized_slots_snapshot().contains(slot) { + return false; + } + ctx.local_types + .get(name) + .is_some_and(|ty| eval_literal_direct_read_write_type_supported(ty, kind)) +} + +/// Returns true when a read/write eval result kind fits the caller local type. +fn eval_literal_direct_read_write_type_supported( + ty: &PhpType, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match ty.codegen_repr() { + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + _ => false, + } +} + +/// Returns true when local-scalar eval writes can be synced without eval scope state. +fn eval_literal_local_scalar_direct_sync_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_local_scalar_writes_with_static_calls( + fragment, + |name, args| eval_literal_static_function_supported_by_lowering(ctx, name, args), + ) else { + return false; + }; + writes + .iter() + .all(|(name, kind)| eval_literal_local_scalar_direct_sync_name_supported(ctx, name, *kind)) +} + +/// Returns true when one local-scalar write can target caller storage directly or be ignored. +fn eval_literal_local_scalar_direct_sync_name_supported( + ctx: &LoweringContext<'_, '_>, + name: &str, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(_slot) = ctx.local_slots.get(name) else { + return true; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_local_scalar_direct_sync_type_supported(ty, kind) +} + +/// Returns true when a local-scalar write kind fits the caller local type. +fn eval_literal_local_scalar_direct_sync_type_supported( + ty: &PhpType, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => true, + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + PhpType::Bool => kind == crate::eval_aot::DirectLocalStoreScalarKind::Bool, + PhpType::TaggedScalar => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + _ => false, + } +} + +/// Returns true when all scope-read variables can be passed as direct Mixed params. +fn eval_literal_scope_read_params_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + read_names: &std::collections::BTreeSet, + array_read_constraints: &std::collections::BTreeSet, + assoc_array_read_constraints: &std::collections::BTreeSet, + float_predicate_read_constraints: &std::collections::BTreeSet, +) -> bool { + read_names + .iter() + .all(|name| eval_literal_scope_read_param_supported_by_lowering(ctx, name)) + && array_read_constraints + .iter() + .all(|name| eval_literal_scope_read_array_param_supported_by_lowering(ctx, name)) + && assoc_array_read_constraints + .iter() + .all(|name| eval_literal_scope_read_assoc_array_param_supported_by_lowering(ctx, name)) + && float_predicate_read_constraints.iter().all(|name| { + eval_literal_scope_read_float_predicate_param_supported_by_lowering(ctx, name) + }) +} + +/// Returns true when all constrained scope reads fit caller local types. +fn eval_literal_scope_constraints_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + array_read_constraints: &std::collections::BTreeSet, + assoc_array_read_constraints: &std::collections::BTreeSet, + float_predicate_read_constraints: &std::collections::BTreeSet, +) -> bool { + array_read_constraints + .iter() + .all(|name| eval_literal_scope_read_array_param_supported_by_lowering(ctx, name)) + && assoc_array_read_constraints + .iter() + .all(|name| eval_literal_scope_read_assoc_array_param_supported_by_lowering(ctx, name)) + && float_predicate_read_constraints.iter().all(|name| { + eval_literal_scope_read_float_predicate_param_supported_by_lowering(ctx, name) + }) +} + +/// Returns true when one read variable has no eval runtime state dependency. +fn eval_literal_scope_read_param_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(slot) = ctx.local_slots.get(name) else { + return true; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_scope_read_param_type_supported(ty) + && ctx.initialized_slots_snapshot().contains(slot) +} + +/// Returns true when one direct read-param is statically known to be array-like. +fn eval_literal_scope_read_array_param_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(slot) = ctx.local_slots.get(name) else { + return false; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_scope_read_array_param_type_supported(ty) + && ctx.initialized_slots_snapshot().contains(slot) +} + +/// Returns true when one direct read-param is statically known to be associative-array-like. +fn eval_literal_scope_read_assoc_array_param_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(slot) = ctx.local_slots.get(name) else { + return false; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_scope_read_assoc_array_param_type_supported(ty) + && ctx.initialized_slots_snapshot().contains(slot) +} + +/// Returns true when one direct read-param can feed float predicate builtins safely. +fn eval_literal_scope_read_float_predicate_param_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + let Some(slot) = ctx.local_slots.get(name) else { + return false; + }; + if ctx.is_ref_bound_local(name) { + return false; + } + if ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { + return false; + } + let Some(ty) = ctx.local_types.get(name) else { + return false; + }; + eval_literal_scope_read_float_predicate_param_type_supported(ty) + && ctx.initialized_slots_snapshot().contains(slot) +} + +/// Returns true when a local type can be boxed to the param-mode Mixed ABI. +fn eval_literal_scope_read_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Void + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + | PhpType::Mixed + | PhpType::Union(_) + ) +} + +/// Returns true when a local type satisfies array-only direct read-param semantics. +fn eval_literal_scope_read_array_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } + ) +} + +/// Returns true when a local type satisfies associative-array direct read-param semantics. +fn eval_literal_scope_read_assoc_array_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::AssocArray { .. }) +} + +/// Returns true when a local type can reach IEEE float predicates without TypeError. +fn eval_literal_scope_read_float_predicate_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Int | PhpType::Float) +} + /// Returns the literal eval fragment when the call is a simple `eval('...')`. fn eval_literal_fragment<'a>(name: &str, args: &'a [Expr]) -> Option<&'a str> { if php_symbol_key(name.trim_start_matches('\\')) != "eval" @@ -1948,6 +2335,58 @@ fn eval_literal_fragment<'a>(name: &str, args: &'a [Expr]) -> Option<&'a str> { } } +/// Returns true when a literal-eval static function call can avoid the eval barrier. +fn eval_literal_static_function_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + name: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let key = php_symbol_key(name.trim_start_matches('\\')); + let Some(signature) = ctx + .functions + .iter() + .find(|(function_name, _)| php_symbol_key(function_name.trim_start_matches('\\')) == key) + .map(|(_, signature)| signature) + else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) +} + +/// Returns true when a literal-eval static method call can avoid the eval barrier. +fn eval_literal_static_method_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 || !matches!(receiver, StaticReceiver::Named(_)) { + return false; + } + let Some(class_name) = static_receiver_class_name(ctx, receiver) else { + return false; + }; + let method_key = php_symbol_key(method); + let Some(class_info) = ctx.classes.get(class_name.as_str()) else { + return false; + }; + if class_info + .static_method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public) + != &Visibility::Public + { + return false; + } + let Some(signature) = static_method_implementation_signature(ctx, receiver, method) else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) +} + /// Returns true when a dynamic eval fallback can preserve simple positional call semantics. fn plain_positional_call_args(args: &[Expr]) -> bool { !crate::types::call_args::has_named_args(args) @@ -2293,9 +2732,15 @@ fn lower_nullable_native_isset_offset_probe( Some(expr.span), ); let temp_name = ctx.declare_hidden_temp(PhpType::Bool); - let null_block = ctx.builder.create_named_block("isset.native.null", Vec::new()); - let probe_block = ctx.builder.create_named_block("isset.native.probe", Vec::new()); - let merge = ctx.builder.create_named_block("isset.native.merge", Vec::new()); + let null_block = ctx + .builder + .create_named_block("isset.native.null", Vec::new()); + let probe_block = ctx + .builder + .create_named_block("isset.native.probe", Vec::new()); + let merge = ctx + .builder + .create_named_block("isset.native.merge", Vec::new()); ctx.builder.terminate(Terminator::CondBr { cond: is_null.value, then_target: null_block, @@ -2899,13 +3344,12 @@ fn lower_dynamic_call_user_func_array( } let signature = callable_descriptor_signature_for_expr(ctx, callback_expr); let callback = lower_expr(ctx, callback_expr); - let arg_array = - lower_descriptor_invoker_arg_array_for_call_user_func_array( - ctx, - arg_array_expr, - signature.as_ref(), - ) - .unwrap_or_else(|| lower_expr(ctx, arg_array_expr)); + let arg_array = lower_descriptor_invoker_arg_array_for_call_user_func_array( + ctx, + arg_array_expr, + signature.as_ref(), + ) + .unwrap_or_else(|| lower_expr(ctx, arg_array_expr)); Some(emit_callable_descriptor_invoke( ctx, callback, @@ -4672,11 +5116,8 @@ fn lower_static_array_push( Op::ArrayPush.default_effects(), Some(expr.span), ); - let elem_ty = super::stmt::indexed_array_write_element_type( - ctx, - array_value, - updated_ty.as_ref(), - ); + let elem_ty = + super::stmt::indexed_array_write_element_type(ctx, array_value, updated_ty.as_ref()); super::stmt::finish_indexed_array_local_write( ctx, array_name, @@ -5147,7 +5588,10 @@ fn coerce_operands_to_params( if !matches!(operand_ty, PhpType::Int | PhpType::Bool) { continue; } - let lowered = LoweredValue { value, ir_type: IrType::I64 }; + let lowered = LoweredValue { + value, + ir_type: IrType::I64, + }; operands[index] = coerce_to_float_at_span(ctx, lowered, None).value; } operands @@ -8040,9 +8484,15 @@ fn lower_nullable_array_access( ); let result_type = PhpType::Mixed; let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); - let null_block = ctx.builder.create_named_block("nullable.index.null", Vec::new()); - let read_block = ctx.builder.create_named_block("nullable.index.read", Vec::new()); - let merge = ctx.builder.create_named_block("nullable.index.merge", Vec::new()); + let null_block = ctx + .builder + .create_named_block("nullable.index.null", Vec::new()); + let read_block = ctx + .builder + .create_named_block("nullable.index.read", Vec::new()); + let merge = ctx + .builder + .create_named_block("nullable.index.merge", Vec::new()); ctx.builder.terminate(Terminator::CondBr { cond: is_null.value, then_target: null_block, @@ -8849,7 +9299,9 @@ fn lower_closure_call(ctx: &mut LoweringContext<'_, '_>, var: &str, args: &[Expr let callable = ctx.load_local(var, Some(expr.span)); let result_type = result_type.unwrap_or_else(|| dynamic_callable_result_type(ctx, callable.value, expr)); if instance_signature.is_none() { - if let Some(arg_container) = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) { + if let Some(arg_container) = + lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) + { return emit_callable_descriptor_invoke( ctx, callable, @@ -8939,7 +9391,9 @@ fn lower_expr_call(ctx: &mut LoweringContext<'_, '_>, callee: &Expr, args: &[Exp } } let result_type = dynamic_callable_result_type(ctx, lowered_callee.value, expr); - if let Some(arg_container) = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) { + if let Some(arg_container) = + lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) + { return emit_callable_descriptor_invoke( ctx, lowered_callee, @@ -8989,14 +9443,10 @@ fn lower_expr_call_from_value( expr: &Expr, ) -> LoweredValue { let result_type = dynamic_callable_result_type(ctx, callee.value, expr); - if let Some(arg_container) = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) { - return emit_callable_descriptor_invoke( - ctx, - callee, - arg_container, - result_type, - expr.span, - ); + if let Some(arg_container) = + lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span) + { + return emit_callable_descriptor_invoke(ctx, callee, arg_container, result_type, expr.span); } let mut operands = vec![callee.value]; operands.extend(lower_args(ctx, args)); @@ -9303,7 +9753,8 @@ fn lower_first_class_callable_expr_call( .as_ref() .map(|signature| normalize_value_php_type(signature.return_type.codegen_repr())) .unwrap_or_else(|| dynamic_callable_result_type(ctx, callable.value, expr)); - let arg_container = lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span)?; + let arg_container = + lower_untyped_descriptor_invoker_arg_container(ctx, args, expr.span)?; Some(emit_callable_descriptor_invoke( ctx, callable, @@ -10187,22 +10638,14 @@ fn lower_method_call( return null_value; } if op == Op::MethodCall { - if let Some(value) = lower_reflection_function_invoke_call( - ctx, - Some(object_expr), - method, - args, - expr, - ) { + if let Some(value) = + lower_reflection_function_invoke_call(ctx, Some(object_expr), method, args, expr) + { return value; } - if let Some(value) = lower_reflection_method_invoke_call( - ctx, - Some(object_expr), - method, - args, - expr, - ) { + if let Some(value) = + lower_reflection_method_invoke_call(ctx, Some(object_expr), method, args, expr) + { return value; } } @@ -10413,9 +10856,15 @@ fn lower_nullable_regular_method_call( ) -> LoweredValue { let result_type = method_call_result_type(ctx, object.value, method, Op::MethodCall, expr); let temp_name = ctx.declare_owned_hidden_temp(result_type.clone()); - let fatal_block = ctx.builder.create_named_block("method.null.fatal", Vec::new()); - let call_block = ctx.builder.create_named_block("method.non_null.call", Vec::new()); - let merge = ctx.builder.create_named_block("method.nullable.merge", Vec::new()); + let fatal_block = ctx + .builder + .create_named_block("method.null.fatal", Vec::new()); + let call_block = ctx + .builder + .create_named_block("method.non_null.call", Vec::new()); + let merge = ctx + .builder + .create_named_block("method.nullable.merge", Vec::new()); let is_null = ctx.emit_value( Op::IsNull, vec![object.value], @@ -10777,11 +11226,19 @@ fn lower_reflection_method_invoke_call( "invokeargs" => reflection_method_invoke_args_array(ctx, args), _ => return None, }) else { - return Some(lower_reflection_method_invoke_unsupported(ctx, &method_key, expr)); + return Some(lower_reflection_method_invoke_unsupported( + ctx, + &method_key, + expr, + )); }; let Some(target_kind) = reflection_method_target_kind(ctx, &class_name, &reflected_method) else { - return Some(lower_reflection_method_invoke_unsupported(ctx, &method_key, expr)); + return Some(lower_reflection_method_invoke_unsupported( + ctx, + &method_key, + expr, + )); }; match target_kind { ReflectionMethodTargetKind::Static => Some(lower_reflection_static_method_invoke( @@ -10813,11 +11270,7 @@ fn lower_reflection_static_method_invoke( ) -> LoweredValue { let ignored_object = lower_expr(ctx, object_arg); if ctx.value_is_owning_temporary(ignored_object) { - crate::ir_lower::ownership::release_if_owned( - ctx, - ignored_object, - Some(object_arg.span), - ); + crate::ir_lower::ownership::release_if_owned(ctx, ignored_object, Some(object_arg.span)); } let receiver = StaticReceiver::Named(Name::from(class_name.to_string())); lower_static_method_call(ctx, &receiver, reflected_method, forwarded_args, expr) @@ -10838,7 +11291,13 @@ fn lower_reflection_instance_method_invoke( return null_value; } if value_is_nullable(ctx, object.value) { - return lower_nullable_regular_method_call(ctx, object, reflected_method, forwarded_args, expr); + return lower_nullable_regular_method_call( + ctx, + object, + reflected_method, + forwarded_args, + expr, + ); } lower_method_call_with_receiver( ctx, @@ -10864,7 +11323,10 @@ fn reflection_method_invoke_args(args: &[Expr]) -> Option<(Expr, Vec)> { let mut args = args.into_iter(); if let Some(first) = args.next() { match first.kind { - ExprKind::NamedArg { ref name, ref value } if php_symbol_key(name) == "object" => { + ExprKind::NamedArg { + ref name, + ref value, + } if php_symbol_key(name) == "object" => { object = Some((**value).clone()); } ExprKind::NamedArg { .. } => forwarded.push(first), @@ -10873,7 +11335,10 @@ fn reflection_method_invoke_args(args: &[Expr]) -> Option<(Expr, Vec)> { } for arg in args { match arg.kind { - ExprKind::NamedArg { ref name, ref value } if php_symbol_key(name) == "object" => { + ExprKind::NamedArg { + ref name, + ref value, + } if php_symbol_key(name) == "object" => { if object.replace((**value).clone()).is_some() { return None; } @@ -12609,7 +13074,10 @@ fn resolve_known_class_name(ctx: &LoweringContext<'_, '_>, class_name: &str) -> } /// Resolves a PHP function name case-insensitively against known user functions. -fn resolve_known_function_name(ctx: &LoweringContext<'_, '_>, function_name: &str) -> Option { +fn resolve_known_function_name( + ctx: &LoweringContext<'_, '_>, + function_name: &str, +) -> Option { let key = php_symbol_key(function_name.trim_start_matches('\\')); ctx.functions .keys() @@ -13744,8 +14212,10 @@ fn lower_yield_from_array( let body = ctx.builder.create_named_block("yieldfrom.body", Vec::new()); let exit = ctx.builder.create_named_block("yieldfrom.exit", Vec::new()); if !ctx.builder.insertion_block_is_terminated() { - ctx.builder - .terminate(Terminator::Br { target: header, args: Vec::new() }); + ctx.builder.terminate(Terminator::Br { + target: header, + args: Vec::new(), + }); } ctx.builder.position_at_end(header); @@ -13794,8 +14264,10 @@ fn lower_yield_from_array( Some(span), ); if !ctx.builder.insertion_block_is_terminated() { - ctx.builder - .terminate(Terminator::Br { target: header, args: Vec::new() }); + ctx.builder.terminate(Terminator::Br { + target: header, + args: Vec::new(), + }); } ctx.builder.position_at_end(exit); diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 55a6f10970..8235511616 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -28,7 +28,14 @@ use crate::types::{ }; /// AST parameter tuple shape used by function, method, and closure declarations. -type AstParams = [(String, Option, Option, bool)]; +type AstParams = [( + String, + Option, + Option, + bool, +)]; + +const EVAL_AOT_SCOPE_PARAM: &str = "__eir_eval_scope"; const CALLED_CLASS_ID_PARAM: &str = "__elephc_called_class_id"; @@ -74,6 +81,8 @@ pub(crate) fn lower_main( None, true, all_global_var_names, + module.source_path.clone(), + None, ); add_closures(module, closures); module.add_function(function); @@ -130,10 +139,7 @@ fn collect_global_var_names_in_body( collect_global_var_names_in_body(body, names); } crate::parser::ast::StmtKind::For { - init, - update, - body, - .. + init, update, body, .. } => { if let Some(init) = init { collect_global_var_names_in_body(std::slice::from_ref(init.as_ref()), names); @@ -190,18 +196,14 @@ pub(crate) fn lower_user_function( ) { let fallback = signature_from_ast(params, return_type); let signature = check_result.functions.get(name).unwrap_or(&fallback); - let eir_signature = eir_signature_with_php_param_contracts( - name, - signature, - &check_result.callable_param_sigs, - ); + let eir_signature = + eir_signature_with_php_param_contracts(name, signature, &check_result.callable_param_sigs); // A generator's compiled body is a coroutine that returns the value passed // to `return` (Mixed, read back via `Generator::getReturn()`), not the // `Generator` object itself. The public signature stays `Generator` for // callers; only the EIR body return type becomes Mixed so `return $x` // lowers to a plain boxed Mixed return instead of a Generator coercion. - let body_return_type = - generator_body_return_type(body, &eir_signature.return_type); + let body_return_type = generator_body_return_type(body, &eir_signature.return_type); let mut function = Function::new( name.to_string(), return_ir_type(&body_return_type), @@ -245,6 +247,8 @@ pub(crate) fn lower_user_function( None, false, std::collections::HashSet::new(), + module.source_path.clone(), + None, ); add_closures(module, closures); module.add_function(function); @@ -337,11 +341,169 @@ pub(crate) fn lower_class_method( None, false, std::collections::HashSet::new(), + module.source_path.clone(), + None, ); add_closures(module, closures); module.class_methods.push(function); } +/// Lowers one no-scope literal eval fragment as an internal EIR function. +pub(crate) fn lower_eval_aot_function( + name: &str, + body: &[Stmt], + module: &mut Module, + check_result: &CheckResult, + constants: &std::collections::HashMap, + fiber_return_sigs: &std::collections::HashMap, +) { + let return_type = PhpType::Mixed; + let signature = FunctionSig { + params: Vec::new(), + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), + defaults: Vec::new(), + return_type: return_type.clone(), + declared_return: false, + ref_params: Vec::new(), + declared_params: Vec::new(), + variadic: None, + deprecation: None, + }; + let mut function = Function::new( + name.to_string(), + return_ir_type(&return_type), + return_type.clone(), + ); + function.source_signature = Some(source_signature(name, &signature)); + function.signature = Some(eir_runtime_metadata_signature(&signature)); + let closures = lower_body_into_function( + &mut function, + &mut module.data, + body, + TypeEnv::new(), + check_result.global_env.clone(), + &check_result.functions, + &check_result.extern_functions, + &check_result.extern_globals, + &check_result.callable_param_sigs, + fiber_return_sigs, + &check_result.classes, + &check_result.enums, + &check_result.interfaces, + &check_result.packed_classes, + constants, + None, + return_type, + &[], + None, + false, + collect_global_var_names(body), + module.source_path.clone(), + None, + ); + add_closures(module, closures); + module.add_function(function); +} + +/// Lowers one literal eval fragment as an internal EIR function that reads eval scope. +pub(crate) fn lower_eval_aot_scope_read_function( + name: &str, + body: &[Stmt], + scope_reads: &std::collections::BTreeSet, + scope_direct_writes: &std::collections::BTreeSet, + scope_flush_writes: &std::collections::BTreeSet, + module: &mut Module, + check_result: &CheckResult, + constants: &std::collections::HashMap, + fiber_return_sigs: &std::collections::HashMap, +) { + let return_type = PhpType::Mixed; + let use_read_params = + !scope_reads.is_empty() && scope_direct_writes.is_empty() && scope_flush_writes.is_empty(); + let params = if use_read_params { + scope_reads + .iter() + .map(|name| (name.clone(), PhpType::Mixed)) + .collect::>() + } else { + vec![(EVAL_AOT_SCOPE_PARAM.to_string(), PhpType::Int)] + }; + let signature = FunctionSig { + params, + param_type_exprs: Vec::new(), + param_attributes: Vec::new(), + defaults: Vec::new(), + return_type: return_type.clone(), + declared_return: false, + ref_params: vec![ + false; + if use_read_params { + scope_reads.len() + } else { + 1 + } + ], + declared_params: vec![ + false; + if use_read_params { + scope_reads.len() + } else { + 1 + } + ], + variadic: None, + deprecation: None, + }; + let mut function = Function::new( + name.to_string(), + return_ir_type(&return_type), + return_type.clone(), + ); + function.params = function_params(&signature); + function.source_signature = Some(source_signature(name, &signature)); + function.signature = Some(eir_runtime_metadata_signature(&signature)); + let mut env = TypeEnv::new(); + for (param_name, param_type) in &signature.params { + env.insert(param_name.clone(), param_type.clone()); + } + let eval_scope_reads = (!use_read_params).then(|| { + ( + EVAL_AOT_SCOPE_PARAM.to_string(), + scope_reads.iter().cloned().collect(), + scope_direct_writes.iter().cloned().collect(), + scope_flush_writes.clone(), + ) + }); + let closures = lower_body_into_function( + &mut function, + &mut module.data, + body, + env, + check_result.global_env.clone(), + &check_result.functions, + &check_result.extern_functions, + &check_result.extern_globals, + &check_result.callable_param_sigs, + fiber_return_sigs, + &check_result.classes, + &check_result.enums, + &check_result.interfaces, + &check_result.packed_classes, + constants, + None, + return_type, + &signature.params, + None, + false, + collect_global_var_names(body), + module.source_path.clone(), + eval_scope_reads, + ); + add_closures(module, closures); + module.add_function(function); +} + /// Builds fallback method signature metadata from parsed class-like method syntax. pub(crate) fn method_signature_from_ast(method: &ClassMethod) -> FunctionSig { let mut signature = signature_from_ast_with_variadic( @@ -428,6 +590,8 @@ pub(crate) fn lower_property_init_thunk( None, false, std::collections::HashSet::new(), + module.source_path.clone(), + None, ); add_closures(module, closures); module.add_function(function); @@ -571,16 +735,15 @@ fn lower_closure_function_with_signature( attach_generator_source_if_needed(&mut function, body, signature.params.len()); let env = env_with_closure_captures(&signature, captures); let lowered_params = params_with_closure_captures(&signature, captures); - let recursive_binding = - self_ref_callable_capture.map(|local_name| RecursiveClosureBinding { - local_name: local_name.to_string(), - closure_name: name.to_string(), - signature: signature.clone(), - capture_names: captures - .iter() - .map(|(capture_name, _, _)| capture_name.clone()) - .collect(), - }); + let recursive_binding = self_ref_callable_capture.map(|local_name| RecursiveClosureBinding { + local_name: local_name.to_string(), + closure_name: name.to_string(), + signature: signature.clone(), + capture_names: captures + .iter() + .map(|(capture_name, _, _)| capture_name.clone()) + .collect(), + }); let closures = lower_body_into_function( &mut function, parent.data, @@ -604,6 +767,8 @@ fn lower_closure_function_with_signature( recursive_binding, false, collect_global_var_names(body), + parent.source_path().map(str::to_string), + None, ); parent.extend_closures(std::iter::once(function).chain(closures)); signature @@ -633,6 +798,13 @@ fn lower_body_into_function( recursive_closure_binding: Option, in_main: bool, all_global_var_names: std::collections::HashSet, + source_path: Option, + eval_scope_reads: Option<( + String, + std::collections::HashSet, + std::collections::HashSet, + std::collections::BTreeSet, + )>, ) -> Vec { let owner_name = function.name.clone(); let function_by_ref_return = function.flags.by_ref_return; @@ -666,8 +838,13 @@ fn lower_body_into_function( return_php_type, in_main, all_global_var_names, + source_path, ); ctx.by_ref_return = function_by_ref_return; + ctx.by_ref_return = function_by_ref_return; + if let Some((scope_param, read_names, write_names, flush_names)) = eval_scope_reads { + ctx.enable_eval_scope_access(scope_param, read_names, write_names, flush_names); + } for (index, (name, php_type)) in params.iter().enumerate() { ctx.declare_local(name, php_type.clone()); ctx.mark_local_initialized(name); @@ -760,17 +937,20 @@ fn terminate_open_block(ctx: &mut LoweringContext<'_, '_>) { return; } if matches!(ctx.return_php_type, PhpType::Never) { - let message = - ctx.intern_string("Fatal error: A never-returning function must not implicitly return\n"); + let message = ctx + .intern_string("Fatal error: A never-returning function must not implicitly return\n"); ctx.builder.terminate(Terminator::Fatal { message }); return; } if ctx.return_type == IrType::Void { + ctx.emit_eval_scope_finalizer(None); ctx.builder.terminate(Terminator::Return { value: None }); return; } + ctx.emit_eval_scope_finalizer(None); let value = emit_default_return_value(ctx); - ctx.builder.terminate(Terminator::Return { value: Some(value) }); + ctx.builder + .terminate(Terminator::Return { value: Some(value) }); } /// Emits a placeholder value compatible with the function return storage type. @@ -898,11 +1078,16 @@ pub(crate) fn eir_signature_with_php_param_contracts( let mut eir_signature = signature.clone(); let mut has_dynamic_untyped_param = false; for (index, (name, php_type)) in eir_signature.params.iter_mut().enumerate() { - let declared = signature.declared_params.get(index).copied().unwrap_or(false); + let declared = signature + .declared_params + .get(index) + .copied() + .unwrap_or(false); let by_ref = signature.ref_params.get(index).copied().unwrap_or(false); let variadic = signature.variadic.as_deref() == Some(name.as_str()); if !declared && !by_ref && !variadic { - if preserve_untyped_eir_param_contract(owner_name, name, php_type, callable_param_sigs) { + if preserve_untyped_eir_param_contract(owner_name, name, php_type, callable_param_sigs) + { continue; } *php_type = PhpType::Mixed; @@ -1151,10 +1336,7 @@ fn stmt_contains_value_return(stmt: &Stmt) -> bool { | StmtKind::IncludeOnceGuard { body, .. } | StmtKind::Synthetic(body) => body_contains_value_return(body), StmtKind::For { - init, - update, - body, - .. + init, update, body, .. } => { init.as_ref() .is_some_and(|stmt| stmt_contains_value_return(stmt.as_ref())) @@ -1201,7 +1383,9 @@ fn signature_from_ast_with_variadic( .map(|(name, ty, _, _)| { ( name.clone(), - ty.as_ref().map(type_expr_to_php_type).unwrap_or(PhpType::Mixed), + ty.as_ref() + .map(type_expr_to_php_type) + .unwrap_or(PhpType::Mixed), ) }) .collect(), @@ -1210,7 +1394,10 @@ fn signature_from_ast_with_variadic( .map(|(_, type_ann, _, _)| type_ann.clone()) .collect(), param_attributes: Vec::new(), - defaults: params.iter().map(|(_, _, default, _)| default.clone()).collect(), + defaults: params + .iter() + .map(|(_, _, default, _)| default.clone()) + .collect(), return_type: return_type .map(type_expr_to_php_type) .unwrap_or(PhpType::Mixed), diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 4e5e0fd5fc..c0c36f4cc8 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -9,19 +9,21 @@ //! statements themselves are no-ops inside `main`. //! - The module is validated before it is returned to CLI/test callers. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::path::Path; use crate::codegen::platform::Target; use crate::codegen::RuntimeFeatures; use crate::intrinsics::IntrinsicCall; use crate::ir::{ - validate_module, ExternDecl, ExternParamDecl, Function, Immediate, IrType, Module, Op, - TraitMethodInfo, + validate_module, ExternDecl, ExternParamDecl, Function, Immediate, IrType, LocalKind, Module, + Op, TraitMethodInfo, }; use crate::ir_lower::{builtin_datetime, function, LoweringError}; use crate::names::php_symbol_key; -use crate::parser::ast::{ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind, Visibility}; +use crate::parser::ast::{ + ClassMethod, Expr, ExprKind, Program, StaticReceiver, Stmt, StmtKind, Visibility, +}; use crate::types::{CheckResult, ClassInfo, FunctionSig, InterfaceInfo, PhpType}; /// Lowers an optimized typed AST program into a validated EIR module. @@ -37,11 +39,29 @@ pub(crate) fn lower( module.global_constants = constants.clone(); let fiber_return_sigs = crate::ir_lower::fibers::collect_fiber_return_sigs(program); populate_metadata(&mut module, program, check_result); - lower_function_declarations(program, &mut module, check_result, &constants, &fiber_return_sigs); - lower_class_like_methods(program, &mut module, check_result, &constants, &fiber_return_sigs); + lower_function_declarations( + program, + &mut module, + check_result, + &constants, + &fiber_return_sigs, + ); + lower_class_like_methods( + program, + &mut module, + check_result, + &constants, + &fiber_return_sigs, + ); lower_property_init_thunks(&mut module, check_result, &constants, &fiber_return_sigs); lower_builtin_reflection_methods(&mut module, check_result, &constants, &fiber_return_sigs); - function::lower_main(program, &mut module, check_result, &constants, &fiber_return_sigs); + function::lower_main( + program, + &mut module, + check_result, + &constants, + &fiber_return_sigs, + ); lower_referenced_builtin_spl_methods(&mut module, check_result, &constants, &fiber_return_sigs); builtin_datetime::lower_referenced_builtin_datetime_methods( &mut module, @@ -49,6 +69,7 @@ pub(crate) fn lower( &constants, &fiber_return_sigs, ); + lower_literal_eval_aot_functions(&mut module, check_result, &constants, &fiber_return_sigs); include_lowered_runtime_features(&mut module); validate_module(&module)?; Ok(module) @@ -195,7 +216,11 @@ fn dynamic_untyped_param_names( ) -> HashSet { let mut names = HashSet::new(); for (index, (name, php_type)) in signature.params.iter().enumerate() { - let declared = signature.declared_params.get(index).copied().unwrap_or(false); + let declared = signature + .declared_params + .get(index) + .copied() + .unwrap_or(false); let by_ref = signature.ref_params.get(index).copied().unwrap_or(false); let variadic = signature.variadic.as_deref() == Some(name.as_str()); let preserved = matches!(php_type.codegen_repr(), PhpType::Callable) @@ -249,10 +274,7 @@ fn stmt_returns_dynamic_param(stmt: &Stmt, dynamic_params: &HashSet) -> | StmtKind::IncludeOnceGuard { body, .. } | StmtKind::Synthetic(body) => body_returns_dynamic_param(body, dynamic_params), StmtKind::For { - init, - update, - body, - .. + init, update, body, .. } => { init.as_ref() .is_some_and(|stmt| stmt_returns_dynamic_param(stmt.as_ref(), dynamic_params)) @@ -290,8 +312,7 @@ fn stmt_returns_dynamic_param(stmt: &Stmt, dynamic_params: &HashSet) -> fn expr_exposes_dynamic_param(expr: &Expr, dynamic_params: &HashSet) -> bool { match &expr.kind { ExprKind::Variable(name) => dynamic_params.contains(name), - ExprKind::NullCoalesce { value, default } - | ExprKind::ShortTernary { value, default } => { + ExprKind::NullCoalesce { value, default } | ExprKind::ShortTernary { value, default } => { expr_exposes_dynamic_param(value, dynamic_params) || expr_exposes_dynamic_param(default, dynamic_params) } @@ -321,14 +342,18 @@ fn include_lowered_runtime_features(module: &mut Module) { module.required_runtime_features.regex |= features.regex; module.required_runtime_features.phar_archive |= features.phar_archive; module.required_runtime_features.descriptor_invoker |= features.descriptor_invoker; - module.required_runtime_features.eval |= features.eval; + module.required_runtime_features.eval_bridge |= features.eval_bridge; + module.required_runtime_features.eval_scope |= features.eval_scope; } /// Derives optional runtime features from the actual EIR instruction stream. fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { let mut features = RuntimeFeatures::none(); for function in all_lowered_functions(module) { - for inst in &function.instructions { + if function_contains_eval_state(function) { + features.eval_scope = true; + } + for (inst_index, inst) in function.instructions.iter().enumerate() { match inst.op { Op::BuiltinCall => { if builtin_call_requires_regex(module, inst) { @@ -341,18 +366,25 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { features.descriptor_invoker = true; } if builtin_call_requires_eval(module, inst) { - features.eval = true; + features.eval_bridge = true; } } - Op::EvalLiteralCall - | Op::EvalFunctionCall + Op::EvalLiteralCall => { + if eval_literal_call_requires_bridge(module, function, inst_index, inst) { + features.eval_bridge = true; + } + } + Op::EvalScopeGet | Op::EvalScopeSet => { + features.eval_scope = true; + } + Op::EvalFunctionCall | Op::EvalFunctionCallArray | Op::EvalFunctionExists | Op::EvalClassExists | Op::EvalConstantExists | Op::EvalConstantFetch | Op::EvalStaticMethodCall => { - features.eval = true; + features.eval_bridge = true; } Op::ExprCall | Op::CallableDescriptorInvoke => { features.descriptor_invoker = true; @@ -364,6 +396,527 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { features } +/// Returns true when a lowered function owns hidden eval scope/context state. +fn function_contains_eval_state(function: &Function) -> bool { + function.locals.iter().any(|local| { + matches!( + local.kind, + LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + ) + }) +} + +/// Returns true when a literal eval call still needs the magician bridge runtime. +fn eval_literal_call_requires_bridge( + module: &Module, + function: &Function, + inst_index: usize, + inst: &crate::ir::Instruction, +) -> bool { + let Some(Immediate::Data(data)) = inst.immediate else { + return true; + }; + let Some(fragment) = module.data.strings.get(data.as_raw() as usize) else { + return true; + }; + if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { + return false; + } + if eval_literal_call_can_use_direct_read_write(function, inst_index, fragment) { + return false; + } + if eval_literal_call_can_use_local_scalar_direct_sync(module, function, fragment) { + return false; + } + let plan = crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + fragment, + module.source_path.as_deref(), + |name, args| eval_literal_static_function_supported_by_module(module, name, args), + |receiver, method, args| { + eval_literal_static_method_supported_by_module(module, receiver, method, args) + }, + ); + if plan.uses_scope_read_params() { + return !eval_literal_call_can_use_scope_read_params(module, function, inst_index, &plan); + } + if plan.requires_runtime_eval_scope() + && !eval_literal_call_scope_constraints_supported(module, function, inst_index, &plan) + { + return true; + } + plan.requires_runtime_eval_bridge() +} + +/// Returns true when a boxed read/write eval can read and update caller locals directly. +fn eval_literal_call_can_use_direct_read_write( + function: &Function, + inst_index: usize, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_direct_local_read_write_writes(fragment) + else { + return false; + }; + writes.iter().all(|(name, kind)| { + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return false; + }; + eval_direct_read_write_slot_initialized(function, slot.id, inst_index) + && eval_direct_read_write_kind_supported(function, slot, inst_index, *kind) + }) +} + +/// Returns true when a local slot is initialized before the eval instruction. +fn eval_direct_read_write_slot_initialized( + function: &Function, + slot: crate::ir::LocalSlotId, + inst_index: usize, +) -> bool { + if function + .params + .get(slot.as_raw() as usize) + .is_some_and(|param| !param.by_ref) + { + return true; + } + function + .instructions + .iter() + .take(inst_index) + .any(|inst| inst.op == Op::StoreLocal && inst.immediate == Some(Immediate::LocalSlot(slot))) +} + +/// Returns true when a direct read/write value kind fits the caller slot type. +fn eval_direct_read_write_kind_supported( + function: &Function, + slot: &crate::ir::LocalSlot, + inst_index: usize, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match slot.php_type.codegen_repr() { + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + PhpType::Mixed | PhpType::Union(_) => { + kind == crate::eval_aot::DirectLocalStoreScalarKind::Float + && eval_direct_read_write_previous_store_type(function, slot.id, inst_index) + .is_some_and(|ty| matches!(ty.codegen_repr(), PhpType::Int | PhpType::Float)) + } + _ => false, + } +} + +/// Returns the source type of the latest direct local store before an eval instruction. +fn eval_direct_read_write_previous_store_type( + function: &Function, + slot: crate::ir::LocalSlotId, + inst_index: usize, +) -> Option { + function + .instructions + .iter() + .take(inst_index) + .rev() + .find(|inst| { + inst.op == Op::StoreLocal && inst.immediate == Some(Immediate::LocalSlot(slot)) + }) + .and_then(|inst| inst.operands.first().copied()) + .and_then(|value| function.value(value)) + .map(|value| value.php_type.codegen_repr()) +} + +/// Returns true when a read-only eval call can pass direct Mixed params safely. +fn eval_literal_call_can_use_scope_read_params( + module: &Module, + function: &Function, + inst_index: usize, + plan: &crate::eval_aot::EvalAotPlan, +) -> bool { + plan.reads().iter().all(|name| { + eval_literal_call_scope_read_param_supported(module, function, inst_index, name) + }) && plan.array_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_array_param_supported(module, function, inst_index, name) + }) && plan.assoc_array_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_assoc_array_param_supported(module, function, inst_index, name) + }) && plan.float_predicate_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_float_predicate_param_supported( + module, function, inst_index, name, + ) + }) +} + +/// Returns true when scope-based eval AOT satisfies caller-side type constraints. +fn eval_literal_call_scope_constraints_supported( + module: &Module, + function: &Function, + inst_index: usize, + plan: &crate::eval_aot::EvalAotPlan, +) -> bool { + plan.array_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_array_param_supported(module, function, inst_index, name) + }) && plan.assoc_array_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_assoc_array_param_supported(module, function, inst_index, name) + }) && plan.float_predicate_read_constraints().iter().all(|name| { + eval_literal_call_scope_read_float_predicate_param_supported( + module, function, inst_index, name, + ) + }) +} + +/// Returns true when one caller read can be boxed or represented as undefined null. +fn eval_literal_call_scope_read_param_supported( + _module: &Module, + function: &Function, + inst_index: usize, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) { + return false; + } + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return true; + }; + eval_scope_read_param_type_supported(&slot.php_type) + && eval_direct_read_write_slot_initialized(function, slot.id, inst_index) +} + +/// Returns true when one caller read is initialized with an array-compatible type. +fn eval_literal_call_scope_read_array_param_supported( + _module: &Module, + function: &Function, + inst_index: usize, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) { + return false; + } + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return false; + }; + eval_scope_read_array_param_type_supported(&slot.php_type) + && eval_direct_read_write_slot_initialized(function, slot.id, inst_index) +} + +/// Returns true when one caller read is initialized with an associative-array type. +fn eval_literal_call_scope_read_assoc_array_param_supported( + _module: &Module, + function: &Function, + inst_index: usize, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) { + return false; + } + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return false; + }; + eval_scope_read_assoc_array_param_type_supported(&slot.php_type) + && eval_direct_read_write_slot_initialized(function, slot.id, inst_index) +} + +/// Returns true when one caller read can feed IEEE float predicates safely. +fn eval_literal_call_scope_read_float_predicate_param_supported( + _module: &Module, + function: &Function, + inst_index: usize, + name: &str, +) -> bool { + if crate::superglobals::is_superglobal(name) { + return false; + } + let Some(slot) = eval_local_scalar_direct_sync_slot(function, name) else { + return false; + }; + eval_scope_read_float_predicate_param_type_supported(&slot.php_type) + && eval_direct_read_write_slot_initialized(function, slot.id, inst_index) +} + +/// Returns true when a caller local can be boxed into a direct eval read param. +fn eval_scope_read_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int + | PhpType::Bool + | PhpType::Float + | PhpType::Str + | PhpType::Void + | PhpType::Array(_) + | PhpType::AssocArray { .. } + | PhpType::Object(_) + | PhpType::Mixed + | PhpType::Union(_) + ) +} + +/// Returns true when a caller local satisfies array-only read-param semantics. +fn eval_scope_read_array_param_type_supported(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Array(_) | PhpType::AssocArray { .. } + ) +} + +/// Returns true when a caller local satisfies associative-array-only semantics. +fn eval_scope_read_assoc_array_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::AssocArray { .. }) +} + +/// Returns true when a caller local can feed IEEE float predicates without TypeError. +fn eval_scope_read_float_predicate_param_type_supported(ty: &PhpType) -> bool { + matches!(ty.codegen_repr(), PhpType::Int | PhpType::Float) +} + +/// Returns true when the legacy local-scalar AOT path can avoid eval scope runtime. +fn eval_literal_call_can_use_local_scalar_direct_sync( + module: &Module, + function: &Function, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_local_scalar_writes_with_static_calls( + fragment, + |name, args| eval_literal_static_function_supported_by_module(module, name, args), + ) else { + return false; + }; + writes.iter().all(|(name, kind)| { + eval_local_scalar_direct_sync_slot(function, name) + .is_none_or(|slot| eval_local_scalar_direct_sync_kind_supported(&slot.php_type, *kind)) + }) +} + +/// Returns the caller local slot that would receive a direct local-scalar eval write. +fn eval_local_scalar_direct_sync_slot<'a>( + function: &'a Function, + name: &str, +) -> Option<&'a crate::ir::LocalSlot> { + function + .locals + .iter() + .find(|local| local.name.as_deref() == Some(name) && local.kind == LocalKind::PhpLocal) +} + +/// Returns true when a local-scalar value can be stored in the caller slot type. +fn eval_local_scalar_direct_sync_kind_supported( + target_ty: &PhpType, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match target_ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => true, + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + PhpType::Bool => kind == crate::eval_aot::DirectLocalStoreScalarKind::Bool, + PhpType::TaggedScalar => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + _ => false, + } +} + +/// Returns true when a static function call matches the codegen-supported subset. +fn eval_literal_static_function_supported_by_module( + module: &Module, + name: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let key = php_symbol_key(name.trim_start_matches('\\')); + let Some(function) = module + .functions + .iter() + .find(|function| php_symbol_key(function.name.trim_start_matches('\\')) == key) + else { + return false; + }; + let Some(signature) = &function.signature else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) +} + +/// Returns true when a static method call matches the codegen-supported subset. +fn eval_literal_static_method_supported_by_module( + module: &Module, + receiver: &StaticReceiver, + method: &str, + args: &[Expr], +) -> bool { + if args.len() > 6 { + return false; + } + let StaticReceiver::Named(class_name) = receiver else { + return false; + }; + let class_name = class_name.as_str().trim_start_matches('\\'); + let method_key = php_symbol_key(method); + let Some(receiver_info) = module.class_infos.get(class_name) else { + return false; + }; + if receiver_info + .static_method_visibilities + .get(&method_key) + .unwrap_or(&Visibility::Public) + != &Visibility::Public + { + return false; + } + let impl_class = receiver_info + .static_method_impl_classes + .get(&method_key) + .map(String::as_str) + .unwrap_or(class_name); + let Some(signature) = module + .class_infos + .get(impl_class) + .and_then(|class_info| class_info.static_methods.get(&method_key)) + else { + return false; + }; + crate::eval_aot::static_function_signature_supported(signature, args) +} + +/// Adds internal EIR functions for literal eval fragments accepted by the EIR AOT subset. +fn lower_literal_eval_aot_functions( + module: &mut Module, + check_result: &CheckResult, + constants: &std::collections::HashMap, + fiber_return_sigs: &std::collections::HashMap, +) { + let candidates = collect_literal_eval_aot_function_candidates(module); + let mut lowered_names = all_lowered_functions(module) + .map(|function| function.name.clone()) + .collect::>(); + for (name, body) in candidates { + match body { + EvalAotFunctionCandidate::NoScope { body } => { + if !lowered_names.insert(name.clone()) { + continue; + } + function::lower_eval_aot_function( + &name, + &body, + module, + check_result, + constants, + fiber_return_sigs, + ); + } + EvalAotFunctionCandidate::ScopeRead { + body, + reads, + direct_writes, + flush_writes, + } => { + if !lowered_names.insert(name.clone()) { + continue; + } + function::lower_eval_aot_scope_read_function( + &name, + &body, + &reads, + &direct_writes, + &flush_writes, + module, + check_result, + constants, + fiber_return_sigs, + ); + } + } + } +} + +/// Candidate shape for an internal eval AOT EIR function. +enum EvalAotFunctionCandidate { + NoScope { + body: Program, + }, + ScopeRead { + body: Program, + reads: BTreeSet, + direct_writes: BTreeSet, + flush_writes: BTreeSet, + }, +} + +/// Collects unique literal eval fragments that can be emitted as no-scope EIR functions. +fn collect_literal_eval_aot_function_candidates( + module: &Module, +) -> Vec<(String, EvalAotFunctionCandidate)> { + let mut candidates = Vec::new(); + let mut seen = HashSet::new(); + for function in all_lowered_functions(module) { + for (inst_index, inst) in function.instructions.iter().enumerate() { + let Some(fragment) = eval_literal_fragment_from_inst(module, inst) else { + continue; + }; + let mut plan = + crate::eval_aot::plan_literal_fragment_with_source_path_and_static_and_method_calls( + &fragment, + module.source_path.as_deref(), + |name, args| { + eval_literal_static_function_supported_by_module(module, name, args) + }, + |receiver, method, args| { + eval_literal_static_method_supported_by_module( + module, receiver, method, args, + ) + }, + ); + if let Some(name) = plan.take_function_name() { + if !seen.insert(name.clone()) { + continue; + } + let Some(program) = plan.take_eir_program() else { + continue; + }; + candidates.push((name, EvalAotFunctionCandidate::NoScope { body: program })); + continue; + } + if crate::eval_aot::literal_fragment_direct_local_store_writes(&fragment).is_some() + || eval_literal_call_can_use_direct_read_write(function, inst_index, &fragment) + || eval_literal_call_can_use_local_scalar_direct_sync(module, function, &fragment) + { + continue; + } + let Some(name) = plan.take_scope_read_function_name() else { + continue; + }; + if !seen.insert(name.clone()) { + continue; + }; + let reads = plan.reads().clone(); + let direct_writes = plan.direct_writes().clone(); + let flush_writes = plan.flush_writes().clone(); + let Some(program) = plan.take_scope_read_eir_program() else { + continue; + }; + candidates.push(( + name, + EvalAotFunctionCandidate::ScopeRead { + body: program, + reads, + direct_writes, + flush_writes, + }, + )); + } + } + candidates +} + +/// Returns the string payload from an `EvalLiteralCall` instruction. +fn eval_literal_fragment_from_inst( + module: &Module, + inst: &crate::ir::Instruction, +) -> Option { + if inst.op != Op::EvalLiteralCall { + return None; + } + let Some(Immediate::Data(data)) = inst.immediate else { + return None; + }; + module.data.strings.get(data.as_raw() as usize).cloned() +} + /// Iterates every function-like body already materialized into the EIR module. fn all_lowered_functions(module: &Module) -> impl Iterator { module @@ -932,7 +1485,13 @@ fn lower_function_declarations( StmtKind::NamespaceBlock { body, .. } | StmtKind::Synthetic(body) | StmtKind::IncludeOnceGuard { body, .. } => { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::If { then_body, @@ -940,12 +1499,30 @@ fn lower_function_declarations( else_body, .. } => { - lower_function_declarations(then_body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + then_body, + module, + check_result, + constants, + fiber_return_sigs, + ); for (_, body) in elseif_clauses { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = else_body { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::IfDef { @@ -953,23 +1530,53 @@ fn lower_function_declarations( else_body, .. } => { - lower_function_declarations(then_body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + then_body, + module, + check_result, + constants, + fiber_return_sigs, + ); if let Some(body) = else_body { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::While { body, .. } | StmtKind::DoWhile { body, .. } | StmtKind::For { body, .. } | StmtKind::Foreach { body, .. } => { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::Switch { cases, default, .. } => { for (_, body) in cases { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = default { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::Try { @@ -977,12 +1584,30 @@ fn lower_function_declarations( catches, finally_body, } => { - lower_function_declarations(try_body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + try_body, + module, + check_result, + constants, + fiber_return_sigs, + ); for catch in catches { - lower_function_declarations(&catch.body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + &catch.body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = finally_body { - lower_function_declarations(body, module, check_result, constants, fiber_return_sigs); + lower_function_declarations( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } _ => {} @@ -1006,11 +1631,25 @@ fn lower_class_like_methods( .get(name) .map(|class_info| class_info.method_decls.as_slice()) .unwrap_or(methods.as_slice()); - lower_methods_for_class_like(name, methods, module, check_result, constants, fiber_return_sigs); + lower_methods_for_class_like( + name, + methods, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::TraitDecl { .. } => {} StmtKind::InterfaceDecl { name, methods, .. } => { - lower_methods_for_class_like(name, methods, module, check_result, constants, fiber_return_sigs); + lower_methods_for_class_like( + name, + methods, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::EnumDecl { name, methods, .. } => { // Enum methods are lowered like class methods on the case singleton; prefer the @@ -1020,7 +1659,14 @@ fn lower_class_like_methods( .get(name) .map(|class_info| class_info.method_decls.as_slice()) .unwrap_or(methods.as_slice()); - lower_methods_for_class_like(name, methods, module, check_result, constants, fiber_return_sigs); + lower_methods_for_class_like( + name, + methods, + module, + check_result, + constants, + fiber_return_sigs, + ); } StmtKind::NamespaceBlock { body, .. } | StmtKind::Synthetic(body) @@ -1033,12 +1679,30 @@ fn lower_class_like_methods( else_body, .. } => { - lower_class_like_methods(then_body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + then_body, + module, + check_result, + constants, + fiber_return_sigs, + ); for (_, body) in elseif_clauses { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = else_body { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::IfDef { @@ -1046,9 +1710,21 @@ fn lower_class_like_methods( else_body, .. } => { - lower_class_like_methods(then_body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + then_body, + module, + check_result, + constants, + fiber_return_sigs, + ); if let Some(body) = else_body { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::While { body, .. } @@ -1059,10 +1735,22 @@ fn lower_class_like_methods( } StmtKind::Switch { cases, default, .. } => { for (_, body) in cases { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = default { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } StmtKind::Try { @@ -1070,12 +1758,30 @@ fn lower_class_like_methods( catches, finally_body, } => { - lower_class_like_methods(try_body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + try_body, + module, + check_result, + constants, + fiber_return_sigs, + ); for catch in catches { - lower_class_like_methods(&catch.body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + &catch.body, + module, + check_result, + constants, + fiber_return_sigs, + ); } if let Some(body) = finally_body { - lower_class_like_methods(body, module, check_result, constants, fiber_return_sigs); + lower_class_like_methods( + body, + module, + check_result, + constants, + fiber_return_sigs, + ); } } _ => {} @@ -1138,7 +1844,13 @@ fn lower_builtin_reflection_methods( "ReflectionUnionType", "ReflectionIntersectionType", ] { - lower_builtin_reflection_class_methods(class_name, module, check_result, constants, fiber_return_sigs); + lower_builtin_reflection_class_methods( + class_name, + module, + check_result, + constants, + fiber_return_sigs, + ); } } @@ -1162,10 +1874,11 @@ fn lower_builtin_reflection_class_methods( let method_key = crate::names::php_symbol_key(&method.name); let body = if class_name == "ReflectionAttribute" && method_key == "newinstance" { let function_attrs = function_attribute_sources(module); - generated_body = crate::codegen::reflection::build_attribute_new_instance_body_with_extra( - &check_result.classes, - &function_attrs, - ); + generated_body = + crate::codegen::reflection::build_attribute_new_instance_body_with_extra( + &check_result.classes, + &function_attrs, + ); generated_body.as_slice() } else if class_name == "ReflectionAttribute" && method_key == "getarguments" { // Materialize captured attribute arguments through the normal array @@ -1236,7 +1949,14 @@ fn lower_referenced_builtin_spl_methods( let before = module.class_methods.len(); for (class_name, method_key) in methods { - lower_builtin_spl_method(&class_name, &method_key, module, check_result, constants, fiber_return_sigs); + lower_builtin_spl_method( + &class_name, + &method_key, + module, + check_result, + constants, + fiber_return_sigs, + ); } for method in module.class_methods.iter_mut().skip(before) { method.flags.is_synthetic = true; @@ -1575,7 +2295,11 @@ fn push_builtin_spl_interface_metadata_methods( return; }; let mut seen = HashSet::new(); - let mut stack = class_info.interfaces.iter().map(String::as_str).collect::>(); + let mut stack = class_info + .interfaces + .iter() + .map(String::as_str) + .collect::>(); while let Some(interface_name) = stack.pop() { if !seen.insert(interface_name.to_string()) { continue; @@ -1590,12 +2314,7 @@ fn push_builtin_spl_interface_metadata_methods( continue; } } - push_supported_builtin_spl_method_for_receiver( - methods, - module, - class_name, - method_key, - ); + push_supported_builtin_spl_method_for_receiver(methods, module, class_name, method_key); } stack.extend(interface_info.parents.iter().map(String::as_str)); } @@ -1874,18 +2593,14 @@ fn is_supported_builtin_spl_method(class_name: &str, method_key: &str) -> bool { "__construct" | "current" | "key" | "getflags" | "setflags" ), "GlobIterator" => matches!(method_key, "__construct" | "count" | "setflags"), - "RecursiveDirectoryIterator" => matches!( - method_key, - "__construct" | "haschildren" | "getchildren" - ), + "RecursiveDirectoryIterator" => { + matches!(method_key, "__construct" | "haschildren" | "getchildren") + } "RecursiveCachingIterator" => matches!( method_key, "__construct" | "haschildren" | "getchildren" | "__elephcassumerecursiveiterator" ), - "EmptyIterator" => matches!( - method_key, - "current" | "key" | "next" | "rewind" | "valid" - ), + "EmptyIterator" => matches!(method_key, "current" | "key" | "next" | "rewind" | "valid"), "ArrayIterator" => matches!( method_key, "__construct" @@ -2109,12 +2824,7 @@ fn is_supported_builtin_spl_method(class_name: &str, method_key: &str) -> bool { ), "IteratorIterator" => matches!( method_key, - "current" - | "key" - | "next" - | "rewind" - | "valid" - | "getinneriterator" + "current" | "key" | "next" | "rewind" | "valid" | "getinneriterator" ), "LimitIterator" => matches!( method_key, @@ -2256,7 +2966,11 @@ fn is_supported_builtin_spl_method(class_name: &str, method_key: &str) -> bool { } /// Returns true when this SPL method is implemented by an intrinsic runtime wrapper. -fn runtime_intrinsic_method_has_wrapper(class_name: &str, method_key: &str, is_static: bool) -> bool { +fn runtime_intrinsic_method_has_wrapper( + class_name: &str, + method_key: &str, + is_static: bool, +) -> bool { let intrinsic = if is_static { IntrinsicCall::static_method(class_name, method_key) } else { diff --git a/src/ir_lower/stmt/mod.rs b/src/ir_lower/stmt/mod.rs index 60c8a956f9..02383306ee 100644 --- a/src/ir_lower/stmt/mod.rs +++ b/src/ir_lower/stmt/mod.rs @@ -15,17 +15,17 @@ use crate::ir::{ BlockId, CmpPredicate, Immediate, IrType, LocalKind, LocalSlotId, Op, Ownership, SwitchCase, Terminator, }; -use crate::ir_lower::context::{FinallyFrame, LoopCleanup, LoopFrame, LoweredValue, LoweringContext}; +use crate::ir_lower::context::{ + FinallyFrame, LoopCleanup, LoopFrame, LoweredValue, LoweringContext, +}; use crate::ir_lower::effects_lookup; use crate::ir_lower::expr::{ array_access_element_result_type, coerce_to_int_at_span, lower_callable_array_for_assignment, lower_array_literal_with_expected_type, lower_closure_for_assignment, lower_expr, reflection_arg_array_binding_for_expr, reflection_class_binding_for_expr, reflection_function_binding_for_expr, reflection_method_binding_for_expr, - reflection_property_binding_for_expr, - static_callable_binding_for_expr, - string_op_uses_scratch_storage, - type_satisfies_array_access_for_ir, + reflection_property_binding_for_expr, static_callable_binding_for_expr, + string_op_uses_scratch_storage, type_satisfies_array_access_for_ir, }; use crate::names::{php_symbol_key, property_hook_set_method}; use crate::parser::ast::{ @@ -49,7 +49,14 @@ pub(crate) fn lower_stmt(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { then_body, elseif_clauses, else_body, - } => lower_if(ctx, condition, then_body, elseif_clauses, else_body.as_deref(), stmt.span), + } => lower_if( + ctx, + condition, + then_body, + elseif_clauses, + else_body.as_deref(), + stmt.span, + ), StmtKind::IfDef { symbol, then_body, @@ -62,8 +69,18 @@ pub(crate) fn lower_stmt(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { condition, update, body, - } => lower_for(ctx, init.as_deref(), condition.as_ref(), update.as_deref(), body), - StmtKind::ArrayAssign { array, index, value } => { + } => lower_for( + ctx, + init.as_deref(), + condition.as_ref(), + update.as_deref(), + body, + ), + StmtKind::ArrayAssign { + array, + index, + value, + } => { lower_array_assign(ctx, array, index, value, stmt.span); } StmtKind::NestedArrayAssign { target, value } => { @@ -81,7 +98,14 @@ pub(crate) fn lower_stmt(ctx: &mut LoweringContext<'_, '_>, stmt: &Stmt) { value_var, value_by_ref, body, - } => lower_foreach(ctx, array, key_var.as_deref(), value_var, *value_by_ref, body), + } => lower_foreach( + ctx, + array, + key_var.as_deref(), + value_var, + *value_by_ref, + body, + ), StmtKind::Switch { subject, cases, @@ -381,8 +405,15 @@ fn lower_if( span: Span, ) { let merge = ctx.builder.create_named_block("if.merge", Vec::new()); - let merge_reachable = - lower_if_chain(ctx, condition, then_body, elseif_clauses, else_body, merge, span); + let merge_reachable = lower_if_chain( + ctx, + condition, + then_body, + elseif_clauses, + else_body, + merge, + span, + ); ctx.builder.position_at_end(merge); if !merge_reachable { ctx.builder.terminate(Terminator::Unreachable); @@ -427,25 +458,26 @@ fn lower_if_chain( ctx.clear_static_callable_locals(); ctx.builder.position_at_end(else_block); ctx.restore_initialized_slots(split_initialized.clone()); - let else_reachable = if let Some(((next_condition, next_body), rest)) = elseif_clauses.split_first() { - lower_if_chain(ctx, next_condition, next_body, rest, else_body, merge, span) - } else if let Some(else_body) = else_body { - lower_block(ctx, else_body); - if !ctx.builder.insertion_block_is_terminated() { - branch_to(ctx, merge); - true - } else { - false - } - } else { - lower_noop(ctx, span); - if !ctx.builder.insertion_block_is_terminated() { - branch_to(ctx, merge); - true + let else_reachable = + if let Some(((next_condition, next_body), rest)) = elseif_clauses.split_first() { + lower_if_chain(ctx, next_condition, next_body, rest, else_body, merge, span) + } else if let Some(else_body) = else_body { + lower_block(ctx, else_body); + if !ctx.builder.insertion_block_is_terminated() { + branch_to(ctx, merge); + true + } else { + false + } } else { - false - } - }; + lower_noop(ctx, span); + if !ctx.builder.insertion_block_is_terminated() { + branch_to(ctx, merge); + true + } else { + false + } + }; merge_reachable |= else_reachable; let else_initialized = ctx.initialized_slots_snapshot(); ctx.restore_initialized_slots(merge_initialized_slots( @@ -620,7 +652,11 @@ fn lower_for( /// be freed (a per-write heap leak that exhausts the heap under `--web`). Non-string /// refcounted values (objects, arrays) are moved, or retained only when borrowed, /// by the write itself, so they must not be released here. -fn release_persisted_string_operand(ctx: &mut LoweringContext<'_, '_>, value: LoweredValue, span: Span) { +fn release_persisted_string_operand( + ctx: &mut LoweringContext<'_, '_>, + value: LoweredValue, + span: Span, +) { let ty = ctx.builder.value_php_type(value.value); // Only release a FRESH owning string temporary (a call/concat result, etc.). // A borrowed load of a variable that still owns the string (e.g. the prelude's @@ -721,11 +757,24 @@ fn lower_array_assign( Some(span), ); let elem_ty = indexed_array_write_element_type(ctx, array_value, updated_ty.as_ref()); - finish_indexed_array_local_write(ctx, array, array_value, updated_ty, needs_storeback, span); + finish_indexed_array_local_write( + ctx, + array, + array_value, + updated_ty, + needs_storeback, + span, + ); release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value_value, span); return; } - ctx.emit_void(op, vec![array_value.value, index_value.value, value_value.value], None, op.default_effects(), Some(span)); + ctx.emit_void( + op, + vec![array_value.value, index_value.value, value_value.value], + None, + op.default_effects(), + Some(span), + ); release_persisted_string_operand(ctx, index_value, span); release_persisted_string_operand(ctx, value_value, span); } @@ -815,9 +864,7 @@ fn lower_mixed_key_array_set( fn promoted_assoc_array_type(current_ty: PhpType, value_ty: PhpType) -> PhpType { let value_ty = normalize_array_write_element_type(value_ty.codegen_repr()); let assoc_value_ty = match current_ty.codegen_repr() { - PhpType::Array(elem_ty) if is_empty_indexed_array_element(elem_ty.as_ref()) => { - value_ty - } + PhpType::Array(elem_ty) if is_empty_indexed_array_element(elem_ty.as_ref()) => value_ty, PhpType::Array(elem_ty) => { let elem_ty = normalize_array_write_element_type(elem_ty.codegen_repr()); if elem_ty == value_ty { @@ -835,7 +882,12 @@ fn promoted_assoc_array_type(current_ty: PhpType, value_ty: PhpType) -> PhpType } /// Lowers a nested array assignment that already carries an expression target. -fn lower_nested_array_assign(ctx: &mut LoweringContext<'_, '_>, target: &Expr, value: &Expr, span: Span) { +fn lower_nested_array_assign( + ctx: &mut LoweringContext<'_, '_>, + target: &Expr, + value: &Expr, + span: Span, +) { let target = lower_expr(ctx, target); let value = lower_expr(ctx, value); ctx.emit_void( @@ -859,18 +911,38 @@ fn lower_array_push(ctx: &mut LoweringContext<'_, '_>, array: &str, value: &Expr Op::RuntimeCall }; if op == Op::ArrayPush { - let (array_value, updated_ty, needs_storeback) = if ref_bound_mixed_indexed_array_write(ctx, array, value) { - (array_value, Some(ctx.local_type(array)), true) - } else { - prepare_indexed_array_local_write(ctx, array_value, value, span) - }; - ctx.emit_void(op, vec![array_value.value, value.value], None, op.default_effects(), Some(span)); + let (array_value, updated_ty, needs_storeback) = + if ref_bound_mixed_indexed_array_write(ctx, array, value) { + (array_value, Some(ctx.local_type(array)), true) + } else { + prepare_indexed_array_local_write(ctx, array_value, value, span) + }; + ctx.emit_void( + op, + vec![array_value.value, value.value], + None, + op.default_effects(), + Some(span), + ); let elem_ty = indexed_array_write_element_type(ctx, array_value, updated_ty.as_ref()); - finish_indexed_array_local_write(ctx, array, array_value, updated_ty, needs_storeback, span); + finish_indexed_array_local_write( + ctx, + array, + array_value, + updated_ty, + needs_storeback, + span, + ); release_indexed_array_write_operand(ctx, elem_ty.as_ref(), value, span); return; } - ctx.emit_void(op, vec![array_value.value, value.value], None, op.default_effects(), Some(span)); + ctx.emit_void( + op, + vec![array_value.value, value.value], + None, + op.default_effects(), + Some(span), + ); release_persisted_string_operand(ctx, value, span); } @@ -996,9 +1068,9 @@ pub(super) fn ref_bound_mixed_indexed_array_write( /// Returns the refined array type after writing a value into an indexed array. fn indexed_array_write_updated_type(current_ty: PhpType, value_ty: PhpType) -> Option { match current_ty.codegen_repr() { - PhpType::Array(elem_ty) if is_empty_indexed_array_element(elem_ty.as_ref()) => { - Some(PhpType::Array(Box::new(normalize_empty_array_write_element_type(value_ty)))) - } + PhpType::Array(elem_ty) if is_empty_indexed_array_element(elem_ty.as_ref()) => Some( + PhpType::Array(Box::new(normalize_empty_array_write_element_type(value_ty))), + ), PhpType::Array(elem_ty) if elem_ty.codegen_repr() == PhpType::Mixed => None, PhpType::Array(elem_ty) => { let elem_ty = elem_ty.codegen_repr(); @@ -1024,8 +1096,7 @@ fn indexed_array_write_needs_mixed_conversion(current_ty: &PhpType, updated_ty: let PhpType::Array(updated_elem) = updated_ty.codegen_repr() else { return false; }; - updated_elem.codegen_repr() == PhpType::Mixed - && current_elem.codegen_repr() != PhpType::Mixed + updated_elem.codegen_repr() == PhpType::Mixed && current_elem.codegen_repr() != PhpType::Mixed } /// Returns true for the placeholder element type used by empty indexed arrays. @@ -1161,7 +1232,12 @@ fn lower_foreach( } else { let value_ty = foreach_value_type(&source_ty); if value_ty == PhpType::Mixed { - initialize_foreach_mixed_local_if_needed(ctx, value_var, value_needs_null_init, array.span); + initialize_foreach_mixed_local_if_needed( + ctx, + value_var, + value_needs_null_init, + array.span, + ); } else if value_needs_null_init { ctx.declare_local(value_var, value_ty.clone()); ctx.set_local_type(value_var, value_ty); @@ -1193,7 +1269,10 @@ fn lower_foreach( ctx.builder.position_at_end(body_block); let cleanup = ctx .value_is_owning_temporary(source) - .then_some(LoopCleanup { value: source, span: array.span }); + .then_some(LoopCleanup { + value: source, + span: array.span, + }); ctx.loop_stack.push(LoopFrame { break_block: exit, continue_block: header, @@ -1297,7 +1376,7 @@ fn initialize_foreach_mixed_local_if_needed( Op::MixedBox.default_effects(), Some(span), ); - ctx.store_local(name, boxed, PhpType::Mixed, Some(span)); + ctx.store_foreach_initializer_local_only(name, boxed, PhpType::Mixed, Some(span)); } /// Lowers a `switch` with source-ordered pattern evaluation and PHP fallthrough. @@ -1351,7 +1430,11 @@ fn lower_static_switch_dispatch( let Some(value) = int_case_value(case_expr) else { continue; }; - switch_cases.push(SwitchCase { value, target: *case_block, args: Vec::new() }); + switch_cases.push(SwitchCase { + value, + target: *case_block, + args: Vec::new(), + }); } } ctx.builder.terminate(Terminator::Switch { @@ -1414,7 +1497,12 @@ fn lower_dynamic_switch_dispatch( let case_value = coerce_to_int(ctx, case_value, Some(case_expr.span)); ctx.emit_value( Op::ICmp, - vec![int_subject.expect("non-string subject is pre-coerced").value, case_value.value], + vec![ + int_subject + .expect("non-string subject is pre-coerced") + .value, + case_value.value, + ], Some(Immediate::CmpPredicate(CmpPredicate::Eq)), PhpType::Bool, Op::ICmp.default_effects(), @@ -1458,29 +1546,40 @@ fn lower_switch_bodies( default_block: BlockId, exit: BlockId, ) { + let default_index = default + .and_then(|default| switch_default_source_index(cases, default)) + .unwrap_or(cases.len()); ctx.clear_static_callable_locals(); ctx.loop_stack.push(LoopFrame { break_block: exit, continue_block: exit, cleanup: None, }); - for (index, ((_, body), block)) in cases.iter().zip(blocks).enumerate() { - ctx.builder.position_at_end(*block); - lower_block(ctx, body); - if !ctx.builder.insertion_block_is_terminated() { - if let Some(next_block) = blocks.get(index + 1) { - branch_to(ctx, *next_block); - } else { - branch_to(ctx, default_block); + for index in 0..=cases.len() { + if default.is_some() && default_index == index { + ctx.builder.position_at_end(default_block); + if let Some(default) = default { + lower_block(ctx, default); + } + if !ctx.builder.insertion_block_is_terminated() { + branch_to(ctx, blocks.get(index).copied().unwrap_or(exit)); } + ctx.clear_static_callable_locals(); + } + if let Some((_, body)) = cases.get(index) { + ctx.builder.position_at_end(blocks[index]); + lower_block(ctx, body); + if !ctx.builder.insertion_block_is_terminated() { + branch_to( + ctx, + switch_next_body_block(index + 1, blocks, default_index, default_block, exit), + ); + } + ctx.clear_static_callable_locals(); } - ctx.clear_static_callable_locals(); - } - ctx.builder.position_at_end(default_block); - if let Some(default) = default { - lower_block(ctx, default); } - if !ctx.builder.insertion_block_is_terminated() { + if default.is_none() { + ctx.builder.position_at_end(default_block); branch_to(ctx, exit); } ctx.loop_stack.pop(); @@ -1488,8 +1587,59 @@ fn lower_switch_bodies( ctx.clear_static_callable_locals(); } +/// Returns the source-order insertion point for a non-empty switch default body. +fn switch_default_source_index( + cases: &[(Vec, Vec)], + default: &[Stmt], +) -> Option { + if cases.is_empty() { + return Some(0); + } + let default_start = default.first()?.span; + if default_start == Span::dummy() { + return None; + } + let mut default_index = 0; + for (conditions, _) in cases { + let case_start = conditions.first()?.span; + if case_start == Span::dummy() { + return None; + } + if span_is_before(case_start, default_start) { + default_index += 1; + } + } + Some(default_index) +} + +/// Returns the block that follows one source-ordered switch body. +fn switch_next_body_block( + next_index: usize, + blocks: &[BlockId], + default_index: usize, + default_block: BlockId, + exit: BlockId, +) -> BlockId { + if default_index == next_index { + default_block + } else { + blocks.get(next_index).copied().unwrap_or(exit) + } +} + +/// Returns true when `span` appears before `pivot` in the same source file. +fn span_is_before(span: Span, pivot: Span) -> bool { + span.line < pivot.line || (span.line == pivot.line && span.col < pivot.col) +} + /// Lowers include/require statements through a high-level runtime call. -fn lower_include(ctx: &mut LoweringContext<'_, '_>, path: &Expr, once: bool, required: bool, span: Span) { +fn lower_include( + ctx: &mut LoweringContext<'_, '_>, + path: &Expr, + once: bool, + required: bool, + span: Span, +) { let path = lower_expr(ctx, path); let label = format!("include once={} required={}", once, required); let data = ctx.intern_string(&label); @@ -1516,7 +1666,12 @@ fn lower_include_once_mark(ctx: &mut LoweringContext<'_, '_>, label: &str, span: } /// Lowers an include-once guarded body. -fn lower_include_once_guard(ctx: &mut LoweringContext<'_, '_>, label: &str, body: &[Stmt], span: Span) { +fn lower_include_once_guard( + ctx: &mut LoweringContext<'_, '_>, + label: &str, + body: &[Stmt], + span: Span, +) { let data = ctx.intern_string(label); let should_run = ctx .builder @@ -1531,8 +1686,12 @@ fn lower_include_once_guard(ctx: &mut LoweringContext<'_, '_>, label: &str, body Some(span), ) .expect("include_once_guard produces a branch condition"); - let body_block = ctx.builder.create_named_block("include_once_body", Vec::new()); - let after_block = ctx.builder.create_named_block("include_once_after", Vec::new()); + let body_block = ctx + .builder + .create_named_block("include_once_body", Vec::new()); + let after_block = ctx + .builder + .create_named_block("include_once_after", Vec::new()); ctx.builder.terminate(Terminator::CondBr { cond: should_run, then_target: body_block, @@ -1577,7 +1736,9 @@ fn lower_try_catch( catches: &[CatchClause], span: Span, ) { - let handler_block = ctx.builder.create_named_block("try.catch_dispatch", Vec::new()); + let handler_block = ctx + .builder + .create_named_block("try.catch_dispatch", Vec::new()); let after_block = ctx.builder.create_named_block("try.after", Vec::new()); let handler_token = handler_block.as_raw() as i64; @@ -1639,7 +1800,9 @@ fn lower_try_catch_finally( finally_body: &[Stmt], span: Span, ) { - let handler_block = ctx.builder.create_named_block("try.catch_dispatch", Vec::new()); + let handler_block = ctx + .builder + .create_named_block("try.catch_dispatch", Vec::new()); let after_block = ctx.builder.create_named_block("try.after", Vec::new()); let handler_token = handler_block.as_raw() as i64; @@ -1700,7 +1863,9 @@ fn lower_catch_dispatch( } let current = lower_current_exception(ctx, span); - ctx.builder.terminate(Terminator::Throw { value: current.value }); + ctx.builder.terminate(Terminator::Throw { + value: current.value, + }); } /// Lowers catch dispatch for `try`/`catch`/`finally`. @@ -1731,7 +1896,9 @@ fn lower_catch_dispatch_with_finally( let current = lower_current_exception(ctx, span); lower_block(ctx, finally_body); if !ctx.builder.insertion_block_is_terminated() { - ctx.builder.terminate(Terminator::Throw { value: current.value }); + ctx.builder.terminate(Terminator::Throw { + value: current.value, + }); } } @@ -1752,7 +1919,8 @@ fn lower_catch_match( let mismatch = if idx + 1 == catch.exception_types.len() { next_catch } else { - ctx.builder.create_named_block("try.catch_type_next", Vec::new()) + ctx.builder + .create_named_block("try.catch_type_next", Vec::new()) }; let current = lower_current_exception(ctx, span); let data = ctx.intern_class_name(catch_type.as_str()); @@ -1791,12 +1959,15 @@ fn lower_current_exception(ctx: &mut LoweringContext<'_, '_>, span: Span) -> Low /// Binds and clears the active exception for a matched catch clause. fn lower_catch_bind(ctx: &mut LoweringContext<'_, '_>, catch: &CatchClause, span: Span) { - let (immediate, php_type) = catch.variable.as_ref().map_or((None, PhpType::Void), |variable| { - let php_type = catch_variable_type(catch); - let slot = ctx.declare_local(variable, php_type.clone()); - ctx.set_local_type(variable, php_type.clone()); - (Some(Immediate::LocalSlot(slot)), php_type) - }); + let (immediate, php_type) = catch + .variable + .as_ref() + .map_or((None, PhpType::Void), |variable| { + let php_type = catch_variable_type(catch); + let slot = ctx.declare_local(variable, php_type.clone()); + ctx.set_local_type(variable, php_type.clone()); + (Some(Immediate::LocalSlot(slot)), php_type) + }); ctx.builder.emit_with_effects( Op::CatchBind, Vec::new(), @@ -1812,7 +1983,11 @@ fn lower_catch_bind(ctx: &mut LoweringContext<'_, '_>, catch: &CatchClause, span /// Returns the local type to use for a catch variable. fn catch_variable_type(catch: &CatchClause) -> PhpType { if catch.exception_types.len() == 1 { - return PhpType::Object(catch.exception_types[0].trim_start_matches('\\').to_string()); + return PhpType::Object( + catch.exception_types[0] + .trim_start_matches('\\') + .to_string(), + ); } PhpType::Object("Throwable".to_string()) } @@ -1832,7 +2007,11 @@ fn lower_continue(ctx: &mut LoweringContext<'_, '_>, level: usize) { ctx.builder.terminate(Terminator::Unreachable); return; }; - terminate_branch(ctx, frame.continue_block, loop_cleanup_count_for_branch(level)); + terminate_branch( + ctx, + frame.continue_block, + loop_cleanup_count_for_branch(level), + ); } /// Lowers a return statement using the current function return contract. @@ -1947,13 +2126,7 @@ fn acquire_borrowed_return_value( } if !matches!( ctx.builder.value_defining_op(value.value), - Some( - Op::ArrayGet - | Op::HashGet - | Op::PropGet - | Op::DynamicPropGet - | Op::NullsafePropGet - ) + Some(Op::ArrayGet | Op::HashGet | Op::PropGet | Op::DynamicPropGet | Op::NullsafePropGet) ) { return value; } @@ -1969,6 +2142,7 @@ fn terminate_return(ctx: &mut LoweringContext<'_, '_>, value: Option, target: BlockId, loop_cle return; } emit_innermost_loop_cleanups(ctx, loop_cleanup_count); - ctx.builder.terminate(Terminator::Br { target, args: Vec::new() }); + ctx.builder.terminate(Terminator::Br { + target, + args: Vec::new(), + }); } /// Terminates with a throw after running finally bodies that apply to uncaught throws. @@ -2162,7 +2339,11 @@ fn lower_list_unpack(ctx: &mut LoweringContext<'_, '_>, vars: &[String], value: } /// Emits the positional integer key used to read one list-unpack element. -fn lower_list_unpack_index(ctx: &mut LoweringContext<'_, '_>, index: usize, span: Span) -> LoweredValue { +fn lower_list_unpack_index( + ctx: &mut LoweringContext<'_, '_>, + index: usize, + span: Span, +) -> LoweredValue { ctx.emit_value( Op::ConstI64, Vec::new(), @@ -2231,7 +2412,11 @@ fn lower_global(ctx: &mut LoweringContext<'_, '_>, vars: &[String]) { /// Lowers a static local variable initialization. fn lower_static_var(ctx: &mut LoweringContext<'_, '_>, name: &str, init: &Expr, span: Span) { let value = lower_expr(ctx, init); - let slot = ctx.declare_local_with_kind(name, ctx.builder.value_php_type(value.value), LocalKind::StaticLocal); + let slot = ctx.declare_local_with_kind( + name, + ctx.builder.value_php_type(value.value), + LocalKind::StaticLocal, + ); ctx.builder.emit_with_effects( Op::InitStaticLocal, vec![value.value], @@ -2321,12 +2506,14 @@ fn magic_set_receiver_has_method( let Some(class_info) = ctx.classes.get(normalized) else { return false; }; - if class_info.properties.iter().any(|(name, _)| name == property) { + if class_info + .properties + .iter() + .any(|(name, _)| name == property) + { return false; } - class_info - .methods - .contains_key(&php_symbol_key("__set")) + class_info.methods.contains_key(&php_symbol_key("__set")) } /// Lowers an undeclared property write to a normal `__set` instance-method call. @@ -2524,9 +2711,8 @@ fn lower_static_property_array_assign( return; } - let property_value = if let Some(property_ty) = - static_property_type(ctx, receiver, property) - .filter(|ty| type_satisfies_array_access_for_ir(ctx, ty)) + let property_value = if let Some(property_ty) = static_property_type(ctx, receiver, property) + .filter(|ty| type_satisfies_array_access_for_ir(ctx, ty)) { load_static_property_as(ctx, receiver, property, property_ty, span) } else { @@ -2607,7 +2793,12 @@ fn lower_property_array_push( Op::PropSet.default_effects(), Some(span), ); - release_rewritten_property_value_after_retaining_store(ctx, &property_ty, property_value, span); + release_rewritten_property_value_after_retaining_store( + ctx, + &property_ty, + property_value, + span, + ); return; } @@ -2664,7 +2855,12 @@ fn lower_property_array_assign( Op::PropSet.default_effects(), Some(span), ); - release_rewritten_property_value_after_retaining_store(ctx, &property_ty, property_value, span); + release_rewritten_property_value_after_retaining_store( + ctx, + &property_ty, + property_value, + span, + ); return; } if let Some(property_ty) = @@ -2698,13 +2894,17 @@ fn lower_property_array_assign( Op::PropSet.default_effects(), Some(span), ); - release_rewritten_property_value_after_retaining_store(ctx, &property_ty, property_value, span); + release_rewritten_property_value_after_retaining_store( + ctx, + &property_ty, + property_value, + span, + ); return; } - if let Some(property_ty) = - object_property_type(ctx, object.value, property) - .filter(|ty| type_satisfies_array_access_for_ir(ctx, ty)) + if let Some(property_ty) = object_property_type(ctx, object.value, property) + .filter(|ty| type_satisfies_array_access_for_ir(ctx, ty)) { let data = ctx.intern_string(property); let property_value = ctx.emit_value( @@ -2749,7 +2949,8 @@ fn release_property_assignment_source_after_retaining_store( if !ctx.value_is_owning_temporary(value) { return; } - if !property_store_keeps_independent_ref(property_ty, &ctx.builder.value_php_type(value.value)) { + if !property_store_keeps_independent_ref(property_ty, &ctx.builder.value_php_type(value.value)) + { return; } crate::ir_lower::ownership::release_if_owned(ctx, value, Some(span)); @@ -2814,7 +3015,13 @@ fn indexed_property_array_element_type(property_ty: &PhpType) -> Option /// Emits a no-op marker for declaration-only or frontend-only statements. fn lower_noop(ctx: &mut LoweringContext<'_, '_>, span: Span) { - ctx.emit_void(Op::Nop, Vec::new(), None, Op::Nop.default_effects(), Some(span)); + ctx.emit_void( + Op::Nop, + Vec::new(), + None, + Op::Nop.default_effects(), + Some(span), + ); } /// Records a function variant group in high-level EIR metadata form. @@ -2856,7 +3063,10 @@ fn lower_function_variant_mark( /// Emits a branch to `target` if the current block can still fall through. fn branch_to(ctx: &mut LoweringContext<'_, '_>, target: BlockId) { if !ctx.builder.insertion_block_is_terminated() { - ctx.builder.terminate(Terminator::Br { target, args: Vec::new() }); + ctx.builder.terminate(Terminator::Br { + target, + args: Vec::new(), + }); } } @@ -2931,7 +3141,10 @@ fn emit_const_bool( span, ) .expect("const_bool produces a value"); - LoweredValue { value, ir_type: IrType::I64 } + LoweredValue { + value, + ir_type: IrType::I64, + } } /// Emits a null sentinel value. @@ -2949,7 +3162,10 @@ fn emit_null_value(ctx: &mut LoweringContext<'_, '_>, span: Option) -> Low span, ) .expect("const_null produces a value"); - LoweredValue { value, ir_type: IrType::I64 } + LoweredValue { + value, + ir_type: IrType::I64, + } } /// Coerces a value to the current function return storage type when needed. @@ -3016,7 +3232,10 @@ fn coerce_to_tagged_scalar( if value.ir_type == IrType::TaggedScalar { return value; } - if matches!(ctx.builder.value_php_type(value.value).codegen_repr(), PhpType::Void) { + if matches!( + ctx.builder.value_php_type(value.value).codegen_repr(), + PhpType::Void + ) { return ctx.emit_value( Op::ConstNull, Vec::new(), @@ -3052,8 +3271,14 @@ fn coerce_container_to_return_type( Op::ArrayToMixed } ( - PhpType::AssocArray { value: source_value, .. }, - PhpType::AssocArray { value: return_value, .. }, + PhpType::AssocArray { + value: source_value, + .. + }, + PhpType::AssocArray { + value: return_value, + .. + }, ) if source_value.codegen_repr() != PhpType::Mixed && return_value.codegen_repr() == PhpType::Mixed => { @@ -3260,7 +3485,9 @@ fn static_receiver_class_name( StaticReceiver::Self_ | StaticReceiver::Static => ctx.current_class.clone(), StaticReceiver::Parent => { let current = ctx.current_class.as_deref()?; - ctx.classes.get(current).and_then(|class_info| class_info.parent.clone()) + ctx.classes + .get(current) + .and_then(|class_info| class_info.parent.clone()) } } } diff --git a/src/ir_passes/dead_store.rs b/src/ir_passes/dead_store.rs index 99da80cb2d..cc7150d61b 100644 --- a/src/ir_passes/dead_store.rs +++ b/src/ir_passes/dead_store.rs @@ -42,8 +42,8 @@ impl IrPass for DeadStore { /// Neutralizes dead scalar `store_local` instructions and reports whether any /// store changed. The literal pool is unused because the pass never /// materializes new constants. - fn run(&self, function: &mut Function, _data: &mut DataPool) -> bool { - let eligible = eligible_slots(function); + fn run(&self, function: &mut Function, data: &mut DataPool) -> bool { + let eligible = eligible_slots(function, data); if eligible.is_empty() { return false; } @@ -70,7 +70,7 @@ impl IrPass for DeadStore { /// `unset_local`, static-local or global access, list unpack, …) makes the slot /// ineligible because it could read or alias the slot in a way this pass does not /// model. -fn eligible_slots(function: &Function) -> HashSet { +fn eligible_slots(function: &Function, data: &DataPool) -> HashSet { let mut eligible: HashSet = function .locals .iter() @@ -95,9 +95,50 @@ fn eligible_slots(function: &Function) -> HashSet { } exclude_address_escaping_slots(function, &mut eligible); + exclude_eval_literal_read_slots(function, data, &mut eligible); eligible } +/// Drops slots read implicitly by literal `eval` calls from dead-store eligibility. +/// +/// Direct eval AOT read-param lowering materializes those slot loads in codegen +/// rather than as explicit EIR `LoadLocal` instructions, so the liveness scan +/// cannot see them. Excluding the known read names keeps stores that feed the +/// eval fragment alive while leaving ordinary scalar locals eligible. +fn exclude_eval_literal_read_slots( + function: &Function, + data: &DataPool, + eligible: &mut HashSet, +) { + let slots_by_name: HashMap = function + .locals + .iter() + .filter(|local| eligible.contains(&local.id)) + .filter_map(|local| local.name.as_ref().map(|name| (name.clone(), local.id))) + .collect(); + if slots_by_name.is_empty() { + return; + } + + for inst in &function.instructions { + if inst.op != Op::EvalLiteralCall { + continue; + } + let Some(Immediate::Data(data_id)) = inst.immediate else { + continue; + }; + let Some(fragment) = data.strings.get(data_id.as_raw() as usize) else { + continue; + }; + let plan = crate::eval_aot::plan_literal_fragment_with_static_calls(fragment, |_, _| false); + for name in plan.reads() { + if let Some(slot) = slots_by_name.get(name) { + eligible.remove(slot); + } + } + } +} + /// Drops slots whose loaded value can be reinterpreted as the slot's address. /// /// A by-reference call argument or closure capture aliases the underlying slot: @@ -188,7 +229,11 @@ fn immediate_slots(immediate: Option<&Immediate>) -> Vec { } /// Returns the eligible local slot loaded or stored by an instruction, if any. -fn instruction_slot(function: &Function, inst_id: InstId, eligible: &HashSet) -> Option<(Op, LocalSlotId)> { +fn instruction_slot( + function: &Function, + inst_id: InstId, + eligible: &HashSet, +) -> Option<(Op, LocalSlotId)> { let inst = function.instruction(inst_id)?; let Some(Immediate::LocalSlot(slot)) = inst.immediate else { return None; diff --git a/src/lib.rs b/src/lib.rs index 26e1d782c7..1963a36daa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,8 +22,11 @@ pub mod codegen_support; pub mod conditional; /// Error and warning reporting. pub mod errors; +mod eval_aot; /// `#[Export]` attribute scan for cdylib emission. pub mod exports; +/// Image (GD/Exif/Imagick/Gmagick/Cairo) standard-library prelude injection. +pub mod image_prelude; /// Intrinsic call handling. pub mod intrinsics; /// Intermediate representation used by the EIR backend track. @@ -38,18 +41,16 @@ pub mod lexer; pub mod list_id_prelude; /// Magic constant substitution. pub mod magic_constants; -/// Name resolution and mangling. -pub mod names; /// Namespace and use resolution. pub mod name_resolver; +/// Name resolution and mangling. +pub mod names; /// Optimizer passes. pub mod optimize; /// Parser for PHP syntax. pub mod parser; /// PDO (SQLite) standard-library prelude injection. pub mod pdo_prelude; -/// Image (GD/Exif/Imagick/Gmagick/Cairo) standard-library prelude injection. -pub mod image_prelude; /// Resolution of includes. pub mod resolver; /// Source span tracking. diff --git a/src/main.rs b/src/main.rs index 556394e8b1..95731f4ddc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,9 @@ mod codegen; mod codegen_support; mod conditional; mod errors; +mod eval_aot; mod exports; +mod image_prelude; mod intrinsics; #[allow(dead_code, unused_imports)] mod ir; @@ -23,12 +25,11 @@ mod ir; mod ir_lower; #[allow(dead_code, unused_imports)] mod ir_passes; -mod linker; mod lexer; +mod linker; mod list_id_prelude; mod magic_constants; mod name_resolver; -mod image_prelude; mod names; mod optimize; mod parser; diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index c964708389..eba88d4287 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -33,34 +33,4304 @@ echo is_callable("eval") ? "1" : "0"; assert_eq!(out, "00"); } -/// Verifies a program containing `eval()` references the bridge symbol and requests libelephc-magician. +/// Verifies an unsupported literal `eval()` still references the bridge symbol and requests libelephc-magician. #[test] fn test_eval_codegen_requires_eval_bridge() { let dir = make_cli_test_dir("elephc_magician_bridge_asm"); + let (user_asm, _runtime_asm, required_libraries) = compile_source_to_asm_with_options( + " 1, "b" => 2]) + + count(["1" => "a", 1 => "b", "01" => "c"]) + + count([1, [2, 3], ["x" => 4]]) + + count([true => "yes", 1 => "one", false => "no", 0 => "zero"]) + + count([null => "empty", "" => "blank", "name" => "Ada"]) + + count([2.0 => "two", 2 => "int", -2.0 => "minus", -2 => "intminus"]);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static array count should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static array count should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array count should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array count should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array count should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "16"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies runtime `count()` calls on eval-created local arrays use EIR AOT. +#[test] +fn test_literal_eval_local_array_count_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_local_array_count_aot"); + let source = r#" 1, "b" => 2]; return count($items) + count($map);'); +echo ":" . count($items) . ":" . count($map); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval runtime count on local arrays should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval runtime count on local arrays should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "runtime count on eval-created arrays should flush created locals through scope helpers:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_context_new"), + "scope-only runtime count on local arrays should not create an eval bridge context:\n{user_asm}" + ); + assert!( + runtime_asm.contains("__elephc_eval_scope_set"), + "scope-only runtime count on local arrays should emit core eval scope helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only runtime count on local arrays should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "5:3:2"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `count()` can consume caller-scope arrays through direct-param EIR AOT. +#[test] +fn test_literal_eval_count_scope_read_array_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_count_scope_read_array_aot"); + let source = r#" 1, "b" => 2]; +echo eval('return count($items) . ":" . count($map);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "count over caller-scope arrays should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "count over caller-scope arrays should not reference eval runtime helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "count over caller-scope arrays should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "count over caller-scope arrays should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "3:2"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies named `count()` with default mode can use direct-param EIR AOT. +#[test] +fn test_literal_eval_count_named_default_mode_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_count_named_default_mode_aot"); + let source = r#" null]) ? "Y" : "bad") + . (array_key_exists("missing", ["name" => 1]) ? "bad" : "N") + . (array_key_exists("1", [1 => "one"]) ? "I" : "bad") + . (array_key_exists(2, ["0" => "a", "01" => "b", 2 => "c"]) ? "K" : "bad") + . (array_key_exists(0, ["x", "y"]) ? "Z" : "bad") + . (array_key_exists(2, ["x", "y"]) ? "bad" : "O") + . (array_key_exists(true, [1 => "yes"]) ? "T" : "bad") + . (array_key_exists(false, [0 => "no"]) ? "F" : "bad") + . (array_key_exists(null, ["" => "empty"]) ? "E" : "bad") + . (array_key_exists(2.0, [2.0 => "two"]) ? "D" : "bad") + . (array_key_exists(-2.0, [-2.0 => "minus"]) ? "M" : "bad");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static array_key_exists should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static array_key_exists should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array_key_exists should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array_key_exists should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array_key_exists should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "YNIKZOTFEDM"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `array_key_exists()` can inspect caller-scope arrays through direct-param AOT. +#[test] +fn test_literal_eval_array_key_exists_scope_read_array_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_array_key_exists_scope_read_array_aot"); + let source = r#" null, "age" => 42]; +echo eval('return (array_key_exists(1, $items) ? "I" : "bad") + . ":" . (array_key_exists(3, $items) ? "bad" : "N") + . ":" . (array_key_exists("name", $map) ? "A" : "bad") + . ":" . (array_key_exists("missing", $map) ? "bad" : "M");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "array_key_exists over caller-scope arrays should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "array_key_exists over caller-scope arrays should not reference eval runtime helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "array_key_exists over caller-scope arrays should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "array_key_exists over caller-scope arrays should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "I:N:A:M"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies caller-scope `array_key_exists()` supports null and integral float keys in EIR AOT. +#[test] +fn test_literal_eval_array_key_exists_scope_read_null_and_float_keys_use_eir_aot() { + let dir = + make_cli_test_dir("elephc_literal_eval_array_key_exists_scope_read_null_float_keys_aot"); + let source = r#" "empty", -2 => "minus", 2 => "two"]; +echo eval('return (array_key_exists(1.0, $items) ? "F" : "bad") + . ":" . (array_key_exists(null, $map) ? "N" : "bad") + . ":" . (array_key_exists(-2.0, $map) ? "M" : "bad");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "array_key_exists with null/integral float keys should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "array_key_exists with null/integral float keys should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "array_key_exists with null/integral float keys should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "array_key_exists with null/integral float keys should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "F:N:M"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies named args for EIR-safe runtime builtins stay on the eval AOT path. +#[test] +fn test_literal_eval_named_runtime_builtins_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_named_runtime_builtins_aot"); + let source = r#" "Ada"]; +echo eval('return count(value: $items) + . ":" . (boolval(value: $flag) ? "B" : "bad") + . ":" . (array_key_exists(array: $map, key: "name") ? "Y" : "bad") + . ":" . (array_key_exists(key: "missing", array: $map) ? "bad" : "N");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "named runtime builtins should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "named runtime builtins should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "named runtime builtins should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "named runtime builtins should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "2:B:Y:N"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static spread args for EIR-safe runtime builtins stay on the eval AOT path. +#[test] +fn test_literal_eval_static_spread_runtime_builtins_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_spread_runtime_builtins_aot"); + let source = r#" "Ada"]; +echo eval('return count(...["value" => $items]) + . ":" . (boolval(...["value" => $flag]) ? "B" : "bad") + . ":" . (array_key_exists(...["array" => $map, "key" => "name"]) ? "Y" : "bad") + . ":" . (array_key_exists(...["key" => "missing", "array" => $map]) ? "bad" : "N");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "static-spread runtime builtins should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "static-spread runtime builtins should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "static-spread runtime builtins should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "static-spread runtime builtins should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "2:B:Y:N"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies dynamic spread args for runtime builtins keep the eval bridge fallback. +#[test] +fn test_literal_eval_dynamic_spread_runtime_builtin_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_dynamic_spread_runtime_builtin_bridge"); + let source = r#" $items]; +echo eval('return count(...$args);'); +"#; + let (user_asm, _runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("__elephc_eval_execute"), + "dynamic-spread runtime builtin should keep the interpreter bridge fallback:\n{user_asm}" + ); + assert!( + required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "dynamic-spread runtime builtin should link elephc_magician for fallback: {required_libraries:?}" + ); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `array_key_exists()` on a caller scalar keeps the bridge fallback. +#[test] +fn test_literal_eval_array_key_exists_scope_read_scalar_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_array_key_exists_scope_read_scalar_bridge"); + let source = r#" "Ada"]["name"];'); +echo ":"; +echo eval('return isset(["name" => "Ada"]["name"]) ? "Y" : "bad";'); +echo ":"; +echo eval('return isset(["name" => null]["name"]) ? "bad" : "N";'); +echo ":"; +echo eval('return isset(["name" => "Ada"]["missing"]) ? "bad" : "M";'); +echo ":"; +echo eval('return empty(["name" => ""]["name"]) ? "E" : "bad";'); +echo ":"; +echo eval('return empty(["name" => "Ada"]["name"]) ? "bad" : "V";'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static array reads should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static array reads should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array reads should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array reads should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array reads should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "2:Ada:Y:N:M:E:V"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static array literals returned from eval lower through EIR AOT. +#[test] +fn test_literal_eval_static_array_literal_return_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_array_literal_return_aot"); + let source = r#" "L", "right" => "R"];'); +echo $map["left"] . $map["right"]; +echo ":"; +$rows = eval('return [[10, 20], ["name" => "Ada"]];'); +echo $rows[0][1] . ":" . $rows[1]["name"]; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static array returns should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static array returns should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array returns should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array returns should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array returns should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "ab:LR:20:Ada"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static associative array auto keys can lower through eval EIR AOT. +#[test] +fn test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_array_next_key_aot"); + let source = r#" "two", "tail"][3];'); +echo ":"; +echo eval('return [-2 => "minus", "tail"][-1];'); +echo ":"; +echo eval('return ["2" => "two", "tail"][3];'); +echo ":"; +echo eval('return ["02" => "two", "tail"][0];'); +echo ":"; +echo eval('return [true => "yes", "tail"][2];'); +echo ":"; +echo eval('return [false => "no", "tail"][1];'); +echo ":"; +echo eval('return [null => "empty"][""];'); +echo ":"; +echo eval('return [null => "empty", "tail"][0];'); +echo ":"; +echo eval('return [2.0 => "two", "tail"][3];'); +echo ":"; +echo eval('return [-2.0 => "minus", "tail"][-1];'); +echo ":"; +echo eval('return array(2 => "two", "tail")[3];'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "static array next-key reads should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static array next-key reads should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static array next-key reads should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static array next-key reads should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static array next-key reads should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!( + out, + "tail:tail:tail:tail:tail:tail:empty:tail:tail:tail:tail" + ); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies fractional float array keys stay on the bridge because PHP emits a precision warning. +#[test] +fn test_eval_static_array_fractional_float_key_uses_bridge_fallback() { + let dir = make_cli_test_dir("elephc_eval_static_array_fractional_float_key_bridge"); + let source = r#" "two", "tail"][3];'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT fallback"), + "fractional float array key should keep the explicit literal eval fallback marker:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_execute"), + "fractional float array key should execute through the bridge:\n{user_asm}" + ); + assert!( + required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "fractional float array key fallback should link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "tail"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static array writes into eval scope lower through EIR AOT. +#[test] +fn test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers() { + let dir = make_cli_test_dir("elephc_literal_eval_static_array_scope_write_aot"); + let source = r#" "Ada"];'); +echo $map["name"]; +echo ":"; +eval('$legacy = array("x", "y");'); +echo $legacy[0] . $legacy[1]; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "static array scope writes should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "static array scope writes should flush through eval scope set:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static array scope writes should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + runtime_asm.contains("__elephc_eval_scope_set"), + "static array scope writes should emit core eval scope helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only static array eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "b:Ada:xy"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies named/static-spread builtin args normalize before literal eval AOT folding. +#[test] +fn test_literal_eval_static_builtin_named_args_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_builtin_named_args_aot"); + let source = r#" "cd"]) + + count(value: [1, 2, 3]) + + (array_key_exists(array: ["name" => null], key: "name") ? 10 : 0) + + (str_contains(needle: "x", haystack: "xyz") ? 20 : 0) + + strlen(str_repeat(times: 3, string: "q")) + + strlen(substr(length: 2, string: "abcdef", offset: 1));'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static builtin named args should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static builtin named args should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static builtin named args should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static builtin named args should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static builtin named args should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "42"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies foldable static builtins also work in direct local-scalar statement bodies. +#[test] +fn test_literal_eval_static_scalar_builtins_in_local_body_use_aot() { + let dir = make_cli_test_dir("elephc_literal_eval_static_scalar_builtins_local_body_aot"); + let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options( + " "B", "left" => "A"]) + . ":" . join_static_spread(...["left" => "C", "right" => "D", "bang" => true]);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval static function static-spread args should use EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval static function static-spread args should not call the bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static-spread function eval should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static-spread function eval should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static-spread function eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "A:B.:C:D!"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies dynamic spread args for scalar user functions keep the eval bridge fallback. +#[test] +fn test_literal_eval_static_user_function_dynamic_spread_args_keep_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_static_function_dynamic_spread_args_bridge"); + let source = r#" "A", "right" => "B"]; +echo eval('return join_dynamic_spread(...$args);'); +"#; + let (user_asm, _runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("__elephc_eval_execute"), + "dynamic-spread static function call should keep the interpreter bridge fallback:\n{user_asm}" + ); + assert!( + required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "dynamic-spread static function call should link elephc_magician for fallback: {required_libraries:?}" + ); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static user-function scalar defaults are accepted by literal eval EIR AOT. +#[test] +fn test_literal_eval_static_user_function_defaults_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_function_defaults_aot"); + let source = r#" $s]) + . ":" . call_user_func("strtoupper", "az");'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "static call_user_func builtin callbacks should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "static call_user_func builtin callbacks should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "static call_user_func builtin callbacks should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "static call_user_func builtin callbacks should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "4:4:AZ"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static `call_user_func*()` user callbacks can use literal eval EIR AOT. +#[test] +fn test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_call_user_func_user_function_aot"); + let source = r#" "D", "left" => "C", "bang" => true]);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "static call_user_func user callbacks should use EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static call_user_func user callbacks should not call the bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "static call_user_func user callbacks should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "static call_user_func user callbacks should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "static call_user_func user callbacks should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "A:B.|C:D!"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies dynamic `call_user_func()` callbacks keep the eval bridge fallback. +#[test] +fn test_literal_eval_dynamic_call_user_func_callback_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_call_user_func_dynamic_callback_bridge"); + let source = r#"= 2, + "null/fallthrough literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "null/fallthrough literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only null/fallthrough literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only null/fallthrough literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only null/fallthrough literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "N:body:N"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies ternary expressions inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_ternary_expressions_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_ternary_eir_aot"); + let source = r#"= 2, + "ternary literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "ternary literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only ternary literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only ternary literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only ternary literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "yes:fallback:len"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies null coalesce inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_null_coalesce_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_null_coalesce_eir_aot"); + let source = r#"= 4, + "null coalesce literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "null coalesce caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "null coalesce literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only null coalesce literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only null coalesce literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only null coalesce literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "literal:set:caller:missing:nullcaller"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies match expressions inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_match_expression_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_match_eir_aot"); + let source = r#" "int", "1" => "string", default => "other" };'); +echo ":"; +$x = 3; +echo eval('return match ($x) { 1, 2 => "small", 3 => "three", default => "other" };'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm + .matches("eval literal AOT compiled EIR function") + .count() + >= 2, + "match literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "match literal eval caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "match literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only match literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only match literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only match literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "string:three"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies strict equality operators inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_strict_equality_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_strict_equality_eir_aot"); + let source = r#"= 2, + "strict-equality literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "strict-equality caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "strict-equality literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only strict-equality literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only strict-equality literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only strict-equality literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "S:D:same"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies logical xor inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_logical_xor_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_logical_xor_eir_aot"); + let source = r#"= 2, + "logical xor literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "logical xor caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "logical xor literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only logical xor literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only logical xor literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only logical xor literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "T:F"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies scalar casts inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_scalar_casts_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_scalar_casts_eir_aot"); + let source = r#"= 3, + "scalar-cast literal evals should use internal EIR AOT functions:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "scalar-cast caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "scalar-cast literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only scalar-cast literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only scalar-cast literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only scalar-cast literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "42:7:F:1.25:42"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies division and exponentiation inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_division_and_pow_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_division_pow_eir_aot"); + let source = r#"= 2, + "division and exponentiation literal evals should use EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "division and exponentiation literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_context_new"), + "native-only division/pow literal evals should not create an eval context:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_execute"), + "division and exponentiation literal evals should not emit the eval bridge runtime:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "division and exponentiation literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "4.5:512"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies compound division/modulo inside literal eval uses direct local sync. +#[test] +fn test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge() { + let dir = make_cli_test_dir("elephc_literal_eval_division_modulo_assign_eir_aot"); + let source = r#"> 2); +$x = 6; $x &= 3; echo ":" . $x; +$x = 4; $x |= 1; echo "," . $x; +$x = 7; $x ^= 3; echo "," . $x; +$x = 1; $x <<= 5; echo "," . $x; +$x = 64; $x >>= 3; echo "," . $x;'); +echo ":" . $x; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled local scalar with direct local sync"), + "bitwise/shift literal eval should use local-scalar AOT with direct sync:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_scope_set"), + "bitwise/shift literal eval should not write through eval scope:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "bitwise/shift literal eval should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "bitwise/shift literal eval should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "bitwise/shift literal eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "1:7:6:-1:16:-4:2,5,4,32,8:8"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies spaceship comparisons inside literal eval can lower through EIR AOT. +#[test] +fn test_literal_eval_spaceship_uses_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_spaceship_eir_aot"); + let source = r#" 2;'); +echo ":"; +echo eval('return 2.5 <=> 2.5;'); +echo ":"; +$a = 12; +echo eval('return $a <=> 10;'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm + .matches("eval literal AOT compiled EIR function") + .count() + >= 3, + "spaceship literal evals should use EIR AOT:\n{user_asm}" + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "spaceship caller reads should use direct read params:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "spaceship literal evals should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only spaceship literal evals should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only spaceship literal evals should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only spaceship literal evals should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "-1:0:1"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies float literal eval returns can lower through an internal EIR AOT function. +#[test] +fn test_literal_eval_float_return_uses_aot_without_execute_bridge() { + let dir = make_cli_test_dir("elephc_literal_eval_float_return_eir_aot"); + let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options( + "i; } + public function key(): int { return $this->i; } + public function next(): void { $this->i = $this->i + 1; } + public function valid(): bool { return $this->i < 0; } + public function rewind(): void { $this->i = 0; } +} +$items = [1, 2]; +$iterator = new EvalAotDirectIterator(); +$n = 42; +echo eval('return (is_iterable($items) ? "A" : "bad") . + (is_iterable($iterator) ? "I" : "bad") . + (is_iterable($n) ? "bad" : "N");'); +"#, + &dir, + 8_388_608, + false, + false, + ); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with direct read params"), + "is_iterable over caller-scope values should use direct read-param EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "is_iterable over caller-scope values should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_context_new"), + "is_iterable over caller-scope values should not allocate an eval bridge context:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_execute"), + "is_iterable over caller-scope values should not emit the interpreter bridge:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "is_iterable over caller-scope values should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "AIN"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies `is_numeric()` and `is_resource()` use direct-param EIR AOT for scope reads. +#[test] +fn test_literal_eval_numeric_resource_probes_scope_read_use_eir_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_numeric_resource_probe_scope_read_aot"); + let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options( + r#" 0; --$k) { echo $k; }'); +echo ":" . $i . ":" . $j . ":" . $k; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled local scalar with direct local sync"), + "literal eval local inc/dec should use local-scalar AOT with direct sync:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_scope_set"), + "literal eval local inc/dec should not write through eval scope:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval local inc/dec should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "literal eval local inc/dec should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "literal eval local inc/dec should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "1012321:1:3:0"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies supported `switch` statements inside literal eval lower through local-scalar AOT. +#[test] +fn test_literal_eval_switch_uses_aot_without_execute_bridge() { + let dir = make_cli_test_dir("elephc_literal_eval_switch_aot"); + let source = r#" 1, "b" => 2] as $key => $value) { echo $key . ":" . $value . ";"; }'); +echo ":" . $key . ":" . $value; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with scope reads"), + "static foreach eval should use the scope-aware EIR AOT function path:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "static foreach eval should synchronize loop variables through eval scope helpers:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static foreach eval should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_execute"), + "static foreach eval should not emit the eval execute runtime bridge:\n{runtime_asm}" + ); + assert!( + runtime_asm.contains("__elephc_eval_scope_set"), + "static foreach eval should emit core eval scope helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only static foreach eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "123:3|a:1;b:2;:b:2"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static empty foreach in eval uses AOT without publishing loop locals. +#[test] +fn test_literal_eval_static_empty_foreach_uses_eir_aot_without_scope_helpers() { + let dir = make_cli_test_dir("elephc_literal_eval_static_empty_foreach_aot"); + let source = r#" 10, "y" => 20]; +$key = "old-key"; +$value = 0; +eval('foreach ($pairs as $key => $value) { echo $key . ":" . $value . ";"; }'); +echo ":" . $key . ":" . $value . "|"; +$empty = []; +$kept = "keep"; +eval('foreach ($empty as $kept) { echo "bad"; }'); +echo $kept; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function with scope reads"), + "foreach over caller-scope arrays should use the scope-aware EIR AOT function path:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_get"), + "foreach over caller-scope arrays should read the source through eval scope helpers:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "foreach over caller-scope arrays should synchronize loop variables through eval scope helpers:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "foreach over caller-scope arrays should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_execute"), + "foreach over caller-scope arrays should not emit the eval execute runtime bridge:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only foreach over caller-scope arrays should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "ab:b|x:10;y:20;:y:20|keep"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies foreach over a caller scalar keeps the bridge fallback. +#[test] +fn test_literal_eval_foreach_scope_scalar_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_foreach_scope_scalar_bridge"); + let source = r#" 3; echo 4 >= 4; echo 5 != 6; echo 7 == 7 /// Verifies eval spaceship comparisons return boxed -1/0/1 integers. #[test] -fn test_eval_spaceship_execute_through_bridge() { +fn test_eval_spaceship_executes() { let out = compile_and_run( r#" 2; echo ":"; echo 2 <=> 2; echo ":"; echo 3 <=> 2;'); @@ -786,19 +5163,97 @@ fn test_eval_indexed_array_literal_and_read() { assert_eq!(out, "2"); } -/// Verifies eval accepts PHP's legacy `array(...)` literal syntax. +/// Verifies legacy `array(...)` static reads normalize and lower through eval EIR AOT. #[test] -fn test_eval_legacy_array_literal_executes_through_bridge() { - let out = compile_and_run( - r#" "Ada",)["name"];'); echo ":"; +$rows = eval('return ARRAY(array(10, 20), array("name" => "Ada"));'); +echo $rows[0][1] . ":" . $rows[1]["name"]; +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "legacy array syntax static reads should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "legacy array syntax static reads should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only legacy array syntax static reads should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only legacy array syntax static reads should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only legacy array syntax static reads should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "b:Ada:20:Ada"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies legacy `array(...)` next-key assignment can read the local array through EIR AOT. +#[test] +fn test_literal_eval_legacy_array_literal_next_key_scope_assignment_uses_eir_aot_scope_helpers() { + let dir = make_cli_test_dir("elephc_eval_legacy_array_next_key_aot_scope"); + let source = r#" "two", "tail",); echo $items[3];'); -"#, +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "legacy array next-key eval assignment should use EIR AOT:\n{user_asm}" + ); + assert!( + user_asm.contains("__elephc_eval_scope_set"), + "legacy array next-key eval assignment should write through eval scope helpers:\n{user_asm}" ); - assert_eq!(out, "b:Ada:tail"); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "legacy array next-key eval assignment should not execute through the bridge:\n{user_asm}" + ); + assert!( + runtime_asm.contains("__elephc_eval_scope_set"), + "legacy array next-key eval assignment should emit core eval scope helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "scope-only legacy array next-key eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "tail"); + let _ = fs::remove_dir_all(&dir); } /// Verifies eval indexed-array writes mutate an array visible to native code. @@ -1017,10 +5472,7 @@ echo ":" . (ptr_is_null($buf) ? "freed" : "live"); return ":" . function_exists("ptr_read16") . is_callable("ptr_write_string") . function_exists("buffer_new");'); "#, ); - assert_eq!( - out, - "4:123456789:255,1:4660:305419896:5:GET /:0:freed:111" - ); + assert_eq!(out, "4:123456789:255,1:4660:305419896:5:GET /:0:freed:111"); } /// Verifies eval `count()` dispatches through `Countable` for generated/AOT objects. @@ -1446,10 +5898,7 @@ echo array_unshift($class::${$staticName}, "u") . ":" . EvalArrayPopShiftPropert echo function_exists("array_pop") && function_exists("array_shift");'); "#, ); - assert_eq!( - out, - "3:2:2:1:2:3:4:5:2:5:6:2:6:q:1:p:2:r:s:t:2:u:t:1" - ); + assert_eq!(out, "3:2:2:1:2:3:4:5:2:5:6:2:6:q:1:p:2:r:s:t:2:u:t:1"); } /// Verifies eval `array_push()` and `array_unshift()` mutate writable lvalue arguments. @@ -1791,10 +6240,7 @@ echo ":"; echo function_exists("usort") && function_exists("uasort") && function_exists("uksort");'); "#, ); - assert_eq!( - out, - "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1:123:1:13:1:a2b1:1" - ); + assert_eq!(out, "ccc1:123:ccc1:b1c2a3:1:a2b1:c1:21:1:123:1:13:1:a2b1:1"); } /// Verifies eval iterator array helpers dispatch through direct and dynamic calls. @@ -4146,10 +8592,7 @@ echo isset($missing?->{eval_dynamic_write_bad()}) ? "bad" : "nullset"; echo "|"; echo empty($missing?->{eval_dynamic_write_bad()}) ? "nullempty" : "bad";'); "#, ); - assert_eq!( - out, - "name:value:Ada|set|empty|unset|nullset|nullempty" - ); + assert_eq!(out, "name:value:Ada|set|empty|unset|nullset|nullempty"); } /// Verifies eval-declared object properties can bind to local variables by reference. @@ -4716,7 +9159,11 @@ eval('define("EvalErrorContractConst", 1);'); echo eval('return define("EvalErrorContractConst", 2) ? "bad" : "ok";'); "#, ); - assert!(warning.success, "warning fixture should not fail: {}", warning.stderr); + assert!( + warning.success, + "warning fixture should not fail: {}", + warning.stderr + ); assert_eq!(warning.stdout, "ok"); assert!( warning @@ -4731,8 +9178,14 @@ echo eval('return define("EvalErrorContractConst", 2) ? "bad" : "ok";'); #[test] fn test_eval_bridge_failure_paths_do_not_leak_rust_panics() { for (source, expected) in [ - (" 0) { echo "D"; } else { echo "d"; } echo ":"; if (strlen(__FILE__) > strlen(__DIR__)) { echo "F"; } else { echo "f"; }'); -"#, +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval __FILE__/__DIR__ should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval __FILE__/__DIR__ should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only literal eval __FILE__/__DIR__ should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only literal eval __FILE__/__DIR__ should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only literal eval __FILE__/__DIR__ should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], ); assert_eq!(out, "D:F"); + let _ = fs::remove_dir_all(&dir); } -/// Verifies eval scope magic constants are empty even from namespaced method callers. +/// Verifies eval scope magic constants are empty through EIR AOT even from namespaced methods. #[test] -fn test_eval_scope_magic_constants_are_empty_strings() { - let out = compile_and_run( - r#"run(); -"#, +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "literal eval scope magic constants should use the internal EIR AOT function path:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "literal eval scope magic constants should not call the interpreter bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only literal eval scope magic constants should not reference eval bridge helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only literal eval scope magic constants should not emit eval bridge runtime helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only literal eval scope magic constants should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], ); - assert_eq!(out, "[||]"); + assert_eq!(out, "[||||]"); + let _ = fs::remove_dir_all(&dir); } /// Verifies eval trait methods expose PHP magic constants from the declaring trait. @@ -5196,10 +9715,7 @@ echo Box::aliasStat();'); "EvalBridgeTraitMagic\\Inner::stat|stat" ); - assert_eq!( - out, - expected - ); + assert_eq!(out, expected); } /// Verifies eval trait member defaults expose PHP magic constants through the bridge. @@ -7378,7 +11894,10 @@ echo ":" . ($case->getValue() === EvalAotReflectConstEnum::Ready ? "case" : "bad return "";'); "#, ); - assert_eq!(out, "OBIz:10:s:4:8:1:s:1:OWN:10:F:EvalAotReflectConstChild:s:case:7"); + assert_eq!( + out, + "OBIz:10:s:4:8:1:s:1:OWN:10:F:EvalAotReflectConstChild:s:case:7" + ); } /// Verifies eval ReflectionClass materializes generated/AOT float constant arithmetic. @@ -9452,10 +13971,7 @@ echo count($aliasUses) . ":"; echo $aliasUses["EvalRelationInnerTrait"];'); "#, ); - assert_eq!( - out, - "1:EvalRelationInnerTrait:1:EvalRelationInnerTrait" - ); + assert_eq!(out, "1:EvalRelationInnerTrait:1:EvalRelationInnerTrait"); } /// Verifies eval `instanceof` probes AOT and eval-declared class metadata. @@ -9790,10 +14306,7 @@ echo call_user_func_array([$box, "read"], ["value" => "L"]);'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!( - out.stdout, - "I:box:CD:box:EF:box:GH:box:IJ:M:box:K:box:L" - ); + assert_eq!(out.stdout, "I:box:CD:box:EF:box:GH:box:IJ:M:box:K:box:L"); } /// Verifies eval first-class callables retain access to inherited protected AOT methods. @@ -11726,7 +16239,10 @@ try { "program failed: stdout={:?} stderr={}", err.stdout, err.stderr ); - assert_eq!(err.stdout, "Error:Property EvalHookReadOnly::$answer is read-only"); + assert_eq!( + err.stdout, + "Error:Property EvalHookReadOnly::$answer is read-only" + ); } /// Verifies eval-declared by-reference get hook syntax reads through the accessor. @@ -15249,7 +19765,10 @@ echo count($instanceItems) . ":" . $instanceItems[0] . ":" . $instanceItems["nam "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!(out.stdout, "3:plain:Ada:value:3:plain:Ada:value:3:plain:Ada:value"); + assert_eq!( + out.stdout, + "3:plain:Ada:value:3:plain:Ada:value:3:plain:Ada:value" + ); } /// Verifies eval attribute array arguments preserve PHP-normalized scalar keys. @@ -15668,10 +20187,7 @@ echo count($aliases) . ":" . $aliases["aliasOriginal"];'); "program failed: stdout={:?} stderr={}", out.stdout, out.stderr ); - assert_eq!( - out.stdout, - "1:EvalAotReflectAliasTrait::original" - ); + assert_eq!(out.stdout, "1:EvalAotReflectAliasTrait::original"); } /// Verifies eval ReflectionClass exposes generated/AOT enum trait metadata. @@ -16367,10 +20883,7 @@ eval_aot_modifier_line("EvalAotModifierEnum"); '); "#, ); - assert_eq!( - out, - "Afre/64/ic:aFre/32/IC:afRe/65536/IC:aFrE/32/ic:" - ); + assert_eq!(out, "Afre/64/ic:aFre/32/IC:afRe/65536/IC:aFrE/32/ic:"); } /// Verifies eval ReflectionClass lifecycle predicates use generated/AOT lifecycle visibility. @@ -22556,7 +27069,10 @@ echo ":"; echo is_subclass_of($box, "DynEvalNativeIntrospectBase") ? "P" : "p"; "#, ); - assert_eq!(out, "DynEvalNativeIntrospectChild:DynEvalNativeIntrospectBase:C:B:s:P"); + assert_eq!( + out, + "DynEvalNativeIntrospectChild:DynEvalNativeIntrospectBase:C:B:s:P" + ); } /// Verifies native `instanceof` sees eval-declared class metadata after the barrier. From 6b0e0b0ced3ad4fc61d9650077e24f8428e40e2c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 20:39:14 +0200 Subject: [PATCH 1044/1208] bench(eval): add magician benchmark suite --- .github/workflows/ci.yml | 10 +- .plans/elephc-magician-performance-plan.md | 418 +++++++++++++++--- benchmarks/README.md | 6 + benchmarks/magician/README.md | 37 ++ .../magician/cases/algebra_heavy/eval.php | 2 + .../magician/cases/algebra_heavy/expected.txt | 1 + .../cases/algebra_heavy/metadata.json | 12 + .../magician/cases/algebra_heavy/native.php | 14 + .../magician/cases/arithmetic_loop/eval.php | 9 + .../cases/arithmetic_loop/expected.txt | 1 + .../cases/arithmetic_loop/metadata.json | 12 + .../magician/cases/arithmetic_loop/native.php | 8 + .../cases/array_reads_writes/eval.php | 3 + .../cases/array_reads_writes/expected.txt | 1 + .../cases/array_reads_writes/metadata.json | 12 + .../cases/array_reads_writes/native.php | 15 + .../magician/cases/builtin_calls/eval.php | 9 + .../magician/cases/builtin_calls/expected.txt | 1 + .../cases/builtin_calls/metadata.json | 12 + .../magician/cases/builtin_calls/native.php | 8 + .../magician/cases/callable_dispatch/eval.php | 12 + .../cases/callable_dispatch/expected.txt | 1 + .../cases/callable_dispatch/metadata.json | 19 + .../cases/callable_dispatch/native.php | 12 + .../cases/eval_declared_function/eval.php | 11 + .../cases/eval_declared_function/expected.txt | 1 + .../eval_declared_function/metadata.json | 19 + .../cases/eval_declared_function/native.php | 12 + .../magician/cases/include_repeated/eval.php | 9 + .../cases/include_repeated/expected.txt | 1 + .../cases/include_repeated/metadata.json | 12 + .../cases/include_repeated/native.php | 8 + .../magician/cases/include_repeated/piece.php | 2 + .../magician/cases/large_eval_loop/eval.php | 3 + .../cases/large_eval_loop/expected.txt | 1 + .../cases/large_eval_loop/metadata.json | 12 + .../magician/cases/large_eval_loop/native.php | 8 + .../cases/literal_scalar_aot/eval.php | 8 + .../cases/literal_scalar_aot/expected.txt | 1 + .../cases/literal_scalar_aot/metadata.json | 12 + .../cases/literal_scalar_aot/native.php | 8 + .../literal_scope_read_write_aot/eval.php | 8 + .../literal_scope_read_write_aot/expected.txt | 1 + .../metadata.json | 12 + .../literal_scope_read_write_aot/native.php | 8 + .../cases/repeated_small_eval/eval.php | 9 + .../cases/repeated_small_eval/expected.txt | 1 + .../cases/repeated_small_eval/metadata.json | 12 + .../cases/repeated_small_eval/native.php | 8 + .../magician/cases/string_output/eval.php | 9 + .../magician/cases/string_output/expected.txt | 1 + .../cases/string_output/metadata.json | 12 + .../magician/cases/string_output/native.php | 8 + scripts/benchmark_magician.py | 416 +++++++++++++++++ 54 files changed, 1215 insertions(+), 53 deletions(-) create mode 100644 benchmarks/magician/README.md create mode 100644 benchmarks/magician/cases/algebra_heavy/eval.php create mode 100644 benchmarks/magician/cases/algebra_heavy/expected.txt create mode 100644 benchmarks/magician/cases/algebra_heavy/metadata.json create mode 100644 benchmarks/magician/cases/algebra_heavy/native.php create mode 100644 benchmarks/magician/cases/arithmetic_loop/eval.php create mode 100644 benchmarks/magician/cases/arithmetic_loop/expected.txt create mode 100644 benchmarks/magician/cases/arithmetic_loop/metadata.json create mode 100644 benchmarks/magician/cases/arithmetic_loop/native.php create mode 100644 benchmarks/magician/cases/array_reads_writes/eval.php create mode 100644 benchmarks/magician/cases/array_reads_writes/expected.txt create mode 100644 benchmarks/magician/cases/array_reads_writes/metadata.json create mode 100644 benchmarks/magician/cases/array_reads_writes/native.php create mode 100644 benchmarks/magician/cases/builtin_calls/eval.php create mode 100644 benchmarks/magician/cases/builtin_calls/expected.txt create mode 100644 benchmarks/magician/cases/builtin_calls/metadata.json create mode 100644 benchmarks/magician/cases/builtin_calls/native.php create mode 100644 benchmarks/magician/cases/callable_dispatch/eval.php create mode 100644 benchmarks/magician/cases/callable_dispatch/expected.txt create mode 100644 benchmarks/magician/cases/callable_dispatch/metadata.json create mode 100644 benchmarks/magician/cases/callable_dispatch/native.php create mode 100644 benchmarks/magician/cases/eval_declared_function/eval.php create mode 100644 benchmarks/magician/cases/eval_declared_function/expected.txt create mode 100644 benchmarks/magician/cases/eval_declared_function/metadata.json create mode 100644 benchmarks/magician/cases/eval_declared_function/native.php create mode 100644 benchmarks/magician/cases/include_repeated/eval.php create mode 100644 benchmarks/magician/cases/include_repeated/expected.txt create mode 100644 benchmarks/magician/cases/include_repeated/metadata.json create mode 100644 benchmarks/magician/cases/include_repeated/native.php create mode 100644 benchmarks/magician/cases/include_repeated/piece.php create mode 100644 benchmarks/magician/cases/large_eval_loop/eval.php create mode 100644 benchmarks/magician/cases/large_eval_loop/expected.txt create mode 100644 benchmarks/magician/cases/large_eval_loop/metadata.json create mode 100644 benchmarks/magician/cases/large_eval_loop/native.php create mode 100644 benchmarks/magician/cases/literal_scalar_aot/eval.php create mode 100644 benchmarks/magician/cases/literal_scalar_aot/expected.txt create mode 100644 benchmarks/magician/cases/literal_scalar_aot/metadata.json create mode 100644 benchmarks/magician/cases/literal_scalar_aot/native.php create mode 100644 benchmarks/magician/cases/literal_scope_read_write_aot/eval.php create mode 100644 benchmarks/magician/cases/literal_scope_read_write_aot/expected.txt create mode 100644 benchmarks/magician/cases/literal_scope_read_write_aot/metadata.json create mode 100644 benchmarks/magician/cases/literal_scope_read_write_aot/native.php create mode 100644 benchmarks/magician/cases/repeated_small_eval/eval.php create mode 100644 benchmarks/magician/cases/repeated_small_eval/expected.txt create mode 100644 benchmarks/magician/cases/repeated_small_eval/metadata.json create mode 100644 benchmarks/magician/cases/repeated_small_eval/native.php create mode 100644 benchmarks/magician/cases/string_output/eval.php create mode 100644 benchmarks/magician/cases/string_output/expected.txt create mode 100644 benchmarks/magician/cases/string_output/metadata.json create mode 100644 benchmarks/magician/cases/string_output/native.php create mode 100644 scripts/benchmark_magician.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c0426a726..4cb545963b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -584,8 +584,14 @@ jobs: - name: Run benchmark suite run: python3 scripts/benchmark_suite.py --iterations 3 --warmup 1 --json benchmark-results.json --markdown benchmark-results.md + - name: Run magician benchmark suite + run: python3 scripts/benchmark_magician.py --iterations 3 --warmup 1 --json magician-benchmark-results.json --markdown magician-benchmark-results.md + - name: Publish benchmark summary - run: cat benchmark-results.md >> "$GITHUB_STEP_SUMMARY" + run: | + cat benchmark-results.md >> "$GITHUB_STEP_SUMMARY" + printf '\n## Magician eval benchmark suite\n\n' >> "$GITHUB_STEP_SUMMARY" + cat magician-benchmark-results.md >> "$GITHUB_STEP_SUMMARY" - name: Upload benchmark results uses: actions/upload-artifact@v4 @@ -594,3 +600,5 @@ jobs: path: | benchmark-results.json benchmark-results.md + magician-benchmark-results.json + magician-benchmark-results.md diff --git a/.plans/elephc-magician-performance-plan.md b/.plans/elephc-magician-performance-plan.md index 9c49730076..2b1724ae64 100644 --- a/.plans/elephc-magician-performance-plan.md +++ b/.plans/elephc-magician-performance-plan.md @@ -2,19 +2,19 @@ | Order | Work item | Objective | Status | Notes | |---:|---|---|---|---| -| 1 | Dedicated magician benchmark suite | Measure real hot spots before larger interpreter or bridge refactors | Not started | Establishes a stable baseline for parse, dispatch, scalar arithmetic, builtin calls, callable calls, arrays, references, and include paths. | +| 1 | Dedicated magician benchmark suite | Measure real hot spots before larger interpreter or bridge refactors | Done | Added `scripts/benchmark_magician.py` plus fixture cases under `benchmarks/magician/cases/`; CI now publishes magician benchmark artifacts. | | 2 | Eval fragment parse cache | Avoid repeated tokenization and parsing for identical runtime source bytes | Done | Implemented in commit `4ba0efb5c` through `crates/elephc-magician/src/parse_cache.rs`. | | 3 | Parse-error cache | Avoid reparsing repeated invalid fragments | Done | Included in the same parse cache as successful `EvalProgram` results. | | 4 | Dynamic context/scope preservation for cached parses | Ensure caching does not freeze magic constants, variables, declarations, or runtime state | Done | The cache stores only immutable parse results; execution still receives the current `ElephcEvalContext` and `ElephcEvalScope`. | -| 5 | Include parse cache | Avoid repeated parsing of identical PHP code blocks loaded through include/require | Partially done | The parse cache now covers parsed include code blocks, but file reads, stat/canonicalize work, and include-path resolution are not cached. | -| 6 | Eval symbol lookup cache | Speed up repeated function/class/method lookup for symbols declared by eval | Not started | Should come after parse caching and before deeper interpreter changes. | -| 7 | Direct builtin dispatch | Avoid repeated string matching and generic dispatch for common builtins | Not started | Needs benchmark data to prioritize the first builtin groups. | -| 8 | Callable resolution cache | Avoid reconstructing equivalent string/array/first-class/closure callables repeatedly | Not started | Best implemented after symbol and builtin lookup behavior is stable. | -| 9 | Reduce `RuntimeValueOps` calls on simple operations | Cut bridge overhead for arithmetic, comparisons, casts, and simple output paths | Not started | This starts touching the interpreter core and needs before/after benchmark evidence. | -| 10 | Unboxed scalar fast paths | Avoid boxing/unboxing for hot int/float/bool/string paths inside eval execution | Not started | Likely high impact for compute-heavy loops, but more invasive than dispatch caches. | -| 11 | Compact bytecode or linear EvalIR form | Reduce tree-walk overhead and branch-heavy dispatch in the current EvalIR interpreter | Not started | Should be guided by benchmarks after simpler caches and scalar fast paths are measured. | -| 12 | Array/reference/COW bridge optimizations | Reduce cost of array mutation and by-reference parameter handling | Not started | Semantically delicate; should happen after scalar and dispatch fast paths are stable. | -| 13 | AOT for literal `eval` | Compile `eval('...')` fragments ahead of time instead of interpreting them through magician | Partial | Commit `4bc962407` marks literal eval calls as AOT candidates, but still uses the bridge fallback. | +| 5 | Include parse cache | Avoid repeated parsing of identical PHP code blocks loaded through include/require | Done | Added a metadata-validated include-file cache for file bytes and PHP block splitting; include_once state remains context-local and stale-prone missing/path checks stay live. | +| 6 | Eval symbol lookup cache | Speed up repeated function/class/method lookup for symbols declared by eval | Done | Added per-context caches for dynamic/native functions, constants, class-like symbols, aliases, and eval class methods. | +| 7 | Direct builtin dispatch | Avoid repeated string matching and generic dispatch for common builtins | Done | Added a hot positional direct-dispatch layer for benchmark-relevant scalar/core builtins while named/spread calls still use generic binding. | +| 8 | Callable resolution cache | Avoid reconstructing equivalent string/array/first-class/closure callables repeatedly | Done | Added bounded stable string-callable normalization caching; object, array, first-class, and scope-sensitive special-class callables still resolve live. | +| 9 | Reduce `RuntimeValueOps` calls on simple operations | Cut bridge overhead for arithmetic, comparisons, casts, and simple output paths | Done | Added a combined non-object echo bridge so scalar/null/array output avoids the separate `type_tag` call; arithmetic coercion shortcuts remain deferred to unboxed scalar work. | +| 10 | Unboxed scalar fast paths | Avoid boxing/unboxing for hot int/float/bool/string paths inside eval execution | Done | Added a conservative int/bool temporary evaluator for pure assignment, return, and condition boundaries; scope cells remain boxed and risky coercions fall back to runtime hooks. | +| 11 | Compact bytecode or linear EvalIR form | Reduce tree-walk overhead and branch-heavy dispatch in the current EvalIR interpreter | Done | Added an optional cached straight-line linear executable with a small stack VM; loops, declarations, includes, OOP, short-circuit expressions, and other complex forms stay on EvalIR fallback. | +| 12 | Array/reference/COW bridge optimizations | Reduce cost of array mutation and by-reference parameter handling | Done | Added narrow integer-key array write fast paths for eval scope-variable indexed append/set; associative append, references, and broad COW behavior remain on the existing semantic path. | +| 13 | AOT for literal `eval` | Compile `eval('...')` fragments ahead of time instead of interpreting them through magician | Done | Added conservative literal AOT for scalar constants, scalar output/returns/stores, and boxed Mixed scope reads/read-modify-writes; unsupported builtins, declarations, includes, OOP, and control flow keep the bridge fallback. | ## 1. Dedicated Magician Benchmark Suite @@ -43,6 +43,22 @@ The suite should include both microbenchmarks and a few mixed workloads: - Callable dispatch inside eval. - Include/require with repeated code blocks. +### Current State + +Done. + +The implementation adds: + +- `scripts/benchmark_magician.py` +- `benchmarks/magician/README.md` +- `benchmarks/magician/cases/*` + +The suite compares native elephc, elephc through magician eval, native PHP, and +PHP through eval. It records runtime wall clock, eval invocation counts, +fragment sizes, literal-vs-dynamic source shape, parse-cache expectations, and +stdout correctness. The existing benchmark CI job also runs the magician suite +and uploads markdown/JSON artifacts. + ### Likely Files - `benches/` or `scripts/bench-eval-*` depending on existing project conventions. @@ -60,6 +76,13 @@ Each benchmark should record: - Whether parse cache should hit. - Output correctness against PHP where practical. +Validation run after implementation: + +- `python3 -m py_compile scripts/benchmark_magician.py` +- `python3 scripts/benchmark_magician.py --list` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case repeated_small_eval --json /tmp/elephc-magician-bench.json --markdown /tmp/elephc-magician-bench.md` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --json /tmp/elephc-magician-bench-all.json --markdown /tmp/elephc-magician-bench-all.md` + ### Risks Benchmark results can be misleading if compile+assemble+link time is included for runtime comparisons. Runtime-only binaries should be generated once and executed repeatedly. @@ -174,19 +197,23 @@ Avoid reparsing identical PHP code blocks loaded through include/require. ### Current State -Partially done. +Done. -The current parse cache is used by `eval_execute_include_code()`, so the parsed PHP code block can be reused if the exact source bytes match. +The parse cache is used by `eval_execute_include_code()`, so parsed PHP code +blocks are reused when exact source bytes match. -### Remaining Work +The implementation now also adds a bounded process-local include-file cache in +`crates/elephc-magician/src/interpreter/include_exec.rs`. Cache entries store: -The following costs are not cached yet: +- Canonical include key for `include_once`/`require_once`. +- File metadata used to reject stale entries. +- File bytes. +- Precomputed raw/PHP-code block ranges for `` split scanning. -- File read bytes. -- `canonicalize()` for include-once keys. -- Include path resolution. -- Open/stat checks. -- Split scanning for multiple `` blocks in one file. +The cache deliberately keeps missing-file checks, cwd-first resolution checks, +and caller-directory fallback checks live. Caching those negative/path decisions +would hide files created later at runtime or violate PHP's cwd-first include +order. `include_once` state remains in `ElephcEvalContext`. ### Likely Files @@ -201,6 +228,10 @@ The following costs are not cached yet: 4. Keep include_once semantics in `ElephcEvalContext`; do not move "already included" behavior into a global cache. 5. Ensure `__FILE__` and `__DIR__` still come from the current include path, not from cached source metadata. +All five steps are implemented. The benchmark suite added in point 1 includes +`include_repeated`, and the cache uses the current resolved include path when +executing cached bytes so magic constants remain call-site sensitive. + ### Validation Run focused include tests: @@ -210,6 +241,17 @@ Run focused include tests: - `execute_program_missing_include_warns_and_returns_false` - `execute_program_missing_require_is_runtime_fatal` +Validation run after implementation: + +- `cargo test -p elephc-magician execute_program_include_uses_call_site_and_returns_file_result` +- `cargo test -p elephc-magician execute_program_include_once_skips_regularly_included_file` +- `cargo test -p elephc-magician execute_program_regular_include_observes_modified_file_after_cache_hit` +- `cargo test -p elephc-magician execute_program_missing_include_warns_and_returns_false` +- `cargo test -p elephc-magician execute_program_missing_require_is_runtime_fatal` +- `cargo test --test codegen_tests test_eval_fragment_include_once_and_plain_file` +- `cargo test --test codegen_tests test_eval_fragment_include_executes_php_file_and_returns_value` +- `cargo test --test codegen_tests test_eval_fragment_missing_require_fails` + ### Risks File caches can easily become stale. A conservative first version should cache parsed source bytes only within one process and avoid hiding file changes unless PHP-compatible behavior is explicitly defined. @@ -240,6 +282,22 @@ Once parsing is cached, repeated dynamic calls may still pay for name normalizat 5. Keep case-insensitive PHP behavior canonical. 6. Preserve namespace fallback for builtins and user symbols. +### Current State + +Done. + +The implementation adds a per-context `EvalSymbolLookupCache` in +`crates/elephc-magician/src/context.rs`. It caches: + +- Dynamic/native function classification. +- Dynamic constant key resolution. +- Class-like resolution for classes, interfaces, traits, enums, and aliases. +- Inherited and directly declared eval class method lookups. + +The cache is protected by a poison-recovering `Mutex` so FFI `catch_unwind` +wrappers remain unwind-safe. Declarations and alias registration clear the +affected cache families, so cached misses do not hide later eval declarations. + ### Validation Add tests for: @@ -249,6 +307,20 @@ Add tests for: - Namespaced calls with builtin fallback. - `function_exists`, `class_exists`, and reflection seeing updated declarations after eval. +Validation run after implementation: + +- `cargo test -p elephc-magician function_exists_sees_function_declared_after_cached_miss` +- `cargo test -p elephc-magician constant_exists_sees_constant_defined_after_cached_miss` +- `cargo test -p elephc-magician dynamic_class_exists_sees_alias_declared_after_cached_miss` +- `cargo test -p elephc-magician class_method_lookup_sees_class_declared_after_cached_miss` +- `cargo test -p elephc-magician function_exists_reports_declared_eval_function` +- `cargo test -p elephc-magician dynamic_class_exists_reports_declared_eval_class` +- `cargo test -p elephc-magician execute_program_class_alias_registers_aliases` +- `cargo test -p elephc-magician execute_program_static_callable_array_dispatches_eval_method` +- `cargo test --test codegen_tests test_function_exists_sees_builtins_and_eval_declared_functions_after_eval` +- `cargo test --test codegen_tests test_eval_declared_function_can_be_called_with_call_user_func` +- `cargo test --test codegen_tests test_eval_function_exists_builtin_case_insensitive` + ### Risks Incorrect caching here can make declarations invisible or make duplicate declarations appear valid. This must be treated as a semantic change, not a pure optimization. @@ -278,6 +350,27 @@ Eval currently supports many builtins through interpreter dispatch. If a hot loo 4. Preserve PHP case-insensitivity and namespace fallback. 5. Ensure direct dispatch and generic dispatch share argument validation. +### Current State + +Done. + +The benchmark suite's `builtin_calls` case stresses repeated `strlen()` and +`intval()` inside cached eval fragments, so the first direct path targets that +hot scalar/core family. The implementation adds +`crates/elephc-magician/src/interpreter/builtins/registry/direct.rs` with a +compact `EvalDirectBuiltin` selector for: + +- `strlen` +- `intval`, `floatval`, `strval`, `boolval` +- `count` +- `ord` +- `abs` + +The fast path only accepts plain positional direct calls. Named arguments, +spread arguments, dynamic callables, first-class callables, and unknown names +fall through to the existing generic builtin binding/dispatch path, so argument +validation and callable semantics remain centralized. + ### Validation For each optimized builtin group, add parity tests for: @@ -288,6 +381,22 @@ For each optimized builtin group, add parity tests for: - Named arguments when supported. - First-class callable and callable aliases when relevant. +Validation run after implementation: + +- `cargo test -p elephc-magician execute_program_dispatches_hot_direct_builtins_with_generic_fallbacks` +- `cargo test -p elephc-magician execute_program_dispatches_cast_builtins` +- `cargo test -p elephc-magician execute_program_dispatches_abs_builtin` +- `cargo test -p elephc-magician execute_program_dispatches_ord_builtin` +- `cargo test -p elephc-magician execute_program_counts_eval_countable_objects` +- `cargo test -p elephc-magician execute_program_namespace_call_falls_back_to_builtin` +- `cargo test -p elephc-magician execute_program_namespace_function_overrides_builtin_fallback` +- `cargo test -p elephc-magician execute_program_first_class_callables_dispatch_functions_and_methods` +- `cargo test --test codegen_tests test_eval_dispatches_simple_builtin_calls` +- `cargo test --test codegen_tests test_eval_dispatches_cast_builtin_calls` +- `cargo test --test codegen_tests test_eval_dispatches_abs_builtin_call` +- `cargo test --test codegen_tests test_namespaced_calls_fall_back_to_builtin_before_and_after_eval` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case builtin_calls --json /tmp/elephc-magician-builtin-bench.json --markdown /tmp/elephc-magician-builtin-bench.md` + ### Risks A second builtin registry can drift from the canonical catalog. Any direct dispatch table must be generated from or tightly tied to the existing metadata source. @@ -317,6 +426,30 @@ Callable semantics now cover string callables, array callables, first-class call 4. Keep bound object callables separate from static function/class callables. 5. Do not cache by raw runtime-cell pointer unless ownership and lifetime are explicit. +### Current State + +Done. + +The implementation adds a bounded per-context string-callable normalization cache +to `ElephcEvalContext`. Cached entries cover stable callback strings only: + +- Function strings such as `"strlen"` or `"eval_declared_fn"`. +- Ordinary static method strings such as `"ClassName::method"`. + +The cache stores normalized `Named` or `StaticMethod` callable metadata, not a +validated callable target. Invocation and `is_callable()` still perform live +symbol/method checks, so cached misses do not hide later eval declarations. + +These forms deliberately bypass the cache: + +- Object callables. +- Callable arrays. +- First-class callable/Closure objects. +- Scope-sensitive `self::`, `static::`, and `parent::` string callables. + +That keeps `$this`, called-class, lexical scope, and runtime-cell lifetime rules +owned by the existing live resolver. + ### Validation Add tests for repeated: @@ -328,6 +461,19 @@ Add tests for repeated: - `Closure::fromCallable`. - By-reference callable arguments. +Validation run after implementation: + +- `cargo test -p elephc-magician execute_program_string_callable_cache_sees_late_function_declaration` +- `cargo test -p elephc-magician execute_program_string_callable_cache_sees_late_static_method_declaration` +- `cargo test -p elephc-magician execute_program_call_user_func_dispatches_builtin` +- `cargo test -p elephc-magician execute_program_callable_array_variable_dispatches_object_method` +- `cargo test -p elephc-magician execute_program_static_callable_array_dispatches_eval_method` +- `cargo test -p elephc-magician execute_program_first_class_callables_dispatch_functions_and_methods` +- `cargo test -p elephc-magician execute_program_static_runtime_callables_write_back_by_ref_type_coercion` +- `cargo test --test codegen_tests test_eval_closure_from_callable_special_string_callables_preserve_by_ref_writeback` +- `cargo test --test codegen_tests test_eval_declared_callable_forms_preserve_by_ref_writeback` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case callable_dispatch --json /tmp/elephc-magician-callable-bench.json --markdown /tmp/elephc-magician-callable-bench.md` + ### Risks Bound method callables can carry `$this`, visibility scope, or closure binding state. Caching must not reuse a callable across an incompatible object or visibility context. @@ -342,6 +488,30 @@ Reduce the number of runtime bridge calls needed for simple operations inside ev Even after parse and dispatch are cached, simple arithmetic can still be slow if each operator repeatedly boxes, unboxes, allocates, or crosses through generic runtime hooks. +### Current State + +Done. + +The implementation adds `RuntimeValueOps::echo_non_object()` and a generated +`__elephc_eval_value_echo_non_object` bridge for ARM64 and x86_64. Eval `echo` +statements and `print` expressions now try the combined non-object path first: +scalar/null/array output is handled in one runtime bridge call, while object +values still defer to the existing interpreter `__toString()` dispatch before +emitting the result. + +Before this change, common scalar output paid one bridge call for `type_tag()` +and one for `echo()`. The fast path now uses one bridge call for the same +non-object cases. PHP numeric operation shortcuts were intentionally left on +the generic runtime hooks because exact scalar coercion and error/fatal +semantics are subtle enough to belong with the unboxed scalar fast-path work. + +Benchmark smoke after the change: + +```text +python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case string_output --json /tmp/elephc-magician-output-bench.json --markdown /tmp/elephc-magician-output-bench.md +string_output | 1200 evals | elephc native 310.35 ms | elephc eval 395.56 ms | eval/native 1.27x +``` + ### Likely Files - `crates/elephc-magician/src/interpreter/expressions.rs` @@ -359,14 +529,18 @@ Even after parse and dispatch are cached, simple arithmetic can still be slow if ### Validation -Add focused interpreter tests for: - -- Integer arithmetic. -- Float arithmetic. -- String numeric coercion. -- Division/modulo edge cases. -- Boolean comparisons. -- Error/fatal paths. +Validation run after implementation: + +- `cargo test -p elephc-magician execute_program_print_returns_one` +- `cargo test -p elephc-magician execute_program_echoes_and_unsets_scope_value` +- `cargo test -p elephc-magician execute_program_evaluates_division_and_modulo` +- `cargo test -p elephc-magician execute_program_evaluates_exponentiation` +- `cargo test -p elephc-magician execute_program_echoes_comma_list` +- `cargo test --test codegen_tests test_eval_output_fast_path_preserves_scalar_and_object_echo` +- `cargo test --test codegen_tests test_eval_scalar_add_executes_through_bridge` +- `./scripts/test-linux-x86_64.sh test_eval_output_fast_path_preserves_scalar_and_object_echo` +- `./scripts/test-linux-arm64.sh test_eval_output_fast_path_preserves_scalar_and_object_echo` +- `git diff --check` ### Risks @@ -382,6 +556,33 @@ Avoid boxing and unboxing hot scalar values inside eval loops. Compute-heavy eval programs are likely dominated by repeated scalar loads, arithmetic, comparisons, and stores. Keeping scalars in a compact internal representation can reduce allocation and bridge overhead. +### Current State + +Done. + +The implementation adds `crates/elephc-magician/src/interpreter/unboxed.rs`, +which evaluates small pure int/bool expression trees into temporary +`EvalUnboxedScalar` values. The fast path is wired only at explicit boundaries: + +- `StoreVar` boxes once before writing the scope cell. +- `return expr` boxes once before returning. +- `if`, `while`, `do/while`, and `for` conditions use unboxed truthiness when + the condition is a pure supported scalar expression. + +Scope-visible values remain runtime cells. The unboxed evaluator reads only +visible int/bool scope cells and handles checked integer add/sub/mul/mod, +integer comparisons, same-kind equality, logical not/and/or/xor, and integer +unary plus/minus. Overflow, division/modulo-by-zero, strings, floats, arrays, +objects, calls, refs, and other PHP-coercion-sensitive cases return `None` and +fall back to the existing runtime hooks. + +Benchmark smoke after the change: + +```text +python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case arithmetic_loop --json /tmp/elephc-magician-arithmetic-bench.json --markdown /tmp/elephc-magician-arithmetic-bench.md +arithmetic_loop | 4000 evals | elephc native 296.28 ms | elephc eval 396.30 ms | eval/native 1.34x +``` + ### Likely Files - `crates/elephc-magician/src/value.rs` @@ -401,14 +602,17 @@ Compute-heavy eval programs are likely dominated by repeated scalar loads, arith ### Validation -Create benchmark and correctness coverage for: +Validation run after implementation: -- Integer loops. -- Float loops. -- Mixed scalar expressions. -- Assignments back into scope. -- Function calls that force boxing. -- Early return and throwable cleanup. +- `cargo test -p elephc-magician execute_program_unboxes_integer_store_expression_until_assignment` +- `cargo test -p elephc-magician execute_program_unboxed_integer_store_falls_back_for_modulo_by_zero` +- `cargo test -p elephc-magician execute_program_evaluates_compound_assignments` +- `cargo test -p elephc-magician execute_program_evaluates_division_and_modulo` +- `cargo test --test codegen_tests test_eval_unboxed_integer_store_expression_executes_through_bridge` +- `cargo test --test codegen_tests test_eval_division_modulo_execute_through_bridge` +- `cargo test --test codegen_tests test_eval_for_loop_uses_less_than_condition` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case arithmetic_loop --json /tmp/elephc-magician-arithmetic-bench.json --markdown /tmp/elephc-magician-arithmetic-bench.md` +- `git diff --check` ### Risks @@ -440,6 +644,23 @@ The current EvalIR is a structured tree. A compact linear representation can imp 5. Add loops and control flow after linear basic blocks are proven. 6. Keep declarations and complex OOP constructs on the existing EvalIR path until needed. +### Current State + +Done. + +The implementation adds `crates/elephc-magician/src/eval_linear.rs` and stores +`CachedEvalFragment { program, linear }` entries in the parse cache. Linear +lowering is intentionally conservative and currently accepts straight-line +fragments made of assignment, echo, print, expression statements, and return +over constants, variable loads, unary operators, and non-short-circuit binary +operators. + +The interpreter executes the lowered form through a small stack machine in +`crates/elephc-magician/src/interpreter/statements.rs`. Int/bool stack values +stay unboxed where the point-10 scalar rules apply, and values are boxed at +store, echo, pop, and return boundaries. Unsupported fragments continue through +the existing EvalIR interpreter. + ### Validation Run parity tests with both execution engines if a temporary dual path exists: @@ -448,6 +669,19 @@ Run parity tests with both execution engines if a temporary dual path exists: - Focused codegen eval tests. - PHP cross-checks for edge cases. +Validation run after implementation: + +- `cargo test -p elephc-magician linear_lowering` +- `cargo test -p elephc-magician execute_prepared_program_uses_linear_unboxed_scalar_path` +- `cargo test --test codegen_tests test_eval_linear_cached_fragment_and_evalir_fallback_execute_through_bridge` +- `cargo test --test codegen_tests test_eval_unboxed_integer_store_expression_executes_through_bridge` +- `cargo test --test codegen_tests test_eval_for_loop_uses_less_than_condition` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case repeated_small_eval --json /tmp/elephc-magician-linear-bench.json --markdown /tmp/elephc-magician-linear-bench.md` + +Benchmark smoke result: + +- `repeated_small_eval`: 5000 evals, elephc native `329.19 ms`, elephc eval `378.11 ms`, eval/native `1.15x`. + ### Risks This is a larger architectural change. It should not happen before benchmark data proves tree dispatch is a real bottleneck after parse caching and scalar fast paths. @@ -478,6 +712,25 @@ Array and reference-heavy eval code can be expensive because correctness require 4. Keep reference binding and mutation behavior covered by regression tests. 5. Add cleanup tests for normal return, fatal, and throwable paths. +### Current State + +Done for the low-risk indexed-array write subset. + +The implementation adds `RuntimeValueOps::array_set_int_index()` with a +production bridge wrapper, `__elephc_eval_value_array_set_int_index`, emitted +for both ARM64 and x86_64. Eval scope-variable writes now use this hook when: + +- `$array[] = value` targets an indexed array or a newly created array. The + current length is read before evaluating the RHS, preserving the existing + source-order behavior. +- `$array[$i] = value` has a pure unboxed integer index. + +Associative-array append still uses `eval_array_append_key()` so PHP's +largest-integer-key behavior is preserved. String keys, side-effecting index +expressions, object `ArrayAccess`, by-reference writeback, property/static +array writes, and broad COW/reference handling continue through the existing +generic paths. + ### Validation Add tests for: @@ -489,6 +742,20 @@ Add tests for: - Aliasing across eval and native code. - Cleanup after fatal and uncaught throwable. +Validation run after implementation: + +- `cargo test -p elephc-magician execute_program_appends_indexed_scope_array_with_direct_int_key_write` +- `cargo test -p elephc-magician execute_program_sets_scope_array_integer_indexes_with_direct_int_key_write` +- `cargo test -p elephc-magician execute_program_appends_assoc_scope_array` +- `cargo test --test codegen_tests test_eval_indexed_array_append_is_visible_after_eval` +- `cargo test --test codegen_tests test_eval_indexed_array_write_is_visible_after_eval` +- `cargo test --test codegen_tests test_eval_assoc_array_append_uses_php_next_key` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case array_reads_writes --json /tmp/elephc-magician-array-bench.json --markdown /tmp/elephc-magician-array-bench.md` + +Benchmark smoke result: + +- `array_reads_writes`: 1 eval, elephc native `348.16 ms`, elephc eval `403.57 ms`, eval/native `1.16x`. + ### Risks This area has high semantic risk. Incorrect optimization can create stale aliases, missed mutations, double frees, or leaks. @@ -501,9 +768,15 @@ Compile literal eval fragments ahead of time so `eval('...')` can bypass the mag ### Current State -Partial. +Done for the conservative eligibility slice implemented in this plan. -Commit `4bc962407` marks literal eval calls as AOT candidates through the EIR opcode `EvalLiteralCall`, but the backend still emits the bridge fallback to `__elephc_eval_execute`. +Literal eval calls still lower to the EIR opcode `EvalLiteralCall`, but the +backend now parses the literal fragment at compile time and directly emits +native code for eligible fragments. The current AOT subset accepts scalar +constants, simple scalar arithmetic, scalar concat, scalar `echo` / `print`, +scalar `return`, scalar variable stores, and boxed Mixed scope reads used by +read-modify-write expressions. Unsupported fragments keep the bridge fallback +to `__elephc_eval_execute`. ### Motivation @@ -522,30 +795,71 @@ This is the largest potential speedup for literal eval because the code can beco ### Implementation Plan -1. Keep the existing fallback bridge as the compatibility path. -2. Parse literal fragments at compile time. +1. Keep the existing fallback bridge as the compatibility path. Done. +2. Parse literal fragments at compile time. Done for the scalar AOT subset. 3. Run the same frontend passes needed for normal PHP code where possible. -4. Reject or fall back for constructs that require runtime-only context. -5. Lower eligible literal eval fragments into EIR. -6. Materialize eval-visible scope reads/writes through the same dynamic-scope bridge used by runtime eval. + Deferred for broader eligibility; the current subset accepts only syntax + that can be evaluated conservatively without cross-pass rewrites. +4. Reject or fall back for constructs that require runtime-only context. Done. +5. Lower eligible literal eval fragments into native codegen. Done for the + conservative scalar and scope read/write subset through the `EvalLiteralCall` + backend path. +6. Materialize eval-visible scope reads/writes through the same dynamic-scope + bridge used by runtime eval. Done for scalar variable stores and boxed Mixed + scope reads used by supported read-modify-write expressions. 7. Preserve eval return semantics: `return` exits eval, not the caller function. + Done for scalar returns. 8. Preserve declaration side effects for functions/classes declared by eval. -9. Add diagnostics or assembly markers showing whether a literal eval used AOT or fallback. -10. Expand eligibility gradually. + Done by falling back for declarations. +9. Add diagnostics or assembly markers showing whether a literal eval used AOT + or fallback. Done. +10. Expand eligibility gradually. Scope read/read-modify-write expressions are + now included; builtins, declarations, includes, OOP, control flow, and full + frontend-pass lowering remain on the compatibility fallback. ### Validation Add parity tests for: -- Literal eval assigning existing variables. -- Literal eval creating variables. -- Literal eval returning values. -- Literal eval with output. -- Literal eval declarations. -- Literal eval using builtins. -- Literal eval inside functions and methods. -- Fallback when unsupported syntax is present. -- No magician link for programs without eval. +- Literal eval assigning existing variables. Covered by scalar store and + existing native visibility tests. +- Literal eval creating variables. Covered. +- Literal eval returning values. Covered. +- Literal eval with output. Covered. +- Literal eval declarations. Covered by fallback behavior; direct declaration + AOT remains unsupported. +- Literal eval using builtins. Covered by fallback behavior; direct builtin AOT + remains unsupported. +- Literal eval inside functions and methods. Existing bridge tests cover the + fallback path; direct scalar AOT uses the same function-local eval scope. +- Fallback when unsupported syntax is present. Covered. +- No magician link for programs without eval. Covered. + +Validation run after implementation: + +- `cargo test --test codegen_tests test_literal_eval_scalar_return_uses_aot_without_execute_bridge` +- `cargo test --test codegen_tests test_literal_eval_scalar_store_uses_aot_scope_write` +- `cargo test --test codegen_tests test_literal_eval_scope_read_write_uses_aot_without_execute_bridge` +- `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge` +- `cargo test --test codegen_tests test_dynamic_eval_does_not_emit_literal_aot_marker` +- `cargo test --test codegen_tests test_eval_return_value_is_available_to_native_code` +- `cargo test --test codegen_tests test_eval_created_variable_is_visible_after_eval` +- `cargo test --test codegen_tests test_eval_scope_persists_between_eval_calls` +- `cargo test --test codegen_tests test_eval_can_change_existing_local_type` +- `cargo test --test codegen_tests test_eval_scalar_add_executes_through_aot` +- `cargo test --test codegen_tests test_eval_scalar_echo_executes_through_aot` +- `cargo test --test codegen_tests test_non_eval_program_does_not_request_eval_bridge` +- `cargo test --test codegen_tests test_eval_reads_and_writes_existing_local` +- `cargo test --test codegen_tests test_eval_return_and_scope_write_are_visible` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case literal_scalar_aot --json /tmp/elephc-magician-literal-aot-bench.json --markdown /tmp/elephc-magician-literal-aot-bench.md` +- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case literal_scope_read_write_aot --json /tmp/elephc-magician-literal-scope-aot-bench.json --markdown /tmp/elephc-magician-literal-scope-aot-bench.md` + +Focused benchmark evidence: + +- `literal_scalar_aot`: 5000 literal scalar eval returns, eval `408.72 ms` + versus native `436.25 ms` (`0.94x`). +- `literal_scope_read_write_aot`: 5000 literal eval scope read/write + invocations, eval `365.63 ms` versus native `316.49 ms` (`1.16x`). ### Risks diff --git a/benchmarks/README.md b/benchmarks/README.md index 8964a3474b..0275a0ca09 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -6,6 +6,12 @@ This directory contains small, deterministic benchmark programs used to compare: - the PHP interpreter - equivalent `C` implementations +For the focused `eval()`/`elephc-magician` benchmarks, see +[`magician/README.md`](magician/README.md). That suite compares native elephc, +elephc through runtime eval, native PHP, and PHP through eval while recording +eval invocation counts, fragment size, literal-vs-dynamic source shape, and +parse-cache expectations. + Run the suite with: ```bash diff --git a/benchmarks/magician/README.md b/benchmarks/magician/README.md new file mode 100644 index 0000000000..df445ed56b --- /dev/null +++ b/benchmarks/magician/README.md @@ -0,0 +1,37 @@ +# Magician Eval Benchmark Suite + +This directory contains focused runtime benchmarks for the optional +`elephc-magician` eval bridge. The cases compare four paths for the same +workload: + +- `elephc`-compiled native PHP without `eval` +- `elephc`-compiled PHP that enters magician through `eval` +- PHP interpreter execution without `eval` +- PHP interpreter execution through `eval` + +Run the suite with: + +```bash +python3 scripts/benchmark_magician.py +``` + +Useful options: + +- `--iterations N` to control measured runs per variant +- `--warmup N` to control warmup runs per variant +- `--case NAME` to run a single benchmark +- `--list` to show available cases +- `--json PATH` to write machine-readable results +- `--markdown PATH` to write the markdown summary table + +The runner builds `target/release/elephc` and `libelephc_magician.a` when +needed, compiles each `native.php` and `eval.php` fixture once in an isolated +temporary directory, then measures only repeated binary/PHP execution. Each run +checks stdout against `expected.txt`. When PHP is installed, the PHP native and +eval variants are checked against the same output so the benchmark doubles as a +small parity guard. + +Every case has a `metadata.json` file describing eval invocation counts, +fragment source size, literal-vs-dynamic source shape, and whether the parse +cache should hit. The JSON output preserves these fields so timing artifacts can +be interpreted without reopening the fixture. diff --git a/benchmarks/magician/cases/algebra_heavy/eval.php b/benchmarks/magician/cases/algebra_heavy/eval.php new file mode 100644 index 0000000000..5e3fb86bbd --- /dev/null +++ b/benchmarks/magician/cases/algebra_heavy/eval.php @@ -0,0 +1,2 @@ + int: + return len(self.source.encode()) + + +@dataclass +class CaseMetadata: + name: str + description: str + fragments: list[EvalFragment] + + @property + def eval_invocations(self) -> int: + return sum(fragment.invocations for fragment in self.fragments) + + @property + def max_fragment_size_bytes(self) -> int: + if not self.fragments: + return 0 + return max(fragment.size_bytes for fragment in self.fragments) + + @property + def parse_cache_summary(self) -> str: + if self.eval_invocations == 0: + return "n/a" + if any(fragment.parse_cache_should_hit for fragment in self.fragments): + return "hit expected" + return "no hit expected" + + @property + def source_kind_summary(self) -> str: + kinds = {fragment.source_kind for fragment in self.fragments if fragment.invocations > 0} + if not kinds: + return "n/a" + if len(kinds) == 1: + return next(iter(kinds)) + return "mixed" + + +@dataclass +class CommandResult: + label: str + median_ms: float | None + samples_ms: list[float] + detail: str + + +@dataclass +class BenchmarkResult: + case: str + metadata: CaseMetadata + elephc_native: CommandResult + elephc_eval: CommandResult + php_native: CommandResult + php_eval: CommandResult + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run the elephc magician/eval benchmark suite.") + parser.add_argument("--iterations", type=int, default=5, help="Measured runs per benchmark variant.") + parser.add_argument("--warmup", type=int, default=1, help="Warmup runs per benchmark variant.") + parser.add_argument( + "--case", + action="append", + default=[], + help="Magician benchmark case name to run. Can be passed multiple times.", + ) + parser.add_argument("--json", type=Path, help="Write machine-readable benchmark results to this path.") + parser.add_argument("--markdown", type=Path, help="Write the markdown summary table to this path.") + parser.add_argument("--list", action="store_true", help="List available magician benchmark cases and exit.") + return parser.parse_args() + + +def repo_root() -> Path: + return Path(__file__).resolve().parent.parent + + +def benchmark_root() -> Path: + return repo_root() / "benchmarks" / "magician" / "cases" + + +def release_artifacts_ready() -> bool: + target_dir = repo_root() / "target" / "release" + return (target_dir / "elephc").exists() and (target_dir / "libelephc_magician.a").exists() + + +def elephc_bin() -> Path: + if not release_artifacts_ready(): + subprocess.run( + ["cargo", "build", "--release"], + cwd=repo_root(), + check=True, + ) + return repo_root() / "target" / "release" / "elephc" + + +def load_metadata(case_dir: Path) -> CaseMetadata: + data = json.loads((case_dir / "metadata.json").read_text()) + fragments: list[EvalFragment] = [] + for index, item in enumerate(data.get("eval_fragments", [])): + label = str(item.get("label", f"fragment-{index}")) + source = item.get("source") + invocations = item.get("invocations") + source_kind = item.get("source_kind") + parse_cache_should_hit = item.get("parse_cache_should_hit") + + if not isinstance(source, str): + raise ValueError(f"{case_dir.name}: eval fragment {label} must provide a string source") + if not isinstance(invocations, int) or invocations < 0: + raise ValueError(f"{case_dir.name}: eval fragment {label} must provide non-negative invocations") + if source_kind not in SOURCE_KINDS: + raise ValueError( + f"{case_dir.name}: eval fragment {label} source_kind must be one of " + f"{', '.join(sorted(SOURCE_KINDS))}" + ) + if not isinstance(parse_cache_should_hit, bool): + raise ValueError(f"{case_dir.name}: eval fragment {label} must provide parse_cache_should_hit") + + fragments.append( + EvalFragment( + label=label, + source=source, + invocations=invocations, + source_kind=source_kind, + parse_cache_should_hit=parse_cache_should_hit, + ) + ) + + if not fragments: + raise ValueError(f"{case_dir.name}: metadata.json must define at least one eval fragment") + + return CaseMetadata( + name=case_dir.name, + description=str(data.get("description", "")), + fragments=fragments, + ) + + +def available_cases(selected: list[str]) -> list[Path]: + root = benchmark_root() + cases = sorted(path for path in root.iterdir() if path.is_dir()) + if not selected: + return cases + wanted = set(selected) + selected_cases = [case for case in cases if case.name in wanted] + missing = sorted(wanted - {case.name for case in selected_cases}) + if missing: + raise SystemExit(f"unknown magician benchmark case(s): {', '.join(missing)}") + return selected_cases + + +def run_process(cmd: list[str], cwd: Path) -> subprocess.CompletedProcess[str]: + try: + return subprocess.run(cmd, cwd=cwd, text=True, capture_output=True, check=True) + except subprocess.CalledProcessError as error: + raise RuntimeError( + "command failed: {cmd}\nstdout:\n{stdout}\nstderr:\n{stderr}".format( + cmd=" ".join(cmd), + stdout=error.stdout, + stderr=error.stderr, + ) + ) from error + + +def run_checked(cmd: list[str], cwd: Path, expected: str) -> None: + output = run_process(cmd, cwd) + if output.stdout != expected: + raise RuntimeError( + f"unexpected stdout for {' '.join(cmd)}\nexpected: {expected!r}\nactual: {output.stdout!r}" + ) + + +def measure_command(label: str, cmd: list[str], cwd: Path, expected: str, iterations: int, warmup: int) -> CommandResult: + for _ in range(warmup): + run_checked(cmd, cwd, expected) + + samples: list[float] = [] + for _ in range(iterations): + started = time.perf_counter() + run_checked(cmd, cwd, expected) + samples.append((time.perf_counter() - started) * 1000.0) + + return CommandResult( + label=label, + median_ms=statistics.median(samples), + samples_ms=samples, + detail=f"{iterations} runs", + ) + + +def skipped_result(label: str, detail: str) -> CommandResult: + return CommandResult(label=label, median_ms=None, samples_ms=[], detail=detail) + + +def compile_elephc_variant(variant: str, cwd: Path) -> Path: + source = cwd / f"{variant}.php" + run_process([str(elephc_bin()), str(source)], cwd) + return cwd / variant + + +def maybe_measure_php(variant: str, cwd: Path, expected: str, iterations: int, warmup: int) -> CommandResult: + php = shutil.which("php") + label = f"php-{variant}" + if php is None: + return skipped_result(label, "php not found") + return measure_command(label, [php, str(cwd / f"{variant}.php")], cwd, expected, iterations, warmup) + + +def measure_case(case_dir: Path, iterations: int, warmup: int) -> BenchmarkResult: + metadata = load_metadata(case_dir) + expected = (case_dir / "expected.txt").read_text() + if not expected.endswith("\n"): + expected += "\n" + + with tempfile.TemporaryDirectory(prefix=f"elephc-magician-bench-{case_dir.name}-") as temp_dir: + cwd = Path(temp_dir) / case_dir.name + shutil.copytree(case_dir, cwd) + + native_binary = compile_elephc_variant("native", cwd) + eval_binary = compile_elephc_variant("eval", cwd) + + elephc_native = measure_command( + "elephc-native", + [str(native_binary)], + cwd, + expected, + iterations, + warmup, + ) + elephc_eval = measure_command( + "elephc-eval", + [str(eval_binary)], + cwd, + expected, + iterations, + warmup, + ) + php_native = maybe_measure_php("native", cwd, expected, iterations, warmup) + php_eval = maybe_measure_php("eval", cwd, expected, iterations, warmup) + + return BenchmarkResult( + case=case_dir.name, + metadata=metadata, + elephc_native=elephc_native, + elephc_eval=elephc_eval, + php_native=php_native, + php_eval=php_eval, + ) + + +def format_ms(value: float | None) -> str: + if value is None: + return "n/a" + return f"{value:.2f}" + + +def ratio(numerator: float | None, denominator: float | None) -> str: + value = ratio_value(numerator, denominator) + if value is None: + return "n/a" + return f"{value:.2f}x" + + +def ratio_value(numerator: float | None, denominator: float | None) -> float | None: + if numerator is None or denominator is None or denominator == 0: + return None + return numerator / denominator + + +def fragment_size_summary(metadata: CaseMetadata) -> str: + sizes = [fragment.size_bytes for fragment in metadata.fragments] + if len(sizes) == 1: + return str(sizes[0]) + return f"{metadata.max_fragment_size_bytes} max" + + +def result_to_dict(result: CommandResult) -> dict[str, object]: + return { + "label": result.label, + "median_ms": result.median_ms, + "samples_ms": result.samples_ms, + "detail": result.detail, + "skipped": result.median_ms is None, + } + + +def metadata_to_dict(metadata: CaseMetadata) -> dict[str, object]: + return { + "description": metadata.description, + "eval_invocations": metadata.eval_invocations, + "literal_or_dynamic": metadata.source_kind_summary, + "parse_cache": metadata.parse_cache_summary, + "max_fragment_size_bytes": metadata.max_fragment_size_bytes, + "eval_fragments": [ + { + "label": fragment.label, + "invocations": fragment.invocations, + "source_kind": fragment.source_kind, + "parse_cache_should_hit": fragment.parse_cache_should_hit, + "size_bytes": fragment.size_bytes, + } + for fragment in metadata.fragments + ], + } + + +def benchmark_to_dict(result: BenchmarkResult) -> dict[str, object]: + return { + "case": result.case, + "metadata": metadata_to_dict(result.metadata), + "elephc_native": result_to_dict(result.elephc_native), + "elephc_eval": result_to_dict(result.elephc_eval), + "php_native": result_to_dict(result.php_native), + "php_eval": result_to_dict(result.php_eval), + "ratios": { + "elephc_eval_vs_native": ratio_value(result.elephc_eval.median_ms, result.elephc_native.median_ms), + "php_eval_vs_native": ratio_value(result.php_eval.median_ms, result.php_native.median_ms), + "elephc_eval_vs_php_eval": ratio_value(result.elephc_eval.median_ms, result.php_eval.median_ms), + }, + } + + +def render_markdown(results: list[BenchmarkResult]) -> str: + lines = [ + "| case | evals | fragment bytes | kind | cache | elephc native ms | elephc eval ms | eval/native | php native ms | php eval ms | php eval/native |", + "|---|---:|---:|---|---|---:|---:|---:|---:|---:|---:|", + ] + for result in results: + lines.append( + "| {case} | {evals} | {fragment_bytes} | {kind} | {cache} | {elephc_native} | {elephc_eval} | {elephc_ratio} | {php_native} | {php_eval} | {php_ratio} |".format( + case=result.case, + evals=result.metadata.eval_invocations, + fragment_bytes=fragment_size_summary(result.metadata), + kind=result.metadata.source_kind_summary, + cache=result.metadata.parse_cache_summary, + elephc_native=format_ms(result.elephc_native.median_ms), + elephc_eval=format_ms(result.elephc_eval.median_ms), + elephc_ratio=ratio(result.elephc_eval.median_ms, result.elephc_native.median_ms), + php_native=format_ms(result.php_native.median_ms), + php_eval=format_ms(result.php_eval.median_ms), + php_ratio=ratio(result.php_eval.median_ms, result.php_native.median_ms), + ) + ) + return "\n".join(lines) + "\n" + + +def run_benchmarks(cases: list[Path], iterations: int, warmup: int) -> list[BenchmarkResult]: + return [measure_case(case_dir, iterations, warmup) for case_dir in cases] + + +def list_cases(cases: list[Path]) -> None: + for case_dir in cases: + metadata = load_metadata(case_dir) + print(f"{case_dir.name}: {metadata.description}") + + +def main() -> None: + args = parse_args() + if args.iterations < 1: + raise SystemExit("--iterations must be at least 1") + if args.warmup < 0: + raise SystemExit("--warmup must be at least 0") + + cases = available_cases(args.case) + if not cases: + raise SystemExit("no magician benchmark cases selected") + + if args.list: + list_cases(cases) + return + + results = run_benchmarks(cases, args.iterations, args.warmup) + markdown = render_markdown(results) + print(markdown, end="") + + if args.markdown: + args.markdown.write_text(markdown) + + if args.json: + payload: dict[str, Any] = { + "iterations": args.iterations, + "warmup": args.warmup, + "cases": [benchmark_to_dict(result) for result in results], + } + args.json.write_text(json.dumps(payload, indent=2) + "\n") + + +if __name__ == "__main__": + main() From cd026940a62399b660064ddfd40a6b31396b1d12 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 20:51:26 +0200 Subject: [PATCH 1045/1208] feat(eval): support static method callback AOT --- .plans/elephc-eval-aot-complete-plan.md | 19 ++++++ src/eval_aot.rs | 70 ++++++++++++++++++--- tests/codegen/eval.rs | 83 +++++++++++++++++++++++++ 3 files changed, 162 insertions(+), 10 deletions(-) diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md index fd0be58239..9809501707 100644 --- a/.plans/elephc-eval-aot-complete-plan.md +++ b/.plans/elephc-eval-aot-complete-plan.md @@ -3337,3 +3337,22 @@ Verifiche locali: - `cargo check --tests` - `rustfmt --check src/eval_aot.rs src/ir_lower/program.rs src/ir_lower/expr/mod.rs src/codegen_ir/lower_inst/builtins/eval.rs tests/codegen/eval.rs` - `git diff --check -- src/eval_aot.rs src/ir_lower/program.rs src/ir_lower/expr/mod.rs src/codegen_ir/lower_inst/builtins/eval.rs tests/codegen/eval.rs .plans/elephc-eval-aot-complete-plan.md` + +## Aggiornamento: callback a metodi statici + +Tranche completata: + +- il classificatore AOT per literal eval riconosce callback statiche + compile-time in forma `"Class::method"` e `["Class", "method"]`; +- `call_user_func()` e `call_user_func_array()` possono restare nel percorso + EIR AOT quando il metodo statico target e' pubblico, tipizzato e supportato + dagli stessi predicate delle chiamate statiche dirette; +- callback statiche verso metodi non tipizzati o non supportati continuano a + usare il fallback magician. + +Verifiche locali: + +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_callbacks_use_aot_without_magician -- --exact --nocapture` +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician -- --exact --nocapture` +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_uses_aot_without_magician -- --exact --nocapture` diff --git a/src/eval_aot.rs b/src/eval_aot.rs index 096c8252b5..ed501180d4 100644 --- a/src/eval_aot.rs +++ b/src/eval_aot.rs @@ -13,7 +13,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; -use crate::names::php_symbol_key; +use crate::names::{php_symbol_key, Name}; use crate::parser::ast::{ BinOp, CallableTarget, CastType, Expr, ExprKind, Program, StaticReceiver, Stmt, StmtKind, }; @@ -3519,7 +3519,7 @@ where } } -/// Returns true when a compile-time string callback names a safe builtin or user function. +/// Returns true when a compile-time callback names a safe function or static method. fn static_callback_call_is_eir_safe( callback: &Expr, callback_args: &[Expr], @@ -3530,16 +3530,66 @@ fn static_callback_call_is_eir_safe( where S: EirStaticCallSupport, { - let ExprKind::StringLiteral(callback_name) = &callback.kind else { - return false; + if let Some((receiver, method)) = static_callback_static_method_parts(callback) { + return support.static_method_supported(&receiver, method.as_str(), callback_args); + } + static_callback_function_name(callback).is_some_and(|callback_name| { + let short_callback = callback_name.trim_start_matches('\\'); + eir_runtime_builtin_call_is_safe(short_callback, callback_args, support, facts, scope_reads) + || fold_static_builtin_call(short_callback, callback_args).is_some() + || support.function_supported(short_callback, callback_args) + }) +} + +/// Returns the function name from a compile-time callback expression. +fn static_callback_function_name(callback: &Expr) -> Option<&str> { + match &callback.kind { + ExprKind::StringLiteral(name) if !name.contains("::") => Some(name.as_str()), + _ => None, + } +} + +/// Returns the named receiver and method from a compile-time static-method callback. +fn static_callback_static_method_parts(callback: &Expr) -> Option<(StaticReceiver, String)> { + match &callback.kind { + ExprKind::StringLiteral(name) => static_callback_static_method_string_parts(name), + ExprKind::ArrayLiteral(items) => static_callback_static_method_array_parts(items), + _ => None, + } +} + +/// Splits a literal `Class::method` callback into its receiver and method. +fn static_callback_static_method_string_parts(name: &str) -> Option<(StaticReceiver, String)> { + let (class_name, method) = name.trim_start_matches('\\').rsplit_once("::")?; + if class_name.is_empty() || method.is_empty() { + return None; + } + Some(( + StaticReceiver::Named(Name::from(class_name.trim_start_matches('\\').to_string())), + method.to_string(), + )) +} + +/// Extracts a literal `["Class", "method"]` callback target. +fn static_callback_static_method_array_parts(items: &[Expr]) -> Option<(StaticReceiver, String)> { + let [class_expr, method_expr] = items else { + return None; }; - if callback_name.contains("::") { - return false; + let ExprKind::StringLiteral(class_name) = &class_expr.kind else { + return None; + }; + let ExprKind::StringLiteral(method) = &method_expr.kind else { + return None; + }; + if class_name.is_empty() || method.is_empty() { + return None; } - let short_callback = callback_name.trim_start_matches('\\'); - eir_runtime_builtin_call_is_safe(short_callback, callback_args, support, facts, scope_reads) - || fold_static_builtin_call(short_callback, callback_args).is_some() - || support.function_supported(short_callback, callback_args) + Some(( + StaticReceiver::Named(Name::from( + class_name.as_str().trim_start_matches('\\').to_string(), + )), + method.clone(), + )) } /// Converts a static `call_user_func_array()` argument array into callback args. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eba88d4287..0d3a850b6e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1783,6 +1783,89 @@ echo eval('return call_user_func("eval_cuf_join", "A", "B") let _ = fs::remove_dir_all(&dir); } +/// Verifies static method callbacks can use literal eval EIR AOT. +#[test] +fn test_literal_eval_static_method_callbacks_use_aot_without_magician() { + let dir = make_cli_test_dir("elephc_literal_eval_static_method_callbacks_aot"); + let source = r#" "F", "left" => "E"]) + . "|" . call_user_func_array(["EvalAotStaticMethodCallbackBox", "inc"], [41]);'); +"#; + let (user_asm, runtime_asm, required_libraries) = + compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); + assert!( + user_asm.contains("eval literal AOT compiled EIR function"), + "static method callbacks should use EIR AOT:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_execute"), + "static method callbacks should not call the bridge:\n{user_asm}" + ); + assert!( + !user_asm.contains("__elephc_eval_"), + "native-only static method callback eval should not reference eval helpers:\n{user_asm}" + ); + assert!( + !runtime_asm.contains("__elephc_eval_"), + "native-only static method callback eval should not emit eval helpers:\n{runtime_asm}" + ); + assert!( + !required_libraries + .iter() + .any(|lib| lib == "elephc_magician"), + "native-only static method callback eval should not link elephc_magician: {required_libraries:?}" + ); + let runtime_obj = runtime_obj_for_asm(&runtime_asm); + let out = assemble_and_run( + &user_asm, + &runtime_obj, + &dir, + &required_libraries, + &default_link_paths(), + &[], + ); + assert_eq!(out, "A:B.|C:D!|E:F.|42"); + let _ = fs::remove_dir_all(&dir); +} + +/// Verifies static method callbacks without declared scalar signatures keep the bridge fallback. +#[test] +fn test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback() { + let dir = make_cli_test_dir("elephc_literal_eval_static_method_callback_untyped_bridge"); + let source = r#" Date: Mon, 6 Jul 2026 20:58:52 +0200 Subject: [PATCH 1046/1208] feat(eval): accept class constant static callbacks --- .plans/elephc-eval-aot-complete-plan.md | 18 +++++++++++ src/eval_aot.rs | 43 ++++++++++++++++--------- tests/codegen/eval.rs | 6 ++-- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md index 9809501707..5a3a5c5346 100644 --- a/.plans/elephc-eval-aot-complete-plan.md +++ b/.plans/elephc-eval-aot-complete-plan.md @@ -3356,3 +3356,21 @@ Verifiche locali: - `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` - `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician -- --exact --nocapture` - `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_uses_aot_without_magician -- --exact --nocapture` + +## Aggiornamento: callback statici con Class::class + +Tranche completata: + +- il classificatore AOT accetta anche callable array statici in forma + `[NamedClass::class, "method"]`; +- il supporto resta limitato a receiver nominali, lasciando fuori `self::class`, + `static::class` e `parent::class` finche' il gate AOT non riceve il contesto + di classe necessario; +- la stessa copertura AOT verifica string callback, callable array con stringa, + callable array con `Class::class`, `call_user_func()` e + `call_user_func_array()`. + +Verifiche locali: + +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_callbacks_use_aot_without_magician -- --exact --nocapture` +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` diff --git a/src/eval_aot.rs b/src/eval_aot.rs index ed501180d4..f2013fba04 100644 --- a/src/eval_aot.rs +++ b/src/eval_aot.rs @@ -3561,13 +3561,11 @@ fn static_callback_static_method_parts(callback: &Expr) -> Option<(StaticReceive /// Splits a literal `Class::method` callback into its receiver and method. fn static_callback_static_method_string_parts(name: &str) -> Option<(StaticReceiver, String)> { let (class_name, method) = name.trim_start_matches('\\').rsplit_once("::")?; - if class_name.is_empty() || method.is_empty() { + let receiver = static_callback_static_method_named_receiver(class_name)?; + if method.is_empty() { return None; } - Some(( - StaticReceiver::Named(Name::from(class_name.trim_start_matches('\\').to_string())), - method.to_string(), - )) + Some((receiver, method.to_string())) } /// Extracts a literal `["Class", "method"]` callback target. @@ -3575,21 +3573,36 @@ fn static_callback_static_method_array_parts(items: &[Expr]) -> Option<(StaticRe let [class_expr, method_expr] = items else { return None; }; - let ExprKind::StringLiteral(class_name) = &class_expr.kind else { - return None; - }; + let receiver = static_callback_static_method_array_receiver(class_expr)?; let ExprKind::StringLiteral(method) = &method_expr.kind else { return None; }; - if class_name.is_empty() || method.is_empty() { + if method.is_empty() { + return None; + } + Some((receiver, method.clone())) +} + +/// Returns the static receiver from the class part of a callable array. +fn static_callback_static_method_array_receiver(class_expr: &Expr) -> Option { + match &class_expr.kind { + ExprKind::StringLiteral(class_name) => { + static_callback_static_method_named_receiver(class_name) + } + ExprKind::ClassConstant { + receiver: StaticReceiver::Named(name), + } => Some(StaticReceiver::Named(name.clone())), + _ => None, + } +} + +/// Returns a named static receiver from a literal class name. +fn static_callback_static_method_named_receiver(class_name: &str) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if class_name.is_empty() { return None; } - Some(( - StaticReceiver::Named(Name::from( - class_name.as_str().trim_start_matches('\\').to_string(), - )), - method.clone(), - )) + Some(StaticReceiver::Named(Name::from(class_name.to_string()))) } /// Converts a static `call_user_func_array()` argument array into callback args. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0d3a850b6e..eaa81fdb1c 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1800,7 +1800,9 @@ class EvalAotStaticMethodCallbackBox { echo eval('return call_user_func("EvalAotStaticMethodCallbackBox::join", "A", "B") . "|" . call_user_func(["EvalAotStaticMethodCallbackBox", "join"], "C", "D", true) . "|" . call_user_func_array("EvalAotStaticMethodCallbackBox::join", ["right" => "F", "left" => "E"]) - . "|" . call_user_func_array(["EvalAotStaticMethodCallbackBox", "inc"], [41]);'); + . "|" . call_user_func_array(["EvalAotStaticMethodCallbackBox", "inc"], [41]) + . "|" . call_user_func([EvalAotStaticMethodCallbackBox::class, "join"], "G", "H") + . "|" . call_user_func_array([EvalAotStaticMethodCallbackBox::class, "inc"], [9]);'); "#; let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); @@ -1835,7 +1837,7 @@ echo eval('return call_user_func("EvalAotStaticMethodCallbackBox::join", "A", "B &default_link_paths(), &[], ); - assert_eq!(out, "A:B.|C:D!|E:F.|42"); + assert_eq!(out, "A:B.|C:D!|E:F.|42|G:H.|10"); let _ = fs::remove_dir_all(&dir); } From 687a1a955bf0d2859a79f2453b55f0f1cbc1107e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 21:07:19 +0200 Subject: [PATCH 1047/1208] feat(eval): support first-class static callbacks in eval AOT --- .plans/elephc-eval-aot-complete-plan.md | 19 +++++++++++++++++++ src/eval_aot.rs | 4 ++++ tests/codegen/eval.rs | 12 +++++++----- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md index 5a3a5c5346..9d7323f916 100644 --- a/.plans/elephc-eval-aot-complete-plan.md +++ b/.plans/elephc-eval-aot-complete-plan.md @@ -3374,3 +3374,22 @@ Verifiche locali: - `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_callbacks_use_aot_without_magician -- --exact --nocapture` - `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` + +## Aggiornamento: callback first-class statici + +Tranche completata: + +- il gate AOT riconosce callback first-class compile-time in + `call_user_func()` e `call_user_func_array()` per funzioni note e metodi + statici nominali; +- la classificazione generale di un first-class callable come valore resta + conservativa: il supporto vale solo quando il callable e' usato come callback + statico immediato; +- i predicate esistenti continuano a escludere metodi non tipizzati, receiver + non nominali e signature fuori subset. + +Verifiche locali: + +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician -- --exact --nocapture` +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_callbacks_use_aot_without_magician -- --exact --nocapture` +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` diff --git a/src/eval_aot.rs b/src/eval_aot.rs index f2013fba04..165f1852a6 100644 --- a/src/eval_aot.rs +++ b/src/eval_aot.rs @@ -3545,6 +3545,7 @@ where fn static_callback_function_name(callback: &Expr) -> Option<&str> { match &callback.kind { ExprKind::StringLiteral(name) if !name.contains("::") => Some(name.as_str()), + ExprKind::FirstClassCallable(CallableTarget::Function(name)) => Some(name.as_str()), _ => None, } } @@ -3553,6 +3554,9 @@ fn static_callback_function_name(callback: &Expr) -> Option<&str> { fn static_callback_static_method_parts(callback: &Expr) -> Option<(StaticReceiver, String)> { match &callback.kind { ExprKind::StringLiteral(name) => static_callback_static_method_string_parts(name), + ExprKind::FirstClassCallable(CallableTarget::StaticMethod { receiver, method }) => { + Some((receiver.clone(), method.clone())) + } ExprKind::ArrayLiteral(items) => static_callback_static_method_array_parts(items), _ => None, } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index eaa81fdb1c..e0bc68d07e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1744,7 +1744,8 @@ function eval_cuf_join(string $left, string $right, bool $bang = false): string return $left . ":" . $right . ($bang ? "!" : "."); } echo eval('return call_user_func("eval_cuf_join", "A", "B") - . "|" . call_user_func_array("eval_cuf_join", ["right" => "D", "left" => "C", "bang" => true]);'); + . "|" . call_user_func_array("eval_cuf_join", ["right" => "D", "left" => "C", "bang" => true]) + . "|" . call_user_func(eval_cuf_join(...), "E", "F", true);'); "#; let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); @@ -1779,7 +1780,7 @@ echo eval('return call_user_func("eval_cuf_join", "A", "B") &default_link_paths(), &[], ); - assert_eq!(out, "A:B.|C:D!"); + assert_eq!(out, "A:B.|C:D!|E:F!"); let _ = fs::remove_dir_all(&dir); } @@ -1802,7 +1803,8 @@ echo eval('return call_user_func("EvalAotStaticMethodCallbackBox::join", "A", "B . "|" . call_user_func_array("EvalAotStaticMethodCallbackBox::join", ["right" => "F", "left" => "E"]) . "|" . call_user_func_array(["EvalAotStaticMethodCallbackBox", "inc"], [41]) . "|" . call_user_func([EvalAotStaticMethodCallbackBox::class, "join"], "G", "H") - . "|" . call_user_func_array([EvalAotStaticMethodCallbackBox::class, "inc"], [9]);'); + . "|" . call_user_func_array([EvalAotStaticMethodCallbackBox::class, "inc"], [9]) + . "|" . call_user_func(EvalAotStaticMethodCallbackBox::join(...), "I", "J", true);'); "#; let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); @@ -1837,7 +1839,7 @@ echo eval('return call_user_func("EvalAotStaticMethodCallbackBox::join", "A", "B &default_link_paths(), &[], ); - assert_eq!(out, "A:B.|C:D!|E:F.|42|G:H.|10"); + assert_eq!(out, "A:B.|C:D!|E:F.|42|G:H.|10|I:J!"); let _ = fs::remove_dir_all(&dir); } @@ -1851,7 +1853,7 @@ class EvalAotUntypedStaticMethodCallbackBox { return $left + $right; } } -echo eval('return call_user_func("EvalAotUntypedStaticMethodCallbackBox::add", 1, 2);'); +echo eval('return call_user_func(EvalAotUntypedStaticMethodCallbackBox::add(...), 1, 2);'); "#; let (user_asm, _runtime_asm, required_libraries) = compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); From 5e45df82d0276bb16bf9482694d0881e938d399f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 21:11:31 +0200 Subject: [PATCH 1048/1208] test(eval): cover first-class call_user_func_array AOT --- .plans/elephc-eval-aot-complete-plan.md | 2 ++ tests/codegen/eval.rs | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md index 9d7323f916..6684e67391 100644 --- a/.plans/elephc-eval-aot-complete-plan.md +++ b/.plans/elephc-eval-aot-complete-plan.md @@ -3385,6 +3385,8 @@ Tranche completata: - la classificazione generale di un first-class callable come valore resta conservativa: il supporto vale solo quando il callable e' usato come callback statico immediato; +- la copertura verifica sia `call_user_func()` sia `call_user_func_array()` con + first-class callable a funzione utente e metodo statico; - i predicate esistenti continuano a escludere metodi non tipizzati, receiver non nominali e signature fuori subset. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index e0bc68d07e..ab10b37538 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -1745,7 +1745,8 @@ function eval_cuf_join(string $left, string $right, bool $bang = false): string } echo eval('return call_user_func("eval_cuf_join", "A", "B") . "|" . call_user_func_array("eval_cuf_join", ["right" => "D", "left" => "C", "bang" => true]) - . "|" . call_user_func(eval_cuf_join(...), "E", "F", true);'); + . "|" . call_user_func(eval_cuf_join(...), "E", "F", true) + . "|" . call_user_func_array(eval_cuf_join(...), ["right" => "H", "left" => "G"]);'); "#; let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); @@ -1780,7 +1781,7 @@ echo eval('return call_user_func("eval_cuf_join", "A", "B") &default_link_paths(), &[], ); - assert_eq!(out, "A:B.|C:D!|E:F!"); + assert_eq!(out, "A:B.|C:D!|E:F!|G:H."); let _ = fs::remove_dir_all(&dir); } @@ -1804,7 +1805,8 @@ echo eval('return call_user_func("EvalAotStaticMethodCallbackBox::join", "A", "B . "|" . call_user_func_array(["EvalAotStaticMethodCallbackBox", "inc"], [41]) . "|" . call_user_func([EvalAotStaticMethodCallbackBox::class, "join"], "G", "H") . "|" . call_user_func_array([EvalAotStaticMethodCallbackBox::class, "inc"], [9]) - . "|" . call_user_func(EvalAotStaticMethodCallbackBox::join(...), "I", "J", true);'); + . "|" . call_user_func(EvalAotStaticMethodCallbackBox::join(...), "I", "J", true) + . "|" . call_user_func_array(EvalAotStaticMethodCallbackBox::join(...), ["right" => "L", "left" => "K"]);'); "#; let (user_asm, runtime_asm, required_libraries) = compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); @@ -1839,7 +1841,7 @@ echo eval('return call_user_func("EvalAotStaticMethodCallbackBox::join", "A", "B &default_link_paths(), &[], ); - assert_eq!(out, "A:B.|C:D!|E:F.|42|G:H.|10|I:J!"); + assert_eq!(out, "A:B.|C:D!|E:F.|42|G:H.|10|I:J!|K:L."); let _ = fs::remove_dir_all(&dir); } @@ -1853,7 +1855,7 @@ class EvalAotUntypedStaticMethodCallbackBox { return $left + $right; } } -echo eval('return call_user_func(EvalAotUntypedStaticMethodCallbackBox::add(...), 1, 2);'); +echo eval('return call_user_func_array(EvalAotUntypedStaticMethodCallbackBox::add(...), [1, 2]);'); "#; let (user_asm, _runtime_asm, required_libraries) = compile_source_to_asm_with_options(source, &dir, 8_388_608, false, false); From a8cc771ca7f3442c7fe8232017e4fc1f74b14fa6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 21:28:07 +0200 Subject: [PATCH 1049/1208] docs(eval): record prime-loop target checks --- .plans/elephc-eval-aot-complete-plan.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md index 6684e67391..9c6d64a646 100644 --- a/.plans/elephc-eval-aot-complete-plan.md +++ b/.plans/elephc-eval-aot-complete-plan.md @@ -2557,9 +2557,20 @@ Aggiornamento verifica target-aware - 2026-07-06: - `./scripts/test-linux-x86_64.sh literal_eval_is_iterable_scope`: passato; - `./scripts/test-linux-x86_64.sh literal_eval_type_probes_scope`: passato; - i runner Docker x86_64 sono usciti senza container residui; -- la verifica locale Linux ARM64 non e' stata rilanciata in questa tranche: - resta demandata alla matrice CI o a un filtro Docker ARM64 dedicato prima della - chiusura completa del piano. +- la verifica locale Linux ARM64 per i casi direct-read resta demandata alla + matrice CI o a filtri Docker dedicati prima di cambiare quel sottoinsieme. + +Aggiornamento verifica target-aware prime-loop - 2026-07-06: + +- il test prime-sum literal eval (`100000`, output `454396537`, no bridge, no + `elephc_magician`) e' passato sul target locale macOS ARM64: + - `cargo test --test codegen_tests codegen::eval::test_literal_eval_prime_loop_uses_aot_without_execute_bridge -- --exact --nocapture`; +- lo stesso filtro e' passato su Linux x86_64 via Docker: + - `./scripts/test-linux-x86_64.sh test_literal_eval_prime_loop_uses_aot_without_execute_bridge`; +- lo stesso filtro e' passato su Linux ARM64 via Docker: + - `./scripts/test-linux-arm64.sh test_literal_eval_prime_loop_uses_aot_without_execute_bridge`; +- i run Docker hanno emesso solo i warning preesistenti su `libc::time_t` in + `elephc-magician`. Aggiornamento Milestone 4/7.88 - 2026-07-06: From 127202e7a498e45a76a6db251d49def58208c399 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 21:32:06 +0200 Subject: [PATCH 1050/1208] docs(eval): record prime-loop benchmark result --- .plans/elephc-eval-aot-complete-plan.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md index 9c6d64a646..ee958f421e 100644 --- a/.plans/elephc-eval-aot-complete-plan.md +++ b/.plans/elephc-eval-aot-complete-plan.md @@ -2572,6 +2572,19 @@ Aggiornamento verifica target-aware prime-loop - 2026-07-06: - i run Docker hanno emesso solo i warning preesistenti su `libc::time_t` in `elephc-magician`. +Aggiornamento benchmark prime-loop - 2026-07-06: + +- ricostruito `target/release/elephc` con `cargo build --release`; +- eseguito benchmark manuale fuori repo, senza aggiungere il caso alla suite + permanente, sullo stesso workload prime-sum fino a `100000`; +- output verificato identico in tutti i casi: `454396537`; +- mediane su 12 run: + - Elephc standard: `13.35 ms`; + - Elephc via eval literal AOT: `9.74 ms`; + - PHP standard: `97.77 ms`; + - PHP via eval: `117.76 ms`; +- RSS sample `Elephc via eval`: `1572864` byte di maximum resident set size. + Aggiornamento Milestone 4/7.88 - 2026-07-06: - esteso il path EIR AOT direct-read per `count($callerArray)` senza aprire From cfcd7d9362d2e222b563b5aedd4b0e8281d88fee Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 21:32:48 +0200 Subject: [PATCH 1051/1208] docs(eval): close eval AOT completion plan --- .plans/elephc-eval-aot-complete-plan.md | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md index ee958f421e..9396eaaba6 100644 --- a/.plans/elephc-eval-aot-complete-plan.md +++ b/.plans/elephc-eval-aot-complete-plan.md @@ -3419,3 +3419,50 @@ Verifiche locali: - `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician -- --exact --nocapture` - `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_callbacks_use_aot_without_magician -- --exact --nocapture` - `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` + +## Chiusura piano - 2026-07-06 + +Audit dei criteri di completamento: + +1. I literal eval nel subset supportato entrano in percorsi AOT nativi e i + fallback restano marcati con motivo leggibile. +2. Il benchmark prime-sum fino a `100000` passa senza bridge, senza + `elephc_magician`, con output `454396537`. +3. Scope read/write del caller e variabili create da eval sono coperti dai test + direct-local/direct-param/scope-helper senza linkare magician quando il + frammento e' fully AOT. +4. `return`, fallthrough/null e output side-effect sono coperti da test dedicati. +5. Builtin statici comuni, chiamate a funzioni note, metodi statici pubblici, + `call_user_func*()` statici e first-class callback statici sono coperti dal + percorso AOT. +6. I fallback non supportati restano verificati: eval dinamico, callback + dinamici, signature non tipizzate, spread dinamici, casi array/object non + modellati e semantiche PHP conservative. +7. I programmi con solo eval fully AOT non linkano `elephc_magician`; il test + prime-loop verifica anche assenza di helper runtime `__elephc_eval_*`. +8. La soluzione non embedda il compilatore nel binario finale: l'AOT avviene nel + compilatore e i binari fully AOT non linkano il bridge magician. +9. Il prime-loop AOT e' passato su macOS ARM64 locale, Linux x86_64 Docker e + Linux ARM64 Docker con filtro dedicato. +10. `git diff --check` passa sull'intero worktree corrente; zero file risultano + modificati solo da whitespace rispetto a `git diff -w`. + +Ultime verifiche locali registrate: + +- `cargo build --release` +- `cargo test --test codegen_tests codegen::eval::test_literal_eval_prime_loop_uses_aot_without_execute_bridge -- --exact --nocapture` +- `./scripts/test-linux-x86_64.sh test_literal_eval_prime_loop_uses_aot_without_execute_bridge` +- `./scripts/test-linux-arm64.sh test_literal_eval_prime_loop_uses_aot_without_execute_bridge` +- benchmark manuale prime-sum fuori repo su 12 run: + - Elephc standard `13.35 ms`; + - Elephc via eval literal AOT `9.74 ms`; + - PHP standard `97.77 ms`; + - PHP via eval `117.76 ms`; + - RSS sample Elephc via eval `1572864` byte. +- `python3 scripts/benchmark_magician.py --case algebra_heavy --iterations 5 --warmup 1` + - Elephc native `2.84 ms`; + - Elephc eval `4.90 ms`; + - PHP native `78.54 ms`; + - PHP eval `80.17 ms`. +- `git diff --check` +- `comm -23 <(git diff --name-only | sort) <(git diff -w --name-only | sort) | wc -l` From 4b7bd094e9b635c142eea7e0ff400912343dffbf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 6 Jul 2026 21:48:55 +0200 Subject: [PATCH 1052/1208] fix(eval): use split runtime feature flags --- src/codegen/frame.rs | 2 +- src/codegen/lower_inst/static_properties.rs | 4 +- src/codegen/lower_inst/strings.rs | 2 +- src/codegen/mod.rs | 57 ++++++++++++--------- src/ir_lower/builtin_datetime.rs | 2 +- 5 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index 32d5799f20..cd32a96c48 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -1167,7 +1167,7 @@ fn store_argv_global_if_needed(ctx: &mut FunctionContext<'_>) { /// Returns true when a process superglobal needs program-global storage. fn superglobal_storage_needed(ctx: &FunctionContext<'_>, name: &str) -> bool { - ctx.module.required_runtime_features.eval + ctx.module.required_runtime_features.eval_bridge || ctx .module .data diff --git a/src/codegen/lower_inst/static_properties.rs b/src/codegen/lower_inst/static_properties.rs index 9b121930f1..7dc0ab36e1 100644 --- a/src/codegen/lower_inst/static_properties.rs +++ b/src/codegen/lower_inst/static_properties.rs @@ -348,7 +348,7 @@ fn emit_eval_native_frame_static_property_get_if_needed( inst: &Instruction, slot: &StaticPropertySlot, ) -> Result> { - if !slot.late_bound || !ctx.module.required_runtime_features.eval { + if !slot.late_bound || !ctx.module.required_runtime_features.eval_bridge { return Ok(None); } let frame_class = super::current_method_class(ctx)?.to_string(); @@ -373,7 +373,7 @@ fn emit_eval_native_frame_static_property_set_if_needed( value: ValueId, slot: &StaticPropertySlot, ) -> Result> { - if !slot.late_bound || !ctx.module.required_runtime_features.eval { + if !slot.late_bound || !ctx.module.required_runtime_features.eval_bridge { return Ok(None); } let frame_class = super::current_method_class(ctx)?.to_string(); diff --git a/src/codegen/lower_inst/strings.rs b/src/codegen/lower_inst/strings.rs index 6a33d687f0..3d1896ea90 100644 --- a/src/codegen/lower_inst/strings.rs +++ b/src/codegen/lower_inst/strings.rs @@ -111,7 +111,7 @@ fn emit_eval_native_frame_called_class_override_probe( ctx: &mut FunctionContext<'_>, done_label: &str, ) { - if !ctx.module.required_runtime_features.eval { + if !ctx.module.required_runtime_features.eval_bridge { return; } let Some(frame_class) = current_late_static_frame_class(ctx).map(str::to_string) else { diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index e6dee45844..7ab3a30e31 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -200,42 +200,51 @@ fn finalize_user_asm( emit: Emit, exported_functions: &HashMap, ) -> String { - eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); - eval_static_property_helpers::emit_eval_static_property_helpers(module, &mut emitter, &mut data); - eval_class_constant_helpers::emit_eval_class_constant_helpers( - module, - &mut emitter, - &mut data, - ); + let eval_bridge = module.required_runtime_features.eval_bridge; + if eval_bridge { + eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); + eval_static_property_helpers::emit_eval_static_property_helpers( + module, + &mut emitter, + &mut data, + ); + eval_class_constant_helpers::emit_eval_class_constant_helpers( + module, + &mut emitter, + &mut data, + ); + } let eval_callable_support_needed = - eval_callable_helpers::module_needs_eval_callable_descriptor_support(module); + eval_bridge && eval_callable_helpers::module_needs_eval_callable_descriptor_support(module); let eval_callable_support = eval_callable_helpers::emit_eval_callable_descriptor_support( module, &mut emitter, &mut data, eval_callable_support_needed, ); - eval_constructor_helpers::emit_eval_constructor_helpers( - module, - &mut emitter, - &mut data, - &eval_callable_support, - ); - eval_method_helpers::emit_eval_method_helpers( - module, - &mut emitter, - &mut data, - &eval_callable_support, - ); - eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); - eval_reflection_owner_helpers::emit_eval_reflection_owner_helpers(module, &mut emitter); + if eval_bridge { + eval_constructor_helpers::emit_eval_constructor_helpers( + module, + &mut emitter, + &mut data, + &eval_callable_support, + ); + eval_method_helpers::emit_eval_method_helpers( + module, + &mut emitter, + &mut data, + &eval_callable_support, + ); + eval_reflection_helpers::emit_eval_reflection_helpers(module, &mut emitter); + eval_reflection_owner_helpers::emit_eval_reflection_owner_helpers(module, &mut emitter); + } let data_output = data.emit(); let empty_globals = HashSet::::new(); let empty_static_vars = HashMap::<(String, String), PhpType>::new(); let user_functions = runtime_user_function_sigs(module); let function_variant_groups = runtime_function_variant_groups(module); let mut allowed_class_names = runtime_referenced_class_names(module); - if module_uses_dynamic_callable_lookup(module) || module.required_runtime_features.eval { + if module_uses_dynamic_callable_lookup(module) || module.required_runtime_features.eval_bridge { allowed_class_names.extend(module.class_infos.keys().cloned()); } let runtime_interfaces = runtime_referenced_interfaces(module, &allowed_class_names); @@ -843,7 +852,7 @@ fn runtime_referenced_interfaces( class_names: &HashSet, ) -> HashMap { let mut names = HashSet::new(); - if module.required_runtime_features.eval { + if module.required_runtime_features.eval_bridge { names.extend(module.interface_infos.keys().cloned()); } if module_uses_dynamic_instanceof(module) { diff --git a/src/ir_lower/builtin_datetime.rs b/src/ir_lower/builtin_datetime.rs index c8bb0c841c..2dc3065a81 100644 --- a/src/ir_lower/builtin_datetime.rs +++ b/src/ir_lower/builtin_datetime.rs @@ -195,7 +195,7 @@ fn eval_date_alias_builtin_datetime_methods(module: &Module) -> Vec<(String, Str /// Returns true when the lowered module has any dependency on the eval bridge. fn module_uses_eval(module: &Module) -> bool { - module.required_runtime_features.eval + module.required_runtime_features.eval_bridge || module .functions .iter() From 8c94862d3b3f6c9c08ed99224e4462175dec9569 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 19:03:28 +0200 Subject: [PATCH 1053/1208] Fix eval branch after main rebase --- src/builtin_metadata.rs | 6 +- src/builtins/registry.rs | 2 + src/codegen/context.rs | 61 ++ src/codegen/eval_callable_helpers.rs | 863 ++++++++++++++---- src/codegen/eval_constructor_helpers.rs | 2 +- src/codegen/eval_method_helpers.rs | 2 +- src/codegen/lower_inst.rs | 51 +- src/codegen/lower_inst/arrays.rs | 2 +- src/codegen/lower_inst/builtins/eval.rs | 60 +- src/codegen/lower_inst/callables.rs | 2 + src/codegen/lower_inst/objects.rs | 21 +- src/codegen/lower_inst/objects/reflection.rs | 9 +- src/codegen/mod.rs | 2 + src/codegen_support/callable_descriptor.rs | 6 + src/eval_aot.rs | 2 +- src/image_prelude/detect.rs | 6 + src/ir_lower/expr/mod.rs | 1 + src/ir_lower/function.rs | 5 +- src/list_id_prelude/detect.rs | 10 + src/types/call_args/mod.rs | 4 +- src/types/call_args/planner.rs | 3 - src/types/checker/builtin_types/calendar.rs | 1 + .../checker/builtin_types/date_period.rs | 6 + src/types/checker/builtin_types/datetime.rs | 35 + .../checker/builtin_types/declarations.rs | 2 + src/types/checker/builtin_types/reflection.rs | 66 ++ src/types/checker/driver/mod.rs | 1 + src/tz_prelude/detect.rs | 6 + src/var_export_prelude/detect.rs | 6 + tests/builtin_parity_tests.rs | 47 + tests/codegen/support/projects.rs | 8 +- 31 files changed, 1025 insertions(+), 273 deletions(-) diff --git a/src/builtin_metadata.rs b/src/builtin_metadata.rs index 32bcbac18c..fbab246f2d 100644 --- a/src/builtin_metadata.rs +++ b/src/builtin_metadata.rs @@ -27,7 +27,11 @@ pub struct BuiltinSignatureMetadata { /// Returns the compiler's PHP-visible builtin names. pub fn php_visible_builtin_names() -> &'static [&'static str] { - crate::types::checker::builtins::supported_builtin_function_names() + static NAMES: std::sync::OnceLock<&'static [&'static str]> = std::sync::OnceLock::new(); + NAMES.get_or_init(|| { + let names = crate::types::checker::builtins::supported_builtin_function_names(); + Box::leak(names.into_boxed_slice()) + }) } /// Returns comparison metadata for one builtin signature, when the compiler tracks it. diff --git a/src/builtins/registry.rs b/src/builtins/registry.rs index 5584e29f4a..3c5a342388 100644 --- a/src/builtins/registry.rs +++ b/src/builtins/registry.rs @@ -165,6 +165,8 @@ pub fn function_sig(name: &str) -> Option { let def = lookup(name)?; Some(FunctionSig { params: def.params.clone(), + param_type_exprs: vec![None; def.params.len()], + param_attributes: vec![Vec::new(); def.params.len()], defaults: def.defaults.clone(), return_type: def.return_type.clone(), declared_return: false, diff --git a/src/codegen/context.rs b/src/codegen/context.rs index fc3e5a0822..44e821727d 100644 --- a/src/codegen/context.rs +++ b/src/codegen/context.rs @@ -406,6 +406,17 @@ impl<'a> FunctionContext<'a> { Ok(()) } + /// Stores the current result register(s) directly into an addressable local slot. + pub(super) fn store_current_result_to_local(&mut self, slot: LocalSlotId) -> Result<()> { + let target_ty = self.local_php_type(slot)?; + if self.local_stores_ref_cell_pointer(slot) { + return self.store_current_result_to_ref_cell_local(slot, &target_ty); + } + let offset = self.local_offset(slot)?; + self.store_current_result_at_offset(&target_ty, offset); + Ok(()) + } + /// After an in-place hash/array mutation whose runtime helper returns the /// possibly-reallocated container pointer in `value`'s register (already /// persisted via `store_result_value`), writes that pointer back to global @@ -481,6 +492,56 @@ impl<'a> FunctionContext<'a> { Ok(()) } + /// Stores the current result register(s) through a local ref-cell pointer slot. + fn store_current_result_to_ref_cell_local( + &mut self, + slot: LocalSlotId, + target_ty: &PhpType, + ) -> Result<()> { + reject_multiword_ref_cell_local(target_ty, "store")?; + let offset = self.local_offset(slot)?; + let pointer_reg = abi::symbol_scratch_reg(self.emitter); + abi::load_at_offset(self.emitter, pointer_reg, offset); + match target_ty.codegen_repr() { + PhpType::Str => { + let (ptr_reg, len_reg) = abi::string_result_regs(self.emitter); + abi::emit_store_to_address(self.emitter, ptr_reg, pointer_reg, 0); + abi::emit_store_to_address(self.emitter, len_reg, pointer_reg, 8); + } + PhpType::Float => { + abi::emit_store_to_address( + self.emitter, + abi::float_result_reg(self.emitter), + pointer_reg, + 0, + ); + } + PhpType::TaggedScalar => { + abi::emit_store_to_address( + self.emitter, + abi::int_result_reg(self.emitter), + pointer_reg, + 0, + ); + abi::emit_store_to_address( + self.emitter, + crate::codegen::sentinels::tagged_scalar_tag_reg(self.emitter), + pointer_reg, + 8, + ); + } + _ => { + abi::emit_store_to_address( + self.emitter, + abi::int_result_reg(self.emitter), + pointer_reg, + 0, + ); + } + } + Ok(()) + } + /// Stores the current result register(s) into a frame offset. fn store_current_result_at_offset(&mut self, ty: &PhpType, offset: usize) { match &ty.codegen_repr() { diff --git a/src/codegen/eval_callable_helpers.rs b/src/codegen/eval_callable_helpers.rs index 28b6aebec2..5998c73ee1 100644 --- a/src/codegen/eval_callable_helpers.rs +++ b/src/codegen/eval_callable_helpers.rs @@ -17,17 +17,17 @@ use crate::codegen::abi; use crate::codegen::callable_descriptor::{ self, CallableDescriptorInvocation, CallableDescriptorShape, }; +use std::collections::HashSet; + use crate::codegen::callable_dispatch::{ - self, RuntimeCallableCase, RuntimeCallableSelector, RuntimeInstanceCallableShape, - RuntimeInstanceMethodCallableCase, RuntimeStaticMethodCallableCase, + self, RuntimeCallableCase, RuntimeCallableSelector, RuntimeStaticMethodCallableCase, }; -use crate::codegen::context::DeferredClosure; use crate::codegen::data_section::DataSection; use crate::codegen::emit::Emitter; +use crate::codegen::runtime_callable_invoker::RuntimeCallableInvoker; use crate::ir::{Function, LocalKind, Module}; -use crate::names::{function_symbol, php_symbol_key}; -use crate::parser::ast::{Expr, ExprKind, Stmt, StmtKind, Visibility}; -use crate::span::Span; +use crate::names::{function_symbol, method_symbol, php_symbol_key, static_method_symbol}; +use crate::parser::ast::Visibility; use crate::types::{callable_wrapper_sig, FunctionSig, PhpType}; const EVAL_RECEIVER_CAPTURE_PARAM: &str = "__elephc_eval_callable_receiver"; @@ -49,12 +49,46 @@ const AARCH64_EVAL_CONTEXT_FROM_FP_OFFSET: i64 = 16; /// Callable descriptors available to eval constructor and method bridges. pub(super) struct EvalCallableDescriptorSupport { string_cases: Vec, - instance_array_cases: Vec, + instance_array_cases: Vec, static_array_cases: Vec, - object_cases: Vec, + object_cases: Vec, dynamic_descriptor_label: Option, } +/// Runtime instance-method callable case emitted specifically for eval bridges. +#[derive(Clone)] +struct EvalInstanceMethodCallableCase { + class_id: u64, + method_name: String, + case: RuntimeCallableCase, +} + +/// Receiver-bound callable descriptor shape used by eval bridge metadata. +#[derive(Clone, Copy)] +enum EvalInstanceCallableShape { + InstanceMethod, + ObjectInvoke, +} + +/// Stable label allocator for eval bridge helper bodies emitted outside `FunctionContext`. +struct EvalCallableEmitState { + next_id: usize, +} + +impl EvalCallableEmitState { + /// Creates an empty label state for one eval callable-support emission pass. + fn new() -> Self { + Self { next_id: 0 } + } + + /// Returns a unique global/local label for generated eval callable support. + fn next_label(&mut self, prefix: &str) -> String { + let id = self.next_id; + self.next_id += 1; + format!("__elephc_eval_{}_{}", prefix, id) + } +} + impl EvalCallableDescriptorSupport { /// Returns true when no callable descriptor case is available. pub(super) fn is_empty(&self) -> bool { @@ -148,20 +182,13 @@ pub(super) fn emit_eval_callable_descriptor_support( dynamic_descriptor_label: None, }; } - let mut legacy_ctx = super::lower_inst::legacy_context_from_eir_module(module); - legacy_ctx.functions.retain(|name, _| { - module - .functions - .iter() - .any(|function| !function.flags.is_main && function.name == *name) - }); - let string_cases = eval_user_function_callable_cases(&mut legacy_ctx, data); - let instance_array_cases = eval_instance_method_callable_cases(module, &mut legacy_ctx, data); - let static_array_cases = eval_static_method_callable_cases(module, &mut legacy_ctx, data); - let object_cases = eval_invokable_object_callable_cases(module, &mut legacy_ctx, data); + let mut state = EvalCallableEmitState::new(); + let string_cases = eval_user_function_callable_cases(module, emitter, data, &mut state); + let instance_array_cases = eval_instance_method_callable_cases(module, emitter, data, &mut state); + let static_array_cases = eval_static_method_callable_cases(module, emitter, data, &mut state); + let object_cases = eval_invokable_object_callable_cases(module, emitter, data, &mut state); let dynamic_descriptor_label = Some(eval_dynamic_callable_descriptor(data)); emit_eval_dynamic_callable_invoker(module, emitter, data); - emit_deferred_callable_support(emitter, data, &mut legacy_ctx); EvalCallableDescriptorSupport { string_cases, instance_array_cases, @@ -350,20 +377,18 @@ fn emit_x86_64_eval_dynamic_callable_fatal(emitter: &mut Emitter, data: &mut Dat /// Builds descriptor cases for user functions visible as eval string callables. fn eval_user_function_callable_cases( - legacy_ctx: &mut crate::codegen::context::Context, + module: &Module, + emitter: &mut Emitter, data: &mut DataSection, + state: &mut EvalCallableEmitState, ) -> Vec { - let mut functions = legacy_ctx - .functions - .iter() - .map(|(name, sig)| (name.clone(), sig.clone())) - .collect::>(); + let mut functions = eval_user_function_sigs(module); functions.sort_by(|left, right| left.0.cmp(&right.0)); let mut cases = Vec::with_capacity(functions.len()); for (name, sig) in functions { let case_sig = callable_wrapper_sig(&sig); let invoker_label = - callable_dispatch::ensure_runtime_descriptor_invoker(legacy_ctx, &[], &case_sig); + emit_eval_runtime_callable_invoker_inline(emitter, data, state, &case_sig, &[]); let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( data, &function_symbol(&name), @@ -373,32 +398,60 @@ fn eval_user_function_callable_cases( &[], &[], CallableDescriptorInvocation::named(CallableDescriptorShape::Function, &name), - invoker_label.as_deref(), + Some(&invoker_label), ); cases.push(RuntimeCallableCase { label: function_symbol(&name), descriptor_label, php_name: Some(name), - sig: case_sig, - captures: Vec::new(), - has_invoker: invoker_label.is_some(), - invoker_label, }); } cases } +/// Collects user function signatures available to eval string-callable descriptors. +fn eval_user_function_sigs(module: &Module) -> Vec<(String, FunctionSig)> { + let mut functions = module + .functions + .iter() + .filter(|function| { + !function.flags.is_main && !function.name.starts_with("_class_propinit_") + }) + .map(|function| { + ( + function.name.clone(), + super::lower_inst::function_signature_from_eir(function), + ) + }) + .collect::>(); + for group in super::function_variants::collect_dispatch_groups(module) { + if functions.iter().any(|(name, _)| name == &group.name) { + continue; + } + if let Some(function) = super::function_variants::variant_callee_for_group(module, &group.name) + { + functions.push(( + group.name.clone(), + super::lower_inst::function_signature_from_eir(function), + )); + } + } + functions +} + /// Builds descriptor cases for emitted public instance methods visible to eval. fn eval_instance_method_callable_cases( module: &Module, - legacy_ctx: &mut crate::codegen::context::Context, + emitter: &mut Emitter, data: &mut DataSection, -) -> Vec { + state: &mut EvalCallableEmitState, +) -> Vec { eval_instance_callable_cases( module, - legacy_ctx, + emitter, data, - RuntimeInstanceCallableShape::InstanceMethod, + state, + EvalInstanceCallableShape::InstanceMethod, |_| true, ) } @@ -406,14 +459,16 @@ fn eval_instance_method_callable_cases( /// Builds descriptor cases for emitted public `__invoke` methods visible to eval. fn eval_invokable_object_callable_cases( module: &Module, - legacy_ctx: &mut crate::codegen::context::Context, + emitter: &mut Emitter, data: &mut DataSection, -) -> Vec { + state: &mut EvalCallableEmitState, +) -> Vec { eval_instance_callable_cases( module, - legacy_ctx, + emitter, data, - RuntimeInstanceCallableShape::ObjectInvoke, + state, + EvalInstanceCallableShape::ObjectInvoke, |method_name| php_symbol_key(method_name) == "__invoke", ) } @@ -421,15 +476,16 @@ fn eval_invokable_object_callable_cases( /// Builds receiver-bound descriptor cases for public emitted instance methods. fn eval_instance_callable_cases( module: &Module, - legacy_ctx: &mut crate::codegen::context::Context, + emitter: &mut Emitter, data: &mut DataSection, - shape: RuntimeInstanceCallableShape, + state: &mut EvalCallableEmitState, + shape: EvalInstanceCallableShape, include_method: impl Fn(&str) -> bool, -) -> Vec { - let emitted_methods = super::eir_class_method_keys(module); +) -> Vec { + let emitted_methods = eval_eir_class_method_keys(module); let mut candidates = Vec::new(); for (class_name, class_info) in &module.class_infos { - for method_name in class_info.methods.keys() { + for (method_name, sig) in &class_info.methods { if !include_method(method_name) { continue; } @@ -446,11 +502,14 @@ fn eval_instance_callable_cases( .get(&method_key) .map(String::as_str) .unwrap_or(class_name); - if emitted_methods.contains(&(impl_class.to_string(), method_key, false)) { + if emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), false)) { candidates.push(( class_name.clone(), class_info.class_id, method_name.clone(), + method_key, + impl_class.to_string(), + sig.clone(), )); } } @@ -458,33 +517,40 @@ fn eval_instance_callable_cases( candidates.sort_by(|left, right| (&left.0, &left.2).cmp(&(&right.0, &right.2))); candidates .into_iter() - .filter_map(|(class_name, class_id, method_name)| { - receiver_bound_instance_method_case( - legacy_ctx, - data, - &class_name, - &method_name, - shape, - ) - .map(|case| RuntimeInstanceMethodCallableCase { - class_id, - method_name, - case, - }) - }) + .map( + |(class_name, class_id, method_name, method_key, impl_class, sig)| { + let case = receiver_bound_instance_method_case( + emitter, + data, + state, + &class_name, + &impl_class, + &method_name, + &method_key, + &sig, + shape, + ); + EvalInstanceMethodCallableCase { + class_id, + method_name, + case, + } + }, + ) .collect() } /// Builds descriptor cases for emitted public static methods visible to eval. fn eval_static_method_callable_cases( module: &Module, - legacy_ctx: &mut crate::codegen::context::Context, + emitter: &mut Emitter, data: &mut DataSection, + state: &mut EvalCallableEmitState, ) -> Vec { - let emitted_methods = super::eir_class_method_keys(module); + let emitted_methods = eval_eir_class_method_keys(module); let mut candidates = Vec::new(); for (class_name, class_info) in &module.class_infos { - for method_name in class_info.static_methods.keys() { + for (method_name, sig) in &class_info.static_methods { if !class_info .static_method_visibilities .get(method_name) @@ -492,165 +558,165 @@ fn eval_static_method_callable_cases( { continue; } - let method_key = crate::names::php_symbol_key(method_name); + let method_key = php_symbol_key(method_name); let impl_class = class_info .static_method_impl_classes .get(&method_key) .map(String::as_str) .unwrap_or(class_name); - if emitted_methods.contains(&(impl_class.to_string(), method_key, true)) { - candidates.push((class_name.clone(), method_name.clone())); + if emitted_methods.contains(&(impl_class.to_string(), method_key.clone(), true)) { + candidates.push(( + class_name.clone(), + method_name.clone(), + method_key, + impl_class.to_string(), + class_info.class_id, + sig.clone(), + )); } } } candidates.sort_by(|left, right| (&left.0, &left.1).cmp(&(&right.0, &right.1))); candidates .into_iter() - .filter_map(|(class_name, method_name)| { - callable_dispatch::runtime_static_method_case( - legacy_ctx, - data, - &class_name, - &method_name, - ) - .map(|case| RuntimeStaticMethodCallableCase { - class_name, - method_name, - case, - }) + .map( + |(class_name, method_name, method_key, impl_class, class_id, sig)| { + let wrapper_sig = callable_dispatch::static_method_runtime_wrapper_sig(&sig); + let entry_label = emit_eval_static_method_descriptor_entry_wrapper( + emitter, + state, + &impl_class, + &method_key, + &wrapper_sig, + class_id, + ); + let php_name = format!("{}::{}", class_name, method_name); + let invoker_label = + emit_eval_runtime_callable_invoker_inline(emitter, data, state, &wrapper_sig, &[]); + let descriptor_label = + callable_descriptor::static_descriptor_with_optional_invoker_meta( + data, + &entry_label, + Some(&php_name), + callable_descriptor::CALLABLE_DESC_KIND_STATIC_METHOD, + Some(&wrapper_sig), + &[], + &[], + CallableDescriptorInvocation::method( + CallableDescriptorShape::StaticMethod, + Some(class_name.clone()), + method_name.as_str(), + ), + Some(&invoker_label), + ); + RuntimeStaticMethodCallableCase { + class_name, + method_name, + case: RuntimeCallableCase { + label: entry_label, + descriptor_label, + php_name: Some(php_name), + }, + } + }, + ) + .collect() +} + +/// Returns class-method symbols backed by lowered EIR functions. +fn eval_eir_class_method_keys(module: &Module) -> HashSet<(String, String, bool)> { + module + .class_methods + .iter() + .filter_map(|function| { + let (class_name, method_name) = function.name.rsplit_once("::")?; + Some(( + class_name.to_string(), + php_symbol_key(method_name), + function.flags.is_static, + )) }) .collect() } /// Builds a receiver-bound descriptor case for one public instance method. +#[allow(clippy::too_many_arguments)] fn receiver_bound_instance_method_case( - legacy_ctx: &mut crate::codegen::context::Context, + emitter: &mut Emitter, data: &mut DataSection, + state: &mut EvalCallableEmitState, class_name: &str, + impl_class: &str, method_name: &str, - shape: RuntimeInstanceCallableShape, -) -> Option { - let (resolved_method_name, sig) = { - let class_info = legacy_ctx.classes.get(class_name)?; - let method_key = php_symbol_key(method_name); - let (resolved_method_name, sig) = class_info - .methods - .iter() - .find(|(candidate, _)| php_symbol_key(candidate) == method_key)?; - if !class_info - .method_visibilities - .get(resolved_method_name) - .is_some_and(|visibility| matches!(visibility, Visibility::Public)) - { - return None; - } - (resolved_method_name.clone(), sig.clone()) - }; - - let case_sig = callable_wrapper_sig(&sig); + method_key: &str, + sig: &FunctionSig, + shape: EvalInstanceCallableShape, +) -> RuntimeCallableCase { + let case_sig = callable_wrapper_sig(sig); let hidden_name = unique_hidden_param(EVAL_RECEIVER_CAPTURE_PARAM, &case_sig); let capture_ty = PhpType::Object(class_name.to_string()); let captures = vec![(hidden_name.clone(), capture_ty.clone(), false)]; - let hidden_params = vec![(hidden_name.clone(), capture_ty, false)]; - let wrapper_label = legacy_ctx.next_label("eval_callable_instance_method"); - let params = case_sig - .params - .iter() - .map(|(name, _)| name.clone()) - .collect::>(); - legacy_ctx.deferred_closures.push(DeferredClosure { - label: wrapper_label.clone(), - params, - body: receiver_bound_instance_method_wrapper_body( - &hidden_name, - &resolved_method_name, - &case_sig, - ), - sig: case_sig.clone(), - captures: captures.clone(), - hidden_params: hidden_params.clone(), - current_class: Some(class_name.to_string()), - needed: true, - }); - + let entry_label = emit_eval_instance_method_descriptor_entry_wrapper( + emitter, + state, + impl_class, + method_key, + &case_sig, + ); let invoker_label = - callable_dispatch::ensure_runtime_descriptor_invoker(legacy_ctx, &hidden_params, &case_sig); + emit_eval_runtime_callable_invoker_inline(emitter, data, state, &case_sig, &captures); let (kind, invocation_shape) = match shape { - RuntimeInstanceCallableShape::ObjectInvoke => ( + EvalInstanceCallableShape::ObjectInvoke => ( callable_descriptor::CALLABLE_DESC_KIND_OBJECT_INVOKE, CallableDescriptorShape::ObjectInvoke, ), - RuntimeInstanceCallableShape::InstanceMethod => ( + EvalInstanceCallableShape::InstanceMethod => ( callable_descriptor::CALLABLE_DESC_KIND_INSTANCE_METHOD, CallableDescriptorShape::InstanceMethod, ), }; - let php_name = format!("{}::{}", class_name, resolved_method_name); + let php_name = format!("{}::{}", class_name, method_name); let descriptor_label = callable_descriptor::static_descriptor_with_optional_invoker_meta( data, - &wrapper_label, + &entry_label, Some(&php_name), kind, Some(&case_sig), &captures, - &hidden_params, + &captures, CallableDescriptorInvocation::method( invocation_shape, Some(class_name.to_string()), - resolved_method_name, + method_name, ), - invoker_label.as_deref(), + Some(&invoker_label), ); - Some(RuntimeCallableCase { - label: wrapper_label, + RuntimeCallableCase { + label: entry_label, descriptor_label, php_name: Some(php_name), - sig: case_sig, - captures, - has_invoker: invoker_label.is_some(), - invoker_label, - }) + } } -/// Builds the synthetic wrapper body for a receiver-bound eval callable descriptor. -fn receiver_bound_instance_method_wrapper_body( - receiver_param: &str, - method_name: &str, +/// Emits a descriptor invoker inline and branches around its global entry body. +fn emit_eval_runtime_callable_invoker_inline( + emitter: &mut Emitter, + data: &mut DataSection, + state: &mut EvalCallableEmitState, sig: &FunctionSig, -) -> Vec { - let last_param_idx = sig.params.len().saturating_sub(1); - let args = sig - .params - .iter() - .enumerate() - .map(|(idx, (name, _))| { - let var = Expr::new(ExprKind::Variable(name.clone()), Span::dummy()); - if sig.variadic.is_some() && idx == last_param_idx { - Expr::new(ExprKind::Spread(Box::new(var)), Span::dummy()) - } else { - var - } - }) - .collect(); - let call = Expr::new( - ExprKind::MethodCall { - object: Box::new(Expr::new( - ExprKind::Variable(receiver_param.to_string()), - Span::dummy(), - )), - method: method_name.to_string(), - args, - }, - Span::dummy(), - ); - if sig.return_type == PhpType::Void { - vec![ - Stmt::new(StmtKind::ExprStmt(call), Span::dummy()), - Stmt::new(StmtKind::Return(None), Span::dummy()), - ] - } else { - vec![Stmt::new(StmtKind::Return(Some(call)), Span::dummy())] - } + captures: &[(String, PhpType, bool)], +) -> String { + let label = state.next_label("callable_invoker"); + let done_label = state.next_label("callable_invoker_done"); + let invoker = RuntimeCallableInvoker { + label: &label, + sig, + captures, + }; + abi::emit_jump(emitter, &done_label); + super::runtime_callable_invoker::emit_runtime_callable_invoker(emitter, data, &invoker); + emitter.label(&done_label); + label } /// Returns a hidden receiver parameter name that cannot collide with visible parameters. @@ -668,24 +734,432 @@ fn unique_hidden_param(base: &str, sig: &FunctionSig) -> String { } } -/// Emits deferred callable support bodies behind one jump. -fn emit_deferred_callable_support( +/// Emits an entry wrapper that receives visible args followed by the captured receiver. +fn emit_eval_instance_method_descriptor_entry_wrapper( emitter: &mut Emitter, - data: &mut DataSection, - legacy_ctx: &mut crate::codegen::context::Context, + state: &mut EvalCallableEmitState, + class_name: &str, + method_key: &str, + sig: &FunctionSig, +) -> String { + let visible_arg_types = descriptor_visible_arg_types(sig); + let wrapper_label = state.next_label("callable_instance_method"); + let done_label = state.next_label("callable_instance_method_done"); + abi::emit_jump(emitter, &done_label); + emitter.label(&wrapper_label); + emit_eval_instance_method_descriptor_entry_wrapper_body( + emitter, + class_name, + method_key, + &visible_arg_types, + ); + emitter.label(&done_label); + wrapper_label +} + +/// Emits an entry wrapper that prepends a concrete called-class id before calling a static method. +fn emit_eval_static_method_descriptor_entry_wrapper( + emitter: &mut Emitter, + state: &mut EvalCallableEmitState, + impl_class: &str, + method_key: &str, + sig: &FunctionSig, + called_class_id: u64, +) -> String { + let visible_arg_types = descriptor_visible_arg_types(sig); + let wrapper_label = state.next_label("static_method_descriptor_entry"); + let done_label = state.next_label("static_method_descriptor_entry_done"); + abi::emit_jump(emitter, &done_label); + emitter.label(&wrapper_label); + emit_eval_static_method_descriptor_entry_wrapper_body( + emitter, + impl_class, + method_key, + &visible_arg_types, + called_class_id, + ); + emitter.label(&done_label); + wrapper_label +} + +/// Returns codegen-representation parameter types for a descriptor entry wrapper. +fn descriptor_visible_arg_types(sig: &FunctionSig) -> Vec { + sig.params.iter().map(|(_, ty)| ty.codegen_repr()).collect() +} + +/// Emits a descriptor entry wrapper body by reordering visible args after the receiver. +fn emit_eval_instance_method_descriptor_entry_wrapper_body( + emitter: &mut Emitter, + class_name: &str, + method_key: &str, + visible_arg_types: &[PhpType], ) { - if legacy_ctx.deferred_closures.is_empty() - && legacy_ctx.deferred_fiber_wrappers.is_empty() - && legacy_ctx.deferred_callback_wrappers.is_empty() - && legacy_ctx.deferred_extern_callback_trampolines.is_empty() - && legacy_ctx.deferred_runtime_callable_invokers.is_empty() + let receiver_ty = descriptor_receiver_type(class_name); + let incoming_types = descriptor_entry_incoming_types(visible_arg_types, &receiver_ty); + let actual_types = descriptor_entry_actual_types(visible_arg_types, &receiver_ty); + let incoming_assignments = + abi::build_outgoing_arg_assignments_for_target(emitter.target, &incoming_types, 0); + let actual_assignments = + abi::build_outgoing_arg_assignments_for_target(emitter.target, &actual_types, 0); + let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); + let (actual_stack_offsets, actual_overflow_bytes) = + descriptor_entry_stack_offsets(&actual_assignments); + let frame_size = descriptor_entry_frame_size(incoming_types.len()); + + abi::emit_frame_prologue(emitter, frame_size); + for (idx, (ty, assignment)) in incoming_types + .iter() + .zip(incoming_assignments.iter()) + .enumerate() { - return; + store_descriptor_entry_incoming_arg( + emitter, + ty, + assignment, + descriptor_entry_slot_offset(idx), + incoming_stack_offsets[idx], + ); } - let done_label = legacy_ctx.next_label("eval_callable_support_done"); - abi::emit_jump(emitter, &done_label); - crate::codegen::emit_deferred_closures(emitter, data, legacy_ctx); - emitter.label(&done_label); + if actual_overflow_bytes > 0 { + abi::emit_reserve_temporary_stack(emitter, actual_overflow_bytes); + } + for (idx, (ty, assignment)) in actual_types + .iter() + .zip(actual_assignments.iter()) + .enumerate() + { + let source_idx = if idx == 0 { + visible_arg_types.len() + } else { + idx - 1 + }; + load_descriptor_entry_actual_arg( + emitter, + ty, + assignment, + descriptor_entry_slot_offset(source_idx), + actual_stack_offsets[idx], + ); + } + abi::emit_call_label(emitter, &method_symbol(class_name, method_key)); + if actual_overflow_bytes > 0 { + abi::emit_release_temporary_stack(emitter, actual_overflow_bytes); + } + abi::emit_frame_restore(emitter, frame_size); + abi::emit_return(emitter); +} + +/// Emits a static descriptor entry wrapper body by prepending the called-class id. +fn emit_eval_static_method_descriptor_entry_wrapper_body( + emitter: &mut Emitter, + impl_class: &str, + method_key: &str, + visible_arg_types: &[PhpType], + called_class_id: u64, +) { + let actual_types = { + let mut types = Vec::with_capacity(visible_arg_types.len() + 1); + types.push(PhpType::Int); + types.extend_from_slice(visible_arg_types); + types + }; + let incoming_assignments = + abi::build_outgoing_arg_assignments_for_target(emitter.target, visible_arg_types, 0); + let actual_assignments = + abi::build_outgoing_arg_assignments_for_target(emitter.target, &actual_types, 0); + let (incoming_stack_offsets, _) = descriptor_entry_stack_offsets(&incoming_assignments); + let (actual_stack_offsets, actual_overflow_bytes) = + descriptor_entry_stack_offsets(&actual_assignments); + let frame_size = descriptor_entry_frame_size(visible_arg_types.len()); + + abi::emit_frame_prologue(emitter, frame_size); + for (idx, (ty, assignment)) in visible_arg_types + .iter() + .zip(incoming_assignments.iter()) + .enumerate() + { + store_descriptor_entry_incoming_arg( + emitter, + ty, + assignment, + descriptor_entry_slot_offset(idx), + incoming_stack_offsets[idx], + ); + } + if actual_overflow_bytes > 0 { + abi::emit_reserve_temporary_stack(emitter, actual_overflow_bytes); + } + for (idx, (ty, assignment)) in actual_types + .iter() + .zip(actual_assignments.iter()) + .enumerate() + { + if idx == 0 { + load_descriptor_entry_static_class_id( + emitter, + called_class_id, + assignment, + actual_stack_offsets[idx], + ); + } else { + load_descriptor_entry_actual_arg( + emitter, + ty, + assignment, + descriptor_entry_slot_offset(idx - 1), + actual_stack_offsets[idx], + ); + } + } + abi::emit_call_label(emitter, &static_method_symbol(impl_class, method_key)); + if actual_overflow_bytes > 0 { + abi::emit_release_temporary_stack(emitter, actual_overflow_bytes); + } + abi::emit_frame_restore(emitter, frame_size); + abi::emit_return(emitter); +} + +/// Loads the concrete called-class id into a descriptor wrapper's outgoing ABI slot. +fn load_descriptor_entry_static_class_id( + emitter: &mut Emitter, + class_id: u64, + assignment: &abi::OutgoingArgAssignment, + stack_offset: Option, +) { + let reg = if assignment.in_register() { + abi::int_arg_reg_name(emitter.target, assignment.start_reg) + } else { + abi::secondary_scratch_reg(emitter) + }; + abi::emit_load_int_immediate(emitter, reg, class_id as i64); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, reg, out_offset); + } +} + +/// Returns the runtime receiver type threaded through the descriptor entry wrapper. +fn descriptor_receiver_type(class_name: &str) -> PhpType { + PhpType::Object(class_name.to_string()) +} + +/// Returns the wrapper incoming argument order: visible args followed by receiver. +fn descriptor_entry_incoming_types( + visible_arg_types: &[PhpType], + receiver_ty: &PhpType, +) -> Vec { + let mut types = visible_arg_types.to_vec(); + types.push(receiver_ty.clone()); + types +} + +/// Returns the real method ABI argument order: receiver followed by visible args. +fn descriptor_entry_actual_types( + visible_arg_types: &[PhpType], + receiver_ty: &PhpType, +) -> Vec { + let mut types = Vec::with_capacity(visible_arg_types.len() + 1); + types.push(receiver_ty.clone()); + types.extend_from_slice(visible_arg_types); + types +} + +/// Returns an aligned frame size for descriptor entry wrapper spill slots plus footer. +fn descriptor_entry_frame_size(slot_count: usize) -> usize { + align16((slot_count + 1) * 16) +} + +/// Returns the frame offset for a descriptor entry wrapper spill slot. +fn descriptor_entry_slot_offset(idx: usize) -> usize { + (idx + 1) * 16 +} + +/// Returns the local/outgoing byte size used for one descriptor wrapper argument. +fn descriptor_entry_arg_slot_size(ty: &PhpType) -> usize { + match ty.codegen_repr() { + PhpType::Void | PhpType::Never => 0, + _ => 16, + } +} + +/// Returns stack offsets for ABI assignments that overflow their target registers. +fn descriptor_entry_stack_offsets( + assignments: &[abi::OutgoingArgAssignment], +) -> (Vec>, usize) { + let mut offsets = vec![None; assignments.len()]; + let mut next_offset = 0usize; + for (idx, assignment) in assignments.iter().enumerate() { + if assignment.in_register() { + continue; + } + offsets[idx] = Some(next_offset); + next_offset += descriptor_entry_arg_slot_size(&assignment.ty); + } + (offsets, next_offset) +} + +/// Converts a descriptor overflow offset into a caller-stack frame offset. +fn descriptor_entry_caller_stack_offset(emitter: &Emitter, stack_offset: usize) -> usize { + let cursor = abi::IncomingArgCursor::for_target(emitter.target, 0); + cursor.caller_stack_offset + stack_offset +} + +/// Returns integer scratch registers that cannot overlap live descriptor argument registers. +fn descriptor_entry_int_spill_pair(emitter: &Emitter) -> (&'static str, &'static str) { + let lo_reg = abi::secondary_scratch_reg(emitter); + let hi_reg = match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => abi::tertiary_scratch_reg(emitter), + crate::codegen::platform::Arch::X86_64 => "r11", + }; + (lo_reg, hi_reg) +} + +/// Stores one incoming descriptor entry argument into its spill slot. +fn store_descriptor_entry_incoming_arg( + emitter: &mut Emitter, + ty: &PhpType, + assignment: &abi::OutgoingArgAssignment, + offset: usize, + stack_offset: Option, +) { + match ty.codegen_repr() { + PhpType::Float => { + let reg = if assignment.in_register() { + abi::float_arg_reg_name(emitter.target, assignment.start_reg) + } else { + let caller_offset = + descriptor_entry_caller_stack_offset(emitter, stack_offset.expect("stack offset")); + let spill_reg = match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => "d15", + crate::codegen::platform::Arch::X86_64 => "xmm15", + }; + abi::load_from_caller_stack(emitter, spill_reg, caller_offset); + spill_reg + }; + abi::store_at_offset(emitter, reg, offset); + } + PhpType::Str => { + let (ptr_reg, len_reg) = if assignment.in_register() { + ( + abi::int_arg_reg_name(emitter.target, assignment.start_reg), + abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1), + ) + } else { + let caller_offset = + descriptor_entry_caller_stack_offset(emitter, stack_offset.expect("stack offset")); + let (ptr_spill_reg, len_spill_reg) = descriptor_entry_int_spill_pair(emitter); + abi::load_from_caller_stack(emitter, ptr_spill_reg, caller_offset); + abi::load_from_caller_stack(emitter, len_spill_reg, caller_offset + 8); + (ptr_spill_reg, len_spill_reg) + }; + abi::store_at_offset(emitter, ptr_reg, offset); + abi::store_at_offset(emitter, len_reg, offset - 8); + } + PhpType::TaggedScalar => { + let (payload_reg, tag_reg) = if assignment.in_register() { + ( + abi::int_arg_reg_name(emitter.target, assignment.start_reg), + abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1), + ) + } else { + let caller_offset = + descriptor_entry_caller_stack_offset(emitter, stack_offset.expect("stack offset")); + let (payload_spill_reg, tag_spill_reg) = descriptor_entry_int_spill_pair(emitter); + abi::load_from_caller_stack(emitter, payload_spill_reg, caller_offset); + abi::load_from_caller_stack(emitter, tag_spill_reg, caller_offset + 8); + (payload_spill_reg, tag_spill_reg) + }; + abi::store_at_offset(emitter, payload_reg, offset); + abi::store_at_offset(emitter, tag_reg, offset - 8); + } + PhpType::Void | PhpType::Never => {} + _ => { + let reg = if assignment.in_register() { + abi::int_arg_reg_name(emitter.target, assignment.start_reg) + } else { + let caller_offset = + descriptor_entry_caller_stack_offset(emitter, stack_offset.expect("stack offset")); + let spill_reg = abi::secondary_scratch_reg(emitter); + abi::load_from_caller_stack(emitter, spill_reg, caller_offset); + spill_reg + }; + abi::store_at_offset(emitter, reg, offset); + } + } +} + +/// Loads one spilled descriptor entry argument into its real method ABI assignment. +fn load_descriptor_entry_actual_arg( + emitter: &mut Emitter, + ty: &PhpType, + assignment: &abi::OutgoingArgAssignment, + offset: usize, + stack_offset: Option, +) { + match ty.codegen_repr() { + PhpType::Float => { + let reg = if assignment.in_register() { + abi::float_arg_reg_name(emitter.target, assignment.start_reg) + } else { + match emitter.target.arch { + crate::codegen::platform::Arch::AArch64 => "d15", + crate::codegen::platform::Arch::X86_64 => "xmm15", + } + }; + abi::load_at_offset(emitter, reg, offset); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, reg, out_offset); + } + } + PhpType::Str => { + let (ptr_reg, len_reg) = if assignment.in_register() { + ( + abi::int_arg_reg_name(emitter.target, assignment.start_reg), + abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1), + ) + } else { + descriptor_entry_int_spill_pair(emitter) + }; + abi::load_at_offset(emitter, ptr_reg, offset); + abi::load_at_offset(emitter, len_reg, offset - 8); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, ptr_reg, out_offset); + abi::emit_store_to_sp(emitter, len_reg, out_offset + 8); + } + } + PhpType::TaggedScalar => { + let (payload_reg, tag_reg) = if assignment.in_register() { + ( + abi::int_arg_reg_name(emitter.target, assignment.start_reg), + abi::int_arg_reg_name(emitter.target, assignment.start_reg + 1), + ) + } else { + descriptor_entry_int_spill_pair(emitter) + }; + abi::load_at_offset(emitter, payload_reg, offset); + abi::load_at_offset(emitter, tag_reg, offset - 8); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, payload_reg, out_offset); + abi::emit_store_to_sp(emitter, tag_reg, out_offset + 8); + } + } + PhpType::Void | PhpType::Never => {} + _ => { + let reg = if assignment.in_register() { + abi::int_arg_reg_name(emitter.target, assignment.start_reg) + } else { + abi::secondary_scratch_reg(emitter) + }; + abi::load_at_offset(emitter, reg, offset); + if let Some(out_offset) = stack_offset { + abi::emit_store_to_sp(emitter, reg, out_offset); + } + } + } +} + +/// Rounds `value` up to a 16-byte multiple. +fn align16(value: usize) -> usize { + (value + 15) & !15 } /// Converts the ARM64 boxed eval argument slot into a callable descriptor pointer. @@ -956,7 +1430,7 @@ fn emit_x86_64_eval_dynamic_callable_descriptor( /// Looks up a descriptor by the string callable currently saved on the temp stack. fn emit_eval_string_callable_descriptor_lookup( - module: &Module, + _module: &Module, emitter: &mut Emitter, data: &mut DataSection, support: &EvalCallableDescriptorSupport, @@ -976,19 +1450,20 @@ fn emit_eval_string_callable_descriptor_lookup( len_offset: 8, call_reg: result_reg, }; - let mut legacy_ctx = super::lower_inst::legacy_context_from_eir_module(module); - for case in support + for (index, case) in support .string_cases .iter() .chain(support.static_array_cases.iter().map(|case| &case.case)) + .enumerate() { - let next_case = legacy_ctx.next_label("eval_callable_next"); + let next_case = format!("{}_eval_callable_next_{}", label_prefix, index); + let matched_label = format!("{}_eval_callable_match_{}", label_prefix, index); callable_dispatch::emit_branch_if_callable_case_mismatch( &selector, case, &next_case, emitter, - &mut legacy_ctx, + &matched_label, data, ); abi::emit_jump(emitter, &done_label); @@ -1155,7 +1630,7 @@ fn emit_eval_invokable_object_descriptor_lookup( fn emit_branch_if_instance_callable_array_case_mismatch( emitter: &mut Emitter, data: &mut DataSection, - case: &RuntimeInstanceMethodCallableCase, + case: &EvalInstanceMethodCallableCase, next_label: &str, ) { emit_branch_if_stack_tag_mismatch( diff --git a/src/codegen/eval_constructor_helpers.rs b/src/codegen/eval_constructor_helpers.rs index 2c38306254..129832fc6f 100644 --- a/src/codegen/eval_constructor_helpers.rs +++ b/src/codegen/eval_constructor_helpers.rs @@ -17,7 +17,7 @@ use std::collections::BTreeMap; use crate::codegen::abi; -use crate::codegen::context::{ +use crate::codegen_support::try_handlers::{ TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, TRY_HANDLER_SLOT_SIZE, }; use crate::codegen::data_section::DataSection; diff --git a/src/codegen/eval_method_helpers.rs b/src/codegen/eval_method_helpers.rs index 76d1f1f44a..a9b146f586 100644 --- a/src/codegen/eval_method_helpers.rs +++ b/src/codegen/eval_method_helpers.rs @@ -14,7 +14,7 @@ use std::collections::BTreeMap; use crate::codegen::abi; -use crate::codegen::context::{ +use crate::codegen_support::try_handlers::{ TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, TRY_HANDLER_SLOT_SIZE, }; use crate::codegen::data_section::DataSection; diff --git a/src/codegen/lower_inst.rs b/src/codegen/lower_inst.rs index c875105afc..9775c3a8f4 100644 --- a/src/codegen/lower_inst.rs +++ b/src/codegen/lower_inst.rs @@ -15,7 +15,6 @@ use crate::codegen::{ emit_box_current_value_as_mixed, emit_box_runtime_payload_as_mixed, runtime, runtime_value_tag, }; -use crate::codegen::context::Context as LegacyContext; use crate::codegen_support::try_handlers::{ TRY_HANDLER_DIAG_DEPTH_OFFSET, TRY_HANDLER_JMP_BUF_OFFSET, }; @@ -27,9 +26,7 @@ use crate::ir::{ use crate::names::{ function_symbol, ir_global_symbol, method_symbol, php_symbol_key, static_method_symbol, }; -use crate::types::{ - callable_wrapper_sig, first_class_callable_builtin_sig, ExternFunctionSig, FunctionSig, PhpType, -}; +use crate::types::{callable_wrapper_sig, first_class_callable_builtin_sig, FunctionSig, PhpType}; use super::context::FunctionContext; use super::function_variants; @@ -1601,52 +1598,6 @@ fn emit_runtime_call_wrapper_inline( Ok(label) } -/// Builds the legacy metadata context needed by reused descriptor-invoker emitters. -pub(crate) fn legacy_context_from_eir_module(module: &crate::ir::Module) -> LegacyContext { - let mut ctx = LegacyContext::new(); - for function in module - .functions - .iter() - .filter(|function| !is_property_init_thunk_function(function)) - .chain(module.class_methods.iter()) - .chain(module.closures.iter()) - { - ctx.functions - .insert(function.name.clone(), function_signature_from_eir(function)); - } - ctx.function_variant_groups = super::function_variants::collect_dispatch_groups(module) - .into_iter() - .map(|group| group.name) - .collect(); - ctx.callable_param_sigs = module.callable_param_sigs.clone(); - for decl in &module.extern_decls { - ctx.extern_functions.insert( - decl.name.clone(), - ExternFunctionSig { - name: decl.name.clone(), - params: decl - .params - .iter() - .map(|param| (param.name.clone(), param.php_type.clone())) - .collect(), - return_type: decl.return_php_type.clone(), - library: decl.link_libs.first().cloned(), - }, - ); - } - ctx.classes = module.class_infos.clone(); - ctx.interfaces = module.interface_infos.clone(); - ctx.enums = module.enum_infos.clone(); - ctx.packed_classes = module.packed_class_infos.clone(); - ctx.extern_classes = module.extern_class_infos.clone(); - ctx -} - -/// Returns true for synthetic property-default init thunks, which are not PHP callables. -fn is_property_init_thunk_function(function: &crate::ir::Function) -> bool { - function.name.starts_with("_class_propinit_") -} - /// Builds the EIR body for a PHP-ABI wrapper around a builtin or extern call. fn build_runtime_call_wrapper_function( module: &mut Module, diff --git a/src/codegen/lower_inst/arrays.rs b/src/codegen/lower_inst/arrays.rs index 93603be763..bfe2414ea3 100644 --- a/src/codegen/lower_inst/arrays.rs +++ b/src/codegen/lower_inst/arrays.rs @@ -14,7 +14,7 @@ use crate::codegen::{ emit_box_runtime_payload_as_mixed, emit_release_pushed_refcounted_temp_after_array_push, runtime_value_tag, }; -use crate::codegen::builtins::arrays::call_user_func_array::INVOKER_ARG_REF_CELL_TAG; +use crate::codegen::callable_invoker_args::INVOKER_ARG_REF_CELL_TAG; use crate::codegen::platform::Arch; use crate::codegen::sentinels::TAGGED_SCALAR_ARRAY_VALUE_TYPE; use crate::ir::{Immediate, Instruction, LocalSlotId, Op, ValueDef, ValueId}; diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 1b00b73973..81be7b1d80 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -27,8 +27,8 @@ use crate::parser::ast::{ BinOp, Expr, ExprKind, StaticReceiver, Stmt, StmtKind, TypeExpr, Visibility, }; use crate::types::{ - is_php_integer_array_key, AttrArgValue, ClassInfo, FunctionSig, InterfaceInfo, PhpType, - PropertyHookContract, + is_php_integer_array_key, AttrArgEntry, AttrArgValue, AttrKey, ClassInfo, FunctionSig, + InterfaceInfo, PhpType, PropertyHookContract, }; use super::super::super::context::FunctionContext; @@ -403,7 +403,7 @@ struct EvalNativeMemberAttributeRegistration { class_name: String, member_name: String, attribute_name: String, - attribute_args: Option>, + attribute_args: Option>, } /// Native callable default that can be registered with libelephc-magician. @@ -6801,19 +6801,24 @@ fn collect_eval_native_member_attributes( class_name: &str, member_name: &str, attribute_names: &[String], - attribute_args: &[Option>], + attribute_args: &[Option>], registrations: &mut Vec, ) { for (index, attribute_name) in attribute_names.iter().enumerate() { let Some(args) = attribute_args.get(index).cloned().flatten() else { continue; }; + let attribute_args = if eval_native_member_attribute_args_supported(&args) { + Some(args) + } else { + None + }; registrations.push(EvalNativeMemberAttributeRegistration { owner_kind, class_name: class_name.to_string(), member_name: member_name.to_string(), attribute_name: attribute_name.clone(), - attribute_args: Some(args), + attribute_args, }); } } @@ -9174,7 +9179,7 @@ fn eval_native_member_attribute_record( record.push(NATIVE_ATTRIBUTE_ARGS_SUPPORTED); eval_native_member_attribute_push_u32(&mut record, args.len()); for arg in args { - eval_native_member_attribute_push_arg(&mut record, arg); + eval_native_member_attribute_push_entry(&mut record, arg); } } None => record.push(NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED), @@ -9182,7 +9187,38 @@ fn eval_native_member_attribute_record( record } -/// Encodes one attribute argument into a member-attribute registration record. +/// Returns true when an attribute argument list can be encoded for eval registration. +fn eval_native_member_attribute_args_supported(args: &[AttrArgEntry]) -> bool { + args.iter() + .all(|entry| eval_native_member_attribute_value_supported(&entry.value)) +} + +/// Returns true when one attribute argument value can be encoded for eval registration. +fn eval_native_member_attribute_value_supported(value: &AttrArgValue) -> bool { + match value { + AttrArgValue::ConstRef(_) | AttrArgValue::ScopedConst(_, _) => false, + AttrArgValue::Array(elements) => eval_native_member_attribute_args_supported(elements), + AttrArgValue::Null + | AttrArgValue::Bool(_) + | AttrArgValue::Int(_) + | AttrArgValue::Float(_) + | AttrArgValue::Str(_) => true, + } +} + +/// Encodes one keyed attribute argument entry into a member-attribute registration record. +fn eval_native_member_attribute_push_entry(record: &mut Vec, entry: &AttrArgEntry) { + match &entry.key { + Some(AttrKey::Str(name)) => { + record.push(NATIVE_ATTRIBUTE_ARG_NAMED); + eval_native_member_attribute_push_string(record, name); + eval_native_member_attribute_push_arg(record, &entry.value); + } + Some(AttrKey::Int(_)) | None => eval_native_member_attribute_push_arg(record, &entry.value), + } +} + +/// Encodes one attribute argument value into a member-attribute registration record. fn eval_native_member_attribute_push_arg(record: &mut Vec, arg: &AttrArgValue) { match arg { AttrArgValue::Null => record.push(NATIVE_ATTRIBUTE_ARG_NULL), @@ -9202,18 +9238,16 @@ fn eval_native_member_attribute_push_arg(record: &mut Vec, arg: &AttrArgValu record.push(NATIVE_ATTRIBUTE_ARG_STRING); eval_native_member_attribute_push_string(record, value); } - AttrArgValue::Named { name, value } => { - record.push(NATIVE_ATTRIBUTE_ARG_NAMED); - eval_native_member_attribute_push_string(record, name); - eval_native_member_attribute_push_arg(record, value); - } AttrArgValue::Array(elements) => { record.push(NATIVE_ATTRIBUTE_ARG_ARRAY); eval_native_member_attribute_push_u32(record, elements.len()); for element in elements { - eval_native_member_attribute_push_arg(record, element); + eval_native_member_attribute_push_entry(record, element); } } + AttrArgValue::ConstRef(_) | AttrArgValue::ScopedConst(_, _) => { + record.push(NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED); + } } } diff --git a/src/codegen/lower_inst/callables.rs b/src/codegen/lower_inst/callables.rs index bc9534209f..c4e3a60877 100644 --- a/src/codegen/lower_inst/callables.rs +++ b/src/codegen/lower_inst/callables.rs @@ -351,6 +351,8 @@ fn extern_decl_signature(decl: &crate::ir::ExternDecl) -> FunctionSig { .iter() .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), + param_type_exprs: vec![None; decl.params.len()], + param_attributes: vec![Vec::new(); decl.params.len()], defaults: vec![None; decl.params.len()], return_type: decl.return_php_type.clone(), declared_return: true, diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index 91d805a406..a0c28399ff 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -243,17 +243,25 @@ pub(super) fn lower_object_clone_shallow( class_name ))); } - let (class_id, property_count, allow_dynamic_properties, retained_offsets) = { + let ( + class_id, + property_count, + allow_dynamic_properties, + retained_offsets, + owned_reference_property_offsets, + ) = { let class_info = ctx.module.class_infos.get(&class_name).ok_or_else(|| { CodegenIrError::unsupported(format!("unknown class {}", class_name)) })?; let retained_offsets = cloned_property_retain_offsets(class_info); + let owned_reference_property_offsets = owned_reference_property_offsets(class_info); ( class_info.class_id, class_info.properties.len(), class_info.allow_dynamic_properties, retained_offsets, + owned_reference_property_offsets, ) }; let result = inst @@ -262,7 +270,14 @@ pub(super) fn lower_object_clone_shallow( let result_reg = abi::int_result_reg(ctx.emitter); ctx.load_value_to_reg(source, result_reg)?; abi::emit_push_reg(ctx.emitter, result_reg); - emit_object_allocation(ctx, class_id, property_count, allow_dynamic_properties, &[])?; + emit_object_allocation( + ctx, + class_id, + property_count, + allow_dynamic_properties, + &[], + &owned_reference_property_offsets, + )?; ctx.store_result_value(result)?; let source_reg = abi::secondary_scratch_reg(ctx.emitter); let dest_reg = abi::symbol_scratch_reg(ctx.emitter); @@ -1647,6 +1662,7 @@ fn emit_dynamic_new_without_constructor_mixed_candidate( candidate.property_count, candidate.allow_dynamic_properties, &candidate.uninitialized_marker_offsets, + &candidate.owned_reference_property_offsets, )?; let object_reg = abi::int_result_reg(ctx.emitter); abi::emit_push_reg(ctx.emitter, object_reg); @@ -1936,6 +1952,7 @@ fn dynamic_new_without_constructor_candidate( property_count: class_info.properties.len(), allow_dynamic_properties: class_info.allow_dynamic_properties, uninitialized_marker_offsets: uninitialized_property_marker_offsets(class_info), + owned_reference_property_offsets: owned_reference_property_offsets(class_info), property_defaults, constructor_impl: None, })) diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 1d7c41cbc9..2cc187a947 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -689,7 +689,7 @@ fn emit_reflection_owner_object( )?; } else if class_name == "ReflectionFunction" { let (_, short_name) = reflection_name_parts(reflected_name); - emit_reflection_string_property_by_name(ctx, "__short_name", short_name)?; + emit_reflection_owner_string_property_by_name(ctx, class_name, "__short_name", short_name)?; } if class_name == "ReflectionEnum" { let case_names = metadata @@ -1791,7 +1791,7 @@ fn reflection_class_constant_metadata( resolve_reflection_enum_case(ctx, &reflected_class, &constant_name) { return Ok(ReflectionOwnerMetadata { - reflected_name: Some(constant_name), + reflected_name: Some(constant_name.clone()), attr_names: case.attribute_names.clone(), attr_args: case.attribute_args.clone(), interface_names: Vec::new(), @@ -6240,6 +6240,7 @@ fn emit_reflection_member_object( property_count, false, &uninitialized_marker_offsets, + &[], )?; let class_info = ctx .module @@ -6423,6 +6424,7 @@ fn emit_reflection_parameter_object( property_count, false, &uninitialized_marker_offsets, + &[], )?; emit_reflection_parameter_properties(ctx, parameter) } @@ -6840,6 +6842,7 @@ fn emit_reflection_union_type_object( property_count, false, &uninitialized_marker_offsets, + &[], )?; emit_reflection_union_type_types_property(ctx, &type_metadata.types)?; emit_reflection_owner_bool_property( @@ -6906,6 +6909,7 @@ fn emit_reflection_intersection_type_object( property_count, false, &uninitialized_marker_offsets, + &[], )?; emit_reflection_intersection_type_types_property(ctx, &type_metadata.types)?; emit_reflection_owner_bool_property(ctx, "ReflectionIntersectionType", "__allows_null", false)?; @@ -6986,6 +6990,7 @@ fn emit_reflection_named_type_object( property_count, false, &uninitialized_marker_offsets, + &[], )?; emit_reflection_string_property(ctx, &type_metadata.name, name_offset, name_offset + 8); emit_reflection_owner_bool_property( diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 7ab3a30e31..fcc60e2ced 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -336,6 +336,8 @@ fn ir_function_sig(function: &Function) -> FunctionSig { .iter() .map(|param| (param.name.clone(), param.php_type.clone())) .collect(), + param_type_exprs: vec![None; function.params.len()], + param_attributes: vec![Vec::new(); function.params.len()], defaults: vec![None; function.params.len()], return_type: function.return_php_type.clone(), declared_return: false, diff --git a/src/codegen_support/callable_descriptor.rs b/src/codegen_support/callable_descriptor.rs index 2918002878..63eea5e4dd 100644 --- a/src/codegen_support/callable_descriptor.rs +++ b/src/codegen_support/callable_descriptor.rs @@ -24,11 +24,17 @@ use crate::types::{FunctionSig, PhpType}; pub(crate) const CALLABLE_DESC_KIND_CLOSURE: u64 = CallableDescriptorShape::Closure as u64; pub(crate) const CALLABLE_DESC_KIND_FIRST_CLASS: u64 = CallableDescriptorShape::FirstClass as u64; +pub(crate) const CALLABLE_DESC_KIND_CALLBACK_ADAPTER: u64 = + CallableDescriptorShape::CallbackAdapter as u64; +pub(crate) const CALLABLE_DESC_KIND_OBJECT_INVOKE: u64 = + CallableDescriptorShape::ObjectInvoke as u64; pub(crate) const CALLABLE_DESC_KIND_FUNCTION: u64 = CallableDescriptorShape::Function as u64; pub(crate) const CALLABLE_DESC_KIND_BUILTIN: u64 = CallableDescriptorShape::Builtin as u64; pub(crate) const CALLABLE_DESC_KIND_EXTERN: u64 = CallableDescriptorShape::Extern as u64; pub(crate) const CALLABLE_DESC_KIND_STATIC_METHOD: u64 = CallableDescriptorShape::StaticMethod as u64; +pub(crate) const CALLABLE_DESC_KIND_INSTANCE_METHOD: u64 = + CallableDescriptorShape::InstanceMethod as u64; pub(crate) const CALLABLE_DESC_ENTRY_OFFSET: usize = 8; #[allow(dead_code)] diff --git a/src/eval_aot.rs b/src/eval_aot.rs index 165f1852a6..7dab17efe2 100644 --- a/src/eval_aot.rs +++ b/src/eval_aot.rs @@ -2355,7 +2355,7 @@ fn collect_stmt_scope_access(stmt: &Stmt, access: &mut EvalScopeAccess) { } StmtKind::RefAssign { target, source } => { access.write(target); - access.read(source); + collect_expr_scope_access(source, access); } StmtKind::If { condition, diff --git a/src/image_prelude/detect.rs b/src/image_prelude/detect.rs index a3dd2163a8..ef2fa5d95c 100644 --- a/src/image_prelude/detect.rs +++ b/src/image_prelude/detect.rs @@ -248,6 +248,7 @@ fn expr_refs_image(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) @@ -340,6 +341,11 @@ fn expr_refs_image(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_refs_image(object) || args.iter().any(expr_refs_image) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_refs_image(object) || expr_refs_image(method) || args.iter().any(expr_refs_image), ExprKind::StaticMethodCall { receiver, args, .. } => { receiver_refs_image(receiver) || args.iter().any(expr_refs_image) } diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 4f12532726..d38ffc1380 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -9242,6 +9242,7 @@ fn expr_contains_eval_call(expr: &Expr) -> bool { key.as_ref().is_some_and(|key| expr_contains_eval_call(key)) || value.as_ref().is_some_and(|value| expr_contains_eval_call(value)) } + ExprKind::IncludeValue { path, .. } => expr_contains_eval_call(path), ExprKind::StringLiteral(_) | ExprKind::IntLiteral(_) | ExprKind::FloatLiteral(_) diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 8235511616..6bb84f84f4 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -365,6 +365,7 @@ pub(crate) fn lower_eval_aot_function( defaults: Vec::new(), return_type: return_type.clone(), declared_return: false, + by_ref_return: false, ref_params: Vec::new(), declared_params: Vec::new(), variadic: None, @@ -392,6 +393,7 @@ pub(crate) fn lower_eval_aot_function( &check_result.enums, &check_result.interfaces, &check_result.packed_classes, + &check_result.throw_access_sites, constants, None, return_type, @@ -436,6 +438,7 @@ pub(crate) fn lower_eval_aot_scope_read_function( defaults: Vec::new(), return_type: return_type.clone(), declared_return: false, + by_ref_return: false, ref_params: vec![ false; if use_read_params { @@ -490,6 +493,7 @@ pub(crate) fn lower_eval_aot_scope_read_function( &check_result.enums, &check_result.interfaces, &check_result.packed_classes, + &check_result.throw_access_sites, constants, None, return_type, @@ -841,7 +845,6 @@ fn lower_body_into_function( source_path, ); ctx.by_ref_return = function_by_ref_return; - ctx.by_ref_return = function_by_ref_return; if let Some((scope_param, read_names, write_names, flush_names)) = eval_scope_reads { ctx.enable_eval_scope_access(scope_param, read_names, write_names, flush_names); } diff --git a/src/list_id_prelude/detect.rs b/src/list_id_prelude/detect.rs index 54bbcf0dd9..8b03391fb0 100644 --- a/src/list_id_prelude/detect.rs +++ b/src/list_id_prelude/detect.rs @@ -150,6 +150,15 @@ fn expr_refs_listid(expr: &Expr) -> bool { method, args, } => method_is_listid(method) || expr_refs_listid(object) || args.iter().any(expr_refs_listid), + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + expr_refs_listid(object) + || expr_refs_listid(method) + || args.iter().any(expr_refs_listid) + } ExprKind::StaticMethodCall { method, args, .. } => { method_is_listid(method) || args.iter().any(expr_refs_listid) } @@ -165,6 +174,7 @@ fn expr_refs_listid(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) diff --git a/src/types/call_args/mod.rs b/src/types/call_args/mod.rs index b7f4243a9a..903b73ce80 100644 --- a/src/types/call_args/mod.rs +++ b/src/types/call_args/mod.rs @@ -18,5 +18,7 @@ pub(crate) use matching::{named_param_index, regular_param_count}; pub(crate) use plan::{ CallArgPlan, CallArgPlanError, PlannedRegularArg, PlannedSourceValue, SpreadBoundsCheck, }; -pub(crate) use planner::plan_call_args_with_regular_param_count_and_assoc_spreads; +pub(crate) use planner::{ + plan_call_args, plan_call_args_with_regular_param_count_and_assoc_spreads, +}; pub(crate) use static_spread::{expand_static_assoc_spread_args, has_named_args}; diff --git a/src/types/call_args/planner.rs b/src/types/call_args/planner.rs index 7db5da0532..f881a66c62 100644 --- a/src/types/call_args/planner.rs +++ b/src/types/call_args/planner.rs @@ -13,7 +13,6 @@ use crate::parser::ast::{Expr, ExprKind}; use crate::span::Span; use crate::types::FunctionSig; -#[cfg(test)] use super::matching::regular_param_count; use super::matching::{NamedParamMatch, NamedParamTracker}; use super::plan::{ @@ -28,7 +27,6 @@ use super::static_spread::{expand_static_assoc_spread_args_with_origins, Expande /// - `trim_trailing_defaults`: when `true`, elide trailing default-only slots from the plan. /// - `allow_unknown_named_variadic`: when `true`, unknown named args are allowed and routed /// to the variadic parameter if the signature is variadic. -#[cfg(test)] pub(crate) fn plan_call_args( sig: &FunctionSig, args: &[Expr], @@ -50,7 +48,6 @@ pub(crate) fn plan_call_args( /// supplied `regular_param_count` rather than inferring it from the signature. /// Use this when the caller knows the visible parameter count (e.g., internal /// signatures with hidden implementation parameters). -#[cfg(test)] pub(crate) fn plan_call_args_with_regular_param_count( sig: &FunctionSig, args: &[Expr], diff --git a/src/types/checker/builtin_types/calendar.rs b/src/types/checker/builtin_types/calendar.rs index 1c0abe463d..2556f38304 100644 --- a/src/types/checker/builtin_types/calendar.rs +++ b/src/types/checker/builtin_types/calendar.rs @@ -42,6 +42,7 @@ fn cal_method( is_final: false, has_body: true, params, + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, diff --git a/src/types/checker/builtin_types/date_period.rs b/src/types/checker/builtin_types/date_period.rs index b0fcde3d01..6ee4eb622d 100644 --- a/src/types/checker/builtin_types/date_period.rs +++ b/src/types/checker/builtin_types/date_period.rs @@ -180,6 +180,7 @@ fn method_vis( is_final: false, has_body: true, params, + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -214,6 +215,7 @@ fn int_property(name: &str) -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(int_lit(0)), span: dummy(), attributes: Vec::new(), @@ -233,6 +235,7 @@ fn bool_property(name: &str) -> ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(Expr::new(ExprKind::BoolLiteral(false), dummy())), span: dummy(), attributes: Vec::new(), @@ -613,6 +616,7 @@ fn date_period_create_from_iso8601_string() -> ClassMethod { param("specification", Some(TypeExpr::Str), None), param("options", Some(TypeExpr::Int), Some(int_lit(0))), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -673,6 +677,7 @@ pub(crate) fn inject_builtin_date_period(class_map: &mut HashMap ClassProperty { is_static: false, is_abstract: false, by_ref: false, + is_promoted: false, default: Some(default), span: dummy(), attributes: Vec::new(), @@ -278,6 +280,7 @@ fn datetime_zone_list_identifiers() -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -436,6 +439,7 @@ fn datetime_zone_list_abbreviations() -> ClassMethod { is_final: false, has_body: true, params: Vec::new(), + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -1551,6 +1555,7 @@ fn datetime_create_from_format(class_name: &str) -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -1590,6 +1595,7 @@ fn datetime_get_last_errors(class_name: &str) -> ClassMethod { is_final: false, has_body: true, params: vec![], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -1636,6 +1642,7 @@ fn datetime_create_from_object(method_name: &str, target_class: &str) -> ClassMe None, false, )], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -1680,6 +1687,7 @@ fn datetime_create_from_timestamp(class_name: &str) -> ClassMethod { None, false, )], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -1731,6 +1739,7 @@ fn datetime_set_isodate(class_name: &str) -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -1942,6 +1951,7 @@ fn datetime_date_parse_from_format() -> ClassMethod { ("format".to_string(), Some(TypeExpr::Str), None, false), ("datetime".to_string(), Some(TypeExpr::Str), None, false), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2028,6 +2038,7 @@ fn datetime_gettimeofday() -> ClassMethod { Some(Expr::new(ExprKind::BoolLiteral(false), dummy())), false, )], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2227,6 +2238,7 @@ fn datetime_extract_micros() -> ClassMethod { is_final: false, has_body: true, params: vec![("s".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2296,6 +2308,7 @@ fn datetime_extract_modify_micros() -> ClassMethod { is_final: false, has_body: true, params: vec![("m".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2320,6 +2333,7 @@ fn datetime_strip_modify_micros() -> ClassMethod { is_final: false, has_body: true, params: vec![("m".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2344,6 +2358,7 @@ fn datetime_strip_micros() -> ClassMethod { is_final: false, has_body: true, params: vec![("s".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2375,6 +2390,7 @@ fn datetime_strftime() -> ClassMethod { ("timestamp".to_string(), Some(TypeExpr::Int), None, false), ("utc".to_string(), Some(TypeExpr::Bool), None, false), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2608,6 +2624,7 @@ fn datetime_tz_name_from_abbr() -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2771,6 +2788,7 @@ fn datetime_strptime() -> ClassMethod { ("timestamp".to_string(), Some(TypeExpr::Str), None, false), ("format".to_string(), Some(TypeExpr::Str), None, false), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2801,6 +2819,7 @@ fn datetime_sun_rs() -> ClassMethod { ("altit".to_string(), Some(TypeExpr::Float), None, false), ("limb".to_string(), Some(TypeExpr::Int), None, false), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2829,6 +2848,7 @@ fn datetime_sun_val() -> ClassMethod { ("rc".to_string(), Some(TypeExpr::Int), None, false), ("tsval".to_string(), Some(TypeExpr::Int), None, false), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2857,6 +2877,7 @@ fn datetime_sun_info() -> ClassMethod { ("latitude".to_string(), Some(TypeExpr::Float), None, false), ("longitude".to_string(), Some(TypeExpr::Float), None, false), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2916,6 +2937,7 @@ fn datetime_sunfunc() -> ClassMethod { false, ), ], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2942,6 +2964,7 @@ fn datetime_date_parse() -> ClassMethod { is_final: false, has_body: true, params: vec![("datetime".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -2991,6 +3014,7 @@ fn abstract_method( is_final: false, has_body: false, params, + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -3300,6 +3324,7 @@ fn date_interval_create_from_date_string() -> ClassMethod { is_final: false, has_body: true, params: vec![("datetime".to_string(), Some(TypeExpr::Str), None, false)], + param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, variadic_type: None, @@ -3798,6 +3823,7 @@ pub(crate) fn inject_builtin_datetime( "DateInterval".to_string(), FlattenedClass { name: "DateInterval".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -3824,6 +3850,7 @@ pub(crate) fn inject_builtin_datetime( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -3833,6 +3860,7 @@ pub(crate) fn inject_builtin_datetime( "DateTimeZone".to_string(), FlattenedClass { name: "DateTimeZone".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -3867,6 +3895,7 @@ pub(crate) fn inject_builtin_datetime( attributes: Vec::new(), constants: datetime_zone_group_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -3876,6 +3905,7 @@ pub(crate) fn inject_builtin_datetime( "DateTimeImmutable".to_string(), FlattenedClass { name: "DateTimeImmutable".to_string(), + span: dummy(), extends: None, implements: vec!["DateTimeInterface".to_string()], is_abstract: false, @@ -3896,6 +3926,7 @@ pub(crate) fn inject_builtin_datetime( attributes: Vec::new(), constants: datetime_format_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -3928,6 +3959,7 @@ pub(crate) fn inject_builtin_datetime( "DateTime".to_string(), FlattenedClass { name: "DateTime".to_string(), + span: dummy(), extends: None, implements: vec!["DateTimeInterface".to_string()], is_abstract: false, @@ -3938,6 +3970,7 @@ pub(crate) fn inject_builtin_datetime( attributes: Vec::new(), constants: datetime_format_constants(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); } @@ -3953,6 +3986,7 @@ pub(crate) fn inject_builtin_datetime( fn date_exception_subclass(name: &str, parent: &str) -> FlattenedClass { FlattenedClass { name: name.to_string(), + span: dummy(), extends: Some(parent.to_string()), implements: Vec::new(), is_abstract: false, @@ -3963,6 +3997,7 @@ fn date_exception_subclass(name: &str, parent: &str) -> FlattenedClass { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } diff --git a/src/types/checker/builtin_types/declarations.rs b/src/types/checker/builtin_types/declarations.rs index f9fa8eda9a..dae06ef040 100644 --- a/src/types/checker/builtin_types/declarations.rs +++ b/src/types/checker/builtin_types/declarations.rs @@ -265,6 +265,7 @@ pub(crate) fn inject_builtin_throwables( "ArithmeticError".to_string(), FlattenedClass { name: "ArithmeticError".to_string(), + span: crate::span::Span::dummy(), extends: Some("Error".to_string()), implements: Vec::new(), is_abstract: false, @@ -275,6 +276,7 @@ pub(crate) fn inject_builtin_throwables( attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), }, ); diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 4470652164..aedb890ea8 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -24,6 +24,11 @@ use crate::types::PhpType; use super::super::Checker; +/// Returns a dummy source span for synthetic reflection AST nodes. +fn dummy() -> crate::span::Span { + crate::span::Span::dummy() +} + /// Injects the built-in reflection types into `class_map` after verifying /// none are already declared. Each type is a dummy shell; runtime population /// happens in codegen. Returns an error if any reflection name is already in use. @@ -69,6 +74,7 @@ pub(crate) fn inject_builtin_reflection( "ReflectionAttribute".to_string(), FlattenedClass { name: "ReflectionAttribute".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -536,6 +542,7 @@ fn builtin_reflection_method_invoke_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), dummy_span, @@ -567,6 +574,7 @@ fn builtin_reflection_method_invoke_args_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), dummy_span, @@ -592,6 +600,7 @@ fn builtin_reflection_method_create_from_method_name_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("ReflectionMethod"))), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::NewObject { @@ -626,6 +635,7 @@ fn builtin_reflection_set_accessible_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), + by_ref_return: false, body: Vec::new(), span: dummy_span, attributes: Vec::new(), @@ -651,6 +661,7 @@ fn builtin_reflection_function_invoke_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), dummy_span, @@ -679,6 +690,7 @@ fn builtin_reflection_function_invoke_args_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new(ExprKind::Null, dummy_span))), dummy_span, @@ -737,6 +749,7 @@ fn builtin_reflection_slot_getter( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(return_type), by_ref_return: false, @@ -770,6 +783,7 @@ fn builtin_reflection_function_constructor_method() -> ClassMethod { params: vec![("function".to_string(), Some(TypeExpr::Str), None, false)], param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -785,6 +799,7 @@ fn builtin_reflection_function_constructor_method() -> ClassMethod { fn builtin_reflection_function() -> FlattenedClass { FlattenedClass { name: "ReflectionFunction".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -831,6 +846,7 @@ fn builtin_reflection_function() -> FlattenedClass { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } @@ -840,6 +856,7 @@ fn builtin_reflection_function() -> FlattenedClass { fn builtin_reflection_parameter() -> FlattenedClass { FlattenedClass { name: "ReflectionParameter".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -979,6 +996,7 @@ fn builtin_reflection_parameter() -> FlattenedClass { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } @@ -996,6 +1014,7 @@ fn builtin_reflection_parameter_get_default_value_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), by_ref_return: false, @@ -1174,6 +1193,7 @@ fn builtin_reflection_parameter_is_default_value_constant_method() -> ClassMetho params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), by_ref_return: false, @@ -1205,6 +1225,7 @@ fn builtin_reflection_parameter_get_default_value_constant_name_method() -> Clas params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), by_ref_return: false, @@ -1276,6 +1297,7 @@ fn builtin_reflection_parameter_can_be_passed_by_value_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), by_ref_return: false, @@ -1300,6 +1322,7 @@ fn builtin_reflection_parameter_can_be_passed_by_value_method() -> ClassMethod { fn builtin_reflection_named_type() -> FlattenedClass { FlattenedClass { name: "ReflectionNamedType".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -1331,6 +1354,7 @@ fn builtin_reflection_named_type() -> FlattenedClass { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } @@ -1338,6 +1362,7 @@ fn builtin_reflection_named_type() -> FlattenedClass { fn builtin_reflection_union_type() -> FlattenedClass { FlattenedClass { name: "ReflectionUnionType".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -1371,6 +1396,7 @@ fn builtin_reflection_union_type() -> FlattenedClass { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } @@ -1378,6 +1404,7 @@ fn builtin_reflection_union_type() -> FlattenedClass { fn builtin_reflection_intersection_type() -> FlattenedClass { FlattenedClass { name: "ReflectionIntersectionType".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -1411,6 +1438,7 @@ fn builtin_reflection_intersection_type() -> FlattenedClass { attributes: Vec::new(), constants: Vec::new(), used_traits: Vec::new(), + trait_aliases: Vec::new(), } } @@ -1439,6 +1467,7 @@ fn builtin_reflection_named_type_to_string_method() -> ClassMethod { params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -1530,6 +1559,7 @@ fn builtin_reflection_composite_type_to_string_method( params: Vec::new(), param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Str), by_ref_return: false, @@ -1584,6 +1614,7 @@ fn reflection_composite_type_append_body( fn builtin_reflection_class() -> FlattenedClass { FlattenedClass { name: "ReflectionClass".to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -2089,6 +2120,7 @@ fn builtin_reflection_class_int_method(method_name: &str, property: &str) -> Cla variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { @@ -2150,6 +2182,7 @@ fn builtin_reflection_class_has_name_method( variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(Some(contains)), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -2189,6 +2222,7 @@ fn builtin_reflection_class_get_constant_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![ Stmt::new( StmtKind::Assign { @@ -2267,6 +2301,7 @@ fn builtin_reflection_class_get_static_property_value_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(value_or_default)), dummy_span, @@ -2295,6 +2330,7 @@ fn builtin_reflection_class_set_static_property_value_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), + by_ref_return: false, body: Vec::new(), span: dummy_span, attributes: Vec::new(), @@ -2332,6 +2368,7 @@ fn builtin_reflection_class_get_reflection_constant_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![ Stmt::new( StmtKind::Foreach { @@ -2464,6 +2501,7 @@ fn builtin_reflection_class_implements_interface_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![ missing_interface_check, Stmt::new( @@ -2596,6 +2634,7 @@ fn builtin_reflection_class_is_subclass_of_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![ missing_target_check, Stmt::new( @@ -2664,6 +2703,7 @@ fn builtin_reflection_class_is_instance_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::InstanceOf { @@ -2804,6 +2844,7 @@ fn builtin_reflection_class_array_method( variadic_by_ref: false, variadic_type: None, return_type: Some(return_type.clone()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { @@ -2871,6 +2912,7 @@ fn builtin_reflection_class_filtered_array_method( variadic_by_ref: false, variadic_type: None, return_type: Some(return_type.clone()), + by_ref_return: false, body: vec![ Stmt::new( StmtKind::If { @@ -3025,6 +3067,7 @@ fn builtin_reflection_class_get_member_method( variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(return_class))), + by_ref_return: false, body: vec![ Stmt::new( StmtKind::Foreach { @@ -3071,6 +3114,7 @@ fn builtin_reflection_class_nullable_object_method( variadic_by_ref: false, variadic_type: None, return_type: Some(nullable_object_type(class_name)), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { @@ -3106,6 +3150,7 @@ fn builtin_reflection_class_object_method( variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified(class_name))), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { @@ -3137,6 +3182,7 @@ fn builtin_reflection_class_mixed_method(method_name: &str, property: &str) -> C variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { @@ -3168,6 +3214,7 @@ fn builtin_reflection_class_bool_method(method_name: &str, property: &str) -> Cl variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::PropertyAccess { @@ -3199,6 +3246,7 @@ fn builtin_reflection_method_get_prototype_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Named(Name::unqualified("ReflectionMethod"))), + by_ref_return: false, body: vec![ Stmt::new( StmtKind::If { @@ -3247,6 +3295,7 @@ fn builtin_reflection_property_is_default_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::Not(Box::new(reflection_this_property( @@ -3278,6 +3327,7 @@ fn builtin_reflection_constant_false_union_method(method_name: &str) -> ClassMet variadic_by_ref: false, variadic_type: None, return_type: Some(string_or_bool_type()), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -3300,6 +3350,7 @@ fn builtin_reflection_constant_false_bool_method(method_name: &str) -> ClassMeth variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -3322,6 +3373,7 @@ fn builtin_reflection_constant_empty_array_method(method_name: &str) -> ClassMet variadic_by_ref: false, variadic_type: None, return_type: Some(array_type()), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(empty_array()), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -3344,6 +3396,7 @@ fn builtin_reflection_constant_null_mixed_method(method_name: &str) -> ClassMeth variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(null_expr()), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -3376,6 +3429,7 @@ fn builtin_reflection_method_name_predicate_method( variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(Some(comparison)), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -3398,6 +3452,7 @@ fn builtin_reflection_property_has_type_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::BinaryOp { @@ -3452,6 +3507,7 @@ fn builtin_reflection_property_modifier_mask_method(method_name: &str, mask: i64 variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(Some(comparison)), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -3483,6 +3539,7 @@ fn builtin_reflection_property_has_hook_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(Some(has_hook)), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -3515,6 +3572,7 @@ fn builtin_reflection_property_get_hook_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(nullable_object_type("ReflectionMethod")), + by_ref_return: false, body: vec![ Stmt::new( StmtKind::If { @@ -3566,6 +3624,7 @@ fn builtin_reflection_property_get_value_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(mixed_type()), + by_ref_return: false, body: vec![ reflection_property_static_get_value_return(dummy_span), reflection_property_object_required_guard("getValue", dummy_span), @@ -3600,6 +3659,7 @@ fn builtin_reflection_property_set_value_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), + by_ref_return: false, body: vec![ reflection_property_static_value_guard("setValue", dummy_span), reflection_property_object_required_guard("setValue", dummy_span), @@ -3657,6 +3717,7 @@ fn builtin_reflection_property_is_initialized_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![ static_return, reflection_property_object_required_guard("isInitialized", dummy_span), @@ -3800,6 +3861,7 @@ fn builtin_reflection_property_is_lazy_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![Stmt::new(StmtKind::Return(false_bool()), dummy_span)], span: dummy_span, attributes: Vec::new(), @@ -3833,6 +3895,7 @@ fn builtin_reflection_property_skip_lazy_initialization_method() -> ClassMethod variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Void), + by_ref_return: false, body: vec![ Stmt::new( StmtKind::If { @@ -4074,6 +4137,7 @@ fn builtin_reflection_owner_class( methods.push(builtin_reflection_owner_get_attributes_method()); FlattenedClass { name: name.to_string(), + span: dummy(), extends: None, implements: Vec::new(), is_abstract: false, @@ -4162,6 +4226,7 @@ fn builtin_reflection_parameter_count_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(TypeExpr::Int), + by_ref_return: false, body: vec![Stmt::new( StmtKind::Return(Some(Expr::new( ExprKind::FunctionCall { @@ -4214,6 +4279,7 @@ fn builtin_reflection_function_method_is_variadic_method() -> ClassMethod { variadic_by_ref: false, variadic_type: None, return_type: Some(bool_type()), + by_ref_return: false, body: vec![ Stmt::new( StmtKind::Assign { diff --git a/src/types/checker/driver/mod.rs b/src/types/checker/driver/mod.rs index 82cb724271..531d9b064b 100644 --- a/src/types/checker/driver/mod.rs +++ b/src/types/checker/driver/mod.rs @@ -451,6 +451,7 @@ fn trait_method_reflection_sig(method: &ClassMethod) -> FunctionSig { defaults, return_type: PhpType::Mixed, declared_return: method.return_type.is_some(), + by_ref_return: method.by_ref_return, ref_params, declared_params: method .params diff --git a/src/tz_prelude/detect.rs b/src/tz_prelude/detect.rs index c0928323be..4693863b4a 100644 --- a/src/tz_prelude/detect.rs +++ b/src/tz_prelude/detect.rs @@ -153,6 +153,11 @@ fn expr_refs_tz(expr: &Expr) -> bool { method, args, } => method_is_tz(method) || expr_refs_tz(object) || args.iter().any(expr_refs_tz), + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_refs_tz(object) || expr_refs_tz(method) || args.iter().any(expr_refs_tz), ExprKind::StaticMethodCall { method, args, .. } => { method_is_tz(method) || args.iter().any(expr_refs_tz) } @@ -166,6 +171,7 @@ fn expr_refs_tz(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) diff --git a/src/var_export_prelude/detect.rs b/src/var_export_prelude/detect.rs index f61569e903..bb51010027 100644 --- a/src/var_export_prelude/detect.rs +++ b/src/var_export_prelude/detect.rs @@ -147,6 +147,11 @@ fn expr_refs_ve(expr: &Expr) -> bool { | ExprKind::NullsafeMethodCall { object, args, .. } => { expr_refs_ve(object) || args.iter().any(expr_refs_ve) } + ExprKind::NullsafeDynamicMethodCall { + object, + method, + args, + } => expr_refs_ve(object) || expr_refs_ve(method) || args.iter().any(expr_refs_ve), ExprKind::StaticMethodCall { args, .. } => args.iter().any(expr_refs_ve), ExprKind::FirstClassCallable(target) => callable_target_refs_ve(target), @@ -158,6 +163,7 @@ fn expr_refs_ve(expr: &Expr) -> bool { | ExprKind::Not(inner) | ExprKind::BitNot(inner) | ExprKind::Throw(inner) + | ExprKind::Clone(inner) | ExprKind::ErrorSuppress(inner) | ExprKind::Print(inner) | ExprKind::Spread(inner) diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 8299b90dc2..8410f1d2bc 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -60,6 +60,27 @@ const EVAL_ONLY_REFLECTION_BUILTINS: &[&str] = &[ "get_object_vars", ]; +/// Static-only registered builtins exist in the compiler before magician/eval has runtime support. +const STATIC_ONLY_REGISTRY_BUILTINS: &[&str] = &[ + "array_all", + "array_any", + "array_diff_assoc", + "array_find", + "array_intersect_assoc", + "array_is_list", + "array_key_first", + "array_key_last", + "array_merge_recursive", + "array_multisort", + "array_replace", + "array_replace_recursive", + "array_udiff", + "array_uintersect", + "array_walk_recursive", + "serialize", + "unserialize", +]; + /// Eval supports these PHP optional parameters before the static backend does. const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[ "array_reverse", @@ -81,6 +102,7 @@ fn static_php_visible_builtins_are_visible_to_eval() { let missing = elephc::builtin_metadata::php_visible_builtin_names() .iter() .copied() + .filter(|name| !STATIC_ONLY_REGISTRY_BUILTINS.contains(name)) .filter(|name| !elephc_magician::builtin_metadata::php_visible_builtin_exists(name)) .collect::>(); @@ -99,11 +121,13 @@ fn static_php_visible_builtins_have_eval_dispatch_literals() { let missing_direct = elephc::builtin_metadata::php_visible_builtin_names() .iter() .copied() + .filter(|name| !STATIC_ONLY_REGISTRY_BUILTINS.contains(name)) .filter(|name| !direct_dispatch_names.contains(*name)) .collect::>(); let missing_dynamic = elephc::builtin_metadata::php_visible_builtin_names() .iter() .copied() + .filter(|name| !STATIC_ONLY_REGISTRY_BUILTINS.contains(name)) .filter(|name| !dynamic_dispatch_names.contains(*name)) .collect::>(); @@ -176,6 +200,9 @@ fn shared_builtin_signature_shape_matches_static_signatures() { let mut mismatched_signatures = Vec::new(); for name in elephc::builtin_metadata::php_visible_builtin_names() { + if STATIC_ONLY_REGISTRY_BUILTINS.contains(name) { + continue; + } let Some(static_meta) = elephc::builtin_metadata::builtin_signature_metadata(name) else { missing_static_signature.push(*name); continue; @@ -225,6 +252,26 @@ fn shared_builtin_signature_shape_matches_static_signatures() { ); } +/// Documents compiler-visible builtins whose eval support has not landed yet. +#[test] +fn static_only_registry_builtins_remain_documented_until_eval_support_lands() { + let static_names = elephc::builtin_metadata::php_visible_builtin_names() + .iter() + .copied() + .collect::>(); + + for name in STATIC_ONLY_REGISTRY_BUILTINS { + assert!( + static_names.contains(name), + "{name} is no longer compiler-visible; remove it from the static-only allowlist" + ); + assert!( + !elephc_magician::builtin_metadata::php_visible_builtin_exists(name), + "{name} is now eval-visible; remove it from the static-only allowlist" + ); + } +} + /// Verifies a documented eval signature extension keeps the static prefix contract. fn assert_eval_signature_extends_static_signature( name: &str, diff --git a/tests/codegen/support/projects.rs b/tests/codegen/support/projects.rs index 3a0249e168..beb4200c88 100644 --- a/tests/codegen/support/projects.rs +++ b/tests/codegen/support/projects.rs @@ -27,12 +27,13 @@ fn required_libraries_for_runtime_features( fn generate_project_asm( program: &elephc::parser::ast::Program, check_result: &elephc::types::CheckResult, + source_path: &Path, heap_size: usize, gc_stats: bool, heap_debug: bool, requires_elephc_tls: bool, ) -> (String, String, elephc::codegen::RuntimeFeatures) { - let ir_module = lower_and_validate_ir_for_codegen_fixture(program, check_result); + let ir_module = lower_and_validate_ir_for_codegen_fixture(program, check_result, source_path); let exported_functions = HashMap::new(); let regalloc_linear = !matches!(std::env::var("ELEPHC_REGALLOC").as_deref(), Ok("stack")); let user_asm = elephc::codegen::generate_user_asm_from_ir_with_options( @@ -198,7 +199,7 @@ pub(crate) fn compile_expect_type_error(source: &str) -> String { elephc::codegen::set_autoload_rule_count(autoload_registry.rule_count()); let resolved = elephc::resolver::resolve(ast, &dir).expect("resolve failed"); let resolved = elephc::autoload::collect_aliases(resolved); - let resolved = elephc::pdo_prelude::inject_if_used(resolved); + let resolved = elephc::pdo_prelude::inject_if_used(resolved, false); let resolved = elephc::name_resolver::resolve(resolved).expect("name resolve failed"); let resolved = elephc::autoload::run(resolved, &dir, &autoload_registry).expect("autoload failed"); @@ -271,6 +272,7 @@ pub(crate) fn compile_and_run_files_expect_failure( let (user_asm, runtime_asm, runtime_features) = generate_project_asm( &optimized, &check_result, + &php_path, 8_388_608, false, false, @@ -344,6 +346,7 @@ pub(crate) fn compile_and_run_files_with_defines( let (user_asm, runtime_asm, runtime_features) = generate_project_asm( &optimized, &check_result, + &php_path, 8_388_608, false, false, @@ -451,6 +454,7 @@ pub(crate) fn compile_and_run_with_stdin(source: &str, stdin_data: &str) -> Stri let (user_asm, runtime_asm, runtime_features) = generate_project_asm( &optimized, &check_result, + &synthetic_main, 8_388_608, false, false, From 0b64abb54a32ac0718efa01e6d25f44bc9999b3e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 20:15:03 +0200 Subject: [PATCH 1054/1208] docs(eval): consolidate eval and magician plans --- .plans/elephc-eval-aot-complete-plan.md | 3468 -------------------- .plans/elephc-eval-complete-plan.md | 987 ------ .plans/elephc-eval-magician-plan.md | 378 +++ .plans/elephc-magician-performance-plan.md | 875 ----- 4 files changed, 378 insertions(+), 5330 deletions(-) delete mode 100644 .plans/elephc-eval-aot-complete-plan.md delete mode 100644 .plans/elephc-eval-complete-plan.md create mode 100644 .plans/elephc-eval-magician-plan.md delete mode 100644 .plans/elephc-magician-performance-plan.md diff --git a/.plans/elephc-eval-aot-complete-plan.md b/.plans/elephc-eval-aot-complete-plan.md deleted file mode 100644 index 9396eaaba6..0000000000 --- a/.plans/elephc-eval-aot-complete-plan.md +++ /dev/null @@ -1,3468 +0,0 @@ -# Piano: completare AOT per literal `eval` - -## Obiettivo - -Completare il percorso AOT per `eval('...')` quando il codice da eseguire e' -una stringa nota a compile time, evitando il passaggio attraverso -`__elephc_eval_execute` per tutti i frammenti che possono essere compilati in -modo staticamente sicuro. - -Il binario finale non deve incorporare il compilatore elephc, il parser, il -type checker o il codegen. Tutto il lavoro di parsing/lowering/codegen del -frammento literal deve avvenire a compile time. A runtime il programma deve -contenere solo: - -- codice nativo generato per il frammento AOT; -- ABI glue per leggere/scrivere lo scope eval quando necessario; -- runtime helpers gia' esistenti; -- `libelephc-magician` solo quando rimangono eval dinamici o fallback non-AOT. - -## Motivazione - -Il piano performance di magician ha introdotto un primo AOT conservativo per -literal eval, ma oggi il subset e' limitato a scalar return/output/store e a -scope read/write semplici. Frammenti reali come: - -```php -eval('$sum = 0; $n = 2; while ($n <= 100000) { ... } echo $sum;'); -``` - -sono ancora marcati come fallback e chiamano: - -```asm -bl ___elephc_eval_execute -``` - -Sul benchmark "somma dei numeri primi fino a 100000", questo mantiene il path -eval intorno a 800 ms e circa 642 MB RSS, contro circa 14 ms per Elephc -standard e circa 92 ms per PHP CLI. Il completamento dell'AOT deve trasformare -questo tipo di literal eval in codice nativo. - -## Principi - -1. Il backend attivo resta AST -> EIR -> `src/codegen_ir/`. Non estendere il - backend AST legacy. -2. Il fallback a `__elephc_eval_execute` resta obbligatorio per codice dinamico - o per costrutti non ancora supportati. -3. `eval` non ritorna implicitamente l'ultima espressione: senza `return`, - ritorna `null`. -4. `return` dentro eval esce dal frammento eval, non dalla funzione chiamante. -5. Il frammento AOT deve preservare la semantica di scope PHP: variabili del - caller visibili, scritture visibili dopo eval, variabili nuove create da - eval visibili dopo eval. -6. Nessun costrutto deve passare ad AOT se non e' semanticamente coperto da - test e fallback sicuro. -7. Ogni fase deve supportare `macos-aarch64`, `linux-aarch64` e - `linux-x86_64`, oppure essere esplicitamente target-gated con diagnostica, - test e documentazione. - -## Stato attuale - -Gia' implementato: - -- `EvalLiteralCall` conserva il frammento literal nel payload EIR. -- `src/codegen_ir/lower_inst/builtins/eval.rs` prova un parser AOT prima del - bridge. -- Il subset AOT attuale supporta scalar constants, scalar arithmetic, concat, - `echo` / `print`, `return`, scalar stores e read-modify-write scope access - tramite boxed Mixed helpers. -- I fallback literal emettono marker assembly `eval literal AOT fallback`. -- I path AOT emettono marker assembly `eval literal AOT compiled`. - -Mancante per "AOT completo" in senso utile: - -- `while`, `if`, `break`, `continue`; -- locals interni del frammento senza roundtrip continuo nello scope dinamico; -- operatori di confronto e truthiness di controllo; -- compound assignment nel subset AOT; -- chiamate statiche a builtins/funzioni gia' note; -- integrazione piu' diretta con la pipeline frontend/EIR per evitare un secondo - mini-codegen parallelo troppo grande; -- registrazione e test target-aware per frammenti AOT non banali; -- benchmark di accettazione sul caso "primi fino a 100000". - -## Stato implementazione - 2026-07-05 - -Implementato in questo ramo: - -- nuovo percorso AOT `local scalar` in - `src/codegen_ir/lower_inst/builtins/eval.rs`, separato dal percorso boxed - Mixed esistente; -- locals interni del frammento su stack temporaneo, con flag `defined` per - flushare nello scope solo le variabili effettivamente assegnate a runtime; -- supporto AOT per: - - assignment semplice e compound normalizzato dal parser; - - `echo`, `print`, `return`; - - `while`, `if`/`elseif`/`else`, `break`, `continue`; - - valori `int`/`bool` e stringhe solo in output; - - operatori `+`, `-`, `*`, `%`, `<`, `<=`, `>`, `>=`, `==`, `!=`, `&&`, - `||`, concat in `echo`; - - `strlen("literal")` foldato a compile time come primo builtin statico; - - chiamate a funzioni utente statiche gia' note con argomenti `int`/`bool` - in registri e ritorno `int`; -- fallback conservativo ancora attivo per costrutti non coperti, verificato con - `foreach`; -- test assembly/runtime per: - - loop `while` self-contained senza bridge; - - benchmark primi `100000` senza bridge, output `454396537`; - - `strlen("literal")` senza bridge; - - chiamata `inc(int $x): int` senza bridge; - - regressioni boxed scope read/write esistenti. - -Evidenza locale: - -- `cargo test --test codegen_tests literal_eval_ -- --nocapture` - - 5 test passati prima dell'aggiunta del test `strlen`; -- `cargo test --test codegen_tests test_literal_eval_static_strlen_uses_aot_without_execute_bridge -- --nocapture` - - passato; -- `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture` - - passato; -- benchmark temporaneo primi `100000`, 5 iterazioni: - - Elephc standard: mediana `13.451 ms`; - - Elephc via eval AOT: mediana `10.561 ms`; - - PHP standard: mediana `87.725 ms`; - - PHP via eval: mediana `85.066 ms`; - - assembly eval: `__elephc_eval_execute` assente, marker - `eval literal AOT compiled local scalar` presente; -- benchmark `benchmarks/magician/cases/algebra_heavy`, 5 iterazioni: - - Elephc standard: `3.52 ms`; - - Elephc via eval AOT: `4.43 ms`; - - PHP standard: `65.50 ms`; - - PHP via eval: `66.66 ms`. - -Ancora aperto: - -- il percorso `local scalar` e' ancora un mini-codegen manuale, non una - funzione EIR interna; -- il link a `elephc_magician` e' stato rimosso solo per literal eval - native-only senza locals/scope (`return 7`, `strlen("...")`, chiamata utente - statica scalar-register); resta richiesto per frammenti con locals creati - dentro eval per preservare la visibilita' PHP nello scope chiamante; -- gli helper `eval_scope_get/set` e `eval_value_*` usati per preservare scope - vivono ancora sotto la feature runtime eval; -- builtin statici foldabili supportati per `strlen("literal")`, - `intval()` e `abs()`; -- chiamate a funzioni statiche utente supportate solo per il subset - scalar-register (`int`/`bool` args, ritorno `int`, niente by-ref/variadic). - -Aggiornamento Milestone 6 - 2026-07-05: - -- `src/ir_lower/expr/mod.rs` evita `apply_eval_barrier()` per literal eval - classificati come native-only, quindi non dichiara hidden eval - context/scope quando non servono; -- `src/ir_lower/program.rs` distingue `EvalLiteralCall` native-only nello scan - `RuntimeFeatures`, includendo il sottoinsieme di chiamate utente statiche - note con argomenti letterali `int`/`bool` e ritorno `int`; -- `src/codegen_ir/lower_inst/builtins/eval.rs` usa il boxing core runtime per - i ritorni native-only e forza invece lo scope-sync per frammenti con locals - interni, evitando di perdere variabili create da eval; -- test aggiornati: `return 7`, `strlen("test")` e `inc(41)` ora verificano - assenza di `__elephc_eval_*` in user/runtime asm e assenza di - `elephc_magician` tra le librerie richieste; -- aggiunto test esplicito per `$a = 10; echo eval('return $a + 20;');`, che - verifica read dello scope caller via AOT e output `30`; -- `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 8/8; -- `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato. - -Aggiornamento Milestone 2 - 2026-07-05: - -- il parser AOT boxed ora conserva `scope_reads` e `scope_writes` per ogni - frammento literal; -- il path AOT boxed filtra la sync scope: - - flush solo dei nomi letti o scritti dal frammento; - - reload solo dei nomi scritti dal frammento; - - separazione local/global mantenuta tramite le tabelle sync esistenti; -- il test `$a = 10; $unused = 99; echo eval('return $a + 20;')` verifica che - venga emesso un solo `__elephc_eval_scope_set`, quindi `$unused` non viene - flushato nello scope eval; -- verifiche: - - `cargo test --test codegen_tests literal_eval_scope -- --nocapture`: 2/2; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 8/8; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato. - -Aggiornamento Milestone 4 - 2026-07-05: - -- il subset local-scalar AOT folda static builtins puri quando il risultato e' - completamente noto a compile time: - - `strlen("literal")`; - - `intval()` su `int`, `bool` o stringa intera parsabile; - - `abs()` su espressioni integer-literal rappresentabili come `int`; -- il classifier `RuntimeFeatures` riconosce gli stessi fold, quindi i casi - native-only non linkano `elephc_magician`; -- aggiunti test: - - builtin case-insensitive in caller namespaced: - `STRLEN("ab") + InTvAl("40") + ABS(-2)` -> `44`, senza helper eval e - senza `elephc_magician`; - - body local-scalar con `$x = intval("42"); echo $x + 1; return abs(-10);` - -> `4310`, senza bridge; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins -- --nocapture`: 2/2; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 10/10; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato. - -Aggiornamento Milestone 7 - 2026-07-05: - -- aggiunto `src/eval_aot.rs` come modulo target-independent per parsing dei - literal eval, classificazione del subset native-only e folding dei builtin - statici supportati; -- `src/ir_lower/program.rs` e `src/ir_lower/expr/mod.rs` ora usano lo stesso - classifier condiviso per decidere rispettivamente feature runtime e barriera - eval in lowering; -- `src/codegen_ir/lower_inst/builtins/eval.rs` riusa lo stesso parsing/folding - condiviso prima di emettere i path AOT, riducendo la duplicazione tra scan - feature e codegen; -- questo non e' ancora il target finale delle funzioni EIR interne, ma sposta - le decisioni di eleggibilita' fuori dal lowerer assembly e rende piu' - difficile divergere tra "non linkare magician" e "emettere davvero AOT"; -- aggiunto un primo path reale a funzione EIR interna per literal eval - self-contained senza variabili/scope: - - nome interno deterministico non esprimibile come nome PHP sorgente; - - generazione della `Function` EIR sintetica dopo il lowering dei body - ordinari e prima dello scan `RuntimeFeatures`; - - call-site `eval()` ancora rappresentato da `EvalLiteralCall`, ma il - lowerer assembly chiama la funzione EIR sintetica quando presente; - - il subset iniziale e' volutamente stretto (`echo`, `print`, `return`, - `if`/`elseif`/`else` senza scope, literal scalar, operatori scalar - no-scope, builtin statici foldabili e chiamate statiche a funzioni utente - gia' ammesse dal classifier) mentre assignment/local/scope restano sul path - AOT manuale esistente; - - i builtin statici foldabili vengono riscritti a literal integer nell'AST - del frammento prima di abbassare la funzione EIR, evitando dipendenze da - name-resolution runtime o case-sensitivity del frammento eval; -- lo scan `RuntimeFeatures` considera no-bridge anche i frammenti eleggibili - alla funzione EIR interna, non solo il vecchio subset native-only scalar; -- `src/ir_lower/expr/mod.rs` non applica piu' la barriera eval ai literal che - verranno gestiti da funzione EIR interna: questo evita di dichiarare - `EvalContext`/`EvalScope` nascosti e di trascinare helper user-side del bridge - quando il bridge non puo' essere chiamato; -- scelta semantica ancora aperta: i frammenti con assegnazioni o locals creati - dentro eval restano sul percorso `local scalar` manuale finche' non esistono - primitive EIR per sincronizzare correttamente lo scope PHP. Spostarli subito - in una funzione EIR no-param perderebbe la visibilita' delle variabili create - o modificate da eval; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_scalar_return_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_user_function_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins -- --nocapture`: 2/2; - - `cargo test --test codegen_tests test_literal_eval_if_without_scope_uses_eir_aot_function -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_scope -- --nocapture`: 2/2; - - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; - - riproduzione CLI manuale con `eval('if (true) { echo "yes"; } else { echo "no"; }')`: compilazione/link riusciti, output `yes`. - -Aggiornamento Milestone 7.1 - 2026-07-05: - -- `src/eval_aot.rs` ora materializza un `EvalAotPlan` condiviso invece di - esporre solo classifier booleani separati; -- il piano contiene: - - nome funzione EIR sintetica, quando il frammento e' eleggibile; - - AST gia' parsato/foldato per il lowering EIR; - - set conservativi di variabili lette e scritte; - - flag per variabili create dinamicamente, necessita' di eval context, - necessita' di global scope e motivo di fallback; -- `src/ir_lower/program.rs` usa `EvalAotPlan::requires_runtime_eval_bridge()` - per decidere se il modulo deve ancora linkare il runtime eval/magician; -- la raccolta dei candidati AOT EIR usa il nome e il body calcolati dal piano, - evitando di duplicare hashing/classificazione nel lowerer; -- `src/ir_lower/expr/mod.rs` continua a usare lo stesso piano per decidere se - applicare la barriera eval; -- nessuna nuova semantica di assignment/local e' stata spostata nella funzione - EIR no-param: i frammenti che leggono o scrivono scope restano sul percorso - AOT scope/local esistente finche' non vengono introdotte primitive EIR - esplicite per `eval_scope_get/set`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_if_without_scope_uses_eir_aot_function -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4. - -Aggiornamento Milestone 7.2 - 2026-07-05: - -- introdotte primitive EIR esplicite `EvalScopeGet` e `EvalScopeSet`, con - validazione immediato `GlobalName`, effetti conservativi heap/fatal/refcount - e dispatch nel backend EIR target-aware; -- `src/codegen_ir/lower_inst/builtins/eval.rs` abbassa le primitive verso gli - helper runtime esistenti `__elephc_eval_scope_get/set`, usando ABI helpers - invece di registri hardcoded; -- `src/eval_aot.rs` ora puo' produrre una seconda funzione EIR sintetica per - frammenti literal che leggono variabili note dallo scope caller ma non - scrivono scope e non creano nuove variabili; -- la funzione sintetica scope-read riceve un handle allo scope eval, e - `LoweringContext` trasforma le letture selezionate di variabili PHP in - `EvalScopeGet`; -- il call-site `eval()` sincronizza solo i nomi effettivamente letti dal - frammento prima di chiamare la funzione EIR scope-read. La selezione - local/global e' stata corretta per non trattare un nome internato dalla - funzione sintetica come storage globale reale; -- caso supportato e verificato: `$a = 10; echo eval('return $a + 20;')` - usa funzione EIR AOT con scope-read, emette un solo - `__elephc_eval_scope_set`, legge con `__elephc_eval_scope_get`, non chiama - `__elephc_eval_execute` e produce `30`; -- restano fuori da questa migrazione le scritture scope/local complete: - `EvalScopeSet` esiste come primitiva e backend, ma assignment/read-write - dentro eval resta sul percorso AOT manuale finche' non viene portato al - lowering EIR con ownership/reload completi; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_scope_read_return_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; - - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`; - - `git diff --check` focalizzato sui file AOT eval toccati: passato. - -Aggiornamento Milestone 7.3 - 2026-07-05: - -- estesa la funzione EIR scope-AOT ai frammenti read/write semplici, per - esempio `$x = $x + 1`, usando `EvalScopeGet` per leggere il valore caller e - `EvalScopeSet` per scrivere il risultato nello scope eval; -- `EvalAotPlan` distingue ora le letture che devono arrivare dal caller dalle - letture di variabili gia' inizializzate dentro il frammento. Questo evita di - spostare benchmark/local temporaries come prime-loop e while local-scalar nel - path scope EIR, che sarebbe corretto ma molto piu' costoso; -- `LoweringContext` supporta `enable_eval_scope_access(read_names, - write_names)`: le letture selezionate abbassano a `EvalScopeGet`, le - assegnazioni semplici selezionate abbassano a `EvalScopeSet` senza creare - slot locali interni; -- il call-site EIR scope-AOT: - - flusha nello scope solo i nomi letti dal caller; - - chiama la funzione EIR sintetica con l'handle dello scope; - - salva il valore di ritorno durante i reload; - - ricarica nel caller solo i nomi scritti dal frammento; - - per i global-backed writes legge dallo scope locale scritto dalla funzione - EIR, non dallo scope globale separato del bridge; -- il test read/write ora verifica output runtime (`2`) oltre a marker EIR, - `__elephc_eval_scope_get/set` e assenza di `__elephc_eval_execute`; -- restano aperti: - - creazione completa di nuove variabili via funzione EIR, quando non c'e' - una lettura caller iniziale; - - assegnazioni non semplici (`array`, property, list/unpack, by-ref, dynamic - variable); - - ownership/reload piu' ampi per shape array/object oltre al subset gia' - coperto dai sync helpers esistenti; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_scope_read_write_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; - - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. - -Aggiornamento Milestone 7.4 - 2026-07-05: - -- abilitata la funzione EIR scope-AOT anche per frammenti writes-only lineari - senza letture interne di variabili, come `eval('$created = "yes";')`; -- il gate e' volutamente stretto: non prende body con letture interne dopo - una scrittura (`$x = intval("42"); echo $x`) e non prende loop/local body - come il prime benchmark, che restano sul percorso local-scalar ottimizzato; -- il classifier conserva due viste distinte: - - letture raw del frammento, usate per capire se un writes-only e' davvero - una creazione/store lineare; - - letture che devono arrivare dal caller, usate per decidere il flush scope - prima della funzione EIR; -- il test `test_literal_eval_scalar_store_uses_aot_scope_write` ora verifica - esplicitamente il marker `eval literal AOT compiled EIR function`, la - scrittura `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute` e - output runtime `yes`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_scalar_store_uses_aot_scope_write -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins_in_local_body_use_aot -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; - - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. - -Aggiornamento Milestone 7.5 - 2026-07-05: - -- aggiunto un finalizer EIR per funzioni eval scope-aware: prima di ogni - `return` esplicito o implicito, `LoweringContext` puo' flushare un set - selezionato di local slot nel runtime eval scope tramite `EvalScopeSet`; -- `EvalAotPlan` distingue ora: - - `writes`: tutti i nomi scritti dal frammento, usati dal call-site per il - reload nel caller; - - `direct_writes`: assegnazioni che devono scrivere subito nello scope - durante il corpo EIR, usate per read-modify-write come `$x = $x + 1`; - - `flush_writes`: assegnazioni mantenute come local EIR interni e scritte - nello scope solo a fine funzione; -- i frammenti con write locali ma senza letture iniziali dal caller, inclusi - local-body con builtin foldabili, `while` self-contained e il prime-sum - benchmark, ora vengono abbassati a funzione EIR scope-aware invece che al - mini-codegen manuale `local scalar`; -- il vecchio percorso manuale `local scalar` resta come fallback di codegen per - frammenti non ancora pianificati come funzione EIR, ma non e' piu' il path - atteso per il benchmark dei primi coperto dai test; -- `requires_runtime_eval_bridge()` non tratta piu' read/write come motivo di - bridge quando il piano ha gia' una funzione EIR scope-aware: serve il runtime - eval-scope, ma non l'entrypoint interpretato `__elephc_eval_execute`; -- test aggiornati per aspettarsi il marker - `eval literal AOT compiled EIR function` nei casi local-body, while e - prime-loop; -- verifiche eseguite finora: - - `cargo test --test codegen_tests test_literal_eval_local_while_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins_in_local_body_use_aot -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_prime_loop_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 11/11; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; - - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`; - - `git diff --check` focalizzato sui file AOT eval toccati: passato. - -Aggiornamento Milestone 7.6 - 2026-07-05: - -- ampliato il folding compile-time condiviso dei builtin statici puri nel - planner AOT eval, in modo che scan runtime, lowering EIR sintetico e codegen - vedano lo stesso sottoinsieme; -- oltre a `strlen`, `intval` e `abs`, il planner folda ora: - - `boolval()` su literal `int`/`bool`/`string`/`null`; - - `ord("...")`; - - `chr()`; - - `min()` / `max()` su argomenti integer literal; - - `strtolower()`, `strtoupper()` e `strrev()` su stringhe ASCII literal; -- le trasformazioni stringa sono volutamente ASCII-only per non promettere - semantica byte-string non ancora rappresentabile in modo sicuro dall'AST - `StringLiteral`; -- aggiunto test namespaced/case-insensitive che verifica EIR AOT, assenza di - `__elephc_eval_execute`, assenza di helper eval in user/runtime assembly, - assenza di `elephc_magician` e output `AB:cd:cba77`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_more_static_builtins_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 12/12. - -Aggiornamento Milestone 7.7 - 2026-07-05: - -- il path EIR scope-read read-only puo' ora evitare completamente lo scope - runtime eval: la funzione sintetica riceve un parametro `mixed` per ogni - variabile letta dal caller; -- `src/ir_lower/function.rs` genera la signature diretta dei read params e - abbassa il body senza `EvalScopeGet` quando il frammento non scrive scope; -- `src/codegen_ir/lower_inst/builtins/eval.rs` materializza i parametri - leggendo i local slot del caller, boxandoli a `Mixed`, e chiama la funzione - EIR sintetica senza `__elephc_eval_scope_get/set` e senza - `__elephc_eval_execute`; -- `src/ir_lower/expr/mod.rs` evita la barriera eval per questo subset solo se - le variabili lette sono local PHP ordinari, non global/superglobal/by-ref, e - hanno tipi supportati; -- `src/ir_lower/program.rs` rileva comunque eventuale stato eval hidden nei - locals prima di decidere le runtime features, evitando link mancanti quando - altri path richiedono ancora magician; -- `src/ir_passes/dead_store.rs` esclude dal dead-store scalar slots letti - implicitamente da `EvalLiteralCall`: senza questa esclusione il DSE non vedeva - il load generato piu' tardi in codegen e trasformava `$a = 10` in `nop`; -- il test `$a = 10; $unused = 99; echo eval('return $a + 20;'); echo ':' . - $unused;` ora verifica: - - marker `eval literal AOT compiled EIR function with direct read params`; - - assenza di helper `__elephc_eval_*` in user/runtime assembly; - - assenza di `elephc_magician` tra le librerie richieste; - - output runtime `30:99`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_scope_read_return_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 12/12; - - `cargo test --test codegen_tests eval_scope -- --nocapture`: 4/4; - - riproduzione CLI manuale con `>` e l'unario `~`, riusando il normale lowering EIR invece di - introdurre codegen eval-specifico; -- gli assignment compound gia' normalizzati dal parser possono quindi usare il - path AOT anche per `/=`, `%=`, `&=`, `|=`, `^=`, `<<=` e `>>=`, con flush - finale nello scope eval quando il frammento crea o aggiorna variabili; -- il folding condiviso dei builtin statici ricorre anche dentro l'operando di - `~`, in linea con gli altri operatori unari supportati; -- aggiunti test per: - - divisione e potenza in frammenti no-scope, senza bridge, eval context o - link a `elephc_magician`; - - `/=` e `%=` con variabile locale creata dal frammento e visibile dopo - `eval`; - - bitwise, shift e operatori compound bitwise/shift con sync finale dello - scope; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_division_and_pow_use_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_bitwise_shift_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 29/29. - -Aggiornamento Milestone 3/7.23 - 2026-07-06: - -- il gate condiviso EIR AOT accetta ora anche l'operatore spaceship `<=>` - quando gli operandi appartengono al subset EIR sicuro; -- il supporto riusa `Op::Spaceship` e il normale lowering EIR, senza aggiungere - codegen specifico per eval; -- aggiunto test con: - - confronti no-scope interi e float; - - confronto su variabile caller `$a` tramite direct read params; - - assenza di `__elephc_eval_*`, runtime bridge e `elephc_magician`; -- rinominato il test runtime generico da `test_eval_spaceship_execute_through_bridge` - a `test_eval_spaceship_executes`, per non descrivere piu' come bridge un caso - che ora puo' passare dall'AOT; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_spaceship_uses_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_spaceship_executes -- --nocapture`: passato. - -Aggiornamento Milestone 4/7.24 - 2026-07-06: - -- il folder condiviso dei builtin statici pure per literal eval AOT copre ora - anche un sottoinsieme conservativo di builtin numerici con risultato `float`: - - `floor()` e `ceil()` su literal numerici finiti; - - `sqrt()` su literal numerici finiti non negativi; - - `round()` a un solo argomento su literal numerici finiti; -- il fold evita casi potenzialmente divergenti o non ancora rappresentati in - modo stabile nel subset eval AOT: precisione esplicita di `round`, `NaN`, - `INF`, `sqrt()` negativo, stringhe numeriche parziali e interi non - rappresentabili esattamente come `f64`; -- il programma foldato contiene `FloatLiteral`, quindi il normale gate EIR AOT - e il normale lowering EIR gestiscono il frammento senza codegen specifico per - eval; -- aggiunto test namespaced/case-insensitive con `FLOOR`, `ceil`, `sqrt` e - `round`, verificando funzione EIR AOT, assenza di helper eval, assenza di - runtime bridge e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_numeric_static_builtins_use_eir_aot_without_magician -- --nocapture`: passato. - -Aggiornamento Milestone 0/7.25 - 2026-07-06: - -- il planner condiviso `EvalAotPlan` espone ora una ragione conservativa di - fallback leggibile, invece di distinguere solo parse error e fallback - generico; -- il classifier target-independent riconosce le principali famiglie che devono - restare sul bridge finche' non sono modellate nell'AOT: - - include/require; - - declaration runtime; - - `global`/`static`; - - references/by-ref; - - dynamic calls; - - dynamic class/member access; - - object/member access; - - array/iterable; - - try/throw; - - control-flow o scope non supportato; -- il marker assembly del fallback literal mantiene il prefisso stabile - `eval literal AOT fallback`, ma ora include anche la ragione quando il - planner puo' classificarla; -- aggiornato il test fallback `foreach ([1] as $x)` per verificare il marker - `array/iterable semantics need bridge fallback`; -- verifiche: - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo check --tests`: passato. - -Aggiornamento Milestone 4/7.26 - 2026-07-06: - -- il folder condiviso dei builtin statici pure per literal eval AOT copre ora - `count()` su array literal completamente statici; -- il fold resta volutamente conservativo: - - supporta solo la forma a un argomento, senza `COUNT_RECURSIVE`; - - richiede che tutti i valori dell'array siano literal/fold statici senza - side effect; - - rifiuta spread e chiavi non letterali; - - per array associativi normalizza le chiavi stringa-intero e deduplica le - chiavi finali, cosi' `["1" => "a", 1 => "b"]` conta come PHP; -- il folder ricorre ora dentro array literal e assoc literal prima di tentare - i fold builtin, cosi' array statici con elementi gia' foldabili restano - side-effect-free; -- aggiunto test namespaced/case-insensitive con `COUNT([1, 2, 3])`, assoc - statici, chiavi duplicate normalizzate e array annidati top-level, verificando - funzione EIR AOT, assenza di helper eval, assenza di runtime bridge e assenza - di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_array_count_uses_eir_aot_without_magician -- --nocapture`: passato. - -Aggiornamento Milestone 4/7.27 - 2026-07-06: - -- il folder condiviso dei builtin statici pure per literal eval AOT copre ora - anche un sottoinsieme conservativo di builtin stringa: - - `substr()` su stringhe ASCII literal, offset non negativo e lunghezza - opzionale non negativa; - - `str_repeat()` su stringhe ASCII literal e repeat count non negativo, con - limite statico sul risultato foldato per evitare di gonfiare il binario in - compile time; -- i casi byte/locale sensibili restano volutamente fuori dal fold: stringhe - non ASCII, offset/lunghezze negative e risultati statici troppo grandi - continuano a usare il fallback disponibile; -- aggiunto test namespaced/case-insensitive con `SUBSTR`, `substr`, - `str_repeat` e `strlen(str_repeat(...))`, verificando funzione EIR AOT, - assenza di helper eval, assenza di runtime bridge e assenza di - `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_string_builtins_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 33/33. - -Aggiornamento Milestone 4/7.28 - 2026-07-06: - -- il folder condiviso dei builtin statici pure per literal eval AOT copre ora - anche trasformazioni ASCII monargomento: - - `ucfirst()` e `lcfirst()` su stringhe ASCII literal; - - `trim()`, `ltrim()`, `rtrim()` e alias `chop()` con maschera PHP default; -- il fold resta limitato alla forma senza `charlist` esplicito e rifiuta - stringhe non ASCII, cosi' non duplica la semantica completa delle maschere - custom o di stringhe byte non rappresentabili stabilmente nel subset; -- aggiunto test namespaced/case-insensitive con first-character case, trim - bilaterale e trim laterali, verificando funzione EIR AOT, assenza di helper - eval, assenza di runtime bridge e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_ascii_text_builtins_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 34/34. - -Aggiornamento Milestone 4/7.29 - 2026-07-06: - -- il folder condiviso dei builtin statici pure per literal eval AOT copre ora - anche predicate stringa binari su literal ASCII: - - `str_contains()`; - - `str_starts_with()`; - - `str_ends_with()`; -- il risultato viene riscritto a `BoolLiteral` prima del gate EIR, quindi - ternari/condizioni nel frammento possono restare sul percorso funzione EIR - senza emettere helper runtime di ricerca stringa; -- il fold rifiuta stringhe non ASCII per mantenere il sottoinsieme byte-stable - gia' usato dalle altre trasformazioni statiche stringa; -- aggiunto test namespaced/case-insensitive con match positivo, negativo e - needle vuoto, verificando funzione EIR AOT, assenza di helper eval, assenza - di runtime bridge e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_string_predicates_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 35/35. - -Aggiornamento Milestone 4/7.30 - 2026-07-06: - -- il folder condiviso dei builtin statici pure per literal eval AOT copre ora - `array_key_exists()` su array literal completamente statici; -- la normalizzazione delle chiavi statiche e' stata centralizzata in - `static_array_key_ids()`, riusata anche da `count()`: - - array numerici producono chiavi `0..len-1`; - - array associativi normalizzano stringhe-intero come PHP; - - duplicate key vengono deduplicate nella vista finale; -- il fold resta limitato a chiavi literal `int`/`string` e array literal senza - side effect, lasciando sul fallback chiavi dinamiche, spread o array non - statici; -- aggiunto test namespaced/case-insensitive con chiavi presenti/mancanti, - normalizzazione `"1"`/`1`, array numerici e associativi, verificando funzione - EIR AOT, assenza di helper eval, assenza di runtime bridge e assenza di - `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_array_count_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 36/36. - -Aggiornamento Milestone 4/7.31 - 2026-07-06: - -- il folder condiviso dei builtin statici pure per literal eval AOT normalizza - ora gli argomenti tramite `src/types/call_args::plan_call_args()` e le - signature canoniche `builtin_call_sig()` prima di provare il fold; -- questo abilita named arguments e static associative spread nei fold AOT senza - duplicare le regole PHP di matching parametri, duplicate detection, - ordinamento named/positional e default trailing; -- i fold restano conservativi: spread dinamici o materializzazioni non - statiche producono espressioni normalizzate non foldabili e quindi restano - fallback come prima; -- aggiunto test con: - - `strlen(string: "...")`; - - `strlen(...["string" => "..."])`; - - `count(value: [...])`; - - `array_key_exists(array: ..., key: ...)`; - - `str_contains(needle: ..., haystack: ...)`; - - `str_repeat(times: ..., string: ...)`; - - `substr(length: ..., string: ..., offset: ...)`; - verificando funzione EIR AOT, assenza di helper eval, assenza di runtime - bridge e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_builtin_named_args_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_scalar_builtins_use_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 37/37. - -Aggiornamento Milestone 5/7.32 - 2026-07-06: - -- il gate condiviso per chiamate statiche a funzioni utente dentro literal eval - AOT normalizza ora gli argomenti con `plan_call_args()` prima di verificare - tipi scalar-register e arita'; -- questo abilita named arguments nelle chiamate utente statiche AOT senza - mantenere un controllo locale sugli `ExprKind::NamedArg` sorgenti; -- il controllo resta conservativo: - - niente by-ref; - - niente variadic user function; - - ritorni solo `int`/`bool`/`float`/`string`; - - argomenti normalizzati solo se restano literal scalar supportati; -- aggiunto test con funzione utente `join_named(string $left, string $right, - bool $bang): string` chiamata da eval con named args fuori ordine, - verificando funzione EIR AOT, assenza di helper eval, assenza di runtime - bridge e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_user_function_named_args_use_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 38/38. - -Aggiornamento Milestone 5/7.33 - 2026-07-06: - -- il gate condiviso per chiamate statiche a funzioni utente dentro literal eval - AOT accetta ora anche default scalar dei parametri quando il call-site omette - argomenti opzionali; -- per chiamate posizionali senza spread il gate estende gli argomenti con i - default trailing gia' presenti nella `FunctionSig`, e poi applica lo stesso - controllo scalar-register usato per gli argomenti espliciti; -- per chiamate named/static-spread il gate usa `plan_call_args(..., - trim_trailing_defaults = false)` cosi' i default intermedi vengono - materializzati dal planner condiviso; -- il supporto resta conservativo: default non scalar, by-ref, variadic e tipi - non `int`/`bool`/`float`/`string` restano fuori dal subset AOT; -- aggiunto test con funzione utente `greet_default(string $name, - string $suffix = "!", bool $loud = true): string` chiamata da eval sia con - argomenti posizionali omessi sia con named args fuori ordine e default - intermedio, verificando funzione EIR AOT, assenza di helper eval, assenza di - runtime bridge e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_user_function_defaults_use_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 39/39. - -Aggiornamento Milestone 3/7.34 - 2026-07-06: - -- il gate condiviso EIR AOT accetta ora incrementi/decrementi locali - (`$i++`, `++$i`, `$i--`, `--$i`) quando la variabile e' nota come local int - definita dal frammento eval; -- la classificazione AOT traccia ora `EirLocalFacts` invece del solo set di - variabili assegnate: - - `assigned` indica variabili definite nel path corrente; - - `int_locals` indica variabili sicuramente intere, usate per accettare - inc/dec senza aprire casi PHP string/float/bool non modellati; -- i facts vengono propagati conservativamente: - - i loop usano una copia dei facts per il body/update; - - gli `if` con `else` mantengono solo facts presenti in tutti i branch; - - i `switch` non promuovono nuovi facts oltre il blocco, come prima per le - assegnazioni; -- aggiunto test con post/pre increment e decrement, `for ($j++)` e - `for (--$k)` in literal eval, verificando funzione EIR AOT, flush scope - finale e assenza di `__elephc_eval_execute`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_local_inc_dec_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 40/40. - -Aggiornamento Milestone 3/7.35 - 2026-07-06: - -- il gate condiviso EIR AOT accetta ora i construct `isset()` ed `empty()` - quando restano nel subset scalare gia' modellato dal normale lowering EIR; -- `isset()` viene abilitato solo per variabili locali sicuramente assegnate - nel frammento eval: - - niente named args o spread; - - niente variabili mancanti del caller; - - niente offset, property, nullsafe o object probes finche' la semantica lazy - specifica non viene modellata nel piano AOT; -- `empty()` viene abilitato per un singolo argomento senza named/spread quando - l'argomento e' una variabile locale assegnata o un'espressione gia' accettata - dal gate EIR AOT; -- aggiunto test con `$zero`, `$blank`, `$value` e `$nullish` definiti dentro - literal eval, verificando `isset`, `empty`, flush finale dello scope e - assenza di `__elephc_eval_execute`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_local_isset_empty_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 41/41. - -Aggiornamento Milestone 3/7.36 - 2026-07-06: - -- il gate EIR AOT accetta ora `print` anche quando e' usato come espressione, - non solo come `ExprStmt(print ...)`; -- la classificazione mantiene la semantica side-effectful di `print` delegando - al normale lowering EIR, ma usa il fatto che `print` ritorna `1` per - mantenere il tracking `int_locals` su assegnamenti come `$x = print "A";`; -- aggiunto test con `$x = print "A";` ed `echo print "B";` dentro literal eval, - verificando output, flush finale dello scope e assenza di - `__elephc_eval_execute`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_print_expression_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 42/42. - -Aggiornamento Milestone 3/7.37 - 2026-07-06: - -- il gate EIR AOT accetta ora espressioni con soppressione errori (`@expr`) - quando l'espressione interna e' gia' nel subset AOT; -- il lowering resta quello EIR normale (`ErrorSuppressBegin` / - `ErrorSuppressEnd`) e quindi non introduce un percorso speciale nel bridge - eval; -- il tracking `int_locals` propaga il tipo intero attraverso `@expr` quando il - valore interno e' noto intero, ad esempio `$x = @intval("4");`; -- aggiunto test con `@strlen("ab")` e `$x = @intval("4")` dentro literal eval, - verificando output, flush finale dello scope e assenza di - `__elephc_eval_execute`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_error_suppress_expression_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 43/43. - -Aggiornamento Milestone 3/7.38 - 2026-07-06: - -- il gate EIR AOT per `isset()` ed `empty()` accetta ora anche variabili lette - dallo scope caller tramite direct read params, non solo variabili locali gia' - assegnate nel frammento eval; -- le variabili caller mancanti continuano a essere materializzate come - `Mixed null`, come gia' avveniva per `??`, quindi: - - `isset($missing)` resta `false`; - - `empty($missing)` resta `true`; - - non serve creare eval scope/context runtime; -- il supporto resta ristretto alle variabili semplici: offset, property, - nullsafe/object probes e named/spread args restano fuori dal subset AOT fino - a modellazione lazy dedicata; -- aggiunto test con `$missing`, `$nullish`, `$zero` e `$blank` dal caller, - verificando direct read params, assenza di helper `__elephc_eval_*`, assenza - di runtime bridge, assenza di `elephc_magician` e output PHP corretto; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_scope_isset_empty_uses_direct_params_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. - -Aggiornamento Milestone 6/7.40 - 2026-07-06: - -- aggiunto un sottoinsieme direct local-store per literal eval write-only con - assegnamenti statici a variabili semplici, ad esempio - `eval('$created = "yes";'); echo $created;`; -- il classifier condiviso in `src/eval_aot.rs` riconosce questi frammenti e - restituisce l'elenco ordinato delle write statiche con categoria scalare - (`null`, `bool`, `int`, `float`, `string`); -- `src/ir_lower/expr/mod.rs` salta la eval barrier per questi frammenti, quindi - non dichiara gli slot nascosti `EvalContext` / `EvalScope` / - `EvalGlobalScope` quando non servono; -- `src/ir_lower/program.rs` evita sia la funzione AOT sintetica con - `EvalScopeSet` sia il flag `features.eval` per lo stesso subset; -- `src/codegen_ir/lower_inst/builtins/eval.rs` emette store diretti nei local - slot del caller per tipi scalari supportati, incluso `Mixed` tramite boxing - core runtime, senza `__elephc_eval_scope_set` e senza - `__elephc_eval_execute`; -- aggiunto test `test_literal_eval_scalar_store_uses_direct_local_write_without_magician`, - con verifica di output `yes`, marker direct-local-store, assenza di - `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute` e assenza di - `elephc_magician` tra le librerie richieste; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_scalar_store_uses_direct_local_write_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. - -Gap residuo Milestone 6 - 2026-07-06: - -- i frammenti con scope-write EIR piu' ricchi che non rientrano nel subset - local-scalar direct-sync usano ancora il path `EvalScopeSet`; -- `src/ir_lower/program.rs` marca correttamente ogni `EvalScopeGet` / - `EvalScopeSet` come `features.eval = true`, quindi quei casi continuano a - linkare `elephc_magician` finche' lo scope glue AOT non verra' separato dal - bridge interpretato o sostituito da direct write/reload piu' generale. - -Aggiornamento Milestone 6/7.41 - 2026-07-06: - -- aggiunto un classifier condiviso per il sottoinsieme local-scalar AOT - (`int`/`bool`, assegnamenti semplici, `if`, `while`, `break`, `continue`, - `echo`, `print`, `return` e chiamate statiche gia' supportate dal vecchio - path local-scalar); -- quando il frammento rientra in questo subset, `src/ir_lower/program.rs` - evita di generare la funzione EIR scope-aware che avrebbe introdotto - `EvalScopeSet`, e lo scan runtime non abilita `features.eval`; -- `src/ir_lower/expr/mod.rs` evita la eval barrier per lo stesso subset solo - se le write statiche possono essere ignorate perche' non materializzate nel - caller oppure scritte in local PHP compatibili (`Mixed`/union, `int`, `bool`, - o tagged int); -- `src/codegen_ir/lower_inst/builtins/eval.rs` ha ora una finalizzazione - `local scalar with direct local sync`: il frammento gira sui suoi slot - temporanei e, a fine esecuzione, copia le variabili definite nei local slot - del caller senza `ElephcEvalScope`; -- il test `test_literal_eval_local_while_uses_aot_without_execute_bridge` - legge `$sum` dopo `eval`, verificando output `55:55`, marker direct-sync, - assenza di `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, - assenza di helper eval runtime e assenza di `elephc_magician`; -- il test `test_literal_eval_prime_loop_uses_aot_without_execute_bridge` - verifica ora il benchmark dei primi con path local-scalar direct-sync, - output `454396537`, assenza di `__elephc_eval_scope_set`, - assenza di `__elephc_eval_execute` e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_local_while_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_prime_loop_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44; - - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. - -Aggiornamento Milestone 6/7.42 - 2026-07-06: - -- aggiunto un sottoinsieme direct read/write per literal eval boxed con - assegnamenti interi che leggono la stessa variabile del caller, ad esempio - `eval('$x = $x + 1;')`; -- il classifier condiviso accetta solo espressioni intere strette - (`literal int`, `$target`, unary minus e `+`, `-`, `*`, `%`) e non copre - ancora `Mixed`, stringhe, concat, altri nomi caller o conversioni PHP - implicite; -- `src/ir_lower/program.rs` salta la funzione EIR scope-aware e non abilita - `features.eval` solo quando il target e' un local PHP `int` inizializzato - prima dell'eval; -- `src/ir_lower/expr/mod.rs` evita la eval barrier per lo stesso subset solo - se il local e' gia' nello snapshot di inizializzazione e ha tipo `int`; -- `src/codegen_ir/lower_inst/builtins/eval.rs` emette il marker - `eval literal AOT compiled direct local read/write stores`, carica il local - caller, calcola l'espressione intera target-aware e riscrive direttamente lo - slot locale senza `EvalScopeGet`/`EvalScopeSet`; -- il test `test_literal_eval_scope_read_write_uses_aot_without_execute_bridge` - ora verifica output `2`, assenza di ogni `__elephc_eval_*` in user/runtime - assembly e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_scope_read_write_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. - -Aggiornamento Milestone 6/7.43 - 2026-07-06: - -- esteso il subset local-scalar direct-sync a `do/while` e `for`, con - semantica di `continue` corretta: - - in `do/while`, `continue` salta al controllo finale della condizione; - - in `for`, `continue` salta alla clausola `update` prima di tornare alla - condizione; -- il classifier condiviso in `src/eval_aot.rs` riconosce ora `do/while`, - `for` e builtin statici interi foldabili, come `strlen("...")`, nello stesso - subset usato dal backend local-scalar; -- `src/codegen_ir/lower_inst/builtins/eval.rs` rappresenta ed emette - `DoWhile` e `For` nel mini-codegen local-scalar esistente, senza introdurre - `EvalScopeSet` o helper eval runtime; -- i test `test_literal_eval_do_while_uses_aot_without_execute_bridge`, - `test_literal_eval_for_loop_uses_aot_without_execute_bridge`, - `test_literal_eval_local_inc_dec_uses_aot_without_execute_bridge` e - `test_literal_eval_static_scalar_builtins_in_local_body_use_aot` verificano - marker `local scalar with direct local sync`, assenza di - `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, assenza di - helper `__elephc_eval_*` nel runtime e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_do_while_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_for_loop_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44; - - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. - -Aggiornamento Milestone 6/7.44 - 2026-07-06: - -- esteso il subset local-scalar direct-sync a `print` usato come espressione: - il backend emette l'output dell'operando, poi materializza il valore PHP - `1` nel registro risultato; -- `@expr` viene accettato nel subset local-scalar quando `expr` e' gia' - supportato dal subset stesso; questo copre i builtin statici foldabili nei - test senza introdurre nuova semantica di error reporting; -- `src/eval_aot.rs` e `src/codegen_ir/lower_inst/builtins/eval.rs` sono stati - allineati: il classifier vede `print` come side effect di echo + risultato - `int`, e il backend rappresenta `Print` come espressione local-scalar; -- i test `test_literal_eval_print_expression_uses_aot_without_execute_bridge` - e `test_literal_eval_error_suppress_expression_uses_aot_without_execute_bridge` - verificano ora il marker `local scalar with direct local sync`, assenza di - `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, assenza di - helper `__elephc_eval_*` nel runtime e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_print_expression_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_error_suppress_expression_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44; - - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. - -Aggiornamento Milestone 6/7.45 - 2026-07-06: - -- esteso il subset local-scalar direct-sync agli operatori interi bitwise e - shift: `~`, `&`, `|`, `^`, `<<`, `>>`; -- il classifier condiviso accetta solo operandi `int`, evitando conversioni - PHP implicite non modellate; -- il backend local-scalar emette lowering target-aware: - - AArch64: `mvn`, `and`, `orr`, `eor`, `lsl`, `asr`; - - x86_64: `not`, `and`, `or`, `xor`, `shl`, `sar`, con shift count in - `cl`; -- il test `test_literal_eval_bitwise_shift_uses_aot_without_execute_bridge` - verifica ora il marker `local scalar with direct local sync`, assenza di - `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, assenza di - helper `__elephc_eval_*` nel runtime e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_bitwise_shift_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44; - - `cargo fmt`: passato con warning noti di rustfmt stabile su `ignore`. - -Aggiornamento Milestone 4/7.39 - 2026-07-06: - -- il folder condiviso dei builtin statici pure per literal eval AOT copre ora: - - `gettype()` su literal `int`, `float`, `string`, `bool` e `null`, con le - spelling PHP stabili `integer`, `double`, `string`, `boolean`, `NULL`; - - `is_scalar()` sugli stessi literal scalari/null; -- il fold resta conservativo e non folda array/object/argomenti non literal, - cosi' non elimina side effect di valutazione non modellati; -- esteso il test dei builtin statici con `GETTYPE(1.25)`, `gettype(null)`, - `IS_SCALAR("x")` e `is_scalar(null)`, mantenendo funzione EIR AOT, assenza - di helper eval e assenza di `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_more_static_builtins_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. - -Aggiornamento Milestone 3/7.46 - 2026-07-06: - -- il percorso local-scalar direct-sync accetta ora `switch` quando subject, - case e body appartengono al subset `int`/`bool` gia' supportato; -- il lowering conserva il fallthrough PHP tra case consecutivi ed emette un - target `break` locale allo switch; -- `continue` dentro lo switch resta fallback conservativo per evitare semantiche - ambigue rispetto ai loop esterni; -- `default` prima di un `case` resta fallback perche' il lowerer local-scalar - non ricostruisce ancora quell'ordine di esecuzione PHP; -- il test `test_literal_eval_switch_uses_aot_without_execute_bridge` verifica - ora marker `eval literal AOT compiled local scalar with direct local sync`, - assenza di `__elephc_eval_scope_set`, assenza di `__elephc_eval_execute`, - assenza di helper runtime `__elephc_eval_` e assenza di `elephc_magician`; -- il test `test_literal_eval_switch_default_before_case_uses_bridge_fallback` - copre il fallback obbligatorio per `default` prima di `case`; -- residui noti dopo questa tranche: - - `test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge` - e' AOT senza bridge ma usa ancora sync scope boxed, perche' `/=` produce - float PHP anche quando il divisore e' esatto; portarlo al direct-sync - richiede storage/boxing float o un modello valore piu' ricco; - - `test_literal_eval_local_isset_empty_uses_aot_without_execute_bridge` e' - AOT senza bridge ma usa ancora sync scope boxed, perche' il direct-sync - attuale non ha storage locale diretto per string/null usati da - `isset()`/`empty()`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_switch_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_switch_default_before_case_uses_bridge_fallback -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. - -Aggiornamento Milestone 6/7.47 - 2026-07-06: - -- esteso il percorso local-scalar direct-sync a variabili locali `string` e - `null` con tipo stabile, usando slot temporanei piu' larghi per conservare - pointer/length delle stringhe oltre al flag `defined`; -- aggiunto lowering diretto per `isset()` e `empty()` su variabili gia' - assegnate nel frammento eval: - - `isset()` controlla `defined` e rifiuta `null`; - - `empty()` implementa truthiness PHP per `int`, `bool`, `null`, stringa - vuota e stringa `"0"`; -- aggiunto supporto local-scalar per ternario con rami omogenei, necessario per - forme comuni come `echo empty($x) ? "Y" : "n";`; -- il flush diretto verso i local slot del caller ora puo' materializzare anche - stringhe e null senza passare da `__elephc_eval_scope_set`; -- il caso write-only `$created = "yes"` viene ora assorbito dallo stesso path - local-scalar direct-sync, restando senza scope eval, senza bridge e senza - `elephc_magician`; -- il test `test_literal_eval_local_isset_empty_uses_aot_without_execute_bridge` - verifica ora marker `eval literal AOT compiled local scalar with direct local - sync`, assenza di `__elephc_eval_scope_set`, assenza di - `__elephc_eval_execute`, assenza di helper runtime `__elephc_eval_`, assenza - di `elephc_magician` e output `InZBvL:x`; -- residuo noto dopo questa tranche: - - `test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge` - resta AOT senza bridge ma usa sync scope boxed, perche' `/=` richiede - rappresentazione float corretta nel direct-sync o una migrazione piu' - ampia al modello EIR multi-return/scope-write; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_local_isset_empty_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_scalar_store_uses_direct_local_write_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. - -Aggiornamento Milestone 6/7.48 - 2026-07-06: - -- chiuso il residuo `/=` + `%=` nel percorso local-scalar direct-sync: - `test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge` - ora usa marker `eval literal AOT compiled local scalar with direct local sync` - e non emette piu' `__elephc_eval_scope_set`; -- il classifier condiviso in `src/eval_aot.rs` accetta ora `float`, `/` come - risultato `float` e `%` su operandi numerici con risultato `int`; -- i cambi tipo local-scalar sono permessi solo nel flusso lineare del - frammento, mentre branch/loop/switch restano conservativi per evitare tipi - finali path-dependent; -- il lowering local-scalar in - `src/codegen_ir/lower_inst/builtins/eval.rs` conserva il tipo al punto di - lettura della variabile, quindi uno stesso slot puo' transitare correttamente - da `int` a `float` e tornare a `int`; -- aggiunti storage/load temporanei per `float`, divisione target-aware - `d0/d1` o `xmm0/xmm1`, modulo con coercizione numerica a int e boxing - eval-runtime/core-runtime dei float; -- il gating in `src/ir_lower/program.rs` considera supportato anche il - direct-sync verso slot caller `float`; -- dopo questa tranche non restano asserzioni positive su - `__elephc_eval_scope_set` nei test `literal_eval_`: i casi rimasti sono - asserzioni negative; -- verifiche: - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests test_literal_eval_division_modulo_assign_uses_aot_without_execute_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 44/44. - -Aggiornamento Milestone 6/7.49 - 2026-07-06: - -- esteso il path boxed direct read/write per literal eval a read-modify-write - numerici con risultato `float`, per esempio: - ` case successivo`; -- il gate `eval_aot` permette ora il caso no-scope quando gli span consentono - di ricostruire in modo sicuro la posizione del `default`; -- aggiunti: - - `test_switch_default_before_case_falls_through_in_source_order`; - - `test_literal_eval_switch_default_before_case_no_scope_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests switch_default_before_case -- --nocapture`: 3/3; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 47/47; - - `git diff --check`: passato. - -Aggiornamento Milestone 3/7.53 - 2026-07-06: - -- completata la stessa correzione `default` source-order anche nel CFG - optimizer dello `switch`, usato da reachability, DCE e analisi dei tail path; -- `build_switch_cfg` mantiene gli indici dei `case` invariati, ma calcola i - successori di fallthrough considerando la posizione sorgente del `default`: - un `case` prima del `default` cade nel `default`, e un `default` prima di un - `case` cade nel `case` successivo; -- aggiunto - `test_build_switch_cfg_tracks_default_before_later_case_successors`, che - verifica il grafo `case 1 -> default -> case 2` e la reachability entrando dal - blocco `default`; -- verifiche: - - `cargo test -p elephc --lib test_build_switch_cfg_tracks_default_before_later_case_successors -- --nocapture`: passato; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests switch_default_before_case -- --nocapture`: 3/3; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 47/47; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.54 - 2026-07-06: - -- promosso un sottoinsieme statico di array literal nel percorso eval EIR AOT: - accessi immediati come `[1, 2, 3][1]` e - `["name" => "Ada"]["name"]`; -- il gate resta conservativo: non abilita `foreach`, mutazioni array, append, - array letti dallo scope del caller o array literal restituiti direttamente; -- aggiunti helper nel classifier per riconoscere solo receiver `ArrayLiteral` / - `ArrayLiteralAssoc` con chiavi statiche `int|string` e valori gia' - EIR-safe; -- aggiunto - `test_literal_eval_static_array_literal_read_uses_eir_aot_without_magician`, - che verifica marker `eval literal AOT compiled EIR function`, assenza di - `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di - `elephc_magician` e output `2:Ada`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests array_literal_and -- --nocapture`: 3/3; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 48/48; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.55 - 2026-07-06: - -- promosso anche il return di array literal statici dal percorso eval EIR AOT, - per esempio `eval('return ["a", "b"];')` e - `eval('return ["left" => "L", "right" => "R"];')`; -- il return resta boxed `Mixed` attraverso il normale lowering EIR della - funzione eval AOT, quindi il caller puo' indicizzare il valore restituito - senza bridge eval; -- il gate e' stato ristretto per gli `ArrayLiteralAssoc`: accetta solo coppie - con chiavi esplicite stabili `int` o stringhe non intere PHP, e rifiuta le - chiavi sintetiche generate dal parser per entry associative non keyate; -- il vincolo evita di promuovere forme con semantica PHP del next automatic key, - come `["2" => "two", "tail"]`, che restano correttamente sul fallback bridge - finche' il lowering EIR non modella il cursore automatico PHP completo; -- aggiunto - `test_literal_eval_static_array_literal_return_uses_eir_aot_without_magician`, - che verifica marker `eval literal AOT compiled EIR function`, assenza di - `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di - `elephc_magician` e output `ab:LR`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_array_literal_return_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests array_literal -- --nocapture`: 35/35; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 49/49; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.56 - 2026-07-06: - -- promosso il vecchio test dei commenti eval da semplice verifica output - "through bridge" a guardrail esplicito del percorso EIR AOT; -- il caso coperto usa un frammento con commenti `//`, `#`, `/* ... */` e - `__LINE__`; e' sicuro per l'AOT attuale perche' `__LINE__` viene abbassato - dal parser a `IntLiteral`, senza richiedere il pass globale delle magic - constants; -- il test rinominato - `test_literal_eval_comments_and_line_magic_use_eir_aot_without_magician` - verifica marker `eval literal AOT compiled EIR function`, assenza di - `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di - `elephc_magician` e output `4`; -- le altre magic constants non abbassate dal parser richiedono una sostituzione - contestuale esplicita prima del lowering del frammento eval; -- verifica: - - `cargo test --test codegen_tests test_literal_eval_comments_and_line_magic_use_eir_aot_without_magician -- --nocapture`: passato. - -Aggiornamento Milestone 7/3.57 - 2026-07-06: - -- aggiunto un planner eval AOT contestuale: - `plan_literal_fragment_with_source_path_and_static_calls`; -- il planner contestuale parse-a il frammento literal e applica - `magic_constants::substitute_file_and_scope_constants` quando il modulo EIR - espone `source_path`, trasformando `__FILE__`, `__DIR__`, - `__NAMESPACE__`, `__CLASS__`, `__TRAIT__`, `__FUNCTION__` e `__METHOD__` - in literal prima di classificare il frammento; -- il `LoweringContext` porta ora il `source_path` canonico del modulo, cosi' - anche la decisione `eval_literal_needs_barrier` usa la stessa classificazione - contestuale dello scan modulo e del backend; -- aggiornati i call-site EIR contestuali in: - - `src/ir_lower/expr/mod.rs`, per evitare una barrier eval quando il - frammento con magic constants e' fully AOT; - - `src/ir_lower/program.rs`, per materializzare la funzione eval AOT e per - non marcare `RuntimeFeatures::eval` quando non serve; - - `src/codegen_ir/lower_inst/builtins/eval.rs`, per marker/fallback e - chiamate scope-read coerenti con il piano contestuale; -- promossi a EIR AOT senza bridge: - - `__FILE__` / `__DIR__` dentro eval literal, con metadata di call-site; - - magic constants di scope top-level eval (`__CLASS__`, `__NAMESPACE__`, - `__TRAIT__`, `__FUNCTION__`, `__METHOD__`) che restano stringhe vuote - anche se l'eval e' chiamato da un metodo namespaced; -- aggiunti/rinominati i test: - - `test_literal_eval_magic_file_and_dir_use_eir_aot_without_magician`; - - `test_literal_eval_scope_magic_constants_use_eir_aot_without_magician`; -- verifiche: - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests test_literal_eval_magic_file_and_dir_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_scope_magic_constants_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 52/52; - - `cargo test --test codegen_tests eval_magic -- --nocapture`: 2/2; - - `cargo test --test codegen_tests scope_magic -- --nocapture`: 1/1; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.58 - 2026-07-06: - -- promosso il caso PHP `echo` con lista separata da virgole nel percorso EIR - AOT per eval literal; -- il parser rappresenta `echo "a", "b", "c";` come - `StmtKind::Synthetic([...Echo...])`; il classifier eval AOT ora tratta - `Synthetic` come una sequenza di statement sia nel percorso no-scope EIR sia - nel percorso scope-write lineare; -- questo evita il fallback bridge per sintassi che non aggiunge semantica - dinamica, ma solo raggruppa statement gia' supportati; -- unificati i vecchi test output-only per comma-echo, `print` statement e - `return print` in - `test_literal_eval_echo_comma_and_print_use_eir_aot_without_magician`; -- il test verifica marker `eval literal AOT compiled EIR function`, assenza di - `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di - `elephc_magician` e output `abc:x:y1`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_echo_comma_and_print_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 53/53; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `git diff --check`: passato. - -Aggiornamento fallback policy - 2026-07-06: - -- separato il vecchio test legacy `array(...)` eval in due guardrail espliciti: - - `test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician`; - - `test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge`; -- i due casi ora hanno policy distinte: - - `array(...)` statico viene normalizzato dal parser nello stesso AST di `[]` - e puo' riusare il gate AOT esistente per literal array/read; - - l'assegnamento con chiave esplicita seguita da elemento non keyato dipende - dalla semantica PHP del next automatic key, che non va approssimata; -- verifiche: - - `cargo test --test parser_tests parse_legacy`: 3/3; - - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato. - -Aggiornamento Milestone 7/3.59 - 2026-07-06: - -- chiuso il fallback sintattico per la vecchia forma PHP `array(...)` quando il - contenuto e' staticamente supportato; -- estratto il parsing degli array in `src/parser/expr/arrays.rs`, usato sia da - `[...]` sia da `array(...)`, cosi' il resto della pipeline vede sempre - `ExprKind::ArrayLiteral` o `ExprKind::ArrayLiteralAssoc`; -- `parse_named_expr` intercetta `array(...)` non qualificato e - case-insensitive prima della normale logica di function-call, evitando che - `=>` venga trattato come errore degli argomenti; -- il test - `test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician` - verifica marker `eval literal AOT compiled EIR function`, assenza di - `__elephc_eval_execute`, assenza di helper `__elephc_eval_`, assenza di - `elephc_magician` e output `b:Ada`; -- resta bridge il caso - `test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge`, - perche' scrive nello scope eval e dipende dalla semantica di next-key in un - assegnamento array non ancora modellato nel percorso AOT scope-write; -- verifiche: - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo check --tests`: passato; - - `cargo test --test parser_tests parse_legacy`: 3/3; - - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 54/54; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.60 - 2026-07-06: - -- promosso il sottoinsieme di nested array literal statici nel gate eval EIR - AOT no-scope; -- `expr_is_eir_static_array_value_safe` ora tratta `ArrayLiteral` e - `ArrayLiteralAssoc` come sorgenti array statiche ricorsive, continuando a - rifiutare `Spread` e chiavi associative sintetiche generate dal parser; -- questo consente forme come: - - `eval('return [[10, 20], ["name" => "Ada"]];')`; - - `eval('return ARRAY(array(10, 20), array("name" => "Ada"));')`; -- il test static-array-return ora verifica anche output nested `20:Ada`, - mentre il test legacy-array verifica la forma case-insensitive `ARRAY(...)` - e nested `array(...)` senza bridge; -- resta invariato il guardrail bridge per `array(2 => "two", "tail")` dentro - assegnamento scope-write, perche' quello dipende ancora dal cursore PHP - `next_auto_key` in un percorso di sincronizzazione scope piu' ampio; -- verifiche: - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_array_literal_return_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 54/54; - - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.61 - 2026-07-06: - -- promosso in eval EIR AOT il sottoinsieme statico di array associativi con - elementi non keyati dopo chiavi intere o stringhe-intere note; -- il parser degli array ora mantiene un cursore `next_auto_key` allineato a PHP - 8.4 per: - - prima chiave intera negativa, dove il prossimo indice diventa `key + 1`; - - chiavi stringa intere come `"2"`, normalizzate a chiave intera; - - chiavi stringa non intere come `"02"`, che non avanzano il cursore; - - chiavi negative successive che non abbassano un cursore gia' avanzato; -- il gate eval AOT ricostruisce lo stesso cursore prima di accettare chiavi - sintetiche generate dal parser; se una chiave sintetica non corrisponde al - cursore ricostruito, il frammento resta fallback; -- restano volutamente fuori da questa tranche chiavi `null`, `bool` e `float` - nel gate AOT, perche' ampliano la superficie di coercioni/diagnostiche - PHP-observable; il test runtime bridge esistente continua a coprire quei casi; -- aggiunto - `test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician`, - che verifica `[2 => ...]`, `[-2 => ...]`, `["2" => ...]`, `["02" => ...]` - e `array(2 => ..., ...)` senza `__elephc_eval_execute`, senza helper eval - bridge e senza `elephc_magician`; -- verifiche: - - `php -r` su PHP 8.4.19 per confermare i casi `-2`, `2.7`, `true`, - `false`, `null`, `"02"` e `"2"`; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test parser_tests next_key`: 2/2; - - `cargo test --test parser_tests negative_key_does_not_decrease -- --nocapture`: 1/1; - - `cargo test --test codegen_tests test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 55/55; - - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.62 - 2026-07-06: - -- esteso il gate eval EIR AOT anche alle chiavi booleane statiche negli array - associativi; -- il parser calcola gia' `true` come chiave intera `1` e `false` come chiave - intera `0` per il cursore `next_auto_key`; il gate ora accetta - `ExprKind::BoolLiteral` come chiave statica e aggiorna lo stesso cursore; -- il test parser - `test_parse_assoc_array_next_key_after_boolean_keys` verifica che - `[true => "yes", "tail", false => "no", "end"]` generi chiavi sintetiche - `2` e `3`; -- il test - `test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician` - copre ora anche `[true => "yes", "tail"][2]` e - `[false => "no", "tail"][1]` senza bridge; -- restano fuori dal gate AOT `float` e `null` keys: - - `float` puo' emettere diagnostiche PHP-observable di conversione implicita; - - `null` richiede chiave stringa vuota e non e' ancora nel subset di key - materialization ammesso dal gate; -- verifiche: - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test parser_tests boolean_keys -- --nocapture`: 1/1; - - `cargo test --test codegen_tests test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 55/55; - - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.63 - 2026-07-06: - -- esteso il gate eval EIR AOT alle chiavi `null` statiche negli array - associativi; -- `null` non avanza il cursore `next_auto_key`, coerentemente con PHP 8.4: - viene materializzato come chiave stringa vuota `""`, mentre l'entry non - keyata successiva usa ancora la chiave automatica corrente; -- il backend EIR `HashSet` accetta ora `PhpType::Void` come chiave hash, - materializzandola come stringa vuota persistente su entrambi i target - supportati dal lowerer (`aarch64` e `x86_64`); -- il test parser - `test_parse_assoc_array_null_key_does_not_advance_auto_key` verifica che - `[null => "empty", "tail"]` mantenga `null` come chiave esplicita e generi - la chiave sintetica `0` per `tail`; -- il test - `test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician` - copre ora anche `[null => "empty"][""]` e - `[null => "empty", "tail"][0]` senza bridge eval; -- resta fuori dal gate AOT la chiave `float`, perche' in PHP 8.4 puo' - produrre diagnostiche osservabili di conversione implicita quando perde - precisione; -- verifiche: - - `php -r` su PHP 8.4.19 per confermare che `null` diventa chiave `""` e non - avanza l'auto-key; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test parser_tests null_key -- --nocapture`: 1/1; - - `cargo test --test codegen_tests test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 55/55; - - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.64 - 2026-07-06: - -- aggiunto un guardrail esplicito per gli assegnamenti di array statici nello - scope eval tramite funzione EIR AOT; -- il percorso e' gia' nativo rispetto al bridge `execute`: frammenti come - `eval('$items = ["a", "b"];')`, `eval('$map = ["name" => "Ada"];')` e - `eval('$legacy = array("x", "y");')` vengono abbassati a funzione EIR e - sincronizzati nello scope con `__elephc_eval_scope_set`; -- il test - `test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers` - verifica: - - marker `eval literal AOT compiled EIR function`; - - presenza di `__elephc_eval_scope_set`; - - assenza di `__elephc_eval_execute`; - - output runtime `b:Ada:xy`; -- questa tranche non chiude ancora Milestone 6: gli helper di eval scope sono - ancora forniti da `elephc_magician`, quindi il test documenta esplicitamente - il link corrente alla libreria invece di dichiarare un percorso fully - magician-free; -- in questa tranche restava bridge il caso con lettura interna della variabile - array appena assegnata (`$items = array(...); echo $items[3];`), perche' il - gate EIR consentiva array access statici su literal ma non ancora su - variabili locali note come array; -- verifiche: - - compilazione temporanea del frammento scope-write array per ispezionare - marker assembly e output; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test codegen_tests test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 56/56; - - `cargo test --test codegen_tests test_eval_legacy_array_literal_next_key_scope_assignment_uses_bridge -- --nocapture`: passato; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.65 - 2026-07-06: - -- chiuso il gap della tranche precedente: il classificatore EIR AOT ora tiene - traccia anche delle variabili locali sicuramente assegnate da array literal - statici; -- `EirLocalFacts` conserva `array_locals` accanto a `assigned` e `int_locals`; - un assegnamento mantiene il fact solo se il valore e' un array literal - staticamente materializzabile dal gate AOT, altrimenti lo rimuove; -- il merge dei branch conserva un local array fact solo quando la variabile e' - presente in tutti i rami, quindi un array access successivo e' ammesso solo - se l'assegnamento e' definitivamente avvenuto; -- `expr_is_eir_static_array_source_safe` accetta ora anche variabili locali - tracciate come array statici, permettendo frammenti come - `eval('$items = array(2 => "two", "tail",); echo $items[3];')` senza - `__elephc_eval_execute`; -- il test bridge precedente e' stato promosso a regressione positiva: - `test_literal_eval_legacy_array_literal_next_key_scope_assignment_uses_eir_aot_scope_helpers` - verifica marker EIR AOT, presenza di `__elephc_eval_scope_set`, assenza del - bridge execute e output runtime `tail`; -- il percorso continua a linkare `elephc_magician` per gli helper eval-scope: - questa tranche elimina il bridge di esecuzione, non ancora la dipendenza - dagli helper di scope; -- verifiche: - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_next_key_scope_assignment_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 57/57; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.66 - 2026-07-06: - -- esteso il gate eval EIR AOT alle chiavi float statiche solo quando PHP le - converte a intero senza diagnostiche osservabili; -- sono accettati float literal finiti e integral-valued, inclusa la forma - negativa (`2.0 => ...`, `-2.0 => ...`), mentre float con parte frazionaria, - `NAN` e `INF` restano fallback bridge; -- il cursore `next_auto_key` del gate usa lo stesso intero normalizzato, quindi - `[2.0 => "two", "tail"][3]` e `[-2.0 => "minus", "tail"][-1]` passano da - funzione EIR AOT senza helper eval; -- `test_eval_static_array_fractional_float_key_uses_bridge_fallback` blocca il - caso `2.7 => ...` sul bridge, perche' PHP 8.4 emette - `Implicit conversion from float ... to int loses precision`; -- verifiche: - - `php -r` su PHP 8.4.19 per confermare conversioni silenziose di `2.0` e - `-2.0`, e deprecation per `2.7`, `-2.7`, `NAN`, `INF`; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test codegen_tests test_literal_eval_static_array_next_auto_key_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_static_array_fractional_float_key_uses_bridge_fallback -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 57/57; - - `cargo test --test codegen_tests test_eval_assoc_array_literal_unkeyed_entries_use_next_key -- --nocapture`: passato; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.67 - 2026-07-06: - -- allineato il fold statico di `array_key_exists()` alla normalizzazione delle - chiavi gia' usata dal gate array AOT; -- `static_array_key_fold_id()` ora riusa `static_integer_array_key_value()` per - int, bool, stringhe intere, float integrali e forme negative supportate; -- `null` viene normalizzato come chiave stringa vuota `s:`, mentre stringhe non - intere restano chiavi stringa; -- il fold resta conservativo per float frazionari, `NAN` e `INF`, cosi' non - elimina diagnostiche PHP-observable; -- il test - `test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician` - copre ora anche `true`, `false`, `null`, `2.0` e `-2.0` in AOT senza helper - eval e senza `elephc_magician`; -- lo stesso normalizzatore alimenta `count()` sugli array statici: il test - `test_literal_eval_static_array_count_uses_eir_aot_without_magician` copre - ora collisioni `true`/`1`, `false`/`0`, `null`/`""`, `2.0`/`2` e - `-2.0`/`-2`; -- verifiche: - - `php -r` su PHP 8.4.19 per confermare `array_key_exists()` con `true`, - `false`, `null`, `2.0` e la deprecation di `2.7`; - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test codegen_tests test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_array_count_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 57/57; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.68 - 2026-07-06: - -- esteso il gate EIR AOT di `isset()` oltre le sole variabili semplici: - accetta ora anche `ArrayAccess` quando l'accesso e' gia' static-array safe - secondo `expr_is_eir_function_safe`; -- questo abilita `isset(["name" => "Ada"]["name"])` senza bridge, ma continua a - escludere array da scope caller, oggetti `ArrayAccess` e accessi dinamici non - gia' modellati dal subset statico; -- `empty()` non ha richiesto cambio perche' gia' delegava al controllo - espressione generico nel caso non-variable; -- il test - `test_literal_eval_static_array_literal_read_uses_eir_aot_without_magician` - copre ora: - - key presente con valore non-null -> `isset` true; - - key presente con valore `null` -> `isset` false; - - key mancante -> `isset` false; - - `empty()` true su stringa vuota letta da static-array access; - - `empty()` false su stringa non vuota letta da static-array access; - mantenendo assenza di helper eval, runtime bridge e `elephc_magician`; -- verifiche: - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test codegen_tests test_literal_eval_static_array_literal_read_uses_eir_aot_without_magician -- --nocapture`: passato, poi ripassato dopo l'estensione `empty`; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 57/57; - - `git diff --check`: passato. - -Aggiornamento Milestone 7/3.69 - 2026-07-06: - -- abilitato `foreach` EIR AOT per sorgenti static-array literal non vuote, - sia indexed sia associative, quando il frammento usa lo scope-aware EIR AOT; -- il gate resta conservativo: - - `foreach` by-ref resta fallback; - - in questa tranche `foreach` su array letto dallo scope caller restava bridge - fallback, poi superato dal Milestone 4/7.89; - - array statici vuoti restano esclusi per non dichiarare key/value come - definite quando PHP non esegue il body; -- `collect_scope_reads_before_writes` ora modella `foreach` come assegnazione - di `value_var` e, se presente, `key_var` prima del body: le letture di - `$item`, `$key` e `$value` dentro il body non vengono piu' classificate come - reads dallo scope caller; -- dopo il loop, key/value vengono considerate definite solo per sorgenti literal - statiche non vuote, mantenendo conservativo il caso dinamico; -- corretto il lowering EIR di `EvalScopeSet`: quando il valore da pubblicare - nello scope e' gia' un `Mixed`/`Union`, il backend fa `__rt_incref` e passa - `EVAL_SCOPE_FLAG_OWNED`; cosi' lo scope possiede una cell separata e il - cleanup dei locals della funzione AOT non invalida il valore riletto dal - caller; -- questa correzione e' necessaria per casi come - `foreach (["a" => 1, "b" => 2] as $key => $value)`, dove `$key` e' un - `Mixed` string key che deve restare visibile dopo `eval`; -- test aggiunto: - `test_literal_eval_static_foreach_uses_eir_aot_scope_helpers`, che verifica - output corretto, assenza di `__elephc_eval_execute`, uso di - `__elephc_eval_scope_set`, e persistenza di `$item`, `$key`, `$value` dopo - eval; -- aggiornato `test_eval_codegen_requires_eval_bridge` per coprire il fallback - storico di `foreach` su array proveniente dallo scope caller, superato dal - Milestone 4/7.89; -- verifiche: - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 58/58. - -Gap residuo Milestone 6/7.70 - 2026-07-06: - -- il link a `elephc_magician` resta attivo per frammenti EIR AOT che usano - `EvalScopeGet`/`EvalScopeSet`, anche quando non chiamano - `__elephc_eval_execute`; -- la causa e' che `RuntimeFeatures::eval` oggi e' sovraccarico: - - abilita il bridge eval dinamico e quindi `pcre2-*` + `elephc_magician`; - - abilita anche hidden eval scope locals, scope helper calls, class metadata - extra, `$argv`/`$argc` storage e probe di late static binding legati a eval; -- rimuovere `features.eval = true` da `EvalScopeGet`/`EvalScopeSet` non e' - sufficiente: le chiamate generated continuerebbero a referenziare - `__elephc_eval_scope_new/free/get/set`, simboli oggi esportati da - `elephc_magician`; -- piano tecnico per chiudere Milestone 6 in modo pulito: - 1. dividere `RuntimeFeatures::eval` in almeno due feature, per esempio - `eval_bridge` e `eval_scope`; - 2. mantenere `eval_bridge` per `__elephc_eval_execute`, eval function/class - dynamic calls, callable/class/property helpers consumati da magician e - PCRE richiesto da codice dinamicamente parsato; - 3. introdurre un path `eval_scope` separato per gli helper scope-only usati da - EIR AOT; - 4. spostare o reimplementare in core runtime i soli helper scope-only - `__elephc_eval_scope_new`, `__elephc_eval_scope_free`, - `__elephc_eval_scope_get`, `__elephc_eval_scope_set` e la semantica - ownership/dirty/unset minima che serve al caller reload; - 5. lasciare in `elephc_magician` il bridge completo e gli helper che richiedono - parser/interprete dinamico; - 6. aggiornare `required_libraries_for_runtime_features()` affinche' - `eval_scope` non aggiunga `elephc_magician` ne' PCRE; - 7. aggiungere regressioni per scope-read/write EIR AOT e `foreach` statico che - verifichino assenza di `__elephc_eval_execute` e assenza di - `elephc_magician` quando nessun fallback dinamico rimane; -- alternativa parziale ma meno generale: aggiungere direct caller-local sync per - alcuni frammenti write-only oggi passati da `EvalScopeSet`; questo riduce - casi specifici ma non elimina il bisogno di helper scope-only per EIR AOT con - read/write dinamicamente visibili. - -Aggiornamento Milestone 6/7.71 - 2026-07-06: - -- introdotto lo split logico di `RuntimeFeatures::eval` in - `RuntimeFeatures::{eval_bridge, eval_scope}`; -- `EvalScopeGet`, `EvalScopeSet` e la presenza di hidden eval scope state ora - richiedono `eval_scope`, mentre `__elephc_eval_execute`, fallback literal, - eval dynamic calls e helper dinamici restano sotto `eval_bridge`; -- i consumer che servono solo al bridge dinamico, come metadata extra, - superglobal storage per `$argv`/`$argc`, probe late-static e override static - property/called-class, ora guardano `eval_bridge` invece della vecchia feature - unica; -- testato empiricamente il caso scope-only senza bridge libraries: il link - fallisce perche' lo staticlib Rust trascina ancora oggetti che referenziano - wrapper `__elephc_eval_*` e simboli PCRE; -- per questo motivo `eval_scope` oggi continua deliberatamente a linkare - `pcre2-posix`, `pcre2-8` ed `elephc_magician`, ed emette ancora i bridge - wrappers quando servono helper scope-only; -- questo non chiude Milestone 6, ma isola il debito residuo: il prossimo taglio - deve spostare `__elephc_eval_scope_new/free/get/set` e la semantica minima di - ownership/dirty scope nel core runtime, poi `eval_scope` potra' smettere di - richiedere `elephc_magician` e PCRE; -- test aggiornato in `runtime_features`: - `test_eval_scope_runtime_features_keep_bridge_libraries_until_core_scope_runtime`; -- verifiche: - - `cargo fmt`: passato con i warning rustfmt gia' noti sull'opzione stabile - `ignore`; - - `cargo check --tests`: passato; - - `cargo test --lib test_eval_scope_runtime_features_keep_bridge_libraries_until_core_scope_runtime -- --nocapture`: unit test passato prima di interrompere il resto del traversal workspace; - - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 58/58. - -Aggiornamento Milestone 6/7.72 - 2026-07-06: - -- implementato un provider core runtime per gli helper `eval_scope` usati dal - percorso AOT scope-only: - - `__elephc_eval_scope_new`; - - `__elephc_eval_scope_free`; - - `__elephc_eval_scope_get`; - - `__elephc_eval_scope_set`; - - `__elephc_eval_value_null`, necessario per reload Mixed mancanti; -- `eval_scope && !eval_bridge` ora emette questi helper dal core runtime e non - richiede piu' `pcre2-*` ne' `elephc_magician`; -- `eval_bridge` continua a usare il provider completo in `elephc_magician`, in - modo da evitare doppie definizioni di simboli e mantenere nel bridge dinamico - gli helper che dipendono da parser/interprete eval; -- aggiunta una barriera di lowering scope-only per literal eval AOT che hanno - bisogno di `EvalScopeGet`/`EvalScopeSet` ma non del contesto dinamico: viene - dichiarato solo l'hidden local `EvalScope`, evitando riferimenti a - `__elephc_eval_context_free`; -- la finalizzazione dell'assembly EIR ora emette helper metadata/reflection, - callable e dynamic eval solo quando `eval_bridge` e' davvero richiesto; -- aggiornate le regressioni scope-only per verificare assenza di - `elephc_magician` e presenza degli helper core `__elephc_eval_scope_*`; -- limite residuo: il provider core copre solo `new/free/get/set` e il valore - `null` minimo. Alias globali, `unset`, clear-dirty e semantiche piu' - dinamiche restano bridge-only finche' non vengono portate nel subset AOT; -- verifiche: - - `cargo test --lib test_eval_scope_runtime_features_omit_bridge_libraries -- --nocapture`: unit test passato prima di interrompere il resto del traversal workspace; - - `cargo test --test codegen_tests test_literal_eval_static_array_scope_write_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_legacy_array_literal_next_key_scope_assignment_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 58/58. - -Aggiornamento Milestone 4/7.73 - 2026-07-06: - -- il classifier AOT EIR ora distingue una prima categoria di builtin statici - runtime-safe, separata dai fold compile-time su soli literal; -- abilitato `strlen()` dentro literal eval EIR AOT quando l'argomento e' - posizionale e arriva gia' al backend come `Str` o boxed `Mixed`, per esempio - una variabile del caller passata tramite direct read params; -- questo copre casi come: - - ```php - $s = "abcd"; - echo eval('return strlen($s);'); - ``` - - senza passare da `__elephc_eval_execute` e senza linkare `elephc_magician`; -- il gate resta conservativo: - - niente named/spread args in questo nuovo path runtime-safe; - - nessuna estensione implicita agli altri builtin; - - argomenti non string/Mixed restano fuori dal subset finche' non sono - modellati con la stessa semantica del type checker/backend ordinario; -- test aggiunto: - `test_literal_eval_strlen_scope_read_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_strlen_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 59/59; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.74 - 2026-07-06: - -- estesa la whitelist dei builtin statici runtime-safe con `count()` su array - concreti gia' modellabili dal subset EIR AOT; -- il nuovo caso coperto e' `count($items)` quando `$items` e' una local del - frammento assegnata da static array literal, quindi il backend ordinario puo' - leggere la lunghezza dell'array senza bridge; -- poiche' le variabili create dentro `eval` restano visibili nel caller, il - path corretto e' scope-only EIR AOT: usa `__elephc_eval_scope_set` core per - pubblicare `$items`/`$map`, ma non crea `EvalContext`, non chiama - `__elephc_eval_execute` e non linka `elephc_magician`; -- il gate resta conservativo: - - niente `count($callerVar)` su direct read param `Mixed`, finche' non viene - modellata la diagnostica per non-array; - - niente named/spread args in questo runtime-safe path; - - array provenienti da sorgenti dinamiche restano fuori dal subset; -- test aggiunto: - `test_literal_eval_local_array_count_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_local_array_count_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 60/60; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.75 - 2026-07-06: - -- estesa la whitelist dei builtin statici runtime-safe con `intval()` quando - l'argomento puo' raggiungere il backend ordinario come scalar, stringa, - null-like o boxed `Mixed`; -- il caso principale coperto e' una variabile del caller passata al frammento - eval tramite direct read params: - - ```php - $s = "42"; - echo eval('return intval($s) + 8;'); - ``` - -- questo path usa una funzione EIR AOT interna con parametri Mixed diretti, - quindi non crea scope runtime, non chiama `__elephc_eval_execute` e non - linka `elephc_magician`; -- il gate resta conservativo: - - niente named/spread args; - - array locali/statici non sono accettati come argomento `intval()`; - - altri builtin di cast restano esclusi finche' non vengono verificati - contro il lowerer EIR ordinario; -- test aggiunto: - `test_literal_eval_intval_scope_read_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_intval_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 61/61; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.76 - 2026-07-06: - -- aggiunto supporto EIR ordinario per `floatval()` su `Mixed`/`Union`, usando - il runtime helper gia' esistente `__rt_mixed_cast_float`; -- estesa la whitelist dei builtin statici runtime-safe con `floatval()` sugli - stessi argomenti scalar-safe gia' ammessi per `intval()`; -- il caso eval coperto e': - - ```php - $s = "1.5"; - echo eval('return floatval($s) + 2.25;'); - ``` - - che ora usa direct read params verso una funzione EIR AOT interna, senza - scope runtime, senza `__elephc_eval_execute` e senza `elephc_magician`; -- aggiunta anche una regressione non-eval per provare il backend ordinario: - `floatval(json_decode("2.5")) + 0.5`; -- il gate resta conservativo: - - niente named/spread args; - - array locali/statici restano esclusi dagli argomenti `floatval()`; - - `boolval()`/`strval()` non sono stati allargati in questa tranche; -- test aggiunti: - - `test_json_decode_float_value_can_be_floatval`; - - `test_literal_eval_floatval_scope_read_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_json_decode_float_value_can_be_floatval -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_floatval_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests json_decode_float -- --nocapture`: 3/3; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 62/62; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.77 - 2026-07-06: - -- aggiunto supporto EIR ordinario per `boolval()` su `Mixed`/`Union`, usando - il runtime helper gia' esistente `__rt_mixed_cast_bool`; -- estesa la whitelist dei builtin statici runtime-safe con `boolval()` sugli - stessi argomenti scalar-safe gia' ammessi per `intval()`/`floatval()`; -- il caso eval coperto e': - - ```php - $s = "0"; - echo eval('return boolval($s) ? "bad" : "ok";'); - ``` - - che ora usa direct read params verso una funzione EIR AOT interna, senza - scope runtime, senza `__elephc_eval_execute` e senza `elephc_magician`; -- aggiunta anche una regressione non-eval per provare il backend ordinario: - `boolval(json_decode("true"))`, `boolval(json_decode("false"))` e la - truthiness speciale PHP della stringa `"0"`; -- il gate resta conservativo: - - niente named/spread args; - - array locali/statici restano esclusi dagli argomenti `boolval()`; - - `strval()` non e' stato allargato in questa tranche; -- test aggiunti: - - `test_json_decode_bool_value_can_be_boolval`; - - `test_literal_eval_boolval_scope_read_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_json_decode_bool_value_can_be_boolval -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_boolval_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests json_decode_bool -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 63/63; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.78 - 2026-07-06: - -- verificato che `strval()` passa gia' dal cast a stringa EIR ordinario, che - supporta `Mixed`/`Union` tramite `__rt_mixed_cast_string` e la gestione - object-aware di `__toString()`; -- estesa la whitelist dei builtin statici runtime-safe con `strval()` sugli - stessi argomenti scalar-safe gia' ammessi per `intval()`/`floatval()`/ - `boolval()`; -- il caso eval coperto e': - - ```php - $s = false; - echo eval('return "[" . strval($s) . "]";'); - ``` - - che ora usa direct read params verso una funzione EIR AOT interna, senza - scope runtime, senza `__elephc_eval_execute` e senza `elephc_magician`; -- aggiunta anche una regressione non-eval per provare `strval()` su payload - `Mixed` da `json_decode()`: - - numero intero; - - `true`; - - `false` come stringa vuota; - - stringa JSON; -- il gate resta conservativo: - - niente named/spread args; - - array locali/statici restano esclusi dagli argomenti `strval()`; - - object/string-context dinamici restano affidati al normale lowerer EIR e - non vengono allargati nel classifier oltre i casi scalar-safe; -- test aggiunti: - - `test_json_decode_scalar_value_can_be_strval`; - - `test_literal_eval_strval_scope_read_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_json_decode_scalar_value_can_be_strval -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_strval_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests json_decode_str -- --nocapture`: 4/4; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 64/64; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.79 - 2026-07-06: - -- estesa la whitelist dei builtin statici runtime-safe con type probes scalari: - - `gettype()`; - - `is_int()` / `is_integer()` / `is_long()`; - - `is_float()` / `is_double()` / `is_real()`; - - `is_bool()`; - - `is_null()`; - - `is_scalar()`; - - `is_string()`; -- il caso eval coperto e': - - ```php - $i = 42; - $f = 1.5; - $b = false; - $n = null; - $s = "hi"; - echo eval('return gettype($i) . ":" . - (is_integer($i) ? "I" : "bad") . - (is_double($f) ? "D" : "bad") . - (is_bool($b) ? "B" : "bad") . - (is_null($n) ? "N" : "bad") . - (is_scalar($s) ? "S" : "bad") . - (is_string($s) ? "T" : "bad");'); - ``` - - che ora usa direct read params verso una funzione EIR AOT interna, senza - scope runtime, senza `__elephc_eval_execute` e senza `elephc_magician`; -- corretto un bug nel fold custom dei builtin statici di `eval_aot`: `is_*()` - su argomenti non literal veniva foldato a `false`; ora i non-literal - ritornano `None` e arrivano al lowerer EIR runtime-safe; -- aggiunta anche una regressione non-eval per provare `gettype()` e gli - `is_*()` su payload `Mixed` da `json_decode()`; -- il gate resta conservativo: - - niente named/spread args; - - argomenti limitati agli stessi casi scalar-safe usati dai cast runtime-safe; - - `is_array()`/`is_object()`/`is_iterable()` restano fuori da questa tranche; -- test aggiunti: - - `test_json_decode_scalar_type_predicates_accept_mixed`; - - `test_literal_eval_type_probes_scope_read_use_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_json_decode_scalar_type_predicates_accept_mixed -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_type_probes_scope_read_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 65/65; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.80 - 2026-07-06: - -- estesa la whitelist dei builtin statici runtime-safe con `is_array()` solo - per sorgenti array gia' materializzabili dal percorso EIR AOT: - - literal array statici; - - variabili locali del frammento assegnate da literal array; -- il caso eval coperto e': - - ```php - echo eval('$a = [1, 2]; return is_array($a) ? "A" : "bad";'); - ``` - - che ora passa dalla funzione EIR AOT interna e non chiama - `__elephc_eval_execute`; -- a differenza dei type probe scalari su read-only caller scope, questo caso - usa ancora `__elephc_eval_scope_set`: l'assegnazione `$a = [...]` dentro - `eval` deve creare/aggiornare `$a` nello scope del chiamante secondo - semantica PHP; -- aggiunta una regressione non-eval per provare `is_array()` su payload - `Mixed` da `json_decode()`; -- il gate resta conservativo: - - niente named/spread args; - - niente `is_array($callerVar)` direct-read finche' non esiste un path sicuro - per distinguere array/scalari dal valore scope letto; - - niente `is_object()`/`is_iterable()` in questa tranche; -- test aggiunti: - - `test_json_decode_array_value_can_be_is_array`; - - `test_literal_eval_is_array_local_array_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_json_decode_array_value_can_be_is_array -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_is_array_local_array_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 66/66; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.81 - 2026-07-06: - -- estesa la whitelist dei builtin statici runtime-safe con `is_iterable()` - solo per la parte array-safe gia' coperta da `is_array()`: - - literal array statici; - - variabili locali del frammento assegnate da literal array; -- il caso eval coperto e': - - ```php - echo eval('$a = [1, 2]; return is_iterable($a) ? "T" : "bad";'); - ``` - - che passa dalla funzione EIR AOT interna, non chiama - `__elephc_eval_execute` e non linka `elephc_magician`; -- resta intenzionalmente escluso `is_iterable($callerVar)` direct-read quando - il valore puo' essere oggetto/Iterator, perche' quel caso richiede ancora una - prova scope/object-safe separata; -- resta fuori anche `is_object()` in eval AOT runtime-safe; -- aggiunta una regressione non-eval per provare `is_iterable()` su payload - `Mixed` array da `json_decode()`; -- test aggiunti: - - `test_json_decode_array_value_can_be_is_iterable`; - - `test_literal_eval_is_iterable_local_array_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_json_decode_array_value_can_be_is_iterable -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_is_iterable_local_array_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 67/67; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.82 - 2026-07-06: - -- estesa la whitelist dei builtin statici runtime-safe con `is_object()` per: - - valori scalar/literal/array gia' gestiti dal subset EIR, con risultato - staticamente falso quando non sono oggetti; - - caller-scope reads passati come direct read params `Mixed`, cosi' il - lowerer ordinario puo' controllare il tag object a runtime senza - `eval_scope`; -- il caso eval coperto e': - - ```php - $o = json_decode("{}"); - $i = 42; - echo eval('return (is_object($o) ? "O" : "bad") . ":" . - (is_object($i) ? "bad" : "I");'); - ``` - - che passa dalla funzione EIR AOT interna con direct read params, senza - `__elephc_eval_execute`, senza `__elephc_eval_scope_*` e senza - `elephc_magician`; -- resta esclusa la creazione/lettura di proprieta' oggetto dentro eval AOT, - perche' quello e' un tema object/member semantics separato; -- aggiunta una regressione non-eval per provare `is_object()` su payload - `Mixed` object da `json_decode()`; -- test aggiunti: - - `test_json_decode_object_value_can_be_is_object`; - - `test_literal_eval_is_object_scope_read_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_json_decode_object_value_can_be_is_object -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_is_object_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 68/68; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.83 - 2026-07-06: - -- estesa la whitelist dei builtin statici runtime-safe con: - - `is_numeric()` su valori scalar-safe e caller-scope reads direct-param; - - `is_resource()` su valori scalar-safe e caller-scope reads direct-param; -- il caso eval coperto e': - - ```php - $n = "42"; - $s = "abc"; - $h = fopen("php://memory", "r+"); - echo eval('return (is_numeric($n) ? "N" : "bad") . - (is_numeric($s) ? "bad" : "S") . ":" . - (is_resource($h) ? "H" : "bad");'); - ``` - - che passa dalla funzione EIR AOT interna con direct read params, senza - `__elephc_eval_execute`, senza `__elephc_eval_scope_*` e senza - `elephc_magician`; -- il gate resta conservativo: - - array locali/statici non vengono aperti per `is_numeric()` anche se PHP - restituirebbe staticamente `false`; - - `is_nan()`/`is_finite()`/`is_infinite()` restano fuori da questa tranche - perche' richiedono coercione numerica/float, non solo type-probe; -- aggiunta una regressione non-eval per provare `is_numeric()` su payload - `Mixed` scalari da `json_decode()`; -- test aggiunti: - - `test_json_decode_scalar_value_can_be_is_numeric`; - - `test_literal_eval_numeric_resource_probes_scope_read_use_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_json_decode_scalar_value_can_be_is_numeric -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_numeric_resource_probes_scope_read_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 69/69; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.84 - 2026-07-06: - -- estesa la whitelist dei builtin statici runtime-safe con: - - `is_nan()`; - - `is_finite()`; - - `is_infinite()`; -- il supporto e' volutamente limitato ad argomenti numerici provati dentro il - frammento AOT: - - literal `int`/`float`/`bool`, inclusi `NAN`, `INF` e `-INF`; - - variabili locali del frammento assegnate da valori `int`/`float`; - - cast espliciti `(int)`, `(float)`, `(bool)` gia' abbassabili dal subset EIR; -- il caso eval coperto e': - - ```php - echo eval('$nan = NAN; $inf = INF; $num = 2.5; - return (is_nan($nan) ? "N" : "bad") . - (is_infinite($inf) ? "I" : "bad") . - (is_finite($num) ? "F" : "bad") . - (is_finite($inf) ? "bad" : "f");'); - ``` - - che passa dalla funzione EIR AOT interna, non chiama - `__elephc_eval_execute` e non linka `elephc_magician`; -- aggiunto un guardrail esplicito per non usare direct read params su stringhe - del caller: - - ```php - $s = "abc"; - echo eval('return is_finite($s) ? "bad" : "ok";'); - ``` - - resta bridge fallback, perche' PHP 8.4 lancia `TypeError` per stringhe non - numeriche in queste funzioni mentre il lowerer `Mixed` passerebbe da cast a - float; -- test aggiunti: - - `test_literal_eval_float_predicates_local_values_use_eir_aot_without_magician`; - - `test_literal_eval_float_predicates_scope_string_use_bridge_fallback`; -- verifiche: - - `php -r` su PHP 8.4 per confermare il comportamento TypeError delle - stringhe non numeriche con `is_nan()`/`is_finite()`/`is_infinite()`; - - `cargo test --test codegen_tests test_literal_eval_float_predicates_local_values_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_float_predicates_scope_string_use_bridge_fallback -- --nocapture`: passato; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 71/71; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.85 - 2026-07-06: - -- esteso il path EIR AOT direct-read per `is_array()` quando l'argomento e' - una variabile dello scope chiamante: - - il classifier `eval_aot` ora considera sicuro il probe `is_array($x)` anche - quando `$x` arriva come read-param `Mixed`; - - il lowering IR accetta array/hash tra i tipi che possono essere passati come - parametri diretti al frammento eval AOT; - - il codegen eval allinea la whitelist dei locals sincronizzabili, evitando il - disallineamento in cui il plan rimuoveva il barrier ma la funzione AOT non - trovava la sorgente `$items`; -- caso coperto: - - ```php - $items = [1, 2]; - $n = 42; - echo eval('return (is_array($items) ? "A" : "bad") . ":" . - (is_array($n) ? "bad" : "N");'); - ``` - - passa tramite funzione EIR AOT con direct read params, non chiama - `__elephc_eval_execute`, non alloca `__elephc_eval_context_new` e non linka - `elephc_magician`; -- `is_iterable()` su variabili caller-scope e' stato lasciato fuori da questa - tranche durante il primo giro; il passo successivo 4/7.86 completa quel caso - dopo aver allineato le whitelist array/object tra classifier, lowering e - codegen; -- test aggiunto: - - `test_literal_eval_is_array_scope_read_uses_eir_aot_without_magician`; -- regressione verificata: - - `test_literal_eval_is_iterable_local_array_uses_eir_aot_without_magician` - resta verde, quindi il supporto eval-local array per `is_iterable()` non e' - stato ridotto; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_is_array_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_is_iterable_local_array_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 72/72; - - `cargo check --tests`: passato; - - `git diff --check`: passato; - - scansione whitespace/conflitti sui file toccati: pulita. - -Aggiornamento Milestone 4/7.86 - 2026-07-06: - -- esteso anche `is_iterable()` sul path EIR AOT direct-read per valori dello - scope chiamante: - - array/list e hash arrivano come `Mixed` e passano dai tag runtime 4/5; - - oggetti arrivano come `Mixed` con tag runtime 6 e vengono verificati tramite - i metadati `Iterator`/`IteratorAggregate` gia' usati dal lowerer EIR; - - scalari caller-scope restano falsi senza fallback; -- il supporto ha richiesto di tenere coerenti tre predicati separati: - - `eval_aot` per decidere se generare la funzione scope-read AOT; - - `ir_lower::expr` per evitare il barrier eval completo quando i read params - sono davvero passabili; - - `codegen_ir::lower_inst::builtins::eval` per ritrovare le sorgenti locali - array/object al momento della chiamata direct-param; -- caso coperto: - - ```php - class EvalAotDirectIterator implements Iterator { /* metodi Iterator */ } - $items = [1, 2]; - $iterator = new EvalAotDirectIterator(); - $n = 42; - echo eval('return (is_iterable($items) ? "A" : "bad") . - (is_iterable($iterator) ? "I" : "bad") . - (is_iterable($n) ? "bad" : "N");'); - ``` - - produce `AIN`, usa la funzione EIR AOT con direct read params, non chiama - `__elephc_eval_execute`, non alloca `__elephc_eval_context_new` e non linka - `elephc_magician`; -- test aggiunto: - - `test_literal_eval_is_iterable_scope_read_uses_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_is_iterable_scope_read_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 73/73; - - `cargo check --tests`: passato; - - `git diff --check`: passato; - - scansione whitespace/conflitti sui file toccati: pulita. - -Aggiornamento Milestone 4/7.87 - 2026-07-06: - -- consolidata la copertura dei type-probe read-only su valori dello scope - chiamante non scalari: - - `gettype($items)` con `$items` array caller-scope restituisce `array`; - - `gettype($o)` con `$o` object/Mixed caller-scope restituisce `object`; - - `is_scalar($items)` e `is_scalar($o)` restano falsi senza fallback; -- non e' stato necessario nuovo lowering: il lavoro 4/7.85 e 4/7.86 aveva gia' - allineato boxing direct-param, type whitelist e source lookup per array/object; - questa tranche rende esplicita la regressione nel test esistente; -- test esteso: - - `test_literal_eval_type_probes_scope_read_use_eir_aot_without_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_type_probes_scope_read_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 73/73; - - `cargo check --tests`: passato; - - `git diff --check`: passato; - - scansione whitespace/conflitti sui file toccati: pulita. - -Aggiornamento verifica target-aware - 2026-07-06: - -- aggiunta evidenza Linux x86_64 Docker per i nuovi casi direct-read su scope - caller non scalare: - - `./scripts/test-linux-x86_64.sh literal_eval_is_array_scope`: passato; - - `./scripts/test-linux-x86_64.sh literal_eval_is_iterable_scope`: passato; - - `./scripts/test-linux-x86_64.sh literal_eval_type_probes_scope`: passato; -- i runner Docker x86_64 sono usciti senza container residui; -- la verifica locale Linux ARM64 per i casi direct-read resta demandata alla - matrice CI o a filtri Docker dedicati prima di cambiare quel sottoinsieme. - -Aggiornamento verifica target-aware prime-loop - 2026-07-06: - -- il test prime-sum literal eval (`100000`, output `454396537`, no bridge, no - `elephc_magician`) e' passato sul target locale macOS ARM64: - - `cargo test --test codegen_tests codegen::eval::test_literal_eval_prime_loop_uses_aot_without_execute_bridge -- --exact --nocapture`; -- lo stesso filtro e' passato su Linux x86_64 via Docker: - - `./scripts/test-linux-x86_64.sh test_literal_eval_prime_loop_uses_aot_without_execute_bridge`; -- lo stesso filtro e' passato su Linux ARM64 via Docker: - - `./scripts/test-linux-arm64.sh test_literal_eval_prime_loop_uses_aot_without_execute_bridge`; -- i run Docker hanno emesso solo i warning preesistenti su `libc::time_t` in - `elephc-magician`. - -Aggiornamento benchmark prime-loop - 2026-07-06: - -- ricostruito `target/release/elephc` con `cargo build --release`; -- eseguito benchmark manuale fuori repo, senza aggiungere il caso alla suite - permanente, sullo stesso workload prime-sum fino a `100000`; -- output verificato identico in tutti i casi: `454396537`; -- mediane su 12 run: - - Elephc standard: `13.35 ms`; - - Elephc via eval literal AOT: `9.74 ms`; - - PHP standard: `97.77 ms`; - - PHP via eval: `117.76 ms`; -- RSS sample `Elephc via eval`: `1572864` byte di maximum resident set size. - -Aggiornamento Milestone 4/7.88 - 2026-07-06: - -- esteso il path EIR AOT direct-read per `count($callerArray)` senza aprire - `count($callerScalar)`: - - il plan `eval_aot` registra ora un set di read scope che devono essere - array-like quando il frammento contiene `count($nome)` su una variabile - proveniente dal caller; - - il classifier accetta `count($scopeRead)` solo come builtin `count()`, senza - promuovere genericamente `$scopeRead` a sorgente array per altri usi come - `$scopeRead[0]`; - - il lowering AST->EIR, la feature discovery e il backend EIR controllano il - vincolo contro i tipi locali del caller prima di scegliere il direct-param - AOT; - - se il caller ha uno scalare, il call-site resta sul bridge e continua a - linkare `elephc_magician`; -- test aggiunti: - - `test_literal_eval_count_scope_read_array_uses_eir_aot_without_magician`; - - `test_literal_eval_count_scope_read_scalar_keeps_bridge_fallback`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_count_scope_read_array_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_count_scope_read_scalar_keeps_bridge_fallback -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_count -- --nocapture`: 2/2; - - `cargo test --test codegen_tests array_count_uses_eir_aot_without_magician -- --nocapture`: 2/2; - - `cargo check --tests`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 75/75; - - `git diff --check`: passato; - - scansione whitespace/conflitti sui file toccati: pulita. - -Aggiornamento Milestone 4/7.89 - 2026-07-06: - -- abilitato `foreach ($callerArray as ...)` nel path EIR AOT scope-aware quando - la sorgente letta dallo scope caller soddisfa il vincolo array-like gia' - usato per `count($callerArray)`; -- il classifier registra il read array-constrained per la sorgente del - `foreach`, ma non marca key/value come definitely-assigned dopo il loop se la - sorgente non e' una static-array literal non vuota; -- il lowering/codegen controllano il vincolo array anche per il path con runtime - eval scope, cosi' `foreach ($callerScalar as ...)` resta fallback bridge; -- corretto il preambolo EIR di `foreach`: l'inizializzazione tecnica dei loop - locals a boxed `null` ora aggiorna solo lo slot locale e non pubblica nello - scope eval, altrimenti `foreach ([] as $kept)` sovrascriveva il caller con - `null`; -- il call site scope-aware pre-flusha anche i nomi scritti gia' presenti nel - caller, oltre ai nomi letti, in modo che una scrittura condizionale non - eseguita lasci invariato il valore dopo il reload; -- test aggiunti: - - `test_literal_eval_foreach_scope_array_uses_eir_aot_scope_helpers`, che - verifica output corretto, `__elephc_eval_scope_get/set`, assenza di - `__elephc_eval_execute`, assenza di `elephc_magician`, caso array vuoto e - key/value su array associativo del caller; - - `test_literal_eval_foreach_scope_scalar_keeps_bridge_fallback`, che mantiene - il bridge su sorgente scalare; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_foreach_scope_array_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests test_literal_eval_foreach_scope_scalar_keeps_bridge_fallback -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_foreach -- --nocapture`: 2/2; - - `cargo test --test codegen_tests eval_foreach -- --nocapture`: 9/9; - - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 77/77; - - `cargo check --tests`: passato; - - `./scripts/test-linux-x86_64.sh literal_eval_foreach`: 2/2 sui test - `codegen_tests`, ripassato dopo l'estensione key/value associativa; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.90 - 2026-07-06: - -- esteso il path EIR AOT direct-param per i predicati IEEE float - `is_nan()`, `is_infinite()` e `is_finite()` quando l'argomento letto dallo - scope caller e' un local inizializzato di tipo `int` o `float`; -- il planner registra ora `float_predicate_read_constraints`, separato dai - vincoli array usati da `count()`/`foreach`, cosi' il classifier puo' accettare - `$scopeRead` ma lowering, feature scan e backend verificano il tipo concreto - del caller prima di scegliere AOT; -- il vincolo resta conservativo: stringhe del caller, ref-bound locals, - superglobal/global alias e locals non inizializzati restano fallback bridge, - preservando i `TypeError` PHP-observable; -- test aggiunto: - - `test_literal_eval_float_predicates_scope_numeric_use_eir_aot_without_magician`, - che copre `NAN`, `INF`, float finite e int dal caller via direct read params - senza `__elephc_eval_execute`, senza eval-scope helper e senza - `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests literal_eval_float_predicates -- --nocapture`: 3/3, ripassato dopo `rustfmt`; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 78/78; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs src/ir_lower/expr/mod.rs src/ir_lower/program.rs src/codegen_ir/lower_inst/builtins/eval.rs tests/codegen/eval.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.91 - 2026-07-06: - -- abilitato `foreach ([])` / `foreach ([] as ...)` nel path EIR AOT no-scope; -- il planner ora distingue le sorgenti foreach staticamente vuote: - - la sorgente array viene ancora analizzata; - - il body resta richiesto nel subset EIR perche' viene comunque abbassato dal - backend; - - reads/writes del body non vengono registrati nello scope eval, perche' il - body e' runtime-irraggiungibile; - - key/value non vengono registrati come scritture e quindi non vengono flushati - nel caller; -- questo rimuove il vecchio fallback per array statici vuoti senza cambiare il - comportamento dei foreach statici non vuoti, che continuano a sincronizzare - key/value tramite eval-scope helper; -- test aggiunto: - - `test_literal_eval_static_empty_foreach_uses_eir_aot_without_scope_helpers`, - che verifica EIR AOT, assenza di `__elephc_eval_*`, assenza di - `elephc_magician` e preservazione della variabile caller `$kept`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_static_empty_foreach_uses_eir_aot_without_scope_helpers -- --nocapture`: passato, ripassato dopo `rustfmt`; - - `cargo test --test codegen_tests test_literal_eval_static_foreach_uses_eir_aot_scope_helpers -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_foreach -- --nocapture`: 2/2; - - `cargo test --test codegen_tests literal_eval_ -- --nocapture`: 79/79; - - `cargo check --tests`: passato. - -Aggiornamento Milestone 4/7.92 - 2026-07-06: - -- esteso il subset EIR AOT per `array_key_exists()` su array letti dallo scope - caller: - - `array_key_exists(1, $callerIndexed)` puo' usare direct read params quando il - caller ha un local array inizializzato; - - `array_key_exists("name", $callerAssoc)` puo' usare direct read params quando - il caller ha un local associative-array inizializzato; -- il planner registra un vincolo separato `assoc_array_read_constraints`, oltre - al vincolo array-like gia' usato da `count()`/`foreach`, cosi' string-key probes - su indexed array restano bridge fallback invece di entrare in un path AOT con - semantica numeric-string incompleta; -- aggiunto lowering target-aware di `array_key_exists()` su receiver `Mixed`: - - unbox del receiver; - - tag `4` -> `__rt_array_key_exists` per chiavi `int`/`bool`; - - tag `5` -> `__rt_hash_get` per chiavi `int`/`bool`/`string`; - - altri tag -> `false`; - - il payload array/hash viene preservato su stack temporaneo mentre la chiave - viene materializzata, senza aggiungere un nuovo helper runtime globale; -- il gate resta conservativo: - - chiavi dinamiche (`array_key_exists($k, $items)`) restano fuori da questa - tranche; - - `null`/`float` keys sui caller-scope arrays restano fuori finche' il backend - runtime non modella tutta la normalizzazione PHP; - - caller scalari e string-key probes su indexed arrays restano fallback bridge; -- test aggiunti: - - `test_literal_eval_array_key_exists_scope_read_array_uses_eir_aot_without_magician`, - che copre indexed array, associative array, chiave presente con valore `null`, - miss, direct-read EIR AOT, assenza di `__elephc_eval_*` e assenza di - `elephc_magician`; - - `test_literal_eval_array_key_exists_scope_read_scalar_keeps_bridge_fallback`; - - `test_literal_eval_array_key_exists_string_key_indexed_scope_keeps_bridge_fallback`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_array_key_exists_scope_read_array_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_array_key_exists_ -- --nocapture`: 3/3; - - `cargo test --test codegen_tests test_literal_eval_static_array_key_exists_uses_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 7/7; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs src/ir_lower/expr/mod.rs src/ir_lower/program.rs src/codegen_ir/lower_inst/builtins/eval.rs src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs tests/codegen/eval.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.93 - 2026-07-06: - -- chiuso un pezzo del gap lasciato in 4/7.92: `array_key_exists()` su array letti - dallo scope caller ora puo' restare in EIR AOT anche con: - - chiave statica `null`, limitata a receiver associativi per modellare la - normalizzazione PHP a chiave stringa vuota; - - chiavi statiche `float` integralmente rappresentabili, incluse forme negate - come `-2.0`, abbassate a integer key prima del probe indexed/hash; -- il lowering target-aware e' stato esteso su AArch64 e x86_64: - - indexed arrays convertono le chiavi float con `fcvtzs`/`cvttsd2si` prima di - chiamare `__rt_array_key_exists`; - - associative arrays materializzano `null` come stringa vuota e float integrali - come integer key prima di chiamare `__rt_hash_get`; - - receiver `Mixed` accetta ora chiavi `int`/`bool`/`float` su indexed/hash e - forza `string`/`null` solo sul path hash-only; -- il gate resta conservativo sui float frazionari (`2.7`, ecc.): restano bridge - fallback per preservare la deprecation PHP sulla conversione float->int con - perdita di precisione; -- test aggiunti: - - `test_literal_eval_array_key_exists_scope_read_null_and_float_keys_use_eir_aot`, - che copre `1.0` su indexed array, `null` su assoc array e `-2.0` su assoc - array via direct-read EIR AOT, senza `__elephc_eval_*` e senza - `elephc_magician`; - - `test_literal_eval_array_key_exists_fractional_float_key_keeps_bridge_fallback`, - che mantiene `array_key_exists(2.7, $items)` sul bridge; -- verifiche: - - `cargo test --test codegen_tests literal_eval_array_key_exists_ -- --nocapture`: 5/5; - - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 9/9; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs tests/codegen/eval.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.94 - 2026-07-06: - -- il gate EIR AOT per builtin runtime ora normalizza named arguments tramite - `builtin_call_sig()` + `plan_call_args()` prima di applicare i controlli gia' - esistenti sugli argomenti in ordine firma; -- la normalizzazione e' condivisa anche dal collector dei vincoli array, cosi' - `array_key_exists(array: $map, key: "name")` registra il vincolo associativo - sul parametro `array` corretto e non sulla posizione sorgente; -- questa tranche resta volutamente fixed-arity/no-spread: - - spread/unpack continuano a restare fallback; - - builtin con default opzionali restano da abilitare uno alla volta quando il - backend EIR accetta la forma materializzata; -- test aggiunto: - - `test_literal_eval_named_runtime_builtins_use_eir_aot_without_magician`, - che copre `boolval(value: $flag)`, - `array_key_exists(array: $map, key: "name")` e - `array_key_exists(key: "missing", array: $map)` via direct-read EIR AOT, - senza `__elephc_eval_*` e senza `elephc_magician`; -- verifiche: - - `cargo test --test codegen_tests test_literal_eval_named_runtime_builtins_use_eir_aot_without_magician -- --nocapture`: passato; - - `cargo test --test codegen_tests literal_eval_array_key_exists_ -- --nocapture`: 5/5; - - `cargo test --test codegen_tests literal_eval_named_runtime_builtins -- --nocapture`: 1/1; - - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 9/9; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs tests/codegen/eval.rs`: passato con i - warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.95 - 2026-07-06: - -- chiuso il caso opzionale piu' piccolo lasciato aperto in 4/7.94: - `count(value: $items)` e `count(value: $items, mode: 0)` possono ora restare - in EIR AOT quando `$items` e' un array/hash noto dal caller; -- il classifier accetta `count()` normalizzato con uno o due argomenti solo se - il `mode` opzionale e' il default literal `0`; il collector dei vincoli array - registra comunque il vincolo sul parametro `value`; -- il lowerer EIR `count` accetta ora uno o due operandi e ignora il secondo - solo dopo aver verificato che sia `ConstI64(0)`; -- il `mode` ricorsivo/non-zero resta fallback bridge finche' l'EIR non modella - `COUNT_RECURSIVE` per array annidati, oggetti `Countable` e `Mixed`; -- test aggiunti/aggiornati: - - `test_literal_eval_count_named_default_mode_uses_eir_aot_without_magician`, - che copre `count(value: $items)` e `count(value: $items, mode: 0)` via - direct-read EIR AOT senza helper eval e senza `elephc_magician`; - - `test_literal_eval_count_named_recursive_mode_keeps_bridge_fallback`, che - mantiene `count(value: $items, mode: 1)` sul bridge; - - `test_literal_eval_named_runtime_builtins_use_eir_aot_without_magician` ora - include anche `count(value: $items)`; -- verifiche: - - `cargo test --test codegen_tests literal_eval_count_named -- --nocapture`: 2/2; - - `cargo test --test codegen_tests literal_eval_count -- --nocapture`: 4/4; - - `cargo test --test codegen_tests literal_eval_named_runtime_builtins -- --nocapture`: 1/1; - - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 9/9; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs src/codegen_ir/lower_inst/builtins.rs tests/codegen/eval.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.96 - 2026-07-06: - -- il gate EIR AOT per builtin runtime non scarta piu' gli spread prima del - planner: ora passa sempre da `plan_call_args()` quando la chiamata contiene - named args o spread; -- gli spread statici espandibili dal planner, per esempio - `count(...["value" => $items])`, - `boolval(...["value" => $flag])` e - `array_key_exists(...["array" => $map, "key" => "name"])`, possono quindi - restare in direct-read EIR AOT senza bridge; -- gli spread dinamici o non espansi dal planner restano fallback bridge perche' - `normalize_eir_runtime_builtin_args()` rifiuta ancora ogni `Spread` residuo - dopo la normalizzazione; -- test aggiunti: - - `test_literal_eval_static_spread_runtime_builtins_use_eir_aot_without_magician`, - che copre `count`, `boolval` e `array_key_exists` con spread statico - associativo via direct-read EIR AOT, senza helper eval e senza - `elephc_magician`; - - `test_literal_eval_dynamic_spread_runtime_builtin_keeps_bridge_fallback`, - che mantiene `count(...$args)` sul bridge; -- verifiche: - - `cargo test --test codegen_tests literal_eval_static_spread_runtime_builtins -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_dynamic_spread_runtime_builtin -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_named_runtime_builtins -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_count_named -- --nocapture`: 2/2; - - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 9/9; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs tests/codegen/eval.rs`: passato con i - warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -Aggiornamento Milestone 5/7.97 - 2026-07-06: - -- il gate per chiamate statiche a funzioni utente dentro literal eval AOT ora - rifiuta esplicitamente gli spread rimasti dinamici dopo `plan_call_args()`, - invece di dipendere solo dal controllo finale sui literal scalar; -- gli spread statici espandibili dal planner, per esempio - `join_static_spread(...["right" => "B", "left" => "A"])`, restano nel subset - EIR AOT e possono materializzare anche default scalar opzionali; -- gli spread dinamici, per esempio `join_dynamic_spread(...$args)`, restano sul - bridge finche' il percorso EIR AOT non modella runtime unpack, evaluation - order e controlli di arita'/named args dinamici; -- test aggiunti: - - `test_literal_eval_static_user_function_static_spread_args_use_aot_without_magician`, - che copre named static spread e default scalar in una user function via EIR - AOT, senza helper eval e senza `elephc_magician`; - - `test_literal_eval_static_user_function_dynamic_spread_args_keep_bridge_fallback`, - che mantiene lo spread dinamico sul bridge; -- verifiche: - - `cargo test --test codegen_tests literal_eval_static_user_function_static_spread_args -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_static_user_function_dynamic_spread_args -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_static_user_function_named_args -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_static_user_function_defaults -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_static_scalar_user_functions -- --nocapture`: 1/1; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs tests/codegen/eval.rs`: passato con i - warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/7.98 - 2026-07-06: - -- chiuso il fallback per `array_key_exists("1", $items)` quando `$items` e' un - indexed array caller-scope: la string key ora puo' restare in direct-read EIR - AOT; -- il backend EIR condiviso di `array_key_exists()` normalizza le string key su - array indicizzati con `__rt_hash_normalize_key()`: - - stringhe intere canoniche, come `"1"`, diventano bounds-check tramite - `__rt_array_key_exists`; - - stringhe non intere o con leading zero non canonico, come `"x"` e `"01"`, - restituiscono `false` sugli indexed array senza provare un hash lookup; -- il dispatch Mixed indexed/assoc ora tratta `Str` come chiave valida anche sul - ramo indexed; `null` resta hash-only per la semantica della chiave `""`; -- test aggiunti/aggiornati: - - `test_literal_eval_array_key_exists_string_key_indexed_scope_uses_eir_aot`, - che copre `"1"`, `"x"` e `"01"` via direct-read EIR AOT senza helper eval e - senza `elephc_magician`; - - `test_array_key_exists_indexed_string_keys`, che copre lo stesso - comportamento nel backend EIR condiviso fuori da `eval`; -- verifiche: - - `cargo test --test codegen_tests literal_eval_array_key_exists_string_key_indexed_scope -- --nocapture`: 1/1; - - `cargo test --test codegen_tests test_array_key_exists_indexed_string_keys -- --nocapture`: 1/1; - - `cargo test --test codegen_tests array_key_exists -- --nocapture`: 10/10; - - `./scripts/test-linux-x86_64.sh array_key_exists`: passato, inclusi 10/10 - codegen filtrati, 1/1 error test filtrato, 2/2 smoke test filtrati e 1/1 - test `elephc-magician` filtrato; warning preesistenti su `libc::time_t`; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs src/codegen_ir/lower_inst/builtins/arrays/key_exists.rs tests/codegen/eval.rs tests/codegen/arrays/indexed/search_merge_union.rs`: passato con i warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -Aggiornamento Milestone 4/5/7.99 - 2026-07-06: - -- il gate EIR AOT di literal eval ora riconosce `call_user_func()` e - `call_user_func_array()` quando il callback e' una stringa literal risolta a - un builtin gia' sicuro per AOT oppure a una user function tipizzata gia' - supportata dal subset statico; -- `call_user_func_array()` con array literal viene convertito nello stesso - formato di argomenti usato dal lowering callable esistente: chiavi stringa - diventano named args, chiavi intere restano positional, e chiavi dinamiche - restano fallback; -- i `call_user_func*()` statici verso builtin puri foldabili vengono foldati - prima del controllo AOT, per esempio `call_user_func("strtoupper", "az")` - diventa direttamente una literal e non dipende dal bridge; -- le user function statiche AOT richiedono ora parametri e return type - dichiarati: le funzioni non tipizzate restano sul bridge, evitando il - mismatch in cui il planner module-level le accettava ma il lowerer del - frammento AOT generava un `EvalFunctionCall` senza eval context locale; -- fallback conservato: - - callback variabile, per esempio `call_user_func($fn, "abcd")`, resta sul - bridge; - - callback static method string o array callable non sono stati aperti in - questa tranche; - - user function non tipizzate continuano a usare il dispatch eval esistente; -- test aggiunti: - - `test_literal_eval_static_call_user_func_builtin_uses_aot_without_magician`, - che copre builtin statici via `call_user_func()` e - `call_user_func_array()` con direct-read EIR AOT, senza helper eval e senza - `elephc_magician`; - - `test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician`, - che copre callback a user function tipizzata con positional, named e - default args via EIR AOT; - - `test_literal_eval_dynamic_call_user_func_callback_keeps_bridge_fallback`, - che mantiene il callback variabile sul bridge; -- verifiche: - - `cargo test --test codegen_tests literal_eval_static_call_user_func -- --nocapture`: 2/2; - - `cargo test --test codegen_tests literal_eval_dynamic_call_user_func_callback -- --nocapture`: 1/1; - - `cargo test --test codegen_tests test_eval_fragment_call_user_func -- --nocapture`: 8/8; - - `cargo test --test codegen_tests literal_eval_static_user_function_uses_aot_without_execute_bridge -- --nocapture`: 1/1; - - `cargo test --test codegen_tests literal_eval_static_scalar_user_functions -- --nocapture`: 1/1; - - `cargo check --tests`: passato; - - `rustfmt --check src/eval_aot.rs tests/codegen/eval.rs`: passato con i - warning gia' noti sull'opzione stabile `ignore`; - - `git diff --check`: passato. - -## Architettura proposta - -### 1. Frammenti AOT come funzioni native interne - -Ogni `eval('...')` supportato deve generare una funzione interna univoca, ad -esempio: - -```text -__elephc_eval_aot__ -``` - -ABI logica: - -```text -eval_aot_fn(eval_context, eval_scope, eval_global_scope) -> boxed Mixed -``` - -Le implementazioni possono materializzare questi argomenti tramite helper ABI -target-aware esistenti. La funzione AOT restituisce sempre un boxed Mixed: - -- valore di `return expr;` se il frammento ritorna; -- boxed `null` se il frammento termina senza `return`; -- status/fatal gestito dagli helper esistenti se un helper runtime fallisce. - -Il call site di `eval()` deve: - -1. riconoscere che il frammento literal ha una funzione AOT; -2. preparare context/scope/global scope come il bridge, ma solo quanto serve; -3. flushare nello scope le variabili del caller lette o scritte dal frammento; -4. chiamare la funzione AOT; -5. ricaricare dal dynamic scope le variabili che il frammento puo' aver scritto; -6. saltare completamente `__elephc_eval_execute`. - -### 2. Analisi del frammento - -Introdurre una fase compile-time dedicata: - -```text -literal fragment string - -> tokenizzazione/parsing come PHP fragment - -> name/magic-constant handling compatibile - -> analisi AOT eligibility - -> raccolta read/write/declare/call effects - -> lowering AOT o fallback reason -``` - -Questa fase deve produrre un oggetto simile a: - -```rust -EvalAotPlan { - function_symbol, - reads: BTreeSet, - writes: BTreeSet, - creates_unknown_vars: bool, - needs_eval_context: bool, - needs_global_scope: bool, - fallback_reason: Option, -} -``` - -Motivo: il call site deve sapere quali locals sincronizzare senza trattare ogni -eval literal come barriera massima quando non serve. - -### 3. Non duplicare tutto il codegen a mano - -Il primo subset scalar AOT oggi e' in `eval.rs` con un lowering manuale. Per -completare davvero AOT, il piano deve spostarsi verso uno dei due approcci: - -Approccio preferito: - -- abbassare il frammento eval a una funzione EIR interna; -- riusare `src/ir_lower/`, `src/ir_passes/` e `src/codegen_ir/`; -- aggiungere primitive EIR esplicite per scope eval solo dove il normale - modello di locals statici non basta. - -Approccio temporaneo ammesso solo per sbloccare il benchmark dei primi: - -- estendere il lowering AOT manuale a un mini-subset int/bool/control-flow; -- trattarlo come ponte di breve durata; -- mantenere il piano di convergenza verso EIR-function AOT. - -Il completamento finale non deve lasciare un grande secondo compiler manuale -in `src/codegen_ir/lower_inst/builtins/eval.rs`. - -## Milestone - -### Milestone 0 - Baseline e guardrail - -Obiettivo: rendere misurabile il problema e impedire regressioni di fallback. - -Deliverable: - -- aggiungere benchmark temporaneo o fixture non-CI per "somma primi fino a - 100000" con quattro varianti: - - Elephc standard; - - Elephc literal eval; - - PHP standard; - - PHP eval; -- aggiungere test assembly che conferma lo stato pre-AOT per il frammento con - `while`/`if`/`break`: oggi deve contenere `__elephc_eval_execute`; -- aggiungere test fallback per un costrutto esplicitamente non supportato, cosi' - il fallback resta verificato quando AOT cresce. - -Comandi: - -- `cargo test --test codegen_tests test_eval_prime_loop_literal_fallback_before_full_aot` -- benchmark manuale con heap sufficiente per il fallback, solo come baseline. - -### Milestone 1 - Funzione AOT interna per frammenti self-contained - -Obiettivo: generare una funzione nativa interna per literal eval che non legge -ne' scrive variabili del caller. - -Subset: - -- scalar locals interni al frammento; -- assignment semplice e compound `+=`; -- `echo`; -- `return`; -- `while`; -- `if`; -- `break`; -- `continue`; -- int/bool values; -- operatori `+`, `-`, `*`, `%`, `<=`, `<`, `>=`, `>`, `==`, `!=`, `&&`, `||` - dove gia' supportati dal parser/typechecker o dove abbassabili senza - divergere da PHP. - -Esempio target: - -```php -eval('$sum = 0; $i = 1; while ($i <= 10000) { $sum += $i; $i += 1; } echo $sum;'); -``` - -Requisiti: - -- assembly deve contenere `eval literal AOT compiled`; -- assembly non deve contenere `__elephc_eval_execute`; -- output deve combaciare con PHP; -- se il frammento termina senza `return`, `eval()` deve restituire `null`. - -### Milestone 2 - Scope read/write efficiente - -Obiettivo: supportare frammenti AOT che leggono e scrivono variabili del caller -senza interpretazione runtime. - -Esempi: - -```php -$a = 10; -echo eval('return $a + 20;'); -``` - -```php -$a = 10; -eval('$a = $a + 20;'); -echo $a; -``` - -Requisiti: - -- analisi read/write del frammento; -- flush solo delle variabili lette/scritte; -- reload solo delle variabili scritte o potenzialmente create; -- `global` e alias globali restano fallback finche' non sono modellati; -- variable variables (`$$name`), `unset`, references e by-ref restano fallback - fino a supporto esplicito. - -### Milestone 3 - Controllo di flusso per benchmark dei primi - -Obiettivo: rendere AOT il benchmark: - -```php -eval('$sum = 0; $n = 2; while ($n <= 100000) { ... if (...) { break; } ... } echo $sum;'); -``` - -Requisiti: - -- loop annidati; -- `break` che esce dal loop interno corretto; -- `if` con branch; -- modulo e confronti interi; -- nessuna chiamata a `__elephc_eval_execute`; -- tempo runtime vicino al codice Elephc standard e sensibilmente sotto PHP. - -Acceptance iniziale: - -- output `454396537`; -- `Elephc via eval` non richiede heap da centinaia di MB; -- RSS indicativo vicino a Elephc standard, non al fallback magician; -- `Elephc via eval` non oltre 2x Elephc standard per questo benchmark come - primo target ragionevole. - -### Milestone 4 - Chiamate statiche a builtins - -Obiettivo: permettere a literal eval AOT di chiamare builtins noti. - -Esempi: - -```php -eval('echo strlen("abc");'); -eval('$x = intval("42"); echo $x + 1;'); -eval('echo abs(-10);'); -``` - -Regole: - -- chiamate con nome statico e risoluzione builtin/funzione gia' nota possono - essere AOT; -- chiamate dinamiche (`$f()`), `call_user_func`, method calls, static calls - late-bound restano fallback inizialmente; -- namespace fallback deve seguire le regole gia' usate dal resolver/typechecker; -- named/spread args devono usare `src/types/call_args/`, non una logica nuova. - -Test: - -- builtin case-insensitive dentro eval literal AOT; -- namespaced call con fallback builtin quando applicabile; -- arg count/type error uguale al path statico o fallback controllato. - -### Milestone 5 - Funzioni statiche gia' note - -Obiettivo: supportare chiamate a funzioni definite staticamente prima del punto -eval. - -Esempio: - -```php -function inc($x) { return $x + 1; } -echo eval('return inc(41);'); -``` - -Fallback iniziale: - -- funzioni dichiarate dentro eval; -- classi dichiarate dentro eval; -- duplicate declarations; -- dynamic function names; -- closure/callable dinamici. - -### Milestone 6 - Riduzione fallback magician - -Obiettivo: evitare di linkare `libelephc-magician` quando tutti gli eval del -programma sono literal e pienamente AOT. - -Requisiti: - -- il program usage scan distingue: - - eval dinamico; - - literal eval fallback; - - literal eval fully AOT; -- programmi con solo eval fully AOT non richiedono `elephc_magician`; -- test assembly/link metadata per assenza di `__elephc_eval_execute`, - `__elephc_eval_context_new` e libreria `elephc_magician` quando non servono. - -### Milestone 7 - Integrazione con pipeline EIR - -Obiettivo: sostituire il mini-AOT manuale con un vero lowering del frammento a -funzione EIR interna. - -Lavoro richiesto: - -- rappresentare eval fragment come `Function` EIR interna con ABI speciale; -- dichiarare locals del frammento separati dai locals del caller; -- introdurre istruzioni o builtins EIR per: - - `eval_scope_get`; - - `eval_scope_set`; - - `eval_return_null`; - - eventuale `eval_status_check`; -- far passare la funzione AOT attraverso validator, optimizer, regalloc e - backend target-aware; -- rimuovere o ridurre il lowering manuale in `eval.rs`. - -Questo milestone e' quello che rende "completo" il percorso AOT in modo -manutenibile. - -## Fallback policy - -Un frammento literal deve restare fallback se contiene: - -- codice non parseabile come fragment PHP; -- include/require; -- declaration di funzioni/classi/interfacce/trait/enum finche' non registrate - staticamente nel contesto eval; -- `global`, `static`, references/by-ref, `unset`, variable variables; -- dynamic calls, dynamic class names, object/method/property access non - supportati; -- eccezioni/throw/try finche' non modellati nel frammento AOT; -- costrutti non supportati dal normale EIR backend; -- qualunque comportamento per cui non esiste test PHP-parity. - -Il fallback deve essere esplicito in assembly con un marker che includa una -ragione leggibile quando possibile. - -## File probabili - -- `src/ir/` -- `src/ir_lower/` -- `src/ir_passes/` -- `src/codegen_ir/lower_inst/builtins/eval.rs` -- `src/codegen_ir/` -- `src/codegen/program_usage/` -- `src/types/call_args/` -- `src/types/checker/` -- `src/name_resolver/` -- `src/resolver/` -- `tests/codegen/eval.rs` -- `tests/codegen/optimizer/` -- `benchmarks/magician/cases/` solo se si decide di promuovere i benchmark di - accettazione nella suite permanente. - -## Test richiesti - -Assembly/codegen tests: - -- literal eval con `while` self-contained usa AOT e non bridge; -- literal eval con `if`/`break` usa AOT e produce output corretto; -- literal eval nested loops prime-sum usa AOT e non bridge; -- literal eval senza `return` ritorna `null`; -- literal eval con `return` ritorna il valore senza uscire dal caller; -- literal eval legge `$a` dal caller; -- literal eval scrive `$a` nel caller; -- literal eval crea `$created` visibile dopo eval; -- unsupported dynamic eval non emette marker AOT; -- unsupported literal eval emette fallback e chiama bridge; -- programma con soli eval fully AOT non linka magician, quando Milestone 6 e' - implementato. - -Target-sensitive checks: - -- focused macOS ARM64 codegen tests; -- focused Linux x86_64 Docker test per prime-loop AOT; -- focused Linux ARM64 Docker test per prime-loop AOT. - -Benchmark checks: - -- prime-sum `100000`: - - output `454396537`; - - no `__elephc_eval_execute`; - - RSS non vicino al fallback da centinaia di MB; - - runtime molto sotto PHP CLI e vicino a Elephc standard. - -## Criteri di completamento - -Il percorso AOT per eval puo' essere considerato completo solo quando: - -1. ogni literal eval supportato viene compilato a funzione nativa interna; -2. il benchmark dei primi fino a `100000` passa via AOT senza bridge; -3. scope read/write del caller funziona per variabili note a compile time; -4. `return`/`null` eval semantics sono PHP-compatible; -5. builtins statici comuni funzionano via AOT; -6. i fallback non supportati restano corretti e verificati; -7. programmi senza fallback eval non linkano magician; -8. la soluzione non embedda il compilatore nel binario finale; -9. i test focused passano sui tre target supportati o hanno CI equivalente; -10. `git diff --check` passa. - -## Rischi principali - -- Duplicare troppo codegen in `eval.rs` e creare un secondo backend difficile da - mantenere. -- Trattare eval come codice statico normale e perdere semantica di scope. -- Dimenticare che `eval('$x + 1;')` ritorna `null`, non l'ultima espressione. -- Saltare i fallback per costrutti dinamici e produrre miscompilazioni. -- Ottimizzare il benchmark dei primi con un percorso troppo speciale invece di - completare il meccanismo generale. -- Supportare solo ARM64 e lasciare x86_64/Linux indietro. - -## Ordine consigliato - -1. Stabilizzare baseline e test fallback. -2. Implementare funzione AOT interna self-contained con locals int/bool e - control-flow. -3. Far passare il benchmark dei primi via AOT. -4. Integrare scope read/write del caller nel nuovo modello a funzione AOT. -5. Aggiungere builtins statici. -6. Portare il lowering AOT verso funzione EIR interna riusando la pipeline. -7. Ridurre il link a magician quando tutti gli eval sono fully AOT. - -## Aggiornamento: metodi statici pubblici - -Tranche completata: - -- il planner AOT per literal eval accetta predicate separati per funzioni e - metodi statici; -- `ir_lower::program`, `ir_lower::expr` e il lowering codegen ricostruiscono lo - stesso piano con metadati di classe coerenti; -- entrano in AOT solo chiamate a metodi statici nominali, `public`, con - signature scalare completamente dichiarata e argomenti normalizzabili dal - planner condiviso; -- metodi statici non tipizzati o non coperti restano sul bridge magician. - -Verifiche locali: - -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_uses_aot_without_magician -- --exact --nocapture` -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_keeps_bridge_fallback -- --exact --nocapture` -- `cargo test --test codegen_tests codegen::eval::test_eval_fragment_dispatches_aot_static_methods -- --exact --nocapture` -- `cargo check --tests` -- `rustfmt --check src/eval_aot.rs src/ir_lower/program.rs src/ir_lower/expr/mod.rs src/codegen_ir/lower_inst/builtins/eval.rs tests/codegen/eval.rs` -- `git diff --check -- src/eval_aot.rs src/ir_lower/program.rs src/ir_lower/expr/mod.rs src/codegen_ir/lower_inst/builtins/eval.rs tests/codegen/eval.rs .plans/elephc-eval-aot-complete-plan.md` - -## Aggiornamento: callback a metodi statici - -Tranche completata: - -- il classificatore AOT per literal eval riconosce callback statiche - compile-time in forma `"Class::method"` e `["Class", "method"]`; -- `call_user_func()` e `call_user_func_array()` possono restare nel percorso - EIR AOT quando il metodo statico target e' pubblico, tipizzato e supportato - dagli stessi predicate delle chiamate statiche dirette; -- callback statiche verso metodi non tipizzati o non supportati continuano a - usare il fallback magician. - -Verifiche locali: - -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_callbacks_use_aot_without_magician -- --exact --nocapture` -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician -- --exact --nocapture` -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_uses_aot_without_magician -- --exact --nocapture` - -## Aggiornamento: callback statici con Class::class - -Tranche completata: - -- il classificatore AOT accetta anche callable array statici in forma - `[NamedClass::class, "method"]`; -- il supporto resta limitato a receiver nominali, lasciando fuori `self::class`, - `static::class` e `parent::class` finche' il gate AOT non riceve il contesto - di classe necessario; -- la stessa copertura AOT verifica string callback, callable array con stringa, - callable array con `Class::class`, `call_user_func()` e - `call_user_func_array()`. - -Verifiche locali: - -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_callbacks_use_aot_without_magician -- --exact --nocapture` -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` - -## Aggiornamento: callback first-class statici - -Tranche completata: - -- il gate AOT riconosce callback first-class compile-time in - `call_user_func()` e `call_user_func_array()` per funzioni note e metodi - statici nominali; -- la classificazione generale di un first-class callable come valore resta - conservativa: il supporto vale solo quando il callable e' usato come callback - statico immediato; -- la copertura verifica sia `call_user_func()` sia `call_user_func_array()` con - first-class callable a funzione utente e metodo statico; -- i predicate esistenti continuano a escludere metodi non tipizzati, receiver - non nominali e signature fuori subset. - -Verifiche locali: - -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_call_user_func_user_function_uses_aot_without_magician -- --exact --nocapture` -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_static_method_callbacks_use_aot_without_magician -- --exact --nocapture` -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_untyped_static_method_callback_keeps_bridge_fallback -- --exact --nocapture` - -## Chiusura piano - 2026-07-06 - -Audit dei criteri di completamento: - -1. I literal eval nel subset supportato entrano in percorsi AOT nativi e i - fallback restano marcati con motivo leggibile. -2. Il benchmark prime-sum fino a `100000` passa senza bridge, senza - `elephc_magician`, con output `454396537`. -3. Scope read/write del caller e variabili create da eval sono coperti dai test - direct-local/direct-param/scope-helper senza linkare magician quando il - frammento e' fully AOT. -4. `return`, fallthrough/null e output side-effect sono coperti da test dedicati. -5. Builtin statici comuni, chiamate a funzioni note, metodi statici pubblici, - `call_user_func*()` statici e first-class callback statici sono coperti dal - percorso AOT. -6. I fallback non supportati restano verificati: eval dinamico, callback - dinamici, signature non tipizzate, spread dinamici, casi array/object non - modellati e semantiche PHP conservative. -7. I programmi con solo eval fully AOT non linkano `elephc_magician`; il test - prime-loop verifica anche assenza di helper runtime `__elephc_eval_*`. -8. La soluzione non embedda il compilatore nel binario finale: l'AOT avviene nel - compilatore e i binari fully AOT non linkano il bridge magician. -9. Il prime-loop AOT e' passato su macOS ARM64 locale, Linux x86_64 Docker e - Linux ARM64 Docker con filtro dedicato. -10. `git diff --check` passa sull'intero worktree corrente; zero file risultano - modificati solo da whitespace rispetto a `git diff -w`. - -Ultime verifiche locali registrate: - -- `cargo build --release` -- `cargo test --test codegen_tests codegen::eval::test_literal_eval_prime_loop_uses_aot_without_execute_bridge -- --exact --nocapture` -- `./scripts/test-linux-x86_64.sh test_literal_eval_prime_loop_uses_aot_without_execute_bridge` -- `./scripts/test-linux-arm64.sh test_literal_eval_prime_loop_uses_aot_without_execute_bridge` -- benchmark manuale prime-sum fuori repo su 12 run: - - Elephc standard `13.35 ms`; - - Elephc via eval literal AOT `9.74 ms`; - - PHP standard `97.77 ms`; - - PHP via eval `117.76 ms`; - - RSS sample Elephc via eval `1572864` byte. -- `python3 scripts/benchmark_magician.py --case algebra_heavy --iterations 5 --warmup 1` - - Elephc native `2.84 ms`; - - Elephc eval `4.90 ms`; - - PHP native `78.54 ms`; - - PHP eval `80.17 ms`. -- `git diff --check` -- `comm -23 <(git diff --name-only | sort) <(git diff -w --name-only | sort) | wc -l` diff --git a/.plans/elephc-eval-complete-plan.md b/.plans/elephc-eval-complete-plan.md deleted file mode 100644 index 2678840e8a..0000000000 --- a/.plans/elephc-eval-complete-plan.md +++ /dev/null @@ -1,987 +0,0 @@ -# Piano dettagliato: supporto completo a eval tramite libelephc-magician - -Nota percorso: la richiesta indicava `~/Downlaods`, che sembra un refuso. Questo file e' stato scritto in `~/Downloads`. - -## Obiettivo - -Implementare `eval($code)` completo per il sottoinsieme PHP supportato da elephc, senza imporre parser runtime, interprete o scope dinamico ai programmi che non usano `eval`. - -La soluzione proposta e': - -```text -programma senza eval - -> runtime elephc normale - -> nessuna lib eval - -> nessun interprete - -> stesse performance di oggi - -programma con eval - -> runtime elephc normale - -> + libelephc-magician linkata condizionalmente - -> solo gli scope che arrivano a eval pagano il costo dinamico -``` - -La strategia runtime per le funzioni che contengono `eval` e': - -```text -codice nativo prima di eval - -> resta nativo e ottimizzabile finche' non attraversa la barriera eval - -al punto eval - -> valuta l'argomento in source order - -> materializza lo scope se non esiste - -> flush dei locals vivi nello scope dinamico - -> chiama libelephc-magician - -libelephc-magician - -> parse della stringa PHP - -> lowering a EvalIR/EIR dinamico - -> interpretazione usando lo scope ricevuto - -dopo eval - -> invalida i locals che eval puo' aver letto/scritto/unset - -> ricarica lazy dai dynamic cells al prossimo uso -``` - -## Decisioni architetturali - -1. `libelephc-magician` e' una bridge staticlib, come `elephc_tls`, `elephc_pdo`, `elephc_crypto` e `elephc_phar`. -2. Il linker la include solo quando il programma contiene una chiamata PHP a `eval`. -3. Il backend attivo resta AST -> EIR -> `src/codegen_ir/`; non si estende il backend AST legacy. -4. Il codice statico resta nativo. Non si interpreta tutta la funzione salvo dove necessario. -5. `eval` e' una barriera di effetti totale per optimizer, type checker e lowering. -6. L'interprete eval non introduce un value system separato: lavora sugli stessi valori/celle runtime di elephc. -7. L'EIR statico non deve diventare completamente dinamico. Lato eval conviene introdurre un `EvalIR` o un sottoinsieme EIR dinamico con istruzioni esplicite di scope. -8. Nessun JIT nella prima versione: niente assembler/linker a runtime, niente `dlopen`, niente compilazione ARM64/x86 al volo. - -## Semantica PHP da preservare - -Da verificare con `php -r` e codificare in test: - -1. La stringa passata a `eval` non deve contenere tag ` true se c'e' una call PHP a eval dopo name resolution/case-insensitive builtin lookup -``` - -Required libraries: - -```rust -if features.eval { - libs.push("elephc_magician".to_string()); -} -``` - -Importante: `eval` deve essere rilevato anche se scritto con casing diverso, se PHP lo consente, e anche in presenza di namespace fallback. - -## ABI nativa verso libelephc-magician - -L'ABI deve essere piccola, stabile e target-aware. Non passare enum Rust non `repr(C)` attraverso il confine staticlib. - -Simboli minimi: - -```c -uint32_t __elephc_eval_abi_version(void); - -int32_t __elephc_eval_execute( - ElephcEvalContext *ctx, - ElephcEvalScope *scope, - const uint8_t *code_ptr, - uint64_t code_len, - ElephcEvalResult *out -); -``` - -Possibili codici ritorno: - -```text -0 = ok, out contiene il valore di ritorno di eval o null -1 = parse error -2 = runtime fatal -3 = uncaught throwable -4 = unsupported construct in eval subset -5 = ABI/runtime version mismatch -``` - -`ElephcEvalResult`: - -```c -typedef struct { - uint32_t kind; // normal, return, fatal, throw - void *value_cell; // Mixed/runtime cell, owned secondo contratto runtime - void *error; // optional runtime error/throwable object -} ElephcEvalResult; -``` - -Regole: - -1. Nessun panic Rust deve attraversare l'ABI. -2. Nessuna eccezione C++/unwind attraverso l'ABI. -3. Tutti i valori passati sono opaque handle o puntatori a celle runtime elephc. -4. Il codice generato deve controllare il codice ritorno e abbassarlo al comportamento PHP atteso. -5. L'ABI deve essere identica su `macos-aarch64`, `linux-aarch64`, `linux-x86_64`. - -## EvalContext - -`EvalContext` rappresenta lo stato globale richiesto da eval: - -```text -EvalContext - - ABI version / runtime version - - allocator / GC hooks - - global function table dinamica - - global class table dinamica - - global constants table dinamica - - builtin registry - - current file path - - current line - - current namespace metadata - - current class/function/method metadata - - diagnostics sink -``` - -Il codice nativo deve creare o ottenere un `EvalContext` per processo/program activation. Non va ricreato a ogni chiamata se contiene tabelle globali dinamiche. - -## Dynamic Scope - -Lo scope deve rappresentare celle PHP per nome: - -```text -ElephcEvalScope - - parent/global link opzionale - - map: string name -> RuntimeCell* - - flags per entry: - present - unset - dirty - by_ref - persistent/owned/borrowed - - generation counter -``` - -Ogni variabile locale materializzata diventa una cella: - -```text -$x static local - -> slot nativo corrente - -> cella scope "x" -``` - -Regola chiave: - -```text -il codice nativo puo' continuare a usare slot/registri statici, -ma al punto eval deve sincronizzare i locals osservabili nello scope dinamico. -``` - -## Flush / invalidation / reload - -### Prima di eval - -Il lowering della chiamata `eval($code)` deve: - -1. Valutare `$code` in ordine sorgente. -2. Convertire/coercire `$code` a stringa secondo PHP. -3. Creare lo scope materializzato della activation se non esiste. -4. Calcolare il set di locals vivi e osservabili. -5. Fare flush dei locals nello scope: - -```text -local slot x -> RuntimeCell scope["x"] -local slot y -> RuntimeCell scope["y"] -``` - -6. Passare `EvalContext`, `Scope`, `code_ptr`, `code_len` a `__elephc_eval_execute`. - -### Durante eval - -L'interprete legge e scrive solo lo scope: - -```text -LoadVar("x") -> scope_get("x") -StoreVar("x", v) -> scope_set("x", v) -UnsetVar("x") -> scope_unset("x") -``` - -### Dopo eval - -Il codice nativo non deve fidarsi dei valori statici precedenti. - -Strategia consigliata: - -1. Marcare invalidi tutti i locals flushati. -2. Se libelephc-magician restituisce una dirty set affidabile, invalidare almeno quella. -3. Se non c'e' dirty set o se eval usa variabili variabili, references, `unset`, `global`, invalidare tutto lo scope materializzato. -4. Al prossimo uso statico di `$x`, fare reload lazy: - -```text -if local x invalid: - if scope has "x": - slot x = scope_get("x") - else: - slot x = null/unset semantics secondo contesto -``` - -Questo mantiene il codice prima e dopo eval nativo, ma tratta eval come barriera forte. - -### Variabili create da eval - -Esempio: - -```php -eval('$newVar = 42;'); -echo $newVar; -``` - -Il compiler statico vede `$newVar` dopo eval, ma non ha una definizione precedente. - -Regola proposta: - -1. Dentro una funzione che contiene eval, ogni read di variabile dopo una barriera eval deve poter fare fallback allo scope dinamico. -2. Se la variabile e' nota staticamente e non invalidata, usare lo slot nativo. -3. Se e' unknown/created-by-eval, leggere da `scope_get_or_null`. -4. Il tipo statico dopo eval degrada a `Mixed` per le variabili che possono essere toccate da eval. - -### unset - -Esempio: - -```php -$x = 10; -eval('unset($x);'); -echo $x; -``` - -Lo scope deve poter distinguere: - -```text -present null -missing/unset -``` - -Non basta salvare `Null`, perche' PHP distingue variabile definita a `null` da variabile non definita per `isset`, warning e certi accessi. - -## Cambi type checker - -### Builtin/catalog - -Agganciare `eval` nei punti canonici: - -1. `src/types/checker/builtins/catalog.rs` -2. `src/types/signatures.rs` -3. categoria builtin appropriata sotto `src/types/checker/builtins/` -4. `first_class_callable_builtin_sig()` solo se si decide che `eval` e' callable; PHP lo tratta come language construct, quindi probabilmente non deve diventare first-class callable. -5. `function_exists("eval")` va verificato contro PHP e allineato alla policy scelta. Se PHP restituisce false per language constructs, non inserirlo nel catalogo dei callable normali senza un'eccezione. - -Firma semantica: - -```text -eval(string $code): mixed -``` - -Ma internamente va modellata come language construct: - -```text -reads/writes locals -may define functions/classes/constants -may output -may throw/fatal -may return a value -is never pure -``` - -### Dynamic barrier - -Aggiungere una nozione nel checker: - -```text -FunctionDynamicState - contains_eval: bool - eval_barrier_seen: bool per blocco/control-flow -``` - -Dopo una barriera eval: - -1. I tipi dei locals osservabili diventano `Mixed` o `MaybeUnset`. -2. Le chiamate a funzioni/classi non note potrebbero essere permesse come lookup dinamico se il control-flow ha attraversato un eval che puo' averle dichiarate. -3. Warning/diagnostiche devono evitare falsi positivi su variabili create da eval. - -## Cambi optimizer/effects - -In `src/optimize/effects/`: - -```text -eval effects: - - reads all visible locals - - writes all visible locals - - may unset locals - - may define global functions/classes/constants - - may read/write globals - - may allocate heap - - may output - - may throw/fatal - - may call arbitrary functions -``` - -Regole: - -1. Non eliminare mai `eval`, anche se il risultato non e' usato. -2. Non propagare costanti attraverso `eval` per variabili visibili. -3. Non riordinare side effects attraverso `eval`. -4. Non foldare `eval` come stringa statica nella prima implementazione completa, per evitare due semantiche divergenti. Eventuale AOT static-eval puo' essere una ottimizzazione successiva. -5. Il DCE deve trattare eval come una call con effetti massimi. - -## Cambi IR / lowering - -### Nel programma statico - -Il codice statico non deve usare istruzioni dinamiche ovunque. Aggiungere solo cio' che serve per la barriera: - -```text -EnsureMaterializedScope -FlushLocalToScope(name, local) -CallEvalRuntime(scope, code) -InvalidateScopeLocals(scope) -ReloadLocalFromScope(name, local) -``` - -Queste possono essere: - -1. istruzioni EIR nuove, se servono al backend; -2. oppure lowering diretto in `src/ir_lower/expr/` verso call runtime + metadata; -3. oppure metadata sulla call builtin `eval` che `src/codegen_ir/` abbassa target-aware. - -Preferenza: - -```text -EIR statico esplicito abbastanza da validare ownership e frame layout, -ma senza trasformare tutte le variabili normali in lookup dinamici. -``` - -### Nel runtime eval - -Usare `EvalIR`, non necessariamente l'EIR completo: - -```text -EvalIR - ConstNull - ConstBool - ConstInt - ConstFloat - ConstString - LoadVar(name) - StoreVar(name, value) - UnsetVar(name) - LoadGlobal(name) - BinaryOp(op, left, right) - UnaryOp(op, value) - Echo(value) - Return(value) - CallDynamic(name, args) - If(...) - While(...) - Foreach(...) - ArrayGet/ArraySet - PropertyGet/PropertySet - NewObject - DefineFunction - DefineClass -``` - -Motivo: l'EIR statico e' ottimizzato per frame/locals noti; eval ha bisogno di operazioni by-name e lookup dinamico. - -## Runtime value bridge - -Non creare: - -```rust -enum Value { - Null, - Bool(bool), - Int(i64), - ... -} -``` - -come rappresentazione autonoma se poi il runtime nativo usa un'altra ABI. - -Fare invece: - -```text -EvalValue = handle/cell runtime elephc -``` - -Il bridge deve offrire operazioni: - -```text -value_null() -value_bool(bool) -value_int(i64) -value_float(f64) -value_string(ptr,len) -value_add(a,b) -value_concat(a,b) -value_truthy(v) -value_to_string(v) -value_release(v) -value_retain(v) -array_get/array_set with COW -object_get/object_set -``` - -Queste operazioni devono rispettare: - -1. boxed `Mixed` contract; -2. refcount; -3. ownership temporanei; -4. borrowed vs owned; -5. copy-on-write array/string; -6. cleanup su normal return, fatal, throw. - -## Dynamic function/class tables - -Eval completo richiede tabelle dinamiche globali. - -Esempio: - -```php -eval('function dyn() { return 42; }'); -echo dyn(); -``` - -Il codice statico dopo eval potrebbe chiamare una funzione non nota al compile-time. - -Serve: - -```text -DynamicFunctionTable - name -> EvalFunction - -DynamicClassTable - name -> EvalClass -``` - -Per chiamate statiche a simboli non risolti dopo eval: - -1. Il checker deve permettere lookup dinamico se il path di controllo puo' aver attraversato eval. -2. Il lowering deve generare `__rt_dynamic_call("dyn", args)` invece di direct symbol call. -3. `__rt_dynamic_call` cerca prima funzioni native note, poi funzioni eval-defined. -4. Le funzioni definite da eval possono essere interpretate da libelephc-magician. - -Per chiamate note staticamente: - -```text -foo(); // foo definita nel programma AOT -``` - -continuare a generare direct call quando possibile. - -## Name resolution e namespace - -Eval deve ricevere metadata del punto chiamante: - -```text -current namespace -current file -current line -current class -current function -current method -``` - -Pero' funzioni e classi dichiarate dentro eval vanno verificate contro PHP. Il test locale mostra che una funzione dichiarata da eval dentro `namespace A` risulta globale: - -```text -function_exists("A\\f_eval_test") -> false -function_exists("f_eval_test") -> true -``` - -Da trasformare in test prima di fissare l'implementazione. - -## Codegen target-aware - -Tutto il lowering verso la call ABI deve stare nel percorso EIR attivo: - -```text -src/ir_lower/expr/ -src/codegen_ir/lower_inst/ -src/codegen/abi/ -``` - -Regole: - -1. Non hardcodare registri ARM64 o x86_64 fuori dagli helper ABI. -2. La call a `__elephc_eval_execute` deve passare per gli helper target-aware. -3. Frame slots per scope handle, code string e result devono essere allocati prima del frame sizing. -4. Ogni `emitter.instruction(...)` aggiunta deve avere commento allineato secondo policy. -5. Coprire `macos-aarch64`, `linux-aarch64`, `linux-x86_64`. - -## Pipeline di implementazione - -### Fase 0: spike semantico - -Obiettivo: fissare comportamento PHP prima del codice. - -Deliverable: - -1. File di note o test fixture con output PHP per: - - variable read/write; - - variable creation; - - unset; - - return from eval; - - parse error; - - output; - - function declaration; - - class declaration; - - namespace interaction; - - `$this` in method scope; - - by-ref variables; - - global/static variables. -2. Decidere se il supporto iniziale e' "eval completo per tutto il sottoinsieme elephc" o "eval completo PHP" piu' ampio del compilatore statico. - -### Fase 1: link condizionale e stub ABI - -Obiettivo: linkare `libelephc-magician` solo quando serve. - -Modifiche: - -1. Aggiungere crate `crates/elephc-magician`. -2. Aggiungere `elephc_magician` a `BRIDGES` in `src/linker.rs`. -3. Aggiungere `RuntimeFeatures.eval`. -4. Aggiungere detection `program_requires_eval`. -5. Aggiungere builtin/language construct `eval` al checker. -6. Generare una call a `__elephc_eval_execute` che per ora ritorna un errore controllato. - -Test: - -1. Programma senza eval non linka `elephc_magician`. -2. Programma con eval linka `elephc_magician`. -3. Test linker per `ELEPHC_MAGICIAN_LIB_DIR`. -4. Errore chiaro se la libreria non e' trovata. - -### Fase 2: MaterializedScope minimo - -Obiettivo: avere uno scope dinamico passabile alla lib, ancora senza parser completo. - -Modifiche: - -1. Rappresentare `ElephcEvalScope`. -2. Aggiungere helper runtime per scope: - - create/destroy activation scope; - - set cell by name; - - get cell by name; - - unset by name; - - mark dirty; - - generation counter. -3. Nel lowering, creare scope handle per funzioni con eval. -4. Al punto eval, flush dei locals vivi. -5. Dopo eval, invalidazione totale dei locals flushati. -6. Reload lazy al prossimo uso. - -Test: - -1. Stub eval modifica `$x` via scope e il codice nativo dopo eval vede il nuovo valore. -2. Stub eval crea `$y` e `echo $y` dopo eval funziona. -3. Stub eval unsetta `$x` e il codice nativo vede lo stato unset. - -Nota: gli stub devono essere test-only o nascosti, non comportamento PHP pubblico. - -### Fase 3: parser runtime e fragment parsing - -Obiettivo: parse della stringa eval. - -Opzioni: - -1. Estrarre lexer/parser in crate riusabile, ad esempio `crates/elephc-frontend`. -2. Oppure duplicare temporaneamente solo il parser necessario in `crates/elephc-magician`, ma e' sconsigliato. - -Regole: - -1. `eval` parse-a statement fragment senza tag `x = $this->x + 1;'); - } -} -$a = new A(); -$a->bump(); -echo $a->x; -``` - -### Fase 8: references, globals, static locals - -Obiettivo: chiudere le parti piu' sensibili dello scope PHP. - -Implementare: - -1. references/by-ref nello scope; -2. `global $x` dentro eval; -3. `static $x` se supportato dal compilatore; -4. variabili variabili `${$name}` se supportate; -5. superglobals; -6. closure capture interaction se supportata. - -Test: - -1. modifica by-ref dentro eval; -2. `global` dentro eval modifica global esterno; -3. `unset` su reference; -4. nested eval con scope condiviso. - -### Fase 9: errori, throwable, cleanup - -Obiettivo: ownership e control flow robusti. - -Implementare: - -1. parse error; -2. fatal runtime; -3. exception/throw dentro eval se supportato; -4. cleanup temporanei su return/fatal/throw; -5. GC/refcount stress tests. - -Test: - -1. parse error catchable come PHP supportato; -2. throw dentro eval attraversa chiamante; -3. temporanei refcounted non leakano; -4. array COW dopo eccezione. - -### Fase 10: ottimizzazione performance - -Solo dopo correttezza. - -Possibili ottimizzazioni: - -1. Scope materializzato lazy: creare solo alla prima eval call eseguita. -2. Flush selettivo con liveness precisa. -3. Dirty set dalla lib per reload selettivo. -4. Versioned cells: reload solo se generation cambia. -5. Cache parse/lower per stringhe eval ripetute identiche. -6. Interning nomi variabili nello scope. -7. Fast path per eval senza writes esterni, se l'analisi runtime lo prova. - -Non fare queste ottimizzazioni prima dei test semantici. - -## Strategia test - -### Test unitari compiler - -1. `RuntimeFeatures.eval` detection. -2. Link required libs. -3. Builtin/catalog/signature. -4. Effects: eval non pure, non DCE. -5. Type invalidation after eval. - -### Test codegen end-to-end - -In `tests/codegen/eval.rs` o modulo dedicato: - -1. read local; -2. write local; -3. create local; -4. unset local; -5. return value; -6. output; -7. parse error; -8. dynamic function declaration; -9. dynamic function call after eval; -10. class/method `$this`; -11. arrays and COW; -12. nested eval; -13. eval inside loop; -14. eval in branch; -15. eval in function and top-level. - -### Test error - -In `tests/error_tests/`: - -1. wrong argument count; -2. non-string coercion behavior; -3. unsupported construct inside eval, se il sottoinsieme non lo supporta ancora; -4. duplicate function/class declaration; -5. parse error formatting. - -### Test cross-target - -Per ogni fase che tocca ABI/codegen/runtime: - -```bash -cargo test --test codegen_tests eval -./scripts/test-linux-x86_64.sh eval -./scripts/test-linux-arm64.sh eval -``` - -Prima del merge completo: - -```bash -cargo build -cargo test eval -cargo test -cargo test -- --include-ignored -git diff --check -``` - -## Rischi principali - -1. Scope sync incompleta: bug sottili in variabili create/unset dopo eval. -2. Value model duplicato: se EvalIR usa valori propri, COW/refcount diventano fragili. -3. Dynamic declarations: `eval('function f(){}'); f();` richiede fallback dinamico nel codice statico. -4. Type checker troppo statico: potrebbe rifiutare codice PHP valido dopo eval. -5. Optimizer troppo aggressivo: const propagation/DCE possono miscompilare attraversando eval. -6. ABI instabile tra compiler/runtime eval. -7. Parser runtime troppo accoppiato al compiler CLI. -8. File size e responsabilita': evitare un unico file gigante `interpreter.rs` che contiene parser, scope, lowering e runtime calls. - -## Criteri di completamento - -La feature e' considerata completa solo quando: - -1. Programmi senza eval non linkano `elephc_magician`. -2. Programmi senza eval non cambiano assembly/performance in modo osservabile. -3. Programmi con eval linkano `elephc_magician` automaticamente. -4. Le funzioni con eval usano flush/invalidate/reload corretto. -5. Eval modifica, crea e unsetta variabili dello scope chiamante. -6. `return` dentro eval ritorna il valore di `eval`. -7. Funzioni/classi dichiarate dentro eval sono richiamabili secondo semantica PHP. -8. Ownership/GC/COW sono coperti da test. -9. Tutti i target supportati passano i test eval. -10. Docs PHP e internals descrivono chiaramente che eval abilita un runtime dinamico opzionale. - -## Prima implementazione consigliata - -Sequenza pragmatica: - -1. Link condizionale `elephc_magician` + stub ABI. -2. `contains_eval` + lowering call runtime nel backend EIR. -3. `MaterializedScope` + flush/invalidate/reload con stub test-only. -4. Parser runtime per fragment eval. -5. EvalIR base: scalari, variabili, assign, add/concat, echo, return. -6. Arrays/control flow. -7. Dynamic calls. -8. Declarations. -9. Objects/classes. -10. References/global/static. -11. Performance pass. - -Questa sequenza tiene separati i rischi: prima si dimostra che lo scope condiviso funziona, poi si rende reale l'interprete eval. diff --git a/.plans/elephc-eval-magician-plan.md b/.plans/elephc-eval-magician-plan.md new file mode 100644 index 0000000000..fbd306fb8b --- /dev/null +++ b/.plans/elephc-eval-magician-plan.md @@ -0,0 +1,378 @@ +# Piano: eval, elephc-magician e literal eval AOT + +## Task + +- [x] Definire la semantica target di `eval`: scope caller visibile, scritture + persistenti, variabili create visibili dopo eval, `unset`, output, parse + error, `return` locale al frammento, dichiarazioni dinamiche e `$this`. +- [x] Aggiungere `crates/elephc-magician` come bridge opzionale e linkarlo solo + quando il programma richiede il fallback eval runtime. +- [x] Aggiungere ABI, `RuntimeFeatures`, linker bridge e runtime helper per + chiamare `__elephc_eval_execute` dal backend EIR corrente. +- [x] Implementare `ElephcEvalContext` e `ElephcEvalScope` condivisi tra codice + nativo e interpreter, inclusi flush/reload dei locals osservabili. +- [x] Implementare parser runtime, EvalIR/interpreter e value bridge per il + subset eval supportato da magician. +- [x] Supportare nel fallback magician variabili, assegnamenti, output, return, + control flow, arrays, include/require, chiamate dinamiche, dichiarazioni, + classi/oggetti, reflection, callable, references/by-ref e cleanup errori per + il subset coperto dai test. +- [x] Modellare `eval` come barriera di effetti per optimizer/type checker: + niente DCE, niente propagazione costanti attraverso locals osservabili e + fallback dinamico dove necessario. +- [x] Aggiungere benchmark magician ripetibili con varianti Elephc native, + Elephc eval, PHP native e PHP eval. +- [x] Aggiungere parse cache, parse-error cache e include parse/file cache senza + congelare context, scope, magic constants o include_once state. +- [x] Aggiungere cache per lookup simboli eval, direct builtin dispatch, + callable resolution cache e ottimizzazioni conservative su `RuntimeValueOps`. +- [x] Aggiungere fast path unboxed scalar, linear EvalIR/stack VM opzionale e + ottimizzazioni mirate per array/reference/COW nel bridge. +- [x] Implementare literal `eval` AOT conservativo per scalari, output, return, + store/scope read-write e marker assembly di AOT/fallback. +- [x] Estendere literal eval AOT a locals interni, `while`, `if`, `break`, + `continue`, confronti/truthiness, modulo e benchmark prime-sum fino a + `100000`. +- [x] Estendere literal eval AOT a builtins statici comuni, funzioni statiche + note, metodi statici pubblici tipizzati e callback statici via + `call_user_func*()`. +- [x] Evitare il link a `elephc_magician` per programmi con soli eval literal + fully AOT. +- [x] Aggiornare i test parity per distinguere builtin condivisi, + builtin eval-only documentati e builtin static-only non ancora presenti in + magician. +- [ ] Ridurre il mini-codegen AOT manuale residuo e convergere verso funzioni + EIR interne per i frammenti literal supportati. +- [ ] Espandere AOT solo dove semanticamente coperto: arrays/iterables completi, + object/member access, references/by-ref, `global`, `static`, variable + variables, `try`/`throw`, include/require e dichiarazioni restano fallback + finche' non hanno modello e test dedicati. +- [ ] Chiudere o mantenere esplicitamente il gap dei builtin static-only: + implementarli in magician oppure tenerli in allowlist testata finche' eval non + li espone. +- [ ] Promuovere i benchmark di accettazione AOT piu' utili nella suite + benchmark permanente, senza includere compile/link time nei runtime numbers. +- [ ] Aggiornare docs utente/internals dopo ogni estensione semantica del + subset eval o AOT. +- [ ] Eseguire verifiche focused sui tre target supportati per ogni modifica che + tocca ABI, runtime ownership, codegen eval o fallback/AOT selection. + +## Scope del piano + +Questo piano sostituisce e fonde: + +- `.plans/elephc-eval-complete-plan.md` +- `.plans/elephc-eval-aot-complete-plan.md` +- `.plans/elephc-magician-performance-plan.md` + +Il piano resta in `.plans` per tracciare solo il lavoro eval/magician ancora +aperto. Le sezioni completate documentano lo stato raggiunto e servono come +guardrail per non reintrodurre vecchi approcci o regressioni. + +## Stato corrente + +Il supporto eval esiste su due percorsi: + +1. Fallback runtime tramite `libelephc-magician`, chiamato da + `__elephc_eval_execute`. +2. Literal eval AOT, quando il frammento e' una stringa nota a compile time e il + classificatore lo considera semanticamente sicuro. + +Il backend attivo dopo il rebase su `main` e' il percorso EIR sotto +`src/ir_lower/`, `src/ir_passes/` e `src/codegen/lower_inst/`. I riferimenti +storici a `src/codegen_ir/` nei vecchi piani sono obsoleti. + +I file centrali attuali sono: + +- `crates/elephc-magician/src/` +- `src/eval_aot.rs` +- `src/ir_lower/expr/mod.rs` +- `src/ir_lower/program.rs` +- `src/codegen/lower_inst/builtins/eval.rs` +- `src/codegen_support/runtime/eval_bridge.rs` +- `src/codegen_support/runtime_features.rs` +- `tests/codegen/eval.rs` +- `tests/codegen/eval_callables.rs` +- `tests/codegen/eval_callable_ref_errors.rs` +- `tests/codegen/eval_constructors.rs` +- `tests/codegen/eval_closures.rs` +- `tests/codegen/eval_reflection_invocation.rs` +- `tests/builtin_parity_tests.rs` + +## Architettura consolidata + +### Fallback magician + +`elephc-magician` e' una staticlib bridge opzionale. I programmi senza eval +runtime non devono linkarla. Il fallback resta obbligatorio per: + +- eval dinamico; +- literal eval non parseabile o non supportato dal classificatore AOT; +- costrutti con semantica runtime non ancora modellata in AOT; +- dichiarazioni dinamiche, include/require, references/by-ref, global/static, + variable variables, oggetti/members dinamici e throwable finche' non coperti. + +Il fallback riceve: + +- context globale eval; +- scope locale eval; +- scope globale quando serve; +- puntatore/lunghezza del codice; +- buffer risultato. + +Il value model non deve divergere da quello nativo: boxing, refcount, COW, +references e cleanup devono rimanere coerenti con il runtime elephc. + +### Scope sync + +Il codice nativo deve sincronizzare con lo scope eval solo cio' che e' +osservabile dal frammento: + +- prima della chiamata: flush delle variabili lette/scritte quando serve; +- durante eval: magician opera sullo scope condiviso; +- dopo eval: reload delle variabili che possono essere state scritte, create o + unsettate. + +Quando l'analisi non e' precisa, la semantica vince sulla performance: usare il +fallback o trattare il frammento come barriera piu' forte. + +### Literal eval AOT + +Il compilatore analizza il frammento literal a compile time: + +```text +literal string + -> parse come PHP fragment + -> normalizzazione/nameresolution compatibile col contesto + -> classificazione eligibility AOT + -> piano read/write/call/fallback + -> lowering nativo o fallback magician +``` + +Il piano AOT deve preservare: + +- `return expr;` ritorna da eval, non dal caller; +- fallthrough senza `return` produce `null`; +- output resta side effect visibile; +- variabili caller note a compile time sono leggibili/scrivibili; +- variabili create dal frammento sono visibili dopo eval se il path AOT lo + dichiara supportato; +- ogni costrutto non coperto rimane fallback esplicito. + +I path AOT emettono marker assembly tipo `eval literal AOT compiled...`; i +fallback emettono marker con ragione leggibile quando possibile. + +## Lavoro completato + +### Eval runtime e bridge + +Completato: + +- crate `elephc-magician`; +- ABI C/Rust verso `__elephc_eval_execute`; +- bridge linker `elephc_magician`; +- detection runtime features; +- eval language construct nel checking/lowering; +- materialized scope, context e value bridge; +- flush/reload dei locals osservabili; +- error/status mapping e cleanup. + +La copertura codegen e interpreter include eval in top-level, funzioni, metodi, +scope condiviso, nested eval, return/output, variabili create, mutation di +locals, callables, constructors, closures e reflection. + +### Interpreter magician + +Completato per il subset corrente: + +- lexer/parser runtime per fragment eval senza tag ` +cargo test --test codegen_tests eval_ +git diff --check +``` + +Per modifiche a ABI/codegen/runtime ownership: + +```bash +cargo check +cargo test --test codegen_tests +./scripts/test-linux-x86_64.sh +./scripts/test-linux-arm64.sh +git diff --check +``` + +Per benchmark manuali: + +```bash +python3 scripts/benchmark_magician.py --case algebra_heavy --iterations 5 --warmup 1 +python3 scripts/benchmark_magician.py --case literal_scalar_aot --iterations 5 --warmup 1 +``` + +## Rischi + +- Scope sync incompleta puo' creare variabili stale o mancare creazioni/unset. +- Duplicare codegen AOT manuale crea un secondo backend difficile da mantenere. +- Treating eval as ordinary static code can break PHP eval semantics. +- References, COW, arrays and object properties can introduce double-free, + leaks or missed mutations if bypassano helper runtime. +- `eval('$x + 1;')` ritorna `null`, non l'ultima espressione. +- Fallback selection troppo aggressiva puo' miscompilare codice dinamico. +- Ottimizzazioni magician non devono congelare context/scope/magic constants. +- Ogni path nuovo deve restare target-aware su macOS ARM64, Linux ARM64 e + Linux x86_64. + +## Criteri di completamento finale + +Il lavoro eval/magician puo' considerarsi chiuso quando: + +1. il fallback magician copre in modo testato il subset PHP dichiarato; +2. ogni literal eval supportato usa AOT o fallback esplicito con reason; +3. il subset AOT non dipende da un mini-backend manuale non manutenibile; +4. programmi fully AOT non linkano `elephc_magician`; +5. static/eval builtin parity non ha allowlist stale; +6. benchmark prime-loop e algebra-heavy restano corretti e misurabili; +7. i tre target supportati hanno copertura focused per ogni cambio ABI/codegen; +8. docs e test riflettono esattamente il subset supportato e i fallback. diff --git a/.plans/elephc-magician-performance-plan.md b/.plans/elephc-magician-performance-plan.md deleted file mode 100644 index 2b1724ae64..0000000000 --- a/.plans/elephc-magician-performance-plan.md +++ /dev/null @@ -1,875 +0,0 @@ -# elephc-magician Performance Plan - -| Order | Work item | Objective | Status | Notes | -|---:|---|---|---|---| -| 1 | Dedicated magician benchmark suite | Measure real hot spots before larger interpreter or bridge refactors | Done | Added `scripts/benchmark_magician.py` plus fixture cases under `benchmarks/magician/cases/`; CI now publishes magician benchmark artifacts. | -| 2 | Eval fragment parse cache | Avoid repeated tokenization and parsing for identical runtime source bytes | Done | Implemented in commit `4ba0efb5c` through `crates/elephc-magician/src/parse_cache.rs`. | -| 3 | Parse-error cache | Avoid reparsing repeated invalid fragments | Done | Included in the same parse cache as successful `EvalProgram` results. | -| 4 | Dynamic context/scope preservation for cached parses | Ensure caching does not freeze magic constants, variables, declarations, or runtime state | Done | The cache stores only immutable parse results; execution still receives the current `ElephcEvalContext` and `ElephcEvalScope`. | -| 5 | Include parse cache | Avoid repeated parsing of identical PHP code blocks loaded through include/require | Done | Added a metadata-validated include-file cache for file bytes and PHP block splitting; include_once state remains context-local and stale-prone missing/path checks stay live. | -| 6 | Eval symbol lookup cache | Speed up repeated function/class/method lookup for symbols declared by eval | Done | Added per-context caches for dynamic/native functions, constants, class-like symbols, aliases, and eval class methods. | -| 7 | Direct builtin dispatch | Avoid repeated string matching and generic dispatch for common builtins | Done | Added a hot positional direct-dispatch layer for benchmark-relevant scalar/core builtins while named/spread calls still use generic binding. | -| 8 | Callable resolution cache | Avoid reconstructing equivalent string/array/first-class/closure callables repeatedly | Done | Added bounded stable string-callable normalization caching; object, array, first-class, and scope-sensitive special-class callables still resolve live. | -| 9 | Reduce `RuntimeValueOps` calls on simple operations | Cut bridge overhead for arithmetic, comparisons, casts, and simple output paths | Done | Added a combined non-object echo bridge so scalar/null/array output avoids the separate `type_tag` call; arithmetic coercion shortcuts remain deferred to unboxed scalar work. | -| 10 | Unboxed scalar fast paths | Avoid boxing/unboxing for hot int/float/bool/string paths inside eval execution | Done | Added a conservative int/bool temporary evaluator for pure assignment, return, and condition boundaries; scope cells remain boxed and risky coercions fall back to runtime hooks. | -| 11 | Compact bytecode or linear EvalIR form | Reduce tree-walk overhead and branch-heavy dispatch in the current EvalIR interpreter | Done | Added an optional cached straight-line linear executable with a small stack VM; loops, declarations, includes, OOP, short-circuit expressions, and other complex forms stay on EvalIR fallback. | -| 12 | Array/reference/COW bridge optimizations | Reduce cost of array mutation and by-reference parameter handling | Done | Added narrow integer-key array write fast paths for eval scope-variable indexed append/set; associative append, references, and broad COW behavior remain on the existing semantic path. | -| 13 | AOT for literal `eval` | Compile `eval('...')` fragments ahead of time instead of interpreting them through magician | Done | Added conservative literal AOT for scalar constants, scalar output/returns/stores, and boxed Mixed scope reads/read-modify-writes; unsupported builtins, declarations, includes, OOP, and control flow keep the bridge fallback. | - -## 1. Dedicated Magician Benchmark Suite - -### Goal - -Create repeatable benchmarks that isolate where `elephc-magician` spends time before making larger performance changes. This should prevent optimizing only the obvious parse path while missing the real cost of interpreter dispatch, value boxing, builtin lookup, callable resolution, or array/reference handling. - -### Scope - -Add a small benchmark harness that compares: - -- Native elephc code without eval. -- elephc code that enters magician through `eval`. -- PHP standard execution without eval. -- PHP execution through `eval`. - -The suite should include both microbenchmarks and a few mixed workloads: - -- Repeated identical small `eval` fragments. -- One large `eval` fragment with a compute-heavy loop. -- Arithmetic-only loops. -- String concatenation and output. -- Array reads/writes. -- Function calls declared inside eval. -- Builtin calls inside eval. -- Callable dispatch inside eval. -- Include/require with repeated code blocks. - -### Current State - -Done. - -The implementation adds: - -- `scripts/benchmark_magician.py` -- `benchmarks/magician/README.md` -- `benchmarks/magician/cases/*` - -The suite compares native elephc, elephc through magician eval, native PHP, and -PHP through eval. It records runtime wall clock, eval invocation counts, -fragment sizes, literal-vs-dynamic source shape, parse-cache expectations, and -stdout correctness. The existing benchmark CI job also runs the magician suite -and uploads markdown/JSON artifacts. - -### Likely Files - -- `benches/` or `scripts/bench-eval-*` depending on existing project conventions. -- `tests/codegen/eval*.rs` only for correctness guards, not timing. -- `crates/elephc-magician/src/` only if benchmark-only hooks are required behind `#[cfg(test)]` or a feature gate. - -### Validation - -Each benchmark should record: - -- Runtime wall clock. -- Number of eval invocations. -- Fragment size. -- Whether the fragment is literal or dynamic. -- Whether parse cache should hit. -- Output correctness against PHP where practical. - -Validation run after implementation: - -- `python3 -m py_compile scripts/benchmark_magician.py` -- `python3 scripts/benchmark_magician.py --list` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case repeated_small_eval --json /tmp/elephc-magician-bench.json --markdown /tmp/elephc-magician-bench.md` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --json /tmp/elephc-magician-bench-all.json --markdown /tmp/elephc-magician-bench-all.md` - -### Risks - -Benchmark results can be misleading if compile+assemble+link time is included for runtime comparisons. Runtime-only binaries should be generated once and executed repeatedly. - -## 2. Eval Fragment Parse Cache - -### Goal - -Avoid repeated tokenization and parsing for identical eval source bytes. - -### Current State - -Done in commit `4ba0efb5c`. - -The implementation adds `crates/elephc-magician/src/parse_cache.rs` and routes these call sites through it: - -- `crates/elephc-magician/src/ffi/execute.rs` -- `crates/elephc-magician/src/interpreter/include_exec.rs` - -### Design - -The cache is process-local, bounded, and keyed by exact fragment bytes. It stores immutable `EvalProgram` instances behind `Arc`, plus parse errors. - -Current policy: - -- FIFO capacity: 256 entries. -- Maximum cacheable source: 64 KiB. -- Larger fragments bypass the cache. -- Mutex poisoning is recovered by taking the inner cache. - -### Validation Already Run - -- `cargo test -p elephc-magician parse_cache` -- `cargo test -p elephc-magician execute_program_nested_eval_uses_same_scope` -- `cargo test -p elephc-magician execute_program_include_uses_call_site_and_returns_file_result` -- `cargo test --test codegen_tests test_eval_return_value` -- `git diff --check` - -### Follow-Up - -After benchmarks exist, revisit capacity and maximum source size. If workloads show many repeated fragments above 64 KiB, consider size-based memory budgeting instead of a hard source-length cutoff. - -## 3. Parse-Error Cache - -### Goal - -Avoid reparsing invalid fragments that are repeatedly passed to `eval`. - -### Current State - -Done as part of the parse cache. - -### Design - -The cached result type is: - -```rust -Result, EvalParseError> -``` - -This means both successful parses and parse errors are reusable. - -### Validation Already Run - -The unit test `parse_cache::tests::cache_reuses_parse_errors` verifies that parse errors are cached and returned without reparsing. - -### Follow-Up - -If user code frequently emits many distinct invalid fragments, error caching could retain noise. Benchmark and memory telemetry should decide whether invalid-fragment caching needs a lower capacity or should be disabled for very large invalid inputs. - -## 4. Dynamic Context And Scope Preservation - -### Goal - -Guarantee the parse cache does not alter PHP-observable runtime behavior. - -The cache must not freeze: - -- Variables from the caller scope. -- Variables created by eval. -- Function/class/interface/trait/enum declarations. -- Magic constants that depend on the current call site. -- Include file metadata. -- Pending throw state. -- Return values. - -### Current State - -Done for the parse cache. - -### Design - -The cache stores only the parsed `EvalProgram`. Execution still happens through the existing interpreter entry points and receives the current context and scope every time. - -Magic constants remain safe because EvalIR stores magic-constant nodes and runtime evaluation resolves context-dependent values through `ElephcEvalContext`. - -### Validation Already Run - -- Nested eval scope sharing: `execute_program_nested_eval_uses_same_scope`. -- Include file magic and scope sharing: `execute_program_include_uses_call_site_and_returns_file_result`. -- Native bridge return value: `test_eval_return_value`. - -### Follow-Up - -Add a focused regression test for the same cached fragment executed under two different call sites, verifying `__FILE__` and `__DIR__` remain context-sensitive. - -## 5. Include Parse Cache - -### Goal - -Avoid reparsing identical PHP code blocks loaded through include/require. - -### Current State - -Done. - -The parse cache is used by `eval_execute_include_code()`, so parsed PHP code -blocks are reused when exact source bytes match. - -The implementation now also adds a bounded process-local include-file cache in -`crates/elephc-magician/src/interpreter/include_exec.rs`. Cache entries store: - -- Canonical include key for `include_once`/`require_once`. -- File metadata used to reject stale entries. -- File bytes. -- Precomputed raw/PHP-code block ranges for `` split scanning. - -The cache deliberately keeps missing-file checks, cwd-first resolution checks, -and caller-directory fallback checks live. Caching those negative/path decisions -would hide files created later at runtime or violate PHP's cwd-first include -order. `include_once` state remains in `ElephcEvalContext`. - -### Likely Files - -- `crates/elephc-magician/src/interpreter/include_exec.rs` -- Possibly `crates/elephc-magician/src/context.rs` for include-once metadata if shared caching needs context-level state. - -### Implementation Plan - -1. Measure include-heavy workloads first. -2. Add a small include-file cache only if file I/O dominates. -3. Key file cache entries by canonical path plus file metadata where available. -4. Keep include_once semantics in `ElephcEvalContext`; do not move "already included" behavior into a global cache. -5. Ensure `__FILE__` and `__DIR__` still come from the current include path, not from cached source metadata. - -All five steps are implemented. The benchmark suite added in point 1 includes -`include_repeated`, and the cache uses the current resolved include path when -executing cached bytes so magic constants remain call-site sensitive. - -### Validation - -Run focused include tests: - -- `execute_program_include_uses_call_site_and_returns_file_result` -- `execute_program_include_once_skips_regularly_included_file` -- `execute_program_missing_include_warns_and_returns_false` -- `execute_program_missing_require_is_runtime_fatal` - -Validation run after implementation: - -- `cargo test -p elephc-magician execute_program_include_uses_call_site_and_returns_file_result` -- `cargo test -p elephc-magician execute_program_include_once_skips_regularly_included_file` -- `cargo test -p elephc-magician execute_program_regular_include_observes_modified_file_after_cache_hit` -- `cargo test -p elephc-magician execute_program_missing_include_warns_and_returns_false` -- `cargo test -p elephc-magician execute_program_missing_require_is_runtime_fatal` -- `cargo test --test codegen_tests test_eval_fragment_include_once_and_plain_file` -- `cargo test --test codegen_tests test_eval_fragment_include_executes_php_file_and_returns_value` -- `cargo test --test codegen_tests test_eval_fragment_missing_require_fails` - -### Risks - -File caches can easily become stale. A conservative first version should cache parsed source bytes only within one process and avoid hiding file changes unless PHP-compatible behavior is explicitly defined. - -## 6. Eval Symbol Lookup Cache - -### Goal - -Speed up repeated lookup of functions, classes, methods, interfaces, traits, and enums declared dynamically through eval. - -### Motivation - -Once parsing is cached, repeated dynamic calls may still pay for name normalization, case-insensitive matching, namespace fallback, and symbol-table scans. - -### Likely Files - -- `crates/elephc-magician/src/context.rs` -- `crates/elephc-magician/src/interpreter/dynamic_functions.rs` -- `crates/elephc-magician/src/interpreter/reflection.rs` -- `crates/elephc-magician/src/interpreter/statements.rs` - -### Implementation Plan - -1. Inventory current lookup paths for function, class, method, and constant resolution. -2. Identify whether each lookup is already stored in a normalized map. -3. Add cache layers only where repeated lookup still performs normalization or scanning. -4. Invalidate or update caches when eval declares a new symbol. -5. Keep case-insensitive PHP behavior canonical. -6. Preserve namespace fallback for builtins and user symbols. - -### Current State - -Done. - -The implementation adds a per-context `EvalSymbolLookupCache` in -`crates/elephc-magician/src/context.rs`. It caches: - -- Dynamic/native function classification. -- Dynamic constant key resolution. -- Class-like resolution for classes, interfaces, traits, enums, and aliases. -- Inherited and directly declared eval class method lookups. - -The cache is protected by a poison-recovering `Mutex` so FFI `catch_unwind` -wrappers remain unwind-safe. Declarations and alias registration clear the -affected cache families, so cached misses do not hide later eval declarations. - -### Validation - -Add tests for: - -- Repeated calls to an eval-declared function. -- Case-insensitive lookup. -- Namespaced calls with builtin fallback. -- `function_exists`, `class_exists`, and reflection seeing updated declarations after eval. - -Validation run after implementation: - -- `cargo test -p elephc-magician function_exists_sees_function_declared_after_cached_miss` -- `cargo test -p elephc-magician constant_exists_sees_constant_defined_after_cached_miss` -- `cargo test -p elephc-magician dynamic_class_exists_sees_alias_declared_after_cached_miss` -- `cargo test -p elephc-magician class_method_lookup_sees_class_declared_after_cached_miss` -- `cargo test -p elephc-magician function_exists_reports_declared_eval_function` -- `cargo test -p elephc-magician dynamic_class_exists_reports_declared_eval_class` -- `cargo test -p elephc-magician execute_program_class_alias_registers_aliases` -- `cargo test -p elephc-magician execute_program_static_callable_array_dispatches_eval_method` -- `cargo test --test codegen_tests test_function_exists_sees_builtins_and_eval_declared_functions_after_eval` -- `cargo test --test codegen_tests test_eval_declared_function_can_be_called_with_call_user_func` -- `cargo test --test codegen_tests test_eval_function_exists_builtin_case_insensitive` - -### Risks - -Incorrect caching here can make declarations invisible or make duplicate declarations appear valid. This must be treated as a semantic change, not a pure optimization. - -## 7. Direct Builtin Dispatch - -### Goal - -Avoid repeated generic builtin lookup and string matching for common builtin calls inside eval. - -### Motivation - -Eval currently supports many builtins through interpreter dispatch. If a hot loop repeatedly calls the same builtin, the dispatch path should not repeatedly resolve the same function name from scratch. - -### Likely Files - -- `crates/elephc-magician/src/interpreter/builtins/` -- `crates/elephc-magician/src/interpreter/core_builtins.rs` -- `crates/elephc-magician/src/interpreter/builtin_metadata.rs` -- `crates/elephc-magician/src/interpreter/dynamic_functions.rs` - -### Implementation Plan - -1. Use benchmarks to rank builtin categories by runtime cost. -2. Add a compact builtin id or function pointer to parsed call expressions when safe. -3. Keep unknown/dynamic call paths generic. -4. Preserve PHP case-insensitivity and namespace fallback. -5. Ensure direct dispatch and generic dispatch share argument validation. - -### Current State - -Done. - -The benchmark suite's `builtin_calls` case stresses repeated `strlen()` and -`intval()` inside cached eval fragments, so the first direct path targets that -hot scalar/core family. The implementation adds -`crates/elephc-magician/src/interpreter/builtins/registry/direct.rs` with a -compact `EvalDirectBuiltin` selector for: - -- `strlen` -- `intval`, `floatval`, `strval`, `boolval` -- `count` -- `ord` -- `abs` - -The fast path only accepts plain positional direct calls. Named arguments, -spread arguments, dynamic callables, first-class callables, and unknown names -fall through to the existing generic builtin binding/dispatch path, so argument -validation and callable semantics remain centralized. - -### Validation - -For each optimized builtin group, add parity tests for: - -- Direct call. -- Case-insensitive call. -- Namespaced fallback. -- Named arguments when supported. -- First-class callable and callable aliases when relevant. - -Validation run after implementation: - -- `cargo test -p elephc-magician execute_program_dispatches_hot_direct_builtins_with_generic_fallbacks` -- `cargo test -p elephc-magician execute_program_dispatches_cast_builtins` -- `cargo test -p elephc-magician execute_program_dispatches_abs_builtin` -- `cargo test -p elephc-magician execute_program_dispatches_ord_builtin` -- `cargo test -p elephc-magician execute_program_counts_eval_countable_objects` -- `cargo test -p elephc-magician execute_program_namespace_call_falls_back_to_builtin` -- `cargo test -p elephc-magician execute_program_namespace_function_overrides_builtin_fallback` -- `cargo test -p elephc-magician execute_program_first_class_callables_dispatch_functions_and_methods` -- `cargo test --test codegen_tests test_eval_dispatches_simple_builtin_calls` -- `cargo test --test codegen_tests test_eval_dispatches_cast_builtin_calls` -- `cargo test --test codegen_tests test_eval_dispatches_abs_builtin_call` -- `cargo test --test codegen_tests test_namespaced_calls_fall_back_to_builtin_before_and_after_eval` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case builtin_calls --json /tmp/elephc-magician-builtin-bench.json --markdown /tmp/elephc-magician-builtin-bench.md` - -### Risks - -A second builtin registry can drift from the canonical catalog. Any direct dispatch table must be generated from or tightly tied to the existing metadata source. - -## 8. Callable Resolution Cache - -### Goal - -Avoid rebuilding equivalent callable targets repeatedly. - -### Motivation - -Callable semantics now cover string callables, array callables, first-class callable syntax, and closures. Repeatedly resolving the same target can become expensive in callback-heavy eval programs. - -### Likely Files - -- `crates/elephc-magician/src/interpreter/dynamic_functions.rs` -- `crates/elephc-magician/src/interpreter/reflection.rs` -- `crates/elephc-magician/src/context.rs` -- `crates/elephc-magician/src/ffi/callables.rs` - -### Implementation Plan - -1. Define a canonical callable key for stable cases. -2. Cache only callables whose target is stable under current context rules. -3. Invalidate or bypass on symbol-table changes when needed. -4. Keep bound object callables separate from static function/class callables. -5. Do not cache by raw runtime-cell pointer unless ownership and lifetime are explicit. - -### Current State - -Done. - -The implementation adds a bounded per-context string-callable normalization cache -to `ElephcEvalContext`. Cached entries cover stable callback strings only: - -- Function strings such as `"strlen"` or `"eval_declared_fn"`. -- Ordinary static method strings such as `"ClassName::method"`. - -The cache stores normalized `Named` or `StaticMethod` callable metadata, not a -validated callable target. Invocation and `is_callable()` still perform live -symbol/method checks, so cached misses do not hide later eval declarations. - -These forms deliberately bypass the cache: - -- Object callables. -- Callable arrays. -- First-class callable/Closure objects. -- Scope-sensitive `self::`, `static::`, and `parent::` string callables. - -That keeps `$this`, called-class, lexical scope, and runtime-cell lifetime rules -owned by the existing live resolver. - -### Validation - -Add tests for repeated: - -- String function callables. -- Static string callables like `Class::method`. -- Array callables. -- First-class callables. -- `Closure::fromCallable`. -- By-reference callable arguments. - -Validation run after implementation: - -- `cargo test -p elephc-magician execute_program_string_callable_cache_sees_late_function_declaration` -- `cargo test -p elephc-magician execute_program_string_callable_cache_sees_late_static_method_declaration` -- `cargo test -p elephc-magician execute_program_call_user_func_dispatches_builtin` -- `cargo test -p elephc-magician execute_program_callable_array_variable_dispatches_object_method` -- `cargo test -p elephc-magician execute_program_static_callable_array_dispatches_eval_method` -- `cargo test -p elephc-magician execute_program_first_class_callables_dispatch_functions_and_methods` -- `cargo test -p elephc-magician execute_program_static_runtime_callables_write_back_by_ref_type_coercion` -- `cargo test --test codegen_tests test_eval_closure_from_callable_special_string_callables_preserve_by_ref_writeback` -- `cargo test --test codegen_tests test_eval_declared_callable_forms_preserve_by_ref_writeback` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case callable_dispatch --json /tmp/elephc-magician-callable-bench.json --markdown /tmp/elephc-magician-callable-bench.md` - -### Risks - -Bound method callables can carry `$this`, visibility scope, or closure binding state. Caching must not reuse a callable across an incompatible object or visibility context. - -## 9. Reduce `RuntimeValueOps` Calls On Simple Operations - -### Goal - -Reduce the number of runtime bridge calls needed for simple operations inside eval. - -### Motivation - -Even after parse and dispatch are cached, simple arithmetic can still be slow if each operator repeatedly boxes, unboxes, allocates, or crosses through generic runtime hooks. - -### Current State - -Done. - -The implementation adds `RuntimeValueOps::echo_non_object()` and a generated -`__elephc_eval_value_echo_non_object` bridge for ARM64 and x86_64. Eval `echo` -statements and `print` expressions now try the combined non-object path first: -scalar/null/array output is handled in one runtime bridge call, while object -values still defer to the existing interpreter `__toString()` dispatch before -emitting the result. - -Before this change, common scalar output paid one bridge call for `type_tag()` -and one for `echo()`. The fast path now uses one bridge call for the same -non-object cases. PHP numeric operation shortcuts were intentionally left on -the generic runtime hooks because exact scalar coercion and error/fatal -semantics are subtle enough to belong with the unboxed scalar fast-path work. - -Benchmark smoke after the change: - -```text -python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case string_output --json /tmp/elephc-magician-output-bench.json --markdown /tmp/elephc-magician-output-bench.md -string_output | 1200 evals | elephc native 310.35 ms | elephc eval 395.56 ms | eval/native 1.27x -``` - -### Likely Files - -- `crates/elephc-magician/src/interpreter/expressions.rs` -- `crates/elephc-magician/src/interpreter/constant_eval.rs` -- `crates/elephc-magician/src/interpreter/runtime_ops.rs` -- `crates/elephc-magician/src/runtime_hooks/ops.rs` - -### Implementation Plan - -1. Count `RuntimeValueOps` calls in arithmetic-heavy eval benchmarks. -2. Identify pure scalar operations where both operands are already scalar cells. -3. Add internal helper paths that perform combined operation + allocation where safe. -4. Avoid changing behavior for arrays, objects, strings with PHP coercion edge cases, refs, or `mixed` values until covered. -5. Keep fatal/error behavior identical to the current generic path. - -### Validation - -Validation run after implementation: - -- `cargo test -p elephc-magician execute_program_print_returns_one` -- `cargo test -p elephc-magician execute_program_echoes_and_unsets_scope_value` -- `cargo test -p elephc-magician execute_program_evaluates_division_and_modulo` -- `cargo test -p elephc-magician execute_program_evaluates_exponentiation` -- `cargo test -p elephc-magician execute_program_echoes_comma_list` -- `cargo test --test codegen_tests test_eval_output_fast_path_preserves_scalar_and_object_echo` -- `cargo test --test codegen_tests test_eval_scalar_add_executes_through_bridge` -- `./scripts/test-linux-x86_64.sh test_eval_output_fast_path_preserves_scalar_and_object_echo` -- `./scripts/test-linux-arm64.sh test_eval_output_fast_path_preserves_scalar_and_object_echo` -- `git diff --check` - -### Risks - -PHP scalar coercion is subtle. Every fast path needs either exact compatibility or an explicit fallback to the current generic path. - -## 10. Unboxed Scalar Fast Paths - -### Goal - -Avoid boxing and unboxing hot scalar values inside eval loops. - -### Motivation - -Compute-heavy eval programs are likely dominated by repeated scalar loads, arithmetic, comparisons, and stores. Keeping scalars in a compact internal representation can reduce allocation and bridge overhead. - -### Current State - -Done. - -The implementation adds `crates/elephc-magician/src/interpreter/unboxed.rs`, -which evaluates small pure int/bool expression trees into temporary -`EvalUnboxedScalar` values. The fast path is wired only at explicit boundaries: - -- `StoreVar` boxes once before writing the scope cell. -- `return expr` boxes once before returning. -- `if`, `while`, `do/while`, and `for` conditions use unboxed truthiness when - the condition is a pure supported scalar expression. - -Scope-visible values remain runtime cells. The unboxed evaluator reads only -visible int/bool scope cells and handles checked integer add/sub/mul/mod, -integer comparisons, same-kind equality, logical not/and/or/xor, and integer -unary plus/minus. Overflow, division/modulo-by-zero, strings, floats, arrays, -objects, calls, refs, and other PHP-coercion-sensitive cases return `None` and -fall back to the existing runtime hooks. - -Benchmark smoke after the change: - -```text -python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case arithmetic_loop --json /tmp/elephc-magician-arithmetic-bench.json --markdown /tmp/elephc-magician-arithmetic-bench.md -arithmetic_loop | 4000 evals | elephc native 296.28 ms | elephc eval 396.30 ms | eval/native 1.34x -``` - -### Likely Files - -- `crates/elephc-magician/src/value.rs` -- `crates/elephc-magician/src/interpreter/expressions.rs` -- `crates/elephc-magician/src/interpreter/statements.rs` -- `crates/elephc-magician/src/interpreter/scope_cells.rs` -- `crates/elephc-magician/src/runtime_hooks/` - -### Implementation Plan - -1. Introduce an interpreter-local value enum for hot temporaries, not persistent scope cells. -2. Keep scope-visible values as runtime cells unless ownership semantics are fully modeled. -3. Add unboxed paths for integer and boolean first. -4. Add float after edge cases are verified against PHP. -5. Add string only for immutable literals or clearly owned strings. -6. Box only when a value escapes to scope, output, by-ref parameters, arrays, objects, or runtime hooks. - -### Validation - -Validation run after implementation: - -- `cargo test -p elephc-magician execute_program_unboxes_integer_store_expression_until_assignment` -- `cargo test -p elephc-magician execute_program_unboxed_integer_store_falls_back_for_modulo_by_zero` -- `cargo test -p elephc-magician execute_program_evaluates_compound_assignments` -- `cargo test -p elephc-magician execute_program_evaluates_division_and_modulo` -- `cargo test --test codegen_tests test_eval_unboxed_integer_store_expression_executes_through_bridge` -- `cargo test --test codegen_tests test_eval_division_modulo_execute_through_bridge` -- `cargo test --test codegen_tests test_eval_for_loop_uses_less_than_condition` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case arithmetic_loop --json /tmp/elephc-magician-arithmetic-bench.json --markdown /tmp/elephc-magician-arithmetic-bench.md` -- `git diff --check` - -### Risks - -The danger is splitting magician into two incompatible value systems. The unboxed layer should be a temporary execution optimization with explicit boxing boundaries. - -## 11. Compact Bytecode Or Linear EvalIR Form - -### Goal - -Reduce tree-walk and branch-heavy interpreter dispatch overhead. - -### Motivation - -The current EvalIR is a structured tree. A compact linear representation can improve cache locality and simplify dispatch, especially for loops. - -### Likely Files - -- `crates/elephc-magician/src/eval_ir.rs` -- New module such as `crates/elephc-magician/src/eval_bytecode.rs` -- `crates/elephc-magician/src/interpreter/` -- `crates/elephc-magician/src/parser/` - -### Implementation Plan - -1. Do not replace EvalIR immediately. -2. Add an optional lowering step from `EvalProgram` to a compact executable form. -3. Cache the lowered executable form alongside or inside the parse cache. -4. Start with expression-heavy straight-line code. -5. Add loops and control flow after linear basic blocks are proven. -6. Keep declarations and complex OOP constructs on the existing EvalIR path until needed. - -### Current State - -Done. - -The implementation adds `crates/elephc-magician/src/eval_linear.rs` and stores -`CachedEvalFragment { program, linear }` entries in the parse cache. Linear -lowering is intentionally conservative and currently accepts straight-line -fragments made of assignment, echo, print, expression statements, and return -over constants, variable loads, unary operators, and non-short-circuit binary -operators. - -The interpreter executes the lowered form through a small stack machine in -`crates/elephc-magician/src/interpreter/statements.rs`. Int/bool stack values -stay unboxed where the point-10 scalar rules apply, and values are boxed at -store, echo, pop, and return boundaries. Unsupported fragments continue through -the existing EvalIR interpreter. - -### Validation - -Run parity tests with both execution engines if a temporary dual path exists: - -- Existing magician interpreter unit tests. -- Focused codegen eval tests. -- PHP cross-checks for edge cases. - -Validation run after implementation: - -- `cargo test -p elephc-magician linear_lowering` -- `cargo test -p elephc-magician execute_prepared_program_uses_linear_unboxed_scalar_path` -- `cargo test --test codegen_tests test_eval_linear_cached_fragment_and_evalir_fallback_execute_through_bridge` -- `cargo test --test codegen_tests test_eval_unboxed_integer_store_expression_executes_through_bridge` -- `cargo test --test codegen_tests test_eval_for_loop_uses_less_than_condition` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case repeated_small_eval --json /tmp/elephc-magician-linear-bench.json --markdown /tmp/elephc-magician-linear-bench.md` - -Benchmark smoke result: - -- `repeated_small_eval`: 5000 evals, elephc native `329.19 ms`, elephc eval `378.11 ms`, eval/native `1.15x`. - -### Risks - -This is a larger architectural change. It should not happen before benchmark data proves tree dispatch is a real bottleneck after parse caching and scalar fast paths. - -## 12. Array, Reference, And COW Bridge Optimizations - -### Goal - -Reduce overhead for array mutation, by-reference parameters, and copy-on-write behavior. - -### Motivation - -Array and reference-heavy eval code can be expensive because correctness requires preserving PHP aliasing, reference cells, and COW rules. - -### Likely Files - -- `crates/elephc-magician/src/interpreter/array_literals.rs` -- `crates/elephc-magician/src/interpreter/scope_cells.rs` -- `crates/elephc-magician/src/interpreter/statements.rs` -- `crates/elephc-magician/src/runtime_hooks/` -- `crates/elephc-magician/src/ffi/` - -### Implementation Plan - -1. Benchmark array mutation and by-ref workloads separately. -2. Identify repeated helper patterns that can be fused safely. -3. Optimize append/set/get paths before broad COW changes. -4. Keep reference binding and mutation behavior covered by regression tests. -5. Add cleanup tests for normal return, fatal, and throwable paths. - -### Current State - -Done for the low-risk indexed-array write subset. - -The implementation adds `RuntimeValueOps::array_set_int_index()` with a -production bridge wrapper, `__elephc_eval_value_array_set_int_index`, emitted -for both ARM64 and x86_64. Eval scope-variable writes now use this hook when: - -- `$array[] = value` targets an indexed array or a newly created array. The - current length is read before evaluating the RHS, preserving the existing - source-order behavior. -- `$array[$i] = value` has a pure unboxed integer index. - -Associative-array append still uses `eval_array_append_key()` so PHP's -largest-integer-key behavior is preserved. String keys, side-effecting index -expressions, object `ArrayAccess`, by-reference writeback, property/static -array writes, and broad COW/reference handling continue through the existing -generic paths. - -### Validation - -Add tests for: - -- Array append and indexed set. -- Associative key set. -- By-ref function and method parameters. -- Ref-like builtin parameters. -- Aliasing across eval and native code. -- Cleanup after fatal and uncaught throwable. - -Validation run after implementation: - -- `cargo test -p elephc-magician execute_program_appends_indexed_scope_array_with_direct_int_key_write` -- `cargo test -p elephc-magician execute_program_sets_scope_array_integer_indexes_with_direct_int_key_write` -- `cargo test -p elephc-magician execute_program_appends_assoc_scope_array` -- `cargo test --test codegen_tests test_eval_indexed_array_append_is_visible_after_eval` -- `cargo test --test codegen_tests test_eval_indexed_array_write_is_visible_after_eval` -- `cargo test --test codegen_tests test_eval_assoc_array_append_uses_php_next_key` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case array_reads_writes --json /tmp/elephc-magician-array-bench.json --markdown /tmp/elephc-magician-array-bench.md` - -Benchmark smoke result: - -- `array_reads_writes`: 1 eval, elephc native `348.16 ms`, elephc eval `403.57 ms`, eval/native `1.16x`. - -### Risks - -This area has high semantic risk. Incorrect optimization can create stale aliases, missed mutations, double frees, or leaks. - -## 13. AOT For Literal `eval` - -### Goal - -Compile literal eval fragments ahead of time so `eval('...')` can bypass the magician interpreter where possible. - -### Current State - -Done for the conservative eligibility slice implemented in this plan. - -Literal eval calls still lower to the EIR opcode `EvalLiteralCall`, but the -backend now parses the literal fragment at compile time and directly emits -native code for eligible fragments. The current AOT subset accepts scalar -constants, simple scalar arithmetic, scalar concat, scalar `echo` / `print`, -scalar `return`, scalar variable stores, and boxed Mixed scope reads used by -read-modify-write expressions. Unsupported fragments keep the bridge fallback -to `__elephc_eval_execute`. - -### Motivation - -This is the largest potential speedup for literal eval because the code can become normal native elephc code instead of runtime-parsed and interpreted code. - -### Likely Files - -- `src/ir/` -- `src/ir_lower/expr/` -- `src/codegen_ir/lower_inst/` -- `src/types/` -- `src/name_resolver/` -- `src/resolver/` -- `src/optimize/` -- `crates/elephc-magician/` only for fallback and compatibility. - -### Implementation Plan - -1. Keep the existing fallback bridge as the compatibility path. Done. -2. Parse literal fragments at compile time. Done for the scalar AOT subset. -3. Run the same frontend passes needed for normal PHP code where possible. - Deferred for broader eligibility; the current subset accepts only syntax - that can be evaluated conservatively without cross-pass rewrites. -4. Reject or fall back for constructs that require runtime-only context. Done. -5. Lower eligible literal eval fragments into native codegen. Done for the - conservative scalar and scope read/write subset through the `EvalLiteralCall` - backend path. -6. Materialize eval-visible scope reads/writes through the same dynamic-scope - bridge used by runtime eval. Done for scalar variable stores and boxed Mixed - scope reads used by supported read-modify-write expressions. -7. Preserve eval return semantics: `return` exits eval, not the caller function. - Done for scalar returns. -8. Preserve declaration side effects for functions/classes declared by eval. - Done by falling back for declarations. -9. Add diagnostics or assembly markers showing whether a literal eval used AOT - or fallback. Done. -10. Expand eligibility gradually. Scope read/read-modify-write expressions are - now included; builtins, declarations, includes, OOP, control flow, and full - frontend-pass lowering remain on the compatibility fallback. - -### Validation - -Add parity tests for: - -- Literal eval assigning existing variables. Covered by scalar store and - existing native visibility tests. -- Literal eval creating variables. Covered. -- Literal eval returning values. Covered. -- Literal eval with output. Covered. -- Literal eval declarations. Covered by fallback behavior; direct declaration - AOT remains unsupported. -- Literal eval using builtins. Covered by fallback behavior; direct builtin AOT - remains unsupported. -- Literal eval inside functions and methods. Existing bridge tests cover the - fallback path; direct scalar AOT uses the same function-local eval scope. -- Fallback when unsupported syntax is present. Covered. -- No magician link for programs without eval. Covered. - -Validation run after implementation: - -- `cargo test --test codegen_tests test_literal_eval_scalar_return_uses_aot_without_execute_bridge` -- `cargo test --test codegen_tests test_literal_eval_scalar_store_uses_aot_scope_write` -- `cargo test --test codegen_tests test_literal_eval_scope_read_write_uses_aot_without_execute_bridge` -- `cargo test --test codegen_tests test_eval_codegen_requires_eval_bridge` -- `cargo test --test codegen_tests test_dynamic_eval_does_not_emit_literal_aot_marker` -- `cargo test --test codegen_tests test_eval_return_value_is_available_to_native_code` -- `cargo test --test codegen_tests test_eval_created_variable_is_visible_after_eval` -- `cargo test --test codegen_tests test_eval_scope_persists_between_eval_calls` -- `cargo test --test codegen_tests test_eval_can_change_existing_local_type` -- `cargo test --test codegen_tests test_eval_scalar_add_executes_through_aot` -- `cargo test --test codegen_tests test_eval_scalar_echo_executes_through_aot` -- `cargo test --test codegen_tests test_non_eval_program_does_not_request_eval_bridge` -- `cargo test --test codegen_tests test_eval_reads_and_writes_existing_local` -- `cargo test --test codegen_tests test_eval_return_and_scope_write_are_visible` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case literal_scalar_aot --json /tmp/elephc-magician-literal-aot-bench.json --markdown /tmp/elephc-magician-literal-aot-bench.md` -- `python3 scripts/benchmark_magician.py --iterations 1 --warmup 0 --case literal_scope_read_write_aot --json /tmp/elephc-magician-literal-scope-aot-bench.json --markdown /tmp/elephc-magician-literal-scope-aot-bench.md` - -Focused benchmark evidence: - -- `literal_scalar_aot`: 5000 literal scalar eval returns, eval `408.72 ms` - versus native `436.25 ms` (`0.94x`). -- `literal_scope_read_write_aot`: 5000 literal eval scope read/write - invocations, eval `365.63 ms` versus native `316.49 ms` (`1.16x`). - -### Risks - -AOT eval crosses the static/dynamic boundary. The main risk is accidentally treating eval code as ordinary static code and losing PHP eval semantics for scope, declarations, magic constants, or returns. - -## Recommended Milestone Order - -1. Keep the completed parse cache as the first performance improvement. -2. Add benchmark coverage before changing interpreter internals. -3. Implement symbol lookup, builtin dispatch, and callable caches while they can still be verified as mostly semantic-preserving optimizations. -4. Use benchmark data to decide between reducing `RuntimeValueOps`, unboxed scalars, or compact bytecode first. -5. Treat array/reference/COW improvements as a separate correctness-heavy milestone. -6. Continue AOT literal eval as the strategic long-term path, using the bridge fallback for all unsupported cases. From a56652da1dafe6971afe95ff28df812a1978260e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 20:20:55 +0200 Subject: [PATCH 1055/1208] fix: remove warning-only dead code --- src/codegen_support/reflection.rs | 28 ------------- src/optimize/effects/builtins.rs | 1 - src/types/checker/builtins/numeric.rs | 15 ------- src/types/checker/inference/objects/access.rs | 39 ------------------- .../checker/inference/objects/constructors.rs | 1 - src/types/schema.rs | 5 --- 6 files changed, 89 deletions(-) diff --git a/src/codegen_support/reflection.rs b/src/codegen_support/reflection.rs index 3551e882d0..9f1378681d 100644 --- a/src/codegen_support/reflection.rs +++ b/src/codegen_support/reflection.rs @@ -107,17 +107,6 @@ pub(crate) fn collect_attribute_factories_with_extra( .collect() } -/// Returns the factory id for the given attribute `attr_name` with -/// `attr_args`. Returns 0 if the class cannot be resolved or no matching -/// factory exists. -pub(crate) fn attribute_factory_id( - classes: &HashMap, - attr_name: &str, - attr_args: &[AttrArgEntry], -) -> i64 { - attribute_factory_id_with_extra(classes, &[], attr_name, attr_args) -} - /// Returns the factory id for an attribute, considering classes plus extra /// metadata sources such as top-level function attributes retained by EIR. pub(crate) fn attribute_factory_id_with_extra( @@ -139,11 +128,6 @@ pub(crate) fn attribute_factory_id_with_extra( .unwrap_or(0) } -/// Builds the synthetic dispatch body for `ReflectionAttribute::newInstance()`. -pub(crate) fn build_attribute_new_instance_body(classes: &HashMap) -> Vec { - build_attribute_new_instance_body_with_extra(classes, &[]) -} - /// Builds the synthetic `ReflectionAttribute::newInstance()` body using class /// metadata plus additional attribute metadata sources. pub(crate) fn build_attribute_new_instance_body_with_extra( @@ -292,18 +276,6 @@ fn attr_key_expr(key: &AttrKey) -> Expr { Expr::new(kind, span) } -/// Builds the synthetic body for `ReflectionAttribute::getArguments()`. For -/// each attribute whose class resolves, it dispatches on the factory id and -/// returns the captured arguments as a lowered array literal — so named -/// arguments and associative arrays are materialized through the normal array -/// path. Attributes without a resolvable class fall back to the `$__args` -/// property populated at construction. -pub(crate) fn build_attribute_get_arguments_body( - classes: &HashMap, -) -> Vec { - build_attribute_get_arguments_body_with_extra(classes, &[]) -} - /// Builds the synthetic `ReflectionAttribute::getArguments()` body using class /// metadata plus additional attribute metadata sources. pub(crate) fn build_attribute_get_arguments_body_with_extra( diff --git a/src/optimize/effects/builtins.rs b/src/optimize/effects/builtins.rs index 38ab9ef91a..b3cae89802 100644 --- a/src/optimize/effects/builtins.rs +++ b/src/optimize/effects/builtins.rs @@ -50,7 +50,6 @@ pub(in crate::optimize) fn is_pure_non_throwing_builtin(name: &str) -> bool { | "is_long" | "is_null" | "is_numeric" - | "is_object" | "is_real" | "is_string" | "is_resource" diff --git a/src/types/checker/builtins/numeric.rs b/src/types/checker/builtins/numeric.rs index cb70cfac8f..5bf8f2cfbb 100644 --- a/src/types/checker/builtins/numeric.rs +++ b/src/types/checker/builtins/numeric.rs @@ -236,18 +236,3 @@ fn unset_property_probe_is_valid_on_class( } Ok(true) } - -/// Returns the most precise supported result type for `abs($value)`. -fn abs_result_type(ty: &PhpType) -> PhpType { - match ty { - PhpType::Float => PhpType::Float, - PhpType::Mixed => PhpType::Mixed, - PhpType::Union(members) if members.iter().any(|member| *member == PhpType::Float) => { - PhpType::Mixed - } - PhpType::Union(members) if members.iter().any(|member| *member == PhpType::Mixed) => { - PhpType::Mixed - } - _ => PhpType::Int, - } -} diff --git a/src/types/checker/inference/objects/access.rs b/src/types/checker/inference/objects/access.rs index 0db858d2a3..a6c0dae2f2 100644 --- a/src/types/checker/inference/objects/access.rs +++ b/src/types/checker/inference/objects/access.rs @@ -112,45 +112,6 @@ impl Checker { )) } - /// Returns the class whose `magic` method (`__isset` or `__unset`) should - /// handle `isset($obj->prop)` / `unset($obj->prop)`: that is, when `$obj` is - /// an object whose class declares `magic` and `prop` is not normally - /// accessible. Infers (and type-checks) the receiver object as a side effect, - /// so callers can skip inferring the bare property access — which would - /// otherwise reject the property before the magic call is reached. - pub(crate) fn isset_unset_property_magic_class( - &mut self, - arg: &Expr, - magic: &str, - env: &TypeEnv, - ) -> Result, CompileError> { - let (object, property) = match &arg.kind { - ExprKind::PropertyAccess { object, property } - | ExprKind::NullsafePropertyAccess { object, property } => (object.as_ref(), property), - _ => return Ok(None), - }; - let PhpType::Object(class_name) = self.infer_type(object, env)? else { - return Ok(None); - }; - let normalized = class_name.trim_start_matches('\\').to_string(); - let Some(class_info) = self.classes.get(&normalized) else { - return Ok(None); - }; - if let Some(visibility) = class_info.property_visibilities.get(property) { - let declaring_class = class_info - .property_declaring_classes - .get(property) - .map(String::as_str) - .unwrap_or(normalized.as_str()); - if self.can_access_member(declaring_class, visibility) { - return Ok(None); - } - } else if class_info.visible_property(property).is_some() { - return Ok(None); - } - Ok(class_info.methods.contains_key(magic).then_some(normalized)) - } - /// Infers the type of a nullsafe property access expression (`$obj?->prop`). /// /// For `Mixed` receivers returns `Mixed`. For valid nullable object unions, diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 691b34de62..011227bf15 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -1263,7 +1263,6 @@ fn is_reflection_owner_class(class_name: &str) -> bool { | "ReflectionFunction" | "ReflectionMethod" | "ReflectionProperty" - | "ReflectionFunction" | "ReflectionParameter" | "ReflectionClassConstant" | "ReflectionEnumUnitCase" diff --git a/src/types/schema.rs b/src/types/schema.rs index f10a018192..eb12de78d2 100644 --- a/src/types/schema.rs +++ b/src/types/schema.rs @@ -404,11 +404,6 @@ impl ClassInfo { .unwrap_or_else(|| self.reference_properties.contains(property)) } - /// Returns whether the property visible by name stores a by-reference cell. - pub fn visible_property_is_reference(&self, property: &str) -> bool { - self.visible_property(property) - .is_some_and(|(index, (name, _))| self.property_slot_is_reference(index, name)) - } } /// Converts a property offset into a `properties` vector index when it points From 04aadbf59fd42e4f6bcb73d78f20c3a7b9f3e287 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 20:33:20 +0200 Subject: [PATCH 1056/1208] docs: add builtin docs update skill --- .claude/skills/update-builtin-docs/SKILL.md | 49 +++ .../update-builtin-docs/agents/openai.yaml | 4 + AGENTS.md | 9 +- docs/internals/builtins/array/count.md | 2 +- .../builtins/class/class_attribute_args.md | 4 +- .../builtins/class/class_attribute_names.md | 2 +- docs/internals/builtins/class/class_exists.md | 4 +- .../builtins/class/class_get_attributes.md | 2 +- docs/internals/builtins/class/enum_exists.md | 4 +- .../builtins/class/function_exists.md | 2 +- .../builtins/class/get_declared_classes.md | 2 +- .../builtins/class/get_declared_interfaces.md | 2 +- .../builtins/class/get_declared_traits.md | 2 +- .../builtins/class/interface_exists.md | 4 +- docs/internals/builtins/class/trait_exists.md | 4 +- docs/internals/builtins/misc/define.md | 2 +- docs/internals/builtins/misc/defined.md | 2 +- docs/internals/builtins/misc/empty.md | 2 +- docs/internals/builtins/misc/phpversion.md | 2 +- docs/internals/builtins/string/strlen.md | 4 +- docs/internals/builtins/type/boolval.md | 7 +- docs/internals/builtins/type/floatval.md | 5 +- .../builtins/type/get_resource_id.md | 2 +- .../builtins/type/get_resource_type.md | 2 +- docs/internals/builtins/type/gettype.md | 2 +- docs/internals/builtins/type/intval.md | 4 +- docs/internals/builtins/type/is_array.md | 4 +- docs/internals/builtins/type/is_bool.md | 4 +- docs/internals/builtins/type/is_callable.md | 3 +- docs/internals/builtins/type/is_float.md | 4 +- docs/internals/builtins/type/is_int.md | 4 +- docs/internals/builtins/type/is_iterable.md | 4 +- docs/internals/builtins/type/is_null.md | 4 +- docs/internals/builtins/type/is_object.md | 4 +- docs/internals/builtins/type/is_resource.md | 2 +- docs/internals/builtins/type/is_scalar.md | 4 +- docs/internals/builtins/type/is_string.md | 4 +- scripts/docs/builtin_registry.json | 354 +++++++----------- 38 files changed, 255 insertions(+), 270 deletions(-) create mode 100644 .claude/skills/update-builtin-docs/SKILL.md create mode 100644 .claude/skills/update-builtin-docs/agents/openai.yaml diff --git a/.claude/skills/update-builtin-docs/SKILL.md b/.claude/skills/update-builtin-docs/SKILL.md new file mode 100644 index 0000000000..7f704f150d --- /dev/null +++ b/.claude/skills/update-builtin-docs/SKILL.md @@ -0,0 +1,49 @@ +--- +name: update-builtin-docs +description: Regenerate and audit Elephc's generated builtin documentation from the builtin! registry. Use when a change touches src/builtins, builtin signatures, builtin lowering hooks, docs/php/builtins, docs/internals/builtins, scripts/docs/builtin_registry.json, or before opening a PR that changes PHP builtins. +--- + +# Update Builtin Docs + +Run the same generated-docs workflow enforced by the `builtins-docs-sync` CI job. +Use the repo root as the working directory. + +## Workflow + +1. Build the exporter binary that reads the single-source `builtin!` registry: + +```bash +cargo build --bin gen_builtins +``` + +2. Regenerate the JSON registry and Markdown pages: + +```bash +python3 scripts/docs/extract_builtins.py --render --force +``` + +3. Run the docs audits used by CI: + +```bash +python3 scripts/docs/audit_builtins.py +python3 scripts/docs/elephc_builtins/validate_site_compat.py +``` + +4. Inspect generated changes before reporting or committing: + +```bash +git status --short -- docs/php/builtins.md docs/php/builtins docs/internals/builtins scripts/docs/builtin_registry.json +git diff --check +``` + +## Rules + +- Treat `src/builtins/` and the `builtin!` declarations as the source of truth. +- Do not hand-edit generated builtin pages to fix drift; fix the registry, lowering metadata, or `scripts/docs/elephc_builtins/` generator inputs, then rerun the workflow. +- If the user asked only for a sync check, also run: + +```bash +git diff --exit-code -- docs/php/builtins.md docs/php/builtins docs/internals/builtins scripts/docs/builtin_registry.json +``` + +- If generated files changed, include those files in the same PR as the builtin change unless the user explicitly wants a separate docs-only follow-up. diff --git a/.claude/skills/update-builtin-docs/agents/openai.yaml b/.claude/skills/update-builtin-docs/agents/openai.yaml new file mode 100644 index 0000000000..1b2e286607 --- /dev/null +++ b/.claude/skills/update-builtin-docs/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Update Builtin Docs" + short_description: "Regenerate generated builtin docs" + default_prompt: "Update the generated builtin documentation and run the CI-equivalent audits." diff --git a/AGENTS.md b/AGENTS.md index 6245315fa5..f5c13718d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -254,6 +254,13 @@ Key invariants: checker-resident (`numeric`/`arrays` `check_builtin`), not in the registry. - Add codegen + error tests (include a case-insensitive or namespaced call for PHP-visible builtins); keep the parity gates in `src/builtins/parity_tests.rs` green. +- Before opening a PR that adds, removes, or changes PHP-visible builtins, run the + `update-builtin-docs` skill or the equivalent CI sequence: + `cargo build --bin gen_builtins`, + `python3 scripts/docs/extract_builtins.py --render --force`, + `python3 scripts/docs/audit_builtins.py`, and + `python3 scripts/docs/elephc_builtins/validate_site_compat.py`. Commit the + generated docs and registry. ### Adding a new EIR optimization pass @@ -502,7 +509,7 @@ sidebar: **Documentation must be kept up to date.** When adding a new feature: -1. **PHP syntax feature** (operator, built-in, statement, etc.) → update the relevant page in `docs/php/`. Add the function signature, parameters, return type, and a short example. +1. **PHP syntax feature** (operator, built-in, statement, etc.) → update the relevant page in `docs/php/`. Add the function signature, parameters, return type, and a short example. For builtins, prefer the generated-docs path: run the `update-builtin-docs` skill before opening a PR, or run the manual builtins docs sequence above. Commit updates under `docs/php/builtins*`, `docs/internals/builtins/`, and `scripts/docs/builtin_registry.json`. 2. **Compiler extension** (pointer, buffer, extern, ifdef) → update the relevant page in `docs/beyond-php/`. 3. **Compiler internals change** (pipeline, type checker, optimizer, codegen, runtime, ABI, memory model) → update the relevant page in `docs/internals/`. 4. **Compilation flow or CLI change** (new/changed flag, env var, pipeline phase, target, output mode) → update the relevant page in `docs/compiling/`, keeping `docs/compiling/cli-reference.md` authoritative and in sync with `src/cli.rs`. Mirror user-facing flag examples in `README.md`. diff --git a/docs/internals/builtins/array/count.md b/docs/internals/builtins/array/count.md index ff5bb8097d..0742c2bead 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/count.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:440](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L440) (`lower_count`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1024](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1024) (`lower_count`) - **Function symbol**: `lower_count()` diff --git a/docs/internals/builtins/class/class_attribute_args.md b/docs/internals/builtins/class/class_attribute_args.md index c94a5df71d..9cb2bc3e7c 100644 --- a/docs/internals/builtins/class/class_attribute_args.md +++ b/docs/internals/builtins/class/class_attribute_args.md @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_attribute_args.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:52](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L52) (`lower_class_attribute_args`) +- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:62](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L62) (`lower_class_attribute_args`) - **Function symbol**: `lower_class_attribute_args()` ### Lowering notes -- Lowers `class_attribute_args(class, attr)` into an indexed Mixed array. +- Lowers `class_attribute_args(class, attr)` into a Mixed PHP argument array. ## Runtime helpers diff --git a/docs/internals/builtins/class/class_attribute_names.md b/docs/internals/builtins/class/class_attribute_names.md index cf38873252..8502d2cfce 100644 --- a/docs/internals/builtins/class/class_attribute_names.md +++ b/docs/internals/builtins/class/class_attribute_names.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_attribute_names.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:36](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L36) (`lower_class_attribute_names`) +- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:46](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L46) (`lower_class_attribute_names`) - **Function symbol**: `lower_class_attribute_names()` diff --git a/docs/internals/builtins/class/class_exists.md b/docs/internals/builtins/class/class_exists.md index cc3671b2f7..0414fdd36a 100644 --- a/docs/internals/builtins/class/class_exists.md +++ b/docs/internals/builtins/class/class_exists.md @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/class_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` ### Lowering notes -- Lowers AOT class/interface/enum existence checks for literal names. +- Lowers AOT class/interface/enum existence checks for literal or dynamic string names. ## Runtime helpers diff --git a/docs/internals/builtins/class/class_get_attributes.md b/docs/internals/builtins/class/class_get_attributes.md index 7ae201dc05..6bf0e1384b 100644 --- a/docs/internals/builtins/class/class_get_attributes.md +++ b/docs/internals/builtins/class/class_get_attributes.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/class_get_attributes.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:68](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L68) (`lower_class_get_attributes`) +- **Lowering**: [`src/codegen/lower_inst/builtins/attributes.rs`:78](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/attributes.rs#L78) (`lower_class_get_attributes`) - **Function symbol**: `lower_class_get_attributes()` diff --git a/docs/internals/builtins/class/enum_exists.md b/docs/internals/builtins/class/enum_exists.md index 4d97bdcb0a..3ed3f27253 100644 --- a/docs/internals/builtins/class/enum_exists.md +++ b/docs/internals/builtins/class/enum_exists.md @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/enum_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` ### Lowering notes -- Lowers AOT class/interface/enum existence checks for literal names. +- Lowers AOT class/interface/enum existence checks for literal or dynamic string names. ## Runtime helpers diff --git a/docs/internals/builtins/class/function_exists.md b/docs/internals/builtins/class/function_exists.md index e72e15dfbb..88ebb31fad 100644 --- a/docs/internals/builtins/class/function_exists.md +++ b/docs/internals/builtins/class/function_exists.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/function_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:276](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L276) (`lower_function_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:566](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L566) (`lower_function_exists`) - **Function symbol**: `lower_function_exists()` diff --git a/docs/internals/builtins/class/get_declared_classes.md b/docs/internals/builtins/class/get_declared_classes.md index aae271d112..fc0d64a8c3 100644 --- a/docs/internals/builtins/class/get_declared_classes.md +++ b/docs/internals/builtins/class/get_declared_classes.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_classes.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:395](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L395) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` diff --git a/docs/internals/builtins/class/get_declared_interfaces.md b/docs/internals/builtins/class/get_declared_interfaces.md index 05df0d4739..e03bdfe839 100644 --- a/docs/internals/builtins/class/get_declared_interfaces.md +++ b/docs/internals/builtins/class/get_declared_interfaces.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_interfaces.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:395](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L395) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` diff --git a/docs/internals/builtins/class/get_declared_traits.md b/docs/internals/builtins/class/get_declared_traits.md index f9a034bfc1..1d2b0cae74 100644 --- a/docs/internals/builtins/class/get_declared_traits.md +++ b/docs/internals/builtins/class/get_declared_traits.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/get_declared_traits.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:388](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L388) (`lower_get_declared_names`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:395](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L395) (`lower_get_declared_names`) - **Function symbol**: `lower_get_declared_names()` diff --git a/docs/internals/builtins/class/interface_exists.md b/docs/internals/builtins/class/interface_exists.md index 11a13350ab..f6e1b21a3b 100644 --- a/docs/internals/builtins/class/interface_exists.md +++ b/docs/internals/builtins/class/interface_exists.md @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/interface_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` ### Lowering notes -- Lowers AOT class/interface/enum existence checks for literal names. +- Lowers AOT class/interface/enum existence checks for literal or dynamic string names. ## Runtime helpers diff --git a/docs/internals/builtins/class/trait_exists.md b/docs/internals/builtins/class/trait_exists.md index 93e606d30c..96e23075a3 100644 --- a/docs/internals/builtins/class/trait_exists.md +++ b/docs/internals/builtins/class/trait_exists.md @@ -10,13 +10,13 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/callables/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/callables/trait_exists.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:293](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L293) (`lower_class_like_exists`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:583](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L583) (`lower_class_like_exists`) - **Function symbol**: `lower_class_like_exists()` ### Lowering notes -- Lowers AOT class/interface/enum existence checks for literal names. +- Lowers AOT class/interface/enum existence checks for literal or dynamic string names. ## Runtime helpers diff --git a/docs/internals/builtins/misc/define.md b/docs/internals/builtins/misc/define.md index a3181ab954..0c0d382b39 100644 --- a/docs/internals/builtins/misc/define.md +++ b/docs/internals/builtins/misc/define.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/define.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/define.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:82](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L82) (`lower_define`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:372](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L372) (`lower_define`) - **Function symbol**: `lower_define()` diff --git a/docs/internals/builtins/misc/defined.md b/docs/internals/builtins/misc/defined.md index ba5a89d8ad..0fa8159e80 100644 --- a/docs/internals/builtins/misc/defined.md +++ b/docs/internals/builtins/misc/defined.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/defined.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:261](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L261) (`lower_defined`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:551](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L551) (`lower_defined`) - **Function symbol**: `lower_defined()` diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md index f14496d564..df66edec21 100644 --- a/docs/internals/builtins/misc/empty.md +++ b/docs/internals/builtins/misc/empty.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:619](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L619) (`lower_empty`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1217](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1217) (`lower_empty`) - **Function symbol**: `lower_empty()` diff --git a/docs/internals/builtins/misc/phpversion.md b/docs/internals/builtins/misc/phpversion.md index 0d0b423615..f01da4534a 100644 --- a/docs/internals/builtins/misc/phpversion.md +++ b/docs/internals/builtins/misc/phpversion.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/system/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/system/phpversion.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:251](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L251) (`lower_phpversion`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:541](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L541) (`lower_phpversion`) - **Function symbol**: `lower_phpversion()` diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index 34232da6d5..0997839a10 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen() — internals" description: "Compiler internals for strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 393 + order: 392 --- ## `strlen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strlen.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:494](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L494) (`lower_strlen`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1078](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1078) (`lower_strlen`) - **Function symbol**: `lower_strlen()` diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index 13f9260588..5dd2a60d39 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval() — internals" description: "Compiler internals for boolval(): lowering path, type checks, and runtime helpers." sidebar: - order: 410 + order: 409 --- ## `boolval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/boolval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:587](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L587) (`lower_boolval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1175](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1175) (`lower_boolval`) - **Function symbol**: `lower_boolval()` @@ -20,7 +20,8 @@ sidebar: ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_mixed_cast_bool` ## Signature summary diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index 70d1628b01..95c974ddc4 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval() — internals" description: "Compiler internals for floatval(): lowering path, type checks, and runtime helpers." sidebar: - order: 415 + order: 414 --- ## `floatval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/floatval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:557](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L557) (`lower_floatval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1141](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1141) (`lower_floatval`) - **Function symbol**: `lower_floatval()` @@ -21,6 +21,7 @@ sidebar: ## Runtime helpers The following runtime helpers are referenced: +- `__rt_mixed_cast_float` - `__rt_str_to_number` ## Signature summary diff --git a/docs/internals/builtins/type/get_resource_id.md b/docs/internals/builtins/type/get_resource_id.md index 7235132ff7..b6e4dae6f5 100644 --- a/docs/internals/builtins/type/get_resource_id.md +++ b/docs/internals/builtins/type/get_resource_id.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_id.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:424](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L424) (`lower_get_resource_id`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:431](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L431) (`lower_get_resource_id`) - **Function symbol**: `lower_get_resource_id()` diff --git a/docs/internals/builtins/type/get_resource_type.md b/docs/internals/builtins/type/get_resource_type.md index b76f7d6a3b..9188f37c23 100644 --- a/docs/internals/builtins/type/get_resource_type.md +++ b/docs/internals/builtins/type/get_resource_type.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/get_resource_type.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:412](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L412) (`lower_get_resource_type`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:419](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L419) (`lower_get_resource_type`) - **Function symbol**: `lower_get_resource_type()` diff --git a/docs/internals/builtins/type/gettype.md b/docs/internals/builtins/type/gettype.md index e129b63f73..2d299af69d 100644 --- a/docs/internals/builtins/type/gettype.md +++ b/docs/internals/builtins/type/gettype.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/gettype.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:129](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L129) (`lower_gettype`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:419](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L419) (`lower_gettype`) - **Function symbol**: `lower_gettype()` diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index f092b4e091..dc2b310f0f 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval() — internals" description: "Compiler internals for intval(): lowering path, type checks, and runtime helpers." sidebar: - order: 419 + order: 418 --- ## `intval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/intval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:524](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L524) (`lower_intval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1108](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1108) (`lower_intval`) - **Function symbol**: `lower_intval()` diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index 01835551a7..078e9615ae 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array() — internals" description: "Compiler internals for is_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 420 + order: 419 --- ## `is_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_array.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1004](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1004) (`lower_is_array`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1601](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1601) (`lower_is_array`) - **Function symbol**: `lower_is_array()` diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index 625121835e..f3e0deb682 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool() — internals" description: "Compiler internals for is_bool(): lowering path, type checks, and runtime helpers." sidebar: - order: 421 + order: 420 --- ## `is_bool()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_bool.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:737](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L737) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1335](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1335) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_callable.md b/docs/internals/builtins/type/is_callable.md index cb13a9c333..d5454cb286 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_callable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:319](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L319) (`lower_is_callable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:712](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L712) (`lower_is_callable`) - **Function symbol**: `lower_is_callable()` @@ -23,7 +23,6 @@ sidebar: The following runtime helpers are referenced: - `__rt_is_callable_array` - `__rt_is_callable_assoc` -- `__rt_is_callable_object` ## Signature summary diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index e9c07a96bc..8b7e6f3485 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float() — internals" description: "Compiler internals for is_float(): lowering path, type checks, and runtime helpers." sidebar: - order: 423 + order: 422 --- ## `is_float()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_float.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:737](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L737) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1335](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1335) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index 6c7ab32a8e..a2a9ecfbc8 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int() — internals" description: "Compiler internals for is_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 424 + order: 423 --- ## `is_int()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_int.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:737](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L737) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1335](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1335) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index 611f2fa937..cd4aff2f91 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable() — internals" description: "Compiler internals for is_iterable(): lowering path, type checks, and runtime helpers." sidebar: - order: 425 + order: 424 --- ## `is_iterable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_iterable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:795](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L795) (`lower_is_iterable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1393](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1393) (`lower_is_iterable`) - **Function symbol**: `lower_is_iterable()` diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index c7a1b70df9..2a0b9611e0 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null() — internals" description: "Compiler internals for is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 426 + order: 425 --- ## `is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_null.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:994](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L994) (`lower_is_null_builtin`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1591](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1591) (`lower_is_null_builtin`) - **Function symbol**: `lower_is_null_builtin()` diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index 700c2004d2..11ab42720c 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object() — internals" description: "Compiler internals for is_object(): lowering path, type checks, and runtime helpers." sidebar: - order: 428 + order: 427 --- ## `is_object()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_object.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1019](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1019) (`lower_is_object`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1616](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1616) (`lower_is_object`) - **Function symbol**: `lower_is_object()` diff --git a/docs/internals/builtins/type/is_resource.md b/docs/internals/builtins/type/is_resource.md index 50ef78f7a6..0a2f85075f 100644 --- a/docs/internals/builtins/type/is_resource.md +++ b/docs/internals/builtins/type/is_resource.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_resource.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:400](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L400) (`lower_is_resource`) +- **Lowering**: [`src/codegen/lower_inst/builtins/types.rs`:407](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins/types.rs#L407) (`lower_is_resource`) - **Function symbol**: `lower_is_resource()` diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index e02f6771df..5bad78af8b 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar() — internals" description: "Compiler internals for is_scalar(): lowering path, type checks, and runtime helpers." sidebar: - order: 430 + order: 429 --- ## `is_scalar()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_scalar.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1035](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1035) (`lower_is_scalar`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1632](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1632) (`lower_is_scalar`) - **Function symbol**: `lower_is_scalar()` diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index c9906e7c32..f5058c3a97 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string() — internals" description: "Compiler internals for is_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 431 + order: 430 --- ## `is_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_string.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:737](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L737) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1335](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1335) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 11221f1026..c34e36e4be 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -934,7 +934,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/addslashes.rs", @@ -3172,7 +3173,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/base64_decode.rs", @@ -3211,7 +3213,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/base64_encode.rs", @@ -3294,7 +3297,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/bin2hex.rs", @@ -3328,11 +3332,13 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_boolval", - "codegen_line": 587, + "codegen_line": 1175, "notes": [ "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`." ], - "runtime_helpers": [], + "runtime_helpers": [ + "__rt_mixed_cast_bool" + ], "sig_arm": null, "sig_file": "src/builtins/types/boolval.rs", "sig_line": null @@ -3788,7 +3794,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", - "codegen_line": 130, + "codegen_line": 112, "notes": [ "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], @@ -3878,7 +3884,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_chr", - "codegen_line": 876, + "codegen_line": 858, "notes": [ "Lowers `chr()` by converting an integer code point into a one-byte string." ], @@ -4019,9 +4025,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", "codegen_function": "lower_class_attribute_args", - "codegen_line": 52, + "codegen_line": 62, "notes": [ - "Lowers `class_attribute_args(class, attr)` into an indexed Mixed array." + "Lowers `class_attribute_args(class, attr)` into a Mixed PHP argument array." ], "runtime_helpers": [], "sig_arm": null, @@ -4063,7 +4069,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", "codegen_function": "lower_class_attribute_names", - "codegen_line": 36, + "codegen_line": 46, "notes": [ "Lowers `class_attribute_names(class)` into an indexed string array." ], @@ -4100,9 +4106,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 583, "notes": [ - "Lowers AOT class/interface/enum existence checks for literal names." + "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], "runtime_helpers": [], "sig_arm": null, @@ -4144,7 +4150,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/attributes.rs", "codegen_function": "lower_class_get_attributes", - "codegen_line": 68, + "codegen_line": 78, "notes": [ "Lowers `class_get_attributes(class)` into an array of `ReflectionAttribute` objects." ], @@ -4524,7 +4530,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_count", - "codegen_line": 440, + "codegen_line": 1024, "notes": [ "Lowers `count(array)` for concrete array values by reading the runtime length header.", "Called from `crate::builtins::array::count` (the registry home) via a thin wrapper.", @@ -4574,7 +4580,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_crc32", - "codegen_line": 366, + "codegen_line": 348, "notes": [ "Lowers `crc32(string)` through the shared checksum runtime helper." ], @@ -4890,7 +4896,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_define", - "codegen_line": 82, + "codegen_line": 372, "notes": [ "Lowers `define(\"NAME\", value)` with the duplicate-name runtime guard." ], @@ -4934,7 +4940,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_defined", - "codegen_line": 261, + "codegen_line": 551, "notes": [ "Lowers `defined(\"NAME\")` for compile-time string constant names." ], @@ -5168,7 +5174,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_empty", - "codegen_line": 619, + "codegen_line": 1217, "notes": [ "Lowers `empty()` for concrete scalar and array-like operands." ], @@ -5207,9 +5213,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 583, "notes": [ - "Lowers AOT class/interface/enum existence checks for literal names." + "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], "runtime_helpers": [], "sig_arm": null, @@ -5360,7 +5366,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_explode", - "codegen_line": 169, + "codegen_line": 151, "notes": [ "Lowers `explode(delimiter, string)` into the shared string-array splitter helper." ], @@ -6291,11 +6297,12 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_floatval", - "codegen_line": 557, + "codegen_line": 1141, "notes": [ "Lowers `floatval()` for concrete scalar operands." ], "runtime_helpers": [ + "__rt_mixed_cast_float", "__rt_str_to_number" ], "sig_arm": null, @@ -7090,7 +7097,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_function_exists", - "codegen_line": 276, + "codegen_line": 566, "notes": [ "Lowers `function_exists(\"name\")` for compile-time string names.", "Recognizes user functions, externs, catalog builtins, and the date/time procedural aliases", @@ -7215,7 +7222,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 395, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7244,7 +7251,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 395, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7273,7 +7280,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_declared_names", - "codegen_line": 388, + "codegen_line": 395, "notes": [ "Lowers `get_declared_classes/interfaces/traits()` using the shared declaration registry." ], @@ -7339,7 +7346,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_resource_id", - "codegen_line": 424, + "codegen_line": 431, "notes": [ "Lowers `get_resource_id(resource)` by unboxing the native handle and making it one-based." ], @@ -7376,7 +7383,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_get_resource_type", - "codegen_line": 412, + "codegen_line": 419, "notes": [ "Lowers `get_resource_type(resource)` to elephc's current resource type label." ], @@ -7811,7 +7818,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_gettype", - "codegen_line": 129, + "codegen_line": 419, "notes": [ "Lowers `gettype(value)` for statically concrete PHP types." ], @@ -8014,7 +8021,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_grapheme_strrev", - "codegen_line": 106, + "codegen_line": 88, "notes": [ "Lowers `grapheme_strrev()` and boxes its `string|false` result as `Mixed`." ], @@ -8054,7 +8061,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzcompress", - "codegen_line": 420, + "codegen_line": 402, "notes": [ "Lowers `gzcompress(data, level?)` through inline zlib `compress2` calls." ], @@ -8098,7 +8105,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzdeflate", - "codegen_line": 436, + "codegen_line": 418, "notes": [ "Lowers `gzdeflate(data, level?)` through inline raw-DEFLATE zlib calls." ], @@ -8142,7 +8149,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzinflate", - "codegen_line": 454, + "codegen_line": 436, "notes": [ "Lowers `gzinflate(data, max_length?)` and boxes zlib failures as PHP false." ], @@ -8186,7 +8193,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzuncompress", - "codegen_line": 475, + "codegen_line": 457, "notes": [ "Lowers `gzuncompress(data, max_length?)` and boxes zlib failures as PHP false." ], @@ -8232,7 +8239,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash", - "codegen_line": 227, + "codegen_line": 209, "notes": [ "Lowers `hash(algo, data, binary?)` through the shared runtime digest dispatcher." ], @@ -8285,7 +8292,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_algos", - "codegen_line": 272, + "codegen_line": 254, "notes": [ "Lowers `hash_algos()` through the runtime algorithm-list builder." ], @@ -8317,7 +8324,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_copy", - "codegen_line": 351, + "codegen_line": 333, "notes": [ "Lowers `hash_copy(context)` through the incremental hash clone helper." ], @@ -8359,7 +8366,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_equals", - "codegen_line": 265, + "codegen_line": 247, "notes": [ "Lowers `hash_equals(known, user)` through the timing-safe runtime compare helper." ], @@ -8458,7 +8465,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_final", - "codegen_line": 320, + "codegen_line": 302, "notes": [ "Lowers `hash_final(context, binary?)` through the incremental hash finalizer." ], @@ -8504,7 +8511,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_hmac", - "codegen_line": 246, + "codegen_line": 228, "notes": [ "Lowers `hash_hmac(algo, data, key, binary?)` through the shared HMAC runtime dispatcher." ], @@ -8565,7 +8572,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_init", - "codegen_line": 284, + "codegen_line": 266, "notes": [ "Lowers `hash_init(algo)` and returns a boxed HashContext resource." ], @@ -8618,7 +8625,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_update", - "codegen_line": 295, + "codegen_line": 277, "notes": [ "Lowers `hash_update(context, data)` through the incremental hash runtime helper." ], @@ -8727,7 +8734,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/hex2bin.rs", @@ -8810,7 +8818,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/html_entity_decode.rs", @@ -8843,19 +8852,13 @@ "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", - "codegen_function": "lower_html_escape", - "codegen_line": 93, + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `htmlspecialchars()` / `htmlentities()` \u2014 escapes the subject string (operand 0).", - "`name` is the calling builtin's PHP name, used in argument-coercion diagnostics. The", - "optional `flags` and `encoding` arguments are accepted (so the common `htmlspecialchars($s,", - "ENT_QUOTES)` call form compiles) but not applied: `__rt_htmlspecialchars` implements the", - "ENT_QUOTES behaviour, which matches PHP's default flag set and the overwhelmingly-common", - "ENT_QUOTES call. (A flag-aware runtime \u2014 doctype-dependent `'` vs `'` \u2014 is a follow-up.)" + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ "__rt_grapheme_strrev", - "__rt_htmlspecialchars", "__rt_strcopy" ], "sig_arm": null, @@ -8871,20 +8874,6 @@ "name": "string", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": "11", - "name": "flags", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": "'UTF-8'", - "name": "encoding", - "optional": true, - "type": "string" } ], "return_type": "string", @@ -8903,19 +8892,13 @@ "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", - "codegen_function": "lower_html_escape", - "codegen_line": 93, + "codegen_function": "lower_unary_string_runtime", + "codegen_line": 76, "notes": [ - "Lowers `htmlspecialchars()` / `htmlentities()` \u2014 escapes the subject string (operand 0).", - "`name` is the calling builtin's PHP name, used in argument-coercion diagnostics. The", - "optional `flags` and `encoding` arguments are accepted (so the common `htmlspecialchars($s,", - "ENT_QUOTES)` call form compiles) but not applied: `__rt_htmlspecialchars` implements the", - "ENT_QUOTES behaviour, which matches PHP's default flag set and the overwhelmingly-common", - "ENT_QUOTES call. (A flag-aware runtime \u2014 doctype-dependent `'` vs `'` \u2014 is a follow-up.)" + "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ "__rt_grapheme_strrev", - "__rt_htmlspecialchars", "__rt_strcopy" ], "sig_arm": null, @@ -8931,20 +8914,6 @@ "name": "string", "optional": false, "type": "string" - }, - { - "by_ref": false, - "default": "11", - "name": "flags", - "optional": true, - "type": "int" - }, - { - "by_ref": false, - "default": "'UTF-8'", - "name": "encoding", - "optional": true, - "type": "string" } ], "return_type": "string", @@ -9051,7 +9020,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_implode", - "codegen_line": 210, + "codegen_line": 192, "notes": [ "Lowers `implode(glue, array)` by selecting the string or integer array helper." ], @@ -9146,7 +9115,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_inet", - "codegen_line": 515, + "codegen_line": 497, "notes": [ "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." ], @@ -9185,7 +9154,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_inet", - "codegen_line": 515, + "codegen_line": 497, "notes": [ "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." ], @@ -9268,9 +9237,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 583, "notes": [ - "Lowers AOT class/interface/enum existence checks for literal names." + "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], "runtime_helpers": [], "sig_arm": null, @@ -9312,7 +9281,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_intval", - "codegen_line": 524, + "codegen_line": 1108, "notes": [ "Lowers `intval()` for concrete scalar operands." ], @@ -9352,7 +9321,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ip2long", - "codegen_line": 506, + "codegen_line": 488, "notes": [ "Lowers `ip2long(string)` and boxes invalid-address results as PHP false." ], @@ -9443,7 +9412,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_array", - "codegen_line": 1004, + "codegen_line": 1601, "notes": [ "Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value", "whose runtime tag is an indexed (4) or associative (5) array. An `iterable`-typed value is", @@ -9482,7 +9451,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 737, + "codegen_line": 1335, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9519,14 +9488,13 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_callable", - "codegen_line": 319, + "codegen_line": 712, "notes": [ "Lowers `is_callable(value)` through static lookup or runtime callable-shape helpers." ], "runtime_helpers": [ "__rt_is_callable_array", - "__rt_is_callable_assoc", - "__rt_is_callable_object" + "__rt_is_callable_assoc" ], "sig_arm": null, "sig_file": "src/builtins/types/is_callable.rs", @@ -9721,7 +9689,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 737, + "codegen_line": 1335, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9795,7 +9763,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 737, + "codegen_line": 1335, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9832,7 +9800,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_iterable", - "codegen_line": 795, + "codegen_line": 1393, "notes": [ "Lowers `is_iterable()` for concrete values and boxed Mixed payloads." ], @@ -9948,7 +9916,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_null_builtin", - "codegen_line": 994, + "codegen_line": 1591, "notes": [ "Lowers `is_null()` for concrete scalar values and boxed Mixed payloads." ], @@ -10022,7 +9990,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_object", - "codegen_line": 1019, + "codegen_line": 1616, "notes": [ "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose", "runtime tag is an object (6)." @@ -10101,7 +10069,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/types.rs", "codegen_function": "lower_is_resource", - "codegen_line": 400, + "codegen_line": 407, "notes": [ "Lowers `is_resource(value)` for static resources and boxed Mixed resource cells." ], @@ -10138,7 +10106,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_scalar", - "codegen_line": 1035, + "codegen_line": 1632, "notes": [ "Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed", "Mixed/Union value whose runtime tag is int (0), string (1), float (2), or bool (3). Null,", @@ -10177,7 +10145,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 737, + "codegen_line": 1335, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -10826,7 +10794,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_lcfirst", - "codegen_line": 122, + "codegen_line": 104, "notes": [ "Lowers `lcfirst()` by copying the string and lowercasing the first ASCII byte." ], @@ -11218,7 +11186,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_long2ip", - "codegen_line": 494, + "codegen_line": 476, "notes": [ "Lowers `long2ip(value)` through the IPv4 formatting runtime helper." ], @@ -11298,7 +11266,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", - "codegen_line": 130, + "codegen_line": 112, "notes": [ "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], @@ -11368,61 +11336,6 @@ "slug": "max", "sub_area": "Math" }, - { - "area": "Regex", - "canonical_name": "mb_ereg_match", - "description": "Tests whether a regex pattern matches the beginning of a string (multibyte).", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", - "codegen_function": "lower_mb_ereg_match", - "codegen_line": 52, - "notes": [ - "Lowers `mb_ereg_match(pattern, subject, options = null)` as a start-anchored regex match.", - "The bare delimiter-less pattern and subject use the shared regex string loader. Optional", - "options are passed as a string pair when present, or as `(0, 0)` for `null`/omitted options." - ], - "runtime_helpers": [ - "__rt_mb_ereg_match" - ], - "sig_arm": null, - "sig_file": "src/builtins/string/mb_ereg_match.rs", - "sig_line": null - }, - "name": "mb_ereg_match", - "sig": { - "params": [ - { - "by_ref": false, - "default": null, - "name": "pattern", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": null, - "name": "subject", - "optional": false, - "type": "string" - }, - { - "by_ref": false, - "default": "null", - "name": "options", - "optional": true, - "type": "string" - } - ], - "return_type": "bool", - "variadic": null - }, - "slug": "mb_ereg_match", - "sub_area": "Regex" - }, { "area": "String", "canonical_name": "md5", @@ -11434,7 +11347,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_md5", - "codegen_line": 373, + "codegen_line": 355, "notes": [ "Lowers `md5(data, binary?)` through the shared crypto-backed runtime helper." ], @@ -11816,7 +11729,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/nl2br.rs", @@ -11850,7 +11764,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_number_format", - "codegen_line": 893, + "codegen_line": 875, "notes": [ "Lowers `number_format()` by arranging its runtime helper arguments." ], @@ -11951,7 +11865,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ord", - "codegen_line": 852, + "codegen_line": 834, "notes": [ "Lowers `ord()` by returning the first byte of a string or zero for empty input." ], @@ -12217,7 +12131,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_phpversion", - "codegen_line": 251, + "codegen_line": 541, "notes": [ "Lowers `phpversion()` as the compiler package version string." ], @@ -12371,6 +12285,7 @@ ], "runtime_helpers": [ "__rt_preg_match", + "__rt_preg_match_all", "__rt_preg_match_capture" ], "sig_arm": null, @@ -12419,7 +12334,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", "codegen_function": "lower_preg_match_all", - "codegen_line": 66, + "codegen_line": 49, "notes": [ "Lowers `preg_match_all(pattern, subject)` through the shared regex runtime helper." ], @@ -12466,7 +12381,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", "codegen_function": "lower_preg_replace", - "codegen_line": 79, + "codegen_line": 62, "notes": [ "Lowers `preg_replace(pattern, replacement, subject)` through the regex replacement helper." ], @@ -12519,7 +12434,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", "codegen_function": "lower_preg_replace_callback", - "codegen_line": 101, + "codegen_line": 84, "notes": [ "Lowers `preg_replace_callback(pattern, callback, subject)` through supported direct callbacks." ], @@ -12572,7 +12487,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", "codegen_function": "lower_preg_split", - "codegen_line": 405, + "codegen_line": 388, "notes": [ "Lowers `preg_split(pattern, subject, limit?, flags?)` through the regex split helper." ], @@ -12687,7 +12602,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_printf", - "codegen_line": 535, + "codegen_line": 517, "notes": [ "Lowers `printf(format, values...)` as `sprintf()` followed by stdout emission." ], @@ -13552,7 +13467,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/rawurldecode.rs", @@ -13591,7 +13507,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/rawurlencode.rs", @@ -14139,7 +14056,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", - "codegen_line": 130, + "codegen_line": 112, "notes": [ "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], @@ -14311,7 +14228,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_sha1", - "codegen_line": 378, + "codegen_line": 360, "notes": [ "Lowers `sha1(data, binary?)` through the shared crypto-backed runtime helper." ], @@ -14937,7 +14854,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_sprintf", - "codegen_line": 529, + "codegen_line": 511, "notes": [ "Lowers `sprintf(format, values...)` by packing variadic records for `__rt_sprintf`." ], @@ -15013,7 +14930,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_sscanf", - "codegen_line": 181, + "codegen_line": 163, "notes": [ "Lowers `sscanf(string, format)` into the shared scanner helper." ], @@ -15101,7 +15018,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_contains", - "codegen_line": 700, + "codegen_line": 682, "notes": [ "Lowers `str_contains()` through `strpos()` and converts found positions to bool." ], @@ -15147,7 +15064,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_binary_string_runtime", - "codegen_line": 157, + "codegen_line": 139, "notes": [ "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], @@ -15193,7 +15110,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_replace", - "codegen_line": 798, + "codegen_line": 780, "notes": [ "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], @@ -15251,7 +15168,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_pad", - "codegen_line": 836, + "codegen_line": 818, "notes": [ "Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper." ], @@ -15311,7 +15228,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_repeat", - "codegen_line": 764, + "codegen_line": 746, "notes": [ "Lowers `str_repeat(string, times)` through the shared runtime helper." ], @@ -15357,7 +15274,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_replace", - "codegen_line": 798, + "codegen_line": 780, "notes": [ "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], @@ -15415,7 +15332,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_split", - "codegen_line": 194, + "codegen_line": 176, "notes": [ "Lowers `str_split(string, length?)` into the fixed-width string-array splitter." ], @@ -15461,7 +15378,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_binary_string_runtime", - "codegen_line": 157, + "codegen_line": 139, "notes": [ "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], @@ -15507,7 +15424,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_binary_string_runtime", - "codegen_line": 157, + "codegen_line": 139, "notes": [ "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], @@ -15553,7 +15470,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_binary_string_runtime", - "codegen_line": 157, + "codegen_line": 139, "notes": [ "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], @@ -17589,7 +17506,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/stripslashes.rs", @@ -17623,7 +17541,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_strlen", - "codegen_line": 494, + "codegen_line": 1078, "notes": [ "Lowers `strlen()` by coercing string-like values and returning the byte length." ], @@ -17662,7 +17580,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_position", - "codegen_line": 718, + "codegen_line": 700, "notes": [ "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." ], @@ -17718,7 +17636,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/strrev.rs", @@ -17752,7 +17671,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_position", - "codegen_line": 718, + "codegen_line": 700, "notes": [ "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." ], @@ -17803,7 +17722,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_strstr", - "codegen_line": 780, + "codegen_line": 762, "notes": [ "Lowers `strstr(haystack, needle)` by searching and returning the matching suffix." ], @@ -17859,7 +17778,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/strtolower.rs", @@ -17950,7 +17870,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/strtoupper.rs", @@ -17984,7 +17905,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_substr", - "codegen_line": 731, + "codegen_line": 713, "notes": [ "Lowers `substr(string, offset, length?)` with target-local pointer arithmetic." ], @@ -18037,7 +17958,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_substr_replace", - "codegen_line": 748, + "codegen_line": 730, "notes": [ "Lowers `substr_replace(string, replacement, start, length?)`." ], @@ -18454,9 +18375,9 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_class_like_exists", - "codegen_line": 293, + "codegen_line": 583, "notes": [ - "Lowers AOT class/interface/enum existence checks for literal names." + "Lowers AOT class/interface/enum existence checks for literal or dynamic string names." ], "runtime_helpers": [], "sig_arm": null, @@ -18498,7 +18419,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", - "codegen_line": 130, + "codegen_line": 112, "notes": [ "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], @@ -18588,7 +18509,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ucfirst", - "codegen_line": 114, + "codegen_line": 96, "notes": [ "Lowers `ucfirst()` by copying the string and uppercasing the first ASCII byte." ], @@ -18632,7 +18553,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/ucwords.rs", @@ -18892,7 +18814,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/urldecode.rs", @@ -18931,7 +18854,8 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_htmlspecialchars" + "__rt_grapheme_strrev", + "__rt_strcopy" ], "sig_arm": null, "sig_file": "src/builtins/string/urlencode.rs", @@ -19142,7 +19066,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_vprintf", - "codegen_line": 548, + "codegen_line": 530, "notes": [ "Lowers `vprintf(format, values)` as `vsprintf()` followed by stdout emission." ], @@ -19188,7 +19112,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_vsprintf", - "codegen_line": 542, + "codegen_line": 524, "notes": [ "Lowers `vsprintf(format, values)` through the array-to-sprintf runtime bridge." ], @@ -19234,7 +19158,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_wordwrap", - "codegen_line": 820, + "codegen_line": 802, "notes": [ "Lowers `wordwrap(string, width?, break?, cut?)` through the shared runtime helper." ], From 5717c15829c735d180424d541b3645aba5fe4400 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 20:44:38 +0200 Subject: [PATCH 1057/1208] docs: keep plans in english --- .plans/elephc-eval-magician-plan.md | 496 +++++++++--------- .../elephc-magician-builtin-structure-plan.md | 221 ++++++++ .plans/phpstorm-plugin.md | 211 ++++---- CONTRIBUTING.md | 2 +- docs/internals/the-ir.md | 2 - 5 files changed, 592 insertions(+), 340 deletions(-) create mode 100644 .plans/elephc-magician-builtin-structure-plan.md diff --git a/.plans/elephc-eval-magician-plan.md b/.plans/elephc-eval-magician-plan.md index fbd306fb8b..54dee04e3f 100644 --- a/.plans/elephc-eval-magician-plan.md +++ b/.plans/elephc-eval-magician-plan.md @@ -1,88 +1,86 @@ -# Piano: eval, elephc-magician e literal eval AOT +# Plan: eval, elephc-magician, and Literal Eval AOT ## Task -- [x] Definire la semantica target di `eval`: scope caller visibile, scritture - persistenti, variabili create visibili dopo eval, `unset`, output, parse - error, `return` locale al frammento, dichiarazioni dinamiche e `$this`. -- [x] Aggiungere `crates/elephc-magician` come bridge opzionale e linkarlo solo - quando il programma richiede il fallback eval runtime. -- [x] Aggiungere ABI, `RuntimeFeatures`, linker bridge e runtime helper per - chiamare `__elephc_eval_execute` dal backend EIR corrente. -- [x] Implementare `ElephcEvalContext` e `ElephcEvalScope` condivisi tra codice - nativo e interpreter, inclusi flush/reload dei locals osservabili. -- [x] Implementare parser runtime, EvalIR/interpreter e value bridge per il - subset eval supportato da magician. -- [x] Supportare nel fallback magician variabili, assegnamenti, output, return, - control flow, arrays, include/require, chiamate dinamiche, dichiarazioni, - classi/oggetti, reflection, callable, references/by-ref e cleanup errori per - il subset coperto dai test. -- [x] Modellare `eval` come barriera di effetti per optimizer/type checker: - niente DCE, niente propagazione costanti attraverso locals osservabili e - fallback dinamico dove necessario. -- [x] Aggiungere benchmark magician ripetibili con varianti Elephc native, - Elephc eval, PHP native e PHP eval. -- [x] Aggiungere parse cache, parse-error cache e include parse/file cache senza - congelare context, scope, magic constants o include_once state. -- [x] Aggiungere cache per lookup simboli eval, direct builtin dispatch, - callable resolution cache e ottimizzazioni conservative su `RuntimeValueOps`. -- [x] Aggiungere fast path unboxed scalar, linear EvalIR/stack VM opzionale e - ottimizzazioni mirate per array/reference/COW nel bridge. -- [x] Implementare literal `eval` AOT conservativo per scalari, output, return, - store/scope read-write e marker assembly di AOT/fallback. -- [x] Estendere literal eval AOT a locals interni, `while`, `if`, `break`, - `continue`, confronti/truthiness, modulo e benchmark prime-sum fino a +- [x] Define the target semantics of `eval`: visible caller scope, persistent + writes, variables created inside eval visible after eval, `unset`, output, + parse errors, fragment-local `return`, dynamic declarations, and `$this`. +- [x] Add `crates/elephc-magician` as an optional bridge and link it only when a + program requires the runtime eval fallback. +- [x] Add the ABI, `RuntimeFeatures`, linker bridge, and runtime helpers needed + to call `__elephc_eval_execute` from the current EIR backend. +- [x] Implement `ElephcEvalContext` and `ElephcEvalScope` shared by native code + and the interpreter, including flush/reload of observable locals. +- [x] Implement runtime parsing, EvalIR/interpreter, and the value bridge for the + eval subset supported by magician. +- [x] Support variables, assignments, output, return, control flow, arrays, + include/require, dynamic calls, declarations, classes/objects, reflection, + callables, references/by-ref, and error cleanup in the magician fallback for + the subset covered by tests. +- [x] Model `eval` as an effect barrier for the optimizer/type checker: no DCE, + no constant propagation through observable locals, and dynamic fallback where + needed. +- [x] Add repeatable magician benchmarks with Elephc native, Elephc eval, PHP + native, and PHP eval variants. +- [x] Add parse cache, parse-error cache, and include parse/file cache without + freezing context, scope, magic constants, or include_once state. +- [x] Add caches for eval symbol lookup, direct builtin dispatch, callable + resolution, and conservative `RuntimeValueOps` optimizations. +- [x] Add an unboxed scalar fast path, optional linear EvalIR/stack VM, and + targeted array/reference/COW optimizations in the bridge. +- [x] Implement conservative literal `eval` AOT for scalars, output, return, + store/scope read-write, and AOT/fallback assembly markers. +- [x] Extend literal eval AOT to internal locals, `while`, `if`, `break`, + `continue`, comparisons/truthiness, modulo, and the prime-sum benchmark up to `100000`. -- [x] Estendere literal eval AOT a builtins statici comuni, funzioni statiche - note, metodi statici pubblici tipizzati e callback statici via - `call_user_func*()`. -- [x] Evitare il link a `elephc_magician` per programmi con soli eval literal +- [x] Extend literal eval AOT to common static builtins, known static functions, + typed public static methods, and static callbacks through `call_user_func*()`. +- [x] Avoid linking `elephc_magician` for programs whose literal eval calls are fully AOT. -- [x] Aggiornare i test parity per distinguere builtin condivisi, - builtin eval-only documentati e builtin static-only non ancora presenti in - magician. -- [ ] Ridurre il mini-codegen AOT manuale residuo e convergere verso funzioni - EIR interne per i frammenti literal supportati. -- [ ] Espandere AOT solo dove semanticamente coperto: arrays/iterables completi, +- [x] Update parity tests to distinguish shared builtins, documented eval-only + builtins, and static-only builtins not yet present in magician. +- [ ] Reduce the remaining manual AOT mini-codegen and converge on internal EIR + functions for supported literal fragments. +- [ ] Expand AOT only where semantics are covered. Full arrays/iterables, object/member access, references/by-ref, `global`, `static`, variable - variables, `try`/`throw`, include/require e dichiarazioni restano fallback - finche' non hanno modello e test dedicati. -- [ ] Chiudere o mantenere esplicitamente il gap dei builtin static-only: - implementarli in magician oppure tenerli in allowlist testata finche' eval non - li espone. -- [ ] Promuovere i benchmark di accettazione AOT piu' utili nella suite - benchmark permanente, senza includere compile/link time nei runtime numbers. -- [ ] Aggiornare docs utente/internals dopo ogni estensione semantica del - subset eval o AOT. -- [ ] Eseguire verifiche focused sui tre target supportati per ogni modifica che - tocca ABI, runtime ownership, codegen eval o fallback/AOT selection. - -## Scope del piano - -Questo piano sostituisce e fonde: + variables, `try`/`throw`, include/require, and declarations stay fallback + until they have a dedicated model and tests. +- [ ] Close or explicitly maintain the static-only builtin gap: implement them + in magician or keep them in a tested allowlist until eval exposes them. +- [ ] Promote the most useful AOT acceptance benchmarks into the permanent + benchmark suite without including compile/link time in runtime numbers. +- [ ] Update user/internal docs after every semantic extension of the eval or AOT + subset. +- [ ] Run focused checks on all three supported targets for every change that + touches ABI, runtime ownership, eval codegen, or fallback/AOT selection. + +## Plan Scope + +This plan replaces and merges: - `.plans/elephc-eval-complete-plan.md` - `.plans/elephc-eval-aot-complete-plan.md` - `.plans/elephc-magician-performance-plan.md` -Il piano resta in `.plans` per tracciare solo il lavoro eval/magician ancora -aperto. Le sezioni completate documentano lo stato raggiunto e servono come -guardrail per non reintrodurre vecchi approcci o regressioni. +This plan remains in `.plans` to track only the remaining eval/magician work. +All plans in `.plans` must be written in English. Completed sections document +the state already reached and act as guardrails against reintroducing old +approaches or regressions. -## Stato corrente +## Current State -Il supporto eval esiste su due percorsi: +Eval support has two paths: -1. Fallback runtime tramite `libelephc-magician`, chiamato da +1. Runtime fallback through `libelephc-magician`, called by `__elephc_eval_execute`. -2. Literal eval AOT, quando il frammento e' una stringa nota a compile time e il - classificatore lo considera semanticamente sicuro. +2. Literal eval AOT, when the fragment is a compile-time-known string and the + classifier considers it semantically safe. -Il backend attivo dopo il rebase su `main` e' il percorso EIR sotto -`src/ir_lower/`, `src/ir_passes/` e `src/codegen/lower_inst/`. I riferimenti -storici a `src/codegen_ir/` nei vecchi piani sono obsoleti. +After the rebase onto `main`, the active backend is the EIR path under +`src/ir_lower/`, `src/ir_passes/`, and `src/codegen/lower_inst/`. Historical +references to `src/codegen_ir/` in older plans are obsolete. -I file centrali attuali sono: +Current central files: - `crates/elephc-magician/src/` - `src/eval_aot.rs` @@ -99,224 +97,224 @@ I file centrali attuali sono: - `tests/codegen/eval_reflection_invocation.rs` - `tests/builtin_parity_tests.rs` -## Architettura consolidata +## Consolidated Architecture -### Fallback magician +### Magician Fallback -`elephc-magician` e' una staticlib bridge opzionale. I programmi senza eval -runtime non devono linkarla. Il fallback resta obbligatorio per: +`elephc-magician` is an optional bridge staticlib. Programs without runtime eval +must not link it. The fallback remains mandatory for: -- eval dinamico; -- literal eval non parseabile o non supportato dal classificatore AOT; -- costrutti con semantica runtime non ancora modellata in AOT; -- dichiarazioni dinamiche, include/require, references/by-ref, global/static, - variable variables, oggetti/members dinamici e throwable finche' non coperti. +- dynamic eval; +- literal eval that cannot be parsed or is not supported by the AOT classifier; +- constructs whose runtime semantics are not yet modeled in AOT; +- dynamic declarations, include/require, references/by-ref, global/static, + variable variables, dynamic objects/members, and throwables until covered. -Il fallback riceve: +The fallback receives: -- context globale eval; -- scope locale eval; -- scope globale quando serve; -- puntatore/lunghezza del codice; -- buffer risultato. +- global eval context; +- local eval scope; +- global scope when needed; +- code pointer/length; +- result buffer. -Il value model non deve divergere da quello nativo: boxing, refcount, COW, -references e cleanup devono rimanere coerenti con il runtime elephc. +The value model must not diverge from native runtime behavior. Boxing, refcount, +COW, references, and cleanup must stay consistent with the elephc runtime. -### Scope sync +### Scope Sync -Il codice nativo deve sincronizzare con lo scope eval solo cio' che e' -osservabile dal frammento: +Native code must synchronize with eval scope only for values observable by the +fragment: -- prima della chiamata: flush delle variabili lette/scritte quando serve; -- durante eval: magician opera sullo scope condiviso; -- dopo eval: reload delle variabili che possono essere state scritte, create o - unsettate. +- before the call: flush variables read or written when needed; +- during eval: magician operates on the shared scope; +- after eval: reload variables that may have been written, created, or unset. -Quando l'analisi non e' precisa, la semantica vince sulla performance: usare il -fallback o trattare il frammento come barriera piu' forte. +When analysis is imprecise, semantics wins over performance: use the fallback or +treat the fragment as a stronger barrier. -### Literal eval AOT +### Literal Eval AOT -Il compilatore analizza il frammento literal a compile time: +The compiler analyzes literal fragments at compile time: ```text literal string - -> parse come PHP fragment - -> normalizzazione/nameresolution compatibile col contesto - -> classificazione eligibility AOT - -> piano read/write/call/fallback - -> lowering nativo o fallback magician + -> parse as PHP fragment + -> normalize/name-resolve compatibly with the context + -> classify AOT eligibility + -> plan reads/writes/calls/fallback + -> native lowering or magician fallback ``` -Il piano AOT deve preservare: +The AOT plan must preserve: -- `return expr;` ritorna da eval, non dal caller; -- fallthrough senza `return` produce `null`; -- output resta side effect visibile; -- variabili caller note a compile time sono leggibili/scrivibili; -- variabili create dal frammento sono visibili dopo eval se il path AOT lo - dichiara supportato; -- ogni costrutto non coperto rimane fallback esplicito. +- `return expr;` returns from eval, not from the caller; +- fallthrough without `return` produces `null`; +- output remains a visible side effect; +- caller variables known at compile time can be read and written; +- variables created by the fragment are visible after eval if that AOT path + declares creation support; +- every uncovered construct remains an explicit fallback. -I path AOT emettono marker assembly tipo `eval literal AOT compiled...`; i -fallback emettono marker con ragione leggibile quando possibile. +AOT paths emit assembly markers such as `eval literal AOT compiled...`. +Fallback paths emit markers with a readable reason where possible. -## Lavoro completato +## Completed Work -### Eval runtime e bridge +### Eval Runtime and Bridge -Completato: +Completed: -- crate `elephc-magician`; -- ABI C/Rust verso `__elephc_eval_execute`; -- bridge linker `elephc_magician`; -- detection runtime features; -- eval language construct nel checking/lowering; -- materialized scope, context e value bridge; -- flush/reload dei locals osservabili; -- error/status mapping e cleanup. +- `elephc-magician` crate; +- C/Rust ABI for `__elephc_eval_execute`; +- `elephc_magician` linker bridge; +- runtime feature detection; +- eval language construct in checking/lowering; +- materialized scope, context, and value bridge; +- observable-local flush/reload; +- error/status mapping and cleanup. -La copertura codegen e interpreter include eval in top-level, funzioni, metodi, -scope condiviso, nested eval, return/output, variabili create, mutation di -locals, callables, constructors, closures e reflection. +Codegen and interpreter coverage includes eval at top level, in functions, and +in methods, shared scope, nested eval, return/output, created variables, local +mutation, callables, constructors, closures, and reflection. -### Interpreter magician +### Magician Interpreter -Completato per il subset corrente: +Completed for the current subset: -- lexer/parser runtime per fragment eval senza tag ` git diff --check ``` -Per modifiche a ABI/codegen/runtime ownership: +For ABI/codegen/runtime ownership changes: ```bash cargo check @@ -344,35 +342,37 @@ cargo test --test codegen_tests git diff --check ``` -Per benchmark manuali: +For manual benchmarks: ```bash python3 scripts/benchmark_magician.py --case algebra_heavy --iterations 5 --warmup 1 python3 scripts/benchmark_magician.py --case literal_scalar_aot --iterations 5 --warmup 1 ``` -## Rischi +## Risks -- Scope sync incompleta puo' creare variabili stale o mancare creazioni/unset. -- Duplicare codegen AOT manuale crea un secondo backend difficile da mantenere. +- Incomplete scope sync can create stale variables or miss creations/unsets. +- Duplicating manual AOT codegen creates a second backend that is hard to + maintain. - Treating eval as ordinary static code can break PHP eval semantics. -- References, COW, arrays and object properties can introduce double-free, - leaks or missed mutations if bypassano helper runtime. -- `eval('$x + 1;')` ritorna `null`, non l'ultima espressione. -- Fallback selection troppo aggressiva puo' miscompilare codice dinamico. -- Ottimizzazioni magician non devono congelare context/scope/magic constants. -- Ogni path nuovo deve restare target-aware su macOS ARM64, Linux ARM64 e - Linux x86_64. - -## Criteri di completamento finale - -Il lavoro eval/magician puo' considerarsi chiuso quando: - -1. il fallback magician copre in modo testato il subset PHP dichiarato; -2. ogni literal eval supportato usa AOT o fallback esplicito con reason; -3. il subset AOT non dipende da un mini-backend manuale non manutenibile; -4. programmi fully AOT non linkano `elephc_magician`; -5. static/eval builtin parity non ha allowlist stale; -6. benchmark prime-loop e algebra-heavy restano corretti e misurabili; -7. i tre target supportati hanno copertura focused per ogni cambio ABI/codegen; -8. docs e test riflettono esattamente il subset supportato e i fallback. +- References, COW, arrays, and object properties can introduce double-free, + leaks, or missed mutations if they bypass runtime helpers. +- `eval('$x + 1;')` returns `null`, not the last expression. +- Over-aggressive fallback selection can miscompile dynamic code. +- Magician optimizations must not freeze context/scope/magic constants. +- Every new path must stay target-aware on macOS ARM64, Linux ARM64, and Linux + x86_64. + +## Final Completion Criteria + +The eval/magician work can be considered closed when: + +1. magician fallback covers the declared PHP subset with tests; +2. every supported literal eval uses AOT or an explicit fallback reason; +3. the AOT subset does not depend on an unmaintainable manual mini-backend; +4. fully AOT programs do not link `elephc_magician`; +5. static/eval builtin parity has no stale allowlist entries; +6. prime-loop and algebra-heavy benchmarks remain correct and measurable; +7. all three supported targets have focused coverage for every ABI/codegen + change; +8. docs and tests exactly reflect the supported subset and fallbacks. diff --git a/.plans/elephc-magician-builtin-structure-plan.md b/.plans/elephc-magician-builtin-structure-plan.md new file mode 100644 index 0000000000..7ab3a76706 --- /dev/null +++ b/.plans/elephc-magician-builtin-structure-plan.md @@ -0,0 +1,221 @@ +# Plan: elephc-magician Builtin Structure + +## Task + +- [ ] Phase 1: introduce a declarative eval-side registry without changing + runtime behavior. +- [ ] Phase 1: migrate a small pilot set of simple builtins into the new + per-builtin layout and derive metadata from that registry. +- [ ] Phase 1: update parity tests to query the registry instead of searching + dispatcher string literals for migrated builtins. +- [ ] Phase 2: migrate already implemented magician builtins area by area while + keeping fallback to existing dispatchers until each area is complete. +- [ ] Phase 2: remove duplicate manual tables for names, signatures, defaults, + by-ref parameters, and dispatch in migrated areas. +- [ ] Phase 2: keep ordinary files below 500 LoC, leaving exceptions only for + cohesive single-scope helpers documented in their module preambles. +- [ ] Phase 3: split remaining large builtin files (`symbols.rs`, + `filesystem/streams.rs`, `class_metadata/oop_introspection.rs`, + `registry/callable.rs`, `arrays/core.rs`) into builtin home files and shared + helpers. +- [ ] Phase 3: replace the giant direct-dispatch match in + `interpreter/expressions.rs` with smaller registry lookups, preserving special + paths for language constructs and by-ref/source-sensitive calls. +- [ ] Phase 3: update agent/contributor documentation if the workflow for adding + eval builtins changes. + +## Goal + +`elephc-magician` should move closer to the builtin model used by `elephc`: one +home file per builtin, metadata next to the implementation, and dispatch derived +from one source of truth. The crate remains a separate eval bridge; it should not +depend directly on the main `elephc` crate only to share compiler-internal types. + +The goal is not to copy `src/builtins/` mechanically. It is to replicate the +useful properties: + +- builtin declarations live next to the code they implement; +- names, parameters, defaults, by-ref flags, variadics, and dispatch are derived + from one source; +- files stay small and cohesive; +- exceptions are justified only for single-scope engines/helpers. + +## Current State + +`elephc` uses `src/builtins//.rs` with the `builtin!` macro. That +file drives catalog lookup, signatures, type checking, lowering, and generated +documentation. + +`elephc-magician` currently has several manual sources: + +- `crates/elephc-magician/src/interpreter/builtins/registry/names.rs` for the + PHP-visible builtin list; +- `crates/elephc-magician/src/interpreter/builtins/registry/signature.rs` for + signature shape, defaults, and by-ref metadata; +- `crates/elephc-magician/src/interpreter/expressions.rs` for direct dispatch of + positional builtin calls; +- `crates/elephc-magician/src/interpreter/builtins/registry/dispatch/*.rs` for + dynamic/by-value dispatch; +- family files under `interpreter/builtins/` for the actual implementations. + +This duplication makes adding a builtin expensive and makes parity tests depend +on string literals inside dispatchers. + +## Target Layout + +Suggested layout: + +```text +crates/elephc-magician/src/interpreter/builtins/ + macros.rs + spec.rs + registry/ + mod.rs + binding.rs + callable.rs + dynamic_mutation.rs + array/ + count.rs + array_map.rs + array_reduce.rs + helpers.rs + string/ + strlen.rs + strrev.rs + substr.rs + helpers.rs + types/ + boolval.rs + intval.rs + is_array.rs + filesystem/ + fopen.rs + fread.rs + stream_helpers.rs +``` + +Each builtin home file should contain: + +- a Rustdoc module preamble; +- the `eval_builtin!` declaration; +- a direct-call wrapper over `EvalExpr` arguments, when needed; +- a dynamic/by-value wrapper over `RuntimeCellHandle`, when needed; +- a mutating/by-ref wrapper, if the builtin can write into caller storage; +- delegation to shared helpers when multiple builtins use the same algorithm. + +Shared helpers must not become generic buckets. If a helper exceeds 500 LoC but +has one clear scope, its preamble must explain why keeping it cohesive is better +than splitting it mechanically. + +## Phase 1: Declarative Registry Without a Big Bang + +Introduce eval-side infrastructure alongside the existing code. + +Components: + +- `spec.rs` with `EvalBuiltinSpec`, `EvalArea`, parameters, defaults, by-ref + flags, variadics, and dispatch hooks; +- `macros.rs` with `eval_builtin!`, modeled on the main `builtin!` macro but + using magician-specific types; +- `registry/mod.rs` with case-insensitive lookup, ordered name iteration, and + conversion into `builtin_metadata`; +- compatibility with old dispatchers: an unmigrated builtin continues through + the existing match path. + +Suggested pilot builtins: + +- `strlen`; +- `count`; +- `boolval`; +- `abs`; +- `strrev`. + +They cover strings, arrays/countable objects, casts, math, and string runtime +helpers, while staying small enough for a readable first PR. + +Minimum checks: + +- `cargo test -p elephc-magician `; +- `cargo test --test builtin_parity_tests`; +- `git diff --check`. + +## Phase 2: Area-by-Area Migration + +Migrate one area at a time without changing behavior. + +Suggested order: + +1. `types` and scalar casts/predicates; +2. simple math/formatting builtins; +3. stateless string builtins; +4. non-mutating array builtins; +5. JSON, regex, and time; +6. filesystem/stream builtins; +7. symbols/reflection/class metadata; +8. by-ref/mutating and callable special cases. + +For each area: + +- create home files for each builtin; +- move metadata into `eval_builtin!`; +- derive names/signatures/defaults/by-ref data from the registry; +- convert the area's dynamic dispatch to registry lookup; +- reduce direct dispatch to registry lookup or area-scoped dispatch; +- update parity tests so they do not depend on `include_str!` over legacy files; +- keep the area's focused tests green. + +During Phase 2, a hybrid system is acceptable: the new registry for migrated +areas, manual dispatchers for areas not yet migrated. Duplicating metadata for a +migrated builtin is not acceptable. + +## Phase 3: Large-File Cleanup and Legacy Path Removal + +Once most builtins use the registry: + +- remove or reduce `registry/names.rs`; +- reduce `registry/signature.rs` to common helpers or remove it; +- split `registry/dispatch/*.rs` files that have become long matches only; +- split `interpreter/expressions.rs`, leaving only: + - language constructs (`eval`, `isset`, `unset`, `empty`); + - source-sensitive or by-ref calls that cannot pre-evaluate every argument; + - ordered fallback into registry direct-call dispatch. + +Files that need explicit treatment: + +- `interpreter/builtins/symbols.rs`; +- `interpreter/builtins/filesystem/streams.rs`; +- `interpreter/builtins/class_metadata/oop_introspection.rs`; +- `interpreter/builtins/registry/callable.rs`; +- `interpreter/builtins/arrays/core.rs`. + +For each one, decide whether: + +- it is a multi-builtin bucket that should be split into home files; +- it is a single-scope helper that should stay cohesive with an explicit + preamble; +- it mixes responsibilities and should be split before builtin migration. + +## Acceptance Criteria + +- Adding an eval builtin requires one home file and at most area `mod.rs` wiring, + not edits to four separate manual tables. +- `elephc_magician::builtin_metadata` is derived from the registry and stays + aligned with parity tests. +- Parity tests compare the static registry and eval registry, not string + literals scattered through dispatchers. +- Ordinary files stay below 500 LoC; single-scope exceptions are justified in + module preambles. +- Work can land area by area, and every PR leaves the relevant focused tests + green. + +## Risk Notes + +- By-ref builtins and dynamic callables preserve evaluation order and writable + targets. Migrate them after simple by-value builtins. +- `elephc-magician` is an ABI-facing staticlib; do not expose unstable Rust + types across the C boundary. +- Avoid a direct dependency on `elephc` for sharing `BuiltinSpec` unless there is + an explicit future decision to extract a common crate. +- Do not perform a mechanical migration without tests. The main risks are + breaking named arguments, spread arguments, defaults, and PHP-compatible + warnings. diff --git a/.plans/phpstorm-plugin.md b/.plans/phpstorm-plugin.md index 546bece6ac..e3bc5e05d1 100644 --- a/.plans/phpstorm-plugin.md +++ b/.plans/phpstorm-plugin.md @@ -1,45 +1,54 @@ -# JetBrains Plugin per elephc +# JetBrains Plugin for elephc ## Context -elephc compila PHP a binari nativi ARM64 ed estende PHP con due famiglie di sintassi non-standard: -1. **`extern`** — dichiarazioni FFI (funzioni, classi, globali, blocchi raggruppati con nome libreria) -2. **`ptr` / `ptr`** — tipo puntatore opaco, funzioni built-in (`ptr()`, `ptr_cast()`, `ptr_null()`, ecc.) +elephc compiles PHP to native ARM64 binaries and extends PHP with two families +of non-standard syntax: -PhpStorm non riconosce queste estensioni e segna errori ovunque. Serve un plugin che le integri senza perdere il supporto PHP nativo. +1. **`extern`**: FFI declarations for functions, classes, globals, and named + library blocks. +2. **`ptr` / `ptr`**: opaque pointer types and builtins such as `ptr()`, + `ptr_cast()`, `ptr_null()`, and related helpers. -## Approccio architetturale: PHP Plugin Extension +PhpStorm does not recognize these extensions and reports errors throughout +elephc code. The plugin should integrate the extensions without losing native +PHP support. -**Non** un linguaggio custom, **non** language injection. Il plugin **estende** il plugin PHP esistente con: -- Stub file per le funzioni built-in → elimina errori "undefined function" -- `HighlightInfoFilter` → sopprime errori di parsing su `extern` e `ptr_cast()` -- Completion contributor → autocompletamento per keyword e tipi custom -- File-based index → indicizza le dichiarazioni `extern` nel progetto -- Type provider → risolve tipi di ritorno per `ptr_cast()` +## Architectural Approach: PHP Plugin Extension -### Perché non altre strade +Do **not** implement a custom language or language injection. The plugin should +extend the existing PHP plugin with: -| Approccio | Problema | +- stub files for builtins, removing "undefined function" errors; +- `HighlightInfoFilter`, suppressing parse errors for `extern` and + `ptr_cast()`; +- completion contributor for custom keywords and types; +- file-based index for project `extern` declarations; +- type provider for `ptr_cast()` return types. + +### Why Not Other Paths + +| Approach | Problem | |---|---| -| Linguaggio custom | Perde tutta l'intelligenza PHP — elephc è 95% PHP standard | -| Language injection | È per linguaggi embedded (SQL in stringhe), non estensioni sintattiche | -| File type `.elephc` | Gli utenti scrivono `.php`, cambiare estensione rompe l'ecosistema | +| Custom language | Loses all PHP intelligence; elephc is 95% standard PHP. | +| Language injection | Intended for embedded languages such as SQL in strings, not syntax extensions. | +| `.elephc` file type | Users write `.php`; changing extensions breaks the ecosystem. | -## Struttura del plugin +## Plugin Structure -``` +```text elephc-intellij-plugin/ ├── build.gradle.kts ├── src/main/resources/ │ ├── META-INF/plugin.xml -│ └── stubs/_elephc_stubs.php # stub functions + ptr class +│ └── stubs/_elephc_stubs.php └── src/main/kotlin/com/illegalstudio/elephc/ - ├── stubs/ElephcStubLibraryProvider.kt # registra gli stub - ├── suppress/ElephcHighlightFilter.kt # sopprime errori extern/ptr_cast + ├── stubs/ElephcStubLibraryProvider.kt + ├── suppress/ElephcHighlightFilter.kt ├── completion/ElephcCompletionContributor.kt - ├── index/ElephcExternIndex.kt # indicizza extern declarations - ├── types/ElephcTypeProvider.kt # tipi per ptr_cast() - ├── run/ElephcRunConfiguration.kt # compile & run + ├── index/ElephcExternIndex.kt + ├── types/ElephcTypeProvider.kt + ├── run/ElephcRunConfiguration.kt └── settings/ElephcSettingsConfigurable.kt ``` @@ -70,15 +79,15 @@ elephc-intellij-plugin/ ``` -## Componenti in dettaglio +## Components -### 1. Stub Library (risolve ~80% dei problemi) +### 1. Stub Library -File `_elephc_stubs.php` bundled nel plugin con dichiarazioni per tutte le funzioni `ptr_*`: +Bundle `_elephc_stubs.php` with declarations for all `ptr_*` functions: ```php (...)`** — PHP parsa `ptr_cast < T > (...)` come due confronti; il filtro riconosce il pattern via regex `ptr_cast\s*<\s*\w+\s*>\s*\(` e sopprime -3. **`ptr` in type hints** — le angle brackets nei type hint causano errori; sopprimere quando il contesto è `ptr<\w+>` +1. A line starts with `extern` for functions, classes, globals, or named + library blocks. +2. `ptr_cast(...)` is parsed by PHP as comparisons. Recognize the pattern + with `ptr_cast\s*<\s*\w+\s*>\s*\(`. +3. `ptr` appears in type hints and angle brackets cause syntax errors. -Questo è il componente più critico — senza, l'IDE è inutilizzabile su codice elephc. +This is the most critical component. Without it, the IDE is unusable on elephc +code. ### 3. Extern Declaration Index -`FileBasedIndexExtension` che scansiona `.php` per dichiarazioni extern. Non può usare il PSI di PHP (che non parsa `extern`), quindi usa un mini-parser a regex: +Implement a `FileBasedIndexExtension` that scans `.php` files for `extern` +declarations. PHP PSI cannot be used because it does not parse `extern`, so use +a small regex parser: -- `extern\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*(\w+)` → funzione singola -- `extern\s+"([^"]+)"\s*\{` → inizio blocco con nome libreria -- `extern\s+class\s+(\w+)\s*\{` → classe extern -- `extern\s+global\s+(\w+)\s+\$(\w+)` → globale extern +- `extern\s+function\s+(\w+)\s*\(([^)]*)\)\s*:\s*(\w+)` for one function; +- `extern\s+"([^"]+)"\s*\{` for a library block start; +- `extern\s+class\s+(\w+)\s*\{` for an extern class; +- `extern\s+global\s+(\w+)\s+\$(\w+)` for an extern global. -L'indice alimenta completion e navigation. +The index feeds completion and navigation. -Riferimento per la sintassi esatta: `src/parser/stmt/ffi.rs`. +Exact syntax reference: `src/parser/stmt/ffi.rs`. ### 4. Completion Contributor -- Dopo `ext` → suggerisce `extern` -- Dopo `extern` → suggerisce `function`, `class`, `global`, `"` (per nome libreria) -- In posizione type hint → suggerisce `ptr`, `ptr` (classi dall'indice extern) -- Dopo `ptr_cast<` → suggerisce nomi di classi extern +- After `ext`, suggest `extern`. +- After `extern`, suggest `function`, `class`, `global`, and `"` for a library + name. +- In type-hint position, suggest `ptr` and `ptr` using classes from + the extern index. +- After `ptr_cast<`, suggest extern class names. -### 5. Type Provider (fase 2) +### 5. Type Provider -`PhpTypeProvider4` che risolve il tipo di ritorno di `ptr_cast()`. Esamina il testo raw intorno all'espressione (il PSI è rotto dalle angle brackets) e restituisce `ptr` come tipo. +Implement `PhpTypeProvider4` to resolve the return type of `ptr_cast()`. +Because angle brackets break PHP PSI, inspect raw text around the expression and +return `ptr` as the type. -## Piano di implementazione a fasi +## Implementation Phases -### MVP — elimina il rumore +### MVP: Remove Noise -1. Setup progetto Gradle con dipendenza `com.jetbrains.php` -2. Creare e bundlare `_elephc_stubs.php` -3. Implementare `ElephcStubLibraryProvider` -4. Implementare `ElephcHighlightFilter` -5. Testare con `examples/ffi/main.php` e altri esempi del progetto +1. Set up the Gradle project with a `com.jetbrains.php` dependency. +2. Create and bundle `_elephc_stubs.php`. +3. Implement `ElephcStubLibraryProvider`. +4. Implement `ElephcHighlightFilter`. +5. Test against `examples/ffi/main.php` and other project examples. -### Fase 2 — intelligenza +### Phase 2: Intelligence -6. `ElephcExternIndex` per indicizzare dichiarazioni extern -7. `ElephcCompletionContributor` per keyword e tipi -8. Reference resolution: Ctrl+click su chiamata extern → dichiarazione -9. `ElephcTypeProvider` per `ptr_cast()` +6. Implement `ElephcExternIndex` for extern declaration indexing. +7. Implement `ElephcCompletionContributor` for keywords and types. +8. Add reference resolution: Ctrl-click on extern calls should navigate to the + declaration. +9. Implement `ElephcTypeProvider` for `ptr_cast()`. -### Fase 3 — developer experience +### Phase 3: Developer Experience -10. Run configuration (compile + esegui binario) -11. Parsing errori elephc con link cliccabili (line:col) -12. Gutter icons su dichiarazioni extern -13. Settings page per path del binario elephc +10. Add a run configuration that compiles and runs the binary. +11. Parse elephc errors into clickable `line:col` links. +12. Add gutter icons on extern declarations. +13. Add a settings page for the elephc binary path. -## Sfide principali +## Main Challenges -| Sfida | Impatto | Mitigazione | +| Challenge | Impact | Mitigation | |---|---|---| -| PHP PSI non parsa `extern` | Nessun PSI strutturato per dichiarazioni extern | Mini-parser custom a regex nel FileBasedIndex | -| `ptr_cast()` rompe il parser PHP | PHP interpreta `<`/`>` come confronti | HighlightFilter + text-level regex per riconoscere il pattern | -| `ptr` nei type hint | Angle brackets non valide in type position | Soppressione errori + stub class `ptr` per il caso semplice | -| API PHP plugin instabili | Possibili breaking changes tra versioni PhpStorm | Dichiarare range `since-build`/`until-build` stretto, usare solo API stabili | - -## File di riferimento nel codebase elephc - -- `src/parser/ast/stmt.rs`, `src/parser/ast/expr.rs`, `src/parser/ast/ffi.rs` — definiscono i nodi AST custom (ExternFunctionDecl, PtrCast, CType, ecc.) -- `src/parser/stmt/ffi.rs` — parsing delle dichiarazioni extern -- `src/parser/expr/prefix_complex.rs` — parsing di `ptr_cast()` -- `src/types/checker/builtins/pointers.rs` e `src/types/checker/builtins/catalog.rs` — firme, catalogo e validazione dei built-in `ptr_*` -- `examples/ffi/main.php` — esempio canonico con tutta la sintassi extern - -## Verifica - -1. Aprire il progetto elephc in PhpStorm con il plugin installato -2. Verificare che `examples/ffi/main.php` non mostri errori rossi su `extern` e `ptr_cast` -3. Verificare che `ptr_null()`, `ptr_get()`, ecc. abbiano autocompletamento e documentazione -4. Ctrl+click su una chiamata extern → dovrebbe navigare alla dichiarazione -5. Compilare ed eseguire un esempio dalla run configuration +| PHP PSI does not parse `extern`. | No structured PSI for extern declarations. | Custom regex parser in the file-based index. | +| `ptr_cast()` breaks the PHP parser. | PHP interprets `<` and `>` as comparisons. | Highlight filter plus text-level regex recognition. | +| `ptr` in type hints. | Angle brackets are invalid in type position. | Error suppression plus stub class `ptr` for the simple case. | +| PHP plugin APIs can be unstable. | PhpStorm versions may break the plugin. | Declare a narrow `since-build`/`until-build` range and use stable APIs only. | + +## elephc Codebase References + +- `src/parser/ast/stmt.rs`, `src/parser/ast/expr.rs`, + `src/parser/ast/ffi.rs`: custom AST nodes such as `ExternFunctionDecl`, + `PtrCast`, and `CType`. +- `src/parser/stmt/ffi.rs`: extern declaration parsing. +- `src/parser/expr/prefix_complex.rs`: `ptr_cast()` parsing. +- `src/types/checker/builtins/pointers.rs` and + `src/types/checker/builtins/catalog.rs`: signatures, catalog entries, and + validation for `ptr_*` builtins. +- `examples/ffi/main.php`: canonical example that exercises extern syntax. + +## Verification + +1. Open the elephc project in PhpStorm with the plugin installed. +2. Verify that `examples/ffi/main.php` has no red errors on `extern` and + `ptr_cast`. +3. Verify that `ptr_null()`, `ptr_get()`, and related helpers get completion and + documentation. +4. Ctrl-click an extern call and verify navigation to its declaration. +5. Compile and run an example from the run configuration. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd338d3f72..837fed0c09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ Contributions created with the help of AI tools are welcome. What matters to us ## Planning Larger Work -If you're working toward something bigger than a single, self-contained change, we recommend writing a plan before you dive into the code. Plans live in the `.plans` directory of the repository. +If you're working toward something bigger than a single, self-contained change, we recommend writing a plan before you dive into the code. Plans live in the `.plans` directory of the repository, and every plan in `.plans` must be written in English. Start each plan with a checklist of the tasks it involves, then follow it with the detailed implementation notes for each of them. Keeping the task list up front makes the plan's progress easy to verify at a glance — whether it's complete is simply a matter of checking which tasks are marked done. diff --git a/docs/internals/the-ir.md b/docs/internals/the-ir.md index 28a40ed833..b770cf0ed7 100644 --- a/docs/internals/the-ir.md +++ b/docs/internals/the-ir.md @@ -10,8 +10,6 @@ The diagnostic `--emit-ir` path lowers the checked and optimized AST into validated textual EIR, and normal executable/cdylib builds lower that same EIR into assembly. -**Implementation phases:** `.plans/eir-*.md` - **Authoritative source audit for this spec:** - `src/types/model.rs` From 2817c2261ab4a54861c2a32cbe8facf55a70a698 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 21:11:48 +0200 Subject: [PATCH 1058/1208] feat(magician): add declarative eval builtin registry --- .../elephc-magician-builtin-structure-plan.md | 6 +- Cargo.lock | 1 + crates/elephc-magician/Cargo.toml | 1 + .../src/interpreter/builtin_metadata.rs | 7 + .../src/interpreter/builtins/array/count.rs | 18 ++ .../src/interpreter/builtins/array/mod.rs | 11 + .../src/interpreter/builtins/macros.rs | 51 +++++ .../src/interpreter/builtins/math/abs.rs | 16 ++ .../src/interpreter/builtins/math/mod.rs | 11 + .../src/interpreter/builtins/mod.rs | 8 + .../interpreter/builtins/registry/binding.rs | 5 +- .../interpreter/builtins/registry/callable.rs | 1 - .../builtins/registry/dispatch/arrays.rs | 1 - .../builtins/registry/dispatch/core.rs | 1 - .../builtins/registry/dispatch/mod.rs | 5 + .../src/interpreter/builtins/registry/mod.rs | 169 +++++++++++++++ .../interpreter/builtins/registry/names.rs | 22 +- .../builtins/registry/signature.rs | 17 +- .../src/interpreter/builtins/spec.rs | 202 ++++++++++++++++++ .../src/interpreter/builtins/string/mod.rs | 12 ++ .../src/interpreter/builtins/string/strlen.rs | 16 ++ .../src/interpreter/builtins/string/strrev.rs | 16 ++ .../src/interpreter/builtins/types/boolval.rs | 16 ++ .../src/interpreter/builtins/types/mod.rs | 11 + .../src/interpreter/expressions.rs | 4 + tests/builtin_parity_tests.rs | 17 ++ 26 files changed, 635 insertions(+), 10 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/count.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/macros.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/abs.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/spec.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strlen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strrev.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/boolval.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/mod.rs diff --git a/.plans/elephc-magician-builtin-structure-plan.md b/.plans/elephc-magician-builtin-structure-plan.md index 7ab3a76706..00db7d9841 100644 --- a/.plans/elephc-magician-builtin-structure-plan.md +++ b/.plans/elephc-magician-builtin-structure-plan.md @@ -2,11 +2,11 @@ ## Task -- [ ] Phase 1: introduce a declarative eval-side registry without changing +- [x] Phase 1: introduce a declarative eval-side registry without changing runtime behavior. -- [ ] Phase 1: migrate a small pilot set of simple builtins into the new +- [x] Phase 1: migrate a small pilot set of simple builtins into the new per-builtin layout and derive metadata from that registry. -- [ ] Phase 1: update parity tests to query the registry instead of searching +- [x] Phase 1: update parity tests to query the registry instead of searching dispatcher string literals for migrated builtins. - [ ] Phase 2: migrate already implemented magician builtins area by area while keeping fallback to existing dispatchers until each area is complete. diff --git a/Cargo.lock b/Cargo.lock index 3eadc75989..f8f865759c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,7 @@ dependencies = [ "elephc-crypto", "elephc-phar", "flate2", + "inventory", "libc", "unicode-segmentation", ] diff --git a/crates/elephc-magician/Cargo.toml b/crates/elephc-magician/Cargo.toml index e93bcae2ab..0090199bde 100644 --- a/crates/elephc-magician/Cargo.toml +++ b/crates/elephc-magician/Cargo.toml @@ -13,5 +13,6 @@ crate-type = ["staticlib", "rlib"] elephc-crypto = { path = "../elephc-crypto" } elephc-phar = { path = "../elephc-phar" } flate2 = "1" +inventory = "0.3" libc = "0.2" unicode-segmentation = "1" diff --git a/crates/elephc-magician/src/interpreter/builtin_metadata.rs b/crates/elephc-magician/src/interpreter/builtin_metadata.rs index 589d58763d..1d63ae392a 100644 --- a/crates/elephc-magician/src/interpreter/builtin_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtin_metadata.rs @@ -11,6 +11,7 @@ //! - Signature shape is the same registry data used by eval named-argument binding. use super::builtins::{ + eval_declared_builtin_exists, eval_builtin_param_names, eval_builtin_signature_shape, eval_php_visible_builtin_exists, eval_php_visible_builtin_function_names, }; @@ -41,6 +42,12 @@ pub fn php_visible_builtin_names() -> &'static [&'static str] { eval_php_visible_builtin_function_names() } +/// Returns whether the eval builtin is backed by the declarative registry. +pub fn php_visible_builtin_is_registry_declared(name: &str) -> bool { + let canonical = php_symbol_key(name); + eval_declared_builtin_exists(&canonical) +} + /// Returns comparison metadata for one eval builtin signature, when named calls are tracked. pub fn builtin_signature_metadata(name: &str) -> Option { let canonical = php_symbol_key(name); diff --git a/crates/elephc-magician/src/interpreter/builtins/array/count.rs b/crates/elephc-magician/src/interpreter/builtins/array/count.rs new file mode 100644 index 0000000000..053b825ca6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/count.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `count`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing count hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "count", + area: Array, + params: [value, mode = EvalBuiltinDefaultValue::Int(0)], + direct: Count, + values: Count, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs new file mode 100644 index 0000000000..52daf3169b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -0,0 +1,11 @@ +//! Purpose: +//! Per-builtin declarations for array and collection functions migrated to the +//! eval builtin registry. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!`. + +mod count; diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs new file mode 100644 index 0000000000..11e45e224f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Declarative helpers for registering eval-side PHP builtins. +//! The macro keeps per-builtin files compact while preserving the interpreter +//! registry as the runtime lookup surface. +//! +//! Called from: +//! - `crate::interpreter::builtins::::` home files. +//! +//! Key details: +//! - Macro expansion submits static metadata to `inventory`. +//! - Dispatch hooks are magician-specific enums so handlers can stay generic +//! over `RuntimeValueOps`. + +macro_rules! eval_builtin { + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(= $default:expr)?),* $(,)?], + direct: $direct:ident, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(stringify!($param)),*], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: stringify!($param), + default: eval_builtin!(@default $($default)?), + by_ref: false, + }, + )* + ], + variadic: None, + by_ref_params: &[], + direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + } + } + }; + + (@default) => { + None + }; + + (@default $default:expr) => { + Some($default) + }; +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/abs.rs b/crates/elephc-magician/src/interpreter/builtins/math/abs.rs new file mode 100644 index 0000000000..80ac4d6b13 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/abs.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `abs`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing numeric hook. + +eval_builtin! { + name: "abs", + area: Math, + params: [num], + direct: Abs, + values: Abs, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs new file mode 100644 index 0000000000..3b8fec2f10 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs @@ -0,0 +1,11 @@ +//! Purpose: +//! Per-builtin declarations for numeric functions migrated to the eval builtin +//! registry. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!`. + +mod abs; diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 7f9cb9435b..af32cb2f6c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -11,10 +11,15 @@ //! execution helpers without widening crate-level visibility. //! - Runtime value creation and PHP coercions still flow through `RuntimeValueOps`. +#[macro_use] +mod macros; + +mod array; mod arrays; mod class_metadata; mod filesystem; mod formatting; +mod math; mod network_env; mod process_control; mod raw_memory; @@ -23,9 +28,12 @@ mod regex; mod registry; mod scalars; mod spl_autoload; +mod spec; +mod string; mod strings; mod symbols; mod time; +mod types; pub(super) use arrays::*; pub(super) use class_metadata::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index aefe64a42e..758baeaba4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -8,7 +8,6 @@ //! - Helpers are scoped to the eval interpreter and operate on already parsed //! EvalIR call metadata or evaluated runtime-cell handles. -use super::super::super::*; use super::*; /// Evaluates a direct PHP-visible builtin call with named or spread arguments. @@ -120,6 +119,10 @@ fn eval_builtin_default_arg( pub(in crate::interpreter) fn eval_builtin_param_names( name: &str, ) -> Option<&'static [&'static str]> { + if let Some(params) = eval_declared_builtin_param_names(name) { + return Some(params); + } + match name { "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), "array_chunk" => Some(&["array", "length"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 2e5472837d..9ef7e92b59 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -8,7 +8,6 @@ //! - Helpers are scoped to the eval interpreter and operate on already parsed //! EvalIR call metadata or evaluated runtime-cell handles. -use super::super::super::*; use super::*; /// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs index 373b9f3d15..dc496c1fa6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs @@ -8,7 +8,6 @@ //! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can //! continue probing other builtin families. -use super::super::super::super::*; use super::super::super::*; use super::super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs index 1c07bd2c0a..a0b954ddbc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs @@ -8,7 +8,6 @@ //! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can //! continue probing other builtin families. -use super::super::super::super::*; use super::super::super::*; use super::super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index c02cf5e018..ca4b329b48 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -20,6 +20,7 @@ mod strings; mod symbols; mod time; +use super::eval_declared_builtin_values_call; use super::super::super::*; use arrays::*; @@ -41,6 +42,10 @@ pub(in crate::interpreter) fn eval_builtin_with_values( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { + if let Some(result) = eval_declared_builtin_values_call(name, evaluated_args, context, values)? { + return Ok(Some(result)); + } + if let Some(result) = eval_arrays_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 4906fc56bf..1c5782d038 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -9,6 +9,12 @@ //! - The large by-value dispatch match is isolated from argument planning and //! callable normalization. +use std::collections::HashMap; +use std::sync::OnceLock; + +use super::super::*; +use super::spec::EvalBuiltinSpec; + mod binding; mod callable; mod callable_validation; @@ -24,3 +30,166 @@ pub(in crate::interpreter) use dispatch::*; pub(in crate::interpreter) use dynamic_mutation::*; pub(in crate::interpreter) use names::*; pub(in crate::interpreter) use signature::*; + +/// Lazy registry of builtins migrated to declarative eval specs. +struct DeclaredBuiltinRegistry { + /// Case-insensitive lookup keyed by canonical lowercase PHP builtin name. + by_name: HashMap, + /// Stable ordered list of registered canonical names. + names: Vec<&'static str>, +} + +/// Global eval builtin registry built from inventory submissions. +static DECLARED_BUILTIN_REGISTRY: OnceLock = OnceLock::new(); + +/// Builds the declarative registry and rejects duplicate builtin names. +fn build_declared_builtin_registry() -> DeclaredBuiltinRegistry { + let mut by_name = HashMap::new(); + let mut names = Vec::new(); + + for spec in inventory::iter:: { + validate_declared_builtin_spec(spec); + let key = spec.name.to_ascii_lowercase(); + if by_name.insert(key, spec).is_some() { + panic!( + "duplicate eval builtin name registered in inventory: \"{}\"", + spec.name + ); + } + names.push(spec.name); + } + + names.sort_unstable(); + DeclaredBuiltinRegistry { by_name, names } +} + +/// Validates static spec invariants before the registry is exposed. +fn validate_declared_builtin_spec(spec: &EvalBuiltinSpec) { + assert_eq!( + spec.params.len(), + spec.param_names.len(), + "eval builtin {} has mismatched params and param_names", + spec.name + ); + for (param, name) in spec.params.iter().zip(spec.param_names.iter()) { + assert_eq!( + param.name, *name, + "eval builtin {} has a param_names entry out of sync", + spec.name + ); + if param.by_ref { + assert!( + spec.by_ref_params.contains(¶m.name), + "eval builtin {} marks {} by-ref without listing it", + spec.name, + param.name + ); + } + } + let _ = spec.area(); +} + +/// Returns the declarative registry, initializing it on first access. +fn declared_builtin_registry() -> &'static DeclaredBuiltinRegistry { + DECLARED_BUILTIN_REGISTRY.get_or_init(build_declared_builtin_registry) +} + +/// Looks up a declaratively migrated eval builtin with PHP case-insensitive matching. +pub(in crate::interpreter) fn eval_declared_builtin_spec( + name: &str, +) -> Option<&'static EvalBuiltinSpec> { + let key = name.trim_start_matches('\\').to_ascii_lowercase(); + declared_builtin_registry().by_name.get(&key).copied() +} + +/// Returns whether a PHP-visible builtin has migrated into the declarative registry. +pub(in crate::interpreter) fn eval_declared_builtin_exists(name: &str) -> bool { + eval_declared_builtin_spec(name).is_some() +} + +/// Returns stable canonical names for builtins in the declarative registry. +pub(in crate::interpreter) fn eval_declared_builtin_function_names() -> &'static [&'static str] { + declared_builtin_registry().names.as_slice() +} + +/// Returns PHP parameter names for a declaratively migrated builtin. +pub(in crate::interpreter) fn eval_declared_builtin_param_names( + name: &str, +) -> Option<&'static [&'static str]> { + eval_declared_builtin_spec(name).map(|spec| spec.param_names) +} + +/// Returns a default value from a declaratively migrated builtin spec. +pub(in crate::interpreter) fn eval_declared_builtin_default_value( + name: &str, + param_index: usize, +) -> Option { + eval_declared_builtin_spec(name).and_then(|spec| spec.default_value(param_index)) +} + +/// Dispatches a declaratively migrated builtin from unevaluated positional expressions. +pub(in crate::interpreter) fn eval_declared_builtin_direct_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(spec) = eval_declared_builtin_spec(name) else { + return Ok(None); + }; + let Some(hook) = spec.direct else { + return Ok(None); + }; + hook.call(spec.name, args, context, scope, values).map(Some) +} + +/// Dispatches a declaratively migrated builtin from already evaluated argument cells. +pub(in crate::interpreter) fn eval_declared_builtin_values_call( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(spec) = eval_declared_builtin_spec(name) else { + return Ok(None); + }; + let Some(hook) = spec.values else { + return Ok(None); + }; + hook.call(spec.name, evaluated_args, context, values) + .map(Some) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Verifies all Phase 1 pilot builtins are present in the declarative registry. + #[test] + fn declared_builtin_registry_exposes_phase_one_pilots() { + for name in ["abs", "boolval", "count", "strlen", "strrev"] { + assert!( + eval_declared_builtin_exists(name), + "{name} should be registered declaratively" + ); + } + } + + /// Verifies pilot builtin metadata is derived from declarative specs. + #[test] + fn declared_builtin_registry_derives_phase_one_metadata() { + assert_eq!( + eval_declared_builtin_param_names("count"), + Some(["value", "mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("count", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("strlen"), + Some(["string"].as_slice()) + ); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 8463c26a5e..a9bc34f9f6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -8,6 +8,10 @@ //! - The slice is the source of truth for PHP-visible eval builtin names. //! - Lookup callers pass canonical lowercase PHP symbol names. +use std::sync::OnceLock; + +use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; + /// PHP-visible builtin names implemented by the eval interpreter. pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[ "abs", @@ -433,12 +437,26 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "wordwrap", ]; +/// Combined PHP-visible builtin names from legacy and declarative registries. +static EVAL_PHP_VISIBLE_BUILTIN_FUNCTION_NAMES: OnceLock> = OnceLock::new(); + /// Returns the eval interpreter's PHP-visible builtin names. pub(in crate::interpreter) fn eval_php_visible_builtin_function_names() -> &'static [&'static str] { - EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS + EVAL_PHP_VISIBLE_BUILTIN_FUNCTION_NAMES + .get_or_init(|| { + let mut names = EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS.to_vec(); + for name in eval_declared_builtin_function_names() { + if !names.contains(name) { + names.push(name); + } + } + names.sort_unstable(); + names + }) + .as_slice() } /// Returns true for PHP-visible builtin names implemented by the eval interpreter. pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> bool { - EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS.contains(&name) + eval_declared_builtin_exists(name) || EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS.contains(&name) } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 6f03cccc3b..415132534d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -12,7 +12,9 @@ //! - Default values mirror the dispatcher defaults so named calls can skip //! optional parameters without changing positional semantics. -use super::eval_builtin_param_names; +use super::{ + eval_builtin_param_names, eval_declared_builtin_default_value, eval_declared_builtin_spec, +}; /// Comparison-friendly shape for one eval builtin signature. #[derive(Debug, Clone, PartialEq, Eq)] @@ -50,6 +52,15 @@ pub(in crate::interpreter) enum EvalBuiltinDefaultValue { pub(in crate::interpreter) fn eval_builtin_signature_shape( name: &str, ) -> Option { + if let Some(spec) = eval_declared_builtin_spec(name) { + return Some(EvalBuiltinSignatureShape { + required_param_count: spec.required_param_count(), + default_param_count: spec.default_param_count(), + variadic: spec.variadic, + by_ref_params: spec.by_ref_param_names(), + }); + } + let params = eval_builtin_param_names(name)?; Some(match name { "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => optional(params, 1), @@ -151,6 +162,10 @@ pub(in crate::interpreter) fn eval_builtin_default_value( name: &str, param_index: usize, ) -> Option { + if let Some(default_value) = eval_declared_builtin_default_value(name, param_index) { + return Some(default_value); + } + use EvalBuiltinDefaultValue::*; Some(match (name, param_index) { diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs new file mode 100644 index 0000000000..b7b2f19a46 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -0,0 +1,202 @@ +//! Purpose: +//! Declarative builtin specifications for the eval interpreter. +//! Each spec owns PHP-visible metadata plus optional direct and evaluated-arg +//! dispatch hooks for one builtin. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` lookup and metadata helpers. +//! - `eval_builtin!` submissions in per-builtin home files. +//! +//! Key details: +//! - Specs are collected with `inventory` to let builtin files register +//! themselves without growing a central match. +//! - Hook enums keep calls monomorphized over `RuntimeValueOps`. + +use super::super::{ + eval_builtin_count, eval_builtin_strlen, eval_count_result, ElephcEvalContext, ElephcEvalScope, + EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, +}; +use super::{eval_builtin_abs, eval_builtin_cast, eval_builtin_strrev, eval_cast_result}; +pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; + +/// Broad domain used to group eval builtin home files. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(in crate::interpreter) enum EvalArea { + /// Array and collection builtins. + Array, + /// Numeric and mathematical builtins. + Math, + /// String-processing builtins. + String, + /// Scalar conversion and type-related builtins. + Types, +} + +/// Parameter metadata for one eval builtin argument. +#[derive(Clone, Copy)] +pub(in crate::interpreter) struct EvalParamSpec { + /// PHP-visible parameter name. + pub(in crate::interpreter) name: &'static str, + /// Optional PHP default value. + pub(in crate::interpreter) default: Option, + /// Whether this parameter must bind to caller storage. + pub(in crate::interpreter) by_ref: bool, +} + +/// Direct expression-level dispatch hooks for migrated builtins. +#[derive(Clone, Copy)] +pub(in crate::interpreter) enum EvalDirectHook { + /// Dispatches `abs(...)`. + Abs, + /// Dispatches `boolval(...)`. + Boolval, + /// Dispatches `count(...)`. + Count, + /// Dispatches `strlen(...)`. + Strlen, + /// Dispatches `strrev(...)`. + Strrev, +} + +/// Evaluated-argument dispatch hooks for migrated builtins. +#[derive(Clone, Copy)] +pub(in crate::interpreter) enum EvalValuesHook { + /// Dispatches `abs(...)`. + Abs, + /// Dispatches `boolval(...)`. + Boolval, + /// Dispatches `count(...)`. + Count, + /// Dispatches `strlen(...)`. + Strlen, + /// Dispatches `strrev(...)`. + Strrev, +} + +/// Static declaration for one PHP-visible eval builtin. +pub(in crate::interpreter) struct EvalBuiltinSpec { + /// Canonical lowercase PHP builtin name. + pub(in crate::interpreter) name: &'static str, + /// Builtin family used by the file layout. + pub(in crate::interpreter) area: EvalArea, + /// Parameter names in PHP call order. + pub(in crate::interpreter) param_names: &'static [&'static str], + /// Parameter metadata in PHP call order. + pub(in crate::interpreter) params: &'static [EvalParamSpec], + /// Variadic parameter name, when supported. + pub(in crate::interpreter) variadic: Option<&'static str>, + /// Parameter names that must bind by reference. + pub(in crate::interpreter) by_ref_params: &'static [&'static str], + /// Direct expression-level dispatch hook. + pub(in crate::interpreter) direct: Option, + /// Evaluated-argument dispatch hook. + pub(in crate::interpreter) values: Option, +} + +impl EvalBuiltinSpec { + /// Returns this builtin's file-layout area. + pub(in crate::interpreter) fn area(&self) -> EvalArea { + self.area + } + + /// Returns the number of required leading parameters. + pub(in crate::interpreter) fn required_param_count(&self) -> usize { + self.params + .iter() + .take_while(|param| param.default.is_none()) + .count() + } + + /// Returns the number of parameters that define defaults. + pub(in crate::interpreter) fn default_param_count(&self) -> usize { + self.params + .iter() + .filter(|param| param.default.is_some()) + .count() + } + + /// Returns by-reference parameter names, checking they agree with param flags in debug builds. + pub(in crate::interpreter) fn by_ref_param_names(&self) -> &'static [&'static str] { + debug_assert!(self + .params + .iter() + .filter(|param| param.by_ref) + .all(|param| self.by_ref_params.contains(¶m.name))); + self.by_ref_params + } + + /// Returns the default value for one PHP parameter slot. + pub(in crate::interpreter) fn default_value( + &self, + param_index: usize, + ) -> Option { + self.params.get(param_index).and_then(|param| param.default) + } +} + +impl EvalDirectHook { + /// Runs a direct expression-level builtin call through the migrated hook. + pub(in crate::interpreter) fn call( + self, + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, + ) -> Result { + match self { + Self::Abs => eval_builtin_abs(args, context, scope, values), + Self::Boolval => eval_builtin_cast(name, args, context, scope, values), + Self::Count => eval_builtin_count(args, context, scope, values), + Self::Strlen => eval_builtin_strlen(args, context, scope, values), + Self::Strrev => eval_builtin_strrev(args, context, scope, values), + } + } +} + +impl EvalValuesHook { + /// Runs an evaluated-argument builtin call through the migrated hook. + pub(in crate::interpreter) fn call( + self, + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + ) -> Result { + match self { + Self::Abs => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.abs(*value) + } + Self::Boolval => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_cast_result(name, *value, context, values) + } + Self::Count => match evaluated_args { + [value] => eval_count_result(*value, None, context, values), + [value, mode] => eval_count_result(*value, Some(*mode), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Strlen => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let bytes = values.string_bytes(*value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) + } + Self::Strrev => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.strrev(*value) + } + } + } +} + +inventory::collect!(EvalBuiltinSpec); diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs new file mode 100644 index 0000000000..de708e0e6f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -0,0 +1,12 @@ +//! Purpose: +//! Per-builtin declarations for string functions migrated to the eval builtin +//! registry. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!`. + +mod strlen; +mod strrev; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs b/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs new file mode 100644 index 0000000000..b93d90b9be --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `strlen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing string-length hook. + +eval_builtin! { + name: "strlen", + area: String, + params: [string], + direct: Strlen, + values: Strlen, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs b/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs new file mode 100644 index 0000000000..514bc0366f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `strrev`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing string-reversal hook. + +eval_builtin! { + name: "strrev", + area: String, + params: [string], + direct: Strrev, + values: Strrev, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs b/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs new file mode 100644 index 0000000000..6595aaad56 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `boolval`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing scalar-cast hook. + +eval_builtin! { + name: "boolval", + area: Types, + params: [value], + direct: Boolval, + values: Boolval, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/mod.rs b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs new file mode 100644 index 0000000000..7e23043c87 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs @@ -0,0 +1,11 @@ +//! Purpose: +//! Per-builtin declarations for scalar type and conversion functions migrated +//! to the eval builtin registry. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!`. + +mod boolval; diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index f737b3c0fa..da6a695bd5 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1527,6 +1527,10 @@ pub(in crate::interpreter) fn eval_positional_expr_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { + if let Some(result) = eval_declared_builtin_direct_call(name, args, context, scope, values)? { + return Ok(result); + } + match name { "abs" => eval_builtin_abs(args, context, scope, values), "addslashes" | "stripslashes" => eval_builtin_slashes(name, args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 8410f1d2bc..ff912397a4 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -96,6 +96,10 @@ const EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["is_callable", "preg /// Eval supports variadic debug output before the static backend does. const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; +/// Builtins migrated to magician's declarative eval registry during the Phase 1 pilot. +const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = + &["abs", "boolval", "count", "strlen", "strrev"]; + /// Verifies every static builtin is visible through eval's function lookup. #[test] fn static_php_visible_builtins_are_visible_to_eval() { @@ -122,12 +126,14 @@ fn static_php_visible_builtins_have_eval_dispatch_literals() { .iter() .copied() .filter(|name| !STATIC_ONLY_REGISTRY_BUILTINS.contains(name)) + .filter(|name| !elephc_magician::builtin_metadata::php_visible_builtin_is_registry_declared(name)) .filter(|name| !direct_dispatch_names.contains(*name)) .collect::>(); let missing_dynamic = elephc::builtin_metadata::php_visible_builtin_names() .iter() .copied() .filter(|name| !STATIC_ONLY_REGISTRY_BUILTINS.contains(name)) + .filter(|name| !elephc_magician::builtin_metadata::php_visible_builtin_is_registry_declared(name)) .filter(|name| !dynamic_dispatch_names.contains(*name)) .collect::>(); @@ -141,6 +147,17 @@ fn static_php_visible_builtins_have_eval_dispatch_literals() { ); } +/// Verifies Phase 1 migrated builtins are backed by magician's declarative registry. +#[test] +fn phase_one_eval_builtins_are_registry_declared() { + for name in EVAL_DECLARATIVE_REGISTRY_BUILTINS { + assert!( + elephc_magician::builtin_metadata::php_visible_builtin_is_registry_declared(name), + "{name} should be declared through the eval builtin registry" + ); + } +} + /// Extracts lowercase PHP-symbol string literals from Rust source snippets. fn php_symbol_string_literals(sources: &[&str]) -> BTreeSet { let mut literals = BTreeSet::new(); From ef16715facfda430c048cf1e20c7e9d6796f9859 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 21:55:06 +0200 Subject: [PATCH 1059/1208] refactor(magician): migrate type builtins to eval registry --- .../interpreter/builtins/registry/binding.rs | 6 +-- .../builtins/registry/dispatch/scalars.rs | 23 +--------- .../src/interpreter/builtins/registry/mod.rs | 46 +++++++++++++++++-- .../interpreter/builtins/registry/names.rs | 23 ---------- .../src/interpreter/builtins/spec.rs | 44 ++++++++++++++---- .../src/interpreter/builtins/types/boolval.rs | 4 +- .../interpreter/builtins/types/floatval.rs | 16 +++++++ .../src/interpreter/builtins/types/gettype.rs | 16 +++++++ .../src/interpreter/builtins/types/intval.rs | 16 +++++++ .../interpreter/builtins/types/is_array.rs | 16 +++++++ .../src/interpreter/builtins/types/is_bool.rs | 16 +++++++ .../interpreter/builtins/types/is_double.rs | 16 +++++++ .../interpreter/builtins/types/is_finite.rs | 16 +++++++ .../interpreter/builtins/types/is_float.rs | 16 +++++++ .../interpreter/builtins/types/is_infinite.rs | 16 +++++++ .../src/interpreter/builtins/types/is_int.rs | 16 +++++++ .../interpreter/builtins/types/is_integer.rs | 16 +++++++ .../interpreter/builtins/types/is_iterable.rs | 16 +++++++ .../src/interpreter/builtins/types/is_long.rs | 16 +++++++ .../src/interpreter/builtins/types/is_nan.rs | 16 +++++++ .../src/interpreter/builtins/types/is_null.rs | 16 +++++++ .../interpreter/builtins/types/is_numeric.rs | 16 +++++++ .../interpreter/builtins/types/is_object.rs | 16 +++++++ .../src/interpreter/builtins/types/is_real.rs | 16 +++++++ .../interpreter/builtins/types/is_resource.rs | 16 +++++++ .../interpreter/builtins/types/is_scalar.rs | 16 +++++++ .../interpreter/builtins/types/is_string.rs | 16 +++++++ .../src/interpreter/builtins/types/mod.rs | 22 +++++++++ .../src/interpreter/builtins/types/strval.rs | 16 +++++++ .../src/interpreter/expressions.rs | 10 ---- tests/builtin_parity_tests.rs | 37 +++++++++++++-- 31 files changed, 486 insertions(+), 81 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/floatval.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/gettype.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/intval.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_array.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_double.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_float.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_int.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_long.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_null.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_object.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_real.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/is_string.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/strval.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 758baeaba4..5cb5de3c9c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -158,14 +158,10 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "grapheme_strrev" | "hex2bin" | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" | "urlencode" => Some(&["string"]), - "boolval" | "empty" | "floatval" | "gettype" | "intval" | "is_array" | "is_bool" - | "is_double" | "is_float" | "is_int" | "is_integer" | "is_iterable" | "is_long" - | "is_null" | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_string" - | "is_scalar" | "strval" => Some(&["value"]), + "empty" => Some(&["value"]), "is_callable" => Some(&["value", "syntax_only", "callable_name"]), "buffer_new" => Some(&["length"]), "buffer_len" | "buffer_free" => Some(&["buffer"]), - "is_finite" | "is_infinite" | "is_nan" => Some(&["num"]), "settype" => Some(&["var", "type"]), "get_called_class" => Some(&[]), "get_class" => Some(&["object"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs index 8516925d4b..d380d06ea3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs @@ -15,7 +15,7 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_scalars_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { @@ -78,34 +78,13 @@ pub(in crate::interpreter) fn eval_scalars_builtin_with_values( }; eval_settype_value_result(*value, *type_name, values)? } - "boolval" | "floatval" | "intval" | "strval" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_cast_result(name, *value, context, values)? - } "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, - "gettype" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gettype_result(*value, values)? - } "intdiv" => { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_intdiv_result(*left, *right, values)? } - "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" - | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_scalar" - | "is_string" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_type_predicate_result(name, *value, context, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 1c5782d038..3fd2aa72ee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -165,10 +165,38 @@ pub(in crate::interpreter) fn eval_declared_builtin_values_call( mod tests { use super::*; - /// Verifies all Phase 1 pilot builtins are present in the declarative registry. + /// Verifies migrated builtins are present in the declarative registry. #[test] - fn declared_builtin_registry_exposes_phase_one_pilots() { - for name in ["abs", "boolval", "count", "strlen", "strrev"] { + fn declared_builtin_registry_exposes_migrated_builtins() { + for name in [ + "abs", + "boolval", + "count", + "floatval", + "gettype", + "intval", + "is_array", + "is_bool", + "is_double", + "is_finite", + "is_float", + "is_infinite", + "is_int", + "is_integer", + "is_iterable", + "is_long", + "is_nan", + "is_null", + "is_numeric", + "is_object", + "is_real", + "is_resource", + "is_scalar", + "is_string", + "strlen", + "strrev", + "strval", + ] { assert!( eval_declared_builtin_exists(name), "{name} should be registered declaratively" @@ -176,9 +204,9 @@ mod tests { } } - /// Verifies pilot builtin metadata is derived from declarative specs. + /// Verifies migrated builtin metadata is derived from declarative specs. #[test] - fn declared_builtin_registry_derives_phase_one_metadata() { + fn declared_builtin_registry_derives_migrated_metadata() { assert_eq!( eval_declared_builtin_param_names("count"), Some(["value", "mode"].as_slice()) @@ -191,5 +219,13 @@ mod tests { eval_declared_builtin_param_names("strlen"), Some(["string"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("is_finite"), + Some(["num"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("is_object"), + Some(["value"].as_slice()) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index a9bc34f9f6..21701ca23a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -57,7 +57,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "base64_encode", "basename", "bin2hex", - "boolval", "buffer_free", "buffer_len", "buffer_new", @@ -128,7 +127,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "fileperms", "filesize", "filetype", - "floatval", "flock", "floor", "fmod", @@ -168,7 +166,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "getprotobynumber", "getservbyname", "getservbyport", - "gettype", "gmdate", "gmmktime", "glob", @@ -200,33 +197,14 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "inet_pton", "intdiv", "interface_exists", - "intval", "ip2long", "is_a", - "is_array", - "is_bool", "is_callable", "is_dir", - "is_double", "is_executable", "is_file", - "is_finite", - "is_float", - "is_infinite", - "is_int", - "is_integer", - "is_iterable", "is_link", - "is_long", - "is_nan", - "is_null", - "is_numeric", - "is_object", "is_readable", - "is_real", - "is_resource", - "is_scalar", - "is_string", "is_subclass_of", "is_writable", "is_writeable", @@ -405,7 +383,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "strtolower", "strtotime", "strtoupper", - "strval", "substr", "substr_replace", "symlink", diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index b7b2f19a46..87823751d2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -13,10 +13,14 @@ //! - Hook enums keep calls monomorphized over `RuntimeValueOps`. use super::super::{ - eval_builtin_count, eval_builtin_strlen, eval_count_result, ElephcEvalContext, ElephcEvalScope, - EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, + eval_builtin_count, eval_builtin_gettype, eval_builtin_strlen, eval_builtin_type_predicate, + eval_count_result, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, + RuntimeCellHandle, RuntimeValueOps, +}; +use super::{ + eval_builtin_abs, eval_builtin_cast, eval_builtin_strrev, eval_cast_result, + eval_gettype_result, eval_type_predicate_result, }; -use super::{eval_builtin_abs, eval_builtin_cast, eval_builtin_strrev, eval_cast_result}; pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; /// Broad domain used to group eval builtin home files. @@ -48,14 +52,18 @@ pub(in crate::interpreter) struct EvalParamSpec { pub(in crate::interpreter) enum EvalDirectHook { /// Dispatches `abs(...)`. Abs, - /// Dispatches `boolval(...)`. - Boolval, + /// Dispatches scalar cast builtins. + Cast, /// Dispatches `count(...)`. Count, + /// Dispatches `gettype(...)`. + Gettype, /// Dispatches `strlen(...)`. Strlen, /// Dispatches `strrev(...)`. Strrev, + /// Dispatches scalar and container type predicates. + TypePredicate, } /// Evaluated-argument dispatch hooks for migrated builtins. @@ -63,14 +71,18 @@ pub(in crate::interpreter) enum EvalDirectHook { pub(in crate::interpreter) enum EvalValuesHook { /// Dispatches `abs(...)`. Abs, - /// Dispatches `boolval(...)`. - Boolval, + /// Dispatches scalar cast builtins. + Cast, /// Dispatches `count(...)`. Count, + /// Dispatches `gettype(...)`. + Gettype, /// Dispatches `strlen(...)`. Strlen, /// Dispatches `strrev(...)`. Strrev, + /// Dispatches scalar and container type predicates. + TypePredicate, } /// Static declaration for one PHP-visible eval builtin. @@ -146,10 +158,12 @@ impl EvalDirectHook { ) -> Result { match self { Self::Abs => eval_builtin_abs(args, context, scope, values), - Self::Boolval => eval_builtin_cast(name, args, context, scope, values), + Self::Cast => eval_builtin_cast(name, args, context, scope, values), Self::Count => eval_builtin_count(args, context, scope, values), + Self::Gettype => eval_builtin_gettype(args, context, scope, values), Self::Strlen => eval_builtin_strlen(args, context, scope, values), Self::Strrev => eval_builtin_strrev(args, context, scope, values), + Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), } } } @@ -170,7 +184,7 @@ impl EvalValuesHook { }; values.abs(*value) } - Self::Boolval => { + Self::Cast => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; @@ -181,6 +195,12 @@ impl EvalValuesHook { [value, mode] => eval_count_result(*value, Some(*mode), context, values), _ => Err(EvalStatus::RuntimeFatal), }, + Self::Gettype => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gettype_result(*value, values) + } Self::Strlen => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -195,6 +215,12 @@ impl EvalValuesHook { }; values.strrev(*value) } + Self::TypePredicate => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_type_predicate_result(name, *value, context, values) + } } } } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs b/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs index 6595aaad56..e91203bbfa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs @@ -11,6 +11,6 @@ eval_builtin! { name: "boolval", area: Types, params: [value], - direct: Boolval, - values: Boolval, + direct: Cast, + values: Cast, } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs b/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs new file mode 100644 index 0000000000..55556021ec --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `floatval`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing scalar-cast hook. + +eval_builtin! { + name: "floatval", + area: Types, + params: [value], + direct: Cast, + values: Cast, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs b/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs new file mode 100644 index 0000000000..a2b4760e9c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `gettype`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-name hook. + +eval_builtin! { + name: "gettype", + area: Types, + params: [value], + direct: Gettype, + values: Gettype, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/intval.rs b/crates/elephc-magician/src/interpreter/builtins/types/intval.rs new file mode 100644 index 0000000000..1519ebdda0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/intval.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `intval`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing scalar-cast hook. + +eval_builtin! { + name: "intval", + area: Types, + params: [value], + direct: Cast, + values: Cast, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs new file mode 100644 index 0000000000..324b50104a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_array", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs new file mode 100644 index 0000000000..f0928c6c5e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_bool`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_bool", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs new file mode 100644 index 0000000000..0b3910dc0e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_double`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_double", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs new file mode 100644 index 0000000000..bfda4bc085 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_finite`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_finite", + area: Types, + params: [num], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs new file mode 100644 index 0000000000..ed1468a313 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_float`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_float", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs new file mode 100644 index 0000000000..c30dcb75e2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_infinite`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_infinite", + area: Types, + params: [num], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs new file mode 100644 index 0000000000..cfadd63c07 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_int`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_int", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs new file mode 100644 index 0000000000..09fc8b6e7e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_integer`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_integer", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs new file mode 100644 index 0000000000..ae459e4e78 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_iterable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_iterable", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs new file mode 100644 index 0000000000..0743ac5d79 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_long`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_long", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs new file mode 100644 index 0000000000..7151933d8d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_nan`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_nan", + area: Types, + params: [num], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs new file mode 100644 index 0000000000..7e42dda224 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_null`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_null", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs new file mode 100644 index 0000000000..c8835c6b02 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_numeric`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_numeric", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs new file mode 100644 index 0000000000..b6d06d9bd1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_object`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_object", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs new file mode 100644 index 0000000000..bf34df22db --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_real`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_real", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs new file mode 100644 index 0000000000..0840018721 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_resource`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_resource", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs new file mode 100644 index 0000000000..67b463dc59 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_scalar`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_scalar", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs new file mode 100644 index 0000000000..6f359cad72 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_string`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing type-predicate hook. + +eval_builtin! { + name: "is_string", + area: Types, + params: [value], + direct: TypePredicate, + values: TypePredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/mod.rs b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs index 7e23043c87..e976ba4309 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs @@ -9,3 +9,25 @@ //! - Leaf files register metadata through `eval_builtin!`. mod boolval; +mod floatval; +mod gettype; +mod intval; +mod is_array; +mod is_bool; +mod is_double; +mod is_finite; +mod is_float; +mod is_infinite; +mod is_int; +mod is_integer; +mod is_iterable; +mod is_long; +mod is_nan; +mod is_null; +mod is_numeric; +mod is_object; +mod is_real; +mod is_resource; +mod is_scalar; +mod is_string; +mod strval; diff --git a/crates/elephc-magician/src/interpreter/builtins/types/strval.rs b/crates/elephc-magician/src/interpreter/builtins/types/strval.rs new file mode 100644 index 0000000000..e17d9b24e2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/strval.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `strval`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing scalar-cast hook. + +eval_builtin! { + name: "strval", + area: Types, + params: [value], + direct: Cast, + values: Cast, +} diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index da6a695bd5..a50d890011 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1611,9 +1611,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_unary_directory(name, args, context, scope, values) } "chop" => eval_builtin_trim_like(name, args, context, scope, values), - "boolval" | "floatval" | "intval" | "strval" => { - eval_builtin_cast(name, args, context, scope, values) - } "count" => eval_builtin_count(args, context, scope, values), "copy" | "link" | "rename" | "symlink" => { eval_builtin_binary_path_bool(name, args, context, scope, values) @@ -1699,7 +1696,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), - "gettype" => eval_builtin_gettype(args, context, scope, values), "glob" => eval_builtin_glob(args, context, scope, values), "grapheme_strrev" => eval_builtin_grapheme_strrev(args, context, scope, values), "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { @@ -1733,12 +1729,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), "hrtime" => eval_builtin_hrtime(args, context, scope, values), "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), - "is_array" | "is_bool" | "is_double" | "is_finite" | "is_float" | "is_infinite" - | "is_int" | "is_integer" | "is_iterable" | "is_long" | "is_nan" | "is_null" - | "is_numeric" | "is_object" | "is_real" | "is_resource" | "is_scalar" - | "is_string" => { - eval_builtin_type_predicate(name, args, context, scope, values) - } "ip2long" => eval_builtin_ip2long(args, context, scope, values), "json_decode" => eval_builtin_json_decode(args, context, scope, values), "json_encode" => eval_builtin_json_encode(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index ff912397a4..96006e324c 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -96,9 +96,36 @@ const EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["is_callable", "preg /// Eval supports variadic debug output before the static backend does. const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; -/// Builtins migrated to magician's declarative eval registry during the Phase 1 pilot. -const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = - &["abs", "boolval", "count", "strlen", "strrev"]; +/// Builtins migrated to magician's declarative eval registry. +const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ + "abs", + "boolval", + "count", + "floatval", + "gettype", + "intval", + "is_array", + "is_bool", + "is_double", + "is_finite", + "is_float", + "is_infinite", + "is_int", + "is_integer", + "is_iterable", + "is_long", + "is_nan", + "is_null", + "is_numeric", + "is_object", + "is_real", + "is_resource", + "is_scalar", + "is_string", + "strlen", + "strrev", + "strval", +]; /// Verifies every static builtin is visible through eval's function lookup. #[test] @@ -147,9 +174,9 @@ fn static_php_visible_builtins_have_eval_dispatch_literals() { ); } -/// Verifies Phase 1 migrated builtins are backed by magician's declarative registry. +/// Verifies migrated builtins are backed by magician's declarative registry. #[test] -fn phase_one_eval_builtins_are_registry_declared() { +fn migrated_eval_builtins_are_registry_declared() { for name in EVAL_DECLARATIVE_REGISTRY_BUILTINS { assert!( elephc_magician::builtin_metadata::php_visible_builtin_is_registry_declared(name), From c8cb4c555a61ab44af21de6dd52632e47f9b804f Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 22:09:59 +0200 Subject: [PATCH 1060/1208] refactor(magician): migrate math builtins to eval registry --- .../builtins/formatting/number_format.rs | 14 ++ .../src/interpreter/builtins/macros.rs | 30 +++ .../src/interpreter/builtins/math/acos.rs | 16 ++ .../src/interpreter/builtins/math/asin.rs | 16 ++ .../src/interpreter/builtins/math/atan.rs | 16 ++ .../src/interpreter/builtins/math/atan2.rs | 16 ++ .../src/interpreter/builtins/math/ceil.rs | 16 ++ .../src/interpreter/builtins/math/clamp.rs | 16 ++ .../src/interpreter/builtins/math/cos.rs | 16 ++ .../src/interpreter/builtins/math/cosh.rs | 16 ++ .../src/interpreter/builtins/math/deg2rad.rs | 16 ++ .../src/interpreter/builtins/math/exp.rs | 16 ++ .../src/interpreter/builtins/math/fdiv.rs | 16 ++ .../src/interpreter/builtins/math/floor.rs | 16 ++ .../src/interpreter/builtins/math/fmod.rs | 16 ++ .../src/interpreter/builtins/math/hypot.rs | 16 ++ .../src/interpreter/builtins/math/intdiv.rs | 16 ++ .../src/interpreter/builtins/math/log.rs | 18 ++ .../src/interpreter/builtins/math/log10.rs | 16 ++ .../src/interpreter/builtins/math/log2.rs | 16 ++ .../src/interpreter/builtins/math/max.rs | 17 ++ .../src/interpreter/builtins/math/min.rs | 17 ++ .../src/interpreter/builtins/math/mod.rs | 29 +++ .../src/interpreter/builtins/math/pi.rs | 16 ++ .../src/interpreter/builtins/math/pow.rs | 16 ++ .../src/interpreter/builtins/math/rad2deg.rs | 16 ++ .../src/interpreter/builtins/math/round.rs | 18 ++ .../src/interpreter/builtins/math/sin.rs | 16 ++ .../src/interpreter/builtins/math/sinh.rs | 16 ++ .../src/interpreter/builtins/math/sqrt.rs | 16 ++ .../src/interpreter/builtins/math/tan.rs | 16 ++ .../src/interpreter/builtins/math/tanh.rs | 16 ++ .../interpreter/builtins/registry/binding.rs | 19 -- .../builtins/registry/dispatch/formatting.rs | 50 ----- .../builtins/registry/dispatch/scalars.rs | 49 ----- .../src/interpreter/builtins/registry/mod.rs | 43 +++- .../interpreter/builtins/registry/names.rs | 31 --- .../builtins/registry/signature.rs | 10 +- .../src/interpreter/builtins/spec.rs | 185 +++++++++++++++++- .../src/interpreter/expressions.rs | 18 -- tests/builtin_parity_tests.rs | 30 +++ 41 files changed, 793 insertions(+), 185 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/acos.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/asin.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/atan.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/atan2.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/ceil.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/clamp.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/cos.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/cosh.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/exp.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/floor.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/fmod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/hypot.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/log.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/log10.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/log2.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/max.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/min.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/pi.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/pow.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/round.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/sin.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/sinh.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/tan.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/tanh.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs index d28e7968fb..c80e56c982 100644 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs @@ -11,6 +11,20 @@ use super::super::super::*; use super::super::*; use super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "number_format", + area: Formatting, + params: [ + num, + decimals = EvalBuiltinDefaultValue::Int(0), + decimal_separator = EvalBuiltinDefaultValue::String("."), + thousands_separator = EvalBuiltinDefaultValue::String(","), + ], + direct: NumberFormat, + values: NumberFormat, +} /// Evaluates PHP `number_format(...)` over one number and optional separators. pub(in crate::interpreter) fn eval_builtin_number_format( diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index 11e45e224f..b6f0590ea7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -12,6 +12,36 @@ //! over `RuntimeValueOps`. macro_rules! eval_builtin { + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(= $default:expr)?),* $(,)?], + variadic: $variadic:ident, + direct: $direct:ident, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(stringify!($param),)* stringify!($variadic)], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: stringify!($param), + default: eval_builtin!(@default $($default)?), + by_ref: false, + }, + )* + ], + variadic: Some(stringify!($variadic)), + by_ref_params: &[], + direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + } + } + }; + ( name: $name:literal, area: $area:ident, diff --git a/crates/elephc-magician/src/interpreter/builtins/math/acos.rs b/crates/elephc-magician/src/interpreter/builtins/math/acos.rs new file mode 100644 index 0000000000..e5c57a264d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/acos.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `acos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "acos", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/asin.rs b/crates/elephc-magician/src/interpreter/builtins/math/asin.rs new file mode 100644 index 0000000000..96c4fa5759 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/asin.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `asin`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "asin", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/atan.rs b/crates/elephc-magician/src/interpreter/builtins/math/atan.rs new file mode 100644 index 0000000000..6afa0af30f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/atan.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `atan`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "atan", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs b/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs new file mode 100644 index 0000000000..2cfd46a231 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `atan2`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing paired float math hook. + +eval_builtin! { + name: "atan2", + area: Math, + params: [y, x], + direct: FloatPair, + values: FloatPair, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs b/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs new file mode 100644 index 0000000000..13394c0d98 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ceil`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing numeric rounding hook. + +eval_builtin! { + name: "ceil", + area: Math, + params: [num], + direct: Ceil, + values: Ceil, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs b/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs new file mode 100644 index 0000000000..552cb22f12 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `clamp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing clamp hook. + +eval_builtin! { + name: "clamp", + area: Math, + params: [value, min, max], + direct: Clamp, + values: Clamp, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/cos.rs b/crates/elephc-magician/src/interpreter/builtins/math/cos.rs new file mode 100644 index 0000000000..66229ff13b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/cos.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `cos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "cos", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs b/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs new file mode 100644 index 0000000000..aa7cec9324 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `cosh`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "cosh", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs b/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs new file mode 100644 index 0000000000..3eb2d7090c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `deg2rad`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "deg2rad", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/exp.rs b/crates/elephc-magician/src/interpreter/builtins/math/exp.rs new file mode 100644 index 0000000000..411c9a26d4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/exp.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `exp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "exp", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs b/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs new file mode 100644 index 0000000000..02b57bd79d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fdiv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing binary float math hook. + +eval_builtin! { + name: "fdiv", + area: Math, + params: [num1, num2], + direct: FloatBinary, + values: FloatBinary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/floor.rs b/crates/elephc-magician/src/interpreter/builtins/math/floor.rs new file mode 100644 index 0000000000..1ce804056c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/floor.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `floor`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing numeric rounding hook. + +eval_builtin! { + name: "floor", + area: Math, + params: [num], + direct: Floor, + values: Floor, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs b/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs new file mode 100644 index 0000000000..dab4a930f7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fmod`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing binary float math hook. + +eval_builtin! { + name: "fmod", + area: Math, + params: [num1, num2], + direct: FloatBinary, + values: FloatBinary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs b/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs new file mode 100644 index 0000000000..958cdea049 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `hypot`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing paired float math hook. + +eval_builtin! { + name: "hypot", + area: Math, + params: [x, y], + direct: FloatPair, + values: FloatPair, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs b/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs new file mode 100644 index 0000000000..14d27b307d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `intdiv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing integer-division hook. + +eval_builtin! { + name: "intdiv", + area: Math, + params: [num1, num2], + direct: Intdiv, + values: Intdiv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log.rs b/crates/elephc-magician/src/interpreter/builtins/math/log.rs new file mode 100644 index 0000000000..afd723d84c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/log.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `log`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing logarithm hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "log", + area: Math, + params: [num, base = EvalBuiltinDefaultValue::Float(std::f64::consts::E)], + direct: Log, + values: Log, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log10.rs b/crates/elephc-magician/src/interpreter/builtins/math/log10.rs new file mode 100644 index 0000000000..31cdb28c6c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/log10.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `log10`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "log10", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log2.rs b/crates/elephc-magician/src/interpreter/builtins/math/log2.rs new file mode 100644 index 0000000000..e4fb488ee6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/log2.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `log2`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "log2", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/max.rs b/crates/elephc-magician/src/interpreter/builtins/math/max.rs new file mode 100644 index 0000000000..822b762299 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/max.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `max`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing variadic comparison hook. + +eval_builtin! { + name: "max", + area: Math, + params: [value], + variadic: values, + direct: MinMax, + values: MinMax, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/min.rs b/crates/elephc-magician/src/interpreter/builtins/math/min.rs new file mode 100644 index 0000000000..fb25d75cb3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/min.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `min`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing variadic comparison hook. + +eval_builtin! { + name: "min", + area: Math, + params: [value], + variadic: values, + direct: MinMax, + values: MinMax, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs index 3b8fec2f10..1f0b445251 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs @@ -9,3 +9,32 @@ //! - Leaf files register metadata through `eval_builtin!`. mod abs; +mod acos; +mod asin; +mod atan; +mod atan2; +mod ceil; +mod clamp; +mod cos; +mod cosh; +mod deg2rad; +mod exp; +mod fdiv; +mod floor; +mod fmod; +mod hypot; +mod intdiv; +mod log; +mod log10; +mod log2; +mod max; +mod min; +mod pi; +mod pow; +mod rad2deg; +mod round; +mod sin; +mod sinh; +mod sqrt; +mod tan; +mod tanh; diff --git a/crates/elephc-magician/src/interpreter/builtins/math/pi.rs b/crates/elephc-magician/src/interpreter/builtins/math/pi.rs new file mode 100644 index 0000000000..ebe283b84c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/pi.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `pi`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing constant hook. + +eval_builtin! { + name: "pi", + area: Math, + params: [], + direct: Pi, + values: Pi, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/pow.rs b/crates/elephc-magician/src/interpreter/builtins/math/pow.rs new file mode 100644 index 0000000000..04b988cd2b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/pow.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `pow`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing exponentiation hook. + +eval_builtin! { + name: "pow", + area: Math, + params: [num, exponent], + direct: Pow, + values: Pow, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs b/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs new file mode 100644 index 0000000000..f32aad60e8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `rad2deg`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "rad2deg", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/round.rs b/crates/elephc-magician/src/interpreter/builtins/math/round.rs new file mode 100644 index 0000000000..7d6ebebc15 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/round.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `round`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing numeric rounding hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "round", + area: Math, + params: [num, precision = EvalBuiltinDefaultValue::Int(0)], + direct: Round, + values: Round, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sin.rs b/crates/elephc-magician/src/interpreter/builtins/math/sin.rs new file mode 100644 index 0000000000..32b96a1456 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/sin.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `sin`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "sin", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs b/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs new file mode 100644 index 0000000000..e6a57c6d39 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `sinh`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "sinh", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs b/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs new file mode 100644 index 0000000000..809130ae91 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `sqrt`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing square-root hook. + +eval_builtin! { + name: "sqrt", + area: Math, + params: [num], + direct: Sqrt, + values: Sqrt, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/tan.rs b/crates/elephc-magician/src/interpreter/builtins/math/tan.rs new file mode 100644 index 0000000000..eafd59207f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/tan.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `tan`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "tan", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs b/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs new file mode 100644 index 0000000000..4efbf057cd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `tanh`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing unary float math hook. + +eval_builtin! { + name: "tanh", + area: Math, + params: [num], + direct: FloatUnary, + values: FloatUnary, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 5cb5de3c9c..2a8e04e335 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -124,7 +124,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } match name { - "abs" | "ceil" | "floor" | "sqrt" => Some(&["num"]), "array_chunk" => Some(&["array", "length"]), "array_column" => Some(&["array", "column_key"]), "array_combine" => Some(&["keys", "values"]), @@ -151,9 +150,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), "array_slice" => Some(&["array", "offset", "length"]), "array_splice" => Some(&["array", "offset", "length", "replacement"]), - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => Some(&["num"]), - "atan2" => Some(&["y", "x"]), "basename" => Some(&["path", "suffix"]), "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "grapheme_strrev" | "hex2bin" | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" @@ -188,7 +184,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "chmod" => Some(&["filename", "permissions"]), "chr" => Some(&["codepoint"]), "closedir" | "readdir" | "rewinddir" => Some(&["dir_handle"]), - "clamp" => Some(&["value", "min", "max"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), @@ -206,7 +201,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "disk_free_space" | "disk_total_space" => Some(&["directory"]), "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), "explode" => Some(&["separator", "string", "limit"]), - "fdiv" | "fmod" => Some(&["num1", "num2"]), "fclose" | "fgetc" | "fgets" @@ -266,12 +260,10 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "http_response_code" => Some(&["response_code"]), "gzcompress" | "gzdeflate" => Some(&["data", "level"]), "gzinflate" | "gzuncompress" => Some(&["data", "max_length"]), - "hypot" => Some(&["x", "y"]), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), - "intdiv" => Some(&["num1", "num2"]), "iterator_apply" => Some(&["iterator", "callback", "args"]), "iterator_count" => Some(&["iterator"]), "iterator_to_array" => Some(&["iterator", "preserve_keys"]), @@ -283,8 +275,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "json_validate" => Some(&["json", "depth", "flags"]), "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), - "log" => Some(&["num", "base"]), - "max" | "min" => Some(&["value", "values"]), "md5" | "sha1" => Some(&["string", "binary"]), "localtime" => Some(&["timestamp", "associative"]), "microtime" => Some(&["as_float"]), @@ -292,20 +282,12 @@ pub(in crate::interpreter) fn eval_builtin_param_names( Some(&["hour", "minute", "second", "month", "day", "year"]) } "nl2br" => Some(&["string", "use_xhtml"]), - "number_format" => Some(&[ - "num", - "decimals", - "decimal_separator", - "thousands_separator", - ]), "ord" => Some(&["character"]), "pathinfo" => Some(&["path", "flags"]), - "pi" => Some(&[]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), "pclose" => Some(&["handle"]), "popen" => Some(&["command", "mode"]), - "pow" => Some(&["num", "exponent"]), "ptr" => Some(&["value"]), "ptr_null" => Some(&[]), "ptr_is_null" | "ptr_get" | "ptr_read8" | "ptr_read16" | "ptr_read32" => { @@ -331,7 +313,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "realpath" => Some(&["path"]), "stream_resolve_include_path" => Some(&["filename"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), - "round" => Some(&["num", "precision"]), "sleep" => Some(&["seconds"]), "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), "spl_autoload_unregister" => Some(&["callback"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs index 933c8904fe..bb7d9f66c0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs @@ -19,35 +19,6 @@ pub(in crate::interpreter) fn eval_formatting_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "ceil" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.ceil(*value)? - } - "floor" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.floor(*value)? - } - "pi" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI)? - } - "pow" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.pow(*left, *right)? - } - "round" => match evaluated_args { - [value] => values.round(*value, None)?, - [value, precision] => values.round(*value, Some(*precision))?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "sscanf" => { let [input, format, ..] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -55,27 +26,6 @@ pub(in crate::interpreter) fn eval_formatting_builtin_with_values( eval_sscanf_result(*input, *format, values)? } "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, - "number_format" => match evaluated_args { - [value] => eval_number_format_result(*value, None, None, None, values)?, - [value, decimals] => { - eval_number_format_result(*value, Some(*decimals), None, None, values)? - } - [value, decimals, decimal_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - None, - values, - )?, - [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - Some(*thousands_separator), - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, _ => return Ok(None), }; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs index d380d06ea3..568d6e501b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs @@ -19,42 +19,6 @@ pub(in crate::interpreter) fn eval_scalars_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "abs" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.abs(*value)? - } - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_unary_result(name, *value, values)? - } - "atan2" | "hypot" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_pair_result(name, *left, *right, values)? - } - "clamp" => { - let [value, min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_clamp_result(*value, *min, *max, values)? - } - "fdiv" | "fmod" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_binary_result(name, *left, *right, values)? - } - "log" => match evaluated_args { - [num] => eval_log_result(*num, None, values)?, - [num, base] => eval_log_result(*num, Some(*base), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "rand" | "mt_rand" => match evaluated_args { [] => eval_rand_result(None, None, values)?, [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, @@ -66,25 +30,12 @@ pub(in crate::interpreter) fn eval_scalars_builtin_with_values( }; eval_random_int_result(*min, *max, values)? } - "sqrt" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.sqrt(*value)? - } "settype" => { let [value, type_name] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_settype_value_result(*value, *type_name, values)? } - "max" | "min" => eval_min_max_result(name, evaluated_args, values)?, - "intdiv" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_intdiv_result(*left, *right, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 3fd2aa72ee..673e276db3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -65,8 +65,9 @@ fn build_declared_builtin_registry() -> DeclaredBuiltinRegistry { /// Validates static spec invariants before the registry is exposed. fn validate_declared_builtin_spec(spec: &EvalBuiltinSpec) { + let expected_param_names = spec.params.len() + usize::from(spec.variadic.is_some()); assert_eq!( - spec.params.len(), + expected_param_names, spec.param_names.len(), "eval builtin {} has mismatched params and param_names", spec.name @@ -86,6 +87,14 @@ fn validate_declared_builtin_spec(spec: &EvalBuiltinSpec) { ); } } + if let Some(variadic) = spec.variadic { + assert_eq!( + spec.param_names.last().copied(), + Some(variadic), + "eval builtin {} has a variadic name out of sync", + spec.name + ); + } let _ = spec.area(); } @@ -165,11 +174,12 @@ pub(in crate::interpreter) fn eval_declared_builtin_values_call( mod tests { use super::*; - /// Verifies migrated builtins are present in the declarative registry. + /// Verifies representative migrated builtins are present in the declarative registry. #[test] - fn declared_builtin_registry_exposes_migrated_builtins() { + fn declared_builtin_registry_exposes_representative_migrated_builtins() { for name in [ "abs", + "acos", "boolval", "count", "floatval", @@ -193,6 +203,9 @@ mod tests { "is_resource", "is_scalar", "is_string", + "log", + "min", + "number_format", "strlen", "strrev", "strval", @@ -227,5 +240,29 @@ mod tests { eval_declared_builtin_param_names("is_object"), Some(["value"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("log"), + Some(["num", "base"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("log", 1), + Some(EvalBuiltinDefaultValue::Float(std::f64::consts::E)) + ); + assert_eq!( + eval_declared_builtin_param_names("max"), + Some(["value", "values"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("number_format"), + Some( + [ + "num", + "decimals", + "decimal_separator", + "thousands_separator", + ] + .as_slice() + ) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 21701ca23a..90dc5f792c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -14,8 +14,6 @@ use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; /// PHP-visible builtin names implemented by the eval interpreter. pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[ - "abs", - "acos", "addslashes", "array_chunk", "array_column", @@ -49,10 +47,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "array_values", "array_walk", "arsort", - "asin", "asort", - "atan", - "atan2", "base64_decode", "base64_encode", "basename", @@ -62,14 +57,12 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "buffer_new", "call_user_func", "call_user_func_array", - "ceil", "chdir", "chgrp", "chmod", "chop", "chown", "chr", - "clamp", "class_alias", "class_attribute_args", "class_attribute_names", @@ -81,8 +74,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "clearstatcache", "closedir", "copy", - "cos", - "cosh", "count", "crc32", "ctype_alnum", @@ -95,7 +86,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "date_default_timezone_set", "define", "defined", - "deg2rad", "die", "dirname", "disk_free_space", @@ -104,11 +94,9 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "enum_exists", "exec", "exit", - "exp", "explode", "fclose", "fdatasync", - "fdiv", "feof", "fflush", "fgetc", @@ -128,8 +116,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "filesize", "filetype", "flock", - "floor", - "fmod", "fnmatch", "fopen", "fpassthru", @@ -190,12 +176,10 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "htmlspecialchars", "hrtime", "http_response_code", - "hypot", "implode", "in_array", "inet_ntop", "inet_pton", - "intdiv", "interface_exists", "ip2long", "is_a", @@ -225,24 +209,18 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "link", "linkinfo", "localtime", - "log", - "log10", - "log2", "long2ip", "lstat", "ltrim", - "max", "md5", "method_exists", "microtime", - "min", "mkdir", "mktime", "mt_rand", "natcasesort", "natsort", "nl2br", - "number_format", "opendir", "ord", "passthru", @@ -251,9 +229,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "pfsockopen", "php_uname", "phpversion", - "pi", "popen", - "pow", "ptr", "ptr_get", "ptr_is_null", @@ -278,7 +254,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "printf", "property_exists", "putenv", - "rad2deg", "rand", "random_int", "range", @@ -295,7 +270,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "rewind", "rewinddir", "rmdir", - "round", "rsort", "rtrim", "scandir", @@ -303,8 +277,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "sha1", "shell_exec", "shuffle", - "sin", - "sinh", "sleep", "sort", "spl_autoload", @@ -317,7 +289,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "spl_object_hash", "spl_object_id", "sprintf", - "sqrt", "sscanf", "stat", "str_contains", @@ -388,8 +359,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "symlink", "sys_get_temp_dir", "system", - "tan", - "tanh", "tempnam", "time", "tmpfile", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 415132534d..031416f28a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -102,7 +102,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "hash_hmac" => optional(params, 3), "hash_init" => optional(params, 1), "hash_final" | "md5" | "sha1" => optional(params, 1), - "number_format" => optional(params, 1), "array_pop" | "array_shift" => fixed_by_ref(params, &["array"]), "array_reverse" => optional(params, 1), @@ -122,8 +121,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "call_user_func" => variadic(params, &[]), - "log" | "round" | "date" | "gmdate" | "nl2br" | "strtotime" => optional(params, 1), - "min" | "max" => variadic(params, &[]), + "date" | "gmdate" | "nl2br" | "strtotime" => optional(params, 1), "json_encode" | "json_decode" | "json_validate" => optional(params, 1), "preg_match" | "preg_match_all" => optional_by_ref(params, 2, &["matches"]), @@ -220,10 +218,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("hash_init", 1) => Int(0), ("hash_init", 2) => String(""), ("hash_final" | "md5" | "sha1", 1) => Bool(false), - ("number_format", 1) => Int(0), - ("number_format", 2) => String("."), - ("number_format", 3) => String(","), - ("array_reverse", 1) => Bool(false), ("in_array" | "array_search", 2) => Bool(false), ("array_slice" | "array_splice", 2) => Null, @@ -232,8 +226,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("array_filter", 2) => Int(0), ("array_reduce", 2) => Null, - ("log", 1) => Float(std::f64::consts::E), - ("round", 1) => Int(0), ("date" | "gmdate", 1) => Null, ("strtotime", 1) => Null, ("nl2br", 1) => Bool(true), diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index 87823751d2..c0d8149a90 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -13,13 +13,18 @@ //! - Hook enums keep calls monomorphized over `RuntimeValueOps`. use super::super::{ - eval_builtin_count, eval_builtin_gettype, eval_builtin_strlen, eval_builtin_type_predicate, - eval_count_result, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, - RuntimeCellHandle, RuntimeValueOps, + eval_builtin_ceil, eval_builtin_clamp, eval_builtin_count, eval_builtin_float_binary, + eval_builtin_float_pair, eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, + eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, eval_builtin_number_format, + eval_builtin_pi, eval_builtin_pow, eval_builtin_round, eval_builtin_sqrt, eval_builtin_strlen, + eval_builtin_type_predicate, eval_count_result, ElephcEvalContext, ElephcEvalScope, EvalExpr, + EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::{ eval_builtin_abs, eval_builtin_cast, eval_builtin_strrev, eval_cast_result, - eval_gettype_result, eval_type_predicate_result, + eval_clamp_result, eval_float_binary_result, eval_float_pair_result, eval_float_unary_result, + eval_gettype_result, eval_intdiv_result, eval_log_result, eval_min_max_result, + eval_number_format_result, eval_type_predicate_result, }; pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; @@ -28,6 +33,8 @@ pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; pub(in crate::interpreter) enum EvalArea { /// Array and collection builtins. Array, + /// Formatting and display-oriented numeric builtins. + Formatting, /// Numeric and mathematical builtins. Math, /// String-processing builtins. @@ -54,10 +61,38 @@ pub(in crate::interpreter) enum EvalDirectHook { Abs, /// Dispatches scalar cast builtins. Cast, + /// Dispatches `ceil(...)`. + Ceil, + /// Dispatches `clamp(...)`. + Clamp, /// Dispatches `count(...)`. Count, + /// Dispatches binary floating-point builtins. + FloatBinary, + /// Dispatches paired floating-point builtins. + FloatPair, + /// Dispatches unary floating-point builtins. + FloatUnary, + /// Dispatches `floor(...)`. + Floor, /// Dispatches `gettype(...)`. Gettype, + /// Dispatches `intdiv(...)`. + Intdiv, + /// Dispatches `log(...)`. + Log, + /// Dispatches `min(...)` and `max(...)`. + MinMax, + /// Dispatches `number_format(...)`. + NumberFormat, + /// Dispatches `pi()`. + Pi, + /// Dispatches `pow(...)`. + Pow, + /// Dispatches `round(...)`. + Round, + /// Dispatches `sqrt(...)`. + Sqrt, /// Dispatches `strlen(...)`. Strlen, /// Dispatches `strrev(...)`. @@ -73,10 +108,38 @@ pub(in crate::interpreter) enum EvalValuesHook { Abs, /// Dispatches scalar cast builtins. Cast, + /// Dispatches `ceil(...)`. + Ceil, + /// Dispatches `clamp(...)`. + Clamp, /// Dispatches `count(...)`. Count, + /// Dispatches binary floating-point builtins. + FloatBinary, + /// Dispatches paired floating-point builtins. + FloatPair, + /// Dispatches unary floating-point builtins. + FloatUnary, + /// Dispatches `floor(...)`. + Floor, /// Dispatches `gettype(...)`. Gettype, + /// Dispatches `intdiv(...)`. + Intdiv, + /// Dispatches `log(...)`. + Log, + /// Dispatches `min(...)` and `max(...)`. + MinMax, + /// Dispatches `number_format(...)`. + NumberFormat, + /// Dispatches `pi()`. + Pi, + /// Dispatches `pow(...)`. + Pow, + /// Dispatches `round(...)`. + Round, + /// Dispatches `sqrt(...)`. + Sqrt, /// Dispatches `strlen(...)`. Strlen, /// Dispatches `strrev(...)`. @@ -121,10 +184,12 @@ impl EvalBuiltinSpec { /// Returns the number of parameters that define defaults. pub(in crate::interpreter) fn default_param_count(&self) -> usize { - self.params + let fixed_defaults = self + .params .iter() .filter(|param| param.default.is_some()) - .count() + .count(); + fixed_defaults + usize::from(self.variadic.is_some()) } /// Returns by-reference parameter names, checking they agree with param flags in debug builds. @@ -159,8 +224,22 @@ impl EvalDirectHook { match self { Self::Abs => eval_builtin_abs(args, context, scope, values), Self::Cast => eval_builtin_cast(name, args, context, scope, values), + Self::Ceil => eval_builtin_ceil(args, context, scope, values), + Self::Clamp => eval_builtin_clamp(args, context, scope, values), Self::Count => eval_builtin_count(args, context, scope, values), + Self::FloatBinary => eval_builtin_float_binary(name, args, context, scope, values), + Self::FloatPair => eval_builtin_float_pair(name, args, context, scope, values), + Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), + Self::Floor => eval_builtin_floor(args, context, scope, values), Self::Gettype => eval_builtin_gettype(args, context, scope, values), + Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), + Self::Log => eval_builtin_log(args, context, scope, values), + Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), + Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), + Self::Pi => eval_builtin_pi(args, values), + Self::Pow => eval_builtin_pow(args, context, scope, values), + Self::Round => eval_builtin_round(args, context, scope, values), + Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), Self::Strlen => eval_builtin_strlen(args, context, scope, values), Self::Strrev => eval_builtin_strrev(args, context, scope, values), Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), @@ -190,17 +269,111 @@ impl EvalValuesHook { }; eval_cast_result(name, *value, context, values) } + Self::Ceil => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.ceil(*value) + } + Self::Clamp => { + let [value, min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_clamp_result(*value, *min, *max, values) + } Self::Count => match evaluated_args { [value] => eval_count_result(*value, None, context, values), [value, mode] => eval_count_result(*value, Some(*mode), context, values), _ => Err(EvalStatus::RuntimeFatal), }, + Self::FloatBinary => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_binary_result(name, *left, *right, values) + } + Self::FloatPair => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_pair_result(name, *left, *right, values) + } + Self::FloatUnary => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_unary_result(name, *value, values) + } + Self::Floor => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.floor(*value) + } Self::Gettype => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_gettype_result(*value, values) } + Self::Intdiv => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_intdiv_result(*left, *right, values) + } + Self::Log => match evaluated_args { + [num] => eval_log_result(*num, None, values), + [num, base] => eval_log_result(*num, Some(*base), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::MinMax => eval_min_max_result(name, evaluated_args, values), + Self::NumberFormat => match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values), + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values) + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + ), + [value, decimals, decimal_separator, thousands_separator] => { + eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Pi => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI) + } + Self::Pow => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.pow(*left, *right) + } + Self::Round => match evaluated_args { + [value] => values.round(*value, None), + [value, precision] => values.round(*value, Some(*precision)), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Sqrt => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.sqrt(*value) + } Self::Strlen => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index a50d890011..eef9663b54 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1532,7 +1532,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } match name { - "abs" => eval_builtin_abs(args, context, scope, values), "addslashes" | "stripslashes" => eval_builtin_slashes(name, args, context, scope, values), "array_combine" => eval_builtin_array_combine(args, context, scope, values), "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), @@ -1566,22 +1565,15 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "array_slice" => eval_builtin_array_slice(args, context, scope, values), "array_unique" => eval_builtin_array_unique(args, context, scope, values), - "acos" | "asin" | "atan" | "cos" | "cosh" | "deg2rad" | "exp" | "log2" | "log10" - | "rad2deg" | "sin" | "sinh" | "tan" | "tanh" => { - eval_builtin_float_unary(name, args, context, scope, values) - } - "atan2" | "hypot" => eval_builtin_float_pair(name, args, context, scope, values), "base64_encode" => eval_builtin_base64_encode(args, context, scope, values), "base64_decode" => eval_builtin_base64_decode(args, context, scope, values), "basename" => eval_builtin_basename(args, context, scope, values), "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), - "ceil" => eval_builtin_ceil(args, context, scope, values), "chdir" | "mkdir" | "rmdir" => { eval_builtin_unary_path_bool(name, args, context, scope, values) } "chmod" => eval_builtin_chmod(args, context, scope, values), "chr" => eval_builtin_chr(args, context, scope, values), - "clamp" => eval_builtin_clamp(args, context, scope, values), "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), @@ -1640,7 +1632,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), - "fdiv" | "fmod" => eval_builtin_float_binary(name, args, context, scope, values), "file" => eval_builtin_file(args, context, scope, values), "file_exists" => eval_builtin_file_probe(name, args, context, scope, values), "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" @@ -1673,7 +1664,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), "fwrite" => eval_builtin_fwrite(args, context, scope, values), "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), - "floor" => eval_builtin_floor(args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(name, args, context, scope, values) } @@ -1721,7 +1711,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "implode" => eval_builtin_implode(args, context, scope, values), "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), - "intdiv" => eval_builtin_intdiv(args, context, scope, values), "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), @@ -1737,22 +1726,17 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "json_validate" => eval_builtin_json_validate(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), - "log" => eval_builtin_log(args, context, scope, values), - "max" | "min" => eval_builtin_min_max(name, args, context, scope, values), "localtime" => eval_builtin_localtime(args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), "mktime" | "gmmktime" => eval_builtin_mktime_like(name, args, context, scope, values), "nl2br" => eval_builtin_nl2br(args, context, scope, values), - "number_format" => eval_builtin_number_format(args, context, scope, values), "ord" => eval_builtin_ord(args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), - "pi" => eval_builtin_pi(args, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), "pclose" => eval_builtin_pclose(args, context, scope, values), "popen" => eval_builtin_popen(args, context, scope, values), - "pow" => eval_builtin_pow(args, context, scope, values), "preg_match" => eval_builtin_preg_match(args, context, scope, values), "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), @@ -1777,11 +1761,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "round" => eval_builtin_round(args, context, scope, values), "scandir" => eval_builtin_scandir(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "sleep" => eval_builtin_sleep(args, context, scope, values), - "sqrt" => eval_builtin_sqrt(args, context, scope, values), "spl_autoload_register" | "spl_autoload_unregister" => { eval_builtin_spl_autoload_bool(name, args, context, scope, values) } diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 96006e324c..35e6c6d194 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -99,10 +99,25 @@ const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; /// Builtins migrated to magician's declarative eval registry. const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "abs", + "acos", + "asin", + "atan", + "atan2", "boolval", + "ceil", + "clamp", + "cos", + "cosh", "count", + "deg2rad", + "exp", + "fdiv", "floatval", + "floor", + "fmod", "gettype", + "hypot", + "intdiv", "intval", "is_array", "is_bool", @@ -122,9 +137,24 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "is_resource", "is_scalar", "is_string", + "log", + "log10", + "log2", + "max", + "min", + "number_format", + "pi", + "pow", + "rad2deg", + "round", + "sin", + "sinh", + "sqrt", "strlen", "strrev", "strval", + "tan", + "tanh", ]; /// Verifies every static builtin is visible through eval's function lookup. From d12822bd851c2c3f79a4b9ebf5ad842b957a5bcf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 22:28:36 +0200 Subject: [PATCH 1061/1208] refactor(magician): migrate simple string builtins to eval registry --- .../src/interpreter/builtins/hooks.rs | 445 ++++++++++++++++++ .../src/interpreter/builtins/mod.rs | 1 + .../interpreter/builtins/registry/binding.rs | 9 +- .../builtins/registry/dispatch/strings.rs | 72 --- .../src/interpreter/builtins/registry/mod.rs | 15 + .../interpreter/builtins/registry/names.rs | 18 - .../src/interpreter/builtins/spec.rs | 296 +----------- .../interpreter/builtins/string/addslashes.rs | 16 + .../builtins/string/base64_decode.rs | 16 + .../builtins/string/base64_encode.rs | 16 + .../interpreter/builtins/string/bin2hex.rs | 16 + .../src/interpreter/builtins/string/chr.rs | 16 + .../src/interpreter/builtins/string/crc32.rs | 16 + .../builtins/string/ctype_alnum.rs | 16 + .../builtins/string/ctype_alpha.rs | 16 + .../builtins/string/ctype_digit.rs | 16 + .../builtins/string/ctype_space.rs | 16 + .../interpreter/builtins/string/hex2bin.rs | 16 + .../src/interpreter/builtins/string/mod.rs | 18 + .../src/interpreter/builtins/string/ord.rs | 16 + .../builtins/string/rawurldecode.rs | 16 + .../builtins/string/rawurlencode.rs | 16 + .../interpreter/builtins/string/str_repeat.rs | 16 + .../builtins/string/stripslashes.rs | 16 + .../interpreter/builtins/string/urldecode.rs | 16 + .../interpreter/builtins/string/urlencode.rs | 16 + .../src/interpreter/expressions.rs | 14 - tests/builtin_parity_tests.rs | 18 + 28 files changed, 787 insertions(+), 407 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/chr.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/crc32.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/ord.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks.rs b/crates/elephc-magician/src/interpreter/builtins/hooks.rs new file mode 100644 index 0000000000..61893de9a9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks.rs @@ -0,0 +1,445 @@ +//! Purpose: +//! Dispatch hook enums for eval builtins that have migrated into declarative +//! registry entries. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry` when a migrated builtin is invoked. +//! +//! Key details: +//! - Hooks keep the registry static while calls remain generic over +//! `RuntimeValueOps`. +//! - Existing direct and evaluated-result helpers still own PHP semantics. + +use super::super::{ + eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, + eval_builtin_ceil, eval_builtin_chr, eval_builtin_clamp, eval_builtin_count, + eval_builtin_crc32, eval_builtin_ctype, eval_builtin_float_binary, eval_builtin_float_pair, + eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, eval_builtin_hex2bin, + eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, eval_builtin_number_format, + eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, eval_builtin_round, eval_builtin_slashes, + eval_builtin_sqrt, eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_type_predicate, + eval_builtin_url_decode, eval_builtin_url_encode, eval_count_result, eval_ord_result, + ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, +}; +use super::{ + eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_builtin_abs, + eval_builtin_cast, eval_builtin_strrev, eval_cast_result, eval_chr_result, eval_clamp_result, + eval_crc32_result, eval_ctype_result, eval_float_binary_result, eval_float_pair_result, + eval_float_unary_result, eval_gettype_result, eval_hex2bin_result, eval_intdiv_result, + eval_log_result, eval_min_max_result, eval_number_format_result, eval_slashes_result, + eval_str_repeat_result, eval_type_predicate_result, eval_url_decode_result, + eval_url_encode_result, +}; + +/// Direct expression-level dispatch hooks for migrated builtins. +#[derive(Clone, Copy)] +pub(in crate::interpreter) enum EvalDirectHook { + /// Dispatches `abs(...)`. + Abs, + /// Dispatches `base64_decode(...)`. + Base64Decode, + /// Dispatches `base64_encode(...)`. + Base64Encode, + /// Dispatches `bin2hex(...)`. + Bin2Hex, + /// Dispatches scalar cast builtins. + Cast, + /// Dispatches `ceil(...)`. + Ceil, + /// Dispatches `chr(...)`. + Chr, + /// Dispatches `clamp(...)`. + Clamp, + /// Dispatches `count(...)`. + Count, + /// Dispatches `crc32(...)`. + Crc32, + /// Dispatches `ctype_*` predicates. + Ctype, + /// Dispatches binary floating-point builtins. + FloatBinary, + /// Dispatches paired floating-point builtins. + FloatPair, + /// Dispatches unary floating-point builtins. + FloatUnary, + /// Dispatches `floor(...)`. + Floor, + /// Dispatches `gettype(...)`. + Gettype, + /// Dispatches `hex2bin(...)`. + Hex2Bin, + /// Dispatches `intdiv(...)`. + Intdiv, + /// Dispatches `log(...)`. + Log, + /// Dispatches `min(...)` and `max(...)`. + MinMax, + /// Dispatches `number_format(...)`. + NumberFormat, + /// Dispatches `ord(...)`. + Ord, + /// Dispatches `pi()`. + Pi, + /// Dispatches `pow(...)`. + Pow, + /// Dispatches `round(...)`. + Round, + /// Dispatches `addslashes(...)` and `stripslashes(...)`. + Slashes, + /// Dispatches `sqrt(...)`. + Sqrt, + /// Dispatches `strlen(...)`. + Strlen, + /// Dispatches `str_repeat(...)`. + StrRepeat, + /// Dispatches `strrev(...)`. + Strrev, + /// Dispatches scalar and container type predicates. + TypePredicate, + /// Dispatches URL decode builtins. + UrlDecode, + /// Dispatches URL encode builtins. + UrlEncode, +} + +/// Evaluated-argument dispatch hooks for migrated builtins. +#[derive(Clone, Copy)] +pub(in crate::interpreter) enum EvalValuesHook { + /// Dispatches `abs(...)`. + Abs, + /// Dispatches `base64_decode(...)`. + Base64Decode, + /// Dispatches `base64_encode(...)`. + Base64Encode, + /// Dispatches `bin2hex(...)`. + Bin2Hex, + /// Dispatches scalar cast builtins. + Cast, + /// Dispatches `ceil(...)`. + Ceil, + /// Dispatches `chr(...)`. + Chr, + /// Dispatches `clamp(...)`. + Clamp, + /// Dispatches `count(...)`. + Count, + /// Dispatches `crc32(...)`. + Crc32, + /// Dispatches `ctype_*` predicates. + Ctype, + /// Dispatches binary floating-point builtins. + FloatBinary, + /// Dispatches paired floating-point builtins. + FloatPair, + /// Dispatches unary floating-point builtins. + FloatUnary, + /// Dispatches `floor(...)`. + Floor, + /// Dispatches `gettype(...)`. + Gettype, + /// Dispatches `hex2bin(...)`. + Hex2Bin, + /// Dispatches `intdiv(...)`. + Intdiv, + /// Dispatches `log(...)`. + Log, + /// Dispatches `min(...)` and `max(...)`. + MinMax, + /// Dispatches `number_format(...)`. + NumberFormat, + /// Dispatches `ord(...)`. + Ord, + /// Dispatches `pi()`. + Pi, + /// Dispatches `pow(...)`. + Pow, + /// Dispatches `round(...)`. + Round, + /// Dispatches `addslashes(...)` and `stripslashes(...)`. + Slashes, + /// Dispatches `sqrt(...)`. + Sqrt, + /// Dispatches `strlen(...)`. + Strlen, + /// Dispatches `str_repeat(...)`. + StrRepeat, + /// Dispatches `strrev(...)`. + Strrev, + /// Dispatches scalar and container type predicates. + TypePredicate, + /// Dispatches URL decode builtins. + UrlDecode, + /// Dispatches URL encode builtins. + UrlEncode, +} + +impl EvalDirectHook { + /// Runs a direct expression-level builtin call through the migrated hook. + pub(in crate::interpreter) fn call( + self, + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, + ) -> Result { + match self { + Self::Abs => eval_builtin_abs(args, context, scope, values), + Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), + Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), + Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), + Self::Cast => eval_builtin_cast(name, args, context, scope, values), + Self::Ceil => eval_builtin_ceil(args, context, scope, values), + Self::Chr => eval_builtin_chr(args, context, scope, values), + Self::Clamp => eval_builtin_clamp(args, context, scope, values), + Self::Count => eval_builtin_count(args, context, scope, values), + Self::Crc32 => eval_builtin_crc32(args, context, scope, values), + Self::Ctype => eval_builtin_ctype(name, args, context, scope, values), + Self::FloatBinary => eval_builtin_float_binary(name, args, context, scope, values), + Self::FloatPair => eval_builtin_float_pair(name, args, context, scope, values), + Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), + Self::Floor => eval_builtin_floor(args, context, scope, values), + Self::Gettype => eval_builtin_gettype(args, context, scope, values), + Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), + Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), + Self::Log => eval_builtin_log(args, context, scope, values), + Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), + Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), + Self::Ord => eval_builtin_ord(args, context, scope, values), + Self::Pi => eval_builtin_pi(args, values), + Self::Pow => eval_builtin_pow(args, context, scope, values), + Self::Round => eval_builtin_round(args, context, scope, values), + Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), + Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), + Self::Strlen => eval_builtin_strlen(args, context, scope, values), + Self::StrRepeat => eval_builtin_str_repeat(args, context, scope, values), + Self::Strrev => eval_builtin_strrev(args, context, scope, values), + Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), + Self::UrlDecode => eval_builtin_url_decode(name, args, context, scope, values), + Self::UrlEncode => eval_builtin_url_encode(name, args, context, scope, values), + } + } +} + +impl EvalValuesHook { + /// Runs an evaluated-argument builtin call through the migrated hook. + pub(in crate::interpreter) fn call( + self, + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + ) -> Result { + match self { + Self::Abs => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.abs(*value) + } + Self::Base64Decode => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_decode_result(*value, values) + } + Self::Base64Encode => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_base64_encode_result(*value, values) + } + Self::Bin2Hex => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_bin2hex_result(*value, values) + } + Self::Cast => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_cast_result(name, *value, context, values) + } + Self::Ceil => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.ceil(*value) + } + Self::Chr => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_chr_result(*value, values) + } + Self::Clamp => { + let [value, min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_clamp_result(*value, *min, *max, values) + } + Self::Count => match evaluated_args { + [value] => eval_count_result(*value, None, context, values), + [value, mode] => eval_count_result(*value, Some(*mode), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Crc32 => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_crc32_result(*value, values) + } + Self::Ctype => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ctype_result(name, *value, values) + } + Self::FloatBinary => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_binary_result(name, *left, *right, values) + } + Self::FloatPair => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_pair_result(name, *left, *right, values) + } + Self::FloatUnary => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_float_unary_result(name, *value, values) + } + Self::Floor => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.floor(*value) + } + Self::Gettype => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gettype_result(*value, values) + } + Self::Hex2Bin => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hex2bin_result(*value, values) + } + Self::Intdiv => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_intdiv_result(*left, *right, values) + } + Self::Log => match evaluated_args { + [num] => eval_log_result(*num, None, values), + [num, base] => eval_log_result(*num, Some(*base), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::MinMax => eval_min_max_result(name, evaluated_args, values), + Self::NumberFormat => match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values), + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values) + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + ), + [value, decimals, decimal_separator, thousands_separator] => { + eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Ord => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ord_result(*value, values) + } + Self::Pi => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.float(std::f64::consts::PI) + } + Self::Pow => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.pow(*left, *right) + } + Self::Round => match evaluated_args { + [value] => values.round(*value, None), + [value, precision] => values.round(*value, Some(*precision)), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Slashes => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_slashes_result(name, *value, values) + } + Self::Sqrt => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.sqrt(*value) + } + Self::Strlen => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let bytes = values.string_bytes(*value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) + } + Self::StrRepeat => { + let [value, times] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_repeat_result(*value, *times, values) + } + Self::Strrev => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + values.strrev(*value) + } + Self::TypePredicate => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_type_predicate_result(name, *value, context, values) + } + Self::UrlDecode => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_decode_result(name, *value, values) + } + Self::UrlEncode => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_url_encode_result(name, *value, values) + } + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index af32cb2f6c..9e74d63b02 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -19,6 +19,7 @@ mod arrays; mod class_metadata; mod filesystem; mod formatting; +mod hooks; mod math; mod network_env; mod process_control; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 2a8e04e335..ed97c0be41 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -151,9 +151,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "array_slice" => Some(&["array", "offset", "length"]), "array_splice" => Some(&["array", "offset", "length", "replacement"]), "basename" => Some(&["path", "suffix"]), - "addslashes" | "base64_decode" | "base64_encode" | "bin2hex" | "grapheme_strrev" - | "hex2bin" | "rawurldecode" | "rawurlencode" | "stripslashes" | "urldecode" - | "urlencode" => Some(&["string"]), + "grapheme_strrev" => Some(&["string"]), "empty" => Some(&["value"]), "is_callable" => Some(&["value", "syntax_only", "callable_name"]), "buffer_new" => Some(&["length"]), @@ -182,14 +180,11 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), "chdir" | "mkdir" | "opendir" | "rmdir" | "scandir" => Some(&["directory"]), "chmod" => Some(&["filename", "permissions"]), - "chr" => Some(&["codepoint"]), "closedir" | "readdir" | "rewinddir" => Some(&["dir_handle"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), "copy" | "rename" => Some(&["from", "to"]), - "crc32" => Some(&["string"]), - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => Some(&["text"]), "checkdate" => Some(&["month", "day", "year"]), "date" | "gmdate" => Some(&["format", "timestamp"]), "date_default_timezone_get" => Some(&[]), @@ -282,7 +277,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( Some(&["hour", "minute", "second", "month", "day", "year"]) } "nl2br" => Some(&["string", "use_xhtml"]), - "ord" => Some(&["character"]), "pathinfo" => Some(&["path", "flags"]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), @@ -369,7 +363,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject", "count"]), "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), - "str_repeat" => Some(&["string", "times"]), "str_split" => Some(&["string", "length"]), "substr" => Some(&["string", "offset", "length"]), "substr_replace" => Some(&["string", "replace", "offset", "length"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs index 20ab04d2a2..8be04bb10a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs @@ -19,36 +19,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "addslashes" | "stripslashes" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_slashes_result(name, *value, values)? - } - "base64_encode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_encode_result(*value, values)? - } - "base64_decode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_decode_result(*value, values)? - } - "bin2hex" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_bin2hex_result(*value, values)? - } - "chr" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chr_result(*value, values)? - } "grapheme_strrev" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -58,30 +28,12 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { eval_gzip_result(name, evaluated_args, values)? } - "rawurldecode" | "urldecode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_decode_result(name, *value, values)? - } - "rawurlencode" | "urlencode" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_encode_result(name, *value, values)? - } "strrev" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; values.strrev(*value)? } - "str_repeat" => { - let [value, times] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_repeat_result(*value, *times, values)? - } "str_replace" | "str_ireplace" => { let [search, replace, subject] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -117,30 +69,12 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, - "crc32" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_crc32_result(*value, values)? - } - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ctype_result(name, *value, values)? - } "explode" => { let [separator, string] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_explode_result(*separator, *string, values)? } - "ord" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ord_result(*value, values)? - } "implode" => { let [separator, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -207,12 +141,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( }; eval_hash_equals_result(*known, *user, values)? } - "hex2bin" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hex2bin_result(*value, values)? - } "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 673e276db3..438a9bfeea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -180,10 +180,15 @@ mod tests { for name in [ "abs", "acos", + "addslashes", "boolval", + "base64_encode", + "bin2hex", "count", + "ctype_alpha", "floatval", "gettype", + "hex2bin", "intval", "is_array", "is_bool", @@ -206,7 +211,9 @@ mod tests { "log", "min", "number_format", + "rawurlencode", "strlen", + "str_repeat", "strrev", "strval", ] { @@ -264,5 +271,13 @@ mod tests { .as_slice() ) ); + assert_eq!( + eval_declared_builtin_param_names("ctype_alpha"), + Some(["text"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("str_repeat"), + Some(["string", "times"].as_slice()) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 90dc5f792c..06a399a775 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -14,7 +14,6 @@ use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; /// PHP-visible builtin names implemented by the eval interpreter. pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[ - "addslashes", "array_chunk", "array_column", "array_combine", @@ -48,10 +47,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "array_walk", "arsort", "asort", - "base64_decode", - "base64_encode", "basename", - "bin2hex", "buffer_free", "buffer_len", "buffer_new", @@ -62,7 +58,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "chmod", "chop", "chown", - "chr", "class_alias", "class_attribute_args", "class_attribute_names", @@ -75,11 +70,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "closedir", "copy", "count", - "crc32", - "ctype_alnum", - "ctype_alpha", - "ctype_digit", - "ctype_space", "checkdate", "date", "date_default_timezone_get", @@ -170,7 +160,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "hash_init", "hash_update", "header", - "hex2bin", "html_entity_decode", "htmlentities", "htmlspecialchars", @@ -222,7 +211,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "natsort", "nl2br", "opendir", - "ord", "passthru", "pathinfo", "pclose", @@ -257,8 +245,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "rand", "random_int", "range", - "rawurldecode", - "rawurlencode", "readdir", "readfile", "readline", @@ -295,7 +281,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "str_ends_with", "str_ireplace", "str_pad", - "str_repeat", "str_replace", "str_split", "str_starts_with", @@ -345,7 +330,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_wrapper_register", "stream_wrapper_restore", "stream_wrapper_unregister", - "stripslashes", "strlen", "strpos", "strrev", @@ -372,8 +356,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "umask", "unlink", "unset", - "urldecode", - "urlencode", "usleep", "usort", "var_dump", diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index c0d8149a90..b05314835d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -12,20 +12,7 @@ //! themselves without growing a central match. //! - Hook enums keep calls monomorphized over `RuntimeValueOps`. -use super::super::{ - eval_builtin_ceil, eval_builtin_clamp, eval_builtin_count, eval_builtin_float_binary, - eval_builtin_float_pair, eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, - eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, eval_builtin_number_format, - eval_builtin_pi, eval_builtin_pow, eval_builtin_round, eval_builtin_sqrt, eval_builtin_strlen, - eval_builtin_type_predicate, eval_count_result, ElephcEvalContext, ElephcEvalScope, EvalExpr, - EvalStatus, RuntimeCellHandle, RuntimeValueOps, -}; -use super::{ - eval_builtin_abs, eval_builtin_cast, eval_builtin_strrev, eval_cast_result, - eval_clamp_result, eval_float_binary_result, eval_float_pair_result, eval_float_unary_result, - eval_gettype_result, eval_intdiv_result, eval_log_result, eval_min_max_result, - eval_number_format_result, eval_type_predicate_result, -}; +pub(in crate::interpreter) use super::hooks::{EvalDirectHook, EvalValuesHook}; pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; /// Broad domain used to group eval builtin home files. @@ -54,100 +41,6 @@ pub(in crate::interpreter) struct EvalParamSpec { pub(in crate::interpreter) by_ref: bool, } -/// Direct expression-level dispatch hooks for migrated builtins. -#[derive(Clone, Copy)] -pub(in crate::interpreter) enum EvalDirectHook { - /// Dispatches `abs(...)`. - Abs, - /// Dispatches scalar cast builtins. - Cast, - /// Dispatches `ceil(...)`. - Ceil, - /// Dispatches `clamp(...)`. - Clamp, - /// Dispatches `count(...)`. - Count, - /// Dispatches binary floating-point builtins. - FloatBinary, - /// Dispatches paired floating-point builtins. - FloatPair, - /// Dispatches unary floating-point builtins. - FloatUnary, - /// Dispatches `floor(...)`. - Floor, - /// Dispatches `gettype(...)`. - Gettype, - /// Dispatches `intdiv(...)`. - Intdiv, - /// Dispatches `log(...)`. - Log, - /// Dispatches `min(...)` and `max(...)`. - MinMax, - /// Dispatches `number_format(...)`. - NumberFormat, - /// Dispatches `pi()`. - Pi, - /// Dispatches `pow(...)`. - Pow, - /// Dispatches `round(...)`. - Round, - /// Dispatches `sqrt(...)`. - Sqrt, - /// Dispatches `strlen(...)`. - Strlen, - /// Dispatches `strrev(...)`. - Strrev, - /// Dispatches scalar and container type predicates. - TypePredicate, -} - -/// Evaluated-argument dispatch hooks for migrated builtins. -#[derive(Clone, Copy)] -pub(in crate::interpreter) enum EvalValuesHook { - /// Dispatches `abs(...)`. - Abs, - /// Dispatches scalar cast builtins. - Cast, - /// Dispatches `ceil(...)`. - Ceil, - /// Dispatches `clamp(...)`. - Clamp, - /// Dispatches `count(...)`. - Count, - /// Dispatches binary floating-point builtins. - FloatBinary, - /// Dispatches paired floating-point builtins. - FloatPair, - /// Dispatches unary floating-point builtins. - FloatUnary, - /// Dispatches `floor(...)`. - Floor, - /// Dispatches `gettype(...)`. - Gettype, - /// Dispatches `intdiv(...)`. - Intdiv, - /// Dispatches `log(...)`. - Log, - /// Dispatches `min(...)` and `max(...)`. - MinMax, - /// Dispatches `number_format(...)`. - NumberFormat, - /// Dispatches `pi()`. - Pi, - /// Dispatches `pow(...)`. - Pow, - /// Dispatches `round(...)`. - Round, - /// Dispatches `sqrt(...)`. - Sqrt, - /// Dispatches `strlen(...)`. - Strlen, - /// Dispatches `strrev(...)`. - Strrev, - /// Dispatches scalar and container type predicates. - TypePredicate, -} - /// Static declaration for one PHP-visible eval builtin. pub(in crate::interpreter) struct EvalBuiltinSpec { /// Canonical lowercase PHP builtin name. @@ -211,191 +104,4 @@ impl EvalBuiltinSpec { } } -impl EvalDirectHook { - /// Runs a direct expression-level builtin call through the migrated hook. - pub(in crate::interpreter) fn call( - self, - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, - ) -> Result { - match self { - Self::Abs => eval_builtin_abs(args, context, scope, values), - Self::Cast => eval_builtin_cast(name, args, context, scope, values), - Self::Ceil => eval_builtin_ceil(args, context, scope, values), - Self::Clamp => eval_builtin_clamp(args, context, scope, values), - Self::Count => eval_builtin_count(args, context, scope, values), - Self::FloatBinary => eval_builtin_float_binary(name, args, context, scope, values), - Self::FloatPair => eval_builtin_float_pair(name, args, context, scope, values), - Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), - Self::Floor => eval_builtin_floor(args, context, scope, values), - Self::Gettype => eval_builtin_gettype(args, context, scope, values), - Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), - Self::Log => eval_builtin_log(args, context, scope, values), - Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), - Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), - Self::Pi => eval_builtin_pi(args, values), - Self::Pow => eval_builtin_pow(args, context, scope, values), - Self::Round => eval_builtin_round(args, context, scope, values), - Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), - Self::Strlen => eval_builtin_strlen(args, context, scope, values), - Self::Strrev => eval_builtin_strrev(args, context, scope, values), - Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), - } - } -} - -impl EvalValuesHook { - /// Runs an evaluated-argument builtin call through the migrated hook. - pub(in crate::interpreter) fn call( - self, - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, - ) -> Result { - match self { - Self::Abs => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.abs(*value) - } - Self::Cast => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_cast_result(name, *value, context, values) - } - Self::Ceil => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.ceil(*value) - } - Self::Clamp => { - let [value, min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_clamp_result(*value, *min, *max, values) - } - Self::Count => match evaluated_args { - [value] => eval_count_result(*value, None, context, values), - [value, mode] => eval_count_result(*value, Some(*mode), context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - Self::FloatBinary => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_binary_result(name, *left, *right, values) - } - Self::FloatPair => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_pair_result(name, *left, *right, values) - } - Self::FloatUnary => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_unary_result(name, *value, values) - } - Self::Floor => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.floor(*value) - } - Self::Gettype => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gettype_result(*value, values) - } - Self::Intdiv => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_intdiv_result(*left, *right, values) - } - Self::Log => match evaluated_args { - [num] => eval_log_result(*num, None, values), - [num, base] => eval_log_result(*num, Some(*base), values), - _ => Err(EvalStatus::RuntimeFatal), - }, - Self::MinMax => eval_min_max_result(name, evaluated_args, values), - Self::NumberFormat => match evaluated_args { - [value] => eval_number_format_result(*value, None, None, None, values), - [value, decimals] => { - eval_number_format_result(*value, Some(*decimals), None, None, values) - } - [value, decimals, decimal_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - None, - values, - ), - [value, decimals, decimal_separator, thousands_separator] => { - eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - Some(*thousands_separator), - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - Self::Pi => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI) - } - Self::Pow => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.pow(*left, *right) - } - Self::Round => match evaluated_args { - [value] => values.round(*value, None), - [value, precision] => values.round(*value, Some(*precision)), - _ => Err(EvalStatus::RuntimeFatal), - }, - Self::Sqrt => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.sqrt(*value) - } - Self::Strlen => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let bytes = values.string_bytes(*value)?; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len) - } - Self::Strrev => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.strrev(*value) - } - Self::TypePredicate => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_type_predicate_result(name, *value, context, values) - } - } - } -} - inventory::collect!(EvalBuiltinSpec); diff --git a/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs b/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs new file mode 100644 index 0000000000..7e148bc672 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `addslashes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing slash escaping hook. + +eval_builtin! { + name: "addslashes", + area: String, + params: [string], + direct: Slashes, + values: Slashes, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs b/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs new file mode 100644 index 0000000000..153b535aea --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `base64_decode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing Base64 decode hook. + +eval_builtin! { + name: "base64_decode", + area: String, + params: [string], + direct: Base64Decode, + values: Base64Decode, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs b/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs new file mode 100644 index 0000000000..23873bb9c1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `base64_encode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing Base64 encode hook. + +eval_builtin! { + name: "base64_encode", + area: String, + params: [string], + direct: Base64Encode, + values: Base64Encode, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs b/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs new file mode 100644 index 0000000000..f4d195ac21 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `bin2hex`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing hex encode hook. + +eval_builtin! { + name: "bin2hex", + area: String, + params: [string], + direct: Bin2Hex, + values: Bin2Hex, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/chr.rs b/crates/elephc-magician/src/interpreter/builtins/string/chr.rs new file mode 100644 index 0000000000..c711937950 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/chr.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `chr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing byte-string hook. + +eval_builtin! { + name: "chr", + area: String, + params: [codepoint], + direct: Chr, + values: Chr, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs b/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs new file mode 100644 index 0000000000..dd7ee4b26e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `crc32`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing checksum hook. + +eval_builtin! { + name: "crc32", + area: String, + params: [string], + direct: Crc32, + values: Crc32, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs new file mode 100644 index 0000000000..1ac8b7a634 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ctype_alnum`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing ASCII ctype hook. + +eval_builtin! { + name: "ctype_alnum", + area: String, + params: [text], + direct: Ctype, + values: Ctype, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs new file mode 100644 index 0000000000..e3d0f0c588 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ctype_alpha`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing ASCII ctype hook. + +eval_builtin! { + name: "ctype_alpha", + area: String, + params: [text], + direct: Ctype, + values: Ctype, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs new file mode 100644 index 0000000000..dca25e6e84 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ctype_digit`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing ASCII ctype hook. + +eval_builtin! { + name: "ctype_digit", + area: String, + params: [text], + direct: Ctype, + values: Ctype, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs new file mode 100644 index 0000000000..8da38979ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ctype_space`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing ASCII ctype hook. + +eval_builtin! { + name: "ctype_space", + area: String, + params: [text], + direct: Ctype, + values: Ctype, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs b/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs new file mode 100644 index 0000000000..7d0e4951ae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `hex2bin`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing hex decode hook. + +eval_builtin! { + name: "hex2bin", + area: String, + params: [string], + direct: Hex2Bin, + values: Hex2Bin, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs index de708e0e6f..f2a3560ecd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -8,5 +8,23 @@ //! Key details: //! - Leaf files register metadata through `eval_builtin!`. +mod addslashes; +mod base64_decode; +mod base64_encode; +mod bin2hex; +mod chr; +mod crc32; +mod ctype_alnum; +mod ctype_alpha; +mod ctype_digit; +mod ctype_space; +mod hex2bin; +mod ord; +mod rawurldecode; +mod rawurlencode; mod strlen; +mod str_repeat; mod strrev; +mod stripslashes; +mod urldecode; +mod urlencode; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ord.rs b/crates/elephc-magician/src/interpreter/builtins/string/ord.rs new file mode 100644 index 0000000000..a086893c84 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ord.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ord`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing byte introspection hook. + +eval_builtin! { + name: "ord", + area: String, + params: [character], + direct: Ord, + values: Ord, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs b/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs new file mode 100644 index 0000000000..d61f9b30b6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `rawurldecode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing URL decode hook. + +eval_builtin! { + name: "rawurldecode", + area: String, + params: [string], + direct: UrlDecode, + values: UrlDecode, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs b/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs new file mode 100644 index 0000000000..9424cf6445 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `rawurlencode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing URL encode hook. + +eval_builtin! { + name: "rawurlencode", + area: String, + params: [string], + direct: UrlEncode, + values: UrlEncode, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs new file mode 100644 index 0000000000..71484e4a1d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `str_repeat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing repeat hook. + +eval_builtin! { + name: "str_repeat", + area: String, + params: [string, times], + direct: StrRepeat, + values: StrRepeat, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs b/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs new file mode 100644 index 0000000000..174133dd22 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stripslashes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing slash unescaping hook. + +eval_builtin! { + name: "stripslashes", + area: String, + params: [string], + direct: Slashes, + values: Slashes, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs b/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs new file mode 100644 index 0000000000..5e38fe99a4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `urldecode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing URL decode hook. + +eval_builtin! { + name: "urldecode", + area: String, + params: [string], + direct: UrlDecode, + values: UrlDecode, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs b/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs new file mode 100644 index 0000000000..8504090034 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `urlencode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the existing URL encode hook. + +eval_builtin! { + name: "urlencode", + area: String, + params: [string], + direct: UrlEncode, + values: UrlEncode, +} diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index eef9663b54..5157b1e12c 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1532,7 +1532,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } match name { - "addslashes" | "stripslashes" => eval_builtin_slashes(name, args, context, scope, values), "array_combine" => eval_builtin_array_combine(args, context, scope, values), "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), "array_column" => eval_builtin_array_column(args, context, scope, values), @@ -1565,15 +1564,11 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "array_slice" => eval_builtin_array_slice(args, context, scope, values), "array_unique" => eval_builtin_array_unique(args, context, scope, values), - "base64_encode" => eval_builtin_base64_encode(args, context, scope, values), - "base64_decode" => eval_builtin_base64_decode(args, context, scope, values), "basename" => eval_builtin_basename(args, context, scope, values), - "bin2hex" => eval_builtin_bin2hex(args, context, scope, values), "chdir" | "mkdir" | "rmdir" => { eval_builtin_unary_path_bool(name, args, context, scope, values) } "chmod" => eval_builtin_chmod(args, context, scope, values), - "chr" => eval_builtin_chr(args, context, scope, values), "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), @@ -1607,10 +1602,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "copy" | "link" | "rename" | "symlink" => { eval_builtin_binary_path_bool(name, args, context, scope, values) } - "crc32" => eval_builtin_crc32(args, context, scope, values), - "ctype_alnum" | "ctype_alpha" | "ctype_digit" | "ctype_space" => { - eval_builtin_ctype(name, args, context, scope, values) - } "checkdate" => eval_builtin_checkdate(args, context, scope, values), "date" | "gmdate" => eval_builtin_date_like(name, args, context, scope, values), "date_default_timezone_get" => { @@ -1704,7 +1695,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "hash_final" => eval_builtin_hash_final(args, context, scope, values), "hash_init" => eval_builtin_hash_init(args, context, scope, values), "hash_update" => eval_builtin_hash_update(args, context, scope, values), - "hex2bin" => eval_builtin_hex2bin(args, context, scope, values), "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { eval_builtin_html_entity(name, args, context, scope, values) } @@ -1730,7 +1720,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "microtime" => eval_builtin_microtime(args, context, scope, values), "mktime" | "gmmktime" => eval_builtin_mktime_like(name, args, context, scope, values), "nl2br" => eval_builtin_nl2br(args, context, scope, values), - "ord" => eval_builtin_ord(args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), @@ -1753,8 +1742,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), "random_int" => eval_builtin_random_int(args, context, scope, values), "range" => eval_builtin_range(args, context, scope, values), - "rawurldecode" | "urldecode" => eval_builtin_url_decode(name, args, context, scope, values), - "rawurlencode" | "urlencode" => eval_builtin_url_encode(name, args, context, scope, values), "readfile" => eval_builtin_readfile(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), @@ -1862,7 +1849,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "strtotime" => eval_builtin_strtotime(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "strrev" => eval_builtin_strrev(args, context, scope, values), - "str_repeat" => eval_builtin_str_repeat(args, context, scope, values), "str_replace" | "str_ireplace" => { eval_builtin_str_replace(name, args, context, scope, values) } diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 35e6c6d194..24287a922d 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -100,15 +100,25 @@ const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "abs", "acos", + "addslashes", "asin", "atan", "atan2", + "base64_decode", + "base64_encode", + "bin2hex", "boolval", "ceil", + "chr", "clamp", "cos", "cosh", "count", + "crc32", + "ctype_alnum", + "ctype_alpha", + "ctype_digit", + "ctype_space", "deg2rad", "exp", "fdiv", @@ -116,6 +126,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "floor", "fmod", "gettype", + "hex2bin", "hypot", "intdiv", "intval", @@ -143,18 +154,25 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "max", "min", "number_format", + "ord", "pi", "pow", "rad2deg", + "rawurldecode", + "rawurlencode", "round", "sin", "sinh", "sqrt", "strlen", + "str_repeat", "strrev", + "stripslashes", "strval", "tan", "tanh", + "urldecode", + "urlencode", ]; /// Verifies every static builtin is visible through eval's function lookup. From ca3693a1a80e8f1275912b2ecd39502a0ec85e5a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 22:45:38 +0200 Subject: [PATCH 1062/1208] refactor(magician): migrate stateless string builtins to eval registry --- .../src/interpreter/builtins/hooks/direct.rs | 192 ++++++++++++ .../src/interpreter/builtins/hooks/mod.rs | 15 + .../builtins/{hooks.rs => hooks/values.rs} | 293 +++++++++--------- .../src/interpreter/builtins/macros.rs | 18 +- .../interpreter/builtins/registry/binding.rs | 16 - .../builtins/registry/dispatch/strings.rs | 114 ------- .../src/interpreter/builtins/registry/mod.rs | 15 + .../interpreter/builtins/registry/names.rs | 27 -- .../builtins/registry/signature.rs | 25 +- .../src/interpreter/builtins/string/chop.rs | 18 ++ .../interpreter/builtins/string/lcfirst.rs | 16 + .../src/interpreter/builtins/string/ltrim.rs | 18 ++ .../src/interpreter/builtins/string/mod.rs | 25 ++ .../src/interpreter/builtins/string/nl2br.rs | 18 ++ .../src/interpreter/builtins/string/rtrim.rs | 18 ++ .../builtins/string/str_contains.rs | 16 + .../builtins/string/str_ends_with.rs | 16 + .../builtins/string/str_ireplace.rs | 18 ++ .../interpreter/builtins/string/str_pad.rs | 23 ++ .../builtins/string/str_replace.rs | 18 ++ .../interpreter/builtins/string/str_split.rs | 18 ++ .../builtins/string/str_starts_with.rs | 16 + .../interpreter/builtins/string/strcasecmp.rs | 16 + .../src/interpreter/builtins/string/strcmp.rs | 16 + .../src/interpreter/builtins/string/strpos.rs | 18 ++ .../interpreter/builtins/string/strrpos.rs | 18 ++ .../src/interpreter/builtins/string/strstr.rs | 18 ++ .../interpreter/builtins/string/strtolower.rs | 16 + .../interpreter/builtins/string/strtoupper.rs | 16 + .../src/interpreter/builtins/string/substr.rs | 18 ++ .../builtins/string/substr_replace.rs | 18 ++ .../src/interpreter/builtins/string/trim.rs | 18 ++ .../interpreter/builtins/string/ucfirst.rs | 16 + .../interpreter/builtins/string/ucwords.rs | 18 ++ .../interpreter/builtins/string/wordwrap.rs | 23 ++ .../src/interpreter/expressions.rs | 24 -- tests/builtin_parity_tests.rs | 25 ++ 37 files changed, 879 insertions(+), 352 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs rename crates/elephc-magician/src/interpreter/builtins/{hooks.rs => hooks/values.rs} (60%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/chop.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/str_split.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strpos.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strstr.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/substr.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/trim.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs new file mode 100644 index 0000000000..1a72e561f6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -0,0 +1,192 @@ +//! Purpose: +//! Direct expression-level dispatch hooks for eval builtins migrated into the +//! declarative registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::registry::eval_declared_builtin_direct_call`. +//! +//! Key details: +//! - Direct hooks preserve source-order evaluation in existing builtin helpers. +//! - Hook variants remain static metadata referenced from per-builtin files. + +use super::super::super::{ + eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, + eval_builtin_ceil, eval_builtin_chr, eval_builtin_clamp, eval_builtin_count, + eval_builtin_crc32, eval_builtin_ctype, eval_builtin_float_binary, eval_builtin_float_pair, + eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, eval_builtin_hex2bin, + eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, eval_builtin_number_format, + eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, eval_builtin_round, eval_builtin_slashes, + eval_builtin_sqrt, eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_type_predicate, + eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, + EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, +}; +use super::super::{ + eval_builtin_abs, eval_builtin_cast, eval_builtin_nl2br, eval_builtin_str_pad, + eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_string_case, + eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, + eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, + eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, +}; + +/// Direct expression-level dispatch hooks for migrated builtins. +#[derive(Clone, Copy)] +pub(in crate::interpreter) enum EvalDirectHook { + /// Dispatches `abs(...)`. + Abs, + /// Dispatches `base64_decode(...)`. + Base64Decode, + /// Dispatches `base64_encode(...)`. + Base64Encode, + /// Dispatches `bin2hex(...)`. + Bin2Hex, + /// Dispatches scalar cast builtins. + Cast, + /// Dispatches `ceil(...)`. + Ceil, + /// Dispatches `chr(...)`. + Chr, + /// Dispatches `clamp(...)`. + Clamp, + /// Dispatches `count(...)`. + Count, + /// Dispatches `crc32(...)`. + Crc32, + /// Dispatches `ctype_*` predicates. + Ctype, + /// Dispatches binary floating-point builtins. + FloatBinary, + /// Dispatches paired floating-point builtins. + FloatPair, + /// Dispatches unary floating-point builtins. + FloatUnary, + /// Dispatches `floor(...)`. + Floor, + /// Dispatches `gettype(...)`. + Gettype, + /// Dispatches `hex2bin(...)`. + Hex2Bin, + /// Dispatches `intdiv(...)`. + Intdiv, + /// Dispatches `log(...)`. + Log, + /// Dispatches `min(...)` and `max(...)`. + MinMax, + /// Dispatches `number_format(...)`. + NumberFormat, + /// Dispatches `ord(...)`. + Ord, + /// Dispatches `pi()`. + Pi, + /// Dispatches `pow(...)`. + Pow, + /// Dispatches `round(...)`. + Round, + /// Dispatches `addslashes(...)` and `stripslashes(...)`. + Slashes, + /// Dispatches `sqrt(...)`. + Sqrt, + /// Dispatches string ASCII case-conversion builtins. + StringCase, + /// Dispatches string comparison builtins. + StringCompare, + /// Dispatches string position builtins. + StringPosition, + /// Dispatches string search predicate builtins. + StringSearch, + /// Dispatches `str_pad(...)`. + StrPad, + /// Dispatches `str_replace(...)` and `str_ireplace(...)`. + StrReplace, + /// Dispatches `str_split(...)`. + StrSplit, + /// Dispatches `strlen(...)`. + Strlen, + /// Dispatches `str_repeat(...)`. + StrRepeat, + /// Dispatches `strrev(...)`. + Strrev, + /// Dispatches `strstr(...)`. + Strstr, + /// Dispatches `substr(...)`. + Substr, + /// Dispatches `substr_replace(...)`. + SubstrReplace, + /// Dispatches trim-family builtins. + TrimLike, + /// Dispatches scalar and container type predicates. + TypePredicate, + /// Dispatches `ucwords(...)`. + Ucwords, + /// Dispatches `nl2br(...)`. + Nl2br, + /// Dispatches `wordwrap(...)`. + Wordwrap, + /// Dispatches URL decode builtins. + UrlDecode, + /// Dispatches URL encode builtins. + UrlEncode, +} + +impl EvalDirectHook { + /// Runs a direct expression-level builtin call through the migrated hook. + pub(in crate::interpreter) fn call( + self, + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, + ) -> Result { + match self { + Self::Abs => eval_builtin_abs(args, context, scope, values), + Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), + Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), + Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), + Self::Cast => eval_builtin_cast(name, args, context, scope, values), + Self::Ceil => eval_builtin_ceil(args, context, scope, values), + Self::Chr => eval_builtin_chr(args, context, scope, values), + Self::Clamp => eval_builtin_clamp(args, context, scope, values), + Self::Count => eval_builtin_count(args, context, scope, values), + Self::Crc32 => eval_builtin_crc32(args, context, scope, values), + Self::Ctype => eval_builtin_ctype(name, args, context, scope, values), + Self::FloatBinary => eval_builtin_float_binary(name, args, context, scope, values), + Self::FloatPair => eval_builtin_float_pair(name, args, context, scope, values), + Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), + Self::Floor => eval_builtin_floor(args, context, scope, values), + Self::Gettype => eval_builtin_gettype(args, context, scope, values), + Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), + Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), + Self::Log => eval_builtin_log(args, context, scope, values), + Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), + Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), + Self::Ord => eval_builtin_ord(args, context, scope, values), + Self::Pi => eval_builtin_pi(args, values), + Self::Pow => eval_builtin_pow(args, context, scope, values), + Self::Round => eval_builtin_round(args, context, scope, values), + Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), + Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), + Self::StringCase => eval_builtin_string_case(name, args, context, scope, values), + Self::StringCompare => eval_builtin_string_compare(name, args, context, scope, values), + Self::StringPosition => { + eval_builtin_string_position(name, args, context, scope, values) + } + Self::StringSearch => eval_builtin_string_search(name, args, context, scope, values), + Self::StrPad => eval_builtin_str_pad(args, context, scope, values), + Self::StrReplace => eval_builtin_str_replace(name, args, context, scope, values), + Self::StrSplit => eval_builtin_str_split(args, context, scope, values), + Self::Strlen => eval_builtin_strlen(args, context, scope, values), + Self::StrRepeat => eval_builtin_str_repeat(args, context, scope, values), + Self::Strrev => eval_builtin_strrev(args, context, scope, values), + Self::Strstr => eval_builtin_strstr(args, context, scope, values), + Self::Substr => eval_builtin_substr(args, context, scope, values), + Self::SubstrReplace => eval_builtin_substr_replace(args, context, scope, values), + Self::TrimLike => eval_builtin_trim_like(name, args, context, scope, values), + Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), + Self::Ucwords => eval_builtin_ucwords(args, context, scope, values), + Self::Nl2br => eval_builtin_nl2br(args, context, scope, values), + Self::Wordwrap => eval_builtin_wordwrap(args, context, scope, values), + Self::UrlDecode => eval_builtin_url_decode(name, args, context, scope, values), + Self::UrlEncode => eval_builtin_url_encode(name, args, context, scope, values), + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs new file mode 100644 index 0000000000..504650ad17 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Groups declarative registry dispatch hooks for eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::spec` re-exports used by `eval_builtin!`. +//! +//! Key details: +//! - Direct expression dispatch and already-evaluated argument dispatch are kept +//! in separate files so each hook table can grow independently. + +mod direct; +mod values; + +pub(in crate::interpreter) use direct::EvalDirectHook; +pub(in crate::interpreter) use values::EvalValuesHook; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs similarity index 60% rename from crates/elephc-magician/src/interpreter/builtins/hooks.rs rename to crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 61893de9a9..31bd4dcef0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -1,107 +1,32 @@ //! Purpose: -//! Dispatch hook enums for eval builtins that have migrated into declarative -//! registry entries. +//! Already-evaluated argument dispatch hooks for eval builtins migrated into the +//! declarative registry. //! //! Called from: -//! - `crate::interpreter::builtins::registry` when a migrated builtin is invoked. +//! - `crate::interpreter::builtins::registry::eval_declared_builtin_values_call`. //! //! Key details: -//! - Hooks keep the registry static while calls remain generic over -//! `RuntimeValueOps`. -//! - Existing direct and evaluated-result helpers still own PHP semantics. +//! - Values hooks run after named/default argument binding has produced PHP +//! parameter order. +//! - Runtime-cell coercions stay in the existing builtin result helpers. -use super::super::{ - eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, - eval_builtin_ceil, eval_builtin_chr, eval_builtin_clamp, eval_builtin_count, - eval_builtin_crc32, eval_builtin_ctype, eval_builtin_float_binary, eval_builtin_float_pair, - eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, eval_builtin_hex2bin, - eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, eval_builtin_number_format, - eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, eval_builtin_round, eval_builtin_slashes, - eval_builtin_sqrt, eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_type_predicate, - eval_builtin_url_decode, eval_builtin_url_encode, eval_count_result, eval_ord_result, - ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, +use super::super::super::{ + eval_count_result, eval_ord_result, ElephcEvalContext, EvalStatus, RuntimeCellHandle, + RuntimeValueOps, }; -use super::{ - eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_builtin_abs, - eval_builtin_cast, eval_builtin_strrev, eval_cast_result, eval_chr_result, eval_clamp_result, - eval_crc32_result, eval_ctype_result, eval_float_binary_result, eval_float_pair_result, - eval_float_unary_result, eval_gettype_result, eval_hex2bin_result, eval_intdiv_result, - eval_log_result, eval_min_max_result, eval_number_format_result, eval_slashes_result, - eval_str_repeat_result, eval_type_predicate_result, eval_url_decode_result, - eval_url_encode_result, +use super::super::{ + eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, + eval_chr_result, eval_clamp_result, eval_crc32_result, eval_ctype_result, + eval_float_binary_result, eval_float_pair_result, eval_float_unary_result, + eval_gettype_result, eval_hex2bin_result, eval_intdiv_result, eval_log_result, + eval_min_max_result, eval_nl2br_result, eval_number_format_result, eval_slashes_result, + eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, + eval_string_case_result, eval_string_compare_result, eval_string_position_result, + eval_string_search_result, eval_strstr_result, eval_substr_replace_result, + eval_substr_result, eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, + eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; -/// Direct expression-level dispatch hooks for migrated builtins. -#[derive(Clone, Copy)] -pub(in crate::interpreter) enum EvalDirectHook { - /// Dispatches `abs(...)`. - Abs, - /// Dispatches `base64_decode(...)`. - Base64Decode, - /// Dispatches `base64_encode(...)`. - Base64Encode, - /// Dispatches `bin2hex(...)`. - Bin2Hex, - /// Dispatches scalar cast builtins. - Cast, - /// Dispatches `ceil(...)`. - Ceil, - /// Dispatches `chr(...)`. - Chr, - /// Dispatches `clamp(...)`. - Clamp, - /// Dispatches `count(...)`. - Count, - /// Dispatches `crc32(...)`. - Crc32, - /// Dispatches `ctype_*` predicates. - Ctype, - /// Dispatches binary floating-point builtins. - FloatBinary, - /// Dispatches paired floating-point builtins. - FloatPair, - /// Dispatches unary floating-point builtins. - FloatUnary, - /// Dispatches `floor(...)`. - Floor, - /// Dispatches `gettype(...)`. - Gettype, - /// Dispatches `hex2bin(...)`. - Hex2Bin, - /// Dispatches `intdiv(...)`. - Intdiv, - /// Dispatches `log(...)`. - Log, - /// Dispatches `min(...)` and `max(...)`. - MinMax, - /// Dispatches `number_format(...)`. - NumberFormat, - /// Dispatches `ord(...)`. - Ord, - /// Dispatches `pi()`. - Pi, - /// Dispatches `pow(...)`. - Pow, - /// Dispatches `round(...)`. - Round, - /// Dispatches `addslashes(...)` and `stripslashes(...)`. - Slashes, - /// Dispatches `sqrt(...)`. - Sqrt, - /// Dispatches `strlen(...)`. - Strlen, - /// Dispatches `str_repeat(...)`. - StrRepeat, - /// Dispatches `strrev(...)`. - Strrev, - /// Dispatches scalar and container type predicates. - TypePredicate, - /// Dispatches URL decode builtins. - UrlDecode, - /// Dispatches URL encode builtins. - UrlEncode, -} - /// Evaluated-argument dispatch hooks for migrated builtins. #[derive(Clone, Copy)] pub(in crate::interpreter) enum EvalValuesHook { @@ -159,68 +84,48 @@ pub(in crate::interpreter) enum EvalValuesHook { Slashes, /// Dispatches `sqrt(...)`. Sqrt, + /// Dispatches string ASCII case-conversion builtins. + StringCase, + /// Dispatches string comparison builtins. + StringCompare, + /// Dispatches string position builtins. + StringPosition, + /// Dispatches string search predicate builtins. + StringSearch, + /// Dispatches `str_pad(...)`. + StrPad, + /// Dispatches `str_replace(...)` and `str_ireplace(...)`. + StrReplace, + /// Dispatches `str_split(...)`. + StrSplit, /// Dispatches `strlen(...)`. Strlen, /// Dispatches `str_repeat(...)`. StrRepeat, /// Dispatches `strrev(...)`. Strrev, + /// Dispatches `strstr(...)`. + Strstr, + /// Dispatches `substr(...)`. + Substr, + /// Dispatches `substr_replace(...)`. + SubstrReplace, + /// Dispatches trim-family builtins. + TrimLike, /// Dispatches scalar and container type predicates. TypePredicate, + /// Dispatches `ucwords(...)`. + Ucwords, + /// Dispatches `nl2br(...)`. + Nl2br, + /// Dispatches `wordwrap(...)`. + Wordwrap, /// Dispatches URL decode builtins. UrlDecode, /// Dispatches URL encode builtins. UrlEncode, } -impl EvalDirectHook { - /// Runs a direct expression-level builtin call through the migrated hook. - pub(in crate::interpreter) fn call( - self, - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, - ) -> Result { - match self { - Self::Abs => eval_builtin_abs(args, context, scope, values), - Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), - Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), - Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), - Self::Cast => eval_builtin_cast(name, args, context, scope, values), - Self::Ceil => eval_builtin_ceil(args, context, scope, values), - Self::Chr => eval_builtin_chr(args, context, scope, values), - Self::Clamp => eval_builtin_clamp(args, context, scope, values), - Self::Count => eval_builtin_count(args, context, scope, values), - Self::Crc32 => eval_builtin_crc32(args, context, scope, values), - Self::Ctype => eval_builtin_ctype(name, args, context, scope, values), - Self::FloatBinary => eval_builtin_float_binary(name, args, context, scope, values), - Self::FloatPair => eval_builtin_float_pair(name, args, context, scope, values), - Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), - Self::Floor => eval_builtin_floor(args, context, scope, values), - Self::Gettype => eval_builtin_gettype(args, context, scope, values), - Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), - Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), - Self::Log => eval_builtin_log(args, context, scope, values), - Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), - Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), - Self::Ord => eval_builtin_ord(args, context, scope, values), - Self::Pi => eval_builtin_pi(args, values), - Self::Pow => eval_builtin_pow(args, context, scope, values), - Self::Round => eval_builtin_round(args, context, scope, values), - Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), - Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), - Self::Strlen => eval_builtin_strlen(args, context, scope, values), - Self::StrRepeat => eval_builtin_str_repeat(args, context, scope, values), - Self::Strrev => eval_builtin_strrev(args, context, scope, values), - Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), - Self::UrlDecode => eval_builtin_url_decode(name, args, context, scope, values), - Self::UrlEncode => eval_builtin_url_encode(name, args, context, scope, values), - } - } -} - impl EvalValuesHook { /// Runs an evaluated-argument builtin call through the migrated hook. pub(in crate::interpreter) fn call( @@ -402,6 +307,51 @@ impl EvalValuesHook { }; values.sqrt(*value) } + Self::StringCase => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_case_result(name, *value, values) + } + Self::StringCompare => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_compare_result(name, *left, *right, values) + } + Self::StringPosition => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_position_result(name, *haystack, *needle, values) + } + Self::StringSearch => { + let [haystack, needle] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_string_search_result(name, *haystack, *needle, values) + } + Self::StrPad => match evaluated_args { + [value, length] => eval_str_pad_result(*value, *length, None, None, values), + [value, length, pad_string] => { + eval_str_pad_result(*value, *length, Some(*pad_string), None, values) + } + [value, length, pad_string, pad_type] => { + eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StrReplace => { + let [search, replace, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_str_replace_result(name, *search, *replace, *subject, values) + } + Self::StrSplit => match evaluated_args { + [value] => eval_str_split_result(*value, None, values), + [value, length] => eval_str_split_result(*value, Some(*length), values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Strlen => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -422,12 +372,69 @@ impl EvalValuesHook { }; values.strrev(*value) } + Self::Strstr => match evaluated_args { + [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values), + [haystack, needle, before_needle] => { + let before_needle = values.truthy(*before_needle)?; + eval_strstr_result(*haystack, *needle, before_needle, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Substr => match evaluated_args { + [value, offset] => eval_substr_result(*value, *offset, None, values), + [value, offset, length] => { + eval_substr_result(*value, *offset, Some(*length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::SubstrReplace => match evaluated_args { + [value, replace, offset] => { + eval_substr_replace_result(*value, *replace, *offset, None, values) + } + [value, replace, offset, length] => { + eval_substr_replace_result(*value, *replace, *offset, Some(*length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::TrimLike => match evaluated_args { + [value] => eval_trim_like_result(name, *value, None, values), + [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::TypePredicate => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_type_predicate_result(name, *value, context, values) } + Self::Ucwords => match evaluated_args { + [value] => eval_ucwords_result(*value, None, values), + [value, separators] => eval_ucwords_result(*value, Some(*separators), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Nl2br => match evaluated_args { + [value] => eval_nl2br_result(*value, true, values), + [value, use_xhtml] => { + let use_xhtml = values.truthy(*use_xhtml)?; + eval_nl2br_result(*value, use_xhtml, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::Wordwrap => match evaluated_args { + [value] => eval_wordwrap_result(*value, None, None, None, values), + [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values), + [value, width, break_string] => { + eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values) + } + [value, width, break_string, cut] => eval_wordwrap_result( + *value, + Some(*width), + Some(*break_string), + Some(*cut), + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::UrlDecode => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index b6f0590ea7..13d65e407d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -24,17 +24,17 @@ macro_rules! eval_builtin { $crate::interpreter::builtins::spec::EvalBuiltinSpec { name: $name, area: $crate::interpreter::builtins::spec::EvalArea::$area, - param_names: &[$(stringify!($param),)* stringify!($variadic)], + param_names: &[$(eval_builtin!(@name_str $param),)* eval_builtin!(@name_str $variadic)], params: &[ $( $crate::interpreter::builtins::spec::EvalParamSpec { - name: stringify!($param), + name: eval_builtin!(@name_str $param), default: eval_builtin!(@default $($default)?), by_ref: false, }, )* ], - variadic: Some(stringify!($variadic)), + variadic: Some(eval_builtin!(@name_str $variadic)), by_ref_params: &[], direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), @@ -53,11 +53,11 @@ macro_rules! eval_builtin { $crate::interpreter::builtins::spec::EvalBuiltinSpec { name: $name, area: $crate::interpreter::builtins::spec::EvalArea::$area, - param_names: &[$(stringify!($param)),*], + param_names: &[$(eval_builtin!(@name_str $param)),*], params: &[ $( $crate::interpreter::builtins::spec::EvalParamSpec { - name: stringify!($param), + name: eval_builtin!(@name_str $param), default: eval_builtin!(@default $($default)?), by_ref: false, }, @@ -78,4 +78,12 @@ macro_rules! eval_builtin { (@default $default:expr) => { Some($default) }; + + (@name_str r#break) => { + "break" + }; + + (@name_str $name:ident) => { + stringify!($name) + }; } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index ed97c0be41..95192e8968 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -182,7 +182,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "chmod" => Some(&["filename", "permissions"]), "closedir" | "readdir" | "rewinddir" => Some(&["dir_handle"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), - "chop" | "ltrim" | "rtrim" | "trim" => Some(&["string", "characters"]), "count" => Some(&["value", "mode"]), "copy" | "rename" => Some(&["from", "to"]), "checkdate" => Some(&["month", "day", "year"]), @@ -276,7 +275,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "mktime" | "gmmktime" => { Some(&["hour", "minute", "second", "month", "day", "year"]) } - "nl2br" => Some(&["string", "use_xhtml"]), "pathinfo" => Some(&["path", "flags"]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), @@ -356,31 +354,17 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_socket_shutdown" => Some(&["stream", "mode"]), "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), - "strcasecmp" | "strcmp" => Some(&["string1", "string2"]), - "str_contains" | "str_ends_with" | "str_starts_with" => Some(&["haystack", "needle"]), "strtotime" => Some(&["datetime", "baseTimestamp"]), - "strstr" => Some(&["haystack", "needle", "before_needle"]), - "str_pad" => Some(&["string", "length", "pad_string", "pad_type"]), - "str_replace" | "str_ireplace" => Some(&["search", "replace", "subject", "count"]), - "strpos" | "strrpos" => Some(&["haystack", "needle", "offset"]), - "str_split" => Some(&["string", "length"]), - "substr" => Some(&["string", "offset", "length"]), - "substr_replace" => Some(&["string", "replace", "offset", "length"]), "sys_get_temp_dir" | "time" | "tmpfile" => Some(&[]), "tempnam" => Some(&["directory", "prefix"]), "touch" => Some(&["filename", "mtime", "atime"]), "chown" | "lchown" => Some(&["filename", "user"]), "chgrp" | "lchgrp" => Some(&["filename", "group"]), - "lcfirst" | "strlen" | "strrev" | "strtolower" | "strtoupper" | "ucfirst" => { - Some(&["string"]) - } "long2ip" => Some(&["ip"]), - "ucwords" => Some(&["string", "separators"]), "umask" => Some(&["mask"]), "usleep" => Some(&["microseconds"]), "vfprintf" => Some(&["stream", "format", "values"]), "vsprintf" | "vprintf" => Some(&["format", "values"]), - "wordwrap" => Some(&["string", "width", "break", "cut_long_words"]), _ => None, } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs index 8be04bb10a..ed42649a5b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs @@ -28,47 +28,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { eval_gzip_result(name, evaluated_args, values)? } - "strrev" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.strrev(*value)? - } - "str_replace" | "str_ireplace" => { - let [search, replace, subject] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_replace_result(name, *search, *replace, *subject, values)? - } - "str_pad" => match evaluated_args { - [value, length] => eval_str_pad_result(*value, *length, None, None, values)?, - [value, length, pad_string] => { - eval_str_pad_result(*value, *length, Some(*pad_string), None, values)? - } - [value, length, pad_string, pad_type] => { - eval_str_pad_result(*value, *length, Some(*pad_string), Some(*pad_type), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "str_split" => match evaluated_args { - [value] => eval_str_split_result(*value, None, values)?, - [value, length] => eval_str_split_result(*value, Some(*length), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "substr" => match evaluated_args { - [value, offset] => eval_substr_result(*value, *offset, None, values)?, - [value, offset, length] => eval_substr_result(*value, *offset, Some(*length), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "substr_replace" => match evaluated_args { - [value, replace, offset] => { - eval_substr_replace_result(*value, *replace, *offset, None, values)? - } - [value, replace, offset, length] => { - eval_substr_replace_result(*value, *replace, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, "explode" => { let [separator, string] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -81,19 +40,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( }; eval_implode_result(*separator, *array, values)? } - "nl2br" => match evaluated_args { - [value] => eval_nl2br_result(*value, true, values)?, - [value, use_xhtml] => { - let use_xhtml = values.truthy(*use_xhtml)?; - eval_nl2br_result(*value, use_xhtml, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "trim" | "ltrim" | "rtrim" | "chop" => match evaluated_args { - [value] => eval_trim_like_result(name, *value, None, values)?, - [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { eval_hash_one_shot_result(name, evaluated_args, values)? } @@ -147,66 +93,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( }; eval_html_entity_result(name, *value, values)? } - "strlen" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let bytes = values.string_bytes(*value)?; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len)? - } - "strpos" | "strrpos" => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_position_result(name, *haystack, *needle, values)? - } - "str_contains" | "str_starts_with" | "str_ends_with" => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_search_result(name, *haystack, *needle, values)? - } - "strstr" => match evaluated_args { - [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values)?, - [haystack, needle, before_needle] => { - let before_needle = values.truthy(*before_needle)?; - eval_strstr_result(*haystack, *needle, before_needle, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "strcmp" | "strcasecmp" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_compare_result(name, *left, *right, values)? - } - "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_case_result(name, *value, values)? - } - "ucwords" => match evaluated_args { - [value] => eval_ucwords_result(*value, None, values)?, - [value, separators] => eval_ucwords_result(*value, Some(*separators), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "wordwrap" => match evaluated_args { - [value] => eval_wordwrap_result(*value, None, None, None, values)?, - [value, width] => eval_wordwrap_result(*value, Some(*width), None, None, values)?, - [value, width, break_string] => { - eval_wordwrap_result(*value, Some(*width), Some(*break_string), None, values)? - } - [value, width, break_string, cut] => eval_wordwrap_result( - *value, - Some(*width), - Some(*break_string), - Some(*cut), - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 438a9bfeea..691f53964c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -210,12 +210,19 @@ mod tests { "is_string", "log", "min", + "nl2br", "number_format", "rawurlencode", + "str_contains", + "str_pad", + "str_replace", "strlen", "str_repeat", "strrev", + "substr", + "trim", "strval", + "wordwrap", ] { assert!( eval_declared_builtin_exists(name), @@ -279,5 +286,13 @@ mod tests { eval_declared_builtin_param_names("str_repeat"), Some(["string", "times"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("wordwrap"), + Some(["string", "width", "break", "cut_long_words"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("wordwrap", 2), + Some(EvalBuiltinDefaultValue::String("\n")) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 06a399a775..75b2b027e0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -56,7 +56,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "chdir", "chgrp", "chmod", - "chop", "chown", "class_alias", "class_attribute_args", @@ -192,7 +191,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "json_validate", "krsort", "ksort", - "lcfirst", "lchgrp", "lchown", "link", @@ -200,7 +198,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "localtime", "long2ip", "lstat", - "ltrim", "md5", "method_exists", "microtime", @@ -209,7 +206,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "mt_rand", "natcasesort", "natsort", - "nl2br", "opendir", "passthru", "pathinfo", @@ -257,7 +253,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "rewinddir", "rmdir", "rsort", - "rtrim", "scandir", "settype", "sha1", @@ -277,15 +272,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "sprintf", "sscanf", "stat", - "str_contains", - "str_ends_with", - "str_ireplace", - "str_pad", - "str_replace", - "str_split", - "str_starts_with", - "strcasecmp", - "strcmp", "stream_bucket_append", "stream_bucket_make_writeable", "stream_bucket_new", @@ -330,16 +316,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_wrapper_register", "stream_wrapper_restore", "stream_wrapper_unregister", - "strlen", - "strpos", - "strrev", - "strrpos", - "strstr", - "strtolower", "strtotime", - "strtoupper", - "substr", - "substr_replace", "symlink", "sys_get_temp_dir", "system", @@ -348,10 +325,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "tmpfile", "touch", "trait_exists", - "trim", "uasort", - "ucfirst", - "ucwords", "uksort", "umask", "unlink", @@ -362,7 +336,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "vfprintf", "vprintf", "vsprintf", - "wordwrap", ]; /// Combined PHP-visible builtin names from legacy and declarative registries. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 031416f28a..9c74262a5f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -86,15 +86,8 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( optional(params, 0) } - "trim" | "ltrim" | "rtrim" | "chop" | "ucwords" | "str_split" | "wordwrap" => { - optional(params, 1) - } - "substr" | "strpos" | "strrpos" | "strstr" | "explode" | "str_pad" => { - optional(params, 2) - } - "str_replace" | "str_ireplace" => optional(params, 3), + "explode" => optional(params, 2), "implode" => optional(params, 1), - "substr_replace" => optional(params, 3), "sprintf" | "printf" | "sscanf" => variadic(params, &[]), "fprintf" | "fscanf" => variadic(params, &[]), @@ -121,7 +114,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "call_user_func" => variadic(params, &[]), - "date" | "gmdate" | "nl2br" | "strtotime" => optional(params, 1), + "date" | "gmdate" | "strtotime" => optional(params, 1), "json_encode" | "json_decode" | "json_validate" => optional(params, 1), "preg_match" | "preg_match_all" => optional_by_ref(params, 2, &["matches"]), @@ -197,21 +190,8 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("readline" | "umask", 0) => Null, ("exit" | "die", 0) => Int(0), - ("trim" | "ltrim" | "rtrim" | "chop", 1) => Bytes(b" \n\r\t\x0b\x0c\0"), - ("ucwords", 1) => Bytes(b" \t\r\n\x0c\x0b"), - ("substr", 2) => Null, - ("strpos" | "strrpos", 2) => Int(0), - ("strstr", 2) => Bool(false), - ("str_replace" | "str_ireplace", 3) => Null, ("explode", 2) => Int(i64::MAX), ("implode", 0) => Null, - ("substr_replace", 3) => Null, - ("str_pad", 2) => String(" "), - ("str_pad", 3) => Int(1), - ("str_split", 1) => Int(1), - ("wordwrap", 1) => Int(75), - ("wordwrap", 2) => String("\n"), - ("wordwrap", 3) => Bool(false), ("hash" | "hash_file", 2) => Bool(false), ("hash_hmac", 3) => Bool(false), @@ -228,7 +208,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("date" | "gmdate", 1) => Null, ("strtotime", 1) => Null, - ("nl2br", 1) => Bool(true), ("json_encode", 1) => Int(0), ("json_encode", 2) => Int(512), ("json_decode", 1) => Null, diff --git a/crates/elephc-magician/src/interpreter/builtins/string/chop.rs b/crates/elephc-magician/src/interpreter/builtins/string/chop.rs new file mode 100644 index 0000000000..f5f93556ec --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/chop.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `chop`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the trim-family hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "chop", + area: String, + params: [string, characters = EvalBuiltinDefaultValue::Bytes(b" \n\r\t\x0b\x0c\0")], + direct: TrimLike, + values: TrimLike, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs b/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs new file mode 100644 index 0000000000..0e0bf6cb01 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `lcfirst`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-case hook. + +eval_builtin! { + name: "lcfirst", + area: String, + params: [string], + direct: StringCase, + values: StringCase, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs b/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs new file mode 100644 index 0000000000..45cc57ab9b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `ltrim`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the trim-family hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "ltrim", + area: String, + params: [string, characters = EvalBuiltinDefaultValue::Bytes(b" \n\r\t\x0b\x0c\0")], + direct: TrimLike, + values: TrimLike, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs index f2a3560ecd..4e0dadbba0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -12,6 +12,7 @@ mod addslashes; mod base64_decode; mod base64_encode; mod bin2hex; +mod chop; mod chr; mod crc32; mod ctype_alnum; @@ -19,12 +20,36 @@ mod ctype_alpha; mod ctype_digit; mod ctype_space; mod hex2bin; +mod lcfirst; +mod ltrim; +mod nl2br; mod ord; mod rawurldecode; mod rawurlencode; +mod rtrim; +mod str_contains; +mod str_ends_with; +mod str_ireplace; +mod str_pad; +mod str_replace; mod strlen; mod str_repeat; +mod str_split; +mod str_starts_with; mod strrev; mod stripslashes; +mod strcasecmp; +mod strcmp; +mod strpos; +mod strrpos; +mod strstr; +mod strtolower; +mod strtoupper; +mod substr; +mod substr_replace; +mod trim; +mod ucfirst; +mod ucwords; mod urldecode; mod urlencode; +mod wordwrap; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs b/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs new file mode 100644 index 0000000000..24fd059c9e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `nl2br`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the newline-to-break hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "nl2br", + area: String, + params: [string, use_xhtml = EvalBuiltinDefaultValue::Bool(true)], + direct: Nl2br, + values: Nl2br, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs b/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs new file mode 100644 index 0000000000..f90ba44319 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `rtrim`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the trim-family hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "rtrim", + area: String, + params: [string, characters = EvalBuiltinDefaultValue::Bytes(b" \n\r\t\x0b\x0c\0")], + direct: TrimLike, + values: TrimLike, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs new file mode 100644 index 0000000000..43bb2f962c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `str_contains`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-search predicate hook. + +eval_builtin! { + name: "str_contains", + area: String, + params: [haystack, needle], + direct: StringSearch, + values: StringSearch, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs new file mode 100644 index 0000000000..4433f7a4d5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `str_ends_with`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-search predicate hook. + +eval_builtin! { + name: "str_ends_with", + area: String, + params: [haystack, needle], + direct: StringSearch, + values: StringSearch, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs new file mode 100644 index 0000000000..4361699d7b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `str_ireplace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-replace hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_ireplace", + area: String, + params: [search, replace, subject, count = EvalBuiltinDefaultValue::Null], + direct: StrReplace, + values: StrReplace, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs new file mode 100644 index 0000000000..e8502a3409 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `str_pad`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-pad hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_pad", + area: String, + params: [ + string, + length, + pad_string = EvalBuiltinDefaultValue::String(" "), + pad_type = EvalBuiltinDefaultValue::Int(1), + ], + direct: StrPad, + values: StrPad, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs new file mode 100644 index 0000000000..326c73fc4f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `str_replace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-replace hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_replace", + area: String, + params: [search, replace, subject, count = EvalBuiltinDefaultValue::Null], + direct: StrReplace, + values: StrReplace, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs new file mode 100644 index 0000000000..560d43c97c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `str_split`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-split hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "str_split", + area: String, + params: [string, length = EvalBuiltinDefaultValue::Int(1)], + direct: StrSplit, + values: StrSplit, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs new file mode 100644 index 0000000000..1c29ae7f10 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `str_starts_with`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-search predicate hook. + +eval_builtin! { + name: "str_starts_with", + area: String, + params: [haystack, needle], + direct: StringSearch, + values: StringSearch, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs b/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs new file mode 100644 index 0000000000..29096627d2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `strcasecmp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-compare hook. + +eval_builtin! { + name: "strcasecmp", + area: String, + params: [string1, string2], + direct: StringCompare, + values: StringCompare, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs b/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs new file mode 100644 index 0000000000..efa45a0790 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `strcmp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-compare hook. + +eval_builtin! { + name: "strcmp", + area: String, + params: [string1, string2], + direct: StringCompare, + values: StringCompare, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs new file mode 100644 index 0000000000..c0e0bc012b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `strpos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-position hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strpos", + area: String, + params: [haystack, needle, offset = EvalBuiltinDefaultValue::Int(0)], + direct: StringPosition, + values: StringPosition, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs new file mode 100644 index 0000000000..a9704e3f80 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `strrpos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-position hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strrpos", + area: String, + params: [haystack, needle, offset = EvalBuiltinDefaultValue::Int(0)], + direct: StringPosition, + values: StringPosition, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs b/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs new file mode 100644 index 0000000000..ccd3e4f225 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `strstr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the strstr hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strstr", + area: String, + params: [haystack, needle, before_needle = EvalBuiltinDefaultValue::Bool(false)], + direct: Strstr, + values: Strstr, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs b/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs new file mode 100644 index 0000000000..585ec8c57c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `strtolower`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-case hook. + +eval_builtin! { + name: "strtolower", + area: String, + params: [string], + direct: StringCase, + values: StringCase, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs b/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs new file mode 100644 index 0000000000..9513a8af7d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `strtoupper`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-case hook. + +eval_builtin! { + name: "strtoupper", + area: String, + params: [string], + direct: StringCase, + values: StringCase, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/substr.rs b/crates/elephc-magician/src/interpreter/builtins/string/substr.rs new file mode 100644 index 0000000000..032ae8abe7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/substr.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `substr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the substring hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "substr", + area: String, + params: [string, offset, length = EvalBuiltinDefaultValue::Null], + direct: Substr, + values: Substr, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs b/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs new file mode 100644 index 0000000000..cde861329b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `substr_replace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the substring-replace hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "substr_replace", + area: String, + params: [string, replace, offset, length = EvalBuiltinDefaultValue::Null], + direct: SubstrReplace, + values: SubstrReplace, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/trim.rs b/crates/elephc-magician/src/interpreter/builtins/string/trim.rs new file mode 100644 index 0000000000..472812d550 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/trim.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `trim`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the trim-family hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "trim", + area: String, + params: [string, characters = EvalBuiltinDefaultValue::Bytes(b" \n\r\t\x0b\x0c\0")], + direct: TrimLike, + values: TrimLike, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs b/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs new file mode 100644 index 0000000000..8ebe1a805c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ucfirst`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string-case hook. + +eval_builtin! { + name: "ucfirst", + area: String, + params: [string], + direct: StringCase, + values: StringCase, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs b/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs new file mode 100644 index 0000000000..d94d2fbf8e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `ucwords`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the ucwords hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "ucwords", + area: String, + params: [string, separators = EvalBuiltinDefaultValue::Bytes(b" \t\r\n\x0c\x0b")], + direct: Ucwords, + values: Ucwords, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs b/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs new file mode 100644 index 0000000000..458047498d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `wordwrap`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the wordwrap hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "wordwrap", + area: String, + params: [ + string, + width = EvalBuiltinDefaultValue::Int(75), + r#break = EvalBuiltinDefaultValue::String("\n"), + cut_long_words = EvalBuiltinDefaultValue::Bool(false), + ], + direct: Wordwrap, + values: Wordwrap, +} diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 5157b1e12c..f511f257e5 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1597,7 +1597,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "closedir" | "readdir" | "rewinddir" => { eval_builtin_unary_directory(name, args, context, scope, values) } - "chop" => eval_builtin_trim_like(name, args, context, scope, values), "count" => eval_builtin_count(args, context, scope, values), "copy" | "link" | "rename" | "symlink" => { eval_builtin_binary_path_bool(name, args, context, scope, values) @@ -1715,11 +1714,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "json_last_error_msg" => eval_builtin_json_last_error_msg(args, context, values), "json_validate" => eval_builtin_json_validate(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), - "ltrim" | "rtrim" => eval_builtin_trim_like(name, args, context, scope, values), "localtime" => eval_builtin_localtime(args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), "mktime" | "gmmktime" => eval_builtin_mktime_like(name, args, context, scope, values), - "nl2br" => eval_builtin_nl2br(args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), @@ -1848,34 +1845,13 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), "strtotime" => eval_builtin_strtotime(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), - "strrev" => eval_builtin_strrev(args, context, scope, values), - "str_replace" | "str_ireplace" => { - eval_builtin_str_replace(name, args, context, scope, values) - } - "str_pad" => eval_builtin_str_pad(args, context, scope, values), - "str_split" => eval_builtin_str_split(args, context, scope, values), - "strstr" => eval_builtin_strstr(args, context, scope, values), - "substr" => eval_builtin_substr(args, context, scope, values), - "substr_replace" => eval_builtin_substr_replace(args, context, scope, values), - "str_contains" | "str_starts_with" | "str_ends_with" => { - eval_builtin_string_search(name, args, context, scope, values) - } - "strcmp" | "strcasecmp" => eval_builtin_string_compare(name, args, context, scope, values), - "strlen" => eval_builtin_strlen(args, context, scope, values), - "strpos" | "strrpos" => eval_builtin_string_position(name, args, context, scope, values), - "lcfirst" | "strtolower" | "strtoupper" | "ucfirst" => { - eval_builtin_string_case(name, args, context, scope, values) - } "long2ip" => eval_builtin_long2ip(args, context, scope, values), - "trim" => eval_builtin_trim_like(name, args, context, scope, values), - "ucwords" => eval_builtin_ucwords(args, context, scope, values), "unset" => eval_builtin_unset(args, context, scope, values), "umask" => eval_builtin_umask(args, context, scope, values), "usleep" => eval_builtin_usleep(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), - "wordwrap" => eval_builtin_wordwrap(args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), } } diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 24287a922d..9f35c28c15 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -110,6 +110,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "boolval", "ceil", "chr", + "chop", "clamp", "cos", "cosh", @@ -148,11 +149,14 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "is_resource", "is_scalar", "is_string", + "lcfirst", "log", "log10", "log2", + "ltrim", "max", "min", + "nl2br", "number_format", "ord", "pi", @@ -161,18 +165,39 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "rawurldecode", "rawurlencode", "round", + "rtrim", "sin", "sinh", "sqrt", + "str_contains", + "str_ends_with", + "str_ireplace", + "str_pad", + "str_replace", + "str_split", + "str_starts_with", + "strcasecmp", + "strcmp", "strlen", "str_repeat", "strrev", + "strpos", + "strrpos", + "strstr", + "strtolower", "stripslashes", + "strtoupper", + "substr", + "substr_replace", "strval", "tan", "tanh", + "trim", + "ucfirst", + "ucwords", "urldecode", "urlencode", + "wordwrap", ]; /// Verifies every static builtin is visible through eval's function lookup. From a4830e0459ecb1e4a8b8588763b6473dc10d7065 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 22:54:26 +0200 Subject: [PATCH 1063/1208] refactor(magician): migrate remaining simple string helpers --- .../src/interpreter/builtins/hooks/direct.rs | 12 ++++++- .../src/interpreter/builtins/hooks/values.rs | 33 ++++++++++++++++--- .../interpreter/builtins/registry/binding.rs | 3 -- .../builtins/registry/dispatch/strings.rs | 18 ---------- .../src/interpreter/builtins/registry/mod.rs | 3 ++ .../interpreter/builtins/registry/names.rs | 5 --- .../builtins/string/grapheme_strrev.rs | 16 +++++++++ .../builtins/string/hash_equals.rs | 16 +++++++++ .../builtins/string/html_entity_decode.rs | 16 +++++++++ .../builtins/string/htmlentities.rs | 16 +++++++++ .../builtins/string/htmlspecialchars.rs | 16 +++++++++ .../src/interpreter/builtins/string/mod.rs | 5 +++ .../src/interpreter/expressions.rs | 5 --- tests/builtin_parity_tests.rs | 5 +++ 14 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 1a72e561f6..773794cc82 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -22,7 +22,8 @@ use super::super::super::{ }; use super::super::{ eval_builtin_abs, eval_builtin_cast, eval_builtin_nl2br, eval_builtin_str_pad, - eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_string_case, + eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_grapheme_strrev, + eval_builtin_hash_equals, eval_builtin_html_entity, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, @@ -63,8 +64,14 @@ pub(in crate::interpreter) enum EvalDirectHook { Floor, /// Dispatches `gettype(...)`. Gettype, + /// Dispatches `grapheme_strrev(...)`. + GraphemeStrrev, + /// Dispatches `hash_equals(...)`. + HashEquals, /// Dispatches `hex2bin(...)`. Hex2Bin, + /// Dispatches HTML entity encode/decode builtins. + HtmlEntity, /// Dispatches `intdiv(...)`. Intdiv, /// Dispatches `log(...)`. @@ -154,7 +161,10 @@ impl EvalDirectHook { Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), Self::Floor => eval_builtin_floor(args, context, scope, values), Self::Gettype => eval_builtin_gettype(args, context, scope, values), + Self::GraphemeStrrev => eval_builtin_grapheme_strrev(args, context, scope, values), + Self::HashEquals => eval_builtin_hash_equals(args, context, scope, values), Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), + Self::HtmlEntity => eval_builtin_html_entity(name, args, context, scope, values), Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), Self::Log => eval_builtin_log(args, context, scope, values), Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 31bd4dcef0..7b4c5c2e5c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -18,13 +18,14 @@ use super::super::{ eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, eval_chr_result, eval_clamp_result, eval_crc32_result, eval_ctype_result, eval_float_binary_result, eval_float_pair_result, eval_float_unary_result, - eval_gettype_result, eval_hex2bin_result, eval_intdiv_result, eval_log_result, + eval_gettype_result, eval_grapheme_strrev_result, eval_hash_equals_result, + eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_log_result, eval_min_max_result, eval_nl2br_result, eval_number_format_result, eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, - eval_string_search_result, eval_strstr_result, eval_substr_replace_result, - eval_substr_result, eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, - eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, + eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, + eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, + eval_url_encode_result, eval_wordwrap_result, }; /// Evaluated-argument dispatch hooks for migrated builtins. @@ -62,8 +63,14 @@ pub(in crate::interpreter) enum EvalValuesHook { Floor, /// Dispatches `gettype(...)`. Gettype, + /// Dispatches `grapheme_strrev(...)`. + GraphemeStrrev, + /// Dispatches `hash_equals(...)`. + HashEquals, /// Dispatches `hex2bin(...)`. Hex2Bin, + /// Dispatches HTML entity encode/decode builtins. + HtmlEntity, /// Dispatches `intdiv(...)`. Intdiv, /// Dispatches `log(...)`. @@ -231,12 +238,30 @@ impl EvalValuesHook { }; eval_gettype_result(*value, values) } + Self::GraphemeStrrev => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_grapheme_strrev_result(*value, values) + } + Self::HashEquals => { + let [known, user] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_equals_result(*known, *user, values) + } Self::Hex2Bin => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_hex2bin_result(*value, values) } + Self::HtmlEntity => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_html_entity_result(name, *value, values) + } Self::Intdiv => { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 95192e8968..8eb0fb5f26 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -151,7 +151,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "array_slice" => Some(&["array", "offset", "length"]), "array_splice" => Some(&["array", "offset", "length", "replacement"]), "basename" => Some(&["path", "suffix"]), - "grapheme_strrev" => Some(&["string"]), "empty" => Some(&["value"]), "is_callable" => Some(&["value", "syntax_only", "callable_name"]), "buffer_new" => Some(&["length"]), @@ -243,7 +242,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "hash" => Some(&["algo", "data", "binary"]), "hash_algos" => Some(&[]), "hash_copy" => Some(&["context"]), - "hash_equals" => Some(&["known_string", "user_string"]), "hash_file" => Some(&["algo", "filename", "binary"]), "hash_final" => Some(&["context", "binary"]), "hash_hmac" => Some(&["algo", "data", "key", "binary"]), @@ -254,7 +252,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "http_response_code" => Some(&["response_code"]), "gzcompress" | "gzdeflate" => Some(&["data", "level"]), "gzinflate" | "gzuncompress" => Some(&["data", "max_length"]), - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => Some(&["string"]), "implode" => Some(&["separator", "array"]), "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs index ed42649a5b..27690d9b2c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs @@ -19,12 +19,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "grapheme_strrev" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_grapheme_strrev_result(*value, values)? - } "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { eval_gzip_result(name, evaluated_args, values)? } @@ -81,18 +75,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( }; eval_stream_bool_predicate_result(name, *stream, values)? } - "hash_equals" => { - let [known, user] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_equals_result(*known, *user, values)? - } - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_html_entity_result(name, *value, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 691f53964c..d517de84e3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -188,7 +188,10 @@ mod tests { "ctype_alpha", "floatval", "gettype", + "grapheme_strrev", + "hash_equals", "hex2bin", + "htmlspecialchars", "intval", "is_array", "is_bool", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 75b2b027e0..fd595889f7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -144,7 +144,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "gmdate", "gmmktime", "glob", - "grapheme_strrev", "gzcompress", "gzdeflate", "gzinflate", @@ -152,16 +151,12 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "hash", "hash_algos", "hash_copy", - "hash_equals", "hash_file", "hash_final", "hash_hmac", "hash_init", "hash_update", "header", - "html_entity_decode", - "htmlentities", - "htmlspecialchars", "hrtime", "http_response_code", "implode", diff --git a/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs b/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs new file mode 100644 index 0000000000..1b1b3630ef --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `grapheme_strrev`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the grapheme string reverse hook. + +eval_builtin! { + name: "grapheme_strrev", + area: String, + params: [string], + direct: GraphemeStrrev, + values: GraphemeStrrev, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs new file mode 100644 index 0000000000..714cc58bd7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_equals`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the constant-time byte compare hook. + +eval_builtin! { + name: "hash_equals", + area: String, + params: [known_string, user_string], + direct: HashEquals, + values: HashEquals, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs b/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs new file mode 100644 index 0000000000..7623ce2c20 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `html_entity_decode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the HTML entity hook. + +eval_builtin! { + name: "html_entity_decode", + area: String, + params: [string], + direct: HtmlEntity, + values: HtmlEntity, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs b/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs new file mode 100644 index 0000000000..63e9fc3cc3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `htmlentities`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the HTML entity hook. + +eval_builtin! { + name: "htmlentities", + area: String, + params: [string], + direct: HtmlEntity, + values: HtmlEntity, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs b/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs new file mode 100644 index 0000000000..a2a2004fa6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `htmlspecialchars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the HTML entity hook. + +eval_builtin! { + name: "htmlspecialchars", + area: String, + params: [string], + direct: HtmlEntity, + values: HtmlEntity, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs index 4e0dadbba0..09795f3923 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -19,7 +19,12 @@ mod ctype_alnum; mod ctype_alpha; mod ctype_digit; mod ctype_space; +mod grapheme_strrev; +mod hash_equals; mod hex2bin; +mod html_entity_decode; +mod htmlentities; +mod htmlspecialchars; mod lcfirst; mod ltrim; mod nl2br; diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index f511f257e5..8c861e03ca 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1677,7 +1677,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), "glob" => eval_builtin_glob(args, context, scope, values), - "grapheme_strrev" => eval_builtin_grapheme_strrev(args, context, scope, values), "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { eval_builtin_gzip(name, args, context, scope, values) } @@ -1690,13 +1689,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "hash_algos" => eval_builtin_hash_algos(args, values), "hash_copy" => eval_builtin_hash_copy(args, context, scope, values), - "hash_equals" => eval_builtin_hash_equals(args, context, scope, values), "hash_final" => eval_builtin_hash_final(args, context, scope, values), "hash_init" => eval_builtin_hash_init(args, context, scope, values), "hash_update" => eval_builtin_hash_update(args, context, scope, values), - "html_entity_decode" | "htmlentities" | "htmlspecialchars" => { - eval_builtin_html_entity(name, args, context, scope, values) - } "implode" => eval_builtin_implode(args, context, scope, values), "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 9f35c28c15..610dac4e46 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -127,7 +127,12 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "floor", "fmod", "gettype", + "grapheme_strrev", + "hash_equals", "hex2bin", + "html_entity_decode", + "htmlentities", + "htmlspecialchars", "hypot", "intdiv", "intval", From cb92cb2aa89e0238a9e3fd513f538f7d8c821b27 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 23:19:53 +0200 Subject: [PATCH 1064/1208] refactor(magician): migrate simple array builtins to eval registry --- .../interpreter/builtins/array/array_flip.rs | 16 + .../builtins/array/array_key_exists.rs | 16 + .../interpreter/builtins/array/array_keys.rs | 16 + .../interpreter/builtins/array/array_pad.rs | 16 + .../builtins/array/array_product.rs | 16 + .../interpreter/builtins/array/array_rand.rs | 16 + .../builtins/array/array_reverse.rs | 18 + .../builtins/array/array_search.rs | 18 + .../interpreter/builtins/array/array_slice.rs | 18 + .../interpreter/builtins/array/array_sum.rs | 16 + .../builtins/array/array_unique.rs | 16 + .../builtins/array/array_values.rs | 16 + .../interpreter/builtins/array/in_array.rs | 18 + .../src/interpreter/builtins/array/mod.rs | 14 + .../src/interpreter/builtins/array/range.rs | 16 + .../src/interpreter/builtins/hooks/direct.rs | 49 ++- .../src/interpreter/builtins/hooks/values.rs | 391 ++++++++---------- .../interpreter/builtins/registry/binding.rs | 14 +- .../builtins/registry/dispatch/arrays.rs | 74 ---- .../src/interpreter/builtins/registry/mod.rs | 5 + .../interpreter/builtins/registry/names.rs | 15 - .../builtins/registry/signature.rs | 9 +- .../src/interpreter/expressions.rs | 18 - tests/builtin_parity_tests.rs | 14 + 24 files changed, 493 insertions(+), 342 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_product.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_search.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_values.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/in_array.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/range.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs new file mode 100644 index 0000000000..083203bf12 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_flip`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-flip hook. + +eval_builtin! { + name: "array_flip", + area: Array, + params: [array], + direct: ArrayFlip, + values: ArrayFlip, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs new file mode 100644 index 0000000000..b6141d2c7d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_key_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the runtime key-existence hook. + +eval_builtin! { + name: "array_key_exists", + area: Array, + params: [key, array], + direct: ArrayKeyExists, + values: ArrayKeyExists, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs new file mode 100644 index 0000000000..74f7041978 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_keys`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-projection hook. + +eval_builtin! { + name: "array_keys", + area: Array, + params: [array], + direct: ArrayProjection, + values: ArrayProjection, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs new file mode 100644 index 0000000000..0324d0c257 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_pad`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-pad hook. + +eval_builtin! { + name: "array_pad", + area: Array, + params: [array, length, value], + direct: ArrayPad, + values: ArrayPad, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs new file mode 100644 index 0000000000..4cbeb24576 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_product`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-aggregate hook. + +eval_builtin! { + name: "array_product", + area: Array, + params: [array], + direct: ArrayAggregate, + values: ArrayAggregate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs new file mode 100644 index 0000000000..2b9f2ece6f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_rand`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-rand hook. + +eval_builtin! { + name: "array_rand", + area: Array, + params: [array], + direct: ArrayRand, + values: ArrayRand, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs new file mode 100644 index 0000000000..de522377e6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `array_reverse`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-reverse hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "array_reverse", + area: Array, + params: [array, preserve_keys = EvalBuiltinDefaultValue::Bool(false)], + direct: ArrayReverse, + values: ArrayReverse, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs new file mode 100644 index 0000000000..d8fa73dc26 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `array_search`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-search hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "array_search", + area: Array, + params: [needle, haystack, strict = EvalBuiltinDefaultValue::Bool(false)], + direct: ArraySearch, + values: ArraySearch, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs new file mode 100644 index 0000000000..326e2fbff0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `array_slice`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-slice hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "array_slice", + area: Array, + params: [array, offset, length = EvalBuiltinDefaultValue::Null], + direct: ArraySlice, + values: ArraySlice, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs new file mode 100644 index 0000000000..86cb403b75 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_sum`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-aggregate hook. + +eval_builtin! { + name: "array_sum", + area: Array, + params: [array], + direct: ArrayAggregate, + values: ArrayAggregate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs new file mode 100644 index 0000000000..d40d81f146 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_unique`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-unique hook. + +eval_builtin! { + name: "array_unique", + area: Array, + params: [array], + direct: ArrayUnique, + values: ArrayUnique, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs new file mode 100644 index 0000000000..6cc18ad261 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_values`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-projection hook. + +eval_builtin! { + name: "array_values", + area: Array, + params: [array], + direct: ArrayProjection, + values: ArrayProjection, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs b/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs new file mode 100644 index 0000000000..c51b7ad8d6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `in_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the array-search hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "in_array", + area: Array, + params: [needle, haystack, strict = EvalBuiltinDefaultValue::Bool(false)], + direct: ArraySearch, + values: ArraySearch, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs index 52daf3169b..70a35983ee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -8,4 +8,18 @@ //! Key details: //! - Leaf files register metadata through `eval_builtin!`. +mod array_flip; +mod array_key_exists; +mod array_keys; +mod array_pad; +mod array_product; +mod array_rand; +mod array_reverse; +mod array_search; +mod array_slice; +mod array_sum; +mod array_unique; +mod array_values; mod count; +mod in_array; +mod range; diff --git a/crates/elephc-magician/src/interpreter/builtins/array/range.rs b/crates/elephc-magician/src/interpreter/builtins/array/range.rs new file mode 100644 index 0000000000..d532fc4691 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/range.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `range`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the integer range hook. + +eval_builtin! { + name: "range", + area: Array, + params: [start, end], + direct: Range, + values: Range, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 773794cc82..6b4cc190fd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -21,12 +21,16 @@ use super::super::super::{ EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::super::{ - eval_builtin_abs, eval_builtin_cast, eval_builtin_nl2br, eval_builtin_str_pad, - eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_grapheme_strrev, - eval_builtin_hash_equals, eval_builtin_html_entity, eval_builtin_string_case, - eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, - eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, - eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, + eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_flip, + eval_builtin_array_key_exists, eval_builtin_array_pad, eval_builtin_array_projection, + eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, + eval_builtin_array_slice, eval_builtin_array_unique, eval_builtin_cast, + eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, + eval_builtin_nl2br, eval_builtin_range, eval_builtin_str_pad, eval_builtin_str_replace, + eval_builtin_str_split, eval_builtin_string_case, eval_builtin_string_compare, + eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, + eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, eval_builtin_trim_like, + eval_builtin_ucwords, eval_builtin_wordwrap, }; /// Direct expression-level dispatch hooks for migrated builtins. @@ -34,6 +38,26 @@ use super::super::{ pub(in crate::interpreter) enum EvalDirectHook { /// Dispatches `abs(...)`. Abs, + /// Dispatches `array_sum(...)` and `array_product(...)`. + ArrayAggregate, + /// Dispatches `array_flip(...)`. + ArrayFlip, + /// Dispatches `array_key_exists(...)`. + ArrayKeyExists, + /// Dispatches `array_pad(...)`. + ArrayPad, + /// Dispatches `array_keys(...)` and `array_values(...)`. + ArrayProjection, + /// Dispatches `array_rand(...)`. + ArrayRand, + /// Dispatches `array_reverse(...)`. + ArrayReverse, + /// Dispatches `array_search(...)` and `in_array(...)`. + ArraySearch, + /// Dispatches `array_slice(...)`. + ArraySlice, + /// Dispatches `array_unique(...)`. + ArrayUnique, /// Dispatches `base64_decode(...)`. Base64Decode, /// Dispatches `base64_encode(...)`. @@ -88,6 +112,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Pow, /// Dispatches `round(...)`. Round, + /// Dispatches `range(...)`. + Range, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, /// Dispatches `sqrt(...)`. @@ -146,6 +172,16 @@ impl EvalDirectHook { ) -> Result { match self { Self::Abs => eval_builtin_abs(args, context, scope, values), + Self::ArrayAggregate => eval_builtin_array_aggregate(name, args, context, scope, values), + Self::ArrayFlip => eval_builtin_array_flip(args, context, scope, values), + Self::ArrayKeyExists => eval_builtin_array_key_exists(args, context, scope, values), + Self::ArrayPad => eval_builtin_array_pad(args, context, scope, values), + Self::ArrayProjection => eval_builtin_array_projection(name, args, context, scope, values), + Self::ArrayRand => eval_builtin_array_rand(args, context, scope, values), + Self::ArrayReverse => eval_builtin_array_reverse(args, context, scope, values), + Self::ArraySearch => eval_builtin_array_search(name, args, context, scope, values), + Self::ArraySlice => eval_builtin_array_slice(args, context, scope, values), + Self::ArrayUnique => eval_builtin_array_unique(args, context, scope, values), Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), @@ -173,6 +209,7 @@ impl EvalDirectHook { Self::Pi => eval_builtin_pi(args, values), Self::Pow => eval_builtin_pow(args, context, scope, values), Self::Round => eval_builtin_round(args, context, scope, values), + Self::Range => eval_builtin_range(args, context, scope, values), Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), Self::StringCase => eval_builtin_string_case(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 7b4c5c2e5c..950c72d6cc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -15,16 +15,20 @@ use super::super::super::{ RuntimeValueOps, }; use super::super::{ + eval_array_aggregate_result, eval_array_flip_result, eval_array_pad_result, + eval_array_projection_result, eval_array_rand_result, eval_array_reverse_result, + eval_array_search_result, eval_array_slice_result, eval_array_unique_result, eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, eval_chr_result, eval_clamp_result, eval_crc32_result, eval_ctype_result, eval_float_binary_result, eval_float_pair_result, eval_float_unary_result, eval_gettype_result, eval_grapheme_strrev_result, eval_hash_equals_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_log_result, - eval_min_max_result, eval_nl2br_result, eval_number_format_result, eval_slashes_result, - eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, - eval_string_case_result, eval_string_compare_result, eval_string_position_result, - eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, - eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, + eval_min_max_result, eval_nl2br_result, eval_number_format_result, eval_range_result, + eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, + eval_str_split_result, eval_string_case_result, eval_string_compare_result, + eval_string_position_result, eval_string_search_result, eval_strstr_result, + eval_substr_replace_result, eval_substr_result, eval_trim_like_result, + eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; @@ -33,6 +37,26 @@ use super::super::{ pub(in crate::interpreter) enum EvalValuesHook { /// Dispatches `abs(...)`. Abs, + /// Dispatches `array_sum(...)` and `array_product(...)`. + ArrayAggregate, + /// Dispatches `array_flip(...)`. + ArrayFlip, + /// Dispatches `array_key_exists(...)`. + ArrayKeyExists, + /// Dispatches `array_pad(...)`. + ArrayPad, + /// Dispatches `array_keys(...)` and `array_values(...)`. + ArrayProjection, + /// Dispatches `array_rand(...)`. + ArrayRand, + /// Dispatches `array_reverse(...)`. + ArrayReverse, + /// Dispatches `array_search(...)` and `in_array(...)`. + ArraySearch, + /// Dispatches `array_slice(...)`. + ArraySlice, + /// Dispatches `array_unique(...)`. + ArrayUnique, /// Dispatches `base64_decode(...)`. Base64Decode, /// Dispatches `base64_encode(...)`. @@ -87,6 +111,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Pow, /// Dispatches `round(...)`. Round, + /// Dispatches `range(...)`. + Range, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, /// Dispatches `sqrt(...)`. @@ -143,131 +169,74 @@ impl EvalValuesHook { values: &mut impl RuntimeValueOps, ) -> Result { match self { - Self::Abs => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.abs(*value) - } - Self::Base64Decode => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_decode_result(*value, values) - } - Self::Base64Encode => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_base64_encode_result(*value, values) - } - Self::Bin2Hex => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_bin2hex_result(*value, values) - } - Self::Cast => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_cast_result(name, *value, context, values) - } - Self::Ceil => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.ceil(*value) - } - Self::Chr => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chr_result(*value, values) - } - Self::Clamp => { - let [value, min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_clamp_result(*value, *min, *max, values) - } + Self::Abs => one_arg(evaluated_args, values, |value, values| values.abs(value)), + Self::ArrayAggregate => one_arg(evaluated_args, values, |array, values| { + eval_array_aggregate_result(name, array, values) + }), + Self::ArrayFlip => one_arg(evaluated_args, values, eval_array_flip_result), + Self::ArrayKeyExists => two_args(evaluated_args, values, |key, array, values| { + values.array_key_exists(key, array) + }), + Self::ArrayPad => three_args(evaluated_args, values, eval_array_pad_result), + Self::ArrayProjection => one_arg(evaluated_args, values, |array, values| { + eval_array_projection_result(name, array, values) + }), + Self::ArrayRand => one_arg(evaluated_args, values, eval_array_rand_result), + Self::ArrayReverse => match evaluated_args { + [array] => eval_array_reverse_result(*array, false, values), + [array, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_reverse_result(*array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::ArraySearch => two_args(evaluated_args, values, |needle, array, values| { + eval_array_search_result(name, needle, array, values) + }), + Self::ArraySlice => match evaluated_args { + [array, offset] => eval_array_slice_result(*array, *offset, None, values), + [array, offset, length] => { + eval_array_slice_result(*array, *offset, Some(*length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::ArrayUnique => one_arg(evaluated_args, values, eval_array_unique_result), + Self::Base64Decode => one_arg(evaluated_args, values, eval_base64_decode_result), + Self::Base64Encode => one_arg(evaluated_args, values, eval_base64_encode_result), + Self::Bin2Hex => one_arg(evaluated_args, values, eval_bin2hex_result), + Self::Cast => one_arg(evaluated_args, values, |value, values| { + eval_cast_result(name, value, context, values) + }), + Self::Ceil => one_arg(evaluated_args, values, |value, values| values.ceil(value)), + Self::Chr => one_arg(evaluated_args, values, eval_chr_result), + Self::Clamp => three_args(evaluated_args, values, eval_clamp_result), Self::Count => match evaluated_args { [value] => eval_count_result(*value, None, context, values), [value, mode] => eval_count_result(*value, Some(*mode), context, values), _ => Err(EvalStatus::RuntimeFatal), }, - Self::Crc32 => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_crc32_result(*value, values) - } - Self::Ctype => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ctype_result(name, *value, values) - } - Self::FloatBinary => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_binary_result(name, *left, *right, values) - } - Self::FloatPair => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_pair_result(name, *left, *right, values) - } - Self::FloatUnary => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_float_unary_result(name, *value, values) - } - Self::Floor => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.floor(*value) - } - Self::Gettype => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gettype_result(*value, values) - } - Self::GraphemeStrrev => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_grapheme_strrev_result(*value, values) - } - Self::HashEquals => { - let [known, user] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_equals_result(*known, *user, values) - } - Self::Hex2Bin => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hex2bin_result(*value, values) - } - Self::HtmlEntity => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_html_entity_result(name, *value, values) - } - Self::Intdiv => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_intdiv_result(*left, *right, values) - } + Self::Crc32 => one_arg(evaluated_args, values, eval_crc32_result), + Self::Ctype => one_arg(evaluated_args, values, |value, values| { + eval_ctype_result(name, value, values) + }), + Self::FloatBinary => two_args(evaluated_args, values, |left, right, values| { + eval_float_binary_result(name, left, right, values) + }), + Self::FloatPair => two_args(evaluated_args, values, |left, right, values| { + eval_float_pair_result(name, left, right, values) + }), + Self::FloatUnary => one_arg(evaluated_args, values, |value, values| { + eval_float_unary_result(name, value, values) + }), + Self::Floor => one_arg(evaluated_args, values, |value, values| values.floor(value)), + Self::Gettype => one_arg(evaluated_args, values, eval_gettype_result), + Self::GraphemeStrrev => one_arg(evaluated_args, values, eval_grapheme_strrev_result), + Self::HashEquals => two_args(evaluated_args, values, eval_hash_equals_result), + Self::Hex2Bin => one_arg(evaluated_args, values, eval_hex2bin_result), + Self::HtmlEntity => one_arg(evaluated_args, values, |value, values| { + eval_html_entity_result(name, value, values) + }), + Self::Intdiv => two_args(evaluated_args, values, eval_intdiv_result), Self::Log => match evaluated_args { [num] => eval_log_result(*num, None, values), [num, base] => eval_log_result(*num, Some(*base), values), @@ -297,65 +266,38 @@ impl EvalValuesHook { } _ => Err(EvalStatus::RuntimeFatal), }, - Self::Ord => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ord_result(*value, values) - } + Self::Ord => one_arg(evaluated_args, values, eval_ord_result), Self::Pi => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } values.float(std::f64::consts::PI) } - Self::Pow => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.pow(*left, *right) - } + Self::Pow => two_args(evaluated_args, values, |left, right, values| { + values.pow(left, right) + }), Self::Round => match evaluated_args { [value] => values.round(*value, None), [value, precision] => values.round(*value, Some(*precision)), _ => Err(EvalStatus::RuntimeFatal), }, - Self::Slashes => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_slashes_result(name, *value, values) - } - Self::Sqrt => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.sqrt(*value) - } - Self::StringCase => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_case_result(name, *value, values) - } - Self::StringCompare => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_compare_result(name, *left, *right, values) - } - Self::StringPosition => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_position_result(name, *haystack, *needle, values) - } - Self::StringSearch => { - let [haystack, needle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_string_search_result(name, *haystack, *needle, values) - } + Self::Range => two_args(evaluated_args, values, eval_range_result), + Self::Slashes => one_arg(evaluated_args, values, |value, values| { + eval_slashes_result(name, value, values) + }), + Self::Sqrt => one_arg(evaluated_args, values, |value, values| values.sqrt(value)), + Self::StringCase => one_arg(evaluated_args, values, |value, values| { + eval_string_case_result(name, value, values) + }), + Self::StringCompare => two_args(evaluated_args, values, |left, right, values| { + eval_string_compare_result(name, left, right, values) + }), + Self::StringPosition => two_args(evaluated_args, values, |haystack, needle, values| { + eval_string_position_result(name, haystack, needle, values) + }), + Self::StringSearch => two_args(evaluated_args, values, |haystack, needle, values| { + eval_string_search_result(name, haystack, needle, values) + }), Self::StrPad => match evaluated_args { [value, length] => eval_str_pad_result(*value, *length, None, None, values), [value, length, pad_string] => { @@ -366,12 +308,9 @@ impl EvalValuesHook { } _ => Err(EvalStatus::RuntimeFatal), }, - Self::StrReplace => { - let [search, replace, subject] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_replace_result(name, *search, *replace, *subject, values) - } + Self::StrReplace => three_args(evaluated_args, values, |search, replace, subject, values| { + eval_str_replace_result(name, search, replace, subject, values) + }), Self::StrSplit => match evaluated_args { [value] => eval_str_split_result(*value, None, values), [value, length] => eval_str_split_result(*value, Some(*length), values), @@ -385,18 +324,8 @@ impl EvalValuesHook { let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; values.int(len) } - Self::StrRepeat => { - let [value, times] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_str_repeat_result(*value, *times, values) - } - Self::Strrev => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.strrev(*value) - } + Self::StrRepeat => two_args(evaluated_args, values, eval_str_repeat_result), + Self::Strrev => one_arg(evaluated_args, values, |value, values| values.strrev(value)), Self::Strstr => match evaluated_args { [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values), [haystack, needle, before_needle] => { @@ -426,12 +355,9 @@ impl EvalValuesHook { [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values), _ => Err(EvalStatus::RuntimeFatal), }, - Self::TypePredicate => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_type_predicate_result(name, *value, context, values) - } + Self::TypePredicate => one_arg(evaluated_args, values, |value, values| { + eval_type_predicate_result(name, value, context, values) + }), Self::Ucwords => match evaluated_args { [value] => eval_ucwords_result(*value, None, values), [value, separators] => eval_ucwords_result(*value, Some(*separators), values), @@ -460,18 +386,65 @@ impl EvalValuesHook { ), _ => Err(EvalStatus::RuntimeFatal), }, - Self::UrlDecode => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_decode_result(name, *value, values) - } - Self::UrlEncode => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_url_encode_result(name, *value, values) - } + Self::UrlDecode => one_arg(evaluated_args, values, |value, values| { + eval_url_decode_result(name, value, values) + }), + Self::UrlEncode => one_arg(evaluated_args, values, |value, values| { + eval_url_encode_result(name, value, values) + }), } } } + +/// Validates and dispatches one evaluated builtin argument. +fn one_arg( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce(RuntimeCellHandle, &mut V) -> Result, +{ + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*value, values) +} + +/// Validates and dispatches two evaluated builtin arguments. +fn two_args( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce(RuntimeCellHandle, RuntimeCellHandle, &mut V) -> Result, +{ + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*left, *right, values) +} + +/// Validates and dispatches three evaluated builtin arguments. +fn three_args( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce( + RuntimeCellHandle, + RuntimeCellHandle, + RuntimeCellHandle, + &mut V, + ) -> Result, +{ + let [first, second, third] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*first, *second, *third, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 8eb0fb5f26..d2fbbfbf1e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -134,21 +134,13 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "array_reduce" => Some(&["array", "callback", "initial"]), "array_walk" => Some(&["array", "callback"]), "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), - "array_flip" | "array_keys" | "array_pop" | "array_product" | "array_shift" - | "array_sum" | "array_unique" | "array_rand" | "array_values" | "arsort" | "asort" - | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { - Some(&["array"]) - } + "array_pop" | "array_shift" | "arsort" | "asort" | "krsort" | "ksort" + | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => Some(&["array"]), "array_merge" => Some(&["arrays"]), "array_diff" | "array_intersect" | "array_diff_key" | "array_intersect_key" => { Some(&["array", "arrays"]) } "array_push" | "array_unshift" => Some(&["array", "values"]), - "array_key_exists" => Some(&["key", "array"]), - "array_pad" => Some(&["array", "length", "value"]), - "array_reverse" => Some(&["array", "preserve_keys"]), - "array_search" | "in_array" => Some(&["needle", "haystack", "strict"]), - "array_slice" => Some(&["array", "offset", "length"]), "array_splice" => Some(&["array", "offset", "length", "replacement"]), "basename" => Some(&["path", "suffix"]), "empty" => Some(&["value"]), @@ -181,7 +173,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "chmod" => Some(&["filename", "permissions"]), "closedir" | "readdir" | "rewinddir" => Some(&["dir_handle"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), - "count" => Some(&["value", "mode"]), "copy" | "rename" => Some(&["from", "to"]), "checkdate" => Some(&["month", "day", "year"]), "date" | "gmdate" => Some(&["format", "timestamp"]), @@ -297,7 +288,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "var_dump" => Some(&["value", "values"]), "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), - "range" => Some(&["start", "end"]), "readline" => Some(&["prompt"]), "realpath" => Some(&["path"]), "stream_resolve_include_path" => Some(&["filename"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs index dc496c1fa6..edd7f5e5bf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs @@ -137,36 +137,6 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( ))?; eval_user_sort_value_result(name, *array, *callback, context, values)? } - "array_flip" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_flip_result(*array, values)? - } - "array_pad" => { - let [array, length, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_pad_result(*array, *length, *value, values)? - } - "array_product" | "array_sum" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_aggregate_result(name, *array, values)? - } - "array_keys" | "array_values" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_projection_result(name, *array, values)? - } - "array_key_exists" => { - let [key, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.array_key_exists(*key, *array)? - } "array_diff" | "array_intersect" => { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -185,50 +155,6 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( }; eval_array_merge_result(*left, *right, values)? } - "array_rand" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_rand_result(*array, values)? - } - "array_reverse" => match evaluated_args { - [array] => eval_array_reverse_result(*array, false, values)?, - [array, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_array_reverse_result(*array, preserve_keys, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_search" | "in_array" => { - let [needle, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_search_result(name, *needle, *array, values)? - } - "array_slice" => match evaluated_args { - [array, offset] => eval_array_slice_result(*array, *offset, None, values)?, - [array, offset, length] => { - eval_array_slice_result(*array, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_unique" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_unique_result(*array, values)? - } - "range" => { - let [start, end] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_range_result(*start, *end, values)? - } - "count" => match evaluated_args { - [value] => eval_count_result(*value, None, context, values)?, - [value, mode] => eval_count_result(*value, Some(*mode), context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "iterator_apply" => match evaluated_args { [iterator, callback] => { let callback = eval_callable(*callback, context, values)?; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index d517de84e3..1f82a9d154 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -181,6 +181,10 @@ mod tests { "abs", "acos", "addslashes", + "array_key_exists", + "array_keys", + "array_reverse", + "array_sum", "boolval", "base64_encode", "bin2hex", @@ -215,6 +219,7 @@ mod tests { "min", "nl2br", "number_format", + "range", "rawurlencode", "str_contains", "str_pad", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index fd595889f7..6b4bc46549 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -22,28 +22,16 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "array_fill", "array_fill_keys", "array_filter", - "array_flip", "array_intersect", "array_intersect_key", - "array_key_exists", - "array_keys", "array_map", "array_merge", - "array_pad", "array_pop", - "array_product", "array_push", - "array_rand", "array_reduce", - "array_reverse", - "array_search", "array_shift", - "array_slice", "array_splice", - "array_sum", - "array_unique", "array_unshift", - "array_values", "array_walk", "arsort", "asort", @@ -68,7 +56,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "clearstatcache", "closedir", "copy", - "count", "checkdate", "date", "date_default_timezone_get", @@ -160,7 +147,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "hrtime", "http_response_code", "implode", - "in_array", "inet_ntop", "inet_pton", "interface_exists", @@ -235,7 +221,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "putenv", "rand", "random_int", - "range", "readdir", "readfile", "readline", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 9c74262a5f..5df3405447 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -76,7 +76,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "get_class" | "get_parent_class" => optional(params, 0), "is_a" | "is_subclass_of" => optional(params, 2), - "count" => optional(params, 1), "getdate" | "hrtime" => optional(params, 0), "header" => optional(params, 1), "http_response_code" => optional(params, 0), @@ -97,16 +96,13 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "hash_final" | "md5" | "sha1" => optional(params, 1), "array_pop" | "array_shift" => fixed_by_ref(params, &["array"]), - "array_reverse" => optional(params, 1), "sort" | "rsort" | "shuffle" | "natsort" | "natcasesort" | "asort" | "arsort" | "ksort" | "krsort" => fixed_by_ref(params, &["array"]), - "in_array" | "array_search" => optional(params, 2), "array_push" | "array_unshift" => variadic(params, &["array"]), "array_merge" => variadic(params, &[]), "array_diff" | "array_intersect" | "array_diff_key" | "array_intersect_key" => { variadic(params, &[]) } - "array_slice" => optional(params, 2), "array_splice" => optional_by_ref(params, 2, &["array"]), "array_map" => variadic(params, &[]), "array_filter" => optional(params, 1), @@ -175,7 +171,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_a", 2) => Bool(false), ("is_subclass_of", 2) => Bool(true), - ("count", 1) => Int(0), ("getdate", 0) => Null, ("header", 1) => Bool(true), ("header", 2) => Int(0), @@ -198,9 +193,7 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("hash_init", 1) => Int(0), ("hash_init", 2) => String(""), ("hash_final" | "md5" | "sha1", 1) => Bool(false), - ("array_reverse", 1) => Bool(false), - ("in_array" | "array_search", 2) => Bool(false), - ("array_slice" | "array_splice", 2) => Null, + ("array_splice", 2) => Null, ("array_splice", 3) => EmptyArray, ("array_filter", 1) => Null, ("array_filter", 2) => Int(0), diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 8c861e03ca..1759086efc 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1538,14 +1538,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "array_fill" => eval_builtin_array_fill(args, context, scope, values), "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), "array_filter" => eval_builtin_array_filter(args, context, scope, values), - "array_flip" => eval_builtin_array_flip(args, context, scope, values), "array_map" => eval_builtin_array_map(args, context, scope, values), "array_reduce" => eval_builtin_array_reduce(args, context, scope, values), "array_walk" => eval_builtin_array_walk(args, context, scope, values), - "array_keys" | "array_values" => { - eval_builtin_array_projection(name, args, context, scope, values) - } - "array_key_exists" => eval_builtin_array_key_exists(args, context, scope, values), "array_diff" | "array_intersect" => { eval_builtin_array_value_set(name, args, context, scope, values) } @@ -1553,17 +1548,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_array_key_set(name, args, context, scope, values) } "array_merge" => eval_builtin_array_merge(args, context, scope, values), - "array_product" | "array_sum" => { - eval_builtin_array_aggregate(name, args, context, scope, values) - } - "array_pad" => eval_builtin_array_pad(args, context, scope, values), - "array_rand" => eval_builtin_array_rand(args, context, scope, values), - "array_reverse" => eval_builtin_array_reverse(args, context, scope, values), - "array_search" | "in_array" => { - eval_builtin_array_search(name, args, context, scope, values) - } - "array_slice" => eval_builtin_array_slice(args, context, scope, values), - "array_unique" => eval_builtin_array_unique(args, context, scope, values), "basename" => eval_builtin_basename(args, context, scope, values), "chdir" | "mkdir" | "rmdir" => { eval_builtin_unary_path_bool(name, args, context, scope, values) @@ -1597,7 +1581,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "closedir" | "readdir" | "rewinddir" => { eval_builtin_unary_directory(name, args, context, scope, values) } - "count" => eval_builtin_count(args, context, scope, values), "copy" | "link" | "rename" | "symlink" => { eval_builtin_binary_path_bool(name, args, context, scope, values) } @@ -1733,7 +1716,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "putenv" => eval_builtin_putenv(args, context, scope, values), "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), "random_int" => eval_builtin_random_int(args, context, scope, values), - "range" => eval_builtin_range(args, context, scope, values), "readfile" => eval_builtin_readfile(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 610dac4e46..a9ad5c274d 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -101,6 +101,18 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "abs", "acos", "addslashes", + "array_flip", + "array_key_exists", + "array_keys", + "array_pad", + "array_product", + "array_rand", + "array_reverse", + "array_search", + "array_slice", + "array_sum", + "array_unique", + "array_values", "asin", "atan", "atan2", @@ -154,6 +166,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "is_resource", "is_scalar", "is_string", + "in_array", "lcfirst", "log", "log10", @@ -167,6 +180,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "pi", "pow", "rad2deg", + "range", "rawurldecode", "rawurlencode", "round", From a873dd7cb9c0ea6f33337518509b4e182e779409 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 23:35:48 +0200 Subject: [PATCH 1065/1208] refactor(magician): migrate json builtins to eval registry --- .../src/interpreter/builtins/hooks/direct.rs | 13 ++- .../src/interpreter/builtins/hooks/values.rs | 19 ++-- .../interpreter/builtins/json/json_decode.rs | 23 ++++ .../interpreter/builtins/json/json_encode.rs | 22 ++++ .../builtins/json/json_last_error.rs | 16 +++ .../builtins/json/json_last_error_msg.rs | 16 +++ .../builtins/json/json_validate.rs | 22 ++++ .../src/interpreter/builtins/json/mod.rs | 104 ++++++++++++++++++ .../src/interpreter/builtins/mod.rs | 2 + .../interpreter/builtins/registry/binding.rs | 4 - .../builtins/registry/dispatch/json.rs | 75 ------------- .../builtins/registry/dispatch/mod.rs | 5 - .../src/interpreter/builtins/registry/mod.rs | 21 ++++ .../interpreter/builtins/registry/names.rs | 5 - .../builtins/registry/signature.rs | 9 -- .../src/interpreter/builtins/spec.rs | 2 + .../src/interpreter/expressions.rs | 5 - tests/builtin_parity_tests.rs | 8 +- 18 files changed, 252 insertions(+), 119 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/json/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/json.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 6b4cc190fd..64a6e08b19 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -26,11 +26,11 @@ use super::super::{ eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, eval_builtin_array_slice, eval_builtin_array_unique, eval_builtin_cast, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, - eval_builtin_nl2br, eval_builtin_range, eval_builtin_str_pad, eval_builtin_str_replace, - eval_builtin_str_split, eval_builtin_string_case, eval_builtin_string_compare, - eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, - eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, eval_builtin_trim_like, - eval_builtin_ucwords, eval_builtin_wordwrap, + eval_builtin_json_call, eval_builtin_nl2br, eval_builtin_range, eval_builtin_str_pad, + eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_string_case, + eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, + eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, + eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, }; /// Direct expression-level dispatch hooks for migrated builtins. @@ -98,6 +98,8 @@ pub(in crate::interpreter) enum EvalDirectHook { HtmlEntity, /// Dispatches `intdiv(...)`. Intdiv, + /// Dispatches JSON builtins. + Json, /// Dispatches `log(...)`. Log, /// Dispatches `min(...)` and `max(...)`. @@ -202,6 +204,7 @@ impl EvalDirectHook { Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), Self::HtmlEntity => eval_builtin_html_entity(name, args, context, scope, values), Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), + Self::Json => eval_builtin_json_call(name, args, context, scope, values), Self::Log => eval_builtin_log(args, context, scope, values), Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 950c72d6cc..22930b5509 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -22,14 +22,14 @@ use super::super::{ eval_chr_result, eval_clamp_result, eval_crc32_result, eval_ctype_result, eval_float_binary_result, eval_float_pair_result, eval_float_unary_result, eval_gettype_result, eval_grapheme_strrev_result, eval_hash_equals_result, - eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_log_result, - eval_min_max_result, eval_nl2br_result, eval_number_format_result, eval_range_result, - eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, - eval_str_split_result, eval_string_case_result, eval_string_compare_result, - eval_string_position_result, eval_string_search_result, eval_strstr_result, - eval_substr_replace_result, eval_substr_result, eval_trim_like_result, - eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, - eval_url_encode_result, eval_wordwrap_result, + eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_json_values_result, + eval_log_result, eval_min_max_result, eval_nl2br_result, eval_number_format_result, + eval_range_result, eval_slashes_result, eval_str_pad_result, eval_str_replace_result, + eval_str_repeat_result, eval_str_split_result, eval_string_case_result, + eval_string_compare_result, eval_string_position_result, eval_string_search_result, + eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_trim_like_result, + eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, + eval_wordwrap_result, }; /// Evaluated-argument dispatch hooks for migrated builtins. @@ -97,6 +97,8 @@ pub(in crate::interpreter) enum EvalValuesHook { HtmlEntity, /// Dispatches `intdiv(...)`. Intdiv, + /// Dispatches JSON builtins. + Json, /// Dispatches `log(...)`. Log, /// Dispatches `min(...)` and `max(...)`. @@ -237,6 +239,7 @@ impl EvalValuesHook { eval_html_entity_result(name, value, values) }), Self::Intdiv => two_args(evaluated_args, values, eval_intdiv_result), + Self::Json => eval_json_values_result(name, evaluated_args, context, values), Self::Log => match evaluated_args { [num] => eval_log_result(*num, None, values), [num, base] => eval_log_result(*num, Some(*base), values), diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs new file mode 100644 index 0000000000..6a492966fb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `json_decode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::json`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the JSON decode hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "json_decode", + area: Json, + params: [ + json, + associative = EvalBuiltinDefaultValue::Null, + depth = EvalBuiltinDefaultValue::Int(512), + flags = EvalBuiltinDefaultValue::Int(0), + ], + direct: Json, + values: Json, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs new file mode 100644 index 0000000000..759d67d33f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `json_encode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::json`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the JSON encode hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "json_encode", + area: Json, + params: [ + value, + flags = EvalBuiltinDefaultValue::Int(0), + depth = EvalBuiltinDefaultValue::Int(512), + ], + direct: Json, + values: Json, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs new file mode 100644 index 0000000000..6963cedaf1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `json_last_error`. +//! +//! Called from: +//! - `crate::interpreter::builtins::json`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the JSON error-state hook. + +eval_builtin! { + name: "json_last_error", + area: Json, + params: [], + direct: Json, + values: Json, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs new file mode 100644 index 0000000000..6df25ac9d1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `json_last_error_msg`. +//! +//! Called from: +//! - `crate::interpreter::builtins::json`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the JSON error-message hook. + +eval_builtin! { + name: "json_last_error_msg", + area: Json, + params: [], + direct: Json, + values: Json, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs new file mode 100644 index 0000000000..e37fc5f8bf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `json_validate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::json`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the JSON validation hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "json_validate", + area: Json, + params: [ + json, + depth = EvalBuiltinDefaultValue::Int(512), + flags = EvalBuiltinDefaultValue::Int(0), + ], + direct: Json, + values: Json, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/json/mod.rs b/crates/elephc-magician/src/interpreter/builtins/json/mod.rs new file mode 100644 index 0000000000..52beee2415 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/json/mod.rs @@ -0,0 +1,104 @@ +//! Purpose: +//! Declarative eval registry entries and dispatch adapters for JSON builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! - `crate::interpreter::builtins::hooks` for migrated JSON dispatch. +//! +//! Key details: +//! - The JSON parser/encoder engine remains in `crate::interpreter::json`. +//! - This module only owns registry metadata and small hook adapters. + +use super::super::{ + eval_builtin_json_decode, eval_builtin_json_encode, eval_builtin_json_last_error, + eval_builtin_json_last_error_msg, eval_builtin_json_validate, eval_json_decode_result, + eval_json_encode_result, eval_json_validate_result, ElephcEvalContext, ElephcEvalScope, + EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, +}; + +mod json_decode; +mod json_encode; +mod json_last_error; +mod json_last_error_msg; +mod json_validate; + +/// Dispatches direct expression-level calls for declaratively migrated JSON builtins. +pub(in crate::interpreter) fn eval_builtin_json_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "json_decode" => eval_builtin_json_decode(args, context, scope, values), + "json_encode" => eval_builtin_json_encode(args, context, scope, values), + "json_last_error" => eval_builtin_json_last_error(args, context, values), + "json_last_error_msg" => eval_builtin_json_last_error_msg(args, context, values), + "json_validate" => eval_builtin_json_validate(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated JSON builtins. +pub(in crate::interpreter) fn eval_json_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "json_decode" => match evaluated_args { + [json] => eval_json_decode_result(*json, None, None, None, context, values), + [json, associative] => { + eval_json_decode_result(*json, Some(*associative), None, None, context, values) + } + [json, associative, depth] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + None, + context, + values, + ), + [json, associative, depth, flags] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + Some(*flags), + context, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + }, + "json_encode" => match evaluated_args { + [value] => eval_json_encode_result(*value, None, None, context, values), + [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values), + [value, flags, depth] => { + eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "json_last_error" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.int(context.json_last_error()) + } + "json_last_error_msg" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.string(context.json_last_error_msg()) + } + "json_validate" => match evaluated_args { + [json] => eval_json_validate_result(*json, None, None, context, values), + [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values), + [json, depth, flags] => { + eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 9e74d63b02..a55aa89dff 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -20,6 +20,7 @@ mod class_metadata; mod filesystem; mod formatting; mod hooks; +mod json; mod math; mod network_env; mod process_control; @@ -40,6 +41,7 @@ pub(super) use arrays::*; pub(super) use class_metadata::*; pub(super) use filesystem::*; pub(super) use formatting::*; +pub(super) use json::*; pub(super) use network_env::*; pub(super) use process_control::*; pub(super) use raw_memory::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index d2fbbfbf1e..b13a3a6fbb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -251,10 +251,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "iterator_to_array" => Some(&["iterator", "preserve_keys"]), "ip2long" => Some(&["ip"]), "isset" | "unset" => Some(&["var", "vars"]), - "json_decode" => Some(&["json", "associative", "depth", "flags"]), - "json_encode" => Some(&["value", "flags", "depth"]), - "json_last_error" | "json_last_error_msg" => Some(&[]), - "json_validate" => Some(&["json", "depth", "flags"]), "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), "md5" | "sha1" => Some(&["string", "binary"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/json.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/json.rs deleted file mode 100644 index c546079920..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/json.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Purpose: -//! Dispatches already evaluated JSON builtins by dynamic callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. - -use super::super::super::super::*; - -/// Attempts to dispatch evaluated JSON builtins. -pub(in crate::interpreter) fn eval_json_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "json_decode" => match evaluated_args { - [json] => eval_json_decode_result(*json, None, None, None, context, values)?, - [json, associative] => { - eval_json_decode_result(*json, Some(*associative), None, None, context, values)? - } - [json, associative, depth] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - None, - context, - values, - )?, - [json, associative, depth, flags] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - Some(*flags), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "json_encode" => match evaluated_args { - [value] => eval_json_encode_result(*value, None, None, context, values)?, - [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values)?, - [value, flags, depth] => { - eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "json_last_error" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.int(context.json_last_error())? - } - "json_last_error_msg" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.string(context.json_last_error_msg())? - } - "json_validate" => match evaluated_args { - [json] => eval_json_validate_result(*json, None, None, context, values)?, - [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values)?, - [json, depth, flags] => { - eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index ca4b329b48..222531a697 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -12,7 +12,6 @@ mod arrays; mod core; mod filesystem; mod formatting; -mod json; mod network_env; mod regex; mod scalars; @@ -27,7 +26,6 @@ use arrays::*; use core::*; use filesystem::*; use formatting::*; -use json::*; use network_env::*; use regex::*; use scalars::*; @@ -92,8 +90,5 @@ pub(in crate::interpreter) fn eval_builtin_with_values( if let Some(result) = eval_core_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } - if let Some(result) = eval_json_builtin_with_values(name, evaluated_args, context, values)? { - return Ok(Some(result)); - } Ok(None) } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 1f82a9d154..b74711cf22 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -215,6 +215,11 @@ mod tests { "is_resource", "is_scalar", "is_string", + "json_decode", + "json_encode", + "json_last_error", + "json_last_error_msg", + "json_validate", "log", "min", "nl2br", @@ -302,5 +307,21 @@ mod tests { eval_declared_builtin_default_value("wordwrap", 2), Some(EvalBuiltinDefaultValue::String("\n")) ); + assert_eq!( + eval_declared_builtin_param_names("json_decode"), + Some(["json", "associative", "depth", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("json_decode", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("json_encode", 2), + Some(EvalBuiltinDefaultValue::Int(512)) + ); + assert_eq!( + eval_declared_builtin_param_names("json_last_error"), + Some([].as_slice()) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 6b4bc46549..1ce64fb711 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -165,11 +165,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "iterator_apply", "iterator_count", "iterator_to_array", - "json_decode", - "json_encode", - "json_last_error", - "json_last_error_msg", - "json_validate", "krsort", "ksort", "lchgrp", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 5df3405447..c8342cb81c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -111,7 +111,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "call_user_func" => variadic(params, &[]), "date" | "gmdate" | "strtotime" => optional(params, 1), - "json_encode" | "json_decode" | "json_validate" => optional(params, 1), "preg_match" | "preg_match_all" => optional_by_ref(params, 2, &["matches"]), "preg_split" => optional(params, 2), @@ -201,14 +200,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("date" | "gmdate", 1) => Null, ("strtotime", 1) => Null, - ("json_encode", 1) => Int(0), - ("json_encode", 2) => Int(512), - ("json_decode", 1) => Null, - ("json_decode", 2) => Int(512), - ("json_decode", 3) => Int(0), - ("json_validate", 1) => Int(512), - ("json_validate", 2) => Int(0), - ("preg_match" | "preg_match_all", 2) => EmptyArray, ("preg_match" | "preg_match_all", 3) => Int(0), ("preg_split", 2) => Int(-1), diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index b05314835d..e2e34b65b6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -22,6 +22,8 @@ pub(in crate::interpreter) enum EvalArea { Array, /// Formatting and display-oriented numeric builtins. Formatting, + /// JSON encoding, decoding, validation, and error-state builtins. + Json, /// Numeric and mathematical builtins. Math, /// String-processing builtins. diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 1759086efc..5eee7af6e6 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1686,11 +1686,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "hrtime" => eval_builtin_hrtime(args, context, scope, values), "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), "ip2long" => eval_builtin_ip2long(args, context, scope, values), - "json_decode" => eval_builtin_json_decode(args, context, scope, values), - "json_encode" => eval_builtin_json_encode(args, context, scope, values), - "json_last_error" => eval_builtin_json_last_error(args, context, values), - "json_last_error_msg" => eval_builtin_json_last_error_msg(args, context, values), - "json_validate" => eval_builtin_json_validate(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "localtime" => eval_builtin_localtime(args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index a9ad5c274d..cf9656dec9 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -29,9 +29,6 @@ const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs" ), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/json.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs" ), @@ -167,6 +164,11 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "is_scalar", "is_string", "in_array", + "json_decode", + "json_encode", + "json_last_error", + "json_last_error_msg", + "json_validate", "lcfirst", "log", "log10", From 13f83888a8ade5b5405019252fadd9359ae26e97 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Tue, 7 Jul 2026 23:56:11 +0200 Subject: [PATCH 1066/1208] refactor(magician): migrate time builtins to eval registry --- .../src/interpreter/builtins/hooks/direct.rs | 5 +- .../src/interpreter/builtins/hooks/values.rs | 9 +- .../interpreter/builtins/registry/binding.rs | 16 +- .../builtins/registry/dispatch/time.rs | 87 +--------- .../src/interpreter/builtins/registry/mod.rs | 56 +++++++ .../interpreter/builtins/registry/names.rs | 15 -- .../builtins/registry/signature.rs | 15 +- .../src/interpreter/builtins/spec.rs | 2 + .../builtins/time/declarations/checkdate.rs | 16 ++ .../builtins/time/declarations/date.rs | 18 +++ .../declarations/date_default_timezone_get.rs | 16 ++ .../declarations/date_default_timezone_set.rs | 16 ++ .../builtins/time/declarations/getdate.rs | 18 +++ .../builtins/time/declarations/gmdate.rs | 18 +++ .../builtins/time/declarations/gmmktime.rs | 16 ++ .../builtins/time/declarations/hrtime.rs | 18 +++ .../builtins/time/declarations/localtime.rs | 21 +++ .../builtins/time/declarations/microtime.rs | 18 +++ .../builtins/time/declarations/mktime.rs | 16 ++ .../builtins/time/declarations/mod.rs | 148 ++++++++++++++++++ .../builtins/time/declarations/sleep.rs | 16 ++ .../builtins/time/declarations/strtotime.rs | 18 +++ .../builtins/time/declarations/time.rs | 16 ++ .../builtins/time/declarations/usleep.rs | 16 ++ .../src/interpreter/builtins/time/mod.rs | 2 + .../src/interpreter/expressions.rs | 17 -- tests/builtin_parity_tests.rs | 15 ++ 27 files changed, 497 insertions(+), 147 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/checkdate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/date.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_get.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_set.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/getdate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/gmdate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/gmmktime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/hrtime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/localtime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/microtime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/mktime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/sleep.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/strtotime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/time.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/usleep.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 64a6e08b19..0d4a125460 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -30,7 +30,7 @@ use super::super::{ eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, - eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, + eval_builtin_time_call, eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, }; /// Direct expression-level dispatch hooks for migrated builtins. @@ -146,6 +146,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Substr, /// Dispatches `substr_replace(...)`. SubstrReplace, + /// Dispatches date, time, and sleep builtins. + Time, /// Dispatches trim-family builtins. TrimLike, /// Dispatches scalar and container type predicates. @@ -230,6 +232,7 @@ impl EvalDirectHook { Self::Strstr => eval_builtin_strstr(args, context, scope, values), Self::Substr => eval_builtin_substr(args, context, scope, values), Self::SubstrReplace => eval_builtin_substr_replace(args, context, scope, values), + Self::Time => eval_builtin_time_call(name, args, context, scope, values), Self::TrimLike => eval_builtin_trim_like(name, args, context, scope, values), Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), Self::Ucwords => eval_builtin_ucwords(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 22930b5509..e4e2682587 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -27,9 +27,9 @@ use super::super::{ eval_range_result, eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, eval_string_search_result, - eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_trim_like_result, - eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, - eval_wordwrap_result, + eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, + eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, + eval_url_encode_result, eval_wordwrap_result, }; /// Evaluated-argument dispatch hooks for migrated builtins. @@ -145,6 +145,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Substr, /// Dispatches `substr_replace(...)`. SubstrReplace, + /// Dispatches date, time, and sleep builtins. + Time, /// Dispatches trim-family builtins. TrimLike, /// Dispatches scalar and container type predicates. @@ -353,6 +355,7 @@ impl EvalValuesHook { } _ => Err(EvalStatus::RuntimeFatal), }, + Self::Time => eval_time_values_result(name, evaluated_args, context, values), Self::TrimLike => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values), [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index b13a3a6fbb..6e13efe99e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -174,10 +174,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "closedir" | "readdir" | "rewinddir" => Some(&["dir_handle"]), "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), "copy" | "rename" => Some(&["from", "to"]), - "checkdate" => Some(&["month", "day", "year"]), - "date" | "gmdate" => Some(&["format", "timestamp"]), - "date_default_timezone_get" => Some(&[]), - "date_default_timezone_set" => Some(&["timezoneId"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "die" | "exit" => Some(&["status"]), @@ -217,7 +213,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "ftruncate" => Some(&["stream", "size"]), "fwrite" => Some(&["stream", "data"]), "function_exists" => Some(&["function"]), - "getdate" => Some(&["timestamp"]), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), "gethostbyaddr" => Some(&["ip"]), "gethostbyname" => Some(&["hostname"]), @@ -239,7 +234,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "hash_init" => Some(&["algo", "flags", "key"]), "hash_update" => Some(&["context", "data"]), "header" => Some(&["header", "replace", "response_code"]), - "hrtime" => Some(&["as_number"]), "http_response_code" => Some(&["response_code"]), "gzcompress" | "gzdeflate" => Some(&["data", "level"]), "gzinflate" | "gzuncompress" => Some(&["data", "max_length"]), @@ -254,11 +248,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), "md5" | "sha1" => Some(&["string", "binary"]), - "localtime" => Some(&["timestamp", "associative"]), - "microtime" => Some(&["as_float"]), - "mktime" | "gmmktime" => { - Some(&["hour", "minute", "second", "month", "day", "year"]) - } "pathinfo" => Some(&["path", "flags"]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), @@ -288,7 +277,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "realpath" => Some(&["path"]), "stream_resolve_include_path" => Some(&["filename"]), "realpath_cache_get" | "realpath_cache_size" => Some(&[]), - "sleep" => Some(&["seconds"]), "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), "spl_autoload_unregister" => Some(&["callback"]), "spl_autoload_functions" | "spl_classes" => Some(&[]), @@ -337,15 +325,13 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_socket_shutdown" => Some(&["stream", "mode"]), "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), - "strtotime" => Some(&["datetime", "baseTimestamp"]), - "sys_get_temp_dir" | "time" | "tmpfile" => Some(&[]), + "sys_get_temp_dir" | "tmpfile" => Some(&[]), "tempnam" => Some(&["directory", "prefix"]), "touch" => Some(&["filename", "mtime", "atime"]), "chown" | "lchown" => Some(&["filename", "user"]), "chgrp" | "lchgrp" => Some(&["filename", "group"]), "long2ip" => Some(&["ip"]), "umask" => Some(&["mask"]), - "usleep" => Some(&["microseconds"]), "vfprintf" => Some(&["stream", "format", "values"]), "vsprintf" | "vprintf" => Some(&["format", "values"]), _ => None, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs index f31148efbc..b56a6775ca 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs @@ -1,17 +1,18 @@ //! Purpose: -//! Dispatches already evaluated date, time, and sleep builtins by dynamic callable name. +//! Dispatches remaining already evaluated response-header builtins by dynamic callable name. //! //! Called from: //! - `crate::interpreter::builtins::registry::dispatch`. //! //! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. +//! - Date, time, and sleep builtins have migrated to declarative specs. +//! - Returns `Ok(None)` for names outside this small legacy surface so the +//! parent dispatcher can continue probing other builtin families. use super::super::super::super::*; use super::super::super::*; -/// Attempts to dispatch evaluated date, time, and sleep builtins. +/// Attempts to dispatch evaluated header and response-code builtins. pub(in crate::interpreter) fn eval_time_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], @@ -19,39 +20,6 @@ pub(in crate::interpreter) fn eval_time_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "checkdate" => { - let [month, day, year] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_checkdate_result(*month, *day, *year, values)? - } - "date" | "gmdate" => match evaluated_args { - [format] => eval_date_result(name, *format, None, context, values)?, - [format, timestamp] => eval_date_result(name, *format, Some(*timestamp), context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "date_default_timezone_get" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_date_default_timezone_get_result(context, values)? - } - "date_default_timezone_set" => { - let [timezone] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_date_default_timezone_set_result(*timezone, context, values)? - } - "getdate" => match evaluated_args { - [] => eval_getdate_result(None, context, values)?, - [timestamp] => eval_getdate_result(Some(*timestamp), context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "hrtime" => match evaluated_args { - [] => eval_hrtime_result(None, values)?, - [as_number] => eval_hrtime_result(Some(*as_number), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "header" => match evaluated_args { [line] => eval_header_result(*line, None, None, context, values)?, [line, replace] => eval_header_result(*line, Some(*replace), None, context, values)?, @@ -65,51 +33,6 @@ pub(in crate::interpreter) fn eval_time_builtin_with_values( [response_code] => eval_http_response_code_result(Some(*response_code), context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "localtime" => match evaluated_args { - [] => eval_localtime_result(None, None, context, values)?, - [timestamp] => eval_localtime_result(Some(*timestamp), None, context, values)?, - [timestamp, associative] => { - eval_localtime_result(Some(*timestamp), Some(*associative), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "microtime" => match evaluated_args { - [] | [_] => eval_microtime_result(values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "mktime" | "gmmktime" => { - let [hour, minute, second, month, day, year] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_mktime_result( - name, *hour, *minute, *second, *month, *day, *year, context, values, - )? - } - "sleep" => { - let [seconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sleep_result(*seconds, values)? - } - "time" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values)? - } - "strtotime" => match evaluated_args { - [datetime] => eval_strtotime_result(*datetime, None, context, values)?, - [datetime, base_timestamp] => { - eval_strtotime_result(*datetime, Some(*base_timestamp), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "usleep" => { - let [microseconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_usleep_result(*microseconds, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index b74711cf22..563877934a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -188,14 +188,22 @@ mod tests { "boolval", "base64_encode", "bin2hex", + "checkdate", "count", "ctype_alpha", + "date", + "date_default_timezone_get", + "date_default_timezone_set", "floatval", + "getdate", "gettype", + "gmdate", + "gmmktime", "grapheme_strrev", "hash_equals", "hex2bin", "htmlspecialchars", + "hrtime", "intval", "is_array", "is_bool", @@ -220,21 +228,28 @@ mod tests { "json_last_error", "json_last_error_msg", "json_validate", + "localtime", "log", + "microtime", "min", + "mktime", "nl2br", "number_format", "range", "rawurlencode", + "sleep", "str_contains", "str_pad", "str_replace", "strlen", "str_repeat", "strrev", + "strtotime", "substr", + "time", "trim", "strval", + "usleep", "wordwrap", ] { assert!( @@ -323,5 +338,46 @@ mod tests { eval_declared_builtin_param_names("json_last_error"), Some([].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("date"), + Some(["format", "timestamp"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("date", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_param_names("date_default_timezone_get"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getdate"), + Some(["timestamp"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("hrtime"), + Some(["as_number"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hrtime", 0), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_default_value("localtime", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("localtime", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("microtime"), + Some(["as_float"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("strtotime"), + Some(["datetime", "baseTimestamp"].as_slice()) + ); + assert_eq!(eval_declared_builtin_param_names("time"), Some([].as_slice())); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 1ce64fb711..e6ffac598a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -56,10 +56,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "clearstatcache", "closedir", "copy", - "checkdate", - "date", - "date_default_timezone_get", - "date_default_timezone_set", "define", "defined", "die", @@ -107,7 +103,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ftruncate", "function_exists", "fwrite", - "getdate", "get_called_class", "get_class", "get_class_methods", @@ -128,8 +123,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "getprotobynumber", "getservbyname", "getservbyport", - "gmdate", - "gmmktime", "glob", "gzcompress", "gzdeflate", @@ -144,7 +137,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "hash_init", "hash_update", "header", - "hrtime", "http_response_code", "implode", "inet_ntop", @@ -171,14 +163,11 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "lchown", "link", "linkinfo", - "localtime", "long2ip", "lstat", "md5", "method_exists", - "microtime", "mkdir", - "mktime", "mt_rand", "natcasesort", "natsort", @@ -233,7 +222,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "sha1", "shell_exec", "shuffle", - "sleep", "sort", "spl_autoload", "spl_autoload_call", @@ -291,12 +279,10 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_wrapper_register", "stream_wrapper_restore", "stream_wrapper_unregister", - "strtotime", "symlink", "sys_get_temp_dir", "system", "tempnam", - "time", "tmpfile", "touch", "trait_exists", @@ -305,7 +291,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "umask", "unlink", "unset", - "usleep", "usort", "var_dump", "vfprintf", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index c8342cb81c..f4dd5e4b27 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -76,14 +76,10 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "get_class" | "get_parent_class" => optional(params, 0), "is_a" | "is_subclass_of" => optional(params, 2), - "getdate" | "hrtime" => optional(params, 0), "header" => optional(params, 1), "http_response_code" => optional(params, 0), "is_callable" => optional_by_ref(params, 1, &["callable_name"]), - "localtime" => optional(params, 0), - "microtime" | "php_uname" | "readline" | "umask" | "exit" | "die" => { - optional(params, 0) - } + "php_uname" | "readline" | "umask" | "exit" | "die" => optional(params, 0), "explode" => optional(params, 2), "implode" => optional(params, 1), @@ -110,8 +106,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "call_user_func" => variadic(params, &[]), - "date" | "gmdate" | "strtotime" => optional(params, 1), - "preg_match" | "preg_match_all" => optional_by_ref(params, 2, &["matches"]), "preg_split" => optional(params, 2), "print_r" => optional(params, 1), @@ -170,16 +164,11 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_a", 2) => Bool(false), ("is_subclass_of", 2) => Bool(true), - ("getdate", 0) => Null, ("header", 1) => Bool(true), ("header", 2) => Int(0), - ("hrtime", 0) => Bool(false), ("http_response_code", 0) => Int(0), ("is_callable", 1) => Bool(false), ("is_callable", 2) => Null, - ("localtime", 0) => Null, - ("localtime", 1) => Bool(false), - ("microtime", 0) => Bool(false), ("php_uname", 0) => String("a"), ("readline" | "umask", 0) => Null, ("exit" | "die", 0) => Int(0), @@ -198,8 +187,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("array_filter", 2) => Int(0), ("array_reduce", 2) => Null, - ("date" | "gmdate", 1) => Null, - ("strtotime", 1) => Null, ("preg_match" | "preg_match_all", 2) => EmptyArray, ("preg_match" | "preg_match_all", 3) => Int(0), ("preg_split", 2) => Int(-1), diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index e2e34b65b6..3e3798c183 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -28,6 +28,8 @@ pub(in crate::interpreter) enum EvalArea { Math, /// String-processing builtins. String, + /// Date, time, and sleep builtins. + Time, /// Scalar conversion and type-related builtins. Types, } diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/checkdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/checkdate.rs new file mode 100644 index 0000000000..4068c996fd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/checkdate.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `checkdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the calendar hook. + +eval_builtin! { + name: "checkdate", + area: Time, + params: [month, day, year], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date.rs new file mode 100644 index 0000000000..d17a5546d8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `date`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the date-format hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "date", + area: Time, + params: [format, timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_get.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_get.rs new file mode 100644 index 0000000000..9648e6dd68 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_get.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `date_default_timezone_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to eval context timezone state. + +eval_builtin! { + name: "date_default_timezone_get", + area: Time, + params: [], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_set.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_set.rs new file mode 100644 index 0000000000..2cc1f4bbaf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_set.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `date_default_timezone_set`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to eval context timezone state. + +eval_builtin! { + name: "date_default_timezone_set", + area: Time, + params: [timezoneId], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/getdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/getdate.rs new file mode 100644 index 0000000000..1657921038 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/getdate.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `getdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the local calendar hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "getdate", + area: Time, + params: [timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmdate.rs new file mode 100644 index 0000000000..f520305a69 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmdate.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `gmdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the UTC date-format hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gmdate", + area: Time, + params: [format, timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmmktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmmktime.rs new file mode 100644 index 0000000000..c10d760ced --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmmktime.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `gmmktime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the UTC mktime hook. + +eval_builtin! { + name: "gmmktime", + area: Time, + params: [hour, minute, second, month, day, year], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/hrtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/hrtime.rs new file mode 100644 index 0000000000..d59f229bb2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/hrtime.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `hrtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the high-resolution clock hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hrtime", + area: Time, + params: [as_number = EvalBuiltinDefaultValue::Bool(false)], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/localtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/localtime.rs new file mode 100644 index 0000000000..179d6359dd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/localtime.rs @@ -0,0 +1,21 @@ +//! Purpose: +//! Declarative eval registry entry for `localtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the local calendar hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "localtime", + area: Time, + params: [ + timestamp = EvalBuiltinDefaultValue::Null, + associative = EvalBuiltinDefaultValue::Bool(false), + ], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/microtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/microtime.rs new file mode 100644 index 0000000000..39e2a72267 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/microtime.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `microtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the wall-clock hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "microtime", + area: Time, + params: [as_float = EvalBuiltinDefaultValue::Bool(false)], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/mktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/mktime.rs new file mode 100644 index 0000000000..292665a565 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/mktime.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `mktime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the local mktime hook. + +eval_builtin! { + name: "mktime", + area: Time, + params: [hour, minute, second, month, day, year], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs new file mode 100644 index 0000000000..4074a43ec9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs @@ -0,0 +1,148 @@ +//! Purpose: +//! Declarative eval registry entries and dispatch adapters for time builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` module loading. +//! - `crate::interpreter::builtins::hooks` for migrated time dispatch. +//! +//! Key details: +//! - Time/date algorithms remain in sibling helper modules such as `date`, +//! `calendar`, `clock`, `mktime`, `sleep`, and `strtotime`. +//! - This module owns registry metadata and small hook adapters only. + +use super::super::super::*; +use super::*; + +mod checkdate; +mod date; +mod date_default_timezone_get; +mod date_default_timezone_set; +mod getdate; +mod gmdate; +mod gmmktime; +mod hrtime; +mod localtime; +mod microtime; +mod mktime; +mod sleep; +mod strtotime; +mod time; +mod usleep; + +/// Dispatches direct expression-level calls for declaratively migrated time builtins. +pub(in crate::interpreter) fn eval_builtin_time_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "checkdate" => eval_builtin_checkdate(args, context, scope, values), + "date" | "gmdate" => eval_builtin_date_like(name, args, context, scope, values), + "date_default_timezone_get" => eval_builtin_date_default_timezone_get(args, context, values), + "date_default_timezone_set" => { + eval_builtin_date_default_timezone_set(args, context, scope, values) + } + "getdate" => eval_builtin_getdate(args, context, scope, values), + "gmmktime" | "mktime" => eval_builtin_mktime_like(name, args, context, scope, values), + "hrtime" => eval_builtin_hrtime(args, context, scope, values), + "localtime" => eval_builtin_localtime(args, context, scope, values), + "microtime" => eval_builtin_microtime(args, context, scope, values), + "sleep" => eval_builtin_sleep(args, context, scope, values), + "strtotime" => eval_builtin_strtotime(args, context, scope, values), + "time" => eval_builtin_time(args, values), + "usleep" => eval_builtin_usleep(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated time builtins. +pub(in crate::interpreter) fn eval_time_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "checkdate" => { + let [month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_checkdate_result(*month, *day, *year, values) + } + "date" | "gmdate" => match evaluated_args { + [format] => eval_date_result(name, *format, None, context, values), + [format, timestamp] => eval_date_result(name, *format, Some(*timestamp), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "date_default_timezone_get" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_date_default_timezone_get_result(context, values) + } + "date_default_timezone_set" => { + let [timezone] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_date_default_timezone_set_result(*timezone, context, values) + } + "getdate" => match evaluated_args { + [] => eval_getdate_result(None, context, values), + [timestamp] => eval_getdate_result(Some(*timestamp), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "gmmktime" | "mktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_mktime_result( + name, *hour, *minute, *second, *month, *day, *year, context, values, + ) + } + "hrtime" => match evaluated_args { + [] => eval_hrtime_result(None, values), + [as_number] => eval_hrtime_result(Some(*as_number), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "localtime" => match evaluated_args { + [] => eval_localtime_result(None, None, context, values), + [timestamp] => eval_localtime_result(Some(*timestamp), None, context, values), + [timestamp, associative] => { + eval_localtime_result(Some(*timestamp), Some(*associative), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "microtime" => match evaluated_args { + [] | [_] => eval_microtime_result(values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "sleep" => { + let [seconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sleep_result(*seconds, values) + } + "strtotime" => match evaluated_args { + [datetime] => eval_strtotime_result(*datetime, None, context, values), + [datetime, base_timestamp] => { + eval_strtotime_result(*datetime, Some(*base_timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "time" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) + } + "usleep" => { + let [microseconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_usleep_result(*microseconds, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/sleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/sleep.rs new file mode 100644 index 0000000000..fe124c34f8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/sleep.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `sleep`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the sleep hook. + +eval_builtin! { + name: "sleep", + area: Time, + params: [seconds], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/strtotime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/strtotime.rs new file mode 100644 index 0000000000..309bf2ee88 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/strtotime.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `strtotime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the date parser hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strtotime", + area: Time, + params: [datetime, baseTimestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/time.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/time.rs new file mode 100644 index 0000000000..84e74f1318 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/time.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `time`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the wall-clock hook. + +eval_builtin! { + name: "time", + area: Time, + params: [], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/usleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/usleep.rs new file mode 100644 index 0000000000..35965c8e3f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/usleep.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `usleep`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the microsecond sleep hook. + +eval_builtin! { + name: "usleep", + area: Time, + params: [microseconds], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs index ee2aa91489..09f06f347a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs @@ -13,6 +13,7 @@ mod calendar; mod aliases; mod clock; mod date; +mod declarations; mod mktime; mod sleep; mod strtotime; @@ -22,6 +23,7 @@ pub(in crate::interpreter) use aliases::*; pub(in crate::interpreter) use calendar::*; pub(in crate::interpreter) use clock::*; pub(in crate::interpreter) use date::*; +pub(in crate::interpreter) use declarations::*; pub(in crate::interpreter) use mktime::*; pub(in crate::interpreter) use sleep::*; pub(in crate::interpreter) use strtotime::*; diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 5eee7af6e6..ca630dc467 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1584,14 +1584,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "copy" | "link" | "rename" | "symlink" => { eval_builtin_binary_path_bool(name, args, context, scope, values) } - "checkdate" => eval_builtin_checkdate(args, context, scope, values), - "date" | "gmdate" => eval_builtin_date_like(name, args, context, scope, values), - "date_default_timezone_get" => { - eval_builtin_date_default_timezone_get(args, context, values) - } - "date_default_timezone_set" => { - eval_builtin_date_default_timezone_set(args, context, scope, values) - } "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "dirname" => eval_builtin_dirname(args, context, scope, values), @@ -1640,7 +1632,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(name, args, context, scope, values) } - "getdate" => eval_builtin_getdate(args, context, scope, values), "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), "gethostname" => eval_builtin_gethostname(args, values), @@ -1683,13 +1674,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), - "hrtime" => eval_builtin_hrtime(args, context, scope, values), "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), "ip2long" => eval_builtin_ip2long(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), - "localtime" => eval_builtin_localtime(args, context, scope, values), - "microtime" => eval_builtin_microtime(args, context, scope, values), - "mktime" | "gmmktime" => eval_builtin_mktime_like(name, args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), @@ -1719,7 +1706,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), "scandir" => eval_builtin_scandir(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), - "sleep" => eval_builtin_sleep(args, context, scope, values), "spl_autoload_register" | "spl_autoload_unregister" => { eval_builtin_spl_autoload_bool(name, args, context, scope, values) } @@ -1740,7 +1726,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "tempnam" => eval_builtin_tempnam(args, context, scope, values), - "time" => eval_builtin_time(args, values), "touch" => eval_builtin_touch(args, context, scope, values), "tmpfile" => eval_builtin_tmpfile(args, context, values), "stream_is_local" | "stream_supports_lock" => { @@ -1815,12 +1800,10 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_stream_set_buffer_like(name, args, context, scope, values) } "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), - "strtotime" => eval_builtin_strtotime(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), "long2ip" => eval_builtin_long2ip(args, context, scope, values), "unset" => eval_builtin_unset(args, context, scope, values), "umask" => eval_builtin_umask(args, context, scope, values), - "usleep" => eval_builtin_usleep(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index cf9656dec9..ce14f086c2 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -118,6 +118,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "bin2hex", "boolval", "ceil", + "checkdate", "chr", "chop", "clamp", @@ -129,19 +130,26 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "ctype_alpha", "ctype_digit", "ctype_space", + "date", + "date_default_timezone_get", + "date_default_timezone_set", "deg2rad", "exp", "fdiv", "floatval", "floor", "fmod", + "getdate", "gettype", + "gmdate", + "gmmktime", "grapheme_strrev", "hash_equals", "hex2bin", "html_entity_decode", "htmlentities", "htmlspecialchars", + "hrtime", "hypot", "intdiv", "intval", @@ -170,12 +178,15 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "json_last_error_msg", "json_validate", "lcfirst", + "localtime", "log", "log10", "log2", "ltrim", "max", + "microtime", "min", + "mktime", "nl2br", "number_format", "ord", @@ -189,6 +200,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "rtrim", "sin", "sinh", + "sleep", "sqrt", "str_contains", "str_ends_with", @@ -208,16 +220,19 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "strtolower", "stripslashes", "strtoupper", + "strtotime", "substr", "substr_replace", "strval", "tan", "tanh", + "time", "trim", "ucfirst", "ucwords", "urldecode", "urlencode", + "usleep", "wordwrap", ]; From 7e079f1577fb8b2ca9f85fd8caa42d352e44e736 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 00:17:22 +0200 Subject: [PATCH 1067/1208] refactor(magician): migrate regex builtins to eval registry --- .../src/interpreter/builtins/hooks/direct.rs | 12 +- .../src/interpreter/builtins/hooks/values.rs | 16 ++- .../src/interpreter/builtins/macros.rs | 38 ++++++ .../builtins/regex/declarations/mod.rs | 119 ++++++++++++++++++ .../builtins/regex/declarations/preg_match.rs | 25 ++++ .../regex/declarations/preg_match_all.rs | 25 ++++ .../regex/declarations/preg_replace.rs | 16 +++ .../declarations/preg_replace_callback.rs | 17 +++ .../builtins/regex/declarations/preg_split.rs | 23 ++++ .../src/interpreter/builtins/regex/mod.rs | 2 + .../interpreter/builtins/registry/binding.rs | 4 - .../builtins/registry/dispatch/mod.rs | 5 - .../builtins/registry/dispatch/regex.rs | 91 -------------- .../src/interpreter/builtins/registry/mod.rs | 39 ++++++ .../interpreter/builtins/registry/names.rs | 5 - .../builtins/registry/signature.rs | 6 - .../src/interpreter/builtins/spec.rs | 2 + .../src/interpreter/expressions.rs | 5 - tests/builtin_parity_tests.rs | 8 +- 19 files changed, 329 insertions(+), 129 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match_all.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace_callback.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_split.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 0d4a125460..badcd51595 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -27,10 +27,11 @@ use super::super::{ eval_builtin_array_slice, eval_builtin_array_unique, eval_builtin_cast, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_nl2br, eval_builtin_range, eval_builtin_str_pad, - eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_string_case, - eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, - eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, - eval_builtin_time_call, eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, + eval_builtin_regex_call, eval_builtin_str_replace, eval_builtin_str_split, + eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, + eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, + eval_builtin_substr_replace, eval_builtin_time_call, eval_builtin_trim_like, + eval_builtin_ucwords, eval_builtin_wordwrap, }; /// Direct expression-level dispatch hooks for migrated builtins. @@ -116,6 +117,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Round, /// Dispatches `range(...)`. Range, + /// Dispatches regex builtins. + Regex, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, /// Dispatches `sqrt(...)`. @@ -215,6 +218,7 @@ impl EvalDirectHook { Self::Pow => eval_builtin_pow(args, context, scope, values), Self::Round => eval_builtin_round(args, context, scope, values), Self::Range => eval_builtin_range(args, context, scope, values), + Self::Regex => eval_builtin_regex_call(name, args, context, scope, values), Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), Self::StringCase => eval_builtin_string_case(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index e4e2682587..cabab72852 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -24,12 +24,13 @@ use super::super::{ eval_gettype_result, eval_grapheme_strrev_result, eval_hash_equals_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_json_values_result, eval_log_result, eval_min_max_result, eval_nl2br_result, eval_number_format_result, - eval_range_result, eval_slashes_result, eval_str_pad_result, eval_str_replace_result, - eval_str_repeat_result, eval_str_split_result, eval_string_case_result, - eval_string_compare_result, eval_string_position_result, eval_string_search_result, - eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, - eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, - eval_url_encode_result, eval_wordwrap_result, + eval_range_result, eval_regex_values_result, eval_slashes_result, eval_str_pad_result, + eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, + eval_string_case_result, eval_string_compare_result, eval_string_position_result, + eval_string_search_result, eval_strstr_result, eval_substr_replace_result, + eval_substr_result, eval_time_values_result, eval_trim_like_result, + eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, + eval_wordwrap_result, }; /// Evaluated-argument dispatch hooks for migrated builtins. @@ -115,6 +116,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Round, /// Dispatches `range(...)`. Range, + /// Dispatches regex builtins. + Regex, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, /// Dispatches `sqrt(...)`. @@ -287,6 +290,7 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::Range => two_args(evaluated_args, values, eval_range_result), + Self::Regex => eval_regex_values_result(name, evaluated_args, context, values), Self::Slashes => one_arg(evaluated_args, values, |value, values| { eval_slashes_result(name, value, values) }), diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index 13d65e407d..16d0df2933 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -12,6 +12,36 @@ //! over `RuntimeValueOps`. macro_rules! eval_builtin { + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(: $mode:ident)? $(= $default:expr)?),* $(,)?], + by_ref: [$($by_ref:ident),* $(,)?], + direct: $direct:ident, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param)),*], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: eval_builtin!(@param_by_ref $($mode)?), + }, + )* + ], + variadic: None, + by_ref_params: &[$(eval_builtin!(@name_str $by_ref)),*], + direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + } + } + }; + ( name: $name:literal, area: $area:ident, @@ -79,6 +109,14 @@ macro_rules! eval_builtin { Some($default) }; + (@param_by_ref) => { + false + }; + + (@param_by_ref by_ref) => { + true + }; + (@name_str r#break) => { "break" }; diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/mod.rs new file mode 100644 index 0000000000..e1ff9e5fe2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/mod.rs @@ -0,0 +1,119 @@ +//! Purpose: +//! Declarative eval registry entries and dispatch adapters for preg regex builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex` module loading. +//! - `crate::interpreter::builtins::hooks` for migrated regex dispatch. +//! +//! Key details: +//! - Regex parsing, matching, capture assembly, replacement, and split behavior +//! stay in sibling helper modules. +//! - `preg_match()` and `preg_match_all()` keep source-sensitive direct paths +//! for `$matches` by-reference writeback. + +use super::super::super::*; +use super::*; + +mod preg_match; +mod preg_match_all; +mod preg_replace; +mod preg_replace_callback; +mod preg_split; + +/// Dispatches direct expression-level calls for declaratively migrated regex builtins. +pub(in crate::interpreter) fn eval_builtin_regex_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "preg_match" => eval_builtin_preg_match(args, context, scope, values), + "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), + "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), + "preg_replace_callback" => { + eval_builtin_preg_replace_callback(args, context, scope, values) + } + "preg_split" => eval_builtin_preg_split(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated regex builtins. +pub(in crate::interpreter) fn eval_regex_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "preg_match" => match evaluated_args { + [pattern, subject] => eval_preg_match_result(*pattern, *subject, values), + [pattern, subject, _matches] => { + values.warning( + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (matched, matches) = + eval_preg_match_capture_result(*pattern, *subject, None, values)?; + values.release(matches)?; + Ok(matched) + } + [pattern, subject, _matches, flags] => { + values.warning( + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (matched, matches) = + eval_preg_match_capture_result(*pattern, *subject, Some(*flags), values)?; + values.release(matches)?; + Ok(matched) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "preg_match_all" => match evaluated_args { + [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values), + [pattern, subject, _matches] => { + values.warning( + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (count, matches) = + eval_preg_match_all_capture_result(*pattern, *subject, None, values)?; + values.release(matches)?; + Ok(count) + } + [pattern, subject, _matches, flags] => { + values.warning( + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (count, matches) = + eval_preg_match_all_capture_result(*pattern, *subject, Some(*flags), values)?; + values.release(matches)?; + Ok(count) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "preg_replace" => { + let [pattern, replacement, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_preg_replace_result(*pattern, *replacement, *subject, values) + } + "preg_replace_callback" => { + let [pattern, callback, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values) + } + "preg_split" => match evaluated_args { + [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values), + [pattern, subject, limit] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), None, values) + } + [pattern, subject, limit, flags] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match.rs new file mode 100644 index 0000000000..0d3cfa771a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match.rs @@ -0,0 +1,25 @@ +//! Purpose: +//! Declarative eval registry entry for `preg_match`. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::declarations`. +//! +//! Key details: +//! - `$matches` is by-reference and is still written through the regex direct +//! and dynamic mutation helpers. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "preg_match", + area: Regex, + params: [ + pattern, + subject, + matches: by_ref = EvalBuiltinDefaultValue::EmptyArray, + flags = EvalBuiltinDefaultValue::Int(0), + ], + by_ref: [matches], + direct: Regex, + values: Regex, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match_all.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match_all.rs new file mode 100644 index 0000000000..eef99dda15 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match_all.rs @@ -0,0 +1,25 @@ +//! Purpose: +//! Declarative eval registry entry for `preg_match_all`. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::declarations`. +//! +//! Key details: +//! - `$matches` is by-reference and is still written through the regex direct +//! and dynamic mutation helpers. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "preg_match_all", + area: Regex, + params: [ + pattern, + subject, + matches: by_ref = EvalBuiltinDefaultValue::EmptyArray, + flags = EvalBuiltinDefaultValue::Int(0), + ], + by_ref: [matches], + direct: Regex, + values: Regex, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace.rs new file mode 100644 index 0000000000..0d331229e6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `preg_replace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the regex replacement hook. + +eval_builtin! { + name: "preg_replace", + area: Regex, + params: [pattern, replacement, subject], + direct: Regex, + values: Regex, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace_callback.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace_callback.rs new file mode 100644 index 0000000000..1c091792d9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace_callback.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `preg_replace_callback`. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::declarations`. +//! +//! Key details: +//! - Direct calls keep lexical scope for callback names; evaluated dynamic +//! dispatch uses the same scope-free behavior as the legacy dispatcher. + +eval_builtin! { + name: "preg_replace_callback", + area: Regex, + params: [pattern, callback, subject], + direct: Regex, + values: Regex, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_split.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_split.rs new file mode 100644 index 0000000000..7063696235 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_split.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `preg_split`. +//! +//! Called from: +//! - `crate::interpreter::builtins::regex::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the split hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "preg_split", + area: Regex, + params: [ + pattern, + subject, + limit = EvalBuiltinDefaultValue::Int(-1), + flags = EvalBuiltinDefaultValue::Int(0), + ], + direct: Regex, + values: Regex, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs index ade026f965..4595c62477 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs @@ -11,6 +11,7 @@ //! behavior through `RuntimeValueOps`. mod captures; +mod declarations; mod engine; mod match_all; mod match_one; @@ -22,6 +23,7 @@ mod split_helpers; mod targets; pub(in crate::interpreter) use captures::*; +pub(in crate::interpreter) use declarations::*; pub(in crate::interpreter) use engine::*; pub(in crate::interpreter) use match_all::*; pub(in crate::interpreter) use match_one::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 6e13efe99e..cc32c3e825 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -265,10 +265,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } "ptr_write_string" => Some(&["pointer", "string"]), "ptr_sizeof" => Some(&["type"]), - "preg_match" | "preg_match_all" => Some(&["pattern", "subject", "matches", "flags"]), - "preg_replace" => Some(&["pattern", "replacement", "subject"]), - "preg_replace_callback" => Some(&["pattern", "callback", "subject"]), - "preg_split" => Some(&["pattern", "subject", "limit", "flags"]), "print_r" => Some(&["value", "return"]), "var_dump" => Some(&["value", "values"]), "putenv" => Some(&["assignment"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 222531a697..4e97f16215 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -13,7 +13,6 @@ mod core; mod filesystem; mod formatting; mod network_env; -mod regex; mod scalars; mod strings; mod symbols; @@ -27,7 +26,6 @@ use core::*; use filesystem::*; use formatting::*; use network_env::*; -use regex::*; use scalars::*; use strings::*; use symbols::*; @@ -57,9 +55,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( { return Ok(Some(result)); } - if let Some(result) = eval_regex_builtin_with_values(name, evaluated_args, context, values)? { - return Ok(Some(result)); - } if let Some(result) = eval_raw_memory_builtin_with_values(name, evaluated_args, context, values)? { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs deleted file mode 100644 index c1217bdaad..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Purpose: -//! Dispatches already evaluated preg regex builtins by dynamic callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. - -use super::super::super::super::*; -use super::super::super::*; - -/// Attempts to dispatch evaluated preg regex builtins. -pub(in crate::interpreter) fn eval_regex_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "preg_match" => match evaluated_args { - [pattern, subject] => eval_preg_match_result(*pattern, *subject, values)?, - [pattern, subject, _matches] => { - values.warning( - "preg_match(): Argument #3 ($matches) must be passed by reference, value given", - )?; - let (matched, matches) = - eval_preg_match_capture_result(*pattern, *subject, None, values)?; - values.release(matches)?; - matched - } - [pattern, subject, _matches, flags] => { - values.warning( - "preg_match(): Argument #3 ($matches) must be passed by reference, value given", - )?; - let (matched, matches) = - eval_preg_match_capture_result(*pattern, *subject, Some(*flags), values)?; - values.release(matches)?; - matched - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_match_all" => match evaluated_args { - [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values)?, - [pattern, subject, _matches] => { - values.warning( - "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", - )?; - let (count, matches) = - eval_preg_match_all_capture_result(*pattern, *subject, None, values)?; - values.release(matches)?; - count - } - [pattern, subject, _matches, flags] => { - values.warning( - "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", - )?; - let (count, matches) = - eval_preg_match_all_capture_result(*pattern, *subject, Some(*flags), values)?; - values.release(matches)?; - count - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_replace" => match evaluated_args { - [pattern, replacement, subject] => { - eval_preg_replace_result(*pattern, *replacement, *subject, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_replace_callback" => match evaluated_args { - [pattern, callback, subject] => { - eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "preg_split" => match evaluated_args { - [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values)?, - [pattern, subject, limit] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), None, values)? - } - [pattern, subject, limit, flags] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 563877934a..8da7d9b530 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -87,6 +87,16 @@ fn validate_declared_builtin_spec(spec: &EvalBuiltinSpec) { ); } } + for by_ref_name in spec.by_ref_params { + assert!( + spec.params + .iter() + .any(|param| param.name == *by_ref_name && param.by_ref), + "eval builtin {} lists {} as by-ref without marking the parameter", + spec.name, + by_ref_name + ); + } if let Some(variadic) = spec.variadic { assert_eq!( spec.param_names.last().copied(), @@ -235,6 +245,11 @@ mod tests { "mktime", "nl2br", "number_format", + "preg_match", + "preg_match_all", + "preg_replace", + "preg_replace_callback", + "preg_split", "range", "rawurlencode", "sleep", @@ -379,5 +394,29 @@ mod tests { Some(["datetime", "baseTimestamp"].as_slice()) ); assert_eq!(eval_declared_builtin_param_names("time"), Some([].as_slice())); + assert_eq!( + eval_declared_builtin_param_names("preg_match"), + Some(["pattern", "subject", "matches", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_match", 2), + Some(EvalBuiltinDefaultValue::EmptyArray) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_match_all", 3), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_builtin_signature_shape("preg_match").map(|shape| shape.by_ref_params), + Some(["matches"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("preg_replace_callback"), + Some(["pattern", "callback", "subject"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_split", 2), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index e6ffac598a..283b9a8a63 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -194,11 +194,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ptr_write16", "ptr_write32", "ptr_write_string", - "preg_match", - "preg_match_all", - "preg_replace", - "preg_replace_callback", - "preg_split", "print_r", "printf", "property_exists", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index f4dd5e4b27..bd26f02c2b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -106,8 +106,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "call_user_func" => variadic(params, &[]), - "preg_match" | "preg_match_all" => optional_by_ref(params, 2, &["matches"]), - "preg_split" => optional(params, 2), "print_r" => optional(params, 1), "var_dump" => variadic(params, &[]), @@ -187,10 +185,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("array_filter", 2) => Int(0), ("array_reduce", 2) => Null, - ("preg_match" | "preg_match_all", 2) => EmptyArray, - ("preg_match" | "preg_match_all", 3) => Int(0), - ("preg_split", 2) => Int(-1), - ("preg_split", 3) => Int(0), ("print_r", 1) => Bool(false), ("touch", 1 | 2) => Null, diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index 3e3798c183..4deaeaee2d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -26,6 +26,8 @@ pub(in crate::interpreter) enum EvalArea { Json, /// Numeric and mathematical builtins. Math, + /// PCRE-style regex builtins. + Regex, /// String-processing builtins. String, /// Date, time, and sleep builtins. diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index ca630dc467..5ba08d3d2c 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1683,11 +1683,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "phpversion" => eval_builtin_phpversion(args, values), "pclose" => eval_builtin_pclose(args, context, scope, values), "popen" => eval_builtin_popen(args, context, scope, values), - "preg_match" => eval_builtin_preg_match(args, context, scope, values), - "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), - "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), - "preg_replace_callback" => eval_builtin_preg_replace_callback(args, context, scope, values), - "preg_split" => eval_builtin_preg_split(args, context, scope, values), "buffer_free" | "buffer_len" | "buffer_new" | "ptr" | "ptr_get" | "ptr_is_null" | "ptr_null" | "ptr_offset" | "ptr_read8" | "ptr_read16" | "ptr_read32" | "ptr_read_string" | "ptr_set" | "ptr_sizeof" | "ptr_write8" | "ptr_write16" diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index ce14f086c2..3a0c431553 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -32,9 +32,6 @@ const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs" ), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/regex.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs" ), @@ -192,6 +189,11 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "ord", "pi", "pow", + "preg_match", + "preg_match_all", + "preg_replace", + "preg_replace_callback", + "preg_split", "rad2deg", "range", "rawurldecode", From 1d1c18679fdfa8e03fb374fd7f2d037bbea96abf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 00:31:51 +0200 Subject: [PATCH 1068/1208] refactor(magician): migrate path builtins to eval registry --- .../filesystem/declarations/basename.rs | 18 +++++ .../filesystem/declarations/dirname.rs | 18 +++++ .../filesystem/declarations/fnmatch.rs | 18 +++++ .../builtins/filesystem/declarations/mod.rs | 68 +++++++++++++++++++ .../filesystem/declarations/pathinfo.rs | 18 +++++ .../interpreter/builtins/filesystem/mod.rs | 2 + .../src/interpreter/builtins/hooks/direct.rs | 9 ++- .../src/interpreter/builtins/hooks/values.rs | 15 ++-- .../interpreter/builtins/registry/binding.rs | 4 -- .../builtins/registry/dispatch/filesystem.rs | 22 ------ .../src/interpreter/builtins/registry/mod.rs | 24 +++++++ .../interpreter/builtins/registry/names.rs | 4 -- .../builtins/registry/signature.rs | 8 +-- .../src/interpreter/builtins/spec.rs | 2 + .../src/interpreter/expressions.rs | 4 -- tests/builtin_parity_tests.rs | 4 ++ tests/codegen/eval.rs | 15 ++++ 17 files changed, 204 insertions(+), 49 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/basename.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/dirname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fnmatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pathinfo.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/basename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/basename.rs new file mode 100644 index 0000000000..ae566f13db --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/basename.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `basename`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the path helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "basename", + area: Filesystem, + params: [path, suffix = EvalBuiltinDefaultValue::String("")], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/dirname.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/dirname.rs new file mode 100644 index 0000000000..d6bd3206ec --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/dirname.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `dirname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the path helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "dirname", + area: Filesystem, + params: [path, levels = EvalBuiltinDefaultValue::Int(1)], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fnmatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fnmatch.rs new file mode 100644 index 0000000000..9c7c68d838 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fnmatch.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `fnmatch`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the fnmatch helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fnmatch", + area: Filesystem, + params: [pattern, filename, flags = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs new file mode 100644 index 0000000000..bb8df5c07a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Declarative eval registry entries and dispatch adapters for filesystem builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem` module loading. +//! - `crate::interpreter::builtins::hooks` for migrated filesystem dispatch. +//! +//! Key details: +//! - This first tranche covers path/string-like helpers only; stream, stat, +//! mutating, and by-reference filesystem calls stay on the legacy path. + +use super::super::super::*; +use super::*; + +mod basename; +mod dirname; +mod fnmatch; +mod pathinfo; + +/// Dispatches direct expression-level calls for declaratively migrated filesystem builtins. +pub(in crate::interpreter) fn eval_builtin_filesystem_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "basename" => eval_builtin_basename(args, context, scope, values), + "dirname" => eval_builtin_dirname(args, context, scope, values), + "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), + "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated filesystem builtins. +pub(in crate::interpreter) fn eval_filesystem_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "basename" => match evaluated_args { + [path] => eval_basename_result(*path, None, values), + [path, suffix] => eval_basename_result(*path, Some(*suffix), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "dirname" => match evaluated_args { + [path] => eval_dirname_result(*path, None, values), + [path, levels] => eval_dirname_result(*path, Some(*levels), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "fnmatch" => match evaluated_args { + [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values), + [pattern, filename, flags] => { + eval_fnmatch_result(*pattern, *filename, Some(*flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "pathinfo" => match evaluated_args { + [path] => eval_pathinfo_result(*path, None, values), + [path, flags] => eval_pathinfo_result(*path, Some(*flags), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pathinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pathinfo.rs new file mode 100644 index 0000000000..55999acb2c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pathinfo.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `pathinfo`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the pathinfo helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "pathinfo", + area: Filesystem, + params: [path, flags = EvalBuiltinDefaultValue::Int(15)], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index a18ff9477d..cdcf9b53da 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -9,6 +9,7 @@ //! host filesystem. mod directories; +mod declarations; mod file_contents; mod file_io; mod fnmatch; @@ -33,6 +34,7 @@ mod user_wrapper_stat; mod user_wrapper_streams; pub(in crate::interpreter) use directories::*; +pub(in crate::interpreter) use declarations::*; pub(in crate::interpreter) use file_contents::*; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index badcd51595..e28d96e04c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -25,9 +25,9 @@ use super::super::{ eval_builtin_array_key_exists, eval_builtin_array_pad, eval_builtin_array_projection, eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, eval_builtin_array_slice, eval_builtin_array_unique, eval_builtin_cast, - eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, - eval_builtin_json_call, eval_builtin_nl2br, eval_builtin_range, eval_builtin_str_pad, - eval_builtin_regex_call, eval_builtin_str_replace, eval_builtin_str_split, + eval_builtin_filesystem_call, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, + eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_nl2br, eval_builtin_range, + eval_builtin_regex_call, eval_builtin_str_pad, eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, eval_builtin_time_call, eval_builtin_trim_like, @@ -79,6 +79,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Crc32, /// Dispatches `ctype_*` predicates. Ctype, + /// Dispatches filesystem and path builtins. + Filesystem, /// Dispatches binary floating-point builtins. FloatBinary, /// Dispatches paired floating-point builtins. @@ -199,6 +201,7 @@ impl EvalDirectHook { Self::Count => eval_builtin_count(args, context, scope, values), Self::Crc32 => eval_builtin_crc32(args, context, scope, values), Self::Ctype => eval_builtin_ctype(name, args, context, scope, values), + Self::Filesystem => eval_builtin_filesystem_call(name, args, context, scope, values), Self::FloatBinary => eval_builtin_float_binary(name, args, context, scope, values), Self::FloatPair => eval_builtin_float_pair(name, args, context, scope, values), Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index cabab72852..2f7e263cb5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -20,12 +20,12 @@ use super::super::{ eval_array_search_result, eval_array_slice_result, eval_array_unique_result, eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, eval_chr_result, eval_clamp_result, eval_crc32_result, eval_ctype_result, - eval_float_binary_result, eval_float_pair_result, eval_float_unary_result, - eval_gettype_result, eval_grapheme_strrev_result, eval_hash_equals_result, - eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_json_values_result, - eval_log_result, eval_min_max_result, eval_nl2br_result, eval_number_format_result, - eval_range_result, eval_regex_values_result, eval_slashes_result, eval_str_pad_result, - eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, + eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, + eval_float_unary_result, eval_gettype_result, eval_grapheme_strrev_result, + eval_hash_equals_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, + eval_json_values_result, eval_log_result, eval_min_max_result, eval_nl2br_result, + eval_number_format_result, eval_range_result, eval_regex_values_result, eval_slashes_result, + eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, eval_trim_like_result, @@ -78,6 +78,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Crc32, /// Dispatches `ctype_*` predicates. Ctype, + /// Dispatches filesystem and path builtins. + Filesystem, /// Dispatches binary floating-point builtins. FloatBinary, /// Dispatches paired floating-point builtins. @@ -226,6 +228,7 @@ impl EvalValuesHook { Self::Ctype => one_arg(evaluated_args, values, |value, values| { eval_ctype_result(name, value, values) }), + Self::Filesystem => eval_filesystem_values_result(name, evaluated_args, values), Self::FloatBinary => two_args(evaluated_args, values, |left, right, values| { eval_float_binary_result(name, left, right, values) }), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index cc32c3e825..ffca60d90f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -142,7 +142,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } "array_push" | "array_unshift" => Some(&["array", "values"]), "array_splice" => Some(&["array", "offset", "length", "replacement"]), - "basename" => Some(&["path", "suffix"]), "empty" => Some(&["value"]), "is_callable" => Some(&["value", "syntax_only", "callable_name"]), "buffer_new" => Some(&["length"]), @@ -177,7 +176,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "die" | "exit" => Some(&["status"]), - "dirname" => Some(&["path", "levels"]), "disk_free_space" | "disk_total_space" => Some(&["directory"]), "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), "explode" => Some(&["separator", "string", "limit"]), @@ -193,7 +191,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "rewind" | "fstat" | "stream_get_meta_data" => Some(&["stream"]), - "fnmatch" => Some(&["pattern", "filename", "flags"]), "fgetcsv" => Some(&["stream", "length", "separator"]), "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" @@ -248,7 +245,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "link" | "symlink" => Some(&["target", "link"]), "linkinfo" | "readlink" => Some(&["path"]), "md5" | "sha1" => Some(&["string", "binary"]), - "pathinfo" => Some(&["path", "flags"]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), "pclose" => Some(&["handle"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 08b46c8d58..dfc357dd8f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -116,13 +116,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_filetype_result(*filename, context, values)? } - "fnmatch" => match evaluated_args { - [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, - [pattern, filename, flags] => { - eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, "fgetcsv" => match evaluated_args { [stream] => eval_fgetcsv_result(*stream, None, None, context, values)?, [stream, length] => { @@ -249,16 +242,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_scandir_result(*directory, values)? } - "basename" => match evaluated_args { - [path] => eval_basename_result(*path, None, values)?, - [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "dirname" => match evaluated_args { - [path] => eval_dirname_result(*path, None, values)?, - [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "disk_free_space" | "disk_total_space" => { let [directory] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -283,11 +266,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_opendir_result(*directory, context, values)? } - "pathinfo" => match evaluated_args { - [path] => eval_pathinfo_result(*path, None, values)?, - [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "pclose" => { let [handle] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 8da7d9b530..18046b3a91 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -195,6 +195,7 @@ mod tests { "array_keys", "array_reverse", "array_sum", + "basename", "boolval", "base64_encode", "bin2hex", @@ -204,7 +205,9 @@ mod tests { "date", "date_default_timezone_get", "date_default_timezone_set", + "dirname", "floatval", + "fnmatch", "getdate", "gettype", "gmdate", @@ -245,6 +248,7 @@ mod tests { "mktime", "nl2br", "number_format", + "pathinfo", "preg_match", "preg_match_all", "preg_replace", @@ -418,5 +422,25 @@ mod tests { eval_declared_builtin_default_value("preg_split", 2), Some(EvalBuiltinDefaultValue::Int(-1)) ); + assert_eq!( + eval_declared_builtin_param_names("basename"), + Some(["path", "suffix"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("basename", 1), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_default_value("dirname", 1), + Some(EvalBuiltinDefaultValue::Int(1)) + ); + assert_eq!( + eval_declared_builtin_default_value("fnmatch", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_default_value("pathinfo", 1), + Some(EvalBuiltinDefaultValue::Int(15)) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 283b9a8a63..5953679cb4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -35,7 +35,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "array_walk", "arsort", "asort", - "basename", "buffer_free", "buffer_len", "buffer_new", @@ -59,7 +58,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "define", "defined", "die", - "dirname", "disk_free_space", "disk_total_space", "empty", @@ -88,7 +86,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "filesize", "filetype", "flock", - "fnmatch", "fopen", "fpassthru", "fprintf", @@ -173,7 +170,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "natsort", "opendir", "passthru", - "pathinfo", "pclose", "pfsockopen", "php_uname", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index bd26f02c2b..11cb94731d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -109,8 +109,8 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "print_r" => optional(params, 1), "var_dump" => variadic(params, &[]), - "touch" | "basename" | "dirname" | "pathinfo" => optional(params, 1), - "fnmatch" | "fopen" | "fseek" | "fputcsv" => optional(params, 2), + "touch" => optional(params, 1), + "fopen" | "fseek" | "fputcsv" => optional(params, 2), "flock" => optional_by_ref(params, 2, &["would_block"]), "fgetcsv" => optional(params, 1), "clearstatcache" => optional(params, 0), @@ -188,10 +188,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("print_r", 1) => Bool(false), ("touch", 1 | 2) => Null, - ("basename", 1) => String(""), - ("dirname", 1) => Int(1), - ("fnmatch", 2) => Int(0), - ("pathinfo", 1) => Int(15), ("fopen", 2) => Bool(false), ("fopen", 3) => Null, ("flock", 2) => Null, diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index 4deaeaee2d..ac58098fa9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -20,6 +20,8 @@ pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; pub(in crate::interpreter) enum EvalArea { /// Array and collection builtins. Array, + /// Filesystem, path, and stream builtins. + Filesystem, /// Formatting and display-oriented numeric builtins. Formatting, /// JSON encoding, decoding, validation, and error-state builtins. diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 5ba08d3d2c..a12548e5ea 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1548,7 +1548,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_array_key_set(name, args, context, scope, values) } "array_merge" => eval_builtin_array_merge(args, context, scope, values), - "basename" => eval_builtin_basename(args, context, scope, values), "chdir" | "mkdir" | "rmdir" => { eval_builtin_unary_path_bool(name, args, context, scope, values) } @@ -1586,7 +1585,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), - "dirname" => eval_builtin_dirname(args, context, scope, values), "die" | "exit" => eval_builtin_exit(args, context, scope, values), "disk_free_space" | "disk_total_space" => { eval_builtin_disk_space(name, args, context, scope, values) @@ -1617,7 +1615,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( | "stream_get_meta_data" => eval_builtin_unary_stream(name, args, context, scope, values), "filesize" => eval_builtin_filesize(args, context, scope, values), "filetype" => eval_builtin_filetype(args, context, scope, values), - "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), "fopen" => eval_builtin_fopen(args, context, scope, values), "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), @@ -1678,7 +1675,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "ip2long" => eval_builtin_ip2long(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), - "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), "pclose" => eval_builtin_pclose(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 3a0c431553..7026569c83 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -110,6 +110,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "asin", "atan", "atan2", + "basename", "base64_decode", "base64_encode", "bin2hex", @@ -131,11 +132,13 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "date_default_timezone_get", "date_default_timezone_set", "deg2rad", + "dirname", "exp", "fdiv", "floatval", "floor", "fmod", + "fnmatch", "getdate", "gettype", "gmdate", @@ -187,6 +190,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "nl2br", "number_format", "ord", + "pathinfo", "pi", "pow", "preg_match", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index ab10b37538..58b514193e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7812,6 +7812,21 @@ echo function_exists("fnmatch"); echo defined("FNM_CASEFOLD");'); ); } +/// Verifies eval `basename()` and `dirname()` support defaults, named call arrays, and function probes. +#[test] +fn test_eval_dispatches_basename_dirname_builtin_calls() { + let out = compile_and_run( + r#" "/a/b/c", "levels" => 2]) . ":"; +echo function_exists("basename") && function_exists("dirname");'); +"#, + ); + assert_eq!(out, "syslog:/usr/local:file.txt:/a:1"); +} + /// Verifies eval `pathinfo()` supports arrays, component flags, constants, and callables. #[test] fn test_eval_dispatches_pathinfo_builtin_call() { From b08f32659f3b867fb22de671df7b0b8682a04d84 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 00:48:02 +0200 Subject: [PATCH 1069/1208] refactor(magician): migrate filesystem query builtins --- .../declarations/disk_free_space.rs | 16 +++++ .../declarations/disk_total_space.rs | 16 +++++ .../filesystem/declarations/getcwd.rs | 16 +++++ .../builtins/filesystem/declarations/glob.rs | 16 +++++ .../filesystem/declarations/linkinfo.rs | 16 +++++ .../builtins/filesystem/declarations/mod.rs | 69 ++++++++++++++++++- .../filesystem/declarations/readlink.rs | 16 +++++ .../filesystem/declarations/realpath.rs | 16 +++++ .../declarations/realpath_cache_get.rs | 16 +++++ .../declarations/realpath_cache_size.rs | 16 +++++ .../stream_resolve_include_path.rs | 16 +++++ .../declarations/sys_get_temp_dir.rs | 16 +++++ .../interpreter/builtins/registry/binding.rs | 9 +-- .../builtins/registry/dispatch/filesystem.rs | 60 ---------------- .../src/interpreter/builtins/registry/mod.rs | 43 ++++++++++++ .../interpreter/builtins/registry/names.rs | 11 --- .../src/interpreter/expressions.rs | 14 ---- tests/builtin_parity_tests.rs | 11 +++ tests/codegen/eval.rs | 15 ++++ 19 files changed, 313 insertions(+), 95 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_free_space.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_total_space.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/getcwd.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/glob.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/linkinfo.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readlink.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_get.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_size.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_resolve_include_path.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/sys_get_temp_dir.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_free_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_free_space.rs new file mode 100644 index 0000000000..15a6b1d921 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_free_space.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `disk_free_space`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the disk-space helper. + +eval_builtin! { + name: "disk_free_space", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_total_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_total_space.rs new file mode 100644 index 0000000000..667c557426 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_total_space.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `disk_total_space`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the disk-space helper. + +eval_builtin! { + name: "disk_total_space", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/getcwd.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/getcwd.rs new file mode 100644 index 0000000000..8eb634875f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/getcwd.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `getcwd`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the current-working-directory helper. + +eval_builtin! { + name: "getcwd", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/glob.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/glob.rs new file mode 100644 index 0000000000..b9d95bde1a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/glob.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `glob`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the local glob helper. + +eval_builtin! { + name: "glob", + area: Filesystem, + params: [pattern], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/linkinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/linkinfo.rs new file mode 100644 index 0000000000..dd8794ad05 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/linkinfo.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `linkinfo`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the symbolic-link metadata helper. + +eval_builtin! { + name: "linkinfo", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index bb8df5c07a..6e2a1a1802 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -6,16 +6,27 @@ //! - `crate::interpreter::builtins::hooks` for migrated filesystem dispatch. //! //! Key details: -//! - This first tranche covers path/string-like helpers only; stream, stat, -//! mutating, and by-reference filesystem calls stay on the legacy path. +//! - This covers simple path/query helpers; stream, stat, mutating, and +//! by-reference filesystem calls stay on the legacy path. use super::super::super::*; use super::*; mod basename; mod dirname; +mod disk_free_space; +mod disk_total_space; mod fnmatch; +mod getcwd; +mod glob; +mod linkinfo; mod pathinfo; +mod readlink; +mod realpath; +mod realpath_cache_get; +mod realpath_cache_size; +mod stream_resolve_include_path; +mod sys_get_temp_dir; /// Dispatches direct expression-level calls for declaratively migrated filesystem builtins. pub(in crate::interpreter) fn eval_builtin_filesystem_call( @@ -28,8 +39,22 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( match name { "basename" => eval_builtin_basename(args, context, scope, values), "dirname" => eval_builtin_dirname(args, context, scope, values), + "disk_free_space" | "disk_total_space" => { + eval_builtin_disk_space(name, args, context, scope, values) + } "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), + "getcwd" => eval_builtin_getcwd(args, values), + "glob" => eval_builtin_glob(args, context, scope, values), + "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), + "readlink" => eval_builtin_readlink(args, context, scope, values), + "realpath" => eval_builtin_realpath(args, context, scope, values), + "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), + "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), + "stream_resolve_include_path" => { + eval_builtin_stream_resolve_include_path(args, context, scope, values) + } + "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), _ => Err(EvalStatus::RuntimeFatal), } } @@ -51,6 +76,10 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [path, levels] => eval_dirname_result(*path, Some(*levels), values), _ => Err(EvalStatus::RuntimeFatal), }, + "disk_free_space" | "disk_total_space" => match evaluated_args { + [directory] => eval_disk_space_result(name, *directory, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "fnmatch" => match evaluated_args { [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values), [pattern, filename, flags] => { @@ -58,11 +87,47 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( } _ => Err(EvalStatus::RuntimeFatal), }, + "getcwd" => match evaluated_args { + [] => eval_getcwd_result(values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "glob" => match evaluated_args { + [pattern] => eval_glob_result(*pattern, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "linkinfo" => match evaluated_args { + [path] => eval_linkinfo_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "pathinfo" => match evaluated_args { [path] => eval_pathinfo_result(*path, None, values), [path, flags] => eval_pathinfo_result(*path, Some(*flags), values), _ => Err(EvalStatus::RuntimeFatal), }, + "readlink" => match evaluated_args { + [path] => eval_readlink_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "realpath" => match evaluated_args { + [path] => eval_realpath_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "realpath_cache_get" => match evaluated_args { + [] => eval_realpath_cache_get_result(values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "realpath_cache_size" => match evaluated_args { + [] => eval_realpath_cache_size_result(values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "stream_resolve_include_path" => match evaluated_args { + [filename] => eval_stream_resolve_include_path_result(*filename, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "sys_get_temp_dir" => match evaluated_args { + [] => eval_sys_get_temp_dir_result(values), + _ => Err(EvalStatus::RuntimeFatal), + }, _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readlink.rs new file mode 100644 index 0000000000..df087854cf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readlink.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `readlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the symbolic-link target helper. + +eval_builtin! { + name: "readlink", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath.rs new file mode 100644 index 0000000000..c9ff9ed562 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `realpath`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the canonical path helper. + +eval_builtin! { + name: "realpath", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_get.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_get.rs new file mode 100644 index 0000000000..60e40bb54a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_get.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `realpath_cache_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to elephc's empty realpath-cache helper. + +eval_builtin! { + name: "realpath_cache_get", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_size.rs new file mode 100644 index 0000000000..d366ffe618 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_size.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `realpath_cache_size`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to elephc's empty realpath-cache helper. + +eval_builtin! { + name: "realpath_cache_size", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_resolve_include_path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_resolve_include_path.rs new file mode 100644 index 0000000000..e9c1718ef8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_resolve_include_path.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_resolve_include_path`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the include-path resolution helper. + +eval_builtin! { + name: "stream_resolve_include_path", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/sys_get_temp_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/sys_get_temp_dir.rs new file mode 100644 index 0000000000..4cecdd33bf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/sys_get_temp_dir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `sys_get_temp_dir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the temporary-directory helper. + +eval_builtin! { + name: "sys_get_temp_dir", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index ffca60d90f..deeb7e5333 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -176,7 +176,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "die" | "exit" => Some(&["status"]), - "disk_free_space" | "disk_total_space" => Some(&["directory"]), "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), "explode" => Some(&["separator", "string", "limit"]), "fclose" @@ -219,9 +218,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "getservbyname" => Some(&["service", "protocol"]), "getservbyport" => Some(&["port", "protocol"]), "get_resource_id" | "get_resource_type" => Some(&["resource"]), - "getcwd" => Some(&[]), "getenv" => Some(&["name"]), - "glob" => Some(&["pattern"]), "hash" => Some(&["algo", "data", "binary"]), "hash_algos" => Some(&[]), "hash_copy" => Some(&["context"]), @@ -243,7 +240,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "ip2long" => Some(&["ip"]), "isset" | "unset" => Some(&["var", "vars"]), "link" | "symlink" => Some(&["target", "link"]), - "linkinfo" | "readlink" => Some(&["path"]), "md5" | "sha1" => Some(&["string", "binary"]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), @@ -266,9 +262,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "putenv" => Some(&["assignment"]), "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "readline" => Some(&["prompt"]), - "realpath" => Some(&["path"]), - "stream_resolve_include_path" => Some(&["filename"]), - "realpath_cache_get" | "realpath_cache_size" => Some(&[]), "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), "spl_autoload_unregister" => Some(&["callback"]), "spl_autoload_functions" | "spl_classes" => Some(&[]), @@ -317,7 +310,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_socket_shutdown" => Some(&["stream", "mode"]), "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), - "sys_get_temp_dir" | "tmpfile" => Some(&[]), + "tmpfile" => Some(&[]), "tempnam" => Some(&["directory", "prefix"]), "touch" => Some(&["filename", "mtime", "atime"]), "chown" | "lchown" => Some(&["filename", "user"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index dfc357dd8f..1d6b1cd900 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -217,12 +217,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_stat_array_result(name, *filename, context, values)? } - "linkinfo" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_linkinfo_result(*path, values)? - } "readfile" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -242,24 +236,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_scandir_result(*directory, values)? } - "disk_free_space" | "disk_total_space" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_disk_space_result(name, *directory, values)? - } - "getcwd" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_getcwd_result(values)? - } - "glob" => { - let [pattern] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_glob_result(*pattern, values)? - } "opendir" => { let [directory] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -278,18 +254,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_popen_result(*command, *mode, context, values)? } - "realpath" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_realpath_result(*path, values)? - } - "stream_resolve_include_path" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_resolve_include_path_result(*filename, values)? - } "stream_copy_to_stream" => match evaluated_args { [from, to] => { eval_stream_copy_to_stream_result(*from, *to, None, None, context, values)? @@ -542,24 +506,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( )?, _ => return Err(EvalStatus::RuntimeFatal), }, - "realpath_cache_get" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_get_result(values)? - } - "realpath_cache_size" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_size_result(values)? - } - "sys_get_temp_dir" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_sys_get_temp_dir_result(values)? - } "tempnam" => { let [directory, prefix] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -593,12 +539,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( [mask] => eval_umask_result(Some(*mask), values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "readlink" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_readlink_result(*path, values)? - } "unlink" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 18046b3a91..74bbef131f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -206,12 +206,16 @@ mod tests { "date_default_timezone_get", "date_default_timezone_set", "dirname", + "disk_free_space", + "disk_total_space", "floatval", "fnmatch", "getdate", + "getcwd", "gettype", "gmdate", "gmmktime", + "glob", "grapheme_strrev", "hash_equals", "hex2bin", @@ -241,6 +245,7 @@ mod tests { "json_last_error", "json_last_error_msg", "json_validate", + "linkinfo", "localtime", "log", "microtime", @@ -256,7 +261,12 @@ mod tests { "preg_split", "range", "rawurlencode", + "readlink", + "realpath", + "realpath_cache_get", + "realpath_cache_size", "sleep", + "stream_resolve_include_path", "str_contains", "str_pad", "str_replace", @@ -265,6 +275,7 @@ mod tests { "strrev", "strtotime", "substr", + "sys_get_temp_dir", "time", "trim", "strval", @@ -442,5 +453,37 @@ mod tests { eval_declared_builtin_default_value("pathinfo", 1), Some(EvalBuiltinDefaultValue::Int(15)) ); + assert_eq!( + eval_declared_builtin_param_names("disk_free_space"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getcwd"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("glob"), + Some(["pattern"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("linkinfo"), + Some(["path"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("realpath"), + Some(["path"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_resolve_include_path"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("realpath_cache_get"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("sys_get_temp_dir"), + Some([].as_slice()) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 5953679cb4..2bc0542bc7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -58,8 +58,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "define", "defined", "die", - "disk_free_space", - "disk_total_space", "empty", "enum_exists", "exec", @@ -111,7 +109,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "get_parent_class", "get_resource_id", "get_resource_type", - "getcwd", "getenv", "gethostbyaddr", "gethostbyname", @@ -120,7 +117,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "getprotobynumber", "getservbyname", "getservbyport", - "glob", "gzcompress", "gzdeflate", "gzinflate", @@ -159,7 +155,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "lchgrp", "lchown", "link", - "linkinfo", "long2ip", "lstat", "md5", @@ -199,10 +194,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "readdir", "readfile", "readline", - "readlink", - "realpath", - "realpath_cache_get", - "realpath_cache_size", "rename", "rewind", "rewinddir", @@ -250,7 +241,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_get_wrappers", "stream_is_local", "stream_isatty", - "stream_resolve_include_path", "stream_select", "stream_set_blocking", "stream_set_chunk_size", @@ -271,7 +261,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_wrapper_restore", "stream_wrapper_unregister", "symlink", - "sys_get_temp_dir", "system", "tempnam", "tmpfile", diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index a12548e5ea..bd9f1d69f7 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1586,9 +1586,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "die" | "exit" => eval_builtin_exit(args, context, scope, values), - "disk_free_space" | "disk_total_space" => { - eval_builtin_disk_space(name, args, context, scope, values) - } "empty" => eval_builtin_empty(args, context, scope, values), "exec" | "shell_exec" | "system" | "passthru" => { eval_builtin_process_command(name, args, context, scope, values) @@ -1645,9 +1642,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "get_resource_id" | "get_resource_type" => { eval_builtin_resource_introspection(name, args, context, scope, values) } - "getcwd" => eval_builtin_getcwd(args, values), "getenv" => eval_builtin_getenv(args, context, scope, values), - "glob" => eval_builtin_glob(args, context, scope, values), "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { eval_builtin_gzip(name, args, context, scope, values) } @@ -1673,7 +1668,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), "ip2long" => eval_builtin_ip2long(args, context, scope, values), - "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), @@ -1691,10 +1685,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "random_int" => eval_builtin_random_int(args, context, scope, values), "readfile" => eval_builtin_readfile(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), - "readlink" => eval_builtin_readlink(args, context, scope, values), - "realpath" => eval_builtin_realpath(args, context, scope, values), - "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), - "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), "scandir" => eval_builtin_scandir(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "spl_autoload_register" | "spl_autoload_unregister" => { @@ -1715,7 +1705,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "sscanf" => eval_builtin_sscanf(args, context, scope, values), "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), - "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "tempnam" => eval_builtin_tempnam(args, context, scope, values), "touch" => eval_builtin_touch(args, context, scope, values), "tmpfile" => eval_builtin_tmpfile(args, context, values), @@ -1725,9 +1714,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { eval_builtin_stream_introspection(name, args, context, values) } - "stream_resolve_include_path" => { - eval_builtin_stream_resolve_include_path(args, context, scope, values) - } "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), "stream_context_get_default" => { diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 7026569c83..f2e4f3ece7 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -133,6 +133,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "date_default_timezone_set", "deg2rad", "dirname", + "disk_free_space", + "disk_total_space", "exp", "fdiv", "floatval", @@ -140,9 +142,11 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "fmod", "fnmatch", "getdate", + "getcwd", "gettype", "gmdate", "gmmktime", + "glob", "grapheme_strrev", "hash_equals", "hex2bin", @@ -178,6 +182,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "json_last_error_msg", "json_validate", "lcfirst", + "linkinfo", "localtime", "log", "log10", @@ -202,6 +207,10 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "range", "rawurldecode", "rawurlencode", + "readlink", + "realpath", + "realpath_cache_get", + "realpath_cache_size", "round", "rtrim", "sin", @@ -230,6 +239,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "substr", "substr_replace", "strval", + "stream_resolve_include_path", + "sys_get_temp_dir", "tan", "tanh", "time", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 58b514193e..033eaec43e 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7622,6 +7622,21 @@ echo ":"; echo function_exists("realpath");'); assert_eq!(out, "resolved:false:call:array-false:1"); } +/// Verifies eval `stream_resolve_include_path()` dispatches directly and dynamically. +#[test] +fn test_eval_dispatches_stream_resolve_include_path_builtin_call() { + let out = compile_and_run( + r#" "elephc-magician-missing-path"]) === false ? "array-false" : "bad"; +echo ":"; echo function_exists("stream_resolve_include_path");'); +"#, + ); + assert_eq!(out, "resolved:false:call:array-false:1"); +} + /// Verifies eval regex builtins handle captures, replacement, callbacks, and splitting. #[test] fn test_eval_dispatches_preg_builtin_calls() { From 49dab2d7333b95098c4710d954a0edc90e7717a5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 01:00:19 +0200 Subject: [PATCH 1070/1208] refactor(magician): migrate filesystem stat builtins --- .../filesystem/declarations/file_exists.rs | 16 ++++++ .../filesystem/declarations/fileatime.rs | 16 ++++++ .../filesystem/declarations/filectime.rs | 16 ++++++ .../filesystem/declarations/filegroup.rs | 16 ++++++ .../filesystem/declarations/fileinode.rs | 16 ++++++ .../filesystem/declarations/filemtime.rs | 16 ++++++ .../filesystem/declarations/fileowner.rs | 16 ++++++ .../filesystem/declarations/fileperms.rs | 16 ++++++ .../filesystem/declarations/filesize.rs | 16 ++++++ .../filesystem/declarations/filetype.rs | 16 ++++++ .../filesystem/declarations/is_dir.rs | 16 ++++++ .../filesystem/declarations/is_executable.rs | 16 ++++++ .../filesystem/declarations/is_file.rs | 16 ++++++ .../filesystem/declarations/is_link.rs | 16 ++++++ .../filesystem/declarations/is_readable.rs | 16 ++++++ .../filesystem/declarations/is_writable.rs | 16 ++++++ .../filesystem/declarations/is_writeable.rs | 16 ++++++ .../builtins/filesystem/declarations/lstat.rs | 16 ++++++ .../builtins/filesystem/declarations/mod.rs | 51 +++++++++++++++++++ .../builtins/filesystem/declarations/stat.rs | 16 ++++++ .../src/interpreter/builtins/hooks/values.rs | 2 +- .../interpreter/builtins/registry/binding.rs | 5 +- .../builtins/registry/dispatch/filesystem.rs | 32 ------------ .../src/interpreter/builtins/registry/mod.rs | 39 ++++++++++++++ .../interpreter/builtins/registry/names.rs | 19 ------- .../src/interpreter/expressions.rs | 8 --- tests/builtin_parity_tests.rs | 19 +++++++ 27 files changed, 415 insertions(+), 64 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileatime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filectime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filegroup.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileinode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filemtime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileowner.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileperms.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filesize.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filetype.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_dir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_executable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_file.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_link.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_readable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writeable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lstat.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stat.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_exists.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_exists.rs new file mode 100644 index 0000000000..b2c98ac2dd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_exists.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `file_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-probe helper. + +eval_builtin! { + name: "file_exists", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileatime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileatime.rs new file mode 100644 index 0000000000..543f753dcd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileatime.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fileatime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the scalar stat helper. + +eval_builtin! { + name: "fileatime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filectime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filectime.rs new file mode 100644 index 0000000000..3a52e88f55 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filectime.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `filectime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the scalar stat helper. + +eval_builtin! { + name: "filectime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filegroup.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filegroup.rs new file mode 100644 index 0000000000..ea9fc7e16b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filegroup.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `filegroup`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the scalar stat helper. + +eval_builtin! { + name: "filegroup", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileinode.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileinode.rs new file mode 100644 index 0000000000..2d64f43193 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileinode.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fileinode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the scalar stat helper. + +eval_builtin! { + name: "fileinode", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filemtime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filemtime.rs new file mode 100644 index 0000000000..25312fae3e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filemtime.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `filemtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the scalar stat helper. + +eval_builtin! { + name: "filemtime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileowner.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileowner.rs new file mode 100644 index 0000000000..cbc464ac49 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileowner.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fileowner`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the scalar stat helper. + +eval_builtin! { + name: "fileowner", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileperms.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileperms.rs new file mode 100644 index 0000000000..9b369dd0d8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileperms.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fileperms`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the scalar stat helper. + +eval_builtin! { + name: "fileperms", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filesize.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filesize.rs new file mode 100644 index 0000000000..11e142d1ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filesize.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `filesize`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the filesize helper. + +eval_builtin! { + name: "filesize", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filetype.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filetype.rs new file mode 100644 index 0000000000..8d52494c94 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filetype.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `filetype`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the filetype helper. + +eval_builtin! { + name: "filetype", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_dir.rs new file mode 100644 index 0000000000..6c1f17a1d7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_dir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_dir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-probe helper. + +eval_builtin! { + name: "is_dir", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_executable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_executable.rs new file mode 100644 index 0000000000..25571ff12f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_executable.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_executable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-probe helper. + +eval_builtin! { + name: "is_executable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_file.rs new file mode 100644 index 0000000000..332bc0c43b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_file.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-probe helper. + +eval_builtin! { + name: "is_file", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_link.rs new file mode 100644 index 0000000000..3d9c590d15 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_link.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_link`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-probe helper. + +eval_builtin! { + name: "is_link", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_readable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_readable.rs new file mode 100644 index 0000000000..80ec54a92e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_readable.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_readable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-probe helper. + +eval_builtin! { + name: "is_readable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writable.rs new file mode 100644 index 0000000000..d7c5be6f44 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writable.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_writable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-probe helper. + +eval_builtin! { + name: "is_writable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writeable.rs new file mode 100644 index 0000000000..e8d6a48144 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writeable.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `is_writeable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-probe helper. + +eval_builtin! { + name: "is_writeable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lstat.rs new file mode 100644 index 0000000000..cef1b286c9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lstat.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `lstat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stat-array helper. + +eval_builtin! { + name: "lstat", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index 6e2a1a1802..1b5d2ffc68 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -16,15 +16,34 @@ mod basename; mod dirname; mod disk_free_space; mod disk_total_space; +mod file_exists; +mod fileatime; +mod filectime; +mod filegroup; +mod fileinode; +mod filemtime; +mod fileowner; +mod fileperms; +mod filesize; +mod filetype; mod fnmatch; mod getcwd; mod glob; +mod is_dir; +mod is_executable; +mod is_file; +mod is_link; +mod is_readable; +mod is_writable; +mod is_writeable; mod linkinfo; +mod lstat; mod pathinfo; mod readlink; mod realpath; mod realpath_cache_get; mod realpath_cache_size; +mod stat; mod stream_resolve_include_path; mod sys_get_temp_dir; @@ -42,6 +61,14 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "disk_free_space" | "disk_total_space" => { eval_builtin_disk_space(name, args, context, scope, values) } + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => { + eval_builtin_file_probe(name, args, context, scope, values) + } + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), + "filesize" => eval_builtin_filesize(args, context, scope, values), + "filetype" => eval_builtin_filetype(args, context, scope, values), "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), "glob" => eval_builtin_glob(args, context, scope, values), @@ -51,6 +78,7 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), + "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "stream_resolve_include_path" => { eval_builtin_stream_resolve_include_path(args, context, scope, values) } @@ -63,6 +91,7 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( pub(in crate::interpreter) fn eval_filesystem_values_result( name: &str, evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { match name { @@ -80,6 +109,24 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [directory] => eval_disk_space_result(name, *directory, values), _ => Err(EvalStatus::RuntimeFatal), }, + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => match evaluated_args { + [filename] => eval_file_probe_result(name, *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => match evaluated_args { + [filename] => eval_file_stat_scalar_result(name, *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "filesize" => match evaluated_args { + [filename] => eval_filesize_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "filetype" => match evaluated_args { + [filename] => eval_filetype_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "fnmatch" => match evaluated_args { [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values), [pattern, filename, flags] => { @@ -120,6 +167,10 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [] => eval_realpath_cache_size_result(values), _ => Err(EvalStatus::RuntimeFatal), }, + "stat" | "lstat" => match evaluated_args { + [filename] => eval_stat_array_result(name, *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "stream_resolve_include_path" => match evaluated_args { [filename] => eval_stream_resolve_include_path_result(*filename, values), _ => Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stat.rs new file mode 100644 index 0000000000..aaab6907b7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stat.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stat-array helper. + +eval_builtin! { + name: "stat", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 2f7e263cb5..5f4c709160 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -228,7 +228,7 @@ impl EvalValuesHook { Self::Ctype => one_arg(evaluated_args, values, |value, values| { eval_ctype_result(name, value, values) }), - Self::Filesystem => eval_filesystem_values_result(name, evaluated_args, values), + Self::Filesystem => eval_filesystem_values_result(name, evaluated_args, context, values), Self::FloatBinary => two_args(evaluated_args, values, |left, right, values| { eval_float_binary_result(name, left, right, values) }), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index deeb7e5333..a4bd15c4b7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -191,10 +191,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "fstat" | "stream_get_meta_data" => Some(&["stream"]), "fgetcsv" => Some(&["stream", "length", "separator"]), - "file" | "file_get_contents" | "file_exists" | "fileatime" | "filectime" | "filegroup" - | "fileinode" | "filemtime" | "fileowner" | "fileperms" | "filesize" | "filetype" - | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" - | "is_writeable" | "lstat" | "readfile" | "stat" | "unlink" => Some(&["filename"]), + "file" | "file_get_contents" | "readfile" | "unlink" => Some(&["filename"]), "file_put_contents" => Some(&["filename", "data"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 1d6b1cd900..43fbeef114 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -61,20 +61,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_file_result(*filename, context, values)? } - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_probe_result(name, *filename, context, values)? - } - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_stat_scalar_result(name, *filename, context, values)? - } "file_get_contents" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -104,18 +90,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_unary_stream_result(name, *stream, context, values)? } - "filesize" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_filesize_result(*filename, context, values)? - } - "filetype" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_filetype_result(*filename, context, values)? - } "fgetcsv" => match evaluated_args { [stream] => eval_fgetcsv_result(*stream, None, None, context, values)?, [stream, length] => { @@ -211,12 +185,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_fwrite_result(*stream, *data, context, values)? } - "stat" | "lstat" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stat_array_result(name, *filename, context, values)? - } "readfile" => { let [filename] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 74bbef131f..5b2921db38 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -208,6 +208,16 @@ mod tests { "dirname", "disk_free_space", "disk_total_space", + "file_exists", + "fileatime", + "filectime", + "filegroup", + "fileinode", + "filemtime", + "fileowner", + "fileperms", + "filesize", + "filetype", "floatval", "fnmatch", "getdate", @@ -224,22 +234,29 @@ mod tests { "intval", "is_array", "is_bool", + "is_dir", "is_double", + "is_executable", + "is_file", "is_finite", "is_float", "is_infinite", "is_int", "is_integer", "is_iterable", + "is_link", "is_long", "is_nan", "is_null", "is_numeric", "is_object", + "is_readable", "is_real", "is_resource", "is_scalar", "is_string", + "is_writable", + "is_writeable", "json_decode", "json_encode", "json_last_error", @@ -248,6 +265,7 @@ mod tests { "linkinfo", "localtime", "log", + "lstat", "microtime", "min", "mktime", @@ -266,6 +284,7 @@ mod tests { "realpath_cache_get", "realpath_cache_size", "sleep", + "stat", "stream_resolve_include_path", "str_contains", "str_pad", @@ -485,5 +504,25 @@ mod tests { eval_declared_builtin_param_names("sys_get_temp_dir"), Some([].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("file_exists"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("filemtime"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("filesize"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("is_writable"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stat"), + Some(["filename"].as_slice()) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 2bc0542bc7..f3c814e10e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -71,18 +71,8 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "fgetcsv", "fgets", "file", - "file_exists", "file_get_contents", "file_put_contents", - "fileatime", - "filectime", - "filegroup", - "fileinode", - "filemtime", - "fileowner", - "fileperms", - "filesize", - "filetype", "flock", "fopen", "fpassthru", @@ -138,14 +128,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ip2long", "is_a", "is_callable", - "is_dir", - "is_executable", - "is_file", - "is_link", - "is_readable", "is_subclass_of", - "is_writable", - "is_writeable", "isset", "iterator_apply", "iterator_count", @@ -156,7 +139,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "lchown", "link", "long2ip", - "lstat", "md5", "method_exists", "mkdir", @@ -216,7 +198,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "spl_object_id", "sprintf", "sscanf", - "stat", "stream_bucket_append", "stream_bucket_make_writeable", "stream_bucket_new", diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index bd9f1d69f7..58d0d76251 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1593,9 +1593,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), "file" => eval_builtin_file(args, context, scope, values), - "file_exists" => eval_builtin_file_probe(name, args, context, scope, values), - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "fclose" @@ -1610,8 +1607,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( | "rewind" | "fstat" | "stream_get_meta_data" => eval_builtin_unary_stream(name, args, context, scope, values), - "filesize" => eval_builtin_filesize(args, context, scope, values), - "filetype" => eval_builtin_filetype(args, context, scope, values), "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), "fopen" => eval_builtin_fopen(args, context, scope, values), "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), @@ -1622,7 +1617,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "fseek" => eval_builtin_fseek(args, context, scope, values), "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), "fwrite" => eval_builtin_fwrite(args, context, scope, values), - "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(name, args, context, scope, values) } @@ -1664,8 +1658,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), - "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" - | "is_writeable" => eval_builtin_file_probe(name, args, context, scope, values), "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), "ip2long" => eval_builtin_ip2long(args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index f2e4f3ece7..b8d94d6a7f 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -137,6 +137,16 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "disk_total_space", "exp", "fdiv", + "file_exists", + "fileatime", + "filectime", + "filegroup", + "fileinode", + "filemtime", + "fileowner", + "fileperms", + "filesize", + "filetype", "floatval", "floor", "fmod", @@ -159,23 +169,30 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "intval", "is_array", "is_bool", + "is_dir", "is_double", + "is_executable", + "is_file", "is_finite", "is_float", "is_infinite", "is_int", "is_integer", "is_iterable", + "is_link", "is_long", "is_nan", "is_null", "is_numeric", "is_object", + "is_readable", "is_real", "is_resource", "is_scalar", "is_string", "in_array", + "is_writable", + "is_writeable", "json_decode", "json_encode", "json_last_error", @@ -187,6 +204,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "log", "log10", "log2", + "lstat", "ltrim", "max", "microtime", @@ -217,6 +235,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "sinh", "sleep", "sqrt", + "stat", "str_contains", "str_ends_with", "str_ireplace", From 2b105166e2c6d71b1df6ec213449dbfed9058de3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 01:11:40 +0200 Subject: [PATCH 1071/1208] refactor(magician): migrate filesystem content builtins --- .../builtins/filesystem/declarations/file.rs | 16 +++++++++++++ .../declarations/file_get_contents.rs | 16 +++++++++++++ .../declarations/file_put_contents.rs | 16 +++++++++++++ .../builtins/filesystem/declarations/mod.rs | 24 +++++++++++++++++++ .../filesystem/declarations/readfile.rs | 16 +++++++++++++ .../interpreter/builtins/registry/binding.rs | 3 +-- .../builtins/registry/dispatch/filesystem.rs | 24 ------------------- .../src/interpreter/builtins/registry/mod.rs | 20 ++++++++++++++++ .../interpreter/builtins/registry/names.rs | 4 ---- .../src/interpreter/expressions.rs | 4 ---- tests/builtin_parity_tests.rs | 4 ++++ 11 files changed, 113 insertions(+), 34 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_get_contents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_put_contents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readfile.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file.rs new file mode 100644 index 0000000000..9ec84abe4f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the file-lines helper. + +eval_builtin! { + name: "file", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_get_contents.rs new file mode 100644 index 0000000000..3ac7b0ca1f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_get_contents.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `file_get_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the one-shot file read helper. + +eval_builtin! { + name: "file_get_contents", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_put_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_put_contents.rs new file mode 100644 index 0000000000..e51ed365a4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_put_contents.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `file_put_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the one-shot file write helper. + +eval_builtin! { + name: "file_put_contents", + area: Filesystem, + params: [filename, data], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index 1b5d2ffc68..251b9323a5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -16,7 +16,10 @@ mod basename; mod dirname; mod disk_free_space; mod disk_total_space; +mod file; mod file_exists; +mod file_get_contents; +mod file_put_contents; mod fileatime; mod filectime; mod filegroup; @@ -39,6 +42,7 @@ mod is_writeable; mod linkinfo; mod lstat; mod pathinfo; +mod readfile; mod readlink; mod realpath; mod realpath_cache_get; @@ -67,6 +71,9 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( } "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), + "file" => eval_builtin_file(args, context, scope, values), + "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), + "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "filesize" => eval_builtin_filesize(args, context, scope, values), "filetype" => eval_builtin_filetype(args, context, scope, values), "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), @@ -74,6 +81,7 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "glob" => eval_builtin_glob(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), + "readfile" => eval_builtin_readfile(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), @@ -119,6 +127,18 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [filename] => eval_file_stat_scalar_result(name, *filename, context, values), _ => Err(EvalStatus::RuntimeFatal), }, + "file" => match evaluated_args { + [filename] => eval_file_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "file_get_contents" => match evaluated_args { + [filename] => eval_file_get_contents_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "file_put_contents" => match evaluated_args { + [filename, data] => eval_file_put_contents_result(*filename, *data, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "filesize" => match evaluated_args { [filename] => eval_filesize_result(*filename, context, values), _ => Err(EvalStatus::RuntimeFatal), @@ -151,6 +171,10 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [path, flags] => eval_pathinfo_result(*path, Some(*flags), values), _ => Err(EvalStatus::RuntimeFatal), }, + "readfile" => match evaluated_args { + [filename] => eval_readfile_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "readlink" => match evaluated_args { [path] => eval_readlink_result(*path, values), _ => Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readfile.rs new file mode 100644 index 0000000000..115b45140f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readfile.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `readfile`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the streaming file output helper. + +eval_builtin! { + name: "readfile", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index a4bd15c4b7..61bd79ef77 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -191,8 +191,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "fstat" | "stream_get_meta_data" => Some(&["stream"]), "fgetcsv" => Some(&["stream", "length", "separator"]), - "file" | "file_get_contents" | "readfile" | "unlink" => Some(&["filename"]), - "file_put_contents" => Some(&["filename", "data"]), + "unlink" => Some(&["filename"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), "fprintf" => Some(&["stream", "format", "values"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 43fbeef114..9ef5c6afe6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -55,24 +55,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_binary_path_bool_result(name, *from, *to, context, values)? } - "file" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_result(*filename, context, values)? - } - "file_get_contents" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_get_contents_result(*filename, context, values)? - } - "file_put_contents" => { - let [filename, data] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_file_put_contents_result(*filename, *data, context, values)? - } "fclose" | "fgetc" | "fgets" @@ -185,12 +167,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_fwrite_result(*stream, *data, context, values)? } - "readfile" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_readfile_result(*filename, context, values)? - } "readline" => { if evaluated_args.len() > 1 { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 5b2921db38..4d35984c9c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -208,7 +208,10 @@ mod tests { "dirname", "disk_free_space", "disk_total_space", + "file", "file_exists", + "file_get_contents", + "file_put_contents", "fileatime", "filectime", "filegroup", @@ -279,6 +282,7 @@ mod tests { "preg_split", "range", "rawurlencode", + "readfile", "readlink", "realpath", "realpath_cache_get", @@ -508,6 +512,22 @@ mod tests { eval_declared_builtin_param_names("file_exists"), Some(["filename"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("file"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file_get_contents"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file_put_contents"), + Some(["filename", "data"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("readfile"), + Some(["filename"].as_slice()) + ); assert_eq!( eval_declared_builtin_param_names("filemtime"), Some(["filename"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index f3c814e10e..b486f79026 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -70,9 +70,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "fgetc", "fgetcsv", "fgets", - "file", - "file_get_contents", - "file_put_contents", "flock", "fopen", "fpassthru", @@ -174,7 +171,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "rand", "random_int", "readdir", - "readfile", "readline", "rename", "rewind", diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 58d0d76251..b54795b86f 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1592,9 +1592,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), - "file" => eval_builtin_file(args, context, scope, values), - "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), - "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "fclose" | "fgetc" | "fgets" @@ -1675,7 +1672,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "putenv" => eval_builtin_putenv(args, context, scope, values), "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), "random_int" => eval_builtin_random_int(args, context, scope, values), - "readfile" => eval_builtin_readfile(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "scandir" => eval_builtin_scandir(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index b8d94d6a7f..cc39c5e7f4 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -137,7 +137,10 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "disk_total_space", "exp", "fdiv", + "file", "file_exists", + "file_get_contents", + "file_put_contents", "fileatime", "filectime", "filegroup", @@ -225,6 +228,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "range", "rawurldecode", "rawurlencode", + "readfile", "readlink", "realpath", "realpath_cache_get", From 469c14ef7aea815ce01f26fd377f703f7c514764 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 01:25:51 +0200 Subject: [PATCH 1072/1208] refactor(magician): migrate filesystem path mutation builtins --- .../builtins/filesystem/declarations/chdir.rs | 16 ++++ .../builtins/filesystem/declarations/chgrp.rs | 16 ++++ .../builtins/filesystem/declarations/chmod.rs | 16 ++++ .../builtins/filesystem/declarations/chown.rs | 16 ++++ .../filesystem/declarations/clearstatcache.rs | 21 +++++ .../builtins/filesystem/declarations/copy.rs | 16 ++++ .../filesystem/declarations/lchgrp.rs | 16 ++++ .../filesystem/declarations/lchown.rs | 16 ++++ .../builtins/filesystem/declarations/link.rs | 16 ++++ .../builtins/filesystem/declarations/mkdir.rs | 16 ++++ .../builtins/filesystem/declarations/mod.rs | 88 ++++++++++++++++++- .../filesystem/declarations/rename.rs | 16 ++++ .../builtins/filesystem/declarations/rmdir.rs | 16 ++++ .../filesystem/declarations/scandir.rs | 16 ++++ .../filesystem/declarations/symlink.rs | 16 ++++ .../filesystem/declarations/tempnam.rs | 16 ++++ .../builtins/filesystem/declarations/touch.rs | 22 +++++ .../builtins/filesystem/declarations/umask.rs | 18 ++++ .../filesystem/declarations/unlink.rs | 16 ++++ .../interpreter/builtins/registry/binding.rs | 12 +-- .../builtins/registry/dispatch/filesystem.rs | 63 ------------- .../src/interpreter/builtins/registry/mod.rs | 66 ++++++++++++++ .../interpreter/builtins/registry/names.rs | 18 ---- .../builtins/registry/signature.rs | 9 +- .../src/interpreter/expressions.rs | 16 ---- tests/builtin_parity_tests.rs | 18 ++++ tests/codegen/eval.rs | 12 ++- 27 files changed, 484 insertions(+), 119 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chdir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chgrp.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chmod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chown.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/clearstatcache.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/copy.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchgrp.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchown.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/link.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mkdir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rename.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rmdir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/scandir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/symlink.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tempnam.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/touch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/umask.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/unlink.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chdir.rs new file mode 100644 index 0000000000..5f5a9b3d01 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chdir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `chdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary path operation helper. + +eval_builtin! { + name: "chdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chgrp.rs new file mode 100644 index 0000000000..aea6d1bfb2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chgrp.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `chgrp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the ownership/group helper. + +eval_builtin! { + name: "chgrp", + area: Filesystem, + params: [filename, group], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chmod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chmod.rs new file mode 100644 index 0000000000..6d5f3325ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chmod.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `chmod`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the chmod helper. + +eval_builtin! { + name: "chmod", + area: Filesystem, + params: [filename, permissions], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chown.rs new file mode 100644 index 0000000000..e3f8361f2c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chown.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `chown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the ownership/group helper. + +eval_builtin! { + name: "chown", + area: Filesystem, + params: [filename, user], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/clearstatcache.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/clearstatcache.rs new file mode 100644 index 0000000000..bd368df1ad --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/clearstatcache.rs @@ -0,0 +1,21 @@ +//! Purpose: +//! Declarative eval registry entry for `clearstatcache`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to eval's ordered no-op stat-cache helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "clearstatcache", + area: Filesystem, + params: [ + clear_realpath_cache = EvalBuiltinDefaultValue::Bool(false), + filename = EvalBuiltinDefaultValue::String("") + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/copy.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/copy.rs new file mode 100644 index 0000000000..5d71626c96 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/copy.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `copy`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the binary path operation helper. + +eval_builtin! { + name: "copy", + area: Filesystem, + params: [from, to], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchgrp.rs new file mode 100644 index 0000000000..781084d372 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchgrp.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `lchgrp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the ownership/group helper. + +eval_builtin! { + name: "lchgrp", + area: Filesystem, + params: [filename, group], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchown.rs new file mode 100644 index 0000000000..07942551d7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchown.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `lchown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the ownership/group helper. + +eval_builtin! { + name: "lchown", + area: Filesystem, + params: [filename, user], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/link.rs new file mode 100644 index 0000000000..c8358d06a0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/link.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `link`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the binary path operation helper. + +eval_builtin! { + name: "link", + area: Filesystem, + params: [target, link], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mkdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mkdir.rs new file mode 100644 index 0000000000..44dee75853 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mkdir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `mkdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary path operation helper. + +eval_builtin! { + name: "mkdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index 251b9323a5..1839083258 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -6,13 +6,19 @@ //! - `crate::interpreter::builtins::hooks` for migrated filesystem dispatch. //! //! Key details: -//! - This covers simple path/query helpers; stream, stat, mutating, and -//! by-reference filesystem calls stay on the legacy path. +//! - This covers simple path/query/content and non-by-ref mutation helpers; +//! stream and by-reference filesystem calls stay on the legacy path. use super::super::super::*; use super::*; mod basename; +mod chdir; +mod chgrp; +mod chmod; +mod chown; +mod clearstatcache; +mod copy; mod dirname; mod disk_free_space; mod disk_total_space; @@ -39,17 +45,29 @@ mod is_link; mod is_readable; mod is_writable; mod is_writeable; +mod lchgrp; +mod lchown; +mod link; mod linkinfo; mod lstat; +mod mkdir; mod pathinfo; mod readfile; mod readlink; mod realpath; mod realpath_cache_get; mod realpath_cache_size; +mod rename; +mod rmdir; +mod scandir; mod stat; mod stream_resolve_include_path; +mod symlink; mod sys_get_temp_dir; +mod tempnam; +mod touch; +mod umask; +mod unlink; /// Dispatches direct expression-level calls for declaratively migrated filesystem builtins. pub(in crate::interpreter) fn eval_builtin_filesystem_call( @@ -61,6 +79,17 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( ) -> Result { match name { "basename" => eval_builtin_basename(args, context, scope, values), + "chdir" | "mkdir" | "rmdir" => { + eval_builtin_unary_path_bool(name, args, context, scope, values) + } + "chmod" => eval_builtin_chmod(args, context, scope, values), + "chown" | "chgrp" | "lchown" | "lchgrp" => { + eval_builtin_chown_like(name, args, context, scope, values) + } + "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), + "copy" | "link" | "rename" | "symlink" => { + eval_builtin_binary_path_bool(name, args, context, scope, values) + } "dirname" => eval_builtin_dirname(args, context, scope, values), "disk_free_space" | "disk_total_space" => { eval_builtin_disk_space(name, args, context, scope, values) @@ -86,11 +115,16 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), + "scandir" => eval_builtin_scandir(args, context, scope, values), "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "stream_resolve_include_path" => { eval_builtin_stream_resolve_include_path(args, context, scope, values) } "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), + "tempnam" => eval_builtin_tempnam(args, context, scope, values), + "touch" => eval_builtin_touch(args, context, scope, values), + "umask" => eval_builtin_umask(args, context, scope, values), + "unlink" => eval_builtin_unlink(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), } } @@ -108,6 +142,31 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [path, suffix] => eval_basename_result(*path, Some(*suffix), values), _ => Err(EvalStatus::RuntimeFatal), }, + "chdir" | "mkdir" | "rmdir" => match evaluated_args { + [path] => eval_unary_path_bool_result(name, *path, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "chmod" => match evaluated_args { + [filename, permissions] => eval_chmod_result(*filename, *permissions, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "chown" | "chgrp" | "lchown" | "lchgrp" => match evaluated_args { + [filename, principal] => { + eval_chown_like_result(name, *filename, *principal, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "clearstatcache" => { + if evaluated_args.len() > 2 { + Err(EvalStatus::RuntimeFatal) + } else { + values.null() + } + } + "copy" | "link" | "rename" | "symlink" => match evaluated_args { + [from, to] => eval_binary_path_bool_result(name, *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "dirname" => match evaluated_args { [path] => eval_dirname_result(*path, None, values), [path, levels] => eval_dirname_result(*path, Some(*levels), values), @@ -191,6 +250,10 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [] => eval_realpath_cache_size_result(values), _ => Err(EvalStatus::RuntimeFatal), }, + "scandir" => match evaluated_args { + [directory] => eval_scandir_result(*directory, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "stat" | "lstat" => match evaluated_args { [filename] => eval_stat_array_result(name, *filename, context, values), _ => Err(EvalStatus::RuntimeFatal), @@ -203,6 +266,27 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [] => eval_sys_get_temp_dir_result(values), _ => Err(EvalStatus::RuntimeFatal), }, + "tempnam" => match evaluated_args { + [directory, prefix] => eval_tempnam_result(*directory, *prefix, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "touch" => match evaluated_args { + [filename] => eval_touch_result(*filename, None, None, context, values), + [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, context, values), + [filename, mtime, atime] => { + eval_touch_result(*filename, Some(*mtime), Some(*atime), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "umask" => match evaluated_args { + [] => eval_umask_result(None, values), + [mask] => eval_umask_result(Some(*mask), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "unlink" => match evaluated_args { + [filename] => eval_unlink_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rename.rs new file mode 100644 index 0000000000..4246784746 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rename.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `rename`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the binary path operation helper. + +eval_builtin! { + name: "rename", + area: Filesystem, + params: [from, to], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rmdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rmdir.rs new file mode 100644 index 0000000000..8980355a37 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rmdir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `rmdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary path operation helper. + +eval_builtin! { + name: "rmdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/scandir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/scandir.rs new file mode 100644 index 0000000000..9ba03d3763 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/scandir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `scandir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the directory listing helper. + +eval_builtin! { + name: "scandir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/symlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/symlink.rs new file mode 100644 index 0000000000..b8d0d30558 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/symlink.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `symlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the binary path operation helper. + +eval_builtin! { + name: "symlink", + area: Filesystem, + params: [target, link], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tempnam.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tempnam.rs new file mode 100644 index 0000000000..5c50e181d1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tempnam.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `tempnam`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the temporary-name helper. + +eval_builtin! { + name: "tempnam", + area: Filesystem, + params: [directory, prefix], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/touch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/touch.rs new file mode 100644 index 0000000000..1c752aa5af --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/touch.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `touch`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the timestamp mutation helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "touch", + area: Filesystem, + params: [ + filename, + mtime = EvalBuiltinDefaultValue::Null, + atime = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/umask.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/umask.rs new file mode 100644 index 0000000000..d1ef942a03 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/umask.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `umask`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process umask helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "umask", + area: Filesystem, + params: [mask = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/unlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/unlink.rs new file mode 100644 index 0000000000..04a0049763 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/unlink.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `unlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unlink helper. + +eval_builtin! { + name: "unlink", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 61bd79ef77..31152ed858 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -168,11 +168,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "interface_exists" => Some(&["interface", "autoload"]), "trait_exists" => Some(&["trait", "autoload"]), "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), - "chdir" | "mkdir" | "opendir" | "rmdir" | "scandir" => Some(&["directory"]), - "chmod" => Some(&["filename", "permissions"]), + "opendir" => Some(&["directory"]), "closedir" | "readdir" | "rewinddir" => Some(&["dir_handle"]), - "clearstatcache" => Some(&["clear_realpath_cache", "filename"]), - "copy" | "rename" => Some(&["from", "to"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "die" | "exit" => Some(&["status"]), @@ -191,7 +188,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( | "fstat" | "stream_get_meta_data" => Some(&["stream"]), "fgetcsv" => Some(&["stream", "length", "separator"]), - "unlink" => Some(&["filename"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), "fprintf" => Some(&["stream", "format", "values"]), @@ -235,7 +231,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "iterator_to_array" => Some(&["iterator", "preserve_keys"]), "ip2long" => Some(&["ip"]), "isset" | "unset" => Some(&["var", "vars"]), - "link" | "symlink" => Some(&["target", "link"]), "md5" | "sha1" => Some(&["string", "binary"]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), @@ -307,12 +302,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), "tmpfile" => Some(&[]), - "tempnam" => Some(&["directory", "prefix"]), - "touch" => Some(&["filename", "mtime", "atime"]), - "chown" | "lchown" => Some(&["filename", "user"]), - "chgrp" | "lchgrp" => Some(&["filename", "group"]), "long2ip" => Some(&["ip"]), - "umask" => Some(&["mask"]), "vfprintf" => Some(&["stream", "format", "values"]), "vsprintf" | "vprintf" => Some(&["format", "values"]), _ => None, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 9ef5c6afe6..405237d9da 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -19,42 +19,12 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "chdir" | "mkdir" | "rmdir" => { - let [path] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unary_path_bool_result(name, *path, context, values)? - } - "chmod" => { - let [filename, permissions] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chmod_result(*filename, *permissions, context, values)? - } - "chown" | "chgrp" | "lchown" | "lchgrp" => { - let [filename, principal] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_chown_like_result(name, *filename, *principal, context, values)? - } "closedir" | "readdir" | "rewinddir" => { let [dir_handle] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_unary_directory_result(name, *dir_handle, context, values)? } - "clearstatcache" => { - if evaluated_args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - values.null()? - } - "copy" | "link" | "rename" | "symlink" => { - let [from, to] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_binary_path_bool_result(name, *from, *to, context, values)? - } "fclose" | "fgetc" | "fgets" @@ -174,12 +144,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let prompt = evaluated_args.first().copied(); eval_readline_result(prompt, values)? } - "scandir" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_scandir_result(*directory, values)? - } "opendir" => { let [directory] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -450,12 +414,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( )?, _ => return Err(EvalStatus::RuntimeFatal), }, - "tempnam" => { - let [directory, prefix] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_tempnam_result(*directory, *prefix, values)? - } "tmpfile" => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); @@ -468,27 +426,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_vfprintf_result(*stream, *format, *array, context, values)? } - "touch" => match evaluated_args { - [filename] => eval_touch_result(*filename, None, None, context, values)?, - [filename, mtime] => { - eval_touch_result(*filename, Some(*mtime), None, context, values)? - } - [filename, mtime, atime] => { - eval_touch_result(*filename, Some(*mtime), Some(*atime), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "umask" => match evaluated_args { - [] => eval_umask_result(None, values)?, - [mask] => eval_umask_result(Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "unlink" => { - let [filename] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unlink_result(*filename, context, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 4d35984c9c..70ed8ed1df 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -200,6 +200,12 @@ mod tests { "base64_encode", "bin2hex", "checkdate", + "chdir", + "chgrp", + "chmod", + "chown", + "clearstatcache", + "copy", "count", "ctype_alpha", "date", @@ -265,12 +271,16 @@ mod tests { "json_last_error", "json_last_error_msg", "json_validate", + "lchgrp", + "lchown", + "link", "linkinfo", "localtime", "log", "lstat", "microtime", "min", + "mkdir", "mktime", "nl2br", "number_format", @@ -287,6 +297,9 @@ mod tests { "realpath", "realpath_cache_get", "realpath_cache_size", + "rename", + "rmdir", + "scandir", "sleep", "stat", "stream_resolve_include_path", @@ -298,10 +311,15 @@ mod tests { "strrev", "strtotime", "substr", + "symlink", "sys_get_temp_dir", + "tempnam", "time", + "touch", "trim", "strval", + "umask", + "unlink", "usleep", "wordwrap", ] { @@ -544,5 +562,53 @@ mod tests { eval_declared_builtin_param_names("stat"), Some(["filename"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("chdir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chmod"), + Some(["filename", "permissions"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chown"), + Some(["filename", "user"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chgrp"), + Some(["filename", "group"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("clearstatcache", 0), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_default_value("clearstatcache", 1), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_param_names("link"), + Some(["target", "link"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("rename"), + Some(["from", "to"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("scandir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("tempnam"), + Some(["directory", "prefix"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("touch", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("umask", 0), + Some(EvalBuiltinDefaultValue::Null) + ); } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index b486f79026..ce1a55aa7f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -40,10 +40,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "buffer_new", "call_user_func", "call_user_func_array", - "chdir", - "chgrp", - "chmod", - "chown", "class_alias", "class_attribute_args", "class_attribute_names", @@ -52,9 +48,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "class_implements", "class_parents", "class_uses", - "clearstatcache", "closedir", - "copy", "define", "defined", "die", @@ -132,13 +126,9 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "iterator_to_array", "krsort", "ksort", - "lchgrp", - "lchown", - "link", "long2ip", "md5", "method_exists", - "mkdir", "mt_rand", "natcasesort", "natsort", @@ -172,12 +162,9 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "random_int", "readdir", "readline", - "rename", "rewind", "rewinddir", - "rmdir", "rsort", - "scandir", "settype", "sha1", "shell_exec", @@ -237,16 +224,11 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_wrapper_register", "stream_wrapper_restore", "stream_wrapper_unregister", - "symlink", "system", - "tempnam", "tmpfile", - "touch", "trait_exists", "uasort", "uksort", - "umask", - "unlink", "unset", "usort", "var_dump", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 11cb94731d..cf075e010d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -79,7 +79,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "header" => optional(params, 1), "http_response_code" => optional(params, 0), "is_callable" => optional_by_ref(params, 1, &["callable_name"]), - "php_uname" | "readline" | "umask" | "exit" | "die" => optional(params, 0), + "php_uname" | "readline" | "exit" | "die" => optional(params, 0), "explode" => optional(params, 2), "implode" => optional(params, 1), @@ -109,11 +109,9 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "print_r" => optional(params, 1), "var_dump" => variadic(params, &[]), - "touch" => optional(params, 1), "fopen" | "fseek" | "fputcsv" => optional(params, 2), "flock" => optional_by_ref(params, 2, &["would_block"]), "fgetcsv" => optional(params, 1), - "clearstatcache" => optional(params, 0), "stream_get_contents" => optional(params, 1), "stream_copy_to_stream" => optional(params, 2), "stream_socket_accept" => optional_by_ref(params, 1, &["peer_name"]), @@ -168,7 +166,7 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_callable", 1) => Bool(false), ("is_callable", 2) => Null, ("php_uname", 0) => String("a"), - ("readline" | "umask", 0) => Null, + ("readline", 0) => Null, ("exit" | "die", 0) => Int(0), ("explode", 2) => Int(i64::MAX), @@ -187,7 +185,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("print_r", 1) => Bool(false), - ("touch", 1 | 2) => Null, ("fopen", 2) => Bool(false), ("fopen", 3) => Null, ("flock", 2) => Null, @@ -196,8 +193,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("fgetcsv", 2) => String(","), ("fputcsv", 2) => String(","), ("fputcsv", 3) => String("\""), - ("clearstatcache", 0) => Bool(false), - ("clearstatcache", 1) => String(""), ("stream_get_contents", 1) => Null, ("stream_get_contents", 2) => Int(-1), ("stream_copy_to_stream", 2) => Null, diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index b54795b86f..af0d65e33a 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1548,11 +1548,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_array_key_set(name, args, context, scope, values) } "array_merge" => eval_builtin_array_merge(args, context, scope, values), - "chdir" | "mkdir" | "rmdir" => { - eval_builtin_unary_path_bool(name, args, context, scope, values) - } - "chmod" => eval_builtin_chmod(args, context, scope, values), - "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "class_alias" => eval_builtin_class_alias(args, context, scope, values), @@ -1580,9 +1575,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "closedir" | "readdir" | "rewinddir" => { eval_builtin_unary_directory(name, args, context, scope, values) } - "copy" | "link" | "rename" | "symlink" => { - eval_builtin_binary_path_bool(name, args, context, scope, values) - } "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "die" | "exit" => eval_builtin_exit(args, context, scope, values), @@ -1641,9 +1633,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_hash_one_shot(name, args, context, scope, values) } "header" => eval_builtin_header(args, context, scope, values), - "chown" | "chgrp" | "lchown" | "lchgrp" => { - eval_builtin_chown_like(name, args, context, scope, values) - } "hash_algos" => eval_builtin_hash_algos(args, values), "hash_copy" => eval_builtin_hash_copy(args, context, scope, values), "hash_final" => eval_builtin_hash_final(args, context, scope, values), @@ -1673,7 +1662,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), "random_int" => eval_builtin_random_int(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), - "scandir" => eval_builtin_scandir(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "spl_autoload_register" | "spl_autoload_unregister" => { eval_builtin_spl_autoload_bool(name, args, context, scope, values) @@ -1693,8 +1681,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "sscanf" => eval_builtin_sscanf(args, context, scope, values), "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), - "tempnam" => eval_builtin_tempnam(args, context, scope, values), - "touch" => eval_builtin_touch(args, context, scope, values), "tmpfile" => eval_builtin_tmpfile(args, context, values), "stream_is_local" | "stream_supports_lock" => { eval_builtin_stream_bool_predicate(name, args, context, scope, values) @@ -1765,10 +1751,8 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_stream_set_buffer_like(name, args, context, scope, values) } "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), - "unlink" => eval_builtin_unlink(args, context, scope, values), "long2ip" => eval_builtin_long2ip(args, context, scope, values), "unset" => eval_builtin_unset(args, context, scope, values), - "umask" => eval_builtin_umask(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index cc39c5e7f4..d6caac20ad 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -117,11 +117,17 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "boolval", "ceil", "checkdate", + "chdir", + "chgrp", + "chmod", + "chown", "chr", "chop", + "clearstatcache", "clamp", "cos", "cosh", + "copy", "count", "crc32", "ctype_alnum", @@ -201,7 +207,10 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "json_last_error", "json_last_error_msg", "json_validate", + "lchgrp", + "lchown", "lcfirst", + "link", "linkinfo", "localtime", "log", @@ -212,6 +221,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "max", "microtime", "min", + "mkdir", "mktime", "nl2br", "number_format", @@ -233,8 +243,11 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "realpath", "realpath_cache_get", "realpath_cache_size", + "rename", "round", "rtrim", + "rmdir", + "scandir", "sin", "sinh", "sleep", @@ -263,13 +276,18 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "substr_replace", "strval", "stream_resolve_include_path", + "symlink", "sys_get_temp_dir", "tan", "tanh", + "tempnam", "time", + "touch", "trim", "ucfirst", "ucwords", + "umask", + "unlink", "urldecode", "urlencode", "usleep", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 033eaec43e..0388a0a739 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8222,6 +8222,10 @@ eval('file_put_contents("eval-mod.txt", "x"); echo chmod(filename: "eval-mod.txt", permissions: 384) ? "chmod" : "bad"; echo ":"; echo (fileperms("eval-mod.txt") & 511) === 384 ? "mode" : "bad"; echo ":"; echo chmod("eval-missing-mod.txt", 384) ? "bad" : "chmod-false"; echo ":"; +echo chown("eval-missing-owner.txt", 1000) ? "bad" : "chown-false"; echo ":"; +echo chgrp("eval-missing-group.txt", 1000) ? "bad" : "chgrp-false"; echo ":"; +echo lchown("eval-missing-link.txt", 1000) ? "bad" : "lchown-false"; echo ":"; +echo lchgrp("eval-missing-link.txt", 1000) ? "bad" : "lchgrp-false"; echo ":"; $tmp = tempnam(directory: ".", prefix: "evm"); echo file_exists($tmp) && str_starts_with(basename($tmp), "evm") ? "tempnam" : "bad"; echo ":"; unlink($tmp); @@ -8233,17 +8237,21 @@ $probe = umask(); $restore = umask($before); echo $probe === 18 && $restore === 18 ? "probe" : "bad"; echo ":"; echo call_user_func("chmod", "eval-mod.txt", 420) ? "callchmod" : "bad"; echo ":"; +echo call_user_func("chown", "eval-missing-call-owner.txt", 1000) ? "bad" : "callchown-false"; echo ":"; +echo call_user_func_array("chgrp", ["filename" => "eval-missing-call-group.txt", "group" => 1000]) ? "bad" : "callchgrp-false"; echo ":"; $call_tmp = call_user_func_array("tempnam", ["directory" => ".", "prefix" => "evc"]); echo file_exists($call_tmp) && str_starts_with(basename($call_tmp), "evc") ? "calltempnam" : "bad"; echo ":"; unlink($call_tmp); echo unlink("eval-mod.txt") ? "cleanup" : "bad"; echo ":"; -echo function_exists("chmod"); echo function_exists("tempnam"); echo function_exists("umask"); +echo function_exists("chmod"); echo function_exists("chown"); echo function_exists("chgrp"); +echo function_exists("lchown"); echo function_exists("lchgrp"); echo function_exists("tempnam"); +echo function_exists("umask"); '); "#, ); assert_eq!( out, - "chmod:mode:chmod-false:tempnam:umask:probe:callchmod:calltempnam:cleanup:111" + "chmod:mode:chmod-false:chown-false:chgrp-false:lchown-false:lchgrp-false:tempnam:umask:probe:callchmod:callchown-false:callchgrp-false:calltempnam:cleanup:1111111" ); } From a8e8141bfcac937136b73e48f7d4f1766dd140f0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 01:37:29 +0200 Subject: [PATCH 1073/1208] refactor(magician): migrate filesystem process builtins --- .../builtins/filesystem/declarations/mod.rs | 18 +++++++++++++++ .../filesystem/declarations/pclose.rs | 16 +++++++++++++ .../builtins/filesystem/declarations/popen.rs | 16 +++++++++++++ .../filesystem/declarations/tmpfile.rs | 16 +++++++++++++ .../interpreter/builtins/registry/binding.rs | 3 --- .../builtins/registry/dispatch/filesystem.rs | 18 --------------- .../src/interpreter/builtins/registry/mod.rs | 15 ++++++++++++ .../interpreter/builtins/registry/names.rs | 3 --- .../src/interpreter/expressions.rs | 3 --- tests/builtin_parity_tests.rs | 3 +++ tests/codegen/eval.rs | 23 +++++++++++++++++++ 11 files changed, 107 insertions(+), 27 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pclose.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/popen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tmpfile.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index 1839083258..8586ec5e7e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -52,6 +52,8 @@ mod linkinfo; mod lstat; mod mkdir; mod pathinfo; +mod pclose; +mod popen; mod readfile; mod readlink; mod realpath; @@ -65,6 +67,7 @@ mod stream_resolve_include_path; mod symlink; mod sys_get_temp_dir; mod tempnam; +mod tmpfile; mod touch; mod umask; mod unlink; @@ -110,6 +113,8 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "glob" => eval_builtin_glob(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), + "pclose" => eval_builtin_pclose(args, context, scope, values), + "popen" => eval_builtin_popen(args, context, scope, values), "readfile" => eval_builtin_readfile(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath" => eval_builtin_realpath(args, context, scope, values), @@ -122,6 +127,7 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( } "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "tempnam" => eval_builtin_tempnam(args, context, scope, values), + "tmpfile" => eval_builtin_tmpfile(args, context, values), "touch" => eval_builtin_touch(args, context, scope, values), "umask" => eval_builtin_umask(args, context, scope, values), "unlink" => eval_builtin_unlink(args, context, scope, values), @@ -230,6 +236,14 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [path, flags] => eval_pathinfo_result(*path, Some(*flags), values), _ => Err(EvalStatus::RuntimeFatal), }, + "pclose" => match evaluated_args { + [handle] => eval_pclose_result(*handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "popen" => match evaluated_args { + [command, mode] => eval_popen_result(*command, *mode, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "readfile" => match evaluated_args { [filename] => eval_readfile_result(*filename, context, values), _ => Err(EvalStatus::RuntimeFatal), @@ -270,6 +284,10 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [directory, prefix] => eval_tempnam_result(*directory, *prefix, values), _ => Err(EvalStatus::RuntimeFatal), }, + "tmpfile" => match evaluated_args { + [] => eval_tmpfile_result(context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "touch" => match evaluated_args { [filename] => eval_touch_result(*filename, None, None, context, values), [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, context, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pclose.rs new file mode 100644 index 0000000000..edcadec4a4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pclose.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `pclose`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process-pipe close helper. + +eval_builtin! { + name: "pclose", + area: Filesystem, + params: [handle], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/popen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/popen.rs new file mode 100644 index 0000000000..ce2b084812 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/popen.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `popen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process-pipe open helper. + +eval_builtin! { + name: "popen", + area: Filesystem, + params: [command, mode], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tmpfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tmpfile.rs new file mode 100644 index 0000000000..ac62f7e5ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tmpfile.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `tmpfile`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the temporary stream helper. + +eval_builtin! { + name: "tmpfile", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 31152ed858..ecea30d438 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -234,8 +234,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "md5" | "sha1" => Some(&["string", "binary"]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), - "pclose" => Some(&["handle"]), - "popen" => Some(&["command", "mode"]), "ptr" => Some(&["value"]), "ptr_null" => Some(&[]), "ptr_is_null" | "ptr_get" | "ptr_read8" | "ptr_read16" | "ptr_read32" => { @@ -301,7 +299,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_socket_shutdown" => Some(&["stream", "mode"]), "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), - "tmpfile" => Some(&[]), "long2ip" => Some(&["ip"]), "vfprintf" => Some(&["stream", "format", "values"]), "vsprintf" | "vprintf" => Some(&["format", "values"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 405237d9da..c4f3562eae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -150,18 +150,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_opendir_result(*directory, context, values)? } - "pclose" => { - let [handle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pclose_result(*handle, context, values)? - } - "popen" => { - let [command, mode] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_popen_result(*command, *mode, context, values)? - } "stream_copy_to_stream" => match evaluated_args { [from, to] => { eval_stream_copy_to_stream_result(*from, *to, None, None, context, values)? @@ -414,12 +402,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( )?, _ => return Err(EvalStatus::RuntimeFatal), }, - "tmpfile" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_tmpfile_result(context, values)? - } "vfprintf" => { let [stream, format, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 70ed8ed1df..2f4672a684 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -285,6 +285,8 @@ mod tests { "nl2br", "number_format", "pathinfo", + "pclose", + "popen", "preg_match", "preg_match_all", "preg_replace", @@ -315,6 +317,7 @@ mod tests { "sys_get_temp_dir", "tempnam", "time", + "tmpfile", "touch", "trim", "strval", @@ -602,6 +605,18 @@ mod tests { eval_declared_builtin_param_names("tempnam"), Some(["directory", "prefix"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("popen"), + Some(["command", "mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("pclose"), + Some(["handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("tmpfile"), + Some([].as_slice()) + ); assert_eq!( eval_declared_builtin_default_value("touch", 1), Some(EvalBuiltinDefaultValue::Null) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index ce1a55aa7f..e8d9232d55 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -134,11 +134,9 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "natsort", "opendir", "passthru", - "pclose", "pfsockopen", "php_uname", "phpversion", - "popen", "ptr", "ptr_get", "ptr_is_null", @@ -225,7 +223,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_wrapper_restore", "stream_wrapper_unregister", "system", - "tmpfile", "trait_exists", "uasort", "uksort", diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index af0d65e33a..aaf4a07462 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1649,8 +1649,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "opendir" => eval_builtin_opendir(args, context, scope, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), - "pclose" => eval_builtin_pclose(args, context, scope, values), - "popen" => eval_builtin_popen(args, context, scope, values), "buffer_free" | "buffer_len" | "buffer_new" | "ptr" | "ptr_get" | "ptr_is_null" | "ptr_null" | "ptr_offset" | "ptr_read8" | "ptr_read16" | "ptr_read32" | "ptr_read_string" | "ptr_set" | "ptr_sizeof" | "ptr_write8" | "ptr_write16" @@ -1681,7 +1679,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "sscanf" => eval_builtin_sscanf(args, context, scope, values), "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), - "tmpfile" => eval_builtin_tmpfile(args, context, values), "stream_is_local" | "stream_supports_lock" => { eval_builtin_stream_bool_predicate(name, args, context, scope, values) } diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index d6caac20ad..cf4ce75ea9 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -227,7 +227,9 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "number_format", "ord", "pathinfo", + "pclose", "pi", + "popen", "pow", "preg_match", "preg_match_all", @@ -282,6 +284,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "tanh", "tempnam", "time", + "tmpfile", "touch", "trim", "ucfirst", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 0388a0a739..670c1e8f74 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8280,6 +8280,29 @@ echo function_exists("touch"); ); } +/// Verifies eval process-pipe and temporary stream builtins dispatch dynamically. +#[test] +fn test_eval_dispatches_process_pipe_and_tmpfile_builtin_calls() { + let out = compile_and_run( + r#" "printf q", "mode" => "r"]); +echo fread($callPipe, 1) . ":"; +echo call_user_func("pclose", $callPipe) . ":"; +echo function_exists("tmpfile"); echo function_exists("popen"); echo function_exists("pclose");'); +"#, + ); + assert_eq!(out, "tmpfile:3:abc:xyz:0:calltmp:q:0:111"); +} + /// Verifies eval `bin2hex()` converts byte strings directly and by callable dispatch. #[test] fn test_eval_dispatches_bin2hex_builtin_call() { From c37254c000898ba6326ed8b189ee1674368c8ca0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 01:51:45 +0200 Subject: [PATCH 1074/1208] refactor(magician): migrate directory builtins --- .../filesystem/declarations/closedir.rs | 16 ++++++++++ .../builtins/filesystem/declarations/mod.rs | 16 ++++++++++ .../filesystem/declarations/opendir.rs | 16 ++++++++++ .../filesystem/declarations/readdir.rs | 16 ++++++++++ .../filesystem/declarations/rewinddir.rs | 16 ++++++++++ .../interpreter/builtins/registry/binding.rs | 2 -- .../builtins/registry/dispatch/filesystem.rs | 12 -------- .../src/interpreter/builtins/registry/mod.rs | 20 +++++++++++++ .../interpreter/builtins/registry/names.rs | 4 --- .../src/interpreter/expressions.rs | 4 --- tests/builtin_parity_tests.rs | 4 +++ tests/codegen/eval.rs | 30 +++++++++++++++++++ 12 files changed, 134 insertions(+), 22 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/closedir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/opendir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readdir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewinddir.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/closedir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/closedir.rs new file mode 100644 index 0000000000..0a342a5dfc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/closedir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `closedir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the directory resource close helper. + +eval_builtin! { + name: "closedir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index 8586ec5e7e..5c4cb9a968 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -17,6 +17,7 @@ mod chdir; mod chgrp; mod chmod; mod chown; +mod closedir; mod clearstatcache; mod copy; mod dirname; @@ -51,15 +52,18 @@ mod link; mod linkinfo; mod lstat; mod mkdir; +mod opendir; mod pathinfo; mod pclose; mod popen; +mod readdir; mod readfile; mod readlink; mod realpath; mod realpath_cache_get; mod realpath_cache_size; mod rename; +mod rewinddir; mod rmdir; mod scandir; mod stat; @@ -112,9 +116,13 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "getcwd" => eval_builtin_getcwd(args, values), "glob" => eval_builtin_glob(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), + "opendir" => eval_builtin_opendir(args, context, scope, values), "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "pclose" => eval_builtin_pclose(args, context, scope, values), "popen" => eval_builtin_popen(args, context, scope, values), + "closedir" | "readdir" | "rewinddir" => { + eval_builtin_unary_directory(name, args, context, scope, values) + } "readfile" => eval_builtin_readfile(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath" => eval_builtin_realpath(args, context, scope, values), @@ -231,6 +239,10 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [path] => eval_linkinfo_result(*path, values), _ => Err(EvalStatus::RuntimeFatal), }, + "opendir" => match evaluated_args { + [directory] => eval_opendir_result(*directory, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "pathinfo" => match evaluated_args { [path] => eval_pathinfo_result(*path, None, values), [path, flags] => eval_pathinfo_result(*path, Some(*flags), values), @@ -244,6 +256,10 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [command, mode] => eval_popen_result(*command, *mode, context, values), _ => Err(EvalStatus::RuntimeFatal), }, + "closedir" | "readdir" | "rewinddir" => match evaluated_args { + [dir_handle] => eval_unary_directory_result(name, *dir_handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "readfile" => match evaluated_args { [filename] => eval_readfile_result(*filename, context, values), _ => Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/opendir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/opendir.rs new file mode 100644 index 0000000000..3dd071e63a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/opendir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `opendir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the directory resource open helper. + +eval_builtin! { + name: "opendir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readdir.rs new file mode 100644 index 0000000000..50f2dd3da7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readdir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `readdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the directory resource read helper. + +eval_builtin! { + name: "readdir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewinddir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewinddir.rs new file mode 100644 index 0000000000..336446c92d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewinddir.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `rewinddir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the directory resource rewind helper. + +eval_builtin! { + name: "rewinddir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index ecea30d438..86b5a1b7a6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -168,8 +168,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "interface_exists" => Some(&["interface", "autoload"]), "trait_exists" => Some(&["trait", "autoload"]), "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), - "opendir" => Some(&["directory"]), - "closedir" | "readdir" | "rewinddir" => Some(&["dir_handle"]), "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "die" | "exit" => Some(&["status"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index c4f3562eae..467c790011 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -19,12 +19,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "closedir" | "readdir" | "rewinddir" => { - let [dir_handle] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unary_directory_result(name, *dir_handle, context, values)? - } "fclose" | "fgetc" | "fgets" @@ -144,12 +138,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let prompt = evaluated_args.first().copied(); eval_readline_result(prompt, values)? } - "opendir" => { - let [directory] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_opendir_result(*directory, context, values)? - } "stream_copy_to_stream" => match evaluated_args { [from, to] => { eval_stream_copy_to_stream_result(*from, *to, None, None, context, values)? diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 2f4672a684..093a937249 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -204,6 +204,7 @@ mod tests { "chgrp", "chmod", "chown", + "closedir", "clearstatcache", "copy", "count", @@ -284,6 +285,7 @@ mod tests { "mktime", "nl2br", "number_format", + "opendir", "pathinfo", "pclose", "popen", @@ -294,12 +296,14 @@ mod tests { "preg_split", "range", "rawurlencode", + "readdir", "readfile", "readlink", "realpath", "realpath_cache_get", "realpath_cache_size", "rename", + "rewinddir", "rmdir", "scandir", "sleep", @@ -613,6 +617,22 @@ mod tests { eval_declared_builtin_param_names("pclose"), Some(["handle"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("opendir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("closedir"), + Some(["dir_handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("readdir"), + Some(["dir_handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("rewinddir"), + Some(["dir_handle"].as_slice()) + ); assert_eq!( eval_declared_builtin_param_names("tmpfile"), Some([].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index e8d9232d55..1cacc88d3b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -48,7 +48,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "class_implements", "class_parents", "class_uses", - "closedir", "define", "defined", "die", @@ -132,7 +131,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "mt_rand", "natcasesort", "natsort", - "opendir", "passthru", "pfsockopen", "php_uname", @@ -158,10 +156,8 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "putenv", "rand", "random_int", - "readdir", "readline", "rewind", - "rewinddir", "rsort", "settype", "sha1", diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index aaf4a07462..9d9addbf00 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1572,9 +1572,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_class_like_exists(name, args, context, scope, values) } "is_a" | "is_subclass_of" => eval_builtin_is_a_relation(name, args, context, scope, values), - "closedir" | "readdir" | "rewinddir" => { - eval_builtin_unary_directory(name, args, context, scope, values) - } "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "die" | "exit" => eval_builtin_exit(args, context, scope, values), @@ -1646,7 +1643,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), "ip2long" => eval_builtin_ip2long(args, context, scope, values), - "opendir" => eval_builtin_opendir(args, context, scope, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), "buffer_free" | "buffer_len" | "buffer_new" | "ptr" | "ptr_get" | "ptr_is_null" diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index cf4ce75ea9..79135dcb65 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -121,6 +121,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "chgrp", "chmod", "chown", + "closedir", "chr", "chop", "clearstatcache", @@ -225,6 +226,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "mktime", "nl2br", "number_format", + "opendir", "ord", "pathinfo", "pclose", @@ -240,6 +242,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "range", "rawurldecode", "rawurlencode", + "readdir", "readfile", "readlink", "realpath", @@ -248,6 +251,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "rename", "round", "rtrim", + "rewinddir", "rmdir", "scandir", "sin", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 670c1e8f74..8d488b7be9 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -8177,6 +8177,36 @@ echo function_exists("file"); echo function_exists("readfile"); echo function_ex ); } +/// Verifies eval directory resource builtins dispatch directly and dynamically. +#[test] +fn test_eval_dispatches_directory_resource_builtin_calls() { + let out = compile_and_run( + r#" $call]) === null ? "callclose" : "bad"; echo ":"; +echo unlink("eval-dir-handle/a.txt") && rmdir("eval-dir-handle") ? "cleanup" : "bad"; echo ":"; +echo function_exists("opendir"); echo function_exists("readdir"); echo function_exists("rewinddir"); echo function_exists("closedir"); +'); +"#, + ); + assert_eq!(out, "iter:callread:callrewind:callclose:cleanup:1111"); +} + /// Verifies eval `glob()` expands local patterns and dispatches dynamically. #[test] fn test_eval_dispatches_glob_builtin_calls() { From 40b2be5e20d6038373567b96409db197533ce3ea Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 02:04:10 +0200 Subject: [PATCH 1075/1208] refactor(magician): migrate stream query builtins --- .../src/interpreter/builtins/hooks/direct.rs | 17 ++++++++++--- .../src/interpreter/builtins/hooks/values.rs | 20 ++++++++++++--- .../interpreter/builtins/registry/binding.rs | 3 +-- .../builtins/registry/dispatch/network_env.rs | 8 +----- .../builtins/registry/dispatch/strings.rs | 6 ----- .../src/interpreter/builtins/registry/mod.rs | 25 +++++++++++++++++++ .../interpreter/builtins/registry/names.rs | 5 ---- .../builtins/strings/declarations/mod.rs | 14 +++++++++++ .../declarations/stream_get_filters.rs | 16 ++++++++++++ .../declarations/stream_get_transports.rs | 16 ++++++++++++ .../declarations/stream_get_wrappers.rs | 16 ++++++++++++ .../strings/declarations/stream_is_local.rs | 16 ++++++++++++ .../declarations/stream_supports_lock.rs | 16 ++++++++++++ .../src/interpreter/builtins/strings/mod.rs | 1 + .../src/interpreter/expressions.rs | 6 ----- tests/builtin_parity_tests.rs | 5 ++++ tests/codegen/eval.rs | 10 ++++++-- 17 files changed, 166 insertions(+), 34 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_filters.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_transports.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_wrappers.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_is_local.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_supports_lock.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index e28d96e04c..f2c7868df5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -28,9 +28,10 @@ use super::super::{ eval_builtin_filesystem_call, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_nl2br, eval_builtin_range, eval_builtin_regex_call, eval_builtin_str_pad, eval_builtin_str_replace, eval_builtin_str_split, - eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, - eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, - eval_builtin_substr_replace, eval_builtin_time_call, eval_builtin_trim_like, + eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, eval_builtin_string_case, + eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, + eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, + eval_builtin_time_call, eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, }; @@ -133,6 +134,10 @@ pub(in crate::interpreter) enum EvalDirectHook { StringPosition, /// Dispatches string search predicate builtins. StringSearch, + /// Dispatches stream boolean predicate builtins. + StreamBoolPredicate, + /// Dispatches stream introspection list builtins. + StreamIntrospection, /// Dispatches `str_pad(...)`. StrPad, /// Dispatches `str_replace(...)` and `str_ireplace(...)`. @@ -230,6 +235,12 @@ impl EvalDirectHook { eval_builtin_string_position(name, args, context, scope, values) } Self::StringSearch => eval_builtin_string_search(name, args, context, scope, values), + Self::StreamBoolPredicate => { + eval_builtin_stream_bool_predicate(name, args, context, scope, values) + } + Self::StreamIntrospection => { + eval_builtin_stream_introspection(name, args, context, values) + } Self::StrPad => eval_builtin_str_pad(args, context, scope, values), Self::StrReplace => eval_builtin_str_replace(name, args, context, scope, values), Self::StrSplit => eval_builtin_str_split(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 5f4c709160..f877235441 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -26,9 +26,10 @@ use super::super::{ eval_json_values_result, eval_log_result, eval_min_max_result, eval_nl2br_result, eval_number_format_result, eval_range_result, eval_regex_values_result, eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, - eval_string_case_result, eval_string_compare_result, eval_string_position_result, - eval_string_search_result, eval_strstr_result, eval_substr_replace_result, - eval_substr_result, eval_time_values_result, eval_trim_like_result, + eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, + eval_string_compare_result, eval_string_position_result, eval_string_search_result, + eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, + eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; @@ -132,6 +133,10 @@ pub(in crate::interpreter) enum EvalValuesHook { StringPosition, /// Dispatches string search predicate builtins. StringSearch, + /// Dispatches stream boolean predicate builtins. + StreamBoolPredicate, + /// Dispatches stream introspection list builtins. + StreamIntrospection, /// Dispatches `str_pad(...)`. StrPad, /// Dispatches `str_replace(...)` and `str_ireplace(...)`. @@ -310,6 +315,15 @@ impl EvalValuesHook { Self::StringSearch => two_args(evaluated_args, values, |haystack, needle, values| { eval_string_search_result(name, haystack, needle, values) }), + Self::StreamBoolPredicate => one_arg(evaluated_args, values, |stream, values| { + eval_stream_bool_predicate_result(name, stream, values) + }), + Self::StreamIntrospection => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_result(name, context, values) + } Self::StrPad => match evaluated_args { [value, length] => eval_str_pad_result(*value, *length, None, None, values), [value, length, pad_string] => { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 86b5a1b7a6..c634495058 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -258,7 +258,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "spl_object_id" | "spl_object_hash" => Some(&["object"]), "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), - "stream_is_local" | "stream_isatty" | "stream_supports_lock" => Some(&["stream"]), + "stream_isatty" => Some(&["stream"]), "stream_bucket_make_writeable" => Some(&["brigade"]), "stream_bucket_new" => Some(&["stream", "buffer"]), "stream_bucket_append" | "stream_bucket_prepend" => Some(&["brigade", "bucket"]), @@ -278,7 +278,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_filter_remove" => Some(&["stream_filter"]), "stream_get_contents" => Some(&["stream", "length", "offset"]), "stream_get_line" => Some(&["stream", "length", "ending"]), - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => Some(&[]), "stream_set_blocking" => Some(&["stream", "enable"]), "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { Some(&["stream", "size"]) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs index 029f0bca0e..ca224e22aa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs @@ -15,7 +15,7 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_network_env_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { @@ -108,12 +108,6 @@ pub(in crate::interpreter) fn eval_network_env_builtin_with_values( }; eval_putenv_result(*assignment, values)? } - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_introspection_result(name, context, values)? - } "long2ip" => { let [value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs index 27690d9b2c..d65851f2e9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs @@ -69,12 +69,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( }; eval_hash_update_result(*hash_context, *data, context, values)? } - "stream_is_local" | "stream_supports_lock" => { - let [stream] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_bool_predicate_result(name, *stream, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 093a937249..dad5404415 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -308,7 +308,12 @@ mod tests { "scandir", "sleep", "stat", + "stream_get_filters", + "stream_get_transports", + "stream_get_wrappers", + "stream_is_local", "stream_resolve_include_path", + "stream_supports_lock", "str_contains", "str_pad", "str_replace", @@ -637,6 +642,26 @@ mod tests { eval_declared_builtin_param_names("tmpfile"), Some([].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_wrappers"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_transports"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_filters"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_is_local"), + Some(["stream"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_supports_lock"), + Some(["stream"].as_slice()) + ); assert_eq!( eval_declared_builtin_default_value("touch", 1), Some(EvalBuiltinDefaultValue::Null) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 1cacc88d3b..c37b481c5f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -192,12 +192,8 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_filter_register", "stream_filter_remove", "stream_get_contents", - "stream_get_filters", "stream_get_line", "stream_get_meta_data", - "stream_get_transports", - "stream_get_wrappers", - "stream_is_local", "stream_isatty", "stream_select", "stream_set_blocking", @@ -214,7 +210,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_socket_sendto", "stream_socket_server", "stream_socket_shutdown", - "stream_supports_lock", "stream_wrapper_register", "stream_wrapper_restore", "stream_wrapper_unregister", diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs new file mode 100644 index 0000000000..269464cae9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs @@ -0,0 +1,14 @@ +//! Purpose: +//! Declarative eval registry entries for string-adjacent stream introspection builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings` module loading. +//! +//! Key details: +//! - Runtime behavior stays delegated to existing stream-introspection helpers. + +mod stream_get_filters; +mod stream_get_transports; +mod stream_get_wrappers; +mod stream_is_local; +mod stream_supports_lock; diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_filters.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_filters.rs new file mode 100644 index 0000000000..9c08a044a4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_filters.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_filters`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the static stream-filter list helper. + +eval_builtin! { + name: "stream_get_filters", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_transports.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_transports.rs new file mode 100644 index 0000000000..9dc5bdcd1d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_transports.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_transports`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the static stream-transport list helper. + +eval_builtin! { + name: "stream_get_transports", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_wrappers.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_wrappers.rs new file mode 100644 index 0000000000..412b59c490 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_wrappers.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_wrappers`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the eval stream-wrapper registry helper. + +eval_builtin! { + name: "stream_get_wrappers", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_is_local.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_is_local.rs new file mode 100644 index 0000000000..3200dc2b51 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_is_local.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_is_local`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream boolean predicate helper. + +eval_builtin! { + name: "stream_is_local", + area: String, + params: [stream], + direct: StreamBoolPredicate, + values: StreamBoolPredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_supports_lock.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_supports_lock.rs new file mode 100644 index 0000000000..076771f0a5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_supports_lock.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_supports_lock`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream boolean predicate helper. + +eval_builtin! { + name: "stream_supports_lock", + area: String, + params: [stream], + direct: StreamBoolPredicate, + values: StreamBoolPredicate, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/mod.rs b/crates/elephc-magician/src/interpreter/builtins/strings/mod.rs index 7293821e52..c9506be692 100644 --- a/crates/elephc-magician/src/interpreter/builtins/strings/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/strings/mod.rs @@ -9,6 +9,7 @@ //! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime behavior. mod ctype; +mod declarations; mod grapheme_strrev; mod gzip; mod hash; diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 9d9addbf00..8dce675cad 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1675,12 +1675,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "sscanf" => eval_builtin_sscanf(args, context, scope, values), "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), - "stream_is_local" | "stream_supports_lock" => { - eval_builtin_stream_bool_predicate(name, args, context, scope, values) - } - "stream_get_filters" | "stream_get_transports" | "stream_get_wrappers" => { - eval_builtin_stream_introspection(name, args, context, values) - } "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), "stream_context_get_default" => { diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 79135dcb65..7e32f4c778 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -259,6 +259,10 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "sleep", "sqrt", "stat", + "stream_get_filters", + "stream_get_transports", + "stream_get_wrappers", + "stream_is_local", "str_contains", "str_ends_with", "str_ireplace", @@ -282,6 +286,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "substr_replace", "strval", "stream_resolve_include_path", + "stream_supports_lock", "symlink", "sys_get_temp_dir", "tan", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 8d488b7be9..67865a28fc 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -7551,12 +7551,18 @@ $call_transports = call_user_func_array("stream_get_transports", []); echo $call_transports[11] . ":"; $call_filters = call_user_func_array("stream_get_filters", []); echo $call_filters[13] . ":"; -echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); echo function_exists("stream_get_filters");'); +$tmp = tmpfile(); +echo stream_is_local("php://memory") ? "local" : "bad"; echo ":"; +echo stream_supports_lock($tmp) ? "lock" : "bad"; echo ":"; +echo call_user_func("stream_is_local", "file://tmp") ? "calllocal" : "bad"; echo ":"; +echo call_user_func_array("stream_supports_lock", ["stream" => $tmp]) ? "calllock" : "bad"; echo ":"; +echo function_exists("stream_get_wrappers"); echo function_exists("stream_get_transports"); echo function_exists("stream_get_filters"); +echo function_exists("stream_is_local"); echo function_exists("stream_supports_lock");'); "#, ); assert_eq!( out, - "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:111" + "11:file:https:12:tcp:tlsv1.0:14:string.rot13:glob:tlsv1.3:bzip2.decompress:local:lock:calllocal:calllock:11111" ); } From 0c4b12eeb24870ae8c950bf08ffa1d9deb40bebd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 02:12:09 +0200 Subject: [PATCH 1076/1208] refactor(magician): migrate stream setting builtins --- .../builtins/filesystem/declarations/mod.rs | 35 +++++++++++++++++++ .../filesystem/declarations/stream_isatty.rs | 16 +++++++++ .../declarations/stream_set_blocking.rs | 16 +++++++++ .../declarations/stream_set_chunk_size.rs | 16 +++++++++ .../declarations/stream_set_read_buffer.rs | 16 +++++++++ .../declarations/stream_set_timeout.rs | 18 ++++++++++ .../declarations/stream_set_write_buffer.rs | 16 +++++++++ .../interpreter/builtins/registry/binding.rs | 6 ---- .../builtins/registry/dispatch/filesystem.rs | 31 ---------------- .../src/interpreter/builtins/registry/mod.rs | 34 ++++++++++++++++++ .../interpreter/builtins/registry/names.rs | 6 ---- .../builtins/registry/signature.rs | 3 +- .../src/interpreter/expressions.rs | 6 ---- tests/builtin_parity_tests.rs | 6 ++++ 14 files changed, 174 insertions(+), 51 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_isatty.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_blocking.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_chunk_size.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_read_buffer.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_timeout.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_write_buffer.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index 5c4cb9a968..1afb680859 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -67,7 +67,13 @@ mod rewinddir; mod rmdir; mod scandir; mod stat; +mod stream_isatty; mod stream_resolve_include_path; +mod stream_set_blocking; +mod stream_set_chunk_size; +mod stream_set_read_buffer; +mod stream_set_timeout; +mod stream_set_write_buffer; mod symlink; mod sys_get_temp_dir; mod tempnam; @@ -133,6 +139,12 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "stream_resolve_include_path" => { eval_builtin_stream_resolve_include_path(args, context, scope, values) } + "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), + "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), + "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { + eval_builtin_stream_set_buffer_like(name, args, context, scope, values) + } + "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), "tempnam" => eval_builtin_tempnam(args, context, scope, values), "tmpfile" => eval_builtin_tmpfile(args, context, values), @@ -292,6 +304,29 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [filename] => eval_stream_resolve_include_path_result(*filename, values), _ => Err(EvalStatus::RuntimeFatal), }, + "stream_isatty" => match evaluated_args { + [stream] => eval_stream_isatty_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "stream_set_blocking" => match evaluated_args { + [stream, enable] => eval_stream_set_blocking_result(*stream, *enable, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { + match evaluated_args { + [stream, size] => { + eval_stream_set_buffer_like_result(name, *stream, *size, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } + } + "stream_set_timeout" => match evaluated_args { + [stream, seconds] => eval_stream_set_timeout_result(*stream, *seconds, None, context, values), + [stream, seconds, microseconds] => { + eval_stream_set_timeout_result(*stream, *seconds, Some(*microseconds), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, "sys_get_temp_dir" => match evaluated_args { [] => eval_sys_get_temp_dir_result(values), _ => Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_isatty.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_isatty.rs new file mode 100644 index 0000000000..62f09a0b22 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_isatty.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_isatty`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream descriptor predicate helper. + +eval_builtin! { + name: "stream_isatty", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_blocking.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_blocking.rs new file mode 100644 index 0000000000..ef5f9f23d4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_blocking.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_blocking`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream blocking-mode helper. + +eval_builtin! { + name: "stream_set_blocking", + area: Filesystem, + params: [stream, enable], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_chunk_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_chunk_size.rs new file mode 100644 index 0000000000..22ebbb8ad9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_chunk_size.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_chunk_size`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream chunk-size metadata helper. + +eval_builtin! { + name: "stream_set_chunk_size", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_read_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_read_buffer.rs new file mode 100644 index 0000000000..c3621011a8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_read_buffer.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_read_buffer`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream buffer-setting helper. + +eval_builtin! { + name: "stream_set_read_buffer", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_timeout.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_timeout.rs new file mode 100644 index 0000000000..1382d9ef85 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_timeout.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_timeout`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream timeout-setting helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_set_timeout", + area: Filesystem, + params: [stream, seconds, microseconds = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_write_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_write_buffer.rs new file mode 100644 index 0000000000..da2f40132b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_write_buffer.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_write_buffer`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream buffer-setting helper. + +eval_builtin! { + name: "stream_set_write_buffer", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index c634495058..87229dcb77 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -258,7 +258,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "spl_object_id" | "spl_object_hash" => Some(&["object"]), "sscanf" => Some(&["string", "format", "vars"]), "sprintf" | "printf" => Some(&["format", "values"]), - "stream_isatty" => Some(&["stream"]), "stream_bucket_make_writeable" => Some(&["brigade"]), "stream_bucket_new" => Some(&["stream", "buffer"]), "stream_bucket_append" | "stream_bucket_prepend" => Some(&["brigade", "bucket"]), @@ -278,11 +277,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_filter_remove" => Some(&["stream_filter"]), "stream_get_contents" => Some(&["stream", "length", "offset"]), "stream_get_line" => Some(&["stream", "length", "ending"]), - "stream_set_blocking" => Some(&["stream", "enable"]), - "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { - Some(&["stream", "size"]) - } - "stream_set_timeout" => Some(&["stream", "seconds", "microseconds"]), "stream_select" => Some(&["read", "write", "except", "seconds", "microseconds"]), "stream_socket_server" | "stream_socket_client" => Some(&["address"]), "stream_socket_accept" => Some(&["socket", "timeout", "peer_name"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 467c790011..98fe946766 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -359,37 +359,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, - "stream_isatty" => { - let [stream] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_isatty_result(*stream, context, values)? - } - "stream_set_blocking" => { - let [stream, enable] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_set_blocking_result(*stream, *enable, context, values)? - } - "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { - let [stream, size] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_set_buffer_like_result(name, *stream, *size, context, values)? - } - "stream_set_timeout" => match evaluated_args { - [stream, seconds] => { - eval_stream_set_timeout_result(*stream, *seconds, None, context, values)? - } - [stream, seconds, microseconds] => eval_stream_set_timeout_result( - *stream, - *seconds, - Some(*microseconds), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "vfprintf" => { let [stream, format, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index dad5404415..19087a4f5c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -312,7 +312,13 @@ mod tests { "stream_get_transports", "stream_get_wrappers", "stream_is_local", + "stream_isatty", "stream_resolve_include_path", + "stream_set_blocking", + "stream_set_chunk_size", + "stream_set_read_buffer", + "stream_set_timeout", + "stream_set_write_buffer", "stream_supports_lock", "str_contains", "str_pad", @@ -662,6 +668,34 @@ mod tests { eval_declared_builtin_param_names("stream_supports_lock"), Some(["stream"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("stream_isatty"), + Some(["stream"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_blocking"), + Some(["stream", "enable"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_chunk_size"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_read_buffer"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_write_buffer"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_timeout"), + Some(["stream", "seconds", "microseconds"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_set_timeout", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); assert_eq!( eval_declared_builtin_default_value("touch", 1), Some(EvalBuiltinDefaultValue::Null) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index c37b481c5f..5cf869334b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -194,13 +194,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_get_contents", "stream_get_line", "stream_get_meta_data", - "stream_isatty", "stream_select", - "stream_set_blocking", - "stream_set_chunk_size", - "stream_set_read_buffer", - "stream_set_timeout", - "stream_set_write_buffer", "stream_socket_accept", "stream_socket_client", "stream_socket_enable_crypto", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index cf075e010d..1c4b661bea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -121,7 +121,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "stream_wrapper_register" | "stream_socket_enable_crypto" => optional(params, 2), "stream_context_create" | "stream_context_get_default" => optional(params, 0), "stream_context_set_option" => optional(params, 2), - "stream_get_line" | "stream_set_timeout" | "stream_socket_sendto" + "stream_get_line" | "stream_socket_sendto" | "stream_filter_append" | "stream_filter_prepend" => optional(params, 2), "stream_select" => optional_by_ref(params, 4, &["read", "write", "except"]), "stream_socket_recvfrom" => optional_by_ref(params, 2, &["address"]), @@ -206,7 +206,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("stream_context_set_option", 2 | 3) => Null, ("stream_get_line", 2) => String(""), ("stream_select", 4) => Int(0), - ("stream_set_timeout", 2) => Int(0), ("stream_socket_sendto", 2) => Int(0), ("stream_socket_sendto", 3) => String(""), ("stream_socket_recvfrom", 2) => Int(0), diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 8dce675cad..e45e349e71 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1732,12 +1732,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), - "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), - "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), - "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { - eval_builtin_stream_set_buffer_like(name, args, context, scope, values) - } - "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), "long2ip" => eval_builtin_long2ip(args, context, scope, values), "unset" => eval_builtin_unset(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 7e32f4c778..f4f3b418bb 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -263,6 +263,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "stream_get_transports", "stream_get_wrappers", "stream_is_local", + "stream_isatty", "str_contains", "str_ends_with", "str_ireplace", @@ -286,6 +287,11 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "substr_replace", "strval", "stream_resolve_include_path", + "stream_set_blocking", + "stream_set_chunk_size", + "stream_set_read_buffer", + "stream_set_timeout", + "stream_set_write_buffer", "stream_supports_lock", "symlink", "sys_get_temp_dir", From d8b1fb9347c70c38428722cd9b8ccdebdf151761 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 02:23:04 +0200 Subject: [PATCH 1077/1208] refactor(magician): migrate stream content builtins --- .../builtins/filesystem/declarations/fread.rs | 16 +++++ .../builtins/filesystem/declarations/fseek.rs | 18 +++++ .../filesystem/declarations/ftruncate.rs | 16 +++++ .../filesystem/declarations/fwrite.rs | 16 +++++ .../builtins/filesystem/declarations/mod.rs | 69 +++++++++++++++++++ .../declarations/stream_copy_to_stream.rs | 23 +++++++ .../declarations/stream_get_contents.rs | 22 ++++++ .../declarations/stream_get_line.rs | 18 +++++ .../interpreter/builtins/registry/binding.rs | 7 -- .../builtins/registry/dispatch/filesystem.rs | 65 ----------------- .../src/interpreter/builtins/registry/mod.rs | 59 ++++++++++++++++ .../interpreter/builtins/registry/names.rs | 7 -- .../builtins/registry/signature.rs | 15 ++-- .../src/interpreter/expressions.rs | 7 -- tests/builtin_parity_tests.rs | 7 ++ 15 files changed, 268 insertions(+), 97 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fread.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fseek.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftruncate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fwrite.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_copy_to_stream.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_contents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_line.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fread.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fread.rs new file mode 100644 index 0000000000..b93efa8cbb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fread.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fread`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream read helper. + +eval_builtin! { + name: "fread", + area: Filesystem, + params: [stream, length], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fseek.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fseek.rs new file mode 100644 index 0000000000..9b3a92bbfc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fseek.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `fseek`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream seek helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fseek", + area: Filesystem, + params: [stream, offset, whence = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftruncate.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftruncate.rs new file mode 100644 index 0000000000..f4490302bb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftruncate.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ftruncate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream truncate helper. + +eval_builtin! { + name: "ftruncate", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fwrite.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fwrite.rs new file mode 100644 index 0000000000..a4c8048558 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fwrite.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fwrite`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream write helper. + +eval_builtin! { + name: "fwrite", + area: Filesystem, + params: [stream, data], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index 1afb680859..fc637f0397 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -37,6 +37,10 @@ mod fileperms; mod filesize; mod filetype; mod fnmatch; +mod fread; +mod fseek; +mod ftruncate; +mod fwrite; mod getcwd; mod glob; mod is_dir; @@ -67,6 +71,9 @@ mod rewinddir; mod rmdir; mod scandir; mod stat; +mod stream_copy_to_stream; +mod stream_get_contents; +mod stream_get_line; mod stream_isatty; mod stream_resolve_include_path; mod stream_set_blocking; @@ -119,6 +126,10 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "filesize" => eval_builtin_filesize(args, context, scope, values), "filetype" => eval_builtin_filetype(args, context, scope, values), "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), + "fread" => eval_builtin_fread(args, context, scope, values), + "fseek" => eval_builtin_fseek(args, context, scope, values), + "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), + "fwrite" => eval_builtin_fwrite(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), "glob" => eval_builtin_glob(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), @@ -136,6 +147,9 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), "scandir" => eval_builtin_scandir(args, context, scope, values), "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), + "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), + "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), + "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), "stream_resolve_include_path" => { eval_builtin_stream_resolve_include_path(args, context, scope, values) } @@ -239,6 +253,25 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( } _ => Err(EvalStatus::RuntimeFatal), }, + "fread" => match evaluated_args { + [stream, length] => eval_fread_result(*stream, *length, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "fseek" => match evaluated_args { + [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values), + [stream, offset, whence] => { + eval_fseek_result(*stream, *offset, Some(*whence), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "ftruncate" => match evaluated_args { + [stream, size] => eval_ftruncate_result(*stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "fwrite" => match evaluated_args { + [stream, data] => eval_fwrite_result(*stream, *data, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "getcwd" => match evaluated_args { [] => eval_getcwd_result(values), _ => Err(EvalStatus::RuntimeFatal), @@ -300,6 +333,42 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [filename] => eval_stat_array_result(name, *filename, context, values), _ => Err(EvalStatus::RuntimeFatal), }, + "stream_copy_to_stream" => match evaluated_args { + [from, to] => eval_stream_copy_to_stream_result(*from, *to, None, None, context, values), + [from, to, length] => { + eval_stream_copy_to_stream_result(*from, *to, Some(*length), None, context, values) + } + [from, to, length, offset] => eval_stream_copy_to_stream_result( + *from, + *to, + Some(*length), + Some(*offset), + context, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + }, + "stream_get_contents" => match evaluated_args { + [stream] => eval_stream_get_contents_result(*stream, None, None, context, values), + [stream, length] => { + eval_stream_get_contents_result(*stream, Some(*length), None, context, values) + } + [stream, length, offset] => eval_stream_get_contents_result( + *stream, + Some(*length), + Some(*offset), + context, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + }, + "stream_get_line" => match evaluated_args { + [stream, length] => eval_stream_get_line_result(*stream, *length, None, context, values), + [stream, length, ending] => { + eval_stream_get_line_result(*stream, *length, Some(*ending), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, "stream_resolve_include_path" => match evaluated_args { [filename] => eval_stream_resolve_include_path_result(*filename, values), _ => Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_copy_to_stream.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_copy_to_stream.rs new file mode 100644 index 0000000000..541594f374 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_copy_to_stream.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_copy_to_stream`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream copy helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_copy_to_stream", + area: Filesystem, + params: [ + from, + to, + length = EvalBuiltinDefaultValue::Null, + offset = EvalBuiltinDefaultValue::Int(-1) + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_contents.rs new file mode 100644 index 0000000000..ca7cfec81c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_contents.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the bounded stream read helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_get_contents", + area: Filesystem, + params: [ + stream, + length = EvalBuiltinDefaultValue::Null, + offset = EvalBuiltinDefaultValue::Int(-1) + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_line.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_line.rs new file mode 100644 index 0000000000..be765332ab --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_line.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_line`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the delimiter-aware stream line helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_get_line", + area: Filesystem, + params: [stream, length, ending = EvalBuiltinDefaultValue::String("")], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 87229dcb77..2594b083bd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -193,11 +193,7 @@ pub(in crate::interpreter) fn eval_builtin_param_names( Some(&["hostname", "port", "error_code", "error_message", "timeout"]) } "flock" => Some(&["stream", "operation", "would_block"]), - "fread" => Some(&["stream", "length"]), "fscanf" => Some(&["stream", "format", "vars"]), - "fseek" => Some(&["stream", "offset", "whence"]), - "ftruncate" => Some(&["stream", "size"]), - "fwrite" => Some(&["stream", "data"]), "function_exists" => Some(&["function"]), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), "gethostbyaddr" => Some(&["ip"]), @@ -261,7 +257,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_bucket_make_writeable" => Some(&["brigade"]), "stream_bucket_new" => Some(&["stream", "buffer"]), "stream_bucket_append" | "stream_bucket_prepend" => Some(&["brigade", "bucket"]), - "stream_copy_to_stream" => Some(&["from", "to", "length", "offset"]), "stream_context_create" => Some(&["options", "params"]), "stream_context_get_default" => Some(&["options"]), "stream_context_get_options" | "stream_context_get_params" => Some(&["context"]), @@ -275,8 +270,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( Some(&["stream", "filtername", "read_write", "params"]) } "stream_filter_remove" => Some(&["stream_filter"]), - "stream_get_contents" => Some(&["stream", "length", "offset"]), - "stream_get_line" => Some(&["stream", "length", "ending"]), "stream_select" => Some(&["read", "write", "except", "seconds", "microseconds"]), "stream_socket_server" | "stream_socket_client" => Some(&["address"]), "stream_socket_accept" => Some(&["socket", "timeout", "peer_name"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 98fe946766..6006dc6903 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -93,12 +93,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( )?; values.bool_value(success)? } - "fread" => { - let [stream, length] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_fread_result(*stream, *length, context, values)? - } "fsockopen" | "pfsockopen" => { if !(2..=5).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); @@ -112,25 +106,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } eval_fscanf_result(evaluated_args[0], evaluated_args[1], context, values)? } - "fseek" => match evaluated_args { - [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values)?, - [stream, offset, whence] => { - eval_fseek_result(*stream, *offset, Some(*whence), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "ftruncate" => { - let [stream, size] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ftruncate_result(*stream, *size, context, values)? - } - "fwrite" => { - let [stream, data] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_fwrite_result(*stream, *data, context, values)? - } "readline" => { if evaluated_args.len() > 1 { return Err(EvalStatus::RuntimeFatal); @@ -138,23 +113,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let prompt = evaluated_args.first().copied(); eval_readline_result(prompt, values)? } - "stream_copy_to_stream" => match evaluated_args { - [from, to] => { - eval_stream_copy_to_stream_result(*from, *to, None, None, context, values)? - } - [from, to, length] => { - eval_stream_copy_to_stream_result(*from, *to, Some(*length), None, context, values)? - } - [from, to, length, offset] => eval_stream_copy_to_stream_result( - *from, - *to, - Some(*length), - Some(*offset), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "stream_context_create" => match evaluated_args { [] => eval_stream_context_create_result(None, context, values)?, [options] => eval_stream_context_create_result(Some(*options), context, values)?, @@ -336,29 +294,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_stream_socket_pair_result(context, values)? } - "stream_get_contents" => match evaluated_args { - [stream] => eval_stream_get_contents_result(*stream, None, None, context, values)?, - [stream, length] => { - eval_stream_get_contents_result(*stream, Some(*length), None, context, values)? - } - [stream, length, offset] => eval_stream_get_contents_result( - *stream, - Some(*length), - Some(*offset), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stream_get_line" => match evaluated_args { - [stream, length] => { - eval_stream_get_line_result(*stream, *length, None, context, values)? - } - [stream, length, ending] => { - eval_stream_get_line_result(*stream, *length, Some(*ending), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, "vfprintf" => { let [stream, format, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 19087a4f5c..5e653e282f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -230,6 +230,10 @@ mod tests { "filetype", "floatval", "fnmatch", + "fread", + "fseek", + "ftruncate", + "fwrite", "getdate", "getcwd", "gettype", @@ -308,7 +312,10 @@ mod tests { "scandir", "sleep", "stat", + "stream_copy_to_stream", + "stream_get_contents", "stream_get_filters", + "stream_get_line", "stream_get_transports", "stream_get_wrappers", "stream_is_local", @@ -648,6 +655,58 @@ mod tests { eval_declared_builtin_param_names("tmpfile"), Some([].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("fread"), + Some(["stream", "length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fwrite"), + Some(["stream", "data"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fseek"), + Some(["stream", "offset", "whence"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("fseek", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("ftruncate"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_copy_to_stream"), + Some(["from", "to", "length", "offset"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_copy_to_stream", 2), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_copy_to_stream", 3), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_contents"), + Some(["stream", "length", "offset"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_contents", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_contents", 2), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_line"), + Some(["stream", "length", "ending"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_line", 2), + Some(EvalBuiltinDefaultValue::String("")) + ); assert_eq!( eval_declared_builtin_param_names("stream_get_wrappers"), Some([].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 5cf869334b..363ad1e4ac 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -68,16 +68,12 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "fpassthru", "fprintf", "fputcsv", - "fread", "fscanf", - "fseek", "fsockopen", "fstat", "fsync", "ftell", - "ftruncate", "function_exists", - "fwrite", "get_called_class", "get_class", "get_class_methods", @@ -186,13 +182,10 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_context_set_default", "stream_context_set_option", "stream_context_set_params", - "stream_copy_to_stream", "stream_filter_append", "stream_filter_prepend", "stream_filter_register", "stream_filter_remove", - "stream_get_contents", - "stream_get_line", "stream_get_meta_data", "stream_select", "stream_socket_accept", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 1c4b661bea..1f2db4d270 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -109,11 +109,9 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "print_r" => optional(params, 1), "var_dump" => variadic(params, &[]), - "fopen" | "fseek" | "fputcsv" => optional(params, 2), + "fopen" | "fputcsv" => optional(params, 2), "flock" => optional_by_ref(params, 2, &["would_block"]), "fgetcsv" => optional(params, 1), - "stream_get_contents" => optional(params, 1), - "stream_copy_to_stream" => optional(params, 2), "stream_socket_accept" => optional_by_ref(params, 1, &["peer_name"]), "fsockopen" | "pfsockopen" => { optional_by_ref(params, 2, &["error_code", "error_message"]) @@ -121,8 +119,9 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "stream_wrapper_register" | "stream_socket_enable_crypto" => optional(params, 2), "stream_context_create" | "stream_context_get_default" => optional(params, 0), "stream_context_set_option" => optional(params, 2), - "stream_get_line" | "stream_socket_sendto" - | "stream_filter_append" | "stream_filter_prepend" => optional(params, 2), + "stream_socket_sendto" | "stream_filter_append" | "stream_filter_prepend" => { + optional(params, 2) + } "stream_select" => optional_by_ref(params, 4, &["read", "write", "except"]), "stream_socket_recvfrom" => optional_by_ref(params, 2, &["address"]), @@ -188,15 +187,10 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("fopen", 2) => Bool(false), ("fopen", 3) => Null, ("flock", 2) => Null, - ("fseek", 2) => Int(0), ("fgetcsv", 1) => Null, ("fgetcsv", 2) => String(","), ("fputcsv", 2) => String(","), ("fputcsv", 3) => String("\""), - ("stream_get_contents", 1) => Null, - ("stream_get_contents", 2) => Int(-1), - ("stream_copy_to_stream", 2) => Null, - ("stream_copy_to_stream", 3) => Int(-1), ("stream_socket_accept", 1 | 2) => Null, ("fsockopen" | "pfsockopen", 2 | 3 | 4) => Null, ("stream_wrapper_register", 2) => Int(0), @@ -204,7 +198,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("stream_context_create", 0 | 1) => Null, ("stream_context_get_default", 0) => Null, ("stream_context_set_option", 2 | 3) => Null, - ("stream_get_line", 2) => String(""), ("stream_select", 4) => Int(0), ("stream_socket_sendto", 2) => Int(0), ("stream_socket_sendto", 3) => String(""), diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index e45e349e71..b398744079 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1597,12 +1597,8 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "fopen" => eval_builtin_fopen(args, context, scope, values), "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), "fprintf" => eval_builtin_fprintf(args, context, scope, values), - "fread" => eval_builtin_fread(args, context, scope, values), "fsockopen" | "pfsockopen" => eval_builtin_fsockopen(args, context, scope, values), "fscanf" => eval_builtin_fscanf(args, context, scope, values), - "fseek" => eval_builtin_fseek(args, context, scope, values), - "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), - "fwrite" => eval_builtin_fwrite(args, context, scope, values), "function_exists" | "is_callable" => { eval_builtin_function_probe(name, args, context, scope, values) } @@ -1675,7 +1671,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "sscanf" => eval_builtin_sscanf(args, context, scope, values), "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), - "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), "stream_context_get_default" => { eval_builtin_stream_context_get_default(args, context, scope, values) @@ -1730,8 +1725,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_stream_socket_recvfrom(args, context, scope, values) } "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), - "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), - "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), "long2ip" => eval_builtin_long2ip(args, context, scope, values), "unset" => eval_builtin_unset(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index f4f3b418bb..806b56efbe 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -161,6 +161,10 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "floor", "fmod", "fnmatch", + "fread", + "fseek", + "ftruncate", + "fwrite", "getdate", "getcwd", "gettype", @@ -259,7 +263,10 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "sleep", "sqrt", "stat", + "stream_copy_to_stream", + "stream_get_contents", "stream_get_filters", + "stream_get_line", "stream_get_transports", "stream_get_wrappers", "stream_is_local", From c83964f129ffc240f85558628adc12924c827796 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 02:32:12 +0200 Subject: [PATCH 1078/1208] refactor(magician): migrate unary stream builtins --- .../filesystem/declarations/fclose.rs | 16 ++++++++++ .../filesystem/declarations/fdatasync.rs | 16 ++++++++++ .../builtins/filesystem/declarations/feof.rs | 16 ++++++++++ .../filesystem/declarations/fflush.rs | 16 ++++++++++ .../builtins/filesystem/declarations/fgetc.rs | 16 ++++++++++ .../builtins/filesystem/declarations/fgets.rs | 16 ++++++++++ .../filesystem/declarations/fpassthru.rs | 16 ++++++++++ .../builtins/filesystem/declarations/fstat.rs | 16 ++++++++++ .../builtins/filesystem/declarations/fsync.rs | 16 ++++++++++ .../builtins/filesystem/declarations/ftell.rs | 16 ++++++++++ .../builtins/filesystem/declarations/mod.rs | 23 +++++++++++++ .../filesystem/declarations/rewind.rs | 16 ++++++++++ .../declarations/stream_get_meta_data.rs | 16 ++++++++++ .../interpreter/builtins/registry/binding.rs | 12 ------- .../builtins/registry/dispatch/filesystem.rs | 17 ---------- .../src/interpreter/builtins/registry/mod.rs | 32 +++++++++++++++++++ .../interpreter/builtins/registry/names.rs | 12 ------- .../src/interpreter/expressions.rs | 12 ------- tests/builtin_parity_tests.rs | 12 +++++++ 19 files changed, 259 insertions(+), 53 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fclose.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fdatasync.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/feof.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fflush.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetc.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgets.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fpassthru.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fstat.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsync.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftell.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewind.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_meta_data.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fclose.rs new file mode 100644 index 0000000000..be62a0edaf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fclose.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fclose`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "fclose", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fdatasync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fdatasync.rs new file mode 100644 index 0000000000..659807e793 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fdatasync.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fdatasync`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "fdatasync", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/feof.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/feof.rs new file mode 100644 index 0000000000..ec37ac1feb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/feof.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `feof`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "feof", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fflush.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fflush.rs new file mode 100644 index 0000000000..f660f2767b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fflush.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fflush`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "fflush", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetc.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetc.rs new file mode 100644 index 0000000000..e3442c9cf3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetc.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fgetc`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "fgetc", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgets.rs new file mode 100644 index 0000000000..03c8290958 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgets.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fgets`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "fgets", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fpassthru.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fpassthru.rs new file mode 100644 index 0000000000..79441e27b7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fpassthru.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fpassthru`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "fpassthru", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fstat.rs new file mode 100644 index 0000000000..7f32cf7503 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fstat.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fstat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "fstat", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsync.rs new file mode 100644 index 0000000000..c3ac875eaf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsync.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `fsync`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "fsync", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftell.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftell.rs new file mode 100644 index 0000000000..588b34c798 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftell.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ftell`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "ftell", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index fc637f0397..687da77ff1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -36,10 +36,20 @@ mod fileowner; mod fileperms; mod filesize; mod filetype; +mod fclose; +mod fdatasync; +mod feof; +mod fflush; +mod fgetc; +mod fgets; mod fnmatch; +mod fpassthru; mod fread; mod fseek; +mod fstat; mod ftruncate; +mod fsync; +mod ftell; mod fwrite; mod getcwd; mod glob; @@ -67,6 +77,7 @@ mod realpath; mod realpath_cache_get; mod realpath_cache_size; mod rename; +mod rewind; mod rewinddir; mod rmdir; mod scandir; @@ -74,6 +85,7 @@ mod stat; mod stream_copy_to_stream; mod stream_get_contents; mod stream_get_line; +mod stream_get_meta_data; mod stream_isatty; mod stream_resolve_include_path; mod stream_set_blocking; @@ -125,6 +137,10 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "filesize" => eval_builtin_filesize(args, context, scope, values), "filetype" => eval_builtin_filetype(args, context, scope, values), + "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" + | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { + eval_builtin_unary_stream(name, args, context, scope, values) + } "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), "fread" => eval_builtin_fread(args, context, scope, values), "fseek" => eval_builtin_fseek(args, context, scope, values), @@ -246,6 +262,13 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( [filename] => eval_filetype_result(*filename, context, values), _ => Err(EvalStatus::RuntimeFatal), }, + "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" + | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { + match evaluated_args { + [stream] => eval_unary_stream_result(name, *stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } + } "fnmatch" => match evaluated_args { [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values), [pattern, filename, flags] => { diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewind.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewind.rs new file mode 100644 index 0000000000..6c9c2aff2b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewind.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `rewind`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "rewind", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_meta_data.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_meta_data.rs new file mode 100644 index 0000000000..a546d85b41 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_meta_data.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_meta_data`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the unary stream helper. + +eval_builtin! { + name: "stream_get_meta_data", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 2594b083bd..c2d7795875 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -173,18 +173,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "die" | "exit" => Some(&["status"]), "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), "explode" => Some(&["separator", "string", "limit"]), - "fclose" - | "fgetc" - | "fgets" - | "feof" - | "fflush" - | "fpassthru" - | "fsync" - | "fdatasync" - | "ftell" - | "rewind" - | "fstat" - | "stream_get_meta_data" => Some(&["stream"]), "fgetcsv" => Some(&["stream", "length", "separator"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs index 6006dc6903..eac4dbc1b9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs @@ -19,23 +19,6 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "fclose" - | "fgetc" - | "fgets" - | "feof" - | "fflush" - | "fpassthru" - | "fsync" - | "fdatasync" - | "ftell" - | "rewind" - | "fstat" - | "stream_get_meta_data" => { - let [stream] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_unary_stream_result(name, *stream, context, values)? - } "fgetcsv" => match evaluated_args { [stream] => eval_fgetcsv_result(*stream, None, None, context, values)?, [stream, length] => { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 5e653e282f..df3bb599c6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -228,11 +228,21 @@ mod tests { "fileperms", "filesize", "filetype", + "fclose", + "fdatasync", + "feof", + "fflush", + "fgetc", + "fgets", "floatval", "fnmatch", + "fpassthru", "fread", "fseek", + "fstat", "ftruncate", + "fsync", + "ftell", "fwrite", "getdate", "getcwd", @@ -307,6 +317,7 @@ mod tests { "realpath_cache_get", "realpath_cache_size", "rename", + "rewind", "rewinddir", "rmdir", "scandir", @@ -316,6 +327,7 @@ mod tests { "stream_get_contents", "stream_get_filters", "stream_get_line", + "stream_get_meta_data", "stream_get_transports", "stream_get_wrappers", "stream_is_local", @@ -655,6 +667,26 @@ mod tests { eval_declared_builtin_param_names("tmpfile"), Some([].as_slice()) ); + for name in [ + "fclose", + "fgetc", + "fgets", + "feof", + "fflush", + "fpassthru", + "fsync", + "fdatasync", + "ftell", + "rewind", + "fstat", + "stream_get_meta_data", + ] { + assert_eq!( + eval_declared_builtin_param_names(name), + Some(["stream"].as_slice()), + "{name} should declare one stream parameter" + ); + } assert_eq!( eval_declared_builtin_param_names("fread"), Some(["stream", "length"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 363ad1e4ac..d8db2febde 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -56,23 +56,13 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "exec", "exit", "explode", - "fclose", - "fdatasync", - "feof", - "fflush", - "fgetc", "fgetcsv", - "fgets", "flock", "fopen", - "fpassthru", "fprintf", "fputcsv", "fscanf", "fsockopen", - "fstat", - "fsync", - "ftell", "function_exists", "get_called_class", "get_class", @@ -153,7 +143,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "rand", "random_int", "readline", - "rewind", "rsort", "settype", "sha1", @@ -186,7 +175,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_filter_prepend", "stream_filter_register", "stream_filter_remove", - "stream_get_meta_data", "stream_select", "stream_socket_accept", "stream_socket_client", diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index b398744079..0bd3ac29f4 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1581,18 +1581,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "eval" => eval_nested_eval(args, context, scope, values), "explode" => eval_builtin_explode(args, context, scope, values), - "fclose" - | "fgetc" - | "fgets" - | "feof" - | "fflush" - | "fpassthru" - | "fsync" - | "fdatasync" - | "ftell" - | "rewind" - | "fstat" - | "stream_get_meta_data" => eval_builtin_unary_stream(name, args, context, scope, values), "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), "fopen" => eval_builtin_fopen(args, context, scope, values), "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 806b56efbe..353299f269 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -157,13 +157,23 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "fileperms", "filesize", "filetype", + "fclose", + "fdatasync", + "feof", + "fflush", + "fgetc", + "fgets", "floatval", "floor", "fmod", "fnmatch", + "fpassthru", "fread", "fseek", + "fstat", "ftruncate", + "fsync", + "ftell", "fwrite", "getdate", "getcwd", @@ -255,6 +265,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "rename", "round", "rtrim", + "rewind", "rewinddir", "rmdir", "scandir", @@ -267,6 +278,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "stream_get_contents", "stream_get_filters", "stream_get_line", + "stream_get_meta_data", "stream_get_transports", "stream_get_wrappers", "stream_is_local", From d91f4e60cadf72768ae18966836d5af89ef5b4a5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 02:48:06 +0200 Subject: [PATCH 1079/1208] refactor(magician): migrate hash and gzip builtins --- .../src/interpreter/builtins/hooks/direct.rs | 33 ++++++++-- .../src/interpreter/builtins/hooks/hash.rs | 64 +++++++++++++++++++ .../src/interpreter/builtins/hooks/mod.rs | 5 +- .../src/interpreter/builtins/hooks/values.rs | 34 ++++++---- .../interpreter/builtins/registry/binding.rs | 11 ---- .../builtins/registry/dispatch/strings.rs | 47 ++------------ .../src/interpreter/builtins/registry/mod.rs | 58 +++++++++++++++++ .../interpreter/builtins/registry/names.rs | 14 ---- .../builtins/registry/signature.rs | 15 ----- .../strings/declarations/gzcompress.rs | 18 ++++++ .../strings/declarations/gzdeflate.rs | 18 ++++++ .../strings/declarations/gzinflate.rs | 18 ++++++ .../strings/declarations/gzuncompress.rs | 18 ++++++ .../builtins/strings/declarations/hash.rs | 18 ++++++ .../strings/declarations/hash_algos.rs | 16 +++++ .../strings/declarations/hash_copy.rs | 16 +++++ .../strings/declarations/hash_file.rs | 18 ++++++ .../strings/declarations/hash_final.rs | 18 ++++++ .../strings/declarations/hash_hmac.rs | 18 ++++++ .../strings/declarations/hash_init.rs | 23 +++++++ .../strings/declarations/hash_update.rs | 16 +++++ .../builtins/strings/declarations/md5.rs | 18 ++++++ .../builtins/strings/declarations/mod.rs | 14 ++++ .../builtins/strings/declarations/sha1.rs | 18 ++++++ .../src/interpreter/expressions.rs | 11 ---- tests/builtin_parity_tests.rs | 14 ++++ 26 files changed, 459 insertions(+), 112 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/hash.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzcompress.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzdeflate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzinflate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzuncompress.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_algos.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_copy.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_file.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_final.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_hmac.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_init.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_update.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/md5.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/sha1.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index f2c7868df5..cd1cce0a43 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -13,12 +13,15 @@ use super::super::super::{ eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, eval_builtin_ceil, eval_builtin_chr, eval_builtin_clamp, eval_builtin_count, eval_builtin_crc32, eval_builtin_ctype, eval_builtin_float_binary, eval_builtin_float_pair, - eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, eval_builtin_hex2bin, - eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, eval_builtin_number_format, - eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, eval_builtin_round, eval_builtin_slashes, - eval_builtin_sqrt, eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_type_predicate, - eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, - EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, + eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, eval_builtin_gzip, + eval_builtin_hash_algos, eval_builtin_hash_copy, eval_builtin_hash_final, + eval_builtin_hash_init, eval_builtin_hash_one_shot, eval_builtin_hash_update, + eval_builtin_hex2bin, eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, + eval_builtin_number_format, eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, + eval_builtin_round, eval_builtin_slashes, eval_builtin_sqrt, eval_builtin_str_repeat, + eval_builtin_strlen, eval_builtin_type_predicate, eval_builtin_url_decode, + eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, + RuntimeCellHandle, RuntimeValueOps, }; use super::super::{ eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_flip, @@ -94,8 +97,16 @@ pub(in crate::interpreter) enum EvalDirectHook { Gettype, /// Dispatches `grapheme_strrev(...)`. GraphemeStrrev, + /// Dispatches gzip/zlib string builtins. + Gzip, + /// Dispatches `hash_algos()`. + HashAlgos, + /// Dispatches incremental hash-context builtins. + HashContext, /// Dispatches `hash_equals(...)`. HashEquals, + /// Dispatches one-shot hash digest builtins. + HashOneShot, /// Dispatches `hex2bin(...)`. Hex2Bin, /// Dispatches HTML entity encode/decode builtins. @@ -213,7 +224,17 @@ impl EvalDirectHook { Self::Floor => eval_builtin_floor(args, context, scope, values), Self::Gettype => eval_builtin_gettype(args, context, scope, values), Self::GraphemeStrrev => eval_builtin_grapheme_strrev(args, context, scope, values), + Self::Gzip => eval_builtin_gzip(name, args, context, scope, values), + Self::HashAlgos => eval_builtin_hash_algos(args, values), + Self::HashContext => match name { + "hash_copy" => eval_builtin_hash_copy(args, context, scope, values), + "hash_final" => eval_builtin_hash_final(args, context, scope, values), + "hash_init" => eval_builtin_hash_init(args, context, scope, values), + "hash_update" => eval_builtin_hash_update(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::HashEquals => eval_builtin_hash_equals(args, context, scope, values), + Self::HashOneShot => eval_builtin_hash_one_shot(name, args, context, scope, values), Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), Self::HtmlEntity => eval_builtin_html_entity(name, args, context, scope, values), Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/hash.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/hash.rs new file mode 100644 index 0000000000..8a3fcf1a59 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/hash.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Focused dispatch helpers for declarative hash builtin hooks. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::values`. +//! +//! Key details: +//! - These helpers keep the generic evaluated-argument hook table below the +//! ordinary file-size limit while preserving the existing hash behavior. + +use super::super::super::{ElephcEvalContext, EvalStatus, RuntimeCellHandle, RuntimeValueOps}; +use super::super::{ + eval_hash_algos_result, eval_hash_copy_result, eval_hash_final_result, eval_hash_init_result, + eval_hash_update_result, +}; + +/// Dispatches evaluated `hash_algos()` calls. +pub(super) fn eval_hash_algos_values( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + +/// Dispatches evaluated incremental hash-context builtin calls. +pub(super) fn eval_hash_context_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "hash_copy" => { + let [hash_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_copy_result(*hash_context, context, values) + } + "hash_final" => match evaluated_args { + [hash_context] => eval_hash_final_result(*hash_context, false, context, values), + [hash_context, binary] => { + let binary = values.truthy(*binary)?; + eval_hash_final_result(*hash_context, binary, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "hash_init" => { + let [algo] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_init_result(*algo, context, values) + } + "hash_update" => { + let [hash_context, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_update_result(*hash_context, *data, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs index 504650ad17..caeeceb850 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs @@ -5,10 +5,11 @@ //! - `crate::interpreter::builtins::spec` re-exports used by `eval_builtin!`. //! //! Key details: -//! - Direct expression dispatch and already-evaluated argument dispatch are kept -//! in separate files so each hook table can grow independently. +//! - Direct expression dispatch, already-evaluated argument dispatch, and +//! focused hook helpers stay split so ordinary files remain small. mod direct; +mod hash; mod values; pub(in crate::interpreter) use direct::EvalDirectHook; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index f877235441..2bf2929b45 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -21,18 +21,18 @@ use super::super::{ eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, eval_chr_result, eval_clamp_result, eval_crc32_result, eval_ctype_result, eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, - eval_float_unary_result, eval_gettype_result, eval_grapheme_strrev_result, - eval_hash_equals_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, - eval_json_values_result, eval_log_result, eval_min_max_result, eval_nl2br_result, - eval_number_format_result, eval_range_result, eval_regex_values_result, eval_slashes_result, - eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, - eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, - eval_string_compare_result, eval_string_position_result, eval_string_search_result, - eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, - eval_trim_like_result, - eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, - eval_wordwrap_result, + eval_float_unary_result, eval_gettype_result, eval_grapheme_strrev_result, eval_gzip_result, + eval_hash_equals_result, eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, + eval_intdiv_result, eval_json_values_result, eval_log_result, eval_min_max_result, + eval_nl2br_result, eval_number_format_result, eval_range_result, eval_regex_values_result, + eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, + eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, + eval_string_case_result, eval_string_compare_result, eval_string_position_result, + eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, + eval_time_values_result, eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, + eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; +use super::hash::{eval_hash_algos_values, eval_hash_context_values}; /// Evaluated-argument dispatch hooks for migrated builtins. #[derive(Clone, Copy)] @@ -93,8 +93,16 @@ pub(in crate::interpreter) enum EvalValuesHook { Gettype, /// Dispatches `grapheme_strrev(...)`. GraphemeStrrev, + /// Dispatches gzip/zlib string builtins. + Gzip, + /// Dispatches `hash_algos()`. + HashAlgos, + /// Dispatches incremental hash-context builtins. + HashContext, /// Dispatches `hash_equals(...)`. HashEquals, + /// Dispatches one-shot hash digest builtins. + HashOneShot, /// Dispatches `hex2bin(...)`. Hex2Bin, /// Dispatches HTML entity encode/decode builtins. @@ -246,7 +254,11 @@ impl EvalValuesHook { Self::Floor => one_arg(evaluated_args, values, |value, values| values.floor(value)), Self::Gettype => one_arg(evaluated_args, values, eval_gettype_result), Self::GraphemeStrrev => one_arg(evaluated_args, values, eval_grapheme_strrev_result), + Self::Gzip => eval_gzip_result(name, evaluated_args, values), + Self::HashAlgos => eval_hash_algos_values(evaluated_args, values), + Self::HashContext => eval_hash_context_values(name, evaluated_args, context, values), Self::HashEquals => two_args(evaluated_args, values, eval_hash_equals_result), + Self::HashOneShot => eval_hash_one_shot_result(name, evaluated_args, values), Self::Hex2Bin => one_arg(evaluated_args, values, eval_hex2bin_result), Self::HtmlEntity => one_arg(evaluated_args, values, |value, values| { eval_html_entity_result(name, value, values) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index c2d7795875..ce657db563 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -193,18 +193,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "getservbyport" => Some(&["port", "protocol"]), "get_resource_id" | "get_resource_type" => Some(&["resource"]), "getenv" => Some(&["name"]), - "hash" => Some(&["algo", "data", "binary"]), - "hash_algos" => Some(&[]), - "hash_copy" => Some(&["context"]), - "hash_file" => Some(&["algo", "filename", "binary"]), - "hash_final" => Some(&["context", "binary"]), - "hash_hmac" => Some(&["algo", "data", "key", "binary"]), - "hash_init" => Some(&["algo", "flags", "key"]), - "hash_update" => Some(&["context", "data"]), "header" => Some(&["header", "replace", "response_code"]), "http_response_code" => Some(&["response_code"]), - "gzcompress" | "gzdeflate" => Some(&["data", "level"]), - "gzinflate" | "gzuncompress" => Some(&["data", "max_length"]), "implode" => Some(&["separator", "array"]), "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), @@ -213,7 +203,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "iterator_to_array" => Some(&["iterator", "preserve_keys"]), "ip2long" => Some(&["ip"]), "isset" | "unset" => Some(&["var", "vars"]), - "md5" | "sha1" => Some(&["string", "binary"]), "php_uname" => Some(&["mode"]), "phpversion" => Some(&[]), "ptr" => Some(&["value"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs index d65851f2e9..7343c5a2b0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs @@ -1,12 +1,13 @@ //! Purpose: -//! Dispatches already evaluated string, hash, encoding, and ctype builtins by dynamic callable name. +//! Dispatches remaining already evaluated string builtins by dynamic callable name. //! //! Called from: //! - `crate::interpreter::builtins::registry::dispatch`. //! //! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. +//! - Gzip and hash builtins have migrated to declarative specs. +//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher +//! can continue probing other builtin families. use super::super::super::super::*; use super::super::super::*; @@ -15,13 +16,10 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_strings_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { - eval_gzip_result(name, evaluated_args, values)? - } "explode" => { let [separator, string] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -34,41 +32,6 @@ pub(in crate::interpreter) fn eval_strings_builtin_with_values( }; eval_implode_result(*separator, *array, values)? } - "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { - eval_hash_one_shot_result(name, evaluated_args, values)? - } - "hash_algos" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values)? - } - "hash_copy" => { - let [hash_context] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_copy_result(*hash_context, context, values)? - } - "hash_final" => match evaluated_args { - [hash_context] => eval_hash_final_result(*hash_context, false, context, values)?, - [hash_context, binary] => { - let binary = values.truthy(*binary)?; - eval_hash_final_result(*hash_context, binary, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "hash_init" => { - let [algo] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_init_result(*algo, context, values)? - } - "hash_update" => { - let [hash_context, data] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_update_result(*hash_context, *data, context, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index df3bb599c6..008ddd6d66 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -251,7 +251,19 @@ mod tests { "gmmktime", "glob", "grapheme_strrev", + "gzcompress", + "gzdeflate", + "gzinflate", + "gzuncompress", + "hash", + "hash_algos", + "hash_copy", "hash_equals", + "hash_file", + "hash_final", + "hash_hmac", + "hash_init", + "hash_update", "hex2bin", "htmlspecialchars", "hrtime", @@ -294,6 +306,7 @@ mod tests { "log", "lstat", "microtime", + "md5", "min", "mkdir", "mktime", @@ -321,6 +334,7 @@ mod tests { "rewinddir", "rmdir", "scandir", + "sha1", "sleep", "stat", "stream_copy_to_stream", @@ -751,6 +765,50 @@ mod tests { eval_declared_builtin_param_names("stream_get_filters"), Some([].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("gzcompress"), + Some(["data", "level"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("gzcompress", 1), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_default_value("gzinflate", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("hash"), + Some(["algo", "data", "binary"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hash", 2), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("hash_algos"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("hash_init"), + Some(["algo", "flags", "key"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hash_init", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_default_value("hash_init", 2), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_param_names("md5"), + Some(["string", "binary"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("sha1", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); assert_eq!( eval_declared_builtin_param_names("stream_is_local"), Some(["stream"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index d8db2febde..9de92a88ce 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -83,18 +83,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "getprotobynumber", "getservbyname", "getservbyport", - "gzcompress", - "gzdeflate", - "gzinflate", - "gzuncompress", - "hash", - "hash_algos", - "hash_copy", - "hash_file", - "hash_final", - "hash_hmac", - "hash_init", - "hash_update", "header", "http_response_code", "implode", @@ -112,7 +100,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "krsort", "ksort", "long2ip", - "md5", "method_exists", "mt_rand", "natcasesort", @@ -145,7 +132,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "readline", "rsort", "settype", - "sha1", "shell_exec", "shuffle", "sort", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 1f2db4d270..5b12f092a9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -63,8 +63,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( let params = eval_builtin_param_names(name)?; Some(match name { - "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => optional(params, 1), - "isset" | "unset" => variadic(params, &[]), "settype" => fixed_by_ref(params, &["var"]), @@ -86,11 +84,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "sprintf" | "printf" | "sscanf" => variadic(params, &[]), "fprintf" | "fscanf" => variadic(params, &[]), - "hash" | "hash_file" => optional(params, 2), - "hash_hmac" => optional(params, 3), - "hash_init" => optional(params, 1), - "hash_final" | "md5" | "sha1" => optional(params, 1), - "array_pop" | "array_shift" => fixed_by_ref(params, &["array"]), "sort" | "rsort" | "shuffle" | "natsort" | "natcasesort" | "asort" | "arsort" | "ksort" | "krsort" => fixed_by_ref(params, &["array"]), @@ -144,9 +137,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( use EvalBuiltinDefaultValue::*; Some(match (name, param_index) { - ("gzcompress" | "gzdeflate", 1) => Int(-1), - ("gzinflate" | "gzuncompress", 1) => Int(0), - ("class_alias", 2) => Bool(true), ( "class_exists" | "interface_exists" | "trait_exists" | "enum_exists" @@ -171,11 +161,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("explode", 2) => Int(i64::MAX), ("implode", 0) => Null, - ("hash" | "hash_file", 2) => Bool(false), - ("hash_hmac", 3) => Bool(false), - ("hash_init", 1) => Int(0), - ("hash_init", 2) => String(""), - ("hash_final" | "md5" | "sha1", 1) => Bool(false), ("array_splice", 2) => Null, ("array_splice", 3) => EmptyArray, ("array_filter", 1) => Null, diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzcompress.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzcompress.rs new file mode 100644 index 0000000000..8aaa4f2b69 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzcompress.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `gzcompress`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the gzip/zlib hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzcompress", + area: String, + params: [data, level = EvalBuiltinDefaultValue::Int(-1)], + direct: Gzip, + values: Gzip, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzdeflate.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzdeflate.rs new file mode 100644 index 0000000000..d7b218bdc2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzdeflate.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `gzdeflate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the gzip/zlib hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzdeflate", + area: String, + params: [data, level = EvalBuiltinDefaultValue::Int(-1)], + direct: Gzip, + values: Gzip, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzinflate.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzinflate.rs new file mode 100644 index 0000000000..8418065191 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzinflate.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `gzinflate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the gzip/zlib hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzinflate", + area: String, + params: [data, max_length = EvalBuiltinDefaultValue::Int(0)], + direct: Gzip, + values: Gzip, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzuncompress.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzuncompress.rs new file mode 100644 index 0000000000..99f81a55a1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzuncompress.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `gzuncompress`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the gzip/zlib hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzuncompress", + area: String, + params: [data, max_length = EvalBuiltinDefaultValue::Int(0)], + direct: Gzip, + values: Gzip, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash.rs new file mode 100644 index 0000000000..dbc750a3e6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `hash`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the one-shot hash hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash", + area: String, + params: [algo, data, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_algos.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_algos.rs new file mode 100644 index 0000000000..866ad7f15a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_algos.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_algos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the static hash algorithm list helper. + +eval_builtin! { + name: "hash_algos", + area: String, + params: [], + direct: HashAlgos, + values: HashAlgos, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_copy.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_copy.rs new file mode 100644 index 0000000000..78cf1b5ec9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_copy.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_copy`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the incremental hash-context hook. + +eval_builtin! { + name: "hash_copy", + area: String, + params: [context], + direct: HashContext, + values: HashContext, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_file.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_file.rs new file mode 100644 index 0000000000..d3e22f743e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_file.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the one-shot hash hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_file", + area: String, + params: [algo, filename, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_final.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_final.rs new file mode 100644 index 0000000000..d0f94d54ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_final.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_final`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the incremental hash-context hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_final", + area: String, + params: [context, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashContext, + values: HashContext, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_hmac.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_hmac.rs new file mode 100644 index 0000000000..268e9b1a8a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_hmac.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_hmac`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the one-shot hash hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_hmac", + area: String, + params: [algo, data, key, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_init.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_init.rs new file mode 100644 index 0000000000..6c5a43e0e5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_init.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_init`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the incremental hash-context hook. +//! - Optional HMAC parameters remain metadata-only for current eval behavior. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_init", + area: String, + params: [ + algo, + flags = EvalBuiltinDefaultValue::Int(0), + key = EvalBuiltinDefaultValue::String(""), + ], + direct: HashContext, + values: HashContext, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_update.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_update.rs new file mode 100644 index 0000000000..fddcc9deb6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_update.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_update`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the incremental hash-context hook. + +eval_builtin! { + name: "hash_update", + area: String, + params: [context, data], + direct: HashContext, + values: HashContext, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/md5.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/md5.rs new file mode 100644 index 0000000000..1b7a3c2f8a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/md5.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `md5`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the one-shot hash hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "md5", + area: String, + params: [string, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs index 269464cae9..edfa7c13f3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs @@ -12,3 +12,17 @@ mod stream_get_transports; mod stream_get_wrappers; mod stream_is_local; mod stream_supports_lock; +mod gzcompress; +mod gzdeflate; +mod gzinflate; +mod gzuncompress; +mod hash; +mod hash_algos; +mod hash_copy; +mod hash_file; +mod hash_final; +mod hash_hmac; +mod hash_init; +mod hash_update; +mod md5; +mod sha1; diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/sha1.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/sha1.rs new file mode 100644 index 0000000000..69932f1bd7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/sha1.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `sha1`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the one-shot hash hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "sha1", + area: String, + params: [string, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 0bd3ac29f4..57a94c7c8b 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1607,18 +1607,7 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_resource_introspection(name, args, context, scope, values) } "getenv" => eval_builtin_getenv(args, context, scope, values), - "gzcompress" | "gzdeflate" | "gzinflate" | "gzuncompress" => { - eval_builtin_gzip(name, args, context, scope, values) - } - "hash" | "hash_file" | "hash_hmac" | "md5" | "sha1" => { - eval_builtin_hash_one_shot(name, args, context, scope, values) - } "header" => eval_builtin_header(args, context, scope, values), - "hash_algos" => eval_builtin_hash_algos(args, values), - "hash_copy" => eval_builtin_hash_copy(args, context, scope, values), - "hash_final" => eval_builtin_hash_final(args, context, scope, values), - "hash_init" => eval_builtin_hash_init(args, context, scope, values), - "hash_update" => eval_builtin_hash_update(args, context, scope, values), "implode" => eval_builtin_implode(args, context, scope, values), "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 353299f269..085c32da89 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -182,7 +182,19 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "gmmktime", "glob", "grapheme_strrev", + "gzcompress", + "gzdeflate", + "gzinflate", + "gzuncompress", + "hash", + "hash_algos", + "hash_copy", "hash_equals", + "hash_file", + "hash_final", + "hash_hmac", + "hash_init", + "hash_update", "hex2bin", "html_entity_decode", "htmlentities", @@ -235,6 +247,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "ltrim", "max", "microtime", + "md5", "min", "mkdir", "mktime", @@ -269,6 +282,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "rewinddir", "rmdir", "scandir", + "sha1", "sin", "sinh", "sleep", From 7e9e1ca89255dca7c3af52cd22ef51b6f13eaf86 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 03:02:40 +0200 Subject: [PATCH 1080/1208] refactor(magician): migrate split join string builtins --- .../src/interpreter/builtins/hooks/direct.rs | 25 +++++++----- .../src/interpreter/builtins/hooks/mod.rs | 1 + .../builtins/hooks/string_split_join.rs | 35 +++++++++++++++++ .../src/interpreter/builtins/hooks/values.rs | 4 ++ .../src/interpreter/builtins/macros.rs | 34 +++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 2 - .../builtins/registry/dispatch/mod.rs | 5 --- .../builtins/registry/dispatch/strings.rs | 38 ------------------- .../src/interpreter/builtins/registry/mod.rs | 29 ++++++++++++++ .../interpreter/builtins/registry/names.rs | 2 - .../builtins/registry/signature.rs | 5 --- .../src/interpreter/builtins/spec.rs | 5 +++ .../builtins/strings/declarations/explode.rs | 18 +++++++++ .../builtins/strings/declarations/implode.rs | 19 ++++++++++ .../builtins/strings/declarations/mod.rs | 2 + .../src/interpreter/expressions.rs | 2 - tests/builtin_parity_tests.rs | 5 +-- 17 files changed, 165 insertions(+), 66 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/string_split_join.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/explode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/implode.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index cd1cce0a43..9e6ac5b8e7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -12,16 +12,16 @@ use super::super::super::{ eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, eval_builtin_ceil, eval_builtin_chr, eval_builtin_clamp, eval_builtin_count, - eval_builtin_crc32, eval_builtin_ctype, eval_builtin_float_binary, eval_builtin_float_pair, - eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, eval_builtin_gzip, - eval_builtin_hash_algos, eval_builtin_hash_copy, eval_builtin_hash_final, + eval_builtin_crc32, eval_builtin_ctype, eval_builtin_explode, eval_builtin_float_binary, + eval_builtin_float_pair, eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, + eval_builtin_gzip, eval_builtin_hash_algos, eval_builtin_hash_copy, eval_builtin_hash_final, eval_builtin_hash_init, eval_builtin_hash_one_shot, eval_builtin_hash_update, - eval_builtin_hex2bin, eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, - eval_builtin_number_format, eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, - eval_builtin_round, eval_builtin_slashes, eval_builtin_sqrt, eval_builtin_str_repeat, - eval_builtin_strlen, eval_builtin_type_predicate, eval_builtin_url_decode, - eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, - RuntimeCellHandle, RuntimeValueOps, + eval_builtin_hex2bin, eval_builtin_implode, eval_builtin_intdiv, eval_builtin_log, + eval_builtin_min_max, eval_builtin_number_format, eval_builtin_ord, eval_builtin_pi, + eval_builtin_pow, eval_builtin_round, eval_builtin_slashes, eval_builtin_sqrt, + eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_type_predicate, + eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, + EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::super::{ eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_flip, @@ -145,6 +145,8 @@ pub(in crate::interpreter) enum EvalDirectHook { StringPosition, /// Dispatches string search predicate builtins. StringSearch, + /// Dispatches `explode(...)` and `implode(...)`. + StringSplitJoin, /// Dispatches stream boolean predicate builtins. StreamBoolPredicate, /// Dispatches stream introspection list builtins. @@ -256,6 +258,11 @@ impl EvalDirectHook { eval_builtin_string_position(name, args, context, scope, values) } Self::StringSearch => eval_builtin_string_search(name, args, context, scope, values), + Self::StringSplitJoin => match name { + "explode" => eval_builtin_explode(args, context, scope, values), + "implode" => eval_builtin_implode(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::StreamBoolPredicate => { eval_builtin_stream_bool_predicate(name, args, context, scope, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs index caeeceb850..a2621565de 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs @@ -10,6 +10,7 @@ mod direct; mod hash; +mod string_split_join; mod values; pub(in crate::interpreter) use direct::EvalDirectHook; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/string_split_join.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/string_split_join.rs new file mode 100644 index 0000000000..196739e902 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/string_split_join.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Focused dispatch helpers for declarative `explode` and `implode` hooks. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::values`. +//! +//! Key details: +//! - The eval implementation still requires the currently supported two-argument +//! runtime form even though signature metadata exposes PHP-compatible defaults. + +use super::super::super::{EvalStatus, RuntimeCellHandle, RuntimeValueOps}; +use super::super::{eval_explode_result, eval_implode_result}; + +/// Dispatches evaluated `explode` and `implode` calls. +pub(super) fn eval_string_split_join_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "explode" => { + let [separator, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_explode_result(*separator, *string, values) + } + "implode" => { + let [separator, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_implode_result(*separator, *array, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 2bf2929b45..ed6cdbac90 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -33,6 +33,7 @@ use super::super::{ eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; +use super::string_split_join::eval_string_split_join_values; /// Evaluated-argument dispatch hooks for migrated builtins. #[derive(Clone, Copy)] @@ -141,6 +142,8 @@ pub(in crate::interpreter) enum EvalValuesHook { StringPosition, /// Dispatches string search predicate builtins. StringSearch, + /// Dispatches `explode(...)` and `implode(...)`. + StringSplitJoin, /// Dispatches stream boolean predicate builtins. StreamBoolPredicate, /// Dispatches stream introspection list builtins. @@ -327,6 +330,7 @@ impl EvalValuesHook { Self::StringSearch => two_args(evaluated_args, values, |haystack, needle, values| { eval_string_search_result(name, haystack, needle, values) }), + Self::StringSplitJoin => eval_string_split_join_values(name, evaluated_args, values), Self::StreamBoolPredicate => one_arg(evaluated_args, values, |stream, values| { eval_stream_bool_predicate_result(name, stream, values) }), diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index 16d0df2933..c64c504167 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -36,6 +36,7 @@ macro_rules! eval_builtin { ], variadic: None, by_ref_params: &[$(eval_builtin!(@name_str $by_ref)),*], + required_param_count: None, direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), } @@ -66,6 +67,7 @@ macro_rules! eval_builtin { ], variadic: Some(eval_builtin!(@name_str $variadic)), by_ref_params: &[], + required_param_count: None, direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), } @@ -76,6 +78,7 @@ macro_rules! eval_builtin { name: $name:literal, area: $area:ident, params: [$($param:ident $(= $default:expr)?),* $(,)?], + required: $required:expr, direct: $direct:ident, values: $values:ident $(,)? ) => { @@ -95,6 +98,37 @@ macro_rules! eval_builtin { ], variadic: None, by_ref_params: &[], + required_param_count: Some($required), + direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + } + } + }; + + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(= $default:expr)?),* $(,)?], + direct: $direct:ident, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param)),*], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: false, + }, + )* + ], + variadic: None, + by_ref_params: &[], + required_param_count: None, direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index ce657db563..51f3a97991 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -172,7 +172,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "defined" => Some(&["constant_name"]), "die" | "exit" => Some(&["status"]), "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), - "explode" => Some(&["separator", "string", "limit"]), "fgetcsv" => Some(&["stream", "length", "separator"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), @@ -195,7 +194,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "getenv" => Some(&["name"]), "header" => Some(&["header", "replace", "response_code"]), "http_response_code" => Some(&["response_code"]), - "implode" => Some(&["separator", "array"]), "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), "iterator_apply" => Some(&["iterator", "callback", "args"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 4e97f16215..64270d3e6d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -14,7 +14,6 @@ mod filesystem; mod formatting; mod network_env; mod scalars; -mod strings; mod symbols; mod time; @@ -27,7 +26,6 @@ use filesystem::*; use formatting::*; use network_env::*; use scalars::*; -use strings::*; use symbols::*; use time::*; @@ -60,9 +58,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( { return Ok(Some(result)); } - if let Some(result) = eval_strings_builtin_with_values(name, evaluated_args, context, values)? { - return Ok(Some(result)); - } if let Some(result) = eval_scalars_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs deleted file mode 100644 index 7343c5a2b0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Purpose: -//! Dispatches remaining already evaluated string builtins by dynamic callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Gzip and hash builtins have migrated to declarative specs. -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher -//! can continue probing other builtin families. - -use super::super::super::super::*; -use super::super::super::*; - -/// Attempts to dispatch evaluated string, hash, encoding, and ctype builtins. -pub(in crate::interpreter) fn eval_strings_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "explode" => { - let [separator, string] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_explode_result(*separator, *string, values)? - } - "implode" => { - let [separator, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_implode_result(*separator, *array, values)? - } - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 008ddd6d66..c769d6fd8f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -105,6 +105,13 @@ fn validate_declared_builtin_spec(spec: &EvalBuiltinSpec) { spec.name ); } + if let Some(required_param_count) = spec.required_param_count { + assert!( + required_param_count <= spec.params.len(), + "eval builtin {} has a required parameter count larger than its parameter list", + spec.name + ); + } let _ = spec.area(); } @@ -215,6 +222,7 @@ mod tests { "dirname", "disk_free_space", "disk_total_space", + "explode", "file", "file_exists", "file_get_contents", @@ -267,6 +275,7 @@ mod tests { "hex2bin", "htmlspecialchars", "hrtime", + "implode", "intval", "is_array", "is_bool", @@ -765,6 +774,26 @@ mod tests { eval_declared_builtin_param_names("stream_get_filters"), Some([].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("explode"), + Some(["separator", "string", "limit"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("explode", 2), + Some(EvalBuiltinDefaultValue::Int(i64::MAX)) + ); + assert_eq!( + eval_declared_builtin_param_names("implode"), + Some(["separator", "array"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("implode", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_builtin_signature_shape("implode").map(|shape| shape.required_param_count), + Some(1) + ); assert_eq!( eval_declared_builtin_param_names("gzcompress"), Some(["data", "level"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 9de92a88ce..5e3d91db6a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -55,7 +55,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "enum_exists", "exec", "exit", - "explode", "fgetcsv", "flock", "fopen", @@ -85,7 +84,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "getservbyport", "header", "http_response_code", - "implode", "inet_ntop", "inet_pton", "interface_exists", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 5b12f092a9..423d01c092 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -79,8 +79,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "is_callable" => optional_by_ref(params, 1, &["callable_name"]), "php_uname" | "readline" | "exit" | "die" => optional(params, 0), - "explode" => optional(params, 2), - "implode" => optional(params, 1), "sprintf" | "printf" | "sscanf" => variadic(params, &[]), "fprintf" | "fscanf" => variadic(params, &[]), @@ -158,9 +156,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("readline", 0) => Null, ("exit" | "die", 0) => Int(0), - ("explode", 2) => Int(i64::MAX), - ("implode", 0) => Null, - ("array_splice", 2) => Null, ("array_splice", 3) => EmptyArray, ("array_filter", 1) => Null, diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index ac58098fa9..4c92c970b1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -63,6 +63,8 @@ pub(in crate::interpreter) struct EvalBuiltinSpec { pub(in crate::interpreter) variadic: Option<&'static str>, /// Parameter names that must bind by reference. pub(in crate::interpreter) by_ref_params: &'static [&'static str], + /// Explicit required parameter count for non-trailing default shapes. + pub(in crate::interpreter) required_param_count: Option, /// Direct expression-level dispatch hook. pub(in crate::interpreter) direct: Option, /// Evaluated-argument dispatch hook. @@ -77,6 +79,9 @@ impl EvalBuiltinSpec { /// Returns the number of required leading parameters. pub(in crate::interpreter) fn required_param_count(&self) -> usize { + if let Some(required_param_count) = self.required_param_count { + return required_param_count; + } self.params .iter() .take_while(|param| param.default.is_none()) diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/explode.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/explode.rs new file mode 100644 index 0000000000..32876f56e1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/explode.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `explode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string split/join hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "explode", + area: String, + params: [separator, string, limit = EvalBuiltinDefaultValue::Int(i64::MAX)], + direct: StringSplitJoin, + values: StringSplitJoin, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/implode.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/implode.rs new file mode 100644 index 0000000000..0796996aa1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/implode.rs @@ -0,0 +1,19 @@ +//! Purpose: +//! Declarative eval registry entry for `implode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::strings::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the string split/join hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "implode", + area: String, + params: [separator = EvalBuiltinDefaultValue::Null, array], + required: 1, + direct: StringSplitJoin, + values: StringSplitJoin, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs index edfa7c13f3..a3584c0252 100644 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs @@ -12,6 +12,7 @@ mod stream_get_transports; mod stream_get_wrappers; mod stream_is_local; mod stream_supports_lock; +mod explode; mod gzcompress; mod gzdeflate; mod gzinflate; @@ -24,5 +25,6 @@ mod hash_final; mod hash_hmac; mod hash_init; mod hash_update; +mod implode; mod md5; mod sha1; diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 57a94c7c8b..a2f80e5d17 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1580,7 +1580,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_process_command(name, args, context, scope, values) } "eval" => eval_nested_eval(args, context, scope, values), - "explode" => eval_builtin_explode(args, context, scope, values), "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), "fopen" => eval_builtin_fopen(args, context, scope, values), "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), @@ -1608,7 +1607,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "getenv" => eval_builtin_getenv(args, context, scope, values), "header" => eval_builtin_header(args, context, scope, values), - "implode" => eval_builtin_implode(args, context, scope, values), "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 085c32da89..08f91b52ed 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -35,9 +35,6 @@ const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs" ), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/strings.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs" ), @@ -142,6 +139,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "dirname", "disk_free_space", "disk_total_space", + "explode", "exp", "fdiv", "file", @@ -201,6 +199,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "htmlspecialchars", "hrtime", "hypot", + "implode", "intdiv", "intval", "is_array", From f2ca6a19ab31f00a61b59af7c8827fe1390f144a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 03:15:23 +0200 Subject: [PATCH 1081/1208] refactor(magician): migrate printf formatting builtins --- .../builtins/formatting/declarations/mod.rs | 15 ++++++++ .../formatting/declarations/printf.rs | 17 +++++++++ .../formatting/declarations/sprintf.rs | 17 +++++++++ .../formatting/declarations/sscanf.rs | 17 +++++++++ .../formatting/declarations/vprintf.rs | 16 +++++++++ .../formatting/declarations/vsprintf.rs | 16 +++++++++ .../builtins/formatting/dispatch.rs | 35 +++++++++++++++++++ .../interpreter/builtins/formatting/mod.rs | 1 + .../src/interpreter/builtins/hooks/direct.rs | 20 ++++++----- .../src/interpreter/builtins/hooks/values.rs | 24 +++++++------ .../interpreter/builtins/registry/binding.rs | 3 -- .../builtins/registry/dispatch/formatting.rs | 33 ----------------- .../builtins/registry/dispatch/mod.rs | 7 ---- .../src/interpreter/builtins/registry/mod.rs | 25 +++++++++++++ .../interpreter/builtins/registry/names.rs | 5 --- .../builtins/registry/signature.rs | 1 - .../src/interpreter/expressions.rs | 3 -- tests/builtin_parity_tests.rs | 8 +++-- 18 files changed, 190 insertions(+), 73 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/printf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sprintf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sscanf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vprintf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vsprintf.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/mod.rs new file mode 100644 index 0000000000..048f85a30e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/mod.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Declarative eval registry entries for printf-family formatting builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting` module loading. +//! +//! Key details: +//! - Formatting algorithms remain in sibling helper modules; this module owns +//! per-builtin registry metadata only. + +mod printf; +mod sprintf; +mod sscanf; +mod vprintf; +mod vsprintf; diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/printf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/printf.rs new file mode 100644 index 0000000000..958451823b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/printf.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `printf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the formatting hook. + +eval_builtin! { + name: "printf", + area: Formatting, + params: [format], + variadic: values, + direct: Formatting, + values: Formatting, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sprintf.rs new file mode 100644 index 0000000000..9c394f0996 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sprintf.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `sprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the formatting hook. + +eval_builtin! { + name: "sprintf", + area: Formatting, + params: [format], + variadic: values, + direct: Formatting, + values: Formatting, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sscanf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sscanf.rs new file mode 100644 index 0000000000..cd1ef8b901 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sscanf.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `sscanf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the formatting hook. + +eval_builtin! { + name: "sscanf", + area: Formatting, + params: [string, format], + variadic: vars, + direct: Formatting, + values: Formatting, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vprintf.rs new file mode 100644 index 0000000000..1ddfb207e0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vprintf.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `vprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the formatting hook. + +eval_builtin! { + name: "vprintf", + area: Formatting, + params: [format, values], + direct: Formatting, + values: Formatting, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vsprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vsprintf.rs new file mode 100644 index 0000000000..7657e2e9f9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vsprintf.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `vsprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::formatting::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the formatting hook. + +eval_builtin! { + name: "vsprintf", + area: Formatting, + params: [format, values], + direct: Formatting, + values: Formatting, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs index 48f09afc0d..83beee8d6e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs @@ -12,6 +12,22 @@ use super::super::super::*; use super::*; +/// Dispatches direct expression-level calls for declaratively migrated formatting builtins. +pub(in crate::interpreter) fn eval_builtin_formatting_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "sscanf" => eval_builtin_sscanf(args, context, scope, values), + "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), + "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Evaluates direct positional `sprintf()` or `printf()` calls in source order. pub(in crate::interpreter) fn eval_builtin_sprintf_like( name: &str, @@ -67,3 +83,22 @@ pub(in crate::interpreter) fn eval_vsprintf_like_result( _ => Err(EvalStatus::UnsupportedConstruct), } } + +/// Dispatches evaluated-argument calls for declaratively migrated formatting builtins. +pub(in crate::interpreter) fn eval_formatting_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "sscanf" => { + let [input, format, ..] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sscanf_result(*input, *format, values) + } + "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values), + "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs index 7efc488eb6..298cb48168 100644 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs @@ -10,6 +10,7 @@ //! behavior through `RuntimeValueOps`. mod common; +mod declarations; mod dispatch; mod math; mod number_format; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 9e6ac5b8e7..00541ee4ca 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -14,14 +14,15 @@ use super::super::super::{ eval_builtin_ceil, eval_builtin_chr, eval_builtin_clamp, eval_builtin_count, eval_builtin_crc32, eval_builtin_ctype, eval_builtin_explode, eval_builtin_float_binary, eval_builtin_float_pair, eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, - eval_builtin_gzip, eval_builtin_hash_algos, eval_builtin_hash_copy, eval_builtin_hash_final, - eval_builtin_hash_init, eval_builtin_hash_one_shot, eval_builtin_hash_update, - eval_builtin_hex2bin, eval_builtin_implode, eval_builtin_intdiv, eval_builtin_log, - eval_builtin_min_max, eval_builtin_number_format, eval_builtin_ord, eval_builtin_pi, - eval_builtin_pow, eval_builtin_round, eval_builtin_slashes, eval_builtin_sqrt, - eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_type_predicate, - eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, - EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, + eval_builtin_formatting_call, eval_builtin_gzip, eval_builtin_hash_algos, + eval_builtin_hash_copy, eval_builtin_hash_final, eval_builtin_hash_init, + eval_builtin_hash_one_shot, eval_builtin_hash_update, eval_builtin_hex2bin, + eval_builtin_implode, eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, + eval_builtin_number_format, eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, + eval_builtin_round, eval_builtin_slashes, eval_builtin_sqrt, eval_builtin_str_repeat, + eval_builtin_strlen, eval_builtin_type_predicate, eval_builtin_url_decode, + eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, + RuntimeCellHandle, RuntimeValueOps, }; use super::super::{ eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_flip, @@ -91,6 +92,8 @@ pub(in crate::interpreter) enum EvalDirectHook { FloatPair, /// Dispatches unary floating-point builtins. FloatUnary, + /// Dispatches printf-family formatting builtins. + Formatting, /// Dispatches `floor(...)`. Floor, /// Dispatches `gettype(...)`. @@ -223,6 +226,7 @@ impl EvalDirectHook { Self::FloatBinary => eval_builtin_float_binary(name, args, context, scope, values), Self::FloatPair => eval_builtin_float_pair(name, args, context, scope, values), Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), + Self::Formatting => eval_builtin_formatting_call(name, args, context, scope, values), Self::Floor => eval_builtin_floor(args, context, scope, values), Self::Gettype => eval_builtin_gettype(args, context, scope, values), Self::GraphemeStrrev => eval_builtin_grapheme_strrev(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index ed6cdbac90..3d93ca874f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -21,16 +21,17 @@ use super::super::{ eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, eval_chr_result, eval_clamp_result, eval_crc32_result, eval_ctype_result, eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, - eval_float_unary_result, eval_gettype_result, eval_grapheme_strrev_result, eval_gzip_result, - eval_hash_equals_result, eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, - eval_intdiv_result, eval_json_values_result, eval_log_result, eval_min_max_result, - eval_nl2br_result, eval_number_format_result, eval_range_result, eval_regex_values_result, - eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, - eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, - eval_string_case_result, eval_string_compare_result, eval_string_position_result, - eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, - eval_time_values_result, eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, - eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, + eval_float_unary_result, eval_formatting_values_result, eval_gettype_result, + eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, + eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, + eval_json_values_result, eval_log_result, eval_min_max_result, eval_nl2br_result, + eval_number_format_result, eval_range_result, eval_regex_values_result, eval_slashes_result, + eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, + eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, + eval_string_compare_result, eval_string_position_result, eval_string_search_result, + eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, + eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, + eval_url_encode_result, eval_wordwrap_result, }; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; use super::string_split_join::eval_string_split_join_values; @@ -88,6 +89,8 @@ pub(in crate::interpreter) enum EvalValuesHook { FloatPair, /// Dispatches unary floating-point builtins. FloatUnary, + /// Dispatches printf-family formatting builtins. + Formatting, /// Dispatches `floor(...)`. Floor, /// Dispatches `gettype(...)`. @@ -254,6 +257,7 @@ impl EvalValuesHook { Self::FloatUnary => one_arg(evaluated_args, values, |value, values| { eval_float_unary_result(name, value, values) }), + Self::Formatting => eval_formatting_values_result(name, evaluated_args, values), Self::Floor => one_arg(evaluated_args, values, |value, values| values.floor(value)), Self::Gettype => one_arg(evaluated_args, values, eval_gettype_result), Self::GraphemeStrrev => one_arg(evaluated_args, values, eval_grapheme_strrev_result), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 51f3a97991..cc578f55ad 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -227,8 +227,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "spl_autoload_call" => Some(&["class"]), "spl_autoload" => Some(&["class", "file_extensions"]), "spl_object_id" | "spl_object_hash" => Some(&["object"]), - "sscanf" => Some(&["string", "format", "vars"]), - "sprintf" | "printf" => Some(&["format", "values"]), "stream_bucket_make_writeable" => Some(&["brigade"]), "stream_bucket_new" => Some(&["stream", "buffer"]), "stream_bucket_append" | "stream_bucket_prepend" => Some(&["brigade", "bucket"]), @@ -260,7 +258,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), "long2ip" => Some(&["ip"]), "vfprintf" => Some(&["stream", "format", "values"]), - "vsprintf" | "vprintf" => Some(&["format", "values"]), _ => None, } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs deleted file mode 100644 index bb7d9f66c0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Purpose: -//! Dispatches already evaluated numeric formatting and printf-family builtins by dynamic callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. - -use super::super::super::super::*; -use super::super::super::*; - -/// Attempts to dispatch evaluated numeric formatting and printf-family builtins. -pub(in crate::interpreter) fn eval_formatting_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "sscanf" => { - let [input, format, ..] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sscanf_result(*input, *format, values)? - } - "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values)?, - "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values)?, - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 64270d3e6d..18cdcb1449 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -11,7 +11,6 @@ mod arrays; mod core; mod filesystem; -mod formatting; mod network_env; mod scalars; mod symbols; @@ -23,7 +22,6 @@ use super::super::super::*; use arrays::*; use core::*; use filesystem::*; -use formatting::*; use network_env::*; use scalars::*; use symbols::*; @@ -48,11 +46,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( { return Ok(Some(result)); } - if let Some(result) = - eval_formatting_builtin_with_values(name, evaluated_args, context, values)? - { - return Ok(Some(result)); - } if let Some(result) = eval_raw_memory_builtin_with_values(name, evaluated_args, context, values)? { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index c769d6fd8f..76e0ef1e1b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -325,6 +325,7 @@ mod tests { "pathinfo", "pclose", "popen", + "printf", "preg_match", "preg_match_all", "preg_replace", @@ -345,6 +346,8 @@ mod tests { "scandir", "sha1", "sleep", + "sprintf", + "sscanf", "stat", "stream_copy_to_stream", "stream_get_contents", @@ -381,6 +384,8 @@ mod tests { "umask", "unlink", "usleep", + "vprintf", + "vsprintf", "wordwrap", ] { assert!( @@ -794,6 +799,26 @@ mod tests { eval_builtin_signature_shape("implode").map(|shape| shape.required_param_count), Some(1) ); + assert_eq!( + eval_declared_builtin_param_names("sprintf"), + Some(["format", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("sprintf").map(|shape| shape.variadic), + Some(Some("values")) + ); + assert_eq!( + eval_declared_builtin_param_names("sscanf"), + Some(["string", "format", "vars"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("sscanf").map(|shape| shape.variadic), + Some(Some("vars")) + ); + assert_eq!( + eval_declared_builtin_param_names("vsprintf"), + Some(["format", "values"].as_slice()) + ); assert_eq!( eval_declared_builtin_param_names("gzcompress"), Some(["data", "level"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 5e3d91db6a..1ccc317a4b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -122,7 +122,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ptr_write32", "ptr_write_string", "print_r", - "printf", "property_exists", "putenv", "rand", @@ -142,8 +141,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "spl_classes", "spl_object_hash", "spl_object_id", - "sprintf", - "sscanf", "stream_bucket_append", "stream_bucket_make_writeable", "stream_bucket_new", @@ -180,8 +177,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "usort", "var_dump", "vfprintf", - "vprintf", - "vsprintf", ]; /// Combined PHP-visible builtin names from legacy and declarative registries. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 423d01c092..34ee24a63c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -79,7 +79,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "is_callable" => optional_by_ref(params, 1, &["callable_name"]), "php_uname" | "readline" | "exit" | "die" => optional(params, 0), - "sprintf" | "printf" | "sscanf" => variadic(params, &[]), "fprintf" | "fscanf" => variadic(params, &[]), "array_pop" | "array_shift" => fixed_by_ref(params, &["array"]), diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index a2f80e5d17..0ac6bf2aea 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1644,8 +1644,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "spl_object_id" | "spl_object_hash" => { eval_builtin_spl_object_identity(name, args, context, scope, values) } - "sscanf" => eval_builtin_sscanf(args, context, scope, values), - "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), "stream_context_get_default" => { eval_builtin_stream_context_get_default(args, context, scope, values) @@ -1704,7 +1702,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "unset" => eval_builtin_unset(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), - "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), _ => Err(EvalStatus::UnsupportedConstruct), } } diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 08f91b52ed..f13ee5a6a2 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -26,9 +26,6 @@ const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs" ), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/formatting.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs" ), @@ -259,6 +256,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "pi", "popen", "pow", + "printf", "preg_match", "preg_match_all", "preg_replace", @@ -286,6 +284,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "sinh", "sleep", "sqrt", + "sprintf", + "sscanf", "stat", "stream_copy_to_stream", "stream_get_contents", @@ -341,6 +341,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "urldecode", "urlencode", "usleep", + "vprintf", + "vsprintf", "wordwrap", ]; From d0f0468a143bf77987d35dcf6706586b32608809 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 03:23:22 +0200 Subject: [PATCH 1082/1208] refactor(magician): migrate response header builtins --- .../interpreter/builtins/registry/binding.rs | 2 - .../builtins/registry/dispatch/mod.rs | 5 --- .../builtins/registry/dispatch/time.rs | 39 ------------------- .../src/interpreter/builtins/registry/mod.rs | 22 +++++++++++ .../interpreter/builtins/registry/names.rs | 2 - .../builtins/registry/signature.rs | 5 --- .../builtins/time/declarations/header.rs | 22 +++++++++++ .../time/declarations/http_response_code.rs | 18 +++++++++ .../builtins/time/declarations/mod.rs | 25 ++++++++++-- .../src/interpreter/expressions.rs | 2 - tests/builtin_parity_tests.rs | 5 +-- 11 files changed, 86 insertions(+), 61 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/header.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/http_response_code.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index cc578f55ad..a84b10c1ae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -192,8 +192,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "getservbyport" => Some(&["port", "protocol"]), "get_resource_id" | "get_resource_type" => Some(&["resource"]), "getenv" => Some(&["name"]), - "header" => Some(&["header", "replace", "response_code"]), - "http_response_code" => Some(&["response_code"]), "inet_ntop" => Some(&["ip"]), "inet_pton" => Some(&["ip"]), "iterator_apply" => Some(&["iterator", "callback", "args"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 18cdcb1449..aa6c3d6962 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -14,7 +14,6 @@ mod filesystem; mod network_env; mod scalars; mod symbols; -mod time; use super::eval_declared_builtin_values_call; use super::super::super::*; @@ -25,7 +24,6 @@ use filesystem::*; use network_env::*; use scalars::*; use symbols::*; -use time::*; /// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. pub(in crate::interpreter) fn eval_builtin_with_values( @@ -59,9 +57,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( { return Ok(Some(result)); } - if let Some(result) = eval_time_builtin_with_values(name, evaluated_args, context, values)? { - return Ok(Some(result)); - } if let Some(result) = eval_network_env_builtin_with_values(name, evaluated_args, context, values)? { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs deleted file mode 100644 index b56a6775ca..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Purpose: -//! Dispatches remaining already evaluated response-header builtins by dynamic callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Date, time, and sleep builtins have migrated to declarative specs. -//! - Returns `Ok(None)` for names outside this small legacy surface so the -//! parent dispatcher can continue probing other builtin families. - -use super::super::super::super::*; -use super::super::super::*; - -/// Attempts to dispatch evaluated header and response-code builtins. -pub(in crate::interpreter) fn eval_time_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "header" => match evaluated_args { - [line] => eval_header_result(*line, None, None, context, values)?, - [line, replace] => eval_header_result(*line, Some(*replace), None, context, values)?, - [line, replace, response_code] => { - eval_header_result(*line, Some(*replace), Some(*response_code), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "http_response_code" => match evaluated_args { - [] => eval_http_response_code_result(None, context, values)?, - [response_code] => eval_http_response_code_result(Some(*response_code), context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 76e0ef1e1b..c2a964747c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -272,9 +272,11 @@ mod tests { "hash_hmac", "hash_init", "hash_update", + "header", "hex2bin", "htmlspecialchars", "hrtime", + "http_response_code", "implode", "intval", "is_array", @@ -486,6 +488,26 @@ mod tests { eval_declared_builtin_param_names("date_default_timezone_get"), Some([].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("header"), + Some(["header", "replace", "response_code"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("header", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); + assert_eq!( + eval_declared_builtin_default_value("header", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("http_response_code"), + Some(["response_code"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("http_response_code", 0), + Some(EvalBuiltinDefaultValue::Int(0)) + ); assert_eq!( eval_declared_builtin_param_names("getdate"), Some(["timestamp"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 1ccc317a4b..52f0ac2930 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -82,8 +82,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "getprotobynumber", "getservbyname", "getservbyport", - "header", - "http_response_code", "inet_ntop", "inet_pton", "interface_exists", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 34ee24a63c..96052a64e7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -74,8 +74,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "get_class" | "get_parent_class" => optional(params, 0), "is_a" | "is_subclass_of" => optional(params, 2), - "header" => optional(params, 1), - "http_response_code" => optional(params, 0), "is_callable" => optional_by_ref(params, 1, &["callable_name"]), "php_uname" | "readline" | "exit" | "die" => optional(params, 0), @@ -146,9 +144,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_a", 2) => Bool(false), ("is_subclass_of", 2) => Bool(true), - ("header", 1) => Bool(true), - ("header", 2) => Int(0), - ("http_response_code", 0) => Int(0), ("is_callable", 1) => Bool(false), ("is_callable", 2) => Null, ("php_uname", 0) => String("a"), diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/header.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/header.rs new file mode 100644 index 0000000000..ae57b7f301 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/header.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `header`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the time/system hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "header", + area: Time, + params: [ + header, + replace = EvalBuiltinDefaultValue::Bool(true), + response_code = EvalBuiltinDefaultValue::Int(0), + ], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/http_response_code.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/http_response_code.rs new file mode 100644 index 0000000000..55d6ec694d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/http_response_code.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `http_response_code`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the time/system hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "http_response_code", + area: Time, + params: [response_code = EvalBuiltinDefaultValue::Int(0)], + direct: Time, + values: Time, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs index 4074a43ec9..c45180ba61 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs @@ -1,13 +1,15 @@ //! Purpose: -//! Declarative eval registry entries and dispatch adapters for time builtins. +//! Declarative eval registry entries and dispatch adapters for time and +//! response-header builtins. //! //! Called from: //! - `crate::interpreter::builtins::time` module loading. //! - `crate::interpreter::builtins::hooks` for migrated time dispatch. //! //! Key details: -//! - Time/date algorithms remain in sibling helper modules such as `date`, -//! `calendar`, `clock`, `mktime`, `sleep`, and `strtotime`. +//! - Time/date and response-state algorithms remain in sibling helper modules +//! such as `date`, `calendar`, `clock`, `mktime`, `sleep`, `strtotime`, and +//! `system`. //! - This module owns registry metadata and small hook adapters only. use super::super::super::*; @@ -20,7 +22,9 @@ mod date_default_timezone_set; mod getdate; mod gmdate; mod gmmktime; +mod header; mod hrtime; +mod http_response_code; mod localtime; mod microtime; mod mktime; @@ -46,7 +50,9 @@ pub(in crate::interpreter) fn eval_builtin_time_call( } "getdate" => eval_builtin_getdate(args, context, scope, values), "gmmktime" | "mktime" => eval_builtin_mktime_like(name, args, context, scope, values), + "header" => eval_builtin_header(args, context, scope, values), "hrtime" => eval_builtin_hrtime(args, context, scope, values), + "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), "localtime" => eval_builtin_localtime(args, context, scope, values), "microtime" => eval_builtin_microtime(args, context, scope, values), "sleep" => eval_builtin_sleep(args, context, scope, values), @@ -101,11 +107,24 @@ pub(in crate::interpreter) fn eval_time_values_result( name, *hour, *minute, *second, *month, *day, *year, context, values, ) } + "header" => match evaluated_args { + [line] => eval_header_result(*line, None, None, context, values), + [line, replace] => eval_header_result(*line, Some(*replace), None, context, values), + [line, replace, response_code] => { + eval_header_result(*line, Some(*replace), Some(*response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, "hrtime" => match evaluated_args { [] => eval_hrtime_result(None, values), [as_number] => eval_hrtime_result(Some(*as_number), values), _ => Err(EvalStatus::RuntimeFatal), }, + "http_response_code" => match evaluated_args { + [] => eval_http_response_code_result(None, context, values), + [response_code] => eval_http_response_code_result(Some(*response_code), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, "localtime" => match evaluated_args { [] => eval_localtime_result(None, None, context, values), [timestamp] => eval_localtime_result(Some(*timestamp), None, context, values), diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 0ac6bf2aea..35df8b7d74 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1606,13 +1606,11 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_resource_introspection(name, args, context, scope, values) } "getenv" => eval_builtin_getenv(args, context, scope, values), - "header" => eval_builtin_header(args, context, scope, values), "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), - "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), "ip2long" => eval_builtin_ip2long(args, context, scope, values), "php_uname" => eval_builtin_php_uname(args, context, scope, values), "phpversion" => eval_builtin_phpversion(args, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index f13ee5a6a2..f164ff569a 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -35,9 +35,6 @@ const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs" ), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/time.rs" - ), ]; /// Eval-only reflection probes exist because magician can inspect dynamic eval metadata before the AOT catalog exposes them. @@ -190,11 +187,13 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "hash_hmac", "hash_init", "hash_update", + "header", "hex2bin", "html_entity_decode", "htmlentities", "htmlspecialchars", "hrtime", + "http_response_code", "hypot", "implode", "intdiv", From d403e3d7b8044c7220d40d821e8b573d20afc7d8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 03:39:36 +0200 Subject: [PATCH 1083/1208] refactor(magician): migrate random builtins --- .../src/interpreter/builtins/hooks/direct.rs | 15 +++++-- .../src/interpreter/builtins/hooks/mod.rs | 2 + .../builtins/hooks/number_format.rs | 40 +++++++++++++++++++ .../src/interpreter/builtins/hooks/random.rs | 34 ++++++++++++++++ .../src/interpreter/builtins/hooks/values.rs | 33 ++++----------- .../src/interpreter/builtins/math/mod.rs | 3 ++ .../src/interpreter/builtins/math/mt_rand.rs | 16 ++++++++ .../src/interpreter/builtins/math/rand.rs | 16 ++++++++ .../interpreter/builtins/math/random_int.rs | 16 ++++++++ .../interpreter/builtins/registry/binding.rs | 1 - .../builtins/registry/dispatch/scalars.rs | 15 +------ .../src/interpreter/builtins/registry/mod.rs | 10 +++++ .../interpreter/builtins/registry/names.rs | 3 -- .../src/interpreter/expressions.rs | 2 - tests/builtin_parity_tests.rs | 3 ++ 15 files changed, 161 insertions(+), 48 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/number_format.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/random.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/rand.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/random_int.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 00541ee4ca..0db255d63d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -19,10 +19,10 @@ use super::super::super::{ eval_builtin_hash_one_shot, eval_builtin_hash_update, eval_builtin_hex2bin, eval_builtin_implode, eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, eval_builtin_number_format, eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, - eval_builtin_round, eval_builtin_slashes, eval_builtin_sqrt, eval_builtin_str_repeat, - eval_builtin_strlen, eval_builtin_type_predicate, eval_builtin_url_decode, - eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, - RuntimeCellHandle, RuntimeValueOps, + eval_builtin_rand, eval_builtin_random_int, eval_builtin_round, eval_builtin_slashes, + eval_builtin_sqrt, eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_type_predicate, + eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, + EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::super::{ eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_flip, @@ -130,6 +130,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Pi, /// Dispatches `pow(...)`. Pow, + /// Dispatches random-number builtins. + Random, /// Dispatches `round(...)`. Round, /// Dispatches `range(...)`. @@ -251,6 +253,11 @@ impl EvalDirectHook { Self::Ord => eval_builtin_ord(args, context, scope, values), Self::Pi => eval_builtin_pi(args, values), Self::Pow => eval_builtin_pow(args, context, scope, values), + Self::Random => match name { + "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), + "random_int" => eval_builtin_random_int(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Round => eval_builtin_round(args, context, scope, values), Self::Range => eval_builtin_range(args, context, scope, values), Self::Regex => eval_builtin_regex_call(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs index a2621565de..f1ec512c1f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs @@ -10,6 +10,8 @@ mod direct; mod hash; +mod number_format; +mod random; mod string_split_join; mod values; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/number_format.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/number_format.rs new file mode 100644 index 0000000000..245a562374 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/number_format.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Focused evaluated-argument dispatch helper for `number_format`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::values`. +//! +//! Key details: +//! - Keeping the arity match here prevents the generic values hook table from +//! growing past the ordinary file-size limit. + +use super::super::super::{EvalStatus, RuntimeCellHandle, RuntimeValueOps}; +use super::super::eval_number_format_result; + +/// Dispatches evaluated `number_format` arguments. +pub(super) fn eval_number_format_values( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values), + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values) + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + ), + [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/random.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/random.rs new file mode 100644 index 0000000000..b3e01cdf6a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/random.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Focused dispatch helpers for declarative random-number builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::values`. +//! +//! Key details: +//! - `rand` and `mt_rand` accept either no arguments or an inclusive min/max +//! range, while `random_int` requires an inclusive min/max range. + +use super::super::super::{EvalStatus, RuntimeCellHandle, RuntimeValueOps}; +use super::super::{eval_rand_result, eval_random_int_result}; + +/// Dispatches evaluated random-number builtin calls. +pub(super) fn eval_random_values( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "rand" | "mt_rand" => match evaluated_args { + [] => eval_rand_result(None, None, values), + [min, max] => eval_rand_result(Some(*min), Some(*max), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "random_int" => { + let [min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_random_int_result(*min, *max, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 3d93ca874f..d95b0f2a9d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -25,8 +25,8 @@ use super::super::{ eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_json_values_result, eval_log_result, eval_min_max_result, eval_nl2br_result, - eval_number_format_result, eval_range_result, eval_regex_values_result, eval_slashes_result, - eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, + eval_range_result, eval_regex_values_result, eval_slashes_result, eval_str_pad_result, + eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, @@ -34,6 +34,8 @@ use super::super::{ eval_url_encode_result, eval_wordwrap_result, }; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; +use super::number_format::eval_number_format_values; +use super::random::eval_random_values; use super::string_split_join::eval_string_split_join_values; /// Evaluated-argument dispatch hooks for migrated builtins. @@ -127,6 +129,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Pi, /// Dispatches `pow(...)`. Pow, + /// Dispatches random-number builtins. + Random, /// Dispatches `round(...)`. Round, /// Dispatches `range(...)`. @@ -278,29 +282,7 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::MinMax => eval_min_max_result(name, evaluated_args, values), - Self::NumberFormat => match evaluated_args { - [value] => eval_number_format_result(*value, None, None, None, values), - [value, decimals] => { - eval_number_format_result(*value, Some(*decimals), None, None, values) - } - [value, decimals, decimal_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - None, - values, - ), - [value, decimals, decimal_separator, thousands_separator] => { - eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - Some(*thousands_separator), - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - }, + Self::NumberFormat => eval_number_format_values(evaluated_args, values), Self::Ord => one_arg(evaluated_args, values, eval_ord_result), Self::Pi => { if !evaluated_args.is_empty() { @@ -311,6 +293,7 @@ impl EvalValuesHook { Self::Pow => two_args(evaluated_args, values, |left, right, values| { values.pow(left, right) }), + Self::Random => eval_random_values(name, evaluated_args, values), Self::Round => match evaluated_args { [value] => values.round(*value, None), [value, precision] => values.round(*value, Some(*precision)), diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs index 1f0b445251..6cd64f8780 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs @@ -29,8 +29,11 @@ mod log10; mod log2; mod max; mod min; +mod mt_rand; mod pi; mod pow; +mod rand; +mod random_int; mod rad2deg; mod round; mod sin; diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs b/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs new file mode 100644 index 0000000000..3659a6ea05 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `mt_rand`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the random-number hook. + +eval_builtin! { + name: "mt_rand", + area: Math, + params: [min, max], + direct: Random, + values: Random, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/rand.rs b/crates/elephc-magician/src/interpreter/builtins/math/rand.rs new file mode 100644 index 0000000000..59b8847de0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/rand.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `rand`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the random-number hook. + +eval_builtin! { + name: "rand", + area: Math, + params: [min, max], + direct: Random, + values: Random, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs b/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs new file mode 100644 index 0000000000..af2a3a6f75 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `random_int`. +//! +//! Called from: +//! - `crate::interpreter::builtins::math`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the random-number hook. + +eval_builtin! { + name: "random_int", + area: Math, + params: [min, max], + direct: Random, + values: Random, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index a84b10c1ae..0da76c1c28 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -216,7 +216,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "print_r" => Some(&["value", "return"]), "var_dump" => Some(&["value", "values"]), "putenv" => Some(&["assignment"]), - "rand" | "mt_rand" | "random_int" => Some(&["min", "max"]), "readline" => Some(&["prompt"]), "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), "spl_autoload_unregister" => Some(&["callback"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs index 568d6e501b..754f58119d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Dispatches already evaluated scalar math, casts, predicates, and random builtins by dynamic callable name. +//! Dispatches remaining already evaluated scalar mutation builtins by dynamic callable name. //! //! Called from: //! - `crate::interpreter::builtins::registry::dispatch`. @@ -11,7 +11,7 @@ use super::super::super::super::*; use super::super::super::*; -/// Attempts to dispatch evaluated scalar math, casts, predicates, and random builtins. +/// Attempts to dispatch evaluated scalar mutation builtins. pub(in crate::interpreter) fn eval_scalars_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], @@ -19,17 +19,6 @@ pub(in crate::interpreter) fn eval_scalars_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "rand" | "mt_rand" => match evaluated_args { - [] => eval_rand_result(None, None, values)?, - [min, max] => eval_rand_result(Some(*min), Some(*max), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "random_int" => { - let [min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_random_int_result(*min, *max, values)? - } "settype" => { let [value, type_name] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index c2a964747c..2872bffe0b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -321,6 +321,7 @@ mod tests { "min", "mkdir", "mktime", + "mt_rand", "nl2br", "number_format", "opendir", @@ -333,6 +334,8 @@ mod tests { "preg_replace", "preg_replace_callback", "preg_split", + "rand", + "random_int", "range", "rawurlencode", "readdir", @@ -432,6 +435,13 @@ mod tests { eval_declared_builtin_param_names("max"), Some(["value", "values"].as_slice()) ); + for name in ["rand", "mt_rand", "random_int"] { + assert_eq!( + eval_declared_builtin_param_names(name), + Some(["min", "max"].as_slice()), + "{name} should declare min/max parameters" + ); + } assert_eq!( eval_declared_builtin_param_names("number_format"), Some( diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 52f0ac2930..6cf713fee2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -97,7 +97,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ksort", "long2ip", "method_exists", - "mt_rand", "natcasesort", "natsort", "passthru", @@ -122,8 +121,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "print_r", "property_exists", "putenv", - "rand", - "random_int", "readline", "rsort", "settype", diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 35df8b7d74..cb6e81e54e 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1622,8 +1622,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( } "print_r" => eval_builtin_print_r(args, context, scope, values), "putenv" => eval_builtin_putenv(args, context, scope, values), - "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), - "random_int" => eval_builtin_random_int(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "spl_autoload_register" | "spl_autoload_unregister" => { diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index f164ff569a..9909e7353e 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -246,6 +246,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "min", "mkdir", "mktime", + "mt_rand", "nl2br", "number_format", "opendir", @@ -262,6 +263,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "preg_replace_callback", "preg_split", "rad2deg", + "rand", + "random_int", "range", "rawurldecode", "rawurlencode", From eb2eac04a49b7afdaf5c781c6e15d734c0fa084c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 03:55:43 +0200 Subject: [PATCH 1084/1208] refactor(magician): migrate network env builtins --- .../src/interpreter/builtins/hooks/direct.rs | 16 +- .../src/interpreter/builtins/hooks/values.rs | 9 +- .../builtins/network_env/declarations/exec.rs | 16 ++ .../network_env/declarations/getenv.rs | 16 ++ .../network_env/declarations/gethostbyaddr.rs | 16 ++ .../network_env/declarations/gethostbyname.rs | 16 ++ .../network_env/declarations/gethostname.rs | 16 ++ .../declarations/getprotobyname.rs | 16 ++ .../declarations/getprotobynumber.rs | 16 ++ .../network_env/declarations/getservbyname.rs | 16 ++ .../network_env/declarations/getservbyport.rs | 16 ++ .../network_env/declarations/inet_ntop.rs | 16 ++ .../network_env/declarations/inet_pton.rs | 16 ++ .../network_env/declarations/ip2long.rs | 16 ++ .../network_env/declarations/long2ip.rs | 16 ++ .../builtins/network_env/declarations/mod.rs | 171 ++++++++++++++++++ .../network_env/declarations/passthru.rs | 16 ++ .../network_env/declarations/php_uname.rs | 18 ++ .../network_env/declarations/phpversion.rs | 16 ++ .../network_env/declarations/putenv.rs | 16 ++ .../network_env/declarations/shell_exec.rs | 16 ++ .../network_env/declarations/system.rs | 16 ++ .../interpreter/builtins/network_env/mod.rs | 2 + .../interpreter/builtins/registry/binding.rs | 16 -- .../builtins/registry/dispatch/mod.rs | 7 - .../builtins/registry/dispatch/network_env.rs | 120 ------------ .../src/interpreter/builtins/registry/mod.rs | 39 ++++ .../interpreter/builtins/registry/names.rs | 19 -- .../builtins/registry/signature.rs | 3 +- .../src/interpreter/builtins/spec.rs | 2 + .../src/interpreter/expressions.rs | 18 -- tests/builtin_parity_tests.rs | 22 ++- 32 files changed, 556 insertions(+), 194 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/exec.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getenv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyaddr.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobyname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobynumber.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyport.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_ntop.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_pton.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/ip2long.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/long2ip.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/passthru.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/php_uname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/phpversion.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/putenv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/shell_exec.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/system.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 0db255d63d..b484ffb8a3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -30,12 +30,13 @@ use super::super::{ eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, eval_builtin_array_slice, eval_builtin_array_unique, eval_builtin_cast, eval_builtin_filesystem_call, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, - eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_nl2br, eval_builtin_range, - eval_builtin_regex_call, eval_builtin_str_pad, eval_builtin_str_replace, eval_builtin_str_split, - eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, eval_builtin_string_case, - eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, - eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, - eval_builtin_time_call, eval_builtin_trim_like, + eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_network_env_call, + eval_builtin_nl2br, eval_builtin_range, eval_builtin_regex_call, eval_builtin_str_pad, + eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_stream_bool_predicate, + eval_builtin_stream_introspection, eval_builtin_string_case, eval_builtin_string_compare, + eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, + eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, eval_builtin_time_call, + eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, }; @@ -122,6 +123,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Log, /// Dispatches `min(...)` and `max(...)`. MinMax, + /// Dispatches network, host, environment, and process builtins. + NetworkEnv, /// Dispatches `number_format(...)`. NumberFormat, /// Dispatches `ord(...)`. @@ -249,6 +252,7 @@ impl EvalDirectHook { Self::Json => eval_builtin_json_call(name, args, context, scope, values), Self::Log => eval_builtin_log(args, context, scope, values), Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), + Self::NetworkEnv => eval_builtin_network_env_call(name, args, context, scope, values), Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), Self::Ord => eval_builtin_ord(args, context, scope, values), Self::Pi => eval_builtin_pi(args, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index d95b0f2a9d..a97bd74c32 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -24,9 +24,9 @@ use super::super::{ eval_float_unary_result, eval_formatting_values_result, eval_gettype_result, eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, - eval_json_values_result, eval_log_result, eval_min_max_result, eval_nl2br_result, - eval_range_result, eval_regex_values_result, eval_slashes_result, eval_str_pad_result, - eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, + eval_json_values_result, eval_log_result, eval_min_max_result, eval_network_env_values_result, + eval_nl2br_result, eval_range_result, eval_regex_values_result, eval_slashes_result, + eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, @@ -121,6 +121,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Log, /// Dispatches `min(...)` and `max(...)`. MinMax, + /// Dispatches network, host, environment, and process builtins. + NetworkEnv, /// Dispatches `number_format(...)`. NumberFormat, /// Dispatches `ord(...)`. @@ -282,6 +284,7 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::MinMax => eval_min_max_result(name, evaluated_args, values), + Self::NetworkEnv => eval_network_env_values_result(name, evaluated_args, values), Self::NumberFormat => eval_number_format_values(evaluated_args, values), Self::Ord => one_arg(evaluated_args, values, eval_ord_result), Self::Pi => { diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/exec.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/exec.rs new file mode 100644 index 0000000000..1fed80ea9f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/exec.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `exec`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process-command hook. + +eval_builtin! { + name: "exec", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getenv.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getenv.rs new file mode 100644 index 0000000000..a04236fc41 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getenv.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `getenv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the environment hook. + +eval_builtin! { + name: "getenv", + area: NetworkEnv, + params: [name], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyaddr.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyaddr.rs new file mode 100644 index 0000000000..bfb4cfe976 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyaddr.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `gethostbyaddr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the host lookup hook. + +eval_builtin! { + name: "gethostbyaddr", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyname.rs new file mode 100644 index 0000000000..933f72b215 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyname.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `gethostbyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the host lookup hook. + +eval_builtin! { + name: "gethostbyname", + area: NetworkEnv, + params: [hostname], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostname.rs new file mode 100644 index 0000000000..1a13ea2e54 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostname.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `gethostname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the host lookup hook. + +eval_builtin! { + name: "gethostname", + area: NetworkEnv, + params: [], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobyname.rs new file mode 100644 index 0000000000..d63ab81b78 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobyname.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `getprotobyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the protocol lookup hook. + +eval_builtin! { + name: "getprotobyname", + area: NetworkEnv, + params: [protocol], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobynumber.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobynumber.rs new file mode 100644 index 0000000000..b23fcea873 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobynumber.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `getprotobynumber`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the protocol lookup hook. + +eval_builtin! { + name: "getprotobynumber", + area: NetworkEnv, + params: [protocol], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyname.rs new file mode 100644 index 0000000000..9afb8c1889 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyname.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `getservbyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the service lookup hook. + +eval_builtin! { + name: "getservbyname", + area: NetworkEnv, + params: [service, protocol], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyport.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyport.rs new file mode 100644 index 0000000000..463542a1c3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyport.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `getservbyport`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the service lookup hook. + +eval_builtin! { + name: "getservbyport", + area: NetworkEnv, + params: [port, protocol], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_ntop.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_ntop.rs new file mode 100644 index 0000000000..68c0ad384a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_ntop.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `inet_ntop`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the IP conversion hook. + +eval_builtin! { + name: "inet_ntop", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_pton.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_pton.rs new file mode 100644 index 0000000000..cfde090007 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_pton.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `inet_pton`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the IP conversion hook. + +eval_builtin! { + name: "inet_pton", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/ip2long.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/ip2long.rs new file mode 100644 index 0000000000..27b262183b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/ip2long.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ip2long`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the IP conversion hook. + +eval_builtin! { + name: "ip2long", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/long2ip.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/long2ip.rs new file mode 100644 index 0000000000..9981e24f15 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/long2ip.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `long2ip`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the IP conversion hook. + +eval_builtin! { + name: "long2ip", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/mod.rs new file mode 100644 index 0000000000..71fd5e4e89 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/mod.rs @@ -0,0 +1,171 @@ +//! Purpose: +//! Declarative eval registry entries and dispatch adapters for network, +//! environment, process, and system-information builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` module loading. +//! - `crate::interpreter::builtins::hooks` for migrated network/env dispatch. +//! +//! Key details: +//! - Runtime behavior stays in the focused sibling helper modules; this module +//! owns registry metadata and small hook adapters only. + +use super::super::super::*; +use super::*; + +mod exec; +mod getenv; +mod gethostbyaddr; +mod gethostbyname; +mod gethostname; +mod getprotobyname; +mod getprotobynumber; +mod getservbyname; +mod getservbyport; +mod inet_ntop; +mod inet_pton; +mod ip2long; +mod long2ip; +mod passthru; +mod php_uname; +mod phpversion; +mod putenv; +mod shell_exec; +mod system; + +/// Dispatches direct expression-level calls for declaratively migrated network/env builtins. +pub(in crate::interpreter) fn eval_builtin_network_env_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "exec" | "shell_exec" | "system" | "passthru" => { + eval_builtin_process_command(name, args, context, scope, values) + } + "getenv" => eval_builtin_getenv(args, context, scope, values), + "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), + "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), + "gethostname" => eval_builtin_gethostname(args, values), + "getprotobyname" => eval_builtin_getprotobyname(args, context, scope, values), + "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), + "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), + "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), + "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), + "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), + "ip2long" => eval_builtin_ip2long(args, context, scope, values), + "long2ip" => eval_builtin_long2ip(args, context, scope, values), + "php_uname" => super::super::eval_builtin_php_uname(args, context, scope, values), + "phpversion" => super::super::eval_builtin_phpversion(args, values), + "putenv" => eval_builtin_putenv(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated network/env builtins. +pub(in crate::interpreter) fn eval_network_env_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "php_uname" => match evaluated_args { + [] => super::super::eval_php_uname_result(None, values), + [mode] => super::super::eval_php_uname_result(Some(*mode), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "gethostbyaddr" => { + let [ip] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyaddr_result(*ip, values) + } + "gethostbyname" => { + let [hostname] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyname_result(*hostname, values) + } + "gethostname" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_gethostname_result(values) + } + "getprotobyname" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobyname_result(*protocol, values) + } + "getprotobynumber" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobynumber_result(*protocol, values) + } + "getservbyname" => { + let [service, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyname_result(*service, *protocol, values) + } + "getservbyport" => { + let [port, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyport_result(*port, *protocol, values) + } + "getenv" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getenv_result(*name, values) + } + "exec" | "shell_exec" | "system" | "passthru" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_process_command_result(name, *command, values) + } + "inet_ntop" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_ntop_result(*value, values) + } + "inet_pton" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_pton_result(*value, values) + } + "ip2long" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ip2long_result(*value, values) + } + "phpversion" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + super::super::eval_phpversion_result(values) + } + "putenv" => { + let [assignment] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_putenv_result(*assignment, values) + } + "long2ip" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_long2ip_result(*value, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/passthru.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/passthru.rs new file mode 100644 index 0000000000..6376968ddd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/passthru.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `passthru`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process-command hook. + +eval_builtin! { + name: "passthru", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/php_uname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/php_uname.rs new file mode 100644 index 0000000000..89067c6fa4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/php_uname.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `php_uname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the system-information hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "php_uname", + area: NetworkEnv, + params: [mode = EvalBuiltinDefaultValue::String("a")], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/phpversion.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/phpversion.rs new file mode 100644 index 0000000000..23d7ad887a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/phpversion.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `phpversion`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the system-information hook. + +eval_builtin! { + name: "phpversion", + area: NetworkEnv, + params: [], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/putenv.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/putenv.rs new file mode 100644 index 0000000000..3c9bdb1625 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/putenv.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `putenv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the environment hook. + +eval_builtin! { + name: "putenv", + area: NetworkEnv, + params: [assignment], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/shell_exec.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/shell_exec.rs new file mode 100644 index 0000000000..34308ab1a0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/shell_exec.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `shell_exec`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process-command hook. + +eval_builtin! { + name: "shell_exec", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/system.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/system.rs new file mode 100644 index 0000000000..7ea5d2e0cf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/system.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `system`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process-command hook. + +eval_builtin! { + name: "system", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs index db58637bf0..fc913dc119 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs @@ -10,6 +10,7 @@ //! process-global resolver storage. mod cache; +mod declarations; mod env; mod hosts; mod ip; @@ -17,6 +18,7 @@ mod process; mod protocols; pub(in crate::interpreter) use cache::*; +pub(in crate::interpreter) use declarations::*; pub(in crate::interpreter) use env::*; pub(in crate::interpreter) use hosts::*; pub(in crate::interpreter) use ip::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 0da76c1c28..fda237b445 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -171,7 +171,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "define" => Some(&["constant_name", "value"]), "defined" => Some(&["constant_name"]), "die" | "exit" => Some(&["status"]), - "exec" | "shell_exec" | "system" | "passthru" => Some(&["command"]), "fgetcsv" => Some(&["stream", "length", "separator"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), @@ -183,24 +182,11 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "fscanf" => Some(&["stream", "format", "vars"]), "function_exists" => Some(&["function"]), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), - "gethostbyaddr" => Some(&["ip"]), - "gethostbyname" => Some(&["hostname"]), - "gethostname" => Some(&[]), - "getprotobyname" => Some(&["protocol"]), - "getprotobynumber" => Some(&["protocol"]), - "getservbyname" => Some(&["service", "protocol"]), - "getservbyport" => Some(&["port", "protocol"]), "get_resource_id" | "get_resource_type" => Some(&["resource"]), - "getenv" => Some(&["name"]), - "inet_ntop" => Some(&["ip"]), - "inet_pton" => Some(&["ip"]), "iterator_apply" => Some(&["iterator", "callback", "args"]), "iterator_count" => Some(&["iterator"]), "iterator_to_array" => Some(&["iterator", "preserve_keys"]), - "ip2long" => Some(&["ip"]), "isset" | "unset" => Some(&["var", "vars"]), - "php_uname" => Some(&["mode"]), - "phpversion" => Some(&[]), "ptr" => Some(&["value"]), "ptr_null" => Some(&[]), "ptr_is_null" | "ptr_get" | "ptr_read8" | "ptr_read16" | "ptr_read32" => { @@ -215,7 +201,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "ptr_sizeof" => Some(&["type"]), "print_r" => Some(&["value", "return"]), "var_dump" => Some(&["value", "values"]), - "putenv" => Some(&["assignment"]), "readline" => Some(&["prompt"]), "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), "spl_autoload_unregister" => Some(&["callback"]), @@ -253,7 +238,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "stream_socket_shutdown" => Some(&["stream", "mode"]), "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), - "long2ip" => Some(&["ip"]), "vfprintf" => Some(&["stream", "format", "values"]), _ => None, } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index aa6c3d6962..4052532099 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -11,7 +11,6 @@ mod arrays; mod core; mod filesystem; -mod network_env; mod scalars; mod symbols; @@ -21,7 +20,6 @@ use super::super::super::*; use arrays::*; use core::*; use filesystem::*; -use network_env::*; use scalars::*; use symbols::*; @@ -57,11 +55,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( { return Ok(Some(result)); } - if let Some(result) = - eval_network_env_builtin_with_values(name, evaluated_args, context, values)? - { - return Ok(Some(result)); - } if let Some(result) = eval_symbols_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs deleted file mode 100644 index ca224e22aa..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs +++ /dev/null @@ -1,120 +0,0 @@ -//! Purpose: -//! Dispatches already evaluated network, environment, and stream-introspection builtins by dynamic callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. - -use super::super::super::super::*; -use super::super::super::*; - -/// Attempts to dispatch evaluated network, environment, and stream-introspection builtins. -pub(in crate::interpreter) fn eval_network_env_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "php_uname" => match evaluated_args { - [] => eval_php_uname_result(None, values)?, - [mode] => eval_php_uname_result(Some(*mode), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "gethostbyaddr" => { - let [ip] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyaddr_result(*ip, values)? - } - "gethostbyname" => { - let [hostname] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyname_result(*hostname, values)? - } - "gethostname" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_gethostname_result(values)? - } - "getprotobyname" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobyname_result(*protocol, values)? - } - "getprotobynumber" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobynumber_result(*protocol, values)? - } - "getservbyname" => { - let [service, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyname_result(*service, *protocol, values)? - } - "getservbyport" => { - let [port, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyport_result(*port, *protocol, values)? - } - "getenv" => { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getenv_result(*name, values)? - } - "exec" | "shell_exec" | "system" | "passthru" => { - let [command] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_process_command_result(name, *command, values)? - } - "inet_ntop" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_ntop_result(*value, values)? - } - "inet_pton" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_pton_result(*value, values)? - } - "ip2long" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ip2long_result(*value, values)? - } - "phpversion" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_phpversion_result(values)? - } - "putenv" => { - let [assignment] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_putenv_result(*assignment, values)? - } - "long2ip" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_long2ip_result(*value, values)? - } - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 2872bffe0b..50b82a425e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -222,6 +222,7 @@ mod tests { "dirname", "disk_free_space", "disk_total_space", + "exec", "explode", "file", "file_exists", @@ -254,6 +255,14 @@ mod tests { "fwrite", "getdate", "getcwd", + "getenv", + "gethostbyaddr", + "gethostbyname", + "gethostname", + "getprotobyname", + "getprotobynumber", + "getservbyname", + "getservbyport", "gettype", "gmdate", "gmmktime", @@ -278,6 +287,8 @@ mod tests { "hrtime", "http_response_code", "implode", + "inet_ntop", + "inet_pton", "intval", "is_array", "is_bool", @@ -304,6 +315,7 @@ mod tests { "is_string", "is_writable", "is_writeable", + "ip2long", "json_decode", "json_encode", "json_last_error", @@ -315,6 +327,7 @@ mod tests { "linkinfo", "localtime", "log", + "long2ip", "lstat", "microtime", "md5", @@ -327,6 +340,9 @@ mod tests { "opendir", "pathinfo", "pclose", + "passthru", + "php_uname", + "phpversion", "popen", "printf", "preg_match", @@ -334,6 +350,7 @@ mod tests { "preg_replace", "preg_replace_callback", "preg_split", + "putenv", "rand", "random_int", "range", @@ -350,6 +367,7 @@ mod tests { "rmdir", "scandir", "sha1", + "shell_exec", "sleep", "sprintf", "sscanf", @@ -378,6 +396,7 @@ mod tests { "strrev", "strtotime", "substr", + "system", "symlink", "sys_get_temp_dir", "tempnam", @@ -518,6 +537,26 @@ mod tests { eval_declared_builtin_default_value("http_response_code", 0), Some(EvalBuiltinDefaultValue::Int(0)) ); + assert_eq!( + eval_declared_builtin_param_names("php_uname"), + Some(["mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("php_uname", 0), + Some(EvalBuiltinDefaultValue::String("a")) + ); + assert_eq!( + eval_declared_builtin_param_names("phpversion"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getenv"), + Some(["name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getservbyname"), + Some(["service", "protocol"].as_slice()) + ); assert_eq!( eval_declared_builtin_param_names("getdate"), Some(["timestamp"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 6cf713fee2..27f538e9b8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -53,7 +53,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "die", "empty", "enum_exists", - "exec", "exit", "fgetcsv", "flock", @@ -74,18 +73,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "get_parent_class", "get_resource_id", "get_resource_type", - "getenv", - "gethostbyaddr", - "gethostbyname", - "gethostname", - "getprotobyname", - "getprotobynumber", - "getservbyname", - "getservbyport", - "inet_ntop", - "inet_pton", "interface_exists", - "ip2long", "is_a", "is_callable", "is_subclass_of", @@ -95,14 +83,10 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "iterator_to_array", "krsort", "ksort", - "long2ip", "method_exists", "natcasesort", "natsort", - "passthru", "pfsockopen", - "php_uname", - "phpversion", "ptr", "ptr_get", "ptr_is_null", @@ -120,11 +104,9 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ptr_write_string", "print_r", "property_exists", - "putenv", "readline", "rsort", "settype", - "shell_exec", "shuffle", "sort", "spl_autoload", @@ -164,7 +146,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_wrapper_register", "stream_wrapper_restore", "stream_wrapper_unregister", - "system", "trait_exists", "uasort", "uksort", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 96052a64e7..c93b795581 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -75,7 +75,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "is_a" | "is_subclass_of" => optional(params, 2), "is_callable" => optional_by_ref(params, 1, &["callable_name"]), - "php_uname" | "readline" | "exit" | "die" => optional(params, 0), + "readline" | "exit" | "die" => optional(params, 0), "fprintf" | "fscanf" => variadic(params, &[]), @@ -146,7 +146,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_callable", 1) => Bool(false), ("is_callable", 2) => Null, - ("php_uname", 0) => String("a"), ("readline", 0) => Null, ("exit" | "die", 0) => Int(0), diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index 4c92c970b1..983b7d1ec1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -28,6 +28,8 @@ pub(in crate::interpreter) enum EvalArea { Json, /// Numeric and mathematical builtins. Math, + /// Network, host, environment, and process builtins. + NetworkEnv, /// PCRE-style regex builtins. Regex, /// String-processing builtins. diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index cb6e81e54e..f972a9ba53 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1576,9 +1576,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "defined" => eval_builtin_defined(args, context, scope, values), "die" | "exit" => eval_builtin_exit(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), - "exec" | "shell_exec" | "system" | "passthru" => { - eval_builtin_process_command(name, args, context, scope, values) - } "eval" => eval_nested_eval(args, context, scope, values), "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), "fopen" => eval_builtin_fopen(args, context, scope, values), @@ -1589,13 +1586,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "function_exists" | "is_callable" => { eval_builtin_function_probe(name, args, context, scope, values) } - "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), - "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), - "gethostname" => eval_builtin_gethostname(args, values), - "getprotobyname" => eval_builtin_getprotobyname(args, context, scope, values), - "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), - "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), - "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), "get_called_class" => eval_builtin_get_called_class(args, context, values), "get_class" => eval_builtin_get_class(args, context, scope, values), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { @@ -1605,15 +1595,9 @@ pub(in crate::interpreter) fn eval_positional_expr_call( "get_resource_id" | "get_resource_type" => { eval_builtin_resource_introspection(name, args, context, scope, values) } - "getenv" => eval_builtin_getenv(args, context, scope, values), - "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), - "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), - "ip2long" => eval_builtin_ip2long(args, context, scope, values), - "php_uname" => eval_builtin_php_uname(args, context, scope, values), - "phpversion" => eval_builtin_phpversion(args, values), "buffer_free" | "buffer_len" | "buffer_new" | "ptr" | "ptr_get" | "ptr_is_null" | "ptr_null" | "ptr_offset" | "ptr_read8" | "ptr_read16" | "ptr_read32" | "ptr_read_string" | "ptr_set" | "ptr_sizeof" | "ptr_write8" | "ptr_write16" @@ -1621,7 +1605,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_raw_memory(name, args, context, scope, values) } "print_r" => eval_builtin_print_r(args, context, scope, values), - "putenv" => eval_builtin_putenv(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "isset" => eval_builtin_isset(args, context, scope, values), "spl_autoload_register" | "spl_autoload_unregister" => { @@ -1694,7 +1677,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_stream_socket_recvfrom(args, context, scope, values) } "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), - "long2ip" => eval_builtin_long2ip(args, context, scope, values), "unset" => eval_builtin_unset(args, context, scope, values), "var_dump" => eval_builtin_var_dump(args, context, scope, values), "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 9909e7353e..b26bf8de07 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -26,9 +26,6 @@ const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs" ), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/network_env.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs" ), @@ -133,6 +130,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "dirname", "disk_free_space", "disk_total_space", + "exec", "explode", "exp", "fdiv", @@ -169,6 +167,14 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "fwrite", "getdate", "getcwd", + "getenv", + "gethostbyaddr", + "gethostbyname", + "gethostname", + "getprotobyname", + "getprotobynumber", + "getservbyname", + "getservbyport", "gettype", "gmdate", "gmmktime", @@ -196,6 +202,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "http_response_code", "hypot", "implode", + "inet_ntop", + "inet_pton", "intdiv", "intval", "is_array", @@ -224,6 +232,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "in_array", "is_writable", "is_writeable", + "ip2long", "json_decode", "json_encode", "json_last_error", @@ -238,6 +247,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "log", "log10", "log2", + "long2ip", "lstat", "ltrim", "max", @@ -253,10 +263,14 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "ord", "pathinfo", "pclose", + "passthru", "pi", + "php_uname", + "phpversion", "popen", "pow", "printf", + "putenv", "preg_match", "preg_match_all", "preg_replace", @@ -282,6 +296,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "rmdir", "scandir", "sha1", + "shell_exec", "sin", "sinh", "sleep", @@ -327,6 +342,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "stream_set_timeout", "stream_set_write_buffer", "stream_supports_lock", + "system", "symlink", "sys_get_temp_dir", "tan", From f252bec6241a20850045619a6015bdcd4c649fae Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 04:30:21 +0200 Subject: [PATCH 1085/1208] refactor(magician): migrate core callable builtins --- .../core/declarations/call_user_func.rs | 17 +++++ .../core/declarations/call_user_func_array.rs | 16 +++++ .../builtins/core/declarations/define.rs | 16 +++++ .../builtins/core/declarations/defined.rs | 16 +++++ .../builtins/core/declarations/die.rs | 18 ++++++ .../builtins/core/declarations/exit.rs | 18 ++++++ .../builtins/core/declarations/mod.rs | 62 +++++++++++++++++++ .../src/interpreter/builtins/core/mod.rs | 13 ++++ .../src/interpreter/builtins/hooks/direct.rs | 5 +- .../src/interpreter/builtins/hooks/values.rs | 7 ++- .../src/interpreter/builtins/mod.rs | 2 + .../interpreter/builtins/registry/binding.rs | 5 -- .../builtins/registry/dispatch/core.rs | 24 ++----- .../src/interpreter/builtins/registry/mod.rs | 18 ++++++ .../interpreter/builtins/registry/names.rs | 6 -- .../builtins/registry/signature.rs | 8 +-- .../src/interpreter/builtins/spec.rs | 2 + .../src/interpreter/expressions.rs | 5 -- tests/builtin_parity_tests.rs | 6 ++ 19 files changed, 219 insertions(+), 45 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func_array.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/define.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/defined.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/die.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/exit.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/mod.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func.rs new file mode 100644 index 0000000000..f5ea6582eb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `call_user_func`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the callable dispatch hook. + +eval_builtin! { + name: "call_user_func", + area: Core, + params: [callback], + variadic: args, + direct: Core, + values: Core, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func_array.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func_array.rs new file mode 100644 index 0000000000..971cd77f10 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func_array.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `call_user_func_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the callable dispatch hook. + +eval_builtin! { + name: "call_user_func_array", + area: Core, + params: [callback, args], + direct: Core, + values: Core, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/define.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/define.rs new file mode 100644 index 0000000000..67cfa0d938 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/define.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `define`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the dynamic-constant hook. + +eval_builtin! { + name: "define", + area: Core, + params: [constant_name, value], + direct: Core, + values: Core, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/defined.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/defined.rs new file mode 100644 index 0000000000..cdbead189f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/defined.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `defined`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the dynamic-constant hook. + +eval_builtin! { + name: "defined", + area: Core, + params: [constant_name], + direct: Core, + values: Core, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/die.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/die.rs new file mode 100644 index 0000000000..cfa348a945 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/die.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `die`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process-control hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "die", + area: Core, + params: [status = EvalBuiltinDefaultValue::Int(0)], + direct: Core, + values: Core, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/exit.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/exit.rs new file mode 100644 index 0000000000..781d471360 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/exit.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `exit`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the process-control hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "exit", + area: Core, + params: [status = EvalBuiltinDefaultValue::Int(0)], + direct: Core, + values: Core, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs new file mode 100644 index 0000000000..916d4bac0d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs @@ -0,0 +1,62 @@ +//! Purpose: +//! Declarative eval registry entries and dispatch adapters for core builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::core` module loading. +//! - `crate::interpreter::builtins::hooks` for migrated core dispatch. +//! +//! Key details: +//! - Adapters preserve the existing direct-call helpers for lexical-scope +//! sensitive callable dispatch and process-control behavior. + +use super::super::super::*; +use super::super::*; + +mod call_user_func; +mod call_user_func_array; +mod define; +mod defined; +mod die; +mod exit; + +/// Dispatches direct expression-level calls for declaratively migrated core builtins. +pub(in crate::interpreter) fn eval_builtin_core_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), + "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "define" => eval_builtin_define(args, context, scope, values), + "defined" => eval_builtin_defined(args, context, scope, values), + "die" | "exit" => eval_builtin_exit(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated core builtins. +pub(in crate::interpreter) fn eval_core_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "call_user_func" => { + eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) + } + "call_user_func_array" => { + let [callback, arg_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_call_user_func_array_with_values(*callback, *arg_array, context, values) + } + "define" => eval_define_result(evaluated_args, context, values), + "defined" => eval_defined_result(evaluated_args, context, values), + "die" | "exit" => eval_exit_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/mod.rs b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs new file mode 100644 index 0000000000..9e04bd5e90 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs @@ -0,0 +1,13 @@ +//! Purpose: +//! Groups declarative eval metadata for core callable, constant, and +//! process-control builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` re-exports used by registry hooks. +//! +//! Key details: +//! - Runtime behavior stays delegated to existing focused interpreter helpers. + +mod declarations; + +pub(in crate::interpreter) use declarations::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index b484ffb8a3..bb4e5b5fa4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -28,7 +28,7 @@ use super::super::{ eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_flip, eval_builtin_array_key_exists, eval_builtin_array_pad, eval_builtin_array_projection, eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, - eval_builtin_array_slice, eval_builtin_array_unique, eval_builtin_cast, + eval_builtin_array_slice, eval_builtin_array_unique, eval_builtin_cast, eval_builtin_core_call, eval_builtin_filesystem_call, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_range, eval_builtin_regex_call, eval_builtin_str_pad, @@ -81,6 +81,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Clamp, /// Dispatches `count(...)`. Count, + /// Dispatches core callable, constant, and process-control builtins. + Core, /// Dispatches `crc32(...)`. Crc32, /// Dispatches `ctype_*` predicates. @@ -225,6 +227,7 @@ impl EvalDirectHook { Self::Chr => eval_builtin_chr(args, context, scope, values), Self::Clamp => eval_builtin_clamp(args, context, scope, values), Self::Count => eval_builtin_count(args, context, scope, values), + Self::Core => eval_builtin_core_call(name, args, context, scope, values), Self::Crc32 => eval_builtin_crc32(args, context, scope, values), Self::Ctype => eval_builtin_ctype(name, args, context, scope, values), Self::Filesystem => eval_builtin_filesystem_call(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index a97bd74c32..8f731df9e0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -19,8 +19,8 @@ use super::super::{ eval_array_projection_result, eval_array_rand_result, eval_array_reverse_result, eval_array_search_result, eval_array_slice_result, eval_array_unique_result, eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, - eval_chr_result, eval_clamp_result, eval_crc32_result, eval_ctype_result, - eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, + eval_chr_result, eval_clamp_result, eval_core_values_result, eval_crc32_result, + eval_ctype_result, eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, eval_float_unary_result, eval_formatting_values_result, eval_gettype_result, eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, @@ -79,6 +79,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Clamp, /// Dispatches `count(...)`. Count, + /// Dispatches core callable, constant, and process-control builtins. + Core, /// Dispatches `crc32(...)`. Crc32, /// Dispatches `ctype_*` predicates. @@ -249,6 +251,7 @@ impl EvalValuesHook { [value, mode] => eval_count_result(*value, Some(*mode), context, values), _ => Err(EvalStatus::RuntimeFatal), }, + Self::Core => eval_core_values_result(name, evaluated_args, context, values), Self::Crc32 => one_arg(evaluated_args, values, eval_crc32_result), Self::Ctype => one_arg(evaluated_args, values, |value, values| { eval_ctype_result(name, value, values) diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index a55aa89dff..c1c45d6d82 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -17,6 +17,7 @@ mod macros; mod array; mod arrays; mod class_metadata; +mod core; mod filesystem; mod formatting; mod hooks; @@ -39,6 +40,7 @@ mod types; pub(super) use arrays::*; pub(super) use class_metadata::*; +pub(super) use core::*; pub(super) use filesystem::*; pub(super) use formatting::*; pub(super) use json::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index fda237b445..e388880ff3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -153,8 +153,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "get_class_vars" => Some(&["class"]), "get_object_vars" => Some(&["object"]), "get_parent_class" => Some(&["object_or_class"]), - "call_user_func" => Some(&["callback", "args"]), - "call_user_func_array" => Some(&["callback", "args"]), "class_alias" => Some(&["class", "alias", "autoload"]), "class_attribute_args" => Some(&["class_name", "attribute_name"]), "class_attribute_names" | "class_get_attributes" => Some(&["class_name"]), @@ -168,9 +166,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "interface_exists" => Some(&["interface", "autoload"]), "trait_exists" => Some(&["trait", "autoload"]), "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), - "define" => Some(&["constant_name", "value"]), - "defined" => Some(&["constant_name"]), - "die" | "exit" => Some(&["status"]), "fgetcsv" => Some(&["stream", "length", "separator"]), "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs index a0b954ddbc..95061d5076 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs @@ -1,17 +1,17 @@ //! Purpose: -//! Dispatches already evaluated core callable, constant, and debug-output builtins by dynamic callable name. +//! Dispatches remaining already evaluated core debug-output builtins by dynamic +//! callable name. //! //! Called from: //! - `crate::interpreter::builtins::registry::dispatch`. //! //! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. +//! - Callable, constant, and process-control builtins migrate through the +//! declarative registry; this file preserves legacy debug-output behavior. -use super::super::super::*; use super::super::*; -/// Attempts to dispatch evaluated core callable, constant, and debug-output builtins. +/// Attempts to dispatch evaluated core debug-output builtins. pub(in crate::interpreter) fn eval_core_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], @@ -21,20 +21,6 @@ pub(in crate::interpreter) fn eval_core_builtin_with_values( let result = match name { "print_r" => eval_print_r_result(evaluated_args, context, values)?, "var_dump" => eval_var_dump_result(evaluated_args, context, values)?, - "call_user_func" => { - return eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) - .map(Some); - } - "call_user_func_array" => { - let [callback, arg_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - return eval_call_user_func_array_with_values(*callback, *arg_array, context, values) - .map(Some); - } - "define" => eval_define_result(evaluated_args, context, values)?, - "defined" => eval_defined_result(evaluated_args, context, values)?, - "die" | "exit" => return eval_exit_result(evaluated_args, values).map(Some), _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 50b82a425e..a89b388d01 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -206,6 +206,8 @@ mod tests { "boolval", "base64_encode", "bin2hex", + "call_user_func", + "call_user_func_array", "checkdate", "chdir", "chgrp", @@ -219,10 +221,14 @@ mod tests { "date", "date_default_timezone_get", "date_default_timezone_set", + "define", + "defined", "dirname", "disk_free_space", "disk_total_space", + "die", "exec", + "exit", "explode", "file", "file_exists", @@ -557,6 +563,18 @@ mod tests { eval_declared_builtin_param_names("getservbyname"), Some(["service", "protocol"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("call_user_func"), + Some(["callback", "args"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("exit"), + Some(["status"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("exit", 0), + Some(EvalBuiltinDefaultValue::Int(0)) + ); assert_eq!( eval_declared_builtin_param_names("getdate"), Some(["timestamp"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 27f538e9b8..121f9030e2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -38,8 +38,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "buffer_free", "buffer_len", "buffer_new", - "call_user_func", - "call_user_func_array", "class_alias", "class_attribute_args", "class_attribute_names", @@ -48,12 +46,8 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "class_implements", "class_parents", "class_uses", - "define", - "defined", - "die", "empty", "enum_exists", - "exit", "fgetcsv", "flock", "fopen", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index c93b795581..602c0f6f3d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -75,7 +75,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "is_a" | "is_subclass_of" => optional(params, 2), "is_callable" => optional_by_ref(params, 1, &["callable_name"]), - "readline" | "exit" | "die" => optional(params, 0), + "readline" => optional(params, 0), "fprintf" | "fscanf" => variadic(params, &[]), @@ -92,11 +92,8 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_filter" => optional(params, 1), "array_reduce" => optional(params, 2), "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), - "call_user_func" => variadic(params, &[]), - "print_r" => optional(params, 1), "var_dump" => variadic(params, &[]), - "fopen" | "fputcsv" => optional(params, 2), "flock" => optional_by_ref(params, 2, &["would_block"]), "fgetcsv" => optional(params, 1), @@ -147,14 +144,11 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_callable", 1) => Bool(false), ("is_callable", 2) => Null, ("readline", 0) => Null, - ("exit" | "die", 0) => Int(0), - ("array_splice", 2) => Null, ("array_splice", 3) => EmptyArray, ("array_filter", 1) => Null, ("array_filter", 2) => Int(0), ("array_reduce", 2) => Null, - ("print_r", 1) => Bool(false), ("fopen", 2) => Bool(false), diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index 983b7d1ec1..d35feaf12d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -20,6 +20,8 @@ pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; pub(in crate::interpreter) enum EvalArea { /// Array and collection builtins. Array, + /// Core callable, constant, and process-control builtins. + Core, /// Filesystem, path, and stream builtins. Filesystem, /// Formatting and display-oriented numeric builtins. diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index f972a9ba53..8cab6305d4 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1548,8 +1548,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_array_key_set(name, args, context, scope, values) } "array_merge" => eval_builtin_array_merge(args, context, scope, values), - "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), - "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), "class_alias" => eval_builtin_class_alias(args, context, scope, values), "class_attribute_args" => { eval_builtin_class_attribute_metadata(name, args, context, scope, values) @@ -1572,9 +1570,6 @@ pub(in crate::interpreter) fn eval_positional_expr_call( eval_builtin_class_like_exists(name, args, context, scope, values) } "is_a" | "is_subclass_of" => eval_builtin_is_a_relation(name, args, context, scope, values), - "define" => eval_builtin_define(args, context, scope, values), - "defined" => eval_builtin_defined(args, context, scope, values), - "die" | "exit" => eval_builtin_exit(args, context, scope, values), "empty" => eval_builtin_empty(args, context, scope, values), "eval" => eval_nested_eval(args, context, scope, values), "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index b26bf8de07..ddcba330b8 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -103,6 +103,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "base64_encode", "bin2hex", "boolval", + "call_user_func", + "call_user_func_array", "ceil", "checkdate", "chdir", @@ -126,11 +128,15 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "date", "date_default_timezone_get", "date_default_timezone_set", + "define", + "defined", "deg2rad", + "die", "dirname", "disk_free_space", "disk_total_space", "exec", + "exit", "explode", "exp", "fdiv", From ec8c7f89728e05566c188d86184f2e6532c0fa4c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 04:47:55 +0200 Subject: [PATCH 1086/1208] refactor(magician): migrate settype builtin --- .../src/interpreter/builtins/hooks/values.rs | 17 +++++---- .../src/interpreter/builtins/macros.rs | 35 +++++++++++++++++++ .../interpreter/builtins/registry/binding.rs | 1 - .../builtins/registry/dispatch/mod.rs | 5 --- .../builtins/registry/dispatch/scalars.rs | 31 ---------------- .../src/interpreter/builtins/registry/mod.rs | 9 +++++ .../interpreter/builtins/registry/names.rs | 1 - .../builtins/registry/signature.rs | 2 -- .../src/interpreter/builtins/types/mod.rs | 1 + .../src/interpreter/builtins/types/settype.rs | 18 ++++++++++ tests/builtin_parity_tests.rs | 4 +-- 11 files changed, 74 insertions(+), 50 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/types/settype.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 8f731df9e0..eadad5cf14 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -25,13 +25,13 @@ use super::super::{ eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_json_values_result, eval_log_result, eval_min_max_result, eval_network_env_values_result, - eval_nl2br_result, eval_range_result, eval_regex_values_result, eval_slashes_result, - eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, - eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, - eval_string_compare_result, eval_string_position_result, eval_string_search_result, - eval_strstr_result, eval_substr_replace_result, eval_substr_result, eval_time_values_result, - eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, - eval_url_encode_result, eval_wordwrap_result, + eval_nl2br_result, eval_range_result, eval_regex_values_result, eval_settype_value_result, + eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, + eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, + eval_string_case_result, eval_string_compare_result, eval_string_position_result, + eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, + eval_time_values_result, eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, + eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; use super::number_format::eval_number_format_values; @@ -141,6 +141,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Range, /// Dispatches regex builtins. Regex, + /// Dispatches by-value `settype(...)` callable calls. + Settype, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, /// Dispatches `sqrt(...)`. @@ -307,6 +309,7 @@ impl EvalValuesHook { }, Self::Range => two_args(evaluated_args, values, eval_range_result), Self::Regex => eval_regex_values_result(name, evaluated_args, context, values), + Self::Settype => two_args(evaluated_args, values, eval_settype_value_result), Self::Slashes => one_arg(evaluated_args, values, |value, values| { eval_slashes_result(name, value, values) }), diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index c64c504167..7dcd0fea75 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -12,6 +12,37 @@ //! over `RuntimeValueOps`. macro_rules! eval_builtin { + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(: $mode:ident)? $(= $default:expr)?),* $(,)?], + by_ref: [$($by_ref:ident),* $(,)?], + direct: none, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param)),*], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: eval_builtin!(@param_by_ref $($mode)?), + }, + )* + ], + variadic: None, + by_ref_params: &[$(eval_builtin!(@name_str $by_ref)),*], + required_param_count: None, + direct: None, + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + } + } + }; + ( name: $name:literal, area: $area:ident, @@ -155,6 +186,10 @@ macro_rules! eval_builtin { "break" }; + (@name_str r#type) => { + "type" + }; + (@name_str $name:ident) => { stringify!($name) }; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index e388880ff3..92c3cf2c0b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -146,7 +146,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "is_callable" => Some(&["value", "syntax_only", "callable_name"]), "buffer_new" => Some(&["length"]), "buffer_len" | "buffer_free" => Some(&["buffer"]), - "settype" => Some(&["var", "type"]), "get_called_class" => Some(&[]), "get_class" => Some(&["object"]), "get_class_methods" => Some(&["object_or_class"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 4052532099..ffd6b312cf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -11,7 +11,6 @@ mod arrays; mod core; mod filesystem; -mod scalars; mod symbols; use super::eval_declared_builtin_values_call; @@ -20,7 +19,6 @@ use super::super::super::*; use arrays::*; use core::*; use filesystem::*; -use scalars::*; use symbols::*; /// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. @@ -47,9 +45,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( { return Ok(Some(result)); } - if let Some(result) = eval_scalars_builtin_with_values(name, evaluated_args, context, values)? { - return Ok(Some(result)); - } if let Some(result) = eval_date_procedural_alias_with_values(name, evaluated_args, context, values)? { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs deleted file mode 100644 index 754f58119d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Purpose: -//! Dispatches remaining already evaluated scalar mutation builtins by dynamic callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. - -use super::super::super::super::*; -use super::super::super::*; - -/// Attempts to dispatch evaluated scalar mutation builtins. -pub(in crate::interpreter) fn eval_scalars_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "settype" => { - let [value, type_name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_settype_value_result(*value, *type_name, values)? - } - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index a89b388d01..823a1b00ca 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -374,6 +374,7 @@ mod tests { "scandir", "sha1", "shell_exec", + "settype", "sleep", "sprintf", "sscanf", @@ -460,6 +461,14 @@ mod tests { eval_declared_builtin_param_names("max"), Some(["value", "values"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("settype"), + Some(["var", "type"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_spec("settype").map(EvalBuiltinSpec::by_ref_param_names), + Some(["var"].as_slice()) + ); for name in ["rand", "mt_rand", "random_int"] { assert_eq!( eval_declared_builtin_param_names(name), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 121f9030e2..0cb597f714 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -100,7 +100,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "property_exists", "readline", "rsort", - "settype", "shuffle", "sort", "spl_autoload", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 602c0f6f3d..10c005e7c7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -64,8 +64,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( let params = eval_builtin_param_names(name)?; Some(match name { "isset" | "unset" => variadic(params, &[]), - "settype" => fixed_by_ref(params, &["var"]), - "class_alias" => optional(params, 2), "class_exists" | "interface_exists" | "trait_exists" | "enum_exists" | "class_implements" | "class_parents" | "class_uses" => optional(params, 1), diff --git a/crates/elephc-magician/src/interpreter/builtins/types/mod.rs b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs index e976ba4309..176de478ab 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs @@ -30,4 +30,5 @@ mod is_real; mod is_resource; mod is_scalar; mod is_string; +mod settype; mod strval; diff --git a/crates/elephc-magician/src/interpreter/builtins/types/settype.rs b/crates/elephc-magician/src/interpreter/builtins/types/settype.rs new file mode 100644 index 0000000000..71c86e402b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/types/settype.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `settype`. +//! +//! Called from: +//! - `crate::interpreter::builtins::types`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path because they +//! need writable `EvalCallArg` targets. + +eval_builtin! { + name: "settype", + area: Types, + params: [var: by_ref, r#type], + by_ref: [var], + direct: none, + values: Settype, +} diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index ddcba330b8..c08e0b4fa9 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -26,9 +26,6 @@ const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs" ), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/scalars.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs" ), @@ -303,6 +300,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "scandir", "sha1", "shell_exec", + "settype", "sin", "sinh", "sleep", From cfeca6cd19eb3fc320ff98f1ff72ccc487e92f8e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 04:59:40 +0200 Subject: [PATCH 1087/1208] refactor(magician): migrate debug output builtins --- .../builtins/core/declarations/mod.rs | 6 +++++ .../builtins/core/declarations/print_r.rs | 18 +++++++++++++ .../builtins/core/declarations/var_dump.rs | 17 ++++++++++++ .../src/interpreter/builtins/core/mod.rs | 4 +-- .../src/interpreter/builtins/hooks/direct.rs | 2 +- .../src/interpreter/builtins/hooks/values.rs | 2 +- .../src/interpreter/builtins/macros.rs | 4 +++ .../interpreter/builtins/registry/binding.rs | 2 -- .../builtins/registry/dispatch/core.rs | 27 ------------------- .../builtins/registry/dispatch/mod.rs | 5 ---- .../src/interpreter/builtins/registry/mod.rs | 18 +++++++++++++ .../interpreter/builtins/registry/names.rs | 2 -- .../builtins/registry/signature.rs | 4 --- .../src/interpreter/builtins/spec.rs | 2 +- tests/builtin_parity_tests.rs | 5 ++-- 15 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/print_r.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/var_dump.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs index 916d4bac0d..9c9adc93fd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs @@ -18,6 +18,8 @@ mod define; mod defined; mod die; mod exit; +mod print_r; +mod var_dump; /// Dispatches direct expression-level calls for declaratively migrated core builtins. pub(in crate::interpreter) fn eval_builtin_core_call( @@ -33,6 +35,8 @@ pub(in crate::interpreter) fn eval_builtin_core_call( "define" => eval_builtin_define(args, context, scope, values), "defined" => eval_builtin_defined(args, context, scope, values), "die" | "exit" => eval_builtin_exit(args, context, scope, values), + "print_r" => eval_builtin_print_r(args, context, scope, values), + "var_dump" => eval_builtin_var_dump(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), } } @@ -57,6 +61,8 @@ pub(in crate::interpreter) fn eval_core_values_result( "define" => eval_define_result(evaluated_args, context, values), "defined" => eval_defined_result(evaluated_args, context, values), "die" | "exit" => eval_exit_result(evaluated_args, values), + "print_r" => eval_print_r_result(evaluated_args, context, values), + "var_dump" => eval_var_dump_result(evaluated_args, context, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/print_r.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/print_r.rs new file mode 100644 index 0000000000..f10282e448 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/print_r.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `print_r`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the debug-output hook. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "print_r", + area: Core, + params: [value, r#return = EvalBuiltinDefaultValue::Bool(false)], + direct: Core, + values: Core, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/var_dump.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/var_dump.rs new file mode 100644 index 0000000000..a9900caff5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/declarations/var_dump.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `var_dump`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the debug-output hook. + +eval_builtin! { + name: "var_dump", + area: Core, + params: [value], + variadic: values, + direct: Core, + values: Core, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/mod.rs b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs index 9e04bd5e90..200c668666 100644 --- a/crates/elephc-magician/src/interpreter/builtins/core/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs @@ -1,6 +1,6 @@ //! Purpose: -//! Groups declarative eval metadata for core callable, constant, and -//! process-control builtins. +//! Groups declarative eval metadata for core callable, constant, +//! process-control, and debug-output builtins. //! //! Called from: //! - `crate::interpreter::builtins` re-exports used by registry hooks. diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index bb4e5b5fa4..e0bd323941 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -81,7 +81,7 @@ pub(in crate::interpreter) enum EvalDirectHook { Clamp, /// Dispatches `count(...)`. Count, - /// Dispatches core callable, constant, and process-control builtins. + /// Dispatches core callable, constant, process-control, and debug-output builtins. Core, /// Dispatches `crc32(...)`. Crc32, diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index eadad5cf14..9ed495ddf3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -79,7 +79,7 @@ pub(in crate::interpreter) enum EvalValuesHook { Clamp, /// Dispatches `count(...)`. Count, - /// Dispatches core callable, constant, and process-control builtins. + /// Dispatches core callable, constant, process-control, and debug-output builtins. Core, /// Dispatches `crc32(...)`. Crc32, diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index 7dcd0fea75..d128457acc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -186,6 +186,10 @@ macro_rules! eval_builtin { "break" }; + (@name_str r#return) => { + "return" + }; + (@name_str r#type) => { "type" }; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index 92c3cf2c0b..c8b537fb82 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -193,8 +193,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } "ptr_write_string" => Some(&["pointer", "string"]), "ptr_sizeof" => Some(&["type"]), - "print_r" => Some(&["value", "return"]), - "var_dump" => Some(&["value", "values"]), "readline" => Some(&["prompt"]), "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), "spl_autoload_unregister" => Some(&["callback"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs deleted file mode 100644 index 95061d5076..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Purpose: -//! Dispatches remaining already evaluated core debug-output builtins by dynamic -//! callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Callable, constant, and process-control builtins migrate through the -//! declarative registry; this file preserves legacy debug-output behavior. - -use super::super::*; - -/// Attempts to dispatch evaluated core debug-output builtins. -pub(in crate::interpreter) fn eval_core_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "print_r" => eval_print_r_result(evaluated_args, context, values)?, - "var_dump" => eval_var_dump_result(evaluated_args, context, values)?, - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index ffd6b312cf..344f7335a2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -9,7 +9,6 @@ //! builtin family and returns `Ok(None)` when the name is outside its domain. mod arrays; -mod core; mod filesystem; mod symbols; @@ -17,7 +16,6 @@ use super::eval_declared_builtin_values_call; use super::super::super::*; use arrays::*; -use core::*; use filesystem::*; use symbols::*; @@ -53,8 +51,5 @@ pub(in crate::interpreter) fn eval_builtin_with_values( if let Some(result) = eval_symbols_builtin_with_values(name, evaluated_args, context, values)? { return Ok(Some(result)); } - if let Some(result) = eval_core_builtin_with_values(name, evaluated_args, context, values)? { - return Ok(Some(result)); - } Ok(None) } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 823a1b00ca..1cf313af8e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -350,6 +350,7 @@ mod tests { "php_uname", "phpversion", "popen", + "print_r", "printf", "preg_match", "preg_match_all", @@ -415,6 +416,7 @@ mod tests { "umask", "unlink", "usleep", + "var_dump", "vprintf", "vsprintf", "wordwrap", @@ -469,6 +471,22 @@ mod tests { eval_declared_builtin_spec("settype").map(EvalBuiltinSpec::by_ref_param_names), Some(["var"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("print_r"), + Some(["value", "return"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("print_r", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("var_dump"), + Some(["value", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("var_dump").map(|shape| shape.variadic), + Some(Some("values")) + ); for name in ["rand", "mt_rand", "random_int"] { assert_eq!( eval_declared_builtin_param_names(name), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 0cb597f714..4ed1bba5b7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -96,7 +96,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ptr_write16", "ptr_write32", "ptr_write_string", - "print_r", "property_exists", "readline", "rsort", @@ -144,7 +143,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "uksort", "unset", "usort", - "var_dump", "vfprintf", ]; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 10c005e7c7..ed532ad655 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -90,8 +90,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "array_filter" => optional(params, 1), "array_reduce" => optional(params, 2), "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), - "print_r" => optional(params, 1), - "var_dump" => variadic(params, &[]), "fopen" | "fputcsv" => optional(params, 2), "flock" => optional_by_ref(params, 2, &["would_block"]), "fgetcsv" => optional(params, 1), @@ -147,8 +145,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("array_filter", 1) => Null, ("array_filter", 2) => Int(0), ("array_reduce", 2) => Null, - ("print_r", 1) => Bool(false), - ("fopen", 2) => Bool(false), ("fopen", 3) => Null, ("flock", 2) => Null, diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index d35feaf12d..de457b5ff1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -20,7 +20,7 @@ pub(in crate::interpreter) use super::registry::EvalBuiltinDefaultValue; pub(in crate::interpreter) enum EvalArea { /// Array and collection builtins. Array, - /// Core callable, constant, and process-control builtins. + /// Core callable, constant, process-control, and debug-output builtins. Core, /// Filesystem, path, and stream builtins. Filesystem, diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index c08e0b4fa9..8565b6df8d 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -20,9 +20,6 @@ const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs" ), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/core.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs" ), @@ -272,6 +269,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "phpversion", "popen", "pow", + "print_r", "printf", "putenv", "preg_match", @@ -363,6 +361,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "urldecode", "urlencode", "usleep", + "var_dump", "vprintf", "vsprintf", "wordwrap", From 56f00116a130928ea64ea61011a44f646fede579 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 05:22:02 +0200 Subject: [PATCH 1088/1208] refactor(magician): migrate non-mutating array builtins --- .../interpreter/builtins/array/array_chunk.rs | 16 ++ .../builtins/array/array_column.rs | 16 ++ .../builtins/array/array_combine.rs | 16 ++ .../interpreter/builtins/array/array_diff.rs | 17 ++ .../builtins/array/array_diff_key.rs | 17 ++ .../interpreter/builtins/array/array_fill.rs | 16 ++ .../builtins/array/array_fill_keys.rs | 16 ++ .../builtins/array/array_filter.rs | 22 +++ .../builtins/array/array_intersect.rs | 17 ++ .../builtins/array/array_intersect_key.rs | 17 ++ .../interpreter/builtins/array/array_map.rs | 17 ++ .../interpreter/builtins/array/array_merge.rs | 17 ++ .../builtins/array/array_reduce.rs | 18 ++ .../builtins/array/iterator_apply.rs | 18 ++ .../builtins/array/iterator_count.rs | 16 ++ .../builtins/array/iterator_to_array.rs | 18 ++ .../src/interpreter/builtins/array/mod.rs | 19 +++ .../builtins/array/non_mutating.rs | 155 ++++++++++++++++++ .../src/interpreter/builtins/hooks/direct.rs | 25 +-- .../src/interpreter/builtins/hooks/values.rs | 4 + .../src/interpreter/builtins/mod.rs | 1 + .../interpreter/builtins/registry/binding.rs | 15 -- .../builtins/registry/dispatch/arrays.rs | 104 +----------- .../src/interpreter/builtins/registry/mod.rs | 44 +++++ .../interpreter/builtins/registry/names.rs | 16 -- .../builtins/registry/signature.rs | 14 -- tests/builtin_parity_tests.rs | 16 ++ tests/codegen/eval.rs | 3 +- 28 files changed, 531 insertions(+), 159 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_column.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_map.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs new file mode 100644 index 0000000000..38c9eed202 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_chunk`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_chunk", + area: Array, + params: [array, length], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs new file mode 100644 index 0000000000..fe9d23e838 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_column`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_column", + area: Array, + params: [array, column_key], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs new file mode 100644 index 0000000000..3b438cd7bf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_combine`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_combine", + area: Array, + params: [keys, values], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs new file mode 100644 index 0000000000..3b0f19148f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_diff`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_diff", + area: Array, + params: [array], + variadic: arrays, + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs new file mode 100644 index 0000000000..3101de8269 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_diff_key`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_diff_key", + area: Array, + params: [array], + variadic: arrays, + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs new file mode 100644 index 0000000000..77f95d8e4b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_fill`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_fill", + area: Array, + params: [start_index, count, value], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs new file mode 100644 index 0000000000..e2325a6251 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `array_fill_keys`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_fill_keys", + area: Array, + params: [keys, value], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs new file mode 100644 index 0000000000..9dd3a536d2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `array_filter`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "array_filter", + area: Array, + params: [ + array, + callback = EvalBuiltinDefaultValue::Null, + mode = EvalBuiltinDefaultValue::Int(0), + ], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs new file mode 100644 index 0000000000..698597ffc6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_intersect`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_intersect", + area: Array, + params: [array], + variadic: arrays, + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs new file mode 100644 index 0000000000..7dbc1eb7f2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_intersect_key`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_intersect_key", + area: Array, + params: [array], + variadic: arrays, + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs new file mode 100644 index 0000000000..00b1bb221a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_map`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_map", + area: Array, + params: [callback, array], + variadic: arrays, + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs new file mode 100644 index 0000000000..d4b153d646 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_merge`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "array_merge", + area: Array, + params: [], + variadic: arrays, + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs new file mode 100644 index 0000000000..e931021fc6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `array_reduce`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "array_reduce", + area: Array, + params: [array, callback, initial = EvalBuiltinDefaultValue::Null], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs new file mode 100644 index 0000000000..d5a98fd713 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `iterator_apply`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "iterator_apply", + area: Array, + params: [iterator, callback, args = EvalBuiltinDefaultValue::Null], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs new file mode 100644 index 0000000000..2646d010ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `iterator_count`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +eval_builtin! { + name: "iterator_count", + area: Array, + params: [iterator], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs new file mode 100644 index 0000000000..3c9a0a3f79 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `iterator_to_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the non-mutating array hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "iterator_to_array", + area: Array, + params: [iterator, preserve_keys = EvalBuiltinDefaultValue::Bool(true)], + direct: Array, + values: Array, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs index 70a35983ee..b73a27513a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -8,12 +8,25 @@ //! Key details: //! - Leaf files register metadata through `eval_builtin!`. +mod array_chunk; +mod array_column; +mod array_combine; +mod array_diff; +mod array_diff_key; +mod array_fill; +mod array_fill_keys; +mod array_filter; mod array_flip; +mod array_intersect; +mod array_intersect_key; mod array_key_exists; mod array_keys; +mod array_map; +mod array_merge; mod array_pad; mod array_product; mod array_rand; +mod array_reduce; mod array_reverse; mod array_search; mod array_slice; @@ -22,4 +35,10 @@ mod array_unique; mod array_values; mod count; mod in_array; +mod iterator_apply; +mod iterator_count; +mod iterator_to_array; +mod non_mutating; mod range; + +pub(in crate::interpreter) use non_mutating::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs b/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs new file mode 100644 index 0000000000..d8828c69f1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs @@ -0,0 +1,155 @@ +//! Purpose: +//! Shared registry hooks for non-mutating array and iterator builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Mutating/by-reference array builtins stay on the source-sensitive legacy +//! dispatch path until their writable-target handling is migrated. + +use super::super::super::*; +use super::super::*; + +/// Dispatches direct non-mutating array and iterator calls from declarative specs. +pub(in crate::interpreter) fn eval_builtin_array_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), + "array_column" => eval_builtin_array_column(args, context, scope, values), + "array_combine" => eval_builtin_array_combine(args, context, scope, values), + "array_diff" | "array_intersect" => { + eval_builtin_array_value_set(name, args, context, scope, values) + } + "array_diff_key" | "array_intersect_key" => { + eval_builtin_array_key_set(name, args, context, scope, values) + } + "array_fill" => eval_builtin_array_fill(args, context, scope, values), + "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), + "array_filter" => eval_builtin_array_filter(args, context, scope, values), + "array_map" => eval_builtin_array_map(args, context, scope, values), + "array_merge" => eval_builtin_array_merge(args, context, scope, values), + "array_reduce" => eval_builtin_array_reduce(args, context, scope, values), + "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), + "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), + "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated non-mutating array and iterator calls from declarative specs. +pub(in crate::interpreter) fn eval_array_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "array_chunk" => { + let [array, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_chunk_result(*array, *length, values) + } + "array_column" => { + let [array, column_key] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_column_result(*array, *column_key, values) + } + "array_combine" => { + let [keys, values_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_combine_result(*keys, *values_array, values) + } + "array_diff" | "array_intersect" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_value_set_result(name, *left, *right, values) + } + "array_diff_key" | "array_intersect_key" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_key_set_result(name, *left, *right, values) + } + "array_fill" => { + let [start, count, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_result(*start, *count, *value, values) + } + "array_fill_keys" => { + let [keys, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_fill_keys_result(*keys, *value, values) + } + "array_filter" => match evaluated_args { + [array] => eval_array_filter_result(*array, None, None, context, values), + [array, callback] => { + eval_array_filter_result(*array, Some(*callback), None, context, values) + } + [array, callback, mode] => { + eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "array_map" => { + let Some((callback, arrays)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_map_result(*callback, arrays, context, values) + } + "array_merge" => { + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_array_merge_result(*left, *right, values) + } + "array_reduce" => match evaluated_args { + [array, callback] => { + let initial = values.null()?; + eval_array_reduce_result(*array, *callback, initial, context, values) + } + [array, callback, initial] => { + eval_array_reduce_result(*array, *callback, *initial, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "iterator_apply" => match evaluated_args { + [iterator, callback] => { + let callback = eval_callable(*callback, context, values)?; + eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, args] => { + let callback = eval_callable(*callback, context, values)?; + let callback_args = eval_iterator_apply_arg_values(*args, context, values)?; + eval_iterator_apply_result(*iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "iterator_count" => { + let [iterator] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_iterator_count_result(*iterator, values) + } + "iterator_to_array" => match evaluated_args { + [iterator] => eval_iterator_to_array_result(*iterator, true, values), + [iterator, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_iterator_to_array_result(*iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index e0bd323941..215263d0e4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -26,17 +26,17 @@ use super::super::super::{ }; use super::super::{ eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_flip, - eval_builtin_array_key_exists, eval_builtin_array_pad, eval_builtin_array_projection, - eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, - eval_builtin_array_slice, eval_builtin_array_unique, eval_builtin_cast, eval_builtin_core_call, - eval_builtin_filesystem_call, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, - eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_network_env_call, - eval_builtin_nl2br, eval_builtin_range, eval_builtin_regex_call, eval_builtin_str_pad, - eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_stream_bool_predicate, - eval_builtin_stream_introspection, eval_builtin_string_case, eval_builtin_string_compare, - eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, - eval_builtin_strstr, eval_builtin_substr, eval_builtin_substr_replace, eval_builtin_time_call, - eval_builtin_trim_like, + eval_builtin_array_call, eval_builtin_array_key_exists, eval_builtin_array_pad, + eval_builtin_array_projection, eval_builtin_array_rand, eval_builtin_array_reverse, + eval_builtin_array_search, eval_builtin_array_slice, eval_builtin_array_unique, + eval_builtin_cast, eval_builtin_core_call, eval_builtin_filesystem_call, + eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, + eval_builtin_json_call, eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_range, + eval_builtin_regex_call, eval_builtin_str_pad, eval_builtin_str_replace, + eval_builtin_str_split, eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, + eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, + eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, + eval_builtin_substr_replace, eval_builtin_time_call, eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, }; @@ -47,6 +47,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Abs, /// Dispatches `array_sum(...)` and `array_product(...)`. ArrayAggregate, + /// Dispatches non-mutating array and iterator builtins. + Array, /// Dispatches `array_flip(...)`. ArrayFlip, /// Dispatches `array_key_exists(...)`. @@ -210,6 +212,7 @@ impl EvalDirectHook { match self { Self::Abs => eval_builtin_abs(args, context, scope, values), Self::ArrayAggregate => eval_builtin_array_aggregate(name, args, context, scope, values), + Self::Array => eval_builtin_array_call(name, args, context, scope, values), Self::ArrayFlip => eval_builtin_array_flip(args, context, scope, values), Self::ArrayKeyExists => eval_builtin_array_key_exists(args, context, scope, values), Self::ArrayPad => eval_builtin_array_pad(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 9ed495ddf3..df07f58af5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -18,6 +18,7 @@ use super::super::{ eval_array_aggregate_result, eval_array_flip_result, eval_array_pad_result, eval_array_projection_result, eval_array_rand_result, eval_array_reverse_result, eval_array_search_result, eval_array_slice_result, eval_array_unique_result, + eval_array_values_result, eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, eval_chr_result, eval_clamp_result, eval_core_values_result, eval_crc32_result, eval_ctype_result, eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, @@ -45,6 +46,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Abs, /// Dispatches `array_sum(...)` and `array_product(...)`. ArrayAggregate, + /// Dispatches non-mutating array and iterator builtins. + Array, /// Dispatches `array_flip(...)`. ArrayFlip, /// Dispatches `array_key_exists(...)`. @@ -211,6 +214,7 @@ impl EvalValuesHook { Self::ArrayAggregate => one_arg(evaluated_args, values, |array, values| { eval_array_aggregate_result(name, array, values) }), + Self::Array => eval_array_values_result(name, evaluated_args, context, values), Self::ArrayFlip => one_arg(evaluated_args, values, eval_array_flip_result), Self::ArrayKeyExists => two_args(evaluated_args, values, |key, array, values| { values.array_key_exists(key, array) diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index c1c45d6d82..9658c32e0c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -39,6 +39,7 @@ mod time; mod types; pub(super) use arrays::*; +pub(super) use array::*; pub(super) use class_metadata::*; pub(super) use core::*; pub(super) use filesystem::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index c8b537fb82..fce36547d1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -124,22 +124,10 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } match name { - "array_chunk" => Some(&["array", "length"]), - "array_column" => Some(&["array", "column_key"]), - "array_combine" => Some(&["keys", "values"]), - "array_fill" => Some(&["start_index", "count", "value"]), - "array_fill_keys" => Some(&["keys", "value"]), - "array_filter" => Some(&["array", "callback", "mode"]), - "array_map" => Some(&["callback", "array", "arrays"]), - "array_reduce" => Some(&["array", "callback", "initial"]), "array_walk" => Some(&["array", "callback"]), "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), "array_pop" | "array_shift" | "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => Some(&["array"]), - "array_merge" => Some(&["arrays"]), - "array_diff" | "array_intersect" | "array_diff_key" | "array_intersect_key" => { - Some(&["array", "arrays"]) - } "array_push" | "array_unshift" => Some(&["array", "values"]), "array_splice" => Some(&["array", "offset", "length", "replacement"]), "empty" => Some(&["value"]), @@ -177,9 +165,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "function_exists" => Some(&["function"]), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), "get_resource_id" | "get_resource_type" => Some(&["resource"]), - "iterator_apply" => Some(&["iterator", "callback", "args"]), - "iterator_count" => Some(&["iterator"]), - "iterator_to_array" => Some(&["iterator", "preserve_keys"]), "isset" | "unset" => Some(&["var", "vars"]), "ptr" => Some(&["value"]), "ptr_null" => Some(&[]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs index edd7f5e5bf..9dff10f02b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Dispatches already evaluated array and iterator builtins by dynamic callable name. +//! Dispatches remaining already evaluated mutating array builtins by dynamic callable name. //! //! Called from: //! - `crate::interpreter::builtins::registry::dispatch`. @@ -11,7 +11,7 @@ use super::super::super::*; use super::super::*; -/// Attempts to dispatch evaluated array and iterator builtins. +/// Attempts to dispatch evaluated mutating array builtins. pub(in crate::interpreter) fn eval_arrays_builtin_with_values( name: &str, evaluated_args: &[RuntimeCellHandle], @@ -19,62 +19,6 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "array_combine" => { - let [keys, values_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_combine_result(*keys, *values_array, values)? - } - "array_column" => { - let [array, column_key] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_column_result(*array, *column_key, values)? - } - "array_chunk" => { - let [array, length] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_chunk_result(*array, *length, values)? - } - "array_fill" => { - let [start, count, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_result(*start, *count, *value, values)? - } - "array_fill_keys" => { - let [keys, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_keys_result(*keys, *value, values)? - } - "array_filter" => match evaluated_args { - [array] => eval_array_filter_result(*array, None, None, context, values)?, - [array, callback] => { - eval_array_filter_result(*array, Some(*callback), None, context, values)? - } - [array, callback, mode] => { - eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "array_map" => { - let Some((callback, arrays)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_map_result(*callback, arrays, context, values)? - } - "array_reduce" => match evaluated_args { - [array, callback] => { - let initial = values.null()?; - eval_array_reduce_result(*array, *callback, initial, context, values)? - } - [array, callback, initial] => { - eval_array_reduce_result(*array, *callback, *initial, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, "array_walk" => { let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -137,50 +81,6 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( ))?; eval_user_sort_value_result(name, *array, *callback, context, values)? } - "array_diff" | "array_intersect" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_value_set_result(name, *left, *right, values)? - } - "array_diff_key" | "array_intersect_key" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_key_set_result(name, *left, *right, values)? - } - "array_merge" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_merge_result(*left, *right, values)? - } - "iterator_apply" => match evaluated_args { - [iterator, callback] => { - let callback = eval_callable(*callback, context, values)?; - eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values)? - } - [iterator, callback, args] => { - let callback = eval_callable(*callback, context, values)?; - let callback_args = eval_iterator_apply_arg_values(*args, context, values)?; - eval_iterator_apply_result(*iterator, &callback, callback_args, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "iterator_count" => { - let [iterator] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_iterator_count_result(*iterator, values)? - } - "iterator_to_array" => match evaluated_args { - [iterator] => eval_iterator_to_array_result(*iterator, true, values)?, - [iterator, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_iterator_to_array_result(*iterator, preserve_keys, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 1cf313af8e..5cf8259294 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -198,8 +198,21 @@ mod tests { "abs", "acos", "addslashes", + "array_chunk", + "array_column", + "array_combine", + "array_diff", + "array_diff_key", + "array_fill", + "array_fill_keys", + "array_filter", "array_key_exists", "array_keys", + "array_intersect", + "array_intersect_key", + "array_map", + "array_merge", + "array_reduce", "array_reverse", "array_sum", "basename", @@ -296,6 +309,9 @@ mod tests { "inet_ntop", "inet_pton", "intval", + "iterator_apply", + "iterator_count", + "iterator_to_array", "is_array", "is_bool", "is_dir", @@ -463,6 +479,34 @@ mod tests { eval_declared_builtin_param_names("max"), Some(["value", "values"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("array_map"), + Some(["callback", "array", "arrays"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("array_map").map(|shape| shape.variadic), + Some(Some("arrays")) + ); + assert_eq!( + eval_declared_builtin_param_names("array_filter"), + Some(["array", "callback", "mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("array_filter", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("array_filter", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("iterator_to_array"), + Some(["iterator", "preserve_keys"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("iterator_to_array", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); assert_eq!( eval_declared_builtin_param_names("settype"), Some(["var", "type"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 4ed1bba5b7..69ed39af53 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -14,21 +14,8 @@ use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; /// PHP-visible builtin names implemented by the eval interpreter. pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[ - "array_chunk", - "array_column", - "array_combine", - "array_diff", - "array_diff_key", - "array_fill", - "array_fill_keys", - "array_filter", - "array_intersect", - "array_intersect_key", - "array_map", - "array_merge", "array_pop", "array_push", - "array_reduce", "array_shift", "array_splice", "array_unshift", @@ -72,9 +59,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "is_callable", "is_subclass_of", "isset", - "iterator_apply", - "iterator_count", - "iterator_to_array", "krsort", "ksort", "method_exists", diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index ed532ad655..134752fc9e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -67,8 +67,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "class_alias" => optional(params, 2), "class_exists" | "interface_exists" | "trait_exists" | "enum_exists" | "class_implements" | "class_parents" | "class_uses" => optional(params, 1), - "iterator_to_array" => optional(params, 1), - "iterator_apply" => optional(params, 2), "get_class" | "get_parent_class" => optional(params, 0), "is_a" | "is_subclass_of" => optional(params, 2), @@ -81,14 +79,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "sort" | "rsort" | "shuffle" | "natsort" | "natcasesort" | "asort" | "arsort" | "ksort" | "krsort" => fixed_by_ref(params, &["array"]), "array_push" | "array_unshift" => variadic(params, &["array"]), - "array_merge" => variadic(params, &[]), - "array_diff" | "array_intersect" | "array_diff_key" | "array_intersect_key" => { - variadic(params, &[]) - } "array_splice" => optional_by_ref(params, 2, &["array"]), - "array_map" => variadic(params, &[]), - "array_filter" => optional(params, 1), - "array_reduce" => optional(params, 2), "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "fopen" | "fputcsv" => optional(params, 2), "flock" => optional_by_ref(params, 2, &["would_block"]), @@ -131,8 +122,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( | "class_implements" | "class_parents" | "class_uses", 1, ) => Bool(true), - ("iterator_to_array", 1) => Bool(true), - ("iterator_apply", 2) => Null, ("get_class" | "get_parent_class", 0) => Null, ("is_a", 2) => Bool(false), ("is_subclass_of", 2) => Bool(true), @@ -142,9 +131,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("readline", 0) => Null, ("array_splice", 2) => Null, ("array_splice", 3) => EmptyArray, - ("array_filter", 1) => Null, - ("array_filter", 2) => Int(0), - ("array_reduce", 2) => Null, ("fopen", 2) => Bool(false), ("fopen", 3) => Null, ("flock", 2) => Null, diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 8565b6df8d..e8c8c9c018 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -77,12 +77,25 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "abs", "acos", "addslashes", + "array_chunk", + "array_column", + "array_combine", + "array_diff", + "array_diff_key", + "array_fill", + "array_fill_keys", + "array_filter", "array_flip", + "array_intersect", + "array_intersect_key", "array_key_exists", "array_keys", + "array_map", + "array_merge", "array_pad", "array_product", "array_rand", + "array_reduce", "array_reverse", "array_search", "array_slice", @@ -238,6 +251,9 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "json_last_error", "json_last_error_msg", "json_validate", + "iterator_apply", + "iterator_count", + "iterator_to_array", "lchgrp", "lchown", "lcfirst", diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 67865a28fc..4dc3ca43fe 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -5950,7 +5950,8 @@ fn test_eval_dispatches_array_walk_builtin_call() { let out = compile_and_run( r#" 2, "b" => 3], "eval_walk_show") ? "T:" : "F:"; +$walk = ["a" => 2, "b" => 3]; +echo array_walk($walk, "eval_walk_show") ? "T:" : "F:"; $call = call_user_func("array_walk", [4, 5], "eval_walk_show"); $spread = call_user_func_array("array_walk", ["array" => ["z" => 6], "callback" => "eval_walk_show"]); echo function_exists("array_walk");'); From 1f55f29a613c92908b43744a14285fc012e8a207 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 05:35:57 +0200 Subject: [PATCH 1089/1208] refactor(magician): migrate mutating array builtins --- .../interpreter/builtins/array/array_pop.rs | 17 ++++++ .../interpreter/builtins/array/array_push.rs | 18 ++++++ .../interpreter/builtins/array/array_shift.rs | 17 ++++++ .../builtins/array/array_splice.rs | 24 ++++++++ .../builtins/array/array_unshift.rs | 18 ++++++ .../interpreter/builtins/array/array_walk.rs | 17 ++++++ .../src/interpreter/builtins/array/arsort.rs | 17 ++++++ .../src/interpreter/builtins/array/asort.rs | 17 ++++++ .../src/interpreter/builtins/array/krsort.rs | 17 ++++++ .../src/interpreter/builtins/array/ksort.rs | 17 ++++++ .../src/interpreter/builtins/array/mod.rs | 20 +++++++ .../dispatch/arrays.rs => array/mutating.rs} | 56 +++++++++---------- .../interpreter/builtins/array/natcasesort.rs | 17 ++++++ .../src/interpreter/builtins/array/natsort.rs | 17 ++++++ .../src/interpreter/builtins/array/rsort.rs | 17 ++++++ .../src/interpreter/builtins/array/shuffle.rs | 17 ++++++ .../src/interpreter/builtins/array/sort.rs | 17 ++++++ .../src/interpreter/builtins/array/uasort.rs | 17 ++++++ .../src/interpreter/builtins/array/uksort.rs | 17 ++++++ .../src/interpreter/builtins/array/usort.rs | 17 ++++++ .../src/interpreter/builtins/hooks/values.rs | 7 ++- .../src/interpreter/builtins/macros.rs | 32 +++++++++++ .../interpreter/builtins/registry/binding.rs | 6 -- .../builtins/registry/dispatch/mod.rs | 5 -- .../src/interpreter/builtins/registry/mod.rs | 37 ++++++++++++ .../interpreter/builtins/registry/names.rs | 18 ------ .../builtins/registry/signature.rs | 16 ------ tests/builtin_parity_tests.rs | 21 ++++++- 28 files changed, 455 insertions(+), 78 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_push.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/arsort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/asort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/krsort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/ksort.rs rename crates/elephc-magician/src/interpreter/builtins/{registry/dispatch/arrays.rs => array/mutating.rs} (65%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/natsort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/rsort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/sort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/uasort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/uksort.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/usort.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs new file mode 100644 index 0000000000..f9799b36c6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_pop`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "array_pop", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs new file mode 100644 index 0000000000..565b89b67b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `array_push`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "array_push", + area: Array, + params: [array: by_ref], + variadic: values, + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs new file mode 100644 index 0000000000..3fdb97b56b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_shift`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "array_shift", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs new file mode 100644 index 0000000000..24439e8a35 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs @@ -0,0 +1,24 @@ +//! Purpose: +//! Declarative eval registry entry for `array_splice`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "array_splice", + area: Array, + params: [ + array: by_ref, + offset, + length = EvalBuiltinDefaultValue::Null, + replacement = EvalBuiltinDefaultValue::EmptyArray, + ], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs new file mode 100644 index 0000000000..27ac824bc6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `array_unshift`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "array_unshift", + area: Array, + params: [array: by_ref], + variadic: values, + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs new file mode 100644 index 0000000000..f54859cdea --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `array_walk`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "array_walk", + area: Array, + params: [array: by_ref, callback], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs new file mode 100644 index 0000000000..22d6c09899 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `arsort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "arsort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/asort.rs b/crates/elephc-magician/src/interpreter/builtins/array/asort.rs new file mode 100644 index 0000000000..dd4f794337 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/asort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `asort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "asort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs new file mode 100644 index 0000000000..c003a5c309 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `krsort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "krsort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs b/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs new file mode 100644 index 0000000000..2fc106f2ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `ksort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "ksort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs index b73a27513a..a6a7607989 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -23,22 +23,42 @@ mod array_key_exists; mod array_keys; mod array_map; mod array_merge; +mod array_pop; mod array_pad; mod array_product; +mod array_push; mod array_rand; mod array_reduce; mod array_reverse; mod array_search; +mod array_shift; mod array_slice; +mod array_splice; mod array_sum; mod array_unique; +mod array_unshift; mod array_values; +mod array_walk; +mod arsort; +mod asort; mod count; mod in_array; mod iterator_apply; mod iterator_count; mod iterator_to_array; +mod krsort; +mod ksort; +mod mutating; +mod natcasesort; +mod natsort; mod non_mutating; mod range; +mod rsort; +mod shuffle; +mod sort; +mod uasort; +mod uksort; +mod usort; +pub(in crate::interpreter) use mutating::*; pub(in crate::interpreter) use non_mutating::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs b/crates/elephc-magician/src/interpreter/builtins/array/mutating.rs similarity index 65% rename from crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs rename to crates/elephc-magician/src/interpreter/builtins/array/mutating.rs index 9dff10f02b..587c28c8d0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mutating.rs @@ -1,24 +1,24 @@ //! Purpose: -//! Dispatches remaining already evaluated mutating array builtins by dynamic callable name. +//! Values-only registry hook for mutating/by-reference array builtins. //! //! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. +//! - Direct calls stay on the source-sensitive writable-target path; this hook +//! preserves callable by-value warning behavior. use super::super::super::*; use super::super::*; -/// Attempts to dispatch evaluated mutating array builtins. -pub(in crate::interpreter) fn eval_arrays_builtin_with_values( +/// Dispatches by-value callable calls for mutating array builtins. +pub(in crate::interpreter) fn eval_array_mutating_values_result( name: &str, evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { +) -> Result { + match name { "array_walk" => { let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -26,25 +26,21 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( values.warning( "array_walk(): Argument #1 ($array) must be passed by reference, value given", )?; - eval_array_walk_result(*array, *callback, context, values)? + eval_array_walk_result(*array, *callback, context, values) } "array_pop" | "array_shift" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_pop_shift_value_result(name, *array, values)? + warn_array_by_value(name, values)?; + eval_array_pop_shift_value_result(name, *array, values) } "array_push" | "array_unshift" => { let Some((array, inserted)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_push_unshift_count_result(*array, inserted.len(), values)? + warn_array_by_value(name, values)?; + eval_array_push_unshift_count_result(*array, inserted.len(), values) } "array_splice" => { let result = match evaluated_args { @@ -60,28 +56,30 @@ pub(in crate::interpreter) fn eval_arrays_builtin_with_values( values.warning( "array_splice(): Argument #1 ($array) must be passed by reference, value given", )?; - result + Ok(result) } "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_array_sort_value_result(*array, values)? + warn_array_by_value(name, values)?; + eval_array_sort_value_result(*array, values) } "uasort" | "uksort" | "usort" => { let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - ))?; - eval_user_sort_value_result(name, *array, *callback, context, values)? + warn_array_by_value(name, values)?; + eval_user_sort_value_result(name, *array, *callback, context, values) } - _ => return Ok(None), - }; - Ok(Some(result)) + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Emits the standard by-value warning for array mutators. +fn warn_array_by_value(name: &str, values: &mut impl RuntimeValueOps) -> Result<(), EvalStatus> { + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + )) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs b/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs new file mode 100644 index 0000000000..1d54e3a652 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `natcasesort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "natcasesort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs new file mode 100644 index 0000000000..9997870ab2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `natsort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "natsort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs new file mode 100644 index 0000000000..569279bddb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `rsort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "rsort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs b/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs new file mode 100644 index 0000000000..b39971759f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `shuffle`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "shuffle", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/sort.rs b/crates/elephc-magician/src/interpreter/builtins/array/sort.rs new file mode 100644 index 0000000000..c2728bda99 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/sort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `sort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "sort", + area: Array, + params: [array: by_ref], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs b/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs new file mode 100644 index 0000000000..0349dc7025 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `uasort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "uasort", + area: Array, + params: [array: by_ref, callback], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs b/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs new file mode 100644 index 0000000000..ecdca25c50 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `uksort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "uksort", + area: Array, + params: [array: by_ref, callback], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/usort.rs b/crates/elephc-magician/src/interpreter/builtins/array/usort.rs new file mode 100644 index 0000000000..2bfb80bd80 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/usort.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `usort`. +//! +//! Called from: +//! - `crate::interpreter::builtins::array`. +//! +//! Key details: +//! - Direct calls stay on the source-sensitive by-reference path. + +eval_builtin! { + name: "usort", + area: Array, + params: [array: by_ref, callback], + by_ref: [array], + direct: none, + values: ArrayMutating, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index df07f58af5..81eda84fc8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -18,7 +18,7 @@ use super::super::{ eval_array_aggregate_result, eval_array_flip_result, eval_array_pad_result, eval_array_projection_result, eval_array_rand_result, eval_array_reverse_result, eval_array_search_result, eval_array_slice_result, eval_array_unique_result, - eval_array_values_result, + eval_array_mutating_values_result, eval_array_values_result, eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, eval_chr_result, eval_clamp_result, eval_core_values_result, eval_crc32_result, eval_ctype_result, eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, @@ -48,6 +48,8 @@ pub(in crate::interpreter) enum EvalValuesHook { ArrayAggregate, /// Dispatches non-mutating array and iterator builtins. Array, + /// Dispatches by-value calls for mutating array builtins. + ArrayMutating, /// Dispatches `array_flip(...)`. ArrayFlip, /// Dispatches `array_key_exists(...)`. @@ -215,6 +217,9 @@ impl EvalValuesHook { eval_array_aggregate_result(name, array, values) }), Self::Array => eval_array_values_result(name, evaluated_args, context, values), + Self::ArrayMutating => { + eval_array_mutating_values_result(name, evaluated_args, context, values) + } Self::ArrayFlip => one_arg(evaluated_args, values, eval_array_flip_result), Self::ArrayKeyExists => two_args(evaluated_args, values, |key, array, values| { values.array_key_exists(key, array) diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index d128457acc..1877253ac2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -74,6 +74,38 @@ macro_rules! eval_builtin { } }; + ( + name: $name:literal, + area: $area:ident, + params: [$($param:ident $(: $mode:ident)? $(= $default:expr)?),* $(,)?], + variadic: $variadic:ident, + by_ref: [$($by_ref:ident),* $(,)?], + direct: none, + values: $values:ident $(,)? + ) => { + inventory::submit! { + $crate::interpreter::builtins::spec::EvalBuiltinSpec { + name: $name, + area: $crate::interpreter::builtins::spec::EvalArea::$area, + param_names: &[$(eval_builtin!(@name_str $param),)* eval_builtin!(@name_str $variadic)], + params: &[ + $( + $crate::interpreter::builtins::spec::EvalParamSpec { + name: eval_builtin!(@name_str $param), + default: eval_builtin!(@default $($default)?), + by_ref: eval_builtin!(@param_by_ref $($mode)?), + }, + )* + ], + variadic: Some(eval_builtin!(@name_str $variadic)), + by_ref_params: &[$(eval_builtin!(@name_str $by_ref)),*], + required_param_count: None, + direct: None, + values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + } + } + }; + ( name: $name:literal, area: $area:ident, diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index fce36547d1..a4e8c3a7f8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -124,12 +124,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } match name { - "array_walk" => Some(&["array", "callback"]), - "uasort" | "uksort" | "usort" => Some(&["array", "callback"]), - "array_pop" | "array_shift" | "arsort" | "asort" | "krsort" | "ksort" - | "natcasesort" | "natsort" | "rsort" | "shuffle" | "sort" => Some(&["array"]), - "array_push" | "array_unshift" => Some(&["array", "values"]), - "array_splice" => Some(&["array", "offset", "length", "replacement"]), "empty" => Some(&["value"]), "is_callable" => Some(&["value", "syntax_only", "callable_name"]), "buffer_new" => Some(&["length"]), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 344f7335a2..9060eb9add 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -8,14 +8,12 @@ //! - Each child dispatcher handles already evaluated runtime-cell arguments for one //! builtin family and returns `Ok(None)` when the name is outside its domain. -mod arrays; mod filesystem; mod symbols; use super::eval_declared_builtin_values_call; use super::super::super::*; -use arrays::*; use filesystem::*; use symbols::*; @@ -30,9 +28,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( return Ok(Some(result)); } - if let Some(result) = eval_arrays_builtin_with_values(name, evaluated_args, context, values)? { - return Ok(Some(result)); - } if let Some(result) = eval_filesystem_builtin_with_values(name, evaluated_args, context, values)? { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 5cf8259294..432e2e647f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -212,9 +212,17 @@ mod tests { "array_intersect_key", "array_map", "array_merge", + "array_pop", + "array_push", "array_reduce", "array_reverse", + "array_shift", + "array_splice", "array_sum", + "array_unshift", + "array_walk", + "arsort", + "asort", "basename", "boolval", "base64_encode", @@ -343,6 +351,8 @@ mod tests { "json_last_error", "json_last_error_msg", "json_validate", + "krsort", + "ksort", "lchgrp", "lchown", "link", @@ -357,6 +367,8 @@ mod tests { "mkdir", "mktime", "mt_rand", + "natcasesort", + "natsort", "nl2br", "number_format", "opendir", @@ -392,7 +404,9 @@ mod tests { "sha1", "shell_exec", "settype", + "shuffle", "sleep", + "sort", "sprintf", "sscanf", "stat", @@ -429,9 +443,12 @@ mod tests { "touch", "trim", "strval", + "uasort", + "uksort", "umask", "unlink", "usleep", + "usort", "var_dump", "vprintf", "vsprintf", @@ -507,6 +524,26 @@ mod tests { eval_declared_builtin_default_value("iterator_to_array", 1), Some(EvalBuiltinDefaultValue::Bool(true)) ); + assert_eq!( + eval_declared_builtin_spec("array_pop").map(EvalBuiltinSpec::by_ref_param_names), + Some(["array"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("array_push"), + Some(["array", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("array_push").map(|shape| shape.variadic), + Some(Some("values")) + ); + assert_eq!( + eval_declared_builtin_default_value("array_splice", 2), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("array_splice", 3), + Some(EvalBuiltinDefaultValue::EmptyArray) + ); assert_eq!( eval_declared_builtin_param_names("settype"), Some(["var", "type"].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 69ed39af53..269f7ffcb5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -14,14 +14,6 @@ use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; /// PHP-visible builtin names implemented by the eval interpreter. pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[ - "array_pop", - "array_push", - "array_shift", - "array_splice", - "array_unshift", - "array_walk", - "arsort", - "asort", "buffer_free", "buffer_len", "buffer_new", @@ -59,11 +51,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "is_callable", "is_subclass_of", "isset", - "krsort", - "ksort", "method_exists", - "natcasesort", - "natsort", "pfsockopen", "ptr", "ptr_get", @@ -82,9 +70,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ptr_write_string", "property_exists", "readline", - "rsort", - "shuffle", - "sort", "spl_autoload", "spl_autoload_call", "spl_autoload_extensions", @@ -123,10 +108,7 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "stream_wrapper_restore", "stream_wrapper_unregister", "trait_exists", - "uasort", - "uksort", "unset", - "usort", "vfprintf", ]; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 134752fc9e..fbeb7d3d48 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -75,12 +75,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "fprintf" | "fscanf" => variadic(params, &[]), - "array_pop" | "array_shift" => fixed_by_ref(params, &["array"]), - "sort" | "rsort" | "shuffle" | "natsort" | "natcasesort" | "asort" | "arsort" - | "ksort" | "krsort" => fixed_by_ref(params, &["array"]), - "array_push" | "array_unshift" => variadic(params, &["array"]), - "array_splice" => optional_by_ref(params, 2, &["array"]), - "array_walk" | "usort" | "uksort" | "uasort" => fixed_by_ref(params, &["array"]), "fopen" | "fputcsv" => optional(params, 2), "flock" => optional_by_ref(params, 2, &["would_block"]), "fgetcsv" => optional(params, 1), @@ -129,8 +123,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_callable", 1) => Bool(false), ("is_callable", 2) => Null, ("readline", 0) => Null, - ("array_splice", 2) => Null, - ("array_splice", 3) => EmptyArray, ("fopen", 2) => Bool(false), ("fopen", 3) => Null, ("flock", 2) => Null, @@ -168,14 +160,6 @@ fn fixed(params: &[&'static str]) -> EvalBuiltinSignatureShape { shape(params.len(), 0, None, &[]) } -/// Builds fixed-arity signature shape with by-reference parameters. -fn fixed_by_ref( - params: &[&'static str], - by_ref_params: &'static [&'static str], -) -> EvalBuiltinSignatureShape { - shape(params.len(), 0, None, by_ref_params) -} - /// Builds trailing-default signature shape. fn optional(params: &[&'static str], required_param_count: usize) -> EvalBuiltinSignatureShape { shape( diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index e8c8c9c018..361f3cb702 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -17,9 +17,6 @@ const EVAL_DIRECT_DISPATCH_SOURCES: &[&str] = &[include_str!( const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!("../crates/elephc-magician/src/interpreter/builtins/raw_memory.rs"), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/arrays.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs" ), @@ -93,16 +90,24 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "array_map", "array_merge", "array_pad", + "array_pop", "array_product", + "array_push", "array_rand", "array_reduce", "array_reverse", "array_search", + "array_shift", "array_slice", + "array_splice", "array_sum", "array_unique", + "array_unshift", "array_values", + "array_walk", + "arsort", "asin", + "asort", "atan", "atan2", "basename", @@ -254,6 +259,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "iterator_apply", "iterator_count", "iterator_to_array", + "krsort", + "ksort", "lchgrp", "lchown", "lcfirst", @@ -273,6 +280,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "mkdir", "mktime", "mt_rand", + "natcasesort", + "natsort", "nl2br", "number_format", "opendir", @@ -307,6 +316,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "realpath_cache_size", "rename", "round", + "rsort", "rtrim", "rewind", "rewinddir", @@ -315,9 +325,11 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "sha1", "shell_exec", "settype", + "shuffle", "sin", "sinh", "sleep", + "sort", "sqrt", "sprintf", "sscanf", @@ -370,6 +382,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "tmpfile", "touch", "trim", + "uasort", + "uksort", "ucfirst", "ucwords", "umask", @@ -377,6 +391,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "urldecode", "urlencode", "usleep", + "usort", "var_dump", "vprintf", "vsprintf", From 99cc80f3904b0142d573b73a0edcf2f862c76b85 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 06:11:26 +0200 Subject: [PATCH 1090/1208] refactor(magician): migrate filesystem builtins --- .../declarations/direct_dispatch.rs | 152 +++++++ .../filesystem/declarations/fgetcsv.rs | 22 + .../builtins/filesystem/declarations/flock.rs | 19 + .../builtins/filesystem/declarations/fopen.rs | 23 + .../filesystem/declarations/fprintf.rs | 17 + .../filesystem/declarations/fputcsv.rs | 23 + .../filesystem/declarations/fscanf.rs | 17 + .../filesystem/declarations/fsockopen.rs | 25 ++ .../builtins/filesystem/declarations/mod.rs | 401 ++---------------- .../declarations/path_values_dispatch.rs | 194 +++++++++ .../filesystem/declarations/pfsockopen.rs | 25 ++ .../filesystem/declarations/readline.rs | 18 + .../declarations/stream_bucket_append.rs | 16 + .../stream_bucket_make_writeable.rs | 16 + .../declarations/stream_bucket_new.rs | 16 + .../declarations/stream_bucket_prepend.rs | 16 + .../declarations/stream_context_create.rs | 21 + .../stream_context_get_default.rs | 18 + .../stream_context_get_options.rs | 16 + .../declarations/stream_context_get_params.rs | 16 + .../stream_context_set_default.rs | 16 + .../declarations/stream_context_set_option.rs | 24 ++ .../declarations/stream_context_set_params.rs | 16 + .../declarations/stream_filter_append.rs | 23 + .../declarations/stream_filter_prepend.rs | 23 + .../declarations/stream_filter_register.rs | 16 + .../declarations/stream_filter_remove.rs | 16 + .../filesystem/declarations/stream_select.rs | 25 ++ .../declarations/stream_socket_accept.rs | 23 + .../declarations/stream_socket_client.rs | 16 + .../stream_socket_enable_crypto.rs | 23 + .../declarations/stream_socket_get_name.rs | 16 + .../declarations/stream_socket_pair.rs | 16 + .../declarations/stream_socket_recvfrom.rs | 24 ++ .../declarations/stream_socket_sendto.rs | 23 + .../declarations/stream_socket_server.rs | 16 + .../declarations/stream_socket_shutdown.rs | 16 + .../declarations/stream_values_dispatch.rs} | 205 ++++++--- .../declarations/stream_wrapper_register.rs | 22 + .../declarations/stream_wrapper_restore.rs | 16 + .../declarations/stream_wrapper_unregister.rs | 16 + .../declarations/values_dispatch.rs | 34 ++ .../filesystem/declarations/vfprintf.rs | 16 + .../src/interpreter/builtins/macros.rs | 4 + .../interpreter/builtins/registry/binding.rs | 40 -- .../builtins/registry/dispatch/mod.rs | 7 - .../src/interpreter/builtins/registry/mod.rs | 58 +++ .../interpreter/builtins/registry/names.rs | 38 -- .../builtins/registry/signature.rs | 41 -- tests/builtin_parity_tests.rs | 41 +- 50 files changed, 1400 insertions(+), 541 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/direct_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetcsv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/flock.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fopen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fprintf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fputcsv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fscanf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsockopen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/path_values_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pfsockopen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readline.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_append.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_make_writeable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_new.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_prepend.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_create.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_default.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_options.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_params.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_default.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_option.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_params.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_append.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_prepend.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_register.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_remove.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_select.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_accept.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_client.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_enable_crypto.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_get_name.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_pair.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_recvfrom.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_sendto.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_server.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_shutdown.rs rename crates/elephc-magician/src/interpreter/builtins/{registry/dispatch/filesystem.rs => filesystem/declarations/stream_values_dispatch.rs} (71%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_register.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_restore.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_unregister.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/values_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/vfprintf.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/direct_dispatch.rs new file mode 100644 index 0000000000..eed79b4eeb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/direct_dispatch.rs @@ -0,0 +1,152 @@ +//! Purpose: +//! Direct expression-level dispatch for filesystem builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalDirectHook::call()`. +//! +//! Key details: +//! - This dispatcher keeps filesystem call lowering area-scoped while per-builtin +//! metadata lives in individual declaration files. + +use super::super::super::super::*; +use super::super::*; + +/// Dispatches direct expression-level calls for declaratively migrated filesystem builtins. +pub(in crate::interpreter) fn eval_builtin_filesystem_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "basename" => eval_builtin_basename(args, context, scope, values), + "chdir" | "mkdir" | "rmdir" => { + eval_builtin_unary_path_bool(name, args, context, scope, values) + } + "chmod" => eval_builtin_chmod(args, context, scope, values), + "chown" | "chgrp" | "lchown" | "lchgrp" => { + eval_builtin_chown_like(name, args, context, scope, values) + } + "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), + "copy" | "link" | "rename" | "symlink" => { + eval_builtin_binary_path_bool(name, args, context, scope, values) + } + "dirname" => eval_builtin_dirname(args, context, scope, values), + "disk_free_space" | "disk_total_space" => { + eval_builtin_disk_space(name, args, context, scope, values) + } + "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), + "file" => eval_builtin_file(args, context, scope, values), + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => { + eval_builtin_file_probe(name, args, context, scope, values) + } + "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), + "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), + "filesize" => eval_builtin_filesize(args, context, scope, values), + "filetype" => eval_builtin_filetype(args, context, scope, values), + "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" + | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { + eval_builtin_unary_stream(name, args, context, scope, values) + } + "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), + "fopen" => eval_builtin_fopen(args, context, scope, values), + "fprintf" => eval_builtin_fprintf(args, context, scope, values), + "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), + "fread" => eval_builtin_fread(args, context, scope, values), + "fscanf" => eval_builtin_fscanf(args, context, scope, values), + "fseek" => eval_builtin_fseek(args, context, scope, values), + "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), + "fwrite" => eval_builtin_fwrite(args, context, scope, values), + "getcwd" => eval_builtin_getcwd(args, values), + "glob" => eval_builtin_glob(args, context, scope, values), + "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), + "opendir" => eval_builtin_opendir(args, context, scope, values), + "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), + "pclose" => eval_builtin_pclose(args, context, scope, values), + "popen" => eval_builtin_popen(args, context, scope, values), + "closedir" | "readdir" | "rewinddir" => { + eval_builtin_unary_directory(name, args, context, scope, values) + } + "readfile" => eval_builtin_readfile(args, context, scope, values), + "readline" => eval_builtin_readline(args, context, scope, values), + "readlink" => eval_builtin_readlink(args, context, scope, values), + "realpath" => eval_builtin_realpath(args, context, scope, values), + "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), + "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), + "scandir" => eval_builtin_scandir(args, context, scope, values), + "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), + "stream_bucket_append" | "stream_bucket_prepend" => { + eval_builtin_stream_bucket_push(name, args, context, scope, values) + } + "stream_bucket_make_writeable" => { + eval_builtin_stream_bucket_make_writeable(args, context, scope, values) + } + "stream_bucket_new" => eval_builtin_stream_bucket_new(args, context, scope, values), + "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), + "stream_context_get_default" => { + eval_builtin_stream_context_get_default(args, context, scope, values) + } + "stream_context_get_options" => { + eval_builtin_stream_context_get_options(args, context, scope, values) + } + "stream_context_get_params" => { + eval_builtin_stream_context_get_params(args, context, scope, values) + } + "stream_context_set_default" => { + eval_builtin_stream_context_set_default(args, context, scope, values) + } + "stream_context_set_option" => { + eval_builtin_stream_context_set_option(args, context, scope, values) + } + "stream_context_set_params" => { + eval_builtin_stream_context_set_params(args, context, scope, values) + } + "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), + "stream_filter_append" | "stream_filter_prepend" => { + eval_builtin_stream_filter_attach(name, args, context, scope, values) + } + "stream_filter_register" => { + eval_builtin_stream_filter_register(args, context, scope, values) + } + "stream_filter_remove" => eval_builtin_stream_filter_remove(args, context, scope, values), + "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), + "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), + "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), + "stream_resolve_include_path" => { + eval_builtin_stream_resolve_include_path(args, context, scope, values) + } + "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), + "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { + eval_builtin_stream_set_buffer_like(name, args, context, scope, values) + } + "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), + "stream_socket_client" => eval_builtin_stream_socket_client(args, context, scope, values), + "stream_socket_enable_crypto" => { + eval_builtin_stream_socket_enable_crypto(args, context, scope, values) + } + "stream_socket_get_name" => { + eval_builtin_stream_socket_get_name(args, context, scope, values) + } + "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), + "stream_socket_sendto" => eval_builtin_stream_socket_sendto(args, context, scope, values), + "stream_socket_server" => eval_builtin_stream_socket_server(args, context, scope, values), + "stream_socket_shutdown" => { + eval_builtin_stream_socket_shutdown(args, context, scope, values) + } + "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { + eval_builtin_stream_wrapper_registry(name, args, context, scope, values) + } + "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), + "tempnam" => eval_builtin_tempnam(args, context, scope, values), + "tmpfile" => eval_builtin_tmpfile(args, context, values), + "touch" => eval_builtin_touch(args, context, scope, values), + "umask" => eval_builtin_umask(args, context, scope, values), + "unlink" => eval_builtin_unlink(args, context, scope, values), + "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetcsv.rs new file mode 100644 index 0000000000..ede354ea77 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetcsv.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `fgetcsv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the CSV stream read helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fgetcsv", + area: Filesystem, + params: [ + stream, + length = EvalBuiltinDefaultValue::Null, + separator = EvalBuiltinDefaultValue::String(",") + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/flock.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/flock.rs new file mode 100644 index 0000000000..e3647532ba --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/flock.rs @@ -0,0 +1,19 @@ +//! Purpose: +//! Declarative eval registry entry for `flock`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference path. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "flock", + area: Filesystem, + params: [stream, operation, would_block: by_ref = EvalBuiltinDefaultValue::Null], + by_ref: [would_block], + direct: none, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fopen.rs new file mode 100644 index 0000000000..e5060bc11e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fopen.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `fopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream-opening helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fopen", + area: Filesystem, + params: [ + filename, + mode, + use_include_path = EvalBuiltinDefaultValue::Bool(false), + context = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fprintf.rs new file mode 100644 index 0000000000..695f969501 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fprintf.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `fprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Variadic values are formatted by the existing printf-family helper. + +eval_builtin! { + name: "fprintf", + area: Filesystem, + params: [stream, format], + variadic: values, + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fputcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fputcsv.rs new file mode 100644 index 0000000000..1c760df14d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fputcsv.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `fputcsv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the CSV stream write helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fputcsv", + area: Filesystem, + params: [ + stream, + fields, + separator = EvalBuiltinDefaultValue::String(","), + enclosure = EvalBuiltinDefaultValue::String("\"") + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fscanf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fscanf.rs new file mode 100644 index 0000000000..47a6442cd7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fscanf.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `fscanf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - The current eval implementation returns parsed values and ignores output vars. + +eval_builtin! { + name: "fscanf", + area: Filesystem, + params: [stream, format], + variadic: vars, + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsockopen.rs new file mode 100644 index 0000000000..fb35f30761 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsockopen.rs @@ -0,0 +1,25 @@ +//! Purpose: +//! Declarative eval registry entry for `fsockopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference error-output path. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fsockopen", + area: Filesystem, + params: [ + hostname, + port, + error_code: by_ref = EvalBuiltinDefaultValue::Null, + error_message: by_ref = EvalBuiltinDefaultValue::Null, + timeout = EvalBuiltinDefaultValue::Null + ], + by_ref: [error_code, error_message], + direct: none, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs index 687da77ff1..97be4b72e5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs @@ -6,11 +6,8 @@ //! - `crate::interpreter::builtins::hooks` for migrated filesystem dispatch. //! //! Key details: -//! - This covers simple path/query/content and non-by-ref mutation helpers; -//! stream and by-reference filesystem calls stay on the legacy path. - -use super::super::super::*; -use super::*; +//! - Stream and by-reference-output builtins are registered here; direct calls +//! that must preserve writable caller storage keep source-sensitive helpers. mod basename; mod chdir; @@ -20,6 +17,7 @@ mod chown; mod closedir; mod clearstatcache; mod copy; +mod direct_dispatch; mod dirname; mod disk_free_space; mod disk_total_space; @@ -40,14 +38,21 @@ mod fclose; mod fdatasync; mod feof; mod fflush; +mod fgetcsv; mod fgetc; mod fgets; mod fnmatch; +mod flock; +mod fopen; +mod fprintf; mod fpassthru; +mod fputcsv; mod fread; +mod fscanf; mod fseek; mod fstat; mod ftruncate; +mod fsockopen; mod fsync; mod ftell; mod fwrite; @@ -67,10 +72,13 @@ mod linkinfo; mod lstat; mod mkdir; mod opendir; +mod path_values_dispatch; mod pathinfo; mod pclose; +mod pfsockopen; mod popen; mod readdir; +mod readline; mod readfile; mod readlink; mod realpath; @@ -82,17 +90,46 @@ mod rewinddir; mod rmdir; mod scandir; mod stat; +mod stream_bucket_append; +mod stream_bucket_make_writeable; +mod stream_bucket_new; +mod stream_bucket_prepend; +mod stream_context_create; +mod stream_context_get_default; +mod stream_context_get_options; +mod stream_context_get_params; +mod stream_context_set_default; +mod stream_context_set_option; +mod stream_context_set_params; mod stream_copy_to_stream; +mod stream_filter_append; +mod stream_filter_prepend; +mod stream_filter_register; +mod stream_filter_remove; mod stream_get_contents; mod stream_get_line; mod stream_get_meta_data; mod stream_isatty; mod stream_resolve_include_path; +mod stream_select; mod stream_set_blocking; mod stream_set_chunk_size; mod stream_set_read_buffer; mod stream_set_timeout; mod stream_set_write_buffer; +mod stream_socket_accept; +mod stream_socket_client; +mod stream_socket_enable_crypto; +mod stream_socket_get_name; +mod stream_socket_pair; +mod stream_socket_recvfrom; +mod stream_socket_sendto; +mod stream_socket_server; +mod stream_socket_shutdown; +mod stream_values_dispatch; +mod stream_wrapper_register; +mod stream_wrapper_restore; +mod stream_wrapper_unregister; mod symlink; mod sys_get_temp_dir; mod tempnam; @@ -100,354 +137,8 @@ mod tmpfile; mod touch; mod umask; mod unlink; +mod values_dispatch; +mod vfprintf; -/// Dispatches direct expression-level calls for declaratively migrated filesystem builtins. -pub(in crate::interpreter) fn eval_builtin_filesystem_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "basename" => eval_builtin_basename(args, context, scope, values), - "chdir" | "mkdir" | "rmdir" => { - eval_builtin_unary_path_bool(name, args, context, scope, values) - } - "chmod" => eval_builtin_chmod(args, context, scope, values), - "chown" | "chgrp" | "lchown" | "lchgrp" => { - eval_builtin_chown_like(name, args, context, scope, values) - } - "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), - "copy" | "link" | "rename" | "symlink" => { - eval_builtin_binary_path_bool(name, args, context, scope, values) - } - "dirname" => eval_builtin_dirname(args, context, scope, values), - "disk_free_space" | "disk_total_space" => { - eval_builtin_disk_space(name, args, context, scope, values) - } - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => { - eval_builtin_file_probe(name, args, context, scope, values) - } - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), - "file" => eval_builtin_file(args, context, scope, values), - "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), - "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), - "filesize" => eval_builtin_filesize(args, context, scope, values), - "filetype" => eval_builtin_filetype(args, context, scope, values), - "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" - | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { - eval_builtin_unary_stream(name, args, context, scope, values) - } - "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), - "fread" => eval_builtin_fread(args, context, scope, values), - "fseek" => eval_builtin_fseek(args, context, scope, values), - "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), - "fwrite" => eval_builtin_fwrite(args, context, scope, values), - "getcwd" => eval_builtin_getcwd(args, values), - "glob" => eval_builtin_glob(args, context, scope, values), - "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), - "opendir" => eval_builtin_opendir(args, context, scope, values), - "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), - "pclose" => eval_builtin_pclose(args, context, scope, values), - "popen" => eval_builtin_popen(args, context, scope, values), - "closedir" | "readdir" | "rewinddir" => { - eval_builtin_unary_directory(name, args, context, scope, values) - } - "readfile" => eval_builtin_readfile(args, context, scope, values), - "readlink" => eval_builtin_readlink(args, context, scope, values), - "realpath" => eval_builtin_realpath(args, context, scope, values), - "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), - "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "scandir" => eval_builtin_scandir(args, context, scope, values), - "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), - "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), - "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), - "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), - "stream_resolve_include_path" => { - eval_builtin_stream_resolve_include_path(args, context, scope, values) - } - "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), - "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), - "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { - eval_builtin_stream_set_buffer_like(name, args, context, scope, values) - } - "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), - "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), - "tempnam" => eval_builtin_tempnam(args, context, scope, values), - "tmpfile" => eval_builtin_tmpfile(args, context, values), - "touch" => eval_builtin_touch(args, context, scope, values), - "umask" => eval_builtin_umask(args, context, scope, values), - "unlink" => eval_builtin_unlink(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated filesystem builtins. -pub(in crate::interpreter) fn eval_filesystem_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "basename" => match evaluated_args { - [path] => eval_basename_result(*path, None, values), - [path, suffix] => eval_basename_result(*path, Some(*suffix), values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "chdir" | "mkdir" | "rmdir" => match evaluated_args { - [path] => eval_unary_path_bool_result(name, *path, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "chmod" => match evaluated_args { - [filename, permissions] => eval_chmod_result(*filename, *permissions, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "chown" | "chgrp" | "lchown" | "lchgrp" => match evaluated_args { - [filename, principal] => { - eval_chown_like_result(name, *filename, *principal, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "clearstatcache" => { - if evaluated_args.len() > 2 { - Err(EvalStatus::RuntimeFatal) - } else { - values.null() - } - } - "copy" | "link" | "rename" | "symlink" => match evaluated_args { - [from, to] => eval_binary_path_bool_result(name, *from, *to, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "dirname" => match evaluated_args { - [path] => eval_dirname_result(*path, None, values), - [path, levels] => eval_dirname_result(*path, Some(*levels), values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "disk_free_space" | "disk_total_space" => match evaluated_args { - [directory] => eval_disk_space_result(name, *directory, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => match evaluated_args { - [filename] => eval_file_probe_result(name, *filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => match evaluated_args { - [filename] => eval_file_stat_scalar_result(name, *filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "file" => match evaluated_args { - [filename] => eval_file_result(*filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "file_get_contents" => match evaluated_args { - [filename] => eval_file_get_contents_result(*filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "file_put_contents" => match evaluated_args { - [filename, data] => eval_file_put_contents_result(*filename, *data, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "filesize" => match evaluated_args { - [filename] => eval_filesize_result(*filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "filetype" => match evaluated_args { - [filename] => eval_filetype_result(*filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" - | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { - match evaluated_args { - [stream] => eval_unary_stream_result(name, *stream, context, values), - _ => Err(EvalStatus::RuntimeFatal), - } - } - "fnmatch" => match evaluated_args { - [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values), - [pattern, filename, flags] => { - eval_fnmatch_result(*pattern, *filename, Some(*flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "fread" => match evaluated_args { - [stream, length] => eval_fread_result(*stream, *length, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "fseek" => match evaluated_args { - [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values), - [stream, offset, whence] => { - eval_fseek_result(*stream, *offset, Some(*whence), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "ftruncate" => match evaluated_args { - [stream, size] => eval_ftruncate_result(*stream, *size, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "fwrite" => match evaluated_args { - [stream, data] => eval_fwrite_result(*stream, *data, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "getcwd" => match evaluated_args { - [] => eval_getcwd_result(values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "glob" => match evaluated_args { - [pattern] => eval_glob_result(*pattern, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "linkinfo" => match evaluated_args { - [path] => eval_linkinfo_result(*path, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "opendir" => match evaluated_args { - [directory] => eval_opendir_result(*directory, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "pathinfo" => match evaluated_args { - [path] => eval_pathinfo_result(*path, None, values), - [path, flags] => eval_pathinfo_result(*path, Some(*flags), values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "pclose" => match evaluated_args { - [handle] => eval_pclose_result(*handle, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "popen" => match evaluated_args { - [command, mode] => eval_popen_result(*command, *mode, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "closedir" | "readdir" | "rewinddir" => match evaluated_args { - [dir_handle] => eval_unary_directory_result(name, *dir_handle, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "readfile" => match evaluated_args { - [filename] => eval_readfile_result(*filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "readlink" => match evaluated_args { - [path] => eval_readlink_result(*path, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "realpath" => match evaluated_args { - [path] => eval_realpath_result(*path, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "realpath_cache_get" => match evaluated_args { - [] => eval_realpath_cache_get_result(values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "realpath_cache_size" => match evaluated_args { - [] => eval_realpath_cache_size_result(values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "scandir" => match evaluated_args { - [directory] => eval_scandir_result(*directory, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "stat" | "lstat" => match evaluated_args { - [filename] => eval_stat_array_result(name, *filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "stream_copy_to_stream" => match evaluated_args { - [from, to] => eval_stream_copy_to_stream_result(*from, *to, None, None, context, values), - [from, to, length] => { - eval_stream_copy_to_stream_result(*from, *to, Some(*length), None, context, values) - } - [from, to, length, offset] => eval_stream_copy_to_stream_result( - *from, - *to, - Some(*length), - Some(*offset), - context, - values, - ), - _ => Err(EvalStatus::RuntimeFatal), - }, - "stream_get_contents" => match evaluated_args { - [stream] => eval_stream_get_contents_result(*stream, None, None, context, values), - [stream, length] => { - eval_stream_get_contents_result(*stream, Some(*length), None, context, values) - } - [stream, length, offset] => eval_stream_get_contents_result( - *stream, - Some(*length), - Some(*offset), - context, - values, - ), - _ => Err(EvalStatus::RuntimeFatal), - }, - "stream_get_line" => match evaluated_args { - [stream, length] => eval_stream_get_line_result(*stream, *length, None, context, values), - [stream, length, ending] => { - eval_stream_get_line_result(*stream, *length, Some(*ending), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "stream_resolve_include_path" => match evaluated_args { - [filename] => eval_stream_resolve_include_path_result(*filename, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "stream_isatty" => match evaluated_args { - [stream] => eval_stream_isatty_result(*stream, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "stream_set_blocking" => match evaluated_args { - [stream, enable] => eval_stream_set_blocking_result(*stream, *enable, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { - match evaluated_args { - [stream, size] => { - eval_stream_set_buffer_like_result(name, *stream, *size, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } - } - "stream_set_timeout" => match evaluated_args { - [stream, seconds] => eval_stream_set_timeout_result(*stream, *seconds, None, context, values), - [stream, seconds, microseconds] => { - eval_stream_set_timeout_result(*stream, *seconds, Some(*microseconds), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "sys_get_temp_dir" => match evaluated_args { - [] => eval_sys_get_temp_dir_result(values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "tempnam" => match evaluated_args { - [directory, prefix] => eval_tempnam_result(*directory, *prefix, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "tmpfile" => match evaluated_args { - [] => eval_tmpfile_result(context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "touch" => match evaluated_args { - [filename] => eval_touch_result(*filename, None, None, context, values), - [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, context, values), - [filename, mtime, atime] => { - eval_touch_result(*filename, Some(*mtime), Some(*atime), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "umask" => match evaluated_args { - [] => eval_umask_result(None, values), - [mask] => eval_umask_result(Some(*mask), values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "unlink" => match evaluated_args { - [filename] => eval_unlink_result(*filename, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - _ => Err(EvalStatus::RuntimeFatal), - } -} +pub(in crate::interpreter) use direct_dispatch::eval_builtin_filesystem_call; +pub(in crate::interpreter) use values_dispatch::eval_filesystem_values_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/path_values_dispatch.rs new file mode 100644 index 0000000000..2b2239a683 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/path_values_dispatch.rs @@ -0,0 +1,194 @@ +//! Purpose: +//! Evaluated-argument dispatch for declarative path, file, directory, and stat builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations::values_dispatch`. +//! +//! Key details: +//! - This module owns by-value dispatch for filesystem helpers that do not work +//! with stream resource cursor state. + +use super::super::super::super::*; +use super::super::*; + +/// Attempts evaluated-argument dispatch for path and file builtins. +pub(in crate::interpreter::builtins::filesystem::declarations) fn eval_filesystem_path_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let result = match name { + "basename" => match evaluated_args { + [path] => eval_basename_result(*path, None, values)?, + [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "chdir" | "mkdir" | "rmdir" => match evaluated_args { + [path] => eval_unary_path_bool_result(name, *path, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "chmod" => match evaluated_args { + [filename, permissions] => eval_chmod_result(*filename, *permissions, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "chown" | "chgrp" | "lchown" | "lchgrp" => match evaluated_args { + [filename, principal] => { + eval_chown_like_result(name, *filename, *principal, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "clearstatcache" => { + if evaluated_args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + values.null()? + } + "copy" | "link" | "rename" | "symlink" => match evaluated_args { + [from, to] => eval_binary_path_bool_result(name, *from, *to, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "dirname" => match evaluated_args { + [path] => eval_dirname_result(*path, None, values)?, + [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "disk_free_space" | "disk_total_space" => match evaluated_args { + [directory] => eval_disk_space_result(name, *directory, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "file" => match evaluated_args { + [filename] => eval_file_result(*filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => match evaluated_args { + [filename] => eval_file_probe_result(name, *filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "file_get_contents" => match evaluated_args { + [filename] => eval_file_get_contents_result(*filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "file_put_contents" => match evaluated_args { + [filename, data] => { + eval_file_put_contents_result(*filename, *data, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => match evaluated_args { + [filename] => eval_file_stat_scalar_result(name, *filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "filesize" => match evaluated_args { + [filename] => eval_filesize_result(*filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "filetype" => match evaluated_args { + [filename] => eval_filetype_result(*filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "fnmatch" => match evaluated_args { + [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, + [pattern, filename, flags] => { + eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "getcwd" => match evaluated_args { + [] => eval_getcwd_result(values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "glob" => match evaluated_args { + [pattern] => eval_glob_result(*pattern, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "linkinfo" => match evaluated_args { + [path] => eval_linkinfo_result(*path, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "opendir" => match evaluated_args { + [directory] => eval_opendir_result(*directory, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "pathinfo" => match evaluated_args { + [path] => eval_pathinfo_result(*path, None, values)?, + [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "pclose" => match evaluated_args { + [handle] => eval_pclose_result(*handle, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "popen" => match evaluated_args { + [command, mode] => eval_popen_result(*command, *mode, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "closedir" | "readdir" | "rewinddir" => match evaluated_args { + [dir_handle] => eval_unary_directory_result(name, *dir_handle, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "readfile" => match evaluated_args { + [filename] => eval_readfile_result(*filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "readlink" => match evaluated_args { + [path] => eval_readlink_result(*path, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "realpath" => match evaluated_args { + [path] => eval_realpath_result(*path, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "realpath_cache_get" => match evaluated_args { + [] => eval_realpath_cache_get_result(values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "realpath_cache_size" => match evaluated_args { + [] => eval_realpath_cache_size_result(values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "scandir" => match evaluated_args { + [directory] => eval_scandir_result(*directory, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stat" | "lstat" => match evaluated_args { + [filename] => eval_stat_array_result(name, *filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "sys_get_temp_dir" => match evaluated_args { + [] => eval_sys_get_temp_dir_result(values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "tempnam" => match evaluated_args { + [directory, prefix] => eval_tempnam_result(*directory, *prefix, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "tmpfile" => match evaluated_args { + [] => eval_tmpfile_result(context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "touch" => match evaluated_args { + [filename] => eval_touch_result(*filename, None, None, context, values)?, + [filename, mtime] => { + eval_touch_result(*filename, Some(*mtime), None, context, values)? + } + [filename, mtime, atime] => { + eval_touch_result(*filename, Some(*mtime), Some(*atime), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "umask" => match evaluated_args { + [] => eval_umask_result(None, values)?, + [mask] => eval_umask_result(Some(*mask), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "unlink" => match evaluated_args { + [filename] => eval_unlink_result(*filename, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + _ => return Ok(None), + }; + Ok(Some(result)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pfsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pfsockopen.rs new file mode 100644 index 0000000000..7b3ef6b346 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pfsockopen.rs @@ -0,0 +1,25 @@ +//! Purpose: +//! Declarative eval registry entry for `pfsockopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference error-output path. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "pfsockopen", + area: Filesystem, + params: [ + hostname, + port, + error_code: by_ref = EvalBuiltinDefaultValue::Null, + error_message: by_ref = EvalBuiltinDefaultValue::Null, + timeout = EvalBuiltinDefaultValue::Null + ], + by_ref: [error_code, error_message], + direct: none, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readline.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readline.rs new file mode 100644 index 0000000000..cebcc638d9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readline.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `readline`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the host stdin helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "readline", + area: Filesystem, + params: [prompt = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_append.rs new file mode 100644 index 0000000000..0eabc5b9fe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_append.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_bucket_append`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream bucket push helper. + +eval_builtin! { + name: "stream_bucket_append", + area: Filesystem, + params: [brigade, bucket], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_make_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_make_writeable.rs new file mode 100644 index 0000000000..2a82f0323e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_make_writeable.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_bucket_make_writeable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream bucket read helper. + +eval_builtin! { + name: "stream_bucket_make_writeable", + area: Filesystem, + params: [brigade], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_new.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_new.rs new file mode 100644 index 0000000000..89092f504c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_new.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_bucket_new`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream bucket object helper. + +eval_builtin! { + name: "stream_bucket_new", + area: Filesystem, + params: [stream, buffer], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_prepend.rs new file mode 100644 index 0000000000..4766438308 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_prepend.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_bucket_prepend`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream bucket push helper. + +eval_builtin! { + name: "stream_bucket_prepend", + area: Filesystem, + params: [brigade, bucket], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_create.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_create.rs new file mode 100644 index 0000000000..776d293825 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_create.rs @@ -0,0 +1,21 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_create`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream context helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_create", + area: Filesystem, + params: [ + options = EvalBuiltinDefaultValue::Null, + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_default.rs new file mode 100644 index 0000000000..e4e723b659 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_default.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_default`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the default stream context helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_get_default", + area: Filesystem, + params: [options = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_options.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_options.rs new file mode 100644 index 0000000000..d7bc520142 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_options.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_options`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream context option reader. + +eval_builtin! { + name: "stream_context_get_options", + area: Filesystem, + params: [context], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_params.rs new file mode 100644 index 0000000000..a0b123a9e6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_params.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_params`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Eval mirrors the main backend's current empty-params behavior. + +eval_builtin! { + name: "stream_context_get_params", + area: Filesystem, + params: [context], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_default.rs new file mode 100644 index 0000000000..ef3a639841 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_default.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_default`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream context helper. + +eval_builtin! { + name: "stream_context_set_default", + area: Filesystem, + params: [options], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_option.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_option.rs new file mode 100644 index 0000000000..558d53488f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_option.rs @@ -0,0 +1,24 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_option`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - The signature keeps the existing two-argument and four-argument forms. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_set_option", + area: Filesystem, + params: [ + context, + wrapper_or_options, + option_name = EvalBuiltinDefaultValue::Null, + value = EvalBuiltinDefaultValue::Null + ], + required: 2, + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_params.rs new file mode 100644 index 0000000000..a3657e7e47 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_params.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_params`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the accepted no-op helper. + +eval_builtin! { + name: "stream_context_set_params", + area: Filesystem, + params: [context, params], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_append.rs new file mode 100644 index 0000000000..3cd42b458d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_append.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_filter_append`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream filter attachment helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_filter_append", + area: Filesystem, + params: [ + stream, + filtername, + read_write = EvalBuiltinDefaultValue::Int(3), + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_prepend.rs new file mode 100644 index 0000000000..9811b60aea --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_prepend.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_filter_prepend`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream filter attachment helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_filter_prepend", + area: Filesystem, + params: [ + stream, + filtername, + read_write = EvalBuiltinDefaultValue::Int(3), + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_register.rs new file mode 100644 index 0000000000..c8b3b11c09 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_register.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_filter_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the conservative filter registry helper. + +eval_builtin! { + name: "stream_filter_register", + area: Filesystem, + params: [filter_name, r#class], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_remove.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_remove.rs new file mode 100644 index 0000000000..a68cc7f36d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_remove.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_filter_remove`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream filter removal helper. + +eval_builtin! { + name: "stream_filter_remove", + area: Filesystem, + params: [stream_filter], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_select.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_select.rs new file mode 100644 index 0000000000..a52daff985 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_select.rs @@ -0,0 +1,25 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_select`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference array path. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_select", + area: Filesystem, + params: [ + read: by_ref, + write: by_ref, + except: by_ref, + seconds, + microseconds = EvalBuiltinDefaultValue::Int(0) + ], + by_ref: [read, write, except], + direct: none, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_accept.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_accept.rs new file mode 100644 index 0000000000..c1051e2531 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_accept.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_accept`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference peer-name path. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_accept", + area: Filesystem, + params: [ + socket, + timeout = EvalBuiltinDefaultValue::Null, + peer_name: by_ref = EvalBuiltinDefaultValue::Null + ], + by_ref: [peer_name], + direct: none, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_client.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_client.rs new file mode 100644 index 0000000000..1402abeebf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_client.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_client`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the TCP client stream helper. + +eval_builtin! { + name: "stream_socket_client", + area: Filesystem, + params: [address], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_enable_crypto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_enable_crypto.rs new file mode 100644 index 0000000000..fbc315979e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_enable_crypto.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_enable_crypto`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the conservative TLS status helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_enable_crypto", + area: Filesystem, + params: [ + stream, + enable, + crypto_method = EvalBuiltinDefaultValue::Null, + session_stream = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_get_name.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_get_name.rs new file mode 100644 index 0000000000..036df6be02 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_get_name.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_get_name`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the socket-name lookup helper. + +eval_builtin! { + name: "stream_socket_get_name", + area: Filesystem, + params: [socket, remote], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_pair.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_pair.rs new file mode 100644 index 0000000000..d1a5000d74 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_pair.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_pair`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the local socket-pair helper. + +eval_builtin! { + name: "stream_socket_pair", + area: Filesystem, + params: [domain, r#type, protocol], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_recvfrom.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_recvfrom.rs new file mode 100644 index 0000000000..677d34c19f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_recvfrom.rs @@ -0,0 +1,24 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_recvfrom`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference address path. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_recvfrom", + area: Filesystem, + params: [ + socket, + length, + flags = EvalBuiltinDefaultValue::Int(0), + address: by_ref = EvalBuiltinDefaultValue::String("") + ], + by_ref: [address], + direct: none, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_sendto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_sendto.rs new file mode 100644 index 0000000000..d89a5e3445 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_sendto.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_sendto`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the connected-socket write helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_sendto", + area: Filesystem, + params: [ + socket, + data, + flags = EvalBuiltinDefaultValue::Int(0), + address = EvalBuiltinDefaultValue::String("") + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_server.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_server.rs new file mode 100644 index 0000000000..ca3916816c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_server.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_server`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the TCP listener helper. + +eval_builtin! { + name: "stream_socket_server", + area: Filesystem, + params: [address], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_shutdown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_shutdown.rs new file mode 100644 index 0000000000..f59d61957f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_shutdown.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_shutdown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the socket shutdown helper. + +eval_builtin! { + name: "stream_socket_shutdown", + area: Filesystem, + params: [stream, mode], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_values_dispatch.rs similarity index 71% rename from crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_values_dispatch.rs index eac4dbc1b9..a78a9b3b8a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_values_dispatch.rs @@ -1,24 +1,31 @@ //! Purpose: -//! Dispatches already evaluated filesystem and path builtins by dynamic callable name. +//! Evaluated-argument dispatch for declarative stream, socket, context, and CSV builtins. //! //! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. +//! - `crate::interpreter::builtins::filesystem::declarations::values_dispatch`. //! //! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. +//! - By-value callable forms emit PHP-style warnings for parameters that are +//! normally by-reference outputs. use super::super::super::super::*; -use super::super::super::*; +use super::super::*; -/// Attempts to dispatch evaluated filesystem and path builtins. -pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( +/// Attempts evaluated-argument dispatch for stream and socket builtins. +pub(in crate::interpreter::builtins::filesystem::declarations) fn eval_filesystem_stream_values_result( name: &str, evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { + "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" + | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { + match evaluated_args { + [stream] => eval_unary_stream_result(name, *stream, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + } + } "fgetcsv" => match evaluated_args { [stream] => eval_fgetcsv_result(*stream, None, None, context, values)?, [stream, length] => { @@ -29,12 +36,34 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } _ => return Err(EvalStatus::RuntimeFatal), }, + "flock" => { + if !(2..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + if evaluated_args.len() >= 3 { + values.warning( + "flock(): Argument #3 ($would_block) must be passed by reference, value given", + )?; + } + let (success, _) = + eval_flock_result(evaluated_args[0], evaluated_args[1], context, values)?; + values.bool_value(success)? + } "fopen" => { if !(2..=4).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); } eval_fopen_result(evaluated_args[0], evaluated_args[1], context, values)? } + "fprintf" => { + let Some((stream, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let Some((format, format_args)) = rest.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_fprintf_result(*stream, *format, format_args, context, values)? + } "fputcsv" => match evaluated_args { [stream, fields] => eval_fputcsv_result(*stream, *fields, None, None, context, values)?, [stream, fields, separator] => { @@ -50,32 +79,23 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( )?, _ => return Err(EvalStatus::RuntimeFatal), }, - "fprintf" => { - let Some((stream, rest)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let Some((format, format_args)) = rest.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_fprintf_result(*stream, *format, format_args, context, values)? - } - "flock" => { - if !(2..=3).contains(&evaluated_args.len()) { + "fread" => match evaluated_args { + [stream, length] => eval_fread_result(*stream, *length, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "fscanf" => { + if evaluated_args.len() < 2 { return Err(EvalStatus::RuntimeFatal); } - if evaluated_args.len() >= 3 { - values.warning( - "flock(): Argument #3 ($would_block) must be passed by reference, value given", - )?; - } - let (success, _) = eval_flock_result( - evaluated_args[0], - evaluated_args[1], - context, - values, - )?; - values.bool_value(success)? + eval_fscanf_result(evaluated_args[0], evaluated_args[1], context, values)? } + "fseek" => match evaluated_args { + [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values)?, + [stream, offset, whence] => { + eval_fseek_result(*stream, *offset, Some(*whence), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, "fsockopen" | "pfsockopen" => { if !(2..=5).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); @@ -83,12 +103,14 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( eval_fsockopen_by_value_ref_warnings(name, evaluated_args.len(), values)?; eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values)? } - "fscanf" => { - if evaluated_args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - eval_fscanf_result(evaluated_args[0], evaluated_args[1], context, values)? - } + "ftruncate" => match evaluated_args { + [stream, size] => eval_ftruncate_result(*stream, *size, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "fwrite" => match evaluated_args { + [stream, data] => eval_fwrite_result(*stream, *data, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "readline" => { if evaluated_args.len() > 1 { return Err(EvalStatus::RuntimeFatal); @@ -96,6 +118,24 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( let prompt = evaluated_args.first().copied(); eval_readline_result(prompt, values)? } + "stream_bucket_new" => { + let [stream, buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_new_result(*stream, *buffer, context, values)? + } + "stream_bucket_make_writeable" => { + let [brigade] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_make_writeable_result(*brigade, values)? + } + "stream_bucket_append" | "stream_bucket_prepend" => { + let [brigade, bucket] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_push_result(name, *brigade, *bucket, values)? + } "stream_context_create" => match evaluated_args { [] => eval_stream_context_create_result(None, context, values)?, [options] => eval_stream_context_create_result(Some(*options), context, values)?, @@ -154,9 +194,21 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( } values.bool_value(true)? } - "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { - eval_stream_wrapper_registry_result(name, evaluated_args, context, values)? - } + "stream_copy_to_stream" => match evaluated_args { + [from, to] => eval_stream_copy_to_stream_result(*from, *to, None, None, context, values)?, + [from, to, length] => { + eval_stream_copy_to_stream_result(*from, *to, Some(*length), None, context, values)? + } + [from, to, length, offset] => eval_stream_copy_to_stream_result( + *from, + *to, + Some(*length), + Some(*offset), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "stream_filter_register" => { let [filter_name, class] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -181,28 +233,64 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_stream_filter_remove_result(*stream_filter, context, values)? } - "stream_bucket_new" => { - let [stream, buffer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_bucket_new_result(*stream, *buffer, context, values)? - } - "stream_bucket_make_writeable" => { - let [brigade] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_bucket_make_writeable_result(*brigade, values)? - } - "stream_bucket_append" | "stream_bucket_prepend" => { - let [brigade, bucket] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_bucket_push_result(name, *brigade, *bucket, values)? - } + "stream_get_contents" => match evaluated_args { + [stream] => eval_stream_get_contents_result(*stream, None, None, context, values)?, + [stream, length] => { + eval_stream_get_contents_result(*stream, Some(*length), None, context, values)? + } + [stream, length, offset] => eval_stream_get_contents_result( + *stream, + Some(*length), + Some(*offset), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_get_line" => match evaluated_args { + [stream, length] => eval_stream_get_line_result(*stream, *length, None, context, values)?, + [stream, length, ending] => { + eval_stream_get_line_result(*stream, *length, Some(*ending), context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_resolve_include_path" => match evaluated_args { + [filename] => eval_stream_resolve_include_path_result(*filename, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_isatty" => match evaluated_args { + [stream] => eval_stream_isatty_result(*stream, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "stream_select" => { eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; eval_stream_select_result(evaluated_args, context, values)? } + "stream_set_blocking" => match evaluated_args { + [stream, enable] => eval_stream_set_blocking_result(*stream, *enable, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }, + "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { + match evaluated_args { + [stream, size] => { + eval_stream_set_buffer_like_result(name, *stream, *size, context, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + "stream_set_timeout" => match evaluated_args { + [stream, seconds] => { + eval_stream_set_timeout_result(*stream, *seconds, None, context, values)? + } + [stream, seconds, microseconds] => eval_stream_set_timeout_result( + *stream, + *seconds, + Some(*microseconds), + context, + values, + )?, + _ => return Err(EvalStatus::RuntimeFatal), + }, "stream_socket_server" => { let [address] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -277,6 +365,9 @@ pub(in crate::interpreter) fn eval_filesystem_builtin_with_values( }; eval_stream_socket_pair_result(context, values)? } + "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { + eval_stream_wrapper_registry_result(name, evaluated_args, context, values)? + } "vfprintf" => { let [stream, format, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_register.rs new file mode 100644 index 0000000000..bfd1ebe565 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_register.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_wrapper_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream wrapper registry helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_wrapper_register", + area: Filesystem, + params: [ + protocol, + r#class, + flags = EvalBuiltinDefaultValue::Int(0) + ], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_restore.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_restore.rs new file mode 100644 index 0000000000..ba925f6aa5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_restore.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_wrapper_restore`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream wrapper registry helper. + +eval_builtin! { + name: "stream_wrapper_restore", + area: Filesystem, + params: [protocol], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_unregister.rs new file mode 100644 index 0000000000..05e5218deb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_unregister.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_wrapper_unregister`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the stream wrapper registry helper. + +eval_builtin! { + name: "stream_wrapper_unregister", + area: Filesystem, + params: [protocol], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/values_dispatch.rs new file mode 100644 index 0000000000..a71d22fc33 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/values_dispatch.rs @@ -0,0 +1,34 @@ +//! Purpose: +//! Routes evaluated-argument filesystem registry hooks to focused value dispatchers. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalValuesHook::call()`. +//! +//! Key details: +//! - Values hooks run after named/default argument binding has produced PHP +//! parameter order. + +use super::super::super::super::*; + +use super::path_values_dispatch::eval_filesystem_path_values_result; +use super::stream_values_dispatch::eval_filesystem_stream_values_result; + +/// Dispatches evaluated-argument calls for declaratively migrated filesystem builtins. +pub(in crate::interpreter) fn eval_filesystem_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = + eval_filesystem_path_values_result(name, evaluated_args, context, values)? + { + return Ok(result); + } + if let Some(result) = + eval_filesystem_stream_values_result(name, evaluated_args, context, values)? + { + return Ok(result); + } + Err(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/vfprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/vfprintf.rs new file mode 100644 index 0000000000..dd5847480e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/vfprintf.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `vfprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the vprintf-family stream write helper. + +eval_builtin! { + name: "vfprintf", + area: Filesystem, + params: [stream, format, values], + direct: Filesystem, + values: Filesystem, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index 1877253ac2..cda537cd44 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -218,6 +218,10 @@ macro_rules! eval_builtin { "break" }; + (@name_str r#class) => { + "class" + }; + (@name_str r#return) => { "return" }; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index a4e8c3a7f8..b8cc2f53e9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -147,15 +147,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "interface_exists" => Some(&["interface", "autoload"]), "trait_exists" => Some(&["trait", "autoload"]), "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), - "fgetcsv" => Some(&["stream", "length", "separator"]), - "fopen" => Some(&["filename", "mode", "use_include_path", "context"]), - "fputcsv" => Some(&["stream", "fields", "separator", "enclosure"]), - "fprintf" => Some(&["stream", "format", "values"]), - "fsockopen" | "pfsockopen" => { - Some(&["hostname", "port", "error_code", "error_message", "timeout"]) - } - "flock" => Some(&["stream", "operation", "would_block"]), - "fscanf" => Some(&["stream", "format", "vars"]), "function_exists" => Some(&["function"]), "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), "get_resource_id" | "get_resource_type" => Some(&["resource"]), @@ -172,7 +163,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } "ptr_write_string" => Some(&["pointer", "string"]), "ptr_sizeof" => Some(&["type"]), - "readline" => Some(&["prompt"]), "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), "spl_autoload_unregister" => Some(&["callback"]), "spl_autoload_functions" | "spl_classes" => Some(&[]), @@ -180,36 +170,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( "spl_autoload_call" => Some(&["class"]), "spl_autoload" => Some(&["class", "file_extensions"]), "spl_object_id" | "spl_object_hash" => Some(&["object"]), - "stream_bucket_make_writeable" => Some(&["brigade"]), - "stream_bucket_new" => Some(&["stream", "buffer"]), - "stream_bucket_append" | "stream_bucket_prepend" => Some(&["brigade", "bucket"]), - "stream_context_create" => Some(&["options", "params"]), - "stream_context_get_default" => Some(&["options"]), - "stream_context_get_options" | "stream_context_get_params" => Some(&["context"]), - "stream_context_set_default" => Some(&["options"]), - "stream_context_set_option" => { - Some(&["context", "wrapper_or_options", "option_name", "value"]) - } - "stream_context_set_params" => Some(&["context", "params"]), - "stream_filter_register" => Some(&["filter_name", "class"]), - "stream_filter_append" | "stream_filter_prepend" => { - Some(&["stream", "filtername", "read_write", "params"]) - } - "stream_filter_remove" => Some(&["stream_filter"]), - "stream_select" => Some(&["read", "write", "except", "seconds", "microseconds"]), - "stream_socket_server" | "stream_socket_client" => Some(&["address"]), - "stream_socket_accept" => Some(&["socket", "timeout", "peer_name"]), - "stream_socket_enable_crypto" => { - Some(&["stream", "enable", "crypto_method", "session_stream"]) - } - "stream_socket_get_name" => Some(&["socket", "remote"]), - "stream_socket_pair" => Some(&["domain", "type", "protocol"]), - "stream_socket_recvfrom" => Some(&["socket", "length", "flags", "address"]), - "stream_socket_sendto" => Some(&["socket", "data", "flags", "address"]), - "stream_socket_shutdown" => Some(&["stream", "mode"]), - "stream_wrapper_register" => Some(&["protocol", "class", "flags"]), - "stream_wrapper_unregister" | "stream_wrapper_restore" => Some(&["protocol"]), - "vfprintf" => Some(&["stream", "format", "values"]), _ => None, } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 9060eb9add..fac87802e8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -8,13 +8,11 @@ //! - Each child dispatcher handles already evaluated runtime-cell arguments for one //! builtin family and returns `Ok(None)` when the name is outside its domain. -mod filesystem; mod symbols; use super::eval_declared_builtin_values_call; use super::super::super::*; -use filesystem::*; use symbols::*; /// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. @@ -28,11 +26,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( return Ok(Some(result)); } - if let Some(result) = - eval_filesystem_builtin_with_values(name, evaluated_args, context, values)? - { - return Ok(Some(result)); - } if let Some(result) = eval_raw_memory_builtin_with_values(name, evaluated_args, context, values)? { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 432e2e647f..0f8e7edbbd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -916,6 +916,30 @@ mod tests { eval_declared_builtin_param_names("fread"), Some(["stream", "length"].as_slice()) ); + assert_eq!( + eval_declared_builtin_param_names("fgetcsv"), + Some(["stream", "length", "separator"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("fgetcsv", 2), + Some(EvalBuiltinDefaultValue::String(",")) + ); + assert_eq!( + eval_declared_builtin_param_names("flock"), + Some(["stream", "operation", "would_block"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("flock").map(|shape| shape.by_ref_params), + Some(["would_block"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fsockopen"), + Some(["hostname", "port", "error_code", "error_message", "timeout"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("fsockopen").map(|shape| shape.by_ref_params), + Some(["error_code", "error_message"].as_slice()) + ); assert_eq!( eval_declared_builtin_param_names("fwrite"), Some(["stream", "data"].as_slice()) @@ -956,6 +980,15 @@ mod tests { eval_declared_builtin_default_value("stream_get_contents", 2), Some(EvalBuiltinDefaultValue::Int(-1)) ); + assert_eq!( + eval_declared_builtin_param_names("stream_context_set_option"), + Some(["context", "wrapper_or_options", "option_name", "value"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_context_set_option") + .map(|shape| shape.required_param_count), + Some(2) + ); assert_eq!( eval_declared_builtin_param_names("stream_get_line"), Some(["stream", "length", "ending"].as_slice()) @@ -964,6 +997,31 @@ mod tests { eval_declared_builtin_default_value("stream_get_line", 2), Some(EvalBuiltinDefaultValue::String("")) ); + assert_eq!( + eval_declared_builtin_param_names("stream_select"), + Some(["read", "write", "except", "seconds", "microseconds"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_select").map(|shape| shape.by_ref_params), + Some(["read", "write", "except"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_socket_recvfrom"), + Some(["socket", "length", "flags", "address"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_socket_recvfrom") + .map(|shape| shape.by_ref_params), + Some(["address"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_wrapper_register"), + Some(["protocol", "class", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("vfprintf"), + Some(["stream", "format", "values"].as_slice()) + ); assert_eq!( eval_declared_builtin_param_names("stream_get_wrappers"), Some([].as_slice()) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index 269f7ffcb5..e896f301ce 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -27,13 +27,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "class_uses", "empty", "enum_exists", - "fgetcsv", - "flock", - "fopen", - "fprintf", - "fputcsv", - "fscanf", - "fsockopen", "function_exists", "get_called_class", "get_class", @@ -52,7 +45,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "is_subclass_of", "isset", "method_exists", - "pfsockopen", "ptr", "ptr_get", "ptr_is_null", @@ -69,7 +61,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ptr_write32", "ptr_write_string", "property_exists", - "readline", "spl_autoload", "spl_autoload_call", "spl_autoload_extensions", @@ -79,37 +70,8 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "spl_classes", "spl_object_hash", "spl_object_id", - "stream_bucket_append", - "stream_bucket_make_writeable", - "stream_bucket_new", - "stream_bucket_prepend", - "stream_context_create", - "stream_context_get_default", - "stream_context_get_options", - "stream_context_get_params", - "stream_context_set_default", - "stream_context_set_option", - "stream_context_set_params", - "stream_filter_append", - "stream_filter_prepend", - "stream_filter_register", - "stream_filter_remove", - "stream_select", - "stream_socket_accept", - "stream_socket_client", - "stream_socket_enable_crypto", - "stream_socket_get_name", - "stream_socket_pair", - "stream_socket_recvfrom", - "stream_socket_sendto", - "stream_socket_server", - "stream_socket_shutdown", - "stream_wrapper_register", - "stream_wrapper_restore", - "stream_wrapper_unregister", "trait_exists", "unset", - "vfprintf", ]; /// Combined PHP-visible builtin names from legacy and declarative registries. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index fbeb7d3d48..903728c642 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -71,25 +71,6 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( "is_a" | "is_subclass_of" => optional(params, 2), "is_callable" => optional_by_ref(params, 1, &["callable_name"]), - "readline" => optional(params, 0), - - "fprintf" | "fscanf" => variadic(params, &[]), - - "fopen" | "fputcsv" => optional(params, 2), - "flock" => optional_by_ref(params, 2, &["would_block"]), - "fgetcsv" => optional(params, 1), - "stream_socket_accept" => optional_by_ref(params, 1, &["peer_name"]), - "fsockopen" | "pfsockopen" => { - optional_by_ref(params, 2, &["error_code", "error_message"]) - } - "stream_wrapper_register" | "stream_socket_enable_crypto" => optional(params, 2), - "stream_context_create" | "stream_context_get_default" => optional(params, 0), - "stream_context_set_option" => optional(params, 2), - "stream_socket_sendto" | "stream_filter_append" | "stream_filter_prepend" => { - optional(params, 2) - } - "stream_select" => optional_by_ref(params, 4, &["read", "write", "except"]), - "stream_socket_recvfrom" => optional_by_ref(params, 2, &["address"]), "spl_autoload_register" | "spl_autoload_extensions" => optional(params, 0), "spl_autoload" => optional(params, 1), @@ -122,28 +103,6 @@ pub(in crate::interpreter) fn eval_builtin_default_value( ("is_callable", 1) => Bool(false), ("is_callable", 2) => Null, - ("readline", 0) => Null, - ("fopen", 2) => Bool(false), - ("fopen", 3) => Null, - ("flock", 2) => Null, - ("fgetcsv", 1) => Null, - ("fgetcsv", 2) => String(","), - ("fputcsv", 2) => String(","), - ("fputcsv", 3) => String("\""), - ("stream_socket_accept", 1 | 2) => Null, - ("fsockopen" | "pfsockopen", 2 | 3 | 4) => Null, - ("stream_wrapper_register", 2) => Int(0), - ("stream_socket_enable_crypto", 2 | 3) => Null, - ("stream_context_create", 0 | 1) => Null, - ("stream_context_get_default", 0) => Null, - ("stream_context_set_option", 2 | 3) => Null, - ("stream_select", 4) => Int(0), - ("stream_socket_sendto", 2) => Int(0), - ("stream_socket_sendto", 3) => String(""), - ("stream_socket_recvfrom", 2) => Int(0), - ("stream_socket_recvfrom", 3) => String(""), - ("stream_filter_append" | "stream_filter_prepend", 2) => Int(3), - ("stream_filter_append" | "stream_filter_prepend", 3) => Null, ("spl_autoload_register", 0) => Null, ("spl_autoload_register", 1) => Bool(true), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 361f3cb702..ee0e87d03c 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -17,9 +17,6 @@ const EVAL_DIRECT_DISPATCH_SOURCES: &[&str] = &[include_str!( const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ include_str!("../crates/elephc-magician/src/interpreter/builtins/raw_memory.rs"), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/filesystem.rs" - ), include_str!( "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs" ), @@ -169,15 +166,22 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "fdatasync", "feof", "fflush", + "fgetcsv", "fgetc", "fgets", "floatval", "floor", + "flock", "fmod", "fnmatch", + "fopen", + "fprintf", "fpassthru", + "fputcsv", "fread", + "fscanf", "fseek", + "fsockopen", "fstat", "ftruncate", "fsync", @@ -289,6 +293,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "pathinfo", "pclose", "passthru", + "pfsockopen", "pi", "php_uname", "phpversion", @@ -310,6 +315,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "rawurlencode", "readdir", "readfile", + "readline", "readlink", "realpath", "realpath_cache_get", @@ -334,7 +340,22 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "sprintf", "sscanf", "stat", + "stream_bucket_append", + "stream_bucket_make_writeable", + "stream_bucket_new", + "stream_bucket_prepend", + "stream_context_create", + "stream_context_get_default", + "stream_context_get_options", + "stream_context_get_params", + "stream_context_set_default", + "stream_context_set_option", + "stream_context_set_params", "stream_copy_to_stream", + "stream_filter_append", + "stream_filter_prepend", + "stream_filter_register", + "stream_filter_remove", "stream_get_contents", "stream_get_filters", "stream_get_line", @@ -343,6 +364,16 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "stream_get_wrappers", "stream_is_local", "stream_isatty", + "stream_select", + "stream_socket_accept", + "stream_socket_client", + "stream_socket_enable_crypto", + "stream_socket_get_name", + "stream_socket_pair", + "stream_socket_recvfrom", + "stream_socket_sendto", + "stream_socket_server", + "stream_socket_shutdown", "str_contains", "str_ends_with", "str_ireplace", @@ -372,6 +403,9 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "stream_set_timeout", "stream_set_write_buffer", "stream_supports_lock", + "stream_wrapper_register", + "stream_wrapper_restore", + "stream_wrapper_unregister", "system", "symlink", "sys_get_temp_dir", @@ -393,6 +427,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "usleep", "usort", "var_dump", + "vfprintf", "vprintf", "vsprintf", "wordwrap", From e04d2003c7b1f1ef9a0adb7eaea72c9f3c522de0 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 06:34:19 +0200 Subject: [PATCH 1091/1208] refactor(magician): migrate symbol builtins --- .../src/interpreter/builtins/hooks/arity.rs | 64 ++++++ .../src/interpreter/builtins/hooks/direct.rs | 5 +- .../src/interpreter/builtins/hooks/mod.rs | 1 + .../src/interpreter/builtins/hooks/values.rs | 62 +---- .../src/interpreter/builtins/macros.rs | 8 + .../interpreter/builtins/registry/binding.rs | 32 --- .../builtins/registry/dispatch/mod.rs | 7 - .../builtins/registry/dispatch/symbols.rs | 112 --------- .../src/interpreter/builtins/registry/mod.rs | 51 +++++ .../interpreter/builtins/registry/names.rs | 40 ---- .../builtins/registry/signature.rs | 78 +------ .../src/interpreter/builtins/spec.rs | 2 + .../src/interpreter/builtins/symbols.rs | 6 + .../symbols/declarations/class_alias.rs | 18 ++ .../declarations/class_attribute_args.rs | 16 ++ .../declarations/class_attribute_names.rs | 16 ++ .../symbols/declarations/class_exists.rs | 18 ++ .../declarations/class_get_attributes.rs | 16 ++ .../symbols/declarations/class_implements.rs | 18 ++ .../symbols/declarations/class_parents.rs | 18 ++ .../symbols/declarations/class_uses.rs | 18 ++ .../builtins/symbols/declarations/empty.rs | 16 ++ .../symbols/declarations/enum_exists.rs | 18 ++ .../symbols/declarations/function_exists.rs | 16 ++ .../symbols/declarations/get_called_class.rs | 16 ++ .../symbols/declarations/get_class.rs | 18 ++ .../symbols/declarations/get_class_methods.rs | 16 ++ .../symbols/declarations/get_class_vars.rs | 16 ++ .../declarations/get_declared_classes.rs | 16 ++ .../declarations/get_declared_interfaces.rs | 16 ++ .../declarations/get_declared_traits.rs | 16 ++ .../symbols/declarations/get_object_vars.rs | 16 ++ .../symbols/declarations/get_parent_class.rs | 18 ++ .../symbols/declarations/get_resource_id.rs | 16 ++ .../symbols/declarations/get_resource_type.rs | 16 ++ .../symbols/declarations/interface_exists.rs | 18 ++ .../builtins/symbols/declarations/is_a.rs | 18 ++ .../symbols/declarations/is_callable.rs | 23 ++ .../symbols/declarations/is_subclass_of.rs | 18 ++ .../builtins/symbols/declarations/isset.rs | 17 ++ .../symbols/declarations/method_exists.rs | 16 ++ .../builtins/symbols/declarations/mod.rs | 213 ++++++++++++++++++ .../symbols/declarations/property_exists.rs | 16 ++ .../symbols/declarations/spl_autoload.rs | 18 ++ .../symbols/declarations/spl_autoload_call.rs | 16 ++ .../declarations/spl_autoload_extensions.rs | 18 ++ .../declarations/spl_autoload_functions.rs | 16 ++ .../declarations/spl_autoload_register.rs | 22 ++ .../declarations/spl_autoload_unregister.rs | 16 ++ .../symbols/declarations/spl_classes.rs | 16 ++ .../symbols/declarations/spl_object_hash.rs | 16 ++ .../symbols/declarations/spl_object_id.rs | 16 ++ .../symbols/declarations/trait_exists.rs | 18 ++ .../builtins/symbols/declarations/unset.rs | 17 ++ tests/builtin_parity_tests.rs | 49 +++- 55 files changed, 1084 insertions(+), 329 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/arity.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_alias.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_args.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_names.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_get_attributes.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_implements.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_parents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_uses.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/empty.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/enum_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/function_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_called_class.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_methods.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_vars.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_classes.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_interfaces.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_traits.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_object_vars.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_parent_class.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_id.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_type.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/interface_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_a.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_callable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_subclass_of.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/isset.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/method_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/property_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_call.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_extensions.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_functions.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_register.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_unregister.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_classes.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_hash.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_id.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/trait_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/unset.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/arity.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/arity.rs new file mode 100644 index 0000000000..df391d4d9a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/arity.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Shared fixed-arity helpers for declarative builtin values hooks. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::values`. +//! +//! Key details: +//! - Helpers validate already-bound argument slices before delegating to the +//! concrete runtime-value operation. + +use super::super::super::{EvalStatus, RuntimeCellHandle, RuntimeValueOps}; + +/// Validates and dispatches one evaluated builtin argument. +pub(super) fn one_arg( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce(RuntimeCellHandle, &mut V) -> Result, +{ + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*value, values) +} + +/// Validates and dispatches two evaluated builtin arguments. +pub(super) fn two_args( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce(RuntimeCellHandle, RuntimeCellHandle, &mut V) -> Result, +{ + let [left, right] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*left, *right, values) +} + +/// Validates and dispatches three evaluated builtin arguments. +pub(super) fn three_args( + evaluated_args: &[RuntimeCellHandle], + values: &mut V, + callback: F, +) -> Result +where + V: RuntimeValueOps, + F: FnOnce( + RuntimeCellHandle, + RuntimeCellHandle, + RuntimeCellHandle, + &mut V, + ) -> Result, +{ + let [first, second, third] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + callback(*first, *second, *third, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 215263d0e4..deda7ba14a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -36,7 +36,7 @@ use super::super::{ eval_builtin_str_split, eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, - eval_builtin_substr_replace, eval_builtin_time_call, eval_builtin_trim_like, + eval_builtin_substr_replace, eval_builtin_symbols_call, eval_builtin_time_call, eval_builtin_trim_like, eval_builtin_ucwords, eval_builtin_wordwrap, }; @@ -181,6 +181,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Substr, /// Dispatches `substr_replace(...)`. SubstrReplace, + /// Dispatches symbol, class metadata, SPL, and language-construct probes. + Symbols, /// Dispatches date, time, and sleep builtins. Time, /// Dispatches trim-family builtins. @@ -299,6 +301,7 @@ impl EvalDirectHook { Self::Strstr => eval_builtin_strstr(args, context, scope, values), Self::Substr => eval_builtin_substr(args, context, scope, values), Self::SubstrReplace => eval_builtin_substr_replace(args, context, scope, values), + Self::Symbols => eval_builtin_symbols_call(name, args, context, scope, values), Self::Time => eval_builtin_time_call(name, args, context, scope, values), Self::TrimLike => eval_builtin_trim_like(name, args, context, scope, values), Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs index f1ec512c1f..ef9ea87c97 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs @@ -8,6 +8,7 @@ //! - Direct expression dispatch, already-evaluated argument dispatch, and //! focused hook helpers stay split so ordinary files remain small. +mod arity; mod direct; mod hash; mod number_format; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 81eda84fc8..c37e582a6f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -31,9 +31,11 @@ use super::super::{ eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, - eval_time_values_result, eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, - eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, + eval_symbols_values_result, eval_time_values_result, eval_trim_like_result, + eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, + eval_wordwrap_result, }; +use super::arity::{one_arg, three_args, two_args}; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; use super::number_format::eval_number_format_values; use super::random::eval_random_values; @@ -184,6 +186,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Substr, /// Dispatches `substr_replace(...)`. SubstrReplace, + /// Dispatches symbol, class metadata, SPL, and language-construct probes. + Symbols, /// Dispatches date, time, and sleep builtins. Time, /// Dispatches trim-family builtins. @@ -397,6 +401,7 @@ impl EvalValuesHook { } _ => Err(EvalStatus::RuntimeFatal), }, + Self::Symbols => eval_symbols_values_result(name, evaluated_args, context, values), Self::Time => eval_time_values_result(name, evaluated_args, context, values), Self::TrimLike => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values), @@ -443,56 +448,3 @@ impl EvalValuesHook { } } } - -/// Validates and dispatches one evaluated builtin argument. -fn one_arg( - evaluated_args: &[RuntimeCellHandle], - values: &mut V, - callback: F, -) -> Result -where - V: RuntimeValueOps, - F: FnOnce(RuntimeCellHandle, &mut V) -> Result, -{ - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - callback(*value, values) -} - -/// Validates and dispatches two evaluated builtin arguments. -fn two_args( - evaluated_args: &[RuntimeCellHandle], - values: &mut V, - callback: F, -) -> Result -where - V: RuntimeValueOps, - F: FnOnce(RuntimeCellHandle, RuntimeCellHandle, &mut V) -> Result, -{ - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - callback(*left, *right, values) -} - -/// Validates and dispatches three evaluated builtin arguments. -fn three_args( - evaluated_args: &[RuntimeCellHandle], - values: &mut V, - callback: F, -) -> Result -where - V: RuntimeValueOps, - F: FnOnce( - RuntimeCellHandle, - RuntimeCellHandle, - RuntimeCellHandle, - &mut V, - ) -> Result, -{ - let [first, second, third] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - callback(*first, *second, *third, values) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index cda537cd44..ead2b04c4d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -222,10 +222,18 @@ macro_rules! eval_builtin { "class" }; + (@name_str r#enum) => { + "enum" + }; + (@name_str r#return) => { "return" }; + (@name_str r#trait) => { + "trait" + }; + (@name_str r#type) => { "type" }; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index b8cc2f53e9..ed108ca74e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -124,33 +124,8 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } match name { - "empty" => Some(&["value"]), - "is_callable" => Some(&["value", "syntax_only", "callable_name"]), "buffer_new" => Some(&["length"]), "buffer_len" | "buffer_free" => Some(&["buffer"]), - "get_called_class" => Some(&[]), - "get_class" => Some(&["object"]), - "get_class_methods" => Some(&["object_or_class"]), - "get_class_vars" => Some(&["class"]), - "get_object_vars" => Some(&["object"]), - "get_parent_class" => Some(&["object_or_class"]), - "class_alias" => Some(&["class", "alias", "autoload"]), - "class_attribute_args" => Some(&["class_name", "attribute_name"]), - "class_attribute_names" | "class_get_attributes" => Some(&["class_name"]), - "class_exists" => Some(&["class", "autoload"]), - "class_implements" | "class_parents" | "class_uses" => { - Some(&["object_or_class", "autoload"]) - } - "method_exists" => Some(&["object_or_class", "method"]), - "property_exists" => Some(&["object_or_class", "property"]), - "enum_exists" => Some(&["enum", "autoload"]), - "interface_exists" => Some(&["interface", "autoload"]), - "trait_exists" => Some(&["trait", "autoload"]), - "is_a" | "is_subclass_of" => Some(&["object_or_class", "class", "allow_string"]), - "function_exists" => Some(&["function"]), - "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => Some(&[]), - "get_resource_id" | "get_resource_type" => Some(&["resource"]), - "isset" | "unset" => Some(&["var", "vars"]), "ptr" => Some(&["value"]), "ptr_null" => Some(&[]), "ptr_is_null" | "ptr_get" | "ptr_read8" | "ptr_read16" | "ptr_read32" => { @@ -163,13 +138,6 @@ pub(in crate::interpreter) fn eval_builtin_param_names( } "ptr_write_string" => Some(&["pointer", "string"]), "ptr_sizeof" => Some(&["type"]), - "spl_autoload_register" => Some(&["callback", "throw", "prepend"]), - "spl_autoload_unregister" => Some(&["callback"]), - "spl_autoload_functions" | "spl_classes" => Some(&[]), - "spl_autoload_extensions" => Some(&["file_extensions"]), - "spl_autoload_call" => Some(&["class"]), - "spl_autoload" => Some(&["class", "file_extensions"]), - "spl_object_id" | "spl_object_hash" => Some(&["object"]), _ => None, } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index fac87802e8..6019c1311a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -8,13 +8,9 @@ //! - Each child dispatcher handles already evaluated runtime-cell arguments for one //! builtin family and returns `Ok(None)` when the name is outside its domain. -mod symbols; - use super::eval_declared_builtin_values_call; use super::super::super::*; -use symbols::*; - /// Evaluates PHP-visible builtins when they are invoked through a dynamic callable name. pub(in crate::interpreter) fn eval_builtin_with_values( name: &str, @@ -36,8 +32,5 @@ pub(in crate::interpreter) fn eval_builtin_with_values( { return Ok(Some(result)); } - if let Some(result) = eval_symbols_builtin_with_values(name, evaluated_args, context, values)? { - return Ok(Some(result)); - } Ok(None) } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs deleted file mode 100644 index 882c38e504..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Purpose: -//! Dispatches already evaluated symbol, class, object, and resource introspection builtins by dynamic callable name. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry::dispatch`. -//! -//! Key details: -//! - Returns `Ok(None)` for names outside this domain so the parent dispatcher can -//! continue probing other builtin families. - -use super::super::super::super::*; -use super::super::super::*; - -/// Attempts to dispatch evaluated symbol, class, object, and resource introspection builtins. -pub(in crate::interpreter) fn eval_symbols_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "spl_classes" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values)? - } - "spl_autoload_register" | "spl_autoload_unregister" => { - eval_spl_autoload_bool_result(name, evaluated_args, values)? - } - "spl_autoload" | "spl_autoload_call" => { - eval_spl_autoload_void_result(name, evaluated_args, values)? - } - "spl_autoload_functions" => eval_spl_autoload_functions_result(evaluated_args, values)?, - "spl_autoload_extensions" => { - eval_spl_autoload_extensions_result(evaluated_args, context, values)? - } - "spl_object_id" | "spl_object_hash" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_spl_object_identity_result(name, *object, values)? - } - "function_exists" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_function_probe_result(name, *value, context, values)? - } - "is_callable" => eval_is_callable_with_values(evaluated_args, context, values)?, - "empty" => eval_empty_result(evaluated_args, values)?, - "isset" => eval_isset_result(evaluated_args, values)?, - "unset" => eval_unset_result(evaluated_args, values)?, - "class_exists" => eval_class_exists_result(evaluated_args, context, values)?, - "class_alias" => eval_class_alias_result(evaluated_args, context, values)?, - "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { - eval_class_attribute_metadata_result(name, evaluated_args, context, values)? - } - "class_implements" | "class_parents" | "class_uses" => { - eval_class_relation_result(name, evaluated_args, context, values)? - } - "method_exists" | "property_exists" => { - eval_member_exists_result(name, evaluated_args, context, values)? - } - "get_class_methods" => eval_get_class_methods_result(evaluated_args, context, values)?, - "get_class_vars" => eval_get_class_vars_result(evaluated_args, context, values)?, - "get_object_vars" => eval_get_object_vars_result(evaluated_args, context, values)?, - "enum_exists" | "trait_exists" => { - eval_class_like_exists_result(name, evaluated_args, context, values)? - } - "interface_exists" => eval_interface_exists_result(evaluated_args, context, values)?, - "is_a" | "is_subclass_of" => { - eval_is_a_relation_result(name, evaluated_args, context, values)? - } - "get_called_class" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_called_class_result(context, values)? - } - "get_class" => { - match evaluated_args { - [] => eval_get_class_no_arg_result(context, values)?, - [object] => eval_get_class_result(*object, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - } - } - "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_declared_symbols_result(name, context, values)? - } - "get_parent_class" => { - match evaluated_args { - [] => eval_get_parent_class_no_arg_result(context, values)?, - [object_or_class] => { - eval_get_parent_class_result(*object_or_class, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - "get_resource_id" | "get_resource_type" => { - let [resource] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_resource_introspection_result(name, *resource, values)? - } - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 0f8e7edbbd..c338653ef6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -234,6 +234,8 @@ mod tests { "chgrp", "chmod", "chown", + "class_alias", + "class_exists", "closedir", "clearstatcache", "copy", @@ -250,6 +252,7 @@ mod tests { "die", "exec", "exit", + "function_exists", "explode", "file", "file_exists", @@ -281,6 +284,7 @@ mod tests { "ftell", "fwrite", "getdate", + "get_class", "getcwd", "getenv", "gethostbyaddr", @@ -317,11 +321,13 @@ mod tests { "inet_ntop", "inet_pton", "intval", + "interface_exists", "iterator_apply", "iterator_count", "iterator_to_array", "is_array", "is_bool", + "is_callable", "is_dir", "is_double", "is_executable", @@ -343,6 +349,7 @@ mod tests { "is_resource", "is_scalar", "is_string", + "is_subclass_of", "is_writable", "is_writeable", "ip2long", @@ -408,6 +415,8 @@ mod tests { "sleep", "sort", "sprintf", + "spl_autoload", + "spl_object_id", "sscanf", "stat", "stream_copy_to_stream", @@ -441,12 +450,14 @@ mod tests { "time", "tmpfile", "touch", + "trait_exists", "trim", "strval", "uasort", "uksort", "umask", "unlink", + "unset", "usleep", "usort", "var_dump", @@ -568,6 +579,46 @@ mod tests { eval_builtin_signature_shape("var_dump").map(|shape| shape.variadic), Some(Some("values")) ); + assert_eq!( + eval_declared_builtin_param_names("class_exists"), + Some(["class", "autoload"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("class_exists", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); + assert_eq!( + eval_declared_builtin_param_names("get_class"), + Some(["object"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("get_class", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_param_names("is_callable"), + Some(["value", "syntax_only", "callable_name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_spec("is_callable").map(EvalBuiltinSpec::by_ref_param_names), + Some(["callable_name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("is_callable", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_builtin_signature_shape("isset").map(|shape| shape.variadic), + Some(Some("vars")) + ); + assert_eq!( + eval_declared_builtin_param_names("spl_autoload_register"), + Some(["callback", "throw", "prepend"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("spl_autoload_register", 2), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); for name in ["rand", "mt_rand", "random_int"] { assert_eq!( eval_declared_builtin_param_names(name), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index e896f301ce..f936986478 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -17,34 +17,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "buffer_free", "buffer_len", "buffer_new", - "class_alias", - "class_attribute_args", - "class_attribute_names", - "class_exists", - "class_get_attributes", - "class_implements", - "class_parents", - "class_uses", - "empty", - "enum_exists", - "function_exists", - "get_called_class", - "get_class", - "get_class_methods", - "get_class_vars", - "get_declared_classes", - "get_declared_interfaces", - "get_declared_traits", - "get_object_vars", - "get_parent_class", - "get_resource_id", - "get_resource_type", - "interface_exists", - "is_a", - "is_callable", - "is_subclass_of", - "isset", - "method_exists", "ptr", "ptr_get", "ptr_is_null", @@ -60,18 +32,6 @@ pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = & "ptr_write16", "ptr_write32", "ptr_write_string", - "property_exists", - "spl_autoload", - "spl_autoload_call", - "spl_autoload_extensions", - "spl_autoload_functions", - "spl_autoload_register", - "spl_autoload_unregister", - "spl_classes", - "spl_object_hash", - "spl_object_id", - "trait_exists", - "unset", ]; /// Combined PHP-visible builtin names from legacy and declarative registries. diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index 903728c642..b64bd82ddd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -62,21 +62,7 @@ pub(in crate::interpreter) fn eval_builtin_signature_shape( } let params = eval_builtin_param_names(name)?; - Some(match name { - "isset" | "unset" => variadic(params, &[]), - "class_alias" => optional(params, 2), - "class_exists" | "interface_exists" | "trait_exists" | "enum_exists" - | "class_implements" | "class_parents" | "class_uses" => optional(params, 1), - "get_class" | "get_parent_class" => optional(params, 0), - "is_a" | "is_subclass_of" => optional(params, 2), - - "is_callable" => optional_by_ref(params, 1, &["callable_name"]), - - "spl_autoload_register" | "spl_autoload_extensions" => optional(params, 0), - "spl_autoload" => optional(params, 1), - - _ => fixed(params), - }) + Some(fixed(params)) } /// Returns the concrete default value for one optional builtin parameter. @@ -88,30 +74,7 @@ pub(in crate::interpreter) fn eval_builtin_default_value( return Some(default_value); } - use EvalBuiltinDefaultValue::*; - - Some(match (name, param_index) { - ("class_alias", 2) => Bool(true), - ( - "class_exists" | "interface_exists" | "trait_exists" | "enum_exists" - | "class_implements" | "class_parents" | "class_uses", - 1, - ) => Bool(true), - ("get_class" | "get_parent_class", 0) => Null, - ("is_a", 2) => Bool(false), - ("is_subclass_of", 2) => Bool(true), - - ("is_callable", 1) => Bool(false), - ("is_callable", 2) => Null, - - ("spl_autoload_register", 0) => Null, - ("spl_autoload_register", 1) => Bool(true), - ("spl_autoload_register", 2) => Bool(false), - ("spl_autoload_extensions", 0) => Null, - ("spl_autoload", 1) => Null, - - _ => return None, - }) + None } /// Builds fixed-arity signature shape. @@ -119,43 +82,6 @@ fn fixed(params: &[&'static str]) -> EvalBuiltinSignatureShape { shape(params.len(), 0, None, &[]) } -/// Builds trailing-default signature shape. -fn optional(params: &[&'static str], required_param_count: usize) -> EvalBuiltinSignatureShape { - shape( - required_param_count, - params.len().saturating_sub(required_param_count), - None, - &[], - ) -} - -/// Builds trailing-default signature shape with by-reference parameters. -fn optional_by_ref( - params: &[&'static str], - required_param_count: usize, - by_ref_params: &'static [&'static str], -) -> EvalBuiltinSignatureShape { - shape( - required_param_count, - params.len().saturating_sub(required_param_count), - None, - by_ref_params, - ) -} - -/// Builds variadic signature shape. -fn variadic( - params: &[&'static str], - by_ref_params: &'static [&'static str], -) -> EvalBuiltinSignatureShape { - shape( - params.len().saturating_sub(1), - 1, - params.last().copied(), - by_ref_params, - ) -} - /// Builds the raw signature-shape value. fn shape( required_param_count: usize, diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index de457b5ff1..6775693e79 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -36,6 +36,8 @@ pub(in crate::interpreter) enum EvalArea { Regex, /// String-processing builtins. String, + /// Symbol, class metadata, SPL, and language-construct probes. + Symbols, /// Date, time, and sleep builtins. Time, /// Scalar conversion and type-related builtins. diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 62802b31f0..04798dbd4e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -9,6 +9,12 @@ //! behavior through `RuntimeValueOps`. use super::super::*; + +mod declarations; + +pub(in crate::interpreter) use declarations::{ + eval_builtin_symbols_call, eval_symbols_values_result, +}; use super::*; const EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS: &[&str] = &[ diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_alias.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_alias.rs new file mode 100644 index 0000000000..4b5953ec1e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_alias.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `class_alias`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the symbol dispatch adapter. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_alias", + area: Symbols, + params: [r#class, alias, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_args.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_args.rs new file mode 100644 index 0000000000..91a2d05cd0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_args.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `class_attribute_args`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-attribute metadata helper. + +eval_builtin! { + name: "class_attribute_args", + area: Symbols, + params: [class_name, attribute_name], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_names.rs new file mode 100644 index 0000000000..a1d18f083a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_names.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `class_attribute_names`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-attribute metadata helper. + +eval_builtin! { + name: "class_attribute_names", + area: Symbols, + params: [class_name], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_exists.rs new file mode 100644 index 0000000000..279e92921c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_exists.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `class_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-existence probe. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_exists", + area: Symbols, + params: [r#class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_get_attributes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_get_attributes.rs new file mode 100644 index 0000000000..c6228f81a3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_get_attributes.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `class_get_attributes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-attribute metadata helper. + +eval_builtin! { + name: "class_get_attributes", + area: Symbols, + params: [class_name], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_implements.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_implements.rs new file mode 100644 index 0000000000..f353f86425 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_implements.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `class_implements`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation metadata helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_implements", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_parents.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_parents.rs new file mode 100644 index 0000000000..8a3fabef30 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_parents.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `class_parents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation metadata helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_parents", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_uses.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_uses.rs new file mode 100644 index 0000000000..188da8681c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_uses.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `class_uses`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation metadata helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_uses", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/empty.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/empty.rs new file mode 100644 index 0000000000..20def552bb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/empty.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `empty`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so missing variables are not evaluated normally. + +eval_builtin! { + name: "empty", + area: Symbols, + params: [value], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/enum_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/enum_exists.rs new file mode 100644 index 0000000000..c24fd26119 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/enum_exists.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `enum_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-like existence probe. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "enum_exists", + area: Symbols, + params: [r#enum, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/function_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/function_exists.rs new file mode 100644 index 0000000000..2bd686cb45 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/function_exists.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `function_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the builtin/function probe helper. + +eval_builtin! { + name: "function_exists", + area: Symbols, + params: [function], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_called_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_called_class.rs new file mode 100644 index 0000000000..c1c8bd7c40 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_called_class.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_called_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the current-class scope helper. + +eval_builtin! { + name: "get_called_class", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class.rs new file mode 100644 index 0000000000..449b0602ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `get_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-name introspection helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "get_class", + area: Symbols, + params: [object = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_methods.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_methods.rs new file mode 100644 index 0000000000..c98736b9f0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_methods.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_class_methods`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP introspection helper. + +eval_builtin! { + name: "get_class_methods", + area: Symbols, + params: [object_or_class], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_vars.rs new file mode 100644 index 0000000000..1f1bae3ca6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_vars.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_class_vars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP introspection helper. + +eval_builtin! { + name: "get_class_vars", + area: Symbols, + params: [r#class], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_classes.rs new file mode 100644 index 0000000000..ccc9bb654a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_classes.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_declared_classes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the declared-symbols helper. + +eval_builtin! { + name: "get_declared_classes", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_interfaces.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_interfaces.rs new file mode 100644 index 0000000000..19006cf99c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_interfaces.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_declared_interfaces`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the declared-symbols helper. + +eval_builtin! { + name: "get_declared_interfaces", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_traits.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_traits.rs new file mode 100644 index 0000000000..9c6abbb84d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_traits.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_declared_traits`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the declared-symbols helper. + +eval_builtin! { + name: "get_declared_traits", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_object_vars.rs new file mode 100644 index 0000000000..cec350dd9a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_object_vars.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_object_vars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP introspection helper. + +eval_builtin! { + name: "get_object_vars", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_parent_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_parent_class.rs new file mode 100644 index 0000000000..31a7cd4fe8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_parent_class.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `get_parent_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the parent-class introspection helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "get_parent_class", + area: Symbols, + params: [object_or_class = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_id.rs new file mode 100644 index 0000000000..49fc720e52 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_id.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_resource_id`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the resource introspection helper. + +eval_builtin! { + name: "get_resource_id", + area: Symbols, + params: [resource], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_type.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_type.rs new file mode 100644 index 0000000000..2a7468201c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_type.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `get_resource_type`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the resource introspection helper. + +eval_builtin! { + name: "get_resource_type", + area: Symbols, + params: [resource], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/interface_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/interface_exists.rs new file mode 100644 index 0000000000..a4bbac70d7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/interface_exists.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `interface_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the interface-existence probe. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "interface_exists", + area: Symbols, + params: [interface, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_a.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_a.rs new file mode 100644 index 0000000000..e28e830132 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_a.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `is_a`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation predicate helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_a", + area: Symbols, + params: [object_or_class, r#class, allow_string = EvalBuiltinDefaultValue::Bool(false)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_callable.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_callable.rs new file mode 100644 index 0000000000..df5cbb9d9b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_callable.rs @@ -0,0 +1,23 @@ +//! Purpose: +//! Declarative eval registry entry for `is_callable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Direct and dynamic-ref paths preserve `$callable_name` writeback elsewhere. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_callable", + area: Symbols, + params: [ + value, + syntax_only = EvalBuiltinDefaultValue::Bool(false), + callable_name: by_ref = EvalBuiltinDefaultValue::Null + ], + by_ref: [callable_name], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_subclass_of.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_subclass_of.rs new file mode 100644 index 0000000000..285a140cc3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_subclass_of.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `is_subclass_of`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation predicate helper. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_subclass_of", + area: Symbols, + params: [object_or_class, r#class, allow_string = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/isset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/isset.rs new file mode 100644 index 0000000000..cf3e878704 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/isset.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `isset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so operands are checked without normal reads. + +eval_builtin! { + name: "isset", + area: Symbols, + params: [var], + variadic: vars, + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/method_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/method_exists.rs new file mode 100644 index 0000000000..190d6d8c50 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/method_exists.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `method_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP member-existence helper. + +eval_builtin! { + name: "method_exists", + area: Symbols, + params: [object_or_class, method], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs new file mode 100644 index 0000000000..0c5d789151 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs @@ -0,0 +1,213 @@ +//! Purpose: +//! Declarative eval registry entries and dispatch adapters for symbol and class metadata builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols` module loading. +//! - `crate::interpreter::builtins::hooks` for migrated symbol dispatch. +//! +//! Key details: +//! - Direct adapters preserve source-sensitive language constructs and +//! `is_callable()` by-reference writeback. +//! - Values adapters replace the legacy dynamic symbols dispatcher. + +use super::super::super::*; +use super::super::*; + +mod class_alias; +mod class_attribute_args; +mod class_attribute_names; +mod class_exists; +mod class_get_attributes; +mod class_implements; +mod class_parents; +mod class_uses; +mod empty; +mod enum_exists; +mod function_exists; +mod get_called_class; +mod get_class; +mod get_class_methods; +mod get_class_vars; +mod get_declared_classes; +mod get_declared_interfaces; +mod get_declared_traits; +mod get_object_vars; +mod get_parent_class; +mod get_resource_id; +mod get_resource_type; +mod interface_exists; +mod is_a; +mod is_callable; +mod is_subclass_of; +mod isset; +mod method_exists; +mod property_exists; +mod spl_autoload; +mod spl_autoload_call; +mod spl_autoload_extensions; +mod spl_autoload_functions; +mod spl_autoload_register; +mod spl_autoload_unregister; +mod spl_classes; +mod spl_object_hash; +mod spl_object_id; +mod trait_exists; +mod unset; + +/// Dispatches direct expression-level calls for declaratively migrated symbol builtins. +pub(in crate::interpreter) fn eval_builtin_symbols_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "class_alias" => eval_builtin_class_alias(args, context, scope, values), + "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { + eval_builtin_class_attribute_metadata(name, args, context, scope, values) + } + "class_exists" => eval_builtin_class_exists(args, context, scope, values), + "class_implements" | "class_parents" | "class_uses" => { + eval_builtin_class_relation(name, args, context, scope, values) + } + "empty" => eval_builtin_empty(args, context, scope, values), + "enum_exists" | "trait_exists" => { + eval_builtin_class_like_exists(name, args, context, scope, values) + } + "function_exists" | "is_callable" => { + eval_builtin_function_probe(name, args, context, scope, values) + } + "get_called_class" => eval_builtin_get_called_class(args, context, values), + "get_class" => eval_builtin_get_class(args, context, scope, values), + "get_class_methods" => eval_builtin_get_class_methods(args, context, scope, values), + "get_class_vars" => eval_builtin_get_class_vars(args, context, scope, values), + "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { + eval_builtin_get_declared_symbols(name, args, context, values) + } + "get_object_vars" => eval_builtin_get_object_vars(args, context, scope, values), + "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), + "get_resource_id" | "get_resource_type" => { + eval_builtin_resource_introspection(name, args, context, scope, values) + } + "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), + "is_a" | "is_subclass_of" => { + eval_builtin_is_a_relation(name, args, context, scope, values) + } + "isset" => eval_builtin_isset(args, context, scope, values), + "method_exists" | "property_exists" => { + eval_builtin_member_exists(name, args, context, scope, values) + } + "spl_autoload" | "spl_autoload_call" => { + eval_builtin_spl_autoload_void(name, args, context, scope, values) + } + "spl_autoload_extensions" => { + eval_builtin_spl_autoload_extensions(args, context, scope, values) + } + "spl_autoload_functions" => { + eval_builtin_spl_autoload_functions(args, context, scope, values) + } + "spl_autoload_register" | "spl_autoload_unregister" => { + eval_builtin_spl_autoload_bool(name, args, context, scope, values) + } + "spl_classes" => eval_builtin_spl_classes(args, values), + "spl_object_hash" | "spl_object_id" => { + eval_builtin_spl_object_identity(name, args, context, scope, values) + } + "unset" => eval_builtin_unset(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated symbol builtins. +pub(in crate::interpreter) fn eval_symbols_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "class_alias" => eval_class_alias_result(evaluated_args, context, values), + "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { + eval_class_attribute_metadata_result(name, evaluated_args, context, values) + } + "class_exists" => eval_class_exists_result(evaluated_args, context, values), + "class_implements" | "class_parents" | "class_uses" => { + eval_class_relation_result(name, evaluated_args, context, values) + } + "empty" => eval_empty_result(evaluated_args, values), + "enum_exists" | "trait_exists" => { + eval_class_like_exists_result(name, evaluated_args, context, values) + } + "function_exists" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_function_probe_result(name, *value, context, values) + } + "get_called_class" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_called_class_result(context, values) + } + "get_class" => match evaluated_args { + [] => eval_get_class_no_arg_result(context, values), + [object] => eval_get_class_result(*object, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "get_class_methods" => eval_get_class_methods_result(evaluated_args, context, values), + "get_class_vars" => eval_get_class_vars_result(evaluated_args, context, values), + "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_declared_symbols_result(name, context, values) + } + "get_object_vars" => eval_get_object_vars_result(evaluated_args, context, values), + "get_parent_class" => match evaluated_args { + [] => eval_get_parent_class_no_arg_result(context, values), + [object_or_class] => eval_get_parent_class_result(*object_or_class, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "get_resource_id" | "get_resource_type" => { + let [resource] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_resource_introspection_result(name, *resource, values) + } + "interface_exists" => eval_interface_exists_result(evaluated_args, context, values), + "is_a" | "is_subclass_of" => { + eval_is_a_relation_result(name, evaluated_args, context, values) + } + "is_callable" => eval_is_callable_with_values(evaluated_args, context, values), + "isset" => eval_isset_result(evaluated_args, values), + "method_exists" | "property_exists" => { + eval_member_exists_result(name, evaluated_args, context, values) + } + "spl_autoload" | "spl_autoload_call" => { + eval_spl_autoload_void_result(name, evaluated_args, values) + } + "spl_autoload_extensions" => { + eval_spl_autoload_extensions_result(evaluated_args, context, values) + } + "spl_autoload_functions" => eval_spl_autoload_functions_result(evaluated_args, values), + "spl_autoload_register" | "spl_autoload_unregister" => { + eval_spl_autoload_bool_result(name, evaluated_args, values) + } + "spl_classes" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values) + } + "spl_object_hash" | "spl_object_id" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_spl_object_identity_result(name, *object, values) + } + "unset" => eval_unset_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/property_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/property_exists.rs new file mode 100644 index 0000000000..99b35e4fa7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/property_exists.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `property_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP member-existence helper. + +eval_builtin! { + name: "property_exists", + area: Symbols, + params: [object_or_class, property], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload.rs new file mode 100644 index 0000000000..b5bfdc0e7a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload stub. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload", + area: Symbols, + params: [class, file_extensions = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_call.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_call.rs new file mode 100644 index 0000000000..4399ce7ffc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_call.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_call`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload stub. + +eval_builtin! { + name: "spl_autoload_call", + area: Symbols, + params: [class], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_extensions.rs new file mode 100644 index 0000000000..d6638aa405 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_extensions.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_extensions`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to eval-local autoload extension state. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload_extensions", + area: Symbols, + params: [file_extensions = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_functions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_functions.rs new file mode 100644 index 0000000000..f6f01926cb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_functions.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_functions`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload stub. + +eval_builtin! { + name: "spl_autoload_functions", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_register.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_register.rs new file mode 100644 index 0000000000..9a85579345 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_register.rs @@ -0,0 +1,22 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload registration stub. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload_register", + area: Symbols, + params: [ + callback = EvalBuiltinDefaultValue::Null, + throw = EvalBuiltinDefaultValue::Bool(true), + prepend = EvalBuiltinDefaultValue::Bool(false), + ], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_unregister.rs new file mode 100644 index 0000000000..3b87bfeb6c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_unregister.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_unregister`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload registration stub. + +eval_builtin! { + name: "spl_autoload_unregister", + area: Symbols, + params: [callback], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_classes.rs new file mode 100644 index 0000000000..960999857f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_classes.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_classes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL classes helper. + +eval_builtin! { + name: "spl_classes", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_hash.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_hash.rs new file mode 100644 index 0000000000..f4395b3cfe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_hash.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_object_hash`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL object identity helper. + +eval_builtin! { + name: "spl_object_hash", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_id.rs new file mode 100644 index 0000000000..f5c827c580 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_id.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_object_id`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL object identity helper. + +eval_builtin! { + name: "spl_object_id", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/trait_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/trait_exists.rs new file mode 100644 index 0000000000..24ecc4996a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/trait_exists.rs @@ -0,0 +1,18 @@ +//! Purpose: +//! Declarative eval registry entry for `trait_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-like existence probe. + +use super::super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "trait_exists", + area: Symbols, + params: [r#trait, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/unset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/unset.rs new file mode 100644 index 0000000000..295b2de553 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/unset.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `unset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols::declarations`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so writable operands can be removed. + +eval_builtin! { + name: "unset", + area: Symbols, + params: [var], + variadic: vars, + direct: Symbols, + values: Symbols, +} diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index ee0e87d03c..b47bc81893 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -15,12 +15,9 @@ const EVAL_DIRECT_DISPATCH_SOURCES: &[&str] = &[include_str!( "../crates/elephc-magician/src/interpreter/expressions.rs" )]; -const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[ - include_str!("../crates/elephc-magician/src/interpreter/builtins/raw_memory.rs"), - include_str!( - "../crates/elephc-magician/src/interpreter/builtins/registry/dispatch/symbols.rs" - ), -]; +const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[include_str!( + "../crates/elephc-magician/src/interpreter/builtins/raw_memory.rs" +)]; /// Eval-only reflection probes exist because magician can inspect dynamic eval metadata before the AOT catalog exposes them. const EVAL_ONLY_REFLECTION_BUILTINS: &[&str] = &[ @@ -120,6 +117,14 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "chgrp", "chmod", "chown", + "class_alias", + "class_attribute_args", + "class_attribute_names", + "class_exists", + "class_get_attributes", + "class_implements", + "class_parents", + "class_uses", "closedir", "chr", "chop", @@ -146,6 +151,8 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "disk_total_space", "exec", "exit", + "empty", + "enum_exists", "explode", "exp", "fdiv", @@ -187,14 +194,26 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "fsync", "ftell", "fwrite", + "function_exists", "getdate", + "get_called_class", + "get_class", + "get_class_methods", + "get_class_vars", "getcwd", + "get_declared_classes", + "get_declared_interfaces", + "get_declared_traits", "getenv", "gethostbyaddr", "gethostbyname", "gethostname", "getprotobyname", "getprotobynumber", + "get_object_vars", + "get_parent_class", + "get_resource_id", + "get_resource_type", "getservbyname", "getservbyport", "gettype", @@ -226,10 +245,13 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "implode", "inet_ntop", "inet_pton", + "interface_exists", "intdiv", "intval", + "is_a", "is_array", "is_bool", + "is_callable", "is_dir", "is_double", "is_executable", @@ -251,7 +273,9 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "is_resource", "is_scalar", "is_string", + "is_subclass_of", "in_array", + "isset", "is_writable", "is_writeable", "ip2long", @@ -278,6 +302,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "lstat", "ltrim", "max", + "method_exists", "microtime", "md5", "min", @@ -299,6 +324,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "phpversion", "popen", "pow", + "property_exists", "print_r", "printf", "putenv", @@ -338,6 +364,15 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "sort", "sqrt", "sprintf", + "spl_autoload", + "spl_autoload_call", + "spl_autoload_extensions", + "spl_autoload_functions", + "spl_autoload_register", + "spl_autoload_unregister", + "spl_classes", + "spl_object_hash", + "spl_object_id", "sscanf", "stat", "stream_bucket_append", @@ -415,6 +450,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "time", "tmpfile", "touch", + "trait_exists", "trim", "uasort", "uksort", @@ -422,6 +458,7 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "ucwords", "umask", "unlink", + "unset", "urldecode", "urlencode", "usleep", From 5648740a184a50a47c9962cff200a924d2c006a6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 06:48:46 +0200 Subject: [PATCH 1092/1208] refactor(magician): migrate raw memory builtins --- .../src/interpreter/builtins/hooks/direct.rs | 5 +- .../src/interpreter/builtins/hooks/values.rs | 5 +- .../src/interpreter/builtins/raw_memory.rs | 24 +++------ .../raw_memory/declarations/buffer_free.rs | 16 ++++++ .../raw_memory/declarations/buffer_len.rs | 16 ++++++ .../raw_memory/declarations/buffer_new.rs | 16 ++++++ .../builtins/raw_memory/declarations/mod.rs | 54 +++++++++++++++++++ .../builtins/raw_memory/declarations/ptr.rs | 17 ++++++ .../raw_memory/declarations/ptr_get.rs | 16 ++++++ .../raw_memory/declarations/ptr_is_null.rs | 16 ++++++ .../raw_memory/declarations/ptr_null.rs | 16 ++++++ .../raw_memory/declarations/ptr_offset.rs | 16 ++++++ .../raw_memory/declarations/ptr_read16.rs | 16 ++++++ .../raw_memory/declarations/ptr_read32.rs | 16 ++++++ .../raw_memory/declarations/ptr_read8.rs | 16 ++++++ .../declarations/ptr_read_string.rs | 16 ++++++ .../raw_memory/declarations/ptr_set.rs | 16 ++++++ .../raw_memory/declarations/ptr_sizeof.rs | 16 ++++++ .../raw_memory/declarations/ptr_write16.rs | 16 ++++++ .../raw_memory/declarations/ptr_write32.rs | 16 ++++++ .../raw_memory/declarations/ptr_write8.rs | 16 ++++++ .../declarations/ptr_write_string.rs | 16 ++++++ .../interpreter/builtins/registry/binding.rs | 18 +------ .../builtins/registry/dispatch/mod.rs | 13 ++--- .../src/interpreter/builtins/registry/mod.rs | 16 ++++++ .../interpreter/builtins/registry/names.rs | 21 +------- .../src/interpreter/builtins/spec.rs | 2 + tests/builtin_parity_tests.rs | 18 +++++++ 28 files changed, 400 insertions(+), 65 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_free.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_len.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_new.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_get.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_is_null.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_null.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_offset.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read16.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read32.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read8.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read_string.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_set.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_sizeof.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write16.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write32.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write8.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write_string.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index deda7ba14a..2c91d7aa05 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -32,7 +32,7 @@ use super::super::{ eval_builtin_cast, eval_builtin_core_call, eval_builtin_filesystem_call, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_range, - eval_builtin_regex_call, eval_builtin_str_pad, eval_builtin_str_replace, + eval_builtin_raw_memory_call, eval_builtin_regex_call, eval_builtin_str_pad, eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, @@ -145,6 +145,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Range, /// Dispatches regex builtins. Regex, + /// Dispatches raw pointer and buffer extension builtins. + RawMemory, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, /// Dispatches `sqrt(...)`. @@ -273,6 +275,7 @@ impl EvalDirectHook { Self::Round => eval_builtin_round(args, context, scope, values), Self::Range => eval_builtin_range(args, context, scope, values), Self::Regex => eval_builtin_regex_call(name, args, context, scope, values), + Self::RawMemory => eval_builtin_raw_memory_call(name, args, context, scope, values), Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), Self::StringCase => eval_builtin_string_case(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index c37e582a6f..e947070216 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -31,7 +31,7 @@ use super::super::{ eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, - eval_symbols_values_result, eval_time_values_result, eval_trim_like_result, + eval_raw_memory_values_result, eval_symbols_values_result, eval_time_values_result, eval_trim_like_result, eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; @@ -148,6 +148,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Range, /// Dispatches regex builtins. Regex, + /// Dispatches raw pointer and buffer extension builtins. + RawMemory, /// Dispatches by-value `settype(...)` callable calls. Settype, /// Dispatches `addslashes(...)` and `stripslashes(...)`. @@ -322,6 +324,7 @@ impl EvalValuesHook { }, Self::Range => two_args(evaluated_args, values, eval_range_result), Self::Regex => eval_regex_values_result(name, evaluated_args, context, values), + Self::RawMemory => eval_raw_memory_values_result(name, evaluated_args, context, values), Self::Settype => two_args(evaluated_args, values, eval_settype_value_result), Self::Slashes => one_arg(evaluated_args, values, |value, values| { eval_slashes_result(name, value, values) diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs index 7823feda7b..7abf25302d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs @@ -19,6 +19,12 @@ use std::slice; use super::super::*; +mod declarations; + +pub(in crate::interpreter) use declarations::{ + eval_builtin_raw_memory_call, eval_raw_memory_values_result, +}; + const BUFFER_HEADER_WORDS: usize = 2; const BUFFER_DEFAULT_STRIDE: usize = 8; @@ -48,24 +54,6 @@ pub(in crate::interpreter) fn eval_builtin_raw_memory( eval_raw_memory_builtin_result(name, &evaluated_args, context, values) } -/// Dispatches already evaluated raw-memory builtin arguments for dynamic calls. -pub(in crate::interpreter) fn eval_raw_memory_builtin_with_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - match name { - "buffer_free" | "buffer_len" | "buffer_new" | "ptr" | "ptr_get" | "ptr_is_null" - | "ptr_null" | "ptr_offset" | "ptr_read8" | "ptr_read16" | "ptr_read32" - | "ptr_read_string" | "ptr_set" | "ptr_sizeof" | "ptr_write8" | "ptr_write16" - | "ptr_write32" | "ptr_write_string" => { - eval_raw_memory_builtin_result(name, evaluated_args, context, values).map(Some) - } - _ => Ok(None), - } -} - /// Applies one raw-memory builtin to already evaluated runtime cells. fn eval_raw_memory_builtin_result( name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_free.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_free.rs new file mode 100644 index 0000000000..3c982349b3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_free.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `buffer_free`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so a local buffer variable can be nulled. + +eval_builtin! { + name: "buffer_free", + area: RawMemory, + params: [buffer], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_len.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_len.rs new file mode 100644 index 0000000000..a31392879f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_len.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `buffer_len`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "buffer_len", + area: RawMemory, + params: [buffer], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_new.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_new.rs new file mode 100644 index 0000000000..e87f259655 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_new.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `buffer_new`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "buffer_new", + area: RawMemory, + params: [length], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/mod.rs new file mode 100644 index 0000000000..fffb6970ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/mod.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Declarative eval registry entries and dispatch adapters for raw-memory builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory` module loading. +//! - `crate::interpreter::builtins::hooks` for migrated raw-memory dispatch. +//! +//! Key details: +//! - Direct calls delegate to the existing source-sensitive helper so +//! `buffer_free($local)` can null the source variable. +//! - Values calls keep the by-value `ptr(...)` unsupported behavior. + +use super::super::super::*; +use super::{eval_builtin_raw_memory, eval_raw_memory_builtin_result}; + +mod buffer_free; +mod buffer_len; +mod buffer_new; +mod ptr; +mod ptr_get; +mod ptr_is_null; +mod ptr_null; +mod ptr_offset; +mod ptr_read16; +mod ptr_read32; +mod ptr_read8; +mod ptr_read_string; +mod ptr_set; +mod ptr_sizeof; +mod ptr_write16; +mod ptr_write32; +mod ptr_write8; +mod ptr_write_string; + +/// Dispatches direct expression-level calls for raw-memory builtins. +pub(in crate::interpreter) fn eval_builtin_raw_memory_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_raw_memory(name, args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for raw-memory builtins. +pub(in crate::interpreter) fn eval_raw_memory_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_raw_memory_builtin_result(name, evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr.rs new file mode 100644 index 0000000000..60e0d4ea46 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr.rs @@ -0,0 +1,17 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Eval keeps `ptr(...)` unsupported because by-value cells do not expose raw +//! lvalue storage addresses safely. + +eval_builtin! { + name: "ptr", + area: RawMemory, + params: [value], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_get.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_get.rs new file mode 100644 index 0000000000..8ed7329beb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_get.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_get", + area: RawMemory, + params: [pointer], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_is_null.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_is_null.rs new file mode 100644 index 0000000000..da5952c561 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_is_null.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_is_null`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_is_null", + area: RawMemory, + params: [pointer], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_null.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_null.rs new file mode 100644 index 0000000000..b2065b74ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_null.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_null`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_null", + area: RawMemory, + params: [], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_offset.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_offset.rs new file mode 100644 index 0000000000..aab9bd6856 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_offset.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_offset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_offset", + area: RawMemory, + params: [pointer, offset], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read16.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read16.rs new file mode 100644 index 0000000000..b478b18134 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read16.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_read16`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_read16", + area: RawMemory, + params: [pointer], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read32.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read32.rs new file mode 100644 index 0000000000..c720c5b70b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read32.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_read32`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_read32", + area: RawMemory, + params: [pointer], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read8.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read8.rs new file mode 100644 index 0000000000..0ac58f0a1c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read8.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_read8`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_read8", + area: RawMemory, + params: [pointer], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read_string.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read_string.rs new file mode 100644 index 0000000000..6efaa6ccbe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read_string.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_read_string`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_read_string", + area: RawMemory, + params: [pointer, length], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_set.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_set.rs new file mode 100644 index 0000000000..df3338995e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_set.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_set`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_set", + area: RawMemory, + params: [pointer, value], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_sizeof.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_sizeof.rs new file mode 100644 index 0000000000..92a03b957e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_sizeof.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_sizeof`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_sizeof", + area: RawMemory, + params: [r#type], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write16.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write16.rs new file mode 100644 index 0000000000..95dc034d90 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write16.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_write16`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_write16", + area: RawMemory, + params: [pointer, value], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write32.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write32.rs new file mode 100644 index 0000000000..6d1eaa7d43 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write32.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_write32`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_write32", + area: RawMemory, + params: [pointer, value], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write8.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write8.rs new file mode 100644 index 0000000000..f062626f5e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write8.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_write8`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_write8", + area: RawMemory, + params: [pointer, value], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write_string.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write_string.rs new file mode 100644 index 0000000000..211b987042 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write_string.rs @@ -0,0 +1,16 @@ +//! Purpose: +//! Declarative eval registry entry for `ptr_write_string`. +//! +//! Called from: +//! - `crate::interpreter::builtins::raw_memory::declarations`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the raw-memory helper. + +eval_builtin! { + name: "ptr_write_string", + area: RawMemory, + params: [pointer, string], + direct: RawMemory, + values: RawMemory, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs index ed108ca74e..4b0ffee3ff 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/binding.rs @@ -123,21 +123,5 @@ pub(in crate::interpreter) fn eval_builtin_param_names( return Some(params); } - match name { - "buffer_new" => Some(&["length"]), - "buffer_len" | "buffer_free" => Some(&["buffer"]), - "ptr" => Some(&["value"]), - "ptr_null" => Some(&[]), - "ptr_is_null" | "ptr_get" | "ptr_read8" | "ptr_read16" | "ptr_read32" => { - Some(&["pointer"]) - } - "ptr_offset" => Some(&["pointer", "offset"]), - "ptr_read_string" => Some(&["pointer", "length"]), - "ptr_set" | "ptr_write8" | "ptr_write16" | "ptr_write32" => { - Some(&["pointer", "value"]) - } - "ptr_write_string" => Some(&["pointer", "string"]), - "ptr_sizeof" => Some(&["type"]), - _ => None, - } + None } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs index 6019c1311a..640e44cf5e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dispatch/mod.rs @@ -1,12 +1,14 @@ //! Purpose: -//! Routes by-value dynamic builtin dispatch to focused builtin-family dispatchers. +//! Routes by-value dynamic builtin dispatch through declarative registry lookup +//! and eval-only runtime alias fallbacks. //! //! Called from: //! - `crate::interpreter::builtins::registry` re-exports. //! //! Key details: -//! - Each child dispatcher handles already evaluated runtime-cell arguments for one -//! builtin family and returns `Ok(None)` when the name is outside its domain. +//! - Migrated builtins dispatch through `eval_declared_builtin_values_call`. +//! - Procedural date/time aliases remain a runtime fallback because eval cannot +//! run the static name-resolver rewrite before dispatch. use super::eval_declared_builtin_values_call; use super::super::super::*; @@ -22,11 +24,6 @@ pub(in crate::interpreter) fn eval_builtin_with_values( return Ok(Some(result)); } - if let Some(result) = - eval_raw_memory_builtin_with_values(name, evaluated_args, context, values)? - { - return Ok(Some(result)); - } if let Some(result) = eval_date_procedural_alias_with_values(name, evaluated_args, context, values)? { diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index c338653ef6..a589d62baf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -227,6 +227,7 @@ mod tests { "boolval", "base64_encode", "bin2hex", + "buffer_new", "call_user_func", "call_user_func_array", "checkdate", @@ -387,6 +388,9 @@ mod tests { "popen", "print_r", "printf", + "ptr_null", + "ptr_read16", + "ptr_write_string", "preg_match", "preg_match_all", "preg_replace", @@ -619,6 +623,18 @@ mod tests { eval_declared_builtin_default_value("spl_autoload_register", 2), Some(EvalBuiltinDefaultValue::Bool(false)) ); + assert_eq!( + eval_declared_builtin_param_names("buffer_new"), + Some(["length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("ptr_read_string"), + Some(["pointer", "length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("ptr_sizeof"), + Some(["type"].as_slice()) + ); for name in ["rand", "mt_rand", "random_int"] { assert_eq!( eval_declared_builtin_param_names(name), diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index f936986478..b0a70246a1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -13,26 +13,7 @@ use std::sync::OnceLock; use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; /// PHP-visible builtin names implemented by the eval interpreter. -pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[ - "buffer_free", - "buffer_len", - "buffer_new", - "ptr", - "ptr_get", - "ptr_is_null", - "ptr_null", - "ptr_offset", - "ptr_read8", - "ptr_read16", - "ptr_read32", - "ptr_read_string", - "ptr_set", - "ptr_sizeof", - "ptr_write8", - "ptr_write16", - "ptr_write32", - "ptr_write_string", -]; +pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[]; /// Combined PHP-visible builtin names from legacy and declarative registries. static EVAL_PHP_VISIBLE_BUILTIN_FUNCTION_NAMES: OnceLock> = OnceLock::new(); diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index 6775693e79..a9fa2e91a8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -34,6 +34,8 @@ pub(in crate::interpreter) enum EvalArea { NetworkEnv, /// PCRE-style regex builtins. Regex, + /// Raw pointer and buffer extension builtins. + RawMemory, /// String-processing builtins. String, /// Symbol, class metadata, SPL, and language-construct probes. diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index b47bc81893..f8055d03ac 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -109,6 +109,9 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "base64_encode", "bin2hex", "boolval", + "buffer_free", + "buffer_len", + "buffer_new", "call_user_func", "call_user_func_array", "ceil", @@ -327,6 +330,21 @@ const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ "property_exists", "print_r", "printf", + "ptr", + "ptr_get", + "ptr_is_null", + "ptr_null", + "ptr_offset", + "ptr_read8", + "ptr_read16", + "ptr_read32", + "ptr_read_string", + "ptr_set", + "ptr_sizeof", + "ptr_write8", + "ptr_write16", + "ptr_write32", + "ptr_write_string", "putenv", "preg_match", "preg_match_all", From 9cd06eb64f057197676f8b92d4e6de4fe960947e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 06:50:52 +0200 Subject: [PATCH 1093/1208] docs(plans): mark magician phase 2 complete --- .plans/elephc-magician-builtin-structure-plan.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.plans/elephc-magician-builtin-structure-plan.md b/.plans/elephc-magician-builtin-structure-plan.md index 00db7d9841..be9a2fba17 100644 --- a/.plans/elephc-magician-builtin-structure-plan.md +++ b/.plans/elephc-magician-builtin-structure-plan.md @@ -8,11 +8,11 @@ per-builtin layout and derive metadata from that registry. - [x] Phase 1: update parity tests to query the registry instead of searching dispatcher string literals for migrated builtins. -- [ ] Phase 2: migrate already implemented magician builtins area by area while +- [x] Phase 2: migrate already implemented magician builtins area by area while keeping fallback to existing dispatchers until each area is complete. -- [ ] Phase 2: remove duplicate manual tables for names, signatures, defaults, +- [x] Phase 2: remove duplicate manual tables for names, signatures, defaults, by-ref parameters, and dispatch in migrated areas. -- [ ] Phase 2: keep ordinary files below 500 LoC, leaving exceptions only for +- [x] Phase 2: keep ordinary files below 500 LoC, leaving exceptions only for cohesive single-scope helpers documented in their module preambles. - [ ] Phase 3: split remaining large builtin files (`symbols.rs`, `filesystem/streams.rs`, `class_metadata/oop_introspection.rs`, @@ -168,6 +168,11 @@ During Phase 2, a hybrid system is acceptable: the new registry for migrated areas, manual dispatchers for areas not yet migrated. Duplicating metadata for a migrated builtin is not acceptable. +Phase 2 completion leaves the procedural date/time alias fallback explicit in +`registry/dispatch/mod.rs`. Eval cannot run the static name-resolver rewrite +before runtime dispatch, so aliases such as date/time procedural names remain an +eval-only runtime bridge rather than duplicated builtin metadata. + ## Phase 3: Large-File Cleanup and Legacy Path Removal Once most builtins use the registry: From 45e21e47bd8a77bdb5903272b0f85137b56758c4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 08:46:21 +0200 Subject: [PATCH 1094/1208] refactor(magician): route direct builtins through registry --- .../interpreter/builtins/arrays/callbacks.rs | 347 +++++++++++++++++ .../src/interpreter/builtins/arrays/core.rs | 360 ------------------ .../src/interpreter/builtins/arrays/mod.rs | 2 + .../builtins/filesystem/stream_sockets.rs | 124 ------ .../src/interpreter/builtins/math/mod.rs | 5 + .../src/interpreter/builtins/math/runtime.rs | 26 ++ .../src/interpreter/builtins/mod.rs | 1 + .../src/interpreter/builtins/registry/mod.rs | 45 +++ .../interpreter/builtins/registry/names.rs | 27 +- .../builtins/registry/signature.rs | 51 +-- .../src/interpreter/expressions.rs | 155 +------- 11 files changed, 447 insertions(+), 696 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/callbacks.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/math/runtime.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/callbacks.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/callbacks.rs new file mode 100644 index 0000000000..62e13589fd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/callbacks.rs @@ -0,0 +1,347 @@ +//! Purpose: +//! Callback-driven array builtins such as `array_map`, `array_reduce`, and +//! `array_walk`. +//! +//! Called from: +//! - `crate::interpreter::builtins::arrays` re-exports. +//! - Declarative array builtin dispatch hooks. +//! +//! Key details: +//! - Direct calls preserve lexical scope for callback string resolution. +//! - `array_walk` keeps writable array element targets for by-reference callback +//! parameters. + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `array_map()` for one or more arrays and an optional callback. +pub(in crate::interpreter) fn eval_builtin_array_map( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, arrays)) = args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let mut evaluated_arrays = Vec::with_capacity(arrays.len()); + for array in arrays { + evaluated_arrays.push(eval_expr(array, context, scope, values)?); + } + eval_array_map_result_from_scope(callback, &evaluated_arrays, Some(scope), context, values) +} + +/// Maps one eval array with PHP key preservation for the one-array form. +pub(in crate::interpreter) fn eval_array_map_result( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_map_result_from_scope(callback, arrays, None, context, values) +} + +/// Maps one or more eval arrays with optional lexical scope for callback names. +fn eval_array_map_result_from_scope( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = arrays else { + return eval_array_map_variadic_result_from_scope( + callback, + arrays, + lexical_scope, + context, + values, + ); + }; + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) + }; + let len = values.array_len(*array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(*array, position)?; + let value = values.array_get(*array, key)?; + let mapped = if let Some(callback) = callback.as_ref() { + eval_evaluated_callable_with_values(callback, vec![value], context, values)? + } else { + value + }; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Maps multiple eval arrays with optional lexical scope for callback names. +fn eval_array_map_variadic_result_from_scope( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if arrays.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) + }; + let mut lengths = Vec::with_capacity(arrays.len()); + let mut max_len = 0; + for array in arrays { + let len = values.array_len(*array)?; + max_len = max_len.max(len); + lengths.push(len); + } + + let mut result = values.array_new(max_len)?; + for position in 0..max_len { + let mut callback_args = Vec::with_capacity(arrays.len()); + for (array, len) in arrays.iter().zip(lengths.iter()) { + let value = if position < *len { + let key = values.array_iter_key(*array, position)?; + values.array_get(*array, key)? + } else { + values.null()? + }; + callback_args.push(value); + } + let mapped = if let Some(callback) = callback.as_ref() { + eval_evaluated_callable_with_values(callback, callback_args, context, values)? + } else { + eval_array_map_zipped_row(callback_args, values)? + }; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Builds one row for `array_map(null, $a, $b, ...)`. +pub(in crate::interpreter) fn eval_array_map_zipped_row( + values_row: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut row = values.array_new(values_row.len())?; + for (index, value) in values_row.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + row = values.array_set(row, key, value)?; + } + Ok(row) +} + +/// Evaluates PHP `array_reduce()` with an optional initial carry value. +pub(in crate::interpreter) fn eval_builtin_array_reduce( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, callback, initial) = match args { + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + (array, callback, values.null()?) + } + [array, callback, initial] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let initial = eval_expr(initial, context, scope, values)?; + (array, callback, initial) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_array_reduce_result_from_scope(array, callback, initial, Some(scope), context, values) +} + +/// Reduces one eval array by invoking a callable with carry and item cells. +pub(in crate::interpreter) fn eval_array_reduce_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_reduce_result_from_scope(array, callback, initial, None, context, values) +} + +/// Reduces one eval array with optional lexical scope for callback names. +fn eval_array_reduce_result_from_scope( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + let mut carry = initial; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + carry = + eval_evaluated_callable_with_values(&callback, vec![carry, value], context, values)?; + } + Ok(carry) +} + +/// Evaluates direct PHP `array_walk()` calls and preserves element by-ref targets. +pub(in crate::interpreter) fn eval_builtin_array_walk_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, array_target, callback) = + eval_array_walk_direct_args(args, context, scope, values)?; + eval_array_walk_ref_result_from_scope(array, array_target, callback, Some(scope), context, values) +} + +/// Evaluates and binds direct `array_walk()` arguments in PHP source order. +fn eval_array_walk_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut array_target = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array_target.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + array_target = Some(eval_array_mutation_lvalue_arg(arg, context, scope, values)?); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (array, array_target) = array_target.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, array_target, callback)) +} + +/// Walks one writable eval array by invoking a callable with element ref targets. +pub(in crate::interpreter) fn eval_array_walk_ref_result( + array: RuntimeCellHandle, + array_target: EvalReferenceTarget, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_walk_ref_result_from_scope(array, array_target, callback, None, context, values) +} + +/// Walks one writable eval array with optional lexical scope for callback names. +fn eval_array_walk_ref_result_from_scope( + array: RuntimeCellHandle, + array_target: EvalReferenceTarget, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let current_array = eval_reference_target_value(&array_target, context, values)?; + let key = values.array_iter_key(current_array, position)?; + let value = values.array_get(current_array, key)?; + let ref_target = EvalReferenceTarget::NestedArrayElement { + array_target: Box::new(array_target.clone()), + index: key, + }; + let args = vec![ + EvaluatedCallArg { + name: None, + value, + ref_target: Some(ref_target), + }, + EvaluatedCallArg { + name: None, + value: key, + ref_target: None, + }, + ]; + let _ = eval_evaluated_callable_with_call_array_args(&callback, args, context, values)?; + } + values.bool_value(true) +} + +/// Walks one eval array by invoking a callable with value and key cells. +pub(in crate::interpreter) fn eval_array_walk_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_walk_result_from_scope(array, callback, None, context, values) +} + +/// Walks one eval array with optional lexical scope for callback names. +fn eval_array_walk_result_from_scope( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let _ = eval_evaluated_callable_with_values(&callback, vec![value, key], context, values)?; + } + values.bool_value(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs index db291431ec..026fae0999 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs @@ -11,19 +11,6 @@ use super::super::super::*; use super::super::*; -pub(in crate::interpreter) fn eval_builtin_abs( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.abs(value) -} - /// Evaluates PHP array aggregate builtins over one eval array expression. pub(in crate::interpreter) fn eval_builtin_array_aggregate( name: &str, @@ -218,350 +205,3 @@ pub(in crate::interpreter) fn eval_array_fill_keys_result( } Ok(result) } - -/// Evaluates PHP `array_map()` for one source array and a callable or null callback. -pub(in crate::interpreter) fn eval_builtin_array_map( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((callback, arrays)) = args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let callback = eval_expr(callback, context, scope, values)?; - let mut evaluated_arrays = Vec::with_capacity(arrays.len()); - for array in arrays { - evaluated_arrays.push(eval_expr(array, context, scope, values)?); - } - eval_array_map_result_from_scope(callback, &evaluated_arrays, Some(scope), context, values) -} - -/// Maps one eval array with PHP key preservation for the one-array form. -pub(in crate::interpreter) fn eval_array_map_result( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_map_result_from_scope(callback, arrays, None, context, values) -} - -/// Maps one or more eval arrays with optional lexical scope for callback names. -fn eval_array_map_result_from_scope( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = arrays else { - return eval_array_map_variadic_result_from_scope( - callback, - arrays, - lexical_scope, - context, - values, - ); - }; - let callback = if values.is_null(callback)? { - None - } else { - Some(eval_callable_with_optional_scope( - callback, - context, - lexical_scope, - values, - )?) - }; - let len = values.array_len(*array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(*array, position)?; - let value = values.array_get(*array, key)?; - let mapped = if let Some(callback) = callback.as_ref() { - eval_evaluated_callable_with_values(callback, vec![value], context, values)? - } else { - value - }; - result = values.array_set(result, key, mapped)?; - } - Ok(result) -} - -/// Maps multiple eval arrays with optional lexical scope for callback names. -fn eval_array_map_variadic_result_from_scope( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if arrays.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let callback = if values.is_null(callback)? { - None - } else { - Some(eval_callable_with_optional_scope( - callback, - context, - lexical_scope, - values, - )?) - }; - let mut lengths = Vec::with_capacity(arrays.len()); - let mut max_len = 0; - for array in arrays { - let len = values.array_len(*array)?; - max_len = max_len.max(len); - lengths.push(len); - } - - let mut result = values.array_new(max_len)?; - for position in 0..max_len { - let mut callback_args = Vec::with_capacity(arrays.len()); - for (array, len) in arrays.iter().zip(lengths.iter()) { - let value = if position < *len { - let key = values.array_iter_key(*array, position)?; - values.array_get(*array, key)? - } else { - values.null()? - }; - callback_args.push(value); - } - let mapped = if let Some(callback) = callback.as_ref() { - eval_evaluated_callable_with_values(callback, callback_args, context, values)? - } else { - eval_array_map_zipped_row(callback_args, values)? - }; - let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, mapped)?; - } - Ok(result) -} - -/// Builds one row for `array_map(null, $a, $b, ...)`. -pub(in crate::interpreter) fn eval_array_map_zipped_row( - values_row: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut row = values.array_new(values_row.len())?; - for (index, value) in values_row.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - row = values.array_set(row, key, value)?; - } - Ok(row) -} - -/// Evaluates PHP `array_reduce()` with an optional initial carry value. -pub(in crate::interpreter) fn eval_builtin_array_reduce( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, callback, initial) = match args { - [array, callback] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - (array, callback, values.null()?) - } - [array, callback, initial] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let initial = eval_expr(initial, context, scope, values)?; - (array, callback, initial) - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_array_reduce_result_from_scope(array, callback, initial, Some(scope), context, values) -} - -/// Reduces one eval array by invoking a callable with carry and item cells. -pub(in crate::interpreter) fn eval_array_reduce_result( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - initial: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_reduce_result_from_scope(array, callback, initial, None, context, values) -} - -/// Reduces one eval array with optional lexical scope for callback names. -fn eval_array_reduce_result_from_scope( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - initial: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; - let len = values.array_len(array)?; - let mut carry = initial; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - carry = - eval_evaluated_callable_with_values(&callback, vec![carry, value], context, values)?; - } - Ok(carry) -} - -/// Evaluates direct PHP `array_walk()` calls and preserves element by-ref targets. -pub(in crate::interpreter) fn eval_builtin_array_walk_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, array_target, callback) = - eval_array_walk_direct_args(args, context, scope, values)?; - eval_array_walk_ref_result_from_scope(array, array_target, callback, Some(scope), context, values) -} - -/// Evaluates and binds direct `array_walk()` arguments in PHP source order. -fn eval_array_walk_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { - let mut array_target = None; - let mut callback = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "callback", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array_target.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - array_target = Some(eval_array_mutation_lvalue_arg(arg, context, scope, values)?); - } - "callback" => { - if callback.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - callback = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let (array, array_target) = array_target.ok_or(EvalStatus::RuntimeFatal)?; - let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, array_target, callback)) -} - -/// Walks one writable eval array by invoking a callable with element ref targets. -pub(in crate::interpreter) fn eval_array_walk_ref_result( - array: RuntimeCellHandle, - array_target: EvalReferenceTarget, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_walk_ref_result_from_scope(array, array_target, callback, None, context, values) -} - -/// Walks one writable eval array with optional lexical scope for callback names. -fn eval_array_walk_ref_result_from_scope( - array: RuntimeCellHandle, - array_target: EvalReferenceTarget, - callback: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; - let len = values.array_len(array)?; - for position in 0..len { - let current_array = eval_reference_target_value(&array_target, context, values)?; - let key = values.array_iter_key(current_array, position)?; - let value = values.array_get(current_array, key)?; - let ref_target = EvalReferenceTarget::NestedArrayElement { - array_target: Box::new(array_target.clone()), - index: key, - }; - let args = vec![ - EvaluatedCallArg { - name: None, - value, - ref_target: Some(ref_target), - }, - EvaluatedCallArg { - name: None, - value: key, - ref_target: None, - }, - ]; - let _ = eval_evaluated_callable_with_call_array_args(&callback, args, context, values)?; - } - values.bool_value(true) -} - -/// Evaluates PHP `array_walk()` for by-value callable dispatch. -pub(in crate::interpreter) fn eval_builtin_array_walk( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, callback] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - eval_array_walk_result_from_scope(array, callback, Some(scope), context, values) -} - -/// Walks one eval array by invoking a callable with value and key cells. -pub(in crate::interpreter) fn eval_array_walk_result( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_walk_result_from_scope(array, callback, None, context, values) -} - -/// Walks one eval array with optional lexical scope for callback names. -fn eval_array_walk_result_from_scope( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let _ = eval_evaluated_callable_with_values(&callback, vec![value, key], context, values)?; - } - values.bool_value(true) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs index d99c076633..835a8fad0b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs @@ -11,6 +11,7 @@ //! runtime cell allocation and coercion. mod access; +mod callbacks; mod core; mod filters; mod mutation; @@ -19,6 +20,7 @@ mod sort; mod splice; pub(in crate::interpreter) use access::*; +pub(in crate::interpreter) use callbacks::*; pub(in crate::interpreter) use core::*; pub(in crate::interpreter) use filters::*; pub(in crate::interpreter) use mutation::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs index 293b52bcc0..140185be13 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs @@ -68,46 +68,6 @@ pub(in crate::interpreter) fn eval_stream_socket_client_result( } } -/// Evaluates `fsockopen()` or `pfsockopen()`. -pub(in crate::interpreter) fn eval_builtin_fsockopen( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=5).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let host = eval_expr(&args[0], context, scope, values)?; - let port = eval_expr(&args[1], context, scope, values)?; - let error_code_target = args - .get(2) - .map(|error_code| eval_socket_output_ref_target(error_code, context, scope, values)) - .transpose()?; - let error_message_target = args - .get(3) - .map(|error_message| eval_socket_output_ref_target(error_message, context, scope, values)) - .transpose()?; - if let Some(timeout) = args.get(4) { - let _ = eval_expr(timeout, context, scope, values)?; - } - let (result, error_code, error_message) = - eval_fsockopen_with_error_result(host, port, context, values)?; - eval_write_socket_int_output_ref_target( - error_code_target.as_ref(), - error_code, - context, - values, - )?; - eval_write_socket_output_ref_target( - error_message_target.as_ref(), - Some(error_message), - context, - values, - )?; - Ok(result) -} - /// Evaluates `fsockopen()` or `pfsockopen()` over full eval call metadata. pub(in crate::interpreter) fn eval_builtin_fsockopen_call( args: &[EvalCallArg], @@ -185,29 +145,6 @@ pub(in crate::interpreter) fn eval_fsockopen_with_error_result( } } -/// Evaluates `stream_socket_accept(socket, timeout = null, peer_name = null)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_accept( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(1..=3).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let socket = eval_expr(&args[0], context, scope, values)?; - if let Some(timeout) = args.get(1) { - let _ = eval_expr(timeout, context, scope, values)?; - } - let peer_name_target = args - .get(2) - .map(|peer_name| eval_socket_output_ref_target(peer_name, context, scope, values)) - .transpose()?; - let (result, peer_name) = eval_stream_socket_accept_with_peer_result(socket, context, values)?; - eval_write_socket_output_ref_target(peer_name_target.as_ref(), peer_name, context, values)?; - Ok(result) -} - /// Evaluates `stream_socket_accept()` over full eval call metadata. pub(in crate::interpreter) fn eval_builtin_stream_socket_accept_call( args: &[EvalCallArg], @@ -382,31 +319,6 @@ pub(in crate::interpreter) fn eval_stream_socket_sendto_result( eval_fwrite_result(stream, data, context, values) } -/// Evaluates `stream_socket_recvfrom(stream, length, flags = 0, address = null)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let length = eval_expr(&args[1], context, scope, values)?; - if let Some(flags) = args.get(2) { - let _ = eval_expr(flags, context, scope, values)?; - } - let address_target = args - .get(3) - .map(|address| eval_socket_output_ref_target(address, context, scope, values)) - .transpose()?; - let (result, address) = - eval_stream_socket_recvfrom_with_address_result(stream, length, context, values)?; - eval_write_socket_output_ref_target(address_target.as_ref(), address, context, values)?; - Ok(result) -} - /// Evaluates `stream_socket_recvfrom()` over full eval call metadata. pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom_call( args: &[EvalCallArg], @@ -454,17 +366,6 @@ pub(in crate::interpreter) fn eval_stream_socket_recvfrom_with_address_result( Ok((result, address)) } -/// Captures a writable output argument target for socket by-reference parameters. -fn eval_socket_output_ref_target( - expr: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (_, target) = eval_call_arg_value(expr, context, scope, values)?; - target.ok_or(EvalStatus::RuntimeFatal) -} - /// Writes a socket output string to a captured by-reference target when available. fn eval_write_socket_output_ref_target( target: Option<&EvalReferenceTarget>, @@ -538,31 +439,6 @@ pub(in crate::interpreter) fn eval_stream_socket_pair_result( values.array_set(result, key, value) } -/// Evaluates `stream_select(...)` as a conservative non-blocking readiness check. -pub(in crate::interpreter) fn eval_builtin_stream_select( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(4..=5).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - let mut targets = Vec::with_capacity(3); - for arg in args.iter().take(3) { - let (value, target) = eval_call_arg_value(arg, context, scope, values)?; - evaluated_args.push(value); - targets.push(target.ok_or(EvalStatus::RuntimeFatal)?); - } - for arg in args.iter().skip(3) { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - let result = eval_stream_select_result(&evaluated_args, context, values)?; - eval_write_stream_select_empty_arrays(&targets, context, values)?; - Ok(result) -} - /// Evaluates `stream_select()` over full eval call metadata. pub(in crate::interpreter) fn eval_builtin_stream_select_call( args: &[EvalCallArg], diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs index 6cd64f8780..de9eb854ca 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs @@ -7,6 +7,8 @@ //! //! Key details: //! - Leaf files register metadata through `eval_builtin!`. +//! - Runtime helpers stay in focused support modules when direct hooks need +//! expression evaluation. mod abs; mod acos; @@ -36,8 +38,11 @@ mod rand; mod random_int; mod rad2deg; mod round; +mod runtime; mod sin; mod sinh; mod sqrt; mod tan; mod tanh; + +pub(in crate::interpreter) use runtime::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/math/runtime.rs b/crates/elephc-magician/src/interpreter/builtins/math/runtime.rs new file mode 100644 index 0000000000..c3347fba80 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/math/runtime.rs @@ -0,0 +1,26 @@ +//! Purpose: +//! Runtime helper implementations for numeric builtins whose metadata lives in +//! per-builtin declaration files. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::direct`. +//! +//! Key details: +//! - Helpers evaluate direct-call `EvalExpr` arguments before delegating to +//! runtime numeric operations. + +use super::super::super::*; + +/// Evaluates PHP `abs(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_abs( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.abs(value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 9658c32e0c..af14f6d6af 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -45,6 +45,7 @@ pub(super) use core::*; pub(super) use filesystem::*; pub(super) use formatting::*; pub(super) use json::*; +pub(super) use math::*; pub(super) use network_env::*; pub(super) use process_control::*; pub(super) use raw_memory::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index a589d62baf..19abf88485 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -1230,4 +1230,49 @@ mod tests { Some(EvalBuiltinDefaultValue::Null) ); } + + /// Verifies direct-call fallback is needed only for source-sensitive pre-dispatched builtins. + #[test] + fn declared_builtin_registry_marks_only_pre_dispatched_builtins_without_direct_hooks() { + let mut without_direct: Vec<&str> = eval_declared_builtin_function_names() + .iter() + .copied() + .filter(|name| { + eval_declared_builtin_spec(name) + .is_some_and(|spec| spec.direct.is_none()) + }) + .collect(); + without_direct.sort_unstable(); + + assert_eq!( + without_direct, + [ + "array_pop", + "array_push", + "array_shift", + "array_splice", + "array_unshift", + "array_walk", + "arsort", + "asort", + "flock", + "fsockopen", + "krsort", + "ksort", + "natcasesort", + "natsort", + "pfsockopen", + "rsort", + "settype", + "shuffle", + "sort", + "stream_select", + "stream_socket_accept", + "stream_socket_recvfrom", + "uasort", + "uksort", + "usort", + ] + ); + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs index b0a70246a1..50bff7f4ac 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/names.rs @@ -1,40 +1,21 @@ //! Purpose: -//! Builtin existence name table used by eval function probes. +//! Builtin existence helpers used by eval function probes. //! //! Called from: //! - `crate::interpreter::builtins::registry` re-exports. //! //! Key details: -//! - The slice is the source of truth for PHP-visible eval builtin names. +//! - Declarative specs are the source of truth for PHP-visible eval builtin names. //! - Lookup callers pass canonical lowercase PHP symbol names. -use std::sync::OnceLock; - use super::{eval_declared_builtin_exists, eval_declared_builtin_function_names}; -/// PHP-visible builtin names implemented by the eval interpreter. -pub(in crate::interpreter) const EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS: &[&str] = &[]; - -/// Combined PHP-visible builtin names from legacy and declarative registries. -static EVAL_PHP_VISIBLE_BUILTIN_FUNCTION_NAMES: OnceLock> = OnceLock::new(); - /// Returns the eval interpreter's PHP-visible builtin names. pub(in crate::interpreter) fn eval_php_visible_builtin_function_names() -> &'static [&'static str] { - EVAL_PHP_VISIBLE_BUILTIN_FUNCTION_NAMES - .get_or_init(|| { - let mut names = EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS.to_vec(); - for name in eval_declared_builtin_function_names() { - if !names.contains(name) { - names.push(name); - } - } - names.sort_unstable(); - names - }) - .as_slice() + eval_declared_builtin_function_names() } /// Returns true for PHP-visible builtin names implemented by the eval interpreter. pub(in crate::interpreter) fn eval_php_visible_builtin_exists(name: &str) -> bool { - eval_declared_builtin_exists(name) || EVAL_PHP_VISIBLE_BUILTIN_FUNCTIONS.contains(&name) + eval_declared_builtin_exists(name) } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs index b64bd82ddd..8de9ac53fb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/signature.rs @@ -1,20 +1,16 @@ //! Purpose: -//! Signature-shape metadata for PHP-visible eval builtin calls. -//! Keeps named/default/variadic/by-reference shape visible to parity tests -//! without duplicating runtime dispatch behavior. +//! Signature-shape metadata derived from PHP-visible eval builtin declarations. //! //! Called from: //! - `crate::interpreter::builtin_metadata` //! - builtin registry tests and argument binding audits. //! //! Key details: -//! - Parameter names come from `eval_builtin_param_names()`. -//! - Default values mirror the dispatcher defaults so named calls can skip -//! optional parameters without changing positional semantics. +//! - Declarative specs are the only signature source after builtin migration. +//! - Default values let named calls skip optional parameters without changing +//! positional semantics. -use super::{ - eval_builtin_param_names, eval_declared_builtin_default_value, eval_declared_builtin_spec, -}; +use super::{eval_declared_builtin_default_value, eval_declared_builtin_spec}; /// Comparison-friendly shape for one eval builtin signature. #[derive(Debug, Clone, PartialEq, Eq)] @@ -52,17 +48,14 @@ pub(in crate::interpreter) enum EvalBuiltinDefaultValue { pub(in crate::interpreter) fn eval_builtin_signature_shape( name: &str, ) -> Option { - if let Some(spec) = eval_declared_builtin_spec(name) { - return Some(EvalBuiltinSignatureShape { + eval_declared_builtin_spec(name).map(|spec| { + EvalBuiltinSignatureShape { required_param_count: spec.required_param_count(), default_param_count: spec.default_param_count(), variadic: spec.variadic, by_ref_params: spec.by_ref_param_names(), - }); - } - - let params = eval_builtin_param_names(name)?; - Some(fixed(params)) + } + }) } /// Returns the concrete default value for one optional builtin parameter. @@ -70,29 +63,5 @@ pub(in crate::interpreter) fn eval_builtin_default_value( name: &str, param_index: usize, ) -> Option { - if let Some(default_value) = eval_declared_builtin_default_value(name, param_index) { - return Some(default_value); - } - - None -} - -/// Builds fixed-arity signature shape. -fn fixed(params: &[&'static str]) -> EvalBuiltinSignatureShape { - shape(params.len(), 0, None, &[]) -} - -/// Builds the raw signature-shape value. -fn shape( - required_param_count: usize, - default_param_count: usize, - variadic: Option<&'static str>, - by_ref_params: &'static [&'static str], -) -> EvalBuiltinSignatureShape { - EvalBuiltinSignatureShape { - required_param_count, - default_param_count, - variadic, - by_ref_params, - } + eval_declared_builtin_default_value(name, param_index) } diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 8cab6305d4..0cd8e0048b 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Evaluates EvalIR expressions, match expressions, function-like calls, and positional builtin dispatch. +//! Evaluates EvalIR expressions, match expressions, and function-like calls. //! //! Called from: //! - `crate::interpreter::statements` for expression statements and expression-bearing statements. @@ -1519,7 +1519,7 @@ pub(in crate::interpreter) fn eval_call_args_are_plain_positional(args: &[EvalCa .all(|arg| arg.name().is_none() && !arg.is_spread()) } -/// Evaluates builtins and language constructs after positional-only argument validation. +/// Evaluates registry-backed direct builtins and language constructs after positional-only validation. pub(in crate::interpreter) fn eval_positional_expr_call( name: &str, args: &[EvalExpr], @@ -1527,154 +1527,13 @@ pub(in crate::interpreter) fn eval_positional_expr_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { + if name == "eval" { + return eval_nested_eval(args, context, scope, values); + } + if let Some(result) = eval_declared_builtin_direct_call(name, args, context, scope, values)? { return Ok(result); } - match name { - "array_combine" => eval_builtin_array_combine(args, context, scope, values), - "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), - "array_column" => eval_builtin_array_column(args, context, scope, values), - "array_fill" => eval_builtin_array_fill(args, context, scope, values), - "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), - "array_filter" => eval_builtin_array_filter(args, context, scope, values), - "array_map" => eval_builtin_array_map(args, context, scope, values), - "array_reduce" => eval_builtin_array_reduce(args, context, scope, values), - "array_walk" => eval_builtin_array_walk(args, context, scope, values), - "array_diff" | "array_intersect" => { - eval_builtin_array_value_set(name, args, context, scope, values) - } - "array_diff_key" | "array_intersect_key" => { - eval_builtin_array_key_set(name, args, context, scope, values) - } - "array_merge" => eval_builtin_array_merge(args, context, scope, values), - "class_alias" => eval_builtin_class_alias(args, context, scope, values), - "class_attribute_args" => { - eval_builtin_class_attribute_metadata(name, args, context, scope, values) - } - "class_attribute_names" | "class_get_attributes" => { - eval_builtin_class_attribute_metadata(name, args, context, scope, values) - } - "class_exists" => eval_builtin_class_exists(args, context, scope, values), - "class_implements" | "class_parents" | "class_uses" => { - eval_builtin_class_relation(name, args, context, scope, values) - } - "method_exists" | "property_exists" => { - eval_builtin_member_exists(name, args, context, scope, values) - } - "get_class_methods" => eval_builtin_get_class_methods(args, context, scope, values), - "get_class_vars" => eval_builtin_get_class_vars(args, context, scope, values), - "get_object_vars" => eval_builtin_get_object_vars(args, context, scope, values), - "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), - "trait_exists" | "enum_exists" => { - eval_builtin_class_like_exists(name, args, context, scope, values) - } - "is_a" | "is_subclass_of" => eval_builtin_is_a_relation(name, args, context, scope, values), - "empty" => eval_builtin_empty(args, context, scope, values), - "eval" => eval_nested_eval(args, context, scope, values), - "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), - "fopen" => eval_builtin_fopen(args, context, scope, values), - "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), - "fprintf" => eval_builtin_fprintf(args, context, scope, values), - "fsockopen" | "pfsockopen" => eval_builtin_fsockopen(args, context, scope, values), - "fscanf" => eval_builtin_fscanf(args, context, scope, values), - "function_exists" | "is_callable" => { - eval_builtin_function_probe(name, args, context, scope, values) - } - "get_called_class" => eval_builtin_get_called_class(args, context, values), - "get_class" => eval_builtin_get_class(args, context, scope, values), - "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { - eval_builtin_get_declared_symbols(name, args, context, values) - } - "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), - "get_resource_id" | "get_resource_type" => { - eval_builtin_resource_introspection(name, args, context, scope, values) - } - "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), - "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), - "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), - "buffer_free" | "buffer_len" | "buffer_new" | "ptr" | "ptr_get" | "ptr_is_null" - | "ptr_null" | "ptr_offset" | "ptr_read8" | "ptr_read16" | "ptr_read32" - | "ptr_read_string" | "ptr_set" | "ptr_sizeof" | "ptr_write8" | "ptr_write16" - | "ptr_write32" | "ptr_write_string" => { - eval_builtin_raw_memory(name, args, context, scope, values) - } - "print_r" => eval_builtin_print_r(args, context, scope, values), - "readline" => eval_builtin_readline(args, context, scope, values), - "isset" => eval_builtin_isset(args, context, scope, values), - "spl_autoload_register" | "spl_autoload_unregister" => { - eval_builtin_spl_autoload_bool(name, args, context, scope, values) - } - "spl_autoload" | "spl_autoload_call" => { - eval_builtin_spl_autoload_void(name, args, context, scope, values) - } - "spl_autoload_functions" => { - eval_builtin_spl_autoload_functions(args, context, scope, values) - } - "spl_autoload_extensions" => { - eval_builtin_spl_autoload_extensions(args, context, scope, values) - } - "spl_classes" => eval_builtin_spl_classes(args, values), - "spl_object_id" | "spl_object_hash" => { - eval_builtin_spl_object_identity(name, args, context, scope, values) - } - "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), - "stream_context_get_default" => { - eval_builtin_stream_context_get_default(args, context, scope, values) - } - "stream_context_get_options" => { - eval_builtin_stream_context_get_options(args, context, scope, values) - } - "stream_context_get_params" => { - eval_builtin_stream_context_get_params(args, context, scope, values) - } - "stream_context_set_default" => { - eval_builtin_stream_context_set_default(args, context, scope, values) - } - "stream_context_set_option" => { - eval_builtin_stream_context_set_option(args, context, scope, values) - } - "stream_context_set_params" => { - eval_builtin_stream_context_set_params(args, context, scope, values) - } - "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { - eval_builtin_stream_wrapper_registry(name, args, context, scope, values) - } - "stream_filter_register" => { - eval_builtin_stream_filter_register(args, context, scope, values) - } - "stream_filter_append" | "stream_filter_prepend" => { - eval_builtin_stream_filter_attach(name, args, context, scope, values) - } - "stream_filter_remove" => eval_builtin_stream_filter_remove(args, context, scope, values), - "stream_bucket_new" => eval_builtin_stream_bucket_new(args, context, scope, values), - "stream_bucket_make_writeable" => { - eval_builtin_stream_bucket_make_writeable(args, context, scope, values) - } - "stream_bucket_append" | "stream_bucket_prepend" => { - eval_builtin_stream_bucket_push(name, args, context, scope, values) - } - "stream_select" => eval_builtin_stream_select(args, context, scope, values), - "stream_socket_server" => eval_builtin_stream_socket_server(args, context, scope, values), - "stream_socket_client" => eval_builtin_stream_socket_client(args, context, scope, values), - "stream_socket_accept" => eval_builtin_stream_socket_accept(args, context, scope, values), - "stream_socket_get_name" => { - eval_builtin_stream_socket_get_name(args, context, scope, values) - } - "stream_socket_shutdown" => { - eval_builtin_stream_socket_shutdown(args, context, scope, values) - } - "stream_socket_enable_crypto" => { - eval_builtin_stream_socket_enable_crypto(args, context, scope, values) - } - "stream_socket_sendto" => eval_builtin_stream_socket_sendto(args, context, scope, values), - "stream_socket_recvfrom" => { - eval_builtin_stream_socket_recvfrom(args, context, scope, values) - } - "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), - "unset" => eval_builtin_unset(args, context, scope, values), - "var_dump" => eval_builtin_var_dump(args, context, scope, values), - "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } + Err(EvalStatus::UnsupportedConstruct) } From a2032ee072846e45c8da800dfe81037e04091899 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 09:00:06 +0200 Subject: [PATCH 1095/1208] refactor(magician): split filesystem stream helpers --- .../builtins/filesystem/streams.rs | 627 +----------------- .../builtins/filesystem/streams/common.rs | 96 +++ .../builtins/filesystem/streams/csv_format.rs | 299 +++++++++ .../builtins/filesystem/streams/flock.rs | 122 ++++ .../builtins/filesystem/streams/metadata.rs | 67 ++ .../builtins/filesystem/streams/open.rs | 88 +++ 6 files changed, 683 insertions(+), 616 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/common.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/flock.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/metadata.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/open.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index 61cfe52340..f65dfaa83c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -15,83 +15,17 @@ use super::super::super::*; use super::*; -/// Evaluates PHP `fopen($filename, $mode, ...)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fopen( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let filename = eval_expr(&args[0], context, scope, values)?; - let mode = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - eval_expr(arg, context, scope, values)?; - } - let filename = eval_path_string(filename, values)?; - let mode = eval_stream_string(mode, values)?; - eval_fopen_path_result(&filename, &mode, context, scope, values) -} - -/// Opens a local file stream and returns a resource cell or PHP false. -pub(in crate::interpreter) fn eval_fopen_result( - filename: RuntimeCellHandle, - mode: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let filename = eval_path_string(filename, values)?; - let mode = eval_stream_string(mode, values)?; - let mut scope = ElephcEvalScope::new(); - eval_fopen_path_result(&filename, &mode, context, &mut scope, values) -} - -/// Opens a stream by already-coerced path and mode strings. -fn eval_fopen_path_result( - filename: &str, - mode: &str, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = - eval_user_wrapper_fopen_result(filename, mode, context, scope, values)? - { - return Ok(result); - } - match context.stream_resources_mut().open_path(filename, mode) { - Some(id) => values.resource(id), - None => { - values.warning("Warning: fopen(): Failed to open stream\n")?; - values.bool_value(false) - } - } -} - -/// Evaluates PHP `tmpfile()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_tmpfile( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_tmpfile_result(context, values) -} - -/// Creates an anonymous temporary file stream resource or returns PHP false. -pub(in crate::interpreter) fn eval_tmpfile_result( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match context.stream_resources_mut().open_tmpfile() { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} +mod common; +mod csv_format; +mod flock; +mod metadata; +mod open; + +pub(in crate::interpreter) use common::*; +pub(in crate::interpreter) use csv_format::*; +pub(in crate::interpreter) use flock::*; +pub(in crate::interpreter) use metadata::*; +pub(in crate::interpreter) use open::*; /// Evaluates one unary stream builtin over an eval expression. pub(in crate::interpreter) fn eval_builtin_unary_stream( @@ -225,53 +159,6 @@ pub(in crate::interpreter) fn eval_builtin_fread( eval_fread_result(stream, length, context, values) } -/// Evaluates PHP `fgetcsv($stream, $length = null, $separator = ",")`. -pub(in crate::interpreter) fn eval_builtin_fgetcsv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(1..=3).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let length = match args.get(1) { - Some(length) => Some(eval_expr(length, context, scope, values)?), - None => None, - }; - let separator = match args.get(2) { - Some(separator) => Some(eval_expr(separator, context, scope, values)?), - None => None, - }; - eval_fgetcsv_result(stream, length, separator, context, values) -} - -/// Reads and parses one CSV record from a materialized stream resource. -pub(in crate::interpreter) fn eval_fgetcsv_result( - stream: RuntimeCellHandle, - length: Option, - separator: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let length = eval_optional_stream_length(length, values)?.unwrap_or(usize::MAX); - let separator = eval_optional_delimiter(separator, b',', values)?; - let Some(mut line) = context - .stream_resources_mut() - .read_line(id, length, None, true, true) - else { - return values.bool_value(false); - }; - if line.is_empty() { - return values.bool_value(false); - } - eval_trim_csv_line_end(&mut line); - let fields = eval_parse_csv_record(&line, separator, b'"'); - eval_csv_fields_array(&fields, values) -} - /// Reads bytes from a materialized stream resource. pub(in crate::interpreter) fn eval_fread_result( stream: RuntimeCellHandle, @@ -323,259 +210,6 @@ pub(in crate::interpreter) fn eval_fwrite_result( } } -/// Evaluates PHP `fputcsv($stream, $fields, $separator = ",", $enclosure = "\"")`. -pub(in crate::interpreter) fn eval_builtin_fputcsv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let fields = eval_expr(&args[1], context, scope, values)?; - let separator = match args.get(2) { - Some(separator) => Some(eval_expr(separator, context, scope, values)?), - None => None, - }; - let enclosure = match args.get(3) { - Some(enclosure) => Some(eval_expr(enclosure, context, scope, values)?), - None => None, - }; - eval_fputcsv_result(stream, fields, separator, enclosure, context, values) -} - -/// Formats and writes one CSV record to a materialized stream resource. -pub(in crate::interpreter) fn eval_fputcsv_result( - stream: RuntimeCellHandle, - fields: RuntimeCellHandle, - separator: Option, - enclosure: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let separator = eval_optional_delimiter(separator, b',', values)?; - let enclosure = eval_optional_delimiter(enclosure, b'"', values)?; - let output = eval_format_csv_record(fields, separator, enclosure, values)?; - match context.stream_resources_mut().write(id, &output) { - Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `fprintf($stream, $format, ...$values)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fprintf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let format = eval_expr(&args[1], context, scope, values)?; - let mut format_args = Vec::with_capacity(args.len().saturating_sub(2)); - for arg in &args[2..] { - format_args.push(eval_expr(arg, context, scope, values)?); - } - eval_fprintf_result(stream, format, &format_args, context, values) -} - -/// Evaluates PHP `fscanf($stream, $format, ...$vars)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fscanf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let format = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - eval_expr(arg, context, scope, values)?; - } - eval_fscanf_result(stream, format, context, values) -} - -/// Reads one line from a stream and scans it with the eval `sscanf()` subset. -pub(in crate::interpreter) fn eval_fscanf_result( - stream: RuntimeCellHandle, - format: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let Some(line) = context - .stream_resources_mut() - .read_line(id, usize::MAX, None, true, true) - else { - return values.bool_value(false); - }; - let input = values.string_bytes_value(&line)?; - eval_sscanf_result(input, format, values) -} - -/// Formats and writes `fprintf()` arguments to a materialized stream resource. -pub(in crate::interpreter) fn eval_fprintf_result( - stream: RuntimeCellHandle, - format: RuntimeCellHandle, - format_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let format = values.string_bytes(format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - match context.stream_resources_mut().write(id, &output) { - Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `flock($stream, $operation, &$would_block = null)` over eval call args. -pub(in crate::interpreter) fn eval_builtin_flock( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (stream, operation, would_block_target) = - eval_flock_direct_args(args, context, scope, values)?; - let (success, would_block) = eval_flock_result(stream, operation, context, values)?; - if let Some(target) = would_block_target { - let value = values.bool_value(would_block)?; - eval_write_direct_ref_target( - &target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - values.bool_value(success) -} - -/// Applies a materialized PHP `flock()` operation to a local eval stream resource. -pub(in crate::interpreter) fn eval_flock_result( - stream: RuntimeCellHandle, - operation: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(bool, bool), EvalStatus> { - let id = eval_stream_resource_id(stream, values)?; - let operation = eval_int_value(operation, values)?; - if let Some(success) = eval_user_wrapper_flock_result(id, operation, context, values)? { - return Ok((success, false)); - } - Ok(context - .stream_resources() - .flock(id, operation) - .unwrap_or((false, false))) -} - -/// Evaluates and binds direct `flock()` arguments while keeping by-ref output writable. -fn eval_flock_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result< - ( - RuntimeCellHandle, - RuntimeCellHandle, - Option, - ), - EvalStatus, -> { - let mut stream = None; - let mut operation = None; - let mut would_block = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "stream", - 1 => "operation", - 2 => "would_block", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "stream" => { - if stream.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - stream = Some(eval_expr(arg.value(), context, scope, values)?); - } - "operation" => { - if operation.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - operation = Some(eval_expr(arg.value(), context, scope, values)?); - } - "would_block" => { - if would_block.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let (_, target) = eval_call_arg_value(arg.value(), context, scope, values)?; - would_block = Some(target.ok_or(EvalStatus::RuntimeFatal)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let stream = stream.ok_or(EvalStatus::RuntimeFatal)?; - let operation = operation.ok_or(EvalStatus::RuntimeFatal)?; - Ok((stream, operation, would_block)) -} - -/// Evaluates PHP `vfprintf($stream, $format, $values)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_vfprintf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, format, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let format = eval_expr(format, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_vfprintf_result(stream, format, array, context, values) -} - -/// Formats and writes `vfprintf()` array arguments to a materialized stream resource. -pub(in crate::interpreter) fn eval_vfprintf_result( - stream: RuntimeCellHandle, - format: RuntimeCellHandle, - array: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let format_args = eval_sprintf_argument_array_values(array, values)?; - eval_fprintf_result(stream, format, &format_args, context, values) -} - /// Evaluates PHP `fseek($stream, $offset, $whence = SEEK_SET)` over eval expressions. pub(in crate::interpreter) fn eval_builtin_fseek( args: &[EvalExpr], @@ -799,242 +433,3 @@ pub(in crate::interpreter) fn eval_stream_copy_to_stream_result( None => values.bool_value(false), } } - -/// Builds PHP's stream metadata array for one eval-local stream resource. -fn eval_stream_get_meta_data_result( - id: i64, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(meta) = context.stream_resources().meta_data(id) else { - return values.bool_value(false); - }; - let mut result = values.assoc_new(9)?; - result = eval_stream_meta_set_bool(result, "timed_out", false, values)?; - result = eval_stream_meta_set_bool(result, "blocked", true, values)?; - result = eval_stream_meta_set_bool(result, "eof", meta.eof, values)?; - result = eval_stream_meta_set_string(result, "wrapper_type", "plainfile", values)?; - result = eval_stream_meta_set_string(result, "stream_type", "STDIO", values)?; - result = eval_stream_meta_set_string(result, "mode", &meta.mode, values)?; - result = eval_stream_meta_set_int(result, "unread_bytes", 0, values)?; - result = eval_stream_meta_set_bool(result, "seekable", true, values)?; - eval_stream_meta_set_string(result, "uri", &meta.uri, values) -} - -/// Converts a runtime resource cell into eval's zero-based stream id. -fn eval_stream_resource_id( - stream: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(stream)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(stream, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} - -/// Converts a stream length argument into a non-negative `usize`. -fn eval_nonnegative_usize( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Converts an optional stream length where null and -1 mean "read all". -fn eval_optional_stream_length( - value: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(value) = value else { - return Ok(None); - }; - if values.type_tag(value)? == EVAL_TAG_NULL { - return Ok(None); - } - let value = eval_int_value(value, values)?; - if value == -1 { - return Ok(None); - } - Ok(Some( - usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal)?, - )) -} - -/// Converts an optional absolute stream offset where null and -1 mean no seek. -fn eval_optional_stream_offset( - value: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(value) = value else { - return Ok(None); - }; - if values.type_tag(value)? == EVAL_TAG_NULL { - return Ok(None); - } - let value = eval_int_value(value, values)?; - if value < 0 { - Ok(None) - } else { - Ok(Some(value)) - } -} - -/// Converts one runtime cell to a UTF-8 string for stream mode arguments. -fn eval_stream_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - Ok(String::from_utf8_lossy(&bytes).into_owned()) -} - -/// Converts an optional one-byte delimiter argument to a byte value. -fn eval_optional_delimiter( - value: Option, - default: u8, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(value) = value else { - return Ok(default); - }; - if values.type_tag(value)? == EVAL_TAG_NULL { - return Ok(default); - } - Ok(values.string_bytes(value)?.first().copied().unwrap_or(default)) -} - -/// Removes CR/LF line terminators from a CSV record buffer. -fn eval_trim_csv_line_end(line: &mut Vec) { - if line.ends_with(b"\n") { - line.pop(); - } - if line.ends_with(b"\r") { - line.pop(); - } -} - -/// Parses one CSV record using PHP-style doubled-enclosure escaping. -fn eval_parse_csv_record(line: &[u8], separator: u8, enclosure: u8) -> Vec> { - let mut fields = Vec::new(); - let mut field = Vec::new(); - let mut quoted = false; - let mut index = 0; - while index < line.len() { - let byte = line[index]; - if quoted { - if byte == enclosure { - if line.get(index + 1).copied() == Some(enclosure) { - field.push(enclosure); - index += 2; - continue; - } - quoted = false; - } else { - field.push(byte); - } - } else if byte == enclosure && field.is_empty() { - quoted = true; - } else if byte == separator { - fields.push(std::mem::take(&mut field)); - } else { - field.push(byte); - } - index += 1; - } - fields.push(field); - fields -} - -/// Builds a PHP indexed array from parsed CSV field bytes. -fn eval_csv_fields_array( - fields: &[Vec], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(fields.len())?; - for (index, field) in fields.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, field, values)?; - } - Ok(result) -} - -/// Formats one PHP array-like value as a CSV record ending in LF. -fn eval_format_csv_record( - fields: RuntimeCellHandle, - separator: u8, - enclosure: u8, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !values.is_array_like(fields)? { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(fields)?; - let mut output = Vec::new(); - for position in 0..len { - if position > 0 { - output.push(separator); - } - let key = values.array_iter_key(fields, position)?; - let value = values.array_get(fields, key)?; - let bytes = values.string_bytes(value)?; - eval_append_csv_field(&mut output, &bytes, separator, enclosure); - } - output.push(b'\n'); - Ok(output) -} - -/// Appends one CSV field, quoting and escaping only when required. -fn eval_append_csv_field(output: &mut Vec, field: &[u8], separator: u8, enclosure: u8) { - let needs_quotes = field - .iter() - .any(|byte| matches!(*byte, b'\n' | b'\r') || *byte == separator || *byte == enclosure); - if !needs_quotes { - output.extend_from_slice(field); - return; - } - output.push(enclosure); - for byte in field { - if *byte == enclosure { - output.push(enclosure); - } - output.push(*byte); - } - output.push(enclosure); -} - -/// Inserts a boolean field into the stream metadata array. -fn eval_stream_meta_set_bool( - array: RuntimeCellHandle, - key: &str, - value: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.bool_value(value)?; - values.array_set(array, key, value) -} - -/// Inserts an integer field into the stream metadata array. -fn eval_stream_meta_set_int( - array: RuntimeCellHandle, - key: &str, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Inserts a string field into the stream metadata array. -fn eval_stream_meta_set_string( - array: RuntimeCellHandle, - key: &str, - value: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.string(value)?; - values.array_set(array, key, value) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/common.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/common.rs new file mode 100644 index 0000000000..c863450408 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/common.rs @@ -0,0 +1,96 @@ +//! Purpose: +//! Shared stream argument coercions for eval filesystem stream builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams`. +//! - Stream CSV/formatting helper modules. +//! +//! Key details: +//! - Runtime resource payloads are zero-based while PHP-visible ids are +//! one-based resource handles. + +use super::*; + +/// Converts a runtime resource cell into eval's zero-based stream id. +pub(in crate::interpreter) fn eval_stream_resource_id( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts a stream length argument into a non-negative `usize`. +pub(in crate::interpreter) fn eval_nonnegative_usize( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts an optional stream length where null and -1 mean "read all". +pub(in crate::interpreter) fn eval_optional_stream_length( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = value else { + return Ok(None); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(None); + } + let value = eval_int_value(value, values)?; + if value == -1 { + return Ok(None); + } + Ok(Some( + usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal)?, + )) +} + +/// Converts an optional absolute stream offset where null and -1 mean no seek. +pub(in crate::interpreter) fn eval_optional_stream_offset( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = value else { + return Ok(None); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(None); + } + let value = eval_int_value(value, values)?; + if value < 0 { + Ok(None) + } else { + Ok(Some(value)) + } +} + +/// Converts one runtime cell to a UTF-8 string for stream mode arguments. +pub(in crate::interpreter) fn eval_stream_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + Ok(String::from_utf8_lossy(&bytes).into_owned()) +} + +/// Converts an optional one-byte delimiter argument to a byte value. +pub(in crate::interpreter) fn eval_optional_delimiter( + value: Option, + default: u8, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(value) = value else { + return Ok(default); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(default); + } + Ok(values.string_bytes(value)?.first().copied().unwrap_or(default)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs new file mode 100644 index 0000000000..f891e6dcba --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs @@ -0,0 +1,299 @@ +//! Purpose: +//! CSV and printf-family stream builtins for eval-local file resources. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams` re-exports. +//! - Filesystem stream declaration dispatchers. +//! +//! Key details: +//! - CSV parsing implements the small PHP-compatible subset used by eval tests. +//! - Formatting delegates to the shared `sprintf` byte formatter. + +use super::*; + +/// Evaluates PHP `fgetcsv($stream, $length = null, $separator = ",")`. +pub(in crate::interpreter) fn eval_builtin_fgetcsv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = match args.get(1) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let separator = match args.get(2) { + Some(separator) => Some(eval_expr(separator, context, scope, values)?), + None => None, + }; + eval_fgetcsv_result(stream, length, separator, context, values) +} + +/// Reads and parses one CSV record from a materialized stream resource. +pub(in crate::interpreter) fn eval_fgetcsv_result( + stream: RuntimeCellHandle, + length: Option, + separator: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_optional_stream_length(length, values)?.unwrap_or(usize::MAX); + let separator = eval_optional_delimiter(separator, b',', values)?; + let Some(mut line) = context + .stream_resources_mut() + .read_line(id, length, None, true, true) + else { + return values.bool_value(false); + }; + if line.is_empty() { + return values.bool_value(false); + } + eval_trim_csv_line_end(&mut line); + let fields = eval_parse_csv_record(&line, separator, b'"'); + eval_csv_fields_array(&fields, values) +} + +/// Evaluates PHP `fputcsv($stream, $fields, $separator = ",", $enclosure = "\"")`. +pub(in crate::interpreter) fn eval_builtin_fputcsv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let fields = eval_expr(&args[1], context, scope, values)?; + let separator = match args.get(2) { + Some(separator) => Some(eval_expr(separator, context, scope, values)?), + None => None, + }; + let enclosure = match args.get(3) { + Some(enclosure) => Some(eval_expr(enclosure, context, scope, values)?), + None => None, + }; + eval_fputcsv_result(stream, fields, separator, enclosure, context, values) +} + +/// Formats and writes one CSV record to a materialized stream resource. +pub(in crate::interpreter) fn eval_fputcsv_result( + stream: RuntimeCellHandle, + fields: RuntimeCellHandle, + separator: Option, + enclosure: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let separator = eval_optional_delimiter(separator, b',', values)?; + let enclosure = eval_optional_delimiter(enclosure, b'"', values)?; + let output = eval_format_csv_record(fields, separator, enclosure, values)?; + match context.stream_resources_mut().write(id, &output) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `fprintf($stream, $format, ...$values)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + let mut format_args = Vec::with_capacity(args.len().saturating_sub(2)); + for arg in &args[2..] { + format_args.push(eval_expr(arg, context, scope, values)?); + } + eval_fprintf_result(stream, format, &format_args, context, values) +} + +/// Evaluates PHP `fscanf($stream, $format, ...$vars)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_fscanf_result(stream, format, context, values) +} + +/// Reads one line from a stream and scans it with the eval `sscanf()` subset. +pub(in crate::interpreter) fn eval_fscanf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let Some(line) = context + .stream_resources_mut() + .read_line(id, usize::MAX, None, true, true) + else { + return values.bool_value(false); + }; + let input = values.string_bytes_value(&line)?; + eval_sscanf_result(input, format, values) +} + +/// Formats and writes `fprintf()` arguments to a materialized stream resource. +pub(in crate::interpreter) fn eval_fprintf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + format_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let format = values.string_bytes(format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + match context.stream_resources_mut().write(id, &output) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} + +/// Evaluates PHP `vfprintf($stream, $format, $values)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_vfprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, format, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let format = eval_expr(format, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_vfprintf_result(stream, format, array, context, values) +} + +/// Formats and writes `vfprintf()` array arguments to a materialized stream resource. +pub(in crate::interpreter) fn eval_vfprintf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let format_args = eval_sprintf_argument_array_values(array, values)?; + eval_fprintf_result(stream, format, &format_args, context, values) +} + +/// Removes CR/LF line terminators from a CSV record buffer. +fn eval_trim_csv_line_end(line: &mut Vec) { + if line.ends_with(b"\n") { + line.pop(); + } + if line.ends_with(b"\r") { + line.pop(); + } +} + +/// Parses one CSV record using PHP-style doubled-enclosure escaping. +fn eval_parse_csv_record(line: &[u8], separator: u8, enclosure: u8) -> Vec> { + let mut fields = Vec::new(); + let mut field = Vec::new(); + let mut quoted = false; + let mut index = 0; + while index < line.len() { + let byte = line[index]; + if quoted { + if byte == enclosure { + if line.get(index + 1).copied() == Some(enclosure) { + field.push(enclosure); + index += 2; + continue; + } + quoted = false; + } else { + field.push(byte); + } + } else if byte == enclosure && field.is_empty() { + quoted = true; + } else if byte == separator { + fields.push(std::mem::take(&mut field)); + } else { + field.push(byte); + } + index += 1; + } + fields.push(field); + fields +} + +/// Builds a PHP indexed array from parsed CSV field bytes. +fn eval_csv_fields_array( + fields: &[Vec], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(fields.len())?; + for (index, field) in fields.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, field, values)?; + } + Ok(result) +} + +/// Formats one PHP array-like value as a CSV record ending in LF. +fn eval_format_csv_record( + fields: RuntimeCellHandle, + separator: u8, + enclosure: u8, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(fields)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(fields)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.push(separator); + } + let key = values.array_iter_key(fields, position)?; + let value = values.array_get(fields, key)?; + let bytes = values.string_bytes(value)?; + eval_append_csv_field(&mut output, &bytes, separator, enclosure); + } + output.push(b'\n'); + Ok(output) +} + +/// Appends one CSV field, quoting and escaping only when required. +fn eval_append_csv_field(output: &mut Vec, field: &[u8], separator: u8, enclosure: u8) { + let needs_quotes = field + .iter() + .any(|byte| matches!(*byte, b'\n' | b'\r') || *byte == separator || *byte == enclosure); + if !needs_quotes { + output.extend_from_slice(field); + return; + } + output.push(enclosure); + for byte in field { + if *byte == enclosure { + output.push(enclosure); + } + output.push(*byte); + } + output.push(enclosure); +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/flock.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/flock.rs new file mode 100644 index 0000000000..e517e81019 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/flock.rs @@ -0,0 +1,122 @@ +//! Purpose: +//! Source-sensitive `flock` handling for eval stream resources. +//! +//! Called from: +//! - `crate::interpreter::eval_call` before generic builtin dispatch. +//! - Filesystem stream declaration dispatchers for by-value callable calls. +//! +//! Key details: +//! - Direct calls keep the optional `$would_block` output parameter writable. + +use super::*; + +/// Evaluates PHP `flock($stream, $operation, &$would_block = null)` over eval call args. +pub(in crate::interpreter) fn eval_builtin_flock( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (stream, operation, would_block_target) = + eval_flock_direct_args(args, context, scope, values)?; + let (success, would_block) = eval_flock_result(stream, operation, context, values)?; + if let Some(target) = would_block_target { + let value = values.bool_value(would_block)?; + eval_write_direct_ref_target( + &target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + values.bool_value(success) +} + +/// Applies a materialized PHP `flock()` operation to a local eval stream resource. +pub(in crate::interpreter) fn eval_flock_result( + stream: RuntimeCellHandle, + operation: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(bool, bool), EvalStatus> { + let id = eval_stream_resource_id(stream, values)?; + let operation = eval_int_value(operation, values)?; + if let Some(success) = eval_user_wrapper_flock_result(id, operation, context, values)? { + return Ok((success, false)); + } + Ok(context + .stream_resources() + .flock(id, operation) + .unwrap_or((false, false))) +} + +/// Evaluates and binds direct `flock()` arguments while keeping by-ref output writable. +fn eval_flock_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result< + ( + RuntimeCellHandle, + RuntimeCellHandle, + Option, + ), + EvalStatus, +> { + let mut stream = None; + let mut operation = None; + let mut would_block = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "stream", + 1 => "operation", + 2 => "would_block", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "stream" => { + if stream.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + stream = Some(eval_expr(arg.value(), context, scope, values)?); + } + "operation" => { + if operation.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + operation = Some(eval_expr(arg.value(), context, scope, values)?); + } + "would_block" => { + if would_block.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let (_, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + would_block = Some(target.ok_or(EvalStatus::RuntimeFatal)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let stream = stream.ok_or(EvalStatus::RuntimeFatal)?; + let operation = operation.ok_or(EvalStatus::RuntimeFatal)?; + Ok((stream, operation, would_block)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/metadata.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/metadata.rs new file mode 100644 index 0000000000..fa8a3ed13c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/metadata.rs @@ -0,0 +1,67 @@ +//! Purpose: +//! Builds PHP metadata arrays for eval-local stream resources. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams::eval_unary_stream_result`. +//! +//! Key details: +//! - Metadata mirrors eval's local stream table, not host PHP stream wrappers. + +use super::*; + +/// Builds PHP's stream metadata array for one eval-local stream resource. +pub(in crate::interpreter) fn eval_stream_get_meta_data_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(meta) = context.stream_resources().meta_data(id) else { + return values.bool_value(false); + }; + let mut result = values.assoc_new(9)?; + result = eval_stream_meta_set_bool(result, "timed_out", false, values)?; + result = eval_stream_meta_set_bool(result, "blocked", true, values)?; + result = eval_stream_meta_set_bool(result, "eof", meta.eof, values)?; + result = eval_stream_meta_set_string(result, "wrapper_type", "plainfile", values)?; + result = eval_stream_meta_set_string(result, "stream_type", "STDIO", values)?; + result = eval_stream_meta_set_string(result, "mode", &meta.mode, values)?; + result = eval_stream_meta_set_int(result, "unread_bytes", 0, values)?; + result = eval_stream_meta_set_bool(result, "seekable", true, values)?; + eval_stream_meta_set_string(result, "uri", &meta.uri, values) +} + +/// Inserts a boolean field into the stream metadata array. +fn eval_stream_meta_set_bool( + array: RuntimeCellHandle, + key: &str, + value: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.bool_value(value)?; + values.array_set(array, key, value) +} + +/// Inserts an integer field into the stream metadata array. +fn eval_stream_meta_set_int( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts a string field into the stream metadata array. +fn eval_stream_meta_set_string( + array: RuntimeCellHandle, + key: &str, + value: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string(value)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/open.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/open.rs new file mode 100644 index 0000000000..54a4c3552e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/open.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Stream-opening builtins for eval-local file resources. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::streams` re-exports. +//! - Filesystem stream declaration dispatchers. +//! +//! Key details: +//! - User wrapper `stream_open` gets the first chance before local file handles +//! are inserted into eval's stream table. + +use super::*; + +/// Evaluates PHP `fopen($filename, $mode, ...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fopen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let filename = eval_expr(&args[0], context, scope, values)?; + let mode = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + let filename = eval_path_string(filename, values)?; + let mode = eval_stream_string(mode, values)?; + eval_fopen_path_result(&filename, &mode, context, scope, values) +} + +/// Opens a local file stream and returns a resource cell or PHP false. +pub(in crate::interpreter) fn eval_fopen_result( + filename: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let filename = eval_path_string(filename, values)?; + let mode = eval_stream_string(mode, values)?; + let mut scope = ElephcEvalScope::new(); + eval_fopen_path_result(&filename, &mode, context, &mut scope, values) +} + +/// Opens a stream by already-coerced path and mode strings. +fn eval_fopen_path_result( + filename: &str, + mode: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_user_wrapper_fopen_result(filename, mode, context, scope, values)? { + return Ok(result); + } + match context.stream_resources_mut().open_path(filename, mode) { + Some(id) => values.resource(id), + None => { + values.warning("Warning: fopen(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} + +/// Evaluates PHP `tmpfile()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_tmpfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_tmpfile_result(context, values) +} + +/// Creates an anonymous temporary file stream resource or returns PHP false. +pub(in crate::interpreter) fn eval_tmpfile_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match context.stream_resources_mut().open_tmpfile() { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} From 14c7c2298320d2bcf2bd6ab5cc3391194d7780cd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 09:11:01 +0200 Subject: [PATCH 1096/1208] refactor(magician): split oop introspection helpers --- .../class_metadata/oop_introspection.rs | 860 +----------------- .../oop_introspection/class_vars.rs | 197 ++++ .../oop_introspection/common.rs | 180 ++++ .../oop_introspection/methods.rs | 191 ++++ .../oop_introspection/object_vars.rs | 325 +++++++ 5 files changed, 902 insertions(+), 851 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/class_vars.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/methods.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index a35d013480..40dcce861f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -15,11 +15,16 @@ use super::super::super::*; use super::{eval_class_metadata_name, eval_class_relation_name_exists}; -use std::collections::HashSet; -const EVAL_CLASS_METADATA_FLAG_STATIC: u64 = 1; -const EVAL_CLASS_METADATA_FLAG_PROTECTED: u64 = 4; -const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; +mod class_vars; +mod common; +mod methods; +mod object_vars; + +pub(in crate::interpreter) use class_vars::*; +pub(in crate::interpreter) use methods::*; +pub(in crate::interpreter) use object_vars::*; +use common::*; /// Evaluates `method_exists()` or `property_exists()` from eval expressions. pub(in crate::interpreter) fn eval_builtin_member_exists( @@ -56,113 +61,6 @@ pub(in crate::interpreter) fn eval_member_exists_result( values.bool_value(exists) } -/// Evaluates `get_class_methods()` from eval expressions. -pub(in crate::interpreter) fn eval_builtin_get_class_methods( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let target = eval_expr(target, context, scope, values)?; - eval_get_class_methods_result(&[target], context, values) -} - -/// Evaluates materialized `get_class_methods()` arguments. -pub(in crate::interpreter) fn eval_get_class_methods_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let (class_name, target_is_object) = eval_class_metadata_target_name(*target, context, values)?; - if !target_is_object && !eval_class_relation_name_exists(&class_name, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let names = eval_class_method_names_for_scope(&class_name, context, values)?; - eval_indexed_string_array_result(&names, values) -} - -/// Evaluates `get_class_vars()` from eval expressions. -pub(in crate::interpreter) fn eval_builtin_get_class_vars( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let target = eval_expr(target, context, scope, values)?; - eval_get_class_vars_result(&[target], context, values) -} - -/// Evaluates materialized `get_class_vars()` arguments. -pub(in crate::interpreter) fn eval_get_class_vars_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let class_name = eval_resolved_class_metadata_name(*target, context, values)?; - if context.has_class(&class_name) || context.has_enum(&class_name) { - return eval_dynamic_class_vars_result(&class_name, context, values); - } - if context.has_trait(&class_name) { - return eval_dynamic_trait_vars_result(&class_name, context, values); - } - if context.has_interface(&class_name) { - return values.assoc_new(0); - } - if eval_class_relation_name_exists(&class_name, context, values)? { - return eval_runtime_class_vars_result(&class_name, context, values); - } - Err(EvalStatus::RuntimeFatal) -} - -/// Evaluates `get_object_vars()` from eval expressions. -pub(in crate::interpreter) fn eval_builtin_get_object_vars( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_get_object_vars_result(&[object], context, values) -} - -/// Evaluates materialized `get_object_vars()` arguments. -pub(in crate::interpreter) fn eval_get_object_vars_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - if values.type_tag(*object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let Ok(identity) = values.object_identity(*object) else { - return eval_public_object_vars_result(*object, values); - }; - let Some(class) = context.dynamic_object_class(identity) else { - let class_name = eval_object_class_metadata_name(*object, context, values)?; - return eval_runtime_object_vars_result(*object, &class_name, context, values); - }; - let class_name = class.name().to_string(); - eval_dynamic_object_vars_result(*object, &class_name, context, values) -} - /// Resolves a `method_exists()` target and applies PHP object-vs-string lookup rules. fn eval_method_exists_target( target: RuntimeCellHandle, @@ -446,743 +344,3 @@ fn eval_current_scope_private_property_exists_on_object( && !is_static && eval_same_class_metadata_name(&declaring_class, current_class)) } - -/// Resolves an object-or-class argument to a PHP class name and records whether it was an object. -fn eval_class_metadata_target_name( - target: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(String, bool), EvalStatus> { - match values.type_tag(target)? { - EVAL_TAG_OBJECT => Ok(( - eval_object_class_metadata_name(target, context, values)?, - true, - )), - EVAL_TAG_STRING => Ok(( - eval_resolved_class_metadata_name(target, context, values)?, - false, - )), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Resolves an object cell to its eval or runtime class name. -fn eval_object_class_metadata_name( - object: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - if let Some(class_name) = context.dynamic_object_class_name(identity) { - return Ok(class_name); - } - let class_name = values.object_class_name(object)?; - let class_name_bytes = values.string_bytes(class_name); - values.release(class_name)?; - let class_name = String::from_utf8(class_name_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(class_name.trim_start_matches('\\').to_string()) -} - -/// Reads a class-name cell and applies eval alias resolution. -fn eval_resolved_class_metadata_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_class_metadata_name(name, values)?; - Ok(context.resolve_class_name(&name).unwrap_or(name)) -} - -/// Collects PHP-visible methods for `get_class_methods()` in the current eval scope. -fn eval_class_method_names_for_scope( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if context.has_class(class_name) || context.has_enum(class_name) { - let mut names = Vec::new(); - let mut seen = HashSet::new(); - for name in context.class_method_names(class_name) { - let Some((declaring_class, method)) = context.class_method(class_name, &name) else { - eval_push_unique_method_name(&mut names, &mut seen, name); - continue; - }; - if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() { - eval_push_unique_method_name(&mut names, &mut seen, name); - } - } - eval_add_current_scope_private_method_names( - &mut names, &mut seen, class_name, context, values, - )?; - eval_add_native_parent_method_names( - &mut names, &mut seen, class_name, context, values, - )?; - return Ok(names); - } - if context.has_interface(class_name) { - return Ok(context.interface_method_names(class_name)); - } - if let Some(trait_decl) = context.trait_decl(class_name) { - return Ok(trait_decl - .methods() - .iter() - .filter(|method| method.visibility() == EvalVisibility::Public) - .map(|method| method.name().to_string()) - .collect()); - } - let method_names = values.reflection_method_names(class_name)?; - let names = eval_runtime_string_array_to_vec(method_names, values)?; - values.release(method_names)?; - let mut names = eval_visible_runtime_method_names(class_name, names, context, values)?; - let mut seen = names - .iter() - .map(|name| name.to_ascii_lowercase()) - .collect::>(); - eval_add_current_scope_private_method_names(&mut names, &mut seen, class_name, context, values)?; - Ok(names) -} - -/// Filters generated runtime methods to the surface visible from the current eval scope. -fn eval_visible_runtime_method_names( - lookup_class_name: &str, - names: Vec, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut result = Vec::new(); - for name in names { - let Some((declaring_class, visibility)) = - eval_runtime_method_access_metadata(lookup_class_name, &name, values)? - else { - continue; - }; - if validate_eval_member_access(&declaring_class, visibility, context).is_ok() { - result.push(name); - } - } - Ok(result) -} - -/// Adds generated/AOT parent method names inherited by one eval class. -fn eval_add_native_parent_method_names( - names: &mut Vec, - seen: &mut HashSet, - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(parent) = context.class_native_parent_name(class_name) else { - return Ok(()); - }; - let method_names = values.reflection_method_names(&parent)?; - let parent_names = eval_runtime_string_array_to_vec(method_names, values)?; - values.release(method_names)?; - let parent_names = - eval_visible_runtime_method_names(&parent, parent_names, context, values)?; - for name in parent_names { - eval_push_unique_method_name(names, seen, name); - } - Ok(()) -} - -/// Adds private methods declared by the current eval scope when PHP would expose them. -fn eval_add_current_scope_private_method_names( - names: &mut Vec, - seen: &mut HashSet, - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(current_class) = context.current_class_scope() else { - return Ok(()); - }; - if !eval_class_metadata_is_a(class_name, current_class, context) { - return Ok(()); - } - if let Some(class) = context.class(current_class) { - for method in class.methods() { - if method.visibility() == EvalVisibility::Private { - eval_push_unique_method_name(names, seen, method.name().to_string()); - } - } - return Ok(()); - } - if context.has_interface(current_class) || context.has_trait(current_class) { - return Ok(()); - } - if !eval_class_relation_name_exists(current_class, context, values)? { - return Ok(()); - } - let method_names = values.reflection_method_names(current_class)?; - let current_names = eval_runtime_string_array_to_vec(method_names, values)?; - values.release(method_names)?; - for name in current_names { - let Some((declaring_class, visibility)) = - eval_runtime_method_access_metadata(current_class, &name, values)? - else { - continue; - }; - if visibility == EvalVisibility::Private - && eval_same_class_metadata_name(&declaring_class, current_class) - { - eval_push_unique_method_name(names, seen, name); - } - } - Ok(()) -} - -/// Returns whether one eval or generated/AOT class name is the same as or extends another. -fn eval_class_metadata_is_a( - class_name: &str, - target: &str, - context: &ElephcEvalContext, -) -> bool { - eval_same_class_metadata_name(class_name, target) - || context.class_is_a(class_name, target, false) - || eval_native_class_metadata_is_a(class_name, target, context) -} - -/// Returns whether generated/AOT parent metadata proves one class extends another. -fn eval_native_class_metadata_is_a( - class_name: &str, - target: &str, - context: &ElephcEvalContext, -) -> bool { - let target = target.trim_start_matches('\\'); - let mut current = class_name.trim_start_matches('\\').to_string(); - let mut seen = HashSet::new(); - loop { - if !seen.insert(current.to_ascii_lowercase()) { - return false; - } - if eval_same_class_metadata_name(¤t, target) { - return true; - } - let Some(parent) = context.native_class_parent(¤t) else { - return false; - }; - current = parent.to_string(); - } -} - -/// Returns whether two PHP class names refer to the same normalized metadata name. -fn eval_same_class_metadata_name(left: &str, right: &str) -> bool { - left.trim_start_matches('\\') - .eq_ignore_ascii_case(right.trim_start_matches('\\')) -} - -/// Appends one method name while preserving PHP's case-insensitive uniqueness rule. -fn eval_push_unique_method_name( - names: &mut Vec, - seen: &mut HashSet, - name: String, -) { - if seen.insert(name.to_ascii_lowercase()) { - names.push(name); - } -} - -/// Returns access metadata for one generated/AOT method name, if reflection exposes it. -fn eval_runtime_method_access_metadata( - class_name: &str, - method_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { - return Ok(None); - }; - let declaring_class = values - .reflection_method_declaring_class(class_name, method_name)? - .unwrap_or_else(|| class_name.to_string()); - Ok(Some(( - declaring_class, - eval_runtime_member_visibility(flags), - ))) -} - -/// Builds `get_class_vars()` for an eval-declared class or enum. -fn eval_dynamic_class_vars_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(0)?; - let mut emitted_keys = HashSet::new(); - if let Some(enum_decl) = context.enum_decl(class_name) { - let name_value = values.null()?; - result = eval_add_class_var_entry(result, "name", name_value, values)?; - emitted_keys.insert(String::from("name")); - if enum_decl.backing_type().is_some() { - let value_value = values.null()?; - result = eval_add_class_var_entry(result, "value", value_value, values)?; - emitted_keys.insert(String::from("value")); - } - } - for class in context.class_chain(class_name).into_iter().rev() { - for property in class.properties() { - if emitted_keys.contains(property.name()) - || validate_eval_member_access(class.name(), property.visibility(), context) - .is_err() - { - continue; - } - let value = - eval_class_vars_property_default_value(class.name(), property, context, values)?; - result = eval_add_class_var_entry(result, property.name(), value, values)?; - emitted_keys.insert(property.name().to_string()); - } - } - Ok(result) -} - -/// Builds `get_class_vars()` for an eval-declared trait. -fn eval_dynamic_trait_vars_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(trait_decl) = context.trait_decl(class_name) else { - return Err(EvalStatus::RuntimeFatal); - }; - let trait_name = trait_decl.name().to_string(); - let properties = trait_decl.properties().to_vec(); - let mut result = values.assoc_new(properties.len())?; - let mut emitted_keys = HashSet::new(); - for property in properties { - if emitted_keys.contains(property.name()) - || validate_eval_member_access(&trait_name, property.visibility(), context).is_err() - { - continue; - } - let value = - eval_class_vars_property_default_value(&trait_name, &property, context, values)?; - result = eval_add_class_var_entry(result, property.name(), value, values)?; - emitted_keys.insert(property.name().to_string()); - } - Ok(result) -} - -/// Builds `get_class_vars()` data for generated/AOT class metadata. -fn eval_runtime_class_vars_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_names = values.reflection_property_names(class_name)?; - let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; - values.release(property_names)?; - let mut result = values.assoc_new(declared_names.len())?; - let mut emitted_keys = HashSet::new(); - for property_name in declared_names { - if emitted_keys.contains(&property_name) { - continue; - } - let Some((declaring_class, visibility, _is_static)) = - eval_runtime_property_access_metadata(class_name, &property_name, values)? - else { - continue; - }; - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - continue; - } - let value = eval_runtime_class_var_default_value( - class_name, - &declaring_class, - &property_name, - context, - values, - )?; - result = eval_add_class_var_entry(result, &property_name, value, values)?; - emitted_keys.insert(property_name); - } - Ok(result) -} - -/// Materializes one eval-declared property default for `get_class_vars()`. -fn eval_class_vars_property_default_value( - declaring_class: &str, - property: &EvalClassProperty, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(default) = property.default() else { - return values.null(); - }; - context.push_class_scope(declaring_class.to_string()); - context.push_called_class_scope(declaring_class.to_string()); - context.push_class_like_member_magic_scope(declaring_class, property.trait_origin()); - let result = eval_method_parameter_default(default, context, values); - context.pop_magic_scope(); - context.pop_called_class_scope(); - context.pop_class_scope(); - result -} - -/// Materializes one generated/AOT property default for `get_class_vars()`. -fn eval_runtime_class_var_default_value( - runtime_class: &str, - declaring_class: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(default) = context - .native_property_default(declaring_class, property_name) - .or_else(|| context.native_property_default(runtime_class, property_name)) - { - return materialize_native_callable_default(&default, context, values); - } - values.null() -} - -/// Adds one string-keyed class variable value to an associative result array. -fn eval_add_class_var_entry( - result: RuntimeCellHandle, - property_name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(property_name)?; - values.array_set(result, key, value) -} - -/// Builds `get_object_vars()` for an eval-declared object. -fn eval_dynamic_object_vars_result( - object: RuntimeCellHandle, - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - let mut result = values.assoc_new(property_count)?; - let mut emitted_keys = HashSet::new(); - let storage_keys = eval_declared_object_storage_names(class_name, context); - result = eval_add_enum_object_vars( - result, - object, - class_name, - &mut emitted_keys, - context, - values, - )?; - result = eval_add_declared_object_vars( - result, - object, - class_name, - &mut emitted_keys, - context, - values, - )?; - eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &storage_keys, values) -} - -/// Builds `get_object_vars()` for generated/AOT objects from reflection metadata. -fn eval_runtime_object_vars_result( - object: RuntimeCellHandle, - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_names = values.reflection_property_names(class_name)?; - let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; - values.release(property_names)?; - let property_count = values.object_property_len(object)?; - let mut result = values.assoc_new(declared_names.len() + property_count)?; - let mut emitted_keys = HashSet::new(); - result = eval_add_runtime_scope_private_object_vars( - result, - object, - &mut emitted_keys, - context, - values, - )?; - result = eval_add_runtime_declared_object_vars( - result, - object, - class_name, - &declared_names, - &mut emitted_keys, - context, - values, - )?; - eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &HashSet::new(), values) -} - -/// Adds generated/AOT private properties declared by the current eval class scope. -fn eval_add_runtime_scope_private_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - emitted_keys: &mut HashSet, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(current_class) = context.current_class_scope() else { - return Ok(result); - }; - if !values.object_is_a(object, current_class, false)? { - return Ok(result); - } - let property_names = values.reflection_property_names(current_class)?; - let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; - values.release(property_names)?; - for property_name in declared_names { - let Some((_, visibility, is_static)) = - eval_runtime_property_access_metadata(current_class, &property_name, values)? - else { - continue; - }; - if is_static - || visibility != EvalVisibility::Private - || emitted_keys.contains(&property_name) - || !values.property_is_initialized(object, &property_name)? - { - continue; - } - emitted_keys.insert(property_name.clone()); - let key = values.string(&property_name)?; - let value = values.property_get(object, &property_name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Adds generated/AOT declared instance properties visible from the current eval scope. -fn eval_add_runtime_declared_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - class_name: &str, - property_names: &[String], - emitted_keys: &mut HashSet, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - for property_name in property_names { - let Some((declaring_class, visibility, is_static)) = - eval_runtime_property_access_metadata(class_name, property_name, values)? - else { - continue; - }; - if is_static - || validate_eval_member_access(&declaring_class, visibility, context).is_err() - || emitted_keys.contains(property_name) - || !values.property_is_initialized(object, property_name)? - { - continue; - } - emitted_keys.insert(property_name.clone()); - let key = values.string(property_name)?; - let value = values.property_get(object, property_name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Returns access metadata for one generated/AOT property name, if reflection exposes it. -fn eval_runtime_property_access_metadata( - class_name: &str, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(flags) = values.reflection_property_flags(class_name, property_name)? else { - return Ok(None); - }; - let declaring_class = values - .reflection_property_declaring_class(class_name, property_name)? - .unwrap_or_else(|| class_name.to_string()); - Ok(Some(( - declaring_class, - eval_runtime_member_visibility(flags), - flags & EVAL_CLASS_METADATA_FLAG_STATIC != 0, - ))) -} - -/// Converts generated/AOT reflection member flags into eval visibility metadata. -fn eval_runtime_member_visibility(flags: u64) -> EvalVisibility { - if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_CLASS_METADATA_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - } -} - -/// Adds synthetic enum properties exposed by PHP enum case objects. -fn eval_add_enum_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - class_name: &str, - emitted_keys: &mut HashSet, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(enum_decl) = context.enum_decl(class_name) else { - return Ok(result); - }; - let is_backed = enum_decl.backing_type().is_some(); - result = eval_add_object_var(result, object, "name", emitted_keys, context, values)?; - if is_backed { - result = eval_add_object_var(result, object, "value", emitted_keys, context, values)?; - } - Ok(result) -} - -/// Adds declared instance properties visible from the current eval scope. -fn eval_add_declared_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - class_name: &str, - emitted_keys: &mut HashSet, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - for class in context.class_chain(class_name) { - for property in class.properties() { - if property.is_static() - || validate_eval_member_access(class.name(), property.visibility(), context) - .is_err() - || emitted_keys.contains(property.name()) - { - continue; - } - let storage_property_name = eval_instance_property_storage_name(class.name(), property); - if !property.is_virtual() - && !context.dynamic_property_is_initialized(identity, &storage_property_name) - { - continue; - } - result = eval_add_object_var( - result, - object, - property.name(), - emitted_keys, - context, - values, - )?; - } - } - Ok(result) -} - -/// Adds one visible object variable to an associative result array. -fn eval_add_object_var( - result: RuntimeCellHandle, - object: RuntimeCellHandle, - property_name: &str, - emitted_keys: &mut HashSet, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - emitted_keys.insert(property_name.to_string()); - let key = values.string(property_name)?; - let value = eval_property_get_result(object, property_name, context, values)?; - values.array_set(result, key, value) -} - -/// Adds public dynamic properties that are not declared storage slots. -fn eval_add_dynamic_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - emitted_keys: &mut HashSet, - storage_keys: &HashSet, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - if key_name.contains('\0') - || storage_keys.contains(&key_name) - || !emitted_keys.insert(key_name.clone()) - { - continue; - } - let key = values.string(&key_name)?; - let value = values.property_get(object, &key_name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Returns physical storage names used by declared eval object properties. -fn eval_declared_object_storage_names( - class_name: &str, - context: &ElephcEvalContext, -) -> HashSet { - let mut names = HashSet::new(); - for class in context.class_chain(class_name) { - for property in class.properties() { - names.insert(eval_instance_property_storage_name(class.name(), property)); - } - } - names -} - -/// Builds `get_object_vars()` for runtime objects with public bridge-visible properties. -fn eval_public_object_vars_result( - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - let mut result = values.assoc_new(property_count)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.string(&key_name)?; - let value = values.property_get(object, &key_name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Returns whether an object has a public bridge-visible property by exact name. -fn eval_object_public_property_exists( - object: RuntimeCellHandle, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - if key_bytes? == property_name.as_bytes() { - return Ok(true); - } - } - Ok(false) -} - -/// Builds an indexed PHP array from owned Rust strings. -fn eval_indexed_string_array_result( - names: &[String], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(names.len())?; - for (index, name) in names.iter().enumerate() { - let key = values.int(index as i64)?; - let value = values.string(name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Copies a runtime string array into Rust-owned strings for class metadata helpers. -fn eval_runtime_string_array_to_vec( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut result = Vec::with_capacity(len); - for position in 0..len { - let key = values.int(position as i64)?; - let value = values.array_get(array, key)?; - result.push(eval_class_metadata_name(value, values)?); - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/class_vars.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/class_vars.rs new file mode 100644 index 0000000000..18bf8ddbdb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/class_vars.rs @@ -0,0 +1,197 @@ +//! Purpose: +//! Implements `get_class_vars()` for eval-declared and generated/AOT metadata. +//! +//! Called from: +//! - `crate::interpreter::builtins::class_metadata::oop_introspection`. +//! - Declarative class metadata builtin dispatch hooks. +//! +//! Key details: +//! - Eval-declared defaults are materialized in the declaring class scope. +//! - Generated/AOT defaults use native callable default metadata when present. + +use super::*; +use std::collections::HashSet; + +/// Evaluates `get_class_vars()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_class_vars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + eval_get_class_vars_result(&[target], context, values) +} + +/// Evaluates materialized `get_class_vars()` arguments. +pub(in crate::interpreter) fn eval_get_class_vars_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let class_name = eval_resolved_class_metadata_name(*target, context, values)?; + if context.has_class(&class_name) || context.has_enum(&class_name) { + return eval_dynamic_class_vars_result(&class_name, context, values); + } + if context.has_trait(&class_name) { + return eval_dynamic_trait_vars_result(&class_name, context, values); + } + if context.has_interface(&class_name) { + return values.assoc_new(0); + } + if eval_class_relation_name_exists(&class_name, context, values)? { + return eval_runtime_class_vars_result(&class_name, context, values); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Builds `get_class_vars()` for an eval-declared class or enum. +fn eval_dynamic_class_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(0)?; + let mut emitted_keys = HashSet::new(); + if let Some(enum_decl) = context.enum_decl(class_name) { + let name_value = values.null()?; + result = eval_add_class_var_entry(result, "name", name_value, values)?; + emitted_keys.insert(String::from("name")); + if enum_decl.backing_type().is_some() { + let value_value = values.null()?; + result = eval_add_class_var_entry(result, "value", value_value, values)?; + emitted_keys.insert(String::from("value")); + } + } + for class in context.class_chain(class_name).into_iter().rev() { + for property in class.properties() { + if emitted_keys.contains(property.name()) + || validate_eval_member_access(class.name(), property.visibility(), context) + .is_err() + { + continue; + } + let value = + eval_class_vars_property_default_value(class.name(), property, context, values)?; + result = eval_add_class_var_entry(result, property.name(), value, values)?; + emitted_keys.insert(property.name().to_string()); + } + } + Ok(result) +} + +/// Builds `get_class_vars()` for an eval-declared trait. +fn eval_dynamic_trait_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(trait_decl) = context.trait_decl(class_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + let trait_name = trait_decl.name().to_string(); + let properties = trait_decl.properties().to_vec(); + let mut result = values.assoc_new(properties.len())?; + let mut emitted_keys = HashSet::new(); + for property in properties { + if emitted_keys.contains(property.name()) + || validate_eval_member_access(&trait_name, property.visibility(), context).is_err() + { + continue; + } + let value = eval_class_vars_property_default_value(&trait_name, &property, context, values)?; + result = eval_add_class_var_entry(result, property.name(), value, values)?; + emitted_keys.insert(property.name().to_string()); + } + Ok(result) +} + +/// Builds `get_class_vars()` data for generated/AOT class metadata. +fn eval_runtime_class_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_names = values.reflection_property_names(class_name)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + let mut result = values.assoc_new(declared_names.len())?; + let mut emitted_keys = HashSet::new(); + for property_name in declared_names { + if emitted_keys.contains(&property_name) { + continue; + } + let Some((declaring_class, visibility, _is_static)) = + eval_runtime_property_access_metadata(class_name, &property_name, values)? + else { + continue; + }; + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + continue; + } + let value = eval_runtime_class_var_default_value( + class_name, + &declaring_class, + &property_name, + context, + values, + )?; + result = eval_add_class_var_entry(result, &property_name, value, values)?; + emitted_keys.insert(property_name); + } + Ok(result) +} + +/// Materializes one eval-declared property default for `get_class_vars()`. +fn eval_class_vars_property_default_value( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(default) = property.default() else { + return values.null(); + }; + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + context.push_class_like_member_magic_scope(declaring_class, property.trait_origin()); + let result = eval_method_parameter_default(default, context, values); + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + result +} + +/// Materializes one generated/AOT property default for `get_class_vars()`. +fn eval_runtime_class_var_default_value( + runtime_class: &str, + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(default) = context + .native_property_default(declaring_class, property_name) + .or_else(|| context.native_property_default(runtime_class, property_name)) + { + return materialize_native_callable_default(&default, context, values); + } + values.null() +} + +/// Adds one string-keyed class variable value to an associative result array. +fn eval_add_class_var_entry( + result: RuntimeCellHandle, + property_name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(property_name)?; + values.array_set(result, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs new file mode 100644 index 0000000000..2d8c771364 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs @@ -0,0 +1,180 @@ +//! Purpose: +//! Shared class-name resolution and reflection metadata helpers for eval OOP +//! introspection builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::class_metadata::oop_introspection` modules. +//! +//! Key details: +//! - Helpers normalize PHP class names case-insensitively while preserving +//! runtime reflection ownership and visibility metadata. + +use super::*; +use std::collections::HashSet; + +pub(super) const EVAL_CLASS_METADATA_FLAG_STATIC: u64 = 1; +const EVAL_CLASS_METADATA_FLAG_PROTECTED: u64 = 4; +const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; + +/// Resolves an object-or-class argument to a PHP class name and records whether it was an object. +pub(super) fn eval_class_metadata_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(String, bool), EvalStatus> { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => Ok(( + eval_object_class_metadata_name(target, context, values)?, + true, + )), + EVAL_TAG_STRING => Ok(( + eval_resolved_class_metadata_name(target, context, values)?, + false, + )), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves an object cell to its eval or runtime class name. +pub(super) fn eval_object_class_metadata_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + let class_name = values.object_class_name(object)?; + let class_name_bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(class_name_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Reads a class-name cell and applies eval alias resolution. +pub(super) fn eval_resolved_class_metadata_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_class_metadata_name(name, values)?; + Ok(context.resolve_class_name(&name).unwrap_or(name)) +} + +/// Returns whether one eval or generated/AOT class name is the same as or extends another. +pub(super) fn eval_class_metadata_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + eval_same_class_metadata_name(class_name, target) + || context.class_is_a(class_name, target, false) + || eval_native_class_metadata_is_a(class_name, target, context) +} + +/// Returns whether generated/AOT parent metadata proves one class extends another. +fn eval_native_class_metadata_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + let target = target.trim_start_matches('\\'); + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return false; + } + if eval_same_class_metadata_name(¤t, target) { + return true; + } + let Some(parent) = context.native_class_parent(¤t) else { + return false; + }; + current = parent.to_string(); + } +} + +/// Returns whether two PHP class names refer to the same normalized metadata name. +pub(super) fn eval_same_class_metadata_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns access metadata for one generated/AOT method name, if reflection exposes it. +pub(super) fn eval_runtime_method_access_metadata( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_method_declaring_class(class_name, method_name)? + .unwrap_or_else(|| class_name.to_string()); + Ok(Some(( + declaring_class, + eval_runtime_member_visibility(flags), + ))) +} + +/// Returns access metadata for one generated/AOT property name, if reflection exposes it. +pub(super) fn eval_runtime_property_access_metadata( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_property_flags(class_name, property_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_property_declaring_class(class_name, property_name)? + .unwrap_or_else(|| class_name.to_string()); + Ok(Some(( + declaring_class, + eval_runtime_member_visibility(flags), + flags & EVAL_CLASS_METADATA_FLAG_STATIC != 0, + ))) +} + +/// Converts generated/AOT reflection member flags into eval visibility metadata. +fn eval_runtime_member_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_CLASS_METADATA_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + +/// Builds an indexed PHP array from owned Rust strings. +pub(super) fn eval_indexed_string_array_result( + names: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + let key = values.int(index as i64)?; + let value = values.string(name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Copies a runtime string array into Rust-owned strings for class metadata helpers. +pub(super) fn eval_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_class_metadata_name(value, values)?); + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/methods.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/methods.rs new file mode 100644 index 0000000000..908c763a6a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/methods.rs @@ -0,0 +1,191 @@ +//! Purpose: +//! Implements `get_class_methods()` for eval and generated/AOT class metadata. +//! +//! Called from: +//! - `crate::interpreter::builtins::class_metadata::oop_introspection`. +//! - Declarative class metadata builtin dispatch hooks. +//! +//! Key details: +//! - Method lists are filtered through the current eval scope and PHP visibility +//! rules before materialization. + +use super::*; +use std::collections::HashSet; + +/// Evaluates `get_class_methods()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_class_methods( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + eval_get_class_methods_result(&[target], context, values) +} + +/// Evaluates materialized `get_class_methods()` arguments. +pub(in crate::interpreter) fn eval_get_class_methods_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let (class_name, target_is_object) = + eval_class_metadata_target_name(*target, context, values)?; + if !target_is_object && !eval_class_relation_name_exists(&class_name, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let names = eval_class_method_names_for_scope(&class_name, context, values)?; + eval_indexed_string_array_result(&names, values) +} + +/// Collects PHP-visible methods for `get_class_methods()` in the current eval scope. +fn eval_class_method_names_for_scope( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(class_name) || context.has_enum(class_name) { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for name in context.class_method_names(class_name) { + let Some((declaring_class, method)) = context.class_method(class_name, &name) else { + eval_push_unique_method_name(&mut names, &mut seen, name); + continue; + }; + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() { + eval_push_unique_method_name(&mut names, &mut seen, name); + } + } + eval_add_current_scope_private_method_names( + &mut names, &mut seen, class_name, context, values, + )?; + eval_add_native_parent_method_names(&mut names, &mut seen, class_name, context, values)?; + return Ok(names); + } + if context.has_interface(class_name) { + return Ok(context.interface_method_names(class_name)); + } + if let Some(trait_decl) = context.trait_decl(class_name) { + return Ok(trait_decl + .methods() + .iter() + .filter(|method| method.visibility() == EvalVisibility::Public) + .map(|method| method.name().to_string()) + .collect()); + } + let method_names = values.reflection_method_names(class_name)?; + let names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + let mut names = eval_visible_runtime_method_names(class_name, names, context, values)?; + let mut seen = names + .iter() + .map(|name| name.to_ascii_lowercase()) + .collect::>(); + eval_add_current_scope_private_method_names(&mut names, &mut seen, class_name, context, values)?; + Ok(names) +} + +/// Filters generated runtime methods to the surface visible from the current eval scope. +fn eval_visible_runtime_method_names( + lookup_class_name: &str, + names: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut result = Vec::new(); + for name in names { + let Some((declaring_class, visibility)) = + eval_runtime_method_access_metadata(lookup_class_name, &name, values)? + else { + continue; + }; + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() { + result.push(name); + } + } + Ok(result) +} + +/// Adds generated/AOT parent method names inherited by one eval class. +fn eval_add_native_parent_method_names( + names: &mut Vec, + seen: &mut HashSet, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(()); + }; + let method_names = values.reflection_method_names(&parent)?; + let parent_names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + let parent_names = eval_visible_runtime_method_names(&parent, parent_names, context, values)?; + for name in parent_names { + eval_push_unique_method_name(names, seen, name); + } + Ok(()) +} + +/// Adds private methods declared by the current eval scope when PHP would expose them. +fn eval_add_current_scope_private_method_names( + names: &mut Vec, + seen: &mut HashSet, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(current_class) = context.current_class_scope() else { + return Ok(()); + }; + if !eval_class_metadata_is_a(class_name, current_class, context) { + return Ok(()); + } + if let Some(class) = context.class(current_class) { + for method in class.methods() { + if method.visibility() == EvalVisibility::Private { + eval_push_unique_method_name(names, seen, method.name().to_string()); + } + } + return Ok(()); + } + if context.has_interface(current_class) || context.has_trait(current_class) { + return Ok(()); + } + if !eval_class_relation_name_exists(current_class, context, values)? { + return Ok(()); + } + let method_names = values.reflection_method_names(current_class)?; + let current_names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + for name in current_names { + let Some((declaring_class, visibility)) = + eval_runtime_method_access_metadata(current_class, &name, values)? + else { + continue; + }; + if visibility == EvalVisibility::Private + && eval_same_class_metadata_name(&declaring_class, current_class) + { + eval_push_unique_method_name(names, seen, name); + } + } + Ok(()) +} + +/// Appends one method name while preserving PHP's case-insensitive uniqueness rule. +fn eval_push_unique_method_name( + names: &mut Vec, + seen: &mut HashSet, + name: String, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs new file mode 100644 index 0000000000..d6f1863d81 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs @@ -0,0 +1,325 @@ +//! Purpose: +//! Implements `get_object_vars()` for eval-declared and generated/AOT objects. +//! +//! Called from: +//! - `crate::interpreter::builtins::class_metadata::oop_introspection`. +//! - Declarative class metadata builtin dispatch hooks. +//! +//! Key details: +//! - Declared eval properties use storage-name filtering so inaccessible +//! protected/private slots do not leak as public dynamic properties. + +use super::*; +use std::collections::HashSet; + +/// Evaluates `get_object_vars()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_object_vars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_get_object_vars_result(&[object], context, values) +} + +/// Evaluates materialized `get_object_vars()` arguments. +pub(in crate::interpreter) fn eval_get_object_vars_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + if values.type_tag(*object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let Ok(identity) = values.object_identity(*object) else { + return eval_public_object_vars_result(*object, values); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_object_class_metadata_name(*object, context, values)?; + return eval_runtime_object_vars_result(*object, &class_name, context, values); + }; + let class_name = class.name().to_string(); + eval_dynamic_object_vars_result(*object, &class_name, context, values) +} + +/// Builds `get_object_vars()` for an eval-declared object. +fn eval_dynamic_object_vars_result( + object: RuntimeCellHandle, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(property_count)?; + let mut emitted_keys = HashSet::new(); + let storage_keys = eval_declared_object_storage_names(class_name, context); + result = eval_add_enum_object_vars(result, object, class_name, &mut emitted_keys, context, values)?; + result = eval_add_declared_object_vars( + result, + object, + class_name, + &mut emitted_keys, + context, + values, + )?; + eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &storage_keys, values) +} + +/// Builds `get_object_vars()` for generated/AOT objects from reflection metadata. +fn eval_runtime_object_vars_result( + object: RuntimeCellHandle, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_names = values.reflection_property_names(class_name)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(declared_names.len() + property_count)?; + let mut emitted_keys = HashSet::new(); + result = eval_add_runtime_scope_private_object_vars( + result, + object, + &mut emitted_keys, + context, + values, + )?; + result = eval_add_runtime_declared_object_vars( + result, + object, + class_name, + &declared_names, + &mut emitted_keys, + context, + values, + )?; + eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &HashSet::new(), values) +} + +/// Adds generated/AOT private properties declared by the current eval class scope. +fn eval_add_runtime_scope_private_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + emitted_keys: &mut HashSet, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(current_class) = context.current_class_scope() else { + return Ok(result); + }; + if !values.object_is_a(object, current_class, false)? { + return Ok(result); + } + let property_names = values.reflection_property_names(current_class)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + for property_name in declared_names { + let Some((_, visibility, is_static)) = + eval_runtime_property_access_metadata(current_class, &property_name, values)? + else { + continue; + }; + if is_static + || visibility != EvalVisibility::Private + || emitted_keys.contains(&property_name) + || !values.property_is_initialized(object, &property_name)? + { + continue; + } + emitted_keys.insert(property_name.clone()); + let key = values.string(&property_name)?; + let value = values.property_get(object, &property_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Adds generated/AOT declared instance properties visible from the current eval scope. +fn eval_add_runtime_declared_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + property_names: &[String], + emitted_keys: &mut HashSet, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + for property_name in property_names { + let Some((declaring_class, visibility, is_static)) = + eval_runtime_property_access_metadata(class_name, property_name, values)? + else { + continue; + }; + if is_static + || validate_eval_member_access(&declaring_class, visibility, context).is_err() + || emitted_keys.contains(property_name) + || !values.property_is_initialized(object, property_name)? + { + continue; + } + emitted_keys.insert(property_name.clone()); + let key = values.string(property_name)?; + let value = values.property_get(object, property_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Adds synthetic enum properties exposed by PHP enum case objects. +fn eval_add_enum_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(enum_decl) = context.enum_decl(class_name) else { + return Ok(result); + }; + let is_backed = enum_decl.backing_type().is_some(); + result = eval_add_object_var(result, object, "name", emitted_keys, context, values)?; + if is_backed { + result = eval_add_object_var(result, object, "value", emitted_keys, context, values)?; + } + Ok(result) +} + +/// Adds declared instance properties visible from the current eval scope. +fn eval_add_declared_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + for class in context.class_chain(class_name) { + for property in class.properties() { + if property.is_static() + || validate_eval_member_access(class.name(), property.visibility(), context) + .is_err() + || emitted_keys.contains(property.name()) + { + continue; + } + let storage_property_name = eval_instance_property_storage_name(class.name(), property); + if !property.is_virtual() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + continue; + } + result = eval_add_object_var( + result, + object, + property.name(), + emitted_keys, + context, + values, + )?; + } + } + Ok(result) +} + +/// Adds one visible object variable to an associative result array. +fn eval_add_object_var( + result: RuntimeCellHandle, + object: RuntimeCellHandle, + property_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + emitted_keys.insert(property_name.to_string()); + let key = values.string(property_name)?; + let value = eval_property_get_result(object, property_name, context, values)?; + values.array_set(result, key, value) +} + +/// Adds public dynamic properties that are not declared storage slots. +fn eval_add_dynamic_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + emitted_keys: &mut HashSet, + storage_keys: &HashSet, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + if key_name.contains('\0') + || storage_keys.contains(&key_name) + || !emitted_keys.insert(key_name.clone()) + { + continue; + } + let key = values.string(&key_name)?; + let value = values.property_get(object, &key_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns physical storage names used by declared eval object properties. +fn eval_declared_object_storage_names( + class_name: &str, + context: &ElephcEvalContext, +) -> HashSet { + let mut names = HashSet::new(); + for class in context.class_chain(class_name) { + for property in class.properties() { + names.insert(eval_instance_property_storage_name(class.name(), property)); + } + } + names +} + +/// Builds `get_object_vars()` for runtime objects with public bridge-visible properties. +fn eval_public_object_vars_result( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(property_count)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.string(&key_name)?; + let value = values.property_get(object, &key_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns whether an object has a public bridge-visible property by exact name. +pub(super) fn eval_object_public_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} From a112ba7d976c82462ce6be2e05724c52c3e91cc2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 09:35:41 +0200 Subject: [PATCH 1097/1208] refactor(magician): finish builtin structure cleanup --- .../elephc-magician-builtin-structure-plan.md | 28 +- .../builtins/filesystem/stream_sockets.rs | 560 +----- .../filesystem/stream_sockets/common.rs | 51 + .../filesystem/stream_sockets/datagrams.rs | 86 + .../filesystem/stream_sockets/lifecycle.rs | 157 ++ .../filesystem/stream_sockets/open.rs | 142 ++ .../filesystem/stream_sockets/selection.rs | 160 ++ .../interpreter/builtins/registry/callable.rs | 3 + .../builtins/registry/callable_validation.rs | 3 + .../builtins/registry/dynamic_mutation.rs | 3 + .../src/interpreter/builtins/registry/mod.rs | 1089 +----------- .../interpreter/builtins/registry/tests.rs | 20 + .../builtins/registry/tests/direct_hooks.rs | 55 + .../builtins/registry/tests/exposure.rs | 295 ++++ .../builtins/registry/tests/metadata_core.rs | 172 ++ .../registry/tests/metadata_filesystem.rs | 147 ++ .../builtins/registry/tests/metadata_misc.rs | 143 ++ .../builtins/registry/tests/metadata_regex.rs | 39 + .../registry/tests/metadata_streams.rs | 181 ++ .../registry/tests/metadata_time_and_env.rs | 159 ++ .../src/interpreter/builtins/symbols.rs | 1546 +---------------- .../builtins/symbols/callable_probe.rs | 493 ++++++ .../builtins/symbols/class_names.rs | 282 +++ .../builtins/symbols/class_relations.rs | 247 +++ .../interpreter/builtins/symbols/constants.rs | 111 ++ .../builtins/symbols/function_probe.rs | 139 ++ .../builtins/symbols/language_constructs.rs | 324 ++++ .../src/interpreter/builtins/time/aliases.rs | 3 + .../src/interpreter/expressions.rs | 1183 +------------ .../src/interpreter/expressions/calls.rs | 196 +++ .../expressions/calls/first_class.rs | 487 ++++++ .../expressions/calls/first_class_support.rs | 150 ++ .../src/interpreter/expressions/evaluation.rs | 391 +++++ 33 files changed, 4706 insertions(+), 4339 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/common.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/lifecycle.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/open.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/selection.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests/exposure.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_misc.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_regex.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_streams.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_time_and_env.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_names.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_relations.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/constants.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/function_probe.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/language_constructs.rs create mode 100644 crates/elephc-magician/src/interpreter/expressions/calls.rs create mode 100644 crates/elephc-magician/src/interpreter/expressions/calls/first_class.rs create mode 100644 crates/elephc-magician/src/interpreter/expressions/calls/first_class_support.rs create mode 100644 crates/elephc-magician/src/interpreter/expressions/evaluation.rs diff --git a/.plans/elephc-magician-builtin-structure-plan.md b/.plans/elephc-magician-builtin-structure-plan.md index be9a2fba17..69301be49b 100644 --- a/.plans/elephc-magician-builtin-structure-plan.md +++ b/.plans/elephc-magician-builtin-structure-plan.md @@ -14,14 +14,14 @@ by-ref parameters, and dispatch in migrated areas. - [x] Phase 2: keep ordinary files below 500 LoC, leaving exceptions only for cohesive single-scope helpers documented in their module preambles. -- [ ] Phase 3: split remaining large builtin files (`symbols.rs`, +- [x] Phase 3: split remaining large builtin files (`symbols.rs`, `filesystem/streams.rs`, `class_metadata/oop_introspection.rs`, `registry/callable.rs`, `arrays/core.rs`) into builtin home files and shared helpers. -- [ ] Phase 3: replace the giant direct-dispatch match in +- [x] Phase 3: replace the giant direct-dispatch match in `interpreter/expressions.rs` with smaller registry lookups, preserving special paths for language constructs and by-ref/source-sensitive calls. -- [ ] Phase 3: update agent/contributor documentation if the workflow for adding +- [x] Phase 3: update agent/contributor documentation if the workflow for adding eval builtins changes. ## Goal @@ -200,6 +200,28 @@ For each one, decide whether: preamble; - it mixes responsibilities and should be split before builtin migration. +Phase 3 completion notes: + +- `registry/names.rs` and `registry/signature.rs` are now thin registry-derived + helpers rather than manual tables. +- `interpreter/expressions.rs` no longer contains the giant positional builtin + dispatch match. Function-like calls live under `interpreter/expressions/calls*` + and fall through to `eval_declared_builtin_direct_call()` after preserving the + special source-sensitive and by-reference paths. +- `interpreter/builtins/symbols.rs` is now an orchestration module with focused + `symbols/` modules for callable probes, function probes, constants, + class-name lookup, class relations, and language constructs. +- `filesystem/streams.rs`, `class_metadata/oop_introspection.rs`, and + `arrays/core.rs` have already been split into focused helper modules. +- `filesystem/stream_sockets.rs` was also split during the cleanup because it + was a multi-builtin stream-socket bucket above the ordinary file-size target. +- `registry/callable.rs`, `registry/callable_validation.rs`, + `registry/dynamic_mutation.rs`, and `time/aliases.rs` remain above 500 LoC as + documented single-scope engines in their module preambles. +- No `AGENTS.md` or contributor workflow update was needed for Phase 3 because + adding eval builtins still follows the existing one-home-file plus area + `mod.rs` wiring model established by the declarative registry. + ## Acceptance Criteria - Adding an eval builtin requires one home file and at most area `mod.rs` wiring, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs index 140185be13..6257039e54 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs @@ -1,556 +1,26 @@ //! Purpose: -//! Implements eval stream socket builtins over host TCP and local socket pairs. +//! Orchestrates eval stream socket builtins over host TCP and local socket pairs. //! //! Called from: //! - `crate::interpreter::expressions::eval_positional_expr_call()`. //! - Dynamic callable dispatch under `builtins::registry::dispatch`. //! //! Key details: -//! - TCP streams are inserted into eval's normal File-backed stream table so -//! existing fread/fwrite/close paths keep working. -//! - TLS enablement is conservative: disabling succeeds for valid streams, -//! enabling returns false because eval does not own TLS state. +//! - Concrete socket builtin behavior lives in focused `stream_sockets/` modules +//! so stream resource helpers and by-reference writeback stay isolated. use super::super::super::*; use super::*; -/// Evaluates `stream_socket_server(address)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_server( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [address] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let address = eval_expr(address, context, scope, values)?; - eval_stream_socket_server_result(address, context, values) -} - -/// Opens a TCP listener resource. -pub(in crate::interpreter) fn eval_stream_socket_server_result( - address: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_path_string(address, values)?; - match context.stream_resources_mut().open_tcp_listener(&address) { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Evaluates `stream_socket_client(address)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_client( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [address] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let address = eval_expr(address, context, scope, values)?; - eval_stream_socket_client_result(address, context, values) -} - -/// Opens a connected TCP stream resource. -pub(in crate::interpreter) fn eval_stream_socket_client_result( - address: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_path_string(address, values)?; - match context.stream_resources_mut().open_tcp_stream(&address) { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Evaluates `fsockopen()` or `pfsockopen()` over full eval call metadata. -pub(in crate::interpreter) fn eval_builtin_fsockopen_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let (bound, _) = bind_evaluated_ref_builtin_args( - &["hostname", "port", "error_code", "error_message", "timeout"], - &evaluated_args, - false, - )?; - let host = required_evaluated_ref_arg(&bound, 0)?; - let port = required_evaluated_ref_arg(&bound, 1)?; - let error_code_target = optional_evaluated_ref_arg(&bound, 2) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - let error_message_target = optional_evaluated_ref_arg(&bound, 3) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - let (result, error_code, error_message) = - eval_fsockopen_with_error_result(host.value, port.value, context, values)?; - eval_write_socket_int_output_ref_target( - error_code_target.as_ref(), - error_code, - context, - values, - )?; - eval_write_socket_output_ref_target( - error_message_target.as_ref(), - Some(error_message), - context, - values, - )?; - Ok(result) -} - -/// Opens a connected TCP stream from host and port cells. -pub(in crate::interpreter) fn eval_fsockopen_result( - host: RuntimeCellHandle, - port: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let host = eval_path_string(host, values)?; - let port = eval_int_value(port, values)?; - match context - .stream_resources_mut() - .open_tcp_stream_host_port(&host, port) - { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Opens a host/port TCP stream and returns PHP `fsockopen()` error outputs. -pub(in crate::interpreter) fn eval_fsockopen_with_error_result( - host: RuntimeCellHandle, - port: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, i64, String), EvalStatus> { - let host = eval_path_string(host, values)?; - let port = eval_int_value(port, values)?; - match context - .stream_resources_mut() - .open_tcp_stream_host_port_result(&host, port) - { - Ok(id) => Ok((values.resource(id)?, 0, String::new())), - Err(error) => { - let error_code = i64::from(error.raw_os_error().unwrap_or(0)); - Ok((values.bool_value(false)?, error_code, error.to_string())) - } - } -} - -/// Evaluates `stream_socket_accept()` over full eval call metadata. -pub(in crate::interpreter) fn eval_builtin_stream_socket_accept_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let (bound, _) = bind_evaluated_ref_builtin_args( - &["socket", "timeout", "peer_name"], - &evaluated_args, - false, - )?; - let socket = required_evaluated_ref_arg(&bound, 0)?; - let peer_name_target = optional_evaluated_ref_arg(&bound, 2) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - let (result, peer_name) = - eval_stream_socket_accept_with_peer_result(socket.value, context, values)?; - eval_write_socket_output_ref_target(peer_name_target.as_ref(), peer_name, context, values)?; - Ok(result) -} - -/// Accepts one pending TCP connection from a listener resource. -pub(in crate::interpreter) fn eval_stream_socket_accept_result( - socket: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_socket_resource_id(socket, values)?; - match context.stream_resources_mut().accept_tcp(id) { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Accepts one TCP connection and returns the accepted resource plus peer endpoint name. -pub(in crate::interpreter) fn eval_stream_socket_accept_with_peer_result( - socket: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, Option), EvalStatus> { - let id = eval_socket_resource_id(socket, values)?; - let Some(accepted_id) = context.stream_resources_mut().accept_tcp(id) else { - return values.bool_value(false).map(|result| (result, None)); - }; - let peer_name = context.stream_resources().socket_name(accepted_id, true); - let result = values.resource(accepted_id)?; - Ok((result, peer_name)) -} - -/// Evaluates `stream_socket_get_name(socket, remote)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_get_name( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [socket, remote] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let socket = eval_expr(socket, context, scope, values)?; - let remote = eval_expr(remote, context, scope, values)?; - eval_stream_socket_get_name_result(socket, remote, context, values) -} - -/// Returns a tracked local or remote socket endpoint name. -pub(in crate::interpreter) fn eval_stream_socket_get_name_result( - socket: RuntimeCellHandle, - remote: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_socket_resource_id(socket, values)?; - let remote = values.truthy(remote)?; - match context.stream_resources().socket_name(id, remote) { - Some(name) => values.string(&name), - None => values.bool_value(false), - } -} - -/// Evaluates `stream_socket_shutdown(stream, mode)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_shutdown( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, mode] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_stream_socket_shutdown_result(stream, mode, context, values) -} - -/// Applies a socket shutdown mode to a stream resource. -pub(in crate::interpreter) fn eval_stream_socket_shutdown_result( - stream: RuntimeCellHandle, - mode: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_socket_resource_id(stream, values)?; - let mode = eval_int_value(mode, values)?; - values.bool_value( - context - .stream_resources() - .socket_shutdown(id, mode) - .unwrap_or(false), - ) -} - -/// Evaluates `stream_socket_enable_crypto(...)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_enable_crypto( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let enable = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - let _ = eval_expr(arg, context, scope, values)?; - } - eval_stream_socket_enable_crypto_result(stream, enable, context, values) -} - -/// Returns TLS enablement status for eval socket streams. -pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_result( - stream: RuntimeCellHandle, - enable: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_socket_resource_id(stream, values)?; - if !context.stream_resources().has_stream(id) { - return values.bool_value(false); - } - let disabled = !values.truthy(enable)?; - values.bool_value(disabled) -} - -/// Evaluates `stream_socket_sendto(stream, data, flags = 0, address = null)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_sendto( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let data = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - let _ = eval_expr(arg, context, scope, values)?; - } - eval_stream_socket_sendto_result(stream, data, context, values) -} - -/// Writes bytes to a connected socket stream. -pub(in crate::interpreter) fn eval_stream_socket_sendto_result( - stream: RuntimeCellHandle, - data: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_fwrite_result(stream, data, context, values) -} - -/// Evaluates `stream_socket_recvfrom()` over full eval call metadata. -pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let (bound, _) = bind_evaluated_ref_builtin_args( - &["socket", "length", "flags", "address"], - &evaluated_args, - false, - )?; - let socket = required_evaluated_ref_arg(&bound, 0)?; - let length = required_evaluated_ref_arg(&bound, 1)?; - let address_target = optional_evaluated_ref_arg(&bound, 3) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - let (result, address) = - eval_stream_socket_recvfrom_with_address_result(socket.value, length.value, context, values)?; - eval_write_socket_output_ref_target(address_target.as_ref(), address, context, values)?; - Ok(result) -} - -/// Reads bytes from a connected socket stream. -pub(in crate::interpreter) fn eval_stream_socket_recvfrom_result( - stream: RuntimeCellHandle, - length: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_fread_result(stream, length, context, values) -} - -/// Reads bytes from a connected socket stream and returns the tracked remote endpoint name. -pub(in crate::interpreter) fn eval_stream_socket_recvfrom_with_address_result( - stream: RuntimeCellHandle, - length: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, Option), EvalStatus> { - let id = eval_socket_resource_id(stream, values)?; - let address = context.stream_resources().socket_name(id, true); - let result = eval_fread_result(stream, length, context, values)?; - Ok((result, address)) -} - -/// Writes a socket output string to a captured by-reference target when available. -fn eval_write_socket_output_ref_target( - target: Option<&EvalReferenceTarget>, - value: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some((target, value)) = target.zip(value) else { - return Ok(()); - }; - let value = values.string(&value)?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - ) -} - -/// Writes a socket output integer to a captured by-reference target when available. -fn eval_write_socket_int_output_ref_target( - target: Option<&EvalReferenceTarget>, - value: i64, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(target) = target else { - return Ok(()); - }; - let value = values.int(value)?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - ) -} - -/// Evaluates `stream_socket_pair(domain, type, protocol)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_pair( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [domain, socket_type, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let _ = eval_expr(domain, context, scope, values)?; - let _ = eval_expr(socket_type, context, scope, values)?; - let _ = eval_expr(protocol, context, scope, values)?; - eval_stream_socket_pair_result(context, values) -} - -/// Creates a pair of connected local stream resources. -pub(in crate::interpreter) fn eval_stream_socket_pair_result( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((left, right)) = context.stream_resources_mut().open_socket_pair() else { - return values.bool_value(false); - }; - let mut result = values.array_new(2)?; - let key = values.int(0)?; - let value = values.resource(left)?; - result = values.array_set(result, key, value)?; - let key = values.int(1)?; - let value = values.resource(right)?; - values.array_set(result, key, value) -} - -/// Evaluates `stream_select()` over full eval call metadata. -pub(in crate::interpreter) fn eval_builtin_stream_select_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let (bound, _) = bind_evaluated_ref_builtin_args( - &["read", "write", "except", "seconds", "microseconds"], - &evaluated_args, - false, - )?; - let read = required_evaluated_ref_arg(&bound, 0)?; - let write = required_evaluated_ref_arg(&bound, 1)?; - let except = required_evaluated_ref_arg(&bound, 2)?; - let seconds = required_evaluated_ref_arg(&bound, 3)?; - let targets = vec![ - read.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, - write.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, - except.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, - ]; - let mut selected_args = vec![read.value, write.value, except.value, seconds.value]; - if let Some(microseconds) = optional_evaluated_ref_arg(&bound, 4) { - selected_args.push(microseconds.value); - } - let result = eval_stream_select_result(&selected_args, context, values)?; - eval_write_stream_select_empty_arrays(&targets, context, values)?; - Ok(result) -} - -/// Evaluates materialized `stream_select(...)` arguments. -pub(in crate::interpreter) fn eval_stream_select_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(4..=5).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - for array in evaluated_args.iter().take(3) { - eval_stream_select_cast_array(*array, context, values)?; - } - values.int(0) -} - -/// Writes conservative empty readiness arrays back to `stream_select()` lvalues. -fn eval_write_stream_select_empty_arrays( - targets: &[EvalReferenceTarget], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for target in targets { - let value = values.array_new(0)?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - Ok(()) -} - -/// Invokes `stream_cast(STREAM_CAST_FOR_SELECT)` for wrapper resources in an array. -fn eval_stream_select_cast_array( - array: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if !values.is_array_like(array)? { - return Ok(()); - } - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - eval_stream_select_cast_value(value, context, values)?; - } - Ok(()) -} - -/// Invokes `stream_cast()` for one userspace-wrapper stream resource value. -fn eval_stream_select_cast_value( - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if values.type_tag(value)? != EVAL_TAG_RESOURCE { - return Ok(()); - } - let display_id = eval_int_value(value, values)?; - let Some(id) = display_id.checked_sub(1) else { - return Ok(()); - }; - let Some(result) = - eval_user_wrapper_stream_cast_result(id, EVAL_STREAM_CAST_FOR_SELECT, context, values)? - else { - return Ok(()); - }; - values.release(result) -} - -/// Converts a runtime resource cell into eval's zero-based socket id. -fn eval_socket_resource_id( - resource: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(resource)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(resource, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} +mod common; +mod datagrams; +mod lifecycle; +mod open; +mod selection; + +use common::*; +pub(in crate::interpreter) use datagrams::*; +pub(in crate::interpreter) use lifecycle::*; +pub(in crate::interpreter) use open::*; +pub(in crate::interpreter) use selection::*; +use selection::eval_socket_resource_id; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/common.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/common.rs new file mode 100644 index 0000000000..b3edc000f4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/common.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Shared by-reference writeback helpers for stream socket builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_sockets` submodules. +//! +//! Key details: +//! - Helpers write directly to captured eval reference targets and preserve owned +//! scope-cell replacement semantics. + +use super::*; + +/// Writes a socket output string to a captured by-reference target when available. +pub(super) fn eval_write_socket_output_ref_target( + target: Option<&EvalReferenceTarget>, + value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((target, value)) = target.zip(value) else { + return Ok(()); + }; + let value = values.string(&value)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} + +/// Writes a socket output integer to a captured by-reference target when available. +pub(super) fn eval_write_socket_int_output_ref_target( + target: Option<&EvalReferenceTarget>, + value: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(target) = target else { + return Ok(()); + }; + let value = values.int(value)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs new file mode 100644 index 0000000000..9471c298d3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Sends and receives data through eval stream socket resources. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_sockets` re-exports. +//! +//! Key details: +//! - `stream_socket_recvfrom()` preserves optional by-reference address writeback +//! for both direct and dynamic callable dispatch. + +use super::*; + +/// Evaluates `stream_socket_sendto(stream, data, flags = 0, address = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_sendto( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let data = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_sendto_result(stream, data, context, values) +} + +/// Writes bytes to a connected socket stream. +pub(in crate::interpreter) fn eval_stream_socket_sendto_result( + stream: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_fwrite_result(stream, data, context, values) +} + +/// Evaluates `stream_socket_recvfrom()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "length", "flags", "address"], + &evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let length = required_evaluated_ref_arg(&bound, 1)?; + let address_target = optional_evaluated_ref_arg(&bound, 3) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, address) = + eval_stream_socket_recvfrom_with_address_result(socket.value, length.value, context, values)?; + eval_write_socket_output_ref_target(address_target.as_ref(), address, context, values)?; + Ok(result) +} + +/// Reads bytes from a connected socket stream. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_fread_result(stream, length, context, values) +} + +/// Reads bytes from a connected socket stream and returns the tracked remote endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_with_address_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let id = eval_socket_resource_id(stream, values)?; + let address = context.stream_resources().socket_name(id, true); + let result = eval_fread_result(stream, length, context, values)?; + Ok((result, address)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/lifecycle.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/lifecycle.rs new file mode 100644 index 0000000000..5ad93c2898 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/lifecycle.rs @@ -0,0 +1,157 @@ +//! Purpose: +//! Accepts stream socket connections and exposes socket metadata/lifecycle calls. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_sockets` re-exports. +//! +//! Key details: +//! - TLS enablement is conservative: disabling succeeds for valid streams while +//! enabling reports false because eval does not manage TLS state. + +use super::*; + +/// Evaluates `stream_socket_accept()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_socket_accept_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "timeout", "peer_name"], + &evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let peer_name_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, peer_name) = + eval_stream_socket_accept_with_peer_result(socket.value, context, values)?; + eval_write_socket_output_ref_target(peer_name_target.as_ref(), peer_name, context, values)?; + Ok(result) +} + +/// Accepts one pending TCP connection from a listener resource. +pub(in crate::interpreter) fn eval_stream_socket_accept_result( + socket: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(socket, values)?; + match context.stream_resources_mut().accept_tcp(id) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Accepts one TCP connection and returns the accepted resource plus peer endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_accept_with_peer_result( + socket: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let id = eval_socket_resource_id(socket, values)?; + let Some(accepted_id) = context.stream_resources_mut().accept_tcp(id) else { + return values.bool_value(false).map(|result| (result, None)); + }; + let peer_name = context.stream_resources().socket_name(accepted_id, true); + let result = values.resource(accepted_id)?; + Ok((result, peer_name)) +} + +/// Evaluates `stream_socket_get_name(socket, remote)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_get_name( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [socket, remote] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let socket = eval_expr(socket, context, scope, values)?; + let remote = eval_expr(remote, context, scope, values)?; + eval_stream_socket_get_name_result(socket, remote, context, values) +} + +/// Returns a tracked local or remote socket endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_get_name_result( + socket: RuntimeCellHandle, + remote: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(socket, values)?; + let remote = values.truthy(remote)?; + match context.stream_resources().socket_name(id, remote) { + Some(name) => values.string(&name), + None => values.bool_value(false), + } +} + +/// Evaluates `stream_socket_shutdown(stream, mode)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_shutdown( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, mode] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_stream_socket_shutdown_result(stream, mode, context, values) +} + +/// Applies a socket shutdown mode to a stream resource. +pub(in crate::interpreter) fn eval_stream_socket_shutdown_result( + stream: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(stream, values)?; + let mode = eval_int_value(mode, values)?; + values.bool_value( + context + .stream_resources() + .socket_shutdown(id, mode) + .unwrap_or(false), + ) +} + +/// Evaluates `stream_socket_enable_crypto(...)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_enable_crypto( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let enable = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_enable_crypto_result(stream, enable, context, values) +} + +/// Returns TLS enablement status for eval socket streams. +pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_result( + stream: RuntimeCellHandle, + enable: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(stream, values)?; + if !context.stream_resources().has_stream(id) { + return values.bool_value(false); + } + let disabled = !values.truthy(enable)?; + values.bool_value(disabled) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/open.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/open.rs new file mode 100644 index 0000000000..aad812d0c3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/open.rs @@ -0,0 +1,142 @@ +//! Purpose: +//! Opens TCP stream socket resources for eval stream socket builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_sockets` re-exports. +//! +//! Key details: +//! - Opened sockets enter eval's normal stream table so existing stream IO +//! helpers own reads, writes, and close behavior. + +use super::*; + +/// Evaluates `stream_socket_server(address)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_server( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [address] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_expr(address, context, scope, values)?; + eval_stream_socket_server_result(address, context, values) +} + +/// Opens a TCP listener resource. +pub(in crate::interpreter) fn eval_stream_socket_server_result( + address: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_path_string(address, values)?; + match context.stream_resources_mut().open_tcp_listener(&address) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates `stream_socket_client(address)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_client( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [address] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_expr(address, context, scope, values)?; + eval_stream_socket_client_result(address, context, values) +} + +/// Opens a connected TCP stream resource. +pub(in crate::interpreter) fn eval_stream_socket_client_result( + address: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_path_string(address, values)?; + match context.stream_resources_mut().open_tcp_stream(&address) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Evaluates `fsockopen()` or `pfsockopen()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_fsockopen_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["hostname", "port", "error_code", "error_message", "timeout"], + &evaluated_args, + false, + )?; + let host = required_evaluated_ref_arg(&bound, 0)?; + let port = required_evaluated_ref_arg(&bound, 1)?; + let error_code_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let error_message_target = optional_evaluated_ref_arg(&bound, 3) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, error_code, error_message) = + eval_fsockopen_with_error_result(host.value, port.value, context, values)?; + eval_write_socket_int_output_ref_target( + error_code_target.as_ref(), + error_code, + context, + values, + )?; + eval_write_socket_output_ref_target( + error_message_target.as_ref(), + Some(error_message), + context, + values, + )?; + Ok(result) +} + +/// Opens a connected TCP stream from host and port cells. +pub(in crate::interpreter) fn eval_fsockopen_result( + host: RuntimeCellHandle, + port: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let host = eval_path_string(host, values)?; + let port = eval_int_value(port, values)?; + match context + .stream_resources_mut() + .open_tcp_stream_host_port(&host, port) + { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Opens a host/port TCP stream and returns PHP `fsockopen()` error outputs. +pub(in crate::interpreter) fn eval_fsockopen_with_error_result( + host: RuntimeCellHandle, + port: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, i64, String), EvalStatus> { + let host = eval_path_string(host, values)?; + let port = eval_int_value(port, values)?; + match context + .stream_resources_mut() + .open_tcp_stream_host_port_result(&host, port) + { + Ok(id) => Ok((values.resource(id)?, 0, String::new())), + Err(error) => { + let error_code = i64::from(error.raw_os_error().unwrap_or(0)); + Ok((values.bool_value(false)?, error_code, error.to_string())) + } + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/selection.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/selection.rs new file mode 100644 index 0000000000..9b06440848 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/selection.rs @@ -0,0 +1,160 @@ +//! Purpose: +//! Implements stream socket pairs and `stream_select()` over eval resources. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem::stream_sockets` re-exports. +//! +//! Key details: +//! - `stream_select()` rewrites read/write/except arrays through by-reference +//! targets after validating resource handles. + +use super::*; + +/// Evaluates `stream_socket_pair(domain, type, protocol)`. +pub(in crate::interpreter) fn eval_builtin_stream_socket_pair( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [domain, socket_type, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let _ = eval_expr(domain, context, scope, values)?; + let _ = eval_expr(socket_type, context, scope, values)?; + let _ = eval_expr(protocol, context, scope, values)?; + eval_stream_socket_pair_result(context, values) +} + +/// Creates a pair of connected local stream resources. +pub(in crate::interpreter) fn eval_stream_socket_pair_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((left, right)) = context.stream_resources_mut().open_socket_pair() else { + return values.bool_value(false); + }; + let mut result = values.array_new(2)?; + let key = values.int(0)?; + let value = values.resource(left)?; + result = values.array_set(result, key, value)?; + let key = values.int(1)?; + let value = values.resource(right)?; + values.array_set(result, key, value) +} + +/// Evaluates `stream_select()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_select_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["read", "write", "except", "seconds", "microseconds"], + &evaluated_args, + false, + )?; + let read = required_evaluated_ref_arg(&bound, 0)?; + let write = required_evaluated_ref_arg(&bound, 1)?; + let except = required_evaluated_ref_arg(&bound, 2)?; + let seconds = required_evaluated_ref_arg(&bound, 3)?; + let targets = vec![ + read.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + write.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + except.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + ]; + let mut selected_args = vec![read.value, write.value, except.value, seconds.value]; + if let Some(microseconds) = optional_evaluated_ref_arg(&bound, 4) { + selected_args.push(microseconds.value); + } + let result = eval_stream_select_result(&selected_args, context, values)?; + eval_write_stream_select_empty_arrays(&targets, context, values)?; + Ok(result) +} + +/// Evaluates materialized `stream_select(...)` arguments. +pub(in crate::interpreter) fn eval_stream_select_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(4..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + for array in evaluated_args.iter().take(3) { + eval_stream_select_cast_array(*array, context, values)?; + } + values.int(0) +} + +/// Writes conservative empty readiness arrays back to `stream_select()` lvalues. +fn eval_write_stream_select_empty_arrays( + targets: &[EvalReferenceTarget], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for target in targets { + let value = values.array_new(0)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(()) +} + +/// Invokes `stream_cast(STREAM_CAST_FOR_SELECT)` for wrapper resources in an array. +fn eval_stream_select_cast_array( + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.is_array_like(array)? { + return Ok(()); + } + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + eval_stream_select_cast_value(value, context, values)?; + } + Ok(()) +} + +/// Invokes `stream_cast()` for one userspace-wrapper stream resource value. +fn eval_stream_select_cast_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_RESOURCE { + return Ok(()); + } + let display_id = eval_int_value(value, values)?; + let Some(id) = display_id.checked_sub(1) else { + return Ok(()); + }; + let Some(result) = + eval_user_wrapper_stream_cast_result(id, EVAL_STREAM_CAST_FOR_SELECT, context, values)? + else { + return Ok(()); + }; + values.release(result) +} + +/// Converts a runtime resource cell into eval's zero-based socket id. +pub(super) fn eval_socket_resource_id( + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(resource, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 9ef7e92b59..384c553014 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -7,6 +7,9 @@ //! Key details: //! - Helpers are scoped to the eval interpreter and operate on already parsed //! EvalIR call metadata or evaluated runtime-cell handles. +//! - This file is a deliberate >500 LoC single-scope callable engine: splitting +//! normalization, ref-target preservation, and invocation would spread one PHP +//! callable state machine across artificial modules. use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs index 3a4bca6649..da80aa6505 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable_validation.rs @@ -8,6 +8,9 @@ //! Key details: //! - Direct callable invocation still uses normal method-call errors; these helpers //! are scoped to `call_user_func` and `call_user_func_array`. +//! - This file is a deliberate >500 LoC single-scope validation matrix for PHP's +//! callback TypeErrors; splitting it would duplicate method visibility and AOT +//! bridge checks across call_user_func surfaces. use super::super::super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs index f17750ca6c..94dde9d5cc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/dynamic_mutation.rs @@ -8,6 +8,9 @@ //! Key details: //! - This module only handles builtin calls whose direct PHP semantics can write //! to caller storage. Other builtins continue through the by-value dispatcher. +//! - This file is a deliberate >500 LoC single-scope by-reference dispatcher: +//! the shared concern is preserving captured writeback targets, not builtin +//! area ownership, so splitting by area would duplicate binding semantics. use super::super::super::*; use super::super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs index 19abf88485..e7979d4444 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/mod.rs @@ -188,1091 +188,4 @@ pub(in crate::interpreter) fn eval_declared_builtin_values_call( } #[cfg(test)] -mod tests { - use super::*; - - /// Verifies representative migrated builtins are present in the declarative registry. - #[test] - fn declared_builtin_registry_exposes_representative_migrated_builtins() { - for name in [ - "abs", - "acos", - "addslashes", - "array_chunk", - "array_column", - "array_combine", - "array_diff", - "array_diff_key", - "array_fill", - "array_fill_keys", - "array_filter", - "array_key_exists", - "array_keys", - "array_intersect", - "array_intersect_key", - "array_map", - "array_merge", - "array_pop", - "array_push", - "array_reduce", - "array_reverse", - "array_shift", - "array_splice", - "array_sum", - "array_unshift", - "array_walk", - "arsort", - "asort", - "basename", - "boolval", - "base64_encode", - "bin2hex", - "buffer_new", - "call_user_func", - "call_user_func_array", - "checkdate", - "chdir", - "chgrp", - "chmod", - "chown", - "class_alias", - "class_exists", - "closedir", - "clearstatcache", - "copy", - "count", - "ctype_alpha", - "date", - "date_default_timezone_get", - "date_default_timezone_set", - "define", - "defined", - "dirname", - "disk_free_space", - "disk_total_space", - "die", - "exec", - "exit", - "function_exists", - "explode", - "file", - "file_exists", - "file_get_contents", - "file_put_contents", - "fileatime", - "filectime", - "filegroup", - "fileinode", - "filemtime", - "fileowner", - "fileperms", - "filesize", - "filetype", - "fclose", - "fdatasync", - "feof", - "fflush", - "fgetc", - "fgets", - "floatval", - "fnmatch", - "fpassthru", - "fread", - "fseek", - "fstat", - "ftruncate", - "fsync", - "ftell", - "fwrite", - "getdate", - "get_class", - "getcwd", - "getenv", - "gethostbyaddr", - "gethostbyname", - "gethostname", - "getprotobyname", - "getprotobynumber", - "getservbyname", - "getservbyport", - "gettype", - "gmdate", - "gmmktime", - "glob", - "grapheme_strrev", - "gzcompress", - "gzdeflate", - "gzinflate", - "gzuncompress", - "hash", - "hash_algos", - "hash_copy", - "hash_equals", - "hash_file", - "hash_final", - "hash_hmac", - "hash_init", - "hash_update", - "header", - "hex2bin", - "htmlspecialchars", - "hrtime", - "http_response_code", - "implode", - "inet_ntop", - "inet_pton", - "intval", - "interface_exists", - "iterator_apply", - "iterator_count", - "iterator_to_array", - "is_array", - "is_bool", - "is_callable", - "is_dir", - "is_double", - "is_executable", - "is_file", - "is_finite", - "is_float", - "is_infinite", - "is_int", - "is_integer", - "is_iterable", - "is_link", - "is_long", - "is_nan", - "is_null", - "is_numeric", - "is_object", - "is_readable", - "is_real", - "is_resource", - "is_scalar", - "is_string", - "is_subclass_of", - "is_writable", - "is_writeable", - "ip2long", - "json_decode", - "json_encode", - "json_last_error", - "json_last_error_msg", - "json_validate", - "krsort", - "ksort", - "lchgrp", - "lchown", - "link", - "linkinfo", - "localtime", - "log", - "long2ip", - "lstat", - "microtime", - "md5", - "min", - "mkdir", - "mktime", - "mt_rand", - "natcasesort", - "natsort", - "nl2br", - "number_format", - "opendir", - "pathinfo", - "pclose", - "passthru", - "php_uname", - "phpversion", - "popen", - "print_r", - "printf", - "ptr_null", - "ptr_read16", - "ptr_write_string", - "preg_match", - "preg_match_all", - "preg_replace", - "preg_replace_callback", - "preg_split", - "putenv", - "rand", - "random_int", - "range", - "rawurlencode", - "readdir", - "readfile", - "readlink", - "realpath", - "realpath_cache_get", - "realpath_cache_size", - "rename", - "rewind", - "rewinddir", - "rmdir", - "scandir", - "sha1", - "shell_exec", - "settype", - "shuffle", - "sleep", - "sort", - "sprintf", - "spl_autoload", - "spl_object_id", - "sscanf", - "stat", - "stream_copy_to_stream", - "stream_get_contents", - "stream_get_filters", - "stream_get_line", - "stream_get_meta_data", - "stream_get_transports", - "stream_get_wrappers", - "stream_is_local", - "stream_isatty", - "stream_resolve_include_path", - "stream_set_blocking", - "stream_set_chunk_size", - "stream_set_read_buffer", - "stream_set_timeout", - "stream_set_write_buffer", - "stream_supports_lock", - "str_contains", - "str_pad", - "str_replace", - "strlen", - "str_repeat", - "strrev", - "strtotime", - "substr", - "system", - "symlink", - "sys_get_temp_dir", - "tempnam", - "time", - "tmpfile", - "touch", - "trait_exists", - "trim", - "strval", - "uasort", - "uksort", - "umask", - "unlink", - "unset", - "usleep", - "usort", - "var_dump", - "vprintf", - "vsprintf", - "wordwrap", - ] { - assert!( - eval_declared_builtin_exists(name), - "{name} should be registered declaratively" - ); - } - } - - /// Verifies migrated builtin metadata is derived from declarative specs. - #[test] - fn declared_builtin_registry_derives_migrated_metadata() { - assert_eq!( - eval_declared_builtin_param_names("count"), - Some(["value", "mode"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("count", 1), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_param_names("strlen"), - Some(["string"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("is_finite"), - Some(["num"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("is_object"), - Some(["value"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("log"), - Some(["num", "base"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("log", 1), - Some(EvalBuiltinDefaultValue::Float(std::f64::consts::E)) - ); - assert_eq!( - eval_declared_builtin_param_names("max"), - Some(["value", "values"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("array_map"), - Some(["callback", "array", "arrays"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("array_map").map(|shape| shape.variadic), - Some(Some("arrays")) - ); - assert_eq!( - eval_declared_builtin_param_names("array_filter"), - Some(["array", "callback", "mode"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("array_filter", 1), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_default_value("array_filter", 2), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_param_names("iterator_to_array"), - Some(["iterator", "preserve_keys"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("iterator_to_array", 1), - Some(EvalBuiltinDefaultValue::Bool(true)) - ); - assert_eq!( - eval_declared_builtin_spec("array_pop").map(EvalBuiltinSpec::by_ref_param_names), - Some(["array"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("array_push"), - Some(["array", "values"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("array_push").map(|shape| shape.variadic), - Some(Some("values")) - ); - assert_eq!( - eval_declared_builtin_default_value("array_splice", 2), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_default_value("array_splice", 3), - Some(EvalBuiltinDefaultValue::EmptyArray) - ); - assert_eq!( - eval_declared_builtin_param_names("settype"), - Some(["var", "type"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_spec("settype").map(EvalBuiltinSpec::by_ref_param_names), - Some(["var"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("print_r"), - Some(["value", "return"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("print_r", 1), - Some(EvalBuiltinDefaultValue::Bool(false)) - ); - assert_eq!( - eval_declared_builtin_param_names("var_dump"), - Some(["value", "values"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("var_dump").map(|shape| shape.variadic), - Some(Some("values")) - ); - assert_eq!( - eval_declared_builtin_param_names("class_exists"), - Some(["class", "autoload"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("class_exists", 1), - Some(EvalBuiltinDefaultValue::Bool(true)) - ); - assert_eq!( - eval_declared_builtin_param_names("get_class"), - Some(["object"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("get_class", 0), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_param_names("is_callable"), - Some(["value", "syntax_only", "callable_name"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_spec("is_callable").map(EvalBuiltinSpec::by_ref_param_names), - Some(["callable_name"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("is_callable", 1), - Some(EvalBuiltinDefaultValue::Bool(false)) - ); - assert_eq!( - eval_builtin_signature_shape("isset").map(|shape| shape.variadic), - Some(Some("vars")) - ); - assert_eq!( - eval_declared_builtin_param_names("spl_autoload_register"), - Some(["callback", "throw", "prepend"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("spl_autoload_register", 2), - Some(EvalBuiltinDefaultValue::Bool(false)) - ); - assert_eq!( - eval_declared_builtin_param_names("buffer_new"), - Some(["length"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("ptr_read_string"), - Some(["pointer", "length"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("ptr_sizeof"), - Some(["type"].as_slice()) - ); - for name in ["rand", "mt_rand", "random_int"] { - assert_eq!( - eval_declared_builtin_param_names(name), - Some(["min", "max"].as_slice()), - "{name} should declare min/max parameters" - ); - } - assert_eq!( - eval_declared_builtin_param_names("number_format"), - Some( - [ - "num", - "decimals", - "decimal_separator", - "thousands_separator", - ] - .as_slice() - ) - ); - assert_eq!( - eval_declared_builtin_param_names("ctype_alpha"), - Some(["text"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("str_repeat"), - Some(["string", "times"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("wordwrap"), - Some(["string", "width", "break", "cut_long_words"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("wordwrap", 2), - Some(EvalBuiltinDefaultValue::String("\n")) - ); - assert_eq!( - eval_declared_builtin_param_names("json_decode"), - Some(["json", "associative", "depth", "flags"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("json_decode", 1), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_default_value("json_encode", 2), - Some(EvalBuiltinDefaultValue::Int(512)) - ); - assert_eq!( - eval_declared_builtin_param_names("json_last_error"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("date"), - Some(["format", "timestamp"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("date", 1), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_param_names("date_default_timezone_get"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("header"), - Some(["header", "replace", "response_code"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("header", 1), - Some(EvalBuiltinDefaultValue::Bool(true)) - ); - assert_eq!( - eval_declared_builtin_default_value("header", 2), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_param_names("http_response_code"), - Some(["response_code"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("http_response_code", 0), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_param_names("php_uname"), - Some(["mode"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("php_uname", 0), - Some(EvalBuiltinDefaultValue::String("a")) - ); - assert_eq!( - eval_declared_builtin_param_names("phpversion"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("getenv"), - Some(["name"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("getservbyname"), - Some(["service", "protocol"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("call_user_func"), - Some(["callback", "args"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("exit"), - Some(["status"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("exit", 0), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_param_names("getdate"), - Some(["timestamp"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("hrtime"), - Some(["as_number"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("hrtime", 0), - Some(EvalBuiltinDefaultValue::Bool(false)) - ); - assert_eq!( - eval_declared_builtin_default_value("localtime", 0), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_default_value("localtime", 1), - Some(EvalBuiltinDefaultValue::Bool(false)) - ); - assert_eq!( - eval_declared_builtin_param_names("microtime"), - Some(["as_float"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("strtotime"), - Some(["datetime", "baseTimestamp"].as_slice()) - ); - assert_eq!(eval_declared_builtin_param_names("time"), Some([].as_slice())); - assert_eq!( - eval_declared_builtin_param_names("preg_match"), - Some(["pattern", "subject", "matches", "flags"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("preg_match", 2), - Some(EvalBuiltinDefaultValue::EmptyArray) - ); - assert_eq!( - eval_declared_builtin_default_value("preg_match_all", 3), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_builtin_signature_shape("preg_match").map(|shape| shape.by_ref_params), - Some(["matches"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("preg_replace_callback"), - Some(["pattern", "callback", "subject"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("preg_split", 2), - Some(EvalBuiltinDefaultValue::Int(-1)) - ); - assert_eq!( - eval_declared_builtin_param_names("basename"), - Some(["path", "suffix"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("basename", 1), - Some(EvalBuiltinDefaultValue::String("")) - ); - assert_eq!( - eval_declared_builtin_default_value("dirname", 1), - Some(EvalBuiltinDefaultValue::Int(1)) - ); - assert_eq!( - eval_declared_builtin_default_value("fnmatch", 2), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_default_value("pathinfo", 1), - Some(EvalBuiltinDefaultValue::Int(15)) - ); - assert_eq!( - eval_declared_builtin_param_names("disk_free_space"), - Some(["directory"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("getcwd"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("glob"), - Some(["pattern"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("linkinfo"), - Some(["path"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("realpath"), - Some(["path"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_resolve_include_path"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("realpath_cache_get"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("sys_get_temp_dir"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("file_exists"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("file"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("file_get_contents"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("file_put_contents"), - Some(["filename", "data"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("readfile"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("filemtime"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("filesize"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("is_writable"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stat"), - Some(["filename"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("chdir"), - Some(["directory"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("chmod"), - Some(["filename", "permissions"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("chown"), - Some(["filename", "user"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("chgrp"), - Some(["filename", "group"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("clearstatcache", 0), - Some(EvalBuiltinDefaultValue::Bool(false)) - ); - assert_eq!( - eval_declared_builtin_default_value("clearstatcache", 1), - Some(EvalBuiltinDefaultValue::String("")) - ); - assert_eq!( - eval_declared_builtin_param_names("link"), - Some(["target", "link"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("rename"), - Some(["from", "to"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("scandir"), - Some(["directory"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("tempnam"), - Some(["directory", "prefix"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("popen"), - Some(["command", "mode"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("pclose"), - Some(["handle"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("opendir"), - Some(["directory"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("closedir"), - Some(["dir_handle"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("readdir"), - Some(["dir_handle"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("rewinddir"), - Some(["dir_handle"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("tmpfile"), - Some([].as_slice()) - ); - for name in [ - "fclose", - "fgetc", - "fgets", - "feof", - "fflush", - "fpassthru", - "fsync", - "fdatasync", - "ftell", - "rewind", - "fstat", - "stream_get_meta_data", - ] { - assert_eq!( - eval_declared_builtin_param_names(name), - Some(["stream"].as_slice()), - "{name} should declare one stream parameter" - ); - } - assert_eq!( - eval_declared_builtin_param_names("fread"), - Some(["stream", "length"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("fgetcsv"), - Some(["stream", "length", "separator"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("fgetcsv", 2), - Some(EvalBuiltinDefaultValue::String(",")) - ); - assert_eq!( - eval_declared_builtin_param_names("flock"), - Some(["stream", "operation", "would_block"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("flock").map(|shape| shape.by_ref_params), - Some(["would_block"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("fsockopen"), - Some(["hostname", "port", "error_code", "error_message", "timeout"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("fsockopen").map(|shape| shape.by_ref_params), - Some(["error_code", "error_message"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("fwrite"), - Some(["stream", "data"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("fseek"), - Some(["stream", "offset", "whence"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("fseek", 2), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_param_names("ftruncate"), - Some(["stream", "size"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_copy_to_stream"), - Some(["from", "to", "length", "offset"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("stream_copy_to_stream", 2), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_default_value("stream_copy_to_stream", 3), - Some(EvalBuiltinDefaultValue::Int(-1)) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_get_contents"), - Some(["stream", "length", "offset"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("stream_get_contents", 1), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_default_value("stream_get_contents", 2), - Some(EvalBuiltinDefaultValue::Int(-1)) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_context_set_option"), - Some(["context", "wrapper_or_options", "option_name", "value"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("stream_context_set_option") - .map(|shape| shape.required_param_count), - Some(2) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_get_line"), - Some(["stream", "length", "ending"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("stream_get_line", 2), - Some(EvalBuiltinDefaultValue::String("")) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_select"), - Some(["read", "write", "except", "seconds", "microseconds"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("stream_select").map(|shape| shape.by_ref_params), - Some(["read", "write", "except"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_socket_recvfrom"), - Some(["socket", "length", "flags", "address"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("stream_socket_recvfrom") - .map(|shape| shape.by_ref_params), - Some(["address"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_wrapper_register"), - Some(["protocol", "class", "flags"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("vfprintf"), - Some(["stream", "format", "values"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_get_wrappers"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_get_transports"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_get_filters"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("explode"), - Some(["separator", "string", "limit"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("explode", 2), - Some(EvalBuiltinDefaultValue::Int(i64::MAX)) - ); - assert_eq!( - eval_declared_builtin_param_names("implode"), - Some(["separator", "array"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("implode", 0), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_builtin_signature_shape("implode").map(|shape| shape.required_param_count), - Some(1) - ); - assert_eq!( - eval_declared_builtin_param_names("sprintf"), - Some(["format", "values"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("sprintf").map(|shape| shape.variadic), - Some(Some("values")) - ); - assert_eq!( - eval_declared_builtin_param_names("sscanf"), - Some(["string", "format", "vars"].as_slice()) - ); - assert_eq!( - eval_builtin_signature_shape("sscanf").map(|shape| shape.variadic), - Some(Some("vars")) - ); - assert_eq!( - eval_declared_builtin_param_names("vsprintf"), - Some(["format", "values"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("gzcompress"), - Some(["data", "level"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("gzcompress", 1), - Some(EvalBuiltinDefaultValue::Int(-1)) - ); - assert_eq!( - eval_declared_builtin_default_value("gzinflate", 1), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_param_names("hash"), - Some(["algo", "data", "binary"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("hash", 2), - Some(EvalBuiltinDefaultValue::Bool(false)) - ); - assert_eq!( - eval_declared_builtin_param_names("hash_algos"), - Some([].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("hash_init"), - Some(["algo", "flags", "key"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("hash_init", 1), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_default_value("hash_init", 2), - Some(EvalBuiltinDefaultValue::String("")) - ); - assert_eq!( - eval_declared_builtin_param_names("md5"), - Some(["string", "binary"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("sha1", 1), - Some(EvalBuiltinDefaultValue::Bool(false)) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_is_local"), - Some(["stream"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_supports_lock"), - Some(["stream"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_isatty"), - Some(["stream"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_set_blocking"), - Some(["stream", "enable"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_set_chunk_size"), - Some(["stream", "size"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_set_read_buffer"), - Some(["stream", "size"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_set_write_buffer"), - Some(["stream", "size"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_param_names("stream_set_timeout"), - Some(["stream", "seconds", "microseconds"].as_slice()) - ); - assert_eq!( - eval_declared_builtin_default_value("stream_set_timeout", 2), - Some(EvalBuiltinDefaultValue::Int(0)) - ); - assert_eq!( - eval_declared_builtin_default_value("touch", 1), - Some(EvalBuiltinDefaultValue::Null) - ); - assert_eq!( - eval_declared_builtin_default_value("umask", 0), - Some(EvalBuiltinDefaultValue::Null) - ); - } - - /// Verifies direct-call fallback is needed only for source-sensitive pre-dispatched builtins. - #[test] - fn declared_builtin_registry_marks_only_pre_dispatched_builtins_without_direct_hooks() { - let mut without_direct: Vec<&str> = eval_declared_builtin_function_names() - .iter() - .copied() - .filter(|name| { - eval_declared_builtin_spec(name) - .is_some_and(|spec| spec.direct.is_none()) - }) - .collect(); - without_direct.sort_unstable(); - - assert_eq!( - without_direct, - [ - "array_pop", - "array_push", - "array_shift", - "array_splice", - "array_unshift", - "array_walk", - "arsort", - "asort", - "flock", - "fsockopen", - "krsort", - "ksort", - "natcasesort", - "natsort", - "pfsockopen", - "rsort", - "settype", - "shuffle", - "sort", - "stream_select", - "stream_socket_accept", - "stream_socket_recvfrom", - "uasort", - "uksort", - "usort", - ] - ); - } -} +mod tests; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs new file mode 100644 index 0000000000..832db3c675 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests.rs @@ -0,0 +1,20 @@ +//! Purpose: +//! Test module wiring for eval builtin registry discovery and metadata checks. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Focused child modules keep large registry assertions near their area while +//! still sharing access to private registry helpers. + +mod direct_hooks; +mod exposure; +mod metadata_core; +mod metadata_filesystem; +mod metadata_misc; +mod metadata_regex; +mod metadata_streams; +mod metadata_time_and_env; + +use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs new file mode 100644 index 0000000000..fe28c54bc5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/direct_hooks.rs @@ -0,0 +1,55 @@ +//! Purpose: +//! Registry direct-hook invariant tests for source-sensitive builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies direct-call fallback is needed only for source-sensitive pre-dispatched builtins. + #[test] + fn declared_builtin_registry_marks_only_pre_dispatched_builtins_without_direct_hooks() { + let mut without_direct: Vec<&str> = eval_declared_builtin_function_names() + .iter() + .copied() + .filter(|name| { + eval_declared_builtin_spec(name) + .is_some_and(|spec| spec.direct.is_none()) + }) + .collect(); + without_direct.sort_unstable(); + + assert_eq!( + without_direct, + [ + "array_pop", + "array_push", + "array_shift", + "array_splice", + "array_unshift", + "array_walk", + "arsort", + "asort", + "flock", + "fsockopen", + "krsort", + "ksort", + "natcasesort", + "natsort", + "pfsockopen", + "rsort", + "settype", + "shuffle", + "sort", + "stream_select", + "stream_socket_accept", + "stream_socket_recvfrom", + "uasort", + "uksort", + "usort", + ] + ); + } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/exposure.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/exposure.rs new file mode 100644 index 0000000000..bdb1b9d621 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/exposure.rs @@ -0,0 +1,295 @@ +//! Purpose: +//! Registry exposure tests for representative migrated eval builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies representative migrated builtins are present in the declarative registry. + #[test] + fn declared_builtin_registry_exposes_representative_migrated_builtins() { + for name in [ + "abs", + "acos", + "addslashes", + "array_chunk", + "array_column", + "array_combine", + "array_diff", + "array_diff_key", + "array_fill", + "array_fill_keys", + "array_filter", + "array_key_exists", + "array_keys", + "array_intersect", + "array_intersect_key", + "array_map", + "array_merge", + "array_pop", + "array_push", + "array_reduce", + "array_reverse", + "array_shift", + "array_splice", + "array_sum", + "array_unshift", + "array_walk", + "arsort", + "asort", + "basename", + "boolval", + "base64_encode", + "bin2hex", + "buffer_new", + "call_user_func", + "call_user_func_array", + "checkdate", + "chdir", + "chgrp", + "chmod", + "chown", + "class_alias", + "class_exists", + "closedir", + "clearstatcache", + "copy", + "count", + "ctype_alpha", + "date", + "date_default_timezone_get", + "date_default_timezone_set", + "define", + "defined", + "dirname", + "disk_free_space", + "disk_total_space", + "die", + "exec", + "exit", + "function_exists", + "explode", + "file", + "file_exists", + "file_get_contents", + "file_put_contents", + "fileatime", + "filectime", + "filegroup", + "fileinode", + "filemtime", + "fileowner", + "fileperms", + "filesize", + "filetype", + "fclose", + "fdatasync", + "feof", + "fflush", + "fgetc", + "fgets", + "floatval", + "fnmatch", + "fpassthru", + "fread", + "fseek", + "fstat", + "ftruncate", + "fsync", + "ftell", + "fwrite", + "getdate", + "get_class", + "getcwd", + "getenv", + "gethostbyaddr", + "gethostbyname", + "gethostname", + "getprotobyname", + "getprotobynumber", + "getservbyname", + "getservbyport", + "gettype", + "gmdate", + "gmmktime", + "glob", + "grapheme_strrev", + "gzcompress", + "gzdeflate", + "gzinflate", + "gzuncompress", + "hash", + "hash_algos", + "hash_copy", + "hash_equals", + "hash_file", + "hash_final", + "hash_hmac", + "hash_init", + "hash_update", + "header", + "hex2bin", + "htmlspecialchars", + "hrtime", + "http_response_code", + "implode", + "inet_ntop", + "inet_pton", + "intval", + "interface_exists", + "iterator_apply", + "iterator_count", + "iterator_to_array", + "is_array", + "is_bool", + "is_callable", + "is_dir", + "is_double", + "is_executable", + "is_file", + "is_finite", + "is_float", + "is_infinite", + "is_int", + "is_integer", + "is_iterable", + "is_link", + "is_long", + "is_nan", + "is_null", + "is_numeric", + "is_object", + "is_readable", + "is_real", + "is_resource", + "is_scalar", + "is_string", + "is_subclass_of", + "is_writable", + "is_writeable", + "ip2long", + "json_decode", + "json_encode", + "json_last_error", + "json_last_error_msg", + "json_validate", + "krsort", + "ksort", + "lchgrp", + "lchown", + "link", + "linkinfo", + "localtime", + "log", + "long2ip", + "lstat", + "microtime", + "md5", + "min", + "mkdir", + "mktime", + "mt_rand", + "natcasesort", + "natsort", + "nl2br", + "number_format", + "opendir", + "pathinfo", + "pclose", + "passthru", + "php_uname", + "phpversion", + "popen", + "print_r", + "printf", + "ptr_null", + "ptr_read16", + "ptr_write_string", + "preg_match", + "preg_match_all", + "preg_replace", + "preg_replace_callback", + "preg_split", + "putenv", + "rand", + "random_int", + "range", + "rawurlencode", + "readdir", + "readfile", + "readlink", + "realpath", + "realpath_cache_get", + "realpath_cache_size", + "rename", + "rewind", + "rewinddir", + "rmdir", + "scandir", + "sha1", + "shell_exec", + "settype", + "shuffle", + "sleep", + "sort", + "sprintf", + "spl_autoload", + "spl_object_id", + "sscanf", + "stat", + "stream_copy_to_stream", + "stream_get_contents", + "stream_get_filters", + "stream_get_line", + "stream_get_meta_data", + "stream_get_transports", + "stream_get_wrappers", + "stream_is_local", + "stream_isatty", + "stream_resolve_include_path", + "stream_set_blocking", + "stream_set_chunk_size", + "stream_set_read_buffer", + "stream_set_timeout", + "stream_set_write_buffer", + "stream_supports_lock", + "str_contains", + "str_pad", + "str_replace", + "strlen", + "str_repeat", + "strrev", + "strtotime", + "substr", + "system", + "symlink", + "sys_get_temp_dir", + "tempnam", + "time", + "tmpfile", + "touch", + "trait_exists", + "trim", + "strval", + "uasort", + "uksort", + "umask", + "unlink", + "unset", + "usleep", + "usort", + "var_dump", + "vprintf", + "vsprintf", + "wordwrap", + ] { + assert!( + eval_declared_builtin_exists(name), + "{name} should be registered declaratively" + ); + } + } diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs new file mode 100644 index 0000000000..3b2da14758 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_core.rs @@ -0,0 +1,172 @@ +//! Purpose: +//! Registry metadata tests for core, arrays, class, callable, and raw-memory builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_core_metadata() { + assert_eq!( + eval_declared_builtin_param_names("count"), + Some(["value", "mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("count", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("strlen"), + Some(["string"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("is_finite"), + Some(["num"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("is_object"), + Some(["value"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("log"), + Some(["num", "base"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("log", 1), + Some(EvalBuiltinDefaultValue::Float(std::f64::consts::E)) + ); + assert_eq!( + eval_declared_builtin_param_names("max"), + Some(["value", "values"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("array_map"), + Some(["callback", "array", "arrays"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("array_map").map(|shape| shape.variadic), + Some(Some("arrays")) + ); + assert_eq!( + eval_declared_builtin_param_names("array_filter"), + Some(["array", "callback", "mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("array_filter", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("array_filter", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("iterator_to_array"), + Some(["iterator", "preserve_keys"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("iterator_to_array", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); + assert_eq!( + eval_declared_builtin_spec("array_pop").map(EvalBuiltinSpec::by_ref_param_names), + Some(["array"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("array_push"), + Some(["array", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("array_push").map(|shape| shape.variadic), + Some(Some("values")) + ); + assert_eq!( + eval_declared_builtin_default_value("array_splice", 2), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("array_splice", 3), + Some(EvalBuiltinDefaultValue::EmptyArray) + ); + assert_eq!( + eval_declared_builtin_param_names("settype"), + Some(["var", "type"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_spec("settype").map(EvalBuiltinSpec::by_ref_param_names), + Some(["var"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("print_r"), + Some(["value", "return"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("print_r", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("var_dump"), + Some(["value", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("var_dump").map(|shape| shape.variadic), + Some(Some("values")) + ); + assert_eq!( + eval_declared_builtin_param_names("class_exists"), + Some(["class", "autoload"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("class_exists", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); + assert_eq!( + eval_declared_builtin_param_names("get_class"), + Some(["object"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("get_class", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_param_names("is_callable"), + Some(["value", "syntax_only", "callable_name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_spec("is_callable").map(EvalBuiltinSpec::by_ref_param_names), + Some(["callable_name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("is_callable", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_builtin_signature_shape("isset").map(|shape| shape.variadic), + Some(Some("vars")) + ); + assert_eq!( + eval_declared_builtin_param_names("spl_autoload_register"), + Some(["callback", "throw", "prepend"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("spl_autoload_register", 2), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("buffer_new"), + Some(["length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("ptr_read_string"), + Some(["pointer", "length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("ptr_sizeof"), + Some(["type"].as_slice()) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs new file mode 100644 index 0000000000..0d5073d556 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_filesystem.rs @@ -0,0 +1,147 @@ +//! Purpose: +//! Registry metadata tests for filesystem path, file, and directory builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_filesystem_metadata() { assert_eq!( + eval_declared_builtin_param_names("basename"), + Some(["path", "suffix"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("basename", 1), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_default_value("dirname", 1), + Some(EvalBuiltinDefaultValue::Int(1)) + ); + assert_eq!( + eval_declared_builtin_default_value("fnmatch", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_default_value("pathinfo", 1), + Some(EvalBuiltinDefaultValue::Int(15)) + ); + assert_eq!( + eval_declared_builtin_param_names("disk_free_space"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getcwd"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("glob"), + Some(["pattern"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("linkinfo"), + Some(["path"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("realpath"), + Some(["path"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_resolve_include_path"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("realpath_cache_get"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("sys_get_temp_dir"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file_exists"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file_get_contents"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("file_put_contents"), + Some(["filename", "data"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("readfile"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("filemtime"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("filesize"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("is_writable"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stat"), + Some(["filename"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chdir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chmod"), + Some(["filename", "permissions"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chown"), + Some(["filename", "user"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("chgrp"), + Some(["filename", "group"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("clearstatcache", 0), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_default_value("clearstatcache", 1), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_param_names("link"), + Some(["target", "link"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("rename"), + Some(["from", "to"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("scandir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("tempnam"), + Some(["directory", "prefix"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("popen"), + Some(["command", "mode"].as_slice()) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_misc.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_misc.rs new file mode 100644 index 0000000000..ebd6c9dd0e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_misc.rs @@ -0,0 +1,143 @@ +//! Purpose: +//! Registry metadata tests for string formatting, compression, hash, and stream-setting builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_misc_metadata() { assert_eq!( + eval_declared_builtin_param_names("explode"), + Some(["separator", "string", "limit"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("explode", 2), + Some(EvalBuiltinDefaultValue::Int(i64::MAX)) + ); + assert_eq!( + eval_declared_builtin_param_names("implode"), + Some(["separator", "array"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("implode", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_builtin_signature_shape("implode").map(|shape| shape.required_param_count), + Some(1) + ); + assert_eq!( + eval_declared_builtin_param_names("sprintf"), + Some(["format", "values"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("sprintf").map(|shape| shape.variadic), + Some(Some("values")) + ); + assert_eq!( + eval_declared_builtin_param_names("sscanf"), + Some(["string", "format", "vars"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("sscanf").map(|shape| shape.variadic), + Some(Some("vars")) + ); + assert_eq!( + eval_declared_builtin_param_names("vsprintf"), + Some(["format", "values"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("gzcompress"), + Some(["data", "level"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("gzcompress", 1), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_default_value("gzinflate", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("hash"), + Some(["algo", "data", "binary"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hash", 2), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("hash_algos"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("hash_init"), + Some(["algo", "flags", "key"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hash_init", 1), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_default_value("hash_init", 2), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_param_names("md5"), + Some(["string", "binary"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("sha1", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_is_local"), + Some(["stream"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_supports_lock"), + Some(["stream"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_isatty"), + Some(["stream"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_blocking"), + Some(["stream", "enable"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_chunk_size"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_read_buffer"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_write_buffer"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_set_timeout"), + Some(["stream", "seconds", "microseconds"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_set_timeout", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_default_value("touch", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("umask", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_regex.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_regex.rs new file mode 100644 index 0000000000..6ded1e0b56 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_regex.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Registry metadata tests for regex builtin signatures and defaults. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_regex_metadata() { assert_eq!( + eval_declared_builtin_param_names("preg_match"), + Some(["pattern", "subject", "matches", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_match", 2), + Some(EvalBuiltinDefaultValue::EmptyArray) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_match_all", 3), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_builtin_signature_shape("preg_match").map(|shape| shape.by_ref_params), + Some(["matches"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("preg_replace_callback"), + Some(["pattern", "callback", "subject"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("preg_split", 2), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_streams.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_streams.rs new file mode 100644 index 0000000000..d930852ecb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_streams.rs @@ -0,0 +1,181 @@ +//! Purpose: +//! Registry metadata tests for stream and stream-socket builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_stream_metadata() { assert_eq!( + eval_declared_builtin_param_names("pclose"), + Some(["handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("opendir"), + Some(["directory"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("closedir"), + Some(["dir_handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("readdir"), + Some(["dir_handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("rewinddir"), + Some(["dir_handle"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("tmpfile"), + Some([].as_slice()) + ); + for name in [ + "fclose", + "fgetc", + "fgets", + "feof", + "fflush", + "fpassthru", + "fsync", + "fdatasync", + "ftell", + "rewind", + "fstat", + "stream_get_meta_data", + ] { + assert_eq!( + eval_declared_builtin_param_names(name), + Some(["stream"].as_slice()), + "{name} should declare one stream parameter" + ); + } + assert_eq!( + eval_declared_builtin_param_names("fread"), + Some(["stream", "length"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fgetcsv"), + Some(["stream", "length", "separator"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("fgetcsv", 2), + Some(EvalBuiltinDefaultValue::String(",")) + ); + assert_eq!( + eval_declared_builtin_param_names("flock"), + Some(["stream", "operation", "would_block"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("flock").map(|shape| shape.by_ref_params), + Some(["would_block"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fsockopen"), + Some(["hostname", "port", "error_code", "error_message", "timeout"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("fsockopen").map(|shape| shape.by_ref_params), + Some(["error_code", "error_message"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fwrite"), + Some(["stream", "data"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("fseek"), + Some(["stream", "offset", "whence"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("fseek", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("ftruncate"), + Some(["stream", "size"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_copy_to_stream"), + Some(["from", "to", "length", "offset"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_copy_to_stream", 2), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_copy_to_stream", 3), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_contents"), + Some(["stream", "length", "offset"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_contents", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_contents", 2), + Some(EvalBuiltinDefaultValue::Int(-1)) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_context_set_option"), + Some(["context", "wrapper_or_options", "option_name", "value"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_context_set_option") + .map(|shape| shape.required_param_count), + Some(2) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_line"), + Some(["stream", "length", "ending"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("stream_get_line", 2), + Some(EvalBuiltinDefaultValue::String("")) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_select"), + Some(["read", "write", "except", "seconds", "microseconds"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_select").map(|shape| shape.by_ref_params), + Some(["read", "write", "except"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_socket_recvfrom"), + Some(["socket", "length", "flags", "address"].as_slice()) + ); + assert_eq!( + eval_builtin_signature_shape("stream_socket_recvfrom") + .map(|shape| shape.by_ref_params), + Some(["address"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_wrapper_register"), + Some(["protocol", "class", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("vfprintf"), + Some(["stream", "format", "values"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_wrappers"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_transports"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("stream_get_filters"), + Some([].as_slice()) + ); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_time_and_env.rs b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_time_and_env.rs new file mode 100644 index 0000000000..1d4ed6c9d1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/tests/metadata_time_and_env.rs @@ -0,0 +1,159 @@ +//! Purpose: +//! Registry metadata tests for random, scalar formatting, JSON, environment, and time builtins. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Assertions use registry metadata APIs rather than dispatcher literals. + +use super::*; + +/// Verifies migrated builtin metadata for this registry area. +#[test] +fn declared_builtin_registry_derives_time_and_env_metadata() { for name in ["rand", "mt_rand", "random_int"] { + assert_eq!( + eval_declared_builtin_param_names(name), + Some(["min", "max"].as_slice()), + "{name} should declare min/max parameters" + ); + } + assert_eq!( + eval_declared_builtin_param_names("number_format"), + Some( + [ + "num", + "decimals", + "decimal_separator", + "thousands_separator", + ] + .as_slice() + ) + ); + assert_eq!( + eval_declared_builtin_param_names("ctype_alpha"), + Some(["text"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("str_repeat"), + Some(["string", "times"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("wordwrap"), + Some(["string", "width", "break", "cut_long_words"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("wordwrap", 2), + Some(EvalBuiltinDefaultValue::String("\n")) + ); + assert_eq!( + eval_declared_builtin_param_names("json_decode"), + Some(["json", "associative", "depth", "flags"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("json_decode", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("json_encode", 2), + Some(EvalBuiltinDefaultValue::Int(512)) + ); + assert_eq!( + eval_declared_builtin_param_names("json_last_error"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("date"), + Some(["format", "timestamp"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("date", 1), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_param_names("date_default_timezone_get"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("header"), + Some(["header", "replace", "response_code"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("header", 1), + Some(EvalBuiltinDefaultValue::Bool(true)) + ); + assert_eq!( + eval_declared_builtin_default_value("header", 2), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("http_response_code"), + Some(["response_code"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("http_response_code", 0), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("php_uname"), + Some(["mode"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("php_uname", 0), + Some(EvalBuiltinDefaultValue::String("a")) + ); + assert_eq!( + eval_declared_builtin_param_names("phpversion"), + Some([].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getenv"), + Some(["name"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("getservbyname"), + Some(["service", "protocol"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("call_user_func"), + Some(["callback", "args"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("exit"), + Some(["status"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("exit", 0), + Some(EvalBuiltinDefaultValue::Int(0)) + ); + assert_eq!( + eval_declared_builtin_param_names("getdate"), + Some(["timestamp"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("hrtime"), + Some(["as_number"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_default_value("hrtime", 0), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_default_value("localtime", 0), + Some(EvalBuiltinDefaultValue::Null) + ); + assert_eq!( + eval_declared_builtin_default_value("localtime", 1), + Some(EvalBuiltinDefaultValue::Bool(false)) + ); + assert_eq!( + eval_declared_builtin_param_names("microtime"), + Some(["as_float"].as_slice()) + ); + assert_eq!( + eval_declared_builtin_param_names("strtotime"), + Some(["datetime", "baseTimestamp"].as_slice()) + ); + assert_eq!(eval_declared_builtin_param_names("time"), Some([].as_slice())); + +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 04798dbd4e..11061da876 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -1,1546 +1,30 @@ //! Purpose: -//! Symbol, constant, class, and language-construct builtin probes. +//! Orchestrates symbol, constant, class, and language-construct eval builtins. //! //! Called from: //! - `crate::interpreter::builtins` re-exports used by core call dispatch. //! //! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime -//! behavior through `RuntimeValueOps`. +//! - Concrete builtin behavior lives in focused `symbols/` modules so each +//! source file stays cohesive and below the ordinary 500 LoC guideline. use super::super::*; +mod callable_probe; +mod class_names; +mod class_relations; +mod constants; mod declarations; +mod function_probe; +mod language_constructs; +pub(in crate::interpreter) use callable_probe::*; +pub(in crate::interpreter) use class_names::*; +pub(in crate::interpreter) use class_relations::*; +pub(in crate::interpreter) use constants::*; pub(in crate::interpreter) use declarations::{ eval_builtin_symbols_call, eval_symbols_values_result, }; +pub(in crate::interpreter) use function_probe::*; +pub(in crate::interpreter) use language_constructs::*; use super::*; - -const EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS: &[&str] = &[ - "cal_days_in_month", - "cal_from_jd", - "cal_info", - "cal_to_jd", - "date_add", - "date_create", - "date_create_from_format", - "date_create_immutable", - "date_create_immutable_from_format", - "date_date_set", - "date_diff", - "date_format", - "date_get_last_errors", - "date_interval_create_from_date_string", - "date_interval_format", - "date_isodate_set", - "date_modify", - "date_offset_get", - "date_parse", - "date_parse_from_format", - "date_sub", - "date_sun_info", - "date_sunrise", - "date_sunset", - "date_time_set", - "date_timestamp_get", - "date_timestamp_set", - "date_timezone_get", - "date_timezone_set", - "easter_date", - "easter_days", - "frenchtojd", - "gettimeofday", - "gmstrftime", - "gregoriantojd", - "idate", - "jddayofweek", - "jdmonthname", - "jdtofrench", - "jdtogregorian", - "jdtojewish", - "jdtojulian", - "jdtounix", - "jewishtojd", - "juliantojd", - "mktime", - "gmmktime", - "strftime", - "strptime", - "timezone_abbreviations_list", - "timezone_identifiers_list", - "timezone_location_get", - "timezone_name_from_abbr", - "timezone_name_get", - "timezone_offset_get", - "timezone_open", - "timezone_transitions_get", - "timezone_version_get", - "unixtojd", -]; - -/// Evaluates `function_exists()` and `is_callable()` inside an eval fragment. -pub(in crate::interpreter) fn eval_builtin_function_probe( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "function_exists" => { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_function_probe_result(name, value, context, values) - } - "is_callable" => eval_builtin_is_callable(args, context, scope, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates `is_callable()` over full eval call metadata so `$callable_name` stays writable. -pub(in crate::interpreter) fn eval_builtin_is_callable_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - eval_is_callable_call_with_evaluated_args_from_scope( - &evaluated_args, - Some(scope), - context, - values, - ) -} - -/// Evaluates `is_callable()` from already evaluated arguments that may retain ref targets. -pub(in crate::interpreter) fn eval_is_callable_call_with_evaluated_args( - evaluated_args: &[EvaluatedCallArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_is_callable_call_with_evaluated_args_from_scope(evaluated_args, None, context, values) -} - -/// Evaluates materialized `is_callable()` args with optional special-class callable scope. -fn eval_is_callable_call_with_evaluated_args_from_scope( - evaluated_args: &[EvaluatedCallArg], - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (bound, _) = bind_evaluated_ref_builtin_args( - &["value", "syntax_only", "callable_name"], - evaluated_args, - false, - )?; - let value = required_evaluated_ref_arg(&bound, 0)?; - let syntax_only = optional_evaluated_ref_arg(&bound, 1) - .map(|arg| values.truthy(arg.value)) - .transpose()? - .unwrap_or(false); - let callable_name_target = optional_evaluated_ref_arg(&bound, 2) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - eval_is_callable_result( - value.value, - syntax_only, - callable_name_target.as_ref(), - lexical_scope, - context, - values, - ) -} - -/// Evaluates by-value dynamic `is_callable()` arguments without `$callable_name` writeback. -pub(in crate::interpreter) fn eval_is_callable_with_values( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match evaluated_args { - [value] => eval_is_callable_result(*value, false, None, None, context, values), - [value, syntax_only] => { - let syntax_only = values.truthy(*syntax_only)?; - eval_is_callable_result(*value, syntax_only, None, None, context, values) - } - [value, syntax_only, _callable_name] => { - let syntax_only = values.truthy(*syntax_only)?; - eval_is_callable_result(*value, syntax_only, None, None, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates `function_exists()` and `is_callable()` from materialized arguments. -pub(in crate::interpreter) fn eval_function_probe_result( - name: &str, - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match name { - "function_exists" => { - let name = eval_function_probe_name(value, values)?; - eval_function_probe_exists(context, &name) - } - "is_callable" => eval_is_callable_value(value, None, context, values)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(exists) -} - -/// Evaluates positional `is_callable()` arguments inside an eval fragment. -fn eval_builtin_is_callable( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_is_callable_result(value, false, None, Some(scope), context, values) - } - [value, syntax_only] => { - let value = eval_expr(value, context, scope, values)?; - let syntax_only = eval_expr(syntax_only, context, scope, values)?; - let syntax_only = values.truthy(syntax_only)?; - eval_is_callable_result(value, syntax_only, None, Some(scope), context, values) - } - [value, syntax_only, callable_name] => { - let value = eval_expr(value, context, scope, values)?; - let syntax_only = eval_expr(syntax_only, context, scope, values)?; - let syntax_only = values.truthy(syntax_only)?; - let (_, callable_name_target) = - eval_call_arg_value(callable_name, context, scope, values)?; - let callable_name_target = callable_name_target.ok_or(EvalStatus::RuntimeFatal)?; - eval_is_callable_result( - value, - syntax_only, - Some(&callable_name_target), - Some(scope), - context, - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates `define(name, value)` for eval dynamic constant-name registration. -pub(in crate::interpreter) fn eval_builtin_define( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - let defined = eval_define_name(name, value, context, values)?; - values.bool_value(defined) -} - -/// Evaluates `defined(name)` against eval dynamic constant names. -pub(in crate::interpreter) fn eval_builtin_defined( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - let exists = eval_defined_name(name, context, values)?; - values.bool_value(exists) -} - -/// Evaluates `define(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_define_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let defined = eval_define_name(*name, *value, context, values)?; - values.bool_value(defined) -} - -/// Evaluates `defined(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_defined_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let exists = eval_defined_name(*name, context, values)?; - values.bool_value(exists) -} - -/// Normalizes and registers one eval dynamic constant name. -pub(in crate::interpreter) fn eval_define_name( - name: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_constant_name(name, values)?; - if name.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - if eval_predefined_constant_value(&name).is_some() || context.has_constant(&name) { - values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; - return Ok(false); - } - let value = values.retain(value)?; - if context.define_constant(&name, value) { - Ok(true) - } else { - values.release(value)?; - Ok(false) - } -} - -/// Normalizes and probes one eval dynamic constant name. -pub(in crate::interpreter) fn eval_defined_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_constant_name(name, values)?; - Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) -} - -/// Reads a PHP constant name from a runtime cell without changing case. -pub(in crate::interpreter) fn eval_constant_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. -pub(in crate::interpreter) fn eval_builtin_class_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = match args { - [name] => eval_expr(name, context, scope, values)?, - [name, autoload] => { - let name = eval_expr(name, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - name - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_class_exists_name(name, context, values)?; - values.bool_value(exists) -} - -/// Evaluates `class_exists(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_class_exists_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [name] => eval_class_exists_name(*name, context, values)?, - [name, _autoload] => eval_class_exists_name(*name, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. -pub(in crate::interpreter) fn eval_class_exists_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\'); - if name.eq_ignore_ascii_case("Closure") { - return Ok(true); - } - if context.has_class(name) { - return Ok(true); - } - values.class_exists(name) -} - -/// Evaluates `class_alias(class, alias, autoload?)` against eval and generated class tables. -pub(in crate::interpreter) fn eval_builtin_class_alias( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (class, alias) = match args { - [class, alias] => ( - eval_expr(class, context, scope, values)?, - eval_expr(alias, context, scope, values)?, - ), - [class, alias, autoload] => { - let class = eval_expr(class, context, scope, values)?; - let alias = eval_expr(alias, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - (class, alias) - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_class_alias_result(&[class, alias], context, values) -} - -/// Evaluates `class_alias(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_class_alias_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (class, alias) = match evaluated_args { - [class, alias] => (*class, *alias), - [class, alias, _autoload] => (*class, *alias), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let class = eval_class_alias_name(class, values)?; - let alias = eval_class_alias_name(alias, values)?; - if alias.is_empty() - || context.resolve_class_like_name(&alias).is_some() - || values.class_exists(&alias)? - || eval_runtime_interface_exists(&alias, values)? - || values.trait_exists(&alias)? - || values.enum_exists(&alias)? - { - return values.bool_value(false); - } - let aliased = if context.resolve_class_like_name(&class).is_some() { - context.define_class_alias(&class, &alias) - } else if values.enum_exists(&class)? { - context.define_external_enum_alias(&class, &alias) - } else if values.class_exists(&class)? { - context.define_external_class_alias(&class, &alias) - } else if eval_runtime_interface_exists(&class, values)? { - context.define_external_interface_alias(&class, &alias) - } else if values.trait_exists(&class)? { - context.define_external_trait_alias(&class, &alias) - } else { - false - }; - values.bool_value(aliased) -} - -/// Reads and normalizes one `class_alias()` class-name argument. -fn eval_class_alias_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(name.trim_start_matches('\\').to_string()) -} - -/// Evaluates `get_declared_classes/interfaces/traits()` for eval-visible declarations. -pub(in crate::interpreter) fn eval_builtin_get_declared_symbols( - name: &str, - args: &[EvalExpr], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_declared_symbols_result(name, context, values) -} - -/// Builds an indexed array for eval-visible declared class-like names. -pub(in crate::interpreter) fn eval_get_declared_symbols_result( - name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "get_declared_classes" => { - eval_dynamic_string_array_result(context.declared_class_names(), values) - } - "get_declared_interfaces" => { - eval_dynamic_string_array_result(context.declared_interface_names(), values) - } - "get_declared_traits" => { - eval_dynamic_string_array_result(context.declared_trait_names(), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds one indexed PHP array from runtime-owned strings. -fn eval_dynamic_string_array_result( - items: &[String], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(items.len())?; - for (index, item) in items.iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string(item)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates `interface_exists(...)` against generated interface-name metadata. -pub(in crate::interpreter) fn eval_builtin_interface_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = match args { - [name] => eval_expr(name, context, scope, values)?, - [name, autoload] => { - let name = eval_expr(name, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - name - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_interface_exists_name(name, context, values)?; - values.bool_value(exists) -} - -/// Evaluates `interface_exists(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_interface_exists_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [name] => eval_interface_exists_name(*name, context, values)?, - [name, _autoload] => eval_interface_exists_name(*name, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP interface-name cell and probes eval and generated interface metadata. -pub(in crate::interpreter) fn eval_interface_exists_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\'); - Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) -} - -/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. -pub(in crate::interpreter) fn eval_builtin_class_like_exists( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let symbol = match args { - [symbol] => eval_expr(symbol, context, scope, values)?, - [symbol, autoload] => { - let symbol = eval_expr(symbol, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - symbol - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_class_like_exists_name(name, symbol, context, values)?; - values.bool_value(exists) -} - -/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. -pub(in crate::interpreter) fn eval_class_like_exists_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [symbol] => eval_class_like_exists_name(name, *symbol, context, values)?, - [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. -pub(in crate::interpreter) fn eval_class_like_exists_name( - name: &str, - symbol: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let symbol = values.string_bytes(symbol)?; - let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; - let symbol = symbol.trim_start_matches('\\'); - match name { - "trait_exists" => Ok(context.has_trait(symbol) || values.trait_exists(symbol)?), - "enum_exists" => Ok(context.has_enum(symbol) || values.enum_exists(symbol)?), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. -pub(in crate::interpreter) fn eval_builtin_is_a_relation( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_is_a_relation_result(name, &evaluated_args, context, values) -} - -/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. -pub(in crate::interpreter) fn eval_is_a_relation_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (object_or_class, target_class, allow_string) = match evaluated_args { - [object_or_class, target_class] => { - (*object_or_class, *target_class, name == "is_subclass_of") - } - [object_or_class, target_class, allow_string] => ( - *object_or_class, - *target_class, - values.truthy(*allow_string)?, - ), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let target_class = values.string_bytes(target_class)?; - let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_class = target_class.trim_start_matches('\\'); - let resolved_target_class = context - .resolve_class_like_name(target_class) - .unwrap_or_else(|| target_class.to_string()); - let is_object = values.type_tag(object_or_class)? == 6; - let exclude_self = name == "is_subclass_of"; - let result = if is_object { - dynamic_object_is_a( - object_or_class, - &resolved_target_class, - exclude_self, - context, - values, - )? - .map_or_else( - || values.object_is_a(object_or_class, &resolved_target_class, exclude_self), - Ok, - )? - } else if allow_string && values.type_tag(object_or_class)? == EVAL_TAG_STRING { - let source_class = values.string_bytes(object_or_class)?; - let source_class = String::from_utf8(source_class).map_err(|_| EvalStatus::RuntimeFatal)?; - let resolved_source_class = context - .resolve_class_like_name(&source_class) - .unwrap_or_else(|| source_class.trim_start_matches('\\').to_string()); - if context.class(&resolved_source_class).is_some() { - eval_class_string_is_a( - &resolved_source_class, - &resolved_target_class, - exclude_self, - context, - values, - )? - } else if context.interface(&resolved_source_class).is_some() { - eval_interface_string_is_a( - &resolved_source_class, - &resolved_target_class, - exclude_self, - context, - values, - )? - } else if context.trait_decl(&resolved_source_class).is_some() { - !exclude_self - && eval_class_like_name_matches(&resolved_source_class, &resolved_target_class) - } else { - values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? - } - } else if allow_string { - values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? - } else { - false - }; - values.bool_value(result) -} - -/// Returns whether an interface string source satisfies a class-like target. -fn eval_interface_string_is_a( - source_class: &str, - target_class: &str, - exclude_self: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !exclude_self && eval_class_like_name_matches(source_class, target_class) { - return Ok(true); - } - Ok(eval_interface_runtime_parent_names(source_class, context, values)? - .iter() - .any(|parent| eval_class_like_name_matches(parent, target_class))) -} - -/// Returns whether an eval class-string source satisfies a class-like target. -fn eval_class_string_is_a( - source_class: &str, - target_class: &str, - exclude_self: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if context.class_is_a(source_class, target_class, exclude_self) { - return Ok(true); - } - Ok(eval_class_runtime_interface_names(source_class, context, values)? - .iter() - .any(|interface| eval_class_like_name_matches(interface, target_class))) -} - -/// Returns eval class interfaces plus generated/AOT inherited interface names. -fn eval_class_runtime_interface_names( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - if let Some(parent) = context.class_native_parent_name(class_name) { - for name in eval_runtime_class_interface_names(&parent, values)? { - eval_push_unique_class_name(name, &mut names, &mut seen); - } - } - for name in context.class_interface_names(class_name) { - eval_push_unique_class_name(name.clone(), &mut names, &mut seen); - if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { - for parent in eval_runtime_class_interface_names(&name, values)? { - eval_push_unique_class_name(parent, &mut names, &mut seen); - } - } - } - Ok(names) -} - -/// Returns eval interface parents plus generated/AOT inherited interface names. -fn eval_interface_runtime_parent_names( - interface_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - for name in context.interface_parent_names(interface_name) { - eval_push_unique_class_name(name.clone(), &mut names, &mut seen); - if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { - for parent in eval_runtime_class_interface_names(&name, values)? { - eval_push_unique_class_name(parent, &mut names, &mut seen); - } - } - } - Ok(names) -} - -/// Returns generated/AOT interface names visible for one class-like symbol. -fn eval_runtime_class_interface_names( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let names_array = values.reflection_class_interface_names(class_name)?; - let names = eval_string_array_to_vec(names_array, values)?; - values.release(names_array)?; - Ok(names) -} - -/// Copies a runtime string array into Rust-owned names. -fn eval_string_array_to_vec( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut result = Vec::with_capacity(len); - for position in 0..len { - let key = values.int(position as i64)?; - let value = values.array_get(array, key)?; - let bytes = values.string_bytes(value)?; - result.push(String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?); - } - Ok(result) -} - -/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. -fn eval_push_unique_class_name( - name: String, - names: &mut Vec, - seen: &mut std::collections::HashSet, -) { - if seen.insert(name.to_ascii_lowercase()) { - names.push(name); - } -} - -/// Returns whether two class-like names match PHP's case-insensitive class-name rules. -fn eval_class_like_name_matches(left: &str, right: &str) -> bool { - left.trim_start_matches('\\') - .eq_ignore_ascii_case(right.trim_start_matches('\\')) -} - -/// Returns whether an eval-created object matches a dynamic class/interface target. -pub(in crate::interpreter) fn dynamic_object_is_a( - object: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let identity = values.object_identity(object)?; - if context.dynamic_object_is_class(identity, "Closure") { - return Ok(Some( - !exclude_self && eval_class_like_name_matches("Closure", target_class), - )); - } - let Some(class) = context.dynamic_object_class(identity) else { - return Ok(None); - }; - if eval_class_string_is_a(class.name(), target_class, exclude_self, context, values)? { - return Ok(Some(true)); - } - if context.class_native_parent_name(class.name()).is_some() { - return values - .object_is_a(object, target_class, exclude_self) - .map(Some); - } - Ok(Some(false)) -} - -/// Evaluates PHP's `isset(...)` language construct over eval-visible values. -pub(in crate::interpreter) fn eval_builtin_isset( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - if !eval_isset_arg(arg, context, scope, values)? { - return values.bool_value(false); - } - } - values.bool_value(true) -} - -/// Evaluates PHP's `empty(...)` language construct over eval-visible values. -pub(in crate::interpreter) fn eval_builtin_empty( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let empty = eval_empty_arg(arg, context, scope, values)?; - values.bool_value(empty) -} - -/// Evaluates direct `unset(...)` calls over eval-visible variables and object properties. -pub(in crate::interpreter) fn eval_builtin_unset( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - match arg { - EvalExpr::LoadVar(name) => { - if let Some(replaced) = unset_scope_cell(scope, name.clone()) { - values.release(replaced)?; - } - } - EvalExpr::PropertyGet { object, property } => { - let object = eval_expr(object, context, scope, values)?; - eval_property_unset_result(object, property, context, values)?; - } - EvalExpr::DynamicPropertyGet { object, property } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_property_unset_result(object, &property, context, values)?; - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - values.null() -} - -/// Evaluates callable `isset(...)` over already materialized values. -pub(in crate::interpreter) fn eval_isset_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - if evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - for value in evaluated_args { - if values.is_null(*value)? { - return values.bool_value(false); - } - } - values.bool_value(true) -} - -/// Evaluates callable `empty(...)` over one already materialized value. -pub(in crate::interpreter) fn eval_empty_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let empty = !values.truthy(*value)?; - values.bool_value(empty) -} - -/// Evaluates callable `unset(...)` after values have already been materialized. -pub(in crate::interpreter) fn eval_unset_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - if evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.null() -} - -/// Evaluates one `empty` operand without warning or failing on missing variables. -pub(in crate::interpreter) fn eval_empty_arg( - arg: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let EvalExpr::LoadVar(name) = arg { - let Some(value) = visible_scope_cell(context, scope, name) else { - return Ok(true); - }; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::PropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if !eval_property_isset_result(object, property, context, values)? { - return Ok(true); - } - let value = eval_property_get_result(object, property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::DynamicPropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - if !eval_property_isset_result(object, &property, context, values)? { - return Ok(true); - } - let value = eval_property_get_result(object, &property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::NullsafePropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if values.is_null(object)? { - return Ok(true); - } - if !eval_property_isset_result(object, property, context, values)? { - return Ok(true); - } - let value = eval_property_get_result(object, property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if values.is_null(object)? { - return Ok(true); - } - let property = eval_dynamic_member_name(property, context, scope, values)?; - if !eval_property_isset_result(object, &property, context, values)? { - return Ok(true); - } - let value = eval_property_get_result(object, &property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::StaticPropertyGet { - class_name, - property, - } = arg - { - if !eval_static_property_isset_result(class_name, property, context, values)? { - return Ok(true); - } - let value = eval_static_property_get_result(class_name, property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } = arg - { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - if !eval_static_property_isset_result(&class_name, property, context, values)? { - return Ok(true); - } - let value = eval_static_property_get_result(&class_name, property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } = arg - { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - if !eval_static_property_isset_result(&class_name, &property, context, values)? { - return Ok(true); - } - let value = eval_static_property_get_result(&class_name, &property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::ArrayGet { array, index } = arg { - let array = eval_expr(array, context, scope, values)?; - let index = eval_expr(index, context, scope, values)?; - if values.type_tag(array)? == EVAL_TAG_OBJECT { - return eval_array_access_empty_result(array, index, context, values); - } - let value = values.array_get(array, index)?; - return Ok(!values.truthy(value)?); - } - let value = eval_expr(arg, context, scope, values)?; - Ok(!values.truthy(value)?) -} - -/// Evaluates one `isset` operand without allocating a null cell for missing variables. -pub(in crate::interpreter) fn eval_isset_arg( - arg: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let EvalExpr::LoadVar(name) = arg { - let Some(value) = visible_scope_cell(context, scope, name) else { - return Ok(false); - }; - return Ok(!values.is_null(value)?); - } - if let EvalExpr::PropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - return eval_property_isset_result(object, property, context, values); - } - if let EvalExpr::DynamicPropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - return eval_property_isset_result(object, &property, context, values); - } - if let EvalExpr::NullsafePropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if values.is_null(object)? { - return Ok(false); - } - return eval_property_isset_result(object, property, context, values); - } - if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if values.is_null(object)? { - return Ok(false); - } - let property = eval_dynamic_member_name(property, context, scope, values)?; - return eval_property_isset_result(object, &property, context, values); - } - if let EvalExpr::StaticPropertyGet { - class_name, - property, - } = arg - { - return eval_static_property_isset_result(class_name, property, context, values); - } - if let EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } = arg - { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - return eval_static_property_isset_result(&class_name, property, context, values); - } - if let EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } = arg - { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - return eval_static_property_isset_result(&class_name, &property, context, values); - } - if let EvalExpr::ArrayGet { array, index } = arg { - let array = eval_expr(array, context, scope, values)?; - let index = eval_expr(index, context, scope, values)?; - if values.type_tag(array)? == EVAL_TAG_OBJECT { - return eval_array_access_isset_result(array, index, context, values); - } - let value = values.array_get(array, index)?; - return Ok(!values.is_null(value)?); - } - let value = eval_expr(arg, context, scope, values)?; - Ok(!values.is_null(value)?) -} - -/// Evaluates `empty($object[$key])` through `ArrayAccess::offsetExists()` and `offsetGet()`. -fn eval_array_access_empty_result( - object: RuntimeCellHandle, - index: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !eval_array_access_isset_result(object, index, context, values)? { - return Ok(true); - } - let value = eval_array_get_result(object, index, context, values)?; - Ok(!values.truthy(value)?) -} - -/// Evaluates `isset($object[$key])` through `ArrayAccess::offsetExists()`. -fn eval_array_access_isset_result( - object: RuntimeCellHandle, - index: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !eval_array_access_object_matches(object, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let result = eval_method_call_result(object, "offsetExists", vec![index], context, values)?; - let exists = values.truthy(result)?; - values.release(result)?; - Ok(exists) -} - -/// Returns true when a PHP function name is visible to eval builtin probes. -pub(in crate::interpreter) fn eval_function_probe_exists( - context: &ElephcEvalContext, - name: &str, -) -> bool { - !name.contains("::") - && (context.has_function(name) - || eval_php_visible_builtin_exists(name) - || eval_date_procedural_alias_exists(name)) -} - -/// Returns true for DateTime/calendar/timezone aliases that static elephc desugars. -fn eval_date_procedural_alias_exists(name: &str) -> bool { - let bare = name.rsplit('\\').next().unwrap_or(""); - EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS.contains(&bare) -} - -/// Reads and normalizes a function-probe string argument. -fn eval_function_probe_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(name.trim_start_matches('\\').to_ascii_lowercase()) -} - -/// Returns whether one runtime value is callable from the current eval scope. -fn eval_is_callable_value( - value: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = match lexical_scope { - Some(scope) => eval_callable_from_scope(value, context, scope, values), - None => eval_callable(value, context, values), - }; - let Ok(callback) = callback else { - return Ok(false); - }; - eval_callable_probe_exists(&callback, context, values) -} - -/// Evaluates `is_callable()` and writes PHP's display callable name when requested. -fn eval_is_callable_result( - value: RuntimeCellHandle, - syntax_only: bool, - callable_name_target: Option<&EvalReferenceTarget>, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callable_name = callable_name_target - .map(|_| eval_callable_display_name(value, context, values)) - .transpose()?; - let callable = if syntax_only { - eval_is_callable_syntax_only(value, context, values)? - } else { - eval_is_callable_value(value, lexical_scope, context, values)? - }; - if let Some((target, name)) = callable_name_target.zip(callable_name.as_deref()) { - let name = values.string(name)?; - eval_write_direct_ref_target( - target, - name, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - values.bool_value(callable) -} - -/// Returns PHP's syntax-only callable result without requiring the target to exist. -fn eval_is_callable_syntax_only( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? == EVAL_TAG_STRING { - return Ok(true); - } - if values.type_tag(value)? == EVAL_TAG_OBJECT { - return eval_is_callable_value(value, None, context, values); - } - if values.is_array_like(value)? { - return eval_callable_array_display_name(value, context, values).map(|name| name.is_some()); - } - Ok(false) -} - -/// Builds PHP's `$callable_name` output for one probed callable value. -fn eval_callable_display_name( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? == EVAL_TAG_STRING { - let bytes = values.string_bytes(value)?; - return String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal); - } - if values.type_tag(value)? == EVAL_TAG_OBJECT { - let class_name = eval_callable_object_class_name(value, context, values)?; - return Ok(format!("{class_name}::__invoke")); - } - if values.is_array_like(value)? { - return Ok(eval_callable_array_display_name(value, context, values)? - .unwrap_or_else(|| String::from("Array"))); - } - let string = values.cast_string(value)?; - let bytes = values.string_bytes(string); - values.release(string)?; - String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Builds PHP's `$callable_name` output for a syntactically valid callable array. -fn eval_callable_array_display_name( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.array_len(value)? != 2 { - return Ok(None); - } - let zero = values.int(0)?; - let one = values.int(1)?; - let receiver = values.array_get(value, zero)?; - let method = values.array_get(value, one)?; - if values.type_tag(method)? != EVAL_TAG_STRING { - return Ok(None); - } - let method = - String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; - let receiver_name = match values.type_tag(receiver)? { - EVAL_TAG_OBJECT => eval_callable_object_class_name(receiver, context, values)?, - EVAL_TAG_STRING => String::from_utf8(values.string_bytes(receiver)?) - .map_err(|_| EvalStatus::RuntimeFatal)?, - _ => return Ok(None), - }; - Ok(Some(format!("{receiver_name}::{method}"))) -} - -/// Returns the PHP-visible class name used when formatting callable object probes. -fn eval_callable_object_class_name( - object: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - if context.closure_object_target(identity).is_some() { - return Ok(String::from("Closure")); - } - if let Some(class_name) = context.dynamic_object_class_name(identity) { - return Ok(class_name); - } - runtime_object_class_name(object, values) -} - -/// Returns whether a normalized eval callback has an invokable target. -fn eval_callable_probe_exists( - callback: &EvaluatedCallable, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named { name, .. } => { - Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) - } - EvaluatedCallable::BoundClosure { name, .. } => { - Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) - } - EvaluatedCallable::InvokableObject { object } => { - eval_object_method_callable_probe(*object, "__invoke", context, values) - } - EvaluatedCallable::ObjectMethod { - object, - method, - native_class, - .. - } => { - if native_class.is_some() { - Ok(true) - } else { - eval_object_method_callable_probe(*object, method, context, values) - } - } - EvaluatedCallable::StaticMethod { - class_name, - method, - native_class, - .. - } => { - if native_class.is_some() { - Ok(true) - } else { - eval_static_method_callable_probe(class_name, method, context, values) - } - } - } -} - -/// Returns whether one object method can be called from the current eval scope. -fn eval_object_method_callable_probe( - object: RuntimeCellHandle, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Ok(identity) = values.object_identity(object) else { - return Ok(false); - }; - if context.closure_object_target(identity).is_some() - && method_name.eq_ignore_ascii_case("__invoke") - { - return Ok(true); - } - let Some(class) = context.dynamic_object_class(identity) else { - return eval_aot_object_method_callable_probe(object, method_name, context, values); - }; - if eval_enum_static_builtin_applies(class.name(), method_name, context).is_some() { - return Ok(true); - } - let Some((declaring_class, method)) = - eval_dynamic_method_for_call(class.name(), method_name, context) - else { - if eval_dynamic_class_native_method_callable_probe( - class.name(), - method_name, - context, - values, - )? { - return Ok(true); - } - return Ok(eval_instance_magic_method_callable_probe( - class.name(), - context, - )); - }; - if method.is_abstract() { - return Ok(false); - } - if method_name.eq_ignore_ascii_case("__invoke") { - return Ok(true); - } - Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() - || eval_instance_magic_method_callable_probe(class.name(), context)) -} - -/// Returns whether one static method can be called from the current eval scope. -fn eval_static_method_callable_probe( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { - return Ok(true); - } - if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { - if !method.is_static() || method.is_abstract() { - return Ok(false); - } - return Ok(validate_eval_member_access(&declaring_class, method.visibility(), context) - .is_ok() - || eval_static_magic_method_callable_probe(&class_name, context)); - } - if context.has_class(&class_name) - || context.has_interface(&class_name) - || context.has_trait(&class_name) - || context.has_enum(&class_name) - { - if eval_static_magic_method_callable_probe(&class_name, context) { - return Ok(true); - } - if let Some(parent) = context.class_native_parent_name(&class_name) { - return eval_aot_static_method_callable_probe( - &parent, - method_name, - context, - values, - ); - } - return Ok(false); - } - eval_aot_static_method_callable_probe(&class_name, method_name, context, values) -} - -/// Returns whether a generated/AOT object method can be called from the current eval scope. -fn eval_aot_object_method_callable_probe( - object: RuntimeCellHandle, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = runtime_object_class_name(object, values)?; - eval_aot_class_method_callable_probe(&class_name, method_name, context, values) -} - -/// Returns whether an eval class can call a generated/AOT parent instance method. -fn eval_dynamic_class_native_method_callable_probe( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(parent) = context.class_native_parent_name(class_name) else { - return Ok(false); - }; - eval_aot_class_method_callable_probe(&parent, method_name, context, values) -} - -/// Returns whether one generated/AOT class instance method can be called from eval. -fn eval_aot_class_method_callable_probe( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, visibility, _, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? - else { - return eval_aot_instance_magic_method_callable_probe(&class_name, context, values); - }; - if is_abstract { - return Ok(false); - } - Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() - || eval_aot_instance_magic_method_callable_probe(&class_name, context, values)?) -} - -/// Returns whether a generated/AOT static method can be called from the current eval scope. -fn eval_aot_static_method_callable_probe( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? - else { - return eval_aot_static_magic_method_callable_probe(class_name, context, values); - }; - if !is_static || is_abstract { - return Ok(false); - } - Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() - || eval_aot_static_magic_method_callable_probe(class_name, context, values)?) -} - -/// Returns whether a generated/AOT class has a callable instance `__call()` fallback. -fn eval_aot_instance_magic_method_callable_probe( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? - .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) -} - -/// Returns whether a generated/AOT class has a callable static `__callStatic()` fallback. -fn eval_aot_static_magic_method_callable_probe( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok( - eval_aot_method_dispatch_metadata_in_hierarchy( - class_name, - "__callStatic", - context, - values, - )? - .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), - ) -} - -/// Returns whether an eval class has a callable instance `__call()` fallback. -fn eval_instance_magic_method_callable_probe( - class_name: &str, - context: &ElephcEvalContext, -) -> bool { - context - .class_method(class_name, "__call") - .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) -} - -/// Returns whether an eval class has a callable static `__callStatic()` fallback. -fn eval_static_magic_method_callable_probe(class_name: &str, context: &ElephcEvalContext) -> bool { - context - .class_method(class_name, "__callStatic") - .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs new file mode 100644 index 0000000000..392bb8d24a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs @@ -0,0 +1,493 @@ +//! Purpose: +//! Callable probes for eval `is_callable()` and PHP callable display names. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols` re-exports. +//! +//! Key details: +//! - `$callable_name` writeback preserves by-reference targets captured during +//! argument evaluation. +//! - Syntax-only callable checks avoid resolving non-object string targets. + +use super::*; + +/// Evaluates `is_callable()` over full eval call metadata so `$callable_name` stays writable. +pub(in crate::interpreter) fn eval_builtin_is_callable_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_is_callable_call_with_evaluated_args_from_scope( + &evaluated_args, + Some(scope), + context, + values, + ) +} + +/// Evaluates `is_callable()` from already evaluated arguments that may retain ref targets. +pub(in crate::interpreter) fn eval_is_callable_call_with_evaluated_args( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_is_callable_call_with_evaluated_args_from_scope(evaluated_args, None, context, values) +} + +/// Evaluates materialized `is_callable()` args with optional special-class callable scope. +fn eval_is_callable_call_with_evaluated_args_from_scope( + evaluated_args: &[EvaluatedCallArg], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["value", "syntax_only", "callable_name"], + evaluated_args, + false, + )?; + let value = required_evaluated_ref_arg(&bound, 0)?; + let syntax_only = optional_evaluated_ref_arg(&bound, 1) + .map(|arg| values.truthy(arg.value)) + .transpose()? + .unwrap_or(false); + let callable_name_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + eval_is_callable_result( + value.value, + syntax_only, + callable_name_target.as_ref(), + lexical_scope, + context, + values, + ) +} + +/// Evaluates by-value dynamic `is_callable()` arguments without `$callable_name` writeback. +pub(in crate::interpreter) fn eval_is_callable_with_values( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_is_callable_result(*value, false, None, None, context, values), + [value, syntax_only] => { + let syntax_only = values.truthy(*syntax_only)?; + eval_is_callable_result(*value, syntax_only, None, None, context, values) + } + [value, syntax_only, _callable_name] => { + let syntax_only = values.truthy(*syntax_only)?; + eval_is_callable_result(*value, syntax_only, None, None, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates positional `is_callable()` arguments inside an eval fragment. +pub(super) fn eval_builtin_is_callable( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_is_callable_result(value, false, None, Some(scope), context, values) + } + [value, syntax_only] => { + let value = eval_expr(value, context, scope, values)?; + let syntax_only = eval_expr(syntax_only, context, scope, values)?; + let syntax_only = values.truthy(syntax_only)?; + eval_is_callable_result(value, syntax_only, None, Some(scope), context, values) + } + [value, syntax_only, callable_name] => { + let value = eval_expr(value, context, scope, values)?; + let syntax_only = eval_expr(syntax_only, context, scope, values)?; + let syntax_only = values.truthy(syntax_only)?; + let (_, callable_name_target) = + eval_call_arg_value(callable_name, context, scope, values)?; + let callable_name_target = callable_name_target.ok_or(EvalStatus::RuntimeFatal)?; + eval_is_callable_result( + value, + syntax_only, + Some(&callable_name_target), + Some(scope), + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether one runtime value is callable from the current eval scope. +pub(super) fn eval_is_callable_value( + value: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = match lexical_scope { + Some(scope) => eval_callable_from_scope(value, context, scope, values), + None => eval_callable(value, context, values), + }; + let Ok(callback) = callback else { + return Ok(false); + }; + eval_callable_probe_exists(&callback, context, values) +} + +/// Evaluates `is_callable()` and writes PHP's display callable name when requested. +fn eval_is_callable_result( + value: RuntimeCellHandle, + syntax_only: bool, + callable_name_target: Option<&EvalReferenceTarget>, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callable_name = callable_name_target + .map(|_| eval_callable_display_name(value, context, values)) + .transpose()?; + let callable = if syntax_only { + eval_is_callable_syntax_only(value, context, values)? + } else { + eval_is_callable_value(value, lexical_scope, context, values)? + }; + if let Some((target, name)) = callable_name_target.zip(callable_name.as_deref()) { + let name = values.string(name)?; + eval_write_direct_ref_target( + target, + name, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + values.bool_value(callable) +} + +/// Returns PHP's syntax-only callable result without requiring the target to exist. +fn eval_is_callable_syntax_only( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_STRING { + return Ok(true); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT { + return eval_is_callable_value(value, None, context, values); + } + if values.is_array_like(value)? { + return eval_callable_array_display_name(value, context, values).map(|name| name.is_some()); + } + Ok(false) +} + +/// Builds PHP's `$callable_name` output for one probed callable value. +fn eval_callable_display_name( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_STRING { + let bytes = values.string_bytes(value)?; + return String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT { + let class_name = eval_callable_object_class_name(value, context, values)?; + return Ok(format!("{class_name}::__invoke")); + } + if values.is_array_like(value)? { + return Ok(eval_callable_array_display_name(value, context, values)? + .unwrap_or_else(|| String::from("Array"))); + } + let string = values.cast_string(value)?; + let bytes = values.string_bytes(string); + values.release(string)?; + String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Builds PHP's `$callable_name` output for a syntactically valid callable array. +fn eval_callable_array_display_name( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(value)? != 2 { + return Ok(None); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(value, zero)?; + let method = values.array_get(value, one)?; + if values.type_tag(method)? != EVAL_TAG_STRING { + return Ok(None); + } + let method = + String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; + let receiver_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_callable_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => String::from_utf8(values.string_bytes(receiver)?) + .map_err(|_| EvalStatus::RuntimeFatal)?, + _ => return Ok(None), + }; + Ok(Some(format!("{receiver_name}::{method}"))) +} + +/// Returns the PHP-visible class name used when formatting callable object probes. +fn eval_callable_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if context.closure_object_target(identity).is_some() { + return Ok(String::from("Closure")); + } + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + runtime_object_class_name(object, values) +} + +/// Returns whether a normalized eval callback has an invokable target. +fn eval_callable_probe_exists( + callback: &EvaluatedCallable, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => { + Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) + } + EvaluatedCallable::BoundClosure { name, .. } => { + Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) + } + EvaluatedCallable::InvokableObject { object } => { + eval_object_method_callable_probe(*object, "__invoke", context, values) + } + EvaluatedCallable::ObjectMethod { + object, + method, + native_class, + .. + } => { + if native_class.is_some() { + Ok(true) + } else { + eval_object_method_callable_probe(*object, method, context, values) + } + } + EvaluatedCallable::StaticMethod { + class_name, + method, + native_class, + .. + } => { + if native_class.is_some() { + Ok(true) + } else { + eval_static_method_callable_probe(class_name, method, context, values) + } + } + } +} + +/// Returns whether one object method can be called from the current eval scope. +fn eval_object_method_callable_probe( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return Ok(false); + }; + if context.closure_object_target(identity).is_some() + && method_name.eq_ignore_ascii_case("__invoke") + { + return Ok(true); + } + let Some(class) = context.dynamic_object_class(identity) else { + return eval_aot_object_method_callable_probe(object, method_name, context, values); + }; + if eval_enum_static_builtin_applies(class.name(), method_name, context).is_some() { + return Ok(true); + } + let Some((declaring_class, method)) = + eval_dynamic_method_for_call(class.name(), method_name, context) + else { + if eval_dynamic_class_native_method_callable_probe( + class.name(), + method_name, + context, + values, + )? { + return Ok(true); + } + return Ok(eval_instance_magic_method_callable_probe( + class.name(), + context, + )); + }; + if method.is_abstract() { + return Ok(false); + } + if method_name.eq_ignore_ascii_case("__invoke") { + return Ok(true); + } + Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_instance_magic_method_callable_probe(class.name(), context)) +} + +/// Returns whether one static method can be called from the current eval scope. +fn eval_static_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { + return Ok(true); + } + if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { + if !method.is_static() || method.is_abstract() { + return Ok(false); + } + return Ok(validate_eval_member_access(&declaring_class, method.visibility(), context) + .is_ok() + || eval_static_magic_method_callable_probe(&class_name, context)); + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { + if eval_static_magic_method_callable_probe(&class_name, context) { + return Ok(true); + } + if let Some(parent) = context.class_native_parent_name(&class_name) { + return eval_aot_static_method_callable_probe( + &parent, + method_name, + context, + values, + ); + } + return Ok(false); + } + eval_aot_static_method_callable_probe(&class_name, method_name, context, values) +} + +/// Returns whether a generated/AOT object method can be called from the current eval scope. +fn eval_aot_object_method_callable_probe( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = runtime_object_class_name(object, values)?; + eval_aot_class_method_callable_probe(&class_name, method_name, context, values) +} + +/// Returns whether an eval class can call a generated/AOT parent instance method. +fn eval_dynamic_class_native_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_aot_class_method_callable_probe(&parent, method_name, context, values) +} + +/// Returns whether one generated/AOT class instance method can be called from eval. +fn eval_aot_class_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? + else { + return eval_aot_instance_magic_method_callable_probe(&class_name, context, values); + }; + if is_abstract { + return Ok(false); + } + Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_aot_instance_magic_method_callable_probe(&class_name, context, values)?) +} + +/// Returns whether a generated/AOT static method can be called from the current eval scope. +fn eval_aot_static_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + return eval_aot_static_magic_method_callable_probe(class_name, context, values); + }; + if !is_static || is_abstract { + return Ok(false); + } + Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_aot_static_magic_method_callable_probe(class_name, context, values)?) +} + +/// Returns whether a generated/AOT class has a callable instance `__call()` fallback. +fn eval_aot_instance_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether a generated/AOT class has a callable static `__callStatic()` fallback. +fn eval_aot_static_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Returns whether an eval class has a callable instance `__call()` fallback. +fn eval_instance_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a callable static `__callStatic()` fallback. +fn eval_static_magic_method_callable_probe(class_name: &str, context: &ElephcEvalContext) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_names.rs new file mode 100644 index 0000000000..138d762f93 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_names.rs @@ -0,0 +1,282 @@ +//! Purpose: +//! Class-like symbol existence, aliasing, and declared-symbol array builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols` re-exports. +//! +//! Key details: +//! - Lookup checks eval declarations before generated/AOT runtime metadata. +//! - `class_alias()` records external aliases in the eval context when the +//! aliased class-like symbol is provided by runtime metadata. + +use super::*; + +/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. +pub(in crate::interpreter) fn eval_builtin_class_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_exists_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `class_exists(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_class_exists_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_class_exists_name(*name, context, values)?, + [name, _autoload] => eval_class_exists_name(*name, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. +pub(in crate::interpreter) fn eval_class_exists_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\'); + if name.eq_ignore_ascii_case("Closure") { + return Ok(true); + } + if context.has_class(name) { + return Ok(true); + } + values.class_exists(name) +} + +/// Evaluates `class_alias(class, alias, autoload?)` against eval and generated class tables. +pub(in crate::interpreter) fn eval_builtin_class_alias( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (class, alias) = match args { + [class, alias] => ( + eval_expr(class, context, scope, values)?, + eval_expr(alias, context, scope, values)?, + ), + [class, alias, autoload] => { + let class = eval_expr(class, context, scope, values)?; + let alias = eval_expr(alias, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + (class, alias) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_alias_result(&[class, alias], context, values) +} + +/// Evaluates `class_alias(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_class_alias_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (class, alias) = match evaluated_args { + [class, alias] => (*class, *alias), + [class, alias, _autoload] => (*class, *alias), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let class = eval_class_alias_name(class, values)?; + let alias = eval_class_alias_name(alias, values)?; + if alias.is_empty() + || context.resolve_class_like_name(&alias).is_some() + || values.class_exists(&alias)? + || eval_runtime_interface_exists(&alias, values)? + || values.trait_exists(&alias)? + || values.enum_exists(&alias)? + { + return values.bool_value(false); + } + let aliased = if context.resolve_class_like_name(&class).is_some() { + context.define_class_alias(&class, &alias) + } else if values.enum_exists(&class)? { + context.define_external_enum_alias(&class, &alias) + } else if values.class_exists(&class)? { + context.define_external_class_alias(&class, &alias) + } else if eval_runtime_interface_exists(&class, values)? { + context.define_external_interface_alias(&class, &alias) + } else if values.trait_exists(&class)? { + context.define_external_trait_alias(&class, &alias) + } else { + false + }; + values.bool_value(aliased) +} + +/// Reads and normalizes one `class_alias()` class-name argument. +fn eval_class_alias_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_string()) +} + +/// Evaluates `get_declared_classes/interfaces/traits()` for eval-visible declarations. +pub(in crate::interpreter) fn eval_builtin_get_declared_symbols( + name: &str, + args: &[EvalExpr], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_declared_symbols_result(name, context, values) +} + +/// Builds an indexed array for eval-visible declared class-like names. +pub(in crate::interpreter) fn eval_get_declared_symbols_result( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "get_declared_classes" => { + eval_dynamic_string_array_result(context.declared_class_names(), values) + } + "get_declared_interfaces" => { + eval_dynamic_string_array_result(context.declared_interface_names(), values) + } + "get_declared_traits" => { + eval_dynamic_string_array_result(context.declared_trait_names(), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds one indexed PHP array from runtime-owned strings. +fn eval_dynamic_string_array_result( + items: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Evaluates `interface_exists(...)` against generated interface-name metadata. +pub(in crate::interpreter) fn eval_builtin_interface_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_interface_exists_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `interface_exists(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_interface_exists_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_interface_exists_name(*name, context, values)?, + [name, _autoload] => eval_interface_exists_name(*name, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP interface-name cell and probes eval and generated interface metadata. +pub(in crate::interpreter) fn eval_interface_exists_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\'); + Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) +} + +/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. +pub(in crate::interpreter) fn eval_builtin_class_like_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = match args { + [symbol] => eval_expr(symbol, context, scope, values)?, + [symbol, autoload] => { + let symbol = eval_expr(symbol, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + symbol + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_like_exists_name(name, symbol, context, values)?; + values.bool_value(exists) +} + +/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. +pub(in crate::interpreter) fn eval_class_like_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [symbol] => eval_class_like_exists_name(name, *symbol, context, values)?, + [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. +pub(in crate::interpreter) fn eval_class_like_exists_name( + name: &str, + symbol: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = values.string_bytes(symbol)?; + let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; + let symbol = symbol.trim_start_matches('\\'); + match name { + "trait_exists" => Ok(context.has_trait(symbol) || values.trait_exists(symbol)?), + "enum_exists" => Ok(context.has_enum(symbol) || values.enum_exists(symbol)?), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_relations.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_relations.rs new file mode 100644 index 0000000000..eb5a91c403 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_relations.rs @@ -0,0 +1,247 @@ +//! Purpose: +//! Class-like relationship probes for eval `is_a()` and `is_subclass_of()`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols` re-exports. +//! +//! Key details: +//! - Eval-created classes are checked first, then generated/AOT object and +//! interface metadata fills inherited relationships. + +use super::*; + +/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. +pub(in crate::interpreter) fn eval_builtin_is_a_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_is_a_relation_result(name, &evaluated_args, context, values) +} + +/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. +pub(in crate::interpreter) fn eval_is_a_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_or_class, target_class, allow_string) = match evaluated_args { + [object_or_class, target_class] => { + (*object_or_class, *target_class, name == "is_subclass_of") + } + [object_or_class, target_class, allow_string] => ( + *object_or_class, + *target_class, + values.truthy(*allow_string)?, + ), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let target_class = values.string_bytes(target_class)?; + let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_class = target_class.trim_start_matches('\\'); + let resolved_target_class = context + .resolve_class_like_name(target_class) + .unwrap_or_else(|| target_class.to_string()); + let is_object = values.type_tag(object_or_class)? == 6; + let exclude_self = name == "is_subclass_of"; + let result = if is_object { + dynamic_object_is_a( + object_or_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + .map_or_else( + || values.object_is_a(object_or_class, &resolved_target_class, exclude_self), + Ok, + )? + } else if allow_string && values.type_tag(object_or_class)? == EVAL_TAG_STRING { + let source_class = values.string_bytes(object_or_class)?; + let source_class = String::from_utf8(source_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let resolved_source_class = context + .resolve_class_like_name(&source_class) + .unwrap_or_else(|| source_class.trim_start_matches('\\').to_string()); + if context.class(&resolved_source_class).is_some() { + eval_class_string_is_a( + &resolved_source_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + } else if context.interface(&resolved_source_class).is_some() { + eval_interface_string_is_a( + &resolved_source_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + } else if context.trait_decl(&resolved_source_class).is_some() { + !exclude_self + && eval_class_like_name_matches(&resolved_source_class, &resolved_target_class) + } else { + values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? + } + } else if allow_string { + values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? + } else { + false + }; + values.bool_value(result) +} + +/// Returns whether an interface string source satisfies a class-like target. +fn eval_interface_string_is_a( + source_class: &str, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !exclude_self && eval_class_like_name_matches(source_class, target_class) { + return Ok(true); + } + Ok(eval_interface_runtime_parent_names(source_class, context, values)? + .iter() + .any(|parent| eval_class_like_name_matches(parent, target_class))) +} + +/// Returns whether an eval class-string source satisfies a class-like target. +fn eval_class_string_is_a( + source_class: &str, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.class_is_a(source_class, target_class, exclude_self) { + return Ok(true); + } + Ok(eval_class_runtime_interface_names(source_class, context, values)? + .iter() + .any(|interface| eval_class_like_name_matches(interface, target_class))) +} + +/// Returns eval class interfaces plus generated/AOT inherited interface names. +fn eval_class_runtime_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_runtime_class_interface_names(&parent, values)? { + eval_push_unique_class_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus generated/AOT inherited interface names. +fn eval_interface_runtime_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns generated/AOT interface names visible for one class-like symbol. +fn eval_runtime_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let names_array = values.reflection_class_interface_names(class_name)?; + let names = eval_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Copies a runtime string array into Rust-owned names. +fn eval_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + let bytes = values.string_bytes(value)?; + result.push(String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?); + } + Ok(result) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +fn eval_push_unique_class_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + +/// Returns whether two class-like names match PHP's case-insensitive class-name rules. +fn eval_class_like_name_matches(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns whether an eval-created object matches a dynamic class/interface target. +pub(in crate::interpreter) fn dynamic_object_is_a( + object: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let identity = values.object_identity(object)?; + if context.dynamic_object_is_class(identity, "Closure") { + return Ok(Some( + !exclude_self && eval_class_like_name_matches("Closure", target_class), + )); + } + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(None); + }; + if eval_class_string_is_a(class.name(), target_class, exclude_self, context, values)? { + return Ok(Some(true)); + } + if context.class_native_parent_name(class.name()).is_some() { + return values + .object_is_a(object, target_class, exclude_self) + .map(Some); + } + Ok(Some(false)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/constants.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/constants.rs new file mode 100644 index 0000000000..da0d72c833 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/constants.rs @@ -0,0 +1,111 @@ +//! Purpose: +//! Dynamic constant definition and lookup builtins for runtime eval fragments. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols` re-exports. +//! +//! Key details: +//! - Dynamic constants are stored in the eval context and checked after +//! predefined constants using PHP's case-sensitive dynamic constant names. + +use super::*; + +/// Evaluates `define(name, value)` for eval dynamic constant-name registration. +pub(in crate::interpreter) fn eval_builtin_define( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + let defined = eval_define_name(name, value, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `defined(name)` against eval dynamic constant names. +pub(in crate::interpreter) fn eval_builtin_defined( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let exists = eval_defined_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `define(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_define_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let defined = eval_define_name(*name, *value, context, values)?; + values.bool_value(defined) +} + +/// Evaluates `defined(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_defined_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let exists = eval_defined_name(*name, context, values)?; + values.bool_value(exists) +} + +/// Normalizes and registers one eval dynamic constant name. +pub(in crate::interpreter) fn eval_define_name( + name: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + if name.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if eval_predefined_constant_value(&name).is_some() || context.has_constant(&name) { + values.warning(DEFINE_ALREADY_DEFINED_WARNING)?; + return Ok(false); + } + let value = values.retain(value)?; + if context.define_constant(&name, value) { + Ok(true) + } else { + values.release(value)?; + Ok(false) + } +} + +/// Normalizes and probes one eval dynamic constant name. +pub(in crate::interpreter) fn eval_defined_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) +} + +/// Reads a PHP constant name from a runtime cell without changing case. +pub(in crate::interpreter) fn eval_constant_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/function_probe.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/function_probe.rs new file mode 100644 index 0000000000..34d25ea02a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/function_probe.rs @@ -0,0 +1,139 @@ +//! Purpose: +//! Function-name probes for eval `function_exists()` and function-like symbol lookup. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols` re-exports. +//! +//! Key details: +//! - Procedural date/time aliases stay visible to eval because static name +//! resolver rewrites do not run inside runtime eval fragments. + +use super::*; + +const EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS: &[&str] = &[ + "cal_days_in_month", + "cal_from_jd", + "cal_info", + "cal_to_jd", + "date_add", + "date_create", + "date_create_from_format", + "date_create_immutable", + "date_create_immutable_from_format", + "date_date_set", + "date_diff", + "date_format", + "date_get_last_errors", + "date_interval_create_from_date_string", + "date_interval_format", + "date_isodate_set", + "date_modify", + "date_offset_get", + "date_parse", + "date_parse_from_format", + "date_sub", + "date_sun_info", + "date_sunrise", + "date_sunset", + "date_time_set", + "date_timestamp_get", + "date_timestamp_set", + "date_timezone_get", + "date_timezone_set", + "easter_date", + "easter_days", + "frenchtojd", + "gettimeofday", + "gmstrftime", + "gregoriantojd", + "idate", + "jddayofweek", + "jdmonthname", + "jdtofrench", + "jdtogregorian", + "jdtojewish", + "jdtojulian", + "jdtounix", + "jewishtojd", + "juliantojd", + "mktime", + "gmmktime", + "strftime", + "strptime", + "timezone_abbreviations_list", + "timezone_identifiers_list", + "timezone_location_get", + "timezone_name_from_abbr", + "timezone_name_get", + "timezone_offset_get", + "timezone_open", + "timezone_transitions_get", + "timezone_version_get", + "unixtojd", +]; + +/// Evaluates `function_exists()` and `is_callable()` inside an eval fragment. +pub(in crate::interpreter) fn eval_builtin_function_probe( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "function_exists" => { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_function_probe_result(name, value, context, values) + } + "is_callable" => eval_builtin_is_callable(args, context, scope, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates `function_exists()` and `is_callable()` from materialized arguments. +pub(in crate::interpreter) fn eval_function_probe_result( + name: &str, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match name { + "function_exists" => { + let name = eval_function_probe_name(value, values)?; + eval_function_probe_exists(context, &name) + } + "is_callable" => eval_is_callable_value(value, None, context, values)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(exists) +} + +/// Returns true when a PHP function name is visible to eval builtin probes. +pub(in crate::interpreter) fn eval_function_probe_exists( + context: &ElephcEvalContext, + name: &str, +) -> bool { + !name.contains("::") + && (context.has_function(name) + || eval_php_visible_builtin_exists(name) + || eval_date_procedural_alias_exists(name)) +} + +/// Returns true for DateTime/calendar/timezone aliases that static elephc desugars. +fn eval_date_procedural_alias_exists(name: &str) -> bool { + let bare = name.rsplit('\\').next().unwrap_or(""); + EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS.contains(&bare) +} + +/// Reads and normalizes a function-probe string argument. +fn eval_function_probe_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_ascii_lowercase()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/language_constructs.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/language_constructs.rs new file mode 100644 index 0000000000..d5511cfd4b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/language_constructs.rs @@ -0,0 +1,324 @@ +//! Purpose: +//! Eval implementations of PHP language constructs `isset`, `empty`, and `unset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols` re-exports. +//! +//! Key details: +//! - These constructs receive unevaluated source expressions so missing variables, +//! properties, array access, and by-reference unset targets keep PHP semantics. + +use super::*; + +/// Evaluates PHP's `isset(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_builtin_isset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + if !eval_isset_arg(arg, context, scope, values)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates PHP's `empty(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_builtin_empty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = eval_empty_arg(arg, context, scope, values)?; + values.bool_value(empty) +} + +/// Evaluates direct `unset(...)` calls over eval-visible variables and object properties. +pub(in crate::interpreter) fn eval_builtin_unset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + match arg { + EvalExpr::LoadVar(name) => { + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { + values.release(replaced)?; + } + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_unset_result(object, property, context, values)?; + } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_unset_result(object, &property, context, values)?; + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + values.null() +} + +/// Evaluates callable `isset(...)` over already materialized values. +pub(in crate::interpreter) fn eval_isset_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for value in evaluated_args { + if values.is_null(*value)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates callable `empty(...)` over one already materialized value. +pub(in crate::interpreter) fn eval_empty_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = !values.truthy(*value)?; + values.bool_value(empty) +} + +/// Evaluates callable `unset(...)` after values have already been materialized. +pub(in crate::interpreter) fn eval_unset_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.null() +} + +/// Evaluates one `empty` operand without warning or failing on missing variables. +pub(in crate::interpreter) fn eval_empty_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(true); + }; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::PropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if !eval_property_isset_result(object, property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_property_isset_result(object, &property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::NullsafePropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(true); + } + if !eval_property_isset_result(object, property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(true); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_property_isset_result(object, &property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::StaticPropertyGet { + class_name, + property, + } = arg + { + if !eval_static_property_isset_result(class_name, property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(class_name, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + if !eval_static_property_isset_result(&class_name, property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(&class_name, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_static_property_isset_result(&class_name, &property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(&class_name, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::ArrayGet { array, index } = arg { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return eval_array_access_empty_result(array, index, context, values); + } + let value = values.array_get(array, index)?; + return Ok(!values.truthy(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.truthy(value)?) +} + +/// Evaluates one `isset` operand without allocating a null cell for missing variables. +pub(in crate::interpreter) fn eval_isset_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(false); + }; + return Ok(!values.is_null(value)?); + } + if let EvalExpr::PropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + return eval_property_isset_result(object, property, context, values); + } + if let EvalExpr::DynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_property_isset_result(object, &property, context, values); + } + if let EvalExpr::NullsafePropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(false); + } + return eval_property_isset_result(object, property, context, values); + } + if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(false); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_property_isset_result(object, &property, context, values); + } + if let EvalExpr::StaticPropertyGet { + class_name, + property, + } = arg + { + return eval_static_property_isset_result(class_name, property, context, values); + } + if let EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + return eval_static_property_isset_result(&class_name, property, context, values); + } + if let EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_static_property_isset_result(&class_name, &property, context, values); + } + if let EvalExpr::ArrayGet { array, index } = arg { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return eval_array_access_isset_result(array, index, context, values); + } + let value = values.array_get(array, index)?; + return Ok(!values.is_null(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.is_null(value)?) +} + +/// Evaluates `empty($object[$key])` through `ArrayAccess::offsetExists()` and `offsetGet()`. +fn eval_array_access_empty_result( + object: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_array_access_isset_result(object, index, context, values)? { + return Ok(true); + } + let value = eval_array_get_result(object, index, context, values)?; + Ok(!values.truthy(value)?) +} + +/// Evaluates `isset($object[$key])` through `ArrayAccess::offsetExists()`. +fn eval_array_access_isset_result( + object: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_method_call_result(object, "offsetExists", vec![index], context, values)?; + let exists = values.truthy(result)?; + values.release(result)?; + Ok(exists) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs index f1895e3b77..9d3031c1a0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs @@ -9,6 +9,9 @@ //! Key details: //! - The eval parser cannot run the static name-resolver rewrite, so this module //! mirrors the alias dispatch at runtime and delegates to native AOT bridges. +//! - This file is a deliberate >500 LoC single-scope runtime bridge for +//! procedural date/time aliases; splitting by alias would obscure the shared +//! fallback and timezone-table rules. use super::super::super::*; use super::*; diff --git a/crates/elephc-magician/src/interpreter/expressions.rs b/crates/elephc-magician/src/interpreter/expressions.rs index 0cd8e0048b..1b406f8c54 100644 --- a/crates/elephc-magician/src/interpreter/expressions.rs +++ b/crates/elephc-magician/src/interpreter/expressions.rs @@ -11,6 +11,17 @@ use super::*; +mod calls; + +pub(in crate::interpreter) use calls::*; +mod evaluation; + +pub(in crate::interpreter) use evaluation::{ + eval_array_access_object_matches, eval_array_get_result, eval_binary_result, + eval_dynamic_class_name, eval_dynamic_member_name, eval_match_expr, +}; +use evaluation::*; + /// Evaluates one expression to an opaque runtime-cell handle. pub(in crate::interpreter) fn eval_expr( expr: &EvalExpr, @@ -365,1175 +376,3 @@ pub(in crate::interpreter) fn eval_expr( } } } - -/// Applies one already-evaluated binary operation with eval runtime semantics. -pub(in crate::interpreter) fn eval_binary_result( - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match op { - EvalBinOp::Add => values.add(left, right), - EvalBinOp::Sub => values.sub(left, right), - EvalBinOp::Mul => values.mul(left, right), - EvalBinOp::Div => values.div(left, right), - EvalBinOp::Mod => values.modulo(left, right), - EvalBinOp::Pow => values.pow(left, right), - EvalBinOp::BitAnd - | EvalBinOp::BitOr - | EvalBinOp::BitXor - | EvalBinOp::ShiftLeft - | EvalBinOp::ShiftRight => values.bitwise(op, left, right), - EvalBinOp::Concat => { - let left = eval_string_context_value(left, context, values)?; - let right = eval_string_context_value(right, context, values)?; - values.concat(left, right) - } - EvalBinOp::LogicalXor => { - let left_truthy = values.truthy(left)?; - let right_truthy = values.truthy(right)?; - values.bool_value(left_truthy ^ right_truthy) - } - EvalBinOp::LooseEq - | EvalBinOp::LooseNotEq - | EvalBinOp::StrictEq - | EvalBinOp::StrictNotEq - | EvalBinOp::Lt - | EvalBinOp::LtEq - | EvalBinOp::Gt - | EvalBinOp::GtEq => values.compare(op, left, right), - EvalBinOp::Spaceship => values.spaceship(left, right), - EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates a runtime property or method name expression and returns its PHP string bytes as UTF-8. -pub(in crate::interpreter) fn eval_dynamic_member_name( - expr: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_expr(expr, context, scope, values)?; - let value = eval_string_context_value(value, context, values)?; - let bytes = values.string_bytes(value)?; - String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Reads an array element or dispatches `ArrayAccess::offsetGet()` for objects. -pub(in crate::interpreter) fn eval_array_get_result( - array: RuntimeCellHandle, - index: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(array)? != EVAL_TAG_OBJECT { - if let Some(target) = eval_array_reference_key(index, values)? - .and_then(|key| context.array_element_alias(array, &key).cloned()) - { - return eval_reference_target_value(&target, context, values); - } - return values.array_get(array, index); - } - if !eval_array_access_object_matches(array, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - eval_method_call_result(array, "offsetGet", vec![index], context, values) -} - -/// Returns whether an object value satisfies PHP's `ArrayAccess` interface. -pub(in crate::interpreter) fn eval_array_access_object_matches( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - dynamic_object_is_a(value, "ArrayAccess", false, context, values)? - .map_or_else(|| values.object_is_a(value, "ArrayAccess", false), Ok) -} - -/// Evaluates one PHP scalar cast expression through the runtime conversion hooks. -fn eval_cast_expr( - target: &EvalCastType, - expr: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_expr(expr, context, scope, values)?; - match target { - EvalCastType::Int => values.cast_int(value), - EvalCastType::Float => values.cast_float(value), - EvalCastType::String => { - let value = eval_string_context_value(value, context, values)?; - values.cast_string(value) - } - EvalCastType::Bool => values.cast_bool(value), - } -} - -/// Constructs an object after the target class name and constructor arguments have been evaluated. -fn eval_new_object_result( - class_name: &str, - args: Vec, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(object) = - eval_reflection_owner_new_object(class_name, args.clone(), context, values)? - { - return Ok(object); - } - if let Some(class) = context.class(class_name).cloned() { - return eval_dynamic_class_new_object(&class, args, context, scope, values); - } - let object = values.new_object(class_name)?; - if let Err(err) = - eval_native_constructor_with_evaluated_args(class_name, object, args, context, values) - { - let _ = values.release(object); - return Err(err); - } - Ok(object) -} - -/// Resolves special class names used by `new` while preserving AOT fallback names. -fn eval_new_object_class_name( - class_name: &str, - context: &ElephcEvalContext, -) -> Result { - match class_name.to_ascii_lowercase().as_str() { - "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), - _ => Ok(context - .resolve_class_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), - } -} - -/// Resolves a runtime class-name value used by dynamic class operations. -pub(in crate::interpreter) fn eval_dynamic_class_name( - class_name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(class_name)? { - EVAL_TAG_STRING => { - let bytes = values.string_bytes(class_name)?; - let class_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(context - .resolve_class_like_name(&class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())) - } - EVAL_TAG_OBJECT => eval_instanceof_object_target_name(class_name, context, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the runtime class name for `$object::class` and rejects non-object dynamic receivers. -fn eval_dynamic_class_name_fetch_result( - class_name: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(class_name)?; - if tag == EVAL_TAG_OBJECT { - let class_name = eval_instanceof_object_target_name(class_name, context, values)?; - return values.string(&class_name); - } - eval_throw_type_error( - &format!( - "Cannot use \"::class\" on {}", - eval_class_name_fetch_type_error_name(tag) - ), - context, - values, - ) -} - -/// Returns PHP's type label for dynamic `::class` TypeError diagnostics. -fn eval_class_name_fetch_type_error_name(tag: u64) -> &'static str { - match tag { - EVAL_TAG_INT => "int", - EVAL_TAG_FLOAT => "float", - EVAL_TAG_STRING => "string", - EVAL_TAG_BOOL => "bool", - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", - EVAL_TAG_RESOURCE => "resource", - EVAL_TAG_NULL => "null", - _ => "null", - } -} - -/// Evaluates PHP's `instanceof` operator over static and dynamic class targets. -fn eval_instanceof_expr( - value: &EvalExpr, - target: &EvalInstanceOfTarget, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_expr(value, context, scope, values)?; - let result = match target { - EvalInstanceOfTarget::ClassName(class_name) => { - if values.type_tag(value)? != EVAL_TAG_OBJECT { - return values.bool_value(false); - } - let target_class = eval_instanceof_static_target_name(class_name, context)?; - eval_instanceof_object_result(value, &target_class, context, values)? - } - EvalInstanceOfTarget::Expr(target) => { - let target = eval_expr(target, context, scope, values)?; - let target_class = eval_instanceof_dynamic_target_name(target, context, values)?; - if values.type_tag(value)? == EVAL_TAG_OBJECT { - eval_instanceof_object_result(value, &target_class, context, values)? - } else { - false - } - } - }; - values.bool_value(result) -} - -/// Resolves a static `instanceof` target according to eval class aliases and scope keywords. -fn eval_instanceof_static_target_name( - class_name: &str, - context: &ElephcEvalContext, -) -> Result { - match class_name.to_ascii_lowercase().as_str() { - "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), - _ => Ok(eval_instanceof_resolved_target_name(class_name, context)), - } -} - -/// Resolves a dynamic `instanceof` target cell to the PHP class name it represents. -fn eval_instanceof_dynamic_target_name( - target: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(target)? { - EVAL_TAG_STRING => { - let target = eval_instanceof_string_target_name(target, values)?; - Ok(eval_instanceof_resolved_target_name(&target, context)) - } - EVAL_TAG_OBJECT => eval_instanceof_object_target_name(target, context, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Reads and normalizes one string-valued dynamic `instanceof` target. -fn eval_instanceof_string_target_name( - target: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(target)?; - let target = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(target.trim_start_matches('\\').to_string()) -} - -/// Reads the runtime class of an object-valued dynamic `instanceof` target. -fn eval_instanceof_object_target_name( - target: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(target)?; - if let Some(class) = context.dynamic_object_class(identity) { - return Ok(class.name().to_string()); - } - let class_name = values.object_class_name(target)?; - let bytes = values.string_bytes(class_name); - values.release(class_name)?; - let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(class_name.trim_start_matches('\\').to_string()) -} - -/// Applies eval alias resolution to a target class name without requiring it to exist. -fn eval_instanceof_resolved_target_name(target: &str, context: &ElephcEvalContext) -> String { - context - .resolve_class_name(target) - .unwrap_or_else(|| target.trim_start_matches('\\').to_string()) -} - -/// Tests one object cell against a resolved `instanceof` target class/interface name. -fn eval_instanceof_object_result( - value: RuntimeCellHandle, - target_class: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - dynamic_object_is_a(value, target_class, false, context, values)? - .map_or_else(|| values.object_is_a(value, target_class, false), Ok) -} - -/// Materializes one eval closure literal as a PHP-visible `Closure` object. -fn eval_closure_expr( - function: &EvalFunction, - captures: &[crate::eval_ir::EvalClosureCapture], - is_static: bool, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bindings = Vec::with_capacity(captures.len()); - for capture in captures { - bindings.push(eval_closure_capture(capture, context, scope, values)?); - } - let closure = EvalClosure::new(function.clone(), bindings, is_static); - let name = context.define_closure(closure); - eval_closure_object_expr(EvalClosureObjectTarget::Named(name), context, values) -} - -/// Materializes one PHP-visible `Closure` object for an eval callable target. -fn eval_closure_object_expr( - target: EvalClosureObjectTarget, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = values.new_object("stdClass")?; - let identity = values.object_identity(object)?; - context.register_closure_object_target(identity, target); - Ok(object) -} - -/// Evaluates one closure capture from the defining scope. -fn eval_closure_capture( - capture: &crate::eval_ir::EvalClosureCapture, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if capture.by_ref() { - let expr = EvalExpr::LoadVar(capture.name().to_string()); - let (value, target) = eval_call_arg_value(&expr, context, scope, values)?; - return Ok(EvalClosureCaptureBinding::new( - capture.name(), - value, - target, - )); - } - let value = if let Some(value) = visible_scope_cell(context, scope, capture.name()) { - values.retain(value)? - } else { - values.null()? - }; - Ok(EvalClosureCaptureBinding::new(capture.name(), value, None)) -} - -/// Evaluates a PHP `match` expression with strict comparison and lazy arm values. -pub(in crate::interpreter) fn eval_match_expr( - subject: &EvalExpr, - arms: &[EvalMatchArm], - default: Option<&EvalExpr>, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let subject = eval_expr(subject, context, scope, values)?; - for arm in arms { - for pattern in &arm.patterns { - let pattern = eval_expr(pattern, context, scope, values)?; - let matched = values.compare(EvalBinOp::StrictEq, subject, pattern)?; - if values.truthy(matched)? { - return eval_expr(&arm.value, context, scope, values); - } - } - } - default - .map(|expr| eval_expr(expr, context, scope, values)) - .unwrap_or(Err(EvalStatus::RuntimeFatal)) -} - -/// Returns cloned positional argument expressions, rejecting named arguments. -pub(in crate::interpreter) fn positional_call_arg_exprs( - args: &[EvalCallArg], -) -> Result, EvalStatus> { - if args - .iter() - .any(|arg| arg.name().is_some() || arg.is_spread()) - { - return Err(EvalStatus::RuntimeFatal); - } - Ok(args.iter().map(|arg| arg.value().clone()).collect()) -} - -/// Evaluates method-call arguments, preserving named metadata for eval method binding. -pub(in crate::interpreter) fn eval_method_call_arg_values( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - eval_call_arg_values(args, context, scope, values) -} - -/// Evaluates supported function-like calls from a runtime eval fragment. -pub(in crate::interpreter) fn eval_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_expr_language_construct_name(name) { - let args = positional_call_arg_exprs(args)?; - return eval_positional_expr_call(name, &args, context, scope, values); - } - if name == "flock" { - return eval_builtin_flock(args, context, scope, values); - } - if name == "preg_match" { - return eval_builtin_preg_match_call(args, context, scope, values); - } - if name == "preg_match_all" { - return eval_builtin_preg_match_all_call(args, context, scope, values); - } - if name == "is_callable" { - return eval_builtin_is_callable_call(args, context, scope, values); - } - if matches!(name, "fsockopen" | "pfsockopen") { - return eval_builtin_fsockopen_call(args, context, scope, values); - } - if let Some(result) = eval_date_procedural_alias_call(name, args, context, scope, values)? { - return Ok(result); - } - if name == "stream_select" { - return eval_builtin_stream_select_call(args, context, scope, values); - } - if name == "stream_socket_accept" { - return eval_builtin_stream_socket_accept_call(args, context, scope, values); - } - if name == "stream_socket_recvfrom" { - return eval_builtin_stream_socket_recvfrom_call(args, context, scope, values); - } - if matches!( - name, - "array_pop" - | "array_push" - | "array_shift" - | "array_splice" - | "array_unshift" - | "array_walk" - | "arsort" - | "asort" - | "krsort" - | "ksort" - | "natcasesort" - | "natsort" - | "rsort" - | "shuffle" - | "sort" - | "settype" - | "uasort" - | "uksort" - | "usort" - ) { - return eval_builtin_array_pop_shift_call(name, args, context, scope, values); - } - if eval_php_visible_builtin_exists(name) { - if eval_call_args_are_plain_positional(args) { - let args = positional_call_arg_exprs(args)?; - return eval_positional_expr_call(name, &args, context, scope, values); - } - return eval_builtin_call(name, args, context, scope, values); - } - - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function(&function, args, context, scope, values); - } - if let Some(function) = context.native_function(name) { - return eval_native_function(function, args, context, scope, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Evaluates an unqualified namespaced function call with PHP's global fallback. -pub(in crate::interpreter) fn eval_namespaced_call( - name: &str, - fallback_name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function(&function, args, context, scope, values); - } - if let Some(function) = context.native_function(name) { - return eval_native_function(function, args, context, scope, values); - } - eval_call(fallback_name, args, context, scope, values) -} - -/// Resolves a first-class function callable name with PHP namespace fallback rules. -fn eval_function_callable_expr( - name: &str, - fallback_name: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_function_probe_exists(context, name) { - return eval_closure_object_expr( - EvalClosureObjectTarget::Named(name.trim_start_matches('\\').to_ascii_lowercase()), - context, - values, - ); - } - if let Some(fallback_name) = fallback_name { - if eval_function_probe_exists(context, fallback_name) { - return eval_closure_object_expr( - EvalClosureObjectTarget::Named( - fallback_name.trim_start_matches('\\').to_ascii_lowercase(), - ), - context, - values, - ); - } - } - eval_throw_error( - &format!("Call to undefined function {}()", name.trim_start_matches('\\')), - context, - values, - ) -} - -/// Materializes an invokable-object first-class callable as a PHP `Closure` object. -fn eval_invokable_callable_expr( - object: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = eval_expr(object, context, scope, values)?; - eval_invokable_object_precheck(object, context, values)?; - eval_closure_object_expr( - EvalClosureObjectTarget::InvokableObject { object }, - context, - values, - ) -} - -/// Materializes an object method first-class callable and records captured AOT bridge scope. -fn eval_method_callable_expr( - object: &EvalExpr, - method: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = eval_expr(object, context, scope, values)?; - let method = eval_dynamic_member_name(method, context, scope, values)?; - let target = eval_method_callable_target(object, method, context, values)?; - eval_closure_object_expr(target, context, values) -} - -/// Validates and builds the retained target for an object method first-class callable. -fn eval_method_callable_target( - object: RuntimeCellHandle, - method_name: String, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Ok(identity) = values.object_identity(object) else { - return Ok(EvalClosureObjectTarget::ObjectMethod { - object, - method: method_name, - called_class: None, - native_class: None, - bridge_scope: None, - }); - }; - let Some(class) = context.dynamic_object_class(identity) else { - let class_name = runtime_object_class_name(object, values)?; - return eval_native_object_method_callable_target( - object, - &class_name, - &class_name, - method_name, - context, - values, - ); - }; - let called_class_name = class.name().to_string(); - if let Some((declaring_class, method)) = - eval_dynamic_method_for_call(&called_class_name, &method_name, context) - { - if method.is_abstract() { - return eval_first_class_abstract_method_error( - &declaring_class, - method.name(), - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() - && !eval_instance_magic_callable_for_class(&called_class_name, context) - { - return eval_first_class_method_access_error( - &declaring_class, - method.name(), - method.visibility(), - context, - values, - ); - } - return Ok(EvalClosureObjectTarget::ObjectMethod { - object, - method: method_name, - called_class: Some(called_class_name), - native_class: None, - bridge_scope: None, - }); - } - if let Some(parent) = context.class_native_parent_name(&called_class_name) { - return eval_native_object_method_callable_target( - object, - &parent, - &called_class_name, - method_name, - context, - values, - ); - } - if eval_instance_magic_callable_for_class(&called_class_name, context) { - return Ok(EvalClosureObjectTarget::ObjectMethod { - object, - method: method_name, - called_class: Some(called_class_name), - native_class: None, - bridge_scope: None, - }); - } - eval_first_class_undefined_method_error(&called_class_name, &method_name, context, values) -} - -/// Validates generated/AOT object-method first-class callable metadata. -fn eval_native_object_method_callable_target( - object: RuntimeCellHandle, - native_class: &str, - called_class_name: &str, - method_name: String, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, visibility, _, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(native_class, &method_name, context, values)? - else { - if eval_native_instance_magic_callable_for_class(native_class, context, values)? - || !values.class_exists(native_class)? - { - return Ok(EvalClosureObjectTarget::ObjectMethod { - object, - method: method_name, - called_class: Some(called_class_name.to_string()), - native_class: None, - bridge_scope: None, - }); - } - return eval_first_class_undefined_method_error( - called_class_name, - &method_name, - context, - values, - ); - }; - if is_abstract { - return eval_first_class_abstract_method_error( - &declaring_class, - &method_name, - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - if eval_native_instance_magic_callable_for_class(native_class, context, values)? { - return Ok(EvalClosureObjectTarget::ObjectMethod { - object, - method: method_name, - called_class: Some(called_class_name.to_string()), - native_class: None, - bridge_scope: None, - }); - } - return eval_first_class_method_access_error( - &declaring_class, - &method_name, - visibility, - context, - values, - ); - } - Ok(EvalClosureObjectTarget::ObjectMethod { - object, - method: method_name, - called_class: Some(called_class_name.to_string()), - native_class: Some(native_class.to_string()), - bridge_scope: Some(declaring_class), - }) -} - -/// Materializes a first-class static method callable while retaining late-static metadata. -fn eval_static_method_callable_expr( - class_name: &str, - method: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let receiver = resolve_eval_static_method_receiver(class_name, context)?; - let method = eval_dynamic_member_name(method, context, scope, values)?; - let target = eval_static_method_callable_target( - receiver.dispatch_class, - method, - Some(receiver.called_class), - Some(scope), - context, - values, - )?; - eval_closure_object_expr(target, context, values) -} - -/// Materializes a runtime-class static first-class callable as a PHP `Closure` object. -fn eval_dynamic_static_method_callable_expr( - class_name: &EvalExpr, - method: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let receiver = resolve_eval_static_method_receiver(&class_name, context)?; - let method = eval_dynamic_member_name(method, context, scope, values)?; - let target = eval_static_method_callable_target( - receiver.dispatch_class, - method, - Some(receiver.called_class), - Some(scope), - context, - values, - )?; - eval_closure_object_expr(target, context, values) -} - -/// Validates and builds the retained target for a static-method first-class callable. -fn eval_static_method_callable_target( - dispatch_class: String, - method_name: String, - called_class: Option, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some((declaring_class, method)) = - eval_dynamic_static_method_for_call(&dispatch_class, &method_name, context) - { - if method.is_abstract() { - return eval_first_class_abstract_method_error( - &declaring_class, - method.name(), - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() - && !eval_static_magic_callable_for_class(&dispatch_class, context) - { - return eval_first_class_method_access_error( - &declaring_class, - method.name(), - method.visibility(), - context, - values, - ); - } - if !method.is_static() { - if let Some(object) = eval_static_syntax_instance_receiver( - &dispatch_class, - lexical_scope, - context, - values, - )? { - return Ok(EvalClosureObjectTarget::ObjectMethod { - object, - method: method_name, - called_class, - native_class: None, - bridge_scope: None, - }); - } - return eval_first_class_non_static_method_error( - &declaring_class, - method.name(), - context, - values, - ); - } - return Ok(EvalClosureObjectTarget::StaticMethod { - class_name: dispatch_class, - method: method_name, - called_class, - native_class: None, - bridge_scope: None, - }); - } - if context.has_class(&dispatch_class) { - if let Some(parent) = context.class_native_parent_name(&dispatch_class) { - return eval_native_static_method_callable_target( - dispatch_class, - parent, - method_name, - called_class, - lexical_scope, - context, - values, - ); - } - if eval_static_magic_callable_for_class(&dispatch_class, context) { - return Ok(EvalClosureObjectTarget::StaticMethod { - class_name: dispatch_class, - method: method_name, - called_class, - native_class: None, - bridge_scope: None, - }); - } - return eval_first_class_undefined_method_error( - &dispatch_class, - &method_name, - context, - values, - ); - } - if context.has_interface(&dispatch_class) - || context.has_trait(&dispatch_class) - || context.has_enum(&dispatch_class) - { - if eval_static_magic_callable_for_class(&dispatch_class, context) { - return Ok(EvalClosureObjectTarget::StaticMethod { - class_name: dispatch_class, - method: method_name, - called_class, - native_class: None, - bridge_scope: None, - }); - } - return eval_first_class_undefined_method_error( - &dispatch_class, - &method_name, - context, - values, - ); - } - eval_native_static_method_callable_target( - dispatch_class.clone(), - dispatch_class, - method_name, - called_class, - lexical_scope, - context, - values, - ) -} - -/// Validates generated/AOT static-method first-class callable metadata. -fn eval_native_static_method_callable_target( - dispatch_class: String, - native_class: String, - method_name: String, - called_class: Option, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy( - &native_class, - &method_name, - context, - values, - )? - else { - if context - .native_static_method_signature(&native_class, &method_name) - .is_some() - { - return Ok(EvalClosureObjectTarget::StaticMethod { - class_name: dispatch_class, - method: method_name, - called_class, - native_class: None, - bridge_scope: None, - }); - } - if eval_native_static_magic_callable_for_class(&native_class, context, values)? - || !values.class_exists(&native_class)? - { - return Ok(EvalClosureObjectTarget::StaticMethod { - class_name: dispatch_class, - method: method_name, - called_class, - native_class: None, - bridge_scope: None, - }); - } - return eval_first_class_undefined_method_error( - &dispatch_class, - &method_name, - context, - values, - ); - }; - if is_abstract { - return eval_first_class_abstract_method_error( - &declaring_class, - &method_name, - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - if eval_native_static_magic_callable_for_class(&native_class, context, values)? { - return Ok(EvalClosureObjectTarget::StaticMethod { - class_name: dispatch_class, - method: method_name, - called_class, - native_class: None, - bridge_scope: None, - }); - } - return eval_first_class_method_access_error( - &declaring_class, - &method_name, - visibility, - context, - values, - ); - } - if !is_static { - if let Some(object) = eval_static_syntax_instance_receiver( - &dispatch_class, - lexical_scope, - context, - values, - )? { - return Ok(EvalClosureObjectTarget::ObjectMethod { - object, - method: method_name, - called_class, - native_class: Some(native_class), - bridge_scope: Some(declaring_class), - }); - } - return eval_first_class_non_static_method_error( - &declaring_class, - &method_name, - context, - values, - ); - } - Ok(EvalClosureObjectTarget::StaticMethod { - class_name: dispatch_class, - method: method_name, - called_class, - native_class: Some(native_class), - bridge_scope: Some(declaring_class), - }) -} - -/// Returns whether an eval class has an instance magic-call fallback for a callable. -fn eval_instance_magic_callable_for_class( - class_name: &str, - context: &ElephcEvalContext, -) -> bool { - context - .class_method(class_name, "__call") - .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) -} - -/// Returns whether an eval class has a static magic-call fallback for a callable. -fn eval_static_magic_callable_for_class( - class_name: &str, - context: &ElephcEvalContext, -) -> bool { - context - .class_method(class_name, "__callStatic") - .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) -} - -/// Returns whether an AOT class has an instance magic-call fallback for a callable. -fn eval_native_instance_magic_callable_for_class( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? - .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) -} - -/// Returns whether an AOT class has a static magic-call fallback for a callable. -fn eval_native_static_magic_callable_for_class( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok( - eval_aot_method_dispatch_metadata_in_hierarchy( - class_name, - "__callStatic", - context, - values, - )? - .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), - ) -} - -/// Throws PHP's first-class callable error for an inaccessible method. -fn eval_first_class_method_access_error( - declaring_class: &str, - method_name: &str, - visibility: EvalVisibility, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Call to {} method {}::{}() from {}", - eval_callable_visibility_label(visibility), - declaring_class.trim_start_matches('\\'), - method_name, - eval_callable_scope_label(context) - ), - context, - values, - ) -} - -/// Throws PHP's first-class callable error for an instance method used statically. -fn eval_first_class_non_static_method_error( - declaring_class: &str, - method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Non-static method {}::{}() cannot be called statically", - declaring_class.trim_start_matches('\\'), - method_name - ), - context, - values, - ) -} - -/// Throws PHP's first-class callable error for an abstract method target. -fn eval_first_class_abstract_method_error( - declaring_class: &str, - method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Cannot call abstract method {}::{}()", - declaring_class.trim_start_matches('\\'), - method_name - ), - context, - values, - ) -} - -/// Throws PHP's first-class callable error for a missing method target. -fn eval_first_class_undefined_method_error( - class_name: &str, - method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Call to undefined method {}::{}()", - class_name.trim_start_matches('\\'), - method_name - ), - context, - values, - ) -} - -/// Returns the current PHP scope label used in callable access errors. -fn eval_callable_scope_label(context: &ElephcEvalContext) -> String { - context.current_class_scope().map_or_else( - || String::from("global scope"), - |class_name| format!("scope {}", class_name.trim_start_matches('\\')), - ) -} - -/// Returns PHP's lowercase visibility label for callable access errors. -fn eval_callable_visibility_label(visibility: EvalVisibility) -> &'static str { - match visibility { - EvalVisibility::Public => "public", - EvalVisibility::Protected => "protected", - EvalVisibility::Private => "private", - } -} - -/// Evaluates a variable or expression callable and dispatches it with source-order arguments. -pub(in crate::interpreter) fn eval_dynamic_call( - callee: &EvalExpr, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_expr(callee, context, scope, values)?; - if values.type_tag(callback)? == EVAL_TAG_OBJECT { - let is_closure_object = values - .object_identity(callback) - .ok() - .and_then(|identity| context.closure_object_target(identity)) - .is_some(); - if !is_closure_object { - eval_invokable_object_precheck(callback, context, values)?; - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - return eval_invokable_object_call_result(callback, evaluated_args, context, values); - } - } - let callback = eval_callable(callback, context, values)?; - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) -} - -/// Returns true for language constructs that need unevaluated argument expressions. -pub(in crate::interpreter) fn eval_expr_language_construct_name(name: &str) -> bool { - matches!(name, "empty" | "eval" | "isset" | "unset") -} - -/// Returns true when every source argument is plain positional. -pub(in crate::interpreter) fn eval_call_args_are_plain_positional(args: &[EvalCallArg]) -> bool { - args.iter() - .all(|arg| arg.name().is_none() && !arg.is_spread()) -} - -/// Evaluates registry-backed direct builtins and language constructs after positional-only validation. -pub(in crate::interpreter) fn eval_positional_expr_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if name == "eval" { - return eval_nested_eval(args, context, scope, values); - } - - if let Some(result) = eval_declared_builtin_direct_call(name, args, context, scope, values)? { - return Ok(result); - } - - Err(EvalStatus::UnsupportedConstruct) -} diff --git a/crates/elephc-magician/src/interpreter/expressions/calls.rs b/crates/elephc-magician/src/interpreter/expressions/calls.rs new file mode 100644 index 0000000000..f3e4161380 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions/calls.rs @@ -0,0 +1,196 @@ +//! Purpose: +//! Evaluates function-like EvalIR calls and first-class callable expressions. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_expr()` for call-shaped expressions. +//! +//! Key details: +//! - Source-sensitive constructs and by-reference builtins keep unevaluated or +//! ref-target arguments before ordinary registry direct-call dispatch. +//! - Dynamic callables preserve PHP source-order argument evaluation before +//! normalized callable invocation. + +use super::*; + +mod first_class; +mod first_class_support; + +pub(in crate::interpreter) use first_class::*; +use first_class_support::*; + +/// Returns cloned positional argument expressions, rejecting named arguments. +pub(in crate::interpreter) fn positional_call_arg_exprs( + args: &[EvalCallArg], +) -> Result, EvalStatus> { + if args + .iter() + .any(|arg| arg.name().is_some() || arg.is_spread()) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(args.iter().map(|arg| arg.value().clone()).collect()) +} + +/// Evaluates method-call arguments, preserving named metadata for eval method binding. +pub(in crate::interpreter) fn eval_method_call_arg_values( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_call_arg_values(args, context, scope, values) +} + +/// Evaluates supported function-like calls from a runtime eval fragment. +pub(in crate::interpreter) fn eval_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_expr_language_construct_name(name) { + let args = positional_call_arg_exprs(args)?; + return eval_positional_expr_call(name, &args, context, scope, values); + } + if name == "flock" { + return eval_builtin_flock(args, context, scope, values); + } + if name == "preg_match" { + return eval_builtin_preg_match_call(args, context, scope, values); + } + if name == "preg_match_all" { + return eval_builtin_preg_match_all_call(args, context, scope, values); + } + if name == "is_callable" { + return eval_builtin_is_callable_call(args, context, scope, values); + } + if matches!(name, "fsockopen" | "pfsockopen") { + return eval_builtin_fsockopen_call(args, context, scope, values); + } + if let Some(result) = eval_date_procedural_alias_call(name, args, context, scope, values)? { + return Ok(result); + } + if name == "stream_select" { + return eval_builtin_stream_select_call(args, context, scope, values); + } + if name == "stream_socket_accept" { + return eval_builtin_stream_socket_accept_call(args, context, scope, values); + } + if name == "stream_socket_recvfrom" { + return eval_builtin_stream_socket_recvfrom_call(args, context, scope, values); + } + if matches!( + name, + "array_pop" + | "array_push" + | "array_shift" + | "array_splice" + | "array_unshift" + | "array_walk" + | "arsort" + | "asort" + | "krsort" + | "ksort" + | "natcasesort" + | "natsort" + | "rsort" + | "shuffle" + | "sort" + | "settype" + | "uasort" + | "uksort" + | "usort" + ) { + return eval_builtin_array_pop_shift_call(name, args, context, scope, values); + } + if eval_php_visible_builtin_exists(name) { + if eval_call_args_are_plain_positional(args) { + let args = positional_call_arg_exprs(args)?; + return eval_positional_expr_call(name, &args, context, scope, values); + } + return eval_builtin_call(name, args, context, scope, values); + } + + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Evaluates an unqualified namespaced function call with PHP's global fallback. +pub(in crate::interpreter) fn eval_namespaced_call( + name: &str, + fallback_name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function(&function, args, context, scope, values); + } + if let Some(function) = context.native_function(name) { + return eval_native_function(function, args, context, scope, values); + } + eval_call(fallback_name, args, context, scope, values) +} + +/// Evaluates a variable or expression callable and dispatches it with source-order arguments. +pub(in crate::interpreter) fn eval_dynamic_call( + callee: &EvalExpr, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_expr(callee, context, scope, values)?; + if values.type_tag(callback)? == EVAL_TAG_OBJECT { + let is_closure_object = values + .object_identity(callback) + .ok() + .and_then(|identity| context.closure_object_target(identity)) + .is_some(); + if !is_closure_object { + eval_invokable_object_precheck(callback, context, values)?; + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + return eval_invokable_object_call_result(callback, evaluated_args, context, values); + } + } + let callback = eval_callable(callback, context, values)?; + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) +} + +/// Returns true for language constructs that need unevaluated argument expressions. +pub(in crate::interpreter) fn eval_expr_language_construct_name(name: &str) -> bool { + matches!(name, "empty" | "eval" | "isset" | "unset") +} + +/// Returns true when every source argument is plain positional. +pub(in crate::interpreter) fn eval_call_args_are_plain_positional(args: &[EvalCallArg]) -> bool { + args.iter() + .all(|arg| arg.name().is_none() && !arg.is_spread()) +} + +/// Evaluates registry-backed direct builtins and language constructs after positional-only validation. +pub(in crate::interpreter) fn eval_positional_expr_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "eval" { + return eval_nested_eval(args, context, scope, values); + } + + if let Some(result) = eval_declared_builtin_direct_call(name, args, context, scope, values)? { + return Ok(result); + } + + Err(EvalStatus::UnsupportedConstruct) +} diff --git a/crates/elephc-magician/src/interpreter/expressions/calls/first_class.rs b/crates/elephc-magician/src/interpreter/expressions/calls/first_class.rs new file mode 100644 index 0000000000..34d3917b9f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions/calls/first_class.rs @@ -0,0 +1,487 @@ +//! Purpose: +//! Materializes PHP first-class callable expressions into eval Closure objects. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_expr()` through `expressions::calls`. +//! +//! Key details: +//! - Object and static-method callables validate visibility, abstract methods, +//! late-static receiver metadata, and generated/AOT bridge scope before capture. + +use super::*; + +/// Resolves a first-class function callable name with PHP namespace fallback rules. +pub(in crate::interpreter) fn eval_function_callable_expr( + name: &str, + fallback_name: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_function_probe_exists(context, name) { + return eval_closure_object_expr( + EvalClosureObjectTarget::Named(name.trim_start_matches('\\').to_ascii_lowercase()), + context, + values, + ); + } + if let Some(fallback_name) = fallback_name { + if eval_function_probe_exists(context, fallback_name) { + return eval_closure_object_expr( + EvalClosureObjectTarget::Named( + fallback_name.trim_start_matches('\\').to_ascii_lowercase(), + ), + context, + values, + ); + } + } + eval_throw_error( + &format!("Call to undefined function {}()", name.trim_start_matches('\\')), + context, + values, + ) +} + +/// Materializes an invokable-object first-class callable as a PHP `Closure` object. +pub(in crate::interpreter) fn eval_invokable_callable_expr( + object: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_expr(object, context, scope, values)?; + eval_invokable_object_precheck(object, context, values)?; + eval_closure_object_expr( + EvalClosureObjectTarget::InvokableObject { object }, + context, + values, + ) +} + +/// Materializes an object method first-class callable and records captured AOT bridge scope. +pub(in crate::interpreter) fn eval_method_callable_expr( + object: &EvalExpr, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_expr(object, context, scope, values)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let target = eval_method_callable_target(object, method, context, values)?; + eval_closure_object_expr(target, context, values) +} + +/// Validates and builds the retained target for an object method first-class callable. +fn eval_method_callable_target( + object: RuntimeCellHandle, + method_name: String, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: None, + native_class: None, + bridge_scope: None, + }); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + return eval_native_object_method_callable_target( + object, + &class_name, + &class_name, + method_name, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_method_for_call(&called_class_name, &method_name, context) + { + if method.is_abstract() { + return eval_first_class_abstract_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + && !eval_instance_magic_callable_for_class(&called_class_name, context) + { + return eval_first_class_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name), + native_class: None, + bridge_scope: None, + }); + } + if let Some(parent) = context.class_native_parent_name(&called_class_name) { + return eval_native_object_method_callable_target( + object, + &parent, + &called_class_name, + method_name, + context, + values, + ); + } + if eval_instance_magic_callable_for_class(&called_class_name, context) { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name), + native_class: None, + bridge_scope: None, + }); + } + eval_first_class_undefined_method_error(&called_class_name, &method_name, context, values) +} + +/// Validates generated/AOT object-method first-class callable metadata. +fn eval_native_object_method_callable_target( + object: RuntimeCellHandle, + native_class: &str, + called_class_name: &str, + method_name: String, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(native_class, &method_name, context, values)? + else { + if eval_native_instance_magic_callable_for_class(native_class, context, values)? + || !values.class_exists(native_class)? + { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + called_class_name, + &method_name, + context, + values, + ); + }; + if is_abstract { + return eval_first_class_abstract_method_error( + &declaring_class, + &method_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if eval_native_instance_magic_callable_for_class(native_class, context, values)? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_method_access_error( + &declaring_class, + &method_name, + visibility, + context, + values, + ); + } + Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class: Some(called_class_name.to_string()), + native_class: Some(native_class.to_string()), + bridge_scope: Some(declaring_class), + }) +} + +/// Materializes a first-class static method callable while retaining late-static metadata. +pub(in crate::interpreter) fn eval_static_method_callable_expr( + class_name: &str, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let target = eval_static_method_callable_target( + receiver.dispatch_class, + method, + Some(receiver.called_class), + Some(scope), + context, + values, + )?; + eval_closure_object_expr(target, context, values) +} + +/// Materializes a runtime-class static first-class callable as a PHP `Closure` object. +pub(in crate::interpreter) fn eval_dynamic_static_method_callable_expr( + class_name: &EvalExpr, + method: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let receiver = resolve_eval_static_method_receiver(&class_name, context)?; + let method = eval_dynamic_member_name(method, context, scope, values)?; + let target = eval_static_method_callable_target( + receiver.dispatch_class, + method, + Some(receiver.called_class), + Some(scope), + context, + values, + )?; + eval_closure_object_expr(target, context, values) +} + +/// Validates and builds the retained target for a static-method first-class callable. +fn eval_static_method_callable_target( + dispatch_class: String, + method_name: String, + called_class: Option, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some((declaring_class, method)) = + eval_dynamic_static_method_for_call(&dispatch_class, &method_name, context) + { + if method.is_abstract() { + return eval_first_class_abstract_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + && !eval_static_magic_callable_for_class(&dispatch_class, context) + { + return eval_first_class_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + if !method.is_static() { + if let Some(object) = eval_static_syntax_instance_receiver( + &dispatch_class, + lexical_scope, + context, + values, + )? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_non_static_method_error( + &declaring_class, + method.name(), + context, + values, + ); + } + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + if context.has_class(&dispatch_class) { + if let Some(parent) = context.class_native_parent_name(&dispatch_class) { + return eval_native_static_method_callable_target( + dispatch_class, + parent, + method_name, + called_class, + lexical_scope, + context, + values, + ); + } + if eval_static_magic_callable_for_class(&dispatch_class, context) { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); + } + if context.has_interface(&dispatch_class) + || context.has_trait(&dispatch_class) + || context.has_enum(&dispatch_class) + { + if eval_static_magic_callable_for_class(&dispatch_class, context) { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); + } + eval_native_static_method_callable_target( + dispatch_class.clone(), + dispatch_class, + method_name, + called_class, + lexical_scope, + context, + values, + ) +} + +/// Validates generated/AOT static-method first-class callable metadata. +fn eval_native_static_method_callable_target( + dispatch_class: String, + native_class: String, + method_name: String, + called_class: Option, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + &method_name, + context, + values, + )? + else { + if context + .native_static_method_signature(&native_class, &method_name) + .is_some() + { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + if eval_native_static_magic_callable_for_class(&native_class, context, values)? + || !values.class_exists(&native_class)? + { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_undefined_method_error( + &dispatch_class, + &method_name, + context, + values, + ); + }; + if is_abstract { + return eval_first_class_abstract_method_error( + &declaring_class, + &method_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if eval_native_static_magic_callable_for_class(&native_class, context, values)? { + return Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: None, + bridge_scope: None, + }); + } + return eval_first_class_method_access_error( + &declaring_class, + &method_name, + visibility, + context, + values, + ); + } + if !is_static { + if let Some(object) = eval_static_syntax_instance_receiver( + &dispatch_class, + lexical_scope, + context, + values, + )? { + return Ok(EvalClosureObjectTarget::ObjectMethod { + object, + method: method_name, + called_class, + native_class: Some(native_class), + bridge_scope: Some(declaring_class), + }); + } + return eval_first_class_non_static_method_error( + &declaring_class, + &method_name, + context, + values, + ); + } + Ok(EvalClosureObjectTarget::StaticMethod { + class_name: dispatch_class, + method: method_name, + called_class, + native_class: Some(native_class), + bridge_scope: Some(declaring_class), + }) +} diff --git a/crates/elephc-magician/src/interpreter/expressions/calls/first_class_support.rs b/crates/elephc-magician/src/interpreter/expressions/calls/first_class_support.rs new file mode 100644 index 0000000000..b5634bfde2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions/calls/first_class_support.rs @@ -0,0 +1,150 @@ +//! Purpose: +//! Shared magic-call probes and TypeError helpers for first-class callables. +//! +//! Called from: +//! - `crate::interpreter::expressions::calls::first_class`. +//! +//! Key details: +//! - Error messages preserve PHP visibility labels and current eval class scope. +//! - AOT magic-call probes use generated method dispatch metadata. + +use super::*; + +/// Returns whether an eval class has an instance magic-call fallback for a callable. +pub(super) fn eval_instance_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a static magic-call fallback for a callable. +pub(super) fn eval_static_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) +} + +/// Returns whether an AOT class has an instance magic-call fallback for a callable. +pub(super) fn eval_native_instance_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether an AOT class has a static magic-call fallback for a callable. +pub(super) fn eval_native_static_magic_callable_for_class( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Throws PHP's first-class callable error for an inaccessible method. +pub(super) fn eval_first_class_method_access_error( + declaring_class: &str, + method_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} method {}::{}() from {}", + eval_callable_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + method_name, + eval_callable_scope_label(context) + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for an instance method used statically. +pub(super) fn eval_first_class_non_static_method_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Non-static method {}::{}() cannot be called statically", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for an abstract method target. +pub(super) fn eval_first_class_abstract_method_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot call abstract method {}::{}()", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's first-class callable error for a missing method target. +pub(super) fn eval_first_class_undefined_method_error( + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to undefined method {}::{}()", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Returns the current PHP scope label used in callable access errors. +fn eval_callable_scope_label(context: &ElephcEvalContext) -> String { + context.current_class_scope().map_or_else( + || String::from("global scope"), + |class_name| format!("scope {}", class_name.trim_start_matches('\\')), + ) +} + +/// Returns PHP's lowercase visibility label for callable access errors. +fn eval_callable_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} diff --git a/crates/elephc-magician/src/interpreter/expressions/evaluation.rs b/crates/elephc-magician/src/interpreter/expressions/evaluation.rs new file mode 100644 index 0000000000..810cf0dd46 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/expressions/evaluation.rs @@ -0,0 +1,391 @@ +//! Purpose: +//! Shared evaluated-expression helpers for operators, class names, closures, and match. +//! +//! Called from: +//! - `crate::interpreter::expressions::eval_expr()`. +//! +//! Key details: +//! - Helpers operate after the parent evaluator has selected the expression shape +//! and preserve PHP runtime conversion, class-alias, and closure-capture rules. + +use super::*; + +/// Applies one already-evaluated binary operation with eval runtime semantics. +pub(in crate::interpreter) fn eval_binary_result( + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match op { + EvalBinOp::Add => values.add(left, right), + EvalBinOp::Sub => values.sub(left, right), + EvalBinOp::Mul => values.mul(left, right), + EvalBinOp::Div => values.div(left, right), + EvalBinOp::Mod => values.modulo(left, right), + EvalBinOp::Pow => values.pow(left, right), + EvalBinOp::BitAnd + | EvalBinOp::BitOr + | EvalBinOp::BitXor + | EvalBinOp::ShiftLeft + | EvalBinOp::ShiftRight => values.bitwise(op, left, right), + EvalBinOp::Concat => { + let left = eval_string_context_value(left, context, values)?; + let right = eval_string_context_value(right, context, values)?; + values.concat(left, right) + } + EvalBinOp::LogicalXor => { + let left_truthy = values.truthy(left)?; + let right_truthy = values.truthy(right)?; + values.bool_value(left_truthy ^ right_truthy) + } + EvalBinOp::LooseEq + | EvalBinOp::LooseNotEq + | EvalBinOp::StrictEq + | EvalBinOp::StrictNotEq + | EvalBinOp::Lt + | EvalBinOp::LtEq + | EvalBinOp::Gt + | EvalBinOp::GtEq => values.compare(op, left, right), + EvalBinOp::Spaceship => values.spaceship(left, right), + EvalBinOp::LogicalAnd | EvalBinOp::LogicalOr => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Evaluates a runtime property or method name expression and returns its PHP string bytes as UTF-8. +pub(in crate::interpreter) fn eval_dynamic_member_name( + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(expr, context, scope, values)?; + let value = eval_string_context_value(value, context, values)?; + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Reads an array element or dispatches `ArrayAccess::offsetGet()` for objects. +pub(in crate::interpreter) fn eval_array_get_result( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(array)? != EVAL_TAG_OBJECT { + if let Some(target) = eval_array_reference_key(index, values)? + .and_then(|key| context.array_element_alias(array, &key).cloned()) + { + return eval_reference_target_value(&target, context, values); + } + return values.array_get(array, index); + } + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_method_call_result(array, "offsetGet", vec![index], context, values) +} + +/// Returns whether an object value satisfies PHP's `ArrayAccess` interface. +pub(in crate::interpreter) fn eval_array_access_object_matches( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, "ArrayAccess", false, context, values)? + .map_or_else(|| values.object_is_a(value, "ArrayAccess", false), Ok) +} + +/// Evaluates one PHP scalar cast expression through the runtime conversion hooks. +pub(super) fn eval_cast_expr( + target: &EvalCastType, + expr: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(expr, context, scope, values)?; + match target { + EvalCastType::Int => values.cast_int(value), + EvalCastType::Float => values.cast_float(value), + EvalCastType::String => { + let value = eval_string_context_value(value, context, values)?; + values.cast_string(value) + } + EvalCastType::Bool => values.cast_bool(value), + } +} + +/// Constructs an object after the target class name and constructor arguments have been evaluated. +pub(super) fn eval_new_object_result( + class_name: &str, + args: Vec, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(object) = + eval_reflection_owner_new_object(class_name, args.clone(), context, values)? + { + return Ok(object); + } + if let Some(class) = context.class(class_name).cloned() { + return eval_dynamic_class_new_object(&class, args, context, scope, values); + } + let object = values.new_object(class_name)?; + if let Err(err) = + eval_native_constructor_with_evaluated_args(class_name, object, args, context, values) + { + let _ = values.release(object); + return Err(err); + } + Ok(object) +} + +/// Resolves special class names used by `new` while preserving AOT fallback names. +pub(super) fn eval_new_object_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Resolves a runtime class-name value used by dynamic class operations. +pub(in crate::interpreter) fn eval_dynamic_class_name( + class_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(class_name)? { + EVAL_TAG_STRING => { + let bytes = values.string_bytes(class_name)?; + let class_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())) + } + EVAL_TAG_OBJECT => eval_instanceof_object_target_name(class_name, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the runtime class name for `$object::class` and rejects non-object dynamic receivers. +pub(super) fn eval_dynamic_class_name_fetch_result( + class_name: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(class_name)?; + if tag == EVAL_TAG_OBJECT { + let class_name = eval_instanceof_object_target_name(class_name, context, values)?; + return values.string(&class_name); + } + eval_throw_type_error( + &format!( + "Cannot use \"::class\" on {}", + eval_class_name_fetch_type_error_name(tag) + ), + context, + values, + ) +} + +/// Returns PHP's type label for dynamic `::class` TypeError diagnostics. +fn eval_class_name_fetch_type_error_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "int", + EVAL_TAG_FLOAT => "float", + EVAL_TAG_STRING => "string", + EVAL_TAG_BOOL => "bool", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_NULL => "null", + _ => "null", + } +} + +/// Evaluates PHP's `instanceof` operator over static and dynamic class targets. +pub(super) fn eval_instanceof_expr( + value: &EvalExpr, + target: &EvalInstanceOfTarget, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(value, context, scope, values)?; + let result = match target { + EvalInstanceOfTarget::ClassName(class_name) => { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return values.bool_value(false); + } + let target_class = eval_instanceof_static_target_name(class_name, context)?; + eval_instanceof_object_result(value, &target_class, context, values)? + } + EvalInstanceOfTarget::Expr(target) => { + let target = eval_expr(target, context, scope, values)?; + let target_class = eval_instanceof_dynamic_target_name(target, context, values)?; + if values.type_tag(value)? == EVAL_TAG_OBJECT { + eval_instanceof_object_result(value, &target_class, context, values)? + } else { + false + } + } + }; + values.bool_value(result) +} + +/// Resolves a static `instanceof` target according to eval class aliases and scope keywords. +fn eval_instanceof_static_target_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(eval_instanceof_resolved_target_name(class_name, context)), + } +} + +/// Resolves a dynamic `instanceof` target cell to the PHP class name it represents. +fn eval_instanceof_dynamic_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_STRING => { + let target = eval_instanceof_string_target_name(target, values)?; + Ok(eval_instanceof_resolved_target_name(&target, context)) + } + EVAL_TAG_OBJECT => eval_instanceof_object_target_name(target, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads and normalizes one string-valued dynamic `instanceof` target. +fn eval_instanceof_string_target_name( + target: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(target)?; + let target = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(target.trim_start_matches('\\').to_string()) +} + +/// Reads the runtime class of an object-valued dynamic `instanceof` target. +fn eval_instanceof_object_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(target)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + let class_name = values.object_class_name(target)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Applies eval alias resolution to a target class name without requiring it to exist. +fn eval_instanceof_resolved_target_name(target: &str, context: &ElephcEvalContext) -> String { + context + .resolve_class_name(target) + .unwrap_or_else(|| target.trim_start_matches('\\').to_string()) +} + +/// Tests one object cell against a resolved `instanceof` target class/interface name. +fn eval_instanceof_object_result( + value: RuntimeCellHandle, + target_class: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, target_class, false, context, values)? + .map_or_else(|| values.object_is_a(value, target_class, false), Ok) +} + +/// Materializes one eval closure literal as a PHP-visible `Closure` object. +pub(super) fn eval_closure_expr( + function: &EvalFunction, + captures: &[crate::eval_ir::EvalClosureCapture], + is_static: bool, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bindings = Vec::with_capacity(captures.len()); + for capture in captures { + bindings.push(eval_closure_capture(capture, context, scope, values)?); + } + let closure = EvalClosure::new(function.clone(), bindings, is_static); + let name = context.define_closure(closure); + eval_closure_object_expr(EvalClosureObjectTarget::Named(name), context, values) +} + +/// Materializes one PHP-visible `Closure` object for an eval callable target. +pub(super) fn eval_closure_object_expr( + target: EvalClosureObjectTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_closure_object_target(identity, target); + Ok(object) +} + +/// Evaluates one closure capture from the defining scope. +fn eval_closure_capture( + capture: &crate::eval_ir::EvalClosureCapture, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if capture.by_ref() { + let expr = EvalExpr::LoadVar(capture.name().to_string()); + let (value, target) = eval_call_arg_value(&expr, context, scope, values)?; + return Ok(EvalClosureCaptureBinding::new( + capture.name(), + value, + target, + )); + } + let value = if let Some(value) = visible_scope_cell(context, scope, capture.name()) { + values.retain(value)? + } else { + values.null()? + }; + Ok(EvalClosureCaptureBinding::new(capture.name(), value, None)) +} + +/// Evaluates a PHP `match` expression with strict comparison and lazy arm values. +pub(in crate::interpreter) fn eval_match_expr( + subject: &EvalExpr, + arms: &[EvalMatchArm], + default: Option<&EvalExpr>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let subject = eval_expr(subject, context, scope, values)?; + for arm in arms { + for pattern in &arm.patterns { + let pattern = eval_expr(pattern, context, scope, values)?; + let matched = values.compare(EvalBinOp::StrictEq, subject, pattern)?; + if values.truthy(matched)? { + return eval_expr(&arm.value, context, scope, values); + } + } + } + default + .map(|expr| eval_expr(expr, context, scope, values)) + .unwrap_or(Err(EvalStatus::RuntimeFatal)) +} From 2199a5383df2ff97672071e823d83e0bb14e2dce Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 14:36:55 +0200 Subject: [PATCH 1098/1208] refactor(magician): co-locate array projection builtins --- .../elephc-magician-builtin-structure-plan.md | 36 +++++++++++++-- .../interpreter/builtins/array/array_keys.rs | 42 +++++++++++++++-- .../builtins/array/array_values.rs | 43 +++++++++++++++-- .../src/interpreter/builtins/array/mod.rs | 2 + .../builtins/array/non_mutating.rs | 2 +- .../builtins/arrays/filters/iterator.rs | 3 +- .../builtins/arrays/filters/mod.rs | 2 - .../builtins/arrays/filters/projection.rs | 46 ------------------- .../src/interpreter/builtins/hooks/direct.rs | 16 ++++--- .../src/interpreter/builtins/hooks/values.rs | 20 ++++---- 10 files changed, 132 insertions(+), 80 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/projection.rs diff --git a/.plans/elephc-magician-builtin-structure-plan.md b/.plans/elephc-magician-builtin-structure-plan.md index 69301be49b..b3c31cc66d 100644 --- a/.plans/elephc-magician-builtin-structure-plan.md +++ b/.plans/elephc-magician-builtin-structure-plan.md @@ -23,6 +23,11 @@ paths for language constructs and by-ref/source-sensitive calls. - [x] Phase 3: update agent/contributor documentation if the workflow for adding eval builtins changes. +- [x] Phase 4: convert `array_keys` and `array_values` so each builtin home file + contains both its `eval_builtin!` declaration and its PHP-visible + implementation. +- [ ] Phase 4: merge the remaining declaration-only builtin home files with + their PHP-visible direct/by-value implementations, area by area. ## Goal @@ -101,11 +106,13 @@ Each builtin home file should contain: - a direct-call wrapper over `EvalExpr` arguments, when needed; - a dynamic/by-value wrapper over `RuntimeCellHandle`, when needed; - a mutating/by-ref wrapper, if the builtin can write into caller storage; -- delegation to shared helpers when multiple builtins use the same algorithm. +- the PHP-visible builtin implementation for that entry. -Shared helpers must not become generic buckets. If a helper exceeds 500 LoC but -has one clear scope, its preamble must explain why keeping it cohesive is better -than splitting it mechanically. +Shared helpers are still allowed for non-PHP-visible common algorithms, but they +must not be a separate per-builtin implementation file with a declaration-only +home file forwarding into it. If a helper exceeds 500 LoC but has one clear +scope, its preamble must explain why keeping it cohesive is better than splitting +it mechanically. ## Phase 1: Declarative Registry Without a Big Bang @@ -222,10 +229,31 @@ Phase 3 completion notes: adding eval builtins still follows the existing one-home-file plus area `mod.rs` wiring model established by the declarative registry. +## Phase 4: Merge Declarations with Implementations + +After Phase 3, several builtins still had a home file that only registered +metadata and then delegated to an implementation helper elsewhere. That is no +longer the target model. + +For each migrated builtin: + +- keep the `eval_builtin!` declaration in the builtin home file; +- move the PHP-visible direct wrapper into that same file; +- move the PHP-visible evaluated-argument/result wrapper into that same file; +- keep shared helper modules only when they represent a real common algorithm, + not a one-builtin implementation hidden away from the declaration; +- remove old `declarations/` folders and declaration-only files as each area is + migrated. + +The pilot conversion moved `array_keys` and `array_values` into this stricter +shape and deleted the old shared projection implementation file. + ## Acceptance Criteria - Adding an eval builtin requires one home file and at most area `mod.rs` wiring, not edits to four separate manual tables. +- A migrated builtin's home file is not declaration-only when + `elephc-magician` owns that builtin's implementation. - `elephc_magician::builtin_metadata` is derived from the registry and stays aligned with parity tests. - Parity tests compare the static registry and eval registry, not string diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs index 74f7041978..065dc38ce7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs @@ -1,16 +1,48 @@ //! Purpose: -//! Declarative eval registry entry for `array_keys`. +//! Eval registry entry and implementation for `array_keys`. //! //! Called from: -//! - `crate::interpreter::builtins::array`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the array-projection hook. +//! - Output is always a sequential indexed array containing input keys in +//! iteration order. + +use super::super::super::*; eval_builtin! { name: "array_keys", area: Array, params: [array], - direct: ArrayProjection, - values: ArrayProjection, + direct: ArrayKeys, + values: ArrayKeys, +} + +/// Evaluates PHP `array_keys()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_keys( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_keys_result(array, values) +} + +/// Builds the sequential result array for `array_keys()`. +pub(in crate::interpreter) fn eval_array_keys_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let index = values.int(position as i64)?; + result = values.array_set(result, index, key)?; + } + Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs index 6cc18ad261..a943b426ba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs @@ -1,16 +1,49 @@ //! Purpose: -//! Declarative eval registry entry for `array_values`. +//! Eval registry entry and implementation for `array_values`. //! //! Called from: -//! - `crate::interpreter::builtins::array`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the array-projection hook. +//! - Output is always a sequential indexed array containing input values in +//! iteration order. + +use super::super::super::*; eval_builtin! { name: "array_values", area: Array, params: [array], - direct: ArrayProjection, - values: ArrayProjection, + direct: ArrayValues, + values: ArrayValues, +} + +/// Evaluates PHP `array_values()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_values( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_values_result(array, values) +} + +/// Builds the sequential result array for `array_values()`. +pub(in crate::interpreter) fn eval_array_values_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let index = values.int(position as i64)?; + result = values.array_set(result, index, value)?; + } + Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs index a6a7607989..3e177170f9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -60,5 +60,7 @@ mod uasort; mod uksort; mod usort; +pub(in crate::interpreter) use array_keys::*; +pub(in crate::interpreter) use array_values::*; pub(in crate::interpreter) use mutating::*; pub(in crate::interpreter) use non_mutating::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs b/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs index d8828c69f1..a0a571effd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs @@ -43,7 +43,7 @@ pub(in crate::interpreter) fn eval_builtin_array_call( } /// Dispatches evaluated non-mutating array and iterator calls from declarative specs. -pub(in crate::interpreter) fn eval_array_values_result( +pub(in crate::interpreter) fn eval_array_non_mutating_values_result( name: &str, evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs index c13a75a8ea..901e643c8d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs @@ -10,7 +10,6 @@ use super::super::super::super::*; use super::super::super::*; -use super::*; /// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. pub(in crate::interpreter) fn eval_builtin_iterator_apply( @@ -176,7 +175,7 @@ pub(in crate::interpreter) fn eval_iterator_to_array_result( if preserve_keys { return eval_array_copy_preserve_keys(iterator, values); } - eval_array_projection_result("array_values", iterator, values) + eval_array_values_result(iterator, values) } /// Copies one array-like eval value while preserving iteration keys and order. diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs index 2d898b6de4..04f2db2970 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs @@ -14,7 +14,6 @@ mod filter; mod flip; mod iterator; mod pad; -mod projection; mod reverse; mod slice; mod unique; @@ -24,7 +23,6 @@ pub(in crate::interpreter) use filter::*; pub(in crate::interpreter) use flip::*; pub(in crate::interpreter) use iterator::*; pub(in crate::interpreter) use pad::*; -pub(in crate::interpreter) use projection::*; pub(in crate::interpreter) use reverse::*; pub(in crate::interpreter) use slice::*; pub(in crate::interpreter) use unique::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/projection.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/projection.rs deleted file mode 100644 index 9a58072f4c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/projection.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Purpose: -//! Implements `array_keys()` and `array_values()` eval projections. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` re-exports. -//! -//! Key details: -//! - Projection output is always a sequential indexed runtime array. - -use super::super::super::super::*; - -/// Evaluates PHP array projection builtins over one eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_projection( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_projection_result(name, array, values) -} - -/// Builds the indexed result array for `array_keys()` or `array_values()`. -pub(in crate::interpreter) fn eval_array_projection_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = match name { - "array_keys" => key, - "array_values" => values.array_get(array, key)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let index = values.int(position as i64)?; - result = values.array_set(result, index, value)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 2c91d7aa05..dde99d7a4f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -25,10 +25,11 @@ use super::super::super::{ EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::super::{ - eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_flip, - eval_builtin_array_call, eval_builtin_array_key_exists, eval_builtin_array_pad, - eval_builtin_array_projection, eval_builtin_array_rand, eval_builtin_array_reverse, + eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_call, + eval_builtin_array_flip, eval_builtin_array_key_exists, eval_builtin_array_keys, + eval_builtin_array_pad, eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, eval_builtin_array_slice, eval_builtin_array_unique, + eval_builtin_array_values, eval_builtin_cast, eval_builtin_core_call, eval_builtin_filesystem_call, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_range, @@ -55,8 +56,8 @@ pub(in crate::interpreter) enum EvalDirectHook { ArrayKeyExists, /// Dispatches `array_pad(...)`. ArrayPad, - /// Dispatches `array_keys(...)` and `array_values(...)`. - ArrayProjection, + /// Dispatches `array_keys(...)`. + ArrayKeys, /// Dispatches `array_rand(...)`. ArrayRand, /// Dispatches `array_reverse(...)`. @@ -67,6 +68,8 @@ pub(in crate::interpreter) enum EvalDirectHook { ArraySlice, /// Dispatches `array_unique(...)`. ArrayUnique, + /// Dispatches `array_values(...)`. + ArrayValues, /// Dispatches `base64_decode(...)`. Base64Decode, /// Dispatches `base64_encode(...)`. @@ -220,12 +223,13 @@ impl EvalDirectHook { Self::ArrayFlip => eval_builtin_array_flip(args, context, scope, values), Self::ArrayKeyExists => eval_builtin_array_key_exists(args, context, scope, values), Self::ArrayPad => eval_builtin_array_pad(args, context, scope, values), - Self::ArrayProjection => eval_builtin_array_projection(name, args, context, scope, values), + Self::ArrayKeys => eval_builtin_array_keys(args, context, scope, values), Self::ArrayRand => eval_builtin_array_rand(args, context, scope, values), Self::ArrayReverse => eval_builtin_array_reverse(args, context, scope, values), Self::ArraySearch => eval_builtin_array_search(name, args, context, scope, values), Self::ArraySlice => eval_builtin_array_slice(args, context, scope, values), Self::ArrayUnique => eval_builtin_array_unique(args, context, scope, values), + Self::ArrayValues => eval_builtin_array_values(args, context, scope, values), Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index e947070216..5ef4858dc8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -15,10 +15,11 @@ use super::super::super::{ RuntimeValueOps, }; use super::super::{ - eval_array_aggregate_result, eval_array_flip_result, eval_array_pad_result, - eval_array_projection_result, eval_array_rand_result, eval_array_reverse_result, + eval_array_aggregate_result, eval_array_flip_result, eval_array_keys_result, + eval_array_mutating_values_result, eval_array_non_mutating_values_result, + eval_array_pad_result, eval_array_rand_result, eval_array_reverse_result, eval_array_search_result, eval_array_slice_result, eval_array_unique_result, - eval_array_mutating_values_result, eval_array_values_result, + eval_array_values_result, eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, eval_chr_result, eval_clamp_result, eval_core_values_result, eval_crc32_result, eval_ctype_result, eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, @@ -58,8 +59,8 @@ pub(in crate::interpreter) enum EvalValuesHook { ArrayKeyExists, /// Dispatches `array_pad(...)`. ArrayPad, - /// Dispatches `array_keys(...)` and `array_values(...)`. - ArrayProjection, + /// Dispatches `array_keys(...)`. + ArrayKeys, /// Dispatches `array_rand(...)`. ArrayRand, /// Dispatches `array_reverse(...)`. @@ -70,6 +71,8 @@ pub(in crate::interpreter) enum EvalValuesHook { ArraySlice, /// Dispatches `array_unique(...)`. ArrayUnique, + /// Dispatches `array_values(...)`. + ArrayValues, /// Dispatches `base64_decode(...)`. Base64Decode, /// Dispatches `base64_encode(...)`. @@ -222,7 +225,7 @@ impl EvalValuesHook { Self::ArrayAggregate => one_arg(evaluated_args, values, |array, values| { eval_array_aggregate_result(name, array, values) }), - Self::Array => eval_array_values_result(name, evaluated_args, context, values), + Self::Array => eval_array_non_mutating_values_result(name, evaluated_args, context, values), Self::ArrayMutating => { eval_array_mutating_values_result(name, evaluated_args, context, values) } @@ -231,9 +234,7 @@ impl EvalValuesHook { values.array_key_exists(key, array) }), Self::ArrayPad => three_args(evaluated_args, values, eval_array_pad_result), - Self::ArrayProjection => one_arg(evaluated_args, values, |array, values| { - eval_array_projection_result(name, array, values) - }), + Self::ArrayKeys => one_arg(evaluated_args, values, eval_array_keys_result), Self::ArrayRand => one_arg(evaluated_args, values, eval_array_rand_result), Self::ArrayReverse => match evaluated_args { [array] => eval_array_reverse_result(*array, false, values), @@ -254,6 +255,7 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::ArrayUnique => one_arg(evaluated_args, values, eval_array_unique_result), + Self::ArrayValues => one_arg(evaluated_args, values, eval_array_values_result), Self::Base64Decode => one_arg(evaluated_args, values, eval_base64_decode_result), Self::Base64Encode => one_arg(evaluated_args, values, eval_base64_encode_result), Self::Bin2Hex => one_arg(evaluated_args, values, eval_bin2hex_result), From 0e6269318d11f97efc342ab948c73902b2be971c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 14:56:40 +0200 Subject: [PATCH 1099/1208] refactor(magician): co-locate type builtin implementations --- .../interpreter/builtins/arrays/mutation.rs | 111 +------------- .../src/interpreter/builtins/hooks/direct.rs | 92 ++++++++++-- .../src/interpreter/builtins/hooks/values.rs | 108 +++++++++++--- .../src/interpreter/builtins/mod.rs | 1 + .../src/interpreter/builtins/scalars/types.rs | 136 +----------------- .../src/interpreter/builtins/types/boolval.rs | 35 ++++- .../interpreter/builtins/types/floatval.rs | 35 ++++- .../src/interpreter/builtins/types/gettype.rs | 31 +++- .../src/interpreter/builtins/types/intval.rs | 35 ++++- .../interpreter/builtins/types/is_array.rs | 35 ++++- .../src/interpreter/builtins/types/is_bool.rs | 35 ++++- .../interpreter/builtins/types/is_double.rs | 35 ++++- .../interpreter/builtins/types/is_finite.rs | 35 ++++- .../interpreter/builtins/types/is_float.rs | 35 ++++- .../interpreter/builtins/types/is_infinite.rs | 35 ++++- .../src/interpreter/builtins/types/is_int.rs | 35 ++++- .../interpreter/builtins/types/is_integer.rs | 35 ++++- .../interpreter/builtins/types/is_iterable.rs | 61 +++++++- .../src/interpreter/builtins/types/is_long.rs | 35 ++++- .../src/interpreter/builtins/types/is_nan.rs | 35 ++++- .../src/interpreter/builtins/types/is_null.rs | 35 ++++- .../interpreter/builtins/types/is_numeric.rs | 38 ++++- .../interpreter/builtins/types/is_object.rs | 35 ++++- .../src/interpreter/builtins/types/is_real.rs | 35 ++++- .../interpreter/builtins/types/is_resource.rs | 35 ++++- .../interpreter/builtins/types/is_scalar.rs | 35 ++++- .../interpreter/builtins/types/is_string.rs | 35 ++++- .../src/interpreter/builtins/types/mod.rs | 32 ++++- .../src/interpreter/builtins/types/settype.rs | 130 ++++++++++++++++- .../src/interpreter/builtins/types/strval.rs | 37 ++++- 30 files changed, 1047 insertions(+), 395 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs index 7e26cbafb8..c65e003aee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs @@ -1,5 +1,5 @@ //! Purpose: -//! By-reference settype and array mutation dispatch for eval builtin calls. +//! By-reference array mutation dispatch for eval builtin calls. //! //! Called from: //! - `crate::interpreter::builtins::arrays` re-exports. @@ -11,83 +11,6 @@ use super::super::super::*; use super::super::*; -/// Evaluates direct by-reference `settype()` calls and writes the converted cell back. -pub(in crate::interpreter) fn eval_builtin_settype_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (value, target, type_name) = eval_settype_direct_args(args, context, scope, values)?; - let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { - return values.bool_value(false); - }; - eval_write_direct_ref_target( - &target, - converted, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - values.bool_value(true) -} - -/// Evaluates and binds direct `settype()` arguments while preserving source order. -pub(in crate::interpreter) fn eval_settype_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { - let mut var_target = None; - let mut type_name = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "var", - 1 => "type", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "var" => { - if var_target.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let (value, target) = eval_call_arg_value(arg.value(), context, scope, values)?; - let target = target.ok_or(EvalStatus::RuntimeFatal)?; - var_target = Some((value, target)); - } - "type" => { - if type_name.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - type_name = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let (value, target) = var_target.ok_or(EvalStatus::RuntimeFatal)?; - let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; - Ok((value, target, type_name)) -} - /// Captures the first by-reference array mutator argument as a writable lvalue. pub(in crate::interpreter) fn eval_array_mutation_lvalue_arg( arg: &EvalCallArg, @@ -106,38 +29,6 @@ pub(in crate::interpreter) fn eval_array_mutation_lvalue_arg( Ok((array, target)) } -/// Applies the eval-supported `settype()` scalar target conversion. -pub(in crate::interpreter) fn eval_settype_cast_value( - value: RuntimeCellHandle, - type_name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let type_name = values.string_bytes(type_name)?; - let type_name = String::from_utf8_lossy(&type_name).to_ascii_lowercase(); - let converted = match type_name.as_str() { - "bool" | "boolean" => Some(values.cast_bool(value)?), - "float" | "double" => Some(values.cast_float(value)?), - "int" | "integer" => Some(values.cast_int(value)?), - "string" => Some(values.cast_string(value)?), - _ => None, - }; - Ok(converted) -} - -/// Evaluates by-value `settype()` callable dispatch without mutating the source argument. -pub(in crate::interpreter) fn eval_settype_value_result( - value: RuntimeCellHandle, - type_name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - values.warning("settype(): Argument #1 ($var) must be passed by reference, value given")?; - if let Some(converted) = eval_settype_cast_value(value, type_name, values)? { - values.release(converted)?; - return values.bool_value(true); - } - values.bool_value(false) -} - /// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. pub(in crate::interpreter) fn eval_builtin_array_pop_shift_call( name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index dde99d7a4f..8a27057b47 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -13,14 +13,14 @@ use super::super::super::{ eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, eval_builtin_ceil, eval_builtin_chr, eval_builtin_clamp, eval_builtin_count, eval_builtin_crc32, eval_builtin_ctype, eval_builtin_explode, eval_builtin_float_binary, - eval_builtin_float_pair, eval_builtin_float_unary, eval_builtin_floor, eval_builtin_gettype, + eval_builtin_float_pair, eval_builtin_float_unary, eval_builtin_floor, eval_builtin_formatting_call, eval_builtin_gzip, eval_builtin_hash_algos, eval_builtin_hash_copy, eval_builtin_hash_final, eval_builtin_hash_init, eval_builtin_hash_one_shot, eval_builtin_hash_update, eval_builtin_hex2bin, eval_builtin_implode, eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, eval_builtin_number_format, eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, eval_builtin_rand, eval_builtin_random_int, eval_builtin_round, eval_builtin_slashes, - eval_builtin_sqrt, eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_type_predicate, + eval_builtin_sqrt, eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; @@ -29,16 +29,24 @@ use super::super::{ eval_builtin_array_flip, eval_builtin_array_key_exists, eval_builtin_array_keys, eval_builtin_array_pad, eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, eval_builtin_array_slice, eval_builtin_array_unique, - eval_builtin_array_values, - eval_builtin_cast, eval_builtin_core_call, eval_builtin_filesystem_call, + eval_builtin_array_values, eval_builtin_boolval, eval_builtin_core_call, + eval_builtin_filesystem_call, eval_builtin_floatval, eval_builtin_gettype, + eval_builtin_intval, eval_builtin_is_array, eval_builtin_is_bool, + eval_builtin_is_double, eval_builtin_is_finite, eval_builtin_is_float, + eval_builtin_is_infinite, eval_builtin_is_int, eval_builtin_is_integer, + eval_builtin_is_iterable, eval_builtin_is_long, eval_builtin_is_nan, + eval_builtin_is_null, eval_builtin_is_numeric, eval_builtin_is_object, + eval_builtin_is_real, eval_builtin_is_resource, eval_builtin_is_scalar, + eval_builtin_is_string, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, eval_builtin_json_call, eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_range, eval_builtin_raw_memory_call, eval_builtin_regex_call, eval_builtin_str_pad, eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, - eval_builtin_substr_replace, eval_builtin_symbols_call, eval_builtin_time_call, eval_builtin_trim_like, - eval_builtin_ucwords, eval_builtin_wordwrap, + eval_builtin_strval, eval_builtin_substr_replace, eval_builtin_symbols_call, + eval_builtin_time_call, eval_builtin_trim_like, eval_builtin_ucwords, + eval_builtin_wordwrap, }; /// Direct expression-level dispatch hooks for migrated builtins. @@ -76,8 +84,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Base64Encode, /// Dispatches `bin2hex(...)`. Bin2Hex, - /// Dispatches scalar cast builtins. - Cast, + /// Dispatches `boolval(...)`. + Boolval, /// Dispatches `ceil(...)`. Ceil, /// Dispatches `chr(...)`. @@ -106,6 +114,46 @@ pub(in crate::interpreter) enum EvalDirectHook { Floor, /// Dispatches `gettype(...)`. Gettype, + /// Dispatches `floatval(...)`. + Floatval, + /// Dispatches `intval(...)`. + Intval, + /// Dispatches `is_array(...)`. + IsArray, + /// Dispatches `is_bool(...)`. + IsBool, + /// Dispatches `is_double(...)`. + IsDouble, + /// Dispatches `is_finite(...)`. + IsFinite, + /// Dispatches `is_float(...)`. + IsFloat, + /// Dispatches `is_infinite(...)`. + IsInfinite, + /// Dispatches `is_int(...)`. + IsInt, + /// Dispatches `is_integer(...)`. + IsInteger, + /// Dispatches `is_iterable(...)`. + IsIterable, + /// Dispatches `is_long(...)`. + IsLong, + /// Dispatches `is_nan(...)`. + IsNan, + /// Dispatches `is_null(...)`. + IsNull, + /// Dispatches `is_numeric(...)`. + IsNumeric, + /// Dispatches `is_object(...)`. + IsObject, + /// Dispatches `is_real(...)`. + IsReal, + /// Dispatches `is_resource(...)`. + IsResource, + /// Dispatches `is_scalar(...)`. + IsScalar, + /// Dispatches `is_string(...)`. + IsString, /// Dispatches `grapheme_strrev(...)`. GraphemeStrrev, /// Dispatches gzip/zlib string builtins. @@ -178,6 +226,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Strlen, /// Dispatches `str_repeat(...)`. StrRepeat, + /// Dispatches `strval(...)`. + Strval, /// Dispatches `strrev(...)`. Strrev, /// Dispatches `strstr(...)`. @@ -192,8 +242,6 @@ pub(in crate::interpreter) enum EvalDirectHook { Time, /// Dispatches trim-family builtins. TrimLike, - /// Dispatches scalar and container type predicates. - TypePredicate, /// Dispatches `ucwords(...)`. Ucwords, /// Dispatches `nl2br(...)`. @@ -233,7 +281,7 @@ impl EvalDirectHook { Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), - Self::Cast => eval_builtin_cast(name, args, context, scope, values), + Self::Boolval => eval_builtin_boolval(args, context, scope, values), Self::Ceil => eval_builtin_ceil(args, context, scope, values), Self::Chr => eval_builtin_chr(args, context, scope, values), Self::Clamp => eval_builtin_clamp(args, context, scope, values), @@ -248,6 +296,26 @@ impl EvalDirectHook { Self::Formatting => eval_builtin_formatting_call(name, args, context, scope, values), Self::Floor => eval_builtin_floor(args, context, scope, values), Self::Gettype => eval_builtin_gettype(args, context, scope, values), + Self::Floatval => eval_builtin_floatval(args, context, scope, values), + Self::Intval => eval_builtin_intval(args, context, scope, values), + Self::IsArray => eval_builtin_is_array(args, context, scope, values), + Self::IsBool => eval_builtin_is_bool(args, context, scope, values), + Self::IsDouble => eval_builtin_is_double(args, context, scope, values), + Self::IsFinite => eval_builtin_is_finite(args, context, scope, values), + Self::IsFloat => eval_builtin_is_float(args, context, scope, values), + Self::IsInfinite => eval_builtin_is_infinite(args, context, scope, values), + Self::IsInt => eval_builtin_is_int(args, context, scope, values), + Self::IsInteger => eval_builtin_is_integer(args, context, scope, values), + Self::IsIterable => eval_builtin_is_iterable(args, context, scope, values), + Self::IsLong => eval_builtin_is_long(args, context, scope, values), + Self::IsNan => eval_builtin_is_nan(args, context, scope, values), + Self::IsNull => eval_builtin_is_null(args, context, scope, values), + Self::IsNumeric => eval_builtin_is_numeric(args, context, scope, values), + Self::IsObject => eval_builtin_is_object(args, context, scope, values), + Self::IsReal => eval_builtin_is_real(args, context, scope, values), + Self::IsResource => eval_builtin_is_resource(args, context, scope, values), + Self::IsScalar => eval_builtin_is_scalar(args, context, scope, values), + Self::IsString => eval_builtin_is_string(args, context, scope, values), Self::GraphemeStrrev => eval_builtin_grapheme_strrev(args, context, scope, values), Self::Gzip => eval_builtin_gzip(name, args, context, scope, values), Self::HashAlgos => eval_builtin_hash_algos(args, values), @@ -304,6 +372,7 @@ impl EvalDirectHook { Self::StrSplit => eval_builtin_str_split(args, context, scope, values), Self::Strlen => eval_builtin_strlen(args, context, scope, values), Self::StrRepeat => eval_builtin_str_repeat(args, context, scope, values), + Self::Strval => eval_builtin_strval(args, context, scope, values), Self::Strrev => eval_builtin_strrev(args, context, scope, values), Self::Strstr => eval_builtin_strstr(args, context, scope, values), Self::Substr => eval_builtin_substr(args, context, scope, values), @@ -311,7 +380,6 @@ impl EvalDirectHook { Self::Symbols => eval_builtin_symbols_call(name, args, context, scope, values), Self::Time => eval_builtin_time_call(name, args, context, scope, values), Self::TrimLike => eval_builtin_trim_like(name, args, context, scope, values), - Self::TypePredicate => eval_builtin_type_predicate(name, args, context, scope, values), Self::Ucwords => eval_builtin_ucwords(args, context, scope, values), Self::Nl2br => eval_builtin_nl2br(args, context, scope, values), Self::Wordwrap => eval_builtin_wordwrap(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 5ef4858dc8..8ef46c0835 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -19,21 +19,29 @@ use super::super::{ eval_array_mutating_values_result, eval_array_non_mutating_values_result, eval_array_pad_result, eval_array_rand_result, eval_array_reverse_result, eval_array_search_result, eval_array_slice_result, eval_array_unique_result, - eval_array_values_result, - eval_base64_decode_result, eval_base64_encode_result, eval_bin2hex_result, eval_cast_result, - eval_chr_result, eval_clamp_result, eval_core_values_result, eval_crc32_result, - eval_ctype_result, eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, - eval_float_unary_result, eval_formatting_values_result, eval_gettype_result, + eval_array_values_result, eval_base64_decode_result, eval_base64_encode_result, + eval_bin2hex_result, eval_boolval_result, eval_chr_result, eval_clamp_result, + eval_core_values_result, eval_crc32_result, eval_ctype_result, + eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, + eval_float_unary_result, eval_floatval_result, eval_formatting_values_result, + eval_gettype_result, eval_intval_result, eval_is_array_result, eval_is_bool_result, + eval_is_double_result, eval_is_finite_result, eval_is_float_result, + eval_is_infinite_result, eval_is_int_result, eval_is_integer_result, + eval_is_iterable_result, eval_is_long_result, eval_is_nan_result, + eval_is_null_result, eval_is_numeric_result, eval_is_object_result, + eval_is_real_result, eval_is_resource_result, eval_is_scalar_result, + eval_is_string_result, eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, eval_json_values_result, eval_log_result, eval_min_max_result, eval_network_env_values_result, - eval_nl2br_result, eval_range_result, eval_regex_values_result, eval_settype_value_result, + eval_nl2br_result, eval_range_result, eval_regex_values_result, eval_settype_values_result, eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, - eval_string_search_result, eval_strstr_result, eval_substr_replace_result, eval_substr_result, - eval_raw_memory_values_result, eval_symbols_values_result, eval_time_values_result, eval_trim_like_result, - eval_type_predicate_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, + eval_string_search_result, eval_strstr_result, eval_strval_result, + eval_substr_replace_result, eval_substr_result, eval_raw_memory_values_result, + eval_symbols_values_result, eval_time_values_result, eval_trim_like_result, + eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; use super::arity::{one_arg, three_args, two_args}; @@ -79,8 +87,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Base64Encode, /// Dispatches `bin2hex(...)`. Bin2Hex, - /// Dispatches scalar cast builtins. - Cast, + /// Dispatches `boolval(...)`. + Boolval, /// Dispatches `ceil(...)`. Ceil, /// Dispatches `chr(...)`. @@ -109,6 +117,46 @@ pub(in crate::interpreter) enum EvalValuesHook { Floor, /// Dispatches `gettype(...)`. Gettype, + /// Dispatches `floatval(...)`. + Floatval, + /// Dispatches `intval(...)`. + Intval, + /// Dispatches `is_array(...)`. + IsArray, + /// Dispatches `is_bool(...)`. + IsBool, + /// Dispatches `is_double(...)`. + IsDouble, + /// Dispatches `is_finite(...)`. + IsFinite, + /// Dispatches `is_float(...)`. + IsFloat, + /// Dispatches `is_infinite(...)`. + IsInfinite, + /// Dispatches `is_int(...)`. + IsInt, + /// Dispatches `is_integer(...)`. + IsInteger, + /// Dispatches `is_iterable(...)`. + IsIterable, + /// Dispatches `is_long(...)`. + IsLong, + /// Dispatches `is_nan(...)`. + IsNan, + /// Dispatches `is_null(...)`. + IsNull, + /// Dispatches `is_numeric(...)`. + IsNumeric, + /// Dispatches `is_object(...)`. + IsObject, + /// Dispatches `is_real(...)`. + IsReal, + /// Dispatches `is_resource(...)`. + IsResource, + /// Dispatches `is_scalar(...)`. + IsScalar, + /// Dispatches `is_string(...)`. + IsString, /// Dispatches `grapheme_strrev(...)`. GraphemeStrrev, /// Dispatches gzip/zlib string builtins. @@ -183,6 +231,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Strlen, /// Dispatches `str_repeat(...)`. StrRepeat, + /// Dispatches `strval(...)`. + Strval, /// Dispatches `strrev(...)`. Strrev, /// Dispatches `strstr(...)`. @@ -197,8 +247,6 @@ pub(in crate::interpreter) enum EvalValuesHook { Time, /// Dispatches trim-family builtins. TrimLike, - /// Dispatches scalar and container type predicates. - TypePredicate, /// Dispatches `ucwords(...)`. Ucwords, /// Dispatches `nl2br(...)`. @@ -259,9 +307,7 @@ impl EvalValuesHook { Self::Base64Decode => one_arg(evaluated_args, values, eval_base64_decode_result), Self::Base64Encode => one_arg(evaluated_args, values, eval_base64_encode_result), Self::Bin2Hex => one_arg(evaluated_args, values, eval_bin2hex_result), - Self::Cast => one_arg(evaluated_args, values, |value, values| { - eval_cast_result(name, value, context, values) - }), + Self::Boolval => one_arg(evaluated_args, values, eval_boolval_result), Self::Ceil => one_arg(evaluated_args, values, |value, values| values.ceil(value)), Self::Chr => one_arg(evaluated_args, values, eval_chr_result), Self::Clamp => three_args(evaluated_args, values, eval_clamp_result), @@ -288,6 +334,28 @@ impl EvalValuesHook { Self::Formatting => eval_formatting_values_result(name, evaluated_args, values), Self::Floor => one_arg(evaluated_args, values, |value, values| values.floor(value)), Self::Gettype => one_arg(evaluated_args, values, eval_gettype_result), + Self::Floatval => one_arg(evaluated_args, values, eval_floatval_result), + Self::Intval => one_arg(evaluated_args, values, eval_intval_result), + Self::IsArray => one_arg(evaluated_args, values, eval_is_array_result), + Self::IsBool => one_arg(evaluated_args, values, eval_is_bool_result), + Self::IsDouble => one_arg(evaluated_args, values, eval_is_double_result), + Self::IsFinite => one_arg(evaluated_args, values, eval_is_finite_result), + Self::IsFloat => one_arg(evaluated_args, values, eval_is_float_result), + Self::IsInfinite => one_arg(evaluated_args, values, eval_is_infinite_result), + Self::IsInt => one_arg(evaluated_args, values, eval_is_int_result), + Self::IsInteger => one_arg(evaluated_args, values, eval_is_integer_result), + Self::IsIterable => one_arg(evaluated_args, values, |value, values| { + eval_is_iterable_result(value, context, values) + }), + Self::IsLong => one_arg(evaluated_args, values, eval_is_long_result), + Self::IsNan => one_arg(evaluated_args, values, eval_is_nan_result), + Self::IsNull => one_arg(evaluated_args, values, eval_is_null_result), + Self::IsNumeric => one_arg(evaluated_args, values, eval_is_numeric_result), + Self::IsObject => one_arg(evaluated_args, values, eval_is_object_result), + Self::IsReal => one_arg(evaluated_args, values, eval_is_real_result), + Self::IsResource => one_arg(evaluated_args, values, eval_is_resource_result), + Self::IsScalar => one_arg(evaluated_args, values, eval_is_scalar_result), + Self::IsString => one_arg(evaluated_args, values, eval_is_string_result), Self::GraphemeStrrev => one_arg(evaluated_args, values, eval_grapheme_strrev_result), Self::Gzip => eval_gzip_result(name, evaluated_args, values), Self::HashAlgos => eval_hash_algos_values(evaluated_args, values), @@ -327,7 +395,7 @@ impl EvalValuesHook { Self::Range => two_args(evaluated_args, values, eval_range_result), Self::Regex => eval_regex_values_result(name, evaluated_args, context, values), Self::RawMemory => eval_raw_memory_values_result(name, evaluated_args, context, values), - Self::Settype => two_args(evaluated_args, values, eval_settype_value_result), + Self::Settype => eval_settype_values_result(evaluated_args, values), Self::Slashes => one_arg(evaluated_args, values, |value, values| { eval_slashes_result(name, value, values) }), @@ -381,6 +449,9 @@ impl EvalValuesHook { values.int(len) } Self::StrRepeat => two_args(evaluated_args, values, eval_str_repeat_result), + Self::Strval => one_arg(evaluated_args, values, |value, values| { + eval_strval_result(value, context, values) + }), Self::Strrev => one_arg(evaluated_args, values, |value, values| values.strrev(value)), Self::Strstr => match evaluated_args { [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values), @@ -413,9 +484,6 @@ impl EvalValuesHook { [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values), _ => Err(EvalStatus::RuntimeFatal), }, - Self::TypePredicate => one_arg(evaluated_args, values, |value, values| { - eval_type_predicate_result(name, value, context, values) - }), Self::Ucwords => match evaluated_args { [value] => eval_ucwords_result(*value, None, values), [value, separators] => eval_ucwords_result(*value, Some(*separators), values), diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index af14f6d6af..0476ae3088 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -57,3 +57,4 @@ pub(super) use spl_autoload::*; pub(super) use strings::*; pub(super) use symbols::*; pub(super) use time::*; +pub(super) use types::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs index 80e25a0492..218c9faf20 100644 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs @@ -1,71 +1,14 @@ //! Purpose: -//! Scalar casts, type names, object metadata, and type predicate builtins. +//! Shared scalar type helpers plus class, object, and resource introspection builtins. //! //! Called from: //! - `crate::interpreter::builtins::scalars` re-exports. //! //! Key details: -//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. +//! - Runtime cells remain opaque and shared PHP coercions flow through +//! `RuntimeValueOps`. use super::super::super::*; -use super::super::*; - -/// Evaluates PHP scalar cast builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_cast( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_cast_result(name, value, context, values) -} - -/// Dispatches an already evaluated value through the matching PHP cast hook. -pub(in crate::interpreter) fn eval_cast_result( - name: &str, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "intval" => values.cast_int(value), - "floatval" => values.cast_float(value), - "strval" => { - let value = eval_string_context_value(value, context, values)?; - values.cast_string(value) - } - "boolval" => values.cast_bool(value), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP's `gettype(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_gettype( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_gettype_result(value, values) -} - -/// Converts one boxed runtime tag into PHP's `gettype()` spelling. -pub(in crate::interpreter) fn eval_gettype_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - values.string(eval_gettype_name(tag)) -} /// Evaluates PHP's `get_called_class()` against the current eval method scope. pub(in crate::interpreter) fn eval_builtin_get_called_class( @@ -277,79 +220,6 @@ pub(in crate::interpreter) fn eval_gettype_name(tag: u64) -> &'static str { } } -/// Evaluates PHP scalar/container type predicate builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_type_predicate( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_type_predicate_result(name, value, context, values) -} - -/// Converts a concrete runtime tag into a PHP `is_*` predicate result. -pub(in crate::interpreter) fn eval_type_predicate_result( - name: &str, - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - let result = match name { - "is_int" | "is_integer" | "is_long" => tag == EVAL_TAG_INT, - "is_float" | "is_double" | "is_real" => tag == EVAL_TAG_FLOAT, - "is_string" => tag == EVAL_TAG_STRING, - "is_bool" => tag == EVAL_TAG_BOOL, - "is_null" => tag == EVAL_TAG_NULL, - "is_array" => matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC), - "is_iterable" => eval_is_iterable_value(tag, value, context, values)?, - "is_object" => tag == EVAL_TAG_OBJECT, - "is_resource" => tag == EVAL_TAG_RESOURCE, - "is_scalar" => matches!( - tag, - EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_STRING | EVAL_TAG_BOOL - ), - "is_nan" => eval_float_value(value, values)?.is_nan(), - "is_infinite" => eval_float_value(value, values)?.is_infinite(), - "is_finite" => eval_float_value(value, values)?.is_finite(), - "is_numeric" => { - tag == EVAL_TAG_INT - || tag == EVAL_TAG_FLOAT - || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)) - } - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(result) -} - -/// Returns PHP's `is_iterable()` result for arrays and Traversable-compatible objects. -fn eval_is_iterable_value( - tag: u64, - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Ok(true); - } - if tag != EVAL_TAG_OBJECT { - return Ok(false); - } - for target in ["Traversable", "Iterator", "IteratorAggregate"] { - if dynamic_object_is_a(value, target, false, context, values)? - .map_or_else(|| values.object_is_a(value, target, false), Ok)? - { - return Ok(true); - } - } - Ok(false) -} - /// Matches the static backend's legacy ASCII numeric-string scan. pub(in crate::interpreter) fn eval_is_numeric_string(bytes: &[u8]) -> bool { if bytes.is_empty() { diff --git a/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs b/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs index e91203bbfa..0cb7912194 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `boolval`. +//! Eval registry entry and implementation for `boolval`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing scalar-cast hook. +//! - Cast behavior is implemented here; shared scalar coercions still flow +//! through `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "boolval", area: Types, params: [value], - direct: Cast, - values: Cast, + direct: Boolval, + values: Boolval, +} + +/// Evaluates PHP `boolval()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_boolval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_boolval_result(value, values) +} + +/// Applies PHP `boolval()` to one already evaluated value. +pub(in crate::interpreter) fn eval_boolval_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.cast_bool(value) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs b/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs index 55556021ec..cc33612feb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `floatval`. +//! Eval registry entry and implementation for `floatval`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing scalar-cast hook. +//! - Cast behavior is implemented here; shared scalar coercions still flow +//! through `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "floatval", area: Types, params: [value], - direct: Cast, - values: Cast, + direct: Floatval, + values: Floatval, +} + +/// Evaluates PHP `floatval()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_floatval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_floatval_result(value, values) +} + +/// Applies PHP `floatval()` to one already evaluated value. +pub(in crate::interpreter) fn eval_floatval_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.cast_float(value) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs b/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs index a2b4760e9c..8d75c8e819 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `gettype`. +//! Eval registry entry and implementation for `gettype`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-name hook. +//! - Runtime tags are mapped to PHP's historical `gettype()` names. + +use super::super::super::*; eval_builtin! { name: "gettype", @@ -14,3 +16,26 @@ eval_builtin! { direct: Gettype, values: Gettype, } + +/// Evaluates PHP `gettype()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gettype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_gettype_result(value, values) +} + +/// Converts one boxed runtime tag into PHP's `gettype()` spelling. +pub(in crate::interpreter) fn eval_gettype_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.string(eval_gettype_name(tag)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/intval.rs b/crates/elephc-magician/src/interpreter/builtins/types/intval.rs index 1519ebdda0..6354b892e6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/intval.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/intval.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `intval`. +//! Eval registry entry and implementation for `intval`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing scalar-cast hook. +//! - Cast behavior is implemented here; shared scalar coercions still flow +//! through `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "intval", area: Types, params: [value], - direct: Cast, - values: Cast, + direct: Intval, + values: Intval, +} + +/// Evaluates PHP `intval()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_intval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_intval_result(value, values) +} + +/// Applies PHP `intval()` to one already evaluated value. +pub(in crate::interpreter) fn eval_intval_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.cast_int(value) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs index 324b50104a..3d20d64187 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_array`. +//! Eval registry entry and implementation for `is_array`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_array", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsArray, + values: IsArray, +} + +/// Evaluates PHP `is_array()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_array_result(value, values) +} + +/// Applies PHP `is_array()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_array_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs index f0928c6c5e..0df7c527e5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_bool`. +//! Eval registry entry and implementation for `is_bool`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_bool", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsBool, + values: IsBool, +} + +/// Evaluates PHP `is_bool()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_bool( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_bool_result(value, values) +} + +/// Applies PHP `is_bool()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_bool_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_BOOL) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs index 0b3910dc0e..e9442978a5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_double.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_double`. +//! Eval registry entry and implementation for `is_double`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_double", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsDouble, + values: IsDouble, +} + +/// Evaluates PHP `is_double()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_double( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_double_result(value, values) +} + +/// Applies PHP `is_double()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_double_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_FLOAT) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs index bfda4bc085..5f325e3574 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_finite`. +//! Eval registry entry and implementation for `is_finite`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The argument is coerced with eval numeric semantics before the float check. + +use super::super::super::*; eval_builtin! { name: "is_finite", area: Types, params: [num], - direct: TypePredicate, - values: TypePredicate, + direct: IsFinite, + values: IsFinite, +} + +/// Evaluates PHP `is_finite()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_finite( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_is_finite_result(num, values) +} + +/// Applies PHP `is_finite()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_finite_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_float_value(num, values)?.is_finite(); + values.bool_value(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs index ed1468a313..0512825fb2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_float`. +//! Eval registry entry and implementation for `is_float`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_float", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsFloat, + values: IsFloat, +} + +/// Evaluates PHP `is_float()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_float( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_float_result(value, values) +} + +/// Applies PHP `is_float()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_float_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_FLOAT) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs index c30dcb75e2..62963f58a1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_infinite`. +//! Eval registry entry and implementation for `is_infinite`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The argument is coerced with eval numeric semantics before the float check. + +use super::super::super::*; eval_builtin! { name: "is_infinite", area: Types, params: [num], - direct: TypePredicate, - values: TypePredicate, + direct: IsInfinite, + values: IsInfinite, +} + +/// Evaluates PHP `is_infinite()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_infinite( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_is_infinite_result(num, values) +} + +/// Applies PHP `is_infinite()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_infinite_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_float_value(num, values)?.is_infinite(); + values.bool_value(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs index cfadd63c07..26bd8acc90 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_int`. +//! Eval registry entry and implementation for `is_int`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_int", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsInt, + values: IsInt, +} + +/// Evaluates PHP `is_int()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_int( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_int_result(value, values) +} + +/// Applies PHP `is_int()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_int_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_INT) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs index 09fc8b6e7e..a8db000173 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_integer.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_integer`. +//! Eval registry entry and implementation for `is_integer`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_integer", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsInteger, + values: IsInteger, +} + +/// Evaluates PHP `is_integer()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_integer( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_integer_result(value, values) +} + +/// Applies PHP `is_integer()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_integer_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_INT) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs index ae459e4e78..461bf83a17 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs @@ -1,16 +1,67 @@ //! Purpose: -//! Declarative eval registry entry for `is_iterable`. +//! Eval registry entry and implementation for `is_iterable`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - Arrays are iterable directly; objects are checked against Traversable-style +//! relationships in the current eval context. + +use super::super::super::*; eval_builtin! { name: "is_iterable", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsIterable, + values: IsIterable, +} + +/// Evaluates PHP `is_iterable()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_iterable( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_iterable_result(value, context, values) +} + +/// Applies PHP `is_iterable()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_iterable_result( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + let result = eval_is_iterable_value(tag, value, context, values)?; + values.bool_value(result) +} + +/// Returns PHP's `is_iterable()` result for arrays and Traversable-compatible objects. +fn eval_is_iterable_value( + tag: u64, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Ok(true); + } + if tag != EVAL_TAG_OBJECT { + return Ok(false); + } + for target in ["Traversable", "Iterator", "IteratorAggregate"] { + if dynamic_object_is_a(value, target, false, context, values)? + .map_or_else(|| values.object_is_a(value, target, false), Ok)? + { + return Ok(true); + } + } + Ok(false) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs index 0743ac5d79..de6fb9d769 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_long.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_long`. +//! Eval registry entry and implementation for `is_long`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_long", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsLong, + values: IsLong, +} + +/// Evaluates PHP `is_long()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_long( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_long_result(value, values) +} + +/// Applies PHP `is_long()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_long_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_INT) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs index 7151933d8d..1d083e5063 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_nan`. +//! Eval registry entry and implementation for `is_nan`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The argument is coerced with eval numeric semantics before the float check. + +use super::super::super::*; eval_builtin! { name: "is_nan", area: Types, params: [num], - direct: TypePredicate, - values: TypePredicate, + direct: IsNan, + values: IsNan, +} + +/// Evaluates PHP `is_nan()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_nan( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_is_nan_result(num, values) +} + +/// Applies PHP `is_nan()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_nan_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_float_value(num, values)?.is_nan(); + values.bool_value(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs index 7e42dda224..40035af0b7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_null`. +//! Eval registry entry and implementation for `is_null`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_null", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsNull, + values: IsNull, +} + +/// Evaluates PHP `is_null()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_null( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_null_result(value, values) +} + +/// Applies PHP `is_null()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_null_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_NULL) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs index c8835c6b02..8f47dcd534 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs @@ -1,16 +1,44 @@ //! Purpose: -//! Declarative eval registry entry for `is_numeric`. +//! Eval registry entry and implementation for `is_numeric`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - Numeric strings follow the legacy ASCII scan shared with the static backend. + +use super::super::super::*; eval_builtin! { name: "is_numeric", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsNumeric, + values: IsNumeric, +} + +/// Evaluates PHP `is_numeric()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_numeric( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_numeric_result(value, values) +} + +/// Applies PHP `is_numeric()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_numeric_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + let result = tag == EVAL_TAG_INT + || tag == EVAL_TAG_FLOAT + || (tag == EVAL_TAG_STRING && eval_is_numeric_string(&values.string_bytes(value)?)); + values.bool_value(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs index b6d06d9bd1..10483b88aa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_object`. +//! Eval registry entry and implementation for `is_object`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_object", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsObject, + values: IsObject, +} + +/// Evaluates PHP `is_object()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_object( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_object_result(value, values) +} + +/// Applies PHP `is_object()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_object_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_OBJECT) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs index bf34df22db..d2b0c4cef7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_real.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_real`. +//! Eval registry entry and implementation for `is_real`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_real", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsReal, + values: IsReal, +} + +/// Evaluates PHP `is_real()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_real( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_real_result(value, values) +} + +/// Applies PHP `is_real()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_real_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_FLOAT) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs index 0840018721..5cb27d8a50 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_resource`. +//! Eval registry entry and implementation for `is_resource`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_resource", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsResource, + values: IsResource, +} + +/// Evaluates PHP `is_resource()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_resource( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_resource_result(value, values) +} + +/// Applies PHP `is_resource()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_resource_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_RESOURCE) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs index 67b463dc59..50f02ff9a2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_scalar`. +//! Eval registry entry and implementation for `is_scalar`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_scalar", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsScalar, + values: IsScalar, +} + +/// Evaluates PHP `is_scalar()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_scalar( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_scalar_result(value, values) +} + +/// Applies PHP `is_scalar()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_scalar_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(matches!(tag, EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_STRING | EVAL_TAG_BOOL)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs b/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs index 6f359cad72..48e2bc3a20 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs @@ -1,16 +1,41 @@ //! Purpose: -//! Declarative eval registry entry for `is_string`. +//! Eval registry entry and implementation for `is_string`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing type-predicate hook. +//! - The predicate reads the runtime tag directly and returns a PHP boolean. + +use super::super::super::*; eval_builtin! { name: "is_string", area: Types, params: [value], - direct: TypePredicate, - values: TypePredicate, + direct: IsString, + values: IsString, +} + +/// Evaluates PHP `is_string()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_is_string( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_is_string_result(value, values) +} + +/// Applies PHP `is_string()` to one already evaluated value. +pub(in crate::interpreter) fn eval_is_string_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + values.bool_value(tag == EVAL_TAG_STRING) } diff --git a/crates/elephc-magician/src/interpreter/builtins/types/mod.rs b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs index 176de478ab..a84aa8fdba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/mod.rs @@ -1,12 +1,13 @@ //! Purpose: -//! Per-builtin declarations for scalar type and conversion functions migrated -//! to the eval builtin registry. +//! Per-builtin eval registry entries and implementations for scalar type and +//! conversion functions. //! //! Called from: //! - `crate::interpreter::builtins` module loading. //! //! Key details: -//! - Leaf files register metadata through `eval_builtin!`. +//! - Leaf files register metadata through `eval_builtin!` and own their +//! PHP-visible direct/by-value wrappers. mod boolval; mod floatval; @@ -32,3 +33,28 @@ mod is_scalar; mod is_string; mod settype; mod strval; + +pub(in crate::interpreter) use boolval::*; +pub(in crate::interpreter) use floatval::*; +pub(in crate::interpreter) use gettype::*; +pub(in crate::interpreter) use intval::*; +pub(in crate::interpreter) use is_array::*; +pub(in crate::interpreter) use is_bool::*; +pub(in crate::interpreter) use is_double::*; +pub(in crate::interpreter) use is_finite::*; +pub(in crate::interpreter) use is_float::*; +pub(in crate::interpreter) use is_infinite::*; +pub(in crate::interpreter) use is_int::*; +pub(in crate::interpreter) use is_integer::*; +pub(in crate::interpreter) use is_iterable::*; +pub(in crate::interpreter) use is_long::*; +pub(in crate::interpreter) use is_nan::*; +pub(in crate::interpreter) use is_null::*; +pub(in crate::interpreter) use is_numeric::*; +pub(in crate::interpreter) use is_object::*; +pub(in crate::interpreter) use is_real::*; +pub(in crate::interpreter) use is_resource::*; +pub(in crate::interpreter) use is_scalar::*; +pub(in crate::interpreter) use is_string::*; +pub(in crate::interpreter) use settype::*; +pub(in crate::interpreter) use strval::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/types/settype.rs b/crates/elephc-magician/src/interpreter/builtins/types/settype.rs index 71c86e402b..4fb414201c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/settype.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/settype.rs @@ -1,12 +1,14 @@ //! Purpose: -//! Declarative eval registry entry for `settype`. +//! Eval registry entry and implementation for `settype`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Direct calls stay on the source-sensitive by-reference path because they -//! need writable `EvalCallArg` targets. +//! - Direct calls preserve the writable first argument target and write the +//! converted value back after source-order argument evaluation. + +use super::super::super::*; eval_builtin! { name: "settype", @@ -16,3 +18,123 @@ eval_builtin! { direct: none, values: Settype, } + +/// Evaluates direct by-reference `settype()` calls and writes the converted cell back. +pub(in crate::interpreter) fn eval_builtin_settype_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (value, target, type_name) = eval_settype_direct_args(args, context, scope, values)?; + let Some(converted) = eval_settype_cast_value(value, type_name, values)? else { + return values.bool_value(false); + }; + eval_write_direct_ref_target( + &target, + converted, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + values.bool_value(true) +} + +/// Evaluates and binds direct `settype()` arguments while preserving source order. +pub(in crate::interpreter) fn eval_settype_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut var_target = None; + let mut type_name = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "var", + 1 => "type", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "var" => { + if var_target.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let (value, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + let target = target.ok_or(EvalStatus::RuntimeFatal)?; + var_target = Some((value, target)); + } + "type" => { + if type_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + type_name = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (value, target) = var_target.ok_or(EvalStatus::RuntimeFatal)?; + let type_name = type_name.ok_or(EvalStatus::RuntimeFatal)?; + Ok((value, target, type_name)) +} + +/// Dispatches by-value `settype()` callable calls after argument binding. +pub(in crate::interpreter) fn eval_settype_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_settype_value_result(*value, *type_name, values) +} + +/// Applies the eval-supported `settype()` scalar target conversion. +pub(in crate::interpreter) fn eval_settype_cast_value( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let type_name = values.string_bytes(type_name)?; + let type_name = String::from_utf8_lossy(&type_name).to_ascii_lowercase(); + let converted = match type_name.as_str() { + "bool" | "boolean" => Some(values.cast_bool(value)?), + "float" | "double" => Some(values.cast_float(value)?), + "int" | "integer" => Some(values.cast_int(value)?), + "string" => Some(values.cast_string(value)?), + _ => None, + }; + Ok(converted) +} + +/// Evaluates by-value `settype()` callable dispatch without mutating the source argument. +pub(in crate::interpreter) fn eval_settype_value_result( + value: RuntimeCellHandle, + type_name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.warning("settype(): Argument #1 ($var) must be passed by reference, value given")?; + if let Some(converted) = eval_settype_cast_value(value, type_name, values)? { + values.release(converted)?; + return values.bool_value(true); + } + values.bool_value(false) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/types/strval.rs b/crates/elephc-magician/src/interpreter/builtins/types/strval.rs index e17d9b24e2..038ebc1fea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/types/strval.rs +++ b/crates/elephc-magician/src/interpreter/builtins/types/strval.rs @@ -1,16 +1,43 @@ //! Purpose: -//! Declarative eval registry entry for `strval`. +//! Eval registry entry and implementation for `strval`. //! //! Called from: -//! - `crate::interpreter::builtins::types`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing scalar-cast hook. +//! - Cast behavior is implemented here; shared scalar coercions still flow +//! through `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "strval", area: Types, params: [value], - direct: Cast, - values: Cast, + direct: Strval, + values: Strval, +} + +/// Evaluates PHP `strval()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strval( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_strval_result(value, context, values) +} + +/// Applies PHP `strval()` to one already evaluated value. +pub(in crate::interpreter) fn eval_strval_result( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_string_context_value(value, context, values)?; + values.cast_string(value) } From 5e56199d356a4b22026b290edf009332f8848dbd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 15:10:42 +0200 Subject: [PATCH 1100/1208] refactor(magician): co-locate math builtin implementations --- .../src/interpreter/builtins/arrays/access.rs | 71 ----- .../interpreter/builtins/formatting/math.rs | 86 ------ .../interpreter/builtins/formatting/mod.rs | 4 +- .../src/interpreter/builtins/hooks/direct.rs | 128 ++++++--- .../src/interpreter/builtins/hooks/mod.rs | 1 - .../src/interpreter/builtins/hooks/random.rs | 34 --- .../src/interpreter/builtins/hooks/values.rs | 140 +++++++--- .../src/interpreter/builtins/math/abs.rs | 30 +- .../src/interpreter/builtins/math/acos.rs | 36 ++- .../src/interpreter/builtins/math/asin.rs | 36 ++- .../src/interpreter/builtins/math/atan.rs | 36 ++- .../src/interpreter/builtins/math/atan2.rs | 38 ++- .../src/interpreter/builtins/math/ceil.rs | 30 +- .../src/interpreter/builtins/math/clamp.rs | 60 +++- .../src/interpreter/builtins/math/cos.rs | 36 ++- .../src/interpreter/builtins/math/cosh.rs | 36 ++- .../src/interpreter/builtins/math/deg2rad.rs | 36 ++- .../src/interpreter/builtins/math/exp.rs | 36 ++- .../src/interpreter/builtins/math/fdiv.rs | 36 ++- .../src/interpreter/builtins/math/floor.rs | 30 +- .../src/interpreter/builtins/math/fmod.rs | 36 ++- .../src/interpreter/builtins/math/hypot.rs | 38 ++- .../src/interpreter/builtins/math/intdiv.rs | 38 ++- .../src/interpreter/builtins/math/log.rs | 43 ++- .../src/interpreter/builtins/math/log10.rs | 36 ++- .../src/interpreter/builtins/math/log2.rs | 36 ++- .../src/interpreter/builtins/math/max.rs | 37 ++- .../src/interpreter/builtins/math/min.rs | 37 ++- .../src/interpreter/builtins/math/mod.rs | 43 ++- .../src/interpreter/builtins/math/mt_rand.rs | 30 +- .../src/interpreter/builtins/math/pi.rs | 26 +- .../src/interpreter/builtins/math/pow.rs | 32 ++- .../src/interpreter/builtins/math/rad2deg.rs | 36 ++- .../src/interpreter/builtins/math/rand.rs | 62 ++++- .../interpreter/builtins/math/random_int.rs | 57 +++- .../src/interpreter/builtins/math/round.rs | 38 ++- .../src/interpreter/builtins/math/runtime.rs | 26 -- .../src/interpreter/builtins/math/sin.rs | 36 ++- .../src/interpreter/builtins/math/sinh.rs | 36 ++- .../src/interpreter/builtins/math/sqrt.rs | 30 +- .../src/interpreter/builtins/math/tan.rs | 36 ++- .../src/interpreter/builtins/math/tanh.rs | 36 ++- .../src/interpreter/builtins/scalars/math.rs | 258 +----------------- .../interpreter/builtins/strings/simple.rs | 16 +- 44 files changed, 1319 insertions(+), 720 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/math.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/random.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/math/runtime.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs index 018f0d695d..e26ad3ac9e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs @@ -211,77 +211,6 @@ pub(in crate::interpreter) fn eval_random_u128() -> u128 { value ^ (value >> 31) } -/// Evaluates PHP `rand()` and `mt_rand()` over zero args or an inclusive range. -pub(in crate::interpreter) fn eval_builtin_rand( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_rand_result(None, None, values), - [min, max] => { - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_rand_result(Some(min), Some(max), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `random_int()` over an inclusive integer range. -pub(in crate::interpreter) fn eval_builtin_random_int( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [min, max] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_random_int_result(min, max, values) -} - -/// Returns one non-cryptographic random integer using PHP's inclusive range rules. -pub(in crate::interpreter) fn eval_rand_result( - min: Option, - max: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let (min, max) = match (min, max) { - (None, None) => (0, i64::from(i32::MAX)), - (Some(min), Some(max)) => (eval_int_value(min, values)?, eval_int_value(max, values)?), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let low = min.min(max); - let high = min.max(max); - let width = (i128::from(high) - i128::from(low) + 1) as u128; - let offset = (eval_random_u128() % width) as i128; - let sampled = i128::from(low) + offset; - let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(sampled) -} - -/// Returns one eval `random_int()` value in the inclusive range `[min, max]`. -pub(in crate::interpreter) fn eval_random_int_result( - min: RuntimeCellHandle, - max: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let min = eval_int_value(min, values)?; - let max = eval_int_value(max, values)?; - if min > max { - return Err(EvalStatus::RuntimeFatal); - } - let width = (i128::from(max) - i128::from(min) + 1) as u128; - let offset = (eval_random_u128() % width) as i128; - let sampled = i128::from(min) + offset; - let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(sampled) -} - /// Evaluates PHP `range()` over integer-compatible start and end expressions. pub(in crate::interpreter) fn eval_builtin_range( args: &[EvalExpr], diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/math.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/math.rs deleted file mode 100644 index fb496881dd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/math.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Purpose: -//! Implements small numeric math wrapper builtins for eval execution. -//! -//! Called from: -//! - `crate::interpreter::builtins::formatting` re-exports. -//! -//! Key details: -//! - Argument evaluation stays in PHP source order before delegating scalar math -//! behavior to `RuntimeValueOps`. - -use super::super::super::*; - -/// Evaluates PHP's `ceil(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_ceil( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.ceil(value) -} - -/// Evaluates PHP's `floor(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_floor( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.floor(value) -} - -/// Evaluates PHP's zero-argument `pi()` builtin. -pub(in crate::interpreter) fn eval_builtin_pi( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.float(std::f64::consts::PI) -} - -/// Evaluates PHP's `pow(...)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_pow( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - values.pow(left, right) -} - -/// Evaluates PHP's `round(...)` over one value and an optional precision expression. -pub(in crate::interpreter) fn eval_builtin_round( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - values.round(value, None) - } - [value, precision] => { - let value = eval_expr(value, context, scope, values)?; - let precision = eval_expr(precision, context, scope, values)?; - values.round(value, Some(precision)) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs index 298cb48168..d3f0116037 100644 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Groups numeric formatting, printf-family, scanf, and math wrapper eval builtins. +//! Groups numeric formatting, printf-family, and scanf eval builtins. //! Submodules are split by builtin family and shared formatting helpers. //! //! Called from: @@ -12,7 +12,6 @@ mod common; mod declarations; mod dispatch; -mod math; mod number_format; mod printf; mod sprintf_format; @@ -20,7 +19,6 @@ mod sscanf; pub(in crate::interpreter) use common::*; pub(in crate::interpreter) use dispatch::*; -pub(in crate::interpreter) use math::*; pub(in crate::interpreter) use number_format::*; pub(in crate::interpreter) use printf::*; pub(in crate::interpreter) use sprintf_format::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 8a27057b47..aee3e3d1b8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -11,41 +11,45 @@ use super::super::super::{ eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, - eval_builtin_ceil, eval_builtin_chr, eval_builtin_clamp, eval_builtin_count, - eval_builtin_crc32, eval_builtin_ctype, eval_builtin_explode, eval_builtin_float_binary, - eval_builtin_float_pair, eval_builtin_float_unary, eval_builtin_floor, - eval_builtin_formatting_call, eval_builtin_gzip, eval_builtin_hash_algos, + eval_builtin_chr, eval_builtin_count, + eval_builtin_crc32, eval_builtin_ctype, eval_builtin_explode, eval_builtin_formatting_call, eval_builtin_gzip, eval_builtin_hash_algos, eval_builtin_hash_copy, eval_builtin_hash_final, eval_builtin_hash_init, eval_builtin_hash_one_shot, eval_builtin_hash_update, eval_builtin_hex2bin, - eval_builtin_implode, eval_builtin_intdiv, eval_builtin_log, eval_builtin_min_max, - eval_builtin_number_format, eval_builtin_ord, eval_builtin_pi, eval_builtin_pow, - eval_builtin_rand, eval_builtin_random_int, eval_builtin_round, eval_builtin_slashes, - eval_builtin_sqrt, eval_builtin_str_repeat, eval_builtin_strlen, + eval_builtin_implode, eval_builtin_number_format, eval_builtin_ord, eval_builtin_slashes, + eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::super::{ - eval_builtin_abs, eval_builtin_array_aggregate, eval_builtin_array_call, + eval_builtin_abs, eval_builtin_acos, eval_builtin_array_aggregate, eval_builtin_array_call, eval_builtin_array_flip, eval_builtin_array_key_exists, eval_builtin_array_keys, eval_builtin_array_pad, eval_builtin_array_rand, eval_builtin_array_reverse, eval_builtin_array_search, eval_builtin_array_slice, eval_builtin_array_unique, - eval_builtin_array_values, eval_builtin_boolval, eval_builtin_core_call, - eval_builtin_filesystem_call, eval_builtin_floatval, eval_builtin_gettype, - eval_builtin_intval, eval_builtin_is_array, eval_builtin_is_bool, + eval_builtin_array_values, eval_builtin_asin, eval_builtin_atan, eval_builtin_atan2, + eval_builtin_boolval, eval_builtin_ceil, eval_builtin_clamp, eval_builtin_core_call, + eval_builtin_cos, eval_builtin_cosh, eval_builtin_deg2rad, eval_builtin_exp, + eval_builtin_fdiv, eval_builtin_filesystem_call, eval_builtin_floatval, + eval_builtin_floor, eval_builtin_fmod, eval_builtin_gettype, eval_builtin_hypot, + eval_builtin_intdiv, eval_builtin_intval, eval_builtin_is_array, eval_builtin_is_bool, eval_builtin_is_double, eval_builtin_is_finite, eval_builtin_is_float, eval_builtin_is_infinite, eval_builtin_is_int, eval_builtin_is_integer, eval_builtin_is_iterable, eval_builtin_is_long, eval_builtin_is_nan, eval_builtin_is_null, eval_builtin_is_numeric, eval_builtin_is_object, eval_builtin_is_real, eval_builtin_is_resource, eval_builtin_is_scalar, - eval_builtin_is_string, + eval_builtin_is_string, eval_builtin_log, eval_builtin_log2, eval_builtin_log10, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, - eval_builtin_json_call, eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_range, - eval_builtin_raw_memory_call, eval_builtin_regex_call, eval_builtin_str_pad, eval_builtin_str_replace, - eval_builtin_str_split, eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, + eval_builtin_json_call, eval_builtin_max, eval_builtin_min, eval_builtin_mt_rand, + eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_pi, eval_builtin_pow, + eval_builtin_rad2deg, eval_builtin_rand, eval_builtin_random_int, eval_builtin_range, + eval_builtin_round, + eval_builtin_raw_memory_call, eval_builtin_regex_call, eval_builtin_sin, eval_builtin_sinh, + eval_builtin_str_pad, eval_builtin_str_replace, eval_builtin_str_split, + eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, eval_builtin_strval, eval_builtin_substr_replace, eval_builtin_symbols_call, - eval_builtin_time_call, eval_builtin_trim_like, eval_builtin_ucwords, + eval_builtin_tan, eval_builtin_tanh, eval_builtin_time_call, eval_builtin_trim_like, + eval_builtin_ucwords, eval_builtin_wordwrap, }; @@ -102,12 +106,28 @@ pub(in crate::interpreter) enum EvalDirectHook { Ctype, /// Dispatches filesystem and path builtins. Filesystem, - /// Dispatches binary floating-point builtins. - FloatBinary, - /// Dispatches paired floating-point builtins. - FloatPair, - /// Dispatches unary floating-point builtins. - FloatUnary, + /// Dispatches `acos(...)`. + Acos, + /// Dispatches `asin(...)`. + Asin, + /// Dispatches `atan(...)`. + Atan, + /// Dispatches `atan2(...)`. + Atan2, + /// Dispatches `cos(...)`. + Cos, + /// Dispatches `cosh(...)`. + Cosh, + /// Dispatches `deg2rad(...)`. + Deg2rad, + /// Dispatches `exp(...)`. + Exp, + /// Dispatches `fdiv(...)`. + Fdiv, + /// Dispatches `fmod(...)`. + Fmod, + /// Dispatches `hypot(...)`. + Hypot, /// Dispatches printf-family formatting builtins. Formatting, /// Dispatches `floor(...)`. @@ -176,8 +196,14 @@ pub(in crate::interpreter) enum EvalDirectHook { Json, /// Dispatches `log(...)`. Log, - /// Dispatches `min(...)` and `max(...)`. - MinMax, + /// Dispatches `log2(...)`. + Log2, + /// Dispatches `log10(...)`. + Log10, + /// Dispatches `max(...)`. + Max, + /// Dispatches `min(...)`. + Min, /// Dispatches network, host, environment, and process builtins. NetworkEnv, /// Dispatches `number_format(...)`. @@ -188,8 +214,14 @@ pub(in crate::interpreter) enum EvalDirectHook { Pi, /// Dispatches `pow(...)`. Pow, - /// Dispatches random-number builtins. - Random, + /// Dispatches `mt_rand(...)`. + MtRand, + /// Dispatches `rad2deg(...)`. + Rad2deg, + /// Dispatches `rand(...)`. + Rand, + /// Dispatches `random_int(...)`. + RandomInt, /// Dispatches `round(...)`. Round, /// Dispatches `range(...)`. @@ -200,6 +232,10 @@ pub(in crate::interpreter) enum EvalDirectHook { RawMemory, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, + /// Dispatches `sin(...)`. + Sin, + /// Dispatches `sinh(...)`. + Sinh, /// Dispatches `sqrt(...)`. Sqrt, /// Dispatches string ASCII case-conversion builtins. @@ -238,6 +274,10 @@ pub(in crate::interpreter) enum EvalDirectHook { SubstrReplace, /// Dispatches symbol, class metadata, SPL, and language-construct probes. Symbols, + /// Dispatches `tan(...)`. + Tan, + /// Dispatches `tanh(...)`. + Tanh, /// Dispatches date, time, and sleep builtins. Time, /// Dispatches trim-family builtins. @@ -266,6 +306,7 @@ impl EvalDirectHook { ) -> Result { match self { Self::Abs => eval_builtin_abs(args, context, scope, values), + Self::Acos => eval_builtin_acos(args, context, scope, values), Self::ArrayAggregate => eval_builtin_array_aggregate(name, args, context, scope, values), Self::Array => eval_builtin_array_call(name, args, context, scope, values), Self::ArrayFlip => eval_builtin_array_flip(args, context, scope, values), @@ -278,6 +319,9 @@ impl EvalDirectHook { Self::ArraySlice => eval_builtin_array_slice(args, context, scope, values), Self::ArrayUnique => eval_builtin_array_unique(args, context, scope, values), Self::ArrayValues => eval_builtin_array_values(args, context, scope, values), + Self::Asin => eval_builtin_asin(args, context, scope, values), + Self::Atan => eval_builtin_atan(args, context, scope, values), + Self::Atan2 => eval_builtin_atan2(args, context, scope, values), Self::Base64Decode => eval_builtin_base64_decode(args, context, scope, values), Self::Base64Encode => eval_builtin_base64_encode(args, context, scope, values), Self::Bin2Hex => eval_builtin_bin2hex(args, context, scope, values), @@ -287,15 +331,19 @@ impl EvalDirectHook { Self::Clamp => eval_builtin_clamp(args, context, scope, values), Self::Count => eval_builtin_count(args, context, scope, values), Self::Core => eval_builtin_core_call(name, args, context, scope, values), + Self::Cos => eval_builtin_cos(args, context, scope, values), + Self::Cosh => eval_builtin_cosh(args, context, scope, values), Self::Crc32 => eval_builtin_crc32(args, context, scope, values), Self::Ctype => eval_builtin_ctype(name, args, context, scope, values), + Self::Deg2rad => eval_builtin_deg2rad(args, context, scope, values), + Self::Exp => eval_builtin_exp(args, context, scope, values), + Self::Fdiv => eval_builtin_fdiv(args, context, scope, values), Self::Filesystem => eval_builtin_filesystem_call(name, args, context, scope, values), - Self::FloatBinary => eval_builtin_float_binary(name, args, context, scope, values), - Self::FloatPair => eval_builtin_float_pair(name, args, context, scope, values), - Self::FloatUnary => eval_builtin_float_unary(name, args, context, scope, values), + Self::Fmod => eval_builtin_fmod(args, context, scope, values), Self::Formatting => eval_builtin_formatting_call(name, args, context, scope, values), Self::Floor => eval_builtin_floor(args, context, scope, values), Self::Gettype => eval_builtin_gettype(args, context, scope, values), + Self::Hypot => eval_builtin_hypot(args, context, scope, values), Self::Floatval => eval_builtin_floatval(args, context, scope, values), Self::Intval => eval_builtin_intval(args, context, scope, values), Self::IsArray => eval_builtin_is_array(args, context, scope, values), @@ -333,23 +381,27 @@ impl EvalDirectHook { Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), Self::Json => eval_builtin_json_call(name, args, context, scope, values), Self::Log => eval_builtin_log(args, context, scope, values), - Self::MinMax => eval_builtin_min_max(name, args, context, scope, values), + Self::Log2 => eval_builtin_log2(args, context, scope, values), + Self::Log10 => eval_builtin_log10(args, context, scope, values), + Self::Max => eval_builtin_max(args, context, scope, values), + Self::Min => eval_builtin_min(args, context, scope, values), + Self::MtRand => eval_builtin_mt_rand(args, context, scope, values), Self::NetworkEnv => eval_builtin_network_env_call(name, args, context, scope, values), Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), Self::Ord => eval_builtin_ord(args, context, scope, values), Self::Pi => eval_builtin_pi(args, values), Self::Pow => eval_builtin_pow(args, context, scope, values), - Self::Random => match name { - "rand" | "mt_rand" => eval_builtin_rand(args, context, scope, values), - "random_int" => eval_builtin_random_int(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - }, + Self::Rad2deg => eval_builtin_rad2deg(args, context, scope, values), + Self::Rand => eval_builtin_rand(args, context, scope, values), + Self::RandomInt => eval_builtin_random_int(args, context, scope, values), Self::Round => eval_builtin_round(args, context, scope, values), Self::Range => eval_builtin_range(args, context, scope, values), Self::Regex => eval_builtin_regex_call(name, args, context, scope, values), Self::RawMemory => eval_builtin_raw_memory_call(name, args, context, scope, values), + Self::Sin => eval_builtin_sin(args, context, scope, values), + Self::Sinh => eval_builtin_sinh(args, context, scope, values), Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), - Self::Sqrt => eval_builtin_sqrt(args, context, scope, values), + Self::Sqrt => super::super::math::eval_builtin_sqrt(args, context, scope, values), Self::StringCase => eval_builtin_string_case(name, args, context, scope, values), Self::StringCompare => eval_builtin_string_compare(name, args, context, scope, values), Self::StringPosition => { @@ -378,6 +430,8 @@ impl EvalDirectHook { Self::Substr => eval_builtin_substr(args, context, scope, values), Self::SubstrReplace => eval_builtin_substr_replace(args, context, scope, values), Self::Symbols => eval_builtin_symbols_call(name, args, context, scope, values), + Self::Tan => eval_builtin_tan(args, context, scope, values), + Self::Tanh => eval_builtin_tanh(args, context, scope, values), Self::Time => eval_builtin_time_call(name, args, context, scope, values), Self::TrimLike => eval_builtin_trim_like(name, args, context, scope, values), Self::Ucwords => eval_builtin_ucwords(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs index ef9ea87c97..458ff41fb2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs @@ -12,7 +12,6 @@ mod arity; mod direct; mod hash; mod number_format; -mod random; mod string_split_join; mod values; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/random.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/random.rs deleted file mode 100644 index b3e01cdf6a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/random.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Purpose: -//! Focused dispatch helpers for declarative random-number builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks::values`. -//! -//! Key details: -//! - `rand` and `mt_rand` accept either no arguments or an inclusive min/max -//! range, while `random_int` requires an inclusive min/max range. - -use super::super::super::{EvalStatus, RuntimeCellHandle, RuntimeValueOps}; -use super::super::{eval_rand_result, eval_random_int_result}; - -/// Dispatches evaluated random-number builtin calls. -pub(super) fn eval_random_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "rand" | "mt_rand" => match evaluated_args { - [] => eval_rand_result(None, None, values), - [min, max] => eval_rand_result(Some(*min), Some(*max), values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "random_int" => { - let [min, max] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_random_int_result(*min, *max, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 8ef46c0835..4c97240ccf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -15,16 +15,20 @@ use super::super::super::{ RuntimeValueOps, }; use super::super::{ - eval_array_aggregate_result, eval_array_flip_result, eval_array_keys_result, + eval_abs_result, eval_acos_result, eval_array_aggregate_result, eval_array_flip_result, + eval_array_keys_result, eval_array_mutating_values_result, eval_array_non_mutating_values_result, eval_array_pad_result, eval_array_rand_result, eval_array_reverse_result, eval_array_search_result, eval_array_slice_result, eval_array_unique_result, - eval_array_values_result, eval_base64_decode_result, eval_base64_encode_result, - eval_bin2hex_result, eval_boolval_result, eval_chr_result, eval_clamp_result, + eval_array_values_result, eval_asin_result, eval_atan2_result, eval_atan_result, + eval_base64_decode_result, eval_base64_encode_result, + eval_bin2hex_result, eval_boolval_result, eval_ceil_result, eval_chr_result, + eval_clamp_result, eval_cos_result, eval_cosh_result, eval_deg2rad_result, eval_core_values_result, eval_crc32_result, eval_ctype_result, - eval_filesystem_values_result, eval_float_binary_result, eval_float_pair_result, - eval_float_unary_result, eval_floatval_result, eval_formatting_values_result, - eval_gettype_result, eval_intval_result, eval_is_array_result, eval_is_bool_result, + eval_exp_result, eval_fdiv_result, eval_filesystem_values_result, eval_floatval_result, + eval_floor_result, eval_fmod_result, eval_formatting_values_result, + eval_gettype_result, eval_hypot_result, eval_intdiv_result, eval_intval_result, + eval_is_array_result, eval_is_bool_result, eval_is_double_result, eval_is_finite_result, eval_is_float_result, eval_is_infinite_result, eval_is_int_result, eval_is_integer_result, eval_is_iterable_result, eval_is_long_result, eval_is_nan_result, @@ -32,22 +36,26 @@ use super::super::{ eval_is_real_result, eval_is_resource_result, eval_is_scalar_result, eval_is_string_result, eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, - eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, eval_intdiv_result, - eval_json_values_result, eval_log_result, eval_min_max_result, eval_network_env_values_result, - eval_nl2br_result, eval_range_result, eval_regex_values_result, eval_settype_values_result, - eval_slashes_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, + eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, + eval_json_values_result, eval_log2_result, eval_log10_result, eval_log_result, + eval_max_result, eval_min_result, eval_mt_rand_values_result, + eval_network_env_values_result, eval_nl2br_result, eval_pi_result, eval_pow_result, + eval_rad2deg_result, eval_rand_values_result, eval_random_int_values_result, + eval_range_result, eval_regex_values_result, eval_round_result, eval_settype_values_result, + eval_sin_result, eval_sinh_result, eval_slashes_result, eval_sqrt_result, + eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, eval_string_search_result, eval_strstr_result, eval_strval_result, eval_substr_replace_result, eval_substr_result, eval_raw_memory_values_result, - eval_symbols_values_result, eval_time_values_result, eval_trim_like_result, + eval_symbols_values_result, eval_tan_result, eval_tanh_result, eval_time_values_result, + eval_trim_like_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, eval_wordwrap_result, }; use super::arity::{one_arg, three_args, two_args}; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; use super::number_format::eval_number_format_values; -use super::random::eval_random_values; use super::string_split_join::eval_string_split_join_values; /// Evaluated-argument dispatch hooks for migrated builtins. @@ -105,12 +113,28 @@ pub(in crate::interpreter) enum EvalValuesHook { Ctype, /// Dispatches filesystem and path builtins. Filesystem, - /// Dispatches binary floating-point builtins. - FloatBinary, - /// Dispatches paired floating-point builtins. - FloatPair, - /// Dispatches unary floating-point builtins. - FloatUnary, + /// Dispatches `acos(...)`. + Acos, + /// Dispatches `asin(...)`. + Asin, + /// Dispatches `atan(...)`. + Atan, + /// Dispatches `atan2(...)`. + Atan2, + /// Dispatches `cos(...)`. + Cos, + /// Dispatches `cosh(...)`. + Cosh, + /// Dispatches `deg2rad(...)`. + Deg2rad, + /// Dispatches `exp(...)`. + Exp, + /// Dispatches `fdiv(...)`. + Fdiv, + /// Dispatches `fmod(...)`. + Fmod, + /// Dispatches `hypot(...)`. + Hypot, /// Dispatches printf-family formatting builtins. Formatting, /// Dispatches `floor(...)`. @@ -179,8 +203,14 @@ pub(in crate::interpreter) enum EvalValuesHook { Json, /// Dispatches `log(...)`. Log, - /// Dispatches `min(...)` and `max(...)`. - MinMax, + /// Dispatches `log2(...)`. + Log2, + /// Dispatches `log10(...)`. + Log10, + /// Dispatches `max(...)`. + Max, + /// Dispatches `min(...)`. + Min, /// Dispatches network, host, environment, and process builtins. NetworkEnv, /// Dispatches `number_format(...)`. @@ -191,8 +221,14 @@ pub(in crate::interpreter) enum EvalValuesHook { Pi, /// Dispatches `pow(...)`. Pow, - /// Dispatches random-number builtins. - Random, + /// Dispatches `mt_rand(...)`. + MtRand, + /// Dispatches `rad2deg(...)`. + Rad2deg, + /// Dispatches `rand(...)`. + Rand, + /// Dispatches `random_int(...)`. + RandomInt, /// Dispatches `round(...)`. Round, /// Dispatches `range(...)`. @@ -205,6 +241,10 @@ pub(in crate::interpreter) enum EvalValuesHook { Settype, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, + /// Dispatches `sin(...)`. + Sin, + /// Dispatches `sinh(...)`. + Sinh, /// Dispatches `sqrt(...)`. Sqrt, /// Dispatches string ASCII case-conversion builtins. @@ -243,6 +283,10 @@ pub(in crate::interpreter) enum EvalValuesHook { SubstrReplace, /// Dispatches symbol, class metadata, SPL, and language-construct probes. Symbols, + /// Dispatches `tan(...)`. + Tan, + /// Dispatches `tanh(...)`. + Tanh, /// Dispatches date, time, and sleep builtins. Time, /// Dispatches trim-family builtins. @@ -269,7 +313,8 @@ impl EvalValuesHook { values: &mut impl RuntimeValueOps, ) -> Result { match self { - Self::Abs => one_arg(evaluated_args, values, |value, values| values.abs(value)), + Self::Abs => one_arg(evaluated_args, values, eval_abs_result), + Self::Acos => one_arg(evaluated_args, values, eval_acos_result), Self::ArrayAggregate => one_arg(evaluated_args, values, |array, values| { eval_array_aggregate_result(name, array, values) }), @@ -304,11 +349,14 @@ impl EvalValuesHook { }, Self::ArrayUnique => one_arg(evaluated_args, values, eval_array_unique_result), Self::ArrayValues => one_arg(evaluated_args, values, eval_array_values_result), + Self::Asin => one_arg(evaluated_args, values, eval_asin_result), + Self::Atan => one_arg(evaluated_args, values, eval_atan_result), + Self::Atan2 => two_args(evaluated_args, values, eval_atan2_result), Self::Base64Decode => one_arg(evaluated_args, values, eval_base64_decode_result), Self::Base64Encode => one_arg(evaluated_args, values, eval_base64_encode_result), Self::Bin2Hex => one_arg(evaluated_args, values, eval_bin2hex_result), Self::Boolval => one_arg(evaluated_args, values, eval_boolval_result), - Self::Ceil => one_arg(evaluated_args, values, |value, values| values.ceil(value)), + Self::Ceil => one_arg(evaluated_args, values, eval_ceil_result), Self::Chr => one_arg(evaluated_args, values, eval_chr_result), Self::Clamp => three_args(evaluated_args, values, eval_clamp_result), Self::Count => match evaluated_args { @@ -317,23 +365,21 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::Core => eval_core_values_result(name, evaluated_args, context, values), + Self::Cos => one_arg(evaluated_args, values, eval_cos_result), + Self::Cosh => one_arg(evaluated_args, values, eval_cosh_result), Self::Crc32 => one_arg(evaluated_args, values, eval_crc32_result), Self::Ctype => one_arg(evaluated_args, values, |value, values| { eval_ctype_result(name, value, values) }), + Self::Deg2rad => one_arg(evaluated_args, values, eval_deg2rad_result), + Self::Exp => one_arg(evaluated_args, values, eval_exp_result), + Self::Fdiv => two_args(evaluated_args, values, eval_fdiv_result), Self::Filesystem => eval_filesystem_values_result(name, evaluated_args, context, values), - Self::FloatBinary => two_args(evaluated_args, values, |left, right, values| { - eval_float_binary_result(name, left, right, values) - }), - Self::FloatPair => two_args(evaluated_args, values, |left, right, values| { - eval_float_pair_result(name, left, right, values) - }), - Self::FloatUnary => one_arg(evaluated_args, values, |value, values| { - eval_float_unary_result(name, value, values) - }), + Self::Floor => one_arg(evaluated_args, values, eval_floor_result), + Self::Fmod => two_args(evaluated_args, values, eval_fmod_result), Self::Formatting => eval_formatting_values_result(name, evaluated_args, values), - Self::Floor => one_arg(evaluated_args, values, |value, values| values.floor(value)), Self::Gettype => one_arg(evaluated_args, values, eval_gettype_result), + Self::Hypot => two_args(evaluated_args, values, eval_hypot_result), Self::Floatval => one_arg(evaluated_args, values, eval_floatval_result), Self::Intval => one_arg(evaluated_args, values, eval_intval_result), Self::IsArray => one_arg(evaluated_args, values, eval_is_array_result), @@ -373,7 +419,11 @@ impl EvalValuesHook { [num, base] => eval_log_result(*num, Some(*base), values), _ => Err(EvalStatus::RuntimeFatal), }, - Self::MinMax => eval_min_max_result(name, evaluated_args, values), + Self::Log2 => one_arg(evaluated_args, values, eval_log2_result), + Self::Log10 => one_arg(evaluated_args, values, eval_log10_result), + Self::Max => eval_max_result(evaluated_args, values), + Self::Min => eval_min_result(evaluated_args, values), + Self::MtRand => eval_mt_rand_values_result(evaluated_args, values), Self::NetworkEnv => eval_network_env_values_result(name, evaluated_args, values), Self::NumberFormat => eval_number_format_values(evaluated_args, values), Self::Ord => one_arg(evaluated_args, values, eval_ord_result), @@ -381,25 +431,27 @@ impl EvalValuesHook { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - values.float(std::f64::consts::PI) + eval_pi_result(values) } - Self::Pow => two_args(evaluated_args, values, |left, right, values| { - values.pow(left, right) - }), - Self::Random => eval_random_values(name, evaluated_args, values), + Self::Pow => two_args(evaluated_args, values, eval_pow_result), + Self::Rad2deg => one_arg(evaluated_args, values, eval_rad2deg_result), + Self::Rand => eval_rand_values_result(evaluated_args, values), + Self::RandomInt => eval_random_int_values_result(evaluated_args, values), Self::Round => match evaluated_args { - [value] => values.round(*value, None), - [value, precision] => values.round(*value, Some(*precision)), + [value] => eval_round_result(*value, None, values), + [value, precision] => eval_round_result(*value, Some(*precision), values), _ => Err(EvalStatus::RuntimeFatal), }, Self::Range => two_args(evaluated_args, values, eval_range_result), Self::Regex => eval_regex_values_result(name, evaluated_args, context, values), Self::RawMemory => eval_raw_memory_values_result(name, evaluated_args, context, values), Self::Settype => eval_settype_values_result(evaluated_args, values), + Self::Sin => one_arg(evaluated_args, values, eval_sin_result), + Self::Sinh => one_arg(evaluated_args, values, eval_sinh_result), Self::Slashes => one_arg(evaluated_args, values, |value, values| { eval_slashes_result(name, value, values) }), - Self::Sqrt => one_arg(evaluated_args, values, |value, values| values.sqrt(value)), + Self::Sqrt => one_arg(evaluated_args, values, eval_sqrt_result), Self::StringCase => one_arg(evaluated_args, values, |value, values| { eval_string_case_result(name, value, values) }), @@ -478,6 +530,8 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::Symbols => eval_symbols_values_result(name, evaluated_args, context, values), + Self::Tan => one_arg(evaluated_args, values, eval_tan_result), + Self::Tanh => one_arg(evaluated_args, values, eval_tanh_result), Self::Time => eval_time_values_result(name, evaluated_args, context, values), Self::TrimLike => match evaluated_args { [value] => eval_trim_like_result(name, *value, None, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/math/abs.rs b/crates/elephc-magician/src/interpreter/builtins/math/abs.rs index 80ac4d6b13..b46f4cd1de 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/abs.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/abs.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `abs`. +//! Eval registry entry and implementation for `abs`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing numeric hook. +//! - Runtime scalar absolute-value coercions stay delegated to `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "abs", @@ -14,3 +16,25 @@ eval_builtin! { direct: Abs, values: Abs, } + +/// Evaluates PHP `abs()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_abs( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_abs_result(num, values) +} + +/// Applies PHP `abs()` to one already evaluated value. +pub(in crate::interpreter) fn eval_abs_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.abs(num) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/acos.rs b/crates/elephc-magician/src/interpreter/builtins/math/acos.rs index e5c57a264d..93e3693dbc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/acos.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/acos.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `acos`. +//! Eval registry entry and implementation for `acos`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "acos", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Acos, + values: Acos, +} + +/// Evaluates PHP `acos()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_acos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_acos_result(num, values) +} + +/// Applies PHP `acos()` to one already evaluated value. +pub(in crate::interpreter) fn eval_acos_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.acos()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/asin.rs b/crates/elephc-magician/src/interpreter/builtins/math/asin.rs index 96c4fa5759..c1b785a651 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/asin.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/asin.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `asin`. +//! Eval registry entry and implementation for `asin`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "asin", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Asin, + values: Asin, +} + +/// Evaluates PHP `asin()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_asin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_asin_result(num, values) +} + +/// Applies PHP `asin()` to one already evaluated value. +pub(in crate::interpreter) fn eval_asin_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.asin()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/atan.rs b/crates/elephc-magician/src/interpreter/builtins/math/atan.rs index 6afa0af30f..54d240e989 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/atan.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/atan.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `atan`. +//! Eval registry entry and implementation for `atan`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "atan", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Atan, + values: Atan, +} + +/// Evaluates PHP `atan()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_atan( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_atan_result(num, values) +} + +/// Applies PHP `atan()` to one already evaluated value. +pub(in crate::interpreter) fn eval_atan_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.atan()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs b/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs index 2cfd46a231..474cf1dee9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs @@ -1,16 +1,44 @@ //! Purpose: -//! Declarative eval registry entry for `atan2`. +//! Eval registry entry and implementation for `atan2`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing paired float math hook. +//! - Both arguments are evaluated in source order before float coercion. + +use super::super::super::*; eval_builtin! { name: "atan2", area: Math, params: [y, x], - direct: FloatPair, - values: FloatPair, + direct: Atan2, + values: Atan2, +} + +/// Evaluates PHP `atan2()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_atan2( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_atan2_result(left, right, values) +} + +/// Applies PHP `atan2()` to two already evaluated values. +pub(in crate::interpreter) fn eval_atan2_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_float_value(left, values)?; + let right = eval_float_value(right, values)?; + values.float(left.atan2(right)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs b/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs index 13394c0d98..9a15ceb4d9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `ceil`. +//! Eval registry entry and implementation for `ceil`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing numeric rounding hook. +//! - Runtime numeric coercions stay delegated to `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "ceil", @@ -14,3 +16,25 @@ eval_builtin! { direct: Ceil, values: Ceil, } + +/// Evaluates PHP `ceil()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ceil( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_ceil_result(num, values) +} + +/// Applies PHP `ceil()` to one already evaluated value. +pub(in crate::interpreter) fn eval_ceil_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.ceil(num) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs b/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs index 552cb22f12..11de92df83 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `clamp`. +//! Eval registry entry and implementation for `clamp`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing clamp hook. +//! - Bounds are validated before comparison and NaN bounds are runtime fatals. + +use super::super::super::*; eval_builtin! { name: "clamp", @@ -14,3 +16,55 @@ eval_builtin! { direct: Clamp, values: Clamp, } + +/// Evaluates PHP `clamp()` over three eval expressions. +pub(in crate::interpreter) fn eval_builtin_clamp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_clamp_result(value, min, max, values) +} + +/// Selects the inclusive clamp result after validating bound order and NaN bounds. +pub(in crate::interpreter) fn eval_clamp_result( + value: RuntimeCellHandle, + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; + if values.truthy(invalid_bounds)? { + return Err(EvalStatus::RuntimeFatal); + } + let above_max = values.compare(EvalBinOp::Gt, value, max)?; + if values.truthy(above_max)? { + return Ok(max); + } + let below_min = values.compare(EvalBinOp::Lt, value, min)?; + if values.truthy(below_min)? { + return Ok(min); + } + Ok(value) +} + +/// Returns whether a clamp bound is a floating-point NaN value. +fn eval_clamp_bound_is_nan( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_FLOAT { + return Ok(false); + } + Ok(eval_float_value(value, values)?.is_nan()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/cos.rs b/crates/elephc-magician/src/interpreter/builtins/math/cos.rs index 66229ff13b..9db085ed6b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/cos.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/cos.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `cos`. +//! Eval registry entry and implementation for `cos`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "cos", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Cos, + values: Cos, +} + +/// Evaluates PHP `cos()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_cos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_cos_result(num, values) +} + +/// Applies PHP `cos()` to one already evaluated value. +pub(in crate::interpreter) fn eval_cos_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.cos()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs b/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs index aa7cec9324..132a8010fe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `cosh`. +//! Eval registry entry and implementation for `cosh`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "cosh", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Cosh, + values: Cosh, +} + +/// Evaluates PHP `cosh()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_cosh( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_cosh_result(num, values) +} + +/// Applies PHP `cosh()` to one already evaluated value. +pub(in crate::interpreter) fn eval_cosh_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.cosh()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs b/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs index 3eb2d7090c..106c4f199b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `deg2rad`. +//! Eval registry entry and implementation for `deg2rad`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "deg2rad", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Deg2rad, + values: Deg2rad, +} + +/// Evaluates PHP `deg2rad()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_deg2rad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_deg2rad_result(num, values) +} + +/// Applies PHP `deg2rad()` to one already evaluated value. +pub(in crate::interpreter) fn eval_deg2rad_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.to_radians()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/exp.rs b/crates/elephc-magician/src/interpreter/builtins/math/exp.rs index 411c9a26d4..a65da62683 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/exp.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/exp.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `exp`. +//! Eval registry entry and implementation for `exp`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "exp", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Exp, + values: Exp, +} + +/// Evaluates PHP `exp()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_exp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_exp_result(num, values) +} + +/// Applies PHP `exp()` to one already evaluated value. +pub(in crate::interpreter) fn eval_exp_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.exp()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs b/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs index 02b57bd79d..3c61b4c601 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `fdiv`. +//! Eval registry entry and implementation for `fdiv`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing binary float math hook. +//! - Runtime numeric coercion and PHP edge cases stay delegated to `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "fdiv", area: Math, params: [num1, num2], - direct: FloatBinary, - values: FloatBinary, + direct: Fdiv, + values: Fdiv, +} + +/// Evaluates PHP `fdiv()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_fdiv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_fdiv_result(left, right, values) +} + +/// Applies PHP `fdiv()` to two already evaluated values. +pub(in crate::interpreter) fn eval_fdiv_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.fdiv(left, right) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/floor.rs b/crates/elephc-magician/src/interpreter/builtins/math/floor.rs index 1ce804056c..9f51b03dad 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/floor.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/floor.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `floor`. +//! Eval registry entry and implementation for `floor`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing numeric rounding hook. +//! - Runtime numeric coercions stay delegated to `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "floor", @@ -14,3 +16,25 @@ eval_builtin! { direct: Floor, values: Floor, } + +/// Evaluates PHP `floor()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_floor( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_floor_result(num, values) +} + +/// Applies PHP `floor()` to one already evaluated value. +pub(in crate::interpreter) fn eval_floor_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.floor(num) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs b/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs index dab4a930f7..3284726764 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `fmod`. +//! Eval registry entry and implementation for `fmod`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing binary float math hook. +//! - Runtime numeric coercion and PHP edge cases stay delegated to `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "fmod", area: Math, params: [num1, num2], - direct: FloatBinary, - values: FloatBinary, + direct: Fmod, + values: Fmod, +} + +/// Evaluates PHP `fmod()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_fmod( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_fmod_result(left, right, values) +} + +/// Applies PHP `fmod()` to two already evaluated values. +pub(in crate::interpreter) fn eval_fmod_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.fmod(left, right) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs b/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs index 958cdea049..b302f7821b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs @@ -1,16 +1,44 @@ //! Purpose: -//! Declarative eval registry entry for `hypot`. +//! Eval registry entry and implementation for `hypot`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing paired float math hook. +//! - Both arguments are evaluated in source order before float coercion. + +use super::super::super::*; eval_builtin! { name: "hypot", area: Math, params: [x, y], - direct: FloatPair, - values: FloatPair, + direct: Hypot, + values: Hypot, +} + +/// Evaluates PHP `hypot()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_hypot( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_hypot_result(left, right, values) +} + +/// Applies PHP `hypot()` to two already evaluated values. +pub(in crate::interpreter) fn eval_hypot_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_float_value(left, values)?; + let right = eval_float_value(right, values)?; + values.float(left.hypot(right)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs b/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs index 14d27b307d..aeb28395ed 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `intdiv`. +//! Eval registry entry and implementation for `intdiv`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing integer-division hook. +//! - Division by zero and overflowing signed division remain runtime fatals. + +use super::super::super::*; eval_builtin! { name: "intdiv", @@ -14,3 +16,33 @@ eval_builtin! { direct: Intdiv, values: Intdiv, } + +/// Evaluates PHP `intdiv()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_intdiv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_intdiv_result(left, right, values) +} + +/// Applies PHP `intdiv()` to two already evaluated values. +pub(in crate::interpreter) fn eval_intdiv_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left = eval_int_value(left, values)?; + let right = eval_int_value(right, values)?; + if right == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; + values.int(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log.rs b/crates/elephc-magician/src/interpreter/builtins/math/log.rs index afd723d84c..87f56fa14e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/log.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/log.rs @@ -1,12 +1,14 @@ //! Purpose: -//! Declarative eval registry entry for `log`. +//! Eval registry entry and implementation for `log`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing logarithm hook. +//! - The optional base defaults through registry metadata; direct calls still +//! preserve source-order argument evaluation. +use super::super::super::*; use super::super::spec::EvalBuiltinDefaultValue; eval_builtin! { @@ -16,3 +18,38 @@ eval_builtin! { direct: Log, values: Log, } + +/// Evaluates PHP `log()` over one value and an optional base expression. +pub(in crate::interpreter) fn eval_builtin_log( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [num] => { + let num = eval_expr(num, context, scope, values)?; + eval_log_result(num, None, values) + } + [num, base] => { + let num = eval_expr(num, context, scope, values)?; + let base = eval_expr(base, context, scope, values)?; + eval_log_result(num, Some(base), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `log()` to already evaluated arguments. +pub(in crate::interpreter) fn eval_log_result( + num: RuntimeCellHandle, + base: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + let result = match base { + Some(base) => num.log(eval_float_value(base, values)?), + None => num.ln(), + }; + values.float(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log10.rs b/crates/elephc-magician/src/interpreter/builtins/math/log10.rs index 31cdb28c6c..332965b76d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/log10.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/log10.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `log10`. +//! Eval registry entry and implementation for `log10`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "log10", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Log10, + values: Log10, +} + +/// Evaluates PHP `log10()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_log10( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_log10_result(num, values) +} + +/// Applies PHP `log10()` to one already evaluated value. +pub(in crate::interpreter) fn eval_log10_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.log10()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/log2.rs b/crates/elephc-magician/src/interpreter/builtins/math/log2.rs index e4fb488ee6..46bc9fce02 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/log2.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/log2.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `log2`. +//! Eval registry entry and implementation for `log2`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "log2", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Log2, + values: Log2, +} + +/// Evaluates PHP `log2()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_log2( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_log2_result(num, values) +} + +/// Applies PHP `log2()` to one already evaluated value. +pub(in crate::interpreter) fn eval_log2_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.log2()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/max.rs b/crates/elephc-magician/src/interpreter/builtins/math/max.rs index 822b762299..e8bd71de20 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/max.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/max.rs @@ -1,17 +1,44 @@ //! Purpose: -//! Declarative eval registry entry for `max`. +//! Eval registry entry and implementation for `max`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing variadic comparison hook. +//! - Variadic inputs are evaluated in PHP source order before runtime comparison. + +use super::super::super::*; eval_builtin! { name: "max", area: Math, params: [value], variadic: values, - direct: MinMax, - values: MinMax, + direct: Max, + values: Max, +} + +/// Evaluates PHP `max()` over two or more eval expressions. +pub(in crate::interpreter) fn eval_builtin_max( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_max_result(&evaluated_args, values) +} + +/// Applies PHP `max()` to already evaluated values. +pub(in crate::interpreter) fn eval_max_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + eval_min_max_selected(evaluated_args, EvalBinOp::Gt, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/min.rs b/crates/elephc-magician/src/interpreter/builtins/math/min.rs index fb25d75cb3..32f689d885 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/min.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/min.rs @@ -1,17 +1,44 @@ //! Purpose: -//! Declarative eval registry entry for `min`. +//! Eval registry entry and implementation for `min`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing variadic comparison hook. +//! - Variadic inputs are evaluated in PHP source order before runtime comparison. + +use super::super::super::*; eval_builtin! { name: "min", area: Math, params: [value], variadic: values, - direct: MinMax, - values: MinMax, + direct: Min, + values: Min, +} + +/// Evaluates PHP `min()` over two or more eval expressions. +pub(in crate::interpreter) fn eval_builtin_min( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_min_result(&evaluated_args, values) +} + +/// Applies PHP `min()` to already evaluated values. +pub(in crate::interpreter) fn eval_min_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + eval_min_max_selected(evaluated_args, EvalBinOp::Lt, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs index de9eb854ca..ac4de3582a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/mod.rs @@ -1,14 +1,12 @@ //! Purpose: -//! Per-builtin declarations for numeric functions migrated to the eval builtin -//! registry. +//! Per-builtin eval registry entries and implementations for numeric functions. //! //! Called from: //! - `crate::interpreter::builtins` module loading. //! //! Key details: -//! - Leaf files register metadata through `eval_builtin!`. -//! - Runtime helpers stay in focused support modules when direct hooks need -//! expression evaluation. +//! - Leaf files register metadata through `eval_builtin!` and own their +//! PHP-visible direct/by-value wrappers. mod abs; mod acos; @@ -38,11 +36,42 @@ mod rand; mod random_int; mod rad2deg; mod round; -mod runtime; mod sin; mod sinh; mod sqrt; mod tan; mod tanh; -pub(in crate::interpreter) use runtime::*; +pub(in crate::interpreter) use abs::*; +pub(in crate::interpreter) use acos::*; +pub(in crate::interpreter) use asin::*; +pub(in crate::interpreter) use atan::*; +pub(in crate::interpreter) use atan2::*; +pub(in crate::interpreter) use ceil::*; +pub(in crate::interpreter) use clamp::*; +pub(in crate::interpreter) use cos::*; +pub(in crate::interpreter) use cosh::*; +pub(in crate::interpreter) use deg2rad::*; +pub(in crate::interpreter) use exp::*; +pub(in crate::interpreter) use fdiv::*; +pub(in crate::interpreter) use floor::*; +pub(in crate::interpreter) use fmod::*; +pub(in crate::interpreter) use hypot::*; +pub(in crate::interpreter) use intdiv::*; +pub(in crate::interpreter) use log::*; +pub(in crate::interpreter) use log10::*; +pub(in crate::interpreter) use log2::*; +pub(in crate::interpreter) use max::*; +pub(in crate::interpreter) use min::*; +pub(in crate::interpreter) use mt_rand::*; +pub(in crate::interpreter) use pi::*; +pub(in crate::interpreter) use pow::*; +pub(in crate::interpreter) use rad2deg::*; +pub(in crate::interpreter) use rand::*; +pub(in crate::interpreter) use random_int::*; +pub(in crate::interpreter) use round::*; +pub(in crate::interpreter) use sin::*; +pub(in crate::interpreter) use sinh::*; +pub(in crate::interpreter) use sqrt::*; +pub(in crate::interpreter) use tan::*; +pub(in crate::interpreter) use tanh::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs b/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs index 3659a6ea05..fbe37e9d5f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs @@ -1,16 +1,36 @@ //! Purpose: -//! Declarative eval registry entry for `mt_rand`. +//! Eval registry entry and implementation for `mt_rand`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the random-number hook. +//! - Eval mirrors the existing `rand()` range behavior for `mt_rand()`. + +use super::super::super::*; eval_builtin! { name: "mt_rand", area: Math, params: [min, max], - direct: Random, - values: Random, + direct: MtRand, + values: MtRand, +} + +/// Evaluates PHP `mt_rand()` over zero args or an inclusive range. +pub(in crate::interpreter) fn eval_builtin_mt_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_rand(args, context, scope, values) +} + +/// Dispatches by-value `mt_rand()` calls after argument binding. +pub(in crate::interpreter) fn eval_mt_rand_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + eval_rand_values_result(evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/pi.rs b/crates/elephc-magician/src/interpreter/builtins/math/pi.rs index ebe283b84c..7d0461d321 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/pi.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/pi.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `pi`. +//! Eval registry entry and implementation for `pi`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing constant hook. +//! - `pi()` accepts no arguments and returns the platform `f64` PI constant. + +use super::super::super::*; eval_builtin! { name: "pi", @@ -14,3 +16,21 @@ eval_builtin! { direct: Pi, values: Pi, } + +/// Evaluates PHP `pi()` with no eval arguments. +pub(in crate::interpreter) fn eval_builtin_pi( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_pi_result(values) +} + +/// Returns PHP `pi()` as an already evaluated builtin result. +pub(in crate::interpreter) fn eval_pi_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.float(std::f64::consts::PI) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/pow.rs b/crates/elephc-magician/src/interpreter/builtins/math/pow.rs index 04b988cd2b..54c7857f12 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/pow.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/pow.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `pow`. +//! Eval registry entry and implementation for `pow`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing exponentiation hook. +//! - Runtime numeric coercion and PHP edge cases stay delegated to `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "pow", @@ -14,3 +16,27 @@ eval_builtin! { direct: Pow, values: Pow, } + +/// Evaluates PHP `pow()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_pow( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_pow_result(left, right, values) +} + +/// Applies PHP `pow()` to two already evaluated values. +pub(in crate::interpreter) fn eval_pow_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.pow(left, right) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs b/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs index f32aad60e8..cce11e09ae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `rad2deg`. +//! Eval registry entry and implementation for `rad2deg`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "rad2deg", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Rad2deg, + values: Rad2deg, +} + +/// Evaluates PHP `rad2deg()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_rad2deg( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_rad2deg_result(num, values) +} + +/// Applies PHP `rad2deg()` to one already evaluated value. +pub(in crate::interpreter) fn eval_rad2deg_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.to_degrees()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/rand.rs b/crates/elephc-magician/src/interpreter/builtins/math/rand.rs index 59b8847de0..13703b59b1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/rand.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/rand.rs @@ -1,16 +1,68 @@ //! Purpose: -//! Declarative eval registry entry for `rand`. +//! Eval registry entry and implementation for `rand`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the random-number hook. +//! - `rand()` accepts either no arguments or an inclusive min/max range. + +use super::super::super::*; eval_builtin! { name: "rand", area: Math, params: [min, max], - direct: Random, - values: Random, + direct: Rand, + values: Rand, +} + +/// Evaluates PHP `rand()` over zero args or an inclusive range. +pub(in crate::interpreter) fn eval_builtin_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_rand_result(None, None, values), + [min, max] => { + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_rand_result(Some(min), Some(max), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `rand()` calls after argument binding. +pub(in crate::interpreter) fn eval_rand_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_rand_result(None, None, values), + [min, max] => eval_rand_result(Some(*min), Some(*max), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns one non-cryptographic random integer using PHP's inclusive range rules. +pub(in crate::interpreter) fn eval_rand_result( + min: Option, + max: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (min, max) = match (min, max) { + (None, None) => (0, i64::from(i32::MAX)), + (Some(min), Some(max)) => (eval_int_value(min, values)?, eval_int_value(max, values)?), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let low = min.min(max); + let high = min.max(max); + let width = (i128::from(high) - i128::from(low) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(low) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs b/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs index af2a3a6f75..0bc4846bdf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs @@ -1,16 +1,63 @@ //! Purpose: -//! Declarative eval registry entry for `random_int`. +//! Eval registry entry and implementation for `random_int`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the random-number hook. +//! - Eval uses the same process-local pseudo-random source as the existing +//! interpreter implementation; invalid ranges are runtime fatals. + +use super::super::super::*; eval_builtin! { name: "random_int", area: Math, params: [min, max], - direct: Random, - values: Random, + direct: RandomInt, + values: RandomInt, +} + +/// Evaluates PHP `random_int()` over an inclusive integer range. +pub(in crate::interpreter) fn eval_builtin_random_int( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [min, max] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let min = eval_expr(min, context, scope, values)?; + let max = eval_expr(max, context, scope, values)?; + eval_random_int_result(min, max, values) +} + +/// Dispatches by-value `random_int()` calls after argument binding. +pub(in crate::interpreter) fn eval_random_int_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [min, max] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_random_int_result(*min, *max, values) +} + +/// Returns one eval `random_int()` value in the inclusive range `[min, max]`. +pub(in crate::interpreter) fn eval_random_int_result( + min: RuntimeCellHandle, + max: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let min = eval_int_value(min, values)?; + let max = eval_int_value(max, values)?; + if min > max { + return Err(EvalStatus::RuntimeFatal); + } + let width = (i128::from(max) - i128::from(min) + 1) as u128; + let offset = (eval_random_u128() % width) as i128; + let sampled = i128::from(min) + offset; + let sampled = i64::try_from(sampled).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(sampled) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/round.rs b/crates/elephc-magician/src/interpreter/builtins/math/round.rs index 7d6ebebc15..c93d7e02f0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/round.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/round.rs @@ -1,12 +1,14 @@ //! Purpose: -//! Declarative eval registry entry for `round`. +//! Eval registry entry and implementation for `round`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing numeric rounding hook. +//! - The optional precision defaults through registry metadata; direct calls +//! still evaluate arguments in source order. +use super::super::super::*; use super::super::spec::EvalBuiltinDefaultValue; eval_builtin! { @@ -16,3 +18,33 @@ eval_builtin! { direct: Round, values: Round, } + +/// Evaluates PHP `round()` over one value and an optional precision expression. +pub(in crate::interpreter) fn eval_builtin_round( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [num] => { + let num = eval_expr(num, context, scope, values)?; + eval_round_result(num, None, values) + } + [num, precision] => { + let num = eval_expr(num, context, scope, values)?; + let precision = eval_expr(precision, context, scope, values)?; + eval_round_result(num, Some(precision), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `round()` to already evaluated arguments. +pub(in crate::interpreter) fn eval_round_result( + num: RuntimeCellHandle, + precision: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + values.round(num, precision) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/runtime.rs b/crates/elephc-magician/src/interpreter/builtins/math/runtime.rs deleted file mode 100644 index c3347fba80..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/math/runtime.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Purpose: -//! Runtime helper implementations for numeric builtins whose metadata lives in -//! per-builtin declaration files. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks::direct`. -//! -//! Key details: -//! - Helpers evaluate direct-call `EvalExpr` arguments before delegating to -//! runtime numeric operations. - -use super::super::super::*; - -/// Evaluates PHP `abs(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_abs( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.abs(value) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sin.rs b/crates/elephc-magician/src/interpreter/builtins/math/sin.rs index 32b96a1456..7364a981ae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/sin.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/sin.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `sin`. +//! Eval registry entry and implementation for `sin`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "sin", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Sin, + values: Sin, +} + +/// Evaluates PHP `sin()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_sin_result(num, values) +} + +/// Applies PHP `sin()` to one already evaluated value. +pub(in crate::interpreter) fn eval_sin_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.sin()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs b/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs index e6a57c6d39..173a36fc3a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `sinh`. +//! Eval registry entry and implementation for `sinh`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "sinh", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Sinh, + values: Sinh, +} + +/// Evaluates PHP `sinh()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sinh( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_sinh_result(num, values) +} + +/// Applies PHP `sinh()` to one already evaluated value. +pub(in crate::interpreter) fn eval_sinh_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.sinh()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs b/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs index 809130ae91..8e9574321d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `sqrt`. +//! Eval registry entry and implementation for `sqrt`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing square-root hook. +//! - Runtime numeric coercions stay delegated to `RuntimeValueOps`. + +use super::super::super::*; eval_builtin! { name: "sqrt", @@ -14,3 +16,25 @@ eval_builtin! { direct: Sqrt, values: Sqrt, } + +/// Evaluates PHP `sqrt()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_sqrt( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_sqrt_result(num, values) +} + +/// Applies PHP `sqrt()` to one already evaluated value. +pub(in crate::interpreter) fn eval_sqrt_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.sqrt(num) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/math/tan.rs b/crates/elephc-magician/src/interpreter/builtins/math/tan.rs index eafd59207f..143ad07098 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/tan.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/tan.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `tan`. +//! Eval registry entry and implementation for `tan`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "tan", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Tan, + values: Tan, +} + +/// Evaluates PHP `tan()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_tan( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_tan_result(num, values) +} + +/// Applies PHP `tan()` to one already evaluated value. +pub(in crate::interpreter) fn eval_tan_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.tan()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs b/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs index 4efbf057cd..aaaf840110 100644 --- a/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs +++ b/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs @@ -1,16 +1,42 @@ //! Purpose: -//! Declarative eval registry entry for `tanh`. +//! Eval registry entry and implementation for `tanh`. //! //! Called from: -//! - `crate::interpreter::builtins::math`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing unary float math hook. +//! - The argument is coerced through eval numeric semantics before applying +//! the Rust `f64` operation matching PHP's math behavior. + +use super::super::super::*; eval_builtin! { name: "tanh", area: Math, params: [num], - direct: FloatUnary, - values: FloatUnary, + direct: Tanh, + values: Tanh, +} + +/// Evaluates PHP `tanh()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_tanh( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [num] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let num = eval_expr(num, context, scope, values)?; + eval_tanh_result(num, values) +} + +/// Applies PHP `tanh()` to one already evaluated value. +pub(in crate::interpreter) fn eval_tanh_result( + num: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let num = eval_float_value(num, values)?; + values.float(num.tanh()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/math.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/math.rs index 801e91ac35..643f8a6483 100644 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/math.rs +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/math.rs @@ -1,270 +1,24 @@ //! Purpose: -//! Numeric math, clamp, min, and max helpers. +//! Shared numeric helper algorithms used by per-builtin math home files. //! //! Called from: -//! - `crate::interpreter::builtins::scalars` re-exports. +//! - `crate::interpreter::builtins::math::{min,max}`. //! //! Key details: -//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. +//! - Runtime cells remain opaque and ordering stays delegated to +//! `RuntimeValueOps::compare`. use super::super::super::*; -use super::super::*; -use super::*; - -/// Evaluates PHP one-argument floating-point math builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_float_unary( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_float_unary_result(name, value, values) -} - -/// Dispatches an evaluated value through the matching PHP floating-point unary math function. -pub(in crate::interpreter) fn eval_float_unary_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_float_value(value, values)?; - let result = match name { - "acos" => value.acos(), - "asin" => value.asin(), - "atan" => value.atan(), - "cos" => value.cos(), - "cosh" => value.cosh(), - "deg2rad" => value.to_radians(), - "exp" => value.exp(), - "log2" => value.log2(), - "log10" => value.log10(), - "rad2deg" => value.to_degrees(), - "sin" => value.sin(), - "sinh" => value.sinh(), - "tan" => value.tan(), - "tanh" => value.tanh(), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.float(result) -} - -/// Evaluates PHP two-argument floating-point math builtins over eval expressions. -pub(in crate::interpreter) fn eval_builtin_float_pair( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_float_pair_result(name, left, right, values) -} - -/// Dispatches an evaluated pair through PHP `atan2()` or `hypot()`. -pub(in crate::interpreter) fn eval_float_pair_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left = eval_float_value(left, values)?; - let right = eval_float_value(right, values)?; - let result = match name { - "atan2" => left.atan2(right), - "hypot" => left.hypot(right), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.float(result) -} - -/// Evaluates PHP `log($num, $base = e)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_log( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [num] => { - let num = eval_expr(num, context, scope, values)?; - eval_log_result(num, None, values) - } - [num, base] => { - let num = eval_expr(num, context, scope, values)?; - let base = eval_expr(base, context, scope, values)?; - eval_log_result(num, Some(base), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `log()` from already evaluated arguments. -pub(in crate::interpreter) fn eval_log_result( - num: RuntimeCellHandle, - base: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let num = eval_float_value(num, values)?; - let result = match base { - Some(base) => num.log(eval_float_value(base, values)?), - None => num.ln(), - }; - values.float(result) -} - -/// Evaluates PHP `intdiv(...)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_intdiv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_intdiv_result(left, right, values) -} - -/// Computes PHP integer division from already evaluated arguments. -pub(in crate::interpreter) fn eval_intdiv_result( - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left = eval_int_value(left, values)?; - let right = eval_int_value(right, values)?; - if right == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let result = left.checked_div(right).ok_or(EvalStatus::RuntimeFatal)?; - values.int(result) -} - -/// Evaluates PHP floating-point binary math builtins over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_float_binary( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_float_binary_result(name, left, right, values) -} - -/// Dispatches an evaluated pair through the matching PHP float math hook. -pub(in crate::interpreter) fn eval_float_binary_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "fdiv" => values.fdiv(left, right), - "fmod" => values.fmod(left, right), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP `clamp($value, $min, $max)` over three eval expressions. -pub(in crate::interpreter) fn eval_builtin_clamp( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value, min, max] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let min = eval_expr(min, context, scope, values)?; - let max = eval_expr(max, context, scope, values)?; - eval_clamp_result(value, min, max, values) -} - -/// Selects the inclusive clamp result after validating bound order and NaN bounds. -pub(in crate::interpreter) fn eval_clamp_result( - value: RuntimeCellHandle, - min: RuntimeCellHandle, - max: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_clamp_bound_is_nan(min, values)? || eval_clamp_bound_is_nan(max, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let invalid_bounds = values.compare(EvalBinOp::Gt, min, max)?; - if values.truthy(invalid_bounds)? { - return Err(EvalStatus::RuntimeFatal); - } - let above_max = values.compare(EvalBinOp::Gt, value, max)?; - if values.truthy(above_max)? { - return Ok(max); - } - let below_min = values.compare(EvalBinOp::Lt, value, min)?; - if values.truthy(below_min)? { - return Ok(min); - } - Ok(value) -} - -/// Returns whether a clamp bound is a floating-point NaN value. -pub(in crate::interpreter) fn eval_clamp_bound_is_nan( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? != EVAL_TAG_FLOAT { - return Ok(false); - } - Ok(eval_float_value(value, values)?.is_nan()) -} - -/// Evaluates PHP numeric `min(...)` and `max(...)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_min_max( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_min_max_result(name, &evaluated_args, values) -} /// Selects the smallest or largest evaluated cell using runtime comparison hooks. -pub(in crate::interpreter) fn eval_min_max_result( - name: &str, +pub(in crate::interpreter) fn eval_min_max_selected( evaluated_args: &[RuntimeCellHandle], + op: EvalBinOp, values: &mut impl RuntimeValueOps, ) -> Result { let Some((&first, rest)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; - let op = match name { - "min" => EvalBinOp::Lt, - "max" => EvalBinOp::Gt, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; let mut selected = first; for candidate in rest { let better = values.compare(op, *candidate, selected)?; diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/simple.rs b/crates/elephc-magician/src/interpreter/builtins/strings/simple.rs index 52568f1a2c..fa2952d965 100644 --- a/crates/elephc-magician/src/interpreter/builtins/strings/simple.rs +++ b/crates/elephc-magician/src/interpreter/builtins/strings/simple.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Small scalar string wrappers such as sqrt, strrev, and chr. +//! Small scalar string wrappers such as strrev and chr. //! //! Called from: //! - `crate::interpreter::builtins::strings` re-exports. @@ -10,20 +10,6 @@ use super::super::super::*; use super::super::*; -/// Evaluates PHP's `sqrt(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_sqrt( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.sqrt(value) -} - /// Evaluates PHP's `strrev(...)` over one eval expression. pub(in crate::interpreter) fn eval_builtin_strrev( args: &[EvalExpr], From ff4612869fa900dadb6f9ee7845f6a7ea20427ca Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 15:18:24 +0200 Subject: [PATCH 1101/1208] refactor(magician): co-locate json builtin wrappers --- .../src/interpreter/builtins/hooks/direct.rs | 27 +- .../src/interpreter/builtins/hooks/values.rs | 42 +- .../interpreter/builtins/json/json_decode.rs | 293 +++++- .../interpreter/builtins/json/json_encode.rs | 590 ++++++++++- .../builtins/json/json_last_error.rs | 32 +- .../builtins/json/json_last_error_msg.rs | 32 +- .../builtins/json/json_validate.rs | 98 +- .../src/interpreter/builtins/json/mod.rs | 101 +- .../elephc-magician/src/interpreter/json.rs | 922 ------------------ crates/elephc-magician/src/interpreter/mod.rs | 3 - 10 files changed, 1083 insertions(+), 1057 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/json.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index aee3e3d1b8..10213e48da 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -38,10 +38,11 @@ use super::super::{ eval_builtin_is_real, eval_builtin_is_resource, eval_builtin_is_scalar, eval_builtin_is_string, eval_builtin_log, eval_builtin_log2, eval_builtin_log10, eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, - eval_builtin_json_call, eval_builtin_max, eval_builtin_min, eval_builtin_mt_rand, - eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_pi, eval_builtin_pow, - eval_builtin_rad2deg, eval_builtin_rand, eval_builtin_random_int, eval_builtin_range, - eval_builtin_round, + eval_builtin_json_decode, eval_builtin_json_encode, eval_builtin_json_last_error, + eval_builtin_json_last_error_msg, eval_builtin_json_validate, eval_builtin_max, + eval_builtin_min, eval_builtin_mt_rand, eval_builtin_network_env_call, eval_builtin_nl2br, + eval_builtin_pi, eval_builtin_pow, eval_builtin_rad2deg, eval_builtin_rand, + eval_builtin_random_int, eval_builtin_range, eval_builtin_round, eval_builtin_raw_memory_call, eval_builtin_regex_call, eval_builtin_sin, eval_builtin_sinh, eval_builtin_str_pad, eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, @@ -192,8 +193,16 @@ pub(in crate::interpreter) enum EvalDirectHook { HtmlEntity, /// Dispatches `intdiv(...)`. Intdiv, - /// Dispatches JSON builtins. - Json, + /// Dispatches `json_decode(...)`. + JsonDecode, + /// Dispatches `json_encode(...)`. + JsonEncode, + /// Dispatches `json_last_error()`. + JsonLastError, + /// Dispatches `json_last_error_msg()`. + JsonLastErrorMsg, + /// Dispatches `json_validate(...)`. + JsonValidate, /// Dispatches `log(...)`. Log, /// Dispatches `log2(...)`. @@ -379,7 +388,11 @@ impl EvalDirectHook { Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), Self::HtmlEntity => eval_builtin_html_entity(name, args, context, scope, values), Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), - Self::Json => eval_builtin_json_call(name, args, context, scope, values), + Self::JsonDecode => eval_builtin_json_decode(args, context, scope, values), + Self::JsonEncode => eval_builtin_json_encode(args, context, scope, values), + Self::JsonLastError => eval_builtin_json_last_error(args, context, values), + Self::JsonLastErrorMsg => eval_builtin_json_last_error_msg(args, context, values), + Self::JsonValidate => eval_builtin_json_validate(args, context, scope, values), Self::Log => eval_builtin_log(args, context, scope, values), Self::Log2 => eval_builtin_log2(args, context, scope, values), Self::Log10 => eval_builtin_log10(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 4c97240ccf..d19b77d1f2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -37,12 +37,14 @@ use super::super::{ eval_is_string_result, eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, - eval_json_values_result, eval_log2_result, eval_log10_result, eval_log_result, - eval_max_result, eval_min_result, eval_mt_rand_values_result, - eval_network_env_values_result, eval_nl2br_result, eval_pi_result, eval_pow_result, - eval_rad2deg_result, eval_rand_values_result, eval_random_int_values_result, - eval_range_result, eval_regex_values_result, eval_round_result, eval_settype_values_result, - eval_sin_result, eval_sinh_result, eval_slashes_result, eval_sqrt_result, + eval_json_decode_values_result, eval_json_encode_values_result, eval_json_last_error_msg_result, + eval_json_last_error_result, eval_json_validate_values_result, eval_log2_result, + eval_log10_result, eval_log_result, eval_max_result, eval_min_result, + eval_mt_rand_values_result, eval_network_env_values_result, eval_nl2br_result, + eval_pi_result, eval_pow_result, eval_rad2deg_result, eval_rand_values_result, + eval_random_int_values_result, eval_range_result, eval_regex_values_result, eval_round_result, + eval_settype_values_result, eval_sin_result, eval_sinh_result, eval_slashes_result, + eval_sqrt_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, @@ -199,8 +201,16 @@ pub(in crate::interpreter) enum EvalValuesHook { HtmlEntity, /// Dispatches `intdiv(...)`. Intdiv, - /// Dispatches JSON builtins. - Json, + /// Dispatches `json_decode(...)`. + JsonDecode, + /// Dispatches `json_encode(...)`. + JsonEncode, + /// Dispatches `json_last_error()`. + JsonLastError, + /// Dispatches `json_last_error_msg()`. + JsonLastErrorMsg, + /// Dispatches `json_validate(...)`. + JsonValidate, /// Dispatches `log(...)`. Log, /// Dispatches `log2(...)`. @@ -413,7 +423,21 @@ impl EvalValuesHook { eval_html_entity_result(name, value, values) }), Self::Intdiv => two_args(evaluated_args, values, eval_intdiv_result), - Self::Json => eval_json_values_result(name, evaluated_args, context, values), + Self::JsonDecode => eval_json_decode_values_result(evaluated_args, context, values), + Self::JsonEncode => eval_json_encode_values_result(evaluated_args, context, values), + Self::JsonLastError => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_json_last_error_result(context, values) + } + Self::JsonLastErrorMsg => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_json_last_error_msg_result(context, values) + } + Self::JsonValidate => eval_json_validate_values_result(evaluated_args, context, values), Self::Log => match evaluated_args { [num] => eval_log_result(*num, None, values), [num, base] => eval_log_result(*num, Some(*base), values), diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs index 6a492966fb..4324e88625 100644 --- a/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs @@ -1,13 +1,17 @@ //! Purpose: -//! Declarative eval registry entry for `json_decode`. +//! Eval registry entry and dispatch wrappers for `json_decode`. //! //! Called from: -//! - `crate::interpreter::builtins::json`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the JSON decode hook. +//! - This file owns the decoder implementation, runtime-cell materialization, +//! direct wrapper, and by-value dispatch shape. +//! - JSON parse-error helpers live here and are reused by `json_validate`. +use super::super::super::*; use super::super::spec::EvalBuiltinDefaultValue; +use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; eval_builtin! { name: "json_decode", @@ -18,6 +22,285 @@ eval_builtin! { depth = EvalBuiltinDefaultValue::Int(512), flags = EvalBuiltinDefaultValue::Int(0), ], - direct: Json, - values: Json, + direct: JsonDecode, + values: JsonDecode, +} + +/// Evaluates PHP `json_decode()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_json_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [json] => { + let json = eval_expr(json, context, scope, values)?; + eval_json_decode_result(json, None, None, None, context, values) + } + [json, associative] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + eval_json_decode_result(json, Some(associative), None, None, context, values) + } + [json, associative, depth] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_decode_result(json, Some(associative), Some(depth), None, context, values) + } + [json, associative, depth, flags] => { + let json = eval_expr(json, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_decode_result(json, Some(associative), Some(depth), Some(flags), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `json_decode()` calls after argument binding. +pub(in crate::interpreter) fn eval_json_decode_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [json] => eval_json_decode_result(*json, None, None, None, context, values), + [json, associative] => eval_json_decode_result(*json, Some(*associative), None, None, context, values), + [json, associative, depth] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + None, + context, + values, + ), + [json, associative, depth, flags] => eval_json_decode_result( + *json, + Some(*associative), + Some(*depth), + Some(*flags), + context, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Decodes one JSON string into eval runtime cells and records PHP JSON parse state. +fn eval_json_decode_result( + json: RuntimeCellHandle, + associative: Option, + depth: Option, + flags: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + let supported_flags = EVAL_JSON_BIGINT_AS_STRING + | EVAL_JSON_INVALID_UTF8_IGNORE + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE + | EVAL_JSON_THROW_ON_ERROR; + if flags & !supported_flags != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let objects_as_assoc = associative + .map(|associative| values.truthy(associative)) + .transpose()? + .unwrap_or(false); + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let bytes = values.string_bytes(json)?; + let decoded_result = if flags & EVAL_JSON_INVALID_UTF8_SUBSTITUTE != 0 { + json_validate::decode_result_substituting_invalid_utf8(&bytes, depth as usize) + } else if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) + } else { + json_validate::decode_result(&bytes, depth as usize) + }; + let decoded = match decoded_result { + Ok(decoded) => decoded, + Err(error) => { + let (code, message) = eval_json_parse_error_details(error, &bytes); + if flags & EVAL_JSON_THROW_ON_ERROR != 0 { + return eval_throw_json_exception(code, &message, context, values); + } + context.set_json_error(code, message); + return values.null(); + } + }; + context.clear_json_error(); + eval_json_decode_to_cell(decoded, flags, objects_as_assoc, values) +} + +/// Materializes one parsed JSON value as an eval runtime cell. +fn eval_json_decode_to_cell( + value: JsonValue, + flags: i64, + objects_as_assoc: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + match value { + JsonValue::Null => values.null(), + JsonValue::Bool(value) => values.bool_value(value), + JsonValue::Number(value) => eval_json_decode_number_to_cell(&value, flags, values), + JsonValue::String(value) => values.string_bytes_value(&value), + JsonValue::Array(elements) => { + let mut result = values.array_new(elements.len())?; + for (index, element) in elements.into_iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let element = eval_json_decode_to_cell(element, flags, objects_as_assoc, values)?; + result = values.array_set(result, key, element)?; + } + Ok(result) + } + JsonValue::Object(entries) => { + if !objects_as_assoc { + return eval_json_decode_object_to_cell(entries, flags, values); + } + let mut result = values.assoc_new(entries.len())?; + for (key, value) in entries { + let key = values.string_bytes_value(&key)?; + let value = eval_json_decode_to_cell(value, flags, objects_as_assoc, values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) + } + } +} + +/// Materializes a parsed JSON object as a `stdClass` runtime object. +fn eval_json_decode_object_to_cell( + entries: Vec<(Vec, JsonValue)>, + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + for (key, value) in entries { + let key = std::str::from_utf8(&key).map_err(|_| EvalStatus::RuntimeFatal)?; + let value = eval_json_decode_to_cell(value, flags, false, values)?; + values.property_set(object, key, value)?; + } + Ok(object) +} + +/// Materializes one JSON number as an int when possible and as a float otherwise. +fn eval_json_decode_number_to_cell( + value: &[u8], + flags: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + if flags & EVAL_JSON_BIGINT_AS_STRING != 0 && eval_json_number_overflows_i64(value) { + return values.string_bytes_value(value); + } + let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; + if !value.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E')) { + if let Ok(integer) = value.parse::() { + return values.int(integer); + } + } + let float = value.parse::().map_err(|_| EvalStatus::RuntimeFatal)?; + values.float(float) +} + +/// Returns true when one integer-grammar JSON number exceeds PHP's int range. +fn eval_json_number_overflows_i64(value: &[u8]) -> bool { + if value.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) { + return false; + } + let (negative, digits) = if let Some(digits) = value.strip_prefix(b"-") { + (true, digits) + } else { + (false, value) + }; + let threshold = if negative { + b"9223372036854775808".as_slice() + } else { + b"9223372036854775807".as_slice() + }; + digits.len() > threshold.len() || digits.len() == threshold.len() && digits > threshold +} + +/// Records one parser error into the eval-local PHP JSON error slots. +pub(super) fn eval_record_json_parse_error( + context: &mut ElephcEvalContext, + error: JsonParseError, + bytes: &[u8], +) { + let (code, message) = eval_json_parse_error_details(error, bytes); + context.set_json_error(code, message); +} + +/// Builds the PHP JSON error code and message for one parser failure. +fn eval_json_parse_error_details(error: JsonParseError, bytes: &[u8]) -> (i64, String) { + let (code, message) = eval_json_parse_error_status(error.kind()); + let message = eval_json_error_message_with_location(message, bytes, error.offset()); + (code, message) +} + +/// Creates and schedules a `JsonException` through eval's normal Throwable channel. +pub(super) fn eval_throw_json_exception( + code: i64, + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.set_json_error(code, message.to_string()); + let exception = values.new_object("JsonException")?; + let message = values.string(message)?; + let code = values.int(code)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Maps eval JSON parser failures to PHP `JSON_ERROR_*` codes and messages. +fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str) { + match error { + JsonParseErrorKind::Depth => (EVAL_JSON_ERROR_DEPTH, "Maximum stack depth exceeded"), + JsonParseErrorKind::Syntax => (EVAL_JSON_ERROR_SYNTAX, "Syntax error"), + JsonParseErrorKind::ControlChar => ( + EVAL_JSON_ERROR_CTRL_CHAR, + "Control character error, possibly incorrectly encoded", + ), + JsonParseErrorKind::Utf8 => (EVAL_JSON_ERROR_UTF8, EVAL_JSON_UTF8_MESSAGE), + JsonParseErrorKind::Utf16 => ( + EVAL_JSON_ERROR_UTF16, + "Single unpaired UTF-16 surrogate in unicode escape", + ), + } +} + +/// Adds PHP's JSON line/column suffix to one base error message. +fn eval_json_error_message_with_location(message: &str, bytes: &[u8], offset: usize) -> String { + let (line, column) = eval_json_error_location(bytes, offset); + format!("{message} near location {line}:{column}") +} + +/// Converts a zero-based JSON byte offset into PHP-style one-based line and column. +fn eval_json_error_location(bytes: &[u8], offset: usize) -> (usize, usize) { + let mut line = 1; + let mut column = 1; + let offset = offset.min(bytes.len()); + for byte in &bytes[..offset] { + if *byte == b'\n' { + line += 1; + column = 1; + } else { + column += 1; + } + } + (line, column) } diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs index 759d67d33f..ffc5f0b3fb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs @@ -1,12 +1,16 @@ //! Purpose: -//! Declarative eval registry entry for `json_encode`. +//! Eval registry entry and dispatch wrappers for `json_encode`. //! //! Called from: -//! - `crate::interpreter::builtins::json`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the JSON encode hook. +//! - This file owns the encoder implementation, direct wrapper, and by-value +//! dispatch shape. +//! - Shared `JSON_THROW_ON_ERROR` exception construction is reused from +//! `json_decode` instead of a separate area-level helper module. +use super::super::super::*; use super::super::spec::EvalBuiltinDefaultValue; eval_builtin! { @@ -17,6 +21,582 @@ eval_builtin! { flags = EvalBuiltinDefaultValue::Int(0), depth = EvalBuiltinDefaultValue::Int(512), ], - direct: Json, - values: Json, + direct: JsonEncode, + values: JsonEncode, +} + +/// Evaluates PHP `json_encode()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_json_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_json_encode_result(value, None, None, context, values) + } + [value, flags] => { + let value = eval_expr(value, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_encode_result(value, Some(flags), None, context, values) + } + [value, flags, depth] => { + let value = eval_expr(value, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_encode_result(value, Some(flags), Some(depth), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `json_encode()` calls after argument binding. +pub(in crate::interpreter) fn eval_json_encode_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_json_encode_result(*value, None, None, context, values), + [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values), + [value, flags, depth] => eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Encodes one runtime cell as a JSON string for eval's supported flag subset. +fn eval_json_encode_result( + value: RuntimeCellHandle, + flags: Option, + depth: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + let supported_flags = EVAL_JSON_HEX_TAG + | EVAL_JSON_HEX_AMP + | EVAL_JSON_HEX_APOS + | EVAL_JSON_HEX_QUOT + | EVAL_JSON_UNESCAPED_SLASHES + | EVAL_JSON_UNESCAPED_UNICODE + | EVAL_JSON_FORCE_OBJECT + | EVAL_JSON_PRETTY_PRINT + | EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR + | EVAL_JSON_PRESERVE_ZERO_FRACTION + | EVAL_JSON_INVALID_UTF8_IGNORE + | EVAL_JSON_INVALID_UTF8_SUBSTITUTE + | EVAL_JSON_THROW_ON_ERROR; + let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; + if flags & !supported_flags != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let mut output = Vec::new(); + let mut error = None; + eval_json_encode_append( + value, + values, + flags, + depth as usize, + 0, + &mut Vec::new(), + &mut error, + &mut output, + )?; + if let Some(error) = error { + context.set_json_error(error.code, error.message); + if flags & EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR == 0 { + if flags & EVAL_JSON_THROW_ON_ERROR != 0 { + return super::json_decode::eval_throw_json_exception(error.code, error.message, context, values); + } + return values.bool_value(false); + } + } else { + context.clear_json_error(); + } + values.string_bytes_value(&output) +} + +/// Appends one JSON value to the output buffer. +fn eval_json_encode_append( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_INT => output.extend_from_slice(&values.string_bytes(value)?), + EVAL_TAG_FLOAT => { + eval_json_encode_append_float(value, values, flags, error, output)?; + } + EVAL_TAG_STRING => eval_json_encode_append_string( + &values.string_bytes(value)?, + flags, + EvalJsonStringPosition::Value, + error, + output, + )?, + EVAL_TAG_BOOL => { + if values.truthy(value)? { + output.extend_from_slice(b"true"); + } else { + output.extend_from_slice(b"false"); + } + } + EVAL_TAG_ARRAY => { + eval_json_encode_append_indexed_array( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_ASSOC => { + eval_json_encode_append_assoc( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_OBJECT => { + eval_json_encode_append_object( + value, + values, + flags, + depth_limit, + depth, + arrays_seen, + error, + output, + )?; + } + EVAL_TAG_NULL | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), + _ => return Err(EvalStatus::UnsupportedConstruct), + } + Ok(()) +} + +#[derive(Clone, Copy)] +struct EvalJsonEncodeError { + code: i64, + message: &'static str, +} + +/// Marks whether a JSON string is being encoded as a value or as an object key. +#[derive(Clone, Copy)] +enum EvalJsonStringPosition { + Value, + Key, +} + +/// Appends one JSON float while preserving a `.0` suffix when requested. +fn eval_json_encode_append_float( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let float = eval_float_value(value, values)?; + if !float.is_finite() { + *error = Some(EvalJsonEncodeError { + code: EVAL_JSON_ERROR_INF_OR_NAN, + message: EVAL_JSON_INF_OR_NAN_MESSAGE, + }); + output.push(b'0'); + return Ok(()); + } + let bytes = values.string_bytes(value)?; + output.extend_from_slice(&bytes); + if flags & EVAL_JSON_PRESERVE_ZERO_FRACTION != 0 + && !bytes.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) + { + output.extend_from_slice(b".0"); + } + Ok(()) +} + +/// Appends one indexed eval array as a JSON array or forced JSON object. +fn eval_json_encode_append_indexed_array( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let force_object = flags & EVAL_JSON_FORCE_OBJECT != 0; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(if force_object { b'{' } else { b'[' }); + let len = values.array_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.array_iter_key(value, position)?; + if force_object { + eval_json_encode_append_string( + &values.string_bytes(key)?, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + } + let element = values.array_get(value, key)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(if force_object { b'}' } else { b']' }); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one associative eval array as a JSON object. +fn eval_json_encode_append_assoc( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(b'{'); + let len = values.array_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.array_iter_key(value, position)?; + eval_json_encode_append_string( + &values.string_bytes(key)?, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + let element = values.array_get(value, key)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(b'}'); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one eval runtime object as a JSON object. +fn eval_json_encode_append_object( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + flags: i64, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; + let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; + output.push(b'{'); + let len = values.object_property_len(value)?; + if pretty && len > 0 { + output.push(b'\n'); + } + for position in 0..len { + if position > 0 { + output.push(b','); + if pretty { + output.push(b'\n'); + } + } + if pretty { + eval_json_encode_pretty_indent(output, depth + 1); + } + let key = values.object_property_iter_key(value, position)?; + let key_bytes = values.string_bytes(key)?; + eval_json_encode_append_string( + &key_bytes, + flags & !EVAL_JSON_NUMERIC_CHECK, + EvalJsonStringPosition::Key, + error, + output, + )?; + eval_json_encode_append_colon(flags, output); + let property = std::str::from_utf8(&key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let element = values.property_get(value, property)?; + eval_json_encode_append( + element, + values, + flags, + depth_limit, + depth + 1, + arrays_seen, + error, + output, + )?; + } + if pretty && len > 0 { + output.push(b'\n'); + eval_json_encode_pretty_indent(output, depth); + } + output.push(b'}'); + arrays_seen.pop(); + Ok(()) +} + +/// Appends a JSON object colon, including pretty-print spacing when active. +fn eval_json_encode_append_colon(flags: i64, output: &mut Vec) { + if flags & EVAL_JSON_PRETTY_PRINT != 0 { + output.extend_from_slice(b": "); + } else { + output.push(b':'); + } +} + +/// Appends PHP's four-space JSON pretty-print indentation for one nesting level. +fn eval_json_encode_pretty_indent(output: &mut Vec, depth: usize) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} + +/// Records entry into one JSON array/object, rejecting depth overrun and recursion. +fn eval_json_encode_enter_array( + value: RuntimeCellHandle, + depth_limit: usize, + depth: usize, + arrays_seen: &mut Vec, +) -> Result<(), EvalStatus> { + if depth >= depth_limit { + return Err(EvalStatus::RuntimeFatal); + } + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + return Err(EvalStatus::RuntimeFatal); + } + arrays_seen.push(address); + Ok(()) +} + +/// Appends one JSON string with eval-supported PHP flag handling. +fn eval_json_encode_append_string( + bytes: &[u8], + flags: i64, + position: EvalJsonStringPosition, + error: &mut Option, + output: &mut Vec, +) -> Result<(), EvalStatus> { + if flags & EVAL_JSON_NUMERIC_CHECK != 0 { + if let Some(number) = eval_json_numeric_check_bytes(bytes) { + output.extend_from_slice(&number); + return Ok(()); + } + } + let start_len = output.len(); + output.push(b'"'); + if let Ok(value) = std::str::from_utf8(bytes) { + for character in value.chars() { + eval_json_encode_append_char(character, flags, output); + } + } else if flags & (EVAL_JSON_INVALID_UTF8_IGNORE | EVAL_JSON_INVALID_UTF8_SUBSTITUTE) == 0 { + output.truncate(start_len); + *error = Some(EvalJsonEncodeError { + code: EVAL_JSON_ERROR_UTF8, + message: EVAL_JSON_UTF8_MESSAGE, + }); + match position { + EvalJsonStringPosition::Value => output.extend_from_slice(b"null"), + EvalJsonStringPosition::Key => output.extend_from_slice(b"\"\""), + } + return Ok(()); + } else { + eval_json_encode_append_invalid_utf8_bytes(bytes, flags, output)?; + } + output.push(b'"'); + Ok(()) +} + +/// Appends one valid UTF-8 character using PHP JSON string escaping rules. +fn eval_json_encode_append_char(character: char, flags: i64, output: &mut Vec) { + if character.is_ascii() { + eval_json_encode_append_ascii_byte(character as u8, flags, output); + } else if flags & EVAL_JSON_UNESCAPED_UNICODE != 0 { + let mut buffer = [0_u8; 4]; + output.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes()); + } else { + eval_json_encode_append_unicode_escape(character as u32, output); + } +} + +/// Appends one ASCII byte using JSON escaping rules shared by UTF-8 and fallback paths. +fn eval_json_encode_append_ascii_byte(byte: u8, flags: i64, output: &mut Vec) { + match byte { + b'"' if flags & EVAL_JSON_HEX_QUOT != 0 => output.extend_from_slice(b"\\u0022"), + b'"' => output.extend_from_slice(b"\\\""), + b'\\' => output.extend_from_slice(b"\\\\"), + b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { + output.extend_from_slice(b"\\/"); + } + b'/' => output.push(b'/'), + b'<' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003C"), + b'>' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003E"), + b'&' if flags & EVAL_JSON_HEX_AMP != 0 => output.extend_from_slice(b"\\u0026"), + b'\'' if flags & EVAL_JSON_HEX_APOS != 0 => output.extend_from_slice(b"\\u0027"), + b'\x08' => output.extend_from_slice(b"\\b"), + b'\x0c' => output.extend_from_slice(b"\\f"), + b'\n' => output.extend_from_slice(b"\\n"), + b'\r' => output.extend_from_slice(b"\\r"), + b'\t' => output.extend_from_slice(b"\\t"), + control @ 0x00..=0x1f => { + output.extend_from_slice(format!("\\u{control:04x}").as_bytes()); + } + _ => output.push(byte), + } +} + +/// Appends valid scalar values as PHP JSON `\uXXXX` escapes, using surrogate pairs when needed. +fn eval_json_encode_append_unicode_escape(codepoint: u32, output: &mut Vec) { + if codepoint <= 0xffff { + output.extend_from_slice(format!("\\u{codepoint:04x}").as_bytes()); + return; + } + + let codepoint = codepoint - 0x1_0000; + let high = 0xd800 + ((codepoint >> 10) & 0x3ff); + let low = 0xdc00 + (codepoint & 0x3ff); + output.extend_from_slice(format!("\\u{high:04x}\\u{low:04x}").as_bytes()); +} + +/// Appends malformed UTF-8 bytes according to PHP's JSON invalid-UTF-8 flags. +fn eval_json_encode_append_invalid_utf8_bytes( + mut bytes: &[u8], + flags: i64, + output: &mut Vec, +) -> Result<(), EvalStatus> { + while !bytes.is_empty() { + match std::str::from_utf8(bytes) { + Ok(value) => { + for character in value.chars() { + eval_json_encode_append_char(character, flags, output); + } + return Ok(()); + } + Err(error) => { + let valid = &bytes[..error.valid_up_to()]; + for character in std::str::from_utf8(valid) + .map_err(|_| EvalStatus::RuntimeFatal)? + .chars() + { + eval_json_encode_append_char(character, flags, output); + } + let invalid_len = error + .error_len() + .unwrap_or(bytes.len() - valid.len()) + .max(1); + if flags & EVAL_JSON_INVALID_UTF8_IGNORE == 0 { + eval_json_encode_append_char('\u{fffd}', flags, output); + } + bytes = &bytes[valid.len() + invalid_len.min(bytes.len() - valid.len())..]; + } + } + } + Ok(()) +} + +/// Returns the JSON number bytes for a PHP numeric string when `JSON_NUMERIC_CHECK` applies. +fn eval_json_numeric_check_bytes(bytes: &[u8]) -> Option> { + let value = std::str::from_utf8(bytes).ok()?.trim(); + if value.is_empty() { + return None; + } + let integer_grammar = value + .bytes() + .all(|byte| byte.is_ascii_digit() || matches!(byte, b'+' | b'-')); + if integer_grammar { + if let Ok(integer) = value.parse::() { + return Some(integer.to_string().into_bytes()); + } + } + let number = value.parse::().ok()?; + if number.is_finite() { + Some(number.to_string().into_bytes()) + } else { + None + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs index 6963cedaf1..1418be0600 100644 --- a/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs @@ -1,16 +1,38 @@ //! Purpose: -//! Declarative eval registry entry for `json_last_error`. +//! Eval registry entry and implementation for `json_last_error`. //! //! Called from: -//! - `crate::interpreter::builtins::json`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the JSON error-state hook. +//! - The result reads the current eval JSON error code from `ElephcEvalContext`. + +use super::super::super::*; eval_builtin! { name: "json_last_error", area: Json, params: [], - direct: Json, - values: Json, + direct: JsonLastError, + values: JsonLastError, +} + +/// Evaluates PHP `json_last_error()` with no eval arguments. +pub(in crate::interpreter) fn eval_builtin_json_last_error( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_json_last_error_result(context, values) +} + +/// Returns the current JSON error code. +pub(in crate::interpreter) fn eval_json_last_error_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(context.json_last_error()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs index 6df25ac9d1..9d3c41d3ae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs @@ -1,16 +1,38 @@ //! Purpose: -//! Declarative eval registry entry for `json_last_error_msg`. +//! Eval registry entry and implementation for `json_last_error_msg`. //! //! Called from: -//! - `crate::interpreter::builtins::json`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the JSON error-message hook. +//! - The result reads the current eval JSON error message from `ElephcEvalContext`. + +use super::super::super::*; eval_builtin! { name: "json_last_error_msg", area: Json, params: [], - direct: Json, - values: Json, + direct: JsonLastErrorMsg, + values: JsonLastErrorMsg, +} + +/// Evaluates PHP `json_last_error_msg()` with no eval arguments. +pub(in crate::interpreter) fn eval_builtin_json_last_error_msg( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_json_last_error_msg_result(context, values) +} + +/// Returns the current JSON error message. +pub(in crate::interpreter) fn eval_json_last_error_msg_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(context.json_last_error_msg()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs b/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs index e37fc5f8bf..6633c045f2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs +++ b/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs @@ -1,13 +1,19 @@ //! Purpose: -//! Declarative eval registry entry for `json_validate`. +//! Eval registry entry and dispatch wrappers for `json_validate`. //! //! Called from: -//! - `crate::interpreter::builtins::json`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Runtime behavior stays delegated to the JSON validation hook. +//! - This file owns the validation implementation, direct wrapper, and by-value +//! dispatch shape. +//! - JSON parse-error recording is reused from `json_decode` instead of a +//! separate area-level helper module. +use super::json_decode::eval_record_json_parse_error; +use super::super::super::*; use super::super::spec::EvalBuiltinDefaultValue; +use crate::json_validate; eval_builtin! { name: "json_validate", @@ -17,6 +23,88 @@ eval_builtin! { depth = EvalBuiltinDefaultValue::Int(512), flags = EvalBuiltinDefaultValue::Int(0), ], - direct: Json, - values: Json, + direct: JsonValidate, + values: JsonValidate, +} + +/// Evaluates PHP `json_validate()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_json_validate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [json] => { + let json = eval_expr(json, context, scope, values)?; + eval_json_validate_result(json, None, None, context, values) + } + [json, depth] => { + let json = eval_expr(json, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + eval_json_validate_result(json, Some(depth), None, context, values) + } + [json, depth, flags] => { + let json = eval_expr(json, context, scope, values)?; + let depth = eval_expr(depth, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_json_validate_result(json, Some(depth), Some(flags), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `json_validate()` calls after argument binding. +pub(in crate::interpreter) fn eval_json_validate_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [json] => eval_json_validate_result(*json, None, None, context, values), + [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values), + [json, depth, flags] => eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Validates JSON text with eval's current zero-flag JSON subset and records JSON state. +fn eval_json_validate_result( + json: RuntimeCellHandle, + depth: Option, + flags: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = flags + .map(|flags| eval_int_value(flags, values)) + .transpose()? + .unwrap_or(0); + if flags & !EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + return Err(EvalStatus::UnsupportedConstruct); + } + let depth = depth + .map(|depth| eval_int_value(depth, values)) + .transpose()? + .unwrap_or(512); + if depth <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + + let bytes = values.string_bytes(json)?; + let result = if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { + json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) + } else { + json_validate::decode_result(&bytes, depth as usize) + }; + match result { + Ok(_) => { + context.clear_json_error(); + values.bool_value(true) + } + Err(error) => { + eval_record_json_parse_error(context, error, &bytes); + values.bool_value(false) + } + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/json/mod.rs b/crates/elephc-magician/src/interpreter/builtins/json/mod.rs index 52beee2415..d5676f8b5f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/json/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/json/mod.rs @@ -1,20 +1,14 @@ //! Purpose: -//! Declarative eval registry entries and dispatch adapters for JSON builtins. +//! Groups eval registry entries and dispatch wrappers for JSON builtins. //! //! Called from: //! - `crate::interpreter::builtins` module loading. -//! - `crate::interpreter::builtins::hooks` for migrated JSON dispatch. //! //! Key details: -//! - The JSON parser/encoder engine remains in `crate::interpreter::json`. -//! - This module only owns registry metadata and small hook adapters. - -use super::super::{ - eval_builtin_json_decode, eval_builtin_json_encode, eval_builtin_json_last_error, - eval_builtin_json_last_error_msg, eval_builtin_json_validate, eval_json_decode_result, - eval_json_encode_result, eval_json_validate_result, ElephcEvalContext, ElephcEvalScope, - EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, -}; +//! - Leaf files register metadata through `eval_builtin!` and own their +//! PHP-visible direct/by-value wrappers and implementation code. +//! - Helper reuse stays between builtin files instead of an area-level +//! implementation module when one builtin naturally owns the behavior. mod json_decode; mod json_encode; @@ -22,83 +16,8 @@ mod json_last_error; mod json_last_error_msg; mod json_validate; -/// Dispatches direct expression-level calls for declaratively migrated JSON builtins. -pub(in crate::interpreter) fn eval_builtin_json_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "json_decode" => eval_builtin_json_decode(args, context, scope, values), - "json_encode" => eval_builtin_json_encode(args, context, scope, values), - "json_last_error" => eval_builtin_json_last_error(args, context, values), - "json_last_error_msg" => eval_builtin_json_last_error_msg(args, context, values), - "json_validate" => eval_builtin_json_validate(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated JSON builtins. -pub(in crate::interpreter) fn eval_json_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "json_decode" => match evaluated_args { - [json] => eval_json_decode_result(*json, None, None, None, context, values), - [json, associative] => { - eval_json_decode_result(*json, Some(*associative), None, None, context, values) - } - [json, associative, depth] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - None, - context, - values, - ), - [json, associative, depth, flags] => eval_json_decode_result( - *json, - Some(*associative), - Some(*depth), - Some(*flags), - context, - values, - ), - _ => Err(EvalStatus::RuntimeFatal), - }, - "json_encode" => match evaluated_args { - [value] => eval_json_encode_result(*value, None, None, context, values), - [value, flags] => eval_json_encode_result(*value, Some(*flags), None, context, values), - [value, flags, depth] => { - eval_json_encode_result(*value, Some(*flags), Some(*depth), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "json_last_error" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.int(context.json_last_error()) - } - "json_last_error_msg" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.string(context.json_last_error_msg()) - } - "json_validate" => match evaluated_args { - [json] => eval_json_validate_result(*json, None, None, context, values), - [json, depth] => eval_json_validate_result(*json, Some(*depth), None, context, values), - [json, depth, flags] => { - eval_json_validate_result(*json, Some(*depth), Some(*flags), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - _ => Err(EvalStatus::RuntimeFatal), - } -} +pub(in crate::interpreter) use json_decode::*; +pub(in crate::interpreter) use json_encode::*; +pub(in crate::interpreter) use json_last_error::*; +pub(in crate::interpreter) use json_last_error_msg::*; +pub(in crate::interpreter) use json_validate::*; diff --git a/crates/elephc-magician/src/interpreter/json.rs b/crates/elephc-magician/src/interpreter/json.rs deleted file mode 100644 index cc0721d606..0000000000 --- a/crates/elephc-magician/src/interpreter/json.rs +++ /dev/null @@ -1,922 +0,0 @@ -//! Purpose: -//! Implements eval-side JSON builtins and JSON encode/decode helper routines. -//! -//! Called from: -//! - `crate::interpreter::eval_positional_expr_call()` for JSON builtin dispatch. -//! -//! Key details: -//! - JSON parser errors are recorded on `ElephcEvalContext` to mirror PHP `json_last_error()`. -//! - Runtime values are traversed through `RuntimeValueOps`; this module never inspects cells directly. - -use super::*; - -/// Evaluates PHP `json_encode()` for zero-flag scalar and array values. -pub(super) fn eval_builtin_json_encode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_json_encode_result(value, None, None, context, values) - } - [value, flags] => { - let value = eval_expr(value, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_json_encode_result(value, Some(flags), None, context, values) - } - [value, flags, depth] => { - let value = eval_expr(value, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - eval_json_encode_result(value, Some(flags), Some(depth), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Encodes one runtime cell as a JSON string for eval's supported flag subset. -pub(super) fn eval_json_encode_result( - value: RuntimeCellHandle, - flags: Option, - depth: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let flags = flags - .map(|flags| eval_int_value(flags, values)) - .transpose()? - .unwrap_or(0); - let supported_flags = EVAL_JSON_HEX_TAG - | EVAL_JSON_HEX_AMP - | EVAL_JSON_HEX_APOS - | EVAL_JSON_HEX_QUOT - | EVAL_JSON_UNESCAPED_SLASHES - | EVAL_JSON_UNESCAPED_UNICODE - | EVAL_JSON_FORCE_OBJECT - | EVAL_JSON_PRETTY_PRINT - | EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR - | EVAL_JSON_PRESERVE_ZERO_FRACTION - | EVAL_JSON_INVALID_UTF8_IGNORE - | EVAL_JSON_INVALID_UTF8_SUBSTITUTE - | EVAL_JSON_THROW_ON_ERROR; - let supported_flags = supported_flags | EVAL_JSON_NUMERIC_CHECK; - if flags & !supported_flags != 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let depth = depth - .map(|depth| eval_int_value(depth, values)) - .transpose()? - .unwrap_or(512); - if depth <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - - let mut output = Vec::new(); - let mut error = None; - eval_json_encode_append( - value, - values, - flags, - depth as usize, - 0, - &mut Vec::new(), - &mut error, - &mut output, - )?; - if let Some(error) = error { - context.set_json_error(error.code, error.message); - if flags & EVAL_JSON_PARTIAL_OUTPUT_ON_ERROR == 0 { - if flags & EVAL_JSON_THROW_ON_ERROR != 0 { - return eval_throw_json_exception(error.code, error.message, context, values); - } - return values.bool_value(false); - } - } else { - context.clear_json_error(); - } - values.string_bytes_value(&output) -} - -/// Evaluates PHP `json_decode()` for eval-supported JSON text and flags. -pub(super) fn eval_builtin_json_decode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [json] => { - let json = eval_expr(json, context, scope, values)?; - eval_json_decode_result(json, None, None, None, context, values) - } - [json, associative] => { - let json = eval_expr(json, context, scope, values)?; - let associative = eval_expr(associative, context, scope, values)?; - eval_json_decode_result(json, Some(associative), None, None, context, values) - } - [json, associative, depth] => { - let json = eval_expr(json, context, scope, values)?; - let associative = eval_expr(associative, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - eval_json_decode_result(json, Some(associative), Some(depth), None, context, values) - } - [json, associative, depth, flags] => { - let json = eval_expr(json, context, scope, values)?; - let associative = eval_expr(associative, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_json_decode_result( - json, - Some(associative), - Some(depth), - Some(flags), - context, - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Decodes one JSON string into eval runtime cells and records PHP JSON parse state. -pub(super) fn eval_json_decode_result( - json: RuntimeCellHandle, - associative: Option, - depth: Option, - flags: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let flags = flags - .map(|flags| eval_int_value(flags, values)) - .transpose()? - .unwrap_or(0); - let supported_flags = EVAL_JSON_BIGINT_AS_STRING - | EVAL_JSON_INVALID_UTF8_IGNORE - | EVAL_JSON_INVALID_UTF8_SUBSTITUTE - | EVAL_JSON_THROW_ON_ERROR; - if flags & !supported_flags != 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let objects_as_assoc = associative - .map(|associative| values.truthy(associative)) - .transpose()? - .unwrap_or(false); - let depth = depth - .map(|depth| eval_int_value(depth, values)) - .transpose()? - .unwrap_or(512); - if depth <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - - let bytes = values.string_bytes(json)?; - let decoded_result = if flags & EVAL_JSON_INVALID_UTF8_SUBSTITUTE != 0 { - json_validate::decode_result_substituting_invalid_utf8(&bytes, depth as usize) - } else if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { - json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) - } else { - json_validate::decode_result(&bytes, depth as usize) - }; - let decoded = match decoded_result { - Ok(decoded) => decoded, - Err(error) => { - let (code, message) = eval_json_parse_error_details(error, &bytes); - if flags & EVAL_JSON_THROW_ON_ERROR != 0 { - return eval_throw_json_exception(code, &message, context, values); - } - context.set_json_error(code, message); - return values.null(); - } - }; - context.clear_json_error(); - eval_json_decode_to_cell(decoded, flags, objects_as_assoc, values) -} - -/// Materializes one parsed JSON value as an eval runtime cell. -fn eval_json_decode_to_cell( - value: JsonValue, - flags: i64, - objects_as_assoc: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - match value { - JsonValue::Null => values.null(), - JsonValue::Bool(value) => values.bool_value(value), - JsonValue::Number(value) => eval_json_decode_number_to_cell(&value, flags, values), - JsonValue::String(value) => values.string_bytes_value(&value), - JsonValue::Array(elements) => { - let mut result = values.array_new(elements.len())?; - for (index, element) in elements.into_iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let element = eval_json_decode_to_cell(element, flags, objects_as_assoc, values)?; - result = values.array_set(result, key, element)?; - } - Ok(result) - } - JsonValue::Object(entries) => { - if !objects_as_assoc { - return eval_json_decode_object_to_cell(entries, flags, values); - } - let mut result = values.assoc_new(entries.len())?; - for (key, value) in entries { - let key = values.string_bytes_value(&key)?; - let value = eval_json_decode_to_cell(value, flags, objects_as_assoc, values)?; - result = values.array_set(result, key, value)?; - } - Ok(result) - } - } -} - -/// Materializes a parsed JSON object as a `stdClass` runtime object. -fn eval_json_decode_object_to_cell( - entries: Vec<(Vec, JsonValue)>, - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = values.new_object("stdClass")?; - for (key, value) in entries { - let key = std::str::from_utf8(&key).map_err(|_| EvalStatus::RuntimeFatal)?; - let value = eval_json_decode_to_cell(value, flags, false, values)?; - values.property_set(object, key, value)?; - } - Ok(object) -} - -/// Materializes one JSON number as an int when possible and as a float otherwise. -fn eval_json_decode_number_to_cell( - value: &[u8], - flags: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - if flags & EVAL_JSON_BIGINT_AS_STRING != 0 && eval_json_number_overflows_i64(value) { - return values.string_bytes_value(value); - } - let value = std::str::from_utf8(value).map_err(|_| EvalStatus::RuntimeFatal)?; - if !value.bytes().any(|byte| matches!(byte, b'.' | b'e' | b'E')) { - if let Ok(integer) = value.parse::() { - return values.int(integer); - } - } - let float = value.parse::().map_err(|_| EvalStatus::RuntimeFatal)?; - values.float(float) -} - -/// Returns true when one integer-grammar JSON number exceeds PHP's int range. -fn eval_json_number_overflows_i64(value: &[u8]) -> bool { - if value.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) { - return false; - } - let (negative, digits) = if let Some(digits) = value.strip_prefix(b"-") { - (true, digits) - } else { - (false, value) - }; - let threshold = if negative { - b"9223372036854775808".as_slice() - } else { - b"9223372036854775807".as_slice() - }; - digits.len() > threshold.len() || digits.len() == threshold.len() && digits > threshold -} - -/// Evaluates PHP `json_last_error()` from the eval interpreter's current JSON state. -pub(super) fn eval_builtin_json_last_error( - args: &[EvalExpr], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.int(context.json_last_error()) -} - -/// Evaluates PHP `json_last_error_msg()` from the eval interpreter's current JSON state. -pub(super) fn eval_builtin_json_last_error_msg( - args: &[EvalExpr], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.string(context.json_last_error_msg()) -} - -/// Evaluates PHP `json_validate()` for zero-flag JSON text validation. -pub(super) fn eval_builtin_json_validate( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [json] => { - let json = eval_expr(json, context, scope, values)?; - eval_json_validate_result(json, None, None, context, values) - } - [json, depth] => { - let json = eval_expr(json, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - eval_json_validate_result(json, Some(depth), None, context, values) - } - [json, depth, flags] => { - let json = eval_expr(json, context, scope, values)?; - let depth = eval_expr(depth, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_json_validate_result(json, Some(depth), Some(flags), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Validates JSON text with eval's current zero-flag JSON subset and records JSON state. -pub(super) fn eval_json_validate_result( - json: RuntimeCellHandle, - depth: Option, - flags: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let flags = flags - .map(|flags| eval_int_value(flags, values)) - .transpose()? - .unwrap_or(0); - if flags & !EVAL_JSON_INVALID_UTF8_IGNORE != 0 { - return Err(EvalStatus::UnsupportedConstruct); - } - let depth = depth - .map(|depth| eval_int_value(depth, values)) - .transpose()? - .unwrap_or(512); - if depth <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - - let bytes = values.string_bytes(json)?; - let result = if flags & EVAL_JSON_INVALID_UTF8_IGNORE != 0 { - json_validate::decode_result_ignoring_invalid_utf8(&bytes, depth as usize) - } else { - json_validate::decode_result(&bytes, depth as usize) - }; - match result { - Ok(_) => { - context.clear_json_error(); - values.bool_value(true) - } - Err(error) => { - eval_record_json_parse_error(context, error, &bytes); - values.bool_value(false) - } - } -} - -/// Records one parser error into the eval-local PHP JSON error slots. -fn eval_record_json_parse_error( - context: &mut ElephcEvalContext, - error: JsonParseError, - bytes: &[u8], -) { - let (code, message) = eval_json_parse_error_details(error, bytes); - context.set_json_error(code, message); -} - -/// Builds the PHP JSON error code and message for one parser failure. -fn eval_json_parse_error_details(error: JsonParseError, bytes: &[u8]) -> (i64, String) { - let (code, message) = eval_json_parse_error_status(error.kind()); - let message = eval_json_error_message_with_location(message, bytes, error.offset()); - (code, message) -} - -/// Creates and schedules a `JsonException` through eval's normal Throwable channel. -fn eval_throw_json_exception( - code: i64, - message: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - context.set_json_error(code, message.to_string()); - let exception = values.new_object("JsonException")?; - let message = values.string(message)?; - let code = values.int(code)?; - values.construct_object(exception, vec![message, code])?; - context.set_pending_throw(exception); - Err(EvalStatus::UncaughtThrowable) -} - -/// Maps eval JSON parser failures to PHP `JSON_ERROR_*` codes and messages. -fn eval_json_parse_error_status(error: JsonParseErrorKind) -> (i64, &'static str) { - match error { - JsonParseErrorKind::Depth => (EVAL_JSON_ERROR_DEPTH, "Maximum stack depth exceeded"), - JsonParseErrorKind::Syntax => (EVAL_JSON_ERROR_SYNTAX, "Syntax error"), - JsonParseErrorKind::ControlChar => ( - EVAL_JSON_ERROR_CTRL_CHAR, - "Control character error, possibly incorrectly encoded", - ), - JsonParseErrorKind::Utf8 => (EVAL_JSON_ERROR_UTF8, EVAL_JSON_UTF8_MESSAGE), - JsonParseErrorKind::Utf16 => ( - EVAL_JSON_ERROR_UTF16, - "Single unpaired UTF-16 surrogate in unicode escape", - ), - } -} - -/// Adds PHP's JSON line/column suffix to one base error message. -fn eval_json_error_message_with_location(message: &str, bytes: &[u8], offset: usize) -> String { - let (line, column) = eval_json_error_location(bytes, offset); - format!("{message} near location {line}:{column}") -} - -/// Converts a zero-based JSON byte offset into PHP-style one-based line and column. -fn eval_json_error_location(bytes: &[u8], offset: usize) -> (usize, usize) { - let mut line = 1; - let mut column = 1; - let offset = offset.min(bytes.len()); - for byte in &bytes[..offset] { - if *byte == b'\n' { - line += 1; - column = 1; - } else { - column += 1; - } - } - (line, column) -} - -/// Appends one JSON value to the output buffer. -fn eval_json_encode_append( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - match values.type_tag(value)? { - EVAL_TAG_INT => output.extend_from_slice(&values.string_bytes(value)?), - EVAL_TAG_FLOAT => { - eval_json_encode_append_float(value, values, flags, error, output)?; - } - EVAL_TAG_STRING => eval_json_encode_append_string( - &values.string_bytes(value)?, - flags, - EvalJsonStringPosition::Value, - error, - output, - )?, - EVAL_TAG_BOOL => { - if values.truthy(value)? { - output.extend_from_slice(b"true"); - } else { - output.extend_from_slice(b"false"); - } - } - EVAL_TAG_ARRAY => { - eval_json_encode_append_indexed_array( - value, - values, - flags, - depth_limit, - depth, - arrays_seen, - error, - output, - )?; - } - EVAL_TAG_ASSOC => { - eval_json_encode_append_assoc( - value, - values, - flags, - depth_limit, - depth, - arrays_seen, - error, - output, - )?; - } - EVAL_TAG_OBJECT => { - eval_json_encode_append_object( - value, - values, - flags, - depth_limit, - depth, - arrays_seen, - error, - output, - )?; - } - EVAL_TAG_NULL | EVAL_TAG_RESOURCE => output.extend_from_slice(b"null"), - _ => return Err(EvalStatus::UnsupportedConstruct), - } - Ok(()) -} - -#[derive(Clone, Copy)] -struct EvalJsonEncodeError { - code: i64, - message: &'static str, -} - -/// Marks whether a JSON string is being encoded as a value or as an object key. -#[derive(Clone, Copy)] -enum EvalJsonStringPosition { - Value, - Key, -} - -/// Appends one JSON float while preserving a `.0` suffix when requested. -fn eval_json_encode_append_float( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - let float = eval_float_value(value, values)?; - if !float.is_finite() { - *error = Some(EvalJsonEncodeError { - code: EVAL_JSON_ERROR_INF_OR_NAN, - message: EVAL_JSON_INF_OR_NAN_MESSAGE, - }); - output.push(b'0'); - return Ok(()); - } - let bytes = values.string_bytes(value)?; - output.extend_from_slice(&bytes); - if flags & EVAL_JSON_PRESERVE_ZERO_FRACTION != 0 - && !bytes.iter().any(|byte| matches!(*byte, b'.' | b'e' | b'E')) - { - output.extend_from_slice(b".0"); - } - Ok(()) -} - -/// Appends one indexed eval array as a JSON array or forced JSON object. -fn eval_json_encode_append_indexed_array( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; - let force_object = flags & EVAL_JSON_FORCE_OBJECT != 0; - let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; - output.push(if force_object { b'{' } else { b'[' }); - let len = values.array_len(value)?; - if pretty && len > 0 { - output.push(b'\n'); - } - for position in 0..len { - if position > 0 { - output.push(b','); - if pretty { - output.push(b'\n'); - } - } - if pretty { - eval_json_encode_pretty_indent(output, depth + 1); - } - let key = values.array_iter_key(value, position)?; - if force_object { - eval_json_encode_append_string( - &values.string_bytes(key)?, - flags & !EVAL_JSON_NUMERIC_CHECK, - EvalJsonStringPosition::Key, - error, - output, - )?; - eval_json_encode_append_colon(flags, output); - } - let element = values.array_get(value, key)?; - eval_json_encode_append( - element, - values, - flags, - depth_limit, - depth + 1, - arrays_seen, - error, - output, - )?; - } - if pretty && len > 0 { - output.push(b'\n'); - eval_json_encode_pretty_indent(output, depth); - } - output.push(if force_object { b'}' } else { b']' }); - arrays_seen.pop(); - Ok(()) -} - -/// Appends one associative eval array as a JSON object. -fn eval_json_encode_append_assoc( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; - let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; - output.push(b'{'); - let len = values.array_len(value)?; - if pretty && len > 0 { - output.push(b'\n'); - } - for position in 0..len { - if position > 0 { - output.push(b','); - if pretty { - output.push(b'\n'); - } - } - if pretty { - eval_json_encode_pretty_indent(output, depth + 1); - } - let key = values.array_iter_key(value, position)?; - eval_json_encode_append_string( - &values.string_bytes(key)?, - flags & !EVAL_JSON_NUMERIC_CHECK, - EvalJsonStringPosition::Key, - error, - output, - )?; - eval_json_encode_append_colon(flags, output); - let element = values.array_get(value, key)?; - eval_json_encode_append( - element, - values, - flags, - depth_limit, - depth + 1, - arrays_seen, - error, - output, - )?; - } - if pretty && len > 0 { - output.push(b'\n'); - eval_json_encode_pretty_indent(output, depth); - } - output.push(b'}'); - arrays_seen.pop(); - Ok(()) -} - -/// Appends one eval runtime object as a JSON object. -fn eval_json_encode_append_object( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - flags: i64, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_json_encode_enter_array(value, depth_limit, depth, arrays_seen)?; - let pretty = flags & EVAL_JSON_PRETTY_PRINT != 0; - output.push(b'{'); - let len = values.object_property_len(value)?; - if pretty && len > 0 { - output.push(b'\n'); - } - for position in 0..len { - if position > 0 { - output.push(b','); - if pretty { - output.push(b'\n'); - } - } - if pretty { - eval_json_encode_pretty_indent(output, depth + 1); - } - let key = values.object_property_iter_key(value, position)?; - let key_bytes = values.string_bytes(key)?; - eval_json_encode_append_string( - &key_bytes, - flags & !EVAL_JSON_NUMERIC_CHECK, - EvalJsonStringPosition::Key, - error, - output, - )?; - eval_json_encode_append_colon(flags, output); - let property = std::str::from_utf8(&key_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - let element = values.property_get(value, property)?; - eval_json_encode_append( - element, - values, - flags, - depth_limit, - depth + 1, - arrays_seen, - error, - output, - )?; - } - if pretty && len > 0 { - output.push(b'\n'); - eval_json_encode_pretty_indent(output, depth); - } - output.push(b'}'); - arrays_seen.pop(); - Ok(()) -} - -/// Appends a JSON object colon, including pretty-print spacing when active. -fn eval_json_encode_append_colon(flags: i64, output: &mut Vec) { - if flags & EVAL_JSON_PRETTY_PRINT != 0 { - output.extend_from_slice(b": "); - } else { - output.push(b':'); - } -} - -/// Appends PHP's four-space JSON pretty-print indentation for one nesting level. -fn eval_json_encode_pretty_indent(output: &mut Vec, depth: usize) { - for _ in 0..depth { - output.extend_from_slice(b" "); - } -} - -/// Records entry into one JSON array/object, rejecting depth overrun and recursion. -fn eval_json_encode_enter_array( - value: RuntimeCellHandle, - depth_limit: usize, - depth: usize, - arrays_seen: &mut Vec, -) -> Result<(), EvalStatus> { - if depth >= depth_limit { - return Err(EvalStatus::RuntimeFatal); - } - let address = value.as_ptr() as usize; - if arrays_seen.contains(&address) { - return Err(EvalStatus::RuntimeFatal); - } - arrays_seen.push(address); - Ok(()) -} - -/// Appends one JSON string with eval-supported PHP flag handling. -fn eval_json_encode_append_string( - bytes: &[u8], - flags: i64, - position: EvalJsonStringPosition, - error: &mut Option, - output: &mut Vec, -) -> Result<(), EvalStatus> { - if flags & EVAL_JSON_NUMERIC_CHECK != 0 { - if let Some(number) = eval_json_numeric_check_bytes(bytes) { - output.extend_from_slice(&number); - return Ok(()); - } - } - let start_len = output.len(); - output.push(b'"'); - if let Ok(value) = std::str::from_utf8(bytes) { - for character in value.chars() { - eval_json_encode_append_char(character, flags, output); - } - } else if flags & (EVAL_JSON_INVALID_UTF8_IGNORE | EVAL_JSON_INVALID_UTF8_SUBSTITUTE) == 0 { - output.truncate(start_len); - *error = Some(EvalJsonEncodeError { - code: EVAL_JSON_ERROR_UTF8, - message: EVAL_JSON_UTF8_MESSAGE, - }); - match position { - EvalJsonStringPosition::Value => output.extend_from_slice(b"null"), - EvalJsonStringPosition::Key => output.extend_from_slice(b"\"\""), - } - return Ok(()); - } else { - eval_json_encode_append_invalid_utf8_bytes(bytes, flags, output)?; - } - output.push(b'"'); - Ok(()) -} - -/// Appends one valid UTF-8 character using PHP JSON string escaping rules. -fn eval_json_encode_append_char(character: char, flags: i64, output: &mut Vec) { - if character.is_ascii() { - eval_json_encode_append_ascii_byte(character as u8, flags, output); - } else if flags & EVAL_JSON_UNESCAPED_UNICODE != 0 { - let mut buffer = [0_u8; 4]; - output.extend_from_slice(character.encode_utf8(&mut buffer).as_bytes()); - } else { - eval_json_encode_append_unicode_escape(character as u32, output); - } -} - -/// Appends one ASCII byte using JSON escaping rules shared by UTF-8 and fallback paths. -fn eval_json_encode_append_ascii_byte(byte: u8, flags: i64, output: &mut Vec) { - match byte { - b'"' if flags & EVAL_JSON_HEX_QUOT != 0 => output.extend_from_slice(b"\\u0022"), - b'"' => output.extend_from_slice(b"\\\""), - b'\\' => output.extend_from_slice(b"\\\\"), - b'/' if flags & EVAL_JSON_UNESCAPED_SLASHES == 0 => { - output.extend_from_slice(b"\\/"); - } - b'/' => output.push(b'/'), - b'<' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003C"), - b'>' if flags & EVAL_JSON_HEX_TAG != 0 => output.extend_from_slice(b"\\u003E"), - b'&' if flags & EVAL_JSON_HEX_AMP != 0 => output.extend_from_slice(b"\\u0026"), - b'\'' if flags & EVAL_JSON_HEX_APOS != 0 => output.extend_from_slice(b"\\u0027"), - b'\x08' => output.extend_from_slice(b"\\b"), - b'\x0c' => output.extend_from_slice(b"\\f"), - b'\n' => output.extend_from_slice(b"\\n"), - b'\r' => output.extend_from_slice(b"\\r"), - b'\t' => output.extend_from_slice(b"\\t"), - control @ 0x00..=0x1f => { - output.extend_from_slice(format!("\\u{control:04x}").as_bytes()); - } - _ => output.push(byte), - } -} - -/// Appends valid scalar values as PHP JSON `\uXXXX` escapes, using surrogate pairs when needed. -fn eval_json_encode_append_unicode_escape(codepoint: u32, output: &mut Vec) { - if codepoint <= 0xffff { - output.extend_from_slice(format!("\\u{codepoint:04x}").as_bytes()); - return; - } - - let codepoint = codepoint - 0x1_0000; - let high = 0xd800 + ((codepoint >> 10) & 0x3ff); - let low = 0xdc00 + (codepoint & 0x3ff); - output.extend_from_slice(format!("\\u{high:04x}\\u{low:04x}").as_bytes()); -} - -/// Appends malformed UTF-8 bytes according to PHP's JSON invalid-UTF-8 flags. -fn eval_json_encode_append_invalid_utf8_bytes( - mut bytes: &[u8], - flags: i64, - output: &mut Vec, -) -> Result<(), EvalStatus> { - while !bytes.is_empty() { - match std::str::from_utf8(bytes) { - Ok(value) => { - for character in value.chars() { - eval_json_encode_append_char(character, flags, output); - } - return Ok(()); - } - Err(error) => { - let valid = &bytes[..error.valid_up_to()]; - for character in std::str::from_utf8(valid) - .map_err(|_| EvalStatus::RuntimeFatal)? - .chars() - { - eval_json_encode_append_char(character, flags, output); - } - let invalid_len = error - .error_len() - .unwrap_or(bytes.len() - valid.len()) - .max(1); - if flags & EVAL_JSON_INVALID_UTF8_IGNORE == 0 { - eval_json_encode_append_char('\u{fffd}', flags, output); - } - bytes = &bytes[valid.len() + invalid_len.min(bytes.len() - valid.len())..]; - } - } - } - Ok(()) -} - -/// Returns the JSON number bytes for a PHP numeric string when `JSON_NUMERIC_CHECK` applies. -fn eval_json_numeric_check_bytes(bytes: &[u8]) -> Option> { - let value = std::str::from_utf8(bytes).ok()?.trim(); - if value.is_empty() { - return None; - } - let integer_grammar = value - .bytes() - .all(|byte| byte.is_ascii_digit() || matches!(byte, b'+' | b'-')); - if integer_grammar { - if let Ok(integer) = value.parse::() { - return Some(integer.to_string().into_bytes()); - } - } - let number = value.parse::().ok()?; - if number.is_finite() { - Some(number.to_string().into_bytes()) - } else { - None - } -} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index d92650605a..ba28e1ed09 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -23,7 +23,6 @@ mod debug_output; mod dynamic_functions; mod expressions; mod include_exec; -mod json; mod libc_shims; mod reflection; mod return_type_compat; @@ -47,7 +46,6 @@ use crate::eval_ir::{ EvalParameterType, EvalParameterTypeVariant, EvalProgram, EvalStmt, EvalSwitchCase, EvalTrait, EvalTraitAdaptation, EvalUnaryOp, EvalVisibility, }; -use crate::json_validate::{self, JsonParseError, JsonParseErrorKind, JsonValue}; #[cfg(test)] use crate::parser::parse_fragment; use crate::scope::{ElephcEvalScope, ScopeCellOwnership, ScopeEntry}; @@ -68,7 +66,6 @@ use debug_output::*; use dynamic_functions::*; use expressions::*; use include_exec::*; -use json::*; use libc_shims::*; use reflection::*; use return_type_compat::*; From 5d7d5c237a8ea0f3571bd3d6df7103f7a23463cd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 15:37:28 +0200 Subject: [PATCH 1102/1208] refactor(magician): co-locate formatting builtins --- .../builtins/formatting/declarations/mod.rs | 15 --- .../formatting/declarations/printf.rs | 17 --- .../formatting/declarations/sprintf.rs | 17 --- .../formatting/declarations/sscanf.rs | 17 --- .../formatting/declarations/vprintf.rs | 16 --- .../formatting/declarations/vsprintf.rs | 16 --- .../builtins/formatting/dispatch.rs | 104 --------------- .../interpreter/builtins/formatting/mod.rs | 9 +- .../interpreter/builtins/formatting/printf.rs | 125 ++++-------------- .../builtins/formatting/sprintf.rs | 86 ++++++++++++ .../interpreter/builtins/formatting/sscanf.rs | 26 +++- .../builtins/formatting/vprintf.rs | 50 +++++++ .../builtins/formatting/vsprintf.rs | 64 +++++++++ .../src/interpreter/builtins/hooks/direct.rs | 29 ++-- .../src/interpreter/builtins/hooks/values.rs | 28 ++-- 15 files changed, 296 insertions(+), 323 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/printf.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sprintf.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sscanf.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vprintf.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vsprintf.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/mod.rs deleted file mode 100644 index 048f85a30e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries for printf-family formatting builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::formatting` module loading. -//! -//! Key details: -//! - Formatting algorithms remain in sibling helper modules; this module owns -//! per-builtin registry metadata only. - -mod printf; -mod sprintf; -mod sscanf; -mod vprintf; -mod vsprintf; diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/printf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/printf.rs deleted file mode 100644 index 958451823b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/printf.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `printf`. -//! -//! Called from: -//! - `crate::interpreter::builtins::formatting::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the formatting hook. - -eval_builtin! { - name: "printf", - area: Formatting, - params: [format], - variadic: values, - direct: Formatting, - values: Formatting, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sprintf.rs deleted file mode 100644 index 9c394f0996..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sprintf.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `sprintf`. -//! -//! Called from: -//! - `crate::interpreter::builtins::formatting::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the formatting hook. - -eval_builtin! { - name: "sprintf", - area: Formatting, - params: [format], - variadic: values, - direct: Formatting, - values: Formatting, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sscanf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sscanf.rs deleted file mode 100644 index cd1ef8b901..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/sscanf.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `sscanf`. -//! -//! Called from: -//! - `crate::interpreter::builtins::formatting::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the formatting hook. - -eval_builtin! { - name: "sscanf", - area: Formatting, - params: [string, format], - variadic: vars, - direct: Formatting, - values: Formatting, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vprintf.rs deleted file mode 100644 index 1ddfb207e0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vprintf.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `vprintf`. -//! -//! Called from: -//! - `crate::interpreter::builtins::formatting::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the formatting hook. - -eval_builtin! { - name: "vprintf", - area: Formatting, - params: [format, values], - direct: Formatting, - values: Formatting, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vsprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vsprintf.rs deleted file mode 100644 index 7657e2e9f9..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/declarations/vsprintf.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `vsprintf`. -//! -//! Called from: -//! - `crate::interpreter::builtins::formatting::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the formatting hook. - -eval_builtin! { - name: "vsprintf", - area: Formatting, - params: [format, values], - direct: Formatting, - values: Formatting, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs deleted file mode 100644 index 83beee8d6e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/dispatch.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! Purpose: -//! Evaluates direct printf-family eval arguments before dispatching to formatted -//! result helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::registry` builtin dispatch paths. -//! -//! Key details: -//! - Argument expressions are evaluated once in source order before builtin-family -//! selection is applied. - -use super::super::super::*; -use super::*; - -/// Dispatches direct expression-level calls for declaratively migrated formatting builtins. -pub(in crate::interpreter) fn eval_builtin_formatting_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "sscanf" => eval_builtin_sscanf(args, context, scope, values), - "sprintf" | "printf" => eval_builtin_sprintf_like(name, args, context, scope, values), - "vsprintf" | "vprintf" => eval_builtin_vsprintf_like(name, args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates direct positional `sprintf()` or `printf()` calls in source order. -pub(in crate::interpreter) fn eval_builtin_sprintf_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_sprintf_like_result(name, &evaluated_args, values) -} - -/// Evaluates direct positional `vsprintf()` or `vprintf()` calls in source order. -pub(in crate::interpreter) fn eval_builtin_vsprintf_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_vsprintf_like_result(name, &evaluated_args, values) -} - -/// Dispatches already evaluated `sprintf()` or `printf()` arguments. -pub(in crate::interpreter) fn eval_sprintf_like_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "sprintf" => eval_sprintf_result(evaluated_args, values), - "printf" => eval_printf_result(evaluated_args, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Dispatches already evaluated `vsprintf()` or `vprintf()` arguments. -pub(in crate::interpreter) fn eval_vsprintf_like_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "vsprintf" => eval_vsprintf_result(evaluated_args, values), - "vprintf" => eval_vprintf_result(evaluated_args, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated formatting builtins. -pub(in crate::interpreter) fn eval_formatting_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "sscanf" => { - let [input, format, ..] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sscanf_result(*input, *format, values) - } - "sprintf" | "printf" => eval_sprintf_like_result(name, evaluated_args, values), - "vsprintf" | "vprintf" => eval_vsprintf_like_result(name, evaluated_args, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs index d3f0116037..82aab429e9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/mod.rs @@ -10,16 +10,19 @@ //! behavior through `RuntimeValueOps`. mod common; -mod declarations; -mod dispatch; mod number_format; mod printf; +mod sprintf; mod sprintf_format; mod sscanf; +mod vprintf; +mod vsprintf; pub(in crate::interpreter) use common::*; -pub(in crate::interpreter) use dispatch::*; pub(in crate::interpreter) use number_format::*; pub(in crate::interpreter) use printf::*; +pub(in crate::interpreter) use sprintf::*; pub(in crate::interpreter) use sprintf_format::*; pub(in crate::interpreter) use sscanf::*; +pub(in crate::interpreter) use vprintf::*; +pub(in crate::interpreter) use vsprintf::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs index 1d097d94c8..8b4bd0bbe1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs @@ -1,28 +1,36 @@ //! Purpose: -//! Implements result construction for PHP `sprintf`, `printf`, `vsprintf`, and -//! `vprintf` eval builtins. +//! Eval registry entry and implementation for `printf`. //! //! Called from: -//! - `crate::interpreter::builtins::formatting::dispatch`. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - The formatted byte stream is shared by string-returning and echoing variants; -//! echoing variants return the emitted byte count. +//! - `printf()` reuses `sprintf` byte formatting, echoes the result, and returns +//! the emitted byte count. use super::super::super::*; -use super::*; -/// Formats `sprintf()` arguments and returns the resulting PHP string. -pub(in crate::interpreter) fn eval_sprintf_result( - evaluated_args: &[RuntimeCellHandle], +eval_builtin! { + name: "printf", + area: Formatting, + params: [format], + variadic: values, + direct: Printf, + values: Printf, +} + +/// Evaluates direct positional `printf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_printf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let Some((format, format_args)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - values.string_bytes_value(&output) + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_printf_result(&evaluated_args, values) } /// Formats `printf()` arguments, echoes the result, and returns its byte count. @@ -34,94 +42,9 @@ pub(in crate::interpreter) fn eval_printf_result( return Err(EvalStatus::RuntimeFatal); }; let format = values.string_bytes(*format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; + let output = super::sprintf::eval_sprintf_bytes(&format, format_args, values)?; let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; let output = values.string_bytes_value(&output)?; values.echo(output)?; values.int(len) } - -/// Formats `vsprintf()` array arguments and returns the resulting PHP string. -pub(in crate::interpreter) fn eval_vsprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let format_args = eval_sprintf_argument_array_values(*array, values)?; - let output = eval_sprintf_bytes(&format, &format_args, values)?; - values.string_bytes_value(&output) -} - -/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. -pub(in crate::interpreter) fn eval_vprintf_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let format = values.string_bytes(*format)?; - let format_args = eval_sprintf_argument_array_values(*array, values)?; - let output = eval_sprintf_bytes(&format, &format_args, values)?; - let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.int(len) -} - -/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. -pub(in crate::interpreter) fn eval_sprintf_argument_array_values( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !values.is_array_like(array)? { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(array)?; - let mut args = Vec::with_capacity(len); - for position in 0..len { - let key = values.array_iter_key(array, position)?; - args.push(values.array_get(array, key)?); - } - Ok(args) -} - -/// Formats one printf-style byte string through eval runtime value coercions. -pub(in crate::interpreter) fn eval_sprintf_bytes( - format: &[u8], - args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut output = Vec::new(); - let mut index = 0; - let mut arg_index = 0; - while index < format.len() { - if format[index] != b'%' { - output.push(format[index]); - index += 1; - continue; - } - index += 1; - if index >= format.len() { - break; - } - if format[index] == b'%' { - output.push(b'%'); - index += 1; - continue; - } - - let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; - index = next_index; - let Some(arg) = args.get(arg_index).copied() else { - return Err(EvalStatus::RuntimeFatal); - }; - arg_index += 1; - let bytes = eval_format_sprintf_arg(spec, arg, values)?; - output.extend_from_slice(&bytes); - } - Ok(output) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs new file mode 100644 index 0000000000..5b441cbd85 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs @@ -0,0 +1,86 @@ +//! Purpose: +//! Eval registry entry and implementation for `sprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns the string-returning printf-family implementation. +//! - Echoing and vector-argument variants reuse the byte formatter here because +//! they are behavior variants of `sprintf`. + +use super::super::super::*; +use super::*; + +eval_builtin! { + name: "sprintf", + area: Formatting, + params: [format], + variadic: values, + direct: Sprintf, + values: Sprintf, +} + +/// Evaluates direct positional `sprintf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_sprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_sprintf_result(&evaluated_args, values) +} + +/// Formats `sprintf()` arguments and returns the resulting PHP string. +pub(in crate::interpreter) fn eval_sprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((format, format_args)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + values.string_bytes_value(&output) +} + +/// Formats one printf-style byte string through eval runtime value coercions. +pub(in crate::interpreter) fn eval_sprintf_bytes( + format: &[u8], + args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut output = Vec::new(); + let mut index = 0; + let mut arg_index = 0; + while index < format.len() { + if format[index] != b'%' { + output.push(format[index]); + index += 1; + continue; + } + index += 1; + if index >= format.len() { + break; + } + if format[index] == b'%' { + output.push(b'%'); + index += 1; + continue; + } + + let (spec, next_index) = eval_parse_sprintf_spec(format, index)?; + index = next_index; + let Some(arg) = args.get(arg_index).copied() else { + return Err(EvalStatus::RuntimeFatal); + }; + arg_index += 1; + let bytes = eval_format_sprintf_arg(spec, arg, values)?; + output.extend_from_slice(&bytes); + } + Ok(output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs index 8e40c2bd94..c168fc0482 100644 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs @@ -2,14 +2,26 @@ //! Implements eval support for PHP `sscanf()` and its small scanning subset. //! //! Called from: -//! - `crate::interpreter::builtins::formatting` re-exports. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: //! - Only the currently supported `%d`, `%f`, `%s`, and `%%` subset is parsed; //! extra output variables are evaluated for side effects and ignored. +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! the scanning implementation. use super::super::super::*; +eval_builtin! { + name: "sscanf", + area: Formatting, + params: [string, format], + variadic: vars, + direct: Sscanf, + values: Sscanf, +} + + /// Evaluates direct positional `sscanf()` calls in source order. pub(in crate::interpreter) fn eval_builtin_sscanf( args: &[EvalExpr], @@ -28,6 +40,18 @@ pub(in crate::interpreter) fn eval_builtin_sscanf( eval_sscanf_result(input, format, values) } + +/// Dispatches by-value `sscanf()` calls after argument binding. +pub(in crate::interpreter) fn eval_sscanf_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [input, format, ..] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sscanf_result(*input, *format, values) +} + /// Parses one string through the eval `sscanf()` subset and returns an indexed array. pub(in crate::interpreter) fn eval_sscanf_result( input: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs new file mode 100644 index 0000000000..600156d53a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs @@ -0,0 +1,50 @@ +//! Purpose: +//! Eval registry entry and implementation for `vprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `vprintf()` reuses `sprintf` byte formatting and `vsprintf` argument-array +//! expansion, then echoes the result and returns its byte count. + +use super::super::super::*; + +eval_builtin! { + name: "vprintf", + area: Formatting, + params: [format, values], + direct: Vprintf, + values: Vprintf, +} + +/// Evaluates direct positional `vprintf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_vprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_vprintf_result(&evaluated_args, values) +} + +/// Formats `vprintf()` array arguments, echoes the result, and returns its byte count. +pub(in crate::interpreter) fn eval_vprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = super::vsprintf::eval_sprintf_argument_array_values(*array, values)?; + let output = super::sprintf::eval_sprintf_bytes(&format, &format_args, values)?; + let len = i64::try_from(output.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.int(len) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs new file mode 100644 index 0000000000..44a89bc606 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Eval registry entry and implementation for `vsprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `vsprintf()` owns array-to-argument expansion for printf-family vector +//! calls. `vprintf()` reuses that expansion from this file. + +use super::super::super::*; + +eval_builtin! { + name: "vsprintf", + area: Formatting, + params: [format, values], + direct: Vsprintf, + values: Vsprintf, +} + +/// Evaluates direct positional `vsprintf()` calls in source order. +pub(in crate::interpreter) fn eval_builtin_vsprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_vsprintf_result(&evaluated_args, values) +} + +/// Formats `vsprintf()` array arguments and returns the resulting PHP string. +pub(in crate::interpreter) fn eval_vsprintf_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [format, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let format = values.string_bytes(*format)?; + let format_args = eval_sprintf_argument_array_values(*array, values)?; + let output = super::sprintf::eval_sprintf_bytes(&format, &format_args, values)?; + values.string_bytes_value(&output) +} + +/// Reads `vsprintf()` values in PHP array iteration order while ignoring keys. +pub(in crate::interpreter) fn eval_sprintf_argument_array_values( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + let mut args = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(array, position)?; + args.push(values.array_get(array, key)?); + } + Ok(args) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 10213e48da..8fea3893f2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -12,11 +12,12 @@ use super::super::super::{ eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, eval_builtin_chr, eval_builtin_count, - eval_builtin_crc32, eval_builtin_ctype, eval_builtin_explode, eval_builtin_formatting_call, eval_builtin_gzip, eval_builtin_hash_algos, - eval_builtin_hash_copy, eval_builtin_hash_final, eval_builtin_hash_init, - eval_builtin_hash_one_shot, eval_builtin_hash_update, eval_builtin_hex2bin, - eval_builtin_implode, eval_builtin_number_format, eval_builtin_ord, eval_builtin_slashes, - eval_builtin_str_repeat, eval_builtin_strlen, + eval_builtin_crc32, eval_builtin_ctype, eval_builtin_explode, eval_builtin_gzip, + eval_builtin_hash_algos, eval_builtin_hash_copy, eval_builtin_hash_final, + eval_builtin_hash_init, eval_builtin_hash_one_shot, eval_builtin_hash_update, + eval_builtin_hex2bin, eval_builtin_implode, eval_builtin_number_format, eval_builtin_ord, + eval_builtin_printf, eval_builtin_slashes, eval_builtin_sprintf, eval_builtin_sscanf, + eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_vprintf, eval_builtin_vsprintf, eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; @@ -129,8 +130,16 @@ pub(in crate::interpreter) enum EvalDirectHook { Fmod, /// Dispatches `hypot(...)`. Hypot, - /// Dispatches printf-family formatting builtins. - Formatting, + /// Dispatches `printf(...)`. + Printf, + /// Dispatches `sprintf(...)`. + Sprintf, + /// Dispatches `sscanf(...)`. + Sscanf, + /// Dispatches `vprintf(...)`. + Vprintf, + /// Dispatches `vsprintf(...)`. + Vsprintf, /// Dispatches `floor(...)`. Floor, /// Dispatches `gettype(...)`. @@ -349,7 +358,6 @@ impl EvalDirectHook { Self::Fdiv => eval_builtin_fdiv(args, context, scope, values), Self::Filesystem => eval_builtin_filesystem_call(name, args, context, scope, values), Self::Fmod => eval_builtin_fmod(args, context, scope, values), - Self::Formatting => eval_builtin_formatting_call(name, args, context, scope, values), Self::Floor => eval_builtin_floor(args, context, scope, values), Self::Gettype => eval_builtin_gettype(args, context, scope, values), Self::Hypot => eval_builtin_hypot(args, context, scope, values), @@ -403,6 +411,7 @@ impl EvalDirectHook { Self::NumberFormat => eval_builtin_number_format(args, context, scope, values), Self::Ord => eval_builtin_ord(args, context, scope, values), Self::Pi => eval_builtin_pi(args, values), + Self::Printf => eval_builtin_printf(args, context, scope, values), Self::Pow => eval_builtin_pow(args, context, scope, values), Self::Rad2deg => eval_builtin_rad2deg(args, context, scope, values), Self::Rand => eval_builtin_rand(args, context, scope, values), @@ -415,6 +424,8 @@ impl EvalDirectHook { Self::Sinh => eval_builtin_sinh(args, context, scope, values), Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), Self::Sqrt => super::super::math::eval_builtin_sqrt(args, context, scope, values), + Self::Sprintf => eval_builtin_sprintf(args, context, scope, values), + Self::Sscanf => eval_builtin_sscanf(args, context, scope, values), Self::StringCase => eval_builtin_string_case(name, args, context, scope, values), Self::StringCompare => eval_builtin_string_compare(name, args, context, scope, values), Self::StringPosition => { @@ -448,6 +459,8 @@ impl EvalDirectHook { Self::Time => eval_builtin_time_call(name, args, context, scope, values), Self::TrimLike => eval_builtin_trim_like(name, args, context, scope, values), Self::Ucwords => eval_builtin_ucwords(args, context, scope, values), + Self::Vprintf => eval_builtin_vprintf(args, context, scope, values), + Self::Vsprintf => eval_builtin_vsprintf(args, context, scope, values), Self::Nl2br => eval_builtin_nl2br(args, context, scope, values), Self::Wordwrap => eval_builtin_wordwrap(args, context, scope, values), Self::UrlDecode => eval_builtin_url_decode(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index d19b77d1f2..9e3aeed5fe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -26,7 +26,7 @@ use super::super::{ eval_clamp_result, eval_cos_result, eval_cosh_result, eval_deg2rad_result, eval_core_values_result, eval_crc32_result, eval_ctype_result, eval_exp_result, eval_fdiv_result, eval_filesystem_values_result, eval_floatval_result, - eval_floor_result, eval_fmod_result, eval_formatting_values_result, + eval_floor_result, eval_fmod_result, eval_gettype_result, eval_hypot_result, eval_intdiv_result, eval_intval_result, eval_is_array_result, eval_is_bool_result, eval_is_double_result, eval_is_finite_result, eval_is_float_result, @@ -41,10 +41,10 @@ use super::super::{ eval_json_last_error_result, eval_json_validate_values_result, eval_log2_result, eval_log10_result, eval_log_result, eval_max_result, eval_min_result, eval_mt_rand_values_result, eval_network_env_values_result, eval_nl2br_result, - eval_pi_result, eval_pow_result, eval_rad2deg_result, eval_rand_values_result, + eval_pi_result, eval_pow_result, eval_printf_result, eval_rad2deg_result, eval_rand_values_result, eval_random_int_values_result, eval_range_result, eval_regex_values_result, eval_round_result, eval_settype_values_result, eval_sin_result, eval_sinh_result, eval_slashes_result, - eval_sqrt_result, + eval_sprintf_result, eval_sqrt_result, eval_sscanf_values_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, @@ -52,8 +52,8 @@ use super::super::{ eval_substr_replace_result, eval_substr_result, eval_raw_memory_values_result, eval_symbols_values_result, eval_tan_result, eval_tanh_result, eval_time_values_result, eval_trim_like_result, - eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, - eval_wordwrap_result, + eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, eval_vprintf_result, + eval_vsprintf_result, eval_wordwrap_result, }; use super::arity::{one_arg, three_args, two_args}; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; @@ -137,8 +137,16 @@ pub(in crate::interpreter) enum EvalValuesHook { Fmod, /// Dispatches `hypot(...)`. Hypot, - /// Dispatches printf-family formatting builtins. - Formatting, + /// Dispatches `printf(...)`. + Printf, + /// Dispatches `sprintf(...)`. + Sprintf, + /// Dispatches `sscanf(...)`. + Sscanf, + /// Dispatches `vprintf(...)`. + Vprintf, + /// Dispatches `vsprintf(...)`. + Vsprintf, /// Dispatches `floor(...)`. Floor, /// Dispatches `gettype(...)`. @@ -387,7 +395,6 @@ impl EvalValuesHook { Self::Filesystem => eval_filesystem_values_result(name, evaluated_args, context, values), Self::Floor => one_arg(evaluated_args, values, eval_floor_result), Self::Fmod => two_args(evaluated_args, values, eval_fmod_result), - Self::Formatting => eval_formatting_values_result(name, evaluated_args, values), Self::Gettype => one_arg(evaluated_args, values, eval_gettype_result), Self::Hypot => two_args(evaluated_args, values, eval_hypot_result), Self::Floatval => one_arg(evaluated_args, values, eval_floatval_result), @@ -457,6 +464,7 @@ impl EvalValuesHook { } eval_pi_result(values) } + Self::Printf => eval_printf_result(evaluated_args, values), Self::Pow => two_args(evaluated_args, values, eval_pow_result), Self::Rad2deg => one_arg(evaluated_args, values, eval_rad2deg_result), Self::Rand => eval_rand_values_result(evaluated_args, values), @@ -475,7 +483,9 @@ impl EvalValuesHook { Self::Slashes => one_arg(evaluated_args, values, |value, values| { eval_slashes_result(name, value, values) }), + Self::Sprintf => eval_sprintf_result(evaluated_args, values), Self::Sqrt => one_arg(evaluated_args, values, eval_sqrt_result), + Self::Sscanf => eval_sscanf_values_result(evaluated_args, values), Self::StringCase => one_arg(evaluated_args, values, |value, values| { eval_string_case_result(name, value, values) }), @@ -567,6 +577,8 @@ impl EvalValuesHook { [value, separators] => eval_ucwords_result(*value, Some(*separators), values), _ => Err(EvalStatus::RuntimeFatal), }, + Self::Vprintf => eval_vprintf_result(evaluated_args, values), + Self::Vsprintf => eval_vsprintf_result(evaluated_args, values), Self::Nl2br => match evaluated_args { [value] => eval_nl2br_result(*value, true, values), [value, use_xhtml] => { From 06dfcd8da3b232f59c3eac7074f4e3f77bc52d6e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 15:47:45 +0200 Subject: [PATCH 1103/1208] refactor(magician): co-locate regex builtins --- .../src/interpreter/builtins/hooks/direct.rs | 26 +++- .../src/interpreter/builtins/hooks/values.rs | 30 +++-- .../builtins/regex/declarations/mod.rs | 119 ------------------ .../builtins/regex/declarations/preg_match.rs | 25 ---- .../regex/declarations/preg_match_all.rs | 25 ---- .../regex/declarations/preg_replace.rs | 16 --- .../declarations/preg_replace_callback.rs | 17 --- .../builtins/regex/declarations/preg_split.rs | 23 ---- .../src/interpreter/builtins/regex/mod.rs | 20 +-- .../regex/{match_one.rs => preg_match.rs} | 55 +++++++- .../regex/{match_all.rs => preg_match_all.rs} | 54 +++++++- .../builtins/regex/preg_replace.rs | 72 +++++++++++ .../{replace.rs => preg_replace_callback.rs} | 65 ++++------ .../regex/{split.rs => preg_split.rs} | 41 +++++- 14 files changed, 283 insertions(+), 305 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match_all.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace_callback.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_split.rs rename crates/elephc-magician/src/interpreter/builtins/regex/{match_one.rs => preg_match.rs} (74%) rename crates/elephc-magician/src/interpreter/builtins/regex/{match_all.rs => preg_match_all.rs} (80%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs rename crates/elephc-magician/src/interpreter/builtins/regex/{replace.rs => preg_replace_callback.rs} (62%) rename crates/elephc-magician/src/interpreter/builtins/regex/{split.rs => preg_split.rs} (72%) diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 8fea3893f2..7d86db1d46 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -44,8 +44,10 @@ use super::super::{ eval_builtin_min, eval_builtin_mt_rand, eval_builtin_network_env_call, eval_builtin_nl2br, eval_builtin_pi, eval_builtin_pow, eval_builtin_rad2deg, eval_builtin_rand, eval_builtin_random_int, eval_builtin_range, eval_builtin_round, - eval_builtin_raw_memory_call, eval_builtin_regex_call, eval_builtin_sin, eval_builtin_sinh, - eval_builtin_str_pad, eval_builtin_str_replace, eval_builtin_str_split, + eval_builtin_preg_match, eval_builtin_preg_match_all, eval_builtin_preg_replace, + eval_builtin_preg_replace_callback, eval_builtin_preg_split, eval_builtin_raw_memory_call, + eval_builtin_sin, eval_builtin_sinh, eval_builtin_str_pad, eval_builtin_str_replace, + eval_builtin_str_split, eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, @@ -244,8 +246,16 @@ pub(in crate::interpreter) enum EvalDirectHook { Round, /// Dispatches `range(...)`. Range, - /// Dispatches regex builtins. - Regex, + /// Dispatches `preg_match(...)`. + PregMatch, + /// Dispatches `preg_match_all(...)`. + PregMatchAll, + /// Dispatches `preg_replace(...)`. + PregReplace, + /// Dispatches `preg_replace_callback(...)`. + PregReplaceCallback, + /// Dispatches `preg_split(...)`. + PregSplit, /// Dispatches raw pointer and buffer extension builtins. RawMemory, /// Dispatches `addslashes(...)` and `stripslashes(...)`. @@ -418,7 +428,13 @@ impl EvalDirectHook { Self::RandomInt => eval_builtin_random_int(args, context, scope, values), Self::Round => eval_builtin_round(args, context, scope, values), Self::Range => eval_builtin_range(args, context, scope, values), - Self::Regex => eval_builtin_regex_call(name, args, context, scope, values), + Self::PregMatch => eval_builtin_preg_match(args, context, scope, values), + Self::PregMatchAll => eval_builtin_preg_match_all(args, context, scope, values), + Self::PregReplace => eval_builtin_preg_replace(args, context, scope, values), + Self::PregReplaceCallback => { + eval_builtin_preg_replace_callback(args, context, scope, values) + } + Self::PregSplit => eval_builtin_preg_split(args, context, scope, values), Self::RawMemory => eval_builtin_raw_memory_call(name, args, context, scope, values), Self::Sin => eval_builtin_sin(args, context, scope, values), Self::Sinh => eval_builtin_sinh(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 9e3aeed5fe..e2db6d0e26 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -41,10 +41,12 @@ use super::super::{ eval_json_last_error_result, eval_json_validate_values_result, eval_log2_result, eval_log10_result, eval_log_result, eval_max_result, eval_min_result, eval_mt_rand_values_result, eval_network_env_values_result, eval_nl2br_result, - eval_pi_result, eval_pow_result, eval_printf_result, eval_rad2deg_result, eval_rand_values_result, - eval_random_int_values_result, eval_range_result, eval_regex_values_result, eval_round_result, - eval_settype_values_result, eval_sin_result, eval_sinh_result, eval_slashes_result, - eval_sprintf_result, eval_sqrt_result, eval_sscanf_values_result, + eval_pi_result, eval_pow_result, eval_preg_match_all_values_result, eval_preg_match_values_result, + eval_preg_replace_callback_values_result, eval_preg_replace_values_result, + eval_preg_split_values_result, eval_printf_result, eval_rad2deg_result, eval_rand_values_result, + eval_random_int_values_result, eval_range_result, eval_round_result, eval_settype_values_result, + eval_sin_result, eval_sinh_result, eval_slashes_result, eval_sprintf_result, eval_sqrt_result, + eval_sscanf_values_result, eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, @@ -251,8 +253,16 @@ pub(in crate::interpreter) enum EvalValuesHook { Round, /// Dispatches `range(...)`. Range, - /// Dispatches regex builtins. - Regex, + /// Dispatches `preg_match(...)`. + PregMatch, + /// Dispatches `preg_match_all(...)`. + PregMatchAll, + /// Dispatches `preg_replace(...)`. + PregReplace, + /// Dispatches `preg_replace_callback(...)`. + PregReplaceCallback, + /// Dispatches `preg_split(...)`. + PregSplit, /// Dispatches raw pointer and buffer extension builtins. RawMemory, /// Dispatches by-value `settype(...)` callable calls. @@ -475,7 +485,13 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::Range => two_args(evaluated_args, values, eval_range_result), - Self::Regex => eval_regex_values_result(name, evaluated_args, context, values), + Self::PregMatch => eval_preg_match_values_result(evaluated_args, values), + Self::PregMatchAll => eval_preg_match_all_values_result(evaluated_args, values), + Self::PregReplace => eval_preg_replace_values_result(evaluated_args, values), + Self::PregReplaceCallback => { + eval_preg_replace_callback_values_result(evaluated_args, context, values) + } + Self::PregSplit => eval_preg_split_values_result(evaluated_args, values), Self::RawMemory => eval_raw_memory_values_result(name, evaluated_args, context, values), Self::Settype => eval_settype_values_result(evaluated_args, values), Self::Sin => one_arg(evaluated_args, values, eval_sin_result), diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/mod.rs deleted file mode 100644 index e1ff9e5fe2..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/mod.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries and dispatch adapters for preg regex builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::regex` module loading. -//! - `crate::interpreter::builtins::hooks` for migrated regex dispatch. -//! -//! Key details: -//! - Regex parsing, matching, capture assembly, replacement, and split behavior -//! stay in sibling helper modules. -//! - `preg_match()` and `preg_match_all()` keep source-sensitive direct paths -//! for `$matches` by-reference writeback. - -use super::super::super::*; -use super::*; - -mod preg_match; -mod preg_match_all; -mod preg_replace; -mod preg_replace_callback; -mod preg_split; - -/// Dispatches direct expression-level calls for declaratively migrated regex builtins. -pub(in crate::interpreter) fn eval_builtin_regex_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "preg_match" => eval_builtin_preg_match(args, context, scope, values), - "preg_match_all" => eval_builtin_preg_match_all(args, context, scope, values), - "preg_replace" => eval_builtin_preg_replace(args, context, scope, values), - "preg_replace_callback" => { - eval_builtin_preg_replace_callback(args, context, scope, values) - } - "preg_split" => eval_builtin_preg_split(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated regex builtins. -pub(in crate::interpreter) fn eval_regex_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "preg_match" => match evaluated_args { - [pattern, subject] => eval_preg_match_result(*pattern, *subject, values), - [pattern, subject, _matches] => { - values.warning( - "preg_match(): Argument #3 ($matches) must be passed by reference, value given", - )?; - let (matched, matches) = - eval_preg_match_capture_result(*pattern, *subject, None, values)?; - values.release(matches)?; - Ok(matched) - } - [pattern, subject, _matches, flags] => { - values.warning( - "preg_match(): Argument #3 ($matches) must be passed by reference, value given", - )?; - let (matched, matches) = - eval_preg_match_capture_result(*pattern, *subject, Some(*flags), values)?; - values.release(matches)?; - Ok(matched) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "preg_match_all" => match evaluated_args { - [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values), - [pattern, subject, _matches] => { - values.warning( - "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", - )?; - let (count, matches) = - eval_preg_match_all_capture_result(*pattern, *subject, None, values)?; - values.release(matches)?; - Ok(count) - } - [pattern, subject, _matches, flags] => { - values.warning( - "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", - )?; - let (count, matches) = - eval_preg_match_all_capture_result(*pattern, *subject, Some(*flags), values)?; - values.release(matches)?; - Ok(count) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "preg_replace" => { - let [pattern, replacement, subject] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_preg_replace_result(*pattern, *replacement, *subject, values) - } - "preg_replace_callback" => { - let [pattern, callback, subject] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values) - } - "preg_split" => match evaluated_args { - [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values), - [pattern, subject, limit] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), None, values) - } - [pattern, subject, limit, flags] => { - eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match.rs deleted file mode 100644 index 0d3cfa771a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `preg_match`. -//! -//! Called from: -//! - `crate::interpreter::builtins::regex::declarations`. -//! -//! Key details: -//! - `$matches` is by-reference and is still written through the regex direct -//! and dynamic mutation helpers. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "preg_match", - area: Regex, - params: [ - pattern, - subject, - matches: by_ref = EvalBuiltinDefaultValue::EmptyArray, - flags = EvalBuiltinDefaultValue::Int(0), - ], - by_ref: [matches], - direct: Regex, - values: Regex, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match_all.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match_all.rs deleted file mode 100644 index eef99dda15..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_match_all.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `preg_match_all`. -//! -//! Called from: -//! - `crate::interpreter::builtins::regex::declarations`. -//! -//! Key details: -//! - `$matches` is by-reference and is still written through the regex direct -//! and dynamic mutation helpers. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "preg_match_all", - area: Regex, - params: [ - pattern, - subject, - matches: by_ref = EvalBuiltinDefaultValue::EmptyArray, - flags = EvalBuiltinDefaultValue::Int(0), - ], - by_ref: [matches], - direct: Regex, - values: Regex, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace.rs deleted file mode 100644 index 0d331229e6..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `preg_replace`. -//! -//! Called from: -//! - `crate::interpreter::builtins::regex::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the regex replacement hook. - -eval_builtin! { - name: "preg_replace", - area: Regex, - params: [pattern, replacement, subject], - direct: Regex, - values: Regex, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace_callback.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace_callback.rs deleted file mode 100644 index 1c091792d9..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_replace_callback.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `preg_replace_callback`. -//! -//! Called from: -//! - `crate::interpreter::builtins::regex::declarations`. -//! -//! Key details: -//! - Direct calls keep lexical scope for callback names; evaluated dynamic -//! dispatch uses the same scope-free behavior as the legacy dispatcher. - -eval_builtin! { - name: "preg_replace_callback", - area: Regex, - params: [pattern, callback, subject], - direct: Regex, - values: Regex, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_split.rs b/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_split.rs deleted file mode 100644 index 7063696235..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/regex/declarations/preg_split.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `preg_split`. -//! -//! Called from: -//! - `crate::interpreter::builtins::regex::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the split hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "preg_split", - area: Regex, - params: [ - pattern, - subject, - limit = EvalBuiltinDefaultValue::Int(-1), - flags = EvalBuiltinDefaultValue::Int(0), - ], - direct: Regex, - values: Regex, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs index 4595c62477..e653a2a628 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs @@ -11,25 +11,25 @@ //! behavior through `RuntimeValueOps`. mod captures; -mod declarations; mod engine; -mod match_all; -mod match_one; +mod preg_match; +mod preg_match_all; mod pattern; -mod replace; +mod preg_replace; +mod preg_replace_callback; mod replacement; -mod split; +mod preg_split; mod split_helpers; mod targets; pub(in crate::interpreter) use captures::*; -pub(in crate::interpreter) use declarations::*; pub(in crate::interpreter) use engine::*; -pub(in crate::interpreter) use match_all::*; -pub(in crate::interpreter) use match_one::*; +pub(in crate::interpreter) use preg_match::*; +pub(in crate::interpreter) use preg_match_all::*; pub(in crate::interpreter) use pattern::*; -pub(in crate::interpreter) use replace::*; +pub(in crate::interpreter) use preg_replace::*; +pub(in crate::interpreter) use preg_replace_callback::*; pub(in crate::interpreter) use replacement::*; -pub(in crate::interpreter) use split::*; +pub(in crate::interpreter) use preg_split::*; pub(in crate::interpreter) use split_helpers::*; pub(in crate::interpreter) use targets::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs similarity index 74% rename from crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs index cd15e93e27..95372e8e47 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/match_one.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs @@ -1,18 +1,32 @@ //! Purpose: -//! Implements eval support for PHP `preg_match()` and its immediate flags/result -//! helpers. +//! Eval registry entry and implementation for `preg_match`. //! //! Called from: -//! - `crate::interpreter::builtins::regex` re-exports. +//! - `crate::interpreter::builtins::hooks` and special by-ref call handling. //! //! Key details: -//! - `$matches` assignment captures writable caller lvalues and writes back the -//! materialized capture array after regex execution. - +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! - `$matches` writeback for `preg_match()`. use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; use super::super::*; use super::*; +eval_builtin! { + name: "preg_match", + area: Regex, + params: [ + pattern, + subject, + matches: by_ref = EvalBuiltinDefaultValue::EmptyArray, + flags = EvalBuiltinDefaultValue::Int(0), + ], + by_ref: [matches], + direct: PregMatch, + values: PregMatch, +} + + /// Evaluates PHP `preg_match()` over eval expressions. pub(in crate::interpreter) fn eval_builtin_preg_match( args: &[EvalExpr], @@ -133,3 +147,32 @@ pub(in crate::interpreter) fn eval_preg_match_flags( } Ok(flags) } + +/// Dispatches by-value `preg_match()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_match_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, subject] => eval_preg_match_result(*pattern, *subject, values), + [pattern, subject, _matches] => { + values.warning( + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (matched, matches) = + eval_preg_match_capture_result(*pattern, *subject, None, values)?; + values.release(matches)?; + Ok(matched) + } + [pattern, subject, _matches, flags] => { + values.warning( + "preg_match(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (matched, matches) = + eval_preg_match_capture_result(*pattern, *subject, Some(*flags), values)?; + values.release(matches)?; + Ok(matched) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs similarity index 80% rename from crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs index 33e570f506..1e544ba482 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/match_all.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs @@ -1,17 +1,32 @@ //! Purpose: -//! Implements eval support for PHP `preg_match_all()` and capture-matrix assembly. +//! Eval registry entry and implementation for `preg_match_all`. //! //! Called from: -//! - `crate::interpreter::builtins::regex` re-exports. +//! - `crate::interpreter::builtins::hooks` and special by-ref call handling. //! //! Key details: -//! - Pattern-order and set-order arrays share the common capture-value helper so -//! offset and unmatched-null flags remain consistent. - +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! - capture-matrix assembly for `preg_match_all()`. use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; use super::super::*; use super::*; +eval_builtin! { + name: "preg_match_all", + area: Regex, + params: [ + pattern, + subject, + matches: by_ref = EvalBuiltinDefaultValue::EmptyArray, + flags = EvalBuiltinDefaultValue::Int(0), + ], + by_ref: [matches], + direct: PregMatchAll, + values: PregMatchAll, +} + + /// Evaluates PHP `preg_match_all()` over eval expressions. pub(in crate::interpreter) fn eval_builtin_preg_match_all( args: &[EvalExpr], @@ -193,3 +208,32 @@ pub(in crate::interpreter) fn eval_preg_match_all_set_order_array( } Ok(outer) } + +/// Dispatches by-value `preg_match_all()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_match_all_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, subject] => eval_preg_match_all_result(*pattern, *subject, values), + [pattern, subject, _matches] => { + values.warning( + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (count, matches) = + eval_preg_match_all_capture_result(*pattern, *subject, None, values)?; + values.release(matches)?; + Ok(count) + } + [pattern, subject, _matches, flags] => { + values.warning( + "preg_match_all(): Argument #3 ($matches) must be passed by reference, value given", + )?; + let (count, matches) = + eval_preg_match_all_capture_result(*pattern, *subject, Some(*flags), values)?; + values.release(matches)?; + Ok(count) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs new file mode 100644 index 0000000000..0bd14bd899 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Eval registry entry and implementation for `preg_replace`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! backreference replacement expansion for `preg_replace()`. + +use super::super::super::*; +use super::*; + +eval_builtin! { + name: "preg_replace", + area: Regex, + params: [pattern, replacement, subject], + direct: PregReplace, + values: PregReplace, +} + +/// Evaluates PHP `preg_replace()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_preg_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, replacement, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + let replacement = eval_expr(replacement, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_preg_replace_result(pattern, replacement, subject, values) +} + +/// Replaces every regex match with a PHP-style backreference-expanded replacement. +pub(in crate::interpreter) fn eval_preg_replace_result( + pattern: RuntimeCellHandle, + replacement: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let regex = eval_preg_regex(pattern, values)?; + let replacement = values.string_bytes(replacement)?; + let subject = values.string_bytes(subject)?; + let mut result = Vec::with_capacity(subject.len()); + let mut cursor = 0; + for captures in regex.captures_iter(&subject) { + let Some(matched) = captures.get(0) else { + continue; + }; + result.extend_from_slice(&subject[cursor..matched.start()]); + eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); + cursor = matched.end(); + } + result.extend_from_slice(&subject[cursor..]); + values.string_bytes_value(&result) +} + + +/// Dispatches by-value `preg_replace()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_replace_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, replacement, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_preg_replace_result(*pattern, *replacement, *subject, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs similarity index 62% rename from crates/elephc-magician/src/interpreter/builtins/regex/replace.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs index 66ae6b6e50..9f37d770f8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/replace.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs @@ -1,55 +1,23 @@ //! Purpose: -//! Implements eval support for PHP `preg_replace()` and `preg_replace_callback()`. +//! Eval registry entry and implementation for `preg_replace_callback`. //! //! Called from: -//! - `crate::interpreter::builtins::regex` re-exports. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Callback replacement evaluates general eval callables and casts callback -//! results with runtime string coercion. +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! callback invocation for `preg_replace_callback()`. use super::super::super::*; use super::super::*; use super::*; -/// Evaluates PHP `preg_replace()` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_preg_replace( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern, replacement, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - let replacement = eval_expr(replacement, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_preg_replace_result(pattern, replacement, subject, values) -} - -/// Replaces every regex match with a PHP-style backreference-expanded replacement. -pub(in crate::interpreter) fn eval_preg_replace_result( - pattern: RuntimeCellHandle, - replacement: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let regex = eval_preg_regex(pattern, values)?; - let replacement = values.string_bytes(replacement)?; - let subject = values.string_bytes(subject)?; - let mut result = Vec::with_capacity(subject.len()); - let mut cursor = 0; - for captures in regex.captures_iter(&subject) { - let Some(matched) = captures.get(0) else { - continue; - }; - result.extend_from_slice(&subject[cursor..matched.start()]); - eval_preg_expand_replacement(&replacement, &subject, &captures, &mut result); - cursor = matched.end(); - } - result.extend_from_slice(&subject[cursor..]); - values.string_bytes_value(&result) +eval_builtin! { + name: "preg_replace_callback", + area: Regex, + params: [pattern, callback, subject], + direct: PregReplaceCallback, + values: PregReplaceCallback, } /// Evaluates PHP `preg_replace_callback()` over eval expressions. @@ -109,3 +77,16 @@ fn eval_preg_replace_callback_result_from_scope( result.extend_from_slice(&subject[cursor..]); values.string_bytes_value(&result) } + + +/// Dispatches by-value `preg_replace_callback()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_replace_callback_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern, callback, subject] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_preg_replace_callback_result(*pattern, *callback, *subject, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/split.rs b/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs similarity index 72% rename from crates/elephc-magician/src/interpreter/builtins/regex/split.rs rename to crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs index 5e4d43b210..e4e837cfd6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/split.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs @@ -1,16 +1,30 @@ //! Purpose: -//! Implements eval support for PHP `preg_split()` entrypoint and result assembly. +//! Eval registry entry and implementation for `preg_split`. //! //! Called from: -//! - `crate::interpreter::builtins::regex` re-exports. +//! - `crate::interpreter::builtins::hooks`. //! //! Key details: -//! - Split flags are decoded before collecting pieces so delimiter capture, empty -//! filtering, and offset capture share one result path. - +//! - This file owns registry metadata, direct dispatch, by-value dispatch, and +//! - result assembly for `preg_split()`. use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; use super::*; +eval_builtin! { + name: "preg_split", + area: Regex, + params: [ + pattern, + subject, + limit = EvalBuiltinDefaultValue::Int(-1), + flags = EvalBuiltinDefaultValue::Int(0), + ], + direct: PregSplit, + values: PregSplit, +} + + /// Evaluates PHP `preg_split()` over eval expressions. pub(in crate::interpreter) fn eval_builtin_preg_split( args: &[EvalExpr], @@ -87,3 +101,20 @@ pub(in crate::interpreter) fn eval_preg_split_result( } Ok(result) } + +/// Dispatches by-value `preg_split()` calls after argument binding. +pub(in crate::interpreter) fn eval_preg_split_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, subject] => eval_preg_split_result(*pattern, *subject, None, None, values), + [pattern, subject, limit] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), None, values) + } + [pattern, subject, limit, flags] => { + eval_preg_split_result(*pattern, *subject, Some(*limit), Some(*flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} From 1e39fb427817c105735f4b11825359a7357b3513 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 16:02:30 +0200 Subject: [PATCH 1104/1208] refactor(magician): co-locate raw memory builtins --- .../src/interpreter/builtins/hooks/direct.rs | 68 ++- .../src/interpreter/builtins/hooks/values.rs | 68 ++- .../src/interpreter/builtins/raw_memory.rs | 429 ------------------ .../builtins/raw_memory/buffer_free.rs | 88 ++++ .../builtins/raw_memory/buffer_len.rs | 56 +++ .../builtins/raw_memory/buffer_new.rs | 82 ++++ .../raw_memory/declarations/buffer_free.rs | 16 - .../raw_memory/declarations/buffer_len.rs | 16 - .../raw_memory/declarations/buffer_new.rs | 16 - .../builtins/raw_memory/declarations/mod.rs | 54 --- .../builtins/raw_memory/declarations/ptr.rs | 17 - .../raw_memory/declarations/ptr_get.rs | 16 - .../raw_memory/declarations/ptr_is_null.rs | 16 - .../raw_memory/declarations/ptr_null.rs | 16 - .../raw_memory/declarations/ptr_offset.rs | 16 - .../raw_memory/declarations/ptr_read16.rs | 16 - .../raw_memory/declarations/ptr_read32.rs | 16 - .../raw_memory/declarations/ptr_read8.rs | 16 - .../declarations/ptr_read_string.rs | 16 - .../raw_memory/declarations/ptr_set.rs | 16 - .../raw_memory/declarations/ptr_sizeof.rs | 16 - .../raw_memory/declarations/ptr_write16.rs | 16 - .../raw_memory/declarations/ptr_write32.rs | 16 - .../raw_memory/declarations/ptr_write8.rs | 16 - .../declarations/ptr_write_string.rs | 16 - .../interpreter/builtins/raw_memory/mod.rs | 49 ++ .../interpreter/builtins/raw_memory/ptr.rs | 72 +++ .../builtins/raw_memory/ptr_get.rs | 83 ++++ .../builtins/raw_memory/ptr_is_null.rs | 53 +++ .../builtins/raw_memory/ptr_null.rs | 48 ++ .../builtins/raw_memory/ptr_offset.rs | 63 +++ .../builtins/raw_memory/ptr_read16.rs | 56 +++ .../builtins/raw_memory/ptr_read32.rs | 56 +++ .../builtins/raw_memory/ptr_read8.rs | 56 +++ .../builtins/raw_memory/ptr_read_string.rs | 63 +++ .../builtins/raw_memory/ptr_set.rs | 89 ++++ .../builtins/raw_memory/ptr_sizeof.rs | 80 ++++ .../builtins/raw_memory/ptr_write16.rs | 59 +++ .../builtins/raw_memory/ptr_write32.rs | 59 +++ .../builtins/raw_memory/ptr_write8.rs | 59 +++ .../builtins/raw_memory/ptr_write_string.rs | 61 +++ 41 files changed, 1356 insertions(+), 784 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_free.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_len.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_new.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_get.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_is_null.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_null.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_offset.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read16.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read32.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read8.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read_string.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_set.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_sizeof.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write16.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write32.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write8.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write_string.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 7d86db1d46..9feab858c6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -45,9 +45,14 @@ use super::super::{ eval_builtin_pi, eval_builtin_pow, eval_builtin_rad2deg, eval_builtin_rand, eval_builtin_random_int, eval_builtin_range, eval_builtin_round, eval_builtin_preg_match, eval_builtin_preg_match_all, eval_builtin_preg_replace, - eval_builtin_preg_replace_callback, eval_builtin_preg_split, eval_builtin_raw_memory_call, - eval_builtin_sin, eval_builtin_sinh, eval_builtin_str_pad, eval_builtin_str_replace, - eval_builtin_str_split, + eval_builtin_preg_replace_callback, eval_builtin_preg_split, eval_builtin_buffer_free, + eval_builtin_buffer_len, eval_builtin_buffer_new, eval_builtin_ptr, eval_builtin_ptr_get, + eval_builtin_ptr_is_null, eval_builtin_ptr_null, eval_builtin_ptr_offset, + eval_builtin_ptr_read16, eval_builtin_ptr_read32, eval_builtin_ptr_read8, + eval_builtin_ptr_read_string, eval_builtin_ptr_set, eval_builtin_ptr_sizeof, + eval_builtin_ptr_write16, eval_builtin_ptr_write32, eval_builtin_ptr_write8, + eval_builtin_ptr_write_string, eval_builtin_sin, eval_builtin_sinh, eval_builtin_str_pad, + eval_builtin_str_replace, eval_builtin_str_split, eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, @@ -256,8 +261,42 @@ pub(in crate::interpreter) enum EvalDirectHook { PregReplaceCallback, /// Dispatches `preg_split(...)`. PregSplit, - /// Dispatches raw pointer and buffer extension builtins. - RawMemory, + /// Dispatches `buffer_free(...)`. + BufferFree, + /// Dispatches `buffer_len(...)`. + BufferLen, + /// Dispatches `buffer_new(...)`. + BufferNew, + /// Dispatches `ptr(...)`. + Ptr, + /// Dispatches `ptr_get(...)`. + PtrGet, + /// Dispatches `ptr_is_null(...)`. + PtrIsNull, + /// Dispatches `ptr_null()`. + PtrNull, + /// Dispatches `ptr_offset(...)`. + PtrOffset, + /// Dispatches `ptr_read8(...)`. + PtrRead8, + /// Dispatches `ptr_read16(...)`. + PtrRead16, + /// Dispatches `ptr_read32(...)`. + PtrRead32, + /// Dispatches `ptr_read_string(...)`. + PtrReadString, + /// Dispatches `ptr_set(...)`. + PtrSet, + /// Dispatches `ptr_sizeof(...)`. + PtrSizeof, + /// Dispatches `ptr_write8(...)`. + PtrWrite8, + /// Dispatches `ptr_write16(...)`. + PtrWrite16, + /// Dispatches `ptr_write32(...)`. + PtrWrite32, + /// Dispatches `ptr_write_string(...)`. + PtrWriteString, /// Dispatches `addslashes(...)` and `stripslashes(...)`. Slashes, /// Dispatches `sin(...)`. @@ -435,7 +474,24 @@ impl EvalDirectHook { eval_builtin_preg_replace_callback(args, context, scope, values) } Self::PregSplit => eval_builtin_preg_split(args, context, scope, values), - Self::RawMemory => eval_builtin_raw_memory_call(name, args, context, scope, values), + Self::BufferFree => eval_builtin_buffer_free(args, context, scope, values), + Self::BufferLen => eval_builtin_buffer_len(args, context, scope, values), + Self::BufferNew => eval_builtin_buffer_new(args, context, scope, values), + Self::Ptr => eval_builtin_ptr(args, context, scope, values), + Self::PtrGet => eval_builtin_ptr_get(args, context, scope, values), + Self::PtrIsNull => eval_builtin_ptr_is_null(args, context, scope, values), + Self::PtrNull => eval_builtin_ptr_null(args, context, scope, values), + Self::PtrOffset => eval_builtin_ptr_offset(args, context, scope, values), + Self::PtrRead8 => eval_builtin_ptr_read8(args, context, scope, values), + Self::PtrRead16 => eval_builtin_ptr_read16(args, context, scope, values), + Self::PtrRead32 => eval_builtin_ptr_read32(args, context, scope, values), + Self::PtrReadString => eval_builtin_ptr_read_string(args, context, scope, values), + Self::PtrSet => eval_builtin_ptr_set(args, context, scope, values), + Self::PtrSizeof => eval_builtin_ptr_sizeof(args, context, scope, values), + Self::PtrWrite8 => eval_builtin_ptr_write8(args, context, scope, values), + Self::PtrWrite16 => eval_builtin_ptr_write16(args, context, scope, values), + Self::PtrWrite32 => eval_builtin_ptr_write32(args, context, scope, values), + Self::PtrWriteString => eval_builtin_ptr_write_string(args, context, scope, values), Self::Sin => eval_builtin_sin(args, context, scope, values), Self::Sinh => eval_builtin_sinh(args, context, scope, values), Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index e2db6d0e26..89265de98b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -51,9 +51,14 @@ use super::super::{ eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, eval_string_case_result, eval_string_compare_result, eval_string_position_result, eval_string_search_result, eval_strstr_result, eval_strval_result, - eval_substr_replace_result, eval_substr_result, eval_raw_memory_values_result, - eval_symbols_values_result, eval_tan_result, eval_tanh_result, eval_time_values_result, - eval_trim_like_result, + eval_substr_replace_result, eval_substr_result, eval_buffer_free_values_result, + eval_buffer_len_values_result, eval_buffer_new_values_result, eval_ptr_get_values_result, + eval_ptr_is_null_values_result, eval_ptr_null_values_result, eval_ptr_offset_values_result, + eval_ptr_read16_values_result, eval_ptr_read32_values_result, eval_ptr_read8_values_result, + eval_ptr_read_string_values_result, eval_ptr_set_values_result, eval_ptr_sizeof_values_result, + eval_ptr_values_result, eval_ptr_write16_values_result, eval_ptr_write32_values_result, + eval_ptr_write8_values_result, eval_ptr_write_string_values_result, eval_symbols_values_result, + eval_tan_result, eval_tanh_result, eval_time_values_result, eval_trim_like_result, eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, eval_vprintf_result, eval_vsprintf_result, eval_wordwrap_result, }; @@ -263,8 +268,42 @@ pub(in crate::interpreter) enum EvalValuesHook { PregReplaceCallback, /// Dispatches `preg_split(...)`. PregSplit, - /// Dispatches raw pointer and buffer extension builtins. - RawMemory, + /// Dispatches `buffer_free(...)`. + BufferFree, + /// Dispatches `buffer_len(...)`. + BufferLen, + /// Dispatches `buffer_new(...)`. + BufferNew, + /// Dispatches `ptr(...)`. + Ptr, + /// Dispatches `ptr_get(...)`. + PtrGet, + /// Dispatches `ptr_is_null(...)`. + PtrIsNull, + /// Dispatches `ptr_null()`. + PtrNull, + /// Dispatches `ptr_offset(...)`. + PtrOffset, + /// Dispatches `ptr_read8(...)`. + PtrRead8, + /// Dispatches `ptr_read16(...)`. + PtrRead16, + /// Dispatches `ptr_read32(...)`. + PtrRead32, + /// Dispatches `ptr_read_string(...)`. + PtrReadString, + /// Dispatches `ptr_set(...)`. + PtrSet, + /// Dispatches `ptr_sizeof(...)`. + PtrSizeof, + /// Dispatches `ptr_write8(...)`. + PtrWrite8, + /// Dispatches `ptr_write16(...)`. + PtrWrite16, + /// Dispatches `ptr_write32(...)`. + PtrWrite32, + /// Dispatches `ptr_write_string(...)`. + PtrWriteString, /// Dispatches by-value `settype(...)` callable calls. Settype, /// Dispatches `addslashes(...)` and `stripslashes(...)`. @@ -492,7 +531,24 @@ impl EvalValuesHook { eval_preg_replace_callback_values_result(evaluated_args, context, values) } Self::PregSplit => eval_preg_split_values_result(evaluated_args, values), - Self::RawMemory => eval_raw_memory_values_result(name, evaluated_args, context, values), + Self::BufferFree => eval_buffer_free_values_result(evaluated_args, values), + Self::BufferLen => eval_buffer_len_values_result(evaluated_args, values), + Self::BufferNew => eval_buffer_new_values_result(evaluated_args, values), + Self::Ptr => eval_ptr_values_result(evaluated_args), + Self::PtrGet => eval_ptr_get_values_result(evaluated_args, values), + Self::PtrIsNull => eval_ptr_is_null_values_result(evaluated_args, values), + Self::PtrNull => eval_ptr_null_values_result(evaluated_args, values), + Self::PtrOffset => eval_ptr_offset_values_result(evaluated_args, values), + Self::PtrRead8 => eval_ptr_read8_values_result(evaluated_args, values), + Self::PtrRead16 => eval_ptr_read16_values_result(evaluated_args, values), + Self::PtrRead32 => eval_ptr_read32_values_result(evaluated_args, values), + Self::PtrReadString => eval_ptr_read_string_values_result(evaluated_args, values), + Self::PtrSet => eval_ptr_set_values_result(evaluated_args, values), + Self::PtrSizeof => eval_ptr_sizeof_values_result(evaluated_args, context, values), + Self::PtrWrite8 => eval_ptr_write8_values_result(evaluated_args, values), + Self::PtrWrite16 => eval_ptr_write16_values_result(evaluated_args, values), + Self::PtrWrite32 => eval_ptr_write32_values_result(evaluated_args, values), + Self::PtrWriteString => eval_ptr_write_string_values_result(evaluated_args, values), Self::Settype => eval_settype_values_result(evaluated_args, values), Self::Sin => one_arg(evaluated_args, values, eval_sin_result), Self::Sinh => one_arg(evaluated_args, values, eval_sinh_result), diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs deleted file mode 100644 index 7abf25302d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory.rs +++ /dev/null @@ -1,429 +0,0 @@ -//! Purpose: -//! Implements eval-side raw pointer and buffer extension builtins. -//! These helpers expose the AOT-visible names while preserving the raw-address -//! representation as integer runtime cells inside eval. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - `crate::interpreter::builtins::registry::dispatch::eval_builtin_with_values()`. -//! -//! Key details: -//! - `buffer_new()` returns the same header pointer shape used by AOT buffers: -//! length word, stride word, then zeroed payload. -//! - `ptr($var)` still requires lvalue storage that the by-value eval call path -//! does not expose, so it fails instead of inventing an unsafe fake address. - -use std::mem; -use std::ptr; -use std::slice; - -use super::super::*; - -mod declarations; - -pub(in crate::interpreter) use declarations::{ - eval_builtin_raw_memory_call, eval_raw_memory_values_result, -}; - -const BUFFER_HEADER_WORDS: usize = 2; -const BUFFER_DEFAULT_STRIDE: usize = 8; - -/// Evaluates raw-memory builtins whose arguments are still source expressions. -pub(in crate::interpreter) fn eval_builtin_raw_memory( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if name == "ptr" { - let [_value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - return Err(EvalStatus::UnsupportedConstruct); - } - if name == "buffer_free" { - if let [EvalExpr::LoadVar(variable)] = args { - return eval_buffer_free_direct_variable(variable, context, scope, values); - } - } - let evaluated_args = args - .iter() - .map(|arg| eval_expr(arg, context, scope, values)) - .collect::, _>>()?; - eval_raw_memory_builtin_result(name, &evaluated_args, context, values) -} - -/// Applies one raw-memory builtin to already evaluated runtime cells. -fn eval_raw_memory_builtin_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "buffer_new" => { - let [length] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_buffer_new_result(*length, values) - } - "buffer_len" => { - let [buffer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_buffer_len_result(*buffer, values) - } - "buffer_free" => { - let [buffer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_buffer_free_result(*buffer, values) - } - "ptr" => { - let [_value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - Err(EvalStatus::UnsupportedConstruct) - } - "ptr_null" => { - let [] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.int(0) - } - "ptr_is_null" => { - let [pointer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let address = eval_pointer_address(*pointer, values)?; - values.bool_value(address == 0) - } - "ptr_offset" => { - let [pointer, offset] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ptr_offset_result(*pointer, *offset, values) - } - "ptr_get" => { - let [pointer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pointer_read_result(*pointer, PointerReadWidth::Word64, values) - } - "ptr_set" => { - let [pointer, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pointer_write_result(*pointer, *value, PointerWriteWidth::Word64, values) - } - "ptr_read8" => { - let [pointer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pointer_read_result(*pointer, PointerReadWidth::Byte, values) - } - "ptr_read16" => { - let [pointer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pointer_read_result(*pointer, PointerReadWidth::Half, values) - } - "ptr_read32" => { - let [pointer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pointer_read_result(*pointer, PointerReadWidth::Word32, values) - } - "ptr_write8" => { - let [pointer, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pointer_write_result(*pointer, *value, PointerWriteWidth::Byte, values) - } - "ptr_write16" => { - let [pointer, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pointer_write_result(*pointer, *value, PointerWriteWidth::Half, values) - } - "ptr_write32" => { - let [pointer, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_pointer_write_result(*pointer, *value, PointerWriteWidth::Word32, values) - } - "ptr_read_string" => { - let [pointer, length] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ptr_read_string_result(*pointer, *length, values) - } - "ptr_write_string" => { - let [pointer, string] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ptr_write_string_result(*pointer, *string, values) - } - "ptr_sizeof" => { - let [type_name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ptr_sizeof_result(*type_name, context, values) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Allocates a zero-filled AOT-shaped buffer and returns its header address. -fn eval_buffer_new_result( - length: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let length = eval_int_value(length, values)?; - if length < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - let header_bytes = BUFFER_HEADER_WORDS - .checked_mul(mem::size_of::()) - .ok_or(EvalStatus::RuntimeFatal)?; - let payload_bytes = length - .checked_mul(BUFFER_DEFAULT_STRIDE) - .ok_or(EvalStatus::RuntimeFatal)?; - let total_bytes = header_bytes - .checked_add(payload_bytes) - .ok_or(EvalStatus::RuntimeFatal)?; - let allocation = unsafe { libc::calloc(total_bytes.max(1), 1) }; - if allocation.is_null() { - return Err(EvalStatus::RuntimeFatal); - } - unsafe { - let header = allocation.cast::(); - ptr::write(header, length); - ptr::write(header.add(1), BUFFER_DEFAULT_STRIDE); - } - eval_address_value(allocation as usize, values) -} - -/// Reads the logical element count from an AOT-shaped buffer header. -fn eval_buffer_len_result( - buffer: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let header = eval_non_null_pointer(buffer, values)?.cast::(); - let length = unsafe { ptr::read(header) }; - values.int(i64::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Frees an AOT-shaped buffer header and returns PHP null. -fn eval_buffer_free_result( - buffer: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_buffer_free_address(buffer, values)?; - values.null() -} - -/// Frees a local buffer variable and replaces the source variable with null. -fn eval_buffer_free_direct_variable( - variable: &str, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_expr(&EvalExpr::LoadVar(variable.to_string()), context, scope, values)?; - eval_buffer_free_address(value, values)?; - let null = values.null()?; - for replaced in scope.set_respecting_references( - variable.to_string(), - null, - ScopeCellOwnership::Owned, - ) { - values.release(replaced)?; - } - values.null() -} - -/// Frees the raw allocation addressed by an AOT-shaped buffer header pointer. -fn eval_buffer_free_address( - buffer: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let address = eval_non_null_pointer(buffer, values)?; - unsafe { - libc::free(address.cast::()); - } - Ok(()) -} - -/// Computes a derived raw pointer address by adding a signed byte offset. -fn eval_ptr_offset_result( - pointer: RuntimeCellHandle, - offset: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_pointer_address(pointer, values)?; - let offset = eval_int_value(offset, values)?; - let address = if offset >= 0 { - let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; - address.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)? - } else { - let offset = usize::try_from(offset.unsigned_abs()).map_err(|_| EvalStatus::RuntimeFatal)?; - address.checked_sub(offset).ok_or(EvalStatus::RuntimeFatal)? - }; - eval_address_value(address, values) -} - -/// Reads one unsigned or machine-word value from raw memory. -fn eval_pointer_read_result( - pointer: RuntimeCellHandle, - width: PointerReadWidth, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_non_null_pointer(pointer, values)?; - let value = unsafe { - match width { - PointerReadWidth::Byte => i64::from(ptr::read_unaligned(address.cast::())), - PointerReadWidth::Half => i64::from(ptr::read_unaligned(address.cast::())), - PointerReadWidth::Word32 => i64::from(ptr::read_unaligned(address.cast::())), - PointerReadWidth::Word64 => { - let word = ptr::read_unaligned(address.cast::()); - i64::from_ne_bytes(word.to_ne_bytes()) - } - } - }; - values.int(value) -} - -/// Writes one integer payload to raw memory and returns PHP null. -fn eval_pointer_write_result( - pointer: RuntimeCellHandle, - value: RuntimeCellHandle, - width: PointerWriteWidth, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_non_null_pointer(pointer, values)?; - let value = eval_int_value(value, values)?; - unsafe { - match width { - PointerWriteWidth::Byte => ptr::write_unaligned(address.cast::(), value as u8), - PointerWriteWidth::Half => ptr::write_unaligned(address.cast::(), value as u16), - PointerWriteWidth::Word32 => ptr::write_unaligned(address.cast::(), value as u32), - PointerWriteWidth::Word64 => { - ptr::write_unaligned(address.cast::(), u64::from_ne_bytes(value.to_ne_bytes())) - } - } - } - values.null() -} - -/// Copies raw memory bytes into a PHP byte string. -fn eval_ptr_read_string_result( - pointer: RuntimeCellHandle, - length: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_non_null_pointer(pointer, values)?; - let length = eval_int_value(length, values)?; - if length < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - let bytes = unsafe { slice::from_raw_parts(address.cast::(), length) }; - values.string_bytes_value(bytes) -} - -/// Copies PHP string bytes into raw memory and returns the byte count written. -fn eval_ptr_write_string_result( - pointer: RuntimeCellHandle, - string: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_non_null_pointer(pointer, values)?; - let bytes = values.string_bytes(string)?; - unsafe { - ptr::copy_nonoverlapping(bytes.as_ptr(), address.cast::(), bytes.len()); - } - values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Computes the checked byte size for a low-level type name. -fn eval_ptr_sizeof_result( - type_name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(type_name)?; - let type_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - let size = eval_pointer_target_size(type_name.trim_start_matches('\\'), context) - .ok_or(EvalStatus::RuntimeFatal)?; - values.int(i64::try_from(size).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Returns the eval-side byte size for one low-level pointer target name. -fn eval_pointer_target_size(type_name: &str, context: &ElephcEvalContext) -> Option { - match type_name.to_ascii_lowercase().as_str() { - "int" | "integer" => Some(8), - "float" | "double" | "real" => Some(8), - "bool" | "boolean" => Some(8), - "string" => Some(16), - "ptr" | "pointer" => Some(8), - _ => context.class(type_name).map(eval_boxed_class_size), - } -} - -/// Returns the boxed object storage size used by AOT class metadata. -fn eval_boxed_class_size(class: &EvalClass) -> usize { - let instance_properties = class - .properties() - .iter() - .filter(|property| !property.is_static()) - .count(); - 8 + instance_properties * 16 -} - -/// Converts a runtime cell to a raw pointer address encoded as a PHP integer. -fn eval_pointer_address( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_int_value(value, values)?; - usize::try_from(address).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Converts a runtime cell to a non-null raw pointer. -fn eval_non_null_pointer( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<*mut u8, EvalStatus> { - let address = eval_pointer_address(value, values)?; - if address == 0 { - return Err(EvalStatus::RuntimeFatal); - } - Ok(address as *mut u8) -} - -/// Boxes a raw pointer address as a PHP integer cell. -fn eval_address_value( - address: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - values.int(i64::try_from(address).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Widths supported by pointer read helpers. -enum PointerReadWidth { - Byte, - Half, - Word32, - Word64, -} - -/// Widths supported by pointer write helpers. -enum PointerWriteWidth { - Byte, - Half, - Word32, - Word64, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs new file mode 100644 index 0000000000..cd27d98291 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Eval registry entry and implementation for `buffer_free`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so a local buffer variable can be nulled. + +use super::super::super::*; + + +eval_builtin! { + name: "buffer_free", + area: RawMemory, + params: [buffer], + direct: BufferFree, + values: BufferFree, +} + +/// Evaluates PHP `buffer_free()` and nulls direct local variables when possible. +pub(in crate::interpreter) fn eval_builtin_buffer_free( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let [EvalExpr::LoadVar(variable)] = args { + return eval_buffer_free_direct_variable(variable, context, scope, values); + } + let [buffer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let buffer = eval_expr(buffer, context, scope, values)?; + eval_buffer_free_result(buffer, values) +} + +/// Dispatches by-value `buffer_free()` calls after argument binding. +pub(in crate::interpreter) fn eval_buffer_free_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_free_result(*buffer, values) +} + +/// Frees an AOT-shaped buffer header and returns PHP null. +fn eval_buffer_free_result( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_buffer_free_address(buffer, values)?; + values.null() +} + +/// Frees a local buffer variable and replaces the source variable with null. +fn eval_buffer_free_direct_variable( + variable: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_expr(&EvalExpr::LoadVar(variable.to_string()), context, scope, values)?; + eval_buffer_free_address(value, values)?; + let null = values.null()?; + for replaced in scope.set_respecting_references( + variable.to_string(), + null, + ScopeCellOwnership::Owned, + ) { + values.release(replaced)?; + } + values.null() +} + +/// Frees the raw allocation addressed by an AOT-shaped buffer header pointer. +fn eval_buffer_free_address( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let address = super::ptr::eval_non_null_pointer(buffer, values)?; + unsafe { + libc::free(address.cast::()); + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs new file mode 100644 index 0000000000..285880ec4d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `buffer_len`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reads the logical element count from an AOT-shaped buffer header. + +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "buffer_len", + area: RawMemory, + params: [buffer], + direct: BufferLen, + values: BufferLen, +} + +/// Evaluates PHP `buffer_len()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_buffer_len( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [buffer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let buffer = eval_expr(buffer, context, scope, values)?; + eval_buffer_len_result(buffer, values) +} + +/// Dispatches by-value `buffer_len()` calls after argument binding. +pub(in crate::interpreter) fn eval_buffer_len_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_len_result(*buffer, values) +} + +/// Reads the logical element count from an AOT-shaped buffer header. +fn eval_buffer_len_result( + buffer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let header = super::ptr::eval_non_null_pointer(buffer, values)?.cast::(); + let length = unsafe { ptr::read(header) }; + values.int(i64::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs new file mode 100644 index 0000000000..41efa687cb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Eval registry entry and implementation for `buffer_new`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - `buffer_new()` returns the same header pointer shape used by AOT buffers: +//! length word, stride word, then zeroed payload. + +use std::mem; +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "buffer_new", + area: RawMemory, + params: [length], + direct: BufferNew, + values: BufferNew, +} + +const BUFFER_HEADER_WORDS: usize = 2; +const BUFFER_DEFAULT_STRIDE: usize = 8; + +/// Evaluates PHP `buffer_new()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_buffer_new( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let length = eval_expr(length, context, scope, values)?; + eval_buffer_new_result(length, values) +} + +/// Dispatches by-value `buffer_new()` calls after argument binding. +pub(in crate::interpreter) fn eval_buffer_new_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_buffer_new_result(*length, values) +} + +/// Allocates a zero-filled AOT-shaped buffer and returns its header address. +fn eval_buffer_new_result( + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let length = eval_int_value(length, values)?; + if length < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let header_bytes = BUFFER_HEADER_WORDS + .checked_mul(mem::size_of::()) + .ok_or(EvalStatus::RuntimeFatal)?; + let payload_bytes = length + .checked_mul(BUFFER_DEFAULT_STRIDE) + .ok_or(EvalStatus::RuntimeFatal)?; + let total_bytes = header_bytes + .checked_add(payload_bytes) + .ok_or(EvalStatus::RuntimeFatal)?; + let allocation = unsafe { libc::calloc(total_bytes.max(1), 1) }; + if allocation.is_null() { + return Err(EvalStatus::RuntimeFatal); + } + unsafe { + let header = allocation.cast::(); + ptr::write(header, length); + ptr::write(header.add(1), BUFFER_DEFAULT_STRIDE); + } + super::ptr::eval_address_value(allocation as usize, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_free.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_free.rs deleted file mode 100644 index 3c982349b3..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_free.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `buffer_free`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Direct calls stay source-sensitive so a local buffer variable can be nulled. - -eval_builtin! { - name: "buffer_free", - area: RawMemory, - params: [buffer], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_len.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_len.rs deleted file mode 100644 index a31392879f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_len.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `buffer_len`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "buffer_len", - area: RawMemory, - params: [buffer], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_new.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_new.rs deleted file mode 100644 index e87f259655..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/buffer_new.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `buffer_new`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "buffer_new", - area: RawMemory, - params: [length], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/mod.rs deleted file mode 100644 index fffb6970ac..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/mod.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries and dispatch adapters for raw-memory builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory` module loading. -//! - `crate::interpreter::builtins::hooks` for migrated raw-memory dispatch. -//! -//! Key details: -//! - Direct calls delegate to the existing source-sensitive helper so -//! `buffer_free($local)` can null the source variable. -//! - Values calls keep the by-value `ptr(...)` unsupported behavior. - -use super::super::super::*; -use super::{eval_builtin_raw_memory, eval_raw_memory_builtin_result}; - -mod buffer_free; -mod buffer_len; -mod buffer_new; -mod ptr; -mod ptr_get; -mod ptr_is_null; -mod ptr_null; -mod ptr_offset; -mod ptr_read16; -mod ptr_read32; -mod ptr_read8; -mod ptr_read_string; -mod ptr_set; -mod ptr_sizeof; -mod ptr_write16; -mod ptr_write32; -mod ptr_write8; -mod ptr_write_string; - -/// Dispatches direct expression-level calls for raw-memory builtins. -pub(in crate::interpreter) fn eval_builtin_raw_memory_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_builtin_raw_memory(name, args, context, scope, values) -} - -/// Dispatches evaluated-argument calls for raw-memory builtins. -pub(in crate::interpreter) fn eval_raw_memory_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_raw_memory_builtin_result(name, evaluated_args, context, values) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr.rs deleted file mode 100644 index 60e0d4ea46..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Eval keeps `ptr(...)` unsupported because by-value cells do not expose raw -//! lvalue storage addresses safely. - -eval_builtin! { - name: "ptr", - area: RawMemory, - params: [value], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_get.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_get.rs deleted file mode 100644 index 8ed7329beb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_get.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_get`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_get", - area: RawMemory, - params: [pointer], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_is_null.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_is_null.rs deleted file mode 100644 index da5952c561..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_is_null.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_is_null`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_is_null", - area: RawMemory, - params: [pointer], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_null.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_null.rs deleted file mode 100644 index b2065b74ca..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_null.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_null`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_null", - area: RawMemory, - params: [], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_offset.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_offset.rs deleted file mode 100644 index aab9bd6856..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_offset.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_offset`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_offset", - area: RawMemory, - params: [pointer, offset], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read16.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read16.rs deleted file mode 100644 index b478b18134..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read16.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_read16`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_read16", - area: RawMemory, - params: [pointer], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read32.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read32.rs deleted file mode 100644 index c720c5b70b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read32.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_read32`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_read32", - area: RawMemory, - params: [pointer], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read8.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read8.rs deleted file mode 100644 index 0ac58f0a1c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read8.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_read8`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_read8", - area: RawMemory, - params: [pointer], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read_string.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read_string.rs deleted file mode 100644 index 6efaa6ccbe..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_read_string.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_read_string`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_read_string", - area: RawMemory, - params: [pointer, length], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_set.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_set.rs deleted file mode 100644 index df3338995e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_set.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_set`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_set", - area: RawMemory, - params: [pointer, value], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_sizeof.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_sizeof.rs deleted file mode 100644 index 92a03b957e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_sizeof.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_sizeof`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_sizeof", - area: RawMemory, - params: [r#type], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write16.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write16.rs deleted file mode 100644 index 95dc034d90..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write16.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_write16`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_write16", - area: RawMemory, - params: [pointer, value], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write32.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write32.rs deleted file mode 100644 index 6d1eaa7d43..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write32.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_write32`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_write32", - area: RawMemory, - params: [pointer, value], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write8.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write8.rs deleted file mode 100644 index f062626f5e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write8.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_write8`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_write8", - area: RawMemory, - params: [pointer, value], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write_string.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write_string.rs deleted file mode 100644 index 211b987042..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/raw_memory/declarations/ptr_write_string.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ptr_write_string`. -//! -//! Called from: -//! - `crate::interpreter::builtins::raw_memory::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the raw-memory helper. - -eval_builtin! { - name: "ptr_write_string", - area: RawMemory, - params: [pointer, string], - direct: RawMemory, - values: RawMemory, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs new file mode 100644 index 0000000000..4a2401ad23 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs @@ -0,0 +1,49 @@ +//! Purpose: +//! Groups eval raw pointer and buffer extension builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins` module loading. +//! +//! Key details: +//! - Leaf files register metadata through `eval_builtin!` and own their +//! PHP-visible direct/by-value wrappers and implementation code. +//! - Helper reuse stays between builtin files with `ptr` owning raw address +//! conversions, `ptr_get` owning read widths, and `ptr_set` owning write widths. + +mod buffer_free; +mod buffer_len; +mod buffer_new; +mod ptr; +mod ptr_get; +mod ptr_is_null; +mod ptr_null; +mod ptr_offset; +mod ptr_read16; +mod ptr_read32; +mod ptr_read8; +mod ptr_read_string; +mod ptr_set; +mod ptr_sizeof; +mod ptr_write16; +mod ptr_write32; +mod ptr_write8; +mod ptr_write_string; + +pub(in crate::interpreter) use buffer_free::*; +pub(in crate::interpreter) use buffer_len::*; +pub(in crate::interpreter) use buffer_new::*; +pub(in crate::interpreter) use ptr::*; +pub(in crate::interpreter) use ptr_get::*; +pub(in crate::interpreter) use ptr_is_null::*; +pub(in crate::interpreter) use ptr_null::*; +pub(in crate::interpreter) use ptr_offset::*; +pub(in crate::interpreter) use ptr_read16::*; +pub(in crate::interpreter) use ptr_read32::*; +pub(in crate::interpreter) use ptr_read8::*; +pub(in crate::interpreter) use ptr_read_string::*; +pub(in crate::interpreter) use ptr_set::*; +pub(in crate::interpreter) use ptr_sizeof::*; +pub(in crate::interpreter) use ptr_write16::*; +pub(in crate::interpreter) use ptr_write32::*; +pub(in crate::interpreter) use ptr_write8::*; +pub(in crate::interpreter) use ptr_write_string::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs new file mode 100644 index 0000000000..bf327c5ef0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Eval registry entry and raw pointer conversion helpers for `ptr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks` and sibling raw-memory builtins. +//! +//! Key details: +//! - Eval keeps `ptr(...)` unsupported because by-value cells do not expose raw +//! lvalue storage addresses safely. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr", + area: RawMemory, + params: [value], + direct: Ptr, + values: Ptr, +} + +/// Evaluates PHP `ptr()` and rejects unsupported eval lvalue-address extraction. +pub(in crate::interpreter) fn eval_builtin_ptr( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + _values: &mut impl RuntimeValueOps, +) -> Result { + let [_value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + Err(EvalStatus::UnsupportedConstruct) +} + +/// Dispatches by-value `ptr()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_values_result( + evaluated_args: &[RuntimeCellHandle], +) -> Result { + let [_value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + Err(EvalStatus::UnsupportedConstruct) +} + +/// Converts a runtime cell to a raw pointer address encoded as a PHP integer. +pub(super) fn eval_pointer_address( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_int_value(value, values)?; + usize::try_from(address).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Converts a runtime cell to a non-null raw pointer. +pub(super) fn eval_non_null_pointer( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<*mut u8, EvalStatus> { + let address = eval_pointer_address(value, values)?; + if address == 0 { + return Err(EvalStatus::RuntimeFatal); + } + Ok(address as *mut u8) +} + +/// Boxes a raw pointer address as a PHP integer cell. +pub(super) fn eval_address_value( + address: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(i64::try_from(address).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs new file mode 100644 index 0000000000..18799f924f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Owns shared pointer read-width handling for read variants. + +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_get", + area: RawMemory, + params: [pointer], + direct: PtrGet, + values: PtrGet, +} + +/// Evaluates PHP `ptr_get()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_get( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_get_result(pointer, values) +} + +/// Dispatches by-value `ptr_get()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_get_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_get_result(*pointer, values) +} + +/// Reads one raw-memory value for `ptr_get()`. +fn eval_ptr_get_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_pointer_read_result(pointer, PointerReadWidth::Word64, values) +} + +/// Reads one unsigned or machine-word value from raw memory. +pub(super) fn eval_pointer_read_result( + pointer: RuntimeCellHandle, + width: PointerReadWidth, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_non_null_pointer(pointer, values)?; + let value = unsafe { + match width { + PointerReadWidth::Byte => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Half => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Word32 => i64::from(ptr::read_unaligned(address.cast::())), + PointerReadWidth::Word64 => { + let word = ptr::read_unaligned(address.cast::()); + i64::from_ne_bytes(word.to_ne_bytes()) + } + } + }; + values.int(value) +} + +/// Widths supported by pointer read helpers. +pub(super) enum PointerReadWidth { + Byte, + Half, + Word32, + Word64, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs new file mode 100644 index 0000000000..06ac1df27e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_is_null`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Checks the integer raw-address representation against zero. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_is_null", + area: RawMemory, + params: [pointer], + direct: PtrIsNull, + values: PtrIsNull, +} + +/// Evaluates PHP `ptr_is_null()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_is_null( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_is_null_result(pointer, values) +} + +/// Dispatches by-value `ptr_is_null()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_is_null_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_is_null_result(*pointer, values) +} + +/// Returns whether one raw pointer address is null. +fn eval_ptr_is_null_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_pointer_address(pointer, values)?; + values.bool_value(address == 0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs new file mode 100644 index 0000000000..960ee967c5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_null`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Null raw pointers are represented as the integer address zero. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_null", + area: RawMemory, + params: [], + direct: PtrNull, + values: PtrNull, +} + +/// Evaluates PHP `ptr_null()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_ptr_null( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_ptr_null_result(values) +} + +/// Dispatches by-value `ptr_null()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_null_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_ptr_null_result(values) +} + +/// Returns the raw null pointer address. +fn eval_ptr_null_result(values: &mut impl RuntimeValueOps) -> Result { + values.int(0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs new file mode 100644 index 0000000000..6fbb9bc453 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_offset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Performs checked signed byte-offset arithmetic on raw pointer addresses. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_offset", + area: RawMemory, + params: [pointer, offset], + direct: PtrOffset, + values: PtrOffset, +} + +/// Evaluates PHP `ptr_offset()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_offset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, offset] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_ptr_offset_result(pointer, offset, values) +} + +/// Dispatches by-value `ptr_offset()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_offset_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, offset] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_offset_result(*pointer, *offset, values) +} + +/// Computes a derived raw pointer address by adding a signed byte offset. +fn eval_ptr_offset_result( + pointer: RuntimeCellHandle, + offset: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_pointer_address(pointer, values)?; + let offset = eval_int_value(offset, values)?; + let address = if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + address.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)? + } else { + let offset = usize::try_from(offset.unsigned_abs()).map_err(|_| EvalStatus::RuntimeFatal)?; + address.checked_sub(offset).ok_or(EvalStatus::RuntimeFatal)? + }; + super::ptr::eval_address_value(address, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs new file mode 100644 index 0000000000..c643ec81b6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_read16`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_get` raw read-width handling for two-byte reads. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_read16", + area: RawMemory, + params: [pointer], + direct: PtrRead16, + values: PtrRead16, +} + +/// Evaluates PHP `ptr_read16()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_read16( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_read16_result(pointer, values) +} + +/// Dispatches by-value `ptr_read16()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_read16_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read16_result(*pointer, values) +} + +/// Reads one raw-memory value for `ptr_read16()`. +fn eval_ptr_read16_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_get::eval_pointer_read_result( + pointer, + super::ptr_get::PointerReadWidth::Half, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs new file mode 100644 index 0000000000..f9379835a8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_read32`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_get` raw read-width handling for four-byte reads. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_read32", + area: RawMemory, + params: [pointer], + direct: PtrRead32, + values: PtrRead32, +} + +/// Evaluates PHP `ptr_read32()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_read32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_read32_result(pointer, values) +} + +/// Dispatches by-value `ptr_read32()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_read32_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read32_result(*pointer, values) +} + +/// Reads one raw-memory value for `ptr_read32()`. +fn eval_ptr_read32_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_get::eval_pointer_read_result( + pointer, + super::ptr_get::PointerReadWidth::Word32, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs new file mode 100644 index 0000000000..05b9c508c6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_read8`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_get` raw read-width handling for one-byte reads. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_read8", + area: RawMemory, + params: [pointer], + direct: PtrRead8, + values: PtrRead8, +} + +/// Evaluates PHP `ptr_read8()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_read8( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + eval_ptr_read8_result(pointer, values) +} + +/// Dispatches by-value `ptr_read8()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_read8_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read8_result(*pointer, values) +} + +/// Reads one raw-memory value for `ptr_read8()`. +fn eval_ptr_read8_result( + pointer: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_get::eval_pointer_read_result( + pointer, + super::ptr_get::PointerReadWidth::Byte, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs new file mode 100644 index 0000000000..ef95f5c7a1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_read_string`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Copies raw memory bytes into a PHP byte string. + +use std::slice; + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_read_string", + area: RawMemory, + params: [pointer, length], + direct: PtrReadString, + values: PtrReadString, +} + +/// Evaluates PHP `ptr_read_string()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_read_string( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_ptr_read_string_result(pointer, length, values) +} + +/// Dispatches by-value `ptr_read_string()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_read_string_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, length] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_read_string_result(*pointer, *length, values) +} + +/// Copies raw memory bytes into a PHP byte string. +fn eval_ptr_read_string_result( + pointer: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_non_null_pointer(pointer, values)?; + let length = eval_int_value(length, values)?; + if length < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = unsafe { slice::from_raw_parts(address.cast::(), length) }; + values.string_bytes_value(bytes) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs new file mode 100644 index 0000000000..58abe2d814 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_set`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Owns shared pointer write-width handling for write variants. + +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_set", + area: RawMemory, + params: [pointer, value], + direct: PtrSet, + values: PtrSet, +} + +/// Evaluates PHP `ptr_set()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_set( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_ptr_set_result(pointer, value, values) +} + +/// Dispatches by-value `ptr_set()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_set_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_set_result(*pointer, *value, values) +} + +/// Writes one raw-memory value for `ptr_set()`. +fn eval_ptr_set_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_pointer_write_result(pointer, value, PointerWriteWidth::Word64, values) +} + +/// Writes one integer payload to raw memory and returns PHP null. +pub(super) fn eval_pointer_write_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + width: PointerWriteWidth, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_non_null_pointer(pointer, values)?; + let value = eval_int_value(value, values)?; + unsafe { + match width { + PointerWriteWidth::Byte => ptr::write_unaligned(address.cast::(), value as u8), + PointerWriteWidth::Half => ptr::write_unaligned(address.cast::(), value as u16), + PointerWriteWidth::Word32 => ptr::write_unaligned(address.cast::(), value as u32), + PointerWriteWidth::Word64 => { + ptr::write_unaligned( + address.cast::(), + u64::from_ne_bytes(value.to_ne_bytes()), + ) + } + } + } + values.null() +} + +/// Widths supported by pointer write helpers. +pub(super) enum PointerWriteWidth { + Byte, + Half, + Word32, + Word64, +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs new file mode 100644 index 0000000000..e81077f482 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_sizeof`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Computes the checked byte size for scalar pointer targets and boxed classes. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_sizeof", + area: RawMemory, + params: [r#type], + direct: PtrSizeof, + values: PtrSizeof, +} + +/// Evaluates PHP `ptr_sizeof()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ptr_sizeof( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [type_name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let type_name = eval_expr(type_name, context, scope, values)?; + eval_ptr_sizeof_result(type_name, context, values) +} + +/// Dispatches by-value `ptr_sizeof()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_sizeof_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [type_name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_sizeof_result(*type_name, context, values) +} + +/// Computes the checked byte size for a low-level type name. +fn eval_ptr_sizeof_result( + type_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(type_name)?; + let type_name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + let size = eval_pointer_target_size(type_name.trim_start_matches('\\'), context) + .ok_or(EvalStatus::RuntimeFatal)?; + values.int(i64::try_from(size).map_err(|_| EvalStatus::RuntimeFatal)?) +} + +/// Returns the eval-side byte size for one low-level pointer target name. +fn eval_pointer_target_size(type_name: &str, context: &ElephcEvalContext) -> Option { + match type_name.to_ascii_lowercase().as_str() { + "int" | "integer" => Some(8), + "float" | "double" | "real" => Some(8), + "bool" | "boolean" => Some(8), + "string" => Some(16), + "ptr" | "pointer" => Some(8), + _ => context.class(type_name).map(eval_boxed_class_size), + } +} + +/// Returns the boxed object storage size used by AOT class metadata. +fn eval_boxed_class_size(class: &EvalClass) -> usize { + let instance_properties = class + .properties() + .iter() + .filter(|property| !property.is_static()) + .count(); + 8 + instance_properties * 16 +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs new file mode 100644 index 0000000000..d3c90d50ab --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_write16`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_set` raw write-width handling for two-byte writes. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_write16", + area: RawMemory, + params: [pointer, value], + direct: PtrWrite16, + values: PtrWrite16, +} + +/// Evaluates PHP `ptr_write16()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_write16( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_ptr_write16_result(pointer, value, values) +} + +/// Dispatches by-value `ptr_write16()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_write16_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write16_result(*pointer, *value, values) +} + +/// Writes one raw-memory value for `ptr_write16()`. +fn eval_ptr_write16_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_set::eval_pointer_write_result( + pointer, + value, + super::ptr_set::PointerWriteWidth::Half, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs new file mode 100644 index 0000000000..83d2ce68eb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_write32`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_set` raw write-width handling for four-byte writes. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_write32", + area: RawMemory, + params: [pointer, value], + direct: PtrWrite32, + values: PtrWrite32, +} + +/// Evaluates PHP `ptr_write32()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_write32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_ptr_write32_result(pointer, value, values) +} + +/// Dispatches by-value `ptr_write32()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_write32_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write32_result(*pointer, *value, values) +} + +/// Writes one raw-memory value for `ptr_write32()`. +fn eval_ptr_write32_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_set::eval_pointer_write_result( + pointer, + value, + super::ptr_set::PointerWriteWidth::Word32, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs new file mode 100644 index 0000000000..b5092e175e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs @@ -0,0 +1,59 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_write8`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Reuses `ptr_set` raw write-width handling for one-byte writes. + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_write8", + area: RawMemory, + params: [pointer, value], + direct: PtrWrite8, + values: PtrWrite8, +} + +/// Evaluates PHP `ptr_write8()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_write8( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_ptr_write8_result(pointer, value, values) +} + +/// Dispatches by-value `ptr_write8()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_write8_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write8_result(*pointer, *value, values) +} + +/// Writes one raw-memory value for `ptr_write8()`. +fn eval_ptr_write8_result( + pointer: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ptr_set::eval_pointer_write_result( + pointer, + value, + super::ptr_set::PointerWriteWidth::Byte, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs new file mode 100644 index 0000000000..caac4d7433 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs @@ -0,0 +1,61 @@ +//! Purpose: +//! Eval registry entry and implementation for `ptr_write_string`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks`. +//! +//! Key details: +//! - Copies PHP string bytes into raw memory and returns the byte count written. + +use std::ptr; + +use super::super::super::*; + + +eval_builtin! { + name: "ptr_write_string", + area: RawMemory, + params: [pointer, string], + direct: PtrWriteString, + values: PtrWriteString, +} + +/// Evaluates PHP `ptr_write_string()` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_ptr_write_string( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, string] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pointer = eval_expr(pointer, context, scope, values)?; + let string = eval_expr(string, context, scope, values)?; + eval_ptr_write_string_result(pointer, string, values) +} + +/// Dispatches by-value `ptr_write_string()` calls after argument binding. +pub(in crate::interpreter) fn eval_ptr_write_string_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [pointer, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ptr_write_string_result(*pointer, *string, values) +} + +/// Copies PHP string bytes into raw memory and returns the byte count written. +fn eval_ptr_write_string_result( + pointer: RuntimeCellHandle, + string: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = super::ptr::eval_non_null_pointer(pointer, values)?; + let bytes = values.string_bytes(string)?; + unsafe { + ptr::copy_nonoverlapping(bytes.as_ptr(), address.cast::(), bytes.len()); + } + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) +} From c6b3a5c440369b8c749ae2af879a8364b98b9826 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 16:17:46 +0200 Subject: [PATCH 1105/1208] refactor(magician): co-locate core builtin implementations --- .../builtins/core/call_user_func.rs | 71 ++++ .../builtins/core/call_user_func_array.rs | 69 ++++ .../core/declarations/call_user_func.rs | 17 - .../core/declarations/call_user_func_array.rs | 16 - .../builtins/core/declarations/define.rs | 16 - .../builtins/core/declarations/defined.rs | 16 - .../builtins/core/declarations/die.rs | 18 - .../builtins/core/declarations/exit.rs | 18 - .../builtins/core/declarations/mod.rs | 68 ---- .../builtins/core/declarations/print_r.rs | 18 - .../builtins/core/declarations/var_dump.rs | 17 - .../{symbols/constants.rs => core/define.rs} | 58 +-- .../src/interpreter/builtins/core/defined.rs | 58 +++ .../src/interpreter/builtins/core/die.rs | 47 +++ .../src/interpreter/builtins/core/exit.rs | 72 ++++ .../src/interpreter/builtins/core/mod.rs | 74 +++- .../core/print_r.rs} | 351 ++---------------- .../src/interpreter/builtins/core/var_dump.rs | 328 ++++++++++++++++ .../src/interpreter/builtins/mod.rs | 2 - .../interpreter/builtins/process_control.rs | 56 --- .../interpreter/builtins/registry/callable.rs | 99 +---- .../src/interpreter/builtins/symbols.rs | 2 - crates/elephc-magician/src/interpreter/mod.rs | 6 +- 23 files changed, 762 insertions(+), 735 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func_array.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/define.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/defined.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/die.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/exit.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/print_r.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/core/declarations/var_dump.rs rename crates/elephc-magician/src/interpreter/builtins/{symbols/constants.rs => core/define.rs} (56%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/defined.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/die.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/exit.rs rename crates/elephc-magician/src/interpreter/{debug_output.rs => builtins/core/print_r.rs} (54%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/process_control.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs b/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs new file mode 100644 index 0000000000..67f08874c1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and wrapper implementation for `call_user_func`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - Callable normalization and invocation stay in `registry::callable` because +//! those helpers are shared by ordinary dynamic calls, arrays, reflection, and +//! `call_user_func_array`. + +use super::super::super::*; +use super::super::registry::eval_call_user_func_with_values_from_scope; + +eval_builtin! { + name: "call_user_func", + area: Core, + params: [callback], + variadic: args, + direct: Core, + values: Core, +} + +/// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. +pub(in crate::interpreter) fn eval_builtin_call_user_func( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let release_callback = eval_call_user_func_callback_expr_is_temporary(&args[0]); + let mut evaluated_args = Vec::with_capacity(args.len()); + for (index, arg) in args.iter().enumerate() { + let value = match eval_expr(arg, context, scope, values) { + Ok(value) => value, + Err(status) => { + if index > 0 && release_callback { + values.release(evaluated_args[0])?; + } + return Err(status); + } + }; + evaluated_args.push(value); + } + let callback = evaluated_args[0]; + let result = + eval_call_user_func_with_values_from_scope(evaluated_args, Some(scope), context, values); + if release_callback { + values.release(callback)?; + } + result +} + +/// Dispatches `call_user_func` after its callback and arguments are already evaluated. +pub(in crate::interpreter) fn eval_call_user_func_with_values( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_call_user_func_with_values_from_scope(evaluated_args, None, context, values) +} + +/// Returns whether a `call_user_func*` callback expression allocates a temporary cell. +pub(in crate::interpreter) fn eval_call_user_func_callback_expr_is_temporary( + callback: &EvalExpr, +) -> bool { + matches!(callback, EvalExpr::Const(_)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs b/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs new file mode 100644 index 0000000000..1b40006a82 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Eval registry entry and wrapper implementation for `call_user_func_array`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - Callable normalization and invocation stay in `registry::callable` because +//! the callable engine is shared beyond this builtin. + +use super::call_user_func::eval_call_user_func_callback_expr_is_temporary; +use super::super::super::*; +use super::super::registry::eval_call_user_func_array_with_values_from_scope; + +eval_builtin! { + name: "call_user_func_array", + area: Core, + params: [callback, args], + direct: Core, + values: Core, +} + +/// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. +pub(in crate::interpreter) fn eval_builtin_call_user_func_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [callback, arg_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let release_callback = eval_call_user_func_callback_expr_is_temporary(callback); + let release_arg_array = matches!(arg_array, EvalExpr::Array(_)); + let callback = eval_expr(callback, context, scope, values)?; + let arg_array = match eval_expr(arg_array, context, scope, values) { + Ok(arg_array) => arg_array, + Err(status) => { + if release_callback { + values.release(callback)?; + } + return Err(status); + } + }; + let result = eval_call_user_func_array_with_values_from_scope( + callback, + arg_array, + Some(scope), + context, + values, + ); + if release_arg_array { + values.release(arg_array)?; + } + if release_callback { + values.release(callback)?; + } + result +} + +/// Dispatches `call_user_func_array` after callback and array arguments are evaluated. +pub(in crate::interpreter) fn eval_call_user_func_array_with_values( + callback: RuntimeCellHandle, + arg_array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_call_user_func_array_with_values_from_scope(callback, arg_array, None, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func.rs deleted file mode 100644 index f5ea6582eb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `call_user_func`. -//! -//! Called from: -//! - `crate::interpreter::builtins::core::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the callable dispatch hook. - -eval_builtin! { - name: "call_user_func", - area: Core, - params: [callback], - variadic: args, - direct: Core, - values: Core, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func_array.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func_array.rs deleted file mode 100644 index 971cd77f10..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/call_user_func_array.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `call_user_func_array`. -//! -//! Called from: -//! - `crate::interpreter::builtins::core::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the callable dispatch hook. - -eval_builtin! { - name: "call_user_func_array", - area: Core, - params: [callback, args], - direct: Core, - values: Core, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/define.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/define.rs deleted file mode 100644 index 67cfa0d938..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/define.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `define`. -//! -//! Called from: -//! - `crate::interpreter::builtins::core::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the dynamic-constant hook. - -eval_builtin! { - name: "define", - area: Core, - params: [constant_name, value], - direct: Core, - values: Core, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/defined.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/defined.rs deleted file mode 100644 index cdbead189f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/defined.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `defined`. -//! -//! Called from: -//! - `crate::interpreter::builtins::core::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the dynamic-constant hook. - -eval_builtin! { - name: "defined", - area: Core, - params: [constant_name], - direct: Core, - values: Core, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/die.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/die.rs deleted file mode 100644 index cfa348a945..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/die.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `die`. -//! -//! Called from: -//! - `crate::interpreter::builtins::core::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process-control hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "die", - area: Core, - params: [status = EvalBuiltinDefaultValue::Int(0)], - direct: Core, - values: Core, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/exit.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/exit.rs deleted file mode 100644 index 781d471360..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/exit.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `exit`. -//! -//! Called from: -//! - `crate::interpreter::builtins::core::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process-control hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "exit", - area: Core, - params: [status = EvalBuiltinDefaultValue::Int(0)], - direct: Core, - values: Core, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs deleted file mode 100644 index 9c9adc93fd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/mod.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries and dispatch adapters for core builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::core` module loading. -//! - `crate::interpreter::builtins::hooks` for migrated core dispatch. -//! -//! Key details: -//! - Adapters preserve the existing direct-call helpers for lexical-scope -//! sensitive callable dispatch and process-control behavior. - -use super::super::super::*; -use super::super::*; - -mod call_user_func; -mod call_user_func_array; -mod define; -mod defined; -mod die; -mod exit; -mod print_r; -mod var_dump; - -/// Dispatches direct expression-level calls for declaratively migrated core builtins. -pub(in crate::interpreter) fn eval_builtin_core_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), - "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), - "define" => eval_builtin_define(args, context, scope, values), - "defined" => eval_builtin_defined(args, context, scope, values), - "die" | "exit" => eval_builtin_exit(args, context, scope, values), - "print_r" => eval_builtin_print_r(args, context, scope, values), - "var_dump" => eval_builtin_var_dump(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated core builtins. -pub(in crate::interpreter) fn eval_core_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "call_user_func" => { - eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) - } - "call_user_func_array" => { - let [callback, arg_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_call_user_func_array_with_values(*callback, *arg_array, context, values) - } - "define" => eval_define_result(evaluated_args, context, values), - "defined" => eval_defined_result(evaluated_args, context, values), - "die" | "exit" => eval_exit_result(evaluated_args, values), - "print_r" => eval_print_r_result(evaluated_args, context, values), - "var_dump" => eval_var_dump_result(evaluated_args, context, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/print_r.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/print_r.rs deleted file mode 100644 index f10282e448..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/print_r.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `print_r`. -//! -//! Called from: -//! - `crate::interpreter::builtins::core::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the debug-output hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "print_r", - area: Core, - params: [value, r#return = EvalBuiltinDefaultValue::Bool(false)], - direct: Core, - values: Core, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/declarations/var_dump.rs b/crates/elephc-magician/src/interpreter/builtins/core/declarations/var_dump.rs deleted file mode 100644 index a9900caff5..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/core/declarations/var_dump.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `var_dump`. -//! -//! Called from: -//! - `crate::interpreter::builtins::core::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the debug-output hook. - -eval_builtin! { - name: "var_dump", - area: Core, - params: [value], - variadic: values, - direct: Core, - values: Core, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/constants.rs b/crates/elephc-magician/src/interpreter/builtins/core/define.rs similarity index 56% rename from crates/elephc-magician/src/interpreter/builtins/symbols/constants.rs rename to crates/elephc-magician/src/interpreter/builtins/core/define.rs index da0d72c833..887a876dea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/constants.rs +++ b/crates/elephc-magician/src/interpreter/builtins/core/define.rs @@ -1,14 +1,22 @@ //! Purpose: -//! Dynamic constant definition and lookup builtins for runtime eval fragments. +//! Eval registry entry and implementation for `define`. //! //! Called from: -//! - `crate::interpreter::builtins::symbols` re-exports. +//! - `crate::interpreter::builtins::core`. //! //! Key details: -//! - Dynamic constants are stored in the eval context and checked after -//! predefined constants using PHP's case-sensitive dynamic constant names. +//! - Dynamic constants are stored in the eval context after predefined-constant +//! checks and keep PHP's case-sensitive dynamic constant names. -use super::*; +use super::super::super::*; + +eval_builtin! { + name: "define", + area: Core, + params: [constant_name, value], + direct: Core, + values: Core, +} /// Evaluates `define(name, value)` for eval dynamic constant-name registration. pub(in crate::interpreter) fn eval_builtin_define( @@ -26,21 +34,6 @@ pub(in crate::interpreter) fn eval_builtin_define( values.bool_value(defined) } -/// Evaluates `defined(name)` against eval dynamic constant names. -pub(in crate::interpreter) fn eval_builtin_defined( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - let exists = eval_defined_name(name, context, values)?; - values.bool_value(exists) -} - /// Evaluates `define(...)` from already materialized call arguments. pub(in crate::interpreter) fn eval_define_result( evaluated_args: &[RuntimeCellHandle], @@ -54,21 +47,8 @@ pub(in crate::interpreter) fn eval_define_result( values.bool_value(defined) } -/// Evaluates `defined(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_defined_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let exists = eval_defined_name(*name, context, values)?; - values.bool_value(exists) -} - /// Normalizes and registers one eval dynamic constant name. -pub(in crate::interpreter) fn eval_define_name( +fn eval_define_name( name: RuntimeCellHandle, value: RuntimeCellHandle, context: &mut ElephcEvalContext, @@ -91,16 +71,6 @@ pub(in crate::interpreter) fn eval_define_name( } } -/// Normalizes and probes one eval dynamic constant name. -pub(in crate::interpreter) fn eval_defined_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_constant_name(name, values)?; - Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) -} - /// Reads a PHP constant name from a runtime cell without changing case. pub(in crate::interpreter) fn eval_constant_name( name: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/core/defined.rs b/crates/elephc-magician/src/interpreter/builtins/core/defined.rs new file mode 100644 index 0000000000..70d8b71944 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/defined.rs @@ -0,0 +1,58 @@ +//! Purpose: +//! Eval registry entry and implementation for `defined`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - Dynamic names use `define`'s constant-name normalizer so the two builtins +//! stay in lockstep. + +use super::define::eval_constant_name; +use super::super::super::*; + +eval_builtin! { + name: "defined", + area: Core, + params: [constant_name], + direct: Core, + values: Core, +} + +/// Evaluates `defined(name)` against eval dynamic constant names. +pub(in crate::interpreter) fn eval_builtin_defined( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + let exists = eval_defined_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `defined(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_defined_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let exists = eval_defined_name(*name, context, values)?; + values.bool_value(exists) +} + +/// Normalizes and probes one eval dynamic constant name. +fn eval_defined_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_constant_name(name, values)?; + Ok(eval_predefined_constant_value(&name).is_some() || context.has_constant(&name)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/die.rs b/crates/elephc-magician/src/interpreter/builtins/core/die.rs new file mode 100644 index 0000000000..cb5bdf60e5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/die.rs @@ -0,0 +1,47 @@ +//! Purpose: +//! Eval registry entry and alias implementation for `die`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - `die` shares `exit`'s process-status coercion and process termination. + +use super::exit::{eval_exit_status_value, eval_process_exit}; +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "die", + area: Core, + params: [status = EvalBuiltinDefaultValue::Int(0)], + direct: Core, + values: Core, +} + +/// Evaluates direct `die` calls from unevaluated EvalIR arguments. +pub(in crate::interpreter) fn eval_builtin_die( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let status = match args { + [] => 0, + [status] => { + let status = eval_expr(status, context, scope, values)?; + eval_int_value(status, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_process_exit(status) +} + +/// Evaluates by-value `die` calls from already materialized arguments. +pub(in crate::interpreter) fn eval_die_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let status = eval_exit_status_value(evaluated_args, values)?; + eval_process_exit(status) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/exit.rs b/crates/elephc-magician/src/interpreter/builtins/core/exit.rs new file mode 100644 index 0000000000..5efb24e7f1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/exit.rs @@ -0,0 +1,72 @@ +//! Purpose: +//! Eval registry entry and implementation for `exit`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core`. +//! +//! Key details: +//! - The helper terminates the host process to match elephc's compiled behavior. + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "exit", + area: Core, + params: [status = EvalBuiltinDefaultValue::Int(0)], + direct: Core, + values: Core, +} + +/// Evaluates direct `exit` calls from unevaluated EvalIR arguments. +pub(in crate::interpreter) fn eval_builtin_exit( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let status = match args { + [] => 0, + [status] => { + let status = eval_expr(status, context, scope, values)?; + eval_int_value(status, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_process_exit(status) +} + +/// Evaluates by-value `exit` calls from already materialized arguments. +pub(in crate::interpreter) fn eval_exit_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let status = eval_exit_status_value(evaluated_args, values)?; + eval_process_exit(status) +} + +/// Reads the optional PHP integer process status for `exit` and `die`. +pub(in crate::interpreter) fn eval_exit_status_value( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => Ok(0), + [status] => eval_int_value(*status, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Terminates the current process with the PHP integer status clamped to `i32`. +pub(in crate::interpreter) fn eval_process_exit( + status: i64, +) -> Result { + let status = i32::try_from(status).unwrap_or_else(|_| { + if status.is_negative() { + i32::MIN + } else { + i32::MAX + } + }); + std::process::exit(status) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/core/mod.rs b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs index 200c668666..6d6f431cae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/core/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/core/mod.rs @@ -1,13 +1,79 @@ //! Purpose: -//! Groups declarative eval metadata for core callable, constant, +//! Orchestrates eval metadata and implementations for core callable, constant, //! process-control, and debug-output builtins. //! //! Called from: //! - `crate::interpreter::builtins` re-exports used by registry hooks. //! //! Key details: -//! - Runtime behavior stays delegated to existing focused interpreter helpers. +//! - Leaf builtin files own their declarations and builtin-specific wrappers. +//! - The callable dispatch engine remains shared because it is used by more than +//! `call_user_func*`. -mod declarations; +use super::super::*; -pub(in crate::interpreter) use declarations::*; +mod call_user_func; +mod call_user_func_array; +mod define; +mod defined; +mod die; +mod exit; +mod print_r; +mod var_dump; + +pub(in crate::interpreter) use call_user_func::*; +pub(in crate::interpreter) use call_user_func_array::*; +pub(in crate::interpreter) use define::*; +pub(in crate::interpreter) use defined::*; +pub(in crate::interpreter) use die::*; +pub(in crate::interpreter) use exit::*; +pub(in crate::interpreter) use print_r::*; +pub(in crate::interpreter) use var_dump::*; + +/// Dispatches direct expression-level calls for core builtins. +pub(in crate::interpreter) fn eval_builtin_core_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "call_user_func" => eval_builtin_call_user_func(args, context, scope, values), + "call_user_func_array" => eval_builtin_call_user_func_array(args, context, scope, values), + "define" => eval_builtin_define(args, context, scope, values), + "defined" => eval_builtin_defined(args, context, scope, values), + "die" => eval_builtin_die(args, context, scope, values), + "exit" => eval_builtin_exit(args, context, scope, values), + "print_r" => eval_builtin_print_r(args, context, scope, values), + "var_dump" => eval_builtin_var_dump(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for core builtins. +pub(in crate::interpreter) fn eval_core_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "call_user_func" => { + eval_call_user_func_with_values(evaluated_args.to_vec(), context, values) + } + "call_user_func_array" => { + let [callback, arg_array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_call_user_func_array_with_values(*callback, *arg_array, context, values) + } + "define" => eval_define_result(evaluated_args, context, values), + "defined" => eval_defined_result(evaluated_args, context, values), + "die" => eval_die_values_result(evaluated_args, values), + "exit" => eval_exit_values_result(evaluated_args, values), + "print_r" => eval_print_r_result(evaluated_args, context, values), + "var_dump" => eval_var_dump_result(evaluated_args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/debug_output.rs b/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs similarity index 54% rename from crates/elephc-magician/src/interpreter/debug_output.rs rename to crates/elephc-magician/src/interpreter/builtins/core/print_r.rs index 636c616972..fda4b6fe0c 100644 --- a/crates/elephc-magician/src/interpreter/debug_output.rs +++ b/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs @@ -1,28 +1,38 @@ //! Purpose: -//! Implements eval-side debug output builtins such as `print_r()` and `var_dump()`. +//! Eval registry entry and implementation for `print_r` plus shared debug-output helpers. //! //! Called from: -//! - `crate::interpreter::eval_positional_expr_call()` for debug-output builtin dispatch. +//! - `crate::interpreter::builtins::core` direct and by-value dispatch. +//! - `crate::interpreter::builtins::core::var_dump` for shared object metadata rendering. //! //! Key details: -//! - Output formatting walks runtime arrays, scalars, object names, and public or -//! eval-declared object properties through `RuntimeValueOps` and eval context metadata. -//! - Eval property aliases are rendered as references when dumping eval-declared objects. //! - `print_r($value, true)` returns captured output instead of echoing it. +//! - Object metadata helpers stay here so related debug builtins can reuse one +//! PHP-visible object traversal model without a generic implementation bucket. use std::collections::HashSet; -use super::*; +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "print_r", + area: Core, + params: [value, r#return = EvalBuiltinDefaultValue::Bool(false)], + direct: Core, + values: Core, +} + /// Property visibility rendered by `var_dump()` and `print_r()` object output. #[derive(Clone)] -struct EvalDebugPropertyVisibility { - kind: EvalDebugPropertyVisibilityKind, +pub(in crate::interpreter) struct EvalDebugPropertyVisibility { + pub(in crate::interpreter) kind: EvalDebugPropertyVisibilityKind, } /// Concrete PHP visibility shape for one object property key. #[derive(Clone)] -enum EvalDebugPropertyVisibilityKind { +pub(in crate::interpreter) enum EvalDebugPropertyVisibilityKind { Public, Protected, Private(String), @@ -30,15 +40,16 @@ enum EvalDebugPropertyVisibilityKind { /// Object property entry collected before rendering object headers. #[derive(Clone)] -struct EvalDebugObjectProperty { - name: String, - visibility: EvalDebugPropertyVisibility, - value: RuntimeCellHandle, - is_reference: bool, +pub(in crate::interpreter) struct EvalDebugObjectProperty { + pub(in crate::interpreter) name: String, + pub(in crate::interpreter) visibility: EvalDebugPropertyVisibility, + pub(in crate::interpreter) value: RuntimeCellHandle, + pub(in crate::interpreter) is_reference: bool, } /// Evaluates PHP `print_r()` over one value and an optional return flag. -pub(super) fn eval_builtin_print_r( + +pub(in crate::interpreter) fn eval_builtin_print_r( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, @@ -102,310 +113,6 @@ fn eval_print_r_value_result( } } -/// Evaluates PHP `var_dump()` over one or more eval expressions and returns null. -pub(super) fn eval_builtin_var_dump( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_var_dump_result(&evaluated_args, context, values) -} - -/// Emits already materialized values using PHP-style `var_dump()` debug formatting. -pub(in crate::interpreter) fn eval_var_dump_result( - values_to_dump: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values_to_dump.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let mut output = Vec::new(); - let mut arrays_seen = Vec::new(); - let mut objects_seen = Vec::new(); - for value in values_to_dump { - eval_var_dump_append_value( - *value, - context, - values, - 0, - false, - &mut arrays_seen, - &mut objects_seen, - &mut output, - )?; - } - let output = values.string_bytes_value(&output)?; - values.echo(output)?; - values.null() -} - -/// Appends one value and its nested entries to a `var_dump()` byte buffer. -fn eval_var_dump_append_value( - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, - depth: usize, - is_reference: bool, - arrays_seen: &mut Vec, - objects_seen: &mut Vec, - output: &mut Vec, -) -> Result<(), EvalStatus> { - match values.type_tag(value)? { - EVAL_TAG_INT => { - eval_var_dump_append_scalar(b"int", value, values, depth, is_reference, output) - } - EVAL_TAG_STRING => eval_var_dump_append_string(value, values, depth, is_reference, output), - EVAL_TAG_FLOAT => { - eval_var_dump_append_scalar(b"float", value, values, depth, is_reference, output) - } - EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, is_reference, output), - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => eval_var_dump_append_array( - value, - context, - values, - depth, - is_reference, - arrays_seen, - objects_seen, - output, - ), - EVAL_TAG_OBJECT => eval_var_dump_append_object( - value, - context, - values, - depth, - is_reference, - arrays_seen, - objects_seen, - output, - ), - EVAL_TAG_NULL => { - eval_var_dump_append_prefix(depth, is_reference, output); - output.extend_from_slice(b"NULL\n"); - Ok(()) - } - EVAL_TAG_RESOURCE => { - eval_var_dump_append_prefix(depth, is_reference, output); - output.extend_from_slice(b"resource(0) of type (stream)\n"); - Ok(()) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Appends one integer-like or float-like `var_dump()` scalar line. -fn eval_var_dump_append_scalar( - label: &[u8], - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - is_reference: bool, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_var_dump_append_prefix(depth, is_reference, output); - output.extend_from_slice(label); - output.extend_from_slice(b"("); - output.extend_from_slice(&values.string_bytes(value)?); - output.extend_from_slice(b")\n"); - Ok(()) -} - -/// Appends one string `var_dump()` line while preserving raw PHP string bytes. -fn eval_var_dump_append_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - is_reference: bool, - output: &mut Vec, -) -> Result<(), EvalStatus> { - let bytes = values.string_bytes(value)?; - eval_var_dump_append_prefix(depth, is_reference, output); - output.extend_from_slice(b"string("); - output.extend_from_slice(bytes.len().to_string().as_bytes()); - output.extend_from_slice(b") \""); - output.extend_from_slice(&bytes); - output.extend_from_slice(b"\"\n"); - Ok(()) -} - -/// Appends one boolean `var_dump()` line from PHP truthiness. -fn eval_var_dump_append_bool( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - is_reference: bool, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_var_dump_append_prefix(depth, is_reference, output); - if values.truthy(value)? { - output.extend_from_slice(b"bool(true)\n"); - } else { - output.extend_from_slice(b"bool(false)\n"); - } - Ok(()) -} - -/// Appends one array shell and recursively emits foreach-visible entries. -fn eval_var_dump_append_array( - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, - depth: usize, - is_reference: bool, - arrays_seen: &mut Vec, - objects_seen: &mut Vec, - output: &mut Vec, -) -> Result<(), EvalStatus> { - let address = value.as_ptr() as usize; - if arrays_seen.contains(&address) { - eval_var_dump_append_prefix(depth, is_reference, output); - output.extend_from_slice(b"*RECURSION*\n"); - return Ok(()); - } - - arrays_seen.push(address); - let len = values.array_len(value)?; - eval_var_dump_append_prefix(depth, is_reference, output); - output.extend_from_slice(b"array("); - output.extend_from_slice(len.to_string().as_bytes()); - output.extend_from_slice(b") {\n"); - for position in 0..len { - let key = values.array_iter_key(value, position)?; - let element = values.array_get(value, key)?; - eval_var_dump_append_array_key(key, values, depth + 1, output)?; - eval_var_dump_append_value( - element, - context, - values, - depth + 1, - false, - arrays_seen, - objects_seen, - output, - )?; - } - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"}\n"); - arrays_seen.pop(); - Ok(()) -} - -/// Appends one object shell and its collected properties. -fn eval_var_dump_append_object( - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, - depth: usize, - is_reference: bool, - arrays_seen: &mut Vec, - objects_seen: &mut Vec, - output: &mut Vec, -) -> Result<(), EvalStatus> { - let identity = eval_debug_object_identity(value, values); - let object_key = identity.unwrap_or(value.as_ptr() as usize as u64) as usize; - if objects_seen.contains(&object_key) { - eval_var_dump_append_prefix(depth, is_reference, output); - output.extend_from_slice(b"*RECURSION*\n"); - return Ok(()); - } - - objects_seen.push(object_key); - let class_name = eval_debug_object_class_name(value, identity, context, values)?; - let properties = eval_debug_object_properties(value, identity, &class_name, context, values)?; - eval_var_dump_append_prefix(depth, is_reference, output); - output.extend_from_slice(b"object("); - output.extend_from_slice(class_name.as_bytes()); - output.extend_from_slice(b")#"); - output.extend_from_slice(object_key.to_string().as_bytes()); - output.extend_from_slice(b" ("); - output.extend_from_slice(properties.len().to_string().as_bytes()); - output.extend_from_slice(b") {\n"); - for property in &properties { - eval_var_dump_append_object_key(property, depth + 1, output); - eval_var_dump_append_value( - property.value, - context, - values, - depth + 1, - property.is_reference, - arrays_seen, - objects_seen, - output, - )?; - } - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"}\n"); - objects_seen.pop(); - Ok(()) -} - -/// Appends one array key line for an indexed or associative `var_dump()` entry. -fn eval_var_dump_append_array_key( - key: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - depth: usize, - output: &mut Vec, -) -> Result<(), EvalStatus> { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"["); - match values.type_tag(key)? { - EVAL_TAG_STRING => { - output.extend_from_slice(b"\""); - output.extend_from_slice(&values.string_bytes(key)?); - output.extend_from_slice(b"\""); - } - _ => output.extend_from_slice(&values.string_bytes(key)?), - } - output.extend_from_slice(b"]=>\n"); - Ok(()) -} - -/// Appends one object property key line for `var_dump()`. -fn eval_var_dump_append_object_key( - property: &EvalDebugObjectProperty, - depth: usize, - output: &mut Vec, -) { - eval_var_dump_append_indent(depth, output); - output.extend_from_slice(b"[\""); - output.extend_from_slice(property.name.as_bytes()); - output.extend_from_slice(b"\""); - match &property.visibility.kind { - EvalDebugPropertyVisibilityKind::Public => {} - EvalDebugPropertyVisibilityKind::Protected => output.extend_from_slice(b":protected"), - EvalDebugPropertyVisibilityKind::Private(class_name) => { - output.extend_from_slice(b":\""); - output.extend_from_slice(class_name.as_bytes()); - output.extend_from_slice(b"\":private"); - } - } - output.extend_from_slice(b"]=>\n"); -} - -/// Appends one `var_dump()` line prefix, including a reference marker when needed. -fn eval_var_dump_append_prefix(depth: usize, is_reference: bool, output: &mut Vec) { - eval_var_dump_append_indent(depth, output); - if is_reference { - output.extend_from_slice(b"&"); - } -} - -/// Appends the two-space indentation used by PHP `var_dump()` arrays and objects. -fn eval_var_dump_append_indent(depth: usize, output: &mut Vec) { - for _ in 0..depth { - output.extend_from_slice(b" "); - } -} - /// Appends one value to a `print_r()` byte buffer. fn eval_print_r_append_value( value: RuntimeCellHandle, @@ -542,7 +249,7 @@ fn eval_print_r_append_indent(depth: usize, output: &mut Vec) { } /// Returns an object identity without turning non-object-like values into fatals. -fn eval_debug_object_identity( +pub(in crate::interpreter) fn eval_debug_object_identity( value: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Option { @@ -550,7 +257,7 @@ fn eval_debug_object_identity( } /// Resolves the PHP-visible class name for one object value. -fn eval_debug_object_class_name( +pub(in crate::interpreter) fn eval_debug_object_class_name( value: RuntimeCellHandle, identity: Option, context: &ElephcEvalContext, @@ -569,7 +276,7 @@ fn eval_debug_object_class_name( } /// Collects object properties visible to debug-output rendering. -fn eval_debug_object_properties( +pub(in crate::interpreter) fn eval_debug_object_properties( object: RuntimeCellHandle, identity: Option, class_name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs b/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs new file mode 100644 index 0000000000..05cbfeeee9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs @@ -0,0 +1,328 @@ +//! Purpose: +//! Eval registry entry and implementation for `var_dump`. +//! +//! Called from: +//! - `crate::interpreter::builtins::core` direct and by-value dispatch. +//! +//! Key details: +//! - The formatter preserves PHP scalar, array, object, reference, and recursion output shape. +//! - Shared debug object metadata traversal is owned by `print_r` and reused here. + +use super::print_r::{ + eval_debug_object_class_name, eval_debug_object_identity, eval_debug_object_properties, + EvalDebugObjectProperty, EvalDebugPropertyVisibilityKind, +}; +use super::super::super::*; + +eval_builtin! { + name: "var_dump", + area: Core, + params: [value], + variadic: values, + direct: Core, + values: Core, +} + +/// Evaluates PHP `var_dump()` over one or more eval expressions and returns null. +pub(in crate::interpreter) fn eval_builtin_var_dump( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_var_dump_result(&evaluated_args, context, values) +} + +/// Emits already materialized values using PHP-style `var_dump()` debug formatting. +pub(in crate::interpreter) fn eval_var_dump_result( + values_to_dump: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values_to_dump.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let mut output = Vec::new(); + let mut arrays_seen = Vec::new(); + let mut objects_seen = Vec::new(); + for value in values_to_dump { + eval_var_dump_append_value( + *value, + context, + values, + 0, + false, + &mut arrays_seen, + &mut objects_seen, + &mut output, + )?; + } + let output = values.string_bytes_value(&output)?; + values.echo(output)?; + values.null() +} + +/// Appends one value and its nested entries to a `var_dump()` byte buffer. +fn eval_var_dump_append_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + match values.type_tag(value)? { + EVAL_TAG_INT => { + eval_var_dump_append_scalar(b"int", value, values, depth, is_reference, output) + } + EVAL_TAG_STRING => eval_var_dump_append_string(value, values, depth, is_reference, output), + EVAL_TAG_FLOAT => { + eval_var_dump_append_scalar(b"float", value, values, depth, is_reference, output) + } + EVAL_TAG_BOOL => eval_var_dump_append_bool(value, values, depth, is_reference, output), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => eval_var_dump_append_array( + value, + context, + values, + depth, + is_reference, + arrays_seen, + objects_seen, + output, + ), + EVAL_TAG_OBJECT => eval_var_dump_append_object( + value, + context, + values, + depth, + is_reference, + arrays_seen, + objects_seen, + output, + ), + EVAL_TAG_NULL => { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"NULL\n"); + Ok(()) + } + EVAL_TAG_RESOURCE => { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"resource(0) of type (stream)\n"); + Ok(()) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends one integer-like or float-like `var_dump()` scalar line. +fn eval_var_dump_append_scalar( + label: &[u8], + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(label); + output.extend_from_slice(b"("); + output.extend_from_slice(&values.string_bytes(value)?); + output.extend_from_slice(b")\n"); + Ok(()) +} + +/// Appends one string `var_dump()` line while preserving raw PHP string bytes. +fn eval_var_dump_append_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let bytes = values.string_bytes(value)?; + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"string("); + output.extend_from_slice(bytes.len().to_string().as_bytes()); + output.extend_from_slice(b") \""); + output.extend_from_slice(&bytes); + output.extend_from_slice(b"\"\n"); + Ok(()) +} + +/// Appends one boolean `var_dump()` line from PHP truthiness. +fn eval_var_dump_append_bool( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_prefix(depth, is_reference, output); + if values.truthy(value)? { + output.extend_from_slice(b"bool(true)\n"); + } else { + output.extend_from_slice(b"bool(false)\n"); + } + Ok(()) +} + +/// Appends one array shell and recursively emits foreach-visible entries. +fn eval_var_dump_append_array( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"*RECURSION*\n"); + return Ok(()); + } + + arrays_seen.push(address); + let len = values.array_len(value)?; + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"array("); + output.extend_from_slice(len.to_string().as_bytes()); + output.extend_from_slice(b") {\n"); + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + eval_var_dump_append_array_key(key, values, depth + 1, output)?; + eval_var_dump_append_value( + element, + context, + values, + depth + 1, + false, + arrays_seen, + objects_seen, + output, + )?; + } + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"}\n"); + arrays_seen.pop(); + Ok(()) +} + +/// Appends one object shell and its collected properties. +fn eval_var_dump_append_object( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + depth: usize, + is_reference: bool, + arrays_seen: &mut Vec, + objects_seen: &mut Vec, + output: &mut Vec, +) -> Result<(), EvalStatus> { + let identity = eval_debug_object_identity(value, values); + let object_key = identity.unwrap_or(value.as_ptr() as usize as u64) as usize; + if objects_seen.contains(&object_key) { + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"*RECURSION*\n"); + return Ok(()); + } + + objects_seen.push(object_key); + let class_name = eval_debug_object_class_name(value, identity, context, values)?; + let properties = eval_debug_object_properties(value, identity, &class_name, context, values)?; + eval_var_dump_append_prefix(depth, is_reference, output); + output.extend_from_slice(b"object("); + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b")#"); + output.extend_from_slice(object_key.to_string().as_bytes()); + output.extend_from_slice(b" ("); + output.extend_from_slice(properties.len().to_string().as_bytes()); + output.extend_from_slice(b") {\n"); + for property in &properties { + eval_var_dump_append_object_key(property, depth + 1, output); + eval_var_dump_append_value( + property.value, + context, + values, + depth + 1, + property.is_reference, + arrays_seen, + objects_seen, + output, + )?; + } + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"}\n"); + objects_seen.pop(); + Ok(()) +} + +/// Appends one array key line for an indexed or associative `var_dump()` entry. +fn eval_var_dump_append_array_key( + key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + depth: usize, + output: &mut Vec, +) -> Result<(), EvalStatus> { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"["); + match values.type_tag(key)? { + EVAL_TAG_STRING => { + output.extend_from_slice(b"\""); + output.extend_from_slice(&values.string_bytes(key)?); + output.extend_from_slice(b"\""); + } + _ => output.extend_from_slice(&values.string_bytes(key)?), + } + output.extend_from_slice(b"]=>\n"); + Ok(()) +} + +/// Appends one object property key line for `var_dump()`. +fn eval_var_dump_append_object_key( + property: &EvalDebugObjectProperty, + depth: usize, + output: &mut Vec, +) { + eval_var_dump_append_indent(depth, output); + output.extend_from_slice(b"[\""); + output.extend_from_slice(property.name.as_bytes()); + output.extend_from_slice(b"\""); + match &property.visibility.kind { + EvalDebugPropertyVisibilityKind::Public => {} + EvalDebugPropertyVisibilityKind::Protected => output.extend_from_slice(b":protected"), + EvalDebugPropertyVisibilityKind::Private(class_name) => { + output.extend_from_slice(b":\""); + output.extend_from_slice(class_name.as_bytes()); + output.extend_from_slice(b"\":private"); + } + } + output.extend_from_slice(b"]=>\n"); +} + +/// Appends one `var_dump()` line prefix, including a reference marker when needed. +fn eval_var_dump_append_prefix(depth: usize, is_reference: bool, output: &mut Vec) { + eval_var_dump_append_indent(depth, output); + if is_reference { + output.extend_from_slice(b"&"); + } +} + +/// Appends the two-space indentation used by PHP `var_dump()` arrays and objects. +fn eval_var_dump_append_indent(depth: usize, output: &mut Vec) { + for _ in 0..depth { + output.extend_from_slice(b" "); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 0476ae3088..5191eaed65 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -24,7 +24,6 @@ mod hooks; mod json; mod math; mod network_env; -mod process_control; mod raw_memory; mod ref_targets; mod regex; @@ -47,7 +46,6 @@ pub(super) use formatting::*; pub(super) use json::*; pub(super) use math::*; pub(super) use network_env::*; -pub(super) use process_control::*; pub(super) use raw_memory::*; pub(super) use ref_targets::*; pub(super) use regex::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/process_control.rs b/crates/elephc-magician/src/interpreter/builtins/process_control.rs deleted file mode 100644 index b02c22d2c3..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/process_control.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Purpose: -//! Implements eval-side process termination language constructs. -//! -//! Called from: -//! - `crate::interpreter::expressions` direct builtin dispatch. -//! - `crate::interpreter::builtins::registry::dispatch` for dynamic callable dispatch. -//! -//! Key details: -//! - `exit` and `die` match elephc's compiled behavior by terminating the host process. -//! - Tests must avoid executing these helpers directly because they do not return. - -use super::super::*; -use super::*; - -/// Evaluates direct `exit` or `die` calls from unevaluated EvalIR arguments. -pub(in crate::interpreter) fn eval_builtin_exit( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let status = match args { - [] => 0, - [status] => { - let status = eval_expr(status, context, scope, values)?; - eval_int_value(status, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_process_exit(status) -} - -/// Evaluates dynamic `exit` or `die` calls from already materialized arguments. -pub(in crate::interpreter) fn eval_exit_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let status = match evaluated_args { - [] => 0, - [status] => eval_int_value(*status, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_process_exit(status) -} - -/// Terminates the current process with the PHP integer status clamped to `i32`. -fn eval_process_exit(status: i64) -> Result { - let status = i32::try_from(status).unwrap_or_else(|_| { - if status.is_negative() { - i32::MIN - } else { - i32::MAX - } - }); - std::process::exit(status) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 384c553014..c95a457285 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -13,94 +13,8 @@ use super::*; -/// Evaluates `call_user_func($name, ...$args)` inside a runtime eval fragment. -pub(in crate::interpreter) fn eval_builtin_call_user_func( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let release_callback = eval_call_user_func_callback_expr_is_temporary(&args[0]); - let mut evaluated_args = Vec::with_capacity(args.len()); - for (index, arg) in args.iter().enumerate() { - let value = match eval_expr(arg, context, scope, values) { - Ok(value) => value, - Err(status) => { - if index > 0 && release_callback { - values.release(evaluated_args[0])?; - } - return Err(status); - } - }; - evaluated_args.push(value); - } - let callback = evaluated_args[0]; - let result = - eval_call_user_func_with_values_from_scope(evaluated_args, Some(scope), context, values); - if release_callback { - values.release(callback)?; - } - result -} - -/// Evaluates `call_user_func_array($name, $args)` inside a runtime eval fragment. -pub(in crate::interpreter) fn eval_builtin_call_user_func_array( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [callback, arg_array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let release_callback = eval_call_user_func_callback_expr_is_temporary(callback); - let release_arg_array = matches!(arg_array, EvalExpr::Array(_)); - let callback = eval_expr(callback, context, scope, values)?; - let arg_array = match eval_expr(arg_array, context, scope, values) { - Ok(arg_array) => arg_array, - Err(status) => { - if release_callback { - values.release(callback)?; - } - return Err(status); - } - }; - let result = eval_call_user_func_array_with_values_from_scope( - callback, - arg_array, - Some(scope), - context, - values, - ); - if release_arg_array { - values.release(arg_array)?; - } - if release_callback { - values.release(callback)?; - } - result -} - -/// Returns whether a `call_user_func*` callback expression allocates a temporary cell. -fn eval_call_user_func_callback_expr_is_temporary(callback: &EvalExpr) -> bool { - matches!(callback, EvalExpr::Const(_)) -} - -/// Dispatches `call_user_func_array` after callback and array arguments are evaluated. -pub(in crate::interpreter) fn eval_call_user_func_array_with_values( - callback: RuntimeCellHandle, - arg_array: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_call_user_func_array_with_values_from_scope(callback, arg_array, None, context, values) -} - /// Dispatches `call_user_func_array` with optional lexical scope for special class receivers. -fn eval_call_user_func_array_with_values_from_scope( +pub(in crate::interpreter) fn eval_call_user_func_array_with_values_from_scope( callback: RuntimeCellHandle, arg_array: RuntimeCellHandle, lexical_scope: Option<&ElephcEvalScope>, @@ -121,17 +35,8 @@ fn eval_call_user_func_array_with_values_from_scope( eval_evaluated_callable_with_call_array_args(&callback, evaluated_args, context, values) } -/// Dispatches `call_user_func` after its callback and arguments are already evaluated. -pub(in crate::interpreter) fn eval_call_user_func_with_values( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_call_user_func_with_values_from_scope(evaluated_args, None, context, values) -} - /// Dispatches `call_user_func` with optional lexical scope for special class receivers. -fn eval_call_user_func_with_values_from_scope( +pub(in crate::interpreter) fn eval_call_user_func_with_values_from_scope( evaluated_args: Vec, lexical_scope: Option<&ElephcEvalScope>, context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 11061da876..19f2707022 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -13,7 +13,6 @@ use super::super::*; mod callable_probe; mod class_names; mod class_relations; -mod constants; mod declarations; mod function_probe; mod language_constructs; @@ -21,7 +20,6 @@ mod language_constructs; pub(in crate::interpreter) use callable_probe::*; pub(in crate::interpreter) use class_names::*; pub(in crate::interpreter) use class_relations::*; -pub(in crate::interpreter) use constants::*; pub(in crate::interpreter) use declarations::{ eval_builtin_symbols_call, eval_symbols_values_result, }; diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index ba28e1ed09..3be93e59d7 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -19,7 +19,6 @@ mod constant_eval; mod constants; mod control; mod core_builtins; -mod debug_output; mod dynamic_functions; mod expressions; mod include_exec; @@ -57,12 +56,11 @@ use constant_eval::*; use constants::*; pub use control::EvalOutcome; use control::{ - BoundMethodArg, BoundNativeFunctionArgs, BoundNativeFunctionRefSlot, EvalByRefBindingMode, - EvalArraySpliceDirectArgs, EvalControl, EvalPredefinedConstant, EvalSprintfSpec, + BoundMethodArg, BoundNativeFunctionArgs, BoundNativeFunctionRefSlot, EvalArraySpliceDirectArgs, + EvalByRefBindingMode, EvalControl, EvalPredefinedConstant, EvalSprintfSpec, EvaluatedCallArg, EvaluatedCallable, }; use core_builtins::*; -use debug_output::*; use dynamic_functions::*; use expressions::*; use include_exec::*; From f548438db176d799d2c555623b46ba1c6b074160 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 16:29:02 +0200 Subject: [PATCH 1106/1208] refactor(magician): co-locate time builtin implementations --- .../src/interpreter/builtins/time/aliases.rs | 1 - .../src/interpreter/builtins/time/calendar.rs | 274 ------------------ .../interpreter/builtins/time/checkdate.rs | 68 +++++ .../src/interpreter/builtins/time/date.rs | 33 ++- .../time/date_default_timezone_get.rs | 38 +++ .../time/date_default_timezone_set.rs | 44 +++ .../builtins/time/declarations/checkdate.rs | 16 - .../builtins/time/declarations/date.rs | 18 -- .../declarations/date_default_timezone_get.rs | 16 - .../declarations/date_default_timezone_set.rs | 16 - .../builtins/time/declarations/getdate.rs | 18 -- .../builtins/time/declarations/gmdate.rs | 18 -- .../builtins/time/declarations/gmmktime.rs | 16 - .../builtins/time/declarations/header.rs | 22 -- .../builtins/time/declarations/hrtime.rs | 18 -- .../time/declarations/http_response_code.rs | 18 -- .../builtins/time/declarations/localtime.rs | 21 -- .../builtins/time/declarations/microtime.rs | 18 -- .../builtins/time/declarations/mktime.rs | 16 - .../builtins/time/declarations/mod.rs | 167 ----------- .../builtins/time/declarations/sleep.rs | 16 - .../builtins/time/declarations/strtotime.rs | 18 -- .../builtins/time/declarations/time.rs | 16 - .../builtins/time/declarations/usleep.rs | 16 - .../src/interpreter/builtins/time/getdate.rs | 118 ++++++++ .../src/interpreter/builtins/time/gmdate.rs | 40 +++ .../src/interpreter/builtins/time/gmmktime.rs | 42 +++ .../src/interpreter/builtins/time/header.rs | 71 +++++ .../src/interpreter/builtins/time/hrtime.rs | 71 +++++ .../builtins/time/http_response_code.rs | 51 ++++ .../interpreter/builtins/time/localtime.rs | 88 ++++++ .../builtins/time/{clock.rs => microtime.rs} | 38 +-- .../src/interpreter/builtins/time/mktime.rs | 25 +- .../src/interpreter/builtins/time/mod.rs | 187 +++++++++++- .../src/interpreter/builtins/time/sleep.rs | 40 +-- .../interpreter/builtins/time/strtotime.rs | 19 +- .../src/interpreter/builtins/time/system.rs | 127 +------- .../src/interpreter/builtins/time/time.rs | 45 +++ .../src/interpreter/builtins/time/usleep.rs | 44 +++ 39 files changed, 981 insertions(+), 947 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/calendar.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/checkdate.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/date.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_get.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_set.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/getdate.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/gmdate.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/gmmktime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/header.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/hrtime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/http_response_code.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/localtime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/microtime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/mktime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/sleep.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/strtotime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/time.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/time/declarations/usleep.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/getdate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/header.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/localtime.rs rename crates/elephc-magician/src/interpreter/builtins/time/{clock.rs => microtime.rs} (50%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/time.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/time/usleep.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs index 9d3031c1a0..cd794182c3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/aliases.rs @@ -13,7 +13,6 @@ //! procedural date/time aliases; splitting by alias would obscure the shared //! fallback and timezone-table rules. -use super::super::super::*; use super::*; #[path = "../../../../../../src/list_id_prelude/table.rs"] diff --git a/crates/elephc-magician/src/interpreter/builtins/time/calendar.rs b/crates/elephc-magician/src/interpreter/builtins/time/calendar.rs deleted file mode 100644 index a36005ef6c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/calendar.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! Purpose: -//! Implements calendar decomposition helpers such as `checkdate()`, `getdate()`, -//! `localtime()`, and `hrtime()` for eval builtin execution. -//! -//! Called from: -//! - `crate::interpreter::builtins::time` re-exports. -//! -//! Key details: -//! - Local calendar decomposition uses the eval context timezone, which defaults -//! to UTC to match elephc's native runtime initialization. - -use super::super::super::*; -use super::super::*; -use super::*; - -const EVAL_LOCALTIME_KEYS: &[&str; 9] = &[ - "tm_sec", "tm_min", "tm_hour", "tm_mday", "tm_mon", "tm_year", "tm_wday", "tm_yday", - "tm_isdst", -]; - -/// Evaluates PHP `checkdate(month, day, year)` over three eval expressions. -pub(in crate::interpreter) fn eval_builtin_checkdate( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [month, day, year] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let month = eval_expr(month, context, scope, values)?; - let day = eval_expr(day, context, scope, values)?; - let year = eval_expr(year, context, scope, values)?; - eval_checkdate_result(month, day, year, values) -} - -/// Returns whether the supplied month/day/year tuple is a valid Gregorian date. -pub(in crate::interpreter) fn eval_checkdate_result( - month: RuntimeCellHandle, - day: RuntimeCellHandle, - year: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let month = eval_int_value(month, values)?; - let day = eval_int_value(day, values)?; - let year = eval_int_value(year, values)?; - values.bool_value(eval_checkdate_parts(month, day, year)) -} - -/// Tests PHP `checkdate()` bounds and leap-year behavior for integer components. -fn eval_checkdate_parts(month: i64, day: i64, year: i64) -> bool { - if !(1..=12).contains(&month) || !(1..=32767).contains(&year) { - return false; - } - let days = match month { - 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, - 4 | 6 | 9 | 11 => 30, - 2 if eval_is_leap_year(year) => 29, - 2 => 28, - _ => return false, - }; - (1..=days).contains(&day) -} - -/// Returns whether one Gregorian year is a leap year. -fn eval_is_leap_year(year: i64) -> bool { - (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 -} - -/// Evaluates PHP `getdate($timestamp = null)`. -pub(in crate::interpreter) fn eval_builtin_getdate( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_getdate_result(None, context, values), - [timestamp] => { - let timestamp = eval_expr(timestamp, context, scope, values)?; - eval_getdate_result(Some(timestamp), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds PHP's `getdate()` associative array for one optional timestamp. -pub(in crate::interpreter) fn eval_getdate_result( - timestamp: Option, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let timestamp = eval_optional_timestamp(timestamp, values)?; - let tm = eval_context_localtime(timestamp, context)?; - let mut result = values.assoc_new(11)?; - result = eval_array_set_string_int(result, "seconds", i64::from(tm.tm_sec), values)?; - result = eval_array_set_string_int(result, "minutes", i64::from(tm.tm_min), values)?; - result = eval_array_set_string_int(result, "hours", i64::from(tm.tm_hour), values)?; - result = eval_array_set_string_int(result, "mday", i64::from(tm.tm_mday), values)?; - result = eval_array_set_string_int(result, "wday", i64::from(tm.tm_wday), values)?; - result = eval_array_set_string_int(result, "mon", i64::from(tm.tm_mon + 1), values)?; - result = eval_array_set_string_int(result, "year", i64::from(tm.tm_year + 1900), values)?; - result = eval_array_set_string_int(result, "yday", i64::from(tm.tm_yday), values)?; - result = eval_array_set_string_str( - result, - "weekday", - EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(&tm)?], - values, - )?; - result = eval_array_set_string_str( - result, - "month", - EVAL_MONTH_NAMES[eval_tm_month_index(&tm)?], - values, - )?; - eval_array_set_int_int(result, 0, timestamp, values) -} - -/// Evaluates PHP `localtime($timestamp = null, $associative = false)`. -pub(in crate::interpreter) fn eval_builtin_localtime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_localtime_result(None, None, context, values), - [timestamp] => { - let timestamp = eval_expr(timestamp, context, scope, values)?; - eval_localtime_result(Some(timestamp), None, context, values) - } - [timestamp, associative] => { - let timestamp = eval_expr(timestamp, context, scope, values)?; - let associative = eval_expr(associative, context, scope, values)?; - eval_localtime_result(Some(timestamp), Some(associative), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds PHP's `localtime()` array for one optional timestamp and key mode. -pub(in crate::interpreter) fn eval_localtime_result( - timestamp: Option, - associative: Option, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let timestamp = eval_optional_timestamp(timestamp, values)?; - let associative = associative - .map(|value| values.truthy(value)) - .transpose()? - .unwrap_or(false); - let tm = eval_context_localtime(timestamp, context)?; - let fields = [ - tm.tm_sec, - tm.tm_min, - tm.tm_hour, - tm.tm_mday, - tm.tm_mon, - tm.tm_year, - tm.tm_wday, - tm.tm_yday, - tm.tm_isdst, - ]; - if associative { - let mut result = values.assoc_new(fields.len())?; - for (key, value) in EVAL_LOCALTIME_KEYS.iter().zip(fields) { - result = eval_array_set_string_int(result, key, i64::from(value), values)?; - } - return Ok(result); - } - let mut result = values.array_new(fields.len())?; - for (index, value) in fields.into_iter().enumerate() { - result = eval_array_set_int_int(result, index as i64, i64::from(value), values)?; - } - Ok(result) -} - -/// Evaluates PHP `hrtime($as_number = false)`. -pub(in crate::interpreter) fn eval_builtin_hrtime( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_hrtime_result(None, values), - [as_number] => { - let as_number = eval_expr(as_number, context, scope, values)?; - eval_hrtime_result(Some(as_number), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns monotonic time as either nanoseconds or `[seconds, nanoseconds]`. -pub(in crate::interpreter) fn eval_hrtime_result( - as_number: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let (seconds, nanoseconds) = eval_monotonic_time()?; - let as_number = as_number - .map(|value| values.truthy(value)) - .transpose()? - .unwrap_or(false); - if as_number { - let total = seconds - .checked_mul(1_000_000_000) - .and_then(|value| value.checked_add(nanoseconds)) - .ok_or(EvalStatus::RuntimeFatal)?; - return values.int(total); - } - let mut result = values.array_new(2)?; - result = eval_array_set_int_int(result, 0, seconds, values)?; - eval_array_set_int_int(result, 1, nanoseconds, values) -} - -/// Reads the monotonic clock in whole seconds and nanoseconds. -fn eval_monotonic_time() -> Result<(i64, i64), EvalStatus> { - let mut timespec = MaybeUninit::::uninit(); - let status = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, timespec.as_mut_ptr()) }; - if status != 0 { - return Err(EvalStatus::RuntimeFatal); - } - let timespec = unsafe { timespec.assume_init() }; - Ok((timespec.tv_sec, timespec.tv_nsec)) -} - -/// Coerces an optional timestamp argument, treating null/omitted as the current time. -fn eval_optional_timestamp( - timestamp: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - match timestamp { - Some(timestamp) if !values.is_null(timestamp)? => eval_int_value(timestamp, values), - _ => eval_current_unix_timestamp(), - } -} - -/// Writes one string-keyed integer entry into a PHP array. -fn eval_array_set_string_int( - array: RuntimeCellHandle, - key: &str, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Writes one string-keyed string entry into a PHP array. -fn eval_array_set_string_str( - array: RuntimeCellHandle, - key: &str, - value: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.string(value)?; - values.array_set(array, key, value) -} - -/// Writes one integer-keyed integer entry into a PHP array. -fn eval_array_set_int_int( - array: RuntimeCellHandle, - key: i64, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(key)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs new file mode 100644 index 0000000000..105115aad4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs @@ -0,0 +1,68 @@ +//! Purpose: +//! Eval registry entry and implementation for `checkdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Gregorian bounds and leap-year validation are owned by this builtin file. + +use super::super::*; +use super::*; + +eval_builtin! { + name: "checkdate", + area: Time, + params: [month, day, year], + direct: Time, + values: Time, +} + +/// Evaluates PHP `checkdate(month, day, year)` over three eval expressions. +pub(in crate::interpreter) fn eval_builtin_checkdate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [month, day, year] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let month = eval_expr(month, context, scope, values)?; + let day = eval_expr(day, context, scope, values)?; + let year = eval_expr(year, context, scope, values)?; + eval_checkdate_result(month, day, year, values) +} + +/// Returns whether the supplied month/day/year tuple is a valid Gregorian date. +pub(in crate::interpreter) fn eval_checkdate_result( + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let month = eval_int_value(month, values)?; + let day = eval_int_value(day, values)?; + let year = eval_int_value(year, values)?; + values.bool_value(eval_checkdate_parts(month, day, year)) +} + +/// Tests PHP `checkdate()` bounds and leap-year behavior for integer components. +fn eval_checkdate_parts(month: i64, day: i64, year: i64) -> bool { + if !(1..=12).contains(&month) || !(1..=32767).contains(&year) { + return false; + } + let days = match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if eval_is_leap_year(year) => 29, + 2 => 28, + _ => return false, + }; + (1..=days).contains(&day) +} + +/// Returns whether one Gregorian year is a leap year. +fn eval_is_leap_year(year: i64) -> bool { + (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/date.rs b/crates/elephc-magician/src/interpreter/builtins/time/date.rs index b098c107f5..7adcce14de 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/date.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/date.rs @@ -1,19 +1,28 @@ //! Purpose: -//! Implements PHP `date()` formatting for the eval-supported token subset. +//! Eval registry entry and implementation for `date` plus shared date-format helpers. //! //! Called from: -//! - `crate::interpreter::builtins::time` re-exports. +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. //! //! Key details: -//! - Unix timestamps are converted through libc `localtime_r` before PHP date -//! tokens are expanded. +//! - `gmdate` calls this file for shared formatting and UTC/local timestamp conversion. -use super::super::super::*; -use super::super::*; -use super::*; use std::os::unix::ffi::OsStrExt; use std::sync::Mutex; +use super::super::*; +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "date", + area: Time, + params: [format, timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} + static EVAL_TZ_MUTEX: Mutex<()> = Mutex::new(()); unsafe extern "C" { @@ -21,6 +30,16 @@ unsafe extern "C" { fn tzset(); } +/// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. +pub(in crate::interpreter) fn eval_builtin_date( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_date_like("date", args, context, scope, values) +} + /// Evaluates PHP `date($format, $timestamp = time())` for the eval subset. pub(in crate::interpreter) fn eval_builtin_date_like( name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs b/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs new file mode 100644 index 0000000000..7d557aee06 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Eval registry entry and implementation for `date_default_timezone_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - The result reads the eval-local default timezone from the context. + +use super::super::super::*; + +eval_builtin! { + name: "date_default_timezone_get", + area: Time, + params: [], + direct: Time, + values: Time, +} + +/// Evaluates PHP `date_default_timezone_get()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_date_default_timezone_get( + args: &[EvalExpr], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_date_default_timezone_get_result(context, values) +} + +/// Returns the eval-local default timezone identifier. +pub(in crate::interpreter) fn eval_date_default_timezone_get_result( + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(context.default_timezone()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs b/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs new file mode 100644 index 0000000000..0b3c7f38ad --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `date_default_timezone_set`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - The timezone identifier is stored on the eval context. + +use super::super::super::*; + +eval_builtin! { + name: "date_default_timezone_set", + area: Time, + params: [timezoneId], + direct: Time, + values: Time, +} + +/// Evaluates PHP `date_default_timezone_set($timezoneId)`. +pub(in crate::interpreter) fn eval_builtin_date_default_timezone_set( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [timezone] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let timezone = eval_expr(timezone, context, scope, values)?; + eval_date_default_timezone_set_result(timezone, context, values) +} + +/// Stores one eval-local default timezone identifier and reports success. +pub(in crate::interpreter) fn eval_date_default_timezone_set_result( + timezone: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timezone = values.string_bytes(timezone)?; + let timezone = String::from_utf8_lossy(&timezone).into_owned(); + context.set_default_timezone(timezone); + values.bool_value(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/checkdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/checkdate.rs deleted file mode 100644 index 4068c996fd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/checkdate.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `checkdate`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the calendar hook. - -eval_builtin! { - name: "checkdate", - area: Time, - params: [month, day, year], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date.rs deleted file mode 100644 index d17a5546d8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `date`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the date-format hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "date", - area: Time, - params: [format, timestamp = EvalBuiltinDefaultValue::Null], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_get.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_get.rs deleted file mode 100644 index 9648e6dd68..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_get.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `date_default_timezone_get`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to eval context timezone state. - -eval_builtin! { - name: "date_default_timezone_get", - area: Time, - params: [], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_set.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_set.rs deleted file mode 100644 index 2cc1f4bbaf..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/date_default_timezone_set.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `date_default_timezone_set`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to eval context timezone state. - -eval_builtin! { - name: "date_default_timezone_set", - area: Time, - params: [timezoneId], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/getdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/getdate.rs deleted file mode 100644 index 1657921038..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/getdate.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `getdate`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the local calendar hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "getdate", - area: Time, - params: [timestamp = EvalBuiltinDefaultValue::Null], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmdate.rs deleted file mode 100644 index f520305a69..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmdate.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gmdate`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the UTC date-format hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "gmdate", - area: Time, - params: [format, timestamp = EvalBuiltinDefaultValue::Null], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmmktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmmktime.rs deleted file mode 100644 index c10d760ced..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/gmmktime.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gmmktime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the UTC mktime hook. - -eval_builtin! { - name: "gmmktime", - area: Time, - params: [hour, minute, second, month, day, year], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/header.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/header.rs deleted file mode 100644 index ae57b7f301..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/header.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `header`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the time/system hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "header", - area: Time, - params: [ - header, - replace = EvalBuiltinDefaultValue::Bool(true), - response_code = EvalBuiltinDefaultValue::Int(0), - ], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/hrtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/hrtime.rs deleted file mode 100644 index d59f229bb2..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/hrtime.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hrtime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the high-resolution clock hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "hrtime", - area: Time, - params: [as_number = EvalBuiltinDefaultValue::Bool(false)], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/http_response_code.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/http_response_code.rs deleted file mode 100644 index 55d6ec694d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/http_response_code.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `http_response_code`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the time/system hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "http_response_code", - area: Time, - params: [response_code = EvalBuiltinDefaultValue::Int(0)], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/localtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/localtime.rs deleted file mode 100644 index 179d6359dd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/localtime.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `localtime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the local calendar hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "localtime", - area: Time, - params: [ - timestamp = EvalBuiltinDefaultValue::Null, - associative = EvalBuiltinDefaultValue::Bool(false), - ], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/microtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/microtime.rs deleted file mode 100644 index 39e2a72267..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/microtime.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `microtime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the wall-clock hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "microtime", - area: Time, - params: [as_float = EvalBuiltinDefaultValue::Bool(false)], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/mktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/mktime.rs deleted file mode 100644 index 292665a565..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/mktime.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `mktime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the local mktime hook. - -eval_builtin! { - name: "mktime", - area: Time, - params: [hour, minute, second, month, day, year], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs deleted file mode 100644 index c45180ba61..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/mod.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries and dispatch adapters for time and -//! response-header builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::time` module loading. -//! - `crate::interpreter::builtins::hooks` for migrated time dispatch. -//! -//! Key details: -//! - Time/date and response-state algorithms remain in sibling helper modules -//! such as `date`, `calendar`, `clock`, `mktime`, `sleep`, `strtotime`, and -//! `system`. -//! - This module owns registry metadata and small hook adapters only. - -use super::super::super::*; -use super::*; - -mod checkdate; -mod date; -mod date_default_timezone_get; -mod date_default_timezone_set; -mod getdate; -mod gmdate; -mod gmmktime; -mod header; -mod hrtime; -mod http_response_code; -mod localtime; -mod microtime; -mod mktime; -mod sleep; -mod strtotime; -mod time; -mod usleep; - -/// Dispatches direct expression-level calls for declaratively migrated time builtins. -pub(in crate::interpreter) fn eval_builtin_time_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "checkdate" => eval_builtin_checkdate(args, context, scope, values), - "date" | "gmdate" => eval_builtin_date_like(name, args, context, scope, values), - "date_default_timezone_get" => eval_builtin_date_default_timezone_get(args, context, values), - "date_default_timezone_set" => { - eval_builtin_date_default_timezone_set(args, context, scope, values) - } - "getdate" => eval_builtin_getdate(args, context, scope, values), - "gmmktime" | "mktime" => eval_builtin_mktime_like(name, args, context, scope, values), - "header" => eval_builtin_header(args, context, scope, values), - "hrtime" => eval_builtin_hrtime(args, context, scope, values), - "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), - "localtime" => eval_builtin_localtime(args, context, scope, values), - "microtime" => eval_builtin_microtime(args, context, scope, values), - "sleep" => eval_builtin_sleep(args, context, scope, values), - "strtotime" => eval_builtin_strtotime(args, context, scope, values), - "time" => eval_builtin_time(args, values), - "usleep" => eval_builtin_usleep(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated time builtins. -pub(in crate::interpreter) fn eval_time_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "checkdate" => { - let [month, day, year] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_checkdate_result(*month, *day, *year, values) - } - "date" | "gmdate" => match evaluated_args { - [format] => eval_date_result(name, *format, None, context, values), - [format, timestamp] => eval_date_result(name, *format, Some(*timestamp), context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "date_default_timezone_get" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_date_default_timezone_get_result(context, values) - } - "date_default_timezone_set" => { - let [timezone] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_date_default_timezone_set_result(*timezone, context, values) - } - "getdate" => match evaluated_args { - [] => eval_getdate_result(None, context, values), - [timestamp] => eval_getdate_result(Some(*timestamp), context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "gmmktime" | "mktime" => { - let [hour, minute, second, month, day, year] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_mktime_result( - name, *hour, *minute, *second, *month, *day, *year, context, values, - ) - } - "header" => match evaluated_args { - [line] => eval_header_result(*line, None, None, context, values), - [line, replace] => eval_header_result(*line, Some(*replace), None, context, values), - [line, replace, response_code] => { - eval_header_result(*line, Some(*replace), Some(*response_code), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "hrtime" => match evaluated_args { - [] => eval_hrtime_result(None, values), - [as_number] => eval_hrtime_result(Some(*as_number), values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "http_response_code" => match evaluated_args { - [] => eval_http_response_code_result(None, context, values), - [response_code] => eval_http_response_code_result(Some(*response_code), context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "localtime" => match evaluated_args { - [] => eval_localtime_result(None, None, context, values), - [timestamp] => eval_localtime_result(Some(*timestamp), None, context, values), - [timestamp, associative] => { - eval_localtime_result(Some(*timestamp), Some(*associative), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "microtime" => match evaluated_args { - [] | [_] => eval_microtime_result(values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "sleep" => { - let [seconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_sleep_result(*seconds, values) - } - "strtotime" => match evaluated_args { - [datetime] => eval_strtotime_result(*datetime, None, context, values), - [datetime, base_timestamp] => { - eval_strtotime_result(*datetime, Some(*base_timestamp), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "time" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values) - } - "usleep" => { - let [microseconds] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_usleep_result(*microseconds, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/sleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/sleep.rs deleted file mode 100644 index fe124c34f8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/sleep.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `sleep`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the sleep hook. - -eval_builtin! { - name: "sleep", - area: Time, - params: [seconds], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/strtotime.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/strtotime.rs deleted file mode 100644 index 309bf2ee88..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/strtotime.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `strtotime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the date parser hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "strtotime", - area: Time, - params: [datetime, baseTimestamp = EvalBuiltinDefaultValue::Null], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/time.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/time.rs deleted file mode 100644 index 84e74f1318..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/time.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `time`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the wall-clock hook. - -eval_builtin! { - name: "time", - area: Time, - params: [], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/declarations/usleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/declarations/usleep.rs deleted file mode 100644 index 35965c8e3f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/time/declarations/usleep.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `usleep`. -//! -//! Called from: -//! - `crate::interpreter::builtins::time::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the microsecond sleep hook. - -eval_builtin! { - name: "usleep", - area: Time, - params: [microseconds], - direct: Time, - values: Time, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs new file mode 100644 index 0000000000..cd37524365 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Eval registry entry and implementation for `getdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - This file owns optional timestamp coercion and array-entry helpers reused by `localtime`. + +use super::super::*; +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "getdate", + area: Time, + params: [timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} + +/// Evaluates PHP `getdate($timestamp = null)`. +pub(in crate::interpreter) fn eval_builtin_getdate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_getdate_result(None, context, values), + [timestamp] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_getdate_result(Some(timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds PHP's `getdate()` associative array for one optional timestamp. +pub(in crate::interpreter) fn eval_getdate_result( + timestamp: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_optional_timestamp(timestamp, values)?; + let tm = eval_context_localtime(timestamp, context)?; + let mut result = values.assoc_new(11)?; + result = eval_array_set_string_int(result, "seconds", i64::from(tm.tm_sec), values)?; + result = eval_array_set_string_int(result, "minutes", i64::from(tm.tm_min), values)?; + result = eval_array_set_string_int(result, "hours", i64::from(tm.tm_hour), values)?; + result = eval_array_set_string_int(result, "mday", i64::from(tm.tm_mday), values)?; + result = eval_array_set_string_int(result, "wday", i64::from(tm.tm_wday), values)?; + result = eval_array_set_string_int(result, "mon", i64::from(tm.tm_mon + 1), values)?; + result = eval_array_set_string_int(result, "year", i64::from(tm.tm_year + 1900), values)?; + result = eval_array_set_string_int(result, "yday", i64::from(tm.tm_yday), values)?; + result = eval_array_set_string_str( + result, + "weekday", + EVAL_WEEKDAY_NAMES[eval_tm_weekday_index(&tm)?], + values, + )?; + result = eval_array_set_string_str( + result, + "month", + EVAL_MONTH_NAMES[eval_tm_month_index(&tm)?], + values, + )?; + eval_array_set_int_int(result, 0, timestamp, values) +} + + +/// Coerces an optional timestamp argument, treating null/omitted as the current time. +pub(in crate::interpreter) fn eval_optional_timestamp( + timestamp: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + match timestamp { + Some(timestamp) if !values.is_null(timestamp)? => eval_int_value(timestamp, values), + _ => eval_current_unix_timestamp(), + } +} + +/// Writes one string-keyed integer entry into a PHP array. +pub(in crate::interpreter) fn eval_array_set_string_int( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Writes one string-keyed string entry into a PHP array. +pub(in crate::interpreter) fn eval_array_set_string_str( + array: RuntimeCellHandle, + key: &str, + value: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string(value)?; + values.array_set(array, key, value) +} + +/// Writes one integer-keyed integer entry into a PHP array. +pub(in crate::interpreter) fn eval_array_set_int_int( + array: RuntimeCellHandle, + key: i64, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs b/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs new file mode 100644 index 0000000000..87d45745a0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `gmdate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - UTC formatting delegates to the shared formatter owned by `date`. + +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gmdate", + area: Time, + params: [format, timestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} + +/// Evaluates PHP `gmdate($format, $timestamp = time())` for the eval subset. +pub(in crate::interpreter) fn eval_builtin_gmdate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_date_like("gmdate", args, context, scope, values) +} + +/// Formats one UTC timestamp through the shared `date` formatter. +pub(in crate::interpreter) fn eval_gmdate_result( + format: RuntimeCellHandle, + timestamp: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_date_result("gmdate", format, timestamp, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs new file mode 100644 index 0000000000..2f3654de50 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `gmmktime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - UTC timestamp construction delegates to the shared mktime helpers. + +use super::*; + +eval_builtin! { + name: "gmmktime", + area: Time, + params: [hour, minute, second, month, day, year], + direct: Time, + values: Time, +} + +/// Evaluates PHP `gmmktime(hour, minute, second, month, day, year)`. +pub(in crate::interpreter) fn eval_builtin_gmmktime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_mktime_like("gmmktime", args, context, scope, values) +} + +/// Converts PHP date components to a UTC Unix timestamp through libc `timegm`. +pub(in crate::interpreter) fn eval_gmmktime_result( + hour: RuntimeCellHandle, + minute: RuntimeCellHandle, + second: RuntimeCellHandle, + month: RuntimeCellHandle, + day: RuntimeCellHandle, + year: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_mktime_result("gmmktime", hour, minute, second, month, day, year, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/header.rs b/crates/elephc-magician/src/interpreter/builtins/time/header.rs new file mode 100644 index 0000000000..85da2c7cb8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/header.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and implementation for `header`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Only eval-local side effects observable without a web bridge are modeled. + +use super::super::super::*; +use super::super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "header", + area: Time, + params: [ + header, + replace = EvalBuiltinDefaultValue::Bool(true), + response_code = EvalBuiltinDefaultValue::Int(0), + ], + direct: Time, + values: Time, +} + +/// Evaluates PHP `header($header, $replace = true, $response_code = 0)`. +pub(in crate::interpreter) fn eval_builtin_header( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [line] => { + let line = eval_expr(line, context, scope, values)?; + eval_header_result(line, None, None, context, values) + } + [line, replace] => { + let line = eval_expr(line, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + eval_header_result(line, Some(replace), None, context, values) + } + [line, replace, response_code] => { + let line = eval_expr(line, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let response_code = eval_expr(response_code, context, scope, values)?; + eval_header_result(line, Some(replace), Some(response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies eval-local `header()` side effects that are observable without a web bridge. +pub(in crate::interpreter) fn eval_header_result( + line: RuntimeCellHandle, + replace: Option, + response_code: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.string_bytes(line)?; + if let Some(replace) = replace { + let _ = values.truthy(replace)?; + } + if let Some(response_code) = response_code { + let response_code = eval_int_value(response_code, values)?; + let _ = context.replace_http_response_code(response_code); + } + values.null() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs new file mode 100644 index 0000000000..87f4747462 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Eval registry entry and implementation for `hrtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Monotonic time is returned as nanoseconds or `[seconds, nanoseconds]`. + +use super::super::super::*; +use super::super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hrtime", + area: Time, + params: [as_number = EvalBuiltinDefaultValue::Bool(false)], + direct: Time, + values: Time, +} + +/// Evaluates PHP `hrtime($as_number = false)`. +pub(in crate::interpreter) fn eval_builtin_hrtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_hrtime_result(None, values), + [as_number] => { + let as_number = eval_expr(as_number, context, scope, values)?; + eval_hrtime_result(Some(as_number), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns monotonic time as either nanoseconds or `[seconds, nanoseconds]`. +pub(in crate::interpreter) fn eval_hrtime_result( + as_number: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let (seconds, nanoseconds) = eval_monotonic_time()?; + let as_number = as_number + .map(|value| values.truthy(value)) + .transpose()? + .unwrap_or(false); + if as_number { + let total = seconds + .checked_mul(1_000_000_000) + .and_then(|value| value.checked_add(nanoseconds)) + .ok_or(EvalStatus::RuntimeFatal)?; + return values.int(total); + } + let mut result = values.array_new(2)?; + result = eval_array_set_int_int(result, 0, seconds, values)?; + eval_array_set_int_int(result, 1, nanoseconds, values) +} + +/// Reads the monotonic clock in whole seconds and nanoseconds. +fn eval_monotonic_time() -> Result<(i64, i64), EvalStatus> { + let mut timespec = MaybeUninit::::uninit(); + let status = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, timespec.as_mut_ptr()) }; + if status != 0 { + return Err(EvalStatus::RuntimeFatal); + } + let timespec = unsafe { timespec.assume_init() }; + Ok((timespec.tv_sec, timespec.tv_nsec)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs b/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs new file mode 100644 index 0000000000..81c9000bba --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Eval registry entry and implementation for `http_response_code`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Response-code state is eval-local and observable by later calls. + +use super::super::super::*; +use super::super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "http_response_code", + area: Time, + params: [response_code = EvalBuiltinDefaultValue::Int(0)], + direct: Time, + values: Time, +} + +/// Evaluates PHP `http_response_code($response_code = 0)`. +pub(in crate::interpreter) fn eval_builtin_http_response_code( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_http_response_code_result(None, context, values), + [response_code] => { + let response_code = eval_expr(response_code, context, scope, values)?; + eval_http_response_code_result(Some(response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Reads or updates the eval-local HTTP response code. +pub(in crate::interpreter) fn eval_http_response_code_result( + response_code: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = match response_code { + Some(response_code) => context.replace_http_response_code(eval_int_value(response_code, values)?), + None => context.http_response_code(), + }; + values.int(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs b/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs new file mode 100644 index 0000000000..0a364e3004 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs @@ -0,0 +1,88 @@ +//! Purpose: +//! Eval registry entry and implementation for `localtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - `getdate` owns the shared timestamp coercion and array-entry helpers. + +use super::*; + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "localtime", + area: Time, + params: [ + timestamp = EvalBuiltinDefaultValue::Null, + associative = EvalBuiltinDefaultValue::Bool(false), + ], + direct: Time, + values: Time, +} + +const EVAL_LOCALTIME_KEYS: &[&str; 9] = &[ + "tm_sec", "tm_min", "tm_hour", "tm_mday", "tm_mon", "tm_year", "tm_wday", "tm_yday", + "tm_isdst", +]; + +/// Evaluates PHP `localtime($timestamp = null, $associative = false)`. +pub(in crate::interpreter) fn eval_builtin_localtime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_localtime_result(None, None, context, values), + [timestamp] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + eval_localtime_result(Some(timestamp), None, context, values) + } + [timestamp, associative] => { + let timestamp = eval_expr(timestamp, context, scope, values)?; + let associative = eval_expr(associative, context, scope, values)?; + eval_localtime_result(Some(timestamp), Some(associative), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds PHP's `localtime()` array for one optional timestamp and key mode. +pub(in crate::interpreter) fn eval_localtime_result( + timestamp: Option, + associative: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let timestamp = eval_optional_timestamp(timestamp, values)?; + let associative = associative + .map(|value| values.truthy(value)) + .transpose()? + .unwrap_or(false); + let tm = eval_context_localtime(timestamp, context)?; + let fields = [ + tm.tm_sec, + tm.tm_min, + tm.tm_hour, + tm.tm_mday, + tm.tm_mon, + tm.tm_year, + tm.tm_wday, + tm.tm_yday, + tm.tm_isdst, + ]; + if associative { + let mut result = values.assoc_new(fields.len())?; + for (key, value) in EVAL_LOCALTIME_KEYS.iter().zip(fields) { + result = eval_array_set_string_int(result, key, i64::from(value), values)?; + } + return Ok(result); + } + let mut result = values.array_new(fields.len())?; + for (index, value) in fields.into_iter().enumerate() { + result = eval_array_set_int_int(result, index as i64, i64::from(value), values)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/clock.rs b/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs similarity index 50% rename from crates/elephc-magician/src/interpreter/builtins/time/clock.rs rename to crates/elephc-magician/src/interpreter/builtins/time/microtime.rs index 9888181f86..06d13a50a9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/clock.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs @@ -1,40 +1,22 @@ //! Purpose: -//! Implements `time()` and `microtime()` eval builtins. +//! Eval registry entry and implementation for `microtime`. //! //! Called from: -//! - `crate::interpreter::builtins::time` re-exports. +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. //! //! Key details: -//! - Current timestamps are read from `SystemTime::now()` and converted into PHP -//! integer or float runtime cells. +//! - The optional argument is accepted for PHP arity parity but does not alter the result. use super::super::super::*; -/// Evaluates PHP `time()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_time( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_time_result(values) -} - -/// Returns the current Unix timestamp as a boxed PHP integer. -pub(in crate::interpreter) fn eval_time_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.int(eval_current_unix_timestamp()?) -} +use super::super::spec::EvalBuiltinDefaultValue; -/// Returns the current Unix timestamp as an integer payload. -pub(in crate::interpreter) fn eval_current_unix_timestamp() -> Result { - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| EvalStatus::RuntimeFatal)? - .as_secs(); - i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +eval_builtin! { + name: "microtime", + area: Time, + params: [as_float = EvalBuiltinDefaultValue::Bool(false)], + direct: Time, + values: Time, } /// Evaluates PHP `microtime()` with an optional ignored argument. diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs index 47c39a8696..c99473ac0a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs @@ -1,16 +1,33 @@ //! Purpose: -//! Implements PHP `mktime()` conversion from local date components. +//! Eval registry entry and implementation for `mktime` plus shared mktime helpers. //! //! Called from: -//! - `crate::interpreter::builtins::time` re-exports. +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. //! //! Key details: -//! - Component coercion checks libc integer bounds before calling `mktime`. +//! - `gmmktime` and `strtotime` reuse the timestamp conversion helpers from this file. -use super::super::super::*; use super::super::*; use super::*; +eval_builtin! { + name: "mktime", + area: Time, + params: [hour, minute, second, month, day, year], + direct: Time, + values: Time, +} + +/// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. +pub(in crate::interpreter) fn eval_builtin_mktime( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_mktime_like("mktime", args, context, scope, values) +} + /// Evaluates PHP `mktime(hour, minute, second, month, day, year)`. pub(in crate::interpreter) fn eval_builtin_mktime_like( name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs index 09f06f347a..ada5187493 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs @@ -1,30 +1,197 @@ //! Purpose: -//! Groups eval implementations for PHP time, date, sleep, version, and uname -//! related builtins. +//! Orchestrates eval implementations for PHP time, date, sleep, and response +//! header builtins. //! //! Called from: //! - `crate::interpreter::builtins` re-exports used by core call dispatch. //! //! Key details: -//! - Time conversion helpers stay scoped to the eval interpreter and use libc for -//! local calendar conversions where PHP behavior depends on host locale/timezone. +//! - Leaf builtin files own their registry declarations and builtin-specific wrappers. +//! - Shared calendar/date helpers live in the most specific builtin file that owns +//! the underlying behavior, such as `date`, `getdate`, `mktime`, or `time`. + +use super::super::*; -mod calendar; mod aliases; -mod clock; +mod checkdate; mod date; -mod declarations; +mod date_default_timezone_get; +mod date_default_timezone_set; +mod getdate; +mod gmdate; +mod gmmktime; +mod header; +mod hrtime; +mod http_response_code; +mod localtime; +mod microtime; mod mktime; mod sleep; mod strtotime; mod system; +mod time; +mod usleep; pub(in crate::interpreter) use aliases::*; -pub(in crate::interpreter) use calendar::*; -pub(in crate::interpreter) use clock::*; +pub(in crate::interpreter) use checkdate::*; pub(in crate::interpreter) use date::*; -pub(in crate::interpreter) use declarations::*; +pub(in crate::interpreter) use date_default_timezone_get::*; +pub(in crate::interpreter) use date_default_timezone_set::*; +pub(in crate::interpreter) use getdate::*; +pub(in crate::interpreter) use gmdate::*; +pub(in crate::interpreter) use gmmktime::*; +pub(in crate::interpreter) use header::*; +pub(in crate::interpreter) use hrtime::*; +pub(in crate::interpreter) use http_response_code::*; +pub(in crate::interpreter) use localtime::*; +pub(in crate::interpreter) use microtime::*; pub(in crate::interpreter) use mktime::*; pub(in crate::interpreter) use sleep::*; pub(in crate::interpreter) use strtotime::*; pub(in crate::interpreter) use system::*; +pub(in crate::interpreter) use time::*; +pub(in crate::interpreter) use usleep::*; + +/// Dispatches direct expression-level calls for time builtins. +pub(in crate::interpreter) fn eval_builtin_time_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "checkdate" => eval_builtin_checkdate(args, context, scope, values), + "date" => eval_builtin_date(args, context, scope, values), + "gmdate" => eval_builtin_gmdate(args, context, scope, values), + "date_default_timezone_get" => eval_builtin_date_default_timezone_get(args, context, values), + "date_default_timezone_set" => { + eval_builtin_date_default_timezone_set(args, context, scope, values) + } + "getdate" => eval_builtin_getdate(args, context, scope, values), + "gmmktime" => eval_builtin_gmmktime(args, context, scope, values), + "mktime" => eval_builtin_mktime(args, context, scope, values), + "header" => eval_builtin_header(args, context, scope, values), + "hrtime" => eval_builtin_hrtime(args, context, scope, values), + "http_response_code" => eval_builtin_http_response_code(args, context, scope, values), + "localtime" => eval_builtin_localtime(args, context, scope, values), + "microtime" => eval_builtin_microtime(args, context, scope, values), + "sleep" => eval_builtin_sleep(args, context, scope, values), + "strtotime" => eval_builtin_strtotime(args, context, scope, values), + "time" => eval_builtin_time(args, values), + "usleep" => eval_builtin_usleep(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for time builtins. +pub(in crate::interpreter) fn eval_time_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "checkdate" => { + let [month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_checkdate_result(*month, *day, *year, values) + } + "date" => match evaluated_args { + [format] => eval_date_result("date", *format, None, context, values), + [format, timestamp] => eval_date_result("date", *format, Some(*timestamp), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "gmdate" => match evaluated_args { + [format] => eval_gmdate_result(*format, None, context, values), + [format, timestamp] => eval_gmdate_result(*format, Some(*timestamp), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "date_default_timezone_get" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_date_default_timezone_get_result(context, values) + } + "date_default_timezone_set" => { + let [timezone] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_date_default_timezone_set_result(*timezone, context, values) + } + "getdate" => match evaluated_args { + [] => eval_getdate_result(None, context, values), + [timestamp] => eval_getdate_result(Some(*timestamp), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "gmmktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gmmktime_result(*hour, *minute, *second, *month, *day, *year, context, values) + } + "mktime" => { + let [hour, minute, second, month, day, year] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_mktime_result("mktime", *hour, *minute, *second, *month, *day, *year, context, values) + } + "header" => match evaluated_args { + [line] => eval_header_result(*line, None, None, context, values), + [line, replace] => eval_header_result(*line, Some(*replace), None, context, values), + [line, replace, response_code] => { + eval_header_result(*line, Some(*replace), Some(*response_code), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "hrtime" => match evaluated_args { + [] => eval_hrtime_result(None, values), + [as_number] => eval_hrtime_result(Some(*as_number), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "http_response_code" => match evaluated_args { + [] => eval_http_response_code_result(None, context, values), + [response_code] => eval_http_response_code_result(Some(*response_code), context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "localtime" => match evaluated_args { + [] => eval_localtime_result(None, None, context, values), + [timestamp] => eval_localtime_result(Some(*timestamp), None, context, values), + [timestamp, associative] => { + eval_localtime_result(Some(*timestamp), Some(*associative), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "microtime" => match evaluated_args { + [] | [_] => eval_microtime_result(values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "sleep" => { + let [seconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_sleep_result(*seconds, values) + } + "strtotime" => match evaluated_args { + [datetime] => eval_strtotime_result(*datetime, None, context, values), + [datetime, base_timestamp] => { + eval_strtotime_result(*datetime, Some(*base_timestamp), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + "time" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) + } + "usleep" => { + let [microseconds] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_usleep_result(*microseconds, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs index 9e83253e0b..6009bec5a6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs @@ -1,16 +1,23 @@ //! Purpose: -//! Implements PHP `sleep()` and `usleep()` eval builtins. +//! Eval registry entry and implementation for `sleep`. //! //! Called from: -//! - `crate::interpreter::builtins::time` re-exports. +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. //! //! Key details: -//! - Negative durations are rejected as runtime fatals; successful sleeps return -//! PHP-compatible values. +//! - Negative durations are rejected as runtime fatals. use super::super::super::*; use super::super::*; +eval_builtin! { + name: "sleep", + area: Time, + params: [seconds], + direct: Time, + values: Time, +} + /// Evaluates PHP `sleep($seconds)` over one eval expression. pub(in crate::interpreter) fn eval_builtin_sleep( args: &[EvalExpr], @@ -35,28 +42,3 @@ pub(in crate::interpreter) fn eval_sleep_result( std::thread::sleep(std::time::Duration::from_secs(seconds)); values.int(0) } - -/// Evaluates PHP `usleep($microseconds)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_usleep( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [microseconds] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let microseconds = eval_expr(microseconds, context, scope, values)?; - eval_usleep_result(microseconds, values) -} - -/// Sleeps for a non-negative number of microseconds and returns PHP null. -pub(in crate::interpreter) fn eval_usleep_result( - microseconds: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let microseconds = eval_int_value(microseconds, values)?; - let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; - std::thread::sleep(std::time::Duration::from_micros(microseconds)); - values.null() -} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs index db87cdf172..f0876234c9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs @@ -1,16 +1,25 @@ //! Purpose: -//! Implements the eval-supported `strtotime()` date-string subset. +//! Eval registry entry and implementation for `strtotime`. //! //! Called from: -//! - `crate::interpreter::builtins::time` re-exports. +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. //! //! Key details: -//! - Supported strings are fixed-width ISO date/datetime forms normalized through -//! the same local `mktime` path as explicit date components. +//! - The supported parser subset normalizes fixed-width ISO dates through `mktime`. -use super::super::super::*; +use super::super::*; use super::*; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "strtotime", + area: Time, + params: [datetime, baseTimestamp = EvalBuiltinDefaultValue::Null], + direct: Time, + values: Time, +} + /// Evaluates PHP `strtotime(datetime, baseTimestamp = null)` for eval's supported subset. pub(in crate::interpreter) fn eval_builtin_strtotime( args: &[EvalExpr], diff --git a/crates/elephc-magician/src/interpreter/builtins/time/system.rs b/crates/elephc-magician/src/interpreter/builtins/time/system.rs index 3531950a95..d2e481a615 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/system.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/system.rs @@ -1,9 +1,8 @@ //! Purpose: -//! Implements version and uname-related system builtins currently grouped with -//! time/system eval support. +//! Implements shared system information helpers still used by network/env builtins. //! //! Called from: -//! - `crate::interpreter::builtins::time` re-exports. +//! - `crate::interpreter::builtins::network_env` until those builtins are co-located. //! //! Key details: //! - `phpversion()` reads the workspace package version at compile time and @@ -11,128 +10,6 @@ use super::super::super::*; -/// Evaluates PHP `date_default_timezone_get()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_date_default_timezone_get( - args: &[EvalExpr], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_date_default_timezone_get_result(context, values) -} - -/// Returns the eval-local default timezone identifier. -pub(in crate::interpreter) fn eval_date_default_timezone_get_result( - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - values.string(context.default_timezone()) -} - -/// Evaluates PHP `date_default_timezone_set($timezoneId)`. -pub(in crate::interpreter) fn eval_builtin_date_default_timezone_set( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [timezone] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let timezone = eval_expr(timezone, context, scope, values)?; - eval_date_default_timezone_set_result(timezone, context, values) -} - -/// Stores one eval-local default timezone identifier and reports success. -pub(in crate::interpreter) fn eval_date_default_timezone_set_result( - timezone: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let timezone = values.string_bytes(timezone)?; - let timezone = String::from_utf8_lossy(&timezone).into_owned(); - context.set_default_timezone(timezone); - values.bool_value(true) -} - -/// Evaluates PHP `http_response_code($response_code = 0)`. -pub(in crate::interpreter) fn eval_builtin_http_response_code( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_http_response_code_result(None, context, values), - [response_code] => { - let response_code = eval_expr(response_code, context, scope, values)?; - eval_http_response_code_result(Some(response_code), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Reads or updates the eval-local HTTP response code. -pub(in crate::interpreter) fn eval_http_response_code_result( - response_code: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let result = match response_code { - Some(response_code) => context.replace_http_response_code(eval_int_value(response_code, values)?), - None => context.http_response_code(), - }; - values.int(result) -} - -/// Evaluates PHP `header($header, $replace = true, $response_code = 0)`. -pub(in crate::interpreter) fn eval_builtin_header( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [line] => { - let line = eval_expr(line, context, scope, values)?; - eval_header_result(line, None, None, context, values) - } - [line, replace] => { - let line = eval_expr(line, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - eval_header_result(line, Some(replace), None, context, values) - } - [line, replace, response_code] => { - let line = eval_expr(line, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let response_code = eval_expr(response_code, context, scope, values)?; - eval_header_result(line, Some(replace), Some(response_code), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Applies eval-local `header()` side effects that are observable without a web bridge. -pub(in crate::interpreter) fn eval_header_result( - line: RuntimeCellHandle, - replace: Option, - response_code: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let _ = values.string_bytes(line)?; - if let Some(replace) = replace { - let _ = values.truthy(replace)?; - } - if let Some(response_code) = response_code { - let response_code = eval_int_value(response_code, values)?; - let _ = context.replace_http_response_code(response_code); - } - values.null() -} - /// Evaluates PHP `phpversion()` with no arguments. pub(in crate::interpreter) fn eval_builtin_phpversion( args: &[EvalExpr], diff --git a/crates/elephc-magician/src/interpreter/builtins/time/time.rs b/crates/elephc-magician/src/interpreter/builtins/time/time.rs new file mode 100644 index 0000000000..8744baf8eb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/time.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Eval registry entry and implementation for `time`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - The current Unix timestamp helper is reused by date parsing and aliases. + +use super::super::super::*; + +eval_builtin! { + name: "time", + area: Time, + params: [], + direct: Time, + values: Time, +} + +/// Evaluates PHP `time()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_time( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_time_result(values) +} + +/// Returns the current Unix timestamp as a boxed PHP integer. +pub(in crate::interpreter) fn eval_time_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(eval_current_unix_timestamp()?) +} + +/// Returns the current Unix timestamp as an integer payload. +pub(in crate::interpreter) fn eval_current_unix_timestamp() -> Result { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|_| EvalStatus::RuntimeFatal)? + .as_secs(); + i64::try_from(timestamp).map_err(|_| EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs b/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs new file mode 100644 index 0000000000..5d6a0e004e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `usleep`. +//! +//! Called from: +//! - `crate::interpreter::builtins::time` direct and by-value dispatch. +//! +//! Key details: +//! - Negative durations are rejected as runtime fatals and success returns PHP null. + +use super::super::super::*; +use super::super::*; + +eval_builtin! { + name: "usleep", + area: Time, + params: [microseconds], + direct: Time, + values: Time, +} + +/// Evaluates PHP `usleep($microseconds)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_usleep( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [microseconds] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let microseconds = eval_expr(microseconds, context, scope, values)?; + eval_usleep_result(microseconds, values) +} + +/// Sleeps for a non-negative number of microseconds and returns PHP null. +pub(in crate::interpreter) fn eval_usleep_result( + microseconds: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let microseconds = eval_int_value(microseconds, values)?; + let microseconds = u64::try_from(microseconds).map_err(|_| EvalStatus::RuntimeFatal)?; + std::thread::sleep(std::time::Duration::from_micros(microseconds)); + values.null() +} From fd6510574b4c66423c6e3bf16ca7aeaccf2d0ceb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 16:35:36 +0200 Subject: [PATCH 1107/1208] refactor(magician): co-locate network env builtins --- .../builtins/network_env/declarations/exec.rs | 16 -- .../network_env/declarations/getenv.rs | 16 -- .../network_env/declarations/gethostbyaddr.rs | 16 -- .../network_env/declarations/gethostbyname.rs | 16 -- .../network_env/declarations/gethostname.rs | 16 -- .../declarations/getprotobyname.rs | 16 -- .../declarations/getprotobynumber.rs | 16 -- .../network_env/declarations/getservbyname.rs | 16 -- .../network_env/declarations/getservbyport.rs | 16 -- .../network_env/declarations/inet_ntop.rs | 16 -- .../network_env/declarations/inet_pton.rs | 16 -- .../network_env/declarations/ip2long.rs | 16 -- .../network_env/declarations/long2ip.rs | 16 -- .../builtins/network_env/declarations/mod.rs | 171 -------------- .../network_env/declarations/passthru.rs | 16 -- .../network_env/declarations/php_uname.rs | 18 -- .../network_env/declarations/phpversion.rs | 16 -- .../network_env/declarations/putenv.rs | 16 -- .../network_env/declarations/shell_exec.rs | 16 -- .../network_env/declarations/system.rs | 16 -- .../network_env/{process.rs => exec.rs} | 38 ++- .../builtins/network_env/getenv.rs | 45 ++++ .../builtins/network_env/gethostbyaddr.rs | 63 +++++ .../builtins/network_env/gethostbyname.rs | 56 +++++ .../builtins/network_env/gethostname.rs | 52 +++++ .../builtins/network_env/getprotobyname.rs | 83 +++++++ .../builtins/network_env/getprotobynumber.rs | 48 ++++ .../builtins/network_env/getservbyname.rs | 75 ++++++ .../builtins/network_env/getservbyport.rs | 54 +++++ .../interpreter/builtins/network_env/hosts.rs | 128 ----------- .../builtins/network_env/inet_ntop.rs | 45 ++++ .../builtins/network_env/inet_pton.rs | 44 ++++ .../interpreter/builtins/network_env/ip.rs | 158 ------------- .../builtins/network_env/ip2long.rs | 82 +++++++ .../builtins/network_env/long2ip.rs | 48 ++++ .../interpreter/builtins/network_env/mod.rs | 217 ++++++++++++++++-- .../builtins/network_env/passthru.rs | 36 +++ .../system.rs => network_env/php_uname.rs} | 52 +---- .../builtins/network_env/phpversion.rs | 57 +++++ .../builtins/network_env/protocols.rs | 198 ---------------- .../network_env/{env.rs => putenv.rs} | 40 +--- .../builtins/network_env/shell_exec.rs | 36 +++ .../builtins/network_env/system.rs | 36 +++ .../src/interpreter/builtins/time/mod.rs | 2 - 44 files changed, 1112 insertions(+), 1058 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/exec.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getenv.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyaddr.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyname.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostname.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobyname.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobynumber.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyname.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyport.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_ntop.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_pton.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/ip2long.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/long2ip.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/passthru.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/php_uname.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/phpversion.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/putenv.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/shell_exec.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/declarations/system.rs rename crates/elephc-magician/src/interpreter/builtins/network_env/{process.rs => exec.rs} (69%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/hosts.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/ip.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs rename crates/elephc-magician/src/interpreter/builtins/{time/system.rs => network_env/php_uname.rs} (65%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/protocols.rs rename crates/elephc-magician/src/interpreter/builtins/network_env/{env.rs => putenv.rs} (51%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/system.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/exec.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/exec.rs deleted file mode 100644 index 1fed80ea9f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/exec.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `exec`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process-command hook. - -eval_builtin! { - name: "exec", - area: NetworkEnv, - params: [command], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getenv.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getenv.rs deleted file mode 100644 index a04236fc41..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getenv.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `getenv`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the environment hook. - -eval_builtin! { - name: "getenv", - area: NetworkEnv, - params: [name], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyaddr.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyaddr.rs deleted file mode 100644 index bfb4cfe976..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyaddr.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gethostbyaddr`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the host lookup hook. - -eval_builtin! { - name: "gethostbyaddr", - area: NetworkEnv, - params: [ip], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyname.rs deleted file mode 100644 index 933f72b215..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostbyname.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gethostbyname`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the host lookup hook. - -eval_builtin! { - name: "gethostbyname", - area: NetworkEnv, - params: [hostname], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostname.rs deleted file mode 100644 index 1a13ea2e54..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/gethostname.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gethostname`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the host lookup hook. - -eval_builtin! { - name: "gethostname", - area: NetworkEnv, - params: [], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobyname.rs deleted file mode 100644 index d63ab81b78..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobyname.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `getprotobyname`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the protocol lookup hook. - -eval_builtin! { - name: "getprotobyname", - area: NetworkEnv, - params: [protocol], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobynumber.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobynumber.rs deleted file mode 100644 index b23fcea873..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getprotobynumber.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `getprotobynumber`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the protocol lookup hook. - -eval_builtin! { - name: "getprotobynumber", - area: NetworkEnv, - params: [protocol], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyname.rs deleted file mode 100644 index 9afb8c1889..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyname.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `getservbyname`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the service lookup hook. - -eval_builtin! { - name: "getservbyname", - area: NetworkEnv, - params: [service, protocol], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyport.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyport.rs deleted file mode 100644 index 463542a1c3..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/getservbyport.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `getservbyport`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the service lookup hook. - -eval_builtin! { - name: "getservbyport", - area: NetworkEnv, - params: [port, protocol], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_ntop.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_ntop.rs deleted file mode 100644 index 68c0ad384a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_ntop.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `inet_ntop`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the IP conversion hook. - -eval_builtin! { - name: "inet_ntop", - area: NetworkEnv, - params: [ip], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_pton.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_pton.rs deleted file mode 100644 index cfde090007..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/inet_pton.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `inet_pton`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the IP conversion hook. - -eval_builtin! { - name: "inet_pton", - area: NetworkEnv, - params: [ip], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/ip2long.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/ip2long.rs deleted file mode 100644 index 27b262183b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/ip2long.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ip2long`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the IP conversion hook. - -eval_builtin! { - name: "ip2long", - area: NetworkEnv, - params: [ip], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/long2ip.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/long2ip.rs deleted file mode 100644 index 9981e24f15..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/long2ip.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `long2ip`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the IP conversion hook. - -eval_builtin! { - name: "long2ip", - area: NetworkEnv, - params: [ip], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/mod.rs deleted file mode 100644 index 71fd5e4e89..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/mod.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries and dispatch adapters for network, -//! environment, process, and system-information builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env` module loading. -//! - `crate::interpreter::builtins::hooks` for migrated network/env dispatch. -//! -//! Key details: -//! - Runtime behavior stays in the focused sibling helper modules; this module -//! owns registry metadata and small hook adapters only. - -use super::super::super::*; -use super::*; - -mod exec; -mod getenv; -mod gethostbyaddr; -mod gethostbyname; -mod gethostname; -mod getprotobyname; -mod getprotobynumber; -mod getservbyname; -mod getservbyport; -mod inet_ntop; -mod inet_pton; -mod ip2long; -mod long2ip; -mod passthru; -mod php_uname; -mod phpversion; -mod putenv; -mod shell_exec; -mod system; - -/// Dispatches direct expression-level calls for declaratively migrated network/env builtins. -pub(in crate::interpreter) fn eval_builtin_network_env_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "exec" | "shell_exec" | "system" | "passthru" => { - eval_builtin_process_command(name, args, context, scope, values) - } - "getenv" => eval_builtin_getenv(args, context, scope, values), - "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), - "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), - "gethostname" => eval_builtin_gethostname(args, values), - "getprotobyname" => eval_builtin_getprotobyname(args, context, scope, values), - "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), - "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), - "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), - "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), - "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), - "ip2long" => eval_builtin_ip2long(args, context, scope, values), - "long2ip" => eval_builtin_long2ip(args, context, scope, values), - "php_uname" => super::super::eval_builtin_php_uname(args, context, scope, values), - "phpversion" => super::super::eval_builtin_phpversion(args, values), - "putenv" => eval_builtin_putenv(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated network/env builtins. -pub(in crate::interpreter) fn eval_network_env_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "php_uname" => match evaluated_args { - [] => super::super::eval_php_uname_result(None, values), - [mode] => super::super::eval_php_uname_result(Some(*mode), values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "gethostbyaddr" => { - let [ip] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyaddr_result(*ip, values) - } - "gethostbyname" => { - let [hostname] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostbyname_result(*hostname, values) - } - "gethostname" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_gethostname_result(values) - } - "getprotobyname" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobyname_result(*protocol, values) - } - "getprotobynumber" => { - let [protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getprotobynumber_result(*protocol, values) - } - "getservbyname" => { - let [service, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyname_result(*service, *protocol, values) - } - "getservbyport" => { - let [port, protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getservbyport_result(*port, *protocol, values) - } - "getenv" => { - let [name] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_getenv_result(*name, values) - } - "exec" | "shell_exec" | "system" | "passthru" => { - let [command] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_process_command_result(name, *command, values) - } - "inet_ntop" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_ntop_result(*value, values) - } - "inet_pton" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_inet_pton_result(*value, values) - } - "ip2long" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_ip2long_result(*value, values) - } - "phpversion" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - super::super::eval_phpversion_result(values) - } - "putenv" => { - let [assignment] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_putenv_result(*assignment, values) - } - "long2ip" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_long2ip_result(*value, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/passthru.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/passthru.rs deleted file mode 100644 index 6376968ddd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/passthru.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `passthru`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process-command hook. - -eval_builtin! { - name: "passthru", - area: NetworkEnv, - params: [command], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/php_uname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/php_uname.rs deleted file mode 100644 index 89067c6fa4..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/php_uname.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `php_uname`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the system-information hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "php_uname", - area: NetworkEnv, - params: [mode = EvalBuiltinDefaultValue::String("a")], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/phpversion.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/phpversion.rs deleted file mode 100644 index 23d7ad887a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/phpversion.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `phpversion`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the system-information hook. - -eval_builtin! { - name: "phpversion", - area: NetworkEnv, - params: [], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/putenv.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/putenv.rs deleted file mode 100644 index 3c9bdb1625..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/putenv.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `putenv`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the environment hook. - -eval_builtin! { - name: "putenv", - area: NetworkEnv, - params: [assignment], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/shell_exec.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/shell_exec.rs deleted file mode 100644 index 34308ab1a0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/shell_exec.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `shell_exec`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process-command hook. - -eval_builtin! { - name: "shell_exec", - area: NetworkEnv, - params: [command], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/system.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/system.rs deleted file mode 100644 index 7ea5d2e0cf..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/declarations/system.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `system`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process-command hook. - -eval_builtin! { - name: "system", - area: NetworkEnv, - params: [command], - direct: NetworkEnv, - values: NetworkEnv, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/process.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs similarity index 69% rename from crates/elephc-magician/src/interpreter/builtins/network_env/process.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs index 507204021a..0f0dddcd5a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/process.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs @@ -1,19 +1,41 @@ //! Purpose: -//! Implements eval-side shell process builtins backed by the host `/bin/sh`. -//! Capturing and passthrough variants share one command runner so return-value -//! behavior stays aligned with elephc's current native backend contract. +//! Eval registry entry and implementation for `exec` plus shared shell runner helpers. //! //! Called from: -//! - `crate::interpreter::builtins::network_env` direct and callable dispatch. +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. //! //! Key details: -//! - `exec()` and `shell_exec()` return captured stdout as a PHP string. -//! - `system()` echoes captured stdout and returns an empty string; `passthru()` -//! echoes captured stdout and returns null. +//! - `shell_exec`, `system`, and `passthru` call the runner owned by this file. use std::process::Command; -use super::super::super::*; +use super::*; + +eval_builtin! { + name: "exec", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates `exec($command)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_exec( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_process_command("exec", args, context, scope, values) +} + +/// Evaluates already materialized `exec()` command arguments. +pub(in crate::interpreter) fn eval_exec_result( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_process_command_result("exec", command, values) +} /// Evaluates one eval process-control builtin over a command expression. pub(in crate::interpreter) fn eval_builtin_process_command( diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs new file mode 100644 index 0000000000..ca16c9ed39 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Eval registry entry and implementation for `getenv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Unset variables return an empty string to match current eval semantics. + +use super::*; + +eval_builtin! { + name: "getenv", + area: NetworkEnv, + params: [name], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getenv($name)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getenv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [name] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let name = eval_expr(name, context, scope, values)?; + eval_getenv_result(name, values) +} + +/// Reads one environment variable and returns an empty string when it is unset. +pub(in crate::interpreter) fn eval_getenv_result( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8_lossy(&name); + let value = std::env::var_os(name.as_ref()) + .map(|value| value.to_string_lossy().into_owned()) + .unwrap_or_default(); + values.string(&value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs new file mode 100644 index 0000000000..751d9f28d7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs @@ -0,0 +1,63 @@ +//! Purpose: +//! Eval registry entry and implementation for `gethostbyaddr`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - libc resolver storage is copied before any subsequent resolver lookup can overwrite it. + +use super::*; + +eval_builtin! { + name: "gethostbyaddr", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostbyaddr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_gethostbyaddr_result(ip, values) +} + +/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. +pub(in crate::interpreter) fn eval_gethostbyaddr_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip_bytes = values.string_bytes(ip)?; + let ip_text = String::from_utf8_lossy(&ip_bytes); + let Ok(ipv4) = ip_text.parse::() else { + return values.bool_value(false); + }; + let octets = ipv4.octets(); + let resolved = unsafe { + // libc reads the stack-owned IPv4 octets during this call and returns + // static resolver storage, which is copied before the next resolver call. + let host = libc_gethostbyaddr( + octets.as_ptr().cast::(), + octets.len() as libc::socklen_t, + libc::AF_INET, + ); + if host.is_null() || (*host).h_name.is_null() { + None + } else { + Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) + } + }; + match resolved { + Some(name) if !name.is_empty() => values.string_bytes_value(&name), + _ => values.string(ip_text.as_ref()), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs new file mode 100644 index 0000000000..fbf0984dfd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Eval registry entry and implementation for `gethostbyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Failed lookups return the original hostname input. + +use super::*; + +eval_builtin! { + name: "gethostbyname", + area: NetworkEnv, + params: [hostname], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hostname] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hostname = eval_expr(hostname, context, scope, values)?; + eval_gethostbyname_result(hostname, values) +} + +/// Resolves one host name to an IPv4 string, or returns the original input on failure. +pub(in crate::interpreter) fn eval_gethostbyname_result( + hostname: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let hostname = values.string_bytes(hostname)?; + let hostname = String::from_utf8_lossy(&hostname); + if hostname.parse::().is_ok() { + return values.string(hostname.as_ref()); + } + let resolved = (hostname.as_ref(), 0_u16) + .to_socket_addrs() + .ok() + .and_then(|addrs| { + addrs + .filter_map(|addr| match addr.ip() { + std::net::IpAddr::V4(ip) => Some(ip.to_string()), + std::net::IpAddr::V6(_) => None, + }) + .next() + }); + values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs new file mode 100644 index 0000000000..2b4a545e60 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs @@ -0,0 +1,52 @@ +//! Purpose: +//! Eval registry entry and implementation for `gethostname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - The libc hostname buffer is stack-owned and copied into a PHP string. + +use super::*; + +eval_builtin! { + name: "gethostname", + area: NetworkEnv, + params: [], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `gethostname()` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_gethostname( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + let [] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostname_result(values) +} + +/// Reads the current host name through libc and returns an empty string on failure. +pub(in crate::interpreter) fn eval_gethostname_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let mut buffer = [0 as libc::c_char; 256]; + let status = unsafe { + // libc writes at most buffer.len() bytes into this stack buffer. + libc::gethostname(buffer.as_mut_ptr(), buffer.len()) + }; + if status != 0 { + return values.string(""); + } + let length = buffer + .iter() + .position(|byte| *byte == 0) + .unwrap_or(buffer.len()); + let hostname = buffer[..length] + .iter() + .map(|byte| *byte as u8) + .collect::>(); + values.string_bytes_value(&hostname) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs new file mode 100644 index 0000000000..1a1982e1e9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs @@ -0,0 +1,83 @@ +//! Purpose: +//! Eval registry entry and implementation for `getprotobyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Lowercase C-string and protoent-name helpers are owned here for protocol lookups. + +use super::*; + +eval_builtin! { + name: "getprotobyname", + area: NetworkEnv, + params: [protocol], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getprotobyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobyname_result(protocol, values) +} + +/// Looks up an IP protocol number by name or alias. +pub(in crate::interpreter) fn eval_getprotobyname_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy scalar fields before another lookup. + libc_getprotobyname(protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let number = unsafe { (*entry).p_proto }; + values.int(i64::from(number)) +} + + +/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. +pub(in crate::interpreter) fn eval_lowercase_c_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let bytes = values.string_bytes(value)?; + let bytes = bytes + .into_iter() + .map(|byte| byte.to_ascii_lowercase()) + .collect::>(); + Ok(CString::new(bytes).ok()) +} + +/// Copies a protoent canonical name into a PHP string or returns PHP false. +pub(in crate::interpreter) fn eval_protoent_name_or_false( + entry: *mut libc::protoent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).p_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs new file mode 100644 index 0000000000..0b71fb38bc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Eval registry entry and implementation for `getprotobynumber`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Protocol-name extraction delegates to `getprotobyname`. + +use super::*; + +eval_builtin! { + name: "getprotobynumber", + area: NetworkEnv, + params: [protocol], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_getprotobynumber( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getprotobynumber_result(protocol, values) +} + +/// Looks up an IP protocol name by numeric protocol id. +pub(in crate::interpreter) fn eval_getprotobynumber_result( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = eval_int_value(protocol, values)?; + let Ok(protocol) = libc::c_int::try_from(protocol) else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global protoent; copy the name before another lookup. + libc_getprotobynumber(protocol) + }; + eval_protoent_name_or_false(entry, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs new file mode 100644 index 0000000000..478e58b48d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs @@ -0,0 +1,75 @@ +//! Purpose: +//! Eval registry entry and implementation for `getservbyname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Service-name extraction is owned here and reused by `getservbyport`. + +use super::*; + +eval_builtin! { + name: "getservbyname", + area: NetworkEnv, + params: [service, protocol], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_getservbyname( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [service, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let service = eval_expr(service, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyname_result(service, protocol, values) +} + +/// Looks up an internet service port by service name and protocol. +pub(in crate::interpreter) fn eval_getservbyname_result( + service: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(service) = eval_lowercase_c_string(service, values)? else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let entry = unsafe { + // libc returns a process-global servent; copy scalar fields before another lookup. + libc_getservbyname(service.as_ptr(), protocol.as_ptr()) + }; + if entry.is_null() { + return values.bool_value(false); + } + let port = unsafe { u16::from_be((*entry).s_port as u16) }; + values.int(i64::from(port)) +} + + +/// Copies a servent canonical name into a PHP string or returns PHP false. +pub(in crate::interpreter) fn eval_servent_name_or_false( + entry: *mut libc::servent, + values: &mut impl RuntimeValueOps, +) -> Result { + if entry.is_null() { + return values.bool_value(false); + } + let name = unsafe { + let name = (*entry).s_name; + if name.is_null() { + return values.bool_value(false); + } + CStr::from_ptr(name).to_bytes().to_vec() + }; + values.string_bytes_value(&name) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs new file mode 100644 index 0000000000..b3adc46ae3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Eval registry entry and implementation for `getservbyport`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - C-string conversion and service-name extraction delegate to the owner builtin files. + +use super::*; + +eval_builtin! { + name: "getservbyport", + area: NetworkEnv, + params: [port, protocol], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_getservbyport( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [port, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let port = eval_expr(port, context, scope, values)?; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_getservbyport_result(port, protocol, values) +} + +/// Looks up an internet service name by port and protocol. +pub(in crate::interpreter) fn eval_getservbyport_result( + port: RuntimeCellHandle, + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let port = eval_int_value(port, values)?; + let Ok(port) = u16::try_from(port) else { + return values.bool_value(false); + }; + let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { + return values.bool_value(false); + }; + let network_port = port.to_be() as libc::c_int; + let entry = unsafe { + // libc returns a process-global servent; copy the name before another lookup. + libc_getservbyport(network_port, protocol.as_ptr()) + }; + eval_servent_name_or_false(entry, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/hosts.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/hosts.rs deleted file mode 100644 index 6b82a25b9e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/hosts.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Purpose: -//! Implements host-name lookup eval builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env` re-exports. -//! -//! Key details: -//! - Host resolver results from libc are copied immediately because they point at -//! process-global static storage. - -use super::super::super::*; - -/// Evaluates PHP `gethostbyaddr($ip)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_gethostbyaddr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_gethostbyaddr_result(ip, values) -} - -/// Reverse-resolves one IPv4 address, returns the input on miss, or PHP false when malformed. -pub(in crate::interpreter) fn eval_gethostbyaddr_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let ip_bytes = values.string_bytes(ip)?; - let ip_text = String::from_utf8_lossy(&ip_bytes); - let Ok(ipv4) = ip_text.parse::() else { - return values.bool_value(false); - }; - let octets = ipv4.octets(); - let resolved = unsafe { - // libc reads the stack-owned IPv4 octets during this call and returns - // static resolver storage, which is copied before the next resolver call. - let host = libc_gethostbyaddr( - octets.as_ptr().cast::(), - octets.len() as libc::socklen_t, - libc::AF_INET, - ); - if host.is_null() || (*host).h_name.is_null() { - None - } else { - Some(CStr::from_ptr((*host).h_name).to_bytes().to_vec()) - } - }; - match resolved { - Some(name) if !name.is_empty() => values.string_bytes_value(&name), - _ => values.string(ip_text.as_ref()), - } -} - -/// Evaluates PHP `gethostbyname($hostname)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_gethostbyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hostname] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hostname = eval_expr(hostname, context, scope, values)?; - eval_gethostbyname_result(hostname, values) -} - -/// Resolves one host name to an IPv4 string, or returns the original input on failure. -pub(in crate::interpreter) fn eval_gethostbyname_result( - hostname: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let hostname = values.string_bytes(hostname)?; - let hostname = String::from_utf8_lossy(&hostname); - if hostname.parse::().is_ok() { - return values.string(hostname.as_ref()); - } - let resolved = (hostname.as_ref(), 0_u16) - .to_socket_addrs() - .ok() - .and_then(|addrs| { - addrs - .filter_map(|addr| match addr.ip() { - std::net::IpAddr::V4(ip) => Some(ip.to_string()), - std::net::IpAddr::V6(_) => None, - }) - .next() - }); - values.string(resolved.as_deref().unwrap_or_else(|| hostname.as_ref())) -} - -/// Evaluates PHP `gethostname()` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_gethostname( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - let [] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_gethostname_result(values) -} - -/// Reads the current host name through libc and returns an empty string on failure. -pub(in crate::interpreter) fn eval_gethostname_result( - values: &mut impl RuntimeValueOps, -) -> Result { - let mut buffer = [0 as libc::c_char; 256]; - let status = unsafe { - // libc writes at most buffer.len() bytes into this stack buffer. - libc::gethostname(buffer.as_mut_ptr(), buffer.len()) - }; - if status != 0 { - return values.string(""); - } - let length = buffer - .iter() - .position(|byte| *byte == 0) - .unwrap_or(buffer.len()); - let hostname = buffer[..length] - .iter() - .map(|byte| *byte as u8) - .collect::>(); - values.string_bytes_value(&hostname) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs new file mode 100644 index 0000000000..8c599d8b1a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Eval registry entry and implementation for `inet_ntop`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - IPv4 formatting delegates to `long2ip` so byte rendering stays aligned. + +use super::*; + +eval_builtin! { + name: "inet_ntop", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `inet_ntop($binary)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_inet_ntop( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [binary] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let binary = eval_expr(binary, context, scope, values)?; + eval_inet_ntop_result(binary, values) +} + +/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. +pub(in crate::interpreter) fn eval_inet_ntop_result( + binary: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(binary)?; + let [a, b, c, d] = bytes.as_slice() else { + return values.bool_value(false); + }; + let ip = u32::from_be_bytes([*a, *b, *c, *d]); + values.string(&eval_format_ipv4(ip)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs new file mode 100644 index 0000000000..27b14c4a1d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Eval registry entry and implementation for `inet_pton`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - IPv4 parsing delegates to `ip2long` so malformed-address behavior stays aligned. + +use super::*; + +eval_builtin! { + name: "inet_pton", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `inet_pton($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_inet_pton( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_inet_pton_result(ip, values) +} + +/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. +pub(in crate::interpreter) fn eval_inet_pton_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + let Some(ip) = eval_parse_ipv4(&bytes) else { + return values.bool_value(false); + }; + values.string_bytes_value(&ip.to_be_bytes()) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/ip.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/ip.rs deleted file mode 100644 index 48cf864232..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/ip.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! Purpose: -//! Implements IPv4 conversion eval builtins such as `ip2long`, `long2ip`, -//! `inet_pton`, and `inet_ntop`. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env` re-exports. -//! -//! Key details: -//! - The supported eval subset is IPv4-only and returns PHP false for malformed -//! addresses or binary payloads. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP `long2ip($ip)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_long2ip( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_long2ip_result(ip, values) -} - -/// Formats one 32-bit IPv4 integer as a dotted-quad string. -pub(in crate::interpreter) fn eval_long2ip_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let ip = eval_int_value(ip, values)? as u32; - values.string(&eval_format_ipv4(ip)) -} - -/// Evaluates PHP `ip2long($ip)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_ip2long( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_ip2long_result(ip, values) -} - -/// Parses a dotted-quad IPv4 string into an integer or PHP false. -pub(in crate::interpreter) fn eval_ip2long_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(ip)?; - match eval_parse_ipv4(&bytes) { - Some(ip) => values.int(i64::from(ip)), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `inet_pton($ip)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_inet_pton( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [ip] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let ip = eval_expr(ip, context, scope, values)?; - eval_inet_pton_result(ip, values) -} - -/// Packs a dotted-quad IPv4 string into four network-order bytes or PHP false. -pub(in crate::interpreter) fn eval_inet_pton_result( - ip: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(ip)?; - let Some(ip) = eval_parse_ipv4(&bytes) else { - return values.bool_value(false); - }; - values.string_bytes_value(&ip.to_be_bytes()) -} - -/// Evaluates PHP `inet_ntop($binary)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_inet_ntop( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [binary] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let binary = eval_expr(binary, context, scope, values)?; - eval_inet_ntop_result(binary, values) -} - -/// Renders a four-byte IPv4 string as dotted-quad text or PHP false. -pub(in crate::interpreter) fn eval_inet_ntop_result( - binary: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(binary)?; - let [a, b, c, d] = bytes.as_slice() else { - return values.bool_value(false); - }; - let ip = u32::from_be_bytes([*a, *b, *c, *d]); - values.string(&eval_format_ipv4(ip)) -} - -/// Parses exactly four decimal IPv4 octets separated by dots. -pub(in crate::interpreter) fn eval_parse_ipv4(bytes: &[u8]) -> Option { - let mut octets = [0_u8; 4]; - let mut position = 0_usize; - let mut index = 0_usize; - - while index < 4 { - if position >= bytes.len() { - return None; - } - let start = position; - let mut value = 0_u16; - while position < bytes.len() && bytes[position].is_ascii_digit() { - value = value - .checked_mul(10)? - .checked_add(u16::from(bytes[position] - b'0'))?; - position += 1; - if position - start > 3 || value > 255 { - return None; - } - } - if position == start { - return None; - } - octets[index] = value as u8; - index += 1; - if index == 4 { - return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); - } - if bytes.get(position).copied() != Some(b'.') { - return None; - } - position += 1; - } - None -} - -/// Formats one packed IPv4 integer into dotted-quad text. -pub(in crate::interpreter) fn eval_format_ipv4(ip: u32) -> String { - let [a, b, c, d] = ip.to_be_bytes(); - format!("{}.{}.{}.{}", a, b, c, d) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs new file mode 100644 index 0000000000..2fda828176 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Eval registry entry and implementation for `ip2long`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - The dotted-quad parser is owned here and reused by `inet_pton`. + +use super::*; + +eval_builtin! { + name: "ip2long", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `ip2long($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ip2long( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_ip2long_result(ip, values) +} + +/// Parses a dotted-quad IPv4 string into an integer or PHP false. +pub(in crate::interpreter) fn eval_ip2long_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(ip)?; + match eval_parse_ipv4(&bytes) { + Some(ip) => values.int(i64::from(ip)), + None => values.bool_value(false), + } +} + + +/// Parses exactly four decimal IPv4 octets separated by dots. +pub(in crate::interpreter) fn eval_parse_ipv4(bytes: &[u8]) -> Option { + let mut octets = [0_u8; 4]; + let mut position = 0_usize; + let mut index = 0_usize; + + while index < 4 { + if position >= bytes.len() { + return None; + } + let start = position; + let mut value = 0_u16; + while position < bytes.len() && bytes[position].is_ascii_digit() { + value = value + .checked_mul(10)? + .checked_add(u16::from(bytes[position] - b'0'))?; + position += 1; + if position - start > 3 || value > 255 { + return None; + } + } + if position == start { + return None; + } + octets[index] = value as u8; + index += 1; + if index == 4 { + return (position == bytes.len()).then(|| u32::from_be_bytes(octets)); + } + if bytes.get(position).copied() != Some(b'.') { + return None; + } + position += 1; + } + None +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs new file mode 100644 index 0000000000..032145d3ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs @@ -0,0 +1,48 @@ +//! Purpose: +//! Eval registry entry and implementation for `long2ip`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - The IPv4 formatter is owned here and reused by `inet_ntop`. + +use super::*; + +eval_builtin! { + name: "long2ip", + area: NetworkEnv, + params: [ip], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `long2ip($ip)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_long2ip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [ip] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let ip = eval_expr(ip, context, scope, values)?; + eval_long2ip_result(ip, values) +} + +/// Formats one 32-bit IPv4 integer as a dotted-quad string. +pub(in crate::interpreter) fn eval_long2ip_result( + ip: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let ip = eval_int_value(ip, values)? as u32; + values.string(&eval_format_ipv4(ip)) +} + + +/// Formats one packed IPv4 integer into dotted-quad text. +pub(in crate::interpreter) fn eval_format_ipv4(ip: u32) -> String { + let [a, b, c, d] = ip.to_be_bytes(); + format!("{}.{}.{}.{}", a, b, c, d) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs index fc913dc119..b89dd6e9de 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs @@ -1,26 +1,211 @@ //! Purpose: -//! Groups network lookup, IP conversion, environment, and realpath-cache eval -//! builtins by focused runtime domain. +//! Orchestrates network lookup, IP conversion, environment, process, and +//! system-information eval builtins. //! //! Called from: //! - `crate::interpreter::builtins` re-exports used by core call dispatch. //! //! Key details: -//! - libc lookup results are copied before subsequent lookups can overwrite -//! process-global resolver storage. +//! - Leaf builtin files own their registry declarations and builtin-specific wrappers. +//! - Shared helpers live in owner builtin files, such as `exec`, `ip2long`, +//! `long2ip`, `getprotobyname`, and `getservbyname`. + +use super::super::*; mod cache; -mod declarations; -mod env; -mod hosts; -mod ip; -mod process; -mod protocols; +mod exec; +mod getenv; +mod gethostbyaddr; +mod gethostbyname; +mod gethostname; +mod getprotobyname; +mod getprotobynumber; +mod getservbyname; +mod getservbyport; +mod inet_ntop; +mod inet_pton; +mod ip2long; +mod long2ip; +mod passthru; +mod php_uname; +mod phpversion; +mod putenv; +mod shell_exec; +mod system; pub(in crate::interpreter) use cache::*; -pub(in crate::interpreter) use declarations::*; -pub(in crate::interpreter) use env::*; -pub(in crate::interpreter) use hosts::*; -pub(in crate::interpreter) use ip::*; -pub(in crate::interpreter) use process::*; -pub(in crate::interpreter) use protocols::*; +pub(in crate::interpreter) use exec::*; +pub(in crate::interpreter) use getenv::*; +pub(in crate::interpreter) use gethostbyaddr::*; +pub(in crate::interpreter) use gethostbyname::*; +pub(in crate::interpreter) use gethostname::*; +pub(in crate::interpreter) use getprotobyname::*; +pub(in crate::interpreter) use getprotobynumber::*; +pub(in crate::interpreter) use getservbyname::*; +pub(in crate::interpreter) use getservbyport::*; +pub(in crate::interpreter) use inet_ntop::*; +pub(in crate::interpreter) use inet_pton::*; +pub(in crate::interpreter) use ip2long::*; +pub(in crate::interpreter) use long2ip::*; +pub(in crate::interpreter) use passthru::*; +pub(in crate::interpreter) use php_uname::*; +pub(in crate::interpreter) use phpversion::*; +pub(in crate::interpreter) use putenv::*; +pub(in crate::interpreter) use shell_exec::*; +pub(in crate::interpreter) use system::*; + +/// Dispatches direct expression-level calls for network/env builtins. +pub(in crate::interpreter) fn eval_builtin_network_env_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "exec" => eval_builtin_exec(args, context, scope, values), + "shell_exec" => eval_builtin_shell_exec(args, context, scope, values), + "system" => eval_builtin_system(args, context, scope, values), + "passthru" => eval_builtin_passthru(args, context, scope, values), + "getenv" => eval_builtin_getenv(args, context, scope, values), + "gethostbyaddr" => eval_builtin_gethostbyaddr(args, context, scope, values), + "gethostbyname" => eval_builtin_gethostbyname(args, context, scope, values), + "gethostname" => eval_builtin_gethostname(args, values), + "getprotobyname" => eval_builtin_getprotobyname(args, context, scope, values), + "getprotobynumber" => eval_builtin_getprotobynumber(args, context, scope, values), + "getservbyname" => eval_builtin_getservbyname(args, context, scope, values), + "getservbyport" => eval_builtin_getservbyport(args, context, scope, values), + "inet_ntop" => eval_builtin_inet_ntop(args, context, scope, values), + "inet_pton" => eval_builtin_inet_pton(args, context, scope, values), + "ip2long" => eval_builtin_ip2long(args, context, scope, values), + "long2ip" => eval_builtin_long2ip(args, context, scope, values), + "php_uname" => eval_builtin_php_uname(args, context, scope, values), + "phpversion" => eval_builtin_phpversion(args, values), + "putenv" => eval_builtin_putenv(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for network/env builtins. +pub(in crate::interpreter) fn eval_network_env_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "php_uname" => match evaluated_args { + [] => eval_php_uname_result(None, values), + [mode] => eval_php_uname_result(Some(*mode), values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "gethostbyaddr" => { + let [ip] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyaddr_result(*ip, values) + } + "gethostbyname" => { + let [hostname] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_gethostbyname_result(*hostname, values) + } + "gethostname" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_gethostname_result(values) + } + "getprotobyname" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobyname_result(*protocol, values) + } + "getprotobynumber" => { + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getprotobynumber_result(*protocol, values) + } + "getservbyname" => { + let [service, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyname_result(*service, *protocol, values) + } + "getservbyport" => { + let [port, protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getservbyport_result(*port, *protocol, values) + } + "getenv" => { + let [name] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_getenv_result(*name, values) + } + "exec" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_exec_result(*command, values) + } + "shell_exec" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_shell_exec_result(*command, values) + } + "system" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_system_result(*command, values) + } + "passthru" => { + let [command] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_passthru_result(*command, values) + } + "inet_ntop" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_ntop_result(*value, values) + } + "inet_pton" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_inet_pton_result(*value, values) + } + "ip2long" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_ip2long_result(*value, values) + } + "phpversion" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values) + } + "putenv" => { + let [assignment] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_putenv_result(*assignment, values) + } + "long2ip" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_long2ip_result(*value, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs new file mode 100644 index 0000000000..62be490593 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `passthru`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Command execution delegates to the shell runner owned by `exec`. + +use super::*; + +eval_builtin! { + name: "passthru", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates `passthru($command)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_passthru( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_process_command("passthru", args, context, scope, values) +} + +/// Evaluates already materialized `passthru()` command arguments. +pub(in crate::interpreter) fn eval_passthru_result( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_process_command_result("passthru", command, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/system.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs similarity index 65% rename from crates/elephc-magician/src/interpreter/builtins/time/system.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs index d2e481a615..27bc934606 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/system.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs @@ -1,52 +1,22 @@ //! Purpose: -//! Implements shared system information helpers still used by network/env builtins. +//! Eval registry entry and implementation for `php_uname`. //! //! Called from: -//! - `crate::interpreter::builtins::network_env` until those builtins are co-located. +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. //! //! Key details: -//! - `phpversion()` reads the workspace package version at compile time and -//! `php_uname()` formats libc `uname` fields. +//! - libc uname fields are copied into PHP strings before formatting the requested mode. -use super::super::super::*; +use super::*; -/// Evaluates PHP `phpversion()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_phpversion( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_phpversion_result(values) -} - -/// Returns the root elephc package version as a boxed PHP string. -pub(in crate::interpreter) fn eval_phpversion_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.string(eval_compiler_php_version()) -} +use super::super::spec::EvalBuiltinDefaultValue; -/// Reads the root package version from the workspace manifest used by native `phpversion()`. -pub(in crate::interpreter) fn eval_compiler_php_version() -> &'static str { - let mut in_package = false; - for line in EVAL_ROOT_CARGO_TOML.lines() { - let line = line.trim(); - if line == "[package]" { - in_package = true; - continue; - } - if in_package && line.starts_with('[') { - break; - } - if in_package { - if let Some(value) = line.strip_prefix("version = ") { - return value.trim_matches('"'); - } - } - } - env!("CARGO_PKG_VERSION") +eval_builtin! { + name: "php_uname", + area: NetworkEnv, + params: [mode = EvalBuiltinDefaultValue::String("a")], + direct: NetworkEnv, + values: NetworkEnv, } /// Evaluates PHP `php_uname($mode = "a")` over zero or one eval expression. diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs new file mode 100644 index 0000000000..8233a9447d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs @@ -0,0 +1,57 @@ +//! Purpose: +//! Eval registry entry and implementation for `phpversion`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - The compiler package version is read from the workspace manifest. + +use super::*; + +eval_builtin! { + name: "phpversion", + area: NetworkEnv, + params: [], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates PHP `phpversion()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_phpversion( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_phpversion_result(values) +} + +/// Returns the root elephc package version as a boxed PHP string. +pub(in crate::interpreter) fn eval_phpversion_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string(eval_compiler_php_version()) +} + +/// Reads the root package version from the workspace manifest used by native `phpversion()`. +pub(in crate::interpreter) fn eval_compiler_php_version() -> &'static str { + let mut in_package = false; + for line in EVAL_ROOT_CARGO_TOML.lines() { + let line = line.trim(); + if line == "[package]" { + in_package = true; + continue; + } + if in_package && line.starts_with('[') { + break; + } + if in_package { + if let Some(value) = line.strip_prefix("version = ") { + return value.trim_matches('"'); + } + } + } + env!("CARGO_PKG_VERSION") +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/protocols.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/protocols.rs deleted file mode 100644 index ed0ff691dc..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/protocols.rs +++ /dev/null @@ -1,198 +0,0 @@ -//! Purpose: -//! Implements protocol and service database lookup eval builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env` re-exports. -//! -//! Key details: -//! - PHP values are converted to lowercase NUL-free C strings before libc lookup -//! and returned database names are copied into runtime strings. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP `getprotobyname($protocol)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_getprotobyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getprotobyname_result(protocol, values) -} - -/// Looks up an IP protocol number by name or alias. -pub(in crate::interpreter) fn eval_getprotobyname_result( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global protoent; copy scalar fields before another lookup. - libc_getprotobyname(protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); - } - let number = unsafe { (*entry).p_proto }; - values.int(i64::from(number)) -} - -/// Evaluates PHP `getprotobynumber($protocol)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_getprotobynumber( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getprotobynumber_result(protocol, values) -} - -/// Looks up an IP protocol name by numeric protocol id. -pub(in crate::interpreter) fn eval_getprotobynumber_result( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let protocol = eval_int_value(protocol, values)?; - let Ok(protocol) = libc::c_int::try_from(protocol) else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global protoent; copy the name before another lookup. - libc_getprotobynumber(protocol) - }; - eval_protoent_name_or_false(entry, values) -} - -/// Evaluates PHP `getservbyname($service, $protocol)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_getservbyname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [service, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let service = eval_expr(service, context, scope, values)?; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getservbyname_result(service, protocol, values) -} - -/// Looks up an internet service port by service name and protocol. -pub(in crate::interpreter) fn eval_getservbyname_result( - service: RuntimeCellHandle, - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(service) = eval_lowercase_c_string(service, values)? else { - return values.bool_value(false); - }; - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let entry = unsafe { - // libc returns a process-global servent; copy scalar fields before another lookup. - libc_getservbyname(service.as_ptr(), protocol.as_ptr()) - }; - if entry.is_null() { - return values.bool_value(false); - } - let port = unsafe { u16::from_be((*entry).s_port as u16) }; - values.int(i64::from(port)) -} - -/// Evaluates PHP `getservbyport($port, $protocol)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_getservbyport( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [port, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let port = eval_expr(port, context, scope, values)?; - let protocol = eval_expr(protocol, context, scope, values)?; - eval_getservbyport_result(port, protocol, values) -} - -/// Looks up an internet service name by port and protocol. -pub(in crate::interpreter) fn eval_getservbyport_result( - port: RuntimeCellHandle, - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let port = eval_int_value(port, values)?; - let Ok(port) = u16::try_from(port) else { - return values.bool_value(false); - }; - let Some(protocol) = eval_lowercase_c_string(protocol, values)? else { - return values.bool_value(false); - }; - let network_port = port.to_be() as libc::c_int; - let entry = unsafe { - // libc returns a process-global servent; copy the name before another lookup. - libc_getservbyport(network_port, protocol.as_ptr()) - }; - eval_servent_name_or_false(entry, values) -} - -/// Converts a PHP value to a NUL-free lowercase C string for libc database lookups. -pub(in crate::interpreter) fn eval_lowercase_c_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let bytes = values.string_bytes(value)?; - let bytes = bytes - .into_iter() - .map(|byte| byte.to_ascii_lowercase()) - .collect::>(); - Ok(CString::new(bytes).ok()) -} - -/// Copies a protoent canonical name into a PHP string or returns PHP false. -pub(in crate::interpreter) fn eval_protoent_name_or_false( - entry: *mut libc::protoent, - values: &mut impl RuntimeValueOps, -) -> Result { - if entry.is_null() { - return values.bool_value(false); - } - let name = unsafe { - let name = (*entry).p_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) -} - -/// Copies a servent canonical name into a PHP string or returns PHP false. -pub(in crate::interpreter) fn eval_servent_name_or_false( - entry: *mut libc::servent, - values: &mut impl RuntimeValueOps, -) -> Result { - if entry.is_null() { - return values.bool_value(false); - } - let name = unsafe { - let name = (*entry).s_name; - if name.is_null() { - return values.bool_value(false); - } - CStr::from_ptr(name).to_bytes().to_vec() - }; - values.string_bytes_value(&name) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/env.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs similarity index 51% rename from crates/elephc-magician/src/interpreter/builtins/network_env/env.rs rename to crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs index c29afeac11..0ad3b601d7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/env.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs @@ -1,40 +1,20 @@ //! Purpose: -//! Implements environment variable eval builtins. +//! Eval registry entry and implementation for `putenv`. //! //! Called from: -//! - `crate::interpreter::builtins::network_env` re-exports. +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. //! //! Key details: -//! - `getenv` returns an empty string for unset variables and `putenv` mutates the -//! host process environment. +//! - Assignments mutate the host process environment for the current eval process. -use super::super::super::*; +use super::*; -/// Evaluates PHP `getenv($name)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_getenv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [name] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let name = eval_expr(name, context, scope, values)?; - eval_getenv_result(name, values) -} - -/// Reads one environment variable and returns an empty string when it is unset. -pub(in crate::interpreter) fn eval_getenv_result( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8_lossy(&name); - let value = std::env::var_os(name.as_ref()) - .map(|value| value.to_string_lossy().into_owned()) - .unwrap_or_default(); - values.string(&value) +eval_builtin! { + name: "putenv", + area: NetworkEnv, + params: [assignment], + direct: NetworkEnv, + values: NetworkEnv, } /// Evaluates PHP `putenv($assignment)` over one eval expression. diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs new file mode 100644 index 0000000000..35e898706c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `shell_exec`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Command execution delegates to the shell runner owned by `exec`. + +use super::*; + +eval_builtin! { + name: "shell_exec", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates `shell_exec($command)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_shell_exec( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_process_command("shell_exec", args, context, scope, values) +} + +/// Evaluates already materialized `shell_exec()` command arguments. +pub(in crate::interpreter) fn eval_shell_exec_result( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_process_command_result("shell_exec", command, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs new file mode 100644 index 0000000000..aa85fbe25b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Eval registry entry and implementation wrapper for `system`. +//! +//! Called from: +//! - `crate::interpreter::builtins::network_env` direct and by-value dispatch. +//! +//! Key details: +//! - Command execution delegates to the shell runner owned by `exec`. + +use super::*; + +eval_builtin! { + name: "system", + area: NetworkEnv, + params: [command], + direct: NetworkEnv, + values: NetworkEnv, +} + +/// Evaluates `system($command)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_system( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_process_command("system", args, context, scope, values) +} + +/// Evaluates already materialized `system()` command arguments. +pub(in crate::interpreter) fn eval_system_result( + command: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_process_command_result("system", command, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs index ada5187493..e1a34f7cae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/time/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/time/mod.rs @@ -28,7 +28,6 @@ mod microtime; mod mktime; mod sleep; mod strtotime; -mod system; mod time; mod usleep; @@ -48,7 +47,6 @@ pub(in crate::interpreter) use microtime::*; pub(in crate::interpreter) use mktime::*; pub(in crate::interpreter) use sleep::*; pub(in crate::interpreter) use strtotime::*; -pub(in crate::interpreter) use system::*; pub(in crate::interpreter) use time::*; pub(in crate::interpreter) use usleep::*; From ae1ea096fe6ac46d0c603f7bc10f741fbf990ead Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 16:57:39 +0200 Subject: [PATCH 1108/1208] refactor(magician): co-locate string builtin implementations --- .../src/interpreter/builtins/arrays/access.rs | 101 ------ .../src/interpreter/builtins/hooks/direct.rs | 169 ++++++---- .../src/interpreter/builtins/hooks/values.rs | 172 +++++----- .../src/interpreter/builtins/mod.rs | 3 +- .../interpreter/builtins/scalars/base64.rs | 130 -------- .../src/interpreter/builtins/scalars/hex.rs | 93 ------ .../src/interpreter/builtins/scalars/mod.rs | 16 +- .../interpreter/builtins/scalars/search.rs | 218 ------------- .../interpreter/builtins/scalars/slashes.rs | 81 ----- .../interpreter/builtins/scalars/trim_case.rs | 303 ------------------ .../interpreter/builtins/string/addslashes.rs | 38 ++- .../builtins/string/base64_decode.rs | 82 ++++- .../builtins/string/base64_encode.rs | 46 ++- .../interpreter/builtins/string/bin2hex.rs | 38 ++- .../src/interpreter/builtins/string/chop.rs | 23 +- .../src/interpreter/builtins/string/chr.rs | 27 +- .../src/interpreter/builtins/string/crc32.rs | 28 +- .../builtins/string/ctype_alnum.rs | 68 +++- .../builtins/string/ctype_alpha.rs | 22 +- .../builtins/string/ctype_digit.rs | 22 +- .../builtins/string/ctype_space.rs | 22 +- .../interpreter/builtins/string/explode.rs | 69 ++++ .../builtins/string/grapheme_strrev.rs | 32 +- .../{strings/gzip.rs => string/gzcompress.rs} | 48 ++- .../interpreter/builtins/string/gzdeflate.rs | 38 +++ .../interpreter/builtins/string/gzinflate.rs | 38 +++ .../builtins/string/gzuncompress.rs | 38 +++ .../builtins/{strings => string}/hash.rs | 98 ++---- .../interpreter/builtins/string/hash_algos.rs | 51 +++ .../interpreter/builtins/string/hash_copy.rs | 45 +++ .../builtins/string/hash_equals.rs | 37 ++- .../interpreter/builtins/string/hash_file.rs | 54 ++++ .../interpreter/builtins/string/hash_final.rs | 56 ++++ .../interpreter/builtins/string/hash_hmac.rs | 71 ++++ .../interpreter/builtins/string/hash_init.rs | 64 ++++ .../builtins/string/hash_update.rs | 45 +++ .../interpreter/builtins/string/hex2bin.rs | 53 ++- .../builtins/string/html_entity_decode.rs | 22 +- .../builtins/string/htmlentities.rs | 22 +- .../builtins/string/htmlspecialchars.rs | 101 +++++- .../interpreter/builtins/string/implode.rs | 60 ++++ .../interpreter/builtins/string/lcfirst.rs | 22 +- .../src/interpreter/builtins/string/ltrim.rs | 23 +- .../src/interpreter/builtins/string/md5.rs | 38 +++ .../src/interpreter/builtins/string/mod.rs | 110 ++++++- .../src/interpreter/builtins/string/nl2br.rs | 61 +++- .../src/interpreter/builtins/string/ord.rs | 27 +- .../builtins/string/rawurldecode.rs | 22 +- .../builtins/string/rawurlencode.rs | 22 +- .../src/interpreter/builtins/string/rtrim.rs | 23 +- .../src/interpreter/builtins/string/sha1.rs | 38 +++ .../builtins/string/str_contains.rs | 62 +++- .../builtins/string/str_ends_with.rs | 23 +- .../builtins/string/str_ireplace.rs | 24 +- .../interpreter/builtins/string/str_pad.rs | 99 +++++- .../interpreter/builtins/string/str_repeat.rs | 42 ++- .../builtins/string/str_replace.rs | 67 +++- .../interpreter/builtins/string/str_split.rs | 50 ++- .../builtins/string/str_starts_with.rs | 23 +- .../interpreter/builtins/string/strcasecmp.rs | 23 +- .../src/interpreter/builtins/string/strcmp.rs | 64 +++- .../builtins/string/stream_get_filters.rs | 35 ++ .../builtins/string/stream_get_transports.rs | 35 ++ .../builtins/string/stream_get_wrappers.rs | 80 +++++ .../builtins/string/stream_is_local.rs | 69 ++++ .../builtins/string/stream_supports_lock.rs | 36 +++ .../builtins/string/stripslashes.rs | 41 ++- .../src/interpreter/builtins/string/strlen.rs | 20 +- .../src/interpreter/builtins/string/strpos.rs | 68 +++- .../src/interpreter/builtins/string/strrev.rs | 26 +- .../interpreter/builtins/string/strrpos.rs | 23 +- .../src/interpreter/builtins/string/strstr.rs | 66 +++- .../interpreter/builtins/string/strtolower.rs | 75 ++++- .../interpreter/builtins/string/strtoupper.rs | 22 +- .../src/interpreter/builtins/string/substr.rs | 60 +++- .../builtins/string/substr_replace.rs | 67 +++- .../src/interpreter/builtins/string/trim.rs | 84 ++++- .../interpreter/builtins/string/ucfirst.rs | 22 +- .../interpreter/builtins/string/ucwords.rs | 50 ++- .../interpreter/builtins/string/urldecode.rs | 74 ++++- .../interpreter/builtins/string/urlencode.rs | 73 ++++- .../interpreter/builtins/string/wordwrap.rs | 136 +++++++- .../src/interpreter/builtins/strings/ctype.rs | 56 ---- .../builtins/strings/declarations/explode.rs | 18 -- .../strings/declarations/gzcompress.rs | 18 -- .../strings/declarations/gzdeflate.rs | 18 -- .../strings/declarations/gzinflate.rs | 18 -- .../strings/declarations/gzuncompress.rs | 18 -- .../builtins/strings/declarations/hash.rs | 18 -- .../strings/declarations/hash_algos.rs | 16 - .../strings/declarations/hash_copy.rs | 16 - .../strings/declarations/hash_file.rs | 18 -- .../strings/declarations/hash_final.rs | 18 -- .../strings/declarations/hash_hmac.rs | 18 -- .../strings/declarations/hash_init.rs | 23 -- .../strings/declarations/hash_update.rs | 16 - .../builtins/strings/declarations/implode.rs | 19 -- .../builtins/strings/declarations/md5.rs | 18 -- .../builtins/strings/declarations/mod.rs | 30 -- .../builtins/strings/declarations/sha1.rs | 18 -- .../declarations/stream_get_filters.rs | 16 - .../declarations/stream_get_transports.rs | 16 - .../declarations/stream_get_wrappers.rs | 16 - .../strings/declarations/stream_is_local.rs | 16 - .../declarations/stream_supports_lock.rs | 16 - .../builtins/strings/grapheme_strrev.rs | 42 --- .../builtins/strings/hash_context.rs | 143 --------- .../src/interpreter/builtins/strings/html.rs | 97 ------ .../builtins/strings/introspection.rs | 139 -------- .../src/interpreter/builtins/strings/mod.rs | 42 --- .../src/interpreter/builtins/strings/nl2br.rs | 67 ---- .../src/interpreter/builtins/strings/pad.rs | 106 ------ .../interpreter/builtins/strings/repeat.rs | 49 --- .../interpreter/builtins/strings/replace.rs | 74 ----- .../interpreter/builtins/strings/simple.rs | 48 --- .../src/interpreter/builtins/strings/split.rs | 57 ---- .../interpreter/builtins/strings/substr.rs | 130 -------- .../src/interpreter/builtins/strings/url.rs | 114 ------- .../builtins/symbols/declarations/mod.rs | 18 ++ .../src/interpreter/core_builtins.rs | 41 +-- 120 files changed, 3558 insertions(+), 2832 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/scalars/base64.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/scalars/hex.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/scalars/search.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/scalars/slashes.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/scalars/trim_case.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/explode.rs rename crates/elephc-magician/src/interpreter/builtins/{strings/gzip.rs => string/gzcompress.rs} (73%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs rename crates/elephc-magician/src/interpreter/builtins/{strings => string}/hash.rs (61%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/implode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/md5.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/sha1.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/ctype.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/explode.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzcompress.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzdeflate.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzinflate.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzuncompress.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_algos.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_copy.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_file.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_final.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_hmac.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_init.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_update.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/implode.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/md5.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/sha1.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_filters.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_transports.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_wrappers.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_is_local.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_supports_lock.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/grapheme_strrev.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/hash_context.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/html.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/nl2br.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/pad.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/repeat.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/replace.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/simple.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/split.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/substr.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/strings/url.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs index e26ad3ac9e..87c933f0e7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs +++ b/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs @@ -313,104 +313,3 @@ pub(in crate::interpreter) fn eval_array_merge_append_operand( } Ok(result) } - -/// Evaluates PHP `explode()` over separator and string expressions. -pub(in crate::interpreter) fn eval_builtin_explode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [separator, string] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let separator = eval_expr(separator, context, scope, values)?; - let string = eval_expr(string, context, scope, values)?; - eval_explode_result(separator, string, values) -} - -/// Splits one PHP byte string into an indexed array using a non-empty separator. -pub(in crate::interpreter) fn eval_explode_result( - separator: RuntimeCellHandle, - string: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let separator = values.string_bytes(separator)?; - if separator.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let string = values.string_bytes(string)?; - let mut result = values.array_new(0)?; - let mut start = 0; - let mut index = 0_i64; - while let Some(found) = eval_find_subslice(&string, &separator, start) { - result = eval_push_explode_segment(result, index, &string[start..found], values)?; - start = found + separator.len(); - index += 1; - } - eval_push_explode_segment(result, index, &string[start..], values) -} - -/// Appends one split segment to an indexed `explode()` result array. -pub(in crate::interpreter) fn eval_push_explode_segment( - array: RuntimeCellHandle, - index: i64, - segment: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(index)?; - let value = values.string_bytes_value(segment)?; - values.array_set(array, key, value) -} - -/// Finds `needle` inside `haystack` starting from one byte offset. -pub(in crate::interpreter) fn eval_find_subslice( - haystack: &[u8], - needle: &[u8], - start: usize, -) -> Option { - haystack - .get(start..)? - .windows(needle.len()) - .position(|window| window == needle) - .map(|position| position + start) -} - -/// Evaluates PHP `implode()` over separator and array expressions. -pub(in crate::interpreter) fn eval_builtin_implode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [separator, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let separator = eval_expr(separator, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_implode_result(separator, array, values) -} - -/// Joins array values in eval iteration order using PHP string conversion. -pub(in crate::interpreter) fn eval_implode_result( - separator: RuntimeCellHandle, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !values.is_array_like(array)? { - return Err(EvalStatus::RuntimeFatal); - } - let separator = values.string_bytes(separator)?; - let len = values.array_len(array)?; - let mut output = Vec::new(); - for position in 0..len { - if position > 0 { - output.extend_from_slice(&separator); - } - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let value = values.string_bytes(value)?; - output.extend_from_slice(&value); - } - values.string_bytes_value(&output) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 9feab858c6..02ccc72340 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -9,57 +9,10 @@ //! - Direct hooks preserve source-order evaluation in existing builtin helpers. //! - Hook variants remain static metadata referenced from per-builtin files. +use super::super::*; use super::super::super::{ - eval_builtin_base64_decode, eval_builtin_base64_encode, eval_builtin_bin2hex, - eval_builtin_chr, eval_builtin_count, - eval_builtin_crc32, eval_builtin_ctype, eval_builtin_explode, eval_builtin_gzip, - eval_builtin_hash_algos, eval_builtin_hash_copy, eval_builtin_hash_final, - eval_builtin_hash_init, eval_builtin_hash_one_shot, eval_builtin_hash_update, - eval_builtin_hex2bin, eval_builtin_implode, eval_builtin_number_format, eval_builtin_ord, - eval_builtin_printf, eval_builtin_slashes, eval_builtin_sprintf, eval_builtin_sscanf, - eval_builtin_str_repeat, eval_builtin_strlen, eval_builtin_vprintf, eval_builtin_vsprintf, - eval_builtin_url_decode, eval_builtin_url_encode, ElephcEvalContext, ElephcEvalScope, - EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, -}; -use super::super::{ - eval_builtin_abs, eval_builtin_acos, eval_builtin_array_aggregate, eval_builtin_array_call, - eval_builtin_array_flip, eval_builtin_array_key_exists, eval_builtin_array_keys, - eval_builtin_array_pad, eval_builtin_array_rand, eval_builtin_array_reverse, - eval_builtin_array_search, eval_builtin_array_slice, eval_builtin_array_unique, - eval_builtin_array_values, eval_builtin_asin, eval_builtin_atan, eval_builtin_atan2, - eval_builtin_boolval, eval_builtin_ceil, eval_builtin_clamp, eval_builtin_core_call, - eval_builtin_cos, eval_builtin_cosh, eval_builtin_deg2rad, eval_builtin_exp, - eval_builtin_fdiv, eval_builtin_filesystem_call, eval_builtin_floatval, - eval_builtin_floor, eval_builtin_fmod, eval_builtin_gettype, eval_builtin_hypot, - eval_builtin_intdiv, eval_builtin_intval, eval_builtin_is_array, eval_builtin_is_bool, - eval_builtin_is_double, eval_builtin_is_finite, eval_builtin_is_float, - eval_builtin_is_infinite, eval_builtin_is_int, eval_builtin_is_integer, - eval_builtin_is_iterable, eval_builtin_is_long, eval_builtin_is_nan, - eval_builtin_is_null, eval_builtin_is_numeric, eval_builtin_is_object, - eval_builtin_is_real, eval_builtin_is_resource, eval_builtin_is_scalar, - eval_builtin_is_string, eval_builtin_log, eval_builtin_log2, eval_builtin_log10, - eval_builtin_grapheme_strrev, eval_builtin_hash_equals, eval_builtin_html_entity, - eval_builtin_json_decode, eval_builtin_json_encode, eval_builtin_json_last_error, - eval_builtin_json_last_error_msg, eval_builtin_json_validate, eval_builtin_max, - eval_builtin_min, eval_builtin_mt_rand, eval_builtin_network_env_call, eval_builtin_nl2br, - eval_builtin_pi, eval_builtin_pow, eval_builtin_rad2deg, eval_builtin_rand, - eval_builtin_random_int, eval_builtin_range, eval_builtin_round, - eval_builtin_preg_match, eval_builtin_preg_match_all, eval_builtin_preg_replace, - eval_builtin_preg_replace_callback, eval_builtin_preg_split, eval_builtin_buffer_free, - eval_builtin_buffer_len, eval_builtin_buffer_new, eval_builtin_ptr, eval_builtin_ptr_get, - eval_builtin_ptr_is_null, eval_builtin_ptr_null, eval_builtin_ptr_offset, - eval_builtin_ptr_read16, eval_builtin_ptr_read32, eval_builtin_ptr_read8, - eval_builtin_ptr_read_string, eval_builtin_ptr_set, eval_builtin_ptr_sizeof, - eval_builtin_ptr_write16, eval_builtin_ptr_write32, eval_builtin_ptr_write8, - eval_builtin_ptr_write_string, eval_builtin_sin, eval_builtin_sinh, eval_builtin_str_pad, - eval_builtin_str_replace, eval_builtin_str_split, - eval_builtin_stream_bool_predicate, eval_builtin_stream_introspection, - eval_builtin_string_case, eval_builtin_string_compare, eval_builtin_string_position, - eval_builtin_string_search, eval_builtin_strrev, eval_builtin_strstr, eval_builtin_substr, - eval_builtin_strval, eval_builtin_substr_replace, eval_builtin_symbols_call, - eval_builtin_tan, eval_builtin_tanh, eval_builtin_time_call, eval_builtin_trim_like, - eval_builtin_ucwords, - eval_builtin_wordwrap, + eval_builtin_count, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, + RuntimeCellHandle, RuntimeValueOps, }; /// Direct expression-level dispatch hooks for migrated builtins. @@ -401,7 +354,13 @@ impl EvalDirectHook { Self::Cos => eval_builtin_cos(args, context, scope, values), Self::Cosh => eval_builtin_cosh(args, context, scope, values), Self::Crc32 => eval_builtin_crc32(args, context, scope, values), - Self::Ctype => eval_builtin_ctype(name, args, context, scope, values), + Self::Ctype => match name { + "ctype_alnum" => eval_builtin_ctype_alnum(args, context, scope, values), + "ctype_alpha" => eval_builtin_ctype_alpha(args, context, scope, values), + "ctype_digit" => eval_builtin_ctype_digit(args, context, scope, values), + "ctype_space" => eval_builtin_ctype_space(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Deg2rad => eval_builtin_deg2rad(args, context, scope, values), Self::Exp => eval_builtin_exp(args, context, scope, values), Self::Fdiv => eval_builtin_fdiv(args, context, scope, values), @@ -431,7 +390,13 @@ impl EvalDirectHook { Self::IsScalar => eval_builtin_is_scalar(args, context, scope, values), Self::IsString => eval_builtin_is_string(args, context, scope, values), Self::GraphemeStrrev => eval_builtin_grapheme_strrev(args, context, scope, values), - Self::Gzip => eval_builtin_gzip(name, args, context, scope, values), + Self::Gzip => match name { + "gzcompress" => eval_builtin_gzcompress(args, context, scope, values), + "gzdeflate" => eval_builtin_gzdeflate(args, context, scope, values), + "gzinflate" => eval_builtin_gzinflate(args, context, scope, values), + "gzuncompress" => eval_builtin_gzuncompress(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::HashAlgos => eval_builtin_hash_algos(args, values), Self::HashContext => match name { "hash_copy" => eval_builtin_hash_copy(args, context, scope, values), @@ -441,9 +406,23 @@ impl EvalDirectHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::HashEquals => eval_builtin_hash_equals(args, context, scope, values), - Self::HashOneShot => eval_builtin_hash_one_shot(name, args, context, scope, values), + Self::HashOneShot => match name { + "hash" => eval_builtin_hash(args, context, scope, values), + "hash_file" => eval_builtin_hash_file(args, context, scope, values), + "hash_hmac" => eval_builtin_hash_hmac(args, context, scope, values), + "md5" => eval_builtin_md5(args, context, scope, values), + "sha1" => eval_builtin_sha1(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Hex2Bin => eval_builtin_hex2bin(args, context, scope, values), - Self::HtmlEntity => eval_builtin_html_entity(name, args, context, scope, values), + Self::HtmlEntity => match name { + "html_entity_decode" => { + eval_builtin_html_entity_decode(args, context, scope, values) + } + "htmlentities" => eval_builtin_htmlentities(args, context, scope, values), + "htmlspecialchars" => eval_builtin_htmlspecialchars(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Intdiv => eval_builtin_intdiv(args, context, scope, values), Self::JsonDecode => eval_builtin_json_decode(args, context, scope, values), Self::JsonEncode => eval_builtin_json_encode(args, context, scope, values), @@ -494,29 +473,63 @@ impl EvalDirectHook { Self::PtrWriteString => eval_builtin_ptr_write_string(args, context, scope, values), Self::Sin => eval_builtin_sin(args, context, scope, values), Self::Sinh => eval_builtin_sinh(args, context, scope, values), - Self::Slashes => eval_builtin_slashes(name, args, context, scope, values), + Self::Slashes => match name { + "addslashes" => eval_builtin_addslashes(args, context, scope, values), + "stripslashes" => eval_builtin_stripslashes(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Sqrt => super::super::math::eval_builtin_sqrt(args, context, scope, values), Self::Sprintf => eval_builtin_sprintf(args, context, scope, values), Self::Sscanf => eval_builtin_sscanf(args, context, scope, values), - Self::StringCase => eval_builtin_string_case(name, args, context, scope, values), - Self::StringCompare => eval_builtin_string_compare(name, args, context, scope, values), - Self::StringPosition => { - eval_builtin_string_position(name, args, context, scope, values) - } - Self::StringSearch => eval_builtin_string_search(name, args, context, scope, values), + Self::StringCase => match name { + "lcfirst" => eval_builtin_lcfirst(args, context, scope, values), + "strtolower" => eval_builtin_strtolower(args, context, scope, values), + "strtoupper" => eval_builtin_strtoupper(args, context, scope, values), + "ucfirst" => eval_builtin_ucfirst(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StringCompare => match name { + "strcasecmp" => eval_builtin_strcasecmp(args, context, scope, values), + "strcmp" => eval_builtin_strcmp(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StringPosition => match name { + "strpos" => eval_builtin_strpos(args, context, scope, values), + "strrpos" => eval_builtin_strrpos(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StringSearch => match name { + "str_contains" => eval_builtin_str_contains(args, context, scope, values), + "str_ends_with" => eval_builtin_str_ends_with(args, context, scope, values), + "str_starts_with" => eval_builtin_str_starts_with(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::StringSplitJoin => match name { "explode" => eval_builtin_explode(args, context, scope, values), "implode" => eval_builtin_implode(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), }, - Self::StreamBoolPredicate => { - eval_builtin_stream_bool_predicate(name, args, context, scope, values) - } - Self::StreamIntrospection => { - eval_builtin_stream_introspection(name, args, context, values) - } + Self::StreamBoolPredicate => match name { + "stream_is_local" => eval_builtin_stream_is_local(args, context, scope, values), + "stream_supports_lock" => { + eval_builtin_stream_supports_lock(args, context, scope, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::StreamIntrospection => match name { + "stream_get_filters" => eval_builtin_stream_get_filters(args, context, values), + "stream_get_transports" => { + eval_builtin_stream_get_transports(args, context, values) + } + "stream_get_wrappers" => eval_builtin_stream_get_wrappers(args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::StrPad => eval_builtin_str_pad(args, context, scope, values), - Self::StrReplace => eval_builtin_str_replace(name, args, context, scope, values), + Self::StrReplace => match name { + "str_ireplace" => eval_builtin_str_ireplace(args, context, scope, values), + "str_replace" => eval_builtin_str_replace(name, args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::StrSplit => eval_builtin_str_split(args, context, scope, values), Self::Strlen => eval_builtin_strlen(args, context, scope, values), Self::StrRepeat => eval_builtin_str_repeat(args, context, scope, values), @@ -529,14 +542,28 @@ impl EvalDirectHook { Self::Tan => eval_builtin_tan(args, context, scope, values), Self::Tanh => eval_builtin_tanh(args, context, scope, values), Self::Time => eval_builtin_time_call(name, args, context, scope, values), - Self::TrimLike => eval_builtin_trim_like(name, args, context, scope, values), + Self::TrimLike => match name { + "chop" => eval_builtin_chop(args, context, scope, values), + "ltrim" => eval_builtin_ltrim(args, context, scope, values), + "rtrim" => eval_builtin_rtrim(args, context, scope, values), + "trim" => eval_builtin_trim(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Ucwords => eval_builtin_ucwords(args, context, scope, values), Self::Vprintf => eval_builtin_vprintf(args, context, scope, values), Self::Vsprintf => eval_builtin_vsprintf(args, context, scope, values), Self::Nl2br => eval_builtin_nl2br(args, context, scope, values), Self::Wordwrap => eval_builtin_wordwrap(args, context, scope, values), - Self::UrlDecode => eval_builtin_url_decode(name, args, context, scope, values), - Self::UrlEncode => eval_builtin_url_encode(name, args, context, scope, values), + Self::UrlDecode => match name { + "rawurldecode" => eval_builtin_rawurldecode(args, context, scope, values), + "urldecode" => eval_builtin_urldecode(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + Self::UrlEncode => match name { + "rawurlencode" => eval_builtin_rawurlencode(args, context, scope, values), + "urlencode" => eval_builtin_urlencode(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + }, } } } diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 89265de98b..4524e382ea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -10,57 +10,9 @@ //! parameter order. //! - Runtime-cell coercions stay in the existing builtin result helpers. +use super::super::*; use super::super::super::{ - eval_count_result, eval_ord_result, ElephcEvalContext, EvalStatus, RuntimeCellHandle, - RuntimeValueOps, -}; -use super::super::{ - eval_abs_result, eval_acos_result, eval_array_aggregate_result, eval_array_flip_result, - eval_array_keys_result, - eval_array_mutating_values_result, eval_array_non_mutating_values_result, - eval_array_pad_result, eval_array_rand_result, eval_array_reverse_result, - eval_array_search_result, eval_array_slice_result, eval_array_unique_result, - eval_array_values_result, eval_asin_result, eval_atan2_result, eval_atan_result, - eval_base64_decode_result, eval_base64_encode_result, - eval_bin2hex_result, eval_boolval_result, eval_ceil_result, eval_chr_result, - eval_clamp_result, eval_cos_result, eval_cosh_result, eval_deg2rad_result, - eval_core_values_result, eval_crc32_result, eval_ctype_result, - eval_exp_result, eval_fdiv_result, eval_filesystem_values_result, eval_floatval_result, - eval_floor_result, eval_fmod_result, - eval_gettype_result, eval_hypot_result, eval_intdiv_result, eval_intval_result, - eval_is_array_result, eval_is_bool_result, - eval_is_double_result, eval_is_finite_result, eval_is_float_result, - eval_is_infinite_result, eval_is_int_result, eval_is_integer_result, - eval_is_iterable_result, eval_is_long_result, eval_is_nan_result, - eval_is_null_result, eval_is_numeric_result, eval_is_object_result, - eval_is_real_result, eval_is_resource_result, eval_is_scalar_result, - eval_is_string_result, - eval_grapheme_strrev_result, eval_gzip_result, eval_hash_equals_result, - eval_hash_one_shot_result, eval_hex2bin_result, eval_html_entity_result, - eval_json_decode_values_result, eval_json_encode_values_result, eval_json_last_error_msg_result, - eval_json_last_error_result, eval_json_validate_values_result, eval_log2_result, - eval_log10_result, eval_log_result, eval_max_result, eval_min_result, - eval_mt_rand_values_result, eval_network_env_values_result, eval_nl2br_result, - eval_pi_result, eval_pow_result, eval_preg_match_all_values_result, eval_preg_match_values_result, - eval_preg_replace_callback_values_result, eval_preg_replace_values_result, - eval_preg_split_values_result, eval_printf_result, eval_rad2deg_result, eval_rand_values_result, - eval_random_int_values_result, eval_range_result, eval_round_result, eval_settype_values_result, - eval_sin_result, eval_sinh_result, eval_slashes_result, eval_sprintf_result, eval_sqrt_result, - eval_sscanf_values_result, - eval_str_pad_result, eval_str_replace_result, eval_str_repeat_result, - eval_str_split_result, eval_stream_bool_predicate_result, eval_stream_introspection_result, - eval_string_case_result, eval_string_compare_result, eval_string_position_result, - eval_string_search_result, eval_strstr_result, eval_strval_result, - eval_substr_replace_result, eval_substr_result, eval_buffer_free_values_result, - eval_buffer_len_values_result, eval_buffer_new_values_result, eval_ptr_get_values_result, - eval_ptr_is_null_values_result, eval_ptr_null_values_result, eval_ptr_offset_values_result, - eval_ptr_read16_values_result, eval_ptr_read32_values_result, eval_ptr_read8_values_result, - eval_ptr_read_string_values_result, eval_ptr_set_values_result, eval_ptr_sizeof_values_result, - eval_ptr_values_result, eval_ptr_write16_values_result, eval_ptr_write32_values_result, - eval_ptr_write8_values_result, eval_ptr_write_string_values_result, eval_symbols_values_result, - eval_tan_result, eval_tanh_result, eval_time_values_result, eval_trim_like_result, - eval_ucwords_result, eval_url_decode_result, eval_url_encode_result, eval_vprintf_result, - eval_vsprintf_result, eval_wordwrap_result, + eval_count_result, ElephcEvalContext, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::arity::{one_arg, three_args, two_args}; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; @@ -435,8 +387,12 @@ impl EvalValuesHook { Self::Cos => one_arg(evaluated_args, values, eval_cos_result), Self::Cosh => one_arg(evaluated_args, values, eval_cosh_result), Self::Crc32 => one_arg(evaluated_args, values, eval_crc32_result), - Self::Ctype => one_arg(evaluated_args, values, |value, values| { - eval_ctype_result(name, value, values) + Self::Ctype => one_arg(evaluated_args, values, |value, values| match name { + "ctype_alnum" => eval_ctype_alnum_result(value, values), + "ctype_alpha" => eval_ctype_alpha_result(value, values), + "ctype_digit" => eval_ctype_digit_result(value, values), + "ctype_space" => eval_ctype_space_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), }), Self::Deg2rad => one_arg(evaluated_args, values, eval_deg2rad_result), Self::Exp => one_arg(evaluated_args, values, eval_exp_result), @@ -469,14 +425,30 @@ impl EvalValuesHook { Self::IsScalar => one_arg(evaluated_args, values, eval_is_scalar_result), Self::IsString => one_arg(evaluated_args, values, eval_is_string_result), Self::GraphemeStrrev => one_arg(evaluated_args, values, eval_grapheme_strrev_result), - Self::Gzip => eval_gzip_result(name, evaluated_args, values), + Self::Gzip => match name { + "gzcompress" => eval_gzcompress_result(evaluated_args, values), + "gzdeflate" => eval_gzdeflate_result(evaluated_args, values), + "gzinflate" => eval_gzinflate_result(evaluated_args, values), + "gzuncompress" => eval_gzuncompress_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::HashAlgos => eval_hash_algos_values(evaluated_args, values), Self::HashContext => eval_hash_context_values(name, evaluated_args, context, values), Self::HashEquals => two_args(evaluated_args, values, eval_hash_equals_result), - Self::HashOneShot => eval_hash_one_shot_result(name, evaluated_args, values), + Self::HashOneShot => match name { + "hash" => eval_hash_result(evaluated_args, values), + "hash_file" => eval_hash_file_result(evaluated_args, values), + "hash_hmac" => eval_hash_hmac_result(evaluated_args, values), + "md5" => eval_md5_result(evaluated_args, values), + "sha1" => eval_sha1_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::Hex2Bin => one_arg(evaluated_args, values, eval_hex2bin_result), - Self::HtmlEntity => one_arg(evaluated_args, values, |value, values| { - eval_html_entity_result(name, value, values) + Self::HtmlEntity => one_arg(evaluated_args, values, |value, values| match name { + "html_entity_decode" => eval_html_entity_decode_result(value, values), + "htmlentities" => eval_htmlentities_result(value, values), + "htmlspecialchars" => eval_htmlspecialchars_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), }), Self::Intdiv => two_args(evaluated_args, values, eval_intdiv_result), Self::JsonDecode => eval_json_decode_values_result(evaluated_args, context, values), @@ -552,33 +524,61 @@ impl EvalValuesHook { Self::Settype => eval_settype_values_result(evaluated_args, values), Self::Sin => one_arg(evaluated_args, values, eval_sin_result), Self::Sinh => one_arg(evaluated_args, values, eval_sinh_result), - Self::Slashes => one_arg(evaluated_args, values, |value, values| { - eval_slashes_result(name, value, values) + Self::Slashes => one_arg(evaluated_args, values, |value, values| match name { + "addslashes" => eval_addslashes_result(value, values), + "stripslashes" => eval_stripslashes_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), }), Self::Sprintf => eval_sprintf_result(evaluated_args, values), Self::Sqrt => one_arg(evaluated_args, values, eval_sqrt_result), Self::Sscanf => eval_sscanf_values_result(evaluated_args, values), - Self::StringCase => one_arg(evaluated_args, values, |value, values| { - eval_string_case_result(name, value, values) + Self::StringCase => one_arg(evaluated_args, values, |value, values| match name { + "lcfirst" => eval_lcfirst_result(value, values), + "strtolower" => eval_strtolower_result(value, values), + "strtoupper" => eval_strtoupper_result(value, values), + "ucfirst" => eval_ucfirst_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), }), Self::StringCompare => two_args(evaluated_args, values, |left, right, values| { - eval_string_compare_result(name, left, right, values) + match name { + "strcasecmp" => eval_strcasecmp_result(left, right, values), + "strcmp" => eval_strcmp_result(left, right, values), + _ => Err(EvalStatus::RuntimeFatal), + } }), Self::StringPosition => two_args(evaluated_args, values, |haystack, needle, values| { - eval_string_position_result(name, haystack, needle, values) + match name { + "strpos" => eval_strpos_result(haystack, needle, values), + "strrpos" => eval_strrpos_result(haystack, needle, values), + _ => Err(EvalStatus::RuntimeFatal), + } }), Self::StringSearch => two_args(evaluated_args, values, |haystack, needle, values| { - eval_string_search_result(name, haystack, needle, values) + match name { + "str_contains" => eval_str_contains_result(haystack, needle, values), + "str_ends_with" => eval_str_ends_with_result(haystack, needle, values), + "str_starts_with" => eval_str_starts_with_result(haystack, needle, values), + _ => Err(EvalStatus::RuntimeFatal), + } }), Self::StringSplitJoin => eval_string_split_join_values(name, evaluated_args, values), Self::StreamBoolPredicate => one_arg(evaluated_args, values, |stream, values| { - eval_stream_bool_predicate_result(name, stream, values) + match name { + "stream_is_local" => eval_stream_is_local_result(stream, values), + "stream_supports_lock" => eval_stream_supports_lock_result(stream, values), + _ => Err(EvalStatus::RuntimeFatal), + } }), Self::StreamIntrospection => { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); } - eval_stream_introspection_result(name, context, values) + match name { + "stream_get_filters" => eval_stream_get_filters_result(context, values), + "stream_get_transports" => eval_stream_get_transports_result(context, values), + "stream_get_wrappers" => eval_stream_get_wrappers_result(context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } Self::StrPad => match evaluated_args { [value, length] => eval_str_pad_result(*value, *length, None, None, values), @@ -590,9 +590,19 @@ impl EvalValuesHook { } _ => Err(EvalStatus::RuntimeFatal), }, - Self::StrReplace => three_args(evaluated_args, values, |search, replace, subject, values| { - eval_str_replace_result(name, search, replace, subject, values) - }), + Self::StrReplace => { + three_args(evaluated_args, values, |search, replace, subject, values| { + match name { + "str_ireplace" => { + eval_str_ireplace_result(search, replace, subject, values) + } + "str_replace" => { + eval_str_replace_result(name, search, replace, subject, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } + }) + } Self::StrSplit => match evaluated_args { [value] => eval_str_split_result(*value, None, values), [value, length] => eval_str_split_result(*value, Some(*length), values), @@ -610,7 +620,7 @@ impl EvalValuesHook { Self::Strval => one_arg(evaluated_args, values, |value, values| { eval_strval_result(value, context, values) }), - Self::Strrev => one_arg(evaluated_args, values, |value, values| values.strrev(value)), + Self::Strrev => one_arg(evaluated_args, values, eval_strrev_result), Self::Strstr => match evaluated_args { [haystack, needle] => eval_strstr_result(*haystack, *needle, false, values), [haystack, needle, before_needle] => { @@ -639,9 +649,15 @@ impl EvalValuesHook { Self::Tan => one_arg(evaluated_args, values, eval_tan_result), Self::Tanh => one_arg(evaluated_args, values, eval_tanh_result), Self::Time => eval_time_values_result(name, evaluated_args, context, values), - Self::TrimLike => match evaluated_args { - [value] => eval_trim_like_result(name, *value, None, values), - [value, mask] => eval_trim_like_result(name, *value, Some(*mask), values), + Self::TrimLike => match (name, evaluated_args) { + ("chop", [value]) => eval_chop_result(*value, None, values), + ("chop", [value, mask]) => eval_chop_result(*value, Some(*mask), values), + ("ltrim", [value]) => eval_ltrim_result(*value, None, values), + ("ltrim", [value, mask]) => eval_ltrim_result(*value, Some(*mask), values), + ("rtrim", [value]) => eval_rtrim_result(*value, None, values), + ("rtrim", [value, mask]) => eval_rtrim_result(*value, Some(*mask), values), + ("trim", [value]) => eval_trim_result(*value, None, values), + ("trim", [value, mask]) => eval_trim_result(*value, Some(*mask), values), _ => Err(EvalStatus::RuntimeFatal), }, Self::Ucwords => match evaluated_args { @@ -674,11 +690,15 @@ impl EvalValuesHook { ), _ => Err(EvalStatus::RuntimeFatal), }, - Self::UrlDecode => one_arg(evaluated_args, values, |value, values| { - eval_url_decode_result(name, value, values) + Self::UrlDecode => one_arg(evaluated_args, values, |value, values| match name { + "rawurldecode" => eval_rawurldecode_result(value, values), + "urldecode" => eval_urldecode_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), }), - Self::UrlEncode => one_arg(evaluated_args, values, |value, values| { - eval_url_encode_result(name, value, values) + Self::UrlEncode => one_arg(evaluated_args, values, |value, values| match name { + "rawurlencode" => eval_rawurlencode_result(value, values), + "urlencode" => eval_urlencode_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), }), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 5191eaed65..bf2bcedd82 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -32,7 +32,6 @@ mod scalars; mod spl_autoload; mod spec; mod string; -mod strings; mod symbols; mod time; mod types; @@ -52,7 +51,7 @@ pub(super) use regex::*; pub(super) use registry::*; pub(super) use scalars::*; pub(super) use spl_autoload::*; -pub(super) use strings::*; +pub(super) use string::*; pub(super) use symbols::*; pub(super) use time::*; pub(super) use types::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/base64.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/base64.rs deleted file mode 100644 index 2ce773919b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/base64.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! Purpose: -//! Base64 encoding and decoding builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::scalars` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. - -use super::super::super::*; - -/// Evaluates PHP's `base64_encode(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_base64_encode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_encode_result(value, values) -} - -/// Converts one eval value through PHP string conversion and returns Base64 text. -pub(in crate::interpreter) fn eval_base64_encode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); - const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for chunk in bytes.chunks(3) { - let first = chunk[0]; - let second = chunk.get(1).copied().unwrap_or(0); - let third = chunk.get(2).copied().unwrap_or(0); - output.push(ALPHABET[(first >> 2) as usize] as char); - output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); - if chunk.len() > 1 { - output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); - } else { - output.push('='); - } - if chunk.len() > 2 { - output.push(ALPHABET[(third & 0x3f) as usize] as char); - } else { - output.push('='); - } - } - values.string(&output) -} - -/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_base64_decode( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_base64_decode_result(value, values) -} - -/// Converts one eval value through PHP string conversion and decodes Base64 bytes. -pub(in crate::interpreter) fn eval_base64_decode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let input = values.string_bytes(value)?; - let mut output = Vec::with_capacity((input.len() / 4) * 3); - let mut quartet = Vec::with_capacity(4); - for byte in input { - if byte.is_ascii_whitespace() { - continue; - } - if byte == b'=' { - quartet.push(None); - } else if let Some(value) = eval_base64_decode_sextet(byte) { - quartet.push(Some(value)); - } else { - continue; - } - if quartet.len() == 4 { - eval_push_base64_decoded_quartet(&quartet, &mut output); - quartet.clear(); - } - } - if !quartet.is_empty() { - while quartet.len() < 4 { - quartet.push(None); - } - eval_push_base64_decoded_quartet(&quartet, &mut output); - } - values.string_bytes_value(&output) -} - -/// Returns the six-bit Base64 value for one encoded byte. -pub(in crate::interpreter) fn eval_base64_decode_sextet(byte: u8) -> Option { - match byte { - b'A'..=b'Z' => Some(byte - b'A'), - b'a'..=b'z' => Some(byte - b'a' + 26), - b'0'..=b'9' => Some(byte - b'0' + 52), - b'+' => Some(62), - b'/' => Some(63), - _ => None, - } -} - -/// Appends decoded bytes for one padded or unpadded Base64 quartet. -pub(in crate::interpreter) fn eval_push_base64_decoded_quartet( - quartet: &[Option], - output: &mut Vec, -) { - let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { - return; - }; - output.push((first << 2) | (second >> 4)); - let Some(third) = quartet[2] else { - return; - }; - output.push(((second & 0x0f) << 4) | (third >> 2)); - let Some(fourth) = quartet[3] else { - return; - }; - output.push(((third & 0x03) << 6) | fourth); -} diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/hex.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/hex.rs deleted file mode 100644 index 1f906d6476..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/hex.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Purpose: -//! Hex encoding and decoding builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::scalars` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. - -use super::super::super::*; - -/// Evaluates PHP's `bin2hex(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_bin2hex( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_bin2hex_result(value, values) -} - -/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. -pub(in crate::interpreter) fn eval_bin2hex_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.string(&eval_lower_hex_bytes(&bytes)) -} - -/// Converts bytes to lowercase hexadecimal text. -pub(in crate::interpreter) fn eval_lower_hex_bytes(bytes: &[u8]) -> String { - let mut output = String::with_capacity(bytes.len() * 2); - const HEX: &[u8; 16] = b"0123456789abcdef"; - for byte in bytes { - output.push(HEX[(byte >> 4) as usize] as char); - output.push(HEX[(byte & 0x0f) as usize] as char); - } - output -} - -/// Evaluates PHP's `hex2bin(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_hex2bin( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_hex2bin_result(value, values) -} - -/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. -pub(in crate::interpreter) fn eval_hex2bin_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - if bytes.len() % 2 != 0 { - values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; - return values.bool_value(false); - } - let mut output = Vec::with_capacity(bytes.len() / 2); - for pair in bytes.chunks_exact(2) { - let Some(high) = eval_hex_nibble(pair[0]) else { - values.warning(HEX2BIN_INVALID_WARNING)?; - return values.bool_value(false); - }; - let Some(low) = eval_hex_nibble(pair[1]) else { - values.warning(HEX2BIN_INVALID_WARNING)?; - return values.bool_value(false); - }; - output.push((high << 4) | low); - } - values.string_bytes_value(&output) -} - -/// Returns the four-bit value for one hexadecimal byte. -pub(in crate::interpreter) fn eval_hex_nibble(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs index 050b3f3d3b..faa83bd849 100644 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/mod.rs @@ -1,27 +1,17 @@ //! Purpose: -//! Groups scalar, encoding, math, type, string-search, and trim/case eval builtins. -//! Each submodule owns one PHP builtin family while this module re-exports the callable surface. +//! Groups scalar helper functions that are still shared by eval builtins. //! //! Called from: //! - `crate::interpreter::builtins` re-exports used by core call dispatch. //! //! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime behavior. +//! - PHP-visible string builtin implementations live in `builtins::string` leaf +//! files; this module keeps cross-domain scalar helpers only. -mod base64; mod common; -mod hex; mod math; -mod search; -mod slashes; -mod trim_case; mod types; -pub(in crate::interpreter) use base64::*; pub(in crate::interpreter) use common::*; -pub(in crate::interpreter) use hex::*; pub(in crate::interpreter) use math::*; -pub(in crate::interpreter) use search::*; -pub(in crate::interpreter) use slashes::*; -pub(in crate::interpreter) use trim_case::*; pub(in crate::interpreter) use types::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/search.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/search.rs deleted file mode 100644 index 7ed21663d1..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/search.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! Purpose: -//! String comparison, search, position, and strstr helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::scalars` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP's `hash_equals(...)` over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_hash_equals( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [known, user] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let known = eval_expr(known, context, scope, values)?; - let user = eval_expr(user, context, scope, values)?; - eval_hash_equals_result(known, user, values) -} - -/// Compares two converted strings with PHP `hash_equals()` semantics. -pub(in crate::interpreter) fn eval_hash_equals_result( - known: RuntimeCellHandle, - user: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let known = values.string_bytes(known)?; - let user = values.string_bytes(user)?; - if known.len() != user.len() { - return values.bool_value(false); - } - let mut diff = 0u8; - for (known, user) in known.iter().zip(user.iter()) { - diff |= known ^ user; - } - values.bool_value(diff == 0) -} - -/// Evaluates PHP string comparison builtins over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_string_compare( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_string_compare_result(name, left, right, values) -} - -/// Compares two converted strings and returns -1, 0, or 1. -pub(in crate::interpreter) fn eval_string_compare_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut left = values.string_bytes(left)?; - let mut right = values.string_bytes(right)?; - match name { - "strcmp" => {} - "strcasecmp" => { - left.make_ascii_lowercase(); - right.make_ascii_lowercase(); - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - let result = match left.cmp(&right) { - std::cmp::Ordering::Less => -1, - std::cmp::Ordering::Equal => 0, - std::cmp::Ordering::Greater => 1, - }; - values.int(result) -} - -/// Evaluates PHP's byte-string search predicates over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_string_search( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_string_search_result(name, haystack, needle, values) -} - -/// Checks one converted haystack for one converted needle using PHP byte-string semantics. -pub(in crate::interpreter) fn eval_string_search_result( - name: &str, - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let matched = match name { - "str_contains" => { - needle.is_empty() - || haystack - .windows(needle.len()) - .any(|window| window == needle) - } - "str_starts_with" => haystack.starts_with(&needle), - "str_ends_with" => haystack.ends_with(&needle), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(matched) -} - -/// Evaluates PHP byte-string position builtins over two eval expressions. -pub(in crate::interpreter) fn eval_builtin_string_position( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [haystack, needle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_string_position_result(name, haystack, needle, values) -} - -/// Returns the first or last byte offset of a converted needle, or PHP `false`. -pub(in crate::interpreter) fn eval_string_position_result( - name: &str, - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let position = match name { - "strpos" if needle.is_empty() => Some(0), - "strpos" => haystack - .windows(needle.len()) - .position(|window| window == needle), - "strrpos" if needle.is_empty() => Some(haystack.len()), - "strrpos" => haystack - .windows(needle.len()) - .rposition(|window| window == needle), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - match position { - Some(position) => { - let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(position) - } - None => values.bool_value(false), - } -} - -/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. -pub(in crate::interpreter) fn eval_builtin_strstr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [haystack, needle] => { - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - eval_strstr_result(haystack, needle, false, values) - } - [haystack, needle, before_needle] => { - let haystack = eval_expr(haystack, context, scope, values)?; - let needle = eval_expr(needle, context, scope, values)?; - let before_needle = eval_expr(before_needle, context, scope, values)?; - let before_needle = values.truthy(before_needle)?; - eval_strstr_result(haystack, needle, before_needle, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. -pub(in crate::interpreter) fn eval_strstr_result( - haystack: RuntimeCellHandle, - needle: RuntimeCellHandle, - before_needle: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let haystack = values.string_bytes(haystack)?; - let needle = values.string_bytes(needle)?; - let position = if needle.is_empty() { - Some(0) - } else { - eval_find_subslice(&haystack, &needle, 0) - }; - let Some(position) = position else { - return values.bool_value(false); - }; - let result = if before_needle { - &haystack[..position] - } else { - &haystack[position..] - }; - values.string_bytes_value(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/slashes.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/slashes.rs deleted file mode 100644 index 430961ef3e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/slashes.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! Purpose: -//! Slash escaping and unescaping builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::scalars` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. - -use super::super::super::*; - -/// Evaluates PHP's `addslashes(...)` or `stripslashes(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_slashes( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_slashes_result(name, value, values) -} - -/// Applies PHP byte-string escaping or unescaping for addslashes/stripslashes. -pub(in crate::interpreter) fn eval_slashes_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "addslashes" => eval_addslashes_result(value, values), - "stripslashes" => eval_stripslashes_result(value, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. -pub(in crate::interpreter) fn eval_addslashes_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - for byte in bytes { - match byte { - 0 => output.extend_from_slice(b"\\0"), - b'\'' | b'"' | b'\\' => { - output.push(b'\\'); - output.push(byte); - } - _ => output.push(byte), - } - } - values.string_bytes_value(&output) -} - -/// Removes backslash quoting using PHP `stripslashes()` byte semantics. -pub(in crate::interpreter) fn eval_stripslashes_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'\\' { - index += 1; - if let Some(byte) = bytes.get(index).copied() { - output.push(if byte == b'0' { 0 } else { byte }); - index += 1; - } - } else { - output.push(bytes[index]); - index += 1; - } - } - values.string_bytes_value(&output) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/trim_case.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/trim_case.rs deleted file mode 100644 index 3e6209ce77..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/trim_case.rs +++ /dev/null @@ -1,303 +0,0 @@ -//! Purpose: -//! Trim, case conversion, ucwords, and wordwrap helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::scalars` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and all PHP coercions flow through `RuntimeValueOps`. - -use super::super::super::*; -use super::*; - -pub(in crate::interpreter) const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; - -/// Evaluates PHP trim-like string builtins over one eval expression and optional mask. -pub(in crate::interpreter) fn eval_builtin_trim_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_trim_like_result(name, value, None, values) - } - [value, mask] => { - let value = eval_expr(value, context, scope, values)?; - let mask = eval_expr(mask, context, scope, values)?; - eval_trim_like_result(name, value, Some(mask), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Trims one converted string using PHP's default mask or a caller-provided byte mask. -pub(in crate::interpreter) fn eval_trim_like_result( - name: &str, - value: RuntimeCellHandle, - mask: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let explicit_mask; - let trim_mask = if let Some(mask) = mask { - explicit_mask = values.string_bytes(mask)?; - explicit_mask.as_slice() - } else { - PHP_DEFAULT_TRIM_MASK - }; - - let mut start = 0; - let mut end = bytes.len(); - if matches!(name, "trim" | "ltrim") { - while start < end && trim_mask.contains(&bytes[start]) { - start += 1; - } - } - if matches!(name, "trim" | "rtrim" | "chop") { - while end > start && trim_mask.contains(&bytes[end - 1]) { - end -= 1; - } - } - if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { - return Err(EvalStatus::UnsupportedConstruct); - } - - let value = - String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(&value) -} - -/// Evaluates PHP ASCII case-conversion string builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_string_case( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_string_case_result(name, value, values) -} - -/// Converts one eval value through PHP string conversion and ASCII case mapping. -pub(in crate::interpreter) fn eval_string_case_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bytes = values.string_bytes(value)?; - match name { - "strtolower" => { - for byte in &mut bytes { - if byte.is_ascii_uppercase() { - *byte += b'a' - b'A'; - } - } - } - "strtoupper" => { - for byte in &mut bytes { - if byte.is_ascii_lowercase() { - *byte -= b'a' - b'A'; - } - } - } - "ucfirst" => { - if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { - bytes[0] -= b'a' - b'A'; - } - } - "lcfirst" => { - if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { - bytes[0] += b'a' - b'A'; - } - } - _ => return Err(EvalStatus::UnsupportedConstruct), - } - let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(&value) -} - -/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. -pub(in crate::interpreter) fn eval_builtin_ucwords( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_ucwords_result(value, None, values) - } - [value, separators] => { - let value = eval_expr(value, context, scope, values)?; - let separators = eval_expr(separators, context, scope, values)?; - eval_ucwords_result(value, Some(separators), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. -pub(in crate::interpreter) fn eval_ucwords_result( - value: RuntimeCellHandle, - separators: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut bytes = values.string_bytes(value)?; - let separators = match separators { - Some(separators) => values.string_bytes(separators)?, - None => b" \t\r\n\x0c\x0b".to_vec(), - }; - let mut word_start = true; - for byte in &mut bytes { - if separators.contains(byte) { - word_start = true; - } else if word_start { - if byte.is_ascii_lowercase() { - *byte -= b'a' - b'A'; - } - word_start = false; - } - } - values.string_bytes_value(&bytes) -} - -/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. -pub(in crate::interpreter) fn eval_builtin_wordwrap( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_wordwrap_result(value, None, None, None, values) - } - [value, width] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - eval_wordwrap_result(value, Some(width), None, None, values) - } - [value, width, break_string] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - let break_string = eval_expr(break_string, context, scope, values)?; - eval_wordwrap_result(value, Some(width), Some(break_string), None, values) - } - [value, width, break_string, cut] => { - let value = eval_expr(value, context, scope, values)?; - let width = eval_expr(width, context, scope, values)?; - let break_string = eval_expr(break_string, context, scope, values)?; - let cut = eval_expr(cut, context, scope, values)?; - eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Wraps a byte string at PHP word boundaries and preserves existing newlines. -pub(in crate::interpreter) fn eval_wordwrap_result( - value: RuntimeCellHandle, - width: Option, - break_string: Option, - cut: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let width = match width { - Some(width) => eval_int_value(width, values)?, - None => 75, - }; - let break_string = match break_string { - Some(break_string) => values.string_bytes(break_string)?, - None => b"\n".to_vec(), - }; - if break_string.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let cut = match cut { - Some(cut) => values.truthy(cut)?, - None => false, - }; - if width == 0 && cut { - return Err(EvalStatus::RuntimeFatal); - } - if bytes.is_empty() { - return values.string_bytes_value(&bytes); - } - let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); - values.string_bytes_value(&output) -} - -/// Applies the core PHP word-wrap scan over already converted byte slices. -pub(in crate::interpreter) fn eval_wordwrap_bytes( - bytes: &[u8], - width: i64, - break_string: &[u8], - cut: bool, -) -> Vec { - if width < 0 && cut { - let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); - for byte in bytes { - output.extend_from_slice(break_string); - output.push(*byte); - } - return output; - } - - let width = width.max(0) as usize; - let mut output = Vec::with_capacity(bytes.len()); - let mut line_start = 0; - let mut last_space = None; - let mut index = 0; - while index < bytes.len() { - match bytes[index] { - b'\n' => { - output.extend_from_slice(&bytes[line_start..=index]); - index += 1; - line_start = index; - last_space = None; - } - b' ' => { - if index.saturating_sub(line_start) >= width { - output.extend_from_slice(&bytes[line_start..index]); - output.extend_from_slice(break_string); - index += 1; - line_start = index; - last_space = None; - } else { - last_space = Some(index); - index += 1; - } - } - _ if index.saturating_sub(line_start) >= width => { - if let Some(space) = last_space { - output.extend_from_slice(&bytes[line_start..space]); - output.extend_from_slice(break_string); - line_start = space + 1; - last_space = None; - } else if cut && width > 0 { - output.extend_from_slice(&bytes[line_start..index]); - output.extend_from_slice(break_string); - line_start = index; - } else { - index += 1; - } - } - _ => { - index += 1; - } - } - } - output.extend_from_slice(&bytes[line_start..]); - output -} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs b/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs index 7e148bc672..e28e6bd608 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing slash escaping hook. +//! - Runtime dispatch is declared here and implemented through the existing slash escaping hook. eval_builtin! { name: "addslashes", @@ -14,3 +14,39 @@ eval_builtin! { direct: Slashes, values: Slashes, } + +use super::super::super::*; + +/// Evaluates PHP `addslashes(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_addslashes( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_addslashes_result(value, values) +} + +/// Escapes NUL, quotes, and backslashes using PHP `addslashes()` byte semantics. +pub(in crate::interpreter) fn eval_addslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + 0 => output.extend_from_slice(b"\\0"), + b'\'' | b'"' | b'\\' => { + output.push(b'\\'); + output.push(byte); + } + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs b/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs index 153b535aea..d2326aba45 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing Base64 decode hook. +//! - Runtime dispatch is declared here and implemented through the existing Base64 decode hook. eval_builtin! { name: "base64_decode", @@ -14,3 +14,83 @@ eval_builtin! { direct: Base64Decode, values: Base64Decode, } + +use super::super::super::*; + +/// Evaluates PHP's one-argument `base64_decode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_base64_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_decode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes Base64 bytes. +pub(in crate::interpreter) fn eval_base64_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let input = values.string_bytes(value)?; + let mut output = Vec::with_capacity((input.len() / 4) * 3); + let mut quartet = Vec::with_capacity(4); + for byte in input { + if byte.is_ascii_whitespace() { + continue; + } + if byte == b'=' { + quartet.push(None); + } else if let Some(value) = eval_base64_decode_sextet(byte) { + quartet.push(Some(value)); + } else { + continue; + } + if quartet.len() == 4 { + eval_push_base64_decoded_quartet(&quartet, &mut output); + quartet.clear(); + } + } + if !quartet.is_empty() { + while quartet.len() < 4 { + quartet.push(None); + } + eval_push_base64_decoded_quartet(&quartet, &mut output); + } + values.string_bytes_value(&output) +} + +/// Returns the six-bit Base64 value for one encoded byte. +pub(in crate::interpreter) fn eval_base64_decode_sextet(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(byte - b'A'), + b'a'..=b'z' => Some(byte - b'a' + 26), + b'0'..=b'9' => Some(byte - b'0' + 52), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } +} + +/// Appends decoded bytes for one padded or unpadded Base64 quartet. +pub(in crate::interpreter) fn eval_push_base64_decoded_quartet( + quartet: &[Option], + output: &mut Vec, +) { + let (Some(first), Some(second)) = (quartet[0], quartet[1]) else { + return; + }; + output.push((first << 2) | (second >> 4)); + let Some(third) = quartet[2] else { + return; + }; + output.push(((second & 0x0f) << 4) | (third >> 2)); + let Some(fourth) = quartet[3] else { + return; + }; + output.push(((third & 0x03) << 6) | fourth); +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs b/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs index 23873bb9c1..150203d42c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing Base64 encode hook. +//! - Runtime dispatch is declared here and implemented through the existing Base64 encode hook. eval_builtin! { name: "base64_encode", @@ -14,3 +14,47 @@ eval_builtin! { direct: Base64Encode, values: Base64Encode, } + +use super::super::super::*; + +/// Evaluates PHP's `base64_encode(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_base64_encode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_base64_encode_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns Base64 text. +pub(in crate::interpreter) fn eval_base64_encode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = String::with_capacity(((bytes.len() + 2) / 3) * 4); + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for chunk in bytes.chunks(3) { + let first = chunk[0]; + let second = chunk.get(1).copied().unwrap_or(0); + let third = chunk.get(2).copied().unwrap_or(0); + output.push(ALPHABET[(first >> 2) as usize] as char); + output.push(ALPHABET[(((first & 0x03) << 4) | (second >> 4)) as usize] as char); + if chunk.len() > 1 { + output.push(ALPHABET[(((second & 0x0f) << 2) | (third >> 6)) as usize] as char); + } else { + output.push('='); + } + if chunk.len() > 2 { + output.push(ALPHABET[(third & 0x3f) as usize] as char); + } else { + output.push('='); + } + } + values.string(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs b/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs index f4d195ac21..d3e5955524 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing hex encode hook. +//! - Runtime dispatch is declared here and implemented through the existing hex encode hook. eval_builtin! { name: "bin2hex", @@ -14,3 +14,39 @@ eval_builtin! { direct: Bin2Hex, values: Bin2Hex, } + +use super::super::super::*; + +/// Evaluates PHP's `bin2hex(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_bin2hex( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_bin2hex_result(value, values) +} + +/// Converts one eval value through PHP string conversion and returns lowercase hex bytes. +pub(in crate::interpreter) fn eval_bin2hex_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.string(&eval_lower_hex_bytes(&bytes)) +} + +/// Converts bytes to lowercase hexadecimal text. +pub(in crate::interpreter) fn eval_lower_hex_bytes(bytes: &[u8]) -> String { + let mut output = String::with_capacity(bytes.len() * 2); + const HEX: &[u8; 16] = b"0123456789abcdef"; + for byte in bytes { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/chop.rs b/crates/elephc-magician/src/interpreter/builtins/string/chop.rs index f5f93556ec..e3479d2026 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/chop.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/chop.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the trim-family hook. +//! - Runtime dispatch is declared here and implemented through the trim-family hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,24 @@ eval_builtin! { direct: TrimLike, values: TrimLike, } + +use super::super::super::*; + +/// Evaluates PHP `chop(...)` over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_chop( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_builtin_trim_like_named("chop", args, context, scope, values) +} + +/// Applies PHP `chop(...)` to one evaluated string and optional mask. +pub(in crate::interpreter) fn eval_chop_result( + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_trim_like_named_result("chop", value, mask, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/chr.rs b/crates/elephc-magician/src/interpreter/builtins/string/chr.rs index c711937950..f261b4e568 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/chr.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/chr.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing byte-string hook. +//! - Runtime dispatch is declared here and implemented through the existing byte-string hook. eval_builtin! { name: "chr", @@ -14,3 +14,28 @@ eval_builtin! { direct: Chr, values: Chr, } + +use super::super::super::*; + +/// Evaluates PHP's `chr(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_chr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_chr_result(value, values) +} + +/// Converts one eval value to a PHP integer and returns the low byte as a string. +pub(in crate::interpreter) fn eval_chr_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + values.string_bytes_value(&[value as u8]) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs b/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs index dd7ee4b26e..b7a5912f9f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing checksum hook. +//! - Runtime dispatch is declared here and implemented through the existing checksum hook. eval_builtin! { name: "crc32", @@ -14,3 +14,29 @@ eval_builtin! { direct: Crc32, values: Crc32, } + +use super::super::super::*; +use super::super::eval_crc32_bytes; + +/// Evaluates PHP `crc32(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_crc32( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_crc32_result(value, values) +} + +/// Computes PHP's non-negative CRC-32 integer over one converted byte string. +pub(in crate::interpreter) fn eval_crc32_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(eval_crc32_bytes(&bytes))) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs index 1ac8b7a634..96e9f48458 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing ASCII ctype hook. +//! - Runtime dispatch is declared here and implemented through the existing ASCII ctype hook. eval_builtin! { name: "ctype_alnum", @@ -14,3 +14,69 @@ eval_builtin! { direct: Ctype, values: Ctype, } + +use super::super::super::*; + +/// Evaluates PHP `ctype_alnum(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_alnum( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_builtin_ctype_named("ctype_alnum", args, context, scope, values) +} + +/// Returns the PHP boolean result for `ctype_alnum(...)` from one evaluated value. +pub(in crate::interpreter) fn eval_ctype_alnum_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_ctype_named_result("ctype_alnum", value, values) +} + +/// Evaluates a named PHP `ctype_*` predicate over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ctype_named_result(name, value, values) +} + +/// Returns the PHP boolean result for one named ASCII `ctype_*` byte-string check. +pub(in crate::interpreter) fn eval_ctype_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut matches = !bytes.is_empty(); + for byte in bytes { + if !eval_ctype_byte_matches(name, byte)? { + matches = false; + break; + } + } + values.bool_value(matches) +} + +/// Checks one byte against the selected PHP ASCII character class. +pub(in crate::interpreter) fn eval_ctype_byte_matches( + name: &str, + byte: u8, +) -> Result { + match name { + "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), + "ctype_digit" => Ok(byte.is_ascii_digit()), + "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), + "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs index e3d0f0c588..e2bdeecdaa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing ASCII ctype hook. +//! - Runtime dispatch is declared here and implemented through the existing ASCII ctype hook. eval_builtin! { name: "ctype_alpha", @@ -14,3 +14,23 @@ eval_builtin! { direct: Ctype, values: Ctype, } + +use super::super::super::*; + +/// Evaluates PHP `ctype_alpha(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_alpha( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_builtin_ctype_named("ctype_alpha", args, context, scope, values) +} + +/// Returns the PHP boolean result for `ctype_alpha(...)` from one evaluated value. +pub(in crate::interpreter) fn eval_ctype_alpha_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_ctype_named_result("ctype_alpha", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs index dca25e6e84..2b7946f1e4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing ASCII ctype hook. +//! - Runtime dispatch is declared here and implemented through the existing ASCII ctype hook. eval_builtin! { name: "ctype_digit", @@ -14,3 +14,23 @@ eval_builtin! { direct: Ctype, values: Ctype, } + +use super::super::super::*; + +/// Evaluates PHP `ctype_digit(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_digit( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_builtin_ctype_named("ctype_digit", args, context, scope, values) +} + +/// Returns the PHP boolean result for `ctype_digit(...)` from one evaluated value. +pub(in crate::interpreter) fn eval_ctype_digit_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_ctype_named_result("ctype_digit", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs b/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs index 8da38979ff..c82531743e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing ASCII ctype hook. +//! - Runtime dispatch is declared here and implemented through the existing ASCII ctype hook. eval_builtin! { name: "ctype_space", @@ -14,3 +14,23 @@ eval_builtin! { direct: Ctype, values: Ctype, } + +use super::super::super::*; + +/// Evaluates PHP `ctype_space(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_ctype_space( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_builtin_ctype_named("ctype_space", args, context, scope, values) +} + +/// Returns the PHP boolean result for `ctype_space(...)` from one evaluated value. +pub(in crate::interpreter) fn eval_ctype_space_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::ctype_alnum::eval_ctype_named_result("ctype_space", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/explode.rs b/crates/elephc-magician/src/interpreter/builtins/string/explode.rs new file mode 100644 index 0000000000..cf4efc4867 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/explode.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Declarative eval registry entry for `explode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string split/join hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "explode", + area: String, + params: [separator, string, limit = EvalBuiltinDefaultValue::Int(i64::MAX)], + direct: StringSplitJoin, + values: StringSplitJoin, +} + +use super::super::super::*; + +/// Evaluates PHP `explode()` over separator and string expressions. +pub(in crate::interpreter) fn eval_builtin_explode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, string] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let string = eval_expr(string, context, scope, values)?; + eval_explode_result(separator, string, values) +} + +/// Splits one PHP byte string into an indexed array using a non-empty separator. +pub(in crate::interpreter) fn eval_explode_result( + separator: RuntimeCellHandle, + string: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let separator = values.string_bytes(separator)?; + if separator.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let string = values.string_bytes(string)?; + let mut result = values.array_new(0)?; + let mut start = 0; + let mut index = 0_i64; + while let Some(found) = super::strstr::eval_find_subslice(&string, &separator, start) { + result = eval_push_explode_segment(result, index, &string[start..found], values)?; + start = found + separator.len(); + index += 1; + } + eval_push_explode_segment(result, index, &string[start..], values) +} + +/// Appends one split segment to an indexed `explode()` result array. +pub(in crate::interpreter) fn eval_push_explode_segment( + array: RuntimeCellHandle, + index: i64, + segment: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(index)?; + let value = values.string_bytes_value(segment)?; + values.array_set(array, key, value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs b/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs index 1b1b3630ef..c4ab645795 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the grapheme string reverse hook. +//! - Runtime dispatch is declared here and implemented through the grapheme string reverse hook. eval_builtin! { name: "grapheme_strrev", @@ -14,3 +14,33 @@ eval_builtin! { direct: GraphemeStrrev, values: GraphemeStrrev, } + +use super::super::super::*; +use unicode_segmentation::UnicodeSegmentation; + +/// Evaluates PHP `grapheme_strrev(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_grapheme_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_grapheme_strrev_result(value, values) +} + +/// Reverses a materialized PHP string by grapheme cluster or returns false for invalid UTF-8. +pub(in crate::interpreter) fn eval_grapheme_strrev_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let Ok(source) = std::str::from_utf8(&bytes) else { + return values.bool_value(false); + }; + let reversed = source.graphemes(true).rev().collect::(); + values.string(&reversed) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/gzip.rs b/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs similarity index 73% rename from crates/elephc-magician/src/interpreter/builtins/strings/gzip.rs rename to crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs index 37440d7437..1b52868baa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/strings/gzip.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs @@ -1,26 +1,48 @@ //! Purpose: -//! Implements eval gzip/zlib string builtins. -//! Covers zlib-wrapped `gzcompress`/`gzuncompress` and raw-DEFLATE -//! `gzdeflate`/`gzinflate` using Rust-side compression helpers. +//! Declarative eval registry entry for `gzcompress`. //! //! Called from: -//! - `crate::interpreter::builtins::strings` re-exports used by call dispatch. +//! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Decompression failures return PHP false, matching the compiler backend's -//! string-or-false observable contract. +//! - Runtime dispatch is declared here and implemented through the gzip/zlib hook. -use std::io::{Read, Write}; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzcompress", + area: String, + params: [data, level = EvalBuiltinDefaultValue::Int(-1)], + direct: Gzip, + values: Gzip, +} +use super::super::super::*; use flate2::read::{DeflateDecoder, ZlibDecoder}; use flate2::write::{DeflateEncoder, ZlibEncoder}; use flate2::Compression; +use std::io::{Read, Write}; -use super::super::super::*; -use super::super::*; +/// Evaluates PHP `gzcompress(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzcompress( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_builtin_gzip_named("gzcompress", args, context, scope, values) +} + +/// Applies PHP `gzcompress(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_gzcompress_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_gzip_named_result("gzcompress", evaluated_args, values) +} -/// Evaluates one gzip/zlib string builtin over eval expressions. -pub(in crate::interpreter) fn eval_builtin_gzip( +/// Evaluates a named gzip/zlib builtin over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzip_named( name: &str, args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -31,11 +53,11 @@ pub(in crate::interpreter) fn eval_builtin_gzip( for arg in args { evaluated_args.push(eval_expr(arg, context, scope, values)?); } - eval_gzip_result(name, &evaluated_args, values) + eval_gzip_named_result(name, &evaluated_args, values) } /// Dispatches one materialized gzip/zlib builtin call. -pub(in crate::interpreter) fn eval_gzip_result( +pub(in crate::interpreter) fn eval_gzip_named_result( name: &str, evaluated_args: &[RuntimeCellHandle], values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs b/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs new file mode 100644 index 0000000000..726b7d7076 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `gzdeflate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the gzip/zlib hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzdeflate", + area: String, + params: [data, level = EvalBuiltinDefaultValue::Int(-1)], + direct: Gzip, + values: Gzip, +} + +use super::super::super::*; + +/// Evaluates PHP `gzdeflate(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzdeflate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_builtin_gzip_named("gzdeflate", args, context, scope, values) +} + +/// Applies PHP `gzdeflate(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_gzdeflate_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_gzip_named_result("gzdeflate", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs b/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs new file mode 100644 index 0000000000..90a580e0dd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `gzinflate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the gzip/zlib hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzinflate", + area: String, + params: [data, max_length = EvalBuiltinDefaultValue::Int(0)], + direct: Gzip, + values: Gzip, +} + +use super::super::super::*; + +/// Evaluates PHP `gzinflate(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzinflate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_builtin_gzip_named("gzinflate", args, context, scope, values) +} + +/// Applies PHP `gzinflate(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_gzinflate_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_gzip_named_result("gzinflate", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs b/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs new file mode 100644 index 0000000000..4b9b8b0aa1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `gzuncompress`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the gzip/zlib hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "gzuncompress", + area: String, + params: [data, max_length = EvalBuiltinDefaultValue::Int(0)], + direct: Gzip, + values: Gzip, +} + +use super::super::super::*; + +/// Evaluates PHP `gzuncompress(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_gzuncompress( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_builtin_gzip_named("gzuncompress", args, context, scope, values) +} + +/// Applies PHP `gzuncompress(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_gzuncompress_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::gzcompress::eval_gzip_named_result("gzuncompress", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/hash.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash.rs similarity index 61% rename from crates/elephc-magician/src/interpreter/builtins/strings/hash.rs rename to crates/elephc-magician/src/interpreter/builtins/string/hash.rs index 01e0a0ee82..d89a56cb7f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/strings/hash.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash.rs @@ -1,40 +1,44 @@ //! Purpose: -//! CRC32 and one-shot hash digest builtins. +//! Declarative eval registry entry for `hash`. //! //! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. +//! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash", + area: String, + params: [algo, data, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} use super::super::super::*; -use super::super::*; -/// Evaluates PHP `crc32(...)` over one eval string expression. -pub(in crate::interpreter) fn eval_builtin_crc32( +/// Evaluates PHP `hash(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_crc32_result(value, values) + super::hash::eval_builtin_hash_one_shot_named("hash", args, context, scope, values) } -/// Computes PHP's non-negative CRC-32 integer over one converted byte string. -pub(in crate::interpreter) fn eval_crc32_result( - value: RuntimeCellHandle, +/// Applies PHP `hash(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_hash_result( + evaluated_args: &[RuntimeCellHandle], values: &mut impl RuntimeValueOps, ) -> Result { - let bytes = values.string_bytes(value)?; - values.int(i64::from(eval_crc32_bytes(&bytes))) + super::hash::eval_hash_one_shot_named_result("hash", evaluated_args, values) } -/// Evaluates one-shot PHP hash digest builtins over eval expressions. -pub(in crate::interpreter) fn eval_builtin_hash_one_shot( +/// Evaluates a named one-shot PHP hash builtin over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_one_shot_named( name: &str, args: &[EvalExpr], context: &mut ElephcEvalContext, @@ -45,11 +49,11 @@ pub(in crate::interpreter) fn eval_builtin_hash_one_shot( for arg in args { evaluated_args.push(eval_expr(arg, context, scope, values)?); } - eval_hash_one_shot_result(name, &evaluated_args, values) + eval_hash_one_shot_named_result(name, &evaluated_args, values) } /// Computes the result for one-shot PHP hash digest builtins from evaluated args. -pub(in crate::interpreter) fn eval_hash_one_shot_result( +pub(in crate::interpreter) fn eval_hash_one_shot_named_result( name: &str, evaluated_args: &[RuntimeCellHandle], values: &mut impl RuntimeValueOps, @@ -80,7 +84,7 @@ pub(in crate::interpreter) fn eval_hash_one_shot_result( [algo, filename, binary] => (*algo, *filename, values.truthy(*binary)?), _ => return Err(EvalStatus::RuntimeFatal), }; - eval_hash_file_result(algo, filename, binary, values) + super::hash_file::eval_hash_file_digest_result(algo, filename, binary, values) } "hash_hmac" => { let (algo, data, key, binary) = match evaluated_args { @@ -91,27 +95,12 @@ pub(in crate::interpreter) fn eval_hash_one_shot_result( let algo = values.string_bytes(algo)?; let data = values.string_bytes(data)?; let key = values.string_bytes(key)?; - eval_hash_hmac_result(&algo, &data, &key, binary, values) + super::hash_hmac::eval_hash_hmac_digest_result(&algo, &data, &key, binary, values) } _ => Err(EvalStatus::UnsupportedConstruct), } } -/// Reads a local file and returns its PHP hash digest or false when it cannot be read. -pub(in crate::interpreter) fn eval_hash_file_result( - algo: RuntimeCellHandle, - filename: RuntimeCellHandle, - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let algo = values.string_bytes(algo)?; - let path = eval_path_string(filename, values)?; - match std::fs::read(path) { - Ok(data) => eval_hash_digest_result(&algo, &data, binary, values), - Err(_) => values.bool_value(false), - } -} - /// Computes a one-shot raw digest and formats it as PHP hex or raw bytes. pub(in crate::interpreter) fn eval_hash_digest_result( algo: &[u8], @@ -123,18 +112,6 @@ pub(in crate::interpreter) fn eval_hash_digest_result( eval_format_digest_result(&raw, binary, values) } -/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. -pub(in crate::interpreter) fn eval_hash_hmac_result( - algo: &[u8], - data: &[u8], - key: &[u8], - binary: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let raw = eval_crypto_hmac(algo, data, key)?; - eval_format_digest_result(&raw, binary, values) -} - /// Calls the elephc-crypto one-shot hash ABI and returns the raw digest bytes. pub(in crate::interpreter) fn eval_crypto_hash( algo: &[u8], @@ -153,27 +130,6 @@ pub(in crate::interpreter) fn eval_crypto_hash( eval_crypto_digest_bytes(len, &output) } -/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. -pub(in crate::interpreter) fn eval_crypto_hmac( - algo: &[u8], - data: &[u8], - key: &[u8], -) -> Result, EvalStatus> { - let mut output = [0_u8; 64]; - let len = unsafe { - elephc_crypto::elephc_crypto_hmac( - algo.as_ptr(), - algo.len(), - key.as_ptr(), - key.len(), - data.as_ptr(), - data.len(), - output.as_mut_ptr(), - ) - }; - eval_crypto_digest_bytes(len, &output) -} - /// Converts a crypto ABI digest length into an owned digest byte vector. pub(in crate::interpreter) fn eval_crypto_digest_bytes( len: isize, @@ -195,5 +151,5 @@ pub(in crate::interpreter) fn eval_format_digest_result( if binary { return values.string_bytes_value(raw); } - values.string(&eval_lower_hex_bytes(raw)) + values.string(&super::bin2hex::eval_lower_hex_bytes(raw)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs new file mode 100644 index 0000000000..78153b537d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs @@ -0,0 +1,51 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_algos`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the static hash algorithm list helper. + +eval_builtin! { + name: "hash_algos", + area: String, + params: [], + direct: HashAlgos, + values: HashAlgos, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_algos()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_hash_algos( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + +/// Builds the indexed array returned by eval `hash_algos()`. +pub(in crate::interpreter) fn eval_hash_algos_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_HASH_ALGOS, values) +} + +/// Builds one indexed PHP array from a static string slice. +pub(in crate::interpreter) fn eval_static_string_array_result( + items: &[&str], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs new file mode 100644 index 0000000000..1d54be3a84 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_copy`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the incremental hash-context hook. + +eval_builtin! { + name: "hash_copy", + area: String, + params: [context], + direct: HashContext, + values: HashContext, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_copy($context)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hash_copy( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hash_context = eval_expr(hash_context, context, scope, values)?; + eval_hash_copy_result(hash_context, context, values) +} + +/// Clones a materialized incremental hash context into a new resource. +pub(in crate::interpreter) fn eval_hash_copy_result( + hash_context: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::hash_init::eval_hash_context_resource_id(hash_context, values)?; + match context.stream_resources_mut().copy_hash_context(id) { + Some(copy_id) => values.resource(copy_id), + None => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs index 714cc58bd7..9edde5e003 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the constant-time byte compare hook. +//! - Runtime dispatch is declared here and implemented through the constant-time byte compare hook. eval_builtin! { name: "hash_equals", @@ -14,3 +14,38 @@ eval_builtin! { direct: HashEquals, values: HashEquals, } + +use super::super::super::*; + +/// Evaluates PHP's `hash_equals(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_equals( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [known, user] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let known = eval_expr(known, context, scope, values)?; + let user = eval_expr(user, context, scope, values)?; + eval_hash_equals_result(known, user, values) +} + +/// Compares two converted strings with PHP `hash_equals()` semantics. +pub(in crate::interpreter) fn eval_hash_equals_result( + known: RuntimeCellHandle, + user: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let known = values.string_bytes(known)?; + let user = values.string_bytes(user)?; + if known.len() != user.len() { + return values.bool_value(false); + } + let mut diff = 0u8; + for (known, user) in known.iter().zip(user.iter()) { + diff |= known ^ user; + } + values.bool_value(diff == 0) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs new file mode 100644 index 0000000000..1ca2a15280 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_file", + area: String, + params: [algo, filename, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; +use super::super::*; + +/// Evaluates PHP `hash_file(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_file( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("hash_file", args, context, scope, values) +} + +/// Applies PHP `hash_file(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_hash_file_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("hash_file", evaluated_args, values) +} + +/// Reads a local file and returns its PHP hash digest or false when it cannot be read. +pub(in crate::interpreter) fn eval_hash_file_digest_result( + algo: RuntimeCellHandle, + filename: RuntimeCellHandle, + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + let path = eval_path_string(filename, values)?; + match std::fs::read(path) { + Ok(data) => super::hash::eval_hash_digest_result(&algo, &data, binary, values), + Err(_) => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs new file mode 100644 index 0000000000..6757b0019a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_final`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the incremental hash-context hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_final", + area: String, + params: [context, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashContext, + values: HashContext, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_final($context, $binary = false)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_final( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let hash_context = eval_expr(&args[0], context, scope, values)?; + let binary = match args.get(1) { + Some(binary) => { + let binary = eval_expr(binary, context, scope, values)?; + values.truthy(binary)? + } + None => false, + }; + eval_hash_final_result(hash_context, binary, context, values) +} + +/// Finalizes a materialized incremental hash context. +pub(in crate::interpreter) fn eval_hash_final_result( + hash_context: RuntimeCellHandle, + binary: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::hash_init::eval_hash_context_resource_id(hash_context, values)?; + let raw = context + .stream_resources_mut() + .finalize_hash_context(id) + .ok_or(EvalStatus::RuntimeFatal)?; + super::hash::eval_format_digest_result(&raw, binary, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs new file mode 100644 index 0000000000..6702542560 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_hmac`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_hmac", + area: String, + params: [algo, data, key, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_hmac(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_hmac( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("hash_hmac", args, context, scope, values) +} + +/// Applies PHP `hash_hmac(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_hash_hmac_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("hash_hmac", evaluated_args, values) +} + +/// Computes a one-shot raw HMAC digest and formats it as PHP hex or raw bytes. +pub(in crate::interpreter) fn eval_hash_hmac_digest_result( + algo: &[u8], + data: &[u8], + key: &[u8], + binary: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let raw = eval_crypto_hmac(algo, data, key)?; + super::hash::eval_format_digest_result(&raw, binary, values) +} + +/// Calls the elephc-crypto one-shot HMAC ABI and returns the raw digest bytes. +pub(in crate::interpreter) fn eval_crypto_hmac( + algo: &[u8], + data: &[u8], + key: &[u8], +) -> Result, EvalStatus> { + let mut output = [0_u8; 64]; + let len = unsafe { + elephc_crypto::elephc_crypto_hmac( + algo.as_ptr(), + algo.len(), + key.as_ptr(), + key.len(), + data.as_ptr(), + data.len(), + output.as_mut_ptr(), + ) + }; + super::hash::eval_crypto_digest_bytes(len, &output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs new file mode 100644 index 0000000000..e7182f57b7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs @@ -0,0 +1,64 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_init`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the incremental hash-context hook. +//! - Optional HMAC parameters remain metadata-only for current eval behavior. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "hash_init", + area: String, + params: [ + algo, + flags = EvalBuiltinDefaultValue::Int(0), + key = EvalBuiltinDefaultValue::String(""), + ], + direct: HashContext, + values: HashContext, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_init($algo)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hash_init( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [algo] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let algo = eval_expr(algo, context, scope, values)?; + eval_hash_init_result(algo, context, values) +} + +/// Opens an incremental hash context resource. +pub(in crate::interpreter) fn eval_hash_init_result( + algo: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let algo = values.string_bytes(algo)?; + match context.stream_resources_mut().open_hash_context(&algo) { + Some(id) => values.resource(id), + None => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based hash context id. +pub(in crate::interpreter) fn eval_hash_context_resource_id( + hash_context: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(hash_context)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(hash_context, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs new file mode 100644 index 0000000000..b27bfa55f7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Declarative eval registry entry for `hash_update`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the incremental hash-context hook. + +eval_builtin! { + name: "hash_update", + area: String, + params: [context, data], + direct: HashContext, + values: HashContext, +} + +use super::super::super::*; + +/// Evaluates PHP `hash_update($context, $data)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_hash_update( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let hash_context = eval_expr(hash_context, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_hash_update_result(hash_context, data, context, values) +} + +/// Feeds data into a materialized incremental hash context. +pub(in crate::interpreter) fn eval_hash_update_result( + hash_context: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::hash_init::eval_hash_context_resource_id(hash_context, values)?; + let data = values.string_bytes(data)?; + values.bool_value(context.stream_resources_mut().update_hash_context(id, &data)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs b/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs index 7d0e4951ae..14dccc0fb0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing hex decode hook. +//! - Runtime dispatch is declared here and implemented through the existing hex decode hook. eval_builtin! { name: "hex2bin", @@ -14,3 +14,54 @@ eval_builtin! { direct: Hex2Bin, values: Hex2Bin, } + +use super::super::super::*; + +/// Evaluates PHP's `hex2bin(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_hex2bin( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_hex2bin_result(value, values) +} + +/// Converts one eval value through PHP string conversion and decodes hexadecimal bytes. +pub(in crate::interpreter) fn eval_hex2bin_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + if bytes.len() % 2 != 0 { + values.warning(HEX2BIN_ODD_LENGTH_WARNING)?; + return values.bool_value(false); + } + let mut output = Vec::with_capacity(bytes.len() / 2); + for pair in bytes.chunks_exact(2) { + let Some(high) = eval_hex_nibble(pair[0]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + let Some(low) = eval_hex_nibble(pair[1]) else { + values.warning(HEX2BIN_INVALID_WARNING)?; + return values.bool_value(false); + }; + output.push((high << 4) | low); + } + values.string_bytes_value(&output) +} + +/// Returns the four-bit value for one hexadecimal byte. +pub(in crate::interpreter) fn eval_hex_nibble(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs b/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs index 7623ce2c20..e49b76c3fc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the HTML entity hook. +//! - Runtime dispatch is declared here and implemented through the HTML entity hook. eval_builtin! { name: "html_entity_decode", @@ -14,3 +14,23 @@ eval_builtin! { direct: HtmlEntity, values: HtmlEntity, } + +use super::super::super::*; + +/// Evaluates PHP `html_entity_decode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_html_entity_decode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_builtin_html_entity_named("html_entity_decode", args, context, scope, values) +} + +/// Applies PHP `html_entity_decode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_html_entity_decode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_html_entity_decode_value_result(value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs b/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs index 63e9fc3cc3..a86f30109d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the HTML entity hook. +//! - Runtime dispatch is declared here and implemented through the HTML entity hook. eval_builtin! { name: "htmlentities", @@ -14,3 +14,23 @@ eval_builtin! { direct: HtmlEntity, values: HtmlEntity, } + +use super::super::super::*; + +/// Evaluates PHP `htmlentities(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_htmlentities( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_builtin_html_entity_named("htmlentities", args, context, scope, values) +} + +/// Applies PHP `htmlentities(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_htmlentities_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_htmlspecialchars_result(value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs b/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs index a2a2004fa6..d9f717171a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the HTML entity hook. +//! - Runtime dispatch is declared here and implemented through the HTML entity hook. eval_builtin! { name: "htmlspecialchars", @@ -14,3 +14,102 @@ eval_builtin! { direct: HtmlEntity, values: HtmlEntity, } + +use super::super::super::*; + +/// Evaluates PHP `htmlspecialchars(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_htmlspecialchars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::htmlspecialchars::eval_builtin_html_entity_named("htmlspecialchars", args, context, scope, values) +} + +/// Evaluates a named HTML entity encode/decode builtin over one string expression. +pub(in crate::interpreter) fn eval_builtin_html_entity_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_html_entity_named_result(name, value, values) +} + +/// Applies the eval-supported HTML entity transform for one PHP string value. +pub(in crate::interpreter) fn eval_html_entity_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), + "html_entity_decode" => eval_html_entity_decode_value_result(value, values), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} + +/// Encodes the HTML-special byte characters covered by elephc's static helper. +pub(in crate::interpreter) fn eval_htmlspecialchars_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + for byte in bytes { + match byte { + b'&' => output.extend_from_slice(b"&"), + b'<' => output.extend_from_slice(b"<"), + b'>' => output.extend_from_slice(b">"), + b'"' => output.extend_from_slice(b"""), + b'\'' => output.extend_from_slice(b"'"), + _ => output.push(byte), + } + } + values.string_bytes_value(&output) +} + +/// Decodes one pass of the HTML entities emitted by the eval/static encoders. +pub(in crate::interpreter) fn eval_html_entity_decode_value_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'&' { + if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { + output.push(decoded); + index += width; + continue; + } + } + output.push(bytes[index]); + index += 1; + } + values.string_bytes_value(&output) +} + +/// Returns the decoded byte and consumed width for one supported HTML entity. +pub(in crate::interpreter) fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { + for (entity, decoded) in [ + (b"<".as_slice(), b'<'), + (b">".as_slice(), b'>'), + (b""".as_slice(), b'"'), + (b"'".as_slice(), b'\''), + (b"'".as_slice(), b'\''), + (b"&".as_slice(), b'&'), + ] { + if bytes.starts_with(entity) { + return Some((decoded, entity.len())); + } + } + None +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/implode.rs b/crates/elephc-magician/src/interpreter/builtins/string/implode.rs new file mode 100644 index 0000000000..88c3dfc29c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/implode.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Declarative eval registry entry for `implode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the string split/join hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "implode", + area: String, + params: [separator = EvalBuiltinDefaultValue::Null, array], + required: 1, + direct: StringSplitJoin, + values: StringSplitJoin, +} + +use super::super::super::*; + +/// Evaluates PHP `implode()` over separator and array expressions. +pub(in crate::interpreter) fn eval_builtin_implode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let separator = eval_expr(separator, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_implode_result(separator, array, values) +} + +/// Joins array values in eval iteration order using PHP string conversion. +pub(in crate::interpreter) fn eval_implode_result( + separator: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !values.is_array_like(array)? { + return Err(EvalStatus::RuntimeFatal); + } + let separator = values.string_bytes(separator)?; + let len = values.array_len(array)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.extend_from_slice(&separator); + } + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let value = values.string_bytes(value)?; + output.extend_from_slice(&value); + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs b/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs index 0e0bf6cb01..bd3b6c0789 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-case hook. +//! - Runtime dispatch is declared here and implemented through the string-case hook. eval_builtin! { name: "lcfirst", @@ -14,3 +14,23 @@ eval_builtin! { direct: StringCase, values: StringCase, } + +use super::super::super::*; + +/// Evaluates PHP `lcfirst(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_lcfirst( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_builtin_string_case_named("lcfirst", args, context, scope, values) +} + +/// Applies PHP `lcfirst(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_lcfirst_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_string_case_named_result("lcfirst", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs b/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs index 45cc57ab9b..e41668063b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the trim-family hook. +//! - Runtime dispatch is declared here and implemented through the trim-family hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,24 @@ eval_builtin! { direct: TrimLike, values: TrimLike, } + +use super::super::super::*; + +/// Evaluates PHP `ltrim(...)` over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_ltrim( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_builtin_trim_like_named("ltrim", args, context, scope, values) +} + +/// Applies PHP `ltrim(...)` to one evaluated string and optional mask. +pub(in crate::interpreter) fn eval_ltrim_result( + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_trim_like_named_result("ltrim", value, mask, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/md5.rs b/crates/elephc-magician/src/interpreter/builtins/string/md5.rs new file mode 100644 index 0000000000..6814eef643 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/md5.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `md5`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "md5", + area: String, + params: [string, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; + +/// Evaluates PHP `md5(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_md5( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("md5", args, context, scope, values) +} + +/// Applies PHP `md5(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_md5_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("md5", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs index 09795f3923..944eb30326 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/mod.rs @@ -1,12 +1,13 @@ //! Purpose: -//! Per-builtin declarations for string functions migrated to the eval builtin -//! registry. +//! Per-builtin string, hash, encoding, compression, and stream-introspection eval +//! builtins. //! //! Called from: -//! - `crate::interpreter::builtins` module loading. +//! - `crate::interpreter::builtins` module loading and re-exports. //! //! Key details: -//! - Leaf files register metadata through `eval_builtin!`. +//! - Leaf files register metadata through `eval_builtin!` and own the matching +//! eval implementation or delegate to the closest builtin owner for shared code. mod addslashes; mod base64_decode; @@ -19,33 +20,54 @@ mod ctype_alnum; mod ctype_alpha; mod ctype_digit; mod ctype_space; +mod explode; mod grapheme_strrev; +mod gzcompress; +mod gzdeflate; +mod gzinflate; +mod gzuncompress; +mod hash; +mod hash_algos; +mod hash_copy; mod hash_equals; +mod hash_file; +mod hash_final; +mod hash_hmac; +mod hash_init; +mod hash_update; mod hex2bin; mod html_entity_decode; mod htmlentities; mod htmlspecialchars; +mod implode; mod lcfirst; mod ltrim; +mod md5; mod nl2br; mod ord; mod rawurldecode; mod rawurlencode; mod rtrim; +mod sha1; mod str_contains; mod str_ends_with; mod str_ireplace; mod str_pad; -mod str_replace; -mod strlen; mod str_repeat; +mod str_replace; mod str_split; mod str_starts_with; -mod strrev; -mod stripslashes; mod strcasecmp; mod strcmp; +mod stream_get_filters; +mod stream_get_transports; +mod stream_get_wrappers; +mod stream_is_local; +mod stream_supports_lock; +mod stripslashes; +mod strlen; mod strpos; +mod strrev; mod strrpos; mod strstr; mod strtolower; @@ -58,3 +80,75 @@ mod ucwords; mod urldecode; mod urlencode; mod wordwrap; + +pub(in crate::interpreter) use addslashes::*; +pub(in crate::interpreter) use base64_decode::*; +pub(in crate::interpreter) use base64_encode::*; +pub(in crate::interpreter) use bin2hex::*; +pub(in crate::interpreter) use chop::*; +pub(in crate::interpreter) use chr::*; +pub(in crate::interpreter) use crc32::*; +pub(in crate::interpreter) use ctype_alnum::*; +pub(in crate::interpreter) use ctype_alpha::*; +pub(in crate::interpreter) use ctype_digit::*; +pub(in crate::interpreter) use ctype_space::*; +pub(in crate::interpreter) use explode::*; +pub(in crate::interpreter) use grapheme_strrev::*; +pub(in crate::interpreter) use gzcompress::*; +pub(in crate::interpreter) use gzdeflate::*; +pub(in crate::interpreter) use gzinflate::*; +pub(in crate::interpreter) use gzuncompress::*; +pub(in crate::interpreter) use hash::*; +pub(in crate::interpreter) use hash_algos::*; +pub(in crate::interpreter) use hash_copy::*; +pub(in crate::interpreter) use hash_equals::*; +pub(in crate::interpreter) use hash_file::*; +pub(in crate::interpreter) use hash_final::*; +pub(in crate::interpreter) use hash_hmac::*; +pub(in crate::interpreter) use hash_init::*; +pub(in crate::interpreter) use hash_update::*; +pub(in crate::interpreter) use hex2bin::*; +pub(in crate::interpreter) use html_entity_decode::*; +pub(in crate::interpreter) use htmlentities::*; +pub(in crate::interpreter) use htmlspecialchars::*; +pub(in crate::interpreter) use implode::*; +pub(in crate::interpreter) use lcfirst::*; +pub(in crate::interpreter) use ltrim::*; +pub(in crate::interpreter) use md5::*; +pub(in crate::interpreter) use nl2br::*; +pub(in crate::interpreter) use ord::*; +pub(in crate::interpreter) use rawurldecode::*; +pub(in crate::interpreter) use rawurlencode::*; +pub(in crate::interpreter) use rtrim::*; +pub(in crate::interpreter) use sha1::*; +pub(in crate::interpreter) use str_contains::*; +pub(in crate::interpreter) use str_ends_with::*; +pub(in crate::interpreter) use str_ireplace::*; +pub(in crate::interpreter) use str_pad::*; +pub(in crate::interpreter) use str_repeat::*; +pub(in crate::interpreter) use str_replace::*; +pub(in crate::interpreter) use str_split::*; +pub(in crate::interpreter) use str_starts_with::*; +pub(in crate::interpreter) use strcasecmp::*; +pub(in crate::interpreter) use strcmp::*; +pub(in crate::interpreter) use stream_get_filters::*; +pub(in crate::interpreter) use stream_get_transports::*; +pub(in crate::interpreter) use stream_get_wrappers::*; +pub(in crate::interpreter) use stream_is_local::*; +pub(in crate::interpreter) use stream_supports_lock::*; +pub(in crate::interpreter) use stripslashes::*; +pub(in crate::interpreter) use strlen::*; +pub(in crate::interpreter) use strpos::*; +pub(in crate::interpreter) use strrev::*; +pub(in crate::interpreter) use strrpos::*; +pub(in crate::interpreter) use strstr::*; +pub(in crate::interpreter) use strtolower::*; +pub(in crate::interpreter) use strtoupper::*; +pub(in crate::interpreter) use substr::*; +pub(in crate::interpreter) use substr_replace::*; +pub(in crate::interpreter) use trim::*; +pub(in crate::interpreter) use ucfirst::*; +pub(in crate::interpreter) use ucwords::*; +pub(in crate::interpreter) use urldecode::*; +pub(in crate::interpreter) use urlencode::*; +pub(in crate::interpreter) use wordwrap::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs b/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs index 24fd059c9e..44e39078e8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the newline-to-break hook. +//! - Runtime dispatch is declared here and implemented through the newline-to-break hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,62 @@ eval_builtin! { direct: Nl2br, values: Nl2br, } + +use super::super::super::*; + +/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. +pub(in crate::interpreter) fn eval_builtin_nl2br( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_nl2br_result(value, true, values) + } + [value, use_xhtml] => { + let value = eval_expr(value, context, scope, values)?; + let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; + let use_xhtml = values.truthy(use_xhtml)?; + eval_nl2br_result(value, use_xhtml, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. +pub(in crate::interpreter) fn eval_nl2br_result( + value: RuntimeCellHandle, + use_xhtml: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let br = if use_xhtml { + b"
".as_slice() + } else { + b"
".as_slice() + }; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + let byte = bytes[index]; + if byte == b'\r' || byte == b'\n' { + output.extend_from_slice(br); + output.push(byte); + if index + 1 < bytes.len() + && ((byte == b'\r' && bytes[index + 1] == b'\n') + || (byte == b'\n' && bytes[index + 1] == b'\r')) + { + output.push(bytes[index + 1]); + index += 2; + continue; + } + } else { + output.push(byte); + } + index += 1; + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ord.rs b/crates/elephc-magician/src/interpreter/builtins/string/ord.rs index a086893c84..beff5b6db9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/ord.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/ord.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing byte introspection hook. +//! - Runtime dispatch is declared here and implemented through the existing byte introspection hook. eval_builtin! { name: "ord", @@ -14,3 +14,28 @@ eval_builtin! { direct: Ord, values: Ord, } + +use super::super::super::*; + +/// Evaluates the builtin `ord(...)` for the first byte of one coerced string. +pub(in crate::interpreter) fn eval_builtin_ord( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_ord_result(value, values) +} + +/// Returns the first byte of one converted string, or zero for an empty string. +pub(in crate::interpreter) fn eval_ord_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + values.int(i64::from(bytes.first().copied().unwrap_or(0))) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs b/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs index d61f9b30b6..9b98a99c38 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing URL decode hook. +//! - Runtime dispatch is declared here and implemented through the existing URL decode hook. eval_builtin! { name: "rawurldecode", @@ -14,3 +14,23 @@ eval_builtin! { direct: UrlDecode, values: UrlDecode, } + +use super::super::super::*; + +/// Evaluates PHP `rawurldecode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_rawurldecode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urldecode::eval_builtin_url_decode_named("rawurldecode", args, context, scope, values) +} + +/// Applies PHP `rawurldecode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_rawurldecode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urldecode::eval_url_decode_named_result("rawurldecode", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs b/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs index 9424cf6445..6f568304dc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing URL encode hook. +//! - Runtime dispatch is declared here and implemented through the existing URL encode hook. eval_builtin! { name: "rawurlencode", @@ -14,3 +14,23 @@ eval_builtin! { direct: UrlEncode, values: UrlEncode, } + +use super::super::super::*; + +/// Evaluates PHP `rawurlencode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_rawurlencode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urlencode::eval_builtin_url_encode_named("rawurlencode", args, context, scope, values) +} + +/// Applies PHP `rawurlencode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_rawurlencode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urlencode::eval_url_encode_named_result("rawurlencode", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs b/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs index f90ba44319..1e39989021 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the trim-family hook. +//! - Runtime dispatch is declared here and implemented through the trim-family hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,24 @@ eval_builtin! { direct: TrimLike, values: TrimLike, } + +use super::super::super::*; + +/// Evaluates PHP `rtrim(...)` over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_rtrim( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_builtin_trim_like_named("rtrim", args, context, scope, values) +} + +/// Applies PHP `rtrim(...)` to one evaluated string and optional mask. +pub(in crate::interpreter) fn eval_rtrim_result( + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_trim_like_named_result("rtrim", value, mask, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs b/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs new file mode 100644 index 0000000000..d996b8428a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `sha1`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the one-shot hash hook. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "sha1", + area: String, + params: [string, binary = EvalBuiltinDefaultValue::Bool(false)], + direct: HashOneShot, + values: HashOneShot, +} + +use super::super::super::*; + +/// Evaluates PHP `sha1(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_sha1( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_builtin_hash_one_shot_named("sha1", args, context, scope, values) +} + +/// Applies PHP `sha1(...)` to already evaluated arguments. +pub(in crate::interpreter) fn eval_sha1_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + super::hash::eval_hash_one_shot_named_result("sha1", evaluated_args, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs index 43bb2f962c..070a33531e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-search predicate hook. +//! - Runtime dispatch is declared here and implemented through the string-search predicate hook. eval_builtin! { name: "str_contains", @@ -14,3 +14,63 @@ eval_builtin! { direct: StringSearch, values: StringSearch, } + +use super::super::super::*; + +/// Evaluates PHP `str_contains(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_str_contains( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_builtin_string_search_named("str_contains", args, context, scope, values) +} + +/// Applies PHP `str_contains(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_str_contains_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_string_search_named_result("str_contains", haystack, needle, values) +} + +/// Evaluates one named PHP byte-string search predicate. +pub(in crate::interpreter) fn eval_builtin_string_search_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_search_named_result(name, haystack, needle, values) +} + +/// Checks one converted haystack for one converted needle using PHP byte-string semantics. +pub(in crate::interpreter) fn eval_string_search_named_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let matched = match name { + "str_contains" => { + needle.is_empty() + || haystack + .windows(needle.len()) + .any(|window| window == needle) + } + "str_starts_with" => haystack.starts_with(&needle), + "str_ends_with" => haystack.ends_with(&needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + values.bool_value(matched) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs index 4433f7a4d5..f2d24097bb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-search predicate hook. +//! - Runtime dispatch is declared here and implemented through the string-search predicate hook. eval_builtin! { name: "str_ends_with", @@ -14,3 +14,24 @@ eval_builtin! { direct: StringSearch, values: StringSearch, } + +use super::super::super::*; + +/// Evaluates PHP `str_ends_with(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_str_ends_with( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_builtin_string_search_named("str_ends_with", args, context, scope, values) +} + +/// Applies PHP `str_ends_with(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_str_ends_with_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_string_search_named_result("str_ends_with", haystack, needle, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs index 4361699d7b..1ce2aceb62 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-replace hook. +//! - Runtime dispatch is declared here and implemented through the string-replace hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,25 @@ eval_builtin! { direct: StrReplace, values: StrReplace, } + +use super::super::super::*; + +/// Evaluates PHP `str_ireplace(...)` over search, replacement, and subject expressions. +pub(in crate::interpreter) fn eval_builtin_str_ireplace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_replace::eval_builtin_str_replace("str_ireplace", args, context, scope, values) +} + +/// Applies PHP `str_ireplace(...)` to already evaluated search, replacement, and subject values. +pub(in crate::interpreter) fn eval_str_ireplace_result( + search: RuntimeCellHandle, + replace: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_replace::eval_str_replace_result("str_ireplace", search, replace, subject, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs index e8502a3409..db584e9ac5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-pad hook. +//! - Runtime dispatch is declared here and implemented through the string-pad hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -21,3 +21,100 @@ eval_builtin! { direct: StrPad, values: StrPad, } + +use super::super::super::*; + +/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. +pub(in crate::interpreter) fn eval_builtin_str_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_pad_result(value, length, None, None, values) + } + [value, length, pad_string] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), None, values) + } + [value, length, pad_string, pad_type] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let pad_string = eval_expr(pad_string, context, scope, values)?; + let pad_type = eval_expr(pad_type, context, scope, values)?; + eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Pads one byte string to a PHP target length using cyclic pad bytes. +pub(in crate::interpreter) fn eval_str_pad_result( + value: RuntimeCellHandle, + length: RuntimeCellHandle, + pad_string: Option, + pad_type: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let target_length = eval_int_value(length, values)?; + let Ok(target_length) = usize::try_from(target_length) else { + return values.string_bytes_value(&bytes); + }; + if target_length <= bytes.len() { + return values.string_bytes_value(&bytes); + } + + let pad_string = match pad_string { + Some(pad_string) => values.string_bytes(pad_string)?, + None => b" ".to_vec(), + }; + if pad_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let pad_type = match pad_type { + Some(pad_type) => eval_int_value(pad_type, values)?, + None => 1, + }; + let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; + let capacity = bytes + .len() + .checked_add(left_pad) + .and_then(|size| size.checked_add(right_pad)) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + eval_append_repeated_pad(&mut output, &pad_string, left_pad); + output.extend_from_slice(&bytes); + eval_append_repeated_pad(&mut output, &pad_string, right_pad); + values.string_bytes_value(&output) +} + +/// Splits a `str_pad()` pad budget into left and right byte counts. +pub(in crate::interpreter) fn eval_str_pad_sides( + pad_budget: usize, + pad_type: i64, +) -> Result<(usize, usize), EvalStatus> { + match pad_type { + 0 => Ok((pad_budget, 0)), + 1 => Ok((0, pad_budget)), + 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Appends `count` bytes by cycling through the provided non-empty pad string. +pub(in crate::interpreter) fn eval_append_repeated_pad( + output: &mut Vec, + pad_string: &[u8], + count: usize, +) { + for index in 0..count { + output.push(pad_string[index % pad_string.len()]); + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs index 71484e4a1d..7f2470f644 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing repeat hook. +//! - Runtime dispatch is declared here and implemented through the existing repeat hook. eval_builtin! { name: "str_repeat", @@ -14,3 +14,43 @@ eval_builtin! { direct: StrRepeat, values: StrRepeat, } + +use super::super::super::*; + +/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. +pub(in crate::interpreter) fn eval_builtin_str_repeat( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value, times] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let times = eval_expr(times, context, scope, values)?; + eval_str_repeat_result(value, times, values) +} + +/// Repeats one PHP string byte sequence according to a PHP-cast integer count. +pub(in crate::interpreter) fn eval_str_repeat_result( + value: RuntimeCellHandle, + times: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let times = eval_int_value(times, values)?; + if times < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; + let capacity = bytes + .len() + .checked_mul(times) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(capacity); + for _ in 0..times { + output.extend_from_slice(&bytes); + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs index 326c73fc4f..87d62643ff 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-replace hook. +//! - Runtime dispatch is declared here and implemented through the string-replace hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,68 @@ eval_builtin! { direct: StrReplace, values: StrReplace, } + +use super::super::super::*; + +/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_str_replace( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [search, replace, subject] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let search = eval_expr(search, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_str_replace_result(name, search, replace, subject, values) +} + +/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. +pub(in crate::interpreter) fn eval_str_replace_result( + name: &str, + search: RuntimeCellHandle, + replace: RuntimeCellHandle, + subject: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let search = values.string_bytes(search)?; + let replace = values.string_bytes(replace)?; + let subject = values.string_bytes(subject)?; + if search.is_empty() { + return values.string_bytes_value(&subject); + } + + let mut output = Vec::with_capacity(subject.len()); + let mut start = 0; + while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { + output.extend_from_slice(&subject[start..found]); + output.extend_from_slice(&replace); + start = found + search.len(); + } + output.extend_from_slice(&subject[start..]); + values.string_bytes_value(&output) +} + +/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. +pub(in crate::interpreter) fn eval_find_replace_match( + name: &str, + subject: &[u8], + search: &[u8], + start: usize, +) -> Result, EvalStatus> { + match name { + "str_replace" => Ok(super::strstr::eval_find_subslice(subject, search, start)), + "str_ireplace" => Ok(subject + .get(start..) + .and_then(|tail| { + tail.windows(search.len()) + .position(|window| window.eq_ignore_ascii_case(search)) + }) + .map(|position| position + start)), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs index 560d43c97c..edb9716de3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-split hook. +//! - Runtime dispatch is declared here and implemented through the string-split hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,51 @@ eval_builtin! { direct: StrSplit, values: StrSplit, } + +use super::super::super::*; + +/// Evaluates PHP `str_split(...)` over one string and optional chunk length. +pub(in crate::interpreter) fn eval_builtin_str_split( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_str_split_result(value, None, values) + } + [value, length] => { + let value = eval_expr(value, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_str_split_result(value, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. +pub(in crate::interpreter) fn eval_str_split_result( + value: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let length = match length { + Some(length) => eval_int_value(length, values)?, + None => 1, + }; + if length <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = values.array_new(0)?; + for (index, chunk) in bytes.chunks(length).enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string_bytes_value(chunk)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs b/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs index 1c29ae7f10..cdbf36c901 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-search predicate hook. +//! - Runtime dispatch is declared here and implemented through the string-search predicate hook. eval_builtin! { name: "str_starts_with", @@ -14,3 +14,24 @@ eval_builtin! { direct: StringSearch, values: StringSearch, } + +use super::super::super::*; + +/// Evaluates PHP `str_starts_with(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_str_starts_with( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_builtin_string_search_named("str_starts_with", args, context, scope, values) +} + +/// Applies PHP `str_starts_with(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_str_starts_with_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::str_contains::eval_string_search_named_result("str_starts_with", haystack, needle, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs b/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs index 29096627d2..d46f5db679 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-compare hook. +//! - Runtime dispatch is declared here and implemented through the string-compare hook. eval_builtin! { name: "strcasecmp", @@ -14,3 +14,24 @@ eval_builtin! { direct: StringCompare, values: StringCompare, } + +use super::super::super::*; + +/// Evaluates PHP `strcasecmp(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_strcasecmp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strcmp::eval_builtin_string_compare_named("strcasecmp", args, context, scope, values) +} + +/// Applies PHP `strcasecmp(...)` to two evaluated string values. +pub(in crate::interpreter) fn eval_strcasecmp_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strcmp::eval_string_compare_named_result("strcasecmp", left, right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs b/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs index efa45a0790..c337f5d9d9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-compare hook. +//! - Runtime dispatch is declared here and implemented through the string-compare hook. eval_builtin! { name: "strcmp", @@ -14,3 +14,65 @@ eval_builtin! { direct: StringCompare, values: StringCompare, } + +use super::super::super::*; + +/// Evaluates PHP `strcmp(...)` over two eval expressions. +pub(in crate::interpreter) fn eval_builtin_strcmp( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strcmp::eval_builtin_string_compare_named("strcmp", args, context, scope, values) +} + +/// Applies PHP `strcmp(...)` to two evaluated string values. +pub(in crate::interpreter) fn eval_strcmp_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strcmp::eval_string_compare_named_result("strcmp", left, right, values) +} + +/// Evaluates one named PHP string comparison builtin. +pub(in crate::interpreter) fn eval_builtin_string_compare_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_string_compare_named_result(name, left, right, values) +} + +/// Compares two converted strings and returns -1, 0, or 1. +pub(in crate::interpreter) fn eval_string_compare_named_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut left = values.string_bytes(left)?; + let mut right = values.string_bytes(right)?; + match name { + "strcmp" => {} + "strcasecmp" => { + left.make_ascii_lowercase(); + right.make_ascii_lowercase(); + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let result = match left.cmp(&right) { + std::cmp::Ordering::Less => -1, + std::cmp::Ordering::Equal => 0, + std::cmp::Ordering::Greater => 1, + }; + values.int(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs new file mode 100644 index 0000000000..324951f5ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_filters`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the static stream-filter list helper. + +eval_builtin! { + name: "stream_get_filters", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_get_filters()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_get_filters( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_builtin_stream_introspection_named("stream_get_filters", args, context, values) +} + +/// Builds the result for PHP `stream_get_filters()`. +pub(in crate::interpreter) fn eval_stream_get_filters_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_stream_introspection_named_result("stream_get_filters", context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs new file mode 100644 index 0000000000..e91c9b6c53 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_transports`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the static stream-transport list helper. + +eval_builtin! { + name: "stream_get_transports", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_get_transports()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_get_transports( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_builtin_stream_introspection_named("stream_get_transports", args, context, values) +} + +/// Builds the result for PHP `stream_get_transports()`. +pub(in crate::interpreter) fn eval_stream_get_transports_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_stream_introspection_named_result("stream_get_transports", context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs new file mode 100644 index 0000000000..ee20b2328e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs @@ -0,0 +1,80 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_wrappers`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the eval stream-wrapper registry helper. + +eval_builtin! { + name: "stream_get_wrappers", + area: String, + params: [], + direct: StreamIntrospection, + values: StreamIntrospection, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_get_wrappers()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_get_wrappers( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_builtin_stream_introspection_named("stream_get_wrappers", args, context, values) +} + +/// Builds the result for PHP `stream_get_wrappers()`. +pub(in crate::interpreter) fn eval_stream_get_wrappers_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_get_wrappers::eval_stream_introspection_named_result("stream_get_wrappers", context, values) +} + +/// Evaluates a named stream introspection builtin with no arguments. +pub(in crate::interpreter) fn eval_builtin_stream_introspection_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_introspection_named_result(name, context, values) +} + +/// Builds the static list returned by one eval stream introspection builtin. +pub(in crate::interpreter) fn eval_stream_introspection_named_result( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let items = match name { + "stream_get_filters" => return eval_static_string_array_result(EVAL_STREAM_FILTERS, values), + "stream_get_transports" => { + return eval_static_string_array_result(EVAL_STREAM_TRANSPORTS, values); + } + "stream_get_wrappers" => context.stream_resources().stream_wrappers(EVAL_STREAM_WRAPPERS), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_owned_string_array_result(&items, values) +} + +/// Builds one indexed PHP array from an owned string slice. +pub(in crate::interpreter) fn eval_owned_string_array_result( + items: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs new file mode 100644 index 0000000000..60a855bc06 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs @@ -0,0 +1,69 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_is_local`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the stream boolean predicate helper. + +eval_builtin! { + name: "stream_is_local", + area: String, + params: [stream], + direct: StreamBoolPredicate, + values: StreamBoolPredicate, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_is_local(...)` over one stream expression. +pub(in crate::interpreter) fn eval_builtin_stream_is_local( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_is_local::eval_builtin_stream_bool_predicate_named("stream_is_local", args, context, scope, values) +} + +/// Builds the result for PHP `stream_is_local(...)` from one evaluated stream value. +pub(in crate::interpreter) fn eval_stream_is_local_result( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_is_local::eval_stream_bool_predicate_named_result("stream_is_local", stream, values) +} + +/// Evaluates a named stream boolean predicate over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_bool_predicate_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_bool_predicate_named_result(name, stream, values) +} + +/// Returns elephc's fixed stream-locality and lock-support predicate values. +pub(in crate::interpreter) fn eval_stream_bool_predicate_named_result( + name: &str, + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "stream_is_local" => values.bool_value(true), + "stream_supports_lock" => { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs b/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs new file mode 100644 index 0000000000..f303ec3da8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs @@ -0,0 +1,36 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_supports_lock`. +//! +//! Called from: +//! - `crate::interpreter::builtins::string`. +//! +//! Key details: +//! - Runtime dispatch is declared here and implemented through the stream boolean predicate helper. + +eval_builtin! { + name: "stream_supports_lock", + area: String, + params: [stream], + direct: StreamBoolPredicate, + values: StreamBoolPredicate, +} + +use super::super::super::*; + +/// Evaluates PHP `stream_supports_lock(...)` over one stream expression. +pub(in crate::interpreter) fn eval_builtin_stream_supports_lock( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_is_local::eval_builtin_stream_bool_predicate_named("stream_supports_lock", args, context, scope, values) +} + +/// Builds the result for PHP `stream_supports_lock(...)` from one evaluated stream value. +pub(in crate::interpreter) fn eval_stream_supports_lock_result( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::stream_is_local::eval_stream_bool_predicate_named_result("stream_supports_lock", stream, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs b/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs index 174133dd22..0304dfa981 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing slash unescaping hook. +//! - Runtime dispatch is declared here and implemented through the existing slash unescaping hook. eval_builtin! { name: "stripslashes", @@ -14,3 +14,42 @@ eval_builtin! { direct: Slashes, values: Slashes, } + +use super::super::super::*; + +/// Evaluates PHP `stripslashes(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stripslashes( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_stripslashes_result(value, values) +} + +/// Removes backslash quoting using PHP `stripslashes()` byte semantics. +pub(in crate::interpreter) fn eval_stripslashes_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'\\' { + index += 1; + if let Some(byte) = bytes.get(index).copied() { + output.push(if byte == b'0' { 0 } else { byte }); + index += 1; + } + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs b/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs index b93d90b9be..2cd65327a0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing string-length hook. +//! - Runtime dispatch is declared here and implemented through the existing string-length hook. eval_builtin! { name: "strlen", @@ -14,3 +14,21 @@ eval_builtin! { direct: Strlen, values: Strlen, } + +use super::super::super::*; + +/// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. +pub(in crate::interpreter) fn eval_builtin_strlen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + let bytes = values.string_bytes(value)?; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs index c0e0bc012b..ab68534750 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-position hook. +//! - Runtime dispatch is declared here and implemented through the string-position hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,69 @@ eval_builtin! { direct: StringPosition, values: StringPosition, } + +use super::super::super::*; + +/// Evaluates PHP `strpos(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_strpos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_builtin_string_position_named("strpos", args, context, scope, values) +} + +/// Applies PHP `strpos(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_strpos_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_string_position_named_result("strpos", haystack, needle, values) +} + +/// Evaluates one named PHP byte-string position builtin. +pub(in crate::interpreter) fn eval_builtin_string_position_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [haystack, needle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_string_position_named_result(name, haystack, needle, values) +} + +/// Returns the first or last byte offset of a converted needle, or PHP `false`. +pub(in crate::interpreter) fn eval_string_position_named_result( + name: &str, + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = match name { + "strpos" if needle.is_empty() => Some(0), + "strpos" => haystack + .windows(needle.len()) + .position(|window| window == needle), + "strrpos" if needle.is_empty() => Some(haystack.len()), + "strrpos" => haystack + .windows(needle.len()) + .rposition(|window| window == needle), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + match position { + Some(position) => { + let position = i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(position) + } + None => values.bool_value(false), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs b/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs index 514bc0366f..dfc5602517 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing string-reversal hook. +//! - Runtime dispatch is declared here and implemented through the existing string-reversal hook. eval_builtin! { name: "strrev", @@ -14,3 +14,27 @@ eval_builtin! { direct: Strrev, values: Strrev, } + +use super::super::super::*; + +/// Evaluates PHP's `strrev(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strrev( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + values.strrev(value) +} + +/// Reverses one converted eval string using the runtime string helper. +pub(in crate::interpreter) fn eval_strrev_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + values.strrev(value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs b/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs index a9704e3f80..93b806a4db 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-position hook. +//! - Runtime dispatch is declared here and implemented through the string-position hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,24 @@ eval_builtin! { direct: StringPosition, values: StringPosition, } + +use super::super::super::*; + +/// Evaluates PHP `strrpos(...)` over haystack and needle expressions. +pub(in crate::interpreter) fn eval_builtin_strrpos( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_builtin_string_position_named("strrpos", args, context, scope, values) +} + +/// Applies PHP `strrpos(...)` to evaluated haystack and needle values. +pub(in crate::interpreter) fn eval_strrpos_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strpos::eval_string_position_named_result("strrpos", haystack, needle, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs b/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs index ccd3e4f225..b5d88b1e33 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the strstr hook. +//! - Runtime dispatch is declared here and implemented through the strstr hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,67 @@ eval_builtin! { direct: Strstr, values: Strstr, } + +use super::super::super::*; + +/// Evaluates PHP `strstr(...)` over haystack, needle, and optional prefix mode. +pub(in crate::interpreter) fn eval_builtin_strstr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [haystack, needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + eval_strstr_result(haystack, needle, false, values) + } + [haystack, needle, before_needle] => { + let haystack = eval_expr(haystack, context, scope, values)?; + let needle = eval_expr(needle, context, scope, values)?; + let before_needle = eval_expr(before_needle, context, scope, values)?; + let before_needle = values.truthy(before_needle)?; + eval_strstr_result(haystack, needle, before_needle, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the suffix or prefix selected by PHP `strstr()`, or `false` when absent. +pub(in crate::interpreter) fn eval_strstr_result( + haystack: RuntimeCellHandle, + needle: RuntimeCellHandle, + before_needle: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let haystack = values.string_bytes(haystack)?; + let needle = values.string_bytes(needle)?; + let position = if needle.is_empty() { + Some(0) + } else { + eval_find_subslice(&haystack, &needle, 0) + }; + let Some(position) = position else { + return values.bool_value(false); + }; + let result = if before_needle { + &haystack[..position] + } else { + &haystack[position..] + }; + values.string_bytes_value(result) +} + +/// Finds `needle` inside `haystack` starting from one byte offset. +pub(in crate::interpreter) fn eval_find_subslice( + haystack: &[u8], + needle: &[u8], + start: usize, +) -> Option { + haystack + .get(start..)? + .windows(needle.len()) + .position(|window| window == needle) + .map(|position| position + start) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs b/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs index 585ec8c57c..7b4918239d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-case hook. +//! - Runtime dispatch is declared here and implemented through the string-case hook. eval_builtin! { name: "strtolower", @@ -14,3 +14,76 @@ eval_builtin! { direct: StringCase, values: StringCase, } + +use super::super::super::*; + +/// Evaluates PHP `strtolower(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strtolower( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_builtin_string_case_named("strtolower", args, context, scope, values) +} + +/// Applies PHP `strtolower(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_strtolower_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_string_case_named_result("strtolower", value, values) +} + +/// Evaluates one named ASCII case-conversion string builtin. +pub(in crate::interpreter) fn eval_builtin_string_case_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_string_case_named_result(name, value, values) +} + +/// Converts one eval value through PHP string conversion and ASCII case mapping. +pub(in crate::interpreter) fn eval_string_case_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + match name { + "strtolower" => { + for byte in &mut bytes { + if byte.is_ascii_uppercase() { + *byte += b'a' - b'A'; + } + } + } + "strtoupper" => { + for byte in &mut bytes { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + } + } + "ucfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_lowercase()) { + bytes[0] -= b'a' - b'A'; + } + } + "lcfirst" => { + if bytes.first().is_some_and(|byte| byte.is_ascii_uppercase()) { + bytes[0] += b'a' - b'A'; + } + } + _ => return Err(EvalStatus::UnsupportedConstruct), + } + let value = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs b/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs index 9513a8af7d..c231c6a6a4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-case hook. +//! - Runtime dispatch is declared here and implemented through the string-case hook. eval_builtin! { name: "strtoupper", @@ -14,3 +14,23 @@ eval_builtin! { direct: StringCase, values: StringCase, } + +use super::super::super::*; + +/// Evaluates PHP `strtoupper(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_strtoupper( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_builtin_string_case_named("strtoupper", args, context, scope, values) +} + +/// Applies PHP `strtoupper(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_strtoupper_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_string_case_named_result("strtoupper", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/substr.rs b/crates/elephc-magician/src/interpreter/builtins/string/substr.rs index 032ae8abe7..565814fb11 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/substr.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/substr.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the substring hook. +//! - Runtime dispatch is declared here and implemented through the substring hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,61 @@ eval_builtin! { direct: Substr, values: Substr, } + +use super::super::super::*; + +/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. +pub(in crate::interpreter) fn eval_builtin_substr( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, offset] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_result(value, offset, None, values) + } + [value, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_result(value, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Slices a PHP byte string using PHP `substr()` offset and length rules. +pub(in crate::interpreter) fn eval_substr_result( + value: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(0) + } else { + start.saturating_add(length).min(total) + } + } + }; + let end = end.max(start); + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string_bytes_value(&bytes[start..end]) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs b/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs index cde861329b..b2e635feec 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the substring-replace hook. +//! - Runtime dispatch is declared here and implemented through the substring-replace hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,68 @@ eval_builtin! { direct: SubstrReplace, values: SubstrReplace, } + +use super::super::super::*; + +/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. +pub(in crate::interpreter) fn eval_builtin_substr_replace( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value, replace, offset] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, None, values) + } + [value, replace, offset, length] => { + let value = eval_expr(value, context, scope, values)?; + let replace = eval_expr(replace, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_substr_replace_result(value, replace, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. +pub(in crate::interpreter) fn eval_substr_replace_result( + value: RuntimeCellHandle, + replace: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let replacement = values.string_bytes(replace)?; + let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let offset = eval_int_value(offset, values)?; + let start = if offset < 0 { + (total + offset).max(0) + } else { + offset.min(total) + }; + let end = match length { + None => total, + Some(length) if values.is_null(length)? => total, + Some(length) => { + let length = eval_int_value(length, values)?; + if length < 0 { + (total + length).max(start) + } else { + start.saturating_add(length).min(total) + } + } + }; + let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; + let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut output = Vec::with_capacity(bytes.len() + replacement.len()); + output.extend_from_slice(&bytes[..start]); + output.extend_from_slice(&replacement); + output.extend_from_slice(&bytes[end..]); + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/trim.rs b/crates/elephc-magician/src/interpreter/builtins/string/trim.rs index 472812d550..b3983c23ed 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/trim.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/trim.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the trim-family hook. +//! - Runtime dispatch is declared here and implemented through the trim-family hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,85 @@ eval_builtin! { direct: TrimLike, values: TrimLike, } + +use super::super::super::*; + +/// Evaluates PHP `trim(...)` over one eval expression and optional mask. +pub(in crate::interpreter) fn eval_builtin_trim( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_builtin_trim_like_named("trim", args, context, scope, values) +} + +/// Applies PHP `trim(...)` to one evaluated string and optional mask. +pub(in crate::interpreter) fn eval_trim_result( + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + super::trim::eval_trim_like_named_result("trim", value, mask, values) +} + +pub(in crate::interpreter) const PHP_DEFAULT_TRIM_MASK: &[u8] = b" \n\r\t\x0B\x0C\0"; + +/// Evaluates one named PHP trim-family builtin. +pub(in crate::interpreter) fn eval_builtin_trim_like_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_trim_like_named_result(name, value, None, values) + } + [value, mask] => { + let value = eval_expr(value, context, scope, values)?; + let mask = eval_expr(mask, context, scope, values)?; + eval_trim_like_named_result(name, value, Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Trims one converted string using PHP's default mask or a caller-provided byte mask. +pub(in crate::interpreter) fn eval_trim_like_named_result( + name: &str, + value: RuntimeCellHandle, + mask: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let explicit_mask; + let trim_mask = if let Some(mask) = mask { + explicit_mask = values.string_bytes(mask)?; + explicit_mask.as_slice() + } else { + PHP_DEFAULT_TRIM_MASK + }; + + let mut start = 0; + let mut end = bytes.len(); + if matches!(name, "trim" | "ltrim") { + while start < end && trim_mask.contains(&bytes[start]) { + start += 1; + } + } + if matches!(name, "trim" | "rtrim" | "chop") { + while end > start && trim_mask.contains(&bytes[end - 1]) { + end -= 1; + } + } + if !matches!(name, "trim" | "ltrim" | "rtrim" | "chop") { + return Err(EvalStatus::UnsupportedConstruct); + } + + let value = + String::from_utf8(bytes[start..end].to_vec()).map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(&value) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs b/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs index 8ebe1a805c..49d934b5b2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the string-case hook. +//! - Runtime dispatch is declared here and implemented through the string-case hook. eval_builtin! { name: "ucfirst", @@ -14,3 +14,23 @@ eval_builtin! { direct: StringCase, values: StringCase, } + +use super::super::super::*; + +/// Evaluates PHP `ucfirst(...)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ucfirst( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_builtin_string_case_named("ucfirst", args, context, scope, values) +} + +/// Applies PHP `ucfirst(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_ucfirst_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::strtolower::eval_string_case_named_result("ucfirst", value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs b/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs index d94d2fbf8e..f73c3a6f2d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the ucwords hook. +//! - Runtime dispatch is declared here and implemented through the ucwords hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -16,3 +16,51 @@ eval_builtin! { direct: Ucwords, values: Ucwords, } + +use super::super::super::*; + +/// Evaluates PHP `ucwords(...)` over one string and optional separator expression. +pub(in crate::interpreter) fn eval_builtin_ucwords( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_ucwords_result(value, None, values) + } + [value, separators] => { + let value = eval_expr(value, context, scope, values)?; + let separators = eval_expr(separators, context, scope, values)?; + eval_ucwords_result(value, Some(separators), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Uppercases ASCII lowercase bytes at the start of words separated by PHP delimiters. +pub(in crate::interpreter) fn eval_ucwords_result( + value: RuntimeCellHandle, + separators: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut bytes = values.string_bytes(value)?; + let separators = match separators { + Some(separators) => values.string_bytes(separators)?, + None => b" \t\r\n\x0c\x0b".to_vec(), + }; + let mut word_start = true; + for byte in &mut bytes { + if separators.contains(byte) { + word_start = true; + } else if word_start { + if byte.is_ascii_lowercase() { + *byte -= b'a' - b'A'; + } + word_start = false; + } + } + values.string_bytes_value(&bytes) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs b/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs index 5e38fe99a4..60e6688b3d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing URL decode hook. +//! - Runtime dispatch is declared here and implemented through the existing URL decode hook. eval_builtin! { name: "urldecode", @@ -14,3 +14,75 @@ eval_builtin! { direct: UrlDecode, values: UrlDecode, } + +use super::super::super::*; + +/// Evaluates PHP `urldecode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_urldecode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urldecode::eval_builtin_url_decode_named("urldecode", args, context, scope, values) +} + +/// Applies PHP `urldecode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_urldecode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urldecode::eval_url_decode_named_result("urldecode", value, values) +} + +/// Evaluates a named PHP URL decoder over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_url_decode_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_decode_named_result(name, value, values) +} + +/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. +pub(in crate::interpreter) fn eval_url_decode_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let plus_to_space = match name { + "urldecode" => true, + "rawurldecode" => false, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + let mut index = 0; + while index < bytes.len() { + if bytes[index] == b'+' && plus_to_space { + output.push(b' '); + index += 1; + } else if bytes[index] == b'%' && index + 2 < bytes.len() { + if let (Some(high), Some(low)) = ( + super::hex2bin::eval_hex_nibble(bytes[index + 1]), + super::hex2bin::eval_hex_nibble(bytes[index + 2]), + ) { + output.push((high << 4) | low); + index += 3; + continue; + } + output.push(bytes[index]); + index += 1; + } else { + output.push(bytes[index]); + index += 1; + } + } + values.string_bytes_value(&output) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs b/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs index 8504090034..04b80e2070 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing URL encode hook. +//! - Runtime dispatch is declared here and implemented through the existing URL encode hook. eval_builtin! { name: "urlencode", @@ -14,3 +14,74 @@ eval_builtin! { direct: UrlEncode, values: UrlEncode, } + +use super::super::super::*; + +/// Evaluates PHP `urlencode(...)` over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_urlencode( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urlencode::eval_builtin_url_encode_named("urlencode", args, context, scope, values) +} + +/// Applies PHP `urlencode(...)` to one evaluated string value. +pub(in crate::interpreter) fn eval_urlencode_result( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + super::urlencode::eval_url_encode_named_result("urlencode", value, values) +} + +/// Evaluates a named PHP URL encoder over one eval string expression. +pub(in crate::interpreter) fn eval_builtin_url_encode_named( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_url_encode_named_result(name, value, values) +} + +/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. +pub(in crate::interpreter) fn eval_url_encode_named_result( + name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let mut output = Vec::with_capacity(bytes.len()); + const HEX: &[u8; 16] = b"0123456789ABCDEF"; + for byte in bytes { + if eval_url_encode_keeps_byte(name, byte)? { + output.push(byte); + } else if name == "urlencode" && byte == b' ' { + output.push(b'+'); + } else { + output.push(b'%'); + output.push(HEX[(byte >> 4) as usize]); + output.push(HEX[(byte & 0x0f) as usize]); + } + } + values.string_bytes_value(&output) +} + +/// Returns whether a byte remains unescaped for the selected PHP URL encoder. +pub(in crate::interpreter) fn eval_url_encode_keeps_byte( + name: &str, + byte: u8, +) -> Result { + let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); + match name { + "urlencode" => Ok(common), + "rawurlencode" => Ok(common || byte == b'~'), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs b/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs index 458047498d..1bb799f86b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime behavior stays delegated to the wordwrap hook. +//! - Runtime dispatch is declared here and implemented through the wordwrap hook. use super::super::spec::EvalBuiltinDefaultValue; @@ -21,3 +21,137 @@ eval_builtin! { direct: Wordwrap, values: Wordwrap, } + +use super::super::super::*; + +/// Evaluates PHP `wordwrap(...)` over one string and optional wrapping controls. +pub(in crate::interpreter) fn eval_builtin_wordwrap( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_wordwrap_result(value, None, None, None, values) + } + [value, width] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + eval_wordwrap_result(value, Some(width), None, None, values) + } + [value, width, break_string] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), None, values) + } + [value, width, break_string, cut] => { + let value = eval_expr(value, context, scope, values)?; + let width = eval_expr(width, context, scope, values)?; + let break_string = eval_expr(break_string, context, scope, values)?; + let cut = eval_expr(cut, context, scope, values)?; + eval_wordwrap_result(value, Some(width), Some(break_string), Some(cut), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Wraps a byte string at PHP word boundaries and preserves existing newlines. +pub(in crate::interpreter) fn eval_wordwrap_result( + value: RuntimeCellHandle, + width: Option, + break_string: Option, + cut: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let width = match width { + Some(width) => eval_int_value(width, values)?, + None => 75, + }; + let break_string = match break_string { + Some(break_string) => values.string_bytes(break_string)?, + None => b"\n".to_vec(), + }; + if break_string.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let cut = match cut { + Some(cut) => values.truthy(cut)?, + None => false, + }; + if width == 0 && cut { + return Err(EvalStatus::RuntimeFatal); + } + if bytes.is_empty() { + return values.string_bytes_value(&bytes); + } + let output = eval_wordwrap_bytes(&bytes, width, &break_string, cut); + values.string_bytes_value(&output) +} + +/// Applies the core PHP word-wrap scan over already converted byte slices. +pub(in crate::interpreter) fn eval_wordwrap_bytes( + bytes: &[u8], + width: i64, + break_string: &[u8], + cut: bool, +) -> Vec { + if width < 0 && cut { + let mut output = Vec::with_capacity(bytes.len() + (bytes.len() * break_string.len())); + for byte in bytes { + output.extend_from_slice(break_string); + output.push(*byte); + } + return output; + } + + let width = width.max(0) as usize; + let mut output = Vec::with_capacity(bytes.len()); + let mut line_start = 0; + let mut last_space = None; + let mut index = 0; + while index < bytes.len() { + match bytes[index] { + b'\n' => { + output.extend_from_slice(&bytes[line_start..=index]); + index += 1; + line_start = index; + last_space = None; + } + b' ' => { + if index.saturating_sub(line_start) >= width { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + index += 1; + line_start = index; + last_space = None; + } else { + last_space = Some(index); + index += 1; + } + } + _ if index.saturating_sub(line_start) >= width => { + if let Some(space) = last_space { + output.extend_from_slice(&bytes[line_start..space]); + output.extend_from_slice(break_string); + line_start = space + 1; + last_space = None; + } else if cut && width > 0 { + output.extend_from_slice(&bytes[line_start..index]); + output.extend_from_slice(break_string); + line_start = index; + } else { + index += 1; + } + } + _ => { + index += 1; + } + } + } + output.extend_from_slice(&bytes[line_start..]); + output +} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/ctype.rs b/crates/elephc-magician/src/interpreter/builtins/strings/ctype.rs deleted file mode 100644 index 78a4d5daa8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/ctype.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Purpose: -//! ASCII ctype predicate builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; - -/// Evaluates PHP `ctype_*` predicates over one eval string expression. -pub(in crate::interpreter) fn eval_builtin_ctype( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_ctype_result(name, value, values) -} - -/// Returns the PHP boolean result for one ASCII `ctype_*` byte-string check. -pub(in crate::interpreter) fn eval_ctype_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut matches = !bytes.is_empty(); - for byte in bytes { - if !eval_ctype_byte_matches(name, byte)? { - matches = false; - break; - } - } - values.bool_value(matches) -} - -/// Checks one byte against the selected PHP ASCII character class. -pub(in crate::interpreter) fn eval_ctype_byte_matches( - name: &str, - byte: u8, -) -> Result { - match name { - "ctype_alpha" => Ok(byte.is_ascii_alphabetic()), - "ctype_digit" => Ok(byte.is_ascii_digit()), - "ctype_alnum" => Ok(byte.is_ascii_alphanumeric()), - "ctype_space" => Ok(matches!(byte, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/explode.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/explode.rs deleted file mode 100644 index 32876f56e1..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/explode.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `explode`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the string split/join hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "explode", - area: String, - params: [separator, string, limit = EvalBuiltinDefaultValue::Int(i64::MAX)], - direct: StringSplitJoin, - values: StringSplitJoin, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzcompress.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzcompress.rs deleted file mode 100644 index 8aaa4f2b69..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzcompress.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gzcompress`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the gzip/zlib hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "gzcompress", - area: String, - params: [data, level = EvalBuiltinDefaultValue::Int(-1)], - direct: Gzip, - values: Gzip, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzdeflate.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzdeflate.rs deleted file mode 100644 index d7b218bdc2..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzdeflate.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gzdeflate`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the gzip/zlib hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "gzdeflate", - area: String, - params: [data, level = EvalBuiltinDefaultValue::Int(-1)], - direct: Gzip, - values: Gzip, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzinflate.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzinflate.rs deleted file mode 100644 index 8418065191..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzinflate.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gzinflate`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the gzip/zlib hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "gzinflate", - area: String, - params: [data, max_length = EvalBuiltinDefaultValue::Int(0)], - direct: Gzip, - values: Gzip, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzuncompress.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzuncompress.rs deleted file mode 100644 index 99f81a55a1..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/gzuncompress.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `gzuncompress`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the gzip/zlib hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "gzuncompress", - area: String, - params: [data, max_length = EvalBuiltinDefaultValue::Int(0)], - direct: Gzip, - values: Gzip, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash.rs deleted file mode 100644 index dbc750a3e6..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hash`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the one-shot hash hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "hash", - area: String, - params: [algo, data, binary = EvalBuiltinDefaultValue::Bool(false)], - direct: HashOneShot, - values: HashOneShot, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_algos.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_algos.rs deleted file mode 100644 index 866ad7f15a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_algos.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hash_algos`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the static hash algorithm list helper. - -eval_builtin! { - name: "hash_algos", - area: String, - params: [], - direct: HashAlgos, - values: HashAlgos, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_copy.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_copy.rs deleted file mode 100644 index 78cf1b5ec9..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_copy.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hash_copy`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the incremental hash-context hook. - -eval_builtin! { - name: "hash_copy", - area: String, - params: [context], - direct: HashContext, - values: HashContext, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_file.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_file.rs deleted file mode 100644 index d3e22f743e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_file.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hash_file`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the one-shot hash hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "hash_file", - area: String, - params: [algo, filename, binary = EvalBuiltinDefaultValue::Bool(false)], - direct: HashOneShot, - values: HashOneShot, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_final.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_final.rs deleted file mode 100644 index d0f94d54ff..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_final.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hash_final`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the incremental hash-context hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "hash_final", - area: String, - params: [context, binary = EvalBuiltinDefaultValue::Bool(false)], - direct: HashContext, - values: HashContext, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_hmac.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_hmac.rs deleted file mode 100644 index 268e9b1a8a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_hmac.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hash_hmac`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the one-shot hash hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "hash_hmac", - area: String, - params: [algo, data, key, binary = EvalBuiltinDefaultValue::Bool(false)], - direct: HashOneShot, - values: HashOneShot, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_init.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_init.rs deleted file mode 100644 index 6c5a43e0e5..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_init.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hash_init`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the incremental hash-context hook. -//! - Optional HMAC parameters remain metadata-only for current eval behavior. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "hash_init", - area: String, - params: [ - algo, - flags = EvalBuiltinDefaultValue::Int(0), - key = EvalBuiltinDefaultValue::String(""), - ], - direct: HashContext, - values: HashContext, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_update.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_update.rs deleted file mode 100644 index fddcc9deb6..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/hash_update.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `hash_update`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the incremental hash-context hook. - -eval_builtin! { - name: "hash_update", - area: String, - params: [context, data], - direct: HashContext, - values: HashContext, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/implode.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/implode.rs deleted file mode 100644 index 0796996aa1..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/implode.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `implode`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the string split/join hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "implode", - area: String, - params: [separator = EvalBuiltinDefaultValue::Null, array], - required: 1, - direct: StringSplitJoin, - values: StringSplitJoin, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/md5.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/md5.rs deleted file mode 100644 index 1b7a3c2f8a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/md5.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `md5`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the one-shot hash hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "md5", - area: String, - params: [string, binary = EvalBuiltinDefaultValue::Bool(false)], - direct: HashOneShot, - values: HashOneShot, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs deleted file mode 100644 index a3584c0252..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries for string-adjacent stream introspection builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` module loading. -//! -//! Key details: -//! - Runtime behavior stays delegated to existing stream-introspection helpers. - -mod stream_get_filters; -mod stream_get_transports; -mod stream_get_wrappers; -mod stream_is_local; -mod stream_supports_lock; -mod explode; -mod gzcompress; -mod gzdeflate; -mod gzinflate; -mod gzuncompress; -mod hash; -mod hash_algos; -mod hash_copy; -mod hash_file; -mod hash_final; -mod hash_hmac; -mod hash_init; -mod hash_update; -mod implode; -mod md5; -mod sha1; diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/sha1.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/sha1.rs deleted file mode 100644 index 69932f1bd7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/sha1.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `sha1`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the one-shot hash hook. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "sha1", - area: String, - params: [string, binary = EvalBuiltinDefaultValue::Bool(false)], - direct: HashOneShot, - values: HashOneShot, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_filters.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_filters.rs deleted file mode 100644 index 9c08a044a4..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_filters.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_get_filters`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the static stream-filter list helper. - -eval_builtin! { - name: "stream_get_filters", - area: String, - params: [], - direct: StreamIntrospection, - values: StreamIntrospection, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_transports.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_transports.rs deleted file mode 100644 index 9dc5bdcd1d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_transports.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_get_transports`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the static stream-transport list helper. - -eval_builtin! { - name: "stream_get_transports", - area: String, - params: [], - direct: StreamIntrospection, - values: StreamIntrospection, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_wrappers.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_wrappers.rs deleted file mode 100644 index 412b59c490..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_get_wrappers.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_get_wrappers`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the eval stream-wrapper registry helper. - -eval_builtin! { - name: "stream_get_wrappers", - area: String, - params: [], - direct: StreamIntrospection, - values: StreamIntrospection, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_is_local.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_is_local.rs deleted file mode 100644 index 3200dc2b51..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_is_local.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_is_local`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream boolean predicate helper. - -eval_builtin! { - name: "stream_is_local", - area: String, - params: [stream], - direct: StreamBoolPredicate, - values: StreamBoolPredicate, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_supports_lock.rs b/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_supports_lock.rs deleted file mode 100644 index 076771f0a5..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/declarations/stream_supports_lock.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_supports_lock`. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream boolean predicate helper. - -eval_builtin! { - name: "stream_supports_lock", - area: String, - params: [stream], - direct: StreamBoolPredicate, - values: StreamBoolPredicate, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/grapheme_strrev.rs b/crates/elephc-magician/src/interpreter/builtins/strings/grapheme_strrev.rs deleted file mode 100644 index b730a9da66..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/grapheme_strrev.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Implements eval's PHP `grapheme_strrev()` builtin. -//! Reverses valid UTF-8 strings by grapheme cluster while preserving each -//! cluster's internal byte order. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports used by call dispatch. -//! -//! Key details: -//! - Invalid UTF-8 returns PHP false, matching elephc's `__rt_grapheme_strrev` -//! string-or-false contract. - -use unicode_segmentation::UnicodeSegmentation; - -use super::super::super::*; - -/// Evaluates PHP `grapheme_strrev(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_grapheme_strrev( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_grapheme_strrev_result(value, values) -} - -/// Reverses a materialized PHP string by grapheme cluster or returns false for invalid UTF-8. -pub(in crate::interpreter) fn eval_grapheme_strrev_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let Ok(source) = std::str::from_utf8(&bytes) else { - return values.bool_value(false); - }; - let reversed = source.graphemes(true).rev().collect::(); - values.string(&reversed) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/hash_context.rs b/crates/elephc-magician/src/interpreter/builtins/strings/hash_context.rs deleted file mode 100644 index 3946332b54..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/hash_context.rs +++ /dev/null @@ -1,143 +0,0 @@ -//! Purpose: -//! Implements eval incremental hash context builtins. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - HashContext resources are owned by the eval resource table and backed by -//! elephc-crypto opaque handles. -//! - HMAC streaming mode is intentionally not supported, matching the main type checker. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP `hash_init($algo)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_hash_init( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [algo] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let algo = eval_expr(algo, context, scope, values)?; - eval_hash_init_result(algo, context, values) -} - -/// Opens an incremental hash context resource. -pub(in crate::interpreter) fn eval_hash_init_result( - algo: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let algo = values.string_bytes(algo)?; - match context.stream_resources_mut().open_hash_context(&algo) { - Some(id) => values.resource(id), - None => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `hash_update($context, $data)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_hash_update( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hash_context, data] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hash_context = eval_expr(hash_context, context, scope, values)?; - let data = eval_expr(data, context, scope, values)?; - eval_hash_update_result(hash_context, data, context, values) -} - -/// Feeds data into a materialized incremental hash context. -pub(in crate::interpreter) fn eval_hash_update_result( - hash_context: RuntimeCellHandle, - data: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_hash_context_resource_id(hash_context, values)?; - let data = values.string_bytes(data)?; - values.bool_value(context.stream_resources_mut().update_hash_context(id, &data)) -} - -/// Evaluates PHP `hash_final($context, $binary = false)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_hash_final( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(1..=2).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let hash_context = eval_expr(&args[0], context, scope, values)?; - let binary = match args.get(1) { - Some(binary) => { - let binary = eval_expr(binary, context, scope, values)?; - values.truthy(binary)? - } - None => false, - }; - eval_hash_final_result(hash_context, binary, context, values) -} - -/// Finalizes a materialized incremental hash context. -pub(in crate::interpreter) fn eval_hash_final_result( - hash_context: RuntimeCellHandle, - binary: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_hash_context_resource_id(hash_context, values)?; - let raw = context - .stream_resources_mut() - .finalize_hash_context(id) - .ok_or(EvalStatus::RuntimeFatal)?; - eval_format_digest_result(&raw, binary, values) -} - -/// Evaluates PHP `hash_copy($context)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_hash_copy( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [hash_context] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let hash_context = eval_expr(hash_context, context, scope, values)?; - eval_hash_copy_result(hash_context, context, values) -} - -/// Clones a materialized incremental hash context into a new resource. -pub(in crate::interpreter) fn eval_hash_copy_result( - hash_context: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_hash_context_resource_id(hash_context, values)?; - match context.stream_resources_mut().copy_hash_context(id) { - Some(copy_id) => values.resource(copy_id), - None => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts a runtime resource cell into eval's zero-based hash context id. -fn eval_hash_context_resource_id( - hash_context: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(hash_context)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(hash_context, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/html.rs b/crates/elephc-magician/src/interpreter/builtins/strings/html.rs deleted file mode 100644 index 84ca0f1211..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/html.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Purpose: -//! HTML entity encode/decode builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; - -/// Evaluates eval HTML entity encode/decode builtins over one string expression. -pub(in crate::interpreter) fn eval_builtin_html_entity( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_html_entity_result(name, value, values) -} - -/// Applies the eval-supported HTML entity transform for one PHP string value. -pub(in crate::interpreter) fn eval_html_entity_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "htmlspecialchars" | "htmlentities" => eval_htmlspecialchars_result(value, values), - "html_entity_decode" => eval_html_entity_decode_result(value, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Encodes the HTML-special byte characters covered by elephc's static helper. -pub(in crate::interpreter) fn eval_htmlspecialchars_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - for byte in bytes { - match byte { - b'&' => output.extend_from_slice(b"&"), - b'<' => output.extend_from_slice(b"<"), - b'>' => output.extend_from_slice(b">"), - b'"' => output.extend_from_slice(b"""), - b'\'' => output.extend_from_slice(b"'"), - _ => output.push(byte), - } - } - values.string_bytes_value(&output) -} - -/// Decodes one pass of the HTML entities emitted by the eval/static encoders. -pub(in crate::interpreter) fn eval_html_entity_decode_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'&' { - if let Some((decoded, width)) = eval_html_entity_at(&bytes[index..]) { - output.push(decoded); - index += width; - continue; - } - } - output.push(bytes[index]); - index += 1; - } - values.string_bytes_value(&output) -} - -/// Returns the decoded byte and consumed width for one supported HTML entity. -pub(in crate::interpreter) fn eval_html_entity_at(bytes: &[u8]) -> Option<(u8, usize)> { - for (entity, decoded) in [ - (b"<".as_slice(), b'<'), - (b">".as_slice(), b'>'), - (b""".as_slice(), b'"'), - (b"'".as_slice(), b'\''), - (b"'".as_slice(), b'\''), - (b"&".as_slice(), b'&'), - ] { - if bytes.starts_with(entity) { - return Some((decoded, entity.len())); - } - } - None -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs b/crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs deleted file mode 100644 index 3a6cffadc8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/introspection.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Purpose: -//! Hash algorithm, SPL class, and stream introspection list builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; - -/// Evaluates PHP `hash_algos()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_hash_algos( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values) -} - -/// Builds the indexed array returned by eval `hash_algos()`. -pub(in crate::interpreter) fn eval_hash_algos_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_HASH_ALGOS, values) -} - -/// Builds one indexed PHP array from a static string slice. -pub(in crate::interpreter) fn eval_static_string_array_result( - items: &[&str], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(items.len())?; - for (index, item) in items.iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string(item)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `spl_classes()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_spl_classes( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values) -} - -/// Builds the static class-name list returned by eval `spl_classes()`. -pub(in crate::interpreter) fn eval_spl_classes_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) -} - -/// Evaluates PHP stream introspection list builtins with no arguments. -pub(in crate::interpreter) fn eval_builtin_stream_introspection( - name: &str, - args: &[EvalExpr], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_introspection_result(name, context, values) -} - -/// Builds the static list returned by one eval stream introspection builtin. -pub(in crate::interpreter) fn eval_stream_introspection_result( - name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let items = match name { - "stream_get_filters" => return eval_static_string_array_result(EVAL_STREAM_FILTERS, values), - "stream_get_transports" => { - return eval_static_string_array_result(EVAL_STREAM_TRANSPORTS, values); - } - "stream_get_wrappers" => context.stream_resources().stream_wrappers(EVAL_STREAM_WRAPPERS), - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_owned_string_array_result(&items, values) -} - -/// Builds one indexed PHP array from an owned string slice. -fn eval_owned_string_array_result( - items: &[String], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(items.len())?; - for (index, item) in items.iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string(item)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP stream boolean-introspection builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_stream_bool_predicate( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - eval_stream_bool_predicate_result(name, stream, values) -} - -/// Returns elephc's fixed stream-locality and lock-support predicate values. -pub(in crate::interpreter) fn eval_stream_bool_predicate_result( - name: &str, - stream: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "stream_is_local" => values.bool_value(true), - "stream_supports_lock" => { - if values.type_tag(stream)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - values.bool_value(true) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/mod.rs b/crates/elephc-magician/src/interpreter/builtins/strings/mod.rs deleted file mode 100644 index c9506be692..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Groups string, hash, ctype, SPL, and stream-introspection eval builtins. -//! Submodules are split by PHP builtin family and re-exported for call dispatch. -//! -//! Called from: -//! - `crate::interpreter::builtins` re-exports used by core call dispatch. -//! -//! Key details: -//! - Helpers stay scoped to the eval interpreter and preserve PHP-visible runtime behavior. - -mod ctype; -mod declarations; -mod grapheme_strrev; -mod gzip; -mod hash; -mod hash_context; -mod html; -mod introspection; -mod nl2br; -mod pad; -mod repeat; -mod replace; -mod simple; -mod split; -mod substr; -mod url; - -pub(in crate::interpreter) use ctype::*; -pub(in crate::interpreter) use grapheme_strrev::*; -pub(in crate::interpreter) use gzip::*; -pub(in crate::interpreter) use hash::*; -pub(in crate::interpreter) use hash_context::*; -pub(in crate::interpreter) use html::*; -pub(in crate::interpreter) use introspection::*; -pub(in crate::interpreter) use nl2br::*; -pub(in crate::interpreter) use pad::*; -pub(in crate::interpreter) use repeat::*; -pub(in crate::interpreter) use replace::*; -pub(in crate::interpreter) use simple::*; -pub(in crate::interpreter) use split::*; -pub(in crate::interpreter) use substr::*; -pub(in crate::interpreter) use url::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/nl2br.rs b/crates/elephc-magician/src/interpreter/builtins/strings/nl2br.rs deleted file mode 100644 index 0ce573bed5..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/nl2br.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Purpose: -//! Newline-to-break string builtin. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; - -/// Evaluates PHP's `nl2br(...)` over one eval expression and optional XHTML flag. -pub(in crate::interpreter) fn eval_builtin_nl2br( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_nl2br_result(value, true, values) - } - [value, use_xhtml] => { - let value = eval_expr(value, context, scope, values)?; - let use_xhtml = eval_expr(use_xhtml, context, scope, values)?; - let use_xhtml = values.truthy(use_xhtml)?; - eval_nl2br_result(value, use_xhtml, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Inserts an HTML line break before each PHP newline sequence while preserving bytes. -pub(in crate::interpreter) fn eval_nl2br_result( - value: RuntimeCellHandle, - use_xhtml: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let br = if use_xhtml { - b"
".as_slice() - } else { - b"
".as_slice() - }; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - let byte = bytes[index]; - if byte == b'\r' || byte == b'\n' { - output.extend_from_slice(br); - output.push(byte); - if index + 1 < bytes.len() - && ((byte == b'\r' && bytes[index + 1] == b'\n') - || (byte == b'\n' && bytes[index + 1] == b'\r')) - { - output.push(bytes[index + 1]); - index += 2; - continue; - } - } else { - output.push(byte); - } - index += 1; - } - values.string_bytes_value(&output) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/pad.rs b/crates/elephc-magician/src/interpreter/builtins/strings/pad.rs deleted file mode 100644 index 9189031406..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/pad.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Purpose: -//! String padding builtin. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP `str_pad(...)` over a string, target length, pad string, and pad mode. -pub(in crate::interpreter) fn eval_builtin_str_pad( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, length] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_str_pad_result(value, length, None, None, values) - } - [value, length, pad_string] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let pad_string = eval_expr(pad_string, context, scope, values)?; - eval_str_pad_result(value, length, Some(pad_string), None, values) - } - [value, length, pad_string, pad_type] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let pad_string = eval_expr(pad_string, context, scope, values)?; - let pad_type = eval_expr(pad_type, context, scope, values)?; - eval_str_pad_result(value, length, Some(pad_string), Some(pad_type), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Pads one byte string to a PHP target length using cyclic pad bytes. -pub(in crate::interpreter) fn eval_str_pad_result( - value: RuntimeCellHandle, - length: RuntimeCellHandle, - pad_string: Option, - pad_type: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let target_length = eval_int_value(length, values)?; - let Ok(target_length) = usize::try_from(target_length) else { - return values.string_bytes_value(&bytes); - }; - if target_length <= bytes.len() { - return values.string_bytes_value(&bytes); - } - - let pad_string = match pad_string { - Some(pad_string) => values.string_bytes(pad_string)?, - None => b" ".to_vec(), - }; - if pad_string.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let pad_type = match pad_type { - Some(pad_type) => eval_int_value(pad_type, values)?, - None => 1, - }; - let (left_pad, right_pad) = eval_str_pad_sides(target_length - bytes.len(), pad_type)?; - let capacity = bytes - .len() - .checked_add(left_pad) - .and_then(|size| size.checked_add(right_pad)) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(capacity); - eval_append_repeated_pad(&mut output, &pad_string, left_pad); - output.extend_from_slice(&bytes); - eval_append_repeated_pad(&mut output, &pad_string, right_pad); - values.string_bytes_value(&output) -} - -/// Splits a `str_pad()` pad budget into left and right byte counts. -pub(in crate::interpreter) fn eval_str_pad_sides( - pad_budget: usize, - pad_type: i64, -) -> Result<(usize, usize), EvalStatus> { - match pad_type { - 0 => Ok((pad_budget, 0)), - 1 => Ok((0, pad_budget)), - 2 => Ok((pad_budget / 2, pad_budget - (pad_budget / 2))), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Appends `count` bytes by cycling through the provided non-empty pad string. -pub(in crate::interpreter) fn eval_append_repeated_pad( - output: &mut Vec, - pad_string: &[u8], - count: usize, -) { - for index in 0..count { - output.push(pad_string[index % pad_string.len()]); - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/repeat.rs b/crates/elephc-magician/src/interpreter/builtins/strings/repeat.rs deleted file mode 100644 index 68c1e2e670..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/repeat.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Purpose: -//! String repeat builtin. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP's `str_repeat(...)` over one eval expression pair. -pub(in crate::interpreter) fn eval_builtin_str_repeat( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value, times] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let times = eval_expr(times, context, scope, values)?; - eval_str_repeat_result(value, times, values) -} - -/// Repeats one PHP string byte sequence according to a PHP-cast integer count. -pub(in crate::interpreter) fn eval_str_repeat_result( - value: RuntimeCellHandle, - times: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let times = eval_int_value(times, values)?; - if times < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let times = usize::try_from(times).map_err(|_| EvalStatus::RuntimeFatal)?; - let capacity = bytes - .len() - .checked_mul(times) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(capacity); - for _ in 0..times { - output.extend_from_slice(&bytes); - } - values.string_bytes_value(&output) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/replace.rs b/crates/elephc-magician/src/interpreter/builtins/strings/replace.rs deleted file mode 100644 index d074e80a07..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/replace.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Purpose: -//! String replace builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP's `str_replace(...)` or `str_ireplace(...)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_str_replace( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [search, replace, subject] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let search = eval_expr(search, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let subject = eval_expr(subject, context, scope, values)?; - eval_str_replace_result(name, search, replace, subject, values) -} - -/// Replaces every non-overlapping occurrence of a byte-string needle in a subject. -pub(in crate::interpreter) fn eval_str_replace_result( - name: &str, - search: RuntimeCellHandle, - replace: RuntimeCellHandle, - subject: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let search = values.string_bytes(search)?; - let replace = values.string_bytes(replace)?; - let subject = values.string_bytes(subject)?; - if search.is_empty() { - return values.string_bytes_value(&subject); - } - - let mut output = Vec::with_capacity(subject.len()); - let mut start = 0; - while let Some(found) = eval_find_replace_match(name, &subject, &search, start)? { - output.extend_from_slice(&subject[start..found]); - output.extend_from_slice(&replace); - start = found + search.len(); - } - output.extend_from_slice(&subject[start..]); - values.string_bytes_value(&output) -} - -/// Finds the next replacement match using case-sensitive or ASCII-insensitive comparison. -pub(in crate::interpreter) fn eval_find_replace_match( - name: &str, - subject: &[u8], - search: &[u8], - start: usize, -) -> Result, EvalStatus> { - match name { - "str_replace" => Ok(eval_find_subslice(subject, search, start)), - "str_ireplace" => Ok(subject - .get(start..) - .and_then(|tail| { - tail.windows(search.len()) - .position(|window| window.eq_ignore_ascii_case(search)) - }) - .map(|position| position + start)), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/simple.rs b/crates/elephc-magician/src/interpreter/builtins/strings/simple.rs deleted file mode 100644 index fa2952d965..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/simple.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! Purpose: -//! Small scalar string wrappers such as strrev and chr. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP's `strrev(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_strrev( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - values.strrev(value) -} - -/// Evaluates PHP's `chr(...)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_chr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_chr_result(value, values) -} - -/// Converts one eval value to a PHP integer and returns the low byte as a string. -pub(in crate::interpreter) fn eval_chr_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - values.string_bytes_value(&[value as u8]) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/split.rs b/crates/elephc-magician/src/interpreter/builtins/strings/split.rs deleted file mode 100644 index 9615e57124..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/split.rs +++ /dev/null @@ -1,57 +0,0 @@ -//! Purpose: -//! String split builtin. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP `str_split(...)` over one string and optional chunk length. -pub(in crate::interpreter) fn eval_builtin_str_split( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_str_split_result(value, None, values) - } - [value, length] => { - let value = eval_expr(value, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_str_split_result(value, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Splits one byte string into indexed string chunks using PHP `str_split()` rules. -pub(in crate::interpreter) fn eval_str_split_result( - value: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let length = match length { - Some(length) => eval_int_value(length, values)?, - None => 1, - }; - if length <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut result = values.array_new(0)?; - for (index, chunk) in bytes.chunks(length).enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string_bytes_value(chunk)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/substr.rs b/crates/elephc-magician/src/interpreter/builtins/strings/substr.rs deleted file mode 100644 index 44345a18a8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/substr.rs +++ /dev/null @@ -1,130 +0,0 @@ -//! Purpose: -//! Substring and substring replacement builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP's `substr(...)` over one eval string, offset, and optional length. -pub(in crate::interpreter) fn eval_builtin_substr( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, offset] => { - let value = eval_expr(value, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_substr_result(value, offset, None, values) - } - [value, offset, length] => { - let value = eval_expr(value, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_substr_result(value, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Slices a PHP byte string using PHP `substr()` offset and length rules. -pub(in crate::interpreter) fn eval_substr_result( - value: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = eval_int_value(offset, values)?; - let start = if offset < 0 { - (total + offset).max(0) - } else { - offset.min(total) - }; - let end = match length { - None => total, - Some(length) if values.is_null(length)? => total, - Some(length) => { - let length = eval_int_value(length, values)?; - if length < 0 { - (total + length).max(0) - } else { - start.saturating_add(length).min(total) - } - } - }; - let end = end.max(start); - let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; - let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; - values.string_bytes_value(&bytes[start..end]) -} - -/// Evaluates PHP's `substr_replace(...)` over eval scalar byte strings. -pub(in crate::interpreter) fn eval_builtin_substr_replace( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value, replace, offset] => { - let value = eval_expr(value, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_substr_replace_result(value, replace, offset, None, values) - } - [value, replace, offset, length] => { - let value = eval_expr(value, context, scope, values)?; - let replace = eval_expr(replace, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_substr_replace_result(value, replace, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Replaces the byte range selected by PHP `substr_replace()` scalar rules. -pub(in crate::interpreter) fn eval_substr_replace_result( - value: RuntimeCellHandle, - replace: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let replacement = values.string_bytes(replace)?; - let total = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let offset = eval_int_value(offset, values)?; - let start = if offset < 0 { - (total + offset).max(0) - } else { - offset.min(total) - }; - let end = match length { - None => total, - Some(length) if values.is_null(length)? => total, - Some(length) => { - let length = eval_int_value(length, values)?; - if length < 0 { - (total + length).max(start) - } else { - start.saturating_add(length).min(total) - } - } - }; - let start = usize::try_from(start).map_err(|_| EvalStatus::RuntimeFatal)?; - let end = usize::try_from(end).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut output = Vec::with_capacity(bytes.len() + replacement.len()); - output.extend_from_slice(&bytes[..start]); - output.extend_from_slice(&replacement); - output.extend_from_slice(&bytes[end..]); - values.string_bytes_value(&output) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/strings/url.rs b/crates/elephc-magician/src/interpreter/builtins/strings/url.rs deleted file mode 100644 index 42171253fd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/strings/url.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! Purpose: -//! URL encode and decode builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::strings` re-exports. -//! -//! Key details: -//! - Runtime cells remain opaque and string bytes are obtained through `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP URL encode builtins over one eval string expression. -pub(in crate::interpreter) fn eval_builtin_url_encode( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_url_encode_result(name, value, values) -} - -/// Percent-encodes one PHP string using query-style or RFC 3986 URL rules. -pub(in crate::interpreter) fn eval_url_encode_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - const HEX: &[u8; 16] = b"0123456789ABCDEF"; - for byte in bytes { - if eval_url_encode_keeps_byte(name, byte)? { - output.push(byte); - } else if name == "urlencode" && byte == b' ' { - output.push(b'+'); - } else { - output.push(b'%'); - output.push(HEX[(byte >> 4) as usize]); - output.push(HEX[(byte & 0x0f) as usize]); - } - } - values.string_bytes_value(&output) -} - -/// Returns whether a byte remains unescaped for the selected PHP URL encoder. -pub(in crate::interpreter) fn eval_url_encode_keeps_byte( - name: &str, - byte: u8, -) -> Result { - let common = byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.'); - match name { - "urlencode" => Ok(common), - "rawurlencode" => Ok(common || byte == b'~'), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP URL decode builtins over one eval string expression. -pub(in crate::interpreter) fn eval_builtin_url_decode( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_url_decode_result(name, value, values) -} - -/// Decodes `%XX` sequences and optionally maps `+` to space for `urldecode()`. -pub(in crate::interpreter) fn eval_url_decode_result( - name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let plus_to_space = match name { - "urldecode" => true, - "rawurldecode" => false, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let bytes = values.string_bytes(value)?; - let mut output = Vec::with_capacity(bytes.len()); - let mut index = 0; - while index < bytes.len() { - if bytes[index] == b'+' && plus_to_space { - output.push(b' '); - index += 1; - } else if bytes[index] == b'%' && index + 2 < bytes.len() { - if let (Some(high), Some(low)) = ( - eval_hex_nibble(bytes[index + 1]), - eval_hex_nibble(bytes[index + 2]), - ) { - output.push((high << 4) | low); - index += 3; - continue; - } - output.push(bytes[index]); - index += 1; - } else { - output.push(bytes[index]); - index += 1; - } - } - values.string_bytes_value(&output) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs index 0c5d789151..bc9903c712 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs @@ -211,3 +211,21 @@ pub(in crate::interpreter) fn eval_symbols_values_result( _ => Err(EvalStatus::RuntimeFatal), } } + +/// Evaluates PHP `spl_classes()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_spl_classes( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values) +} + +/// Builds the static class-name list returned by eval `spl_classes()`. +pub(in crate::interpreter) fn eval_spl_classes_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) +} diff --git a/crates/elephc-magician/src/interpreter/core_builtins.rs b/crates/elephc-magician/src/interpreter/core_builtins.rs index 6555ef6894..7df910723b 100644 --- a/crates/elephc-magician/src/interpreter/core_builtins.rs +++ b/crates/elephc-magician/src/interpreter/core_builtins.rs @@ -5,49 +5,10 @@ //! - `crate::interpreter::expressions::eval_positional_expr_call()`. //! //! Key details: -//! - These helpers are kept out of large domain builtin files because they are short and rely on core eval traversal. +//! - Domain builtins live in their `builtins::` leaf modules; this file only keeps shared core helpers. use super::*; -/// Evaluates the builtin `strlen(...)` for one PHP-coerced string argument. -pub(in crate::interpreter) fn eval_builtin_strlen( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - let bytes = values.string_bytes(value)?; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len) -} - -/// Evaluates the builtin `ord(...)` for the first byte of one coerced string. -pub(in crate::interpreter) fn eval_builtin_ord( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_ord_result(value, values) -} - -/// Returns the first byte of one converted string, or zero for an empty string. -pub(in crate::interpreter) fn eval_ord_result( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - values.int(i64::from(bytes.first().copied().unwrap_or(0))) -} - /// Evaluates the builtin `count(...)` for arrays and `Countable` objects. pub(in crate::interpreter) fn eval_builtin_count( args: &[EvalExpr], From f8bfce15621537bdbab5a3400ef358261e5309d1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 17:05:17 +0200 Subject: [PATCH 1109/1208] refactor(magician): co-locate filesystem builtin declarations --- .../builtins/filesystem/basename.rs | 39 +++ .../interpreter/builtins/filesystem/chdir.rs | 37 +++ .../interpreter/builtins/filesystem/chgrp.rs | 37 +++ .../interpreter/builtins/filesystem/chmod.rs | 37 +++ .../interpreter/builtins/filesystem/chown.rs | 37 +++ .../builtins/filesystem/clearstatcache.rs | 42 +++ .../builtins/filesystem/closedir.rs | 37 +++ .../interpreter/builtins/filesystem/copy.rs | 37 +++ .../filesystem/declarations/basename.rs | 18 -- .../builtins/filesystem/declarations/chdir.rs | 16 - .../builtins/filesystem/declarations/chgrp.rs | 16 - .../builtins/filesystem/declarations/chmod.rs | 16 - .../builtins/filesystem/declarations/chown.rs | 16 - .../filesystem/declarations/clearstatcache.rs | 21 -- .../filesystem/declarations/closedir.rs | 16 - .../builtins/filesystem/declarations/copy.rs | 16 - .../declarations/direct_dispatch.rs | 152 --------- .../filesystem/declarations/dirname.rs | 18 -- .../declarations/disk_free_space.rs | 16 - .../declarations/disk_total_space.rs | 16 - .../filesystem/declarations/fclose.rs | 16 - .../filesystem/declarations/fdatasync.rs | 16 - .../builtins/filesystem/declarations/feof.rs | 16 - .../filesystem/declarations/fflush.rs | 16 - .../builtins/filesystem/declarations/fgetc.rs | 16 - .../filesystem/declarations/fgetcsv.rs | 22 -- .../builtins/filesystem/declarations/fgets.rs | 16 - .../builtins/filesystem/declarations/file.rs | 16 - .../filesystem/declarations/file_exists.rs | 16 - .../declarations/file_get_contents.rs | 16 - .../declarations/file_put_contents.rs | 16 - .../filesystem/declarations/fileatime.rs | 16 - .../filesystem/declarations/filectime.rs | 16 - .../filesystem/declarations/filegroup.rs | 16 - .../filesystem/declarations/fileinode.rs | 16 - .../filesystem/declarations/filemtime.rs | 16 - .../filesystem/declarations/fileowner.rs | 16 - .../filesystem/declarations/fileperms.rs | 16 - .../filesystem/declarations/filesize.rs | 16 - .../filesystem/declarations/filetype.rs | 16 - .../builtins/filesystem/declarations/flock.rs | 19 -- .../filesystem/declarations/fnmatch.rs | 18 -- .../builtins/filesystem/declarations/fopen.rs | 23 -- .../filesystem/declarations/fpassthru.rs | 16 - .../filesystem/declarations/fprintf.rs | 17 - .../filesystem/declarations/fputcsv.rs | 23 -- .../builtins/filesystem/declarations/fread.rs | 16 - .../filesystem/declarations/fscanf.rs | 17 - .../builtins/filesystem/declarations/fseek.rs | 18 -- .../filesystem/declarations/fsockopen.rs | 25 -- .../builtins/filesystem/declarations/fstat.rs | 16 - .../builtins/filesystem/declarations/fsync.rs | 16 - .../builtins/filesystem/declarations/ftell.rs | 16 - .../filesystem/declarations/ftruncate.rs | 16 - .../filesystem/declarations/fwrite.rs | 16 - .../filesystem/declarations/getcwd.rs | 16 - .../builtins/filesystem/declarations/glob.rs | 16 - .../filesystem/declarations/is_dir.rs | 16 - .../filesystem/declarations/is_executable.rs | 16 - .../filesystem/declarations/is_file.rs | 16 - .../filesystem/declarations/is_link.rs | 16 - .../filesystem/declarations/is_readable.rs | 16 - .../filesystem/declarations/is_writable.rs | 16 - .../filesystem/declarations/is_writeable.rs | 16 - .../filesystem/declarations/lchgrp.rs | 16 - .../filesystem/declarations/lchown.rs | 16 - .../builtins/filesystem/declarations/link.rs | 16 - .../filesystem/declarations/linkinfo.rs | 16 - .../builtins/filesystem/declarations/lstat.rs | 16 - .../builtins/filesystem/declarations/mkdir.rs | 16 - .../builtins/filesystem/declarations/mod.rs | 144 --------- .../filesystem/declarations/opendir.rs | 16 - .../filesystem/declarations/pathinfo.rs | 18 -- .../filesystem/declarations/pclose.rs | 16 - .../filesystem/declarations/pfsockopen.rs | 25 -- .../builtins/filesystem/declarations/popen.rs | 16 - .../filesystem/declarations/readdir.rs | 16 - .../filesystem/declarations/readfile.rs | 16 - .../filesystem/declarations/readline.rs | 18 -- .../filesystem/declarations/readlink.rs | 16 - .../filesystem/declarations/realpath.rs | 16 - .../declarations/realpath_cache_get.rs | 16 - .../declarations/realpath_cache_size.rs | 16 - .../filesystem/declarations/rename.rs | 16 - .../filesystem/declarations/rewind.rs | 16 - .../filesystem/declarations/rewinddir.rs | 16 - .../builtins/filesystem/declarations/rmdir.rs | 16 - .../filesystem/declarations/scandir.rs | 16 - .../builtins/filesystem/declarations/stat.rs | 16 - .../declarations/stream_bucket_append.rs | 16 - .../stream_bucket_make_writeable.rs | 16 - .../declarations/stream_bucket_new.rs | 16 - .../declarations/stream_bucket_prepend.rs | 16 - .../declarations/stream_context_create.rs | 21 -- .../stream_context_get_default.rs | 18 -- .../stream_context_get_options.rs | 16 - .../declarations/stream_context_get_params.rs | 16 - .../stream_context_set_default.rs | 16 - .../declarations/stream_context_set_option.rs | 24 -- .../declarations/stream_context_set_params.rs | 16 - .../declarations/stream_copy_to_stream.rs | 23 -- .../declarations/stream_filter_append.rs | 23 -- .../declarations/stream_filter_prepend.rs | 23 -- .../declarations/stream_filter_register.rs | 16 - .../declarations/stream_filter_remove.rs | 16 - .../declarations/stream_get_contents.rs | 22 -- .../declarations/stream_get_line.rs | 18 -- .../declarations/stream_get_meta_data.rs | 16 - .../filesystem/declarations/stream_isatty.rs | 16 - .../stream_resolve_include_path.rs | 16 - .../filesystem/declarations/stream_select.rs | 25 -- .../declarations/stream_set_blocking.rs | 16 - .../declarations/stream_set_chunk_size.rs | 16 - .../declarations/stream_set_read_buffer.rs | 16 - .../declarations/stream_set_timeout.rs | 18 -- .../declarations/stream_set_write_buffer.rs | 16 - .../declarations/stream_socket_accept.rs | 23 -- .../declarations/stream_socket_client.rs | 16 - .../stream_socket_enable_crypto.rs | 23 -- .../declarations/stream_socket_get_name.rs | 16 - .../declarations/stream_socket_pair.rs | 16 - .../declarations/stream_socket_recvfrom.rs | 24 -- .../declarations/stream_socket_sendto.rs | 23 -- .../declarations/stream_socket_server.rs | 16 - .../declarations/stream_socket_shutdown.rs | 16 - .../declarations/stream_wrapper_register.rs | 22 -- .../declarations/stream_wrapper_restore.rs | 16 - .../declarations/stream_wrapper_unregister.rs | 16 - .../filesystem/declarations/symlink.rs | 16 - .../declarations/sys_get_temp_dir.rs | 16 - .../filesystem/declarations/tempnam.rs | 16 - .../filesystem/declarations/tmpfile.rs | 16 - .../builtins/filesystem/declarations/touch.rs | 22 -- .../builtins/filesystem/declarations/umask.rs | 18 -- .../filesystem/declarations/unlink.rs | 16 - .../declarations/values_dispatch.rs | 34 -- .../filesystem/declarations/vfprintf.rs | 16 - .../builtins/filesystem/direct_dispatch.rs | 291 ++++++++++++++++++ .../builtins/filesystem/dirname.rs | 39 +++ .../builtins/filesystem/disk_free_space.rs | 37 +++ .../builtins/filesystem/disk_total_space.rs | 37 +++ .../interpreter/builtins/filesystem/fclose.rs | 37 +++ .../builtins/filesystem/fdatasync.rs | 37 +++ .../interpreter/builtins/filesystem/feof.rs | 37 +++ .../interpreter/builtins/filesystem/fflush.rs | 37 +++ .../interpreter/builtins/filesystem/fgetc.rs | 37 +++ .../builtins/filesystem/fgetcsv.rs | 43 +++ .../interpreter/builtins/filesystem/fgets.rs | 37 +++ .../interpreter/builtins/filesystem/file.rs | 37 +++ .../builtins/filesystem/file_exists.rs | 37 +++ .../builtins/filesystem/file_get_contents.rs | 37 +++ .../builtins/filesystem/file_put_contents.rs | 37 +++ .../builtins/filesystem/fileatime.rs | 37 +++ .../builtins/filesystem/filectime.rs | 37 +++ .../builtins/filesystem/filegroup.rs | 37 +++ .../builtins/filesystem/fileinode.rs | 37 +++ .../builtins/filesystem/filemtime.rs | 37 +++ .../builtins/filesystem/fileowner.rs | 37 +++ .../builtins/filesystem/fileperms.rs | 37 +++ .../builtins/filesystem/filesize.rs | 37 +++ .../builtins/filesystem/filetype.rs | 37 +++ .../interpreter/builtins/filesystem/flock.rs | 40 +++ .../builtins/filesystem/fnmatch.rs | 36 ++- .../interpreter/builtins/filesystem/fopen.rs | 44 +++ .../builtins/filesystem/fpassthru.rs | 37 +++ .../builtins/filesystem/fprintf.rs | 38 +++ .../builtins/filesystem/fputcsv.rs | 44 +++ .../interpreter/builtins/filesystem/fread.rs | 37 +++ .../interpreter/builtins/filesystem/fscanf.rs | 38 +++ .../interpreter/builtins/filesystem/fseek.rs | 39 +++ .../builtins/filesystem/fsockopen.rs | 46 +++ .../interpreter/builtins/filesystem/fstat.rs | 37 +++ .../interpreter/builtins/filesystem/fsync.rs | 37 +++ .../interpreter/builtins/filesystem/ftell.rs | 37 +++ .../builtins/filesystem/ftruncate.rs | 37 +++ .../interpreter/builtins/filesystem/fwrite.rs | 37 +++ .../interpreter/builtins/filesystem/getcwd.rs | 37 +++ .../interpreter/builtins/filesystem/glob.rs | 37 +++ .../interpreter/builtins/filesystem/is_dir.rs | 37 +++ .../builtins/filesystem/is_executable.rs | 37 +++ .../builtins/filesystem/is_file.rs | 37 +++ .../builtins/filesystem/is_link.rs | 37 +++ .../builtins/filesystem/is_readable.rs | 37 +++ .../builtins/filesystem/is_writable.rs | 37 +++ .../builtins/filesystem/is_writeable.rs | 37 +++ .../interpreter/builtins/filesystem/lchgrp.rs | 37 +++ .../interpreter/builtins/filesystem/lchown.rs | 37 +++ .../interpreter/builtins/filesystem/link.rs | 37 +++ .../builtins/filesystem/linkinfo.rs | 37 +++ .../interpreter/builtins/filesystem/lstat.rs | 37 +++ .../interpreter/builtins/filesystem/mkdir.rs | 37 +++ .../interpreter/builtins/filesystem/mod.rs | 143 ++++++++- .../builtins/filesystem/opendir.rs | 37 +++ .../path_values_dispatch.rs | 8 +- .../builtins/filesystem/pathinfo.rs | 39 +++ .../interpreter/builtins/filesystem/pclose.rs | 37 +++ .../builtins/filesystem/pfsockopen.rs | 46 +++ .../interpreter/builtins/filesystem/popen.rs | 37 +++ .../builtins/filesystem/readdir.rs | 37 +++ .../builtins/filesystem/readfile.rs | 37 +++ .../builtins/filesystem/readline.rs | 39 ++- .../builtins/filesystem/readlink.rs | 37 +++ .../builtins/filesystem/realpath.rs | 37 +++ .../builtins/filesystem/realpath_cache_get.rs | 37 +++ .../filesystem/realpath_cache_size.rs | 37 +++ .../interpreter/builtins/filesystem/rename.rs | 37 +++ .../interpreter/builtins/filesystem/rewind.rs | 37 +++ .../builtins/filesystem/rewinddir.rs | 37 +++ .../interpreter/builtins/filesystem/rmdir.rs | 37 +++ .../builtins/filesystem/scandir.rs | 37 +++ .../interpreter/builtins/filesystem/stat.rs | 37 +++ .../filesystem/stream_bucket_append.rs | 37 +++ .../stream_bucket_make_writeable.rs | 37 +++ .../builtins/filesystem/stream_bucket_new.rs | 37 +++ .../filesystem/stream_bucket_prepend.rs | 37 +++ .../filesystem/stream_context_create.rs | 42 +++ .../filesystem/stream_context_get_default.rs | 39 +++ .../filesystem/stream_context_get_options.rs | 37 +++ .../filesystem/stream_context_get_params.rs | 37 +++ .../filesystem/stream_context_set_default.rs | 37 +++ .../filesystem/stream_context_set_option.rs | 45 +++ .../filesystem/stream_context_set_params.rs | 37 +++ .../filesystem/stream_copy_to_stream.rs | 44 +++ .../filesystem/stream_filter_append.rs | 44 +++ .../filesystem/stream_filter_prepend.rs | 44 +++ .../filesystem/stream_filter_register.rs | 37 +++ .../filesystem/stream_filter_remove.rs | 37 +++ .../filesystem/stream_get_contents.rs | 43 +++ .../builtins/filesystem/stream_get_line.rs | 39 +++ .../filesystem/stream_get_meta_data.rs | 37 +++ .../builtins/filesystem/stream_isatty.rs | 37 +++ .../filesystem/stream_resolve_include_path.rs | 37 +++ .../builtins/filesystem/stream_select.rs | 46 +++ .../filesystem/stream_set_blocking.rs | 37 +++ .../filesystem/stream_set_chunk_size.rs | 37 +++ .../filesystem/stream_set_read_buffer.rs | 37 +++ .../builtins/filesystem/stream_set_timeout.rs | 39 +++ .../filesystem/stream_set_write_buffer.rs | 37 +++ .../filesystem/stream_socket_accept.rs | 44 +++ .../filesystem/stream_socket_client.rs | 37 +++ .../filesystem/stream_socket_enable_crypto.rs | 44 +++ .../filesystem/stream_socket_get_name.rs | 37 +++ .../builtins/filesystem/stream_socket_pair.rs | 37 +++ .../filesystem/stream_socket_recvfrom.rs | 45 +++ .../filesystem/stream_socket_sendto.rs | 44 +++ .../filesystem/stream_socket_server.rs | 37 +++ .../filesystem/stream_socket_shutdown.rs | 37 +++ .../stream_values_dispatch.rs | 8 +- .../filesystem/stream_wrapper_register.rs | 43 +++ .../filesystem/stream_wrapper_restore.rs | 37 +++ .../filesystem/stream_wrapper_unregister.rs | 37 +++ .../builtins/filesystem/symlink.rs | 37 +++ .../builtins/filesystem/sys_get_temp_dir.rs | 37 +++ .../builtins/filesystem/tempnam.rs | 37 +++ .../builtins/filesystem/tmpfile.rs | 37 +++ .../interpreter/builtins/filesystem/touch.rs | 43 +++ .../interpreter/builtins/filesystem/umask.rs | 39 +++ .../interpreter/builtins/filesystem/unlink.rs | 37 +++ .../builtins/filesystem/values_dispatch.rs | 172 +++++++++++ .../builtins/filesystem/vfprintf.rs | 37 +++ 260 files changed, 5415 insertions(+), 2528 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/basename.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chdir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chgrp.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chmod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chown.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/clearstatcache.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/closedir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/copy.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/direct_dispatch.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/dirname.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_free_space.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_total_space.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fclose.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fdatasync.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/feof.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fflush.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetc.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetcsv.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgets.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_exists.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_get_contents.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_put_contents.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileatime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filectime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filegroup.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileinode.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filemtime.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileowner.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileperms.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filesize.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filetype.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/flock.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fnmatch.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fopen.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fpassthru.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fprintf.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fputcsv.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fread.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fscanf.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fseek.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsockopen.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fstat.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsync.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftell.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftruncate.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fwrite.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/getcwd.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/glob.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_dir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_executable.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_file.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_link.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_readable.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writable.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writeable.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchgrp.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchown.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/link.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/linkinfo.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lstat.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mkdir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/opendir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pathinfo.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pclose.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pfsockopen.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/popen.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readdir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readfile.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readline.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readlink.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_get.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_size.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rename.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewind.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewinddir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rmdir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/scandir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stat.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_append.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_make_writeable.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_new.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_prepend.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_create.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_default.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_options.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_params.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_default.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_option.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_params.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_copy_to_stream.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_append.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_prepend.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_register.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_remove.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_contents.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_line.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_meta_data.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_isatty.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_resolve_include_path.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_select.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_blocking.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_chunk_size.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_read_buffer.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_timeout.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_write_buffer.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_accept.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_client.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_enable_crypto.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_get_name.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_pair.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_recvfrom.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_sendto.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_server.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_shutdown.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_register.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_restore.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_unregister.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/symlink.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/sys_get_temp_dir.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tempnam.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tmpfile.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/touch.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/umask.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/unlink.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/values_dispatch.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/vfprintf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs rename crates/elephc-magician/src/interpreter/builtins/filesystem/{declarations => }/path_values_dispatch.rs (97%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs rename crates/elephc-magician/src/interpreter/builtins/filesystem/{declarations => }/stream_values_dispatch.rs (98%) create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs new file mode 100644 index 0000000000..0afb583e83 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `basename`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the path helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "basename", + area: Filesystem, + params: [path, suffix = EvalBuiltinDefaultValue::String("")], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `basename` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_basename_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("basename", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `basename` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_basename_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("basename", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs new file mode 100644 index 0000000000..fd886064d4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `chdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary path operation helper. + +eval_builtin! { + name: "chdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `chdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chdir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("chdir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `chdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chdir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("chdir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs new file mode 100644 index 0000000000..f27affbf93 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `chgrp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the ownership/group helper. + +eval_builtin! { + name: "chgrp", + area: Filesystem, + params: [filename, group], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `chgrp` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chgrp_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("chgrp", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `chgrp` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chgrp_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("chgrp", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs new file mode 100644 index 0000000000..09d5ecdd0e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `chmod`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the chmod helper. + +eval_builtin! { + name: "chmod", + area: Filesystem, + params: [filename, permissions], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `chmod` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chmod_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("chmod", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `chmod` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chmod_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("chmod", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs new file mode 100644 index 0000000000..228ea6e04a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `chown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the ownership/group helper. + +eval_builtin! { + name: "chown", + area: Filesystem, + params: [filename, user], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `chown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chown_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("chown", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `chown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_chown_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("chown", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs new file mode 100644 index 0000000000..1437e5fcf2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Declarative eval registry entry for `clearstatcache`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through eval's ordered no-op stat-cache helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "clearstatcache", + area: Filesystem, + params: [ + clear_realpath_cache = EvalBuiltinDefaultValue::Bool(false), + filename = EvalBuiltinDefaultValue::String("") + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `clearstatcache` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_clearstatcache_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("clearstatcache", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `clearstatcache` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_clearstatcache_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("clearstatcache", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs new file mode 100644 index 0000000000..0f0c6b55fb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `closedir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory resource close helper. + +eval_builtin! { + name: "closedir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `closedir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_closedir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("closedir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `closedir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_closedir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("closedir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs new file mode 100644 index 0000000000..78c2b7a899 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `copy`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the binary path operation helper. + +eval_builtin! { + name: "copy", + area: Filesystem, + params: [from, to], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `copy` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_copy_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("copy", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `copy` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_copy_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("copy", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/basename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/basename.rs deleted file mode 100644 index ae566f13db..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/basename.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `basename`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the path helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "basename", - area: Filesystem, - params: [path, suffix = EvalBuiltinDefaultValue::String("")], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chdir.rs deleted file mode 100644 index 5f5a9b3d01..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chdir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `chdir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary path operation helper. - -eval_builtin! { - name: "chdir", - area: Filesystem, - params: [directory], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chgrp.rs deleted file mode 100644 index aea6d1bfb2..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chgrp.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `chgrp`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the ownership/group helper. - -eval_builtin! { - name: "chgrp", - area: Filesystem, - params: [filename, group], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chmod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chmod.rs deleted file mode 100644 index 6d5f3325ca..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chmod.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `chmod`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the chmod helper. - -eval_builtin! { - name: "chmod", - area: Filesystem, - params: [filename, permissions], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chown.rs deleted file mode 100644 index e3f8361f2c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/chown.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `chown`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the ownership/group helper. - -eval_builtin! { - name: "chown", - area: Filesystem, - params: [filename, user], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/clearstatcache.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/clearstatcache.rs deleted file mode 100644 index bd368df1ad..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/clearstatcache.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `clearstatcache`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to eval's ordered no-op stat-cache helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "clearstatcache", - area: Filesystem, - params: [ - clear_realpath_cache = EvalBuiltinDefaultValue::Bool(false), - filename = EvalBuiltinDefaultValue::String("") - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/closedir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/closedir.rs deleted file mode 100644 index 0a342a5dfc..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/closedir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `closedir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the directory resource close helper. - -eval_builtin! { - name: "closedir", - area: Filesystem, - params: [dir_handle], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/copy.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/copy.rs deleted file mode 100644 index 5d71626c96..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/copy.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `copy`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the binary path operation helper. - -eval_builtin! { - name: "copy", - area: Filesystem, - params: [from, to], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/direct_dispatch.rs deleted file mode 100644 index eed79b4eeb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/direct_dispatch.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! Purpose: -//! Direct expression-level dispatch for filesystem builtins declared in the eval registry. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks::EvalDirectHook::call()`. -//! -//! Key details: -//! - This dispatcher keeps filesystem call lowering area-scoped while per-builtin -//! metadata lives in individual declaration files. - -use super::super::super::super::*; -use super::super::*; - -/// Dispatches direct expression-level calls for declaratively migrated filesystem builtins. -pub(in crate::interpreter) fn eval_builtin_filesystem_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "basename" => eval_builtin_basename(args, context, scope, values), - "chdir" | "mkdir" | "rmdir" => { - eval_builtin_unary_path_bool(name, args, context, scope, values) - } - "chmod" => eval_builtin_chmod(args, context, scope, values), - "chown" | "chgrp" | "lchown" | "lchgrp" => { - eval_builtin_chown_like(name, args, context, scope, values) - } - "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), - "copy" | "link" | "rename" | "symlink" => { - eval_builtin_binary_path_bool(name, args, context, scope, values) - } - "dirname" => eval_builtin_dirname(args, context, scope, values), - "disk_free_space" | "disk_total_space" => { - eval_builtin_disk_space(name, args, context, scope, values) - } - "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), - "file" => eval_builtin_file(args, context, scope, values), - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => { - eval_builtin_file_probe(name, args, context, scope, values) - } - "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), - "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), - "filesize" => eval_builtin_filesize(args, context, scope, values), - "filetype" => eval_builtin_filetype(args, context, scope, values), - "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" - | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { - eval_builtin_unary_stream(name, args, context, scope, values) - } - "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), - "fopen" => eval_builtin_fopen(args, context, scope, values), - "fprintf" => eval_builtin_fprintf(args, context, scope, values), - "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), - "fread" => eval_builtin_fread(args, context, scope, values), - "fscanf" => eval_builtin_fscanf(args, context, scope, values), - "fseek" => eval_builtin_fseek(args, context, scope, values), - "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), - "fwrite" => eval_builtin_fwrite(args, context, scope, values), - "getcwd" => eval_builtin_getcwd(args, values), - "glob" => eval_builtin_glob(args, context, scope, values), - "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), - "opendir" => eval_builtin_opendir(args, context, scope, values), - "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), - "pclose" => eval_builtin_pclose(args, context, scope, values), - "popen" => eval_builtin_popen(args, context, scope, values), - "closedir" | "readdir" | "rewinddir" => { - eval_builtin_unary_directory(name, args, context, scope, values) - } - "readfile" => eval_builtin_readfile(args, context, scope, values), - "readline" => eval_builtin_readline(args, context, scope, values), - "readlink" => eval_builtin_readlink(args, context, scope, values), - "realpath" => eval_builtin_realpath(args, context, scope, values), - "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), - "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "scandir" => eval_builtin_scandir(args, context, scope, values), - "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), - "stream_bucket_append" | "stream_bucket_prepend" => { - eval_builtin_stream_bucket_push(name, args, context, scope, values) - } - "stream_bucket_make_writeable" => { - eval_builtin_stream_bucket_make_writeable(args, context, scope, values) - } - "stream_bucket_new" => eval_builtin_stream_bucket_new(args, context, scope, values), - "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), - "stream_context_get_default" => { - eval_builtin_stream_context_get_default(args, context, scope, values) - } - "stream_context_get_options" => { - eval_builtin_stream_context_get_options(args, context, scope, values) - } - "stream_context_get_params" => { - eval_builtin_stream_context_get_params(args, context, scope, values) - } - "stream_context_set_default" => { - eval_builtin_stream_context_set_default(args, context, scope, values) - } - "stream_context_set_option" => { - eval_builtin_stream_context_set_option(args, context, scope, values) - } - "stream_context_set_params" => { - eval_builtin_stream_context_set_params(args, context, scope, values) - } - "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), - "stream_filter_append" | "stream_filter_prepend" => { - eval_builtin_stream_filter_attach(name, args, context, scope, values) - } - "stream_filter_register" => { - eval_builtin_stream_filter_register(args, context, scope, values) - } - "stream_filter_remove" => eval_builtin_stream_filter_remove(args, context, scope, values), - "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), - "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), - "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), - "stream_resolve_include_path" => { - eval_builtin_stream_resolve_include_path(args, context, scope, values) - } - "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), - "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { - eval_builtin_stream_set_buffer_like(name, args, context, scope, values) - } - "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), - "stream_socket_client" => eval_builtin_stream_socket_client(args, context, scope, values), - "stream_socket_enable_crypto" => { - eval_builtin_stream_socket_enable_crypto(args, context, scope, values) - } - "stream_socket_get_name" => { - eval_builtin_stream_socket_get_name(args, context, scope, values) - } - "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), - "stream_socket_sendto" => eval_builtin_stream_socket_sendto(args, context, scope, values), - "stream_socket_server" => eval_builtin_stream_socket_server(args, context, scope, values), - "stream_socket_shutdown" => { - eval_builtin_stream_socket_shutdown(args, context, scope, values) - } - "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { - eval_builtin_stream_wrapper_registry(name, args, context, scope, values) - } - "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), - "tempnam" => eval_builtin_tempnam(args, context, scope, values), - "tmpfile" => eval_builtin_tmpfile(args, context, values), - "touch" => eval_builtin_touch(args, context, scope, values), - "umask" => eval_builtin_umask(args, context, scope, values), - "unlink" => eval_builtin_unlink(args, context, scope, values), - "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/dirname.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/dirname.rs deleted file mode 100644 index d6bd3206ec..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/dirname.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `dirname`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the path helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "dirname", - area: Filesystem, - params: [path, levels = EvalBuiltinDefaultValue::Int(1)], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_free_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_free_space.rs deleted file mode 100644 index 15a6b1d921..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_free_space.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `disk_free_space`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the disk-space helper. - -eval_builtin! { - name: "disk_free_space", - area: Filesystem, - params: [directory], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_total_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_total_space.rs deleted file mode 100644 index 667c557426..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/disk_total_space.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `disk_total_space`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the disk-space helper. - -eval_builtin! { - name: "disk_total_space", - area: Filesystem, - params: [directory], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fclose.rs deleted file mode 100644 index be62a0edaf..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fclose.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fclose`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "fclose", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fdatasync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fdatasync.rs deleted file mode 100644 index 659807e793..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fdatasync.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fdatasync`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "fdatasync", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/feof.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/feof.rs deleted file mode 100644 index ec37ac1feb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/feof.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `feof`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "feof", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fflush.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fflush.rs deleted file mode 100644 index f660f2767b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fflush.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fflush`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "fflush", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetc.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetc.rs deleted file mode 100644 index e3442c9cf3..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetc.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fgetc`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "fgetc", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetcsv.rs deleted file mode 100644 index ede354ea77..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgetcsv.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fgetcsv`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the CSV stream read helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "fgetcsv", - area: Filesystem, - params: [ - stream, - length = EvalBuiltinDefaultValue::Null, - separator = EvalBuiltinDefaultValue::String(",") - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgets.rs deleted file mode 100644 index 03c8290958..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fgets.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fgets`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "fgets", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file.rs deleted file mode 100644 index 9ec84abe4f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `file`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-lines helper. - -eval_builtin! { - name: "file", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_exists.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_exists.rs deleted file mode 100644 index b2c98ac2dd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_exists.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `file_exists`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-probe helper. - -eval_builtin! { - name: "file_exists", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_get_contents.rs deleted file mode 100644 index 3ac7b0ca1f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_get_contents.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `file_get_contents`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the one-shot file read helper. - -eval_builtin! { - name: "file_get_contents", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_put_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_put_contents.rs deleted file mode 100644 index e51ed365a4..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/file_put_contents.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `file_put_contents`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the one-shot file write helper. - -eval_builtin! { - name: "file_put_contents", - area: Filesystem, - params: [filename, data], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileatime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileatime.rs deleted file mode 100644 index 543f753dcd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileatime.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fileatime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the scalar stat helper. - -eval_builtin! { - name: "fileatime", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filectime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filectime.rs deleted file mode 100644 index 3a52e88f55..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filectime.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `filectime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the scalar stat helper. - -eval_builtin! { - name: "filectime", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filegroup.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filegroup.rs deleted file mode 100644 index ea9fc7e16b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filegroup.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `filegroup`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the scalar stat helper. - -eval_builtin! { - name: "filegroup", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileinode.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileinode.rs deleted file mode 100644 index 2d64f43193..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileinode.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fileinode`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the scalar stat helper. - -eval_builtin! { - name: "fileinode", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filemtime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filemtime.rs deleted file mode 100644 index 25312fae3e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filemtime.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `filemtime`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the scalar stat helper. - -eval_builtin! { - name: "filemtime", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileowner.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileowner.rs deleted file mode 100644 index cbc464ac49..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileowner.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fileowner`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the scalar stat helper. - -eval_builtin! { - name: "fileowner", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileperms.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileperms.rs deleted file mode 100644 index 9b369dd0d8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fileperms.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fileperms`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the scalar stat helper. - -eval_builtin! { - name: "fileperms", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filesize.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filesize.rs deleted file mode 100644 index 11e142d1ac..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filesize.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `filesize`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the filesize helper. - -eval_builtin! { - name: "filesize", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filetype.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filetype.rs deleted file mode 100644 index 8d52494c94..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/filetype.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `filetype`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the filetype helper. - -eval_builtin! { - name: "filetype", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/flock.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/flock.rs deleted file mode 100644 index e3647532ba..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/flock.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `flock`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Direct calls keep their source-sensitive by-reference path. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "flock", - area: Filesystem, - params: [stream, operation, would_block: by_ref = EvalBuiltinDefaultValue::Null], - by_ref: [would_block], - direct: none, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fnmatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fnmatch.rs deleted file mode 100644 index 9c7c68d838..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fnmatch.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fnmatch`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the fnmatch helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "fnmatch", - area: Filesystem, - params: [pattern, filename, flags = EvalBuiltinDefaultValue::Int(0)], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fopen.rs deleted file mode 100644 index e5060bc11e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fopen.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fopen`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream-opening helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "fopen", - area: Filesystem, - params: [ - filename, - mode, - use_include_path = EvalBuiltinDefaultValue::Bool(false), - context = EvalBuiltinDefaultValue::Null - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fpassthru.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fpassthru.rs deleted file mode 100644 index 79441e27b7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fpassthru.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fpassthru`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "fpassthru", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fprintf.rs deleted file mode 100644 index 695f969501..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fprintf.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fprintf`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Variadic values are formatted by the existing printf-family helper. - -eval_builtin! { - name: "fprintf", - area: Filesystem, - params: [stream, format], - variadic: values, - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fputcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fputcsv.rs deleted file mode 100644 index 1c760df14d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fputcsv.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fputcsv`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the CSV stream write helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "fputcsv", - area: Filesystem, - params: [ - stream, - fields, - separator = EvalBuiltinDefaultValue::String(","), - enclosure = EvalBuiltinDefaultValue::String("\"") - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fread.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fread.rs deleted file mode 100644 index b93efa8cbb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fread.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fread`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream read helper. - -eval_builtin! { - name: "fread", - area: Filesystem, - params: [stream, length], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fscanf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fscanf.rs deleted file mode 100644 index 47a6442cd7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fscanf.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fscanf`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - The current eval implementation returns parsed values and ignores output vars. - -eval_builtin! { - name: "fscanf", - area: Filesystem, - params: [stream, format], - variadic: vars, - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fseek.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fseek.rs deleted file mode 100644 index 9b3a92bbfc..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fseek.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fseek`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream seek helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "fseek", - area: Filesystem, - params: [stream, offset, whence = EvalBuiltinDefaultValue::Int(0)], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsockopen.rs deleted file mode 100644 index fb35f30761..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsockopen.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fsockopen`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Direct calls keep their source-sensitive by-reference error-output path. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "fsockopen", - area: Filesystem, - params: [ - hostname, - port, - error_code: by_ref = EvalBuiltinDefaultValue::Null, - error_message: by_ref = EvalBuiltinDefaultValue::Null, - timeout = EvalBuiltinDefaultValue::Null - ], - by_ref: [error_code, error_message], - direct: none, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fstat.rs deleted file mode 100644 index 7f32cf7503..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fstat.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fstat`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "fstat", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsync.rs deleted file mode 100644 index c3ac875eaf..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fsync.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fsync`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "fsync", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftell.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftell.rs deleted file mode 100644 index 588b34c798..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftell.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ftell`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "ftell", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftruncate.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftruncate.rs deleted file mode 100644 index f4490302bb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/ftruncate.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `ftruncate`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream truncate helper. - -eval_builtin! { - name: "ftruncate", - area: Filesystem, - params: [stream, size], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fwrite.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fwrite.rs deleted file mode 100644 index a4c8048558..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/fwrite.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `fwrite`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream write helper. - -eval_builtin! { - name: "fwrite", - area: Filesystem, - params: [stream, data], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/getcwd.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/getcwd.rs deleted file mode 100644 index 8eb634875f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/getcwd.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `getcwd`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the current-working-directory helper. - -eval_builtin! { - name: "getcwd", - area: Filesystem, - params: [], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/glob.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/glob.rs deleted file mode 100644 index b9d95bde1a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/glob.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `glob`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the local glob helper. - -eval_builtin! { - name: "glob", - area: Filesystem, - params: [pattern], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_dir.rs deleted file mode 100644 index 6c1f17a1d7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_dir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_dir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-probe helper. - -eval_builtin! { - name: "is_dir", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_executable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_executable.rs deleted file mode 100644 index 25571ff12f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_executable.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_executable`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-probe helper. - -eval_builtin! { - name: "is_executable", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_file.rs deleted file mode 100644 index 332bc0c43b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_file.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_file`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-probe helper. - -eval_builtin! { - name: "is_file", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_link.rs deleted file mode 100644 index 3d9c590d15..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_link.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_link`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-probe helper. - -eval_builtin! { - name: "is_link", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_readable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_readable.rs deleted file mode 100644 index 80ec54a92e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_readable.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_readable`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-probe helper. - -eval_builtin! { - name: "is_readable", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writable.rs deleted file mode 100644 index d7c5be6f44..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writable.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_writable`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-probe helper. - -eval_builtin! { - name: "is_writable", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writeable.rs deleted file mode 100644 index e8d6a48144..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/is_writeable.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_writeable`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the file-probe helper. - -eval_builtin! { - name: "is_writeable", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchgrp.rs deleted file mode 100644 index 781084d372..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchgrp.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `lchgrp`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the ownership/group helper. - -eval_builtin! { - name: "lchgrp", - area: Filesystem, - params: [filename, group], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchown.rs deleted file mode 100644 index 07942551d7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lchown.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `lchown`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the ownership/group helper. - -eval_builtin! { - name: "lchown", - area: Filesystem, - params: [filename, user], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/link.rs deleted file mode 100644 index c8358d06a0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/link.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `link`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the binary path operation helper. - -eval_builtin! { - name: "link", - area: Filesystem, - params: [target, link], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/linkinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/linkinfo.rs deleted file mode 100644 index dd8794ad05..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/linkinfo.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `linkinfo`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the symbolic-link metadata helper. - -eval_builtin! { - name: "linkinfo", - area: Filesystem, - params: [path], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lstat.rs deleted file mode 100644 index cef1b286c9..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/lstat.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `lstat`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stat-array helper. - -eval_builtin! { - name: "lstat", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mkdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mkdir.rs deleted file mode 100644 index 44dee75853..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mkdir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `mkdir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary path operation helper. - -eval_builtin! { - name: "mkdir", - area: Filesystem, - params: [directory], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs deleted file mode 100644 index 97be4b72e5..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/mod.rs +++ /dev/null @@ -1,144 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries and dispatch adapters for filesystem builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem` module loading. -//! - `crate::interpreter::builtins::hooks` for migrated filesystem dispatch. -//! -//! Key details: -//! - Stream and by-reference-output builtins are registered here; direct calls -//! that must preserve writable caller storage keep source-sensitive helpers. - -mod basename; -mod chdir; -mod chgrp; -mod chmod; -mod chown; -mod closedir; -mod clearstatcache; -mod copy; -mod direct_dispatch; -mod dirname; -mod disk_free_space; -mod disk_total_space; -mod file; -mod file_exists; -mod file_get_contents; -mod file_put_contents; -mod fileatime; -mod filectime; -mod filegroup; -mod fileinode; -mod filemtime; -mod fileowner; -mod fileperms; -mod filesize; -mod filetype; -mod fclose; -mod fdatasync; -mod feof; -mod fflush; -mod fgetcsv; -mod fgetc; -mod fgets; -mod fnmatch; -mod flock; -mod fopen; -mod fprintf; -mod fpassthru; -mod fputcsv; -mod fread; -mod fscanf; -mod fseek; -mod fstat; -mod ftruncate; -mod fsockopen; -mod fsync; -mod ftell; -mod fwrite; -mod getcwd; -mod glob; -mod is_dir; -mod is_executable; -mod is_file; -mod is_link; -mod is_readable; -mod is_writable; -mod is_writeable; -mod lchgrp; -mod lchown; -mod link; -mod linkinfo; -mod lstat; -mod mkdir; -mod opendir; -mod path_values_dispatch; -mod pathinfo; -mod pclose; -mod pfsockopen; -mod popen; -mod readdir; -mod readline; -mod readfile; -mod readlink; -mod realpath; -mod realpath_cache_get; -mod realpath_cache_size; -mod rename; -mod rewind; -mod rewinddir; -mod rmdir; -mod scandir; -mod stat; -mod stream_bucket_append; -mod stream_bucket_make_writeable; -mod stream_bucket_new; -mod stream_bucket_prepend; -mod stream_context_create; -mod stream_context_get_default; -mod stream_context_get_options; -mod stream_context_get_params; -mod stream_context_set_default; -mod stream_context_set_option; -mod stream_context_set_params; -mod stream_copy_to_stream; -mod stream_filter_append; -mod stream_filter_prepend; -mod stream_filter_register; -mod stream_filter_remove; -mod stream_get_contents; -mod stream_get_line; -mod stream_get_meta_data; -mod stream_isatty; -mod stream_resolve_include_path; -mod stream_select; -mod stream_set_blocking; -mod stream_set_chunk_size; -mod stream_set_read_buffer; -mod stream_set_timeout; -mod stream_set_write_buffer; -mod stream_socket_accept; -mod stream_socket_client; -mod stream_socket_enable_crypto; -mod stream_socket_get_name; -mod stream_socket_pair; -mod stream_socket_recvfrom; -mod stream_socket_sendto; -mod stream_socket_server; -mod stream_socket_shutdown; -mod stream_values_dispatch; -mod stream_wrapper_register; -mod stream_wrapper_restore; -mod stream_wrapper_unregister; -mod symlink; -mod sys_get_temp_dir; -mod tempnam; -mod tmpfile; -mod touch; -mod umask; -mod unlink; -mod values_dispatch; -mod vfprintf; - -pub(in crate::interpreter) use direct_dispatch::eval_builtin_filesystem_call; -pub(in crate::interpreter) use values_dispatch::eval_filesystem_values_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/opendir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/opendir.rs deleted file mode 100644 index 3dd071e63a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/opendir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `opendir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the directory resource open helper. - -eval_builtin! { - name: "opendir", - area: Filesystem, - params: [directory], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pathinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pathinfo.rs deleted file mode 100644 index 55999acb2c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pathinfo.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `pathinfo`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the pathinfo helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "pathinfo", - area: Filesystem, - params: [path, flags = EvalBuiltinDefaultValue::Int(15)], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pclose.rs deleted file mode 100644 index edcadec4a4..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pclose.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `pclose`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process-pipe close helper. - -eval_builtin! { - name: "pclose", - area: Filesystem, - params: [handle], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pfsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pfsockopen.rs deleted file mode 100644 index 7b3ef6b346..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/pfsockopen.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `pfsockopen`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Direct calls keep their source-sensitive by-reference error-output path. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "pfsockopen", - area: Filesystem, - params: [ - hostname, - port, - error_code: by_ref = EvalBuiltinDefaultValue::Null, - error_message: by_ref = EvalBuiltinDefaultValue::Null, - timeout = EvalBuiltinDefaultValue::Null - ], - by_ref: [error_code, error_message], - direct: none, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/popen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/popen.rs deleted file mode 100644 index ce2b084812..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/popen.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `popen`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process-pipe open helper. - -eval_builtin! { - name: "popen", - area: Filesystem, - params: [command, mode], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readdir.rs deleted file mode 100644 index 50f2dd3da7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readdir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `readdir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the directory resource read helper. - -eval_builtin! { - name: "readdir", - area: Filesystem, - params: [dir_handle], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readfile.rs deleted file mode 100644 index 115b45140f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readfile.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `readfile`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the streaming file output helper. - -eval_builtin! { - name: "readfile", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readline.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readline.rs deleted file mode 100644 index cebcc638d9..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readline.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `readline`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the host stdin helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "readline", - area: Filesystem, - params: [prompt = EvalBuiltinDefaultValue::Null], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readlink.rs deleted file mode 100644 index df087854cf..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/readlink.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `readlink`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the symbolic-link target helper. - -eval_builtin! { - name: "readlink", - area: Filesystem, - params: [path], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath.rs deleted file mode 100644 index c9ff9ed562..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `realpath`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the canonical path helper. - -eval_builtin! { - name: "realpath", - area: Filesystem, - params: [path], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_get.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_get.rs deleted file mode 100644 index 60e40bb54a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_get.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `realpath_cache_get`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to elephc's empty realpath-cache helper. - -eval_builtin! { - name: "realpath_cache_get", - area: Filesystem, - params: [], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_size.rs deleted file mode 100644 index d366ffe618..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/realpath_cache_size.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `realpath_cache_size`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to elephc's empty realpath-cache helper. - -eval_builtin! { - name: "realpath_cache_size", - area: Filesystem, - params: [], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rename.rs deleted file mode 100644 index 4246784746..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rename.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `rename`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the binary path operation helper. - -eval_builtin! { - name: "rename", - area: Filesystem, - params: [from, to], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewind.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewind.rs deleted file mode 100644 index 6c9c2aff2b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewind.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `rewind`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "rewind", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewinddir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewinddir.rs deleted file mode 100644 index 336446c92d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rewinddir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `rewinddir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the directory resource rewind helper. - -eval_builtin! { - name: "rewinddir", - area: Filesystem, - params: [dir_handle], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rmdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rmdir.rs deleted file mode 100644 index 8980355a37..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/rmdir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `rmdir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary path operation helper. - -eval_builtin! { - name: "rmdir", - area: Filesystem, - params: [directory], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/scandir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/scandir.rs deleted file mode 100644 index 9ba03d3763..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/scandir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `scandir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the directory listing helper. - -eval_builtin! { - name: "scandir", - area: Filesystem, - params: [directory], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stat.rs deleted file mode 100644 index aaab6907b7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stat.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stat`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stat-array helper. - -eval_builtin! { - name: "stat", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_append.rs deleted file mode 100644 index 0eabc5b9fe..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_append.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_bucket_append`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream bucket push helper. - -eval_builtin! { - name: "stream_bucket_append", - area: Filesystem, - params: [brigade, bucket], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_make_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_make_writeable.rs deleted file mode 100644 index 2a82f0323e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_make_writeable.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_bucket_make_writeable`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream bucket read helper. - -eval_builtin! { - name: "stream_bucket_make_writeable", - area: Filesystem, - params: [brigade], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_new.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_new.rs deleted file mode 100644 index 89092f504c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_new.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_bucket_new`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream bucket object helper. - -eval_builtin! { - name: "stream_bucket_new", - area: Filesystem, - params: [stream, buffer], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_prepend.rs deleted file mode 100644 index 4766438308..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_bucket_prepend.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_bucket_prepend`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream bucket push helper. - -eval_builtin! { - name: "stream_bucket_prepend", - area: Filesystem, - params: [brigade, bucket], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_create.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_create.rs deleted file mode 100644 index 776d293825..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_create.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_context_create`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream context helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_context_create", - area: Filesystem, - params: [ - options = EvalBuiltinDefaultValue::Null, - params = EvalBuiltinDefaultValue::Null - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_default.rs deleted file mode 100644 index e4e723b659..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_default.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_context_get_default`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the default stream context helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_context_get_default", - area: Filesystem, - params: [options = EvalBuiltinDefaultValue::Null], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_options.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_options.rs deleted file mode 100644 index d7bc520142..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_options.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_context_get_options`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream context option reader. - -eval_builtin! { - name: "stream_context_get_options", - area: Filesystem, - params: [context], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_params.rs deleted file mode 100644 index a0b123a9e6..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_get_params.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_context_get_params`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Eval mirrors the main backend's current empty-params behavior. - -eval_builtin! { - name: "stream_context_get_params", - area: Filesystem, - params: [context], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_default.rs deleted file mode 100644 index ef3a639841..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_default.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_context_set_default`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream context helper. - -eval_builtin! { - name: "stream_context_set_default", - area: Filesystem, - params: [options], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_option.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_option.rs deleted file mode 100644 index 558d53488f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_option.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_context_set_option`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - The signature keeps the existing two-argument and four-argument forms. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_context_set_option", - area: Filesystem, - params: [ - context, - wrapper_or_options, - option_name = EvalBuiltinDefaultValue::Null, - value = EvalBuiltinDefaultValue::Null - ], - required: 2, - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_params.rs deleted file mode 100644 index a3657e7e47..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_context_set_params.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_context_set_params`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the accepted no-op helper. - -eval_builtin! { - name: "stream_context_set_params", - area: Filesystem, - params: [context, params], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_copy_to_stream.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_copy_to_stream.rs deleted file mode 100644 index 541594f374..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_copy_to_stream.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_copy_to_stream`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream copy helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_copy_to_stream", - area: Filesystem, - params: [ - from, - to, - length = EvalBuiltinDefaultValue::Null, - offset = EvalBuiltinDefaultValue::Int(-1) - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_append.rs deleted file mode 100644 index 3cd42b458d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_append.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_filter_append`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream filter attachment helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_filter_append", - area: Filesystem, - params: [ - stream, - filtername, - read_write = EvalBuiltinDefaultValue::Int(3), - params = EvalBuiltinDefaultValue::Null - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_prepend.rs deleted file mode 100644 index 9811b60aea..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_prepend.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_filter_prepend`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream filter attachment helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_filter_prepend", - area: Filesystem, - params: [ - stream, - filtername, - read_write = EvalBuiltinDefaultValue::Int(3), - params = EvalBuiltinDefaultValue::Null - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_register.rs deleted file mode 100644 index c8b3b11c09..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_register.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_filter_register`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the conservative filter registry helper. - -eval_builtin! { - name: "stream_filter_register", - area: Filesystem, - params: [filter_name, r#class], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_remove.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_remove.rs deleted file mode 100644 index a68cc7f36d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_filter_remove.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_filter_remove`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream filter removal helper. - -eval_builtin! { - name: "stream_filter_remove", - area: Filesystem, - params: [stream_filter], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_contents.rs deleted file mode 100644 index ca7cfec81c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_contents.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_get_contents`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the bounded stream read helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_get_contents", - area: Filesystem, - params: [ - stream, - length = EvalBuiltinDefaultValue::Null, - offset = EvalBuiltinDefaultValue::Int(-1) - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_line.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_line.rs deleted file mode 100644 index be765332ab..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_line.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_get_line`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the delimiter-aware stream line helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_get_line", - area: Filesystem, - params: [stream, length, ending = EvalBuiltinDefaultValue::String("")], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_meta_data.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_meta_data.rs deleted file mode 100644 index a546d85b41..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_get_meta_data.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_get_meta_data`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unary stream helper. - -eval_builtin! { - name: "stream_get_meta_data", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_isatty.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_isatty.rs deleted file mode 100644 index 62f09a0b22..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_isatty.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_isatty`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream descriptor predicate helper. - -eval_builtin! { - name: "stream_isatty", - area: Filesystem, - params: [stream], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_resolve_include_path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_resolve_include_path.rs deleted file mode 100644 index e9c1718ef8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_resolve_include_path.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_resolve_include_path`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the include-path resolution helper. - -eval_builtin! { - name: "stream_resolve_include_path", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_select.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_select.rs deleted file mode 100644 index a52daff985..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_select.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_select`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Direct calls keep their source-sensitive by-reference array path. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_select", - area: Filesystem, - params: [ - read: by_ref, - write: by_ref, - except: by_ref, - seconds, - microseconds = EvalBuiltinDefaultValue::Int(0) - ], - by_ref: [read, write, except], - direct: none, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_blocking.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_blocking.rs deleted file mode 100644 index ef5f9f23d4..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_blocking.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_set_blocking`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream blocking-mode helper. - -eval_builtin! { - name: "stream_set_blocking", - area: Filesystem, - params: [stream, enable], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_chunk_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_chunk_size.rs deleted file mode 100644 index 22ebbb8ad9..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_chunk_size.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_set_chunk_size`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream chunk-size metadata helper. - -eval_builtin! { - name: "stream_set_chunk_size", - area: Filesystem, - params: [stream, size], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_read_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_read_buffer.rs deleted file mode 100644 index c3621011a8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_read_buffer.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_set_read_buffer`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream buffer-setting helper. - -eval_builtin! { - name: "stream_set_read_buffer", - area: Filesystem, - params: [stream, size], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_timeout.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_timeout.rs deleted file mode 100644 index 1382d9ef85..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_timeout.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_set_timeout`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream timeout-setting helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_set_timeout", - area: Filesystem, - params: [stream, seconds, microseconds = EvalBuiltinDefaultValue::Int(0)], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_write_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_write_buffer.rs deleted file mode 100644 index da2f40132b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_set_write_buffer.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_set_write_buffer`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream buffer-setting helper. - -eval_builtin! { - name: "stream_set_write_buffer", - area: Filesystem, - params: [stream, size], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_accept.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_accept.rs deleted file mode 100644 index c1051e2531..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_accept.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_accept`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Direct calls keep their source-sensitive by-reference peer-name path. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_socket_accept", - area: Filesystem, - params: [ - socket, - timeout = EvalBuiltinDefaultValue::Null, - peer_name: by_ref = EvalBuiltinDefaultValue::Null - ], - by_ref: [peer_name], - direct: none, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_client.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_client.rs deleted file mode 100644 index 1402abeebf..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_client.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_client`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the TCP client stream helper. - -eval_builtin! { - name: "stream_socket_client", - area: Filesystem, - params: [address], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_enable_crypto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_enable_crypto.rs deleted file mode 100644 index fbc315979e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_enable_crypto.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_enable_crypto`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the conservative TLS status helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_socket_enable_crypto", - area: Filesystem, - params: [ - stream, - enable, - crypto_method = EvalBuiltinDefaultValue::Null, - session_stream = EvalBuiltinDefaultValue::Null - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_get_name.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_get_name.rs deleted file mode 100644 index 036df6be02..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_get_name.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_get_name`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the socket-name lookup helper. - -eval_builtin! { - name: "stream_socket_get_name", - area: Filesystem, - params: [socket, remote], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_pair.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_pair.rs deleted file mode 100644 index d1a5000d74..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_pair.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_pair`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the local socket-pair helper. - -eval_builtin! { - name: "stream_socket_pair", - area: Filesystem, - params: [domain, r#type, protocol], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_recvfrom.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_recvfrom.rs deleted file mode 100644 index 677d34c19f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_recvfrom.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_recvfrom`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Direct calls keep their source-sensitive by-reference address path. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_socket_recvfrom", - area: Filesystem, - params: [ - socket, - length, - flags = EvalBuiltinDefaultValue::Int(0), - address: by_ref = EvalBuiltinDefaultValue::String("") - ], - by_ref: [address], - direct: none, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_sendto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_sendto.rs deleted file mode 100644 index d89a5e3445..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_sendto.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_sendto`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the connected-socket write helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_socket_sendto", - area: Filesystem, - params: [ - socket, - data, - flags = EvalBuiltinDefaultValue::Int(0), - address = EvalBuiltinDefaultValue::String("") - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_server.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_server.rs deleted file mode 100644 index ca3916816c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_server.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_server`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the TCP listener helper. - -eval_builtin! { - name: "stream_socket_server", - area: Filesystem, - params: [address], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_shutdown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_shutdown.rs deleted file mode 100644 index f59d61957f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_socket_shutdown.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_socket_shutdown`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the socket shutdown helper. - -eval_builtin! { - name: "stream_socket_shutdown", - area: Filesystem, - params: [stream, mode], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_register.rs deleted file mode 100644 index bfd1ebe565..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_register.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_wrapper_register`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream wrapper registry helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "stream_wrapper_register", - area: Filesystem, - params: [ - protocol, - r#class, - flags = EvalBuiltinDefaultValue::Int(0) - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_restore.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_restore.rs deleted file mode 100644 index ba925f6aa5..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_restore.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_wrapper_restore`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream wrapper registry helper. - -eval_builtin! { - name: "stream_wrapper_restore", - area: Filesystem, - params: [protocol], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_unregister.rs deleted file mode 100644 index 05e5218deb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_wrapper_unregister.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `stream_wrapper_unregister`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the stream wrapper registry helper. - -eval_builtin! { - name: "stream_wrapper_unregister", - area: Filesystem, - params: [protocol], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/symlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/symlink.rs deleted file mode 100644 index b8d0d30558..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/symlink.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `symlink`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the binary path operation helper. - -eval_builtin! { - name: "symlink", - area: Filesystem, - params: [target, link], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/sys_get_temp_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/sys_get_temp_dir.rs deleted file mode 100644 index 4cecdd33bf..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/sys_get_temp_dir.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `sys_get_temp_dir`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the temporary-directory helper. - -eval_builtin! { - name: "sys_get_temp_dir", - area: Filesystem, - params: [], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tempnam.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tempnam.rs deleted file mode 100644 index 5c50e181d1..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tempnam.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `tempnam`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the temporary-name helper. - -eval_builtin! { - name: "tempnam", - area: Filesystem, - params: [directory, prefix], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tmpfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tmpfile.rs deleted file mode 100644 index ac62f7e5ac..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/tmpfile.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `tmpfile`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the temporary stream helper. - -eval_builtin! { - name: "tmpfile", - area: Filesystem, - params: [], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/touch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/touch.rs deleted file mode 100644 index 1c752aa5af..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/touch.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `touch`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the timestamp mutation helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "touch", - area: Filesystem, - params: [ - filename, - mtime = EvalBuiltinDefaultValue::Null, - atime = EvalBuiltinDefaultValue::Null - ], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/umask.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/umask.rs deleted file mode 100644 index d1ef942a03..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/umask.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `umask`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the process umask helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "umask", - area: Filesystem, - params: [mask = EvalBuiltinDefaultValue::Null], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/unlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/unlink.rs deleted file mode 100644 index 04a0049763..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/unlink.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `unlink`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the unlink helper. - -eval_builtin! { - name: "unlink", - area: Filesystem, - params: [filename], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/values_dispatch.rs deleted file mode 100644 index a71d22fc33..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/values_dispatch.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Purpose: -//! Routes evaluated-argument filesystem registry hooks to focused value dispatchers. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks::EvalValuesHook::call()`. -//! -//! Key details: -//! - Values hooks run after named/default argument binding has produced PHP -//! parameter order. - -use super::super::super::super::*; - -use super::path_values_dispatch::eval_filesystem_path_values_result; -use super::stream_values_dispatch::eval_filesystem_stream_values_result; - -/// Dispatches evaluated-argument calls for declaratively migrated filesystem builtins. -pub(in crate::interpreter) fn eval_filesystem_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = - eval_filesystem_path_values_result(name, evaluated_args, context, values)? - { - return Ok(result); - } - if let Some(result) = - eval_filesystem_stream_values_result(name, evaluated_args, context, values)? - { - return Ok(result); - } - Err(EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/vfprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/vfprintf.rs deleted file mode 100644 index dd5847480e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/vfprintf.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `vfprintf`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the vprintf-family stream write helper. - -eval_builtin! { - name: "vfprintf", - area: Filesystem, - params: [stream, format, values], - direct: Filesystem, - values: Filesystem, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs new file mode 100644 index 0000000000..3458c760e3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -0,0 +1,291 @@ +//! Purpose: +//! Direct expression-level dispatch for filesystem builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalDirectHook::call()`. +//! +//! Key details: +//! - This dispatcher keeps filesystem call lowering area-scoped while per-builtin +//! metadata lives in individual declaration files. + +use super::super::super::*; +use super::*; + +/// Routes direct expression-level filesystem builtin calls through per-builtin leaf wrappers. +pub(in crate::interpreter) fn eval_builtin_filesystem_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), + "chdir" => super::chdir::eval_chdir_declared_call(args, context, scope, values), + "chgrp" => super::chgrp::eval_chgrp_declared_call(args, context, scope, values), + "chmod" => super::chmod::eval_chmod_declared_call(args, context, scope, values), + "chown" => super::chown::eval_chown_declared_call(args, context, scope, values), + "clearstatcache" => super::clearstatcache::eval_clearstatcache_declared_call(args, context, scope, values), + "closedir" => super::closedir::eval_closedir_declared_call(args, context, scope, values), + "copy" => super::copy::eval_copy_declared_call(args, context, scope, values), + "dirname" => super::dirname::eval_dirname_declared_call(args, context, scope, values), + "disk_free_space" => super::disk_free_space::eval_disk_free_space_declared_call(args, context, scope, values), + "disk_total_space" => super::disk_total_space::eval_disk_total_space_declared_call(args, context, scope, values), + "fclose" => super::fclose::eval_fclose_declared_call(args, context, scope, values), + "fdatasync" => super::fdatasync::eval_fdatasync_declared_call(args, context, scope, values), + "feof" => super::feof::eval_feof_declared_call(args, context, scope, values), + "fflush" => super::fflush::eval_fflush_declared_call(args, context, scope, values), + "fgetc" => super::fgetc::eval_fgetc_declared_call(args, context, scope, values), + "fgetcsv" => super::fgetcsv::eval_fgetcsv_declared_call(args, context, scope, values), + "fgets" => super::fgets::eval_fgets_declared_call(args, context, scope, values), + "file" => super::file::eval_file_declared_call(args, context, scope, values), + "file_exists" => super::file_exists::eval_file_exists_declared_call(args, context, scope, values), + "file_get_contents" => super::file_get_contents::eval_file_get_contents_declared_call(args, context, scope, values), + "file_put_contents" => super::file_put_contents::eval_file_put_contents_declared_call(args, context, scope, values), + "fileatime" => super::fileatime::eval_fileatime_declared_call(args, context, scope, values), + "filectime" => super::filectime::eval_filectime_declared_call(args, context, scope, values), + "filegroup" => super::filegroup::eval_filegroup_declared_call(args, context, scope, values), + "fileinode" => super::fileinode::eval_fileinode_declared_call(args, context, scope, values), + "filemtime" => super::filemtime::eval_filemtime_declared_call(args, context, scope, values), + "fileowner" => super::fileowner::eval_fileowner_declared_call(args, context, scope, values), + "fileperms" => super::fileperms::eval_fileperms_declared_call(args, context, scope, values), + "filesize" => super::filesize::eval_filesize_declared_call(args, context, scope, values), + "filetype" => super::filetype::eval_filetype_declared_call(args, context, scope, values), + "flock" => super::flock::eval_flock_declared_call(args, context, scope, values), + "fnmatch" => super::fnmatch::eval_fnmatch_declared_call(args, context, scope, values), + "fopen" => super::fopen::eval_fopen_declared_call(args, context, scope, values), + "fpassthru" => super::fpassthru::eval_fpassthru_declared_call(args, context, scope, values), + "fprintf" => super::fprintf::eval_fprintf_declared_call(args, context, scope, values), + "fputcsv" => super::fputcsv::eval_fputcsv_declared_call(args, context, scope, values), + "fread" => super::fread::eval_fread_declared_call(args, context, scope, values), + "fscanf" => super::fscanf::eval_fscanf_declared_call(args, context, scope, values), + "fseek" => super::fseek::eval_fseek_declared_call(args, context, scope, values), + "fsockopen" => super::fsockopen::eval_fsockopen_declared_call(args, context, scope, values), + "fstat" => super::fstat::eval_fstat_declared_call(args, context, scope, values), + "fsync" => super::fsync::eval_fsync_declared_call(args, context, scope, values), + "ftell" => super::ftell::eval_ftell_declared_call(args, context, scope, values), + "ftruncate" => super::ftruncate::eval_ftruncate_declared_call(args, context, scope, values), + "fwrite" => super::fwrite::eval_fwrite_declared_call(args, context, scope, values), + "getcwd" => super::getcwd::eval_getcwd_declared_call(args, context, scope, values), + "glob" => super::glob::eval_glob_declared_call(args, context, scope, values), + "is_dir" => super::is_dir::eval_is_dir_declared_call(args, context, scope, values), + "is_executable" => super::is_executable::eval_is_executable_declared_call(args, context, scope, values), + "is_file" => super::is_file::eval_is_file_declared_call(args, context, scope, values), + "is_link" => super::is_link::eval_is_link_declared_call(args, context, scope, values), + "is_readable" => super::is_readable::eval_is_readable_declared_call(args, context, scope, values), + "is_writable" => super::is_writable::eval_is_writable_declared_call(args, context, scope, values), + "is_writeable" => super::is_writeable::eval_is_writeable_declared_call(args, context, scope, values), + "lchgrp" => super::lchgrp::eval_lchgrp_declared_call(args, context, scope, values), + "lchown" => super::lchown::eval_lchown_declared_call(args, context, scope, values), + "link" => super::link::eval_link_declared_call(args, context, scope, values), + "linkinfo" => super::linkinfo::eval_linkinfo_declared_call(args, context, scope, values), + "lstat" => super::lstat::eval_lstat_declared_call(args, context, scope, values), + "mkdir" => super::mkdir::eval_mkdir_declared_call(args, context, scope, values), + "opendir" => super::opendir::eval_opendir_declared_call(args, context, scope, values), + "pathinfo" => super::pathinfo::eval_pathinfo_declared_call(args, context, scope, values), + "pclose" => super::pclose::eval_pclose_declared_call(args, context, scope, values), + "pfsockopen" => super::pfsockopen::eval_pfsockopen_declared_call(args, context, scope, values), + "popen" => super::popen::eval_popen_declared_call(args, context, scope, values), + "readdir" => super::readdir::eval_readdir_declared_call(args, context, scope, values), + "readfile" => super::readfile::eval_readfile_declared_call(args, context, scope, values), + "readline" => super::readline::eval_readline_declared_call(args, context, scope, values), + "readlink" => super::readlink::eval_readlink_declared_call(args, context, scope, values), + "realpath" => super::realpath::eval_realpath_declared_call(args, context, scope, values), + "realpath_cache_get" => super::realpath_cache_get::eval_realpath_cache_get_declared_call(args, context, scope, values), + "realpath_cache_size" => super::realpath_cache_size::eval_realpath_cache_size_declared_call(args, context, scope, values), + "rename" => super::rename::eval_rename_declared_call(args, context, scope, values), + "rewind" => super::rewind::eval_rewind_declared_call(args, context, scope, values), + "rewinddir" => super::rewinddir::eval_rewinddir_declared_call(args, context, scope, values), + "rmdir" => super::rmdir::eval_rmdir_declared_call(args, context, scope, values), + "scandir" => super::scandir::eval_scandir_declared_call(args, context, scope, values), + "stat" => super::stat::eval_stat_declared_call(args, context, scope, values), + "stream_bucket_append" => super::stream_bucket_append::eval_stream_bucket_append_declared_call(args, context, scope, values), + "stream_bucket_make_writeable" => super::stream_bucket_make_writeable::eval_stream_bucket_make_writeable_declared_call(args, context, scope, values), + "stream_bucket_new" => super::stream_bucket_new::eval_stream_bucket_new_declared_call(args, context, scope, values), + "stream_bucket_prepend" => super::stream_bucket_prepend::eval_stream_bucket_prepend_declared_call(args, context, scope, values), + "stream_context_create" => super::stream_context_create::eval_stream_context_create_declared_call(args, context, scope, values), + "stream_context_get_default" => super::stream_context_get_default::eval_stream_context_get_default_declared_call(args, context, scope, values), + "stream_context_get_options" => super::stream_context_get_options::eval_stream_context_get_options_declared_call(args, context, scope, values), + "stream_context_get_params" => super::stream_context_get_params::eval_stream_context_get_params_declared_call(args, context, scope, values), + "stream_context_set_default" => super::stream_context_set_default::eval_stream_context_set_default_declared_call(args, context, scope, values), + "stream_context_set_option" => super::stream_context_set_option::eval_stream_context_set_option_declared_call(args, context, scope, values), + "stream_context_set_params" => super::stream_context_set_params::eval_stream_context_set_params_declared_call(args, context, scope, values), + "stream_copy_to_stream" => super::stream_copy_to_stream::eval_stream_copy_to_stream_declared_call(args, context, scope, values), + "stream_filter_append" => super::stream_filter_append::eval_stream_filter_append_declared_call(args, context, scope, values), + "stream_filter_prepend" => super::stream_filter_prepend::eval_stream_filter_prepend_declared_call(args, context, scope, values), + "stream_filter_register" => super::stream_filter_register::eval_stream_filter_register_declared_call(args, context, scope, values), + "stream_filter_remove" => super::stream_filter_remove::eval_stream_filter_remove_declared_call(args, context, scope, values), + "stream_get_contents" => super::stream_get_contents::eval_stream_get_contents_declared_call(args, context, scope, values), + "stream_get_line" => super::stream_get_line::eval_stream_get_line_declared_call(args, context, scope, values), + "stream_get_meta_data" => super::stream_get_meta_data::eval_stream_get_meta_data_declared_call(args, context, scope, values), + "stream_isatty" => super::stream_isatty::eval_stream_isatty_declared_call(args, context, scope, values), + "stream_resolve_include_path" => super::stream_resolve_include_path::eval_stream_resolve_include_path_declared_call(args, context, scope, values), + "stream_select" => super::stream_select::eval_stream_select_declared_call(args, context, scope, values), + "stream_set_blocking" => super::stream_set_blocking::eval_stream_set_blocking_declared_call(args, context, scope, values), + "stream_set_chunk_size" => super::stream_set_chunk_size::eval_stream_set_chunk_size_declared_call(args, context, scope, values), + "stream_set_read_buffer" => super::stream_set_read_buffer::eval_stream_set_read_buffer_declared_call(args, context, scope, values), + "stream_set_timeout" => super::stream_set_timeout::eval_stream_set_timeout_declared_call(args, context, scope, values), + "stream_set_write_buffer" => super::stream_set_write_buffer::eval_stream_set_write_buffer_declared_call(args, context, scope, values), + "stream_socket_accept" => super::stream_socket_accept::eval_stream_socket_accept_declared_call(args, context, scope, values), + "stream_socket_client" => super::stream_socket_client::eval_stream_socket_client_declared_call(args, context, scope, values), + "stream_socket_enable_crypto" => super::stream_socket_enable_crypto::eval_stream_socket_enable_crypto_declared_call(args, context, scope, values), + "stream_socket_get_name" => super::stream_socket_get_name::eval_stream_socket_get_name_declared_call(args, context, scope, values), + "stream_socket_pair" => super::stream_socket_pair::eval_stream_socket_pair_declared_call(args, context, scope, values), + "stream_socket_recvfrom" => super::stream_socket_recvfrom::eval_stream_socket_recvfrom_declared_call(args, context, scope, values), + "stream_socket_sendto" => super::stream_socket_sendto::eval_stream_socket_sendto_declared_call(args, context, scope, values), + "stream_socket_server" => super::stream_socket_server::eval_stream_socket_server_declared_call(args, context, scope, values), + "stream_socket_shutdown" => super::stream_socket_shutdown::eval_stream_socket_shutdown_declared_call(args, context, scope, values), + "stream_wrapper_register" => super::stream_wrapper_register::eval_stream_wrapper_register_declared_call(args, context, scope, values), + "stream_wrapper_restore" => super::stream_wrapper_restore::eval_stream_wrapper_restore_declared_call(args, context, scope, values), + "stream_wrapper_unregister" => super::stream_wrapper_unregister::eval_stream_wrapper_unregister_declared_call(args, context, scope, values), + "symlink" => super::symlink::eval_symlink_declared_call(args, context, scope, values), + "sys_get_temp_dir" => super::sys_get_temp_dir::eval_sys_get_temp_dir_declared_call(args, context, scope, values), + "tempnam" => super::tempnam::eval_tempnam_declared_call(args, context, scope, values), + "tmpfile" => super::tmpfile::eval_tmpfile_declared_call(args, context, scope, values), + "touch" => super::touch::eval_touch_declared_call(args, context, scope, values), + "umask" => super::umask::eval_umask_declared_call(args, context, scope, values), + "unlink" => super::unlink::eval_unlink_declared_call(args, context, scope, values), + "vfprintf" => super::vfprintf::eval_vfprintf_declared_call(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches direct expression-level calls for declaratively migrated filesystem builtins. +pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "basename" => eval_builtin_basename(args, context, scope, values), + "chdir" | "mkdir" | "rmdir" => { + eval_builtin_unary_path_bool(name, args, context, scope, values) + } + "chmod" => eval_builtin_chmod(args, context, scope, values), + "chown" | "chgrp" | "lchown" | "lchgrp" => { + eval_builtin_chown_like(name, args, context, scope, values) + } + "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), + "copy" | "link" | "rename" | "symlink" => { + eval_builtin_binary_path_bool(name, args, context, scope, values) + } + "dirname" => eval_builtin_dirname(args, context, scope, values), + "disk_free_space" | "disk_total_space" => { + eval_builtin_disk_space(name, args, context, scope, values) + } + "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), + "file" => eval_builtin_file(args, context, scope, values), + "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" + | "is_writable" | "is_writeable" => { + eval_builtin_file_probe(name, args, context, scope, values) + } + "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), + "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), + "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" + | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), + "filesize" => eval_builtin_filesize(args, context, scope, values), + "filetype" => eval_builtin_filetype(args, context, scope, values), + "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" + | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { + eval_builtin_unary_stream(name, args, context, scope, values) + } + "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), + "fopen" => eval_builtin_fopen(args, context, scope, values), + "fprintf" => eval_builtin_fprintf(args, context, scope, values), + "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), + "fread" => eval_builtin_fread(args, context, scope, values), + "fscanf" => eval_builtin_fscanf(args, context, scope, values), + "fseek" => eval_builtin_fseek(args, context, scope, values), + "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), + "fwrite" => eval_builtin_fwrite(args, context, scope, values), + "getcwd" => eval_builtin_getcwd(args, values), + "glob" => eval_builtin_glob(args, context, scope, values), + "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), + "opendir" => eval_builtin_opendir(args, context, scope, values), + "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), + "pclose" => eval_builtin_pclose(args, context, scope, values), + "popen" => eval_builtin_popen(args, context, scope, values), + "closedir" | "readdir" | "rewinddir" => { + eval_builtin_unary_directory(name, args, context, scope, values) + } + "readfile" => eval_builtin_readfile(args, context, scope, values), + "readline" => eval_builtin_readline(args, context, scope, values), + "readlink" => eval_builtin_readlink(args, context, scope, values), + "realpath" => eval_builtin_realpath(args, context, scope, values), + "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), + "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), + "scandir" => eval_builtin_scandir(args, context, scope, values), + "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), + "stream_bucket_append" | "stream_bucket_prepend" => { + eval_builtin_stream_bucket_push(name, args, context, scope, values) + } + "stream_bucket_make_writeable" => { + eval_builtin_stream_bucket_make_writeable(args, context, scope, values) + } + "stream_bucket_new" => eval_builtin_stream_bucket_new(args, context, scope, values), + "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), + "stream_context_get_default" => { + eval_builtin_stream_context_get_default(args, context, scope, values) + } + "stream_context_get_options" => { + eval_builtin_stream_context_get_options(args, context, scope, values) + } + "stream_context_get_params" => { + eval_builtin_stream_context_get_params(args, context, scope, values) + } + "stream_context_set_default" => { + eval_builtin_stream_context_set_default(args, context, scope, values) + } + "stream_context_set_option" => { + eval_builtin_stream_context_set_option(args, context, scope, values) + } + "stream_context_set_params" => { + eval_builtin_stream_context_set_params(args, context, scope, values) + } + "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), + "stream_filter_append" | "stream_filter_prepend" => { + eval_builtin_stream_filter_attach(name, args, context, scope, values) + } + "stream_filter_register" => { + eval_builtin_stream_filter_register(args, context, scope, values) + } + "stream_filter_remove" => eval_builtin_stream_filter_remove(args, context, scope, values), + "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), + "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), + "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), + "stream_resolve_include_path" => { + eval_builtin_stream_resolve_include_path(args, context, scope, values) + } + "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), + "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { + eval_builtin_stream_set_buffer_like(name, args, context, scope, values) + } + "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), + "stream_socket_client" => eval_builtin_stream_socket_client(args, context, scope, values), + "stream_socket_enable_crypto" => { + eval_builtin_stream_socket_enable_crypto(args, context, scope, values) + } + "stream_socket_get_name" => { + eval_builtin_stream_socket_get_name(args, context, scope, values) + } + "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), + "stream_socket_sendto" => eval_builtin_stream_socket_sendto(args, context, scope, values), + "stream_socket_server" => eval_builtin_stream_socket_server(args, context, scope, values), + "stream_socket_shutdown" => { + eval_builtin_stream_socket_shutdown(args, context, scope, values) + } + "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { + eval_builtin_stream_wrapper_registry(name, args, context, scope, values) + } + "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), + "tempnam" => eval_builtin_tempnam(args, context, scope, values), + "tmpfile" => eval_builtin_tmpfile(args, context, values), + "touch" => eval_builtin_touch(args, context, scope, values), + "umask" => eval_builtin_umask(args, context, scope, values), + "unlink" => eval_builtin_unlink(args, context, scope, values), + "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs new file mode 100644 index 0000000000..f834556e59 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `dirname`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the path helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "dirname", + area: Filesystem, + params: [path, levels = EvalBuiltinDefaultValue::Int(1)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `dirname` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_dirname_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("dirname", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `dirname` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_dirname_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("dirname", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs new file mode 100644 index 0000000000..9f339da3b0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `disk_free_space`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the disk-space helper. + +eval_builtin! { + name: "disk_free_space", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `disk_free_space` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_disk_free_space_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("disk_free_space", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `disk_free_space` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_disk_free_space_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("disk_free_space", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs new file mode 100644 index 0000000000..da9847e072 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `disk_total_space`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the disk-space helper. + +eval_builtin! { + name: "disk_total_space", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `disk_total_space` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_disk_total_space_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("disk_total_space", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `disk_total_space` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_disk_total_space_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("disk_total_space", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs new file mode 100644 index 0000000000..41620e671b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fclose`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fclose", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fclose` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fclose_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fclose", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fclose` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fclose_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fclose", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs new file mode 100644 index 0000000000..5fc208d769 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fdatasync`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fdatasync", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fdatasync` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fdatasync_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fdatasync", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fdatasync` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fdatasync_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fdatasync", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs new file mode 100644 index 0000000000..7408dc8e04 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `feof`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "feof", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `feof` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_feof_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("feof", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `feof` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_feof_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("feof", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs new file mode 100644 index 0000000000..fea5b92588 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fflush`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fflush", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fflush` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fflush_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fflush", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fflush` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fflush_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fflush", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs new file mode 100644 index 0000000000..d49b037493 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fgetc`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fgetc", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fgetc` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgetc_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fgetc", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fgetc` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgetc_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fgetc", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs new file mode 100644 index 0000000000..871d9bde35 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Declarative eval registry entry for `fgetcsv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the CSV stream read helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fgetcsv", + area: Filesystem, + params: [ + stream, + length = EvalBuiltinDefaultValue::Null, + separator = EvalBuiltinDefaultValue::String(",") + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fgetcsv` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgetcsv_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fgetcsv", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fgetcsv` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgetcsv_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fgetcsv", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs new file mode 100644 index 0000000000..d08f4db612 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fgets`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fgets", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fgets` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgets_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fgets", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fgets` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fgets_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fgets", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs new file mode 100644 index 0000000000..6fea274f91 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-lines helper. + +eval_builtin! { + name: "file", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `file` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("file", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `file` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("file", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs new file mode 100644 index 0000000000..00d329a39e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `file_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "file_exists", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `file_exists` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("file_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `file_exists` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("file_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs new file mode 100644 index 0000000000..bccde03fa5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `file_get_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the one-shot file read helper. + +eval_builtin! { + name: "file_get_contents", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `file_get_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_get_contents_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("file_get_contents", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `file_get_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_get_contents_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("file_get_contents", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs new file mode 100644 index 0000000000..bd65c549e2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `file_put_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the one-shot file write helper. + +eval_builtin! { + name: "file_put_contents", + area: Filesystem, + params: [filename, data], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `file_put_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_put_contents_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("file_put_contents", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `file_put_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_file_put_contents_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("file_put_contents", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs new file mode 100644 index 0000000000..c687a63c9c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fileatime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "fileatime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fileatime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileatime_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fileatime", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fileatime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileatime_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fileatime", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs new file mode 100644 index 0000000000..3282466187 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `filectime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "filectime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `filectime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filectime_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("filectime", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filectime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filectime_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("filectime", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs new file mode 100644 index 0000000000..c099b77b3c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `filegroup`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "filegroup", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `filegroup` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filegroup_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("filegroup", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filegroup` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filegroup_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("filegroup", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs new file mode 100644 index 0000000000..e228a2c528 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fileinode`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "fileinode", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fileinode` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileinode_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fileinode", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fileinode` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileinode_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fileinode", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs new file mode 100644 index 0000000000..7e7cf57568 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `filemtime`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "filemtime", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `filemtime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filemtime_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("filemtime", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filemtime` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filemtime_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("filemtime", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs new file mode 100644 index 0000000000..2e51ace233 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fileowner`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "fileowner", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fileowner` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileowner_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fileowner", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fileowner` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileowner_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fileowner", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs new file mode 100644 index 0000000000..fc4a7ce8c6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fileperms`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the scalar stat helper. + +eval_builtin! { + name: "fileperms", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fileperms` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileperms_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fileperms", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fileperms` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fileperms_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fileperms", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs new file mode 100644 index 0000000000..f2c534741e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `filesize`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the filesize helper. + +eval_builtin! { + name: "filesize", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `filesize` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filesize_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("filesize", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filesize` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filesize_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("filesize", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs new file mode 100644 index 0000000000..a3b30f08f2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `filetype`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the filetype helper. + +eval_builtin! { + name: "filetype", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `filetype` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filetype_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("filetype", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `filetype` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_filetype_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("filetype", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs new file mode 100644 index 0000000000..321623f02d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Declarative eval registry entry for `flock`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "flock", + area: Filesystem, + params: [stream, operation, would_block: by_ref = EvalBuiltinDefaultValue::Null], + by_ref: [would_block], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `flock` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_flock_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("flock", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `flock` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_flock_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("flock", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs index 37550ebbc4..973bb8963c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs @@ -1,13 +1,43 @@ //! Purpose: -//! fnmatch implementation and wildcard/class matching helpers. +//! Declarative eval registry entry for `fnmatch`. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem` re-exports. +//! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. +//! - Runtime dispatch is declared here and delegated through the fnmatch helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fnmatch", + area: Filesystem, + params: [pattern, filename, flags = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} use super::super::super::*; + +/// Dispatches direct eval calls for the `fnmatch` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fnmatch_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fnmatch", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fnmatch` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fnmatch_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fnmatch", evaluated_args, context, values) +} + use super::super::*; /// Evaluates PHP `fnmatch($pattern, $filename, $flags = 0)` over eval expressions. diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs new file mode 100644 index 0000000000..7263f918b6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `fopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream-opening helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fopen", + area: Filesystem, + params: [ + filename, + mode, + use_include_path = EvalBuiltinDefaultValue::Bool(false), + context = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fopen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fopen_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fopen", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fopen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fopen_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fopen", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs new file mode 100644 index 0000000000..0f693a0572 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fpassthru`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fpassthru", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fpassthru` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fpassthru_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fpassthru", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fpassthru` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fpassthru_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fpassthru", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs new file mode 100644 index 0000000000..d807d3e52b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `fprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Variadic values are formatted by the existing printf-family helper. + +eval_builtin! { + name: "fprintf", + area: Filesystem, + params: [stream, format], + variadic: values, + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fprintf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fprintf_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fprintf", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fprintf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fprintf_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fprintf", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs new file mode 100644 index 0000000000..8f9808e0cb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `fputcsv`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the CSV stream write helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fputcsv", + area: Filesystem, + params: [ + stream, + fields, + separator = EvalBuiltinDefaultValue::String(","), + enclosure = EvalBuiltinDefaultValue::String("\"") + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fputcsv` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fputcsv_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fputcsv", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fputcsv` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fputcsv_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fputcsv", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs new file mode 100644 index 0000000000..f47b0440d7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fread`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream read helper. + +eval_builtin! { + name: "fread", + area: Filesystem, + params: [stream, length], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fread` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fread_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fread", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fread` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fread_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fread", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs new file mode 100644 index 0000000000..18ccbccf29 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `fscanf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - The current eval implementation returns parsed values and ignores output vars. + +eval_builtin! { + name: "fscanf", + area: Filesystem, + params: [stream, format], + variadic: vars, + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fscanf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fscanf_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fscanf", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fscanf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fscanf_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fscanf", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs new file mode 100644 index 0000000000..ea096f66db --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `fseek`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream seek helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fseek", + area: Filesystem, + params: [stream, offset, whence = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fseek` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fseek_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fseek", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fseek` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fseek_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fseek", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs new file mode 100644 index 0000000000..84e97e27bf --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Declarative eval registry entry for `fsockopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference error-output path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "fsockopen", + area: Filesystem, + params: [ + hostname, + port, + error_code: by_ref = EvalBuiltinDefaultValue::Null, + error_message: by_ref = EvalBuiltinDefaultValue::Null, + timeout = EvalBuiltinDefaultValue::Null + ], + by_ref: [error_code, error_message], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fsockopen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fsockopen_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fsockopen", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fsockopen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fsockopen_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fsockopen", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs new file mode 100644 index 0000000000..b7e0412521 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fstat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fstat", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fstat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fstat_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fstat", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fstat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fstat_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fstat", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs new file mode 100644 index 0000000000..e7ecf063de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fsync`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "fsync", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fsync` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fsync_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fsync", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fsync` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fsync_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fsync", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs new file mode 100644 index 0000000000..bd6687a3ca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `ftell`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "ftell", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `ftell` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_ftell_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("ftell", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `ftell` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_ftell_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("ftell", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs new file mode 100644 index 0000000000..c5ef7b4887 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `ftruncate`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream truncate helper. + +eval_builtin! { + name: "ftruncate", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `ftruncate` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_ftruncate_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("ftruncate", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `ftruncate` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_ftruncate_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("ftruncate", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs new file mode 100644 index 0000000000..596e33d673 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `fwrite`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream write helper. + +eval_builtin! { + name: "fwrite", + area: Filesystem, + params: [stream, data], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `fwrite` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fwrite_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("fwrite", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `fwrite` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_fwrite_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("fwrite", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs new file mode 100644 index 0000000000..f5208b0ccc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `getcwd`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the current-working-directory helper. + +eval_builtin! { + name: "getcwd", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `getcwd` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_getcwd_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("getcwd", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `getcwd` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_getcwd_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("getcwd", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs new file mode 100644 index 0000000000..66e9817d19 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `glob`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the local glob helper. + +eval_builtin! { + name: "glob", + area: Filesystem, + params: [pattern], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `glob` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_glob_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("glob", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `glob` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_glob_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("glob", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs new file mode 100644 index 0000000000..60a9542d33 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `is_dir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_dir", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_dir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_dir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("is_dir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_dir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_dir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("is_dir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs new file mode 100644 index 0000000000..2b328ecb4d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `is_executable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_executable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_executable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_executable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("is_executable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_executable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_executable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("is_executable", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs new file mode 100644 index 0000000000..93fdcd2efb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `is_file`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_file", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_file` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_file_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("is_file", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_file` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_file_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("is_file", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs new file mode 100644 index 0000000000..ec9e0bce1b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `is_link`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_link", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_link` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_link_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("is_link", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_link` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_link_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("is_link", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs new file mode 100644 index 0000000000..837287193d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `is_readable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_readable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_readable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_readable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("is_readable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_readable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_readable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("is_readable", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs new file mode 100644 index 0000000000..762ef4f7c6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `is_writable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_writable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_writable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_writable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("is_writable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_writable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_writable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("is_writable", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs new file mode 100644 index 0000000000..f971dbe761 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `is_writeable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the file-probe helper. + +eval_builtin! { + name: "is_writeable", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_writeable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_writeable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("is_writeable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_writeable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_writeable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("is_writeable", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs new file mode 100644 index 0000000000..1d6a188819 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `lchgrp`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the ownership/group helper. + +eval_builtin! { + name: "lchgrp", + area: Filesystem, + params: [filename, group], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `lchgrp` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lchgrp_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("lchgrp", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `lchgrp` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lchgrp_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("lchgrp", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs new file mode 100644 index 0000000000..8839d8f289 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `lchown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the ownership/group helper. + +eval_builtin! { + name: "lchown", + area: Filesystem, + params: [filename, user], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `lchown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lchown_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("lchown", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `lchown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lchown_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("lchown", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs new file mode 100644 index 0000000000..d8c402580f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `link`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the binary path operation helper. + +eval_builtin! { + name: "link", + area: Filesystem, + params: [target, link], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `link` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_link_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("link", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `link` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_link_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("link", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs new file mode 100644 index 0000000000..5bb7185992 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `linkinfo`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the symbolic-link metadata helper. + +eval_builtin! { + name: "linkinfo", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `linkinfo` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_linkinfo_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("linkinfo", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `linkinfo` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_linkinfo_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("linkinfo", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs new file mode 100644 index 0000000000..e03d963e66 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `lstat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stat-array helper. + +eval_builtin! { + name: "lstat", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `lstat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lstat_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("lstat", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `lstat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_lstat_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("lstat", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs new file mode 100644 index 0000000000..f104ceb07d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `mkdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary path operation helper. + +eval_builtin! { + name: "mkdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `mkdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_mkdir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("mkdir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `mkdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_mkdir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("mkdir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index cdcf9b53da..6e5ff46058 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -1,27 +1,153 @@ //! Purpose: -//! Groups filesystem, path, glob, stat, and fnmatch builtins for eval. +//! Groups filesystem, path, glob, stat, stream, socket, and fnmatch builtins for eval. //! //! Called from: //! - `crate::interpreter::builtins` filesystem-related dispatch. //! //! Key details: -//! - Path arguments are converted through PHP string coercion before touching the -//! host filesystem. +//! - Per-builtin leaf files register metadata and expose small dispatch wrappers. +//! - Shared helpers stay in focused owner modules for path, stream, socket, and +//! wrapper behavior. +mod basename; +mod chdir; +mod chgrp; +mod chmod; +mod chown; +mod clearstatcache; +mod closedir; +mod copy; +mod direct_dispatch; mod directories; -mod declarations; +mod dirname; +mod disk_free_space; +mod disk_total_space; +mod fclose; +mod fdatasync; +mod feof; +mod fflush; +mod fgetc; +mod fgetcsv; +mod fgets; +mod file; mod file_contents; +mod file_exists; +mod file_get_contents; mod file_io; +mod file_put_contents; +mod fileatime; +mod filectime; +mod filegroup; +mod fileinode; +mod filemtime; +mod fileowner; +mod fileperms; +mod filesize; +mod filetype; +mod flock; mod fnmatch; +mod fopen; +mod fpassthru; +mod fprintf; +mod fputcsv; +mod fread; +mod fscanf; +mod fseek; +mod fsockopen; +mod fstat; +mod fsync; +mod ftell; +mod ftruncate; +mod fwrite; +mod getcwd; +mod glob; +mod is_dir; +mod is_executable; +mod is_file; +mod is_link; +mod is_readable; +mod is_writable; +mod is_writeable; +mod lchgrp; +mod lchown; +mod link; +mod linkinfo; +mod lstat; +mod mkdir; +mod opendir; mod ops; mod path; +mod path_values_dispatch; +mod pathinfo; +mod pclose; +mod pfsockopen; +mod popen; mod process_pipes; +mod readdir; +mod readfile; mod readline; -mod stream_extensions; +mod readlink; +mod realpath; +mod realpath_cache_get; +mod realpath_cache_size; +mod rename; +mod rewind; +mod rewinddir; +mod rmdir; +mod scandir; +mod stat; +mod stream_bucket_append; +mod stream_bucket_make_writeable; +mod stream_bucket_new; +mod stream_bucket_prepend; mod stream_context; +mod stream_context_create; +mod stream_context_get_default; +mod stream_context_get_options; +mod stream_context_get_params; +mod stream_context_set_default; +mod stream_context_set_option; +mod stream_context_set_params; +mod stream_copy_to_stream; +mod stream_extensions; +mod stream_filter_append; +mod stream_filter_prepend; +mod stream_filter_register; +mod stream_filter_remove; +mod stream_get_contents; +mod stream_get_line; +mod stream_get_meta_data; +mod stream_isatty; +mod stream_resolve_include_path; +mod stream_select; +mod stream_set_blocking; +mod stream_set_chunk_size; +mod stream_set_read_buffer; +mod stream_set_timeout; +mod stream_set_write_buffer; mod stream_settings; +mod stream_socket_accept; +mod stream_socket_client; +mod stream_socket_enable_crypto; +mod stream_socket_get_name; +mod stream_socket_pair; +mod stream_socket_recvfrom; +mod stream_socket_sendto; +mod stream_socket_server; +mod stream_socket_shutdown; mod stream_sockets; +mod stream_values_dispatch; +mod stream_wrapper_register; +mod stream_wrapper_restore; +mod stream_wrapper_unregister; mod streams; +mod symlink; +mod sys_get_temp_dir; +mod tempnam; +mod tmpfile; +mod touch; +mod umask; +mod unlink; mod user_wrapper_cast; mod user_wrapper_controls; mod user_wrapper_directories; @@ -32,9 +158,10 @@ mod user_wrapper_options; mod user_wrapper_path_ops; mod user_wrapper_stat; mod user_wrapper_streams; +mod values_dispatch; +mod vfprintf; pub(in crate::interpreter) use directories::*; -pub(in crate::interpreter) use declarations::*; pub(in crate::interpreter) use file_contents::*; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; @@ -42,8 +169,8 @@ pub(in crate::interpreter) use ops::*; pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use process_pipes::*; pub(in crate::interpreter) use readline::*; -pub(in crate::interpreter) use stream_extensions::*; pub(in crate::interpreter) use stream_context::*; +pub(in crate::interpreter) use stream_extensions::*; pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; @@ -57,3 +184,5 @@ pub(in crate::interpreter) use user_wrapper_options::*; pub(in crate::interpreter) use user_wrapper_path_ops::*; pub(in crate::interpreter) use user_wrapper_stat::*; pub(in crate::interpreter) use user_wrapper_streams::*; +pub(in crate::interpreter) use direct_dispatch::eval_builtin_filesystem_call; +pub(in crate::interpreter) use values_dispatch::eval_filesystem_values_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs new file mode 100644 index 0000000000..f5a14a7eda --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `opendir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory resource open helper. + +eval_builtin! { + name: "opendir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `opendir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_opendir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("opendir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `opendir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_opendir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("opendir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs similarity index 97% rename from crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/path_values_dispatch.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs index 2b2239a683..e94091ec88 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/path_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs @@ -2,17 +2,17 @@ //! Evaluated-argument dispatch for declarative path, file, directory, and stat builtins. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations::values_dispatch`. +//! - `crate::interpreter::builtins::filesystem::values_dispatch`. //! //! Key details: //! - This module owns by-value dispatch for filesystem helpers that do not work //! with stream resource cursor state. -use super::super::super::super::*; -use super::super::*; +use super::super::super::*; +use super::*; /// Attempts evaluated-argument dispatch for path and file builtins. -pub(in crate::interpreter::builtins::filesystem::declarations) fn eval_filesystem_path_values_result( +pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_result( name: &str, evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs new file mode 100644 index 0000000000..e50c3626fb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `pathinfo`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the pathinfo helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "pathinfo", + area: Filesystem, + params: [path, flags = EvalBuiltinDefaultValue::Int(15)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `pathinfo` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pathinfo_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("pathinfo", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `pathinfo` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pathinfo_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("pathinfo", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs new file mode 100644 index 0000000000..0b2639378a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `pclose`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the process-pipe close helper. + +eval_builtin! { + name: "pclose", + area: Filesystem, + params: [handle], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `pclose` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pclose_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("pclose", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `pclose` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pclose_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("pclose", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs new file mode 100644 index 0000000000..274d44357a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Declarative eval registry entry for `pfsockopen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference error-output path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "pfsockopen", + area: Filesystem, + params: [ + hostname, + port, + error_code: by_ref = EvalBuiltinDefaultValue::Null, + error_message: by_ref = EvalBuiltinDefaultValue::Null, + timeout = EvalBuiltinDefaultValue::Null + ], + by_ref: [error_code, error_message], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `pfsockopen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pfsockopen_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("pfsockopen", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `pfsockopen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_pfsockopen_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("pfsockopen", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs new file mode 100644 index 0000000000..757d1bb242 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `popen`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the process-pipe open helper. + +eval_builtin! { + name: "popen", + area: Filesystem, + params: [command, mode], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `popen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_popen_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("popen", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `popen` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_popen_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("popen", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs new file mode 100644 index 0000000000..13fc89d821 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `readdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory resource read helper. + +eval_builtin! { + name: "readdir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `readdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readdir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("readdir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `readdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readdir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("readdir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs new file mode 100644 index 0000000000..45925547c6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `readfile`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the streaming file output helper. + +eval_builtin! { + name: "readfile", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `readfile` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readfile_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("readfile", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `readfile` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readfile_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("readfile", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs index 8978ab90f5..df7b175bab 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs @@ -1,18 +1,45 @@ //! Purpose: -//! Implements eval's PHP `readline()` builtin against host stdin. +//! Declarative eval registry entry for `readline`. //! //! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - EOF returns PHP `false`, matching the runtime `__rt_fgets` helper. -//! - Returned lines exclude a trailing LF or CRLF terminator. +//! - Runtime dispatch is declared here and delegated through the host stdin helper. -use std::io; +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "readline", + area: Filesystem, + params: [prompt = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} use super::super::super::*; +/// Dispatches direct eval calls for the `readline` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readline_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("readline", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `readline` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readline_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("readline", evaluated_args, context, values) +} + +use std::io; + /// Evaluates `readline([prompt])`. pub(in crate::interpreter) fn eval_builtin_readline( args: &[EvalExpr], diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs new file mode 100644 index 0000000000..55ce2ed3f1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `readlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the symbolic-link target helper. + +eval_builtin! { + name: "readlink", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `readlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readlink_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("readlink", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `readlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_readlink_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("readlink", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs new file mode 100644 index 0000000000..bd5fbd9a65 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `realpath`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the canonical path helper. + +eval_builtin! { + name: "realpath", + area: Filesystem, + params: [path], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `realpath` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_realpath_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("realpath", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `realpath` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_realpath_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("realpath", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs new file mode 100644 index 0000000000..79e84c68ce --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `realpath_cache_get`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through elephc's empty realpath-cache helper. + +eval_builtin! { + name: "realpath_cache_get", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `realpath_cache_get` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_realpath_cache_get_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("realpath_cache_get", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `realpath_cache_get` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_realpath_cache_get_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("realpath_cache_get", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs new file mode 100644 index 0000000000..2091db0299 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `realpath_cache_size`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through elephc's empty realpath-cache helper. + +eval_builtin! { + name: "realpath_cache_size", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `realpath_cache_size` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_realpath_cache_size_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("realpath_cache_size", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `realpath_cache_size` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_realpath_cache_size_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("realpath_cache_size", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs new file mode 100644 index 0000000000..f91e280dd1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `rename`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the binary path operation helper. + +eval_builtin! { + name: "rename", + area: Filesystem, + params: [from, to], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `rename` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rename_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("rename", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `rename` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rename_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("rename", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs new file mode 100644 index 0000000000..300e8a22a7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `rewind`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "rewind", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `rewind` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rewind_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("rewind", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `rewind` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rewind_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("rewind", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs new file mode 100644 index 0000000000..5f41a42c3c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `rewinddir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory resource rewind helper. + +eval_builtin! { + name: "rewinddir", + area: Filesystem, + params: [dir_handle], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `rewinddir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rewinddir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("rewinddir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `rewinddir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rewinddir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("rewinddir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs new file mode 100644 index 0000000000..2ff9dfffb1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `rmdir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary path operation helper. + +eval_builtin! { + name: "rmdir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `rmdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rmdir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("rmdir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `rmdir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_rmdir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("rmdir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs new file mode 100644 index 0000000000..cdbc1e4c30 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `scandir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the directory listing helper. + +eval_builtin! { + name: "scandir", + area: Filesystem, + params: [directory], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `scandir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_scandir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("scandir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `scandir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_scandir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("scandir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs new file mode 100644 index 0000000000..35089e58b0 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stat`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stat-array helper. + +eval_builtin! { + name: "stat", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stat_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stat", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stat` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stat_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stat", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs new file mode 100644 index 0000000000..c4e0f42362 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_bucket_append`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream bucket push helper. + +eval_builtin! { + name: "stream_bucket_append", + area: Filesystem, + params: [brigade, bucket], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_bucket_append` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_bucket_append_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_bucket_append", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_bucket_append` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_bucket_append_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_bucket_append", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs new file mode 100644 index 0000000000..4a613e515d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_bucket_make_writeable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream bucket read helper. + +eval_builtin! { + name: "stream_bucket_make_writeable", + area: Filesystem, + params: [brigade], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_bucket_make_writeable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_bucket_make_writeable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_bucket_make_writeable` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_bucket_make_writeable", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs new file mode 100644 index 0000000000..5db038df80 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_bucket_new`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream bucket object helper. + +eval_builtin! { + name: "stream_bucket_new", + area: Filesystem, + params: [stream, buffer], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_bucket_new` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_bucket_new_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_bucket_new", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_bucket_new` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_bucket_new_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_bucket_new", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs new file mode 100644 index 0000000000..0aa2f92a25 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_bucket_prepend`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream bucket push helper. + +eval_builtin! { + name: "stream_bucket_prepend", + area: Filesystem, + params: [brigade, bucket], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_bucket_prepend` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_bucket_prepend_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_bucket_prepend", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_bucket_prepend` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_bucket_prepend_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_bucket_prepend", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs new file mode 100644 index 0000000000..b828402de7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs @@ -0,0 +1,42 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_create`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream context helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_create", + area: Filesystem, + params: [ + options = EvalBuiltinDefaultValue::Null, + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_context_create` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_create_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_create", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_context_create` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_create_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_context_create", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs new file mode 100644 index 0000000000..5de9f1d4b8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_default`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the default stream context helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_get_default", + area: Filesystem, + params: [options = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_context_get_default` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_get_default_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_get_default", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_context_get_default` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_get_default_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_context_get_default", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs new file mode 100644 index 0000000000..111825030c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_options`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream context option reader. + +eval_builtin! { + name: "stream_context_get_options", + area: Filesystem, + params: [context], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_context_get_options` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_get_options_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_get_options", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_context_get_options` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_get_options_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_context_get_options", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs new file mode 100644 index 0000000000..674f6dae70 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_get_params`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Eval mirrors the main backend's current empty-params behavior. + +eval_builtin! { + name: "stream_context_get_params", + area: Filesystem, + params: [context], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_context_get_params` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_get_params_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_get_params", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_context_get_params` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_get_params_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_context_get_params", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs new file mode 100644 index 0000000000..a5b1d52571 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_default`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream context helper. + +eval_builtin! { + name: "stream_context_set_default", + area: Filesystem, + params: [options], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_context_set_default` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_set_default_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_set_default", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_context_set_default` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_set_default_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_context_set_default", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs new file mode 100644 index 0000000000..0ec2feae25 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_option`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - The signature keeps the existing two-argument and four-argument forms. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_context_set_option", + area: Filesystem, + params: [ + context, + wrapper_or_options, + option_name = EvalBuiltinDefaultValue::Null, + value = EvalBuiltinDefaultValue::Null + ], + required: 2, + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_context_set_option` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_set_option_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_set_option", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_context_set_option` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_set_option_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_context_set_option", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs new file mode 100644 index 0000000000..1fd8406950 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_context_set_params`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the accepted no-op helper. + +eval_builtin! { + name: "stream_context_set_params", + area: Filesystem, + params: [context, params], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_context_set_params` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_set_params_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_set_params", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_context_set_params` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_context_set_params_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_context_set_params", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs new file mode 100644 index 0000000000..dc5b56a2db --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_copy_to_stream`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream copy helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_copy_to_stream", + area: Filesystem, + params: [ + from, + to, + length = EvalBuiltinDefaultValue::Null, + offset = EvalBuiltinDefaultValue::Int(-1) + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_copy_to_stream` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_copy_to_stream_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_copy_to_stream", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_copy_to_stream` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_copy_to_stream_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_copy_to_stream", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs new file mode 100644 index 0000000000..d805bc178b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_filter_append`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream filter attachment helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_filter_append", + area: Filesystem, + params: [ + stream, + filtername, + read_write = EvalBuiltinDefaultValue::Int(3), + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_filter_append` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_filter_append_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_filter_append", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_filter_append` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_filter_append_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_filter_append", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs new file mode 100644 index 0000000000..5fa08f58ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_filter_prepend`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream filter attachment helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_filter_prepend", + area: Filesystem, + params: [ + stream, + filtername, + read_write = EvalBuiltinDefaultValue::Int(3), + params = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_filter_prepend` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_filter_prepend_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_filter_prepend", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_filter_prepend` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_filter_prepend_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_filter_prepend", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs new file mode 100644 index 0000000000..18e4da5978 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_filter_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the conservative filter registry helper. + +eval_builtin! { + name: "stream_filter_register", + area: Filesystem, + params: [filter_name, r#class], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_filter_register` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_filter_register_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_filter_register", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_filter_register` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_filter_register_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_filter_register", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs new file mode 100644 index 0000000000..28872fb103 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_filter_remove`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream filter removal helper. + +eval_builtin! { + name: "stream_filter_remove", + area: Filesystem, + params: [stream_filter], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_filter_remove` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_filter_remove_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_filter_remove", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_filter_remove` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_filter_remove_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_filter_remove", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs new file mode 100644 index 0000000000..6d2223bda4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_contents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the bounded stream read helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_get_contents", + area: Filesystem, + params: [ + stream, + length = EvalBuiltinDefaultValue::Null, + offset = EvalBuiltinDefaultValue::Int(-1) + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_get_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_contents_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_get_contents", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_get_contents` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_contents_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_get_contents", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs new file mode 100644 index 0000000000..19c1449170 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_line`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the delimiter-aware stream line helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_get_line", + area: Filesystem, + params: [stream, length, ending = EvalBuiltinDefaultValue::String("")], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_get_line` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_line_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_get_line", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_get_line` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_line_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_get_line", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs new file mode 100644 index 0000000000..f7178a8283 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_get_meta_data`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unary stream helper. + +eval_builtin! { + name: "stream_get_meta_data", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_get_meta_data` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_meta_data_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_get_meta_data", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_get_meta_data` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_get_meta_data_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_get_meta_data", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs new file mode 100644 index 0000000000..e97fc5b1d3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_isatty`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream descriptor predicate helper. + +eval_builtin! { + name: "stream_isatty", + area: Filesystem, + params: [stream], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_isatty` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_isatty_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_isatty", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_isatty` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_isatty_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_isatty", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs new file mode 100644 index 0000000000..d4b2057b19 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_resolve_include_path`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the include-path resolution helper. + +eval_builtin! { + name: "stream_resolve_include_path", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_resolve_include_path` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_resolve_include_path_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_resolve_include_path", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_resolve_include_path` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_resolve_include_path_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_resolve_include_path", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs new file mode 100644 index 0000000000..b841aab25c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs @@ -0,0 +1,46 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_select`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference array path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_select", + area: Filesystem, + params: [ + read: by_ref, + write: by_ref, + except: by_ref, + seconds, + microseconds = EvalBuiltinDefaultValue::Int(0) + ], + by_ref: [read, write, except], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_select` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_select_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_select", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_select` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_select_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_select", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs new file mode 100644 index 0000000000..3803b82a71 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_blocking`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream blocking-mode helper. + +eval_builtin! { + name: "stream_set_blocking", + area: Filesystem, + params: [stream, enable], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_set_blocking` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_blocking_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_blocking", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_blocking` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_blocking_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_set_blocking", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs new file mode 100644 index 0000000000..fbb872f5b7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_chunk_size`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream chunk-size metadata helper. + +eval_builtin! { + name: "stream_set_chunk_size", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_set_chunk_size` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_chunk_size_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_chunk_size", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_chunk_size` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_chunk_size_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_set_chunk_size", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs new file mode 100644 index 0000000000..08ba984182 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_read_buffer`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream buffer-setting helper. + +eval_builtin! { + name: "stream_set_read_buffer", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_set_read_buffer` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_read_buffer_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_read_buffer", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_read_buffer` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_read_buffer_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_set_read_buffer", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs new file mode 100644 index 0000000000..9cb82c326c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_timeout`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream timeout-setting helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_set_timeout", + area: Filesystem, + params: [stream, seconds, microseconds = EvalBuiltinDefaultValue::Int(0)], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_set_timeout` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_timeout_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_timeout", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_timeout` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_timeout_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_set_timeout", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs new file mode 100644 index 0000000000..ae553d5087 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_set_write_buffer`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream buffer-setting helper. + +eval_builtin! { + name: "stream_set_write_buffer", + area: Filesystem, + params: [stream, size], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_set_write_buffer` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_write_buffer_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_write_buffer", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_set_write_buffer` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_set_write_buffer_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_set_write_buffer", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs new file mode 100644 index 0000000000..7b5514d69d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_accept`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference peer-name path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_accept", + area: Filesystem, + params: [ + socket, + timeout = EvalBuiltinDefaultValue::Null, + peer_name: by_ref = EvalBuiltinDefaultValue::Null + ], + by_ref: [peer_name], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_accept` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_accept_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_accept", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_accept` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_accept_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_accept", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs new file mode 100644 index 0000000000..e7df895137 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_client`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the TCP client stream helper. + +eval_builtin! { + name: "stream_socket_client", + area: Filesystem, + params: [address], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_client` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_client_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_client", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_client` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_client_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_client", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs new file mode 100644 index 0000000000..38e2833d1b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_enable_crypto`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the conservative TLS status helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_enable_crypto", + area: Filesystem, + params: [ + stream, + enable, + crypto_method = EvalBuiltinDefaultValue::Null, + session_stream = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_enable_crypto` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_enable_crypto", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_enable_crypto` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_enable_crypto", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs new file mode 100644 index 0000000000..ebd3249151 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_get_name`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the socket-name lookup helper. + +eval_builtin! { + name: "stream_socket_get_name", + area: Filesystem, + params: [socket, remote], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_get_name` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_get_name_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_get_name", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_get_name` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_get_name_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_get_name", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs new file mode 100644 index 0000000000..7f61198b4c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_pair`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the local socket-pair helper. + +eval_builtin! { + name: "stream_socket_pair", + area: Filesystem, + params: [domain, r#type, protocol], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_pair` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_pair_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_pair", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_pair` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_pair_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_pair", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs new file mode 100644 index 0000000000..3c27c97a06 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs @@ -0,0 +1,45 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_recvfrom`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Direct calls keep their source-sensitive by-reference address path. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_recvfrom", + area: Filesystem, + params: [ + socket, + length, + flags = EvalBuiltinDefaultValue::Int(0), + address: by_ref = EvalBuiltinDefaultValue::String("") + ], + by_ref: [address], + direct: none, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_recvfrom` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_recvfrom", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_recvfrom` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_recvfrom", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs new file mode 100644 index 0000000000..0af7e84be6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_sendto`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the connected-socket write helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_socket_sendto", + area: Filesystem, + params: [ + socket, + data, + flags = EvalBuiltinDefaultValue::Int(0), + address = EvalBuiltinDefaultValue::String("") + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_sendto` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_sendto_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_sendto", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_sendto` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_sendto_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_sendto", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs new file mode 100644 index 0000000000..26afeae52f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_server`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the TCP listener helper. + +eval_builtin! { + name: "stream_socket_server", + area: Filesystem, + params: [address], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_server` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_server_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_server", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_server` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_server_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_server", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs new file mode 100644 index 0000000000..b3227c2798 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_socket_shutdown`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the socket shutdown helper. + +eval_builtin! { + name: "stream_socket_shutdown", + area: Filesystem, + params: [stream, mode], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_socket_shutdown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_shutdown_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_shutdown", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_socket_shutdown` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_socket_shutdown_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_shutdown", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs similarity index 98% rename from crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_values_dispatch.rs rename to crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs index a78a9b3b8a..247aa5a71d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/declarations/stream_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs @@ -2,17 +2,17 @@ //! Evaluated-argument dispatch for declarative stream, socket, context, and CSV builtins. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::declarations::values_dispatch`. +//! - `crate::interpreter::builtins::filesystem::values_dispatch`. //! //! Key details: //! - By-value callable forms emit PHP-style warnings for parameters that are //! normally by-reference outputs. -use super::super::super::super::*; -use super::super::*; +use super::super::super::*; +use super::*; /// Attempts evaluated-argument dispatch for stream and socket builtins. -pub(in crate::interpreter::builtins::filesystem::declarations) fn eval_filesystem_stream_values_result( +pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_values_result( name: &str, evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs new file mode 100644 index 0000000000..fdce45ca68 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_wrapper_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream wrapper registry helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "stream_wrapper_register", + area: Filesystem, + params: [ + protocol, + r#class, + flags = EvalBuiltinDefaultValue::Int(0) + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_wrapper_register` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_wrapper_register_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_wrapper_register", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_wrapper_register` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_wrapper_register_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_wrapper_register", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs new file mode 100644 index 0000000000..f1c37e2b69 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_wrapper_restore`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream wrapper registry helper. + +eval_builtin! { + name: "stream_wrapper_restore", + area: Filesystem, + params: [protocol], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_wrapper_restore` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_wrapper_restore_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_wrapper_restore", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_wrapper_restore` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_wrapper_restore_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_wrapper_restore", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs new file mode 100644 index 0000000000..c073a17ff8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `stream_wrapper_unregister`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the stream wrapper registry helper. + +eval_builtin! { + name: "stream_wrapper_unregister", + area: Filesystem, + params: [protocol], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `stream_wrapper_unregister` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_wrapper_unregister_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_wrapper_unregister", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `stream_wrapper_unregister` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_stream_wrapper_unregister_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("stream_wrapper_unregister", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs new file mode 100644 index 0000000000..a645278505 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `symlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the binary path operation helper. + +eval_builtin! { + name: "symlink", + area: Filesystem, + params: [target, link], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `symlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_symlink_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("symlink", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `symlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_symlink_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("symlink", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs new file mode 100644 index 0000000000..31c7398f6c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `sys_get_temp_dir`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the temporary-directory helper. + +eval_builtin! { + name: "sys_get_temp_dir", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `sys_get_temp_dir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_sys_get_temp_dir_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("sys_get_temp_dir", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `sys_get_temp_dir` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_sys_get_temp_dir_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("sys_get_temp_dir", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs new file mode 100644 index 0000000000..c4a7afe26e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `tempnam`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the temporary-name helper. + +eval_builtin! { + name: "tempnam", + area: Filesystem, + params: [directory, prefix], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `tempnam` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_tempnam_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("tempnam", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `tempnam` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_tempnam_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("tempnam", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs new file mode 100644 index 0000000000..878b9a1519 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `tmpfile`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the temporary stream helper. + +eval_builtin! { + name: "tmpfile", + area: Filesystem, + params: [], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `tmpfile` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_tmpfile_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("tmpfile", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `tmpfile` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_tmpfile_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("tmpfile", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs new file mode 100644 index 0000000000..5f09127527 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Declarative eval registry entry for `touch`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the timestamp mutation helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "touch", + area: Filesystem, + params: [ + filename, + mtime = EvalBuiltinDefaultValue::Null, + atime = EvalBuiltinDefaultValue::Null + ], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `touch` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_touch_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("touch", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `touch` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_touch_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("touch", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs new file mode 100644 index 0000000000..710c99e7e8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `umask`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the process umask helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "umask", + area: Filesystem, + params: [mask = EvalBuiltinDefaultValue::Null], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `umask` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_umask_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("umask", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `umask` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_umask_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("umask", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs new file mode 100644 index 0000000000..840141f2e6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `unlink`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the unlink helper. + +eval_builtin! { + name: "unlink", + area: Filesystem, + params: [filename], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `unlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_unlink_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("unlink", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `unlink` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_unlink_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("unlink", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs new file mode 100644 index 0000000000..7143fd8955 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs @@ -0,0 +1,172 @@ +//! Purpose: +//! Routes evaluated-argument filesystem registry hooks to focused value dispatchers. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalValuesHook::call()`. +//! +//! Key details: +//! - Values hooks run after named/default argument binding has produced PHP +//! parameter order. + +use super::super::super::*; + +use super::path_values_dispatch::eval_filesystem_path_values_result; +use super::stream_values_dispatch::eval_filesystem_stream_values_result; + +/// Routes evaluated-argument filesystem builtin calls through per-builtin leaf wrappers. +pub(in crate::interpreter) fn eval_filesystem_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "basename" => super::basename::eval_basename_declared_values_result(evaluated_args, context, values), + "chdir" => super::chdir::eval_chdir_declared_values_result(evaluated_args, context, values), + "chgrp" => super::chgrp::eval_chgrp_declared_values_result(evaluated_args, context, values), + "chmod" => super::chmod::eval_chmod_declared_values_result(evaluated_args, context, values), + "chown" => super::chown::eval_chown_declared_values_result(evaluated_args, context, values), + "clearstatcache" => super::clearstatcache::eval_clearstatcache_declared_values_result(evaluated_args, context, values), + "closedir" => super::closedir::eval_closedir_declared_values_result(evaluated_args, context, values), + "copy" => super::copy::eval_copy_declared_values_result(evaluated_args, context, values), + "dirname" => super::dirname::eval_dirname_declared_values_result(evaluated_args, context, values), + "disk_free_space" => super::disk_free_space::eval_disk_free_space_declared_values_result(evaluated_args, context, values), + "disk_total_space" => super::disk_total_space::eval_disk_total_space_declared_values_result(evaluated_args, context, values), + "fclose" => super::fclose::eval_fclose_declared_values_result(evaluated_args, context, values), + "fdatasync" => super::fdatasync::eval_fdatasync_declared_values_result(evaluated_args, context, values), + "feof" => super::feof::eval_feof_declared_values_result(evaluated_args, context, values), + "fflush" => super::fflush::eval_fflush_declared_values_result(evaluated_args, context, values), + "fgetc" => super::fgetc::eval_fgetc_declared_values_result(evaluated_args, context, values), + "fgetcsv" => super::fgetcsv::eval_fgetcsv_declared_values_result(evaluated_args, context, values), + "fgets" => super::fgets::eval_fgets_declared_values_result(evaluated_args, context, values), + "file" => super::file::eval_file_declared_values_result(evaluated_args, context, values), + "file_exists" => super::file_exists::eval_file_exists_declared_values_result(evaluated_args, context, values), + "file_get_contents" => super::file_get_contents::eval_file_get_contents_declared_values_result(evaluated_args, context, values), + "file_put_contents" => super::file_put_contents::eval_file_put_contents_declared_values_result(evaluated_args, context, values), + "fileatime" => super::fileatime::eval_fileatime_declared_values_result(evaluated_args, context, values), + "filectime" => super::filectime::eval_filectime_declared_values_result(evaluated_args, context, values), + "filegroup" => super::filegroup::eval_filegroup_declared_values_result(evaluated_args, context, values), + "fileinode" => super::fileinode::eval_fileinode_declared_values_result(evaluated_args, context, values), + "filemtime" => super::filemtime::eval_filemtime_declared_values_result(evaluated_args, context, values), + "fileowner" => super::fileowner::eval_fileowner_declared_values_result(evaluated_args, context, values), + "fileperms" => super::fileperms::eval_fileperms_declared_values_result(evaluated_args, context, values), + "filesize" => super::filesize::eval_filesize_declared_values_result(evaluated_args, context, values), + "filetype" => super::filetype::eval_filetype_declared_values_result(evaluated_args, context, values), + "flock" => super::flock::eval_flock_declared_values_result(evaluated_args, context, values), + "fnmatch" => super::fnmatch::eval_fnmatch_declared_values_result(evaluated_args, context, values), + "fopen" => super::fopen::eval_fopen_declared_values_result(evaluated_args, context, values), + "fpassthru" => super::fpassthru::eval_fpassthru_declared_values_result(evaluated_args, context, values), + "fprintf" => super::fprintf::eval_fprintf_declared_values_result(evaluated_args, context, values), + "fputcsv" => super::fputcsv::eval_fputcsv_declared_values_result(evaluated_args, context, values), + "fread" => super::fread::eval_fread_declared_values_result(evaluated_args, context, values), + "fscanf" => super::fscanf::eval_fscanf_declared_values_result(evaluated_args, context, values), + "fseek" => super::fseek::eval_fseek_declared_values_result(evaluated_args, context, values), + "fsockopen" => super::fsockopen::eval_fsockopen_declared_values_result(evaluated_args, context, values), + "fstat" => super::fstat::eval_fstat_declared_values_result(evaluated_args, context, values), + "fsync" => super::fsync::eval_fsync_declared_values_result(evaluated_args, context, values), + "ftell" => super::ftell::eval_ftell_declared_values_result(evaluated_args, context, values), + "ftruncate" => super::ftruncate::eval_ftruncate_declared_values_result(evaluated_args, context, values), + "fwrite" => super::fwrite::eval_fwrite_declared_values_result(evaluated_args, context, values), + "getcwd" => super::getcwd::eval_getcwd_declared_values_result(evaluated_args, context, values), + "glob" => super::glob::eval_glob_declared_values_result(evaluated_args, context, values), + "is_dir" => super::is_dir::eval_is_dir_declared_values_result(evaluated_args, context, values), + "is_executable" => super::is_executable::eval_is_executable_declared_values_result(evaluated_args, context, values), + "is_file" => super::is_file::eval_is_file_declared_values_result(evaluated_args, context, values), + "is_link" => super::is_link::eval_is_link_declared_values_result(evaluated_args, context, values), + "is_readable" => super::is_readable::eval_is_readable_declared_values_result(evaluated_args, context, values), + "is_writable" => super::is_writable::eval_is_writable_declared_values_result(evaluated_args, context, values), + "is_writeable" => super::is_writeable::eval_is_writeable_declared_values_result(evaluated_args, context, values), + "lchgrp" => super::lchgrp::eval_lchgrp_declared_values_result(evaluated_args, context, values), + "lchown" => super::lchown::eval_lchown_declared_values_result(evaluated_args, context, values), + "link" => super::link::eval_link_declared_values_result(evaluated_args, context, values), + "linkinfo" => super::linkinfo::eval_linkinfo_declared_values_result(evaluated_args, context, values), + "lstat" => super::lstat::eval_lstat_declared_values_result(evaluated_args, context, values), + "mkdir" => super::mkdir::eval_mkdir_declared_values_result(evaluated_args, context, values), + "opendir" => super::opendir::eval_opendir_declared_values_result(evaluated_args, context, values), + "pathinfo" => super::pathinfo::eval_pathinfo_declared_values_result(evaluated_args, context, values), + "pclose" => super::pclose::eval_pclose_declared_values_result(evaluated_args, context, values), + "pfsockopen" => super::pfsockopen::eval_pfsockopen_declared_values_result(evaluated_args, context, values), + "popen" => super::popen::eval_popen_declared_values_result(evaluated_args, context, values), + "readdir" => super::readdir::eval_readdir_declared_values_result(evaluated_args, context, values), + "readfile" => super::readfile::eval_readfile_declared_values_result(evaluated_args, context, values), + "readline" => super::readline::eval_readline_declared_values_result(evaluated_args, context, values), + "readlink" => super::readlink::eval_readlink_declared_values_result(evaluated_args, context, values), + "realpath" => super::realpath::eval_realpath_declared_values_result(evaluated_args, context, values), + "realpath_cache_get" => super::realpath_cache_get::eval_realpath_cache_get_declared_values_result(evaluated_args, context, values), + "realpath_cache_size" => super::realpath_cache_size::eval_realpath_cache_size_declared_values_result(evaluated_args, context, values), + "rename" => super::rename::eval_rename_declared_values_result(evaluated_args, context, values), + "rewind" => super::rewind::eval_rewind_declared_values_result(evaluated_args, context, values), + "rewinddir" => super::rewinddir::eval_rewinddir_declared_values_result(evaluated_args, context, values), + "rmdir" => super::rmdir::eval_rmdir_declared_values_result(evaluated_args, context, values), + "scandir" => super::scandir::eval_scandir_declared_values_result(evaluated_args, context, values), + "stat" => super::stat::eval_stat_declared_values_result(evaluated_args, context, values), + "stream_bucket_append" => super::stream_bucket_append::eval_stream_bucket_append_declared_values_result(evaluated_args, context, values), + "stream_bucket_make_writeable" => super::stream_bucket_make_writeable::eval_stream_bucket_make_writeable_declared_values_result(evaluated_args, context, values), + "stream_bucket_new" => super::stream_bucket_new::eval_stream_bucket_new_declared_values_result(evaluated_args, context, values), + "stream_bucket_prepend" => super::stream_bucket_prepend::eval_stream_bucket_prepend_declared_values_result(evaluated_args, context, values), + "stream_context_create" => super::stream_context_create::eval_stream_context_create_declared_values_result(evaluated_args, context, values), + "stream_context_get_default" => super::stream_context_get_default::eval_stream_context_get_default_declared_values_result(evaluated_args, context, values), + "stream_context_get_options" => super::stream_context_get_options::eval_stream_context_get_options_declared_values_result(evaluated_args, context, values), + "stream_context_get_params" => super::stream_context_get_params::eval_stream_context_get_params_declared_values_result(evaluated_args, context, values), + "stream_context_set_default" => super::stream_context_set_default::eval_stream_context_set_default_declared_values_result(evaluated_args, context, values), + "stream_context_set_option" => super::stream_context_set_option::eval_stream_context_set_option_declared_values_result(evaluated_args, context, values), + "stream_context_set_params" => super::stream_context_set_params::eval_stream_context_set_params_declared_values_result(evaluated_args, context, values), + "stream_copy_to_stream" => super::stream_copy_to_stream::eval_stream_copy_to_stream_declared_values_result(evaluated_args, context, values), + "stream_filter_append" => super::stream_filter_append::eval_stream_filter_append_declared_values_result(evaluated_args, context, values), + "stream_filter_prepend" => super::stream_filter_prepend::eval_stream_filter_prepend_declared_values_result(evaluated_args, context, values), + "stream_filter_register" => super::stream_filter_register::eval_stream_filter_register_declared_values_result(evaluated_args, context, values), + "stream_filter_remove" => super::stream_filter_remove::eval_stream_filter_remove_declared_values_result(evaluated_args, context, values), + "stream_get_contents" => super::stream_get_contents::eval_stream_get_contents_declared_values_result(evaluated_args, context, values), + "stream_get_line" => super::stream_get_line::eval_stream_get_line_declared_values_result(evaluated_args, context, values), + "stream_get_meta_data" => super::stream_get_meta_data::eval_stream_get_meta_data_declared_values_result(evaluated_args, context, values), + "stream_isatty" => super::stream_isatty::eval_stream_isatty_declared_values_result(evaluated_args, context, values), + "stream_resolve_include_path" => super::stream_resolve_include_path::eval_stream_resolve_include_path_declared_values_result(evaluated_args, context, values), + "stream_select" => super::stream_select::eval_stream_select_declared_values_result(evaluated_args, context, values), + "stream_set_blocking" => super::stream_set_blocking::eval_stream_set_blocking_declared_values_result(evaluated_args, context, values), + "stream_set_chunk_size" => super::stream_set_chunk_size::eval_stream_set_chunk_size_declared_values_result(evaluated_args, context, values), + "stream_set_read_buffer" => super::stream_set_read_buffer::eval_stream_set_read_buffer_declared_values_result(evaluated_args, context, values), + "stream_set_timeout" => super::stream_set_timeout::eval_stream_set_timeout_declared_values_result(evaluated_args, context, values), + "stream_set_write_buffer" => super::stream_set_write_buffer::eval_stream_set_write_buffer_declared_values_result(evaluated_args, context, values), + "stream_socket_accept" => super::stream_socket_accept::eval_stream_socket_accept_declared_values_result(evaluated_args, context, values), + "stream_socket_client" => super::stream_socket_client::eval_stream_socket_client_declared_values_result(evaluated_args, context, values), + "stream_socket_enable_crypto" => super::stream_socket_enable_crypto::eval_stream_socket_enable_crypto_declared_values_result(evaluated_args, context, values), + "stream_socket_get_name" => super::stream_socket_get_name::eval_stream_socket_get_name_declared_values_result(evaluated_args, context, values), + "stream_socket_pair" => super::stream_socket_pair::eval_stream_socket_pair_declared_values_result(evaluated_args, context, values), + "stream_socket_recvfrom" => super::stream_socket_recvfrom::eval_stream_socket_recvfrom_declared_values_result(evaluated_args, context, values), + "stream_socket_sendto" => super::stream_socket_sendto::eval_stream_socket_sendto_declared_values_result(evaluated_args, context, values), + "stream_socket_server" => super::stream_socket_server::eval_stream_socket_server_declared_values_result(evaluated_args, context, values), + "stream_socket_shutdown" => super::stream_socket_shutdown::eval_stream_socket_shutdown_declared_values_result(evaluated_args, context, values), + "stream_wrapper_register" => super::stream_wrapper_register::eval_stream_wrapper_register_declared_values_result(evaluated_args, context, values), + "stream_wrapper_restore" => super::stream_wrapper_restore::eval_stream_wrapper_restore_declared_values_result(evaluated_args, context, values), + "stream_wrapper_unregister" => super::stream_wrapper_unregister::eval_stream_wrapper_unregister_declared_values_result(evaluated_args, context, values), + "symlink" => super::symlink::eval_symlink_declared_values_result(evaluated_args, context, values), + "sys_get_temp_dir" => super::sys_get_temp_dir::eval_sys_get_temp_dir_declared_values_result(evaluated_args, context, values), + "tempnam" => super::tempnam::eval_tempnam_declared_values_result(evaluated_args, context, values), + "tmpfile" => super::tmpfile::eval_tmpfile_declared_values_result(evaluated_args, context, values), + "touch" => super::touch::eval_touch_declared_values_result(evaluated_args, context, values), + "umask" => super::umask::eval_umask_declared_values_result(evaluated_args, context, values), + "unlink" => super::unlink::eval_unlink_declared_values_result(evaluated_args, context, values), + "vfprintf" => super::vfprintf::eval_vfprintf_declared_values_result(evaluated_args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated filesystem builtins. +pub(in crate::interpreter) fn eval_filesystem_values_result_impl( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = + eval_filesystem_path_values_result(name, evaluated_args, context, values)? + { + return Ok(result); + } + if let Some(result) = + eval_filesystem_stream_values_result(name, evaluated_args, context, values)? + { + return Ok(result); + } + Err(EvalStatus::RuntimeFatal) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs new file mode 100644 index 0000000000..7d9d85a8cc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `vfprintf`. +//! +//! Called from: +//! - `crate::interpreter::builtins::filesystem`. +//! +//! Key details: +//! - Runtime dispatch is declared here and delegated through the vprintf-family stream write helper. + +eval_builtin! { + name: "vfprintf", + area: Filesystem, + params: [stream, format, values], + direct: Filesystem, + values: Filesystem, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `vfprintf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_vfprintf_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::direct_dispatch::eval_builtin_filesystem_call_impl("vfprintf", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `vfprintf` filesystem builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_vfprintf_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::values_dispatch::eval_filesystem_values_result_impl("vfprintf", evaluated_args, context, values) +} From 4d7ff0f4aea81afa1cfe4541b82ccb3d5dbb6439 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 17:08:55 +0200 Subject: [PATCH 1110/1208] refactor(magician): co-locate symbol builtin declarations --- .../src/interpreter/builtins/symbols.rs | 46 ++- .../builtins/symbols/class_alias.rs | 39 +++ .../builtins/symbols/class_attribute_args.rs | 37 +++ .../builtins/symbols/class_attribute_names.rs | 37 +++ .../builtins/symbols/class_exists.rs | 39 +++ .../builtins/symbols/class_get_attributes.rs | 37 +++ .../builtins/symbols/class_implements.rs | 39 +++ .../builtins/symbols/class_parents.rs | 39 +++ .../builtins/symbols/class_uses.rs | 39 +++ .../symbols/declarations/class_alias.rs | 18 -- .../declarations/class_attribute_args.rs | 16 - .../declarations/class_attribute_names.rs | 16 - .../symbols/declarations/class_exists.rs | 18 -- .../declarations/class_get_attributes.rs | 16 - .../symbols/declarations/class_implements.rs | 18 -- .../symbols/declarations/class_parents.rs | 18 -- .../symbols/declarations/class_uses.rs | 18 -- .../builtins/symbols/declarations/empty.rs | 16 - .../symbols/declarations/enum_exists.rs | 18 -- .../symbols/declarations/function_exists.rs | 16 - .../symbols/declarations/get_called_class.rs | 16 - .../symbols/declarations/get_class.rs | 18 -- .../symbols/declarations/get_class_methods.rs | 16 - .../symbols/declarations/get_class_vars.rs | 16 - .../declarations/get_declared_classes.rs | 16 - .../declarations/get_declared_interfaces.rs | 16 - .../declarations/get_declared_traits.rs | 16 - .../symbols/declarations/get_object_vars.rs | 16 - .../symbols/declarations/get_parent_class.rs | 18 -- .../symbols/declarations/get_resource_id.rs | 16 - .../symbols/declarations/get_resource_type.rs | 16 - .../symbols/declarations/interface_exists.rs | 18 -- .../builtins/symbols/declarations/is_a.rs | 18 -- .../symbols/declarations/is_callable.rs | 23 -- .../symbols/declarations/is_subclass_of.rs | 18 -- .../builtins/symbols/declarations/isset.rs | 17 -- .../symbols/declarations/method_exists.rs | 16 - .../builtins/symbols/declarations/mod.rs | 231 --------------- .../symbols/declarations/property_exists.rs | 16 - .../symbols/declarations/spl_autoload.rs | 18 -- .../symbols/declarations/spl_autoload_call.rs | 16 - .../declarations/spl_autoload_extensions.rs | 18 -- .../declarations/spl_autoload_functions.rs | 16 - .../declarations/spl_autoload_register.rs | 22 -- .../declarations/spl_autoload_unregister.rs | 16 - .../symbols/declarations/spl_classes.rs | 16 - .../symbols/declarations/spl_object_hash.rs | 16 - .../symbols/declarations/spl_object_id.rs | 16 - .../symbols/declarations/trait_exists.rs | 18 -- .../builtins/symbols/declarations/unset.rs | 17 -- .../interpreter/builtins/symbols/dispatch.rs | 275 ++++++++++++++++++ .../src/interpreter/builtins/symbols/empty.rs | 37 +++ .../builtins/symbols/enum_exists.rs | 39 +++ .../builtins/symbols/function_exists.rs | 37 +++ .../builtins/symbols/get_called_class.rs | 37 +++ .../interpreter/builtins/symbols/get_class.rs | 39 +++ .../builtins/symbols/get_class_methods.rs | 37 +++ .../builtins/symbols/get_class_vars.rs | 37 +++ .../builtins/symbols/get_declared_classes.rs | 37 +++ .../symbols/get_declared_interfaces.rs | 37 +++ .../builtins/symbols/get_declared_traits.rs | 37 +++ .../builtins/symbols/get_object_vars.rs | 37 +++ .../builtins/symbols/get_parent_class.rs | 39 +++ .../builtins/symbols/get_resource_id.rs | 37 +++ .../builtins/symbols/get_resource_type.rs | 37 +++ .../builtins/symbols/interface_exists.rs | 39 +++ .../src/interpreter/builtins/symbols/is_a.rs | 39 +++ .../builtins/symbols/is_callable.rs | 44 +++ .../builtins/symbols/is_subclass_of.rs | 39 +++ .../src/interpreter/builtins/symbols/isset.rs | 38 +++ .../builtins/symbols/method_exists.rs | 37 +++ .../builtins/symbols/property_exists.rs | 37 +++ .../builtins/symbols/spl_autoload.rs | 39 +++ .../builtins/symbols/spl_autoload_call.rs | 37 +++ .../symbols/spl_autoload_extensions.rs | 39 +++ .../symbols/spl_autoload_functions.rs | 37 +++ .../builtins/symbols/spl_autoload_register.rs | 43 +++ .../symbols/spl_autoload_unregister.rs | 37 +++ .../builtins/symbols/spl_classes.rs | 56 ++++ .../builtins/symbols/spl_object_hash.rs | 37 +++ .../builtins/symbols/spl_object_id.rs | 37 +++ .../builtins/symbols/trait_exists.rs | 39 +++ .../src/interpreter/builtins/symbols/unset.rs | 38 +++ 83 files changed, 1859 insertions(+), 918 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_alias.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_args.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_names.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_exists.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_get_attributes.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_implements.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_parents.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_uses.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/empty.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/enum_exists.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/function_exists.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_called_class.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_methods.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_vars.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_classes.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_interfaces.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_traits.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_object_vars.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_parent_class.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_id.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_type.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/interface_exists.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_a.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_callable.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_subclass_of.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/isset.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/method_exists.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/property_exists.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_call.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_extensions.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_functions.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_register.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_unregister.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_classes.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_hash.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_id.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/trait_exists.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/declarations/unset.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 19f2707022..e34c0a02ad 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -11,18 +11,56 @@ use super::super::*; mod callable_probe; +mod class_alias; +mod class_attribute_args; +mod class_attribute_names; +mod class_exists; +mod class_get_attributes; +mod class_implements; mod class_names; +mod class_parents; mod class_relations; -mod declarations; +mod class_uses; +mod dispatch; +mod empty; +mod enum_exists; +mod function_exists; mod function_probe; +mod get_called_class; +mod get_class; +mod get_class_methods; +mod get_class_vars; +mod get_declared_classes; +mod get_declared_interfaces; +mod get_declared_traits; +mod get_object_vars; +mod get_parent_class; +mod get_resource_id; +mod get_resource_type; +mod interface_exists; +mod is_a; +mod is_callable; +mod is_subclass_of; +mod isset; mod language_constructs; +mod method_exists; +mod property_exists; +mod spl_autoload; +mod spl_autoload_call; +mod spl_autoload_extensions; +mod spl_autoload_functions; +mod spl_autoload_register; +mod spl_autoload_unregister; +mod spl_classes; +mod spl_object_hash; +mod spl_object_id; +mod trait_exists; +mod unset; pub(in crate::interpreter) use callable_probe::*; pub(in crate::interpreter) use class_names::*; pub(in crate::interpreter) use class_relations::*; -pub(in crate::interpreter) use declarations::{ - eval_builtin_symbols_call, eval_symbols_values_result, -}; +pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; pub(in crate::interpreter) use function_probe::*; pub(in crate::interpreter) use language_constructs::*; use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs new file mode 100644 index 0000000000..10ba0795c4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `class_alias`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the symbol dispatch adapter. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_alias", + area: Symbols, + params: [r#class, alias, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_alias` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_alias_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("class_alias", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_alias` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_alias_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("class_alias", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs new file mode 100644 index 0000000000..396864996c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `class_attribute_args`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-attribute metadata helper. + +eval_builtin! { + name: "class_attribute_args", + area: Symbols, + params: [class_name, attribute_name], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_attribute_args` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_attribute_args_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("class_attribute_args", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_attribute_args` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_attribute_args_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("class_attribute_args", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs new file mode 100644 index 0000000000..3b0bf5955e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `class_attribute_names`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-attribute metadata helper. + +eval_builtin! { + name: "class_attribute_names", + area: Symbols, + params: [class_name], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_attribute_names` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_attribute_names_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("class_attribute_names", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_attribute_names` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_attribute_names_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("class_attribute_names", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs new file mode 100644 index 0000000000..0566182f61 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `class_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-existence probe. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_exists", + area: Symbols, + params: [r#class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("class_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("class_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs new file mode 100644 index 0000000000..a4cde7e8f8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `class_get_attributes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-attribute metadata helper. + +eval_builtin! { + name: "class_get_attributes", + area: Symbols, + params: [class_name], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_get_attributes` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_get_attributes_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("class_get_attributes", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_get_attributes` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_get_attributes_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("class_get_attributes", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs new file mode 100644 index 0000000000..01ef53c347 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `class_implements`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation metadata helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_implements", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_implements` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_implements_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("class_implements", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_implements` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_implements_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("class_implements", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs new file mode 100644 index 0000000000..aa1a064502 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `class_parents`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation metadata helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_parents", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_parents` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_parents_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("class_parents", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_parents` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_parents_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("class_parents", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs new file mode 100644 index 0000000000..9a5bc37322 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `class_uses`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation metadata helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "class_uses", + area: Symbols, + params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `class_uses` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_uses_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("class_uses", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `class_uses` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_class_uses_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("class_uses", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_alias.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_alias.rs deleted file mode 100644 index 4b5953ec1e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_alias.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `class_alias`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the symbol dispatch adapter. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "class_alias", - area: Symbols, - params: [r#class, alias, autoload = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_args.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_args.rs deleted file mode 100644 index 91a2d05cd0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_args.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `class_attribute_args`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-attribute metadata helper. - -eval_builtin! { - name: "class_attribute_args", - area: Symbols, - params: [class_name, attribute_name], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_names.rs deleted file mode 100644 index a1d18f083a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_attribute_names.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `class_attribute_names`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-attribute metadata helper. - -eval_builtin! { - name: "class_attribute_names", - area: Symbols, - params: [class_name], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_exists.rs deleted file mode 100644 index 279e92921c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_exists.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `class_exists`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-existence probe. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "class_exists", - area: Symbols, - params: [r#class, autoload = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_get_attributes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_get_attributes.rs deleted file mode 100644 index c6228f81a3..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_get_attributes.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `class_get_attributes`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-attribute metadata helper. - -eval_builtin! { - name: "class_get_attributes", - area: Symbols, - params: [class_name], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_implements.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_implements.rs deleted file mode 100644 index f353f86425..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_implements.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `class_implements`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-relation metadata helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "class_implements", - area: Symbols, - params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_parents.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_parents.rs deleted file mode 100644 index 8a3fabef30..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_parents.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `class_parents`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-relation metadata helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "class_parents", - area: Symbols, - params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_uses.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_uses.rs deleted file mode 100644 index 188da8681c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/class_uses.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `class_uses`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-relation metadata helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "class_uses", - area: Symbols, - params: [object_or_class, autoload = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/empty.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/empty.rs deleted file mode 100644 index 20def552bb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/empty.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `empty`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Direct calls stay source-sensitive so missing variables are not evaluated normally. - -eval_builtin! { - name: "empty", - area: Symbols, - params: [value], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/enum_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/enum_exists.rs deleted file mode 100644 index c24fd26119..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/enum_exists.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `enum_exists`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-like existence probe. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "enum_exists", - area: Symbols, - params: [r#enum, autoload = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/function_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/function_exists.rs deleted file mode 100644 index 2bd686cb45..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/function_exists.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `function_exists`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the builtin/function probe helper. - -eval_builtin! { - name: "function_exists", - area: Symbols, - params: [function], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_called_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_called_class.rs deleted file mode 100644 index c1c8bd7c40..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_called_class.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_called_class`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the current-class scope helper. - -eval_builtin! { - name: "get_called_class", - area: Symbols, - params: [], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class.rs deleted file mode 100644 index 449b0602ca..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_class`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-name introspection helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "get_class", - area: Symbols, - params: [object = EvalBuiltinDefaultValue::Null], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_methods.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_methods.rs deleted file mode 100644 index c98736b9f0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_methods.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_class_methods`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the OOP introspection helper. - -eval_builtin! { - name: "get_class_methods", - area: Symbols, - params: [object_or_class], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_vars.rs deleted file mode 100644 index 1f1bae3ca6..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_class_vars.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_class_vars`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the OOP introspection helper. - -eval_builtin! { - name: "get_class_vars", - area: Symbols, - params: [r#class], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_classes.rs deleted file mode 100644 index ccc9bb654a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_classes.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_declared_classes`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the declared-symbols helper. - -eval_builtin! { - name: "get_declared_classes", - area: Symbols, - params: [], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_interfaces.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_interfaces.rs deleted file mode 100644 index 19006cf99c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_interfaces.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_declared_interfaces`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the declared-symbols helper. - -eval_builtin! { - name: "get_declared_interfaces", - area: Symbols, - params: [], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_traits.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_traits.rs deleted file mode 100644 index 9c6abbb84d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_declared_traits.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_declared_traits`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the declared-symbols helper. - -eval_builtin! { - name: "get_declared_traits", - area: Symbols, - params: [], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_object_vars.rs deleted file mode 100644 index cec350dd9a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_object_vars.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_object_vars`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the OOP introspection helper. - -eval_builtin! { - name: "get_object_vars", - area: Symbols, - params: [object], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_parent_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_parent_class.rs deleted file mode 100644 index 31a7cd4fe8..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_parent_class.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_parent_class`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the parent-class introspection helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "get_parent_class", - area: Symbols, - params: [object_or_class = EvalBuiltinDefaultValue::Null], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_id.rs deleted file mode 100644 index 49fc720e52..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_id.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_resource_id`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the resource introspection helper. - -eval_builtin! { - name: "get_resource_id", - area: Symbols, - params: [resource], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_type.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_type.rs deleted file mode 100644 index 2a7468201c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/get_resource_type.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `get_resource_type`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the resource introspection helper. - -eval_builtin! { - name: "get_resource_type", - area: Symbols, - params: [resource], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/interface_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/interface_exists.rs deleted file mode 100644 index a4bbac70d7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/interface_exists.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `interface_exists`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the interface-existence probe. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "interface_exists", - area: Symbols, - params: [interface, autoload = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_a.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_a.rs deleted file mode 100644 index e28e830132..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_a.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_a`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-relation predicate helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "is_a", - area: Symbols, - params: [object_or_class, r#class, allow_string = EvalBuiltinDefaultValue::Bool(false)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_callable.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_callable.rs deleted file mode 100644 index df5cbb9d9b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_callable.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_callable`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Direct and dynamic-ref paths preserve `$callable_name` writeback elsewhere. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "is_callable", - area: Symbols, - params: [ - value, - syntax_only = EvalBuiltinDefaultValue::Bool(false), - callable_name: by_ref = EvalBuiltinDefaultValue::Null - ], - by_ref: [callable_name], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_subclass_of.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_subclass_of.rs deleted file mode 100644 index 285a140cc3..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/is_subclass_of.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `is_subclass_of`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-relation predicate helper. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "is_subclass_of", - area: Symbols, - params: [object_or_class, r#class, allow_string = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/isset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/isset.rs deleted file mode 100644 index cf3e878704..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/isset.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `isset`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Direct calls stay source-sensitive so operands are checked without normal reads. - -eval_builtin! { - name: "isset", - area: Symbols, - params: [var], - variadic: vars, - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/method_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/method_exists.rs deleted file mode 100644 index 190d6d8c50..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/method_exists.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `method_exists`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the OOP member-existence helper. - -eval_builtin! { - name: "method_exists", - area: Symbols, - params: [object_or_class, method], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs deleted file mode 100644 index bc9903c712..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/mod.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Purpose: -//! Declarative eval registry entries and dispatch adapters for symbol and class metadata builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols` module loading. -//! - `crate::interpreter::builtins::hooks` for migrated symbol dispatch. -//! -//! Key details: -//! - Direct adapters preserve source-sensitive language constructs and -//! `is_callable()` by-reference writeback. -//! - Values adapters replace the legacy dynamic symbols dispatcher. - -use super::super::super::*; -use super::super::*; - -mod class_alias; -mod class_attribute_args; -mod class_attribute_names; -mod class_exists; -mod class_get_attributes; -mod class_implements; -mod class_parents; -mod class_uses; -mod empty; -mod enum_exists; -mod function_exists; -mod get_called_class; -mod get_class; -mod get_class_methods; -mod get_class_vars; -mod get_declared_classes; -mod get_declared_interfaces; -mod get_declared_traits; -mod get_object_vars; -mod get_parent_class; -mod get_resource_id; -mod get_resource_type; -mod interface_exists; -mod is_a; -mod is_callable; -mod is_subclass_of; -mod isset; -mod method_exists; -mod property_exists; -mod spl_autoload; -mod spl_autoload_call; -mod spl_autoload_extensions; -mod spl_autoload_functions; -mod spl_autoload_register; -mod spl_autoload_unregister; -mod spl_classes; -mod spl_object_hash; -mod spl_object_id; -mod trait_exists; -mod unset; - -/// Dispatches direct expression-level calls for declaratively migrated symbol builtins. -pub(in crate::interpreter) fn eval_builtin_symbols_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "class_alias" => eval_builtin_class_alias(args, context, scope, values), - "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { - eval_builtin_class_attribute_metadata(name, args, context, scope, values) - } - "class_exists" => eval_builtin_class_exists(args, context, scope, values), - "class_implements" | "class_parents" | "class_uses" => { - eval_builtin_class_relation(name, args, context, scope, values) - } - "empty" => eval_builtin_empty(args, context, scope, values), - "enum_exists" | "trait_exists" => { - eval_builtin_class_like_exists(name, args, context, scope, values) - } - "function_exists" | "is_callable" => { - eval_builtin_function_probe(name, args, context, scope, values) - } - "get_called_class" => eval_builtin_get_called_class(args, context, values), - "get_class" => eval_builtin_get_class(args, context, scope, values), - "get_class_methods" => eval_builtin_get_class_methods(args, context, scope, values), - "get_class_vars" => eval_builtin_get_class_vars(args, context, scope, values), - "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { - eval_builtin_get_declared_symbols(name, args, context, values) - } - "get_object_vars" => eval_builtin_get_object_vars(args, context, scope, values), - "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), - "get_resource_id" | "get_resource_type" => { - eval_builtin_resource_introspection(name, args, context, scope, values) - } - "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), - "is_a" | "is_subclass_of" => { - eval_builtin_is_a_relation(name, args, context, scope, values) - } - "isset" => eval_builtin_isset(args, context, scope, values), - "method_exists" | "property_exists" => { - eval_builtin_member_exists(name, args, context, scope, values) - } - "spl_autoload" | "spl_autoload_call" => { - eval_builtin_spl_autoload_void(name, args, context, scope, values) - } - "spl_autoload_extensions" => { - eval_builtin_spl_autoload_extensions(args, context, scope, values) - } - "spl_autoload_functions" => { - eval_builtin_spl_autoload_functions(args, context, scope, values) - } - "spl_autoload_register" | "spl_autoload_unregister" => { - eval_builtin_spl_autoload_bool(name, args, context, scope, values) - } - "spl_classes" => eval_builtin_spl_classes(args, values), - "spl_object_hash" | "spl_object_id" => { - eval_builtin_spl_object_identity(name, args, context, scope, values) - } - "unset" => eval_builtin_unset(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated symbol builtins. -pub(in crate::interpreter) fn eval_symbols_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "class_alias" => eval_class_alias_result(evaluated_args, context, values), - "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { - eval_class_attribute_metadata_result(name, evaluated_args, context, values) - } - "class_exists" => eval_class_exists_result(evaluated_args, context, values), - "class_implements" | "class_parents" | "class_uses" => { - eval_class_relation_result(name, evaluated_args, context, values) - } - "empty" => eval_empty_result(evaluated_args, values), - "enum_exists" | "trait_exists" => { - eval_class_like_exists_result(name, evaluated_args, context, values) - } - "function_exists" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_function_probe_result(name, *value, context, values) - } - "get_called_class" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_called_class_result(context, values) - } - "get_class" => match evaluated_args { - [] => eval_get_class_no_arg_result(context, values), - [object] => eval_get_class_result(*object, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "get_class_methods" => eval_get_class_methods_result(evaluated_args, context, values), - "get_class_vars" => eval_get_class_vars_result(evaluated_args, context, values), - "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_declared_symbols_result(name, context, values) - } - "get_object_vars" => eval_get_object_vars_result(evaluated_args, context, values), - "get_parent_class" => match evaluated_args { - [] => eval_get_parent_class_no_arg_result(context, values), - [object_or_class] => eval_get_parent_class_result(*object_or_class, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "get_resource_id" | "get_resource_type" => { - let [resource] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_resource_introspection_result(name, *resource, values) - } - "interface_exists" => eval_interface_exists_result(evaluated_args, context, values), - "is_a" | "is_subclass_of" => { - eval_is_a_relation_result(name, evaluated_args, context, values) - } - "is_callable" => eval_is_callable_with_values(evaluated_args, context, values), - "isset" => eval_isset_result(evaluated_args, values), - "method_exists" | "property_exists" => { - eval_member_exists_result(name, evaluated_args, context, values) - } - "spl_autoload" | "spl_autoload_call" => { - eval_spl_autoload_void_result(name, evaluated_args, values) - } - "spl_autoload_extensions" => { - eval_spl_autoload_extensions_result(evaluated_args, context, values) - } - "spl_autoload_functions" => eval_spl_autoload_functions_result(evaluated_args, values), - "spl_autoload_register" | "spl_autoload_unregister" => { - eval_spl_autoload_bool_result(name, evaluated_args, values) - } - "spl_classes" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values) - } - "spl_object_hash" | "spl_object_id" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_spl_object_identity_result(name, *object, values) - } - "unset" => eval_unset_result(evaluated_args, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `spl_classes()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_spl_classes( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_spl_classes_result(values) -} - -/// Builds the static class-name list returned by eval `spl_classes()`. -pub(in crate::interpreter) fn eval_spl_classes_result( - values: &mut impl RuntimeValueOps, -) -> Result { - eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/property_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/property_exists.rs deleted file mode 100644 index 99b35e4fa7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/property_exists.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `property_exists`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the OOP member-existence helper. - -eval_builtin! { - name: "property_exists", - area: Symbols, - params: [object_or_class, property], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload.rs deleted file mode 100644 index b5bfdc0e7a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_autoload`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the SPL autoload stub. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "spl_autoload", - area: Symbols, - params: [class, file_extensions = EvalBuiltinDefaultValue::Null], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_call.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_call.rs deleted file mode 100644 index 4399ce7ffc..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_call.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_autoload_call`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the SPL autoload stub. - -eval_builtin! { - name: "spl_autoload_call", - area: Symbols, - params: [class], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_extensions.rs deleted file mode 100644 index d6638aa405..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_extensions.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_autoload_extensions`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to eval-local autoload extension state. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "spl_autoload_extensions", - area: Symbols, - params: [file_extensions = EvalBuiltinDefaultValue::Null], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_functions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_functions.rs deleted file mode 100644 index f6f01926cb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_functions.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_autoload_functions`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the SPL autoload stub. - -eval_builtin! { - name: "spl_autoload_functions", - area: Symbols, - params: [], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_register.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_register.rs deleted file mode 100644 index 9a85579345..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_register.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_autoload_register`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the SPL autoload registration stub. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "spl_autoload_register", - area: Symbols, - params: [ - callback = EvalBuiltinDefaultValue::Null, - throw = EvalBuiltinDefaultValue::Bool(true), - prepend = EvalBuiltinDefaultValue::Bool(false), - ], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_unregister.rs deleted file mode 100644 index 3b87bfeb6c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_autoload_unregister.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_autoload_unregister`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the SPL autoload registration stub. - -eval_builtin! { - name: "spl_autoload_unregister", - area: Symbols, - params: [callback], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_classes.rs deleted file mode 100644 index 960999857f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_classes.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_classes`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the SPL classes helper. - -eval_builtin! { - name: "spl_classes", - area: Symbols, - params: [], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_hash.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_hash.rs deleted file mode 100644 index f4395b3cfe..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_hash.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_object_hash`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the SPL object identity helper. - -eval_builtin! { - name: "spl_object_hash", - area: Symbols, - params: [object], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_id.rs deleted file mode 100644 index f5c827c580..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/spl_object_id.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `spl_object_id`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the SPL object identity helper. - -eval_builtin! { - name: "spl_object_id", - area: Symbols, - params: [object], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/trait_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/trait_exists.rs deleted file mode 100644 index 24ecc4996a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/trait_exists.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `trait_exists`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Runtime behavior stays delegated to the class-like existence probe. - -use super::super::super::spec::EvalBuiltinDefaultValue; - -eval_builtin! { - name: "trait_exists", - area: Symbols, - params: [r#trait, autoload = EvalBuiltinDefaultValue::Bool(true)], - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/unset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/unset.rs deleted file mode 100644 index 295b2de553..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/declarations/unset.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Purpose: -//! Declarative eval registry entry for `unset`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols::declarations`. -//! -//! Key details: -//! - Direct calls stay source-sensitive so writable operands can be removed. - -eval_builtin! { - name: "unset", - area: Symbols, - params: [var], - variadic: vars, - direct: Symbols, - values: Symbols, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs new file mode 100644 index 0000000000..1ef48a00de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs @@ -0,0 +1,275 @@ +//! Purpose: +//! Direct and evaluated-argument dispatch for symbol builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks` for migrated symbol dispatch. +//! +//! Key details: +//! - Public dispatch routes through per-builtin leaf wrappers so declaration +//! files own their registry entry and runtime adapter. +//! - Internal impl dispatch keeps grouped helper behavior unchanged. + +use super::*; + +/// Routes direct expression-level symbol builtin calls through per-builtin leaf wrappers. +pub(in crate::interpreter) fn eval_builtin_symbols_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "class_alias" => super::class_alias::eval_class_alias_declared_call(args, context, scope, values), + "class_attribute_args" => super::class_attribute_args::eval_class_attribute_args_declared_call(args, context, scope, values), + "class_attribute_names" => super::class_attribute_names::eval_class_attribute_names_declared_call(args, context, scope, values), + "class_exists" => super::class_exists::eval_class_exists_declared_call(args, context, scope, values), + "class_get_attributes" => super::class_get_attributes::eval_class_get_attributes_declared_call(args, context, scope, values), + "class_implements" => super::class_implements::eval_class_implements_declared_call(args, context, scope, values), + "class_parents" => super::class_parents::eval_class_parents_declared_call(args, context, scope, values), + "class_uses" => super::class_uses::eval_class_uses_declared_call(args, context, scope, values), + "empty" => super::empty::eval_empty_declared_call(args, context, scope, values), + "enum_exists" => super::enum_exists::eval_enum_exists_declared_call(args, context, scope, values), + "function_exists" => super::function_exists::eval_function_exists_declared_call(args, context, scope, values), + "get_called_class" => super::get_called_class::eval_get_called_class_declared_call(args, context, scope, values), + "get_class" => super::get_class::eval_get_class_declared_call(args, context, scope, values), + "get_class_methods" => super::get_class_methods::eval_get_class_methods_declared_call(args, context, scope, values), + "get_class_vars" => super::get_class_vars::eval_get_class_vars_declared_call(args, context, scope, values), + "get_declared_classes" => super::get_declared_classes::eval_get_declared_classes_declared_call(args, context, scope, values), + "get_declared_interfaces" => super::get_declared_interfaces::eval_get_declared_interfaces_declared_call(args, context, scope, values), + "get_declared_traits" => super::get_declared_traits::eval_get_declared_traits_declared_call(args, context, scope, values), + "get_object_vars" => super::get_object_vars::eval_get_object_vars_declared_call(args, context, scope, values), + "get_parent_class" => super::get_parent_class::eval_get_parent_class_declared_call(args, context, scope, values), + "get_resource_id" => super::get_resource_id::eval_get_resource_id_declared_call(args, context, scope, values), + "get_resource_type" => super::get_resource_type::eval_get_resource_type_declared_call(args, context, scope, values), + "interface_exists" => super::interface_exists::eval_interface_exists_declared_call(args, context, scope, values), + "is_a" => super::is_a::eval_is_a_declared_call(args, context, scope, values), + "is_callable" => super::is_callable::eval_is_callable_declared_call(args, context, scope, values), + "is_subclass_of" => super::is_subclass_of::eval_is_subclass_of_declared_call(args, context, scope, values), + "isset" => super::isset::eval_isset_declared_call(args, context, scope, values), + "method_exists" => super::method_exists::eval_method_exists_declared_call(args, context, scope, values), + "property_exists" => super::property_exists::eval_property_exists_declared_call(args, context, scope, values), + "spl_autoload" => super::spl_autoload::eval_spl_autoload_declared_call(args, context, scope, values), + "spl_autoload_call" => super::spl_autoload_call::eval_spl_autoload_call_declared_call(args, context, scope, values), + "spl_autoload_extensions" => super::spl_autoload_extensions::eval_spl_autoload_extensions_declared_call(args, context, scope, values), + "spl_autoload_functions" => super::spl_autoload_functions::eval_spl_autoload_functions_declared_call(args, context, scope, values), + "spl_autoload_register" => super::spl_autoload_register::eval_spl_autoload_register_declared_call(args, context, scope, values), + "spl_autoload_unregister" => super::spl_autoload_unregister::eval_spl_autoload_unregister_declared_call(args, context, scope, values), + "spl_classes" => super::spl_classes::eval_spl_classes_declared_call(args, context, scope, values), + "spl_object_hash" => super::spl_object_hash::eval_spl_object_hash_declared_call(args, context, scope, values), + "spl_object_id" => super::spl_object_id::eval_spl_object_id_declared_call(args, context, scope, values), + "trait_exists" => super::trait_exists::eval_trait_exists_declared_call(args, context, scope, values), + "unset" => super::unset::eval_unset_declared_call(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Routes evaluated-argument symbol builtin calls through per-builtin leaf wrappers. +pub(in crate::interpreter) fn eval_symbols_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "class_alias" => super::class_alias::eval_class_alias_declared_values_result(evaluated_args, context, values), + "class_attribute_args" => super::class_attribute_args::eval_class_attribute_args_declared_values_result(evaluated_args, context, values), + "class_attribute_names" => super::class_attribute_names::eval_class_attribute_names_declared_values_result(evaluated_args, context, values), + "class_exists" => super::class_exists::eval_class_exists_declared_values_result(evaluated_args, context, values), + "class_get_attributes" => super::class_get_attributes::eval_class_get_attributes_declared_values_result(evaluated_args, context, values), + "class_implements" => super::class_implements::eval_class_implements_declared_values_result(evaluated_args, context, values), + "class_parents" => super::class_parents::eval_class_parents_declared_values_result(evaluated_args, context, values), + "class_uses" => super::class_uses::eval_class_uses_declared_values_result(evaluated_args, context, values), + "empty" => super::empty::eval_empty_declared_values_result(evaluated_args, context, values), + "enum_exists" => super::enum_exists::eval_enum_exists_declared_values_result(evaluated_args, context, values), + "function_exists" => super::function_exists::eval_function_exists_declared_values_result(evaluated_args, context, values), + "get_called_class" => super::get_called_class::eval_get_called_class_declared_values_result(evaluated_args, context, values), + "get_class" => super::get_class::eval_get_class_declared_values_result(evaluated_args, context, values), + "get_class_methods" => super::get_class_methods::eval_get_class_methods_declared_values_result(evaluated_args, context, values), + "get_class_vars" => super::get_class_vars::eval_get_class_vars_declared_values_result(evaluated_args, context, values), + "get_declared_classes" => super::get_declared_classes::eval_get_declared_classes_declared_values_result(evaluated_args, context, values), + "get_declared_interfaces" => super::get_declared_interfaces::eval_get_declared_interfaces_declared_values_result(evaluated_args, context, values), + "get_declared_traits" => super::get_declared_traits::eval_get_declared_traits_declared_values_result(evaluated_args, context, values), + "get_object_vars" => super::get_object_vars::eval_get_object_vars_declared_values_result(evaluated_args, context, values), + "get_parent_class" => super::get_parent_class::eval_get_parent_class_declared_values_result(evaluated_args, context, values), + "get_resource_id" => super::get_resource_id::eval_get_resource_id_declared_values_result(evaluated_args, context, values), + "get_resource_type" => super::get_resource_type::eval_get_resource_type_declared_values_result(evaluated_args, context, values), + "interface_exists" => super::interface_exists::eval_interface_exists_declared_values_result(evaluated_args, context, values), + "is_a" => super::is_a::eval_is_a_declared_values_result(evaluated_args, context, values), + "is_callable" => super::is_callable::eval_is_callable_declared_values_result(evaluated_args, context, values), + "is_subclass_of" => super::is_subclass_of::eval_is_subclass_of_declared_values_result(evaluated_args, context, values), + "isset" => super::isset::eval_isset_declared_values_result(evaluated_args, context, values), + "method_exists" => super::method_exists::eval_method_exists_declared_values_result(evaluated_args, context, values), + "property_exists" => super::property_exists::eval_property_exists_declared_values_result(evaluated_args, context, values), + "spl_autoload" => super::spl_autoload::eval_spl_autoload_declared_values_result(evaluated_args, context, values), + "spl_autoload_call" => super::spl_autoload_call::eval_spl_autoload_call_declared_values_result(evaluated_args, context, values), + "spl_autoload_extensions" => super::spl_autoload_extensions::eval_spl_autoload_extensions_declared_values_result(evaluated_args, context, values), + "spl_autoload_functions" => super::spl_autoload_functions::eval_spl_autoload_functions_declared_values_result(evaluated_args, context, values), + "spl_autoload_register" => super::spl_autoload_register::eval_spl_autoload_register_declared_values_result(evaluated_args, context, values), + "spl_autoload_unregister" => super::spl_autoload_unregister::eval_spl_autoload_unregister_declared_values_result(evaluated_args, context, values), + "spl_classes" => super::spl_classes::eval_spl_classes_declared_values_result(evaluated_args, context, values), + "spl_object_hash" => super::spl_object_hash::eval_spl_object_hash_declared_values_result(evaluated_args, context, values), + "spl_object_id" => super::spl_object_id::eval_spl_object_id_declared_values_result(evaluated_args, context, values), + "trait_exists" => super::trait_exists::eval_trait_exists_declared_values_result(evaluated_args, context, values), + "unset" => super::unset::eval_unset_declared_values_result(evaluated_args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches direct expression-level calls for declaratively migrated symbol builtins. +pub(in crate::interpreter) fn eval_builtin_symbols_call_impl( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "class_alias" => eval_builtin_class_alias(args, context, scope, values), + "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { + eval_builtin_class_attribute_metadata(name, args, context, scope, values) + } + "class_exists" => eval_builtin_class_exists(args, context, scope, values), + "class_implements" | "class_parents" | "class_uses" => { + eval_builtin_class_relation(name, args, context, scope, values) + } + "empty" => eval_builtin_empty(args, context, scope, values), + "enum_exists" | "trait_exists" => { + eval_builtin_class_like_exists(name, args, context, scope, values) + } + "function_exists" | "is_callable" => { + eval_builtin_function_probe(name, args, context, scope, values) + } + "get_called_class" => eval_builtin_get_called_class(args, context, values), + "get_class" => eval_builtin_get_class(args, context, scope, values), + "get_class_methods" => eval_builtin_get_class_methods(args, context, scope, values), + "get_class_vars" => eval_builtin_get_class_vars(args, context, scope, values), + "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { + eval_builtin_get_declared_symbols(name, args, context, values) + } + "get_object_vars" => eval_builtin_get_object_vars(args, context, scope, values), + "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), + "get_resource_id" | "get_resource_type" => { + eval_builtin_resource_introspection(name, args, context, scope, values) + } + "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), + "is_a" | "is_subclass_of" => { + eval_builtin_is_a_relation(name, args, context, scope, values) + } + "isset" => eval_builtin_isset(args, context, scope, values), + "method_exists" | "property_exists" => { + eval_builtin_member_exists(name, args, context, scope, values) + } + "spl_autoload" | "spl_autoload_call" => { + eval_builtin_spl_autoload_void(name, args, context, scope, values) + } + "spl_autoload_extensions" => { + eval_builtin_spl_autoload_extensions(args, context, scope, values) + } + "spl_autoload_functions" => { + eval_builtin_spl_autoload_functions(args, context, scope, values) + } + "spl_autoload_register" | "spl_autoload_unregister" => { + eval_builtin_spl_autoload_bool(name, args, context, scope, values) + } + "spl_classes" => super::spl_classes::eval_builtin_spl_classes(args, values), + "spl_object_hash" | "spl_object_id" => { + eval_builtin_spl_object_identity(name, args, context, scope, values) + } + "unset" => eval_builtin_unset(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches evaluated-argument calls for declaratively migrated symbol builtins. +pub(in crate::interpreter) fn eval_symbols_values_result_impl( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "class_alias" => eval_class_alias_result(evaluated_args, context, values), + "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { + eval_class_attribute_metadata_result(name, evaluated_args, context, values) + } + "class_exists" => eval_class_exists_result(evaluated_args, context, values), + "class_implements" | "class_parents" | "class_uses" => { + eval_class_relation_result(name, evaluated_args, context, values) + } + "empty" => eval_empty_result(evaluated_args, values), + "enum_exists" | "trait_exists" => { + eval_class_like_exists_result(name, evaluated_args, context, values) + } + "function_exists" => { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_function_probe_result(name, *value, context, values) + } + "get_called_class" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_called_class_result(context, values) + } + "get_class" => match evaluated_args { + [] => eval_get_class_no_arg_result(context, values), + [object] => eval_get_class_result(*object, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "get_class_methods" => eval_get_class_methods_result(evaluated_args, context, values), + "get_class_vars" => eval_get_class_vars_result(evaluated_args, context, values), + "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_declared_symbols_result(name, context, values) + } + "get_object_vars" => eval_get_object_vars_result(evaluated_args, context, values), + "get_parent_class" => match evaluated_args { + [] => eval_get_parent_class_no_arg_result(context, values), + [object_or_class] => eval_get_parent_class_result(*object_or_class, context, values), + _ => Err(EvalStatus::RuntimeFatal), + }, + "get_resource_id" | "get_resource_type" => { + let [resource] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_resource_introspection_result(name, *resource, values) + } + "interface_exists" => eval_interface_exists_result(evaluated_args, context, values), + "is_a" | "is_subclass_of" => { + eval_is_a_relation_result(name, evaluated_args, context, values) + } + "is_callable" => eval_is_callable_with_values(evaluated_args, context, values), + "isset" => eval_isset_result(evaluated_args, values), + "method_exists" | "property_exists" => { + eval_member_exists_result(name, evaluated_args, context, values) + } + "spl_autoload" | "spl_autoload_call" => { + eval_spl_autoload_void_result(name, evaluated_args, values) + } + "spl_autoload_extensions" => { + eval_spl_autoload_extensions_result(evaluated_args, context, values) + } + "spl_autoload_functions" => eval_spl_autoload_functions_result(evaluated_args, values), + "spl_autoload_register" | "spl_autoload_unregister" => { + eval_spl_autoload_bool_result(name, evaluated_args, values) + } + "spl_classes" => { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + super::spl_classes::eval_spl_classes_result(values) + } + "spl_object_hash" | "spl_object_id" => { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_spl_object_identity_result(name, *object, values) + } + "unset" => eval_unset_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs new file mode 100644 index 0000000000..0eae66799a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `empty`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so missing variables are not evaluated normally. + +eval_builtin! { + name: "empty", + area: Symbols, + params: [value], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `empty` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_empty_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("empty", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `empty` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_empty_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("empty", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs new file mode 100644 index 0000000000..efbbe08ca8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `enum_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-like existence probe. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "enum_exists", + area: Symbols, + params: [r#enum, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `enum_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_enum_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("enum_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `enum_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_enum_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("enum_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs new file mode 100644 index 0000000000..6d3b11b277 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `function_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the builtin/function probe helper. + +eval_builtin! { + name: "function_exists", + area: Symbols, + params: [function], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `function_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_function_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("function_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `function_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_function_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("function_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs new file mode 100644 index 0000000000..f402196417 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_called_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the current-class scope helper. + +eval_builtin! { + name: "get_called_class", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_called_class` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_called_class_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_called_class", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_called_class` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_called_class_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_called_class", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs new file mode 100644 index 0000000000..329b7b2fce --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `get_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-name introspection helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "get_class", + area: Symbols, + params: [object = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_class` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_class_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_class", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_class` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_class_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_class", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs new file mode 100644 index 0000000000..0770da992f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_class_methods`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP introspection helper. + +eval_builtin! { + name: "get_class_methods", + area: Symbols, + params: [object_or_class], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_class_methods` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_class_methods_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_class_methods", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_class_methods` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_class_methods_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_class_methods", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs new file mode 100644 index 0000000000..8fdd5d17b1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_class_vars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP introspection helper. + +eval_builtin! { + name: "get_class_vars", + area: Symbols, + params: [r#class], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_class_vars` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_class_vars_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_class_vars", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_class_vars` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_class_vars_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_class_vars", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs new file mode 100644 index 0000000000..1aefc89057 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_declared_classes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the declared-symbols helper. + +eval_builtin! { + name: "get_declared_classes", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_declared_classes` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_declared_classes_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_declared_classes", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_declared_classes` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_declared_classes_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_declared_classes", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs new file mode 100644 index 0000000000..33f6b41685 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_declared_interfaces`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the declared-symbols helper. + +eval_builtin! { + name: "get_declared_interfaces", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_declared_interfaces` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_declared_interfaces_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_declared_interfaces", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_declared_interfaces` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_declared_interfaces_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_declared_interfaces", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs new file mode 100644 index 0000000000..ee50106f24 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_declared_traits`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the declared-symbols helper. + +eval_builtin! { + name: "get_declared_traits", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_declared_traits` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_declared_traits_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_declared_traits", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_declared_traits` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_declared_traits_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_declared_traits", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs new file mode 100644 index 0000000000..802aae2f0d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_object_vars`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP introspection helper. + +eval_builtin! { + name: "get_object_vars", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_object_vars` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_object_vars_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_object_vars", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_object_vars` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_object_vars_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_object_vars", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs new file mode 100644 index 0000000000..209a5ee7c9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `get_parent_class`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the parent-class introspection helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "get_parent_class", + area: Symbols, + params: [object_or_class = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_parent_class` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_parent_class_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_parent_class", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_parent_class` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_parent_class_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_parent_class", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs new file mode 100644 index 0000000000..672dce8d8a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_resource_id`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the resource introspection helper. + +eval_builtin! { + name: "get_resource_id", + area: Symbols, + params: [resource], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_resource_id` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_resource_id_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_resource_id", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_resource_id` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_resource_id_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_resource_id", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs new file mode 100644 index 0000000000..01e3e1a458 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `get_resource_type`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the resource introspection helper. + +eval_builtin! { + name: "get_resource_type", + area: Symbols, + params: [resource], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `get_resource_type` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_resource_type_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("get_resource_type", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `get_resource_type` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_get_resource_type_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("get_resource_type", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs new file mode 100644 index 0000000000..4cfa40f965 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `interface_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the interface-existence probe. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "interface_exists", + area: Symbols, + params: [interface, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `interface_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_interface_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("interface_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `interface_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_interface_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("interface_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs new file mode 100644 index 0000000000..01f767e87a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `is_a`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation predicate helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_a", + area: Symbols, + params: [object_or_class, r#class, allow_string = EvalBuiltinDefaultValue::Bool(false)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_a` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_a_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("is_a", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_a` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_a_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("is_a", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs new file mode 100644 index 0000000000..4a9dc3b4ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs @@ -0,0 +1,44 @@ +//! Purpose: +//! Declarative eval registry entry for `is_callable`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Direct and dynamic-ref paths preserve `$callable_name` writeback elsewhere. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_callable", + area: Symbols, + params: [ + value, + syntax_only = EvalBuiltinDefaultValue::Bool(false), + callable_name: by_ref = EvalBuiltinDefaultValue::Null + ], + by_ref: [callable_name], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_callable` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_callable_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("is_callable", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_callable` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_callable_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("is_callable", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs new file mode 100644 index 0000000000..32e9e8ed94 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `is_subclass_of`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-relation predicate helper. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "is_subclass_of", + area: Symbols, + params: [object_or_class, r#class, allow_string = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `is_subclass_of` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_subclass_of_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("is_subclass_of", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `is_subclass_of` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_is_subclass_of_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("is_subclass_of", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs new file mode 100644 index 0000000000..960b34d755 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `isset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so operands are checked without normal reads. + +eval_builtin! { + name: "isset", + area: Symbols, + params: [var], + variadic: vars, + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `isset` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_isset_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("isset", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `isset` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_isset_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("isset", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs new file mode 100644 index 0000000000..de9c2f8b3d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `method_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP member-existence helper. + +eval_builtin! { + name: "method_exists", + area: Symbols, + params: [object_or_class, method], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `method_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_method_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("method_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `method_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_method_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("method_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs new file mode 100644 index 0000000000..8218748eb6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `property_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the OOP member-existence helper. + +eval_builtin! { + name: "property_exists", + area: Symbols, + params: [object_or_class, property], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `property_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_property_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("property_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `property_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_property_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("property_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs new file mode 100644 index 0000000000..d889e22ab9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload stub. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload", + area: Symbols, + params: [class, file_extensions = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_autoload` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_autoload", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_autoload` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_autoload", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs new file mode 100644 index 0000000000..f397f18315 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_call`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload stub. + +eval_builtin! { + name: "spl_autoload_call", + area: Symbols, + params: [class], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_autoload_call` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_call_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_call", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_autoload_call` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_call_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_autoload_call", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs new file mode 100644 index 0000000000..85766c7662 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_extensions`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to eval-local autoload extension state. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload_extensions", + area: Symbols, + params: [file_extensions = EvalBuiltinDefaultValue::Null], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_autoload_extensions` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_extensions_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_extensions", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_autoload_extensions` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_extensions_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_autoload_extensions", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs new file mode 100644 index 0000000000..6e07b28c67 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_functions`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload stub. + +eval_builtin! { + name: "spl_autoload_functions", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_autoload_functions` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_functions_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_functions", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_autoload_functions` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_functions_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_autoload_functions", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs new file mode 100644 index 0000000000..0b9183c7ff --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs @@ -0,0 +1,43 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_register`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload registration stub. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "spl_autoload_register", + area: Symbols, + params: [ + callback = EvalBuiltinDefaultValue::Null, + throw = EvalBuiltinDefaultValue::Bool(true), + prepend = EvalBuiltinDefaultValue::Bool(false), + ], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_autoload_register` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_register_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_register", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_autoload_register` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_register_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_autoload_register", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs new file mode 100644 index 0000000000..199b0f6ad7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_autoload_unregister`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL autoload registration stub. + +eval_builtin! { + name: "spl_autoload_unregister", + area: Symbols, + params: [callback], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_autoload_unregister` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_unregister_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_unregister", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_autoload_unregister` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_autoload_unregister_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_autoload_unregister", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs new file mode 100644 index 0000000000..11aadff3a3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs @@ -0,0 +1,56 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_classes`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL classes helper. + +eval_builtin! { + name: "spl_classes", + area: Symbols, + params: [], + direct: Symbols, + values: Symbols, +} + +use super::super::eval_static_string_array_result; +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_classes` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_classes_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_classes", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_classes` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_classes_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_classes", evaluated_args, context, values) +} + +/// Evaluates PHP `spl_classes()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_spl_classes( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_spl_classes_result(values) +} + +/// Builds the static class-name list returned by eval `spl_classes()`. +pub(in crate::interpreter) fn eval_spl_classes_result( + values: &mut impl RuntimeValueOps, +) -> Result { + eval_static_string_array_result(EVAL_SPL_CLASS_NAMES, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs new file mode 100644 index 0000000000..debc762226 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_object_hash`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL object identity helper. + +eval_builtin! { + name: "spl_object_hash", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_object_hash` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_object_hash_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_object_hash", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_object_hash` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_object_hash_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_object_hash", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs new file mode 100644 index 0000000000..c2a34b0763 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs @@ -0,0 +1,37 @@ +//! Purpose: +//! Declarative eval registry entry for `spl_object_id`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the SPL object identity helper. + +eval_builtin! { + name: "spl_object_id", + area: Symbols, + params: [object], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `spl_object_id` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_object_id_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("spl_object_id", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `spl_object_id` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_spl_object_id_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("spl_object_id", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs new file mode 100644 index 0000000000..ca1a8e87b4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs @@ -0,0 +1,39 @@ +//! Purpose: +//! Declarative eval registry entry for `trait_exists`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Runtime behavior stays delegated to the class-like existence probe. + +use super::super::spec::EvalBuiltinDefaultValue; + +eval_builtin! { + name: "trait_exists", + area: Symbols, + params: [r#trait, autoload = EvalBuiltinDefaultValue::Bool(true)], + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `trait_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_trait_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("trait_exists", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `trait_exists` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_trait_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("trait_exists", evaluated_args, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs new file mode 100644 index 0000000000..ba7c218b5e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs @@ -0,0 +1,38 @@ +//! Purpose: +//! Declarative eval registry entry for `unset`. +//! +//! Called from: +//! - `crate::interpreter::builtins::symbols`. +//! +//! Key details: +//! - Direct calls stay source-sensitive so writable operands can be removed. + +eval_builtin! { + name: "unset", + area: Symbols, + params: [var], + variadic: vars, + direct: Symbols, + values: Symbols, +} + +use super::super::super::*; + +/// Dispatches direct eval calls for the `unset` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_unset_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_builtin_symbols_call_impl("unset", args, context, scope, values) +} + +/// Dispatches evaluated-argument calls for the `unset` symbol builtin through the area dispatcher. +pub(in crate::interpreter) fn eval_unset_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::dispatch::eval_symbols_values_result_impl("unset", evaluated_args, context, values) +} From 614ed7b6dfc1ffe7a31b0e60a70bedd6b1e73051 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 17:20:40 +0200 Subject: [PATCH 1111/1208] refactor(magician): co-locate array builtin adapters --- .../interpreter/builtins/array/array_chunk.rs | 21 +++ .../builtins/array/array_column.rs | 21 +++ .../builtins/array/array_combine.rs | 21 +++ .../interpreter/builtins/array/array_diff.rs | 21 +++ .../builtins/array/array_diff_key.rs | 21 +++ .../interpreter/builtins/array/array_fill.rs | 21 +++ .../builtins/array/array_fill_keys.rs | 21 +++ .../builtins/array/array_filter.rs | 25 +++ .../interpreter/builtins/array/array_flip.rs | 21 +++ .../builtins/array/array_intersect.rs | 21 +++ .../builtins/array/array_intersect_key.rs | 21 +++ .../builtins/array/array_key_exists.rs | 21 +++ .../interpreter/builtins/array/array_keys.rs | 19 +++ .../interpreter/builtins/array/array_map.rs | 21 +++ .../interpreter/builtins/array/array_merge.rs | 21 +++ .../interpreter/builtins/array/array_pad.rs | 21 +++ .../interpreter/builtins/array/array_pop.rs | 22 +++ .../builtins/array/array_product.rs | 21 +++ .../interpreter/builtins/array/array_push.rs | 12 ++ .../interpreter/builtins/array/array_rand.rs | 21 +++ .../builtins/array/array_reduce.rs | 27 +++ .../builtins/array/array_reverse.rs | 27 +++ .../builtins/array/array_search.rs | 21 +++ .../interpreter/builtins/array/array_shift.rs | 12 ++ .../interpreter/builtins/array/array_slice.rs | 24 +++ .../builtins/array/array_splice.rs | 17 ++ .../interpreter/builtins/array/array_sum.rs | 21 +++ .../builtins/array/array_unique.rs | 21 +++ .../builtins/array/array_unshift.rs | 12 ++ .../builtins/array/array_values.rs | 19 +++ .../interpreter/builtins/array/array_walk.rs | 12 ++ .../src/interpreter/builtins/array/arsort.rs | 12 ++ .../src/interpreter/builtins/array/asort.rs | 12 ++ .../src/interpreter/builtins/array/count.rs | 24 +++ .../builtins/array/direct_dispatch.rs | 54 ++++++ .../interpreter/builtins/array/in_array.rs | 21 +++ .../builtins/array/iterator_apply.rs | 31 ++++ .../builtins/array/iterator_count.rs | 21 +++ .../builtins/array/iterator_to_array.rs | 27 +++ .../src/interpreter/builtins/array/krsort.rs | 12 ++ .../src/interpreter/builtins/array/ksort.rs | 12 ++ .../src/interpreter/builtins/array/mod.rs | 19 +-- .../interpreter/builtins/array/mutating.rs | 85 ---------- .../interpreter/builtins/array/natcasesort.rs | 12 ++ .../src/interpreter/builtins/array/natsort.rs | 12 ++ .../builtins/array/non_mutating.rs | 155 ------------------ .../src/interpreter/builtins/array/range.rs | 21 +++ .../src/interpreter/builtins/array/rsort.rs | 12 ++ .../src/interpreter/builtins/array/shuffle.rs | 12 ++ .../src/interpreter/builtins/array/sort.rs | 12 ++ .../src/interpreter/builtins/array/uasort.rs | 12 ++ .../src/interpreter/builtins/array/uksort.rs | 12 ++ .../src/interpreter/builtins/array/usort.rs | 12 ++ .../builtins/array/values_dispatch.rs | 71 ++++++++ .../src/interpreter/builtins/hooks/direct.rs | 30 ++-- .../src/interpreter/builtins/hooks/values.rs | 57 ++----- 56 files changed, 1082 insertions(+), 305 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/array/mutating.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs index 38c9eed202..cbe8db20f9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_chunk", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_chunk` array builtin. +pub(in crate::interpreter) fn eval_array_chunk_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_chunk(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_chunk` array builtin. +pub(in crate::interpreter) fn eval_array_chunk_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_chunk_result(*array, *length, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs index fe9d23e838..bef1fd9f8d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_column", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_column` array builtin. +pub(in crate::interpreter) fn eval_array_column_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_column(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_column` array builtin. +pub(in crate::interpreter) fn eval_array_column_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, column_key] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_column_result(*array, *column_key, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs index 3b438cd7bf..021ed2bae6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_combine", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_combine` array builtin. +pub(in crate::interpreter) fn eval_array_combine_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_combine(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_combine` array builtin. +pub(in crate::interpreter) fn eval_array_combine_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, values_array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_combine_result(*keys, *values_array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs index 3b0f19148f..6529ac7ce9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_diff", area: Array, @@ -15,3 +17,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_diff` array builtin. +pub(in crate::interpreter) fn eval_array_diff_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_value_set("array_diff", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_diff` array builtin. +pub(in crate::interpreter) fn eval_array_diff_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_value_set_result("array_diff", *left, *right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs index 3101de8269..59fb336f52 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_diff_key", area: Array, @@ -15,3 +17,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_diff_key` array builtin. +pub(in crate::interpreter) fn eval_array_diff_key_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_key_set("array_diff_key", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_diff_key` array builtin. +pub(in crate::interpreter) fn eval_array_diff_key_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_key_set_result("array_diff_key", *left, *right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs index 77f95d8e4b..e58f6750ac 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_fill", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_fill` array builtin. +pub(in crate::interpreter) fn eval_array_fill_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_fill(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_fill` array builtin. +pub(in crate::interpreter) fn eval_array_fill_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, count, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_fill_result(*start, *count, *value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs index e2325a6251..71e0cc9d2b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_fill_keys", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_fill_keys` array builtin. +pub(in crate::interpreter) fn eval_array_fill_keys_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_fill_keys(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_fill_keys` array builtin. +pub(in crate::interpreter) fn eval_array_fill_keys_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_fill_keys_result(*keys, *value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs index 9dd3a536d2..706a0fd6ea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "array_filter", area: Array, @@ -20,3 +22,26 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_filter` array builtin. +pub(in crate::interpreter) fn eval_array_filter_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_filter(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_filter` array builtin. +pub(in crate::interpreter) fn eval_array_filter_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [array] => eval_array_filter_result(*array, None, None, context, values), + [array, callback] => eval_array_filter_result(*array, Some(*callback), None, context, values), + [array, callback, mode] => eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs index 083203bf12..9291deb3ee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the array-flip hook. +use super::super::super::*; + eval_builtin! { name: "array_flip", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: ArrayFlip, values: ArrayFlip, } +/// Dispatches direct eval calls for the `array_flip` array builtin. +pub(in crate::interpreter) fn eval_array_flip_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_flip(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_flip` array builtin. +pub(in crate::interpreter) fn eval_array_flip_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_flip_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs index 698597ffc6..fcf390242b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_intersect", area: Array, @@ -15,3 +17,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_intersect` array builtin. +pub(in crate::interpreter) fn eval_array_intersect_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_value_set("array_intersect", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_intersect` array builtin. +pub(in crate::interpreter) fn eval_array_intersect_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_value_set_result("array_intersect", *left, *right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs index 7dbc1eb7f2..09029a0cf3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_intersect_key", area: Array, @@ -15,3 +17,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_intersect_key` array builtin. +pub(in crate::interpreter) fn eval_array_intersect_key_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_key_set("array_intersect_key", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_intersect_key` array builtin. +pub(in crate::interpreter) fn eval_array_intersect_key_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_key_set_result("array_intersect_key", *left, *right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs index b6141d2c7d..36e85fd7ae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the runtime key-existence hook. +use super::super::super::*; + eval_builtin! { name: "array_key_exists", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: ArrayKeyExists, values: ArrayKeyExists, } +/// Dispatches direct eval calls for the `array_key_exists` array builtin. +pub(in crate::interpreter) fn eval_array_key_exists_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_key_exists(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_key_exists` array builtin. +pub(in crate::interpreter) fn eval_array_key_exists_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [key, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + values.array_key_exists(*key, *array) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs index 065dc38ce7..acfe560383 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs @@ -46,3 +46,22 @@ pub(in crate::interpreter) fn eval_array_keys_result( } Ok(result) } +/// Dispatches direct eval calls for the `array_keys` array builtin. +pub(in crate::interpreter) fn eval_array_keys_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_keys(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_keys` array builtin. +pub(in crate::interpreter) fn eval_array_keys_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_keys_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs index 00b1bb221a..56cb38ce9a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_map", area: Array, @@ -15,3 +17,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_map` array builtin. +pub(in crate::interpreter) fn eval_array_map_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_map(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_map` array builtin. +pub(in crate::interpreter) fn eval_array_map_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, arrays)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_map_result(*callback, arrays, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs index d4b153d646..e17c24711d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "array_merge", area: Array, @@ -15,3 +17,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_merge` array builtin. +pub(in crate::interpreter) fn eval_array_merge_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_merge(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_merge` array builtin. +pub(in crate::interpreter) fn eval_array_merge_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_merge_result(*left, *right, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs index 0324d0c257..2dca5c68c2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the array-pad hook. +use super::super::super::*; + eval_builtin! { name: "array_pad", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: ArrayPad, values: ArrayPad, } +/// Dispatches direct eval calls for the `array_pad` array builtin. +pub(in crate::interpreter) fn eval_array_pad_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_pad(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_pad` array builtin. +pub(in crate::interpreter) fn eval_array_pad_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_pad_result(*array, *length, *value, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs index f9799b36c6..ed855d066e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "array_pop", area: Array, @@ -15,3 +17,23 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `array_pop` array mutator. +pub(in crate::interpreter) fn eval_array_pop_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_warn_array_by_value("array_pop", values)?; + eval_array_pop_shift_value_result("array_pop", *array, values) +} + +/// Emits the standard by-value warning for array mutator callable calls. +pub(in crate::interpreter) fn eval_warn_array_by_value( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + values.warning(&format!( + "{name}(): Argument #1 ($array) must be passed by reference, value given" + )) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs index 4cbeb24576..d4a80cbffc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the array-aggregate hook. +use super::super::super::*; + eval_builtin! { name: "array_product", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: ArrayAggregate, values: ArrayAggregate, } +/// Dispatches direct eval calls for the `array_product` array builtin. +pub(in crate::interpreter) fn eval_array_product_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_aggregate("array_product", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_product` array builtin. +pub(in crate::interpreter) fn eval_array_product_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_aggregate_result("array_product", *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs index 565b89b67b..6edf660fd9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "array_push", area: Array, @@ -16,3 +18,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `array_push` array mutator. +pub(in crate::interpreter) fn eval_array_push_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((array, inserted)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("array_push", values)?; + eval_array_push_unshift_count_result(*array, inserted.len(), values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs index 2b9f2ece6f..01d28dd1a9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the array-rand hook. +use super::super::super::*; + eval_builtin! { name: "array_rand", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: ArrayRand, values: ArrayRand, } +/// Dispatches direct eval calls for the `array_rand` array builtin. +pub(in crate::interpreter) fn eval_array_rand_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_rand(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_rand` array builtin. +pub(in crate::interpreter) fn eval_array_rand_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_rand_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs index e931021fc6..45e0ac89a6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "array_reduce", area: Array, @@ -16,3 +18,28 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `array_reduce` array builtin. +pub(in crate::interpreter) fn eval_array_reduce_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_reduce(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_reduce` array builtin. +pub(in crate::interpreter) fn eval_array_reduce_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [array, callback] => { + let initial = values.null()?; + eval_array_reduce_result(*array, *callback, initial, context, values) + } + [array, callback, initial] => eval_array_reduce_result(*array, *callback, *initial, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs index de522377e6..2e794f004c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "array_reverse", area: Array, @@ -16,3 +18,28 @@ eval_builtin! { direct: ArrayReverse, values: ArrayReverse, } +/// Dispatches direct eval calls for the `array_reverse` array builtin. +pub(in crate::interpreter) fn eval_array_reverse_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_reverse(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_reverse` array builtin. +pub(in crate::interpreter) fn eval_array_reverse_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [array] => eval_array_reverse_result(*array, false, values), + [array, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_array_reverse_result(*array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs index d8fa73dc26..75faff3b63 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "array_search", area: Array, @@ -16,3 +18,22 @@ eval_builtin! { direct: ArraySearch, values: ArraySearch, } +/// Dispatches direct eval calls for the `array_search` array builtin. +pub(in crate::interpreter) fn eval_array_search_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_search("array_search", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_search` array builtin. +pub(in crate::interpreter) fn eval_array_search_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_search_result("array_search", *needle, *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs index 3fdb97b56b..e1acd091a9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "array_shift", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `array_shift` array mutator. +pub(in crate::interpreter) fn eval_array_shift_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("array_shift", values)?; + eval_array_pop_shift_value_result("array_shift", *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs index 326e2fbff0..891e3098fa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "array_slice", area: Array, @@ -16,3 +18,25 @@ eval_builtin! { direct: ArraySlice, values: ArraySlice, } +/// Dispatches direct eval calls for the `array_slice` array builtin. +pub(in crate::interpreter) fn eval_array_slice_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_slice(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_slice` array builtin. +pub(in crate::interpreter) fn eval_array_slice_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [array, offset] => eval_array_slice_result(*array, *offset, None, values), + [array, offset, length] => eval_array_slice_result(*array, *offset, Some(*length), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs index 24439e8a35..8a777f7fcc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "array_splice", area: Array, @@ -22,3 +24,18 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `array_splice` array mutator. +pub(in crate::interpreter) fn eval_array_splice_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = match evaluated_args { + [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, + [array, offset, length] => eval_array_splice_value_result(*array, *offset, Some(*length), values)?, + [array, offset, length, _replacement] => eval_array_splice_value_result(*array, *offset, Some(*length), values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.warning("array_splice(): Argument #1 ($array) must be passed by reference, value given")?; + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs index 86cb403b75..820780c8d3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the array-aggregate hook. +use super::super::super::*; + eval_builtin! { name: "array_sum", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: ArrayAggregate, values: ArrayAggregate, } +/// Dispatches direct eval calls for the `array_sum` array builtin. +pub(in crate::interpreter) fn eval_array_sum_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_aggregate("array_sum", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_sum` array builtin. +pub(in crate::interpreter) fn eval_array_sum_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_aggregate_result("array_sum", *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs index d40d81f146..879425c893 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the array-unique hook. +use super::super::super::*; + eval_builtin! { name: "array_unique", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: ArrayUnique, values: ArrayUnique, } +/// Dispatches direct eval calls for the `array_unique` array builtin. +pub(in crate::interpreter) fn eval_array_unique_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_unique(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_unique` array builtin. +pub(in crate::interpreter) fn eval_array_unique_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_unique_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs index 27ac824bc6..cc15fff2e2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "array_unshift", area: Array, @@ -16,3 +18,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `array_unshift` array mutator. +pub(in crate::interpreter) fn eval_array_unshift_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((array, inserted)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("array_unshift", values)?; + eval_array_push_unshift_count_result(*array, inserted.len(), values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs index a943b426ba..34f40324ce 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs @@ -47,3 +47,22 @@ pub(in crate::interpreter) fn eval_array_values_result( } Ok(result) } +/// Dispatches direct eval calls for the `array_values` array builtin. +pub(in crate::interpreter) fn eval_array_values_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_values(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `array_values` array builtin. +pub(in crate::interpreter) fn eval_array_values_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_values_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs index f54859cdea..2f57b1c56c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "array_walk", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `array_walk` array mutator. +pub(in crate::interpreter) fn eval_array_walk_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + values.warning("array_walk(): Argument #1 ($array) must be passed by reference, value given")?; + eval_array_walk_result(*array, *callback, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs index 22d6c09899..b6d7d9effb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "arsort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `arsort` array mutator. +pub(in crate::interpreter) fn eval_arsort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("arsort", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/asort.rs b/crates/elephc-magician/src/interpreter/builtins/array/asort.rs index dd4f794337..ecd5c17e53 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/asort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/asort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "asort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `asort` array mutator. +pub(in crate::interpreter) fn eval_asort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("asort", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/count.rs b/crates/elephc-magician/src/interpreter/builtins/array/count.rs index 053b825ca6..2376e4e483 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/count.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/count.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "count", area: Array, @@ -16,3 +18,25 @@ eval_builtin! { direct: Count, values: Count, } +/// Dispatches direct eval calls for the `count` array builtin. +pub(in crate::interpreter) fn eval_count_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_count(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `count` array builtin. +pub(in crate::interpreter) fn eval_count_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_count_result(*value, None, context, values), + [value, mode] => eval_count_result(*value, Some(*mode), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs new file mode 100644 index 0000000000..0ecfdd1af4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/direct_dispatch.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Area-level direct dispatch for array builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalDirectHook::call()`. +//! +//! Key details: +//! - Dispatch stays thin and routes every builtin through its leaf adapter. + +use super::super::super::*; + +/// Routes direct expression-level array builtin calls through per-builtin leaf adapters. +pub(in crate::interpreter) fn eval_builtin_array_declared_call( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "array_sum" => super::array_sum::eval_array_sum_declared_call(args, context, scope, values), + "array_product" => super::array_product::eval_array_product_declared_call(args, context, scope, values), + "array_chunk" => super::array_chunk::eval_array_chunk_declared_call(args, context, scope, values), + "array_column" => super::array_column::eval_array_column_declared_call(args, context, scope, values), + "array_combine" => super::array_combine::eval_array_combine_declared_call(args, context, scope, values), + "array_diff" => super::array_diff::eval_array_diff_declared_call(args, context, scope, values), + "array_diff_key" => super::array_diff_key::eval_array_diff_key_declared_call(args, context, scope, values), + "array_fill" => super::array_fill::eval_array_fill_declared_call(args, context, scope, values), + "array_fill_keys" => super::array_fill_keys::eval_array_fill_keys_declared_call(args, context, scope, values), + "array_filter" => super::array_filter::eval_array_filter_declared_call(args, context, scope, values), + "array_intersect" => super::array_intersect::eval_array_intersect_declared_call(args, context, scope, values), + "array_intersect_key" => super::array_intersect_key::eval_array_intersect_key_declared_call(args, context, scope, values), + "array_map" => super::array_map::eval_array_map_declared_call(args, context, scope, values), + "array_merge" => super::array_merge::eval_array_merge_declared_call(args, context, scope, values), + "array_reduce" => super::array_reduce::eval_array_reduce_declared_call(args, context, scope, values), + "iterator_apply" => super::iterator_apply::eval_iterator_apply_declared_call(args, context, scope, values), + "iterator_count" => super::iterator_count::eval_iterator_count_declared_call(args, context, scope, values), + "iterator_to_array" => super::iterator_to_array::eval_iterator_to_array_declared_call(args, context, scope, values), + "array_flip" => super::array_flip::eval_array_flip_declared_call(args, context, scope, values), + "array_key_exists" => super::array_key_exists::eval_array_key_exists_declared_call(args, context, scope, values), + "array_pad" => super::array_pad::eval_array_pad_declared_call(args, context, scope, values), + "array_keys" => super::array_keys::eval_array_keys_declared_call(args, context, scope, values), + "array_rand" => super::array_rand::eval_array_rand_declared_call(args, context, scope, values), + "array_reverse" => super::array_reverse::eval_array_reverse_declared_call(args, context, scope, values), + "array_search" => super::array_search::eval_array_search_declared_call(args, context, scope, values), + "in_array" => super::in_array::eval_in_array_declared_call(args, context, scope, values), + "array_slice" => super::array_slice::eval_array_slice_declared_call(args, context, scope, values), + "array_unique" => super::array_unique::eval_array_unique_declared_call(args, context, scope, values), + "array_values" => super::array_values::eval_array_values_declared_call(args, context, scope, values), + "count" => super::count::eval_count_declared_call(args, context, scope, values), + "range" => super::range::eval_range_declared_call(args, context, scope, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs b/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs index c51b7ad8d6..684ec55c63 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "in_array", area: Array, @@ -16,3 +18,22 @@ eval_builtin! { direct: ArraySearch, values: ArraySearch, } +/// Dispatches direct eval calls for the `in_array` array builtin. +pub(in crate::interpreter) fn eval_in_array_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_array_search("in_array", args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `in_array` array builtin. +pub(in crate::interpreter) fn eval_in_array_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_array_search_result("in_array", *needle, *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs index d5a98fd713..e9dab17f7d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "iterator_apply", area: Array, @@ -16,3 +18,32 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `iterator_apply` array builtin. +pub(in crate::interpreter) fn eval_iterator_apply_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_iterator_apply(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `iterator_apply` array builtin. +pub(in crate::interpreter) fn eval_iterator_apply_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [iterator, callback] => { + let callback = eval_callable(*callback, context, values)?; + eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, args] => { + let callback = eval_callable(*callback, context, values)?; + let callback_args = eval_iterator_apply_arg_values(*args, context, values)?; + eval_iterator_apply_result(*iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs index 2646d010ca..1782e2bb93 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the non-mutating array hook. +use super::super::super::*; + eval_builtin! { name: "iterator_count", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `iterator_count` array builtin. +pub(in crate::interpreter) fn eval_iterator_count_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_iterator_count(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `iterator_count` array builtin. +pub(in crate::interpreter) fn eval_iterator_count_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [iterator] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_iterator_count_result(*iterator, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs index 3c9a0a3f79..e56c3af469 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs @@ -9,6 +9,8 @@ use super::super::spec::EvalBuiltinDefaultValue; +use super::super::super::*; + eval_builtin! { name: "iterator_to_array", area: Array, @@ -16,3 +18,28 @@ eval_builtin! { direct: Array, values: Array, } +/// Dispatches direct eval calls for the `iterator_to_array` array builtin. +pub(in crate::interpreter) fn eval_iterator_to_array_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_iterator_to_array(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `iterator_to_array` array builtin. +pub(in crate::interpreter) fn eval_iterator_to_array_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [iterator] => eval_iterator_to_array_result(*iterator, true, values), + [iterator, preserve_keys] => { + let preserve_keys = values.truthy(*preserve_keys)?; + eval_iterator_to_array_result(*iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs index c003a5c309..2c381bc8cd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "krsort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `krsort` array mutator. +pub(in crate::interpreter) fn eval_krsort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("krsort", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs b/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs index 2fc106f2ff..1757f34f08 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "ksort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `ksort` array mutator. +pub(in crate::interpreter) fn eval_ksort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("ksort", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs index 3e177170f9..a6ed73817d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -1,12 +1,12 @@ //! Purpose: -//! Per-builtin declarations for array and collection functions migrated to the -//! eval builtin registry. +//! Per-builtin declarations and eval adapters for array and collection functions. //! //! Called from: //! - `crate::interpreter::builtins` module loading. //! //! Key details: -//! - Leaf files register metadata through `eval_builtin!`. +//! - Leaf files register metadata through `eval_builtin!` and own the concrete +//! direct or evaluated-argument adapter used by registry hooks. mod array_chunk; mod array_column; @@ -23,8 +23,8 @@ mod array_key_exists; mod array_keys; mod array_map; mod array_merge; -mod array_pop; mod array_pad; +mod array_pop; mod array_product; mod array_push; mod array_rand; @@ -42,16 +42,15 @@ mod array_walk; mod arsort; mod asort; mod count; +mod direct_dispatch; mod in_array; mod iterator_apply; mod iterator_count; mod iterator_to_array; mod krsort; mod ksort; -mod mutating; mod natcasesort; mod natsort; -mod non_mutating; mod range; mod rsort; mod shuffle; @@ -59,8 +58,8 @@ mod sort; mod uasort; mod uksort; mod usort; +mod values_dispatch; -pub(in crate::interpreter) use array_keys::*; -pub(in crate::interpreter) use array_values::*; -pub(in crate::interpreter) use mutating::*; -pub(in crate::interpreter) use non_mutating::*; +pub(in crate::interpreter) use direct_dispatch::eval_builtin_array_declared_call; +pub(in crate::interpreter) use array_values::eval_array_values_result; +pub(in crate::interpreter) use values_dispatch::eval_array_declared_values_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mutating.rs b/crates/elephc-magician/src/interpreter/builtins/array/mutating.rs deleted file mode 100644 index 587c28c8d0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/array/mutating.rs +++ /dev/null @@ -1,85 +0,0 @@ -//! Purpose: -//! Values-only registry hook for mutating/by-reference array builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks`. -//! -//! Key details: -//! - Direct calls stay on the source-sensitive writable-target path; this hook -//! preserves callable by-value warning behavior. - -use super::super::super::*; -use super::super::*; - -/// Dispatches by-value callable calls for mutating array builtins. -pub(in crate::interpreter) fn eval_array_mutating_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "array_walk" => { - let [array, callback] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - values.warning( - "array_walk(): Argument #1 ($array) must be passed by reference, value given", - )?; - eval_array_walk_result(*array, *callback, context, values) - } - "array_pop" | "array_shift" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - warn_array_by_value(name, values)?; - eval_array_pop_shift_value_result(name, *array, values) - } - "array_push" | "array_unshift" => { - let Some((array, inserted)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - warn_array_by_value(name, values)?; - eval_array_push_unshift_count_result(*array, inserted.len(), values) - } - "array_splice" => { - let result = match evaluated_args { - [array, offset] => eval_array_splice_value_result(*array, *offset, None, values)?, - [array, offset, length] => { - eval_array_splice_value_result(*array, *offset, Some(*length), values)? - } - [array, offset, length, _replacement] => { - eval_array_splice_value_result(*array, *offset, Some(*length), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.warning( - "array_splice(): Argument #1 ($array) must be passed by reference, value given", - )?; - Ok(result) - } - "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" - | "shuffle" | "sort" => { - let [array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - warn_array_by_value(name, values)?; - eval_array_sort_value_result(*array, values) - } - "uasort" | "uksort" | "usort" => { - let [array, callback] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - warn_array_by_value(name, values)?; - eval_user_sort_value_result(name, *array, *callback, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Emits the standard by-value warning for array mutators. -fn warn_array_by_value(name: &str, values: &mut impl RuntimeValueOps) -> Result<(), EvalStatus> { - values.warning(&format!( - "{name}(): Argument #1 ($array) must be passed by reference, value given" - )) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs b/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs index 1d54e3a652..f88ef3d076 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "natcasesort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `natcasesort` array mutator. +pub(in crate::interpreter) fn eval_natcasesort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("natcasesort", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs index 9997870ab2..e16002366e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "natsort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `natsort` array mutator. +pub(in crate::interpreter) fn eval_natsort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("natsort", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs b/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs deleted file mode 100644 index a0a571effd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/array/non_mutating.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Purpose: -//! Shared registry hooks for non-mutating array and iterator builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks`. -//! -//! Key details: -//! - Mutating/by-reference array builtins stay on the source-sensitive legacy -//! dispatch path until their writable-target handling is migrated. - -use super::super::super::*; -use super::super::*; - -/// Dispatches direct non-mutating array and iterator calls from declarative specs. -pub(in crate::interpreter) fn eval_builtin_array_call( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "array_chunk" => eval_builtin_array_chunk(args, context, scope, values), - "array_column" => eval_builtin_array_column(args, context, scope, values), - "array_combine" => eval_builtin_array_combine(args, context, scope, values), - "array_diff" | "array_intersect" => { - eval_builtin_array_value_set(name, args, context, scope, values) - } - "array_diff_key" | "array_intersect_key" => { - eval_builtin_array_key_set(name, args, context, scope, values) - } - "array_fill" => eval_builtin_array_fill(args, context, scope, values), - "array_fill_keys" => eval_builtin_array_fill_keys(args, context, scope, values), - "array_filter" => eval_builtin_array_filter(args, context, scope, values), - "array_map" => eval_builtin_array_map(args, context, scope, values), - "array_merge" => eval_builtin_array_merge(args, context, scope, values), - "array_reduce" => eval_builtin_array_reduce(args, context, scope, values), - "iterator_apply" => eval_builtin_iterator_apply(args, context, scope, values), - "iterator_count" => eval_builtin_iterator_count(args, context, scope, values), - "iterator_to_array" => eval_builtin_iterator_to_array(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated non-mutating array and iterator calls from declarative specs. -pub(in crate::interpreter) fn eval_array_non_mutating_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "array_chunk" => { - let [array, length] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_chunk_result(*array, *length, values) - } - "array_column" => { - let [array, column_key] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_column_result(*array, *column_key, values) - } - "array_combine" => { - let [keys, values_array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_combine_result(*keys, *values_array, values) - } - "array_diff" | "array_intersect" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_value_set_result(name, *left, *right, values) - } - "array_diff_key" | "array_intersect_key" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_key_set_result(name, *left, *right, values) - } - "array_fill" => { - let [start, count, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_result(*start, *count, *value, values) - } - "array_fill_keys" => { - let [keys, value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_fill_keys_result(*keys, *value, values) - } - "array_filter" => match evaluated_args { - [array] => eval_array_filter_result(*array, None, None, context, values), - [array, callback] => { - eval_array_filter_result(*array, Some(*callback), None, context, values) - } - [array, callback, mode] => { - eval_array_filter_result(*array, Some(*callback), Some(*mode), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "array_map" => { - let Some((callback, arrays)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_map_result(*callback, arrays, context, values) - } - "array_merge" => { - let [left, right] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_merge_result(*left, *right, values) - } - "array_reduce" => match evaluated_args { - [array, callback] => { - let initial = values.null()?; - eval_array_reduce_result(*array, *callback, initial, context, values) - } - [array, callback, initial] => { - eval_array_reduce_result(*array, *callback, *initial, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "iterator_apply" => match evaluated_args { - [iterator, callback] => { - let callback = eval_callable(*callback, context, values)?; - eval_iterator_apply_result(*iterator, &callback, Vec::new(), context, values) - } - [iterator, callback, args] => { - let callback = eval_callable(*callback, context, values)?; - let callback_args = eval_iterator_apply_arg_values(*args, context, values)?; - eval_iterator_apply_result(*iterator, &callback, callback_args, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "iterator_count" => { - let [iterator] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_iterator_count_result(*iterator, values) - } - "iterator_to_array" => match evaluated_args { - [iterator] => eval_iterator_to_array_result(*iterator, true, values), - [iterator, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_iterator_to_array_result(*iterator, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/range.rs b/crates/elephc-magician/src/interpreter/builtins/array/range.rs index d532fc4691..dd73fb154e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/range.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/range.rs @@ -7,6 +7,8 @@ //! Key details: //! - Runtime behavior stays delegated to the integer range hook. +use super::super::super::*; + eval_builtin! { name: "range", area: Array, @@ -14,3 +16,22 @@ eval_builtin! { direct: Range, values: Range, } +/// Dispatches direct eval calls for the `range` array builtin. +pub(in crate::interpreter) fn eval_range_declared_call( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_builtin_range(args, context, scope, values) +} + +/// Dispatches evaluated-argument eval calls for the `range` array builtin. +pub(in crate::interpreter) fn eval_range_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, end] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + eval_range_result(*start, *end, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs index 569279bddb..92437858a1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "rsort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `rsort` array mutator. +pub(in crate::interpreter) fn eval_rsort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("rsort", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs b/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs index b39971759f..adb1549bb9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "shuffle", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `shuffle` array mutator. +pub(in crate::interpreter) fn eval_shuffle_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("shuffle", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/sort.rs b/crates/elephc-magician/src/interpreter/builtins/array/sort.rs index c2728bda99..afadd13fd9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/sort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/sort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "sort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `sort` array mutator. +pub(in crate::interpreter) fn eval_sort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("sort", values)?; + eval_array_sort_value_result(*array, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs b/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs index 0349dc7025..fde130a187 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "uasort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `uasort` array mutator. +pub(in crate::interpreter) fn eval_uasort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("uasort", values)?; + eval_user_sort_value_result("uasort", *array, *callback, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs b/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs index ecdca25c50..ddab3f019d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "uksort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `uksort` array mutator. +pub(in crate::interpreter) fn eval_uksort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("uksort", values)?; + eval_user_sort_value_result("uksort", *array, *callback, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/usort.rs b/crates/elephc-magician/src/interpreter/builtins/array/usort.rs index 2bfb80bd80..51a8e02480 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/usort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/usort.rs @@ -7,6 +7,8 @@ //! Key details: //! - Direct calls stay on the source-sensitive by-reference path. +use super::super::super::*; + eval_builtin! { name: "usort", area: Array, @@ -15,3 +17,13 @@ eval_builtin! { direct: none, values: ArrayMutating, } +/// Dispatches by-value callable eval calls for the `usort` array mutator. +pub(in crate::interpreter) fn eval_usort_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; + super::array_pop::eval_warn_array_by_value("usort", values)?; + eval_user_sort_value_result("usort", *array, *callback, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs new file mode 100644 index 0000000000..3b7ab6a749 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/values_dispatch.rs @@ -0,0 +1,71 @@ +//! Purpose: +//! Area-level evaluated-argument dispatch for array builtins declared in the eval registry. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks::EvalValuesHook::call()`. +//! +//! Key details: +//! - Dispatch stays thin and routes every builtin through its leaf adapter. + +use super::super::super::*; + +/// Routes evaluated-argument array builtin calls through per-builtin leaf adapters. +pub(in crate::interpreter) fn eval_array_declared_values_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "array_sum" => super::array_sum::eval_array_sum_declared_values_result(evaluated_args, context, values), + "array_product" => super::array_product::eval_array_product_declared_values_result(evaluated_args, context, values), + "array_chunk" => super::array_chunk::eval_array_chunk_declared_values_result(evaluated_args, context, values), + "array_column" => super::array_column::eval_array_column_declared_values_result(evaluated_args, context, values), + "array_combine" => super::array_combine::eval_array_combine_declared_values_result(evaluated_args, context, values), + "array_diff" => super::array_diff::eval_array_diff_declared_values_result(evaluated_args, context, values), + "array_diff_key" => super::array_diff_key::eval_array_diff_key_declared_values_result(evaluated_args, context, values), + "array_fill" => super::array_fill::eval_array_fill_declared_values_result(evaluated_args, context, values), + "array_fill_keys" => super::array_fill_keys::eval_array_fill_keys_declared_values_result(evaluated_args, context, values), + "array_filter" => super::array_filter::eval_array_filter_declared_values_result(evaluated_args, context, values), + "array_intersect" => super::array_intersect::eval_array_intersect_declared_values_result(evaluated_args, context, values), + "array_intersect_key" => super::array_intersect_key::eval_array_intersect_key_declared_values_result(evaluated_args, context, values), + "array_map" => super::array_map::eval_array_map_declared_values_result(evaluated_args, context, values), + "array_merge" => super::array_merge::eval_array_merge_declared_values_result(evaluated_args, context, values), + "array_reduce" => super::array_reduce::eval_array_reduce_declared_values_result(evaluated_args, context, values), + "iterator_apply" => super::iterator_apply::eval_iterator_apply_declared_values_result(evaluated_args, context, values), + "iterator_count" => super::iterator_count::eval_iterator_count_declared_values_result(evaluated_args, context, values), + "iterator_to_array" => super::iterator_to_array::eval_iterator_to_array_declared_values_result(evaluated_args, context, values), + "array_flip" => super::array_flip::eval_array_flip_declared_values_result(evaluated_args, context, values), + "array_key_exists" => super::array_key_exists::eval_array_key_exists_declared_values_result(evaluated_args, context, values), + "array_pad" => super::array_pad::eval_array_pad_declared_values_result(evaluated_args, context, values), + "array_keys" => super::array_keys::eval_array_keys_declared_values_result(evaluated_args, context, values), + "array_rand" => super::array_rand::eval_array_rand_declared_values_result(evaluated_args, context, values), + "array_reverse" => super::array_reverse::eval_array_reverse_declared_values_result(evaluated_args, context, values), + "array_search" => super::array_search::eval_array_search_declared_values_result(evaluated_args, context, values), + "in_array" => super::in_array::eval_in_array_declared_values_result(evaluated_args, context, values), + "array_slice" => super::array_slice::eval_array_slice_declared_values_result(evaluated_args, context, values), + "array_unique" => super::array_unique::eval_array_unique_declared_values_result(evaluated_args, context, values), + "array_values" => super::array_values::eval_array_values_declared_values_result(evaluated_args, context, values), + "count" => super::count::eval_count_declared_values_result(evaluated_args, context, values), + "range" => super::range::eval_range_declared_values_result(evaluated_args, context, values), + "array_walk" => super::array_walk::eval_array_walk_declared_values_result(evaluated_args, context, values), + "array_pop" => super::array_pop::eval_array_pop_declared_values_result(evaluated_args, context, values), + "array_shift" => super::array_shift::eval_array_shift_declared_values_result(evaluated_args, context, values), + "array_push" => super::array_push::eval_array_push_declared_values_result(evaluated_args, context, values), + "array_unshift" => super::array_unshift::eval_array_unshift_declared_values_result(evaluated_args, context, values), + "array_splice" => super::array_splice::eval_array_splice_declared_values_result(evaluated_args, context, values), + "arsort" => super::arsort::eval_arsort_declared_values_result(evaluated_args, context, values), + "asort" => super::asort::eval_asort_declared_values_result(evaluated_args, context, values), + "krsort" => super::krsort::eval_krsort_declared_values_result(evaluated_args, context, values), + "ksort" => super::ksort::eval_ksort_declared_values_result(evaluated_args, context, values), + "natcasesort" => super::natcasesort::eval_natcasesort_declared_values_result(evaluated_args, context, values), + "natsort" => super::natsort::eval_natsort_declared_values_result(evaluated_args, context, values), + "rsort" => super::rsort::eval_rsort_declared_values_result(evaluated_args, context, values), + "shuffle" => super::shuffle::eval_shuffle_declared_values_result(evaluated_args, context, values), + "sort" => super::sort::eval_sort_declared_values_result(evaluated_args, context, values), + "uasort" => super::uasort::eval_uasort_declared_values_result(evaluated_args, context, values), + "uksort" => super::uksort::eval_uksort_declared_values_result(evaluated_args, context, values), + "usort" => super::usort::eval_usort_declared_values_result(evaluated_args, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 02ccc72340..6e2af92644 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -11,7 +11,7 @@ use super::super::*; use super::super::super::{ - eval_builtin_count, ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, + ElephcEvalContext, ElephcEvalScope, EvalExpr, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; @@ -327,18 +327,20 @@ impl EvalDirectHook { match self { Self::Abs => eval_builtin_abs(args, context, scope, values), Self::Acos => eval_builtin_acos(args, context, scope, values), - Self::ArrayAggregate => eval_builtin_array_aggregate(name, args, context, scope, values), - Self::Array => eval_builtin_array_call(name, args, context, scope, values), - Self::ArrayFlip => eval_builtin_array_flip(args, context, scope, values), - Self::ArrayKeyExists => eval_builtin_array_key_exists(args, context, scope, values), - Self::ArrayPad => eval_builtin_array_pad(args, context, scope, values), - Self::ArrayKeys => eval_builtin_array_keys(args, context, scope, values), - Self::ArrayRand => eval_builtin_array_rand(args, context, scope, values), - Self::ArrayReverse => eval_builtin_array_reverse(args, context, scope, values), - Self::ArraySearch => eval_builtin_array_search(name, args, context, scope, values), - Self::ArraySlice => eval_builtin_array_slice(args, context, scope, values), - Self::ArrayUnique => eval_builtin_array_unique(args, context, scope, values), - Self::ArrayValues => eval_builtin_array_values(args, context, scope, values), + Self::ArrayAggregate + | Self::Array + | Self::ArrayFlip + | Self::ArrayKeyExists + | Self::ArrayPad + | Self::ArrayKeys + | Self::ArrayRand + | Self::ArrayReverse + | Self::ArraySearch + | Self::ArraySlice + | Self::ArrayUnique + | Self::ArrayValues + | Self::Count + | Self::Range => eval_builtin_array_declared_call(name, args, context, scope, values), Self::Asin => eval_builtin_asin(args, context, scope, values), Self::Atan => eval_builtin_atan(args, context, scope, values), Self::Atan2 => eval_builtin_atan2(args, context, scope, values), @@ -349,7 +351,6 @@ impl EvalDirectHook { Self::Ceil => eval_builtin_ceil(args, context, scope, values), Self::Chr => eval_builtin_chr(args, context, scope, values), Self::Clamp => eval_builtin_clamp(args, context, scope, values), - Self::Count => eval_builtin_count(args, context, scope, values), Self::Core => eval_builtin_core_call(name, args, context, scope, values), Self::Cos => eval_builtin_cos(args, context, scope, values), Self::Cosh => eval_builtin_cosh(args, context, scope, values), @@ -445,7 +446,6 @@ impl EvalDirectHook { Self::Rand => eval_builtin_rand(args, context, scope, values), Self::RandomInt => eval_builtin_random_int(args, context, scope, values), Self::Round => eval_builtin_round(args, context, scope, values), - Self::Range => eval_builtin_range(args, context, scope, values), Self::PregMatch => eval_builtin_preg_match(args, context, scope, values), Self::PregMatchAll => eval_builtin_preg_match_all(args, context, scope, values), Self::PregReplace => eval_builtin_preg_replace(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 4524e382ea..341a40befc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -12,7 +12,7 @@ use super::super::*; use super::super::super::{ - eval_count_result, ElephcEvalContext, EvalStatus, RuntimeCellHandle, RuntimeValueOps, + ElephcEvalContext, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::arity::{one_arg, three_args, two_args}; use super::hash::{eval_hash_algos_values, eval_hash_context_values}; @@ -334,40 +334,23 @@ impl EvalValuesHook { match self { Self::Abs => one_arg(evaluated_args, values, eval_abs_result), Self::Acos => one_arg(evaluated_args, values, eval_acos_result), - Self::ArrayAggregate => one_arg(evaluated_args, values, |array, values| { - eval_array_aggregate_result(name, array, values) - }), - Self::Array => eval_array_non_mutating_values_result(name, evaluated_args, context, values), - Self::ArrayMutating => { - eval_array_mutating_values_result(name, evaluated_args, context, values) + Self::ArrayAggregate + | Self::Array + | Self::ArrayMutating + | Self::ArrayFlip + | Self::ArrayKeyExists + | Self::ArrayPad + | Self::ArrayKeys + | Self::ArrayRand + | Self::ArrayReverse + | Self::ArraySearch + | Self::ArraySlice + | Self::ArrayUnique + | Self::ArrayValues + | Self::Count + | Self::Range => { + eval_array_declared_values_result(name, evaluated_args, context, values) } - Self::ArrayFlip => one_arg(evaluated_args, values, eval_array_flip_result), - Self::ArrayKeyExists => two_args(evaluated_args, values, |key, array, values| { - values.array_key_exists(key, array) - }), - Self::ArrayPad => three_args(evaluated_args, values, eval_array_pad_result), - Self::ArrayKeys => one_arg(evaluated_args, values, eval_array_keys_result), - Self::ArrayRand => one_arg(evaluated_args, values, eval_array_rand_result), - Self::ArrayReverse => match evaluated_args { - [array] => eval_array_reverse_result(*array, false, values), - [array, preserve_keys] => { - let preserve_keys = values.truthy(*preserve_keys)?; - eval_array_reverse_result(*array, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - Self::ArraySearch => two_args(evaluated_args, values, |needle, array, values| { - eval_array_search_result(name, needle, array, values) - }), - Self::ArraySlice => match evaluated_args { - [array, offset] => eval_array_slice_result(*array, *offset, None, values), - [array, offset, length] => { - eval_array_slice_result(*array, *offset, Some(*length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - Self::ArrayUnique => one_arg(evaluated_args, values, eval_array_unique_result), - Self::ArrayValues => one_arg(evaluated_args, values, eval_array_values_result), Self::Asin => one_arg(evaluated_args, values, eval_asin_result), Self::Atan => one_arg(evaluated_args, values, eval_atan_result), Self::Atan2 => two_args(evaluated_args, values, eval_atan2_result), @@ -378,11 +361,6 @@ impl EvalValuesHook { Self::Ceil => one_arg(evaluated_args, values, eval_ceil_result), Self::Chr => one_arg(evaluated_args, values, eval_chr_result), Self::Clamp => three_args(evaluated_args, values, eval_clamp_result), - Self::Count => match evaluated_args { - [value] => eval_count_result(*value, None, context, values), - [value, mode] => eval_count_result(*value, Some(*mode), context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, Self::Core => eval_core_values_result(name, evaluated_args, context, values), Self::Cos => one_arg(evaluated_args, values, eval_cos_result), Self::Cosh => one_arg(evaluated_args, values, eval_cosh_result), @@ -495,7 +473,6 @@ impl EvalValuesHook { [value, precision] => eval_round_result(*value, Some(*precision), values), _ => Err(EvalStatus::RuntimeFatal), }, - Self::Range => two_args(evaluated_args, values, eval_range_result), Self::PregMatch => eval_preg_match_values_result(evaluated_args, values), Self::PregMatchAll => eval_preg_match_all_values_result(evaluated_args, values), Self::PregReplace => eval_preg_replace_values_result(evaluated_args, values), From ec207fda917c9c50967c816d835d9b4dc7bb3e18 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:15:07 +0200 Subject: [PATCH 1112/1208] refactor(magician): move array builtin implementations home --- .../interpreter/builtins/array/array_chunk.rs | 50 +++ .../builtins/array/array_column.rs | 44 +++ .../builtins/array/array_combine.rs | 38 ++ .../interpreter/builtins/array/array_diff.rs | 52 +++ .../builtins/array/array_diff_key.rs | 42 ++ .../interpreter/builtins/array/array_fill.rs | 43 +++ .../builtins/array/array_fill_keys.rs | 31 ++ .../builtins/array/array_filter.rs | 124 ++++++ .../interpreter/builtins/array/array_flip.rs | 32 ++ .../builtins/array/array_intersect.rs | 4 +- .../builtins/array/array_intersect_key.rs | 4 +- .../builtins/array/array_key_exists.rs | 15 + .../interpreter/builtins/array/array_map.rs | 135 +++++++ .../interpreter/builtins/array/array_merge.rs | 57 +++ .../interpreter/builtins/array/array_pad.rs | 75 ++++ .../interpreter/builtins/array/array_pop.rs | 153 ++++++++ .../builtins/array/array_product.rs | 4 +- .../interpreter/builtins/array/array_push.rs | 188 +++++++++ .../interpreter/builtins/array/array_rand.rs | 32 ++ .../builtins/array/array_reduce.rs | 56 +++ .../builtins/array/array_reverse.rs | 58 +++ .../builtins/array/array_search.rs | 43 +++ .../interpreter/builtins/array/array_shift.rs | 2 +- .../interpreter/builtins/array/array_slice.rs | 87 +++++ .../builtins/array/array_splice.rs | 314 +++++++++++++++ .../interpreter/builtins/array/array_sum.rs | 39 ++ .../builtins/array/array_unique.rs | 35 ++ .../builtins/array/array_unshift.rs | 2 +- .../interpreter/builtins/array/array_walk.rs | 141 +++++++ .../src/interpreter/builtins/array/arsort.rs | 2 +- .../src/interpreter/builtins/array/asort.rs | 2 +- .../interpreter/builtins/array/in_array.rs | 4 +- .../builtins/array/iterator_apply.rs | 104 +++++ .../builtins/array/iterator_count.rs | 26 ++ .../builtins/array/iterator_to_array.rs | 52 +++ .../src/interpreter/builtins/array/krsort.rs | 2 +- .../src/interpreter/builtins/array/ksort.rs | 2 +- .../src/interpreter/builtins/array/mod.rs | 12 +- .../builtins/array/mutating_dispatch.rs | 40 ++ .../interpreter/builtins/array/mutation.rs | 29 ++ .../interpreter/builtins/array/natcasesort.rs | 2 +- .../src/interpreter/builtins/array/natsort.rs | 2 +- .../src/interpreter/builtins/array/range.rs | 46 +++ .../src/interpreter/builtins/array/rsort.rs | 2 +- .../src/interpreter/builtins/array/shuffle.rs | 2 +- .../src/interpreter/builtins/array/sort.rs | 361 ++++++++++++++++++ .../src/interpreter/builtins/array/uasort.rs | 2 +- .../src/interpreter/builtins/array/uksort.rs | 2 +- .../src/interpreter/builtins/array/usort.rs | 215 +++++++++++ .../src/interpreter/builtins/arrays/access.rs | 315 --------------- .../interpreter/builtins/arrays/callbacks.rs | 347 ----------------- .../src/interpreter/builtins/arrays/core.rs | 207 ---------- .../builtins/arrays/filters/chunk.rs | 61 --- .../builtins/arrays/filters/filter.rs | 136 ------- .../builtins/arrays/filters/flip.rs | 42 -- .../builtins/arrays/filters/iterator.rs | 194 ---------- .../builtins/arrays/filters/mod.rs | 28 -- .../builtins/arrays/filters/pad.rs | 86 ----- .../builtins/arrays/filters/reverse.rs | 68 ---- .../builtins/arrays/filters/slice.rs | 98 ----- .../builtins/arrays/filters/unique.rs | 45 --- .../src/interpreter/builtins/arrays/mod.rs | 29 -- .../interpreter/builtins/arrays/mutation.rs | 101 ----- .../interpreter/builtins/arrays/push_pop.rs | 312 --------------- .../builtins/arrays/sort/direct.rs | 122 ------ .../interpreter/builtins/arrays/sort/mod.rs | 18 - .../builtins/arrays/sort/standard.rs | 345 ----------------- .../interpreter/builtins/arrays/sort/user.rs | 148 ------- .../src/interpreter/builtins/arrays/splice.rs | 327 ---------------- .../src/interpreter/builtins/mod.rs | 4 +- .../src/interpreter/builtins/random.rs | 28 ++ .../src/interpreter/expressions/calls.rs | 2 +- 72 files changed, 2819 insertions(+), 3053 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/array/mutation.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/access.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/callbacks.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/core.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/chunk.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/flip.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/pad.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/reverse.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/slice.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/filters/unique.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/push_pop.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/sort/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/sort/standard.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/random.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs index cbe8db20f9..88c41d405c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs @@ -35,3 +35,53 @@ pub(in crate::interpreter) fn eval_array_chunk_declared_values_result( let [array, length] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_chunk_result(*array, *length, values) } + +/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. +pub(in crate::interpreter) fn eval_builtin_array_chunk( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_chunk_result(array, length, values) +} + +/// Builds an `array_chunk()` result as nested reindexed arrays. +pub(in crate::interpreter) fn eval_array_chunk_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let chunk_size = eval_int_value(length, values)?; + if chunk_size <= 0 { + return Err(EvalStatus::RuntimeFatal); + } + let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; + let len = values.array_len(array)?; + let chunk_count = len.div_ceil(chunk_size); + let mut result = values.array_new(chunk_count)?; + + for chunk_index in 0..chunk_count { + let start = chunk_index * chunk_size; + let end = usize::min(start + chunk_size, len); + let mut chunk = values.array_new(end - start)?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let value = values.array_get(array, source_key)?; + let target_index = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_index = values.int(target_index)?; + chunk = values.array_set(chunk, target_index, value)?; + } + let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let result_key = values.int(result_key)?; + result = values.array_set(result, result_key, chunk)?; + } + + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs index bef1fd9f8d..5f6ebb2603 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs @@ -35,3 +35,47 @@ pub(in crate::interpreter) fn eval_array_column_declared_values_result( let [array, column_key] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_column_result(*array, *column_key, values) } + +/// Evaluates PHP `array_column()` over row-array and column-key expressions. +pub(in crate::interpreter) fn eval_builtin_array_column( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, column_key] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let column_key = eval_expr(column_key, context, scope, values)?; + eval_array_column_result(array, column_key, values) +} + +/// Builds `array_column()` by extracting present row columns into a reindexed array. +pub(in crate::interpreter) fn eval_array_column_result( + array: RuntimeCellHandle, + column_key: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len)?; + let mut output_index = 0_i64; + for position in 0..len { + let row_key = values.array_iter_key(array, position)?; + let row = values.array_get(array, row_key)?; + if !matches!(values.type_tag(row)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + continue; + } + let exists = values.array_key_exists(column_key, row)?; + if !values.truthy(exists)? { + continue; + } + let column = values.array_get(row, column_key)?; + let target_key = values.int(output_index)?; + output_index = output_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, column)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs index 021ed2bae6..843ec10562 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs @@ -35,3 +35,41 @@ pub(in crate::interpreter) fn eval_array_combine_declared_values_result( let [keys, values_array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_combine_result(*keys, *values_array, values) } + +/// Evaluates PHP `array_combine()` over key and value array expressions. +pub(in crate::interpreter) fn eval_builtin_array_combine( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, values_array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let values_array = eval_expr(values_array, context, scope, values)?; + eval_array_combine_result(keys, values_array, values) +} + +/// Builds the associative result for `array_combine()` from two eval arrays. +pub(in crate::interpreter) fn eval_array_combine_result( + keys: RuntimeCellHandle, + values_array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + if len != values.array_len(values_array)? { + return Err(EvalStatus::RuntimeFatal); + } + + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + let target_key = values.cast_string(target_key)?; + let value_key = values.array_iter_key(values_array, position)?; + let value = values.array_get(values_array, value_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs index 6529ac7ce9..f70d6b0fb0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs @@ -36,3 +36,55 @@ pub(in crate::interpreter) fn eval_array_diff_declared_values_result( let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_value_set_result("array_diff", *left, *right, values) } + +/// Evaluates PHP value-set array builtins over two eval array expressions. +pub(in crate::interpreter) fn eval_builtin_array_value_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_value_set_result(name, left, right, values) +} + +/// Builds `array_diff()` or `array_intersect()` using PHP's default string comparison mode. +pub(in crate::interpreter) fn eval_array_value_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let mut right_values = Vec::with_capacity(right_len); + for position in 0..right_len { + let key = values.array_iter_key(right, position)?; + let value = values.array_get(right, key)?; + right_values.push(values.string_bytes(value)?); + } + + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let comparable = values.string_bytes(value)?; + let found = right_values + .iter() + .any(|right_value| right_value == &comparable); + let keep = match name { + "array_diff" => !found, + "array_intersect" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs index 59fb336f52..fabb72b64c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs @@ -36,3 +36,45 @@ pub(in crate::interpreter) fn eval_array_diff_key_declared_values_result( let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_key_set_result("array_diff_key", *left, *right, values) } + +/// Evaluates PHP key-set array builtins over two eval array expressions. +pub(in crate::interpreter) fn eval_builtin_array_key_set( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_key_set_result(name, left, right, values) +} + +/// Builds `array_diff_key()` or `array_intersect_key()` by testing first-array keys. +pub(in crate::interpreter) fn eval_array_key_set_result( + name: &str, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let mut result = values.assoc_new(left_len)?; + for position in 0..left_len { + let key = values.array_iter_key(left, position)?; + let value = values.array_get(left, key)?; + let exists = values.array_key_exists(key, right)?; + let found = values.truthy(exists)?; + let keep = match name { + "array_diff_key" => !found, + "array_intersect_key" => found, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs index e58f6750ac..89e4d3a552 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs @@ -35,3 +35,46 @@ pub(in crate::interpreter) fn eval_array_fill_declared_values_result( let [start, count, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_fill_result(*start, *count, *value, values) } + +/// Evaluates PHP `array_fill()` over start, count, and value expressions. +pub(in crate::interpreter) fn eval_builtin_array_fill( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, count, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let count = eval_expr(count, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_result(start, count, value, values) +} + +/// Builds an `array_fill()` result with PHP's explicit integer key range. +pub(in crate::interpreter) fn eval_array_fill_result( + start: RuntimeCellHandle, + count: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let count = eval_int_value(count, values)?; + if count < 0 { + return Err(EvalStatus::RuntimeFatal); + } + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let mut result = if start == 0 { + values.array_new(count)? + } else { + values.assoc_new(count)? + }; + for offset in 0..count { + let offset = i64::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = start.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs index 71e0cc9d2b..a5f3dd4e4b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs @@ -35,3 +35,34 @@ pub(in crate::interpreter) fn eval_array_fill_keys_declared_values_result( let [keys, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_fill_keys_result(*keys, *value, values) } + +/// Evaluates PHP `array_fill_keys()` over key-array and value expressions. +pub(in crate::interpreter) fn eval_builtin_array_fill_keys( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [keys, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let keys = eval_expr(keys, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_fill_keys_result(keys, value, values) +} + +/// Builds an `array_fill_keys()` result preserving the source key iteration order. +pub(in crate::interpreter) fn eval_array_fill_keys_result( + keys: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(keys)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let source_key = values.array_iter_key(keys, position)?; + let target_key = values.array_get(keys, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs index 706a0fd6ea..3c9aa52b63 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs @@ -45,3 +45,127 @@ pub(in crate::interpreter) fn eval_array_filter_declared_values_result( _ => Err(EvalStatus::RuntimeFatal), } } + +/// Evaluates PHP `array_filter()` for null and callable filtering modes. +pub(in crate::interpreter) fn eval_builtin_array_filter( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_filter_result_from_scope(array, None, None, Some(scope), context, values) + } + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + eval_array_filter_result_from_scope( + array, + Some(callback), + None, + Some(scope), + context, + values, + ) + } + [array, callback, mode] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_array_filter_result_from_scope( + array, + Some(callback), + Some(mode), + Some(scope), + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Filters eval array entries through PHP truthiness or a callable callback. +pub(in crate::interpreter) fn eval_array_filter_result( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_filter_result_from_scope(array, callback, mode, None, context, values) +} + +/// Filters eval array entries with optional lexical scope for callback names. +fn eval_array_filter_result_from_scope( + array: RuntimeCellHandle, + callback: Option, + mode: Option, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = match callback { + Some(callback) if !values.is_null(callback)? => { + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) + } + _ => None, + }; + let mode = match mode { + Some(mode) => eval_array_filter_mode_value(mode, values)?, + None => EVAL_ARRAY_FILTER_USE_VALUE, + }; + + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let keep = if let Some(callback) = callback.as_ref() { + let args = eval_array_filter_callback_args(mode, key, value)?; + let result = eval_evaluated_callable_with_values(callback, args, context, values)?; + values.truthy(result)? + } else { + values.truthy(value)? + }; + if keep { + result = values.array_set(result, key, value)?; + } + } + Ok(result) +} + +/// Reads and validates the optional `array_filter()` callback mode. +pub(in crate::interpreter) fn eval_array_filter_mode_value( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = eval_int_value(mode, values)?; + match mode { + EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { + Ok(mode) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the callback argument list for one `array_filter()` entry. +pub(in crate::interpreter) fn eval_array_filter_callback_args( + mode: i64, + key: RuntimeCellHandle, + value: RuntimeCellHandle, +) -> Result, EvalStatus> { + match mode { + EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), + EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), + EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs index 9291deb3ee..096ff25cf0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs @@ -35,3 +35,35 @@ pub(in crate::interpreter) fn eval_array_flip_declared_values_result( let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_flip_result(*array, values) } + +/// Evaluates PHP `array_flip()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_flip( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_flip_result(array, values) +} + +/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. +pub(in crate::interpreter) fn eval_array_flip_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { + continue; + } + result = values.array_set(result, value, key)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs index fcf390242b..0078e69312 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_array_intersect_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - eval_builtin_array_value_set("array_intersect", args, context, scope, values) + super::array_diff::eval_builtin_array_value_set("array_intersect", args, context, scope, values) } /// Dispatches evaluated-argument eval calls for the `array_intersect` array builtin. @@ -34,5 +34,5 @@ pub(in crate::interpreter) fn eval_array_intersect_declared_values_result( values: &mut impl RuntimeValueOps, ) -> Result { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_array_value_set_result("array_intersect", *left, *right, values) + super::array_diff::eval_array_value_set_result("array_intersect", *left, *right, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs index 09029a0cf3..80d188076e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_array_intersect_key_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - eval_builtin_array_key_set("array_intersect_key", args, context, scope, values) + super::array_diff_key::eval_builtin_array_key_set("array_intersect_key", args, context, scope, values) } /// Dispatches evaluated-argument eval calls for the `array_intersect_key` array builtin. @@ -34,5 +34,5 @@ pub(in crate::interpreter) fn eval_array_intersect_key_declared_values_result( values: &mut impl RuntimeValueOps, ) -> Result { let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_array_key_set_result("array_intersect_key", *left, *right, values) + super::array_diff_key::eval_array_key_set_result("array_intersect_key", *left, *right, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs index 36e85fd7ae..fef9a1dce3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs @@ -35,3 +35,18 @@ pub(in crate::interpreter) fn eval_array_key_exists_declared_values_result( let [key, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; values.array_key_exists(*key, *array) } + +/// Evaluates PHP `array_key_exists()` over a key and array expression. +pub(in crate::interpreter) fn eval_builtin_array_key_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [key, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let key = eval_expr(key, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + values.array_key_exists(key, array) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs index 56cb38ce9a..ddcaa7c78a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs @@ -36,3 +36,138 @@ pub(in crate::interpreter) fn eval_array_map_declared_values_result( let Some((callback, arrays)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; eval_array_map_result(*callback, arrays, context, values) } + +/// Evaluates PHP `array_map()` for one or more arrays and an optional callback. +pub(in crate::interpreter) fn eval_builtin_array_map( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((callback, arrays)) = args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let callback = eval_expr(callback, context, scope, values)?; + let mut evaluated_arrays = Vec::with_capacity(arrays.len()); + for array in arrays { + evaluated_arrays.push(eval_expr(array, context, scope, values)?); + } + eval_array_map_result_from_scope(callback, &evaluated_arrays, Some(scope), context, values) +} + +/// Maps one eval array with PHP key preservation for the one-array form. +pub(in crate::interpreter) fn eval_array_map_result( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_map_result_from_scope(callback, arrays, None, context, values) +} + +/// Maps one or more eval arrays with optional lexical scope for callback names. +fn eval_array_map_result_from_scope( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = arrays else { + return eval_array_map_variadic_result_from_scope( + callback, + arrays, + lexical_scope, + context, + values, + ); + }; + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) + }; + let len = values.array_len(*array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(*array, position)?; + let value = values.array_get(*array, key)?; + let mapped = if let Some(callback) = callback.as_ref() { + eval_evaluated_callable_with_values(callback, vec![value], context, values)? + } else { + value + }; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Maps multiple eval arrays with optional lexical scope for callback names. +fn eval_array_map_variadic_result_from_scope( + callback: RuntimeCellHandle, + arrays: &[RuntimeCellHandle], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if arrays.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let callback = if values.is_null(callback)? { + None + } else { + Some(eval_callable_with_optional_scope( + callback, + context, + lexical_scope, + values, + )?) + }; + let mut lengths = Vec::with_capacity(arrays.len()); + let mut max_len = 0; + for array in arrays { + let len = values.array_len(*array)?; + max_len = max_len.max(len); + lengths.push(len); + } + + let mut result = values.array_new(max_len)?; + for position in 0..max_len { + let mut callback_args = Vec::with_capacity(arrays.len()); + for (array, len) in arrays.iter().zip(lengths.iter()) { + let value = if position < *len { + let key = values.array_iter_key(*array, position)?; + values.array_get(*array, key)? + } else { + values.null()? + }; + callback_args.push(value); + } + let mapped = if let Some(callback) = callback.as_ref() { + eval_evaluated_callable_with_values(callback, callback_args, context, values)? + } else { + eval_array_map_zipped_row(callback_args, values)? + }; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, mapped)?; + } + Ok(result) +} + +/// Builds one row for `array_map(null, $a, $b, ...)`. +pub(in crate::interpreter) fn eval_array_map_zipped_row( + values_row: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut row = values.array_new(values_row.len())?; + for (index, value) in values_row.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + row = values.array_set(row, key, value)?; + } + Ok(row) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs index e17c24711d..b865f2953f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs @@ -36,3 +36,60 @@ pub(in crate::interpreter) fn eval_array_merge_declared_values_result( let [left, right] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_merge_result(*left, *right, values) } + +/// Evaluates PHP `array_merge()` over two array expressions. +pub(in crate::interpreter) fn eval_builtin_array_merge( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [left, right] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let left = eval_expr(left, context, scope, values)?; + let right = eval_expr(right, context, scope, values)?; + eval_array_merge_result(left, right, values) +} + +/// Builds an `array_merge()` result with PHP numeric reindexing and string-key overwrites. +pub(in crate::interpreter) fn eval_array_merge_result( + left: RuntimeCellHandle, + right: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let left_len = values.array_len(left)?; + let right_len = values.array_len(right)?; + let capacity = left_len + .checked_add(right_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let mut result = values.assoc_new(capacity)?; + let mut next_numeric_key = 0_i64; + result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; + eval_array_merge_append_operand(result, right, &mut next_numeric_key, values) +} + +/// Appends one source array to an `array_merge()` result using PHP key handling. +pub(in crate::interpreter) fn eval_array_merge_append_operand( + mut result: RuntimeCellHandle, + source: RuntimeCellHandle, + next_numeric_key: &mut i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(source)?; + for position in 0..len { + let source_key = values.array_iter_key(source, position)?; + let source_value = values.array_get(source, source_key)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_STRING { + source_key + } else { + let target_key = values.int(*next_numeric_key)?; + *next_numeric_key = (*next_numeric_key) + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + target_key + }; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs index 2dca5c68c2..0cca24d3fa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs @@ -35,3 +35,78 @@ pub(in crate::interpreter) fn eval_array_pad_declared_values_result( let [array, length, value] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_pad_result(*array, *length, *value, values) } + +/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. +pub(in crate::interpreter) fn eval_builtin_array_pad( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array, length, value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_array_pad_result(array, length, value, values) +} + +/// Builds an `array_pad()` result by copying values and padding left or right. +pub(in crate::interpreter) fn eval_array_pad_result( + array: RuntimeCellHandle, + length: RuntimeCellHandle, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let target = eval_int_value(length, values)?; + let target_len = target + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + let result_len = usize::max(len, target_len); + let pad_count = result_len.saturating_sub(len); + let mut result = values.array_new(result_len)?; + let mut output_index = 0usize; + + if target < 0 { + let (padded, next_index) = + eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; + result = padded; + output_index = next_index; + } + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + output_index += 1; + } + + if target > 0 { + result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; + } + + Ok(result) +} + +/// Appends the same pad value at consecutive indexed positions in an array result. +pub(in crate::interpreter) fn eval_array_pad_append_repeated( + mut array: RuntimeCellHandle, + start_index: usize, + count: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, usize), EvalStatus> { + let mut next_index = start_index; + for _ in 0..count { + let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + array = values.array_set(array, key, value)?; + next_index += 1; + } + Ok((array, next_index)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs index ed855d066e..8242e26368 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs @@ -37,3 +37,156 @@ pub(in crate::interpreter) fn eval_warn_array_by_value( "{name}(): Argument #1 ($array) must be passed by reference, value given" )) } + +/// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. +pub(in crate::interpreter) fn eval_array_pop_shift_value_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(array)?; + if len == 0 { + return values.null(); + } + let position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let key = values.array_iter_key(array, position)?; + values.array_get(array, key) +} + +/// Builds the return value plus replacement array for direct pop/shift write-back. +pub(in crate::interpreter) fn eval_array_pop_shift_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let len = values.array_len(array)?; + let tag = values.type_tag(array)?; + if len == 0 { + let replacement = match tag { + EVAL_TAG_ARRAY => values.array_new(0)?, + EVAL_TAG_ASSOC => values.assoc_new(0)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + return Ok((values.null()?, replacement)); + } + + let removed_position = match name { + "array_pop" => len - 1, + "array_shift" => 0, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let removed_key = values.array_iter_key(array, removed_position)?; + let removed_value = values.array_get(array, removed_key)?; + let replacement = match tag { + EVAL_TAG_ARRAY => { + eval_array_pop_shift_indexed_replacement(array, removed_position, len, values)? + } + EVAL_TAG_ASSOC => { + eval_array_pop_shift_assoc_replacement(name, array, removed_position, len, values)? + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + Ok((removed_value, replacement)) +} + +/// Rebuilds an indexed array after removing one position and reindexing values. +pub(in crate::interpreter) fn eval_array_pop_shift_indexed_replacement( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(len.saturating_sub(1))?; + let mut target = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after pop/shift, preserving PHP key behavior. +pub(in crate::interpreter) fn eval_array_pop_shift_assoc_replacement( + name: &str, + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if name == "array_shift" + && eval_array_remaining_keys_are_int(array, removed_position, len, values)? + { + return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); + } + + let mut result = values.assoc_new(len.saturating_sub(1))?; + let mut next_int_key = 0_i64; + for position in 0..len { + if position == removed_position { + continue; + } + let source_key = values.array_iter_key(array, position)?; + let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every remaining key is an integer after removing one element. +pub(in crate::interpreter) fn eval_array_remaining_keys_are_int( + array: RuntimeCellHandle, + removed_position: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if position == removed_position { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. +pub(in crate::interpreter) fn eval_array_pop_shift_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let (array, target) = super::mutation::eval_array_mutation_lvalue_arg(arg, context, scope, values)?; + + let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs index d4a80cbffc..6a1c5299ca 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs @@ -23,7 +23,7 @@ pub(in crate::interpreter) fn eval_array_product_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - eval_builtin_array_aggregate("array_product", args, context, scope, values) + super::array_sum::eval_builtin_array_aggregate("array_product", args, context, scope, values) } /// Dispatches evaluated-argument eval calls for the `array_product` array builtin. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_array_product_declared_values_result( values: &mut impl RuntimeValueOps, ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_array_aggregate_result("array_product", *array, values) + super::array_sum::eval_array_aggregate_result("array_product", *array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs index 6edf660fd9..abcca5d0b3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs @@ -28,3 +28,191 @@ pub(in crate::interpreter) fn eval_array_push_declared_values_result( super::array_pop::eval_warn_array_by_value("array_push", values)?; eval_array_push_unshift_count_result(*array, inserted.len(), values) } + +/// Returns the resulting element count for by-value push/unshift dynamic calls. +pub(in crate::interpreter) fn eval_array_push_unshift_count_result( + array: RuntimeCellHandle, + inserted_len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let total = values + .array_len(array)? + .checked_add(inserted_len) + .ok_or(EvalStatus::RuntimeFatal)?; + let total = i64::try_from(total).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(total) +} + +/// Builds the replacement array for direct push/unshift write-back. +pub(in crate::interpreter) fn eval_array_push_unshift_replacement( + name: &str, + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, values.type_tag(array)?) { + ("array_push", EVAL_TAG_ARRAY) => { + eval_array_push_indexed_replacement(array, inserted, values) + } + ("array_push", EVAL_TAG_ASSOC) => { + eval_array_push_assoc_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ARRAY) => { + eval_array_unshift_indexed_replacement(array, inserted, values) + } + ("array_unshift", EVAL_TAG_ASSOC) => { + eval_array_unshift_assoc_replacement(array, inserted, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Rebuilds an indexed array after appending values. +pub(in crate::interpreter) fn eval_array_push_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let target_key = + values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, target_key, value)?; + } + for (offset, value) in inserted.iter().copied().enumerate() { + let position = len.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after appending values at PHP's next integer keys. +pub(in crate::interpreter) fn eval_array_push_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_key = 0_i64; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? == EVAL_TAG_INT { + next_key = next_key.max(eval_int_value(key, values)?.saturating_add(1)); + } + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + for value in inserted.iter().copied() { + let key = values.int(next_key)?; + next_key = next_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an indexed array after prepending values and reindexing the original tail. +pub(in crate::interpreter) fn eval_array_unshift_indexed_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.array_new(len.saturating_add(inserted.len()))?; + let mut target = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Rebuilds an associative array after prepending values and reindexing integer keys. +pub(in crate::interpreter) fn eval_array_unshift_assoc_replacement( + array: RuntimeCellHandle, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if eval_array_keys_are_int(array, len, values)? { + return eval_array_unshift_indexed_replacement(array, inserted, values); + } + + let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; + let mut next_int_key = 0_i64; + for value in inserted.iter().copied() { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, key, value)?; + } + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in the array is integer-shaped. +pub(in crate::interpreter) fn eval_array_keys_are_int( + array: RuntimeCellHandle, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Evaluates direct by-reference `array_push()` / `array_unshift()` calls and writes back the array. +pub(in crate::interpreter) fn eval_array_push_unshift_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 || !eval_call_args_are_plain_positional(args) { + return Err(EvalStatus::RuntimeFatal); + } + let (array, target) = super::mutation::eval_array_mutation_lvalue_arg(&args[0], context, scope, values)?; + let mut inserted = Vec::with_capacity(args.len() - 1); + for arg in &args[1..] { + inserted.push(eval_expr(arg.value(), context, scope, values)?); + } + + let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; + let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs index 01d28dd1a9..b2c27aecac 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs @@ -35,3 +35,35 @@ pub(in crate::interpreter) fn eval_array_rand_declared_values_result( let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_rand_result(*array, values) } + +/// Evaluates PHP `array_rand()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_rand( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_rand_result(array, values) +} + +/// Returns a valid random key from a non-empty eval array. +pub(in crate::interpreter) fn eval_array_rand_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + if len == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let position = eval_random_position(len); + values.array_iter_key(array, position) +} + +/// Chooses a pseudo-random array position within `[0, len)`. +pub(in crate::interpreter) fn eval_random_position(len: usize) -> usize { + (eval_random_u128() % (len as u128)) as usize +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs index 45e0ac89a6..1c59489a95 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs @@ -43,3 +43,59 @@ pub(in crate::interpreter) fn eval_array_reduce_declared_values_result( _ => Err(EvalStatus::RuntimeFatal), } } + +/// Evaluates PHP `array_reduce()` with an optional initial carry value. +pub(in crate::interpreter) fn eval_builtin_array_reduce( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, callback, initial) = match args { + [array, callback] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + (array, callback, values.null()?) + } + [array, callback, initial] => { + let array = eval_expr(array, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let initial = eval_expr(initial, context, scope, values)?; + (array, callback, initial) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_array_reduce_result_from_scope(array, callback, initial, Some(scope), context, values) +} + +/// Reduces one eval array by invoking a callable with carry and item cells. +pub(in crate::interpreter) fn eval_array_reduce_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_reduce_result_from_scope(array, callback, initial, None, context, values) +} + +/// Reduces one eval array with optional lexical scope for callback names. +fn eval_array_reduce_result_from_scope( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + initial: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + let mut carry = initial; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + carry = + eval_evaluated_callable_with_values(&callback, vec![carry, value], context, values)?; + } + Ok(carry) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs index 2e794f004c..f90abdaff5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs @@ -43,3 +43,61 @@ pub(in crate::interpreter) fn eval_array_reverse_declared_values_result( _ => Err(EvalStatus::RuntimeFatal), } } + +/// Evaluates PHP `array_reverse()` over an eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_reverse( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array] => { + let array = eval_expr(array, context, scope, values)?; + eval_array_reverse_result(array, false, values) + } + [array, preserve_keys] => { + let array = eval_expr(array, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_array_reverse_result(array, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_reverse()` result while preserving PHP key rules. +pub(in crate::interpreter) fn eval_array_reverse_result( + array: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut keys = Vec::with_capacity(len); + let mut has_string_key = false; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; + keys.push(key); + } + + let mut result = if preserve_keys || has_string_key { + values.assoc_new(len)? + } else { + values.array_new(len)? + }; + let mut next_numeric_key = 0_i64; + + for key in keys.into_iter().rev() { + let value = values.array_get(array, key)?; + let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { + key + } else { + let key = values.int(next_numeric_key)?; + next_numeric_key += 1; + key + }; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs index 75faff3b63..d56a1eaf81 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs @@ -37,3 +37,46 @@ pub(in crate::interpreter) fn eval_array_search_declared_values_result( let [needle, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_search_result("array_search", *needle, *array, values) } + +/// Evaluates PHP array search builtins over a needle and haystack expression. +pub(in crate::interpreter) fn eval_builtin_array_search( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [needle, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let needle = eval_expr(needle, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_array_search_result(name, needle, array, values) +} + +/// Searches an eval array with PHP's default loose comparison semantics. +pub(in crate::interpreter) fn eval_array_search_result( + name: &str, + needle: RuntimeCellHandle, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let equal = values.compare(EvalBinOp::LooseEq, needle, value)?; + if values.truthy(equal)? { + return match name { + "in_array" => values.bool_value(true), + "array_search" => Ok(key), + _ => Err(EvalStatus::UnsupportedConstruct), + }; + } + } + match name { + "in_array" => values.bool_value(false), + "array_search" => values.bool_value(false), + _ => Err(EvalStatus::UnsupportedConstruct), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs index e1acd091a9..2c319edaf4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_array_shift_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("array_shift", values)?; - eval_array_pop_shift_value_result("array_shift", *array, values) + super::array_pop::eval_array_pop_shift_value_result("array_shift", *array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs index 891e3098fa..4f87ec7ea6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs @@ -40,3 +40,90 @@ pub(in crate::interpreter) fn eval_array_slice_declared_values_result( _ => Err(EvalStatus::RuntimeFatal), } } + +/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. +pub(in crate::interpreter) fn eval_builtin_array_slice( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [array, offset] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + eval_array_slice_result(array, offset, None, values) + } + [array, offset, length] => { + let array = eval_expr(array, context, scope, values)?; + let offset = eval_expr(offset, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_array_slice_result(array, offset, Some(length), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds an `array_slice()` result with PHP offset and length bounds. +pub(in crate::interpreter) fn eval_array_slice_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + + let mut result = values.array_new(end.saturating_sub(start))?; + for source_position in start..end { + let source_key = values.array_iter_key(array, source_position)?; + let source_value = values.array_get(array, source_key)?; + let target_key = + i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_key = values.int(target_key)?; + result = values.array_set(result, target_key, source_value)?; + } + Ok(result) +} + +/// Converts a PHP array-slice offset into a bounded source position. +pub(in crate::interpreter) fn eval_slice_start( + len: usize, + offset: i64, +) -> Result { + if offset >= 0 { + let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(offset, len)); + } + + let tail = offset + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(len.saturating_sub(tail)) +} + +/// Converts a PHP array-slice length into a bounded exclusive end position. +pub(in crate::interpreter) fn eval_slice_end( + len: usize, + start: usize, + length: i64, +) -> Result { + if length >= 0 { + let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; + return Ok(usize::min(start.saturating_add(length), len)); + } + + let tail = length + .checked_abs() + .ok_or(EvalStatus::RuntimeFatal) + .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; + Ok(usize::max(start, len.saturating_sub(tail))) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs index 8a777f7fcc..59904e8439 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs @@ -39,3 +39,317 @@ pub(in crate::interpreter) fn eval_array_splice_declared_values_result( values.warning("array_splice(): Argument #1 ($array) must be passed by reference, value given")?; Ok(result) } + +/// Evaluates direct by-reference `array_splice()` calls and writes back the array. +pub(in crate::interpreter) fn eval_builtin_array_splice_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, target, offset, length, replacement_arg) = + eval_array_splice_direct_args(args, context, scope, values)?; + + let (removed, replacement) = + eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(removed) +} + +/// Evaluates and binds direct `array_splice()` arguments while preserving source order. +pub(in crate::interpreter) fn eval_array_splice_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut array = None; + let mut offset = None; + let mut length = None; + let mut replacement = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "offset", + 2 => "length", + 3 => "replacement", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + array = Some(super::mutation::eval_array_mutation_lvalue_arg( + arg, context, scope, values, + )?); + } + "offset" => { + if offset.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + offset = Some(eval_expr(arg.value(), context, scope, values)?); + } + "length" => { + if length.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + length = Some(eval_expr(arg.value(), context, scope, values)?); + } + "replacement" => { + if replacement.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + replacement = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (array, target) = array.ok_or(EvalStatus::RuntimeFatal)?; + let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, target, offset, length, replacement)) +} + +/// Returns the removed elements that `array_splice()` would produce without mutating the source. +pub(in crate::interpreter) fn eval_array_splice_value_result( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + eval_array_splice_removed(array, start, end, values) +} + +/// Builds both removed and replacement arrays for direct `array_splice()` write-back. +pub(in crate::interpreter) fn eval_array_splice_removed_and_replacement( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; + let removed = eval_array_splice_removed(array, start, end, values)?; + let inserted = eval_array_splice_insert_values(replacement, values)?; + let replacement = eval_array_splice_replacement(array, start, end, &inserted, values)?; + Ok((removed, replacement)) +} + +/// Converts splice offset and length cells into bounded source positions. +pub(in crate::interpreter) fn eval_array_splice_bounds( + array: RuntimeCellHandle, + offset: RuntimeCellHandle, + length: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(usize, usize), EvalStatus> { + let len = values.array_len(array)?; + let offset = eval_int_value(offset, values)?; + let start = super::array_slice::eval_slice_start(len, offset)?; + let end = match length { + Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { + super::array_slice::eval_slice_end(len, start, eval_int_value(length, values)?)? + } + _ => len, + }; + Ok((start, end)) +} + +/// Builds the reindexed/string-key-preserving removed array returned by `array_splice()`. +pub(in crate::interpreter) fn eval_array_splice_removed( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = end.saturating_sub(start); + if eval_array_range_keys_are_int(array, start, end, values)? { + let mut result = values.array_new(len)?; + let mut target = 0_i64; + for position in start..end { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(len)?; + let mut next_int_key = 0_i64; + for position in start..end { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Expands the optional `array_splice()` replacement value into inserted values. +pub(in crate::interpreter) fn eval_array_splice_insert_values( + replacement: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(replacement) = replacement else { + return Ok(Vec::new()); + }; + if !matches!( + values.type_tag(replacement)?, + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC + ) { + return Ok(vec![replacement]); + } + + let len = values.array_len(replacement)?; + let mut inserted = Vec::with_capacity(len); + for position in 0..len { + let key = values.array_iter_key(replacement, position)?; + inserted.push(values.array_get(replacement, key)?); + } + Ok(inserted) +} + +/// Builds the source replacement after removing the requested splice range. +pub(in crate::interpreter) fn eval_array_splice_replacement( + array: RuntimeCellHandle, + start: usize, + end: usize, + inserted: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let new_len = len + .saturating_sub(end.saturating_sub(start)) + .checked_add(inserted.len()) + .ok_or(EvalStatus::RuntimeFatal)?; + if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { + let mut result = values.array_new(new_len)?; + let mut target = 0_i64; + for position in 0..start { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let target_key = values.int(target)?; + target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, value)?; + } + return Ok(result); + } + + let mut result = values.assoc_new(new_len)?; + let mut next_int_key = 0_i64; + for position in 0..start { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + for value in inserted { + let target_key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + result = values.array_set(result, target_key, *value)?; + } + for position in end..len { + let source_key = values.array_iter_key(array, position)?; + let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { + let key = values.int(next_int_key)?; + next_int_key = next_int_key + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + } else { + source_key + }; + let value = values.array_get(array, source_key)?; + result = values.array_set(result, target_key, value)?; + } + Ok(result) +} + +/// Returns true when every key in one source position range is integer-shaped. +pub(in crate::interpreter) fn eval_array_range_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in start..end { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} + +/// Returns true when every key outside the removed splice range is integer-shaped. +pub(in crate::interpreter) fn eval_array_splice_remaining_keys_are_int( + array: RuntimeCellHandle, + start: usize, + end: usize, + len: usize, + values: &mut impl RuntimeValueOps, +) -> Result { + for position in 0..len { + if (start..end).contains(&position) { + continue; + } + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + return Ok(false); + } + } + Ok(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs index 820780c8d3..45f141dc1b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs @@ -35,3 +35,42 @@ pub(in crate::interpreter) fn eval_array_sum_declared_values_result( let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_aggregate_result("array_sum", *array, values) } + +/// Evaluates PHP array aggregate builtins over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_aggregate( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_aggregate_result(name, array, values) +} + +/// Computes `array_sum()` or `array_product()` through eval's numeric value hooks. +pub(in crate::interpreter) fn eval_array_aggregate_result( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = match name { + "array_sum" => values.int(0)?, + "array_product" => values.int(1)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = match name { + "array_sum" => values.add(result, value)?, + "array_product" => values.mul(result, value)?, + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs index 879425c893..90d1f019f6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs @@ -35,3 +35,38 @@ pub(in crate::interpreter) fn eval_array_unique_declared_values_result( let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_array_unique_result(*array, values) } + +/// Evaluates PHP `array_unique()` over one eval array expression. +pub(in crate::interpreter) fn eval_builtin_array_unique( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = eval_expr(array, context, scope, values)?; + eval_array_unique_result(array, values) +} + +/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. +pub(in crate::interpreter) fn eval_array_unique_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut seen = Vec::>::with_capacity(len); + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let unique_key = values.string_bytes(value)?; + if seen.iter().any(|seen_key| seen_key == &unique_key) { + continue; + } + seen.push(unique_key); + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs index cc15fff2e2..9b6d3136d0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs @@ -26,5 +26,5 @@ pub(in crate::interpreter) fn eval_array_unshift_declared_values_result( ) -> Result { let Some((array, inserted)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("array_unshift", values)?; - eval_array_push_unshift_count_result(*array, inserted.len(), values) + super::array_push::eval_array_push_unshift_count_result(*array, inserted.len(), values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs b/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs index 2f57b1c56c..ee36e0d4c8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs @@ -27,3 +27,144 @@ pub(in crate::interpreter) fn eval_array_walk_declared_values_result( values.warning("array_walk(): Argument #1 ($array) must be passed by reference, value given")?; eval_array_walk_result(*array, *callback, context, values) } + +/// Evaluates direct PHP `array_walk()` calls and preserves element by-ref targets. +pub(in crate::interpreter) fn eval_builtin_array_walk_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, array_target, callback) = + eval_array_walk_direct_args(args, context, scope, values)?; + eval_array_walk_ref_result_from_scope(array, array_target, callback, Some(scope), context, values) +} + +/// Evaluates and binds direct `array_walk()` arguments in PHP source order. +fn eval_array_walk_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut array_target = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array_target.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + array_target = Some(super::mutation::eval_array_mutation_lvalue_arg(arg, context, scope, values)?); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (array, array_target) = array_target.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, array_target, callback)) +} + +/// Walks one writable eval array by invoking a callable with element ref targets. +pub(in crate::interpreter) fn eval_array_walk_ref_result( + array: RuntimeCellHandle, + array_target: EvalReferenceTarget, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_walk_ref_result_from_scope(array, array_target, callback, None, context, values) +} + +/// Walks one writable eval array with optional lexical scope for callback names. +fn eval_array_walk_ref_result_from_scope( + array: RuntimeCellHandle, + array_target: EvalReferenceTarget, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let current_array = eval_reference_target_value(&array_target, context, values)?; + let key = values.array_iter_key(current_array, position)?; + let value = values.array_get(current_array, key)?; + let ref_target = EvalReferenceTarget::NestedArrayElement { + array_target: Box::new(array_target.clone()), + index: key, + }; + let args = vec![ + EvaluatedCallArg { + name: None, + value, + ref_target: Some(ref_target), + }, + EvaluatedCallArg { + name: None, + value: key, + ref_target: None, + }, + ]; + let _ = eval_evaluated_callable_with_call_array_args(&callback, args, context, values)?; + } + values.bool_value(true) +} + +/// Walks one eval array by invoking a callable with value and key cells. +pub(in crate::interpreter) fn eval_array_walk_result( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_array_walk_result_from_scope(array, callback, None, context, values) +} + +/// Walks one eval array with optional lexical scope for callback names. +fn eval_array_walk_result_from_scope( + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + let _ = eval_evaluated_callable_with_values(&callback, vec![value, key], context, values)?; + } + values.bool_value(true) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs index b6d7d9effb..94e818a2df 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_arsort_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("arsort", values)?; - eval_array_sort_value_result(*array, values) + super::sort::eval_array_sort_value_result(*array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/asort.rs b/crates/elephc-magician/src/interpreter/builtins/array/asort.rs index ecd5c17e53..a969923046 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/asort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/asort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_asort_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("asort", values)?; - eval_array_sort_value_result(*array, values) + super::sort::eval_array_sort_value_result(*array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs b/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs index 684ec55c63..d773a16621 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs @@ -25,7 +25,7 @@ pub(in crate::interpreter) fn eval_in_array_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - eval_builtin_array_search("in_array", args, context, scope, values) + super::array_search::eval_builtin_array_search("in_array", args, context, scope, values) } /// Dispatches evaluated-argument eval calls for the `in_array` array builtin. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_in_array_declared_values_result( values: &mut impl RuntimeValueOps, ) -> Result { let [needle, array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; - eval_array_search_result("in_array", *needle, *array, values) + super::array_search::eval_array_search_result("in_array", *needle, *array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs index e9dab17f7d..08a41d0b43 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs @@ -47,3 +47,107 @@ pub(in crate::interpreter) fn eval_iterator_apply_declared_values_result( _ => Err(EvalStatus::RuntimeFatal), } } + +/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_apply( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator, callback] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable_from_scope(callback, context, scope, values)?; + eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) + } + [iterator, callback, callback_args] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let callback = eval_expr(callback, context, scope, values)?; + let callback = eval_callable_from_scope(callback, context, scope, values)?; + let callback_args = eval_expr(callback_args, context, scope, values)?; + let callback_args = eval_iterator_apply_arg_values(callback_args, context, values)?; + eval_iterator_apply_result(iterator, &callback, callback_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts the optional `iterator_apply()` callback-args value into call arguments. +pub(in crate::interpreter) fn eval_iterator_apply_arg_values( + args: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(args)? { + return Ok(Vec::new()); + } + if !values.is_array_like(args)? { + return Err(EvalStatus::RuntimeFatal); + } + eval_array_call_arg_values(args, context, values) +} + +/// Applies a callback to each valid position of an eval-supported Traversable object. +pub(in crate::interpreter) fn eval_iterator_apply_result( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(iterator)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let count = match eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + ) { + Ok(count) => count, + Err(EvalStatus::UnsupportedConstruct) => { + let iterator = values.method_call(iterator, "getiterator", Vec::new())?; + eval_iterator_apply_iterator_object( + iterator, + callback, + &callback_args, + context, + values, + )? + } + Err(err) => return Err(err), + }; + values.int(count) +} + +/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. +pub(in crate::interpreter) fn eval_iterator_apply_iterator_object( + iterator: RuntimeCellHandle, + callback: &EvaluatedCallable, + callback_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let _ = values.method_call(iterator, "rewind", Vec::new())?; + let mut count = 0_i64; + loop { + let valid = values.method_call(iterator, "valid", Vec::new())?; + if !values.truthy(valid)? { + return Ok(count); + } + count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let result = eval_evaluated_callable_with_call_array_args( + callback, + callback_args.to_vec(), + context, + values, + )?; + if !values.truthy(result)? { + return Ok(count); + } + let _ = values.method_call(iterator, "next", Vec::new())?; + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs index 1782e2bb93..f04411aff7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs @@ -35,3 +35,29 @@ pub(in crate::interpreter) fn eval_iterator_count_declared_values_result( let [iterator] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_iterator_count_result(*iterator, values) } + +/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [iterator] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_count_result(iterator, values) +} + +/// Returns the element count for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_iterator_count_result( + iterator: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(iterator)?; + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs b/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs index e56c3af469..5a6fceba42 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs @@ -43,3 +43,55 @@ pub(in crate::interpreter) fn eval_iterator_to_array_declared_values_result( _ => Err(EvalStatus::RuntimeFatal), } } + +/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. +pub(in crate::interpreter) fn eval_builtin_iterator_to_array( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [iterator] => { + let iterator = eval_expr(iterator, context, scope, values)?; + eval_iterator_to_array_result(iterator, true, values) + } + [iterator, preserve_keys] => { + let iterator = eval_expr(iterator, context, scope, values)?; + let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; + let preserve_keys = values.truthy(preserve_keys)?; + eval_iterator_to_array_result(iterator, preserve_keys, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Copies eval-supported array iterator inputs into a PHP array result. +pub(in crate::interpreter) fn eval_iterator_to_array_result( + iterator: RuntimeCellHandle, + preserve_keys: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + if preserve_keys { + return eval_array_copy_preserve_keys(iterator, values); + } + super::array_values::eval_array_values_result(iterator, values) +} + +/// Copies one array-like eval value while preserving iteration keys and order. +pub(in crate::interpreter) fn eval_array_copy_preserve_keys( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut result = values.assoc_new(len)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs index 2c381bc8cd..123df91425 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_krsort_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("krsort", values)?; - eval_array_sort_value_result(*array, values) + super::sort::eval_array_sort_value_result(*array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs b/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs index 1757f34f08..ca403d432a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_ksort_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("ksort", values)?; - eval_array_sort_value_result(*array, values) + super::sort::eval_array_sort_value_result(*array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs index a6ed73817d..ed9b8dc282 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/mod.rs @@ -49,6 +49,8 @@ mod iterator_count; mod iterator_to_array; mod krsort; mod ksort; +mod mutating_dispatch; +mod mutation; mod natcasesort; mod natsort; mod range; @@ -60,6 +62,14 @@ mod uksort; mod usort; mod values_dispatch; +pub(in crate::interpreter) use array_pop::eval_array_pop_shift_replacement; +pub(in crate::interpreter) use array_push::{ + eval_array_push_unshift_count_result, eval_array_push_unshift_replacement, +}; +pub(in crate::interpreter) use array_splice::eval_array_splice_removed_and_replacement; +pub(in crate::interpreter) use array_walk::eval_array_walk_ref_result; pub(in crate::interpreter) use direct_dispatch::eval_builtin_array_declared_call; -pub(in crate::interpreter) use array_values::eval_array_values_result; +pub(in crate::interpreter) use mutating_dispatch::eval_builtin_array_mutating_declared_call; +pub(in crate::interpreter) use sort::eval_array_sort_replacement; +pub(in crate::interpreter) use usort::eval_user_sort_replacement; pub(in crate::interpreter) use values_dispatch::eval_array_declared_values_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs new file mode 100644 index 0000000000..769c6dc513 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/mutating_dispatch.rs @@ -0,0 +1,40 @@ +//! Purpose: +//! Area-level direct dispatch for source-sensitive array mutator builtins. +//! +//! Called from: +//! - `crate::interpreter::expressions::calls::eval_call()`. +//! +//! Key details: +//! - Dispatch stays orchestration-only; actual PHP-visible behavior lives in the +//! builtin owner files or the closest shared owner builtin. + +use super::super::super::*; + +/// Routes direct source-sensitive array mutator calls through builtin owner files. +pub(in crate::interpreter) fn eval_builtin_array_mutating_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "settype" => eval_builtin_settype_call(args, context, scope, values), + "array_pop" | "array_shift" => { + super::array_pop::eval_array_pop_shift_declared_call(name, args, context, scope, values) + } + "array_push" | "array_unshift" => { + super::array_push::eval_array_push_unshift_declared_call(name, args, context, scope, values) + } + "array_splice" => super::array_splice::eval_builtin_array_splice_call(args, context, scope, values), + "array_walk" => super::array_walk::eval_builtin_array_walk_call(args, context, scope, values), + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" | "rsort" + | "shuffle" | "sort" => { + super::sort::eval_array_sort_declared_call(name, args, context, scope, values) + } + "uasort" | "uksort" | "usort" => { + super::usort::eval_user_sort_declared_call(name, args, context, scope, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/array/mutation.rs new file mode 100644 index 0000000000..33f10d51ed --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/array/mutation.rs @@ -0,0 +1,29 @@ +//! Purpose: +//! Shared lvalue binding for source-sensitive array mutator builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::array` mutating builtin owners. +//! +//! Key details: +//! - The helper keeps the by-reference storage target together with the current +//! array cell so callers can write back replacements after PHP-visible work. + +use super::super::super::*; + +/// Captures the first by-reference array mutator argument as a writable lvalue. +pub(in crate::interpreter) fn eval_array_mutation_lvalue_arg( + arg: &EvalCallArg, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { + if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { + return Err(EvalStatus::RuntimeFatal); + } + let (array, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + let target = target.ok_or(EvalStatus::RuntimeFatal)?; + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + Ok((array, target)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs b/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs index f88ef3d076..8268460ead 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_natcasesort_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("natcasesort", values)?; - eval_array_sort_value_result(*array, values) + super::sort::eval_array_sort_value_result(*array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs index e16002366e..f50e1ecd02 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_natsort_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("natsort", values)?; - eval_array_sort_value_result(*array, values) + super::sort::eval_array_sort_value_result(*array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/range.rs b/crates/elephc-magician/src/interpreter/builtins/array/range.rs index dd73fb154e..2d9815f184 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/range.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/range.rs @@ -35,3 +35,49 @@ pub(in crate::interpreter) fn eval_range_declared_values_result( let [start, end] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; eval_range_result(*start, *end, values) } + +/// Evaluates PHP `range()` over integer-compatible start and end expressions. +pub(in crate::interpreter) fn eval_builtin_range( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [start, end] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let start = eval_expr(start, context, scope, values)?; + let end = eval_expr(end, context, scope, values)?; + eval_range_result(start, end, values) +} + +/// Builds an inclusive ascending or descending integer `range()` result. +pub(in crate::interpreter) fn eval_range_result( + start: RuntimeCellHandle, + end: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let start = eval_int_value(start, values)?; + let end = eval_int_value(end, values)?; + let distance = if start <= end { + end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? + } else { + start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? + }; + let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; + let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; + let step = if start <= end { 1_i64 } else { -1_i64 }; + let mut current = start; + let mut result = values.array_new(count)?; + + for index in 0..count { + let key = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(key)?; + let value = values.int(current)?; + result = values.array_set(result, key, value)?; + if index + 1 < count { + current = current.checked_add(step).ok_or(EvalStatus::RuntimeFatal)?; + } + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs b/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs index 92437858a1..167b567f68 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_rsort_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("rsort", values)?; - eval_array_sort_value_result(*array, values) + super::sort::eval_array_sort_value_result(*array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs b/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs index adb1549bb9..57b1ab7a49 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_shuffle_declared_values_result( ) -> Result { let [array] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("shuffle", values)?; - eval_array_sort_value_result(*array, values) + super::sort::eval_array_sort_value_result(*array, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/sort.rs b/crates/elephc-magician/src/interpreter/builtins/array/sort.rs index afadd13fd9..a3d0b512e2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/sort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/sort.rs @@ -27,3 +27,364 @@ pub(in crate::interpreter) fn eval_sort_declared_values_result( super::array_pop::eval_warn_array_by_value("sort", values)?; eval_array_sort_value_result(*array, values) } + +/// Returns the dynamic callable result for by-value array ordering calls. +pub(in crate::interpreter) fn eval_array_sort_value_result( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + values.bool_value(true) +} + +/// Sort key shape supported by eval's homogeneous array ordering implementation. +#[derive(Clone)] +pub(in crate::interpreter) enum EvalArraySortKey { + Numeric(f64), + Natural(Vec), + String(Vec), +} + +/// One source array entry plus its precomputed ordering key. +pub(in crate::interpreter) struct EvalArraySortEntry { + sort_key: EvalArraySortKey, + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for eval array ordering builtins. +pub(in crate::interpreter) fn eval_array_sort_replacement( + name: &str, + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut entries = match name { + "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, + "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, + "natsort" => eval_array_natural_sort_entries(array, false, values)?, + "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, + "shuffle" => return eval_array_shuffle_replacement(array, values), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + entries.sort_by(|left, right| { + let order = eval_array_sort_key_cmp(&left.sort_key, &right.sort_key); + if matches!(name, "arsort" | "krsort" | "rsort") { + order.reverse() + } else { + order + } + }); + + if matches!( + name, + "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" + ) { + return eval_array_preserve_key_sort_result(entries, values); + } + eval_array_reindex_sort_result(entries, values) +} + +/// Builds a shuffled, reindexed replacement array for `shuffle()`. +pub(in crate::interpreter) fn eval_array_shuffle_replacement( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + entries.push(values.array_get(array, source_key)?); + } + + for index in (1..entries.len()).rev() { + let swap_with = (eval_random_u128() % ((index + 1) as u128)) as usize; + entries.swap(index, swap_with); + } + + let mut result = values.array_new(entries.len())?; + for (index, value) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds an indexed result for `sort()` / `rsort()` after value ordering. +pub(in crate::interpreter) fn eval_array_reindex_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds a key-preserving associative result after value or key ordering. +pub(in crate::interpreter) fn eval_array_preserve_key_sort_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + +/// Collects values and comparable value-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_value_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(value, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and natural-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_natural_sort_entries( + array: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + let mut expects_numeric = None; + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_natural_sort_key(value, case_insensitive, values)?; + let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); + match expects_numeric { + Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), + Some(_) => {} + None => expects_numeric = Some(is_numeric), + } + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Collects values and comparable key-sort keys from one eval array. +pub(in crate::interpreter) fn eval_array_key_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + let sort_key = eval_array_sort_key(source_key, values)?; + entries.push(EvalArraySortEntry { + sort_key, + source_key, + value, + }); + } + + Ok(entries) +} + +/// Converts one scalar eval value into a natural-sort key. +pub(in crate::interpreter) fn eval_array_natural_sort_key( + value: RuntimeCellHandle, + case_insensitive: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } + EVAL_TAG_STRING => { + let mut bytes = values.string_bytes(value)?; + if case_insensitive { + bytes.make_ascii_lowercase(); + } + Ok(EvalArraySortKey::Natural(bytes)) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts one scalar eval value into a homogeneous sort key. +pub(in crate::interpreter) fn eval_array_sort_key( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_INT | EVAL_TAG_FLOAT => { + Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) + } + EVAL_TAG_STRING => { + let bytes = values.string_bytes(value)?; + match eval_array_numeric_string_sort_key(&bytes) { + Some(value) => Ok(EvalArraySortKey::Numeric(value)), + None => Ok(EvalArraySortKey::String(bytes)), + } + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Parses one PHP numeric string into the numeric sort domain when possible. +pub(in crate::interpreter) fn eval_array_numeric_string_sort_key(bytes: &[u8]) -> Option { + if !eval_is_numeric_string(bytes) { + return None; + } + std::str::from_utf8(bytes).ok()?.parse::().ok() +} + +/// Compares two precomputed eval sort keys. +pub(in crate::interpreter) fn eval_array_sort_key_cmp( + left: &EvalArraySortKey, + right: &EvalArraySortKey, +) -> std::cmp::Ordering { + match (left, right) { + (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { + left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) + } + (EvalArraySortKey::Natural(left), EvalArraySortKey::Natural(right)) => { + eval_natural_bytes_cmp(left, right) + } + (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), + _ => eval_array_sort_key_rank(left).cmp(&eval_array_sort_key_rank(right)), + } +} + +/// Returns a deterministic rank for mixed key-sort domains. +pub(in crate::interpreter) fn eval_array_sort_key_rank(key: &EvalArraySortKey) -> u8 { + match key { + EvalArraySortKey::Numeric(_) => 0, + EvalArraySortKey::Natural(_) => 1, + EvalArraySortKey::String(_) => 2, + } +} + +/// Compares byte strings with a small PHP-style natural ordering. +pub(in crate::interpreter) fn eval_natural_bytes_cmp( + left: &[u8], + right: &[u8], +) -> std::cmp::Ordering { + let mut left_index = 0; + let mut right_index = 0; + while left_index < left.len() && right_index < right.len() { + if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { + let order = eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); + if order != std::cmp::Ordering::Equal { + return order; + } + continue; + } + + let order = left[left_index].cmp(&right[right_index]); + if order != std::cmp::Ordering::Equal { + return order; + } + left_index += 1; + right_index += 1; + } + left.len().cmp(&right.len()) +} + +/// Compares two natural-sort digit runs and advances both byte indexes past them. +pub(in crate::interpreter) fn eval_natural_digit_run_cmp( + left: &[u8], + left_index: &mut usize, + right: &[u8], + right_index: &mut usize, +) -> std::cmp::Ordering { + let left_start = *left_index; + let right_start = *right_index; + while *left_index < left.len() && left[*left_index].is_ascii_digit() { + *left_index += 1; + } + while *right_index < right.len() && right[*right_index].is_ascii_digit() { + *right_index += 1; + } + + let left_digits = &left[left_start..*left_index]; + let right_digits = &right[right_start..*right_index]; + let left_trimmed = eval_trim_leading_zeroes(left_digits); + let right_trimmed = eval_trim_leading_zeroes(right_digits); + left_trimmed + .len() + .cmp(&right_trimmed.len()) + .then_with(|| left_trimmed.cmp(right_trimmed)) + .then_with(|| left_digits.len().cmp(&right_digits.len())) +} + +/// Drops leading zero bytes while keeping one zero for an all-zero digit run. +pub(in crate::interpreter) fn eval_trim_leading_zeroes(digits: &[u8]) -> &[u8] { + let trimmed = digits + .iter() + .position(|digit| *digit != b'0') + .map_or(&digits[digits.len().saturating_sub(1)..], |index| { + &digits[index..] + }); + if trimmed.is_empty() { + digits + } else { + trimmed + } +} + +/// Evaluates direct by-reference array ordering calls and writes back the array. +pub(in crate::interpreter) fn eval_array_sort_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, target) = eval_array_sort_direct_arg(args, context, scope, values)?; + + let replacement = eval_array_sort_replacement(name, array, values)?; + let result = values.bool_value(true)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(result) +} + +/// Extracts the writable array lvalue accepted by eval array ordering builtins. +pub(in crate::interpreter) fn eval_array_sort_direct_arg( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + super::mutation::eval_array_mutation_lvalue_arg(arg, context, scope, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs b/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs index fde130a187..c197e70c9a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_uasort_declared_values_result( ) -> Result { let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("uasort", values)?; - eval_user_sort_value_result("uasort", *array, *callback, context, values) + super::usort::eval_user_sort_value_result("uasort", *array, *callback, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs b/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs index ddab3f019d..b66dff2a97 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs @@ -25,5 +25,5 @@ pub(in crate::interpreter) fn eval_uksort_declared_values_result( ) -> Result { let [array, callback] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); }; super::array_pop::eval_warn_array_by_value("uksort", values)?; - eval_user_sort_value_result("uksort", *array, *callback, context, values) + super::usort::eval_user_sort_value_result("uksort", *array, *callback, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/array/usort.rs b/crates/elephc-magician/src/interpreter/builtins/array/usort.rs index 51a8e02480..945e9b37ed 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/usort.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/usort.rs @@ -27,3 +27,218 @@ pub(in crate::interpreter) fn eval_usort_declared_values_result( super::array_pop::eval_warn_array_by_value("usort", values)?; eval_user_sort_value_result("usort", *array, *callback, context, values) } + +/// Returns the dynamic callable result for by-value user-comparator sort calls. +pub(in crate::interpreter) fn eval_user_sort_value_result( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::RuntimeFatal); + } + let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; + values.release(replacement)?; + values.bool_value(true) +} + +/// One source array entry used by eval user-comparator sort routines. +pub(in crate::interpreter) struct EvalUserSortEntry { + source_key: RuntimeCellHandle, + value: RuntimeCellHandle, +} + +/// Builds the sorted replacement array for user-comparator sort builtins. +pub(in crate::interpreter) fn eval_user_sort_replacement( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_user_sort_replacement_from_scope(name, array, callback, None, context, values) +} + +/// Builds the sorted replacement array with optional lexical scope for callback names. +pub(in crate::interpreter) fn eval_user_sort_replacement_from_scope( + name: &str, + array: RuntimeCellHandle, + callback: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; + let mut entries = eval_user_sort_entries(array, values)?; + eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; + if name == "usort" { + return eval_user_sort_reindex_result(entries, values); + } + eval_user_sort_preserve_key_result(entries, values) +} + +/// Collects source keys and values from one eval array for user sorting. +pub(in crate::interpreter) fn eval_user_sort_entries( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut entries = Vec::with_capacity(len); + for position in 0..len { + let source_key = values.array_iter_key(array, position)?; + let value = values.array_get(array, source_key)?; + entries.push(EvalUserSortEntry { source_key, value }); + } + Ok(entries) +} + +/// Sorts entries by repeatedly invoking the PHP comparator callback. +pub(in crate::interpreter) fn eval_user_sort_entries_in_place( + name: &str, + callback: &EvaluatedCallable, + entries: &mut [EvalUserSortEntry], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for pass in 0..entries.len() { + let upper = entries.len().saturating_sub(pass + 1); + for index in 0..upper { + let comparison = eval_user_sort_compare( + name, + callback, + &entries[index], + &entries[index + 1], + context, + values, + )?; + if comparison > 0 { + entries.swap(index, index + 1); + } + } + } + Ok(()) +} + +/// Invokes one user-sort comparator and returns its integer ordering result. +pub(in crate::interpreter) fn eval_user_sort_compare( + name: &str, + callback: &EvaluatedCallable, + left: &EvalUserSortEntry, + right: &EvalUserSortEntry, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = if name == "uksort" { + vec![left.source_key, right.source_key] + } else { + vec![left.value, right.value] + }; + let result = eval_evaluated_callable_with_values(callback, args, context, values)?; + eval_int_value(result, values) +} + +/// Builds the reindexed result for `usort()`. +pub(in crate::interpreter) fn eval_user_sort_reindex_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(entries.len())?; + for (index, entry) in entries.into_iter().enumerate() { + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, entry.value)?; + } + Ok(result) +} + +/// Builds the key-preserving result for `uksort()` and `uasort()`. +pub(in crate::interpreter) fn eval_user_sort_preserve_key_result( + entries: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(entries.len())?; + for entry in entries { + result = values.array_set(result, entry.source_key, entry.value)?; + } + Ok(result) +} + +/// Evaluates direct by-reference user-comparator sort calls and writes back the array. +pub(in crate::interpreter) fn eval_user_sort_declared_call( + name: &str, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (array, target, callback) = eval_user_sort_direct_args(args, context, scope, values)?; + + let replacement = eval_user_sort_replacement_from_scope( + name, + array, + callback, + Some(scope), + context, + values, + )?; + let result = values.bool_value(true)?; + eval_write_direct_ref_target(&target, replacement, context, values, None)?; + Ok(result) +} + +/// Evaluates and binds direct user-sort arguments while preserving source order. +pub(in crate::interpreter) fn eval_user_sort_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { + let mut array = None; + let mut callback = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "array", + 1 => "callback", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "array" => { + if array.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + array = Some(super::mutation::eval_array_mutation_lvalue_arg( + arg, context, scope, values, + )?); + } + "callback" => { + if callback.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + callback = Some(eval_expr(arg.value(), context, scope, values)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let (array, target) = array.ok_or(EvalStatus::RuntimeFatal)?; + let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; + Ok((array, target, callback)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs deleted file mode 100644 index 87c933f0e7..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/access.rs +++ /dev/null @@ -1,315 +0,0 @@ -//! Purpose: -//! Array key lookup, search, random, range, merge, explode, and implode helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! -//! Key details: -//! - Array cells remain opaque runtime handles and are manipulated through -//! `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP `array_key_exists()` over a key and array expression. -pub(in crate::interpreter) fn eval_builtin_array_key_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [key, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let key = eval_expr(key, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - values.array_key_exists(key, array) -} - -/// Evaluates PHP array search builtins over a needle and haystack expression. -pub(in crate::interpreter) fn eval_builtin_array_search( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [needle, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let needle = eval_expr(needle, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_array_search_result(name, needle, array, values) -} - -/// Searches an eval array with PHP's default loose comparison semantics. -pub(in crate::interpreter) fn eval_array_search_result( - name: &str, - needle: RuntimeCellHandle, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let equal = values.compare(EvalBinOp::LooseEq, needle, value)?; - if values.truthy(equal)? { - return match name { - "in_array" => values.bool_value(true), - "array_search" => Ok(key), - _ => Err(EvalStatus::UnsupportedConstruct), - }; - } - } - match name { - "in_array" => values.bool_value(false), - "array_search" => values.bool_value(false), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP value-set array builtins over two eval array expressions. -pub(in crate::interpreter) fn eval_builtin_array_value_set( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_value_set_result(name, left, right, values) -} - -/// Builds `array_diff()` or `array_intersect()` using PHP's default string comparison mode. -pub(in crate::interpreter) fn eval_array_value_set_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let right_len = values.array_len(right)?; - let mut right_values = Vec::with_capacity(right_len); - for position in 0..right_len { - let key = values.array_iter_key(right, position)?; - let value = values.array_get(right, key)?; - right_values.push(values.string_bytes(value)?); - } - - let mut result = values.assoc_new(left_len)?; - for position in 0..left_len { - let key = values.array_iter_key(left, position)?; - let value = values.array_get(left, key)?; - let comparable = values.string_bytes(value)?; - let found = right_values - .iter() - .any(|right_value| right_value == &comparable); - let keep = match name { - "array_diff" => !found, - "array_intersect" => found, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Evaluates PHP key-set array builtins over two eval array expressions. -pub(in crate::interpreter) fn eval_builtin_array_key_set( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_key_set_result(name, left, right, values) -} - -/// Builds `array_diff_key()` or `array_intersect_key()` by testing first-array keys. -pub(in crate::interpreter) fn eval_array_key_set_result( - name: &str, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let mut result = values.assoc_new(left_len)?; - for position in 0..left_len { - let key = values.array_iter_key(left, position)?; - let value = values.array_get(left, key)?; - let exists = values.array_key_exists(key, right)?; - let found = values.truthy(exists)?; - let keep = match name { - "array_diff_key" => !found, - "array_intersect_key" => found, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Evaluates PHP `array_rand()` over one eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_rand( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_rand_result(array, values) -} - -/// Returns a valid random key from a non-empty eval array. -pub(in crate::interpreter) fn eval_array_rand_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - if len == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let position = eval_random_position(len); - values.array_iter_key(array, position) -} - -/// Chooses a pseudo-random array position within `[0, len)`. -pub(in crate::interpreter) fn eval_random_position(len: usize) -> usize { - (eval_random_u128() % (len as u128)) as usize -} - -/// Produces a process-local pseudo-random word for non-cryptographic eval builtins. -pub(in crate::interpreter) fn eval_random_u128() -> u128 { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - let counter = u128::from(EVAL_RANDOM_COUNTER.fetch_add(1, Ordering::Relaxed)); - let pid = u128::from(std::process::id()); - let mut value = nanos ^ (counter.wrapping_mul(0x9e37_79b9_7f4a_7c15)) ^ (pid << 64); - value ^= value >> 30; - value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); - value ^= value >> 27; - value = value.wrapping_mul(0x94d0_49bb_1331_11eb); - value ^ (value >> 31) -} - -/// Evaluates PHP `range()` over integer-compatible start and end expressions. -pub(in crate::interpreter) fn eval_builtin_range( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [start, end] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let start = eval_expr(start, context, scope, values)?; - let end = eval_expr(end, context, scope, values)?; - eval_range_result(start, end, values) -} - -/// Builds an inclusive ascending or descending integer `range()` result. -pub(in crate::interpreter) fn eval_range_result( - start: RuntimeCellHandle, - end: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let start = eval_int_value(start, values)?; - let end = eval_int_value(end, values)?; - let distance = if start <= end { - end.checked_sub(start).ok_or(EvalStatus::RuntimeFatal)? - } else { - start.checked_sub(end).ok_or(EvalStatus::RuntimeFatal)? - }; - let count = distance.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; - let step = if start <= end { 1_i64 } else { -1_i64 }; - let mut current = start; - let mut result = values.array_new(count)?; - - for index in 0..count { - let key = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - let value = values.int(current)?; - result = values.array_set(result, key, value)?; - if index + 1 < count { - current = current.checked_add(step).ok_or(EvalStatus::RuntimeFatal)?; - } - } - Ok(result) -} - -/// Evaluates PHP `array_merge()` over two array expressions. -pub(in crate::interpreter) fn eval_builtin_array_merge( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [left, right] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let left = eval_expr(left, context, scope, values)?; - let right = eval_expr(right, context, scope, values)?; - eval_array_merge_result(left, right, values) -} - -/// Builds an `array_merge()` result with PHP numeric reindexing and string-key overwrites. -pub(in crate::interpreter) fn eval_array_merge_result( - left: RuntimeCellHandle, - right: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let left_len = values.array_len(left)?; - let right_len = values.array_len(right)?; - let capacity = left_len - .checked_add(right_len) - .ok_or(EvalStatus::RuntimeFatal)?; - let mut result = values.assoc_new(capacity)?; - let mut next_numeric_key = 0_i64; - result = eval_array_merge_append_operand(result, left, &mut next_numeric_key, values)?; - eval_array_merge_append_operand(result, right, &mut next_numeric_key, values) -} - -/// Appends one source array to an `array_merge()` result using PHP key handling. -pub(in crate::interpreter) fn eval_array_merge_append_operand( - mut result: RuntimeCellHandle, - source: RuntimeCellHandle, - next_numeric_key: &mut i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(source)?; - for position in 0..len { - let source_key = values.array_iter_key(source, position)?; - let source_value = values.array_get(source, source_key)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_STRING { - source_key - } else { - let target_key = values.int(*next_numeric_key)?; - *next_numeric_key = (*next_numeric_key) - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - target_key - }; - result = values.array_set(result, target_key, source_value)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/callbacks.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/callbacks.rs deleted file mode 100644 index 62e13589fd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/callbacks.rs +++ /dev/null @@ -1,347 +0,0 @@ -//! Purpose: -//! Callback-driven array builtins such as `array_map`, `array_reduce`, and -//! `array_walk`. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! - Declarative array builtin dispatch hooks. -//! -//! Key details: -//! - Direct calls preserve lexical scope for callback string resolution. -//! - `array_walk` keeps writable array element targets for by-reference callback -//! parameters. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP `array_map()` for one or more arrays and an optional callback. -pub(in crate::interpreter) fn eval_builtin_array_map( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((callback, arrays)) = args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let callback = eval_expr(callback, context, scope, values)?; - let mut evaluated_arrays = Vec::with_capacity(arrays.len()); - for array in arrays { - evaluated_arrays.push(eval_expr(array, context, scope, values)?); - } - eval_array_map_result_from_scope(callback, &evaluated_arrays, Some(scope), context, values) -} - -/// Maps one eval array with PHP key preservation for the one-array form. -pub(in crate::interpreter) fn eval_array_map_result( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_map_result_from_scope(callback, arrays, None, context, values) -} - -/// Maps one or more eval arrays with optional lexical scope for callback names. -fn eval_array_map_result_from_scope( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = arrays else { - return eval_array_map_variadic_result_from_scope( - callback, - arrays, - lexical_scope, - context, - values, - ); - }; - let callback = if values.is_null(callback)? { - None - } else { - Some(eval_callable_with_optional_scope( - callback, - context, - lexical_scope, - values, - )?) - }; - let len = values.array_len(*array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(*array, position)?; - let value = values.array_get(*array, key)?; - let mapped = if let Some(callback) = callback.as_ref() { - eval_evaluated_callable_with_values(callback, vec![value], context, values)? - } else { - value - }; - result = values.array_set(result, key, mapped)?; - } - Ok(result) -} - -/// Maps multiple eval arrays with optional lexical scope for callback names. -fn eval_array_map_variadic_result_from_scope( - callback: RuntimeCellHandle, - arrays: &[RuntimeCellHandle], - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if arrays.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let callback = if values.is_null(callback)? { - None - } else { - Some(eval_callable_with_optional_scope( - callback, - context, - lexical_scope, - values, - )?) - }; - let mut lengths = Vec::with_capacity(arrays.len()); - let mut max_len = 0; - for array in arrays { - let len = values.array_len(*array)?; - max_len = max_len.max(len); - lengths.push(len); - } - - let mut result = values.array_new(max_len)?; - for position in 0..max_len { - let mut callback_args = Vec::with_capacity(arrays.len()); - for (array, len) in arrays.iter().zip(lengths.iter()) { - let value = if position < *len { - let key = values.array_iter_key(*array, position)?; - values.array_get(*array, key)? - } else { - values.null()? - }; - callback_args.push(value); - } - let mapped = if let Some(callback) = callback.as_ref() { - eval_evaluated_callable_with_values(callback, callback_args, context, values)? - } else { - eval_array_map_zipped_row(callback_args, values)? - }; - let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, mapped)?; - } - Ok(result) -} - -/// Builds one row for `array_map(null, $a, $b, ...)`. -pub(in crate::interpreter) fn eval_array_map_zipped_row( - values_row: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut row = values.array_new(values_row.len())?; - for (index, value) in values_row.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - row = values.array_set(row, key, value)?; - } - Ok(row) -} - -/// Evaluates PHP `array_reduce()` with an optional initial carry value. -pub(in crate::interpreter) fn eval_builtin_array_reduce( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, callback, initial) = match args { - [array, callback] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - (array, callback, values.null()?) - } - [array, callback, initial] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let initial = eval_expr(initial, context, scope, values)?; - (array, callback, initial) - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_array_reduce_result_from_scope(array, callback, initial, Some(scope), context, values) -} - -/// Reduces one eval array by invoking a callable with carry and item cells. -pub(in crate::interpreter) fn eval_array_reduce_result( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - initial: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_reduce_result_from_scope(array, callback, initial, None, context, values) -} - -/// Reduces one eval array with optional lexical scope for callback names. -fn eval_array_reduce_result_from_scope( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - initial: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; - let len = values.array_len(array)?; - let mut carry = initial; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - carry = - eval_evaluated_callable_with_values(&callback, vec![carry, value], context, values)?; - } - Ok(carry) -} - -/// Evaluates direct PHP `array_walk()` calls and preserves element by-ref targets. -pub(in crate::interpreter) fn eval_builtin_array_walk_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, array_target, callback) = - eval_array_walk_direct_args(args, context, scope, values)?; - eval_array_walk_ref_result_from_scope(array, array_target, callback, Some(scope), context, values) -} - -/// Evaluates and binds direct `array_walk()` arguments in PHP source order. -fn eval_array_walk_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { - let mut array_target = None; - let mut callback = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "callback", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array_target.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - array_target = Some(eval_array_mutation_lvalue_arg(arg, context, scope, values)?); - } - "callback" => { - if callback.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - callback = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let (array, array_target) = array_target.ok_or(EvalStatus::RuntimeFatal)?; - let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, array_target, callback)) -} - -/// Walks one writable eval array by invoking a callable with element ref targets. -pub(in crate::interpreter) fn eval_array_walk_ref_result( - array: RuntimeCellHandle, - array_target: EvalReferenceTarget, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_walk_ref_result_from_scope(array, array_target, callback, None, context, values) -} - -/// Walks one writable eval array with optional lexical scope for callback names. -fn eval_array_walk_ref_result_from_scope( - array: RuntimeCellHandle, - array_target: EvalReferenceTarget, - callback: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; - let len = values.array_len(array)?; - for position in 0..len { - let current_array = eval_reference_target_value(&array_target, context, values)?; - let key = values.array_iter_key(current_array, position)?; - let value = values.array_get(current_array, key)?; - let ref_target = EvalReferenceTarget::NestedArrayElement { - array_target: Box::new(array_target.clone()), - index: key, - }; - let args = vec![ - EvaluatedCallArg { - name: None, - value, - ref_target: Some(ref_target), - }, - EvaluatedCallArg { - name: None, - value: key, - ref_target: None, - }, - ]; - let _ = eval_evaluated_callable_with_call_array_args(&callback, args, context, values)?; - } - values.bool_value(true) -} - -/// Walks one eval array by invoking a callable with value and key cells. -pub(in crate::interpreter) fn eval_array_walk_result( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_walk_result_from_scope(array, callback, None, context, values) -} - -/// Walks one eval array with optional lexical scope for callback names. -fn eval_array_walk_result_from_scope( - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let _ = eval_evaluated_callable_with_values(&callback, vec![value, key], context, values)?; - } - values.bool_value(true) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs deleted file mode 100644 index 026fae0999..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/core.rs +++ /dev/null @@ -1,207 +0,0 @@ -//! Purpose: -//! Core non-mutating array builtins such as aggregate, fill, map, reduce, and walk. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! -//! Key details: -//! - Array cells remain opaque runtime handles and are manipulated through -//! `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Evaluates PHP array aggregate builtins over one eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_aggregate( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_aggregate_result(name, array, values) -} - -/// Computes `array_sum()` or `array_product()` through eval's numeric value hooks. -pub(in crate::interpreter) fn eval_array_aggregate_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = match name { - "array_sum" => values.int(0)?, - "array_product" => values.int(1)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - result = match name { - "array_sum" => values.add(result, value)?, - "array_product" => values.mul(result, value)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - } - Ok(result) -} - -/// Evaluates PHP `array_combine()` over key and value array expressions. -pub(in crate::interpreter) fn eval_builtin_array_combine( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [keys, values_array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let keys = eval_expr(keys, context, scope, values)?; - let values_array = eval_expr(values_array, context, scope, values)?; - eval_array_combine_result(keys, values_array, values) -} - -/// Builds the associative result for `array_combine()` from two eval arrays. -pub(in crate::interpreter) fn eval_array_combine_result( - keys: RuntimeCellHandle, - values_array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(keys)?; - if len != values.array_len(values_array)? { - return Err(EvalStatus::RuntimeFatal); - } - - let mut result = values.assoc_new(len)?; - for position in 0..len { - let source_key = values.array_iter_key(keys, position)?; - let target_key = values.array_get(keys, source_key)?; - let target_key = values.cast_string(target_key)?; - let value_key = values.array_iter_key(values_array, position)?; - let value = values.array_get(values_array, value_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_column()` over row-array and column-key expressions. -pub(in crate::interpreter) fn eval_builtin_array_column( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, column_key] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let column_key = eval_expr(column_key, context, scope, values)?; - eval_array_column_result(array, column_key, values) -} - -/// Builds `array_column()` by extracting present row columns into a reindexed array. -pub(in crate::interpreter) fn eval_array_column_result( - array: RuntimeCellHandle, - column_key: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len)?; - let mut output_index = 0_i64; - for position in 0..len { - let row_key = values.array_iter_key(array, position)?; - let row = values.array_get(array, row_key)?; - if !matches!(values.type_tag(row)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - continue; - } - let exists = values.array_key_exists(column_key, row)?; - if !values.truthy(exists)? { - continue; - } - let column = values.array_get(row, column_key)?; - let target_key = values.int(output_index)?; - output_index = output_index - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, column)?; - } - Ok(result) -} - -/// Evaluates PHP `array_fill()` over start, count, and value expressions. -pub(in crate::interpreter) fn eval_builtin_array_fill( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [start, count, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let start = eval_expr(start, context, scope, values)?; - let count = eval_expr(count, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_fill_result(start, count, value, values) -} - -/// Builds an `array_fill()` result with PHP's explicit integer key range. -pub(in crate::interpreter) fn eval_array_fill_result( - start: RuntimeCellHandle, - count: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let start = eval_int_value(start, values)?; - let count = eval_int_value(count, values)?; - if count < 0 { - return Err(EvalStatus::RuntimeFatal); - } - let count = usize::try_from(count).map_err(|_| EvalStatus::RuntimeFatal)?; - let mut result = if start == 0 { - values.array_new(count)? - } else { - values.assoc_new(count)? - }; - for offset in 0..count { - let offset = i64::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = start.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates PHP `array_fill_keys()` over key-array and value expressions. -pub(in crate::interpreter) fn eval_builtin_array_fill_keys( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [keys, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let keys = eval_expr(keys, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_fill_keys_result(keys, value, values) -} - -/// Builds an `array_fill_keys()` result preserving the source key iteration order. -pub(in crate::interpreter) fn eval_array_fill_keys_result( - keys: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(keys)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let source_key = values.array_iter_key(keys, position)?; - let target_key = values.array_get(keys, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/chunk.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/chunk.rs deleted file mode 100644 index b6c558f309..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/chunk.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Purpose: -//! Implements PHP `array_chunk()` eval support. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` re-exports. -//! -//! Key details: -//! - Chunks are reindexed arrays and invalid sizes produce runtime fatal status. - -use super::super::super::super::*; -use super::super::super::*; - -/// Evaluates PHP `array_chunk()` over one array and chunk-size expression. -pub(in crate::interpreter) fn eval_builtin_array_chunk( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, length] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_chunk_result(array, length, values) -} - -/// Builds an `array_chunk()` result as nested reindexed arrays. -pub(in crate::interpreter) fn eval_array_chunk_result( - array: RuntimeCellHandle, - length: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let chunk_size = eval_int_value(length, values)?; - if chunk_size <= 0 { - return Err(EvalStatus::RuntimeFatal); - } - let chunk_size = usize::try_from(chunk_size).map_err(|_| EvalStatus::RuntimeFatal)?; - let len = values.array_len(array)?; - let chunk_count = len.div_ceil(chunk_size); - let mut result = values.array_new(chunk_count)?; - - for chunk_index in 0..chunk_count { - let start = chunk_index * chunk_size; - let end = usize::min(start + chunk_size, len); - let mut chunk = values.array_new(end - start)?; - for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let value = values.array_get(array, source_key)?; - let target_index = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_index = values.int(target_index)?; - chunk = values.array_set(chunk, target_index, value)?; - } - let result_key = i64::try_from(chunk_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let result_key = values.int(result_key)?; - result = values.array_set(result, result_key, chunk)?; - } - - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs deleted file mode 100644 index c90dcd2736..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/filter.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! Purpose: -//! Implements PHP `array_filter()` eval support. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` re-exports. -//! -//! Key details: -//! - Callback argument shape follows the supported filter mode and null callbacks -//! use PHP truthiness. - -use super::super::super::super::*; -use super::super::super::*; - -/// Evaluates PHP `array_filter()` for null and callable filtering modes. -pub(in crate::interpreter) fn eval_builtin_array_filter( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array] => { - let array = eval_expr(array, context, scope, values)?; - eval_array_filter_result_from_scope(array, None, None, Some(scope), context, values) - } - [array, callback] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - eval_array_filter_result_from_scope( - array, - Some(callback), - None, - Some(scope), - context, - values, - ) - } - [array, callback, mode] => { - let array = eval_expr(array, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_array_filter_result_from_scope( - array, - Some(callback), - Some(mode), - Some(scope), - context, - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Filters eval array entries through PHP truthiness or a callable callback. -pub(in crate::interpreter) fn eval_array_filter_result( - array: RuntimeCellHandle, - callback: Option, - mode: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_array_filter_result_from_scope(array, callback, mode, None, context, values) -} - -/// Filters eval array entries with optional lexical scope for callback names. -fn eval_array_filter_result_from_scope( - array: RuntimeCellHandle, - callback: Option, - mode: Option, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = match callback { - Some(callback) if !values.is_null(callback)? => { - Some(eval_callable_with_optional_scope( - callback, - context, - lexical_scope, - values, - )?) - } - _ => None, - }; - let mode = match mode { - Some(mode) => eval_array_filter_mode_value(mode, values)?, - None => EVAL_ARRAY_FILTER_USE_VALUE, - }; - - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let keep = if let Some(callback) = callback.as_ref() { - let args = eval_array_filter_callback_args(mode, key, value)?; - let result = eval_evaluated_callable_with_values(callback, args, context, values)?; - values.truthy(result)? - } else { - values.truthy(value)? - }; - if keep { - result = values.array_set(result, key, value)?; - } - } - Ok(result) -} - -/// Reads and validates the optional `array_filter()` callback mode. -pub(in crate::interpreter) fn eval_array_filter_mode_value( - mode: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = eval_int_value(mode, values)?; - match mode { - EVAL_ARRAY_FILTER_USE_VALUE | EVAL_ARRAY_FILTER_USE_BOTH | EVAL_ARRAY_FILTER_USE_KEY => { - Ok(mode) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds the callback argument list for one `array_filter()` entry. -pub(in crate::interpreter) fn eval_array_filter_callback_args( - mode: i64, - key: RuntimeCellHandle, - value: RuntimeCellHandle, -) -> Result, EvalStatus> { - match mode { - EVAL_ARRAY_FILTER_USE_VALUE => Ok(vec![value]), - EVAL_ARRAY_FILTER_USE_BOTH => Ok(vec![value, key]), - EVAL_ARRAY_FILTER_USE_KEY => Ok(vec![key]), - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/flip.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/flip.rs deleted file mode 100644 index 895ca9532f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/flip.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Purpose: -//! Implements PHP `array_flip()` eval support. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` re-exports. -//! -//! Key details: -//! - Only PHP-valid scalar key values are inserted into the flipped result. - -use super::super::super::super::*; - -/// Evaluates PHP `array_flip()` over one eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_flip( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_flip_result(array, values) -} - -/// Builds the associative result for `array_flip()` using PHP's valid value-key subset. -pub(in crate::interpreter) fn eval_array_flip_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - if !matches!(values.type_tag(value)?, EVAL_TAG_INT | EVAL_TAG_STRING) { - continue; - } - result = values.array_set(result, value, key)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs deleted file mode 100644 index 901e643c8d..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/iterator.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! Purpose: -//! Implements iterator-related array eval builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` re-exports. -//! -//! Key details: -//! - Iterator objects are driven through `rewind`, `valid`, callback, and `next` -//! calls in PHP-observable order. - -use super::super::super::super::*; -use super::super::super::*; - -/// Evaluates PHP `iterator_apply()` for eval-supported Traversable object inputs. -pub(in crate::interpreter) fn eval_builtin_iterator_apply( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [iterator, callback] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable_from_scope(callback, context, scope, values)?; - eval_iterator_apply_result(iterator, &callback, Vec::new(), context, values) - } - [iterator, callback, callback_args] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let callback = eval_expr(callback, context, scope, values)?; - let callback = eval_callable_from_scope(callback, context, scope, values)?; - let callback_args = eval_expr(callback_args, context, scope, values)?; - let callback_args = eval_iterator_apply_arg_values(callback_args, context, values)?; - eval_iterator_apply_result(iterator, &callback, callback_args, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts the optional `iterator_apply()` callback-args value into call arguments. -pub(in crate::interpreter) fn eval_iterator_apply_arg_values( - args: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.is_null(args)? { - return Ok(Vec::new()); - } - if !values.is_array_like(args)? { - return Err(EvalStatus::RuntimeFatal); - } - eval_array_call_arg_values(args, context, values) -} - -/// Applies a callback to each valid position of an eval-supported Traversable object. -pub(in crate::interpreter) fn eval_iterator_apply_result( - iterator: RuntimeCellHandle, - callback: &EvaluatedCallable, - callback_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(iterator)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let count = match eval_iterator_apply_iterator_object( - iterator, - callback, - &callback_args, - context, - values, - ) { - Ok(count) => count, - Err(EvalStatus::UnsupportedConstruct) => { - let iterator = values.method_call(iterator, "getiterator", Vec::new())?; - eval_iterator_apply_iterator_object( - iterator, - callback, - &callback_args, - context, - values, - )? - } - Err(err) => return Err(err), - }; - values.int(count) -} - -/// Drives one Iterator object through `rewind()`, `valid()`, callback, and `next()`. -pub(in crate::interpreter) fn eval_iterator_apply_iterator_object( - iterator: RuntimeCellHandle, - callback: &EvaluatedCallable, - callback_args: &[EvaluatedCallArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let _ = values.method_call(iterator, "rewind", Vec::new())?; - let mut count = 0_i64; - loop { - let valid = values.method_call(iterator, "valid", Vec::new())?; - if !values.truthy(valid)? { - return Ok(count); - } - count = count.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - let result = eval_evaluated_callable_with_call_array_args( - callback, - callback_args.to_vec(), - context, - values, - )?; - if !values.truthy(result)? { - return Ok(count); - } - let _ = values.method_call(iterator, "next", Vec::new())?; - } -} - -/// Evaluates PHP `iterator_count()` for eval-supported array iterator inputs. -pub(in crate::interpreter) fn eval_builtin_iterator_count( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [iterator] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let iterator = eval_expr(iterator, context, scope, values)?; - eval_iterator_count_result(iterator, values) -} - -/// Returns the element count for eval-supported array iterator inputs. -pub(in crate::interpreter) fn eval_iterator_count_result( - iterator: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(iterator)?; - values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `iterator_to_array()` for eval-supported array iterator inputs. -pub(in crate::interpreter) fn eval_builtin_iterator_to_array( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [iterator] => { - let iterator = eval_expr(iterator, context, scope, values)?; - eval_iterator_to_array_result(iterator, true, values) - } - [iterator, preserve_keys] => { - let iterator = eval_expr(iterator, context, scope, values)?; - let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; - let preserve_keys = values.truthy(preserve_keys)?; - eval_iterator_to_array_result(iterator, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Copies eval-supported array iterator inputs into a PHP array result. -pub(in crate::interpreter) fn eval_iterator_to_array_result( - iterator: RuntimeCellHandle, - preserve_keys: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(iterator)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - if preserve_keys { - return eval_array_copy_preserve_keys(iterator, values); - } - eval_array_values_result(iterator, values) -} - -/// Copies one array-like eval value while preserving iteration keys and order. -pub(in crate::interpreter) fn eval_array_copy_preserve_keys( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs deleted file mode 100644 index 04f2db2970..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Purpose: -//! Groups array filtering, slicing, projection, iterator, and reversal eval -//! builtins by focused operation family. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! -//! Key details: -//! - Helpers preserve PHP key normalization and iteration order through -//! `RuntimeValueOps`. - -mod chunk; -mod filter; -mod flip; -mod iterator; -mod pad; -mod reverse; -mod slice; -mod unique; - -pub(in crate::interpreter) use chunk::*; -pub(in crate::interpreter) use filter::*; -pub(in crate::interpreter) use flip::*; -pub(in crate::interpreter) use iterator::*; -pub(in crate::interpreter) use pad::*; -pub(in crate::interpreter) use reverse::*; -pub(in crate::interpreter) use slice::*; -pub(in crate::interpreter) use unique::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/pad.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/pad.rs deleted file mode 100644 index f8e4e487a0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/pad.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Purpose: -//! Implements PHP `array_pad()` eval support. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` re-exports. -//! -//! Key details: -//! - Padding can be prepended or appended while preserving copied source values. - -use super::super::super::super::*; -use super::super::super::*; - -/// Evaluates PHP `array_pad()` over array, target length, and pad value expressions. -pub(in crate::interpreter) fn eval_builtin_array_pad( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array, length, value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_array_pad_result(array, length, value, values) -} - -/// Builds an `array_pad()` result by copying values and padding left or right. -pub(in crate::interpreter) fn eval_array_pad_result( - array: RuntimeCellHandle, - length: RuntimeCellHandle, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let target = eval_int_value(length, values)?; - let target_len = target - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - let result_len = usize::max(len, target_len); - let pad_count = result_len.saturating_sub(len); - let mut result = values.array_new(result_len)?; - let mut output_index = 0usize; - - if target < 0 { - let (padded, next_index) = - eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?; - result = padded; - output_index = next_index; - } - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = i64::try_from(output_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; - result = values.array_set(result, target_key, source_value)?; - output_index += 1; - } - - if target > 0 { - result = eval_array_pad_append_repeated(result, output_index, pad_count, value, values)?.0; - } - - Ok(result) -} - -/// Appends the same pad value at consecutive indexed positions in an array result. -pub(in crate::interpreter) fn eval_array_pad_append_repeated( - mut array: RuntimeCellHandle, - start_index: usize, - count: usize, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, usize), EvalStatus> { - let mut next_index = start_index; - for _ in 0..count { - let key = i64::try_from(next_index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(key)?; - array = values.array_set(array, key, value)?; - next_index += 1; - } - Ok((array, next_index)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/reverse.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/reverse.rs deleted file mode 100644 index d94d5e8954..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/reverse.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Purpose: -//! Implements PHP `array_reverse()` eval support. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` re-exports. -//! -//! Key details: -//! - Key preservation follows PHP rules for string and integer keys. - -use super::super::super::super::*; - -/// Evaluates PHP `array_reverse()` over an eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_reverse( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array] => { - let array = eval_expr(array, context, scope, values)?; - eval_array_reverse_result(array, false, values) - } - [array, preserve_keys] => { - let array = eval_expr(array, context, scope, values)?; - let preserve_keys = eval_expr(preserve_keys, context, scope, values)?; - let preserve_keys = values.truthy(preserve_keys)?; - eval_array_reverse_result(array, preserve_keys, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds an `array_reverse()` result while preserving PHP key rules. -pub(in crate::interpreter) fn eval_array_reverse_result( - array: RuntimeCellHandle, - preserve_keys: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut keys = Vec::with_capacity(len); - let mut has_string_key = false; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - has_string_key |= values.type_tag(key)? == EVAL_TAG_STRING; - keys.push(key); - } - - let mut result = if preserve_keys || has_string_key { - values.assoc_new(len)? - } else { - values.array_new(len)? - }; - let mut next_numeric_key = 0_i64; - - for key in keys.into_iter().rev() { - let value = values.array_get(array, key)?; - let target_key = if preserve_keys || values.type_tag(key)? == EVAL_TAG_STRING { - key - } else { - let key = values.int(next_numeric_key)?; - next_numeric_key += 1; - key - }; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/slice.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/slice.rs deleted file mode 100644 index bfc28d11b0..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/slice.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Purpose: -//! Implements PHP `array_slice()` eval support and shared slice-bound helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` and splice helpers. -//! -//! Key details: -//! - Negative offsets and lengths are normalized to bounded source positions. - -use super::super::super::super::*; -use super::super::super::*; - -/// Evaluates PHP `array_slice()` over array, offset, and optional length expressions. -pub(in crate::interpreter) fn eval_builtin_array_slice( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [array, offset] => { - let array = eval_expr(array, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - eval_array_slice_result(array, offset, None, values) - } - [array, offset, length] => { - let array = eval_expr(array, context, scope, values)?; - let offset = eval_expr(offset, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_array_slice_result(array, offset, Some(length), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds an `array_slice()` result with PHP offset and length bounds. -pub(in crate::interpreter) fn eval_array_slice_result( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let offset = eval_int_value(offset, values)?; - let start = eval_slice_start(len, offset)?; - let end = match length { - Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { - eval_slice_end(len, start, eval_int_value(length, values)?)? - } - _ => len, - }; - - let mut result = values.array_new(end.saturating_sub(start))?; - for source_position in start..end { - let source_key = values.array_iter_key(array, source_position)?; - let source_value = values.array_get(array, source_key)?; - let target_key = - i64::try_from(source_position - start).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_key = values.int(target_key)?; - result = values.array_set(result, target_key, source_value)?; - } - Ok(result) -} - -/// Converts a PHP array-slice offset into a bounded source position. -pub(in crate::interpreter) fn eval_slice_start( - len: usize, - offset: i64, -) -> Result { - if offset >= 0 { - let offset = usize::try_from(offset).map_err(|_| EvalStatus::RuntimeFatal)?; - return Ok(usize::min(offset, len)); - } - - let tail = offset - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - Ok(len.saturating_sub(tail)) -} - -/// Converts a PHP array-slice length into a bounded exclusive end position. -pub(in crate::interpreter) fn eval_slice_end( - len: usize, - start: usize, - length: i64, -) -> Result { - if length >= 0 { - let length = usize::try_from(length).map_err(|_| EvalStatus::RuntimeFatal)?; - return Ok(usize::min(start.saturating_add(length), len)); - } - - let tail = length - .checked_abs() - .ok_or(EvalStatus::RuntimeFatal) - .and_then(|value| usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal))?; - Ok(usize::max(start, len.saturating_sub(tail))) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/unique.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/filters/unique.rs deleted file mode 100644 index 6d2708fb87..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/filters/unique.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Purpose: -//! Implements PHP `array_unique()` eval support. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::filters` re-exports. -//! -//! Key details: -//! - Values are compared through PHP's default string comparison mode. - -use super::super::super::super::*; - -/// Evaluates PHP `array_unique()` over one eval array expression. -pub(in crate::interpreter) fn eval_builtin_array_unique( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = eval_expr(array, context, scope, values)?; - eval_array_unique_result(array, values) -} - -/// Builds `array_unique()` by comparing values with PHP's default string comparison mode. -pub(in crate::interpreter) fn eval_array_unique_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut seen = Vec::>::with_capacity(len); - let mut result = values.assoc_new(len)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let unique_key = values.string_bytes(value)?; - if seen.iter().any(|seen_key| seen_key == &unique_key) { - continue; - } - seen.push(unique_key); - result = values.array_set(result, key, value)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs deleted file mode 100644 index 835a8fad0b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Purpose: -//! Groups PHP array and iterator builtins implemented by eval. -//! Submodules separate pure construction, mutating by-reference calls, sorting, -//! splicing, filtering, and key/value access helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins` array-related dispatch. -//! -//! Key details: -//! - Helpers preserve PHP key normalization and use `RuntimeValueOps` for all -//! runtime cell allocation and coercion. - -mod access; -mod callbacks; -mod core; -mod filters; -mod mutation; -mod push_pop; -mod sort; -mod splice; - -pub(in crate::interpreter) use access::*; -pub(in crate::interpreter) use callbacks::*; -pub(in crate::interpreter) use core::*; -pub(in crate::interpreter) use filters::*; -pub(in crate::interpreter) use mutation::*; -pub(in crate::interpreter) use push_pop::*; -pub(in crate::interpreter) use sort::*; -pub(in crate::interpreter) use splice::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs deleted file mode 100644 index c65e003aee..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/mutation.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Purpose: -//! By-reference array mutation dispatch for eval builtin calls. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! -//! Key details: -//! - Array cells remain opaque runtime handles and are manipulated through -//! `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Captures the first by-reference array mutator argument as a writable lvalue. -pub(in crate::interpreter) fn eval_array_mutation_lvalue_arg( - arg: &EvalCallArg, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { - if arg.is_spread() || !matches!(arg.name(), None | Some("array")) { - return Err(EvalStatus::RuntimeFatal); - } - let (array, target) = eval_call_arg_value(arg.value(), context, scope, values)?; - let target = target.ok_or(EvalStatus::RuntimeFatal)?; - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - Ok((array, target)) -} - -/// Evaluates direct by-reference `array_pop()` / `array_shift()` calls and writes back the array. -pub(in crate::interpreter) fn eval_builtin_array_pop_shift_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if name == "settype" { - return eval_builtin_settype_call(args, context, scope, values); - } - if matches!(name, "array_push" | "array_unshift") { - return eval_builtin_array_push_unshift_call(name, args, context, scope, values); - } - if name == "array_splice" { - return eval_builtin_array_splice_call(args, context, scope, values); - } - if name == "array_walk" { - return eval_builtin_array_walk_call(args, context, scope, values); - } - if matches!( - name, - "arsort" - | "asort" - | "krsort" - | "ksort" - | "natcasesort" - | "natsort" - | "rsort" - | "shuffle" - | "sort" - ) { - return eval_builtin_array_sort_call(name, args, context, scope, values); - } - if matches!(name, "uasort" | "uksort" | "usort") { - return eval_builtin_user_sort_call(name, args, context, scope, values); - } - - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let (array, target) = eval_array_mutation_lvalue_arg(arg, context, scope, values)?; - - let (result, replacement) = eval_array_pop_shift_replacement(name, array, values)?; - eval_write_direct_ref_target(&target, replacement, context, values, None)?; - Ok(result) -} - -/// Evaluates direct by-reference `array_push()` / `array_unshift()` calls. -pub(in crate::interpreter) fn eval_builtin_array_push_unshift_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 || !eval_call_args_are_plain_positional(args) { - return Err(EvalStatus::RuntimeFatal); - } - let (array, target) = eval_array_mutation_lvalue_arg(&args[0], context, scope, values)?; - let mut inserted = Vec::with_capacity(args.len() - 1); - for arg in &args[1..] { - inserted.push(eval_expr(arg.value(), context, scope, values)?); - } - - let replacement = eval_array_push_unshift_replacement(name, array, &inserted, values)?; - let result = eval_array_push_unshift_count_result(array, inserted.len(), values)?; - eval_write_direct_ref_target(&target, replacement, context, values, None)?; - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/push_pop.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/push_pop.rs deleted file mode 100644 index 446f8f3400..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/push_pop.rs +++ /dev/null @@ -1,312 +0,0 @@ -//! Purpose: -//! array_pop, array_shift, array_push, and array_unshift replacement helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! -//! Key details: -//! - Array cells remain opaque runtime handles and are manipulated through -//! `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; - -/// Returns the value produced by `array_pop()` / `array_shift()` without mutating the source. -pub(in crate::interpreter) fn eval_array_pop_shift_value_result( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(array)?; - if len == 0 { - return values.null(); - } - let position = match name { - "array_pop" => len - 1, - "array_shift" => 0, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let key = values.array_iter_key(array, position)?; - values.array_get(array, key) -} - -/// Builds the return value plus replacement array for direct pop/shift write-back. -pub(in crate::interpreter) fn eval_array_pop_shift_replacement( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let len = values.array_len(array)?; - let tag = values.type_tag(array)?; - if len == 0 { - let replacement = match tag { - EVAL_TAG_ARRAY => values.array_new(0)?, - EVAL_TAG_ASSOC => values.assoc_new(0)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - return Ok((values.null()?, replacement)); - } - - let removed_position = match name { - "array_pop" => len - 1, - "array_shift" => 0, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let removed_key = values.array_iter_key(array, removed_position)?; - let removed_value = values.array_get(array, removed_key)?; - let replacement = match tag { - EVAL_TAG_ARRAY => { - eval_array_pop_shift_indexed_replacement(array, removed_position, len, values)? - } - EVAL_TAG_ASSOC => { - eval_array_pop_shift_assoc_replacement(name, array, removed_position, len, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - Ok((removed_value, replacement)) -} - -/// Rebuilds an indexed array after removing one position and reindexing values. -pub(in crate::interpreter) fn eval_array_pop_shift_indexed_replacement( - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(len.saturating_sub(1))?; - let mut target = 0_i64; - for position in 0..len { - if position == removed_position { - continue; - } - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after pop/shift, preserving PHP key behavior. -pub(in crate::interpreter) fn eval_array_pop_shift_assoc_replacement( - name: &str, - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - if name == "array_shift" - && eval_array_remaining_keys_are_int(array, removed_position, len, values)? - { - return eval_array_pop_shift_indexed_replacement(array, removed_position, len, values); - } - - let mut result = values.assoc_new(len.saturating_sub(1))?; - let mut next_int_key = 0_i64; - for position in 0..len { - if position == removed_position { - continue; - } - let source_key = values.array_iter_key(array, position)?; - let target_key = if name == "array_shift" && values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every remaining key is an integer after removing one element. -pub(in crate::interpreter) fn eval_array_remaining_keys_are_int( - array: RuntimeCellHandle, - removed_position: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - if position == removed_position { - continue; - } - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Returns the resulting element count for by-value push/unshift dynamic calls. -pub(in crate::interpreter) fn eval_array_push_unshift_count_result( - array: RuntimeCellHandle, - inserted_len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let total = values - .array_len(array)? - .checked_add(inserted_len) - .ok_or(EvalStatus::RuntimeFatal)?; - let total = i64::try_from(total).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(total) -} - -/// Builds the replacement array for direct push/unshift write-back. -pub(in crate::interpreter) fn eval_array_push_unshift_replacement( - name: &str, - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match (name, values.type_tag(array)?) { - ("array_push", EVAL_TAG_ARRAY) => { - eval_array_push_indexed_replacement(array, inserted, values) - } - ("array_push", EVAL_TAG_ASSOC) => { - eval_array_push_assoc_replacement(array, inserted, values) - } - ("array_unshift", EVAL_TAG_ARRAY) => { - eval_array_unshift_indexed_replacement(array, inserted, values) - } - ("array_unshift", EVAL_TAG_ASSOC) => { - eval_array_unshift_assoc_replacement(array, inserted, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Rebuilds an indexed array after appending values. -pub(in crate::interpreter) fn eval_array_push_indexed_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len.saturating_add(inserted.len()))?; - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let target_key = - values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, target_key, value)?; - } - for (offset, value) in inserted.iter().copied().enumerate() { - let position = len.checked_add(offset).ok_or(EvalStatus::RuntimeFatal)?; - let key = values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after appending values at PHP's next integer keys. -pub(in crate::interpreter) fn eval_array_push_assoc_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; - let mut next_key = 0_i64; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? == EVAL_TAG_INT { - next_key = next_key.max(eval_int_value(key, values)?.saturating_add(1)); - } - let value = values.array_get(array, key)?; - result = values.array_set(result, key, value)?; - } - for value in inserted.iter().copied() { - let key = values.int(next_key)?; - next_key = next_key.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an indexed array after prepending values and reindexing the original tail. -pub(in crate::interpreter) fn eval_array_unshift_indexed_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut result = values.array_new(len.saturating_add(inserted.len()))?; - let mut target = 0_i64; - for value in inserted.iter().copied() { - let key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Rebuilds an associative array after prepending values and reindexing integer keys. -pub(in crate::interpreter) fn eval_array_unshift_assoc_replacement( - array: RuntimeCellHandle, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - if eval_array_keys_are_int(array, len, values)? { - return eval_array_unshift_indexed_replacement(array, inserted, values); - } - - let mut result = values.assoc_new(len.saturating_add(inserted.len()))?; - let mut next_int_key = 0_i64; - for value in inserted.iter().copied() { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, key, value)?; - } - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every key in the array is integer-shaped. -pub(in crate::interpreter) fn eval_array_keys_are_int( - array: RuntimeCellHandle, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs deleted file mode 100644 index cdf0ca81ba..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/direct.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Purpose: -//! Binds direct by-reference array sort calls before delegating to sort engines. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::sort` re-exports. -//! -//! Key details: -//! - Direct calls extract a writable lvalue cell and write back the sorted -//! replacement while preserving source-order evaluation of callback arguments. - -use super::super::super::super::*; -use super::super::super::eval_write_direct_ref_target; -use super::super::eval_array_mutation_lvalue_arg; -use super::*; - -/// Evaluates direct by-reference array ordering calls and writes back the array. -pub(in crate::interpreter) fn eval_builtin_array_sort_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, target) = eval_array_sort_direct_arg(args, context, scope, values)?; - - let replacement = eval_array_sort_replacement(name, array, values)?; - let result = values.bool_value(true)?; - eval_write_direct_ref_target(&target, replacement, context, values, None)?; - Ok(result) -} - -/// Evaluates direct by-reference user-comparator sort calls and writes back the array. -pub(in crate::interpreter) fn eval_builtin_user_sort_call( - name: &str, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, target, callback) = eval_user_sort_direct_args(args, context, scope, values)?; - - let replacement = eval_user_sort_replacement_from_scope( - name, - array, - callback, - Some(scope), - context, - values, - )?; - let result = values.bool_value(true)?; - eval_write_direct_ref_target(&target, replacement, context, values, None)?; - Ok(result) -} - -/// Evaluates and binds direct user-sort arguments while preserving source order. -pub(in crate::interpreter) fn eval_user_sort_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, EvalReferenceTarget, RuntimeCellHandle), EvalStatus> { - let mut array = None; - let mut callback = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "callback", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - array = Some(eval_array_mutation_lvalue_arg( - arg, context, scope, values, - )?); - } - "callback" => { - if callback.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - callback = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let (array, target) = array.ok_or(EvalStatus::RuntimeFatal)?; - let callback = callback.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, target, callback)) -} - -/// Extracts the writable array lvalue accepted by eval array ordering builtins. -pub(in crate::interpreter) fn eval_array_sort_direct_arg( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, EvalReferenceTarget), EvalStatus> { - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_array_mutation_lvalue_arg(arg, context, scope, values) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/mod.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/mod.rs deleted file mode 100644 index f2bb0ca419..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Groups eval array sorting builtins into direct-call binding, user-comparator -//! sorting, and standard sorting engines. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! -//! Key details: -//! - Direct by-reference calls update scope cells, while dynamic by-value dispatch -//! returns sorted replacement arrays plus PHP-compatible warnings. - -mod direct; -mod standard; -mod user; - -pub(in crate::interpreter) use direct::*; -pub(in crate::interpreter) use standard::*; -pub(in crate::interpreter) use user::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/standard.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/standard.rs deleted file mode 100644 index 0071c50d06..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/standard.rs +++ /dev/null @@ -1,345 +0,0 @@ -//! Purpose: -//! Implements standard array sorting, key extraction, and natural ordering helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::sort` re-exports. -//! -//! Key details: -//! - Homogeneous sort keys model the eval-supported PHP ordering subset and -//! shuffled output remains deterministic through runtime value operations. - -use super::super::super::super::*; -use super::super::super::*; -use super::super::*; - -/// Returns the dynamic callable result for by-value array ordering calls. -pub(in crate::interpreter) fn eval_array_sort_value_result( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - values.bool_value(true) -} - -/// Sort key shape supported by eval's homogeneous array ordering implementation. -#[derive(Clone)] -pub(in crate::interpreter) enum EvalArraySortKey { - Numeric(f64), - Natural(Vec), - String(Vec), -} - -/// One source array entry plus its precomputed ordering key. -pub(in crate::interpreter) struct EvalArraySortEntry { - sort_key: EvalArraySortKey, - source_key: RuntimeCellHandle, - value: RuntimeCellHandle, -} - -/// Builds the sorted replacement array for eval array ordering builtins. -pub(in crate::interpreter) fn eval_array_sort_replacement( - name: &str, - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut entries = match name { - "krsort" | "ksort" => eval_array_key_sort_entries(array, values)?, - "natcasesort" => eval_array_natural_sort_entries(array, true, values)?, - "natsort" => eval_array_natural_sort_entries(array, false, values)?, - "arsort" | "asort" | "rsort" | "sort" => eval_array_value_sort_entries(array, values)?, - "shuffle" => return eval_array_shuffle_replacement(array, values), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - entries.sort_by(|left, right| { - let order = eval_array_sort_key_cmp(&left.sort_key, &right.sort_key); - if matches!(name, "arsort" | "krsort" | "rsort") { - order.reverse() - } else { - order - } - }); - - if matches!( - name, - "arsort" | "asort" | "krsort" | "ksort" | "natcasesort" | "natsort" - ) { - return eval_array_preserve_key_sort_result(entries, values); - } - eval_array_reindex_sort_result(entries, values) -} - -/// Builds a shuffled, reindexed replacement array for `shuffle()`. -pub(in crate::interpreter) fn eval_array_shuffle_replacement( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - entries.push(values.array_get(array, source_key)?); - } - - for index in (1..entries.len()).rev() { - let swap_with = (eval_random_u128() % ((index + 1) as u128)) as usize; - entries.swap(index, swap_with); - } - - let mut result = values.array_new(entries.len())?; - for (index, value) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Builds an indexed result for `sort()` / `rsort()` after value ordering. -pub(in crate::interpreter) fn eval_array_reindex_sort_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(entries.len())?; - for (index, entry) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, entry.value)?; - } - Ok(result) -} - -/// Builds a key-preserving associative result after value or key ordering. -pub(in crate::interpreter) fn eval_array_preserve_key_sort_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(entries.len())?; - for entry in entries { - result = values.array_set(result, entry.source_key, entry.value)?; - } - Ok(result) -} - -/// Collects values and comparable value-sort keys from one eval array. -pub(in crate::interpreter) fn eval_array_value_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - let mut expects_numeric = None; - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_sort_key(value, values)?; - let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); - match expects_numeric { - Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), - Some(_) => {} - None => expects_numeric = Some(is_numeric), - } - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Collects values and natural-sort keys from one eval array. -pub(in crate::interpreter) fn eval_array_natural_sort_entries( - array: RuntimeCellHandle, - case_insensitive: bool, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - let mut expects_numeric = None; - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_natural_sort_key(value, case_insensitive, values)?; - let is_numeric = matches!(sort_key, EvalArraySortKey::Numeric(_)); - match expects_numeric { - Some(expected) if expected != is_numeric => return Err(EvalStatus::RuntimeFatal), - Some(_) => {} - None => expects_numeric = Some(is_numeric), - } - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Collects values and comparable key-sort keys from one eval array. -pub(in crate::interpreter) fn eval_array_key_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - let sort_key = eval_array_sort_key(source_key, values)?; - entries.push(EvalArraySortEntry { - sort_key, - source_key, - value, - }); - } - - Ok(entries) -} - -/// Converts one scalar eval value into a natural-sort key. -pub(in crate::interpreter) fn eval_array_natural_sort_key( - value: RuntimeCellHandle, - case_insensitive: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => { - Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) - } - EVAL_TAG_STRING => { - let mut bytes = values.string_bytes(value)?; - if case_insensitive { - bytes.make_ascii_lowercase(); - } - Ok(EvalArraySortKey::Natural(bytes)) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts one scalar eval value into a homogeneous sort key. -pub(in crate::interpreter) fn eval_array_sort_key( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(value)? { - EVAL_TAG_INT | EVAL_TAG_FLOAT => { - Ok(EvalArraySortKey::Numeric(eval_float_value(value, values)?)) - } - EVAL_TAG_STRING => { - let bytes = values.string_bytes(value)?; - match eval_array_numeric_string_sort_key(&bytes) { - Some(value) => Ok(EvalArraySortKey::Numeric(value)), - None => Ok(EvalArraySortKey::String(bytes)), - } - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Parses one PHP numeric string into the numeric sort domain when possible. -pub(in crate::interpreter) fn eval_array_numeric_string_sort_key(bytes: &[u8]) -> Option { - if !eval_is_numeric_string(bytes) { - return None; - } - std::str::from_utf8(bytes).ok()?.parse::().ok() -} - -/// Compares two precomputed eval sort keys. -pub(in crate::interpreter) fn eval_array_sort_key_cmp( - left: &EvalArraySortKey, - right: &EvalArraySortKey, -) -> std::cmp::Ordering { - match (left, right) { - (EvalArraySortKey::Numeric(left), EvalArraySortKey::Numeric(right)) => { - left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal) - } - (EvalArraySortKey::Natural(left), EvalArraySortKey::Natural(right)) => { - eval_natural_bytes_cmp(left, right) - } - (EvalArraySortKey::String(left), EvalArraySortKey::String(right)) => left.cmp(right), - _ => eval_array_sort_key_rank(left).cmp(&eval_array_sort_key_rank(right)), - } -} - -/// Returns a deterministic rank for mixed key-sort domains. -pub(in crate::interpreter) fn eval_array_sort_key_rank(key: &EvalArraySortKey) -> u8 { - match key { - EvalArraySortKey::Numeric(_) => 0, - EvalArraySortKey::Natural(_) => 1, - EvalArraySortKey::String(_) => 2, - } -} - -/// Compares byte strings with a small PHP-style natural ordering. -pub(in crate::interpreter) fn eval_natural_bytes_cmp( - left: &[u8], - right: &[u8], -) -> std::cmp::Ordering { - let mut left_index = 0; - let mut right_index = 0; - while left_index < left.len() && right_index < right.len() { - if left[left_index].is_ascii_digit() && right[right_index].is_ascii_digit() { - let order = eval_natural_digit_run_cmp(left, &mut left_index, right, &mut right_index); - if order != std::cmp::Ordering::Equal { - return order; - } - continue; - } - - let order = left[left_index].cmp(&right[right_index]); - if order != std::cmp::Ordering::Equal { - return order; - } - left_index += 1; - right_index += 1; - } - left.len().cmp(&right.len()) -} - -/// Compares two natural-sort digit runs and advances both byte indexes past them. -pub(in crate::interpreter) fn eval_natural_digit_run_cmp( - left: &[u8], - left_index: &mut usize, - right: &[u8], - right_index: &mut usize, -) -> std::cmp::Ordering { - let left_start = *left_index; - let right_start = *right_index; - while *left_index < left.len() && left[*left_index].is_ascii_digit() { - *left_index += 1; - } - while *right_index < right.len() && right[*right_index].is_ascii_digit() { - *right_index += 1; - } - - let left_digits = &left[left_start..*left_index]; - let right_digits = &right[right_start..*right_index]; - let left_trimmed = eval_trim_leading_zeroes(left_digits); - let right_trimmed = eval_trim_leading_zeroes(right_digits); - left_trimmed - .len() - .cmp(&right_trimmed.len()) - .then_with(|| left_trimmed.cmp(right_trimmed)) - .then_with(|| left_digits.len().cmp(&right_digits.len())) -} - -/// Drops leading zero bytes while keeping one zero for an all-zero digit run. -pub(in crate::interpreter) fn eval_trim_leading_zeroes(digits: &[u8]) -> &[u8] { - let trimmed = digits - .iter() - .position(|digit| *digit != b'0') - .map_or(&digits[digits.len().saturating_sub(1)..], |index| { - &digits[index..] - }); - if trimmed.is_empty() { - digits - } else { - trimmed - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs deleted file mode 100644 index 7fd4d32ce9..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/sort/user.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! Purpose: -//! Implements user-comparator array sorting for `usort`, `uasort`, and `uksort`. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays::sort` re-exports. -//! -//! Key details: -//! - Comparator callbacks are invoked through the eval context and their integer -//! return values drive a stable entry ordering. - -use super::super::super::super::*; -use super::super::super::*; - -/// Returns the dynamic callable result for by-value user-comparator sort calls. -pub(in crate::interpreter) fn eval_user_sort_value_result( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let replacement = eval_user_sort_replacement(name, array, callback, context, values)?; - values.release(replacement)?; - values.bool_value(true) -} - -/// One source array entry used by eval user-comparator sort routines. -pub(in crate::interpreter) struct EvalUserSortEntry { - source_key: RuntimeCellHandle, - value: RuntimeCellHandle, -} - -/// Builds the sorted replacement array for user-comparator sort builtins. -pub(in crate::interpreter) fn eval_user_sort_replacement( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_user_sort_replacement_from_scope(name, array, callback, None, context, values) -} - -/// Builds the sorted replacement array with optional lexical scope for callback names. -pub(in crate::interpreter) fn eval_user_sort_replacement_from_scope( - name: &str, - array: RuntimeCellHandle, - callback: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = eval_callable_with_optional_scope(callback, context, lexical_scope, values)?; - let mut entries = eval_user_sort_entries(array, values)?; - eval_user_sort_entries_in_place(name, &callback, &mut entries, context, values)?; - if name == "usort" { - return eval_user_sort_reindex_result(entries, values); - } - eval_user_sort_preserve_key_result(entries, values) -} - -/// Collects source keys and values from one eval array for user sorting. -pub(in crate::interpreter) fn eval_user_sort_entries( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut entries = Vec::with_capacity(len); - for position in 0..len { - let source_key = values.array_iter_key(array, position)?; - let value = values.array_get(array, source_key)?; - entries.push(EvalUserSortEntry { source_key, value }); - } - Ok(entries) -} - -/// Sorts entries by repeatedly invoking the PHP comparator callback. -pub(in crate::interpreter) fn eval_user_sort_entries_in_place( - name: &str, - callback: &EvaluatedCallable, - entries: &mut [EvalUserSortEntry], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for pass in 0..entries.len() { - let upper = entries.len().saturating_sub(pass + 1); - for index in 0..upper { - let comparison = eval_user_sort_compare( - name, - callback, - &entries[index], - &entries[index + 1], - context, - values, - )?; - if comparison > 0 { - entries.swap(index, index + 1); - } - } - } - Ok(()) -} - -/// Invokes one user-sort comparator and returns its integer ordering result. -pub(in crate::interpreter) fn eval_user_sort_compare( - name: &str, - callback: &EvaluatedCallable, - left: &EvalUserSortEntry, - right: &EvalUserSortEntry, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let args = if name == "uksort" { - vec![left.source_key, right.source_key] - } else { - vec![left.value, right.value] - }; - let result = eval_evaluated_callable_with_values(callback, args, context, values)?; - eval_int_value(result, values) -} - -/// Builds the reindexed result for `usort()`. -pub(in crate::interpreter) fn eval_user_sort_reindex_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(entries.len())?; - for (index, entry) in entries.into_iter().enumerate() { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, entry.value)?; - } - Ok(result) -} - -/// Builds the key-preserving result for `uksort()` and `uasort()`. -pub(in crate::interpreter) fn eval_user_sort_preserve_key_result( - entries: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(entries.len())?; - for entry in entries { - result = values.array_set(result, entry.source_key, entry.value)?; - } - Ok(result) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs b/crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs deleted file mode 100644 index b3424f72c1..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/arrays/splice.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! Purpose: -//! array_splice argument handling and replacement construction helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::arrays` re-exports. -//! -//! Key details: -//! - Array cells remain opaque runtime handles and are manipulated through -//! `RuntimeValueOps`. - -use super::super::super::*; -use super::super::*; -use super::*; - -/// Evaluates direct by-reference `array_splice()` calls and writes back the array. -pub(in crate::interpreter) fn eval_builtin_array_splice_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (array, target, offset, length, replacement_arg) = - eval_array_splice_direct_args(args, context, scope, values)?; - - let (removed, replacement) = - eval_array_splice_removed_and_replacement(array, offset, length, replacement_arg, values)?; - eval_write_direct_ref_target(&target, replacement, context, values, None)?; - Ok(removed) -} - -/// Evaluates and binds direct `array_splice()` arguments while preserving source order. -pub(in crate::interpreter) fn eval_array_splice_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut array = None; - let mut offset = None; - let mut length = None; - let mut replacement = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "array", - 1 => "offset", - 2 => "length", - 3 => "replacement", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "array" => { - if array.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - array = Some(eval_array_mutation_lvalue_arg( - arg, context, scope, values, - )?); - } - "offset" => { - if offset.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - offset = Some(eval_expr(arg.value(), context, scope, values)?); - } - "length" => { - if length.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - length = Some(eval_expr(arg.value(), context, scope, values)?); - } - "replacement" => { - if replacement.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - replacement = Some(eval_expr(arg.value(), context, scope, values)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let (array, target) = array.ok_or(EvalStatus::RuntimeFatal)?; - let offset = offset.ok_or(EvalStatus::RuntimeFatal)?; - Ok((array, target, offset, length, replacement)) -} - -/// Returns the removed elements that `array_splice()` would produce without mutating the source. -pub(in crate::interpreter) fn eval_array_splice_value_result( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(values.type_tag(array)?, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::RuntimeFatal); - } - let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; - eval_array_splice_removed(array, start, end, values) -} - -/// Builds both removed and replacement arrays for direct `array_splice()` write-back. -pub(in crate::interpreter) fn eval_array_splice_removed_and_replacement( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - replacement: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let (start, end) = eval_array_splice_bounds(array, offset, length, values)?; - let removed = eval_array_splice_removed(array, start, end, values)?; - let inserted = eval_array_splice_insert_values(replacement, values)?; - let replacement = eval_array_splice_replacement(array, start, end, &inserted, values)?; - Ok((removed, replacement)) -} - -/// Converts splice offset and length cells into bounded source positions. -pub(in crate::interpreter) fn eval_array_splice_bounds( - array: RuntimeCellHandle, - offset: RuntimeCellHandle, - length: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(usize, usize), EvalStatus> { - let len = values.array_len(array)?; - let offset = eval_int_value(offset, values)?; - let start = eval_slice_start(len, offset)?; - let end = match length { - Some(length) if values.type_tag(length)? != EVAL_TAG_NULL => { - eval_slice_end(len, start, eval_int_value(length, values)?)? - } - _ => len, - }; - Ok((start, end)) -} - -/// Builds the reindexed/string-key-preserving removed array returned by `array_splice()`. -pub(in crate::interpreter) fn eval_array_splice_removed( - array: RuntimeCellHandle, - start: usize, - end: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = end.saturating_sub(start); - if eval_array_range_keys_are_int(array, start, end, values)? { - let mut result = values.array_new(len)?; - let mut target = 0_i64; - for position in start..end { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - return Ok(result); - } - - let mut result = values.assoc_new(len)?; - let mut next_int_key = 0_i64; - for position in start..end { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Expands the optional `array_splice()` replacement value into inserted values. -pub(in crate::interpreter) fn eval_array_splice_insert_values( - replacement: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(replacement) = replacement else { - return Ok(Vec::new()); - }; - if !matches!( - values.type_tag(replacement)?, - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC - ) { - return Ok(vec![replacement]); - } - - let len = values.array_len(replacement)?; - let mut inserted = Vec::with_capacity(len); - for position in 0..len { - let key = values.array_iter_key(replacement, position)?; - inserted.push(values.array_get(replacement, key)?); - } - Ok(inserted) -} - -/// Builds the source replacement after removing the requested splice range. -pub(in crate::interpreter) fn eval_array_splice_replacement( - array: RuntimeCellHandle, - start: usize, - end: usize, - inserted: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let new_len = len - .saturating_sub(end.saturating_sub(start)) - .checked_add(inserted.len()) - .ok_or(EvalStatus::RuntimeFatal)?; - if eval_array_splice_remaining_keys_are_int(array, start, end, len, values)? { - let mut result = values.array_new(new_len)?; - let mut target = 0_i64; - for position in 0..start { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - for value in inserted { - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, *value)?; - } - for position in end..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - let target_key = values.int(target)?; - target = target.checked_add(1).ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, value)?; - } - return Ok(result); - } - - let mut result = values.assoc_new(new_len)?; - let mut next_int_key = 0_i64; - for position in 0..start { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - for value in inserted { - let target_key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - result = values.array_set(result, target_key, *value)?; - } - for position in end..len { - let source_key = values.array_iter_key(array, position)?; - let target_key = if values.type_tag(source_key)? == EVAL_TAG_INT { - let key = values.int(next_int_key)?; - next_int_key = next_int_key - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - } else { - source_key - }; - let value = values.array_get(array, source_key)?; - result = values.array_set(result, target_key, value)?; - } - Ok(result) -} - -/// Returns true when every key in one source position range is integer-shaped. -pub(in crate::interpreter) fn eval_array_range_keys_are_int( - array: RuntimeCellHandle, - start: usize, - end: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in start..end { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} - -/// Returns true when every key outside the removed splice range is integer-shaped. -pub(in crate::interpreter) fn eval_array_splice_remaining_keys_are_int( - array: RuntimeCellHandle, - start: usize, - end: usize, - len: usize, - values: &mut impl RuntimeValueOps, -) -> Result { - for position in 0..len { - if (start..end).contains(&position) { - continue; - } - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - return Ok(false); - } - } - Ok(true) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index bf2bcedd82..04e604e600 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -15,7 +15,6 @@ mod macros; mod array; -mod arrays; mod class_metadata; mod core; mod filesystem; @@ -25,6 +24,7 @@ mod json; mod math; mod network_env; mod raw_memory; +mod random; mod ref_targets; mod regex; mod registry; @@ -36,7 +36,6 @@ mod symbols; mod time; mod types; -pub(super) use arrays::*; pub(super) use array::*; pub(super) use class_metadata::*; pub(super) use core::*; @@ -46,6 +45,7 @@ pub(super) use json::*; pub(super) use math::*; pub(super) use network_env::*; pub(super) use raw_memory::*; +pub(super) use random::*; pub(super) use ref_targets::*; pub(super) use regex::*; pub(super) use registry::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/random.rs b/crates/elephc-magician/src/interpreter/builtins/random.rs new file mode 100644 index 0000000000..ef646be040 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/random.rs @@ -0,0 +1,28 @@ +//! Purpose: +//! Shared pseudo-random word source for eval builtins. +//! +//! Called from: +//! - `crate::interpreter::builtins::math` random builtins. +//! - `crate::interpreter::builtins::array` randomizing builtins. +//! +//! Key details: +//! - This is eval-local, process-local, and non-cryptographic; PHP-visible +//! builtin owners decide range and key semantics. + +use super::super::*; + +/// Produces a process-local pseudo-random word for non-cryptographic eval builtins. +pub(in crate::interpreter) fn eval_random_u128() -> u128 { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + let counter = u128::from(EVAL_RANDOM_COUNTER.fetch_add(1, Ordering::Relaxed)); + let pid = u128::from(std::process::id()); + let mut value = nanos ^ (counter.wrapping_mul(0x9e37_79b9_7f4a_7c15)) ^ (pid << 64); + value ^= value >> 30; + value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9); + value ^= value >> 27; + value = value.wrapping_mul(0x94d0_49bb_1331_11eb); + value ^ (value >> 31) +} diff --git a/crates/elephc-magician/src/interpreter/expressions/calls.rs b/crates/elephc-magician/src/interpreter/expressions/calls.rs index f3e4161380..a44858ef0b 100644 --- a/crates/elephc-magician/src/interpreter/expressions/calls.rs +++ b/crates/elephc-magician/src/interpreter/expressions/calls.rs @@ -102,7 +102,7 @@ pub(in crate::interpreter) fn eval_call( | "uksort" | "usort" ) { - return eval_builtin_array_pop_shift_call(name, args, context, scope, values); + return eval_builtin_array_mutating_declared_call(name, args, context, scope, values); } if eval_php_visible_builtin_exists(name) { if eval_call_args_are_plain_positional(args) { From 7123e66f6de1feabaf2ed88797e2f61acbd59914 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:18:52 +0200 Subject: [PATCH 1113/1208] refactor(magician): move filesystem path builtin implementations home --- .../builtins/filesystem/basename.rs | 65 +++- .../builtins/filesystem/direct_dispatch.rs | 8 +- .../builtins/filesystem/dirname.rs | 80 ++++- .../interpreter/builtins/filesystem/path.rs | 309 +----------------- .../filesystem/path_values_dispatch.rs | 19 -- .../builtins/filesystem/pathinfo.rs | 135 +++++++- .../builtins/filesystem/realpath.rs | 35 +- .../filesystem/stream_resolve_include_path.rs | 29 +- .../filesystem/stream_values_dispatch.rs | 4 - 9 files changed, 336 insertions(+), 348 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs index 0afb583e83..5613408937 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs @@ -26,14 +26,75 @@ pub(in crate::interpreter) fn eval_basename_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("basename", args, context, scope, values) + eval_builtin_basename(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `basename` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_basename_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_basename_result(*path, None, values), + [path, suffix] => eval_basename_result(*path, Some(*suffix), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `basename($path, $suffix = "")` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_basename( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("basename", evaluated_args, context, values) + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_basename_result(path, None, values) + } + [path, suffix] => { + let path = eval_expr(path, context, scope, values)?; + let suffix = eval_expr(suffix, context, scope, values)?; + eval_basename_result(path, Some(suffix), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `basename()` bytes and returns them as a runtime string. +pub(in crate::interpreter) fn eval_basename_result( + path: RuntimeCellHandle, + suffix: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let suffix = suffix + .map(|suffix| values.string_bytes(suffix)) + .transpose()?; + let result = eval_basename_bytes(&path, suffix.as_deref()); + values.string_bytes_value(&result) +} + +/// Extracts a PHP basename from one path byte string. +pub(in crate::interpreter) fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return Vec::new(); + } + let mut start = end; + while start > 0 && path[start - 1] != b'/' { + start -= 1; + } + let mut result = path[start..end].to_vec(); + if let Some(suffix) = suffix { + if !suffix.is_empty() && suffix.len() < result.len() && result.ends_with(suffix) { + result.truncate(result.len() - suffix.len()); + } + } + result } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index 3458c760e3..1c013e5726 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -159,7 +159,7 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( values: &mut impl RuntimeValueOps, ) -> Result { match name { - "basename" => eval_builtin_basename(args, context, scope, values), + "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), "chdir" | "mkdir" | "rmdir" => { eval_builtin_unary_path_bool(name, args, context, scope, values) } @@ -171,7 +171,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "copy" | "link" | "rename" | "symlink" => { eval_builtin_binary_path_bool(name, args, context, scope, values) } - "dirname" => eval_builtin_dirname(args, context, scope, values), "disk_free_space" | "disk_total_space" => { eval_builtin_disk_space(name, args, context, scope, values) } @@ -204,7 +203,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "glob" => eval_builtin_glob(args, context, scope, values), "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), - "pathinfo" => eval_builtin_pathinfo(args, context, scope, values), "pclose" => eval_builtin_pclose(args, context, scope, values), "popen" => eval_builtin_popen(args, context, scope, values), "closedir" | "readdir" | "rewinddir" => { @@ -213,7 +211,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "readfile" => eval_builtin_readfile(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), - "realpath" => eval_builtin_realpath(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), "scandir" => eval_builtin_scandir(args, context, scope, values), @@ -255,9 +252,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), - "stream_resolve_include_path" => { - eval_builtin_stream_resolve_include_path(args, context, scope, values) - } "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { eval_builtin_stream_set_buffer_like(name, args, context, scope, values) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs index f834556e59..0a51247a95 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs @@ -26,14 +26,90 @@ pub(in crate::interpreter) fn eval_dirname_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("dirname", args, context, scope, values) + eval_builtin_dirname(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `dirname` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_dirname_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_dirname_result(*path, None, values), + [path, levels] => eval_dirname_result(*path, Some(*levels), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `dirname($path, $levels = 1)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_dirname( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("dirname", evaluated_args, context, values) + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_dirname_result(path, None, values) + } + [path, levels] => { + let path = eval_expr(path, context, scope, values)?; + let levels = eval_expr(levels, context, scope, values)?; + eval_dirname_result(path, Some(levels), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `dirname()` bytes and returns them as a runtime string. +pub(in crate::interpreter) fn eval_dirname_result( + path: RuntimeCellHandle, + levels: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let levels = match levels { + Some(levels) => eval_int_value(levels, values)?, + None => 1, + }; + if levels < 1 { + return Err(EvalStatus::RuntimeFatal); + } + let mut current = path; + for _ in 0..levels { + current = eval_dirname_once(¤t); + } + values.string_bytes_value(¤t) +} + +/// Applies one PHP `dirname()` parent traversal to a path byte string. +pub(in crate::interpreter) fn eval_dirname_once(path: &[u8]) -> Vec { + if path.is_empty() { + return b".".to_vec(); + } + let mut end = path.len(); + while end > 0 && path[end - 1] == b'/' { + end -= 1; + } + if end == 0 { + return b"/".to_vec(); + } + let mut cursor = end; + while cursor > 0 { + cursor -= 1; + if path[cursor] == b'/' { + let mut parent_end = cursor; + while parent_end > 0 && path[parent_end - 1] == b'/' { + parent_end -= 1; + } + return if parent_end == 0 { + b"/".to_vec() + } else { + path[..parent_end].to_vec() + }; + } + } + b".".to_vec() } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs index 957f328857..b313415c73 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Path conversion and basename, dirname, realpath, and pathinfo helpers. +//! Shared filesystem path conversion and permission helpers. //! //! Called from: //! - `crate::interpreter::builtins::filesystem` re-exports. @@ -8,7 +8,6 @@ //! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. use super::super::super::*; -use super::super::*; /// Converts one eval value to a filesystem path string. pub(in crate::interpreter) fn eval_path_string( @@ -55,309 +54,3 @@ pub(in crate::interpreter) fn eval_path_is_writable(path: &std::path::Path) -> b Err(_) => false, } } - -/// Evaluates PHP `basename($path, $suffix = "")` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_basename( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_basename_result(path, None, values) - } - [path, suffix] => { - let path = eval_expr(path, context, scope, values)?; - let suffix = eval_expr(suffix, context, scope, values)?; - eval_basename_result(path, Some(suffix), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `basename()` bytes and returns them as a runtime string. -pub(in crate::interpreter) fn eval_basename_result( - path: RuntimeCellHandle, - suffix: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let suffix = suffix - .map(|suffix| values.string_bytes(suffix)) - .transpose()?; - let result = eval_basename_bytes(&path, suffix.as_deref()); - values.string_bytes_value(&result) -} - -/// Extracts a PHP basename from one path byte string. -pub(in crate::interpreter) fn eval_basename_bytes(path: &[u8], suffix: Option<&[u8]>) -> Vec { - let mut end = path.len(); - while end > 0 && path[end - 1] == b'/' { - end -= 1; - } - if end == 0 { - return Vec::new(); - } - let mut start = end; - while start > 0 && path[start - 1] != b'/' { - start -= 1; - } - let mut result = path[start..end].to_vec(); - if let Some(suffix) = suffix { - if !suffix.is_empty() && suffix.len() < result.len() && result.ends_with(suffix) { - result.truncate(result.len() - suffix.len()); - } - } - result -} - -/// Evaluates PHP `dirname($path, $levels = 1)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_dirname( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_dirname_result(path, None, values) - } - [path, levels] => { - let path = eval_expr(path, context, scope, values)?; - let levels = eval_expr(levels, context, scope, values)?; - eval_dirname_result(path, Some(levels), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `dirname()` bytes and returns them as a runtime string. -pub(in crate::interpreter) fn eval_dirname_result( - path: RuntimeCellHandle, - levels: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let levels = match levels { - Some(levels) => eval_int_value(levels, values)?, - None => 1, - }; - if levels < 1 { - return Err(EvalStatus::RuntimeFatal); - } - let mut current = path; - for _ in 0..levels { - current = eval_dirname_once(¤t); - } - values.string_bytes_value(¤t) -} - -/// Applies one PHP `dirname()` parent traversal to a path byte string. -pub(in crate::interpreter) fn eval_dirname_once(path: &[u8]) -> Vec { - if path.is_empty() { - return b".".to_vec(); - } - let mut end = path.len(); - while end > 0 && path[end - 1] == b'/' { - end -= 1; - } - if end == 0 { - return b"/".to_vec(); - } - let mut cursor = end; - while cursor > 0 { - cursor -= 1; - if path[cursor] == b'/' { - let mut parent_end = cursor; - while parent_end > 0 && path[parent_end - 1] == b'/' { - parent_end -= 1; - } - return if parent_end == 0 { - b"/".to_vec() - } else { - path[..parent_end].to_vec() - }; - } - } - b".".to_vec() -} - -/// Evaluates PHP `realpath($path)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_realpath( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_realpath_result(path, values) -} - -/// Canonicalizes one path or returns PHP false when the path cannot be resolved. -pub(in crate::interpreter) fn eval_realpath_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let path = String::from_utf8_lossy(&path); - let Ok(canonical) = std::fs::canonicalize(path.as_ref()) else { - return values.bool_value(false); - }; - let canonical = canonical.to_string_lossy(); - values.string(canonical.as_ref()) -} - -/// Evaluates PHP `stream_resolve_include_path($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_stream_resolve_include_path( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_stream_resolve_include_path_result(filename, values) -} - -/// Resolves one filename using elephc's realpath-equivalent include-path semantics. -pub(in crate::interpreter) fn eval_stream_resolve_include_path_result( - filename: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_realpath_result(filename, values) -} - -/// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_pathinfo( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [path] => { - let path = eval_expr(path, context, scope, values)?; - eval_pathinfo_result(path, None, values) - } - [path, flags] => { - let path = eval_expr(path, context, scope, values)?; - let flags = eval_expr(flags, context, scope, values)?; - eval_pathinfo_result(path, Some(flags), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Computes PHP `pathinfo()` as either an associative array or one component string. -pub(in crate::interpreter) fn eval_pathinfo_result( - path: RuntimeCellHandle, - flags: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = values.string_bytes(path)?; - let Some(flags) = flags else { - return eval_pathinfo_array_result(&path, values); - }; - let flags = eval_int_value(flags, values)?; - if flags == EVAL_PATHINFO_ALL { - return eval_pathinfo_array_result(&path, values); - } - let component = eval_pathinfo_component_bytes(&path, flags); - values.string_bytes_value(&component) -} - -/// Builds the PHP `pathinfo()` associative-array result for all components. -pub(in crate::interpreter) fn eval_pathinfo_array_result( - path: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(4)?; - if !path.is_empty() { - let dirname = eval_pathinfo_dirname_bytes(path); - result = eval_pathinfo_array_set(result, "dirname", &dirname, values)?; - } - let parts = eval_pathinfo_parts(path); - result = eval_pathinfo_array_set(result, "basename", &parts.basename, values)?; - if parts.has_extension { - result = eval_pathinfo_array_set(result, "extension", &parts.extension, values)?; - } - eval_pathinfo_array_set(result, "filename", &parts.filename, values) -} - -/// Inserts one string component into a PHP `pathinfo()` associative result. -pub(in crate::interpreter) fn eval_pathinfo_array_set( - array: RuntimeCellHandle, - key: &str, - value: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.string_bytes_value(value)?; - values.array_set(array, key, value) -} - -/// Returns one PHP `pathinfo()` component for a non-all bitmask. -pub(in crate::interpreter) fn eval_pathinfo_component_bytes(path: &[u8], flags: i64) -> Vec { - if flags & EVAL_PATHINFO_DIRNAME != 0 { - return eval_pathinfo_dirname_bytes(path); - } - let parts = eval_pathinfo_parts(path); - if flags & EVAL_PATHINFO_BASENAME != 0 { - return parts.basename; - } - if flags & EVAL_PATHINFO_EXTENSION != 0 { - return parts.extension; - } - if flags & EVAL_PATHINFO_FILENAME != 0 { - return parts.filename; - } - Vec::new() -} - -/// Computes the dirname component with `pathinfo("")`'s empty-string exception. -pub(in crate::interpreter) fn eval_pathinfo_dirname_bytes(path: &[u8]) -> Vec { - if path.is_empty() { - Vec::new() - } else { - eval_dirname_once(path) - } -} - -/// Splits pathinfo basename, extension, and filename components. -pub(in crate::interpreter) fn eval_pathinfo_parts(path: &[u8]) -> EvalPathInfoParts { - let basename = eval_basename_bytes(path, None); - let Some(dot) = basename.iter().rposition(|byte| *byte == b'.') else { - return EvalPathInfoParts { - filename: basename.clone(), - basename, - extension: Vec::new(), - has_extension: false, - }; - }; - EvalPathInfoParts { - filename: basename[..dot].to_vec(), - extension: basename[dot + 1..].to_vec(), - basename, - has_extension: true, - } -} - -/// Pathinfo components derived from a basename. -pub(in crate::interpreter) struct EvalPathInfoParts { - /// Full basename component. - basename: Vec, - /// Extension component after the final dot, possibly empty for trailing-dot names. - extension: Vec, - /// Filename component before the final dot. - filename: Vec, - /// Whether the basename contained a dot and therefore has an extension key. - has_extension: bool, -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs index e94091ec88..f6cfe30fc1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs @@ -19,11 +19,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "basename" => match evaluated_args { - [path] => eval_basename_result(*path, None, values)?, - [path, suffix] => eval_basename_result(*path, Some(*suffix), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "chdir" | "mkdir" | "rmdir" => match evaluated_args { [path] => eval_unary_path_bool_result(name, *path, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), @@ -48,11 +43,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [from, to] => eval_binary_path_bool_result(name, *from, *to, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "dirname" => match evaluated_args { - [path] => eval_dirname_result(*path, None, values)?, - [path, levels] => eval_dirname_result(*path, Some(*levels), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "disk_free_space" | "disk_total_space" => match evaluated_args { [directory] => eval_disk_space_result(name, *directory, values)?, _ => return Err(EvalStatus::RuntimeFatal), @@ -112,11 +102,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [directory] => eval_opendir_result(*directory, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "pathinfo" => match evaluated_args { - [path] => eval_pathinfo_result(*path, None, values)?, - [path, flags] => eval_pathinfo_result(*path, Some(*flags), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "pclose" => match evaluated_args { [handle] => eval_pclose_result(*handle, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), @@ -137,10 +122,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [path] => eval_readlink_result(*path, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "realpath" => match evaluated_args { - [path] => eval_realpath_result(*path, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "realpath_cache_get" => match evaluated_args { [] => eval_realpath_cache_get_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs index e50c3626fb..9eabaf8824 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs @@ -26,14 +26,145 @@ pub(in crate::interpreter) fn eval_pathinfo_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("pathinfo", args, context, scope, values) + eval_builtin_pathinfo(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `pathinfo` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_pathinfo_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_pathinfo_result(*path, None, values), + [path, flags] => eval_pathinfo_result(*path, Some(*flags), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `pathinfo($path, $flags = PATHINFO_ALL)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_pathinfo( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [path] => { + let path = eval_expr(path, context, scope, values)?; + eval_pathinfo_result(path, None, values) + } + [path, flags] => { + let path = eval_expr(path, context, scope, values)?; + let flags = eval_expr(flags, context, scope, values)?; + eval_pathinfo_result(path, Some(flags), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Computes PHP `pathinfo()` as either an associative array or one component string. +pub(in crate::interpreter) fn eval_pathinfo_result( + path: RuntimeCellHandle, + flags: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = values.string_bytes(path)?; + let Some(flags) = flags else { + return eval_pathinfo_array_result(&path, values); + }; + let flags = eval_int_value(flags, values)?; + if flags == EVAL_PATHINFO_ALL { + return eval_pathinfo_array_result(&path, values); + } + let component = eval_pathinfo_component_bytes(&path, flags); + values.string_bytes_value(&component) +} + +/// Builds the PHP `pathinfo()` associative-array result for all components. +pub(in crate::interpreter) fn eval_pathinfo_array_result( + path: &[u8], values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("pathinfo", evaluated_args, context, values) + let mut result = values.assoc_new(4)?; + if !path.is_empty() { + let dirname = eval_pathinfo_dirname_bytes(path); + result = eval_pathinfo_array_set(result, "dirname", &dirname, values)?; + } + let parts = eval_pathinfo_parts(path); + result = eval_pathinfo_array_set(result, "basename", &parts.basename, values)?; + if parts.has_extension { + result = eval_pathinfo_array_set(result, "extension", &parts.extension, values)?; + } + eval_pathinfo_array_set(result, "filename", &parts.filename, values) +} + +/// Inserts one string component into a PHP `pathinfo()` associative result. +pub(in crate::interpreter) fn eval_pathinfo_array_set( + array: RuntimeCellHandle, + key: &str, + value: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) +} + +/// Returns one PHP `pathinfo()` component for a non-all bitmask. +pub(in crate::interpreter) fn eval_pathinfo_component_bytes(path: &[u8], flags: i64) -> Vec { + if flags & EVAL_PATHINFO_DIRNAME != 0 { + return eval_pathinfo_dirname_bytes(path); + } + let parts = eval_pathinfo_parts(path); + if flags & EVAL_PATHINFO_BASENAME != 0 { + return parts.basename; + } + if flags & EVAL_PATHINFO_EXTENSION != 0 { + return parts.extension; + } + if flags & EVAL_PATHINFO_FILENAME != 0 { + return parts.filename; + } + Vec::new() +} + +/// Computes the dirname component with `pathinfo("")`'s empty-string exception. +pub(in crate::interpreter) fn eval_pathinfo_dirname_bytes(path: &[u8]) -> Vec { + if path.is_empty() { + Vec::new() + } else { + super::dirname::eval_dirname_once(path) + } +} + +/// Splits pathinfo basename, extension, and filename components. +pub(in crate::interpreter) fn eval_pathinfo_parts(path: &[u8]) -> EvalPathInfoParts { + let basename = super::basename::eval_basename_bytes(path, None); + let Some(dot) = basename.iter().rposition(|byte| *byte == b'.') else { + return EvalPathInfoParts { + filename: basename.clone(), + basename, + extension: Vec::new(), + has_extension: false, + }; + }; + EvalPathInfoParts { + filename: basename[..dot].to_vec(), + extension: basename[dot + 1..].to_vec(), + basename, + has_extension: true, + } +} + +/// Pathinfo components derived from a basename. +pub(in crate::interpreter) struct EvalPathInfoParts { + /// Full basename component. + basename: Vec, + /// Extension component after the final dot, possibly empty for trailing-dot names. + extension: Vec, + /// Filename component before the final dot. + filename: Vec, + /// Whether the basename contained a dot and therefore has an extension key. + has_extension: bool, } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs index bd5fbd9a65..650b5f5abb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs @@ -24,14 +24,45 @@ pub(in crate::interpreter) fn eval_realpath_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("realpath", args, context, scope, values) + eval_builtin_realpath(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `realpath` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_realpath_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_realpath_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `realpath($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_realpath( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_realpath_result(path, values) +} + +/// Canonicalizes one path or returns PHP false when the path cannot be resolved. +pub(in crate::interpreter) fn eval_realpath_result( + path: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("realpath", evaluated_args, context, values) + let path = values.string_bytes(path)?; + let path = String::from_utf8_lossy(&path); + let Ok(canonical) = std::fs::canonicalize(path.as_ref()) else { + return values.bool_value(false); + }; + let canonical = canonical.to_string_lossy(); + values.string(canonical.as_ref()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs index d4b2057b19..4bb9824f55 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs @@ -24,14 +24,39 @@ pub(in crate::interpreter) fn eval_stream_resolve_include_path_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_resolve_include_path", args, context, scope, values) + eval_builtin_stream_resolve_include_path(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_resolve_include_path` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_resolve_include_path_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [filename] => eval_stream_resolve_include_path_result(*filename, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `stream_resolve_include_path($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_resolve_include_path( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_stream_resolve_include_path_result(filename, values) +} + +/// Resolves one filename using elephc's realpath-equivalent include-path semantics. +pub(in crate::interpreter) fn eval_stream_resolve_include_path_result( + filename: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_resolve_include_path", evaluated_args, context, values) + super::realpath::eval_realpath_result(filename, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs index 247aa5a71d..8e838c1dc7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs @@ -254,10 +254,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value } _ => return Err(EvalStatus::RuntimeFatal), }, - "stream_resolve_include_path" => match evaluated_args { - [filename] => eval_stream_resolve_include_path_result(*filename, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "stream_isatty" => match evaluated_args { [stream] => eval_stream_isatty_result(*stream, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), From af7708e0b8f58aa606048fce4de78337ef5ab3db Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:20:47 +0200 Subject: [PATCH 1114/1208] refactor(magician): move filesystem content builtin implementations home --- .../builtins/filesystem/direct_dispatch.rs | 4 - .../interpreter/builtins/filesystem/file.rs | 70 +++++- .../builtins/filesystem/file_contents.rs | 211 ------------------ .../builtins/filesystem/file_get_contents.rs | 61 ++++- .../builtins/filesystem/file_io.rs | 2 +- .../builtins/filesystem/file_put_contents.rs | 53 ++++- .../interpreter/builtins/filesystem/mod.rs | 2 - .../filesystem/path_values_dispatch.rs | 18 -- .../builtins/filesystem/readfile.rs | 48 +++- 9 files changed, 225 insertions(+), 244 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/file_contents.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index 1c013e5726..f05174b991 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -175,13 +175,10 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_disk_space(name, args, context, scope, values) } "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), - "file" => eval_builtin_file(args, context, scope, values), "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => { eval_builtin_file_probe(name, args, context, scope, values) } - "file_get_contents" => eval_builtin_file_get_contents(args, context, scope, values), - "file_put_contents" => eval_builtin_file_put_contents(args, context, scope, values), "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), "filesize" => eval_builtin_filesize(args, context, scope, values), @@ -208,7 +205,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "closedir" | "readdir" | "rewinddir" => { eval_builtin_unary_directory(name, args, context, scope, values) } - "readfile" => eval_builtin_readfile(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs index 6fea274f91..c9154d5090 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `file` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_file_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_file_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("file", args, context, scope, values) + eval_builtin_file(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `file` filesystem builtin through the area dispatcher. @@ -33,5 +34,70 @@ pub(in crate::interpreter) fn eval_file_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("file", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_file_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `file($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_result(filename, context, values) +} + +/// Reads one local file or supported wrapper and returns indexed line byte strings. +pub(in crate::interpreter) fn eval_file_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { + if values.type_tag(result)? == EVAL_TAG_STRING { + let bytes = values.string_bytes(result)?; + return eval_file_lines_array(&bytes, values); + } + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + let bytes = match super::file_get_contents::eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => bytes, + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + return values.array_new(0); + } + }; + eval_file_lines_array(&bytes, values) +} + +/// Splits file payload bytes into runtime array entries, preserving trailing newlines. +fn eval_file_lines_array( + bytes: &[u8], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(0)?; + let mut line_start = 0; + let mut line_index = 0; + for (index, byte) in bytes.iter().enumerate() { + if *byte != b'\n' { + continue; + } + result = + eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; + line_start = index + 1; + line_index += 1; + } + if line_start < bytes.len() { + result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; + } + Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_contents.rs deleted file mode 100644 index 31d29a12dd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_contents.rs +++ /dev/null @@ -1,211 +0,0 @@ -//! Purpose: -//! Implements one-shot file content builtins for eval. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem` re-exports. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - Local paths, built-in URL wrappers, PHAR URLs, and userspace stream wrappers -//! share these entry points for content reads and writes. - -use super::super::super::*; -use super::*; -use crate::stream_wrappers; - -/// Evaluates PHP `file_get_contents($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_file_get_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_get_contents_result(filename, context, values) -} - -/// Reads a local file or supported wrapper into a PHP string, or returns false on failure. -pub(in crate::interpreter) fn eval_file_get_contents_result( - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { - return Ok(result); - } - match eval_read_path_or_wrapper_bytes(&path) { - Ok(bytes) => values.string_bytes_value(&bytes), - Err(_) => { - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - values.bool_value(false) - } - } -} - -/// Evaluates PHP `file($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_file( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_result(filename, context, values) -} - -/// Reads one local file or supported wrapper and returns indexed line byte strings. -pub(in crate::interpreter) fn eval_file_result( - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { - if values.type_tag(result)? == EVAL_TAG_STRING { - let bytes = values.string_bytes(result)?; - return eval_file_lines_array(&bytes, values); - } - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - return values.array_new(0); - } - let bytes = match eval_read_path_or_wrapper_bytes(&path) { - Ok(bytes) => bytes, - Err(_) => { - values.warning("Warning: file_get_contents(): Failed to open stream\n")?; - return values.array_new(0); - } - }; - eval_file_lines_array(&bytes, values) -} - -/// Splits file payload bytes into runtime array entries, preserving trailing newlines. -fn eval_file_lines_array( - bytes: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(0)?; - let mut line_start = 0; - let mut line_index = 0; - for (index, byte) in bytes.iter().enumerate() { - if *byte != b'\n' { - continue; - } - result = - eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; - line_start = index + 1; - line_index += 1; - } - if line_start < bytes.len() { - result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; - } - Ok(result) -} - -/// Evaluates PHP `readfile($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_readfile( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_readfile_result(filename, context, values) -} - -/// Streams one local file or supported wrapper to eval output. -pub(in crate::interpreter) fn eval_readfile_result( - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(result) = eval_user_wrapper_readfile_result(&path, context, values)? { - return Ok(result); - } - if let Some(local_path) = stream_wrappers::local_filesystem_path(&path) { - let path = std::path::Path::new(&local_path); - if path.is_dir() { - return values.int(-1); - } - } - let bytes = match eval_read_path_or_wrapper_bytes(&path) { - Ok(bytes) => bytes, - Err(_) => return values.bool_value(false), - }; - let output = values.string_bytes_value(&bytes)?; - values.echo(output)?; - values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_file_put_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, data] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let data = eval_expr(data, context, scope, values)?; - eval_file_put_contents_result(filename, data, context, values) -} - -/// Writes a PHP string to a local file or supported wrapper and returns a byte count. -pub(in crate::interpreter) fn eval_file_put_contents_result( - filename: RuntimeCellHandle, - data: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let data = values.string_bytes(data)?; - if stream_wrappers::is_phar_stream(&path) { - return match elephc_phar::put_url_bytes(path.as_bytes(), &data) { - Some(len) => values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?), - None => values.bool_value(false), - }; - } - if let Some(result) = - eval_user_wrapper_file_put_contents_result(&path, &data, context, values)? - { - return Ok(result); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - match std::fs::write(path, &data) { - Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), - Err(_) => values.bool_value(false), - } -} - -/// Reads bytes from supported direct path or stream-wrapper URLs. -pub(in crate::interpreter) fn eval_read_path_or_wrapper_bytes( - path: &str, -) -> Result, ()> { - if stream_wrappers::is_data_stream(path) { - return stream_wrappers::decode_data_uri(path).ok_or(()); - } - if stream_wrappers::is_phar_stream(path) { - return elephc_phar::extract_url_bytes(path.as_bytes()).ok_or(()); - } - if stream_wrappers::is_http_stream(path) { - return stream_wrappers::read_http_url(path).ok_or(()); - } - let Some(path) = stream_wrappers::local_filesystem_path(path) else { - return Err(()); - }; - std::fs::read(path).map_err(|_| ()) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs index bccde03fa5..1ffa82f2e3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use super::*; +use crate::stream_wrappers; /// Dispatches direct eval calls for the `file_get_contents` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_file_get_contents_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_file_get_contents_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("file_get_contents", args, context, scope, values) + eval_builtin_file_get_contents(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `file_get_contents` filesystem builtin through the area dispatcher. @@ -33,5 +35,60 @@ pub(in crate::interpreter) fn eval_file_get_contents_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("file_get_contents", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_file_get_contents_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `file_get_contents($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_get_contents_result(filename, context, values) +} + +/// Reads a local file or supported wrapper into a PHP string, or returns false on failure. +pub(in crate::interpreter) fn eval_file_get_contents_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_file_get_contents_result(&path, context, values)? { + return Ok(result); + } + match eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => values.string_bytes_value(&bytes), + Err(_) => { + values.warning("Warning: file_get_contents(): Failed to open stream\n")?; + values.bool_value(false) + } + } +} + +/// Reads bytes from supported direct path or stream-wrapper URLs. +pub(in crate::interpreter) fn eval_read_path_or_wrapper_bytes( + path: &str, +) -> Result, ()> { + if stream_wrappers::is_data_stream(path) { + return stream_wrappers::decode_data_uri(path).ok_or(()); + } + if stream_wrappers::is_phar_stream(path) { + return elephc_phar::extract_url_bytes(path.as_bytes()).ok_or(()); + } + if stream_wrappers::is_http_stream(path) { + return stream_wrappers::read_http_url(path).ok_or(()); + } + let Some(path) = stream_wrappers::local_filesystem_path(path) else { + return Err(()); + }; + std::fs::read(path).map_err(|_| ()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs index 9d04ab78b9..a57afdc2bd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs @@ -156,7 +156,7 @@ pub(in crate::interpreter) fn eval_filesize_result( let size = eval_user_wrapper_stat_int_field(stat, "size", values)?.unwrap_or(0); return values.int(size); } - if let Ok(bytes) = eval_read_path_or_wrapper_bytes(&path) { + if let Ok(bytes) = super::file_get_contents::eval_read_path_or_wrapper_bytes(&path) { return values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?); } let Some(path) = stream_wrappers::local_filesystem_path(&path) else { diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs index bd65c549e2..eda110509c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use super::*; +use crate::stream_wrappers; /// Dispatches direct eval calls for the `file_put_contents` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_file_put_contents_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_file_put_contents_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("file_put_contents", args, context, scope, values) + eval_builtin_file_put_contents(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `file_put_contents` filesystem builtin through the area dispatcher. @@ -33,5 +35,52 @@ pub(in crate::interpreter) fn eval_file_put_contents_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("file_put_contents", evaluated_args, context, values) + match evaluated_args { + [filename, data] => eval_file_put_contents_result(*filename, *data, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `file_put_contents($filename, $data)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_file_put_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_file_put_contents_result(filename, data, context, values) +} + +/// Writes a PHP string to a local file or supported wrapper and returns a byte count. +pub(in crate::interpreter) fn eval_file_put_contents_result( + filename: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let data = values.string_bytes(data)?; + if stream_wrappers::is_phar_stream(&path) { + return match elephc_phar::put_url_bytes(path.as_bytes(), &data) { + Some(len) => values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + }; + } + if let Some(result) = + eval_user_wrapper_file_put_contents_result(&path, &data, context, values)? + { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + match std::fs::write(path, &data) { + Ok(()) => values.int(i64::try_from(data.len()).map_err(|_| EvalStatus::RuntimeFatal)?), + Err(_) => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 6e5ff46058..c623ff9dfa 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -30,7 +30,6 @@ mod fgetc; mod fgetcsv; mod fgets; mod file; -mod file_contents; mod file_exists; mod file_get_contents; mod file_io; @@ -162,7 +161,6 @@ mod values_dispatch; mod vfprintf; pub(in crate::interpreter) use directories::*; -pub(in crate::interpreter) use file_contents::*; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use ops::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs index f6cfe30fc1..03a3a1bdee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs @@ -47,25 +47,11 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [directory] => eval_disk_space_result(name, *directory, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "file" => match evaluated_args { - [filename] => eval_file_result(*filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => match evaluated_args { [filename] => eval_file_probe_result(name, *filename, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "file_get_contents" => match evaluated_args { - [filename] => eval_file_get_contents_result(*filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "file_put_contents" => match evaluated_args { - [filename, data] => { - eval_file_put_contents_result(*filename, *data, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" | "fileperms" => match evaluated_args { [filename] => eval_file_stat_scalar_result(name, *filename, context, values)?, @@ -114,10 +100,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [dir_handle] => eval_unary_directory_result(name, *dir_handle, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "readfile" => match evaluated_args { - [filename] => eval_readfile_result(*filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "readlink" => match evaluated_args { [path] => eval_readlink_result(*path, values)?, _ => return Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs index 45925547c6..62977e0b7e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use super::*; +use crate::stream_wrappers; /// Dispatches direct eval calls for the `readfile` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_readfile_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_readfile_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("readfile", args, context, scope, values) + eval_builtin_readfile(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `readfile` filesystem builtin through the area dispatcher. @@ -33,5 +35,47 @@ pub(in crate::interpreter) fn eval_readfile_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("readfile", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_readfile_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `readfile($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_readfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_readfile_result(filename, context, values) +} + +/// Streams one local file or supported wrapper to eval output. +pub(in crate::interpreter) fn eval_readfile_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(result) = eval_user_wrapper_readfile_result(&path, context, values)? { + return Ok(result); + } + if let Some(local_path) = stream_wrappers::local_filesystem_path(&path) { + let path = std::path::Path::new(&local_path); + if path.is_dir() { + return values.int(-1); + } + } + let bytes = match super::file_get_contents::eval_read_path_or_wrapper_bytes(&path) { + Ok(bytes) => bytes, + Err(_) => return values.bool_value(false), + }; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?) } From e73b5535158f0ccf04097dc3ca07f8b2193a7905 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:27:57 +0200 Subject: [PATCH 1115/1208] refactor(magician): move filesystem ops builtin implementations home --- .../interpreter/builtins/filesystem/chdir.rs | 47 ++- .../interpreter/builtins/filesystem/chgrp.rs | 7 +- .../interpreter/builtins/filesystem/chmod.rs | 50 ++- .../interpreter/builtins/filesystem/chown.rs | 159 +++++++++- .../builtins/filesystem/clearstatcache.rs | 23 +- .../interpreter/builtins/filesystem/copy.rs | 56 +++- .../builtins/filesystem/direct_dispatch.rs | 22 -- .../builtins/filesystem/disk_free_space.rs | 57 +++- .../builtins/filesystem/disk_total_space.rs | 9 +- .../interpreter/builtins/filesystem/file.rs | 4 +- .../interpreter/builtins/filesystem/glob.rs | 131 +++++++- .../interpreter/builtins/filesystem/lchgrp.rs | 7 +- .../interpreter/builtins/filesystem/lchown.rs | 7 +- .../interpreter/builtins/filesystem/link.rs | 7 +- .../builtins/filesystem/linkinfo.rs | 38 ++- .../interpreter/builtins/filesystem/mkdir.rs | 7 +- .../interpreter/builtins/filesystem/mod.rs | 2 - .../builtins/filesystem/ops/disk.rs | 60 ---- .../builtins/filesystem/ops/glob.rs | 136 -------- .../builtins/filesystem/ops/links.rs | 121 -------- .../builtins/filesystem/ops/listing.rs | 60 ---- .../builtins/filesystem/ops/mod.rs | 27 -- .../builtins/filesystem/ops/path_bool.rs | 291 ------------------ .../builtins/filesystem/ops/tempnam.rs | 65 ---- .../builtins/filesystem/ops/touch.rs | 145 --------- .../builtins/filesystem/ops/umask.rs | 47 --- .../filesystem/path_values_dispatch.rs | 67 ---- .../builtins/filesystem/readlink.rs | 37 ++- .../interpreter/builtins/filesystem/rename.rs | 7 +- .../interpreter/builtins/filesystem/rmdir.rs | 7 +- .../builtins/filesystem/scandir.rs | 56 +++- .../builtins/filesystem/streams/csv_format.rs | 2 +- .../builtins/filesystem/symlink.rs | 7 +- .../builtins/filesystem/tempnam.rs | 61 +++- .../interpreter/builtins/filesystem/touch.rs | 144 ++++++++- .../interpreter/builtins/filesystem/umask.rs | 44 ++- .../interpreter/builtins/filesystem/unlink.rs | 42 ++- .../filesystem/user_wrapper_metadata.rs | 4 +- .../filesystem/user_wrapper_path_ops.rs | 4 +- 39 files changed, 970 insertions(+), 1097 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/disk.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/glob.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/listing.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/mod.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/tempnam.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/ops/umask.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs index fd886064d4..49938bd93d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `chdir` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_chdir_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_chdir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("chdir", args, context, scope, values) + eval_builtin_unary_path_bool("chdir", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `chdir` filesystem builtin through the area dispatcher. @@ -33,5 +35,46 @@ pub(in crate::interpreter) fn eval_chdir_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("chdir", evaluated_args, context, values) + match evaluated_args { + [path] => eval_unary_path_bool_result("chdir", *path, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates a one-path filesystem operation that returns a PHP boolean. +pub(in crate::interpreter) fn eval_builtin_unary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_unary_path_bool_result(name, path, context, values) +} + +/// Executes a one-path filesystem operation and returns whether it succeeded. +pub(in crate::interpreter) fn eval_unary_path_bool_result( + name: &str, + path: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(path, values)?; + if let Some(result) = eval_user_wrapper_single_path_op_result(name, &path, context, values)? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let ok = match name { + "chdir" => std::env::set_current_dir(path).is_ok(), + "mkdir" => std::fs::create_dir(path).is_ok(), + "rmdir" => std::fs::remove_dir(path).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs index f27affbf93..b01acb2932 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_chgrp_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("chgrp", args, context, scope, values) + super::chown::eval_builtin_chown_like("chgrp", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `chgrp` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_chgrp_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("chgrp", evaluated_args, context, values) + match evaluated_args { + [filename, principal] => super::chown::eval_chown_like_result("chgrp", *filename, *principal, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs index 09d5ecdd0e..0f580a1f8f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `chmod` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_chmod_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_chmod_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("chmod", args, context, scope, values) + eval_builtin_chmod(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `chmod` filesystem builtin through the area dispatcher. @@ -33,5 +35,49 @@ pub(in crate::interpreter) fn eval_chmod_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("chmod", evaluated_args, context, values) + match evaluated_args { + [filename, permissions] => eval_chmod_result(*filename, *permissions, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_chmod( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, permissions] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let permissions = eval_expr(permissions, context, scope, values)?; + eval_chmod_result(filename, permissions, context, values) +} + +/// Changes one local file's mode and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_chmod_result( + filename: RuntimeCellHandle, + permissions: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let mode = eval_int_value(permissions, values)? as u32; + let metadata_value = values.int(i64::from(mode))?; + if let Some(result) = eval_user_wrapper_stream_metadata_result( + &path, + EVAL_STREAM_META_ACCESS, + metadata_value, + context, + values, + )? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let permissions = std::fs::Permissions::from_mode(mode); + values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs index 228ea6e04a..ad33ac4f01 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs @@ -16,6 +16,9 @@ eval_builtin! { } use super::super::super::*; +use std::ffi::CString; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `chown` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_chown_declared_call( @@ -24,7 +27,7 @@ pub(in crate::interpreter) fn eval_chown_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("chown", args, context, scope, values) + eval_builtin_chown_like("chown", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `chown` filesystem builtin through the area dispatcher. @@ -33,5 +36,157 @@ pub(in crate::interpreter) fn eval_chown_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("chown", evaluated_args, context, values) + match evaluated_args { + [filename, principal] => eval_chown_like_result("chown", *filename, *principal, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP ownership/group path mutation builtins over eval expressions. +pub(in crate::interpreter) fn eval_builtin_chown_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename, principal] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + let principal = eval_expr(principal, context, scope, values)?; + eval_chown_like_result(name, filename, principal, context, values) +} + +/// Changes one local path owner or group and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_chown_like_result( + name: &str, + filename: RuntimeCellHandle, + principal: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if matches!(name, "chown" | "chgrp") { + let (option, metadata_value) = + eval_chown_metadata_arg(name, principal, values)?; + if let Some(result) = + eval_user_wrapper_stream_metadata_result(&path, option, metadata_value, context, values)? + { + return Ok(result); + } + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let Some(path) = eval_c_string(&path) else { + return values.bool_value(false); + }; + let Some((uid, gid)) = eval_chown_principal_ids(name, principal, values)? else { + return values.bool_value(false); + }; + let status = unsafe { + match name { + "chown" | "chgrp" => libc::chown(path.as_ptr(), uid, gid), + "lchown" | "lchgrp" => libc::lchown(path.as_ptr(), uid, gid), + _ => return Err(EvalStatus::RuntimeFatal), + } + }; + values.bool_value(status == 0) +} + +/// Builds the wrapper metadata option and value for `chown()` or `chgrp()`. +fn eval_chown_metadata_arg( + name: &str, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(i64, RuntimeCellHandle), EvalStatus> { + match (name, values.type_tag(principal)?) { + ("chown", EVAL_TAG_INT) => { + let principal = eval_int_value(principal, values)?; + Ok((EVAL_STREAM_META_OWNER, values.int(principal)?)) + } + ("chgrp", EVAL_TAG_INT) => { + let principal = eval_int_value(principal, values)?; + Ok((EVAL_STREAM_META_GROUP, values.int(principal)?)) + } + ("chown", EVAL_TAG_STRING) => Ok((EVAL_STREAM_META_OWNER_NAME, principal)), + ("chgrp", EVAL_TAG_STRING) => Ok((EVAL_STREAM_META_GROUP_NAME, principal)), + ("chown" | "chgrp", _) => Err(EvalStatus::RuntimeFatal), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves one PHP owner/group argument into libc uid/gid slots. +fn eval_chown_principal_ids( + name: &str, + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match (name, values.type_tag(principal)?) { + ("chown" | "lchown", EVAL_TAG_INT) => { + Ok(Some(( + eval_int_value(principal, values)? as libc::uid_t, + !0 as libc::gid_t, + ))) + } + ("chgrp" | "lchgrp", EVAL_TAG_INT) => { + Ok(Some(( + !0 as libc::uid_t, + eval_int_value(principal, values)? as libc::gid_t, + ))) + } + ("chown" | "lchown", EVAL_TAG_STRING) => { + Ok(eval_owner_name_id(principal, values)?.map(|uid| (uid, !0 as libc::gid_t))) + } + ("chgrp" | "lchgrp", EVAL_TAG_STRING) => { + Ok(eval_group_name_id(principal, values)?.map(|gid| (!0 as libc::uid_t, gid))) + } + ("chown" | "chgrp" | "lchown" | "lchgrp", _) => Err(EvalStatus::RuntimeFatal), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves a PHP user-name cell to a libc uid. +fn eval_owner_name_id( + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let name = values.string_bytes(principal)?; + let Some(name) = eval_c_bytes(&name) else { + return Ok(None); + }; + let passwd = unsafe { libc::getpwnam(name.as_ptr()) }; + if passwd.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { (*passwd).pw_uid })) + } +} + +/// Resolves a PHP group-name cell to a libc gid. +fn eval_group_name_id( + principal: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let name = values.string_bytes(principal)?; + let Some(name) = eval_c_bytes(&name) else { + return Ok(None); + }; + let group = unsafe { libc::getgrnam(name.as_ptr()) }; + if group.is_null() { + Ok(None) + } else { + Ok(Some(unsafe { (*group).gr_gid })) + } +} + +/// Converts a Rust path string into a C string, rejecting embedded NUL bytes. +fn eval_c_string(value: &str) -> Option { + CString::new(value).ok() +} + +/// Converts raw PHP bytes into a C string, rejecting embedded NUL bytes. +fn eval_c_bytes(value: &[u8]) -> Option { + CString::new(value).ok() } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs index 1437e5fcf2..a8194d0bac 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs @@ -29,14 +29,33 @@ pub(in crate::interpreter) fn eval_clearstatcache_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("clearstatcache", args, context, scope, values) + eval_builtin_clearstatcache(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `clearstatcache` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_clearstatcache_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + values.null() +} + +/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. +pub(in crate::interpreter) fn eval_builtin_clearstatcache( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("clearstatcache", evaluated_args, context, values) + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + values.null() } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs index 78c2b7a899..2e762e9257 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `copy` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_copy_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_copy_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("copy", args, context, scope, values) + eval_builtin_binary_path_bool("copy", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `copy` filesystem builtin through the area dispatcher. @@ -33,5 +35,55 @@ pub(in crate::interpreter) fn eval_copy_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("copy", evaluated_args, context, values) + match evaluated_args { + [from, to] => eval_binary_path_bool_result("copy", *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates a two-path filesystem operation that returns a PHP boolean. +pub(in crate::interpreter) fn eval_builtin_binary_path_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [from, to] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let from = eval_expr(from, context, scope, values)?; + let to = eval_expr(to, context, scope, values)?; + eval_binary_path_bool_result(name, from, to, context, values) +} + +/// Executes a two-path filesystem operation and returns whether it succeeded. +pub(in crate::interpreter) fn eval_binary_path_bool_result( + name: &str, + from: RuntimeCellHandle, + to: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_path_string(from, values)?; + let to = eval_path_string(to, values)?; + if name == "rename" { + if let Some(result) = eval_user_wrapper_rename_result(&from, &to, context, values)? { + return Ok(result); + } + } + let Some(from) = stream_wrappers::local_filesystem_path(&from) else { + return values.bool_value(false); + }; + let Some(to) = stream_wrappers::local_filesystem_path(&to) else { + return values.bool_value(false); + }; + let ok = match name { + "copy" => std::fs::copy(from, to).is_ok(), + "link" => std::fs::hard_link(from, to).is_ok(), + "rename" => std::fs::rename(from, to).is_ok(), + "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index f05174b991..f8db5dbfa9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -160,20 +160,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( ) -> Result { match name { "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), - "chdir" | "mkdir" | "rmdir" => { - eval_builtin_unary_path_bool(name, args, context, scope, values) - } - "chmod" => eval_builtin_chmod(args, context, scope, values), - "chown" | "chgrp" | "lchown" | "lchgrp" => { - eval_builtin_chown_like(name, args, context, scope, values) - } - "clearstatcache" => eval_builtin_clearstatcache(args, context, scope, values), - "copy" | "link" | "rename" | "symlink" => { - eval_builtin_binary_path_bool(name, args, context, scope, values) - } - "disk_free_space" | "disk_total_space" => { - eval_builtin_disk_space(name, args, context, scope, values) - } "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => { @@ -197,8 +183,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), "fwrite" => eval_builtin_fwrite(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), - "glob" => eval_builtin_glob(args, context, scope, values), - "linkinfo" => eval_builtin_linkinfo(args, context, scope, values), "opendir" => eval_builtin_opendir(args, context, scope, values), "pclose" => eval_builtin_pclose(args, context, scope, values), "popen" => eval_builtin_popen(args, context, scope, values), @@ -206,10 +190,8 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_unary_directory(name, args, context, scope, values) } "readline" => eval_builtin_readline(args, context, scope, values), - "readlink" => eval_builtin_readlink(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "scandir" => eval_builtin_scandir(args, context, scope, values), "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "stream_bucket_append" | "stream_bucket_prepend" => { eval_builtin_stream_bucket_push(name, args, context, scope, values) @@ -270,11 +252,7 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_stream_wrapper_registry(name, args, context, scope, values) } "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), - "tempnam" => eval_builtin_tempnam(args, context, scope, values), "tmpfile" => eval_builtin_tmpfile(args, context, values), - "touch" => eval_builtin_touch(args, context, scope, values), - "umask" => eval_builtin_umask(args, context, scope, values), - "unlink" => eval_builtin_unlink(args, context, scope, values), "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs index 9f339da3b0..206167594f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs @@ -24,14 +24,67 @@ pub(in crate::interpreter) fn eval_disk_free_space_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("disk_free_space", args, context, scope, values) + eval_builtin_disk_space("disk_free_space", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `disk_free_space` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_disk_free_space_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [directory] => eval_disk_space_result("disk_free_space", *directory, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. +pub(in crate::interpreter) fn eval_builtin_disk_space( + name: &str, + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_disk_space_result(name, directory, values) +} + +/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. +pub(in crate::interpreter) fn eval_disk_space_result( + name: &str, + directory: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("disk_free_space", evaluated_args, context, values) + let bytes = values.string_bytes(directory)?; + let Ok(path) = CString::new(bytes) else { + return values.float(0.0); + }; + let mut stats = std::mem::MaybeUninit::::zeroed(); + let status = unsafe { + // libc writes the statvfs fields for this NUL-terminated local path. + libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) + }; + if status != 0 { + return values.float(0.0); + } + let stats = unsafe { + // `statvfs` succeeded, so libc initialized the full stat buffer. + stats.assume_init() + }; + let block_size = if stats.f_frsize > 0 { + stats.f_frsize + } else { + stats.f_bsize + }; + let blocks = match name { + "disk_free_space" => stats.f_bavail, + "disk_total_space" => stats.f_blocks, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.float((block_size as f64) * (blocks as f64)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs index da9847e072..06ee8a1cbf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs @@ -24,14 +24,17 @@ pub(in crate::interpreter) fn eval_disk_total_space_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("disk_total_space", args, context, scope, values) + super::disk_free_space::eval_builtin_disk_space("disk_total_space", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `disk_total_space` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_disk_total_space_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("disk_total_space", evaluated_args, context, values) + match evaluated_args { + [directory] => super::disk_free_space::eval_disk_space_result("disk_total_space", *directory, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs index c9154d5090..c9343252a6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs @@ -92,12 +92,12 @@ fn eval_file_lines_array( continue; } result = - eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; + super::scandir::eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..=index], values)?; line_start = index + 1; line_index += 1; } if line_start < bytes.len() { - result = eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; + result = super::scandir::eval_array_set_indexed_bytes(result, line_index, &bytes[line_start..], values)?; } Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs index 66e9817d19..4e88040f84 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `glob` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_glob_declared_call( @@ -24,14 +25,140 @@ pub(in crate::interpreter) fn eval_glob_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("glob", args, context, scope, values) + eval_builtin_glob(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `glob` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_glob_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern] => eval_glob_result(*pattern, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `glob($pattern)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_glob( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [pattern] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let pattern = eval_expr(pattern, context, scope, values)?; + eval_glob_result(pattern, values) +} + +/// Expands one local glob pattern into a sorted indexed PHP string array. +pub(in crate::interpreter) fn eval_glob_result( + pattern: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("glob", evaluated_args, context, values) + let pattern = eval_path_string(pattern, values)?; + let matches = eval_glob_matches(&pattern); + let mut result = values.array_new(matches.len())?; + for (index, path) in matches.iter().enumerate() { + result = super::scandir::eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; + } + Ok(result) +} + +/// Collects sorted matches for one local glob pattern. +pub(in crate::interpreter) fn eval_glob_matches(pattern: &str) -> Vec { + if pattern.is_empty() { + return Vec::new(); + } + if !eval_glob_component_has_magic(pattern) { + return std::path::Path::new(pattern) + .exists() + .then(|| pattern.to_string()) + .into_iter() + .collect(); + } + let absolute = pattern.starts_with('/'); + let components: Vec<&str> = pattern + .split('/') + .filter(|component| !component.is_empty()) + .collect(); + let mut matches = Vec::new(); + let base = if absolute { + std::path::PathBuf::from("/") + } else { + std::path::PathBuf::from(".") + }; + let prefix = if absolute { "/" } else { "" }; + eval_glob_collect(&base, prefix, &components, &mut matches); + matches.sort(); + matches +} + +/// Recursively expands one glob path component at a time. +pub(in crate::interpreter) fn eval_glob_collect( + base: &std::path::Path, + prefix: &str, + components: &[&str], + matches: &mut Vec, +) { + let Some((component, rest)) = components.split_first() else { + if base.exists() && !prefix.is_empty() { + matches.push(prefix.to_string()); + } + return; + }; + if !eval_glob_component_has_magic(component) { + let next_base = base.join(component); + if rest.is_empty() { + if next_base.exists() { + matches.push(eval_glob_join_output(prefix, component)); + } + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, component); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + return; + } + let Ok(entries) = std::fs::read_dir(base) else { + return; + }; + let mut names = Vec::new(); + for entry in entries.flatten() { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + for name in names { + if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { + continue; + } + let next_base = base.join(&name); + if rest.is_empty() { + matches.push(eval_glob_join_output(prefix, &name)); + } else if next_base.is_dir() { + let next_prefix = eval_glob_join_output(prefix, &name); + eval_glob_collect(&next_base, &next_prefix, rest, matches); + } + } +} + +/// Joins a display path prefix and component while preserving absolute-root output. +pub(in crate::interpreter) fn eval_glob_join_output(prefix: &str, component: &str) -> String { + if prefix.is_empty() { + component.to_string() + } else if prefix == "/" { + format!("/{component}") + } else { + format!("{prefix}/{component}") + } +} + +/// Returns whether a glob component contains wildcard syntax. +pub(in crate::interpreter) fn eval_glob_component_has_magic(component: &str) -> bool { + component + .as_bytes() + .iter() + .any(|byte| matches!(byte, b'*' | b'?' | b'[')) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs index 1d6a188819..380b0309f4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_lchgrp_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("lchgrp", args, context, scope, values) + super::chown::eval_builtin_chown_like("lchgrp", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `lchgrp` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_lchgrp_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("lchgrp", evaluated_args, context, values) + match evaluated_args { + [filename, principal] => super::chown::eval_chown_like_result("lchgrp", *filename, *principal, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs index 8839d8f289..11df55d24e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_lchown_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("lchown", args, context, scope, values) + super::chown::eval_builtin_chown_like("lchown", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `lchown` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_lchown_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("lchown", evaluated_args, context, values) + match evaluated_args { + [filename, principal] => super::chown::eval_chown_like_result("lchown", *filename, *principal, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs index d8c402580f..f8fdab862c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_link_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("link", args, context, scope, values) + super::copy::eval_builtin_binary_path_bool("link", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `link` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_link_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("link", evaluated_args, context, values) + match evaluated_args { + [from, to] => super::copy::eval_binary_path_bool_result("link", *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs index 5bb7185992..1a5b477830 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; /// Dispatches direct eval calls for the `linkinfo` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_linkinfo_declared_call( @@ -24,14 +25,47 @@ pub(in crate::interpreter) fn eval_linkinfo_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("linkinfo", args, context, scope, values) + eval_builtin_linkinfo(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `linkinfo` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_linkinfo_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_linkinfo_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `linkinfo($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_linkinfo( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_linkinfo_result(path, values) +} + +/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. +pub(in crate::interpreter) fn eval_linkinfo_result( + path: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("linkinfo", evaluated_args, context, values) + let path = eval_path_string(path, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.int(-1); + }; + let dev = match std::fs::symlink_metadata(path) { + Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, + Err(_) => -1, + }; + values.int(dev) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs index f104ceb07d..abbeaf606e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_mkdir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("mkdir", args, context, scope, values) + super::chdir::eval_builtin_unary_path_bool("mkdir", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `mkdir` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_mkdir_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("mkdir", evaluated_args, context, values) + match evaluated_args { + [path] => super::chdir::eval_unary_path_bool_result("mkdir", *path, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index c623ff9dfa..b71d0bd8e2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -74,7 +74,6 @@ mod linkinfo; mod lstat; mod mkdir; mod opendir; -mod ops; mod path; mod path_values_dispatch; mod pathinfo; @@ -163,7 +162,6 @@ mod vfprintf; pub(in crate::interpreter) use directories::*; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; -pub(in crate::interpreter) use ops::*; pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use process_pipes::*; pub(in crate::interpreter) use readline::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/disk.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/disk.rs deleted file mode 100644 index 4ed51e447c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/disk.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Purpose: -//! Implements disk-space filesystem eval builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` re-exports. -//! -//! Key details: -//! - `statvfs` failures map to PHP-compatible `0.0` results. - -use super::super::super::super::*; - -/// Evaluates PHP `disk_free_space($directory)` or `disk_total_space($directory)`. -pub(in crate::interpreter) fn eval_builtin_disk_space( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_disk_space_result(name, directory, values) -} - -/// Reports available or total filesystem bytes as a PHP float, or 0.0 on failure. -pub(in crate::interpreter) fn eval_disk_space_result( - name: &str, - directory: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(directory)?; - let Ok(path) = CString::new(bytes) else { - return values.float(0.0); - }; - let mut stats = std::mem::MaybeUninit::::zeroed(); - let status = unsafe { - // libc writes the statvfs fields for this NUL-terminated local path. - libc::statvfs(path.as_ptr(), stats.as_mut_ptr()) - }; - if status != 0 { - return values.float(0.0); - } - let stats = unsafe { - // `statvfs` succeeded, so libc initialized the full stat buffer. - stats.assume_init() - }; - let block_size = if stats.f_frsize > 0 { - stats.f_frsize - } else { - stats.f_bsize - }; - let blocks = match name { - "disk_free_space" => stats.f_bavail, - "disk_total_space" => stats.f_blocks, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.float((block_size as f64) * (blocks as f64)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/glob.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/glob.rs deleted file mode 100644 index 77e35df918..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/glob.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! Purpose: -//! Implements local filesystem glob expansion for eval. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` re-exports. -//! -//! Key details: -//! - Components are expanded recursively and sorted for deterministic PHP array -//! output. - -use super::super::super::super::*; -use super::super::*; -use super::*; - -/// Evaluates PHP `glob($pattern)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_glob( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [pattern] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let pattern = eval_expr(pattern, context, scope, values)?; - eval_glob_result(pattern, values) -} - -/// Expands one local glob pattern into a sorted indexed PHP string array. -pub(in crate::interpreter) fn eval_glob_result( - pattern: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let pattern = eval_path_string(pattern, values)?; - let matches = eval_glob_matches(&pattern); - let mut result = values.array_new(matches.len())?; - for (index, path) in matches.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, path.as_bytes(), values)?; - } - Ok(result) -} - -/// Collects sorted matches for one local glob pattern. -pub(in crate::interpreter) fn eval_glob_matches(pattern: &str) -> Vec { - if pattern.is_empty() { - return Vec::new(); - } - if !eval_glob_component_has_magic(pattern) { - return std::path::Path::new(pattern) - .exists() - .then(|| pattern.to_string()) - .into_iter() - .collect(); - } - let absolute = pattern.starts_with('/'); - let components: Vec<&str> = pattern - .split('/') - .filter(|component| !component.is_empty()) - .collect(); - let mut matches = Vec::new(); - let base = if absolute { - std::path::PathBuf::from("/") - } else { - std::path::PathBuf::from(".") - }; - let prefix = if absolute { "/" } else { "" }; - eval_glob_collect(&base, prefix, &components, &mut matches); - matches.sort(); - matches -} - -/// Recursively expands one glob path component at a time. -pub(in crate::interpreter) fn eval_glob_collect( - base: &std::path::Path, - prefix: &str, - components: &[&str], - matches: &mut Vec, -) { - let Some((component, rest)) = components.split_first() else { - if base.exists() && !prefix.is_empty() { - matches.push(prefix.to_string()); - } - return; - }; - if !eval_glob_component_has_magic(component) { - let next_base = base.join(component); - if rest.is_empty() { - if next_base.exists() { - matches.push(eval_glob_join_output(prefix, component)); - } - } else if next_base.is_dir() { - let next_prefix = eval_glob_join_output(prefix, component); - eval_glob_collect(&next_base, &next_prefix, rest, matches); - } - return; - } - let Ok(entries) = std::fs::read_dir(base) else { - return; - }; - let mut names = Vec::new(); - for entry in entries.flatten() { - names.push(entry.file_name().to_string_lossy().into_owned()); - } - names.sort(); - for name in names { - if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { - continue; - } - let next_base = base.join(&name); - if rest.is_empty() { - matches.push(eval_glob_join_output(prefix, &name)); - } else if next_base.is_dir() { - let next_prefix = eval_glob_join_output(prefix, &name); - eval_glob_collect(&next_base, &next_prefix, rest, matches); - } - } -} - -/// Joins a display path prefix and component while preserving absolute-root output. -pub(in crate::interpreter) fn eval_glob_join_output(prefix: &str, component: &str) -> String { - if prefix.is_empty() { - component.to_string() - } else if prefix == "/" { - format!("/{component}") - } else { - format!("{prefix}/{component}") - } -} - -/// Returns whether a glob component contains wildcard syntax. -pub(in crate::interpreter) fn eval_glob_component_has_magic(component: &str) -> bool { - component - .as_bytes() - .iter() - .any(|byte| matches!(byte, b'*' | b'?' | b'[')) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs deleted file mode 100644 index cf1e43088a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/links.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Purpose: -//! Implements symbolic-link, clearstatcache, and unlink eval builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` re-exports. -//! -//! Key details: -//! - Link failures map to PHP false or documented sentinel values without raising -//! host IO errors through Rust panics. - -use super::super::super::super::*; -use super::super::*; -use crate::stream_wrappers; - -/// Evaluates PHP `readlink($path)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_readlink( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_readlink_result(path, values) -} - -/// Reads one symbolic-link target string, or returns PHP false on failure. -pub(in crate::interpreter) fn eval_readlink_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - match std::fs::read_link(path) { - Ok(target) => values.string(target.to_string_lossy().as_ref()), - Err(_) => values.bool_value(false), - } -} - -/// Evaluates PHP `linkinfo($path)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_linkinfo( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_linkinfo_result(path, values) -} - -/// Returns one symlink metadata device id, or PHP's `-1` failure sentinel. -pub(in crate::interpreter) fn eval_linkinfo_result( - path: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.int(-1); - }; - let dev = match std::fs::symlink_metadata(path) { - Ok(metadata) => i64::try_from(metadata.dev()).map_err(|_| EvalStatus::RuntimeFatal)?, - Err(_) => -1, - }; - values.int(dev) -} - -/// Evaluates `clearstatcache(...)` as an ordered no-op in eval. -pub(in crate::interpreter) fn eval_builtin_clearstatcache( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - eval_expr(arg, context, scope, values)?; - } - values.null() -} - -/// Evaluates PHP `unlink($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_unlink( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_unlink_result(filename, context, values) -} - -/// Deletes one path and returns whether it succeeded. -pub(in crate::interpreter) fn eval_unlink_result( - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if stream_wrappers::is_phar_stream(&path) { - return values.bool_value(elephc_phar::delete_url_bytes(path.as_bytes()).is_some()); - } - if let Some(result) = eval_user_wrapper_unlink_result(&path, context, values)? { - return Ok(result); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - values.bool_value(std::fs::remove_file(path).is_ok()) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/listing.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/listing.rs deleted file mode 100644 index b14c6695be..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/listing.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Purpose: -//! Implements directory listing helpers and indexed byte-array insertion. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` and file-reading helpers. -//! -//! Key details: -//! - Byte-array insertion is reused by `file()` and glob/listing builtins to -//! produce sequential PHP arrays. - -use super::super::super::super::*; -use super::super::*; - -/// Evaluates PHP `scandir($directory)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_scandir( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_scandir_result(directory, values) -} - -/// Lists one local directory into an indexed string array, or an empty array on failure. -pub(in crate::interpreter) fn eval_scandir_result( - directory: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(directory, values)?; - let Ok(entries) = std::fs::read_dir(path) else { - return values.array_new(0); - }; - let mut names = vec![".".to_string(), "..".to_string()]; - for entry in entries { - let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; - names.push(entry.file_name().to_string_lossy().into_owned()); - } - names.sort(); - let mut result = values.array_new(names.len())?; - for (index, name) in names.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; - } - Ok(result) -} - -/// Writes one byte-string value into an indexed runtime array at a zero-based position. -pub(in crate::interpreter) fn eval_array_set_indexed_bytes( - array: RuntimeCellHandle, - index: usize, - value: &[u8], - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.string_bytes_value(value)?; - values.array_set(array, key, value) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/mod.rs deleted file mode 100644 index fa7df70e8a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Purpose: -//! Groups miscellaneous filesystem operation eval builtins by operation family. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem` re-exports. -//! -//! Key details: -//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps` -//! while path coercion remains shared with sibling filesystem modules. - -mod disk; -mod glob; -mod links; -mod listing; -mod path_bool; -mod tempnam; -mod touch; -mod umask; - -pub(in crate::interpreter) use disk::*; -pub(in crate::interpreter) use glob::*; -pub(in crate::interpreter) use links::*; -pub(in crate::interpreter) use listing::*; -pub(in crate::interpreter) use path_bool::*; -pub(in crate::interpreter) use tempnam::*; -pub(in crate::interpreter) use touch::*; -pub(in crate::interpreter) use umask::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs deleted file mode 100644 index 83c0e29a94..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/path_bool.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Purpose: -//! Implements path operations that return PHP booleans, plus `chmod`. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` re-exports. -//! -//! Key details: -//! - Paths are coerced through the shared filesystem path helper before host -//! filesystem operations are attempted. - -use std::ffi::CString; - -use super::super::super::super::*; -use super::super::super::*; -use super::super::*; -use crate::stream_wrappers; - -/// Evaluates a one-path filesystem operation that returns a PHP boolean. -pub(in crate::interpreter) fn eval_builtin_unary_path_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [path] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let path = eval_expr(path, context, scope, values)?; - eval_unary_path_bool_result(name, path, context, values) -} - -/// Executes a one-path filesystem operation and returns whether it succeeded. -pub(in crate::interpreter) fn eval_unary_path_bool_result( - name: &str, - path: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(path, values)?; - if let Some(result) = eval_user_wrapper_single_path_op_result(name, &path, context, values)? { - return Ok(result); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - let ok = match name { - "chdir" => std::env::set_current_dir(path).is_ok(), - "mkdir" => std::fs::create_dir(path).is_ok(), - "rmdir" => std::fs::remove_dir(path).is_ok(), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Evaluates a two-path filesystem operation that returns a PHP boolean. -pub(in crate::interpreter) fn eval_builtin_binary_path_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [from, to] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let from = eval_expr(from, context, scope, values)?; - let to = eval_expr(to, context, scope, values)?; - eval_binary_path_bool_result(name, from, to, context, values) -} - -/// Executes a two-path filesystem operation and returns whether it succeeded. -pub(in crate::interpreter) fn eval_binary_path_bool_result( - name: &str, - from: RuntimeCellHandle, - to: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let from = eval_path_string(from, values)?; - let to = eval_path_string(to, values)?; - if name == "rename" { - if let Some(result) = eval_user_wrapper_rename_result(&from, &to, context, values)? { - return Ok(result); - } - } - let Some(from) = stream_wrappers::local_filesystem_path(&from) else { - return values.bool_value(false); - }; - let Some(to) = stream_wrappers::local_filesystem_path(&to) else { - return values.bool_value(false); - }; - let ok = match name { - "copy" => std::fs::copy(from, to).is_ok(), - "link" => std::fs::hard_link(from, to).is_ok(), - "rename" => std::fs::rename(from, to).is_ok(), - "symlink" => std::os::unix::fs::symlink(from, to).is_ok(), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Evaluates PHP `chmod($filename, $permissions)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_chmod( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, permissions] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let permissions = eval_expr(permissions, context, scope, values)?; - eval_chmod_result(filename, permissions, context, values) -} - -/// Changes one local file's mode and returns whether the operation succeeded. -pub(in crate::interpreter) fn eval_chmod_result( - filename: RuntimeCellHandle, - permissions: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let mode = eval_int_value(permissions, values)? as u32; - let metadata_value = values.int(i64::from(mode))?; - if let Some(result) = eval_user_wrapper_stream_metadata_result( - &path, - EVAL_STREAM_META_ACCESS, - metadata_value, - context, - values, - )? { - return Ok(result); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - let permissions = std::fs::Permissions::from_mode(mode); - values.bool_value(std::fs::set_permissions(path, permissions).is_ok()) -} - -/// Evaluates PHP ownership/group path mutation builtins over eval expressions. -pub(in crate::interpreter) fn eval_builtin_chown_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename, principal] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - let principal = eval_expr(principal, context, scope, values)?; - eval_chown_like_result(name, filename, principal, context, values) -} - -/// Changes one local path owner or group and returns whether the operation succeeded. -pub(in crate::interpreter) fn eval_chown_like_result( - name: &str, - filename: RuntimeCellHandle, - principal: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if matches!(name, "chown" | "chgrp") { - let (option, metadata_value) = - eval_chown_metadata_arg(name, principal, values)?; - if let Some(result) = - eval_user_wrapper_stream_metadata_result(&path, option, metadata_value, context, values)? - { - return Ok(result); - } - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - let Some(path) = eval_c_string(&path) else { - return values.bool_value(false); - }; - let Some((uid, gid)) = eval_chown_principal_ids(name, principal, values)? else { - return values.bool_value(false); - }; - let status = unsafe { - match name { - "chown" | "chgrp" => libc::chown(path.as_ptr(), uid, gid), - "lchown" | "lchgrp" => libc::lchown(path.as_ptr(), uid, gid), - _ => return Err(EvalStatus::RuntimeFatal), - } - }; - values.bool_value(status == 0) -} - -/// Builds the wrapper metadata option and value for `chown()` or `chgrp()`. -fn eval_chown_metadata_arg( - name: &str, - principal: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(i64, RuntimeCellHandle), EvalStatus> { - match (name, values.type_tag(principal)?) { - ("chown", EVAL_TAG_INT) => { - let principal = eval_int_value(principal, values)?; - Ok((EVAL_STREAM_META_OWNER, values.int(principal)?)) - } - ("chgrp", EVAL_TAG_INT) => { - let principal = eval_int_value(principal, values)?; - Ok((EVAL_STREAM_META_GROUP, values.int(principal)?)) - } - ("chown", EVAL_TAG_STRING) => Ok((EVAL_STREAM_META_OWNER_NAME, principal)), - ("chgrp", EVAL_TAG_STRING) => Ok((EVAL_STREAM_META_GROUP_NAME, principal)), - ("chown" | "chgrp", _) => Err(EvalStatus::RuntimeFatal), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Resolves one PHP owner/group argument into libc uid/gid slots. -fn eval_chown_principal_ids( - name: &str, - principal: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - match (name, values.type_tag(principal)?) { - ("chown" | "lchown", EVAL_TAG_INT) => { - Ok(Some(( - eval_int_value(principal, values)? as libc::uid_t, - !0 as libc::gid_t, - ))) - } - ("chgrp" | "lchgrp", EVAL_TAG_INT) => { - Ok(Some(( - !0 as libc::uid_t, - eval_int_value(principal, values)? as libc::gid_t, - ))) - } - ("chown" | "lchown", EVAL_TAG_STRING) => { - Ok(eval_owner_name_id(principal, values)?.map(|uid| (uid, !0 as libc::gid_t))) - } - ("chgrp" | "lchgrp", EVAL_TAG_STRING) => { - Ok(eval_group_name_id(principal, values)?.map(|gid| (!0 as libc::uid_t, gid))) - } - ("chown" | "chgrp" | "lchown" | "lchgrp", _) => Err(EvalStatus::RuntimeFatal), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Resolves a PHP user-name cell to a libc uid. -fn eval_owner_name_id( - principal: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let name = values.string_bytes(principal)?; - let Some(name) = eval_c_bytes(&name) else { - return Ok(None); - }; - let passwd = unsafe { libc::getpwnam(name.as_ptr()) }; - if passwd.is_null() { - Ok(None) - } else { - Ok(Some(unsafe { (*passwd).pw_uid })) - } -} - -/// Resolves a PHP group-name cell to a libc gid. -fn eval_group_name_id( - principal: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let name = values.string_bytes(principal)?; - let Some(name) = eval_c_bytes(&name) else { - return Ok(None); - }; - let group = unsafe { libc::getgrnam(name.as_ptr()) }; - if group.is_null() { - Ok(None) - } else { - Ok(Some(unsafe { (*group).gr_gid })) - } -} - -/// Converts a Rust path string into a C string, rejecting embedded NUL bytes. -fn eval_c_string(value: &str) -> Option { - CString::new(value).ok() -} - -/// Converts raw PHP bytes into a C string, rejecting embedded NUL bytes. -fn eval_c_bytes(value: &[u8]) -> Option { - CString::new(value).ok() -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/tempnam.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/tempnam.rs deleted file mode 100644 index 3372b2de28..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/tempnam.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Implements PHP `tempnam()` eval support. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` re-exports. -//! -//! Key details: -//! - Candidate names are deterministic within process/attempt data and created -//! with exclusive file creation. - -use super::super::super::super::*; -use super::super::*; - -/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_tempnam( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory, prefix] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - let prefix = eval_expr(prefix, context, scope, values)?; - eval_tempnam_result(directory, prefix, values) -} - -/// Creates a unique local temporary file and returns its path, or an empty string on failure. -pub(in crate::interpreter) fn eval_tempnam_result( - directory: RuntimeCellHandle, - prefix: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let directory = eval_path_string(directory, values)?; - let prefix = values.string_bytes(prefix)?; - let prefix = String::from_utf8_lossy(&prefix); - let nonce = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0); - for attempt in 0..1000_u32 { - let candidate = - std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); - match std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&candidate) - { - Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), - Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, - Err(_) => return values.string(""), - } - } - values.string("") -} - -/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. -pub(in crate::interpreter) fn eval_tempnam_filename( - prefix: &str, - nonce: u128, - attempt: u32, -) -> String { - format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs deleted file mode 100644 index ca3a1cfde4..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/touch.rs +++ /dev/null @@ -1,145 +0,0 @@ -//! Purpose: -//! Implements PHP `touch()` eval support and timestamp conversion helpers. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` re-exports. -//! -//! Key details: -//! - Existing files are preserved, missing files are created, and timestamp -//! arguments are resolved to `SystemTime` values. - -use super::super::super::super::*; -use super::super::super::*; -use super::super::*; -use crate::stream_wrappers; - -/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_touch( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [filename] => { - let filename = eval_expr(filename, context, scope, values)?; - eval_touch_result(filename, None, None, context, values) - } - [filename, mtime] => { - let filename = eval_expr(filename, context, scope, values)?; - let mtime = eval_expr(mtime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), None, context, values) - } - [filename, mtime, atime] => { - let filename = eval_expr(filename, context, scope, values)?; - let mtime = eval_expr(mtime, context, scope, values)?; - let atime = eval_expr(atime, context, scope, values)?; - eval_touch_result(filename, Some(mtime), Some(atime), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Creates or stamps one local file and returns whether the operation succeeded. -pub(in crate::interpreter) fn eval_touch_result( - filename: RuntimeCellHandle, - mtime: Option, - atime: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - let (mtime, atime) = eval_touch_times(mtime, atime, values)?; - let metadata_value = eval_touch_metadata_value(mtime, atime, values)?; - if let Some(result) = eval_user_wrapper_stream_metadata_result( - &path, - EVAL_STREAM_META_TOUCH, - metadata_value, - context, - values, - )? { - return Ok(result); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - let file = match std::fs::OpenOptions::new() - .write(true) - .create(true) - .open(path) - { - Ok(file) => file, - Err(_) => return values.bool_value(false), - }; - let times = std::fs::FileTimes::new() - .set_modified(mtime) - .set_accessed(atime); - values.bool_value(file.set_times(times).is_ok()) -} - -/// Builds the `[mtime, atime]` array passed to wrapper `stream_metadata()`. -fn eval_touch_metadata_value( - mtime: std::time::SystemTime, - atime: std::time::SystemTime, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(2)?; - let key = values.int(0)?; - let value = values.int(eval_system_time_to_unix(mtime).ok_or(EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, key, value)?; - let key = values.int(1)?; - let value = values.int(eval_system_time_to_unix(atime).ok_or(EvalStatus::RuntimeFatal)?)?; - values.array_set(result, key, value) -} - -/// Resolves PHP touch timestamp defaults into concrete system times. -pub(in crate::interpreter) fn eval_touch_times( - mtime: Option, - atime: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { - let now = std::time::SystemTime::now(); - let Some(mtime) = mtime else { - return Ok((now, now)); - }; - if values.is_null(mtime)? { - if let Some(atime) = atime { - if !values.is_null(atime)? { - return Err(EvalStatus::RuntimeFatal); - } - } - return Ok((now, now)); - } - let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) - .ok_or(EvalStatus::RuntimeFatal)?; - let Some(atime) = atime else { - return Ok((mtime, mtime)); - }; - if values.is_null(atime)? { - return Ok((mtime, mtime)); - } - let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) - .ok_or(EvalStatus::RuntimeFatal)?; - Ok((mtime, atime)) -} - -/// Converts a Unix timestamp in seconds into a `SystemTime`. -pub(in crate::interpreter) fn eval_system_time_from_unix( - seconds: i64, -) -> Option { - if seconds >= 0 { - std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) - } else { - std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) - } -} - -/// Converts a `SystemTime` back to whole Unix seconds for wrapper metadata. -fn eval_system_time_to_unix(time: std::time::SystemTime) -> Option { - match time.duration_since(std::time::UNIX_EPOCH) { - Ok(duration) => i64::try_from(duration.as_secs()).ok(), - Err(error) => i64::try_from(error.duration().as_secs()) - .ok() - .map(|seconds| -seconds), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/umask.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/umask.rs deleted file mode 100644 index 1cc6fbfa73..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ops/umask.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! Purpose: -//! Implements PHP `umask()` eval support. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` re-exports. -//! -//! Key details: -//! - The previous process umask is returned after optionally applying a new mask. - -use super::super::super::super::*; -use super::super::super::*; - -/// Evaluates PHP `umask($mask = null)` over an optional eval expression. -pub(in crate::interpreter) fn eval_builtin_umask( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_umask_result(None, values), - [mask] => { - let mask = eval_expr(mask, context, scope, values)?; - eval_umask_result(Some(mask), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Applies PHP `umask()` semantics and returns the previous mask. -pub(in crate::interpreter) fn eval_umask_result( - mask: Option, - values: &mut impl RuntimeValueOps, -) -> Result { - let previous = match mask { - Some(mask) => { - let mask = eval_int_value(mask, values)? as u32; - unsafe { umask(mask) } - } - None => unsafe { - let current = umask(0); - umask(current); - current - }, - }; - values.int(i64::from(previous)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs index 03a3a1bdee..0a16778b33 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs @@ -19,34 +19,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "chdir" | "mkdir" | "rmdir" => match evaluated_args { - [path] => eval_unary_path_bool_result(name, *path, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "chmod" => match evaluated_args { - [filename, permissions] => eval_chmod_result(*filename, *permissions, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "chown" | "chgrp" | "lchown" | "lchgrp" => match evaluated_args { - [filename, principal] => { - eval_chown_like_result(name, *filename, *principal, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "clearstatcache" => { - if evaluated_args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - values.null()? - } - "copy" | "link" | "rename" | "symlink" => match evaluated_args { - [from, to] => eval_binary_path_bool_result(name, *from, *to, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "disk_free_space" | "disk_total_space" => match evaluated_args { - [directory] => eval_disk_space_result(name, *directory, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" | "is_writable" | "is_writeable" => match evaluated_args { [filename] => eval_file_probe_result(name, *filename, context, values)?, @@ -76,14 +48,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [] => eval_getcwd_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "glob" => match evaluated_args { - [pattern] => eval_glob_result(*pattern, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "linkinfo" => match evaluated_args { - [path] => eval_linkinfo_result(*path, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "opendir" => match evaluated_args { [directory] => eval_opendir_result(*directory, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), @@ -100,10 +64,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [dir_handle] => eval_unary_directory_result(name, *dir_handle, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "readlink" => match evaluated_args { - [path] => eval_readlink_result(*path, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "realpath_cache_get" => match evaluated_args { [] => eval_realpath_cache_get_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), @@ -112,10 +72,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [] => eval_realpath_cache_size_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "scandir" => match evaluated_args { - [directory] => eval_scandir_result(*directory, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "stat" | "lstat" => match evaluated_args { [filename] => eval_stat_array_result(name, *filename, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), @@ -124,33 +80,10 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [] => eval_sys_get_temp_dir_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "tempnam" => match evaluated_args { - [directory, prefix] => eval_tempnam_result(*directory, *prefix, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "tmpfile" => match evaluated_args { [] => eval_tmpfile_result(context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "touch" => match evaluated_args { - [filename] => eval_touch_result(*filename, None, None, context, values)?, - [filename, mtime] => { - eval_touch_result(*filename, Some(*mtime), None, context, values)? - } - [filename, mtime, atime] => { - eval_touch_result(*filename, Some(*mtime), Some(*atime), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "umask" => match evaluated_args { - [] => eval_umask_result(None, values)?, - [mask] => eval_umask_result(Some(*mask), values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "unlink" => match evaluated_args { - [filename] => eval_unlink_result(*filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs index 55ce2ed3f1..cef215ce07 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; /// Dispatches direct eval calls for the `readlink` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_readlink_declared_call( @@ -24,14 +25,46 @@ pub(in crate::interpreter) fn eval_readlink_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("readlink", args, context, scope, values) + eval_builtin_readlink(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `readlink` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_readlink_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [path] => eval_readlink_result(*path, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `readlink($path)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_readlink( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [path] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let path = eval_expr(path, context, scope, values)?; + eval_readlink_result(path, values) +} + +/// Reads one symbolic-link target string, or returns PHP false on failure. +pub(in crate::interpreter) fn eval_readlink_result( + path: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("readlink", evaluated_args, context, values) + let path = eval_path_string(path, values)?; + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + match std::fs::read_link(path) { + Ok(target) => values.string(target.to_string_lossy().as_ref()), + Err(_) => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs index f91e280dd1..52119bd758 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_rename_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("rename", args, context, scope, values) + super::copy::eval_builtin_binary_path_bool("rename", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `rename` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_rename_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("rename", evaluated_args, context, values) + match evaluated_args { + [from, to] => super::copy::eval_binary_path_bool_result("rename", *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs index 2ff9dfffb1..66e68aff59 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_rmdir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("rmdir", args, context, scope, values) + super::chdir::eval_builtin_unary_path_bool("rmdir", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `rmdir` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_rmdir_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("rmdir", evaluated_args, context, values) + match evaluated_args { + [path] => super::chdir::eval_unary_path_bool_result("rmdir", *path, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs index cdbc1e4c30..59b336036c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `scandir` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_scandir_declared_call( @@ -24,14 +25,65 @@ pub(in crate::interpreter) fn eval_scandir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("scandir", args, context, scope, values) + eval_builtin_scandir(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `scandir` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_scandir_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [directory] => eval_scandir_result(*directory, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `scandir($directory)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_scandir( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_scandir_result(directory, values) +} + +/// Lists one local directory into an indexed string array, or an empty array on failure. +pub(in crate::interpreter) fn eval_scandir_result( + directory: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(directory, values)?; + let Ok(entries) = std::fs::read_dir(path) else { + return values.array_new(0); + }; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.map_err(|_| EvalStatus::RuntimeFatal)?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + result = eval_array_set_indexed_bytes(result, index, name.as_bytes(), values)?; + } + Ok(result) +} + +/// Writes one byte-string value into an indexed runtime array at a zero-based position. +pub(in crate::interpreter) fn eval_array_set_indexed_bytes( + array: RuntimeCellHandle, + index: usize, + value: &[u8], values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("scandir", evaluated_args, context, values) + let key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.string_bytes_value(value)?; + values.array_set(array, key, value) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs index f891e6dcba..7182103536 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs @@ -249,7 +249,7 @@ fn eval_csv_fields_array( ) -> Result { let mut result = values.array_new(fields.len())?; for (index, field) in fields.iter().enumerate() { - result = eval_array_set_indexed_bytes(result, index, field, values)?; + result = super::super::scandir::eval_array_set_indexed_bytes(result, index, field, values)?; } Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs index a645278505..5783332496 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_symlink_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("symlink", args, context, scope, values) + super::copy::eval_builtin_binary_path_bool("symlink", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `symlink` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_symlink_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("symlink", evaluated_args, context, values) + match evaluated_args { + [from, to] => super::copy::eval_binary_path_bool_result("symlink", *from, *to, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs index c4a7afe26e..241a0369a7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `tempnam` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_tempnam_declared_call( @@ -24,14 +25,70 @@ pub(in crate::interpreter) fn eval_tempnam_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("tempnam", args, context, scope, values) + eval_builtin_tempnam(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `tempnam` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_tempnam_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [directory, prefix] => eval_tempnam_result(*directory, *prefix, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `tempnam($directory, $prefix)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_tempnam( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("tempnam", evaluated_args, context, values) + let [directory, prefix] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + let prefix = eval_expr(prefix, context, scope, values)?; + eval_tempnam_result(directory, prefix, values) +} + +/// Creates a unique local temporary file and returns its path, or an empty string on failure. +pub(in crate::interpreter) fn eval_tempnam_result( + directory: RuntimeCellHandle, + prefix: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + let prefix = values.string_bytes(prefix)?; + let prefix = String::from_utf8_lossy(&prefix); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0); + for attempt in 0..1000_u32 { + let candidate = + std::path::Path::new(&directory).join(eval_tempnam_filename(&prefix, nonce, attempt)); + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&candidate) + { + Ok(_) => return values.string(candidate.to_string_lossy().as_ref()), + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(_) => return values.string(""), + } + } + values.string("") +} + +/// Builds one deterministic tempnam candidate basename from prefix, process, and attempt data. +pub(in crate::interpreter) fn eval_tempnam_filename( + prefix: &str, + nonce: u128, + attempt: u32, +) -> String { + format!("{}{}_{:x}_{attempt}", prefix, std::process::id(), nonce) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs index 5f09127527..8bc09e7b09 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs @@ -22,6 +22,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `touch` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_touch_declared_call( @@ -30,7 +32,7 @@ pub(in crate::interpreter) fn eval_touch_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("touch", args, context, scope, values) + eval_builtin_touch(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `touch` filesystem builtin through the area dispatcher. @@ -39,5 +41,143 @@ pub(in crate::interpreter) fn eval_touch_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("touch", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_touch_result(*filename, None, None, context, values), + [filename, mtime] => eval_touch_result(*filename, Some(*mtime), None, context, values), + [filename, mtime, atime] => { + eval_touch_result(*filename, Some(*mtime), Some(*atime), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `touch($filename, $mtime = null, $atime = null)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_touch( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [filename] => { + let filename = eval_expr(filename, context, scope, values)?; + eval_touch_result(filename, None, None, context, values) + } + [filename, mtime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), None, context, values) + } + [filename, mtime, atime] => { + let filename = eval_expr(filename, context, scope, values)?; + let mtime = eval_expr(mtime, context, scope, values)?; + let atime = eval_expr(atime, context, scope, values)?; + eval_touch_result(filename, Some(mtime), Some(atime), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates or stamps one local file and returns whether the operation succeeded. +pub(in crate::interpreter) fn eval_touch_result( + filename: RuntimeCellHandle, + mtime: Option, + atime: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + let (mtime, atime) = eval_touch_times(mtime, atime, values)?; + let metadata_value = eval_touch_metadata_value(mtime, atime, values)?; + if let Some(result) = eval_user_wrapper_stream_metadata_result( + &path, + EVAL_STREAM_META_TOUCH, + metadata_value, + context, + values, + )? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let file = match std::fs::OpenOptions::new() + .write(true) + .create(true) + .open(path) + { + Ok(file) => file, + Err(_) => return values.bool_value(false), + }; + let times = std::fs::FileTimes::new() + .set_modified(mtime) + .set_accessed(atime); + values.bool_value(file.set_times(times).is_ok()) +} + +/// Builds the `[mtime, atime]` array passed to wrapper `stream_metadata()`. +fn eval_touch_metadata_value( + mtime: std::time::SystemTime, + atime: std::time::SystemTime, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(2)?; + let key = values.int(0)?; + let value = values.int(eval_system_time_to_unix(mtime).ok_or(EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, key, value)?; + let key = values.int(1)?; + let value = values.int(eval_system_time_to_unix(atime).ok_or(EvalStatus::RuntimeFatal)?)?; + values.array_set(result, key, value) +} + +/// Resolves PHP touch timestamp defaults into concrete system times. +pub(in crate::interpreter) fn eval_touch_times( + mtime: Option, + atime: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(std::time::SystemTime, std::time::SystemTime), EvalStatus> { + let now = std::time::SystemTime::now(); + let Some(mtime) = mtime else { + return Ok((now, now)); + }; + if values.is_null(mtime)? { + if let Some(atime) = atime { + if !values.is_null(atime)? { + return Err(EvalStatus::RuntimeFatal); + } + } + return Ok((now, now)); + } + let mtime = eval_system_time_from_unix(eval_int_value(mtime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + let Some(atime) = atime else { + return Ok((mtime, mtime)); + }; + if values.is_null(atime)? { + return Ok((mtime, mtime)); + } + let atime = eval_system_time_from_unix(eval_int_value(atime, values)?) + .ok_or(EvalStatus::RuntimeFatal)?; + Ok((mtime, atime)) +} + +/// Converts a Unix timestamp in seconds into a `SystemTime`. +pub(in crate::interpreter) fn eval_system_time_from_unix( + seconds: i64, +) -> Option { + if seconds >= 0 { + std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(seconds as u64)) + } else { + std::time::UNIX_EPOCH.checked_sub(std::time::Duration::from_secs(seconds.unsigned_abs())) + } +} + +/// Converts a `SystemTime` back to whole Unix seconds for wrapper metadata. +fn eval_system_time_to_unix(time: std::time::SystemTime) -> Option { + match time.duration_since(std::time::UNIX_EPOCH) { + Ok(duration) => i64::try_from(duration.as_secs()).ok(), + Err(error) => i64::try_from(error.duration().as_secs()) + .ok() + .map(|seconds| -seconds), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs index 710c99e7e8..3b2f1c97c0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs @@ -26,14 +26,54 @@ pub(in crate::interpreter) fn eval_umask_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("umask", args, context, scope, values) + eval_builtin_umask(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `umask` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_umask_declared_values_result( evaluated_args: &[RuntimeCellHandle], + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => eval_umask_result(None, values), + [mask] => eval_umask_result(Some(*mask), values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `umask($mask = null)` over an optional eval expression. +pub(in crate::interpreter) fn eval_builtin_umask( + args: &[EvalExpr], context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_umask_result(None, values), + [mask] => { + let mask = eval_expr(mask, context, scope, values)?; + eval_umask_result(Some(mask), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Applies PHP `umask()` semantics and returns the previous mask. +pub(in crate::interpreter) fn eval_umask_result( + mask: Option, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("umask", evaluated_args, context, values) + let previous = match mask { + Some(mask) => { + let mask = eval_int_value(mask, values)? as u32; + unsafe { umask(mask) } + } + None => unsafe { + let current = umask(0); + umask(current); + current + }, + }; + values.int(i64::from(previous)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs index 840141f2e6..c0856464de 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `unlink` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_unlink_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_unlink_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("unlink", args, context, scope, values) + eval_builtin_unlink(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `unlink` filesystem builtin through the area dispatcher. @@ -33,5 +35,41 @@ pub(in crate::interpreter) fn eval_unlink_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("unlink", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_unlink_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `unlink($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_unlink( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_unlink_result(filename, context, values) +} + +/// Deletes one path and returns whether it succeeded. +pub(in crate::interpreter) fn eval_unlink_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if stream_wrappers::is_phar_stream(&path) { + return values.bool_value(elephc_phar::delete_url_bytes(path.as_bytes()).is_some()); + } + if let Some(result) = eval_user_wrapper_unlink_result(&path, context, values)? { + return Ok(result); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + values.bool_value(std::fs::remove_file(path).is_ok()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs index 7034869bdf..f6e06e80e4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_metadata.rs @@ -2,8 +2,8 @@ //! Dispatches path-based metadata changes to eval userspace stream wrappers. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` for `touch()`, `chmod()`, -//! `chown()`, and `chgrp()` when the path scheme is a registered wrapper. +//! - `crate::interpreter::builtins::filesystem::touch`, `chmod`, and `chown` +//! builtin owners when the path scheme is a registered wrapper. //! //! Key details: //! - Mirrors the AOT stream-wrapper metadata options used by filesystem diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs index 1d56282d41..fc6b9942bf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_path_ops.rs @@ -2,8 +2,8 @@ //! Dispatches path mutation builtins to eval userspace stream wrappers. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::ops` for `unlink()`, `rename()`, -//! `mkdir()`, and `rmdir()` when the source path scheme is registered. +//! - `crate::interpreter::builtins::filesystem::unlink`, `copy`, and `chdir` +//! builtin owners when the source path scheme is registered. //! //! Key details: //! - Path methods use a throwaway wrapper instance, matching the generated From 5b66ceac6b6e083f195b39f54af96bb83394817d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:29:34 +0200 Subject: [PATCH 1116/1208] refactor(magician): move directory builtin implementations home --- .../builtins/filesystem/closedir.rs | 71 +++++++++++- .../builtins/filesystem/direct_dispatch.rs | 4 - .../builtins/filesystem/directories.rs | 106 ------------------ .../interpreter/builtins/filesystem/mod.rs | 2 - .../builtins/filesystem/opendir.rs | 38 ++++++- .../filesystem/path_values_dispatch.rs | 8 -- .../builtins/filesystem/readdir.rs | 7 +- .../builtins/filesystem/rewinddir.rs | 7 +- .../filesystem/user_wrapper_directories.rs | 4 +- 9 files changed, 117 insertions(+), 130 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs index 0f0c6b55fb..fceb6f6cc4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `closedir` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_closedir_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_closedir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("closedir", args, context, scope, values) + eval_builtin_unary_directory("closedir", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `closedir` filesystem builtin through the area dispatcher. @@ -33,5 +34,71 @@ pub(in crate::interpreter) fn eval_closedir_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("closedir", evaluated_args, context, values) + match evaluated_args { + [dir_handle] => eval_unary_directory_result("closedir", *dir_handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP directory handle builtins over one eval expression. +pub(in crate::interpreter) fn eval_builtin_unary_directory( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [dir_handle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let dir_handle = eval_expr(dir_handle, context, scope, values)?; + eval_unary_directory_result(name, dir_handle, context, values) +} + +/// Evaluates a materialized directory handle builtin argument. +pub(in crate::interpreter) fn eval_unary_directory_result( + name: &str, + dir_handle: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_directory_resource_id(dir_handle, values)?; + match name { + "closedir" => { + if let Some(result) = eval_user_wrapper_closedir_result(id, context, values)? { + return Ok(result); + } + context.stream_resources_mut().close_directory(id); + values.null() + } + "readdir" => { + if let Some(result) = eval_user_wrapper_readdir_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().read_directory(id) { + Some(name) => values.string(&name), + None => values.bool_value(false), + } + } + "rewinddir" => { + if let Some(result) = eval_user_wrapper_rewinddir_result(id, context, values)? { + return Ok(result); + } + context.stream_resources_mut().rewind_directory(id); + values.null() + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based directory id. +fn eval_directory_resource_id( + dir_handle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(dir_handle)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(dir_handle, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index f8db5dbfa9..4a129a16ed 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -183,12 +183,8 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), "fwrite" => eval_builtin_fwrite(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), - "opendir" => eval_builtin_opendir(args, context, scope, values), "pclose" => eval_builtin_pclose(args, context, scope, values), "popen" => eval_builtin_popen(args, context, scope, values), - "closedir" | "readdir" | "rewinddir" => { - eval_builtin_unary_directory(name, args, context, scope, values) - } "readline" => eval_builtin_readline(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs deleted file mode 100644 index d880b6fa06..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/directories.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Purpose: -//! Implements eval-local directory resource builtins backed by host directory listings. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - Directory resources share the eval resource id namespace with file streams. -//! - Entry lists are snapshotted when `opendir()` runs and can be rewound. - -use super::super::super::*; -use super::*; - -/// Evaluates PHP `opendir($directory)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_opendir( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [directory] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let directory = eval_expr(directory, context, scope, values)?; - eval_opendir_result(directory, context, values) -} - -/// Opens a local directory and returns a resource cell or PHP false. -pub(in crate::interpreter) fn eval_opendir_result( - directory: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let directory = eval_path_string(directory, values)?; - if let Some(result) = eval_user_wrapper_opendir_result(&directory, context, values)? { - return Ok(result); - } - match context.stream_resources_mut().open_directory(&directory) { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Evaluates PHP directory handle builtins over one eval expression. -pub(in crate::interpreter) fn eval_builtin_unary_directory( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [dir_handle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let dir_handle = eval_expr(dir_handle, context, scope, values)?; - eval_unary_directory_result(name, dir_handle, context, values) -} - -/// Evaluates a materialized directory handle builtin argument. -pub(in crate::interpreter) fn eval_unary_directory_result( - name: &str, - dir_handle: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_directory_resource_id(dir_handle, values)?; - match name { - "closedir" => { - if let Some(result) = eval_user_wrapper_closedir_result(id, context, values)? { - return Ok(result); - } - context.stream_resources_mut().close_directory(id); - values.null() - } - "readdir" => { - if let Some(result) = eval_user_wrapper_readdir_result(id, context, values)? { - return Ok(result); - } - match context.stream_resources_mut().read_directory(id) { - Some(name) => values.string(&name), - None => values.bool_value(false), - } - } - "rewinddir" => { - if let Some(result) = eval_user_wrapper_rewinddir_result(id, context, values)? { - return Ok(result); - } - context.stream_resources_mut().rewind_directory(id); - values.null() - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts a runtime resource cell into eval's zero-based directory id. -fn eval_directory_resource_id( - dir_handle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(dir_handle)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(dir_handle, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index b71d0bd8e2..4ac3429553 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -18,7 +18,6 @@ mod clearstatcache; mod closedir; mod copy; mod direct_dispatch; -mod directories; mod dirname; mod disk_free_space; mod disk_total_space; @@ -159,7 +158,6 @@ mod user_wrapper_streams; mod values_dispatch; mod vfprintf; -pub(in crate::interpreter) use directories::*; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use path::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs index f5a14a7eda..bf9cc52dbe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `opendir` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_opendir_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_opendir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("opendir", args, context, scope, values) + eval_builtin_opendir(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `opendir` filesystem builtin through the area dispatcher. @@ -33,5 +34,38 @@ pub(in crate::interpreter) fn eval_opendir_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("opendir", evaluated_args, context, values) + match evaluated_args { + [directory] => eval_opendir_result(*directory, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `opendir($directory)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_opendir( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [directory] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let directory = eval_expr(directory, context, scope, values)?; + eval_opendir_result(directory, context, values) +} + +/// Opens a local directory and returns a resource cell or PHP false. +pub(in crate::interpreter) fn eval_opendir_result( + directory: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let directory = eval_path_string(directory, values)?; + if let Some(result) = eval_user_wrapper_opendir_result(&directory, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().open_directory(&directory) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs index 0a16778b33..35e7724e90 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs @@ -48,10 +48,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [] => eval_getcwd_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "opendir" => match evaluated_args { - [directory] => eval_opendir_result(*directory, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "pclose" => match evaluated_args { [handle] => eval_pclose_result(*handle, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), @@ -60,10 +56,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [command, mode] => eval_popen_result(*command, *mode, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "closedir" | "readdir" | "rewinddir" => match evaluated_args { - [dir_handle] => eval_unary_directory_result(name, *dir_handle, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "realpath_cache_get" => match evaluated_args { [] => eval_realpath_cache_get_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs index 13fc89d821..bb35307e51 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_readdir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("readdir", args, context, scope, values) + super::closedir::eval_builtin_unary_directory("readdir", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `readdir` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_readdir_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("readdir", evaluated_args, context, values) + match evaluated_args { + [dir_handle] => super::closedir::eval_unary_directory_result("readdir", *dir_handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs index 5f41a42c3c..a8b5ad7f99 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_rewinddir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("rewinddir", args, context, scope, values) + super::closedir::eval_builtin_unary_directory("rewinddir", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `rewinddir` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_rewinddir_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("rewinddir", evaluated_args, context, values) + match evaluated_args { + [dir_handle] => super::closedir::eval_unary_directory_result("rewinddir", *dir_handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs index bd4e2f05d6..1987339909 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_directories.rs @@ -2,8 +2,8 @@ //! Dispatches eval directory builtins to userspace stream-wrapper directory methods. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::directories` for `opendir()`, -//! `readdir()`, `rewinddir()`, and `closedir()`. +//! - `crate::interpreter::builtins::filesystem::opendir` and `closedir` +//! builtin owners for directory-resource operations. //! //! Key details: //! - `dir_opendir()` owns the wrapper object for the directory resource lifetime; From 5395476b6345cc4130407ce1806abe1b62e13892 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:30:31 +0200 Subject: [PATCH 1117/1208] refactor(magician): move process pipe builtin implementations home --- .../builtins/filesystem/direct_dispatch.rs | 2 - .../interpreter/builtins/filesystem/mod.rs | 2 - .../filesystem/path_values_dispatch.rs | 8 -- .../interpreter/builtins/filesystem/pclose.rs | 7 +- .../interpreter/builtins/filesystem/popen.rs | 92 ++++++++++++++++- .../builtins/filesystem/process_pipes.rs | 99 ------------------- 6 files changed, 95 insertions(+), 115 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/process_pipes.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index 4a129a16ed..b23d9c1279 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -183,8 +183,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), "fwrite" => eval_builtin_fwrite(args, context, scope, values), "getcwd" => eval_builtin_getcwd(args, values), - "pclose" => eval_builtin_pclose(args, context, scope, values), - "popen" => eval_builtin_popen(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 4ac3429553..53bfecbb89 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -79,7 +79,6 @@ mod pathinfo; mod pclose; mod pfsockopen; mod popen; -mod process_pipes; mod readdir; mod readfile; mod readline; @@ -161,7 +160,6 @@ mod vfprintf; pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use path::*; -pub(in crate::interpreter) use process_pipes::*; pub(in crate::interpreter) use readline::*; pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_extensions::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs index 35e7724e90..81203ee15b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs @@ -48,14 +48,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [] => eval_getcwd_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "pclose" => match evaluated_args { - [handle] => eval_pclose_result(*handle, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "popen" => match evaluated_args { - [command, mode] => eval_popen_result(*command, *mode, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "realpath_cache_get" => match evaluated_args { [] => eval_realpath_cache_get_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs index 0b2639378a..d698f77c8e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_pclose_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("pclose", args, context, scope, values) + super::popen::eval_builtin_pclose(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `pclose` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_pclose_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("pclose", evaluated_args, context, values) + match evaluated_args { + [handle] => super::popen::eval_pclose_result(*handle, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs index 757d1bb242..9a542d9d46 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `popen` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_popen_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_popen_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("popen", args, context, scope, values) + eval_builtin_popen(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `popen` filesystem builtin through the area dispatcher. @@ -33,5 +34,92 @@ pub(in crate::interpreter) fn eval_popen_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("popen", evaluated_args, context, values) + match evaluated_args { + [command, mode] => eval_popen_result(*command, *mode, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `popen(command, mode)`. +pub(in crate::interpreter) fn eval_builtin_popen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [command, mode] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let command = eval_expr(command, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_popen_result(command, mode, context, values) +} + +/// Opens a shell process pipe and returns a stream resource or false. +pub(in crate::interpreter) fn eval_popen_result( + command: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let command = eval_path_string(command, values)?; + let mode = eval_process_pipe_mode(mode, values)?; + match context + .stream_resources_mut() + .open_process_pipe(&command, &mode) + { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} +/// Evaluates `pclose(handle)`. +pub(in crate::interpreter) fn eval_builtin_pclose( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [handle] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let handle = eval_expr(handle, context, scope, values)?; + eval_pclose_result(handle, context, values) +} + +/// Closes a process pipe and returns its exit code, or false for invalid handles. +pub(in crate::interpreter) fn eval_pclose_result( + handle: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_process_pipe_resource_id(handle, values)?; + match context.stream_resources_mut().pclose(id) { + Some(status) => values.int(status), + None => values.bool_value(false), + } +} + +/// Reads a `popen()` mode string, accepting read or write pipe modes. +fn eval_process_pipe_mode( + mode: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = values.string_bytes(mode)?; + let mode = String::from_utf8(mode).map_err(|_| EvalStatus::RuntimeFatal)?; + match mode.chars().next() { + Some('r' | 'w') => Ok(mode), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Converts a runtime resource cell into eval's zero-based process-pipe id. +fn eval_process_pipe_resource_id( + handle: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(handle)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(handle, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/process_pipes.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/process_pipes.rs deleted file mode 100644 index b4a925128b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/process_pipes.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! Purpose: -//! Implements eval process pipe builtins `popen()` and `pclose()`. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - Process pipes are stored as normal eval stream resources plus a child -//! process table entry so existing fread/fwrite paths work unchanged. -//! - `pclose()` removes the stream first, then waits for the child exit status. - -use super::super::super::*; -use super::*; - -/// Evaluates `popen(command, mode)`. -pub(in crate::interpreter) fn eval_builtin_popen( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [command, mode] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let command = eval_expr(command, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_popen_result(command, mode, context, values) -} - -/// Opens a shell process pipe and returns a stream resource or false. -pub(in crate::interpreter) fn eval_popen_result( - command: RuntimeCellHandle, - mode: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let command = eval_path_string(command, values)?; - let mode = eval_process_pipe_mode(mode, values)?; - match context - .stream_resources_mut() - .open_process_pipe(&command, &mode) - { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Evaluates `pclose(handle)`. -pub(in crate::interpreter) fn eval_builtin_pclose( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [handle] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let handle = eval_expr(handle, context, scope, values)?; - eval_pclose_result(handle, context, values) -} - -/// Closes a process pipe and returns its exit code, or false for invalid handles. -pub(in crate::interpreter) fn eval_pclose_result( - handle: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_process_pipe_resource_id(handle, values)?; - match context.stream_resources_mut().pclose(id) { - Some(status) => values.int(status), - None => values.bool_value(false), - } -} - -/// Reads a `popen()` mode string, accepting read or write pipe modes. -fn eval_process_pipe_mode( - mode: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = values.string_bytes(mode)?; - let mode = String::from_utf8(mode).map_err(|_| EvalStatus::RuntimeFatal)?; - match mode.chars().next() { - Some('r' | 'w') => Ok(mode), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts a runtime resource cell into eval's zero-based process-pipe id. -fn eval_process_pipe_resource_id( - handle: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(handle)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(handle, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} From 05a24337edbf706bed1d8111b5763877449a5fac Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:33:13 +0200 Subject: [PATCH 1118/1208] refactor(magician): move file metadata builtin implementations home --- .../builtins/filesystem/direct_dispatch.rs | 10 - .../builtins/filesystem/file_exists.rs | 59 +++- .../builtins/filesystem/file_io.rs | 328 ------------------ .../builtins/filesystem/fileatime.rs | 7 +- .../builtins/filesystem/filectime.rs | 7 +- .../builtins/filesystem/filegroup.rs | 7 +- .../builtins/filesystem/fileinode.rs | 7 +- .../builtins/filesystem/filemtime.rs | 7 +- .../builtins/filesystem/fileowner.rs | 7 +- .../builtins/filesystem/fileperms.rs | 7 +- .../builtins/filesystem/filesize.rs | 46 ++- .../builtins/filesystem/filetype.rs | 70 +++- .../interpreter/builtins/filesystem/getcwd.rs | 32 +- .../interpreter/builtins/filesystem/is_dir.rs | 7 +- .../builtins/filesystem/is_executable.rs | 7 +- .../builtins/filesystem/is_file.rs | 7 +- .../builtins/filesystem/is_link.rs | 7 +- .../builtins/filesystem/is_readable.rs | 7 +- .../builtins/filesystem/is_writable.rs | 7 +- .../builtins/filesystem/is_writeable.rs | 7 +- .../interpreter/builtins/filesystem/lstat.rs | 7 +- .../interpreter/builtins/filesystem/mod.rs | 2 - .../filesystem/path_values_dispatch.rs | 26 -- .../interpreter/builtins/filesystem/stat.rs | 157 ++++++++- .../builtins/filesystem/streams.rs | 2 +- .../filesystem/user_wrapper_file_io.rs | 4 +- .../builtins/filesystem/user_wrapper_stat.rs | 4 +- 27 files changed, 431 insertions(+), 414 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index b23d9c1279..02982a4b95 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -161,14 +161,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( match name { "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => { - eval_builtin_file_probe(name, args, context, scope, values) - } - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => eval_builtin_file_stat_scalar(name, args, context, scope, values), - "filesize" => eval_builtin_filesize(args, context, scope, values), - "filetype" => eval_builtin_filetype(args, context, scope, values), "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { eval_builtin_unary_stream(name, args, context, scope, values) @@ -182,11 +174,9 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "fseek" => eval_builtin_fseek(args, context, scope, values), "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), "fwrite" => eval_builtin_fwrite(args, context, scope, values), - "getcwd" => eval_builtin_getcwd(args, values), "readline" => eval_builtin_readline(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "stat" | "lstat" => eval_builtin_stat_array(name, args, context, scope, values), "stream_bucket_append" | "stream_bucket_prepend" => { eval_builtin_stream_bucket_push(name, args, context, scope, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs index 00d329a39e..3307c5d260 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `file_exists` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_file_exists_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_file_exists_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("file_exists", args, context, scope, values) + eval_builtin_file_probe("file_exists", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `file_exists` filesystem builtin through the area dispatcher. @@ -33,5 +35,58 @@ pub(in crate::interpreter) fn eval_file_exists_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("file_exists", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_file_probe_result("file_exists", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates one PHP filesystem predicate over an eval expression. +pub(in crate::interpreter) fn eval_builtin_file_probe( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_probe_result(name, filename, context, values) +} + +/// Computes one local filesystem predicate and returns a PHP boolean. +pub(in crate::interpreter) fn eval_file_probe_result( + name: &str, + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return eval_user_wrapper_file_probe_from_stat(name, stat, values); + } + if stream_wrappers::is_phar_stream(&path) { + let exists = elephc_phar::extract_url_bytes(path.as_bytes()).is_some(); + let supported = matches!(name, "file_exists" | "is_file" | "is_readable"); + return values.bool_value(supported && exists); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let path = std::path::Path::new(&path); + let result = match name { + "file_exists" => path.exists(), + "is_dir" => path.is_dir(), + "is_executable" => eval_path_is_executable(path), + "is_file" => path.is_file(), + "is_link" => std::fs::symlink_metadata(path) + .map(|metadata| metadata.file_type().is_symlink()) + .unwrap_or(false), + "is_readable" => eval_path_is_readable(path), + "is_writable" | "is_writeable" => eval_path_is_writable(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs deleted file mode 100644 index a57afdc2bd..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/file_io.rs +++ /dev/null @@ -1,328 +0,0 @@ -//! Purpose: -//! getcwd, file reads/writes, filesize, filetype, stat, and disk-space builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem` re-exports. -//! -//! Key details: -//! - Helpers return PHP-compatible false/null/string/int cells via `RuntimeValueOps`. - -use super::super::super::*; -use super::*; -use crate::stream_wrappers; - -/// Evaluates PHP `getcwd()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_getcwd( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_getcwd_result(values) -} - -/// Returns the process current working directory as a boxed PHP string. -pub(in crate::interpreter) fn eval_getcwd_result( - values: &mut impl RuntimeValueOps, -) -> Result { - let cwd = std::env::current_dir().map_err(|_| EvalStatus::RuntimeFatal)?; - values.string(cwd.to_string_lossy().as_ref()) -} - -/// Evaluates one PHP filesystem predicate over an eval expression. -pub(in crate::interpreter) fn eval_builtin_file_probe( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_probe_result(name, filename, context, values) -} - -/// Computes one local filesystem predicate and returns a PHP boolean. -pub(in crate::interpreter) fn eval_file_probe_result( - name: &str, - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { - return eval_user_wrapper_file_probe_from_stat(name, stat, values); - } - if stream_wrappers::is_phar_stream(&path) { - let exists = elephc_phar::extract_url_bytes(path.as_bytes()).is_some(); - let supported = matches!(name, "file_exists" | "is_file" | "is_readable"); - return values.bool_value(supported && exists); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - let path = std::path::Path::new(&path); - let result = match name { - "file_exists" => path.exists(), - "is_dir" => path.is_dir(), - "is_executable" => eval_path_is_executable(path), - "is_file" => path.is_file(), - "is_link" => std::fs::symlink_metadata(path) - .map(|metadata| metadata.file_type().is_symlink()) - .unwrap_or(false), - "is_readable" => eval_path_is_readable(path), - "is_writable" | "is_writeable" => eval_path_is_writable(path), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(result) -} - -/// Evaluates one scalar PHP stat metadata builtin over an eval expression. -pub(in crate::interpreter) fn eval_builtin_file_stat_scalar( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_file_stat_scalar_result(name, filename, context, values) -} - -/// Returns scalar stat metadata, using PHP false for failure where native elephc does. -pub(in crate::interpreter) fn eval_file_stat_scalar_result( - name: &str, - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { - return eval_user_wrapper_file_stat_scalar_from_stat(name, stat, values); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return match name { - "filemtime" => values.int(0), - _ => values.bool_value(false), - }; - }; - let metadata = match std::fs::metadata(path) { - Ok(metadata) => metadata, - Err(_) if name == "filemtime" => return values.int(0), - Err(_) => return values.bool_value(false), - }; - match name { - "fileatime" => values.int(metadata.atime()), - "filectime" => values.int(metadata.ctime()), - "filegroup" => values.int(i64::from(metadata.gid())), - "fileinode" => { - values.int(i64::try_from(metadata.ino()).map_err(|_| EvalStatus::RuntimeFatal)?) - } - "filemtime" => values.int(metadata.mtime()), - "fileowner" => values.int(i64::from(metadata.uid())), - "fileperms" => values.int(i64::from(metadata.mode())), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates PHP `filesize($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_filesize( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_filesize_result(filename, context, values) -} - -/// Returns one local file or supported wrapper size in bytes, or zero on failure. -pub(in crate::interpreter) fn eval_filesize_result( - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { - let size = eval_user_wrapper_stat_int_field(stat, "size", values)?.unwrap_or(0); - return values.int(size); - } - if let Ok(bytes) = super::file_get_contents::eval_read_path_or_wrapper_bytes(&path) { - return values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.int(0); - }; - let len = std::fs::metadata(path) - .map(|metadata| metadata.len()) - .unwrap_or(0); - values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) -} - -/// Evaluates PHP `filetype($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_filetype( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_filetype_result(filename, context, values) -} - -/// Returns the PHP filetype string for one path, or false when lstat fails. -pub(in crate::interpreter) fn eval_filetype_result( - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { - return match eval_user_wrapper_stat_int_field(stat, "mode", values)? { - Some(mode) => values.string(eval_filetype_label_from_mode(mode)), - None => values.bool_value(false), - }; - } - if stream_wrappers::is_phar_stream(&path) { - return if elephc_phar::extract_url_bytes(path.as_bytes()).is_some() { - values.string("file") - } else { - values.bool_value(false) - }; - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - let file_type = match std::fs::symlink_metadata(path) { - Ok(metadata) => metadata.file_type(), - Err(_) => return values.bool_value(false), - }; - let label = if file_type.is_file() { - "file" - } else if file_type.is_dir() { - "dir" - } else if file_type.is_symlink() { - "link" - } else if file_type.is_char_device() { - "char" - } else if file_type.is_block_device() { - "block" - } else if file_type.is_fifo() { - "fifo" - } else if file_type.is_socket() { - "socket" - } else { - "unknown" - }; - values.string(label) -} - -/// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_stat_array( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filename] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filename = eval_expr(filename, context, scope, values)?; - eval_stat_array_result(name, filename, context, values) -} - -/// Builds PHP's stat array for one local path, or returns false on stat failure. -pub(in crate::interpreter) fn eval_stat_array_result( - name: &str, - filename: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let path = eval_path_string(filename, values)?; - if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { - return Ok(stat); - } - let Some(path) = stream_wrappers::local_filesystem_path(&path) else { - return values.bool_value(false); - }; - let metadata = match name { - "stat" => std::fs::metadata(path), - "lstat" => std::fs::symlink_metadata(path), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let metadata = match metadata { - Ok(metadata) => metadata, - Err(_) => return values.bool_value(false), - }; - eval_stat_metadata_array(&metadata, values) -} - -/// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. -pub(in crate::interpreter) fn eval_stat_metadata_array( - metadata: &std::fs::Metadata, - values: &mut impl RuntimeValueOps, -) -> Result { - let fields = [ - ("dev", eval_u64_to_i64(metadata.dev())?), - ("ino", eval_u64_to_i64(metadata.ino())?), - ("mode", i64::from(metadata.mode())), - ("nlink", eval_u64_to_i64(metadata.nlink())?), - ("uid", i64::from(metadata.uid())), - ("gid", i64::from(metadata.gid())), - ("rdev", eval_u64_to_i64(metadata.rdev())?), - ("size", eval_u64_to_i64(metadata.size())?), - ("atime", metadata.atime()), - ("mtime", metadata.mtime()), - ("ctime", metadata.ctime()), - ("blksize", eval_u64_to_i64(metadata.blksize())?), - ("blocks", eval_u64_to_i64(metadata.blocks())?), - ]; - let mut result = values.assoc_new(fields.len() * 2)?; - for (index, (name, value)) in fields.iter().enumerate() { - result = eval_stat_array_set_int_key(result, index, *value, values)?; - result = eval_stat_array_set_string_key(result, name, *value, values)?; - } - Ok(result) -} - -/// Inserts one integer stat field under a numeric PHP array key. -pub(in crate::interpreter) fn eval_stat_array_set_int_key( - array: RuntimeCellHandle, - key: usize, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.int(i64::try_from(key).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Inserts one integer stat field under a string PHP array key. -pub(in crate::interpreter) fn eval_stat_array_set_string_key( - array: RuntimeCellHandle, - key: &str, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Converts unsigned stat metadata into the signed integer payload used by PHP cells. -pub(in crate::interpreter) fn eval_u64_to_i64(value: u64) -> Result { - i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs index c687a63c9c..3dbd772bdd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_fileatime_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fileatime", args, context, scope, values) + super::stat::eval_builtin_file_stat_scalar("fileatime", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fileatime` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_fileatime_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fileatime", evaluated_args, context, values) + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("fileatime", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs index 3282466187..df5ccc9460 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_filectime_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("filectime", args, context, scope, values) + super::stat::eval_builtin_file_stat_scalar("filectime", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `filectime` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_filectime_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("filectime", evaluated_args, context, values) + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("filectime", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs index c099b77b3c..c366b78d4e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_filegroup_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("filegroup", args, context, scope, values) + super::stat::eval_builtin_file_stat_scalar("filegroup", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `filegroup` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_filegroup_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("filegroup", evaluated_args, context, values) + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("filegroup", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs index e228a2c528..900f97458d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_fileinode_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fileinode", args, context, scope, values) + super::stat::eval_builtin_file_stat_scalar("fileinode", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fileinode` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_fileinode_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fileinode", evaluated_args, context, values) + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("fileinode", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs index 7e7cf57568..20269d4ff4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_filemtime_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("filemtime", args, context, scope, values) + super::stat::eval_builtin_file_stat_scalar("filemtime", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `filemtime` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_filemtime_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("filemtime", evaluated_args, context, values) + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("filemtime", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs index 2e51ace233..252af59a3e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_fileowner_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fileowner", args, context, scope, values) + super::stat::eval_builtin_file_stat_scalar("fileowner", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fileowner` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_fileowner_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fileowner", evaluated_args, context, values) + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("fileowner", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs index fc4a7ce8c6..8952df9ff6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_fileperms_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fileperms", args, context, scope, values) + super::stat::eval_builtin_file_stat_scalar("fileperms", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fileperms` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_fileperms_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fileperms", evaluated_args, context, values) + match evaluated_args { + [filename] => super::stat::eval_file_stat_scalar_result("fileperms", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs index f2c534741e..f3d22d92ff 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `filesize` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_filesize_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_filesize_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("filesize", args, context, scope, values) + eval_builtin_filesize(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `filesize` filesystem builtin through the area dispatcher. @@ -33,5 +35,45 @@ pub(in crate::interpreter) fn eval_filesize_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("filesize", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_filesize_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `filesize($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_filesize( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filesize_result(filename, context, values) +} + +/// Returns one local file or supported wrapper size in bytes, or zero on failure. +pub(in crate::interpreter) fn eval_filesize_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + let size = eval_user_wrapper_stat_int_field(stat, "size", values)?.unwrap_or(0); + return values.int(size); + } + if let Ok(bytes) = super::file_get_contents::eval_read_path_or_wrapper_bytes(&path) { + return values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.int(0); + }; + let len = std::fs::metadata(path) + .map(|metadata| metadata.len()) + .unwrap_or(0); + values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs index a3b30f08f2..711016f787 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `filetype` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_filetype_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_filetype_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("filetype", args, context, scope, values) + eval_builtin_filetype(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `filetype` filesystem builtin through the area dispatcher. @@ -33,5 +35,69 @@ pub(in crate::interpreter) fn eval_filetype_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("filetype", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_filetype_result(*filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `filetype($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_filetype( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_filetype_result(filename, context, values) +} + +/// Returns the PHP filetype string for one path, or false when lstat fails. +pub(in crate::interpreter) fn eval_filetype_result( + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return match eval_user_wrapper_stat_int_field(stat, "mode", values)? { + Some(mode) => values.string(eval_filetype_label_from_mode(mode)), + None => values.bool_value(false), + }; + } + if stream_wrappers::is_phar_stream(&path) { + return if elephc_phar::extract_url_bytes(path.as_bytes()).is_some() { + values.string("file") + } else { + values.bool_value(false) + }; + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let file_type = match std::fs::symlink_metadata(path) { + Ok(metadata) => metadata.file_type(), + Err(_) => return values.bool_value(false), + }; + let label = if file_type.is_file() { + "file" + } else if file_type.is_dir() { + "dir" + } else if file_type.is_symlink() { + "link" + } else if file_type.is_char_device() { + "char" + } else if file_type.is_block_device() { + "block" + } else if file_type.is_fifo() { + "fifo" + } else if file_type.is_socket() { + "socket" + } else { + "unknown" + }; + values.string(label) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs index f5208b0ccc..e61ecfbc80 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs @@ -20,18 +20,40 @@ use super::super::super::*; /// Dispatches direct eval calls for the `getcwd` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_getcwd_declared_call( args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("getcwd", args, context, scope, values) + eval_builtin_getcwd(args, values) } /// Dispatches evaluated-argument calls for the `getcwd` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_getcwd_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("getcwd", evaluated_args, context, values) + match evaluated_args { + [] => eval_getcwd_result(values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `getcwd()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_getcwd( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_getcwd_result(values) +} + +/// Returns the process current working directory as a boxed PHP string. +pub(in crate::interpreter) fn eval_getcwd_result( + values: &mut impl RuntimeValueOps, +) -> Result { + let cwd = std::env::current_dir().map_err(|_| EvalStatus::RuntimeFatal)?; + values.string(cwd.to_string_lossy().as_ref()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs index 60a9542d33..b7dfe8c168 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_is_dir_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("is_dir", args, context, scope, values) + super::file_exists::eval_builtin_file_probe("is_dir", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_dir` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_is_dir_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("is_dir", evaluated_args, context, values) + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_dir", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs index 2b328ecb4d..e05c7e6e20 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_is_executable_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("is_executable", args, context, scope, values) + super::file_exists::eval_builtin_file_probe("is_executable", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_executable` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_is_executable_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("is_executable", evaluated_args, context, values) + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_executable", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs index 93fdcd2efb..ca1aedc25b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_is_file_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("is_file", args, context, scope, values) + super::file_exists::eval_builtin_file_probe("is_file", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_file` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_is_file_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("is_file", evaluated_args, context, values) + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_file", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs index ec9e0bce1b..08a7007ca4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_is_link_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("is_link", args, context, scope, values) + super::file_exists::eval_builtin_file_probe("is_link", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_link` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_is_link_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("is_link", evaluated_args, context, values) + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_link", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs index 837287193d..3ad31b05ec 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_is_readable_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("is_readable", args, context, scope, values) + super::file_exists::eval_builtin_file_probe("is_readable", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_readable` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_is_readable_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("is_readable", evaluated_args, context, values) + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_readable", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs index 762ef4f7c6..3d51c845e6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_is_writable_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("is_writable", args, context, scope, values) + super::file_exists::eval_builtin_file_probe("is_writable", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_writable` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_is_writable_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("is_writable", evaluated_args, context, values) + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_writable", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs index f971dbe761..191229f8f3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_is_writeable_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("is_writeable", args, context, scope, values) + super::file_exists::eval_builtin_file_probe("is_writeable", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_writeable` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_is_writeable_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("is_writeable", evaluated_args, context, values) + match evaluated_args { + [filename] => super::file_exists::eval_file_probe_result("is_writeable", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs index e03d963e66..b1c3984b4b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_lstat_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("lstat", args, context, scope, values) + super::stat::eval_builtin_stat_array("lstat", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `lstat` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_lstat_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("lstat", evaluated_args, context, values) + match evaluated_args { + [filename] => super::stat::eval_stat_array_result("lstat", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 53bfecbb89..ccbe281e11 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -31,7 +31,6 @@ mod fgets; mod file; mod file_exists; mod file_get_contents; -mod file_io; mod file_put_contents; mod fileatime; mod filectime; @@ -157,7 +156,6 @@ mod user_wrapper_streams; mod values_dispatch; mod vfprintf; -pub(in crate::interpreter) use file_io::*; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use readline::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs index 81203ee15b..aa77bcd73d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs @@ -19,24 +19,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "file_exists" | "is_dir" | "is_executable" | "is_file" | "is_link" | "is_readable" - | "is_writable" | "is_writeable" => match evaluated_args { - [filename] => eval_file_probe_result(name, *filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "fileatime" | "filectime" | "filegroup" | "fileinode" | "filemtime" | "fileowner" - | "fileperms" => match evaluated_args { - [filename] => eval_file_stat_scalar_result(name, *filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "filesize" => match evaluated_args { - [filename] => eval_filesize_result(*filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "filetype" => match evaluated_args { - [filename] => eval_filetype_result(*filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "fnmatch" => match evaluated_args { [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, [pattern, filename, flags] => { @@ -44,10 +26,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ } _ => return Err(EvalStatus::RuntimeFatal), }, - "getcwd" => match evaluated_args { - [] => eval_getcwd_result(values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "realpath_cache_get" => match evaluated_args { [] => eval_realpath_cache_get_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), @@ -56,10 +34,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [] => eval_realpath_cache_size_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "stat" | "lstat" => match evaluated_args { - [filename] => eval_stat_array_result(name, *filename, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "sys_get_temp_dir" => match evaluated_args { [] => eval_sys_get_temp_dir_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs index 35089e58b0..675053d8ba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs @@ -16,6 +16,8 @@ eval_builtin! { } use super::super::super::*; +use crate::stream_wrappers; +use super::*; /// Dispatches direct eval calls for the `stat` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stat_declared_call( @@ -24,7 +26,7 @@ pub(in crate::interpreter) fn eval_stat_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stat", args, context, scope, values) + eval_builtin_stat_array("stat", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stat` filesystem builtin through the area dispatcher. @@ -33,5 +35,156 @@ pub(in crate::interpreter) fn eval_stat_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stat", evaluated_args, context, values) + match evaluated_args { + [filename] => eval_stat_array_result("stat", *filename, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates one scalar PHP stat metadata builtin over an eval expression. +pub(in crate::interpreter) fn eval_builtin_file_stat_scalar( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_file_stat_scalar_result(name, filename, context, values) +} + +/// Returns scalar stat metadata, using PHP false for failure where native elephc does. +pub(in crate::interpreter) fn eval_file_stat_scalar_result( + name: &str, + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return eval_user_wrapper_file_stat_scalar_from_stat(name, stat, values); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return match name { + "filemtime" => values.int(0), + _ => values.bool_value(false), + }; + }; + let metadata = match std::fs::metadata(path) { + Ok(metadata) => metadata, + Err(_) if name == "filemtime" => return values.int(0), + Err(_) => return values.bool_value(false), + }; + match name { + "fileatime" => values.int(metadata.atime()), + "filectime" => values.int(metadata.ctime()), + "filegroup" => values.int(i64::from(metadata.gid())), + "fileinode" => { + values.int(i64::try_from(metadata.ino()).map_err(|_| EvalStatus::RuntimeFatal)?) + } + "filemtime" => values.int(metadata.mtime()), + "fileowner" => values.int(i64::from(metadata.uid())), + "fileperms" => values.int(i64::from(metadata.mode())), + _ => Err(EvalStatus::RuntimeFatal), + } +} +/// Evaluates PHP `stat($filename)` or `lstat($filename)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stat_array( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filename] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filename = eval_expr(filename, context, scope, values)?; + eval_stat_array_result(name, filename, context, values) +} + +/// Builds PHP's stat array for one local path, or returns false on stat failure. +pub(in crate::interpreter) fn eval_stat_array_result( + name: &str, + filename: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let path = eval_path_string(filename, values)?; + if let Some(stat) = eval_user_wrapper_url_stat_result(&path, 0, context, values)? { + return Ok(stat); + } + let Some(path) = stream_wrappers::local_filesystem_path(&path) else { + return values.bool_value(false); + }; + let metadata = match name { + "stat" => std::fs::metadata(path), + "lstat" => std::fs::symlink_metadata(path), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let metadata = match metadata { + Ok(metadata) => metadata, + Err(_) => return values.bool_value(false), + }; + eval_stat_metadata_array(&metadata, values) +} + +/// Converts filesystem metadata into PHP's numeric-and-string keyed stat array. +pub(in crate::interpreter) fn eval_stat_metadata_array( + metadata: &std::fs::Metadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let fields = [ + ("dev", eval_u64_to_i64(metadata.dev())?), + ("ino", eval_u64_to_i64(metadata.ino())?), + ("mode", i64::from(metadata.mode())), + ("nlink", eval_u64_to_i64(metadata.nlink())?), + ("uid", i64::from(metadata.uid())), + ("gid", i64::from(metadata.gid())), + ("rdev", eval_u64_to_i64(metadata.rdev())?), + ("size", eval_u64_to_i64(metadata.size())?), + ("atime", metadata.atime()), + ("mtime", metadata.mtime()), + ("ctime", metadata.ctime()), + ("blksize", eval_u64_to_i64(metadata.blksize())?), + ("blocks", eval_u64_to_i64(metadata.blocks())?), + ]; + let mut result = values.assoc_new(fields.len() * 2)?; + for (index, (name, value)) in fields.iter().enumerate() { + result = eval_stat_array_set_int_key(result, index, *value, values)?; + result = eval_stat_array_set_string_key(result, name, *value, values)?; + } + Ok(result) +} + +/// Inserts one integer stat field under a numeric PHP array key. +pub(in crate::interpreter) fn eval_stat_array_set_int_key( + array: RuntimeCellHandle, + key: usize, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.int(i64::try_from(key).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts one integer stat field under a string PHP array key. +pub(in crate::interpreter) fn eval_stat_array_set_string_key( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Converts unsigned stat metadata into the signed integer payload used by PHP cells. +pub(in crate::interpreter) fn eval_u64_to_i64(value: u64) -> Result { + i64::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index f65dfaa83c..230f0fb39e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -117,7 +117,7 @@ pub(in crate::interpreter) fn eval_unary_stream_result( return Ok(result); } match context.stream_resources().metadata(id) { - Some(metadata) => eval_stat_metadata_array(&metadata, values), + Some(metadata) => super::stat::eval_stat_metadata_array(&metadata, values), None => values.bool_value(false), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs index 72aa4fcfe0..e40395b2c0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_file_io.rs @@ -2,8 +2,8 @@ //! Dispatches one-shot file I/O builtins through eval userspace stream wrappers. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::file_io` for `file()`, -//! `file_get_contents()`, `readfile()`, and `file_put_contents()`. +//! - `crate::interpreter::builtins::filesystem::file`, `file_get_contents`, +//! `readfile`, and `file_put_contents` for one-shot wrapper I/O. //! //! Key details: //! - These helpers open a temporary wrapper stream, perform the requested read or diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs index 7d629c8ee1..a96813d125 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_stat.rs @@ -2,8 +2,8 @@ //! Interprets userspace stream-wrapper stat results for path and stream builtins. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::file_io` when a path belongs -//! to a registered eval userspace stream wrapper. +//! - `crate::interpreter::builtins::filesystem::file_exists`, `stat`, +//! `filesize`, and `filetype` for wrapper-backed paths. //! - `crate::interpreter::builtins::filesystem::streams` when `fstat()` sees a //! userspace-wrapper stream resource. //! From fc25d45e9f0df3507a679c5c7983ae4bcf5420f3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:35:08 +0200 Subject: [PATCH 1119/1208] refactor(magician): move stream opening builtin implementations home --- .../builtins/filesystem/direct_dispatch.rs | 2 - .../interpreter/builtins/filesystem/fopen.rs | 61 ++++++++++++- .../filesystem/path_values_dispatch.rs | 6 +- .../filesystem/stream_values_dispatch.rs | 6 -- .../builtins/filesystem/streams.rs | 2 - .../builtins/filesystem/streams/open.rs | 88 ------------------- .../builtins/filesystem/tmpfile.rs | 32 ++++++- 7 files changed, 89 insertions(+), 108 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/open.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index 02982a4b95..c90493a069 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -166,7 +166,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_unary_stream(name, args, context, scope, values) } "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), - "fopen" => eval_builtin_fopen(args, context, scope, values), "fprintf" => eval_builtin_fprintf(args, context, scope, values), "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), "fread" => eval_builtin_fread(args, context, scope, values), @@ -236,7 +235,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_stream_wrapper_registry(name, args, context, scope, values) } "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), - "tmpfile" => eval_builtin_tmpfile(args, context, values), "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs index 7263f918b6..fe8c987c07 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs @@ -23,6 +23,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fopen` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fopen_declared_call( @@ -31,7 +32,7 @@ pub(in crate::interpreter) fn eval_fopen_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fopen", args, context, scope, values) + eval_builtin_fopen(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fopen` filesystem builtin through the area dispatcher. @@ -40,5 +41,61 @@ pub(in crate::interpreter) fn eval_fopen_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fopen", evaluated_args, context, values) + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_fopen_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Evaluates PHP `fopen($filename, $mode, ...)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fopen( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let filename = eval_expr(&args[0], context, scope, values)?; + let mode = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + let filename = eval_path_string(filename, values)?; + let mode = eval_stream_string(mode, values)?; + eval_fopen_path_result(&filename, &mode, context, scope, values) +} + +/// Opens a local file stream and returns a resource cell or PHP false. +pub(in crate::interpreter) fn eval_fopen_result( + filename: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let filename = eval_path_string(filename, values)?; + let mode = eval_stream_string(mode, values)?; + let mut scope = ElephcEvalScope::new(); + eval_fopen_path_result(&filename, &mode, context, &mut scope, values) +} + +/// Opens a stream by already-coerced path and mode strings. +fn eval_fopen_path_result( + filename: &str, + mode: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_user_wrapper_fopen_result(filename, mode, context, scope, values)? { + return Ok(result); + } + match context.stream_resources_mut().open_path(filename, mode) { + Some(id) => values.resource(id), + None => { + values.warning("Warning: fopen(): Failed to open stream\n")?; + values.bool_value(false) + } + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs index aa77bcd73d..c15508ed4f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs @@ -15,7 +15,7 @@ use super::*; pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_result( name: &str, evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { @@ -38,10 +38,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_ [] => eval_sys_get_temp_dir_result(values)?, _ => return Err(EvalStatus::RuntimeFatal), }, - "tmpfile" => match evaluated_args { - [] => eval_tmpfile_result(context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs index 8e838c1dc7..88a7b5a2d0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs @@ -49,12 +49,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value eval_flock_result(evaluated_args[0], evaluated_args[1], context, values)?; values.bool_value(success)? } - "fopen" => { - if !(2..=4).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - eval_fopen_result(evaluated_args[0], evaluated_args[1], context, values)? - } "fprintf" => { let Some((stream, rest)) = evaluated_args.split_first() else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index 230f0fb39e..8e55a6b413 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -19,13 +19,11 @@ mod common; mod csv_format; mod flock; mod metadata; -mod open; pub(in crate::interpreter) use common::*; pub(in crate::interpreter) use csv_format::*; pub(in crate::interpreter) use flock::*; pub(in crate::interpreter) use metadata::*; -pub(in crate::interpreter) use open::*; /// Evaluates one unary stream builtin over an eval expression. pub(in crate::interpreter) fn eval_builtin_unary_stream( diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/open.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/open.rs deleted file mode 100644 index 54a4c3552e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/open.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! Purpose: -//! Stream-opening builtins for eval-local file resources. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::streams` re-exports. -//! - Filesystem stream declaration dispatchers. -//! -//! Key details: -//! - User wrapper `stream_open` gets the first chance before local file handles -//! are inserted into eval's stream table. - -use super::*; - -/// Evaluates PHP `fopen($filename, $mode, ...)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fopen( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let filename = eval_expr(&args[0], context, scope, values)?; - let mode = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - eval_expr(arg, context, scope, values)?; - } - let filename = eval_path_string(filename, values)?; - let mode = eval_stream_string(mode, values)?; - eval_fopen_path_result(&filename, &mode, context, scope, values) -} - -/// Opens a local file stream and returns a resource cell or PHP false. -pub(in crate::interpreter) fn eval_fopen_result( - filename: RuntimeCellHandle, - mode: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let filename = eval_path_string(filename, values)?; - let mode = eval_stream_string(mode, values)?; - let mut scope = ElephcEvalScope::new(); - eval_fopen_path_result(&filename, &mode, context, &mut scope, values) -} - -/// Opens a stream by already-coerced path and mode strings. -fn eval_fopen_path_result( - filename: &str, - mode: &str, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_user_wrapper_fopen_result(filename, mode, context, scope, values)? { - return Ok(result); - } - match context.stream_resources_mut().open_path(filename, mode) { - Some(id) => values.resource(id), - None => { - values.warning("Warning: fopen(): Failed to open stream\n")?; - values.bool_value(false) - } - } -} - -/// Evaluates PHP `tmpfile()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_tmpfile( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_tmpfile_result(context, values) -} - -/// Creates an anonymous temporary file stream resource or returns PHP false. -pub(in crate::interpreter) fn eval_tmpfile_result( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match context.stream_resources_mut().open_tmpfile() { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs index 878b9a1519..db27dd868b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs @@ -21,10 +21,10 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_tmpfile_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("tmpfile", args, context, scope, values) + eval_builtin_tmpfile(args, context, values) } /// Dispatches evaluated-argument calls for the `tmpfile` filesystem builtin through the area dispatcher. @@ -33,5 +33,31 @@ pub(in crate::interpreter) fn eval_tmpfile_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("tmpfile", evaluated_args, context, values) + match evaluated_args { + [] => eval_tmpfile_result(context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `tmpfile()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_tmpfile( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_tmpfile_result(context, values) +} + +/// Creates an anonymous temporary file stream resource or returns PHP false. +pub(in crate::interpreter) fn eval_tmpfile_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match context.stream_resources_mut().open_tmpfile() { + Some(id) => values.resource(id), + None => values.bool_value(false), + } } From 53d272e7ce673a3371579615e1f920a1ecb97995 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:39:26 +0200 Subject: [PATCH 1120/1208] refactor(magician): move core stream builtin implementations home --- .../builtins/filesystem/direct_dispatch.rs | 11 - .../interpreter/builtins/filesystem/fclose.rs | 35 +- .../builtins/filesystem/fdatasync.rs | 7 +- .../interpreter/builtins/filesystem/feof.rs | 35 +- .../interpreter/builtins/filesystem/fflush.rs | 35 +- .../interpreter/builtins/filesystem/fgetc.rs | 39 +- .../interpreter/builtins/filesystem/fgets.rs | 42 +- .../builtins/filesystem/fpassthru.rs | 41 +- .../interpreter/builtins/filesystem/fread.rs | 41 +- .../interpreter/builtins/filesystem/fseek.rs | 53 ++- .../interpreter/builtins/filesystem/fstat.rs | 38 +- .../interpreter/builtins/filesystem/fsync.rs | 39 +- .../interpreter/builtins/filesystem/ftell.rs | 38 +- .../builtins/filesystem/ftruncate.rs | 41 +- .../interpreter/builtins/filesystem/fwrite.rs | 41 +- .../interpreter/builtins/filesystem/rewind.rs | 35 +- .../filesystem/stream_copy_to_stream.rs | 60 ++- .../filesystem/stream_get_contents.rs | 57 ++- .../builtins/filesystem/stream_get_line.rs | 59 ++- .../filesystem/stream_get_meta_data.rs | 89 +++- .../filesystem/stream_sockets/datagrams.rs | 6 +- .../filesystem/stream_values_dispatch.rs | 62 --- .../builtins/filesystem/streams.rs | 422 +----------------- .../builtins/filesystem/streams/metadata.rs | 67 --- 24 files changed, 795 insertions(+), 598 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/metadata.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index c90493a069..b3e0137922 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -161,18 +161,10 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( match name { "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), - "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" - | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { - eval_builtin_unary_stream(name, args, context, scope, values) - } "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), "fprintf" => eval_builtin_fprintf(args, context, scope, values), "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), - "fread" => eval_builtin_fread(args, context, scope, values), "fscanf" => eval_builtin_fscanf(args, context, scope, values), - "fseek" => eval_builtin_fseek(args, context, scope, values), - "ftruncate" => eval_builtin_ftruncate(args, context, scope, values), - "fwrite" => eval_builtin_fwrite(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), @@ -202,7 +194,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "stream_context_set_params" => { eval_builtin_stream_context_set_params(args, context, scope, values) } - "stream_copy_to_stream" => eval_builtin_stream_copy_to_stream(args, context, scope, values), "stream_filter_append" | "stream_filter_prepend" => { eval_builtin_stream_filter_attach(name, args, context, scope, values) } @@ -210,8 +201,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_stream_filter_register(args, context, scope, values) } "stream_filter_remove" => eval_builtin_stream_filter_remove(args, context, scope, values), - "stream_get_contents" => eval_builtin_stream_get_contents(args, context, scope, values), - "stream_get_line" => eval_builtin_stream_get_line(args, context, scope, values), "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs index 41620e671b..b093a914b3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fclose` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fclose_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fclose_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fclose", args, context, scope, values) + eval_builtin_fclose(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fclose` filesystem builtin through the area dispatcher. @@ -33,5 +34,35 @@ pub(in crate::interpreter) fn eval_fclose_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fclose", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_fclose_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fclose($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fclose( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fclose_result(stream, context, values) +} + +/// Closes one materialized stream resource and returns whether it succeeded. +pub(in crate::interpreter) fn eval_fclose_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fclose_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources_mut().close(id)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs index 5fc208d769..51bcc93b3c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_fdatasync_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fdatasync", args, context, scope, values) + super::fsync::eval_builtin_stream_sync("fdatasync", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fdatasync` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_fdatasync_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fdatasync", evaluated_args, context, values) + match evaluated_args { + [stream] => super::fsync::eval_stream_sync_result("fdatasync", *stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs index 7408dc8e04..53323c632e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `feof` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_feof_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_feof_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("feof", args, context, scope, values) + eval_builtin_feof(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `feof` filesystem builtin through the area dispatcher. @@ -33,5 +34,35 @@ pub(in crate::interpreter) fn eval_feof_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("feof", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_feof_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `feof($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_feof( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_feof_result(stream, context, values) +} + +/// Reports whether a materialized stream resource is at EOF. +pub(in crate::interpreter) fn eval_feof_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_feof_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources().eof(id).unwrap_or(false)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs index fea5b92588..a754b943d3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fflush` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fflush_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fflush_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fflush", args, context, scope, values) + eval_builtin_fflush(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fflush` filesystem builtin through the area dispatcher. @@ -33,5 +34,35 @@ pub(in crate::interpreter) fn eval_fflush_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fflush", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_fflush_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fflush($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fflush( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fflush_result(stream, context, values) +} + +/// Flushes a materialized stream resource. +pub(in crate::interpreter) fn eval_fflush_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fflush_result(id, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources_mut().flush(id)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs index d49b037493..b7b0037f87 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fgetc` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fgetc_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fgetc_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fgetc", args, context, scope, values) + eval_builtin_fgetc(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fgetc` filesystem builtin through the area dispatcher. @@ -33,5 +34,39 @@ pub(in crate::interpreter) fn eval_fgetc_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fgetc", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_fgetc_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fgetc($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fgetc( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fgetc_result(stream, context, values) +} + +/// Reads one byte from a materialized stream resource. +pub(in crate::interpreter) fn eval_fgetc_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fread_result(id, 1, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().read(id, 1) { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs index d08f4db612..f77ba004cc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fgets` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fgets_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fgets_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fgets", args, context, scope, values) + eval_builtin_fgets(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fgets` filesystem builtin through the area dispatcher. @@ -33,5 +34,42 @@ pub(in crate::interpreter) fn eval_fgets_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fgets", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_fgets_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fgets($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fgets( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fgets_result(stream, context, values) +} + +/// Reads one newline-terminated string from a materialized stream resource. +pub(in crate::interpreter) fn eval_fgets_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fgets_result(id, context, values)? { + return Ok(result); + } + match context + .stream_resources_mut() + .read_line(id, usize::MAX, None, true, true) + { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs index 0f693a0572..1eaebdf67f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fpassthru` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fpassthru_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fpassthru_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fpassthru", args, context, scope, values) + eval_builtin_fpassthru(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fpassthru` filesystem builtin through the area dispatcher. @@ -33,5 +34,41 @@ pub(in crate::interpreter) fn eval_fpassthru_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fpassthru", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_fpassthru_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fpassthru($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fpassthru( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fpassthru_result(stream, context, values) +} + +/// Streams all remaining bytes to eval output and returns the emitted byte count. +pub(in crate::interpreter) fn eval_fpassthru_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fpassthru_result(id, context, values)? { + return Ok(result); + } + let Some(bytes) = context.stream_resources_mut().get_contents(id, None, None) else { + return values.bool_value(false); + }; + let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; + let output = values.string_bytes_value(&bytes)?; + values.echo(output)?; + values.int(len) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs index f47b0440d7..77e2400b9a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fread` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fread_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fread_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fread", args, context, scope, values) + eval_builtin_fread(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fread` filesystem builtin through the area dispatcher. @@ -33,5 +34,41 @@ pub(in crate::interpreter) fn eval_fread_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fread", evaluated_args, context, values) + match evaluated_args { + [stream, length] => eval_fread_result(*stream, *length, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fread($stream, $length)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fread( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, length] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let length = eval_expr(length, context, scope, values)?; + eval_fread_result(stream, length, context, values) +} + +/// Reads bytes from a materialized stream resource. +pub(in crate::interpreter) fn eval_fread_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_nonnegative_usize(length, values)?; + if let Some(result) = eval_user_wrapper_fread_result(id, length, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().read(id, length) { + Some(bytes) => values.string_bytes_value(&bytes), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs index ea096f66db..3100ac0cab 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs @@ -18,6 +18,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fseek` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fseek_declared_call( @@ -26,7 +27,7 @@ pub(in crate::interpreter) fn eval_fseek_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fseek", args, context, scope, values) + eval_builtin_fseek(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fseek` filesystem builtin through the area dispatcher. @@ -35,5 +36,53 @@ pub(in crate::interpreter) fn eval_fseek_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fseek", evaluated_args, context, values) + match evaluated_args { + [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values), + [stream, offset, whence] => eval_fseek_result(*stream, *offset, Some(*whence), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fseek($stream, $offset, $whence = SEEK_SET)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fseek( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let offset = eval_expr(&args[1], context, scope, values)?; + let whence = match args.get(2) { + Some(whence) => Some(eval_expr(whence, context, scope, values)?), + None => None, + }; + eval_fseek_result(stream, offset, whence, context, values) +} + +/// Seeks a materialized stream and returns PHP's 0 or -1 status code. +pub(in crate::interpreter) fn eval_fseek_result( + stream: RuntimeCellHandle, + offset: RuntimeCellHandle, + whence: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let offset = eval_int_value(offset, values)?; + let whence = match whence { + Some(whence) => eval_int_value(whence, values)?, + None => 0, + }; + if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, offset, whence, context, values)? { + return values.int(if seek_ok { 0 } else { -1 }); + } + let status = if context.stream_resources_mut().seek(id, offset, whence) { + 0 + } else { + -1 + }; + values.int(status) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs index b7e0412521..18bb208566 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fstat` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fstat_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fstat_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fstat", args, context, scope, values) + eval_builtin_fstat(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fstat` filesystem builtin through the area dispatcher. @@ -33,5 +34,38 @@ pub(in crate::interpreter) fn eval_fstat_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fstat", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_fstat_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fstat($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_fstat( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_fstat_result(stream, context, values) +} + +/// Builds PHP's stat array for a materialized stream resource. +pub(in crate::interpreter) fn eval_fstat_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_fstat_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources().metadata(id) { + Some(metadata) => super::stat::eval_stat_metadata_array(&metadata, values), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs index e7ecf063de..fba1e25a23 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fsync` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fsync_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fsync_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fsync", args, context, scope, values) + eval_builtin_stream_sync("fsync", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fsync` filesystem builtin through the area dispatcher. @@ -33,5 +34,39 @@ pub(in crate::interpreter) fn eval_fsync_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fsync", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_stream_sync_result("fsync", *stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fsync($stream)` or `fdatasync($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_sync( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_sync_result(name, stream, context, values) +} + +/// Synchronizes one materialized stream resource to storage. +pub(in crate::interpreter) fn eval_stream_sync_result( + name: &str, + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let ok = match name { + "fsync" => context.stream_resources_mut().sync_all(id), + "fdatasync" => context.stream_resources_mut().sync_data(id), + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(ok) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs index bd6687a3ca..f3874ab049 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `ftell` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_ftell_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_ftell_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("ftell", args, context, scope, values) + eval_builtin_ftell(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `ftell` filesystem builtin through the area dispatcher. @@ -33,5 +34,38 @@ pub(in crate::interpreter) fn eval_ftell_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("ftell", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_ftell_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `ftell($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_ftell( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_ftell_result(stream, context, values) +} + +/// Returns the current byte offset of a materialized stream resource. +pub(in crate::interpreter) fn eval_ftell_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(result) = eval_user_wrapper_ftell_result(id, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().tell(id) { + Some(position) => values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs index c5ef7b4887..7bb0793e20 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `ftruncate` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_ftruncate_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_ftruncate_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("ftruncate", args, context, scope, values) + eval_builtin_ftruncate(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `ftruncate` filesystem builtin through the area dispatcher. @@ -33,5 +34,41 @@ pub(in crate::interpreter) fn eval_ftruncate_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("ftruncate", evaluated_args, context, values) + match evaluated_args { + [stream, size] => eval_ftruncate_result(*stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `ftruncate($stream, $size)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_ftruncate( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, size] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let size = eval_expr(size, context, scope, values)?; + eval_ftruncate_result(stream, size, context, values) +} + +/// Truncates a materialized stream resource. +pub(in crate::interpreter) fn eval_ftruncate_result( + stream: RuntimeCellHandle, + size: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let size = eval_int_value(size, values)?; + let Ok(size) = u64::try_from(size) else { + return values.bool_value(false); + }; + if let Some(result) = eval_user_wrapper_ftruncate_result(id, size, context, values)? { + return Ok(result); + } + values.bool_value(context.stream_resources_mut().truncate(id, size)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs index 596e33d673..e3a914bfce 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fwrite` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fwrite_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_fwrite_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fwrite", args, context, scope, values) + eval_builtin_fwrite(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fwrite` filesystem builtin through the area dispatcher. @@ -33,5 +34,41 @@ pub(in crate::interpreter) fn eval_fwrite_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fwrite", evaluated_args, context, values) + match evaluated_args { + [stream, data] => eval_fwrite_result(*stream, *data, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fwrite($stream, $data)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fwrite( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, data] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let data = eval_expr(data, context, scope, values)?; + eval_fwrite_result(stream, data, context, values) +} + +/// Writes bytes to a materialized stream resource. +pub(in crate::interpreter) fn eval_fwrite_result( + stream: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let data = values.string_bytes(data)?; + if let Some(result) = eval_user_wrapper_fwrite_result(id, &data, context, values)? { + return Ok(result); + } + match context.stream_resources_mut().write(id, &data) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs index 300e8a22a7..3ef166dfcc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `rewind` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_rewind_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_rewind_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("rewind", args, context, scope, values) + eval_builtin_rewind(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `rewind` filesystem builtin through the area dispatcher. @@ -33,5 +34,35 @@ pub(in crate::interpreter) fn eval_rewind_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("rewind", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_rewind_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `rewind($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_rewind( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_rewind_result(stream, context, values) +} + +/// Rewinds a materialized stream resource to byte offset zero. +pub(in crate::interpreter) fn eval_rewind_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, 0, 0, context, values)? { + return values.bool_value(seek_ok); + } + values.bool_value(context.stream_resources_mut().rewind(id)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs index dc5b56a2db..486e866e35 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs @@ -23,6 +23,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `stream_copy_to_stream` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_copy_to_stream_declared_call( @@ -31,7 +32,7 @@ pub(in crate::interpreter) fn eval_stream_copy_to_stream_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_copy_to_stream", args, context, scope, values) + eval_builtin_stream_copy_to_stream(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_copy_to_stream` filesystem builtin through the area dispatcher. @@ -40,5 +41,60 @@ pub(in crate::interpreter) fn eval_stream_copy_to_stream_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_copy_to_stream", evaluated_args, context, values) + match evaluated_args { + [from, to] => eval_stream_copy_to_stream_result(*from, *to, None, None, context, values), + [from, to, length] => eval_stream_copy_to_stream_result(*from, *to, Some(*length), None, context, values), + [from, to, length, offset] => eval_stream_copy_to_stream_result(*from, *to, Some(*length), Some(*offset), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `stream_copy_to_stream($from, $to, $length = null, $offset = -1)`. +pub(in crate::interpreter) fn eval_builtin_stream_copy_to_stream( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let from = eval_expr(&args[0], context, scope, values)?; + let to = eval_expr(&args[1], context, scope, values)?; + let length = match args.get(2) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let offset = match args.get(3) { + Some(offset) => Some(eval_expr(offset, context, scope, values)?), + None => None, + }; + eval_stream_copy_to_stream_result(from, to, length, offset, context, values) +} + +/// Copies bytes between two materialized stream resources. +pub(in crate::interpreter) fn eval_stream_copy_to_stream_result( + from: RuntimeCellHandle, + to: RuntimeCellHandle, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let from = eval_stream_resource_id(from, values)?; + let to = eval_stream_resource_id(to, values)?; + let length = eval_optional_stream_length(length, values)?; + let offset = eval_optional_stream_offset(offset, values)?; + if let Some(result) = + eval_user_wrapper_stream_copy_to_stream_result(from, to, length, offset, context, values)? + { + return Ok(result); + } + match context + .stream_resources_mut() + .copy_to_stream(from, to, length, offset) + { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs index 6d2223bda4..42f34f4369 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs @@ -22,6 +22,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `stream_get_contents` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_get_contents_declared_call( @@ -30,7 +31,7 @@ pub(in crate::interpreter) fn eval_stream_get_contents_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_get_contents", args, context, scope, values) + eval_builtin_stream_get_contents(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_get_contents` filesystem builtin through the area dispatcher. @@ -39,5 +40,57 @@ pub(in crate::interpreter) fn eval_stream_get_contents_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_get_contents", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_stream_get_contents_result(*stream, None, None, context, values), + [stream, length] => eval_stream_get_contents_result(*stream, Some(*length), None, context, values), + [stream, length, offset] => eval_stream_get_contents_result(*stream, Some(*length), Some(*offset), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `stream_get_contents($stream, $length = null, $offset = -1)`. +pub(in crate::interpreter) fn eval_builtin_stream_get_contents( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = match args.get(1) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let offset = match args.get(2) { + Some(offset) => Some(eval_expr(offset, context, scope, values)?), + None => None, + }; + eval_stream_get_contents_result(stream, length, offset, context, values) +} + +/// Reads the remaining or bounded contents from a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_get_contents_result( + stream: RuntimeCellHandle, + length: Option, + offset: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_optional_stream_length(length, values)?; + let offset = eval_optional_stream_offset(offset, values)?; + if let Some(result) = + eval_user_wrapper_stream_get_contents_result(id, length, offset, context, values)? + { + return Ok(result); + } + match context + .stream_resources_mut() + .get_contents(id, length, offset) + { + Some(bytes) => values.string_bytes_value(&bytes), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs index 19c1449170..4882a80c37 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs @@ -18,6 +18,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `stream_get_line` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_get_line_declared_call( @@ -26,7 +27,7 @@ pub(in crate::interpreter) fn eval_stream_get_line_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_get_line", args, context, scope, values) + eval_builtin_stream_get_line(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_get_line` filesystem builtin through the area dispatcher. @@ -35,5 +36,59 @@ pub(in crate::interpreter) fn eval_stream_get_line_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_get_line", evaluated_args, context, values) + match evaluated_args { + [stream, length] => eval_stream_get_line_result(*stream, *length, None, context, values), + [stream, length, ending] => eval_stream_get_line_result(*stream, *length, Some(*ending), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `stream_get_line($stream, $length, $ending = null)`. +pub(in crate::interpreter) fn eval_builtin_stream_get_line( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = eval_expr(&args[1], context, scope, values)?; + let ending = match args.get(2) { + Some(ending) => Some(eval_expr(ending, context, scope, values)?), + None => None, + }; + eval_stream_get_line_result(stream, length, ending, context, values) +} + +/// Reads one line-like byte sequence from a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_get_line_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + ending: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_nonnegative_usize(length, values)?; + let ending = match ending { + Some(ending) if values.type_tag(ending)? != EVAL_TAG_NULL => { + Some(values.string_bytes(ending)?) + } + _ => None, + }; + if let Some(result) = + eval_user_wrapper_stream_get_line_result(id, length, ending.as_deref(), context, values)? + { + return Ok(result); + } + match context + .stream_resources_mut() + .read_line(id, length, ending.as_deref(), false, false) + { + Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), + Some(_) => values.bool_value(false), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs index f7178a8283..804ac8fb3a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `stream_get_meta_data` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_get_meta_data_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_stream_get_meta_data_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_get_meta_data", args, context, scope, values) + eval_builtin_stream_get_meta_data(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_get_meta_data` filesystem builtin through the area dispatcher. @@ -33,5 +34,89 @@ pub(in crate::interpreter) fn eval_stream_get_meta_data_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_get_meta_data", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_stream_get_meta_data_handle_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds PHP's stream metadata array for one eval-local stream resource. +pub(in crate::interpreter) fn eval_stream_get_meta_data_result( + id: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(meta) = context.stream_resources().meta_data(id) else { + return values.bool_value(false); + }; + let mut result = values.assoc_new(9)?; + result = eval_stream_meta_set_bool(result, "timed_out", false, values)?; + result = eval_stream_meta_set_bool(result, "blocked", true, values)?; + result = eval_stream_meta_set_bool(result, "eof", meta.eof, values)?; + result = eval_stream_meta_set_string(result, "wrapper_type", "plainfile", values)?; + result = eval_stream_meta_set_string(result, "stream_type", "STDIO", values)?; + result = eval_stream_meta_set_string(result, "mode", &meta.mode, values)?; + result = eval_stream_meta_set_int(result, "unread_bytes", 0, values)?; + result = eval_stream_meta_set_bool(result, "seekable", true, values)?; + eval_stream_meta_set_string(result, "uri", &meta.uri, values) +} + +/// Inserts a boolean field into the stream metadata array. +fn eval_stream_meta_set_bool( + array: RuntimeCellHandle, + key: &str, + value: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.bool_value(value)?; + values.array_set(array, key, value) +} + +/// Inserts an integer field into the stream metadata array. +fn eval_stream_meta_set_int( + array: RuntimeCellHandle, + key: &str, + value: i64, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.int(value)?; + values.array_set(array, key, value) +} + +/// Inserts a string field into the stream metadata array. +fn eval_stream_meta_set_string( + array: RuntimeCellHandle, + key: &str, + value: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(key)?; + let value = values.string(value)?; + values.array_set(array, key, value) +} + +/// Evaluates PHP `stream_get_meta_data($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_get_meta_data( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_get_meta_data_handle_result(stream, context, values) +} + +/// Builds PHP metadata for one materialized stream resource handle. +pub(in crate::interpreter) fn eval_stream_get_meta_data_handle_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + eval_stream_get_meta_data_result(id, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs index 9471c298d3..62669dacda 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs @@ -35,7 +35,7 @@ pub(in crate::interpreter) fn eval_stream_socket_sendto_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_fwrite_result(stream, data, context, values) + super::super::fwrite::eval_fwrite_result(stream, data, context, values) } /// Evaluates `stream_socket_recvfrom()` over full eval call metadata. @@ -69,7 +69,7 @@ pub(in crate::interpreter) fn eval_stream_socket_recvfrom_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_fread_result(stream, length, context, values) + super::super::fread::eval_fread_result(stream, length, context, values) } /// Reads bytes from a connected socket stream and returns the tracked remote endpoint name. @@ -81,6 +81,6 @@ pub(in crate::interpreter) fn eval_stream_socket_recvfrom_with_address_result( ) -> Result<(RuntimeCellHandle, Option), EvalStatus> { let id = eval_socket_resource_id(stream, values)?; let address = context.stream_resources().socket_name(id, true); - let result = eval_fread_result(stream, length, context, values)?; + let result = super::super::fread::eval_fread_result(stream, length, context, values)?; Ok((result, address)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs index 88a7b5a2d0..1908751f4b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs @@ -19,13 +19,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "fclose" | "fgetc" | "fgets" | "feof" | "fflush" | "fpassthru" | "fsync" - | "fdatasync" | "ftell" | "rewind" | "fstat" | "stream_get_meta_data" => { - match evaluated_args { - [stream] => eval_unary_stream_result(name, *stream, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - } - } "fgetcsv" => match evaluated_args { [stream] => eval_fgetcsv_result(*stream, None, None, context, values)?, [stream, length] => { @@ -73,23 +66,12 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value )?, _ => return Err(EvalStatus::RuntimeFatal), }, - "fread" => match evaluated_args { - [stream, length] => eval_fread_result(*stream, *length, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "fscanf" => { if evaluated_args.len() < 2 { return Err(EvalStatus::RuntimeFatal); } eval_fscanf_result(evaluated_args[0], evaluated_args[1], context, values)? } - "fseek" => match evaluated_args { - [stream, offset] => eval_fseek_result(*stream, *offset, None, context, values)?, - [stream, offset, whence] => { - eval_fseek_result(*stream, *offset, Some(*whence), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, "fsockopen" | "pfsockopen" => { if !(2..=5).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); @@ -97,14 +79,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value eval_fsockopen_by_value_ref_warnings(name, evaluated_args.len(), values)?; eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values)? } - "ftruncate" => match evaluated_args { - [stream, size] => eval_ftruncate_result(*stream, *size, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "fwrite" => match evaluated_args { - [stream, data] => eval_fwrite_result(*stream, *data, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "readline" => { if evaluated_args.len() > 1 { return Err(EvalStatus::RuntimeFatal); @@ -188,21 +162,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value } values.bool_value(true)? } - "stream_copy_to_stream" => match evaluated_args { - [from, to] => eval_stream_copy_to_stream_result(*from, *to, None, None, context, values)?, - [from, to, length] => { - eval_stream_copy_to_stream_result(*from, *to, Some(*length), None, context, values)? - } - [from, to, length, offset] => eval_stream_copy_to_stream_result( - *from, - *to, - Some(*length), - Some(*offset), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "stream_filter_register" => { let [filter_name, class] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); @@ -227,27 +186,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value }; eval_stream_filter_remove_result(*stream_filter, context, values)? } - "stream_get_contents" => match evaluated_args { - [stream] => eval_stream_get_contents_result(*stream, None, None, context, values)?, - [stream, length] => { - eval_stream_get_contents_result(*stream, Some(*length), None, context, values)? - } - [stream, length, offset] => eval_stream_get_contents_result( - *stream, - Some(*length), - Some(*offset), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stream_get_line" => match evaluated_args { - [stream, length] => eval_stream_get_line_result(*stream, *length, None, context, values)?, - [stream, length, ending] => { - eval_stream_get_line_result(*stream, *length, Some(*ending), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, "stream_isatty" => match evaluated_args { [stream] => eval_stream_isatty_result(*stream, context, values)?, _ => return Err(EvalStatus::RuntimeFatal), diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index 8e55a6b413..fa5ca535a2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -1,16 +1,13 @@ //! Purpose: -//! Implements eval-local file stream builtins backed by host file handles. -//! These builtins turn PHP resource cells into ids stored in the eval context's -//! stream table. +//! Groups shared helpers for eval-local filesystem stream builtins. //! //! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. +//! - Leaf filesystem stream builtin files that need stream-resource coercions, +//! CSV formatting, or direct `flock()` argument binding. //! //! Key details: -//! - Runtime resource payloads are zero-based; `get_resource_id()` exposes payload + 1. -//! - File-backed streams stay in `EvalStreamResources`; userspace wrapper calls -//! delegate to the focused wrapper-dispatch helper module. +//! - Builtin implementations live in their owning leaf files; this module only +//! re-exports shared stream helper modules. use super::super::super::*; use super::*; @@ -18,416 +15,7 @@ use super::*; mod common; mod csv_format; mod flock; -mod metadata; pub(in crate::interpreter) use common::*; pub(in crate::interpreter) use csv_format::*; pub(in crate::interpreter) use flock::*; -pub(in crate::interpreter) use metadata::*; - -/// Evaluates one unary stream builtin over an eval expression. -pub(in crate::interpreter) fn eval_builtin_unary_stream( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - eval_unary_stream_result(name, stream, context, values) -} - -/// Evaluates a materialized unary stream builtin argument. -pub(in crate::interpreter) fn eval_unary_stream_result( - name: &str, - stream: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - match name { - "fclose" => { - if let Some(result) = eval_user_wrapper_fclose_result(id, context, values)? { - return Ok(result); - } - values.bool_value(context.stream_resources_mut().close(id)) - } - "fgetc" => { - if let Some(result) = eval_user_wrapper_fread_result(id, 1, context, values)? { - return Ok(result); - } - match context.stream_resources_mut().read(id, 1) { - Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), - Some(_) => values.bool_value(false), - None => values.bool_value(false), - } - } - "fgets" => { - if let Some(result) = eval_user_wrapper_fgets_result(id, context, values)? { - return Ok(result); - } - match context - .stream_resources_mut() - .read_line(id, usize::MAX, None, true, true) - { - Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), - Some(_) => values.bool_value(false), - None => values.bool_value(false), - } - } - "feof" => { - if let Some(result) = eval_user_wrapper_feof_result(id, context, values)? { - return Ok(result); - } - values.bool_value(context.stream_resources().eof(id).unwrap_or(false)) - } - "fflush" => { - if let Some(result) = eval_user_wrapper_fflush_result(id, context, values)? { - return Ok(result); - } - values.bool_value(context.stream_resources_mut().flush(id)) - } - "fpassthru" => eval_fpassthru_result(id, context, values), - "fsync" => values.bool_value(context.stream_resources_mut().sync_all(id)), - "fdatasync" => values.bool_value(context.stream_resources_mut().sync_data(id)), - "ftell" => { - if let Some(result) = eval_user_wrapper_ftell_result(id, context, values)? { - return Ok(result); - } - match context.stream_resources_mut().tell(id) { - Some(position) => { - values.int(i64::try_from(position).map_err(|_| EvalStatus::RuntimeFatal)?) - } - None => values.bool_value(false), - } - }, - "rewind" => { - if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, 0, 0, context, values)? { - return values.bool_value(seek_ok); - } - values.bool_value(context.stream_resources_mut().rewind(id)) - } - "fstat" => { - if let Some(result) = eval_user_wrapper_fstat_result(id, context, values)? { - return Ok(result); - } - match context.stream_resources().metadata(id) { - Some(metadata) => super::stat::eval_stat_metadata_array(&metadata, values), - None => values.bool_value(false), - } - } - "stream_get_meta_data" => eval_stream_get_meta_data_result(id, context, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Streams all remaining bytes to eval output and returns the emitted byte count. -fn eval_fpassthru_result( - id: i64, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_user_wrapper_fpassthru_result(id, context, values)? { - return Ok(result); - } - let Some(bytes) = context.stream_resources_mut().get_contents(id, None, None) else { - return values.bool_value(false); - }; - let len = i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?; - let output = values.string_bytes_value(&bytes)?; - values.echo(output)?; - values.int(len) -} - -/// Evaluates PHP `fread($stream, $length)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fread( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, length] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let length = eval_expr(length, context, scope, values)?; - eval_fread_result(stream, length, context, values) -} - -/// Reads bytes from a materialized stream resource. -pub(in crate::interpreter) fn eval_fread_result( - stream: RuntimeCellHandle, - length: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let length = eval_nonnegative_usize(length, values)?; - if let Some(result) = eval_user_wrapper_fread_result(id, length, context, values)? { - return Ok(result); - } - match context.stream_resources_mut().read(id, length) { - Some(bytes) => values.string_bytes_value(&bytes), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `fwrite($stream, $data)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fwrite( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, data] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let data = eval_expr(data, context, scope, values)?; - eval_fwrite_result(stream, data, context, values) -} - -/// Writes bytes to a materialized stream resource. -pub(in crate::interpreter) fn eval_fwrite_result( - stream: RuntimeCellHandle, - data: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let data = values.string_bytes(data)?; - if let Some(result) = eval_user_wrapper_fwrite_result(id, &data, context, values)? { - return Ok(result); - } - match context.stream_resources_mut().write(id, &data) { - Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `fseek($stream, $offset, $whence = SEEK_SET)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fseek( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=3).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let offset = eval_expr(&args[1], context, scope, values)?; - let whence = match args.get(2) { - Some(whence) => Some(eval_expr(whence, context, scope, values)?), - None => None, - }; - eval_fseek_result(stream, offset, whence, context, values) -} - -/// Seeks a materialized stream and returns PHP's 0 or -1 status code. -pub(in crate::interpreter) fn eval_fseek_result( - stream: RuntimeCellHandle, - offset: RuntimeCellHandle, - whence: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let offset = eval_int_value(offset, values)?; - let whence = match whence { - Some(whence) => eval_int_value(whence, values)?, - None => 0, - }; - if let Some(seek_ok) = eval_user_wrapper_fseek_result(id, offset, whence, context, values)? { - return values.int(if seek_ok { 0 } else { -1 }); - } - let status = if context.stream_resources_mut().seek(id, offset, whence) { - 0 - } else { - -1 - }; - values.int(status) -} - -/// Evaluates PHP `ftruncate($stream, $size)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_ftruncate( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, size] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let size = eval_expr(size, context, scope, values)?; - eval_ftruncate_result(stream, size, context, values) -} - -/// Truncates a materialized stream resource. -pub(in crate::interpreter) fn eval_ftruncate_result( - stream: RuntimeCellHandle, - size: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let size = eval_int_value(size, values)?; - let Ok(size) = u64::try_from(size) else { - return values.bool_value(false); - }; - if let Some(result) = eval_user_wrapper_ftruncate_result(id, size, context, values)? { - return Ok(result); - } - values.bool_value(context.stream_resources_mut().truncate(id, size)) -} - -/// Evaluates PHP `stream_get_contents($stream, $length = null, $offset = -1)`. -pub(in crate::interpreter) fn eval_builtin_stream_get_contents( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(1..=3).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let length = match args.get(1) { - Some(length) => Some(eval_expr(length, context, scope, values)?), - None => None, - }; - let offset = match args.get(2) { - Some(offset) => Some(eval_expr(offset, context, scope, values)?), - None => None, - }; - eval_stream_get_contents_result(stream, length, offset, context, values) -} - -/// Reads the remaining or bounded contents from a materialized stream resource. -pub(in crate::interpreter) fn eval_stream_get_contents_result( - stream: RuntimeCellHandle, - length: Option, - offset: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let length = eval_optional_stream_length(length, values)?; - let offset = eval_optional_stream_offset(offset, values)?; - if let Some(result) = - eval_user_wrapper_stream_get_contents_result(id, length, offset, context, values)? - { - return Ok(result); - } - match context - .stream_resources_mut() - .get_contents(id, length, offset) - { - Some(bytes) => values.string_bytes_value(&bytes), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `stream_copy_to_stream($from, $to, $length = null, $offset = -1)`. -pub(in crate::interpreter) fn eval_builtin_stream_copy_to_stream( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let from = eval_expr(&args[0], context, scope, values)?; - let to = eval_expr(&args[1], context, scope, values)?; - let length = match args.get(2) { - Some(length) => Some(eval_expr(length, context, scope, values)?), - None => None, - }; - let offset = match args.get(3) { - Some(offset) => Some(eval_expr(offset, context, scope, values)?), - None => None, - }; - eval_stream_copy_to_stream_result(from, to, length, offset, context, values) -} - -/// Evaluates PHP `stream_get_line($stream, $length, $ending = null)`. -pub(in crate::interpreter) fn eval_builtin_stream_get_line( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=3).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let length = eval_expr(&args[1], context, scope, values)?; - let ending = match args.get(2) { - Some(ending) => Some(eval_expr(ending, context, scope, values)?), - None => None, - }; - eval_stream_get_line_result(stream, length, ending, context, values) -} - -/// Reads one line-like byte sequence from a materialized stream resource. -pub(in crate::interpreter) fn eval_stream_get_line_result( - stream: RuntimeCellHandle, - length: RuntimeCellHandle, - ending: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let length = eval_nonnegative_usize(length, values)?; - let ending = match ending { - Some(ending) if values.type_tag(ending)? != EVAL_TAG_NULL => { - Some(values.string_bytes(ending)?) - } - _ => None, - }; - if let Some(result) = - eval_user_wrapper_stream_get_line_result(id, length, ending.as_deref(), context, values)? - { - return Ok(result); - } - match context - .stream_resources_mut() - .read_line(id, length, ending.as_deref(), false, false) - { - Some(bytes) if !bytes.is_empty() => values.string_bytes_value(&bytes), - Some(_) => values.bool_value(false), - None => values.bool_value(false), - } -} - -/// Copies bytes between two materialized stream resources. -pub(in crate::interpreter) fn eval_stream_copy_to_stream_result( - from: RuntimeCellHandle, - to: RuntimeCellHandle, - length: Option, - offset: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let from = eval_stream_resource_id(from, values)?; - let to = eval_stream_resource_id(to, values)?; - let length = eval_optional_stream_length(length, values)?; - let offset = eval_optional_stream_offset(offset, values)?; - if let Some(result) = - eval_user_wrapper_stream_copy_to_stream_result(from, to, length, offset, context, values)? - { - return Ok(result); - } - match context - .stream_resources_mut() - .copy_to_stream(from, to, length, offset) - { - Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), - None => values.bool_value(false), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/metadata.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/metadata.rs deleted file mode 100644 index fa8a3ed13c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/metadata.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Purpose: -//! Builds PHP metadata arrays for eval-local stream resources. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::streams::eval_unary_stream_result`. -//! -//! Key details: -//! - Metadata mirrors eval's local stream table, not host PHP stream wrappers. - -use super::*; - -/// Builds PHP's stream metadata array for one eval-local stream resource. -pub(in crate::interpreter) fn eval_stream_get_meta_data_result( - id: i64, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(meta) = context.stream_resources().meta_data(id) else { - return values.bool_value(false); - }; - let mut result = values.assoc_new(9)?; - result = eval_stream_meta_set_bool(result, "timed_out", false, values)?; - result = eval_stream_meta_set_bool(result, "blocked", true, values)?; - result = eval_stream_meta_set_bool(result, "eof", meta.eof, values)?; - result = eval_stream_meta_set_string(result, "wrapper_type", "plainfile", values)?; - result = eval_stream_meta_set_string(result, "stream_type", "STDIO", values)?; - result = eval_stream_meta_set_string(result, "mode", &meta.mode, values)?; - result = eval_stream_meta_set_int(result, "unread_bytes", 0, values)?; - result = eval_stream_meta_set_bool(result, "seekable", true, values)?; - eval_stream_meta_set_string(result, "uri", &meta.uri, values) -} - -/// Inserts a boolean field into the stream metadata array. -fn eval_stream_meta_set_bool( - array: RuntimeCellHandle, - key: &str, - value: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.bool_value(value)?; - values.array_set(array, key, value) -} - -/// Inserts an integer field into the stream metadata array. -fn eval_stream_meta_set_int( - array: RuntimeCellHandle, - key: &str, - value: i64, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.int(value)?; - values.array_set(array, key, value) -} - -/// Inserts a string field into the stream metadata array. -fn eval_stream_meta_set_string( - array: RuntimeCellHandle, - key: &str, - value: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(key)?; - let value = values.string(value)?; - values.array_set(array, key, value) -} From f79879fa03ff61691aa6a7c13d14b72c3be88378 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:42:09 +0200 Subject: [PATCH 1121/1208] refactor(magician): move stream csv and flock builtins home --- .../builtins/filesystem/direct_dispatch.rs | 5 - .../builtins/filesystem/fgetcsv.rs | 110 ++++++- .../interpreter/builtins/filesystem/flock.rs | 133 +++++++- .../builtins/filesystem/fprintf.rs | 47 ++- .../builtins/filesystem/fputcsv.rs | 95 +++++- .../interpreter/builtins/filesystem/fscanf.rs | 44 ++- .../interpreter/builtins/filesystem/mod.rs | 1 + .../filesystem/stream_values_dispatch.rs | 59 ---- .../builtins/filesystem/streams.rs | 97 +++++- .../builtins/filesystem/streams/common.rs | 96 ------ .../builtins/filesystem/streams/csv_format.rs | 299 ------------------ .../builtins/filesystem/streams/flock.rs | 122 ------- .../builtins/filesystem/vfprintf.rs | 35 +- 13 files changed, 538 insertions(+), 605 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/common.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/streams/flock.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index b3e0137922..c8b99d9742 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -160,11 +160,7 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( ) -> Result { match name { "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), - "fgetcsv" => eval_builtin_fgetcsv(args, context, scope, values), "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), - "fprintf" => eval_builtin_fprintf(args, context, scope, values), - "fputcsv" => eval_builtin_fputcsv(args, context, scope, values), - "fscanf" => eval_builtin_fscanf(args, context, scope, values), "readline" => eval_builtin_readline(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), @@ -224,7 +220,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_stream_wrapper_registry(name, args, context, scope, values) } "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), - "vfprintf" => eval_builtin_vfprintf(args, context, scope, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs index 871d9bde35..2ccb5221b9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs @@ -22,6 +22,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fgetcsv` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fgetcsv_declared_call( @@ -30,7 +31,7 @@ pub(in crate::interpreter) fn eval_fgetcsv_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fgetcsv", args, context, scope, values) + eval_builtin_fgetcsv(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fgetcsv` filesystem builtin through the area dispatcher. @@ -39,5 +40,110 @@ pub(in crate::interpreter) fn eval_fgetcsv_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fgetcsv", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_fgetcsv_result(*stream, None, None, context, values), + [stream, length] => eval_fgetcsv_result(*stream, Some(*length), None, context, values), + [stream, length, separator] => eval_fgetcsv_result(*stream, Some(*length), Some(*separator), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fgetcsv($stream, $length = null, $separator = ",")`. +pub(in crate::interpreter) fn eval_builtin_fgetcsv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = match args.get(1) { + Some(length) => Some(eval_expr(length, context, scope, values)?), + None => None, + }; + let separator = match args.get(2) { + Some(separator) => Some(eval_expr(separator, context, scope, values)?), + None => None, + }; + eval_fgetcsv_result(stream, length, separator, context, values) +} + +/// Reads and parses one CSV record from a materialized stream resource. +pub(in crate::interpreter) fn eval_fgetcsv_result( + stream: RuntimeCellHandle, + length: Option, + separator: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let length = eval_optional_stream_length(length, values)?.unwrap_or(usize::MAX); + let separator = eval_optional_delimiter(separator, b',', values)?; + let Some(mut line) = context + .stream_resources_mut() + .read_line(id, length, None, true, true) + else { + return values.bool_value(false); + }; + if line.is_empty() { + return values.bool_value(false); + } + eval_trim_csv_line_end(&mut line); + let fields = eval_parse_csv_record(&line, separator, b'"'); + eval_csv_fields_array(&fields, values) +} +/// Removes CR/LF line terminators from a CSV record buffer. +fn eval_trim_csv_line_end(line: &mut Vec) { + if line.ends_with(b"\n") { + line.pop(); + } + if line.ends_with(b"\r") { + line.pop(); + } +} + +/// Parses one CSV record using PHP-style doubled-enclosure escaping. +fn eval_parse_csv_record(line: &[u8], separator: u8, enclosure: u8) -> Vec> { + let mut fields = Vec::new(); + let mut field = Vec::new(); + let mut quoted = false; + let mut index = 0; + while index < line.len() { + let byte = line[index]; + if quoted { + if byte == enclosure { + if line.get(index + 1).copied() == Some(enclosure) { + field.push(enclosure); + index += 2; + continue; + } + quoted = false; + } else { + field.push(byte); + } + } else if byte == enclosure && field.is_empty() { + quoted = true; + } else if byte == separator { + fields.push(std::mem::take(&mut field)); + } else { + field.push(byte); + } + index += 1; + } + fields.push(field); + fields +} + +/// Builds a PHP indexed array from parsed CSV field bytes. +fn eval_csv_fields_array( + fields: &[Vec], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(fields.len())?; + for (index, field) in fields.iter().enumerate() { + result = super::scandir::eval_array_set_indexed_bytes(result, index, field, values)?; + } + Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs index 321623f02d..586be58025 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs @@ -19,6 +19,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `flock` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_flock_declared_call( @@ -27,7 +28,17 @@ pub(in crate::interpreter) fn eval_flock_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("flock", args, context, scope, values) + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let operation = eval_expr(&args[1], context, scope, values)?; + if args.len() >= 3 { + eval_expr(&args[2], context, scope, values)?; + values.warning("flock(): Argument #3 ($would_block) must be passed by reference, value given")?; + } + let (success, _) = eval_flock_result(stream, operation, context, values)?; + values.bool_value(success) } /// Dispatches evaluated-argument calls for the `flock` filesystem builtin through the area dispatcher. @@ -36,5 +47,123 @@ pub(in crate::interpreter) fn eval_flock_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("flock", evaluated_args, context, values) + if !(2..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + if evaluated_args.len() >= 3 { + values.warning("flock(): Argument #3 ($would_block) must be passed by reference, value given")?; + } + let (success, _) = eval_flock_result(evaluated_args[0], evaluated_args[1], context, values)?; + values.bool_value(success) +} + +/// Evaluates PHP `flock($stream, $operation, &$would_block = null)` over eval call args. +pub(in crate::interpreter) fn eval_builtin_flock( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (stream, operation, would_block_target) = + eval_flock_direct_args(args, context, scope, values)?; + let (success, would_block) = eval_flock_result(stream, operation, context, values)?; + if let Some(target) = would_block_target { + let value = values.bool_value(would_block)?; + eval_write_direct_ref_target( + &target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + values.bool_value(success) +} + +/// Applies a materialized PHP `flock()` operation to a local eval stream resource. +pub(in crate::interpreter) fn eval_flock_result( + stream: RuntimeCellHandle, + operation: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(bool, bool), EvalStatus> { + let id = eval_stream_resource_id(stream, values)?; + let operation = eval_int_value(operation, values)?; + if let Some(success) = eval_user_wrapper_flock_result(id, operation, context, values)? { + return Ok((success, false)); + } + Ok(context + .stream_resources() + .flock(id, operation) + .unwrap_or((false, false))) +} + +/// Evaluates and binds direct `flock()` arguments while keeping by-ref output writable. +fn eval_flock_direct_args( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result< + ( + RuntimeCellHandle, + RuntimeCellHandle, + Option, + ), + EvalStatus, +> { + let mut stream = None; + let mut operation = None; + let mut would_block = None; + let mut positional_index = 0; + let mut saw_named = false; + + for arg in args { + if arg.is_spread() { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = if let Some(name) = arg.name() { + saw_named = true; + name + } else { + if saw_named { + return Err(EvalStatus::RuntimeFatal); + } + let parameter = match positional_index { + 0 => "stream", + 1 => "operation", + 2 => "would_block", + _ => return Err(EvalStatus::RuntimeFatal), + }; + positional_index += 1; + parameter + }; + + match parameter { + "stream" => { + if stream.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + stream = Some(eval_expr(arg.value(), context, scope, values)?); + } + "operation" => { + if operation.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + operation = Some(eval_expr(arg.value(), context, scope, values)?); + } + "would_block" => { + if would_block.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let (_, target) = eval_call_arg_value(arg.value(), context, scope, values)?; + would_block = Some(target.ok_or(EvalStatus::RuntimeFatal)?); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + + let stream = stream.ok_or(EvalStatus::RuntimeFatal)?; + let operation = operation.ok_or(EvalStatus::RuntimeFatal)?; + Ok((stream, operation, would_block)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs index d807d3e52b..e9d4de11ed 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs @@ -17,6 +17,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fprintf` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fprintf_declared_call( @@ -25,7 +26,7 @@ pub(in crate::interpreter) fn eval_fprintf_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fprintf", args, context, scope, values) + eval_builtin_fprintf(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fprintf` filesystem builtin through the area dispatcher. @@ -34,5 +35,47 @@ pub(in crate::interpreter) fn eval_fprintf_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fprintf", evaluated_args, context, values) + let Some((stream, rest)) = evaluated_args.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + let Some((format, format_args)) = rest.split_first() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_fprintf_result(*stream, *format, format_args, context, values) +} + +/// Evaluates PHP `fprintf($stream, $format, ...$values)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + let mut format_args = Vec::with_capacity(args.len().saturating_sub(2)); + for arg in &args[2..] { + format_args.push(eval_expr(arg, context, scope, values)?); + } + eval_fprintf_result(stream, format, &format_args, context, values) +} + +/// Formats and writes `fprintf()` arguments to a materialized stream resource. +pub(in crate::interpreter) fn eval_fprintf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + format_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let format = values.string_bytes(format)?; + let output = eval_sprintf_bytes(&format, format_args, values)?; + match context.stream_resources_mut().write(id, &output) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs index 8f9808e0cb..bc85a2623e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs @@ -23,6 +23,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fputcsv` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fputcsv_declared_call( @@ -31,7 +32,7 @@ pub(in crate::interpreter) fn eval_fputcsv_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fputcsv", args, context, scope, values) + eval_builtin_fputcsv(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fputcsv` filesystem builtin through the area dispatcher. @@ -40,5 +41,95 @@ pub(in crate::interpreter) fn eval_fputcsv_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fputcsv", evaluated_args, context, values) + match evaluated_args { + [stream, fields] => eval_fputcsv_result(*stream, *fields, None, None, context, values), + [stream, fields, separator] => eval_fputcsv_result(*stream, *fields, Some(*separator), None, context, values), + [stream, fields, separator, enclosure] => eval_fputcsv_result(*stream, *fields, Some(*separator), Some(*enclosure), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `fputcsv($stream, $fields, $separator = ",", $enclosure = "\"")`. +pub(in crate::interpreter) fn eval_builtin_fputcsv( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let fields = eval_expr(&args[1], context, scope, values)?; + let separator = match args.get(2) { + Some(separator) => Some(eval_expr(separator, context, scope, values)?), + None => None, + }; + let enclosure = match args.get(3) { + Some(enclosure) => Some(eval_expr(enclosure, context, scope, values)?), + None => None, + }; + eval_fputcsv_result(stream, fields, separator, enclosure, context, values) +} + +/// Formats and writes one CSV record to a materialized stream resource. +pub(in crate::interpreter) fn eval_fputcsv_result( + stream: RuntimeCellHandle, + fields: RuntimeCellHandle, + separator: Option, + enclosure: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let separator = eval_optional_delimiter(separator, b',', values)?; + let enclosure = eval_optional_delimiter(enclosure, b'"', values)?; + let output = eval_format_csv_record(fields, separator, enclosure, values)?; + match context.stream_resources_mut().write(id, &output) { + Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), + None => values.bool_value(false), + } +} +/// Formats one PHP array-like value as a CSV record ending in LF. +fn eval_format_csv_record( + fields: RuntimeCellHandle, + separator: u8, + enclosure: u8, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !values.is_array_like(fields)? { + return Err(EvalStatus::RuntimeFatal); + } + let len = values.array_len(fields)?; + let mut output = Vec::new(); + for position in 0..len { + if position > 0 { + output.push(separator); + } + let key = values.array_iter_key(fields, position)?; + let value = values.array_get(fields, key)?; + let bytes = values.string_bytes(value)?; + eval_append_csv_field(&mut output, &bytes, separator, enclosure); + } + output.push(b'\n'); + Ok(output) +} + +/// Appends one CSV field, quoting and escaping only when required. +fn eval_append_csv_field(output: &mut Vec, field: &[u8], separator: u8, enclosure: u8) { + let needs_quotes = field + .iter() + .any(|byte| matches!(*byte, b'\n' | b'\r') || *byte == separator || *byte == enclosure); + if !needs_quotes { + output.extend_from_slice(field); + return; + } + output.push(enclosure); + for byte in field { + if *byte == enclosure { + output.push(enclosure); + } + output.push(*byte); + } + output.push(enclosure); } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs index 18ccbccf29..71bcee6310 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs @@ -17,6 +17,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `fscanf` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_fscanf_declared_call( @@ -25,7 +26,7 @@ pub(in crate::interpreter) fn eval_fscanf_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fscanf", args, context, scope, values) + eval_builtin_fscanf(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `fscanf` filesystem builtin through the area dispatcher. @@ -34,5 +35,44 @@ pub(in crate::interpreter) fn eval_fscanf_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fscanf", evaluated_args, context, values) + if evaluated_args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + eval_fscanf_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Evaluates PHP `fscanf($stream, $format, ...$vars)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_fscanf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.len() < 2 { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let format = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_fscanf_result(stream, format, context, values) +} + +/// Reads one line from a stream and scans it with the eval `sscanf()` subset. +pub(in crate::interpreter) fn eval_fscanf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let Some(line) = context + .stream_resources_mut() + .read_line(id, usize::MAX, None, true, true) + else { + return values.bool_value(false); + }; + let input = values.string_bytes_value(&line)?; + eval_sscanf_result(input, format, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index ccbe281e11..e1e5e72a9c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -176,3 +176,4 @@ pub(in crate::interpreter) use user_wrapper_stat::*; pub(in crate::interpreter) use user_wrapper_streams::*; pub(in crate::interpreter) use direct_dispatch::eval_builtin_filesystem_call; pub(in crate::interpreter) use values_dispatch::eval_filesystem_values_result; +pub(in crate::interpreter) use flock::{eval_builtin_flock, eval_flock_result}; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs index 1908751f4b..6660ad4417 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs @@ -19,59 +19,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { let result = match name { - "fgetcsv" => match evaluated_args { - [stream] => eval_fgetcsv_result(*stream, None, None, context, values)?, - [stream, length] => { - eval_fgetcsv_result(*stream, Some(*length), None, context, values)? - } - [stream, length, separator] => { - eval_fgetcsv_result(*stream, Some(*length), Some(*separator), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "flock" => { - if !(2..=3).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - if evaluated_args.len() >= 3 { - values.warning( - "flock(): Argument #3 ($would_block) must be passed by reference, value given", - )?; - } - let (success, _) = - eval_flock_result(evaluated_args[0], evaluated_args[1], context, values)?; - values.bool_value(success)? - } - "fprintf" => { - let Some((stream, rest)) = evaluated_args.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - let Some((format, format_args)) = rest.split_first() else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_fprintf_result(*stream, *format, format_args, context, values)? - } - "fputcsv" => match evaluated_args { - [stream, fields] => eval_fputcsv_result(*stream, *fields, None, None, context, values)?, - [stream, fields, separator] => { - eval_fputcsv_result(*stream, *fields, Some(*separator), None, context, values)? - } - [stream, fields, separator, enclosure] => eval_fputcsv_result( - *stream, - *fields, - Some(*separator), - Some(*enclosure), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "fscanf" => { - if evaluated_args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - eval_fscanf_result(evaluated_args[0], evaluated_args[1], context, values)? - } "fsockopen" | "pfsockopen" => { if !(2..=5).contains(&evaluated_args.len()) { return Err(EvalStatus::RuntimeFatal); @@ -296,12 +243,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { eval_stream_wrapper_registry_result(name, evaluated_args, context, values)? } - "vfprintf" => { - let [stream, format, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_vfprintf_result(*stream, *format, *array, context, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs index fa5ca535a2..c62e7149ff 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams.rs @@ -1,21 +1,94 @@ //! Purpose: -//! Groups shared helpers for eval-local filesystem stream builtins. +//! Shared stream argument coercions for eval filesystem stream builtins. //! //! Called from: -//! - Leaf filesystem stream builtin files that need stream-resource coercions, -//! CSV formatting, or direct `flock()` argument binding. +//! - Leaf filesystem stream builtin files that need stream-resource coercions. //! //! Key details: -//! - Builtin implementations live in their owning leaf files; this module only -//! re-exports shared stream helper modules. +//! - Runtime resource payloads are zero-based while PHP-visible ids are +//! one-based resource handles. use super::super::super::*; -use super::*; +/// Converts a runtime resource cell into eval's zero-based stream id. +pub(in crate::interpreter) fn eval_stream_resource_id( + stream: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} -mod common; -mod csv_format; -mod flock; +/// Converts a stream length argument into a non-negative `usize`. +pub(in crate::interpreter) fn eval_nonnegative_usize( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = eval_int_value(value, values)?; + usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) +} -pub(in crate::interpreter) use common::*; -pub(in crate::interpreter) use csv_format::*; -pub(in crate::interpreter) use flock::*; +/// Converts an optional stream length where null and -1 mean "read all". +pub(in crate::interpreter) fn eval_optional_stream_length( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = value else { + return Ok(None); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(None); + } + let value = eval_int_value(value, values)?; + if value == -1 { + return Ok(None); + } + Ok(Some( + usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal)?, + )) +} + +/// Converts an optional absolute stream offset where null and -1 mean no seek. +pub(in crate::interpreter) fn eval_optional_stream_offset( + value: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(value) = value else { + return Ok(None); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(None); + } + let value = eval_int_value(value, values)?; + if value < 0 { + Ok(None) + } else { + Ok(Some(value)) + } +} + +/// Converts one runtime cell to a UTF-8 string for stream mode arguments. +pub(in crate::interpreter) fn eval_stream_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + Ok(String::from_utf8_lossy(&bytes).into_owned()) +} + +/// Converts an optional one-byte delimiter argument to a byte value. +pub(in crate::interpreter) fn eval_optional_delimiter( + value: Option, + default: u8, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(value) = value else { + return Ok(default); + }; + if values.type_tag(value)? == EVAL_TAG_NULL { + return Ok(default); + } + Ok(values.string_bytes(value)?.first().copied().unwrap_or(default)) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/common.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/common.rs deleted file mode 100644 index c863450408..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/common.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Purpose: -//! Shared stream argument coercions for eval filesystem stream builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::streams`. -//! - Stream CSV/formatting helper modules. -//! -//! Key details: -//! - Runtime resource payloads are zero-based while PHP-visible ids are -//! one-based resource handles. - -use super::*; - -/// Converts a runtime resource cell into eval's zero-based stream id. -pub(in crate::interpreter) fn eval_stream_resource_id( - stream: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(stream)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(stream, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} - -/// Converts a stream length argument into a non-negative `usize`. -pub(in crate::interpreter) fn eval_nonnegative_usize( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = eval_int_value(value, values)?; - usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Converts an optional stream length where null and -1 mean "read all". -pub(in crate::interpreter) fn eval_optional_stream_length( - value: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(value) = value else { - return Ok(None); - }; - if values.type_tag(value)? == EVAL_TAG_NULL { - return Ok(None); - } - let value = eval_int_value(value, values)?; - if value == -1 { - return Ok(None); - } - Ok(Some( - usize::try_from(value).map_err(|_| EvalStatus::RuntimeFatal)?, - )) -} - -/// Converts an optional absolute stream offset where null and -1 mean no seek. -pub(in crate::interpreter) fn eval_optional_stream_offset( - value: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(value) = value else { - return Ok(None); - }; - if values.type_tag(value)? == EVAL_TAG_NULL { - return Ok(None); - } - let value = eval_int_value(value, values)?; - if value < 0 { - Ok(None) - } else { - Ok(Some(value)) - } -} - -/// Converts one runtime cell to a UTF-8 string for stream mode arguments. -pub(in crate::interpreter) fn eval_stream_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - Ok(String::from_utf8_lossy(&bytes).into_owned()) -} - -/// Converts an optional one-byte delimiter argument to a byte value. -pub(in crate::interpreter) fn eval_optional_delimiter( - value: Option, - default: u8, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(value) = value else { - return Ok(default); - }; - if values.type_tag(value)? == EVAL_TAG_NULL { - return Ok(default); - } - Ok(values.string_bytes(value)?.first().copied().unwrap_or(default)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs deleted file mode 100644 index 7182103536..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/csv_format.rs +++ /dev/null @@ -1,299 +0,0 @@ -//! Purpose: -//! CSV and printf-family stream builtins for eval-local file resources. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::streams` re-exports. -//! - Filesystem stream declaration dispatchers. -//! -//! Key details: -//! - CSV parsing implements the small PHP-compatible subset used by eval tests. -//! - Formatting delegates to the shared `sprintf` byte formatter. - -use super::*; - -/// Evaluates PHP `fgetcsv($stream, $length = null, $separator = ",")`. -pub(in crate::interpreter) fn eval_builtin_fgetcsv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(1..=3).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let length = match args.get(1) { - Some(length) => Some(eval_expr(length, context, scope, values)?), - None => None, - }; - let separator = match args.get(2) { - Some(separator) => Some(eval_expr(separator, context, scope, values)?), - None => None, - }; - eval_fgetcsv_result(stream, length, separator, context, values) -} - -/// Reads and parses one CSV record from a materialized stream resource. -pub(in crate::interpreter) fn eval_fgetcsv_result( - stream: RuntimeCellHandle, - length: Option, - separator: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let length = eval_optional_stream_length(length, values)?.unwrap_or(usize::MAX); - let separator = eval_optional_delimiter(separator, b',', values)?; - let Some(mut line) = context - .stream_resources_mut() - .read_line(id, length, None, true, true) - else { - return values.bool_value(false); - }; - if line.is_empty() { - return values.bool_value(false); - } - eval_trim_csv_line_end(&mut line); - let fields = eval_parse_csv_record(&line, separator, b'"'); - eval_csv_fields_array(&fields, values) -} - -/// Evaluates PHP `fputcsv($stream, $fields, $separator = ",", $enclosure = "\"")`. -pub(in crate::interpreter) fn eval_builtin_fputcsv( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let fields = eval_expr(&args[1], context, scope, values)?; - let separator = match args.get(2) { - Some(separator) => Some(eval_expr(separator, context, scope, values)?), - None => None, - }; - let enclosure = match args.get(3) { - Some(enclosure) => Some(eval_expr(enclosure, context, scope, values)?), - None => None, - }; - eval_fputcsv_result(stream, fields, separator, enclosure, context, values) -} - -/// Formats and writes one CSV record to a materialized stream resource. -pub(in crate::interpreter) fn eval_fputcsv_result( - stream: RuntimeCellHandle, - fields: RuntimeCellHandle, - separator: Option, - enclosure: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let separator = eval_optional_delimiter(separator, b',', values)?; - let enclosure = eval_optional_delimiter(enclosure, b'"', values)?; - let output = eval_format_csv_record(fields, separator, enclosure, values)?; - match context.stream_resources_mut().write(id, &output) { - Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `fprintf($stream, $format, ...$values)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fprintf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let format = eval_expr(&args[1], context, scope, values)?; - let mut format_args = Vec::with_capacity(args.len().saturating_sub(2)); - for arg in &args[2..] { - format_args.push(eval_expr(arg, context, scope, values)?); - } - eval_fprintf_result(stream, format, &format_args, context, values) -} - -/// Evaluates PHP `fscanf($stream, $format, ...$vars)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_fscanf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() < 2 { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let format = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - eval_expr(arg, context, scope, values)?; - } - eval_fscanf_result(stream, format, context, values) -} - -/// Reads one line from a stream and scans it with the eval `sscanf()` subset. -pub(in crate::interpreter) fn eval_fscanf_result( - stream: RuntimeCellHandle, - format: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let Some(line) = context - .stream_resources_mut() - .read_line(id, usize::MAX, None, true, true) - else { - return values.bool_value(false); - }; - let input = values.string_bytes_value(&line)?; - eval_sscanf_result(input, format, values) -} - -/// Formats and writes `fprintf()` arguments to a materialized stream resource. -pub(in crate::interpreter) fn eval_fprintf_result( - stream: RuntimeCellHandle, - format: RuntimeCellHandle, - format_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_resource_id(stream, values)?; - let format = values.string_bytes(format)?; - let output = eval_sprintf_bytes(&format, format_args, values)?; - match context.stream_resources_mut().write(id, &output) { - Some(written) => values.int(i64::try_from(written).map_err(|_| EvalStatus::RuntimeFatal)?), - None => values.bool_value(false), - } -} - -/// Evaluates PHP `vfprintf($stream, $format, $values)` over eval expressions. -pub(in crate::interpreter) fn eval_builtin_vfprintf( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, format, array] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let format = eval_expr(format, context, scope, values)?; - let array = eval_expr(array, context, scope, values)?; - eval_vfprintf_result(stream, format, array, context, values) -} - -/// Formats and writes `vfprintf()` array arguments to a materialized stream resource. -pub(in crate::interpreter) fn eval_vfprintf_result( - stream: RuntimeCellHandle, - format: RuntimeCellHandle, - array: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let format_args = eval_sprintf_argument_array_values(array, values)?; - eval_fprintf_result(stream, format, &format_args, context, values) -} - -/// Removes CR/LF line terminators from a CSV record buffer. -fn eval_trim_csv_line_end(line: &mut Vec) { - if line.ends_with(b"\n") { - line.pop(); - } - if line.ends_with(b"\r") { - line.pop(); - } -} - -/// Parses one CSV record using PHP-style doubled-enclosure escaping. -fn eval_parse_csv_record(line: &[u8], separator: u8, enclosure: u8) -> Vec> { - let mut fields = Vec::new(); - let mut field = Vec::new(); - let mut quoted = false; - let mut index = 0; - while index < line.len() { - let byte = line[index]; - if quoted { - if byte == enclosure { - if line.get(index + 1).copied() == Some(enclosure) { - field.push(enclosure); - index += 2; - continue; - } - quoted = false; - } else { - field.push(byte); - } - } else if byte == enclosure && field.is_empty() { - quoted = true; - } else if byte == separator { - fields.push(std::mem::take(&mut field)); - } else { - field.push(byte); - } - index += 1; - } - fields.push(field); - fields -} - -/// Builds a PHP indexed array from parsed CSV field bytes. -fn eval_csv_fields_array( - fields: &[Vec], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(fields.len())?; - for (index, field) in fields.iter().enumerate() { - result = super::super::scandir::eval_array_set_indexed_bytes(result, index, field, values)?; - } - Ok(result) -} - -/// Formats one PHP array-like value as a CSV record ending in LF. -fn eval_format_csv_record( - fields: RuntimeCellHandle, - separator: u8, - enclosure: u8, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !values.is_array_like(fields)? { - return Err(EvalStatus::RuntimeFatal); - } - let len = values.array_len(fields)?; - let mut output = Vec::new(); - for position in 0..len { - if position > 0 { - output.push(separator); - } - let key = values.array_iter_key(fields, position)?; - let value = values.array_get(fields, key)?; - let bytes = values.string_bytes(value)?; - eval_append_csv_field(&mut output, &bytes, separator, enclosure); - } - output.push(b'\n'); - Ok(output) -} - -/// Appends one CSV field, quoting and escaping only when required. -fn eval_append_csv_field(output: &mut Vec, field: &[u8], separator: u8, enclosure: u8) { - let needs_quotes = field - .iter() - .any(|byte| matches!(*byte, b'\n' | b'\r') || *byte == separator || *byte == enclosure); - if !needs_quotes { - output.extend_from_slice(field); - return; - } - output.push(enclosure); - for byte in field { - if *byte == enclosure { - output.push(enclosure); - } - output.push(*byte); - } - output.push(enclosure); -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/flock.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/flock.rs deleted file mode 100644 index e517e81019..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/streams/flock.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Purpose: -//! Source-sensitive `flock` handling for eval stream resources. -//! -//! Called from: -//! - `crate::interpreter::eval_call` before generic builtin dispatch. -//! - Filesystem stream declaration dispatchers for by-value callable calls. -//! -//! Key details: -//! - Direct calls keep the optional `$would_block` output parameter writable. - -use super::*; - -/// Evaluates PHP `flock($stream, $operation, &$would_block = null)` over eval call args. -pub(in crate::interpreter) fn eval_builtin_flock( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (stream, operation, would_block_target) = - eval_flock_direct_args(args, context, scope, values)?; - let (success, would_block) = eval_flock_result(stream, operation, context, values)?; - if let Some(target) = would_block_target { - let value = values.bool_value(would_block)?; - eval_write_direct_ref_target( - &target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - values.bool_value(success) -} - -/// Applies a materialized PHP `flock()` operation to a local eval stream resource. -pub(in crate::interpreter) fn eval_flock_result( - stream: RuntimeCellHandle, - operation: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(bool, bool), EvalStatus> { - let id = eval_stream_resource_id(stream, values)?; - let operation = eval_int_value(operation, values)?; - if let Some(success) = eval_user_wrapper_flock_result(id, operation, context, values)? { - return Ok((success, false)); - } - Ok(context - .stream_resources() - .flock(id, operation) - .unwrap_or((false, false))) -} - -/// Evaluates and binds direct `flock()` arguments while keeping by-ref output writable. -fn eval_flock_direct_args( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result< - ( - RuntimeCellHandle, - RuntimeCellHandle, - Option, - ), - EvalStatus, -> { - let mut stream = None; - let mut operation = None; - let mut would_block = None; - let mut positional_index = 0; - let mut saw_named = false; - - for arg in args { - if arg.is_spread() { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = if let Some(name) = arg.name() { - saw_named = true; - name - } else { - if saw_named { - return Err(EvalStatus::RuntimeFatal); - } - let parameter = match positional_index { - 0 => "stream", - 1 => "operation", - 2 => "would_block", - _ => return Err(EvalStatus::RuntimeFatal), - }; - positional_index += 1; - parameter - }; - - match parameter { - "stream" => { - if stream.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - stream = Some(eval_expr(arg.value(), context, scope, values)?); - } - "operation" => { - if operation.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - operation = Some(eval_expr(arg.value(), context, scope, values)?); - } - "would_block" => { - if would_block.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let (_, target) = eval_call_arg_value(arg.value(), context, scope, values)?; - would_block = Some(target.ok_or(EvalStatus::RuntimeFatal)?); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - - let stream = stream.ok_or(EvalStatus::RuntimeFatal)?; - let operation = operation.ok_or(EvalStatus::RuntimeFatal)?; - Ok((stream, operation, would_block)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs index 7d9d85a8cc..2caf6ad3ba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_vfprintf_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("vfprintf", args, context, scope, values) + eval_builtin_vfprintf(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `vfprintf` filesystem builtin through the area dispatcher. @@ -33,5 +33,36 @@ pub(in crate::interpreter) fn eval_vfprintf_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("vfprintf", evaluated_args, context, values) + match evaluated_args { + [stream, format, array] => eval_vfprintf_result(*stream, *format, *array, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP `vfprintf($stream, $format, $values)` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_vfprintf( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, format, array] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let format = eval_expr(format, context, scope, values)?; + let array = eval_expr(array, context, scope, values)?; + eval_vfprintf_result(stream, format, array, context, values) +} + +/// Formats and writes `vfprintf()` array arguments to a materialized stream resource. +pub(in crate::interpreter) fn eval_vfprintf_result( + stream: RuntimeCellHandle, + format: RuntimeCellHandle, + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let format_args = eval_sprintf_argument_array_values(array, values)?; + super::fprintf::eval_fprintf_result(stream, format, &format_args, context, values) } From 0d22dd2d6f6f8757020ea8aa4a2fc486d738d671 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:44:01 +0200 Subject: [PATCH 1122/1208] refactor(magician): move stream settings builtin implementations home --- .../builtins/filesystem/direct_dispatch.rs | 6 - .../interpreter/builtins/filesystem/mod.rs | 2 - .../builtins/filesystem/stream_isatty.rs | 32 +++- .../filesystem/stream_set_blocking.rs | 45 ++++- .../filesystem/stream_set_chunk_size.rs | 49 ++++- .../filesystem/stream_set_read_buffer.rs | 7 +- .../builtins/filesystem/stream_set_timeout.rs | 60 +++++- .../filesystem/stream_set_write_buffer.rs | 7 +- .../builtins/filesystem/stream_settings.rs | 178 ------------------ .../filesystem/stream_values_dispatch.rs | 29 --- .../filesystem/user_wrapper_options.rs | 4 +- 11 files changed, 190 insertions(+), 229 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index c8b99d9742..eb6e2d2e99 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -197,12 +197,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_stream_filter_register(args, context, scope, values) } "stream_filter_remove" => eval_builtin_stream_filter_remove(args, context, scope, values), - "stream_isatty" => eval_builtin_stream_isatty(args, context, scope, values), - "stream_set_blocking" => eval_builtin_stream_set_blocking(args, context, scope, values), - "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { - eval_builtin_stream_set_buffer_like(name, args, context, scope, values) - } - "stream_set_timeout" => eval_builtin_stream_set_timeout(args, context, scope, values), "stream_socket_client" => eval_builtin_stream_socket_client(args, context, scope, values), "stream_socket_enable_crypto" => { eval_builtin_stream_socket_enable_crypto(args, context, scope, values) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index e1e5e72a9c..f63c49da5c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -120,7 +120,6 @@ mod stream_set_chunk_size; mod stream_set_read_buffer; mod stream_set_timeout; mod stream_set_write_buffer; -mod stream_settings; mod stream_socket_accept; mod stream_socket_client; mod stream_socket_enable_crypto; @@ -161,7 +160,6 @@ pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use readline::*; pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_extensions::*; -pub(in crate::interpreter) use stream_settings::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_cast::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs index e97fc5b1d3..97b6f29b9e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `stream_isatty` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_isatty_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_stream_isatty_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_isatty", args, context, scope, values) + eval_builtin_stream_isatty(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_isatty` filesystem builtin through the area dispatcher. @@ -33,5 +34,32 @@ pub(in crate::interpreter) fn eval_stream_isatty_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_isatty", evaluated_args, context, values) + match evaluated_args { + [stream] => eval_stream_isatty_result(*stream, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `stream_isatty($stream)` over one eval expression. +pub(in crate::interpreter) fn eval_builtin_stream_isatty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + eval_stream_isatty_result(stream, context, values) +} + +/// Returns whether a materialized stream resource is attached to a terminal. +pub(in crate::interpreter) fn eval_stream_isatty_result( + stream: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + values.bool_value(context.stream_resources().isatty(id).unwrap_or(false)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs index 3803b82a71..18871d979f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `stream_set_blocking` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_set_blocking_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_stream_set_blocking_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_blocking", args, context, scope, values) + eval_builtin_stream_set_blocking(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_set_blocking` filesystem builtin through the area dispatcher. @@ -33,5 +34,45 @@ pub(in crate::interpreter) fn eval_stream_set_blocking_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_set_blocking", evaluated_args, context, values) + match evaluated_args { + [stream, enable] => eval_stream_set_blocking_result(*stream, *enable, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `stream_set_blocking($stream, $enable)`. +pub(in crate::interpreter) fn eval_builtin_stream_set_blocking( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, enable] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let enable = eval_expr(enable, context, scope, values)?; + eval_stream_set_blocking_result(stream, enable, context, values) +} + +/// Toggles blocking mode on a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_set_blocking_result( + stream: RuntimeCellHandle, + enable: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let enable = values.truthy(enable)?; + if let Some(result) = eval_user_wrapper_stream_set_option_result( + id, + EVAL_STREAM_OPTION_BLOCKING, + if enable { 1 } else { 0 }, + 0, + context, + values, + )? { + return Ok(result); + } + values.bool_value(context.stream_resources().set_blocking(id, enable).unwrap_or(false)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs index fbb872f5b7..f31b1e5e38 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs @@ -16,6 +16,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `stream_set_chunk_size` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_set_chunk_size_declared_call( @@ -24,7 +25,7 @@ pub(in crate::interpreter) fn eval_stream_set_chunk_size_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_chunk_size", args, context, scope, values) + eval_builtin_stream_set_buffer_like("stream_set_chunk_size", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_set_chunk_size` filesystem builtin through the area dispatcher. @@ -33,5 +34,49 @@ pub(in crate::interpreter) fn eval_stream_set_chunk_size_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_set_chunk_size", evaluated_args, context, values) + match evaluated_args { + [stream, size] => eval_stream_set_buffer_like_result("stream_set_chunk_size", *stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates chunk/read/write buffer setting builtins. +pub(in crate::interpreter) fn eval_builtin_stream_set_buffer_like( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream, size] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let size = eval_expr(size, context, scope, values)?; + eval_stream_set_buffer_like_result(name, stream, size, context, values) +} + +/// Applies a materialized chunk/read/write buffer setting. +pub(in crate::interpreter) fn eval_stream_set_buffer_like_result( + name: &str, + stream: RuntimeCellHandle, + size: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let size = eval_int_value(size, values)?; + match name { + "stream_set_chunk_size" => match context.stream_resources_mut().set_chunk_size(id, size) { + Some(previous) => values.int(previous), + None => values.bool_value(false), + }, + "stream_set_read_buffer" | "stream_set_write_buffer" => { + match context.stream_resources().set_buffer(id, size) { + Some(status) => values.int(status), + None => values.bool_value(false), + } + } + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs index 08ba984182..635e6c616e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_stream_set_read_buffer_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_read_buffer", args, context, scope, values) + super::stream_set_chunk_size::eval_builtin_stream_set_buffer_like("stream_set_read_buffer", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_set_read_buffer` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_stream_set_read_buffer_declared_values_result context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_set_read_buffer", evaluated_args, context, values) + match evaluated_args { + [stream, size] => super::stream_set_chunk_size::eval_stream_set_buffer_like_result("stream_set_read_buffer", *stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs index 9cb82c326c..94cf86cddd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs @@ -18,6 +18,7 @@ eval_builtin! { } use super::super::super::*; +use super::*; /// Dispatches direct eval calls for the `stream_set_timeout` filesystem builtin through the area dispatcher. pub(in crate::interpreter) fn eval_stream_set_timeout_declared_call( @@ -26,7 +27,7 @@ pub(in crate::interpreter) fn eval_stream_set_timeout_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_timeout", args, context, scope, values) + eval_builtin_stream_set_timeout(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_set_timeout` filesystem builtin through the area dispatcher. @@ -35,5 +36,60 @@ pub(in crate::interpreter) fn eval_stream_set_timeout_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_set_timeout", evaluated_args, context, values) + match evaluated_args { + [stream, seconds] => eval_stream_set_timeout_result(*stream, *seconds, None, context, values), + [stream, seconds, microseconds] => eval_stream_set_timeout_result(*stream, *seconds, Some(*microseconds), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `stream_set_timeout($stream, $seconds, $microseconds = 0)`. +pub(in crate::interpreter) fn eval_builtin_stream_set_timeout( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let seconds = eval_expr(&args[1], context, scope, values)?; + let microseconds = match args.get(2) { + Some(microseconds) => Some(eval_expr(microseconds, context, scope, values)?), + None => None, + }; + eval_stream_set_timeout_result(stream, seconds, microseconds, context, values) +} + +/// Applies a timeout request to a materialized stream resource. +pub(in crate::interpreter) fn eval_stream_set_timeout_result( + stream: RuntimeCellHandle, + seconds: RuntimeCellHandle, + microseconds: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_resource_id(stream, values)?; + let seconds = eval_int_value(seconds, values)?; + let microseconds = match microseconds { + Some(microseconds) => eval_int_value(microseconds, values)?, + None => 0, + }; + if let Some(result) = eval_user_wrapper_stream_set_option_result( + id, + EVAL_STREAM_OPTION_READ_TIMEOUT, + seconds, + microseconds, + context, + values, + )? { + return Ok(result); + } + values.bool_value( + context + .stream_resources() + .set_timeout(id, seconds, microseconds) + .unwrap_or(false), + ) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs index ae553d5087..51ffe897c7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_stream_set_write_buffer_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_set_write_buffer", args, context, scope, values) + super::stream_set_chunk_size::eval_builtin_stream_set_buffer_like("stream_set_write_buffer", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `stream_set_write_buffer` filesystem builtin through the area dispatcher. @@ -33,5 +33,8 @@ pub(in crate::interpreter) fn eval_stream_set_write_buffer_declared_values_resul context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_set_write_buffer", evaluated_args, context, values) + match evaluated_args { + [stream, size] => super::stream_set_chunk_size::eval_stream_set_buffer_like_result("stream_set_write_buffer", *stream, *size, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs deleted file mode 100644 index 756745314e..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_settings.rs +++ /dev/null @@ -1,178 +0,0 @@ -//! Purpose: -//! Implements eval stream descriptor predicate and setting builtins. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - `stream_isatty` and `stream_set_blocking` call host libc for local files. -//! - Buffer and chunk-size settings are eval-local metadata; timeout is false for -//! local files because the main backend applies it as a socket option. - -use super::super::super::*; - -/// Evaluates `stream_isatty($stream)` over one eval expression. -pub(in crate::interpreter) fn eval_builtin_stream_isatty( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - eval_stream_isatty_result(stream, context, values) -} - -/// Returns whether a materialized stream resource is attached to a terminal. -pub(in crate::interpreter) fn eval_stream_isatty_result( - stream: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_settings_stream_resource_id(stream, values)?; - values.bool_value(context.stream_resources().isatty(id).unwrap_or(false)) -} - -/// Evaluates `stream_set_blocking($stream, $enable)`. -pub(in crate::interpreter) fn eval_builtin_stream_set_blocking( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, enable] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let enable = eval_expr(enable, context, scope, values)?; - eval_stream_set_blocking_result(stream, enable, context, values) -} - -/// Toggles blocking mode on a materialized stream resource. -pub(in crate::interpreter) fn eval_stream_set_blocking_result( - stream: RuntimeCellHandle, - enable: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_settings_stream_resource_id(stream, values)?; - let enable = values.truthy(enable)?; - if let Some(result) = eval_user_wrapper_stream_set_option_result( - id, - EVAL_STREAM_OPTION_BLOCKING, - if enable { 1 } else { 0 }, - 0, - context, - values, - )? { - return Ok(result); - } - values.bool_value(context.stream_resources().set_blocking(id, enable).unwrap_or(false)) -} - -/// Evaluates `stream_set_timeout($stream, $seconds, $microseconds = 0)`. -pub(in crate::interpreter) fn eval_builtin_stream_set_timeout( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=3).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let seconds = eval_expr(&args[1], context, scope, values)?; - let microseconds = match args.get(2) { - Some(microseconds) => Some(eval_expr(microseconds, context, scope, values)?), - None => None, - }; - eval_stream_set_timeout_result(stream, seconds, microseconds, context, values) -} - -/// Applies a timeout request to a materialized stream resource. -pub(in crate::interpreter) fn eval_stream_set_timeout_result( - stream: RuntimeCellHandle, - seconds: RuntimeCellHandle, - microseconds: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_settings_stream_resource_id(stream, values)?; - let seconds = eval_int_value(seconds, values)?; - let microseconds = match microseconds { - Some(microseconds) => eval_int_value(microseconds, values)?, - None => 0, - }; - if let Some(result) = eval_user_wrapper_stream_set_option_result( - id, - EVAL_STREAM_OPTION_READ_TIMEOUT, - seconds, - microseconds, - context, - values, - )? { - return Ok(result); - } - values.bool_value( - context - .stream_resources() - .set_timeout(id, seconds, microseconds) - .unwrap_or(false), - ) -} - -/// Evaluates chunk/read/write buffer setting builtins. -pub(in crate::interpreter) fn eval_builtin_stream_set_buffer_like( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, size] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let size = eval_expr(size, context, scope, values)?; - eval_stream_set_buffer_like_result(name, stream, size, context, values) -} - -/// Applies a materialized chunk/read/write buffer setting. -pub(in crate::interpreter) fn eval_stream_set_buffer_like_result( - name: &str, - stream: RuntimeCellHandle, - size: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_settings_stream_resource_id(stream, values)?; - let size = eval_int_value(size, values)?; - match name { - "stream_set_chunk_size" => match context.stream_resources_mut().set_chunk_size(id, size) { - Some(previous) => values.int(previous), - None => values.bool_value(false), - }, - "stream_set_read_buffer" | "stream_set_write_buffer" => { - match context.stream_resources().set_buffer(id, size) { - Some(status) => values.int(status), - None => values.bool_value(false), - } - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Converts a runtime resource cell into eval's zero-based stream id. -fn eval_settings_stream_resource_id( - stream: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(stream)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(stream, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs index 6660ad4417..99ac1453ea 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs @@ -133,39 +133,10 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value }; eval_stream_filter_remove_result(*stream_filter, context, values)? } - "stream_isatty" => match evaluated_args { - [stream] => eval_stream_isatty_result(*stream, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "stream_select" => { eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; eval_stream_select_result(evaluated_args, context, values)? } - "stream_set_blocking" => match evaluated_args { - [stream, enable] => eval_stream_set_blocking_result(*stream, *enable, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stream_set_chunk_size" | "stream_set_read_buffer" | "stream_set_write_buffer" => { - match evaluated_args { - [stream, size] => { - eval_stream_set_buffer_like_result(name, *stream, *size, context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - "stream_set_timeout" => match evaluated_args { - [stream, seconds] => { - eval_stream_set_timeout_result(*stream, *seconds, None, context, values)? - } - [stream, seconds, microseconds] => eval_stream_set_timeout_result( - *stream, - *seconds, - Some(*microseconds), - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, "stream_socket_server" => { let [address] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs index d6706b0218..9c00d25c6b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_options.rs @@ -2,8 +2,8 @@ //! Dispatches stream option builtins to eval userspace stream wrappers. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::stream_settings` for -//! `stream_set_blocking()` and `stream_set_timeout()`. +//! - `crate::interpreter::builtins::filesystem::stream_set_blocking` and +//! `stream_set_timeout` when the stream is wrapper-backed. //! //! Key details: //! - Mirrors the generated runtime's `stream_set_option($option, $arg1, $arg2)` From 8c64275c1b5e5a8deca0bab0a0edc353bcda7fa2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:48:05 +0200 Subject: [PATCH 1123/1208] refactor(magician): move stream context builtins home --- .../builtins/filesystem/direct_dispatch.rs | 19 -- .../interpreter/builtins/filesystem/mod.rs | 2 - .../builtins/filesystem/stream_context.rs | 257 ------------------ .../filesystem/stream_context_create.rs | 53 +++- .../filesystem/stream_context_get_default.rs | 28 +- .../filesystem/stream_context_get_options.rs | 30 +- .../filesystem/stream_context_get_params.rs | 28 +- .../filesystem/stream_context_set_default.rs | 17 +- .../filesystem/stream_context_set_option.rs | 115 +++++++- .../filesystem/stream_context_set_params.rs | 29 +- .../filesystem/stream_values_dispatch.rs | 58 ---- 11 files changed, 263 insertions(+), 373 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index eb6e2d2e99..9c3257b8a8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -171,25 +171,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( eval_builtin_stream_bucket_make_writeable(args, context, scope, values) } "stream_bucket_new" => eval_builtin_stream_bucket_new(args, context, scope, values), - "stream_context_create" => eval_builtin_stream_context_create(args, context, scope, values), - "stream_context_get_default" => { - eval_builtin_stream_context_get_default(args, context, scope, values) - } - "stream_context_get_options" => { - eval_builtin_stream_context_get_options(args, context, scope, values) - } - "stream_context_get_params" => { - eval_builtin_stream_context_get_params(args, context, scope, values) - } - "stream_context_set_default" => { - eval_builtin_stream_context_set_default(args, context, scope, values) - } - "stream_context_set_option" => { - eval_builtin_stream_context_set_option(args, context, scope, values) - } - "stream_context_set_params" => { - eval_builtin_stream_context_set_params(args, context, scope, values) - } "stream_filter_append" | "stream_filter_prepend" => { eval_builtin_stream_filter_attach(name, args, context, scope, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index f63c49da5c..7992a23102 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -95,7 +95,6 @@ mod stream_bucket_append; mod stream_bucket_make_writeable; mod stream_bucket_new; mod stream_bucket_prepend; -mod stream_context; mod stream_context_create; mod stream_context_get_default; mod stream_context_get_options; @@ -158,7 +157,6 @@ mod vfprintf; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use readline::*; -pub(in crate::interpreter) use stream_context::*; pub(in crate::interpreter) use stream_extensions::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context.rs deleted file mode 100644 index 76fc2bdb43..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context.rs +++ /dev/null @@ -1,257 +0,0 @@ -//! Purpose: -//! Implements eval stream context metadata builtins. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - Eval stores options per context resource, while `get_params()` mirrors the -//! main backend's current empty-array behavior. -//! - Context resources share the same generic resource id namespace as streams. - -use super::super::super::*; - -/// Evaluates `stream_context_create($options = null, $params = null)`. -pub(in crate::interpreter) fn eval_builtin_stream_context_create( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() > 2 { - return Err(EvalStatus::RuntimeFatal); - } - let options = match args.first() { - Some(options) => Some(eval_expr(options, context, scope, values)?), - None => None, - }; - if let Some(params) = args.get(1) { - eval_expr(params, context, scope, values)?; - } - eval_stream_context_create_result(options, context, values) -} - -/// Creates a stream context resource from materialized optional options. -pub(in crate::interpreter) fn eval_stream_context_create_result( - options: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let options = eval_stream_context_options_arg(options, values)?; - let id = context.stream_resources_mut().open_stream_context(options); - values.resource(id) -} - -/// Evaluates `stream_context_get_default($options = null)`. -pub(in crate::interpreter) fn eval_builtin_stream_context_get_default( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.len() > 1 { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - eval_expr(arg, context, scope, values)?; - } - eval_stream_context_get_default_result(context, values) -} - -/// Returns eval's default stream context resource. -pub(in crate::interpreter) fn eval_stream_context_get_default_result( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = context.stream_resources_mut().default_stream_context(); - values.resource(id) -} - -/// Evaluates `stream_context_set_default($options)`. -pub(in crate::interpreter) fn eval_builtin_stream_context_set_default( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [options] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_expr(options, context, scope, values)?; - eval_stream_context_get_default_result(context, values) -} - -/// Evaluates `stream_context_set_option($context, ...)`. -pub(in crate::interpreter) fn eval_builtin_stream_context_set_option( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [stream_context, options] => { - let stream_context = eval_expr(stream_context, context, scope, values)?; - let options = eval_expr(options, context, scope, values)?; - eval_stream_context_set_options_result(stream_context, options, context, values) - } - [stream_context, wrapper, option, value] => { - let stream_context = eval_expr(stream_context, context, scope, values)?; - let wrapper = eval_expr(wrapper, context, scope, values)?; - let option = eval_expr(option, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_stream_context_set_option_result( - stream_context, - wrapper, - option, - value, - context, - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Stores a materialized options array on a stream context resource. -pub(in crate::interpreter) fn eval_stream_context_set_options_result( - stream_context: RuntimeCellHandle, - options: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_context_resource_id(stream_context, values)?; - let options = eval_stream_context_options_arg(Some(options), values)?; - values.bool_value( - context - .stream_resources_mut() - .set_stream_context_options(id, options), - ) -} - -/// Stores one nested `options[wrapper][option] = value` entry on a stream context. -pub(in crate::interpreter) fn eval_stream_context_set_option_result( - stream_context: RuntimeCellHandle, - wrapper: RuntimeCellHandle, - option: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_context_resource_id(stream_context, values)?; - let wrapper = values.cast_string(wrapper)?; - let option = values.cast_string(option)?; - let options = match context.stream_resources().stream_context_options(id) { - Some(options) => options, - None => values.assoc_new(1)?, - }; - let wrapper_options = eval_stream_context_wrapper_options(options, wrapper, values)?; - let wrapper_options = values.array_set(wrapper_options, option, value)?; - let options = values.array_set(options, wrapper, wrapper_options)?; - values.bool_value( - context - .stream_resources_mut() - .set_stream_context_options(id, Some(options)), - ) -} - -/// Evaluates `stream_context_set_params($context, $params)` as an accepted no-op. -pub(in crate::interpreter) fn eval_builtin_stream_context_set_params( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream_context, params] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_expr(stream_context, context, scope, values)?; - eval_expr(params, context, scope, values)?; - values.bool_value(true) -} - -/// Evaluates `stream_context_get_options($context)`. -pub(in crate::interpreter) fn eval_builtin_stream_context_get_options( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream_context] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream_context = eval_expr(stream_context, context, scope, values)?; - eval_stream_context_get_options_result(stream_context, context, values) -} - -/// Returns persisted stream context options or an empty associative array. -pub(in crate::interpreter) fn eval_stream_context_get_options_result( - stream_context: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_context_resource_id(stream_context, values)?; - match context.stream_resources().stream_context_options(id) { - Some(options) => Ok(options), - None => values.assoc_new(0), - } -} - -/// Evaluates `stream_context_get_params($context)` to an empty associative array. -pub(in crate::interpreter) fn eval_builtin_stream_context_get_params( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream_context] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_expr(stream_context, context, scope, values)?; - values.assoc_new(0) -} - -/// Converts an optional options argument into a stored context option handle. -fn eval_stream_context_options_arg( - options: Option, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(options) = options else { - return Ok(None); - }; - if values.type_tag(options)? == EVAL_TAG_NULL { - return Ok(None); - } - if !values.is_array_like(options)? { - return Err(EvalStatus::RuntimeFatal); - } - Ok(Some(options)) -} - -/// Returns the nested wrapper options array, creating one when missing or scalar. -fn eval_stream_context_wrapper_options( - options: RuntimeCellHandle, - wrapper: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = values.array_key_exists(wrapper, options)?; - if values.truthy(exists)? { - let wrapper_options = values.array_get(options, wrapper)?; - if values.is_array_like(wrapper_options)? { - return Ok(wrapper_options); - } - } - values.assoc_new(1) -} - -/// Converts a runtime resource cell into eval's zero-based stream context id. -fn eval_stream_context_resource_id( - stream_context: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(stream_context)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(stream_context, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs index b828402de7..98dd0d81b0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream context helper. +//! - Owns context resource creation and validates optional options arrays before storage. use super::super::spec::EvalBuiltinDefaultValue; @@ -22,21 +22,64 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_context_create` filesystem builtin through the area dispatcher. +/// Evaluates `stream_context_create($options = null, $params = null)`. pub(in crate::interpreter) fn eval_stream_context_create_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_create", args, context, scope, values) + if args.len() > 2 { + return Err(EvalStatus::RuntimeFatal); + } + let options = match args.first() { + Some(options) => Some(eval_expr(options, context, scope, values)?), + None => None, + }; + if let Some(params) = args.get(1) { + eval_expr(params, context, scope, values)?; + } + eval_stream_context_create_result(options, context, values) } -/// Dispatches evaluated-argument calls for the `stream_context_create` filesystem builtin through the area dispatcher. +/// Creates a stream context resource from already evaluated optional options. pub(in crate::interpreter) fn eval_stream_context_create_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_context_create", evaluated_args, context, values) + match evaluated_args { + [] => eval_stream_context_create_result(None, context, values), + [options] => eval_stream_context_create_result(Some(*options), context, values), + [options, _params] => eval_stream_context_create_result(Some(*options), context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates a stream context resource from materialized optional options. +pub(in crate::interpreter) fn eval_stream_context_create_result( + options: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let options = eval_stream_context_options_arg(options, values)?; + let id = context.stream_resources_mut().open_stream_context(options); + values.resource(id) +} + +/// Converts an optional options argument into a stored context option handle. +pub(in crate::interpreter) fn eval_stream_context_options_arg( + options: Option, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(options) = options else { + return Ok(None); + }; + if values.type_tag(options)? == EVAL_TAG_NULL { + return Ok(None); + } + if !values.is_array_like(options)? { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(options)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs index 5de9f1d4b8..4eed88761e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the default stream context helper. +//! - Returns eval's shared default stream context resource. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +19,39 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_context_get_default` filesystem builtin through the area dispatcher. +/// Evaluates `stream_context_get_default($options = null)`. pub(in crate::interpreter) fn eval_stream_context_get_default_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_get_default", args, context, scope, values) + if args.len() > 1 { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + eval_expr(arg, context, scope, values)?; + } + eval_stream_context_get_default_result(context, values) } -/// Dispatches evaluated-argument calls for the `stream_context_get_default` filesystem builtin through the area dispatcher. +/// Returns the default context after validating already evaluated arguments. pub(in crate::interpreter) fn eval_stream_context_get_default_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_context_get_default", evaluated_args, context, values) + if evaluated_args.len() > 1 { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_context_get_default_result(context, values) +} + +/// Returns eval's default stream context resource. +pub(in crate::interpreter) fn eval_stream_context_get_default_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = context.stream_resources_mut().default_stream_context(); + values.resource(id) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs index 111825030c..5fa2390d46 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream context option reader. +//! - Returns persisted context options or an empty associative array. eval_builtin! { name: "stream_context_get_options", @@ -17,21 +17,41 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_context_get_options` filesystem builtin through the area dispatcher. +/// Evaluates `stream_context_get_options($context)`. pub(in crate::interpreter) fn eval_stream_context_get_options_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_get_options", args, context, scope, values) + let [stream_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_context = eval_expr(stream_context, context, scope, values)?; + eval_stream_context_get_options_result(stream_context, context, values) } -/// Dispatches evaluated-argument calls for the `stream_context_get_options` filesystem builtin through the area dispatcher. +/// Returns options for an already evaluated stream context resource. pub(in crate::interpreter) fn eval_stream_context_get_options_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_context_get_options", evaluated_args, context, values) + let [stream_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_context_get_options_result(*stream_context, context, values) +} + +/// Returns persisted stream context options or an empty associative array. +pub(in crate::interpreter) fn eval_stream_context_get_options_result( + stream_context: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_context_set_option::eval_stream_context_resource_id(stream_context, values)?; + match context.stream_resources().stream_context_options(id) { + Some(options) => Ok(options), + None => values.assoc_new(0), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs index 674f6dae70..7de0673e7d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Eval mirrors the main backend's current empty-params behavior. +//! - Eval validates the context resource and mirrors the main backend's empty-params behavior. eval_builtin! { name: "stream_context_get_params", @@ -17,21 +17,37 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_context_get_params` filesystem builtin through the area dispatcher. +/// Evaluates `stream_context_get_params($context)` to an empty associative array. pub(in crate::interpreter) fn eval_stream_context_get_params_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_get_params", args, context, scope, values) + let [stream_context] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_context = eval_expr(stream_context, context, scope, values)?; + eval_stream_context_get_params_result(stream_context, values) } -/// Dispatches evaluated-argument calls for the `stream_context_get_params` filesystem builtin through the area dispatcher. +/// Returns empty params for an already evaluated stream context resource. pub(in crate::interpreter) fn eval_stream_context_get_params_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_context_get_params_result(*stream_context, values) +} + +/// Validates the stream context resource and returns the current empty params array. +pub(in crate::interpreter) fn eval_stream_context_get_params_result( + stream_context: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_context_get_params", evaluated_args, context, values) + super::stream_context_set_option::eval_stream_context_resource_id(stream_context, values)?; + values.assoc_new(0) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs index a5b1d52571..a8bb8c7183 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream context helper. +//! - Current eval behavior validates arity/evaluation and returns the default context resource. eval_builtin! { name: "stream_context_set_default", @@ -17,21 +17,28 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_context_set_default` filesystem builtin through the area dispatcher. +/// Evaluates `stream_context_set_default($options)`. pub(in crate::interpreter) fn eval_stream_context_set_default_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_set_default", args, context, scope, values) + let [options] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_expr(options, context, scope, values)?; + super::stream_context_get_default::eval_stream_context_get_default_result(context, values) } -/// Dispatches evaluated-argument calls for the `stream_context_set_default` filesystem builtin through the area dispatcher. +/// Returns the default context after validating already evaluated arguments. pub(in crate::interpreter) fn eval_stream_context_set_default_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_context_set_default", evaluated_args, context, values) + let [_options] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + super::stream_context_get_default::eval_stream_context_get_default_result(context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs index 0ec2feae25..9022d91afd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - The signature keeps the existing two-argument and four-argument forms. +//! - Owns both `stream_context_set_option($context, $options)` and the +//! four-argument nested option form. use super::super::spec::EvalBuiltinDefaultValue; @@ -25,21 +26,125 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_context_set_option` filesystem builtin through the area dispatcher. +/// Evaluates `stream_context_set_option($context, ...)`. pub(in crate::interpreter) fn eval_stream_context_set_option_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_set_option", args, context, scope, values) + match args { + [stream_context, options] => { + let stream_context = eval_expr(stream_context, context, scope, values)?; + let options = eval_expr(options, context, scope, values)?; + eval_stream_context_set_options_result(stream_context, options, context, values) + } + [stream_context, wrapper, option, value] => { + let stream_context = eval_expr(stream_context, context, scope, values)?; + let wrapper = eval_expr(wrapper, context, scope, values)?; + let option = eval_expr(option, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_stream_context_set_option_result( + stream_context, + wrapper, + option, + value, + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } } -/// Dispatches evaluated-argument calls for the `stream_context_set_option` filesystem builtin through the area dispatcher. +/// Stores context options from already evaluated arguments. pub(in crate::interpreter) fn eval_stream_context_set_option_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_context_set_option", evaluated_args, context, values) + match evaluated_args { + [stream_context, options] => { + eval_stream_context_set_options_result(*stream_context, *options, context, values) + } + [stream_context, wrapper, option, value] => eval_stream_context_set_option_result( + *stream_context, + *wrapper, + *option, + *value, + context, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Stores a materialized options array on a stream context resource. +pub(in crate::interpreter) fn eval_stream_context_set_options_result( + stream_context: RuntimeCellHandle, + options: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_context_resource_id(stream_context, values)?; + let options = super::stream_context_create::eval_stream_context_options_arg(Some(options), values)?; + values.bool_value( + context + .stream_resources_mut() + .set_stream_context_options(id, options), + ) +} + +/// Stores one nested `options[wrapper][option] = value` entry on a stream context. +pub(in crate::interpreter) fn eval_stream_context_set_option_result( + stream_context: RuntimeCellHandle, + wrapper: RuntimeCellHandle, + option: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_stream_context_resource_id(stream_context, values)?; + let wrapper = values.cast_string(wrapper)?; + let option = values.cast_string(option)?; + let options = match context.stream_resources().stream_context_options(id) { + Some(options) => options, + None => values.assoc_new(1)?, + }; + let wrapper_options = eval_stream_context_wrapper_options(options, wrapper, values)?; + let wrapper_options = values.array_set(wrapper_options, option, value)?; + let options = values.array_set(options, wrapper, wrapper_options)?; + values.bool_value( + context + .stream_resources_mut() + .set_stream_context_options(id, Some(options)), + ) +} + +/// Converts a runtime resource cell into eval's zero-based stream context id. +pub(in crate::interpreter) fn eval_stream_context_resource_id( + stream_context: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(stream_context)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(stream_context, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns the nested wrapper options array, creating one when missing or scalar. +fn eval_stream_context_wrapper_options( + options: RuntimeCellHandle, + wrapper: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = values.array_key_exists(wrapper, options)?; + if values.truthy(exists)? { + let wrapper_options = values.array_get(options, wrapper)?; + if values.is_array_like(wrapper_options)? { + return Ok(wrapper_options); + } + } + values.assoc_new(1) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs index 1fd8406950..0c4c089ead 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the accepted no-op helper. +//! - Eval validates the context resource and accepts params as a no-op. eval_builtin! { name: "stream_context_set_params", @@ -17,21 +17,38 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_context_set_params` filesystem builtin through the area dispatcher. +/// Evaluates `stream_context_set_params($context, $params)` as an accepted no-op. pub(in crate::interpreter) fn eval_stream_context_set_params_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_context_set_params", args, context, scope, values) + let [stream_context, params] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_context = eval_expr(stream_context, context, scope, values)?; + eval_expr(params, context, scope, values)?; + eval_stream_context_set_params_result(stream_context, values) } -/// Dispatches evaluated-argument calls for the `stream_context_set_params` filesystem builtin through the area dispatcher. +/// Returns true after validating already evaluated stream context params. pub(in crate::interpreter) fn eval_stream_context_set_params_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [stream_context, _params] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_context_set_params_result(*stream_context, values) +} + +/// Validates the stream context resource and returns true for accepted params. +pub(in crate::interpreter) fn eval_stream_context_set_params_result( + stream_context: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_context_set_params", evaluated_args, context, values) + super::stream_context_set_option::eval_stream_context_resource_id(stream_context, values)?; + values.bool_value(true) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs index 99ac1453ea..587128e7ed 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs @@ -51,64 +51,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value }; eval_stream_bucket_push_result(name, *brigade, *bucket, values)? } - "stream_context_create" => match evaluated_args { - [] => eval_stream_context_create_result(None, context, values)?, - [options] => eval_stream_context_create_result(Some(*options), context, values)?, - [options, _params] => { - eval_stream_context_create_result(Some(*options), context, values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stream_context_get_default" => { - if evaluated_args.len() > 1 { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_context_get_default_result(context, values)? - } - "stream_context_get_options" => { - let [stream_context] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_context_get_options_result(*stream_context, context, values)? - } - "stream_context_get_params" => { - let [stream_context] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - if values.type_tag(*stream_context)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - values.assoc_new(0)? - } - "stream_context_set_default" => { - let [_options] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_context_get_default_result(context, values)? - } - "stream_context_set_option" => match evaluated_args { - [stream_context, options] => { - eval_stream_context_set_options_result(*stream_context, *options, context, values)? - } - [stream_context, wrapper, option, value] => eval_stream_context_set_option_result( - *stream_context, - *wrapper, - *option, - *value, - context, - values, - )?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "stream_context_set_params" => { - let [stream_context, _params] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - if values.type_tag(*stream_context)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - values.bool_value(true)? - } "stream_filter_register" => { let [filter_name, class] = evaluated_args else { return Err(EvalStatus::RuntimeFatal); From 1c94630791712c2215e99a9f4732f2db863895ba Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 18:55:02 +0200 Subject: [PATCH 1124/1208] refactor(magician): move stream extension builtins home --- .../builtins/filesystem/direct_dispatch.rs | 17 - .../interpreter/builtins/filesystem/mod.rs | 2 - .../filesystem/stream_bucket_append.rs | 49 ++- .../stream_bucket_make_writeable.rs | 34 +- .../builtins/filesystem/stream_bucket_new.rs | 52 ++- .../filesystem/stream_bucket_prepend.rs | 54 ++- .../builtins/filesystem/stream_extensions.rs | 324 ------------------ .../filesystem/stream_filter_append.rs | 49 ++- .../filesystem/stream_filter_prepend.rs | 35 +- .../filesystem/stream_filter_register.rs | 33 +- .../filesystem/stream_filter_remove.rs | 29 +- .../filesystem/stream_values_dispatch.rs | 45 --- .../filesystem/stream_wrapper_register.rs | 59 +++- .../filesystem/stream_wrapper_restore.rs | 33 +- .../filesystem/stream_wrapper_unregister.rs | 33 +- 15 files changed, 390 insertions(+), 458 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index 9c3257b8a8..a98ff07ffc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -164,20 +164,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "readline" => eval_builtin_readline(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "stream_bucket_append" | "stream_bucket_prepend" => { - eval_builtin_stream_bucket_push(name, args, context, scope, values) - } - "stream_bucket_make_writeable" => { - eval_builtin_stream_bucket_make_writeable(args, context, scope, values) - } - "stream_bucket_new" => eval_builtin_stream_bucket_new(args, context, scope, values), - "stream_filter_append" | "stream_filter_prepend" => { - eval_builtin_stream_filter_attach(name, args, context, scope, values) - } - "stream_filter_register" => { - eval_builtin_stream_filter_register(args, context, scope, values) - } - "stream_filter_remove" => eval_builtin_stream_filter_remove(args, context, scope, values), "stream_socket_client" => eval_builtin_stream_socket_client(args, context, scope, values), "stream_socket_enable_crypto" => { eval_builtin_stream_socket_enable_crypto(args, context, scope, values) @@ -191,9 +177,6 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( "stream_socket_shutdown" => { eval_builtin_stream_socket_shutdown(args, context, scope, values) } - "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { - eval_builtin_stream_wrapper_registry(name, args, context, scope, values) - } "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), _ => Err(EvalStatus::RuntimeFatal), } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 7992a23102..07903aad9a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -103,7 +103,6 @@ mod stream_context_set_default; mod stream_context_set_option; mod stream_context_set_params; mod stream_copy_to_stream; -mod stream_extensions; mod stream_filter_append; mod stream_filter_prepend; mod stream_filter_register; @@ -157,7 +156,6 @@ mod vfprintf; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use readline::*; -pub(in crate::interpreter) use stream_extensions::*; pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_cast::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs index c4e0f42362..4af2773a41 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_bucket_append`. +//! Declarative eval registry entry and implementation for `stream_bucket_append`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream bucket push helper. +//! - Appends bucket objects to the brigade `_buckets` array used by eval filters. eval_builtin! { name: "stream_bucket_append", @@ -17,21 +17,56 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_bucket_append` filesystem builtin through the area dispatcher. +/// Evaluates `stream_bucket_append($brigade, $bucket)`. pub(in crate::interpreter) fn eval_stream_bucket_append_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_bucket_append", args, context, scope, values) + let [brigade, bucket] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let brigade = eval_expr(brigade, context, scope, values)?; + let bucket = eval_expr(bucket, context, scope, values)?; + eval_stream_bucket_append_result(brigade, bucket, values) } -/// Dispatches evaluated-argument calls for the `stream_bucket_append` filesystem builtin through the area dispatcher. +/// Appends an already evaluated bucket to an already evaluated brigade. pub(in crate::interpreter) fn eval_stream_bucket_append_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade, bucket] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_append_result(*brigade, *bucket, values) +} + +/// Adds a bucket object to the end of the brigade's `_buckets` array. +pub(in crate::interpreter) fn eval_stream_bucket_append_result( + brigade: RuntimeCellHandle, + bucket: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let buckets = eval_brigade_buckets(brigade, values)?; + let len = values.array_len(buckets)?; + let index = values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let buckets = values.array_set(buckets, index, bucket)?; + values.property_set(brigade, "_buckets", buckets)?; + values.null() +} + +/// Returns an existing brigade bucket array or creates an empty one. +pub(in crate::interpreter) fn eval_brigade_buckets( + brigade: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_bucket_append", evaluated_args, context, values) + let buckets = values.property_get(brigade, "_buckets")?; + if values.is_array_like(buckets)? { + Ok(buckets) + } else { + values.array_new(0) + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs index 4a613e515d..b6a06e45c4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_bucket_make_writeable`. +//! Declarative eval registry entry and implementation for `stream_bucket_make_writeable`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream bucket read helper. +//! - Returns the first queued bucket from an eval brigade object. eval_builtin! { name: "stream_bucket_make_writeable", @@ -17,21 +17,41 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_bucket_make_writeable` filesystem builtin through the area dispatcher. +/// Evaluates `stream_bucket_make_writeable($brigade)`. pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_bucket_make_writeable", args, context, scope, values) + let [brigade] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let brigade = eval_expr(brigade, context, scope, values)?; + eval_stream_bucket_make_writeable_result(brigade, values) } -/// Dispatches evaluated-argument calls for the `stream_bucket_make_writeable` filesystem builtin through the area dispatcher. +/// Returns the first bucket from an already evaluated brigade argument. pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_make_writeable_result(*brigade, values) +} + +/// Returns the first bucket in a brigade, or null when none exists. +pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_result( + brigade: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_bucket_make_writeable", evaluated_args, context, values) + let buckets = values.property_get(brigade, "_buckets")?; + if !values.is_array_like(buckets)? || values.array_len(buckets)? == 0 { + return values.null(); + } + let key = values.int(0)?; + values.array_get(buckets, key) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs index 5db038df80..9aafce2033 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_bucket_new`. +//! Declarative eval registry entry and implementation for `stream_bucket_new`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream bucket object helper. +//! - Creates stdClass bucket objects with `data` and `datalen` properties. eval_builtin! { name: "stream_bucket_new", @@ -17,21 +17,61 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_bucket_new` filesystem builtin through the area dispatcher. +/// Evaluates `stream_bucket_new($stream, $buffer)`. pub(in crate::interpreter) fn eval_stream_bucket_new_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_bucket_new", args, context, scope, values) + let [stream, buffer] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let buffer = eval_expr(buffer, context, scope, values)?; + eval_stream_bucket_new_result(stream, buffer, context, values) } -/// Dispatches evaluated-argument calls for the `stream_bucket_new` filesystem builtin through the area dispatcher. +/// Creates a bucket object from already evaluated stream and buffer arguments. pub(in crate::interpreter) fn eval_stream_bucket_new_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_bucket_new", evaluated_args, context, values) + let [stream, buffer] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_new_result(*stream, *buffer, context, values) +} + +/// Creates a stdClass-backed stream bucket object. +pub(in crate::interpreter) fn eval_stream_bucket_new_result( + stream: RuntimeCellHandle, + buffer: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let stream_id = eval_stream_extension_resource_id(stream, values)?; + if !context.stream_resources().has_stream(stream_id) { + return values.null(); + } + let bytes = values.string_bytes(buffer)?; + let bucket = values.new_object("stdClass")?; + let data = values.string_bytes_value(&bytes)?; + let datalen = values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; + values.property_set(bucket, "data", data)?; + values.property_set(bucket, "datalen", datalen)?; + Ok(bucket) +} + +/// Converts a runtime resource cell into eval's zero-based stream-extension id. +pub(in crate::interpreter) fn eval_stream_extension_resource_id( + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(resource, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs index 0aa2f92a25..2d452c3460 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_bucket_prepend`. +//! Declarative eval registry entry and implementation for `stream_bucket_prepend`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream bucket push helper. +//! - Prepends bucket objects to the brigade `_buckets` array used by eval filters. eval_builtin! { name: "stream_bucket_prepend", @@ -17,21 +17,61 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_bucket_prepend` filesystem builtin through the area dispatcher. +/// Evaluates `stream_bucket_prepend($brigade, $bucket)`. pub(in crate::interpreter) fn eval_stream_bucket_prepend_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_bucket_prepend", args, context, scope, values) + let [brigade, bucket] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let brigade = eval_expr(brigade, context, scope, values)?; + let bucket = eval_expr(bucket, context, scope, values)?; + eval_stream_bucket_prepend_result(brigade, bucket, values) } -/// Dispatches evaluated-argument calls for the `stream_bucket_prepend` filesystem builtin through the area dispatcher. +/// Prepends an already evaluated bucket to an already evaluated brigade. pub(in crate::interpreter) fn eval_stream_bucket_prepend_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [brigade, bucket] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_bucket_prepend_result(*brigade, *bucket, values) +} + +/// Adds a bucket object to the front of the brigade's `_buckets` array. +pub(in crate::interpreter) fn eval_stream_bucket_prepend_result( + brigade: RuntimeCellHandle, + bucket: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let buckets = super::stream_bucket_append::eval_brigade_buckets(brigade, values)?; + let buckets = eval_bucket_prepend(buckets, bucket, values)?; + values.property_set(brigade, "_buckets", buckets)?; + values.null() +} + +/// Builds a new bucket array with the provided bucket at index zero. +fn eval_bucket_prepend( + buckets: RuntimeCellHandle, + bucket: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_bucket_prepend", evaluated_args, context, values) + let len = values.array_len(buckets)?; + let mut result = values.array_new(len + 1)?; + let zero = values.int(0)?; + result = values.array_set(result, zero, bucket)?; + for index in 0..len { + let old_key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; + let value = values.array_get(buckets, old_key)?; + let new_key = + values.int(i64::try_from(index + 1).map_err(|_| EvalStatus::RuntimeFatal)?)?; + result = values.array_set(result, new_key, value)?; + } + Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs deleted file mode 100644 index 66e9faf4df..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_extensions.rs +++ /dev/null @@ -1,324 +0,0 @@ -//! Purpose: -//! Implements eval stream wrapper, stream filter, and stream bucket helper builtins. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - Wrapper/filter registries are conservative eval stubs matching the main -//! backend surface without changing stream bytes. -//! - Buckets are represented as `stdClass` objects with `data`, `datalen`, and -//! brigade `_buckets` properties, mirroring the generated runtime model. - -use super::super::super::*; - -/// Evaluates stream wrapper registration builtins. -pub(in crate::interpreter) fn eval_builtin_stream_wrapper_registry( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "stream_wrapper_register" if (2..=3).contains(&args.len()) => {} - "stream_wrapper_unregister" | "stream_wrapper_restore" if args.len() == 1 => {} - _ => return Err(EvalStatus::RuntimeFatal), - } - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - let value = eval_expr(arg, context, scope, values)?; - evaluated_args.push(value); - } - eval_stream_wrapper_registry_result(name, &evaluated_args, context, values) -} - -/// Evaluates stream wrapper registration builtins from materialized arguments. -pub(in crate::interpreter) fn eval_stream_wrapper_registry_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "stream_wrapper_register" if (2..=3).contains(&evaluated_args.len()) => {} - "stream_wrapper_unregister" | "stream_wrapper_restore" if evaluated_args.len() == 1 => {} - _ => return Err(EvalStatus::RuntimeFatal), - } - let protocol = eval_stream_wrapper_protocol(evaluated_args[0], values)?; - let ok = match name { - "stream_wrapper_register" => { - let class_name = eval_stream_wrapper_class(evaluated_args[1], context, values)?; - context - .stream_resources_mut() - .register_stream_wrapper(&protocol, &class_name, EVAL_STREAM_WRAPPERS) - } - "stream_wrapper_unregister" => context - .stream_resources_mut() - .unregister_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS), - "stream_wrapper_restore" => context - .stream_resources_mut() - .restore_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS), - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(ok) -} - -/// Coerces one stream wrapper protocol argument into an owned string. -fn eval_stream_wrapper_protocol( - protocol: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(protocol)?; - Ok(String::from_utf8_lossy(&bytes).into_owned()) -} - -/// Coerces one stream wrapper class argument into a resolved class-name string. -fn eval_stream_wrapper_class( - class_name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(class_name)?; - let class_name = String::from_utf8_lossy(&bytes).into_owned(); - Ok(context - .resolve_class_name(&class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())) -} - -/// Evaluates `stream_filter_register(filter_name, class)`. -pub(in crate::interpreter) fn eval_builtin_stream_filter_register( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [filter_name, class] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let filter_name = eval_expr(filter_name, context, scope, values)?; - let class = eval_expr(class, context, scope, values)?; - eval_stream_filter_register_result(filter_name, class, values) -} - -/// Evaluates a materialized `stream_filter_register()` call. -pub(in crate::interpreter) fn eval_stream_filter_register_result( - filter_name: RuntimeCellHandle, - class: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let _ = values.string_bytes(filter_name)?; - let _ = values.string_bytes(class)?; - values.bool_value(true) -} - -/// Evaluates `stream_filter_append()` or `stream_filter_prepend()`. -pub(in crate::interpreter) fn eval_builtin_stream_filter_attach( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let filter_name = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - let _ = eval_expr(arg, context, scope, values)?; - } - eval_stream_filter_attach_result(name, stream, filter_name, context, values) -} - -/// Creates an eval-local filter resource for a materialized stream filter attach. -pub(in crate::interpreter) fn eval_stream_filter_attach_result( - name: &str, - stream: RuntimeCellHandle, - filter_name: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(name, "stream_filter_append" | "stream_filter_prepend") { - return Err(EvalStatus::RuntimeFatal); - } - let stream_id = eval_stream_extension_resource_id(stream, values)?; - let _ = values.string_bytes(filter_name)?; - if !context.stream_resources().has_stream(stream_id) { - return values.bool_value(false); - } - let filter_id = context.stream_resources_mut().open_filter_resource(); - values.resource(filter_id) -} - -/// Evaluates `stream_filter_remove(stream_filter)`. -pub(in crate::interpreter) fn eval_builtin_stream_filter_remove( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream_filter] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream_filter = eval_expr(stream_filter, context, scope, values)?; - eval_stream_filter_remove_result(stream_filter, context, values) -} - -/// Removes an eval-local filter resource. -pub(in crate::interpreter) fn eval_stream_filter_remove_result( - stream_filter: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_stream_extension_resource_id(stream_filter, values)?; - values.bool_value(context.stream_resources_mut().close_filter_resource(id)) -} - -/// Evaluates `stream_bucket_new(stream, buffer)`. -pub(in crate::interpreter) fn eval_builtin_stream_bucket_new( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, buffer] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let buffer = eval_expr(buffer, context, scope, values)?; - eval_stream_bucket_new_result(stream, buffer, context, values) -} - -/// Creates a stdClass-backed stream bucket object. -pub(in crate::interpreter) fn eval_stream_bucket_new_result( - stream: RuntimeCellHandle, - buffer: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let stream_id = eval_stream_extension_resource_id(stream, values)?; - if !context.stream_resources().has_stream(stream_id) { - return values.null(); - } - let bytes = values.string_bytes(buffer)?; - let bucket = values.new_object("stdClass")?; - let data = values.string_bytes_value(&bytes)?; - let datalen = values.int(i64::try_from(bytes.len()).map_err(|_| EvalStatus::RuntimeFatal)?)?; - values.property_set(bucket, "data", data)?; - values.property_set(bucket, "datalen", datalen)?; - Ok(bucket) -} - -/// Evaluates `stream_bucket_make_writeable(brigade)`. -pub(in crate::interpreter) fn eval_builtin_stream_bucket_make_writeable( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [brigade] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let brigade = eval_expr(brigade, context, scope, values)?; - eval_stream_bucket_make_writeable_result(brigade, values) -} - -/// Returns the first bucket in a brigade, or null when none exists. -pub(in crate::interpreter) fn eval_stream_bucket_make_writeable_result( - brigade: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let buckets = values.property_get(brigade, "_buckets")?; - if !values.is_array_like(buckets)? || values.array_len(buckets)? == 0 { - return values.null(); - } - let key = values.int(0)?; - values.array_get(buckets, key) -} - -/// Evaluates `stream_bucket_append()` or `stream_bucket_prepend()`. -pub(in crate::interpreter) fn eval_builtin_stream_bucket_push( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [brigade, bucket] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let brigade = eval_expr(brigade, context, scope, values)?; - let bucket = eval_expr(bucket, context, scope, values)?; - eval_stream_bucket_push_result(name, brigade, bucket, values) -} - -/// Adds a bucket object to the brigade's `_buckets` array. -pub(in crate::interpreter) fn eval_stream_bucket_push_result( - name: &str, - brigade: RuntimeCellHandle, - bucket: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let prepend = match name { - "stream_bucket_append" => false, - "stream_bucket_prepend" => true, - _ => return Err(EvalStatus::RuntimeFatal), - }; - let buckets = eval_brigade_buckets(brigade, values)?; - let buckets = if prepend { - eval_bucket_prepend(buckets, bucket, values)? - } else { - let len = values.array_len(buckets)?; - let index = values.int(i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?)?; - values.array_set(buckets, index, bucket)? - }; - values.property_set(brigade, "_buckets", buckets)?; - values.null() -} - -/// Returns an existing brigade bucket array or creates an empty one. -fn eval_brigade_buckets( - brigade: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let buckets = values.property_get(brigade, "_buckets")?; - if values.is_array_like(buckets)? { - Ok(buckets) - } else { - values.array_new(0) - } -} - -/// Builds a new bucket array with the provided bucket at index zero. -fn eval_bucket_prepend( - buckets: RuntimeCellHandle, - bucket: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(buckets)?; - let mut result = values.array_new(len + 1)?; - let zero = values.int(0)?; - result = values.array_set(result, zero, bucket)?; - for index in 0..len { - let old_key = values.int(i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?)?; - let value = values.array_get(buckets, old_key)?; - let new_key = - values.int(i64::try_from(index + 1).map_err(|_| EvalStatus::RuntimeFatal)?)?; - result = values.array_set(result, new_key, value)?; - } - Ok(result) -} - -/// Converts a runtime resource cell into eval's zero-based stream-extension id. -fn eval_stream_extension_resource_id( - resource: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(resource)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(resource, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs index d805bc178b..21466a1308 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_filter_append`. +//! Declarative eval registry entry and implementation for `stream_filter_append`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream filter attachment helper. +//! - Creates eval-local filter resources without transforming stream bytes. use super::super::spec::EvalBuiltinDefaultValue; @@ -24,21 +24,58 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_filter_append` filesystem builtin through the area dispatcher. +/// Evaluates `stream_filter_append($stream, $filtername, $read_write = 3, $params = null)`. pub(in crate::interpreter) fn eval_stream_filter_append_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_filter_append", args, context, scope, values) + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let filter_name = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_filter_attach_result("stream_filter_append", stream, filter_name, context, values) } -/// Dispatches evaluated-argument calls for the `stream_filter_append` filesystem builtin through the area dispatcher. +/// Appends a filter from already evaluated stream filter arguments. pub(in crate::interpreter) fn eval_stream_filter_append_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_filter_append", evaluated_args, context, values) + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_filter_attach_result( + "stream_filter_append", + evaluated_args[0], + evaluated_args[1], + context, + values, + ) +} + +/// Creates an eval-local filter resource for a materialized stream filter attach. +pub(in crate::interpreter) fn eval_stream_filter_attach_result( + name: &str, + stream: RuntimeCellHandle, + filter_name: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(name, "stream_filter_append" | "stream_filter_prepend") { + return Err(EvalStatus::RuntimeFatal); + } + let stream_id = super::stream_bucket_new::eval_stream_extension_resource_id(stream, values)?; + let _ = values.string_bytes(filter_name)?; + if !context.stream_resources().has_stream(stream_id) { + return values.bool_value(false); + } + let filter_id = context.stream_resources_mut().open_filter_resource(); + values.resource(filter_id) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs index 5fa08f58ac..639f620b70 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_filter_prepend`. +//! Declarative eval registry entry and implementation for `stream_filter_prepend`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream filter attachment helper. +//! - Creates eval-local filter resources without transforming stream bytes. use super::super::spec::EvalBuiltinDefaultValue; @@ -24,21 +24,44 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_filter_prepend` filesystem builtin through the area dispatcher. +/// Evaluates `stream_filter_prepend($stream, $filtername, $read_write = 3, $params = null)`. pub(in crate::interpreter) fn eval_stream_filter_prepend_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_filter_prepend", args, context, scope, values) + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let filter_name = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + super::stream_filter_append::eval_stream_filter_attach_result( + "stream_filter_prepend", + stream, + filter_name, + context, + values, + ) } -/// Dispatches evaluated-argument calls for the `stream_filter_prepend` filesystem builtin through the area dispatcher. +/// Prepends a filter from already evaluated stream filter arguments. pub(in crate::interpreter) fn eval_stream_filter_prepend_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_filter_prepend", evaluated_args, context, values) + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + super::stream_filter_append::eval_stream_filter_attach_result( + "stream_filter_prepend", + evaluated_args[0], + evaluated_args[1], + context, + values, + ) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs index 18e4da5978..05a1a00fdf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_filter_register`. +//! Declarative eval registry entry and implementation for `stream_filter_register`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the conservative filter registry helper. +//! - Eval conservatively accepts registrations without mutating stream bytes. eval_builtin! { name: "stream_filter_register", @@ -17,21 +17,40 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_filter_register` filesystem builtin through the area dispatcher. +/// Evaluates `stream_filter_register($filter_name, $class)`. pub(in crate::interpreter) fn eval_stream_filter_register_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_filter_register", args, context, scope, values) + let [filter_name, class] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let filter_name = eval_expr(filter_name, context, scope, values)?; + let class = eval_expr(class, context, scope, values)?; + eval_stream_filter_register_result(filter_name, class, values) } -/// Dispatches evaluated-argument calls for the `stream_filter_register` filesystem builtin through the area dispatcher. +/// Registers an already evaluated stream filter name and class pair. pub(in crate::interpreter) fn eval_stream_filter_register_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [filter_name, class] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_filter_register_result(*filter_name, *class, values) +} + +/// Evaluates a materialized `stream_filter_register()` call. +pub(in crate::interpreter) fn eval_stream_filter_register_result( + filter_name: RuntimeCellHandle, + class: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_filter_register", evaluated_args, context, values) + let _ = values.string_bytes(filter_name)?; + let _ = values.string_bytes(class)?; + values.bool_value(true) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs index 28872fb103..7d7757304b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_filter_remove`. +//! Declarative eval registry entry and implementation for `stream_filter_remove`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream filter removal helper. +//! - Closes eval-local filter resources created by append/prepend. eval_builtin! { name: "stream_filter_remove", @@ -17,21 +17,38 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_filter_remove` filesystem builtin through the area dispatcher. +/// Evaluates `stream_filter_remove($stream_filter)`. pub(in crate::interpreter) fn eval_stream_filter_remove_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_filter_remove", args, context, scope, values) + let [stream_filter] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream_filter = eval_expr(stream_filter, context, scope, values)?; + eval_stream_filter_remove_result(stream_filter, context, values) } -/// Dispatches evaluated-argument calls for the `stream_filter_remove` filesystem builtin through the area dispatcher. +/// Removes an already evaluated eval-local filter resource. pub(in crate::interpreter) fn eval_stream_filter_remove_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_filter_remove", evaluated_args, context, values) + let [stream_filter] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_filter_remove_result(*stream_filter, context, values) +} + +/// Removes an eval-local filter resource. +pub(in crate::interpreter) fn eval_stream_filter_remove_result( + stream_filter: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_bucket_new::eval_stream_extension_resource_id(stream_filter, values)?; + values.bool_value(context.stream_resources_mut().close_filter_resource(id)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs index 587128e7ed..8692ec8662 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs @@ -33,48 +33,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value let prompt = evaluated_args.first().copied(); eval_readline_result(prompt, values)? } - "stream_bucket_new" => { - let [stream, buffer] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_bucket_new_result(*stream, *buffer, context, values)? - } - "stream_bucket_make_writeable" => { - let [brigade] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_bucket_make_writeable_result(*brigade, values)? - } - "stream_bucket_append" | "stream_bucket_prepend" => { - let [brigade, bucket] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_bucket_push_result(name, *brigade, *bucket, values)? - } - "stream_filter_register" => { - let [filter_name, class] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_filter_register_result(*filter_name, *class, values)? - } - "stream_filter_append" | "stream_filter_prepend" => { - if !(2..=4).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_filter_attach_result( - name, - evaluated_args[0], - evaluated_args[1], - context, - values, - )? - } - "stream_filter_remove" => { - let [stream_filter] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_filter_remove_result(*stream_filter, context, values)? - } "stream_select" => { eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; eval_stream_select_result(evaluated_args, context, values)? @@ -153,9 +111,6 @@ pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_value }; eval_stream_socket_pair_result(context, values)? } - "stream_wrapper_register" | "stream_wrapper_unregister" | "stream_wrapper_restore" => { - eval_stream_wrapper_registry_result(name, evaluated_args, context, values)? - } _ => return Ok(None), }; Ok(Some(result)) diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs index fdce45ca68..a2218111a1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_wrapper_register`. +//! Declarative eval registry entry and implementation for `stream_wrapper_register`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream wrapper registry helper. +//! - Registers protocols in the eval stream wrapper registry. use super::super::spec::EvalBuiltinDefaultValue; @@ -23,21 +23,68 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_wrapper_register` filesystem builtin through the area dispatcher. +/// Evaluates `stream_wrapper_register($protocol, $class, $flags = 0)`. pub(in crate::interpreter) fn eval_stream_wrapper_register_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_wrapper_register", args, context, scope, values) + if !(2..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_stream_wrapper_register_result(&evaluated_args, context, values) } -/// Dispatches evaluated-argument calls for the `stream_wrapper_register` filesystem builtin through the area dispatcher. +/// Registers an already evaluated stream wrapper protocol and class. pub(in crate::interpreter) fn eval_stream_wrapper_register_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_wrapper_register", evaluated_args, context, values) + eval_stream_wrapper_register_result(evaluated_args, context, values) +} + +/// Registers a materialized stream wrapper protocol and class. +pub(in crate::interpreter) fn eval_stream_wrapper_register_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(2..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let protocol = eval_stream_wrapper_protocol(evaluated_args[0], values)?; + let class_name = eval_stream_wrapper_class(evaluated_args[1], context, values)?; + values.bool_value(context.stream_resources_mut().register_stream_wrapper( + &protocol, + &class_name, + EVAL_STREAM_WRAPPERS, + )) +} + +/// Coerces one stream wrapper protocol argument into an owned string. +pub(in crate::interpreter) fn eval_stream_wrapper_protocol( + protocol: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(protocol)?; + Ok(String::from_utf8_lossy(&bytes).into_owned()) +} + +/// Coerces one stream wrapper class argument into a resolved class-name string. +fn eval_stream_wrapper_class( + class_name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(class_name)?; + let class_name = String::from_utf8_lossy(&bytes).into_owned(); + Ok(context + .resolve_class_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs index f1c37e2b69..8ea767eecc 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_wrapper_restore`. +//! Declarative eval registry entry and implementation for `stream_wrapper_restore`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream wrapper registry helper. +//! - Restores protocols in the eval stream wrapper registry. eval_builtin! { name: "stream_wrapper_restore", @@ -17,21 +17,42 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_wrapper_restore` filesystem builtin through the area dispatcher. +/// Evaluates `stream_wrapper_restore($protocol)`. pub(in crate::interpreter) fn eval_stream_wrapper_restore_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_wrapper_restore", args, context, scope, values) + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_stream_wrapper_restore_result(protocol, context, values) } -/// Dispatches evaluated-argument calls for the `stream_wrapper_restore` filesystem builtin through the area dispatcher. +/// Restores an already evaluated stream wrapper protocol. pub(in crate::interpreter) fn eval_stream_wrapper_restore_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_wrapper_restore", evaluated_args, context, values) + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_wrapper_restore_result(*protocol, context, values) +} + +/// Restores a materialized stream wrapper protocol. +pub(in crate::interpreter) fn eval_stream_wrapper_restore_result( + protocol: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = super::stream_wrapper_register::eval_stream_wrapper_protocol(protocol, values)?; + values.bool_value( + context + .stream_resources_mut() + .restore_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS), + ) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs index c073a17ff8..b8647cae1f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_wrapper_unregister`. +//! Declarative eval registry entry and implementation for `stream_wrapper_unregister`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the stream wrapper registry helper. +//! - Unregisters protocols in the eval stream wrapper registry. eval_builtin! { name: "stream_wrapper_unregister", @@ -17,21 +17,42 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_wrapper_unregister` filesystem builtin through the area dispatcher. +/// Evaluates `stream_wrapper_unregister($protocol)`. pub(in crate::interpreter) fn eval_stream_wrapper_unregister_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_wrapper_unregister", args, context, scope, values) + let [protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let protocol = eval_expr(protocol, context, scope, values)?; + eval_stream_wrapper_unregister_result(protocol, context, values) } -/// Dispatches evaluated-argument calls for the `stream_wrapper_unregister` filesystem builtin through the area dispatcher. +/// Unregisters an already evaluated stream wrapper protocol. pub(in crate::interpreter) fn eval_stream_wrapper_unregister_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_wrapper_unregister", evaluated_args, context, values) + let [protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_wrapper_unregister_result(*protocol, context, values) +} + +/// Unregisters a materialized stream wrapper protocol. +pub(in crate::interpreter) fn eval_stream_wrapper_unregister_result( + protocol: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let protocol = super::stream_wrapper_register::eval_stream_wrapper_protocol(protocol, values)?; + values.bool_value( + context + .stream_resources_mut() + .unregister_stream_wrapper(&protocol, EVAL_STREAM_WRAPPERS), + ) } From a8bb963f4bb85e62cc68dc3a34e50c2918d0e5bb Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:01:44 +0200 Subject: [PATCH 1125/1208] refactor(magician): move stream socket builtins home --- .../builtins/filesystem/direct_dispatch.rs | 14 -- .../builtins/filesystem/fsockopen.rs | 162 +++++++++++++++++- .../interpreter/builtins/filesystem/mod.rs | 16 +- .../builtins/filesystem/pfsockopen.rs | 30 +++- .../builtins/filesystem/readline.rs | 19 +- .../builtins/filesystem/stream_select.rs | 145 +++++++++++++++- .../filesystem/stream_socket_accept.rs | 84 ++++++++- .../filesystem/stream_socket_client.rs | 33 +++- .../filesystem/stream_socket_enable_crypto.rs | 39 ++++- .../filesystem/stream_socket_get_name.rs | 48 +++++- .../builtins/filesystem/stream_socket_pair.rs | 38 +++- .../filesystem/stream_socket_recvfrom.rs | 91 +++++++++- .../filesystem/stream_socket_sendto.rs | 33 +++- .../filesystem/stream_socket_server.rs | 33 +++- .../filesystem/stream_socket_shutdown.rs | 37 +++- .../builtins/filesystem/stream_sockets.rs | 26 --- .../filesystem/stream_sockets/common.rs | 51 ------ .../filesystem/stream_sockets/datagrams.rs | 86 ---------- .../filesystem/stream_sockets/lifecycle.rs | 157 ----------------- .../filesystem/stream_sockets/open.rs | 142 --------------- .../filesystem/stream_sockets/selection.rs | 160 ----------------- .../filesystem/stream_values_dispatch.rs | 153 ----------------- .../builtins/filesystem/user_wrapper_cast.rs | 2 +- .../builtins/filesystem/values_dispatch.rs | 6 - 24 files changed, 728 insertions(+), 877 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/common.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/lifecycle.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/open.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/selection.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index a98ff07ffc..b7a9092201 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -161,22 +161,8 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( match name { "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), - "readline" => eval_builtin_readline(args, context, scope, values), "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "stream_socket_client" => eval_builtin_stream_socket_client(args, context, scope, values), - "stream_socket_enable_crypto" => { - eval_builtin_stream_socket_enable_crypto(args, context, scope, values) - } - "stream_socket_get_name" => { - eval_builtin_stream_socket_get_name(args, context, scope, values) - } - "stream_socket_pair" => eval_builtin_stream_socket_pair(args, context, scope, values), - "stream_socket_sendto" => eval_builtin_stream_socket_sendto(args, context, scope, values), - "stream_socket_server" => eval_builtin_stream_socket_server(args, context, scope, values), - "stream_socket_shutdown" => { - eval_builtin_stream_socket_shutdown(args, context, scope, values) - } "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), _ => Err(EvalStatus::RuntimeFatal), } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs index 84e97e27bf..ac98708234 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `fsockopen`. +//! Declarative eval registry entry and implementation for `fsockopen`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` for by-reference outputs. //! //! Key details: //! - Direct calls keep their source-sensitive by-reference error-output path. +//! - `pfsockopen` shares this implementation because eval has no persistent sockets. use super::super::spec::EvalBuiltinDefaultValue; @@ -25,22 +27,172 @@ eval_builtin! { } use super::super::super::*; +use super::*; -/// Dispatches direct eval calls for the `fsockopen` filesystem builtin through the area dispatcher. +/// Evaluates a positional `fsockopen()` call without writable error outputs. pub(in crate::interpreter) fn eval_fsockopen_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fsockopen", args, context, scope, values) + if !(2..=5).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let host = eval_expr(&args[0], context, scope, values)?; + let port = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + eval_fsockopen_by_value_ref_warnings("fsockopen", args.len(), values)?; + eval_fsockopen_result(host, port, context, values) } -/// Dispatches evaluated-argument calls for the `fsockopen` filesystem builtin through the area dispatcher. +/// Evaluates a by-value `fsockopen()` call from already evaluated arguments. pub(in crate::interpreter) fn eval_fsockopen_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fsockopen", evaluated_args, context, values) + if !(2..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_fsockopen_by_value_ref_warnings("fsockopen", evaluated_args.len(), values)?; + eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Evaluates `fsockopen()` or `pfsockopen()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_fsockopen_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["hostname", "port", "error_code", "error_message", "timeout"], + &evaluated_args, + false, + )?; + let host = required_evaluated_ref_arg(&bound, 0)?; + let port = required_evaluated_ref_arg(&bound, 1)?; + let error_code_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let error_message_target = optional_evaluated_ref_arg(&bound, 3) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, error_code, error_message) = + eval_fsockopen_with_error_result(host.value, port.value, context, values)?; + eval_write_socket_int_output_ref_target( + error_code_target.as_ref(), + error_code, + context, + values, + )?; + eval_write_socket_output_ref_target( + error_message_target.as_ref(), + Some(error_message), + context, + values, + )?; + Ok(result) +} + +/// Opens a connected TCP stream from host and port cells. +pub(in crate::interpreter) fn eval_fsockopen_result( + host: RuntimeCellHandle, + port: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let host = eval_path_string(host, values)?; + let port = eval_int_value(port, values)?; + match context + .stream_resources_mut() + .open_tcp_stream_host_port(&host, port) + { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Opens a host/port TCP stream and returns PHP `fsockopen()` error outputs. +pub(in crate::interpreter) fn eval_fsockopen_with_error_result( + host: RuntimeCellHandle, + port: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, i64, String), EvalStatus> { + let host = eval_path_string(host, values)?; + let port = eval_int_value(port, values)?; + match context + .stream_resources_mut() + .open_tcp_stream_host_port_result(&host, port) + { + Ok(id) => Ok((values.resource(id)?, 0, String::new())), + Err(error) => { + let error_code = i64::from(error.raw_os_error().unwrap_or(0)); + Ok((values.bool_value(false)?, error_code, error.to_string())) + } + } +} + +/// Emits PHP by-reference warnings for by-value socket error outputs. +pub(in crate::interpreter) fn eval_fsockopen_by_value_ref_warnings( + name: &str, + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if supplied_count >= 3 { + values.warning(&format!( + "{name}(): Argument #3 ($error_code) must be passed by reference, value given" + ))?; + } + if supplied_count >= 4 { + values.warning(&format!( + "{name}(): Argument #4 ($error_message) must be passed by reference, value given" + ))?; + } + Ok(()) +} + +/// Writes a socket output string to a captured by-reference target when available. +pub(in crate::interpreter) fn eval_write_socket_output_ref_target( + target: Option<&EvalReferenceTarget>, + value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some((target, value)) = target.zip(value) else { + return Ok(()); + }; + let value = values.string(&value)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) +} + +/// Writes a socket output integer to a captured by-reference target when available. +pub(in crate::interpreter) fn eval_write_socket_int_output_ref_target( + target: Option<&EvalReferenceTarget>, + value: i64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(target) = target else { + return Ok(()); + }; + let value = values.int(value)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 07903aad9a..29f0392c03 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -127,8 +127,6 @@ mod stream_socket_recvfrom; mod stream_socket_sendto; mod stream_socket_server; mod stream_socket_shutdown; -mod stream_sockets; -mod stream_values_dispatch; mod stream_wrapper_register; mod stream_wrapper_restore; mod stream_wrapper_unregister; @@ -155,8 +153,6 @@ mod vfprintf; pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use path::*; -pub(in crate::interpreter) use readline::*; -pub(in crate::interpreter) use stream_sockets::*; pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_cast::*; pub(in crate::interpreter) use user_wrapper_controls::*; @@ -171,3 +167,15 @@ pub(in crate::interpreter) use user_wrapper_streams::*; pub(in crate::interpreter) use direct_dispatch::eval_builtin_filesystem_call; pub(in crate::interpreter) use values_dispatch::eval_filesystem_values_result; pub(in crate::interpreter) use flock::{eval_builtin_flock, eval_flock_result}; +pub(in crate::interpreter) use fsockopen::{ + eval_builtin_fsockopen_call, eval_fsockopen_with_error_result, +}; +pub(in crate::interpreter) use stream_select::{ + eval_builtin_stream_select_call, eval_stream_select_result, +}; +pub(in crate::interpreter) use stream_socket_accept::{ + eval_builtin_stream_socket_accept_call, eval_stream_socket_accept_with_peer_result, +}; +pub(in crate::interpreter) use stream_socket_recvfrom::{ + eval_builtin_stream_socket_recvfrom_call, eval_stream_socket_recvfrom_with_address_result, +}; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs index 274d44357a..687c71273f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `pfsockopen`. +//! Declarative eval registry entry and implementation for `pfsockopen`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` through the fsockopen path. //! //! Key details: -//! - Direct calls keep their source-sensitive by-reference error-output path. +//! - Eval has no persistent socket table, so runtime behavior delegates to `fsockopen`. use super::super::spec::EvalBuiltinDefaultValue; @@ -26,21 +27,38 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `pfsockopen` filesystem builtin through the area dispatcher. +/// Evaluates a positional `pfsockopen()` call without writable error outputs. pub(in crate::interpreter) fn eval_pfsockopen_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("pfsockopen", args, context, scope, values) + if !(2..=5).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let host = eval_expr(&args[0], context, scope, values)?; + let port = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + eval_expr(arg, context, scope, values)?; + } + super::fsockopen::eval_fsockopen_by_value_ref_warnings("pfsockopen", args.len(), values)?; + super::fsockopen::eval_fsockopen_result(host, port, context, values) } -/// Dispatches evaluated-argument calls for the `pfsockopen` filesystem builtin through the area dispatcher. +/// Evaluates a by-value `pfsockopen()` call from already evaluated arguments. pub(in crate::interpreter) fn eval_pfsockopen_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("pfsockopen", evaluated_args, context, values) + if !(2..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + super::fsockopen::eval_fsockopen_by_value_ref_warnings( + "pfsockopen", + evaluated_args.len(), + values, + )?; + super::fsockopen::eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs index df7b175bab..5f28f73087 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the host stdin helper. +//! - Reads from host stdin and returns false at EOF. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,28 +19,33 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `readline` filesystem builtin through the area dispatcher. +/// Evaluates `readline([prompt])`. pub(in crate::interpreter) fn eval_readline_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("readline", args, context, scope, values) + eval_builtin_readline(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `readline` filesystem builtin through the area dispatcher. +/// Evaluates `readline()` from already evaluated arguments. pub(in crate::interpreter) fn eval_readline_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("readline", evaluated_args, context, values) + let prompt = match evaluated_args { + [] => None, + [prompt] => Some(*prompt), + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_readline_result(prompt, values) } use std::io; -/// Evaluates `readline([prompt])`. +/// Evaluates `readline([prompt])` from unevaluated eval expressions. pub(in crate::interpreter) fn eval_builtin_readline( args: &[EvalExpr], context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs index b841aab25c..0f63f12342 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `stream_select`. +//! Declarative eval registry entry and implementation for `stream_select`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` for by-reference arrays. //! //! Key details: -//! - Direct calls keep their source-sensitive by-reference array path. +//! - `stream_select()` rewrites read/write/except arrays through by-reference +//! targets after validating resource handles. use super::super::spec::EvalBuiltinDefaultValue; @@ -25,22 +27,153 @@ eval_builtin! { } use super::super::super::*; +use super::*; -/// Dispatches direct eval calls for the `stream_select` filesystem builtin through the area dispatcher. +/// Evaluates a positional `stream_select()` call without writable array outputs. pub(in crate::interpreter) fn eval_stream_select_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_select", args, context, scope, values) + if !(4..=5).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; + eval_stream_select_result(&evaluated_args, context, values) } -/// Dispatches evaluated-argument calls for the `stream_select` filesystem builtin through the area dispatcher. +/// Evaluates `stream_select()` from already evaluated by-value arguments. pub(in crate::interpreter) fn eval_stream_select_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_select", evaluated_args, context, values) + eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; + eval_stream_select_result(evaluated_args, context, values) +} + +/// Evaluates `stream_select()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_select_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["read", "write", "except", "seconds", "microseconds"], + &evaluated_args, + false, + )?; + let read = required_evaluated_ref_arg(&bound, 0)?; + let write = required_evaluated_ref_arg(&bound, 1)?; + let except = required_evaluated_ref_arg(&bound, 2)?; + let seconds = required_evaluated_ref_arg(&bound, 3)?; + let targets = vec![ + read.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + write.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + except.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, + ]; + let mut selected_args = vec![read.value, write.value, except.value, seconds.value]; + if let Some(microseconds) = optional_evaluated_ref_arg(&bound, 4) { + selected_args.push(microseconds.value); + } + let result = eval_stream_select_result(&selected_args, context, values)?; + eval_write_stream_select_empty_arrays(&targets, context, values)?; + Ok(result) +} + +/// Evaluates materialized `stream_select(...)` arguments. +pub(in crate::interpreter) fn eval_stream_select_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(4..=5).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + for array in evaluated_args.iter().take(3) { + eval_stream_select_cast_array(*array, context, values)?; + } + values.int(0) +} + +/// Emits PHP by-reference warnings for by-value `stream_select()` array outputs. +fn eval_stream_select_by_value_ref_warnings( + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (index, param_name) in ["read", "write", "except"].iter().enumerate() { + if supplied_count <= index { + continue; + } + values.warning(&format!( + "stream_select(): Argument #{} (${param_name}) must be passed by reference, value given", + index + 1 + ))?; + } + Ok(()) +} + +/// Writes conservative empty readiness arrays back to `stream_select()` lvalues. +fn eval_write_stream_select_empty_arrays( + targets: &[EvalReferenceTarget], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for target in targets { + let value = values.array_new(0)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + Ok(()) +} + +/// Invokes `stream_cast(STREAM_CAST_FOR_SELECT)` for wrapper resources in an array. +fn eval_stream_select_cast_array( + array: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.is_array_like(array)? { + return Ok(()); + } + let len = values.array_len(array)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + eval_stream_select_cast_value(value, context, values)?; + } + Ok(()) +} + +/// Invokes `stream_cast()` for one userspace-wrapper stream resource value. +fn eval_stream_select_cast_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_RESOURCE { + return Ok(()); + } + let display_id = eval_int_value(value, values)?; + let Some(id) = display_id.checked_sub(1) else { + return Ok(()); + }; + let Some(result) = + eval_user_wrapper_stream_cast_result(id, EVAL_STREAM_CAST_FOR_SELECT, context, values)? + else { + return Ok(()); + }; + values.release(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs index 7b5514d69d..bb880c721d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs @@ -1,8 +1,9 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_accept`. +//! Declarative eval registry entry and implementation for `stream_socket_accept`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` for peer-name writeback. //! //! Key details: //! - Direct calls keep their source-sensitive by-reference peer-name path. @@ -24,21 +25,94 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_socket_accept` filesystem builtin through the area dispatcher. +/// Evaluates a positional `stream_socket_accept()` call without writable peer output. pub(in crate::interpreter) fn eval_stream_socket_accept_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_accept", args, context, scope, values) + if !(1..=3).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let socket = eval_expr(&args[0], context, scope, values)?; + for arg in &args[1..] { + eval_expr(arg, context, scope, values)?; + } + if args.len() >= 3 { + values.warning( + "stream_socket_accept(): Argument #3 ($peer_name) must be passed by reference, value given", + )?; + } + eval_stream_socket_accept_result(socket, context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_accept` filesystem builtin through the area dispatcher. +/// Accepts a socket from already evaluated by-value arguments. pub(in crate::interpreter) fn eval_stream_socket_accept_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_accept", evaluated_args, context, values) + if !(1..=3).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + if evaluated_args.len() >= 3 { + values.warning( + "stream_socket_accept(): Argument #3 ($peer_name) must be passed by reference, value given", + )?; + } + eval_stream_socket_accept_result(evaluated_args[0], context, values) +} + +/// Evaluates `stream_socket_accept()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_socket_accept_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = + bind_evaluated_ref_builtin_args(&["socket", "timeout", "peer_name"], &evaluated_args, false)?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let peer_name_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, peer_name) = + eval_stream_socket_accept_with_peer_result(socket.value, context, values)?; + super::fsockopen::eval_write_socket_output_ref_target( + peer_name_target.as_ref(), + peer_name, + context, + values, + )?; + Ok(result) +} + +/// Accepts one pending TCP connection from a listener resource. +pub(in crate::interpreter) fn eval_stream_socket_accept_result( + socket: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_socket_get_name::eval_socket_resource_id(socket, values)?; + match context.stream_resources_mut().accept_tcp(id) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } +} + +/// Accepts one TCP connection and returns the accepted resource plus peer endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_accept_with_peer_result( + socket: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let id = super::stream_socket_get_name::eval_socket_resource_id(socket, values)?; + let Some(accepted_id) = context.stream_resources_mut().accept_tcp(id) else { + return values.bool_value(false).map(|result| (result, None)); + }; + let peer_name = context.stream_resources().socket_name(accepted_id, true); + let result = values.resource(accepted_id)?; + Ok((result, peer_name)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs index e7df895137..f2a6a57f45 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_client`. +//! Declarative eval registry entry and implementation for `stream_socket_client`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the TCP client stream helper. +//! - Opened sockets enter eval's normal stream table. eval_builtin! { name: "stream_socket_client", @@ -16,22 +16,43 @@ eval_builtin! { } use super::super::super::*; +use super::*; -/// Dispatches direct eval calls for the `stream_socket_client` filesystem builtin through the area dispatcher. +/// Evaluates `stream_socket_client($address)`. pub(in crate::interpreter) fn eval_stream_socket_client_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_client", args, context, scope, values) + let [address] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_expr(address, context, scope, values)?; + eval_stream_socket_client_result(address, context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_client` filesystem builtin through the area dispatcher. +/// Opens a connected stream from an already evaluated address argument. pub(in crate::interpreter) fn eval_stream_socket_client_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_client", evaluated_args, context, values) + let [address] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_client_result(*address, context, values) +} + +/// Opens a connected TCP stream resource. +pub(in crate::interpreter) fn eval_stream_socket_client_result( + address: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_path_string(address, values)?; + match context.stream_resources_mut().open_tcp_stream(&address) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs index 38e2833d1b..12b849bdf8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_enable_crypto`. +//! Declarative eval registry entry and implementation for `stream_socket_enable_crypto`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the conservative TLS status helper. +//! - TLS enablement is conservative: disabling succeeds for valid streams while +//! enabling reports false because eval does not manage TLS state. use super::super::spec::EvalBuiltinDefaultValue; @@ -24,21 +25,47 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_socket_enable_crypto` filesystem builtin through the area dispatcher. +/// Evaluates `stream_socket_enable_crypto($stream, $enable, ...)`. pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_enable_crypto", args, context, scope, values) + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let enable = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_enable_crypto_result(stream, enable, context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_enable_crypto` filesystem builtin through the area dispatcher. +/// Evaluates crypto status from already evaluated stream crypto arguments. pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_enable_crypto", evaluated_args, context, values) + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_socket_enable_crypto_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Returns TLS enablement status for eval socket streams. +pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_result( + stream: RuntimeCellHandle, + enable: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_socket_get_name::eval_socket_resource_id(stream, values)?; + if !context.stream_resources().has_stream(id) { + return values.bool_value(false); + } + let disabled = !values.truthy(enable)?; + values.bool_value(disabled) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs index ebd3249151..b0e537456b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_get_name`. +//! Declarative eval registry entry and implementation for `stream_socket_get_name`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the socket-name lookup helper. +//! - Socket resources use eval's one-based displayed resource ids and zero-based +//! internal stream table ids. eval_builtin! { name: "stream_socket_get_name", @@ -17,21 +18,56 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_socket_get_name` filesystem builtin through the area dispatcher. +/// Evaluates `stream_socket_get_name($socket, $remote)`. pub(in crate::interpreter) fn eval_stream_socket_get_name_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_get_name", args, context, scope, values) + let [socket, remote] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let socket = eval_expr(socket, context, scope, values)?; + let remote = eval_expr(remote, context, scope, values)?; + eval_stream_socket_get_name_result(socket, remote, context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_get_name` filesystem builtin through the area dispatcher. +/// Returns a socket name for already evaluated socket and remote arguments. pub(in crate::interpreter) fn eval_stream_socket_get_name_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_get_name", evaluated_args, context, values) + let [socket, remote] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_get_name_result(*socket, *remote, context, values) +} + +/// Returns a tracked local or remote socket endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_get_name_result( + socket: RuntimeCellHandle, + remote: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = eval_socket_resource_id(socket, values)?; + let remote = values.truthy(remote)?; + match context.stream_resources().socket_name(id, remote) { + Some(name) => values.string(&name), + None => values.bool_value(false), + } +} + +/// Converts a runtime resource cell into eval's zero-based socket id. +pub(in crate::interpreter) fn eval_socket_resource_id( + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + let display_id = eval_int_value(resource, values)?; + display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs index 7f61198b4c..444166302e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_pair`. +//! Declarative eval registry entry and implementation for `stream_socket_pair`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the local socket-pair helper. +//! - Creates connected local stream resources in eval's stream resource table. eval_builtin! { name: "stream_socket_pair", @@ -17,21 +17,47 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_socket_pair` filesystem builtin through the area dispatcher. +/// Evaluates `stream_socket_pair($domain, $type, $protocol)`. pub(in crate::interpreter) fn eval_stream_socket_pair_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_pair", args, context, scope, values) + let [domain, socket_type, protocol] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let _ = eval_expr(domain, context, scope, values)?; + let _ = eval_expr(socket_type, context, scope, values)?; + let _ = eval_expr(protocol, context, scope, values)?; + eval_stream_socket_pair_result(context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_pair` filesystem builtin through the area dispatcher. +/// Creates a socket pair after validating already evaluated arguments. pub(in crate::interpreter) fn eval_stream_socket_pair_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_pair", evaluated_args, context, values) + let [_domain, _socket_type, _protocol] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_pair_result(context, values) +} + +/// Creates a pair of connected local stream resources. +pub(in crate::interpreter) fn eval_stream_socket_pair_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((left, right)) = context.stream_resources_mut().open_socket_pair() else { + return values.bool_value(false); + }; + let mut result = values.array_new(2)?; + let key = values.int(0)?; + let value = values.resource(left)?; + result = values.array_set(result, key, value)?; + let key = values.int(1)?; + let value = values.resource(right)?; + values.array_set(result, key, value) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs index 3c27c97a06..5a3101acf4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs @@ -1,11 +1,13 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_recvfrom`. +//! Declarative eval registry entry and implementation for `stream_socket_recvfrom`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. +//! - `crate::interpreter::expressions::eval_call()` for address writeback. //! //! Key details: -//! - Direct calls keep their source-sensitive by-reference address path. +//! - Reads delegate to `fread`, while optional address writeback uses tracked +//! remote endpoint metadata from eval's stream table. use super::super::spec::EvalBuiltinDefaultValue; @@ -25,21 +27,98 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_socket_recvfrom` filesystem builtin through the area dispatcher. +/// Evaluates a positional `stream_socket_recvfrom()` call without writable address output. pub(in crate::interpreter) fn eval_stream_socket_recvfrom_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_recvfrom", args, context, scope, values) + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let length = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + if args.len() >= 4 { + values.warning( + "stream_socket_recvfrom(): Argument #4 ($address) must be passed by reference, value given", + )?; + } + eval_stream_socket_recvfrom_result(stream, length, context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_recvfrom` filesystem builtin through the area dispatcher. +/// Reads bytes from already evaluated socket receive arguments. pub(in crate::interpreter) fn eval_stream_socket_recvfrom_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_recvfrom", evaluated_args, context, values) + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + if evaluated_args.len() >= 4 { + values.warning( + "stream_socket_recvfrom(): Argument #4 ($address) must be passed by reference, value given", + )?; + } + eval_stream_socket_recvfrom_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Evaluates `stream_socket_recvfrom()` over full eval call metadata. +pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + let (bound, _) = bind_evaluated_ref_builtin_args( + &["socket", "length", "flags", "address"], + &evaluated_args, + false, + )?; + let socket = required_evaluated_ref_arg(&bound, 0)?; + let length = required_evaluated_ref_arg(&bound, 1)?; + let address_target = optional_evaluated_ref_arg(&bound, 3) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + let (result, address) = eval_stream_socket_recvfrom_with_address_result( + socket.value, + length.value, + context, + values, + )?; + super::fsockopen::eval_write_socket_output_ref_target( + address_target.as_ref(), + address, + context, + values, + )?; + Ok(result) +} + +/// Reads bytes from a connected socket stream. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::fread::eval_fread_result(stream, length, context, values) +} + +/// Reads bytes from a connected socket stream and returns the tracked remote endpoint name. +pub(in crate::interpreter) fn eval_stream_socket_recvfrom_with_address_result( + stream: RuntimeCellHandle, + length: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let id = super::stream_socket_get_name::eval_socket_resource_id(stream, values)?; + let address = context.stream_resources().socket_name(id, true); + let result = super::fread::eval_fread_result(stream, length, context, values)?; + Ok((result, address)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs index 0af7e84be6..9485d2f3bf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_sendto`. +//! Declarative eval registry entry and implementation for `stream_socket_sendto`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the connected-socket write helper. +//! - Connected socket writes delegate to the same eval stream write path as `fwrite`. use super::super::spec::EvalBuiltinDefaultValue; @@ -24,21 +24,42 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_socket_sendto` filesystem builtin through the area dispatcher. +/// Evaluates `stream_socket_sendto($socket, $data, $flags = 0, $address = "")`. pub(in crate::interpreter) fn eval_stream_socket_sendto_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_sendto", args, context, scope, values) + if !(2..=4).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let stream = eval_expr(&args[0], context, scope, values)?; + let data = eval_expr(&args[1], context, scope, values)?; + for arg in &args[2..] { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_stream_socket_sendto_result(stream, data, context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_sendto` filesystem builtin through the area dispatcher. +/// Writes bytes from already evaluated socket send arguments. pub(in crate::interpreter) fn eval_stream_socket_sendto_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_sendto", evaluated_args, context, values) + if !(2..=4).contains(&evaluated_args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + eval_stream_socket_sendto_result(evaluated_args[0], evaluated_args[1], context, values) +} + +/// Writes bytes to a connected socket stream. +pub(in crate::interpreter) fn eval_stream_socket_sendto_result( + stream: RuntimeCellHandle, + data: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + super::fwrite::eval_fwrite_result(stream, data, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs index 26afeae52f..557989e9bb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_server`. +//! Declarative eval registry entry and implementation for `stream_socket_server`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the TCP listener helper. +//! - Opened listeners enter eval's normal stream table. eval_builtin! { name: "stream_socket_server", @@ -16,22 +16,43 @@ eval_builtin! { } use super::super::super::*; +use super::*; -/// Dispatches direct eval calls for the `stream_socket_server` filesystem builtin through the area dispatcher. +/// Evaluates `stream_socket_server($address)`. pub(in crate::interpreter) fn eval_stream_socket_server_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_server", args, context, scope, values) + let [address] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let address = eval_expr(address, context, scope, values)?; + eval_stream_socket_server_result(address, context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_server` filesystem builtin through the area dispatcher. +/// Opens a listener from an already evaluated address argument. pub(in crate::interpreter) fn eval_stream_socket_server_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_server", evaluated_args, context, values) + let [address] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_server_result(*address, context, values) +} + +/// Opens a TCP listener resource. +pub(in crate::interpreter) fn eval_stream_socket_server_result( + address: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let address = eval_path_string(address, values)?; + match context.stream_resources_mut().open_tcp_listener(&address) { + Some(id) => values.resource(id), + None => values.bool_value(false), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs index b3227c2798..328d4aeabe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `stream_socket_shutdown`. +//! Declarative eval registry entry and implementation for `stream_socket_shutdown`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the socket shutdown helper. +//! - Applies shutdown modes through eval's stream resource table. eval_builtin! { name: "stream_socket_shutdown", @@ -17,21 +17,46 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `stream_socket_shutdown` filesystem builtin through the area dispatcher. +/// Evaluates `stream_socket_shutdown($stream, $mode)`. pub(in crate::interpreter) fn eval_stream_socket_shutdown_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("stream_socket_shutdown", args, context, scope, values) + let [stream, mode] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let stream = eval_expr(stream, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_stream_socket_shutdown_result(stream, mode, context, values) } -/// Dispatches evaluated-argument calls for the `stream_socket_shutdown` filesystem builtin through the area dispatcher. +/// Shuts down an already evaluated socket stream argument. pub(in crate::interpreter) fn eval_stream_socket_shutdown_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("stream_socket_shutdown", evaluated_args, context, values) + let [stream, mode] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_stream_socket_shutdown_result(*stream, *mode, context, values) +} + +/// Applies a socket shutdown mode to a stream resource. +pub(in crate::interpreter) fn eval_stream_socket_shutdown_result( + stream: RuntimeCellHandle, + mode: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let id = super::stream_socket_get_name::eval_socket_resource_id(stream, values)?; + let mode = eval_int_value(mode, values)?; + values.bool_value( + context + .stream_resources() + .socket_shutdown(id, mode) + .unwrap_or(false), + ) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs deleted file mode 100644 index 6257039e54..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Purpose: -//! Orchestrates eval stream socket builtins over host TCP and local socket pairs. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - Concrete socket builtin behavior lives in focused `stream_sockets/` modules -//! so stream resource helpers and by-reference writeback stay isolated. - -use super::super::super::*; -use super::*; - -mod common; -mod datagrams; -mod lifecycle; -mod open; -mod selection; - -use common::*; -pub(in crate::interpreter) use datagrams::*; -pub(in crate::interpreter) use lifecycle::*; -pub(in crate::interpreter) use open::*; -pub(in crate::interpreter) use selection::*; -use selection::eval_socket_resource_id; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/common.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/common.rs deleted file mode 100644 index b3edc000f4..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/common.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Purpose: -//! Shared by-reference writeback helpers for stream socket builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::stream_sockets` submodules. -//! -//! Key details: -//! - Helpers write directly to captured eval reference targets and preserve owned -//! scope-cell replacement semantics. - -use super::*; - -/// Writes a socket output string to a captured by-reference target when available. -pub(super) fn eval_write_socket_output_ref_target( - target: Option<&EvalReferenceTarget>, - value: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some((target, value)) = target.zip(value) else { - return Ok(()); - }; - let value = values.string(&value)?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - ) -} - -/// Writes a socket output integer to a captured by-reference target when available. -pub(super) fn eval_write_socket_int_output_ref_target( - target: Option<&EvalReferenceTarget>, - value: i64, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(target) = target else { - return Ok(()); - }; - let value = values.int(value)?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - ) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs deleted file mode 100644 index 62669dacda..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/datagrams.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! Purpose: -//! Sends and receives data through eval stream socket resources. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::stream_sockets` re-exports. -//! -//! Key details: -//! - `stream_socket_recvfrom()` preserves optional by-reference address writeback -//! for both direct and dynamic callable dispatch. - -use super::*; - -/// Evaluates `stream_socket_sendto(stream, data, flags = 0, address = null)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_sendto( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let data = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - let _ = eval_expr(arg, context, scope, values)?; - } - eval_stream_socket_sendto_result(stream, data, context, values) -} - -/// Writes bytes to a connected socket stream. -pub(in crate::interpreter) fn eval_stream_socket_sendto_result( - stream: RuntimeCellHandle, - data: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - super::super::fwrite::eval_fwrite_result(stream, data, context, values) -} - -/// Evaluates `stream_socket_recvfrom()` over full eval call metadata. -pub(in crate::interpreter) fn eval_builtin_stream_socket_recvfrom_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let (bound, _) = bind_evaluated_ref_builtin_args( - &["socket", "length", "flags", "address"], - &evaluated_args, - false, - )?; - let socket = required_evaluated_ref_arg(&bound, 0)?; - let length = required_evaluated_ref_arg(&bound, 1)?; - let address_target = optional_evaluated_ref_arg(&bound, 3) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - let (result, address) = - eval_stream_socket_recvfrom_with_address_result(socket.value, length.value, context, values)?; - eval_write_socket_output_ref_target(address_target.as_ref(), address, context, values)?; - Ok(result) -} - -/// Reads bytes from a connected socket stream. -pub(in crate::interpreter) fn eval_stream_socket_recvfrom_result( - stream: RuntimeCellHandle, - length: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - super::super::fread::eval_fread_result(stream, length, context, values) -} - -/// Reads bytes from a connected socket stream and returns the tracked remote endpoint name. -pub(in crate::interpreter) fn eval_stream_socket_recvfrom_with_address_result( - stream: RuntimeCellHandle, - length: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, Option), EvalStatus> { - let id = eval_socket_resource_id(stream, values)?; - let address = context.stream_resources().socket_name(id, true); - let result = super::super::fread::eval_fread_result(stream, length, context, values)?; - Ok((result, address)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/lifecycle.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/lifecycle.rs deleted file mode 100644 index 5ad93c2898..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/lifecycle.rs +++ /dev/null @@ -1,157 +0,0 @@ -//! Purpose: -//! Accepts stream socket connections and exposes socket metadata/lifecycle calls. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::stream_sockets` re-exports. -//! -//! Key details: -//! - TLS enablement is conservative: disabling succeeds for valid streams while -//! enabling reports false because eval does not manage TLS state. - -use super::*; - -/// Evaluates `stream_socket_accept()` over full eval call metadata. -pub(in crate::interpreter) fn eval_builtin_stream_socket_accept_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let (bound, _) = bind_evaluated_ref_builtin_args( - &["socket", "timeout", "peer_name"], - &evaluated_args, - false, - )?; - let socket = required_evaluated_ref_arg(&bound, 0)?; - let peer_name_target = optional_evaluated_ref_arg(&bound, 2) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - let (result, peer_name) = - eval_stream_socket_accept_with_peer_result(socket.value, context, values)?; - eval_write_socket_output_ref_target(peer_name_target.as_ref(), peer_name, context, values)?; - Ok(result) -} - -/// Accepts one pending TCP connection from a listener resource. -pub(in crate::interpreter) fn eval_stream_socket_accept_result( - socket: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_socket_resource_id(socket, values)?; - match context.stream_resources_mut().accept_tcp(id) { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Accepts one TCP connection and returns the accepted resource plus peer endpoint name. -pub(in crate::interpreter) fn eval_stream_socket_accept_with_peer_result( - socket: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, Option), EvalStatus> { - let id = eval_socket_resource_id(socket, values)?; - let Some(accepted_id) = context.stream_resources_mut().accept_tcp(id) else { - return values.bool_value(false).map(|result| (result, None)); - }; - let peer_name = context.stream_resources().socket_name(accepted_id, true); - let result = values.resource(accepted_id)?; - Ok((result, peer_name)) -} - -/// Evaluates `stream_socket_get_name(socket, remote)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_get_name( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [socket, remote] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let socket = eval_expr(socket, context, scope, values)?; - let remote = eval_expr(remote, context, scope, values)?; - eval_stream_socket_get_name_result(socket, remote, context, values) -} - -/// Returns a tracked local or remote socket endpoint name. -pub(in crate::interpreter) fn eval_stream_socket_get_name_result( - socket: RuntimeCellHandle, - remote: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_socket_resource_id(socket, values)?; - let remote = values.truthy(remote)?; - match context.stream_resources().socket_name(id, remote) { - Some(name) => values.string(&name), - None => values.bool_value(false), - } -} - -/// Evaluates `stream_socket_shutdown(stream, mode)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_shutdown( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [stream, mode] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let stream = eval_expr(stream, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_stream_socket_shutdown_result(stream, mode, context, values) -} - -/// Applies a socket shutdown mode to a stream resource. -pub(in crate::interpreter) fn eval_stream_socket_shutdown_result( - stream: RuntimeCellHandle, - mode: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_socket_resource_id(stream, values)?; - let mode = eval_int_value(mode, values)?; - values.bool_value( - context - .stream_resources() - .socket_shutdown(id, mode) - .unwrap_or(false), - ) -} - -/// Evaluates `stream_socket_enable_crypto(...)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_enable_crypto( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(2..=4).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let stream = eval_expr(&args[0], context, scope, values)?; - let enable = eval_expr(&args[1], context, scope, values)?; - for arg in &args[2..] { - let _ = eval_expr(arg, context, scope, values)?; - } - eval_stream_socket_enable_crypto_result(stream, enable, context, values) -} - -/// Returns TLS enablement status for eval socket streams. -pub(in crate::interpreter) fn eval_stream_socket_enable_crypto_result( - stream: RuntimeCellHandle, - enable: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let id = eval_socket_resource_id(stream, values)?; - if !context.stream_resources().has_stream(id) { - return values.bool_value(false); - } - let disabled = !values.truthy(enable)?; - values.bool_value(disabled) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/open.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/open.rs deleted file mode 100644 index aad812d0c3..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/open.rs +++ /dev/null @@ -1,142 +0,0 @@ -//! Purpose: -//! Opens TCP stream socket resources for eval stream socket builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::stream_sockets` re-exports. -//! -//! Key details: -//! - Opened sockets enter eval's normal stream table so existing stream IO -//! helpers own reads, writes, and close behavior. - -use super::*; - -/// Evaluates `stream_socket_server(address)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_server( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [address] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let address = eval_expr(address, context, scope, values)?; - eval_stream_socket_server_result(address, context, values) -} - -/// Opens a TCP listener resource. -pub(in crate::interpreter) fn eval_stream_socket_server_result( - address: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_path_string(address, values)?; - match context.stream_resources_mut().open_tcp_listener(&address) { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Evaluates `stream_socket_client(address)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_client( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [address] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let address = eval_expr(address, context, scope, values)?; - eval_stream_socket_client_result(address, context, values) -} - -/// Opens a connected TCP stream resource. -pub(in crate::interpreter) fn eval_stream_socket_client_result( - address: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let address = eval_path_string(address, values)?; - match context.stream_resources_mut().open_tcp_stream(&address) { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Evaluates `fsockopen()` or `pfsockopen()` over full eval call metadata. -pub(in crate::interpreter) fn eval_builtin_fsockopen_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let (bound, _) = bind_evaluated_ref_builtin_args( - &["hostname", "port", "error_code", "error_message", "timeout"], - &evaluated_args, - false, - )?; - let host = required_evaluated_ref_arg(&bound, 0)?; - let port = required_evaluated_ref_arg(&bound, 1)?; - let error_code_target = optional_evaluated_ref_arg(&bound, 2) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - let error_message_target = optional_evaluated_ref_arg(&bound, 3) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - let (result, error_code, error_message) = - eval_fsockopen_with_error_result(host.value, port.value, context, values)?; - eval_write_socket_int_output_ref_target( - error_code_target.as_ref(), - error_code, - context, - values, - )?; - eval_write_socket_output_ref_target( - error_message_target.as_ref(), - Some(error_message), - context, - values, - )?; - Ok(result) -} - -/// Opens a connected TCP stream from host and port cells. -pub(in crate::interpreter) fn eval_fsockopen_result( - host: RuntimeCellHandle, - port: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let host = eval_path_string(host, values)?; - let port = eval_int_value(port, values)?; - match context - .stream_resources_mut() - .open_tcp_stream_host_port(&host, port) - { - Some(id) => values.resource(id), - None => values.bool_value(false), - } -} - -/// Opens a host/port TCP stream and returns PHP `fsockopen()` error outputs. -pub(in crate::interpreter) fn eval_fsockopen_with_error_result( - host: RuntimeCellHandle, - port: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, i64, String), EvalStatus> { - let host = eval_path_string(host, values)?; - let port = eval_int_value(port, values)?; - match context - .stream_resources_mut() - .open_tcp_stream_host_port_result(&host, port) - { - Ok(id) => Ok((values.resource(id)?, 0, String::new())), - Err(error) => { - let error_code = i64::from(error.raw_os_error().unwrap_or(0)); - Ok((values.bool_value(false)?, error_code, error.to_string())) - } - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/selection.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/selection.rs deleted file mode 100644 index 9b06440848..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_sockets/selection.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! Purpose: -//! Implements stream socket pairs and `stream_select()` over eval resources. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::stream_sockets` re-exports. -//! -//! Key details: -//! - `stream_select()` rewrites read/write/except arrays through by-reference -//! targets after validating resource handles. - -use super::*; - -/// Evaluates `stream_socket_pair(domain, type, protocol)`. -pub(in crate::interpreter) fn eval_builtin_stream_socket_pair( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [domain, socket_type, protocol] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let _ = eval_expr(domain, context, scope, values)?; - let _ = eval_expr(socket_type, context, scope, values)?; - let _ = eval_expr(protocol, context, scope, values)?; - eval_stream_socket_pair_result(context, values) -} - -/// Creates a pair of connected local stream resources. -pub(in crate::interpreter) fn eval_stream_socket_pair_result( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((left, right)) = context.stream_resources_mut().open_socket_pair() else { - return values.bool_value(false); - }; - let mut result = values.array_new(2)?; - let key = values.int(0)?; - let value = values.resource(left)?; - result = values.array_set(result, key, value)?; - let key = values.int(1)?; - let value = values.resource(right)?; - values.array_set(result, key, value) -} - -/// Evaluates `stream_select()` over full eval call metadata. -pub(in crate::interpreter) fn eval_builtin_stream_select_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - let (bound, _) = bind_evaluated_ref_builtin_args( - &["read", "write", "except", "seconds", "microseconds"], - &evaluated_args, - false, - )?; - let read = required_evaluated_ref_arg(&bound, 0)?; - let write = required_evaluated_ref_arg(&bound, 1)?; - let except = required_evaluated_ref_arg(&bound, 2)?; - let seconds = required_evaluated_ref_arg(&bound, 3)?; - let targets = vec![ - read.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, - write.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, - except.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)?, - ]; - let mut selected_args = vec![read.value, write.value, except.value, seconds.value]; - if let Some(microseconds) = optional_evaluated_ref_arg(&bound, 4) { - selected_args.push(microseconds.value); - } - let result = eval_stream_select_result(&selected_args, context, values)?; - eval_write_stream_select_empty_arrays(&targets, context, values)?; - Ok(result) -} - -/// Evaluates materialized `stream_select(...)` arguments. -pub(in crate::interpreter) fn eval_stream_select_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(4..=5).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - for array in evaluated_args.iter().take(3) { - eval_stream_select_cast_array(*array, context, values)?; - } - values.int(0) -} - -/// Writes conservative empty readiness arrays back to `stream_select()` lvalues. -fn eval_write_stream_select_empty_arrays( - targets: &[EvalReferenceTarget], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for target in targets { - let value = values.array_new(0)?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - Ok(()) -} - -/// Invokes `stream_cast(STREAM_CAST_FOR_SELECT)` for wrapper resources in an array. -fn eval_stream_select_cast_array( - array: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if !values.is_array_like(array)? { - return Ok(()); - } - let len = values.array_len(array)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - eval_stream_select_cast_value(value, context, values)?; - } - Ok(()) -} - -/// Invokes `stream_cast()` for one userspace-wrapper stream resource value. -fn eval_stream_select_cast_value( - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if values.type_tag(value)? != EVAL_TAG_RESOURCE { - return Ok(()); - } - let display_id = eval_int_value(value, values)?; - let Some(id) = display_id.checked_sub(1) else { - return Ok(()); - }; - let Some(result) = - eval_user_wrapper_stream_cast_result(id, EVAL_STREAM_CAST_FOR_SELECT, context, values)? - else { - return Ok(()); - }; - values.release(result) -} - -/// Converts a runtime resource cell into eval's zero-based socket id. -pub(super) fn eval_socket_resource_id( - resource: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(resource)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - let display_id = eval_int_value(resource, values)?; - display_id.checked_sub(1).ok_or(EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs deleted file mode 100644 index 8692ec8662..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_values_dispatch.rs +++ /dev/null @@ -1,153 +0,0 @@ -//! Purpose: -//! Evaluated-argument dispatch for declarative stream, socket, context, and CSV builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::values_dispatch`. -//! -//! Key details: -//! - By-value callable forms emit PHP-style warnings for parameters that are -//! normally by-reference outputs. - -use super::super::super::*; -use super::*; - -/// Attempts evaluated-argument dispatch for stream and socket builtins. -pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_stream_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "fsockopen" | "pfsockopen" => { - if !(2..=5).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - eval_fsockopen_by_value_ref_warnings(name, evaluated_args.len(), values)?; - eval_fsockopen_result(evaluated_args[0], evaluated_args[1], context, values)? - } - "readline" => { - if evaluated_args.len() > 1 { - return Err(EvalStatus::RuntimeFatal); - } - let prompt = evaluated_args.first().copied(); - eval_readline_result(prompt, values)? - } - "stream_select" => { - eval_stream_select_by_value_ref_warnings(evaluated_args.len(), values)?; - eval_stream_select_result(evaluated_args, context, values)? - } - "stream_socket_server" => { - let [address] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_socket_server_result(*address, context, values)? - } - "stream_socket_client" => { - let [address] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_socket_client_result(*address, context, values)? - } - "stream_socket_accept" => { - if !(1..=3).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - if evaluated_args.len() >= 3 { - values.warning( - "stream_socket_accept(): Argument #3 ($peer_name) must be passed by reference, value given", - )?; - } - eval_stream_socket_accept_result(evaluated_args[0], context, values)? - } - "stream_socket_get_name" => { - let [socket, remote] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_socket_get_name_result(*socket, *remote, context, values)? - } - "stream_socket_shutdown" => { - let [stream, mode] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_socket_shutdown_result(*stream, *mode, context, values)? - } - "stream_socket_enable_crypto" => { - if !(2..=4).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_socket_enable_crypto_result( - evaluated_args[0], - evaluated_args[1], - context, - values, - )? - } - "stream_socket_sendto" => { - if !(2..=4).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - eval_stream_socket_sendto_result(evaluated_args[0], evaluated_args[1], context, values)? - } - "stream_socket_recvfrom" => { - if !(2..=4).contains(&evaluated_args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - if evaluated_args.len() >= 4 { - values.warning( - "stream_socket_recvfrom(): Argument #4 ($address) must be passed by reference, value given", - )?; - } - eval_stream_socket_recvfrom_result( - evaluated_args[0], - evaluated_args[1], - context, - values, - )? - } - "stream_socket_pair" => { - let [_domain, _socket_type, _protocol] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_stream_socket_pair_result(context, values)? - } - _ => return Ok(None), - }; - Ok(Some(result)) -} - -/// Emits PHP by-reference warnings for by-value `fsockopen()` error outputs. -fn eval_fsockopen_by_value_ref_warnings( - name: &str, - supplied_count: usize, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if supplied_count >= 3 { - values.warning(&format!( - "{name}(): Argument #3 ($error_code) must be passed by reference, value given" - ))?; - } - if supplied_count >= 4 { - values.warning(&format!( - "{name}(): Argument #4 ($error_message) must be passed by reference, value given" - ))?; - } - Ok(()) -} - -/// Emits PHP by-reference warnings for by-value `stream_select()` array outputs. -fn eval_stream_select_by_value_ref_warnings( - supplied_count: usize, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for (index, param_name) in ["read", "write", "except"].iter().enumerate() { - if supplied_count <= index { - continue; - } - values.warning(&format!( - "stream_select(): Argument #{} (${param_name}) must be passed by reference, value given", - index + 1 - ))?; - } - Ok(()) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs index c34ef5d1ed..af39e7e07d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/user_wrapper_cast.rs @@ -2,7 +2,7 @@ //! Dispatches eval userspace stream wrappers to `stream_cast()`. //! //! Called from: -//! - `crate::interpreter::builtins::filesystem::stream_sockets`. +//! - `crate::interpreter::builtins::filesystem::stream_select`. //! //! Key details: //! - Magician keeps `stream_select()` conservative and returns no ready streams, diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs index 7143fd8955..5cdf30eed4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs @@ -11,7 +11,6 @@ use super::super::super::*; use super::path_values_dispatch::eval_filesystem_path_values_result; -use super::stream_values_dispatch::eval_filesystem_stream_values_result; /// Routes evaluated-argument filesystem builtin calls through per-builtin leaf wrappers. pub(in crate::interpreter) fn eval_filesystem_values_result( @@ -163,10 +162,5 @@ pub(in crate::interpreter) fn eval_filesystem_values_result_impl( { return Ok(result); } - if let Some(result) = - eval_filesystem_stream_values_result(name, evaluated_args, context, values)? - { - return Ok(result); - } Err(EvalStatus::RuntimeFatal) } From 7c27d4e42427f451989587c081319bdf286f0d92 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:04:16 +0200 Subject: [PATCH 1126/1208] refactor(magician): inline remaining filesystem fallback builtins --- .../builtins/filesystem/direct_dispatch.rs | 19 ------ .../builtins/filesystem/fnmatch.rs | 16 +++-- .../interpreter/builtins/filesystem/glob.rs | 6 +- .../interpreter/builtins/filesystem/mod.rs | 2 - .../filesystem/path_values_dispatch.rs | 44 ------------- .../builtins/filesystem/realpath_cache_get.rs | 39 ++++++++--- .../filesystem/realpath_cache_size.rs | 39 ++++++++--- .../builtins/filesystem/sys_get_temp_dir.rs | 39 ++++++++--- .../builtins/filesystem/values_dispatch.rs | 16 ----- .../interpreter/builtins/network_env/cache.rs | 65 ------------------- .../interpreter/builtins/network_env/mod.rs | 2 - 11 files changed, 105 insertions(+), 182 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/network_env/cache.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs index b7a9092201..ce4dbb5154 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/direct_dispatch.rs @@ -9,7 +9,6 @@ //! metadata lives in individual declaration files. use super::super::super::*; -use super::*; /// Routes direct expression-level filesystem builtin calls through per-builtin leaf wrappers. pub(in crate::interpreter) fn eval_builtin_filesystem_call( @@ -149,21 +148,3 @@ pub(in crate::interpreter) fn eval_builtin_filesystem_call( _ => Err(EvalStatus::RuntimeFatal), } } - -/// Dispatches direct expression-level calls for declaratively migrated filesystem builtins. -pub(in crate::interpreter) fn eval_builtin_filesystem_call_impl( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "basename" => super::basename::eval_basename_declared_call(args, context, scope, values), - "fnmatch" => eval_builtin_fnmatch(args, context, scope, values), - "realpath_cache_get" => eval_builtin_realpath_cache_get(args, values), - "realpath_cache_size" => eval_builtin_realpath_cache_size(args, values), - "sys_get_temp_dir" => eval_builtin_sys_get_temp_dir(args, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs index 973bb8963c..36a8e5ccf4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the fnmatch helper. +//! - Implements the eval-supported shell glob grammar and flags locally. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,23 +19,27 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `fnmatch` filesystem builtin through the area dispatcher. +/// Evaluates `fnmatch($pattern, $filename, $flags = 0)`. pub(in crate::interpreter) fn eval_fnmatch_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("fnmatch", args, context, scope, values) + eval_builtin_fnmatch(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `fnmatch` filesystem builtin through the area dispatcher. +/// Evaluates `fnmatch()` from already evaluated arguments. pub(in crate::interpreter) fn eval_fnmatch_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("fnmatch", evaluated_args, context, values) + match evaluated_args { + [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values), + [pattern, filename, flags] => eval_fnmatch_result(*pattern, *filename, Some(*flags), values), + _ => Err(EvalStatus::RuntimeFatal), + } } use super::super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs index 4e88040f84..e5cb5d9298 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs @@ -131,7 +131,11 @@ pub(in crate::interpreter) fn eval_glob_collect( } names.sort(); for name in names { - if !eval_fnmatch_bytes(component.as_bytes(), name.as_bytes(), EVAL_FNM_PERIOD) { + if !super::fnmatch::eval_fnmatch_bytes( + component.as_bytes(), + name.as_bytes(), + EVAL_FNM_PERIOD, + ) { continue; } let next_base = base.join(&name); diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs index 29f0392c03..774fad9725 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/mod.rs @@ -73,7 +73,6 @@ mod lstat; mod mkdir; mod opendir; mod path; -mod path_values_dispatch; mod pathinfo; mod pclose; mod pfsockopen; @@ -151,7 +150,6 @@ mod user_wrapper_streams; mod values_dispatch; mod vfprintf; -pub(in crate::interpreter) use fnmatch::*; pub(in crate::interpreter) use path::*; pub(in crate::interpreter) use streams::*; pub(in crate::interpreter) use user_wrapper_cast::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs deleted file mode 100644 index c15508ed4f..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/path_values_dispatch.rs +++ /dev/null @@ -1,44 +0,0 @@ -//! Purpose: -//! Evaluated-argument dispatch for declarative path, file, directory, and stat builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::filesystem::values_dispatch`. -//! -//! Key details: -//! - This module owns by-value dispatch for filesystem helpers that do not work -//! with stream resource cursor state. - -use super::super::super::*; -use super::*; - -/// Attempts evaluated-argument dispatch for path and file builtins. -pub(in crate::interpreter::builtins::filesystem) fn eval_filesystem_path_values_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - _context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let result = match name { - "fnmatch" => match evaluated_args { - [pattern, filename] => eval_fnmatch_result(*pattern, *filename, None, values)?, - [pattern, filename, flags] => { - eval_fnmatch_result(*pattern, *filename, Some(*flags), values)? - } - _ => return Err(EvalStatus::RuntimeFatal), - }, - "realpath_cache_get" => match evaluated_args { - [] => eval_realpath_cache_get_result(values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "realpath_cache_size" => match evaluated_args { - [] => eval_realpath_cache_size_result(values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - "sys_get_temp_dir" => match evaluated_args { - [] => eval_sys_get_temp_dir_result(values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }, - _ => return Ok(None), - }; - Ok(Some(result)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs index 79e84c68ce..39643e3d0f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `realpath_cache_get`. +//! Declarative eval registry entry and implementation for `realpath_cache_get`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through elephc's empty realpath-cache helper. +//! - Eval does not maintain a PHP realpath cache, so this returns a stable empty array. eval_builtin! { name: "realpath_cache_get", @@ -17,21 +17,42 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `realpath_cache_get` filesystem builtin through the area dispatcher. +/// Evaluates `realpath_cache_get()` with no arguments. pub(in crate::interpreter) fn eval_realpath_cache_get_declared_call( args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("realpath_cache_get", args, context, scope, values) + eval_builtin_realpath_cache_get(args, values) } -/// Dispatches evaluated-argument calls for the `realpath_cache_get` filesystem builtin through the area dispatcher. +/// Evaluates `realpath_cache_get()` from already evaluated arguments. pub(in crate::interpreter) fn eval_realpath_cache_get_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("realpath_cache_get", evaluated_args, context, values) + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values) +} + +/// Evaluates PHP `realpath_cache_get()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_realpath_cache_get( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_get_result(values) +} + +/// Returns elephc's intentionally empty realpath-cache view. +pub(in crate::interpreter) fn eval_realpath_cache_get_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.array_new(0) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs index 2091db0299..7f0a98e29b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `realpath_cache_size`. +//! Declarative eval registry entry and implementation for `realpath_cache_size`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through elephc's empty realpath-cache helper. +//! - Eval does not maintain a PHP realpath cache, so this returns zero. eval_builtin! { name: "realpath_cache_size", @@ -17,21 +17,42 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `realpath_cache_size` filesystem builtin through the area dispatcher. +/// Evaluates `realpath_cache_size()` with no arguments. pub(in crate::interpreter) fn eval_realpath_cache_size_declared_call( args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("realpath_cache_size", args, context, scope, values) + eval_builtin_realpath_cache_size(args, values) } -/// Dispatches evaluated-argument calls for the `realpath_cache_size` filesystem builtin through the area dispatcher. +/// Evaluates `realpath_cache_size()` from already evaluated arguments. pub(in crate::interpreter) fn eval_realpath_cache_size_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("realpath_cache_size", evaluated_args, context, values) + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values) +} + +/// Evaluates PHP `realpath_cache_size()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_realpath_cache_size( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_realpath_cache_size_result(values) +} + +/// Returns zero because elephc does not maintain a runtime realpath cache. +pub(in crate::interpreter) fn eval_realpath_cache_size_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.int(0) } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs index 31c7398f6c..789b1d91c2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `sys_get_temp_dir`. +//! Declarative eval registry entry and implementation for `sys_get_temp_dir`. //! //! Called from: //! - `crate::interpreter::builtins::filesystem`. //! //! Key details: -//! - Runtime dispatch is declared here and delegated through the temporary-directory helper. +//! - Returns the same temporary directory literal as the native static builtin. eval_builtin! { name: "sys_get_temp_dir", @@ -17,21 +17,42 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `sys_get_temp_dir` filesystem builtin through the area dispatcher. +/// Evaluates `sys_get_temp_dir()` with no arguments. pub(in crate::interpreter) fn eval_sys_get_temp_dir_declared_call( args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::direct_dispatch::eval_builtin_filesystem_call_impl("sys_get_temp_dir", args, context, scope, values) + eval_builtin_sys_get_temp_dir(args, values) } -/// Dispatches evaluated-argument calls for the `sys_get_temp_dir` filesystem builtin through the area dispatcher. +/// Evaluates `sys_get_temp_dir()` from already evaluated arguments. pub(in crate::interpreter) fn eval_sys_get_temp_dir_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::values_dispatch::eval_filesystem_values_result_impl("sys_get_temp_dir", evaluated_args, context, values) + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values) +} + +/// Evaluates PHP `sys_get_temp_dir()` with no arguments. +pub(in crate::interpreter) fn eval_builtin_sys_get_temp_dir( + args: &[EvalExpr], + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_sys_get_temp_dir_result(values) +} + +/// Returns the same temporary directory literal as the native static builtin. +pub(in crate::interpreter) fn eval_sys_get_temp_dir_result( + values: &mut impl RuntimeValueOps, +) -> Result { + values.string("/tmp") } diff --git a/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs index 5cdf30eed4..6eef8321e4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/filesystem/values_dispatch.rs @@ -10,7 +10,6 @@ use super::super::super::*; -use super::path_values_dispatch::eval_filesystem_path_values_result; /// Routes evaluated-argument filesystem builtin calls through per-builtin leaf wrappers. pub(in crate::interpreter) fn eval_filesystem_values_result( @@ -149,18 +148,3 @@ pub(in crate::interpreter) fn eval_filesystem_values_result( _ => Err(EvalStatus::RuntimeFatal), } } - -/// Dispatches evaluated-argument calls for declaratively migrated filesystem builtins. -pub(in crate::interpreter) fn eval_filesystem_values_result_impl( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = - eval_filesystem_path_values_result(name, evaluated_args, context, values)? - { - return Ok(result); - } - Err(EvalStatus::RuntimeFatal) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/cache.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/cache.rs deleted file mode 100644 index 34cf4c1a37..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/cache.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Purpose: -//! Implements temp-dir and intentionally empty realpath-cache eval builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::network_env` re-exports. -//! -//! Key details: -//! - The eval runtime does not maintain a PHP realpath cache, so cache probes -//! return stable empty/zero values. - -use super::super::super::*; - -/// Evaluates PHP `sys_get_temp_dir()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_sys_get_temp_dir( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_sys_get_temp_dir_result(values) -} - -/// Returns the same temporary directory literal as the native static builtin. -pub(in crate::interpreter) fn eval_sys_get_temp_dir_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.string("/tmp") -} - -/// Evaluates PHP `realpath_cache_get()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_realpath_cache_get( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_get_result(values) -} - -/// Returns elephc's intentionally empty realpath-cache view. -pub(in crate::interpreter) fn eval_realpath_cache_get_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.array_new(0) -} - -/// Evaluates PHP `realpath_cache_size()` with no arguments. -pub(in crate::interpreter) fn eval_builtin_realpath_cache_size( - args: &[EvalExpr], - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_realpath_cache_size_result(values) -} - -/// Returns zero because elephc does not maintain a runtime realpath cache. -pub(in crate::interpreter) fn eval_realpath_cache_size_result( - values: &mut impl RuntimeValueOps, -) -> Result { - values.int(0) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs index b89dd6e9de..ccb3f26bb1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/network_env/mod.rs @@ -12,7 +12,6 @@ use super::super::*; -mod cache; mod exec; mod getenv; mod gethostbyaddr; @@ -33,7 +32,6 @@ mod putenv; mod shell_exec; mod system; -pub(in crate::interpreter) use cache::*; pub(in crate::interpreter) use exec::*; pub(in crate::interpreter) use getenv::*; pub(in crate::interpreter) use gethostbyaddr::*; From a815f51878b4adb1a262b94152d53a220cbd4ced Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:11:26 +0200 Subject: [PATCH 1127/1208] refactor(magician): remove symbol builtin impl dispatcher --- .../src/interpreter/builtins/symbols.rs | 2 - .../builtins/symbols/class_alias.rs | 4 +- .../builtins/symbols/class_attribute_args.rs | 4 +- .../builtins/symbols/class_attribute_names.rs | 4 +- .../builtins/symbols/class_exists.rs | 4 +- .../builtins/symbols/class_get_attributes.rs | 4 +- .../builtins/symbols/class_implements.rs | 4 +- .../builtins/symbols/class_parents.rs | 4 +- .../builtins/symbols/class_uses.rs | 4 +- .../interpreter/builtins/symbols/dispatch.rs | 161 +----------------- .../src/interpreter/builtins/symbols/empty.rs | 6 +- .../builtins/symbols/enum_exists.rs | 4 +- .../builtins/symbols/function_exists.rs | 4 +- .../builtins/symbols/get_called_class.rs | 6 +- .../interpreter/builtins/symbols/get_class.rs | 4 +- .../builtins/symbols/get_class_methods.rs | 4 +- .../builtins/symbols/get_class_vars.rs | 4 +- .../builtins/symbols/get_declared_classes.rs | 6 +- .../symbols/get_declared_interfaces.rs | 6 +- .../builtins/symbols/get_declared_traits.rs | 6 +- .../builtins/symbols/get_object_vars.rs | 4 +- .../builtins/symbols/get_parent_class.rs | 4 +- .../builtins/symbols/get_resource_id.rs | 6 +- .../builtins/symbols/get_resource_type.rs | 6 +- .../builtins/symbols/interface_exists.rs | 4 +- .../src/interpreter/builtins/symbols/is_a.rs | 4 +- .../builtins/symbols/is_callable.rs | 4 +- .../builtins/symbols/is_subclass_of.rs | 4 +- .../src/interpreter/builtins/symbols/isset.rs | 6 +- .../builtins/symbols/method_exists.rs | 4 +- .../builtins/symbols/property_exists.rs | 4 +- .../builtins/symbols/spl_autoload.rs | 6 +- .../builtins/symbols/spl_autoload_call.rs | 6 +- .../symbols/spl_autoload_extensions.rs | 4 +- .../symbols/spl_autoload_functions.rs | 6 +- .../builtins/symbols/spl_autoload_register.rs | 6 +- .../symbols/spl_autoload_unregister.rs | 6 +- .../builtins/symbols/spl_classes.rs | 10 +- .../builtins/symbols/spl_object_hash.rs | 6 +- .../builtins/symbols/spl_object_id.rs | 6 +- .../builtins/symbols/trait_exists.rs | 4 +- .../src/interpreter/builtins/symbols/unset.rs | 6 +- 42 files changed, 101 insertions(+), 260 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index e34c0a02ad..57c26176b1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -58,9 +58,7 @@ mod trait_exists; mod unset; pub(in crate::interpreter) use callable_probe::*; -pub(in crate::interpreter) use class_names::*; pub(in crate::interpreter) use class_relations::*; pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; pub(in crate::interpreter) use function_probe::*; -pub(in crate::interpreter) use language_constructs::*; use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs index 10ba0795c4..4c8bc35ca2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_class_alias_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("class_alias", args, context, scope, values) + super::class_names::eval_builtin_class_alias(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `class_alias` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_class_alias_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("class_alias", evaluated_args, context, values) + super::class_names::eval_class_alias_result(evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs index 396864996c..e154e47eab 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_class_attribute_args_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("class_attribute_args", args, context, scope, values) + super::super::eval_builtin_class_attribute_metadata("class_attribute_args", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `class_attribute_args` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_class_attribute_args_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("class_attribute_args", evaluated_args, context, values) + super::super::eval_class_attribute_metadata_result("class_attribute_args", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs index 3b0bf5955e..1d2a795364 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_class_attribute_names_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("class_attribute_names", args, context, scope, values) + super::super::eval_builtin_class_attribute_metadata("class_attribute_names", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `class_attribute_names` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_class_attribute_names_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("class_attribute_names", evaluated_args, context, values) + super::super::eval_class_attribute_metadata_result("class_attribute_names", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs index 0566182f61..7137a29841 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_class_exists_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("class_exists", args, context, scope, values) + super::class_names::eval_builtin_class_exists(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `class_exists` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_class_exists_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("class_exists", evaluated_args, context, values) + super::class_names::eval_class_exists_result(evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs index a4cde7e8f8..dc88036a52 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_class_get_attributes_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("class_get_attributes", args, context, scope, values) + super::super::eval_builtin_class_attribute_metadata("class_get_attributes", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `class_get_attributes` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_class_get_attributes_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("class_get_attributes", evaluated_args, context, values) + super::super::eval_class_attribute_metadata_result("class_get_attributes", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs index 01ef53c347..1775ab30d8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_class_implements_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("class_implements", args, context, scope, values) + super::super::eval_builtin_class_relation("class_implements", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `class_implements` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_class_implements_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("class_implements", evaluated_args, context, values) + super::super::eval_class_relation_result("class_implements", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs index aa1a064502..9c3a0833a6 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_class_parents_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("class_parents", args, context, scope, values) + super::super::eval_builtin_class_relation("class_parents", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `class_parents` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_class_parents_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("class_parents", evaluated_args, context, values) + super::super::eval_class_relation_result("class_parents", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs index 9a5bc37322..fad90ac013 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_class_uses_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("class_uses", args, context, scope, values) + super::super::eval_builtin_class_relation("class_uses", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `class_uses` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_class_uses_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("class_uses", evaluated_args, context, values) + super::super::eval_class_relation_result("class_uses", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs index 1ef48a00de..7ac2761685 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/dispatch.rs @@ -7,7 +7,8 @@ //! Key details: //! - Public dispatch routes through per-builtin leaf wrappers so declaration //! files own their registry entry and runtime adapter. -//! - Internal impl dispatch keeps grouped helper behavior unchanged. +//! - Leaf builtin modules own the concrete direct and materialized-argument +//! entry points so this dispatcher only routes public symbol-area calls. use super::*; @@ -115,161 +116,3 @@ pub(in crate::interpreter) fn eval_symbols_values_result( _ => Err(EvalStatus::RuntimeFatal), } } - -/// Dispatches direct expression-level calls for declaratively migrated symbol builtins. -pub(in crate::interpreter) fn eval_builtin_symbols_call_impl( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "class_alias" => eval_builtin_class_alias(args, context, scope, values), - "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { - eval_builtin_class_attribute_metadata(name, args, context, scope, values) - } - "class_exists" => eval_builtin_class_exists(args, context, scope, values), - "class_implements" | "class_parents" | "class_uses" => { - eval_builtin_class_relation(name, args, context, scope, values) - } - "empty" => eval_builtin_empty(args, context, scope, values), - "enum_exists" | "trait_exists" => { - eval_builtin_class_like_exists(name, args, context, scope, values) - } - "function_exists" | "is_callable" => { - eval_builtin_function_probe(name, args, context, scope, values) - } - "get_called_class" => eval_builtin_get_called_class(args, context, values), - "get_class" => eval_builtin_get_class(args, context, scope, values), - "get_class_methods" => eval_builtin_get_class_methods(args, context, scope, values), - "get_class_vars" => eval_builtin_get_class_vars(args, context, scope, values), - "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { - eval_builtin_get_declared_symbols(name, args, context, values) - } - "get_object_vars" => eval_builtin_get_object_vars(args, context, scope, values), - "get_parent_class" => eval_builtin_get_parent_class(args, context, scope, values), - "get_resource_id" | "get_resource_type" => { - eval_builtin_resource_introspection(name, args, context, scope, values) - } - "interface_exists" => eval_builtin_interface_exists(args, context, scope, values), - "is_a" | "is_subclass_of" => { - eval_builtin_is_a_relation(name, args, context, scope, values) - } - "isset" => eval_builtin_isset(args, context, scope, values), - "method_exists" | "property_exists" => { - eval_builtin_member_exists(name, args, context, scope, values) - } - "spl_autoload" | "spl_autoload_call" => { - eval_builtin_spl_autoload_void(name, args, context, scope, values) - } - "spl_autoload_extensions" => { - eval_builtin_spl_autoload_extensions(args, context, scope, values) - } - "spl_autoload_functions" => { - eval_builtin_spl_autoload_functions(args, context, scope, values) - } - "spl_autoload_register" | "spl_autoload_unregister" => { - eval_builtin_spl_autoload_bool(name, args, context, scope, values) - } - "spl_classes" => super::spl_classes::eval_builtin_spl_classes(args, values), - "spl_object_hash" | "spl_object_id" => { - eval_builtin_spl_object_identity(name, args, context, scope, values) - } - "unset" => eval_builtin_unset(args, context, scope, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Dispatches evaluated-argument calls for declaratively migrated symbol builtins. -pub(in crate::interpreter) fn eval_symbols_values_result_impl( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "class_alias" => eval_class_alias_result(evaluated_args, context, values), - "class_attribute_args" | "class_attribute_names" | "class_get_attributes" => { - eval_class_attribute_metadata_result(name, evaluated_args, context, values) - } - "class_exists" => eval_class_exists_result(evaluated_args, context, values), - "class_implements" | "class_parents" | "class_uses" => { - eval_class_relation_result(name, evaluated_args, context, values) - } - "empty" => eval_empty_result(evaluated_args, values), - "enum_exists" | "trait_exists" => { - eval_class_like_exists_result(name, evaluated_args, context, values) - } - "function_exists" => { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_function_probe_result(name, *value, context, values) - } - "get_called_class" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_called_class_result(context, values) - } - "get_class" => match evaluated_args { - [] => eval_get_class_no_arg_result(context, values), - [object] => eval_get_class_result(*object, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "get_class_methods" => eval_get_class_methods_result(evaluated_args, context, values), - "get_class_vars" => eval_get_class_vars_result(evaluated_args, context, values), - "get_declared_classes" | "get_declared_interfaces" | "get_declared_traits" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_declared_symbols_result(name, context, values) - } - "get_object_vars" => eval_get_object_vars_result(evaluated_args, context, values), - "get_parent_class" => match evaluated_args { - [] => eval_get_parent_class_no_arg_result(context, values), - [object_or_class] => eval_get_parent_class_result(*object_or_class, context, values), - _ => Err(EvalStatus::RuntimeFatal), - }, - "get_resource_id" | "get_resource_type" => { - let [resource] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_resource_introspection_result(name, *resource, values) - } - "interface_exists" => eval_interface_exists_result(evaluated_args, context, values), - "is_a" | "is_subclass_of" => { - eval_is_a_relation_result(name, evaluated_args, context, values) - } - "is_callable" => eval_is_callable_with_values(evaluated_args, context, values), - "isset" => eval_isset_result(evaluated_args, values), - "method_exists" | "property_exists" => { - eval_member_exists_result(name, evaluated_args, context, values) - } - "spl_autoload" | "spl_autoload_call" => { - eval_spl_autoload_void_result(name, evaluated_args, values) - } - "spl_autoload_extensions" => { - eval_spl_autoload_extensions_result(evaluated_args, context, values) - } - "spl_autoload_functions" => eval_spl_autoload_functions_result(evaluated_args, values), - "spl_autoload_register" | "spl_autoload_unregister" => { - eval_spl_autoload_bool_result(name, evaluated_args, values) - } - "spl_classes" => { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - super::spl_classes::eval_spl_classes_result(values) - } - "spl_object_hash" | "spl_object_id" => { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_spl_object_identity_result(name, *object, values) - } - "unset" => eval_unset_result(evaluated_args, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs index 0eae66799a..340f447c98 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs @@ -24,14 +24,14 @@ pub(in crate::interpreter) fn eval_empty_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("empty", args, context, scope, values) + super::language_constructs::eval_builtin_empty(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `empty` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_empty_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("empty", evaluated_args, context, values) + super::language_constructs::eval_empty_result(evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs index efbbe08ca8..a3b274246c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_enum_exists_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("enum_exists", args, context, scope, values) + super::class_names::eval_builtin_class_like_exists("enum_exists", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `enum_exists` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_enum_exists_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("enum_exists", evaluated_args, context, values) + super::class_names::eval_class_like_exists_result("enum_exists", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs index 6d3b11b277..2fa62d0ca1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_function_exists_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("function_exists", args, context, scope, values) + super::function_probe::eval_builtin_function_probe("function_exists", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `function_exists` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_function_exists_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("function_exists", evaluated_args, context, values) + match evaluated_args { [value] => super::function_probe::eval_function_probe_result("function_exists", *value, context, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs index f402196417..5b1bc51791 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs @@ -21,10 +21,10 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_get_called_class_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_called_class", args, context, scope, values) + super::super::eval_builtin_get_called_class(args, context, values) } /// Dispatches evaluated-argument calls for the `get_called_class` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_get_called_class_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_called_class", evaluated_args, context, values) + if evaluated_args.is_empty() { super::super::eval_get_called_class_result(context, values) } else { Err(EvalStatus::RuntimeFatal) } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs index 329b7b2fce..7fe1f3132e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_get_class_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_class", args, context, scope, values) + super::super::eval_builtin_get_class(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `get_class` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_get_class_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_class", evaluated_args, context, values) + match evaluated_args { [] => super::super::eval_get_class_no_arg_result(context, values), [object] => super::super::eval_get_class_result(*object, context, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs index 0770da992f..a071a1d7c2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_get_class_methods_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_class_methods", args, context, scope, values) + super::super::eval_builtin_get_class_methods(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `get_class_methods` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_get_class_methods_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_class_methods", evaluated_args, context, values) + super::super::eval_get_class_methods_result(evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs index 8fdd5d17b1..e2e8e12c0e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_get_class_vars_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_class_vars", args, context, scope, values) + super::super::eval_builtin_get_class_vars(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `get_class_vars` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_get_class_vars_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_class_vars", evaluated_args, context, values) + super::super::eval_get_class_vars_result(evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs index 1aefc89057..5ead21d945 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs @@ -21,10 +21,10 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_get_declared_classes_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_declared_classes", args, context, scope, values) + super::class_names::eval_builtin_get_declared_symbols("get_declared_classes", args, context, values) } /// Dispatches evaluated-argument calls for the `get_declared_classes` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_get_declared_classes_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_declared_classes", evaluated_args, context, values) + if evaluated_args.is_empty() { super::class_names::eval_get_declared_symbols_result("get_declared_classes", context, values) } else { Err(EvalStatus::RuntimeFatal) } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs index 33f6b41685..6de8485593 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs @@ -21,10 +21,10 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_get_declared_interfaces_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_declared_interfaces", args, context, scope, values) + super::class_names::eval_builtin_get_declared_symbols("get_declared_interfaces", args, context, values) } /// Dispatches evaluated-argument calls for the `get_declared_interfaces` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_get_declared_interfaces_declared_values_resul context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_declared_interfaces", evaluated_args, context, values) + if evaluated_args.is_empty() { super::class_names::eval_get_declared_symbols_result("get_declared_interfaces", context, values) } else { Err(EvalStatus::RuntimeFatal) } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs index ee50106f24..48d799f1c7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs @@ -21,10 +21,10 @@ use super::super::super::*; pub(in crate::interpreter) fn eval_get_declared_traits_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_declared_traits", args, context, scope, values) + super::class_names::eval_builtin_get_declared_symbols("get_declared_traits", args, context, values) } /// Dispatches evaluated-argument calls for the `get_declared_traits` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_get_declared_traits_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_declared_traits", evaluated_args, context, values) + if evaluated_args.is_empty() { super::class_names::eval_get_declared_symbols_result("get_declared_traits", context, values) } else { Err(EvalStatus::RuntimeFatal) } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs index 802aae2f0d..bb8904dd7e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_get_object_vars_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_object_vars", args, context, scope, values) + super::super::eval_builtin_get_object_vars(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `get_object_vars` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_get_object_vars_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_object_vars", evaluated_args, context, values) + super::super::eval_get_object_vars_result(evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs index 209a5ee7c9..196c73605a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_get_parent_class_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_parent_class", args, context, scope, values) + super::super::eval_builtin_get_parent_class(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `get_parent_class` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_get_parent_class_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_parent_class", evaluated_args, context, values) + match evaluated_args { [] => super::super::eval_get_parent_class_no_arg_result(context, values), [object_or_class] => super::super::eval_get_parent_class_result(*object_or_class, context, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs index 672dce8d8a..dc19904b88 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs @@ -24,14 +24,14 @@ pub(in crate::interpreter) fn eval_get_resource_id_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_resource_id", args, context, scope, values) + super::super::eval_builtin_resource_introspection("get_resource_id", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `get_resource_id` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_get_resource_id_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_resource_id", evaluated_args, context, values) + match evaluated_args { [resource] => super::super::eval_resource_introspection_result("get_resource_id", *resource, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs index 01e3e1a458..dddb86052c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs @@ -24,14 +24,14 @@ pub(in crate::interpreter) fn eval_get_resource_type_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("get_resource_type", args, context, scope, values) + super::super::eval_builtin_resource_introspection("get_resource_type", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `get_resource_type` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_get_resource_type_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("get_resource_type", evaluated_args, context, values) + match evaluated_args { [resource] => super::super::eval_resource_introspection_result("get_resource_type", *resource, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs index 4cfa40f965..b61a42a158 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_interface_exists_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("interface_exists", args, context, scope, values) + super::class_names::eval_builtin_interface_exists(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `interface_exists` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_interface_exists_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("interface_exists", evaluated_args, context, values) + super::class_names::eval_interface_exists_result(evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs index 01f767e87a..323c30e834 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_is_a_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("is_a", args, context, scope, values) + super::class_relations::eval_builtin_is_a_relation("is_a", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_a` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_is_a_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("is_a", evaluated_args, context, values) + super::class_relations::eval_is_a_relation_result("is_a", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs index 4a9dc3b4ac..5a769a6d7e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs @@ -31,7 +31,7 @@ pub(in crate::interpreter) fn eval_is_callable_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("is_callable", args, context, scope, values) + super::function_probe::eval_builtin_function_probe("is_callable", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_callable` symbol builtin through the area dispatcher. @@ -40,5 +40,5 @@ pub(in crate::interpreter) fn eval_is_callable_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("is_callable", evaluated_args, context, values) + super::callable_probe::eval_is_callable_with_values(evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs index 32e9e8ed94..d8088b336d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_is_subclass_of_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("is_subclass_of", args, context, scope, values) + super::class_relations::eval_builtin_is_a_relation("is_subclass_of", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `is_subclass_of` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_is_subclass_of_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("is_subclass_of", evaluated_args, context, values) + super::class_relations::eval_is_a_relation_result("is_subclass_of", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs index 960b34d755..05a5345437 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs @@ -25,14 +25,14 @@ pub(in crate::interpreter) fn eval_isset_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("isset", args, context, scope, values) + super::language_constructs::eval_builtin_isset(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `isset` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_isset_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("isset", evaluated_args, context, values) + super::language_constructs::eval_isset_result(evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs index de9c2f8b3d..6dccab96a9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_method_exists_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("method_exists", args, context, scope, values) + super::super::eval_builtin_member_exists("method_exists", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `method_exists` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_method_exists_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("method_exists", evaluated_args, context, values) + super::super::eval_member_exists_result("method_exists", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs index 8218748eb6..962af9f9de 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs @@ -24,7 +24,7 @@ pub(in crate::interpreter) fn eval_property_exists_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("property_exists", args, context, scope, values) + super::super::eval_builtin_member_exists("property_exists", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `property_exists` symbol builtin through the area dispatcher. @@ -33,5 +33,5 @@ pub(in crate::interpreter) fn eval_property_exists_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("property_exists", evaluated_args, context, values) + super::super::eval_member_exists_result("property_exists", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs index d889e22ab9..14cf631cb3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs @@ -26,14 +26,14 @@ pub(in crate::interpreter) fn eval_spl_autoload_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_autoload", args, context, scope, values) + super::super::eval_builtin_spl_autoload_void("spl_autoload", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `spl_autoload` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_autoload_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_autoload", evaluated_args, context, values) + super::super::eval_spl_autoload_void_result("spl_autoload", evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs index f397f18315..2ffb3fa6c0 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs @@ -24,14 +24,14 @@ pub(in crate::interpreter) fn eval_spl_autoload_call_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_call", args, context, scope, values) + super::super::eval_builtin_spl_autoload_void("spl_autoload_call", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `spl_autoload_call` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_autoload_call_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_autoload_call", evaluated_args, context, values) + super::super::eval_spl_autoload_void_result("spl_autoload_call", evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs index 85766c7662..c3761684ab 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_spl_autoload_extensions_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_extensions", args, context, scope, values) + super::super::eval_builtin_spl_autoload_extensions(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `spl_autoload_extensions` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_spl_autoload_extensions_declared_values_resul context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_autoload_extensions", evaluated_args, context, values) + super::super::eval_spl_autoload_extensions_result(evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs index 6e07b28c67..437a1256d9 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs @@ -24,14 +24,14 @@ pub(in crate::interpreter) fn eval_spl_autoload_functions_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_functions", args, context, scope, values) + super::super::eval_builtin_spl_autoload_functions(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `spl_autoload_functions` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_autoload_functions_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_autoload_functions", evaluated_args, context, values) + super::super::eval_spl_autoload_functions_result(evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs index 0b9183c7ff..7ae8a706db 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs @@ -30,14 +30,14 @@ pub(in crate::interpreter) fn eval_spl_autoload_register_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_register", args, context, scope, values) + super::super::eval_builtin_spl_autoload_bool("spl_autoload_register", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `spl_autoload_register` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_autoload_register_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_autoload_register", evaluated_args, context, values) + super::super::eval_spl_autoload_bool_result("spl_autoload_register", evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs index 199b0f6ad7..16f6fdd02d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs @@ -24,14 +24,14 @@ pub(in crate::interpreter) fn eval_spl_autoload_unregister_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_autoload_unregister", args, context, scope, values) + super::super::eval_builtin_spl_autoload_bool("spl_autoload_unregister", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `spl_autoload_unregister` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_autoload_unregister_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_autoload_unregister", evaluated_args, context, values) + super::super::eval_spl_autoload_bool_result("spl_autoload_unregister", evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs index 11aadff3a3..e9056e6a1a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs @@ -21,20 +21,20 @@ use super::super::super::*; /// Dispatches direct eval calls for the `spl_classes` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_classes_declared_call( args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_classes", args, context, scope, values) + super::spl_classes::eval_builtin_spl_classes(args, values) } /// Dispatches evaluated-argument calls for the `spl_classes` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_classes_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_classes", evaluated_args, context, values) + if evaluated_args.is_empty() { super::spl_classes::eval_spl_classes_result(values) } else { Err(EvalStatus::RuntimeFatal) } } /// Evaluates PHP `spl_classes()` with no arguments. diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs index debc762226..887f4a9373 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs @@ -24,14 +24,14 @@ pub(in crate::interpreter) fn eval_spl_object_hash_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_object_hash", args, context, scope, values) + super::super::eval_builtin_spl_object_identity("spl_object_hash", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `spl_object_hash` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_object_hash_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_object_hash", evaluated_args, context, values) + match evaluated_args { [object] => super::super::eval_spl_object_identity_result("spl_object_hash", *object, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs index c2a34b0763..5a3f8b3bf5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs @@ -24,14 +24,14 @@ pub(in crate::interpreter) fn eval_spl_object_id_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("spl_object_id", args, context, scope, values) + super::super::eval_builtin_spl_object_identity("spl_object_id", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `spl_object_id` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_spl_object_id_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("spl_object_id", evaluated_args, context, values) + match evaluated_args { [object] => super::super::eval_spl_object_identity_result("spl_object_id", *object, values), _ => Err(EvalStatus::RuntimeFatal), } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs index ca1a8e87b4..85106e3b92 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs @@ -26,7 +26,7 @@ pub(in crate::interpreter) fn eval_trait_exists_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("trait_exists", args, context, scope, values) + super::class_names::eval_builtin_class_like_exists("trait_exists", args, context, scope, values) } /// Dispatches evaluated-argument calls for the `trait_exists` symbol builtin through the area dispatcher. @@ -35,5 +35,5 @@ pub(in crate::interpreter) fn eval_trait_exists_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("trait_exists", evaluated_args, context, values) + super::class_names::eval_class_like_exists_result("trait_exists", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs index ba7c218b5e..0625e29005 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs @@ -25,14 +25,14 @@ pub(in crate::interpreter) fn eval_unset_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_builtin_symbols_call_impl("unset", args, context, scope, values) + super::language_constructs::eval_builtin_unset(args, context, scope, values) } /// Dispatches evaluated-argument calls for the `unset` symbol builtin through the area dispatcher. pub(in crate::interpreter) fn eval_unset_declared_values_result( evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, + _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::dispatch::eval_symbols_values_result_impl("unset", evaluated_args, context, values) + super::language_constructs::eval_unset_result(evaluated_args, values) } From cae99e3393cd55aaa92e756afc106f0ccd0b35f6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:14:38 +0200 Subject: [PATCH 1128/1208] refactor(magician): move language construct builtins home --- .../src/interpreter/builtins/symbols.rs | 1 - .../src/interpreter/builtins/symbols/empty.rs | 154 ++++++++- .../src/interpreter/builtins/symbols/isset.rs | 150 +++++++- .../builtins/symbols/language_constructs.rs | 324 ------------------ .../src/interpreter/builtins/symbols/unset.rs | 53 ++- 5 files changed, 341 insertions(+), 341 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/language_constructs.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 57c26176b1..22a2b017e8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -42,7 +42,6 @@ mod is_a; mod is_callable; mod is_subclass_of; mod isset; -mod language_constructs; mod method_exists; mod property_exists; mod spl_autoload; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs index 340f447c98..81cee91306 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Declarative eval registry entry for `empty`. +//! Eval registry entry and implementation for `empty`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. @@ -17,21 +17,165 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `empty` symbol builtin through the area dispatcher. +/// Evaluates PHP's `empty(...)` language construct over eval-visible values. pub(in crate::interpreter) fn eval_empty_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::language_constructs::eval_builtin_empty(args, context, scope, values) + eval_builtin_empty(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `empty` symbol builtin through the area dispatcher. +/// Evaluates callable `empty(...)` over one already materialized value. pub(in crate::interpreter) fn eval_empty_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::language_constructs::eval_empty_result(evaluated_args, values) + eval_empty_result(evaluated_args, values) +} + +/// Evaluates PHP's `empty(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_builtin_empty( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [arg] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = eval_empty_arg(arg, context, scope, values)?; + values.bool_value(empty) +} + +/// Evaluates callable `empty(...)` over one already materialized value. +pub(in crate::interpreter) fn eval_empty_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let empty = !values.truthy(*value)?; + values.bool_value(empty) +} + +/// Evaluates one `empty` operand without warning or failing on missing variables. +pub(in crate::interpreter) fn eval_empty_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(true); + }; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::PropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if !eval_property_isset_result(object, property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_property_isset_result(object, &property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::NullsafePropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(true); + } + if !eval_property_isset_result(object, property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(true); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_property_isset_result(object, &property, context, values)? { + return Ok(true); + } + let value = eval_property_get_result(object, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::StaticPropertyGet { + class_name, + property, + } = arg + { + if !eval_static_property_isset_result(class_name, property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(class_name, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + if !eval_static_property_isset_result(&class_name, property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(&class_name, property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + if !eval_static_property_isset_result(&class_name, &property, context, values)? { + return Ok(true); + } + let value = eval_static_property_get_result(&class_name, &property, context, values)?; + return Ok(!values.truthy(value)?); + } + if let EvalExpr::ArrayGet { array, index } = arg { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return eval_array_access_empty_result(array, index, context, values); + } + let value = values.array_get(array, index)?; + return Ok(!values.truthy(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.truthy(value)?) +} + +/// Evaluates `empty($object[$key])` through `ArrayAccess::offsetExists()` and `offsetGet()`. +fn eval_array_access_empty_result( + object: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !super::isset::eval_array_access_isset_result(object, index, context, values)? { + return Ok(true); + } + let value = eval_array_get_result(object, index, context, values)?; + Ok(!values.truthy(value)?) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs index 05a5345437..2565f9162f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `isset`. +//! Eval registry entry and implementation for `isset`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Direct calls stay source-sensitive so operands are checked without normal reads. +//! - Direct calls stay source-sensitive so missing variables, object properties, +//! array offsets, and ArrayAccess values keep PHP `isset()` semantics. eval_builtin! { name: "isset", @@ -18,21 +19,158 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `isset` symbol builtin through the area dispatcher. +/// Evaluates PHP's `isset(...)` language construct over eval-visible values. pub(in crate::interpreter) fn eval_isset_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::language_constructs::eval_builtin_isset(args, context, scope, values) + eval_builtin_isset(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `isset` symbol builtin through the area dispatcher. +/// Evaluates callable `isset(...)` over already materialized values. pub(in crate::interpreter) fn eval_isset_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::language_constructs::eval_isset_result(evaluated_args, values) + eval_isset_result(evaluated_args, values) +} + +/// Evaluates PHP's `isset(...)` language construct over eval-visible values. +pub(in crate::interpreter) fn eval_builtin_isset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + if !eval_isset_arg(arg, context, scope, values)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates callable `isset(...)` over already materialized values. +pub(in crate::interpreter) fn eval_isset_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for value in evaluated_args { + if values.is_null(*value)? { + return values.bool_value(false); + } + } + values.bool_value(true) +} + +/// Evaluates one `isset` operand without allocating a null cell for missing variables. +pub(in crate::interpreter) fn eval_isset_arg( + arg: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let EvalExpr::LoadVar(name) = arg { + let Some(value) = visible_scope_cell(context, scope, name) else { + return Ok(false); + }; + return Ok(!values.is_null(value)?); + } + if let EvalExpr::PropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + return eval_property_isset_result(object, property, context, values); + } + if let EvalExpr::DynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_property_isset_result(object, &property, context, values); + } + if let EvalExpr::NullsafePropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(false); + } + return eval_property_isset_result(object, property, context, values); + } + if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { + let object = eval_expr(object, context, scope, values)?; + if values.is_null(object)? { + return Ok(false); + } + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_property_isset_result(object, &property, context, values); + } + if let EvalExpr::StaticPropertyGet { + class_name, + property, + } = arg + { + return eval_static_property_isset_result(class_name, property, context, values); + } + if let EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + return eval_static_property_isset_result(&class_name, property, context, values); + } + if let EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } = arg + { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + return eval_static_property_isset_result(&class_name, &property, context, values); + } + if let EvalExpr::ArrayGet { array, index } = arg { + let array = eval_expr(array, context, scope, values)?; + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + return eval_array_access_isset_result(array, index, context, values); + } + let value = values.array_get(array, index)?; + return Ok(!values.is_null(value)?); + } + let value = eval_expr(arg, context, scope, values)?; + Ok(!values.is_null(value)?) +} + +/// Evaluates `isset($object[$key])` through `ArrayAccess::offsetExists()`. +pub(in crate::interpreter) fn eval_array_access_isset_result( + object: RuntimeCellHandle, + index: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_method_call_result(object, "offsetExists", vec![index], context, values)?; + let exists = values.truthy(result)?; + values.release(result)?; + Ok(exists) +} + +/// Returns whether an object operand implements the eval-visible `ArrayAccess` contract. +fn eval_array_access_object_matches( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let target_class = "ArrayAccess"; + super::class_relations::dynamic_object_is_a(object, target_class, false, context, values)? + .map_or_else(|| values.object_is_a(object, target_class, false), Ok) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/language_constructs.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/language_constructs.rs deleted file mode 100644 index d5511cfd4b..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/language_constructs.rs +++ /dev/null @@ -1,324 +0,0 @@ -//! Purpose: -//! Eval implementations of PHP language constructs `isset`, `empty`, and `unset`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols` re-exports. -//! -//! Key details: -//! - These constructs receive unevaluated source expressions so missing variables, -//! properties, array access, and by-reference unset targets keep PHP semantics. - -use super::*; - -/// Evaluates PHP's `isset(...)` language construct over eval-visible values. -pub(in crate::interpreter) fn eval_builtin_isset( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - if !eval_isset_arg(arg, context, scope, values)? { - return values.bool_value(false); - } - } - values.bool_value(true) -} - -/// Evaluates PHP's `empty(...)` language construct over eval-visible values. -pub(in crate::interpreter) fn eval_builtin_empty( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [arg] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let empty = eval_empty_arg(arg, context, scope, values)?; - values.bool_value(empty) -} - -/// Evaluates direct `unset(...)` calls over eval-visible variables and object properties. -pub(in crate::interpreter) fn eval_builtin_unset( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - for arg in args { - match arg { - EvalExpr::LoadVar(name) => { - if let Some(replaced) = unset_scope_cell(scope, name.clone()) { - values.release(replaced)?; - } - } - EvalExpr::PropertyGet { object, property } => { - let object = eval_expr(object, context, scope, values)?; - eval_property_unset_result(object, property, context, values)?; - } - EvalExpr::DynamicPropertyGet { object, property } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_property_unset_result(object, &property, context, values)?; - } - _ => return Err(EvalStatus::RuntimeFatal), - } - } - values.null() -} - -/// Evaluates callable `isset(...)` over already materialized values. -pub(in crate::interpreter) fn eval_isset_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - if evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - for value in evaluated_args { - if values.is_null(*value)? { - return values.bool_value(false); - } - } - values.bool_value(true) -} - -/// Evaluates callable `empty(...)` over one already materialized value. -pub(in crate::interpreter) fn eval_empty_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - let [value] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let empty = !values.truthy(*value)?; - values.bool_value(empty) -} - -/// Evaluates callable `unset(...)` after values have already been materialized. -pub(in crate::interpreter) fn eval_unset_result( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - if evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.null() -} - -/// Evaluates one `empty` operand without warning or failing on missing variables. -pub(in crate::interpreter) fn eval_empty_arg( - arg: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let EvalExpr::LoadVar(name) = arg { - let Some(value) = visible_scope_cell(context, scope, name) else { - return Ok(true); - }; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::PropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if !eval_property_isset_result(object, property, context, values)? { - return Ok(true); - } - let value = eval_property_get_result(object, property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::DynamicPropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - if !eval_property_isset_result(object, &property, context, values)? { - return Ok(true); - } - let value = eval_property_get_result(object, &property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::NullsafePropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if values.is_null(object)? { - return Ok(true); - } - if !eval_property_isset_result(object, property, context, values)? { - return Ok(true); - } - let value = eval_property_get_result(object, property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if values.is_null(object)? { - return Ok(true); - } - let property = eval_dynamic_member_name(property, context, scope, values)?; - if !eval_property_isset_result(object, &property, context, values)? { - return Ok(true); - } - let value = eval_property_get_result(object, &property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::StaticPropertyGet { - class_name, - property, - } = arg - { - if !eval_static_property_isset_result(class_name, property, context, values)? { - return Ok(true); - } - let value = eval_static_property_get_result(class_name, property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } = arg - { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - if !eval_static_property_isset_result(&class_name, property, context, values)? { - return Ok(true); - } - let value = eval_static_property_get_result(&class_name, property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } = arg - { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - if !eval_static_property_isset_result(&class_name, &property, context, values)? { - return Ok(true); - } - let value = eval_static_property_get_result(&class_name, &property, context, values)?; - return Ok(!values.truthy(value)?); - } - if let EvalExpr::ArrayGet { array, index } = arg { - let array = eval_expr(array, context, scope, values)?; - let index = eval_expr(index, context, scope, values)?; - if values.type_tag(array)? == EVAL_TAG_OBJECT { - return eval_array_access_empty_result(array, index, context, values); - } - let value = values.array_get(array, index)?; - return Ok(!values.truthy(value)?); - } - let value = eval_expr(arg, context, scope, values)?; - Ok(!values.truthy(value)?) -} - -/// Evaluates one `isset` operand without allocating a null cell for missing variables. -pub(in crate::interpreter) fn eval_isset_arg( - arg: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let EvalExpr::LoadVar(name) = arg { - let Some(value) = visible_scope_cell(context, scope, name) else { - return Ok(false); - }; - return Ok(!values.is_null(value)?); - } - if let EvalExpr::PropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - return eval_property_isset_result(object, property, context, values); - } - if let EvalExpr::DynamicPropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - return eval_property_isset_result(object, &property, context, values); - } - if let EvalExpr::NullsafePropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if values.is_null(object)? { - return Ok(false); - } - return eval_property_isset_result(object, property, context, values); - } - if let EvalExpr::NullsafeDynamicPropertyGet { object, property } = arg { - let object = eval_expr(object, context, scope, values)?; - if values.is_null(object)? { - return Ok(false); - } - let property = eval_dynamic_member_name(property, context, scope, values)?; - return eval_property_isset_result(object, &property, context, values); - } - if let EvalExpr::StaticPropertyGet { - class_name, - property, - } = arg - { - return eval_static_property_isset_result(class_name, property, context, values); - } - if let EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } = arg - { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - return eval_static_property_isset_result(&class_name, property, context, values); - } - if let EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } = arg - { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - return eval_static_property_isset_result(&class_name, &property, context, values); - } - if let EvalExpr::ArrayGet { array, index } = arg { - let array = eval_expr(array, context, scope, values)?; - let index = eval_expr(index, context, scope, values)?; - if values.type_tag(array)? == EVAL_TAG_OBJECT { - return eval_array_access_isset_result(array, index, context, values); - } - let value = values.array_get(array, index)?; - return Ok(!values.is_null(value)?); - } - let value = eval_expr(arg, context, scope, values)?; - Ok(!values.is_null(value)?) -} - -/// Evaluates `empty($object[$key])` through `ArrayAccess::offsetExists()` and `offsetGet()`. -fn eval_array_access_empty_result( - object: RuntimeCellHandle, - index: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !eval_array_access_isset_result(object, index, context, values)? { - return Ok(true); - } - let value = eval_array_get_result(object, index, context, values)?; - Ok(!values.truthy(value)?) -} - -/// Evaluates `isset($object[$key])` through `ArrayAccess::offsetExists()`. -fn eval_array_access_isset_result( - object: RuntimeCellHandle, - index: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !eval_array_access_object_matches(object, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let result = eval_method_call_result(object, "offsetExists", vec![index], context, values)?; - let exists = values.truthy(result)?; - values.release(result)?; - Ok(exists) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs index 0625e29005..ff2d897a23 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Declarative eval registry entry for `unset`. +//! Eval registry entry and implementation for `unset`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. @@ -18,21 +18,64 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `unset` symbol builtin through the area dispatcher. +/// Evaluates direct `unset(...)` calls over eval-visible variables and object properties. pub(in crate::interpreter) fn eval_unset_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::language_constructs::eval_builtin_unset(args, context, scope, values) + eval_builtin_unset(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `unset` symbol builtin through the area dispatcher. +/// Evaluates callable `unset(...)` after values have already been materialized. pub(in crate::interpreter) fn eval_unset_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::language_constructs::eval_unset_result(evaluated_args, values) + eval_unset_result(evaluated_args, values) +} + +/// Evaluates direct `unset(...)` calls over eval-visible variables and object properties. +pub(in crate::interpreter) fn eval_builtin_unset( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for arg in args { + match arg { + EvalExpr::LoadVar(name) => { + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { + values.release(replaced)?; + } + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_unset_result(object, property, context, values)?; + } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_unset_result(object, &property, context, values)?; + } + _ => return Err(EvalStatus::RuntimeFatal), + } + } + values.null() +} + +/// Evaluates callable `unset(...)` after values have already been materialized. +pub(in crate::interpreter) fn eval_unset_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.null() } From 310b6130fbffe65cbd93e4d0ac476238e9cb0582 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:16:28 +0200 Subject: [PATCH 1129/1208] refactor(magician): move function probe builtin home --- .../src/interpreter/builtins/symbols.rs | 3 +- .../builtins/symbols/callable_probe.rs | 14 +- .../builtins/symbols/function_exists.rs | 130 +++++++++++++++- .../builtins/symbols/function_probe.rs | 139 ------------------ .../builtins/symbols/is_callable.rs | 6 +- crates/elephc-magician/src/interpreter/mod.rs | 5 +- 6 files changed, 135 insertions(+), 162 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/function_probe.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 22a2b017e8..404398ef60 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -25,7 +25,6 @@ mod dispatch; mod empty; mod enum_exists; mod function_exists; -mod function_probe; mod get_called_class; mod get_class; mod get_class_methods; @@ -59,5 +58,5 @@ mod unset; pub(in crate::interpreter) use callable_probe::*; pub(in crate::interpreter) use class_relations::*; pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; -pub(in crate::interpreter) use function_probe::*; +pub(in crate::interpreter) use function_exists::eval_function_probe_exists; use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs index 392bb8d24a..f03ba1040c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs @@ -87,7 +87,7 @@ pub(in crate::interpreter) fn eval_is_callable_with_values( } /// Evaluates positional `is_callable()` arguments inside an eval fragment. -pub(super) fn eval_builtin_is_callable( +pub(in crate::interpreter) fn eval_builtin_is_callable( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, @@ -125,7 +125,7 @@ pub(super) fn eval_builtin_is_callable( } /// Returns whether one runtime value is callable from the current eval scope. -pub(super) fn eval_is_callable_value( +pub(in crate::interpreter) fn eval_is_callable_value( value: RuntimeCellHandle, lexical_scope: Option<&ElephcEvalScope>, context: &ElephcEvalContext, @@ -263,12 +263,10 @@ fn eval_callable_probe_exists( values: &mut impl RuntimeValueOps, ) -> Result { match callback { - EvaluatedCallable::Named { name, .. } => { - Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) - } - EvaluatedCallable::BoundClosure { name, .. } => { - Ok(context.has_closure(name) || eval_function_probe_exists(context, name)) - } + EvaluatedCallable::Named { name, .. } => Ok(context.has_closure(name) + || super::function_exists::eval_function_probe_exists(context, name)), + EvaluatedCallable::BoundClosure { name, .. } => Ok(context.has_closure(name) + || super::function_exists::eval_function_probe_exists(context, name)), EvaluatedCallable::InvokableObject { object } => { eval_object_method_callable_probe(*object, "__invoke", context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs index 2fa62d0ca1..0be0c03c59 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `function_exists`. +//! Eval registry entry and implementation for `function_exists`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the builtin/function probe helper. +//! - Procedural date/time aliases stay visible to eval because static name +//! resolver rewrites do not run inside runtime eval fragments. eval_builtin! { name: "function_exists", @@ -17,21 +18,138 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `function_exists` symbol builtin through the area dispatcher. +const EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS: &[&str] = &[ + "cal_days_in_month", + "cal_from_jd", + "cal_info", + "cal_to_jd", + "date_add", + "date_create", + "date_create_from_format", + "date_create_immutable", + "date_create_immutable_from_format", + "date_date_set", + "date_diff", + "date_format", + "date_get_last_errors", + "date_interval_create_from_date_string", + "date_interval_format", + "date_isodate_set", + "date_modify", + "date_offset_get", + "date_parse", + "date_parse_from_format", + "date_sub", + "date_sun_info", + "date_sunrise", + "date_sunset", + "date_time_set", + "date_timestamp_get", + "date_timestamp_set", + "date_timezone_get", + "date_timezone_set", + "easter_date", + "easter_days", + "frenchtojd", + "gettimeofday", + "gmstrftime", + "gregoriantojd", + "idate", + "jddayofweek", + "jdmonthname", + "jdtofrench", + "jdtogregorian", + "jdtojewish", + "jdtojulian", + "jdtounix", + "jewishtojd", + "juliantojd", + "mktime", + "gmmktime", + "strftime", + "strptime", + "timezone_abbreviations_list", + "timezone_identifiers_list", + "timezone_location_get", + "timezone_name_from_abbr", + "timezone_name_get", + "timezone_offset_get", + "timezone_open", + "timezone_transitions_get", + "timezone_version_get", + "unixtojd", +]; + +/// Evaluates direct `function_exists(...)` calls inside an eval fragment. pub(in crate::interpreter) fn eval_function_exists_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::function_probe::eval_builtin_function_probe("function_exists", args, context, scope, values) + eval_builtin_function_exists(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `function_exists` symbol builtin through the area dispatcher. +/// Evaluates materialized `function_exists(...)` arguments. pub(in crate::interpreter) fn eval_function_exists_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match evaluated_args { [value] => super::function_probe::eval_function_probe_result("function_exists", *value, context, values), _ => Err(EvalStatus::RuntimeFatal), } + match evaluated_args { + [value] => eval_function_exists_result(*value, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `function_exists()` inside an eval fragment. +pub(in crate::interpreter) fn eval_builtin_function_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [value] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let value = eval_expr(value, context, scope, values)?; + eval_function_exists_result(value, context, values) +} + +/// Evaluates `function_exists()` from one materialized function-name argument. +pub(in crate::interpreter) fn eval_function_exists_result( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_function_probe_name(value, values)?; + let exists = eval_function_probe_exists(context, &name); + values.bool_value(exists) +} + +/// Returns true when a PHP function name is visible to eval builtin probes. +pub(in crate::interpreter) fn eval_function_probe_exists( + context: &ElephcEvalContext, + name: &str, +) -> bool { + !name.contains("::") + && (context.has_function(name) + || eval_php_visible_builtin_exists(name) + || eval_date_procedural_alias_exists(name)) +} + +/// Returns true for DateTime/calendar/timezone aliases that static elephc desugars. +fn eval_date_procedural_alias_exists(name: &str) -> bool { + let bare = name.rsplit('\\').next().unwrap_or(""); + EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS.contains(&bare) +} + +/// Reads and normalizes a function-probe string argument. +fn eval_function_probe_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_ascii_lowercase()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/function_probe.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/function_probe.rs deleted file mode 100644 index 34d25ea02a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/function_probe.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Purpose: -//! Function-name probes for eval `function_exists()` and function-like symbol lookup. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols` re-exports. -//! -//! Key details: -//! - Procedural date/time aliases stay visible to eval because static name -//! resolver rewrites do not run inside runtime eval fragments. - -use super::*; - -const EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS: &[&str] = &[ - "cal_days_in_month", - "cal_from_jd", - "cal_info", - "cal_to_jd", - "date_add", - "date_create", - "date_create_from_format", - "date_create_immutable", - "date_create_immutable_from_format", - "date_date_set", - "date_diff", - "date_format", - "date_get_last_errors", - "date_interval_create_from_date_string", - "date_interval_format", - "date_isodate_set", - "date_modify", - "date_offset_get", - "date_parse", - "date_parse_from_format", - "date_sub", - "date_sun_info", - "date_sunrise", - "date_sunset", - "date_time_set", - "date_timestamp_get", - "date_timestamp_set", - "date_timezone_get", - "date_timezone_set", - "easter_date", - "easter_days", - "frenchtojd", - "gettimeofday", - "gmstrftime", - "gregoriantojd", - "idate", - "jddayofweek", - "jdmonthname", - "jdtofrench", - "jdtogregorian", - "jdtojewish", - "jdtojulian", - "jdtounix", - "jewishtojd", - "juliantojd", - "mktime", - "gmmktime", - "strftime", - "strptime", - "timezone_abbreviations_list", - "timezone_identifiers_list", - "timezone_location_get", - "timezone_name_from_abbr", - "timezone_name_get", - "timezone_offset_get", - "timezone_open", - "timezone_transitions_get", - "timezone_version_get", - "unixtojd", -]; - -/// Evaluates `function_exists()` and `is_callable()` inside an eval fragment. -pub(in crate::interpreter) fn eval_builtin_function_probe( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "function_exists" => { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let value = eval_expr(value, context, scope, values)?; - eval_function_probe_result(name, value, context, values) - } - "is_callable" => eval_builtin_is_callable(args, context, scope, values), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates `function_exists()` and `is_callable()` from materialized arguments. -pub(in crate::interpreter) fn eval_function_probe_result( - name: &str, - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match name { - "function_exists" => { - let name = eval_function_probe_name(value, values)?; - eval_function_probe_exists(context, &name) - } - "is_callable" => eval_is_callable_value(value, None, context, values)?, - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - values.bool_value(exists) -} - -/// Returns true when a PHP function name is visible to eval builtin probes. -pub(in crate::interpreter) fn eval_function_probe_exists( - context: &ElephcEvalContext, - name: &str, -) -> bool { - !name.contains("::") - && (context.has_function(name) - || eval_php_visible_builtin_exists(name) - || eval_date_procedural_alias_exists(name)) -} - -/// Returns true for DateTime/calendar/timezone aliases that static elephc desugars. -fn eval_date_procedural_alias_exists(name: &str) -> bool { - let bare = name.rsplit('\\').next().unwrap_or(""); - EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS.contains(&bare) -} - -/// Reads and normalizes a function-probe string argument. -fn eval_function_probe_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(name.trim_start_matches('\\').to_ascii_lowercase()) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs index 5a769a6d7e..ccbf81e96b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs @@ -24,17 +24,17 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `is_callable` symbol builtin through the area dispatcher. +/// Evaluates direct `is_callable(...)` calls inside an eval fragment. pub(in crate::interpreter) fn eval_is_callable_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::function_probe::eval_builtin_function_probe("is_callable", args, context, scope, values) + super::callable_probe::eval_builtin_is_callable(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `is_callable` symbol builtin through the area dispatcher. +/// Evaluates materialized `is_callable(...)` arguments. pub(in crate::interpreter) fn eval_is_callable_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index 3be93e59d7..f0bc62ee09 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -235,10 +235,7 @@ pub fn execute_context_is_callable( callback: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { - let result = eval_function_probe_result("is_callable", callback, context, values)?; - let callable = values.truthy(result)?; - values.release(result)?; - Ok(callable) + eval_is_callable_value(callback, None, context, values) } /// Constructs a class declared in the shared eval context with prepared positional arguments. From 1244312d6bb5d024ee4f3fc8f641d7d08e64e016 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:18:47 +0200 Subject: [PATCH 1130/1208] refactor(magician): move class relation builtins home --- .../src/interpreter/builtins/symbols.rs | 3 +- .../builtins/symbols/class_relations.rs | 247 ----------------- .../src/interpreter/builtins/symbols/is_a.rs | 249 +++++++++++++++++- .../builtins/symbols/is_subclass_of.rs | 13 +- .../src/interpreter/builtins/symbols/isset.rs | 2 +- 5 files changed, 252 insertions(+), 262 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_relations.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 404398ef60..578372274e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -19,7 +19,6 @@ mod class_get_attributes; mod class_implements; mod class_names; mod class_parents; -mod class_relations; mod class_uses; mod dispatch; mod empty; @@ -56,7 +55,7 @@ mod trait_exists; mod unset; pub(in crate::interpreter) use callable_probe::*; -pub(in crate::interpreter) use class_relations::*; pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; pub(in crate::interpreter) use function_exists::eval_function_probe_exists; +pub(in crate::interpreter) use is_a::dynamic_object_is_a; use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_relations.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_relations.rs deleted file mode 100644 index eb5a91c403..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_relations.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! Purpose: -//! Class-like relationship probes for eval `is_a()` and `is_subclass_of()`. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols` re-exports. -//! -//! Key details: -//! - Eval-created classes are checked first, then generated/AOT object and -//! interface metadata fills inherited relationships. - -use super::*; - -/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. -pub(in crate::interpreter) fn eval_builtin_is_a_relation( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut evaluated_args = Vec::with_capacity(args.len()); - for arg in args { - evaluated_args.push(eval_expr(arg, context, scope, values)?); - } - eval_is_a_relation_result(name, &evaluated_args, context, values) -} - -/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. -pub(in crate::interpreter) fn eval_is_a_relation_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (object_or_class, target_class, allow_string) = match evaluated_args { - [object_or_class, target_class] => { - (*object_or_class, *target_class, name == "is_subclass_of") - } - [object_or_class, target_class, allow_string] => ( - *object_or_class, - *target_class, - values.truthy(*allow_string)?, - ), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let target_class = values.string_bytes(target_class)?; - let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; - let target_class = target_class.trim_start_matches('\\'); - let resolved_target_class = context - .resolve_class_like_name(target_class) - .unwrap_or_else(|| target_class.to_string()); - let is_object = values.type_tag(object_or_class)? == 6; - let exclude_self = name == "is_subclass_of"; - let result = if is_object { - dynamic_object_is_a( - object_or_class, - &resolved_target_class, - exclude_self, - context, - values, - )? - .map_or_else( - || values.object_is_a(object_or_class, &resolved_target_class, exclude_self), - Ok, - )? - } else if allow_string && values.type_tag(object_or_class)? == EVAL_TAG_STRING { - let source_class = values.string_bytes(object_or_class)?; - let source_class = String::from_utf8(source_class).map_err(|_| EvalStatus::RuntimeFatal)?; - let resolved_source_class = context - .resolve_class_like_name(&source_class) - .unwrap_or_else(|| source_class.trim_start_matches('\\').to_string()); - if context.class(&resolved_source_class).is_some() { - eval_class_string_is_a( - &resolved_source_class, - &resolved_target_class, - exclude_self, - context, - values, - )? - } else if context.interface(&resolved_source_class).is_some() { - eval_interface_string_is_a( - &resolved_source_class, - &resolved_target_class, - exclude_self, - context, - values, - )? - } else if context.trait_decl(&resolved_source_class).is_some() { - !exclude_self - && eval_class_like_name_matches(&resolved_source_class, &resolved_target_class) - } else { - values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? - } - } else if allow_string { - values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? - } else { - false - }; - values.bool_value(result) -} - -/// Returns whether an interface string source satisfies a class-like target. -fn eval_interface_string_is_a( - source_class: &str, - target_class: &str, - exclude_self: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !exclude_self && eval_class_like_name_matches(source_class, target_class) { - return Ok(true); - } - Ok(eval_interface_runtime_parent_names(source_class, context, values)? - .iter() - .any(|parent| eval_class_like_name_matches(parent, target_class))) -} - -/// Returns whether an eval class-string source satisfies a class-like target. -fn eval_class_string_is_a( - source_class: &str, - target_class: &str, - exclude_self: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if context.class_is_a(source_class, target_class, exclude_self) { - return Ok(true); - } - Ok(eval_class_runtime_interface_names(source_class, context, values)? - .iter() - .any(|interface| eval_class_like_name_matches(interface, target_class))) -} - -/// Returns eval class interfaces plus generated/AOT inherited interface names. -fn eval_class_runtime_interface_names( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - if let Some(parent) = context.class_native_parent_name(class_name) { - for name in eval_runtime_class_interface_names(&parent, values)? { - eval_push_unique_class_name(name, &mut names, &mut seen); - } - } - for name in context.class_interface_names(class_name) { - eval_push_unique_class_name(name.clone(), &mut names, &mut seen); - if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { - for parent in eval_runtime_class_interface_names(&name, values)? { - eval_push_unique_class_name(parent, &mut names, &mut seen); - } - } - } - Ok(names) -} - -/// Returns eval interface parents plus generated/AOT inherited interface names. -fn eval_interface_runtime_parent_names( - interface_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - for name in context.interface_parent_names(interface_name) { - eval_push_unique_class_name(name.clone(), &mut names, &mut seen); - if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { - for parent in eval_runtime_class_interface_names(&name, values)? { - eval_push_unique_class_name(parent, &mut names, &mut seen); - } - } - } - Ok(names) -} - -/// Returns generated/AOT interface names visible for one class-like symbol. -fn eval_runtime_class_interface_names( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let names_array = values.reflection_class_interface_names(class_name)?; - let names = eval_string_array_to_vec(names_array, values)?; - values.release(names_array)?; - Ok(names) -} - -/// Copies a runtime string array into Rust-owned names. -fn eval_string_array_to_vec( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut result = Vec::with_capacity(len); - for position in 0..len { - let key = values.int(position as i64)?; - let value = values.array_get(array, key)?; - let bytes = values.string_bytes(value)?; - result.push(String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?); - } - Ok(result) -} - -/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. -fn eval_push_unique_class_name( - name: String, - names: &mut Vec, - seen: &mut std::collections::HashSet, -) { - if seen.insert(name.to_ascii_lowercase()) { - names.push(name); - } -} - -/// Returns whether two class-like names match PHP's case-insensitive class-name rules. -fn eval_class_like_name_matches(left: &str, right: &str) -> bool { - left.trim_start_matches('\\') - .eq_ignore_ascii_case(right.trim_start_matches('\\')) -} - -/// Returns whether an eval-created object matches a dynamic class/interface target. -pub(in crate::interpreter) fn dynamic_object_is_a( - object: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let identity = values.object_identity(object)?; - if context.dynamic_object_is_class(identity, "Closure") { - return Ok(Some( - !exclude_self && eval_class_like_name_matches("Closure", target_class), - )); - } - let Some(class) = context.dynamic_object_class(identity) else { - return Ok(None); - }; - if eval_class_string_is_a(class.name(), target_class, exclude_self, context, values)? { - return Ok(Some(true)); - } - if context.class_native_parent_name(class.name()).is_some() { - return values - .object_is_a(object, target_class, exclude_self) - .map(Some); - } - Ok(Some(false)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs index 323c30e834..8545215abd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `is_a`. +//! Eval registry entry and implementation for `is_a`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-relation predicate helper. +//! - Eval-created classes are checked first, then generated/AOT object and +//! interface metadata fills inherited relationships. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +20,257 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `is_a` symbol builtin through the area dispatcher. +/// Evaluates direct `is_a(...)` calls over eval boxed object cells and class strings. pub(in crate::interpreter) fn eval_is_a_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_relations::eval_builtin_is_a_relation("is_a", args, context, scope, values) + eval_builtin_is_a_relation("is_a", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `is_a` symbol builtin through the area dispatcher. +/// Evaluates materialized `is_a(...)` arguments. pub(in crate::interpreter) fn eval_is_a_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_relations::eval_is_a_relation_result("is_a", evaluated_args, context, values) + eval_is_a_relation_result("is_a", evaluated_args, context, values) +} + +/// Evaluates `is_a(...)` and `is_subclass_of(...)` over eval boxed object cells. +pub(in crate::interpreter) fn eval_builtin_is_a_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut evaluated_args = Vec::with_capacity(args.len()); + for arg in args { + evaluated_args.push(eval_expr(arg, context, scope, values)?); + } + eval_is_a_relation_result(name, &evaluated_args, context, values) +} + +/// Evaluates materialized `is_a(...)` or `is_subclass_of(...)` builtin arguments. +pub(in crate::interpreter) fn eval_is_a_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_or_class, target_class, allow_string) = match evaluated_args { + [object_or_class, target_class] => { + (*object_or_class, *target_class, name == "is_subclass_of") + } + [object_or_class, target_class, allow_string] => ( + *object_or_class, + *target_class, + values.truthy(*allow_string)?, + ), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let target_class = values.string_bytes(target_class)?; + let target_class = String::from_utf8(target_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let target_class = target_class.trim_start_matches('\\'); + let resolved_target_class = context + .resolve_class_like_name(target_class) + .unwrap_or_else(|| target_class.to_string()); + let is_object = values.type_tag(object_or_class)? == 6; + let exclude_self = name == "is_subclass_of"; + let result = if is_object { + dynamic_object_is_a( + object_or_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + .map_or_else( + || values.object_is_a(object_or_class, &resolved_target_class, exclude_self), + Ok, + )? + } else if allow_string && values.type_tag(object_or_class)? == EVAL_TAG_STRING { + let source_class = values.string_bytes(object_or_class)?; + let source_class = String::from_utf8(source_class).map_err(|_| EvalStatus::RuntimeFatal)?; + let resolved_source_class = context + .resolve_class_like_name(&source_class) + .unwrap_or_else(|| source_class.trim_start_matches('\\').to_string()); + if context.class(&resolved_source_class).is_some() { + eval_class_string_is_a( + &resolved_source_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + } else if context.interface(&resolved_source_class).is_some() { + eval_interface_string_is_a( + &resolved_source_class, + &resolved_target_class, + exclude_self, + context, + values, + )? + } else if context.trait_decl(&resolved_source_class).is_some() { + !exclude_self + && eval_class_like_name_matches(&resolved_source_class, &resolved_target_class) + } else { + values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? + } + } else if allow_string { + values.object_is_a(object_or_class, &resolved_target_class, exclude_self)? + } else { + false + }; + values.bool_value(result) +} + +/// Returns whether an interface string source satisfies a class-like target. +fn eval_interface_string_is_a( + source_class: &str, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !exclude_self && eval_class_like_name_matches(source_class, target_class) { + return Ok(true); + } + Ok(eval_interface_runtime_parent_names(source_class, context, values)? + .iter() + .any(|parent| eval_class_like_name_matches(parent, target_class))) +} + +/// Returns whether an eval class-string source satisfies a class-like target. +fn eval_class_string_is_a( + source_class: &str, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.class_is_a(source_class, target_class, exclude_self) { + return Ok(true); + } + Ok(eval_class_runtime_interface_names(source_class, context, values)? + .iter() + .any(|interface| eval_class_like_name_matches(interface, target_class))) +} + +/// Returns eval class interfaces plus generated/AOT inherited interface names. +fn eval_class_runtime_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_runtime_class_interface_names(&parent, values)? { + eval_push_unique_class_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus generated/AOT inherited interface names. +fn eval_interface_runtime_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns generated/AOT interface names visible for one class-like symbol. +fn eval_runtime_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let names_array = values.reflection_class_interface_names(class_name)?; + let names = eval_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Copies a runtime string array into Rust-owned names. +fn eval_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + let bytes = values.string_bytes(value)?; + result.push(String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?); + } + Ok(result) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +fn eval_push_unique_class_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + +/// Returns whether two class-like names match PHP's case-insensitive class-name rules. +fn eval_class_like_name_matches(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns whether an eval-created object matches a dynamic class/interface target. +pub(in crate::interpreter) fn dynamic_object_is_a( + object: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let identity = values.object_identity(object)?; + if context.dynamic_object_is_class(identity, "Closure") { + return Ok(Some( + !exclude_self && eval_class_like_name_matches("Closure", target_class), + )); + } + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(None); + }; + if eval_class_string_is_a(class.name(), target_class, exclude_self, context, values)? { + return Ok(Some(true)); + } + if context.class_native_parent_name(class.name()).is_some() { + return values + .object_is_a(object, target_class, exclude_self) + .map(Some); + } + Ok(Some(false)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs index d8088b336d..7e461b78cf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `is_subclass_of`. +//! Eval registry entry for `is_subclass_of`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-relation predicate helper. +//! - Relationship semantics are shared with `is_a()` because PHP only changes +//! the self-match and default string-allowance rules. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +20,21 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `is_subclass_of` symbol builtin through the area dispatcher. +/// Evaluates direct `is_subclass_of(...)` calls through the `is_a` relation owner. pub(in crate::interpreter) fn eval_is_subclass_of_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_relations::eval_builtin_is_a_relation("is_subclass_of", args, context, scope, values) + super::is_a::eval_builtin_is_a_relation("is_subclass_of", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `is_subclass_of` symbol builtin through the area dispatcher. +/// Evaluates materialized `is_subclass_of(...)` arguments through the `is_a` relation owner. pub(in crate::interpreter) fn eval_is_subclass_of_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_relations::eval_is_a_relation_result("is_subclass_of", evaluated_args, context, values) + super::is_a::eval_is_a_relation_result("is_subclass_of", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs index 2565f9162f..3e6f882eba 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs @@ -171,6 +171,6 @@ fn eval_array_access_object_matches( values: &mut impl RuntimeValueOps, ) -> Result { let target_class = "ArrayAccess"; - super::class_relations::dynamic_object_is_a(object, target_class, false, context, values)? + super::is_a::dynamic_object_is_a(object, target_class, false, context, values)? .map_or_else(|| values.object_is_a(object, target_class, false), Ok) } From 1d18daf34093b73189f617d720287ac06b3b0f3c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:22:32 +0200 Subject: [PATCH 1131/1208] refactor(magician): move class name builtins home --- .../src/interpreter/builtins/symbols.rs | 1 - .../builtins/symbols/class_alias.rs | 84 +++++- .../builtins/symbols/class_exists.rs | 65 +++- .../builtins/symbols/class_names.rs | 282 ------------------ .../builtins/symbols/enum_exists.rs | 12 +- .../builtins/symbols/get_declared_classes.rs | 65 +++- .../symbols/get_declared_interfaces.rs | 25 +- .../builtins/symbols/get_declared_traits.rs | 25 +- .../builtins/symbols/interface_exists.rs | 58 +++- .../builtins/symbols/trait_exists.rs | 66 +++- 10 files changed, 352 insertions(+), 331 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/class_names.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 578372274e..b22fd617e8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -17,7 +17,6 @@ mod class_attribute_names; mod class_exists; mod class_get_attributes; mod class_implements; -mod class_names; mod class_parents; mod class_uses; mod dispatch; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs index 4c8bc35ca2..0b8577403e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `class_alias`. +//! Eval registry entry and implementation for `class_alias`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the symbol dispatch adapter. +//! - Runtime aliases are stored in the eval context for eval declarations and +//! generated/AOT class-like metadata. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +20,92 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `class_alias` symbol builtin through the area dispatcher. +/// Evaluates direct `class_alias(class, alias, autoload?)` calls. pub(in crate::interpreter) fn eval_class_alias_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_builtin_class_alias(args, context, scope, values) + eval_builtin_class_alias(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `class_alias` symbol builtin through the area dispatcher. +/// Evaluates materialized `class_alias(...)` arguments. pub(in crate::interpreter) fn eval_class_alias_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_class_alias_result(evaluated_args, context, values) + eval_class_alias_result(evaluated_args, context, values) +} + +/// Evaluates `class_alias(class, alias, autoload?)` against eval and generated class tables. +pub(in crate::interpreter) fn eval_builtin_class_alias( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let (class, alias) = match args { + [class, alias] => ( + eval_expr(class, context, scope, values)?, + eval_expr(alias, context, scope, values)?, + ), + [class, alias, autoload] => { + let class = eval_expr(class, context, scope, values)?; + let alias = eval_expr(alias, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + (class, alias) + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_alias_result(&[class, alias], context, values) +} + +/// Evaluates `class_alias(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_class_alias_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (class, alias) = match evaluated_args { + [class, alias] => (*class, *alias), + [class, alias, _autoload] => (*class, *alias), + _ => return Err(EvalStatus::RuntimeFatal), + }; + let class = eval_class_alias_name(class, values)?; + let alias = eval_class_alias_name(alias, values)?; + if alias.is_empty() + || context.resolve_class_like_name(&alias).is_some() + || values.class_exists(&alias)? + || eval_runtime_interface_exists(&alias, values)? + || values.trait_exists(&alias)? + || values.enum_exists(&alias)? + { + return values.bool_value(false); + } + let aliased = if context.resolve_class_like_name(&class).is_some() { + context.define_class_alias(&class, &alias) + } else if values.enum_exists(&class)? { + context.define_external_enum_alias(&class, &alias) + } else if values.class_exists(&class)? { + context.define_external_class_alias(&class, &alias) + } else if eval_runtime_interface_exists(&class, values)? { + context.define_external_interface_alias(&class, &alias) + } else if values.trait_exists(&class)? { + context.define_external_trait_alias(&class, &alias) + } else { + false + }; + values.bool_value(aliased) +} + +/// Reads and normalizes one `class_alias()` class-name argument. +fn eval_class_alias_name( + name: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(name.trim_start_matches('\\').to_string()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs index 7137a29841..c154105d22 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `class_exists`. +//! Eval registry entry and implementation for `class_exists`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-existence probe. +//! - Lookup checks eval declarations before generated/AOT runtime metadata. +//! - `Closure` is treated as a built-in class-like symbol. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +20,73 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `class_exists` symbol builtin through the area dispatcher. +/// Evaluates direct `class_exists(...)` calls against dynamic and generated class-name tables. pub(in crate::interpreter) fn eval_class_exists_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_builtin_class_exists(args, context, scope, values) + eval_builtin_class_exists(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `class_exists` symbol builtin through the area dispatcher. +/// Evaluates materialized `class_exists(...)` arguments. pub(in crate::interpreter) fn eval_class_exists_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_class_exists_result(evaluated_args, context, values) + eval_class_exists_result(evaluated_args, context, values) +} + +/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. +pub(in crate::interpreter) fn eval_builtin_class_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_exists_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `class_exists(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_class_exists_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_class_exists_name(*name, context, values)?, + [name, _autoload] => eval_class_exists_name(*name, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. +pub(in crate::interpreter) fn eval_class_exists_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\'); + if name.eq_ignore_ascii_case("Closure") { + return Ok(true); + } + if context.has_class(name) { + return Ok(true); + } + values.class_exists(name) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_names.rs deleted file mode 100644 index 138d762f93..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_names.rs +++ /dev/null @@ -1,282 +0,0 @@ -//! Purpose: -//! Class-like symbol existence, aliasing, and declared-symbol array builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols` re-exports. -//! -//! Key details: -//! - Lookup checks eval declarations before generated/AOT runtime metadata. -//! - `class_alias()` records external aliases in the eval context when the -//! aliased class-like symbol is provided by runtime metadata. - -use super::*; - -/// Evaluates `class_exists(...)` against dynamic and generated class-name tables. -pub(in crate::interpreter) fn eval_builtin_class_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = match args { - [name] => eval_expr(name, context, scope, values)?, - [name, autoload] => { - let name = eval_expr(name, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - name - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_class_exists_name(name, context, values)?; - values.bool_value(exists) -} - -/// Evaluates `class_exists(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_class_exists_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [name] => eval_class_exists_name(*name, context, values)?, - [name, _autoload] => eval_class_exists_name(*name, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP class-name cell and probes dynamic names before generated classes. -pub(in crate::interpreter) fn eval_class_exists_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\'); - if name.eq_ignore_ascii_case("Closure") { - return Ok(true); - } - if context.has_class(name) { - return Ok(true); - } - values.class_exists(name) -} - -/// Evaluates `class_alias(class, alias, autoload?)` against eval and generated class tables. -pub(in crate::interpreter) fn eval_builtin_class_alias( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let (class, alias) = match args { - [class, alias] => ( - eval_expr(class, context, scope, values)?, - eval_expr(alias, context, scope, values)?, - ), - [class, alias, autoload] => { - let class = eval_expr(class, context, scope, values)?; - let alias = eval_expr(alias, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - (class, alias) - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_class_alias_result(&[class, alias], context, values) -} - -/// Evaluates `class_alias(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_class_alias_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (class, alias) = match evaluated_args { - [class, alias] => (*class, *alias), - [class, alias, _autoload] => (*class, *alias), - _ => return Err(EvalStatus::RuntimeFatal), - }; - let class = eval_class_alias_name(class, values)?; - let alias = eval_class_alias_name(alias, values)?; - if alias.is_empty() - || context.resolve_class_like_name(&alias).is_some() - || values.class_exists(&alias)? - || eval_runtime_interface_exists(&alias, values)? - || values.trait_exists(&alias)? - || values.enum_exists(&alias)? - { - return values.bool_value(false); - } - let aliased = if context.resolve_class_like_name(&class).is_some() { - context.define_class_alias(&class, &alias) - } else if values.enum_exists(&class)? { - context.define_external_enum_alias(&class, &alias) - } else if values.class_exists(&class)? { - context.define_external_class_alias(&class, &alias) - } else if eval_runtime_interface_exists(&class, values)? { - context.define_external_interface_alias(&class, &alias) - } else if values.trait_exists(&class)? { - context.define_external_trait_alias(&class, &alias) - } else { - false - }; - values.bool_value(aliased) -} - -/// Reads and normalizes one `class_alias()` class-name argument. -fn eval_class_alias_name( - name: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(name.trim_start_matches('\\').to_string()) -} - -/// Evaluates `get_declared_classes/interfaces/traits()` for eval-visible declarations. -pub(in crate::interpreter) fn eval_builtin_get_declared_symbols( - name: &str, - args: &[EvalExpr], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_declared_symbols_result(name, context, values) -} - -/// Builds an indexed array for eval-visible declared class-like names. -pub(in crate::interpreter) fn eval_get_declared_symbols_result( - name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "get_declared_classes" => { - eval_dynamic_string_array_result(context.declared_class_names(), values) - } - "get_declared_interfaces" => { - eval_dynamic_string_array_result(context.declared_interface_names(), values) - } - "get_declared_traits" => { - eval_dynamic_string_array_result(context.declared_trait_names(), values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds one indexed PHP array from runtime-owned strings. -fn eval_dynamic_string_array_result( - items: &[String], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(items.len())?; - for (index, item) in items.iter().enumerate() { - let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.int(index)?; - let value = values.string(item)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Evaluates `interface_exists(...)` against generated interface-name metadata. -pub(in crate::interpreter) fn eval_builtin_interface_exists( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = match args { - [name] => eval_expr(name, context, scope, values)?, - [name, autoload] => { - let name = eval_expr(name, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - name - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_interface_exists_name(name, context, values)?; - values.bool_value(exists) -} - -/// Evaluates `interface_exists(...)` from already materialized call arguments. -pub(in crate::interpreter) fn eval_interface_exists_result( - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [name] => eval_interface_exists_name(*name, context, values)?, - [name, _autoload] => eval_interface_exists_name(*name, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP interface-name cell and probes eval and generated interface metadata. -pub(in crate::interpreter) fn eval_interface_exists_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = values.string_bytes(name)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - let name = name.trim_start_matches('\\'); - Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) -} - -/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. -pub(in crate::interpreter) fn eval_builtin_class_like_exists( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let symbol = match args { - [symbol] => eval_expr(symbol, context, scope, values)?, - [symbol, autoload] => { - let symbol = eval_expr(symbol, context, scope, values)?; - let _ = eval_expr(autoload, context, scope, values)?; - symbol - } - _ => return Err(EvalStatus::RuntimeFatal), - }; - let exists = eval_class_like_exists_name(name, symbol, context, values)?; - values.bool_value(exists) -} - -/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. -pub(in crate::interpreter) fn eval_class_like_exists_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exists = match evaluated_args { - [symbol] => eval_class_like_exists_name(name, *symbol, context, values)?, - [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. -pub(in crate::interpreter) fn eval_class_like_exists_name( - name: &str, - symbol: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let symbol = values.string_bytes(symbol)?; - let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; - let symbol = symbol.trim_start_matches('\\'); - match name { - "trait_exists" => Ok(context.has_trait(symbol) || values.trait_exists(symbol)?), - "enum_exists" => Ok(context.has_enum(symbol) || values.enum_exists(symbol)?), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs index a3b274246c..387e29bc9f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `enum_exists`. +//! Eval registry entry for `enum_exists`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-like existence probe. +//! - Class-like existence semantics are shared with `trait_exists()`. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +19,21 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `enum_exists` symbol builtin through the area dispatcher. +/// Evaluates direct `enum_exists(...)` calls through the `trait_exists` owner. pub(in crate::interpreter) fn eval_enum_exists_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_builtin_class_like_exists("enum_exists", args, context, scope, values) + super::trait_exists::eval_builtin_class_like_exists("enum_exists", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `enum_exists` symbol builtin through the area dispatcher. +/// Evaluates materialized `enum_exists(...)` arguments through the `trait_exists` owner. pub(in crate::interpreter) fn eval_enum_exists_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_class_like_exists_result("enum_exists", evaluated_args, context, values) + super::trait_exists::eval_class_like_exists_result("enum_exists", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs index 5ead21d945..ecf82b10a2 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `get_declared_classes`. +//! Eval registry entry and implementation for `get_declared_classes`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the declared-symbols helper. +//! - The shared `get_declared_*` array builder lives here because classes are +//! the primary declaration table and the interface/trait variants reuse it. eval_builtin! { name: "get_declared_classes", @@ -17,21 +18,73 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `get_declared_classes` symbol builtin through the area dispatcher. +/// Evaluates direct `get_declared_classes()` calls. pub(in crate::interpreter) fn eval_get_declared_classes_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_builtin_get_declared_symbols("get_declared_classes", args, context, values) + eval_builtin_get_declared_symbols("get_declared_classes", args, context, values) } -/// Dispatches evaluated-argument calls for the `get_declared_classes` symbol builtin through the area dispatcher. +/// Evaluates materialized `get_declared_classes()` arguments. pub(in crate::interpreter) fn eval_get_declared_classes_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if evaluated_args.is_empty() { super::class_names::eval_get_declared_symbols_result("get_declared_classes", context, values) } else { Err(EvalStatus::RuntimeFatal) } + if evaluated_args.is_empty() { + eval_get_declared_symbols_result("get_declared_classes", context, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Evaluates `get_declared_classes/interfaces/traits()` for eval-visible declarations. +pub(in crate::interpreter) fn eval_builtin_get_declared_symbols( + name: &str, + args: &[EvalExpr], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_declared_symbols_result(name, context, values) +} + +/// Builds an indexed array for eval-visible declared class-like names. +pub(in crate::interpreter) fn eval_get_declared_symbols_result( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "get_declared_classes" => { + eval_dynamic_string_array_result(context.declared_class_names(), values) + } + "get_declared_interfaces" => { + eval_dynamic_string_array_result(context.declared_interface_names(), values) + } + "get_declared_traits" => { + eval_dynamic_string_array_result(context.declared_trait_names(), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds one indexed PHP array from runtime-owned strings. +fn eval_dynamic_string_array_result( + items: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(items.len())?; + for (index, item) in items.iter().enumerate() { + let index = i64::try_from(index).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.int(index)?; + let value = values.string(item)?; + result = values.array_set(result, key, value)?; + } + Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs index 6de8485593..05baddd173 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `get_declared_interfaces`. +//! Eval registry entry for `get_declared_interfaces`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the declared-symbols helper. +//! - Array construction is shared with `get_declared_classes()`. eval_builtin! { name: "get_declared_interfaces", @@ -17,21 +17,34 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `get_declared_interfaces` symbol builtin through the area dispatcher. +/// Evaluates direct `get_declared_interfaces()` calls. pub(in crate::interpreter) fn eval_get_declared_interfaces_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_builtin_get_declared_symbols("get_declared_interfaces", args, context, values) + super::get_declared_classes::eval_builtin_get_declared_symbols( + "get_declared_interfaces", + args, + context, + values, + ) } -/// Dispatches evaluated-argument calls for the `get_declared_interfaces` symbol builtin through the area dispatcher. +/// Evaluates materialized `get_declared_interfaces()` arguments. pub(in crate::interpreter) fn eval_get_declared_interfaces_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if evaluated_args.is_empty() { super::class_names::eval_get_declared_symbols_result("get_declared_interfaces", context, values) } else { Err(EvalStatus::RuntimeFatal) } + if evaluated_args.is_empty() { + super::get_declared_classes::eval_get_declared_symbols_result( + "get_declared_interfaces", + context, + values, + ) + } else { + Err(EvalStatus::RuntimeFatal) + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs index 48d799f1c7..1549d983ae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `get_declared_traits`. +//! Eval registry entry for `get_declared_traits`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the declared-symbols helper. +//! - Array construction is shared with `get_declared_classes()`. eval_builtin! { name: "get_declared_traits", @@ -17,21 +17,34 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `get_declared_traits` symbol builtin through the area dispatcher. +/// Evaluates direct `get_declared_traits()` calls. pub(in crate::interpreter) fn eval_get_declared_traits_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_builtin_get_declared_symbols("get_declared_traits", args, context, values) + super::get_declared_classes::eval_builtin_get_declared_symbols( + "get_declared_traits", + args, + context, + values, + ) } -/// Dispatches evaluated-argument calls for the `get_declared_traits` symbol builtin through the area dispatcher. +/// Evaluates materialized `get_declared_traits()` arguments. pub(in crate::interpreter) fn eval_get_declared_traits_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if evaluated_args.is_empty() { super::class_names::eval_get_declared_symbols_result("get_declared_traits", context, values) } else { Err(EvalStatus::RuntimeFatal) } + if evaluated_args.is_empty() { + super::get_declared_classes::eval_get_declared_symbols_result( + "get_declared_traits", + context, + values, + ) + } else { + Err(EvalStatus::RuntimeFatal) + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs index b61a42a158..e64befd12e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `interface_exists`. +//! Eval registry entry and implementation for `interface_exists`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the interface-existence probe. +//! - Lookup checks eval interface declarations before generated/AOT runtime metadata. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +19,67 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `interface_exists` symbol builtin through the area dispatcher. +/// Evaluates direct `interface_exists(...)` calls against eval and generated metadata. pub(in crate::interpreter) fn eval_interface_exists_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_builtin_interface_exists(args, context, scope, values) + eval_builtin_interface_exists(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `interface_exists` symbol builtin through the area dispatcher. +/// Evaluates materialized `interface_exists(...)` arguments. pub(in crate::interpreter) fn eval_interface_exists_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_interface_exists_result(evaluated_args, context, values) + eval_interface_exists_result(evaluated_args, context, values) +} + +/// Evaluates `interface_exists(...)` against generated interface-name metadata. +pub(in crate::interpreter) fn eval_builtin_interface_exists( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = match args { + [name] => eval_expr(name, context, scope, values)?, + [name, autoload] => { + let name = eval_expr(name, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + name + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_interface_exists_name(name, context, values)?; + values.bool_value(exists) +} + +/// Evaluates `interface_exists(...)` from already materialized call arguments. +pub(in crate::interpreter) fn eval_interface_exists_result( + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [name] => eval_interface_exists_name(*name, context, values)?, + [name, _autoload] => eval_interface_exists_name(*name, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP interface-name cell and probes eval and generated interface metadata. +pub(in crate::interpreter) fn eval_interface_exists_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = values.string_bytes(name)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + let name = name.trim_start_matches('\\'); + Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs index 85106e3b92..46201190d8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `trait_exists`. +//! Eval registry entry and implementation for `trait_exists`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-like existence probe. +//! - The shared trait/enum existence probe lives here and `enum_exists()` +//! calls it explicitly. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +20,74 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `trait_exists` symbol builtin through the area dispatcher. +/// Evaluates direct `trait_exists(...)` calls. pub(in crate::interpreter) fn eval_trait_exists_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_builtin_class_like_exists("trait_exists", args, context, scope, values) + eval_builtin_class_like_exists("trait_exists", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `trait_exists` symbol builtin through the area dispatcher. +/// Evaluates materialized `trait_exists(...)` arguments. pub(in crate::interpreter) fn eval_trait_exists_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::class_names::eval_class_like_exists_result("trait_exists", evaluated_args, context, values) + eval_class_like_exists_result("trait_exists", evaluated_args, context, values) +} + +/// Evaluates `trait_exists(...)` and `enum_exists(...)` against generated metadata. +pub(in crate::interpreter) fn eval_builtin_class_like_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = match args { + [symbol] => eval_expr(symbol, context, scope, values)?, + [symbol, autoload] => { + let symbol = eval_expr(symbol, context, scope, values)?; + let _ = eval_expr(autoload, context, scope, values)?; + symbol + } + _ => return Err(EvalStatus::RuntimeFatal), + }; + let exists = eval_class_like_exists_name(name, symbol, context, values)?; + values.bool_value(exists) +} + +/// Evaluates materialized `trait_exists(...)` or `enum_exists(...)` arguments. +pub(in crate::interpreter) fn eval_class_like_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exists = match evaluated_args { + [symbol] => eval_class_like_exists_name(name, *symbol, context, values)?, + [symbol, _autoload] => eval_class_like_exists_name(name, *symbol, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Normalizes a PHP class-like name cell and probes generated trait or enum metadata. +pub(in crate::interpreter) fn eval_class_like_exists_name( + name: &str, + symbol: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let symbol = values.string_bytes(symbol)?; + let symbol = String::from_utf8(symbol).map_err(|_| EvalStatus::RuntimeFatal)?; + let symbol = symbol.trim_start_matches('\\'); + match name { + "trait_exists" => Ok(context.has_trait(symbol) || values.trait_exists(symbol)?), + "enum_exists" => Ok(context.has_enum(symbol) || values.enum_exists(symbol)?), + _ => Err(EvalStatus::UnsupportedConstruct), + } } From 8a2c8b73f31fa1d571f2a01eae094f41b7aae47a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:24:50 +0200 Subject: [PATCH 1132/1208] refactor(magician): move spl autoload builtins home --- .../src/interpreter/builtins/mod.rs | 2 - .../src/interpreter/builtins/spl_autoload.rs | 132 ------------------ .../builtins/symbols/spl_autoload.rs | 44 +++++- .../builtins/symbols/spl_autoload_call.rs | 18 ++- .../symbols/spl_autoload_extensions.rs | 46 +++++- .../symbols/spl_autoload_functions.rs | 33 ++++- .../builtins/symbols/spl_autoload_register.rs | 44 +++++- .../symbols/spl_autoload_unregister.rs | 22 ++- 8 files changed, 171 insertions(+), 170 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/spl_autoload.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/mod.rs b/crates/elephc-magician/src/interpreter/builtins/mod.rs index 04e604e600..25e111e44b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/mod.rs @@ -29,7 +29,6 @@ mod ref_targets; mod regex; mod registry; mod scalars; -mod spl_autoload; mod spec; mod string; mod symbols; @@ -50,7 +49,6 @@ pub(super) use ref_targets::*; pub(super) use regex::*; pub(super) use registry::*; pub(super) use scalars::*; -pub(super) use spl_autoload::*; pub(super) use string::*; pub(super) use symbols::*; pub(super) use time::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/spl_autoload.rs b/crates/elephc-magician/src/interpreter/builtins/spl_autoload.rs deleted file mode 100644 index ca09135061..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/spl_autoload.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Purpose: -//! Implements eval SPL autoload helper builtins. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! - Dynamic callable dispatch under `builtins::registry::dispatch`. -//! -//! Key details: -//! - The main EIR backend models autoload registration as conservative stubs. -//! - Eval mirrors that behavior while keeping `spl_autoload_extensions()` as -//! eval-local mutable state on the context. - -use super::super::*; - -/// Evaluates boolean SPL autoload registration stubs. -pub(in crate::interpreter) fn eval_builtin_spl_autoload_bool( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "spl_autoload_register" if args.len() <= 3 => {} - "spl_autoload_unregister" if args.len() == 1 => {} - _ => return Err(EvalStatus::RuntimeFatal), - } - for arg in args { - let _ = eval_expr(arg, context, scope, values)?; - } - values.bool_value(true) -} - -/// Evaluates materialized boolean SPL autoload registration stubs. -pub(in crate::interpreter) fn eval_spl_autoload_bool_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "spl_autoload_register" if evaluated_args.len() <= 3 => values.bool_value(true), - "spl_autoload_unregister" if evaluated_args.len() == 1 => values.bool_value(true), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates void SPL autoload call stubs. -pub(in crate::interpreter) fn eval_builtin_spl_autoload_void( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "spl_autoload_call" if args.len() == 1 => {} - "spl_autoload" if (1..=2).contains(&args.len()) => {} - _ => return Err(EvalStatus::RuntimeFatal), - } - for arg in args { - let _ = eval_expr(arg, context, scope, values)?; - } - eval_spl_autoload_void_result(name, args, values) -} - -/// Evaluates materialized void SPL autoload call stubs. -pub(in crate::interpreter) fn eval_spl_autoload_void_result( - name: &str, - evaluated_args: &[T], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "spl_autoload_call" if evaluated_args.len() == 1 => values.null(), - "spl_autoload" if (1..=2).contains(&evaluated_args.len()) => values.null(), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates `spl_autoload_functions()`. -pub(in crate::interpreter) fn eval_builtin_spl_autoload_functions( - args: &[EvalExpr], - _context: &mut ElephcEvalContext, - _scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_spl_autoload_functions_result(args, values) -} - -/// Evaluates materialized `spl_autoload_functions()`. -pub(in crate::interpreter) fn eval_spl_autoload_functions_result( - evaluated_args: &[T], - values: &mut impl RuntimeValueOps, -) -> Result { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - values.array_new(0) -} - -/// Evaluates `spl_autoload_extensions()`. -pub(in crate::interpreter) fn eval_builtin_spl_autoload_extensions( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = match args { - [] => Vec::new(), - [extensions] => vec![eval_expr(extensions, context, scope, values)?], - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_spl_autoload_extensions_result(&evaluated_args, context, values) -} - -/// Evaluates materialized `spl_autoload_extensions()` arguments. -pub(in crate::interpreter) fn eval_spl_autoload_extensions_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match evaluated_args { - [] => {} - [extensions] if values.type_tag(*extensions)? == EVAL_TAG_NULL => {} - [extensions] => { - let extensions = values.string_bytes(*extensions)?; - let extensions = String::from_utf8(extensions).map_err(|_| EvalStatus::RuntimeFatal)?; - context.set_spl_autoload_extensions(extensions); - } - _ => return Err(EvalStatus::RuntimeFatal), - } - values.string(context.spl_autoload_extensions()) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs index 14cf631cb3..6d38e5e963 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_autoload`. +//! Eval registry entry and implementation for `spl_autoload`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the SPL autoload stub. +//! - Eval mirrors the main backend's conservative no-op autoload behavior. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +19,53 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `spl_autoload` symbol builtin through the area dispatcher. +/// Evaluates direct `spl_autoload(...)` calls as a no-op stub. pub(in crate::interpreter) fn eval_spl_autoload_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_spl_autoload_void("spl_autoload", args, context, scope, values) + eval_builtin_spl_autoload_void("spl_autoload", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `spl_autoload` symbol builtin through the area dispatcher. +/// Evaluates materialized `spl_autoload(...)` arguments as a no-op stub. pub(in crate::interpreter) fn eval_spl_autoload_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_spl_autoload_void_result("spl_autoload", evaluated_args, values) + eval_spl_autoload_void_result("spl_autoload", evaluated_args, values) +} + +/// Evaluates void SPL autoload call stubs. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_void( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_call" if args.len() == 1 => {} + "spl_autoload" if (1..=2).contains(&args.len()) => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + for arg in args { + let _ = eval_expr(arg, context, scope, values)?; + } + eval_spl_autoload_void_result(name, args, values) +} + +/// Evaluates materialized void SPL autoload call stubs. +pub(in crate::interpreter) fn eval_spl_autoload_void_result( + name: &str, + evaluated_args: &[T], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_call" if evaluated_args.len() == 1 => values.null(), + "spl_autoload" if (1..=2).contains(&evaluated_args.len()) => values.null(), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs index 2ffb3fa6c0..116ff1f4ae 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_autoload_call`. +//! Eval registry entry for `spl_autoload_call`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the SPL autoload stub. +//! - No-op stub behavior is shared with `spl_autoload()`. eval_builtin! { name: "spl_autoload_call", @@ -17,21 +17,27 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `spl_autoload_call` symbol builtin through the area dispatcher. +/// Evaluates direct `spl_autoload_call(...)` calls through the `spl_autoload` owner. pub(in crate::interpreter) fn eval_spl_autoload_call_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_spl_autoload_void("spl_autoload_call", args, context, scope, values) + super::spl_autoload::eval_builtin_spl_autoload_void( + "spl_autoload_call", + args, + context, + scope, + values, + ) } -/// Dispatches evaluated-argument calls for the `spl_autoload_call` symbol builtin through the area dispatcher. +/// Evaluates materialized `spl_autoload_call(...)` arguments through the `spl_autoload` owner. pub(in crate::interpreter) fn eval_spl_autoload_call_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_spl_autoload_void_result("spl_autoload_call", evaluated_args, values) + super::spl_autoload::eval_spl_autoload_void_result("spl_autoload_call", evaluated_args, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs index c3761684ab..37f152f305 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_autoload_extensions`. +//! Eval registry entry and implementation for `spl_autoload_extensions`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to eval-local autoload extension state. +//! - The extension list is eval-local mutable state on the context. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +19,55 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `spl_autoload_extensions` symbol builtin through the area dispatcher. +/// Evaluates direct `spl_autoload_extensions(...)` calls. pub(in crate::interpreter) fn eval_spl_autoload_extensions_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_spl_autoload_extensions(args, context, scope, values) + eval_builtin_spl_autoload_extensions(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `spl_autoload_extensions` symbol builtin through the area dispatcher. +/// Evaluates materialized `spl_autoload_extensions(...)` arguments. pub(in crate::interpreter) fn eval_spl_autoload_extensions_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_spl_autoload_extensions_result(evaluated_args, context, values) + eval_spl_autoload_extensions_result(evaluated_args, context, values) +} + +/// Evaluates `spl_autoload_extensions()`. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_extensions( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = match args { + [] => Vec::new(), + [extensions] => vec![eval_expr(extensions, context, scope, values)?], + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_spl_autoload_extensions_result(&evaluated_args, context, values) +} + +/// Evaluates materialized `spl_autoload_extensions()` arguments. +pub(in crate::interpreter) fn eval_spl_autoload_extensions_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [] => {} + [extensions] if values.type_tag(*extensions)? == EVAL_TAG_NULL => {} + [extensions] => { + let extensions = values.string_bytes(*extensions)?; + let extensions = String::from_utf8(extensions).map_err(|_| EvalStatus::RuntimeFatal)?; + context.set_spl_autoload_extensions(extensions); + } + _ => return Err(EvalStatus::RuntimeFatal), + } + values.string(context.spl_autoload_extensions()) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs index 437a1256d9..ec4de7ef4b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_autoload_functions`. +//! Eval registry entry and implementation for `spl_autoload_functions`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the SPL autoload stub. +//! - Eval models an empty autoload function table. eval_builtin! { name: "spl_autoload_functions", @@ -17,21 +17,42 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `spl_autoload_functions` symbol builtin through the area dispatcher. +/// Evaluates direct `spl_autoload_functions()` calls. pub(in crate::interpreter) fn eval_spl_autoload_functions_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_spl_autoload_functions(args, context, scope, values) + eval_builtin_spl_autoload_functions(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `spl_autoload_functions` symbol builtin through the area dispatcher. +/// Evaluates materialized `spl_autoload_functions()` arguments. pub(in crate::interpreter) fn eval_spl_autoload_functions_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_spl_autoload_functions_result(evaluated_args, values) + eval_spl_autoload_functions_result(evaluated_args, values) +} + +/// Evaluates `spl_autoload_functions()`. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_functions( + args: &[EvalExpr], + _context: &mut ElephcEvalContext, + _scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_spl_autoload_functions_result(args, values) +} + +/// Evaluates materialized `spl_autoload_functions()`. +pub(in crate::interpreter) fn eval_spl_autoload_functions_result( + evaluated_args: &[T], + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + values.array_new(0) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs index 7ae8a706db..6c0e029f39 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_autoload_register`. +//! Eval registry entry and implementation for `spl_autoload_register`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the SPL autoload registration stub. +//! - Registration is a conservative successful stub, mirroring the main backend. use super::super::spec::EvalBuiltinDefaultValue; @@ -23,21 +23,53 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `spl_autoload_register` symbol builtin through the area dispatcher. +/// Evaluates direct `spl_autoload_register(...)` calls as a successful stub. pub(in crate::interpreter) fn eval_spl_autoload_register_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_spl_autoload_bool("spl_autoload_register", args, context, scope, values) + eval_builtin_spl_autoload_bool("spl_autoload_register", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `spl_autoload_register` symbol builtin through the area dispatcher. +/// Evaluates materialized `spl_autoload_register(...)` arguments as a successful stub. pub(in crate::interpreter) fn eval_spl_autoload_register_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_spl_autoload_bool_result("spl_autoload_register", evaluated_args, values) + eval_spl_autoload_bool_result("spl_autoload_register", evaluated_args, values) +} + +/// Evaluates boolean SPL autoload registration stubs. +pub(in crate::interpreter) fn eval_builtin_spl_autoload_bool( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_register" if args.len() <= 3 => {} + "spl_autoload_unregister" if args.len() == 1 => {} + _ => return Err(EvalStatus::RuntimeFatal), + } + for arg in args { + let _ = eval_expr(arg, context, scope, values)?; + } + values.bool_value(true) +} + +/// Evaluates materialized boolean SPL autoload registration stubs. +pub(in crate::interpreter) fn eval_spl_autoload_bool_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match name { + "spl_autoload_register" if evaluated_args.len() <= 3 => values.bool_value(true), + "spl_autoload_unregister" if evaluated_args.len() == 1 => values.bool_value(true), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs index 16f6fdd02d..57b7ddd6bf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_autoload_unregister`. +//! Eval registry entry for `spl_autoload_unregister`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the SPL autoload registration stub. +//! - Registration stub behavior is shared with `spl_autoload_register()`. eval_builtin! { name: "spl_autoload_unregister", @@ -17,21 +17,31 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `spl_autoload_unregister` symbol builtin through the area dispatcher. +/// Evaluates direct `spl_autoload_unregister(...)` calls through the registration owner. pub(in crate::interpreter) fn eval_spl_autoload_unregister_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_spl_autoload_bool("spl_autoload_unregister", args, context, scope, values) + super::spl_autoload_register::eval_builtin_spl_autoload_bool( + "spl_autoload_unregister", + args, + context, + scope, + values, + ) } -/// Dispatches evaluated-argument calls for the `spl_autoload_unregister` symbol builtin through the area dispatcher. +/// Evaluates materialized `spl_autoload_unregister(...)` arguments through the registration owner. pub(in crate::interpreter) fn eval_spl_autoload_unregister_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_spl_autoload_bool_result("spl_autoload_unregister", evaluated_args, values) + super::spl_autoload_register::eval_spl_autoload_bool_result( + "spl_autoload_unregister", + evaluated_args, + values, + ) } From 619e6baa1d026e91018691c0f109b35ea1a928af Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:27:36 +0200 Subject: [PATCH 1133/1208] refactor(magician): move scalar introspection builtins home --- .../src/interpreter/builtins/scalars/types.rs | 203 +----------------- .../src/interpreter/builtins/symbols.rs | 2 + .../builtins/symbols/get_called_class.rs | 46 +++- .../interpreter/builtins/symbols/get_class.rs | 63 +++++- .../builtins/symbols/get_parent_class.rs | 72 ++++++- .../builtins/symbols/get_resource_id.rs | 46 +++- .../builtins/symbols/get_resource_type.rs | 25 ++- .../builtins/symbols/spl_object_hash.rs | 23 +- .../builtins/symbols/spl_object_id.rs | 47 +++- 9 files changed, 286 insertions(+), 241 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs index 218c9faf20..b787cdff2d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs +++ b/crates/elephc-magician/src/interpreter/builtins/scalars/types.rs @@ -1,210 +1,15 @@ //! Purpose: -//! Shared scalar type helpers plus class, object, and resource introspection builtins. +//! Shared scalar type helpers used by eval builtins and dynamic conversions. //! //! Called from: -//! - `crate::interpreter::builtins::scalars` re-exports. +//! - `crate::interpreter::builtins::types` and dynamic value conversion paths. //! //! Key details: -//! - Runtime cells remain opaque and shared PHP coercions flow through -//! `RuntimeValueOps`. +//! - PHP-visible symbol/introspection builtins live in their focused symbol +//! builtin files; this module keeps cross-domain scalar predicates only. use super::super::super::*; -/// Evaluates PHP's `get_called_class()` against the current eval method scope. -pub(in crate::interpreter) fn eval_builtin_get_called_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_get_called_class_result(context, values) -} - -/// Returns the current late-static-bound class name or throws PHP's class-scope error. -pub(in crate::interpreter) fn eval_get_called_class_result( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(class_name) = context - .current_called_class_scope() - .or_else(|| context.current_class_scope()) - else { - return eval_throw_error( - "get_called_class() must be called from within a class", - context, - values, - ); - }; - values.string(class_name.trim_start_matches('\\')) -} - -/// Evaluates PHP's `get_class(...)` over one eval object expression. -pub(in crate::interpreter) fn eval_builtin_get_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_get_class_no_arg_result(context, values), - [object] => { - let object = eval_expr(object, context, scope, values)?; - eval_get_class_result(object, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Resolves PHP's deprecated no-arg `get_class()` form from the current class scope. -pub(in crate::interpreter) fn eval_get_class_no_arg_result( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(class_name) = context.current_class_scope() else { - return eval_throw_error( - "get_class() without arguments must be called from within a class", - context, - values, - ); - }; - values.string(class_name.trim_start_matches('\\')) -} - -/// Resolves the PHP-visible class name for one already materialized object cell. -pub(in crate::interpreter) fn eval_get_class_result( - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Ok(identity) = values.object_identity(object) { - if let Some(class_name) = context.dynamic_object_class_name(identity) { - return values.string(&class_name); - } - } - values.object_class_name(object) -} - -/// Evaluates PHP's SPL object identity builtins over one eval object expression. -pub(in crate::interpreter) fn eval_builtin_spl_object_identity( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_spl_object_identity_result(name, object, values) -} - -/// Returns the unboxed object-payload identity in the native SPL builtin spelling. -pub(in crate::interpreter) fn eval_spl_object_identity_result( - name: &str, - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let identity = values.object_identity(object)? as i64; - match name { - "spl_object_id" => values.int(identity), - "spl_object_hash" => values.string(&identity.to_string()), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - -/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. -pub(in crate::interpreter) fn eval_builtin_get_parent_class( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [] => eval_get_parent_class_no_arg_result(context, values), - [object_or_class] => { - let object_or_class = eval_expr(object_or_class, context, scope, values)?; - eval_get_parent_class_result(object_or_class, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Resolves PHP's deprecated no-arg `get_parent_class()` form from the current class scope. -pub(in crate::interpreter) fn eval_get_parent_class_no_arg_result( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(class_name) = context.current_class_scope() else { - return values.string(""); - }; - let class_name = values.string(class_name.trim_start_matches('\\'))?; - eval_get_parent_class_result(class_name, context, values) -} - -/// Resolves the PHP-visible parent class name for one object or class-name cell. -pub(in crate::interpreter) fn eval_get_parent_class_result( - object_or_class: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Ok(identity) = values.object_identity(object_or_class) { - if let Some(class_name) = context.dynamic_object_class_name(identity) { - if let Some(parent) = context.class_parent_names(&class_name).into_iter().next() { - return values.string(&parent); - } - return values.string(""); - } - } - if values.type_tag(object_or_class)? == EVAL_TAG_STRING { - let name = values.string_bytes(object_or_class)?; - let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; - if context.class(&name).is_some() { - if let Some(parent) = context.class_parent_names(&name).into_iter().next() { - return values.string(&parent); - } - return values.string(""); - } - } - values.parent_class_name(object_or_class) -} - -/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. -pub(in crate::interpreter) fn eval_builtin_resource_introspection( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [resource] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let resource = eval_expr(resource, context, scope, values)?; - eval_resource_introspection_result(name, resource, values) -} - -/// Evaluates a materialized resource introspection builtin argument. -pub(in crate::interpreter) fn eval_resource_introspection_result( - name: &str, - resource: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(resource)? != EVAL_TAG_RESOURCE { - return Err(EvalStatus::RuntimeFatal); - } - match name { - "get_resource_type" => values.string("stream"), - "get_resource_id" => values.cast_int(resource), - _ => Err(EvalStatus::UnsupportedConstruct), - } -} - /// Returns the PHP-visible type name for a concrete eval runtime tag. pub(in crate::interpreter) fn eval_gettype_name(tag: u64) -> &'static str { match tag { diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index b22fd617e8..e7b66c3034 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -56,5 +56,7 @@ mod unset; pub(in crate::interpreter) use callable_probe::*; pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; pub(in crate::interpreter) use function_exists::eval_function_probe_exists; +pub(in crate::interpreter) use get_class::eval_get_class_result; +pub(in crate::interpreter) use get_parent_class::eval_get_parent_class_result; pub(in crate::interpreter) use is_a::dynamic_object_is_a; use super::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs index 5b1bc51791..4021a30bc5 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `get_called_class`. +//! Eval registry entry and implementation for `get_called_class`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the current-class scope helper. +//! - The result uses the late-static-bound class scope when present, matching PHP. eval_builtin! { name: "get_called_class", @@ -17,21 +17,55 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `get_called_class` symbol builtin through the area dispatcher. +/// Evaluates direct `get_called_class()` calls. pub(in crate::interpreter) fn eval_get_called_class_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_get_called_class(args, context, values) + eval_builtin_get_called_class(args, context, values) } -/// Dispatches evaluated-argument calls for the `get_called_class` symbol builtin through the area dispatcher. +/// Evaluates materialized `get_called_class()` arguments. pub(in crate::interpreter) fn eval_get_called_class_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if evaluated_args.is_empty() { super::super::eval_get_called_class_result(context, values) } else { Err(EvalStatus::RuntimeFatal) } + if evaluated_args.is_empty() { + eval_get_called_class_result(context, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Evaluates PHP's `get_called_class()` against the current eval method scope. +pub(in crate::interpreter) fn eval_builtin_get_called_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_get_called_class_result(context, values) +} + +/// Returns the current late-static-bound class name or throws PHP's class-scope error. +pub(in crate::interpreter) fn eval_get_called_class_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + else { + return eval_throw_error( + "get_called_class() must be called from within a class", + context, + values, + ); + }; + values.string(class_name.trim_start_matches('\\')) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs index 7fe1f3132e..c838be1611 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `get_class`. +//! Eval registry entry and implementation for `get_class`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-name introspection helper. +//! - Eval-created object class names are resolved from eval context before +//! falling back to runtime object metadata. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +20,71 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `get_class` symbol builtin through the area dispatcher. +/// Evaluates direct `get_class(...)` calls. pub(in crate::interpreter) fn eval_get_class_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_get_class(args, context, scope, values) + eval_builtin_get_class(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `get_class` symbol builtin through the area dispatcher. +/// Evaluates materialized `get_class(...)` arguments. pub(in crate::interpreter) fn eval_get_class_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match evaluated_args { [] => super::super::eval_get_class_no_arg_result(context, values), [object] => super::super::eval_get_class_result(*object, context, values), _ => Err(EvalStatus::RuntimeFatal), } + match evaluated_args { + [] => eval_get_class_no_arg_result(context, values), + [object] => eval_get_class_result(*object, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP's `get_class(...)` over one eval object expression. +pub(in crate::interpreter) fn eval_builtin_get_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_get_class_no_arg_result(context, values), + [object] => { + let object = eval_expr(object, context, scope, values)?; + eval_get_class_result(object, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves PHP's deprecated no-arg `get_class()` form from the current class scope. +pub(in crate::interpreter) fn eval_get_class_no_arg_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context.current_class_scope() else { + return eval_throw_error( + "get_class() without arguments must be called from within a class", + context, + values, + ); + }; + values.string(class_name.trim_start_matches('\\')) +} + +/// Resolves the PHP-visible class name for one already materialized object cell. +pub(in crate::interpreter) fn eval_get_class_result( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return values.string(&class_name); + } + } + values.object_class_name(object) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs index 196c73605a..1e28c6fdee 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `get_parent_class`. +//! Eval registry entry and implementation for `get_parent_class`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the parent-class introspection helper. +//! - Eval-created object and class-string parents are resolved before runtime fallback. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +19,81 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `get_parent_class` symbol builtin through the area dispatcher. +/// Evaluates direct `get_parent_class(...)` calls. pub(in crate::interpreter) fn eval_get_parent_class_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_get_parent_class(args, context, scope, values) + eval_builtin_get_parent_class(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `get_parent_class` symbol builtin through the area dispatcher. +/// Evaluates materialized `get_parent_class(...)` arguments. pub(in crate::interpreter) fn eval_get_parent_class_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match evaluated_args { [] => super::super::eval_get_parent_class_no_arg_result(context, values), [object_or_class] => super::super::eval_get_parent_class_result(*object_or_class, context, values), _ => Err(EvalStatus::RuntimeFatal), } + match evaluated_args { + [] => eval_get_parent_class_no_arg_result(context, values), + [object_or_class] => eval_get_parent_class_result(*object_or_class, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP's `get_parent_class(...)` over one eval object or class-name expression. +pub(in crate::interpreter) fn eval_builtin_get_parent_class( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [] => eval_get_parent_class_no_arg_result(context, values), + [object_or_class] => { + let object_or_class = eval_expr(object_or_class, context, scope, values)?; + eval_get_parent_class_result(object_or_class, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves PHP's deprecated no-arg `get_parent_class()` form from the current class scope. +pub(in crate::interpreter) fn eval_get_parent_class_no_arg_result( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context.current_class_scope() else { + return values.string(""); + }; + let class_name = values.string(class_name.trim_start_matches('\\'))?; + eval_get_parent_class_result(class_name, context, values) +} + +/// Resolves the PHP-visible parent class name for one object or class-name cell. +pub(in crate::interpreter) fn eval_get_parent_class_result( + object_or_class: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object_or_class) { + if let Some(class_name) = context.dynamic_object_class_name(identity) { + if let Some(parent) = context.class_parent_names(&class_name).into_iter().next() { + return values.string(&parent); + } + return values.string(""); + } + } + if values.type_tag(object_or_class)? == EVAL_TAG_STRING { + let name = values.string_bytes(object_or_class)?; + let name = String::from_utf8(name).map_err(|_| EvalStatus::RuntimeFatal)?; + if context.class(&name).is_some() { + if let Some(parent) = context.class_parent_names(&name).into_iter().next() { + return values.string(&parent); + } + return values.string(""); + } + } + values.parent_class_name(object_or_class) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs index dc19904b88..3b891bdc13 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `get_resource_id`. +//! Eval registry entry and implementation for `get_resource_id`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the resource introspection helper. +//! - Resource id/type support is shared with `get_resource_type()`. eval_builtin! { name: "get_resource_id", @@ -17,21 +17,55 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `get_resource_id` symbol builtin through the area dispatcher. +/// Evaluates direct `get_resource_id(...)` calls. pub(in crate::interpreter) fn eval_get_resource_id_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_resource_introspection("get_resource_id", args, context, scope, values) + eval_builtin_resource_introspection("get_resource_id", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `get_resource_id` symbol builtin through the area dispatcher. +/// Evaluates materialized `get_resource_id(...)` arguments. pub(in crate::interpreter) fn eval_get_resource_id_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match evaluated_args { [resource] => super::super::eval_resource_introspection_result("get_resource_id", *resource, values), _ => Err(EvalStatus::RuntimeFatal), } + match evaluated_args { + [resource] => eval_resource_introspection_result("get_resource_id", *resource, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates `get_resource_type(...)` and `get_resource_id(...)` over one eval value. +pub(in crate::interpreter) fn eval_builtin_resource_introspection( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [resource] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let resource = eval_expr(resource, context, scope, values)?; + eval_resource_introspection_result(name, resource, values) +} + +/// Evaluates a materialized resource introspection builtin argument. +pub(in crate::interpreter) fn eval_resource_introspection_result( + name: &str, + resource: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(resource)? != EVAL_TAG_RESOURCE { + return Err(EvalStatus::RuntimeFatal); + } + match name { + "get_resource_type" => values.string("stream"), + "get_resource_id" => values.cast_int(resource), + _ => Err(EvalStatus::UnsupportedConstruct), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs index dddb86052c..bd500e3ff1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `get_resource_type`. +//! Eval registry entry for `get_resource_type`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the resource introspection helper. +//! - Resource introspection implementation is shared with `get_resource_id()`. eval_builtin! { name: "get_resource_type", @@ -17,21 +17,34 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `get_resource_type` symbol builtin through the area dispatcher. +/// Evaluates direct `get_resource_type(...)` calls through the `get_resource_id` owner. pub(in crate::interpreter) fn eval_get_resource_type_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_resource_introspection("get_resource_type", args, context, scope, values) + super::get_resource_id::eval_builtin_resource_introspection( + "get_resource_type", + args, + context, + scope, + values, + ) } -/// Dispatches evaluated-argument calls for the `get_resource_type` symbol builtin through the area dispatcher. +/// Evaluates materialized `get_resource_type(...)` arguments through the `get_resource_id` owner. pub(in crate::interpreter) fn eval_get_resource_type_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match evaluated_args { [resource] => super::super::eval_resource_introspection_result("get_resource_type", *resource, values), _ => Err(EvalStatus::RuntimeFatal), } + match evaluated_args { + [resource] => super::get_resource_id::eval_resource_introspection_result( + "get_resource_type", + *resource, + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs index 887f4a9373..d180480ad4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_object_hash`. +//! Eval registry entry for `spl_object_hash`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the SPL object identity helper. +//! - Object identity semantics are shared with `spl_object_id()`. eval_builtin! { name: "spl_object_hash", @@ -17,21 +17,32 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `spl_object_hash` symbol builtin through the area dispatcher. +/// Evaluates direct `spl_object_hash(...)` calls through the `spl_object_id` owner. pub(in crate::interpreter) fn eval_spl_object_hash_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_spl_object_identity("spl_object_hash", args, context, scope, values) + super::spl_object_id::eval_builtin_spl_object_identity( + "spl_object_hash", + args, + context, + scope, + values, + ) } -/// Dispatches evaluated-argument calls for the `spl_object_hash` symbol builtin through the area dispatcher. +/// Evaluates materialized `spl_object_hash(...)` arguments through the `spl_object_id` owner. pub(in crate::interpreter) fn eval_spl_object_hash_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match evaluated_args { [object] => super::super::eval_spl_object_identity_result("spl_object_hash", *object, values), _ => Err(EvalStatus::RuntimeFatal), } + match evaluated_args { + [object] => { + super::spl_object_id::eval_spl_object_identity_result("spl_object_hash", *object, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs index 5a3f8b3bf5..1c567a6241 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_object_id`. +//! Eval registry entry and implementation for `spl_object_id`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the SPL object identity helper. +//! - `spl_object_hash()` shares the same object-identity implementation. eval_builtin! { name: "spl_object_id", @@ -17,21 +17,56 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `spl_object_id` symbol builtin through the area dispatcher. +/// Evaluates direct `spl_object_id(...)` calls. pub(in crate::interpreter) fn eval_spl_object_id_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_spl_object_identity("spl_object_id", args, context, scope, values) + eval_builtin_spl_object_identity("spl_object_id", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `spl_object_id` symbol builtin through the area dispatcher. +/// Evaluates materialized `spl_object_id(...)` arguments. pub(in crate::interpreter) fn eval_spl_object_id_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - match evaluated_args { [object] => super::super::eval_spl_object_identity_result("spl_object_id", *object, values), _ => Err(EvalStatus::RuntimeFatal), } + match evaluated_args { + [object] => eval_spl_object_identity_result("spl_object_id", *object, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates PHP's SPL object identity builtins over one eval object expression. +pub(in crate::interpreter) fn eval_builtin_spl_object_identity( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_spl_object_identity_result(name, object, values) +} + +/// Returns the unboxed object-payload identity in the native SPL builtin spelling. +pub(in crate::interpreter) fn eval_spl_object_identity_result( + name: &str, + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)? as i64; + match name { + "spl_object_id" => values.int(identity), + "spl_object_hash" => values.string(&identity.to_string()), + _ => Err(EvalStatus::UnsupportedConstruct), + } } From beffb484057b453e494f0037307072eb2b3f74b9 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:29:03 +0200 Subject: [PATCH 1134/1208] refactor(magician): move is_callable implementation home --- .../src/interpreter/builtins/symbols.rs | 7 +- .../builtins/symbols/callable_probe.rs | 491 ------------------ .../builtins/symbols/is_callable.rs | 488 ++++++++++++++++- 3 files changed, 488 insertions(+), 498 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index e7b66c3034..ef80b65208 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -10,7 +10,6 @@ use super::super::*; -mod callable_probe; mod class_alias; mod class_attribute_args; mod class_attribute_names; @@ -53,10 +52,12 @@ mod spl_object_id; mod trait_exists; mod unset; -pub(in crate::interpreter) use callable_probe::*; pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; pub(in crate::interpreter) use function_exists::eval_function_probe_exists; pub(in crate::interpreter) use get_class::eval_get_class_result; pub(in crate::interpreter) use get_parent_class::eval_get_parent_class_result; pub(in crate::interpreter) use is_a::dynamic_object_is_a; -use super::*; +pub(in crate::interpreter) use is_callable::{ + eval_builtin_is_callable_call, eval_is_callable_call_with_evaluated_args, + eval_is_callable_value, +}; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs deleted file mode 100644 index f03ba1040c..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/callable_probe.rs +++ /dev/null @@ -1,491 +0,0 @@ -//! Purpose: -//! Callable probes for eval `is_callable()` and PHP callable display names. -//! -//! Called from: -//! - `crate::interpreter::builtins::symbols` re-exports. -//! -//! Key details: -//! - `$callable_name` writeback preserves by-reference targets captured during -//! argument evaluation. -//! - Syntax-only callable checks avoid resolving non-object string targets. - -use super::*; - -/// Evaluates `is_callable()` over full eval call metadata so `$callable_name` stays writable. -pub(in crate::interpreter) fn eval_builtin_is_callable_call( - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_call_arg_values(args, context, scope, values)?; - eval_is_callable_call_with_evaluated_args_from_scope( - &evaluated_args, - Some(scope), - context, - values, - ) -} - -/// Evaluates `is_callable()` from already evaluated arguments that may retain ref targets. -pub(in crate::interpreter) fn eval_is_callable_call_with_evaluated_args( - evaluated_args: &[EvaluatedCallArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_is_callable_call_with_evaluated_args_from_scope(evaluated_args, None, context, values) -} - -/// Evaluates materialized `is_callable()` args with optional special-class callable scope. -fn eval_is_callable_call_with_evaluated_args_from_scope( - evaluated_args: &[EvaluatedCallArg], - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (bound, _) = bind_evaluated_ref_builtin_args( - &["value", "syntax_only", "callable_name"], - evaluated_args, - false, - )?; - let value = required_evaluated_ref_arg(&bound, 0)?; - let syntax_only = optional_evaluated_ref_arg(&bound, 1) - .map(|arg| values.truthy(arg.value)) - .transpose()? - .unwrap_or(false); - let callable_name_target = optional_evaluated_ref_arg(&bound, 2) - .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) - .transpose()?; - eval_is_callable_result( - value.value, - syntax_only, - callable_name_target.as_ref(), - lexical_scope, - context, - values, - ) -} - -/// Evaluates by-value dynamic `is_callable()` arguments without `$callable_name` writeback. -pub(in crate::interpreter) fn eval_is_callable_with_values( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match evaluated_args { - [value] => eval_is_callable_result(*value, false, None, None, context, values), - [value, syntax_only] => { - let syntax_only = values.truthy(*syntax_only)?; - eval_is_callable_result(*value, syntax_only, None, None, context, values) - } - [value, syntax_only, _callable_name] => { - let syntax_only = values.truthy(*syntax_only)?; - eval_is_callable_result(*value, syntax_only, None, None, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Evaluates positional `is_callable()` arguments inside an eval fragment. -pub(in crate::interpreter) fn eval_builtin_is_callable( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_is_callable_result(value, false, None, Some(scope), context, values) - } - [value, syntax_only] => { - let value = eval_expr(value, context, scope, values)?; - let syntax_only = eval_expr(syntax_only, context, scope, values)?; - let syntax_only = values.truthy(syntax_only)?; - eval_is_callable_result(value, syntax_only, None, Some(scope), context, values) - } - [value, syntax_only, callable_name] => { - let value = eval_expr(value, context, scope, values)?; - let syntax_only = eval_expr(syntax_only, context, scope, values)?; - let syntax_only = values.truthy(syntax_only)?; - let (_, callable_name_target) = - eval_call_arg_value(callable_name, context, scope, values)?; - let callable_name_target = callable_name_target.ok_or(EvalStatus::RuntimeFatal)?; - eval_is_callable_result( - value, - syntax_only, - Some(&callable_name_target), - Some(scope), - context, - values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns whether one runtime value is callable from the current eval scope. -pub(in crate::interpreter) fn eval_is_callable_value( - value: RuntimeCellHandle, - lexical_scope: Option<&ElephcEvalScope>, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callback = match lexical_scope { - Some(scope) => eval_callable_from_scope(value, context, scope, values), - None => eval_callable(value, context, values), - }; - let Ok(callback) = callback else { - return Ok(false); - }; - eval_callable_probe_exists(&callback, context, values) -} - -/// Evaluates `is_callable()` and writes PHP's display callable name when requested. -fn eval_is_callable_result( - value: RuntimeCellHandle, - syntax_only: bool, - callable_name_target: Option<&EvalReferenceTarget>, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callable_name = callable_name_target - .map(|_| eval_callable_display_name(value, context, values)) - .transpose()?; - let callable = if syntax_only { - eval_is_callable_syntax_only(value, context, values)? - } else { - eval_is_callable_value(value, lexical_scope, context, values)? - }; - if let Some((target, name)) = callable_name_target.zip(callable_name.as_deref()) { - let name = values.string(name)?; - eval_write_direct_ref_target( - target, - name, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - values.bool_value(callable) -} - -/// Returns PHP's syntax-only callable result without requiring the target to exist. -fn eval_is_callable_syntax_only( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? == EVAL_TAG_STRING { - return Ok(true); - } - if values.type_tag(value)? == EVAL_TAG_OBJECT { - return eval_is_callable_value(value, None, context, values); - } - if values.is_array_like(value)? { - return eval_callable_array_display_name(value, context, values).map(|name| name.is_some()); - } - Ok(false) -} - -/// Builds PHP's `$callable_name` output for one probed callable value. -fn eval_callable_display_name( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? == EVAL_TAG_STRING { - let bytes = values.string_bytes(value)?; - return String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal); - } - if values.type_tag(value)? == EVAL_TAG_OBJECT { - let class_name = eval_callable_object_class_name(value, context, values)?; - return Ok(format!("{class_name}::__invoke")); - } - if values.is_array_like(value)? { - return Ok(eval_callable_array_display_name(value, context, values)? - .unwrap_or_else(|| String::from("Array"))); - } - let string = values.cast_string(value)?; - let bytes = values.string_bytes(string); - values.release(string)?; - String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Builds PHP's `$callable_name` output for a syntactically valid callable array. -fn eval_callable_array_display_name( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.array_len(value)? != 2 { - return Ok(None); - } - let zero = values.int(0)?; - let one = values.int(1)?; - let receiver = values.array_get(value, zero)?; - let method = values.array_get(value, one)?; - if values.type_tag(method)? != EVAL_TAG_STRING { - return Ok(None); - } - let method = - String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; - let receiver_name = match values.type_tag(receiver)? { - EVAL_TAG_OBJECT => eval_callable_object_class_name(receiver, context, values)?, - EVAL_TAG_STRING => String::from_utf8(values.string_bytes(receiver)?) - .map_err(|_| EvalStatus::RuntimeFatal)?, - _ => return Ok(None), - }; - Ok(Some(format!("{receiver_name}::{method}"))) -} - -/// Returns the PHP-visible class name used when formatting callable object probes. -fn eval_callable_object_class_name( - object: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - if context.closure_object_target(identity).is_some() { - return Ok(String::from("Closure")); - } - if let Some(class_name) = context.dynamic_object_class_name(identity) { - return Ok(class_name); - } - runtime_object_class_name(object, values) -} - -/// Returns whether a normalized eval callback has an invokable target. -fn eval_callable_probe_exists( - callback: &EvaluatedCallable, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named { name, .. } => Ok(context.has_closure(name) - || super::function_exists::eval_function_probe_exists(context, name)), - EvaluatedCallable::BoundClosure { name, .. } => Ok(context.has_closure(name) - || super::function_exists::eval_function_probe_exists(context, name)), - EvaluatedCallable::InvokableObject { object } => { - eval_object_method_callable_probe(*object, "__invoke", context, values) - } - EvaluatedCallable::ObjectMethod { - object, - method, - native_class, - .. - } => { - if native_class.is_some() { - Ok(true) - } else { - eval_object_method_callable_probe(*object, method, context, values) - } - } - EvaluatedCallable::StaticMethod { - class_name, - method, - native_class, - .. - } => { - if native_class.is_some() { - Ok(true) - } else { - eval_static_method_callable_probe(class_name, method, context, values) - } - } - } -} - -/// Returns whether one object method can be called from the current eval scope. -fn eval_object_method_callable_probe( - object: RuntimeCellHandle, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Ok(identity) = values.object_identity(object) else { - return Ok(false); - }; - if context.closure_object_target(identity).is_some() - && method_name.eq_ignore_ascii_case("__invoke") - { - return Ok(true); - } - let Some(class) = context.dynamic_object_class(identity) else { - return eval_aot_object_method_callable_probe(object, method_name, context, values); - }; - if eval_enum_static_builtin_applies(class.name(), method_name, context).is_some() { - return Ok(true); - } - let Some((declaring_class, method)) = - eval_dynamic_method_for_call(class.name(), method_name, context) - else { - if eval_dynamic_class_native_method_callable_probe( - class.name(), - method_name, - context, - values, - )? { - return Ok(true); - } - return Ok(eval_instance_magic_method_callable_probe( - class.name(), - context, - )); - }; - if method.is_abstract() { - return Ok(false); - } - if method_name.eq_ignore_ascii_case("__invoke") { - return Ok(true); - } - Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() - || eval_instance_magic_method_callable_probe(class.name(), context)) -} - -/// Returns whether one static method can be called from the current eval scope. -fn eval_static_method_callable_probe( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { - return Ok(true); - } - if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { - if !method.is_static() || method.is_abstract() { - return Ok(false); - } - return Ok(validate_eval_member_access(&declaring_class, method.visibility(), context) - .is_ok() - || eval_static_magic_method_callable_probe(&class_name, context)); - } - if context.has_class(&class_name) - || context.has_interface(&class_name) - || context.has_trait(&class_name) - || context.has_enum(&class_name) - { - if eval_static_magic_method_callable_probe(&class_name, context) { - return Ok(true); - } - if let Some(parent) = context.class_native_parent_name(&class_name) { - return eval_aot_static_method_callable_probe( - &parent, - method_name, - context, - values, - ); - } - return Ok(false); - } - eval_aot_static_method_callable_probe(&class_name, method_name, context, values) -} - -/// Returns whether a generated/AOT object method can be called from the current eval scope. -fn eval_aot_object_method_callable_probe( - object: RuntimeCellHandle, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = runtime_object_class_name(object, values)?; - eval_aot_class_method_callable_probe(&class_name, method_name, context, values) -} - -/// Returns whether an eval class can call a generated/AOT parent instance method. -fn eval_dynamic_class_native_method_callable_probe( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(parent) = context.class_native_parent_name(class_name) else { - return Ok(false); - }; - eval_aot_class_method_callable_probe(&parent, method_name, context, values) -} - -/// Returns whether one generated/AOT class instance method can be called from eval. -fn eval_aot_class_method_callable_probe( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, visibility, _, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? - else { - return eval_aot_instance_magic_method_callable_probe(&class_name, context, values); - }; - if is_abstract { - return Ok(false); - } - Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() - || eval_aot_instance_magic_method_callable_probe(&class_name, context, values)?) -} - -/// Returns whether a generated/AOT static method can be called from the current eval scope. -fn eval_aot_static_method_callable_probe( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? - else { - return eval_aot_static_magic_method_callable_probe(class_name, context, values); - }; - if !is_static || is_abstract { - return Ok(false); - } - Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() - || eval_aot_static_magic_method_callable_probe(class_name, context, values)?) -} - -/// Returns whether a generated/AOT class has a callable instance `__call()` fallback. -fn eval_aot_instance_magic_method_callable_probe( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? - .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) -} - -/// Returns whether a generated/AOT class has a callable static `__callStatic()` fallback. -fn eval_aot_static_magic_method_callable_probe( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok( - eval_aot_method_dispatch_metadata_in_hierarchy( - class_name, - "__callStatic", - context, - values, - )? - .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), - ) -} - -/// Returns whether an eval class has a callable instance `__call()` fallback. -fn eval_instance_magic_method_callable_probe( - class_name: &str, - context: &ElephcEvalContext, -) -> bool { - context - .class_method(class_name, "__call") - .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) -} - -/// Returns whether an eval class has a callable static `__callStatic()` fallback. -fn eval_static_magic_method_callable_probe(class_name: &str, context: &ElephcEvalContext) -> bool { - context - .class_method(class_name, "__callStatic") - .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs index ccbf81e96b..96d212fa66 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `is_callable`. +//! Eval registry entry and implementation for `is_callable`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Direct and dynamic-ref paths preserve `$callable_name` writeback elsewhere. +//! - Direct and dynamic-ref paths preserve `$callable_name` writeback. +//! - Syntax-only callable checks avoid resolving non-object string targets. use super::super::spec::EvalBuiltinDefaultValue; @@ -31,7 +32,7 @@ pub(in crate::interpreter) fn eval_is_callable_declared_call( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::callable_probe::eval_builtin_is_callable(args, context, scope, values) + eval_builtin_is_callable(args, context, scope, values) } /// Evaluates materialized `is_callable(...)` arguments. @@ -40,5 +41,484 @@ pub(in crate::interpreter) fn eval_is_callable_declared_values_result( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::callable_probe::eval_is_callable_with_values(evaluated_args, context, values) + eval_is_callable_with_values(evaluated_args, context, values) +} + +/// Evaluates `is_callable()` over full eval call metadata so `$callable_name` stays writable. +pub(in crate::interpreter) fn eval_builtin_is_callable_call( + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_call_arg_values(args, context, scope, values)?; + eval_is_callable_call_with_evaluated_args_from_scope( + &evaluated_args, + Some(scope), + context, + values, + ) +} + +/// Evaluates `is_callable()` from already evaluated arguments that may retain ref targets. +pub(in crate::interpreter) fn eval_is_callable_call_with_evaluated_args( + evaluated_args: &[EvaluatedCallArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_is_callable_call_with_evaluated_args_from_scope(evaluated_args, None, context, values) +} + +/// Evaluates materialized `is_callable()` args with optional special-class callable scope. +fn eval_is_callable_call_with_evaluated_args_from_scope( + evaluated_args: &[EvaluatedCallArg], + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (bound, _) = bind_evaluated_ref_builtin_args( + &["value", "syntax_only", "callable_name"], + evaluated_args, + false, + )?; + let value = required_evaluated_ref_arg(&bound, 0)?; + let syntax_only = optional_evaluated_ref_arg(&bound, 1) + .map(|arg| values.truthy(arg.value)) + .transpose()? + .unwrap_or(false); + let callable_name_target = optional_evaluated_ref_arg(&bound, 2) + .map(|arg| arg.ref_target.clone().ok_or(EvalStatus::RuntimeFatal)) + .transpose()?; + eval_is_callable_result( + value.value, + syntax_only, + callable_name_target.as_ref(), + lexical_scope, + context, + values, + ) +} + +/// Evaluates by-value dynamic `is_callable()` arguments without `$callable_name` writeback. +pub(in crate::interpreter) fn eval_is_callable_with_values( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_is_callable_result(*value, false, None, None, context, values), + [value, syntax_only] => { + let syntax_only = values.truthy(*syntax_only)?; + eval_is_callable_result(*value, syntax_only, None, None, context, values) + } + [value, syntax_only, _callable_name] => { + let syntax_only = values.truthy(*syntax_only)?; + eval_is_callable_result(*value, syntax_only, None, None, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Evaluates positional `is_callable()` arguments inside an eval fragment. +pub(in crate::interpreter) fn eval_builtin_is_callable( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_is_callable_result(value, false, None, Some(scope), context, values) + } + [value, syntax_only] => { + let value = eval_expr(value, context, scope, values)?; + let syntax_only = eval_expr(syntax_only, context, scope, values)?; + let syntax_only = values.truthy(syntax_only)?; + eval_is_callable_result(value, syntax_only, None, Some(scope), context, values) + } + [value, syntax_only, callable_name] => { + let value = eval_expr(value, context, scope, values)?; + let syntax_only = eval_expr(syntax_only, context, scope, values)?; + let syntax_only = values.truthy(syntax_only)?; + let (_, callable_name_target) = + eval_call_arg_value(callable_name, context, scope, values)?; + let callable_name_target = callable_name_target.ok_or(EvalStatus::RuntimeFatal)?; + eval_is_callable_result( + value, + syntax_only, + Some(&callable_name_target), + Some(scope), + context, + values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether one runtime value is callable from the current eval scope. +pub(in crate::interpreter) fn eval_is_callable_value( + value: RuntimeCellHandle, + lexical_scope: Option<&ElephcEvalScope>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callback = match lexical_scope { + Some(scope) => eval_callable_from_scope(value, context, scope, values), + None => eval_callable(value, context, values), + }; + let Ok(callback) = callback else { + return Ok(false); + }; + eval_callable_probe_exists(&callback, context, values) +} + +/// Evaluates `is_callable()` and writes PHP's display callable name when requested. +fn eval_is_callable_result( + value: RuntimeCellHandle, + syntax_only: bool, + callable_name_target: Option<&EvalReferenceTarget>, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callable_name = callable_name_target + .map(|_| eval_callable_display_name(value, context, values)) + .transpose()?; + let callable = if syntax_only { + eval_is_callable_syntax_only(value, context, values)? + } else { + eval_is_callable_value(value, lexical_scope, context, values)? + }; + if let Some((target, name)) = callable_name_target.zip(callable_name.as_deref()) { + let name = values.string(name)?; + eval_write_direct_ref_target( + target, + name, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + values.bool_value(callable) +} + +/// Returns PHP's syntax-only callable result without requiring the target to exist. +fn eval_is_callable_syntax_only( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_STRING { + return Ok(true); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT { + return eval_is_callable_value(value, None, context, values); + } + if values.is_array_like(value)? { + return eval_callable_array_display_name(value, context, values).map(|name| name.is_some()); + } + Ok(false) +} + +/// Builds PHP's `$callable_name` output for one probed callable value. +fn eval_callable_display_name( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_STRING { + let bytes = values.string_bytes(value)?; + return String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT { + let class_name = eval_callable_object_class_name(value, context, values)?; + return Ok(format!("{class_name}::__invoke")); + } + if values.is_array_like(value)? { + return Ok(eval_callable_array_display_name(value, context, values)? + .unwrap_or_else(|| String::from("Array"))); + } + let string = values.cast_string(value)?; + let bytes = values.string_bytes(string); + values.release(string)?; + String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Builds PHP's `$callable_name` output for a syntactically valid callable array. +fn eval_callable_array_display_name( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(value)? != 2 { + return Ok(None); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(value, zero)?; + let method = values.array_get(value, one)?; + if values.type_tag(method)? != EVAL_TAG_STRING { + return Ok(None); + } + let method = + String::from_utf8(values.string_bytes(method)?).map_err(|_| EvalStatus::RuntimeFatal)?; + let receiver_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_callable_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => String::from_utf8(values.string_bytes(receiver)?) + .map_err(|_| EvalStatus::RuntimeFatal)?, + _ => return Ok(None), + }; + Ok(Some(format!("{receiver_name}::{method}"))) +} + +/// Returns the PHP-visible class name used when formatting callable object probes. +fn eval_callable_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if context.closure_object_target(identity).is_some() { + return Ok(String::from("Closure")); + } + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + runtime_object_class_name(object, values) +} + +/// Returns whether a normalized eval callback has an invokable target. +fn eval_callable_probe_exists( + callback: &EvaluatedCallable, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => Ok(context.has_closure(name) + || super::function_exists::eval_function_probe_exists(context, name)), + EvaluatedCallable::BoundClosure { name, .. } => Ok(context.has_closure(name) + || super::function_exists::eval_function_probe_exists(context, name)), + EvaluatedCallable::InvokableObject { object } => { + eval_object_method_callable_probe(*object, "__invoke", context, values) + } + EvaluatedCallable::ObjectMethod { + object, + method, + native_class, + .. + } => { + if native_class.is_some() { + Ok(true) + } else { + eval_object_method_callable_probe(*object, method, context, values) + } + } + EvaluatedCallable::StaticMethod { + class_name, + method, + native_class, + .. + } => { + if native_class.is_some() { + Ok(true) + } else { + eval_static_method_callable_probe(class_name, method, context, values) + } + } + } +} + +/// Returns whether one object method can be called from the current eval scope. +fn eval_object_method_callable_probe( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return Ok(false); + }; + if context.closure_object_target(identity).is_some() + && method_name.eq_ignore_ascii_case("__invoke") + { + return Ok(true); + } + let Some(class) = context.dynamic_object_class(identity) else { + return eval_aot_object_method_callable_probe(object, method_name, context, values); + }; + if eval_enum_static_builtin_applies(class.name(), method_name, context).is_some() { + return Ok(true); + } + let Some((declaring_class, method)) = + eval_dynamic_method_for_call(class.name(), method_name, context) + else { + if eval_dynamic_class_native_method_callable_probe( + class.name(), + method_name, + context, + values, + )? { + return Ok(true); + } + return Ok(eval_instance_magic_method_callable_probe( + class.name(), + context, + )); + }; + if method.is_abstract() { + return Ok(false); + } + if method_name.eq_ignore_ascii_case("__invoke") { + return Ok(true); + } + Ok(validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() + || eval_instance_magic_method_callable_probe(class.name(), context)) +} + +/// Returns whether one static method can be called from the current eval scope. +fn eval_static_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { + return Ok(true); + } + if let Some((declaring_class, method)) = context.class_method(&class_name, method_name) { + if !method.is_static() || method.is_abstract() { + return Ok(false); + } + return Ok(validate_eval_member_access(&declaring_class, method.visibility(), context) + .is_ok() + || eval_static_magic_method_callable_probe(&class_name, context)); + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { + if eval_static_magic_method_callable_probe(&class_name, context) { + return Ok(true); + } + if let Some(parent) = context.class_native_parent_name(&class_name) { + return eval_aot_static_method_callable_probe( + &parent, + method_name, + context, + values, + ); + } + return Ok(false); + } + eval_aot_static_method_callable_probe(&class_name, method_name, context, values) +} + +/// Returns whether a generated/AOT object method can be called from the current eval scope. +fn eval_aot_object_method_callable_probe( + object: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = runtime_object_class_name(object, values)?; + eval_aot_class_method_callable_probe(&class_name, method_name, context, values) +} + +/// Returns whether an eval class can call a generated/AOT parent instance method. +fn eval_dynamic_class_native_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_aot_class_method_callable_probe(&parent, method_name, context, values) +} + +/// Returns whether one generated/AOT class instance method can be called from eval. +fn eval_aot_class_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&class_name, method_name, context, values)? + else { + return eval_aot_instance_magic_method_callable_probe(&class_name, context, values); + }; + if is_abstract { + return Ok(false); + } + Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_aot_instance_magic_method_callable_probe(&class_name, context, values)?) +} + +/// Returns whether a generated/AOT static method can be called from the current eval scope. +fn eval_aot_static_method_callable_probe( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + return eval_aot_static_magic_method_callable_probe(class_name, context, values); + }; + if !is_static || is_abstract { + return Ok(false); + } + Ok(validate_eval_member_access(&declaring_class, visibility, context).is_ok() + || eval_aot_static_magic_method_callable_probe(class_name, context, values)?) +} + +/// Returns whether a generated/AOT class has a callable instance `__call()` fallback. +fn eval_aot_instance_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether a generated/AOT class has a callable static `__callStatic()` fallback. +fn eval_aot_static_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Returns whether an eval class has a callable instance `__call()` fallback. +fn eval_instance_magic_method_callable_probe( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context + .class_method(class_name, "__call") + .is_some_and(|(_, method)| !method.is_static() && !method.is_abstract()) +} + +/// Returns whether an eval class has a callable static `__callStatic()` fallback. +fn eval_static_magic_method_callable_probe(class_name: &str, context: &ElephcEvalContext) -> bool { + context + .class_method(class_name, "__callStatic") + .is_some_and(|(_, method)| method.is_static() && !method.is_abstract()) } From 470334291f9bfa6b968f41592b58c02a4352c7c2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:32:45 +0200 Subject: [PATCH 1135/1208] refactor(magician): move class relation metadata builtins home --- .../interpreter/builtins/class_metadata.rs | 249 +---------------- .../src/interpreter/builtins/symbols.rs | 1 + .../builtins/symbols/class_implements.rs | 257 +++++++++++++++++- .../builtins/symbols/class_parents.rs | 10 +- .../builtins/symbols/class_uses.rs | 10 +- 5 files changed, 264 insertions(+), 263 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index 3289ff3b3f..c42ba4c0ce 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -12,226 +12,11 @@ //! backend's unknown-target fallback. use super::super::*; -use super::*; mod oop_introspection; pub(in crate::interpreter) use oop_introspection::*; -/// Evaluates `class_implements()`, `class_parents()`, or `class_uses()`. -pub(in crate::interpreter) fn eval_builtin_class_relation( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if !(1..=2).contains(&args.len()) { - return Err(EvalStatus::RuntimeFatal); - } - let target = eval_expr(&args[0], context, scope, values)?; - if let Some(autoload) = args.get(1) { - let _ = eval_expr(autoload, context, scope, values)?; - } - eval_class_relation_target_result(name, target, context, values) -} - -/// Evaluates materialized class-relation builtin arguments. -pub(in crate::interpreter) fn eval_class_relation_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let target = match evaluated_args { - [target] => *target, - [target, _autoload] => *target, - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_class_relation_target_result(name, target, context, values) -} - -/// Resolves one class-relation target and returns an empty relation set or false. -pub(in crate::interpreter) fn eval_class_relation_target_result( - name: &str, - target: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !matches!(name, "class_implements" | "class_parents" | "class_uses") { - return Err(EvalStatus::RuntimeFatal); - } - let Some(target) = eval_class_relation_target_name(target, context, values)? else { - return values.bool_value(false); - }; - if context.class(&target).is_some() { - return match name { - "class_implements" => { - let names = - eval_class_relation_eval_class_interface_names(&target, context, values)?; - eval_class_relation_names_result(names, values) - } - "class_parents" => { - eval_class_relation_names_result(context.class_parent_names(&target), values) - } - "class_uses" => { - eval_class_relation_names_result(context.class_trait_names(&target), values) - } - _ => Err(EvalStatus::RuntimeFatal), - }; - } - if context.interface(&target).is_some() { - return match name { - "class_implements" => { - let names = - eval_class_relation_eval_interface_parent_names(&target, context, values)?; - eval_class_relation_names_result(names, values) - } - "class_parents" | "class_uses" => values.assoc_new(0), - _ => Err(EvalStatus::RuntimeFatal), - }; - } - if context.trait_decl(&target).is_some() { - return match name { - "class_uses" => { - eval_class_relation_names_result(context.trait_trait_names(&target), values) - } - "class_implements" | "class_parents" => values.assoc_new(0), - _ => Err(EvalStatus::RuntimeFatal), - }; - } - match name { - "class_implements" => eval_runtime_class_interface_names_result(&target, values), - "class_parents" => { - eval_class_relation_names_result( - eval_runtime_class_parent_names(&target, context), - values, - ) - } - "class_uses" => eval_runtime_class_trait_names_result(&target, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds `class_implements()` data for generated/AOT class metadata. -fn eval_runtime_class_interface_names_result( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let names = eval_runtime_class_interface_names(class_name, values)?; - eval_class_relation_names_result(names, values) -} - -/// Returns generated/AOT interface names visible for one class-like symbol. -fn eval_runtime_class_interface_names( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let names_array = values.reflection_class_interface_names(class_name)?; - let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; - values.release(names_array)?; - Ok(names) -} - -/// Builds `class_uses()` data for generated/AOT direct trait-use metadata. -fn eval_runtime_class_trait_names_result( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let names_array = values.reflection_class_trait_names(class_name)?; - let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; - values.release(names_array)?; - eval_class_relation_names_result(names, values) -} - -/// Returns generated/AOT parent names in PHP's nearest-parent-first order. -fn eval_runtime_class_parent_names( - class_name: &str, - context: &ElephcEvalContext, -) -> Vec { - let mut names = Vec::new(); - let mut current = context.native_class_parent(class_name).map(str::to_string); - let mut seen = std::collections::HashSet::new(); - while let Some(parent) = current { - let parent = parent.trim_start_matches('\\').to_string(); - if !seen.insert(parent.to_ascii_lowercase()) { - break; - } - current = context.native_class_parent(&parent).map(str::to_string); - names.push(parent); - } - names -} - -/// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. -fn eval_class_relation_eval_class_interface_names( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - if let Some(parent) = context.class_native_parent_name(class_name) { - for name in eval_runtime_class_interface_names(&parent, values)? { - eval_class_relation_push_unique_name(name, &mut names, &mut seen); - } - } - for name in context.class_interface_names(class_name) { - eval_class_relation_push_unique_name(name.clone(), &mut names, &mut seen); - if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { - for parent in eval_runtime_class_interface_names(&name, values)? { - eval_class_relation_push_unique_name(parent, &mut names, &mut seen); - } - } - } - Ok(names) -} - -/// Returns eval interface parents plus inherited generated/AOT interface parents. -fn eval_class_relation_eval_interface_parent_names( - interface_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - for name in context.interface_parent_names(interface_name) { - eval_class_relation_push_unique_name(name.clone(), &mut names, &mut seen); - if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { - for parent in eval_runtime_class_interface_names(&name, values)? { - eval_class_relation_push_unique_name(parent, &mut names, &mut seen); - } - } - } - Ok(names) -} - -/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. -fn eval_class_relation_push_unique_name( - name: String, - names: &mut Vec, - seen: &mut std::collections::HashSet, -) { - if seen.insert(name.to_ascii_lowercase()) { - names.push(name); - } -} - -/// Copies a runtime string array into Rust-owned class/interface names. -fn eval_class_relation_runtime_string_array_to_vec( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut result = Vec::with_capacity(len); - for position in 0..len { - let key = values.int(position as i64)?; - let value = values.array_get(array, key)?; - result.push(eval_class_metadata_name(value, values)?); - } - Ok(result) -} - /// Evaluates class attribute metadata helpers. pub(in crate::interpreter) fn eval_builtin_class_attribute_metadata( name: &str, @@ -437,24 +222,8 @@ fn eval_attribute_name_matches(attribute_name: &str, query: &str) -> bool { .eq_ignore_ascii_case(query.trim_start_matches('\\')) } -/// Returns whether a class-relation target refers to a known class-like symbol. -fn eval_class_relation_target_name( - target: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.type_tag(target)? == EVAL_TAG_OBJECT { - let name = eval_get_class_result(target, context, values)?; - let name = eval_class_metadata_name(name, values)?; - return Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)); - } - let name = eval_class_metadata_name(target, values)?; - let name = context.resolve_class_like_name(&name).unwrap_or(name); - Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)) -} - /// Returns whether one normalized class-like name exists in eval or runtime metadata. -fn eval_class_relation_name_exists( +pub(in crate::interpreter) fn eval_class_relation_name_exists( name: &str, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -472,22 +241,8 @@ fn eval_class_relation_name_exists( values.enum_exists(name) } -/// Builds a PHP associative class-name array keyed by class-name strings. -fn eval_class_relation_names_result( - names: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(names.len())?; - for name in names { - let key = values.string(&name)?; - let value = values.string(&name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - /// Reads and normalizes one class metadata string argument. -fn eval_class_metadata_name( +pub(in crate::interpreter) fn eval_class_metadata_name( name: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result { diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index ef80b65208..7e72ca0e46 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -53,6 +53,7 @@ mod trait_exists; mod unset; pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; +pub(in crate::interpreter) use class_implements::eval_class_relation_result; pub(in crate::interpreter) use function_exists::eval_function_probe_exists; pub(in crate::interpreter) use get_class::eval_get_class_result; pub(in crate::interpreter) use get_parent_class::eval_get_parent_class_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs index 1775ab30d8..da6e5bbd29 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `class_implements`. +//! Eval registry entry and implementation for `class_implements`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-relation metadata helper. +//! - Shared class-relation logic for `class_parents()` and `class_uses()` lives here. use super::super::spec::EvalBuiltinDefaultValue; @@ -18,22 +18,267 @@ eval_builtin! { } use super::super::super::*; +use super::super::{eval_class_metadata_name, eval_class_relation_name_exists}; -/// Dispatches direct eval calls for the `class_implements` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `class_implements` symbol builtin. pub(in crate::interpreter) fn eval_class_implements_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_class_relation("class_implements", args, context, scope, values) + eval_builtin_class_relation("class_implements", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `class_implements` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `class_implements` symbol builtin. pub(in crate::interpreter) fn eval_class_implements_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_class_relation_result("class_implements", evaluated_args, context, values) + eval_class_relation_result("class_implements", evaluated_args, context, values) +} + +/// Evaluates `class_implements()`, `class_parents()`, or `class_uses()`. +pub(in crate::interpreter) fn eval_builtin_class_relation( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if !(1..=2).contains(&args.len()) { + return Err(EvalStatus::RuntimeFatal); + } + let target = eval_expr(&args[0], context, scope, values)?; + if let Some(autoload) = args.get(1) { + let _ = eval_expr(autoload, context, scope, values)?; + } + eval_class_relation_target_result(name, target, context, values) +} + +/// Evaluates materialized class-relation builtin arguments. +pub(in crate::interpreter) fn eval_class_relation_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let target = match evaluated_args { + [target] => *target, + [target, _autoload] => *target, + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_relation_target_result(name, target, context, values) +} + +/// Resolves one class-relation target and returns an empty relation set or false. +pub(in crate::interpreter) fn eval_class_relation_target_result( + name: &str, + target: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !matches!(name, "class_implements" | "class_parents" | "class_uses") { + return Err(EvalStatus::RuntimeFatal); + } + let Some(target) = eval_class_relation_target_name(target, context, values)? else { + return values.bool_value(false); + }; + if context.class(&target).is_some() { + return match name { + "class_implements" => { + let names = + eval_class_relation_eval_class_interface_names(&target, context, values)?; + eval_class_relation_names_result(names, values) + } + "class_parents" => { + eval_class_relation_names_result(context.class_parent_names(&target), values) + } + "class_uses" => { + eval_class_relation_names_result(context.class_trait_names(&target), values) + } + _ => Err(EvalStatus::RuntimeFatal), + }; + } + if context.interface(&target).is_some() { + return match name { + "class_implements" => { + let names = + eval_class_relation_eval_interface_parent_names(&target, context, values)?; + eval_class_relation_names_result(names, values) + } + "class_parents" | "class_uses" => values.assoc_new(0), + _ => Err(EvalStatus::RuntimeFatal), + }; + } + if context.trait_decl(&target).is_some() { + return match name { + "class_uses" => { + eval_class_relation_names_result(context.trait_trait_names(&target), values) + } + "class_implements" | "class_parents" => values.assoc_new(0), + _ => Err(EvalStatus::RuntimeFatal), + }; + } + match name { + "class_implements" => eval_runtime_class_interface_names_result(&target, values), + "class_parents" => { + eval_class_relation_names_result( + eval_runtime_class_parent_names(&target, context), + values, + ) + } + "class_uses" => eval_runtime_class_trait_names_result(&target, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds `class_implements()` data for generated/AOT class metadata. +fn eval_runtime_class_interface_names_result( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let names = eval_runtime_class_interface_names(class_name, values)?; + eval_class_relation_names_result(names, values) +} + +/// Returns generated/AOT interface names visible for one class-like symbol. +fn eval_runtime_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let names_array = values.reflection_class_interface_names(class_name)?; + let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Builds `class_uses()` data for generated/AOT direct trait-use metadata. +fn eval_runtime_class_trait_names_result( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let names_array = values.reflection_class_trait_names(class_name)?; + let names = eval_class_relation_runtime_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + eval_class_relation_names_result(names, values) +} + +/// Returns generated/AOT parent names in PHP's nearest-parent-first order. +fn eval_runtime_class_parent_names( + class_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let mut names = Vec::new(); + let mut current = context.native_class_parent(class_name).map(str::to_string); + let mut seen = std::collections::HashSet::new(); + while let Some(parent) = current { + let parent = parent.trim_start_matches('\\').to_string(); + if !seen.insert(parent.to_ascii_lowercase()) { + break; + } + current = context.native_class_parent(&parent).map(str::to_string); + names.push(parent); + } + names +} + +/// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. +fn eval_class_relation_eval_class_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_runtime_class_interface_names(&parent, values)? { + eval_class_relation_push_unique_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_class_relation_push_unique_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_class_relation_push_unique_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus inherited generated/AOT interface parents. +fn eval_class_relation_eval_interface_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_class_relation_push_unique_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_runtime_class_interface_names(&name, values)? { + eval_class_relation_push_unique_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +fn eval_class_relation_push_unique_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + +/// Copies a runtime string array into Rust-owned class/interface names. +fn eval_class_relation_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_class_metadata_name(value, values)?); + } + Ok(result) +} + +/// Returns whether a class-relation target refers to a known class-like symbol. +fn eval_class_relation_target_name( + target: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(target)? == EVAL_TAG_OBJECT { + let name = super::get_class::eval_get_class_result(target, context, values)?; + let name = eval_class_metadata_name(name, values)?; + return Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)); + } + let name = eval_class_metadata_name(target, values)?; + let name = context.resolve_class_like_name(&name).unwrap_or(name); + Ok(eval_class_relation_name_exists(&name, context, values)?.then_some(name)) +} + +/// Builds a PHP associative class-name array keyed by class-name strings. +fn eval_class_relation_names_result( + names: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(names.len())?; + for name in names { + let key = values.string(&name)?; + let value = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs index 9c3a0833a6..cc6b8059b4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-relation metadata helper. +//! - Shared class-relation logic lives in `class_implements`. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +19,21 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `class_parents` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `class_parents` symbol builtin. pub(in crate::interpreter) fn eval_class_parents_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_class_relation("class_parents", args, context, scope, values) + super::class_implements::eval_builtin_class_relation("class_parents", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `class_parents` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `class_parents` symbol builtin. pub(in crate::interpreter) fn eval_class_parents_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_class_relation_result("class_parents", evaluated_args, context, values) + super::class_implements::eval_class_relation_result("class_parents", evaluated_args, context, values) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs index fad90ac013..81280baa72 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-relation metadata helper. +//! - Shared class-relation logic lives in `class_implements`. use super::super::spec::EvalBuiltinDefaultValue; @@ -19,21 +19,21 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `class_uses` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `class_uses` symbol builtin. pub(in crate::interpreter) fn eval_class_uses_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_class_relation("class_uses", args, context, scope, values) + super::class_implements::eval_builtin_class_relation("class_uses", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `class_uses` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `class_uses` symbol builtin. pub(in crate::interpreter) fn eval_class_uses_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_class_relation_result("class_uses", evaluated_args, context, values) + super::class_implements::eval_class_relation_result("class_uses", evaluated_args, context, values) } From 95987dc113acce6e71e3dc7e873ab5c5999e4268 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:35:51 +0200 Subject: [PATCH 1136/1208] refactor(magician): move class attribute builtins home --- .../interpreter/builtins/class_metadata.rs | 214 +---------------- .../src/interpreter/builtins/symbols.rs | 1 + .../builtins/symbols/class_attribute_args.rs | 21 +- .../builtins/symbols/class_attribute_names.rs | 219 +++++++++++++++++- .../builtins/symbols/class_get_attributes.rs | 21 +- 5 files changed, 250 insertions(+), 226 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index c42ba4c0ce..3a9a4c89ca 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -1,15 +1,14 @@ //! Purpose: -//! Implements eval class metadata and class-relation introspection builtins. +//! Hosts shared class metadata helpers and OOP introspection support. //! //! Called from: //! - `crate::interpreter::expressions::eval_positional_expr_call()`. //! - Dynamic callable dispatch under `builtins::registry::dispatch`. //! //! Key details: -//! - Eval-declared class-like symbols carry parent, interface, direct trait-use, -//! and class-level attribute metadata for literal positional args. -//! - Missing class-like relation targets return `false`, matching the main -//! backend's unknown-target fallback. +//! - Focused symbol builtin files own PHP-visible declarations and eval behavior. +//! - OOP introspection modules use these helpers for class-name normalization +//! and eval/runtime class-like existence checks. use super::super::*; @@ -17,211 +16,6 @@ mod oop_introspection; pub(in crate::interpreter) use oop_introspection::*; -/// Evaluates class attribute metadata helpers. -pub(in crate::interpreter) fn eval_builtin_class_attribute_metadata( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = match (name, args) { - ("class_attribute_names" | "class_get_attributes", [class_name]) => { - vec![eval_expr(class_name, context, scope, values)?] - } - ("class_attribute_args", [class_name, attribute_name]) => vec![ - eval_expr(class_name, context, scope, values)?, - eval_expr(attribute_name, context, scope, values)?, - ], - _ => return Err(EvalStatus::RuntimeFatal), - }; - eval_class_attribute_metadata_result(name, &evaluated_args, context, values) -} - -/// Evaluates materialized class attribute metadata arguments. -pub(in crate::interpreter) fn eval_class_attribute_metadata_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match (name, evaluated_args) { - ("class_attribute_names", [class_name]) => { - let class_name = eval_class_metadata_name(*class_name, values)?; - let attributes = eval_class_like_attribute_metadata(context, &class_name); - eval_class_attribute_names_result(&attributes, values) - } - ("class_get_attributes", [class_name]) => { - let class_name = eval_class_metadata_name(*class_name, values)?; - let attributes = eval_class_like_attribute_metadata(context, &class_name); - eval_reflection_attribute_array_result( - &attributes, - EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS, - context, - values, - ) - } - ("class_attribute_args", [class_name, attribute_name]) => { - let class_name = eval_class_metadata_name(*class_name, values)?; - let attribute_name = eval_class_metadata_name(*attribute_name, values)?; - let attributes = eval_class_like_attribute_metadata(context, &class_name); - let Some(attribute) = attributes - .iter() - .find(|attribute| eval_attribute_name_matches(attribute.name(), &attribute_name)) - else { - return values.array_new(0); - }; - let Some(args) = attribute.args() else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_class_attribute_args_result(args, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns class-like attributes for a dynamic eval class, interface, trait, or enum. -fn eval_class_like_attributes<'a>( - context: &'a ElephcEvalContext, - name: &str, -) -> Option<&'a [EvalAttribute]> { - if let Some(class) = context.class(name) { - return Some(class.attributes()); - } - if let Some(interface) = context.interface(name) { - return Some(interface.attributes()); - } - if let Some(trait_decl) = context.trait_decl(name) { - return Some(trait_decl.attributes()); - } - context.enum_decl(name).map(EvalEnum::attributes) -} - -/// Returns class-like attributes for eval declarations or generated AOT metadata. -fn eval_class_like_attribute_metadata( - context: &ElephcEvalContext, - name: &str, -) -> Vec { - if let Some(attributes) = eval_class_like_attributes(context, name) { - return attributes.to_vec(); - } - context.native_class_attributes(name) -} - -/// Builds the indexed string array returned by `class_attribute_names()`. -fn eval_class_attribute_names_result( - attributes: &[EvalAttribute], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(attributes.len())?; - for (index, attribute) in attributes.iter().enumerate() { - let key = values.int(index as i64)?; - let value = values.string(attribute.name())?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Builds an indexed `ReflectionAttribute` array from eval-retained attribute metadata. -pub(in crate::interpreter) fn eval_reflection_attribute_array_result( - attributes: &[EvalAttribute], - target: u64, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(attributes.len())?; - for (index, attribute) in attributes.iter().enumerate() { - let Some(args) = attribute.args() else { - return Err(EvalStatus::RuntimeFatal); - }; - let key = values.int(index as i64)?; - let args = eval_class_attribute_args_result(args, values)?; - let repeated = eval_attribute_is_repeated(attributes, attribute.name()); - let reflection_attribute = - values.reflection_attribute_new(attribute.name(), args, target, repeated)?; - let identity = values.object_identity(reflection_attribute)?; - context.register_eval_reflection_attribute(identity, attribute.clone(), target, repeated); - values.release(args)?; - result = values.array_set(result, key, reflection_attribute)?; - } - Ok(result) -} - -/// Returns true when an attribute name appears more than once on the same owner. -fn eval_attribute_is_repeated(attributes: &[EvalAttribute], name: &str) -> bool { - attributes - .iter() - .filter(|attribute| eval_attribute_name_matches(attribute.name(), name)) - .nth(1) - .is_some() -} - -/// Builds the mixed PHP array returned by `class_attribute_args()`. -fn eval_class_attribute_args_result( - args: &[EvalAttributeArg], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(args.len())?; - for (index, arg) in args.iter().enumerate() { - let key = match arg.name() { - Some(name) => values.string(name)?, - None => values.int(index as i64)?, - }; - let value = eval_class_attribute_arg_value(arg.value(), values)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Materializes one retained eval attribute argument as a runtime cell. -fn eval_class_attribute_arg_value( - arg: &EvalAttributeArg, - values: &mut impl RuntimeValueOps, -) -> Result { - match arg { - EvalAttributeArg::String(value) => values.string(value), - EvalAttributeArg::Int(value) => values.int(*value), - EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), - EvalAttributeArg::Bool(value) => values.bool_value(*value), - EvalAttributeArg::Null => values.null(), - EvalAttributeArg::Array(elements) => eval_class_attribute_array_arg_value(elements, values), - EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { - eval_class_attribute_arg_value(value, values) - } - } -} - -/// Materializes one retained attribute array literal as a runtime array cell. -fn eval_class_attribute_array_arg_value( - elements: &[EvalAttributeArg], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = if elements - .iter() - .any(|element| element.name().is_some() || element.int_key().is_some()) - { - values.assoc_new(elements.len())? - } else { - values.array_new(elements.len())? - }; - for (index, element) in elements.iter().enumerate() { - let key = match element.name() { - Some(name) => values.string(name)?, - None => values.int(element.int_key().unwrap_or(index as i64))?, - }; - let value = eval_class_attribute_arg_value(element.value(), values)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Returns whether a query names the same PHP attribute class case-insensitively. -fn eval_attribute_name_matches(attribute_name: &str, query: &str) -> bool { - attribute_name - .trim_start_matches('\\') - .eq_ignore_ascii_case(query.trim_start_matches('\\')) -} - /// Returns whether one normalized class-like name exists in eval or runtime metadata. pub(in crate::interpreter) fn eval_class_relation_name_exists( name: &str, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 7e72ca0e46..cbfc501fe7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -53,6 +53,7 @@ mod trait_exists; mod unset; pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; +pub(in crate::interpreter) use class_attribute_names::eval_reflection_attribute_array_result; pub(in crate::interpreter) use class_implements::eval_class_relation_result; pub(in crate::interpreter) use function_exists::eval_function_probe_exists; pub(in crate::interpreter) use get_class::eval_get_class_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs index e154e47eab..ebd2efc1cd 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-attribute metadata helper. +//! - Shared class-attribute metadata logic lives in `class_attribute_names`. eval_builtin! { name: "class_attribute_args", @@ -17,21 +17,32 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `class_attribute_args` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `class_attribute_args` symbol builtin. pub(in crate::interpreter) fn eval_class_attribute_args_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_class_attribute_metadata("class_attribute_args", args, context, scope, values) + super::class_attribute_names::eval_builtin_class_attribute_metadata( + "class_attribute_args", + args, + context, + scope, + values, + ) } -/// Dispatches evaluated-argument calls for the `class_attribute_args` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `class_attribute_args` symbol builtin. pub(in crate::interpreter) fn eval_class_attribute_args_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_class_attribute_metadata_result("class_attribute_args", evaluated_args, context, values) + super::class_attribute_names::eval_class_attribute_metadata_result( + "class_attribute_args", + evaluated_args, + context, + values, + ) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs index 1d2a795364..e06cda892b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `class_attribute_names`. +//! Eval registry entry and implementation for `class_attribute_names`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-attribute metadata helper. +//! - Shared class-attribute metadata logic for `class_attribute_args()` and +//! `class_get_attributes()` lives here. eval_builtin! { name: "class_attribute_names", @@ -16,22 +17,228 @@ eval_builtin! { } use super::super::super::*; +use super::super::eval_class_metadata_name; -/// Dispatches direct eval calls for the `class_attribute_names` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `class_attribute_names` symbol builtin. pub(in crate::interpreter) fn eval_class_attribute_names_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_class_attribute_metadata("class_attribute_names", args, context, scope, values) + eval_builtin_class_attribute_metadata("class_attribute_names", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `class_attribute_names` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `class_attribute_names` symbol builtin. pub(in crate::interpreter) fn eval_class_attribute_names_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_class_attribute_metadata_result("class_attribute_names", evaluated_args, context, values) + eval_class_attribute_metadata_result("class_attribute_names", evaluated_args, context, values) +} + +/// Evaluates class attribute metadata helpers. +pub(in crate::interpreter) fn eval_builtin_class_attribute_metadata( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = match (name, args) { + ("class_attribute_names" | "class_get_attributes", [class_name]) => { + vec![eval_expr(class_name, context, scope, values)?] + } + ("class_attribute_args", [class_name, attribute_name]) => vec![ + eval_expr(class_name, context, scope, values)?, + eval_expr(attribute_name, context, scope, values)?, + ], + _ => return Err(EvalStatus::RuntimeFatal), + }; + eval_class_attribute_metadata_result(name, &evaluated_args, context, values) +} + +/// Evaluates materialized class attribute metadata arguments. +pub(in crate::interpreter) fn eval_class_attribute_metadata_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match (name, evaluated_args) { + ("class_attribute_names", [class_name]) => { + let class_name = eval_class_metadata_name(*class_name, values)?; + let attributes = eval_class_like_attribute_metadata(context, &class_name); + eval_class_attribute_names_result(&attributes, values) + } + ("class_get_attributes", [class_name]) => { + let class_name = eval_class_metadata_name(*class_name, values)?; + let attributes = eval_class_like_attribute_metadata(context, &class_name); + eval_reflection_attribute_array_result( + &attributes, + EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS, + context, + values, + ) + } + ("class_attribute_args", [class_name, attribute_name]) => { + let class_name = eval_class_metadata_name(*class_name, values)?; + let attribute_name = eval_class_metadata_name(*attribute_name, values)?; + let attributes = eval_class_like_attribute_metadata(context, &class_name); + let Some(attribute) = attributes + .iter() + .find(|attribute| eval_attribute_name_matches(attribute.name(), &attribute_name)) + else { + return values.array_new(0); + }; + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_class_attribute_args_result(args, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns class-like attributes for a dynamic eval class, interface, trait, or enum. +fn eval_class_like_attributes<'a>( + context: &'a ElephcEvalContext, + name: &str, +) -> Option<&'a [EvalAttribute]> { + if let Some(class) = context.class(name) { + return Some(class.attributes()); + } + if let Some(interface) = context.interface(name) { + return Some(interface.attributes()); + } + if let Some(trait_decl) = context.trait_decl(name) { + return Some(trait_decl.attributes()); + } + context.enum_decl(name).map(EvalEnum::attributes) +} + +/// Returns class-like attributes for eval declarations or generated AOT metadata. +fn eval_class_like_attribute_metadata( + context: &ElephcEvalContext, + name: &str, +) -> Vec { + if let Some(attributes) = eval_class_like_attributes(context, name) { + return attributes.to_vec(); + } + context.native_class_attributes(name) +} + +/// Builds the indexed string array returned by `class_attribute_names()`. +fn eval_class_attribute_names_result( + attributes: &[EvalAttribute], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(attributes.len())?; + for (index, attribute) in attributes.iter().enumerate() { + let key = values.int(index as i64)?; + let value = values.string(attribute.name())?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds an indexed `ReflectionAttribute` array from eval-retained attribute metadata. +pub(in crate::interpreter) fn eval_reflection_attribute_array_result( + attributes: &[EvalAttribute], + target: u64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(attributes.len())?; + for (index, attribute) in attributes.iter().enumerate() { + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + let key = values.int(index as i64)?; + let args = eval_class_attribute_args_result(args, values)?; + let repeated = eval_attribute_is_repeated(attributes, attribute.name()); + let reflection_attribute = + values.reflection_attribute_new(attribute.name(), args, target, repeated)?; + let identity = values.object_identity(reflection_attribute)?; + context.register_eval_reflection_attribute(identity, attribute.clone(), target, repeated); + values.release(args)?; + result = values.array_set(result, key, reflection_attribute)?; + } + Ok(result) +} + +/// Returns true when an attribute name appears more than once on the same owner. +fn eval_attribute_is_repeated(attributes: &[EvalAttribute], name: &str) -> bool { + attributes + .iter() + .filter(|attribute| eval_attribute_name_matches(attribute.name(), name)) + .nth(1) + .is_some() +} + +/// Builds the mixed PHP array returned by `class_attribute_args()`. +fn eval_class_attribute_args_result( + args: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(args.len())?; + for (index, arg) in args.iter().enumerate() { + let key = match arg.name() { + Some(name) => values.string(name)?, + None => values.int(index as i64)?, + }; + let value = eval_class_attribute_arg_value(arg.value(), values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Materializes one retained eval attribute argument as a runtime cell. +fn eval_class_attribute_arg_value( + arg: &EvalAttributeArg, + values: &mut impl RuntimeValueOps, +) -> Result { + match arg { + EvalAttributeArg::String(value) => values.string(value), + EvalAttributeArg::Int(value) => values.int(*value), + EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), + EvalAttributeArg::Bool(value) => values.bool_value(*value), + EvalAttributeArg::Null => values.null(), + EvalAttributeArg::Array(elements) => eval_class_attribute_array_arg_value(elements, values), + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + eval_class_attribute_arg_value(value, values) + } + } +} + +/// Materializes one retained attribute array literal as a runtime array cell. +fn eval_class_attribute_array_arg_value( + elements: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = if elements + .iter() + .any(|element| element.name().is_some() || element.int_key().is_some()) + { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; + for (index, element) in elements.iter().enumerate() { + let key = match element.name() { + Some(name) => values.string(name)?, + None => values.int(element.int_key().unwrap_or(index as i64))?, + }; + let value = eval_class_attribute_arg_value(element.value(), values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns whether a query names the same PHP attribute class case-insensitively. +fn eval_attribute_name_matches(attribute_name: &str, query: &str) -> bool { + attribute_name + .trim_start_matches('\\') + .eq_ignore_ascii_case(query.trim_start_matches('\\')) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs index dc88036a52..e9f084e690 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the class-attribute metadata helper. +//! - Shared class-attribute metadata logic lives in `class_attribute_names`. eval_builtin! { name: "class_get_attributes", @@ -17,21 +17,32 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `class_get_attributes` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `class_get_attributes` symbol builtin. pub(in crate::interpreter) fn eval_class_get_attributes_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_class_attribute_metadata("class_get_attributes", args, context, scope, values) + super::class_attribute_names::eval_builtin_class_attribute_metadata( + "class_get_attributes", + args, + context, + scope, + values, + ) } -/// Dispatches evaluated-argument calls for the `class_get_attributes` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `class_get_attributes` symbol builtin. pub(in crate::interpreter) fn eval_class_get_attributes_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_class_attribute_metadata_result("class_get_attributes", evaluated_args, context, values) + super::class_attribute_names::eval_class_attribute_metadata_result( + "class_get_attributes", + evaluated_args, + context, + values, + ) } From dd3764dfd9a5f50a32001d11149ec43f5966b7a1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:38:31 +0200 Subject: [PATCH 1137/1208] refactor(magician): move member existence builtins home --- .../class_metadata/oop_introspection.rs | 326 +---------------- .../oop_introspection/common.rs | 18 +- .../oop_introspection/object_vars.rs | 2 +- .../src/interpreter/builtins/symbols.rs | 1 + .../builtins/symbols/method_exists.rs | 337 +++++++++++++++++- .../builtins/symbols/property_exists.rs | 10 +- 6 files changed, 349 insertions(+), 345 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 40dcce861f..10bc058822 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -1,13 +1,10 @@ //! Purpose: -//! Implements eval OOP introspection builtins for class/object members and -//! visible object variables. +//! Re-exports eval OOP introspection helpers for class/object variable builtins. //! //! Called from: //! - `crate::interpreter::builtins::class_metadata` re-exports. //! //! Key details: -//! - `method_exists()` distinguishes object targets from class-string targets -//! because PHP exposes inherited private methods only on object targets. //! - `get_class_vars()` materializes declarative defaults, not current runtime //! static property state. //! - `get_object_vars()` filters declared storage slots so inaccessible @@ -22,325 +19,6 @@ mod methods; mod object_vars; pub(in crate::interpreter) use class_vars::*; +pub(in crate::interpreter) use common::*; pub(in crate::interpreter) use methods::*; pub(in crate::interpreter) use object_vars::*; -use common::*; - -/// Evaluates `method_exists()` or `property_exists()` from eval expressions. -pub(in crate::interpreter) fn eval_builtin_member_exists( - name: &str, - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target, member] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let target = eval_expr(target, context, scope, values)?; - let member = eval_expr(member, context, scope, values)?; - eval_member_exists_result(name, &[target, member], context, values) -} - -/// Evaluates materialized `method_exists()` or `property_exists()` arguments. -pub(in crate::interpreter) fn eval_member_exists_result( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target, member] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let member = eval_class_metadata_name(*member, values)?; - let exists = match name { - "method_exists" => eval_method_exists_target(*target, &member, context, values)?, - "property_exists" => eval_property_exists_target(*target, &member, context, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - values.bool_value(exists) -} - -/// Resolves a `method_exists()` target and applies PHP object-vs-string lookup rules. -fn eval_method_exists_target( - target: RuntimeCellHandle, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(target)? { - EVAL_TAG_OBJECT => { - let class_name = eval_object_class_metadata_name(target, context, values)?; - eval_method_exists_on_class(&class_name, method_name, true, context, values) - } - EVAL_TAG_STRING => { - let class_name = eval_resolved_class_metadata_name(target, context, values)?; - eval_method_exists_on_class(&class_name, method_name, false, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Resolves a `property_exists()` target and applies declared and dynamic-property lookup rules. -fn eval_property_exists_target( - target: RuntimeCellHandle, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(target)? { - EVAL_TAG_OBJECT => { - let class_name = eval_object_class_metadata_name(target, context, values)?; - if eval_property_exists_on_class(&class_name, property_name, context, values)? { - return Ok(true); - } - if eval_current_scope_private_property_exists_on_object( - &class_name, - property_name, - context, - values, - )? { - return Ok(true); - } - eval_object_public_property_exists(target, property_name, values) - } - EVAL_TAG_STRING => { - let class_name = eval_resolved_class_metadata_name(target, context, values)?; - eval_property_exists_on_class(&class_name, property_name, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Checks method metadata for one resolved class-like name. -fn eval_method_exists_on_class( - class_name: &str, - method_name: &str, - target_is_object: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if context.has_class(class_name) || context.has_enum(class_name) { - if target_is_object { - if context - .class_method_names(class_name) - .iter() - .any(|name| name.eq_ignore_ascii_case(method_name)) - { - return Ok(true); - } - return eval_native_parent_method_exists_on_class( - class_name, - method_name, - target_is_object, - context, - values, - ); - } - if context - .class_method_names(class_name) - .iter() - .any(|name| name.eq_ignore_ascii_case(method_name)) - { - let Some((declaring_class, method)) = context.class_method(class_name, method_name) - else { - return Ok(true); - }; - return Ok(method.visibility() != EvalVisibility::Private - || declaring_class - .trim_start_matches('\\') - .eq_ignore_ascii_case(class_name.trim_start_matches('\\'))); - } - return eval_native_parent_method_exists_on_class( - class_name, - method_name, - target_is_object, - context, - values, - ); - } - if context.has_interface(class_name) { - return Ok(context - .interface_method_names(class_name) - .iter() - .any(|name| name.eq_ignore_ascii_case(method_name))); - } - if context.has_trait(class_name) { - return Ok(context - .trait_method_names(class_name) - .iter() - .any(|name| name.eq_ignore_ascii_case(method_name))); - } - if target_is_object { - return eval_native_method_exists_on_class( - class_name, - class_name, - method_name, - target_is_object, - context, - values, - ); - } - eval_native_method_exists_on_class( - class_name, - class_name, - method_name, - target_is_object, - context, - values, - ) -} - -/// Checks generated/AOT parent method metadata inherited by one eval class. -fn eval_native_parent_method_exists_on_class( - class_name: &str, - method_name: &str, - target_is_object: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(parent) = context.class_native_parent_name(class_name) else { - return Ok(false); - }; - eval_native_method_exists_on_class( - class_name, - &parent, - method_name, - target_is_object, - context, - values, - ) -} - -/// Checks generated/AOT method metadata for method_exists() semantics. -fn eval_native_method_exists_on_class( - reflected_class_name: &str, - lookup_class_name: &str, - method_name: &str, - target_is_object: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, visibility, _, _)) = - eval_aot_method_dispatch_metadata_in_hierarchy( - lookup_class_name, - method_name, - context, - values, - )? - else { - return Ok(false); - }; - if target_is_object || visibility != EvalVisibility::Private { - return Ok(true); - } - Ok(declaring_class - .trim_start_matches('\\') - .eq_ignore_ascii_case(reflected_class_name.trim_start_matches('\\'))) -} - -/// Checks property metadata for one resolved class-like name. -fn eval_property_exists_on_class( - class_name: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if context.has_class(class_name) || context.has_enum(class_name) { - if context - .class_property_names(class_name) - .iter() - .any(|name| name == property_name) - { - return Ok(true); - } - return eval_native_parent_property_exists_on_class( - class_name, - property_name, - context, - values, - ); - } - if context.has_interface(class_name) { - return Ok(context - .interface_property_names(class_name) - .iter() - .any(|name| name == property_name)); - } - if context.has_trait(class_name) { - return Ok(context - .trait_property_names(class_name) - .iter() - .any(|name| name == property_name)); - } - eval_native_property_exists_on_class(class_name, class_name, property_name, values) -} - -/// Checks generated/AOT parent property metadata inherited by one eval class. -fn eval_native_parent_property_exists_on_class( - class_name: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(parent) = context.class_native_parent_name(class_name) else { - return Ok(false); - }; - eval_native_property_exists_on_class(class_name, &parent, property_name, values) -} - -/// Checks generated/AOT property metadata for property_exists() semantics. -fn eval_native_property_exists_on_class( - reflected_class_name: &str, - lookup_class_name: &str, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, visibility, _)) = - eval_runtime_property_access_metadata(lookup_class_name, property_name, values)? - else { - return Ok(false); - }; - if visibility != EvalVisibility::Private { - return Ok(true); - } - Ok(declaring_class - .trim_start_matches('\\') - .eq_ignore_ascii_case(reflected_class_name.trim_start_matches('\\'))) -} - -/// Checks private instance properties declared by the current scope for object targets. -fn eval_current_scope_private_property_exists_on_object( - class_name: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(current_class) = context.current_class_scope() else { - return Ok(false); - }; - if !eval_class_metadata_is_a(class_name, current_class, context) { - return Ok(false); - } - if let Some(class) = context.class(current_class) { - return Ok(class.properties().iter().any(|property| { - property.name() == property_name - && property.visibility() == EvalVisibility::Private - && !property.is_static() - })); - } - if context.has_interface(current_class) || context.has_trait(current_class) { - return Ok(false); - } - if !eval_class_relation_name_exists(current_class, context, values)? { - return Ok(false); - } - let Some((declaring_class, visibility, is_static)) = - eval_runtime_property_access_metadata(current_class, property_name, values)? - else { - return Ok(false); - }; - Ok(visibility == EvalVisibility::Private - && !is_static - && eval_same_class_metadata_name(&declaring_class, current_class)) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs index 2d8c771364..30045f7c23 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs @@ -17,7 +17,7 @@ const EVAL_CLASS_METADATA_FLAG_PROTECTED: u64 = 4; const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; /// Resolves an object-or-class argument to a PHP class name and records whether it was an object. -pub(super) fn eval_class_metadata_target_name( +pub(in crate::interpreter) fn eval_class_metadata_target_name( target: RuntimeCellHandle, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -36,7 +36,7 @@ pub(super) fn eval_class_metadata_target_name( } /// Resolves an object cell to its eval or runtime class name. -pub(super) fn eval_object_class_metadata_name( +pub(in crate::interpreter) fn eval_object_class_metadata_name( object: RuntimeCellHandle, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -53,7 +53,7 @@ pub(super) fn eval_object_class_metadata_name( } /// Reads a class-name cell and applies eval alias resolution. -pub(super) fn eval_resolved_class_metadata_name( +pub(in crate::interpreter) fn eval_resolved_class_metadata_name( name: RuntimeCellHandle, context: &ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -63,7 +63,7 @@ pub(super) fn eval_resolved_class_metadata_name( } /// Returns whether one eval or generated/AOT class name is the same as or extends another. -pub(super) fn eval_class_metadata_is_a( +pub(in crate::interpreter) fn eval_class_metadata_is_a( class_name: &str, target: &str, context: &ElephcEvalContext, @@ -97,13 +97,13 @@ fn eval_native_class_metadata_is_a( } /// Returns whether two PHP class names refer to the same normalized metadata name. -pub(super) fn eval_same_class_metadata_name(left: &str, right: &str) -> bool { +pub(in crate::interpreter) fn eval_same_class_metadata_name(left: &str, right: &str) -> bool { left.trim_start_matches('\\') .eq_ignore_ascii_case(right.trim_start_matches('\\')) } /// Returns access metadata for one generated/AOT method name, if reflection exposes it. -pub(super) fn eval_runtime_method_access_metadata( +pub(in crate::interpreter) fn eval_runtime_method_access_metadata( class_name: &str, method_name: &str, values: &mut impl RuntimeValueOps, @@ -121,7 +121,7 @@ pub(super) fn eval_runtime_method_access_metadata( } /// Returns access metadata for one generated/AOT property name, if reflection exposes it. -pub(super) fn eval_runtime_property_access_metadata( +pub(in crate::interpreter) fn eval_runtime_property_access_metadata( class_name: &str, property_name: &str, values: &mut impl RuntimeValueOps, @@ -151,7 +151,7 @@ fn eval_runtime_member_visibility(flags: u64) -> EvalVisibility { } /// Builds an indexed PHP array from owned Rust strings. -pub(super) fn eval_indexed_string_array_result( +pub(in crate::interpreter) fn eval_indexed_string_array_result( names: &[String], values: &mut impl RuntimeValueOps, ) -> Result { @@ -165,7 +165,7 @@ pub(super) fn eval_indexed_string_array_result( } /// Copies a runtime string array into Rust-owned strings for class metadata helpers. -pub(super) fn eval_runtime_string_array_to_vec( +pub(in crate::interpreter) fn eval_runtime_string_array_to_vec( array: RuntimeCellHandle, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs index d6f1863d81..aeccb626b1 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs @@ -307,7 +307,7 @@ fn eval_public_object_vars_result( } /// Returns whether an object has a public bridge-visible property by exact name. -pub(super) fn eval_object_public_property_exists( +pub(in crate::interpreter) fn eval_object_public_property_exists( object: RuntimeCellHandle, property_name: &str, values: &mut impl RuntimeValueOps, diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index cbfc501fe7..5d3d33994c 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -63,3 +63,4 @@ pub(in crate::interpreter) use is_callable::{ eval_builtin_is_callable_call, eval_is_callable_call_with_evaluated_args, eval_is_callable_value, }; +pub(in crate::interpreter) use method_exists::eval_member_exists_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs index 6dccab96a9..ebbcf0cec7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `method_exists`. +//! Eval registry entry and implementation for `method_exists`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the OOP member-existence helper. +//! - Shared member-existence logic for `property_exists()` lives here. eval_builtin! { name: "method_exists", @@ -16,22 +16,347 @@ eval_builtin! { } use super::super::super::*; +use super::super::{ + eval_class_metadata_is_a, eval_class_metadata_name, eval_class_relation_name_exists, + eval_object_class_metadata_name, eval_object_public_property_exists, + eval_resolved_class_metadata_name, eval_runtime_property_access_metadata, + eval_same_class_metadata_name, +}; -/// Dispatches direct eval calls for the `method_exists` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `method_exists` symbol builtin. pub(in crate::interpreter) fn eval_method_exists_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_member_exists("method_exists", args, context, scope, values) + eval_builtin_member_exists("method_exists", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `method_exists` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `method_exists` symbol builtin. pub(in crate::interpreter) fn eval_method_exists_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_member_exists_result("method_exists", evaluated_args, context, values) + eval_member_exists_result("method_exists", evaluated_args, context, values) +} + +/// Evaluates `method_exists()` or `property_exists()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_member_exists( + name: &str, + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target, member] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + let member = eval_expr(member, context, scope, values)?; + eval_member_exists_result(name, &[target, member], context, values) +} + +/// Evaluates materialized `method_exists()` or `property_exists()` arguments. +pub(in crate::interpreter) fn eval_member_exists_result( + name: &str, + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target, member] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let member = eval_class_metadata_name(*member, values)?; + let exists = match name { + "method_exists" => eval_method_exists_target(*target, &member, context, values)?, + "property_exists" => eval_property_exists_target(*target, &member, context, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + values.bool_value(exists) +} + +/// Resolves a `method_exists()` target and applies PHP object-vs-string lookup rules. +fn eval_method_exists_target( + target: RuntimeCellHandle, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => { + let class_name = eval_object_class_metadata_name(target, context, values)?; + eval_method_exists_on_class(&class_name, method_name, true, context, values) + } + EVAL_TAG_STRING => { + let class_name = eval_resolved_class_metadata_name(target, context, values)?; + eval_method_exists_on_class(&class_name, method_name, false, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves a `property_exists()` target and applies declared and dynamic-property lookup rules. +fn eval_property_exists_target( + target: RuntimeCellHandle, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => { + let class_name = eval_object_class_metadata_name(target, context, values)?; + if eval_property_exists_on_class(&class_name, property_name, context, values)? { + return Ok(true); + } + if eval_current_scope_private_property_exists_on_object( + &class_name, + property_name, + context, + values, + )? { + return Ok(true); + } + eval_object_public_property_exists(target, property_name, values) + } + EVAL_TAG_STRING => { + let class_name = eval_resolved_class_metadata_name(target, context, values)?; + eval_property_exists_on_class(&class_name, property_name, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Checks method metadata for one resolved class-like name. +fn eval_method_exists_on_class( + class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(class_name) || context.has_enum(class_name) { + if target_is_object { + if context + .class_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name)) + { + return Ok(true); + } + return eval_native_parent_method_exists_on_class( + class_name, + method_name, + target_is_object, + context, + values, + ); + } + if context + .class_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name)) + { + let Some((declaring_class, method)) = context.class_method(class_name, method_name) + else { + return Ok(true); + }; + return Ok(method.visibility() != EvalVisibility::Private + || declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(class_name.trim_start_matches('\\'))); + } + return eval_native_parent_method_exists_on_class( + class_name, + method_name, + target_is_object, + context, + values, + ); + } + if context.has_interface(class_name) { + return Ok(context + .interface_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name))); + } + if context.has_trait(class_name) { + return Ok(context + .trait_method_names(class_name) + .iter() + .any(|name| name.eq_ignore_ascii_case(method_name))); + } + if target_is_object { + return eval_native_method_exists_on_class( + class_name, + class_name, + method_name, + target_is_object, + context, + values, + ); + } + eval_native_method_exists_on_class( + class_name, + class_name, + method_name, + target_is_object, + context, + values, + ) +} + +/// Checks generated/AOT parent method metadata inherited by one eval class. +fn eval_native_parent_method_exists_on_class( + class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_native_method_exists_on_class( + class_name, + &parent, + method_name, + target_is_object, + context, + values, + ) +} + +/// Checks generated/AOT method metadata for method_exists() semantics. +fn eval_native_method_exists_on_class( + reflected_class_name: &str, + lookup_class_name: &str, + method_name: &str, + target_is_object: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _, _)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + lookup_class_name, + method_name, + context, + values, + )? + else { + return Ok(false); + }; + if target_is_object || visibility != EvalVisibility::Private { + return Ok(true); + } + Ok(declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(reflected_class_name.trim_start_matches('\\'))) +} + +/// Checks property metadata for one resolved class-like name. +fn eval_property_exists_on_class( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(class_name) || context.has_enum(class_name) { + if context + .class_property_names(class_name) + .iter() + .any(|name| name == property_name) + { + return Ok(true); + } + return eval_native_parent_property_exists_on_class( + class_name, + property_name, + context, + values, + ); + } + if context.has_interface(class_name) { + return Ok(context + .interface_property_names(class_name) + .iter() + .any(|name| name == property_name)); + } + if context.has_trait(class_name) { + return Ok(context + .trait_property_names(class_name) + .iter() + .any(|name| name == property_name)); + } + eval_native_property_exists_on_class(class_name, class_name, property_name, values) +} + +/// Checks generated/AOT parent property metadata inherited by one eval class. +fn eval_native_parent_property_exists_on_class( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(false); + }; + eval_native_property_exists_on_class(class_name, &parent, property_name, values) +} + +/// Checks generated/AOT property metadata for property_exists() semantics. +fn eval_native_property_exists_on_class( + reflected_class_name: &str, + lookup_class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, visibility, _)) = + eval_runtime_property_access_metadata(lookup_class_name, property_name, values)? + else { + return Ok(false); + }; + if visibility != EvalVisibility::Private { + return Ok(true); + } + Ok(declaring_class + .trim_start_matches('\\') + .eq_ignore_ascii_case(reflected_class_name.trim_start_matches('\\'))) +} + +/// Checks private instance properties declared by the current scope for object targets. +fn eval_current_scope_private_property_exists_on_object( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(current_class) = context.current_class_scope() else { + return Ok(false); + }; + if !eval_class_metadata_is_a(class_name, current_class, context) { + return Ok(false); + } + if let Some(class) = context.class(current_class) { + return Ok(class.properties().iter().any(|property| { + property.name() == property_name + && property.visibility() == EvalVisibility::Private + && !property.is_static() + })); + } + if context.has_interface(current_class) || context.has_trait(current_class) { + return Ok(false); + } + if !eval_class_relation_name_exists(current_class, context, values)? { + return Ok(false); + } + let Some((declaring_class, visibility, is_static)) = + eval_runtime_property_access_metadata(current_class, property_name, values)? + else { + return Ok(false); + }; + Ok(visibility == EvalVisibility::Private + && !is_static + && eval_same_class_metadata_name(&declaring_class, current_class)) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs index 962af9f9de..9e6a81c38b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/property_exists.rs @@ -5,7 +5,7 @@ //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the OOP member-existence helper. +//! - Shared member-existence logic lives in `method_exists`. eval_builtin! { name: "property_exists", @@ -17,21 +17,21 @@ eval_builtin! { use super::super::super::*; -/// Dispatches direct eval calls for the `property_exists` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `property_exists` symbol builtin. pub(in crate::interpreter) fn eval_property_exists_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_member_exists("property_exists", args, context, scope, values) + super::method_exists::eval_builtin_member_exists("property_exists", args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `property_exists` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `property_exists` symbol builtin. pub(in crate::interpreter) fn eval_property_exists_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_member_exists_result("property_exists", evaluated_args, context, values) + super::method_exists::eval_member_exists_result("property_exists", evaluated_args, context, values) } From 7396664cc2ff86a8b49d1e47854fb6c2e962e2be Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:39:46 +0200 Subject: [PATCH 1138/1208] refactor(magician): move get_class_methods builtin home --- .../class_metadata/oop_introspection.rs | 2 - .../oop_introspection/methods.rs | 191 ----------------- .../builtins/symbols/get_class_methods.rs | 197 +++++++++++++++++- 3 files changed, 191 insertions(+), 199 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/methods.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 10bc058822..8a47fd9def 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -15,10 +15,8 @@ use super::{eval_class_metadata_name, eval_class_relation_name_exists}; mod class_vars; mod common; -mod methods; mod object_vars; pub(in crate::interpreter) use class_vars::*; pub(in crate::interpreter) use common::*; -pub(in crate::interpreter) use methods::*; pub(in crate::interpreter) use object_vars::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/methods.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/methods.rs deleted file mode 100644 index 908c763a6a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/methods.rs +++ /dev/null @@ -1,191 +0,0 @@ -//! Purpose: -//! Implements `get_class_methods()` for eval and generated/AOT class metadata. -//! -//! Called from: -//! - `crate::interpreter::builtins::class_metadata::oop_introspection`. -//! - Declarative class metadata builtin dispatch hooks. -//! -//! Key details: -//! - Method lists are filtered through the current eval scope and PHP visibility -//! rules before materialization. - -use super::*; -use std::collections::HashSet; - -/// Evaluates `get_class_methods()` from eval expressions. -pub(in crate::interpreter) fn eval_builtin_get_class_methods( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let target = eval_expr(target, context, scope, values)?; - eval_get_class_methods_result(&[target], context, values) -} - -/// Evaluates materialized `get_class_methods()` arguments. -pub(in crate::interpreter) fn eval_get_class_methods_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let (class_name, target_is_object) = - eval_class_metadata_target_name(*target, context, values)?; - if !target_is_object && !eval_class_relation_name_exists(&class_name, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let names = eval_class_method_names_for_scope(&class_name, context, values)?; - eval_indexed_string_array_result(&names, values) -} - -/// Collects PHP-visible methods for `get_class_methods()` in the current eval scope. -fn eval_class_method_names_for_scope( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if context.has_class(class_name) || context.has_enum(class_name) { - let mut names = Vec::new(); - let mut seen = HashSet::new(); - for name in context.class_method_names(class_name) { - let Some((declaring_class, method)) = context.class_method(class_name, &name) else { - eval_push_unique_method_name(&mut names, &mut seen, name); - continue; - }; - if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() { - eval_push_unique_method_name(&mut names, &mut seen, name); - } - } - eval_add_current_scope_private_method_names( - &mut names, &mut seen, class_name, context, values, - )?; - eval_add_native_parent_method_names(&mut names, &mut seen, class_name, context, values)?; - return Ok(names); - } - if context.has_interface(class_name) { - return Ok(context.interface_method_names(class_name)); - } - if let Some(trait_decl) = context.trait_decl(class_name) { - return Ok(trait_decl - .methods() - .iter() - .filter(|method| method.visibility() == EvalVisibility::Public) - .map(|method| method.name().to_string()) - .collect()); - } - let method_names = values.reflection_method_names(class_name)?; - let names = eval_runtime_string_array_to_vec(method_names, values)?; - values.release(method_names)?; - let mut names = eval_visible_runtime_method_names(class_name, names, context, values)?; - let mut seen = names - .iter() - .map(|name| name.to_ascii_lowercase()) - .collect::>(); - eval_add_current_scope_private_method_names(&mut names, &mut seen, class_name, context, values)?; - Ok(names) -} - -/// Filters generated runtime methods to the surface visible from the current eval scope. -fn eval_visible_runtime_method_names( - lookup_class_name: &str, - names: Vec, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut result = Vec::new(); - for name in names { - let Some((declaring_class, visibility)) = - eval_runtime_method_access_metadata(lookup_class_name, &name, values)? - else { - continue; - }; - if validate_eval_member_access(&declaring_class, visibility, context).is_ok() { - result.push(name); - } - } - Ok(result) -} - -/// Adds generated/AOT parent method names inherited by one eval class. -fn eval_add_native_parent_method_names( - names: &mut Vec, - seen: &mut HashSet, - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(parent) = context.class_native_parent_name(class_name) else { - return Ok(()); - }; - let method_names = values.reflection_method_names(&parent)?; - let parent_names = eval_runtime_string_array_to_vec(method_names, values)?; - values.release(method_names)?; - let parent_names = eval_visible_runtime_method_names(&parent, parent_names, context, values)?; - for name in parent_names { - eval_push_unique_method_name(names, seen, name); - } - Ok(()) -} - -/// Adds private methods declared by the current eval scope when PHP would expose them. -fn eval_add_current_scope_private_method_names( - names: &mut Vec, - seen: &mut HashSet, - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(current_class) = context.current_class_scope() else { - return Ok(()); - }; - if !eval_class_metadata_is_a(class_name, current_class, context) { - return Ok(()); - } - if let Some(class) = context.class(current_class) { - for method in class.methods() { - if method.visibility() == EvalVisibility::Private { - eval_push_unique_method_name(names, seen, method.name().to_string()); - } - } - return Ok(()); - } - if context.has_interface(current_class) || context.has_trait(current_class) { - return Ok(()); - } - if !eval_class_relation_name_exists(current_class, context, values)? { - return Ok(()); - } - let method_names = values.reflection_method_names(current_class)?; - let current_names = eval_runtime_string_array_to_vec(method_names, values)?; - values.release(method_names)?; - for name in current_names { - let Some((declaring_class, visibility)) = - eval_runtime_method_access_metadata(current_class, &name, values)? - else { - continue; - }; - if visibility == EvalVisibility::Private - && eval_same_class_metadata_name(&declaring_class, current_class) - { - eval_push_unique_method_name(names, seen, name); - } - } - Ok(()) -} - -/// Appends one method name while preserving PHP's case-insensitive uniqueness rule. -fn eval_push_unique_method_name( - names: &mut Vec, - seen: &mut HashSet, - name: String, -) { - if seen.insert(name.to_ascii_lowercase()) { - names.push(name); - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs index a071a1d7c2..2b4247f623 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `get_class_methods`. +//! Eval registry entry and implementation for `get_class_methods`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the OOP introspection helper. +//! - Method lists are filtered through the current eval scope and PHP visibility +//! rules before materialization. eval_builtin! { name: "get_class_methods", @@ -16,22 +17,206 @@ eval_builtin! { } use super::super::super::*; +use super::super::{ + eval_class_metadata_is_a, eval_class_metadata_target_name, eval_class_relation_name_exists, + eval_indexed_string_array_result, eval_runtime_method_access_metadata, + eval_runtime_string_array_to_vec, eval_same_class_metadata_name, +}; +use std::collections::HashSet; -/// Dispatches direct eval calls for the `get_class_methods` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `get_class_methods` symbol builtin. pub(in crate::interpreter) fn eval_get_class_methods_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_get_class_methods(args, context, scope, values) + eval_builtin_get_class_methods(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `get_class_methods` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `get_class_methods` symbol builtin. pub(in crate::interpreter) fn eval_get_class_methods_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_get_class_methods_result(evaluated_args, context, values) + eval_get_class_methods_result(evaluated_args, context, values) +} + +/// Evaluates `get_class_methods()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_class_methods( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + eval_get_class_methods_result(&[target], context, values) +} + +/// Evaluates materialized `get_class_methods()` arguments. +pub(in crate::interpreter) fn eval_get_class_methods_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let (class_name, target_is_object) = + eval_class_metadata_target_name(*target, context, values)?; + if !target_is_object && !eval_class_relation_name_exists(&class_name, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let names = eval_class_method_names_for_scope(&class_name, context, values)?; + eval_indexed_string_array_result(&names, values) +} + +/// Collects PHP-visible methods for `get_class_methods()` in the current eval scope. +fn eval_class_method_names_for_scope( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(class_name) || context.has_enum(class_name) { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for name in context.class_method_names(class_name) { + let Some((declaring_class, method)) = context.class_method(class_name, &name) else { + eval_push_unique_method_name(&mut names, &mut seen, name); + continue; + }; + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_ok() { + eval_push_unique_method_name(&mut names, &mut seen, name); + } + } + eval_add_current_scope_private_method_names( + &mut names, &mut seen, class_name, context, values, + )?; + eval_add_native_parent_method_names(&mut names, &mut seen, class_name, context, values)?; + return Ok(names); + } + if context.has_interface(class_name) { + return Ok(context.interface_method_names(class_name)); + } + if let Some(trait_decl) = context.trait_decl(class_name) { + return Ok(trait_decl + .methods() + .iter() + .filter(|method| method.visibility() == EvalVisibility::Public) + .map(|method| method.name().to_string()) + .collect()); + } + let method_names = values.reflection_method_names(class_name)?; + let names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + let mut names = eval_visible_runtime_method_names(class_name, names, context, values)?; + let mut seen = names + .iter() + .map(|name| name.to_ascii_lowercase()) + .collect::>(); + eval_add_current_scope_private_method_names(&mut names, &mut seen, class_name, context, values)?; + Ok(names) +} + +/// Filters generated runtime methods to the surface visible from the current eval scope. +fn eval_visible_runtime_method_names( + lookup_class_name: &str, + names: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut result = Vec::new(); + for name in names { + let Some((declaring_class, visibility)) = + eval_runtime_method_access_metadata(lookup_class_name, &name, values)? + else { + continue; + }; + if validate_eval_member_access(&declaring_class, visibility, context).is_ok() { + result.push(name); + } + } + Ok(result) +} + +/// Adds generated/AOT parent method names inherited by one eval class. +fn eval_add_native_parent_method_names( + names: &mut Vec, + seen: &mut HashSet, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = context.class_native_parent_name(class_name) else { + return Ok(()); + }; + let method_names = values.reflection_method_names(&parent)?; + let parent_names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + let parent_names = eval_visible_runtime_method_names(&parent, parent_names, context, values)?; + for name in parent_names { + eval_push_unique_method_name(names, seen, name); + } + Ok(()) +} + +/// Adds private methods declared by the current eval scope when PHP would expose them. +fn eval_add_current_scope_private_method_names( + names: &mut Vec, + seen: &mut HashSet, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(current_class) = context.current_class_scope() else { + return Ok(()); + }; + if !eval_class_metadata_is_a(class_name, current_class, context) { + return Ok(()); + } + if let Some(class) = context.class(current_class) { + for method in class.methods() { + if method.visibility() == EvalVisibility::Private { + eval_push_unique_method_name(names, seen, method.name().to_string()); + } + } + return Ok(()); + } + if context.has_interface(current_class) || context.has_trait(current_class) { + return Ok(()); + } + if !eval_class_relation_name_exists(current_class, context, values)? { + return Ok(()); + } + let method_names = values.reflection_method_names(current_class)?; + let current_names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + for name in current_names { + let Some((declaring_class, visibility)) = + eval_runtime_method_access_metadata(current_class, &name, values)? + else { + continue; + }; + if visibility == EvalVisibility::Private + && eval_same_class_metadata_name(&declaring_class, current_class) + { + eval_push_unique_method_name(names, seen, name); + } + } + Ok(()) +} + +/// Appends one method name while preserving PHP's case-insensitive uniqueness rule. +fn eval_push_unique_method_name( + names: &mut Vec, + seen: &mut HashSet, + name: String, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } } From 4fa28ba0954aa544812278ff0b54849356270f56 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:40:49 +0200 Subject: [PATCH 1139/1208] refactor(magician): move get_class_vars builtin home --- .../class_metadata/oop_introspection.rs | 4 +- .../oop_introspection/class_vars.rs | 197 ----------------- .../builtins/symbols/get_class_vars.rs | 202 +++++++++++++++++- 3 files changed, 197 insertions(+), 206 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/class_vars.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 8a47fd9def..014e371b22 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -11,12 +11,10 @@ //! protected/private eval properties do not leak as dynamic properties. use super::super::super::*; -use super::{eval_class_metadata_name, eval_class_relation_name_exists}; +use super::eval_class_metadata_name; -mod class_vars; mod common; mod object_vars; -pub(in crate::interpreter) use class_vars::*; pub(in crate::interpreter) use common::*; pub(in crate::interpreter) use object_vars::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/class_vars.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/class_vars.rs deleted file mode 100644 index 18bf8ddbdb..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/class_vars.rs +++ /dev/null @@ -1,197 +0,0 @@ -//! Purpose: -//! Implements `get_class_vars()` for eval-declared and generated/AOT metadata. -//! -//! Called from: -//! - `crate::interpreter::builtins::class_metadata::oop_introspection`. -//! - Declarative class metadata builtin dispatch hooks. -//! -//! Key details: -//! - Eval-declared defaults are materialized in the declaring class scope. -//! - Generated/AOT defaults use native callable default metadata when present. - -use super::*; -use std::collections::HashSet; - -/// Evaluates `get_class_vars()` from eval expressions. -pub(in crate::interpreter) fn eval_builtin_get_class_vars( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let target = eval_expr(target, context, scope, values)?; - eval_get_class_vars_result(&[target], context, values) -} - -/// Evaluates materialized `get_class_vars()` arguments. -pub(in crate::interpreter) fn eval_get_class_vars_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [target] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - let class_name = eval_resolved_class_metadata_name(*target, context, values)?; - if context.has_class(&class_name) || context.has_enum(&class_name) { - return eval_dynamic_class_vars_result(&class_name, context, values); - } - if context.has_trait(&class_name) { - return eval_dynamic_trait_vars_result(&class_name, context, values); - } - if context.has_interface(&class_name) { - return values.assoc_new(0); - } - if eval_class_relation_name_exists(&class_name, context, values)? { - return eval_runtime_class_vars_result(&class_name, context, values); - } - Err(EvalStatus::RuntimeFatal) -} - -/// Builds `get_class_vars()` for an eval-declared class or enum. -fn eval_dynamic_class_vars_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(0)?; - let mut emitted_keys = HashSet::new(); - if let Some(enum_decl) = context.enum_decl(class_name) { - let name_value = values.null()?; - result = eval_add_class_var_entry(result, "name", name_value, values)?; - emitted_keys.insert(String::from("name")); - if enum_decl.backing_type().is_some() { - let value_value = values.null()?; - result = eval_add_class_var_entry(result, "value", value_value, values)?; - emitted_keys.insert(String::from("value")); - } - } - for class in context.class_chain(class_name).into_iter().rev() { - for property in class.properties() { - if emitted_keys.contains(property.name()) - || validate_eval_member_access(class.name(), property.visibility(), context) - .is_err() - { - continue; - } - let value = - eval_class_vars_property_default_value(class.name(), property, context, values)?; - result = eval_add_class_var_entry(result, property.name(), value, values)?; - emitted_keys.insert(property.name().to_string()); - } - } - Ok(result) -} - -/// Builds `get_class_vars()` for an eval-declared trait. -fn eval_dynamic_trait_vars_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(trait_decl) = context.trait_decl(class_name) else { - return Err(EvalStatus::RuntimeFatal); - }; - let trait_name = trait_decl.name().to_string(); - let properties = trait_decl.properties().to_vec(); - let mut result = values.assoc_new(properties.len())?; - let mut emitted_keys = HashSet::new(); - for property in properties { - if emitted_keys.contains(property.name()) - || validate_eval_member_access(&trait_name, property.visibility(), context).is_err() - { - continue; - } - let value = eval_class_vars_property_default_value(&trait_name, &property, context, values)?; - result = eval_add_class_var_entry(result, property.name(), value, values)?; - emitted_keys.insert(property.name().to_string()); - } - Ok(result) -} - -/// Builds `get_class_vars()` data for generated/AOT class metadata. -fn eval_runtime_class_vars_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_names = values.reflection_property_names(class_name)?; - let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; - values.release(property_names)?; - let mut result = values.assoc_new(declared_names.len())?; - let mut emitted_keys = HashSet::new(); - for property_name in declared_names { - if emitted_keys.contains(&property_name) { - continue; - } - let Some((declaring_class, visibility, _is_static)) = - eval_runtime_property_access_metadata(class_name, &property_name, values)? - else { - continue; - }; - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - continue; - } - let value = eval_runtime_class_var_default_value( - class_name, - &declaring_class, - &property_name, - context, - values, - )?; - result = eval_add_class_var_entry(result, &property_name, value, values)?; - emitted_keys.insert(property_name); - } - Ok(result) -} - -/// Materializes one eval-declared property default for `get_class_vars()`. -fn eval_class_vars_property_default_value( - declaring_class: &str, - property: &EvalClassProperty, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(default) = property.default() else { - return values.null(); - }; - context.push_class_scope(declaring_class.to_string()); - context.push_called_class_scope(declaring_class.to_string()); - context.push_class_like_member_magic_scope(declaring_class, property.trait_origin()); - let result = eval_method_parameter_default(default, context, values); - context.pop_magic_scope(); - context.pop_called_class_scope(); - context.pop_class_scope(); - result -} - -/// Materializes one generated/AOT property default for `get_class_vars()`. -fn eval_runtime_class_var_default_value( - runtime_class: &str, - declaring_class: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(default) = context - .native_property_default(declaring_class, property_name) - .or_else(|| context.native_property_default(runtime_class, property_name)) - { - return materialize_native_callable_default(&default, context, values); - } - values.null() -} - -/// Adds one string-keyed class variable value to an associative result array. -fn eval_add_class_var_entry( - result: RuntimeCellHandle, - property_name: &str, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let key = values.string(property_name)?; - values.array_set(result, key, value) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs index e2e8e12c0e..ca11620504 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `get_class_vars`. +//! Eval registry entry and implementation for `get_class_vars`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the OOP introspection helper. +//! - Eval-declared defaults are materialized in the declaring class scope. +//! - Generated/AOT defaults use native callable default metadata when present. eval_builtin! { name: "get_class_vars", @@ -16,22 +17,211 @@ eval_builtin! { } use super::super::super::*; +use super::super::{ + eval_class_relation_name_exists, eval_resolved_class_metadata_name, + eval_runtime_property_access_metadata, eval_runtime_string_array_to_vec, +}; +use std::collections::HashSet; -/// Dispatches direct eval calls for the `get_class_vars` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `get_class_vars` symbol builtin. pub(in crate::interpreter) fn eval_get_class_vars_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_get_class_vars(args, context, scope, values) + eval_builtin_get_class_vars(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `get_class_vars` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `get_class_vars` symbol builtin. pub(in crate::interpreter) fn eval_get_class_vars_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_get_class_vars_result(evaluated_args, context, values) + eval_get_class_vars_result(evaluated_args, context, values) +} + +/// Evaluates `get_class_vars()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_class_vars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let target = eval_expr(target, context, scope, values)?; + eval_get_class_vars_result(&[target], context, values) +} + +/// Evaluates materialized `get_class_vars()` arguments. +pub(in crate::interpreter) fn eval_get_class_vars_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [target] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + let class_name = eval_resolved_class_metadata_name(*target, context, values)?; + if context.has_class(&class_name) || context.has_enum(&class_name) { + return eval_dynamic_class_vars_result(&class_name, context, values); + } + if context.has_trait(&class_name) { + return eval_dynamic_trait_vars_result(&class_name, context, values); + } + if context.has_interface(&class_name) { + return values.assoc_new(0); + } + if eval_class_relation_name_exists(&class_name, context, values)? { + return eval_runtime_class_vars_result(&class_name, context, values); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Builds `get_class_vars()` for an eval-declared class or enum. +fn eval_dynamic_class_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(0)?; + let mut emitted_keys = HashSet::new(); + if let Some(enum_decl) = context.enum_decl(class_name) { + let name_value = values.null()?; + result = eval_add_class_var_entry(result, "name", name_value, values)?; + emitted_keys.insert(String::from("name")); + if enum_decl.backing_type().is_some() { + let value_value = values.null()?; + result = eval_add_class_var_entry(result, "value", value_value, values)?; + emitted_keys.insert(String::from("value")); + } + } + for class in context.class_chain(class_name).into_iter().rev() { + for property in class.properties() { + if emitted_keys.contains(property.name()) + || validate_eval_member_access(class.name(), property.visibility(), context) + .is_err() + { + continue; + } + let value = + eval_class_vars_property_default_value(class.name(), property, context, values)?; + result = eval_add_class_var_entry(result, property.name(), value, values)?; + emitted_keys.insert(property.name().to_string()); + } + } + Ok(result) +} + +/// Builds `get_class_vars()` for an eval-declared trait. +fn eval_dynamic_trait_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(trait_decl) = context.trait_decl(class_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + let trait_name = trait_decl.name().to_string(); + let properties = trait_decl.properties().to_vec(); + let mut result = values.assoc_new(properties.len())?; + let mut emitted_keys = HashSet::new(); + for property in properties { + if emitted_keys.contains(property.name()) + || validate_eval_member_access(&trait_name, property.visibility(), context).is_err() + { + continue; + } + let value = eval_class_vars_property_default_value(&trait_name, &property, context, values)?; + result = eval_add_class_var_entry(result, property.name(), value, values)?; + emitted_keys.insert(property.name().to_string()); + } + Ok(result) +} + +/// Builds `get_class_vars()` data for generated/AOT class metadata. +fn eval_runtime_class_vars_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_names = values.reflection_property_names(class_name)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + let mut result = values.assoc_new(declared_names.len())?; + let mut emitted_keys = HashSet::new(); + for property_name in declared_names { + if emitted_keys.contains(&property_name) { + continue; + } + let Some((declaring_class, visibility, _is_static)) = + eval_runtime_property_access_metadata(class_name, &property_name, values)? + else { + continue; + }; + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + continue; + } + let value = eval_runtime_class_var_default_value( + class_name, + &declaring_class, + &property_name, + context, + values, + )?; + result = eval_add_class_var_entry(result, &property_name, value, values)?; + emitted_keys.insert(property_name); + } + Ok(result) +} + +/// Materializes one eval-declared property default for `get_class_vars()`. +fn eval_class_vars_property_default_value( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(default) = property.default() else { + return values.null(); + }; + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + context.push_class_like_member_magic_scope(declaring_class, property.trait_origin()); + let result = eval_method_parameter_default(default, context, values); + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + result +} + +/// Materializes one generated/AOT property default for `get_class_vars()`. +fn eval_runtime_class_var_default_value( + runtime_class: &str, + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(default) = context + .native_property_default(declaring_class, property_name) + .or_else(|| context.native_property_default(runtime_class, property_name)) + { + return materialize_native_callable_default(&default, context, values); + } + values.null() +} + +/// Adds one string-keyed class variable value to an associative result array. +fn eval_add_class_var_entry( + result: RuntimeCellHandle, + property_name: &str, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let key = values.string(property_name)?; + values.array_set(result, key, value) } From d251777fe8352e8ee5ec10a7f39cbb8dfbab8cc7 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:42:00 +0200 Subject: [PATCH 1140/1208] refactor(magician): move get_object_vars builtin home --- .../class_metadata/oop_introspection.rs | 2 - .../oop_introspection/object_vars.rs | 325 ----------------- .../builtins/symbols/get_object_vars.rs | 330 +++++++++++++++++- .../builtins/symbols/method_exists.rs | 6 +- 4 files changed, 327 insertions(+), 336 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs index 014e371b22..0b787f608a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs @@ -14,7 +14,5 @@ use super::super::super::*; use super::eval_class_metadata_name; mod common; -mod object_vars; pub(in crate::interpreter) use common::*; -pub(in crate::interpreter) use object_vars::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs deleted file mode 100644 index aeccb626b1..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/object_vars.rs +++ /dev/null @@ -1,325 +0,0 @@ -//! Purpose: -//! Implements `get_object_vars()` for eval-declared and generated/AOT objects. -//! -//! Called from: -//! - `crate::interpreter::builtins::class_metadata::oop_introspection`. -//! - Declarative class metadata builtin dispatch hooks. -//! -//! Key details: -//! - Declared eval properties use storage-name filtering so inaccessible -//! protected/private slots do not leak as public dynamic properties. - -use super::*; -use std::collections::HashSet; - -/// Evaluates `get_object_vars()` from eval expressions. -pub(in crate::interpreter) fn eval_builtin_get_object_vars( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = args else { - return Err(EvalStatus::RuntimeFatal); - }; - let object = eval_expr(object, context, scope, values)?; - eval_get_object_vars_result(&[object], context, values) -} - -/// Evaluates materialized `get_object_vars()` arguments. -pub(in crate::interpreter) fn eval_get_object_vars_result( - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let [object] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - if values.type_tag(*object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let Ok(identity) = values.object_identity(*object) else { - return eval_public_object_vars_result(*object, values); - }; - let Some(class) = context.dynamic_object_class(identity) else { - let class_name = eval_object_class_metadata_name(*object, context, values)?; - return eval_runtime_object_vars_result(*object, &class_name, context, values); - }; - let class_name = class.name().to_string(); - eval_dynamic_object_vars_result(*object, &class_name, context, values) -} - -/// Builds `get_object_vars()` for an eval-declared object. -fn eval_dynamic_object_vars_result( - object: RuntimeCellHandle, - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - let mut result = values.assoc_new(property_count)?; - let mut emitted_keys = HashSet::new(); - let storage_keys = eval_declared_object_storage_names(class_name, context); - result = eval_add_enum_object_vars(result, object, class_name, &mut emitted_keys, context, values)?; - result = eval_add_declared_object_vars( - result, - object, - class_name, - &mut emitted_keys, - context, - values, - )?; - eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &storage_keys, values) -} - -/// Builds `get_object_vars()` for generated/AOT objects from reflection metadata. -fn eval_runtime_object_vars_result( - object: RuntimeCellHandle, - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_names = values.reflection_property_names(class_name)?; - let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; - values.release(property_names)?; - let property_count = values.object_property_len(object)?; - let mut result = values.assoc_new(declared_names.len() + property_count)?; - let mut emitted_keys = HashSet::new(); - result = eval_add_runtime_scope_private_object_vars( - result, - object, - &mut emitted_keys, - context, - values, - )?; - result = eval_add_runtime_declared_object_vars( - result, - object, - class_name, - &declared_names, - &mut emitted_keys, - context, - values, - )?; - eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &HashSet::new(), values) -} - -/// Adds generated/AOT private properties declared by the current eval class scope. -fn eval_add_runtime_scope_private_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - emitted_keys: &mut HashSet, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(current_class) = context.current_class_scope() else { - return Ok(result); - }; - if !values.object_is_a(object, current_class, false)? { - return Ok(result); - } - let property_names = values.reflection_property_names(current_class)?; - let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; - values.release(property_names)?; - for property_name in declared_names { - let Some((_, visibility, is_static)) = - eval_runtime_property_access_metadata(current_class, &property_name, values)? - else { - continue; - }; - if is_static - || visibility != EvalVisibility::Private - || emitted_keys.contains(&property_name) - || !values.property_is_initialized(object, &property_name)? - { - continue; - } - emitted_keys.insert(property_name.clone()); - let key = values.string(&property_name)?; - let value = values.property_get(object, &property_name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Adds generated/AOT declared instance properties visible from the current eval scope. -fn eval_add_runtime_declared_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - class_name: &str, - property_names: &[String], - emitted_keys: &mut HashSet, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - for property_name in property_names { - let Some((declaring_class, visibility, is_static)) = - eval_runtime_property_access_metadata(class_name, property_name, values)? - else { - continue; - }; - if is_static - || validate_eval_member_access(&declaring_class, visibility, context).is_err() - || emitted_keys.contains(property_name) - || !values.property_is_initialized(object, property_name)? - { - continue; - } - emitted_keys.insert(property_name.clone()); - let key = values.string(property_name)?; - let value = values.property_get(object, property_name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Adds synthetic enum properties exposed by PHP enum case objects. -fn eval_add_enum_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - class_name: &str, - emitted_keys: &mut HashSet, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(enum_decl) = context.enum_decl(class_name) else { - return Ok(result); - }; - let is_backed = enum_decl.backing_type().is_some(); - result = eval_add_object_var(result, object, "name", emitted_keys, context, values)?; - if is_backed { - result = eval_add_object_var(result, object, "value", emitted_keys, context, values)?; - } - Ok(result) -} - -/// Adds declared instance properties visible from the current eval scope. -fn eval_add_declared_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - class_name: &str, - emitted_keys: &mut HashSet, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - for class in context.class_chain(class_name) { - for property in class.properties() { - if property.is_static() - || validate_eval_member_access(class.name(), property.visibility(), context) - .is_err() - || emitted_keys.contains(property.name()) - { - continue; - } - let storage_property_name = eval_instance_property_storage_name(class.name(), property); - if !property.is_virtual() - && !context.dynamic_property_is_initialized(identity, &storage_property_name) - { - continue; - } - result = eval_add_object_var( - result, - object, - property.name(), - emitted_keys, - context, - values, - )?; - } - } - Ok(result) -} - -/// Adds one visible object variable to an associative result array. -fn eval_add_object_var( - result: RuntimeCellHandle, - object: RuntimeCellHandle, - property_name: &str, - emitted_keys: &mut HashSet, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - emitted_keys.insert(property_name.to_string()); - let key = values.string(property_name)?; - let value = eval_property_get_result(object, property_name, context, values)?; - values.array_set(result, key, value) -} - -/// Adds public dynamic properties that are not declared storage slots. -fn eval_add_dynamic_object_vars( - mut result: RuntimeCellHandle, - object: RuntimeCellHandle, - emitted_keys: &mut HashSet, - storage_keys: &HashSet, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - if key_name.contains('\0') - || storage_keys.contains(&key_name) - || !emitted_keys.insert(key_name.clone()) - { - continue; - } - let key = values.string(&key_name)?; - let value = values.property_get(object, &key_name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Returns physical storage names used by declared eval object properties. -fn eval_declared_object_storage_names( - class_name: &str, - context: &ElephcEvalContext, -) -> HashSet { - let mut names = HashSet::new(); - for class in context.class_chain(class_name) { - for property in class.properties() { - names.insert(eval_instance_property_storage_name(class.name(), property)); - } - } - names -} - -/// Builds `get_object_vars()` for runtime objects with public bridge-visible properties. -fn eval_public_object_vars_result( - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - let mut result = values.assoc_new(property_count)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - let key = values.string(&key_name)?; - let value = values.property_get(object, &key_name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Returns whether an object has a public bridge-visible property by exact name. -pub(in crate::interpreter) fn eval_object_public_property_exists( - object: RuntimeCellHandle, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - if key_bytes? == property_name.as_bytes() { - return Ok(true); - } - } - Ok(false) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs index bb8904dd7e..48b6687604 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `get_object_vars`. +//! Eval registry entry and implementation for `get_object_vars`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the OOP introspection helper. +//! - Declared eval properties use storage-name filtering so inaccessible +//! protected/private slots do not leak as public dynamic properties. eval_builtin! { name: "get_object_vars", @@ -16,22 +17,339 @@ eval_builtin! { } use super::super::super::*; +use super::super::{ + eval_object_class_metadata_name, eval_runtime_property_access_metadata, + eval_runtime_string_array_to_vec, +}; +use std::collections::HashSet; -/// Dispatches direct eval calls for the `get_object_vars` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `get_object_vars` symbol builtin. pub(in crate::interpreter) fn eval_get_object_vars_declared_call( args: &[EvalExpr], context: &mut ElephcEvalContext, scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_builtin_get_object_vars(args, context, scope, values) + eval_builtin_get_object_vars(args, context, scope, values) } -/// Dispatches evaluated-argument calls for the `get_object_vars` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `get_object_vars` symbol builtin. pub(in crate::interpreter) fn eval_get_object_vars_declared_values_result( evaluated_args: &[RuntimeCellHandle], context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - super::super::eval_get_object_vars_result(evaluated_args, context, values) + eval_get_object_vars_result(evaluated_args, context, values) +} + +/// Evaluates `get_object_vars()` from eval expressions. +pub(in crate::interpreter) fn eval_builtin_get_object_vars( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = args else { + return Err(EvalStatus::RuntimeFatal); + }; + let object = eval_expr(object, context, scope, values)?; + eval_get_object_vars_result(&[object], context, values) +} + +/// Evaluates materialized `get_object_vars()` arguments. +pub(in crate::interpreter) fn eval_get_object_vars_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [object] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + if values.type_tag(*object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let Ok(identity) = values.object_identity(*object) else { + return eval_public_object_vars_result(*object, values); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_object_class_metadata_name(*object, context, values)?; + return eval_runtime_object_vars_result(*object, &class_name, context, values); + }; + let class_name = class.name().to_string(); + eval_dynamic_object_vars_result(*object, &class_name, context, values) +} + +/// Builds `get_object_vars()` for an eval-declared object. +fn eval_dynamic_object_vars_result( + object: RuntimeCellHandle, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(property_count)?; + let mut emitted_keys = HashSet::new(); + let storage_keys = eval_declared_object_storage_names(class_name, context); + result = eval_add_enum_object_vars(result, object, class_name, &mut emitted_keys, context, values)?; + result = eval_add_declared_object_vars( + result, + object, + class_name, + &mut emitted_keys, + context, + values, + )?; + eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &storage_keys, values) +} + +/// Builds `get_object_vars()` for generated/AOT objects from reflection metadata. +fn eval_runtime_object_vars_result( + object: RuntimeCellHandle, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_names = values.reflection_property_names(class_name)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(declared_names.len() + property_count)?; + let mut emitted_keys = HashSet::new(); + result = eval_add_runtime_scope_private_object_vars( + result, + object, + &mut emitted_keys, + context, + values, + )?; + result = eval_add_runtime_declared_object_vars( + result, + object, + class_name, + &declared_names, + &mut emitted_keys, + context, + values, + )?; + eval_add_dynamic_object_vars(result, object, &mut emitted_keys, &HashSet::new(), values) +} + +/// Adds generated/AOT private properties declared by the current eval class scope. +fn eval_add_runtime_scope_private_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + emitted_keys: &mut HashSet, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(current_class) = context.current_class_scope() else { + return Ok(result); + }; + if !values.object_is_a(object, current_class, false)? { + return Ok(result); + } + let property_names = values.reflection_property_names(current_class)?; + let declared_names = eval_runtime_string_array_to_vec(property_names, values)?; + values.release(property_names)?; + for property_name in declared_names { + let Some((_, visibility, is_static)) = + eval_runtime_property_access_metadata(current_class, &property_name, values)? + else { + continue; + }; + if is_static + || visibility != EvalVisibility::Private + || emitted_keys.contains(&property_name) + || !values.property_is_initialized(object, &property_name)? + { + continue; + } + emitted_keys.insert(property_name.clone()); + let key = values.string(&property_name)?; + let value = values.property_get(object, &property_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Adds generated/AOT declared instance properties visible from the current eval scope. +fn eval_add_runtime_declared_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + property_names: &[String], + emitted_keys: &mut HashSet, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + for property_name in property_names { + let Some((declaring_class, visibility, is_static)) = + eval_runtime_property_access_metadata(class_name, property_name, values)? + else { + continue; + }; + if is_static + || validate_eval_member_access(&declaring_class, visibility, context).is_err() + || emitted_keys.contains(property_name) + || !values.property_is_initialized(object, property_name)? + { + continue; + } + emitted_keys.insert(property_name.clone()); + let key = values.string(property_name)?; + let value = values.property_get(object, property_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Adds synthetic enum properties exposed by PHP enum case objects. +fn eval_add_enum_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(enum_decl) = context.enum_decl(class_name) else { + return Ok(result); + }; + let is_backed = enum_decl.backing_type().is_some(); + result = eval_add_object_var(result, object, "name", emitted_keys, context, values)?; + if is_backed { + result = eval_add_object_var(result, object, "value", emitted_keys, context, values)?; + } + Ok(result) +} + +/// Adds declared instance properties visible from the current eval scope. +fn eval_add_declared_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + class_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + for class in context.class_chain(class_name) { + for property in class.properties() { + if property.is_static() + || validate_eval_member_access(class.name(), property.visibility(), context) + .is_err() + || emitted_keys.contains(property.name()) + { + continue; + } + let storage_property_name = eval_instance_property_storage_name(class.name(), property); + if !property.is_virtual() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + continue; + } + result = eval_add_object_var( + result, + object, + property.name(), + emitted_keys, + context, + values, + )?; + } + } + Ok(result) +} + +/// Adds one visible object variable to an associative result array. +fn eval_add_object_var( + result: RuntimeCellHandle, + object: RuntimeCellHandle, + property_name: &str, + emitted_keys: &mut HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + emitted_keys.insert(property_name.to_string()); + let key = values.string(property_name)?; + let value = eval_property_get_result(object, property_name, context, values)?; + values.array_set(result, key, value) +} + +/// Adds public dynamic properties that are not declared storage slots. +fn eval_add_dynamic_object_vars( + mut result: RuntimeCellHandle, + object: RuntimeCellHandle, + emitted_keys: &mut HashSet, + storage_keys: &HashSet, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + if key_name.contains('\0') + || storage_keys.contains(&key_name) + || !emitted_keys.insert(key_name.clone()) + { + continue; + } + let key = values.string(&key_name)?; + let value = values.property_get(object, &key_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns physical storage names used by declared eval object properties. +fn eval_declared_object_storage_names( + class_name: &str, + context: &ElephcEvalContext, +) -> HashSet { + let mut names = HashSet::new(); + for class in context.class_chain(class_name) { + for property in class.properties() { + names.insert(eval_instance_property_storage_name(class.name(), property)); + } + } + names +} + +/// Builds `get_object_vars()` for runtime objects with public bridge-visible properties. +fn eval_public_object_vars_result( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + let mut result = values.assoc_new(property_count)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let key_name = String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + let key = values.string(&key_name)?; + let value = values.property_get(object, &key_name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns whether an object has a public bridge-visible property by exact name. +pub(in crate::interpreter) fn eval_object_public_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) } diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs index ebbcf0cec7..149e5ab854 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/method_exists.rs @@ -18,10 +18,10 @@ eval_builtin! { use super::super::super::*; use super::super::{ eval_class_metadata_is_a, eval_class_metadata_name, eval_class_relation_name_exists, - eval_object_class_metadata_name, eval_object_public_property_exists, - eval_resolved_class_metadata_name, eval_runtime_property_access_metadata, - eval_same_class_metadata_name, + eval_object_class_metadata_name, eval_resolved_class_metadata_name, + eval_runtime_property_access_metadata, eval_same_class_metadata_name, }; +use super::get_object_vars::eval_object_public_property_exists; /// Dispatches direct eval calls for the `method_exists` symbol builtin. pub(in crate::interpreter) fn eval_method_exists_declared_call( From 229fb6bf39a3dd4fa8ec24a1b0afab76be39a8da Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:43:28 +0200 Subject: [PATCH 1141/1208] refactor(magician): inline class metadata helper module --- .../interpreter/builtins/class_metadata.rs | 172 ++++++++++++++++- .../class_metadata/oop_introspection.rs | 18 -- .../oop_introspection/common.rs | 180 ------------------ 3 files changed, 168 insertions(+), 202 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs index 3a9a4c89ca..9405c84998 100644 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtins/class_metadata.rs @@ -1,5 +1,5 @@ //! Purpose: -//! Hosts shared class metadata helpers and OOP introspection support. +//! Hosts shared class metadata helpers for eval symbol builtins. //! //! Called from: //! - `crate::interpreter::expressions::eval_positional_expr_call()`. @@ -7,14 +7,178 @@ //! //! Key details: //! - Focused symbol builtin files own PHP-visible declarations and eval behavior. -//! - OOP introspection modules use these helpers for class-name normalization +//! - These helpers centralize class-name normalization, visibility metadata, //! and eval/runtime class-like existence checks. use super::super::*; +use std::collections::HashSet; -mod oop_introspection; +const EVAL_CLASS_METADATA_FLAG_STATIC: u64 = 1; +const EVAL_CLASS_METADATA_FLAG_PROTECTED: u64 = 4; +const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; -pub(in crate::interpreter) use oop_introspection::*; +/// Resolves an object-or-class argument to a PHP class name and records whether it was an object. +pub(in crate::interpreter) fn eval_class_metadata_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(String, bool), EvalStatus> { + match values.type_tag(target)? { + EVAL_TAG_OBJECT => Ok(( + eval_object_class_metadata_name(target, context, values)?, + true, + )), + EVAL_TAG_STRING => Ok(( + eval_resolved_class_metadata_name(target, context, values)?, + false, + )), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Resolves an object cell to its eval or runtime class name. +pub(in crate::interpreter) fn eval_object_class_metadata_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + let class_name = values.object_class_name(object)?; + let class_name_bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(class_name_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Reads a class-name cell and applies eval alias resolution. +pub(in crate::interpreter) fn eval_resolved_class_metadata_name( + name: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_class_metadata_name(name, values)?; + Ok(context.resolve_class_name(&name).unwrap_or(name)) +} + +/// Returns whether one eval or generated/AOT class name is the same as or extends another. +pub(in crate::interpreter) fn eval_class_metadata_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + eval_same_class_metadata_name(class_name, target) + || context.class_is_a(class_name, target, false) + || eval_native_class_metadata_is_a(class_name, target, context) +} + +/// Returns whether generated/AOT parent metadata proves one class extends another. +fn eval_native_class_metadata_is_a( + class_name: &str, + target: &str, + context: &ElephcEvalContext, +) -> bool { + let target = target.trim_start_matches('\\'); + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return false; + } + if eval_same_class_metadata_name(¤t, target) { + return true; + } + let Some(parent) = context.native_class_parent(¤t) else { + return false; + }; + current = parent.to_string(); + } +} + +/// Returns whether two PHP class names refer to the same normalized metadata name. +pub(in crate::interpreter) fn eval_same_class_metadata_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns access metadata for one generated/AOT method name, if reflection exposes it. +pub(in crate::interpreter) fn eval_runtime_method_access_metadata( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_method_declaring_class(class_name, method_name)? + .unwrap_or_else(|| class_name.to_string()); + Ok(Some(( + declaring_class, + eval_runtime_member_visibility(flags), + ))) +} + +/// Returns access metadata for one generated/AOT property name, if reflection exposes it. +pub(in crate::interpreter) fn eval_runtime_property_access_metadata( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_property_flags(class_name, property_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_property_declaring_class(class_name, property_name)? + .unwrap_or_else(|| class_name.to_string()); + Ok(Some(( + declaring_class, + eval_runtime_member_visibility(flags), + flags & EVAL_CLASS_METADATA_FLAG_STATIC != 0, + ))) +} + +/// Converts generated/AOT reflection member flags into eval visibility metadata. +fn eval_runtime_member_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_CLASS_METADATA_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + +/// Builds an indexed PHP array from owned Rust strings. +pub(in crate::interpreter) fn eval_indexed_string_array_result( + names: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + for (index, name) in names.iter().enumerate() { + let key = values.int(index as i64)?; + let value = values.string(name)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Copies a runtime string array into Rust-owned strings for class metadata helpers. +pub(in crate::interpreter) fn eval_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_class_metadata_name(value, values)?); + } + Ok(result) +} /// Returns whether one normalized class-like name exists in eval or runtime metadata. pub(in crate::interpreter) fn eval_class_relation_name_exists( diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs deleted file mode 100644 index 0b787f608a..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Purpose: -//! Re-exports eval OOP introspection helpers for class/object variable builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::class_metadata` re-exports. -//! -//! Key details: -//! - `get_class_vars()` materializes declarative defaults, not current runtime -//! static property state. -//! - `get_object_vars()` filters declared storage slots so inaccessible -//! protected/private eval properties do not leak as dynamic properties. - -use super::super::super::*; -use super::eval_class_metadata_name; - -mod common; - -pub(in crate::interpreter) use common::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs b/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs deleted file mode 100644 index 30045f7c23..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/class_metadata/oop_introspection/common.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! Purpose: -//! Shared class-name resolution and reflection metadata helpers for eval OOP -//! introspection builtins. -//! -//! Called from: -//! - `crate::interpreter::builtins::class_metadata::oop_introspection` modules. -//! -//! Key details: -//! - Helpers normalize PHP class names case-insensitively while preserving -//! runtime reflection ownership and visibility metadata. - -use super::*; -use std::collections::HashSet; - -pub(super) const EVAL_CLASS_METADATA_FLAG_STATIC: u64 = 1; -const EVAL_CLASS_METADATA_FLAG_PROTECTED: u64 = 4; -const EVAL_CLASS_METADATA_FLAG_PRIVATE: u64 = 8; - -/// Resolves an object-or-class argument to a PHP class name and records whether it was an object. -pub(in crate::interpreter) fn eval_class_metadata_target_name( - target: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(String, bool), EvalStatus> { - match values.type_tag(target)? { - EVAL_TAG_OBJECT => Ok(( - eval_object_class_metadata_name(target, context, values)?, - true, - )), - EVAL_TAG_STRING => Ok(( - eval_resolved_class_metadata_name(target, context, values)?, - false, - )), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Resolves an object cell to its eval or runtime class name. -pub(in crate::interpreter) fn eval_object_class_metadata_name( - object: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - if let Some(class_name) = context.dynamic_object_class_name(identity) { - return Ok(class_name); - } - let class_name = values.object_class_name(object)?; - let class_name_bytes = values.string_bytes(class_name); - values.release(class_name)?; - let class_name = String::from_utf8(class_name_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(class_name.trim_start_matches('\\').to_string()) -} - -/// Reads a class-name cell and applies eval alias resolution. -pub(in crate::interpreter) fn eval_resolved_class_metadata_name( - name: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_class_metadata_name(name, values)?; - Ok(context.resolve_class_name(&name).unwrap_or(name)) -} - -/// Returns whether one eval or generated/AOT class name is the same as or extends another. -pub(in crate::interpreter) fn eval_class_metadata_is_a( - class_name: &str, - target: &str, - context: &ElephcEvalContext, -) -> bool { - eval_same_class_metadata_name(class_name, target) - || context.class_is_a(class_name, target, false) - || eval_native_class_metadata_is_a(class_name, target, context) -} - -/// Returns whether generated/AOT parent metadata proves one class extends another. -fn eval_native_class_metadata_is_a( - class_name: &str, - target: &str, - context: &ElephcEvalContext, -) -> bool { - let target = target.trim_start_matches('\\'); - let mut current = class_name.trim_start_matches('\\').to_string(); - let mut seen = HashSet::new(); - loop { - if !seen.insert(current.to_ascii_lowercase()) { - return false; - } - if eval_same_class_metadata_name(¤t, target) { - return true; - } - let Some(parent) = context.native_class_parent(¤t) else { - return false; - }; - current = parent.to_string(); - } -} - -/// Returns whether two PHP class names refer to the same normalized metadata name. -pub(in crate::interpreter) fn eval_same_class_metadata_name(left: &str, right: &str) -> bool { - left.trim_start_matches('\\') - .eq_ignore_ascii_case(right.trim_start_matches('\\')) -} - -/// Returns access metadata for one generated/AOT method name, if reflection exposes it. -pub(in crate::interpreter) fn eval_runtime_method_access_metadata( - class_name: &str, - method_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { - return Ok(None); - }; - let declaring_class = values - .reflection_method_declaring_class(class_name, method_name)? - .unwrap_or_else(|| class_name.to_string()); - Ok(Some(( - declaring_class, - eval_runtime_member_visibility(flags), - ))) -} - -/// Returns access metadata for one generated/AOT property name, if reflection exposes it. -pub(in crate::interpreter) fn eval_runtime_property_access_metadata( - class_name: &str, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(flags) = values.reflection_property_flags(class_name, property_name)? else { - return Ok(None); - }; - let declaring_class = values - .reflection_property_declaring_class(class_name, property_name)? - .unwrap_or_else(|| class_name.to_string()); - Ok(Some(( - declaring_class, - eval_runtime_member_visibility(flags), - flags & EVAL_CLASS_METADATA_FLAG_STATIC != 0, - ))) -} - -/// Converts generated/AOT reflection member flags into eval visibility metadata. -fn eval_runtime_member_visibility(flags: u64) -> EvalVisibility { - if flags & EVAL_CLASS_METADATA_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_CLASS_METADATA_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - } -} - -/// Builds an indexed PHP array from owned Rust strings. -pub(in crate::interpreter) fn eval_indexed_string_array_result( - names: &[String], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(names.len())?; - for (index, name) in names.iter().enumerate() { - let key = values.int(index as i64)?; - let value = values.string(name)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Copies a runtime string array into Rust-owned strings for class metadata helpers. -pub(in crate::interpreter) fn eval_runtime_string_array_to_vec( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut result = Vec::with_capacity(len); - for position in 0..len { - let key = values.int(position as i64)?; - let value = values.array_get(array, key)?; - result.push(eval_class_metadata_name(value, values)?); - } - Ok(result) -} From da6a3a9ffba00b500fab12669310b1a8515b10d1 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:44:57 +0200 Subject: [PATCH 1142/1208] refactor(magician): tidy spl_classes builtin leaf --- .../interpreter/builtins/symbols/spl_classes.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs index e9056e6a1a..e3ad7ec773 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs @@ -1,11 +1,11 @@ //! Purpose: -//! Declarative eval registry entry for `spl_classes`. +//! Eval registry entry and implementation for `spl_classes`. //! //! Called from: //! - `crate::interpreter::builtins::symbols`. //! //! Key details: -//! - Runtime behavior stays delegated to the SPL classes helper. +//! - The result is a fixed indexed array of SPL class names. eval_builtin! { name: "spl_classes", @@ -18,23 +18,27 @@ eval_builtin! { use super::super::eval_static_string_array_result; use super::super::super::*; -/// Dispatches direct eval calls for the `spl_classes` symbol builtin through the area dispatcher. +/// Dispatches direct eval calls for the `spl_classes` symbol builtin. pub(in crate::interpreter) fn eval_spl_classes_declared_call( args: &[EvalExpr], _context: &mut ElephcEvalContext, _scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - super::spl_classes::eval_builtin_spl_classes(args, values) + eval_builtin_spl_classes(args, values) } -/// Dispatches evaluated-argument calls for the `spl_classes` symbol builtin through the area dispatcher. +/// Dispatches evaluated-argument calls for the `spl_classes` symbol builtin. pub(in crate::interpreter) fn eval_spl_classes_declared_values_result( evaluated_args: &[RuntimeCellHandle], _context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - if evaluated_args.is_empty() { super::spl_classes::eval_spl_classes_result(values) } else { Err(EvalStatus::RuntimeFatal) } + if evaluated_args.is_empty() { + eval_spl_classes_result(values) + } else { + Err(EvalStatus::RuntimeFatal) + } } /// Evaluates PHP `spl_classes()` with no arguments. From 37625d42103f23705b92c0e6cd4397eb3454d829 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 19:47:59 +0200 Subject: [PATCH 1143/1208] docs(plans): mark magician builtin phase 4 complete --- .plans/elephc-magician-builtin-structure-plan.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.plans/elephc-magician-builtin-structure-plan.md b/.plans/elephc-magician-builtin-structure-plan.md index b3c31cc66d..020a6fcf93 100644 --- a/.plans/elephc-magician-builtin-structure-plan.md +++ b/.plans/elephc-magician-builtin-structure-plan.md @@ -26,7 +26,7 @@ - [x] Phase 4: convert `array_keys` and `array_values` so each builtin home file contains both its `eval_builtin!` declaration and its PHP-visible implementation. -- [ ] Phase 4: merge the remaining declaration-only builtin home files with +- [x] Phase 4: merge the remaining declaration-only builtin home files with their PHP-visible direct/by-value implementations, area by area. ## Goal @@ -248,6 +248,16 @@ For each migrated builtin: The pilot conversion moved `array_keys` and `array_values` into this stricter shape and deleted the old shared projection implementation file. +Phase 4 completion notes: + +- Declaration-only `eval_builtin!` leaf files have been eliminated. Each + migrated builtin home file now contains PHP-visible eval functions after its + registry declaration. +- The remaining shared functions are common algorithms owned by a sibling + builtin file or shared helper module, not one-builtin implementation files + hidden away from declaration-only homes. +- Old `declarations/` folders are gone from `crates/elephc-magician`. + ## Acceptance Criteria - Adding an eval builtin requires one home file and at most area `mod.rs` wiring, From 33ee2180972b2b077616abb68271e5af75799b59 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 21:17:48 +0200 Subject: [PATCH 1144/1208] refactor(magician): keep builtin dispatch in leaf files --- .../src/interpreter/builtins/array/count.rs | 92 +++++++++++++++++- .../builtins/formatting/number_format.rs | 28 ++++++ .../src/interpreter/builtins/hooks/hash.rs | 64 ------------ .../src/interpreter/builtins/hooks/mod.rs | 7 +- .../builtins/hooks/number_format.rs | 40 -------- .../builtins/hooks/string_split_join.rs | 35 ------- .../src/interpreter/builtins/hooks/values.rs | 31 ++++-- .../interpreter/builtins/string/explode.rs | 14 ++- .../interpreter/builtins/string/hash_algos.rs | 14 ++- .../interpreter/builtins/string/hash_copy.rs | 15 ++- .../interpreter/builtins/string/hash_final.rs | 19 +++- .../interpreter/builtins/string/hash_init.rs | 15 ++- .../builtins/string/hash_update.rs | 15 ++- .../interpreter/builtins/string/implode.rs | 14 ++- .../src/interpreter/core_builtins.rs | 97 ------------------- crates/elephc-magician/src/interpreter/mod.rs | 2 - 16 files changed, 243 insertions(+), 259 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/hash.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/number_format.rs delete mode 100644 crates/elephc-magician/src/interpreter/builtins/hooks/string_split_join.rs delete mode 100644 crates/elephc-magician/src/interpreter/core_builtins.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/array/count.rs b/crates/elephc-magician/src/interpreter/builtins/array/count.rs index 2376e4e483..85461169d4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/array/count.rs +++ b/crates/elephc-magician/src/interpreter/builtins/array/count.rs @@ -1,11 +1,12 @@ //! Purpose: -//! Declarative eval registry entry for `count`. +//! Declarative eval registry entry and implementation for `count`. //! //! Called from: //! - `crate::interpreter::builtins::array`. //! //! Key details: -//! - Runtime behavior stays delegated to the existing count hook. +//! - Recursive counting tracks visited arrays to avoid cycles. +//! - Top-level objects dispatch through `Countable::count()` when applicable. use super::super::spec::EvalBuiltinDefaultValue; @@ -40,3 +41,90 @@ pub(in crate::interpreter) fn eval_count_declared_values_result( _ => Err(EvalStatus::RuntimeFatal), } } + +/// Evaluates the builtin `count(...)` for arrays and `Countable` objects. +pub(in crate::interpreter) fn eval_builtin_count( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [value] => { + let value = eval_expr(value, context, scope, values)?; + eval_count_result(value, None, context, values) + } + [value, mode] => { + let value = eval_expr(value, context, scope, values)?; + let mode = eval_expr(mode, context, scope, values)?; + eval_count_result(value, Some(mode), context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Counts an eval array or dispatches top-level `Countable` objects. +pub(in crate::interpreter) fn eval_count_result( + value: RuntimeCellHandle, + mode: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mode = match mode { + Some(mode) => eval_int_value(mode, values)?, + None => EVAL_COUNT_NORMAL, + }; + if !matches!(mode, EVAL_COUNT_NORMAL | EVAL_COUNT_RECURSIVE) { + return Err(EvalStatus::RuntimeFatal); + } + if values.type_tag(value)? == EVAL_TAG_OBJECT + && eval_countable_object_matches(value, context, values)? + { + return eval_method_call_result(value, "count", Vec::new(), context, values); + } + let len = match mode { + EVAL_COUNT_NORMAL => values.array_len(value)?, + EVAL_COUNT_RECURSIVE => eval_count_recursive_len(value, values, &mut Vec::new())?, + _ => unreachable!("count mode was validated before dispatch"), + }; + let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + values.int(len) +} + +/// Returns whether an object value satisfies PHP's `Countable` interface. +fn eval_countable_object_matches( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(value, "Countable", false, context, values)? + .map_or_else(|| values.object_is_a(value, "Countable", false), Ok) +} + +/// Recursively counts nested eval arrays for `count($value, COUNT_RECURSIVE)`. +pub(in crate::interpreter) fn eval_count_recursive_len( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, + arrays_seen: &mut Vec, +) -> Result { + let address = value.as_ptr() as usize; + if arrays_seen.contains(&address) { + return Ok(0); + } + arrays_seen.push(address); + + let len = values.array_len(value)?; + let mut total = len; + for position in 0..len { + let key = values.array_iter_key(value, position)?; + let element = values.array_get(value, key)?; + if values.is_array_like(element)? { + total = total + .checked_add(eval_count_recursive_len(element, values, arrays_seen)?) + .ok_or(EvalStatus::RuntimeFatal)?; + } + } + + arrays_seen.pop(); + Ok(total) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs b/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs index c80e56c982..f00a01bff7 100644 --- a/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs +++ b/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs @@ -92,6 +92,34 @@ pub(in crate::interpreter) fn eval_number_format_result( values.string_bytes_value(&output) } +/// Dispatches evaluated `number_format()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_number_format_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [value] => eval_number_format_result(*value, None, None, None, values), + [value, decimals] => { + eval_number_format_result(*value, Some(*decimals), None, None, values) + } + [value, decimals, decimal_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + None, + values, + ), + [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( + *value, + Some(*decimals), + Some(*decimal_separator), + Some(*thousands_separator), + values, + ), + _ => Err(EvalStatus::RuntimeFatal), + } +} + /// Produces PHP `number_format()` bytes for finite scalar values. pub(in crate::interpreter) fn eval_number_format_bytes( value: f64, diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/hash.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/hash.rs deleted file mode 100644 index 8a3fcf1a59..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/hash.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Purpose: -//! Focused dispatch helpers for declarative hash builtin hooks. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks::values`. -//! -//! Key details: -//! - These helpers keep the generic evaluated-argument hook table below the -//! ordinary file-size limit while preserving the existing hash behavior. - -use super::super::super::{ElephcEvalContext, EvalStatus, RuntimeCellHandle, RuntimeValueOps}; -use super::super::{ - eval_hash_algos_result, eval_hash_copy_result, eval_hash_final_result, eval_hash_init_result, - eval_hash_update_result, -}; - -/// Dispatches evaluated `hash_algos()` calls. -pub(super) fn eval_hash_algos_values( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - eval_hash_algos_result(values) -} - -/// Dispatches evaluated incremental hash-context builtin calls. -pub(super) fn eval_hash_context_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "hash_copy" => { - let [hash_context] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_copy_result(*hash_context, context, values) - } - "hash_final" => match evaluated_args { - [hash_context] => eval_hash_final_result(*hash_context, false, context, values), - [hash_context, binary] => { - let binary = values.truthy(*binary)?; - eval_hash_final_result(*hash_context, binary, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - }, - "hash_init" => { - let [algo] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_init_result(*algo, context, values) - } - "hash_update" => { - let [hash_context, data] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_hash_update_result(*hash_context, *data, context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs index 458ff41fb2..5dc1022135 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/mod.rs @@ -5,14 +5,11 @@ //! - `crate::interpreter::builtins::spec` re-exports used by `eval_builtin!`. //! //! Key details: -//! - Direct expression dispatch, already-evaluated argument dispatch, and -//! focused hook helpers stay split so ordinary files remain small. +//! - Direct expression dispatch and already-evaluated argument dispatch stay +//! split so each dispatcher remains focused. mod arity; mod direct; -mod hash; -mod number_format; -mod string_split_join; mod values; pub(in crate::interpreter) use direct::EvalDirectHook; diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/number_format.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/number_format.rs deleted file mode 100644 index 245a562374..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/number_format.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! Purpose: -//! Focused evaluated-argument dispatch helper for `number_format`. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks::values`. -//! -//! Key details: -//! - Keeping the arity match here prevents the generic values hook table from -//! growing past the ordinary file-size limit. - -use super::super::super::{EvalStatus, RuntimeCellHandle, RuntimeValueOps}; -use super::super::eval_number_format_result; - -/// Dispatches evaluated `number_format` arguments. -pub(super) fn eval_number_format_values( - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match evaluated_args { - [value] => eval_number_format_result(*value, None, None, None, values), - [value, decimals] => { - eval_number_format_result(*value, Some(*decimals), None, None, values) - } - [value, decimals, decimal_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - None, - values, - ), - [value, decimals, decimal_separator, thousands_separator] => eval_number_format_result( - *value, - Some(*decimals), - Some(*decimal_separator), - Some(*thousands_separator), - values, - ), - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/string_split_join.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/string_split_join.rs deleted file mode 100644 index 196739e902..0000000000 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/string_split_join.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Purpose: -//! Focused dispatch helpers for declarative `explode` and `implode` hooks. -//! -//! Called from: -//! - `crate::interpreter::builtins::hooks::values`. -//! -//! Key details: -//! - The eval implementation still requires the currently supported two-argument -//! runtime form even though signature metadata exposes PHP-compatible defaults. - -use super::super::super::{EvalStatus, RuntimeCellHandle, RuntimeValueOps}; -use super::super::{eval_explode_result, eval_implode_result}; - -/// Dispatches evaluated `explode` and `implode` calls. -pub(super) fn eval_string_split_join_values( - name: &str, - evaluated_args: &[RuntimeCellHandle], - values: &mut impl RuntimeValueOps, -) -> Result { - match name { - "explode" => { - let [separator, string] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_explode_result(*separator, *string, values) - } - "implode" => { - let [separator, array] = evaluated_args else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_implode_result(*separator, *array, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index 341a40befc..a9a4eefe9a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -15,9 +15,6 @@ use super::super::super::{ ElephcEvalContext, EvalStatus, RuntimeCellHandle, RuntimeValueOps, }; use super::arity::{one_arg, three_args, two_args}; -use super::hash::{eval_hash_algos_values, eval_hash_context_values}; -use super::number_format::eval_number_format_values; -use super::string_split_join::eval_string_split_join_values; /// Evaluated-argument dispatch hooks for migrated builtins. #[derive(Clone, Copy)] @@ -410,8 +407,22 @@ impl EvalValuesHook { "gzuncompress" => eval_gzuncompress_result(evaluated_args, values), _ => Err(EvalStatus::RuntimeFatal), }, - Self::HashAlgos => eval_hash_algos_values(evaluated_args, values), - Self::HashContext => eval_hash_context_values(name, evaluated_args, context, values), + Self::HashAlgos => eval_hash_algos_declared_values_result(evaluated_args, values), + Self::HashContext => match name { + "hash_copy" => { + eval_hash_copy_declared_values_result(evaluated_args, context, values) + } + "hash_final" => { + eval_hash_final_declared_values_result(evaluated_args, context, values) + } + "hash_init" => { + eval_hash_init_declared_values_result(evaluated_args, context, values) + } + "hash_update" => { + eval_hash_update_declared_values_result(evaluated_args, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + }, Self::HashEquals => two_args(evaluated_args, values, eval_hash_equals_result), Self::HashOneShot => match name { "hash" => eval_hash_result(evaluated_args, values), @@ -455,7 +466,9 @@ impl EvalValuesHook { Self::Min => eval_min_result(evaluated_args, values), Self::MtRand => eval_mt_rand_values_result(evaluated_args, values), Self::NetworkEnv => eval_network_env_values_result(name, evaluated_args, values), - Self::NumberFormat => eval_number_format_values(evaluated_args, values), + Self::NumberFormat => { + eval_number_format_declared_values_result(evaluated_args, values) + } Self::Ord => one_arg(evaluated_args, values, eval_ord_result), Self::Pi => { if !evaluated_args.is_empty() { @@ -538,7 +551,11 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), } }), - Self::StringSplitJoin => eval_string_split_join_values(name, evaluated_args, values), + Self::StringSplitJoin => match name { + "explode" => eval_explode_declared_values_result(evaluated_args, values), + "implode" => eval_implode_declared_values_result(evaluated_args, values), + _ => Err(EvalStatus::RuntimeFatal), + }, Self::StreamBoolPredicate => one_arg(evaluated_args, values, |stream, values| { match name { "stream_is_local" => eval_stream_is_local_result(stream, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/string/explode.rs b/crates/elephc-magician/src/interpreter/builtins/string/explode.rs index cf4efc4867..beac8a3f52 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/explode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/explode.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime dispatch is declared here and implemented through the string split/join hook. +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - The current eval implementation supports the two-argument runtime form. use super::super::spec::EvalBuiltinDefaultValue; @@ -56,6 +57,17 @@ pub(in crate::interpreter) fn eval_explode_result( eval_push_explode_segment(result, index, &string[start..], values) } +/// Dispatches evaluated `explode()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_explode_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, string] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_explode_result(*separator, *string, values) +} + /// Appends one split segment to an indexed `explode()` result array. pub(in crate::interpreter) fn eval_push_explode_segment( array: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs index 78153b537d..b37f874f33 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime dispatch is declared here and implemented through the static hash algorithm list helper. +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - The static string-array helper is shared by list-returning string builtins. eval_builtin! { name: "hash_algos", @@ -35,6 +36,17 @@ pub(in crate::interpreter) fn eval_hash_algos_result( eval_static_string_array_result(EVAL_HASH_ALGOS, values) } +/// Dispatches evaluated `hash_algos()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_algos_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + eval_hash_algos_result(values) +} + /// Builds one indexed PHP array from a static string slice. pub(in crate::interpreter) fn eval_static_string_array_result( items: &[&str], diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs index 1d54be3a84..d2167151d8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime dispatch is declared here and implemented through the incremental hash-context hook. +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - Hash context resources are owned by the eval context stream table. eval_builtin! { name: "hash_copy", @@ -43,3 +44,15 @@ pub(in crate::interpreter) fn eval_hash_copy_result( None => Err(EvalStatus::RuntimeFatal), } } + +/// Dispatches evaluated `hash_copy()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_copy_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_copy_result(*hash_context, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs index 6757b0019a..8da01d7beb 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime dispatch is declared here and implemented through the incremental hash-context hook. +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - Finalizing consumes the hash context resource from the eval stream table. use super::super::spec::EvalBuiltinDefaultValue; @@ -54,3 +55,19 @@ pub(in crate::interpreter) fn eval_hash_final_result( .ok_or(EvalStatus::RuntimeFatal)?; super::hash::eval_format_digest_result(&raw, binary, values) } + +/// Dispatches evaluated `hash_final()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_final_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [hash_context] => eval_hash_final_result(*hash_context, false, context, values), + [hash_context, binary] => { + let binary = values.truthy(*binary)?; + eval_hash_final_result(*hash_context, binary, context, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs index e7182f57b7..03b999f55d 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime dispatch is declared here and implemented through the incremental hash-context hook. +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - Runtime resources are stored in the eval context stream table. //! - Optional HMAC parameters remain metadata-only for current eval behavior. use super::super::spec::EvalBuiltinDefaultValue; @@ -51,6 +52,18 @@ pub(in crate::interpreter) fn eval_hash_init_result( } } +/// Dispatches evaluated `hash_init()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_init_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [algo] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_init_result(*algo, context, values) +} + /// Converts a runtime resource cell into eval's zero-based hash context id. pub(in crate::interpreter) fn eval_hash_context_resource_id( hash_context: RuntimeCellHandle, diff --git a/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs b/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs index b27bfa55f7..51166702cf 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime dispatch is declared here and implemented through the incremental hash-context hook. +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - Hash context resources are owned by the eval context stream table. eval_builtin! { name: "hash_update", @@ -43,3 +44,15 @@ pub(in crate::interpreter) fn eval_hash_update_result( let data = values.string_bytes(data)?; values.bool_value(context.stream_resources_mut().update_hash_context(id, &data)) } + +/// Dispatches evaluated `hash_update()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_hash_update_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let [hash_context, data] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_hash_update_result(*hash_context, *data, context, values) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/string/implode.rs b/crates/elephc-magician/src/interpreter/builtins/string/implode.rs index 88c3dfc29c..94b37983e8 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/implode.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/implode.rs @@ -5,7 +5,8 @@ //! - `crate::interpreter::builtins::string`. //! //! Key details: -//! - Runtime dispatch is declared here and implemented through the string split/join hook. +//! - Direct and evaluated-argument dispatch stay in this leaf. +//! - The current eval implementation supports the two-argument runtime form. use super::super::spec::EvalBuiltinDefaultValue; @@ -58,3 +59,14 @@ pub(in crate::interpreter) fn eval_implode_result( } values.string_bytes_value(&output) } + +/// Dispatches evaluated `implode()` calls through the builtin leaf. +pub(in crate::interpreter) fn eval_implode_declared_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + let [separator, array] = evaluated_args else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_implode_result(*separator, *array, values) +} diff --git a/crates/elephc-magician/src/interpreter/core_builtins.rs b/crates/elephc-magician/src/interpreter/core_builtins.rs deleted file mode 100644 index 7df910723b..0000000000 --- a/crates/elephc-magician/src/interpreter/core_builtins.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Purpose: -//! Implements small core builtins that are tightly coupled to interpreter expression helpers. -//! -//! Called from: -//! - `crate::interpreter::expressions::eval_positional_expr_call()`. -//! -//! Key details: -//! - Domain builtins live in their `builtins::` leaf modules; this file only keeps shared core helpers. - -use super::*; - -/// Evaluates the builtin `count(...)` for arrays and `Countable` objects. -pub(in crate::interpreter) fn eval_builtin_count( - args: &[EvalExpr], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match args { - [value] => { - let value = eval_expr(value, context, scope, values)?; - eval_count_result(value, None, context, values) - } - [value, mode] => { - let value = eval_expr(value, context, scope, values)?; - let mode = eval_expr(mode, context, scope, values)?; - eval_count_result(value, Some(mode), context, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Counts an eval array or dispatches top-level `Countable` objects. -pub(in crate::interpreter) fn eval_count_result( - value: RuntimeCellHandle, - mode: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mode = match mode { - Some(mode) => eval_int_value(mode, values)?, - None => EVAL_COUNT_NORMAL, - }; - if !matches!(mode, EVAL_COUNT_NORMAL | EVAL_COUNT_RECURSIVE) { - return Err(EvalStatus::RuntimeFatal); - } - if values.type_tag(value)? == EVAL_TAG_OBJECT - && eval_countable_object_matches(value, context, values)? - { - return eval_method_call_result(value, "count", Vec::new(), context, values); - } - let len = match mode { - EVAL_COUNT_NORMAL => values.array_len(value)?, - EVAL_COUNT_RECURSIVE => eval_count_recursive_len(value, values, &mut Vec::new())?, - _ => unreachable!("count mode was validated before dispatch"), - }; - let len = i64::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; - values.int(len) -} - -/// Returns whether an object value satisfies PHP's `Countable` interface. -fn eval_countable_object_matches( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - dynamic_object_is_a(value, "Countable", false, context, values)? - .map_or_else(|| values.object_is_a(value, "Countable", false), Ok) -} - -/// Recursively counts nested eval arrays for `count($value, COUNT_RECURSIVE)`. -pub(in crate::interpreter) fn eval_count_recursive_len( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, - arrays_seen: &mut Vec, -) -> Result { - let address = value.as_ptr() as usize; - if arrays_seen.contains(&address) { - return Ok(0); - } - arrays_seen.push(address); - - let len = values.array_len(value)?; - let mut total = len; - for position in 0..len { - let key = values.array_iter_key(value, position)?; - let element = values.array_get(value, key)?; - if values.is_array_like(element)? { - total = total - .checked_add(eval_count_recursive_len(element, values, arrays_seen)?) - .ok_or(EvalStatus::RuntimeFatal)?; - } - } - - arrays_seen.pop(); - Ok(total) -} diff --git a/crates/elephc-magician/src/interpreter/mod.rs b/crates/elephc-magician/src/interpreter/mod.rs index f0bc62ee09..27d861cd7e 100644 --- a/crates/elephc-magician/src/interpreter/mod.rs +++ b/crates/elephc-magician/src/interpreter/mod.rs @@ -18,7 +18,6 @@ mod builtins; mod constant_eval; mod constants; mod control; -mod core_builtins; mod dynamic_functions; mod expressions; mod include_exec; @@ -60,7 +59,6 @@ use control::{ EvalByRefBindingMode, EvalControl, EvalPredefinedConstant, EvalSprintfSpec, EvaluatedCallArg, EvaluatedCallable, }; -use core_builtins::*; use dynamic_functions::*; use expressions::*; use include_exec::*; From 07fcbf5a54816707163ac500e81df30702970e71 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Wed, 8 Jul 2026 21:17:53 +0200 Subject: [PATCH 1145/1208] docs(plans): detail dynamic eval specialization work --- .plans/elephc-eval-magician-plan.md | 334 ++++++++++++++++++++++++++-- 1 file changed, 317 insertions(+), 17 deletions(-) diff --git a/.plans/elephc-eval-magician-plan.md b/.plans/elephc-eval-magician-plan.md index 54dee04e3f..2c686e9ac1 100644 --- a/.plans/elephc-eval-magician-plan.md +++ b/.plans/elephc-eval-magician-plan.md @@ -41,10 +41,25 @@ builtins, and static-only builtins not yet present in magician. - [ ] Reduce the remaining manual AOT mini-codegen and converge on internal EIR functions for supported literal fragments. +- [ ] Define dynamic-name semantics and fallback boundaries for variable + variables, dynamic instance properties, dynamic static properties, dynamic + member access, and `isset`/`empty`/`unset` over those forms. +- [ ] Complete or audit magician fallback support for `$$name`, `${$expr}`, + `$object->$property`, `$object->{$expr}`, nullsafe dynamic property reads, + dynamic static properties, and their write/ref-like variants. +- [ ] Add conservative AOT classification for statically known dynamic names + only when variable, object, method, property, and access-context facts are + precise. +- [ ] Add access-context-aware object/member lowering so specialized + private/protected reads preserve the original method/class scope instead of + becoming public call-site property reads. +- [ ] Add call-site method specialization for constant arguments such as + `$car->getProperty("color")`, guarded by exact receiver/method resolution and + semantic equivalence tests. - [ ] Expand AOT only where semantics are covered. Full arrays/iterables, - object/member access, references/by-ref, `global`, `static`, variable - variables, `try`/`throw`, include/require, and declarations stay fallback - until they have a dedicated model and tests. + references/by-ref, `global`, `static`, unresolved dynamic names, + `try`/`throw`, include/require, and declarations stay fallback until they have + a dedicated model and tests. - [ ] Close or explicitly maintain the static-only builtin gap: implement them in magician or keep them in a tested allowlist until eval exposes them. - [ ] Promote the most useful AOT acceptance benchmarks into the permanent @@ -260,22 +275,287 @@ Done criteria: - the assembly marker remains explicit; - no regression on macOS ARM64, Linux ARM64, or Linux x86_64. -### 2. Extend AOT Beyond the Current Static Subset +### 2. Dynamic Names and Static Object/Member Specialization + +The current AOT classifier deliberately treats object/member access and dynamic +names as fallback territory. That is correct until the compiler can prove the +same PHP-visible behavior as the magician fallback. The goal is not to make +dynamic PHP magically static everywhere; the goal is to recognize cases that are +only syntactically dynamic but semantically fixed at the call site or inside the +analyzed fragment. + +Important example: + +```php +class Car { + private string $color; + + public function getProperty($property) { + if (isset($this->$property)) { + return $this->$property; + } + return null; + } +} + +$car = new Car(); +echo $car->getProperty("color"); +``` + +The optimization must not rewrite this as a source-level `echo $car->color` in +the caller context, because `color` is private. The safe rewrite is either an +inlined clone that preserves `Car` as the lexical access context, or a synthetic +specialized method/helper such as `Car::getProperty$specialized_color($this)` +whose property reads are still authorized by `Car`. + +#### 2.1 Semantic Surfaces + +Cover these PHP forms explicitly before allowing AOT: + +- variable variables: + - `$$name`; + - `${$expr}`; + - read, assignment, compound assignment, increment/decrement, by-reference + binding, `isset`, `empty`, and `unset`; + - local scope, function scope, method scope, and eval-created variables; + - invalid or non-string-like names after PHP coercion. +- dynamic instance properties: + - `$object->$name`; + - `$object->{$expr}`; + - `$object?->$name` and `$object?->{$expr}`; + - read, assignment, compound assignment, array append/set, inc/dec, + by-reference binding, `isset`, `empty`, and `unset`. +- dynamic static properties and members: + - `$class::${$property}`; + - `self::${$property}`, `static::${$property}`, and `parent::${$property}`; + - static property read/write/isset/empty/unset where PHP permits it. +- member resolution rules: + - declared public/protected/private properties; + - private property slots attached to the declaring class; + - inheritance and trait-origin metadata; + - dynamic-property tails and `stdClass` properties; + - typed properties, uninitialized typed properties, readonly properties, and + nullable typed properties; + - `__get`, `__set`, `__isset`, and `__unset`; + - fatal/error behavior for inaccessible, undefined, or invalid accesses. + +Anything outside the modeled surface stays in magician fallback with an explicit +fallback reason. + +#### 2.2 Runtime Fallback Completion + +The fallback remains the semantic oracle. Before AOT is extended, audit and fill +gaps in: + +- `crates/elephc-magician/src/parser/` for all dynamic-name syntax accepted by + the main parser; +- `crates/elephc-magician/src/eval_ir.rs` for distinct read/write/isset/unset + operations instead of ad hoc expression handling; +- `crates/elephc-magician/src/interpreter/` for scope lookup, variable creation, + object property lookup, static property lookup, magic methods, typed-property + state, readonly checks, and by-reference aliases; +- `crates/elephc-magician/src/context.rs` for metadata needed to reflect AOT + classes and native runtime classes accurately; +- native runtime hooks under `src/codegen_support/runtime/` when magician must + call back into generated/AOT object layouts. + +Fallback done criteria: + +- every supported dynamic-name form has direct interpreter tests; +- PHP-equivalence tests cover visible output, return value, warnings/fatals, and + mutation side effects where practical; +- unsupported forms fail or fall back deliberately rather than partially + evaluating with the wrong scope or visibility. + +#### 2.3 AOT Fact Model + +Introduce a small, invalidation-aware fact model before changing the AOT +classifier. Required facts: + +- known local string value: `$property === "color"`; +- known variable-variable target: `$$name` maps to `$color` only while `$name` + and the target scope remain unchanged; +- exact object allocation: `$car` is exactly `new Car()` and cannot be an + unknown subclass at that point; +- receiver method target: `$car->getProperty(...)` resolves to exactly + `Car::getProperty`; +- constant argument facts at call sites; +- declared property identity: `Car::$color` as a property slot, not just the + string `"color"`; +- lexical access context: the class scope that authorized the original + property access; +- initialization/nullability facts only when they are already proven by + existing type/flow analysis. + +Facts must be invalidated by: + +- assignments to any variable participating in the fact; +- variable variables that may alias the variable; +- by-reference calls, references, `global`, `static`, or unknown mutating calls; +- dynamic `eval`, include/require, or unknown callbacks; +- writes to object properties that may affect the resolved slot; +- any path where control-flow merge loses precision. + +The first implementation can be local and conservative. It does not need a full +whole-program optimizer, but it must refuse ambiguous cases. + +#### 2.4 Static Dynamic-Name AOT + +After fallback semantics and facts exist, allow AOT only for narrow cases: + +- `$$name` when `name` is a compile-time-known string and the target variable + is a normal local/scope variable with no active reference ambiguity; +- `${$expr}` when `$expr` folds to the same safe known string; +- `$this->$property` inside a known class method when `$property` is a known + string and resolves to a declared property visible from that method's lexical + class; +- `$object->{$property}` when the object has an exact class fact, the property + string is known, and the access is public or has an explicitly preserved + lexical access context; +- `isset($object->$property)` / `empty($object->$property)` when the lowering can + use a non-reading property probe that preserves PHP behavior for uninitialized + typed properties and magic `__isset`; +- nullsafe dynamic property reads only when the null branch and non-null branch + match PHP evaluation order and side effects. + +Keep fallback for: + +- unknown property names; +- unknown receiver classes; +- possible subclass overrides without an exact receiver or final method/class; +- inaccessible properties without a preserved lexical access context; +- magic methods unless the AOT path explicitly models their dispatch; +- references/by-ref paths until the ref-cell model is identical to fallback; +- dynamic static properties involving late static binding unless `self`/`static` + context is precise. + +#### 2.5 Access-Context-Aware Lowering + +Private and protected properties require a lowering model that distinguishes +source context from call-site context. + +Do not lower a specialized private read to a normal caller-side property access. +Instead, choose one of these representations: + +- an internal EIR property operation carrying: + - object value; + - resolved property identity/slot; + - lexical access context; + - operation kind (`read`, `isset`, `write`, `unset`, etc.); +- or a synthetic specialized method/helper compiled with the original class as + its access context. + +Required invariants: + +- property visibility is checked as if the original method body performed the + access; +- private properties resolve to the declaring class slot, not to a public or + child-class property with the same string name; +- protected properties preserve inheritance visibility rules; +- `isset` and `empty` do not accidentally read uninitialized typed properties; +- readonly and asymmetric property rules remain enforced on writes; +- magic methods are called only when PHP would call them; +- the generated path remains target-aware across macOS ARM64, Linux ARM64, and + Linux x86_64. + +#### 2.6 Call-Site Method Specialization + +Specialization should be a separate phase from basic dynamic-name support. + +Candidate requirements: + +- receiver exactness: + - exact allocation such as `$car = new Car()` in the same analyzable flow; or + - final class/final method evidence strong enough to avoid virtual dispatch + changes; +- method body available in the current compilation unit after includes are + resolved; +- call arguments have stable constant facts or simple value facts; +- the method body does not contain unsupported constructs for the chosen + specialization path; +- no by-reference parameters, references, dynamic `eval`, include/require, + unknown callbacks, or global/static interactions unless explicitly modeled; +- parameter defaults, named arguments, variadics, and spread arguments have been + normalized through the shared call-argument planner. + +Implementation options: + +1. Inline a cloned method body at the call site, preserving lexical class scope. +2. Generate a synthetic internal function/method keyed by receiver class, + method, and constant-argument shape. + +The first version should prefer synthetic helpers if that keeps access context, +cleanup, and debug markers easier to reason about. + +Specialization must preserve: + +- source evaluation order for receiver and arguments; +- method return semantics; +- `$this`, `self`, `static`, and `parent` resolution; +- visibility and property initialization checks; +- side effects before any early return or fatal; +- fallback behavior when any guard/fact is not available at compile time. + +#### 2.7 Tests and Benchmarks + +Add focused coverage in layers: + +- parser tests for `$$name`, `${$expr}`, `$obj->$name`, `$obj->{$expr}`, + nullsafe dynamic properties, and dynamic static properties; +- magician tests for dynamic variables and dynamic properties in normal reads, + writes, `isset`, `empty`, `unset`, typed properties, private/protected/public + properties, magic methods, and dynamic-property tails; +- codegen tests proving unsupported dynamic paths use magician fallback with a + readable marker; +- AOT tests proving statically known dynamic names do not call + `__elephc_eval_execute`; +- specialization tests for the `Car::getProperty("color")` shape, including: + - private property access remains legal through preserved `Car` context; + - uninitialized typed property returns the `isset`/`null` behavior; + - initialized property returns the expected value; + - subclass/override ambiguity falls back; + - public dynamic property access still works without private-context rules; +- negative/error tests for inaccessible properties, readonly writes, invalid + dynamic names, and magic-method edge cases; +- PHP cross-checks with `ELEPHC_PHP_CHECK=1` where behavior is subtle. + +Benchmark only after correctness is in place. A useful manual benchmark would +compare: + +- plain static property access; +- dynamic property access through magician; +- static dynamic-name AOT; +- call-site specialized getter. + +#### 2.8 Done Criteria + +This work is done only when: + +1. fallback semantics cover the declared dynamic-name subset; +2. unsupported dynamic-name cases produce explicit fallback reasons; +3. AOT accepts only statically resolved dynamic names with precise facts; +4. private/protected property specialization preserves lexical access context; +5. call-site specialization never changes virtual dispatch, visibility, + evaluation order, or error behavior; +6. tests cover runtime fallback, AOT acceptance, AOT rejection, and PHP-equivalent + edge cases; +7. focused checks pass on all supported targets for any codegen/runtime changes. + +### 3. General AOT Expansion Beyond Dynamic Names Every new construct must be introduced only with a semantic model and tests. -Reasonable priority: +Reasonable priority after the dynamic-name work: 1. arrays/iterables in AOT once COW and ownership are clear; -2. statically resolvable object/member access; -3. references/by-ref only if the ref-cell model is identical to runtime; -4. `global`, `static`, and variable variables; -5. `try`/`throw`; -6. include/require; -7. declarations inside eval. +2. references/by-ref only if the ref-cell model is identical to runtime; +3. `global` and `static`; +4. `try`/`throw`; +5. include/require; +6. declarations inside eval. Everything not modeled stays fallback. -### 3. Compiler/Eval Builtin Parity +### 4. Compiler/Eval Builtin Parity `tests/builtin_parity_tests.rs` distinguishes: @@ -292,7 +572,7 @@ When a static-only builtin is implemented in magician: - add named/positional tests when relevant; - update benchmarks only if the builtin enters an eval hot path. -### 4. Benchmarks and Measurement +### 5. Benchmarks and Measurement The benchmark suite exists. Remaining work: @@ -302,7 +582,7 @@ The benchmark suite exists. Remaining work: regression or CI artifact; - preserve output correctness against PHP where practical. -### 5. Documentation +### 6. Documentation Update docs when the subset changes: @@ -332,6 +612,17 @@ cargo test --test codegen_tests eval_ git diff --check ``` +For dynamic-name or object/member specialization changes: + +```bash +cargo check +cargo test -p elephc-magician dynamic_property +cargo test -p elephc-magician variable_variable +cargo test --test codegen_tests eval_dynamic +cargo test --test codegen_tests literal_eval_static +git diff --check +``` + For ABI/codegen/runtime ownership changes: ```bash @@ -359,6 +650,13 @@ python3 scripts/benchmark_magician.py --case literal_scalar_aot --iterations 5 - leaks, or missed mutations if they bypass runtime helpers. - `eval('$x + 1;')` returns `null`, not the last expression. - Over-aggressive fallback selection can miscompile dynamic code. +- Variable variables can invalidate otherwise local-looking facts. +- Dynamic property specialization can break private/protected visibility unless + lexical access context is preserved explicitly. +- `isset`/`empty` over typed properties must not be lowered into reads that + fatal on uninitialized state. +- Magic property methods can turn apparently local object access into arbitrary + user code. - Magician optimizations must not freeze context/scope/magic constants. - Every new path must stay target-aware on macOS ARM64, Linux ARM64, and Linux x86_64. @@ -372,7 +670,9 @@ The eval/magician work can be considered closed when: 3. the AOT subset does not depend on an unmaintainable manual mini-backend; 4. fully AOT programs do not link `elephc_magician`; 5. static/eval builtin parity has no stale allowlist entries; -6. prime-loop and algebra-heavy benchmarks remain correct and measurable; -7. all three supported targets have focused coverage for every ABI/codegen +6. dynamic-name and object/member specializations preserve lexical access + context, visibility, and PHP fallback behavior; +7. prime-loop and algebra-heavy benchmarks remain correct and measurable; +8. all three supported targets have focused coverage for every ABI/codegen change; -8. docs and tests exactly reflect the supported subset and fallbacks. +9. docs and tests exactly reflect the supported subset and fallbacks. From 4f3aa30a84941885750c950dddb878f951245e27 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 9 Jul 2026 09:14:45 +0200 Subject: [PATCH 1146/1208] fix: adapt eval branch after main rebase --- src/codegen_support/runtime/data/user.rs | 16 ++++++---------- src/ir/module.rs | 2 +- src/ir_lower/program.rs | 2 +- src/optimize/propagate/expr.rs | 2 +- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index 91694c707b..be03124564 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -57,7 +57,7 @@ pub(crate) fn emit_runtime_data_user( interface_names: &[String], trait_names: &[String], declared_trait_uses: &HashMap>, - declared_trait_source_lines: &HashMap, + declared_trait_source_lines: &HashMap, classes: &HashMap, enums: &HashMap, allowed_class_names: Option<&HashSet>, @@ -1201,9 +1201,7 @@ fn eval_reflection_method_flags_with_source_lines( else { return flags; }; - let Ok(start_line) = u64::try_from(method.span.line) else { - return flags; - }; + let start_line = u64::from(method.span.line); if start_line == 0 || start_line > EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK { return flags; } @@ -1447,7 +1445,7 @@ fn emit_eval_reflection_class_lookup_data( out: &mut String, sorted_classes: &[(&String, &ClassInfo)], sorted_interfaces: &[(&String, &InterfaceInfo)], - declared_trait_source_lines: &HashMap, + declared_trait_source_lines: &HashMap, ) { let mut entries = Vec::new(); let mut index = 0usize; @@ -1530,15 +1528,13 @@ fn eval_reflection_interface_flags(interface_info: &InterfaceInfo) -> u64 { } /// Returns eval ReflectionClass source-location bits retained for one generated/AOT trait. -fn eval_reflection_trait_flags(line: usize) -> u64 { +fn eval_reflection_trait_flags(line: u32) -> u64 { eval_reflection_source_line_flags(line) } /// Encodes declaration line metadata into high ReflectionClass flag bits. -fn eval_reflection_source_line_flags(line: usize) -> u64 { - let Ok(start_line) = u64::try_from(line) else { - return 0; - }; +fn eval_reflection_source_line_flags(line: u32) -> u64 { + let start_line = u64::from(line); if start_line == 0 || start_line > EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK { return 0; } diff --git a/src/ir/module.rs b/src/ir/module.rs index b4b603529f..e55a526edf 100644 --- a/src/ir/module.rs +++ b/src/ir/module.rs @@ -68,7 +68,7 @@ pub struct Module { pub declared_class_names: Vec, pub declared_interface_names: Vec, pub declared_trait_names: Vec, - pub declared_trait_source_lines: HashMap, + pub declared_trait_source_lines: HashMap, pub declared_trait_uses: HashMap>, pub declared_trait_method_names: HashMap>, pub declared_trait_methods: HashMap>, diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index c0c36f4cc8..dd709fb04a 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -1143,7 +1143,7 @@ fn collect_declared_trait_names(program: &Program) -> Vec { } /// Collects source line metadata for user-declared traits, keyed by trait name. -fn collect_declared_trait_source_lines(program: &Program) -> HashMap { +fn collect_declared_trait_source_lines(program: &Program) -> HashMap { let mut lines = HashMap::new(); for stmt in program { match &stmt.kind { diff --git a/src/optimize/propagate/expr.rs b/src/optimize/propagate/expr.rs index f2efa431a7..ddfaf17acd 100644 --- a/src/optimize/propagate/expr.rs +++ b/src/optimize/propagate/expr.rs @@ -331,7 +331,7 @@ pub(crate) fn propagate_expr(expr: Expr, env: &ConstantEnv) -> Expr { ExprKind::NullsafeDynamicMethodCall { object: Box::new(object), method: Box::new(method), - args: propagate_args(args, None), + args: propagate_args(args, None, None), } } ExprKind::StaticMethodCall { From cea43beb8b209a2301678ea6e5b9c7ded63e5f17 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 9 Jul 2026 09:46:55 +0200 Subject: [PATCH 1147/1208] fix: repair nextest archive build --- src/optimize/tests/effects/globals.rs | 2 ++ src/optimize/tests/propagate/collections.rs | 2 ++ src/optimize/tests/propagate/invalidation.rs | 4 ++++ src/optimize/tests/propagate/signatures.rs | 5 +++++ src/optimize/tests/propagate/straight_line.rs | 4 ++++ src/optimize/tests/propagate/targeted_invalidation.rs | 3 +++ src/optimize/tests/propagate/volatility.rs | 1 + tests/builtin_parity_tests.rs | 2 +- 8 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/optimize/tests/effects/globals.rs b/src/optimize/tests/effects/globals.rs index 7236806fcf..c4cc6f0929 100644 --- a/src/optimize/tests/effects/globals.rs +++ b/src/optimize/tests/effects/globals.rs @@ -18,7 +18,9 @@ fn function_decl(name: &str, body: Vec) -> Stmt { StmtKind::FunctionDecl { name: name.to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/propagate/collections.rs b/src/optimize/tests/propagate/collections.rs index 7c724b4a42..1774c39949 100644 --- a/src/optimize/tests/propagate/collections.rs +++ b/src/optimize/tests/propagate/collections.rs @@ -320,7 +320,9 @@ fn test_array_fact_survives_by_value_user_call() { StmtKind::FunctionDecl { name: "reader".to_string(), params: vec![("arr".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/propagate/invalidation.rs b/src/optimize/tests/propagate/invalidation.rs index 09b2ab0439..e6e802bfb1 100644 --- a/src/optimize/tests/propagate/invalidation.rs +++ b/src/optimize/tests/propagate/invalidation.rs @@ -99,7 +99,9 @@ fn test_user_by_ref_param_invalidates_and_retains() { ("p".to_string(), None, None, true), ("q".to_string(), None, None, false), ], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -146,7 +148,9 @@ fn test_top_level_globals_guard() { StmtKind::FunctionDecl { name: "gw".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/propagate/signatures.rs b/src/optimize/tests/propagate/signatures.rs index 63c11bf9fa..488988c54f 100644 --- a/src/optimize/tests/propagate/signatures.rs +++ b/src/optimize/tests/propagate/signatures.rs @@ -25,7 +25,9 @@ fn function_with_params(name: &str, params: Vec<(&str, bool)>) -> Stmt { .into_iter() .map(|(param, is_ref)| (param.to_string(), None, None, is_ref)) .collect(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -48,7 +50,9 @@ fn method_with_params(name: &str, params: Vec<(&str, bool)>) -> ClassMethod { .into_iter() .map(|(param, is_ref)| (param.to_string(), None, None, is_ref)) .collect(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -90,6 +94,7 @@ fn by_ref_property(name: &str) -> ClassProperty { is_static: false, is_abstract: false, by_ref: true, + is_promoted: false, default: None, span: Span::dummy(), attributes: Vec::new(), diff --git a/src/optimize/tests/propagate/straight_line.rs b/src/optimize/tests/propagate/straight_line.rs index a6e6e75012..7a52faf9ec 100644 --- a/src/optimize/tests/propagate/straight_line.rs +++ b/src/optimize/tests/propagate/straight_line.rs @@ -213,7 +213,9 @@ fn test_pure_user_call_keeps_constants() { StmtKind::FunctionDecl { name: "pf".to_string(), params: vec![("a".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -261,7 +263,9 @@ fn test_global_writing_user_call_clears_constants_at_top_level() { StmtKind::FunctionDecl { name: "gw".to_string(), params: Vec::new(), + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, diff --git a/src/optimize/tests/propagate/targeted_invalidation.rs b/src/optimize/tests/propagate/targeted_invalidation.rs index 6fed0f0ae1..4bba07e3de 100644 --- a/src/optimize/tests/propagate/targeted_invalidation.rs +++ b/src/optimize/tests/propagate/targeted_invalidation.rs @@ -33,7 +33,9 @@ fn noisy_function(name: &str) -> Stmt { StmtKind::FunctionDecl { name: name.to_string(), params: vec![("p".to_string(), None, None, false)], + param_attributes: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, by_ref_return: false, @@ -242,6 +244,7 @@ fn test_closure_by_ref_capture_kills_existing_fact() { ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, body: Vec::new(), diff --git a/src/optimize/tests/propagate/volatility.rs b/src/optimize/tests/propagate/volatility.rs index 71dd94048e..4e610a5a62 100644 --- a/src/optimize/tests/propagate/volatility.rs +++ b/src/optimize/tests/propagate/volatility.rs @@ -21,6 +21,7 @@ fn closure_with_captures(captures: Vec, capture_refs: Vec) -> Ex ExprKind::Closure { params: Vec::new(), variadic: None, + variadic_by_ref: false, variadic_type: None, return_type: None, body: Vec::new(), diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index f8055d03ac..1f8779a3c4 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -16,7 +16,7 @@ const EVAL_DIRECT_DISPATCH_SOURCES: &[&str] = &[include_str!( )]; const EVAL_DYNAMIC_DISPATCH_SOURCES: &[&str] = &[include_str!( - "../crates/elephc-magician/src/interpreter/builtins/raw_memory.rs" + "../crates/elephc-magician/src/interpreter/builtins/raw_memory/mod.rs" )]; /// Eval-only reflection probes exist because magician can inspect dynamic eval metadata before the AOT catalog exposes them. From 9ea49f90182dc3cbf0de50fdcc9d5e00efd2a40c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 9 Jul 2026 14:13:41 +0200 Subject: [PATCH 1148/1208] fix: restore CI attribute and image prelude tests --- src/builtins/system/attr_support.rs | 29 +++++++++------------- src/parser/keyword_name.rs | 1 + tests/error_tests/classes_traits.rs | 9 ++++--- tests/parser_tests/classes/declarations.rs | 14 +++++++++++ 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/builtins/system/attr_support.rs b/src/builtins/system/attr_support.rs index b07df26dae..bc996b6c13 100644 --- a/src/builtins/system/attr_support.rs +++ b/src/builtins/system/attr_support.rs @@ -50,33 +50,28 @@ pub(crate) fn class_attribute_args_unsupported( .enumerate() .find(|(_, name)| php_symbol_key(name.trim_start_matches('\\')) == attr_key) .is_some_and(|(idx, _)| match class_info.attribute_args.get(idx) { - // The flat `class_attribute_args()` helper returns a positional - // array of materialized scalars, so it cannot faithfully echo keyed - // arguments (named arguments or associative arrays, at any depth) or - // deferred symbolic references (global/class constants, enum cases). - // Reject them and direct users to - // `ReflectionClass::getAttributes()->getArguments()` instead. + // The flat `class_attribute_args()` helper can materialize literal + // positional, named, and array-keyed arguments. It cannot resolve + // deferred symbolic references (global/class constants, enum cases) + // on this echo path, so those stay rejected. Some(Some(entries)) => attr_entries_unsupported_by_flat_helper(entries), _ => true, }) } /// Returns true when the flat `class_attribute_args()` helper cannot faithfully -/// echo the captured entries: keyed arguments (named arguments or -/// associative-array keys, at any depth) would lose their keys, and deferred -/// symbolic references (global/class constants, enum cases) are not materialized -/// on this echo path. Both are supported through -/// `ReflectionClass::getAttributes()->getArguments()` instead. +/// echo the captured entries: deferred symbolic references (global/class +/// constants, enum cases) are not materialized on this echo path. Literal keyed +/// arguments are supported and retain their keys. pub(crate) fn attr_entries_unsupported_by_flat_helper( entries: &[crate::types::AttrArgEntry], ) -> bool { entries.iter().any(|entry| { - entry.key.is_some() - || matches!( - &entry.value, - crate::types::AttrArgValue::ConstRef(_) - | crate::types::AttrArgValue::ScopedConst(..) - ) + matches!( + &entry.value, + crate::types::AttrArgValue::ConstRef(_) + | crate::types::AttrArgValue::ScopedConst(..) + ) || matches!( &entry.value, crate::types::AttrArgValue::Array(inner) diff --git a/src/parser/keyword_name.rs b/src/parser/keyword_name.rs index 5d95d0351f..12d64d0724 100644 --- a/src/parser/keyword_name.rs +++ b/src/parser/keyword_name.rs @@ -74,6 +74,7 @@ pub(crate) fn bareword_name_from_token(token: &Token) -> Option { Token::Class => Some("class".to_string()), Token::Enum => Some("enum".to_string()), Token::New => Some("new".to_string()), + Token::Clone => Some("clone".to_string()), Token::Public => Some("public".to_string()), Token::Protected => Some("protected".to_string()), Token::Private => Some("private".to_string()), diff --git a/tests/error_tests/classes_traits.rs b/tests/error_tests/classes_traits.rs index 80b3ac83e1..10434f08d8 100644 --- a/tests/error_tests/classes_traits.rs +++ b/tests/error_tests/classes_traits.rs @@ -422,12 +422,13 @@ fn test_error_class_attribute_args_non_string_attr() { ); } -/// Verifies that `class_attribute_args()` on an attribute with named arguments reports -/// "requested attribute uses argument metadata that is not supported yet". +/// Verifies that `class_attribute_args()` on an attribute with an unmaterialized +/// symbolic constant argument reports "requested attribute uses argument metadata +/// that is not supported yet". #[test] -fn test_error_class_attribute_named_args_are_not_silently_dropped() { +fn test_error_class_attribute_const_args_are_not_silently_dropped() { expect_error( - " { + assert_eq!(methods.len(), 1); + assert_eq!(methods[0].name, "clone"); + } + _ => panic!("Expected ClassDecl"), + } +} + /// Verifies that ` Date: Thu, 9 Jul 2026 17:59:23 +0200 Subject: [PATCH 1149/1208] fix(eval): restore AOT eval CI cases --- .../src/interpreter/builtins/symbols.rs | 4 +- .../builtins/symbols/class_attribute_names.rs | 8 +- .../src/interpreter/statements.rs | 9 + src/codegen/eval_reflection_helpers.rs | 6 +- src/codegen/frame.rs | 3 + src/codegen_support/data_section.rs | 5 + src/codegen_support/reflection.rs | 14 +- src/ir_lower/expr/mod.rs | 12 +- src/ir_lower/function.rs | 39 ++++- src/ir_lower/program.rs | 1 + tests/builtin_parity_tests.rs | 4 + tests/codegen/eval.rs | 161 ++++++++++++++++-- tests/codegen/eval_builtin_parity.rs | 27 +++ 13 files changed, 260 insertions(+), 33 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index 5d3d33994c..d2672b895f 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -53,7 +53,9 @@ mod trait_exists; mod unset; pub(in crate::interpreter) use dispatch::{eval_builtin_symbols_call, eval_symbols_values_result}; -pub(in crate::interpreter) use class_attribute_names::eval_reflection_attribute_array_result; +pub(in crate::interpreter) use class_attribute_names::{ + eval_class_attribute_args_result, eval_reflection_attribute_array_result, +}; pub(in crate::interpreter) use class_implements::eval_class_relation_result; pub(in crate::interpreter) use function_exists::eval_function_probe_exists; pub(in crate::interpreter) use get_class::eval_get_class_result; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs index e06cda892b..7d3b596ef3 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs @@ -178,11 +178,15 @@ fn eval_attribute_is_repeated(attributes: &[EvalAttribute], name: &str) -> bool } /// Builds the mixed PHP array returned by `class_attribute_args()`. -fn eval_class_attribute_args_result( +pub(in crate::interpreter) fn eval_class_attribute_args_result( args: &[EvalAttributeArg], values: &mut impl RuntimeValueOps, ) -> Result { - let mut result = values.assoc_new(args.len())?; + let mut result = if args.iter().any(|arg| arg.name().is_some()) { + values.assoc_new(args.len())? + } else { + values.array_new(args.len())? + }; for (index, arg) in args.iter().enumerate() { let key = match arg.name() { Some(name) => values.string(name)?, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 77705c72fc..748023d488 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -8829,6 +8829,15 @@ pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( values, ); } + if method_name.eq_ignore_ascii_case("getArguments") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(args) = attribute_metadata.attribute().args() else { + return Err(EvalStatus::RuntimeFatal); + }; + return eval_class_attribute_args_result(args, values); + } if method_name.eq_ignore_ascii_case("getTarget") { if !evaluated_args.is_empty() { return Err(EvalStatus::RuntimeFatal); diff --git a/src/codegen/eval_reflection_helpers.rs b/src/codegen/eval_reflection_helpers.rs index d48866e52b..abd0b3360e 100644 --- a/src/codegen/eval_reflection_helpers.rs +++ b/src/codegen/eval_reflection_helpers.rs @@ -270,13 +270,12 @@ fn emit_set_args_property_aarch64( emitter.instruction(&format!("b.ne {}", fail_label)); // reject non-array argument metadata emitter.label(args_ok_label); emitter.instruction("str x1, [sp, #32]"); // save the unboxed argument array across incref - emitter.instruction("str x0, [sp, #56]"); // save the argument array tag for the object slot emitter.instruction("mov x0, x1"); // move the array payload into the incref argument register emitter.instruction("bl __rt_incref"); // retain the argument array for ReflectionAttribute ownership emitter.instruction("ldr x1, [sp, #32]"); // reload the retained argument array payload emitter.instruction("ldr x9, [sp, #24]"); // reload the ReflectionAttribute object pointer abi::emit_store_to_address(emitter, "x1", "x9", layout.args_lo); - emitter.instruction("ldr x10, [sp, #56]"); // reload the original argument array tag + emitter.instruction("mov x10, #4"); // store the native property array tag expected by getArguments() abi::emit_store_to_address(emitter, "x10", "x9", layout.args_hi); } @@ -297,13 +296,12 @@ fn emit_set_args_property_x86_64( emitter.instruction(&format!("jne {}", fail_label)); // reject non-array argument metadata emitter.label(args_ok_label); emitter.instruction("mov QWORD PTR [rbp - 56], rdi"); // save the unboxed argument array across incref - emitter.instruction("mov QWORD PTR [rbp - 64], rax"); // save the argument array tag for the object slot emitter.instruction("mov rax, rdi"); // move the array payload into the incref argument register emitter.instruction("call __rt_incref"); // retain the argument array for ReflectionAttribute ownership emitter.instruction("mov rdi, QWORD PTR [rbp - 56]"); // reload the retained argument array payload emitter.instruction("mov r10, QWORD PTR [rbp - 48]"); // reload the ReflectionAttribute object pointer abi::emit_store_to_address(emitter, "rdi", "r10", layout.args_lo); - emitter.instruction("mov r11, QWORD PTR [rbp - 64]"); // reload the original argument array tag + emitter.instruction("mov r11, 4"); // store the native property array tag expected by getArguments() abi::emit_store_to_address(emitter, "r11", "r10", layout.args_hi); } diff --git a/src/codegen/frame.rs b/src/codegen/frame.rs index cd32a96c48..cb9b2cc2cd 100644 --- a/src/codegen/frame.rs +++ b/src/codegen/frame.rs @@ -291,6 +291,9 @@ fn emit_main_global_epilogue_cleanup(ctx: &mut FunctionContext<'_>) { continue; } let symbol = ir_global_symbol(&name); + if !ctx.data.has_comm(&symbol) { + continue; + } ctx.emitter.comment(&format!("epilogue cleanup global ${}", name)); emit_static_symbol_value_cleanup(ctx, &symbol, &ty); abi::emit_store_zero_to_symbol(ctx.emitter, &symbol, 0); diff --git a/src/codegen_support/data_section.rs b/src/codegen_support/data_section.rs index 05a27a5df6..2311aff074 100644 --- a/src/codegen_support/data_section.rs +++ b/src/codegen_support/data_section.rs @@ -131,6 +131,11 @@ impl DataSection { label } + /// Returns true when common storage has been declared for `label`. + pub fn has_comm(&self, label: &str) -> bool { + self.comm_dedup.contains_key(label) + } + /// Adds words to the current runtime or metadata collection. pub fn add_words(&mut self, words: Vec) -> String { if let Some(label) = self.word_dedup.get(&words) { diff --git a/src/codegen_support/reflection.rs b/src/codegen_support/reflection.rs index 9f1378681d..5dc5239970 100644 --- a/src/codegen_support/reflection.rs +++ b/src/codegen_support/reflection.rs @@ -301,11 +301,17 @@ pub(crate) fn build_attribute_get_arguments_body_with_extra( span, )); } - // Every attribute with supported arguments is registered as a factory - // above (class or not), so this is only a defensive default; return an - // empty associative array to match the declared return type. + // Runtime eval materializes `ReflectionAttribute` objects with factory id + // zero and stores their retained arguments in `__args`, so the fallback + // must preserve that path instead of returning an empty factory miss. body.push(Stmt::new( - StmtKind::Return(Some(entries_to_array_expr(&[], true))), + StmtKind::Return(Some(Expr::new( + ExprKind::PropertyAccess { + object: Box::new(Expr::new(ExprKind::This, span)), + property: "__args".to_string(), + }, + span, + ))), span, )); body diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index d38ffc1380..12031627ce 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1953,13 +1953,13 @@ fn emit_builtin_call_value( /// Returns true when a literal `eval` call may still need runtime scope/interpreter state. fn eval_literal_needs_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { - return false; + return true; } if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { - return false; + return true; } if eval_literal_local_scalar_direct_sync_supported_by_lowering(ctx, fragment) { - return false; + return true; } let static_call_supported = |name: &str, args: &[Expr]| { eval_literal_static_function_supported_by_lowering(ctx, name, args) @@ -2002,13 +2002,13 @@ fn eval_literal_needs_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> /// Returns true when a literal `eval` only needs materialized eval-scope state. fn eval_literal_needs_scope_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { - return false; + return true; } if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { - return false; + return true; } if eval_literal_local_scalar_direct_sync_supported_by_lowering(ctx, fragment) { - return false; + return true; } let static_call_supported = |name: &str, args: &[Expr]| { eval_literal_static_function_supported_by_lowering(ctx, name, args) diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 6bb84f84f4..208077c366 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -18,6 +18,7 @@ use crate::ir_lower::context::{ StaticCallableBinding, }; use crate::ir_lower::effects_lookup; +use crate::names::php_symbol_key; use crate::parser::ast::{ AttributeGroup, ClassMethod, Expr, ExprKind, Program, Stmt, StmtKind, TypeExpr, }; @@ -1089,8 +1090,13 @@ pub(crate) fn eir_signature_with_php_param_contracts( let by_ref = signature.ref_params.get(index).copied().unwrap_or(false); let variadic = signature.variadic.as_deref() == Some(name.as_str()); if !declared && !by_ref && !variadic { - if preserve_untyped_eir_param_contract(owner_name, name, php_type, callable_param_sigs) - { + if preserve_untyped_eir_param_contract( + owner_name, + index, + name, + php_type, + callable_param_sigs, + ) { continue; } *php_type = PhpType::Mixed; @@ -1119,14 +1125,41 @@ fn eir_runtime_metadata_signature(signature: &FunctionSig) -> FunctionSig { /// Returns true when an inferred untyped parameter has an EIR-safe concrete ABI contract. fn preserve_untyped_eir_param_contract( owner_name: &str, + param_index: usize, param_name: &str, php_type: &PhpType, callable_param_sigs: &std::collections::HashMap<(String, String), FunctionSig>, ) -> bool { - matches!(php_type.codegen_repr(), PhpType::Callable) + magic_method_param_keeps_eir_contract(owner_name, param_index, php_type) + || matches!(php_type.codegen_repr(), PhpType::Callable) || callable_param_sigs.contains_key(&(owner_name.to_string(), param_name.to_string())) } +/// Returns whether a checker-patched magic-method parameter must keep its real ABI type. +fn magic_method_param_keeps_eir_contract( + owner_name: &str, + param_index: usize, + php_type: &PhpType, +) -> bool { + let Some((_, method_name)) = owner_name.rsplit_once("::") else { + return false; + }; + let method_key = php_symbol_key(method_name); + match method_key.as_str() { + "__get" | "__isset" | "__unset" => { + param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str) + } + "__set" => { + param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str) + } + "__call" | "__callstatic" => { + (param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str)) + || (param_index == 1 && matches!(php_type.codegen_repr(), PhpType::Array(_))) + } + _ => false, + } +} + /// Widens inferred container return elements that may be built from dynamic params. fn dynamic_param_container_return_type(return_type: &PhpType) -> PhpType { match return_type.codegen_repr() { diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index dd709fb04a..4a8e6e75c5 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -352,6 +352,7 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { for function in all_lowered_functions(module) { if function_contains_eval_state(function) { features.eval_scope = true; + features.eval_bridge = true; } for (inst_index, inst) in function.instructions.iter().enumerate() { match inst.op { diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 1f8779a3c4..076ee7d0cc 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -46,6 +46,10 @@ const STATIC_ONLY_REGISTRY_BUILTINS: &[&str] = &[ "array_walk_recursive", "serialize", "unserialize", + "zval_free", + "zval_pack", + "zval_type", + "zval_unpack", ]; /// Eval supports these PHP optional parameters before the static backend does. diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 4dc3ca43fe..34f2dc634b 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -16237,9 +16237,9 @@ try { ); } -/// Verifies eval-declared readonly classes mirror instance/static property rules. +/// Verifies eval-declared readonly classes initialize typed properties and can inherit readonly parents. #[test] -fn test_eval_declared_readonly_class_rules() { +fn test_eval_declared_readonly_class_initializes_and_inherits() { let out = compile_and_run_capture( r#"id() . ":" . $child->id();'); out.stdout, out.stderr ); assert_eq!(out.stdout, "7:9"); +} +/// Verifies eval-declared readonly classes leave static properties mutable. +#[test] +fn test_eval_declared_readonly_class_static_properties_remain_mutable() { let static_out = compile_and_run_capture( r#"dynamic = 8;'); magic.stdout, magic.stderr ); assert_eq!(magic.stdout, "dynamic:8"); +} +/// Verifies eval-declared readonly classes cannot extend non-readonly parents. +#[test] +fn test_eval_declared_readonly_class_rejects_non_readonly_parent() { let parent_err = compile_and_run_expect_failure( r#" Date: Thu, 9 Jul 2026 17:59:29 +0200 Subject: [PATCH 1150/1208] fix(optimizer): invalidate eval and by-ref variadics --- src/optimize/propagate/invalidation.rs | 23 ++++- src/optimize/propagate/signatures.rs | 32 +++++-- src/optimize/tests/propagate/signatures.rs | 24 +++++ src/optimize/tests/propagate/straight_line.rs | 90 +++++++++++++++++++ 4 files changed, 158 insertions(+), 11 deletions(-) diff --git a/src/optimize/propagate/invalidation.rs b/src/optimize/propagate/invalidation.rs index 1d40eaa54e..f645fc4ed6 100644 --- a/src/optimize/propagate/invalidation.rs +++ b/src/optimize/propagate/invalidation.rs @@ -16,9 +16,11 @@ //! - By-ref arguments to user-defined callees are also marked volatile: the //! callee may retain the reference (e.g. in a by-ref closure capture) and //! write it during any later call. Builtins never retain their arguments. -//! - `Invalidation::All` remains for genuinely unknowable writes: `include`, -//! `yield`, spreads into by-ref callees, and global-writing (or unknown) -//! callees invoked from top-level scope. +//! - `Invalidation::All` remains for genuinely unknowable writes: `eval`, +//! `include`, `yield`, spreads into by-ref callees, and global-writing (or +//! unknown) callees invoked from top-level scope. + +use crate::names::php_symbol_key; use super::*; @@ -212,6 +214,11 @@ pub(crate) fn expr_invalidation(expr: &Expr) -> Invalidation { acc.union(unset_target_invalidation(arg)) }) } + ExprKind::FunctionCall { name, .. } + if php_symbol_key(name.as_str().trim_start_matches('\\')) == "eval" => + { + Invalidation::All + } // `ptr($x)` (elephc pointer extension) takes the address of a local: // from this point on, `ptr_set`/`ptr_write*` through any alias of the // pointer rewrites the variable outside the PHP reference model, so @@ -396,7 +403,7 @@ fn call_args_invalidation( if spread_seen && has_by_ref { return Invalidation::All; } - if sig.get(position).is_some_and(|(_, is_ref)| *is_ref) { + if positional_param_is_by_ref(sig, position) { expose_argument_root(arg, retain, &mut inv); } position += 1; @@ -406,6 +413,14 @@ fn call_args_invalidation( inv } +/// Returns whether a positional argument lands on a by-ref parameter. +fn positional_param_is_by_ref(sig: &[(String, bool)], position: usize) -> bool { + if let Some((_, is_ref)) = sig.get(position) { + return *is_ref; + } + sig.last().is_some_and(|(_, is_ref)| *is_ref) +} + /// Records an argument's lvalue root as writable (and volatile when the callee /// can retain the reference). Arguments without a local root — literals, /// property accesses, call results — expose no caller local. diff --git a/src/optimize/propagate/signatures.rs b/src/optimize/propagate/signatures.rs index 862d69e8ae..1f10ab5f13 100644 --- a/src/optimize/propagate/signatures.rs +++ b/src/optimize/propagate/signatures.rs @@ -127,18 +127,28 @@ fn union_params(union: &mut Vec<(String, bool)>, params: &[(String, bool)]) { } /// Converts an AST parameter tuple list into `(param name, is_by_ref)` pairs. -fn params_signature( +fn params_signature_with_variadic( params: &[(String, Option, Option, bool)], + variadic: Option<&str>, + variadic_by_ref: bool, ) -> Vec<(String, bool)> { - params + let mut signature: Vec<_> = params .iter() .map(|(name, _, _, is_ref)| (name.clone(), *is_ref)) - .collect() + .collect(); + if let Some(variadic) = variadic { + signature.push((variadic.to_string(), variadic_by_ref)); + } + signature } /// Records one method declaration into the by-name union and the ctor flag. fn collect_method(method: &ClassMethod, sigs: &mut ByRefSignatures) { - let signature = params_signature(&method.params); + let signature = params_signature_with_variadic( + &method.params, + method.variadic.as_deref(), + method.variadic_by_ref, + ); if method.name.eq_ignore_ascii_case("__construct") && signature.iter().any(|(_, by_ref)| *by_ref) { @@ -173,9 +183,17 @@ fn collect_properties(properties: &[ClassProperty], sigs: &mut ByRefSignatures) fn collect_from_block(body: &[Stmt], sigs: &mut ByRefSignatures) { for stmt in body { match &stmt.kind { - StmtKind::FunctionDecl { name, params, .. } => { - sigs.functions - .insert(name.clone(), params_signature(params)); + StmtKind::FunctionDecl { + name, + params, + variadic, + variadic_by_ref, + .. + } => { + sigs.functions.insert( + name.clone(), + params_signature_with_variadic(params, variadic.as_deref(), *variadic_by_ref), + ); } StmtKind::ClassDecl { properties, diff --git a/src/optimize/tests/propagate/signatures.rs b/src/optimize/tests/propagate/signatures.rs index 488988c54f..b4a7609bbe 100644 --- a/src/optimize/tests/propagate/signatures.rs +++ b/src/optimize/tests/propagate/signatures.rs @@ -118,6 +118,30 @@ fn test_user_function_by_ref_params_collected() { }); } +/// A user function's by-ref variadic parameter is exposed as the trailing +/// by-ref slot used by targeted call invalidation. +#[test] +fn test_user_function_by_ref_variadic_param_collected() { + let mut function = function_with_params("f", Vec::new()); + if let StmtKind::FunctionDecl { + variadic, + variadic_by_ref, + .. + } = &mut function.kind + { + *variadic = Some("items".to_string()); + *variadic_by_ref = true; + } + let sigs = collect_by_ref_signatures(&[function]); + + with_by_ref_signatures(sigs, || { + assert_eq!( + function_by_ref_params("f"), + Some(vec![("items".to_string(), true)]) + ); + }); +} + /// Same-named methods union their by-ref positions across classes and traits /// (dynamic dispatch cannot tell them apart). #[test] diff --git a/src/optimize/tests/propagate/straight_line.rs b/src/optimize/tests/propagate/straight_line.rs index 7a52faf9ec..fd120a9364 100644 --- a/src/optimize/tests/propagate/straight_line.rs +++ b/src/optimize/tests/propagate/straight_line.rs @@ -116,6 +116,96 @@ fn test_propagate_constants_stops_at_eval_barrier() { ); } +/// Tests that `eval` invalidates by-value closure capture facts inside the +/// closure body because the evaluated fragment can update the closure's local copy. +#[test] +fn test_propagate_constants_stops_at_eval_barrier_in_closure_capture() { + let closure = Expr::new( + ExprKind::Closure { + params: Vec::new(), + variadic: None, + variadic_by_ref: false, + variadic_type: None, + return_type: None, + body: vec![ + Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::FunctionCall { + name: Name::from("eval"), + args: vec![Expr::string_lit("$x = $x + 4;")], + }, + Span::dummy(), + )), + Span::dummy(), + ), + Stmt::new(StmtKind::Return(Some(Expr::var("x"))), Span::dummy()), + ], + is_arrow: false, + is_static: false, + by_ref_return: false, + captures: vec!["x".to_string()], + capture_refs: Vec::new(), + }, + Span::dummy(), + ); + let program = vec![Stmt::assign("x", Expr::int_lit(1)), Stmt::assign("fn", closure)]; + + let propagated = propagate_constants(program); + + let StmtKind::Assign { value, .. } = &propagated[1].kind else { + panic!("expected propagated closure assignment"); + }; + let ExprKind::Closure { body, .. } = &value.kind else { + panic!("expected propagated closure expression"); + }; + assert_eq!( + body[1], + Stmt::new(StmtKind::Return(Some(Expr::var("x"))), Span::dummy()) + ); +} + +/// Tests that a call to a user function with `&...$items` invalidates every +/// positional local passed into the variadic tail. +#[test] +fn test_propagate_constants_invalidates_by_ref_variadic_function_args() { + let program = vec![ + Stmt::new( + StmtKind::FunctionDecl { + name: "f".to_string(), + params: Vec::new(), + param_attributes: Vec::new(), + variadic: Some("items".to_string()), + variadic_by_ref: true, + variadic_type: None, + return_type: None, + by_ref_return: false, + body: Vec::new(), + }, + Span::dummy(), + ), + Stmt::assign("a", Expr::int_lit(1)), + Stmt::assign("b", Expr::int_lit(2)), + Stmt::new( + StmtKind::ExprStmt(Expr::new( + ExprKind::FunctionCall { + name: Name::from("f"), + args: vec![Expr::var("a"), Expr::var("b")], + }, + Span::dummy(), + )), + Span::dummy(), + ), + Stmt::echo(Expr::binop(Expr::var("a"), BinOp::Add, Expr::var("b"))), + ]; + + let propagated = propagate_constants(program); + + assert_eq!( + propagated[4], + Stmt::echo(Expr::binop(Expr::var("a"), BinOp::Add, Expr::var("b"))) + ); +} + /// Tests that when both branches of a ternary expression are the same constant, /// the resulting assignment is treated as a uniform constant. `base = flag ? 2 : 2` /// always yields `2`, so `base ** 3` folds to `8.0`. From 2934f7b426a666b6836f729dcd1488591ae2c617 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 9 Jul 2026 17:59:36 +0200 Subject: [PATCH 1151/1208] fix(callables): handle boxed sort callbacks --- src/codegen/lower_inst/builtins/arrays.rs | 25 +++++++-------- src/codegen/runtime_callable_invoker.rs | 39 +++++++++++++++++++++-- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/codegen/lower_inst/builtins/arrays.rs b/src/codegen/lower_inst/builtins/arrays.rs index b56e7aa1fa..1c771982c5 100644 --- a/src/codegen/lower_inst/builtins/arrays.rs +++ b/src/codegen/lower_inst/builtins/arrays.rs @@ -3523,7 +3523,6 @@ fn require_static_method_callback_param_types( sig: &crate::types::FunctionSig, visible_arg_types: &[PhpType], ) -> Result<()> { - let mut needs_mixed_boxes = false; for ((_, param_ty), visible_ty) in sig.params.iter().zip(visible_arg_types.iter()) { let param_ty = param_ty.codegen_repr(); let visible_ty = visible_ty.codegen_repr(); @@ -3531,7 +3530,6 @@ fn require_static_method_callback_param_types( continue; } if param_ty == PhpType::Mixed { - needs_mixed_boxes = true; continue; } if matches!((¶m_ty, &visible_ty), (PhpType::Int | PhpType::Bool, PhpType::Int | PhpType::Bool)) { @@ -3545,18 +3543,6 @@ fn require_static_method_callback_param_types( owner, callback_name, param_ty, visible_ty ))); } - if needs_mixed_boxes - && matches!( - sig.return_type.codegen_repr(), - PhpType::Mixed | PhpType::Union(_) | PhpType::TaggedScalar - ) - { - return Err(CodegenIrError::unsupported(format!( - "{} '{}' with boxed callback args and alias-sensitive return type", - owner, - callback_name - ))); - } Ok(()) } @@ -3814,11 +3800,22 @@ fn cleanup_callback_boxed_args( if !callback_wrapper_has_boxed_args(frame) { return; } + if callback_return_may_alias_boxed_args(return_ty) { + return; + } save_callback_return_value(ctx, frame, return_ty); release_callback_boxed_args(ctx, frame); restore_callback_return_value(ctx, frame, return_ty); } +/// Returns true when the callback result may be one of the boxed wrapper arguments. +fn callback_return_may_alias_boxed_args(return_ty: &PhpType) -> bool { + matches!( + return_ty.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) | PhpType::TaggedScalar + ) +} + /// Emits a local wrapper that prepends the hidden static called-class id. fn emit_static_method_callback_wrapper( ctx: &mut FunctionContext<'_>, diff --git a/src/codegen/runtime_callable_invoker.rs b/src/codegen/runtime_callable_invoker.rs index fac72f1b95..a1c6f43cfe 100644 --- a/src/codegen/runtime_callable_invoker.rs +++ b/src/codegen/runtime_callable_invoker.rs @@ -576,7 +576,13 @@ fn emit_loaded_indexed_array_callback_call( abi::emit_jump(emitter, &loop_label); emitter.label(&loop_done_label); emitter.label(&done_label); - arg_types.push(PhpType::Array(Box::new(variadic_elem_ty))); + let variadic_ty = PhpType::Array(Box::new(variadic_elem_ty)); + if variadic_param_is_by_ref(sig) { + wrap_pushed_value_in_ref_cell(emitter, &variadic_ty); + arg_types.push(PhpType::Int); + } else { + arg_types.push(variadic_ty); + } } push_descriptor_captures_as_hidden_args(captures, emitter, &mut arg_types); @@ -681,7 +687,12 @@ fn emit_loaded_assoc_array_callback_call( ctx, data, ); - arg_types.push(variadic_ty); + if variadic_param_is_by_ref(sig) { + wrap_pushed_value_in_ref_cell(emitter, &variadic_ty); + arg_types.push(PhpType::Int); + } else { + arg_types.push(variadic_ty); + } } push_descriptor_captures_as_hidden_args(captures, emitter, &mut arg_types); @@ -706,6 +717,16 @@ fn callback_arg_target_ty<'a>( } } +/// Returns whether the visible variadic parameter is declared by-reference. +fn variadic_param_is_by_ref(sig: &FunctionSig) -> bool { + sig.variadic.is_some() + && sig + .ref_params + .get(sig.params.len().saturating_sub(1)) + .copied() + .unwrap_or(false) +} + /// Returns the declared target PHP type for a parameter. fn declared_target_ty<'a>(sig: Option<&'a FunctionSig>, param_idx: usize) -> Option<&'a PhpType> { sig.and_then(|sig| { @@ -1173,6 +1194,20 @@ fn push_current_result_ref_arg_address( PhpType::Int } +/// Wraps the value currently on top of the invoker stack in a heap reference cell. +fn wrap_pushed_value_in_ref_cell(emitter: &mut Emitter, val_ty: &PhpType) { + abi::emit_load_int_immediate(emitter, abi::int_result_reg(emitter), 16); + abi::emit_call_label(emitter, "__rt_heap_alloc"); + let cell_reg = abi::symbol_scratch_reg(emitter); + emitter.instruction(&format!( + "mov {}, {}", + cell_reg, + abi::int_result_reg(emitter) + )); + store_pushed_value_to_ref_cell(emitter, cell_reg, val_ty); + abi::emit_push_reg(emitter, cell_reg); +} + /// Stores a just-pushed value into a heap reference cell. fn store_pushed_value_to_ref_cell(emitter: &mut Emitter, cell_reg: &str, val_ty: &PhpType) { let temp_reg = abi::temp_int_reg(emitter.target); From 6b0947a4dc724a8149411a1791c996197e43f8ce Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Thu, 9 Jul 2026 17:59:42 +0200 Subject: [PATCH 1152/1208] fix(reflection): repair function metadata ownership --- src/builtins/registry.rs | 1 + src/codegen/lower_inst/iterators.rs | 12 +++ src/types/checker/builtin_types/reflection.rs | 80 ++++++------------- .../checker/inference/objects/constructors.rs | 35 -------- .../checker/inference/objects/methods.rs | 10 +++ 5 files changed, 48 insertions(+), 90 deletions(-) diff --git a/src/builtins/registry.rs b/src/builtins/registry.rs index 3c5a342388..17bcff99c4 100644 --- a/src/builtins/registry.rs +++ b/src/builtins/registry.rs @@ -196,6 +196,7 @@ pub fn first_class_callable_sig(name: &str) -> Option { let mut fcc_sig = callable_wrapper_sig(&sig); refine_first_class_callable_sig(name, &mut fcc_sig); fcc_sig.declared_return = true; + fcc_sig.declared_params = vec![true; fcc_sig.params.len()]; Some(fcc_sig) } diff --git a/src/codegen/lower_inst/iterators.rs b/src/codegen/lower_inst/iterators.rs index 3b4473d8e0..9196150bdc 100644 --- a/src/codegen/lower_inst/iterators.rs +++ b/src/codegen/lower_inst/iterators.rs @@ -194,6 +194,7 @@ pub(super) fn lower_iter_current_value( Arch::AArch64 => load_current_array_value_aarch64(ctx, offset, &elem)?, Arch::X86_64 => load_current_array_value_x86_64(ctx, offset, &elem)?, } + retain_current_indexed_value_if_unboxed(&mut ctx.emitter, &elem, &result_ty); box_current_indexed_value_if_needed(ctx, &elem, &result_ty)?; } IteratorSourceKind::Hash => match ctx.emitter.target.arch { @@ -223,6 +224,17 @@ fn iter_current_result_type(ctx: &FunctionContext<'_>, inst: &Instruction) -> Re Ok(ctx.value_php_type(result)?.codegen_repr()) } +/// Retains concrete indexed-array foreach values that are returned without Mixed boxing. +fn retain_current_indexed_value_if_unboxed( + emitter: &mut crate::codegen::emit::Emitter, + elem: &PhpType, + result_ty: &PhpType, +) { + if elem.codegen_repr() == result_ty.codegen_repr() { + abi::emit_incref_if_refcounted(emitter, &elem.codegen_repr()); + } +} + /// Boxes an indexed iterator element only when the EIR result expects `Mixed`. fn box_current_indexed_value_if_needed( ctx: &mut FunctionContext<'_>, diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index aedb890ea8..64daae58b9 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -797,57 +797,19 @@ fn builtin_reflection_function_constructor_method() -> ClassMethod { /// attribute, and parameter slots. Codegen populates these from the reflected /// function's signature and attribute metadata. fn builtin_reflection_function() -> FlattenedClass { - FlattenedClass { - name: "ReflectionFunction".to_string(), - span: dummy(), - extends: None, - implements: Vec::new(), - is_abstract: false, - is_final: true, - is_readonly_class: false, - properties: vec![ - builtin_property("__name", Visibility::Private, Some(TypeExpr::Str), empty_string()), - builtin_property( - "__short_name", - Visibility::Private, - Some(TypeExpr::Str), - empty_string(), - ), - builtin_property( - "__parameters", - Visibility::Private, - Some(object_array_type("ReflectionParameter")), - empty_array(), - ), - builtin_property( - "__required_parameter_count", - Visibility::Private, - Some(TypeExpr::Int), - int_lit(0), - ), - builtin_property("__attrs", Visibility::Private, Some(array_type()), empty_array()), - ], - methods: vec![ - builtin_reflection_function_constructor_method(), - builtin_reflection_slot_getter("getName", "__name", TypeExpr::Str), - builtin_reflection_slot_getter("getShortName", "__short_name", TypeExpr::Str), - builtin_reflection_parameter_count_method(), - builtin_reflection_class_int_method( - "getNumberOfRequiredParameters", - "__required_parameter_count", - ), - builtin_reflection_class_array_method( - "getParameters", - "__parameters", - object_array_type("ReflectionParameter"), - ), - builtin_reflection_owner_get_attributes_method(), - ], - attributes: Vec::new(), - constants: Vec::new(), - used_traits: Vec::new(), - trait_aliases: Vec::new(), + let mut class = builtin_reflection_owner_class( + "ReflectionFunction", + true, + vec![("function", Some(TypeExpr::Str), None, false)], + ); + if let Some(constructor) = class + .methods + .iter_mut() + .find(|method| method.name == "__construct") + { + *constructor = builtin_reflection_function_constructor_method(); } + class } /// Builds the `ReflectionParameter` shell with private name/position/optional/ @@ -1390,7 +1352,8 @@ fn builtin_reflection_union_type() -> FlattenedClass { "__types", object_array_type("ReflectionNamedType"), ), - builtin_reflection_composite_type_to_string_method("|", true), + builtin_reflection_composite_type_string_method("__toString", "|", true), + builtin_reflection_composite_type_string_method("getName", "|", true), builtin_reflection_class_bool_method("allowsNull", "__allows_null"), ], attributes: Vec::new(), @@ -1432,7 +1395,8 @@ fn builtin_reflection_intersection_type() -> FlattenedClass { "__types", object_array_type("ReflectionNamedType"), ), - builtin_reflection_composite_type_to_string_method("&", false), + builtin_reflection_composite_type_string_method("__toString", "&", false), + builtin_reflection_composite_type_string_method("getName", "&", false), builtin_reflection_class_bool_method("allowsNull", "__allows_null"), ], attributes: Vec::new(), @@ -1495,8 +1459,9 @@ fn builtin_reflection_named_type_to_string_method() -> ClassMethod { } } -/// Builds `ReflectionUnionType::__toString()` or `ReflectionIntersectionType::__toString()`. -fn builtin_reflection_composite_type_to_string_method( +/// Builds a string-rendering method for `ReflectionUnionType` and `ReflectionIntersectionType`. +fn builtin_reflection_composite_type_string_method( + method_name: &str, separator: &'static str, append_null: bool, ) -> ClassMethod { @@ -1550,7 +1515,7 @@ fn builtin_reflection_composite_type_to_string_method( dummy_span, )); ClassMethod { - name: "__toString".to_string(), + name: method_name.to_string(), visibility: Visibility::Public, is_static: false, is_abstract: false, @@ -5137,6 +5102,11 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invokeArgs")) { sig.return_type = PhpType::Mixed; + sig.params = vec![("args".to_string(), PhpType::Array(Box::new(PhpType::Mixed)))]; + sig.param_type_exprs = vec![Some(array_type())]; + sig.defaults = vec![None]; + sig.ref_params = vec![false]; + sig.declared_params = vec![true]; } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("createFromMethodName")) diff --git a/src/types/checker/inference/objects/constructors.rs b/src/types/checker/inference/objects/constructors.rs index 011227bf15..d8b93f32da 100644 --- a/src/types/checker/inference/objects/constructors.rs +++ b/src/types/checker/inference/objects/constructors.rs @@ -256,15 +256,6 @@ impl Checker { &format!("Constructor '{}::__construct'", class_name), )?; - if class_name == "ReflectionFunction" { - let function_name = self.reflection_string_literal_arg( - class_name, - "function name", - normalized_args.first(), - env, - )?; - return self.validate_reflection_function_target(&function_name, expr); - } if class_name == "ReflectionParameter" { return self.validate_reflection_parameter_constructor(&normalized_args, expr, env); } @@ -778,32 +769,6 @@ impl Checker { } } - /// Validates that `new ReflectionFunction($name)` targets a known - /// user-defined function (matched case-insensitively, as PHP function names - /// are). Builtin functions are not yet reflectable. - fn validate_reflection_function_target( - &self, - function_name: &str, - expr: &Expr, - ) -> Result<(), CompileError> { - let key = crate::names::php_symbol_key(function_name.trim_start_matches('\\')); - let exists = self - .fn_decls - .keys() - .chain(self.functions.keys()) - .any(|name| crate::names::php_symbol_key(name.trim_start_matches('\\')) == key); - if exists { - Ok(()) - } else { - Err(CompileError::new( - expr.span, - &format!( - "ReflectionFunction::__construct(): undefined function '{}'", - function_name - ), - )) - } - } /// Resolves a static receiver to a class name for reflection class constant. /// /// `Named` returns the canonical name. `Self_`/`Static` require a class diff --git a/src/types/checker/inference/objects/methods.rs b/src/types/checker/inference/objects/methods.rs index 155ce20a80..dc07723306 100644 --- a/src/types/checker/inference/objects/methods.rs +++ b/src/types/checker/inference/objects/methods.rs @@ -485,6 +485,10 @@ impl Checker { for (i, arg_ty) in arg_types.iter().enumerate() { if i < regular_param_count && declared_flags.get(i).copied().unwrap_or(false) + && !Self::method_array_param_keeps_generic_shape( + &impl_class_name, + &method_key, + ) && Self::is_generic_array_hint(&sig.params[i].1) && matches!(arg_ty, PhpType::Array(_) | PhpType::AssocArray { .. }) { @@ -536,6 +540,12 @@ impl Checker { Ok(PhpType::Int) } + /// Returns true for builtin method array params whose accepted shape must remain broad. + fn method_array_param_keeps_generic_shape(class_name: &str, method_key: &str) -> bool { + matches!(class_name, "ReflectionFunction" | "ReflectionMethod") + && method_key == php_symbol_key("invokeArgs") + } + /// Builds synthetic `__call` arguments: `[method_name, [args...]]`. /// /// Constructs a `StringLiteral` for the method name and an `ArrayLiteral` From 88db3ac97fd453d940bc554a643aa0905a37d894 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 12:45:52 +0200 Subject: [PATCH 1153/1208] fix(codegen): guard null gap cells in mixed array ref-cell reads Extended indexed writes leave NULL gap cells; the invoker ref-cell tag check dereferenced them and crashed on gap reads. --- src/codegen/lower_inst/arrays.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/codegen/lower_inst/arrays.rs b/src/codegen/lower_inst/arrays.rs index bfe2414ea3..d649f0c1e1 100644 --- a/src/codegen/lower_inst/arrays.rs +++ b/src/codegen/lower_inst/arrays.rs @@ -890,6 +890,17 @@ fn emit_mixed_array_get_deref_invoker_ref_cell( let ref_label = ctx.next_label("array_get_mixed_ref_cell"); let done_label = ctx.next_label("array_get_mixed_done"); let tag_reg = abi::secondary_scratch_reg(ctx.emitter); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter + .instruction(&format!("cbz {}, {}", mixed_reg, done_label)); // null gap cells read as PHP null and carry no tag word to inspect + } + Arch::X86_64 => { + ctx.emitter + .instruction(&format!("test {}, {}", mixed_reg, mixed_reg)); // null gap cells read as PHP null and carry no tag word to inspect + ctx.emitter.instruction(&format!("jz {}", done_label)); // skip marker detection for null gap cells + } + } abi::emit_load_from_address(ctx.emitter, tag_reg, mixed_reg, 0); emit_branch_if_invoker_ref_cell_tag(ctx, tag_reg, &ref_label); abi::emit_incref_if_refcounted(ctx.emitter, &PhpType::Mixed); From bb6049b3b7c741dbeaa70ecd519000612e6070b4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 12:46:06 +0200 Subject: [PATCH 1154/1208] fix(reflection): support isBuiltin on composite reflection types ReflectionParameter::getType() returns a union including ReflectionUnionType/ReflectionIntersectionType; both now expose isBuiltin backed by an __is_builtin slot (AND of member flags). --- src/codegen/lower_inst/objects/reflection.rs | 12 +++++++++ src/types/checker/builtin_types/reflection.rs | 26 ++++++++++++++++--- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 2cc187a947..a3f1d655ee 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -6851,6 +6851,12 @@ fn emit_reflection_union_type_object( "__allows_null", type_metadata.allows_null, )?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionUnionType", + "__is_builtin", + type_metadata.types.iter().all(|member| member.is_builtin), + )?; Ok(()) } @@ -6913,6 +6919,12 @@ fn emit_reflection_intersection_type_object( )?; emit_reflection_intersection_type_types_property(ctx, &type_metadata.types)?; emit_reflection_owner_bool_property(ctx, "ReflectionIntersectionType", "__allows_null", false)?; + emit_reflection_owner_bool_property( + ctx, + "ReflectionIntersectionType", + "__is_builtin", + type_metadata.types.iter().all(|member| member.is_builtin), + )?; Ok(()) } diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 64daae58b9..3fa7d95346 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -1344,6 +1344,12 @@ fn builtin_reflection_union_type() -> FlattenedClass { Some(TypeExpr::Bool), bool_lit(false), ), + builtin_property( + "__is_builtin", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), ], methods: vec![ builtin_reflection_private_constructor_method(), @@ -1355,6 +1361,7 @@ fn builtin_reflection_union_type() -> FlattenedClass { builtin_reflection_composite_type_string_method("__toString", "|", true), builtin_reflection_composite_type_string_method("getName", "|", true), builtin_reflection_class_bool_method("allowsNull", "__allows_null"), + builtin_reflection_class_bool_method("isBuiltin", "__is_builtin"), ], attributes: Vec::new(), constants: Vec::new(), @@ -1387,6 +1394,12 @@ fn builtin_reflection_intersection_type() -> FlattenedClass { Some(TypeExpr::Bool), bool_lit(false), ), + builtin_property( + "__is_builtin", + Visibility::Private, + Some(TypeExpr::Bool), + bool_lit(false), + ), ], methods: vec![ builtin_reflection_private_constructor_method(), @@ -1398,6 +1411,7 @@ fn builtin_reflection_intersection_type() -> FlattenedClass { builtin_reflection_composite_type_string_method("__toString", "&", false), builtin_reflection_composite_type_string_method("getName", "&", false), builtin_reflection_class_bool_method("allowsNull", "__allows_null"), + builtin_reflection_class_bool_method("isBuiltin", "__is_builtin"), ], attributes: Vec::new(), constants: Vec::new(), @@ -5201,8 +5215,10 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionNamedType".to_string(), ))); } - if let Some(sig) = class_info.methods.get_mut("allowsnull") { - sig.return_type = PhpType::Bool; + for method_name in ["allowsnull", "isbuiltin"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { sig.return_type = PhpType::Str; @@ -5214,8 +5230,10 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { "ReflectionNamedType".to_string(), ))); } - if let Some(sig) = class_info.methods.get_mut("allowsnull") { - sig.return_type = PhpType::Bool; + for method_name in ["allowsnull", "isbuiltin"] { + if let Some(sig) = class_info.methods.get_mut(method_name) { + sig.return_type = PhpType::Bool; + } } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("__toString")) { sig.return_type = PhpType::Str; From 8ec205fbd0ae61a73d9576720fb42be37ada432d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 12:46:06 +0200 Subject: [PATCH 1155/1208] fix(checker): enforce eval() arity in the builtin fast-path The fast-path returned before registry arity checks and the call-args planner tolerates zero-arg calls, so eval() type-checked as Ok. --- src/types/checker/builtins/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/types/checker/builtins/mod.rs b/src/types/checker/builtins/mod.rs index 6b3e15ff68..6249bc84ca 100644 --- a/src/types/checker/builtins/mod.rs +++ b/src/types/checker/builtins/mod.rs @@ -84,9 +84,13 @@ impl Checker { }; if name == "eval" { - if let Some(arg) = args.first() { - self.infer_type(arg, env)?; + // eval is not registry-backed, and argument normalization tolerates + // zero-arg calls (trailing defaults are trimmed), so arity must be + // enforced here before the fast-path return. + if args.len() != 1 { + return Err(CompileError::new(span, "eval() takes exactly 1 argument")); } + self.infer_type(&args[0], env)?; return Ok(Some(PhpType::Mixed)); } From c1d376faf29ab7a39043314f2d9c98380b37a937 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 12:46:16 +0200 Subject: [PATCH 1156/1208] feat(magician): align builtin parity with main - implement mb_ereg_match (start-anchored PCRE2 match, i option) - accept htmlspecialchars/htmlentities flags and encoding arguments (evaluated, no effect, matching the static runtime) - drop var_dump from the variadic extension allowlist: the static signature is variadic, shapes now match exactly --- .../src/interpreter/builtins/hooks/direct.rs | 3 + .../src/interpreter/builtins/hooks/values.rs | 25 +++-- .../builtins/regex/mb_ereg_match.rs | 98 +++++++++++++++++++ .../src/interpreter/builtins/regex/mod.rs | 2 + .../builtins/string/htmlentities.rs | 7 +- .../builtins/string/htmlspecialchars.rs | 19 +++- .../tests/builtins_strings_encoding.rs | 24 +++++ docs/php/eval.md | 2 +- tests/builtin_parity_tests.rs | 6 +- 9 files changed, 173 insertions(+), 13 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs index 6e2af92644..d718fea12e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/direct.rs @@ -204,6 +204,8 @@ pub(in crate::interpreter) enum EvalDirectHook { Round, /// Dispatches `range(...)`. Range, + /// Dispatches `mb_ereg_match(...)`. + MbEregMatch, /// Dispatches `preg_match(...)`. PregMatch, /// Dispatches `preg_match_all(...)`. @@ -446,6 +448,7 @@ impl EvalDirectHook { Self::Rand => eval_builtin_rand(args, context, scope, values), Self::RandomInt => eval_builtin_random_int(args, context, scope, values), Self::Round => eval_builtin_round(args, context, scope, values), + Self::MbEregMatch => eval_builtin_mb_ereg_match(args, context, scope, values), Self::PregMatch => eval_builtin_preg_match(args, context, scope, values), Self::PregMatchAll => eval_builtin_preg_match_all(args, context, scope, values), Self::PregReplace => eval_builtin_preg_replace(args, context, scope, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs index a9a4eefe9a..ad08e58ffe 100644 --- a/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs +++ b/crates/elephc-magician/src/interpreter/builtins/hooks/values.rs @@ -207,6 +207,8 @@ pub(in crate::interpreter) enum EvalValuesHook { Round, /// Dispatches `range(...)`. Range, + /// Dispatches `mb_ereg_match(...)`. + MbEregMatch, /// Dispatches `preg_match(...)`. PregMatch, /// Dispatches `preg_match_all(...)`. @@ -433,12 +435,22 @@ impl EvalValuesHook { _ => Err(EvalStatus::RuntimeFatal), }, Self::Hex2Bin => one_arg(evaluated_args, values, eval_hex2bin_result), - Self::HtmlEntity => one_arg(evaluated_args, values, |value, values| match name { - "html_entity_decode" => eval_html_entity_decode_result(value, values), - "htmlentities" => eval_htmlentities_result(value, values), - "htmlspecialchars" => eval_htmlspecialchars_result(value, values), - _ => Err(EvalStatus::RuntimeFatal), - }), + Self::HtmlEntity => { + // htmlspecialchars/htmlentities accept optional flags/encoding args; + // like the static runtime they are accepted without effect (ENT_QUOTES). + let value = match (name, evaluated_args) { + (_, [value]) => *value, + ("htmlspecialchars" | "htmlentities", [value, _flags]) => *value, + ("htmlspecialchars" | "htmlentities", [value, _flags, _encoding]) => *value, + _ => return Err(EvalStatus::RuntimeFatal), + }; + match name { + "html_entity_decode" => eval_html_entity_decode_result(value, values), + "htmlentities" => eval_htmlentities_result(value, values), + "htmlspecialchars" => eval_htmlspecialchars_result(value, values), + _ => Err(EvalStatus::RuntimeFatal), + } + } Self::Intdiv => two_args(evaluated_args, values, eval_intdiv_result), Self::JsonDecode => eval_json_decode_values_result(evaluated_args, context, values), Self::JsonEncode => eval_json_encode_values_result(evaluated_args, context, values), @@ -486,6 +498,7 @@ impl EvalValuesHook { [value, precision] => eval_round_result(*value, Some(*precision), values), _ => Err(EvalStatus::RuntimeFatal), }, + Self::MbEregMatch => eval_mb_ereg_match_values_result(evaluated_args, values), Self::PregMatch => eval_preg_match_values_result(evaluated_args, values), Self::PregMatchAll => eval_preg_match_all_values_result(evaluated_args, values), Self::PregReplace => eval_preg_replace_values_result(evaluated_args, values), diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs new file mode 100644 index 0000000000..6dff7dd1ef --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs @@ -0,0 +1,98 @@ +//! Purpose: +//! Eval registry entry and implementation for `mb_ereg_match`. +//! +//! Called from: +//! - `crate::interpreter::builtins::hooks` direct and by-value dispatch. +//! +//! Key details: +//! - `mb_ereg_match($pattern, $string, $options = null)` is a start-anchored match: +//! the pattern is a raw mbregex body (no preg delimiters), and a successful match +//! counts only when it begins at byte offset 0 — mirroring the AOT runtime helper, +//! which enforces `rm_so == 0` on PCRE2's leftmost match. +//! - The `$options` string maps `i` to case-insensitive compilation; other option +//! bytes are accepted without additional runtime effect, matching the AOT helper. + +use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; +use super::*; + +eval_builtin! { + name: "mb_ereg_match", + area: Regex, + params: [pattern, subject, options = EvalBuiltinDefaultValue::Null], + direct: MbEregMatch, + values: MbEregMatch, +} + +/// Evaluates PHP `mb_ereg_match()` over eval expressions. +pub(in crate::interpreter) fn eval_builtin_mb_ereg_match( + args: &[EvalExpr], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match args { + [pattern, subject] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + eval_mb_ereg_match_result(pattern, subject, None, values) + } + [pattern, subject, options] => { + let pattern = eval_expr(pattern, context, scope, values)?; + let subject = eval_expr(subject, context, scope, values)?; + let options = eval_expr(options, context, scope, values)?; + eval_mb_ereg_match_result(pattern, subject, Some(options), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Dispatches by-value `mb_ereg_match()` calls after argument binding. +pub(in crate::interpreter) fn eval_mb_ereg_match_values_result( + evaluated_args: &[RuntimeCellHandle], + values: &mut impl RuntimeValueOps, +) -> Result { + match evaluated_args { + [pattern, subject] => eval_mb_ereg_match_result(*pattern, *subject, None, values), + [pattern, subject, options] => { + eval_mb_ereg_match_result(*pattern, *subject, Some(*options), values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns whether the raw mbregex pattern matches the subject start. +pub(in crate::interpreter) fn eval_mb_ereg_match_result( + pattern: RuntimeCellHandle, + subject: RuntimeCellHandle, + options: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let modifiers = eval_mb_ereg_match_modifiers(options, values)?; + let pattern = values.string_bytes(pattern)?; + let regex = Regex::compile(&pattern, modifiers)?; + let subject = values.string_bytes(subject)?; + let matched = regex + .captures(&subject) + .and_then(|captures| captures.get(0)) + .is_some_and(|full_match| full_match.start() == 0); + values.bool_value(matched) +} + +/// Translates the optional `mb_ereg_match()` options string into compile modifiers. +fn eval_mb_ereg_match_modifiers( + options: Option, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(options) = options else { + return Ok(EvalPregModifiers::default()); + }; + if values.is_null(options)? { + return Ok(EvalPregModifiers::default()); + } + let options = values.string_bytes(options)?; + Ok(EvalPregModifiers { + case_insensitive: options.contains(&b'i'), + ..EvalPregModifiers::default() + }) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs index e653a2a628..5abd4fb98a 100644 --- a/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs +++ b/crates/elephc-magician/src/interpreter/builtins/regex/mod.rs @@ -12,6 +12,7 @@ mod captures; mod engine; +mod mb_ereg_match; mod preg_match; mod preg_match_all; mod pattern; @@ -24,6 +25,7 @@ mod targets; pub(in crate::interpreter) use captures::*; pub(in crate::interpreter) use engine::*; +pub(in crate::interpreter) use mb_ereg_match::*; pub(in crate::interpreter) use preg_match::*; pub(in crate::interpreter) use preg_match_all::*; pub(in crate::interpreter) use pattern::*; diff --git a/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs b/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs index a86f30109d..b5b7f79620 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs @@ -10,12 +10,17 @@ eval_builtin! { name: "htmlentities", area: String, - params: [string], + params: [ + string, + flags = EvalBuiltinDefaultValue::Int(11), + encoding = EvalBuiltinDefaultValue::String("UTF-8"), + ], direct: HtmlEntity, values: HtmlEntity, } use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; /// Evaluates PHP `htmlentities(...)` over one eval string expression. pub(in crate::interpreter) fn eval_builtin_htmlentities( diff --git a/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs b/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs index d9f717171a..2efbbadded 100644 --- a/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs +++ b/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs @@ -10,12 +10,17 @@ eval_builtin! { name: "htmlspecialchars", area: String, - params: [string], + params: [ + string, + flags = EvalBuiltinDefaultValue::Int(11), + encoding = EvalBuiltinDefaultValue::String("UTF-8"), + ], direct: HtmlEntity, values: HtmlEntity, } use super::super::super::*; +use super::super::spec::EvalBuiltinDefaultValue; /// Evaluates PHP `htmlspecialchars(...)` over one eval string expression. pub(in crate::interpreter) fn eval_builtin_htmlspecialchars( @@ -28,6 +33,8 @@ pub(in crate::interpreter) fn eval_builtin_htmlspecialchars( } /// Evaluates a named HTML entity encode/decode builtin over one string expression. +/// The encoders accept optional flags/encoding arguments; like the static +/// runtime they are evaluated but have no effect (ENT_QUOTES behaviour). pub(in crate::interpreter) fn eval_builtin_html_entity_named( name: &str, args: &[EvalExpr], @@ -35,10 +42,16 @@ pub(in crate::interpreter) fn eval_builtin_html_entity_named( scope: &mut ElephcEvalScope, values: &mut impl RuntimeValueOps, ) -> Result { - let [value] = args else { - return Err(EvalStatus::RuntimeFatal); + let accepts_options = matches!(name, "htmlspecialchars" | "htmlentities"); + let value = match args { + [value] => value, + [value, _] | [value, _, _] if accepts_options => value, + _ => return Err(EvalStatus::RuntimeFatal), }; let value = eval_expr(value, context, scope, values)?; + for extra in &args[1..] { + eval_expr(extra, context, scope, values)?; + } eval_html_entity_named_result(name, value, values) } diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs index 9debe6556e..d54d99fe32 100644 --- a/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs +++ b/crates/elephc-magician/src/interpreter/tests/builtins_strings_encoding.rs @@ -177,6 +177,30 @@ return function_exists("preg_match") && function_exists("preg_match_all") && fun assert_eq!(values.get(result), FakeValue::Bool(true)); } +/// Verifies `mb_ereg_match()` anchors at the subject start and honors the `i` option. +#[test] +fn execute_program_dispatches_mb_ereg_match() { + let program = parse_fragment( + br#"echo (mb_ereg_match('ab', 'abc') ? "y" : "n") . ":"; +echo (mb_ereg_match('bc', 'abc') ? "y" : "n") . ":"; +echo (mb_ereg_match('^[A-Z][A-Za-z0-9]*$', 'Foo') ? "y" : "n") . ":"; +echo (mb_ereg_match('[a-z]+\z', 'abc123') ? "y" : "n") . ":"; +echo (mb_ereg_match('ab', 'AB') ? "y" : "n") . ":"; +echo (mb_ereg_match('ab', 'AB', 'i') ? "y" : "n") . ":"; +echo (mb_ereg_match('ab', 'AB', null) ? "y" : "n") . ":"; +echo (call_user_func("mb_ereg_match", "ab", "abx") ? "y" : "n") . ":"; +return function_exists("mb_ereg_match");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "y:n:y:n:n:y:n:y:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + /// Verifies `preg_replace_callback()` accepts the same callable forms as other callback builtins. #[test] fn execute_program_preg_replace_callback_accepts_general_callables() { diff --git a/docs/php/eval.md b/docs/php/eval.md index 3c7c86647a..be14eb855d 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -824,7 +824,7 @@ where listed below unless a note says otherwise. | Strings, bytes, and formatting | `strlen()`, `ord()`, `chr()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `ucwords()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `strrev()`, `grapheme_strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `gzcompress()`, `gzdeflate()`, `gzinflate()`, `gzuncompress()`, `number_format()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()` | | Hashing | `crc32()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `hash_init()`, `hash_update()`, `hash_final()`, `hash_copy()` | | JSON | `json_encode()`, `json_decode()`, `json_validate()`, `json_last_error()`, `json_last_error_msg()` | -| Regex | `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()` | +| Regex | `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `mb_ereg_match()` | | Arrays and sorting | `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_map()`, `array_filter()`, `array_reduce()`, `array_walk()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()` | | Iterators and SPL | `iterator_count()`, `iterator_to_array()`, `iterator_apply()`, `spl_classes()`, `spl_object_id()`, `spl_object_hash()`, `spl_autoload()`, `spl_autoload_call()`, `spl_autoload_extensions()`, `spl_autoload_functions()`, `spl_autoload_register()`, `spl_autoload_unregister()` | | Math and random | `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `clamp()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `fdiv()`, `fmod()`, `rand()`, `mt_rand()`, `random_int()` | diff --git a/tests/builtin_parity_tests.rs b/tests/builtin_parity_tests.rs index 076ee7d0cc..e859e42c26 100644 --- a/tests/builtin_parity_tests.rs +++ b/tests/builtin_parity_tests.rs @@ -64,8 +64,10 @@ const EVAL_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[ /// Eval supports extra optional by-reference parameters before the static backend does. const EVAL_BY_REF_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["is_callable", "preg_match_all"]; -/// Eval supports variadic debug output before the static backend does. -const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &["var_dump"]; +/// Eval supports variadic behavior before the static backend does. Empty: the +/// static `var_dump` signature is variadic, so its former entry became an exact +/// shape match; keep the slice for the next genuine variadic extension. +const EVAL_VARIADIC_SIGNATURE_EXTENSION_BUILTINS: &[&str] = &[]; /// Builtins migrated to magician's declarative eval registry. const EVAL_DECLARATIVE_REGISTRY_BUILTINS: &[&str] = &[ From 5d15ecafb8fe8ed21b37f2680886b2ac4b049c19 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 12:46:28 +0200 Subject: [PATCH 1157/1208] fix(eval): realign literal-eval barriers, scope runtime, and global sync - direct-local fragments skip the barrier again (reverts the 55d12a0f8 polarity flip that dragged the bridge into native-only programs) - bare expression statements with calls/prints no longer lower to Noop in the local-scalar subset; they fall back to the bridge - the scope barrier declares the global-scope slot too - scope-only functions map to the eval_scope feature (no magician link) and emit the native value wrappers next to the scope helpers - eval_sync_global_type: only Load/StoreGlobal make a name a program global, and regular globals always sync as boxed Mixed --- src/codegen/lower_inst/builtins/eval.rs | 56 ++++++++++++++++++++++- src/codegen_support/runtime/emitters.rs | 5 ++ src/codegen_support/runtime/eval_scope.rs | 26 ++--------- src/ir_lower/context.rs | 3 ++ src/ir_lower/expr/mod.rs | 17 ++++--- src/ir_lower/program.rs | 21 +++++++-- 6 files changed, 95 insertions(+), 33 deletions(-) diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 81be7b1d80..7da3924522 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -1401,12 +1401,52 @@ fn parse_eval_local_scalar_expr_stmt( )) } _ => { - let _ = eval_local_scalar_value_expr(expr, analysis, assigned)?; + let parsed = eval_local_scalar_value_expr(expr, analysis, assigned)?; + // A bare expression statement lowers to Noop, discarding the value. + // Calls and prints inside it would lose their side effects (e.g. a + // dropped `define(...)`), so those fragments must fall back to the + // bridge instead of the local scalar AOT subset. + if eval_local_scalar_expr_has_side_effects(&parsed) { + return None; + } Some((EvalLocalScalarStmt::Noop, false)) } } } +/// Returns true when a parsed local-scalar expression carries side effects +/// that a discarded expression statement would lose. +fn eval_local_scalar_expr_has_side_effects(expr: &EvalLocalScalarExpr) -> bool { + match &expr.kind { + EvalLocalScalarExprKind::Null + | EvalLocalScalarExprKind::Int(_) + | EvalLocalScalarExprKind::Float(_) + | EvalLocalScalarExprKind::Bool(_) + | EvalLocalScalarExprKind::String(_) + | EvalLocalScalarExprKind::LoadVar(_) + | EvalLocalScalarExprKind::Isset(_) + | EvalLocalScalarExprKind::EmptyVar(_) => false, + EvalLocalScalarExprKind::Negate(inner) + | EvalLocalScalarExprKind::BitNot(inner) + | EvalLocalScalarExprKind::Not(inner) => eval_local_scalar_expr_has_side_effects(inner), + EvalLocalScalarExprKind::Print(_) => true, + EvalLocalScalarExprKind::Ternary { + condition, + then_expr, + else_expr, + } => { + eval_local_scalar_expr_has_side_effects(condition) + || eval_local_scalar_expr_has_side_effects(then_expr) + || eval_local_scalar_expr_has_side_effects(else_expr) + } + EvalLocalScalarExprKind::Binary { left, right, .. } => { + eval_local_scalar_expr_has_side_effects(left) + || eval_local_scalar_expr_has_side_effects(right) + } + EvalLocalScalarExprKind::StaticFunctionCall { .. } => true, + } +} + /// Builds an increment/decrement assignment value for local scalar expression statements. fn eval_local_scalar_inc_dec_expr( name: &str, @@ -10158,6 +10198,7 @@ fn push_eval_process_superglobal(globals: &mut Vec, name: &str, /// Returns one unambiguous codegen type used for a program global, if available. fn eval_sync_global_type(ctx: &FunctionContext<'_>, name: &str) -> Option { + let is_superglobal = crate::superglobals::is_superglobal(name); let mut inferred = None; for function in ctx .module @@ -10169,6 +10210,19 @@ fn eval_sync_global_type(ctx: &FunctionContext<'_>, name: &str) -> Option LoweringContext<'m, 'f> { pub(crate) fn apply_eval_scope_barrier(&mut self) { self.eval_barrier_active = true; self.declare_eval_scope_local(); + // Scope-sync codegen paths flush program globals into the local scope, + // so the global-scope handle slot must exist alongside the scope slot. + self.declare_eval_global_scope_local(); } /// Ensures top-level eval fragments can see `$argc` and `$argv` by name. diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 12031627ce..8e1aa7a29e 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1952,14 +1952,17 @@ fn emit_builtin_call_value( /// Returns true when a literal `eval` call may still need runtime scope/interpreter state. fn eval_literal_needs_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { + // Fragments fully handled by the direct-local AOT paths never touch runtime + // eval state; applying the barrier would declare eval locals and drag the + // whole bridge runtime (and magician staticlib) into native-only programs. if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { - return true; + return false; } if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { - return true; + return false; } if eval_literal_local_scalar_direct_sync_supported_by_lowering(ctx, fragment) { - return true; + return false; } let static_call_supported = |name: &str, args: &[Expr]| { eval_literal_static_function_supported_by_lowering(ctx, name, args) @@ -2001,14 +2004,16 @@ fn eval_literal_needs_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> /// Returns true when a literal `eval` only needs materialized eval-scope state. fn eval_literal_needs_scope_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { + // Direct-local AOT fragments also skip the materialized scope: their reads + // and writes go straight to caller locals. if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { - return true; + return false; } if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { - return true; + return false; } if eval_literal_local_scalar_direct_sync_supported_by_lowering(ctx, fragment) { - return true; + return false; } let static_call_supported = |name: &str, args: &[Expr]| { eval_literal_static_function_supported_by_lowering(ctx, name, args) diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 4a8e6e75c5..9a35ea4f3b 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -350,8 +350,10 @@ fn include_lowered_runtime_features(module: &mut Module) { fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { let mut features = RuntimeFeatures::none(); for function in all_lowered_functions(module) { - if function_contains_eval_state(function) { + if function_contains_eval_scope_state(function) { features.eval_scope = true; + } + if function_contains_eval_context_state(function) { features.eval_bridge = true; } for (inst_index, inst) in function.instructions.iter().enumerate() { @@ -397,16 +399,27 @@ fn lowered_runtime_features(module: &Module) -> RuntimeFeatures { features } -/// Returns true when a lowered function owns hidden eval scope/context state. -fn function_contains_eval_state(function: &Function) -> bool { +/// Returns true when a lowered function owns hidden eval scope handle slots. +/// Scope-only functions use the native scope helpers and must not force the +/// magician bridge staticlib into the link. +fn function_contains_eval_scope_state(function: &Function) -> bool { function.locals.iter().any(|local| { matches!( local.kind, - LocalKind::EvalContext | LocalKind::EvalScope | LocalKind::EvalGlobalScope + LocalKind::EvalScope | LocalKind::EvalGlobalScope ) }) } +/// Returns true when a lowered function owns an interpreter context handle, +/// which requires the full magician eval bridge runtime. +fn function_contains_eval_context_state(function: &Function) -> bool { + function + .locals + .iter() + .any(|local| matches!(local.kind, LocalKind::EvalContext)) +} + /// Returns true when a literal eval call still needs the magician bridge runtime. fn eval_literal_call_requires_bridge( module: &Module, From ee3a015994215f02d3deec8a54c27274905fb075 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 12:46:40 +0200 Subject: [PATCH 1158/1208] feat(parser): parse legacy array() as an array literal array(2 => "two", "tail") previously failed with 'Expected , between arguments'; key => value pairs are literal elements, not call args. --- src/parser/expr/prefix.rs | 31 +++++++++++++++++++++++++++---- src/parser/expr/prefix_complex.rs | 9 +++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/parser/expr/prefix.rs b/src/parser/expr/prefix.rs index 50bdca3eb2..3a07ae1bb0 100644 --- a/src/parser/expr/prefix.rs +++ b/src/parser/expr/prefix.rs @@ -461,6 +461,26 @@ fn parse_array_literal( tokens: &[(Token, Span)], pos: &mut usize, span: Span, +) -> Result { + parse_array_literal_with_terminator(tokens, pos, span, &Token::RBracket, "']'") +} + +/// Parses the legacy `array(...)` literal form after its opening parenthesis. +pub(super) fn parse_legacy_array_literal( + tokens: &[(Token, Span)], + pos: &mut usize, + span: Span, +) -> Result { + parse_array_literal_with_terminator(tokens, pos, span, &Token::RParen, "')'") +} + +/// Parses an array literal body up to `closing`, starting at the opening token. +fn parse_array_literal_with_terminator( + tokens: &[(Token, Span)], + pos: &mut usize, + span: Span, + closing: &Token, + closing_desc: &str, ) -> Result { *pos += 1; let mut elems = Vec::new(); @@ -468,7 +488,7 @@ fn parse_array_literal( let mut is_assoc = false; let mut first = true; let mut next_auto_key = 0i64; - while *pos < tokens.len() && tokens[*pos].0 != Token::RBracket { + while *pos < tokens.len() && tokens[*pos].0 != *closing { if !first { if tokens[*pos].0 != Token::Comma { return Err(CompileError::new( @@ -477,7 +497,7 @@ fn parse_array_literal( )); } *pos += 1; - if *pos < tokens.len() && tokens[*pos].0 == Token::RBracket { + if *pos < tokens.len() && tokens[*pos].0 == *closing { break; } } @@ -511,8 +531,11 @@ fn parse_array_literal( } first = false; } - if *pos >= tokens.len() || tokens[*pos].0 != Token::RBracket { - return Err(CompileError::new(span, "Expected ']'")); + if *pos >= tokens.len() || tokens[*pos].0 != *closing { + return Err(CompileError::new( + span, + &format!("Expected {closing_desc}"), + )); } *pos += 1; if is_assoc { diff --git a/src/parser/expr/prefix_complex.rs b/src/parser/expr/prefix_complex.rs index 7f32d923e6..587010846d 100644 --- a/src/parser/expr/prefix_complex.rs +++ b/src/parser/expr/prefix_complex.rs @@ -618,6 +618,15 @@ pub(super) fn parse_named_expr( span: Span, ) -> Result { let name = parse_name(tokens, pos, span, "Expected name")?; + // PHP's legacy `array(...)` construct is an array literal, not a call: + // its elements may use `key => value` pairs that call arguments reject. + if name.parts.len() == 1 + && name.parts[0].eq_ignore_ascii_case("array") + && *pos < tokens.len() + && tokens[*pos].0 == Token::LParen + { + return super::prefix::parse_legacy_array_literal(tokens, pos, span); + } if name.parts.len() == 1 && name.parts[0] == "buffer_new" && *pos < tokens.len() From 9d6314a654a2c1189a395cd8221da83fdb9c4d29 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 12:46:40 +0200 Subject: [PATCH 1159/1208] fix(ir): keep declared_params faithful to source type hints Runtime metadata no longer marks widened Mixed params as declared; reflection hasType()/getType() report untyped params correctly and invokers detect boxed ABI params from the parameter types. --- src/codegen/runtime_callable_invoker.rs | 5 ++++- src/ir_lower/function.rs | 16 ++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/codegen/runtime_callable_invoker.rs b/src/codegen/runtime_callable_invoker.rs index a1c6f43cfe..23e3c1f91a 100644 --- a/src/codegen/runtime_callable_invoker.rs +++ b/src/codegen/runtime_callable_invoker.rs @@ -732,7 +732,10 @@ fn declared_target_ty<'a>(sig: Option<&'a FunctionSig>, param_idx: usize) -> Opt sig.and_then(|sig| { let target_ty = sig.params.get(param_idx).map(|(_, ty)| ty)?; if sig.declared_params.get(param_idx).copied().unwrap_or(false) - || matches!(target_ty.codegen_repr(), PhpType::Mixed) + || matches!( + target_ty.codegen_repr(), + PhpType::Mixed | PhpType::Union(_) + ) { Some(target_ty) } else { diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 208077c366..3b936e2afc 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -1109,17 +1109,13 @@ pub(crate) fn eir_signature_with_php_param_contracts( eir_signature } -/// Marks boxed ABI parameters as materialization targets for reused runtime invokers. +/// Returns the signature stored as runtime metadata on lowered functions. +/// +/// `declared_params` keeps its source meaning ("the parameter has a written +/// type hint") so reflection metadata stays faithful; runtime invokers detect +/// boxed Mixed/Union ABI parameters from the parameter types directly. fn eir_runtime_metadata_signature(signature: &FunctionSig) -> FunctionSig { - let mut signature = signature.clone(); - for (index, (_, php_type)) in signature.params.iter().enumerate() { - if matches!(php_type.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { - if let Some(declared) = signature.declared_params.get_mut(index) { - *declared = true; - } - } - } - signature + signature.clone() } /// Returns true when an inferred untyped parameter has an EIR-safe concrete ABI contract. From 599f84c61b71c7b19fd124d6a134f7e040f182c2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 12:46:40 +0200 Subject: [PATCH 1160/1208] test: refresh eval bridge fixtures and corpus timeout - links-magician-once uses dynamic eval strings (literals compile AOT) - requires-eval-bridge uses an indexed array write; read-only foreach now goes through the scope-read AOT path - lowers_examples_corpus gets the 180s slow-timeout override --- .config/nextest.toml | 10 ++++++++++ tests/codegen/eval.rs | 10 +++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 640e47603b..49ebc2c48c 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -46,3 +46,13 @@ slow-timeout = { period = "180s", terminate-after = 1 } [[profile.ci.overrides]] filter = 'test(parity_function_first_class_callable_dispatch)' slow-timeout = { period = "180s", terminate-after = 1 } + +# `lowers_examples_corpus` type-checks and lowers every program under examples/; +# ~40s alone and past the global 60s cap on loaded runners. +[[profile.default.overrides]] +filter = 'test(lowers_examples_corpus)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.ci.overrides]] +filter = 'test(lowers_examples_corpus)' +slow-timeout = { period = "180s", terminate-after = 1 } diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 34f2dc634b..284b9619d7 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -37,8 +37,10 @@ echo is_callable("eval") ? "1" : "0"; #[test] fn test_eval_codegen_requires_eval_bridge() { let dir = make_cli_test_dir("elephc_magician_bridge_asm"); + // Indexed writes into a caller array still require bridge semantics; + // read-only foreach fragments now compile through the scope-read AOT path. let (user_asm, _runtime_asm, required_libraries) = compile_source_to_asm_with_options( - " Date: Fri, 10 Jul 2026 13:46:13 +0200 Subject: [PATCH 1161/1208] Revert "fix(ir): keep declared_params faithful to source type hints" This reverts commit 893e39d5b7c3be3d2a48b23fb169e659e37d1ac5. --- src/codegen/runtime_callable_invoker.rs | 5 +---- src/ir_lower/function.rs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/codegen/runtime_callable_invoker.rs b/src/codegen/runtime_callable_invoker.rs index 23e3c1f91a..a1c6f43cfe 100644 --- a/src/codegen/runtime_callable_invoker.rs +++ b/src/codegen/runtime_callable_invoker.rs @@ -732,10 +732,7 @@ fn declared_target_ty<'a>(sig: Option<&'a FunctionSig>, param_idx: usize) -> Opt sig.and_then(|sig| { let target_ty = sig.params.get(param_idx).map(|(_, ty)| ty)?; if sig.declared_params.get(param_idx).copied().unwrap_or(false) - || matches!( - target_ty.codegen_repr(), - PhpType::Mixed | PhpType::Union(_) - ) + || matches!(target_ty.codegen_repr(), PhpType::Mixed) { Some(target_ty) } else { diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 3b936e2afc..208077c366 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -1109,13 +1109,17 @@ pub(crate) fn eir_signature_with_php_param_contracts( eir_signature } -/// Returns the signature stored as runtime metadata on lowered functions. -/// -/// `declared_params` keeps its source meaning ("the parameter has a written -/// type hint") so reflection metadata stays faithful; runtime invokers detect -/// boxed Mixed/Union ABI parameters from the parameter types directly. +/// Marks boxed ABI parameters as materialization targets for reused runtime invokers. fn eir_runtime_metadata_signature(signature: &FunctionSig) -> FunctionSig { - signature.clone() + let mut signature = signature.clone(); + for (index, (_, php_type)) in signature.params.iter().enumerate() { + if matches!(php_type.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) { + if let Some(declared) = signature.declared_params.get_mut(index) { + *declared = true; + } + } + } + signature } /// Returns true when an inferred untyped parameter has an EIR-safe concrete ABI contract. From 1584fa4bf1d5df9e3ab3d4a630b93f73772f0e57 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 13:48:16 +0200 Subject: [PATCH 1162/1208] fix(reflection): derive hasType from source type hints declared_params doubles as the invoker boxed-ABI marker, so untyped params widened to Mixed carry a true flag; require the source type expression too. Replaces the reverted declared_params change that broke callback invokers. --- src/codegen/lower_inst/objects/reflection.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index a3f1d655ee..3633a1ca02 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -3921,7 +3921,21 @@ fn reflection_parameter_members_with_declaring_function( let mut parameters = Vec::new(); for (index, (name, ty)) in sig.params.iter().enumerate() { let is_variadic = sig.variadic.as_deref() == Some(name.as_str()); - let has_type = sig.declared_params.get(index).copied().unwrap_or(false); + // `declared_params` doubles as the runtime invoker's boxed-ABI marker + // (see `eir_runtime_metadata_signature`), so untyped params widened to + // Mixed carry a true flag. The source type hint disambiguates: a + // parameter only hasType() when the source actually declared one. + let declared = sig.declared_params.get(index).copied().unwrap_or(false); + let has_type = if sig.param_type_exprs.len() == sig.params.len() { + declared + && sig + .param_type_exprs + .get(index) + .and_then(Option::as_ref) + .is_some() + } else { + declared + }; let type_metadata = reflection_parameter_type_metadata( sig.param_type_exprs.get(index).and_then(Option::as_ref), ty, From 99c2915c60a844dbcbd7f32fb2008561d9083bf6 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 14:39:46 +0200 Subject: [PATCH 1163/1208] fix(eval): widen unspecialized __call args to the boxed Mixed ABI The checker seeds $args as Array; eval-only magic calls never specialize it, and a Never element lowers every $args[N] read to an empty constant. Keep the (Str, Array) contract only for Array. --- src/ir_lower/function.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 208077c366..400a217c7c 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -1153,8 +1153,19 @@ fn magic_method_param_keeps_eir_contract( param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str) } "__call" | "__callstatic" => { + // The $args array keeps its contract only once call sites have + // specialized the element type to Mixed. The checker seeds it as + // Array; eval-only magic calls never specialize it, and a + // Never element would lower every $args[N] read to an empty + // constant, so those fall back to the boxed Mixed widening. Other + // element types must widen too: the invoker's Mixed→Array pass + // hands over boxed-Mixed element cells unconverted. (param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str)) - || (param_index == 1 && matches!(php_type.codegen_repr(), PhpType::Array(_))) + || (param_index == 1 + && matches!( + php_type.codegen_repr(), + PhpType::Array(elem) if matches!(elem.codegen_repr(), PhpType::Mixed) + )) } _ => false, } From a7c19d1240ead8bf58e3d08a2faaf85c1669d5bf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 14:39:46 +0200 Subject: [PATCH 1164/1208] fix(ir): keep raw ABI for stream wrapper and filter contract methods The runtime vtables dispatch stream_open/stream_read/filter/... with raw fixed-ABI arguments; widening their untyped params to boxed Mixed desynchronized dispatcher and body. --- src/codegen_support/runtime/data/mod.rs | 1 + src/codegen_support/runtime/data/user.rs | 12 ++++++++++++ src/codegen_support/runtime/mod.rs | 1 + src/ir_lower/program.rs | 12 ++++++++++++ 4 files changed, 26 insertions(+) diff --git a/src/codegen_support/runtime/data/mod.rs b/src/codegen_support/runtime/data/mod.rs index 7626705ba8..bd133beeb6 100644 --- a/src/codegen_support/runtime/data/mod.rs +++ b/src/codegen_support/runtime/data/mod.rs @@ -15,6 +15,7 @@ mod user; pub(crate) use fixed::emit_runtime_data_fixed; /// Emit fixed runtime data section (heap globals, fatal/assertion messages, lookup tables, builtin callable metadata). pub(crate) use user::emit_runtime_data_user; +pub(crate) use user::{is_user_filter_contract_method, is_user_wrapper_contract_method}; /// Fatal error message when `php_uname()` receives a `$mode` argument whose length is not exactly 1. pub(crate) const PHP_UNAME_MODE_LEN_MSG: &str = diff --git a/src/codegen_support/runtime/data/user.rs b/src/codegen_support/runtime/data/user.rs index be03124564..5105fc0601 100644 --- a/src/codegen_support/runtime/data/user.rs +++ b/src/codegen_support/runtime/data/user.rs @@ -2009,6 +2009,18 @@ pub(crate) const USER_WRAPPER_VTABLE_SLOTS: usize = 23; /// branch with a single load + cmp. pub(crate) const USER_FILTER_VTABLE_SLOTS: usize = 5; +/// Returns true when a method key belongs to the fixed-ABI stream-wrapper +/// vtable surface dispatched by the runtime with raw arguments. +pub(crate) fn is_user_wrapper_contract_method(method_key: &str) -> bool { + USER_WRAPPER_METHOD_NAMES.contains(&method_key) +} + +/// Returns true when a method key belongs to the fixed-ABI user-filter +/// vtable surface dispatched by the runtime with raw arguments. +pub(crate) fn is_user_filter_contract_method(method_key: &str) -> bool { + USER_FILTER_METHOD_NAMES.contains(&method_key) +} + const USER_FILTER_METHOD_NAMES: [&str; 3] = [ "filter", "oncreate", diff --git a/src/codegen_support/runtime/mod.rs b/src/codegen_support/runtime/mod.rs index e07780b20e..cb79493943 100644 --- a/src/codegen_support/runtime/mod.rs +++ b/src/codegen_support/runtime/mod.rs @@ -34,6 +34,7 @@ mod zval; pub(crate) use data::emit_runtime_data_fixed; /// Emit fixed runtime data section (symbols, constants, type metadata). pub(crate) use data::emit_runtime_data_user; +pub(crate) use data::{is_user_filter_contract_method, is_user_wrapper_contract_method}; /// Emit user-program-specific runtime data section. pub(crate) use emitters::emit_runtime; /// Emit full runtime helpers (orchestrates all runtime sections). diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 9a35ea4f3b..09002d8536 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -167,7 +167,19 @@ fn normalize_method_map_for_eir( is_static: bool, callable_param_sigs: &HashMap<(String, String), FunctionSig>, ) { + // Stream-wrapper and user-filter contract methods are invoked through + // runtime vtables with raw fixed-ABI arguments; widening their untyped + // params to boxed Mixed would desynchronize the dispatcher and the body. + let is_wrapper_class = methods.contains_key("stream_open"); + let is_filter_class = methods.contains_key("filter"); for (method_key, signature) in methods.iter_mut() { + if (is_wrapper_class + && crate::codegen_support::runtime::is_user_wrapper_contract_method(method_key)) + || (is_filter_class + && crate::codegen_support::runtime::is_user_filter_contract_method(method_key)) + { + continue; + } let owner_name = format!("{}::{}", class_name, method_key); let mut normalized = function::eir_signature_with_php_param_contracts( &owner_name, From 61a9f80f2f1c0995004326d7df8fe42d79b657f2 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 14:39:46 +0200 Subject: [PATCH 1165/1208] fix(reflection): restore invokeArgs (object, args) parameters The single-args override broke ReflectionMethod::invokeArgs dispatch; only the Mixed return type needs patching. --- src/types/checker/builtin_types/reflection.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 3fa7d95346..6a79b12b14 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -5115,12 +5115,10 @@ pub(crate) fn patch_builtin_reflection_signatures(checker: &mut Checker) { make_reflection_variadic_optional(sig); } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("invokeArgs")) { + // Keep the shell's (object, args) parameters: ReflectionMethod::invokeArgs + // takes the receiver first, and replacing the params with a + // lone args array breaks invoke dispatch. sig.return_type = PhpType::Mixed; - sig.params = vec![("args".to_string(), PhpType::Array(Box::new(PhpType::Mixed)))]; - sig.param_type_exprs = vec![Some(array_type())]; - sig.defaults = vec![None]; - sig.ref_params = vec![false]; - sig.declared_params = vec![true]; } if let Some(sig) = class_info.methods.get_mut(&php_symbol_key("createFromMethodName")) From e8e759807d2543243b908e04e37e8da81f5d7d36 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 14:40:00 +0200 Subject: [PATCH 1166/1208] fix(attributes): fold Foo::class args and construct with named arguments Class-level attribute capture lacked the ClassConstant arm the module capture already had, and newInstance dropped named-argument keys. --- src/codegen_support/reflection.rs | 13 ++++++++++++- src/types/checker/schema/classes/state.rs | 6 ++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/codegen_support/reflection.rs b/src/codegen_support/reflection.rs index 5dc5239970..636b0e0805 100644 --- a/src/codegen_support/reflection.rs +++ b/src/codegen_support/reflection.rs @@ -152,7 +152,18 @@ pub(crate) fn build_attribute_new_instance_body_with_extra( args: factory .args .iter() - .map(|entry| attr_arg_expr(&entry.value)) + .map(|entry| match &entry.key { + // Named attribute arguments must construct with + // named-argument semantics, not positionally. + Some(crate::types::AttrKey::Str(name)) => Expr::new( + ExprKind::NamedArg { + name: name.clone(), + value: Box::new(attr_arg_expr(&entry.value)), + }, + span, + ), + _ => attr_arg_expr(&entry.value), + }) .collect(), }, span, diff --git a/src/types/checker/schema/classes/state.rs b/src/types/checker/schema/classes/state.rs index d43c00f0bf..f5b2e35f50 100644 --- a/src/types/checker/schema/classes/state.rs +++ b/src/types/checker/schema/classes/state.rs @@ -297,6 +297,12 @@ fn fold_attr_value(expr: &crate::parser::ast::Expr) -> Option Some(AttrArgValue::Str(name.as_str().to_string())), + ExprKind::ClassConstant { .. } => None, ExprKind::Negate(inner) => match &inner.kind { ExprKind::IntLiteral(n) => Some(AttrArgValue::Int(n.wrapping_neg())), ExprKind::FloatLiteral(n) => Some(AttrArgValue::Float((-n).to_bits())), From 3cb1db69a95c09991a5771ee0ee6f02af206b6d5 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 14:40:00 +0200 Subject: [PATCH 1167/1208] fix(codegen): unbox Mixed store values into object-typed properties Untyped ctor params widen to boxed Mixed while the checker infers the slot as a concrete object; unbox on store (tag 6), null sentinel for non-object payloads, matching the other Mixed property coercions. --- src/codegen/lower_inst/objects.rs | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index a0c28399ff..ee1f88c5e0 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -4881,6 +4881,9 @@ fn ensure_property_value_supported( if can_coerce_mixed_to_scalar_property(value_ty, &slot.php_type) { return Ok(()); } + if can_unbox_mixed_to_object_property(value_ty, &slot.php_type) { + return Ok(()); + } Err(CodegenIrError::unsupported(format!( "{} assigning PHP type {:?} to {}::${} with PHP type {:?}", inst.op.name(), @@ -4891,6 +4894,14 @@ fn ensure_property_value_supported( ))) } +/// Returns true when a boxed Mixed value can be unboxed into an object-typed +/// property slot. Untyped parameters widen to the boxed Mixed ABI while the +/// checker still infers the slot as a concrete object type. +fn can_unbox_mixed_to_object_property(value_ty: &PhpType, slot_ty: &PhpType) -> bool { + matches!(value_ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) + && matches!(slot_ty.codegen_repr(), PhpType::Object(_)) +} + /// Returns true when a concrete object value is assignable to an object-typed property. fn can_store_object_for_object_property( ctx: &FunctionContext<'_>, @@ -5688,6 +5699,7 @@ fn load_property_store_value_to_result( PhpType::Int => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"), PhpType::Bool => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"), PhpType::Float => abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_float"), + PhpType::Object(_) => emit_mixed_object_for_property_store(ctx), _ => {} } return Ok(()); @@ -5935,6 +5947,40 @@ fn emit_normalized_dynamic_instanceof_value( } /// Unboxes a Mixed/Union tested value and leaves only object payloads as matchable. +/// Unboxes a Mixed store value into the object pointer expected by an +/// object-typed property slot. Non-object payloads store the null sentinel +/// (matching the other lossy Mixed property coercions rather than raising a +/// TypeError). The property store retains the object, so the unboxed pointer +/// is increfed here. +fn emit_mixed_object_for_property_store(ctx: &mut FunctionContext<'_>) { + let object_label = ctx.next_label("prop_store_mixed_value_object"); + let done = ctx.next_label("prop_store_mixed_value_done"); + abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + ctx.emitter.instruction("cmp x0, #6"); // runtime tag 6 means the boxed payload is an object + ctx.emitter.instruction(&format!("b.eq {}", object_label)); // object payloads store their unboxed pointer + ctx.emitter.instruction("mov x0, #0"); // non-object payloads fall back to the null sentinel + ctx.emitter.instruction(&format!("b {}", done)); // skip pointer promotion for non-object payloads + ctx.emitter.label(&object_label); + ctx.emitter.instruction("mov x0, x1"); // promote the unboxed object pointer into the result register + } + Arch::X86_64 => { + ctx.emitter.instruction("cmp rax, 6"); // runtime tag 6 means the boxed payload is an object + ctx.emitter.instruction(&format!("je {}", object_label)); // object payloads store their unboxed pointer + ctx.emitter.instruction("xor eax, eax"); // non-object payloads fall back to the null sentinel + ctx.emitter.instruction(&format!("jmp {}", done)); // skip pointer promotion for non-object payloads + ctx.emitter.label(&object_label); + ctx.emitter.instruction("mov rax, rdi"); // promote the unboxed object pointer into the result register + } + } + ctx.emitter.label(&done); + abi::emit_incref_if_refcounted( + ctx.emitter, + &PhpType::Object(String::new()), // property stores retain the transferred object + ); +} + fn emit_mixed_instanceof_value_normalization(ctx: &mut FunctionContext<'_>) { let object_label = ctx.next_label("instanceof_dynamic_value_object"); let done = ctx.next_label("instanceof_dynamic_value_done"); From b5b41623bb23ef6b93325b8890fe9fe847f18e23 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 14:40:00 +0200 Subject: [PATCH 1168/1208] fix(checker): accept a dynamic autoload flag in class_exists family Existence still folds from the literal class name; the demand walker already treats non-literal flags as false. --- src/builtins/callables/support.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/builtins/callables/support.rs b/src/builtins/callables/support.rs index 8056c26392..36bad54b07 100644 --- a/src/builtins/callables/support.rs +++ b/src/builtins/callables/support.rs @@ -31,17 +31,10 @@ pub(crate) fn check_class_like_exists(cx: &mut BuiltinCheckCtx) -> Result Date: Fri, 10 Jul 2026 14:40:00 +0200 Subject: [PATCH 1169/1208] fix(codegen): stop embedding the source path in native-only programs The path feeds eval Reflection hooks only; gate it on the eval features. Anchor the optimizer string-helper asserts to main's section (synthetic Reflection bodies legitimately call them) and update the two eval-fixture asserts to the literal-AOT markers. --- src/codegen/mod.rs | 11 ++++++++++- .../optimizer/constant_folding/expressions.rs | 18 +++++++++++++++--- .../constant_propagation/straight_line.rs | 8 ++++++-- .../optimizer/dead_code_elimination/basics.rs | 5 ++++- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index fcc60e2ced..f3b461d2f2 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -274,7 +274,16 @@ fn finalize_user_asm( &runtime_classes, &module.enum_infos, Some(&allowed_class_names), - module.source_path.as_deref(), + // The source path feeds eval Reflection source-location hooks only; + // embedding it in native-only programs leaks the build path into the + // assembly (and trips needle-based optimizer asm asserts). + if module.required_runtime_features.eval_bridge + || module.required_runtime_features.eval_scope + { + module.source_path.as_deref() + } else { + None + }, ); let mut user_asm = emitter.output(); diff --git a/tests/codegen/optimizer/constant_folding/expressions.rs b/tests/codegen/optimizer/constant_folding/expressions.rs index 4e60840623..c0e8177f37 100644 --- a/tests/codegen/optimizer/constant_folding/expressions.rs +++ b/tests/codegen/optimizer/constant_folding/expressions.rs @@ -9,6 +9,18 @@ use super::*; +/// Returns only `main`'s section of the user assembly. Synthetic builtin +/// method bodies (e.g. the eval Reflection surface) legitimately call runtime +/// string helpers and would trip needle-based assertions about user code. +fn main_function_asm(user_asm: &str) -> &str { + let Some(start) = user_asm.find("@fn name=main ") else { + return user_asm; + }; + let rest = &user_asm[start..]; + let end = rest[1..].find("@fn name=").map_or(rest.len(), |i| i + 1); + &rest[..end] +} + /// Verifies that nested integer arithmetic with literals is constant-folded at compile time /// and the result is emitted directly as a literal in the generated binary. #[test] @@ -58,7 +70,7 @@ fn test_constant_folding_string_concat_removes_runtime_concat_call() { ); assert!( - !user_asm.contains("__rt_concat"), + !main_function_asm(&user_asm).contains("__rt_concat"), "constant-folded concat expression should not call __rt_concat in user assembly:\n{}", user_asm ); @@ -90,7 +102,7 @@ fn test_constant_folding_null_coalesce_removes_runtime_concat_call() { ); assert!( - !user_asm.contains("__rt_concat"), + !main_function_asm(&user_asm).contains("__rt_concat"), "constant-folded null coalesce should not leave __rt_concat in user assembly:\n{}", user_asm ); @@ -205,7 +217,7 @@ fn test_constant_folding_string_cast_removes_runtime_itoa_call() { compile_source_to_asm_with_options(" Date: Fri, 10 Jul 2026 14:47:48 +0200 Subject: [PATCH 1170/1208] fix(eval): overwrite native locals from direct AOT eval stores - direct-store and local-scalar sync targets accept slots the native code already writes; the store emitters release the previous refcounted value before overwriting - read/write targets accept parameter-initialized slots (by-value closure captures) - the lowering-side direct-store classifier now checks scalar kinds against existing slot types, so type-changing stores keep the barrier --- src/codegen/lower_inst/builtins/eval.rs | 30 ++++++++++---- src/ir_lower/expr/mod.rs | 55 ++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index 7da3924522..eddcc33f31 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -2449,7 +2449,10 @@ fn eval_literal_direct_read_write_local_slot( ctx.local_php_type(slot)?.codegen_repr(), PhpType::Int | PhpType::Float | PhpType::Mixed | PhpType::Union(_) ) - || !eval_literal_local_slot_has_eir_write(ctx, slot) + // Slots initialized by function parameters (including by-value closure + // captures) hold a defined value without an EIR store instruction. + || !(eval_literal_local_slot_has_eir_write(ctx, slot) + || ctx.function.params.iter().any(|param| param.name == name)) { return Ok(None); } @@ -2811,12 +2814,11 @@ fn eval_literal_direct_store_local_slot( let Some(slot) = ctx.local_slot_by_name(name) else { return Ok(None); }; - if ctx.local_kind(slot)? != LocalKind::PhpLocal - || ctx.local_stores_ref_cell_pointer(slot) - || eval_literal_local_slot_has_eir_write(ctx, slot) - { + if ctx.local_kind(slot)? != LocalKind::PhpLocal || ctx.local_stores_ref_cell_pointer(slot) { return Ok(None); } + // Slots the native code already writes are fine: the store emitter + // releases the previous refcounted value before overwriting. Ok(Some(slot)) } @@ -2880,11 +2882,17 @@ fn emit_eval_literal_aot_direct_local_store( )); }; match ctx.local_php_type(slot)?.codegen_repr() { - PhpType::Mixed | PhpType::Union(_) => { + target_ty @ (PhpType::Mixed | PhpType::Union(_)) => { emit_eval_literal_aot_core_mixed_scalar(ctx, value); + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_release_old_direct_local_value(ctx, slot, &target_ty)?; + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); ctx.store_current_result_to_local(slot) } PhpType::Str => { + // Static scalar strings live in the data section; the old value + // may be a heap string and must be released before overwriting. + emit_eval_literal_release_old_direct_local_value(ctx, slot, &PhpType::Str)?; emit_eval_literal_aot_scalar_native_result(ctx, value); ctx.store_current_result_to_local(slot) } @@ -3070,11 +3078,12 @@ fn eval_local_scalar_direct_sync_targets( }; if ctx.local_kind(slot)? != LocalKind::PhpLocal || ctx.local_stores_ref_cell_pointer(slot) - || eval_literal_local_slot_has_eir_write(ctx, slot) || !eval_local_scalar_direct_sync_type_supported(ctx.local_php_type(slot)?, *local_type) { return Ok(None); } + // Slots the native code already writes are fine: the sync store + // releases the previous refcounted value before overwriting. targets.insert(name.clone(), Some(slot)); } Ok(Some(targets)) @@ -4668,12 +4677,17 @@ fn emit_eval_local_scalar_store_current_result_to_direct_local( local_type: EvalLocalScalarType, ) -> Result<()> { match ctx.local_php_type(slot)?.codegen_repr() { - PhpType::Mixed | PhpType::Union(_) => { + target_ty @ (PhpType::Mixed | PhpType::Union(_)) => { emit_eval_local_scalar_box_current_result( ctx, local_type, EvalLocalScalarBoxing::CoreRuntime, ); + // The caller slot may already hold a refcounted value written by + // native code before the eval; release it before overwriting. + abi::emit_push_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); + emit_eval_literal_release_old_direct_local_value(ctx, slot, &target_ty)?; + abi::emit_pop_reg(ctx.emitter, abi::int_result_reg(ctx.emitter)); ctx.store_current_result_to_local(slot) } PhpType::TaggedScalar => match local_type { diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 8e1aa7a29e..cda936e9f0 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1955,7 +1955,7 @@ fn eval_literal_needs_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> // Fragments fully handled by the direct-local AOT paths never touch runtime // eval state; applying the barrier would declare eval locals and drag the // whole bridge runtime (and magician staticlib) into native-only programs. - if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { + if eval_literal_direct_store_supported_by_lowering(ctx, fragment) { return false; } if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { @@ -2006,7 +2006,7 @@ fn eval_literal_needs_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> fn eval_literal_needs_scope_barrier(ctx: &LoweringContext<'_, '_>, fragment: &str) -> bool { // Direct-local AOT fragments also skip the materialized scope: their reads // and writes go straight to caller locals. - if crate::eval_aot::literal_fragment_direct_local_store_writes(fragment).is_some() { + if eval_literal_direct_store_supported_by_lowering(ctx, fragment) { return false; } if eval_literal_direct_read_write_supported_by_lowering(ctx, fragment) { @@ -2035,6 +2035,57 @@ fn eval_literal_needs_scope_barrier(ctx: &LoweringContext<'_, '_>, fragment: &st ) } +/// Returns true when a direct-store eval fragment fits every caller slot type. +/// Mirrors codegen's per-target checks: a store that changes an existing +/// slot's scalar type (e.g. Int -> Str) needs the barrier and a wider path. +fn eval_literal_direct_store_supported_by_lowering( + ctx: &LoweringContext<'_, '_>, + fragment: &str, +) -> bool { + let Some(writes) = crate::eval_aot::literal_fragment_direct_local_store_writes(fragment) + else { + return false; + }; + writes.iter().all(|(name, kind)| { + if crate::superglobals::is_superglobal(name) + || (ctx.in_main && ctx.all_global_var_names.contains(name)) + { + return false; + } + if ctx.local_slots.get(name).is_none() { + return true; + } + if ctx.is_ref_bound_local(name) + || ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) + { + return false; + } + ctx.local_types + .get(name) + .is_some_and(|ty| eval_literal_direct_store_type_supported(ty, *kind)) + }) +} + +/// Returns true when a static scalar store kind fits an existing caller slot type. +fn eval_literal_direct_store_type_supported( + ty: &PhpType, + kind: crate::eval_aot::DirectLocalStoreScalarKind, +) -> bool { + match ty.codegen_repr() { + PhpType::Mixed | PhpType::Union(_) => true, + PhpType::Int => kind == crate::eval_aot::DirectLocalStoreScalarKind::Int, + PhpType::Float => kind == crate::eval_aot::DirectLocalStoreScalarKind::Float, + PhpType::Bool => kind == crate::eval_aot::DirectLocalStoreScalarKind::Bool, + PhpType::Str => kind == crate::eval_aot::DirectLocalStoreScalarKind::String, + PhpType::TaggedScalar => matches!( + kind, + crate::eval_aot::DirectLocalStoreScalarKind::Int + | crate::eval_aot::DirectLocalStoreScalarKind::Null + ), + _ => false, + } +} + /// Returns true when a boxed read/write eval can use direct caller locals. fn eval_literal_direct_read_write_supported_by_lowering( ctx: &LoweringContext<'_, '_>, From d88a3c5417d66422a76bbc334c0ce155896f565a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 17:21:58 +0200 Subject: [PATCH 1171/1208] docs(plans): add dual-frontend grammar governance for eval Track corpus, allowed divergences, fix policy, and non-goals so main and magician parsers stay aligned without premature parser unification. --- .plans/elephc-eval-magician-plan.md | 128 +++++++++++++++++++++++++++- 1 file changed, 125 insertions(+), 3 deletions(-) diff --git a/.plans/elephc-eval-magician-plan.md b/.plans/elephc-eval-magician-plan.md index 2c686e9ac1..6463865856 100644 --- a/.plans/elephc-eval-magician-plan.md +++ b/.plans/elephc-eval-magician-plan.md @@ -41,6 +41,18 @@ builtins, and static-only builtins not yet present in magician. - [ ] Reduce the remaining manual AOT mini-codegen and converge on internal EIR functions for supported literal fragments. +- [ ] Add a shared PHP-fragment grammar corpus that must parse consistently on + the main compiler frontend and on magician (and stay aligned with AOT + acceptance where the fragment is a compile-time literal). +- [ ] Document allowed grammar divergences between main and magician (no ` parse as PHP fragment + -> parse as PHP fragment (compiler frontend) -> normalize/name-resolve compatibly with the context -> classify AOT eligibility -> plan reads/writes/calls/fallback @@ -174,6 +198,22 @@ The AOT plan must preserve: AOT paths emit assembly markers such as `eval literal AOT compiled...`. Fallback paths emit markers with a readable reason where possible. +### Dual frontends (compiler vs magician) + +Two frontends are an intentional architectural trade-off, not an accident to +erase before landing: + +- magician links into user programs and must stay a small optional bridge; +- magician emits EvalIR for by-name runtime execution, not the compile AST that + feeds type checking, optimization, and EIR lowering; +- the compiler frontend also accepts elephc-only extensions (`ptr`, `buffer`, + `extern`, `packed class`, `ifdef`, …) that eval fragments must reject. + +Near-term governance is corpus + documented divergences + fix policy. A shared +pure-PHP syntax crate is a later option only if maintenance cost justifies it. +Do not merge the full compiler parser into magician, and do not make magician +depend on the main `elephc` crate, as a prerequisite for landing eval. + ## Completed Work ### Eval Runtime and Bridge @@ -590,7 +630,72 @@ Update docs when the subset changes: - literal eval AOT does not embed the parser/compiler in the binary; - magician fallback remains compatibility semantics; - fully AOT programs do not link `elephc_magician`; -- constructs that still fall back should be documented when user-visible. +- constructs that still fall back should be documented when user-visible; +- allowed dual-frontend grammar divergences stay documented when user-visible. + +### 7. Parser dual-stack governance + +#### Current state + +Eval has two intentional frontends: + +1. **Compiler frontend** (`src/lexer/`, `src/parser/`) for literal AOT analysis + and all ordinary compilation. +2. **Magician frontend** (`crates/elephc-magician/src/lexer/`, + `crates/elephc-magician/src/parser/`) for runtime fragments, producing + EvalIR for the interpreter. + +Magician does not depend on the `elephc` crate. That keeps the optional +staticlib free of the full compiler pipeline and avoids shipping compile-time +machinery into user binaries. + +#### Near-term work (required) + +1. **Shared pure-PHP fragment grammar corpus** + - one source list of fragments and negative cases; + - must parse (or fail) consistently on main and magician for pure PHP; + - where a fragment is a compile-time literal candidate, AOT acceptance must + not disagree with magician on pure parse validity without an explicit + fallback reason that is about semantics/AOT coverage, not parse success. +2. **Documented allowed divergences** + - eval fragments reject opening ` git diff --check ``` +For grammar dual-frontend parity changes (once the corpus exists): + +```bash +cargo check +cargo test +cargo test -p elephc-magician +git diff --check +``` + For manual benchmarks: ```bash @@ -658,6 +772,11 @@ python3 scripts/benchmark_magician.py --case literal_scalar_aot --iterations 5 - - Magic property methods can turn apparently local object access into arbitrary user code. - Magician optimizations must not freeze context/scope/magic constants. +- Dual frontends can accept or reject different pure PHP fragments, so AOT + (compiler parser) and runtime fallback (magician parser) can silently + diverge without a grammar corpus and fix policy. +- Prematurely merging parsers or making magician depend on the full compiler + can bloat user binaries and create circular crate dependencies. - Every new path must stay target-aware on macOS ARM64, Linux ARM64, and Linux x86_64. @@ -675,4 +794,7 @@ The eval/magician work can be considered closed when: 7. prime-loop and algebra-heavy benchmarks remain correct and measurable; 8. all three supported targets have focused coverage for every ABI/codegen change; -9. docs and tests exactly reflect the supported subset and fallbacks. +9. docs and tests exactly reflect the supported subset and fallbacks; +10. pure-PHP fragment grammar is either shared by a thin syntax crate or kept + aligned by a green dual-frontend corpus with a short, documented + divergence list (full parser unification is not required). From 3b6f4b4641bd3dd31aedefdb791a63c69ecedd83 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 17:35:23 +0200 Subject: [PATCH 1172/1208] fix(dispatch): magic-call args contract, reflection hasType, method name case - keep the __call args contract for any call-site-specialized element type; only the raw Array seed (codegen_repr hides it as Void) falls back to the boxed Mixed widening - reflection hasType: non-Mixed declared params are always genuine (builtin sigs and variadics carry no type expressions); only declared Mixed params need the source hint check - the name resolver no longer lowercases static method names: lookups fold case themselves and __callStatic receives the as-written name --- src/codegen/lower_inst/objects/reflection.rs | 19 +++++++++---------- src/ir_lower/function.rs | 15 ++++++++------- src/name_resolver/expressions.rs | 8 +++++--- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/codegen/lower_inst/objects/reflection.rs b/src/codegen/lower_inst/objects/reflection.rs index 3633a1ca02..a43ccb36a7 100644 --- a/src/codegen/lower_inst/objects/reflection.rs +++ b/src/codegen/lower_inst/objects/reflection.rs @@ -3922,20 +3922,19 @@ fn reflection_parameter_members_with_declaring_function( for (index, (name, ty)) in sig.params.iter().enumerate() { let is_variadic = sig.variadic.as_deref() == Some(name.as_str()); // `declared_params` doubles as the runtime invoker's boxed-ABI marker - // (see `eir_runtime_metadata_signature`), so untyped params widened to - // Mixed carry a true flag. The source type hint disambiguates: a - // parameter only hasType() when the source actually declared one. + // (see `eir_runtime_metadata_signature`), which only ever raises the + // flag for Mixed/Union params. Non-Mixed declared params are always + // genuine (source hints, builtin signatures, variadics); a declared + // Mixed param needs the source type expression to distinguish a real + // `mixed` hint from an untyped param widened for the boxed ABI. let declared = sig.declared_params.get(index).copied().unwrap_or(false); - let has_type = if sig.param_type_exprs.len() == sig.params.len() { - declared - && sig + let has_type = declared + && (!matches!(ty.codegen_repr(), PhpType::Mixed | PhpType::Union(_)) + || sig .param_type_exprs .get(index) .and_then(Option::as_ref) - .is_some() - } else { - declared - }; + .is_some()); let type_metadata = reflection_parameter_type_metadata( sig.param_type_exprs.get(index).and_then(Option::as_ref), ty, diff --git a/src/ir_lower/function.rs b/src/ir_lower/function.rs index 400a217c7c..6f4154dc74 100644 --- a/src/ir_lower/function.rs +++ b/src/ir_lower/function.rs @@ -1154,17 +1154,18 @@ fn magic_method_param_keeps_eir_contract( } "__call" | "__callstatic" => { // The $args array keeps its contract only once call sites have - // specialized the element type to Mixed. The checker seeds it as + // specialized the element type. The checker seeds it as // Array; eval-only magic calls never specialize it, and a // Never element would lower every $args[N] read to an empty - // constant, so those fall back to the boxed Mixed widening. Other - // element types must widen too: the invoker's Mixed→Array pass - // hands over boxed-Mixed element cells unconverted. + // constant, so those fall back to the boxed Mixed widening. (param_index == 0 && matches!(php_type.codegen_repr(), PhpType::Str)) || (param_index == 1 - && matches!( - php_type.codegen_repr(), - PhpType::Array(elem) if matches!(elem.codegen_repr(), PhpType::Mixed) + && matches!(php_type.codegen_repr(), PhpType::Array(_)) + // Check the raw element type: codegen_repr normalizes the + // Never seed to Void and would hide it. + && !matches!( + php_type, + PhpType::Array(elem) if matches!(elem.as_ref(), PhpType::Never) )) } _ => false, diff --git a/src/name_resolver/expressions.rs b/src/name_resolver/expressions.rs index 4f3f0ecd82..42dce9fdc4 100644 --- a/src/name_resolver/expressions.rs +++ b/src/name_resolver/expressions.rs @@ -324,7 +324,9 @@ pub(super) fn resolve_expr( )), _ => receiver.clone(), }; - let resolved_method = php_symbol_key(method); + // Keep the source spelling: dispatch lookups fold case at lookup + // time, and `__callStatic` must receive the as-written name. + let method_key = php_symbol_key(method); let resolved_args: Vec = args .iter() .map(|arg| resolve_expr(arg, current_namespace, imports, symbols)) @@ -335,7 +337,7 @@ pub(super) fn resolve_expr( // function's flow-inferred array return keeps its element type, // so in_array works on the filtered result, where the synthetic method // would yield scalar mixed and regress in_array. - if resolved_method == "listidentifiers" + if method_key == "listidentifiers" && resolved_args.len() <= 2 && matches!( &resolved_receiver, @@ -351,7 +353,7 @@ pub(super) fn resolve_expr( } else { ExprKind::StaticMethodCall { receiver: resolved_receiver, - method: resolved_method, + method: method.clone(), args: resolved_args, } } From f4627215a0e0b17cab981fe9fa6e131466e87149 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:34:31 +0200 Subject: [PATCH 1173/1208] fix(eval): route constant probes through eval registry after barrier-free AOT evals Track that an eval() was lowered even when its fragment avoids the scope barrier; dynamic constant exists/fetch probes must consult the eval context either way, so declare the context local before emitting them. --- src/ir_lower/context.rs | 15 +++++++++++++++ src/ir_lower/expr/constants.rs | 10 ++++++++-- src/ir_lower/expr/mod.rs | 1 + 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/ir_lower/context.rs b/src/ir_lower/context.rs index 7c974d2781..c85894163d 100644 --- a/src/ir_lower/context.rs +++ b/src/ir_lower/context.rs @@ -149,6 +149,7 @@ pub(crate) struct LoweringContext<'m, 'f> { closure_counter: usize, hidden_temp_counter: usize, eval_barrier_active: bool, + eval_executed: bool, eval_scope_read_param: Option, eval_scope_read_names: HashSet, eval_scope_write_names: HashSet, @@ -225,6 +226,7 @@ impl<'m, 'f> LoweringContext<'m, 'f> { closure_counter: 0, hidden_temp_counter: 0, eval_barrier_active: false, + eval_executed: false, eval_scope_read_param: None, eval_scope_read_names: HashSet::new(), eval_scope_write_names: HashSet::new(), @@ -614,6 +616,19 @@ impl<'m, 'f> LoweringContext<'m, 'f> { self.eval_barrier_active } + /// Records that an `eval()` call was lowered, even when its fragment + /// compiled through a barrier-free AOT path. + pub(crate) fn mark_eval_executed(&mut self) { + self.eval_executed = true; + } + + /// Returns true when any `eval()` call was lowered in this function. + /// Unlike `has_eval_barrier`, this also covers barrier-free AOT evals: + /// dynamic constant probes must consult the eval registry either way. + pub(crate) const fn eval_executed(&self) -> bool { + self.eval_executed + } + /// Declares a hidden owner slot for a promoted local ref-cell pointer. fn declare_ref_cell_owner(&mut self, variable: &str, php_type: PhpType) -> LocalSlotId { let name = format!("__eir_ref_owner{}_{}", self.hidden_temp_counter, variable); diff --git a/src/ir_lower/expr/constants.rs b/src/ir_lower/expr/constants.rs index 96379df310..22d26d1f1f 100644 --- a/src/ir_lower/expr/constants.rs +++ b/src/ir_lower/expr/constants.rs @@ -48,7 +48,10 @@ pub(super) fn lower_static_defined_call( return None; }; let exists = ctx.constant_value(constant_name).is_some(); - if !exists && ctx.has_eval_barrier() { + if !exists && (ctx.has_eval_barrier() || ctx.eval_executed()) { + // Barrier-free AOT evals can still define constants dynamically; the + // probe needs the eval context, so make sure its slot exists. + ctx.declare_eval_context_local(); let data = ctx.intern_global_name(constant_name); return Some(ctx.emit_value( Op::EvalConstantExists, @@ -77,7 +80,10 @@ pub(super) fn lower_const_ref( if let Some((value, php_type)) = ctx.constant_value(name.as_str()) { return lower_constant_value(ctx, value, php_type, expr); } - if ctx.has_eval_barrier() { + if ctx.has_eval_barrier() || ctx.eval_executed() { + // Barrier-free AOT evals can still define constants dynamically; the + // fetch needs the eval context, so make sure its slot exists. + ctx.declare_eval_context_local(); let data = ctx.intern_global_name(name.as_str()); return ctx.emit_value( Op::EvalConstantFetch, diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index cda936e9f0..562ed28496 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1939,6 +1939,7 @@ fn emit_builtin_call_value( None => true, }; if php_symbol_key(name.trim_start_matches('\\')) == "eval" { + ctx.mark_eval_executed(); if eval_needs_barrier { ctx.apply_eval_barrier(); } else if eval_literal From a8793e10872d66099c0110fee42a983edae20ece Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:34:47 +0200 Subject: [PATCH 1174/1208] fix(ir_lower): drop count()'s statically-default mode argument Named and spread call plans re-materialize the optional mode default even when the source omits it, tripping the unary count contract in codegen. Prune mode:0 at the AST level and drop a trailing constant-zero mode operand after signature lowering. --- src/ir_lower/expr/mod.rs | 47 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 562ed28496..1a1eb591f2 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -5198,6 +5198,7 @@ fn lower_builtin_call_args( return Vec::new(); } match php_symbol_key(name.trim_start_matches('\\')).as_str() { + "count" => lower_count_args(ctx, sig, args), "date" => lower_date_args(ctx, sig, args), "eval" => lower_eval_args(ctx, sig, args), "json_decode" => lower_json_decode_args(ctx, sig, args), @@ -5223,6 +5224,52 @@ fn lower_builtin_call_args( } } +/// Lowers `count()` arguments, dropping a statically-default mode argument. +/// +/// The EIR backend implements only `COUNT_NORMAL`; a literal `0` mode (named +/// or positional) is semantically a no-op and would otherwise trip the unary +/// count contract in codegen. +fn lower_count_args( + ctx: &mut LoweringContext<'_, '_>, + sig: Option<&FunctionSig>, + args: &[Expr], +) -> Vec { + let pruned: Vec = args + .iter() + .enumerate() + .filter(|(index, arg)| !count_arg_is_static_default_mode(*index, arg)) + .map(|(_, arg)| arg.clone()) + .collect(); + let mut operands = lower_args_with_signature(ctx, sig, &pruned); + // Named and spread plans re-materialize the optional `mode` default even + // after the AST prune; a trailing constant-zero mode stays a no-op for + // the unary count contract, so drop the operand (DCE reclaims the const). + if operands.len() == 2 { + let trailing_zero_mode = ctx + .builder + .value_defining_instruction(operands[1]) + .is_some_and(|inst| { + inst.op == Op::ConstI64 + && matches!(inst.immediate, Some(crate::ir::Immediate::I64(0))) + }); + if trailing_zero_mode { + operands.pop(); + } + } + operands +} + +/// Returns true when a `count()` argument is a statically-zero mode. +fn count_arg_is_static_default_mode(index: usize, arg: &Expr) -> bool { + match &arg.kind { + ExprKind::NamedArg { name, value } => { + name == "mode" && matches!(value.kind, ExprKind::IntLiteral(0)) + } + ExprKind::IntLiteral(0) => index == 1, + _ => false, + } +} + /// Lowers eval's code operand and coerces it through PHP string-conversion rules. fn lower_eval_args( ctx: &mut LoweringContext<'_, '_>, From e123d912628360c7821e8c52390efeafb9cced28 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:34:47 +0200 Subject: [PATCH 1175/1208] fix(parser): follow PHP 8.3 auto-key semantics for explicit array keys Compute the next auto key from all int-normalizing key forms (bools, integral floats, canonical numeric strings, negated literals) and let the first integer-like key seed the cursor so leading negative keys continue from there instead of restarting at zero. --- src/parser/expr/prefix.rs | 72 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/src/parser/expr/prefix.rs b/src/parser/expr/prefix.rs index 3a07ae1bb0..1552df5b7d 100644 --- a/src/parser/expr/prefix.rs +++ b/src/parser/expr/prefix.rs @@ -488,6 +488,7 @@ fn parse_array_literal_with_terminator( let mut is_assoc = false; let mut first = true; let mut next_auto_key = 0i64; + let mut auto_key_initialized = false; while *pos < tokens.len() && tokens[*pos].0 != *closing { if !first { if tokens[*pos].0 != Token::Comma { @@ -519,15 +520,21 @@ fn parse_array_literal_with_terminator( is_assoc = true; *pos += 1; let value = parse_expr(tokens, pos)?; - update_next_auto_key_from_explicit_key(&expr, &mut next_auto_key); + update_next_auto_key_from_explicit_key( + &expr, + &mut next_auto_key, + &mut auto_key_initialized, + ); assoc_elems.push((expr, value)); } else if is_assoc { let key = Expr::new(ExprKind::IntLiteral(next_auto_key), expr.span); assoc_elems.push((key, expr)); next_auto_key += 1; + auto_key_initialized = true; } else { elems.push(expr); next_auto_key += 1; + auto_key_initialized = true; } first = false; } @@ -562,10 +569,65 @@ fn promote_indexed_array_items_to_assoc( } /// Advances the automatic integer key cursor after a statically known integer key. -fn update_next_auto_key_from_explicit_key(key: &Expr, next_auto_key: &mut i64) { - if let ExprKind::IntLiteral(value) = &key.kind { - if *value >= *next_auto_key { - *next_auto_key = *value + 1; +/// +/// The first integer-like key seeds the cursor unconditionally so a leading +/// negative key continues from there (PHP 8.3 semantics); later keys only +/// raise it. +fn update_next_auto_key_from_explicit_key( + key: &Expr, + next_auto_key: &mut i64, + auto_key_initialized: &mut bool, +) { + if let Some(value) = explicit_integer_array_key(key) { + let candidate = value.saturating_add(1); + if !*auto_key_initialized || candidate > *next_auto_key { + *next_auto_key = candidate; } + *auto_key_initialized = true; + } +} + +/// Returns the integer key PHP assigns to an explicit array key literal, +/// covering the int-normalizing forms: bools, integral floats, canonical +/// numeric strings, and negated numeric literals. +fn explicit_integer_array_key(key: &Expr) -> Option { + match &key.kind { + ExprKind::IntLiteral(value) => Some(*value), + ExprKind::BoolLiteral(value) => Some(i64::from(*value)), + ExprKind::FloatLiteral(value) => integral_float_array_key(*value), + ExprKind::StringLiteral(value) => php_integer_string_array_key(value), + ExprKind::Negate(inner) => match &inner.kind { + ExprKind::IntLiteral(value) => value.checked_neg(), + ExprKind::FloatLiteral(value) => integral_float_array_key(-*value), + _ => None, + }, + _ => None, + } +} + +/// Returns the integer key for a float literal PHP casts without truncation. +fn integral_float_array_key(value: f64) -> Option { + if !value.is_finite() || value.fract() != 0.0 { + return None; + } + if value < i64::MIN as f64 || value >= i64::MAX as f64 { + return None; + } + Some(value as i64) +} + +/// Returns the integer key for a canonical PHP integer string ("0", no +/// leading zeros, no "-0"); other strings stay string keys. +fn php_integer_string_array_key(value: &str) -> Option { + if value == "0" { + return value.parse().ok(); + } + let digits = value.strip_prefix('-').unwrap_or(value); + if digits.is_empty() + || digits.starts_with('0') + || !digits.bytes().all(|byte| byte.is_ascii_digit()) + { + return None; } + value.parse().ok() } From 258938da71d559d1bd431bc3eb0b4f828a39ceaf Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:34:47 +0200 Subject: [PATCH 1176/1208] fix(ir): keep directly-returned values of heap-typed functions unfolded Narrowing a returned Heap(Mixed) result to a raw scalar constant breaks the return ABI of internal eval AOT functions. --- src/ir_passes/const_fold.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/ir_passes/const_fold.rs b/src/ir_passes/const_fold.rs index e7fba8b56a..e1930a45ff 100644 --- a/src/ir_passes/const_fold.rs +++ b/src/ir_passes/const_fold.rs @@ -127,6 +127,31 @@ impl IrPass for ConstFold { return false; } + // Values returned by a heap-typed function must keep their boxed + // representation: narrowing a directly-returned Heap(Mixed) result to + // a raw scalar constant would break the return ABI (e.g. the fixed + // Heap(Mixed) contract of internal eval AOT functions). + if matches!(function.return_type, IrType::Heap(_)) { + let returned: std::collections::HashSet = function + .blocks + .iter() + .filter_map(|block| match &block.terminator { + Some(crate::ir::Terminator::Return { value }) => *value, + _ => None, + }) + .collect(); + folds.retain(|(inst_id, _, narrowing)| { + *narrowing == TypeNarrowing::None + || function + .instruction(*inst_id) + .and_then(|inst| inst.result) + .is_none_or(|result| !returned.contains(&result)) + }); + if folds.is_empty() { + return false; + } + } + // Phase 2 (mutate): rewrite each folded instruction in place to a constant. // When the fold narrows the result type (e.g. ICheckedAdd → ConstI64), // also update the instruction's result_type/result_php_type/ownership and From 0268ebf27929862d4b6412f1078809726690bccc Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:34:47 +0200 Subject: [PATCH 1177/1208] feat(codegen): lower array_key_exists for boxed Mixed containers Dispatch on the runtime tag: hashes probe __rt_hash_get, indexed arrays reuse the int-key bounds helper, other payloads answer false. Also accept float keys via integer-cast normalization in both key materializers. --- .../lower_inst/builtins/arrays/key_exists.rs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/codegen/lower_inst/builtins/arrays/key_exists.rs b/src/codegen/lower_inst/builtins/arrays/key_exists.rs index 9978ff51f7..e589047a14 100644 --- a/src/codegen/lower_inst/builtins/arrays/key_exists.rs +++ b/src/codegen/lower_inst/builtins/arrays/key_exists.rs @@ -26,6 +26,9 @@ pub(super) fn lower_array_key_exists(ctx: &mut FunctionContext<'_>, inst: &Instr match ctx.value_php_type(array)?.codegen_repr() { PhpType::Array(_) => lower_indexed_array_key_exists(ctx, inst, key, array), PhpType::AssocArray { .. } => lower_assoc_array_key_exists(ctx, inst, key, array), + PhpType::Mixed | PhpType::Union(_) => { + lower_mixed_container_key_exists(ctx, inst, key, array) + } other => Err(CodegenIrError::unsupported(format!( "array_key_exists for PHP array type {:?}", other @@ -33,6 +36,101 @@ pub(super) fn lower_array_key_exists(ctx: &mut FunctionContext<'_>, inst: &Instr } } +/// Lowers `array_key_exists()` for a boxed Mixed container by dispatching on +/// its runtime tag: hashes probe `__rt_hash_get`, indexed arrays reuse the +/// int-key bounds helper (the key normalizer already folds numeric strings to +/// integer keys), and non-container payloads answer false. +fn lower_mixed_container_key_exists( + ctx: &mut FunctionContext<'_>, + inst: &Instruction, + key: ValueId, + array: ValueId, +) -> Result<()> { + let hash_label = ctx.next_label("mixed_key_exists_hash"); + let indexed_label = ctx.next_label("mixed_key_exists_indexed"); + let missing_label = ctx.next_label("mixed_key_exists_missing"); + let done_label = ctx.next_label("mixed_key_exists_done"); + match ctx.emitter.target.arch { + Arch::AArch64 => { + // Normalize the key first: the helpers leave (key_lo, key_hi) in + // x1/x2 with key_hi == -1 marking integer keys. + materialize_hash_key_aarch64(ctx, key)?; + ctx.load_value_to_reg(array, "x9")?; + ctx.emitter + .instruction(&format!("cbz x9, {}", missing_label)); // null Mixed containers hold no keys + ctx.emitter.instruction("ldr x10, [x9]"); // load the container runtime tag + ctx.emitter.instruction("ldr x0, [x9, #8]"); // load the boxed payload pointer + ctx.emitter + .instruction(&format!("cbz x0, {}", missing_label)); // defensive payload null guard + ctx.emitter.instruction("cmp x10, #5"); // tag 5 = associative hash + ctx.emitter + .instruction(&format!("b.eq {}", hash_label)); // hashes probe the hash table + ctx.emitter.instruction("cmp x10, #4"); // tag 4 = indexed array + ctx.emitter + .instruction(&format!("b.eq {}", indexed_label)); // indexed arrays use the int-key helper + ctx.emitter + .instruction(&format!("b {}", missing_label)); // non-container payloads have no keys + + ctx.emitter.label(&indexed_label); + ctx.emitter.instruction("cmn x2, #1"); // key_hi == -1 marks an integer key + ctx.emitter + .instruction(&format!("b.ne {}", missing_label)); // string keys never exist in indexed arrays + abi::emit_call_label(ctx.emitter, "__rt_array_key_exists"); + ctx.emitter + .instruction(&format!("b {}", done_label)); // result already in x0 + + ctx.emitter.label(&hash_label); + abi::emit_call_label(ctx.emitter, "__rt_hash_get"); + ctx.emitter + .instruction(&format!("b {}", done_label)); // found flag already in x0 + + ctx.emitter.label(&missing_label); + ctx.emitter.instruction("mov x0, #0"); // missing/unsupported containers answer false + ctx.emitter.label(&done_label); + } + Arch::X86_64 => { + // Normalize the key first: the helpers leave (key_lo, key_hi) in + // rsi/rdx with key_hi == -1 marking integer keys. + materialize_hash_key_x86_64(ctx, key)?; + ctx.load_value_to_reg(array, "r10")?; + ctx.emitter.instruction("test r10, r10"); // null Mixed containers hold no keys + ctx.emitter + .instruction(&format!("jz {}", missing_label)); + ctx.emitter.instruction("mov r11, QWORD PTR [r10]"); // load the container runtime tag + ctx.emitter.instruction("mov rdi, QWORD PTR [r10 + 8]"); // load the boxed payload pointer + ctx.emitter.instruction("test rdi, rdi"); // defensive payload null guard + ctx.emitter + .instruction(&format!("jz {}", missing_label)); + ctx.emitter.instruction("cmp r11, 5"); // tag 5 = associative hash + ctx.emitter + .instruction(&format!("je {}", hash_label)); // hashes probe the hash table + ctx.emitter.instruction("cmp r11, 4"); // tag 4 = indexed array + ctx.emitter + .instruction(&format!("je {}", indexed_label)); // indexed arrays use the int-key helper + ctx.emitter + .instruction(&format!("jmp {}", missing_label)); // non-container payloads have no keys + + ctx.emitter.label(&indexed_label); + ctx.emitter.instruction("cmp rdx, -1"); // key_hi == -1 marks an integer key + ctx.emitter + .instruction(&format!("jne {}", missing_label)); // string keys never exist in indexed arrays + abi::emit_call_label(ctx.emitter, "__rt_array_key_exists"); + ctx.emitter + .instruction(&format!("jmp {}", done_label)); // result already in rax + + ctx.emitter.label(&hash_label); + abi::emit_call_label(ctx.emitter, "__rt_hash_get"); + ctx.emitter + .instruction(&format!("jmp {}", done_label)); // found flag already in rax + + ctx.emitter.label(&missing_label); + ctx.emitter.instruction("xor eax, eax"); // missing/unsupported containers answer false + ctx.emitter.label(&done_label); + } + } + store_if_result(ctx, inst) +} + /// Lowers indexed-array key existence through the bounds-check runtime helper. fn lower_indexed_array_key_exists( ctx: &mut FunctionContext<'_>, @@ -90,6 +188,12 @@ fn materialize_hash_key_aarch64(ctx: &mut FunctionContext<'_>, key: ValueId) -> abi::emit_load_int_immediate(ctx.emitter, "x2", -1); Ok(()) } + PhpType::Float => { + ctx.load_value_to_reg(key, "d0")?; + ctx.emitter.instruction("fcvtzs x1, d0"); // PHP casts float array keys to integer keys + abi::emit_load_int_immediate(ctx.emitter, "x2", -1); + Ok(()) + } // PHP null normalizes to the empty string "" as an array key. PhpType::Void | PhpType::Never => { let (label, len) = ctx.data.add_string(b""); @@ -119,6 +223,12 @@ fn materialize_hash_key_x86_64(ctx: &mut FunctionContext<'_>, key: ValueId) -> R abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); Ok(()) } + PhpType::Float => { + ctx.load_value_to_reg(key, "xmm0")?; + ctx.emitter.instruction("cvttsd2si rsi, xmm0"); // PHP casts float array keys to integer keys + abi::emit_load_int_immediate(ctx.emitter, "rdx", -1); + Ok(()) + } // PHP null normalizes to the empty string "" as an array key. PhpType::Void | PhpType::Never => { let (label, len) = ctx.data.add_string(b""); From d241ef22103c25772339351860136573d09dfa2d Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:34:47 +0200 Subject: [PATCH 1178/1208] fix(eval): reload container locals from scope cells after barrier sync Unbox object/array/hash payload pointers when copying a scope cell back into a native local, and fall back to the null pointer when eval removed the entry. --- src/codegen/lower_inst/builtins/eval.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index eddcc33f31..cb4ce22bf7 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -10609,15 +10609,17 @@ fn store_mixed_scope_cell_to_local( abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_string"); ctx.store_current_result_to_local(local.slot)?; } - PhpType::Object(_) => { + PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + // Objects, arrays, and hashes are heap pointers boxed in the + // scope cell; unbox and store the raw payload pointer. abi::emit_call_label(ctx.emitter, "__rt_mixed_unbox"); - let object_reg = match ctx.emitter.target.arch { + let payload_reg = match ctx.emitter.target.arch { Arch::AArch64 => "x1", Arch::X86_64 => "rdi", }; let result_reg = abi::int_result_reg(ctx.emitter); ctx.emitter - .instruction(&format!("mov {}, {}", result_reg, object_reg)); // move unboxed object pointer into the local-store result register + .instruction(&format!("mov {}, {}", result_reg, payload_reg)); // move the unboxed heap pointer into the local-store result register ctx.store_current_result_to_local(local.slot)?; } other => { @@ -10728,7 +10730,9 @@ fn store_missing_scope_entry_to_local( abi::emit_load_int_immediate(ctx.emitter, len_reg, 0); ctx.store_current_result_to_local(local.slot)?; } - PhpType::Object(_) => { + PhpType::Object(_) | PhpType::Array(_) | PhpType::AssocArray { .. } => { + // Heap-pointer locals fall back to the null pointer when eval + // removed the entry, matching the object fallback. abi::emit_load_int_immediate(ctx.emitter, abi::int_result_reg(ctx.emitter), 0); ctx.store_current_result_to_local(local.slot)?; } From 91b3bad6394f2b472504bf2001e9f4a25f6ef739 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:35:02 +0200 Subject: [PATCH 1179/1208] feat(codegen): dispatch is_array/is_integer as runtime string callbacks Both predicates have complete Mixed-tag lowerings and registry/legacy first-class signatures; only the dispatch allowlist excluded them. --- src/codegen_support/callable_dispatch.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/codegen_support/callable_dispatch.rs b/src/codegen_support/callable_dispatch.rs index 4e47bd02eb..4afc9173a5 100644 --- a/src/codegen_support/callable_dispatch.rs +++ b/src/codegen_support/callable_dispatch.rs @@ -113,6 +113,9 @@ pub(crate) fn runtime_builtin_wrapper_supported( "strtolower" | "strtoupper" | "trim" => source_arg_ty.is_none_or(|source_arg_ty| { matches!(source_arg_ty, PhpType::Str) }), + // Type predicates take a Mixed wrapper param; concrete source types + // fold to a static boolean in the predicate lowering. + "is_array" | "is_integer" => true, "gettype" => true, _ => false, } @@ -127,6 +130,8 @@ fn runtime_builtin_name_supported(name: &str) -> bool { | "floatval" | "gettype" | "intval" + | "is_array" + | "is_integer" | "strlen" | "strtolower" | "strtoupper" From 5cbb321e009bc3107356bccd1d4b7a07d28c5fc8 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:35:02 +0200 Subject: [PATCH 1180/1208] fix(reflection): accept indexed argument lists in newInstanceArgs The checker resolves a bare array param to a string-keyed map, rejecting newInstanceArgs(["Q", "R"]); type the param iterable so both array shapes pass. --- src/types/checker/builtin_types/reflection.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/types/checker/builtin_types/reflection.rs b/src/types/checker/builtin_types/reflection.rs index 6a79b12b14..4b27e7e033 100644 --- a/src/types/checker/builtin_types/reflection.rs +++ b/src/types/checker/builtin_types/reflection.rs @@ -714,7 +714,15 @@ fn builtin_reflection_class_new_instance_args_method() -> ClassMethod { is_abstract: false, is_final: false, has_body: true, - params: vec![("args".to_string(), Some(array_type()), empty_array(), false)], + // `iterable` rather than bare `array`: the checker resolves `array` + // to a string-keyed map, which would reject indexed argument lists; + // Iterable accepts both array shapes. + params: vec![( + "args".to_string(), + Some(TypeExpr::Iterable), + empty_array(), + false, + )], param_attributes: Vec::new(), variadic: None, variadic_by_ref: false, From 092bd6d9166ef6841e36a08ff9563edd3f3f2956 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:35:02 +0200 Subject: [PATCH 1181/1208] fix(codegen): box nested attribute arg arrays with the assoc tag emit_mixed_array allocates a hash, but nested values were boxed with the indexed-array tag, so Mixed readers dispatched the payload to the wrong container helpers and returned garbage elements. --- src/codegen/lower_inst/builtins/attributes.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/codegen/lower_inst/builtins/attributes.rs b/src/codegen/lower_inst/builtins/attributes.rs index 1e575e038a..a5f97aee03 100644 --- a/src/codegen/lower_inst/builtins/attributes.rs +++ b/src/codegen/lower_inst/builtins/attributes.rs @@ -557,12 +557,17 @@ fn allocate_mixed_hash(ctx: &mut FunctionContext<'_>, capacity: usize) { fn emit_box_arg(ctx: &mut FunctionContext<'_>, arg: &AttrArgValue) -> Result<()> { if let AttrArgValue::Array(entries) = arg { // Build the nested array (leaves it in the result reg), then box the - // array pointer as a Mixed cell with the array runtime tag. The parent - // array being filled stays parked on the temporary stack across this. + // pointer as a Mixed cell. emit_mixed_array allocates a hash, so the + // cell must carry the assoc tag or Mixed readers dispatch the payload + // to the indexed-array helpers. The parent array being filled stays + // parked on the temporary stack across this. emit_mixed_array(ctx, entries)?; crate::codegen::emit_box_current_value_as_mixed( ctx.emitter, - &PhpType::Array(Box::new(PhpType::Mixed)), + &PhpType::AssocArray { + key: Box::new(PhpType::Mixed), + value: Box::new(PhpType::Mixed), + }, ); return Ok(()); } From e884b4309cb362aff48e741b1e326559ed9075da Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:35:02 +0200 Subject: [PATCH 1182/1208] fix(builtins): keep by-ref params undeclared in first-class callable sigs Registry by-ref params are typed Mixed by generality; marking them declared made the checker demand boxed storage from every argument variable, regressing sort(...) over plain arrays. Reflection hasType is unchanged: Mixed params without a type expression already report false. --- src/builtins/registry.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/builtins/registry.rs b/src/builtins/registry.rs index 17bcff99c4..2df2faf9be 100644 --- a/src/builtins/registry.rs +++ b/src/builtins/registry.rs @@ -196,7 +196,13 @@ pub fn first_class_callable_sig(name: &str) -> Option { let mut fcc_sig = callable_wrapper_sig(&sig); refine_first_class_callable_sig(name, &mut fcc_sig); fcc_sig.declared_return = true; - fcc_sig.declared_params = vec![true; fcc_sig.params.len()]; + // Mark params declared for reflection hasType, but keep by-ref params + // undeclared: their registry type is Mixed by generality, and a declared + // Mixed by-ref param would make the checker demand boxed storage from + // every argument variable (regressing e.g. `sort(...)` on plain arrays). + fcc_sig.declared_params = (0..fcc_sig.params.len()) + .map(|index| !fcc_sig.ref_params.get(index).copied().unwrap_or(false)) + .collect(); Some(fcc_sig) } From 71d89131972009d50a50a2c6e1321262ccbd03ca Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:35:02 +0200 Subject: [PATCH 1183/1208] test(eval): assert full print_r/var_dump output shapes print_r now prints the complete Array block and var_dump the PHP-style object dump; normalize runtime object ids before comparing. --- tests/codegen/eval.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index 284b9619d7..cf27531b7a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -4635,7 +4635,7 @@ echo ":" . ($call ? "call" : "bad") . ":" . ($spread ? "spread" : "bad") . ":"; echo function_exists("print_r");'); "#, ); - assert_eq!(out, "x::Array\n:1z:call:spread:1"); + assert_eq!(out, "x::Array\n(\n [0] => 1\n [1] => 2\n)\n:1z:call:spread:1"); } /// Verifies eval `var_dump()` writes PHP-style diagnostics and returns null. @@ -4690,7 +4690,26 @@ var_dump(new EvalDynamicDumpBox()); var_dump(new EvalAotDumpBox());'); "#, ); - assert_eq!(out, "object(EvalDynamicDumpBox)\nobject(EvalAotDumpBox)\n"); + // Object ids are runtime handles and vary per run; normalize them before + // comparing the PHP-style dump shape. + let normalized: String = { + let mut result = String::new(); + let mut chars = out.chars().peekable(); + while let Some(ch) = chars.next() { + result.push(ch); + if ch == '#' { + while chars.peek().is_some_and(char::is_ascii_digit) { + chars.next(); + } + result.push('N'); + } + } + result + }; + assert_eq!( + normalized, + "object(EvalDynamicDumpBox)#N (0) {\n}\nobject(EvalAotDumpBox)#N (0) {\n}\n" + ); } /// Verifies eval fragments with comments and parser-lowered `__LINE__` use EIR AOT. From 0d3656625e2dce9c8da37e58b0baa211e2c96cac Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Fri, 10 Jul 2026 18:40:48 +0200 Subject: [PATCH 1184/1208] fix(eval): widen scalar slots for type-changing direct eval stores A literal eval storing a different scalar type into an existing local (e.g. $x = 1; eval('$x = "changed";')) previously fell back to the scope path, whose write-back cast the value through the stale slot type. Widen the slot to boxed Mixed storage and keep the native direct-store path; codegen re-lowers every load/store with the final slot type. --- src/ir_lower/expr/mod.rs | 61 +++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/src/ir_lower/expr/mod.rs b/src/ir_lower/expr/mod.rs index 1a1eb591f2..a4cf283cf5 100644 --- a/src/ir_lower/expr/mod.rs +++ b/src/ir_lower/expr/mod.rs @@ -1940,6 +1940,16 @@ fn emit_builtin_call_value( }; if php_symbol_key(name.trim_start_matches('\\')) == "eval" { ctx.mark_eval_executed(); + if let Some(widen_targets) = + eval_literal.and_then(|fragment| eval_literal_direct_store_widen_targets(ctx, fragment)) + { + // A direct store that changes a scalar slot's type keeps the + // native path by widening the slot to boxed Mixed storage first; + // codegen re-lowers every load/store with the final slot type. + for name in widen_targets { + ctx.set_local_type(&name, PhpType::Mixed); + } + } if eval_needs_barrier { ctx.apply_eval_barrier(); } else if eval_literal @@ -2037,34 +2047,57 @@ fn eval_literal_needs_scope_barrier(ctx: &LoweringContext<'_, '_>, fragment: &st } /// Returns true when a direct-store eval fragment fits every caller slot type. -/// Mirrors codegen's per-target checks: a store that changes an existing -/// slot's scalar type (e.g. Int -> Str) needs the barrier and a wider path. fn eval_literal_direct_store_supported_by_lowering( ctx: &LoweringContext<'_, '_>, fragment: &str, ) -> bool { - let Some(writes) = crate::eval_aot::literal_fragment_direct_local_store_writes(fragment) - else { - return false; - }; - writes.iter().all(|(name, kind)| { + eval_literal_direct_store_widen_targets(ctx, fragment).is_some() +} + +/// Classifies a direct-store eval fragment against caller slots. Returns the +/// locals whose scalar slot must widen to Mixed because the store changes +/// their runtime type (e.g. Int -> Str); `None` when any write target rules +/// out the direct path entirely. +fn eval_literal_direct_store_widen_targets( + ctx: &LoweringContext<'_, '_>, + fragment: &str, +) -> Option> { + let writes = crate::eval_aot::literal_fragment_direct_local_store_writes(fragment)?; + let mut widen_targets = Vec::new(); + for (name, kind) in &writes { if crate::superglobals::is_superglobal(name) || (ctx.in_main && ctx.all_global_var_names.contains(name)) { - return false; + return None; } if ctx.local_slots.get(name).is_none() { - return true; + continue; } if ctx.is_ref_bound_local(name) || ctx.local_kinds.get(name).copied() != Some(LocalKind::PhpLocal) { - return false; + return None; } - ctx.local_types - .get(name) - .is_some_and(|ty| eval_literal_direct_store_type_supported(ty, *kind)) - }) + let ty = ctx.local_types.get(name)?; + if eval_literal_direct_store_type_supported(ty, *kind) { + continue; + } + if eval_literal_direct_store_type_widenable(ty) { + widen_targets.push(name.clone()); + } else { + return None; + } + } + Some(widen_targets) +} + +/// Returns true when a scalar slot can widen to Mixed for a type-changing +/// direct eval store. Containers and special storage keep the barrier path. +fn eval_literal_direct_store_type_widenable(ty: &PhpType) -> bool { + matches!( + ty.codegen_repr(), + PhpType::Int | PhpType::Float | PhpType::Bool | PhpType::Str | PhpType::TaggedScalar + ) } /// Returns true when a static scalar store kind fits an existing caller slot type. From a03ffe5fba3cdb5ec1531308c1002a6286b6f1fd Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 00:14:52 +0200 Subject: [PATCH 1185/1208] fix(magician): dispatch calling scope's private method over child overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHP resolves $obj->m() from class scope S to S's own private instance method when S declares one and the receiver is an instance of S, even if a subclass overrides m publicly. The interpreter resolved metadata from the receiver's runtime class, found the override, and sent its class as the bridge scope — masking the ambient scope the AOT caller had pushed. Check the calling scope first and dispatch with it directly; the native bridge's hidden private shadow slots already select the right symbol from the scope string. Applies to direct calls and callable arrays. --- .../interpreter/builtins/registry/callable.rs | 18 ++++++ .../src/interpreter/statements.rs | 55 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index c95a457285..c72eab8d75 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -1130,6 +1130,24 @@ fn eval_native_object_method_call_user_func_result_for_class( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result, EvalStatus> { + if let Some(shadow_scope) = + eval_private_scope_shadow_bridge_scope(class_name, method_name, context, values)? + { + // The calling scope's own private method shadows any override on the + // receiver's class (PHP private-method shadowing); dispatch with the + // scope string so the native shadow slot selects it directly. + return eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&shadow_scope), + called_class_scope, + context, + values, + ) + .map(Some); + } let Some((declaring_class, visibility, is_static, is_abstract)) = eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? else { diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 748023d488..c6fd196a12 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -10241,6 +10241,42 @@ fn native_class_is_a(class_name: &str, target: &str, context: &ElephcEvalContext } } +/// Returns the calling-scope class when PHP's private-method shadowing rule +/// selects it as the dispatch scope: `$obj->m()` from class scope S calls S's +/// own private instance method `m` — never the receiver's override — when S +/// declares one and the receiver is an instance of S. The returned scope +/// string drives the native bridge's hidden private shadow slot directly. +pub(in crate::interpreter) fn eval_private_scope_shadow_bridge_scope( + receiver_class: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(scope_class) = context.current_class_scope() else { + return Ok(None); + }; + let scope_class = scope_class.to_string(); + if !same_eval_class_name(receiver_class, &scope_class) + && !native_class_is_a(receiver_class, &scope_class, context) + { + return Ok(None); + } + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&scope_class, method_name, values)? + else { + return Ok(None); + }; + if visibility == EvalVisibility::Private + && !is_static + && !is_abstract + && same_eval_class_name(&declaring_class, &scope_class) + { + Ok(Some(declaring_class)) + } else { + Ok(None) + } +} + /// Binds method parameters into a fresh method scope and marks by-reference params as aliases. pub(in crate::interpreter) fn bind_method_scope_args( method_scope: &mut ElephcEvalScope, @@ -11420,6 +11456,25 @@ fn eval_native_method_with_evaluated_args_bridge_scope( values: &mut impl RuntimeValueOps, ) -> Result { let mut resolved_bridge_scope = bridge_scope.map(str::to_string); + if resolved_bridge_scope.is_none() { + if let Some(shadow_scope) = + eval_private_scope_shadow_bridge_scope(class_name, method_name, context, values)? + { + // The calling scope's own private method shadows any override on + // the receiver's class; access is inherently allowed, so skip the + // hierarchy resolution (it would find the override instead). + return eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&shadow_scope), + called_class_scope, + context, + values, + ); + } + } let metadata = eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; if let Some((declaring_class, visibility, _, is_abstract)) = metadata { From 794b4a85d448b745464aa726b3d30ff3c77e9652 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 00:40:00 +0200 Subject: [PATCH 1186/1208] fix(eval): handle PhpType::False and TypeExpr::False from main Extend eval result casting (False lowers as Bool) and native type-spec formatting ("false") for the literal-false type introduced by main's narrowing work. --- src/codegen/lower_inst/builtins/eval.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index cb4ce22bf7..c3a8415c18 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -6166,7 +6166,7 @@ fn emit_eval_result_as_type(ctx: &mut FunctionContext<'_>, result_ty: &PhpType) abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_int"); Ok(()) } - PhpType::Bool => { + PhpType::Bool | PhpType::False => { abi::emit_call_label(ctx.emitter, "__rt_mixed_cast_bool"); Ok(()) } @@ -7328,6 +7328,7 @@ fn eval_native_type_expr_spec(type_expr: &TypeExpr) -> Option { TypeExpr::Int => Some("int".to_string()), TypeExpr::Float => Some("float".to_string()), TypeExpr::Bool => Some("bool".to_string()), + TypeExpr::False => Some("false".to_string()), TypeExpr::Str => Some("string".to_string()), TypeExpr::Void => Some("null".to_string()), TypeExpr::Never => None, @@ -7360,6 +7361,7 @@ fn eval_native_php_type_spec(php_type: &PhpType, allow_return_atoms: bool) -> Op PhpType::Float => Some("float".to_string()), PhpType::Str => Some("string".to_string()), PhpType::Bool => Some("bool".to_string()), + PhpType::False => Some("false".to_string()), PhpType::Void if allow_return_atoms => Some("void".to_string()), PhpType::Void => Some("null".to_string()), PhpType::Never if allow_return_atoms => Some("never".to_string()), From 12ce395a448da7ed82aac471c70b2200ef031e21 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 00:47:13 +0200 Subject: [PATCH 1187/1208] test: align stale expectations with branch semantics class_exists() accepts a dynamic autoload flag (never an AOT autoload demand; class_parents keeps the literal-flag coverage) and stacked parameter attributes now persist in the AST as ordered groups. --- tests/error_tests/callables.rs | 15 +++++---------- tests/parser_tests/attributes.rs | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/error_tests/callables.rs b/tests/error_tests/callables.rs index 300ab0eb20..c2c98a0f88 100644 --- a/tests/error_tests/callables.rs +++ b/tests/error_tests/callables.rs @@ -42,16 +42,11 @@ fn test_error_class_exists_requires_literal_name() { ); } -/// Verifies that error class exists requires literal autoload flag. -#[test] -fn test_error_class_exists_requires_literal_autoload_flag() { - // Verifies `class_exists()` with a runtime variable as the autoload flag - // produces a diagnostic because AOT mode requires a literal bool or int. - expect_error( - r#" { + assert_eq!(param_attributes.len(), 1); + assert_eq!(param_attributes[0].len(), 2); + assert_eq!(param_attributes[0][0].attributes[0].name.as_str(), "A"); + assert_eq!(param_attributes[0][1].attributes[0].name.as_str(), "B"); + } + other => panic!("expected FunctionDecl, got {:?}", other), + } } // -- Persistence: attributes are now captured in the AST -- From 9a878687e7e58a80a887c2965668d137e292073a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 00:48:05 +0200 Subject: [PATCH 1188/1208] docs: regenerate builtin pages after rebase onto 0.26.1 --- .../builtins/array/array_key_exists.md | 3 +- docs/internals/builtins/array/count.md | 2 +- docs/internals/builtins/misc/empty.md | 2 +- docs/internals/builtins/string/strlen.md | 4 +- docs/internals/builtins/type/boolval.md | 4 +- docs/internals/builtins/type/floatval.md | 4 +- docs/internals/builtins/type/intval.md | 4 +- docs/internals/builtins/type/is_array.md | 4 +- docs/internals/builtins/type/is_bool.md | 4 +- docs/internals/builtins/type/is_float.md | 4 +- docs/internals/builtins/type/is_int.md | 4 +- docs/internals/builtins/type/is_iterable.md | 4 +- docs/internals/builtins/type/is_null.md | 4 +- docs/internals/builtins/type/is_object.md | 4 +- docs/internals/builtins/type/is_scalar.md | 4 +- docs/internals/builtins/type/is_string.md | 4 +- scripts/docs/builtin_registry.json | 302 +++++++++++------- 17 files changed, 221 insertions(+), 140 deletions(-) diff --git a/docs/internals/builtins/array/array_key_exists.md b/docs/internals/builtins/array/array_key_exists.md index d75a3f886a..25f1d8abdb 100644 --- a/docs/internals/builtins/array/array_key_exists.md +++ b/docs/internals/builtins/array/array_key_exists.md @@ -20,7 +20,8 @@ sidebar: ## Runtime helpers -_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ +The following runtime helpers are referenced: +- `__rt_hash_get` ## Signature summary diff --git a/docs/internals/builtins/array/count.md b/docs/internals/builtins/array/count.md index 0742c2bead..0d86ae6419 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/array/count.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1024](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1024) (`lower_count`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1025](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1025) (`lower_count`) - **Function symbol**: `lower_count()` diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md index df66edec21..4acdb8052b 100644 --- a/docs/internals/builtins/misc/empty.md +++ b/docs/internals/builtins/misc/empty.md @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/types/signatures.rs`](https://github.com/illegalstudio/elephc/blob/main/src/types/signatures.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1217](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1217) (`lower_empty`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1218](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1218) (`lower_empty`) - **Function symbol**: `lower_empty()` diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index 0997839a10..973fff9031 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen() — internals" description: "Compiler internals for strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 392 + order: 393 --- ## `strlen()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/string/strlen.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1078](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1078) (`lower_strlen`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1079](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1079) (`lower_strlen`) - **Function symbol**: `lower_strlen()` diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index 5dd2a60d39..6a6e8a3262 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval() — internals" description: "Compiler internals for boolval(): lowering path, type checks, and runtime helpers." sidebar: - order: 409 + order: 410 --- ## `boolval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/boolval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1175](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1175) (`lower_boolval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1176](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1176) (`lower_boolval`) - **Function symbol**: `lower_boolval()` diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index 95c974ddc4..5d1c9b0e39 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval() — internals" description: "Compiler internals for floatval(): lowering path, type checks, and runtime helpers." sidebar: - order: 414 + order: 415 --- ## `floatval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/floatval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1141](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1141) (`lower_floatval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1142](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1142) (`lower_floatval`) - **Function symbol**: `lower_floatval()` diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index dc2b310f0f..9c8d1c45ae 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval() — internals" description: "Compiler internals for intval(): lowering path, type checks, and runtime helpers." sidebar: - order: 418 + order: 419 --- ## `intval()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/intval.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1108](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1108) (`lower_intval`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1109](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1109) (`lower_intval`) - **Function symbol**: `lower_intval()` diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index 078e9615ae..3d39b6837d 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array() — internals" description: "Compiler internals for is_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 419 + order: 420 --- ## `is_array()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_array.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1601](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1601) (`lower_is_array`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1603](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1603) (`lower_is_array`) - **Function symbol**: `lower_is_array()` diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index f3e0deb682..4a2e5cb2ba 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool() — internals" description: "Compiler internals for is_bool(): lowering path, type checks, and runtime helpers." sidebar: - order: 420 + order: 421 --- ## `is_bool()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_bool.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1335](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1335) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index 8b7e6f3485..22cc335d43 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float() — internals" description: "Compiler internals for is_float(): lowering path, type checks, and runtime helpers." sidebar: - order: 422 + order: 423 --- ## `is_float()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_float.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1335](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1335) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index a2a9ecfbc8..b38c7e5445 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int() — internals" description: "Compiler internals for is_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 423 + order: 424 --- ## `is_int()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_int.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1335](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1335) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index cd4aff2f91..982c72bbbb 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable() — internals" description: "Compiler internals for is_iterable(): lowering path, type checks, and runtime helpers." sidebar: - order: 424 + order: 425 --- ## `is_iterable()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_iterable.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1393](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1393) (`lower_is_iterable`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1394](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1394) (`lower_is_iterable`) - **Function symbol**: `lower_is_iterable()` diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index 2a0b9611e0..5ed5435294 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null() — internals" description: "Compiler internals for is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 425 + order: 426 --- ## `is_null()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_null.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1591](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1591) (`lower_is_null_builtin`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1593](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1593) (`lower_is_null_builtin`) - **Function symbol**: `lower_is_null_builtin()` diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index 11ab42720c..fd162efa8f 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object() — internals" description: "Compiler internals for is_object(): lowering path, type checks, and runtime helpers." sidebar: - order: 427 + order: 428 --- ## `is_object()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_object.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1616](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1616) (`lower_is_object`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1618](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1618) (`lower_is_object`) - **Function symbol**: `lower_is_object()` diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index 5bad78af8b..08b141ddeb 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar() — internals" description: "Compiler internals for is_scalar(): lowering path, type checks, and runtime helpers." sidebar: - order: 429 + order: 430 --- ## `is_scalar()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_scalar.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1632](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1632) (`lower_is_scalar`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1634](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1634) (`lower_is_scalar`) - **Function symbol**: `lower_is_scalar()` diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index f5058c3a97..2b64474ffe 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string() — internals" description: "Compiler internals for is_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 430 + order: 431 --- ## `is_string()` — internals @@ -10,7 +10,7 @@ sidebar: ## Where it lives - **Signature**: [`src/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/src/builtins/types/is_string.rs) -- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1335](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1335) (`lower_static_type_predicate`) +- **Lowering**: [`src/codegen/lower_inst/builtins.rs`:1336](https://github.com/illegalstudio/elephc/blob/main/src/codegen/lower_inst/builtins.rs#L1336) (`lower_static_type_predicate`) - **Function symbol**: `lower_static_type_predicate()` diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index c34e36e4be..4aebd39aad 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -934,8 +934,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/addslashes.rs", @@ -1718,7 +1717,9 @@ "notes": [ "Lowers `array_key_exists()` for indexed arrays and associative arrays." ], - "runtime_helpers": [], + "runtime_helpers": [ + "__rt_hash_get" + ], "sig_arm": null, "sig_file": "src/builtins/array/array_key_exists.rs", "sig_line": null @@ -3173,8 +3174,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/base64_decode.rs", @@ -3213,8 +3213,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/base64_encode.rs", @@ -3297,8 +3296,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/bin2hex.rs", @@ -3332,7 +3330,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_boolval", - "codegen_line": 1175, + "codegen_line": 1176, "notes": [ "Lowers `boolval()` using the same concrete scalar PHP truthiness rules as `IsTruthy`." ], @@ -3794,7 +3792,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", - "codegen_line": 112, + "codegen_line": 130, "notes": [ "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], @@ -3884,7 +3882,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_chr", - "codegen_line": 858, + "codegen_line": 876, "notes": [ "Lowers `chr()` by converting an integer code point into a one-byte string." ], @@ -4530,7 +4528,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_count", - "codegen_line": 1024, + "codegen_line": 1025, "notes": [ "Lowers `count(array)` for concrete array values by reading the runtime length header.", "Called from `crate::builtins::array::count` (the registry home) via a thin wrapper.", @@ -4580,7 +4578,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_crc32", - "codegen_line": 348, + "codegen_line": 366, "notes": [ "Lowers `crc32(string)` through the shared checksum runtime helper." ], @@ -5174,7 +5172,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_empty", - "codegen_line": 1217, + "codegen_line": 1218, "notes": [ "Lowers `empty()` for concrete scalar and array-like operands." ], @@ -5366,7 +5364,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_explode", - "codegen_line": 151, + "codegen_line": 169, "notes": [ "Lowers `explode(delimiter, string)` into the shared string-array splitter helper." ], @@ -6297,7 +6295,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_floatval", - "codegen_line": 1141, + "codegen_line": 1142, "notes": [ "Lowers `floatval()` for concrete scalar operands." ], @@ -8021,7 +8019,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_grapheme_strrev", - "codegen_line": 88, + "codegen_line": 106, "notes": [ "Lowers `grapheme_strrev()` and boxes its `string|false` result as `Mixed`." ], @@ -8061,7 +8059,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzcompress", - "codegen_line": 402, + "codegen_line": 420, "notes": [ "Lowers `gzcompress(data, level?)` through inline zlib `compress2` calls." ], @@ -8105,7 +8103,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzdeflate", - "codegen_line": 418, + "codegen_line": 436, "notes": [ "Lowers `gzdeflate(data, level?)` through inline raw-DEFLATE zlib calls." ], @@ -8149,7 +8147,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzinflate", - "codegen_line": 436, + "codegen_line": 454, "notes": [ "Lowers `gzinflate(data, max_length?)` and boxes zlib failures as PHP false." ], @@ -8193,7 +8191,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_gzuncompress", - "codegen_line": 457, + "codegen_line": 475, "notes": [ "Lowers `gzuncompress(data, max_length?)` and boxes zlib failures as PHP false." ], @@ -8239,7 +8237,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash", - "codegen_line": 209, + "codegen_line": 227, "notes": [ "Lowers `hash(algo, data, binary?)` through the shared runtime digest dispatcher." ], @@ -8292,7 +8290,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_algos", - "codegen_line": 254, + "codegen_line": 272, "notes": [ "Lowers `hash_algos()` through the runtime algorithm-list builder." ], @@ -8324,7 +8322,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_copy", - "codegen_line": 333, + "codegen_line": 351, "notes": [ "Lowers `hash_copy(context)` through the incremental hash clone helper." ], @@ -8366,7 +8364,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_equals", - "codegen_line": 247, + "codegen_line": 265, "notes": [ "Lowers `hash_equals(known, user)` through the timing-safe runtime compare helper." ], @@ -8465,7 +8463,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_final", - "codegen_line": 302, + "codegen_line": 320, "notes": [ "Lowers `hash_final(context, binary?)` through the incremental hash finalizer." ], @@ -8511,7 +8509,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_hmac", - "codegen_line": 228, + "codegen_line": 246, "notes": [ "Lowers `hash_hmac(algo, data, key, binary?)` through the shared HMAC runtime dispatcher." ], @@ -8572,7 +8570,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_init", - "codegen_line": 266, + "codegen_line": 284, "notes": [ "Lowers `hash_init(algo)` and returns a boxed HashContext resource." ], @@ -8625,7 +8623,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_hash_update", - "codegen_line": 277, + "codegen_line": 295, "notes": [ "Lowers `hash_update(context, data)` through the incremental hash runtime helper." ], @@ -8734,8 +8732,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/hex2bin.rs", @@ -8818,8 +8815,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/html_entity_decode.rs", @@ -8852,13 +8848,19 @@ "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_function": "lower_html_escape", + "codegen_line": 93, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." + "Lowers `htmlspecialchars()` / `htmlentities()` \u2014 escapes the subject string (operand 0).", + "`name` is the calling builtin's PHP name, used in argument-coercion diagnostics. The", + "optional `flags` and `encoding` arguments are accepted (so the common `htmlspecialchars($s,", + "ENT_QUOTES)` call form compiles) but not applied: `__rt_htmlspecialchars` implements the", + "ENT_QUOTES behaviour, which matches PHP's default flag set and the overwhelmingly-common", + "ENT_QUOTES call. (A flag-aware runtime \u2014 doctype-dependent `'` vs `'` \u2014 is a follow-up.)" ], "runtime_helpers": [ "__rt_grapheme_strrev", + "__rt_htmlspecialchars", "__rt_strcopy" ], "sig_arm": null, @@ -8874,6 +8876,20 @@ "name": "string", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "11", + "name": "flags", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "'UTF-8'", + "name": "encoding", + "optional": true, + "type": "string" } ], "return_type": "string", @@ -8892,13 +8908,19 @@ "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", - "codegen_function": "lower_unary_string_runtime", - "codegen_line": 76, + "codegen_function": "lower_html_escape", + "codegen_line": 93, "notes": [ - "Lowers a one-argument string builtin that directly delegates to a runtime helper." + "Lowers `htmlspecialchars()` / `htmlentities()` \u2014 escapes the subject string (operand 0).", + "`name` is the calling builtin's PHP name, used in argument-coercion diagnostics. The", + "optional `flags` and `encoding` arguments are accepted (so the common `htmlspecialchars($s,", + "ENT_QUOTES)` call form compiles) but not applied: `__rt_htmlspecialchars` implements the", + "ENT_QUOTES behaviour, which matches PHP's default flag set and the overwhelmingly-common", + "ENT_QUOTES call. (A flag-aware runtime \u2014 doctype-dependent `'` vs `'` \u2014 is a follow-up.)" ], "runtime_helpers": [ "__rt_grapheme_strrev", + "__rt_htmlspecialchars", "__rt_strcopy" ], "sig_arm": null, @@ -8914,6 +8936,20 @@ "name": "string", "optional": false, "type": "string" + }, + { + "by_ref": false, + "default": "11", + "name": "flags", + "optional": true, + "type": "int" + }, + { + "by_ref": false, + "default": "'UTF-8'", + "name": "encoding", + "optional": true, + "type": "string" } ], "return_type": "string", @@ -9020,7 +9056,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_implode", - "codegen_line": 192, + "codegen_line": 210, "notes": [ "Lowers `implode(glue, array)` by selecting the string or integer array helper." ], @@ -9115,7 +9151,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_inet", - "codegen_line": 497, + "codegen_line": 515, "notes": [ "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." ], @@ -9154,7 +9190,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_inet", - "codegen_line": 497, + "codegen_line": 515, "notes": [ "Lowers `inet_ntop()` and `inet_pton()` and boxes invalid-address results as PHP false." ], @@ -9281,7 +9317,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_intval", - "codegen_line": 1108, + "codegen_line": 1109, "notes": [ "Lowers `intval()` for concrete scalar operands." ], @@ -9321,7 +9357,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ip2long", - "codegen_line": 488, + "codegen_line": 506, "notes": [ "Lowers `ip2long(string)` and boxes invalid-address results as PHP false." ], @@ -9412,7 +9448,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_array", - "codegen_line": 1601, + "codegen_line": 1603, "notes": [ "Lowers `is_array()`: true for statically-known arrays/hashes, or a boxed Mixed/Union value", "whose runtime tag is an indexed (4) or associative (5) array. An `iterable`-typed value is", @@ -9451,7 +9487,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 1335, + "codegen_line": 1336, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9689,7 +9725,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 1335, + "codegen_line": 1336, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9763,7 +9799,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 1335, + "codegen_line": 1336, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -9800,7 +9836,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_iterable", - "codegen_line": 1393, + "codegen_line": 1394, "notes": [ "Lowers `is_iterable()` for concrete values and boxed Mixed payloads." ], @@ -9916,7 +9952,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_null_builtin", - "codegen_line": 1591, + "codegen_line": 1593, "notes": [ "Lowers `is_null()` for concrete scalar values and boxed Mixed payloads." ], @@ -9990,7 +10026,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_object", - "codegen_line": 1616, + "codegen_line": 1618, "notes": [ "Lowers `is_object()`: true for statically-known objects, or a boxed Mixed/Union value whose", "runtime tag is an object (6)." @@ -10106,7 +10142,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_is_scalar", - "codegen_line": 1632, + "codegen_line": 1634, "notes": [ "Lowers `is_scalar()`: true for int/float/string/bool, a non-null tagged scalar, or a boxed", "Mixed/Union value whose runtime tag is int (0), string (1), float (2), or bool (3). Null,", @@ -10145,7 +10181,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_static_type_predicate", - "codegen_line": 1335, + "codegen_line": 1336, "notes": [ "Lowers a static `is_*` predicate for concrete non-Mixed values." ], @@ -10794,7 +10830,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_lcfirst", - "codegen_line": 104, + "codegen_line": 122, "notes": [ "Lowers `lcfirst()` by copying the string and lowercasing the first ASCII byte." ], @@ -11186,7 +11222,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_long2ip", - "codegen_line": 476, + "codegen_line": 494, "notes": [ "Lowers `long2ip(value)` through the IPv4 formatting runtime helper." ], @@ -11266,7 +11302,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", - "codegen_line": 112, + "codegen_line": 130, "notes": [ "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], @@ -11336,6 +11372,61 @@ "slug": "max", "sub_area": "Math" }, + { + "area": "Regex", + "canonical_name": "mb_ereg_match", + "description": "Tests whether a regex pattern matches the beginning of a string (multibyte).", + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", + "codegen_function": "lower_mb_ereg_match", + "codegen_line": 52, + "notes": [ + "Lowers `mb_ereg_match(pattern, subject, options = null)` as a start-anchored regex match.", + "The bare delimiter-less pattern and subject use the shared regex string loader. Optional", + "options are passed as a string pair when present, or as `(0, 0)` for `null`/omitted options." + ], + "runtime_helpers": [ + "__rt_mb_ereg_match" + ], + "sig_arm": null, + "sig_file": "src/builtins/string/mb_ereg_match.rs", + "sig_line": null + }, + "name": "mb_ereg_match", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false, + "type": "string" + }, + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true, + "type": "string" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "mb_ereg_match", + "sub_area": "Regex" + }, { "area": "String", "canonical_name": "md5", @@ -11347,7 +11438,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_md5", - "codegen_line": 355, + "codegen_line": 373, "notes": [ "Lowers `md5(data, binary?)` through the shared crypto-backed runtime helper." ], @@ -11729,8 +11820,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/nl2br.rs", @@ -11764,7 +11854,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_number_format", - "codegen_line": 875, + "codegen_line": 893, "notes": [ "Lowers `number_format()` by arranging its runtime helper arguments." ], @@ -11865,7 +11955,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ord", - "codegen_line": 834, + "codegen_line": 852, "notes": [ "Lowers `ord()` by returning the first byte of a string or zero for empty input." ], @@ -12285,7 +12375,6 @@ ], "runtime_helpers": [ "__rt_preg_match", - "__rt_preg_match_all", "__rt_preg_match_capture" ], "sig_arm": null, @@ -12334,7 +12423,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", "codegen_function": "lower_preg_match_all", - "codegen_line": 49, + "codegen_line": 66, "notes": [ "Lowers `preg_match_all(pattern, subject)` through the shared regex runtime helper." ], @@ -12381,7 +12470,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", "codegen_function": "lower_preg_replace", - "codegen_line": 62, + "codegen_line": 79, "notes": [ "Lowers `preg_replace(pattern, replacement, subject)` through the regex replacement helper." ], @@ -12434,7 +12523,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", "codegen_function": "lower_preg_replace_callback", - "codegen_line": 84, + "codegen_line": 101, "notes": [ "Lowers `preg_replace_callback(pattern, callback, subject)` through supported direct callbacks." ], @@ -12487,7 +12576,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/regex.rs", "codegen_function": "lower_preg_split", - "codegen_line": 388, + "codegen_line": 405, "notes": [ "Lowers `preg_split(pattern, subject, limit?, flags?)` through the regex split helper." ], @@ -12602,7 +12691,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_printf", - "codegen_line": 517, + "codegen_line": 535, "notes": [ "Lowers `printf(format, values...)` as `sprintf()` followed by stdout emission." ], @@ -13467,8 +13556,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/rawurldecode.rs", @@ -13507,8 +13595,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/rawurlencode.rs", @@ -14056,7 +14143,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", - "codegen_line": 112, + "codegen_line": 130, "notes": [ "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], @@ -14228,7 +14315,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_sha1", - "codegen_line": 360, + "codegen_line": 378, "notes": [ "Lowers `sha1(data, binary?)` through the shared crypto-backed runtime helper." ], @@ -14854,7 +14941,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_sprintf", - "codegen_line": 511, + "codegen_line": 529, "notes": [ "Lowers `sprintf(format, values...)` by packing variadic records for `__rt_sprintf`." ], @@ -14930,7 +15017,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_sscanf", - "codegen_line": 163, + "codegen_line": 181, "notes": [ "Lowers `sscanf(string, format)` into the shared scanner helper." ], @@ -15018,7 +15105,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_contains", - "codegen_line": 682, + "codegen_line": 700, "notes": [ "Lowers `str_contains()` through `strpos()` and converts found positions to bool." ], @@ -15064,7 +15151,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_binary_string_runtime", - "codegen_line": 139, + "codegen_line": 157, "notes": [ "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], @@ -15110,7 +15197,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_replace", - "codegen_line": 780, + "codegen_line": 798, "notes": [ "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], @@ -15168,7 +15255,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_pad", - "codegen_line": 818, + "codegen_line": 836, "notes": [ "Lowers `str_pad(string, length, pad_string?, pad_type?)` through the shared runtime helper." ], @@ -15228,7 +15315,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_repeat", - "codegen_line": 746, + "codegen_line": 764, "notes": [ "Lowers `str_repeat(string, times)` through the shared runtime helper." ], @@ -15274,7 +15361,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_replace", - "codegen_line": 780, + "codegen_line": 798, "notes": [ "Lowers `str_replace()`/`str_ireplace()` with three string operands." ], @@ -15332,7 +15419,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_str_split", - "codegen_line": 176, + "codegen_line": 194, "notes": [ "Lowers `str_split(string, length?)` into the fixed-width string-array splitter." ], @@ -15378,7 +15465,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_binary_string_runtime", - "codegen_line": 139, + "codegen_line": 157, "notes": [ "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], @@ -15424,7 +15511,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_binary_string_runtime", - "codegen_line": 139, + "codegen_line": 157, "notes": [ "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], @@ -15470,7 +15557,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_binary_string_runtime", - "codegen_line": 139, + "codegen_line": 157, "notes": [ "Lowers a two-argument string builtin that directly delegates to a runtime helper." ], @@ -17506,8 +17593,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/stripslashes.rs", @@ -17541,7 +17627,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins.rs", "codegen_function": "lower_strlen", - "codegen_line": 1078, + "codegen_line": 1079, "notes": [ "Lowers `strlen()` by coercing string-like values and returning the byte length." ], @@ -17580,7 +17666,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_position", - "codegen_line": 700, + "codegen_line": 718, "notes": [ "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." ], @@ -17636,8 +17722,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/strrev.rs", @@ -17671,7 +17756,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_string_position", - "codegen_line": 700, + "codegen_line": 718, "notes": [ "Lowers `strpos()`/`strrpos()` and boxes position-or-false results as Mixed." ], @@ -17722,7 +17807,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_strstr", - "codegen_line": 762, + "codegen_line": 780, "notes": [ "Lowers `strstr(haystack, needle)` by searching and returning the matching suffix." ], @@ -17778,8 +17863,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/strtolower.rs", @@ -17870,8 +17954,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/strtoupper.rs", @@ -17905,7 +17988,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_substr", - "codegen_line": 713, + "codegen_line": 731, "notes": [ "Lowers `substr(string, offset, length?)` with target-local pointer arithmetic." ], @@ -17958,7 +18041,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_substr_replace", - "codegen_line": 730, + "codegen_line": 748, "notes": [ "Lowers `substr_replace(string, replacement, start, length?)`." ], @@ -18419,7 +18502,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_trim_like", - "codegen_line": 112, + "codegen_line": 130, "notes": [ "Lowers `trim()`/`ltrim()`/`rtrim()`/`chop()` for default and explicit masks." ], @@ -18509,7 +18592,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_ucfirst", - "codegen_line": 96, + "codegen_line": 114, "notes": [ "Lowers `ucfirst()` by copying the string and uppercasing the first ASCII byte." ], @@ -18553,8 +18636,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/ucwords.rs", @@ -18814,8 +18896,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/urldecode.rs", @@ -18854,8 +18935,7 @@ "Lowers a one-argument string builtin that directly delegates to a runtime helper." ], "runtime_helpers": [ - "__rt_grapheme_strrev", - "__rt_strcopy" + "__rt_htmlspecialchars" ], "sig_arm": null, "sig_file": "src/builtins/string/urlencode.rs", @@ -19066,7 +19146,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_vprintf", - "codegen_line": 530, + "codegen_line": 548, "notes": [ "Lowers `vprintf(format, values)` as `vsprintf()` followed by stdout emission." ], @@ -19112,7 +19192,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_vsprintf", - "codegen_line": 524, + "codegen_line": 542, "notes": [ "Lowers `vsprintf(format, values)` through the array-to-sprintf runtime bridge." ], @@ -19158,7 +19238,7 @@ "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/strings.rs", "codegen_function": "lower_wordwrap", - "codegen_line": 802, + "codegen_line": 820, "notes": [ "Lowers `wordwrap(string, width?, break?, cut?)` through the shared runtime helper." ], From f066f22b7cbf3d687f8801e23c80491e0719b7e3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 11:10:27 +0200 Subject: [PATCH 1189/1208] feat(magician): expose builtin docs metadata Capture the declaring home file on every eval_builtin! spec via file!(), name EvalArea spellings, and surface a documentation projection (builtin_docs_metadata: area, params with defaults/by-ref, variadic, hook kinds, home file) plus the procedural date-alias name list through the public builtin_metadata API. --- .../src/interpreter/builtin_metadata.rs | 84 ++++++++++++++++++- .../src/interpreter/builtins/macros.rs | 6 ++ .../src/interpreter/builtins/spec.rs | 24 ++++++ .../src/interpreter/builtins/symbols.rs | 4 +- .../builtins/symbols/function_exists.rs | 6 ++ src/bin/gen_builtins.rs | 27 ------ 6 files changed, 120 insertions(+), 31 deletions(-) delete mode 100644 src/bin/gen_builtins.rs diff --git a/crates/elephc-magician/src/interpreter/builtin_metadata.rs b/crates/elephc-magician/src/interpreter/builtin_metadata.rs index 1d63ae392a..a2733e94ea 100644 --- a/crates/elephc-magician/src/interpreter/builtin_metadata.rs +++ b/crates/elephc-magician/src/interpreter/builtin_metadata.rs @@ -11,9 +11,9 @@ //! - Signature shape is the same registry data used by eval named-argument binding. use super::builtins::{ - eval_declared_builtin_exists, - eval_builtin_param_names, eval_builtin_signature_shape, eval_php_visible_builtin_exists, - eval_php_visible_builtin_function_names, + eval_builtin_param_names, eval_builtin_signature_shape, eval_date_procedural_alias_names, + eval_declared_builtin_exists, eval_declared_builtin_spec, eval_php_visible_builtin_exists, + eval_php_visible_builtin_function_names, EvalBuiltinDefaultValue, }; /// A compact, comparison-friendly view of an eval builtin call signature. @@ -69,6 +69,84 @@ pub fn builtin_signature_metadata(name: &str) -> Option, +} + +/// Documentation-oriented metadata for one registry-declared eval builtin. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BuiltinDocsMetadata { + /// Canonical registry name. + pub name: String, + /// Lowercase eval-area spelling (interpreter file layout family). + pub area: String, + /// Parameters in PHP call order. + pub params: Vec, + /// Variadic parameter name, when the builtin accepts one. + pub variadic: Option, + /// Number of leading parameters that must be supplied. + pub required_param_count: usize, + /// Whether an expression-level dispatch hook is registered. + pub has_direct_hook: bool, + /// Whether an evaluated-argument dispatch hook is registered. + pub has_values_hook: bool, + /// Workspace-relative home file that declared the builtin. + pub home_file: String, +} + +/// Returns documentation metadata for one registry-declared eval builtin. +pub fn builtin_docs_metadata(name: &str) -> Option { + let canonical = php_symbol_key(name); + let spec = eval_declared_builtin_spec(&canonical)?; + Some(BuiltinDocsMetadata { + name: spec.name.to_string(), + area: spec.area().name().to_string(), + params: spec + .params + .iter() + .map(|param| BuiltinDocsParam { + name: param.name.to_string(), + by_ref: param.by_ref, + default: param.default.map(default_value_php_repr), + }) + .collect(), + variadic: spec.variadic.map(str::to_string), + required_param_count: spec.required_param_count(), + has_direct_hook: spec.direct.is_some(), + has_values_hook: spec.values.is_some(), + home_file: spec.home_file.to_string(), + }) +} + +/// Returns the procedural date/time alias names the eval dispatcher accepts +/// without declarative registry entries. +pub fn date_procedural_alias_names() -> &'static [&'static str] { + eval_date_procedural_alias_names() +} + +/// Formats an eval builtin default value with its PHP-source spelling. +fn default_value_php_repr(value: EvalBuiltinDefaultValue) -> String { + match value { + EvalBuiltinDefaultValue::Null => "null".to_string(), + EvalBuiltinDefaultValue::Bool(true) => "true".to_string(), + EvalBuiltinDefaultValue::Bool(false) => "false".to_string(), + EvalBuiltinDefaultValue::Int(value) => value.to_string(), + EvalBuiltinDefaultValue::Float(value) => format!("{:?}", value), + EvalBuiltinDefaultValue::String(value) => format!("\"{}\"", value.escape_default()), + EvalBuiltinDefaultValue::Bytes(value) => { + format!("\"{}\"", String::from_utf8_lossy(value).escape_default()) + } + EvalBuiltinDefaultValue::EmptyArray => "[]".to_string(), + } +} + /// Normalizes a PHP symbol name for case-insensitive builtin lookup. fn php_symbol_key(name: &str) -> String { name.trim_start_matches('\\').to_ascii_lowercase() diff --git a/crates/elephc-magician/src/interpreter/builtins/macros.rs b/crates/elephc-magician/src/interpreter/builtins/macros.rs index ead2b04c4d..e2a7e74e3b 100644 --- a/crates/elephc-magician/src/interpreter/builtins/macros.rs +++ b/crates/elephc-magician/src/interpreter/builtins/macros.rs @@ -39,6 +39,7 @@ macro_rules! eval_builtin { required_param_count: None, direct: None, values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), } } }; @@ -70,6 +71,7 @@ macro_rules! eval_builtin { required_param_count: None, direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), } } }; @@ -102,6 +104,7 @@ macro_rules! eval_builtin { required_param_count: None, direct: None, values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), } } }; @@ -133,6 +136,7 @@ macro_rules! eval_builtin { required_param_count: None, direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), } } }; @@ -164,6 +168,7 @@ macro_rules! eval_builtin { required_param_count: Some($required), direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), } } }; @@ -194,6 +199,7 @@ macro_rules! eval_builtin { required_param_count: None, direct: Some($crate::interpreter::builtins::spec::EvalDirectHook::$direct), values: Some($crate::interpreter::builtins::spec::EvalValuesHook::$values), + home_file: file!(), } } }; diff --git a/crates/elephc-magician/src/interpreter/builtins/spec.rs b/crates/elephc-magician/src/interpreter/builtins/spec.rs index a9fa2e91a8..4ddd8c2a15 100644 --- a/crates/elephc-magician/src/interpreter/builtins/spec.rs +++ b/crates/elephc-magician/src/interpreter/builtins/spec.rs @@ -46,6 +46,27 @@ pub(in crate::interpreter) enum EvalArea { Types, } +impl EvalArea { + /// Returns the stable lowercase spelling used by documentation metadata. + pub(in crate::interpreter) fn name(self) -> &'static str { + match self { + EvalArea::Array => "array", + EvalArea::Core => "core", + EvalArea::Filesystem => "filesystem", + EvalArea::Formatting => "formatting", + EvalArea::Json => "json", + EvalArea::Math => "math", + EvalArea::NetworkEnv => "network_env", + EvalArea::Regex => "regex", + EvalArea::RawMemory => "raw_memory", + EvalArea::String => "string", + EvalArea::Symbols => "symbols", + EvalArea::Time => "time", + EvalArea::Types => "types", + } + } +} + /// Parameter metadata for one eval builtin argument. #[derive(Clone, Copy)] pub(in crate::interpreter) struct EvalParamSpec { @@ -77,6 +98,9 @@ pub(in crate::interpreter) struct EvalBuiltinSpec { pub(in crate::interpreter) direct: Option, /// Evaluated-argument dispatch hook. pub(in crate::interpreter) values: Option, + /// Workspace-relative path of the home file that declared this builtin + /// (captured via `file!()` at the `eval_builtin!` invocation site). + pub(in crate::interpreter) home_file: &'static str, } impl EvalBuiltinSpec { diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols.rs b/crates/elephc-magician/src/interpreter/builtins/symbols.rs index d2672b895f..91bbb2e983 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols.rs @@ -57,7 +57,9 @@ pub(in crate::interpreter) use class_attribute_names::{ eval_class_attribute_args_result, eval_reflection_attribute_array_result, }; pub(in crate::interpreter) use class_implements::eval_class_relation_result; -pub(in crate::interpreter) use function_exists::eval_function_probe_exists; +pub(in crate::interpreter) use function_exists::{ + eval_date_procedural_alias_names, eval_function_probe_exists, +}; pub(in crate::interpreter) use get_class::eval_get_class_result; pub(in crate::interpreter) use get_parent_class::eval_get_parent_class_result; pub(in crate::interpreter) use is_a::dynamic_object_is_a; diff --git a/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs index 0be0c03c59..b99c6287e4 100644 --- a/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs +++ b/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs @@ -138,6 +138,12 @@ pub(in crate::interpreter) fn eval_function_probe_exists( || eval_date_procedural_alias_exists(name)) } +/// Returns the procedural date/time alias names the eval dispatcher accepts +/// without declarative registry entries (documentation metadata surface). +pub(in crate::interpreter) fn eval_date_procedural_alias_names() -> &'static [&'static str] { + EVAL_DATE_PROCEDURAL_ALIAS_FUNCTIONS +} + /// Returns true for DateTime/calendar/timezone aliases that static elephc desugars. fn eval_date_procedural_alias_exists(name: &str) -> bool { let bare = name.rsplit('\\').next().unwrap_or(""); diff --git a/src/bin/gen_builtins.rs b/src/bin/gen_builtins.rs deleted file mode 100644 index c40890ceec..0000000000 --- a/src/bin/gen_builtins.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! Purpose: -//! Standalone tool that prints the single-source PHP builtin registry as documentation JSON. -//! -//! Called from: -//! - `cargo run --bin gen_builtins` (documentation generation / CI docs export). -//! -//! Key details: -//! - Delegates all logic to `elephc::builtins::docs`; this binary only serializes the value to -//! pretty JSON on stdout. -//! - `--include-internal` also emits `internal: true` builtins (the docs pipeline renders -//! compiler-internals pages for the `__elephc_*` helpers). Without it, only the PHP-visible -//! surface is emitted. - -/// Prints the builtin documentation JSON (pretty-printed) to stdout. -/// -/// Emits the PHP-visible builtin surface by default; pass `--include-internal` to also include -/// `internal` builtins (used by the documentation generator). -fn main() { - let include_internal = std::env::args().any(|a| a == "--include-internal"); - let value = if include_internal { - elephc::builtins::docs::export_builtins_json_all() - } else { - elephc::builtins::docs::export_builtins_json() - }; - let json = serde_json::to_string_pretty(&value).expect("serialize builtins JSON"); - println!("{}", json); -} From ed1b8104dedd6d413c5b8b2e0d8c7ac2542ae6ea Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 11:10:27 +0200 Subject: [PATCH 1190/1208] feat(docs): emit dual AOT/eval support from the builtins exporter Move gen_builtins from a bin to an example target so it can link the elephc-magician dev-dependency without embedding the interpreter in the elephc binary. Every record gains an eval block (kind: registry | date-alias | none, hooks, params, home file); eval-registry names with no static counterpart are appended as aot_resident (compiler-resident constructs) or eval_only records. CI, skill, and docs references updated. --- .claude/skills/update-builtin-docs/SKILL.md | 10 +- .github/workflows/ci.yml | 9 +- AGENTS.md | 2 +- Cargo.toml | 7 +- scripts/docs/README.md | 16 ++- src/builtins/docs.rs | 10 +- tools/gen_builtins.rs | 152 ++++++++++++++++++++ 7 files changed, 187 insertions(+), 19 deletions(-) create mode 100644 tools/gen_builtins.rs diff --git a/.claude/skills/update-builtin-docs/SKILL.md b/.claude/skills/update-builtin-docs/SKILL.md index 7f704f150d..d0bfe74cae 100644 --- a/.claude/skills/update-builtin-docs/SKILL.md +++ b/.claude/skills/update-builtin-docs/SKILL.md @@ -1,6 +1,6 @@ --- name: update-builtin-docs -description: Regenerate and audit Elephc's generated builtin documentation from the builtin! registry. Use when a change touches src/builtins, builtin signatures, builtin lowering hooks, docs/php/builtins, docs/internals/builtins, scripts/docs/builtin_registry.json, or before opening a PR that changes PHP builtins. +description: Regenerate and audit Elephc's generated builtin documentation from the builtin! and eval_builtin! registries. Use when a change touches src/builtins, crates/elephc-magician/src/interpreter/builtins, builtin signatures, builtin lowering hooks, docs/php/builtins, docs/internals/builtins, scripts/docs/builtin_registry.json, or before opening a PR that changes PHP builtins. --- # Update Builtin Docs @@ -10,10 +10,12 @@ Use the repo root as the working directory. ## Workflow -1. Build the exporter binary that reads the single-source `builtin!` registry: +1. Build the exporter that reads the single-source `builtin!` registry and the + eval interpreter's `eval_builtin!` registry (an example target, so it can + link the elephc-magician dev-dependency): ```bash -cargo build --bin gen_builtins +cargo build --example gen_builtins ``` 2. Regenerate the JSON registry and Markdown pages: @@ -38,7 +40,7 @@ git diff --check ## Rules -- Treat `src/builtins/` and the `builtin!` declarations as the source of truth. +- Treat `src/builtins/` (`builtin!`) and `crates/elephc-magician/src/interpreter/builtins/` (`eval_builtin!`) as the source of truth for the AOT and eval support dimensions respectively. - Do not hand-edit generated builtin pages to fix drift; fix the registry, lowering metadata, or `scripts/docs/elephc_builtins/` generator inputs, then rerun the workflow. - If the user asked only for a sync check, also run: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cb545963b..eaa09e84d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -505,10 +505,11 @@ jobs: rust-builtins-docs- - name: Build the builtin docs exporter - # The docs generator reads the single-source `builtin!` registry via the - # `gen_builtins` binary (`extract.py` prefers the prebuilt binary at - # target/debug/gen_builtins), so it must be built before regeneration. - run: cargo build --bin gen_builtins + # The docs generator reads the single-source `builtin!` registry plus the + # eval interpreter's `eval_builtin!` registry via the `gen_builtins` + # example (`extract.py` prefers the prebuilt binary at + # target/debug/examples/gen_builtins), so it must be built first. + run: cargo build --example gen_builtins - name: Regenerate builtins documentation # Re-run the generator; the committed Markdown pages and JSON registry diff --git a/AGENTS.md b/AGENTS.md index f5c13718d2..91921168a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -256,7 +256,7 @@ Key invariants: PHP-visible builtins); keep the parity gates in `src/builtins/parity_tests.rs` green. - Before opening a PR that adds, removes, or changes PHP-visible builtins, run the `update-builtin-docs` skill or the equivalent CI sequence: - `cargo build --bin gen_builtins`, + `cargo build --example gen_builtins`, `python3 scripts/docs/extract_builtins.py --render --force`, `python3 scripts/docs/audit_builtins.py`, and `python3 scripts/docs/elephc_builtins/validate_site_compat.py`. Commit the diff --git a/Cargo.toml b/Cargo.toml index 9394134f5e..e2cef692a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,9 +29,12 @@ resolver = "2" name = "elephc" path = "src/main.rs" -[[bin]] +# Documentation exporter: declared as an example so it can read the eval +# interpreter's metadata (elephc-magician is a dev-dependency) without +# linking magician into the elephc binary. +[[example]] name = "gen_builtins" -path = "src/bin/gen_builtins.rs" +path = "tools/gen_builtins.rs" [dependencies] inventory = "0.3" diff --git a/scripts/docs/README.md b/scripts/docs/README.md index b4e77c40d0..ef05ad9a65 100644 --- a/scripts/docs/README.md +++ b/scripts/docs/README.md @@ -10,9 +10,11 @@ tree into one Markdown page per supported PHP builtin, in two flavours: source file lowers the call, which runtime helper it dispatches to, what the type checker enforces. -The script is data-driven. Its source of truth is the single-source -`builtin!` registry (`src/builtins/`), read via the `gen_builtins` binary -(`cargo run --bin gen_builtins --include-internal`). It enriches that data +The script is data-driven. Its sources of truth are the single-source +`builtin!` registry (`src/builtins/`) and the eval interpreter's +`eval_builtin!` registry (`crates/elephc-magician/src/interpreter/builtins/`), +both read via the `gen_builtins` example +(`cargo run --example gen_builtins -- --include-internal`). It enriches that data with each builtin's lowering location (parsed from the home file's `lower` hook) and documentation area, then writes a single JSON registry (`scripts/docs/builtin_registry.json`) which the Markdown renderer consumes. @@ -20,13 +22,13 @@ Everything else is generated. ## Usage -From the repo root. The generator invokes the `gen_builtins` binary, so build -it first (the extractor prefers the prebuilt binary at `target/debug/gen_builtins` -and otherwise falls back to `cargo run`): +From the repo root. The generator invokes the `gen_builtins` example, so build +it first (the extractor prefers the prebuilt binary at +`target/debug/examples/gen_builtins` and otherwise falls back to `cargo run`): ```bash # 0. Build the registry exporter the generator reads from -cargo build --bin gen_builtins +cargo build --example gen_builtins # 1. Parse the source and write the JSON registry python3 scripts/docs/extract_builtins.py diff --git a/src/builtins/docs.rs b/src/builtins/docs.rs index c0522d0b12..d8f14becf7 100644 --- a/src/builtins/docs.rs +++ b/src/builtins/docs.rs @@ -3,7 +3,7 @@ //! Every PHP-visible registered builtin is emitted as one object; internal builtins are skipped. //! //! Called from: -//! - `src/bin/gen_builtins.rs` via `elephc::builtins::docs::export_builtins_json()`. +//! - `tools/gen_builtins.rs` (example target) via `elephc::builtins::docs::export_builtins_json()`. //! //! Key details: //! - Uses `crate::builtins::registry::{names, lookup}` so `inventory::iter` runs in the same @@ -69,6 +69,14 @@ fn default_spec_json(default: &DefaultSpec) -> Value { } } +/// Returns true when a PHP-visible builtin exists in the static AOT surface: +/// `builtin!` registry entries plus compiler-resident constructs (`isset`, +/// `strval`, predicate aliases, ...). Documentation tooling uses this to tell +/// resident names apart from genuinely eval-only builtins. +pub fn aot_php_visible_builtin_exists(name: &str) -> bool { + crate::types::checker::builtins::is_php_visible_builtin_function(name) +} + /// Builds the documentation JSON array for every PHP-visible registered builtin. /// /// Iterates the registry in sorted name order, skips `internal` builtins, and emits one object per diff --git a/tools/gen_builtins.rs b/tools/gen_builtins.rs new file mode 100644 index 0000000000..2aecf86836 --- /dev/null +++ b/tools/gen_builtins.rs @@ -0,0 +1,152 @@ +//! Purpose: +//! Standalone docs exporter that prints the single-source PHP builtin registry as JSON, +//! enriched with eval-interpreter (magician) support metadata for every builtin. +//! +//! Called from: +//! - `cargo run --example gen_builtins` (documentation generation / CI docs export). +//! +//! Key details: +//! - Declared as an example (not a bin) so it can read `elephc-magician`'s metadata — +//! magician is a dev-dependency, keeping the eval interpreter out of the `elephc` binary. +//! - Static AOT records come from `elephc::builtins::docs`; each record gains an `eval` +//! block (`kind`: `registry` | `date-alias` | `none`) and builtins only the eval +//! registry exposes are appended with `eval_only: true`. +//! - `--include-internal` also emits `internal: true` builtins (the docs pipeline renders +//! compiler-internals pages for the `__elephc_*` helpers). + +use serde_json::{json, Value}; + +/// Prints the dual-support builtin documentation JSON (pretty-printed) to stdout. +fn main() { + let include_internal = std::env::args().any(|a| a == "--include-internal"); + let value = if include_internal { + elephc::builtins::docs::export_builtins_json_all() + } else { + elephc::builtins::docs::export_builtins_json() + }; + let Value::Array(mut records) = value else { + panic!("builtins JSON export must be a top-level array"); + }; + for record in &mut records { + let name = record["name"] + .as_str() + .expect("builtin record carries a string name") + .to_string(); + let entry = record + .as_object_mut() + .expect("builtin record is a JSON object"); + entry.insert("eval".to_string(), eval_support_json(&name)); + } + append_eval_only_records(&mut records, include_internal); + let json = serde_json::to_string_pretty(&Value::Array(records)).expect("serialize builtins JSON"); + println!("{}", json); +} + +/// Builds the eval-interpreter (magician) support block for one builtin name. +/// +/// `kind` distinguishes how eval'd code reaches the builtin: `registry` for +/// declarative `eval_builtin!` entries, `date-alias` for procedural date/time +/// aliases resolved by the alias dispatcher, `none` when eval'd code cannot +/// call it. +fn eval_support_json(name: &str) -> Value { + if let Some(meta) = elephc_magician::builtin_metadata::builtin_docs_metadata(name) { + let params: Vec = meta + .params + .iter() + .map(|p| { + json!({ + "name": p.name, + "by_ref": p.by_ref, + "optional": p.default.is_some(), + "default": p.default, + }) + }) + .collect(); + let mut hooks: Vec<&str> = Vec::new(); + if meta.has_direct_hook { + hooks.push("direct"); + } + if meta.has_values_hook { + hooks.push("values"); + } + return json!({ + "supported": true, + "kind": "registry", + "area": meta.area, + "hooks": hooks, + "params": params, + "variadic": meta.variadic, + "required_param_count": meta.required_param_count, + "home_file": meta.home_file, + }); + } + let bare = name.trim_start_matches('\\').to_ascii_lowercase(); + if elephc_magician::builtin_metadata::date_procedural_alias_names() + .iter() + .any(|alias| *alias == bare) + { + return json!({ + "supported": true, + "kind": "date-alias", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/aliases.rs", + }); + } + json!({ "supported": false, "kind": "none" }) +} + +/// Appends eval-registry builtins with no static `builtin!` counterpart. +/// +/// Two flavors: `aot_resident: true` when the static compiler still supports +/// the name as a compiler-resident construct or alias (`isset`, `strval`, +/// `is_integer`, ...) — the Python pipeline merges the eval block into its +/// hand-maintained pseudo-entries; `eval_only: true` when only eval'd code can +/// call the builtin. Static fields carry neutral placeholders (magician specs +/// are untyped). +fn append_eval_only_records(records: &mut Vec, include_internal: bool) { + for eval_name in elephc_magician::builtin_metadata::php_visible_builtin_names() { + if elephc::builtins::registry::lookup(eval_name).is_some() { + continue; + } + let Some(meta) = elephc_magician::builtin_metadata::builtin_docs_metadata(eval_name) + else { + continue; + }; + let internal = meta.name.starts_with("__elephc_"); + if internal && !include_internal { + continue; + } + let aot_resident = elephc::builtins::docs::aot_php_visible_builtin_exists(&meta.name); + let params: Vec = meta + .params + .iter() + .map(|p| { + json!({ + "name": p.name, + "type": "mixed", + "by_ref": p.by_ref, + "optional": p.default.is_some(), + "default": p.default, + }) + }) + .collect(); + records.push(json!({ + "name": meta.name, + "area": meta.area, + "internal": internal, + "params": params, + "variadic": meta.variadic, + "returns": "mixed", + "by_ref_return": false, + "min_args": Value::Null, + "max_args": Value::Null, + "arity_error": Value::Null, + "summary": "", + "examples": Vec::::new(), + "php_manual": Value::Null, + "deprecated": Value::Null, + "eval_only": !aot_resident, + "aot_resident": aot_resident, + "eval": eval_support_json(&meta.name), + })); + } +} From c74c5d97ec8999f404b98664684ebf87302529c4 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 11:10:27 +0200 Subject: [PATCH 1191/1208] feat(docs): render the AOT/eval support matrix extract.py consumes the exporter's eval blocks (merging resident ones into the language-construct entries and building pages for eval-only builtins), the JSON registry carries eval/eval_only per record, and the renderer adds an Availability section to user pages, an eval-interpreter section to internals pages, and AOT/eval() columns to the indexes. --- scripts/docs/elephc_builtins/extract.py | 99 +++++++++++++++++++++--- scripts/docs/elephc_builtins/registry.py | 5 ++ scripts/docs/elephc_builtins/render.py | 94 ++++++++++++++++++++-- 3 files changed, 183 insertions(+), 15 deletions(-) diff --git a/scripts/docs/elephc_builtins/extract.py b/scripts/docs/elephc_builtins/extract.py index f40fb36b19..917d5705fa 100644 --- a/scripts/docs/elephc_builtins/extract.py +++ b/scripts/docs/elephc_builtins/extract.py @@ -3,9 +3,11 @@ Since the single-source builtin registry migration, every PHP builtin is declared once via `builtin!` in ``src/builtins//.rs`` and collected through the `inventory` crate. The authoritative data is therefore read from the registry -itself, via the ``gen_builtins`` binary (``cargo run --bin gen_builtins +itself, via the ``gen_builtins`` example (``cargo run --example gen_builtins -- --include-internal``), NOT by regex-scraping ``catalog.rs`` / ``signatures.rs`` -(which the migration emptied). +(which the migration emptied). The exporter also attaches, per builtin, the eval +interpreter's (elephc-magician) support block sourced from the ``eval_builtin!`` +registry, plus records for builtins only the eval interpreter exposes. For each builtin we enrich the registry data with: @@ -60,25 +62,29 @@ # --------------------------------------------------------------------------- def run_gen_builtins(repo: Path) -> list[dict]: - """Return the registry as a list of dicts by invoking the `gen_builtins` binary. + """Return the registry as a list of dicts by invoking the `gen_builtins` example. Includes `internal` builtins (the docs pipeline renders compiler-internals - pages for the `__elephc_*` helpers). Prefers a prebuilt binary under - ``target/{release,debug}/`` when present (fast path for CI, which builds it - first); otherwise falls back to ``cargo run``. + pages for the `__elephc_*` helpers) and per-builtin eval-interpreter support + blocks. Prefers a prebuilt binary under ``target/{release,debug}/examples/`` + when present (fast path for CI, which builds it first); otherwise falls back + to ``cargo run``. """ cmd: list[str] for profile in ("release", "debug"): - exe = repo / "target" / profile / "gen_builtins" + exe = repo / "target" / profile / "examples" / "gen_builtins" if exe.exists(): cmd = [str(exe), "--include-internal"] break else: - cmd = ["cargo", "run", "--quiet", "--bin", "gen_builtins", "--", "--include-internal"] + cmd = [ + "cargo", "run", "--quiet", "--example", "gen_builtins", "--", + "--include-internal", + ] proc = subprocess.run(cmd, cwd=repo, capture_output=True, text=True) if proc.returncode != 0: sys.exit( - "gen_builtins failed (build it with `cargo build --bin gen_builtins`):\n" + "gen_builtins failed (build it with `cargo build --example gen_builtins`):\n" + proc.stderr ) try: @@ -468,10 +474,69 @@ def _render_default(value, optional: bool) -> Optional[str]: } +# Docs area for eval-only builtins, keyed by the magician EvalArea spelling. +EVAL_AREA_TO_DOCS_AREA: dict[str, tuple[str, str]] = { + "array": ("Array", "Array"), + "core": ("Misc", "Misc"), + "filesystem": ("Filesystem", "Filesystem"), + "formatting": ("String", "String"), + "json": ("JSON", "JSON"), + "math": ("Math", "Math"), + "network_env": ("Network", "Network"), + "regex": ("Regex", "Regex"), + "raw_memory": ("Pointer", "Pointer"), + "string": ("String", "String"), + "symbols": ("Class", "Class"), + "time": ("Date", "Date"), + "types": ("Type", "Type"), +} + + # --------------------------------------------------------------------------- # Orchestration # --------------------------------------------------------------------------- +def _eval_only_builtin(entry: dict) -> Builtin: + """Build a Builtin for a name only the eval interpreter (magician) exposes.""" + name = entry["name"] + canonical = name.lower() + eval_support = entry.get("eval") or {} + area, sub_area = EVAL_AREA_TO_DOCS_AREA.get( + eval_support.get("area", ""), ("Misc", "Misc") + ) + params = [ + Parameter( + name=p["name"], + php_type=_normalize_type(p.get("type", "mixed")), + by_ref=bool(p.get("by_ref")), + default=_render_default(p.get("default"), bool(p.get("optional"))), + optional=bool(p.get("optional")), + ) + for p in entry.get("params", []) + ] + description = DESCRIPTION_OVERRIDES.get(canonical, "") or ( + f"{name}() is available inside eval'd code via the magician interpreter; " + "compiled (AOT) code does not support it yet." + ) + return Builtin( + name=name, + canonical_name=canonical, + in_catalog=True, + is_internal=bool(entry.get("internal")), + area=area, + sub_area=sub_area, + sig=BuiltinSig( + params=params, + variadic=entry.get("variadic"), + return_type=_normalize_type(entry.get("returns", "mixed")), + ), + lowering=LoweringInfo(), + description=description, + eval_support=eval_support, + eval_only=True, + ) + + def build_registry(repo: Path) -> list[Builtin]: """Build the full list of builtins from the registry + language constructs.""" src = repo / "src" @@ -502,10 +567,22 @@ def resolve_check_body(fn_name: str) -> str: builtins: list[Builtin] = [] + # Eval blocks for compiler-resident constructs (isset, strval, ...): the + # exporter appends them as `aot_resident` records; their AOT docs live in + # the hand-curated LANGUAGE_CONSTRUCTS entries (or on canonical alias + # pages), so only the eval block is consumed here. + resident_eval: dict[str, dict] = {} + # --- registry builtins (PHP-visible + internal helpers) --- for entry in gen: name = entry["name"] canonical = name.lower() + if entry.get("aot_resident"): + resident_eval[canonical] = entry.get("eval") or {} + continue + if entry.get("eval_only"): + builtins.append(_eval_only_builtin(entry)) + continue is_internal = bool(entry.get("internal")) in_catalog = not is_internal @@ -570,6 +647,7 @@ def resolve_check_body(fn_name: str) -> str: ), lowering=lowering, description=description, + eval_support=entry.get("eval"), ) ) @@ -603,6 +681,7 @@ def resolve_check_body(fn_name: str) -> str: ), lowering=lowering, description=description, + eval_support=resident_eval.get(canonical), ) ) @@ -671,6 +750,8 @@ def _builtin_to_dict(b: Builtin) -> dict: "runtime_helpers": b.lowering.runtime_helpers, "notes": b.lowering.notes, }, + "eval": b.eval_support or {"supported": False, "kind": "none"}, + "eval_only": b.eval_only, } diff --git a/scripts/docs/elephc_builtins/registry.py b/scripts/docs/elephc_builtins/registry.py index 0451c1df4f..6bd7e82a01 100644 --- a/scripts/docs/elephc_builtins/registry.py +++ b/scripts/docs/elephc_builtins/registry.py @@ -1201,6 +1201,11 @@ class Builtin: examples: List[str] = field(default_factory=list) # raw ```php ... ``` blocks see_also: List[str] = field(default_factory=list) notes: List[str] = field(default_factory=list) + # Eval-interpreter (elephc-magician) support block from the gen_builtins + # exporter: {supported, kind, area, hooks, params, variadic, home_file}. + eval_support: Optional[dict] = None + # True when only the eval interpreter exposes this builtin (no AOT support). + eval_only: bool = False def slug(name: str) -> str: diff --git a/scripts/docs/elephc_builtins/render.py b/scripts/docs/elephc_builtins/render.py index 71792fe8be..f59352c110 100644 --- a/scripts/docs/elephc_builtins/render.py +++ b/scripts/docs/elephc_builtins/render.py @@ -45,6 +45,8 @@ {return_section} +{availability_section} + {examples_section} {notes_section} @@ -84,6 +86,10 @@ {checker_notes} +## Eval interpreter (magician) + +{eval_section} + ## Cross-references {user_link} @@ -203,6 +209,72 @@ def _github_url_with_line(repo_root: Path, file_path: str, line: int) -> str: return f"https://github.com/illegalstudio/elephc/blob/main/{rel}#L{line}" +def _availability_section(b: dict) -> str: + """Two-line support matrix: compiled (AOT) vs eval() interpreter.""" + lines = ["## Availability", ""] + if b.get("eval_only"): + lines.append( + "- **Compiled (AOT)**: not available — compiled programs cannot " + "call this builtin yet." + ) + else: + lines.append("- **Compiled (AOT)**: supported by the Elephc code generator.") + ev = b.get("eval") or {} + if ev.get("supported"): + kind = ev.get("kind") + if kind == "registry": + home = ev.get("home_file") or "" + lines.append( + "- **`eval()` (magician interpreter)**: supported — declarative " + f"interpreter builtin ([`{home}`](https://github.com/illegalstudio/elephc/blob/main/{home}))." + ) + elif kind == "date-alias": + lines.append( + "- **`eval()` (magician interpreter)**: supported through the " + "procedural date/time alias dispatcher." + ) + else: + lines.append("- **`eval()` (magician interpreter)**: supported.") + else: + lines.append( + "- **`eval()` (magician interpreter)**: not available inside eval'd code." + ) + return "\n".join(lines) + + +def _eval_internals_section(b: dict) -> str: + """How the eval interpreter reaches this builtin, for the internals page.""" + ev = b.get("eval") or {} + if not ev.get("supported"): + return ( + "_Not callable from eval'd code — the magician interpreter has no " + "entry for this builtin._" + ) + if ev.get("kind") == "date-alias": + home = ev.get("home_file") or "" + return ( + "Dispatched as a procedural date/time alias by " + f"[`{home}`](https://github.com/illegalstudio/elephc/blob/main/{home})." + ) + home = ev.get("home_file") or "" + hooks = ev.get("hooks") or [] + lines = [ + f"- **Declaration**: [`{home}`](https://github.com/illegalstudio/elephc/blob/main/{home}) (`eval_builtin!`)", + "- **Dispatch hooks**: " + + (", ".join(f"`{h}`" for h in hooks) or "_none_"), + ] + by_ref = [p["name"] for p in (ev.get("params") or []) if p.get("by_ref")] + if by_ref: + lines.append( + "- **By-reference parameters**: " + + ", ".join(f"`${n}`" for n in by_ref) + + "." + ) + if ev.get("variadic"): + lines.append(f"- **Variadic**: collects excess arguments into `${ev['variadic']}`.") + return "\n".join(lines) + + def _internals_link(b: dict) -> str: """Cross-link to the internals page for this builtin, if it has been lowered. @@ -241,6 +313,7 @@ def render_user(b: dict, order: int, repo_root: Path) -> str: "Behavior matches the PHP manual unless noted below.", parameters_section=_parameters_section(b), return_section=_return_section(b), + availability_section=_availability_section(b), examples_section=_examples_section(b), notes_section=_notes_section(b), see_also_section=_see_also_section(b), @@ -249,7 +322,12 @@ def render_user(b: dict, order: int, repo_root: Path) -> str: def render_internals(b: dict, order: int, repo_root: Path) -> str: - sig_file = b["lowering"].get("sig_file") or "src/types/signatures.rs" + sig_file = b["lowering"].get("sig_file") + if not sig_file and b.get("eval_only"): + # Eval-only builtins have no static signature home; point at the + # magician declaration instead. + sig_file = (b.get("eval") or {}).get("home_file") + sig_file = sig_file or "src/types/signatures.rs" codegen_file = b["lowering"].get("codegen_file") codegen_line = b["lowering"].get("codegen_line") codegen_function = b["lowering"].get("codegen_function") or "(none — type-checker only)" @@ -303,6 +381,7 @@ def render_internals(b: dict, order: int, repo_root: Path) -> str: runtime_helpers_section=_runtime_helpers_section(b), signature=_signature_line(b), checker_notes=_checker_notes(b), + eval_section=_eval_internals_section(b), see_also_section=see_also_section, ) @@ -362,8 +441,11 @@ def _index_table_rows(builtins: list[dict], link_prefix: str = ".") -> list[str] link = f"{link_prefix}/_internal/{slug(b['name'])}.md" else: link = f"{link_prefix}/{area_folder}/{slug(b['name'])}.md" + aot = "—" if b.get("eval_only") else "✓" + ev = "✓" if (b.get("eval") or {}).get("supported") else "—" rows.append( - f"| [`{b['name']}()`]({link}) | `{sig}` | `{b['sig']['return_type']}` |" + f"| [`{b['name']}()`]({link}) | `{sig}` | `{b['sig']['return_type']}` " + f"| {aot} | {ev} |" ) return rows @@ -382,8 +464,8 @@ def render_area_index(area: str, builtins: list[dict], order: int = 0) -> str: "", f"## {area} builtins", "", - "| Function | Signature | Returns |", - "|---|---|---|", + "| Function | Signature | Returns | AOT | eval() |", + "|---|---|---|:-:|:-:|", ] lines.extend(_index_table_rows(relevant)) return "\n".join(lines) + "\n" @@ -403,8 +485,8 @@ def render_master_index(builtins: list[dict]) -> str: "", "## Builtins", "", - "| Function | Signature | Returns |", - "|---|---|---|", + "| Function | Signature | Returns | AOT | eval() |", + "|---|---|---|:-:|:-:|", ] lines.extend(_index_table_rows(relevant, link_prefix="./builtins")) return "\n".join(lines) + "\n" From a6e27847d2ee3ad3288cda61def79894b15f7322 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 11:10:27 +0200 Subject: [PATCH 1192/1208] docs: regenerate builtin pages with the eval support matrix --- .../_internal/__elephc_gmmktime_raw.md | 6 +- .../builtins/_internal/__elephc_mktime_raw.md | 6 +- .../_internal/__elephc_phar_bzip2_archive.md | 6 +- .../__elephc_phar_decompress_archive.md | 6 +- .../__elephc_phar_get_file_metadata.md | 6 +- .../_internal/__elephc_phar_get_metadata.md | 6 +- .../__elephc_phar_get_signature_hash.md | 6 +- .../__elephc_phar_get_signature_type.md | 6 +- .../_internal/__elephc_phar_get_stub.md | 6 +- .../_internal/__elephc_phar_gzip_archive.md | 6 +- .../_internal/__elephc_phar_list_entries.md | 6 +- .../__elephc_phar_set_compression.md | 6 +- .../__elephc_phar_set_file_metadata.md | 6 +- .../_internal/__elephc_phar_set_metadata.md | 6 +- .../_internal/__elephc_phar_set_stub.md | 6 +- .../__elephc_phar_set_zip_password.md | 6 +- .../_internal/__elephc_phar_sign_hash.md | 6 +- .../_internal/__elephc_phar_sign_openssl.md | 6 +- .../_internal/__elephc_strtotime_raw.md | 6 +- docs/internals/builtins/array/array_all.md | 4 + docs/internals/builtins/array/array_any.md | 4 + docs/internals/builtins/array/array_chunk.md | 5 + docs/internals/builtins/array/array_column.md | 5 + .../internals/builtins/array/array_combine.md | 5 + docs/internals/builtins/array/array_diff.md | 6 + .../builtins/array/array_diff_assoc.md | 4 + .../builtins/array/array_diff_key.md | 6 + docs/internals/builtins/array/array_fill.md | 5 + .../builtins/array/array_fill_keys.md | 5 + docs/internals/builtins/array/array_filter.md | 5 + docs/internals/builtins/array/array_find.md | 4 + docs/internals/builtins/array/array_flip.md | 5 + .../builtins/array/array_intersect.md | 6 + .../builtins/array/array_intersect_assoc.md | 4 + .../builtins/array/array_intersect_key.md | 6 + .../internals/builtins/array/array_is_list.md | 4 + .../builtins/array/array_key_exists.md | 5 + .../builtins/array/array_key_first.md | 4 + .../builtins/array/array_key_last.md | 4 + docs/internals/builtins/array/array_keys.md | 5 + docs/internals/builtins/array/array_map.md | 6 + docs/internals/builtins/array/array_merge.md | 6 + .../builtins/array/array_merge_recursive.md | 4 + .../builtins/array/array_multisort.md | 4 + docs/internals/builtins/array/array_pad.md | 5 + docs/internals/builtins/array/array_pop.md | 6 + .../internals/builtins/array/array_product.md | 5 + docs/internals/builtins/array/array_push.md | 7 + docs/internals/builtins/array/array_rand.md | 5 + docs/internals/builtins/array/array_reduce.md | 5 + .../internals/builtins/array/array_replace.md | 4 + .../builtins/array/array_replace_recursive.md | 4 + .../internals/builtins/array/array_reverse.md | 5 + docs/internals/builtins/array/array_search.md | 5 + docs/internals/builtins/array/array_shift.md | 6 + docs/internals/builtins/array/array_slice.md | 5 + docs/internals/builtins/array/array_splice.md | 6 + docs/internals/builtins/array/array_sum.md | 5 + docs/internals/builtins/array/array_udiff.md | 4 + .../builtins/array/array_uintersect.md | 4 + docs/internals/builtins/array/array_unique.md | 5 + .../internals/builtins/array/array_unshift.md | 7 + docs/internals/builtins/array/array_values.md | 5 + docs/internals/builtins/array/array_walk.md | 6 + .../builtins/array/array_walk_recursive.md | 4 + docs/internals/builtins/array/arsort.md | 6 + docs/internals/builtins/array/asort.md | 6 + .../builtins/array/call_user_func.md | 6 + .../builtins/array/call_user_func_array.md | 5 + docs/internals/builtins/array/count.md | 5 + docs/internals/builtins/array/in_array.md | 5 + docs/internals/builtins/array/krsort.md | 6 + docs/internals/builtins/array/ksort.md | 6 + docs/internals/builtins/array/natcasesort.md | 6 + docs/internals/builtins/array/natsort.md | 6 + docs/internals/builtins/array/range.md | 5 + docs/internals/builtins/array/rsort.md | 6 + docs/internals/builtins/array/shuffle.md | 6 + docs/internals/builtins/array/sort.md | 6 + docs/internals/builtins/array/uasort.md | 6 + docs/internals/builtins/array/uksort.md | 6 + docs/internals/builtins/array/usort.md | 6 + docs/internals/builtins/buffer/buffer_free.md | 5 + docs/internals/builtins/buffer/buffer_len.md | 5 + docs/internals/builtins/class/class_alias.md | 5 + .../builtins/class/class_attribute_args.md | 5 + .../builtins/class/class_attribute_names.md | 5 + docs/internals/builtins/class/class_exists.md | 5 + .../builtins/class/class_get_attributes.md | 5 + .../builtins/class/class_implements.md | 5 + .../internals/builtins/class/class_parents.md | 5 + docs/internals/builtins/class/class_uses.md | 5 + docs/internals/builtins/class/enum_exists.md | 5 + .../builtins/class/function_exists.md | 5 + .../builtins/class/get_called_class.md | 38 + docs/internals/builtins/class/get_class.md | 7 +- .../builtins/class/get_class_methods.md | 38 + .../builtins/class/get_class_vars.md | 38 + .../builtins/class/get_declared_classes.md | 7 +- .../builtins/class/get_declared_interfaces.md | 7 +- .../builtins/class/get_declared_traits.md | 7 +- .../builtins/class/get_object_vars.md | 38 + .../builtins/class/get_parent_class.md | 7 +- .../builtins/class/interface_exists.md | 7 +- docs/internals/builtins/class/is_a.md | 7 +- .../builtins/class/is_subclass_of.md | 7 +- docs/internals/builtins/class/trait_exists.md | 7 +- docs/internals/builtins/date/checkdate.md | 7 +- docs/internals/builtins/date/date.md | 7 +- .../date/date_default_timezone_get.md | 7 +- .../date/date_default_timezone_set.md | 7 +- docs/internals/builtins/date/getdate.md | 7 +- docs/internals/builtins/date/gmdate.md | 7 +- docs/internals/builtins/date/gmmktime.md | 7 +- docs/internals/builtins/date/hrtime.md | 7 +- docs/internals/builtins/date/localtime.md | 7 +- docs/internals/builtins/date/microtime.md | 7 +- docs/internals/builtins/date/mktime.md | 7 +- docs/internals/builtins/date/strtotime.md | 7 +- docs/internals/builtins/date/time.md | 7 +- .../internals/builtins/filesystem/basename.md | 7 +- docs/internals/builtins/filesystem/chdir.md | 7 +- docs/internals/builtins/filesystem/chgrp.md | 7 +- docs/internals/builtins/filesystem/chmod.md | 7 +- docs/internals/builtins/filesystem/chown.md | 7 +- .../builtins/filesystem/clearstatcache.md | 7 +- docs/internals/builtins/filesystem/copy.md | 7 +- docs/internals/builtins/filesystem/dirname.md | 7 +- .../builtins/filesystem/disk_free_space.md | 7 +- .../builtins/filesystem/disk_total_space.md | 7 +- .../builtins/filesystem/file_exists.md | 7 +- .../builtins/filesystem/fileatime.md | 7 +- .../builtins/filesystem/filectime.md | 7 +- .../builtins/filesystem/filegroup.md | 7 +- .../builtins/filesystem/fileinode.md | 7 +- .../builtins/filesystem/filemtime.md | 7 +- .../builtins/filesystem/fileowner.md | 7 +- .../builtins/filesystem/fileperms.md | 7 +- .../internals/builtins/filesystem/filesize.md | 7 +- .../internals/builtins/filesystem/filetype.md | 7 +- docs/internals/builtins/filesystem/fnmatch.md | 7 +- docs/internals/builtins/filesystem/getcwd.md | 7 +- docs/internals/builtins/filesystem/getenv.md | 7 +- docs/internals/builtins/filesystem/glob.md | 7 +- docs/internals/builtins/filesystem/is_dir.md | 7 +- .../builtins/filesystem/is_executable.md | 7 +- docs/internals/builtins/filesystem/is_file.md | 7 +- docs/internals/builtins/filesystem/is_link.md | 7 +- .../builtins/filesystem/is_readable.md | 7 +- .../builtins/filesystem/is_writable.md | 7 +- .../builtins/filesystem/is_writeable.md | 7 +- docs/internals/builtins/filesystem/lchgrp.md | 7 +- docs/internals/builtins/filesystem/lchown.md | 7 +- docs/internals/builtins/filesystem/link.md | 7 +- .../internals/builtins/filesystem/linkinfo.md | 7 +- docs/internals/builtins/filesystem/lstat.md | 7 +- docs/internals/builtins/filesystem/mkdir.md | 7 +- .../internals/builtins/filesystem/pathinfo.md | 7 +- docs/internals/builtins/filesystem/putenv.md | 7 +- .../internals/builtins/filesystem/readfile.md | 7 +- .../internals/builtins/filesystem/readlink.md | 7 +- .../internals/builtins/filesystem/realpath.md | 7 +- .../builtins/filesystem/realpath_cache_get.md | 7 +- .../filesystem/realpath_cache_size.md | 7 +- docs/internals/builtins/filesystem/rename.md | 7 +- docs/internals/builtins/filesystem/rmdir.md | 7 +- docs/internals/builtins/filesystem/scandir.md | 7 +- docs/internals/builtins/filesystem/stat.md | 7 +- docs/internals/builtins/filesystem/symlink.md | 7 +- .../builtins/filesystem/sys_get_temp_dir.md | 7 +- docs/internals/builtins/filesystem/tempnam.md | 7 +- docs/internals/builtins/filesystem/tmpfile.md | 7 +- docs/internals/builtins/filesystem/touch.md | 7 +- docs/internals/builtins/filesystem/umask.md | 7 +- docs/internals/builtins/filesystem/unlink.md | 7 +- docs/internals/builtins/io/closedir.md | 7 +- docs/internals/builtins/io/fclose.md | 7 +- docs/internals/builtins/io/fdatasync.md | 7 +- docs/internals/builtins/io/feof.md | 7 +- docs/internals/builtins/io/fflush.md | 7 +- docs/internals/builtins/io/fgetc.md | 7 +- docs/internals/builtins/io/fgetcsv.md | 7 +- docs/internals/builtins/io/fgets.md | 7 +- docs/internals/builtins/io/file.md | 7 +- .../builtins/io/file_get_contents.md | 7 +- .../builtins/io/file_put_contents.md | 7 +- docs/internals/builtins/io/flock.md | 8 +- docs/internals/builtins/io/fopen.md | 7 +- docs/internals/builtins/io/fpassthru.md | 7 +- docs/internals/builtins/io/fprintf.md | 8 +- docs/internals/builtins/io/fputcsv.md | 7 +- docs/internals/builtins/io/fread.md | 7 +- docs/internals/builtins/io/fscanf.md | 8 +- docs/internals/builtins/io/fseek.md | 7 +- docs/internals/builtins/io/fstat.md | 7 +- docs/internals/builtins/io/fsync.md | 7 +- docs/internals/builtins/io/ftell.md | 7 +- docs/internals/builtins/io/ftruncate.md | 7 +- docs/internals/builtins/io/fwrite.md | 7 +- docs/internals/builtins/io/gethostbyaddr.md | 7 +- docs/internals/builtins/io/gethostbyname.md | 7 +- docs/internals/builtins/io/gethostname.md | 7 +- docs/internals/builtins/io/getprotobyname.md | 7 +- .../internals/builtins/io/getprotobynumber.md | 7 +- docs/internals/builtins/io/getservbyname.md | 7 +- docs/internals/builtins/io/getservbyport.md | 7 +- docs/internals/builtins/io/hash_file.md | 7 +- docs/internals/builtins/io/opendir.md | 7 +- docs/internals/builtins/io/readdir.md | 7 +- docs/internals/builtins/io/rewind.md | 7 +- docs/internals/builtins/io/rewinddir.md | 7 +- .../io/stream_bucket_make_writeable.md | 7 +- .../builtins/io/stream_bucket_new.md | 7 +- .../builtins/io/stream_context_create.md | 7 +- .../builtins/io/stream_context_get_default.md | 7 +- .../builtins/io/stream_context_get_options.md | 7 +- .../builtins/io/stream_context_get_params.md | 7 +- .../builtins/io/stream_context_set_default.md | 7 +- .../builtins/io/stream_context_set_option.md | 7 +- .../builtins/io/stream_context_set_params.md | 7 +- .../builtins/io/stream_copy_to_stream.md | 7 +- .../builtins/io/stream_filter_register.md | 7 +- .../builtins/io/stream_filter_remove.md | 7 +- .../builtins/io/stream_get_contents.md | 7 +- .../builtins/io/stream_get_filters.md | 7 +- docs/internals/builtins/io/stream_get_line.md | 7 +- .../builtins/io/stream_get_meta_data.md | 7 +- .../builtins/io/stream_get_transports.md | 7 +- .../builtins/io/stream_get_wrappers.md | 7 +- docs/internals/builtins/io/stream_is_local.md | 7 +- docs/internals/builtins/io/stream_isatty.md | 7 +- .../io/stream_resolve_include_path.md | 7 +- docs/internals/builtins/io/stream_select.md | 8 +- .../builtins/io/stream_set_blocking.md | 7 +- .../builtins/io/stream_set_chunk_size.md | 7 +- .../builtins/io/stream_set_read_buffer.md | 7 +- .../builtins/io/stream_set_timeout.md | 7 +- .../builtins/io/stream_set_write_buffer.md | 7 +- .../builtins/io/stream_socket_accept.md | 8 +- .../builtins/io/stream_socket_client.md | 7 +- .../io/stream_socket_enable_crypto.md | 7 +- .../builtins/io/stream_socket_get_name.md | 7 +- .../builtins/io/stream_socket_pair.md | 7 +- .../builtins/io/stream_socket_recvfrom.md | 8 +- .../builtins/io/stream_socket_sendto.md | 7 +- .../builtins/io/stream_socket_server.md | 7 +- .../builtins/io/stream_socket_shutdown.md | 7 +- .../builtins/io/stream_supports_lock.md | 7 +- .../builtins/io/stream_wrapper_register.md | 7 +- .../builtins/io/stream_wrapper_restore.md | 7 +- .../builtins/io/stream_wrapper_unregister.md | 7 +- docs/internals/builtins/io/vfprintf.md | 7 +- docs/internals/builtins/json/json_decode.md | 7 +- docs/internals/builtins/json/json_encode.md | 7 +- .../builtins/json/json_last_error.md | 7 +- .../builtins/json/json_last_error_msg.md | 7 +- docs/internals/builtins/json/json_validate.md | 7 +- docs/internals/builtins/math/abs.md | 7 +- docs/internals/builtins/math/acos.md | 7 +- docs/internals/builtins/math/asin.md | 7 +- docs/internals/builtins/math/atan.md | 7 +- docs/internals/builtins/math/atan2.md | 7 +- docs/internals/builtins/math/ceil.md | 7 +- docs/internals/builtins/math/clamp.md | 7 +- docs/internals/builtins/math/cos.md | 7 +- docs/internals/builtins/math/cosh.md | 7 +- docs/internals/builtins/math/deg2rad.md | 7 +- docs/internals/builtins/math/exp.md | 7 +- docs/internals/builtins/math/fdiv.md | 7 +- docs/internals/builtins/math/floor.md | 7 +- docs/internals/builtins/math/fmod.md | 7 +- docs/internals/builtins/math/hypot.md | 7 +- docs/internals/builtins/math/intdiv.md | 7 +- docs/internals/builtins/math/is_finite.md | 7 +- docs/internals/builtins/math/is_infinite.md | 7 +- docs/internals/builtins/math/is_nan.md | 7 +- docs/internals/builtins/math/log.md | 7 +- docs/internals/builtins/math/log10.md | 7 +- docs/internals/builtins/math/log2.md | 7 +- docs/internals/builtins/math/max.md | 8 +- docs/internals/builtins/math/min.md | 8 +- docs/internals/builtins/math/mt_rand.md | 7 +- docs/internals/builtins/math/pi.md | 7 +- docs/internals/builtins/math/pow.md | 7 +- docs/internals/builtins/math/rad2deg.md | 7 +- docs/internals/builtins/math/rand.md | 7 +- docs/internals/builtins/math/random_int.md | 7 +- docs/internals/builtins/math/round.md | 7 +- docs/internals/builtins/math/sin.md | 7 +- docs/internals/builtins/math/sinh.md | 7 +- docs/internals/builtins/math/sqrt.md | 7 +- docs/internals/builtins/math/tan.md | 7 +- docs/internals/builtins/math/tanh.md | 7 +- docs/internals/builtins/misc/buffer_new.md | 7 +- docs/internals/builtins/misc/define.md | 7 +- docs/internals/builtins/misc/defined.md | 7 +- docs/internals/builtins/misc/empty.md | 7 +- docs/internals/builtins/misc/header.md | 7 +- .../builtins/misc/http_response_code.md | 7 +- docs/internals/builtins/misc/isset.md | 8 +- docs/internals/builtins/misc/php_uname.md | 7 +- docs/internals/builtins/misc/phpversion.md | 7 +- docs/internals/builtins/misc/print_r.md | 7 +- docs/internals/builtins/misc/serialize.md | 6 +- docs/internals/builtins/misc/unserialize.md | 6 +- docs/internals/builtins/misc/unset.md | 8 +- docs/internals/builtins/misc/var_dump.md | 8 +- docs/internals/builtins/pointer/ptr.md | 7 +- docs/internals/builtins/pointer/ptr_get.md | 7 +- .../internals/builtins/pointer/ptr_is_null.md | 7 +- docs/internals/builtins/pointer/ptr_null.md | 7 +- docs/internals/builtins/pointer/ptr_offset.md | 7 +- docs/internals/builtins/pointer/ptr_read16.md | 7 +- docs/internals/builtins/pointer/ptr_read32.md | 7 +- docs/internals/builtins/pointer/ptr_read8.md | 7 +- .../builtins/pointer/ptr_read_string.md | 7 +- docs/internals/builtins/pointer/ptr_set.md | 7 +- docs/internals/builtins/pointer/ptr_sizeof.md | 7 +- .../internals/builtins/pointer/ptr_write16.md | 7 +- .../internals/builtins/pointer/ptr_write32.md | 7 +- docs/internals/builtins/pointer/ptr_write8.md | 7 +- .../builtins/pointer/ptr_write_string.md | 7 +- docs/internals/builtins/pointer/zval_free.md | 6 +- docs/internals/builtins/pointer/zval_pack.md | 6 +- docs/internals/builtins/pointer/zval_type.md | 6 +- .../internals/builtins/pointer/zval_unpack.md | 6 +- docs/internals/builtins/process/die.md | 7 +- docs/internals/builtins/process/exec.md | 7 +- docs/internals/builtins/process/exit.md | 7 +- docs/internals/builtins/process/passthru.md | 7 +- docs/internals/builtins/process/pclose.md | 7 +- docs/internals/builtins/process/popen.md | 7 +- docs/internals/builtins/process/readline.md | 7 +- docs/internals/builtins/process/shell_exec.md | 7 +- docs/internals/builtins/process/sleep.md | 7 +- docs/internals/builtins/process/system.md | 7 +- docs/internals/builtins/process/usleep.md | 7 +- .../internals/builtins/regex/mb_ereg_match.md | 7 +- docs/internals/builtins/regex/preg_match.md | 8 +- .../builtins/regex/preg_match_all.md | 8 +- docs/internals/builtins/regex/preg_replace.md | 7 +- .../builtins/regex/preg_replace_callback.md | 7 +- docs/internals/builtins/regex/preg_split.md | 7 +- docs/internals/builtins/spl/iterator_apply.md | 7 +- docs/internals/builtins/spl/iterator_count.md | 7 +- .../builtins/spl/iterator_to_array.md | 7 +- docs/internals/builtins/spl/spl_autoload.md | 7 +- .../builtins/spl/spl_autoload_call.md | 7 +- .../builtins/spl/spl_autoload_extensions.md | 7 +- .../builtins/spl/spl_autoload_functions.md | 7 +- .../builtins/spl/spl_autoload_register.md | 7 +- .../builtins/spl/spl_autoload_unregister.md | 7 +- docs/internals/builtins/spl/spl_classes.md | 7 +- .../internals/builtins/spl/spl_object_hash.md | 7 +- docs/internals/builtins/spl/spl_object_id.md | 7 +- docs/internals/builtins/streams/fsockopen.md | 8 +- docs/internals/builtins/streams/pfsockopen.md | 8 +- .../builtins/streams/stream_bucket_append.md | 7 +- .../builtins/streams/stream_bucket_prepend.md | 7 +- .../builtins/streams/stream_filter_append.md | 7 +- .../builtins/streams/stream_filter_prepend.md | 7 +- docs/internals/builtins/string/addslashes.md | 7 +- .../builtins/string/base64_decode.md | 7 +- .../builtins/string/base64_encode.md | 7 +- docs/internals/builtins/string/bin2hex.md | 7 +- docs/internals/builtins/string/chop.md | 7 +- docs/internals/builtins/string/chr.md | 7 +- docs/internals/builtins/string/crc32.md | 7 +- docs/internals/builtins/string/explode.md | 7 +- .../builtins/string/grapheme_strrev.md | 7 +- docs/internals/builtins/string/gzcompress.md | 7 +- docs/internals/builtins/string/gzdeflate.md | 7 +- docs/internals/builtins/string/gzinflate.md | 7 +- .../internals/builtins/string/gzuncompress.md | 7 +- docs/internals/builtins/string/hash.md | 7 +- docs/internals/builtins/string/hash_algos.md | 7 +- docs/internals/builtins/string/hash_copy.md | 7 +- docs/internals/builtins/string/hash_equals.md | 7 +- docs/internals/builtins/string/hash_final.md | 7 +- docs/internals/builtins/string/hash_hmac.md | 7 +- docs/internals/builtins/string/hash_init.md | 7 +- docs/internals/builtins/string/hash_update.md | 7 +- docs/internals/builtins/string/hex2bin.md | 7 +- .../builtins/string/html_entity_decode.md | 7 +- .../internals/builtins/string/htmlentities.md | 7 +- .../builtins/string/htmlspecialchars.md | 7 +- docs/internals/builtins/string/implode.md | 7 +- docs/internals/builtins/string/inet_ntop.md | 7 +- docs/internals/builtins/string/inet_pton.md | 7 +- docs/internals/builtins/string/ip2long.md | 7 +- docs/internals/builtins/string/lcfirst.md | 7 +- docs/internals/builtins/string/long2ip.md | 7 +- docs/internals/builtins/string/ltrim.md | 7 +- docs/internals/builtins/string/md5.md | 7 +- docs/internals/builtins/string/nl2br.md | 7 +- .../builtins/string/number_format.md | 7 +- docs/internals/builtins/string/ord.md | 7 +- docs/internals/builtins/string/printf.md | 8 +- .../internals/builtins/string/rawurldecode.md | 7 +- .../internals/builtins/string/rawurlencode.md | 7 +- docs/internals/builtins/string/rtrim.md | 7 +- docs/internals/builtins/string/sha1.md | 7 +- docs/internals/builtins/string/sprintf.md | 8 +- docs/internals/builtins/string/sscanf.md | 8 +- .../internals/builtins/string/str_contains.md | 7 +- .../builtins/string/str_ends_with.md | 7 +- .../internals/builtins/string/str_ireplace.md | 7 +- docs/internals/builtins/string/str_pad.md | 7 +- docs/internals/builtins/string/str_repeat.md | 7 +- docs/internals/builtins/string/str_replace.md | 7 +- docs/internals/builtins/string/str_split.md | 7 +- .../builtins/string/str_starts_with.md | 7 +- docs/internals/builtins/string/strcasecmp.md | 7 +- docs/internals/builtins/string/strcmp.md | 7 +- .../internals/builtins/string/stripslashes.md | 7 +- docs/internals/builtins/string/strlen.md | 7 +- docs/internals/builtins/string/strpos.md | 7 +- docs/internals/builtins/string/strrev.md | 7 +- docs/internals/builtins/string/strrpos.md | 7 +- docs/internals/builtins/string/strstr.md | 7 +- docs/internals/builtins/string/strtolower.md | 7 +- docs/internals/builtins/string/strtoupper.md | 7 +- docs/internals/builtins/string/substr.md | 7 +- .../builtins/string/substr_replace.md | 7 +- docs/internals/builtins/string/trim.md | 7 +- docs/internals/builtins/string/ucfirst.md | 7 +- docs/internals/builtins/string/ucwords.md | 7 +- docs/internals/builtins/string/urldecode.md | 7 +- docs/internals/builtins/string/urlencode.md | 7 +- docs/internals/builtins/string/vprintf.md | 7 +- docs/internals/builtins/string/vsprintf.md | 7 +- docs/internals/builtins/string/wordwrap.md | 7 +- docs/internals/builtins/type/boolval.md | 7 +- docs/internals/builtins/type/ctype_alnum.md | 7 +- docs/internals/builtins/type/ctype_alpha.md | 7 +- docs/internals/builtins/type/ctype_digit.md | 7 +- docs/internals/builtins/type/ctype_space.md | 7 +- docs/internals/builtins/type/floatval.md | 7 +- .../builtins/type/get_resource_id.md | 7 +- .../builtins/type/get_resource_type.md | 7 +- docs/internals/builtins/type/gettype.md | 7 +- docs/internals/builtins/type/intval.md | 7 +- docs/internals/builtins/type/is_array.md | 7 +- docs/internals/builtins/type/is_bool.md | 7 +- docs/internals/builtins/type/is_callable.md | 8 +- docs/internals/builtins/type/is_float.md | 7 +- docs/internals/builtins/type/is_int.md | 7 +- docs/internals/builtins/type/is_iterable.md | 7 +- docs/internals/builtins/type/is_null.md | 7 +- docs/internals/builtins/type/is_numeric.md | 7 +- docs/internals/builtins/type/is_object.md | 7 +- docs/internals/builtins/type/is_resource.md | 7 +- docs/internals/builtins/type/is_scalar.md | 7 +- docs/internals/builtins/type/is_string.md | 7 +- docs/internals/builtins/type/settype.md | 8 +- docs/php/builtins.md | 872 +- docs/php/builtins/array.md | 130 +- docs/php/builtins/array/array_all.md | 5 + docs/php/builtins/array/array_any.md | 5 + docs/php/builtins/array/array_chunk.md | 5 + docs/php/builtins/array/array_column.md | 5 + docs/php/builtins/array/array_combine.md | 5 + docs/php/builtins/array/array_diff.md | 5 + docs/php/builtins/array/array_diff_assoc.md | 5 + docs/php/builtins/array/array_diff_key.md | 5 + docs/php/builtins/array/array_fill.md | 5 + docs/php/builtins/array/array_fill_keys.md | 5 + docs/php/builtins/array/array_filter.md | 5 + docs/php/builtins/array/array_find.md | 5 + docs/php/builtins/array/array_flip.md | 5 + docs/php/builtins/array/array_intersect.md | 5 + .../builtins/array/array_intersect_assoc.md | 5 + .../php/builtins/array/array_intersect_key.md | 5 + docs/php/builtins/array/array_is_list.md | 5 + docs/php/builtins/array/array_key_exists.md | 5 + docs/php/builtins/array/array_key_first.md | 5 + docs/php/builtins/array/array_key_last.md | 5 + docs/php/builtins/array/array_keys.md | 5 + docs/php/builtins/array/array_map.md | 5 + docs/php/builtins/array/array_merge.md | 5 + .../builtins/array/array_merge_recursive.md | 5 + docs/php/builtins/array/array_multisort.md | 5 + docs/php/builtins/array/array_pad.md | 5 + docs/php/builtins/array/array_pop.md | 5 + docs/php/builtins/array/array_product.md | 5 + docs/php/builtins/array/array_push.md | 5 + docs/php/builtins/array/array_rand.md | 5 + docs/php/builtins/array/array_reduce.md | 5 + docs/php/builtins/array/array_replace.md | 5 + .../builtins/array/array_replace_recursive.md | 5 + docs/php/builtins/array/array_reverse.md | 5 + docs/php/builtins/array/array_search.md | 5 + docs/php/builtins/array/array_shift.md | 5 + docs/php/builtins/array/array_slice.md | 5 + docs/php/builtins/array/array_splice.md | 5 + docs/php/builtins/array/array_sum.md | 5 + docs/php/builtins/array/array_udiff.md | 5 + docs/php/builtins/array/array_uintersect.md | 5 + docs/php/builtins/array/array_unique.md | 5 + docs/php/builtins/array/array_unshift.md | 5 + docs/php/builtins/array/array_values.md | 5 + docs/php/builtins/array/array_walk.md | 5 + .../builtins/array/array_walk_recursive.md | 5 + docs/php/builtins/array/arsort.md | 5 + docs/php/builtins/array/asort.md | 5 + docs/php/builtins/array/call_user_func.md | 5 + .../builtins/array/call_user_func_array.md | 5 + docs/php/builtins/array/count.md | 5 + docs/php/builtins/array/in_array.md | 5 + docs/php/builtins/array/krsort.md | 5 + docs/php/builtins/array/ksort.md | 5 + docs/php/builtins/array/natcasesort.md | 5 + docs/php/builtins/array/natsort.md | 5 + docs/php/builtins/array/range.md | 5 + docs/php/builtins/array/rsort.md | 5 + docs/php/builtins/array/shuffle.md | 5 + docs/php/builtins/array/sort.md | 5 + docs/php/builtins/array/uasort.md | 5 + docs/php/builtins/array/uksort.md | 5 + docs/php/builtins/array/usort.md | 5 + docs/php/builtins/buffer.md | 8 +- docs/php/builtins/buffer/buffer_free.md | 5 + docs/php/builtins/buffer/buffer_len.md | 5 + docs/php/builtins/class.md | 46 +- docs/php/builtins/class/class_alias.md | 5 + .../builtins/class/class_attribute_args.md | 5 + .../builtins/class/class_attribute_names.md | 5 + docs/php/builtins/class/class_exists.md | 5 + .../builtins/class/class_get_attributes.md | 5 + docs/php/builtins/class/class_implements.md | 5 + docs/php/builtins/class/class_parents.md | 5 + docs/php/builtins/class/class_uses.md | 5 + docs/php/builtins/class/enum_exists.md | 5 + docs/php/builtins/class/function_exists.md | 5 + docs/php/builtins/class/get_called_class.md | 32 + docs/php/builtins/class/get_class.md | 7 +- docs/php/builtins/class/get_class_methods.md | 33 + docs/php/builtins/class/get_class_vars.md | 33 + .../builtins/class/get_declared_classes.md | 7 +- .../builtins/class/get_declared_interfaces.md | 7 +- .../php/builtins/class/get_declared_traits.md | 7 +- docs/php/builtins/class/get_object_vars.md | 33 + docs/php/builtins/class/get_parent_class.md | 7 +- docs/php/builtins/class/interface_exists.md | 7 +- docs/php/builtins/class/is_a.md | 7 +- docs/php/builtins/class/is_subclass_of.md | 7 +- docs/php/builtins/class/trait_exists.md | 7 +- docs/php/builtins/date.md | 30 +- docs/php/builtins/date/checkdate.md | 7 +- docs/php/builtins/date/date.md | 7 +- .../date/date_default_timezone_get.md | 7 +- .../date/date_default_timezone_set.md | 7 +- docs/php/builtins/date/getdate.md | 7 +- docs/php/builtins/date/gmdate.md | 7 +- docs/php/builtins/date/gmmktime.md | 7 +- docs/php/builtins/date/hrtime.md | 7 +- docs/php/builtins/date/localtime.md | 7 +- docs/php/builtins/date/microtime.md | 7 +- docs/php/builtins/date/mktime.md | 7 +- docs/php/builtins/date/strtotime.md | 7 +- docs/php/builtins/date/time.md | 7 +- docs/php/builtins/filesystem.md | 114 +- docs/php/builtins/filesystem/basename.md | 7 +- docs/php/builtins/filesystem/chdir.md | 7 +- docs/php/builtins/filesystem/chgrp.md | 7 +- docs/php/builtins/filesystem/chmod.md | 7 +- docs/php/builtins/filesystem/chown.md | 7 +- .../php/builtins/filesystem/clearstatcache.md | 7 +- docs/php/builtins/filesystem/copy.md | 7 +- docs/php/builtins/filesystem/dirname.md | 7 +- .../builtins/filesystem/disk_free_space.md | 7 +- .../builtins/filesystem/disk_total_space.md | 7 +- docs/php/builtins/filesystem/file_exists.md | 7 +- docs/php/builtins/filesystem/fileatime.md | 7 +- docs/php/builtins/filesystem/filectime.md | 7 +- docs/php/builtins/filesystem/filegroup.md | 7 +- docs/php/builtins/filesystem/fileinode.md | 7 +- docs/php/builtins/filesystem/filemtime.md | 7 +- docs/php/builtins/filesystem/fileowner.md | 7 +- docs/php/builtins/filesystem/fileperms.md | 7 +- docs/php/builtins/filesystem/filesize.md | 7 +- docs/php/builtins/filesystem/filetype.md | 7 +- docs/php/builtins/filesystem/fnmatch.md | 7 +- docs/php/builtins/filesystem/getcwd.md | 7 +- docs/php/builtins/filesystem/getenv.md | 7 +- docs/php/builtins/filesystem/glob.md | 7 +- docs/php/builtins/filesystem/is_dir.md | 7 +- docs/php/builtins/filesystem/is_executable.md | 7 +- docs/php/builtins/filesystem/is_file.md | 7 +- docs/php/builtins/filesystem/is_link.md | 7 +- docs/php/builtins/filesystem/is_readable.md | 7 +- docs/php/builtins/filesystem/is_writable.md | 7 +- docs/php/builtins/filesystem/is_writeable.md | 7 +- docs/php/builtins/filesystem/lchgrp.md | 7 +- docs/php/builtins/filesystem/lchown.md | 7 +- docs/php/builtins/filesystem/link.md | 7 +- docs/php/builtins/filesystem/linkinfo.md | 7 +- docs/php/builtins/filesystem/lstat.md | 7 +- docs/php/builtins/filesystem/mkdir.md | 7 +- docs/php/builtins/filesystem/pathinfo.md | 7 +- docs/php/builtins/filesystem/putenv.md | 7 +- docs/php/builtins/filesystem/readfile.md | 7 +- docs/php/builtins/filesystem/readlink.md | 7 +- docs/php/builtins/filesystem/realpath.md | 7 +- .../builtins/filesystem/realpath_cache_get.md | 7 +- .../filesystem/realpath_cache_size.md | 7 +- docs/php/builtins/filesystem/rename.md | 7 +- docs/php/builtins/filesystem/rmdir.md | 7 +- docs/php/builtins/filesystem/scandir.md | 7 +- docs/php/builtins/filesystem/stat.md | 7 +- docs/php/builtins/filesystem/symlink.md | 7 +- .../builtins/filesystem/sys_get_temp_dir.md | 7 +- docs/php/builtins/filesystem/tempnam.md | 7 +- docs/php/builtins/filesystem/tmpfile.md | 7 +- docs/php/builtins/filesystem/touch.md | 7 +- docs/php/builtins/filesystem/umask.md | 7 +- docs/php/builtins/filesystem/unlink.md | 7 +- docs/php/builtins/io.md | 158 +- docs/php/builtins/io/closedir.md | 7 +- docs/php/builtins/io/fclose.md | 7 +- docs/php/builtins/io/fdatasync.md | 7 +- docs/php/builtins/io/feof.md | 7 +- docs/php/builtins/io/fflush.md | 7 +- docs/php/builtins/io/fgetc.md | 7 +- docs/php/builtins/io/fgetcsv.md | 7 +- docs/php/builtins/io/fgets.md | 7 +- docs/php/builtins/io/file.md | 7 +- docs/php/builtins/io/file_get_contents.md | 7 +- docs/php/builtins/io/file_put_contents.md | 7 +- docs/php/builtins/io/flock.md | 7 +- docs/php/builtins/io/fopen.md | 7 +- docs/php/builtins/io/fpassthru.md | 7 +- docs/php/builtins/io/fprintf.md | 7 +- docs/php/builtins/io/fputcsv.md | 7 +- docs/php/builtins/io/fread.md | 7 +- docs/php/builtins/io/fscanf.md | 7 +- docs/php/builtins/io/fseek.md | 7 +- docs/php/builtins/io/fstat.md | 7 +- docs/php/builtins/io/fsync.md | 7 +- docs/php/builtins/io/ftell.md | 7 +- docs/php/builtins/io/ftruncate.md | 7 +- docs/php/builtins/io/fwrite.md | 7 +- docs/php/builtins/io/gethostbyaddr.md | 7 +- docs/php/builtins/io/gethostbyname.md | 7 +- docs/php/builtins/io/gethostname.md | 7 +- docs/php/builtins/io/getprotobyname.md | 7 +- docs/php/builtins/io/getprotobynumber.md | 7 +- docs/php/builtins/io/getservbyname.md | 7 +- docs/php/builtins/io/getservbyport.md | 7 +- docs/php/builtins/io/hash_file.md | 7 +- docs/php/builtins/io/opendir.md | 7 +- docs/php/builtins/io/readdir.md | 7 +- docs/php/builtins/io/rewind.md | 7 +- docs/php/builtins/io/rewinddir.md | 7 +- .../io/stream_bucket_make_writeable.md | 7 +- docs/php/builtins/io/stream_bucket_new.md | 7 +- docs/php/builtins/io/stream_context_create.md | 7 +- .../builtins/io/stream_context_get_default.md | 7 +- .../builtins/io/stream_context_get_options.md | 7 +- .../builtins/io/stream_context_get_params.md | 7 +- .../builtins/io/stream_context_set_default.md | 7 +- .../builtins/io/stream_context_set_option.md | 7 +- .../builtins/io/stream_context_set_params.md | 7 +- docs/php/builtins/io/stream_copy_to_stream.md | 7 +- .../php/builtins/io/stream_filter_register.md | 7 +- docs/php/builtins/io/stream_filter_remove.md | 7 +- docs/php/builtins/io/stream_get_contents.md | 7 +- docs/php/builtins/io/stream_get_filters.md | 7 +- docs/php/builtins/io/stream_get_line.md | 7 +- docs/php/builtins/io/stream_get_meta_data.md | 7 +- docs/php/builtins/io/stream_get_transports.md | 7 +- docs/php/builtins/io/stream_get_wrappers.md | 7 +- docs/php/builtins/io/stream_is_local.md | 7 +- docs/php/builtins/io/stream_isatty.md | 7 +- .../io/stream_resolve_include_path.md | 7 +- docs/php/builtins/io/stream_select.md | 7 +- docs/php/builtins/io/stream_set_blocking.md | 7 +- docs/php/builtins/io/stream_set_chunk_size.md | 7 +- .../php/builtins/io/stream_set_read_buffer.md | 7 +- docs/php/builtins/io/stream_set_timeout.md | 7 +- .../builtins/io/stream_set_write_buffer.md | 7 +- docs/php/builtins/io/stream_socket_accept.md | 7 +- docs/php/builtins/io/stream_socket_client.md | 7 +- .../io/stream_socket_enable_crypto.md | 7 +- .../php/builtins/io/stream_socket_get_name.md | 7 +- docs/php/builtins/io/stream_socket_pair.md | 7 +- .../php/builtins/io/stream_socket_recvfrom.md | 7 +- docs/php/builtins/io/stream_socket_sendto.md | 7 +- docs/php/builtins/io/stream_socket_server.md | 7 +- .../php/builtins/io/stream_socket_shutdown.md | 7 +- docs/php/builtins/io/stream_supports_lock.md | 7 +- .../builtins/io/stream_wrapper_register.md | 7 +- .../php/builtins/io/stream_wrapper_restore.md | 7 +- .../builtins/io/stream_wrapper_unregister.md | 7 +- docs/php/builtins/io/vfprintf.md | 7 +- docs/php/builtins/json.md | 14 +- docs/php/builtins/json/json_decode.md | 7 +- docs/php/builtins/json/json_encode.md | 7 +- docs/php/builtins/json/json_last_error.md | 7 +- docs/php/builtins/json/json_last_error_msg.md | 7 +- docs/php/builtins/json/json_validate.md | 7 +- docs/php/builtins/math.md | 76 +- docs/php/builtins/math/abs.md | 7 +- docs/php/builtins/math/acos.md | 7 +- docs/php/builtins/math/asin.md | 7 +- docs/php/builtins/math/atan.md | 7 +- docs/php/builtins/math/atan2.md | 7 +- docs/php/builtins/math/ceil.md | 7 +- docs/php/builtins/math/clamp.md | 7 +- docs/php/builtins/math/cos.md | 7 +- docs/php/builtins/math/cosh.md | 7 +- docs/php/builtins/math/deg2rad.md | 7 +- docs/php/builtins/math/exp.md | 7 +- docs/php/builtins/math/fdiv.md | 7 +- docs/php/builtins/math/floor.md | 7 +- docs/php/builtins/math/fmod.md | 7 +- docs/php/builtins/math/hypot.md | 7 +- docs/php/builtins/math/intdiv.md | 7 +- docs/php/builtins/math/is_finite.md | 7 +- docs/php/builtins/math/is_infinite.md | 7 +- docs/php/builtins/math/is_nan.md | 7 +- docs/php/builtins/math/log.md | 7 +- docs/php/builtins/math/log10.md | 7 +- docs/php/builtins/math/log2.md | 7 +- docs/php/builtins/math/max.md | 7 +- docs/php/builtins/math/min.md | 7 +- docs/php/builtins/math/mt_rand.md | 7 +- docs/php/builtins/math/pi.md | 7 +- docs/php/builtins/math/pow.md | 7 +- docs/php/builtins/math/rad2deg.md | 7 +- docs/php/builtins/math/rand.md | 7 +- docs/php/builtins/math/random_int.md | 7 +- docs/php/builtins/math/round.md | 7 +- docs/php/builtins/math/sin.md | 7 +- docs/php/builtins/math/sinh.md | 7 +- docs/php/builtins/math/sqrt.md | 7 +- docs/php/builtins/math/tan.md | 7 +- docs/php/builtins/math/tanh.md | 7 +- docs/php/builtins/misc.md | 32 +- docs/php/builtins/misc/buffer_new.md | 7 +- docs/php/builtins/misc/define.md | 7 +- docs/php/builtins/misc/defined.md | 7 +- docs/php/builtins/misc/empty.md | 7 +- docs/php/builtins/misc/header.md | 7 +- docs/php/builtins/misc/http_response_code.md | 7 +- docs/php/builtins/misc/isset.md | 7 +- docs/php/builtins/misc/php_uname.md | 7 +- docs/php/builtins/misc/phpversion.md | 7 +- docs/php/builtins/misc/print_r.md | 7 +- docs/php/builtins/misc/serialize.md | 7 +- docs/php/builtins/misc/unserialize.md | 7 +- docs/php/builtins/misc/unset.md | 7 +- docs/php/builtins/misc/var_dump.md | 7 +- docs/php/builtins/pointer.md | 42 +- docs/php/builtins/pointer/ptr.md | 7 +- docs/php/builtins/pointer/ptr_get.md | 7 +- docs/php/builtins/pointer/ptr_is_null.md | 7 +- docs/php/builtins/pointer/ptr_null.md | 7 +- docs/php/builtins/pointer/ptr_offset.md | 7 +- docs/php/builtins/pointer/ptr_read16.md | 7 +- docs/php/builtins/pointer/ptr_read32.md | 7 +- docs/php/builtins/pointer/ptr_read8.md | 7 +- docs/php/builtins/pointer/ptr_read_string.md | 7 +- docs/php/builtins/pointer/ptr_set.md | 7 +- docs/php/builtins/pointer/ptr_sizeof.md | 7 +- docs/php/builtins/pointer/ptr_write16.md | 7 +- docs/php/builtins/pointer/ptr_write32.md | 7 +- docs/php/builtins/pointer/ptr_write8.md | 7 +- docs/php/builtins/pointer/ptr_write_string.md | 7 +- docs/php/builtins/pointer/zval_free.md | 7 +- docs/php/builtins/pointer/zval_pack.md | 7 +- docs/php/builtins/pointer/zval_type.md | 7 +- docs/php/builtins/pointer/zval_unpack.md | 7 +- docs/php/builtins/process.md | 26 +- docs/php/builtins/process/die.md | 7 +- docs/php/builtins/process/exec.md | 7 +- docs/php/builtins/process/exit.md | 7 +- docs/php/builtins/process/passthru.md | 7 +- docs/php/builtins/process/pclose.md | 7 +- docs/php/builtins/process/popen.md | 7 +- docs/php/builtins/process/readline.md | 7 +- docs/php/builtins/process/shell_exec.md | 7 +- docs/php/builtins/process/sleep.md | 7 +- docs/php/builtins/process/system.md | 7 +- docs/php/builtins/process/usleep.md | 7 +- docs/php/builtins/regex.md | 16 +- docs/php/builtins/regex/mb_ereg_match.md | 7 +- docs/php/builtins/regex/preg_match.md | 7 +- docs/php/builtins/regex/preg_match_all.md | 7 +- docs/php/builtins/regex/preg_replace.md | 7 +- .../builtins/regex/preg_replace_callback.md | 7 +- docs/php/builtins/regex/preg_split.md | 7 +- docs/php/builtins/spl.md | 28 +- docs/php/builtins/spl/iterator_apply.md | 7 +- docs/php/builtins/spl/iterator_count.md | 7 +- docs/php/builtins/spl/iterator_to_array.md | 7 +- docs/php/builtins/spl/spl_autoload.md | 7 +- docs/php/builtins/spl/spl_autoload_call.md | 7 +- .../builtins/spl/spl_autoload_extensions.md | 7 +- .../builtins/spl/spl_autoload_functions.md | 7 +- .../php/builtins/spl/spl_autoload_register.md | 7 +- .../builtins/spl/spl_autoload_unregister.md | 7 +- docs/php/builtins/spl/spl_classes.md | 7 +- docs/php/builtins/spl/spl_object_hash.md | 7 +- docs/php/builtins/spl/spl_object_id.md | 7 +- docs/php/builtins/streams.md | 16 +- docs/php/builtins/streams/fsockopen.md | 7 +- docs/php/builtins/streams/pfsockopen.md | 7 +- .../builtins/streams/stream_bucket_append.md | 7 +- .../builtins/streams/stream_bucket_prepend.md | 7 +- .../builtins/streams/stream_filter_append.md | 7 +- .../builtins/streams/stream_filter_prepend.md | 7 +- docs/php/builtins/string.md | 146 +- docs/php/builtins/string/addslashes.md | 7 +- docs/php/builtins/string/base64_decode.md | 7 +- docs/php/builtins/string/base64_encode.md | 7 +- docs/php/builtins/string/bin2hex.md | 7 +- docs/php/builtins/string/chop.md | 7 +- docs/php/builtins/string/chr.md | 7 +- docs/php/builtins/string/crc32.md | 7 +- docs/php/builtins/string/explode.md | 7 +- docs/php/builtins/string/grapheme_strrev.md | 7 +- docs/php/builtins/string/gzcompress.md | 7 +- docs/php/builtins/string/gzdeflate.md | 7 +- docs/php/builtins/string/gzinflate.md | 7 +- docs/php/builtins/string/gzuncompress.md | 7 +- docs/php/builtins/string/hash.md | 7 +- docs/php/builtins/string/hash_algos.md | 7 +- docs/php/builtins/string/hash_copy.md | 7 +- docs/php/builtins/string/hash_equals.md | 7 +- docs/php/builtins/string/hash_final.md | 7 +- docs/php/builtins/string/hash_hmac.md | 7 +- docs/php/builtins/string/hash_init.md | 7 +- docs/php/builtins/string/hash_update.md | 7 +- docs/php/builtins/string/hex2bin.md | 7 +- .../php/builtins/string/html_entity_decode.md | 7 +- docs/php/builtins/string/htmlentities.md | 7 +- docs/php/builtins/string/htmlspecialchars.md | 7 +- docs/php/builtins/string/implode.md | 7 +- docs/php/builtins/string/inet_ntop.md | 7 +- docs/php/builtins/string/inet_pton.md | 7 +- docs/php/builtins/string/ip2long.md | 7 +- docs/php/builtins/string/lcfirst.md | 7 +- docs/php/builtins/string/long2ip.md | 7 +- docs/php/builtins/string/ltrim.md | 7 +- docs/php/builtins/string/md5.md | 7 +- docs/php/builtins/string/nl2br.md | 7 +- docs/php/builtins/string/number_format.md | 7 +- docs/php/builtins/string/ord.md | 7 +- docs/php/builtins/string/printf.md | 7 +- docs/php/builtins/string/rawurldecode.md | 7 +- docs/php/builtins/string/rawurlencode.md | 7 +- docs/php/builtins/string/rtrim.md | 7 +- docs/php/builtins/string/sha1.md | 7 +- docs/php/builtins/string/sprintf.md | 7 +- docs/php/builtins/string/sscanf.md | 7 +- docs/php/builtins/string/str_contains.md | 7 +- docs/php/builtins/string/str_ends_with.md | 7 +- docs/php/builtins/string/str_ireplace.md | 7 +- docs/php/builtins/string/str_pad.md | 7 +- docs/php/builtins/string/str_repeat.md | 7 +- docs/php/builtins/string/str_replace.md | 7 +- docs/php/builtins/string/str_split.md | 7 +- docs/php/builtins/string/str_starts_with.md | 7 +- docs/php/builtins/string/strcasecmp.md | 7 +- docs/php/builtins/string/strcmp.md | 7 +- docs/php/builtins/string/stripslashes.md | 7 +- docs/php/builtins/string/strlen.md | 7 +- docs/php/builtins/string/strpos.md | 7 +- docs/php/builtins/string/strrev.md | 7 +- docs/php/builtins/string/strrpos.md | 7 +- docs/php/builtins/string/strstr.md | 7 +- docs/php/builtins/string/strtolower.md | 7 +- docs/php/builtins/string/strtoupper.md | 7 +- docs/php/builtins/string/substr.md | 7 +- docs/php/builtins/string/substr_replace.md | 7 +- docs/php/builtins/string/trim.md | 7 +- docs/php/builtins/string/ucfirst.md | 7 +- docs/php/builtins/string/ucwords.md | 7 +- docs/php/builtins/string/urldecode.md | 7 +- docs/php/builtins/string/urlencode.md | 7 +- docs/php/builtins/string/vprintf.md | 7 +- docs/php/builtins/string/vsprintf.md | 7 +- docs/php/builtins/string/wordwrap.md | 7 +- docs/php/builtins/type.md | 50 +- docs/php/builtins/type/boolval.md | 7 +- docs/php/builtins/type/ctype_alnum.md | 7 +- docs/php/builtins/type/ctype_alpha.md | 7 +- docs/php/builtins/type/ctype_digit.md | 7 +- docs/php/builtins/type/ctype_space.md | 7 +- docs/php/builtins/type/floatval.md | 7 +- docs/php/builtins/type/get_resource_id.md | 7 +- docs/php/builtins/type/get_resource_type.md | 7 +- docs/php/builtins/type/gettype.md | 7 +- docs/php/builtins/type/intval.md | 7 +- docs/php/builtins/type/is_array.md | 7 +- docs/php/builtins/type/is_bool.md | 7 +- docs/php/builtins/type/is_callable.md | 7 +- docs/php/builtins/type/is_float.md | 7 +- docs/php/builtins/type/is_int.md | 7 +- docs/php/builtins/type/is_iterable.md | 7 +- docs/php/builtins/type/is_null.md | 7 +- docs/php/builtins/type/is_numeric.md | 7 +- docs/php/builtins/type/is_object.md | 7 +- docs/php/builtins/type/is_resource.md | 7 +- docs/php/builtins/type/is_scalar.md | 7 +- docs/php/builtins/type/is_string.md | 7 +- docs/php/builtins/type/settype.md | 7 +- scripts/docs/builtin_registry.json | 10690 +++++++++++++++- 909 files changed, 16959 insertions(+), 1706 deletions(-) create mode 100644 docs/internals/builtins/class/get_called_class.md create mode 100644 docs/internals/builtins/class/get_class_methods.md create mode 100644 docs/internals/builtins/class/get_class_vars.md create mode 100644 docs/internals/builtins/class/get_object_vars.md create mode 100644 docs/php/builtins/class/get_called_class.md create mode 100644 docs/php/builtins/class/get_class_methods.md create mode 100644 docs/php/builtins/class/get_class_vars.md create mode 100644 docs/php/builtins/class/get_object_vars.md diff --git a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md index 4b38d39715..fc63d10226 100644 --- a/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_gmmktime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_gmmktime_raw() — internals" description: "Compiler internals for __elephc_gmmktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 433 + order: 437 --- ## `__elephc_gmmktime_raw()` — internals @@ -36,6 +36,10 @@ function __elephc_gmmktime_raw(int $hour, int $minute, int $second, int $month, - **Arity**: takes exactly 6 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_mktime_raw.md b/docs/internals/builtins/_internal/__elephc_mktime_raw.md index 34c78e3e42..d4594ceb36 100644 --- a/docs/internals/builtins/_internal/__elephc_mktime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_mktime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_mktime_raw() — internals" description: "Compiler internals for __elephc_mktime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 434 + order: 438 --- ## `__elephc_mktime_raw()` — internals @@ -36,6 +36,10 @@ function __elephc_mktime_raw(int $hour, int $minute, int $second, int $month, in - **Arity**: takes exactly 6 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md index 43cb83bd8d..0df6b9c8c4 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_bzip2_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_bzip2_archive() — internals" description: "Compiler internals for __elephc_phar_bzip2_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 435 + order: 439 --- ## `__elephc_phar_bzip2_archive()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_bzip2_archive(string $src): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md index f360468a33..cbbf97f852 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_decompress_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_decompress_archive() — internals" description: "Compiler internals for __elephc_phar_decompress_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 436 + order: 440 --- ## `__elephc_phar_decompress_archive()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_decompress_archive(string $src): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md index 8530c2e649..f1cf109141 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_file_metadata() — internals" description: "Compiler internals for __elephc_phar_get_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 437 + order: 441 --- ## `__elephc_phar_get_file_metadata()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_file_metadata(string $url): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md index 3a582db45d..69095125a6 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_metadata() — internals" description: "Compiler internals for __elephc_phar_get_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 438 + order: 442 --- ## `__elephc_phar_get_metadata()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_metadata(string $filename): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md index 9d36ac4f1d..ab785b1d3d 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_hash() — internals" description: "Compiler internals for __elephc_phar_get_signature_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 439 + order: 443 --- ## `__elephc_phar_get_signature_hash()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_signature_hash(string $path): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md index cd75ce2a80..22a982fca5 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_signature_type.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_signature_type() — internals" description: "Compiler internals for __elephc_phar_get_signature_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 440 + order: 444 --- ## `__elephc_phar_get_signature_type()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_signature_type(string $path): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md index b5058c2c39..1a811a61f6 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_get_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_get_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_get_stub() — internals" description: "Compiler internals for __elephc_phar_get_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 441 + order: 445 --- ## `__elephc_phar_get_stub()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_get_stub(string $filename): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md index 43d82a812d..47f661eb6e 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md +++ b/docs/internals/builtins/_internal/__elephc_phar_gzip_archive.md @@ -2,7 +2,7 @@ title: "__elephc_phar_gzip_archive() — internals" description: "Compiler internals for __elephc_phar_gzip_archive(): lowering path, type checks, and runtime helpers." sidebar: - order: 442 + order: 446 --- ## `__elephc_phar_gzip_archive()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_gzip_archive(string $src): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md index 8df039804d..9c8543dbd4 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_list_entries.md +++ b/docs/internals/builtins/_internal/__elephc_phar_list_entries.md @@ -2,7 +2,7 @@ title: "__elephc_phar_list_entries() — internals" description: "Compiler internals for __elephc_phar_list_entries(): lowering path, type checks, and runtime helpers." sidebar: - order: 443 + order: 447 --- ## `__elephc_phar_list_entries()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_list_entries(string $filename): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md index 2071a79440..246ed5d392 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_compression.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_compression.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_compression() — internals" description: "Compiler internals for __elephc_phar_set_compression(): lowering path, type checks, and runtime helpers." sidebar: - order: 444 + order: 448 --- ## `__elephc_phar_set_compression()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_set_compression(string $filename, int $compression): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md index d6c62eaa72..d0a4d1354f 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_file_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_file_metadata() — internals" description: "Compiler internals for __elephc_phar_set_file_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 445 + order: 449 --- ## `__elephc_phar_set_file_metadata()` — internals @@ -34,6 +34,10 @@ function __elephc_phar_set_file_metadata(string $url, string $metadata): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md index c4d71d4840..b9af908d49 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_metadata.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_metadata() — internals" description: "Compiler internals for __elephc_phar_set_metadata(): lowering path, type checks, and runtime helpers." sidebar: - order: 446 + order: 450 --- ## `__elephc_phar_set_metadata()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_set_metadata(string $filename, string $metadata): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md index b6fbd1d0ed..8e64bae7a0 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_stub.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_stub.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_stub() — internals" description: "Compiler internals for __elephc_phar_set_stub(): lowering path, type checks, and runtime helpers." sidebar: - order: 447 + order: 451 --- ## `__elephc_phar_set_stub()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_set_stub(string $filename, string $stub): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md index 8ae1acc485..f2dc51ce58 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md +++ b/docs/internals/builtins/_internal/__elephc_phar_set_zip_password.md @@ -2,7 +2,7 @@ title: "__elephc_phar_set_zip_password() — internals" description: "Compiler internals for __elephc_phar_set_zip_password(): lowering path, type checks, and runtime helpers." sidebar: - order: 448 + order: 452 --- ## `__elephc_phar_set_zip_password()` — internals @@ -33,6 +33,10 @@ function __elephc_phar_set_zip_password(string $password): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md index 08dc961d7f..817eb9f247 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_hash.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_hash() — internals" description: "Compiler internals for __elephc_phar_sign_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 449 + order: 453 --- ## `__elephc_phar_sign_hash()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_sign_hash(string $path, string $algo): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md index eeff1344cb..ea1f5610c9 100644 --- a/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md +++ b/docs/internals/builtins/_internal/__elephc_phar_sign_openssl.md @@ -2,7 +2,7 @@ title: "__elephc_phar_sign_openssl() — internals" description: "Compiler internals for __elephc_phar_sign_openssl(): lowering path, type checks, and runtime helpers." sidebar: - order: 450 + order: 454 --- ## `__elephc_phar_sign_openssl()` — internals @@ -32,6 +32,10 @@ function __elephc_phar_sign_openssl(string $path, string $key): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md index 12ea06148a..852c031755 100644 --- a/docs/internals/builtins/_internal/__elephc_strtotime_raw.md +++ b/docs/internals/builtins/_internal/__elephc_strtotime_raw.md @@ -2,7 +2,7 @@ title: "__elephc_strtotime_raw() — internals" description: "Compiler internals for __elephc_strtotime_raw(): lowering path, type checks, and runtime helpers." sidebar: - order: 451 + order: 455 --- ## `__elephc_strtotime_raw()` — internals @@ -34,6 +34,10 @@ function __elephc_strtotime_raw(string $datetime, int $baseTimestamp = null): in - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - _No user-facing reference — this is a compiler internal helper._ diff --git a/docs/internals/builtins/array/array_all.md b/docs/internals/builtins/array/array_all.md index 25d33ca7c2..e64aa09826 100644 --- a/docs/internals/builtins/array/array_all.md +++ b/docs/internals/builtins/array/array_all.md @@ -34,6 +34,10 @@ function array_all(mixed $array, mixed $callback): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_all()`](../../../php/builtins/array/array_all.md) diff --git a/docs/internals/builtins/array/array_any.md b/docs/internals/builtins/array/array_any.md index 3ade04e927..d64910b86f 100644 --- a/docs/internals/builtins/array/array_any.md +++ b/docs/internals/builtins/array/array_any.md @@ -33,6 +33,10 @@ function array_any(mixed $array, mixed $callback): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_any()`](../../../php/builtins/array/array_any.md) diff --git a/docs/internals/builtins/array/array_chunk.md b/docs/internals/builtins/array/array_chunk.md index 46bd53333e..9ec2e4beb4 100644 --- a/docs/internals/builtins/array/array_chunk.md +++ b/docs/internals/builtins/array/array_chunk.md @@ -32,6 +32,11 @@ function array_chunk(array $array, int $length): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_chunk()`](../../../php/builtins/array/array_chunk.md) diff --git a/docs/internals/builtins/array/array_column.md b/docs/internals/builtins/array/array_column.md index ee7a38c63c..0c353bc116 100644 --- a/docs/internals/builtins/array/array_column.md +++ b/docs/internals/builtins/array/array_column.md @@ -32,6 +32,11 @@ function array_column(array $array, string $column_key): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_column.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_column()`](../../../php/builtins/array/array_column.md) diff --git a/docs/internals/builtins/array/array_combine.md b/docs/internals/builtins/array/array_combine.md index 3443233226..d9c0911885 100644 --- a/docs/internals/builtins/array/array_combine.md +++ b/docs/internals/builtins/array/array_combine.md @@ -32,6 +32,11 @@ function array_combine(array $keys, array $values): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_combine()`](../../../php/builtins/array/array_combine.md) diff --git a/docs/internals/builtins/array/array_diff.md b/docs/internals/builtins/array/array_diff.md index 0b5cbcfb05..a0fdb30f53 100644 --- a/docs/internals/builtins/array/array_diff.md +++ b/docs/internals/builtins/array/array_diff.md @@ -38,6 +38,12 @@ function array_diff(array $array, ...$arrays): array - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_diff()`](../../../php/builtins/array/array_diff.md) diff --git a/docs/internals/builtins/array/array_diff_assoc.md b/docs/internals/builtins/array/array_diff_assoc.md index 9d53e45ba5..b059876459 100644 --- a/docs/internals/builtins/array/array_diff_assoc.md +++ b/docs/internals/builtins/array/array_diff_assoc.md @@ -34,6 +34,10 @@ function array_diff_assoc(array $array, ...$arrays): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_diff_assoc()`](../../../php/builtins/array/array_diff_assoc.md) diff --git a/docs/internals/builtins/array/array_diff_key.md b/docs/internals/builtins/array/array_diff_key.md index d3278e96f6..4d316cfe7c 100644 --- a/docs/internals/builtins/array/array_diff_key.md +++ b/docs/internals/builtins/array/array_diff_key.md @@ -35,6 +35,12 @@ function array_diff_key(array $array, ...$arrays): array - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_diff_key()`](../../../php/builtins/array/array_diff_key.md) diff --git a/docs/internals/builtins/array/array_fill.md b/docs/internals/builtins/array/array_fill.md index 507edba2c7..746629621b 100644 --- a/docs/internals/builtins/array/array_fill.md +++ b/docs/internals/builtins/array/array_fill.md @@ -32,6 +32,11 @@ function array_fill(int $start_index, int $count, mixed $value): array - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_fill()`](../../../php/builtins/array/array_fill.md) diff --git a/docs/internals/builtins/array/array_fill_keys.md b/docs/internals/builtins/array/array_fill_keys.md index 28c5fa95fb..e3cfab84a9 100644 --- a/docs/internals/builtins/array/array_fill_keys.md +++ b/docs/internals/builtins/array/array_fill_keys.md @@ -32,6 +32,11 @@ function array_fill_keys(array $keys, mixed $value): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_fill_keys()`](../../../php/builtins/array/array_fill_keys.md) diff --git a/docs/internals/builtins/array/array_filter.md b/docs/internals/builtins/array/array_filter.md index 55b3469842..99765841d7 100644 --- a/docs/internals/builtins/array/array_filter.md +++ b/docs/internals/builtins/array/array_filter.md @@ -34,6 +34,11 @@ function array_filter(array $array, callable $callback = null, int $mode = 0): a - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_filter()`](../../../php/builtins/array/array_filter.md) diff --git a/docs/internals/builtins/array/array_find.md b/docs/internals/builtins/array/array_find.md index e9df77a4fd..8463488ea6 100644 --- a/docs/internals/builtins/array/array_find.md +++ b/docs/internals/builtins/array/array_find.md @@ -33,6 +33,10 @@ function array_find(mixed $array, mixed $callback): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_find()`](../../../php/builtins/array/array_find.md) diff --git a/docs/internals/builtins/array/array_flip.md b/docs/internals/builtins/array/array_flip.md index d1b8864780..a848d576a9 100644 --- a/docs/internals/builtins/array/array_flip.md +++ b/docs/internals/builtins/array/array_flip.md @@ -32,6 +32,11 @@ function array_flip(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_flip()`](../../../php/builtins/array/array_flip.md) diff --git a/docs/internals/builtins/array/array_intersect.md b/docs/internals/builtins/array/array_intersect.md index e16a8026f6..7bf296e6cb 100644 --- a/docs/internals/builtins/array/array_intersect.md +++ b/docs/internals/builtins/array/array_intersect.md @@ -37,6 +37,12 @@ function array_intersect(array $array, ...$arrays): array - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_intersect()`](../../../php/builtins/array/array_intersect.md) diff --git a/docs/internals/builtins/array/array_intersect_assoc.md b/docs/internals/builtins/array/array_intersect_assoc.md index eeea729525..b4c08d6146 100644 --- a/docs/internals/builtins/array/array_intersect_assoc.md +++ b/docs/internals/builtins/array/array_intersect_assoc.md @@ -37,6 +37,10 @@ function array_intersect_assoc(array $array, ...$arrays): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_intersect_assoc()`](../../../php/builtins/array/array_intersect_assoc.md) diff --git a/docs/internals/builtins/array/array_intersect_key.md b/docs/internals/builtins/array/array_intersect_key.md index 5d15bd7f22..a3ad5c2f48 100644 --- a/docs/internals/builtins/array/array_intersect_key.md +++ b/docs/internals/builtins/array/array_intersect_key.md @@ -34,6 +34,12 @@ function array_intersect_key(array $array, ...$arrays): array - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_intersect_key()`](../../../php/builtins/array/array_intersect_key.md) diff --git a/docs/internals/builtins/array/array_is_list.md b/docs/internals/builtins/array/array_is_list.md index 382509af6f..2428b51449 100644 --- a/docs/internals/builtins/array/array_is_list.md +++ b/docs/internals/builtins/array/array_is_list.md @@ -37,6 +37,10 @@ function array_is_list(mixed $array): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_is_list()`](../../../php/builtins/array/array_is_list.md) diff --git a/docs/internals/builtins/array/array_key_exists.md b/docs/internals/builtins/array/array_key_exists.md index 25f1d8abdb..8456be1378 100644 --- a/docs/internals/builtins/array/array_key_exists.md +++ b/docs/internals/builtins/array/array_key_exists.md @@ -33,6 +33,11 @@ function array_key_exists(string $key, array $array): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_key_exists()`](../../../php/builtins/array/array_key_exists.md) diff --git a/docs/internals/builtins/array/array_key_first.md b/docs/internals/builtins/array/array_key_first.md index 6e1607d78a..72b4203ed2 100644 --- a/docs/internals/builtins/array/array_key_first.md +++ b/docs/internals/builtins/array/array_key_first.md @@ -34,6 +34,10 @@ function array_key_first(array $array): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_key_first()`](../../../php/builtins/array/array_key_first.md) diff --git a/docs/internals/builtins/array/array_key_last.md b/docs/internals/builtins/array/array_key_last.md index dead1f9f6f..7c3c3d554a 100644 --- a/docs/internals/builtins/array/array_key_last.md +++ b/docs/internals/builtins/array/array_key_last.md @@ -34,6 +34,10 @@ function array_key_last(array $array): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_key_last()`](../../../php/builtins/array/array_key_last.md) diff --git a/docs/internals/builtins/array/array_keys.md b/docs/internals/builtins/array/array_keys.md index 4456b39e94..46a8faedfe 100644 --- a/docs/internals/builtins/array/array_keys.md +++ b/docs/internals/builtins/array/array_keys.md @@ -32,6 +32,11 @@ function array_keys(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_keys()`](../../../php/builtins/array/array_keys.md) diff --git a/docs/internals/builtins/array/array_map.md b/docs/internals/builtins/array/array_map.md index fab8abb735..b4c347c37d 100644 --- a/docs/internals/builtins/array/array_map.md +++ b/docs/internals/builtins/array/array_map.md @@ -33,6 +33,12 @@ function array_map(callable $callback, array $array, ...$arrays): array - **Arity**: takes exactly 2 arguments. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_map.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_map()`](../../../php/builtins/array/array_map.md) diff --git a/docs/internals/builtins/array/array_merge.md b/docs/internals/builtins/array/array_merge.md index afd57e2ddf..28d753b6a4 100644 --- a/docs/internals/builtins/array/array_merge.md +++ b/docs/internals/builtins/array/array_merge.md @@ -35,6 +35,12 @@ function array_merge(...$arrays): array - **Arity**: takes no arguments. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$arrays`. + ## Cross-references - [User reference for `array_merge()`](../../../php/builtins/array/array_merge.md) diff --git a/docs/internals/builtins/array/array_merge_recursive.md b/docs/internals/builtins/array/array_merge_recursive.md index 2b9dcd6b1f..d851d593ca 100644 --- a/docs/internals/builtins/array/array_merge_recursive.md +++ b/docs/internals/builtins/array/array_merge_recursive.md @@ -36,6 +36,10 @@ function array_merge_recursive(...$arrays): array - **Arity**: takes no arguments. - **Variadic**: collects excess arguments into `$arrays`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_merge_recursive()`](../../../php/builtins/array/array_merge_recursive.md) diff --git a/docs/internals/builtins/array/array_multisort.md b/docs/internals/builtins/array/array_multisort.md index cbfe1d8d7b..9bc8960212 100644 --- a/docs/internals/builtins/array/array_multisort.md +++ b/docs/internals/builtins/array/array_multisort.md @@ -36,6 +36,10 @@ function array_multisort(array $array1, int $array2): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array1`, `$array2`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_multisort()`](../../../php/builtins/array/array_multisort.md) diff --git a/docs/internals/builtins/array/array_pad.md b/docs/internals/builtins/array/array_pad.md index d24cbcc12e..c2c4958ad5 100644 --- a/docs/internals/builtins/array/array_pad.md +++ b/docs/internals/builtins/array/array_pad.md @@ -32,6 +32,11 @@ function array_pad(array $array, int $length, mixed $value): array - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_pad()`](../../../php/builtins/array/array_pad.md) diff --git a/docs/internals/builtins/array/array_pop.md b/docs/internals/builtins/array/array_pop.md index b524438a57..9f3e0fa6e7 100644 --- a/docs/internals/builtins/array/array_pop.md +++ b/docs/internals/builtins/array/array_pop.md @@ -35,6 +35,12 @@ function array_pop(array $array): mixed - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `array_pop()`](../../../php/builtins/array/array_pop.md) diff --git a/docs/internals/builtins/array/array_product.md b/docs/internals/builtins/array/array_product.md index 9823cef4cb..c049163212 100644 --- a/docs/internals/builtins/array/array_product.md +++ b/docs/internals/builtins/array/array_product.md @@ -33,6 +33,11 @@ function array_product(array $array): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_product.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_product()`](../../../php/builtins/array/array_product.md) diff --git a/docs/internals/builtins/array/array_push.md b/docs/internals/builtins/array/array_push.md index daf4d4d7b6..794b999892 100644 --- a/docs/internals/builtins/array/array_push.md +++ b/docs/internals/builtins/array/array_push.md @@ -34,6 +34,13 @@ function array_push(array $array, ...$values): void - **By-reference parameters**: `$array`. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_push.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `array_push()`](../../../php/builtins/array/array_push.md) diff --git a/docs/internals/builtins/array/array_rand.md b/docs/internals/builtins/array/array_rand.md index 8c035ab48a..867f9a04c8 100644 --- a/docs/internals/builtins/array/array_rand.md +++ b/docs/internals/builtins/array/array_rand.md @@ -34,6 +34,11 @@ function array_rand(array $array): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_rand()`](../../../php/builtins/array/array_rand.md) diff --git a/docs/internals/builtins/array/array_reduce.md b/docs/internals/builtins/array/array_reduce.md index 302d47051e..e01de0eda4 100644 --- a/docs/internals/builtins/array/array_reduce.md +++ b/docs/internals/builtins/array/array_reduce.md @@ -33,6 +33,11 @@ function array_reduce(array $array, callable $callback, mixed $initial = null): - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_reduce()`](../../../php/builtins/array/array_reduce.md) diff --git a/docs/internals/builtins/array/array_replace.md b/docs/internals/builtins/array/array_replace.md index 2df08cdd42..65ea065fa3 100644 --- a/docs/internals/builtins/array/array_replace.md +++ b/docs/internals/builtins/array/array_replace.md @@ -35,6 +35,10 @@ function array_replace(array $array, array $replacements): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_replace()`](../../../php/builtins/array/array_replace.md) diff --git a/docs/internals/builtins/array/array_replace_recursive.md b/docs/internals/builtins/array/array_replace_recursive.md index d85275ea3f..769ca5ef1d 100644 --- a/docs/internals/builtins/array/array_replace_recursive.md +++ b/docs/internals/builtins/array/array_replace_recursive.md @@ -34,6 +34,10 @@ function array_replace_recursive(array $array, array $replacements): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_replace_recursive()`](../../../php/builtins/array/array_replace_recursive.md) diff --git a/docs/internals/builtins/array/array_reverse.md b/docs/internals/builtins/array/array_reverse.md index fec27a94d2..f14f6ad2e9 100644 --- a/docs/internals/builtins/array/array_reverse.md +++ b/docs/internals/builtins/array/array_reverse.md @@ -32,6 +32,11 @@ function array_reverse(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_reverse()`](../../../php/builtins/array/array_reverse.md) diff --git a/docs/internals/builtins/array/array_search.md b/docs/internals/builtins/array/array_search.md index 2d0fc7acc5..d6b70200c1 100644 --- a/docs/internals/builtins/array/array_search.md +++ b/docs/internals/builtins/array/array_search.md @@ -32,6 +32,11 @@ function array_search(mixed $needle, array $haystack, bool $strict = false): mix - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_search.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_search()`](../../../php/builtins/array/array_search.md) diff --git a/docs/internals/builtins/array/array_shift.md b/docs/internals/builtins/array/array_shift.md index a724e7335d..1c3edef69b 100644 --- a/docs/internals/builtins/array/array_shift.md +++ b/docs/internals/builtins/array/array_shift.md @@ -33,6 +33,12 @@ function array_shift(array $array): mixed - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `array_shift()`](../../../php/builtins/array/array_shift.md) diff --git a/docs/internals/builtins/array/array_slice.md b/docs/internals/builtins/array/array_slice.md index f7c38d4b7f..5f9ef633dc 100644 --- a/docs/internals/builtins/array/array_slice.md +++ b/docs/internals/builtins/array/array_slice.md @@ -32,6 +32,11 @@ function array_slice(array $array, int $offset, int $length = null): array - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_slice()`](../../../php/builtins/array/array_slice.md) diff --git a/docs/internals/builtins/array/array_splice.md b/docs/internals/builtins/array/array_splice.md index 9253e05907..5ce948dbfb 100644 --- a/docs/internals/builtins/array/array_splice.md +++ b/docs/internals/builtins/array/array_splice.md @@ -33,6 +33,12 @@ function array_splice(array $array, int $offset, int $length = null): array - **Arity**: takes 2–3 arguments (1 optional). - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `array_splice()`](../../../php/builtins/array/array_splice.md) diff --git a/docs/internals/builtins/array/array_sum.md b/docs/internals/builtins/array/array_sum.md index 5157c8d94b..80f9b86e9a 100644 --- a/docs/internals/builtins/array/array_sum.md +++ b/docs/internals/builtins/array/array_sum.md @@ -34,6 +34,11 @@ function array_sum(array $array): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_sum()`](../../../php/builtins/array/array_sum.md) diff --git a/docs/internals/builtins/array/array_udiff.md b/docs/internals/builtins/array/array_udiff.md index 532e8c0091..782c00f8f1 100644 --- a/docs/internals/builtins/array/array_udiff.md +++ b/docs/internals/builtins/array/array_udiff.md @@ -32,6 +32,10 @@ function array_udiff(array $array1, array $array2, callable $callback): array - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_udiff()`](../../../php/builtins/array/array_udiff.md) diff --git a/docs/internals/builtins/array/array_uintersect.md b/docs/internals/builtins/array/array_uintersect.md index 3e785eacad..682ebfb06b 100644 --- a/docs/internals/builtins/array/array_uintersect.md +++ b/docs/internals/builtins/array/array_uintersect.md @@ -32,6 +32,10 @@ function array_uintersect(array $array1, array $array2, callable $callback): arr - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_uintersect()`](../../../php/builtins/array/array_uintersect.md) diff --git a/docs/internals/builtins/array/array_unique.md b/docs/internals/builtins/array/array_unique.md index 25f753cfee..8c1d7c4e01 100644 --- a/docs/internals/builtins/array/array_unique.md +++ b/docs/internals/builtins/array/array_unique.md @@ -34,6 +34,11 @@ function array_unique(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_unique()`](../../../php/builtins/array/array_unique.md) diff --git a/docs/internals/builtins/array/array_unshift.md b/docs/internals/builtins/array/array_unshift.md index 5a84c4c60c..9abb25dde7 100644 --- a/docs/internals/builtins/array/array_unshift.md +++ b/docs/internals/builtins/array/array_unshift.md @@ -34,6 +34,13 @@ function array_unshift(array $array, ...$values): int - **By-reference parameters**: `$array`. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `array_unshift()`](../../../php/builtins/array/array_unshift.md) diff --git a/docs/internals/builtins/array/array_values.md b/docs/internals/builtins/array/array_values.md index b5b3b90a05..3ef6505258 100644 --- a/docs/internals/builtins/array/array_values.md +++ b/docs/internals/builtins/array/array_values.md @@ -32,6 +32,11 @@ function array_values(array $array): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_values.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `array_values()`](../../../php/builtins/array/array_values.md) diff --git a/docs/internals/builtins/array/array_walk.md b/docs/internals/builtins/array/array_walk.md index 590629ecd4..d77c9e9ed9 100644 --- a/docs/internals/builtins/array/array_walk.md +++ b/docs/internals/builtins/array/array_walk.md @@ -34,6 +34,12 @@ function array_walk(array $array, callable $callback): void - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `array_walk()`](../../../php/builtins/array/array_walk.md) diff --git a/docs/internals/builtins/array/array_walk_recursive.md b/docs/internals/builtins/array/array_walk_recursive.md index ae21641238..5abc52c9ff 100644 --- a/docs/internals/builtins/array/array_walk_recursive.md +++ b/docs/internals/builtins/array/array_walk_recursive.md @@ -36,6 +36,10 @@ function array_walk_recursive(array $array, callable $callback): void - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `array_walk_recursive()`](../../../php/builtins/array/array_walk_recursive.md) diff --git a/docs/internals/builtins/array/arsort.md b/docs/internals/builtins/array/arsort.md index b30e0b6ea0..b301f7e3bb 100644 --- a/docs/internals/builtins/array/arsort.md +++ b/docs/internals/builtins/array/arsort.md @@ -38,6 +38,12 @@ function arsort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/arsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `arsort()`](../../../php/builtins/array/arsort.md) diff --git a/docs/internals/builtins/array/asort.md b/docs/internals/builtins/array/asort.md index d4cff7b4bd..0b1be75076 100644 --- a/docs/internals/builtins/array/asort.md +++ b/docs/internals/builtins/array/asort.md @@ -39,6 +39,12 @@ function asort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/asort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/asort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `asort()`](../../../php/builtins/array/asort.md) diff --git a/docs/internals/builtins/array/call_user_func.md b/docs/internals/builtins/array/call_user_func.md index 281edbe9f8..1692669873 100644 --- a/docs/internals/builtins/array/call_user_func.md +++ b/docs/internals/builtins/array/call_user_func.md @@ -35,6 +35,12 @@ function call_user_func(callable $callback, ...$args): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$args`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$args`. + ## Cross-references - [User reference for `call_user_func()`](../../../php/builtins/array/call_user_func.md) diff --git a/docs/internals/builtins/array/call_user_func_array.md b/docs/internals/builtins/array/call_user_func_array.md index ac8baf6e82..66e41e7a40 100644 --- a/docs/internals/builtins/array/call_user_func_array.md +++ b/docs/internals/builtins/array/call_user_func_array.md @@ -34,6 +34,11 @@ function call_user_func_array(callable $callback, array $args): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `call_user_func_array()`](../../../php/builtins/array/call_user_func_array.md) diff --git a/docs/internals/builtins/array/count.md b/docs/internals/builtins/array/count.md index 0d86ae6419..d37be19c5c 100644 --- a/docs/internals/builtins/array/count.md +++ b/docs/internals/builtins/array/count.md @@ -37,6 +37,11 @@ function count(array $value, int $mode = 0): int - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/count.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `count()`](../../../php/builtins/array/count.md) diff --git a/docs/internals/builtins/array/in_array.md b/docs/internals/builtins/array/in_array.md index eaceecf097..ab394f7e47 100644 --- a/docs/internals/builtins/array/in_array.md +++ b/docs/internals/builtins/array/in_array.md @@ -32,6 +32,11 @@ function in_array(mixed $needle, array $haystack, bool $strict = false): bool - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/in_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `in_array()`](../../../php/builtins/array/in_array.md) diff --git a/docs/internals/builtins/array/krsort.md b/docs/internals/builtins/array/krsort.md index ccd956e871..efe453a87e 100644 --- a/docs/internals/builtins/array/krsort.md +++ b/docs/internals/builtins/array/krsort.md @@ -36,6 +36,12 @@ function krsort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/krsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `krsort()`](../../../php/builtins/array/krsort.md) diff --git a/docs/internals/builtins/array/ksort.md b/docs/internals/builtins/array/ksort.md index dc4160fc4e..49e52da901 100644 --- a/docs/internals/builtins/array/ksort.md +++ b/docs/internals/builtins/array/ksort.md @@ -37,6 +37,12 @@ function ksort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/ksort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `ksort()`](../../../php/builtins/array/ksort.md) diff --git a/docs/internals/builtins/array/natcasesort.md b/docs/internals/builtins/array/natcasesort.md index ab599f0366..6caa65cf61 100644 --- a/docs/internals/builtins/array/natcasesort.md +++ b/docs/internals/builtins/array/natcasesort.md @@ -34,6 +34,12 @@ function natcasesort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `natcasesort()`](../../../php/builtins/array/natcasesort.md) diff --git a/docs/internals/builtins/array/natsort.md b/docs/internals/builtins/array/natsort.md index 0321f5afde..062387d3da 100644 --- a/docs/internals/builtins/array/natsort.md +++ b/docs/internals/builtins/array/natsort.md @@ -35,6 +35,12 @@ function natsort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/natsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `natsort()`](../../../php/builtins/array/natsort.md) diff --git a/docs/internals/builtins/array/range.md b/docs/internals/builtins/array/range.md index c213e1f81c..2d2f03be74 100644 --- a/docs/internals/builtins/array/range.md +++ b/docs/internals/builtins/array/range.md @@ -34,6 +34,11 @@ function range(mixed $start, mixed $end): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/range.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/range.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `range()`](../../../php/builtins/array/range.md) diff --git a/docs/internals/builtins/array/rsort.md b/docs/internals/builtins/array/rsort.md index 0f78283380..d81e68cf20 100644 --- a/docs/internals/builtins/array/rsort.md +++ b/docs/internals/builtins/array/rsort.md @@ -40,6 +40,12 @@ function rsort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/rsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `rsort()`](../../../php/builtins/array/rsort.md) diff --git a/docs/internals/builtins/array/shuffle.md b/docs/internals/builtins/array/shuffle.md index 0e34663a33..a203037bf6 100644 --- a/docs/internals/builtins/array/shuffle.md +++ b/docs/internals/builtins/array/shuffle.md @@ -34,6 +34,12 @@ function shuffle(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `shuffle()`](../../../php/builtins/array/shuffle.md) diff --git a/docs/internals/builtins/array/sort.md b/docs/internals/builtins/array/sort.md index 9a811cf5d8..67fe1986cd 100644 --- a/docs/internals/builtins/array/sort.md +++ b/docs/internals/builtins/array/sort.md @@ -41,6 +41,12 @@ function sort(array $array): bool - **Arity**: takes exactly 1 argument. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/sort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/sort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `sort()`](../../../php/builtins/array/sort.md) diff --git a/docs/internals/builtins/array/uasort.md b/docs/internals/builtins/array/uasort.md index 21f67fbc5a..db9a427472 100644 --- a/docs/internals/builtins/array/uasort.md +++ b/docs/internals/builtins/array/uasort.md @@ -34,6 +34,12 @@ function uasort(array $array, callable $callback): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/uasort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `uasort()`](../../../php/builtins/array/uasort.md) diff --git a/docs/internals/builtins/array/uksort.md b/docs/internals/builtins/array/uksort.md index 3357f721f8..7ada40c5ba 100644 --- a/docs/internals/builtins/array/uksort.md +++ b/docs/internals/builtins/array/uksort.md @@ -34,6 +34,12 @@ function uksort(array $array, callable $callback): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/uksort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `uksort()`](../../../php/builtins/array/uksort.md) diff --git a/docs/internals/builtins/array/usort.md b/docs/internals/builtins/array/usort.md index 66427c380f..ddbae1bcb6 100644 --- a/docs/internals/builtins/array/usort.md +++ b/docs/internals/builtins/array/usort.md @@ -34,6 +34,12 @@ function usort(array $array, callable $callback): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$array`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/usort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/usort.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$array`. + ## Cross-references - [User reference for `usort()`](../../../php/builtins/array/usort.md) diff --git a/docs/internals/builtins/buffer/buffer_free.md b/docs/internals/builtins/buffer/buffer_free.md index a49ffcb874..c54011722f 100644 --- a/docs/internals/builtins/buffer/buffer_free.md +++ b/docs/internals/builtins/buffer/buffer_free.md @@ -32,6 +32,11 @@ function buffer_free(buffer $buffer): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `buffer_free()`](../../../php/builtins/buffer/buffer_free.md) diff --git a/docs/internals/builtins/buffer/buffer_len.md b/docs/internals/builtins/buffer/buffer_len.md index ce22f1982f..a61052032f 100644 --- a/docs/internals/builtins/buffer/buffer_len.md +++ b/docs/internals/builtins/buffer/buffer_len.md @@ -32,6 +32,11 @@ function buffer_len(buffer $buffer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `buffer_len()`](../../../php/builtins/buffer/buffer_len.md) diff --git a/docs/internals/builtins/class/class_alias.md b/docs/internals/builtins/class/class_alias.md index b578f7661e..13ee959fcd 100644 --- a/docs/internals/builtins/class/class_alias.md +++ b/docs/internals/builtins/class/class_alias.md @@ -32,6 +32,11 @@ function class_alias(string $class, string $alias, bool $autoload = true): bool - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_alias()`](../../../php/builtins/class/class_alias.md) diff --git a/docs/internals/builtins/class/class_attribute_args.md b/docs/internals/builtins/class/class_attribute_args.md index 9cb2bc3e7c..f6df5979d8 100644 --- a/docs/internals/builtins/class/class_attribute_args.md +++ b/docs/internals/builtins/class/class_attribute_args.md @@ -32,6 +32,11 @@ function class_attribute_args(string $class_name, string $attribute_name): array - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_attribute_args()`](../../../php/builtins/class/class_attribute_args.md) diff --git a/docs/internals/builtins/class/class_attribute_names.md b/docs/internals/builtins/class/class_attribute_names.md index 8502d2cfce..82453203e7 100644 --- a/docs/internals/builtins/class/class_attribute_names.md +++ b/docs/internals/builtins/class/class_attribute_names.md @@ -32,6 +32,11 @@ function class_attribute_names(string $class_name): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_attribute_names()`](../../../php/builtins/class/class_attribute_names.md) diff --git a/docs/internals/builtins/class/class_exists.md b/docs/internals/builtins/class/class_exists.md index 0414fdd36a..833594322e 100644 --- a/docs/internals/builtins/class/class_exists.md +++ b/docs/internals/builtins/class/class_exists.md @@ -32,6 +32,11 @@ function class_exists(string $class, bool $autoload = true): bool - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_exists()`](../../../php/builtins/class/class_exists.md) diff --git a/docs/internals/builtins/class/class_get_attributes.md b/docs/internals/builtins/class/class_get_attributes.md index 6bf0e1384b..14c69bdcd7 100644 --- a/docs/internals/builtins/class/class_get_attributes.md +++ b/docs/internals/builtins/class/class_get_attributes.md @@ -32,6 +32,11 @@ function class_get_attributes(string $class_name): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_get_attributes()`](../../../php/builtins/class/class_get_attributes.md) diff --git a/docs/internals/builtins/class/class_implements.md b/docs/internals/builtins/class/class_implements.md index 982dcc0290..c2dfafcc1b 100644 --- a/docs/internals/builtins/class/class_implements.md +++ b/docs/internals/builtins/class/class_implements.md @@ -32,6 +32,11 @@ function class_implements(mixed $object_or_class, bool $autoload = true): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_implements()`](../../../php/builtins/class/class_implements.md) diff --git a/docs/internals/builtins/class/class_parents.md b/docs/internals/builtins/class/class_parents.md index 95d5f91740..9e3a631be8 100644 --- a/docs/internals/builtins/class/class_parents.md +++ b/docs/internals/builtins/class/class_parents.md @@ -32,6 +32,11 @@ function class_parents(mixed $object_or_class, bool $autoload = true): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_parents()`](../../../php/builtins/class/class_parents.md) diff --git a/docs/internals/builtins/class/class_uses.md b/docs/internals/builtins/class/class_uses.md index ad4517f2e0..0bb5944156 100644 --- a/docs/internals/builtins/class/class_uses.md +++ b/docs/internals/builtins/class/class_uses.md @@ -32,6 +32,11 @@ function class_uses(mixed $object_or_class, bool $autoload = true): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `class_uses()`](../../../php/builtins/class/class_uses.md) diff --git a/docs/internals/builtins/class/enum_exists.md b/docs/internals/builtins/class/enum_exists.md index 3ed3f27253..4b0d6369fb 100644 --- a/docs/internals/builtins/class/enum_exists.md +++ b/docs/internals/builtins/class/enum_exists.md @@ -32,6 +32,11 @@ function enum_exists(string $enum, bool $autoload = true): bool - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `enum_exists()`](../../../php/builtins/class/enum_exists.md) diff --git a/docs/internals/builtins/class/function_exists.md b/docs/internals/builtins/class/function_exists.md index 88ebb31fad..448b43c900 100644 --- a/docs/internals/builtins/class/function_exists.md +++ b/docs/internals/builtins/class/function_exists.md @@ -37,6 +37,11 @@ function function_exists(string $function): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `function_exists()`](../../../php/builtins/class/function_exists.md) diff --git a/docs/internals/builtins/class/get_called_class.md b/docs/internals/builtins/class/get_called_class.md new file mode 100644 index 0000000000..d9890bf301 --- /dev/null +++ b/docs/internals/builtins/class/get_called_class.md @@ -0,0 +1,38 @@ +--- +title: "get_called_class() — internals" +description: "Compiler internals for get_called_class(): lowering path, type checks, and runtime helpers." +sidebar: + order: 76 +--- + +## `get_called_class()` — internals + +## Where it lives + +- **Signature**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs) +- **Lowering**: [`(not lowered)`:0]() +- **Function symbol**: `(none — type-checker only)()` + + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function get_called_class(): mixed +``` + +## What the type checker enforces + +- **Arity**: takes no arguments. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `get_called_class()`](../../../php/builtins/class/get_called_class.md) diff --git a/docs/internals/builtins/class/get_class.md b/docs/internals/builtins/class/get_class.md index 323193ac61..868ca17e9a 100644 --- a/docs/internals/builtins/class/get_class.md +++ b/docs/internals/builtins/class/get_class.md @@ -2,7 +2,7 @@ title: "get_class() — internals" description: "Compiler internals for get_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 76 + order: 77 --- ## `get_class()` — internals @@ -32,6 +32,11 @@ function get_class(object $object = null): string - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_class()`](../../../php/builtins/class/get_class.md) diff --git a/docs/internals/builtins/class/get_class_methods.md b/docs/internals/builtins/class/get_class_methods.md new file mode 100644 index 0000000000..39bdd7cbae --- /dev/null +++ b/docs/internals/builtins/class/get_class_methods.md @@ -0,0 +1,38 @@ +--- +title: "get_class_methods() — internals" +description: "Compiler internals for get_class_methods(): lowering path, type checks, and runtime helpers." +sidebar: + order: 78 +--- + +## `get_class_methods()` — internals + +## Where it lives + +- **Signature**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs) +- **Lowering**: [`(not lowered)`:0]() +- **Function symbol**: `(none — type-checker only)()` + + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function get_class_methods(mixed $object_or_class): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `get_class_methods()`](../../../php/builtins/class/get_class_methods.md) diff --git a/docs/internals/builtins/class/get_class_vars.md b/docs/internals/builtins/class/get_class_vars.md new file mode 100644 index 0000000000..20e1181fb2 --- /dev/null +++ b/docs/internals/builtins/class/get_class_vars.md @@ -0,0 +1,38 @@ +--- +title: "get_class_vars() — internals" +description: "Compiler internals for get_class_vars(): lowering path, type checks, and runtime helpers." +sidebar: + order: 79 +--- + +## `get_class_vars()` — internals + +## Where it lives + +- **Signature**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs) +- **Lowering**: [`(not lowered)`:0]() +- **Function symbol**: `(none — type-checker only)()` + + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function get_class_vars(mixed $class): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `get_class_vars()`](../../../php/builtins/class/get_class_vars.md) diff --git a/docs/internals/builtins/class/get_declared_classes.md b/docs/internals/builtins/class/get_declared_classes.md index fc0d64a8c3..ed778ad9fc 100644 --- a/docs/internals/builtins/class/get_declared_classes.md +++ b/docs/internals/builtins/class/get_declared_classes.md @@ -2,7 +2,7 @@ title: "get_declared_classes() — internals" description: "Compiler internals for get_declared_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 77 + order: 80 --- ## `get_declared_classes()` — internals @@ -32,6 +32,11 @@ function get_declared_classes(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_declared_classes()`](../../../php/builtins/class/get_declared_classes.md) diff --git a/docs/internals/builtins/class/get_declared_interfaces.md b/docs/internals/builtins/class/get_declared_interfaces.md index e03bdfe839..c39bd5665f 100644 --- a/docs/internals/builtins/class/get_declared_interfaces.md +++ b/docs/internals/builtins/class/get_declared_interfaces.md @@ -2,7 +2,7 @@ title: "get_declared_interfaces() — internals" description: "Compiler internals for get_declared_interfaces(): lowering path, type checks, and runtime helpers." sidebar: - order: 78 + order: 81 --- ## `get_declared_interfaces()` — internals @@ -32,6 +32,11 @@ function get_declared_interfaces(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_declared_interfaces()`](../../../php/builtins/class/get_declared_interfaces.md) diff --git a/docs/internals/builtins/class/get_declared_traits.md b/docs/internals/builtins/class/get_declared_traits.md index 1d2b0cae74..c57b207a5b 100644 --- a/docs/internals/builtins/class/get_declared_traits.md +++ b/docs/internals/builtins/class/get_declared_traits.md @@ -2,7 +2,7 @@ title: "get_declared_traits() — internals" description: "Compiler internals for get_declared_traits(): lowering path, type checks, and runtime helpers." sidebar: - order: 79 + order: 82 --- ## `get_declared_traits()` — internals @@ -32,6 +32,11 @@ function get_declared_traits(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_declared_traits()`](../../../php/builtins/class/get_declared_traits.md) diff --git a/docs/internals/builtins/class/get_object_vars.md b/docs/internals/builtins/class/get_object_vars.md new file mode 100644 index 0000000000..335d79dbba --- /dev/null +++ b/docs/internals/builtins/class/get_object_vars.md @@ -0,0 +1,38 @@ +--- +title: "get_object_vars() — internals" +description: "Compiler internals for get_object_vars(): lowering path, type checks, and runtime helpers." +sidebar: + order: 83 +--- + +## `get_object_vars()` — internals + +## Where it lives + +- **Signature**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs) +- **Lowering**: [`(not lowered)`:0]() +- **Function symbol**: `(none — type-checker only)()` + + +## Runtime helpers + +_No direct `__rt_*` helpers captured — the lowering is inlined or routes through another builtin._ + +## Signature summary + +```php +function get_object_vars(mixed $object): mixed +``` + +## What the type checker enforces + +- **Arity**: takes exactly 1 argument. + +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + +## Cross-references + +- [User reference for `get_object_vars()`](../../../php/builtins/class/get_object_vars.md) diff --git a/docs/internals/builtins/class/get_parent_class.md b/docs/internals/builtins/class/get_parent_class.md index a574bde029..50d0001915 100644 --- a/docs/internals/builtins/class/get_parent_class.md +++ b/docs/internals/builtins/class/get_parent_class.md @@ -2,7 +2,7 @@ title: "get_parent_class() — internals" description: "Compiler internals for get_parent_class(): lowering path, type checks, and runtime helpers." sidebar: - order: 80 + order: 84 --- ## `get_parent_class()` — internals @@ -32,6 +32,11 @@ function get_parent_class(mixed $object_or_class = null): string - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_parent_class()`](../../../php/builtins/class/get_parent_class.md) diff --git a/docs/internals/builtins/class/interface_exists.md b/docs/internals/builtins/class/interface_exists.md index f6e1b21a3b..848f61c5fa 100644 --- a/docs/internals/builtins/class/interface_exists.md +++ b/docs/internals/builtins/class/interface_exists.md @@ -2,7 +2,7 @@ title: "interface_exists() — internals" description: "Compiler internals for interface_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 81 + order: 85 --- ## `interface_exists()` — internals @@ -32,6 +32,11 @@ function interface_exists(string $interface, bool $autoload = true): bool - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `interface_exists()`](../../../php/builtins/class/interface_exists.md) diff --git a/docs/internals/builtins/class/is_a.md b/docs/internals/builtins/class/is_a.md index 3ca72ca135..cfd08acde5 100644 --- a/docs/internals/builtins/class/is_a.md +++ b/docs/internals/builtins/class/is_a.md @@ -2,7 +2,7 @@ title: "is_a() — internals" description: "Compiler internals for is_a(): lowering path, type checks, and runtime helpers." sidebar: - order: 82 + order: 86 --- ## `is_a()` — internals @@ -32,6 +32,11 @@ function is_a(object $object_or_class, string $class, bool $allow_string = false - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_a()`](../../../php/builtins/class/is_a.md) diff --git a/docs/internals/builtins/class/is_subclass_of.md b/docs/internals/builtins/class/is_subclass_of.md index 404beddeac..d18e619544 100644 --- a/docs/internals/builtins/class/is_subclass_of.md +++ b/docs/internals/builtins/class/is_subclass_of.md @@ -2,7 +2,7 @@ title: "is_subclass_of() — internals" description: "Compiler internals for is_subclass_of(): lowering path, type checks, and runtime helpers." sidebar: - order: 83 + order: 87 --- ## `is_subclass_of()` — internals @@ -32,6 +32,11 @@ function is_subclass_of(mixed $object_or_class, string $class, bool $allow_strin - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_subclass_of()`](../../../php/builtins/class/is_subclass_of.md) diff --git a/docs/internals/builtins/class/trait_exists.md b/docs/internals/builtins/class/trait_exists.md index 96e23075a3..4632d523a5 100644 --- a/docs/internals/builtins/class/trait_exists.md +++ b/docs/internals/builtins/class/trait_exists.md @@ -2,7 +2,7 @@ title: "trait_exists() — internals" description: "Compiler internals for trait_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 84 + order: 88 --- ## `trait_exists()` — internals @@ -32,6 +32,11 @@ function trait_exists(string $trait, bool $autoload = true): bool - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `trait_exists()`](../../../php/builtins/class/trait_exists.md) diff --git a/docs/internals/builtins/date/checkdate.md b/docs/internals/builtins/date/checkdate.md index db4899f487..c5f9ace822 100644 --- a/docs/internals/builtins/date/checkdate.md +++ b/docs/internals/builtins/date/checkdate.md @@ -2,7 +2,7 @@ title: "checkdate() — internals" description: "Compiler internals for checkdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 85 + order: 89 --- ## `checkdate()` — internals @@ -37,6 +37,11 @@ function checkdate(int $month, int $day, int $year): bool - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `checkdate()`](../../../php/builtins/date/checkdate.md) diff --git a/docs/internals/builtins/date/date.md b/docs/internals/builtins/date/date.md index 284b39e58b..cc10b987d3 100644 --- a/docs/internals/builtins/date/date.md +++ b/docs/internals/builtins/date/date.md @@ -2,7 +2,7 @@ title: "date() — internals" description: "Compiler internals for date(): lowering path, type checks, and runtime helpers." sidebar: - order: 86 + order: 90 --- ## `date()` — internals @@ -34,6 +34,11 @@ function date(string $format, int $timestamp = null): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/date.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `date()`](../../../php/builtins/date/date.md) diff --git a/docs/internals/builtins/date/date_default_timezone_get.md b/docs/internals/builtins/date/date_default_timezone_get.md index cc1cbfbdb6..182e6b32d4 100644 --- a/docs/internals/builtins/date/date_default_timezone_get.md +++ b/docs/internals/builtins/date/date_default_timezone_get.md @@ -2,7 +2,7 @@ title: "date_default_timezone_get() — internals" description: "Compiler internals for date_default_timezone_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 87 + order: 91 --- ## `date_default_timezone_get()` — internals @@ -36,6 +36,11 @@ function date_default_timezone_get(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `date_default_timezone_get()`](../../../php/builtins/date/date_default_timezone_get.md) diff --git a/docs/internals/builtins/date/date_default_timezone_set.md b/docs/internals/builtins/date/date_default_timezone_set.md index 9cfee9f4a2..9815d9f977 100644 --- a/docs/internals/builtins/date/date_default_timezone_set.md +++ b/docs/internals/builtins/date/date_default_timezone_set.md @@ -2,7 +2,7 @@ title: "date_default_timezone_set() — internals" description: "Compiler internals for date_default_timezone_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 88 + order: 92 --- ## `date_default_timezone_set()` — internals @@ -39,6 +39,11 @@ function date_default_timezone_set(string $timezoneId): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `date_default_timezone_set()`](../../../php/builtins/date/date_default_timezone_set.md) diff --git a/docs/internals/builtins/date/getdate.md b/docs/internals/builtins/date/getdate.md index f13d4477cd..ce42d81bd9 100644 --- a/docs/internals/builtins/date/getdate.md +++ b/docs/internals/builtins/date/getdate.md @@ -2,7 +2,7 @@ title: "getdate() — internals" description: "Compiler internals for getdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 89 + order: 93 --- ## `getdate()` — internals @@ -38,6 +38,11 @@ function getdate(int $timestamp = null): array - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/getdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getdate()`](../../../php/builtins/date/getdate.md) diff --git a/docs/internals/builtins/date/gmdate.md b/docs/internals/builtins/date/gmdate.md index a1d773144b..30a22d4975 100644 --- a/docs/internals/builtins/date/gmdate.md +++ b/docs/internals/builtins/date/gmdate.md @@ -2,7 +2,7 @@ title: "gmdate() — internals" description: "Compiler internals for gmdate(): lowering path, type checks, and runtime helpers." sidebar: - order: 90 + order: 94 --- ## `gmdate()` — internals @@ -36,6 +36,11 @@ function gmdate(string $format, int $timestamp = null): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gmdate()`](../../../php/builtins/date/gmdate.md) diff --git a/docs/internals/builtins/date/gmmktime.md b/docs/internals/builtins/date/gmmktime.md index e711735072..ac6c4037ef 100644 --- a/docs/internals/builtins/date/gmmktime.md +++ b/docs/internals/builtins/date/gmmktime.md @@ -2,7 +2,7 @@ title: "gmmktime() — internals" description: "Compiler internals for gmmktime(): lowering path, type checks, and runtime helpers." sidebar: - order: 91 + order: 95 --- ## `gmmktime()` — internals @@ -37,6 +37,11 @@ function gmmktime(int $hour, int $minute, int $second, int $month, int $day, int - **Arity**: takes exactly 6 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gmmktime()`](../../../php/builtins/date/gmmktime.md) diff --git a/docs/internals/builtins/date/hrtime.md b/docs/internals/builtins/date/hrtime.md index fb9cb930cc..4c8b97a9fe 100644 --- a/docs/internals/builtins/date/hrtime.md +++ b/docs/internals/builtins/date/hrtime.md @@ -2,7 +2,7 @@ title: "hrtime() — internals" description: "Compiler internals for hrtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 92 + order: 96 --- ## `hrtime()` — internals @@ -38,6 +38,11 @@ function hrtime(bool $as_number = false): mixed - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hrtime()`](../../../php/builtins/date/hrtime.md) diff --git a/docs/internals/builtins/date/localtime.md b/docs/internals/builtins/date/localtime.md index 38d517a792..f966a72fca 100644 --- a/docs/internals/builtins/date/localtime.md +++ b/docs/internals/builtins/date/localtime.md @@ -2,7 +2,7 @@ title: "localtime() — internals" description: "Compiler internals for localtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 93 + order: 97 --- ## `localtime()` — internals @@ -39,6 +39,11 @@ function localtime(int $timestamp = -1, bool $associative = false): array - **Arity**: takes 0–2 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/localtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `localtime()`](../../../php/builtins/date/localtime.md) diff --git a/docs/internals/builtins/date/microtime.md b/docs/internals/builtins/date/microtime.md index cf145e0101..0d3b435b9f 100644 --- a/docs/internals/builtins/date/microtime.md +++ b/docs/internals/builtins/date/microtime.md @@ -2,7 +2,7 @@ title: "microtime() — internals" description: "Compiler internals for microtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 94 + order: 98 --- ## `microtime()` — internals @@ -42,6 +42,11 @@ function microtime(bool $as_float = false): mixed - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/microtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `microtime()`](../../../php/builtins/date/microtime.md) diff --git a/docs/internals/builtins/date/mktime.md b/docs/internals/builtins/date/mktime.md index 58eb3e8b67..296b4cbf3e 100644 --- a/docs/internals/builtins/date/mktime.md +++ b/docs/internals/builtins/date/mktime.md @@ -2,7 +2,7 @@ title: "mktime() — internals" description: "Compiler internals for mktime(): lowering path, type checks, and runtime helpers." sidebar: - order: 95 + order: 99 --- ## `mktime()` — internals @@ -35,6 +35,11 @@ function mktime(int $hour, int $minute, int $second, int $month, int $day, int $ - **Arity**: takes exactly 6 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/mktime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `mktime()`](../../../php/builtins/date/mktime.md) diff --git a/docs/internals/builtins/date/strtotime.md b/docs/internals/builtins/date/strtotime.md index 8075a0c1e5..f8ec487bff 100644 --- a/docs/internals/builtins/date/strtotime.md +++ b/docs/internals/builtins/date/strtotime.md @@ -2,7 +2,7 @@ title: "strtotime() — internals" description: "Compiler internals for strtotime(): lowering path, type checks, and runtime helpers." sidebar: - order: 96 + order: 100 --- ## `strtotime()` — internals @@ -39,6 +39,11 @@ function strtotime(string $datetime, int $baseTimestamp = null): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strtotime()`](../../../php/builtins/date/strtotime.md) diff --git a/docs/internals/builtins/date/time.md b/docs/internals/builtins/date/time.md index 6a5cb71832..25def47f16 100644 --- a/docs/internals/builtins/date/time.md +++ b/docs/internals/builtins/date/time.md @@ -2,7 +2,7 @@ title: "time() — internals" description: "Compiler internals for time(): lowering path, type checks, and runtime helpers." sidebar: - order: 97 + order: 101 --- ## `time()` — internals @@ -33,6 +33,11 @@ function time(): int - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/time.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/time.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `time()`](../../../php/builtins/date/time.md) diff --git a/docs/internals/builtins/filesystem/basename.md b/docs/internals/builtins/filesystem/basename.md index 9c4395d000..777532af10 100644 --- a/docs/internals/builtins/filesystem/basename.md +++ b/docs/internals/builtins/filesystem/basename.md @@ -2,7 +2,7 @@ title: "basename() — internals" description: "Compiler internals for basename(): lowering path, type checks, and runtime helpers." sidebar: - order: 98 + order: 102 --- ## `basename()` — internals @@ -32,6 +32,11 @@ function basename(string $path, string $suffix = ''): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `basename()`](../../../php/builtins/filesystem/basename.md) diff --git a/docs/internals/builtins/filesystem/chdir.md b/docs/internals/builtins/filesystem/chdir.md index 29f810231c..d586ef347c 100644 --- a/docs/internals/builtins/filesystem/chdir.md +++ b/docs/internals/builtins/filesystem/chdir.md @@ -2,7 +2,7 @@ title: "chdir() — internals" description: "Compiler internals for chdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 99 + order: 103 --- ## `chdir()` — internals @@ -37,6 +37,11 @@ function chdir(string $directory): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chdir()`](../../../php/builtins/filesystem/chdir.md) diff --git a/docs/internals/builtins/filesystem/chgrp.md b/docs/internals/builtins/filesystem/chgrp.md index 088d98a680..bfb096c9b5 100644 --- a/docs/internals/builtins/filesystem/chgrp.md +++ b/docs/internals/builtins/filesystem/chgrp.md @@ -2,7 +2,7 @@ title: "chgrp() — internals" description: "Compiler internals for chgrp(): lowering path, type checks, and runtime helpers." sidebar: - order: 100 + order: 104 --- ## `chgrp()` — internals @@ -33,6 +33,11 @@ function chgrp(string $filename, string $group): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chgrp()`](../../../php/builtins/filesystem/chgrp.md) diff --git a/docs/internals/builtins/filesystem/chmod.md b/docs/internals/builtins/filesystem/chmod.md index 3b5e76e916..8967dc2314 100644 --- a/docs/internals/builtins/filesystem/chmod.md +++ b/docs/internals/builtins/filesystem/chmod.md @@ -2,7 +2,7 @@ title: "chmod() — internals" description: "Compiler internals for chmod(): lowering path, type checks, and runtime helpers." sidebar: - order: 101 + order: 105 --- ## `chmod()` — internals @@ -32,6 +32,11 @@ function chmod(string $filename, int $permissions): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chmod()`](../../../php/builtins/filesystem/chmod.md) diff --git a/docs/internals/builtins/filesystem/chown.md b/docs/internals/builtins/filesystem/chown.md index ac86b3b4e9..ff024970ad 100644 --- a/docs/internals/builtins/filesystem/chown.md +++ b/docs/internals/builtins/filesystem/chown.md @@ -2,7 +2,7 @@ title: "chown() — internals" description: "Compiler internals for chown(): lowering path, type checks, and runtime helpers." sidebar: - order: 102 + order: 106 --- ## `chown()` — internals @@ -33,6 +33,11 @@ function chown(string $filename, string $user): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chown()`](../../../php/builtins/filesystem/chown.md) diff --git a/docs/internals/builtins/filesystem/clearstatcache.md b/docs/internals/builtins/filesystem/clearstatcache.md index 2417f80455..215e90a5b2 100644 --- a/docs/internals/builtins/filesystem/clearstatcache.md +++ b/docs/internals/builtins/filesystem/clearstatcache.md @@ -2,7 +2,7 @@ title: "clearstatcache() — internals" description: "Compiler internals for clearstatcache(): lowering path, type checks, and runtime helpers." sidebar: - order: 103 + order: 107 --- ## `clearstatcache()` — internals @@ -33,6 +33,11 @@ function clearstatcache(bool $clear_realpath_cache = false, string $filename = ' - **Arity**: takes 0–2 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `clearstatcache()`](../../../php/builtins/filesystem/clearstatcache.md) diff --git a/docs/internals/builtins/filesystem/copy.md b/docs/internals/builtins/filesystem/copy.md index dfc16a31fa..0ff3e31231 100644 --- a/docs/internals/builtins/filesystem/copy.md +++ b/docs/internals/builtins/filesystem/copy.md @@ -2,7 +2,7 @@ title: "copy() — internals" description: "Compiler internals for copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 104 + order: 108 --- ## `copy()` — internals @@ -36,6 +36,11 @@ function copy(string $from, string $to): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `copy()`](../../../php/builtins/filesystem/copy.md) diff --git a/docs/internals/builtins/filesystem/dirname.md b/docs/internals/builtins/filesystem/dirname.md index 6c38a29489..a6b8cee27d 100644 --- a/docs/internals/builtins/filesystem/dirname.md +++ b/docs/internals/builtins/filesystem/dirname.md @@ -2,7 +2,7 @@ title: "dirname() — internals" description: "Compiler internals for dirname(): lowering path, type checks, and runtime helpers." sidebar: - order: 105 + order: 109 --- ## `dirname()` — internals @@ -34,6 +34,11 @@ function dirname(string $path, int $levels = 1): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `dirname()`](../../../php/builtins/filesystem/dirname.md) diff --git a/docs/internals/builtins/filesystem/disk_free_space.md b/docs/internals/builtins/filesystem/disk_free_space.md index 8243b6f2e8..f9b7096bf0 100644 --- a/docs/internals/builtins/filesystem/disk_free_space.md +++ b/docs/internals/builtins/filesystem/disk_free_space.md @@ -2,7 +2,7 @@ title: "disk_free_space() — internals" description: "Compiler internals for disk_free_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 106 + order: 110 --- ## `disk_free_space()` — internals @@ -33,6 +33,11 @@ function disk_free_space(string $directory): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `disk_free_space()`](../../../php/builtins/filesystem/disk_free_space.md) diff --git a/docs/internals/builtins/filesystem/disk_total_space.md b/docs/internals/builtins/filesystem/disk_total_space.md index 85d7f85e99..c4bfc3d60d 100644 --- a/docs/internals/builtins/filesystem/disk_total_space.md +++ b/docs/internals/builtins/filesystem/disk_total_space.md @@ -2,7 +2,7 @@ title: "disk_total_space() — internals" description: "Compiler internals for disk_total_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 107 + order: 111 --- ## `disk_total_space()` — internals @@ -33,6 +33,11 @@ function disk_total_space(string $directory): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `disk_total_space()`](../../../php/builtins/filesystem/disk_total_space.md) diff --git a/docs/internals/builtins/filesystem/file_exists.md b/docs/internals/builtins/filesystem/file_exists.md index a73bca3467..f8d6c1f7aa 100644 --- a/docs/internals/builtins/filesystem/file_exists.md +++ b/docs/internals/builtins/filesystem/file_exists.md @@ -2,7 +2,7 @@ title: "file_exists() — internals" description: "Compiler internals for file_exists(): lowering path, type checks, and runtime helpers." sidebar: - order: 108 + order: 112 --- ## `file_exists()` — internals @@ -34,6 +34,11 @@ function file_exists(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `file_exists()`](../../../php/builtins/filesystem/file_exists.md) diff --git a/docs/internals/builtins/filesystem/fileatime.md b/docs/internals/builtins/filesystem/fileatime.md index 7233c8305e..21e3869e78 100644 --- a/docs/internals/builtins/filesystem/fileatime.md +++ b/docs/internals/builtins/filesystem/fileatime.md @@ -2,7 +2,7 @@ title: "fileatime() — internals" description: "Compiler internals for fileatime(): lowering path, type checks, and runtime helpers." sidebar: - order: 109 + order: 113 --- ## `fileatime()` — internals @@ -36,6 +36,11 @@ function fileatime(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fileatime()`](../../../php/builtins/filesystem/fileatime.md) diff --git a/docs/internals/builtins/filesystem/filectime.md b/docs/internals/builtins/filesystem/filectime.md index 7731c01108..703519fe73 100644 --- a/docs/internals/builtins/filesystem/filectime.md +++ b/docs/internals/builtins/filesystem/filectime.md @@ -2,7 +2,7 @@ title: "filectime() — internals" description: "Compiler internals for filectime(): lowering path, type checks, and runtime helpers." sidebar: - order: 110 + order: 114 --- ## `filectime()` — internals @@ -36,6 +36,11 @@ function filectime(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filectime()`](../../../php/builtins/filesystem/filectime.md) diff --git a/docs/internals/builtins/filesystem/filegroup.md b/docs/internals/builtins/filesystem/filegroup.md index ac29c14fc7..e0338d606f 100644 --- a/docs/internals/builtins/filesystem/filegroup.md +++ b/docs/internals/builtins/filesystem/filegroup.md @@ -2,7 +2,7 @@ title: "filegroup() — internals" description: "Compiler internals for filegroup(): lowering path, type checks, and runtime helpers." sidebar: - order: 111 + order: 115 --- ## `filegroup()` — internals @@ -36,6 +36,11 @@ function filegroup(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filegroup()`](../../../php/builtins/filesystem/filegroup.md) diff --git a/docs/internals/builtins/filesystem/fileinode.md b/docs/internals/builtins/filesystem/fileinode.md index c7a99169fb..2ff550531b 100644 --- a/docs/internals/builtins/filesystem/fileinode.md +++ b/docs/internals/builtins/filesystem/fileinode.md @@ -2,7 +2,7 @@ title: "fileinode() — internals" description: "Compiler internals for fileinode(): lowering path, type checks, and runtime helpers." sidebar: - order: 112 + order: 116 --- ## `fileinode()` — internals @@ -36,6 +36,11 @@ function fileinode(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fileinode()`](../../../php/builtins/filesystem/fileinode.md) diff --git a/docs/internals/builtins/filesystem/filemtime.md b/docs/internals/builtins/filesystem/filemtime.md index 6a800f621d..4260b064c4 100644 --- a/docs/internals/builtins/filesystem/filemtime.md +++ b/docs/internals/builtins/filesystem/filemtime.md @@ -2,7 +2,7 @@ title: "filemtime() — internals" description: "Compiler internals for filemtime(): lowering path, type checks, and runtime helpers." sidebar: - order: 113 + order: 117 --- ## `filemtime()` — internals @@ -37,6 +37,11 @@ function filemtime(string $filename): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filemtime()`](../../../php/builtins/filesystem/filemtime.md) diff --git a/docs/internals/builtins/filesystem/fileowner.md b/docs/internals/builtins/filesystem/fileowner.md index e2be28445b..3f94be073f 100644 --- a/docs/internals/builtins/filesystem/fileowner.md +++ b/docs/internals/builtins/filesystem/fileowner.md @@ -2,7 +2,7 @@ title: "fileowner() — internals" description: "Compiler internals for fileowner(): lowering path, type checks, and runtime helpers." sidebar: - order: 114 + order: 118 --- ## `fileowner()` — internals @@ -35,6 +35,11 @@ function fileowner(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fileowner()`](../../../php/builtins/filesystem/fileowner.md) diff --git a/docs/internals/builtins/filesystem/fileperms.md b/docs/internals/builtins/filesystem/fileperms.md index cf6a95e897..fd3adca837 100644 --- a/docs/internals/builtins/filesystem/fileperms.md +++ b/docs/internals/builtins/filesystem/fileperms.md @@ -2,7 +2,7 @@ title: "fileperms() — internals" description: "Compiler internals for fileperms(): lowering path, type checks, and runtime helpers." sidebar: - order: 115 + order: 119 --- ## `fileperms()` — internals @@ -36,6 +36,11 @@ function fileperms(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fileperms()`](../../../php/builtins/filesystem/fileperms.md) diff --git a/docs/internals/builtins/filesystem/filesize.md b/docs/internals/builtins/filesystem/filesize.md index 38060c85ff..5dd3097a50 100644 --- a/docs/internals/builtins/filesystem/filesize.md +++ b/docs/internals/builtins/filesystem/filesize.md @@ -2,7 +2,7 @@ title: "filesize() — internals" description: "Compiler internals for filesize(): lowering path, type checks, and runtime helpers." sidebar: - order: 116 + order: 120 --- ## `filesize()` — internals @@ -36,6 +36,11 @@ function filesize(string $filename): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filesize()`](../../../php/builtins/filesystem/filesize.md) diff --git a/docs/internals/builtins/filesystem/filetype.md b/docs/internals/builtins/filesystem/filetype.md index c22edde1f6..6993ae2462 100644 --- a/docs/internals/builtins/filesystem/filetype.md +++ b/docs/internals/builtins/filesystem/filetype.md @@ -2,7 +2,7 @@ title: "filetype() — internals" description: "Compiler internals for filetype(): lowering path, type checks, and runtime helpers." sidebar: - order: 117 + order: 121 --- ## `filetype()` — internals @@ -35,6 +35,11 @@ function filetype(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `filetype()`](../../../php/builtins/filesystem/filetype.md) diff --git a/docs/internals/builtins/filesystem/fnmatch.md b/docs/internals/builtins/filesystem/fnmatch.md index 1d00eb2c06..211679560f 100644 --- a/docs/internals/builtins/filesystem/fnmatch.md +++ b/docs/internals/builtins/filesystem/fnmatch.md @@ -2,7 +2,7 @@ title: "fnmatch() — internals" description: "Compiler internals for fnmatch(): lowering path, type checks, and runtime helpers." sidebar: - order: 118 + order: 122 --- ## `fnmatch()` — internals @@ -32,6 +32,11 @@ function fnmatch(string $pattern, string $filename, int $flags = 0): bool - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fnmatch()`](../../../php/builtins/filesystem/fnmatch.md) diff --git a/docs/internals/builtins/filesystem/getcwd.md b/docs/internals/builtins/filesystem/getcwd.md index 9243fb1ab3..0abdebaf32 100644 --- a/docs/internals/builtins/filesystem/getcwd.md +++ b/docs/internals/builtins/filesystem/getcwd.md @@ -2,7 +2,7 @@ title: "getcwd() — internals" description: "Compiler internals for getcwd(): lowering path, type checks, and runtime helpers." sidebar: - order: 119 + order: 123 --- ## `getcwd()` — internals @@ -34,6 +34,11 @@ function getcwd(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getcwd()`](../../../php/builtins/filesystem/getcwd.md) diff --git a/docs/internals/builtins/filesystem/getenv.md b/docs/internals/builtins/filesystem/getenv.md index 0fac463983..7fe7fa1a53 100644 --- a/docs/internals/builtins/filesystem/getenv.md +++ b/docs/internals/builtins/filesystem/getenv.md @@ -2,7 +2,7 @@ title: "getenv() — internals" description: "Compiler internals for getenv(): lowering path, type checks, and runtime helpers." sidebar: - order: 120 + order: 124 --- ## `getenv()` — internals @@ -33,6 +33,11 @@ function getenv(string $name): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getenv()`](../../../php/builtins/filesystem/getenv.md) diff --git a/docs/internals/builtins/filesystem/glob.md b/docs/internals/builtins/filesystem/glob.md index f55d70dc80..071847ad65 100644 --- a/docs/internals/builtins/filesystem/glob.md +++ b/docs/internals/builtins/filesystem/glob.md @@ -2,7 +2,7 @@ title: "glob() — internals" description: "Compiler internals for glob(): lowering path, type checks, and runtime helpers." sidebar: - order: 121 + order: 125 --- ## `glob()` — internals @@ -33,6 +33,11 @@ function glob(string $pattern): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `glob()`](../../../php/builtins/filesystem/glob.md) diff --git a/docs/internals/builtins/filesystem/is_dir.md b/docs/internals/builtins/filesystem/is_dir.md index 8f699adcbb..db5954595b 100644 --- a/docs/internals/builtins/filesystem/is_dir.md +++ b/docs/internals/builtins/filesystem/is_dir.md @@ -2,7 +2,7 @@ title: "is_dir() — internals" description: "Compiler internals for is_dir(): lowering path, type checks, and runtime helpers." sidebar: - order: 122 + order: 126 --- ## `is_dir()` — internals @@ -35,6 +35,11 @@ function is_dir(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_dir()`](../../../php/builtins/filesystem/is_dir.md) diff --git a/docs/internals/builtins/filesystem/is_executable.md b/docs/internals/builtins/filesystem/is_executable.md index 9550fdfcb2..dfe9ab3345 100644 --- a/docs/internals/builtins/filesystem/is_executable.md +++ b/docs/internals/builtins/filesystem/is_executable.md @@ -2,7 +2,7 @@ title: "is_executable() — internals" description: "Compiler internals for is_executable(): lowering path, type checks, and runtime helpers." sidebar: - order: 123 + order: 127 --- ## `is_executable()` — internals @@ -36,6 +36,11 @@ function is_executable(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_executable()`](../../../php/builtins/filesystem/is_executable.md) diff --git a/docs/internals/builtins/filesystem/is_file.md b/docs/internals/builtins/filesystem/is_file.md index 4b2a35ea45..5a5927cac0 100644 --- a/docs/internals/builtins/filesystem/is_file.md +++ b/docs/internals/builtins/filesystem/is_file.md @@ -2,7 +2,7 @@ title: "is_file() — internals" description: "Compiler internals for is_file(): lowering path, type checks, and runtime helpers." sidebar: - order: 124 + order: 128 --- ## `is_file()` — internals @@ -35,6 +35,11 @@ function is_file(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_file()`](../../../php/builtins/filesystem/is_file.md) diff --git a/docs/internals/builtins/filesystem/is_link.md b/docs/internals/builtins/filesystem/is_link.md index 26df330e09..9c6b8a8f28 100644 --- a/docs/internals/builtins/filesystem/is_link.md +++ b/docs/internals/builtins/filesystem/is_link.md @@ -2,7 +2,7 @@ title: "is_link() — internals" description: "Compiler internals for is_link(): lowering path, type checks, and runtime helpers." sidebar: - order: 125 + order: 129 --- ## `is_link()` — internals @@ -36,6 +36,11 @@ function is_link(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_link()`](../../../php/builtins/filesystem/is_link.md) diff --git a/docs/internals/builtins/filesystem/is_readable.md b/docs/internals/builtins/filesystem/is_readable.md index 0e61b1965f..5c853dcdc6 100644 --- a/docs/internals/builtins/filesystem/is_readable.md +++ b/docs/internals/builtins/filesystem/is_readable.md @@ -2,7 +2,7 @@ title: "is_readable() — internals" description: "Compiler internals for is_readable(): lowering path, type checks, and runtime helpers." sidebar: - order: 126 + order: 130 --- ## `is_readable()` — internals @@ -35,6 +35,11 @@ function is_readable(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_readable()`](../../../php/builtins/filesystem/is_readable.md) diff --git a/docs/internals/builtins/filesystem/is_writable.md b/docs/internals/builtins/filesystem/is_writable.md index f20c80490c..91d8827bea 100644 --- a/docs/internals/builtins/filesystem/is_writable.md +++ b/docs/internals/builtins/filesystem/is_writable.md @@ -2,7 +2,7 @@ title: "is_writable() — internals" description: "Compiler internals for is_writable(): lowering path, type checks, and runtime helpers." sidebar: - order: 127 + order: 131 --- ## `is_writable()` — internals @@ -35,6 +35,11 @@ function is_writable(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_writable()`](../../../php/builtins/filesystem/is_writable.md) diff --git a/docs/internals/builtins/filesystem/is_writeable.md b/docs/internals/builtins/filesystem/is_writeable.md index 906ddbb7fe..ebe498e278 100644 --- a/docs/internals/builtins/filesystem/is_writeable.md +++ b/docs/internals/builtins/filesystem/is_writeable.md @@ -2,7 +2,7 @@ title: "is_writeable() — internals" description: "Compiler internals for is_writeable(): lowering path, type checks, and runtime helpers." sidebar: - order: 128 + order: 132 --- ## `is_writeable()` — internals @@ -35,6 +35,11 @@ function is_writeable(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_writeable()`](../../../php/builtins/filesystem/is_writeable.md) diff --git a/docs/internals/builtins/filesystem/lchgrp.md b/docs/internals/builtins/filesystem/lchgrp.md index 054d954188..4b2e612ac4 100644 --- a/docs/internals/builtins/filesystem/lchgrp.md +++ b/docs/internals/builtins/filesystem/lchgrp.md @@ -2,7 +2,7 @@ title: "lchgrp() — internals" description: "Compiler internals for lchgrp(): lowering path, type checks, and runtime helpers." sidebar: - order: 129 + order: 133 --- ## `lchgrp()` — internals @@ -33,6 +33,11 @@ function lchgrp(string $filename, string $group): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `lchgrp()`](../../../php/builtins/filesystem/lchgrp.md) diff --git a/docs/internals/builtins/filesystem/lchown.md b/docs/internals/builtins/filesystem/lchown.md index d4f2cf70d6..47d3cb3ccc 100644 --- a/docs/internals/builtins/filesystem/lchown.md +++ b/docs/internals/builtins/filesystem/lchown.md @@ -2,7 +2,7 @@ title: "lchown() — internals" description: "Compiler internals for lchown(): lowering path, type checks, and runtime helpers." sidebar: - order: 130 + order: 134 --- ## `lchown()` — internals @@ -33,6 +33,11 @@ function lchown(string $filename, string $user): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `lchown()`](../../../php/builtins/filesystem/lchown.md) diff --git a/docs/internals/builtins/filesystem/link.md b/docs/internals/builtins/filesystem/link.md index 4e2421a4cd..2f8d384ce8 100644 --- a/docs/internals/builtins/filesystem/link.md +++ b/docs/internals/builtins/filesystem/link.md @@ -2,7 +2,7 @@ title: "link() — internals" description: "Compiler internals for link(): lowering path, type checks, and runtime helpers." sidebar: - order: 131 + order: 135 --- ## `link()` — internals @@ -36,6 +36,11 @@ function link(string $target, string $link): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `link()`](../../../php/builtins/filesystem/link.md) diff --git a/docs/internals/builtins/filesystem/linkinfo.md b/docs/internals/builtins/filesystem/linkinfo.md index bec27ff255..10991a9f59 100644 --- a/docs/internals/builtins/filesystem/linkinfo.md +++ b/docs/internals/builtins/filesystem/linkinfo.md @@ -2,7 +2,7 @@ title: "linkinfo() — internals" description: "Compiler internals for linkinfo(): lowering path, type checks, and runtime helpers." sidebar: - order: 132 + order: 136 --- ## `linkinfo()` — internals @@ -36,6 +36,11 @@ function linkinfo(string $path): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `linkinfo()`](../../../php/builtins/filesystem/linkinfo.md) diff --git a/docs/internals/builtins/filesystem/lstat.md b/docs/internals/builtins/filesystem/lstat.md index 1adfae4bb3..f0de434f8e 100644 --- a/docs/internals/builtins/filesystem/lstat.md +++ b/docs/internals/builtins/filesystem/lstat.md @@ -2,7 +2,7 @@ title: "lstat() — internals" description: "Compiler internals for lstat(): lowering path, type checks, and runtime helpers." sidebar: - order: 133 + order: 137 --- ## `lstat()` — internals @@ -34,6 +34,11 @@ function lstat(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `lstat()`](../../../php/builtins/filesystem/lstat.md) diff --git a/docs/internals/builtins/filesystem/mkdir.md b/docs/internals/builtins/filesystem/mkdir.md index 4bc31dbde1..878c629c2d 100644 --- a/docs/internals/builtins/filesystem/mkdir.md +++ b/docs/internals/builtins/filesystem/mkdir.md @@ -2,7 +2,7 @@ title: "mkdir() — internals" description: "Compiler internals for mkdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 134 + order: 138 --- ## `mkdir()` — internals @@ -37,6 +37,11 @@ function mkdir(string $directory): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `mkdir()`](../../../php/builtins/filesystem/mkdir.md) diff --git a/docs/internals/builtins/filesystem/pathinfo.md b/docs/internals/builtins/filesystem/pathinfo.md index ebd9883a0c..474456d5fd 100644 --- a/docs/internals/builtins/filesystem/pathinfo.md +++ b/docs/internals/builtins/filesystem/pathinfo.md @@ -2,7 +2,7 @@ title: "pathinfo() — internals" description: "Compiler internals for pathinfo(): lowering path, type checks, and runtime helpers." sidebar: - order: 135 + order: 139 --- ## `pathinfo()` — internals @@ -33,6 +33,11 @@ function pathinfo(string $path, int $flags = 15): array - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `pathinfo()`](../../../php/builtins/filesystem/pathinfo.md) diff --git a/docs/internals/builtins/filesystem/putenv.md b/docs/internals/builtins/filesystem/putenv.md index 39db571ebf..a37f2d5d38 100644 --- a/docs/internals/builtins/filesystem/putenv.md +++ b/docs/internals/builtins/filesystem/putenv.md @@ -2,7 +2,7 @@ title: "putenv() — internals" description: "Compiler internals for putenv(): lowering path, type checks, and runtime helpers." sidebar: - order: 136 + order: 140 --- ## `putenv()` — internals @@ -33,6 +33,11 @@ function putenv(string $assignment): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `putenv()`](../../../php/builtins/filesystem/putenv.md) diff --git a/docs/internals/builtins/filesystem/readfile.md b/docs/internals/builtins/filesystem/readfile.md index ba9ab3f8b9..e884c81357 100644 --- a/docs/internals/builtins/filesystem/readfile.md +++ b/docs/internals/builtins/filesystem/readfile.md @@ -2,7 +2,7 @@ title: "readfile() — internals" description: "Compiler internals for readfile(): lowering path, type checks, and runtime helpers." sidebar: - order: 137 + order: 141 --- ## `readfile()` — internals @@ -32,6 +32,11 @@ function readfile(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `readfile()`](../../../php/builtins/filesystem/readfile.md) diff --git a/docs/internals/builtins/filesystem/readlink.md b/docs/internals/builtins/filesystem/readlink.md index 9b67cd14b3..9b6d002b55 100644 --- a/docs/internals/builtins/filesystem/readlink.md +++ b/docs/internals/builtins/filesystem/readlink.md @@ -2,7 +2,7 @@ title: "readlink() — internals" description: "Compiler internals for readlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 138 + order: 142 --- ## `readlink()` — internals @@ -36,6 +36,11 @@ function readlink(string $path): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `readlink()`](../../../php/builtins/filesystem/readlink.md) diff --git a/docs/internals/builtins/filesystem/realpath.md b/docs/internals/builtins/filesystem/realpath.md index eaee752b33..bd76e2ce45 100644 --- a/docs/internals/builtins/filesystem/realpath.md +++ b/docs/internals/builtins/filesystem/realpath.md @@ -2,7 +2,7 @@ title: "realpath() — internals" description: "Compiler internals for realpath(): lowering path, type checks, and runtime helpers." sidebar: - order: 139 + order: 143 --- ## `realpath()` — internals @@ -33,6 +33,11 @@ function realpath(string $path): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `realpath()`](../../../php/builtins/filesystem/realpath.md) diff --git a/docs/internals/builtins/filesystem/realpath_cache_get.md b/docs/internals/builtins/filesystem/realpath_cache_get.md index e32cb410b5..d3a8cac36e 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_get.md +++ b/docs/internals/builtins/filesystem/realpath_cache_get.md @@ -2,7 +2,7 @@ title: "realpath_cache_get() — internals" description: "Compiler internals for realpath_cache_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 140 + order: 144 --- ## `realpath_cache_get()` — internals @@ -32,6 +32,11 @@ function realpath_cache_get(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `realpath_cache_get()`](../../../php/builtins/filesystem/realpath_cache_get.md) diff --git a/docs/internals/builtins/filesystem/realpath_cache_size.md b/docs/internals/builtins/filesystem/realpath_cache_size.md index 113cb67e63..f4aaca0635 100644 --- a/docs/internals/builtins/filesystem/realpath_cache_size.md +++ b/docs/internals/builtins/filesystem/realpath_cache_size.md @@ -2,7 +2,7 @@ title: "realpath_cache_size() — internals" description: "Compiler internals for realpath_cache_size(): lowering path, type checks, and runtime helpers." sidebar: - order: 141 + order: 145 --- ## `realpath_cache_size()` — internals @@ -32,6 +32,11 @@ function realpath_cache_size(): int - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `realpath_cache_size()`](../../../php/builtins/filesystem/realpath_cache_size.md) diff --git a/docs/internals/builtins/filesystem/rename.md b/docs/internals/builtins/filesystem/rename.md index 33acbcfc62..8ae0666272 100644 --- a/docs/internals/builtins/filesystem/rename.md +++ b/docs/internals/builtins/filesystem/rename.md @@ -2,7 +2,7 @@ title: "rename() — internals" description: "Compiler internals for rename(): lowering path, type checks, and runtime helpers." sidebar: - order: 142 + order: 146 --- ## `rename()` — internals @@ -35,6 +35,11 @@ function rename(string $from, string $to): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rename()`](../../../php/builtins/filesystem/rename.md) diff --git a/docs/internals/builtins/filesystem/rmdir.md b/docs/internals/builtins/filesystem/rmdir.md index 6add2d8250..4330c1bbb2 100644 --- a/docs/internals/builtins/filesystem/rmdir.md +++ b/docs/internals/builtins/filesystem/rmdir.md @@ -2,7 +2,7 @@ title: "rmdir() — internals" description: "Compiler internals for rmdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 143 + order: 147 --- ## `rmdir()` — internals @@ -37,6 +37,11 @@ function rmdir(string $directory): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rmdir()`](../../../php/builtins/filesystem/rmdir.md) diff --git a/docs/internals/builtins/filesystem/scandir.md b/docs/internals/builtins/filesystem/scandir.md index 1225c0afbe..4b2af053d0 100644 --- a/docs/internals/builtins/filesystem/scandir.md +++ b/docs/internals/builtins/filesystem/scandir.md @@ -2,7 +2,7 @@ title: "scandir() — internals" description: "Compiler internals for scandir(): lowering path, type checks, and runtime helpers." sidebar: - order: 144 + order: 148 --- ## `scandir()` — internals @@ -34,6 +34,11 @@ function scandir(string $directory): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `scandir()`](../../../php/builtins/filesystem/scandir.md) diff --git a/docs/internals/builtins/filesystem/stat.md b/docs/internals/builtins/filesystem/stat.md index 0969fc70a2..95eb981237 100644 --- a/docs/internals/builtins/filesystem/stat.md +++ b/docs/internals/builtins/filesystem/stat.md @@ -2,7 +2,7 @@ title: "stat() — internals" description: "Compiler internals for stat(): lowering path, type checks, and runtime helpers." sidebar: - order: 145 + order: 149 --- ## `stat()` — internals @@ -35,6 +35,11 @@ function stat(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stat()`](../../../php/builtins/filesystem/stat.md) diff --git a/docs/internals/builtins/filesystem/symlink.md b/docs/internals/builtins/filesystem/symlink.md index 4930e27f74..111ded5b96 100644 --- a/docs/internals/builtins/filesystem/symlink.md +++ b/docs/internals/builtins/filesystem/symlink.md @@ -2,7 +2,7 @@ title: "symlink() — internals" description: "Compiler internals for symlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 146 + order: 150 --- ## `symlink()` — internals @@ -36,6 +36,11 @@ function symlink(string $target, string $link): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `symlink()`](../../../php/builtins/filesystem/symlink.md) diff --git a/docs/internals/builtins/filesystem/sys_get_temp_dir.md b/docs/internals/builtins/filesystem/sys_get_temp_dir.md index 7c45622094..e3529230de 100644 --- a/docs/internals/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/internals/builtins/filesystem/sys_get_temp_dir.md @@ -2,7 +2,7 @@ title: "sys_get_temp_dir() — internals" description: "Compiler internals for sys_get_temp_dir(): lowering path, type checks, and runtime helpers." sidebar: - order: 147 + order: 151 --- ## `sys_get_temp_dir()` — internals @@ -33,6 +33,11 @@ function sys_get_temp_dir(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sys_get_temp_dir()`](../../../php/builtins/filesystem/sys_get_temp_dir.md) diff --git a/docs/internals/builtins/filesystem/tempnam.md b/docs/internals/builtins/filesystem/tempnam.md index d1de170ba3..026d0887df 100644 --- a/docs/internals/builtins/filesystem/tempnam.md +++ b/docs/internals/builtins/filesystem/tempnam.md @@ -2,7 +2,7 @@ title: "tempnam() — internals" description: "Compiler internals for tempnam(): lowering path, type checks, and runtime helpers." sidebar: - order: 148 + order: 152 --- ## `tempnam()` — internals @@ -35,6 +35,11 @@ function tempnam(string $directory, string $prefix): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `tempnam()`](../../../php/builtins/filesystem/tempnam.md) diff --git a/docs/internals/builtins/filesystem/tmpfile.md b/docs/internals/builtins/filesystem/tmpfile.md index 1c72e490e0..b9be0d96e1 100644 --- a/docs/internals/builtins/filesystem/tmpfile.md +++ b/docs/internals/builtins/filesystem/tmpfile.md @@ -2,7 +2,7 @@ title: "tmpfile() — internals" description: "Compiler internals for tmpfile(): lowering path, type checks, and runtime helpers." sidebar: - order: 149 + order: 153 --- ## `tmpfile()` — internals @@ -35,6 +35,11 @@ function tmpfile(): mixed - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `tmpfile()`](../../../php/builtins/filesystem/tmpfile.md) diff --git a/docs/internals/builtins/filesystem/touch.md b/docs/internals/builtins/filesystem/touch.md index 9a4ebb029c..8c9f885b78 100644 --- a/docs/internals/builtins/filesystem/touch.md +++ b/docs/internals/builtins/filesystem/touch.md @@ -2,7 +2,7 @@ title: "touch() — internals" description: "Compiler internals for touch(): lowering path, type checks, and runtime helpers." sidebar: - order: 150 + order: 154 --- ## `touch()` — internals @@ -32,6 +32,11 @@ function touch(string $filename, int $mtime = null, int $atime = null): bool - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `touch()`](../../../php/builtins/filesystem/touch.md) diff --git a/docs/internals/builtins/filesystem/umask.md b/docs/internals/builtins/filesystem/umask.md index 50b45bae18..0033851868 100644 --- a/docs/internals/builtins/filesystem/umask.md +++ b/docs/internals/builtins/filesystem/umask.md @@ -2,7 +2,7 @@ title: "umask() — internals" description: "Compiler internals for umask(): lowering path, type checks, and runtime helpers." sidebar: - order: 151 + order: 155 --- ## `umask()` — internals @@ -33,6 +33,11 @@ function umask(int $mask = null): int - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `umask()`](../../../php/builtins/filesystem/umask.md) diff --git a/docs/internals/builtins/filesystem/unlink.md b/docs/internals/builtins/filesystem/unlink.md index fb9df6ff4a..c00b70e08c 100644 --- a/docs/internals/builtins/filesystem/unlink.md +++ b/docs/internals/builtins/filesystem/unlink.md @@ -2,7 +2,7 @@ title: "unlink() — internals" description: "Compiler internals for unlink(): lowering path, type checks, and runtime helpers." sidebar: - order: 152 + order: 156 --- ## `unlink()` — internals @@ -35,6 +35,11 @@ function unlink(string $filename): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `unlink()`](../../../php/builtins/filesystem/unlink.md) diff --git a/docs/internals/builtins/io/closedir.md b/docs/internals/builtins/io/closedir.md index 4a8ad88cc8..c8c38c0a21 100644 --- a/docs/internals/builtins/io/closedir.md +++ b/docs/internals/builtins/io/closedir.md @@ -2,7 +2,7 @@ title: "closedir() — internals" description: "Compiler internals for closedir(): lowering path, type checks, and runtime helpers." sidebar: - order: 153 + order: 157 --- ## `closedir()` — internals @@ -36,6 +36,11 @@ function closedir(resource $dir_handle): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `closedir()`](../../../php/builtins/io/closedir.md) diff --git a/docs/internals/builtins/io/fclose.md b/docs/internals/builtins/io/fclose.md index 07d61b2ec2..ffea17f3f1 100644 --- a/docs/internals/builtins/io/fclose.md +++ b/docs/internals/builtins/io/fclose.md @@ -2,7 +2,7 @@ title: "fclose() — internals" description: "Compiler internals for fclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 154 + order: 158 --- ## `fclose()` — internals @@ -32,6 +32,11 @@ function fclose(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fclose()`](../../../php/builtins/io/fclose.md) diff --git a/docs/internals/builtins/io/fdatasync.md b/docs/internals/builtins/io/fdatasync.md index 43ad181d29..9d1b9ef3a5 100644 --- a/docs/internals/builtins/io/fdatasync.md +++ b/docs/internals/builtins/io/fdatasync.md @@ -2,7 +2,7 @@ title: "fdatasync() — internals" description: "Compiler internals for fdatasync(): lowering path, type checks, and runtime helpers." sidebar: - order: 155 + order: 159 --- ## `fdatasync()` — internals @@ -33,6 +33,11 @@ function fdatasync(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fdatasync()`](../../../php/builtins/io/fdatasync.md) diff --git a/docs/internals/builtins/io/feof.md b/docs/internals/builtins/io/feof.md index e24c273b01..749a1bddec 100644 --- a/docs/internals/builtins/io/feof.md +++ b/docs/internals/builtins/io/feof.md @@ -2,7 +2,7 @@ title: "feof() — internals" description: "Compiler internals for feof(): lowering path, type checks, and runtime helpers." sidebar: - order: 156 + order: 160 --- ## `feof()` — internals @@ -34,6 +34,11 @@ function feof(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `feof()`](../../../php/builtins/io/feof.md) diff --git a/docs/internals/builtins/io/fflush.md b/docs/internals/builtins/io/fflush.md index 2fc2d1aade..b285709dc4 100644 --- a/docs/internals/builtins/io/fflush.md +++ b/docs/internals/builtins/io/fflush.md @@ -2,7 +2,7 @@ title: "fflush() — internals" description: "Compiler internals for fflush(): lowering path, type checks, and runtime helpers." sidebar: - order: 157 + order: 161 --- ## `fflush()` — internals @@ -33,6 +33,11 @@ function fflush(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fflush()`](../../../php/builtins/io/fflush.md) diff --git a/docs/internals/builtins/io/fgetc.md b/docs/internals/builtins/io/fgetc.md index 8a79b3f773..b7ae4fb08d 100644 --- a/docs/internals/builtins/io/fgetc.md +++ b/docs/internals/builtins/io/fgetc.md @@ -2,7 +2,7 @@ title: "fgetc() — internals" description: "Compiler internals for fgetc(): lowering path, type checks, and runtime helpers." sidebar: - order: 158 + order: 162 --- ## `fgetc()` — internals @@ -34,6 +34,11 @@ function fgetc(resource $stream): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fgetc()`](../../../php/builtins/io/fgetc.md) diff --git a/docs/internals/builtins/io/fgetcsv.md b/docs/internals/builtins/io/fgetcsv.md index 9e7afe5ddd..04f431b967 100644 --- a/docs/internals/builtins/io/fgetcsv.md +++ b/docs/internals/builtins/io/fgetcsv.md @@ -2,7 +2,7 @@ title: "fgetcsv() — internals" description: "Compiler internals for fgetcsv(): lowering path, type checks, and runtime helpers." sidebar: - order: 159 + order: 163 --- ## `fgetcsv()` — internals @@ -34,6 +34,11 @@ function fgetcsv(resource $stream, int $length = null, string $separator = ','): - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fgetcsv()`](../../../php/builtins/io/fgetcsv.md) diff --git a/docs/internals/builtins/io/fgets.md b/docs/internals/builtins/io/fgets.md index b87a1aeb18..4054d88afd 100644 --- a/docs/internals/builtins/io/fgets.md +++ b/docs/internals/builtins/io/fgets.md @@ -2,7 +2,7 @@ title: "fgets() — internals" description: "Compiler internals for fgets(): lowering path, type checks, and runtime helpers." sidebar: - order: 160 + order: 164 --- ## `fgets()` — internals @@ -34,6 +34,11 @@ function fgets(resource $stream): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fgets()`](../../../php/builtins/io/fgets.md) diff --git a/docs/internals/builtins/io/file.md b/docs/internals/builtins/io/file.md index 53664951e9..21540ef944 100644 --- a/docs/internals/builtins/io/file.md +++ b/docs/internals/builtins/io/file.md @@ -2,7 +2,7 @@ title: "file() — internals" description: "Compiler internals for file(): lowering path, type checks, and runtime helpers." sidebar: - order: 161 + order: 165 --- ## `file()` — internals @@ -34,6 +34,11 @@ function file(string $filename): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `file()`](../../../php/builtins/io/file.md) diff --git a/docs/internals/builtins/io/file_get_contents.md b/docs/internals/builtins/io/file_get_contents.md index 341051a308..fdda2c790d 100644 --- a/docs/internals/builtins/io/file_get_contents.md +++ b/docs/internals/builtins/io/file_get_contents.md @@ -2,7 +2,7 @@ title: "file_get_contents() — internals" description: "Compiler internals for file_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 162 + order: 166 --- ## `file_get_contents()` — internals @@ -34,6 +34,11 @@ function file_get_contents(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `file_get_contents()`](../../../php/builtins/io/file_get_contents.md) diff --git a/docs/internals/builtins/io/file_put_contents.md b/docs/internals/builtins/io/file_put_contents.md index a0dd17d11b..e4b5ada072 100644 --- a/docs/internals/builtins/io/file_put_contents.md +++ b/docs/internals/builtins/io/file_put_contents.md @@ -2,7 +2,7 @@ title: "file_put_contents() — internals" description: "Compiler internals for file_put_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 163 + order: 167 --- ## `file_put_contents()` — internals @@ -34,6 +34,11 @@ function file_put_contents(string $filename, string $data): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `file_put_contents()`](../../../php/builtins/io/file_put_contents.md) diff --git a/docs/internals/builtins/io/flock.md b/docs/internals/builtins/io/flock.md index 05e1b70504..6a2bba850d 100644 --- a/docs/internals/builtins/io/flock.md +++ b/docs/internals/builtins/io/flock.md @@ -2,7 +2,7 @@ title: "flock() — internals" description: "Compiler internals for flock(): lowering path, type checks, and runtime helpers." sidebar: - order: 164 + order: 168 --- ## `flock()` — internals @@ -33,6 +33,12 @@ function flock(resource $stream, int $operation, bool $would_block = null): bool - **Arity**: takes 2–3 arguments (1 optional). - **By-reference parameters**: `$would_block`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$would_block`. + ## Cross-references - [User reference for `flock()`](../../../php/builtins/io/flock.md) diff --git a/docs/internals/builtins/io/fopen.md b/docs/internals/builtins/io/fopen.md index f4af5776b2..ffd6469183 100644 --- a/docs/internals/builtins/io/fopen.md +++ b/docs/internals/builtins/io/fopen.md @@ -2,7 +2,7 @@ title: "fopen() — internals" description: "Compiler internals for fopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 165 + order: 169 --- ## `fopen()` — internals @@ -33,6 +33,11 @@ function fopen(string $filename, string $mode, bool $use_include_path = false, m - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fopen()`](../../../php/builtins/io/fopen.md) diff --git a/docs/internals/builtins/io/fpassthru.md b/docs/internals/builtins/io/fpassthru.md index fc5dd43810..a934e5af61 100644 --- a/docs/internals/builtins/io/fpassthru.md +++ b/docs/internals/builtins/io/fpassthru.md @@ -2,7 +2,7 @@ title: "fpassthru() — internals" description: "Compiler internals for fpassthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 166 + order: 170 --- ## `fpassthru()` — internals @@ -34,6 +34,11 @@ function fpassthru(resource $stream): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fpassthru()`](../../../php/builtins/io/fpassthru.md) diff --git a/docs/internals/builtins/io/fprintf.md b/docs/internals/builtins/io/fprintf.md index 25c68e9a11..d274ce9be5 100644 --- a/docs/internals/builtins/io/fprintf.md +++ b/docs/internals/builtins/io/fprintf.md @@ -2,7 +2,7 @@ title: "fprintf() — internals" description: "Compiler internals for fprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 167 + order: 171 --- ## `fprintf()` — internals @@ -34,6 +34,12 @@ function fprintf(resource $stream, string $format, ...$values): int - **Arity**: takes exactly 2 arguments. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `fprintf()`](../../../php/builtins/io/fprintf.md) diff --git a/docs/internals/builtins/io/fputcsv.md b/docs/internals/builtins/io/fputcsv.md index 88071b7e26..eee759a64d 100644 --- a/docs/internals/builtins/io/fputcsv.md +++ b/docs/internals/builtins/io/fputcsv.md @@ -2,7 +2,7 @@ title: "fputcsv() — internals" description: "Compiler internals for fputcsv(): lowering path, type checks, and runtime helpers." sidebar: - order: 168 + order: 172 --- ## `fputcsv()` — internals @@ -33,6 +33,11 @@ function fputcsv(resource $stream, array $fields, string $separator = ',', strin - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fputcsv()`](../../../php/builtins/io/fputcsv.md) diff --git a/docs/internals/builtins/io/fread.md b/docs/internals/builtins/io/fread.md index 3718fd98c9..636cc0473c 100644 --- a/docs/internals/builtins/io/fread.md +++ b/docs/internals/builtins/io/fread.md @@ -2,7 +2,7 @@ title: "fread() — internals" description: "Compiler internals for fread(): lowering path, type checks, and runtime helpers." sidebar: - order: 169 + order: 173 --- ## `fread()` — internals @@ -33,6 +33,11 @@ function fread(resource $stream, int $length): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fread()`](../../../php/builtins/io/fread.md) diff --git a/docs/internals/builtins/io/fscanf.md b/docs/internals/builtins/io/fscanf.md index ac174d4b98..e007f9917e 100644 --- a/docs/internals/builtins/io/fscanf.md +++ b/docs/internals/builtins/io/fscanf.md @@ -2,7 +2,7 @@ title: "fscanf() — internals" description: "Compiler internals for fscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 170 + order: 174 --- ## `fscanf()` — internals @@ -35,6 +35,12 @@ function fscanf(resource $stream, string $format, ...$vars): array - **Arity**: takes exactly 2 arguments. - **Variadic**: collects excess arguments into `$vars`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$vars`. + ## Cross-references - [User reference for `fscanf()`](../../../php/builtins/io/fscanf.md) diff --git a/docs/internals/builtins/io/fseek.md b/docs/internals/builtins/io/fseek.md index ce92f7c77f..84820d15ef 100644 --- a/docs/internals/builtins/io/fseek.md +++ b/docs/internals/builtins/io/fseek.md @@ -2,7 +2,7 @@ title: "fseek() — internals" description: "Compiler internals for fseek(): lowering path, type checks, and runtime helpers." sidebar: - order: 171 + order: 175 --- ## `fseek()` — internals @@ -32,6 +32,11 @@ function fseek(resource $stream, int $offset, int $whence = 0): int - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fseek()`](../../../php/builtins/io/fseek.md) diff --git a/docs/internals/builtins/io/fstat.md b/docs/internals/builtins/io/fstat.md index b92d1d8b31..057c1308ea 100644 --- a/docs/internals/builtins/io/fstat.md +++ b/docs/internals/builtins/io/fstat.md @@ -2,7 +2,7 @@ title: "fstat() — internals" description: "Compiler internals for fstat(): lowering path, type checks, and runtime helpers." sidebar: - order: 172 + order: 176 --- ## `fstat()` — internals @@ -33,6 +33,11 @@ function fstat(resource $stream): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fstat()`](../../../php/builtins/io/fstat.md) diff --git a/docs/internals/builtins/io/fsync.md b/docs/internals/builtins/io/fsync.md index 5338aab7cd..198a97f9f6 100644 --- a/docs/internals/builtins/io/fsync.md +++ b/docs/internals/builtins/io/fsync.md @@ -2,7 +2,7 @@ title: "fsync() — internals" description: "Compiler internals for fsync(): lowering path, type checks, and runtime helpers." sidebar: - order: 173 + order: 177 --- ## `fsync()` — internals @@ -34,6 +34,11 @@ function fsync(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fsync()`](../../../php/builtins/io/fsync.md) diff --git a/docs/internals/builtins/io/ftell.md b/docs/internals/builtins/io/ftell.md index fce93e4672..823c765304 100644 --- a/docs/internals/builtins/io/ftell.md +++ b/docs/internals/builtins/io/ftell.md @@ -2,7 +2,7 @@ title: "ftell() — internals" description: "Compiler internals for ftell(): lowering path, type checks, and runtime helpers." sidebar: - order: 174 + order: 178 --- ## `ftell()` — internals @@ -33,6 +33,11 @@ function ftell(resource $stream): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ftell()`](../../../php/builtins/io/ftell.md) diff --git a/docs/internals/builtins/io/ftruncate.md b/docs/internals/builtins/io/ftruncate.md index 8a71ce2a37..d0470fe9c9 100644 --- a/docs/internals/builtins/io/ftruncate.md +++ b/docs/internals/builtins/io/ftruncate.md @@ -2,7 +2,7 @@ title: "ftruncate() — internals" description: "Compiler internals for ftruncate(): lowering path, type checks, and runtime helpers." sidebar: - order: 175 + order: 179 --- ## `ftruncate()` — internals @@ -32,6 +32,11 @@ function ftruncate(resource $stream, int $size): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ftruncate()`](../../../php/builtins/io/ftruncate.md) diff --git a/docs/internals/builtins/io/fwrite.md b/docs/internals/builtins/io/fwrite.md index 85551c288d..15c5636223 100644 --- a/docs/internals/builtins/io/fwrite.md +++ b/docs/internals/builtins/io/fwrite.md @@ -2,7 +2,7 @@ title: "fwrite() — internals" description: "Compiler internals for fwrite(): lowering path, type checks, and runtime helpers." sidebar: - order: 176 + order: 180 --- ## `fwrite()` — internals @@ -33,6 +33,11 @@ function fwrite(resource $stream, string $data): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fwrite()`](../../../php/builtins/io/fwrite.md) diff --git a/docs/internals/builtins/io/gethostbyaddr.md b/docs/internals/builtins/io/gethostbyaddr.md index 49ad6a17cc..bec60bfc7e 100644 --- a/docs/internals/builtins/io/gethostbyaddr.md +++ b/docs/internals/builtins/io/gethostbyaddr.md @@ -2,7 +2,7 @@ title: "gethostbyaddr() — internals" description: "Compiler internals for gethostbyaddr(): lowering path, type checks, and runtime helpers." sidebar: - order: 177 + order: 181 --- ## `gethostbyaddr()` — internals @@ -34,6 +34,11 @@ function gethostbyaddr(string $ip): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gethostbyaddr()`](../../../php/builtins/io/gethostbyaddr.md) diff --git a/docs/internals/builtins/io/gethostbyname.md b/docs/internals/builtins/io/gethostbyname.md index 74394b4e6b..3afafd75bd 100644 --- a/docs/internals/builtins/io/gethostbyname.md +++ b/docs/internals/builtins/io/gethostbyname.md @@ -2,7 +2,7 @@ title: "gethostbyname() — internals" description: "Compiler internals for gethostbyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 178 + order: 182 --- ## `gethostbyname()` — internals @@ -34,6 +34,11 @@ function gethostbyname(string $hostname): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gethostbyname()`](../../../php/builtins/io/gethostbyname.md) diff --git a/docs/internals/builtins/io/gethostname.md b/docs/internals/builtins/io/gethostname.md index f5dbcabf0e..c5240428d5 100644 --- a/docs/internals/builtins/io/gethostname.md +++ b/docs/internals/builtins/io/gethostname.md @@ -2,7 +2,7 @@ title: "gethostname() — internals" description: "Compiler internals for gethostname(): lowering path, type checks, and runtime helpers." sidebar: - order: 179 + order: 183 --- ## `gethostname()` — internals @@ -35,6 +35,11 @@ function gethostname(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gethostname()`](../../../php/builtins/io/gethostname.md) diff --git a/docs/internals/builtins/io/getprotobyname.md b/docs/internals/builtins/io/getprotobyname.md index 85c99a16db..9831de7985 100644 --- a/docs/internals/builtins/io/getprotobyname.md +++ b/docs/internals/builtins/io/getprotobyname.md @@ -2,7 +2,7 @@ title: "getprotobyname() — internals" description: "Compiler internals for getprotobyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 180 + order: 184 --- ## `getprotobyname()` — internals @@ -33,6 +33,11 @@ function getprotobyname(string $protocol): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getprotobyname()`](../../../php/builtins/io/getprotobyname.md) diff --git a/docs/internals/builtins/io/getprotobynumber.md b/docs/internals/builtins/io/getprotobynumber.md index f2603bff3f..7b6e21e93a 100644 --- a/docs/internals/builtins/io/getprotobynumber.md +++ b/docs/internals/builtins/io/getprotobynumber.md @@ -2,7 +2,7 @@ title: "getprotobynumber() — internals" description: "Compiler internals for getprotobynumber(): lowering path, type checks, and runtime helpers." sidebar: - order: 181 + order: 185 --- ## `getprotobynumber()` — internals @@ -33,6 +33,11 @@ function getprotobynumber(int $protocol): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getprotobynumber()`](../../../php/builtins/io/getprotobynumber.md) diff --git a/docs/internals/builtins/io/getservbyname.md b/docs/internals/builtins/io/getservbyname.md index cf829bb720..a04bcfa61e 100644 --- a/docs/internals/builtins/io/getservbyname.md +++ b/docs/internals/builtins/io/getservbyname.md @@ -2,7 +2,7 @@ title: "getservbyname() — internals" description: "Compiler internals for getservbyname(): lowering path, type checks, and runtime helpers." sidebar: - order: 182 + order: 186 --- ## `getservbyname()` — internals @@ -33,6 +33,11 @@ function getservbyname(string $service, string $protocol): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getservbyname()`](../../../php/builtins/io/getservbyname.md) diff --git a/docs/internals/builtins/io/getservbyport.md b/docs/internals/builtins/io/getservbyport.md index 1e37fd6dda..dd226c9dac 100644 --- a/docs/internals/builtins/io/getservbyport.md +++ b/docs/internals/builtins/io/getservbyport.md @@ -2,7 +2,7 @@ title: "getservbyport() — internals" description: "Compiler internals for getservbyport(): lowering path, type checks, and runtime helpers." sidebar: - order: 183 + order: 187 --- ## `getservbyport()` — internals @@ -33,6 +33,11 @@ function getservbyport(int $port, string $protocol): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `getservbyport()`](../../../php/builtins/io/getservbyport.md) diff --git a/docs/internals/builtins/io/hash_file.md b/docs/internals/builtins/io/hash_file.md index 7c594feab1..61dc4fcf02 100644 --- a/docs/internals/builtins/io/hash_file.md +++ b/docs/internals/builtins/io/hash_file.md @@ -2,7 +2,7 @@ title: "hash_file() — internals" description: "Compiler internals for hash_file(): lowering path, type checks, and runtime helpers." sidebar: - order: 184 + order: 188 --- ## `hash_file()` — internals @@ -32,6 +32,11 @@ function hash_file(string $algo, string $filename, bool $binary = false): mixed - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_file()`](../../../php/builtins/io/hash_file.md) diff --git a/docs/internals/builtins/io/opendir.md b/docs/internals/builtins/io/opendir.md index 26d9d4b37e..ae1f14ed3e 100644 --- a/docs/internals/builtins/io/opendir.md +++ b/docs/internals/builtins/io/opendir.md @@ -2,7 +2,7 @@ title: "opendir() — internals" description: "Compiler internals for opendir(): lowering path, type checks, and runtime helpers." sidebar: - order: 185 + order: 189 --- ## `opendir()` — internals @@ -35,6 +35,11 @@ function opendir(string $directory): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `opendir()`](../../../php/builtins/io/opendir.md) diff --git a/docs/internals/builtins/io/readdir.md b/docs/internals/builtins/io/readdir.md index b6c1f0da7e..df501cd2bc 100644 --- a/docs/internals/builtins/io/readdir.md +++ b/docs/internals/builtins/io/readdir.md @@ -2,7 +2,7 @@ title: "readdir() — internals" description: "Compiler internals for readdir(): lowering path, type checks, and runtime helpers." sidebar: - order: 186 + order: 190 --- ## `readdir()` — internals @@ -36,6 +36,11 @@ function readdir(resource $dir_handle): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `readdir()`](../../../php/builtins/io/readdir.md) diff --git a/docs/internals/builtins/io/rewind.md b/docs/internals/builtins/io/rewind.md index 6f63dbcf51..3a4aec0a8b 100644 --- a/docs/internals/builtins/io/rewind.md +++ b/docs/internals/builtins/io/rewind.md @@ -2,7 +2,7 @@ title: "rewind() — internals" description: "Compiler internals for rewind(): lowering path, type checks, and runtime helpers." sidebar: - order: 187 + order: 191 --- ## `rewind()` — internals @@ -32,6 +32,11 @@ function rewind(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rewind()`](../../../php/builtins/io/rewind.md) diff --git a/docs/internals/builtins/io/rewinddir.md b/docs/internals/builtins/io/rewinddir.md index 185059fde0..bae6cdb840 100644 --- a/docs/internals/builtins/io/rewinddir.md +++ b/docs/internals/builtins/io/rewinddir.md @@ -2,7 +2,7 @@ title: "rewinddir() — internals" description: "Compiler internals for rewinddir(): lowering path, type checks, and runtime helpers." sidebar: - order: 188 + order: 192 --- ## `rewinddir()` — internals @@ -34,6 +34,11 @@ function rewinddir(resource $dir_handle): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rewinddir()`](../../../php/builtins/io/rewinddir.md) diff --git a/docs/internals/builtins/io/stream_bucket_make_writeable.md b/docs/internals/builtins/io/stream_bucket_make_writeable.md index 6bb8d17674..f1db9bc0b6 100644 --- a/docs/internals/builtins/io/stream_bucket_make_writeable.md +++ b/docs/internals/builtins/io/stream_bucket_make_writeable.md @@ -2,7 +2,7 @@ title: "stream_bucket_make_writeable() — internals" description: "Compiler internals for stream_bucket_make_writeable(): lowering path, type checks, and runtime helpers." sidebar: - order: 189 + order: 193 --- ## `stream_bucket_make_writeable()` — internals @@ -33,6 +33,11 @@ function stream_bucket_make_writeable(mixed $brigade): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_bucket_make_writeable()`](../../../php/builtins/io/stream_bucket_make_writeable.md) diff --git a/docs/internals/builtins/io/stream_bucket_new.md b/docs/internals/builtins/io/stream_bucket_new.md index 568382c658..86e3fee2bf 100644 --- a/docs/internals/builtins/io/stream_bucket_new.md +++ b/docs/internals/builtins/io/stream_bucket_new.md @@ -2,7 +2,7 @@ title: "stream_bucket_new() — internals" description: "Compiler internals for stream_bucket_new(): lowering path, type checks, and runtime helpers." sidebar: - order: 190 + order: 194 --- ## `stream_bucket_new()` — internals @@ -32,6 +32,11 @@ function stream_bucket_new(resource $stream, string $buffer): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_bucket_new()`](../../../php/builtins/io/stream_bucket_new.md) diff --git a/docs/internals/builtins/io/stream_context_create.md b/docs/internals/builtins/io/stream_context_create.md index 294aed3021..0c095976bf 100644 --- a/docs/internals/builtins/io/stream_context_create.md +++ b/docs/internals/builtins/io/stream_context_create.md @@ -2,7 +2,7 @@ title: "stream_context_create() — internals" description: "Compiler internals for stream_context_create(): lowering path, type checks, and runtime helpers." sidebar: - order: 191 + order: 195 --- ## `stream_context_create()` — internals @@ -32,6 +32,11 @@ function stream_context_create(array $options = null, array $params = null): mix - **Arity**: takes 0–2 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_create()`](../../../php/builtins/io/stream_context_create.md) diff --git a/docs/internals/builtins/io/stream_context_get_default.md b/docs/internals/builtins/io/stream_context_get_default.md index e8fa0170ef..196df3ec9a 100644 --- a/docs/internals/builtins/io/stream_context_get_default.md +++ b/docs/internals/builtins/io/stream_context_get_default.md @@ -2,7 +2,7 @@ title: "stream_context_get_default() — internals" description: "Compiler internals for stream_context_get_default(): lowering path, type checks, and runtime helpers." sidebar: - order: 192 + order: 196 --- ## `stream_context_get_default()` — internals @@ -32,6 +32,11 @@ function stream_context_get_default(array $options = null): mixed - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_get_default()`](../../../php/builtins/io/stream_context_get_default.md) diff --git a/docs/internals/builtins/io/stream_context_get_options.md b/docs/internals/builtins/io/stream_context_get_options.md index 1bc88ab6fc..9451ec5a62 100644 --- a/docs/internals/builtins/io/stream_context_get_options.md +++ b/docs/internals/builtins/io/stream_context_get_options.md @@ -2,7 +2,7 @@ title: "stream_context_get_options() — internals" description: "Compiler internals for stream_context_get_options(): lowering path, type checks, and runtime helpers." sidebar: - order: 193 + order: 197 --- ## `stream_context_get_options()` — internals @@ -34,6 +34,11 @@ function stream_context_get_options(resource $context): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_get_options()`](../../../php/builtins/io/stream_context_get_options.md) diff --git a/docs/internals/builtins/io/stream_context_get_params.md b/docs/internals/builtins/io/stream_context_get_params.md index a336ce53ac..d70a703acb 100644 --- a/docs/internals/builtins/io/stream_context_get_params.md +++ b/docs/internals/builtins/io/stream_context_get_params.md @@ -2,7 +2,7 @@ title: "stream_context_get_params() — internals" description: "Compiler internals for stream_context_get_params(): lowering path, type checks, and runtime helpers." sidebar: - order: 194 + order: 198 --- ## `stream_context_get_params()` — internals @@ -32,6 +32,11 @@ function stream_context_get_params(resource $context): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_get_params()`](../../../php/builtins/io/stream_context_get_params.md) diff --git a/docs/internals/builtins/io/stream_context_set_default.md b/docs/internals/builtins/io/stream_context_set_default.md index 11bc262f4a..d8fdcc977a 100644 --- a/docs/internals/builtins/io/stream_context_set_default.md +++ b/docs/internals/builtins/io/stream_context_set_default.md @@ -2,7 +2,7 @@ title: "stream_context_set_default() — internals" description: "Compiler internals for stream_context_set_default(): lowering path, type checks, and runtime helpers." sidebar: - order: 195 + order: 199 --- ## `stream_context_set_default()` — internals @@ -32,6 +32,11 @@ function stream_context_set_default(array $options): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_set_default()`](../../../php/builtins/io/stream_context_set_default.md) diff --git a/docs/internals/builtins/io/stream_context_set_option.md b/docs/internals/builtins/io/stream_context_set_option.md index fc81a0beaf..68c8aa94b5 100644 --- a/docs/internals/builtins/io/stream_context_set_option.md +++ b/docs/internals/builtins/io/stream_context_set_option.md @@ -2,7 +2,7 @@ title: "stream_context_set_option() — internals" description: "Compiler internals for stream_context_set_option(): lowering path, type checks, and runtime helpers." sidebar: - order: 196 + order: 200 --- ## `stream_context_set_option()` — internals @@ -32,6 +32,11 @@ function stream_context_set_option(resource $context, string $wrapper_or_options - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_set_option()`](../../../php/builtins/io/stream_context_set_option.md) diff --git a/docs/internals/builtins/io/stream_context_set_params.md b/docs/internals/builtins/io/stream_context_set_params.md index adacd430ba..20a881fa4b 100644 --- a/docs/internals/builtins/io/stream_context_set_params.md +++ b/docs/internals/builtins/io/stream_context_set_params.md @@ -2,7 +2,7 @@ title: "stream_context_set_params() — internals" description: "Compiler internals for stream_context_set_params(): lowering path, type checks, and runtime helpers." sidebar: - order: 197 + order: 201 --- ## `stream_context_set_params()` — internals @@ -32,6 +32,11 @@ function stream_context_set_params(resource $context, array $params): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_context_set_params()`](../../../php/builtins/io/stream_context_set_params.md) diff --git a/docs/internals/builtins/io/stream_copy_to_stream.md b/docs/internals/builtins/io/stream_copy_to_stream.md index c33e7ed443..3e62a542a3 100644 --- a/docs/internals/builtins/io/stream_copy_to_stream.md +++ b/docs/internals/builtins/io/stream_copy_to_stream.md @@ -2,7 +2,7 @@ title: "stream_copy_to_stream() — internals" description: "Compiler internals for stream_copy_to_stream(): lowering path, type checks, and runtime helpers." sidebar: - order: 198 + order: 202 --- ## `stream_copy_to_stream()` — internals @@ -32,6 +32,11 @@ function stream_copy_to_stream(resource $from, resource $to, int $length = null, - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_copy_to_stream()`](../../../php/builtins/io/stream_copy_to_stream.md) diff --git a/docs/internals/builtins/io/stream_filter_register.md b/docs/internals/builtins/io/stream_filter_register.md index 4871ac00d3..3dbfd60fd0 100644 --- a/docs/internals/builtins/io/stream_filter_register.md +++ b/docs/internals/builtins/io/stream_filter_register.md @@ -2,7 +2,7 @@ title: "stream_filter_register() — internals" description: "Compiler internals for stream_filter_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 199 + order: 203 --- ## `stream_filter_register()` — internals @@ -33,6 +33,11 @@ function stream_filter_register(string $filter_name, string $class): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_filter_register()`](../../../php/builtins/io/stream_filter_register.md) diff --git a/docs/internals/builtins/io/stream_filter_remove.md b/docs/internals/builtins/io/stream_filter_remove.md index 4259c6188a..f3b85782d6 100644 --- a/docs/internals/builtins/io/stream_filter_remove.md +++ b/docs/internals/builtins/io/stream_filter_remove.md @@ -2,7 +2,7 @@ title: "stream_filter_remove() — internals" description: "Compiler internals for stream_filter_remove(): lowering path, type checks, and runtime helpers." sidebar: - order: 200 + order: 204 --- ## `stream_filter_remove()` — internals @@ -33,6 +33,11 @@ function stream_filter_remove(resource $stream_filter): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_filter_remove()`](../../../php/builtins/io/stream_filter_remove.md) diff --git a/docs/internals/builtins/io/stream_get_contents.md b/docs/internals/builtins/io/stream_get_contents.md index 2354839a01..0a67c802bb 100644 --- a/docs/internals/builtins/io/stream_get_contents.md +++ b/docs/internals/builtins/io/stream_get_contents.md @@ -2,7 +2,7 @@ title: "stream_get_contents() — internals" description: "Compiler internals for stream_get_contents(): lowering path, type checks, and runtime helpers." sidebar: - order: 201 + order: 205 --- ## `stream_get_contents()` — internals @@ -32,6 +32,11 @@ function stream_get_contents(resource $stream, int $length = null, int $offset = - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_contents()`](../../../php/builtins/io/stream_get_contents.md) diff --git a/docs/internals/builtins/io/stream_get_filters.md b/docs/internals/builtins/io/stream_get_filters.md index eac2d3dabc..b471fff4e5 100644 --- a/docs/internals/builtins/io/stream_get_filters.md +++ b/docs/internals/builtins/io/stream_get_filters.md @@ -2,7 +2,7 @@ title: "stream_get_filters() — internals" description: "Compiler internals for stream_get_filters(): lowering path, type checks, and runtime helpers." sidebar: - order: 202 + order: 206 --- ## `stream_get_filters()` — internals @@ -32,6 +32,11 @@ function stream_get_filters(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_filters()`](../../../php/builtins/io/stream_get_filters.md) diff --git a/docs/internals/builtins/io/stream_get_line.md b/docs/internals/builtins/io/stream_get_line.md index 52b01cbfb2..bff83ff822 100644 --- a/docs/internals/builtins/io/stream_get_line.md +++ b/docs/internals/builtins/io/stream_get_line.md @@ -2,7 +2,7 @@ title: "stream_get_line() — internals" description: "Compiler internals for stream_get_line(): lowering path, type checks, and runtime helpers." sidebar: - order: 203 + order: 207 --- ## `stream_get_line()` — internals @@ -32,6 +32,11 @@ function stream_get_line(resource $stream, int $length, string $ending = ''): st - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_line()`](../../../php/builtins/io/stream_get_line.md) diff --git a/docs/internals/builtins/io/stream_get_meta_data.md b/docs/internals/builtins/io/stream_get_meta_data.md index c22e797cce..1559741cc5 100644 --- a/docs/internals/builtins/io/stream_get_meta_data.md +++ b/docs/internals/builtins/io/stream_get_meta_data.md @@ -2,7 +2,7 @@ title: "stream_get_meta_data() — internals" description: "Compiler internals for stream_get_meta_data(): lowering path, type checks, and runtime helpers." sidebar: - order: 204 + order: 208 --- ## `stream_get_meta_data()` — internals @@ -33,6 +33,11 @@ function stream_get_meta_data(resource $stream): array - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_meta_data()`](../../../php/builtins/io/stream_get_meta_data.md) diff --git a/docs/internals/builtins/io/stream_get_transports.md b/docs/internals/builtins/io/stream_get_transports.md index 8c08491264..862c7266ed 100644 --- a/docs/internals/builtins/io/stream_get_transports.md +++ b/docs/internals/builtins/io/stream_get_transports.md @@ -2,7 +2,7 @@ title: "stream_get_transports() — internals" description: "Compiler internals for stream_get_transports(): lowering path, type checks, and runtime helpers." sidebar: - order: 205 + order: 209 --- ## `stream_get_transports()` — internals @@ -32,6 +32,11 @@ function stream_get_transports(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_transports()`](../../../php/builtins/io/stream_get_transports.md) diff --git a/docs/internals/builtins/io/stream_get_wrappers.md b/docs/internals/builtins/io/stream_get_wrappers.md index f83d658932..2b47a190a1 100644 --- a/docs/internals/builtins/io/stream_get_wrappers.md +++ b/docs/internals/builtins/io/stream_get_wrappers.md @@ -2,7 +2,7 @@ title: "stream_get_wrappers() — internals" description: "Compiler internals for stream_get_wrappers(): lowering path, type checks, and runtime helpers." sidebar: - order: 206 + order: 210 --- ## `stream_get_wrappers()` — internals @@ -32,6 +32,11 @@ function stream_get_wrappers(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_get_wrappers()`](../../../php/builtins/io/stream_get_wrappers.md) diff --git a/docs/internals/builtins/io/stream_is_local.md b/docs/internals/builtins/io/stream_is_local.md index 2a28e1703b..29135e7615 100644 --- a/docs/internals/builtins/io/stream_is_local.md +++ b/docs/internals/builtins/io/stream_is_local.md @@ -2,7 +2,7 @@ title: "stream_is_local() — internals" description: "Compiler internals for stream_is_local(): lowering path, type checks, and runtime helpers." sidebar: - order: 207 + order: 211 --- ## `stream_is_local()` — internals @@ -32,6 +32,11 @@ function stream_is_local(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_is_local()`](../../../php/builtins/io/stream_is_local.md) diff --git a/docs/internals/builtins/io/stream_isatty.md b/docs/internals/builtins/io/stream_isatty.md index a5ee682754..1c5dc63f8b 100644 --- a/docs/internals/builtins/io/stream_isatty.md +++ b/docs/internals/builtins/io/stream_isatty.md @@ -2,7 +2,7 @@ title: "stream_isatty() — internals" description: "Compiler internals for stream_isatty(): lowering path, type checks, and runtime helpers." sidebar: - order: 208 + order: 212 --- ## `stream_isatty()` — internals @@ -33,6 +33,11 @@ function stream_isatty(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_isatty()`](../../../php/builtins/io/stream_isatty.md) diff --git a/docs/internals/builtins/io/stream_resolve_include_path.md b/docs/internals/builtins/io/stream_resolve_include_path.md index 12122e031c..4cd73d4560 100644 --- a/docs/internals/builtins/io/stream_resolve_include_path.md +++ b/docs/internals/builtins/io/stream_resolve_include_path.md @@ -2,7 +2,7 @@ title: "stream_resolve_include_path() — internals" description: "Compiler internals for stream_resolve_include_path(): lowering path, type checks, and runtime helpers." sidebar: - order: 209 + order: 213 --- ## `stream_resolve_include_path()` — internals @@ -34,6 +34,11 @@ function stream_resolve_include_path(string $filename): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_resolve_include_path()`](../../../php/builtins/io/stream_resolve_include_path.md) diff --git a/docs/internals/builtins/io/stream_select.md b/docs/internals/builtins/io/stream_select.md index d5c0fc7e99..67037055ba 100644 --- a/docs/internals/builtins/io/stream_select.md +++ b/docs/internals/builtins/io/stream_select.md @@ -2,7 +2,7 @@ title: "stream_select() — internals" description: "Compiler internals for stream_select(): lowering path, type checks, and runtime helpers." sidebar: - order: 210 + order: 214 --- ## `stream_select()` — internals @@ -33,6 +33,12 @@ function stream_select(array $read, array $write, array $except, int $seconds, i - **Arity**: takes 4–5 arguments (1 optional). - **By-reference parameters**: `$read`, `$write`, `$except`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$read`, `$write`, `$except`. + ## Cross-references - [User reference for `stream_select()`](../../../php/builtins/io/stream_select.md) diff --git a/docs/internals/builtins/io/stream_set_blocking.md b/docs/internals/builtins/io/stream_set_blocking.md index c2971c593a..ece06ff8e3 100644 --- a/docs/internals/builtins/io/stream_set_blocking.md +++ b/docs/internals/builtins/io/stream_set_blocking.md @@ -2,7 +2,7 @@ title: "stream_set_blocking() — internals" description: "Compiler internals for stream_set_blocking(): lowering path, type checks, and runtime helpers." sidebar: - order: 211 + order: 215 --- ## `stream_set_blocking()` — internals @@ -34,6 +34,11 @@ function stream_set_blocking(resource $stream, bool $enable): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_blocking()`](../../../php/builtins/io/stream_set_blocking.md) diff --git a/docs/internals/builtins/io/stream_set_chunk_size.md b/docs/internals/builtins/io/stream_set_chunk_size.md index 3ebdaabd13..d380eae7c5 100644 --- a/docs/internals/builtins/io/stream_set_chunk_size.md +++ b/docs/internals/builtins/io/stream_set_chunk_size.md @@ -2,7 +2,7 @@ title: "stream_set_chunk_size() — internals" description: "Compiler internals for stream_set_chunk_size(): lowering path, type checks, and runtime helpers." sidebar: - order: 212 + order: 216 --- ## `stream_set_chunk_size()` — internals @@ -32,6 +32,11 @@ function stream_set_chunk_size(resource $stream, int $size): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_chunk_size()`](../../../php/builtins/io/stream_set_chunk_size.md) diff --git a/docs/internals/builtins/io/stream_set_read_buffer.md b/docs/internals/builtins/io/stream_set_read_buffer.md index e955cf8ecc..31953846a3 100644 --- a/docs/internals/builtins/io/stream_set_read_buffer.md +++ b/docs/internals/builtins/io/stream_set_read_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_read_buffer() — internals" description: "Compiler internals for stream_set_read_buffer(): lowering path, type checks, and runtime helpers." sidebar: - order: 213 + order: 217 --- ## `stream_set_read_buffer()` — internals @@ -32,6 +32,11 @@ function stream_set_read_buffer(resource $stream, int $size): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_read_buffer()`](../../../php/builtins/io/stream_set_read_buffer.md) diff --git a/docs/internals/builtins/io/stream_set_timeout.md b/docs/internals/builtins/io/stream_set_timeout.md index 945078a811..2c08a2f859 100644 --- a/docs/internals/builtins/io/stream_set_timeout.md +++ b/docs/internals/builtins/io/stream_set_timeout.md @@ -2,7 +2,7 @@ title: "stream_set_timeout() — internals" description: "Compiler internals for stream_set_timeout(): lowering path, type checks, and runtime helpers." sidebar: - order: 214 + order: 218 --- ## `stream_set_timeout()` — internals @@ -32,6 +32,11 @@ function stream_set_timeout(resource $stream, int $seconds, int $microseconds = - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_timeout()`](../../../php/builtins/io/stream_set_timeout.md) diff --git a/docs/internals/builtins/io/stream_set_write_buffer.md b/docs/internals/builtins/io/stream_set_write_buffer.md index 5687cee45a..8880f6671e 100644 --- a/docs/internals/builtins/io/stream_set_write_buffer.md +++ b/docs/internals/builtins/io/stream_set_write_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_write_buffer() — internals" description: "Compiler internals for stream_set_write_buffer(): lowering path, type checks, and runtime helpers." sidebar: - order: 215 + order: 219 --- ## `stream_set_write_buffer()` — internals @@ -32,6 +32,11 @@ function stream_set_write_buffer(resource $stream, int $size): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_set_write_buffer()`](../../../php/builtins/io/stream_set_write_buffer.md) diff --git a/docs/internals/builtins/io/stream_socket_accept.md b/docs/internals/builtins/io/stream_socket_accept.md index 15bc1cd888..72294d6820 100644 --- a/docs/internals/builtins/io/stream_socket_accept.md +++ b/docs/internals/builtins/io/stream_socket_accept.md @@ -2,7 +2,7 @@ title: "stream_socket_accept() — internals" description: "Compiler internals for stream_socket_accept(): lowering path, type checks, and runtime helpers." sidebar: - order: 216 + order: 220 --- ## `stream_socket_accept()` — internals @@ -34,6 +34,12 @@ function stream_socket_accept(resource $socket, float $timeout = null, string $p - **Arity**: takes 1–3 arguments (2 optional). - **By-reference parameters**: `$peer_name`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$peer_name`. + ## Cross-references - [User reference for `stream_socket_accept()`](../../../php/builtins/io/stream_socket_accept.md) diff --git a/docs/internals/builtins/io/stream_socket_client.md b/docs/internals/builtins/io/stream_socket_client.md index 86a0b421a4..667a4493b8 100644 --- a/docs/internals/builtins/io/stream_socket_client.md +++ b/docs/internals/builtins/io/stream_socket_client.md @@ -2,7 +2,7 @@ title: "stream_socket_client() — internals" description: "Compiler internals for stream_socket_client(): lowering path, type checks, and runtime helpers." sidebar: - order: 217 + order: 221 --- ## `stream_socket_client()` — internals @@ -34,6 +34,11 @@ function stream_socket_client(string $address): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_client()`](../../../php/builtins/io/stream_socket_client.md) diff --git a/docs/internals/builtins/io/stream_socket_enable_crypto.md b/docs/internals/builtins/io/stream_socket_enable_crypto.md index 1b2277fb07..3f12cef623 100644 --- a/docs/internals/builtins/io/stream_socket_enable_crypto.md +++ b/docs/internals/builtins/io/stream_socket_enable_crypto.md @@ -2,7 +2,7 @@ title: "stream_socket_enable_crypto() — internals" description: "Compiler internals for stream_socket_enable_crypto(): lowering path, type checks, and runtime helpers." sidebar: - order: 218 + order: 222 --- ## `stream_socket_enable_crypto()` — internals @@ -32,6 +32,11 @@ function stream_socket_enable_crypto(resource $stream, bool $enable, int $crypto - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_enable_crypto()`](../../../php/builtins/io/stream_socket_enable_crypto.md) diff --git a/docs/internals/builtins/io/stream_socket_get_name.md b/docs/internals/builtins/io/stream_socket_get_name.md index 0108165a9d..1a983ced5d 100644 --- a/docs/internals/builtins/io/stream_socket_get_name.md +++ b/docs/internals/builtins/io/stream_socket_get_name.md @@ -2,7 +2,7 @@ title: "stream_socket_get_name() — internals" description: "Compiler internals for stream_socket_get_name(): lowering path, type checks, and runtime helpers." sidebar: - order: 219 + order: 223 --- ## `stream_socket_get_name()` — internals @@ -33,6 +33,11 @@ function stream_socket_get_name(resource $socket, bool $remote): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_get_name()`](../../../php/builtins/io/stream_socket_get_name.md) diff --git a/docs/internals/builtins/io/stream_socket_pair.md b/docs/internals/builtins/io/stream_socket_pair.md index d0cca12362..11a3f2d947 100644 --- a/docs/internals/builtins/io/stream_socket_pair.md +++ b/docs/internals/builtins/io/stream_socket_pair.md @@ -2,7 +2,7 @@ title: "stream_socket_pair() — internals" description: "Compiler internals for stream_socket_pair(): lowering path, type checks, and runtime helpers." sidebar: - order: 220 + order: 224 --- ## `stream_socket_pair()` — internals @@ -33,6 +33,11 @@ function stream_socket_pair(int $domain, int $type, int $protocol): mixed - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_pair()`](../../../php/builtins/io/stream_socket_pair.md) diff --git a/docs/internals/builtins/io/stream_socket_recvfrom.md b/docs/internals/builtins/io/stream_socket_recvfrom.md index 12ccb5cbcc..477c004ab9 100644 --- a/docs/internals/builtins/io/stream_socket_recvfrom.md +++ b/docs/internals/builtins/io/stream_socket_recvfrom.md @@ -2,7 +2,7 @@ title: "stream_socket_recvfrom() — internals" description: "Compiler internals for stream_socket_recvfrom(): lowering path, type checks, and runtime helpers." sidebar: - order: 221 + order: 225 --- ## `stream_socket_recvfrom()` — internals @@ -33,6 +33,12 @@ function stream_socket_recvfrom(resource $socket, int $length, int $flags = 0, s - **Arity**: takes 2–4 arguments (2 optional). - **By-reference parameters**: `$address`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$address`. + ## Cross-references - [User reference for `stream_socket_recvfrom()`](../../../php/builtins/io/stream_socket_recvfrom.md) diff --git a/docs/internals/builtins/io/stream_socket_sendto.md b/docs/internals/builtins/io/stream_socket_sendto.md index 0350af291b..52c1ce1c38 100644 --- a/docs/internals/builtins/io/stream_socket_sendto.md +++ b/docs/internals/builtins/io/stream_socket_sendto.md @@ -2,7 +2,7 @@ title: "stream_socket_sendto() — internals" description: "Compiler internals for stream_socket_sendto(): lowering path, type checks, and runtime helpers." sidebar: - order: 222 + order: 226 --- ## `stream_socket_sendto()` — internals @@ -32,6 +32,11 @@ function stream_socket_sendto(resource $socket, string $data, int $flags = 0, st - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_sendto()`](../../../php/builtins/io/stream_socket_sendto.md) diff --git a/docs/internals/builtins/io/stream_socket_server.md b/docs/internals/builtins/io/stream_socket_server.md index 86922f9e4e..370a71f217 100644 --- a/docs/internals/builtins/io/stream_socket_server.md +++ b/docs/internals/builtins/io/stream_socket_server.md @@ -2,7 +2,7 @@ title: "stream_socket_server() — internals" description: "Compiler internals for stream_socket_server(): lowering path, type checks, and runtime helpers." sidebar: - order: 223 + order: 227 --- ## `stream_socket_server()` — internals @@ -33,6 +33,11 @@ function stream_socket_server(string $address): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_server()`](../../../php/builtins/io/stream_socket_server.md) diff --git a/docs/internals/builtins/io/stream_socket_shutdown.md b/docs/internals/builtins/io/stream_socket_shutdown.md index e5eb60e571..a3fe46d72c 100644 --- a/docs/internals/builtins/io/stream_socket_shutdown.md +++ b/docs/internals/builtins/io/stream_socket_shutdown.md @@ -2,7 +2,7 @@ title: "stream_socket_shutdown() — internals" description: "Compiler internals for stream_socket_shutdown(): lowering path, type checks, and runtime helpers." sidebar: - order: 224 + order: 228 --- ## `stream_socket_shutdown()` — internals @@ -33,6 +33,11 @@ function stream_socket_shutdown(resource $stream, int $mode): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_socket_shutdown()`](../../../php/builtins/io/stream_socket_shutdown.md) diff --git a/docs/internals/builtins/io/stream_supports_lock.md b/docs/internals/builtins/io/stream_supports_lock.md index 82a20d07f7..adf0c7dfbd 100644 --- a/docs/internals/builtins/io/stream_supports_lock.md +++ b/docs/internals/builtins/io/stream_supports_lock.md @@ -2,7 +2,7 @@ title: "stream_supports_lock() — internals" description: "Compiler internals for stream_supports_lock(): lowering path, type checks, and runtime helpers." sidebar: - order: 225 + order: 229 --- ## `stream_supports_lock()` — internals @@ -33,6 +33,11 @@ function stream_supports_lock(resource $stream): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_supports_lock()`](../../../php/builtins/io/stream_supports_lock.md) diff --git a/docs/internals/builtins/io/stream_wrapper_register.md b/docs/internals/builtins/io/stream_wrapper_register.md index 4ed40920b3..fae50ea126 100644 --- a/docs/internals/builtins/io/stream_wrapper_register.md +++ b/docs/internals/builtins/io/stream_wrapper_register.md @@ -2,7 +2,7 @@ title: "stream_wrapper_register() — internals" description: "Compiler internals for stream_wrapper_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 226 + order: 230 --- ## `stream_wrapper_register()` — internals @@ -33,6 +33,11 @@ function stream_wrapper_register(string $protocol, string $class, int $flags = 0 - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_wrapper_register()`](../../../php/builtins/io/stream_wrapper_register.md) diff --git a/docs/internals/builtins/io/stream_wrapper_restore.md b/docs/internals/builtins/io/stream_wrapper_restore.md index 8e82185c1a..a2cc8bd362 100644 --- a/docs/internals/builtins/io/stream_wrapper_restore.md +++ b/docs/internals/builtins/io/stream_wrapper_restore.md @@ -2,7 +2,7 @@ title: "stream_wrapper_restore() — internals" description: "Compiler internals for stream_wrapper_restore(): lowering path, type checks, and runtime helpers." sidebar: - order: 227 + order: 231 --- ## `stream_wrapper_restore()` — internals @@ -32,6 +32,11 @@ function stream_wrapper_restore(string $protocol): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_wrapper_restore()`](../../../php/builtins/io/stream_wrapper_restore.md) diff --git a/docs/internals/builtins/io/stream_wrapper_unregister.md b/docs/internals/builtins/io/stream_wrapper_unregister.md index aaa6b3b57b..ed8ab7cf02 100644 --- a/docs/internals/builtins/io/stream_wrapper_unregister.md +++ b/docs/internals/builtins/io/stream_wrapper_unregister.md @@ -2,7 +2,7 @@ title: "stream_wrapper_unregister() — internals" description: "Compiler internals for stream_wrapper_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 228 + order: 232 --- ## `stream_wrapper_unregister()` — internals @@ -33,6 +33,11 @@ function stream_wrapper_unregister(string $protocol): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_wrapper_unregister()`](../../../php/builtins/io/stream_wrapper_unregister.md) diff --git a/docs/internals/builtins/io/vfprintf.md b/docs/internals/builtins/io/vfprintf.md index 3a7fdd8e50..266370d440 100644 --- a/docs/internals/builtins/io/vfprintf.md +++ b/docs/internals/builtins/io/vfprintf.md @@ -2,7 +2,7 @@ title: "vfprintf() — internals" description: "Compiler internals for vfprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 229 + order: 233 --- ## `vfprintf()` — internals @@ -34,6 +34,11 @@ function vfprintf(resource $stream, string $format, array $values): int - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `vfprintf()`](../../../php/builtins/io/vfprintf.md) diff --git a/docs/internals/builtins/json/json_decode.md b/docs/internals/builtins/json/json_decode.md index 9c7bdef87c..93169d8572 100644 --- a/docs/internals/builtins/json/json_decode.md +++ b/docs/internals/builtins/json/json_decode.md @@ -2,7 +2,7 @@ title: "json_decode() — internals" description: "Compiler internals for json_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 230 + order: 234 --- ## `json_decode()` — internals @@ -33,6 +33,11 @@ function json_decode(string $json, bool $associative = null, int $depth = 512, i - **Arity**: takes 1–4 arguments (3 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_decode()`](../../../php/builtins/json/json_decode.md) diff --git a/docs/internals/builtins/json/json_encode.md b/docs/internals/builtins/json/json_encode.md index 0b4a9d768a..37b908ea97 100644 --- a/docs/internals/builtins/json/json_encode.md +++ b/docs/internals/builtins/json/json_encode.md @@ -2,7 +2,7 @@ title: "json_encode() — internals" description: "Compiler internals for json_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 231 + order: 235 --- ## `json_encode()` — internals @@ -32,6 +32,11 @@ function json_encode(mixed $value, int $flags = 0, int $depth = 512): string - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_encode()`](../../../php/builtins/json/json_encode.md) diff --git a/docs/internals/builtins/json/json_last_error.md b/docs/internals/builtins/json/json_last_error.md index 0e1d486891..de89d91702 100644 --- a/docs/internals/builtins/json/json_last_error.md +++ b/docs/internals/builtins/json/json_last_error.md @@ -2,7 +2,7 @@ title: "json_last_error() — internals" description: "Compiler internals for json_last_error(): lowering path, type checks, and runtime helpers." sidebar: - order: 232 + order: 236 --- ## `json_last_error()` — internals @@ -33,6 +33,11 @@ function json_last_error(): int - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_last_error()`](../../../php/builtins/json/json_last_error.md) diff --git a/docs/internals/builtins/json/json_last_error_msg.md b/docs/internals/builtins/json/json_last_error_msg.md index cb96b19c43..02f6d79abe 100644 --- a/docs/internals/builtins/json/json_last_error_msg.md +++ b/docs/internals/builtins/json/json_last_error_msg.md @@ -2,7 +2,7 @@ title: "json_last_error_msg() — internals" description: "Compiler internals for json_last_error_msg(): lowering path, type checks, and runtime helpers." sidebar: - order: 233 + order: 237 --- ## `json_last_error_msg()` — internals @@ -34,6 +34,11 @@ function json_last_error_msg(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_last_error_msg()`](../../../php/builtins/json/json_last_error_msg.md) diff --git a/docs/internals/builtins/json/json_validate.md b/docs/internals/builtins/json/json_validate.md index 3fad80ebc2..19b93d6795 100644 --- a/docs/internals/builtins/json/json_validate.md +++ b/docs/internals/builtins/json/json_validate.md @@ -2,7 +2,7 @@ title: "json_validate() — internals" description: "Compiler internals for json_validate(): lowering path, type checks, and runtime helpers." sidebar: - order: 234 + order: 238 --- ## `json_validate()` — internals @@ -33,6 +33,11 @@ function json_validate(string $json, int $depth = 512, int $flags = 0): bool - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `json_validate()`](../../../php/builtins/json/json_validate.md) diff --git a/docs/internals/builtins/math/abs.md b/docs/internals/builtins/math/abs.md index 7922313ede..5a9a42fdb7 100644 --- a/docs/internals/builtins/math/abs.md +++ b/docs/internals/builtins/math/abs.md @@ -2,7 +2,7 @@ title: "abs() — internals" description: "Compiler internals for abs(): lowering path, type checks, and runtime helpers." sidebar: - order: 235 + order: 239 --- ## `abs()` — internals @@ -33,6 +33,11 @@ function abs(int $num): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/abs.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/abs.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `abs()`](../../../php/builtins/math/abs.md) diff --git a/docs/internals/builtins/math/acos.md b/docs/internals/builtins/math/acos.md index 1d800b1477..64ef7b0b38 100644 --- a/docs/internals/builtins/math/acos.md +++ b/docs/internals/builtins/math/acos.md @@ -2,7 +2,7 @@ title: "acos() — internals" description: "Compiler internals for acos(): lowering path, type checks, and runtime helpers." sidebar: - order: 236 + order: 240 --- ## `acos()` — internals @@ -32,6 +32,11 @@ function acos(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/acos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/acos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `acos()`](../../../php/builtins/math/acos.md) diff --git a/docs/internals/builtins/math/asin.md b/docs/internals/builtins/math/asin.md index 849ba063e2..052bd93436 100644 --- a/docs/internals/builtins/math/asin.md +++ b/docs/internals/builtins/math/asin.md @@ -2,7 +2,7 @@ title: "asin() — internals" description: "Compiler internals for asin(): lowering path, type checks, and runtime helpers." sidebar: - order: 237 + order: 241 --- ## `asin()` — internals @@ -32,6 +32,11 @@ function asin(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/asin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/asin.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `asin()`](../../../php/builtins/math/asin.md) diff --git a/docs/internals/builtins/math/atan.md b/docs/internals/builtins/math/atan.md index f60bbb0fe2..1e4325b36a 100644 --- a/docs/internals/builtins/math/atan.md +++ b/docs/internals/builtins/math/atan.md @@ -2,7 +2,7 @@ title: "atan() — internals" description: "Compiler internals for atan(): lowering path, type checks, and runtime helpers." sidebar: - order: 238 + order: 242 --- ## `atan()` — internals @@ -32,6 +32,11 @@ function atan(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/atan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/atan.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `atan()`](../../../php/builtins/math/atan.md) diff --git a/docs/internals/builtins/math/atan2.md b/docs/internals/builtins/math/atan2.md index e45ecf6a8c..2f063340bd 100644 --- a/docs/internals/builtins/math/atan2.md +++ b/docs/internals/builtins/math/atan2.md @@ -2,7 +2,7 @@ title: "atan2() — internals" description: "Compiler internals for atan2(): lowering path, type checks, and runtime helpers." sidebar: - order: 239 + order: 243 --- ## `atan2()` — internals @@ -32,6 +32,11 @@ function atan2(float $y, float $x): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/atan2.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `atan2()`](../../../php/builtins/math/atan2.md) diff --git a/docs/internals/builtins/math/ceil.md b/docs/internals/builtins/math/ceil.md index cfaa80576d..e8960469c2 100644 --- a/docs/internals/builtins/math/ceil.md +++ b/docs/internals/builtins/math/ceil.md @@ -2,7 +2,7 @@ title: "ceil() — internals" description: "Compiler internals for ceil(): lowering path, type checks, and runtime helpers." sidebar: - order: 240 + order: 244 --- ## `ceil()` — internals @@ -32,6 +32,11 @@ function ceil(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/ceil.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ceil()`](../../../php/builtins/math/ceil.md) diff --git a/docs/internals/builtins/math/clamp.md b/docs/internals/builtins/math/clamp.md index ca4f8a19b3..3e7c25b27e 100644 --- a/docs/internals/builtins/math/clamp.md +++ b/docs/internals/builtins/math/clamp.md @@ -2,7 +2,7 @@ title: "clamp() — internals" description: "Compiler internals for clamp(): lowering path, type checks, and runtime helpers." sidebar: - order: 241 + order: 245 --- ## `clamp()` — internals @@ -32,6 +32,11 @@ function clamp(int $value, int $min, int $max): mixed - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/clamp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `clamp()`](../../../php/builtins/math/clamp.md) diff --git a/docs/internals/builtins/math/cos.md b/docs/internals/builtins/math/cos.md index 0e995e6cc9..e6c61301d5 100644 --- a/docs/internals/builtins/math/cos.md +++ b/docs/internals/builtins/math/cos.md @@ -2,7 +2,7 @@ title: "cos() — internals" description: "Compiler internals for cos(): lowering path, type checks, and runtime helpers." sidebar: - order: 242 + order: 246 --- ## `cos()` — internals @@ -32,6 +32,11 @@ function cos(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/cos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/cos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `cos()`](../../../php/builtins/math/cos.md) diff --git a/docs/internals/builtins/math/cosh.md b/docs/internals/builtins/math/cosh.md index 1ce7156dbe..7030bfdced 100644 --- a/docs/internals/builtins/math/cosh.md +++ b/docs/internals/builtins/math/cosh.md @@ -2,7 +2,7 @@ title: "cosh() — internals" description: "Compiler internals for cosh(): lowering path, type checks, and runtime helpers." sidebar: - order: 243 + order: 247 --- ## `cosh()` — internals @@ -32,6 +32,11 @@ function cosh(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/cosh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `cosh()`](../../../php/builtins/math/cosh.md) diff --git a/docs/internals/builtins/math/deg2rad.md b/docs/internals/builtins/math/deg2rad.md index 57aa0dc01e..a73d330f79 100644 --- a/docs/internals/builtins/math/deg2rad.md +++ b/docs/internals/builtins/math/deg2rad.md @@ -2,7 +2,7 @@ title: "deg2rad() — internals" description: "Compiler internals for deg2rad(): lowering path, type checks, and runtime helpers." sidebar: - order: 244 + order: 248 --- ## `deg2rad()` — internals @@ -32,6 +32,11 @@ function deg2rad(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `deg2rad()`](../../../php/builtins/math/deg2rad.md) diff --git a/docs/internals/builtins/math/exp.md b/docs/internals/builtins/math/exp.md index 8e55435630..dc18642e7a 100644 --- a/docs/internals/builtins/math/exp.md +++ b/docs/internals/builtins/math/exp.md @@ -2,7 +2,7 @@ title: "exp() — internals" description: "Compiler internals for exp(): lowering path, type checks, and runtime helpers." sidebar: - order: 245 + order: 249 --- ## `exp()` — internals @@ -32,6 +32,11 @@ function exp(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/exp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/exp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `exp()`](../../../php/builtins/math/exp.md) diff --git a/docs/internals/builtins/math/fdiv.md b/docs/internals/builtins/math/fdiv.md index 2779c26376..7ff386f1d7 100644 --- a/docs/internals/builtins/math/fdiv.md +++ b/docs/internals/builtins/math/fdiv.md @@ -2,7 +2,7 @@ title: "fdiv() — internals" description: "Compiler internals for fdiv(): lowering path, type checks, and runtime helpers." sidebar: - order: 246 + order: 250 --- ## `fdiv()` — internals @@ -32,6 +32,11 @@ function fdiv(float $num1, float $num2): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fdiv()`](../../../php/builtins/math/fdiv.md) diff --git a/docs/internals/builtins/math/floor.md b/docs/internals/builtins/math/floor.md index c197992643..14f7a01be5 100644 --- a/docs/internals/builtins/math/floor.md +++ b/docs/internals/builtins/math/floor.md @@ -2,7 +2,7 @@ title: "floor() — internals" description: "Compiler internals for floor(): lowering path, type checks, and runtime helpers." sidebar: - order: 247 + order: 251 --- ## `floor()` — internals @@ -32,6 +32,11 @@ function floor(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/floor.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/floor.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `floor()`](../../../php/builtins/math/floor.md) diff --git a/docs/internals/builtins/math/fmod.md b/docs/internals/builtins/math/fmod.md index 9ad291188d..afcff9a447 100644 --- a/docs/internals/builtins/math/fmod.md +++ b/docs/internals/builtins/math/fmod.md @@ -2,7 +2,7 @@ title: "fmod() — internals" description: "Compiler internals for fmod(): lowering path, type checks, and runtime helpers." sidebar: - order: 248 + order: 252 --- ## `fmod()` — internals @@ -32,6 +32,11 @@ function fmod(float $num1, float $num2): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/fmod.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `fmod()`](../../../php/builtins/math/fmod.md) diff --git a/docs/internals/builtins/math/hypot.md b/docs/internals/builtins/math/hypot.md index 37b485883f..ea2d25abe4 100644 --- a/docs/internals/builtins/math/hypot.md +++ b/docs/internals/builtins/math/hypot.md @@ -2,7 +2,7 @@ title: "hypot() — internals" description: "Compiler internals for hypot(): lowering path, type checks, and runtime helpers." sidebar: - order: 249 + order: 253 --- ## `hypot()` — internals @@ -32,6 +32,11 @@ function hypot(float $x, float $y): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/hypot.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hypot()`](../../../php/builtins/math/hypot.md) diff --git a/docs/internals/builtins/math/intdiv.md b/docs/internals/builtins/math/intdiv.md index 8121f26dc3..671ed80851 100644 --- a/docs/internals/builtins/math/intdiv.md +++ b/docs/internals/builtins/math/intdiv.md @@ -2,7 +2,7 @@ title: "intdiv() — internals" description: "Compiler internals for intdiv(): lowering path, type checks, and runtime helpers." sidebar: - order: 250 + order: 254 --- ## `intdiv()` — internals @@ -32,6 +32,11 @@ function intdiv(int $num1, int $num2): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `intdiv()`](../../../php/builtins/math/intdiv.md) diff --git a/docs/internals/builtins/math/is_finite.md b/docs/internals/builtins/math/is_finite.md index 1ce482dba4..649142dde7 100644 --- a/docs/internals/builtins/math/is_finite.md +++ b/docs/internals/builtins/math/is_finite.md @@ -2,7 +2,7 @@ title: "is_finite() — internals" description: "Compiler internals for is_finite(): lowering path, type checks, and runtime helpers." sidebar: - order: 251 + order: 255 --- ## `is_finite()` — internals @@ -32,6 +32,11 @@ function is_finite(float $num): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_finite()`](../../../php/builtins/math/is_finite.md) diff --git a/docs/internals/builtins/math/is_infinite.md b/docs/internals/builtins/math/is_infinite.md index 341d462e3f..d58d9f120a 100644 --- a/docs/internals/builtins/math/is_infinite.md +++ b/docs/internals/builtins/math/is_infinite.md @@ -2,7 +2,7 @@ title: "is_infinite() — internals" description: "Compiler internals for is_infinite(): lowering path, type checks, and runtime helpers." sidebar: - order: 252 + order: 256 --- ## `is_infinite()` — internals @@ -32,6 +32,11 @@ function is_infinite(float $num): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_infinite()`](../../../php/builtins/math/is_infinite.md) diff --git a/docs/internals/builtins/math/is_nan.md b/docs/internals/builtins/math/is_nan.md index 3d54cef485..1ad2394394 100644 --- a/docs/internals/builtins/math/is_nan.md +++ b/docs/internals/builtins/math/is_nan.md @@ -2,7 +2,7 @@ title: "is_nan() — internals" description: "Compiler internals for is_nan(): lowering path, type checks, and runtime helpers." sidebar: - order: 253 + order: 257 --- ## `is_nan()` — internals @@ -32,6 +32,11 @@ function is_nan(float $num): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_nan()`](../../../php/builtins/math/is_nan.md) diff --git a/docs/internals/builtins/math/log.md b/docs/internals/builtins/math/log.md index 11999ebe30..6d4cddf357 100644 --- a/docs/internals/builtins/math/log.md +++ b/docs/internals/builtins/math/log.md @@ -2,7 +2,7 @@ title: "log() — internals" description: "Compiler internals for log(): lowering path, type checks, and runtime helpers." sidebar: - order: 254 + order: 258 --- ## `log()` — internals @@ -32,6 +32,11 @@ function log(float $num, float $base = 2.718281828459045): float - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/log.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `log()`](../../../php/builtins/math/log.md) diff --git a/docs/internals/builtins/math/log10.md b/docs/internals/builtins/math/log10.md index b02032e611..171d366da7 100644 --- a/docs/internals/builtins/math/log10.md +++ b/docs/internals/builtins/math/log10.md @@ -2,7 +2,7 @@ title: "log10() — internals" description: "Compiler internals for log10(): lowering path, type checks, and runtime helpers." sidebar: - order: 255 + order: 259 --- ## `log10()` — internals @@ -32,6 +32,11 @@ function log10(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/log10.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log10.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `log10()`](../../../php/builtins/math/log10.md) diff --git a/docs/internals/builtins/math/log2.md b/docs/internals/builtins/math/log2.md index ffa1dbca15..81b8cbb081 100644 --- a/docs/internals/builtins/math/log2.md +++ b/docs/internals/builtins/math/log2.md @@ -2,7 +2,7 @@ title: "log2() — internals" description: "Compiler internals for log2(): lowering path, type checks, and runtime helpers." sidebar: - order: 256 + order: 260 --- ## `log2()` — internals @@ -32,6 +32,11 @@ function log2(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/log2.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log2.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `log2()`](../../../php/builtins/math/log2.md) diff --git a/docs/internals/builtins/math/max.md b/docs/internals/builtins/math/max.md index 0a31bbee79..ad78ea4d65 100644 --- a/docs/internals/builtins/math/max.md +++ b/docs/internals/builtins/math/max.md @@ -2,7 +2,7 @@ title: "max() — internals" description: "Compiler internals for max(): lowering path, type checks, and runtime helpers." sidebar: - order: 257 + order: 261 --- ## `max()` — internals @@ -33,6 +33,12 @@ function max(mixed $value, ...$values): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/max.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/max.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `max()`](../../../php/builtins/math/max.md) diff --git a/docs/internals/builtins/math/min.md b/docs/internals/builtins/math/min.md index d39f3d4b3e..d59126958c 100644 --- a/docs/internals/builtins/math/min.md +++ b/docs/internals/builtins/math/min.md @@ -2,7 +2,7 @@ title: "min() — internals" description: "Compiler internals for min(): lowering path, type checks, and runtime helpers." sidebar: - order: 258 + order: 262 --- ## `min()` — internals @@ -33,6 +33,12 @@ function min(mixed $value, ...$values): mixed - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/min.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/min.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `min()`](../../../php/builtins/math/min.md) diff --git a/docs/internals/builtins/math/mt_rand.md b/docs/internals/builtins/math/mt_rand.md index 468eb0ea11..60746fc7d9 100644 --- a/docs/internals/builtins/math/mt_rand.md +++ b/docs/internals/builtins/math/mt_rand.md @@ -2,7 +2,7 @@ title: "mt_rand() — internals" description: "Compiler internals for mt_rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 259 + order: 263 --- ## `mt_rand()` — internals @@ -33,6 +33,11 @@ function mt_rand(int $min, int $max): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `mt_rand()`](../../../php/builtins/math/mt_rand.md) diff --git a/docs/internals/builtins/math/pi.md b/docs/internals/builtins/math/pi.md index 16b2a09935..17aaacff6b 100644 --- a/docs/internals/builtins/math/pi.md +++ b/docs/internals/builtins/math/pi.md @@ -2,7 +2,7 @@ title: "pi() — internals" description: "Compiler internals for pi(): lowering path, type checks, and runtime helpers." sidebar: - order: 260 + order: 264 --- ## `pi()` — internals @@ -32,6 +32,11 @@ function pi(): float - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/pi.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/pi.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `pi()`](../../../php/builtins/math/pi.md) diff --git a/docs/internals/builtins/math/pow.md b/docs/internals/builtins/math/pow.md index e0996e87bf..31f7bbafef 100644 --- a/docs/internals/builtins/math/pow.md +++ b/docs/internals/builtins/math/pow.md @@ -2,7 +2,7 @@ title: "pow() — internals" description: "Compiler internals for pow(): lowering path, type checks, and runtime helpers." sidebar: - order: 261 + order: 265 --- ## `pow()` — internals @@ -32,6 +32,11 @@ function pow(float $num, float $exponent): float - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/pow.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/pow.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `pow()`](../../../php/builtins/math/pow.md) diff --git a/docs/internals/builtins/math/rad2deg.md b/docs/internals/builtins/math/rad2deg.md index 621a148d0f..9602a4e606 100644 --- a/docs/internals/builtins/math/rad2deg.md +++ b/docs/internals/builtins/math/rad2deg.md @@ -2,7 +2,7 @@ title: "rad2deg() — internals" description: "Compiler internals for rad2deg(): lowering path, type checks, and runtime helpers." sidebar: - order: 262 + order: 266 --- ## `rad2deg()` — internals @@ -32,6 +32,11 @@ function rad2deg(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rad2deg()`](../../../php/builtins/math/rad2deg.md) diff --git a/docs/internals/builtins/math/rand.md b/docs/internals/builtins/math/rand.md index 6aa48de0d2..d1b112c1e3 100644 --- a/docs/internals/builtins/math/rand.md +++ b/docs/internals/builtins/math/rand.md @@ -2,7 +2,7 @@ title: "rand() — internals" description: "Compiler internals for rand(): lowering path, type checks, and runtime helpers." sidebar: - order: 263 + order: 267 --- ## `rand()` — internals @@ -33,6 +33,11 @@ function rand(int $min, int $max): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/rand.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rand()`](../../../php/builtins/math/rand.md) diff --git a/docs/internals/builtins/math/random_int.md b/docs/internals/builtins/math/random_int.md index 90c988932a..ba13aad3e0 100644 --- a/docs/internals/builtins/math/random_int.md +++ b/docs/internals/builtins/math/random_int.md @@ -2,7 +2,7 @@ title: "random_int() — internals" description: "Compiler internals for random_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 264 + order: 268 --- ## `random_int()` — internals @@ -32,6 +32,11 @@ function random_int(int $min, int $max): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/random_int.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `random_int()`](../../../php/builtins/math/random_int.md) diff --git a/docs/internals/builtins/math/round.md b/docs/internals/builtins/math/round.md index de5adfdb6e..3ca3bd0f96 100644 --- a/docs/internals/builtins/math/round.md +++ b/docs/internals/builtins/math/round.md @@ -2,7 +2,7 @@ title: "round() — internals" description: "Compiler internals for round(): lowering path, type checks, and runtime helpers." sidebar: - order: 265 + order: 269 --- ## `round()` — internals @@ -32,6 +32,11 @@ function round(float $num, int $precision = 0): float - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/round.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/round.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `round()`](../../../php/builtins/math/round.md) diff --git a/docs/internals/builtins/math/sin.md b/docs/internals/builtins/math/sin.md index 28477b86ae..667c9121b8 100644 --- a/docs/internals/builtins/math/sin.md +++ b/docs/internals/builtins/math/sin.md @@ -2,7 +2,7 @@ title: "sin() — internals" description: "Compiler internals for sin(): lowering path, type checks, and runtime helpers." sidebar: - order: 266 + order: 270 --- ## `sin()` — internals @@ -32,6 +32,11 @@ function sin(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/sin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sin.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sin()`](../../../php/builtins/math/sin.md) diff --git a/docs/internals/builtins/math/sinh.md b/docs/internals/builtins/math/sinh.md index f9ca8eb208..2e7cf70133 100644 --- a/docs/internals/builtins/math/sinh.md +++ b/docs/internals/builtins/math/sinh.md @@ -2,7 +2,7 @@ title: "sinh() — internals" description: "Compiler internals for sinh(): lowering path, type checks, and runtime helpers." sidebar: - order: 267 + order: 271 --- ## `sinh()` — internals @@ -32,6 +32,11 @@ function sinh(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/sinh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sinh()`](../../../php/builtins/math/sinh.md) diff --git a/docs/internals/builtins/math/sqrt.md b/docs/internals/builtins/math/sqrt.md index 93c18ecd56..91f946a372 100644 --- a/docs/internals/builtins/math/sqrt.md +++ b/docs/internals/builtins/math/sqrt.md @@ -2,7 +2,7 @@ title: "sqrt() — internals" description: "Compiler internals for sqrt(): lowering path, type checks, and runtime helpers." sidebar: - order: 268 + order: 272 --- ## `sqrt()` — internals @@ -32,6 +32,11 @@ function sqrt(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sqrt()`](../../../php/builtins/math/sqrt.md) diff --git a/docs/internals/builtins/math/tan.md b/docs/internals/builtins/math/tan.md index 3344f007d5..870d6cab25 100644 --- a/docs/internals/builtins/math/tan.md +++ b/docs/internals/builtins/math/tan.md @@ -2,7 +2,7 @@ title: "tan() — internals" description: "Compiler internals for tan(): lowering path, type checks, and runtime helpers." sidebar: - order: 269 + order: 273 --- ## `tan()` — internals @@ -32,6 +32,11 @@ function tan(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/tan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/tan.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `tan()`](../../../php/builtins/math/tan.md) diff --git a/docs/internals/builtins/math/tanh.md b/docs/internals/builtins/math/tanh.md index 596d98d893..3fc53e3d67 100644 --- a/docs/internals/builtins/math/tanh.md +++ b/docs/internals/builtins/math/tanh.md @@ -2,7 +2,7 @@ title: "tanh() — internals" description: "Compiler internals for tanh(): lowering path, type checks, and runtime helpers." sidebar: - order: 270 + order: 274 --- ## `tanh()` — internals @@ -32,6 +32,11 @@ function tanh(float $num): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/math/tanh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `tanh()`](../../../php/builtins/math/tanh.md) diff --git a/docs/internals/builtins/misc/buffer_new.md b/docs/internals/builtins/misc/buffer_new.md index 1972bf2820..a044d59f59 100644 --- a/docs/internals/builtins/misc/buffer_new.md +++ b/docs/internals/builtins/misc/buffer_new.md @@ -2,7 +2,7 @@ title: "buffer_new() — internals" description: "Compiler internals for buffer_new(): lowering path, type checks, and runtime helpers." sidebar: - order: 271 + order: 275 --- ## `buffer_new()` — internals @@ -28,6 +28,11 @@ function buffer_new(int $length): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `buffer_new()`](../../../php/builtins/misc/buffer_new.md) diff --git a/docs/internals/builtins/misc/define.md b/docs/internals/builtins/misc/define.md index 0c0d382b39..37079664aa 100644 --- a/docs/internals/builtins/misc/define.md +++ b/docs/internals/builtins/misc/define.md @@ -2,7 +2,7 @@ title: "define() — internals" description: "Compiler internals for define(): lowering path, type checks, and runtime helpers." sidebar: - order: 272 + order: 276 --- ## `define()` — internals @@ -32,6 +32,11 @@ function define(string $constant_name, mixed $value): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/define.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/define.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `define()`](../../../php/builtins/misc/define.md) diff --git a/docs/internals/builtins/misc/defined.md b/docs/internals/builtins/misc/defined.md index 0fa8159e80..83900ba1b5 100644 --- a/docs/internals/builtins/misc/defined.md +++ b/docs/internals/builtins/misc/defined.md @@ -2,7 +2,7 @@ title: "defined() — internals" description: "Compiler internals for defined(): lowering path, type checks, and runtime helpers." sidebar: - order: 273 + order: 277 --- ## `defined()` — internals @@ -32,6 +32,11 @@ function defined(string $constant_name): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/defined.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `defined()`](../../../php/builtins/misc/defined.md) diff --git a/docs/internals/builtins/misc/empty.md b/docs/internals/builtins/misc/empty.md index 4acdb8052b..4830ba86d9 100644 --- a/docs/internals/builtins/misc/empty.md +++ b/docs/internals/builtins/misc/empty.md @@ -2,7 +2,7 @@ title: "empty() — internals" description: "Compiler internals for empty(): lowering path, type checks, and runtime helpers." sidebar: - order: 274 + order: 278 --- ## `empty()` — internals @@ -33,6 +33,11 @@ function empty(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `empty()`](../../../php/builtins/misc/empty.md) diff --git a/docs/internals/builtins/misc/header.md b/docs/internals/builtins/misc/header.md index 55a09c95d7..e250babb92 100644 --- a/docs/internals/builtins/misc/header.md +++ b/docs/internals/builtins/misc/header.md @@ -2,7 +2,7 @@ title: "header() — internals" description: "Compiler internals for header(): lowering path, type checks, and runtime helpers." sidebar: - order: 275 + order: 279 --- ## `header()` — internals @@ -38,6 +38,11 @@ function header(string $header, bool $replace = true, int $response_code = 0): v - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/header.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/header.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `header()`](../../../php/builtins/misc/header.md) diff --git a/docs/internals/builtins/misc/http_response_code.md b/docs/internals/builtins/misc/http_response_code.md index db23b4f74e..03e0f8aba4 100644 --- a/docs/internals/builtins/misc/http_response_code.md +++ b/docs/internals/builtins/misc/http_response_code.md @@ -2,7 +2,7 @@ title: "http_response_code() — internals" description: "Compiler internals for http_response_code(): lowering path, type checks, and runtime helpers." sidebar: - order: 276 + order: 280 --- ## `http_response_code()` — internals @@ -37,6 +37,11 @@ function http_response_code(int $response_code = 0): int - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `http_response_code()`](../../../php/builtins/misc/http_response_code.md) diff --git a/docs/internals/builtins/misc/isset.md b/docs/internals/builtins/misc/isset.md index 6951a04bb5..7f28f83f13 100644 --- a/docs/internals/builtins/misc/isset.md +++ b/docs/internals/builtins/misc/isset.md @@ -2,7 +2,7 @@ title: "isset() — internals" description: "Compiler internals for isset(): lowering path, type checks, and runtime helpers." sidebar: - order: 277 + order: 281 --- ## `isset()` — internals @@ -33,6 +33,12 @@ function isset(mixed $var, ...$vars): bool - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$vars`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$vars`. + ## Cross-references - [User reference for `isset()`](../../../php/builtins/misc/isset.md) diff --git a/docs/internals/builtins/misc/php_uname.md b/docs/internals/builtins/misc/php_uname.md index d24b3ebcb2..919f526874 100644 --- a/docs/internals/builtins/misc/php_uname.md +++ b/docs/internals/builtins/misc/php_uname.md @@ -2,7 +2,7 @@ title: "php_uname() — internals" description: "Compiler internals for php_uname(): lowering path, type checks, and runtime helpers." sidebar: - order: 278 + order: 282 --- ## `php_uname()` — internals @@ -33,6 +33,11 @@ function php_uname(string $mode = 'a'): string - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `php_uname()`](../../../php/builtins/misc/php_uname.md) diff --git a/docs/internals/builtins/misc/phpversion.md b/docs/internals/builtins/misc/phpversion.md index f01da4534a..278abe7712 100644 --- a/docs/internals/builtins/misc/phpversion.md +++ b/docs/internals/builtins/misc/phpversion.md @@ -2,7 +2,7 @@ title: "phpversion() — internals" description: "Compiler internals for phpversion(): lowering path, type checks, and runtime helpers." sidebar: - order: 279 + order: 283 --- ## `phpversion()` — internals @@ -32,6 +32,11 @@ function phpversion(): string - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `phpversion()`](../../../php/builtins/misc/phpversion.md) diff --git a/docs/internals/builtins/misc/print_r.md b/docs/internals/builtins/misc/print_r.md index 7e8d6a11f4..160f3da280 100644 --- a/docs/internals/builtins/misc/print_r.md +++ b/docs/internals/builtins/misc/print_r.md @@ -2,7 +2,7 @@ title: "print_r() — internals" description: "Compiler internals for print_r(): lowering path, type checks, and runtime helpers." sidebar: - order: 280 + order: 284 --- ## `print_r()` — internals @@ -42,6 +42,11 @@ function print_r(mixed $value, bool $return = false): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/print_r.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `print_r()`](../../../php/builtins/misc/print_r.md) diff --git a/docs/internals/builtins/misc/serialize.md b/docs/internals/builtins/misc/serialize.md index fc56f74e99..d5c10d953b 100644 --- a/docs/internals/builtins/misc/serialize.md +++ b/docs/internals/builtins/misc/serialize.md @@ -2,7 +2,7 @@ title: "serialize() — internals" description: "Compiler internals for serialize(): lowering path, type checks, and runtime helpers." sidebar: - order: 281 + order: 285 --- ## `serialize()` — internals @@ -38,6 +38,10 @@ function serialize(mixed $value): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `serialize()`](../../../php/builtins/misc/serialize.md) diff --git a/docs/internals/builtins/misc/unserialize.md b/docs/internals/builtins/misc/unserialize.md index c3b064a5fd..4022a76500 100644 --- a/docs/internals/builtins/misc/unserialize.md +++ b/docs/internals/builtins/misc/unserialize.md @@ -2,7 +2,7 @@ title: "unserialize() — internals" description: "Compiler internals for unserialize(): lowering path, type checks, and runtime helpers." sidebar: - order: 282 + order: 286 --- ## `unserialize()` — internals @@ -38,6 +38,10 @@ function unserialize(string $data, mixed $options = []): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `unserialize()`](../../../php/builtins/misc/unserialize.md) diff --git a/docs/internals/builtins/misc/unset.md b/docs/internals/builtins/misc/unset.md index afe1bc1aad..0f717d1af0 100644 --- a/docs/internals/builtins/misc/unset.md +++ b/docs/internals/builtins/misc/unset.md @@ -2,7 +2,7 @@ title: "unset() — internals" description: "Compiler internals for unset(): lowering path, type checks, and runtime helpers." sidebar: - order: 283 + order: 287 --- ## `unset()` — internals @@ -33,6 +33,12 @@ function unset(mixed $var, ...$vars): void - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$vars`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$vars`. + ## Cross-references - [User reference for `unset()`](../../../php/builtins/misc/unset.md) diff --git a/docs/internals/builtins/misc/var_dump.md b/docs/internals/builtins/misc/var_dump.md index 9e1a6d1f7b..51ce948568 100644 --- a/docs/internals/builtins/misc/var_dump.md +++ b/docs/internals/builtins/misc/var_dump.md @@ -2,7 +2,7 @@ title: "var_dump() — internals" description: "Compiler internals for var_dump(): lowering path, type checks, and runtime helpers." sidebar: - order: 284 + order: 288 --- ## `var_dump()` — internals @@ -34,6 +34,12 @@ function var_dump(mixed $value, ...$values): void - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `var_dump()`](../../../php/builtins/misc/var_dump.md) diff --git a/docs/internals/builtins/pointer/ptr.md b/docs/internals/builtins/pointer/ptr.md index c03606a05b..1b6ae6c09e 100644 --- a/docs/internals/builtins/pointer/ptr.md +++ b/docs/internals/builtins/pointer/ptr.md @@ -2,7 +2,7 @@ title: "ptr() — internals" description: "Compiler internals for ptr(): lowering path, type checks, and runtime helpers." sidebar: - order: 285 + order: 289 --- ## `ptr()` — internals @@ -32,6 +32,11 @@ function ptr(mixed $value): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr()`](../../../php/builtins/pointer/ptr.md) diff --git a/docs/internals/builtins/pointer/ptr_get.md b/docs/internals/builtins/pointer/ptr_get.md index 6727373272..524f0f1a0c 100644 --- a/docs/internals/builtins/pointer/ptr_get.md +++ b/docs/internals/builtins/pointer/ptr_get.md @@ -2,7 +2,7 @@ title: "ptr_get() — internals" description: "Compiler internals for ptr_get(): lowering path, type checks, and runtime helpers." sidebar: - order: 286 + order: 290 --- ## `ptr_get()` — internals @@ -32,6 +32,11 @@ function ptr_get(pointer $pointer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_get()`](../../../php/builtins/pointer/ptr_get.md) diff --git a/docs/internals/builtins/pointer/ptr_is_null.md b/docs/internals/builtins/pointer/ptr_is_null.md index 56954203d0..40c5d408ec 100644 --- a/docs/internals/builtins/pointer/ptr_is_null.md +++ b/docs/internals/builtins/pointer/ptr_is_null.md @@ -2,7 +2,7 @@ title: "ptr_is_null() — internals" description: "Compiler internals for ptr_is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 287 + order: 291 --- ## `ptr_is_null()` — internals @@ -32,6 +32,11 @@ function ptr_is_null(pointer $pointer): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_is_null()`](../../../php/builtins/pointer/ptr_is_null.md) diff --git a/docs/internals/builtins/pointer/ptr_null.md b/docs/internals/builtins/pointer/ptr_null.md index 40512628fb..32487821eb 100644 --- a/docs/internals/builtins/pointer/ptr_null.md +++ b/docs/internals/builtins/pointer/ptr_null.md @@ -2,7 +2,7 @@ title: "ptr_null() — internals" description: "Compiler internals for ptr_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 288 + order: 292 --- ## `ptr_null()` — internals @@ -32,6 +32,11 @@ function ptr_null(): mixed - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_null()`](../../../php/builtins/pointer/ptr_null.md) diff --git a/docs/internals/builtins/pointer/ptr_offset.md b/docs/internals/builtins/pointer/ptr_offset.md index c6b593077d..6c5bf3dad2 100644 --- a/docs/internals/builtins/pointer/ptr_offset.md +++ b/docs/internals/builtins/pointer/ptr_offset.md @@ -2,7 +2,7 @@ title: "ptr_offset() — internals" description: "Compiler internals for ptr_offset(): lowering path, type checks, and runtime helpers." sidebar: - order: 289 + order: 293 --- ## `ptr_offset()` — internals @@ -32,6 +32,11 @@ function ptr_offset(pointer $pointer, int $offset): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_offset()`](../../../php/builtins/pointer/ptr_offset.md) diff --git a/docs/internals/builtins/pointer/ptr_read16.md b/docs/internals/builtins/pointer/ptr_read16.md index bf576d19ea..a38f194c5b 100644 --- a/docs/internals/builtins/pointer/ptr_read16.md +++ b/docs/internals/builtins/pointer/ptr_read16.md @@ -2,7 +2,7 @@ title: "ptr_read16() — internals" description: "Compiler internals for ptr_read16(): lowering path, type checks, and runtime helpers." sidebar: - order: 290 + order: 294 --- ## `ptr_read16()` — internals @@ -33,6 +33,11 @@ function ptr_read16(pointer $pointer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_read16()`](../../../php/builtins/pointer/ptr_read16.md) diff --git a/docs/internals/builtins/pointer/ptr_read32.md b/docs/internals/builtins/pointer/ptr_read32.md index f23fa0ac9c..26aead378c 100644 --- a/docs/internals/builtins/pointer/ptr_read32.md +++ b/docs/internals/builtins/pointer/ptr_read32.md @@ -2,7 +2,7 @@ title: "ptr_read32() — internals" description: "Compiler internals for ptr_read32(): lowering path, type checks, and runtime helpers." sidebar: - order: 291 + order: 295 --- ## `ptr_read32()` — internals @@ -33,6 +33,11 @@ function ptr_read32(pointer $pointer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_read32()`](../../../php/builtins/pointer/ptr_read32.md) diff --git a/docs/internals/builtins/pointer/ptr_read8.md b/docs/internals/builtins/pointer/ptr_read8.md index 2583f5979e..ce1c57bcb1 100644 --- a/docs/internals/builtins/pointer/ptr_read8.md +++ b/docs/internals/builtins/pointer/ptr_read8.md @@ -2,7 +2,7 @@ title: "ptr_read8() — internals" description: "Compiler internals for ptr_read8(): lowering path, type checks, and runtime helpers." sidebar: - order: 292 + order: 296 --- ## `ptr_read8()` — internals @@ -32,6 +32,11 @@ function ptr_read8(pointer $pointer): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_read8()`](../../../php/builtins/pointer/ptr_read8.md) diff --git a/docs/internals/builtins/pointer/ptr_read_string.md b/docs/internals/builtins/pointer/ptr_read_string.md index e0224b38d9..82f33512ed 100644 --- a/docs/internals/builtins/pointer/ptr_read_string.md +++ b/docs/internals/builtins/pointer/ptr_read_string.md @@ -2,7 +2,7 @@ title: "ptr_read_string() — internals" description: "Compiler internals for ptr_read_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 293 + order: 297 --- ## `ptr_read_string()` — internals @@ -33,6 +33,11 @@ function ptr_read_string(pointer $pointer, int $length): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_read_string()`](../../../php/builtins/pointer/ptr_read_string.md) diff --git a/docs/internals/builtins/pointer/ptr_set.md b/docs/internals/builtins/pointer/ptr_set.md index eff6bc0cfe..186aaf5267 100644 --- a/docs/internals/builtins/pointer/ptr_set.md +++ b/docs/internals/builtins/pointer/ptr_set.md @@ -2,7 +2,7 @@ title: "ptr_set() — internals" description: "Compiler internals for ptr_set(): lowering path, type checks, and runtime helpers." sidebar: - order: 294 + order: 298 --- ## `ptr_set()` — internals @@ -32,6 +32,11 @@ function ptr_set(pointer $pointer, mixed $value): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_set()`](../../../php/builtins/pointer/ptr_set.md) diff --git a/docs/internals/builtins/pointer/ptr_sizeof.md b/docs/internals/builtins/pointer/ptr_sizeof.md index c7e326e8be..d0616caf8f 100644 --- a/docs/internals/builtins/pointer/ptr_sizeof.md +++ b/docs/internals/builtins/pointer/ptr_sizeof.md @@ -2,7 +2,7 @@ title: "ptr_sizeof() — internals" description: "Compiler internals for ptr_sizeof(): lowering path, type checks, and runtime helpers." sidebar: - order: 295 + order: 299 --- ## `ptr_sizeof()` — internals @@ -32,6 +32,11 @@ function ptr_sizeof(string $type): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_sizeof()`](../../../php/builtins/pointer/ptr_sizeof.md) diff --git a/docs/internals/builtins/pointer/ptr_write16.md b/docs/internals/builtins/pointer/ptr_write16.md index 0c633f322c..4ab6dcd112 100644 --- a/docs/internals/builtins/pointer/ptr_write16.md +++ b/docs/internals/builtins/pointer/ptr_write16.md @@ -2,7 +2,7 @@ title: "ptr_write16() — internals" description: "Compiler internals for ptr_write16(): lowering path, type checks, and runtime helpers." sidebar: - order: 296 + order: 300 --- ## `ptr_write16()` — internals @@ -33,6 +33,11 @@ function ptr_write16(pointer $pointer, int $value): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_write16()`](../../../php/builtins/pointer/ptr_write16.md) diff --git a/docs/internals/builtins/pointer/ptr_write32.md b/docs/internals/builtins/pointer/ptr_write32.md index 0304a4ffc9..7a3ac6eb84 100644 --- a/docs/internals/builtins/pointer/ptr_write32.md +++ b/docs/internals/builtins/pointer/ptr_write32.md @@ -2,7 +2,7 @@ title: "ptr_write32() — internals" description: "Compiler internals for ptr_write32(): lowering path, type checks, and runtime helpers." sidebar: - order: 297 + order: 301 --- ## `ptr_write32()` — internals @@ -33,6 +33,11 @@ function ptr_write32(pointer $pointer, int $value): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_write32()`](../../../php/builtins/pointer/ptr_write32.md) diff --git a/docs/internals/builtins/pointer/ptr_write8.md b/docs/internals/builtins/pointer/ptr_write8.md index 9b45cf5cfb..0c285cff20 100644 --- a/docs/internals/builtins/pointer/ptr_write8.md +++ b/docs/internals/builtins/pointer/ptr_write8.md @@ -2,7 +2,7 @@ title: "ptr_write8() — internals" description: "Compiler internals for ptr_write8(): lowering path, type checks, and runtime helpers." sidebar: - order: 298 + order: 302 --- ## `ptr_write8()` — internals @@ -32,6 +32,11 @@ function ptr_write8(pointer $pointer, int $value): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_write8()`](../../../php/builtins/pointer/ptr_write8.md) diff --git a/docs/internals/builtins/pointer/ptr_write_string.md b/docs/internals/builtins/pointer/ptr_write_string.md index 649930057a..1d8277df2a 100644 --- a/docs/internals/builtins/pointer/ptr_write_string.md +++ b/docs/internals/builtins/pointer/ptr_write_string.md @@ -2,7 +2,7 @@ title: "ptr_write_string() — internals" description: "Compiler internals for ptr_write_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 299 + order: 303 --- ## `ptr_write_string()` — internals @@ -33,6 +33,11 @@ function ptr_write_string(pointer $pointer, string $string): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ptr_write_string()`](../../../php/builtins/pointer/ptr_write_string.md) diff --git a/docs/internals/builtins/pointer/zval_free.md b/docs/internals/builtins/pointer/zval_free.md index 0a75804ce0..ffe045d625 100644 --- a/docs/internals/builtins/pointer/zval_free.md +++ b/docs/internals/builtins/pointer/zval_free.md @@ -2,7 +2,7 @@ title: "zval_free() — internals" description: "Compiler internals for zval_free(): lowering path, type checks, and runtime helpers." sidebar: - order: 300 + order: 304 --- ## `zval_free()` — internals @@ -33,6 +33,10 @@ function zval_free(pointer $zval): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `zval_free()`](../../../php/builtins/pointer/zval_free.md) diff --git a/docs/internals/builtins/pointer/zval_pack.md b/docs/internals/builtins/pointer/zval_pack.md index 172ec6a16e..e903a83d31 100644 --- a/docs/internals/builtins/pointer/zval_pack.md +++ b/docs/internals/builtins/pointer/zval_pack.md @@ -2,7 +2,7 @@ title: "zval_pack() — internals" description: "Compiler internals for zval_pack(): lowering path, type checks, and runtime helpers." sidebar: - order: 301 + order: 305 --- ## `zval_pack()` — internals @@ -42,6 +42,10 @@ function zval_pack(mixed $value): pointer - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `zval_pack()`](../../../php/builtins/pointer/zval_pack.md) diff --git a/docs/internals/builtins/pointer/zval_type.md b/docs/internals/builtins/pointer/zval_type.md index 36633b0d95..2dedc2723e 100644 --- a/docs/internals/builtins/pointer/zval_type.md +++ b/docs/internals/builtins/pointer/zval_type.md @@ -2,7 +2,7 @@ title: "zval_type() — internals" description: "Compiler internals for zval_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 302 + order: 306 --- ## `zval_type()` — internals @@ -34,6 +34,10 @@ function zval_type(pointer $zval): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `zval_type()`](../../../php/builtins/pointer/zval_type.md) diff --git a/docs/internals/builtins/pointer/zval_unpack.md b/docs/internals/builtins/pointer/zval_unpack.md index 35dcefc48a..867ef4b5a3 100644 --- a/docs/internals/builtins/pointer/zval_unpack.md +++ b/docs/internals/builtins/pointer/zval_unpack.md @@ -2,7 +2,7 @@ title: "zval_unpack() — internals" description: "Compiler internals for zval_unpack(): lowering path, type checks, and runtime helpers." sidebar: - order: 303 + order: 307 --- ## `zval_unpack()` — internals @@ -35,6 +35,10 @@ function zval_unpack(pointer $zval): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +_Not callable from eval'd code — the magician interpreter has no entry for this builtin._ + ## Cross-references - [User reference for `zval_unpack()`](../../../php/builtins/pointer/zval_unpack.md) diff --git a/docs/internals/builtins/process/die.md b/docs/internals/builtins/process/die.md index 11ae6e98db..1d3abf2306 100644 --- a/docs/internals/builtins/process/die.md +++ b/docs/internals/builtins/process/die.md @@ -2,7 +2,7 @@ title: "die() — internals" description: "Compiler internals for die(): lowering path, type checks, and runtime helpers." sidebar: - order: 304 + order: 308 --- ## `die()` — internals @@ -28,6 +28,11 @@ function die(int $status): void - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/die.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/die.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `die()`](../../../php/builtins/process/die.md) diff --git a/docs/internals/builtins/process/exec.md b/docs/internals/builtins/process/exec.md index 94ef06ac80..324570a9a2 100644 --- a/docs/internals/builtins/process/exec.md +++ b/docs/internals/builtins/process/exec.md @@ -2,7 +2,7 @@ title: "exec() — internals" description: "Compiler internals for exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 305 + order: 309 --- ## `exec()` — internals @@ -32,6 +32,11 @@ function exec(string $command): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `exec()`](../../../php/builtins/process/exec.md) diff --git a/docs/internals/builtins/process/exit.md b/docs/internals/builtins/process/exit.md index dbfcf2c1f1..e837336792 100644 --- a/docs/internals/builtins/process/exit.md +++ b/docs/internals/builtins/process/exit.md @@ -2,7 +2,7 @@ title: "exit() — internals" description: "Compiler internals for exit(): lowering path, type checks, and runtime helpers." sidebar: - order: 306 + order: 310 --- ## `exit()` — internals @@ -28,6 +28,11 @@ function exit(int $status): void - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/core/exit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/exit.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `exit()`](../../../php/builtins/process/exit.md) diff --git a/docs/internals/builtins/process/passthru.md b/docs/internals/builtins/process/passthru.md index 23eec13986..ccb9755993 100644 --- a/docs/internals/builtins/process/passthru.md +++ b/docs/internals/builtins/process/passthru.md @@ -2,7 +2,7 @@ title: "passthru() — internals" description: "Compiler internals for passthru(): lowering path, type checks, and runtime helpers." sidebar: - order: 307 + order: 311 --- ## `passthru()` — internals @@ -34,6 +34,11 @@ function passthru(string $command): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `passthru()`](../../../php/builtins/process/passthru.md) diff --git a/docs/internals/builtins/process/pclose.md b/docs/internals/builtins/process/pclose.md index b05e466ea7..1be2c744ca 100644 --- a/docs/internals/builtins/process/pclose.md +++ b/docs/internals/builtins/process/pclose.md @@ -2,7 +2,7 @@ title: "pclose() — internals" description: "Compiler internals for pclose(): lowering path, type checks, and runtime helpers." sidebar: - order: 308 + order: 312 --- ## `pclose()` — internals @@ -33,6 +33,11 @@ function pclose(resource $handle): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `pclose()`](../../../php/builtins/process/pclose.md) diff --git a/docs/internals/builtins/process/popen.md b/docs/internals/builtins/process/popen.md index 326cd40143..045c12c63a 100644 --- a/docs/internals/builtins/process/popen.md +++ b/docs/internals/builtins/process/popen.md @@ -2,7 +2,7 @@ title: "popen() — internals" description: "Compiler internals for popen(): lowering path, type checks, and runtime helpers." sidebar: - order: 309 + order: 313 --- ## `popen()` — internals @@ -33,6 +33,11 @@ function popen(string $command, string $mode): mixed - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `popen()`](../../../php/builtins/process/popen.md) diff --git a/docs/internals/builtins/process/readline.md b/docs/internals/builtins/process/readline.md index e9312ff268..f7b04ba471 100644 --- a/docs/internals/builtins/process/readline.md +++ b/docs/internals/builtins/process/readline.md @@ -2,7 +2,7 @@ title: "readline() — internals" description: "Compiler internals for readline(): lowering path, type checks, and runtime helpers." sidebar: - order: 310 + order: 314 --- ## `readline()` — internals @@ -33,6 +33,11 @@ function readline(string $prompt = null): mixed - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `readline()`](../../../php/builtins/process/readline.md) diff --git a/docs/internals/builtins/process/shell_exec.md b/docs/internals/builtins/process/shell_exec.md index b1453d629b..3aa70c2307 100644 --- a/docs/internals/builtins/process/shell_exec.md +++ b/docs/internals/builtins/process/shell_exec.md @@ -2,7 +2,7 @@ title: "shell_exec() — internals" description: "Compiler internals for shell_exec(): lowering path, type checks, and runtime helpers." sidebar: - order: 311 + order: 315 --- ## `shell_exec()` — internals @@ -32,6 +32,11 @@ function shell_exec(string $command): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `shell_exec()`](../../../php/builtins/process/shell_exec.md) diff --git a/docs/internals/builtins/process/sleep.md b/docs/internals/builtins/process/sleep.md index b3db7c1251..f1663e5541 100644 --- a/docs/internals/builtins/process/sleep.md +++ b/docs/internals/builtins/process/sleep.md @@ -2,7 +2,7 @@ title: "sleep() — internals" description: "Compiler internals for sleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 312 + order: 316 --- ## `sleep()` — internals @@ -33,6 +33,11 @@ function sleep(int $seconds): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/sleep.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sleep()`](../../../php/builtins/process/sleep.md) diff --git a/docs/internals/builtins/process/system.md b/docs/internals/builtins/process/system.md index 21a4512f69..aafbdcb26b 100644 --- a/docs/internals/builtins/process/system.md +++ b/docs/internals/builtins/process/system.md @@ -2,7 +2,7 @@ title: "system() — internals" description: "Compiler internals for system(): lowering path, type checks, and runtime helpers." sidebar: - order: 313 + order: 317 --- ## `system()` — internals @@ -33,6 +33,11 @@ function system(string $command): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/system.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `system()`](../../../php/builtins/process/system.md) diff --git a/docs/internals/builtins/process/usleep.md b/docs/internals/builtins/process/usleep.md index 7f8d0973f3..916eb82a94 100644 --- a/docs/internals/builtins/process/usleep.md +++ b/docs/internals/builtins/process/usleep.md @@ -2,7 +2,7 @@ title: "usleep() — internals" description: "Compiler internals for usleep(): lowering path, type checks, and runtime helpers." sidebar: - order: 314 + order: 318 --- ## `usleep()` — internals @@ -33,6 +33,11 @@ function usleep(int $microseconds): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/time/usleep.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `usleep()`](../../../php/builtins/process/usleep.md) diff --git a/docs/internals/builtins/regex/mb_ereg_match.md b/docs/internals/builtins/regex/mb_ereg_match.md index 8f04e325ff..1da091a972 100644 --- a/docs/internals/builtins/regex/mb_ereg_match.md +++ b/docs/internals/builtins/regex/mb_ereg_match.md @@ -2,7 +2,7 @@ title: "mb_ereg_match() — internals" description: "Compiler internals for mb_ereg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 315 + order: 319 --- ## `mb_ereg_match()` — internals @@ -35,6 +35,11 @@ function mb_ereg_match(string $pattern, string $subject, string $options = null) - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `mb_ereg_match()`](../../../php/builtins/regex/mb_ereg_match.md) diff --git a/docs/internals/builtins/regex/preg_match.md b/docs/internals/builtins/regex/preg_match.md index a2fdc18aee..565ee1c920 100644 --- a/docs/internals/builtins/regex/preg_match.md +++ b/docs/internals/builtins/regex/preg_match.md @@ -2,7 +2,7 @@ title: "preg_match() — internals" description: "Compiler internals for preg_match(): lowering path, type checks, and runtime helpers." sidebar: - order: 316 + order: 320 --- ## `preg_match()` — internals @@ -35,6 +35,12 @@ function preg_match(string $pattern, string $subject, array $matches = []): int - **Arity**: takes 2–3 arguments (1 optional). - **By-reference parameters**: `$matches`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **By-reference parameters**: `$matches`. + ## Cross-references - [User reference for `preg_match()`](../../../php/builtins/regex/preg_match.md) diff --git a/docs/internals/builtins/regex/preg_match_all.md b/docs/internals/builtins/regex/preg_match_all.md index d04fad507c..a59224a011 100644 --- a/docs/internals/builtins/regex/preg_match_all.md +++ b/docs/internals/builtins/regex/preg_match_all.md @@ -2,7 +2,7 @@ title: "preg_match_all() — internals" description: "Compiler internals for preg_match_all(): lowering path, type checks, and runtime helpers." sidebar: - order: 317 + order: 321 --- ## `preg_match_all()` — internals @@ -34,6 +34,12 @@ function preg_match_all(string $pattern, string $subject): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **By-reference parameters**: `$matches`. + ## Cross-references - [User reference for `preg_match_all()`](../../../php/builtins/regex/preg_match_all.md) diff --git a/docs/internals/builtins/regex/preg_replace.md b/docs/internals/builtins/regex/preg_replace.md index 95a4078ed0..58e9d1f106 100644 --- a/docs/internals/builtins/regex/preg_replace.md +++ b/docs/internals/builtins/regex/preg_replace.md @@ -2,7 +2,7 @@ title: "preg_replace() — internals" description: "Compiler internals for preg_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 318 + order: 322 --- ## `preg_replace()` — internals @@ -33,6 +33,11 @@ function preg_replace(string $pattern, string $replacement, string $subject): st - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `preg_replace()`](../../../php/builtins/regex/preg_replace.md) diff --git a/docs/internals/builtins/regex/preg_replace_callback.md b/docs/internals/builtins/regex/preg_replace_callback.md index f961174aa0..f14ba6919f 100644 --- a/docs/internals/builtins/regex/preg_replace_callback.md +++ b/docs/internals/builtins/regex/preg_replace_callback.md @@ -2,7 +2,7 @@ title: "preg_replace_callback() — internals" description: "Compiler internals for preg_replace_callback(): lowering path, type checks, and runtime helpers." sidebar: - order: 319 + order: 323 --- ## `preg_replace_callback()` — internals @@ -33,6 +33,11 @@ function preg_replace_callback(string $pattern, callable $callback, string $subj - **Arity**: takes exactly 3 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `preg_replace_callback()`](../../../php/builtins/regex/preg_replace_callback.md) diff --git a/docs/internals/builtins/regex/preg_split.md b/docs/internals/builtins/regex/preg_split.md index 50f6002e95..500812b224 100644 --- a/docs/internals/builtins/regex/preg_split.md +++ b/docs/internals/builtins/regex/preg_split.md @@ -2,7 +2,7 @@ title: "preg_split() — internals" description: "Compiler internals for preg_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 320 + order: 324 --- ## `preg_split()` — internals @@ -33,6 +33,11 @@ function preg_split(string $pattern, string $subject, int $limit = -1, int $flag - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `preg_split()`](../../../php/builtins/regex/preg_split.md) diff --git a/docs/internals/builtins/spl/iterator_apply.md b/docs/internals/builtins/spl/iterator_apply.md index 289cb98666..1c78f81696 100644 --- a/docs/internals/builtins/spl/iterator_apply.md +++ b/docs/internals/builtins/spl/iterator_apply.md @@ -2,7 +2,7 @@ title: "iterator_apply() — internals" description: "Compiler internals for iterator_apply(): lowering path, type checks, and runtime helpers." sidebar: - order: 321 + order: 325 --- ## `iterator_apply()` — internals @@ -32,6 +32,11 @@ function iterator_apply(traversable $iterator, callable $callback, array $args = - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `iterator_apply()`](../../../php/builtins/spl/iterator_apply.md) diff --git a/docs/internals/builtins/spl/iterator_count.md b/docs/internals/builtins/spl/iterator_count.md index 40f8558a8e..d61ef84c0d 100644 --- a/docs/internals/builtins/spl/iterator_count.md +++ b/docs/internals/builtins/spl/iterator_count.md @@ -2,7 +2,7 @@ title: "iterator_count() — internals" description: "Compiler internals for iterator_count(): lowering path, type checks, and runtime helpers." sidebar: - order: 322 + order: 326 --- ## `iterator_count()` — internals @@ -32,6 +32,11 @@ function iterator_count(traversable $iterator): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `iterator_count()`](../../../php/builtins/spl/iterator_count.md) diff --git a/docs/internals/builtins/spl/iterator_to_array.md b/docs/internals/builtins/spl/iterator_to_array.md index 59ed277019..ab3f1210e2 100644 --- a/docs/internals/builtins/spl/iterator_to_array.md +++ b/docs/internals/builtins/spl/iterator_to_array.md @@ -2,7 +2,7 @@ title: "iterator_to_array() — internals" description: "Compiler internals for iterator_to_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 323 + order: 327 --- ## `iterator_to_array()` — internals @@ -32,6 +32,11 @@ function iterator_to_array(traversable $iterator, bool $preserve_keys = true): a - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `iterator_to_array()`](../../../php/builtins/spl/iterator_to_array.md) diff --git a/docs/internals/builtins/spl/spl_autoload.md b/docs/internals/builtins/spl/spl_autoload.md index db56f3faf3..4350187c46 100644 --- a/docs/internals/builtins/spl/spl_autoload.md +++ b/docs/internals/builtins/spl/spl_autoload.md @@ -2,7 +2,7 @@ title: "spl_autoload() — internals" description: "Compiler internals for spl_autoload(): lowering path, type checks, and runtime helpers." sidebar: - order: 324 + order: 328 --- ## `spl_autoload()` — internals @@ -32,6 +32,11 @@ function spl_autoload(string $class, string $file_extensions = null): void - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload()`](../../../php/builtins/spl/spl_autoload.md) diff --git a/docs/internals/builtins/spl/spl_autoload_call.md b/docs/internals/builtins/spl/spl_autoload_call.md index 5151fdd170..61eead1dd0 100644 --- a/docs/internals/builtins/spl/spl_autoload_call.md +++ b/docs/internals/builtins/spl/spl_autoload_call.md @@ -2,7 +2,7 @@ title: "spl_autoload_call() — internals" description: "Compiler internals for spl_autoload_call(): lowering path, type checks, and runtime helpers." sidebar: - order: 325 + order: 329 --- ## `spl_autoload_call()` — internals @@ -32,6 +32,11 @@ function spl_autoload_call(string $class): void - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_call()`](../../../php/builtins/spl/spl_autoload_call.md) diff --git a/docs/internals/builtins/spl/spl_autoload_extensions.md b/docs/internals/builtins/spl/spl_autoload_extensions.md index 82aba526c5..ca4ad5b48c 100644 --- a/docs/internals/builtins/spl/spl_autoload_extensions.md +++ b/docs/internals/builtins/spl/spl_autoload_extensions.md @@ -2,7 +2,7 @@ title: "spl_autoload_extensions() — internals" description: "Compiler internals for spl_autoload_extensions(): lowering path, type checks, and runtime helpers." sidebar: - order: 326 + order: 330 --- ## `spl_autoload_extensions()` — internals @@ -32,6 +32,11 @@ function spl_autoload_extensions(string $file_extensions = null): string - **Arity**: takes 0–1 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_extensions()`](../../../php/builtins/spl/spl_autoload_extensions.md) diff --git a/docs/internals/builtins/spl/spl_autoload_functions.md b/docs/internals/builtins/spl/spl_autoload_functions.md index 64801b7387..6b051b651b 100644 --- a/docs/internals/builtins/spl/spl_autoload_functions.md +++ b/docs/internals/builtins/spl/spl_autoload_functions.md @@ -2,7 +2,7 @@ title: "spl_autoload_functions() — internals" description: "Compiler internals for spl_autoload_functions(): lowering path, type checks, and runtime helpers." sidebar: - order: 327 + order: 331 --- ## `spl_autoload_functions()` — internals @@ -32,6 +32,11 @@ function spl_autoload_functions(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_functions()`](../../../php/builtins/spl/spl_autoload_functions.md) diff --git a/docs/internals/builtins/spl/spl_autoload_register.md b/docs/internals/builtins/spl/spl_autoload_register.md index b0d2fdee95..45e28c4dfd 100644 --- a/docs/internals/builtins/spl/spl_autoload_register.md +++ b/docs/internals/builtins/spl/spl_autoload_register.md @@ -2,7 +2,7 @@ title: "spl_autoload_register() — internals" description: "Compiler internals for spl_autoload_register(): lowering path, type checks, and runtime helpers." sidebar: - order: 328 + order: 332 --- ## `spl_autoload_register()` — internals @@ -32,6 +32,11 @@ function spl_autoload_register(callable $callback = null, bool $throw = true, bo - **Arity**: takes 0–3 arguments (3 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_register()`](../../../php/builtins/spl/spl_autoload_register.md) diff --git a/docs/internals/builtins/spl/spl_autoload_unregister.md b/docs/internals/builtins/spl/spl_autoload_unregister.md index 6c359de4b0..615c32f85c 100644 --- a/docs/internals/builtins/spl/spl_autoload_unregister.md +++ b/docs/internals/builtins/spl/spl_autoload_unregister.md @@ -2,7 +2,7 @@ title: "spl_autoload_unregister() — internals" description: "Compiler internals for spl_autoload_unregister(): lowering path, type checks, and runtime helpers." sidebar: - order: 329 + order: 333 --- ## `spl_autoload_unregister()` — internals @@ -32,6 +32,11 @@ function spl_autoload_unregister(callable $callback): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_autoload_unregister()`](../../../php/builtins/spl/spl_autoload_unregister.md) diff --git a/docs/internals/builtins/spl/spl_classes.md b/docs/internals/builtins/spl/spl_classes.md index 698624bcbd..625e7d2ade 100644 --- a/docs/internals/builtins/spl/spl_classes.md +++ b/docs/internals/builtins/spl/spl_classes.md @@ -2,7 +2,7 @@ title: "spl_classes() — internals" description: "Compiler internals for spl_classes(): lowering path, type checks, and runtime helpers." sidebar: - order: 330 + order: 334 --- ## `spl_classes()` — internals @@ -33,6 +33,11 @@ function spl_classes(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_classes()`](../../../php/builtins/spl/spl_classes.md) diff --git a/docs/internals/builtins/spl/spl_object_hash.md b/docs/internals/builtins/spl/spl_object_hash.md index 63ffcd5bc2..d086f1789c 100644 --- a/docs/internals/builtins/spl/spl_object_hash.md +++ b/docs/internals/builtins/spl/spl_object_hash.md @@ -2,7 +2,7 @@ title: "spl_object_hash() — internals" description: "Compiler internals for spl_object_hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 331 + order: 335 --- ## `spl_object_hash()` — internals @@ -33,6 +33,11 @@ function spl_object_hash(object $object): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_object_hash()`](../../../php/builtins/spl/spl_object_hash.md) diff --git a/docs/internals/builtins/spl/spl_object_id.md b/docs/internals/builtins/spl/spl_object_id.md index 45e8512cdc..377c1ee60f 100644 --- a/docs/internals/builtins/spl/spl_object_id.md +++ b/docs/internals/builtins/spl/spl_object_id.md @@ -2,7 +2,7 @@ title: "spl_object_id() — internals" description: "Compiler internals for spl_object_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 332 + order: 336 --- ## `spl_object_id()` — internals @@ -33,6 +33,11 @@ function spl_object_id(object $object): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `spl_object_id()`](../../../php/builtins/spl/spl_object_id.md) diff --git a/docs/internals/builtins/streams/fsockopen.md b/docs/internals/builtins/streams/fsockopen.md index 292ccf02fa..ff1aa20e27 100644 --- a/docs/internals/builtins/streams/fsockopen.md +++ b/docs/internals/builtins/streams/fsockopen.md @@ -2,7 +2,7 @@ title: "fsockopen() — internals" description: "Compiler internals for fsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 333 + order: 337 --- ## `fsockopen()` — internals @@ -33,6 +33,12 @@ function fsockopen(string $hostname, int $port, int $error_code = null, string $ - **Arity**: takes 2–5 arguments (3 optional). - **By-reference parameters**: `$error_code`, `$error_message`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$error_code`, `$error_message`. + ## Cross-references - [User reference for `fsockopen()`](../../../php/builtins/streams/fsockopen.md) diff --git a/docs/internals/builtins/streams/pfsockopen.md b/docs/internals/builtins/streams/pfsockopen.md index a834e18d36..22d5e53db5 100644 --- a/docs/internals/builtins/streams/pfsockopen.md +++ b/docs/internals/builtins/streams/pfsockopen.md @@ -2,7 +2,7 @@ title: "pfsockopen() — internals" description: "Compiler internals for pfsockopen(): lowering path, type checks, and runtime helpers." sidebar: - order: 334 + order: 338 --- ## `pfsockopen()` — internals @@ -33,6 +33,12 @@ function pfsockopen(string $hostname, int $port, int $error_code = null, string - **Arity**: takes 2–5 arguments (3 optional). - **By-reference parameters**: `$error_code`, `$error_message`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$error_code`, `$error_message`. + ## Cross-references - [User reference for `pfsockopen()`](../../../php/builtins/streams/pfsockopen.md) diff --git a/docs/internals/builtins/streams/stream_bucket_append.md b/docs/internals/builtins/streams/stream_bucket_append.md index 3035499463..dc670221d4 100644 --- a/docs/internals/builtins/streams/stream_bucket_append.md +++ b/docs/internals/builtins/streams/stream_bucket_append.md @@ -2,7 +2,7 @@ title: "stream_bucket_append() — internals" description: "Compiler internals for stream_bucket_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 335 + order: 339 --- ## `stream_bucket_append()` — internals @@ -32,6 +32,11 @@ function stream_bucket_append(mixed $brigade, mixed $bucket): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_bucket_append()`](../../../php/builtins/streams/stream_bucket_append.md) diff --git a/docs/internals/builtins/streams/stream_bucket_prepend.md b/docs/internals/builtins/streams/stream_bucket_prepend.md index baba2e47b7..11daabb523 100644 --- a/docs/internals/builtins/streams/stream_bucket_prepend.md +++ b/docs/internals/builtins/streams/stream_bucket_prepend.md @@ -2,7 +2,7 @@ title: "stream_bucket_prepend() — internals" description: "Compiler internals for stream_bucket_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 336 + order: 340 --- ## `stream_bucket_prepend()` — internals @@ -32,6 +32,11 @@ function stream_bucket_prepend(mixed $brigade, mixed $bucket): void - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_bucket_prepend()`](../../../php/builtins/streams/stream_bucket_prepend.md) diff --git a/docs/internals/builtins/streams/stream_filter_append.md b/docs/internals/builtins/streams/stream_filter_append.md index ddfcd5a2fe..2aaf87e35a 100644 --- a/docs/internals/builtins/streams/stream_filter_append.md +++ b/docs/internals/builtins/streams/stream_filter_append.md @@ -2,7 +2,7 @@ title: "stream_filter_append() — internals" description: "Compiler internals for stream_filter_append(): lowering path, type checks, and runtime helpers." sidebar: - order: 337 + order: 341 --- ## `stream_filter_append()` — internals @@ -32,6 +32,11 @@ function stream_filter_append(resource $stream, string $filtername, int $read_wr - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_filter_append()`](../../../php/builtins/streams/stream_filter_append.md) diff --git a/docs/internals/builtins/streams/stream_filter_prepend.md b/docs/internals/builtins/streams/stream_filter_prepend.md index 142ff7f65d..d9c6754932 100644 --- a/docs/internals/builtins/streams/stream_filter_prepend.md +++ b/docs/internals/builtins/streams/stream_filter_prepend.md @@ -2,7 +2,7 @@ title: "stream_filter_prepend() — internals" description: "Compiler internals for stream_filter_prepend(): lowering path, type checks, and runtime helpers." sidebar: - order: 338 + order: 342 --- ## `stream_filter_prepend()` — internals @@ -32,6 +32,11 @@ function stream_filter_prepend(resource $stream, string $filtername, int $read_w - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stream_filter_prepend()`](../../../php/builtins/streams/stream_filter_prepend.md) diff --git a/docs/internals/builtins/string/addslashes.md b/docs/internals/builtins/string/addslashes.md index 43bdfd806f..d3d4c065ed 100644 --- a/docs/internals/builtins/string/addslashes.md +++ b/docs/internals/builtins/string/addslashes.md @@ -2,7 +2,7 @@ title: "addslashes() — internals" description: "Compiler internals for addslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 339 + order: 343 --- ## `addslashes()` — internals @@ -33,6 +33,11 @@ function addslashes(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `addslashes()`](../../../php/builtins/string/addslashes.md) diff --git a/docs/internals/builtins/string/base64_decode.md b/docs/internals/builtins/string/base64_decode.md index cf5ef15416..ea182c37d1 100644 --- a/docs/internals/builtins/string/base64_decode.md +++ b/docs/internals/builtins/string/base64_decode.md @@ -2,7 +2,7 @@ title: "base64_decode() — internals" description: "Compiler internals for base64_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 340 + order: 344 --- ## `base64_decode()` — internals @@ -33,6 +33,11 @@ function base64_decode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `base64_decode()`](../../../php/builtins/string/base64_decode.md) diff --git a/docs/internals/builtins/string/base64_encode.md b/docs/internals/builtins/string/base64_encode.md index 6592f02674..f013aefce3 100644 --- a/docs/internals/builtins/string/base64_encode.md +++ b/docs/internals/builtins/string/base64_encode.md @@ -2,7 +2,7 @@ title: "base64_encode() — internals" description: "Compiler internals for base64_encode(): lowering path, type checks, and runtime helpers." sidebar: - order: 341 + order: 345 --- ## `base64_encode()` — internals @@ -33,6 +33,11 @@ function base64_encode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `base64_encode()`](../../../php/builtins/string/base64_encode.md) diff --git a/docs/internals/builtins/string/bin2hex.md b/docs/internals/builtins/string/bin2hex.md index f2f72cd182..bece013c4a 100644 --- a/docs/internals/builtins/string/bin2hex.md +++ b/docs/internals/builtins/string/bin2hex.md @@ -2,7 +2,7 @@ title: "bin2hex() — internals" description: "Compiler internals for bin2hex(): lowering path, type checks, and runtime helpers." sidebar: - order: 342 + order: 346 --- ## `bin2hex()` — internals @@ -33,6 +33,11 @@ function bin2hex(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `bin2hex()`](../../../php/builtins/string/bin2hex.md) diff --git a/docs/internals/builtins/string/chop.md b/docs/internals/builtins/string/chop.md index 93247bbe08..3f797a19b2 100644 --- a/docs/internals/builtins/string/chop.md +++ b/docs/internals/builtins/string/chop.md @@ -2,7 +2,7 @@ title: "chop() — internals" description: "Compiler internals for chop(): lowering path, type checks, and runtime helpers." sidebar: - order: 343 + order: 347 --- ## `chop()` — internals @@ -32,6 +32,11 @@ function chop(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): strin - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/chop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chop.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chop()`](../../../php/builtins/string/chop.md) diff --git a/docs/internals/builtins/string/chr.md b/docs/internals/builtins/string/chr.md index f639686c1d..818ac4cf7d 100644 --- a/docs/internals/builtins/string/chr.md +++ b/docs/internals/builtins/string/chr.md @@ -2,7 +2,7 @@ title: "chr() — internals" description: "Compiler internals for chr(): lowering path, type checks, and runtime helpers." sidebar: - order: 344 + order: 348 --- ## `chr()` — internals @@ -33,6 +33,11 @@ function chr(int $codepoint): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `chr()`](../../../php/builtins/string/chr.md) diff --git a/docs/internals/builtins/string/crc32.md b/docs/internals/builtins/string/crc32.md index ea36ecf1bf..750fd11c00 100644 --- a/docs/internals/builtins/string/crc32.md +++ b/docs/internals/builtins/string/crc32.md @@ -2,7 +2,7 @@ title: "crc32() — internals" description: "Compiler internals for crc32(): lowering path, type checks, and runtime helpers." sidebar: - order: 345 + order: 349 --- ## `crc32()` — internals @@ -36,6 +36,11 @@ function crc32(string $string): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/crc32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `crc32()`](../../../php/builtins/string/crc32.md) diff --git a/docs/internals/builtins/string/explode.md b/docs/internals/builtins/string/explode.md index c5ea4b6468..a2427c6c00 100644 --- a/docs/internals/builtins/string/explode.md +++ b/docs/internals/builtins/string/explode.md @@ -2,7 +2,7 @@ title: "explode() — internals" description: "Compiler internals for explode(): lowering path, type checks, and runtime helpers." sidebar: - order: 346 + order: 350 --- ## `explode()` — internals @@ -34,6 +34,11 @@ function explode(string $separator, string $string, int $limit = PHP_INT_MAX): a - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/explode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/explode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `explode()`](../../../php/builtins/string/explode.md) diff --git a/docs/internals/builtins/string/grapheme_strrev.md b/docs/internals/builtins/string/grapheme_strrev.md index 65d236b7bd..0a96b1f829 100644 --- a/docs/internals/builtins/string/grapheme_strrev.md +++ b/docs/internals/builtins/string/grapheme_strrev.md @@ -2,7 +2,7 @@ title: "grapheme_strrev() — internals" description: "Compiler internals for grapheme_strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 347 + order: 351 --- ## `grapheme_strrev()` — internals @@ -34,6 +34,11 @@ function grapheme_strrev(string $string): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `grapheme_strrev()`](../../../php/builtins/string/grapheme_strrev.md) diff --git a/docs/internals/builtins/string/gzcompress.md b/docs/internals/builtins/string/gzcompress.md index a4c9f8033f..22cb6394ac 100644 --- a/docs/internals/builtins/string/gzcompress.md +++ b/docs/internals/builtins/string/gzcompress.md @@ -2,7 +2,7 @@ title: "gzcompress() — internals" description: "Compiler internals for gzcompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 348 + order: 352 --- ## `gzcompress()` — internals @@ -32,6 +32,11 @@ function gzcompress(string $data, int $level = -1): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gzcompress()`](../../../php/builtins/string/gzcompress.md) diff --git a/docs/internals/builtins/string/gzdeflate.md b/docs/internals/builtins/string/gzdeflate.md index ddaacdfbcb..adaad84833 100644 --- a/docs/internals/builtins/string/gzdeflate.md +++ b/docs/internals/builtins/string/gzdeflate.md @@ -2,7 +2,7 @@ title: "gzdeflate() — internals" description: "Compiler internals for gzdeflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 349 + order: 353 --- ## `gzdeflate()` — internals @@ -32,6 +32,11 @@ function gzdeflate(string $data, int $level = -1): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gzdeflate()`](../../../php/builtins/string/gzdeflate.md) diff --git a/docs/internals/builtins/string/gzinflate.md b/docs/internals/builtins/string/gzinflate.md index 464b9ebadb..8ab991a106 100644 --- a/docs/internals/builtins/string/gzinflate.md +++ b/docs/internals/builtins/string/gzinflate.md @@ -2,7 +2,7 @@ title: "gzinflate() — internals" description: "Compiler internals for gzinflate(): lowering path, type checks, and runtime helpers." sidebar: - order: 350 + order: 354 --- ## `gzinflate()` — internals @@ -32,6 +32,11 @@ function gzinflate(string $data, int $max_length = 0): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gzinflate()`](../../../php/builtins/string/gzinflate.md) diff --git a/docs/internals/builtins/string/gzuncompress.md b/docs/internals/builtins/string/gzuncompress.md index b035b545bb..4c6c9a18be 100644 --- a/docs/internals/builtins/string/gzuncompress.md +++ b/docs/internals/builtins/string/gzuncompress.md @@ -2,7 +2,7 @@ title: "gzuncompress() — internals" description: "Compiler internals for gzuncompress(): lowering path, type checks, and runtime helpers." sidebar: - order: 351 + order: 355 --- ## `gzuncompress()` — internals @@ -33,6 +33,11 @@ function gzuncompress(string $data, int $max_length = 0): mixed - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gzuncompress()`](../../../php/builtins/string/gzuncompress.md) diff --git a/docs/internals/builtins/string/hash.md b/docs/internals/builtins/string/hash.md index 834b5e26bc..ee1208e971 100644 --- a/docs/internals/builtins/string/hash.md +++ b/docs/internals/builtins/string/hash.md @@ -2,7 +2,7 @@ title: "hash() — internals" description: "Compiler internals for hash(): lowering path, type checks, and runtime helpers." sidebar: - order: 352 + order: 356 --- ## `hash()` — internals @@ -33,6 +33,11 @@ function hash(string $algo, string $data, bool $binary = false): string - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash()`](../../../php/builtins/string/hash.md) diff --git a/docs/internals/builtins/string/hash_algos.md b/docs/internals/builtins/string/hash_algos.md index 084f3d91f8..88c16fc3a7 100644 --- a/docs/internals/builtins/string/hash_algos.md +++ b/docs/internals/builtins/string/hash_algos.md @@ -2,7 +2,7 @@ title: "hash_algos() — internals" description: "Compiler internals for hash_algos(): lowering path, type checks, and runtime helpers." sidebar: - order: 353 + order: 357 --- ## `hash_algos()` — internals @@ -34,6 +34,11 @@ function hash_algos(): array - **Arity**: takes no arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_algos()`](../../../php/builtins/string/hash_algos.md) diff --git a/docs/internals/builtins/string/hash_copy.md b/docs/internals/builtins/string/hash_copy.md index 3388b21256..18dc0532f4 100644 --- a/docs/internals/builtins/string/hash_copy.md +++ b/docs/internals/builtins/string/hash_copy.md @@ -2,7 +2,7 @@ title: "hash_copy() — internals" description: "Compiler internals for hash_copy(): lowering path, type checks, and runtime helpers." sidebar: - order: 354 + order: 358 --- ## `hash_copy()` — internals @@ -36,6 +36,11 @@ function hash_copy(resource $context): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_copy()`](../../../php/builtins/string/hash_copy.md) diff --git a/docs/internals/builtins/string/hash_equals.md b/docs/internals/builtins/string/hash_equals.md index 0258c85aa5..1783a09e8c 100644 --- a/docs/internals/builtins/string/hash_equals.md +++ b/docs/internals/builtins/string/hash_equals.md @@ -2,7 +2,7 @@ title: "hash_equals() — internals" description: "Compiler internals for hash_equals(): lowering path, type checks, and runtime helpers." sidebar: - order: 355 + order: 359 --- ## `hash_equals()` — internals @@ -35,6 +35,11 @@ function hash_equals(string $known_string, string $user_string): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_equals()`](../../../php/builtins/string/hash_equals.md) diff --git a/docs/internals/builtins/string/hash_final.md b/docs/internals/builtins/string/hash_final.md index a0d4e30aed..e0f6773001 100644 --- a/docs/internals/builtins/string/hash_final.md +++ b/docs/internals/builtins/string/hash_final.md @@ -2,7 +2,7 @@ title: "hash_final() — internals" description: "Compiler internals for hash_final(): lowering path, type checks, and runtime helpers." sidebar: - order: 356 + order: 360 --- ## `hash_final()` — internals @@ -33,6 +33,11 @@ function hash_final(resource $context, bool $binary = false): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_final()`](../../../php/builtins/string/hash_final.md) diff --git a/docs/internals/builtins/string/hash_hmac.md b/docs/internals/builtins/string/hash_hmac.md index eba37cd6f1..10f244fcc7 100644 --- a/docs/internals/builtins/string/hash_hmac.md +++ b/docs/internals/builtins/string/hash_hmac.md @@ -2,7 +2,7 @@ title: "hash_hmac() — internals" description: "Compiler internals for hash_hmac(): lowering path, type checks, and runtime helpers." sidebar: - order: 357 + order: 361 --- ## `hash_hmac()` — internals @@ -34,6 +34,11 @@ function hash_hmac(string $algo, string $data, string $key, bool $binary = false - **Arity**: takes 3–4 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_hmac()`](../../../php/builtins/string/hash_hmac.md) diff --git a/docs/internals/builtins/string/hash_init.md b/docs/internals/builtins/string/hash_init.md index b83f92ed3c..a5461157bc 100644 --- a/docs/internals/builtins/string/hash_init.md +++ b/docs/internals/builtins/string/hash_init.md @@ -2,7 +2,7 @@ title: "hash_init() — internals" description: "Compiler internals for hash_init(): lowering path, type checks, and runtime helpers." sidebar: - order: 358 + order: 362 --- ## `hash_init()` — internals @@ -33,6 +33,11 @@ function hash_init(string $algo, int $flags = 0, string $key = ''): mixed - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_init()`](../../../php/builtins/string/hash_init.md) diff --git a/docs/internals/builtins/string/hash_update.md b/docs/internals/builtins/string/hash_update.md index 7ebe1692e2..24ff79a549 100644 --- a/docs/internals/builtins/string/hash_update.md +++ b/docs/internals/builtins/string/hash_update.md @@ -2,7 +2,7 @@ title: "hash_update() — internals" description: "Compiler internals for hash_update(): lowering path, type checks, and runtime helpers." sidebar: - order: 359 + order: 363 --- ## `hash_update()` — internals @@ -33,6 +33,11 @@ function hash_update(resource $context, string $data): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hash_update()`](../../../php/builtins/string/hash_update.md) diff --git a/docs/internals/builtins/string/hex2bin.md b/docs/internals/builtins/string/hex2bin.md index c1d005b41a..77c3fd985a 100644 --- a/docs/internals/builtins/string/hex2bin.md +++ b/docs/internals/builtins/string/hex2bin.md @@ -2,7 +2,7 @@ title: "hex2bin() — internals" description: "Compiler internals for hex2bin(): lowering path, type checks, and runtime helpers." sidebar: - order: 360 + order: 364 --- ## `hex2bin()` — internals @@ -33,6 +33,11 @@ function hex2bin(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `hex2bin()`](../../../php/builtins/string/hex2bin.md) diff --git a/docs/internals/builtins/string/html_entity_decode.md b/docs/internals/builtins/string/html_entity_decode.md index 065da3b5ef..88220e74ca 100644 --- a/docs/internals/builtins/string/html_entity_decode.md +++ b/docs/internals/builtins/string/html_entity_decode.md @@ -2,7 +2,7 @@ title: "html_entity_decode() — internals" description: "Compiler internals for html_entity_decode(): lowering path, type checks, and runtime helpers." sidebar: - order: 361 + order: 365 --- ## `html_entity_decode()` — internals @@ -33,6 +33,11 @@ function html_entity_decode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `html_entity_decode()`](../../../php/builtins/string/html_entity_decode.md) diff --git a/docs/internals/builtins/string/htmlentities.md b/docs/internals/builtins/string/htmlentities.md index ee3fca42d7..9146e85efa 100644 --- a/docs/internals/builtins/string/htmlentities.md +++ b/docs/internals/builtins/string/htmlentities.md @@ -2,7 +2,7 @@ title: "htmlentities() — internals" description: "Compiler internals for htmlentities(): lowering path, type checks, and runtime helpers." sidebar: - order: 362 + order: 366 --- ## `htmlentities()` — internals @@ -40,6 +40,11 @@ function htmlentities(string $string, int $flags = 11, string $encoding = 'UTF-8 - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `htmlentities()`](../../../php/builtins/string/htmlentities.md) diff --git a/docs/internals/builtins/string/htmlspecialchars.md b/docs/internals/builtins/string/htmlspecialchars.md index eaa1be0a5b..7bebeaea61 100644 --- a/docs/internals/builtins/string/htmlspecialchars.md +++ b/docs/internals/builtins/string/htmlspecialchars.md @@ -2,7 +2,7 @@ title: "htmlspecialchars() — internals" description: "Compiler internals for htmlspecialchars(): lowering path, type checks, and runtime helpers." sidebar: - order: 363 + order: 367 --- ## `htmlspecialchars()` — internals @@ -40,6 +40,11 @@ function htmlspecialchars(string $string, int $flags = 11, string $encoding = 'U - **Arity**: takes 1–3 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `htmlspecialchars()`](../../../php/builtins/string/htmlspecialchars.md) diff --git a/docs/internals/builtins/string/implode.md b/docs/internals/builtins/string/implode.md index 4ed1555f22..3e2ee5168a 100644 --- a/docs/internals/builtins/string/implode.md +++ b/docs/internals/builtins/string/implode.md @@ -2,7 +2,7 @@ title: "implode() — internals" description: "Compiler internals for implode(): lowering path, type checks, and runtime helpers." sidebar: - order: 364 + order: 368 --- ## `implode()` — internals @@ -32,6 +32,11 @@ function implode(string $separator, array $array = null): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/implode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/implode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `implode()`](../../../php/builtins/string/implode.md) diff --git a/docs/internals/builtins/string/inet_ntop.md b/docs/internals/builtins/string/inet_ntop.md index d5816bdb03..171110348c 100644 --- a/docs/internals/builtins/string/inet_ntop.md +++ b/docs/internals/builtins/string/inet_ntop.md @@ -2,7 +2,7 @@ title: "inet_ntop() — internals" description: "Compiler internals for inet_ntop(): lowering path, type checks, and runtime helpers." sidebar: - order: 365 + order: 369 --- ## `inet_ntop()` — internals @@ -33,6 +33,11 @@ function inet_ntop(string $ip): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `inet_ntop()`](../../../php/builtins/string/inet_ntop.md) diff --git a/docs/internals/builtins/string/inet_pton.md b/docs/internals/builtins/string/inet_pton.md index de8321305a..d103808624 100644 --- a/docs/internals/builtins/string/inet_pton.md +++ b/docs/internals/builtins/string/inet_pton.md @@ -2,7 +2,7 @@ title: "inet_pton() — internals" description: "Compiler internals for inet_pton(): lowering path, type checks, and runtime helpers." sidebar: - order: 366 + order: 370 --- ## `inet_pton()` — internals @@ -33,6 +33,11 @@ function inet_pton(string $ip): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `inet_pton()`](../../../php/builtins/string/inet_pton.md) diff --git a/docs/internals/builtins/string/ip2long.md b/docs/internals/builtins/string/ip2long.md index 88afffbe7d..f3e2527b7a 100644 --- a/docs/internals/builtins/string/ip2long.md +++ b/docs/internals/builtins/string/ip2long.md @@ -2,7 +2,7 @@ title: "ip2long() — internals" description: "Compiler internals for ip2long(): lowering path, type checks, and runtime helpers." sidebar: - order: 367 + order: 371 --- ## `ip2long()` — internals @@ -34,6 +34,11 @@ function ip2long(string $ip): mixed - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ip2long()`](../../../php/builtins/string/ip2long.md) diff --git a/docs/internals/builtins/string/lcfirst.md b/docs/internals/builtins/string/lcfirst.md index b7a9e571b0..8c9e2feed1 100644 --- a/docs/internals/builtins/string/lcfirst.md +++ b/docs/internals/builtins/string/lcfirst.md @@ -2,7 +2,7 @@ title: "lcfirst() — internals" description: "Compiler internals for lcfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 368 + order: 372 --- ## `lcfirst()` — internals @@ -33,6 +33,11 @@ function lcfirst(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `lcfirst()`](../../../php/builtins/string/lcfirst.md) diff --git a/docs/internals/builtins/string/long2ip.md b/docs/internals/builtins/string/long2ip.md index 91542b8e48..ef662e94ba 100644 --- a/docs/internals/builtins/string/long2ip.md +++ b/docs/internals/builtins/string/long2ip.md @@ -2,7 +2,7 @@ title: "long2ip() — internals" description: "Compiler internals for long2ip(): lowering path, type checks, and runtime helpers." sidebar: - order: 369 + order: 373 --- ## `long2ip()` — internals @@ -34,6 +34,11 @@ function long2ip(int $ip): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `long2ip()`](../../../php/builtins/string/long2ip.md) diff --git a/docs/internals/builtins/string/ltrim.md b/docs/internals/builtins/string/ltrim.md index a8424df70b..73491daf2c 100644 --- a/docs/internals/builtins/string/ltrim.md +++ b/docs/internals/builtins/string/ltrim.md @@ -2,7 +2,7 @@ title: "ltrim() — internals" description: "Compiler internals for ltrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 370 + order: 374 --- ## `ltrim()` — internals @@ -32,6 +32,11 @@ function ltrim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): stri - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ltrim()`](../../../php/builtins/string/ltrim.md) diff --git a/docs/internals/builtins/string/md5.md b/docs/internals/builtins/string/md5.md index 3059c302ea..9518bf8c88 100644 --- a/docs/internals/builtins/string/md5.md +++ b/docs/internals/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5() — internals" description: "Compiler internals for md5(): lowering path, type checks, and runtime helpers." sidebar: - order: 371 + order: 375 --- ## `md5()` — internals @@ -35,6 +35,11 @@ function md5(string $string, bool $binary = false): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/md5.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/md5.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `md5()`](../../../php/builtins/string/md5.md) diff --git a/docs/internals/builtins/string/nl2br.md b/docs/internals/builtins/string/nl2br.md index 46df28ab4d..f3c46ae63a 100644 --- a/docs/internals/builtins/string/nl2br.md +++ b/docs/internals/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br() — internals" description: "Compiler internals for nl2br(): lowering path, type checks, and runtime helpers." sidebar: - order: 372 + order: 376 --- ## `nl2br()` — internals @@ -33,6 +33,11 @@ function nl2br(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `nl2br()`](../../../php/builtins/string/nl2br.md) diff --git a/docs/internals/builtins/string/number_format.md b/docs/internals/builtins/string/number_format.md index ff8bc83703..8b94b884d7 100644 --- a/docs/internals/builtins/string/number_format.md +++ b/docs/internals/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format() — internals" description: "Compiler internals for number_format(): lowering path, type checks, and runtime helpers." sidebar: - order: 373 + order: 377 --- ## `number_format()` — internals @@ -33,6 +33,11 @@ function number_format(float $num, int $decimals = 0, string $decimal_separator - **Arity**: takes 1–4 arguments (3 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `number_format()`](../../../php/builtins/string/number_format.md) diff --git a/docs/internals/builtins/string/ord.md b/docs/internals/builtins/string/ord.md index f79f8660ad..36de3e8129 100644 --- a/docs/internals/builtins/string/ord.md +++ b/docs/internals/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord() — internals" description: "Compiler internals for ord(): lowering path, type checks, and runtime helpers." sidebar: - order: 374 + order: 378 --- ## `ord()` — internals @@ -32,6 +32,11 @@ function ord(string $character): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ord.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ord()`](../../../php/builtins/string/ord.md) diff --git a/docs/internals/builtins/string/printf.md b/docs/internals/builtins/string/printf.md index f8482c32da..269c1d532b 100644 --- a/docs/internals/builtins/string/printf.md +++ b/docs/internals/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf() — internals" description: "Compiler internals for printf(): lowering path, type checks, and runtime helpers." sidebar: - order: 375 + order: 379 --- ## `printf()` — internals @@ -34,6 +34,12 @@ function printf(string $format, ...$values): int - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `printf()`](../../../php/builtins/string/printf.md) diff --git a/docs/internals/builtins/string/rawurldecode.md b/docs/internals/builtins/string/rawurldecode.md index 50a9b774ef..f3f2283772 100644 --- a/docs/internals/builtins/string/rawurldecode.md +++ b/docs/internals/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode() — internals" description: "Compiler internals for rawurldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 376 + order: 380 --- ## `rawurldecode()` — internals @@ -33,6 +33,11 @@ function rawurldecode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rawurldecode()`](../../../php/builtins/string/rawurldecode.md) diff --git a/docs/internals/builtins/string/rawurlencode.md b/docs/internals/builtins/string/rawurlencode.md index bd08e09e31..2b5970a386 100644 --- a/docs/internals/builtins/string/rawurlencode.md +++ b/docs/internals/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode() — internals" description: "Compiler internals for rawurlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 377 + order: 381 --- ## `rawurlencode()` — internals @@ -33,6 +33,11 @@ function rawurlencode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rawurlencode()`](../../../php/builtins/string/rawurlencode.md) diff --git a/docs/internals/builtins/string/rtrim.md b/docs/internals/builtins/string/rtrim.md index 170c45b958..102f293b2b 100644 --- a/docs/internals/builtins/string/rtrim.md +++ b/docs/internals/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim() — internals" description: "Compiler internals for rtrim(): lowering path, type checks, and runtime helpers." sidebar: - order: 378 + order: 382 --- ## `rtrim()` — internals @@ -32,6 +32,11 @@ function rtrim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): stri - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `rtrim()`](../../../php/builtins/string/rtrim.md) diff --git a/docs/internals/builtins/string/sha1.md b/docs/internals/builtins/string/sha1.md index b280768e3b..92f892c5d9 100644 --- a/docs/internals/builtins/string/sha1.md +++ b/docs/internals/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1() — internals" description: "Compiler internals for sha1(): lowering path, type checks, and runtime helpers." sidebar: - order: 379 + order: 383 --- ## `sha1()` — internals @@ -34,6 +34,11 @@ function sha1(string $string, bool $binary = false): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/sha1.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `sha1()`](../../../php/builtins/string/sha1.md) diff --git a/docs/internals/builtins/string/sprintf.md b/docs/internals/builtins/string/sprintf.md index 8b4727946b..1ad8926087 100644 --- a/docs/internals/builtins/string/sprintf.md +++ b/docs/internals/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf() — internals" description: "Compiler internals for sprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 380 + order: 384 --- ## `sprintf()` — internals @@ -34,6 +34,12 @@ function sprintf(string $format, ...$values): string - **Arity**: takes exactly 1 argument. - **Variadic**: collects excess arguments into `$values`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$values`. + ## Cross-references - [User reference for `sprintf()`](../../../php/builtins/string/sprintf.md) diff --git a/docs/internals/builtins/string/sscanf.md b/docs/internals/builtins/string/sscanf.md index f0201d4d3f..05bf35a145 100644 --- a/docs/internals/builtins/string/sscanf.md +++ b/docs/internals/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf() — internals" description: "Compiler internals for sscanf(): lowering path, type checks, and runtime helpers." sidebar: - order: 381 + order: 385 --- ## `sscanf()` — internals @@ -35,6 +35,12 @@ function sscanf(string $string, string $format, ...$vars): array - **Arity**: takes exactly 2 arguments. - **Variadic**: collects excess arguments into `$vars`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **Variadic**: collects excess arguments into `$vars`. + ## Cross-references - [User reference for `sscanf()`](../../../php/builtins/string/sscanf.md) diff --git a/docs/internals/builtins/string/str_contains.md b/docs/internals/builtins/string/str_contains.md index 57903400db..985af2503c 100644 --- a/docs/internals/builtins/string/str_contains.md +++ b/docs/internals/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains() — internals" description: "Compiler internals for str_contains(): lowering path, type checks, and runtime helpers." sidebar: - order: 382 + order: 386 --- ## `str_contains()` — internals @@ -33,6 +33,11 @@ function str_contains(string $haystack, string $needle): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_contains()`](../../../php/builtins/string/str_contains.md) diff --git a/docs/internals/builtins/string/str_ends_with.md b/docs/internals/builtins/string/str_ends_with.md index 73c958d7ff..7751e3a6a3 100644 --- a/docs/internals/builtins/string/str_ends_with.md +++ b/docs/internals/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with() — internals" description: "Compiler internals for str_ends_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 383 + order: 387 --- ## `str_ends_with()` — internals @@ -33,6 +33,11 @@ function str_ends_with(string $haystack, string $needle): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_ends_with()`](../../../php/builtins/string/str_ends_with.md) diff --git a/docs/internals/builtins/string/str_ireplace.md b/docs/internals/builtins/string/str_ireplace.md index 452c06ba77..3f1f7ba9be 100644 --- a/docs/internals/builtins/string/str_ireplace.md +++ b/docs/internals/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace() — internals" description: "Compiler internals for str_ireplace(): lowering path, type checks, and runtime helpers." sidebar: - order: 384 + order: 388 --- ## `str_ireplace()` — internals @@ -32,6 +32,11 @@ function str_ireplace(string $search, string $replace, string $subject, int $cou - **Arity**: takes 3–4 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_ireplace()`](../../../php/builtins/string/str_ireplace.md) diff --git a/docs/internals/builtins/string/str_pad.md b/docs/internals/builtins/string/str_pad.md index 185165ee14..d26a219b72 100644 --- a/docs/internals/builtins/string/str_pad.md +++ b/docs/internals/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad() — internals" description: "Compiler internals for str_pad(): lowering path, type checks, and runtime helpers." sidebar: - order: 385 + order: 389 --- ## `str_pad()` — internals @@ -33,6 +33,11 @@ function str_pad(string $string, int $length, string $pad_string = ' ', int $pad - **Arity**: takes 2–4 arguments (2 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_pad()`](../../../php/builtins/string/str_pad.md) diff --git a/docs/internals/builtins/string/str_repeat.md b/docs/internals/builtins/string/str_repeat.md index 74d8e41ef5..1066f3f0e1 100644 --- a/docs/internals/builtins/string/str_repeat.md +++ b/docs/internals/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat() — internals" description: "Compiler internals for str_repeat(): lowering path, type checks, and runtime helpers." sidebar: - order: 386 + order: 390 --- ## `str_repeat()` — internals @@ -33,6 +33,11 @@ function str_repeat(string $string, int $times): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_repeat()`](../../../php/builtins/string/str_repeat.md) diff --git a/docs/internals/builtins/string/str_replace.md b/docs/internals/builtins/string/str_replace.md index 41d9bd924a..1c8c2c06ef 100644 --- a/docs/internals/builtins/string/str_replace.md +++ b/docs/internals/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace() — internals" description: "Compiler internals for str_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 387 + order: 391 --- ## `str_replace()` — internals @@ -32,6 +32,11 @@ function str_replace(string $search, string $replace, string $subject, int $coun - **Arity**: takes 3–4 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_replace()`](../../../php/builtins/string/str_replace.md) diff --git a/docs/internals/builtins/string/str_split.md b/docs/internals/builtins/string/str_split.md index 8ce8559196..67b26a11c7 100644 --- a/docs/internals/builtins/string/str_split.md +++ b/docs/internals/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split() — internals" description: "Compiler internals for str_split(): lowering path, type checks, and runtime helpers." sidebar: - order: 388 + order: 392 --- ## `str_split()` — internals @@ -33,6 +33,11 @@ function str_split(string $string, int $length = 1): array - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_split()`](../../../php/builtins/string/str_split.md) diff --git a/docs/internals/builtins/string/str_starts_with.md b/docs/internals/builtins/string/str_starts_with.md index 5ec7f6574c..ab14e8de86 100644 --- a/docs/internals/builtins/string/str_starts_with.md +++ b/docs/internals/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with() — internals" description: "Compiler internals for str_starts_with(): lowering path, type checks, and runtime helpers." sidebar: - order: 389 + order: 393 --- ## `str_starts_with()` — internals @@ -33,6 +33,11 @@ function str_starts_with(string $haystack, string $needle): bool - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `str_starts_with()`](../../../php/builtins/string/str_starts_with.md) diff --git a/docs/internals/builtins/string/strcasecmp.md b/docs/internals/builtins/string/strcasecmp.md index 94012e8b41..7677ab0473 100644 --- a/docs/internals/builtins/string/strcasecmp.md +++ b/docs/internals/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp() — internals" description: "Compiler internals for strcasecmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 390 + order: 394 --- ## `strcasecmp()` — internals @@ -33,6 +33,11 @@ function strcasecmp(string $string1, string $string2): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strcasecmp()`](../../../php/builtins/string/strcasecmp.md) diff --git a/docs/internals/builtins/string/strcmp.md b/docs/internals/builtins/string/strcmp.md index 98682318f4..fef1c20878 100644 --- a/docs/internals/builtins/string/strcmp.md +++ b/docs/internals/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp() — internals" description: "Compiler internals for strcmp(): lowering path, type checks, and runtime helpers." sidebar: - order: 391 + order: 395 --- ## `strcmp()` — internals @@ -33,6 +33,11 @@ function strcmp(string $string1, string $string2): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strcmp()`](../../../php/builtins/string/strcmp.md) diff --git a/docs/internals/builtins/string/stripslashes.md b/docs/internals/builtins/string/stripslashes.md index 9b04fd06c5..0fd90ea4a7 100644 --- a/docs/internals/builtins/string/stripslashes.md +++ b/docs/internals/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes() — internals" description: "Compiler internals for stripslashes(): lowering path, type checks, and runtime helpers." sidebar: - order: 392 + order: 396 --- ## `stripslashes()` — internals @@ -33,6 +33,11 @@ function stripslashes(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `stripslashes()`](../../../php/builtins/string/stripslashes.md) diff --git a/docs/internals/builtins/string/strlen.md b/docs/internals/builtins/string/strlen.md index 973fff9031..3a9144829b 100644 --- a/docs/internals/builtins/string/strlen.md +++ b/docs/internals/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen() — internals" description: "Compiler internals for strlen(): lowering path, type checks, and runtime helpers." sidebar: - order: 393 + order: 397 --- ## `strlen()` — internals @@ -33,6 +33,11 @@ function strlen(string $string): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strlen()`](../../../php/builtins/string/strlen.md) diff --git a/docs/internals/builtins/string/strpos.md b/docs/internals/builtins/string/strpos.md index d659c06081..e8b8c04234 100644 --- a/docs/internals/builtins/string/strpos.md +++ b/docs/internals/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos() — internals" description: "Compiler internals for strpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 394 + order: 398 --- ## `strpos()` — internals @@ -32,6 +32,11 @@ function strpos(string $haystack, string $needle, int $offset = 0): mixed - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strpos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strpos()`](../../../php/builtins/string/strpos.md) diff --git a/docs/internals/builtins/string/strrev.md b/docs/internals/builtins/string/strrev.md index e660f9b8a0..3b6af20fae 100644 --- a/docs/internals/builtins/string/strrev.md +++ b/docs/internals/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev() — internals" description: "Compiler internals for strrev(): lowering path, type checks, and runtime helpers." sidebar: - order: 395 + order: 399 --- ## `strrev()` — internals @@ -33,6 +33,11 @@ function strrev(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strrev()`](../../../php/builtins/string/strrev.md) diff --git a/docs/internals/builtins/string/strrpos.md b/docs/internals/builtins/string/strrpos.md index 2e112a52a5..b9ef683672 100644 --- a/docs/internals/builtins/string/strrpos.md +++ b/docs/internals/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos() — internals" description: "Compiler internals for strrpos(): lowering path, type checks, and runtime helpers." sidebar: - order: 396 + order: 400 --- ## `strrpos()` — internals @@ -32,6 +32,11 @@ function strrpos(string $haystack, string $needle, int $offset = 0): mixed - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strrpos()`](../../../php/builtins/string/strrpos.md) diff --git a/docs/internals/builtins/string/strstr.md b/docs/internals/builtins/string/strstr.md index 3dc79f9836..2162c7e3c6 100644 --- a/docs/internals/builtins/string/strstr.md +++ b/docs/internals/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr() — internals" description: "Compiler internals for strstr(): lowering path, type checks, and runtime helpers." sidebar: - order: 397 + order: 401 --- ## `strstr()` — internals @@ -32,6 +32,11 @@ function strstr(string $haystack, string $needle, bool $before_needle = false): - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strstr()`](../../../php/builtins/string/strstr.md) diff --git a/docs/internals/builtins/string/strtolower.md b/docs/internals/builtins/string/strtolower.md index 3a75ab8ee6..f7598055a4 100644 --- a/docs/internals/builtins/string/strtolower.md +++ b/docs/internals/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower() — internals" description: "Compiler internals for strtolower(): lowering path, type checks, and runtime helpers." sidebar: - order: 398 + order: 402 --- ## `strtolower()` — internals @@ -33,6 +33,11 @@ function strtolower(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strtolower()`](../../../php/builtins/string/strtolower.md) diff --git a/docs/internals/builtins/string/strtoupper.md b/docs/internals/builtins/string/strtoupper.md index a20e0d5d0e..3e1b44f203 100644 --- a/docs/internals/builtins/string/strtoupper.md +++ b/docs/internals/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper() — internals" description: "Compiler internals for strtoupper(): lowering path, type checks, and runtime helpers." sidebar: - order: 399 + order: 403 --- ## `strtoupper()` — internals @@ -33,6 +33,11 @@ function strtoupper(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `strtoupper()`](../../../php/builtins/string/strtoupper.md) diff --git a/docs/internals/builtins/string/substr.md b/docs/internals/builtins/string/substr.md index ddaf9cb105..1dd8c158ab 100644 --- a/docs/internals/builtins/string/substr.md +++ b/docs/internals/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr() — internals" description: "Compiler internals for substr(): lowering path, type checks, and runtime helpers." sidebar: - order: 400 + order: 404 --- ## `substr()` — internals @@ -33,6 +33,11 @@ function substr(string $string, int $offset, int $length = null): string - **Arity**: takes 2–3 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/substr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/substr.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `substr()`](../../../php/builtins/string/substr.md) diff --git a/docs/internals/builtins/string/substr_replace.md b/docs/internals/builtins/string/substr_replace.md index dd5bf4a2d1..7e8af23a91 100644 --- a/docs/internals/builtins/string/substr_replace.md +++ b/docs/internals/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace() — internals" description: "Compiler internals for substr_replace(): lowering path, type checks, and runtime helpers." sidebar: - order: 401 + order: 405 --- ## `substr_replace()` — internals @@ -34,6 +34,11 @@ function substr_replace(string $string, string $replace, int $offset, int $lengt - **Arity**: takes 3–4 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `substr_replace()`](../../../php/builtins/string/substr_replace.md) diff --git a/docs/internals/builtins/string/trim.md b/docs/internals/builtins/string/trim.md index 1167a00126..c4642fdbec 100644 --- a/docs/internals/builtins/string/trim.md +++ b/docs/internals/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim() — internals" description: "Compiler internals for trim(): lowering path, type checks, and runtime helpers." sidebar: - order: 402 + order: 406 --- ## `trim()` — internals @@ -32,6 +32,11 @@ function trim(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): strin - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/trim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/trim.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `trim()`](../../../php/builtins/string/trim.md) diff --git a/docs/internals/builtins/string/ucfirst.md b/docs/internals/builtins/string/ucfirst.md index f1c7c5bc2d..8ada62280e 100644 --- a/docs/internals/builtins/string/ucfirst.md +++ b/docs/internals/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst() — internals" description: "Compiler internals for ucfirst(): lowering path, type checks, and runtime helpers." sidebar: - order: 403 + order: 407 --- ## `ucfirst()` — internals @@ -33,6 +33,11 @@ function ucfirst(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ucfirst()`](../../../php/builtins/string/ucfirst.md) diff --git a/docs/internals/builtins/string/ucwords.md b/docs/internals/builtins/string/ucwords.md index c30c6a72a8..a8c2c0e952 100644 --- a/docs/internals/builtins/string/ucwords.md +++ b/docs/internals/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords() — internals" description: "Compiler internals for ucwords(): lowering path, type checks, and runtime helpers." sidebar: - order: 404 + order: 408 --- ## `ucwords()` — internals @@ -33,6 +33,11 @@ function ucwords(string $string, string $separators = ' \t\r\n\x0c\x0b'): string - **Arity**: takes 1–2 arguments (1 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ucwords()`](../../../php/builtins/string/ucwords.md) diff --git a/docs/internals/builtins/string/urldecode.md b/docs/internals/builtins/string/urldecode.md index dd47b4404c..3f34e71607 100644 --- a/docs/internals/builtins/string/urldecode.md +++ b/docs/internals/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode() — internals" description: "Compiler internals for urldecode(): lowering path, type checks, and runtime helpers." sidebar: - order: 405 + order: 409 --- ## `urldecode()` — internals @@ -33,6 +33,11 @@ function urldecode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `urldecode()`](../../../php/builtins/string/urldecode.md) diff --git a/docs/internals/builtins/string/urlencode.md b/docs/internals/builtins/string/urlencode.md index 28031967cd..a796b83f6a 100644 --- a/docs/internals/builtins/string/urlencode.md +++ b/docs/internals/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode() — internals" description: "Compiler internals for urlencode(): lowering path, type checks, and runtime helpers." sidebar: - order: 406 + order: 410 --- ## `urlencode()` — internals @@ -33,6 +33,11 @@ function urlencode(string $string): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `urlencode()`](../../../php/builtins/string/urlencode.md) diff --git a/docs/internals/builtins/string/vprintf.md b/docs/internals/builtins/string/vprintf.md index bea0e79a04..cde64be266 100644 --- a/docs/internals/builtins/string/vprintf.md +++ b/docs/internals/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf() — internals" description: "Compiler internals for vprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 407 + order: 411 --- ## `vprintf()` — internals @@ -33,6 +33,11 @@ function vprintf(string $format, array $values): int - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `vprintf()`](../../../php/builtins/string/vprintf.md) diff --git a/docs/internals/builtins/string/vsprintf.md b/docs/internals/builtins/string/vsprintf.md index b85486eb0a..a2a594bb37 100644 --- a/docs/internals/builtins/string/vsprintf.md +++ b/docs/internals/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf() — internals" description: "Compiler internals for vsprintf(): lowering path, type checks, and runtime helpers." sidebar: - order: 408 + order: 412 --- ## `vsprintf()` — internals @@ -33,6 +33,11 @@ function vsprintf(string $format, array $values): string - **Arity**: takes exactly 2 arguments. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `vsprintf()`](../../../php/builtins/string/vsprintf.md) diff --git a/docs/internals/builtins/string/wordwrap.md b/docs/internals/builtins/string/wordwrap.md index 13177007f7..3f1dc4258e 100644 --- a/docs/internals/builtins/string/wordwrap.md +++ b/docs/internals/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap() — internals" description: "Compiler internals for wordwrap(): lowering path, type checks, and runtime helpers." sidebar: - order: 409 + order: 413 --- ## `wordwrap()` — internals @@ -34,6 +34,11 @@ function wordwrap(string $string, int $width = 75, string $break = '\n', bool $c - **Arity**: takes 1–4 arguments (3 optional). +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `wordwrap()`](../../../php/builtins/string/wordwrap.md) diff --git a/docs/internals/builtins/type/boolval.md b/docs/internals/builtins/type/boolval.md index 6a6e8a3262..f6c9053f32 100644 --- a/docs/internals/builtins/type/boolval.md +++ b/docs/internals/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval() — internals" description: "Compiler internals for boolval(): lowering path, type checks, and runtime helpers." sidebar: - order: 410 + order: 414 --- ## `boolval()` — internals @@ -33,6 +33,11 @@ function boolval(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `boolval()`](../../../php/builtins/type/boolval.md) diff --git a/docs/internals/builtins/type/ctype_alnum.md b/docs/internals/builtins/type/ctype_alnum.md index d07a19941d..ee8a26ba95 100644 --- a/docs/internals/builtins/type/ctype_alnum.md +++ b/docs/internals/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum() — internals" description: "Compiler internals for ctype_alnum(): lowering path, type checks, and runtime helpers." sidebar: - order: 411 + order: 415 --- ## `ctype_alnum()` — internals @@ -32,6 +32,11 @@ function ctype_alnum(string $text): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ctype_alnum()`](../../../php/builtins/type/ctype_alnum.md) diff --git a/docs/internals/builtins/type/ctype_alpha.md b/docs/internals/builtins/type/ctype_alpha.md index 07994a87f3..c029edcf71 100644 --- a/docs/internals/builtins/type/ctype_alpha.md +++ b/docs/internals/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha() — internals" description: "Compiler internals for ctype_alpha(): lowering path, type checks, and runtime helpers." sidebar: - order: 412 + order: 416 --- ## `ctype_alpha()` — internals @@ -32,6 +32,11 @@ function ctype_alpha(string $text): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ctype_alpha()`](../../../php/builtins/type/ctype_alpha.md) diff --git a/docs/internals/builtins/type/ctype_digit.md b/docs/internals/builtins/type/ctype_digit.md index 55ca4af83e..dc9cd2cf56 100644 --- a/docs/internals/builtins/type/ctype_digit.md +++ b/docs/internals/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit() — internals" description: "Compiler internals for ctype_digit(): lowering path, type checks, and runtime helpers." sidebar: - order: 413 + order: 417 --- ## `ctype_digit()` — internals @@ -32,6 +32,11 @@ function ctype_digit(string $text): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ctype_digit()`](../../../php/builtins/type/ctype_digit.md) diff --git a/docs/internals/builtins/type/ctype_space.md b/docs/internals/builtins/type/ctype_space.md index dff9389a31..47ddb640ac 100644 --- a/docs/internals/builtins/type/ctype_space.md +++ b/docs/internals/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space() — internals" description: "Compiler internals for ctype_space(): lowering path, type checks, and runtime helpers." sidebar: - order: 414 + order: 418 --- ## `ctype_space()` — internals @@ -32,6 +32,11 @@ function ctype_space(string $text): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `ctype_space()`](../../../php/builtins/type/ctype_space.md) diff --git a/docs/internals/builtins/type/floatval.md b/docs/internals/builtins/type/floatval.md index 5d1c9b0e39..9d7b91bb1d 100644 --- a/docs/internals/builtins/type/floatval.md +++ b/docs/internals/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval() — internals" description: "Compiler internals for floatval(): lowering path, type checks, and runtime helpers." sidebar: - order: 415 + order: 419 --- ## `floatval()` — internals @@ -34,6 +34,11 @@ function floatval(mixed $value): float - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `floatval()`](../../../php/builtins/type/floatval.md) diff --git a/docs/internals/builtins/type/get_resource_id.md b/docs/internals/builtins/type/get_resource_id.md index b6e4dae6f5..68aeaa0a4c 100644 --- a/docs/internals/builtins/type/get_resource_id.md +++ b/docs/internals/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id() — internals" description: "Compiler internals for get_resource_id(): lowering path, type checks, and runtime helpers." sidebar: - order: 416 + order: 420 --- ## `get_resource_id()` — internals @@ -32,6 +32,11 @@ function get_resource_id(resource $resource): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_resource_id()`](../../../php/builtins/type/get_resource_id.md) diff --git a/docs/internals/builtins/type/get_resource_type.md b/docs/internals/builtins/type/get_resource_type.md index 9188f37c23..a40b2cb24f 100644 --- a/docs/internals/builtins/type/get_resource_type.md +++ b/docs/internals/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type() — internals" description: "Compiler internals for get_resource_type(): lowering path, type checks, and runtime helpers." sidebar: - order: 417 + order: 421 --- ## `get_resource_type()` — internals @@ -32,6 +32,11 @@ function get_resource_type(resource $resource): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `get_resource_type()`](../../../php/builtins/type/get_resource_type.md) diff --git a/docs/internals/builtins/type/gettype.md b/docs/internals/builtins/type/gettype.md index 2d299af69d..6940b55ff1 100644 --- a/docs/internals/builtins/type/gettype.md +++ b/docs/internals/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype() — internals" description: "Compiler internals for gettype(): lowering path, type checks, and runtime helpers." sidebar: - order: 418 + order: 422 --- ## `gettype()` — internals @@ -32,6 +32,11 @@ function gettype(mixed $value): string - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `gettype()`](../../../php/builtins/type/gettype.md) diff --git a/docs/internals/builtins/type/intval.md b/docs/internals/builtins/type/intval.md index 9c8d1c45ae..a7f42ca644 100644 --- a/docs/internals/builtins/type/intval.md +++ b/docs/internals/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval() — internals" description: "Compiler internals for intval(): lowering path, type checks, and runtime helpers." sidebar: - order: 419 + order: 423 --- ## `intval()` — internals @@ -34,6 +34,11 @@ function intval(mixed $value): int - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/intval.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `intval()`](../../../php/builtins/type/intval.md) diff --git a/docs/internals/builtins/type/is_array.md b/docs/internals/builtins/type/is_array.md index 3d39b6837d..ebd1fa9a72 100644 --- a/docs/internals/builtins/type/is_array.md +++ b/docs/internals/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array() — internals" description: "Compiler internals for is_array(): lowering path, type checks, and runtime helpers." sidebar: - order: 420 + order: 424 --- ## `is_array()` — internals @@ -34,6 +34,11 @@ function is_array(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_array()`](../../../php/builtins/type/is_array.md) diff --git a/docs/internals/builtins/type/is_bool.md b/docs/internals/builtins/type/is_bool.md index 4a2e5cb2ba..4a9b9ba6cf 100644 --- a/docs/internals/builtins/type/is_bool.md +++ b/docs/internals/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool() — internals" description: "Compiler internals for is_bool(): lowering path, type checks, and runtime helpers." sidebar: - order: 421 + order: 425 --- ## `is_bool()` — internals @@ -32,6 +32,11 @@ function is_bool(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_bool()`](../../../php/builtins/type/is_bool.md) diff --git a/docs/internals/builtins/type/is_callable.md b/docs/internals/builtins/type/is_callable.md index d5454cb286..fe038fea6d 100644 --- a/docs/internals/builtins/type/is_callable.md +++ b/docs/internals/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable() — internals" description: "Compiler internals for is_callable(): lowering path, type checks, and runtime helpers." sidebar: - order: 422 + order: 426 --- ## `is_callable()` — internals @@ -34,6 +34,12 @@ function is_callable(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` +- **By-reference parameters**: `$callable_name`. + ## Cross-references - [User reference for `is_callable()`](../../../php/builtins/type/is_callable.md) diff --git a/docs/internals/builtins/type/is_float.md b/docs/internals/builtins/type/is_float.md index 22cc335d43..4649c86f79 100644 --- a/docs/internals/builtins/type/is_float.md +++ b/docs/internals/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float() — internals" description: "Compiler internals for is_float(): lowering path, type checks, and runtime helpers." sidebar: - order: 423 + order: 427 --- ## `is_float()` — internals @@ -32,6 +32,11 @@ function is_float(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_float()`](../../../php/builtins/type/is_float.md) diff --git a/docs/internals/builtins/type/is_int.md b/docs/internals/builtins/type/is_int.md index b38c7e5445..ac685e9c43 100644 --- a/docs/internals/builtins/type/is_int.md +++ b/docs/internals/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int() — internals" description: "Compiler internals for is_int(): lowering path, type checks, and runtime helpers." sidebar: - order: 424 + order: 428 --- ## `is_int()` — internals @@ -32,6 +32,11 @@ function is_int(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_int()`](../../../php/builtins/type/is_int.md) diff --git a/docs/internals/builtins/type/is_iterable.md b/docs/internals/builtins/type/is_iterable.md index 982c72bbbb..2ab3517822 100644 --- a/docs/internals/builtins/type/is_iterable.md +++ b/docs/internals/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable() — internals" description: "Compiler internals for is_iterable(): lowering path, type checks, and runtime helpers." sidebar: - order: 425 + order: 429 --- ## `is_iterable()` — internals @@ -32,6 +32,11 @@ function is_iterable(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_iterable()`](../../../php/builtins/type/is_iterable.md) diff --git a/docs/internals/builtins/type/is_null.md b/docs/internals/builtins/type/is_null.md index 5ed5435294..b9dba16781 100644 --- a/docs/internals/builtins/type/is_null.md +++ b/docs/internals/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null() — internals" description: "Compiler internals for is_null(): lowering path, type checks, and runtime helpers." sidebar: - order: 426 + order: 430 --- ## `is_null()` — internals @@ -32,6 +32,11 @@ function is_null(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_null()`](../../../php/builtins/type/is_null.md) diff --git a/docs/internals/builtins/type/is_numeric.md b/docs/internals/builtins/type/is_numeric.md index 63460e474e..fb1de2f6b3 100644 --- a/docs/internals/builtins/type/is_numeric.md +++ b/docs/internals/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric() — internals" description: "Compiler internals for is_numeric(): lowering path, type checks, and runtime helpers." sidebar: - order: 427 + order: 431 --- ## `is_numeric()` — internals @@ -32,6 +32,11 @@ function is_numeric(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_numeric()`](../../../php/builtins/type/is_numeric.md) diff --git a/docs/internals/builtins/type/is_object.md b/docs/internals/builtins/type/is_object.md index fd162efa8f..b6cb3a488c 100644 --- a/docs/internals/builtins/type/is_object.md +++ b/docs/internals/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object() — internals" description: "Compiler internals for is_object(): lowering path, type checks, and runtime helpers." sidebar: - order: 428 + order: 432 --- ## `is_object()` — internals @@ -33,6 +33,11 @@ function is_object(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_object()`](../../../php/builtins/type/is_object.md) diff --git a/docs/internals/builtins/type/is_resource.md b/docs/internals/builtins/type/is_resource.md index 0a2f85075f..25cb576ab1 100644 --- a/docs/internals/builtins/type/is_resource.md +++ b/docs/internals/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource() — internals" description: "Compiler internals for is_resource(): lowering path, type checks, and runtime helpers." sidebar: - order: 429 + order: 433 --- ## `is_resource()` — internals @@ -32,6 +32,11 @@ function is_resource(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_resource()`](../../../php/builtins/type/is_resource.md) diff --git a/docs/internals/builtins/type/is_scalar.md b/docs/internals/builtins/type/is_scalar.md index 08b141ddeb..3bf4b2e635 100644 --- a/docs/internals/builtins/type/is_scalar.md +++ b/docs/internals/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar() — internals" description: "Compiler internals for is_scalar(): lowering path, type checks, and runtime helpers." sidebar: - order: 430 + order: 434 --- ## `is_scalar()` — internals @@ -34,6 +34,11 @@ function is_scalar(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_scalar()`](../../../php/builtins/type/is_scalar.md) diff --git a/docs/internals/builtins/type/is_string.md b/docs/internals/builtins/type/is_string.md index 2b64474ffe..3eeaf38330 100644 --- a/docs/internals/builtins/type/is_string.md +++ b/docs/internals/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string() — internals" description: "Compiler internals for is_string(): lowering path, type checks, and runtime helpers." sidebar: - order: 431 + order: 435 --- ## `is_string()` — internals @@ -32,6 +32,11 @@ function is_string(mixed $value): bool - **Arity**: takes exactly 1 argument. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs) (`eval_builtin!`) +- **Dispatch hooks**: `direct`, `values` + ## Cross-references - [User reference for `is_string()`](../../../php/builtins/type/is_string.md) diff --git a/docs/internals/builtins/type/settype.md b/docs/internals/builtins/type/settype.md index 4fb5b65b76..91a22393a9 100644 --- a/docs/internals/builtins/type/settype.md +++ b/docs/internals/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype() — internals" description: "Compiler internals for settype(): lowering path, type checks, and runtime helpers." sidebar: - order: 432 + order: 436 --- ## `settype()` — internals @@ -33,6 +33,12 @@ function settype(mixed $var, string $type): bool - **Arity**: takes exactly 2 arguments. - **By-reference parameters**: `$var`. +## Eval interpreter (magician) + +- **Declaration**: [`crates/elephc-magician/src/interpreter/builtins/types/settype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/settype.rs) (`eval_builtin!`) +- **Dispatch hooks**: `values` +- **By-reference parameters**: `$var`. + ## Cross-references - [User reference for `settype()`](../../../php/builtins/type/settype.md) diff --git a/docs/php/builtins.md b/docs/php/builtins.md index 2549d7161d..e953762a18 100644 --- a/docs/php/builtins.md +++ b/docs/php/builtins.md @@ -7,437 +7,441 @@ sidebar: ## Builtins -| Function | Signature | Returns | -|---|---|---| -| [`array_all()`](./builtins/array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | -| [`array_any()`](./builtins/array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | -| [`array_chunk()`](./builtins/array/array_chunk.md) | `(array $array, int $length): array` | `array` | -| [`array_column()`](./builtins/array/array_column.md) | `(array $array, string $column_key): array` | `array` | -| [`array_combine()`](./builtins/array/array_combine.md) | `(array $keys, array $values): array` | `array` | -| [`array_diff()`](./builtins/array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_diff_assoc()`](./builtins/array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | -| [`array_diff_key()`](./builtins/array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_fill()`](./builtins/array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | -| [`array_fill_keys()`](./builtins/array/array_fill_keys.md) | `(array $keys, mixed $value): array` | `array` | -| [`array_filter()`](./builtins/array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | -| [`array_find()`](./builtins/array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | -| [`array_flip()`](./builtins/array/array_flip.md) | `(array $array): array` | `array` | -| [`array_intersect()`](./builtins/array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_intersect_assoc()`](./builtins/array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | -| [`array_intersect_key()`](./builtins/array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_is_list()`](./builtins/array/array_is_list.md) | `(mixed $array): bool` | `bool` | -| [`array_key_exists()`](./builtins/array/array_key_exists.md) | `(string $key, array $array): bool` | `bool` | -| [`array_key_first()`](./builtins/array/array_key_first.md) | `(array $array): mixed` | `mixed` | -| [`array_key_last()`](./builtins/array/array_key_last.md) | `(array $array): mixed` | `mixed` | -| [`array_keys()`](./builtins/array/array_keys.md) | `(array $array): array` | `array` | -| [`array_map()`](./builtins/array/array_map.md) | `(callable $callback, array $array, ...$arrays): array` | `array` | -| [`array_merge()`](./builtins/array/array_merge.md) | `(...$arrays): array` | `array` | -| [`array_merge_recursive()`](./builtins/array/array_merge_recursive.md) | `(...$arrays): array` | `array` | -| [`array_multisort()`](./builtins/array/array_multisort.md) | `(array $array1, int $array2): bool` | `bool` | -| [`array_pad()`](./builtins/array/array_pad.md) | `(array $array, int $length, mixed $value): array` | `array` | -| [`array_pop()`](./builtins/array/array_pop.md) | `(array $array): mixed` | `mixed` | -| [`array_product()`](./builtins/array/array_product.md) | `(array $array): int` | `int` | -| [`array_push()`](./builtins/array/array_push.md) | `(array $array, ...$values): void` | `void` | -| [`array_rand()`](./builtins/array/array_rand.md) | `(array $array): int` | `int` | -| [`array_reduce()`](./builtins/array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | -| [`array_replace()`](./builtins/array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | -| [`array_replace_recursive()`](./builtins/array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | -| [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array): array` | `array` | -| [`array_search()`](./builtins/array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | -| [`array_shift()`](./builtins/array/array_shift.md) | `(array $array): mixed` | `mixed` | -| [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | -| [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | -| [`array_sum()`](./builtins/array/array_sum.md) | `(array $array): int` | `int` | -| [`array_udiff()`](./builtins/array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | -| [`array_uintersect()`](./builtins/array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | -| [`array_unique()`](./builtins/array/array_unique.md) | `(array $array): array` | `array` | -| [`array_unshift()`](./builtins/array/array_unshift.md) | `(array $array, ...$values): int` | `int` | -| [`array_values()`](./builtins/array/array_values.md) | `(array $array): array` | `array` | -| [`array_walk()`](./builtins/array/array_walk.md) | `(array $array, callable $callback): void` | `void` | -| [`array_walk_recursive()`](./builtins/array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | -| [`arsort()`](./builtins/array/arsort.md) | `(array $array): bool` | `bool` | -| [`asort()`](./builtins/array/asort.md) | `(array $array): bool` | `bool` | -| [`call_user_func()`](./builtins/array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | -| [`call_user_func_array()`](./builtins/array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | -| [`count()`](./builtins/array/count.md) | `(array $value, int $mode = 0): int` | `int` | -| [`in_array()`](./builtins/array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | -| [`krsort()`](./builtins/array/krsort.md) | `(array $array): bool` | `bool` | -| [`ksort()`](./builtins/array/ksort.md) | `(array $array): bool` | `bool` | -| [`natcasesort()`](./builtins/array/natcasesort.md) | `(array $array): bool` | `bool` | -| [`natsort()`](./builtins/array/natsort.md) | `(array $array): bool` | `bool` | -| [`range()`](./builtins/array/range.md) | `(mixed $start, mixed $end): array` | `array` | -| [`rsort()`](./builtins/array/rsort.md) | `(array $array): bool` | `bool` | -| [`shuffle()`](./builtins/array/shuffle.md) | `(array $array): bool` | `bool` | -| [`sort()`](./builtins/array/sort.md) | `(array $array): bool` | `bool` | -| [`uasort()`](./builtins/array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`uksort()`](./builtins/array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`usort()`](./builtins/array/usort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`buffer_free()`](./builtins/buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | -| [`buffer_len()`](./builtins/buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | -| [`class_alias()`](./builtins/class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | -| [`class_attribute_args()`](./builtins/class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | -| [`class_attribute_names()`](./builtins/class/class_attribute_names.md) | `(string $class_name): array` | `array` | -| [`class_exists()`](./builtins/class/class_exists.md) | `(string $class, bool $autoload = true): bool` | `bool` | -| [`class_get_attributes()`](./builtins/class/class_get_attributes.md) | `(string $class_name): array` | `array` | -| [`class_implements()`](./builtins/class/class_implements.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`class_parents()`](./builtins/class/class_parents.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`class_uses()`](./builtins/class/class_uses.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`enum_exists()`](./builtins/class/enum_exists.md) | `(string $enum, bool $autoload = true): bool` | `bool` | -| [`function_exists()`](./builtins/class/function_exists.md) | `(string $function): bool` | `bool` | -| [`get_class()`](./builtins/class/get_class.md) | `(object $object = null): string` | `string` | -| [`get_declared_classes()`](./builtins/class/get_declared_classes.md) | `(): array` | `array` | -| [`get_declared_interfaces()`](./builtins/class/get_declared_interfaces.md) | `(): array` | `array` | -| [`get_declared_traits()`](./builtins/class/get_declared_traits.md) | `(): array` | `array` | -| [`get_parent_class()`](./builtins/class/get_parent_class.md) | `(mixed $object_or_class = null): string` | `string` | -| [`interface_exists()`](./builtins/class/interface_exists.md) | `(string $interface, bool $autoload = true): bool` | `bool` | -| [`is_a()`](./builtins/class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string = false): bool` | `bool` | -| [`is_subclass_of()`](./builtins/class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string = true): bool` | `bool` | -| [`trait_exists()`](./builtins/class/trait_exists.md) | `(string $trait, bool $autoload = true): bool` | `bool` | -| [`checkdate()`](./builtins/date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | -| [`date()`](./builtins/date/date.md) | `(string $format, int $timestamp = null): string` | `string` | -| [`date_default_timezone_get()`](./builtins/date/date_default_timezone_get.md) | `(): string` | `string` | -| [`date_default_timezone_set()`](./builtins/date/date_default_timezone_set.md) | `(string $timezoneId): bool` | `bool` | -| [`getdate()`](./builtins/date/getdate.md) | `(int $timestamp = null): array` | `array` | -| [`gmdate()`](./builtins/date/gmdate.md) | `(string $format, int $timestamp = null): string` | `string` | -| [`gmmktime()`](./builtins/date/gmmktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | -| [`hrtime()`](./builtins/date/hrtime.md) | `(bool $as_number = false): mixed` | `mixed` | -| [`localtime()`](./builtins/date/localtime.md) | `(int $timestamp = -1, bool $associative = false): array` | `array` | -| [`microtime()`](./builtins/date/microtime.md) | `(bool $as_float = false): mixed` | `mixed` | -| [`mktime()`](./builtins/date/mktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | -| [`strtotime()`](./builtins/date/strtotime.md) | `(string $datetime, int $baseTimestamp = null): mixed` | `mixed` | -| [`time()`](./builtins/date/time.md) | `(): int` | `int` | -| [`basename()`](./builtins/filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | -| [`chdir()`](./builtins/filesystem/chdir.md) | `(string $directory): bool` | `bool` | -| [`chgrp()`](./builtins/filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | -| [`chmod()`](./builtins/filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | -| [`chown()`](./builtins/filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | -| [`clearstatcache()`](./builtins/filesystem/clearstatcache.md) | `(bool $clear_realpath_cache = false, string $filename = ''): void` | `void` | -| [`copy()`](./builtins/filesystem/copy.md) | `(string $from, string $to): bool` | `bool` | -| [`dirname()`](./builtins/filesystem/dirname.md) | `(string $path, int $levels = 1): string` | `string` | -| [`disk_free_space()`](./builtins/filesystem/disk_free_space.md) | `(string $directory): float` | `float` | -| [`disk_total_space()`](./builtins/filesystem/disk_total_space.md) | `(string $directory): float` | `float` | -| [`file_exists()`](./builtins/filesystem/file_exists.md) | `(string $filename): bool` | `bool` | -| [`fileatime()`](./builtins/filesystem/fileatime.md) | `(string $filename): mixed` | `mixed` | -| [`filectime()`](./builtins/filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | -| [`filegroup()`](./builtins/filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | -| [`fileinode()`](./builtins/filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | -| [`filemtime()`](./builtins/filesystem/filemtime.md) | `(string $filename): int` | `int` | -| [`fileowner()`](./builtins/filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | -| [`fileperms()`](./builtins/filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | -| [`filesize()`](./builtins/filesystem/filesize.md) | `(string $filename): int` | `int` | -| [`filetype()`](./builtins/filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | -| [`fnmatch()`](./builtins/filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | -| [`getcwd()`](./builtins/filesystem/getcwd.md) | `(): string` | `string` | -| [`getenv()`](./builtins/filesystem/getenv.md) | `(string $name): mixed` | `mixed` | -| [`glob()`](./builtins/filesystem/glob.md) | `(string $pattern): array` | `array` | -| [`is_dir()`](./builtins/filesystem/is_dir.md) | `(string $filename): bool` | `bool` | -| [`is_executable()`](./builtins/filesystem/is_executable.md) | `(string $filename): bool` | `bool` | -| [`is_file()`](./builtins/filesystem/is_file.md) | `(string $filename): bool` | `bool` | -| [`is_link()`](./builtins/filesystem/is_link.md) | `(string $filename): bool` | `bool` | -| [`is_readable()`](./builtins/filesystem/is_readable.md) | `(string $filename): bool` | `bool` | -| [`is_writable()`](./builtins/filesystem/is_writable.md) | `(string $filename): bool` | `bool` | -| [`is_writeable()`](./builtins/filesystem/is_writeable.md) | `(string $filename): bool` | `bool` | -| [`lchgrp()`](./builtins/filesystem/lchgrp.md) | `(string $filename, string $group): bool` | `bool` | -| [`lchown()`](./builtins/filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | -| [`link()`](./builtins/filesystem/link.md) | `(string $target, string $link): bool` | `bool` | -| [`linkinfo()`](./builtins/filesystem/linkinfo.md) | `(string $path): int` | `int` | -| [`lstat()`](./builtins/filesystem/lstat.md) | `(string $filename): mixed` | `mixed` | -| [`mkdir()`](./builtins/filesystem/mkdir.md) | `(string $directory): bool` | `bool` | -| [`pathinfo()`](./builtins/filesystem/pathinfo.md) | `(string $path, int $flags = 15): array` | `array` | -| [`putenv()`](./builtins/filesystem/putenv.md) | `(string $assignment): bool` | `bool` | -| [`readfile()`](./builtins/filesystem/readfile.md) | `(string $filename): mixed` | `mixed` | -| [`readlink()`](./builtins/filesystem/readlink.md) | `(string $path): mixed` | `mixed` | -| [`realpath()`](./builtins/filesystem/realpath.md) | `(string $path): mixed` | `mixed` | -| [`realpath_cache_get()`](./builtins/filesystem/realpath_cache_get.md) | `(): array` | `array` | -| [`realpath_cache_size()`](./builtins/filesystem/realpath_cache_size.md) | `(): int` | `int` | -| [`rename()`](./builtins/filesystem/rename.md) | `(string $from, string $to): bool` | `bool` | -| [`rmdir()`](./builtins/filesystem/rmdir.md) | `(string $directory): bool` | `bool` | -| [`scandir()`](./builtins/filesystem/scandir.md) | `(string $directory): array` | `array` | -| [`stat()`](./builtins/filesystem/stat.md) | `(string $filename): mixed` | `mixed` | -| [`symlink()`](./builtins/filesystem/symlink.md) | `(string $target, string $link): bool` | `bool` | -| [`sys_get_temp_dir()`](./builtins/filesystem/sys_get_temp_dir.md) | `(): string` | `string` | -| [`tempnam()`](./builtins/filesystem/tempnam.md) | `(string $directory, string $prefix): string` | `string` | -| [`tmpfile()`](./builtins/filesystem/tmpfile.md) | `(): mixed` | `mixed` | -| [`touch()`](./builtins/filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | -| [`umask()`](./builtins/filesystem/umask.md) | `(int $mask = null): int` | `int` | -| [`unlink()`](./builtins/filesystem/unlink.md) | `(string $filename): bool` | `bool` | -| [`closedir()`](./builtins/io/closedir.md) | `(resource $dir_handle): void` | `void` | -| [`fclose()`](./builtins/io/fclose.md) | `(resource $stream): bool` | `bool` | -| [`fdatasync()`](./builtins/io/fdatasync.md) | `(resource $stream): bool` | `bool` | -| [`feof()`](./builtins/io/feof.md) | `(resource $stream): bool` | `bool` | -| [`fflush()`](./builtins/io/fflush.md) | `(resource $stream): bool` | `bool` | -| [`fgetc()`](./builtins/io/fgetc.md) | `(resource $stream): mixed` | `mixed` | -| [`fgetcsv()`](./builtins/io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | -| [`fgets()`](./builtins/io/fgets.md) | `(resource $stream): mixed` | `mixed` | -| [`file()`](./builtins/io/file.md) | `(string $filename): array` | `array` | -| [`file_get_contents()`](./builtins/io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | -| [`file_put_contents()`](./builtins/io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | -| [`flock()`](./builtins/io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | -| [`fopen()`](./builtins/io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | -| [`fpassthru()`](./builtins/io/fpassthru.md) | `(resource $stream): int` | `int` | -| [`fprintf()`](./builtins/io/fprintf.md) | `(resource $stream, string $format, ...$values): int` | `int` | -| [`fputcsv()`](./builtins/io/fputcsv.md) | `(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int` | `int` | -| [`fread()`](./builtins/io/fread.md) | `(resource $stream, int $length): string` | `string` | -| [`fscanf()`](./builtins/io/fscanf.md) | `(resource $stream, string $format, ...$vars): array` | `array` | -| [`fseek()`](./builtins/io/fseek.md) | `(resource $stream, int $offset, int $whence = 0): int` | `int` | -| [`fstat()`](./builtins/io/fstat.md) | `(resource $stream): mixed` | `mixed` | -| [`fsync()`](./builtins/io/fsync.md) | `(resource $stream): bool` | `bool` | -| [`ftell()`](./builtins/io/ftell.md) | `(resource $stream): int` | `int` | -| [`ftruncate()`](./builtins/io/ftruncate.md) | `(resource $stream, int $size): bool` | `bool` | -| [`fwrite()`](./builtins/io/fwrite.md) | `(resource $stream, string $data): int` | `int` | -| [`gethostbyaddr()`](./builtins/io/gethostbyaddr.md) | `(string $ip): mixed` | `mixed` | -| [`gethostbyname()`](./builtins/io/gethostbyname.md) | `(string $hostname): string` | `string` | -| [`gethostname()`](./builtins/io/gethostname.md) | `(): string` | `string` | -| [`getprotobyname()`](./builtins/io/getprotobyname.md) | `(string $protocol): mixed` | `mixed` | -| [`getprotobynumber()`](./builtins/io/getprotobynumber.md) | `(int $protocol): mixed` | `mixed` | -| [`getservbyname()`](./builtins/io/getservbyname.md) | `(string $service, string $protocol): mixed` | `mixed` | -| [`getservbyport()`](./builtins/io/getservbyport.md) | `(int $port, string $protocol): mixed` | `mixed` | -| [`hash_file()`](./builtins/io/hash_file.md) | `(string $algo, string $filename, bool $binary = false): mixed` | `mixed` | -| [`opendir()`](./builtins/io/opendir.md) | `(string $directory): mixed` | `mixed` | -| [`readdir()`](./builtins/io/readdir.md) | `(resource $dir_handle): mixed` | `mixed` | -| [`rewind()`](./builtins/io/rewind.md) | `(resource $stream): bool` | `bool` | -| [`rewinddir()`](./builtins/io/rewinddir.md) | `(resource $dir_handle): void` | `void` | -| [`stream_bucket_make_writeable()`](./builtins/io/stream_bucket_make_writeable.md) | `(mixed $brigade): mixed` | `mixed` | -| [`stream_bucket_new()`](./builtins/io/stream_bucket_new.md) | `(resource $stream, string $buffer): mixed` | `mixed` | -| [`stream_context_create()`](./builtins/io/stream_context_create.md) | `(array $options = null, array $params = null): mixed` | `mixed` | -| [`stream_context_get_default()`](./builtins/io/stream_context_get_default.md) | `(array $options = null): mixed` | `mixed` | -| [`stream_context_get_options()`](./builtins/io/stream_context_get_options.md) | `(resource $context): array` | `array` | -| [`stream_context_get_params()`](./builtins/io/stream_context_get_params.md) | `(resource $context): array` | `array` | -| [`stream_context_set_default()`](./builtins/io/stream_context_set_default.md) | `(array $options): mixed` | `mixed` | -| [`stream_context_set_option()`](./builtins/io/stream_context_set_option.md) | `(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool` | `bool` | -| [`stream_context_set_params()`](./builtins/io/stream_context_set_params.md) | `(resource $context, array $params): bool` | `bool` | -| [`stream_copy_to_stream()`](./builtins/io/stream_copy_to_stream.md) | `(resource $from, resource $to, int $length = null, int $offset = -1): mixed` | `mixed` | -| [`stream_filter_register()`](./builtins/io/stream_filter_register.md) | `(string $filter_name, string $class): bool` | `bool` | -| [`stream_filter_remove()`](./builtins/io/stream_filter_remove.md) | `(resource $stream_filter): bool` | `bool` | -| [`stream_get_contents()`](./builtins/io/stream_get_contents.md) | `(resource $stream, int $length = null, int $offset = -1): mixed` | `mixed` | -| [`stream_get_filters()`](./builtins/io/stream_get_filters.md) | `(): array` | `array` | -| [`stream_get_line()`](./builtins/io/stream_get_line.md) | `(resource $stream, int $length, string $ending = ''): string` | `string` | -| [`stream_get_meta_data()`](./builtins/io/stream_get_meta_data.md) | `(resource $stream): array` | `array` | -| [`stream_get_transports()`](./builtins/io/stream_get_transports.md) | `(): array` | `array` | -| [`stream_get_wrappers()`](./builtins/io/stream_get_wrappers.md) | `(): array` | `array` | -| [`stream_is_local()`](./builtins/io/stream_is_local.md) | `(resource $stream): bool` | `bool` | -| [`stream_isatty()`](./builtins/io/stream_isatty.md) | `(resource $stream): bool` | `bool` | -| [`stream_resolve_include_path()`](./builtins/io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | -| [`stream_select()`](./builtins/io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | -| [`stream_set_blocking()`](./builtins/io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | -| [`stream_set_chunk_size()`](./builtins/io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_set_read_buffer()`](./builtins/io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_set_timeout()`](./builtins/io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | -| [`stream_set_write_buffer()`](./builtins/io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_socket_accept()`](./builtins/io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | -| [`stream_socket_client()`](./builtins/io/stream_socket_client.md) | `(string $address): mixed` | `mixed` | -| [`stream_socket_enable_crypto()`](./builtins/io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | `bool` | -| [`stream_socket_get_name()`](./builtins/io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | -| [`stream_socket_pair()`](./builtins/io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | -| [`stream_socket_recvfrom()`](./builtins/io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | -| [`stream_socket_sendto()`](./builtins/io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags = 0, string $address = ''): mixed` | `mixed` | -| [`stream_socket_server()`](./builtins/io/stream_socket_server.md) | `(string $address): mixed` | `mixed` | -| [`stream_socket_shutdown()`](./builtins/io/stream_socket_shutdown.md) | `(resource $stream, int $mode): bool` | `bool` | -| [`stream_supports_lock()`](./builtins/io/stream_supports_lock.md) | `(resource $stream): bool` | `bool` | -| [`stream_wrapper_register()`](./builtins/io/stream_wrapper_register.md) | `(string $protocol, string $class, int $flags = 0): bool` | `bool` | -| [`stream_wrapper_restore()`](./builtins/io/stream_wrapper_restore.md) | `(string $protocol): bool` | `bool` | -| [`stream_wrapper_unregister()`](./builtins/io/stream_wrapper_unregister.md) | `(string $protocol): bool` | `bool` | -| [`vfprintf()`](./builtins/io/vfprintf.md) | `(resource $stream, string $format, array $values): int` | `int` | -| [`json_decode()`](./builtins/json/json_decode.md) | `(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed` | `mixed` | -| [`json_encode()`](./builtins/json/json_encode.md) | `(mixed $value, int $flags = 0, int $depth = 512): string` | `string` | -| [`json_last_error()`](./builtins/json/json_last_error.md) | `(): int` | `int` | -| [`json_last_error_msg()`](./builtins/json/json_last_error_msg.md) | `(): string` | `string` | -| [`json_validate()`](./builtins/json/json_validate.md) | `(string $json, int $depth = 512, int $flags = 0): bool` | `bool` | -| [`abs()`](./builtins/math/abs.md) | `(int $num): mixed` | `mixed` | -| [`acos()`](./builtins/math/acos.md) | `(float $num): float` | `float` | -| [`asin()`](./builtins/math/asin.md) | `(float $num): float` | `float` | -| [`atan()`](./builtins/math/atan.md) | `(float $num): float` | `float` | -| [`atan2()`](./builtins/math/atan2.md) | `(float $y, float $x): float` | `float` | -| [`ceil()`](./builtins/math/ceil.md) | `(float $num): float` | `float` | -| [`clamp()`](./builtins/math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | -| [`cos()`](./builtins/math/cos.md) | `(float $num): float` | `float` | -| [`cosh()`](./builtins/math/cosh.md) | `(float $num): float` | `float` | -| [`deg2rad()`](./builtins/math/deg2rad.md) | `(float $num): float` | `float` | -| [`exp()`](./builtins/math/exp.md) | `(float $num): float` | `float` | -| [`fdiv()`](./builtins/math/fdiv.md) | `(float $num1, float $num2): float` | `float` | -| [`floor()`](./builtins/math/floor.md) | `(float $num): float` | `float` | -| [`fmod()`](./builtins/math/fmod.md) | `(float $num1, float $num2): float` | `float` | -| [`hypot()`](./builtins/math/hypot.md) | `(float $x, float $y): float` | `float` | -| [`intdiv()`](./builtins/math/intdiv.md) | `(int $num1, int $num2): int` | `int` | -| [`is_finite()`](./builtins/math/is_finite.md) | `(float $num): bool` | `bool` | -| [`is_infinite()`](./builtins/math/is_infinite.md) | `(float $num): bool` | `bool` | -| [`is_nan()`](./builtins/math/is_nan.md) | `(float $num): bool` | `bool` | -| [`log()`](./builtins/math/log.md) | `(float $num, float $base = 2.718281828459045): float` | `float` | -| [`log10()`](./builtins/math/log10.md) | `(float $num): float` | `float` | -| [`log2()`](./builtins/math/log2.md) | `(float $num): float` | `float` | -| [`max()`](./builtins/math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | -| [`min()`](./builtins/math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | -| [`mt_rand()`](./builtins/math/mt_rand.md) | `(int $min, int $max): int` | `int` | -| [`pi()`](./builtins/math/pi.md) | `(): float` | `float` | -| [`pow()`](./builtins/math/pow.md) | `(float $num, float $exponent): float` | `float` | -| [`rad2deg()`](./builtins/math/rad2deg.md) | `(float $num): float` | `float` | -| [`rand()`](./builtins/math/rand.md) | `(int $min, int $max): int` | `int` | -| [`random_int()`](./builtins/math/random_int.md) | `(int $min, int $max): int` | `int` | -| [`round()`](./builtins/math/round.md) | `(float $num, int $precision = 0): float` | `float` | -| [`sin()`](./builtins/math/sin.md) | `(float $num): float` | `float` | -| [`sinh()`](./builtins/math/sinh.md) | `(float $num): float` | `float` | -| [`sqrt()`](./builtins/math/sqrt.md) | `(float $num): float` | `float` | -| [`tan()`](./builtins/math/tan.md) | `(float $num): float` | `float` | -| [`tanh()`](./builtins/math/tanh.md) | `(float $num): float` | `float` | -| [`buffer_new()`](./builtins/misc/buffer_new.md) | `(int $length): mixed` | `mixed` | -| [`define()`](./builtins/misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | -| [`defined()`](./builtins/misc/defined.md) | `(string $constant_name): bool` | `bool` | -| [`empty()`](./builtins/misc/empty.md) | `(mixed $value): bool` | `bool` | -| [`header()`](./builtins/misc/header.md) | `(string $header, bool $replace = true, int $response_code = 0): void` | `void` | -| [`http_response_code()`](./builtins/misc/http_response_code.md) | `(int $response_code = 0): int` | `int` | -| [`isset()`](./builtins/misc/isset.md) | `(mixed $var, ...$vars): bool` | `bool` | -| [`php_uname()`](./builtins/misc/php_uname.md) | `(string $mode = 'a'): string` | `string` | -| [`phpversion()`](./builtins/misc/phpversion.md) | `(): string` | `string` | -| [`print_r()`](./builtins/misc/print_r.md) | `(mixed $value, bool $return = false): mixed` | `mixed` | -| [`serialize()`](./builtins/misc/serialize.md) | `(mixed $value): string` | `string` | -| [`unserialize()`](./builtins/misc/unserialize.md) | `(string $data, mixed $options = []): mixed` | `mixed` | -| [`unset()`](./builtins/misc/unset.md) | `(mixed $var, ...$vars): void` | `void` | -| [`var_dump()`](./builtins/misc/var_dump.md) | `(mixed $value, ...$values): void` | `void` | -| [`ptr()`](./builtins/pointer/ptr.md) | `(mixed $value): mixed` | `mixed` | -| [`ptr_get()`](./builtins/pointer/ptr_get.md) | `(pointer $pointer): int` | `int` | -| [`ptr_is_null()`](./builtins/pointer/ptr_is_null.md) | `(pointer $pointer): bool` | `bool` | -| [`ptr_null()`](./builtins/pointer/ptr_null.md) | `(): mixed` | `mixed` | -| [`ptr_offset()`](./builtins/pointer/ptr_offset.md) | `(pointer $pointer, int $offset): mixed` | `mixed` | -| [`ptr_read16()`](./builtins/pointer/ptr_read16.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read32()`](./builtins/pointer/ptr_read32.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read8()`](./builtins/pointer/ptr_read8.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read_string()`](./builtins/pointer/ptr_read_string.md) | `(pointer $pointer, int $length): string` | `string` | -| [`ptr_set()`](./builtins/pointer/ptr_set.md) | `(pointer $pointer, mixed $value): void` | `void` | -| [`ptr_sizeof()`](./builtins/pointer/ptr_sizeof.md) | `(string $type): int` | `int` | -| [`ptr_write16()`](./builtins/pointer/ptr_write16.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write32()`](./builtins/pointer/ptr_write32.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write8()`](./builtins/pointer/ptr_write8.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write_string()`](./builtins/pointer/ptr_write_string.md) | `(pointer $pointer, string $string): int` | `int` | -| [`zval_free()`](./builtins/pointer/zval_free.md) | `(pointer $zval): void` | `void` | -| [`zval_pack()`](./builtins/pointer/zval_pack.md) | `(mixed $value): pointer` | `pointer` | -| [`zval_type()`](./builtins/pointer/zval_type.md) | `(pointer $zval): int` | `int` | -| [`zval_unpack()`](./builtins/pointer/zval_unpack.md) | `(pointer $zval): mixed` | `mixed` | -| [`die()`](./builtins/process/die.md) | `(int $status): void` | `void` | -| [`exec()`](./builtins/process/exec.md) | `(string $command): string` | `string` | -| [`exit()`](./builtins/process/exit.md) | `(int $status): void` | `void` | -| [`passthru()`](./builtins/process/passthru.md) | `(string $command): void` | `void` | -| [`pclose()`](./builtins/process/pclose.md) | `(resource $handle): int` | `int` | -| [`popen()`](./builtins/process/popen.md) | `(string $command, string $mode): mixed` | `mixed` | -| [`readline()`](./builtins/process/readline.md) | `(string $prompt = null): mixed` | `mixed` | -| [`shell_exec()`](./builtins/process/shell_exec.md) | `(string $command): string` | `string` | -| [`sleep()`](./builtins/process/sleep.md) | `(int $seconds): int` | `int` | -| [`system()`](./builtins/process/system.md) | `(string $command): string` | `string` | -| [`usleep()`](./builtins/process/usleep.md) | `(int $microseconds): void` | `void` | -| [`mb_ereg_match()`](./builtins/regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | -| [`preg_match()`](./builtins/regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | -| [`preg_match_all()`](./builtins/regex/preg_match_all.md) | `(string $pattern, string $subject): int` | `int` | -| [`preg_replace()`](./builtins/regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject): string` | `string` | -| [`preg_replace_callback()`](./builtins/regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject): string` | `string` | -| [`preg_split()`](./builtins/regex/preg_split.md) | `(string $pattern, string $subject, int $limit = -1, int $flags = 0): array` | `array` | -| [`iterator_apply()`](./builtins/spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args = null): int` | `int` | -| [`iterator_count()`](./builtins/spl/iterator_count.md) | `(traversable $iterator): int` | `int` | -| [`iterator_to_array()`](./builtins/spl/iterator_to_array.md) | `(traversable $iterator, bool $preserve_keys = true): array` | `array` | -| [`spl_autoload()`](./builtins/spl/spl_autoload.md) | `(string $class, string $file_extensions = null): void` | `void` | -| [`spl_autoload_call()`](./builtins/spl/spl_autoload_call.md) | `(string $class): void` | `void` | -| [`spl_autoload_extensions()`](./builtins/spl/spl_autoload_extensions.md) | `(string $file_extensions = null): string` | `string` | -| [`spl_autoload_functions()`](./builtins/spl/spl_autoload_functions.md) | `(): array` | `array` | -| [`spl_autoload_register()`](./builtins/spl/spl_autoload_register.md) | `(callable $callback = null, bool $throw = true, bool $prepend = false): bool` | `bool` | -| [`spl_autoload_unregister()`](./builtins/spl/spl_autoload_unregister.md) | `(callable $callback): bool` | `bool` | -| [`spl_classes()`](./builtins/spl/spl_classes.md) | `(): array` | `array` | -| [`spl_object_hash()`](./builtins/spl/spl_object_hash.md) | `(object $object): string` | `string` | -| [`spl_object_id()`](./builtins/spl/spl_object_id.md) | `(object $object): int` | `int` | -| [`fsockopen()`](./builtins/streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | -| [`pfsockopen()`](./builtins/streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | -| [`stream_bucket_append()`](./builtins/streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | -| [`stream_bucket_prepend()`](./builtins/streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | -| [`stream_filter_append()`](./builtins/streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | -| [`stream_filter_prepend()`](./builtins/streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | -| [`addslashes()`](./builtins/string/addslashes.md) | `(string $string): string` | `string` | -| [`base64_decode()`](./builtins/string/base64_decode.md) | `(string $string): string` | `string` | -| [`base64_encode()`](./builtins/string/base64_encode.md) | `(string $string): string` | `string` | -| [`bin2hex()`](./builtins/string/bin2hex.md) | `(string $string): string` | `string` | -| [`chop()`](./builtins/string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`chr()`](./builtins/string/chr.md) | `(int $codepoint): string` | `string` | -| [`crc32()`](./builtins/string/crc32.md) | `(string $string): int` | `int` | -| [`explode()`](./builtins/string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | -| [`grapheme_strrev()`](./builtins/string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | -| [`gzcompress()`](./builtins/string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | -| [`gzdeflate()`](./builtins/string/gzdeflate.md) | `(string $data, int $level = -1): string` | `string` | -| [`gzinflate()`](./builtins/string/gzinflate.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | -| [`gzuncompress()`](./builtins/string/gzuncompress.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | -| [`hash()`](./builtins/string/hash.md) | `(string $algo, string $data, bool $binary = false): string` | `string` | -| [`hash_algos()`](./builtins/string/hash_algos.md) | `(): array` | `array` | -| [`hash_copy()`](./builtins/string/hash_copy.md) | `(resource $context): mixed` | `mixed` | -| [`hash_equals()`](./builtins/string/hash_equals.md) | `(string $known_string, string $user_string): bool` | `bool` | -| [`hash_final()`](./builtins/string/hash_final.md) | `(resource $context, bool $binary = false): string` | `string` | -| [`hash_hmac()`](./builtins/string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary = false): string` | `string` | -| [`hash_init()`](./builtins/string/hash_init.md) | `(string $algo, int $flags = 0, string $key = ''): mixed` | `mixed` | -| [`hash_update()`](./builtins/string/hash_update.md) | `(resource $context, string $data): bool` | `bool` | -| [`hex2bin()`](./builtins/string/hex2bin.md) | `(string $string): string` | `string` | -| [`html_entity_decode()`](./builtins/string/html_entity_decode.md) | `(string $string): string` | `string` | -| [`htmlentities()`](./builtins/string/htmlentities.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | -| [`htmlspecialchars()`](./builtins/string/htmlspecialchars.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | -| [`implode()`](./builtins/string/implode.md) | `(string $separator, array $array = null): string` | `string` | -| [`inet_ntop()`](./builtins/string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | -| [`inet_pton()`](./builtins/string/inet_pton.md) | `(string $ip): mixed` | `mixed` | -| [`ip2long()`](./builtins/string/ip2long.md) | `(string $ip): mixed` | `mixed` | -| [`lcfirst()`](./builtins/string/lcfirst.md) | `(string $string): string` | `string` | -| [`long2ip()`](./builtins/string/long2ip.md) | `(int $ip): string` | `string` | -| [`ltrim()`](./builtins/string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`md5()`](./builtins/string/md5.md) | `(string $string, bool $binary = false): string` | `string` | -| [`nl2br()`](./builtins/string/nl2br.md) | `(string $string): string` | `string` | -| [`number_format()`](./builtins/string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | -| [`ord()`](./builtins/string/ord.md) | `(string $character): int` | `int` | -| [`printf()`](./builtins/string/printf.md) | `(string $format, ...$values): int` | `int` | -| [`rawurldecode()`](./builtins/string/rawurldecode.md) | `(string $string): string` | `string` | -| [`rawurlencode()`](./builtins/string/rawurlencode.md) | `(string $string): string` | `string` | -| [`rtrim()`](./builtins/string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`sha1()`](./builtins/string/sha1.md) | `(string $string, bool $binary = false): string` | `string` | -| [`sprintf()`](./builtins/string/sprintf.md) | `(string $format, ...$values): string` | `string` | -| [`sscanf()`](./builtins/string/sscanf.md) | `(string $string, string $format, ...$vars): array` | `array` | -| [`str_contains()`](./builtins/string/str_contains.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`str_ends_with()`](./builtins/string/str_ends_with.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`str_ireplace()`](./builtins/string/str_ireplace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | -| [`str_pad()`](./builtins/string/str_pad.md) | `(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string` | `string` | -| [`str_repeat()`](./builtins/string/str_repeat.md) | `(string $string, int $times): string` | `string` | -| [`str_replace()`](./builtins/string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | -| [`str_split()`](./builtins/string/str_split.md) | `(string $string, int $length = 1): array` | `array` | -| [`str_starts_with()`](./builtins/string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`strcasecmp()`](./builtins/string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | -| [`strcmp()`](./builtins/string/strcmp.md) | `(string $string1, string $string2): int` | `int` | -| [`stripslashes()`](./builtins/string/stripslashes.md) | `(string $string): string` | `string` | -| [`strlen()`](./builtins/string/strlen.md) | `(string $string): int` | `int` | -| [`strpos()`](./builtins/string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | -| [`strrev()`](./builtins/string/strrev.md) | `(string $string): string` | `string` | -| [`strrpos()`](./builtins/string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | -| [`strstr()`](./builtins/string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): string` | `string` | -| [`strtolower()`](./builtins/string/strtolower.md) | `(string $string): string` | `string` | -| [`strtoupper()`](./builtins/string/strtoupper.md) | `(string $string): string` | `string` | -| [`substr()`](./builtins/string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | -| [`substr_replace()`](./builtins/string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | -| [`trim()`](./builtins/string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`ucfirst()`](./builtins/string/ucfirst.md) | `(string $string): string` | `string` | -| [`ucwords()`](./builtins/string/ucwords.md) | `(string $string, string $separators = ' \t\r\n\x0c\x0b'): string` | `string` | -| [`urldecode()`](./builtins/string/urldecode.md) | `(string $string): string` | `string` | -| [`urlencode()`](./builtins/string/urlencode.md) | `(string $string): string` | `string` | -| [`vprintf()`](./builtins/string/vprintf.md) | `(string $format, array $values): int` | `int` | -| [`vsprintf()`](./builtins/string/vsprintf.md) | `(string $format, array $values): string` | `string` | -| [`wordwrap()`](./builtins/string/wordwrap.md) | `(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string` | `string` | -| [`boolval()`](./builtins/type/boolval.md) | `(mixed $value): bool` | `bool` | -| [`ctype_alnum()`](./builtins/type/ctype_alnum.md) | `(string $text): bool` | `bool` | -| [`ctype_alpha()`](./builtins/type/ctype_alpha.md) | `(string $text): bool` | `bool` | -| [`ctype_digit()`](./builtins/type/ctype_digit.md) | `(string $text): bool` | `bool` | -| [`ctype_space()`](./builtins/type/ctype_space.md) | `(string $text): bool` | `bool` | -| [`floatval()`](./builtins/type/floatval.md) | `(mixed $value): float` | `float` | -| [`get_resource_id()`](./builtins/type/get_resource_id.md) | `(resource $resource): int` | `int` | -| [`get_resource_type()`](./builtins/type/get_resource_type.md) | `(resource $resource): string` | `string` | -| [`gettype()`](./builtins/type/gettype.md) | `(mixed $value): string` | `string` | -| [`intval()`](./builtins/type/intval.md) | `(mixed $value): int` | `int` | -| [`is_array()`](./builtins/type/is_array.md) | `(mixed $value): bool` | `bool` | -| [`is_bool()`](./builtins/type/is_bool.md) | `(mixed $value): bool` | `bool` | -| [`is_callable()`](./builtins/type/is_callable.md) | `(mixed $value): bool` | `bool` | -| [`is_float()`](./builtins/type/is_float.md) | `(mixed $value): bool` | `bool` | -| [`is_int()`](./builtins/type/is_int.md) | `(mixed $value): bool` | `bool` | -| [`is_iterable()`](./builtins/type/is_iterable.md) | `(mixed $value): bool` | `bool` | -| [`is_null()`](./builtins/type/is_null.md) | `(mixed $value): bool` | `bool` | -| [`is_numeric()`](./builtins/type/is_numeric.md) | `(mixed $value): bool` | `bool` | -| [`is_object()`](./builtins/type/is_object.md) | `(mixed $value): bool` | `bool` | -| [`is_resource()`](./builtins/type/is_resource.md) | `(mixed $value): bool` | `bool` | -| [`is_scalar()`](./builtins/type/is_scalar.md) | `(mixed $value): bool` | `bool` | -| [`is_string()`](./builtins/type/is_string.md) | `(mixed $value): bool` | `bool` | -| [`settype()`](./builtins/type/settype.md) | `(mixed $var, string $type): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`array_all()`](./builtins/array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | +| [`array_any()`](./builtins/array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | +| [`array_chunk()`](./builtins/array/array_chunk.md) | `(array $array, int $length): array` | `array` | ✓ | ✓ | +| [`array_column()`](./builtins/array/array_column.md) | `(array $array, string $column_key): array` | `array` | ✓ | ✓ | +| [`array_combine()`](./builtins/array/array_combine.md) | `(array $keys, array $values): array` | `array` | ✓ | ✓ | +| [`array_diff()`](./builtins/array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_diff_assoc()`](./builtins/array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | +| [`array_diff_key()`](./builtins/array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_fill()`](./builtins/array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_fill_keys()`](./builtins/array/array_fill_keys.md) | `(array $keys, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_filter()`](./builtins/array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | ✓ | ✓ | +| [`array_find()`](./builtins/array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | ✓ | — | +| [`array_flip()`](./builtins/array/array_flip.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_intersect()`](./builtins/array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_intersect_assoc()`](./builtins/array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | +| [`array_intersect_key()`](./builtins/array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_is_list()`](./builtins/array/array_is_list.md) | `(mixed $array): bool` | `bool` | ✓ | — | +| [`array_key_exists()`](./builtins/array/array_key_exists.md) | `(string $key, array $array): bool` | `bool` | ✓ | ✓ | +| [`array_key_first()`](./builtins/array/array_key_first.md) | `(array $array): mixed` | `mixed` | ✓ | — | +| [`array_key_last()`](./builtins/array/array_key_last.md) | `(array $array): mixed` | `mixed` | ✓ | — | +| [`array_keys()`](./builtins/array/array_keys.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_map()`](./builtins/array/array_map.md) | `(callable $callback, array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_merge()`](./builtins/array/array_merge.md) | `(...$arrays): array` | `array` | ✓ | ✓ | +| [`array_merge_recursive()`](./builtins/array/array_merge_recursive.md) | `(...$arrays): array` | `array` | ✓ | — | +| [`array_multisort()`](./builtins/array/array_multisort.md) | `(array $array1, int $array2): bool` | `bool` | ✓ | — | +| [`array_pad()`](./builtins/array/array_pad.md) | `(array $array, int $length, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_pop()`](./builtins/array/array_pop.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_product()`](./builtins/array/array_product.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_push()`](./builtins/array/array_push.md) | `(array $array, ...$values): void` | `void` | ✓ | ✓ | +| [`array_rand()`](./builtins/array/array_rand.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_reduce()`](./builtins/array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | ✓ | ✓ | +| [`array_replace()`](./builtins/array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | +| [`array_replace_recursive()`](./builtins/array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | +| [`array_reverse()`](./builtins/array/array_reverse.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_search()`](./builtins/array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | +| [`array_shift()`](./builtins/array/array_shift.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_slice()`](./builtins/array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_splice()`](./builtins/array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_sum()`](./builtins/array/array_sum.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_udiff()`](./builtins/array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | +| [`array_uintersect()`](./builtins/array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | +| [`array_unique()`](./builtins/array/array_unique.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_unshift()`](./builtins/array/array_unshift.md) | `(array $array, ...$values): int` | `int` | ✓ | ✓ | +| [`array_values()`](./builtins/array/array_values.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_walk()`](./builtins/array/array_walk.md) | `(array $array, callable $callback): void` | `void` | ✓ | ✓ | +| [`array_walk_recursive()`](./builtins/array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | ✓ | — | +| [`arsort()`](./builtins/array/arsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`asort()`](./builtins/array/asort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`call_user_func()`](./builtins/array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | ✓ | ✓ | +| [`call_user_func_array()`](./builtins/array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | ✓ | ✓ | +| [`count()`](./builtins/array/count.md) | `(array $value, int $mode = 0): int` | `int` | ✓ | ✓ | +| [`in_array()`](./builtins/array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | ✓ | ✓ | +| [`krsort()`](./builtins/array/krsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`ksort()`](./builtins/array/ksort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`natcasesort()`](./builtins/array/natcasesort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`natsort()`](./builtins/array/natsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`range()`](./builtins/array/range.md) | `(mixed $start, mixed $end): array` | `array` | ✓ | ✓ | +| [`rsort()`](./builtins/array/rsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`shuffle()`](./builtins/array/shuffle.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`sort()`](./builtins/array/sort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`uasort()`](./builtins/array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`uksort()`](./builtins/array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`usort()`](./builtins/array/usort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`buffer_free()`](./builtins/buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`buffer_len()`](./builtins/buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | ✓ | ✓ | +| [`class_alias()`](./builtins/class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`class_attribute_args()`](./builtins/class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | ✓ | ✓ | +| [`class_attribute_names()`](./builtins/class/class_attribute_names.md) | `(string $class_name): array` | `array` | ✓ | ✓ | +| [`class_exists()`](./builtins/class/class_exists.md) | `(string $class, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`class_get_attributes()`](./builtins/class/class_get_attributes.md) | `(string $class_name): array` | `array` | ✓ | ✓ | +| [`class_implements()`](./builtins/class/class_implements.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`class_parents()`](./builtins/class/class_parents.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`class_uses()`](./builtins/class/class_uses.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`enum_exists()`](./builtins/class/enum_exists.md) | `(string $enum, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`function_exists()`](./builtins/class/function_exists.md) | `(string $function): bool` | `bool` | ✓ | ✓ | +| [`get_called_class()`](./builtins/class/get_called_class.md) | `(): mixed` | `mixed` | — | ✓ | +| [`get_class()`](./builtins/class/get_class.md) | `(object $object = null): string` | `string` | ✓ | ✓ | +| [`get_class_methods()`](./builtins/class/get_class_methods.md) | `(mixed $object_or_class): mixed` | `mixed` | — | ✓ | +| [`get_class_vars()`](./builtins/class/get_class_vars.md) | `(mixed $class): mixed` | `mixed` | — | ✓ | +| [`get_declared_classes()`](./builtins/class/get_declared_classes.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_declared_interfaces()`](./builtins/class/get_declared_interfaces.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_declared_traits()`](./builtins/class/get_declared_traits.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_object_vars()`](./builtins/class/get_object_vars.md) | `(mixed $object): mixed` | `mixed` | — | ✓ | +| [`get_parent_class()`](./builtins/class/get_parent_class.md) | `(mixed $object_or_class = null): string` | `string` | ✓ | ✓ | +| [`interface_exists()`](./builtins/class/interface_exists.md) | `(string $interface, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`is_a()`](./builtins/class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string = false): bool` | `bool` | ✓ | ✓ | +| [`is_subclass_of()`](./builtins/class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string = true): bool` | `bool` | ✓ | ✓ | +| [`trait_exists()`](./builtins/class/trait_exists.md) | `(string $trait, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`checkdate()`](./builtins/date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | ✓ | ✓ | +| [`date()`](./builtins/date/date.md) | `(string $format, int $timestamp = null): string` | `string` | ✓ | ✓ | +| [`date_default_timezone_get()`](./builtins/date/date_default_timezone_get.md) | `(): string` | `string` | ✓ | ✓ | +| [`date_default_timezone_set()`](./builtins/date/date_default_timezone_set.md) | `(string $timezoneId): bool` | `bool` | ✓ | ✓ | +| [`getdate()`](./builtins/date/getdate.md) | `(int $timestamp = null): array` | `array` | ✓ | ✓ | +| [`gmdate()`](./builtins/date/gmdate.md) | `(string $format, int $timestamp = null): string` | `string` | ✓ | ✓ | +| [`gmmktime()`](./builtins/date/gmmktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | ✓ | ✓ | +| [`hrtime()`](./builtins/date/hrtime.md) | `(bool $as_number = false): mixed` | `mixed` | ✓ | ✓ | +| [`localtime()`](./builtins/date/localtime.md) | `(int $timestamp = -1, bool $associative = false): array` | `array` | ✓ | ✓ | +| [`microtime()`](./builtins/date/microtime.md) | `(bool $as_float = false): mixed` | `mixed` | ✓ | ✓ | +| [`mktime()`](./builtins/date/mktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | ✓ | ✓ | +| [`strtotime()`](./builtins/date/strtotime.md) | `(string $datetime, int $baseTimestamp = null): mixed` | `mixed` | ✓ | ✓ | +| [`time()`](./builtins/date/time.md) | `(): int` | `int` | ✓ | ✓ | +| [`basename()`](./builtins/filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | ✓ | ✓ | +| [`chdir()`](./builtins/filesystem/chdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`chgrp()`](./builtins/filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`chmod()`](./builtins/filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | ✓ | ✓ | +| [`chown()`](./builtins/filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`clearstatcache()`](./builtins/filesystem/clearstatcache.md) | `(bool $clear_realpath_cache = false, string $filename = ''): void` | `void` | ✓ | ✓ | +| [`copy()`](./builtins/filesystem/copy.md) | `(string $from, string $to): bool` | `bool` | ✓ | ✓ | +| [`dirname()`](./builtins/filesystem/dirname.md) | `(string $path, int $levels = 1): string` | `string` | ✓ | ✓ | +| [`disk_free_space()`](./builtins/filesystem/disk_free_space.md) | `(string $directory): float` | `float` | ✓ | ✓ | +| [`disk_total_space()`](./builtins/filesystem/disk_total_space.md) | `(string $directory): float` | `float` | ✓ | ✓ | +| [`file_exists()`](./builtins/filesystem/file_exists.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`fileatime()`](./builtins/filesystem/fileatime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filectime()`](./builtins/filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filegroup()`](./builtins/filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fileinode()`](./builtins/filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filemtime()`](./builtins/filesystem/filemtime.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`fileowner()`](./builtins/filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fileperms()`](./builtins/filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filesize()`](./builtins/filesystem/filesize.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`filetype()`](./builtins/filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fnmatch()`](./builtins/filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`getcwd()`](./builtins/filesystem/getcwd.md) | `(): string` | `string` | ✓ | ✓ | +| [`getenv()`](./builtins/filesystem/getenv.md) | `(string $name): mixed` | `mixed` | ✓ | ✓ | +| [`glob()`](./builtins/filesystem/glob.md) | `(string $pattern): array` | `array` | ✓ | ✓ | +| [`is_dir()`](./builtins/filesystem/is_dir.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_executable()`](./builtins/filesystem/is_executable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_file()`](./builtins/filesystem/is_file.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_link()`](./builtins/filesystem/is_link.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_readable()`](./builtins/filesystem/is_readable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_writable()`](./builtins/filesystem/is_writable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_writeable()`](./builtins/filesystem/is_writeable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`lchgrp()`](./builtins/filesystem/lchgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`lchown()`](./builtins/filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`link()`](./builtins/filesystem/link.md) | `(string $target, string $link): bool` | `bool` | ✓ | ✓ | +| [`linkinfo()`](./builtins/filesystem/linkinfo.md) | `(string $path): int` | `int` | ✓ | ✓ | +| [`lstat()`](./builtins/filesystem/lstat.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`mkdir()`](./builtins/filesystem/mkdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`pathinfo()`](./builtins/filesystem/pathinfo.md) | `(string $path, int $flags = 15): array` | `array` | ✓ | ✓ | +| [`putenv()`](./builtins/filesystem/putenv.md) | `(string $assignment): bool` | `bool` | ✓ | ✓ | +| [`readfile()`](./builtins/filesystem/readfile.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`readlink()`](./builtins/filesystem/readlink.md) | `(string $path): mixed` | `mixed` | ✓ | ✓ | +| [`realpath()`](./builtins/filesystem/realpath.md) | `(string $path): mixed` | `mixed` | ✓ | ✓ | +| [`realpath_cache_get()`](./builtins/filesystem/realpath_cache_get.md) | `(): array` | `array` | ✓ | ✓ | +| [`realpath_cache_size()`](./builtins/filesystem/realpath_cache_size.md) | `(): int` | `int` | ✓ | ✓ | +| [`rename()`](./builtins/filesystem/rename.md) | `(string $from, string $to): bool` | `bool` | ✓ | ✓ | +| [`rmdir()`](./builtins/filesystem/rmdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`scandir()`](./builtins/filesystem/scandir.md) | `(string $directory): array` | `array` | ✓ | ✓ | +| [`stat()`](./builtins/filesystem/stat.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`symlink()`](./builtins/filesystem/symlink.md) | `(string $target, string $link): bool` | `bool` | ✓ | ✓ | +| [`sys_get_temp_dir()`](./builtins/filesystem/sys_get_temp_dir.md) | `(): string` | `string` | ✓ | ✓ | +| [`tempnam()`](./builtins/filesystem/tempnam.md) | `(string $directory, string $prefix): string` | `string` | ✓ | ✓ | +| [`tmpfile()`](./builtins/filesystem/tmpfile.md) | `(): mixed` | `mixed` | ✓ | ✓ | +| [`touch()`](./builtins/filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | ✓ | ✓ | +| [`umask()`](./builtins/filesystem/umask.md) | `(int $mask = null): int` | `int` | ✓ | ✓ | +| [`unlink()`](./builtins/filesystem/unlink.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`closedir()`](./builtins/io/closedir.md) | `(resource $dir_handle): void` | `void` | ✓ | ✓ | +| [`fclose()`](./builtins/io/fclose.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fdatasync()`](./builtins/io/fdatasync.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`feof()`](./builtins/io/feof.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fflush()`](./builtins/io/fflush.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fgetc()`](./builtins/io/fgetc.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`fgetcsv()`](./builtins/io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | ✓ | ✓ | +| [`fgets()`](./builtins/io/fgets.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`file()`](./builtins/io/file.md) | `(string $filename): array` | `array` | ✓ | ✓ | +| [`file_get_contents()`](./builtins/io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`file_put_contents()`](./builtins/io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | ✓ | ✓ | +| [`flock()`](./builtins/io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | ✓ | ✓ | +| [`fopen()`](./builtins/io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | ✓ | ✓ | +| [`fpassthru()`](./builtins/io/fpassthru.md) | `(resource $stream): int` | `int` | ✓ | ✓ | +| [`fprintf()`](./builtins/io/fprintf.md) | `(resource $stream, string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`fputcsv()`](./builtins/io/fputcsv.md) | `(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int` | `int` | ✓ | ✓ | +| [`fread()`](./builtins/io/fread.md) | `(resource $stream, int $length): string` | `string` | ✓ | ✓ | +| [`fscanf()`](./builtins/io/fscanf.md) | `(resource $stream, string $format, ...$vars): array` | `array` | ✓ | ✓ | +| [`fseek()`](./builtins/io/fseek.md) | `(resource $stream, int $offset, int $whence = 0): int` | `int` | ✓ | ✓ | +| [`fstat()`](./builtins/io/fstat.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`fsync()`](./builtins/io/fsync.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`ftell()`](./builtins/io/ftell.md) | `(resource $stream): int` | `int` | ✓ | ✓ | +| [`ftruncate()`](./builtins/io/ftruncate.md) | `(resource $stream, int $size): bool` | `bool` | ✓ | ✓ | +| [`fwrite()`](./builtins/io/fwrite.md) | `(resource $stream, string $data): int` | `int` | ✓ | ✓ | +| [`gethostbyaddr()`](./builtins/io/gethostbyaddr.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`gethostbyname()`](./builtins/io/gethostbyname.md) | `(string $hostname): string` | `string` | ✓ | ✓ | +| [`gethostname()`](./builtins/io/gethostname.md) | `(): string` | `string` | ✓ | ✓ | +| [`getprotobyname()`](./builtins/io/getprotobyname.md) | `(string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getprotobynumber()`](./builtins/io/getprotobynumber.md) | `(int $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getservbyname()`](./builtins/io/getservbyname.md) | `(string $service, string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getservbyport()`](./builtins/io/getservbyport.md) | `(int $port, string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`hash_file()`](./builtins/io/hash_file.md) | `(string $algo, string $filename, bool $binary = false): mixed` | `mixed` | ✓ | ✓ | +| [`opendir()`](./builtins/io/opendir.md) | `(string $directory): mixed` | `mixed` | ✓ | ✓ | +| [`readdir()`](./builtins/io/readdir.md) | `(resource $dir_handle): mixed` | `mixed` | ✓ | ✓ | +| [`rewind()`](./builtins/io/rewind.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`rewinddir()`](./builtins/io/rewinddir.md) | `(resource $dir_handle): void` | `void` | ✓ | ✓ | +| [`stream_bucket_make_writeable()`](./builtins/io/stream_bucket_make_writeable.md) | `(mixed $brigade): mixed` | `mixed` | ✓ | ✓ | +| [`stream_bucket_new()`](./builtins/io/stream_bucket_new.md) | `(resource $stream, string $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_create()`](./builtins/io/stream_context_create.md) | `(array $options = null, array $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_get_default()`](./builtins/io/stream_context_get_default.md) | `(array $options = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_get_options()`](./builtins/io/stream_context_get_options.md) | `(resource $context): array` | `array` | ✓ | ✓ | +| [`stream_context_get_params()`](./builtins/io/stream_context_get_params.md) | `(resource $context): array` | `array` | ✓ | ✓ | +| [`stream_context_set_default()`](./builtins/io/stream_context_set_default.md) | `(array $options): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_set_option()`](./builtins/io/stream_context_set_option.md) | `(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool` | `bool` | ✓ | ✓ | +| [`stream_context_set_params()`](./builtins/io/stream_context_set_params.md) | `(resource $context, array $params): bool` | `bool` | ✓ | ✓ | +| [`stream_copy_to_stream()`](./builtins/io/stream_copy_to_stream.md) | `(resource $from, resource $to, int $length = null, int $offset = -1): mixed` | `mixed` | ✓ | ✓ | +| [`stream_filter_register()`](./builtins/io/stream_filter_register.md) | `(string $filter_name, string $class): bool` | `bool` | ✓ | ✓ | +| [`stream_filter_remove()`](./builtins/io/stream_filter_remove.md) | `(resource $stream_filter): bool` | `bool` | ✓ | ✓ | +| [`stream_get_contents()`](./builtins/io/stream_get_contents.md) | `(resource $stream, int $length = null, int $offset = -1): mixed` | `mixed` | ✓ | ✓ | +| [`stream_get_filters()`](./builtins/io/stream_get_filters.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_get_line()`](./builtins/io/stream_get_line.md) | `(resource $stream, int $length, string $ending = ''): string` | `string` | ✓ | ✓ | +| [`stream_get_meta_data()`](./builtins/io/stream_get_meta_data.md) | `(resource $stream): array` | `array` | ✓ | ✓ | +| [`stream_get_transports()`](./builtins/io/stream_get_transports.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_get_wrappers()`](./builtins/io/stream_get_wrappers.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_is_local()`](./builtins/io/stream_is_local.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_isatty()`](./builtins/io/stream_isatty.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_resolve_include_path()`](./builtins/io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`stream_select()`](./builtins/io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | ✓ | ✓ | +| [`stream_set_blocking()`](./builtins/io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | ✓ | ✓ | +| [`stream_set_chunk_size()`](./builtins/io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_set_read_buffer()`](./builtins/io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_set_timeout()`](./builtins/io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | ✓ | ✓ | +| [`stream_set_write_buffer()`](./builtins/io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_socket_accept()`](./builtins/io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_client()`](./builtins/io/stream_socket_client.md) | `(string $address): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_enable_crypto()`](./builtins/io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | `bool` | ✓ | ✓ | +| [`stream_socket_get_name()`](./builtins/io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_pair()`](./builtins/io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_recvfrom()`](./builtins/io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_sendto()`](./builtins/io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_server()`](./builtins/io/stream_socket_server.md) | `(string $address): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_shutdown()`](./builtins/io/stream_socket_shutdown.md) | `(resource $stream, int $mode): bool` | `bool` | ✓ | ✓ | +| [`stream_supports_lock()`](./builtins/io/stream_supports_lock.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_register()`](./builtins/io/stream_wrapper_register.md) | `(string $protocol, string $class, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_restore()`](./builtins/io/stream_wrapper_restore.md) | `(string $protocol): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_unregister()`](./builtins/io/stream_wrapper_unregister.md) | `(string $protocol): bool` | `bool` | ✓ | ✓ | +| [`vfprintf()`](./builtins/io/vfprintf.md) | `(resource $stream, string $format, array $values): int` | `int` | ✓ | ✓ | +| [`json_decode()`](./builtins/json/json_decode.md) | `(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed` | `mixed` | ✓ | ✓ | +| [`json_encode()`](./builtins/json/json_encode.md) | `(mixed $value, int $flags = 0, int $depth = 512): string` | `string` | ✓ | ✓ | +| [`json_last_error()`](./builtins/json/json_last_error.md) | `(): int` | `int` | ✓ | ✓ | +| [`json_last_error_msg()`](./builtins/json/json_last_error_msg.md) | `(): string` | `string` | ✓ | ✓ | +| [`json_validate()`](./builtins/json/json_validate.md) | `(string $json, int $depth = 512, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`abs()`](./builtins/math/abs.md) | `(int $num): mixed` | `mixed` | ✓ | ✓ | +| [`acos()`](./builtins/math/acos.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`asin()`](./builtins/math/asin.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`atan()`](./builtins/math/atan.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`atan2()`](./builtins/math/atan2.md) | `(float $y, float $x): float` | `float` | ✓ | ✓ | +| [`ceil()`](./builtins/math/ceil.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`clamp()`](./builtins/math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | ✓ | ✓ | +| [`cos()`](./builtins/math/cos.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`cosh()`](./builtins/math/cosh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`deg2rad()`](./builtins/math/deg2rad.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`exp()`](./builtins/math/exp.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`fdiv()`](./builtins/math/fdiv.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`floor()`](./builtins/math/floor.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`fmod()`](./builtins/math/fmod.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`hypot()`](./builtins/math/hypot.md) | `(float $x, float $y): float` | `float` | ✓ | ✓ | +| [`intdiv()`](./builtins/math/intdiv.md) | `(int $num1, int $num2): int` | `int` | ✓ | ✓ | +| [`is_finite()`](./builtins/math/is_finite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`is_infinite()`](./builtins/math/is_infinite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`is_nan()`](./builtins/math/is_nan.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`log()`](./builtins/math/log.md) | `(float $num, float $base = 2.718281828459045): float` | `float` | ✓ | ✓ | +| [`log10()`](./builtins/math/log10.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`log2()`](./builtins/math/log2.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`max()`](./builtins/math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | +| [`min()`](./builtins/math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | +| [`mt_rand()`](./builtins/math/mt_rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`pi()`](./builtins/math/pi.md) | `(): float` | `float` | ✓ | ✓ | +| [`pow()`](./builtins/math/pow.md) | `(float $num, float $exponent): float` | `float` | ✓ | ✓ | +| [`rad2deg()`](./builtins/math/rad2deg.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`rand()`](./builtins/math/rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`random_int()`](./builtins/math/random_int.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`round()`](./builtins/math/round.md) | `(float $num, int $precision = 0): float` | `float` | ✓ | ✓ | +| [`sin()`](./builtins/math/sin.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`sinh()`](./builtins/math/sinh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`sqrt()`](./builtins/math/sqrt.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`tan()`](./builtins/math/tan.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`tanh()`](./builtins/math/tanh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`buffer_new()`](./builtins/misc/buffer_new.md) | `(int $length): mixed` | `mixed` | ✓ | ✓ | +| [`define()`](./builtins/misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | ✓ | ✓ | +| [`defined()`](./builtins/misc/defined.md) | `(string $constant_name): bool` | `bool` | ✓ | ✓ | +| [`empty()`](./builtins/misc/empty.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`header()`](./builtins/misc/header.md) | `(string $header, bool $replace = true, int $response_code = 0): void` | `void` | ✓ | ✓ | +| [`http_response_code()`](./builtins/misc/http_response_code.md) | `(int $response_code = 0): int` | `int` | ✓ | ✓ | +| [`isset()`](./builtins/misc/isset.md) | `(mixed $var, ...$vars): bool` | `bool` | ✓ | ✓ | +| [`php_uname()`](./builtins/misc/php_uname.md) | `(string $mode = 'a'): string` | `string` | ✓ | ✓ | +| [`phpversion()`](./builtins/misc/phpversion.md) | `(): string` | `string` | ✓ | ✓ | +| [`print_r()`](./builtins/misc/print_r.md) | `(mixed $value, bool $return = false): mixed` | `mixed` | ✓ | ✓ | +| [`serialize()`](./builtins/misc/serialize.md) | `(mixed $value): string` | `string` | ✓ | — | +| [`unserialize()`](./builtins/misc/unserialize.md) | `(string $data, mixed $options = []): mixed` | `mixed` | ✓ | — | +| [`unset()`](./builtins/misc/unset.md) | `(mixed $var, ...$vars): void` | `void` | ✓ | ✓ | +| [`var_dump()`](./builtins/misc/var_dump.md) | `(mixed $value, ...$values): void` | `void` | ✓ | ✓ | +| [`ptr()`](./builtins/pointer/ptr.md) | `(mixed $value): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_get()`](./builtins/pointer/ptr_get.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_is_null()`](./builtins/pointer/ptr_is_null.md) | `(pointer $pointer): bool` | `bool` | ✓ | ✓ | +| [`ptr_null()`](./builtins/pointer/ptr_null.md) | `(): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_offset()`](./builtins/pointer/ptr_offset.md) | `(pointer $pointer, int $offset): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_read16()`](./builtins/pointer/ptr_read16.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read32()`](./builtins/pointer/ptr_read32.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read8()`](./builtins/pointer/ptr_read8.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read_string()`](./builtins/pointer/ptr_read_string.md) | `(pointer $pointer, int $length): string` | `string` | ✓ | ✓ | +| [`ptr_set()`](./builtins/pointer/ptr_set.md) | `(pointer $pointer, mixed $value): void` | `void` | ✓ | ✓ | +| [`ptr_sizeof()`](./builtins/pointer/ptr_sizeof.md) | `(string $type): int` | `int` | ✓ | ✓ | +| [`ptr_write16()`](./builtins/pointer/ptr_write16.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write32()`](./builtins/pointer/ptr_write32.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write8()`](./builtins/pointer/ptr_write8.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write_string()`](./builtins/pointer/ptr_write_string.md) | `(pointer $pointer, string $string): int` | `int` | ✓ | ✓ | +| [`zval_free()`](./builtins/pointer/zval_free.md) | `(pointer $zval): void` | `void` | ✓ | — | +| [`zval_pack()`](./builtins/pointer/zval_pack.md) | `(mixed $value): pointer` | `pointer` | ✓ | — | +| [`zval_type()`](./builtins/pointer/zval_type.md) | `(pointer $zval): int` | `int` | ✓ | — | +| [`zval_unpack()`](./builtins/pointer/zval_unpack.md) | `(pointer $zval): mixed` | `mixed` | ✓ | — | +| [`die()`](./builtins/process/die.md) | `(int $status): void` | `void` | ✓ | ✓ | +| [`exec()`](./builtins/process/exec.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`exit()`](./builtins/process/exit.md) | `(int $status): void` | `void` | ✓ | ✓ | +| [`passthru()`](./builtins/process/passthru.md) | `(string $command): void` | `void` | ✓ | ✓ | +| [`pclose()`](./builtins/process/pclose.md) | `(resource $handle): int` | `int` | ✓ | ✓ | +| [`popen()`](./builtins/process/popen.md) | `(string $command, string $mode): mixed` | `mixed` | ✓ | ✓ | +| [`readline()`](./builtins/process/readline.md) | `(string $prompt = null): mixed` | `mixed` | ✓ | ✓ | +| [`shell_exec()`](./builtins/process/shell_exec.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`sleep()`](./builtins/process/sleep.md) | `(int $seconds): int` | `int` | ✓ | ✓ | +| [`system()`](./builtins/process/system.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`usleep()`](./builtins/process/usleep.md) | `(int $microseconds): void` | `void` | ✓ | ✓ | +| [`mb_ereg_match()`](./builtins/regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | ✓ | ✓ | +| [`preg_match()`](./builtins/regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | ✓ | ✓ | +| [`preg_match_all()`](./builtins/regex/preg_match_all.md) | `(string $pattern, string $subject): int` | `int` | ✓ | ✓ | +| [`preg_replace()`](./builtins/regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject): string` | `string` | ✓ | ✓ | +| [`preg_replace_callback()`](./builtins/regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject): string` | `string` | ✓ | ✓ | +| [`preg_split()`](./builtins/regex/preg_split.md) | `(string $pattern, string $subject, int $limit = -1, int $flags = 0): array` | `array` | ✓ | ✓ | +| [`iterator_apply()`](./builtins/spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args = null): int` | `int` | ✓ | ✓ | +| [`iterator_count()`](./builtins/spl/iterator_count.md) | `(traversable $iterator): int` | `int` | ✓ | ✓ | +| [`iterator_to_array()`](./builtins/spl/iterator_to_array.md) | `(traversable $iterator, bool $preserve_keys = true): array` | `array` | ✓ | ✓ | +| [`spl_autoload()`](./builtins/spl/spl_autoload.md) | `(string $class, string $file_extensions = null): void` | `void` | ✓ | ✓ | +| [`spl_autoload_call()`](./builtins/spl/spl_autoload_call.md) | `(string $class): void` | `void` | ✓ | ✓ | +| [`spl_autoload_extensions()`](./builtins/spl/spl_autoload_extensions.md) | `(string $file_extensions = null): string` | `string` | ✓ | ✓ | +| [`spl_autoload_functions()`](./builtins/spl/spl_autoload_functions.md) | `(): array` | `array` | ✓ | ✓ | +| [`spl_autoload_register()`](./builtins/spl/spl_autoload_register.md) | `(callable $callback = null, bool $throw = true, bool $prepend = false): bool` | `bool` | ✓ | ✓ | +| [`spl_autoload_unregister()`](./builtins/spl/spl_autoload_unregister.md) | `(callable $callback): bool` | `bool` | ✓ | ✓ | +| [`spl_classes()`](./builtins/spl/spl_classes.md) | `(): array` | `array` | ✓ | ✓ | +| [`spl_object_hash()`](./builtins/spl/spl_object_hash.md) | `(object $object): string` | `string` | ✓ | ✓ | +| [`spl_object_id()`](./builtins/spl/spl_object_id.md) | `(object $object): int` | `int` | ✓ | ✓ | +| [`fsockopen()`](./builtins/streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`pfsockopen()`](./builtins/streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_bucket_append()`](./builtins/streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | +| [`stream_bucket_prepend()`](./builtins/streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | +| [`stream_filter_append()`](./builtins/streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_filter_prepend()`](./builtins/streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`addslashes()`](./builtins/string/addslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_decode()`](./builtins/string/base64_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_encode()`](./builtins/string/base64_encode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`bin2hex()`](./builtins/string/bin2hex.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`chop()`](./builtins/string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`chr()`](./builtins/string/chr.md) | `(int $codepoint): string` | `string` | ✓ | ✓ | +| [`crc32()`](./builtins/string/crc32.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`explode()`](./builtins/string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | ✓ | ✓ | +| [`grapheme_strrev()`](./builtins/string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | ✓ | ✓ | +| [`gzcompress()`](./builtins/string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | +| [`gzdeflate()`](./builtins/string/gzdeflate.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | +| [`gzinflate()`](./builtins/string/gzinflate.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | ✓ | ✓ | +| [`gzuncompress()`](./builtins/string/gzuncompress.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | ✓ | ✓ | +| [`hash()`](./builtins/string/hash.md) | `(string $algo, string $data, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_algos()`](./builtins/string/hash_algos.md) | `(): array` | `array` | ✓ | ✓ | +| [`hash_copy()`](./builtins/string/hash_copy.md) | `(resource $context): mixed` | `mixed` | ✓ | ✓ | +| [`hash_equals()`](./builtins/string/hash_equals.md) | `(string $known_string, string $user_string): bool` | `bool` | ✓ | ✓ | +| [`hash_final()`](./builtins/string/hash_final.md) | `(resource $context, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_hmac()`](./builtins/string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_init()`](./builtins/string/hash_init.md) | `(string $algo, int $flags = 0, string $key = ''): mixed` | `mixed` | ✓ | ✓ | +| [`hash_update()`](./builtins/string/hash_update.md) | `(resource $context, string $data): bool` | `bool` | ✓ | ✓ | +| [`hex2bin()`](./builtins/string/hex2bin.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`html_entity_decode()`](./builtins/string/html_entity_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`htmlentities()`](./builtins/string/htmlentities.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | ✓ | ✓ | +| [`htmlspecialchars()`](./builtins/string/htmlspecialchars.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | ✓ | ✓ | +| [`implode()`](./builtins/string/implode.md) | `(string $separator, array $array = null): string` | `string` | ✓ | ✓ | +| [`inet_ntop()`](./builtins/string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`inet_pton()`](./builtins/string/inet_pton.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`ip2long()`](./builtins/string/ip2long.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`lcfirst()`](./builtins/string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`long2ip()`](./builtins/string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | +| [`ltrim()`](./builtins/string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`md5()`](./builtins/string/md5.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`nl2br()`](./builtins/string/nl2br.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`number_format()`](./builtins/string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | ✓ | ✓ | +| [`ord()`](./builtins/string/ord.md) | `(string $character): int` | `int` | ✓ | ✓ | +| [`printf()`](./builtins/string/printf.md) | `(string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`rawurldecode()`](./builtins/string/rawurldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`rawurlencode()`](./builtins/string/rawurlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`rtrim()`](./builtins/string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`sha1()`](./builtins/string/sha1.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`sprintf()`](./builtins/string/sprintf.md) | `(string $format, ...$values): string` | `string` | ✓ | ✓ | +| [`sscanf()`](./builtins/string/sscanf.md) | `(string $string, string $format, ...$vars): array` | `array` | ✓ | ✓ | +| [`str_contains()`](./builtins/string/str_contains.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_ends_with()`](./builtins/string/str_ends_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_ireplace()`](./builtins/string/str_ireplace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | +| [`str_pad()`](./builtins/string/str_pad.md) | `(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string` | `string` | ✓ | ✓ | +| [`str_repeat()`](./builtins/string/str_repeat.md) | `(string $string, int $times): string` | `string` | ✓ | ✓ | +| [`str_replace()`](./builtins/string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | +| [`str_split()`](./builtins/string/str_split.md) | `(string $string, int $length = 1): array` | `array` | ✓ | ✓ | +| [`str_starts_with()`](./builtins/string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`strcasecmp()`](./builtins/string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`strcmp()`](./builtins/string/strcmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`stripslashes()`](./builtins/string/stripslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strlen()`](./builtins/string/strlen.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`strpos()`](./builtins/string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | +| [`strrev()`](./builtins/string/strrev.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strrpos()`](./builtins/string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | +| [`strstr()`](./builtins/string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): string` | `string` | ✓ | ✓ | +| [`strtolower()`](./builtins/string/strtolower.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strtoupper()`](./builtins/string/strtoupper.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`substr()`](./builtins/string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`substr_replace()`](./builtins/string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`trim()`](./builtins/string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`ucfirst()`](./builtins/string/ucfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`ucwords()`](./builtins/string/ucwords.md) | `(string $string, string $separators = ' \t\r\n\x0c\x0b'): string` | `string` | ✓ | ✓ | +| [`urldecode()`](./builtins/string/urldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`urlencode()`](./builtins/string/urlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`vprintf()`](./builtins/string/vprintf.md) | `(string $format, array $values): int` | `int` | ✓ | ✓ | +| [`vsprintf()`](./builtins/string/vsprintf.md) | `(string $format, array $values): string` | `string` | ✓ | ✓ | +| [`wordwrap()`](./builtins/string/wordwrap.md) | `(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string` | `string` | ✓ | ✓ | +| [`boolval()`](./builtins/type/boolval.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`ctype_alnum()`](./builtins/type/ctype_alnum.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_alpha()`](./builtins/type/ctype_alpha.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_digit()`](./builtins/type/ctype_digit.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_space()`](./builtins/type/ctype_space.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`floatval()`](./builtins/type/floatval.md) | `(mixed $value): float` | `float` | ✓ | ✓ | +| [`get_resource_id()`](./builtins/type/get_resource_id.md) | `(resource $resource): int` | `int` | ✓ | ✓ | +| [`get_resource_type()`](./builtins/type/get_resource_type.md) | `(resource $resource): string` | `string` | ✓ | ✓ | +| [`gettype()`](./builtins/type/gettype.md) | `(mixed $value): string` | `string` | ✓ | ✓ | +| [`intval()`](./builtins/type/intval.md) | `(mixed $value): int` | `int` | ✓ | ✓ | +| [`is_array()`](./builtins/type/is_array.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_bool()`](./builtins/type/is_bool.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_callable()`](./builtins/type/is_callable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_float()`](./builtins/type/is_float.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_int()`](./builtins/type/is_int.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_iterable()`](./builtins/type/is_iterable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_null()`](./builtins/type/is_null.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_numeric()`](./builtins/type/is_numeric.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_object()`](./builtins/type/is_object.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_resource()`](./builtins/type/is_resource.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_scalar()`](./builtins/type/is_scalar.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_string()`](./builtins/type/is_string.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`settype()`](./builtins/type/settype.md) | `(mixed $var, string $type): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/array.md b/docs/php/builtins/array.md index f2f4d44092..3cee753978 100644 --- a/docs/php/builtins/array.md +++ b/docs/php/builtins/array.md @@ -7,68 +7,68 @@ sidebar: ## Array builtins -| Function | Signature | Returns | -|---|---|---| -| [`array_all()`](./array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | -| [`array_any()`](./array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | -| [`array_chunk()`](./array/array_chunk.md) | `(array $array, int $length): array` | `array` | -| [`array_column()`](./array/array_column.md) | `(array $array, string $column_key): array` | `array` | -| [`array_combine()`](./array/array_combine.md) | `(array $keys, array $values): array` | `array` | -| [`array_diff()`](./array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_diff_assoc()`](./array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | -| [`array_diff_key()`](./array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_fill()`](./array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | -| [`array_fill_keys()`](./array/array_fill_keys.md) | `(array $keys, mixed $value): array` | `array` | -| [`array_filter()`](./array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | -| [`array_find()`](./array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | -| [`array_flip()`](./array/array_flip.md) | `(array $array): array` | `array` | -| [`array_intersect()`](./array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_intersect_assoc()`](./array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | -| [`array_intersect_key()`](./array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | -| [`array_is_list()`](./array/array_is_list.md) | `(mixed $array): bool` | `bool` | -| [`array_key_exists()`](./array/array_key_exists.md) | `(string $key, array $array): bool` | `bool` | -| [`array_key_first()`](./array/array_key_first.md) | `(array $array): mixed` | `mixed` | -| [`array_key_last()`](./array/array_key_last.md) | `(array $array): mixed` | `mixed` | -| [`array_keys()`](./array/array_keys.md) | `(array $array): array` | `array` | -| [`array_map()`](./array/array_map.md) | `(callable $callback, array $array, ...$arrays): array` | `array` | -| [`array_merge()`](./array/array_merge.md) | `(...$arrays): array` | `array` | -| [`array_merge_recursive()`](./array/array_merge_recursive.md) | `(...$arrays): array` | `array` | -| [`array_multisort()`](./array/array_multisort.md) | `(array $array1, int $array2): bool` | `bool` | -| [`array_pad()`](./array/array_pad.md) | `(array $array, int $length, mixed $value): array` | `array` | -| [`array_pop()`](./array/array_pop.md) | `(array $array): mixed` | `mixed` | -| [`array_product()`](./array/array_product.md) | `(array $array): int` | `int` | -| [`array_push()`](./array/array_push.md) | `(array $array, ...$values): void` | `void` | -| [`array_rand()`](./array/array_rand.md) | `(array $array): int` | `int` | -| [`array_reduce()`](./array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | -| [`array_replace()`](./array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | -| [`array_replace_recursive()`](./array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | -| [`array_reverse()`](./array/array_reverse.md) | `(array $array): array` | `array` | -| [`array_search()`](./array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | -| [`array_shift()`](./array/array_shift.md) | `(array $array): mixed` | `mixed` | -| [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | -| [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | -| [`array_sum()`](./array/array_sum.md) | `(array $array): int` | `int` | -| [`array_udiff()`](./array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | -| [`array_uintersect()`](./array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | -| [`array_unique()`](./array/array_unique.md) | `(array $array): array` | `array` | -| [`array_unshift()`](./array/array_unshift.md) | `(array $array, ...$values): int` | `int` | -| [`array_values()`](./array/array_values.md) | `(array $array): array` | `array` | -| [`array_walk()`](./array/array_walk.md) | `(array $array, callable $callback): void` | `void` | -| [`array_walk_recursive()`](./array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | -| [`arsort()`](./array/arsort.md) | `(array $array): bool` | `bool` | -| [`asort()`](./array/asort.md) | `(array $array): bool` | `bool` | -| [`call_user_func()`](./array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | -| [`call_user_func_array()`](./array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | -| [`count()`](./array/count.md) | `(array $value, int $mode = 0): int` | `int` | -| [`in_array()`](./array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | -| [`krsort()`](./array/krsort.md) | `(array $array): bool` | `bool` | -| [`ksort()`](./array/ksort.md) | `(array $array): bool` | `bool` | -| [`natcasesort()`](./array/natcasesort.md) | `(array $array): bool` | `bool` | -| [`natsort()`](./array/natsort.md) | `(array $array): bool` | `bool` | -| [`range()`](./array/range.md) | `(mixed $start, mixed $end): array` | `array` | -| [`rsort()`](./array/rsort.md) | `(array $array): bool` | `bool` | -| [`shuffle()`](./array/shuffle.md) | `(array $array): bool` | `bool` | -| [`sort()`](./array/sort.md) | `(array $array): bool` | `bool` | -| [`uasort()`](./array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`uksort()`](./array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | -| [`usort()`](./array/usort.md) | `(array $array, callable $callback): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`array_all()`](./array/array_all.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | +| [`array_any()`](./array/array_any.md) | `(mixed $array, mixed $callback): bool` | `bool` | ✓ | — | +| [`array_chunk()`](./array/array_chunk.md) | `(array $array, int $length): array` | `array` | ✓ | ✓ | +| [`array_column()`](./array/array_column.md) | `(array $array, string $column_key): array` | `array` | ✓ | ✓ | +| [`array_combine()`](./array/array_combine.md) | `(array $keys, array $values): array` | `array` | ✓ | ✓ | +| [`array_diff()`](./array/array_diff.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_diff_assoc()`](./array/array_diff_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | +| [`array_diff_key()`](./array/array_diff_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_fill()`](./array/array_fill.md) | `(int $start_index, int $count, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_fill_keys()`](./array/array_fill_keys.md) | `(array $keys, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_filter()`](./array/array_filter.md) | `(array $array, callable $callback = null, int $mode = 0): array` | `array` | ✓ | ✓ | +| [`array_find()`](./array/array_find.md) | `(mixed $array, mixed $callback): mixed` | `mixed` | ✓ | — | +| [`array_flip()`](./array/array_flip.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_intersect()`](./array/array_intersect.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_intersect_assoc()`](./array/array_intersect_assoc.md) | `(array $array, ...$arrays): mixed` | `mixed` | ✓ | — | +| [`array_intersect_key()`](./array/array_intersect_key.md) | `(array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_is_list()`](./array/array_is_list.md) | `(mixed $array): bool` | `bool` | ✓ | — | +| [`array_key_exists()`](./array/array_key_exists.md) | `(string $key, array $array): bool` | `bool` | ✓ | ✓ | +| [`array_key_first()`](./array/array_key_first.md) | `(array $array): mixed` | `mixed` | ✓ | — | +| [`array_key_last()`](./array/array_key_last.md) | `(array $array): mixed` | `mixed` | ✓ | — | +| [`array_keys()`](./array/array_keys.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_map()`](./array/array_map.md) | `(callable $callback, array $array, ...$arrays): array` | `array` | ✓ | ✓ | +| [`array_merge()`](./array/array_merge.md) | `(...$arrays): array` | `array` | ✓ | ✓ | +| [`array_merge_recursive()`](./array/array_merge_recursive.md) | `(...$arrays): array` | `array` | ✓ | — | +| [`array_multisort()`](./array/array_multisort.md) | `(array $array1, int $array2): bool` | `bool` | ✓ | — | +| [`array_pad()`](./array/array_pad.md) | `(array $array, int $length, mixed $value): array` | `array` | ✓ | ✓ | +| [`array_pop()`](./array/array_pop.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_product()`](./array/array_product.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_push()`](./array/array_push.md) | `(array $array, ...$values): void` | `void` | ✓ | ✓ | +| [`array_rand()`](./array/array_rand.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_reduce()`](./array/array_reduce.md) | `(array $array, callable $callback, mixed $initial = null): int` | `int` | ✓ | ✓ | +| [`array_replace()`](./array/array_replace.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | +| [`array_replace_recursive()`](./array/array_replace_recursive.md) | `(array $array, array $replacements): mixed` | `mixed` | ✓ | — | +| [`array_reverse()`](./array/array_reverse.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_search()`](./array/array_search.md) | `(mixed $needle, array $haystack, bool $strict = false): mixed` | `mixed` | ✓ | ✓ | +| [`array_shift()`](./array/array_shift.md) | `(array $array): mixed` | `mixed` | ✓ | ✓ | +| [`array_slice()`](./array/array_slice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_splice()`](./array/array_splice.md) | `(array $array, int $offset, int $length = null): array` | `array` | ✓ | ✓ | +| [`array_sum()`](./array/array_sum.md) | `(array $array): int` | `int` | ✓ | ✓ | +| [`array_udiff()`](./array/array_udiff.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | +| [`array_uintersect()`](./array/array_uintersect.md) | `(array $array1, array $array2, callable $callback): array` | `array` | ✓ | — | +| [`array_unique()`](./array/array_unique.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_unshift()`](./array/array_unshift.md) | `(array $array, ...$values): int` | `int` | ✓ | ✓ | +| [`array_values()`](./array/array_values.md) | `(array $array): array` | `array` | ✓ | ✓ | +| [`array_walk()`](./array/array_walk.md) | `(array $array, callable $callback): void` | `void` | ✓ | ✓ | +| [`array_walk_recursive()`](./array/array_walk_recursive.md) | `(array $array, callable $callback): void` | `void` | ✓ | — | +| [`arsort()`](./array/arsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`asort()`](./array/asort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`call_user_func()`](./array/call_user_func.md) | `(callable $callback, ...$args): mixed` | `mixed` | ✓ | ✓ | +| [`call_user_func_array()`](./array/call_user_func_array.md) | `(callable $callback, array $args): mixed` | `mixed` | ✓ | ✓ | +| [`count()`](./array/count.md) | `(array $value, int $mode = 0): int` | `int` | ✓ | ✓ | +| [`in_array()`](./array/in_array.md) | `(mixed $needle, array $haystack, bool $strict = false): bool` | `bool` | ✓ | ✓ | +| [`krsort()`](./array/krsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`ksort()`](./array/ksort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`natcasesort()`](./array/natcasesort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`natsort()`](./array/natsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`range()`](./array/range.md) | `(mixed $start, mixed $end): array` | `array` | ✓ | ✓ | +| [`rsort()`](./array/rsort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`shuffle()`](./array/shuffle.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`sort()`](./array/sort.md) | `(array $array): bool` | `bool` | ✓ | ✓ | +| [`uasort()`](./array/uasort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`uksort()`](./array/uksort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | +| [`usort()`](./array/usort.md) | `(array $array, callable $callback): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/array/array_all.md b/docs/php/builtins/array/array_all.md index 20fa87b4bd..f440ee7aa7 100644 --- a/docs/php/builtins/array/array_all.md +++ b/docs/php/builtins/array/array_all.md @@ -19,6 +19,11 @@ Returns true when every array element satisfies the predicate callback. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_any.md b/docs/php/builtins/array/array_any.md index c8c8658fad..1ba9cee69a 100644 --- a/docs/php/builtins/array/array_any.md +++ b/docs/php/builtins/array/array_any.md @@ -19,6 +19,11 @@ Returns true when at least one array element satisfies the predicate callback. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_chunk.md b/docs/php/builtins/array/array_chunk.md index 3bf1f31b8c..ec6fd9120b 100644 --- a/docs/php/builtins/array/array_chunk.md +++ b/docs/php/builtins/array/array_chunk.md @@ -19,6 +19,11 @@ Splits an array into chunks of the given size. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_column.md b/docs/php/builtins/array/array_column.md index c4f21c1c22..50f1476463 100644 --- a/docs/php/builtins/array/array_column.md +++ b/docs/php/builtins/array/array_column.md @@ -19,6 +19,11 @@ Returns the values from a single column of an array of arrays. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_column.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_column.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_combine.md b/docs/php/builtins/array/array_combine.md index 18b4617d4d..4b1a732456 100644 --- a/docs/php/builtins/array/array_combine.md +++ b/docs/php/builtins/array/array_combine.md @@ -19,6 +19,11 @@ Creates an array by using one array for keys and another for values. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_diff.md b/docs/php/builtins/array/array_diff.md index ea514aa384..c6b87a11e7 100644 --- a/docs/php/builtins/array/array_diff.md +++ b/docs/php/builtins/array/array_diff.md @@ -19,6 +19,11 @@ Computes the difference of arrays. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_diff_assoc.md b/docs/php/builtins/array/array_diff_assoc.md index 488a870134..a8eac7c79c 100644 --- a/docs/php/builtins/array/array_diff_assoc.md +++ b/docs/php/builtins/array/array_diff_assoc.md @@ -19,6 +19,11 @@ Computes the difference of arrays with additional index check. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_diff_key.md b/docs/php/builtins/array/array_diff_key.md index 2fd900fa8a..1e8ee4a3b7 100644 --- a/docs/php/builtins/array/array_diff_key.md +++ b/docs/php/builtins/array/array_diff_key.md @@ -19,6 +19,11 @@ Computes the difference of arrays using keys for comparison. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_fill.md b/docs/php/builtins/array/array_fill.md index c6140e68f5..e1b269d0a4 100644 --- a/docs/php/builtins/array/array_fill.md +++ b/docs/php/builtins/array/array_fill.md @@ -20,6 +20,11 @@ Fill an array with values. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_fill_keys.md b/docs/php/builtins/array/array_fill_keys.md index d8c4197c76..5cdd56b8d8 100644 --- a/docs/php/builtins/array/array_fill_keys.md +++ b/docs/php/builtins/array/array_fill_keys.md @@ -19,6 +19,11 @@ Fill an array with values, specifying keys. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_filter.md b/docs/php/builtins/array/array_filter.md index 0aca1d8604..50a9b378c6 100644 --- a/docs/php/builtins/array/array_filter.md +++ b/docs/php/builtins/array/array_filter.md @@ -20,6 +20,11 @@ Filters elements of an array using a callback function. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_find.md b/docs/php/builtins/array/array_find.md index e21471ea11..1ed4954d8b 100644 --- a/docs/php/builtins/array/array_find.md +++ b/docs/php/builtins/array/array_find.md @@ -19,6 +19,11 @@ Returns the first element satisfying a predicate callback, or null. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_flip.md b/docs/php/builtins/array/array_flip.md index d4a791bde8..027d087f4f 100644 --- a/docs/php/builtins/array/array_flip.md +++ b/docs/php/builtins/array/array_flip.md @@ -18,6 +18,11 @@ Exchanges all keys with their associated values in an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_intersect.md b/docs/php/builtins/array/array_intersect.md index 55c5ce3ab5..671ae97944 100644 --- a/docs/php/builtins/array/array_intersect.md +++ b/docs/php/builtins/array/array_intersect.md @@ -19,6 +19,11 @@ Computes the intersection of arrays. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_intersect_assoc.md b/docs/php/builtins/array/array_intersect_assoc.md index 889e440ea1..434a4e9577 100644 --- a/docs/php/builtins/array/array_intersect_assoc.md +++ b/docs/php/builtins/array/array_intersect_assoc.md @@ -19,6 +19,11 @@ Computes the intersection of arrays with additional index check. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_intersect_key.md b/docs/php/builtins/array/array_intersect_key.md index d9fb6a57c0..bd76ac445a 100644 --- a/docs/php/builtins/array/array_intersect_key.md +++ b/docs/php/builtins/array/array_intersect_key.md @@ -19,6 +19,11 @@ Computes the intersection of arrays using keys for comparison. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_is_list.md b/docs/php/builtins/array/array_is_list.md index c5264801c8..d689508738 100644 --- a/docs/php/builtins/array/array_is_list.md +++ b/docs/php/builtins/array/array_is_list.md @@ -18,6 +18,11 @@ Checks whether an array is a list (sequential 0-based integer keys). **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_key_exists.md b/docs/php/builtins/array/array_key_exists.md index bee1b864a3..33b65b9050 100644 --- a/docs/php/builtins/array/array_key_exists.md +++ b/docs/php/builtins/array/array_key_exists.md @@ -19,6 +19,11 @@ Checks if the given key or index exists in the array. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_key_first.md b/docs/php/builtins/array/array_key_first.md index 64e4ab6b28..8b430fe25c 100644 --- a/docs/php/builtins/array/array_key_first.md +++ b/docs/php/builtins/array/array_key_first.md @@ -18,6 +18,11 @@ Gets the first key of an array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_key_last.md b/docs/php/builtins/array/array_key_last.md index 50da0ddb35..ca00017d7d 100644 --- a/docs/php/builtins/array/array_key_last.md +++ b/docs/php/builtins/array/array_key_last.md @@ -18,6 +18,11 @@ Gets the last key of an array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_keys.md b/docs/php/builtins/array/array_keys.md index dd9eab185a..3063349c30 100644 --- a/docs/php/builtins/array/array_keys.md +++ b/docs/php/builtins/array/array_keys.md @@ -18,6 +18,11 @@ Returns all the keys of an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_map.md b/docs/php/builtins/array/array_map.md index c1dcfac6ca..9d07785976 100644 --- a/docs/php/builtins/array/array_map.md +++ b/docs/php/builtins/array/array_map.md @@ -20,6 +20,11 @@ Applies a callback to the elements of an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_map.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_map.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_merge.md b/docs/php/builtins/array/array_merge.md index 944a0185fb..35e27a8b00 100644 --- a/docs/php/builtins/array/array_merge.md +++ b/docs/php/builtins/array/array_merge.md @@ -18,6 +18,11 @@ Merges the elements of two arrays. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_merge_recursive.md b/docs/php/builtins/array/array_merge_recursive.md index 4cccbd6f2a..43c92f9e3f 100644 --- a/docs/php/builtins/array/array_merge_recursive.md +++ b/docs/php/builtins/array/array_merge_recursive.md @@ -18,6 +18,11 @@ Recursively merges two arrays, combining scalar collisions into lists. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_multisort.md b/docs/php/builtins/array/array_multisort.md index 0d03600acb..0cce61476a 100644 --- a/docs/php/builtins/array/array_multisort.md +++ b/docs/php/builtins/array/array_multisort.md @@ -19,6 +19,11 @@ Sorts multiple arrays or multi-dimensional arrays. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_pad.md b/docs/php/builtins/array/array_pad.md index 57e9cdf9fc..bb2a4021aa 100644 --- a/docs/php/builtins/array/array_pad.md +++ b/docs/php/builtins/array/array_pad.md @@ -20,6 +20,11 @@ Pads an array to the specified length with a value. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_pop.md b/docs/php/builtins/array/array_pop.md index 640e0c5c4d..4857dc9d13 100644 --- a/docs/php/builtins/array/array_pop.md +++ b/docs/php/builtins/array/array_pop.md @@ -18,6 +18,11 @@ Pops the element off the end of array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_product.md b/docs/php/builtins/array/array_product.md index bcf9ca0e28..f7980f2795 100644 --- a/docs/php/builtins/array/array_product.md +++ b/docs/php/builtins/array/array_product.md @@ -18,6 +18,11 @@ Calculate the product of values in an array. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_product.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_product.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_push.md b/docs/php/builtins/array/array_push.md index 1024eb9e30..e964d9cf9d 100644 --- a/docs/php/builtins/array/array_push.md +++ b/docs/php/builtins/array/array_push.md @@ -19,6 +19,11 @@ Pushes one or more elements onto the end of array. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_push.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_push.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_rand.md b/docs/php/builtins/array/array_rand.md index 6194bc5301..fa845001c4 100644 --- a/docs/php/builtins/array/array_rand.md +++ b/docs/php/builtins/array/array_rand.md @@ -18,6 +18,11 @@ Pick one or more random keys out of an array. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_reduce.md b/docs/php/builtins/array/array_reduce.md index 63782c9e33..e0bcba350a 100644 --- a/docs/php/builtins/array/array_reduce.md +++ b/docs/php/builtins/array/array_reduce.md @@ -20,6 +20,11 @@ Iteratively reduces an array to a single value using a callback function. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_replace.md b/docs/php/builtins/array/array_replace.md index f5964ad250..accd854023 100644 --- a/docs/php/builtins/array/array_replace.md +++ b/docs/php/builtins/array/array_replace.md @@ -19,6 +19,11 @@ Replaces elements from passed arrays into the first array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_replace_recursive.md b/docs/php/builtins/array/array_replace_recursive.md index 379edc8897..56e4d1cd41 100644 --- a/docs/php/builtins/array/array_replace_recursive.md +++ b/docs/php/builtins/array/array_replace_recursive.md @@ -19,6 +19,11 @@ Replaces elements from passed arrays into the first array recursively. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_reverse.md b/docs/php/builtins/array/array_reverse.md index 6bd856cbd5..303c296e47 100644 --- a/docs/php/builtins/array/array_reverse.md +++ b/docs/php/builtins/array/array_reverse.md @@ -18,6 +18,11 @@ Returns an array with the elements in reverse order. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_search.md b/docs/php/builtins/array/array_search.md index f2e1a42180..5b0617bc97 100644 --- a/docs/php/builtins/array/array_search.md +++ b/docs/php/builtins/array/array_search.md @@ -20,6 +20,11 @@ Searches the array for a given value and returns the first corresponding key if **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_search.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_search.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_shift.md b/docs/php/builtins/array/array_shift.md index bd4d8dc9b5..031a7af292 100644 --- a/docs/php/builtins/array/array_shift.md +++ b/docs/php/builtins/array/array_shift.md @@ -18,6 +18,11 @@ Shifts an element off the beginning of array. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_slice.md b/docs/php/builtins/array/array_slice.md index 8abb0326ca..792f77b8b9 100644 --- a/docs/php/builtins/array/array_slice.md +++ b/docs/php/builtins/array/array_slice.md @@ -20,6 +20,11 @@ Extracts a slice of an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_splice.md b/docs/php/builtins/array/array_splice.md index eba2b7a107..29eb4e5d37 100644 --- a/docs/php/builtins/array/array_splice.md +++ b/docs/php/builtins/array/array_splice.md @@ -20,6 +20,11 @@ Removes a portion of the array and replaces it with something else. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_sum.md b/docs/php/builtins/array/array_sum.md index 775aceb2fc..c40224640b 100644 --- a/docs/php/builtins/array/array_sum.md +++ b/docs/php/builtins/array/array_sum.md @@ -18,6 +18,11 @@ Calculate the sum of values in an array. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_udiff.md b/docs/php/builtins/array/array_udiff.md index 29fed10285..0e7f340810 100644 --- a/docs/php/builtins/array/array_udiff.md +++ b/docs/php/builtins/array/array_udiff.md @@ -20,6 +20,11 @@ Computes the difference of arrays using a callback comparator. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_uintersect.md b/docs/php/builtins/array/array_uintersect.md index 9ae4631678..44f48f2442 100644 --- a/docs/php/builtins/array/array_uintersect.md +++ b/docs/php/builtins/array/array_uintersect.md @@ -20,6 +20,11 @@ Computes the intersection of arrays using a callback comparator. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_unique.md b/docs/php/builtins/array/array_unique.md index f69c66df25..1e7aa64610 100644 --- a/docs/php/builtins/array/array_unique.md +++ b/docs/php/builtins/array/array_unique.md @@ -18,6 +18,11 @@ Removes duplicate values from an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_unshift.md b/docs/php/builtins/array/array_unshift.md index cd8fa5e2ea..1895f522aa 100644 --- a/docs/php/builtins/array/array_unshift.md +++ b/docs/php/builtins/array/array_unshift.md @@ -19,6 +19,11 @@ Prepends one or more elements to the beginning of an array. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_values.md b/docs/php/builtins/array/array_values.md index 4e3a852a99..1c3b535acc 100644 --- a/docs/php/builtins/array/array_values.md +++ b/docs/php/builtins/array/array_values.md @@ -18,6 +18,11 @@ Returns all the values of an array, re-indexed numerically. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_values.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_values.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_walk.md b/docs/php/builtins/array/array_walk.md index 0b6a72bec4..02fac18576 100644 --- a/docs/php/builtins/array/array_walk.md +++ b/docs/php/builtins/array/array_walk.md @@ -19,6 +19,11 @@ Applies a user function to every member of an array. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/array_walk_recursive.md b/docs/php/builtins/array/array_walk_recursive.md index 7de26032cf..a68806d5a9 100644 --- a/docs/php/builtins/array/array_walk_recursive.md +++ b/docs/php/builtins/array/array_walk_recursive.md @@ -19,6 +19,11 @@ Applies a user function recursively to every member of an array. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/arsort.md b/docs/php/builtins/array/arsort.md index b13789bfc9..dac5fc4854 100644 --- a/docs/php/builtins/array/arsort.md +++ b/docs/php/builtins/array/arsort.md @@ -18,6 +18,11 @@ Sorts an array in descending order and maintains index association. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/arsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/arsort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/asort.md b/docs/php/builtins/array/asort.md index cf3377b9de..6d01495b69 100644 --- a/docs/php/builtins/array/asort.md +++ b/docs/php/builtins/array/asort.md @@ -18,6 +18,11 @@ Sorts an array and maintains index association. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/asort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/asort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/call_user_func.md b/docs/php/builtins/array/call_user_func.md index 937fc917e6..77e6f8958a 100644 --- a/docs/php/builtins/array/call_user_func.md +++ b/docs/php/builtins/array/call_user_func.md @@ -19,6 +19,11 @@ Calls a callback with the given arguments. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/call_user_func_array.md b/docs/php/builtins/array/call_user_func_array.md index e6cd18b3a5..aaeac63b51 100644 --- a/docs/php/builtins/array/call_user_func_array.md +++ b/docs/php/builtins/array/call_user_func_array.md @@ -19,6 +19,11 @@ Calls a callback with an array of parameters. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/count.md b/docs/php/builtins/array/count.md index a865fb07d7..d443759519 100644 --- a/docs/php/builtins/array/count.md +++ b/docs/php/builtins/array/count.md @@ -19,6 +19,11 @@ Counts all elements in an array or Countable object. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/count.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/in_array.md b/docs/php/builtins/array/in_array.md index 9064877063..7c4e70009b 100644 --- a/docs/php/builtins/array/in_array.md +++ b/docs/php/builtins/array/in_array.md @@ -20,6 +20,11 @@ Checks if a value exists in an array. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/in_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/in_array.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/krsort.md b/docs/php/builtins/array/krsort.md index 62873b00b3..d013c99cc1 100644 --- a/docs/php/builtins/array/krsort.md +++ b/docs/php/builtins/array/krsort.md @@ -18,6 +18,11 @@ Sorts an array by key in descending order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/krsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/krsort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/ksort.md b/docs/php/builtins/array/ksort.md index 558cadc3c5..3a14a476be 100644 --- a/docs/php/builtins/array/ksort.md +++ b/docs/php/builtins/array/ksort.md @@ -18,6 +18,11 @@ Sorts an array by key in ascending order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/ksort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/ksort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/natcasesort.md b/docs/php/builtins/array/natcasesort.md index b4f4825c81..e2ef2d6c40 100644 --- a/docs/php/builtins/array/natcasesort.md +++ b/docs/php/builtins/array/natcasesort.md @@ -18,6 +18,11 @@ Sorts an array using a case-insensitive natural order algorithm. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/natsort.md b/docs/php/builtins/array/natsort.md index 81eb9f565f..3c5090bee1 100644 --- a/docs/php/builtins/array/natsort.md +++ b/docs/php/builtins/array/natsort.md @@ -18,6 +18,11 @@ Sorts an array using a natural order algorithm. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/natsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/natsort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/range.md b/docs/php/builtins/array/range.md index 306f693278..f43f4a4e28 100644 --- a/docs/php/builtins/array/range.md +++ b/docs/php/builtins/array/range.md @@ -19,6 +19,11 @@ Create an array containing a range of elements. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/range.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/range.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/rsort.md b/docs/php/builtins/array/rsort.md index 9741571ad5..d412aa6826 100644 --- a/docs/php/builtins/array/rsort.md +++ b/docs/php/builtins/array/rsort.md @@ -18,6 +18,11 @@ Sorts an array in descending order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/rsort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/rsort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/shuffle.md b/docs/php/builtins/array/shuffle.md index 35eeb0a290..557dcc6d31 100644 --- a/docs/php/builtins/array/shuffle.md +++ b/docs/php/builtins/array/shuffle.md @@ -18,6 +18,11 @@ Shuffles an array into random order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/sort.md b/docs/php/builtins/array/sort.md index bd02d22310..1857e4df36 100644 --- a/docs/php/builtins/array/sort.md +++ b/docs/php/builtins/array/sort.md @@ -18,6 +18,11 @@ Sorts an array in ascending order. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/sort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/sort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/uasort.md b/docs/php/builtins/array/uasort.md index 12cd0cbf14..3019bb83e8 100644 --- a/docs/php/builtins/array/uasort.md +++ b/docs/php/builtins/array/uasort.md @@ -19,6 +19,11 @@ Sorts an array with a user-defined comparison function and maintains index assoc **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/uasort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/uasort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/uksort.md b/docs/php/builtins/array/uksort.md index 4968295de1..f81b0650df 100644 --- a/docs/php/builtins/array/uksort.md +++ b/docs/php/builtins/array/uksort.md @@ -19,6 +19,11 @@ Sorts an array by keys using a user-defined comparison function. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/uksort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/uksort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/array/usort.md b/docs/php/builtins/array/usort.md index 76afdff10f..f90c2e23a0 100644 --- a/docs/php/builtins/array/usort.md +++ b/docs/php/builtins/array/usort.md @@ -19,6 +19,11 @@ Sorts an array by values using a user-defined comparison function. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/usort.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/usort.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/buffer.md b/docs/php/builtins/buffer.md index 99cf5e5037..7e4d057561 100644 --- a/docs/php/builtins/buffer.md +++ b/docs/php/builtins/buffer.md @@ -7,7 +7,7 @@ sidebar: ## Buffer builtins -| Function | Signature | Returns | -|---|---|---| -| [`buffer_free()`](./buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | -| [`buffer_len()`](./buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`buffer_free()`](./buffer/buffer_free.md) | `(buffer $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`buffer_len()`](./buffer/buffer_len.md) | `(buffer $buffer): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/buffer/buffer_free.md b/docs/php/builtins/buffer/buffer_free.md index 136dba1345..88169f317c 100644 --- a/docs/php/builtins/buffer/buffer_free.md +++ b/docs/php/builtins/buffer/buffer_free.md @@ -18,6 +18,11 @@ Lowers `buffer_free()` through the direct buffer opcode helper. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/buffer/buffer_len.md b/docs/php/builtins/buffer/buffer_len.md index 07ff13516b..0b7d6d7d3e 100644 --- a/docs/php/builtins/buffer/buffer_len.md +++ b/docs/php/builtins/buffer/buffer_len.md @@ -18,6 +18,11 @@ Lowers `buffer_len()` through the direct buffer opcode helper. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class.md b/docs/php/builtins/class.md index 38b0b23bd1..0118715697 100644 --- a/docs/php/builtins/class.md +++ b/docs/php/builtins/class.md @@ -7,24 +7,28 @@ sidebar: ## Class builtins -| Function | Signature | Returns | -|---|---|---| -| [`class_alias()`](./class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | -| [`class_attribute_args()`](./class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | -| [`class_attribute_names()`](./class/class_attribute_names.md) | `(string $class_name): array` | `array` | -| [`class_exists()`](./class/class_exists.md) | `(string $class, bool $autoload = true): bool` | `bool` | -| [`class_get_attributes()`](./class/class_get_attributes.md) | `(string $class_name): array` | `array` | -| [`class_implements()`](./class/class_implements.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`class_parents()`](./class/class_parents.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`class_uses()`](./class/class_uses.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | -| [`enum_exists()`](./class/enum_exists.md) | `(string $enum, bool $autoload = true): bool` | `bool` | -| [`function_exists()`](./class/function_exists.md) | `(string $function): bool` | `bool` | -| [`get_class()`](./class/get_class.md) | `(object $object = null): string` | `string` | -| [`get_declared_classes()`](./class/get_declared_classes.md) | `(): array` | `array` | -| [`get_declared_interfaces()`](./class/get_declared_interfaces.md) | `(): array` | `array` | -| [`get_declared_traits()`](./class/get_declared_traits.md) | `(): array` | `array` | -| [`get_parent_class()`](./class/get_parent_class.md) | `(mixed $object_or_class = null): string` | `string` | -| [`interface_exists()`](./class/interface_exists.md) | `(string $interface, bool $autoload = true): bool` | `bool` | -| [`is_a()`](./class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string = false): bool` | `bool` | -| [`is_subclass_of()`](./class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string = true): bool` | `bool` | -| [`trait_exists()`](./class/trait_exists.md) | `(string $trait, bool $autoload = true): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`class_alias()`](./class/class_alias.md) | `(string $class, string $alias, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`class_attribute_args()`](./class/class_attribute_args.md) | `(string $class_name, string $attribute_name): array` | `array` | ✓ | ✓ | +| [`class_attribute_names()`](./class/class_attribute_names.md) | `(string $class_name): array` | `array` | ✓ | ✓ | +| [`class_exists()`](./class/class_exists.md) | `(string $class, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`class_get_attributes()`](./class/class_get_attributes.md) | `(string $class_name): array` | `array` | ✓ | ✓ | +| [`class_implements()`](./class/class_implements.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`class_parents()`](./class/class_parents.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`class_uses()`](./class/class_uses.md) | `(mixed $object_or_class, bool $autoload = true): mixed` | `mixed` | ✓ | ✓ | +| [`enum_exists()`](./class/enum_exists.md) | `(string $enum, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`function_exists()`](./class/function_exists.md) | `(string $function): bool` | `bool` | ✓ | ✓ | +| [`get_called_class()`](./class/get_called_class.md) | `(): mixed` | `mixed` | — | ✓ | +| [`get_class()`](./class/get_class.md) | `(object $object = null): string` | `string` | ✓ | ✓ | +| [`get_class_methods()`](./class/get_class_methods.md) | `(mixed $object_or_class): mixed` | `mixed` | — | ✓ | +| [`get_class_vars()`](./class/get_class_vars.md) | `(mixed $class): mixed` | `mixed` | — | ✓ | +| [`get_declared_classes()`](./class/get_declared_classes.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_declared_interfaces()`](./class/get_declared_interfaces.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_declared_traits()`](./class/get_declared_traits.md) | `(): array` | `array` | ✓ | ✓ | +| [`get_object_vars()`](./class/get_object_vars.md) | `(mixed $object): mixed` | `mixed` | — | ✓ | +| [`get_parent_class()`](./class/get_parent_class.md) | `(mixed $object_or_class = null): string` | `string` | ✓ | ✓ | +| [`interface_exists()`](./class/interface_exists.md) | `(string $interface, bool $autoload = true): bool` | `bool` | ✓ | ✓ | +| [`is_a()`](./class/is_a.md) | `(object $object_or_class, string $class, bool $allow_string = false): bool` | `bool` | ✓ | ✓ | +| [`is_subclass_of()`](./class/is_subclass_of.md) | `(mixed $object_or_class, string $class, bool $allow_string = true): bool` | `bool` | ✓ | ✓ | +| [`trait_exists()`](./class/trait_exists.md) | `(string $trait, bool $autoload = true): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/class/class_alias.md b/docs/php/builtins/class/class_alias.md index 3afbf9fbd2..d08c8cd9e3 100644 --- a/docs/php/builtins/class/class_alias.md +++ b/docs/php/builtins/class/class_alias.md @@ -20,6 +20,11 @@ Creates an alias for a class. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_attribute_args.md b/docs/php/builtins/class/class_attribute_args.md index 554d29d965..6302ef4669 100644 --- a/docs/php/builtins/class/class_attribute_args.md +++ b/docs/php/builtins/class/class_attribute_args.md @@ -19,6 +19,11 @@ Returns the constructor arguments of a named attribute applied to a class. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_attribute_names.md b/docs/php/builtins/class/class_attribute_names.md index 78fb12b96c..4d5120b20b 100644 --- a/docs/php/builtins/class/class_attribute_names.md +++ b/docs/php/builtins/class/class_attribute_names.md @@ -18,6 +18,11 @@ Returns the list of attribute names applied to a class. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_exists.md b/docs/php/builtins/class/class_exists.md index 2fad0f1dcd..e8634808ba 100644 --- a/docs/php/builtins/class/class_exists.md +++ b/docs/php/builtins/class/class_exists.md @@ -19,6 +19,11 @@ Checks whether the given class has been defined. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_get_attributes.md b/docs/php/builtins/class/class_get_attributes.md index 1baa768aab..e02fab8ba2 100644 --- a/docs/php/builtins/class/class_get_attributes.md +++ b/docs/php/builtins/class/class_get_attributes.md @@ -18,6 +18,11 @@ Returns an array of ReflectionAttribute objects for all attributes of a class. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_implements.md b/docs/php/builtins/class/class_implements.md index 138b873524..ae568fc5ed 100644 --- a/docs/php/builtins/class/class_implements.md +++ b/docs/php/builtins/class/class_implements.md @@ -19,6 +19,11 @@ Returns the interfaces which are implemented by the given class or its parents. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_parents.md b/docs/php/builtins/class/class_parents.md index 8bf00fa244..1a180532e7 100644 --- a/docs/php/builtins/class/class_parents.md +++ b/docs/php/builtins/class/class_parents.md @@ -19,6 +19,11 @@ Returns the parent classes of the given class. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/class_uses.md b/docs/php/builtins/class/class_uses.md index b2da4b4185..b0bb348d89 100644 --- a/docs/php/builtins/class/class_uses.md +++ b/docs/php/builtins/class/class_uses.md @@ -19,6 +19,11 @@ Returns the traits used by the given class. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/enum_exists.md b/docs/php/builtins/class/enum_exists.md index a1389d8667..4d556eedd1 100644 --- a/docs/php/builtins/class/enum_exists.md +++ b/docs/php/builtins/class/enum_exists.md @@ -19,6 +19,11 @@ Checks if the enum has been defined. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/function_exists.md b/docs/php/builtins/class/function_exists.md index e358d8e6d0..48a003c47f 100644 --- a/docs/php/builtins/class/function_exists.md +++ b/docs/php/builtins/class/function_exists.md @@ -18,6 +18,11 @@ Returns true if the given function has been defined. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_called_class.md b/docs/php/builtins/class/get_called_class.md new file mode 100644 index 0000000000..5d4988d857 --- /dev/null +++ b/docs/php/builtins/class/get_called_class.md @@ -0,0 +1,32 @@ +--- +title: "get_called_class()" +description: "get_called_class() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." +sidebar: + order: 76 +--- + +## get_called_class() + +```php +function get_called_class(): mixed +``` + +get_called_class() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet. + +**Parameters**: none. + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: not available — compiled programs cannot call this builtin yet. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + diff --git a/docs/php/builtins/class/get_class.md b/docs/php/builtins/class/get_class.md index 082cda5c3e..ecd0ae69f9 100644 --- a/docs/php/builtins/class/get_class.md +++ b/docs/php/builtins/class/get_class.md @@ -2,7 +2,7 @@ title: "get_class()" description: "Returns the name of the class of an object." sidebar: - order: 76 + order: 77 --- ## get_class() @@ -18,6 +18,11 @@ Returns the name of the class of an object. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_class_methods.md b/docs/php/builtins/class/get_class_methods.md new file mode 100644 index 0000000000..c87045dfb7 --- /dev/null +++ b/docs/php/builtins/class/get_class_methods.md @@ -0,0 +1,33 @@ +--- +title: "get_class_methods()" +description: "get_class_methods() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." +sidebar: + order: 78 +--- + +## get_class_methods() + +```php +function get_class_methods(mixed $object_or_class): mixed +``` + +get_class_methods() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet. + +**Parameters**: +- `$object_or_class` (`mixed`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: not available — compiled programs cannot call this builtin yet. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + diff --git a/docs/php/builtins/class/get_class_vars.md b/docs/php/builtins/class/get_class_vars.md new file mode 100644 index 0000000000..bd2e53dcfe --- /dev/null +++ b/docs/php/builtins/class/get_class_vars.md @@ -0,0 +1,33 @@ +--- +title: "get_class_vars()" +description: "get_class_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." +sidebar: + order: 79 +--- + +## get_class_vars() + +```php +function get_class_vars(mixed $class): mixed +``` + +get_class_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet. + +**Parameters**: +- `$class` (`mixed`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: not available — compiled programs cannot call this builtin yet. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + diff --git a/docs/php/builtins/class/get_declared_classes.md b/docs/php/builtins/class/get_declared_classes.md index 150f682d7e..0b7833f62b 100644 --- a/docs/php/builtins/class/get_declared_classes.md +++ b/docs/php/builtins/class/get_declared_classes.md @@ -2,7 +2,7 @@ title: "get_declared_classes()" description: "Returns an array of the names of the defined classes." sidebar: - order: 77 + order: 80 --- ## get_declared_classes() @@ -17,6 +17,11 @@ Returns an array of the names of the defined classes. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_declared_interfaces.md b/docs/php/builtins/class/get_declared_interfaces.md index 0994cbbb8c..c70d2fd2fc 100644 --- a/docs/php/builtins/class/get_declared_interfaces.md +++ b/docs/php/builtins/class/get_declared_interfaces.md @@ -2,7 +2,7 @@ title: "get_declared_interfaces()" description: "Returns an array of all declared interfaces." sidebar: - order: 78 + order: 81 --- ## get_declared_interfaces() @@ -17,6 +17,11 @@ Returns an array of all declared interfaces. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_declared_traits.md b/docs/php/builtins/class/get_declared_traits.md index 0c4cc105b9..b84fe80b07 100644 --- a/docs/php/builtins/class/get_declared_traits.md +++ b/docs/php/builtins/class/get_declared_traits.md @@ -2,7 +2,7 @@ title: "get_declared_traits()" description: "Returns an array of all declared traits." sidebar: - order: 79 + order: 82 --- ## get_declared_traits() @@ -17,6 +17,11 @@ Returns an array of all declared traits. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/get_object_vars.md b/docs/php/builtins/class/get_object_vars.md new file mode 100644 index 0000000000..40cfdd3a0e --- /dev/null +++ b/docs/php/builtins/class/get_object_vars.md @@ -0,0 +1,33 @@ +--- +title: "get_object_vars()" +description: "get_object_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet." +sidebar: + order: 83 +--- + +## get_object_vars() + +```php +function get_object_vars(mixed $object): mixed +``` + +get_object_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet. + +**Parameters**: +- `$object` (`mixed`) + +**Returns**: `mixed` + +## Availability + +- **Compiled (AOT)**: not available — compiled programs cannot call this builtin yet. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs)). + +_No examples yet — check `examples/` and `showcases/` for usage patterns._ + + + + + + + diff --git a/docs/php/builtins/class/get_parent_class.md b/docs/php/builtins/class/get_parent_class.md index 35ae146525..ceaa2b2d3f 100644 --- a/docs/php/builtins/class/get_parent_class.md +++ b/docs/php/builtins/class/get_parent_class.md @@ -2,7 +2,7 @@ title: "get_parent_class()" description: "Returns the name of the parent class of an object or class." sidebar: - order: 80 + order: 84 --- ## get_parent_class() @@ -18,6 +18,11 @@ Returns the name of the parent class of an object or class. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/interface_exists.md b/docs/php/builtins/class/interface_exists.md index 3e39f7d827..76002a54e7 100644 --- a/docs/php/builtins/class/interface_exists.md +++ b/docs/php/builtins/class/interface_exists.md @@ -2,7 +2,7 @@ title: "interface_exists()" description: "Checks if the interface has been defined." sidebar: - order: 81 + order: 85 --- ## interface_exists() @@ -19,6 +19,11 @@ Checks if the interface has been defined. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/is_a.md b/docs/php/builtins/class/is_a.md index 6eda7d1fac..e1425d8ba8 100644 --- a/docs/php/builtins/class/is_a.md +++ b/docs/php/builtins/class/is_a.md @@ -2,7 +2,7 @@ title: "is_a()" description: "Checks whether an object is of a given type or has it as one of its parents." sidebar: - order: 82 + order: 86 --- ## is_a() @@ -20,6 +20,11 @@ Checks whether an object is of a given type or has it as one of its parents. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/is_subclass_of.md b/docs/php/builtins/class/is_subclass_of.md index 72822ee334..a906d499a1 100644 --- a/docs/php/builtins/class/is_subclass_of.md +++ b/docs/php/builtins/class/is_subclass_of.md @@ -2,7 +2,7 @@ title: "is_subclass_of()" description: "Checks if the object has a given class as one of its parents or implements it." sidebar: - order: 83 + order: 87 --- ## is_subclass_of() @@ -20,6 +20,11 @@ Checks if the object has a given class as one of its parents or implements it. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/class/trait_exists.md b/docs/php/builtins/class/trait_exists.md index de92f3a1c8..df33e8438c 100644 --- a/docs/php/builtins/class/trait_exists.md +++ b/docs/php/builtins/class/trait_exists.md @@ -2,7 +2,7 @@ title: "trait_exists()" description: "Checks whether the trait exists." sidebar: - order: 84 + order: 88 --- ## trait_exists() @@ -19,6 +19,11 @@ Checks whether the trait exists. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date.md b/docs/php/builtins/date.md index d5c0b90298..9a157e2d1d 100644 --- a/docs/php/builtins/date.md +++ b/docs/php/builtins/date.md @@ -7,18 +7,18 @@ sidebar: ## Date builtins -| Function | Signature | Returns | -|---|---|---| -| [`checkdate()`](./date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | -| [`date()`](./date/date.md) | `(string $format, int $timestamp = null): string` | `string` | -| [`date_default_timezone_get()`](./date/date_default_timezone_get.md) | `(): string` | `string` | -| [`date_default_timezone_set()`](./date/date_default_timezone_set.md) | `(string $timezoneId): bool` | `bool` | -| [`getdate()`](./date/getdate.md) | `(int $timestamp = null): array` | `array` | -| [`gmdate()`](./date/gmdate.md) | `(string $format, int $timestamp = null): string` | `string` | -| [`gmmktime()`](./date/gmmktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | -| [`hrtime()`](./date/hrtime.md) | `(bool $as_number = false): mixed` | `mixed` | -| [`localtime()`](./date/localtime.md) | `(int $timestamp = -1, bool $associative = false): array` | `array` | -| [`microtime()`](./date/microtime.md) | `(bool $as_float = false): mixed` | `mixed` | -| [`mktime()`](./date/mktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | -| [`strtotime()`](./date/strtotime.md) | `(string $datetime, int $baseTimestamp = null): mixed` | `mixed` | -| [`time()`](./date/time.md) | `(): int` | `int` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`checkdate()`](./date/checkdate.md) | `(int $month, int $day, int $year): bool` | `bool` | ✓ | ✓ | +| [`date()`](./date/date.md) | `(string $format, int $timestamp = null): string` | `string` | ✓ | ✓ | +| [`date_default_timezone_get()`](./date/date_default_timezone_get.md) | `(): string` | `string` | ✓ | ✓ | +| [`date_default_timezone_set()`](./date/date_default_timezone_set.md) | `(string $timezoneId): bool` | `bool` | ✓ | ✓ | +| [`getdate()`](./date/getdate.md) | `(int $timestamp = null): array` | `array` | ✓ | ✓ | +| [`gmdate()`](./date/gmdate.md) | `(string $format, int $timestamp = null): string` | `string` | ✓ | ✓ | +| [`gmmktime()`](./date/gmmktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | ✓ | ✓ | +| [`hrtime()`](./date/hrtime.md) | `(bool $as_number = false): mixed` | `mixed` | ✓ | ✓ | +| [`localtime()`](./date/localtime.md) | `(int $timestamp = -1, bool $associative = false): array` | `array` | ✓ | ✓ | +| [`microtime()`](./date/microtime.md) | `(bool $as_float = false): mixed` | `mixed` | ✓ | ✓ | +| [`mktime()`](./date/mktime.md) | `(int $hour, int $minute, int $second, int $month, int $day, int $year): int` | `int` | ✓ | ✓ | +| [`strtotime()`](./date/strtotime.md) | `(string $datetime, int $baseTimestamp = null): mixed` | `mixed` | ✓ | ✓ | +| [`time()`](./date/time.md) | `(): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/date/checkdate.md b/docs/php/builtins/date/checkdate.md index 3f9f33d68f..3338e091e3 100644 --- a/docs/php/builtins/date/checkdate.md +++ b/docs/php/builtins/date/checkdate.md @@ -2,7 +2,7 @@ title: "checkdate()" description: "Validates a Gregorian date." sidebar: - order: 85 + order: 89 --- ## checkdate() @@ -20,6 +20,11 @@ Validates a Gregorian date. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/date.md b/docs/php/builtins/date/date.md index 8251d76f38..8f325c551e 100644 --- a/docs/php/builtins/date/date.md +++ b/docs/php/builtins/date/date.md @@ -2,7 +2,7 @@ title: "date()" description: "Formats a local time/date." sidebar: - order: 86 + order: 90 --- ## date() @@ -19,6 +19,11 @@ Formats a local time/date. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/date.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/date_default_timezone_get.md b/docs/php/builtins/date/date_default_timezone_get.md index 52e9b0f470..3633ce50e7 100644 --- a/docs/php/builtins/date/date_default_timezone_get.md +++ b/docs/php/builtins/date/date_default_timezone_get.md @@ -2,7 +2,7 @@ title: "date_default_timezone_get()" description: "Gets the default timezone." sidebar: - order: 87 + order: 91 --- ## date_default_timezone_get() @@ -17,6 +17,11 @@ Gets the default timezone. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/date_default_timezone_set.md b/docs/php/builtins/date/date_default_timezone_set.md index 8f83807597..7d9e64756c 100644 --- a/docs/php/builtins/date/date_default_timezone_set.md +++ b/docs/php/builtins/date/date_default_timezone_set.md @@ -2,7 +2,7 @@ title: "date_default_timezone_set()" description: "Sets the default timezone." sidebar: - order: 88 + order: 92 --- ## date_default_timezone_set() @@ -18,6 +18,11 @@ Sets the default timezone. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/getdate.md b/docs/php/builtins/date/getdate.md index ac3b17ed08..4df60eec82 100644 --- a/docs/php/builtins/date/getdate.md +++ b/docs/php/builtins/date/getdate.md @@ -2,7 +2,7 @@ title: "getdate()" description: "Returns date/time information." sidebar: - order: 89 + order: 93 --- ## getdate() @@ -18,6 +18,11 @@ Returns date/time information. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/getdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/getdate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/gmdate.md b/docs/php/builtins/date/gmdate.md index 58cc570ae0..6752062fc1 100644 --- a/docs/php/builtins/date/gmdate.md +++ b/docs/php/builtins/date/gmdate.md @@ -2,7 +2,7 @@ title: "gmdate()" description: "Formats a GMT/UTC date and time." sidebar: - order: 90 + order: 94 --- ## gmdate() @@ -19,6 +19,11 @@ Formats a GMT/UTC date and time. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/gmmktime.md b/docs/php/builtins/date/gmmktime.md index 69b8f37f89..6fd65dd616 100644 --- a/docs/php/builtins/date/gmmktime.md +++ b/docs/php/builtins/date/gmmktime.md @@ -2,7 +2,7 @@ title: "gmmktime()" description: "Returns the Unix timestamp for a GMT date." sidebar: - order: 91 + order: 95 --- ## gmmktime() @@ -23,6 +23,11 @@ Returns the Unix timestamp for a GMT date. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/hrtime.md b/docs/php/builtins/date/hrtime.md index 764f21dffc..cd3f8d0e58 100644 --- a/docs/php/builtins/date/hrtime.md +++ b/docs/php/builtins/date/hrtime.md @@ -2,7 +2,7 @@ title: "hrtime()" description: "Returns the current high-resolution time." sidebar: - order: 92 + order: 96 --- ## hrtime() @@ -18,6 +18,11 @@ Returns the current high-resolution time. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/localtime.md b/docs/php/builtins/date/localtime.md index 7afafc1967..cdeed577af 100644 --- a/docs/php/builtins/date/localtime.md +++ b/docs/php/builtins/date/localtime.md @@ -2,7 +2,7 @@ title: "localtime()" description: "Returns the local time." sidebar: - order: 93 + order: 97 --- ## localtime() @@ -19,6 +19,11 @@ Returns the local time. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/localtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/localtime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/microtime.md b/docs/php/builtins/date/microtime.md index 22abe5ed1f..295d1c0029 100644 --- a/docs/php/builtins/date/microtime.md +++ b/docs/php/builtins/date/microtime.md @@ -2,7 +2,7 @@ title: "microtime()" description: "Returns the current Unix timestamp with microseconds." sidebar: - order: 94 + order: 98 --- ## microtime() @@ -18,6 +18,11 @@ Returns the current Unix timestamp with microseconds. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/microtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/microtime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/mktime.md b/docs/php/builtins/date/mktime.md index 109320d678..93f4c14133 100644 --- a/docs/php/builtins/date/mktime.md +++ b/docs/php/builtins/date/mktime.md @@ -2,7 +2,7 @@ title: "mktime()" description: "Returns the Unix timestamp for a date." sidebar: - order: 95 + order: 99 --- ## mktime() @@ -23,6 +23,11 @@ Returns the Unix timestamp for a date. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/mktime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/mktime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/strtotime.md b/docs/php/builtins/date/strtotime.md index b367f34f5c..3dc4815f48 100644 --- a/docs/php/builtins/date/strtotime.md +++ b/docs/php/builtins/date/strtotime.md @@ -2,7 +2,7 @@ title: "strtotime()" description: "Parses an English textual datetime description into a Unix timestamp." sidebar: - order: 96 + order: 100 --- ## strtotime() @@ -19,6 +19,11 @@ Parses an English textual datetime description into a Unix timestamp. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/date/time.md b/docs/php/builtins/date/time.md index a100865cf0..2d852df176 100644 --- a/docs/php/builtins/date/time.md +++ b/docs/php/builtins/date/time.md @@ -2,7 +2,7 @@ title: "time()" description: "Returns the current Unix timestamp." sidebar: - order: 97 + order: 101 --- ## time() @@ -17,6 +17,11 @@ Returns the current Unix timestamp. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/time.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/time.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem.md b/docs/php/builtins/filesystem.md index 1edc01cb60..d8c9af2cf3 100644 --- a/docs/php/builtins/filesystem.md +++ b/docs/php/builtins/filesystem.md @@ -7,60 +7,60 @@ sidebar: ## Filesystem builtins -| Function | Signature | Returns | -|---|---|---| -| [`basename()`](./filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | -| [`chdir()`](./filesystem/chdir.md) | `(string $directory): bool` | `bool` | -| [`chgrp()`](./filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | -| [`chmod()`](./filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | -| [`chown()`](./filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | -| [`clearstatcache()`](./filesystem/clearstatcache.md) | `(bool $clear_realpath_cache = false, string $filename = ''): void` | `void` | -| [`copy()`](./filesystem/copy.md) | `(string $from, string $to): bool` | `bool` | -| [`dirname()`](./filesystem/dirname.md) | `(string $path, int $levels = 1): string` | `string` | -| [`disk_free_space()`](./filesystem/disk_free_space.md) | `(string $directory): float` | `float` | -| [`disk_total_space()`](./filesystem/disk_total_space.md) | `(string $directory): float` | `float` | -| [`file_exists()`](./filesystem/file_exists.md) | `(string $filename): bool` | `bool` | -| [`fileatime()`](./filesystem/fileatime.md) | `(string $filename): mixed` | `mixed` | -| [`filectime()`](./filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | -| [`filegroup()`](./filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | -| [`fileinode()`](./filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | -| [`filemtime()`](./filesystem/filemtime.md) | `(string $filename): int` | `int` | -| [`fileowner()`](./filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | -| [`fileperms()`](./filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | -| [`filesize()`](./filesystem/filesize.md) | `(string $filename): int` | `int` | -| [`filetype()`](./filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | -| [`fnmatch()`](./filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | -| [`getcwd()`](./filesystem/getcwd.md) | `(): string` | `string` | -| [`getenv()`](./filesystem/getenv.md) | `(string $name): mixed` | `mixed` | -| [`glob()`](./filesystem/glob.md) | `(string $pattern): array` | `array` | -| [`is_dir()`](./filesystem/is_dir.md) | `(string $filename): bool` | `bool` | -| [`is_executable()`](./filesystem/is_executable.md) | `(string $filename): bool` | `bool` | -| [`is_file()`](./filesystem/is_file.md) | `(string $filename): bool` | `bool` | -| [`is_link()`](./filesystem/is_link.md) | `(string $filename): bool` | `bool` | -| [`is_readable()`](./filesystem/is_readable.md) | `(string $filename): bool` | `bool` | -| [`is_writable()`](./filesystem/is_writable.md) | `(string $filename): bool` | `bool` | -| [`is_writeable()`](./filesystem/is_writeable.md) | `(string $filename): bool` | `bool` | -| [`lchgrp()`](./filesystem/lchgrp.md) | `(string $filename, string $group): bool` | `bool` | -| [`lchown()`](./filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | -| [`link()`](./filesystem/link.md) | `(string $target, string $link): bool` | `bool` | -| [`linkinfo()`](./filesystem/linkinfo.md) | `(string $path): int` | `int` | -| [`lstat()`](./filesystem/lstat.md) | `(string $filename): mixed` | `mixed` | -| [`mkdir()`](./filesystem/mkdir.md) | `(string $directory): bool` | `bool` | -| [`pathinfo()`](./filesystem/pathinfo.md) | `(string $path, int $flags = 15): array` | `array` | -| [`putenv()`](./filesystem/putenv.md) | `(string $assignment): bool` | `bool` | -| [`readfile()`](./filesystem/readfile.md) | `(string $filename): mixed` | `mixed` | -| [`readlink()`](./filesystem/readlink.md) | `(string $path): mixed` | `mixed` | -| [`realpath()`](./filesystem/realpath.md) | `(string $path): mixed` | `mixed` | -| [`realpath_cache_get()`](./filesystem/realpath_cache_get.md) | `(): array` | `array` | -| [`realpath_cache_size()`](./filesystem/realpath_cache_size.md) | `(): int` | `int` | -| [`rename()`](./filesystem/rename.md) | `(string $from, string $to): bool` | `bool` | -| [`rmdir()`](./filesystem/rmdir.md) | `(string $directory): bool` | `bool` | -| [`scandir()`](./filesystem/scandir.md) | `(string $directory): array` | `array` | -| [`stat()`](./filesystem/stat.md) | `(string $filename): mixed` | `mixed` | -| [`symlink()`](./filesystem/symlink.md) | `(string $target, string $link): bool` | `bool` | -| [`sys_get_temp_dir()`](./filesystem/sys_get_temp_dir.md) | `(): string` | `string` | -| [`tempnam()`](./filesystem/tempnam.md) | `(string $directory, string $prefix): string` | `string` | -| [`tmpfile()`](./filesystem/tmpfile.md) | `(): mixed` | `mixed` | -| [`touch()`](./filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | -| [`umask()`](./filesystem/umask.md) | `(int $mask = null): int` | `int` | -| [`unlink()`](./filesystem/unlink.md) | `(string $filename): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`basename()`](./filesystem/basename.md) | `(string $path, string $suffix = ''): string` | `string` | ✓ | ✓ | +| [`chdir()`](./filesystem/chdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`chgrp()`](./filesystem/chgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`chmod()`](./filesystem/chmod.md) | `(string $filename, int $permissions): bool` | `bool` | ✓ | ✓ | +| [`chown()`](./filesystem/chown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`clearstatcache()`](./filesystem/clearstatcache.md) | `(bool $clear_realpath_cache = false, string $filename = ''): void` | `void` | ✓ | ✓ | +| [`copy()`](./filesystem/copy.md) | `(string $from, string $to): bool` | `bool` | ✓ | ✓ | +| [`dirname()`](./filesystem/dirname.md) | `(string $path, int $levels = 1): string` | `string` | ✓ | ✓ | +| [`disk_free_space()`](./filesystem/disk_free_space.md) | `(string $directory): float` | `float` | ✓ | ✓ | +| [`disk_total_space()`](./filesystem/disk_total_space.md) | `(string $directory): float` | `float` | ✓ | ✓ | +| [`file_exists()`](./filesystem/file_exists.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`fileatime()`](./filesystem/fileatime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filectime()`](./filesystem/filectime.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filegroup()`](./filesystem/filegroup.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fileinode()`](./filesystem/fileinode.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filemtime()`](./filesystem/filemtime.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`fileowner()`](./filesystem/fileowner.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fileperms()`](./filesystem/fileperms.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`filesize()`](./filesystem/filesize.md) | `(string $filename): int` | `int` | ✓ | ✓ | +| [`filetype()`](./filesystem/filetype.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`fnmatch()`](./filesystem/fnmatch.md) | `(string $pattern, string $filename, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`getcwd()`](./filesystem/getcwd.md) | `(): string` | `string` | ✓ | ✓ | +| [`getenv()`](./filesystem/getenv.md) | `(string $name): mixed` | `mixed` | ✓ | ✓ | +| [`glob()`](./filesystem/glob.md) | `(string $pattern): array` | `array` | ✓ | ✓ | +| [`is_dir()`](./filesystem/is_dir.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_executable()`](./filesystem/is_executable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_file()`](./filesystem/is_file.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_link()`](./filesystem/is_link.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_readable()`](./filesystem/is_readable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_writable()`](./filesystem/is_writable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`is_writeable()`](./filesystem/is_writeable.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | +| [`lchgrp()`](./filesystem/lchgrp.md) | `(string $filename, string $group): bool` | `bool` | ✓ | ✓ | +| [`lchown()`](./filesystem/lchown.md) | `(string $filename, string $user): bool` | `bool` | ✓ | ✓ | +| [`link()`](./filesystem/link.md) | `(string $target, string $link): bool` | `bool` | ✓ | ✓ | +| [`linkinfo()`](./filesystem/linkinfo.md) | `(string $path): int` | `int` | ✓ | ✓ | +| [`lstat()`](./filesystem/lstat.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`mkdir()`](./filesystem/mkdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`pathinfo()`](./filesystem/pathinfo.md) | `(string $path, int $flags = 15): array` | `array` | ✓ | ✓ | +| [`putenv()`](./filesystem/putenv.md) | `(string $assignment): bool` | `bool` | ✓ | ✓ | +| [`readfile()`](./filesystem/readfile.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`readlink()`](./filesystem/readlink.md) | `(string $path): mixed` | `mixed` | ✓ | ✓ | +| [`realpath()`](./filesystem/realpath.md) | `(string $path): mixed` | `mixed` | ✓ | ✓ | +| [`realpath_cache_get()`](./filesystem/realpath_cache_get.md) | `(): array` | `array` | ✓ | ✓ | +| [`realpath_cache_size()`](./filesystem/realpath_cache_size.md) | `(): int` | `int` | ✓ | ✓ | +| [`rename()`](./filesystem/rename.md) | `(string $from, string $to): bool` | `bool` | ✓ | ✓ | +| [`rmdir()`](./filesystem/rmdir.md) | `(string $directory): bool` | `bool` | ✓ | ✓ | +| [`scandir()`](./filesystem/scandir.md) | `(string $directory): array` | `array` | ✓ | ✓ | +| [`stat()`](./filesystem/stat.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`symlink()`](./filesystem/symlink.md) | `(string $target, string $link): bool` | `bool` | ✓ | ✓ | +| [`sys_get_temp_dir()`](./filesystem/sys_get_temp_dir.md) | `(): string` | `string` | ✓ | ✓ | +| [`tempnam()`](./filesystem/tempnam.md) | `(string $directory, string $prefix): string` | `string` | ✓ | ✓ | +| [`tmpfile()`](./filesystem/tmpfile.md) | `(): mixed` | `mixed` | ✓ | ✓ | +| [`touch()`](./filesystem/touch.md) | `(string $filename, int $mtime = null, int $atime = null): bool` | `bool` | ✓ | ✓ | +| [`umask()`](./filesystem/umask.md) | `(int $mask = null): int` | `int` | ✓ | ✓ | +| [`unlink()`](./filesystem/unlink.md) | `(string $filename): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/filesystem/basename.md b/docs/php/builtins/filesystem/basename.md index b7fcae60c7..b7ede7c35d 100644 --- a/docs/php/builtins/filesystem/basename.md +++ b/docs/php/builtins/filesystem/basename.md @@ -2,7 +2,7 @@ title: "basename()" description: "Returns the trailing name component of a path." sidebar: - order: 98 + order: 102 --- ## basename() @@ -19,6 +19,11 @@ Returns the trailing name component of a path. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/chdir.md b/docs/php/builtins/filesystem/chdir.md index 79eb4bd9bb..1b04de9d4e 100644 --- a/docs/php/builtins/filesystem/chdir.md +++ b/docs/php/builtins/filesystem/chdir.md @@ -2,7 +2,7 @@ title: "chdir()" description: "Changes the current directory." sidebar: - order: 99 + order: 103 --- ## chdir() @@ -18,6 +18,11 @@ Changes the current directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/chgrp.md b/docs/php/builtins/filesystem/chgrp.md index a5f7d23632..17bddd4e80 100644 --- a/docs/php/builtins/filesystem/chgrp.md +++ b/docs/php/builtins/filesystem/chgrp.md @@ -2,7 +2,7 @@ title: "chgrp()" description: "Changes file group." sidebar: - order: 100 + order: 104 --- ## chgrp() @@ -19,6 +19,11 @@ Changes file group. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/chmod.md b/docs/php/builtins/filesystem/chmod.md index 23a5012e9e..dbb1dfd042 100644 --- a/docs/php/builtins/filesystem/chmod.md +++ b/docs/php/builtins/filesystem/chmod.md @@ -2,7 +2,7 @@ title: "chmod()" description: "Changes file mode." sidebar: - order: 101 + order: 105 --- ## chmod() @@ -19,6 +19,11 @@ Changes file mode. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/chown.md b/docs/php/builtins/filesystem/chown.md index 26d53c1398..6321a5fa86 100644 --- a/docs/php/builtins/filesystem/chown.md +++ b/docs/php/builtins/filesystem/chown.md @@ -2,7 +2,7 @@ title: "chown()" description: "Changes file owner." sidebar: - order: 102 + order: 106 --- ## chown() @@ -19,6 +19,11 @@ Changes file owner. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/clearstatcache.md b/docs/php/builtins/filesystem/clearstatcache.md index 47f99f901c..57ad08f8d6 100644 --- a/docs/php/builtins/filesystem/clearstatcache.md +++ b/docs/php/builtins/filesystem/clearstatcache.md @@ -2,7 +2,7 @@ title: "clearstatcache()" description: "Clears file status cache." sidebar: - order: 103 + order: 107 --- ## clearstatcache() @@ -19,6 +19,11 @@ Clears file status cache. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/copy.md b/docs/php/builtins/filesystem/copy.md index 3d08aa2588..fe60932396 100644 --- a/docs/php/builtins/filesystem/copy.md +++ b/docs/php/builtins/filesystem/copy.md @@ -2,7 +2,7 @@ title: "copy()" description: "Copies a file." sidebar: - order: 104 + order: 108 --- ## copy() @@ -19,6 +19,11 @@ Copies a file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/dirname.md b/docs/php/builtins/filesystem/dirname.md index 4fe76dc5f5..be2ffd47f6 100644 --- a/docs/php/builtins/filesystem/dirname.md +++ b/docs/php/builtins/filesystem/dirname.md @@ -2,7 +2,7 @@ title: "dirname()" description: "Returns a parent directory's path." sidebar: - order: 105 + order: 109 --- ## dirname() @@ -19,6 +19,11 @@ Returns a parent directory's path. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/disk_free_space.md b/docs/php/builtins/filesystem/disk_free_space.md index a13b08e44d..d845244797 100644 --- a/docs/php/builtins/filesystem/disk_free_space.md +++ b/docs/php/builtins/filesystem/disk_free_space.md @@ -2,7 +2,7 @@ title: "disk_free_space()" description: "Returns available space on filesystem or disk partition." sidebar: - order: 106 + order: 110 --- ## disk_free_space() @@ -18,6 +18,11 @@ Returns available space on filesystem or disk partition. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/disk_total_space.md b/docs/php/builtins/filesystem/disk_total_space.md index b774d51383..690fa6a8ff 100644 --- a/docs/php/builtins/filesystem/disk_total_space.md +++ b/docs/php/builtins/filesystem/disk_total_space.md @@ -2,7 +2,7 @@ title: "disk_total_space()" description: "Returns the total size of a filesystem or disk partition." sidebar: - order: 107 + order: 111 --- ## disk_total_space() @@ -18,6 +18,11 @@ Returns the total size of a filesystem or disk partition. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/file_exists.md b/docs/php/builtins/filesystem/file_exists.md index 526d596428..4f0abad943 100644 --- a/docs/php/builtins/filesystem/file_exists.md +++ b/docs/php/builtins/filesystem/file_exists.md @@ -2,7 +2,7 @@ title: "file_exists()" description: "Checks whether a file or directory exists." sidebar: - order: 108 + order: 112 --- ## file_exists() @@ -18,6 +18,11 @@ Checks whether a file or directory exists. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fileatime.md b/docs/php/builtins/filesystem/fileatime.md index 61b1f09e2d..e4abc65c70 100644 --- a/docs/php/builtins/filesystem/fileatime.md +++ b/docs/php/builtins/filesystem/fileatime.md @@ -2,7 +2,7 @@ title: "fileatime()" description: "Gets last access time of file." sidebar: - order: 109 + order: 113 --- ## fileatime() @@ -18,6 +18,11 @@ Gets last access time of file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filectime.md b/docs/php/builtins/filesystem/filectime.md index ea697c09f0..ab4424dc60 100644 --- a/docs/php/builtins/filesystem/filectime.md +++ b/docs/php/builtins/filesystem/filectime.md @@ -2,7 +2,7 @@ title: "filectime()" description: "Gets inode change time of file." sidebar: - order: 110 + order: 114 --- ## filectime() @@ -18,6 +18,11 @@ Gets inode change time of file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filegroup.md b/docs/php/builtins/filesystem/filegroup.md index 18bfc38b86..b56e3f1e92 100644 --- a/docs/php/builtins/filesystem/filegroup.md +++ b/docs/php/builtins/filesystem/filegroup.md @@ -2,7 +2,7 @@ title: "filegroup()" description: "Gets file group." sidebar: - order: 111 + order: 115 --- ## filegroup() @@ -18,6 +18,11 @@ Gets file group. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fileinode.md b/docs/php/builtins/filesystem/fileinode.md index 2110a291ef..3862fc18c5 100644 --- a/docs/php/builtins/filesystem/fileinode.md +++ b/docs/php/builtins/filesystem/fileinode.md @@ -2,7 +2,7 @@ title: "fileinode()" description: "Gets file inode." sidebar: - order: 112 + order: 116 --- ## fileinode() @@ -18,6 +18,11 @@ Gets file inode. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filemtime.md b/docs/php/builtins/filesystem/filemtime.md index dfd9aba90e..103b540ccc 100644 --- a/docs/php/builtins/filesystem/filemtime.md +++ b/docs/php/builtins/filesystem/filemtime.md @@ -2,7 +2,7 @@ title: "filemtime()" description: "Gets file modification time." sidebar: - order: 113 + order: 117 --- ## filemtime() @@ -18,6 +18,11 @@ Gets file modification time. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fileowner.md b/docs/php/builtins/filesystem/fileowner.md index b49e713523..109fd3b59f 100644 --- a/docs/php/builtins/filesystem/fileowner.md +++ b/docs/php/builtins/filesystem/fileowner.md @@ -2,7 +2,7 @@ title: "fileowner()" description: "Gets file owner." sidebar: - order: 114 + order: 118 --- ## fileowner() @@ -18,6 +18,11 @@ Gets file owner. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fileperms.md b/docs/php/builtins/filesystem/fileperms.md index ebb7453ba1..6c62b27d78 100644 --- a/docs/php/builtins/filesystem/fileperms.md +++ b/docs/php/builtins/filesystem/fileperms.md @@ -2,7 +2,7 @@ title: "fileperms()" description: "Gets file permissions." sidebar: - order: 115 + order: 119 --- ## fileperms() @@ -18,6 +18,11 @@ Gets file permissions. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filesize.md b/docs/php/builtins/filesystem/filesize.md index c655357e10..4fed54516d 100644 --- a/docs/php/builtins/filesystem/filesize.md +++ b/docs/php/builtins/filesystem/filesize.md @@ -2,7 +2,7 @@ title: "filesize()" description: "Gets file size." sidebar: - order: 116 + order: 120 --- ## filesize() @@ -18,6 +18,11 @@ Gets file size. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/filetype.md b/docs/php/builtins/filesystem/filetype.md index 61027757ed..e2e67300ac 100644 --- a/docs/php/builtins/filesystem/filetype.md +++ b/docs/php/builtins/filesystem/filetype.md @@ -2,7 +2,7 @@ title: "filetype()" description: "Gets file type." sidebar: - order: 117 + order: 121 --- ## filetype() @@ -18,6 +18,11 @@ Gets file type. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/fnmatch.md b/docs/php/builtins/filesystem/fnmatch.md index 0f8791cc5f..584accdf41 100644 --- a/docs/php/builtins/filesystem/fnmatch.md +++ b/docs/php/builtins/filesystem/fnmatch.md @@ -2,7 +2,7 @@ title: "fnmatch()" description: "Matches a filename against a pattern." sidebar: - order: 118 + order: 122 --- ## fnmatch() @@ -20,6 +20,11 @@ Matches a filename against a pattern. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/getcwd.md b/docs/php/builtins/filesystem/getcwd.md index 330eabaefa..9924acfb7b 100644 --- a/docs/php/builtins/filesystem/getcwd.md +++ b/docs/php/builtins/filesystem/getcwd.md @@ -2,7 +2,7 @@ title: "getcwd()" description: "Gets the current working directory." sidebar: - order: 119 + order: 123 --- ## getcwd() @@ -17,6 +17,11 @@ Gets the current working directory. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/getenv.md b/docs/php/builtins/filesystem/getenv.md index 4d5a9b30e5..7f13d2fd2c 100644 --- a/docs/php/builtins/filesystem/getenv.md +++ b/docs/php/builtins/filesystem/getenv.md @@ -2,7 +2,7 @@ title: "getenv()" description: "Gets the value of an environment variable." sidebar: - order: 120 + order: 124 --- ## getenv() @@ -18,6 +18,11 @@ Gets the value of an environment variable. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/glob.md b/docs/php/builtins/filesystem/glob.md index af23fa2cf9..8155b89884 100644 --- a/docs/php/builtins/filesystem/glob.md +++ b/docs/php/builtins/filesystem/glob.md @@ -2,7 +2,7 @@ title: "glob()" description: "Finds pathnames matching a pattern." sidebar: - order: 121 + order: 125 --- ## glob() @@ -18,6 +18,11 @@ Finds pathnames matching a pattern. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_dir.md b/docs/php/builtins/filesystem/is_dir.md index 967dd2819a..aca56bddee 100644 --- a/docs/php/builtins/filesystem/is_dir.md +++ b/docs/php/builtins/filesystem/is_dir.md @@ -2,7 +2,7 @@ title: "is_dir()" description: "Tells whether the filename is a directory." sidebar: - order: 122 + order: 126 --- ## is_dir() @@ -18,6 +18,11 @@ Tells whether the filename is a directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_executable.md b/docs/php/builtins/filesystem/is_executable.md index 08b532dac1..6b185798cb 100644 --- a/docs/php/builtins/filesystem/is_executable.md +++ b/docs/php/builtins/filesystem/is_executable.md @@ -2,7 +2,7 @@ title: "is_executable()" description: "Tells whether the filename is executable." sidebar: - order: 123 + order: 127 --- ## is_executable() @@ -18,6 +18,11 @@ Tells whether the filename is executable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_file.md b/docs/php/builtins/filesystem/is_file.md index b54871fded..53279f1690 100644 --- a/docs/php/builtins/filesystem/is_file.md +++ b/docs/php/builtins/filesystem/is_file.md @@ -2,7 +2,7 @@ title: "is_file()" description: "Tells whether the filename is a regular file." sidebar: - order: 124 + order: 128 --- ## is_file() @@ -18,6 +18,11 @@ Tells whether the filename is a regular file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_link.md b/docs/php/builtins/filesystem/is_link.md index 96a1ee53ae..98e113c928 100644 --- a/docs/php/builtins/filesystem/is_link.md +++ b/docs/php/builtins/filesystem/is_link.md @@ -2,7 +2,7 @@ title: "is_link()" description: "Tells whether the filename is a symbolic link." sidebar: - order: 125 + order: 129 --- ## is_link() @@ -18,6 +18,11 @@ Tells whether the filename is a symbolic link. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_readable.md b/docs/php/builtins/filesystem/is_readable.md index c024a11243..03347f76f1 100644 --- a/docs/php/builtins/filesystem/is_readable.md +++ b/docs/php/builtins/filesystem/is_readable.md @@ -2,7 +2,7 @@ title: "is_readable()" description: "Tells whether the filename is readable." sidebar: - order: 126 + order: 130 --- ## is_readable() @@ -18,6 +18,11 @@ Tells whether the filename is readable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_writable.md b/docs/php/builtins/filesystem/is_writable.md index 2d121f3082..21b3826a43 100644 --- a/docs/php/builtins/filesystem/is_writable.md +++ b/docs/php/builtins/filesystem/is_writable.md @@ -2,7 +2,7 @@ title: "is_writable()" description: "Tells whether the filename is writable." sidebar: - order: 127 + order: 131 --- ## is_writable() @@ -18,6 +18,11 @@ Tells whether the filename is writable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/is_writeable.md b/docs/php/builtins/filesystem/is_writeable.md index d5c732bdcb..3f3a1cdd51 100644 --- a/docs/php/builtins/filesystem/is_writeable.md +++ b/docs/php/builtins/filesystem/is_writeable.md @@ -2,7 +2,7 @@ title: "is_writeable()" description: "Tells whether the filename is writable (alias of is_writable)." sidebar: - order: 128 + order: 132 --- ## is_writeable() @@ -18,6 +18,11 @@ Tells whether the filename is writable (alias of is_writable). **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/lchgrp.md b/docs/php/builtins/filesystem/lchgrp.md index 60157747b1..ea6dccf835 100644 --- a/docs/php/builtins/filesystem/lchgrp.md +++ b/docs/php/builtins/filesystem/lchgrp.md @@ -2,7 +2,7 @@ title: "lchgrp()" description: "Changes group ownership of a symlink." sidebar: - order: 129 + order: 133 --- ## lchgrp() @@ -19,6 +19,11 @@ Changes group ownership of a symlink. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/lchown.md b/docs/php/builtins/filesystem/lchown.md index 7e1b51edc2..6558c95ffb 100644 --- a/docs/php/builtins/filesystem/lchown.md +++ b/docs/php/builtins/filesystem/lchown.md @@ -2,7 +2,7 @@ title: "lchown()" description: "Changes user ownership of a symlink." sidebar: - order: 130 + order: 134 --- ## lchown() @@ -19,6 +19,11 @@ Changes user ownership of a symlink. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/link.md b/docs/php/builtins/filesystem/link.md index a91aa299a4..c57013fd0c 100644 --- a/docs/php/builtins/filesystem/link.md +++ b/docs/php/builtins/filesystem/link.md @@ -2,7 +2,7 @@ title: "link()" description: "Creates a hard link." sidebar: - order: 131 + order: 135 --- ## link() @@ -19,6 +19,11 @@ Creates a hard link. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/linkinfo.md b/docs/php/builtins/filesystem/linkinfo.md index dbf0e821e3..45b36f1de0 100644 --- a/docs/php/builtins/filesystem/linkinfo.md +++ b/docs/php/builtins/filesystem/linkinfo.md @@ -2,7 +2,7 @@ title: "linkinfo()" description: "Gets information about a link." sidebar: - order: 132 + order: 136 --- ## linkinfo() @@ -18,6 +18,11 @@ Gets information about a link. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/lstat.md b/docs/php/builtins/filesystem/lstat.md index 9872189521..1791c5ebed 100644 --- a/docs/php/builtins/filesystem/lstat.md +++ b/docs/php/builtins/filesystem/lstat.md @@ -2,7 +2,7 @@ title: "lstat()" description: "Gives information about a file or symbolic link." sidebar: - order: 133 + order: 137 --- ## lstat() @@ -18,6 +18,11 @@ Gives information about a file or symbolic link. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/mkdir.md b/docs/php/builtins/filesystem/mkdir.md index b10b78219f..e8a7ce95fd 100644 --- a/docs/php/builtins/filesystem/mkdir.md +++ b/docs/php/builtins/filesystem/mkdir.md @@ -2,7 +2,7 @@ title: "mkdir()" description: "Makes a directory." sidebar: - order: 134 + order: 138 --- ## mkdir() @@ -18,6 +18,11 @@ Makes a directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/pathinfo.md b/docs/php/builtins/filesystem/pathinfo.md index 6f048c5f92..8cde8f248b 100644 --- a/docs/php/builtins/filesystem/pathinfo.md +++ b/docs/php/builtins/filesystem/pathinfo.md @@ -2,7 +2,7 @@ title: "pathinfo()" description: "Returns information about a file path." sidebar: - order: 135 + order: 139 --- ## pathinfo() @@ -19,6 +19,11 @@ Returns information about a file path. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/putenv.md b/docs/php/builtins/filesystem/putenv.md index 3be34a079a..7937bc9403 100644 --- a/docs/php/builtins/filesystem/putenv.md +++ b/docs/php/builtins/filesystem/putenv.md @@ -2,7 +2,7 @@ title: "putenv()" description: "Sets an environment variable." sidebar: - order: 136 + order: 140 --- ## putenv() @@ -18,6 +18,11 @@ Sets an environment variable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/readfile.md b/docs/php/builtins/filesystem/readfile.md index 4ae8854ae2..627e657fe6 100644 --- a/docs/php/builtins/filesystem/readfile.md +++ b/docs/php/builtins/filesystem/readfile.md @@ -2,7 +2,7 @@ title: "readfile()" description: "Outputs a file." sidebar: - order: 137 + order: 141 --- ## readfile() @@ -18,6 +18,11 @@ Outputs a file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/readlink.md b/docs/php/builtins/filesystem/readlink.md index 3225ce379b..644eb5f2bb 100644 --- a/docs/php/builtins/filesystem/readlink.md +++ b/docs/php/builtins/filesystem/readlink.md @@ -2,7 +2,7 @@ title: "readlink()" description: "Returns the target of a symbolic link." sidebar: - order: 138 + order: 142 --- ## readlink() @@ -18,6 +18,11 @@ Returns the target of a symbolic link. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/realpath.md b/docs/php/builtins/filesystem/realpath.md index ad0d795625..5e32979978 100644 --- a/docs/php/builtins/filesystem/realpath.md +++ b/docs/php/builtins/filesystem/realpath.md @@ -2,7 +2,7 @@ title: "realpath()" description: "Returns canonicalized absolute pathname." sidebar: - order: 139 + order: 143 --- ## realpath() @@ -18,6 +18,11 @@ Returns canonicalized absolute pathname. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/realpath_cache_get.md b/docs/php/builtins/filesystem/realpath_cache_get.md index 0780ccfffa..bc92aa2e19 100644 --- a/docs/php/builtins/filesystem/realpath_cache_get.md +++ b/docs/php/builtins/filesystem/realpath_cache_get.md @@ -2,7 +2,7 @@ title: "realpath_cache_get()" description: "Returns realpath cache entries." sidebar: - order: 140 + order: 144 --- ## realpath_cache_get() @@ -17,6 +17,11 @@ Returns realpath cache entries. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/realpath_cache_size.md b/docs/php/builtins/filesystem/realpath_cache_size.md index 7c7b7796e6..030e9a39df 100644 --- a/docs/php/builtins/filesystem/realpath_cache_size.md +++ b/docs/php/builtins/filesystem/realpath_cache_size.md @@ -2,7 +2,7 @@ title: "realpath_cache_size()" description: "Returns the amount of memory used by the realpath cache." sidebar: - order: 141 + order: 145 --- ## realpath_cache_size() @@ -17,6 +17,11 @@ Returns the amount of memory used by the realpath cache. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/rename.md b/docs/php/builtins/filesystem/rename.md index 0075032d5d..cfbcef30ac 100644 --- a/docs/php/builtins/filesystem/rename.md +++ b/docs/php/builtins/filesystem/rename.md @@ -2,7 +2,7 @@ title: "rename()" description: "Renames a file or directory." sidebar: - order: 142 + order: 146 --- ## rename() @@ -19,6 +19,11 @@ Renames a file or directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/rmdir.md b/docs/php/builtins/filesystem/rmdir.md index ac6c8e1a93..3d86c09321 100644 --- a/docs/php/builtins/filesystem/rmdir.md +++ b/docs/php/builtins/filesystem/rmdir.md @@ -2,7 +2,7 @@ title: "rmdir()" description: "Removes a directory." sidebar: - order: 143 + order: 147 --- ## rmdir() @@ -18,6 +18,11 @@ Removes a directory. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/scandir.md b/docs/php/builtins/filesystem/scandir.md index 8540afa3a0..03edbdfd5c 100644 --- a/docs/php/builtins/filesystem/scandir.md +++ b/docs/php/builtins/filesystem/scandir.md @@ -2,7 +2,7 @@ title: "scandir()" description: "Lists files and directories inside the specified path." sidebar: - order: 144 + order: 148 --- ## scandir() @@ -18,6 +18,11 @@ Lists files and directories inside the specified path. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/stat.md b/docs/php/builtins/filesystem/stat.md index 3179e14368..39286104cc 100644 --- a/docs/php/builtins/filesystem/stat.md +++ b/docs/php/builtins/filesystem/stat.md @@ -2,7 +2,7 @@ title: "stat()" description: "Gives information about a file." sidebar: - order: 145 + order: 149 --- ## stat() @@ -18,6 +18,11 @@ Gives information about a file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/symlink.md b/docs/php/builtins/filesystem/symlink.md index 7bd1e3a55c..dfd1abdfa9 100644 --- a/docs/php/builtins/filesystem/symlink.md +++ b/docs/php/builtins/filesystem/symlink.md @@ -2,7 +2,7 @@ title: "symlink()" description: "Creates a symbolic link." sidebar: - order: 146 + order: 150 --- ## symlink() @@ -19,6 +19,11 @@ Creates a symbolic link. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/sys_get_temp_dir.md b/docs/php/builtins/filesystem/sys_get_temp_dir.md index e910e45349..fa0168fa5e 100644 --- a/docs/php/builtins/filesystem/sys_get_temp_dir.md +++ b/docs/php/builtins/filesystem/sys_get_temp_dir.md @@ -2,7 +2,7 @@ title: "sys_get_temp_dir()" description: "Returns the directory path used for temporary files." sidebar: - order: 147 + order: 151 --- ## sys_get_temp_dir() @@ -17,6 +17,11 @@ Returns the directory path used for temporary files. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/tempnam.md b/docs/php/builtins/filesystem/tempnam.md index a8ac18a230..4293223b49 100644 --- a/docs/php/builtins/filesystem/tempnam.md +++ b/docs/php/builtins/filesystem/tempnam.md @@ -2,7 +2,7 @@ title: "tempnam()" description: "Creates a file with a unique filename." sidebar: - order: 148 + order: 152 --- ## tempnam() @@ -19,6 +19,11 @@ Creates a file with a unique filename. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/tmpfile.md b/docs/php/builtins/filesystem/tmpfile.md index 9919570752..2061075d49 100644 --- a/docs/php/builtins/filesystem/tmpfile.md +++ b/docs/php/builtins/filesystem/tmpfile.md @@ -2,7 +2,7 @@ title: "tmpfile()" description: "Creates a temporary file." sidebar: - order: 149 + order: 153 --- ## tmpfile() @@ -17,6 +17,11 @@ Creates a temporary file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/touch.md b/docs/php/builtins/filesystem/touch.md index 7bd652ac00..2773a7a68c 100644 --- a/docs/php/builtins/filesystem/touch.md +++ b/docs/php/builtins/filesystem/touch.md @@ -2,7 +2,7 @@ title: "touch()" description: "Sets access and modification time of a file." sidebar: - order: 150 + order: 154 --- ## touch() @@ -20,6 +20,11 @@ Sets access and modification time of a file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/umask.md b/docs/php/builtins/filesystem/umask.md index c11ca2d250..f6249bd537 100644 --- a/docs/php/builtins/filesystem/umask.md +++ b/docs/php/builtins/filesystem/umask.md @@ -2,7 +2,7 @@ title: "umask()" description: "Changes the current umask." sidebar: - order: 151 + order: 155 --- ## umask() @@ -18,6 +18,11 @@ Changes the current umask. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/filesystem/unlink.md b/docs/php/builtins/filesystem/unlink.md index 1910b9daa3..6b825ae4b9 100644 --- a/docs/php/builtins/filesystem/unlink.md +++ b/docs/php/builtins/filesystem/unlink.md @@ -2,7 +2,7 @@ title: "unlink()" description: "Deletes a file." sidebar: - order: 152 + order: 156 --- ## unlink() @@ -18,6 +18,11 @@ Deletes a file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io.md b/docs/php/builtins/io.md index 135caf50f7..94afe286a0 100644 --- a/docs/php/builtins/io.md +++ b/docs/php/builtins/io.md @@ -7,82 +7,82 @@ sidebar: ## IO builtins -| Function | Signature | Returns | -|---|---|---| -| [`closedir()`](./io/closedir.md) | `(resource $dir_handle): void` | `void` | -| [`fclose()`](./io/fclose.md) | `(resource $stream): bool` | `bool` | -| [`fdatasync()`](./io/fdatasync.md) | `(resource $stream): bool` | `bool` | -| [`feof()`](./io/feof.md) | `(resource $stream): bool` | `bool` | -| [`fflush()`](./io/fflush.md) | `(resource $stream): bool` | `bool` | -| [`fgetc()`](./io/fgetc.md) | `(resource $stream): mixed` | `mixed` | -| [`fgetcsv()`](./io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | -| [`fgets()`](./io/fgets.md) | `(resource $stream): mixed` | `mixed` | -| [`file()`](./io/file.md) | `(string $filename): array` | `array` | -| [`file_get_contents()`](./io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | -| [`file_put_contents()`](./io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | -| [`flock()`](./io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | -| [`fopen()`](./io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | -| [`fpassthru()`](./io/fpassthru.md) | `(resource $stream): int` | `int` | -| [`fprintf()`](./io/fprintf.md) | `(resource $stream, string $format, ...$values): int` | `int` | -| [`fputcsv()`](./io/fputcsv.md) | `(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int` | `int` | -| [`fread()`](./io/fread.md) | `(resource $stream, int $length): string` | `string` | -| [`fscanf()`](./io/fscanf.md) | `(resource $stream, string $format, ...$vars): array` | `array` | -| [`fseek()`](./io/fseek.md) | `(resource $stream, int $offset, int $whence = 0): int` | `int` | -| [`fstat()`](./io/fstat.md) | `(resource $stream): mixed` | `mixed` | -| [`fsync()`](./io/fsync.md) | `(resource $stream): bool` | `bool` | -| [`ftell()`](./io/ftell.md) | `(resource $stream): int` | `int` | -| [`ftruncate()`](./io/ftruncate.md) | `(resource $stream, int $size): bool` | `bool` | -| [`fwrite()`](./io/fwrite.md) | `(resource $stream, string $data): int` | `int` | -| [`gethostbyaddr()`](./io/gethostbyaddr.md) | `(string $ip): mixed` | `mixed` | -| [`gethostbyname()`](./io/gethostbyname.md) | `(string $hostname): string` | `string` | -| [`gethostname()`](./io/gethostname.md) | `(): string` | `string` | -| [`getprotobyname()`](./io/getprotobyname.md) | `(string $protocol): mixed` | `mixed` | -| [`getprotobynumber()`](./io/getprotobynumber.md) | `(int $protocol): mixed` | `mixed` | -| [`getservbyname()`](./io/getservbyname.md) | `(string $service, string $protocol): mixed` | `mixed` | -| [`getservbyport()`](./io/getservbyport.md) | `(int $port, string $protocol): mixed` | `mixed` | -| [`hash_file()`](./io/hash_file.md) | `(string $algo, string $filename, bool $binary = false): mixed` | `mixed` | -| [`opendir()`](./io/opendir.md) | `(string $directory): mixed` | `mixed` | -| [`readdir()`](./io/readdir.md) | `(resource $dir_handle): mixed` | `mixed` | -| [`rewind()`](./io/rewind.md) | `(resource $stream): bool` | `bool` | -| [`rewinddir()`](./io/rewinddir.md) | `(resource $dir_handle): void` | `void` | -| [`stream_bucket_make_writeable()`](./io/stream_bucket_make_writeable.md) | `(mixed $brigade): mixed` | `mixed` | -| [`stream_bucket_new()`](./io/stream_bucket_new.md) | `(resource $stream, string $buffer): mixed` | `mixed` | -| [`stream_context_create()`](./io/stream_context_create.md) | `(array $options = null, array $params = null): mixed` | `mixed` | -| [`stream_context_get_default()`](./io/stream_context_get_default.md) | `(array $options = null): mixed` | `mixed` | -| [`stream_context_get_options()`](./io/stream_context_get_options.md) | `(resource $context): array` | `array` | -| [`stream_context_get_params()`](./io/stream_context_get_params.md) | `(resource $context): array` | `array` | -| [`stream_context_set_default()`](./io/stream_context_set_default.md) | `(array $options): mixed` | `mixed` | -| [`stream_context_set_option()`](./io/stream_context_set_option.md) | `(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool` | `bool` | -| [`stream_context_set_params()`](./io/stream_context_set_params.md) | `(resource $context, array $params): bool` | `bool` | -| [`stream_copy_to_stream()`](./io/stream_copy_to_stream.md) | `(resource $from, resource $to, int $length = null, int $offset = -1): mixed` | `mixed` | -| [`stream_filter_register()`](./io/stream_filter_register.md) | `(string $filter_name, string $class): bool` | `bool` | -| [`stream_filter_remove()`](./io/stream_filter_remove.md) | `(resource $stream_filter): bool` | `bool` | -| [`stream_get_contents()`](./io/stream_get_contents.md) | `(resource $stream, int $length = null, int $offset = -1): mixed` | `mixed` | -| [`stream_get_filters()`](./io/stream_get_filters.md) | `(): array` | `array` | -| [`stream_get_line()`](./io/stream_get_line.md) | `(resource $stream, int $length, string $ending = ''): string` | `string` | -| [`stream_get_meta_data()`](./io/stream_get_meta_data.md) | `(resource $stream): array` | `array` | -| [`stream_get_transports()`](./io/stream_get_transports.md) | `(): array` | `array` | -| [`stream_get_wrappers()`](./io/stream_get_wrappers.md) | `(): array` | `array` | -| [`stream_is_local()`](./io/stream_is_local.md) | `(resource $stream): bool` | `bool` | -| [`stream_isatty()`](./io/stream_isatty.md) | `(resource $stream): bool` | `bool` | -| [`stream_resolve_include_path()`](./io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | -| [`stream_select()`](./io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | -| [`stream_set_blocking()`](./io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | -| [`stream_set_chunk_size()`](./io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_set_read_buffer()`](./io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_set_timeout()`](./io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | -| [`stream_set_write_buffer()`](./io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | -| [`stream_socket_accept()`](./io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | -| [`stream_socket_client()`](./io/stream_socket_client.md) | `(string $address): mixed` | `mixed` | -| [`stream_socket_enable_crypto()`](./io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | `bool` | -| [`stream_socket_get_name()`](./io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | -| [`stream_socket_pair()`](./io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | -| [`stream_socket_recvfrom()`](./io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | -| [`stream_socket_sendto()`](./io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags = 0, string $address = ''): mixed` | `mixed` | -| [`stream_socket_server()`](./io/stream_socket_server.md) | `(string $address): mixed` | `mixed` | -| [`stream_socket_shutdown()`](./io/stream_socket_shutdown.md) | `(resource $stream, int $mode): bool` | `bool` | -| [`stream_supports_lock()`](./io/stream_supports_lock.md) | `(resource $stream): bool` | `bool` | -| [`stream_wrapper_register()`](./io/stream_wrapper_register.md) | `(string $protocol, string $class, int $flags = 0): bool` | `bool` | -| [`stream_wrapper_restore()`](./io/stream_wrapper_restore.md) | `(string $protocol): bool` | `bool` | -| [`stream_wrapper_unregister()`](./io/stream_wrapper_unregister.md) | `(string $protocol): bool` | `bool` | -| [`vfprintf()`](./io/vfprintf.md) | `(resource $stream, string $format, array $values): int` | `int` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`closedir()`](./io/closedir.md) | `(resource $dir_handle): void` | `void` | ✓ | ✓ | +| [`fclose()`](./io/fclose.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fdatasync()`](./io/fdatasync.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`feof()`](./io/feof.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fflush()`](./io/fflush.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`fgetc()`](./io/fgetc.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`fgetcsv()`](./io/fgetcsv.md) | `(resource $stream, int $length = null, string $separator = ','): array` | `array` | ✓ | ✓ | +| [`fgets()`](./io/fgets.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`file()`](./io/file.md) | `(string $filename): array` | `array` | ✓ | ✓ | +| [`file_get_contents()`](./io/file_get_contents.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`file_put_contents()`](./io/file_put_contents.md) | `(string $filename, string $data): int` | `int` | ✓ | ✓ | +| [`flock()`](./io/flock.md) | `(resource $stream, int $operation, bool $would_block = null): bool` | `bool` | ✓ | ✓ | +| [`fopen()`](./io/fopen.md) | `(string $filename, string $mode, bool $use_include_path = false, mixed $context = null): mixed` | `mixed` | ✓ | ✓ | +| [`fpassthru()`](./io/fpassthru.md) | `(resource $stream): int` | `int` | ✓ | ✓ | +| [`fprintf()`](./io/fprintf.md) | `(resource $stream, string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`fputcsv()`](./io/fputcsv.md) | `(resource $stream, array $fields, string $separator = ',', string $enclosure = '"'): int` | `int` | ✓ | ✓ | +| [`fread()`](./io/fread.md) | `(resource $stream, int $length): string` | `string` | ✓ | ✓ | +| [`fscanf()`](./io/fscanf.md) | `(resource $stream, string $format, ...$vars): array` | `array` | ✓ | ✓ | +| [`fseek()`](./io/fseek.md) | `(resource $stream, int $offset, int $whence = 0): int` | `int` | ✓ | ✓ | +| [`fstat()`](./io/fstat.md) | `(resource $stream): mixed` | `mixed` | ✓ | ✓ | +| [`fsync()`](./io/fsync.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`ftell()`](./io/ftell.md) | `(resource $stream): int` | `int` | ✓ | ✓ | +| [`ftruncate()`](./io/ftruncate.md) | `(resource $stream, int $size): bool` | `bool` | ✓ | ✓ | +| [`fwrite()`](./io/fwrite.md) | `(resource $stream, string $data): int` | `int` | ✓ | ✓ | +| [`gethostbyaddr()`](./io/gethostbyaddr.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`gethostbyname()`](./io/gethostbyname.md) | `(string $hostname): string` | `string` | ✓ | ✓ | +| [`gethostname()`](./io/gethostname.md) | `(): string` | `string` | ✓ | ✓ | +| [`getprotobyname()`](./io/getprotobyname.md) | `(string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getprotobynumber()`](./io/getprotobynumber.md) | `(int $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getservbyname()`](./io/getservbyname.md) | `(string $service, string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`getservbyport()`](./io/getservbyport.md) | `(int $port, string $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`hash_file()`](./io/hash_file.md) | `(string $algo, string $filename, bool $binary = false): mixed` | `mixed` | ✓ | ✓ | +| [`opendir()`](./io/opendir.md) | `(string $directory): mixed` | `mixed` | ✓ | ✓ | +| [`readdir()`](./io/readdir.md) | `(resource $dir_handle): mixed` | `mixed` | ✓ | ✓ | +| [`rewind()`](./io/rewind.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`rewinddir()`](./io/rewinddir.md) | `(resource $dir_handle): void` | `void` | ✓ | ✓ | +| [`stream_bucket_make_writeable()`](./io/stream_bucket_make_writeable.md) | `(mixed $brigade): mixed` | `mixed` | ✓ | ✓ | +| [`stream_bucket_new()`](./io/stream_bucket_new.md) | `(resource $stream, string $buffer): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_create()`](./io/stream_context_create.md) | `(array $options = null, array $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_get_default()`](./io/stream_context_get_default.md) | `(array $options = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_get_options()`](./io/stream_context_get_options.md) | `(resource $context): array` | `array` | ✓ | ✓ | +| [`stream_context_get_params()`](./io/stream_context_get_params.md) | `(resource $context): array` | `array` | ✓ | ✓ | +| [`stream_context_set_default()`](./io/stream_context_set_default.md) | `(array $options): mixed` | `mixed` | ✓ | ✓ | +| [`stream_context_set_option()`](./io/stream_context_set_option.md) | `(resource $context, string $wrapper_or_options, string $option_name = null, mixed $value = null): bool` | `bool` | ✓ | ✓ | +| [`stream_context_set_params()`](./io/stream_context_set_params.md) | `(resource $context, array $params): bool` | `bool` | ✓ | ✓ | +| [`stream_copy_to_stream()`](./io/stream_copy_to_stream.md) | `(resource $from, resource $to, int $length = null, int $offset = -1): mixed` | `mixed` | ✓ | ✓ | +| [`stream_filter_register()`](./io/stream_filter_register.md) | `(string $filter_name, string $class): bool` | `bool` | ✓ | ✓ | +| [`stream_filter_remove()`](./io/stream_filter_remove.md) | `(resource $stream_filter): bool` | `bool` | ✓ | ✓ | +| [`stream_get_contents()`](./io/stream_get_contents.md) | `(resource $stream, int $length = null, int $offset = -1): mixed` | `mixed` | ✓ | ✓ | +| [`stream_get_filters()`](./io/stream_get_filters.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_get_line()`](./io/stream_get_line.md) | `(resource $stream, int $length, string $ending = ''): string` | `string` | ✓ | ✓ | +| [`stream_get_meta_data()`](./io/stream_get_meta_data.md) | `(resource $stream): array` | `array` | ✓ | ✓ | +| [`stream_get_transports()`](./io/stream_get_transports.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_get_wrappers()`](./io/stream_get_wrappers.md) | `(): array` | `array` | ✓ | ✓ | +| [`stream_is_local()`](./io/stream_is_local.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_isatty()`](./io/stream_isatty.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_resolve_include_path()`](./io/stream_resolve_include_path.md) | `(string $filename): mixed` | `mixed` | ✓ | ✓ | +| [`stream_select()`](./io/stream_select.md) | `(array $read, array $write, array $except, int $seconds, int $microseconds = 0): int` | `int` | ✓ | ✓ | +| [`stream_set_blocking()`](./io/stream_set_blocking.md) | `(resource $stream, bool $enable): bool` | `bool` | ✓ | ✓ | +| [`stream_set_chunk_size()`](./io/stream_set_chunk_size.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_set_read_buffer()`](./io/stream_set_read_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_set_timeout()`](./io/stream_set_timeout.md) | `(resource $stream, int $seconds, int $microseconds = 0): bool` | `bool` | ✓ | ✓ | +| [`stream_set_write_buffer()`](./io/stream_set_write_buffer.md) | `(resource $stream, int $size): int` | `int` | ✓ | ✓ | +| [`stream_socket_accept()`](./io/stream_socket_accept.md) | `(resource $socket, float $timeout = null, string $peer_name = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_client()`](./io/stream_socket_client.md) | `(string $address): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_enable_crypto()`](./io/stream_socket_enable_crypto.md) | `(resource $stream, bool $enable, int $crypto_method = null, resource $session_stream = null): bool` | `bool` | ✓ | ✓ | +| [`stream_socket_get_name()`](./io/stream_socket_get_name.md) | `(resource $socket, bool $remote): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_pair()`](./io/stream_socket_pair.md) | `(int $domain, int $type, int $protocol): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_recvfrom()`](./io/stream_socket_recvfrom.md) | `(resource $socket, int $length, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_sendto()`](./io/stream_socket_sendto.md) | `(resource $socket, string $data, int $flags = 0, string $address = ''): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_server()`](./io/stream_socket_server.md) | `(string $address): mixed` | `mixed` | ✓ | ✓ | +| [`stream_socket_shutdown()`](./io/stream_socket_shutdown.md) | `(resource $stream, int $mode): bool` | `bool` | ✓ | ✓ | +| [`stream_supports_lock()`](./io/stream_supports_lock.md) | `(resource $stream): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_register()`](./io/stream_wrapper_register.md) | `(string $protocol, string $class, int $flags = 0): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_restore()`](./io/stream_wrapper_restore.md) | `(string $protocol): bool` | `bool` | ✓ | ✓ | +| [`stream_wrapper_unregister()`](./io/stream_wrapper_unregister.md) | `(string $protocol): bool` | `bool` | ✓ | ✓ | +| [`vfprintf()`](./io/vfprintf.md) | `(resource $stream, string $format, array $values): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/io/closedir.md b/docs/php/builtins/io/closedir.md index d2f0c88f2a..01d1651e8e 100644 --- a/docs/php/builtins/io/closedir.md +++ b/docs/php/builtins/io/closedir.md @@ -2,7 +2,7 @@ title: "closedir()" description: "Closes directory handle." sidebar: - order: 153 + order: 157 --- ## closedir() @@ -18,6 +18,11 @@ Closes directory handle. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fclose.md b/docs/php/builtins/io/fclose.md index d9c8c44990..ad568cb4b5 100644 --- a/docs/php/builtins/io/fclose.md +++ b/docs/php/builtins/io/fclose.md @@ -2,7 +2,7 @@ title: "fclose()" description: "Closes an open file pointer." sidebar: - order: 154 + order: 158 --- ## fclose() @@ -18,6 +18,11 @@ Closes an open file pointer. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fdatasync.md b/docs/php/builtins/io/fdatasync.md index 26b0b5562e..6f98421731 100644 --- a/docs/php/builtins/io/fdatasync.md +++ b/docs/php/builtins/io/fdatasync.md @@ -2,7 +2,7 @@ title: "fdatasync()" description: "Synchronizes data (but not meta-data) to file." sidebar: - order: 155 + order: 159 --- ## fdatasync() @@ -18,6 +18,11 @@ Synchronizes data (but not meta-data) to file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/feof.md b/docs/php/builtins/io/feof.md index fcb60ca74e..890d32aab2 100644 --- a/docs/php/builtins/io/feof.md +++ b/docs/php/builtins/io/feof.md @@ -2,7 +2,7 @@ title: "feof()" description: "Tests for end-of-file on a file pointer." sidebar: - order: 156 + order: 160 --- ## feof() @@ -18,6 +18,11 @@ Tests for end-of-file on a file pointer. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fflush.md b/docs/php/builtins/io/fflush.md index ba052577bc..f86c775ae8 100644 --- a/docs/php/builtins/io/fflush.md +++ b/docs/php/builtins/io/fflush.md @@ -2,7 +2,7 @@ title: "fflush()" description: "Flushes the output to a file." sidebar: - order: 157 + order: 161 --- ## fflush() @@ -18,6 +18,11 @@ Flushes the output to a file. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fgetc.md b/docs/php/builtins/io/fgetc.md index f69c10d508..29f28a270d 100644 --- a/docs/php/builtins/io/fgetc.md +++ b/docs/php/builtins/io/fgetc.md @@ -2,7 +2,7 @@ title: "fgetc()" description: "Gets a character from the given file pointer." sidebar: - order: 158 + order: 162 --- ## fgetc() @@ -18,6 +18,11 @@ Gets a character from the given file pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fgetcsv.md b/docs/php/builtins/io/fgetcsv.md index 4baf3d3a40..cf29ce57e9 100644 --- a/docs/php/builtins/io/fgetcsv.md +++ b/docs/php/builtins/io/fgetcsv.md @@ -2,7 +2,7 @@ title: "fgetcsv()" description: "Gets line from file pointer and parse for CSV fields." sidebar: - order: 159 + order: 163 --- ## fgetcsv() @@ -20,6 +20,11 @@ Gets line from file pointer and parse for CSV fields. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fgets.md b/docs/php/builtins/io/fgets.md index 2150096f36..2d5d14744c 100644 --- a/docs/php/builtins/io/fgets.md +++ b/docs/php/builtins/io/fgets.md @@ -2,7 +2,7 @@ title: "fgets()" description: "Gets line from file pointer." sidebar: - order: 160 + order: 164 --- ## fgets() @@ -18,6 +18,11 @@ Gets line from file pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/file.md b/docs/php/builtins/io/file.md index b6176accd9..b4882e0b63 100644 --- a/docs/php/builtins/io/file.md +++ b/docs/php/builtins/io/file.md @@ -2,7 +2,7 @@ title: "file()" description: "Reads an entire file into an array." sidebar: - order: 161 + order: 165 --- ## file() @@ -18,6 +18,11 @@ Reads an entire file into an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/file_get_contents.md b/docs/php/builtins/io/file_get_contents.md index e8d8a6fc69..9a74b62e5b 100644 --- a/docs/php/builtins/io/file_get_contents.md +++ b/docs/php/builtins/io/file_get_contents.md @@ -2,7 +2,7 @@ title: "file_get_contents()" description: "Reads an entire file into a string." sidebar: - order: 162 + order: 166 --- ## file_get_contents() @@ -18,6 +18,11 @@ Reads an entire file into a string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/file_put_contents.md b/docs/php/builtins/io/file_put_contents.md index 0cebb841a2..9d7d8eef53 100644 --- a/docs/php/builtins/io/file_put_contents.md +++ b/docs/php/builtins/io/file_put_contents.md @@ -2,7 +2,7 @@ title: "file_put_contents()" description: "Writes data to a file." sidebar: - order: 163 + order: 167 --- ## file_put_contents() @@ -19,6 +19,11 @@ Writes data to a file. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/flock.md b/docs/php/builtins/io/flock.md index efd46e23e8..661c300ab4 100644 --- a/docs/php/builtins/io/flock.md +++ b/docs/php/builtins/io/flock.md @@ -2,7 +2,7 @@ title: "flock()" description: "Portable advisory file locking." sidebar: - order: 164 + order: 168 --- ## flock() @@ -20,6 +20,11 @@ Portable advisory file locking. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fopen.md b/docs/php/builtins/io/fopen.md index 706973d49d..1ec50f76b0 100644 --- a/docs/php/builtins/io/fopen.md +++ b/docs/php/builtins/io/fopen.md @@ -2,7 +2,7 @@ title: "fopen()" description: "Opens file or URL." sidebar: - order: 165 + order: 169 --- ## fopen() @@ -21,6 +21,11 @@ Opens file or URL. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fpassthru.md b/docs/php/builtins/io/fpassthru.md index 676aa59290..7f32dbfde4 100644 --- a/docs/php/builtins/io/fpassthru.md +++ b/docs/php/builtins/io/fpassthru.md @@ -2,7 +2,7 @@ title: "fpassthru()" description: "Output all remaining data on a file pointer." sidebar: - order: 166 + order: 170 --- ## fpassthru() @@ -18,6 +18,11 @@ Output all remaining data on a file pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fprintf.md b/docs/php/builtins/io/fprintf.md index f9ceafb81e..c365eb2c72 100644 --- a/docs/php/builtins/io/fprintf.md +++ b/docs/php/builtins/io/fprintf.md @@ -2,7 +2,7 @@ title: "fprintf()" description: "Write a formatted string to a stream." sidebar: - order: 167 + order: 171 --- ## fprintf() @@ -20,6 +20,11 @@ Write a formatted string to a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fputcsv.md b/docs/php/builtins/io/fputcsv.md index 3f004533f4..49f251076c 100644 --- a/docs/php/builtins/io/fputcsv.md +++ b/docs/php/builtins/io/fputcsv.md @@ -2,7 +2,7 @@ title: "fputcsv()" description: "Format line as CSV and write to file pointer." sidebar: - order: 168 + order: 172 --- ## fputcsv() @@ -21,6 +21,11 @@ Format line as CSV and write to file pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fread.md b/docs/php/builtins/io/fread.md index 17dbb2afc9..ac66807844 100644 --- a/docs/php/builtins/io/fread.md +++ b/docs/php/builtins/io/fread.md @@ -2,7 +2,7 @@ title: "fread()" description: "Binary-safe file read." sidebar: - order: 169 + order: 173 --- ## fread() @@ -19,6 +19,11 @@ Binary-safe file read. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fscanf.md b/docs/php/builtins/io/fscanf.md index 67d2cd9551..45ebe07e1d 100644 --- a/docs/php/builtins/io/fscanf.md +++ b/docs/php/builtins/io/fscanf.md @@ -2,7 +2,7 @@ title: "fscanf()" description: "Parses input from a file according to a format." sidebar: - order: 170 + order: 174 --- ## fscanf() @@ -20,6 +20,11 @@ Parses input from a file according to a format. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fseek.md b/docs/php/builtins/io/fseek.md index 66fea389be..8e7732bdc6 100644 --- a/docs/php/builtins/io/fseek.md +++ b/docs/php/builtins/io/fseek.md @@ -2,7 +2,7 @@ title: "fseek()" description: "Seeks on a file pointer." sidebar: - order: 171 + order: 175 --- ## fseek() @@ -20,6 +20,11 @@ Seeks on a file pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fstat.md b/docs/php/builtins/io/fstat.md index 673d0c0302..e2b1d2c1f9 100644 --- a/docs/php/builtins/io/fstat.md +++ b/docs/php/builtins/io/fstat.md @@ -2,7 +2,7 @@ title: "fstat()" description: "Gets information about a file using an open file pointer." sidebar: - order: 172 + order: 176 --- ## fstat() @@ -18,6 +18,11 @@ Gets information about a file using an open file pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fsync.md b/docs/php/builtins/io/fsync.md index 58a001a4b6..5e7a7cbfe0 100644 --- a/docs/php/builtins/io/fsync.md +++ b/docs/php/builtins/io/fsync.md @@ -2,7 +2,7 @@ title: "fsync()" description: "Synchronizes changes to the file (including meta-data)." sidebar: - order: 173 + order: 177 --- ## fsync() @@ -18,6 +18,11 @@ Synchronizes changes to the file (including meta-data). **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/ftell.md b/docs/php/builtins/io/ftell.md index 5fc9fee511..0b9aa3c22c 100644 --- a/docs/php/builtins/io/ftell.md +++ b/docs/php/builtins/io/ftell.md @@ -2,7 +2,7 @@ title: "ftell()" description: "Returns the current position of the file read/write pointer." sidebar: - order: 174 + order: 178 --- ## ftell() @@ -18,6 +18,11 @@ Returns the current position of the file read/write pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/ftruncate.md b/docs/php/builtins/io/ftruncate.md index cfa0d10725..8b4c53ba33 100644 --- a/docs/php/builtins/io/ftruncate.md +++ b/docs/php/builtins/io/ftruncate.md @@ -2,7 +2,7 @@ title: "ftruncate()" description: "Truncates a file to a given length." sidebar: - order: 175 + order: 179 --- ## ftruncate() @@ -19,6 +19,11 @@ Truncates a file to a given length. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/fwrite.md b/docs/php/builtins/io/fwrite.md index 3edc50c485..aa8f9ddb21 100644 --- a/docs/php/builtins/io/fwrite.md +++ b/docs/php/builtins/io/fwrite.md @@ -2,7 +2,7 @@ title: "fwrite()" description: "Binary-safe file write." sidebar: - order: 176 + order: 180 --- ## fwrite() @@ -19,6 +19,11 @@ Binary-safe file write. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/gethostbyaddr.md b/docs/php/builtins/io/gethostbyaddr.md index 740bd536b8..653683960d 100644 --- a/docs/php/builtins/io/gethostbyaddr.md +++ b/docs/php/builtins/io/gethostbyaddr.md @@ -2,7 +2,7 @@ title: "gethostbyaddr()" description: "Gets the Internet host name corresponding to a given IP address." sidebar: - order: 177 + order: 181 --- ## gethostbyaddr() @@ -18,6 +18,11 @@ Gets the Internet host name corresponding to a given IP address. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/gethostbyname.md b/docs/php/builtins/io/gethostbyname.md index 669006764a..a9af4c3852 100644 --- a/docs/php/builtins/io/gethostbyname.md +++ b/docs/php/builtins/io/gethostbyname.md @@ -2,7 +2,7 @@ title: "gethostbyname()" description: "Gets the IPv4 address corresponding to the given Internet host name." sidebar: - order: 178 + order: 182 --- ## gethostbyname() @@ -18,6 +18,11 @@ Gets the IPv4 address corresponding to the given Internet host name. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/gethostname.md b/docs/php/builtins/io/gethostname.md index 102c48204e..9f8e85f1bf 100644 --- a/docs/php/builtins/io/gethostname.md +++ b/docs/php/builtins/io/gethostname.md @@ -2,7 +2,7 @@ title: "gethostname()" description: "Gets the standard host name for the local machine." sidebar: - order: 179 + order: 183 --- ## gethostname() @@ -17,6 +17,11 @@ Gets the standard host name for the local machine. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/getprotobyname.md b/docs/php/builtins/io/getprotobyname.md index bddcbae93a..5c1461d66a 100644 --- a/docs/php/builtins/io/getprotobyname.md +++ b/docs/php/builtins/io/getprotobyname.md @@ -2,7 +2,7 @@ title: "getprotobyname()" description: "Gets the protocol number associated with the given protocol name." sidebar: - order: 180 + order: 184 --- ## getprotobyname() @@ -18,6 +18,11 @@ Gets the protocol number associated with the given protocol name. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/getprotobynumber.md b/docs/php/builtins/io/getprotobynumber.md index 14de680e4a..3cc918aaf6 100644 --- a/docs/php/builtins/io/getprotobynumber.md +++ b/docs/php/builtins/io/getprotobynumber.md @@ -2,7 +2,7 @@ title: "getprotobynumber()" description: "Gets the protocol name associated with the given protocol number." sidebar: - order: 181 + order: 185 --- ## getprotobynumber() @@ -18,6 +18,11 @@ Gets the protocol name associated with the given protocol number. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/getservbyname.md b/docs/php/builtins/io/getservbyname.md index 92114158d2..4eb2bce1ed 100644 --- a/docs/php/builtins/io/getservbyname.md +++ b/docs/php/builtins/io/getservbyname.md @@ -2,7 +2,7 @@ title: "getservbyname()" description: "Gets port number associated with an Internet service and protocol." sidebar: - order: 182 + order: 186 --- ## getservbyname() @@ -19,6 +19,11 @@ Gets port number associated with an Internet service and protocol. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/getservbyport.md b/docs/php/builtins/io/getservbyport.md index ac10941245..9974f92837 100644 --- a/docs/php/builtins/io/getservbyport.md +++ b/docs/php/builtins/io/getservbyport.md @@ -2,7 +2,7 @@ title: "getservbyport()" description: "Gets the Internet service that corresponds to a port and protocol." sidebar: - order: 183 + order: 187 --- ## getservbyport() @@ -19,6 +19,11 @@ Gets the Internet service that corresponds to a port and protocol. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/hash_file.md b/docs/php/builtins/io/hash_file.md index 7307465076..a0825f6f0c 100644 --- a/docs/php/builtins/io/hash_file.md +++ b/docs/php/builtins/io/hash_file.md @@ -2,7 +2,7 @@ title: "hash_file()" description: "Generates a hash value using the contents of a given file." sidebar: - order: 184 + order: 188 --- ## hash_file() @@ -20,6 +20,11 @@ Generates a hash value using the contents of a given file. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/opendir.md b/docs/php/builtins/io/opendir.md index 8666aaa8ec..b3b1b7a49e 100644 --- a/docs/php/builtins/io/opendir.md +++ b/docs/php/builtins/io/opendir.md @@ -2,7 +2,7 @@ title: "opendir()" description: "Open directory handle." sidebar: - order: 185 + order: 189 --- ## opendir() @@ -18,6 +18,11 @@ Open directory handle. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/readdir.md b/docs/php/builtins/io/readdir.md index 2626d69f9e..cbb08f1bd0 100644 --- a/docs/php/builtins/io/readdir.md +++ b/docs/php/builtins/io/readdir.md @@ -2,7 +2,7 @@ title: "readdir()" description: "Read entry from directory handle." sidebar: - order: 186 + order: 190 --- ## readdir() @@ -18,6 +18,11 @@ Read entry from directory handle. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/rewind.md b/docs/php/builtins/io/rewind.md index af56e86a73..3296ae8cba 100644 --- a/docs/php/builtins/io/rewind.md +++ b/docs/php/builtins/io/rewind.md @@ -2,7 +2,7 @@ title: "rewind()" description: "Rewind the position of a file pointer." sidebar: - order: 187 + order: 191 --- ## rewind() @@ -18,6 +18,11 @@ Rewind the position of a file pointer. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/rewinddir.md b/docs/php/builtins/io/rewinddir.md index 379f10be46..2221a233e8 100644 --- a/docs/php/builtins/io/rewinddir.md +++ b/docs/php/builtins/io/rewinddir.md @@ -2,7 +2,7 @@ title: "rewinddir()" description: "Rewind directory handle." sidebar: - order: 188 + order: 192 --- ## rewinddir() @@ -18,6 +18,11 @@ Rewind directory handle. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_bucket_make_writeable.md b/docs/php/builtins/io/stream_bucket_make_writeable.md index 9fd73afea8..7859d968c2 100644 --- a/docs/php/builtins/io/stream_bucket_make_writeable.md +++ b/docs/php/builtins/io/stream_bucket_make_writeable.md @@ -2,7 +2,7 @@ title: "stream_bucket_make_writeable()" description: "Returns a bucket object from the brigade for use in a stream filter." sidebar: - order: 189 + order: 193 --- ## stream_bucket_make_writeable() @@ -18,6 +18,11 @@ Returns a bucket object from the brigade for use in a stream filter. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_bucket_new.md b/docs/php/builtins/io/stream_bucket_new.md index ebea411b29..495565d932 100644 --- a/docs/php/builtins/io/stream_bucket_new.md +++ b/docs/php/builtins/io/stream_bucket_new.md @@ -2,7 +2,7 @@ title: "stream_bucket_new()" description: "Creates a new bucket for use in a stream filter." sidebar: - order: 190 + order: 194 --- ## stream_bucket_new() @@ -19,6 +19,11 @@ Creates a new bucket for use in a stream filter. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_create.md b/docs/php/builtins/io/stream_context_create.md index 7e556f6436..c9fab9f744 100644 --- a/docs/php/builtins/io/stream_context_create.md +++ b/docs/php/builtins/io/stream_context_create.md @@ -2,7 +2,7 @@ title: "stream_context_create()" description: "Creates a stream context." sidebar: - order: 191 + order: 195 --- ## stream_context_create() @@ -19,6 +19,11 @@ Creates a stream context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_get_default.md b/docs/php/builtins/io/stream_context_get_default.md index ad032c2764..dbb8115b22 100644 --- a/docs/php/builtins/io/stream_context_get_default.md +++ b/docs/php/builtins/io/stream_context_get_default.md @@ -2,7 +2,7 @@ title: "stream_context_get_default()" description: "Retrieves the default stream context." sidebar: - order: 192 + order: 196 --- ## stream_context_get_default() @@ -18,6 +18,11 @@ Retrieves the default stream context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_get_options.md b/docs/php/builtins/io/stream_context_get_options.md index ffca97723d..fa6f5edd97 100644 --- a/docs/php/builtins/io/stream_context_get_options.md +++ b/docs/php/builtins/io/stream_context_get_options.md @@ -2,7 +2,7 @@ title: "stream_context_get_options()" description: "Retrieves options for the specified stream context." sidebar: - order: 193 + order: 197 --- ## stream_context_get_options() @@ -18,6 +18,11 @@ Retrieves options for the specified stream context. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_get_params.md b/docs/php/builtins/io/stream_context_get_params.md index 836973a127..9aae40c46c 100644 --- a/docs/php/builtins/io/stream_context_get_params.md +++ b/docs/php/builtins/io/stream_context_get_params.md @@ -2,7 +2,7 @@ title: "stream_context_get_params()" description: "Retrieves parameters from the specified stream context." sidebar: - order: 194 + order: 198 --- ## stream_context_get_params() @@ -18,6 +18,11 @@ Retrieves parameters from the specified stream context. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_set_default.md b/docs/php/builtins/io/stream_context_set_default.md index afab538c14..d0bb794088 100644 --- a/docs/php/builtins/io/stream_context_set_default.md +++ b/docs/php/builtins/io/stream_context_set_default.md @@ -2,7 +2,7 @@ title: "stream_context_set_default()" description: "Sets the default stream context." sidebar: - order: 195 + order: 199 --- ## stream_context_set_default() @@ -18,6 +18,11 @@ Sets the default stream context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_set_option.md b/docs/php/builtins/io/stream_context_set_option.md index 2178ce7484..fb3f50d3e2 100644 --- a/docs/php/builtins/io/stream_context_set_option.md +++ b/docs/php/builtins/io/stream_context_set_option.md @@ -2,7 +2,7 @@ title: "stream_context_set_option()" description: "Sets an option on the specified context." sidebar: - order: 196 + order: 200 --- ## stream_context_set_option() @@ -21,6 +21,11 @@ Sets an option on the specified context. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_context_set_params.md b/docs/php/builtins/io/stream_context_set_params.md index da7dcc12fa..eaf336381f 100644 --- a/docs/php/builtins/io/stream_context_set_params.md +++ b/docs/php/builtins/io/stream_context_set_params.md @@ -2,7 +2,7 @@ title: "stream_context_set_params()" description: "Sets parameters on the specified context." sidebar: - order: 197 + order: 201 --- ## stream_context_set_params() @@ -19,6 +19,11 @@ Sets parameters on the specified context. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_copy_to_stream.md b/docs/php/builtins/io/stream_copy_to_stream.md index 1083006e1d..6a9294b801 100644 --- a/docs/php/builtins/io/stream_copy_to_stream.md +++ b/docs/php/builtins/io/stream_copy_to_stream.md @@ -2,7 +2,7 @@ title: "stream_copy_to_stream()" description: "Copies data from one stream to another." sidebar: - order: 198 + order: 202 --- ## stream_copy_to_stream() @@ -21,6 +21,11 @@ Copies data from one stream to another. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_filter_register.md b/docs/php/builtins/io/stream_filter_register.md index 055527fdcc..3f60b09a8d 100644 --- a/docs/php/builtins/io/stream_filter_register.md +++ b/docs/php/builtins/io/stream_filter_register.md @@ -2,7 +2,7 @@ title: "stream_filter_register()" description: "Registers a user-defined stream filter." sidebar: - order: 199 + order: 203 --- ## stream_filter_register() @@ -19,6 +19,11 @@ Registers a user-defined stream filter. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_filter_remove.md b/docs/php/builtins/io/stream_filter_remove.md index b03bbe6b8a..d4d9fa4715 100644 --- a/docs/php/builtins/io/stream_filter_remove.md +++ b/docs/php/builtins/io/stream_filter_remove.md @@ -2,7 +2,7 @@ title: "stream_filter_remove()" description: "Removes a filter from a stream." sidebar: - order: 200 + order: 204 --- ## stream_filter_remove() @@ -18,6 +18,11 @@ Removes a filter from a stream. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_contents.md b/docs/php/builtins/io/stream_get_contents.md index 6e41982c9a..bebe7d5ae9 100644 --- a/docs/php/builtins/io/stream_get_contents.md +++ b/docs/php/builtins/io/stream_get_contents.md @@ -2,7 +2,7 @@ title: "stream_get_contents()" description: "Reads remainder of a stream into a string." sidebar: - order: 201 + order: 205 --- ## stream_get_contents() @@ -20,6 +20,11 @@ Reads remainder of a stream into a string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_filters.md b/docs/php/builtins/io/stream_get_filters.md index 813204a4ec..b306f050dc 100644 --- a/docs/php/builtins/io/stream_get_filters.md +++ b/docs/php/builtins/io/stream_get_filters.md @@ -2,7 +2,7 @@ title: "stream_get_filters()" description: "Retrieves list of registered filters." sidebar: - order: 202 + order: 206 --- ## stream_get_filters() @@ -17,6 +17,11 @@ Retrieves list of registered filters. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_line.md b/docs/php/builtins/io/stream_get_line.md index ed332ef462..b328090567 100644 --- a/docs/php/builtins/io/stream_get_line.md +++ b/docs/php/builtins/io/stream_get_line.md @@ -2,7 +2,7 @@ title: "stream_get_line()" description: "Gets line from stream resource up to a given delimiter." sidebar: - order: 203 + order: 207 --- ## stream_get_line() @@ -20,6 +20,11 @@ Gets line from stream resource up to a given delimiter. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_meta_data.md b/docs/php/builtins/io/stream_get_meta_data.md index 4f8a0c5105..11b796e7e2 100644 --- a/docs/php/builtins/io/stream_get_meta_data.md +++ b/docs/php/builtins/io/stream_get_meta_data.md @@ -2,7 +2,7 @@ title: "stream_get_meta_data()" description: "Retrieves metadata from streams/file pointers." sidebar: - order: 204 + order: 208 --- ## stream_get_meta_data() @@ -18,6 +18,11 @@ Retrieves metadata from streams/file pointers. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_transports.md b/docs/php/builtins/io/stream_get_transports.md index 3e306c33b7..ae6e6635c3 100644 --- a/docs/php/builtins/io/stream_get_transports.md +++ b/docs/php/builtins/io/stream_get_transports.md @@ -2,7 +2,7 @@ title: "stream_get_transports()" description: "Retrieves list of registered socket transports." sidebar: - order: 205 + order: 209 --- ## stream_get_transports() @@ -17,6 +17,11 @@ Retrieves list of registered socket transports. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_get_wrappers.md b/docs/php/builtins/io/stream_get_wrappers.md index c87abb259c..dc8ba7fca2 100644 --- a/docs/php/builtins/io/stream_get_wrappers.md +++ b/docs/php/builtins/io/stream_get_wrappers.md @@ -2,7 +2,7 @@ title: "stream_get_wrappers()" description: "Retrieves list of registered streams." sidebar: - order: 206 + order: 210 --- ## stream_get_wrappers() @@ -17,6 +17,11 @@ Retrieves list of registered streams. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_is_local.md b/docs/php/builtins/io/stream_is_local.md index b30b1f8b69..62fcee0f6e 100644 --- a/docs/php/builtins/io/stream_is_local.md +++ b/docs/php/builtins/io/stream_is_local.md @@ -2,7 +2,7 @@ title: "stream_is_local()" description: "Checks if a stream is a local stream." sidebar: - order: 207 + order: 211 --- ## stream_is_local() @@ -18,6 +18,11 @@ Checks if a stream is a local stream. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_isatty.md b/docs/php/builtins/io/stream_isatty.md index 59e169fb0a..693793775a 100644 --- a/docs/php/builtins/io/stream_isatty.md +++ b/docs/php/builtins/io/stream_isatty.md @@ -2,7 +2,7 @@ title: "stream_isatty()" description: "Checks if a stream is a TTY." sidebar: - order: 208 + order: 212 --- ## stream_isatty() @@ -18,6 +18,11 @@ Checks if a stream is a TTY. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_resolve_include_path.md b/docs/php/builtins/io/stream_resolve_include_path.md index 079a7c35b6..f7be71820d 100644 --- a/docs/php/builtins/io/stream_resolve_include_path.md +++ b/docs/php/builtins/io/stream_resolve_include_path.md @@ -2,7 +2,7 @@ title: "stream_resolve_include_path()" description: "Resolves filename against the include path." sidebar: - order: 209 + order: 213 --- ## stream_resolve_include_path() @@ -18,6 +18,11 @@ Resolves filename against the include path. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_select.md b/docs/php/builtins/io/stream_select.md index b5681c50ab..ca029ef007 100644 --- a/docs/php/builtins/io/stream_select.md +++ b/docs/php/builtins/io/stream_select.md @@ -2,7 +2,7 @@ title: "stream_select()" description: "Runs the equivalent of the select() system call on the given arrays of streams." sidebar: - order: 210 + order: 214 --- ## stream_select() @@ -22,6 +22,11 @@ Runs the equivalent of the select() system call on the given arrays of streams. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_blocking.md b/docs/php/builtins/io/stream_set_blocking.md index 37866fddaf..d4200e093f 100644 --- a/docs/php/builtins/io/stream_set_blocking.md +++ b/docs/php/builtins/io/stream_set_blocking.md @@ -2,7 +2,7 @@ title: "stream_set_blocking()" description: "Sets blocking/non-blocking mode on a stream." sidebar: - order: 211 + order: 215 --- ## stream_set_blocking() @@ -19,6 +19,11 @@ Sets blocking/non-blocking mode on a stream. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_chunk_size.md b/docs/php/builtins/io/stream_set_chunk_size.md index 1a8db790b5..466afa57e0 100644 --- a/docs/php/builtins/io/stream_set_chunk_size.md +++ b/docs/php/builtins/io/stream_set_chunk_size.md @@ -2,7 +2,7 @@ title: "stream_set_chunk_size()" description: "Sets the read chunk size on a stream." sidebar: - order: 212 + order: 216 --- ## stream_set_chunk_size() @@ -19,6 +19,11 @@ Sets the read chunk size on a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_read_buffer.md b/docs/php/builtins/io/stream_set_read_buffer.md index fcbfcaa434..abb460af02 100644 --- a/docs/php/builtins/io/stream_set_read_buffer.md +++ b/docs/php/builtins/io/stream_set_read_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_read_buffer()" description: "Sets the read file buffering on a stream." sidebar: - order: 213 + order: 217 --- ## stream_set_read_buffer() @@ -19,6 +19,11 @@ Sets the read file buffering on a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_timeout.md b/docs/php/builtins/io/stream_set_timeout.md index 990624b8c6..df3f0e17df 100644 --- a/docs/php/builtins/io/stream_set_timeout.md +++ b/docs/php/builtins/io/stream_set_timeout.md @@ -2,7 +2,7 @@ title: "stream_set_timeout()" description: "Sets timeout period on a stream." sidebar: - order: 214 + order: 218 --- ## stream_set_timeout() @@ -20,6 +20,11 @@ Sets timeout period on a stream. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_set_write_buffer.md b/docs/php/builtins/io/stream_set_write_buffer.md index 3ffbc69c95..8d6d84430d 100644 --- a/docs/php/builtins/io/stream_set_write_buffer.md +++ b/docs/php/builtins/io/stream_set_write_buffer.md @@ -2,7 +2,7 @@ title: "stream_set_write_buffer()" description: "Sets the write file buffering on a stream." sidebar: - order: 215 + order: 219 --- ## stream_set_write_buffer() @@ -19,6 +19,11 @@ Sets the write file buffering on a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_accept.md b/docs/php/builtins/io/stream_socket_accept.md index 4f49662b6e..f5cc8fd698 100644 --- a/docs/php/builtins/io/stream_socket_accept.md +++ b/docs/php/builtins/io/stream_socket_accept.md @@ -2,7 +2,7 @@ title: "stream_socket_accept()" description: "Accept a connection on a socket created by stream_socket_server()." sidebar: - order: 216 + order: 220 --- ## stream_socket_accept() @@ -20,6 +20,11 @@ Accept a connection on a socket created by stream_socket_server(). **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_client.md b/docs/php/builtins/io/stream_socket_client.md index d8133a2e64..75fa1d76e5 100644 --- a/docs/php/builtins/io/stream_socket_client.md +++ b/docs/php/builtins/io/stream_socket_client.md @@ -2,7 +2,7 @@ title: "stream_socket_client()" description: "Open Internet or Unix domain socket connection." sidebar: - order: 217 + order: 221 --- ## stream_socket_client() @@ -18,6 +18,11 @@ Open Internet or Unix domain socket connection. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_enable_crypto.md b/docs/php/builtins/io/stream_socket_enable_crypto.md index c3a720b3b4..c176e9a060 100644 --- a/docs/php/builtins/io/stream_socket_enable_crypto.md +++ b/docs/php/builtins/io/stream_socket_enable_crypto.md @@ -2,7 +2,7 @@ title: "stream_socket_enable_crypto()" description: "Turns encryption on/off on an already connected socket." sidebar: - order: 218 + order: 222 --- ## stream_socket_enable_crypto() @@ -21,6 +21,11 @@ Turns encryption on/off on an already connected socket. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_get_name.md b/docs/php/builtins/io/stream_socket_get_name.md index 66a9f9833d..de69154afc 100644 --- a/docs/php/builtins/io/stream_socket_get_name.md +++ b/docs/php/builtins/io/stream_socket_get_name.md @@ -2,7 +2,7 @@ title: "stream_socket_get_name()" description: "Retrieve the name of the local or remote sockets." sidebar: - order: 219 + order: 223 --- ## stream_socket_get_name() @@ -19,6 +19,11 @@ Retrieve the name of the local or remote sockets. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_pair.md b/docs/php/builtins/io/stream_socket_pair.md index 678755e505..cc66e95eab 100644 --- a/docs/php/builtins/io/stream_socket_pair.md +++ b/docs/php/builtins/io/stream_socket_pair.md @@ -2,7 +2,7 @@ title: "stream_socket_pair()" description: "Creates a pair of connected, indistinguishable socket streams." sidebar: - order: 220 + order: 224 --- ## stream_socket_pair() @@ -20,6 +20,11 @@ Creates a pair of connected, indistinguishable socket streams. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_recvfrom.md b/docs/php/builtins/io/stream_socket_recvfrom.md index 3b549eae0e..a148ea1503 100644 --- a/docs/php/builtins/io/stream_socket_recvfrom.md +++ b/docs/php/builtins/io/stream_socket_recvfrom.md @@ -2,7 +2,7 @@ title: "stream_socket_recvfrom()" description: "Receives data from a socket, connected or not." sidebar: - order: 221 + order: 225 --- ## stream_socket_recvfrom() @@ -21,6 +21,11 @@ Receives data from a socket, connected or not. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_sendto.md b/docs/php/builtins/io/stream_socket_sendto.md index 2ae31eb084..d5dfdeab5a 100644 --- a/docs/php/builtins/io/stream_socket_sendto.md +++ b/docs/php/builtins/io/stream_socket_sendto.md @@ -2,7 +2,7 @@ title: "stream_socket_sendto()" description: "Sends a message to a socket, whether it is connected or not." sidebar: - order: 222 + order: 226 --- ## stream_socket_sendto() @@ -21,6 +21,11 @@ Sends a message to a socket, whether it is connected or not. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_server.md b/docs/php/builtins/io/stream_socket_server.md index 658e6bb5c5..5ba8a540ec 100644 --- a/docs/php/builtins/io/stream_socket_server.md +++ b/docs/php/builtins/io/stream_socket_server.md @@ -2,7 +2,7 @@ title: "stream_socket_server()" description: "Create an Internet or Unix domain server socket." sidebar: - order: 223 + order: 227 --- ## stream_socket_server() @@ -18,6 +18,11 @@ Create an Internet or Unix domain server socket. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_socket_shutdown.md b/docs/php/builtins/io/stream_socket_shutdown.md index e36aaa1fe5..8c25fa13f3 100644 --- a/docs/php/builtins/io/stream_socket_shutdown.md +++ b/docs/php/builtins/io/stream_socket_shutdown.md @@ -2,7 +2,7 @@ title: "stream_socket_shutdown()" description: "Shutdown a full-duplex connection." sidebar: - order: 224 + order: 228 --- ## stream_socket_shutdown() @@ -19,6 +19,11 @@ Shutdown a full-duplex connection. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_supports_lock.md b/docs/php/builtins/io/stream_supports_lock.md index 0cde57ca47..0a23c097ab 100644 --- a/docs/php/builtins/io/stream_supports_lock.md +++ b/docs/php/builtins/io/stream_supports_lock.md @@ -2,7 +2,7 @@ title: "stream_supports_lock()" description: "Tells whether the stream supports locking." sidebar: - order: 225 + order: 229 --- ## stream_supports_lock() @@ -18,6 +18,11 @@ Tells whether the stream supports locking. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_wrapper_register.md b/docs/php/builtins/io/stream_wrapper_register.md index b1eb6e6363..c6dada2f1f 100644 --- a/docs/php/builtins/io/stream_wrapper_register.md +++ b/docs/php/builtins/io/stream_wrapper_register.md @@ -2,7 +2,7 @@ title: "stream_wrapper_register()" description: "Registers a URL wrapper implemented as a PHP class." sidebar: - order: 226 + order: 230 --- ## stream_wrapper_register() @@ -20,6 +20,11 @@ Registers a URL wrapper implemented as a PHP class. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_wrapper_restore.md b/docs/php/builtins/io/stream_wrapper_restore.md index b3909c2564..1624e8bfd7 100644 --- a/docs/php/builtins/io/stream_wrapper_restore.md +++ b/docs/php/builtins/io/stream_wrapper_restore.md @@ -2,7 +2,7 @@ title: "stream_wrapper_restore()" description: "Restores a previously unregistered built-in wrapper." sidebar: - order: 227 + order: 231 --- ## stream_wrapper_restore() @@ -18,6 +18,11 @@ Restores a previously unregistered built-in wrapper. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/stream_wrapper_unregister.md b/docs/php/builtins/io/stream_wrapper_unregister.md index 0b96c1b0ea..a8f0d919c5 100644 --- a/docs/php/builtins/io/stream_wrapper_unregister.md +++ b/docs/php/builtins/io/stream_wrapper_unregister.md @@ -2,7 +2,7 @@ title: "stream_wrapper_unregister()" description: "Unregisters a previously registered URL wrapper." sidebar: - order: 228 + order: 232 --- ## stream_wrapper_unregister() @@ -18,6 +18,11 @@ Unregisters a previously registered URL wrapper. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/io/vfprintf.md b/docs/php/builtins/io/vfprintf.md index faef5f65ab..01fdc3b468 100644 --- a/docs/php/builtins/io/vfprintf.md +++ b/docs/php/builtins/io/vfprintf.md @@ -2,7 +2,7 @@ title: "vfprintf()" description: "Write a formatted string to a stream." sidebar: - order: 229 + order: 233 --- ## vfprintf() @@ -20,6 +20,11 @@ Write a formatted string to a stream. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json.md b/docs/php/builtins/json.md index 09a9e16654..4f84bcb3af 100644 --- a/docs/php/builtins/json.md +++ b/docs/php/builtins/json.md @@ -7,10 +7,10 @@ sidebar: ## JSON builtins -| Function | Signature | Returns | -|---|---|---| -| [`json_decode()`](./json/json_decode.md) | `(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed` | `mixed` | -| [`json_encode()`](./json/json_encode.md) | `(mixed $value, int $flags = 0, int $depth = 512): string` | `string` | -| [`json_last_error()`](./json/json_last_error.md) | `(): int` | `int` | -| [`json_last_error_msg()`](./json/json_last_error_msg.md) | `(): string` | `string` | -| [`json_validate()`](./json/json_validate.md) | `(string $json, int $depth = 512, int $flags = 0): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`json_decode()`](./json/json_decode.md) | `(string $json, bool $associative = null, int $depth = 512, int $flags = 0): mixed` | `mixed` | ✓ | ✓ | +| [`json_encode()`](./json/json_encode.md) | `(mixed $value, int $flags = 0, int $depth = 512): string` | `string` | ✓ | ✓ | +| [`json_last_error()`](./json/json_last_error.md) | `(): int` | `int` | ✓ | ✓ | +| [`json_last_error_msg()`](./json/json_last_error_msg.md) | `(): string` | `string` | ✓ | ✓ | +| [`json_validate()`](./json/json_validate.md) | `(string $json, int $depth = 512, int $flags = 0): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/json/json_decode.md b/docs/php/builtins/json/json_decode.md index d3ff351578..ab07c77c52 100644 --- a/docs/php/builtins/json/json_decode.md +++ b/docs/php/builtins/json/json_decode.md @@ -2,7 +2,7 @@ title: "json_decode()" description: "Decodes a JSON string." sidebar: - order: 230 + order: 234 --- ## json_decode() @@ -21,6 +21,11 @@ Decodes a JSON string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json/json_encode.md b/docs/php/builtins/json/json_encode.md index 183c7517f9..b6e896d02a 100644 --- a/docs/php/builtins/json/json_encode.md +++ b/docs/php/builtins/json/json_encode.md @@ -2,7 +2,7 @@ title: "json_encode()" description: "Returns the JSON representation of a value." sidebar: - order: 231 + order: 235 --- ## json_encode() @@ -20,6 +20,11 @@ Returns the JSON representation of a value. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json/json_last_error.md b/docs/php/builtins/json/json_last_error.md index 4873936518..76f567c7ec 100644 --- a/docs/php/builtins/json/json_last_error.md +++ b/docs/php/builtins/json/json_last_error.md @@ -2,7 +2,7 @@ title: "json_last_error()" description: "Returns the last error (if any) occurred during the last JSON encoding/decoding." sidebar: - order: 232 + order: 236 --- ## json_last_error() @@ -17,6 +17,11 @@ Returns the last error (if any) occurred during the last JSON encoding/decoding. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json/json_last_error_msg.md b/docs/php/builtins/json/json_last_error_msg.md index fc3d0d284e..ea693822ef 100644 --- a/docs/php/builtins/json/json_last_error_msg.md +++ b/docs/php/builtins/json/json_last_error_msg.md @@ -2,7 +2,7 @@ title: "json_last_error_msg()" description: "Returns the error string of the last json_encode() or json_decode() call." sidebar: - order: 233 + order: 237 --- ## json_last_error_msg() @@ -17,6 +17,11 @@ Returns the error string of the last json_encode() or json_decode() call. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/json/json_validate.md b/docs/php/builtins/json/json_validate.md index fbcd6e4743..690ba416d0 100644 --- a/docs/php/builtins/json/json_validate.md +++ b/docs/php/builtins/json/json_validate.md @@ -2,7 +2,7 @@ title: "json_validate()" description: "Checks if a string contains valid JSON." sidebar: - order: 234 + order: 238 --- ## json_validate() @@ -20,6 +20,11 @@ Checks if a string contains valid JSON. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math.md b/docs/php/builtins/math.md index c4b4f8457b..39b0a972a3 100644 --- a/docs/php/builtins/math.md +++ b/docs/php/builtins/math.md @@ -7,41 +7,41 @@ sidebar: ## Math builtins -| Function | Signature | Returns | -|---|---|---| -| [`abs()`](./math/abs.md) | `(int $num): mixed` | `mixed` | -| [`acos()`](./math/acos.md) | `(float $num): float` | `float` | -| [`asin()`](./math/asin.md) | `(float $num): float` | `float` | -| [`atan()`](./math/atan.md) | `(float $num): float` | `float` | -| [`atan2()`](./math/atan2.md) | `(float $y, float $x): float` | `float` | -| [`ceil()`](./math/ceil.md) | `(float $num): float` | `float` | -| [`clamp()`](./math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | -| [`cos()`](./math/cos.md) | `(float $num): float` | `float` | -| [`cosh()`](./math/cosh.md) | `(float $num): float` | `float` | -| [`deg2rad()`](./math/deg2rad.md) | `(float $num): float` | `float` | -| [`exp()`](./math/exp.md) | `(float $num): float` | `float` | -| [`fdiv()`](./math/fdiv.md) | `(float $num1, float $num2): float` | `float` | -| [`floor()`](./math/floor.md) | `(float $num): float` | `float` | -| [`fmod()`](./math/fmod.md) | `(float $num1, float $num2): float` | `float` | -| [`hypot()`](./math/hypot.md) | `(float $x, float $y): float` | `float` | -| [`intdiv()`](./math/intdiv.md) | `(int $num1, int $num2): int` | `int` | -| [`is_finite()`](./math/is_finite.md) | `(float $num): bool` | `bool` | -| [`is_infinite()`](./math/is_infinite.md) | `(float $num): bool` | `bool` | -| [`is_nan()`](./math/is_nan.md) | `(float $num): bool` | `bool` | -| [`log()`](./math/log.md) | `(float $num, float $base = 2.718281828459045): float` | `float` | -| [`log10()`](./math/log10.md) | `(float $num): float` | `float` | -| [`log2()`](./math/log2.md) | `(float $num): float` | `float` | -| [`max()`](./math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | -| [`min()`](./math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | -| [`mt_rand()`](./math/mt_rand.md) | `(int $min, int $max): int` | `int` | -| [`pi()`](./math/pi.md) | `(): float` | `float` | -| [`pow()`](./math/pow.md) | `(float $num, float $exponent): float` | `float` | -| [`rad2deg()`](./math/rad2deg.md) | `(float $num): float` | `float` | -| [`rand()`](./math/rand.md) | `(int $min, int $max): int` | `int` | -| [`random_int()`](./math/random_int.md) | `(int $min, int $max): int` | `int` | -| [`round()`](./math/round.md) | `(float $num, int $precision = 0): float` | `float` | -| [`sin()`](./math/sin.md) | `(float $num): float` | `float` | -| [`sinh()`](./math/sinh.md) | `(float $num): float` | `float` | -| [`sqrt()`](./math/sqrt.md) | `(float $num): float` | `float` | -| [`tan()`](./math/tan.md) | `(float $num): float` | `float` | -| [`tanh()`](./math/tanh.md) | `(float $num): float` | `float` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`abs()`](./math/abs.md) | `(int $num): mixed` | `mixed` | ✓ | ✓ | +| [`acos()`](./math/acos.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`asin()`](./math/asin.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`atan()`](./math/atan.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`atan2()`](./math/atan2.md) | `(float $y, float $x): float` | `float` | ✓ | ✓ | +| [`ceil()`](./math/ceil.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`clamp()`](./math/clamp.md) | `(int $value, int $min, int $max): mixed` | `mixed` | ✓ | ✓ | +| [`cos()`](./math/cos.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`cosh()`](./math/cosh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`deg2rad()`](./math/deg2rad.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`exp()`](./math/exp.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`fdiv()`](./math/fdiv.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`floor()`](./math/floor.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`fmod()`](./math/fmod.md) | `(float $num1, float $num2): float` | `float` | ✓ | ✓ | +| [`hypot()`](./math/hypot.md) | `(float $x, float $y): float` | `float` | ✓ | ✓ | +| [`intdiv()`](./math/intdiv.md) | `(int $num1, int $num2): int` | `int` | ✓ | ✓ | +| [`is_finite()`](./math/is_finite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`is_infinite()`](./math/is_infinite.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`is_nan()`](./math/is_nan.md) | `(float $num): bool` | `bool` | ✓ | ✓ | +| [`log()`](./math/log.md) | `(float $num, float $base = 2.718281828459045): float` | `float` | ✓ | ✓ | +| [`log10()`](./math/log10.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`log2()`](./math/log2.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`max()`](./math/max.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | +| [`min()`](./math/min.md) | `(mixed $value, ...$values): mixed` | `mixed` | ✓ | ✓ | +| [`mt_rand()`](./math/mt_rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`pi()`](./math/pi.md) | `(): float` | `float` | ✓ | ✓ | +| [`pow()`](./math/pow.md) | `(float $num, float $exponent): float` | `float` | ✓ | ✓ | +| [`rad2deg()`](./math/rad2deg.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`rand()`](./math/rand.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`random_int()`](./math/random_int.md) | `(int $min, int $max): int` | `int` | ✓ | ✓ | +| [`round()`](./math/round.md) | `(float $num, int $precision = 0): float` | `float` | ✓ | ✓ | +| [`sin()`](./math/sin.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`sinh()`](./math/sinh.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`sqrt()`](./math/sqrt.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`tan()`](./math/tan.md) | `(float $num): float` | `float` | ✓ | ✓ | +| [`tanh()`](./math/tanh.md) | `(float $num): float` | `float` | ✓ | ✓ | diff --git a/docs/php/builtins/math/abs.md b/docs/php/builtins/math/abs.md index a1bd48d104..f5d3642755 100644 --- a/docs/php/builtins/math/abs.md +++ b/docs/php/builtins/math/abs.md @@ -2,7 +2,7 @@ title: "abs()" description: "Absolute value." sidebar: - order: 235 + order: 239 --- ## abs() @@ -18,6 +18,11 @@ Absolute value. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/abs.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/abs.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/acos.md b/docs/php/builtins/math/acos.md index acf751064a..de003253a4 100644 --- a/docs/php/builtins/math/acos.md +++ b/docs/php/builtins/math/acos.md @@ -2,7 +2,7 @@ title: "acos()" description: "Returns the arccosine of a number in radians." sidebar: - order: 236 + order: 240 --- ## acos() @@ -18,6 +18,11 @@ Returns the arccosine of a number in radians. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/acos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/acos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/asin.md b/docs/php/builtins/math/asin.md index e8a5038ca1..6b2ece7366 100644 --- a/docs/php/builtins/math/asin.md +++ b/docs/php/builtins/math/asin.md @@ -2,7 +2,7 @@ title: "asin()" description: "Returns the arcsine of a number in radians." sidebar: - order: 237 + order: 241 --- ## asin() @@ -18,6 +18,11 @@ Returns the arcsine of a number in radians. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/asin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/asin.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/atan.md b/docs/php/builtins/math/atan.md index f93ba54f99..adedd086b6 100644 --- a/docs/php/builtins/math/atan.md +++ b/docs/php/builtins/math/atan.md @@ -2,7 +2,7 @@ title: "atan()" description: "Returns the arctangent of a number in radians." sidebar: - order: 238 + order: 242 --- ## atan() @@ -18,6 +18,11 @@ Returns the arctangent of a number in radians. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/atan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/atan.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/atan2.md b/docs/php/builtins/math/atan2.md index f52df945f5..ae9a819286 100644 --- a/docs/php/builtins/math/atan2.md +++ b/docs/php/builtins/math/atan2.md @@ -2,7 +2,7 @@ title: "atan2()" description: "Returns the arc tangent of two variables." sidebar: - order: 239 + order: 243 --- ## atan2() @@ -19,6 +19,11 @@ Returns the arc tangent of two variables. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/atan2.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/atan2.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/ceil.md b/docs/php/builtins/math/ceil.md index 0f5564bbd9..6db76f5a3a 100644 --- a/docs/php/builtins/math/ceil.md +++ b/docs/php/builtins/math/ceil.md @@ -2,7 +2,7 @@ title: "ceil()" description: "Rounds a number up to the nearest integer." sidebar: - order: 240 + order: 244 --- ## ceil() @@ -18,6 +18,11 @@ Rounds a number up to the nearest integer. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/ceil.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/ceil.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/clamp.md b/docs/php/builtins/math/clamp.md index 1dc2122491..7a1fb45b89 100644 --- a/docs/php/builtins/math/clamp.md +++ b/docs/php/builtins/math/clamp.md @@ -2,7 +2,7 @@ title: "clamp()" description: "Clamps a value to be within a specified range." sidebar: - order: 241 + order: 245 --- ## clamp() @@ -20,6 +20,11 @@ Clamps a value to be within a specified range. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/clamp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/clamp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/cos.md b/docs/php/builtins/math/cos.md index a07542ec1f..598c775655 100644 --- a/docs/php/builtins/math/cos.md +++ b/docs/php/builtins/math/cos.md @@ -2,7 +2,7 @@ title: "cos()" description: "Returns the cosine of a number (radians)." sidebar: - order: 242 + order: 246 --- ## cos() @@ -18,6 +18,11 @@ Returns the cosine of a number (radians). **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/cos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/cos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/cosh.md b/docs/php/builtins/math/cosh.md index ea9da090fb..8cccefa5da 100644 --- a/docs/php/builtins/math/cosh.md +++ b/docs/php/builtins/math/cosh.md @@ -2,7 +2,7 @@ title: "cosh()" description: "Returns the hyperbolic cosine of a number." sidebar: - order: 243 + order: 247 --- ## cosh() @@ -18,6 +18,11 @@ Returns the hyperbolic cosine of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/cosh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/cosh.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/deg2rad.md b/docs/php/builtins/math/deg2rad.md index 4ad81c1f2f..4f4ad04d85 100644 --- a/docs/php/builtins/math/deg2rad.md +++ b/docs/php/builtins/math/deg2rad.md @@ -2,7 +2,7 @@ title: "deg2rad()" description: "Converts a degree value to radians." sidebar: - order: 244 + order: 248 --- ## deg2rad() @@ -18,6 +18,11 @@ Converts a degree value to radians. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/exp.md b/docs/php/builtins/math/exp.md index bb38c14642..0f812b4022 100644 --- a/docs/php/builtins/math/exp.md +++ b/docs/php/builtins/math/exp.md @@ -2,7 +2,7 @@ title: "exp()" description: "Returns e raised to the power of a number." sidebar: - order: 245 + order: 249 --- ## exp() @@ -18,6 +18,11 @@ Returns e raised to the power of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/exp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/exp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/fdiv.md b/docs/php/builtins/math/fdiv.md index cb296e066c..939cf5f706 100644 --- a/docs/php/builtins/math/fdiv.md +++ b/docs/php/builtins/math/fdiv.md @@ -2,7 +2,7 @@ title: "fdiv()" description: "Divides two numbers, according to IEEE 754." sidebar: - order: 246 + order: 250 --- ## fdiv() @@ -19,6 +19,11 @@ Divides two numbers, according to IEEE 754. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/floor.md b/docs/php/builtins/math/floor.md index c920dc07b5..8a44286492 100644 --- a/docs/php/builtins/math/floor.md +++ b/docs/php/builtins/math/floor.md @@ -2,7 +2,7 @@ title: "floor()" description: "Rounds a number down to the nearest integer." sidebar: - order: 247 + order: 251 --- ## floor() @@ -18,6 +18,11 @@ Rounds a number down to the nearest integer. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/floor.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/floor.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/fmod.md b/docs/php/builtins/math/fmod.md index 78a2c94254..983c7344c8 100644 --- a/docs/php/builtins/math/fmod.md +++ b/docs/php/builtins/math/fmod.md @@ -2,7 +2,7 @@ title: "fmod()" description: "Returns the floating point remainder of the division of the arguments." sidebar: - order: 248 + order: 252 --- ## fmod() @@ -19,6 +19,11 @@ Returns the floating point remainder of the division of the arguments. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/fmod.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/fmod.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/hypot.md b/docs/php/builtins/math/hypot.md index 6b97393f53..f2b4feb806 100644 --- a/docs/php/builtins/math/hypot.md +++ b/docs/php/builtins/math/hypot.md @@ -2,7 +2,7 @@ title: "hypot()" description: "Calculates the length of the hypotenuse of a right-angle triangle." sidebar: - order: 249 + order: 253 --- ## hypot() @@ -19,6 +19,11 @@ Calculates the length of the hypotenuse of a right-angle triangle. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/hypot.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/hypot.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/intdiv.md b/docs/php/builtins/math/intdiv.md index 06740dddfb..076ae1bb0d 100644 --- a/docs/php/builtins/math/intdiv.md +++ b/docs/php/builtins/math/intdiv.md @@ -2,7 +2,7 @@ title: "intdiv()" description: "Integer division." sidebar: - order: 250 + order: 254 --- ## intdiv() @@ -19,6 +19,11 @@ Integer division. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/is_finite.md b/docs/php/builtins/math/is_finite.md index eed5a5021e..04b87e5ae2 100644 --- a/docs/php/builtins/math/is_finite.md +++ b/docs/php/builtins/math/is_finite.md @@ -2,7 +2,7 @@ title: "is_finite()" description: "Checks whether a float is finite." sidebar: - order: 251 + order: 255 --- ## is_finite() @@ -18,6 +18,11 @@ Checks whether a float is finite. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/is_infinite.md b/docs/php/builtins/math/is_infinite.md index f2927a37a5..04e78a6023 100644 --- a/docs/php/builtins/math/is_infinite.md +++ b/docs/php/builtins/math/is_infinite.md @@ -2,7 +2,7 @@ title: "is_infinite()" description: "Checks whether a float is infinite." sidebar: - order: 252 + order: 256 --- ## is_infinite() @@ -18,6 +18,11 @@ Checks whether a float is infinite. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/is_nan.md b/docs/php/builtins/math/is_nan.md index 7b5ab79a73..fbfce3be15 100644 --- a/docs/php/builtins/math/is_nan.md +++ b/docs/php/builtins/math/is_nan.md @@ -2,7 +2,7 @@ title: "is_nan()" description: "Checks whether a float is NAN." sidebar: - order: 253 + order: 257 --- ## is_nan() @@ -18,6 +18,11 @@ Checks whether a float is NAN. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/log.md b/docs/php/builtins/math/log.md index 8973c6a012..3633a3a032 100644 --- a/docs/php/builtins/math/log.md +++ b/docs/php/builtins/math/log.md @@ -2,7 +2,7 @@ title: "log()" description: "Natural logarithm." sidebar: - order: 254 + order: 258 --- ## log() @@ -19,6 +19,11 @@ Natural logarithm. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/log.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/log10.md b/docs/php/builtins/math/log10.md index ac21b08007..aaeec288d9 100644 --- a/docs/php/builtins/math/log10.md +++ b/docs/php/builtins/math/log10.md @@ -2,7 +2,7 @@ title: "log10()" description: "Returns the base-10 logarithm of a number." sidebar: - order: 255 + order: 259 --- ## log10() @@ -18,6 +18,11 @@ Returns the base-10 logarithm of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/log10.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log10.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/log2.md b/docs/php/builtins/math/log2.md index 0401ccc162..f5d672ec27 100644 --- a/docs/php/builtins/math/log2.md +++ b/docs/php/builtins/math/log2.md @@ -2,7 +2,7 @@ title: "log2()" description: "Returns the base-2 logarithm of a number." sidebar: - order: 256 + order: 260 --- ## log2() @@ -18,6 +18,11 @@ Returns the base-2 logarithm of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/log2.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/log2.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/max.md b/docs/php/builtins/math/max.md index c92f69734a..6df160c2fe 100644 --- a/docs/php/builtins/math/max.md +++ b/docs/php/builtins/math/max.md @@ -2,7 +2,7 @@ title: "max()" description: "Find highest value." sidebar: - order: 257 + order: 261 --- ## max() @@ -19,6 +19,11 @@ Find highest value. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/max.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/max.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/min.md b/docs/php/builtins/math/min.md index bb959c9892..8e1ad04b2f 100644 --- a/docs/php/builtins/math/min.md +++ b/docs/php/builtins/math/min.md @@ -2,7 +2,7 @@ title: "min()" description: "Find lowest value." sidebar: - order: 258 + order: 262 --- ## min() @@ -19,6 +19,11 @@ Find lowest value. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/min.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/min.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/mt_rand.md b/docs/php/builtins/math/mt_rand.md index 83d6dd9791..81776a4bf8 100644 --- a/docs/php/builtins/math/mt_rand.md +++ b/docs/php/builtins/math/mt_rand.md @@ -2,7 +2,7 @@ title: "mt_rand()" description: "Generate a random value via the Mersenne Twister Random Number Generator." sidebar: - order: 259 + order: 263 --- ## mt_rand() @@ -19,6 +19,11 @@ Generate a random value via the Mersenne Twister Random Number Generator. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/pi.md b/docs/php/builtins/math/pi.md index 231447b7b1..1d8992ba07 100644 --- a/docs/php/builtins/math/pi.md +++ b/docs/php/builtins/math/pi.md @@ -2,7 +2,7 @@ title: "pi()" description: "Gets value of pi." sidebar: - order: 260 + order: 264 --- ## pi() @@ -17,6 +17,11 @@ Gets value of pi. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/pi.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/pi.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/pow.md b/docs/php/builtins/math/pow.md index 9fd3f5b18c..96c2717103 100644 --- a/docs/php/builtins/math/pow.md +++ b/docs/php/builtins/math/pow.md @@ -2,7 +2,7 @@ title: "pow()" description: "Exponential expression." sidebar: - order: 261 + order: 265 --- ## pow() @@ -19,6 +19,11 @@ Exponential expression. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/pow.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/pow.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/rad2deg.md b/docs/php/builtins/math/rad2deg.md index a0382b92c2..afa45fbbdc 100644 --- a/docs/php/builtins/math/rad2deg.md +++ b/docs/php/builtins/math/rad2deg.md @@ -2,7 +2,7 @@ title: "rad2deg()" description: "Converts a radian value to degrees." sidebar: - order: 262 + order: 266 --- ## rad2deg() @@ -18,6 +18,11 @@ Converts a radian value to degrees. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/rand.md b/docs/php/builtins/math/rand.md index 264d5e63e5..a03166aa1b 100644 --- a/docs/php/builtins/math/rand.md +++ b/docs/php/builtins/math/rand.md @@ -2,7 +2,7 @@ title: "rand()" description: "Generate a random integer." sidebar: - order: 263 + order: 267 --- ## rand() @@ -19,6 +19,11 @@ Generate a random integer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/rand.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/rand.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/random_int.md b/docs/php/builtins/math/random_int.md index 86f381fad7..b1952da4d4 100644 --- a/docs/php/builtins/math/random_int.md +++ b/docs/php/builtins/math/random_int.md @@ -2,7 +2,7 @@ title: "random_int()" description: "Get a cryptographically secure, uniformly selected integer." sidebar: - order: 264 + order: 268 --- ## random_int() @@ -19,6 +19,11 @@ Get a cryptographically secure, uniformly selected integer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/random_int.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/random_int.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/round.md b/docs/php/builtins/math/round.md index 3364d0c5fc..67b1868d98 100644 --- a/docs/php/builtins/math/round.md +++ b/docs/php/builtins/math/round.md @@ -2,7 +2,7 @@ title: "round()" description: "Rounds a float." sidebar: - order: 265 + order: 269 --- ## round() @@ -19,6 +19,11 @@ Rounds a float. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/round.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/round.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/sin.md b/docs/php/builtins/math/sin.md index b0a33ec7fc..4b8f6ab992 100644 --- a/docs/php/builtins/math/sin.md +++ b/docs/php/builtins/math/sin.md @@ -2,7 +2,7 @@ title: "sin()" description: "Returns the sine of a number (radians)." sidebar: - order: 266 + order: 270 --- ## sin() @@ -18,6 +18,11 @@ Returns the sine of a number (radians). **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/sin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sin.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/sinh.md b/docs/php/builtins/math/sinh.md index 8bed69d8d1..806e1dc782 100644 --- a/docs/php/builtins/math/sinh.md +++ b/docs/php/builtins/math/sinh.md @@ -2,7 +2,7 @@ title: "sinh()" description: "Returns the hyperbolic sine of a number." sidebar: - order: 267 + order: 271 --- ## sinh() @@ -18,6 +18,11 @@ Returns the hyperbolic sine of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/sinh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sinh.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/sqrt.md b/docs/php/builtins/math/sqrt.md index f6e648cb6b..8fde18fa98 100644 --- a/docs/php/builtins/math/sqrt.md +++ b/docs/php/builtins/math/sqrt.md @@ -2,7 +2,7 @@ title: "sqrt()" description: "Returns the square root of a number." sidebar: - order: 268 + order: 272 --- ## sqrt() @@ -18,6 +18,11 @@ Returns the square root of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/tan.md b/docs/php/builtins/math/tan.md index b54db263a8..3f88a46f9b 100644 --- a/docs/php/builtins/math/tan.md +++ b/docs/php/builtins/math/tan.md @@ -2,7 +2,7 @@ title: "tan()" description: "Returns the tangent of a number (radians)." sidebar: - order: 269 + order: 273 --- ## tan() @@ -18,6 +18,11 @@ Returns the tangent of a number (radians). **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/tan.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/tan.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/math/tanh.md b/docs/php/builtins/math/tanh.md index b3d2c1bdbf..eda998619a 100644 --- a/docs/php/builtins/math/tanh.md +++ b/docs/php/builtins/math/tanh.md @@ -2,7 +2,7 @@ title: "tanh()" description: "Returns the hyperbolic tangent of a number." sidebar: - order: 270 + order: 274 --- ## tanh() @@ -18,6 +18,11 @@ Returns the hyperbolic tangent of a number. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/math/tanh.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/math/tanh.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc.md b/docs/php/builtins/misc.md index 91c54afd83..b14cfc6808 100644 --- a/docs/php/builtins/misc.md +++ b/docs/php/builtins/misc.md @@ -7,19 +7,19 @@ sidebar: ## Misc builtins -| Function | Signature | Returns | -|---|---|---| -| [`buffer_new()`](./misc/buffer_new.md) | `(int $length): mixed` | `mixed` | -| [`define()`](./misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | -| [`defined()`](./misc/defined.md) | `(string $constant_name): bool` | `bool` | -| [`empty()`](./misc/empty.md) | `(mixed $value): bool` | `bool` | -| [`header()`](./misc/header.md) | `(string $header, bool $replace = true, int $response_code = 0): void` | `void` | -| [`http_response_code()`](./misc/http_response_code.md) | `(int $response_code = 0): int` | `int` | -| [`isset()`](./misc/isset.md) | `(mixed $var, ...$vars): bool` | `bool` | -| [`php_uname()`](./misc/php_uname.md) | `(string $mode = 'a'): string` | `string` | -| [`phpversion()`](./misc/phpversion.md) | `(): string` | `string` | -| [`print_r()`](./misc/print_r.md) | `(mixed $value, bool $return = false): mixed` | `mixed` | -| [`serialize()`](./misc/serialize.md) | `(mixed $value): string` | `string` | -| [`unserialize()`](./misc/unserialize.md) | `(string $data, mixed $options = []): mixed` | `mixed` | -| [`unset()`](./misc/unset.md) | `(mixed $var, ...$vars): void` | `void` | -| [`var_dump()`](./misc/var_dump.md) | `(mixed $value, ...$values): void` | `void` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`buffer_new()`](./misc/buffer_new.md) | `(int $length): mixed` | `mixed` | ✓ | ✓ | +| [`define()`](./misc/define.md) | `(string $constant_name, mixed $value): bool` | `bool` | ✓ | ✓ | +| [`defined()`](./misc/defined.md) | `(string $constant_name): bool` | `bool` | ✓ | ✓ | +| [`empty()`](./misc/empty.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`header()`](./misc/header.md) | `(string $header, bool $replace = true, int $response_code = 0): void` | `void` | ✓ | ✓ | +| [`http_response_code()`](./misc/http_response_code.md) | `(int $response_code = 0): int` | `int` | ✓ | ✓ | +| [`isset()`](./misc/isset.md) | `(mixed $var, ...$vars): bool` | `bool` | ✓ | ✓ | +| [`php_uname()`](./misc/php_uname.md) | `(string $mode = 'a'): string` | `string` | ✓ | ✓ | +| [`phpversion()`](./misc/phpversion.md) | `(): string` | `string` | ✓ | ✓ | +| [`print_r()`](./misc/print_r.md) | `(mixed $value, bool $return = false): mixed` | `mixed` | ✓ | ✓ | +| [`serialize()`](./misc/serialize.md) | `(mixed $value): string` | `string` | ✓ | — | +| [`unserialize()`](./misc/unserialize.md) | `(string $data, mixed $options = []): mixed` | `mixed` | ✓ | — | +| [`unset()`](./misc/unset.md) | `(mixed $var, ...$vars): void` | `void` | ✓ | ✓ | +| [`var_dump()`](./misc/var_dump.md) | `(mixed $value, ...$values): void` | `void` | ✓ | ✓ | diff --git a/docs/php/builtins/misc/buffer_new.md b/docs/php/builtins/misc/buffer_new.md index 5245eebc5e..34580c141e 100644 --- a/docs/php/builtins/misc/buffer_new.md +++ b/docs/php/builtins/misc/buffer_new.md @@ -2,7 +2,7 @@ title: "buffer_new()" description: "buffer_new() — misc builtin supported by Elephc." sidebar: - order: 271 + order: 275 --- ## buffer_new() @@ -18,6 +18,11 @@ function buffer_new(int $length): mixed **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/define.md b/docs/php/builtins/misc/define.md index b478f07a63..a9b336b8bb 100644 --- a/docs/php/builtins/misc/define.md +++ b/docs/php/builtins/misc/define.md @@ -2,7 +2,7 @@ title: "define()" description: "Defines a named constant at runtime." sidebar: - order: 272 + order: 276 --- ## define() @@ -19,6 +19,11 @@ Defines a named constant at runtime. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/define.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/define.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/defined.md b/docs/php/builtins/misc/defined.md index 7aaa83bb77..daff838a3e 100644 --- a/docs/php/builtins/misc/defined.md +++ b/docs/php/builtins/misc/defined.md @@ -2,7 +2,7 @@ title: "defined()" description: "Checks whether a given named constant exists." sidebar: - order: 273 + order: 277 --- ## defined() @@ -18,6 +18,11 @@ Checks whether a given named constant exists. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/defined.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/defined.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/empty.md b/docs/php/builtins/misc/empty.md index d39bb13d6e..66b92ebd33 100644 --- a/docs/php/builtins/misc/empty.md +++ b/docs/php/builtins/misc/empty.md @@ -2,7 +2,7 @@ title: "empty()" description: "Determines whether a variable is considered empty." sidebar: - order: 274 + order: 278 --- ## empty() @@ -18,6 +18,11 @@ Determines whether a variable is considered empty. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/header.md b/docs/php/builtins/misc/header.md index 2c2e1e4382..a80983e367 100644 --- a/docs/php/builtins/misc/header.md +++ b/docs/php/builtins/misc/header.md @@ -2,7 +2,7 @@ title: "header()" description: "Sends a raw HTTP header." sidebar: - order: 275 + order: 279 --- ## header() @@ -20,6 +20,11 @@ Sends a raw HTTP header. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/header.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/header.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/http_response_code.md b/docs/php/builtins/misc/http_response_code.md index 000188dc4a..f6791efac3 100644 --- a/docs/php/builtins/misc/http_response_code.md +++ b/docs/php/builtins/misc/http_response_code.md @@ -2,7 +2,7 @@ title: "http_response_code()" description: "Gets or sets the HTTP response code." sidebar: - order: 276 + order: 280 --- ## http_response_code() @@ -18,6 +18,11 @@ Gets or sets the HTTP response code. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/isset.md b/docs/php/builtins/misc/isset.md index 52494d3ba2..fb402cf27f 100644 --- a/docs/php/builtins/misc/isset.md +++ b/docs/php/builtins/misc/isset.md @@ -2,7 +2,7 @@ title: "isset()" description: "Determines whether a variable is set and is not null." sidebar: - order: 277 + order: 281 --- ## isset() @@ -19,6 +19,11 @@ Determines whether a variable is set and is not null. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/php_uname.md b/docs/php/builtins/misc/php_uname.md index 7f84895d09..248bad3bd2 100644 --- a/docs/php/builtins/misc/php_uname.md +++ b/docs/php/builtins/misc/php_uname.md @@ -2,7 +2,7 @@ title: "php_uname()" description: "Returns information about the operating system PHP is running on." sidebar: - order: 278 + order: 282 --- ## php_uname() @@ -18,6 +18,11 @@ Returns information about the operating system PHP is running on. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/phpversion.md b/docs/php/builtins/misc/phpversion.md index ec9ae47c43..1a79779777 100644 --- a/docs/php/builtins/misc/phpversion.md +++ b/docs/php/builtins/misc/phpversion.md @@ -2,7 +2,7 @@ title: "phpversion()" description: "Returns the current PHP version information." sidebar: - order: 279 + order: 283 --- ## phpversion() @@ -17,6 +17,11 @@ Returns the current PHP version information. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/print_r.md b/docs/php/builtins/misc/print_r.md index 5d45f0a5f4..f850db9f64 100644 --- a/docs/php/builtins/misc/print_r.md +++ b/docs/php/builtins/misc/print_r.md @@ -2,7 +2,7 @@ title: "print_r()" description: "Prints human-readable information about a variable." sidebar: - order: 280 + order: 284 --- ## print_r() @@ -19,6 +19,11 @@ Prints human-readable information about a variable. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/print_r.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/print_r.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/serialize.md b/docs/php/builtins/misc/serialize.md index 254d3e27e1..6e737a93d8 100644 --- a/docs/php/builtins/misc/serialize.md +++ b/docs/php/builtins/misc/serialize.md @@ -2,7 +2,7 @@ title: "serialize()" description: "Generates a storable representation of a value." sidebar: - order: 281 + order: 285 --- ## serialize() @@ -18,6 +18,11 @@ Generates a storable representation of a value. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/unserialize.md b/docs/php/builtins/misc/unserialize.md index d1d43ca6cd..fdb83fe98f 100644 --- a/docs/php/builtins/misc/unserialize.md +++ b/docs/php/builtins/misc/unserialize.md @@ -2,7 +2,7 @@ title: "unserialize()" description: "Creates a PHP value from a stored representation." sidebar: - order: 282 + order: 286 --- ## unserialize() @@ -19,6 +19,11 @@ Creates a PHP value from a stored representation. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/unset.md b/docs/php/builtins/misc/unset.md index b80b20fdff..10f96dff1a 100644 --- a/docs/php/builtins/misc/unset.md +++ b/docs/php/builtins/misc/unset.md @@ -2,7 +2,7 @@ title: "unset()" description: "Unsets the given variables." sidebar: - order: 283 + order: 287 --- ## unset() @@ -19,6 +19,11 @@ Unsets the given variables. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/misc/var_dump.md b/docs/php/builtins/misc/var_dump.md index a3767a9f1c..8cefb76d21 100644 --- a/docs/php/builtins/misc/var_dump.md +++ b/docs/php/builtins/misc/var_dump.md @@ -2,7 +2,7 @@ title: "var_dump()" description: "Dumps information about a variable, including its type and value." sidebar: - order: 284 + order: 288 --- ## var_dump() @@ -19,6 +19,11 @@ Dumps information about a variable, including its type and value. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer.md b/docs/php/builtins/pointer.md index 1753862977..7a69dc20b2 100644 --- a/docs/php/builtins/pointer.md +++ b/docs/php/builtins/pointer.md @@ -7,24 +7,24 @@ sidebar: ## Pointer builtins -| Function | Signature | Returns | -|---|---|---| -| [`ptr()`](./pointer/ptr.md) | `(mixed $value): mixed` | `mixed` | -| [`ptr_get()`](./pointer/ptr_get.md) | `(pointer $pointer): int` | `int` | -| [`ptr_is_null()`](./pointer/ptr_is_null.md) | `(pointer $pointer): bool` | `bool` | -| [`ptr_null()`](./pointer/ptr_null.md) | `(): mixed` | `mixed` | -| [`ptr_offset()`](./pointer/ptr_offset.md) | `(pointer $pointer, int $offset): mixed` | `mixed` | -| [`ptr_read16()`](./pointer/ptr_read16.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read32()`](./pointer/ptr_read32.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read8()`](./pointer/ptr_read8.md) | `(pointer $pointer): int` | `int` | -| [`ptr_read_string()`](./pointer/ptr_read_string.md) | `(pointer $pointer, int $length): string` | `string` | -| [`ptr_set()`](./pointer/ptr_set.md) | `(pointer $pointer, mixed $value): void` | `void` | -| [`ptr_sizeof()`](./pointer/ptr_sizeof.md) | `(string $type): int` | `int` | -| [`ptr_write16()`](./pointer/ptr_write16.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write32()`](./pointer/ptr_write32.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write8()`](./pointer/ptr_write8.md) | `(pointer $pointer, int $value): void` | `void` | -| [`ptr_write_string()`](./pointer/ptr_write_string.md) | `(pointer $pointer, string $string): int` | `int` | -| [`zval_free()`](./pointer/zval_free.md) | `(pointer $zval): void` | `void` | -| [`zval_pack()`](./pointer/zval_pack.md) | `(mixed $value): pointer` | `pointer` | -| [`zval_type()`](./pointer/zval_type.md) | `(pointer $zval): int` | `int` | -| [`zval_unpack()`](./pointer/zval_unpack.md) | `(pointer $zval): mixed` | `mixed` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`ptr()`](./pointer/ptr.md) | `(mixed $value): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_get()`](./pointer/ptr_get.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_is_null()`](./pointer/ptr_is_null.md) | `(pointer $pointer): bool` | `bool` | ✓ | ✓ | +| [`ptr_null()`](./pointer/ptr_null.md) | `(): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_offset()`](./pointer/ptr_offset.md) | `(pointer $pointer, int $offset): mixed` | `mixed` | ✓ | ✓ | +| [`ptr_read16()`](./pointer/ptr_read16.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read32()`](./pointer/ptr_read32.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read8()`](./pointer/ptr_read8.md) | `(pointer $pointer): int` | `int` | ✓ | ✓ | +| [`ptr_read_string()`](./pointer/ptr_read_string.md) | `(pointer $pointer, int $length): string` | `string` | ✓ | ✓ | +| [`ptr_set()`](./pointer/ptr_set.md) | `(pointer $pointer, mixed $value): void` | `void` | ✓ | ✓ | +| [`ptr_sizeof()`](./pointer/ptr_sizeof.md) | `(string $type): int` | `int` | ✓ | ✓ | +| [`ptr_write16()`](./pointer/ptr_write16.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write32()`](./pointer/ptr_write32.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write8()`](./pointer/ptr_write8.md) | `(pointer $pointer, int $value): void` | `void` | ✓ | ✓ | +| [`ptr_write_string()`](./pointer/ptr_write_string.md) | `(pointer $pointer, string $string): int` | `int` | ✓ | ✓ | +| [`zval_free()`](./pointer/zval_free.md) | `(pointer $zval): void` | `void` | ✓ | — | +| [`zval_pack()`](./pointer/zval_pack.md) | `(mixed $value): pointer` | `pointer` | ✓ | — | +| [`zval_type()`](./pointer/zval_type.md) | `(pointer $zval): int` | `int` | ✓ | — | +| [`zval_unpack()`](./pointer/zval_unpack.md) | `(pointer $zval): mixed` | `mixed` | ✓ | — | diff --git a/docs/php/builtins/pointer/ptr.md b/docs/php/builtins/pointer/ptr.md index b9dea500b8..9508c7a85c 100644 --- a/docs/php/builtins/pointer/ptr.md +++ b/docs/php/builtins/pointer/ptr.md @@ -2,7 +2,7 @@ title: "ptr()" description: "Returns a raw pointer to the given variable." sidebar: - order: 285 + order: 289 --- ## ptr() @@ -18,6 +18,11 @@ Returns a raw pointer to the given variable. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_get.md b/docs/php/builtins/pointer/ptr_get.md index e42836d62e..bf295807b1 100644 --- a/docs/php/builtins/pointer/ptr_get.md +++ b/docs/php/builtins/pointer/ptr_get.md @@ -2,7 +2,7 @@ title: "ptr_get()" description: "Reads one machine word through a raw pointer and returns it as an integer." sidebar: - order: 286 + order: 290 --- ## ptr_get() @@ -18,6 +18,11 @@ Reads one machine word through a raw pointer and returns it as an integer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_is_null.md b/docs/php/builtins/pointer/ptr_is_null.md index 63a5dd4828..7c1484e556 100644 --- a/docs/php/builtins/pointer/ptr_is_null.md +++ b/docs/php/builtins/pointer/ptr_is_null.md @@ -2,7 +2,7 @@ title: "ptr_is_null()" description: "Returns true if the pointer is null." sidebar: - order: 287 + order: 291 --- ## ptr_is_null() @@ -18,6 +18,11 @@ Returns true if the pointer is null. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_null.md b/docs/php/builtins/pointer/ptr_null.md index b3756ec496..ed2b38d16f 100644 --- a/docs/php/builtins/pointer/ptr_null.md +++ b/docs/php/builtins/pointer/ptr_null.md @@ -2,7 +2,7 @@ title: "ptr_null()" description: "Returns a null raw pointer." sidebar: - order: 288 + order: 292 --- ## ptr_null() @@ -17,6 +17,11 @@ Returns a null raw pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_offset.md b/docs/php/builtins/pointer/ptr_offset.md index 54b7b95c7c..d1fe66c738 100644 --- a/docs/php/builtins/pointer/ptr_offset.md +++ b/docs/php/builtins/pointer/ptr_offset.md @@ -2,7 +2,7 @@ title: "ptr_offset()" description: "Returns a new pointer offset from the given pointer by the given byte count." sidebar: - order: 289 + order: 293 --- ## ptr_offset() @@ -19,6 +19,11 @@ Returns a new pointer offset from the given pointer by the given byte count. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read16.md b/docs/php/builtins/pointer/ptr_read16.md index e1caeb7e71..01fb96f0de 100644 --- a/docs/php/builtins/pointer/ptr_read16.md +++ b/docs/php/builtins/pointer/ptr_read16.md @@ -2,7 +2,7 @@ title: "ptr_read16()" description: "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer." sidebar: - order: 290 + order: 294 --- ## ptr_read16() @@ -18,6 +18,11 @@ Reads one unsigned 16-bit word through a raw pointer and returns it as an intege **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read32.md b/docs/php/builtins/pointer/ptr_read32.md index 351806a9d0..4bb3023d17 100644 --- a/docs/php/builtins/pointer/ptr_read32.md +++ b/docs/php/builtins/pointer/ptr_read32.md @@ -2,7 +2,7 @@ title: "ptr_read32()" description: "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer." sidebar: - order: 291 + order: 295 --- ## ptr_read32() @@ -18,6 +18,11 @@ Reads one unsigned 32-bit word through a raw pointer and returns it as an intege **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read8.md b/docs/php/builtins/pointer/ptr_read8.md index c6325cbc2d..6f0906e47f 100644 --- a/docs/php/builtins/pointer/ptr_read8.md +++ b/docs/php/builtins/pointer/ptr_read8.md @@ -2,7 +2,7 @@ title: "ptr_read8()" description: "Reads one unsigned byte through a raw pointer and returns it as an integer." sidebar: - order: 292 + order: 296 --- ## ptr_read8() @@ -18,6 +18,11 @@ Reads one unsigned byte through a raw pointer and returns it as an integer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_read_string.md b/docs/php/builtins/pointer/ptr_read_string.md index 8240e0cd5a..52f27f4d03 100644 --- a/docs/php/builtins/pointer/ptr_read_string.md +++ b/docs/php/builtins/pointer/ptr_read_string.md @@ -2,7 +2,7 @@ title: "ptr_read_string()" description: "Copies raw bytes from a pointer into a PHP string of the given length." sidebar: - order: 293 + order: 297 --- ## ptr_read_string() @@ -19,6 +19,11 @@ Copies raw bytes from a pointer into a PHP string of the given length. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_set.md b/docs/php/builtins/pointer/ptr_set.md index 546adc0403..155d0c8060 100644 --- a/docs/php/builtins/pointer/ptr_set.md +++ b/docs/php/builtins/pointer/ptr_set.md @@ -2,7 +2,7 @@ title: "ptr_set()" description: "Writes one machine word through a raw pointer." sidebar: - order: 294 + order: 298 --- ## ptr_set() @@ -19,6 +19,11 @@ Writes one machine word through a raw pointer. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_sizeof.md b/docs/php/builtins/pointer/ptr_sizeof.md index b6c17eb104..f6a53fa4a2 100644 --- a/docs/php/builtins/pointer/ptr_sizeof.md +++ b/docs/php/builtins/pointer/ptr_sizeof.md @@ -2,7 +2,7 @@ title: "ptr_sizeof()" description: "Returns the byte size of the named pointer target type." sidebar: - order: 295 + order: 299 --- ## ptr_sizeof() @@ -18,6 +18,11 @@ Returns the byte size of the named pointer target type. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write16.md b/docs/php/builtins/pointer/ptr_write16.md index 0c1f5f6711..f4f7c45bdd 100644 --- a/docs/php/builtins/pointer/ptr_write16.md +++ b/docs/php/builtins/pointer/ptr_write16.md @@ -2,7 +2,7 @@ title: "ptr_write16()" description: "Writes one 16-bit word through a raw pointer." sidebar: - order: 296 + order: 300 --- ## ptr_write16() @@ -19,6 +19,11 @@ Writes one 16-bit word through a raw pointer. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write32.md b/docs/php/builtins/pointer/ptr_write32.md index 6b456effc9..bd624bb964 100644 --- a/docs/php/builtins/pointer/ptr_write32.md +++ b/docs/php/builtins/pointer/ptr_write32.md @@ -2,7 +2,7 @@ title: "ptr_write32()" description: "Writes one 32-bit word through a raw pointer." sidebar: - order: 297 + order: 301 --- ## ptr_write32() @@ -19,6 +19,11 @@ Writes one 32-bit word through a raw pointer. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write8.md b/docs/php/builtins/pointer/ptr_write8.md index 8dd2dd86bb..46d8170bf7 100644 --- a/docs/php/builtins/pointer/ptr_write8.md +++ b/docs/php/builtins/pointer/ptr_write8.md @@ -2,7 +2,7 @@ title: "ptr_write8()" description: "Writes one byte through a raw pointer." sidebar: - order: 298 + order: 302 --- ## ptr_write8() @@ -19,6 +19,11 @@ Writes one byte through a raw pointer. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/ptr_write_string.md b/docs/php/builtins/pointer/ptr_write_string.md index b76409106e..970f2dd3d4 100644 --- a/docs/php/builtins/pointer/ptr_write_string.md +++ b/docs/php/builtins/pointer/ptr_write_string.md @@ -2,7 +2,7 @@ title: "ptr_write_string()" description: "Copies PHP string bytes into raw memory at the given pointer." sidebar: - order: 299 + order: 303 --- ## ptr_write_string() @@ -19,6 +19,11 @@ Copies PHP string bytes into raw memory at the given pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_free.md b/docs/php/builtins/pointer/zval_free.md index 36937da4d2..cd046be29d 100644 --- a/docs/php/builtins/pointer/zval_free.md +++ b/docs/php/builtins/pointer/zval_free.md @@ -2,7 +2,7 @@ title: "zval_free()" description: "Frees a PHP zval pointer allocated by `zval_pack`." sidebar: - order: 300 + order: 304 --- ## zval_free() @@ -18,6 +18,11 @@ Frees a PHP zval pointer allocated by `zval_pack`. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_pack.md b/docs/php/builtins/pointer/zval_pack.md index 64d39a7f95..ae3b82705f 100644 --- a/docs/php/builtins/pointer/zval_pack.md +++ b/docs/php/builtins/pointer/zval_pack.md @@ -2,7 +2,7 @@ title: "zval_pack()" description: "Packs an elephc runtime value into a heap-allocated PHP zval pointer." sidebar: - order: 301 + order: 305 --- ## zval_pack() @@ -18,6 +18,11 @@ Packs an elephc runtime value into a heap-allocated PHP zval pointer. **Returns**: `pointer` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_type.md b/docs/php/builtins/pointer/zval_type.md index c025ec8f3d..6f8351dfb0 100644 --- a/docs/php/builtins/pointer/zval_type.md +++ b/docs/php/builtins/pointer/zval_type.md @@ -2,7 +2,7 @@ title: "zval_type()" description: "Returns the PHP zval type byte for a zval pointer." sidebar: - order: 302 + order: 306 --- ## zval_type() @@ -18,6 +18,11 @@ Returns the PHP zval type byte for a zval pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/pointer/zval_unpack.md b/docs/php/builtins/pointer/zval_unpack.md index 72a3ea8d34..3d0d4c6aab 100644 --- a/docs/php/builtins/pointer/zval_unpack.md +++ b/docs/php/builtins/pointer/zval_unpack.md @@ -2,7 +2,7 @@ title: "zval_unpack()" description: "Unpacks a PHP zval pointer into an owned elephc Mixed value." sidebar: - order: 303 + order: 307 --- ## zval_unpack() @@ -18,6 +18,11 @@ Unpacks a PHP zval pointer into an owned elephc Mixed value. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: not available inside eval'd code. + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process.md b/docs/php/builtins/process.md index dacbecf105..d1d1f080bc 100644 --- a/docs/php/builtins/process.md +++ b/docs/php/builtins/process.md @@ -7,16 +7,16 @@ sidebar: ## Process builtins -| Function | Signature | Returns | -|---|---|---| -| [`die()`](./process/die.md) | `(int $status): void` | `void` | -| [`exec()`](./process/exec.md) | `(string $command): string` | `string` | -| [`exit()`](./process/exit.md) | `(int $status): void` | `void` | -| [`passthru()`](./process/passthru.md) | `(string $command): void` | `void` | -| [`pclose()`](./process/pclose.md) | `(resource $handle): int` | `int` | -| [`popen()`](./process/popen.md) | `(string $command, string $mode): mixed` | `mixed` | -| [`readline()`](./process/readline.md) | `(string $prompt = null): mixed` | `mixed` | -| [`shell_exec()`](./process/shell_exec.md) | `(string $command): string` | `string` | -| [`sleep()`](./process/sleep.md) | `(int $seconds): int` | `int` | -| [`system()`](./process/system.md) | `(string $command): string` | `string` | -| [`usleep()`](./process/usleep.md) | `(int $microseconds): void` | `void` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`die()`](./process/die.md) | `(int $status): void` | `void` | ✓ | ✓ | +| [`exec()`](./process/exec.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`exit()`](./process/exit.md) | `(int $status): void` | `void` | ✓ | ✓ | +| [`passthru()`](./process/passthru.md) | `(string $command): void` | `void` | ✓ | ✓ | +| [`pclose()`](./process/pclose.md) | `(resource $handle): int` | `int` | ✓ | ✓ | +| [`popen()`](./process/popen.md) | `(string $command, string $mode): mixed` | `mixed` | ✓ | ✓ | +| [`readline()`](./process/readline.md) | `(string $prompt = null): mixed` | `mixed` | ✓ | ✓ | +| [`shell_exec()`](./process/shell_exec.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`sleep()`](./process/sleep.md) | `(int $seconds): int` | `int` | ✓ | ✓ | +| [`system()`](./process/system.md) | `(string $command): string` | `string` | ✓ | ✓ | +| [`usleep()`](./process/usleep.md) | `(int $microseconds): void` | `void` | ✓ | ✓ | diff --git a/docs/php/builtins/process/die.md b/docs/php/builtins/process/die.md index e306fd41bd..3b8f3fd6db 100644 --- a/docs/php/builtins/process/die.md +++ b/docs/php/builtins/process/die.md @@ -2,7 +2,7 @@ title: "die()" description: "die() — process builtin supported by Elephc." sidebar: - order: 304 + order: 308 --- ## die() @@ -18,6 +18,11 @@ function die(int $status): void **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/die.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/die.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/exec.md b/docs/php/builtins/process/exec.md index 0daa23a6ed..84c1a4f231 100644 --- a/docs/php/builtins/process/exec.md +++ b/docs/php/builtins/process/exec.md @@ -2,7 +2,7 @@ title: "exec()" description: "Executes an external program and returns the last line of output." sidebar: - order: 305 + order: 309 --- ## exec() @@ -18,6 +18,11 @@ Executes an external program and returns the last line of output. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/exit.md b/docs/php/builtins/process/exit.md index 5b645f94e2..5d84057249 100644 --- a/docs/php/builtins/process/exit.md +++ b/docs/php/builtins/process/exit.md @@ -2,7 +2,7 @@ title: "exit()" description: "exit() — process builtin supported by Elephc." sidebar: - order: 306 + order: 310 --- ## exit() @@ -18,6 +18,11 @@ function exit(int $status): void **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/exit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/exit.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/passthru.md b/docs/php/builtins/process/passthru.md index d23873fd09..52a25996ec 100644 --- a/docs/php/builtins/process/passthru.md +++ b/docs/php/builtins/process/passthru.md @@ -2,7 +2,7 @@ title: "passthru()" description: "Executes an external program and passes its output directly." sidebar: - order: 307 + order: 311 --- ## passthru() @@ -18,6 +18,11 @@ Executes an external program and passes its output directly. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/pclose.md b/docs/php/builtins/process/pclose.md index 9f45bc241e..beff9e05bc 100644 --- a/docs/php/builtins/process/pclose.md +++ b/docs/php/builtins/process/pclose.md @@ -2,7 +2,7 @@ title: "pclose()" description: "Closes process file pointer." sidebar: - order: 308 + order: 312 --- ## pclose() @@ -18,6 +18,11 @@ Closes process file pointer. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/popen.md b/docs/php/builtins/process/popen.md index bc6fdf943d..78c84729e6 100644 --- a/docs/php/builtins/process/popen.md +++ b/docs/php/builtins/process/popen.md @@ -2,7 +2,7 @@ title: "popen()" description: "Opens process file pointer." sidebar: - order: 309 + order: 313 --- ## popen() @@ -19,6 +19,11 @@ Opens process file pointer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/readline.md b/docs/php/builtins/process/readline.md index 0c836f938c..0a6c4a7861 100644 --- a/docs/php/builtins/process/readline.md +++ b/docs/php/builtins/process/readline.md @@ -2,7 +2,7 @@ title: "readline()" description: "Reads a line from the user's terminal." sidebar: - order: 310 + order: 314 --- ## readline() @@ -18,6 +18,11 @@ Reads a line from the user's terminal. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/shell_exec.md b/docs/php/builtins/process/shell_exec.md index 240cf5800d..d3d69c4cf7 100644 --- a/docs/php/builtins/process/shell_exec.md +++ b/docs/php/builtins/process/shell_exec.md @@ -2,7 +2,7 @@ title: "shell_exec()" description: "Executes a command via the shell and returns the complete output as a string." sidebar: - order: 311 + order: 315 --- ## shell_exec() @@ -18,6 +18,11 @@ Executes a command via the shell and returns the complete output as a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/sleep.md b/docs/php/builtins/process/sleep.md index 40fe11aa4c..7810d127af 100644 --- a/docs/php/builtins/process/sleep.md +++ b/docs/php/builtins/process/sleep.md @@ -2,7 +2,7 @@ title: "sleep()" description: "Delays execution for a number of seconds." sidebar: - order: 312 + order: 316 --- ## sleep() @@ -18,6 +18,11 @@ Delays execution for a number of seconds. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/sleep.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/sleep.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/system.md b/docs/php/builtins/process/system.md index bfb15f8368..234eb89169 100644 --- a/docs/php/builtins/process/system.md +++ b/docs/php/builtins/process/system.md @@ -2,7 +2,7 @@ title: "system()" description: "Executes an external program and displays the output." sidebar: - order: 313 + order: 317 --- ## system() @@ -18,6 +18,11 @@ Executes an external program and displays the output. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/system.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/system.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/process/usleep.md b/docs/php/builtins/process/usleep.md index c1bb02b9d8..caff5a8f59 100644 --- a/docs/php/builtins/process/usleep.md +++ b/docs/php/builtins/process/usleep.md @@ -2,7 +2,7 @@ title: "usleep()" description: "Delays execution for a number of microseconds." sidebar: - order: 314 + order: 318 --- ## usleep() @@ -18,6 +18,11 @@ Delays execution for a number of microseconds. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/time/usleep.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/time/usleep.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex.md b/docs/php/builtins/regex.md index d0a58ab919..266750173f 100644 --- a/docs/php/builtins/regex.md +++ b/docs/php/builtins/regex.md @@ -7,11 +7,11 @@ sidebar: ## Regex builtins -| Function | Signature | Returns | -|---|---|---| -| [`mb_ereg_match()`](./regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | -| [`preg_match()`](./regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | -| [`preg_match_all()`](./regex/preg_match_all.md) | `(string $pattern, string $subject): int` | `int` | -| [`preg_replace()`](./regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject): string` | `string` | -| [`preg_replace_callback()`](./regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject): string` | `string` | -| [`preg_split()`](./regex/preg_split.md) | `(string $pattern, string $subject, int $limit = -1, int $flags = 0): array` | `array` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`mb_ereg_match()`](./regex/mb_ereg_match.md) | `(string $pattern, string $subject, string $options = null): bool` | `bool` | ✓ | ✓ | +| [`preg_match()`](./regex/preg_match.md) | `(string $pattern, string $subject, array $matches = []): int` | `int` | ✓ | ✓ | +| [`preg_match_all()`](./regex/preg_match_all.md) | `(string $pattern, string $subject): int` | `int` | ✓ | ✓ | +| [`preg_replace()`](./regex/preg_replace.md) | `(string $pattern, string $replacement, string $subject): string` | `string` | ✓ | ✓ | +| [`preg_replace_callback()`](./regex/preg_replace_callback.md) | `(string $pattern, callable $callback, string $subject): string` | `string` | ✓ | ✓ | +| [`preg_split()`](./regex/preg_split.md) | `(string $pattern, string $subject, int $limit = -1, int $flags = 0): array` | `array` | ✓ | ✓ | diff --git a/docs/php/builtins/regex/mb_ereg_match.md b/docs/php/builtins/regex/mb_ereg_match.md index d8363dda77..85abfa21bf 100644 --- a/docs/php/builtins/regex/mb_ereg_match.md +++ b/docs/php/builtins/regex/mb_ereg_match.md @@ -2,7 +2,7 @@ title: "mb_ereg_match()" description: "Tests whether a regex pattern matches the beginning of a string (multibyte)." sidebar: - order: 315 + order: 319 --- ## mb_ereg_match() @@ -20,6 +20,11 @@ Tests whether a regex pattern matches the beginning of a string (multibyte). **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_match.md b/docs/php/builtins/regex/preg_match.md index 92ba577e78..4821ef06da 100644 --- a/docs/php/builtins/regex/preg_match.md +++ b/docs/php/builtins/regex/preg_match.md @@ -2,7 +2,7 @@ title: "preg_match()" description: "Performs a regular expression match." sidebar: - order: 316 + order: 320 --- ## preg_match() @@ -20,6 +20,11 @@ Performs a regular expression match. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_match_all.md b/docs/php/builtins/regex/preg_match_all.md index 064c0bbd83..d286472894 100644 --- a/docs/php/builtins/regex/preg_match_all.md +++ b/docs/php/builtins/regex/preg_match_all.md @@ -2,7 +2,7 @@ title: "preg_match_all()" description: "Performs a global regular expression match and returns the number of matches." sidebar: - order: 317 + order: 321 --- ## preg_match_all() @@ -19,6 +19,11 @@ Performs a global regular expression match and returns the number of matches. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_replace.md b/docs/php/builtins/regex/preg_replace.md index 804e96c2d9..06e7330961 100644 --- a/docs/php/builtins/regex/preg_replace.md +++ b/docs/php/builtins/regex/preg_replace.md @@ -2,7 +2,7 @@ title: "preg_replace()" description: "Performs a regular expression search and replace." sidebar: - order: 318 + order: 322 --- ## preg_replace() @@ -20,6 +20,11 @@ Performs a regular expression search and replace. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_replace_callback.md b/docs/php/builtins/regex/preg_replace_callback.md index 3d0200b2b5..a54f565707 100644 --- a/docs/php/builtins/regex/preg_replace_callback.md +++ b/docs/php/builtins/regex/preg_replace_callback.md @@ -2,7 +2,7 @@ title: "preg_replace_callback()" description: "Performs a regular expression search and replace using a callback." sidebar: - order: 319 + order: 323 --- ## preg_replace_callback() @@ -20,6 +20,11 @@ Performs a regular expression search and replace using a callback. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/regex/preg_split.md b/docs/php/builtins/regex/preg_split.md index c135f843dd..3cc2a0ce20 100644 --- a/docs/php/builtins/regex/preg_split.md +++ b/docs/php/builtins/regex/preg_split.md @@ -2,7 +2,7 @@ title: "preg_split()" description: "Splits a string by a regular expression." sidebar: - order: 320 + order: 324 --- ## preg_split() @@ -21,6 +21,11 @@ Splits a string by a regular expression. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl.md b/docs/php/builtins/spl.md index c94612f328..3adfe281c2 100644 --- a/docs/php/builtins/spl.md +++ b/docs/php/builtins/spl.md @@ -7,17 +7,17 @@ sidebar: ## SPL builtins -| Function | Signature | Returns | -|---|---|---| -| [`iterator_apply()`](./spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args = null): int` | `int` | -| [`iterator_count()`](./spl/iterator_count.md) | `(traversable $iterator): int` | `int` | -| [`iterator_to_array()`](./spl/iterator_to_array.md) | `(traversable $iterator, bool $preserve_keys = true): array` | `array` | -| [`spl_autoload()`](./spl/spl_autoload.md) | `(string $class, string $file_extensions = null): void` | `void` | -| [`spl_autoload_call()`](./spl/spl_autoload_call.md) | `(string $class): void` | `void` | -| [`spl_autoload_extensions()`](./spl/spl_autoload_extensions.md) | `(string $file_extensions = null): string` | `string` | -| [`spl_autoload_functions()`](./spl/spl_autoload_functions.md) | `(): array` | `array` | -| [`spl_autoload_register()`](./spl/spl_autoload_register.md) | `(callable $callback = null, bool $throw = true, bool $prepend = false): bool` | `bool` | -| [`spl_autoload_unregister()`](./spl/spl_autoload_unregister.md) | `(callable $callback): bool` | `bool` | -| [`spl_classes()`](./spl/spl_classes.md) | `(): array` | `array` | -| [`spl_object_hash()`](./spl/spl_object_hash.md) | `(object $object): string` | `string` | -| [`spl_object_id()`](./spl/spl_object_id.md) | `(object $object): int` | `int` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`iterator_apply()`](./spl/iterator_apply.md) | `(traversable $iterator, callable $callback, array $args = null): int` | `int` | ✓ | ✓ | +| [`iterator_count()`](./spl/iterator_count.md) | `(traversable $iterator): int` | `int` | ✓ | ✓ | +| [`iterator_to_array()`](./spl/iterator_to_array.md) | `(traversable $iterator, bool $preserve_keys = true): array` | `array` | ✓ | ✓ | +| [`spl_autoload()`](./spl/spl_autoload.md) | `(string $class, string $file_extensions = null): void` | `void` | ✓ | ✓ | +| [`spl_autoload_call()`](./spl/spl_autoload_call.md) | `(string $class): void` | `void` | ✓ | ✓ | +| [`spl_autoload_extensions()`](./spl/spl_autoload_extensions.md) | `(string $file_extensions = null): string` | `string` | ✓ | ✓ | +| [`spl_autoload_functions()`](./spl/spl_autoload_functions.md) | `(): array` | `array` | ✓ | ✓ | +| [`spl_autoload_register()`](./spl/spl_autoload_register.md) | `(callable $callback = null, bool $throw = true, bool $prepend = false): bool` | `bool` | ✓ | ✓ | +| [`spl_autoload_unregister()`](./spl/spl_autoload_unregister.md) | `(callable $callback): bool` | `bool` | ✓ | ✓ | +| [`spl_classes()`](./spl/spl_classes.md) | `(): array` | `array` | ✓ | ✓ | +| [`spl_object_hash()`](./spl/spl_object_hash.md) | `(object $object): string` | `string` | ✓ | ✓ | +| [`spl_object_id()`](./spl/spl_object_id.md) | `(object $object): int` | `int` | ✓ | ✓ | diff --git a/docs/php/builtins/spl/iterator_apply.md b/docs/php/builtins/spl/iterator_apply.md index 437b93161e..a4f3bbc5b8 100644 --- a/docs/php/builtins/spl/iterator_apply.md +++ b/docs/php/builtins/spl/iterator_apply.md @@ -2,7 +2,7 @@ title: "iterator_apply()" description: "Call a function for every element in an iterator." sidebar: - order: 321 + order: 325 --- ## iterator_apply() @@ -20,6 +20,11 @@ Call a function for every element in an iterator. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/iterator_count.md b/docs/php/builtins/spl/iterator_count.md index 42a005db0a..18ea84aeed 100644 --- a/docs/php/builtins/spl/iterator_count.md +++ b/docs/php/builtins/spl/iterator_count.md @@ -2,7 +2,7 @@ title: "iterator_count()" description: "Count the elements in an iterator." sidebar: - order: 322 + order: 326 --- ## iterator_count() @@ -18,6 +18,11 @@ Count the elements in an iterator. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/iterator_to_array.md b/docs/php/builtins/spl/iterator_to_array.md index 40bf7bfb8a..c0e42e1e4b 100644 --- a/docs/php/builtins/spl/iterator_to_array.md +++ b/docs/php/builtins/spl/iterator_to_array.md @@ -2,7 +2,7 @@ title: "iterator_to_array()" description: "Copy the iterator into an array." sidebar: - order: 323 + order: 327 --- ## iterator_to_array() @@ -19,6 +19,11 @@ Copy the iterator into an array. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload.md b/docs/php/builtins/spl/spl_autoload.md index 7e1c44fd2b..5d1a916d74 100644 --- a/docs/php/builtins/spl/spl_autoload.md +++ b/docs/php/builtins/spl/spl_autoload.md @@ -2,7 +2,7 @@ title: "spl_autoload()" description: "Default implementation for __autoload()." sidebar: - order: 324 + order: 328 --- ## spl_autoload() @@ -19,6 +19,11 @@ Default implementation for __autoload(). **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_call.md b/docs/php/builtins/spl/spl_autoload_call.md index 7ebd9a9414..713933c9b9 100644 --- a/docs/php/builtins/spl/spl_autoload_call.md +++ b/docs/php/builtins/spl/spl_autoload_call.md @@ -2,7 +2,7 @@ title: "spl_autoload_call()" description: "Try all registered __autoload() functions to load the requested class." sidebar: - order: 325 + order: 329 --- ## spl_autoload_call() @@ -18,6 +18,11 @@ Try all registered __autoload() functions to load the requested class. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_extensions.md b/docs/php/builtins/spl/spl_autoload_extensions.md index 7d3d32a2f6..28a6ee3e12 100644 --- a/docs/php/builtins/spl/spl_autoload_extensions.md +++ b/docs/php/builtins/spl/spl_autoload_extensions.md @@ -2,7 +2,7 @@ title: "spl_autoload_extensions()" description: "Register and return default file extensions for spl_autoload." sidebar: - order: 326 + order: 330 --- ## spl_autoload_extensions() @@ -18,6 +18,11 @@ Register and return default file extensions for spl_autoload. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_functions.md b/docs/php/builtins/spl/spl_autoload_functions.md index 634305ff1f..67192fcf18 100644 --- a/docs/php/builtins/spl/spl_autoload_functions.md +++ b/docs/php/builtins/spl/spl_autoload_functions.md @@ -2,7 +2,7 @@ title: "spl_autoload_functions()" description: "Return all registered __autoload() functions." sidebar: - order: 327 + order: 331 --- ## spl_autoload_functions() @@ -17,6 +17,11 @@ Return all registered __autoload() functions. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_register.md b/docs/php/builtins/spl/spl_autoload_register.md index 61df76125f..425aee9d66 100644 --- a/docs/php/builtins/spl/spl_autoload_register.md +++ b/docs/php/builtins/spl/spl_autoload_register.md @@ -2,7 +2,7 @@ title: "spl_autoload_register()" description: "Register given function as __autoload() implementation." sidebar: - order: 328 + order: 332 --- ## spl_autoload_register() @@ -20,6 +20,11 @@ Register given function as __autoload() implementation. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_autoload_unregister.md b/docs/php/builtins/spl/spl_autoload_unregister.md index 2db76a7b6f..e50dc26c5b 100644 --- a/docs/php/builtins/spl/spl_autoload_unregister.md +++ b/docs/php/builtins/spl/spl_autoload_unregister.md @@ -2,7 +2,7 @@ title: "spl_autoload_unregister()" description: "Unregister given function as __autoload() implementation." sidebar: - order: 329 + order: 333 --- ## spl_autoload_unregister() @@ -18,6 +18,11 @@ Unregister given function as __autoload() implementation. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_classes.md b/docs/php/builtins/spl/spl_classes.md index 177d75646a..e817f411c0 100644 --- a/docs/php/builtins/spl/spl_classes.md +++ b/docs/php/builtins/spl/spl_classes.md @@ -2,7 +2,7 @@ title: "spl_classes()" description: "Return available SPL classes." sidebar: - order: 330 + order: 334 --- ## spl_classes() @@ -17,6 +17,11 @@ Return available SPL classes. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_object_hash.md b/docs/php/builtins/spl/spl_object_hash.md index f236da8bc7..4ca666e110 100644 --- a/docs/php/builtins/spl/spl_object_hash.md +++ b/docs/php/builtins/spl/spl_object_hash.md @@ -2,7 +2,7 @@ title: "spl_object_hash()" description: "Return hash id for given object." sidebar: - order: 331 + order: 335 --- ## spl_object_hash() @@ -18,6 +18,11 @@ Return hash id for given object. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/spl/spl_object_id.md b/docs/php/builtins/spl/spl_object_id.md index 868fe18b3d..652eeb3af8 100644 --- a/docs/php/builtins/spl/spl_object_id.md +++ b/docs/php/builtins/spl/spl_object_id.md @@ -2,7 +2,7 @@ title: "spl_object_id()" description: "Return the integer object handle for given object." sidebar: - order: 332 + order: 336 --- ## spl_object_id() @@ -18,6 +18,11 @@ Return the integer object handle for given object. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams.md b/docs/php/builtins/streams.md index a2ab662d99..4bf6d7b2a6 100644 --- a/docs/php/builtins/streams.md +++ b/docs/php/builtins/streams.md @@ -7,11 +7,11 @@ sidebar: ## Streams builtins -| Function | Signature | Returns | -|---|---|---| -| [`fsockopen()`](./streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | -| [`pfsockopen()`](./streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | -| [`stream_bucket_append()`](./streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | -| [`stream_bucket_prepend()`](./streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | -| [`stream_filter_append()`](./streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | -| [`stream_filter_prepend()`](./streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`fsockopen()`](./streams/fsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`pfsockopen()`](./streams/pfsockopen.md) | `(string $hostname, int $port, int $error_code = null, string $error_message = null, float $timeout = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_bucket_append()`](./streams/stream_bucket_append.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | +| [`stream_bucket_prepend()`](./streams/stream_bucket_prepend.md) | `(mixed $brigade, mixed $bucket): void` | `void` | ✓ | ✓ | +| [`stream_filter_append()`](./streams/stream_filter_append.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | +| [`stream_filter_prepend()`](./streams/stream_filter_prepend.md) | `(resource $stream, string $filtername, int $read_write = 3, mixed $params = null): mixed` | `mixed` | ✓ | ✓ | diff --git a/docs/php/builtins/streams/fsockopen.md b/docs/php/builtins/streams/fsockopen.md index 3fef1b56db..f70cc8c754 100644 --- a/docs/php/builtins/streams/fsockopen.md +++ b/docs/php/builtins/streams/fsockopen.md @@ -2,7 +2,7 @@ title: "fsockopen()" description: "Open Internet or Unix domain socket connection." sidebar: - order: 333 + order: 337 --- ## fsockopen() @@ -22,6 +22,11 @@ Open Internet or Unix domain socket connection. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/pfsockopen.md b/docs/php/builtins/streams/pfsockopen.md index cc4ca4f3db..0df585e2e2 100644 --- a/docs/php/builtins/streams/pfsockopen.md +++ b/docs/php/builtins/streams/pfsockopen.md @@ -2,7 +2,7 @@ title: "pfsockopen()" description: "Open persistent Internet or Unix domain socket connection." sidebar: - order: 334 + order: 338 --- ## pfsockopen() @@ -22,6 +22,11 @@ Open persistent Internet or Unix domain socket connection. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/stream_bucket_append.md b/docs/php/builtins/streams/stream_bucket_append.md index 65a0eb6647..ba7b364912 100644 --- a/docs/php/builtins/streams/stream_bucket_append.md +++ b/docs/php/builtins/streams/stream_bucket_append.md @@ -2,7 +2,7 @@ title: "stream_bucket_append()" description: "Appends a bucket to the brigade." sidebar: - order: 335 + order: 339 --- ## stream_bucket_append() @@ -19,6 +19,11 @@ Appends a bucket to the brigade. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/stream_bucket_prepend.md b/docs/php/builtins/streams/stream_bucket_prepend.md index 7974675c19..ad19b9ae75 100644 --- a/docs/php/builtins/streams/stream_bucket_prepend.md +++ b/docs/php/builtins/streams/stream_bucket_prepend.md @@ -2,7 +2,7 @@ title: "stream_bucket_prepend()" description: "Prepends a bucket to the brigade." sidebar: - order: 336 + order: 340 --- ## stream_bucket_prepend() @@ -19,6 +19,11 @@ Prepends a bucket to the brigade. **Returns**: `void` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/stream_filter_append.md b/docs/php/builtins/streams/stream_filter_append.md index de3a459159..5ee98ec4c1 100644 --- a/docs/php/builtins/streams/stream_filter_append.md +++ b/docs/php/builtins/streams/stream_filter_append.md @@ -2,7 +2,7 @@ title: "stream_filter_append()" description: "Attaches a filter to a stream." sidebar: - order: 337 + order: 341 --- ## stream_filter_append() @@ -21,6 +21,11 @@ Attaches a filter to a stream. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/streams/stream_filter_prepend.md b/docs/php/builtins/streams/stream_filter_prepend.md index 891981ef58..f0b2544ad3 100644 --- a/docs/php/builtins/streams/stream_filter_prepend.md +++ b/docs/php/builtins/streams/stream_filter_prepend.md @@ -2,7 +2,7 @@ title: "stream_filter_prepend()" description: "Attaches a filter to a stream (prepend)." sidebar: - order: 338 + order: 342 --- ## stream_filter_prepend() @@ -21,6 +21,11 @@ Attaches a filter to a stream (prepend). **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string.md b/docs/php/builtins/string.md index 15d7ceb547..9f54a0e732 100644 --- a/docs/php/builtins/string.md +++ b/docs/php/builtins/string.md @@ -7,76 +7,76 @@ sidebar: ## String builtins -| Function | Signature | Returns | -|---|---|---| -| [`addslashes()`](./string/addslashes.md) | `(string $string): string` | `string` | -| [`base64_decode()`](./string/base64_decode.md) | `(string $string): string` | `string` | -| [`base64_encode()`](./string/base64_encode.md) | `(string $string): string` | `string` | -| [`bin2hex()`](./string/bin2hex.md) | `(string $string): string` | `string` | -| [`chop()`](./string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`chr()`](./string/chr.md) | `(int $codepoint): string` | `string` | -| [`crc32()`](./string/crc32.md) | `(string $string): int` | `int` | -| [`explode()`](./string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | -| [`grapheme_strrev()`](./string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | -| [`gzcompress()`](./string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | -| [`gzdeflate()`](./string/gzdeflate.md) | `(string $data, int $level = -1): string` | `string` | -| [`gzinflate()`](./string/gzinflate.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | -| [`gzuncompress()`](./string/gzuncompress.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | -| [`hash()`](./string/hash.md) | `(string $algo, string $data, bool $binary = false): string` | `string` | -| [`hash_algos()`](./string/hash_algos.md) | `(): array` | `array` | -| [`hash_copy()`](./string/hash_copy.md) | `(resource $context): mixed` | `mixed` | -| [`hash_equals()`](./string/hash_equals.md) | `(string $known_string, string $user_string): bool` | `bool` | -| [`hash_final()`](./string/hash_final.md) | `(resource $context, bool $binary = false): string` | `string` | -| [`hash_hmac()`](./string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary = false): string` | `string` | -| [`hash_init()`](./string/hash_init.md) | `(string $algo, int $flags = 0, string $key = ''): mixed` | `mixed` | -| [`hash_update()`](./string/hash_update.md) | `(resource $context, string $data): bool` | `bool` | -| [`hex2bin()`](./string/hex2bin.md) | `(string $string): string` | `string` | -| [`html_entity_decode()`](./string/html_entity_decode.md) | `(string $string): string` | `string` | -| [`htmlentities()`](./string/htmlentities.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | -| [`htmlspecialchars()`](./string/htmlspecialchars.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | -| [`implode()`](./string/implode.md) | `(string $separator, array $array = null): string` | `string` | -| [`inet_ntop()`](./string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | -| [`inet_pton()`](./string/inet_pton.md) | `(string $ip): mixed` | `mixed` | -| [`ip2long()`](./string/ip2long.md) | `(string $ip): mixed` | `mixed` | -| [`lcfirst()`](./string/lcfirst.md) | `(string $string): string` | `string` | -| [`long2ip()`](./string/long2ip.md) | `(int $ip): string` | `string` | -| [`ltrim()`](./string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`md5()`](./string/md5.md) | `(string $string, bool $binary = false): string` | `string` | -| [`nl2br()`](./string/nl2br.md) | `(string $string): string` | `string` | -| [`number_format()`](./string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | -| [`ord()`](./string/ord.md) | `(string $character): int` | `int` | -| [`printf()`](./string/printf.md) | `(string $format, ...$values): int` | `int` | -| [`rawurldecode()`](./string/rawurldecode.md) | `(string $string): string` | `string` | -| [`rawurlencode()`](./string/rawurlencode.md) | `(string $string): string` | `string` | -| [`rtrim()`](./string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`sha1()`](./string/sha1.md) | `(string $string, bool $binary = false): string` | `string` | -| [`sprintf()`](./string/sprintf.md) | `(string $format, ...$values): string` | `string` | -| [`sscanf()`](./string/sscanf.md) | `(string $string, string $format, ...$vars): array` | `array` | -| [`str_contains()`](./string/str_contains.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`str_ends_with()`](./string/str_ends_with.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`str_ireplace()`](./string/str_ireplace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | -| [`str_pad()`](./string/str_pad.md) | `(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string` | `string` | -| [`str_repeat()`](./string/str_repeat.md) | `(string $string, int $times): string` | `string` | -| [`str_replace()`](./string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | -| [`str_split()`](./string/str_split.md) | `(string $string, int $length = 1): array` | `array` | -| [`str_starts_with()`](./string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | -| [`strcasecmp()`](./string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | -| [`strcmp()`](./string/strcmp.md) | `(string $string1, string $string2): int` | `int` | -| [`stripslashes()`](./string/stripslashes.md) | `(string $string): string` | `string` | -| [`strlen()`](./string/strlen.md) | `(string $string): int` | `int` | -| [`strpos()`](./string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | -| [`strrev()`](./string/strrev.md) | `(string $string): string` | `string` | -| [`strrpos()`](./string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | -| [`strstr()`](./string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): string` | `string` | -| [`strtolower()`](./string/strtolower.md) | `(string $string): string` | `string` | -| [`strtoupper()`](./string/strtoupper.md) | `(string $string): string` | `string` | -| [`substr()`](./string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | -| [`substr_replace()`](./string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | -| [`trim()`](./string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | -| [`ucfirst()`](./string/ucfirst.md) | `(string $string): string` | `string` | -| [`ucwords()`](./string/ucwords.md) | `(string $string, string $separators = ' \t\r\n\x0c\x0b'): string` | `string` | -| [`urldecode()`](./string/urldecode.md) | `(string $string): string` | `string` | -| [`urlencode()`](./string/urlencode.md) | `(string $string): string` | `string` | -| [`vprintf()`](./string/vprintf.md) | `(string $format, array $values): int` | `int` | -| [`vsprintf()`](./string/vsprintf.md) | `(string $format, array $values): string` | `string` | -| [`wordwrap()`](./string/wordwrap.md) | `(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string` | `string` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`addslashes()`](./string/addslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_decode()`](./string/base64_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`base64_encode()`](./string/base64_encode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`bin2hex()`](./string/bin2hex.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`chop()`](./string/chop.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`chr()`](./string/chr.md) | `(int $codepoint): string` | `string` | ✓ | ✓ | +| [`crc32()`](./string/crc32.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`explode()`](./string/explode.md) | `(string $separator, string $string, int $limit = PHP_INT_MAX): array` | `array` | ✓ | ✓ | +| [`grapheme_strrev()`](./string/grapheme_strrev.md) | `(string $string): mixed` | `mixed` | ✓ | ✓ | +| [`gzcompress()`](./string/gzcompress.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | +| [`gzdeflate()`](./string/gzdeflate.md) | `(string $data, int $level = -1): string` | `string` | ✓ | ✓ | +| [`gzinflate()`](./string/gzinflate.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | ✓ | ✓ | +| [`gzuncompress()`](./string/gzuncompress.md) | `(string $data, int $max_length = 0): mixed` | `mixed` | ✓ | ✓ | +| [`hash()`](./string/hash.md) | `(string $algo, string $data, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_algos()`](./string/hash_algos.md) | `(): array` | `array` | ✓ | ✓ | +| [`hash_copy()`](./string/hash_copy.md) | `(resource $context): mixed` | `mixed` | ✓ | ✓ | +| [`hash_equals()`](./string/hash_equals.md) | `(string $known_string, string $user_string): bool` | `bool` | ✓ | ✓ | +| [`hash_final()`](./string/hash_final.md) | `(resource $context, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_hmac()`](./string/hash_hmac.md) | `(string $algo, string $data, string $key, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`hash_init()`](./string/hash_init.md) | `(string $algo, int $flags = 0, string $key = ''): mixed` | `mixed` | ✓ | ✓ | +| [`hash_update()`](./string/hash_update.md) | `(resource $context, string $data): bool` | `bool` | ✓ | ✓ | +| [`hex2bin()`](./string/hex2bin.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`html_entity_decode()`](./string/html_entity_decode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`htmlentities()`](./string/htmlentities.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | ✓ | ✓ | +| [`htmlspecialchars()`](./string/htmlspecialchars.md) | `(string $string, int $flags = 11, string $encoding = 'UTF-8'): string` | `string` | ✓ | ✓ | +| [`implode()`](./string/implode.md) | `(string $separator, array $array = null): string` | `string` | ✓ | ✓ | +| [`inet_ntop()`](./string/inet_ntop.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`inet_pton()`](./string/inet_pton.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`ip2long()`](./string/ip2long.md) | `(string $ip): mixed` | `mixed` | ✓ | ✓ | +| [`lcfirst()`](./string/lcfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`long2ip()`](./string/long2ip.md) | `(int $ip): string` | `string` | ✓ | ✓ | +| [`ltrim()`](./string/ltrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`md5()`](./string/md5.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`nl2br()`](./string/nl2br.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`number_format()`](./string/number_format.md) | `(float $num, int $decimals = 0, string $decimal_separator = '.', string $thousands_separator = ','): string` | `string` | ✓ | ✓ | +| [`ord()`](./string/ord.md) | `(string $character): int` | `int` | ✓ | ✓ | +| [`printf()`](./string/printf.md) | `(string $format, ...$values): int` | `int` | ✓ | ✓ | +| [`rawurldecode()`](./string/rawurldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`rawurlencode()`](./string/rawurlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`rtrim()`](./string/rtrim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`sha1()`](./string/sha1.md) | `(string $string, bool $binary = false): string` | `string` | ✓ | ✓ | +| [`sprintf()`](./string/sprintf.md) | `(string $format, ...$values): string` | `string` | ✓ | ✓ | +| [`sscanf()`](./string/sscanf.md) | `(string $string, string $format, ...$vars): array` | `array` | ✓ | ✓ | +| [`str_contains()`](./string/str_contains.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_ends_with()`](./string/str_ends_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`str_ireplace()`](./string/str_ireplace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | +| [`str_pad()`](./string/str_pad.md) | `(string $string, int $length, string $pad_string = ' ', int $pad_type = 1): string` | `string` | ✓ | ✓ | +| [`str_repeat()`](./string/str_repeat.md) | `(string $string, int $times): string` | `string` | ✓ | ✓ | +| [`str_replace()`](./string/str_replace.md) | `(string $search, string $replace, string $subject, int $count = null): string` | `string` | ✓ | ✓ | +| [`str_split()`](./string/str_split.md) | `(string $string, int $length = 1): array` | `array` | ✓ | ✓ | +| [`str_starts_with()`](./string/str_starts_with.md) | `(string $haystack, string $needle): bool` | `bool` | ✓ | ✓ | +| [`strcasecmp()`](./string/strcasecmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`strcmp()`](./string/strcmp.md) | `(string $string1, string $string2): int` | `int` | ✓ | ✓ | +| [`stripslashes()`](./string/stripslashes.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strlen()`](./string/strlen.md) | `(string $string): int` | `int` | ✓ | ✓ | +| [`strpos()`](./string/strpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | +| [`strrev()`](./string/strrev.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strrpos()`](./string/strrpos.md) | `(string $haystack, string $needle, int $offset = 0): mixed` | `mixed` | ✓ | ✓ | +| [`strstr()`](./string/strstr.md) | `(string $haystack, string $needle, bool $before_needle = false): string` | `string` | ✓ | ✓ | +| [`strtolower()`](./string/strtolower.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`strtoupper()`](./string/strtoupper.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`substr()`](./string/substr.md) | `(string $string, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`substr_replace()`](./string/substr_replace.md) | `(string $string, string $replace, int $offset, int $length = null): string` | `string` | ✓ | ✓ | +| [`trim()`](./string/trim.md) | `(string $string, string $characters = ' \n\r\t\x0b\x0c\x00'): string` | `string` | ✓ | ✓ | +| [`ucfirst()`](./string/ucfirst.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`ucwords()`](./string/ucwords.md) | `(string $string, string $separators = ' \t\r\n\x0c\x0b'): string` | `string` | ✓ | ✓ | +| [`urldecode()`](./string/urldecode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`urlencode()`](./string/urlencode.md) | `(string $string): string` | `string` | ✓ | ✓ | +| [`vprintf()`](./string/vprintf.md) | `(string $format, array $values): int` | `int` | ✓ | ✓ | +| [`vsprintf()`](./string/vsprintf.md) | `(string $format, array $values): string` | `string` | ✓ | ✓ | +| [`wordwrap()`](./string/wordwrap.md) | `(string $string, int $width = 75, string $break = '\n', bool $cut_long_words = false): string` | `string` | ✓ | ✓ | diff --git a/docs/php/builtins/string/addslashes.md b/docs/php/builtins/string/addslashes.md index 04191523b7..6546d1ec1e 100644 --- a/docs/php/builtins/string/addslashes.md +++ b/docs/php/builtins/string/addslashes.md @@ -2,7 +2,7 @@ title: "addslashes()" description: "Adds backslashes before characters that need to be escaped." sidebar: - order: 339 + order: 343 --- ## addslashes() @@ -18,6 +18,11 @@ Adds backslashes before characters that need to be escaped. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/base64_decode.md b/docs/php/builtins/string/base64_decode.md index 6349f09018..1a0c846992 100644 --- a/docs/php/builtins/string/base64_decode.md +++ b/docs/php/builtins/string/base64_decode.md @@ -2,7 +2,7 @@ title: "base64_decode()" description: "Decodes a Base64-encoded string back into its original data." sidebar: - order: 340 + order: 344 --- ## base64_decode() @@ -18,6 +18,11 @@ Decodes a Base64-encoded string back into its original data. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/base64_encode.md b/docs/php/builtins/string/base64_encode.md index c9305f8bd4..437423d72d 100644 --- a/docs/php/builtins/string/base64_encode.md +++ b/docs/php/builtins/string/base64_encode.md @@ -2,7 +2,7 @@ title: "base64_encode()" description: "Encodes binary data into a Base64 string." sidebar: - order: 341 + order: 345 --- ## base64_encode() @@ -18,6 +18,11 @@ Encodes binary data into a Base64 string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/bin2hex.md b/docs/php/builtins/string/bin2hex.md index 70bbb66f79..2fa6ceeca1 100644 --- a/docs/php/builtins/string/bin2hex.md +++ b/docs/php/builtins/string/bin2hex.md @@ -2,7 +2,7 @@ title: "bin2hex()" description: "Converts binary data into its hexadecimal string representation." sidebar: - order: 342 + order: 346 --- ## bin2hex() @@ -18,6 +18,11 @@ Converts binary data into its hexadecimal string representation. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/chop.md b/docs/php/builtins/string/chop.md index a5f81e8e05..cf3cbd551d 100644 --- a/docs/php/builtins/string/chop.md +++ b/docs/php/builtins/string/chop.md @@ -2,7 +2,7 @@ title: "chop()" description: "Alias of rtrim: strips whitespace (or other characters) from the end of a string." sidebar: - order: 343 + order: 347 --- ## chop() @@ -19,6 +19,11 @@ Alias of rtrim: strips whitespace (or other characters) from the end of a string **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/chop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chop.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/chr.md b/docs/php/builtins/string/chr.md index 2a49122231..2dcff0cd7a 100644 --- a/docs/php/builtins/string/chr.md +++ b/docs/php/builtins/string/chr.md @@ -2,7 +2,7 @@ title: "chr()" description: "Returns a one-character string from the given byte code point." sidebar: - order: 344 + order: 348 --- ## chr() @@ -18,6 +18,11 @@ Returns a one-character string from the given byte code point. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/chr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/chr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/crc32.md b/docs/php/builtins/string/crc32.md index 22219e2145..7a657e9ddc 100644 --- a/docs/php/builtins/string/crc32.md +++ b/docs/php/builtins/string/crc32.md @@ -2,7 +2,7 @@ title: "crc32()" description: "Calculates the CRC32 polynomial of a string." sidebar: - order: 345 + order: 349 --- ## crc32() @@ -18,6 +18,11 @@ Calculates the CRC32 polynomial of a string. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/crc32.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/crc32.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/explode.md b/docs/php/builtins/string/explode.md index 0e09835b20..922676c675 100644 --- a/docs/php/builtins/string/explode.md +++ b/docs/php/builtins/string/explode.md @@ -2,7 +2,7 @@ title: "explode()" description: "Splits a string by a separator into an array of substrings." sidebar: - order: 346 + order: 350 --- ## explode() @@ -20,6 +20,11 @@ Splits a string by a separator into an array of substrings. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/explode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/explode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/grapheme_strrev.md b/docs/php/builtins/string/grapheme_strrev.md index 9964a742d0..707d554932 100644 --- a/docs/php/builtins/string/grapheme_strrev.md +++ b/docs/php/builtins/string/grapheme_strrev.md @@ -2,7 +2,7 @@ title: "grapheme_strrev()" description: "Reverses a string by grapheme cluster, returning false on failure." sidebar: - order: 347 + order: 351 --- ## grapheme_strrev() @@ -18,6 +18,11 @@ Reverses a string by grapheme cluster, returning false on failure. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/gzcompress.md b/docs/php/builtins/string/gzcompress.md index 56438dda09..e9ba96537a 100644 --- a/docs/php/builtins/string/gzcompress.md +++ b/docs/php/builtins/string/gzcompress.md @@ -2,7 +2,7 @@ title: "gzcompress()" description: "Compress a string using the ZLIB data format." sidebar: - order: 348 + order: 352 --- ## gzcompress() @@ -19,6 +19,11 @@ Compress a string using the ZLIB data format. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/gzdeflate.md b/docs/php/builtins/string/gzdeflate.md index 65a4d21349..6aacc75a90 100644 --- a/docs/php/builtins/string/gzdeflate.md +++ b/docs/php/builtins/string/gzdeflate.md @@ -2,7 +2,7 @@ title: "gzdeflate()" description: "Deflate a string using the DEFLATE data format." sidebar: - order: 349 + order: 353 --- ## gzdeflate() @@ -19,6 +19,11 @@ Deflate a string using the DEFLATE data format. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/gzinflate.md b/docs/php/builtins/string/gzinflate.md index 8de121202a..599d2ab523 100644 --- a/docs/php/builtins/string/gzinflate.md +++ b/docs/php/builtins/string/gzinflate.md @@ -2,7 +2,7 @@ title: "gzinflate()" description: "Inflate a deflated string." sidebar: - order: 350 + order: 354 --- ## gzinflate() @@ -19,6 +19,11 @@ Inflate a deflated string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/gzuncompress.md b/docs/php/builtins/string/gzuncompress.md index 9643a7d864..58482dc2c3 100644 --- a/docs/php/builtins/string/gzuncompress.md +++ b/docs/php/builtins/string/gzuncompress.md @@ -2,7 +2,7 @@ title: "gzuncompress()" description: "Uncompress a compressed string." sidebar: - order: 351 + order: 355 --- ## gzuncompress() @@ -19,6 +19,11 @@ Uncompress a compressed string. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash.md b/docs/php/builtins/string/hash.md index 66507e2540..adf4a3aad0 100644 --- a/docs/php/builtins/string/hash.md +++ b/docs/php/builtins/string/hash.md @@ -2,7 +2,7 @@ title: "hash()" description: "Generates a hash value using the given algorithm." sidebar: - order: 352 + order: 356 --- ## hash() @@ -20,6 +20,11 @@ Generates a hash value using the given algorithm. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_algos.md b/docs/php/builtins/string/hash_algos.md index c125ab2d99..eb305c68c1 100644 --- a/docs/php/builtins/string/hash_algos.md +++ b/docs/php/builtins/string/hash_algos.md @@ -2,7 +2,7 @@ title: "hash_algos()" description: "Returns an array of supported hashing algorithm names." sidebar: - order: 353 + order: 357 --- ## hash_algos() @@ -17,6 +17,11 @@ Returns an array of supported hashing algorithm names. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_copy.md b/docs/php/builtins/string/hash_copy.md index 77bb7266d5..a4162d7380 100644 --- a/docs/php/builtins/string/hash_copy.md +++ b/docs/php/builtins/string/hash_copy.md @@ -2,7 +2,7 @@ title: "hash_copy()" description: "Copies the state of an incremental hashing context." sidebar: - order: 354 + order: 358 --- ## hash_copy() @@ -18,6 +18,11 @@ Copies the state of an incremental hashing context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_equals.md b/docs/php/builtins/string/hash_equals.md index 9ab6e0b6a9..4a26b9b313 100644 --- a/docs/php/builtins/string/hash_equals.md +++ b/docs/php/builtins/string/hash_equals.md @@ -2,7 +2,7 @@ title: "hash_equals()" description: "Compares two strings using a constant-time algorithm." sidebar: - order: 355 + order: 359 --- ## hash_equals() @@ -19,6 +19,11 @@ Compares two strings using a constant-time algorithm. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_final.md b/docs/php/builtins/string/hash_final.md index f3551f6983..a843884520 100644 --- a/docs/php/builtins/string/hash_final.md +++ b/docs/php/builtins/string/hash_final.md @@ -2,7 +2,7 @@ title: "hash_final()" description: "Finalizes an incremental hash and returns the digest string." sidebar: - order: 356 + order: 360 --- ## hash_final() @@ -19,6 +19,11 @@ Finalizes an incremental hash and returns the digest string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_hmac.md b/docs/php/builtins/string/hash_hmac.md index 10006bc301..d96486a98e 100644 --- a/docs/php/builtins/string/hash_hmac.md +++ b/docs/php/builtins/string/hash_hmac.md @@ -2,7 +2,7 @@ title: "hash_hmac()" description: "Generates a keyed hash value using the HMAC method." sidebar: - order: 357 + order: 361 --- ## hash_hmac() @@ -21,6 +21,11 @@ Generates a keyed hash value using the HMAC method. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_init.md b/docs/php/builtins/string/hash_init.md index 79b66685b6..1d5bfde0a9 100644 --- a/docs/php/builtins/string/hash_init.md +++ b/docs/php/builtins/string/hash_init.md @@ -2,7 +2,7 @@ title: "hash_init()" description: "Initialize an incremental hashing context." sidebar: - order: 358 + order: 362 --- ## hash_init() @@ -20,6 +20,11 @@ Initialize an incremental hashing context. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hash_update.md b/docs/php/builtins/string/hash_update.md index f069e597be..16b02a2144 100644 --- a/docs/php/builtins/string/hash_update.md +++ b/docs/php/builtins/string/hash_update.md @@ -2,7 +2,7 @@ title: "hash_update()" description: "Pumps data into an active incremental hashing context." sidebar: - order: 359 + order: 363 --- ## hash_update() @@ -19,6 +19,11 @@ Pumps data into an active incremental hashing context. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/hex2bin.md b/docs/php/builtins/string/hex2bin.md index 39cb3881cb..d5510ff8e2 100644 --- a/docs/php/builtins/string/hex2bin.md +++ b/docs/php/builtins/string/hex2bin.md @@ -2,7 +2,7 @@ title: "hex2bin()" description: "Decodes a hexadecimal string back into its binary representation." sidebar: - order: 360 + order: 364 --- ## hex2bin() @@ -18,6 +18,11 @@ Decodes a hexadecimal string back into its binary representation. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/html_entity_decode.md b/docs/php/builtins/string/html_entity_decode.md index 3a35b9080d..75b76d0d68 100644 --- a/docs/php/builtins/string/html_entity_decode.md +++ b/docs/php/builtins/string/html_entity_decode.md @@ -2,7 +2,7 @@ title: "html_entity_decode()" description: "Converts HTML entities in a string back into their corresponding characters." sidebar: - order: 361 + order: 365 --- ## html_entity_decode() @@ -18,6 +18,11 @@ Converts HTML entities in a string back into their corresponding characters. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/htmlentities.md b/docs/php/builtins/string/htmlentities.md index 69b4d0b5b0..a3ef3f5b16 100644 --- a/docs/php/builtins/string/htmlentities.md +++ b/docs/php/builtins/string/htmlentities.md @@ -2,7 +2,7 @@ title: "htmlentities()" description: "Converts all applicable characters in a string into their HTML entities." sidebar: - order: 362 + order: 366 --- ## htmlentities() @@ -20,6 +20,11 @@ Converts all applicable characters in a string into their HTML entities. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/htmlspecialchars.md b/docs/php/builtins/string/htmlspecialchars.md index 32b107fa75..874fb33cd9 100644 --- a/docs/php/builtins/string/htmlspecialchars.md +++ b/docs/php/builtins/string/htmlspecialchars.md @@ -2,7 +2,7 @@ title: "htmlspecialchars()" description: "Converts the HTML special characters in a string into their entities." sidebar: - order: 363 + order: 367 --- ## htmlspecialchars() @@ -20,6 +20,11 @@ Converts the HTML special characters in a string into their entities. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/implode.md b/docs/php/builtins/string/implode.md index d243faa749..d234c66765 100644 --- a/docs/php/builtins/string/implode.md +++ b/docs/php/builtins/string/implode.md @@ -2,7 +2,7 @@ title: "implode()" description: "Joins array elements into a single string using a separator." sidebar: - order: 364 + order: 368 --- ## implode() @@ -19,6 +19,11 @@ Joins array elements into a single string using a separator. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/implode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/implode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/inet_ntop.md b/docs/php/builtins/string/inet_ntop.md index 1c2a971a06..614b66d70f 100644 --- a/docs/php/builtins/string/inet_ntop.md +++ b/docs/php/builtins/string/inet_ntop.md @@ -2,7 +2,7 @@ title: "inet_ntop()" description: "Converts a packed internet address to a human-readable representation." sidebar: - order: 365 + order: 369 --- ## inet_ntop() @@ -18,6 +18,11 @@ Converts a packed internet address to a human-readable representation. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/inet_pton.md b/docs/php/builtins/string/inet_pton.md index 84d7b7e2a7..add9ca9617 100644 --- a/docs/php/builtins/string/inet_pton.md +++ b/docs/php/builtins/string/inet_pton.md @@ -2,7 +2,7 @@ title: "inet_pton()" description: "Converts a human-readable IP address to its packed in_addr representation." sidebar: - order: 366 + order: 370 --- ## inet_pton() @@ -18,6 +18,11 @@ Converts a human-readable IP address to its packed in_addr representation. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ip2long.md b/docs/php/builtins/string/ip2long.md index a7e935b880..33f40216d5 100644 --- a/docs/php/builtins/string/ip2long.md +++ b/docs/php/builtins/string/ip2long.md @@ -2,7 +2,7 @@ title: "ip2long()" description: "Converts a string containing an IPv4 address into a long integer." sidebar: - order: 367 + order: 371 --- ## ip2long() @@ -18,6 +18,11 @@ Converts a string containing an IPv4 address into a long integer. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/lcfirst.md b/docs/php/builtins/string/lcfirst.md index f52a1f4848..7281b99b3b 100644 --- a/docs/php/builtins/string/lcfirst.md +++ b/docs/php/builtins/string/lcfirst.md @@ -2,7 +2,7 @@ title: "lcfirst()" description: "Lowercases the first character of a string." sidebar: - order: 368 + order: 372 --- ## lcfirst() @@ -18,6 +18,11 @@ Lowercases the first character of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/long2ip.md b/docs/php/builtins/string/long2ip.md index 264925d6c4..ff5fa8ece3 100644 --- a/docs/php/builtins/string/long2ip.md +++ b/docs/php/builtins/string/long2ip.md @@ -2,7 +2,7 @@ title: "long2ip()" description: "Converts an IPv4 address from long integer to dotted string notation." sidebar: - order: 369 + order: 373 --- ## long2ip() @@ -18,6 +18,11 @@ Converts an IPv4 address from long integer to dotted string notation. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ltrim.md b/docs/php/builtins/string/ltrim.md index 77f600bd7a..d09ed8a96f 100644 --- a/docs/php/builtins/string/ltrim.md +++ b/docs/php/builtins/string/ltrim.md @@ -2,7 +2,7 @@ title: "ltrim()" description: "Strips whitespace (or other characters) from the beginning of a string." sidebar: - order: 370 + order: 374 --- ## ltrim() @@ -19,6 +19,11 @@ Strips whitespace (or other characters) from the beginning of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/md5.md b/docs/php/builtins/string/md5.md index fab1475d71..78afd7989c 100644 --- a/docs/php/builtins/string/md5.md +++ b/docs/php/builtins/string/md5.md @@ -2,7 +2,7 @@ title: "md5()" description: "Calculates the MD5 hash of a string." sidebar: - order: 371 + order: 375 --- ## md5() @@ -19,6 +19,11 @@ Calculates the MD5 hash of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/md5.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/md5.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/nl2br.md b/docs/php/builtins/string/nl2br.md index 525ed09f13..d42522a437 100644 --- a/docs/php/builtins/string/nl2br.md +++ b/docs/php/builtins/string/nl2br.md @@ -2,7 +2,7 @@ title: "nl2br()" description: "Inserts HTML line breaks before newlines in a string." sidebar: - order: 372 + order: 376 --- ## nl2br() @@ -18,6 +18,11 @@ Inserts HTML line breaks before newlines in a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/number_format.md b/docs/php/builtins/string/number_format.md index 19e5c3eed3..e662c0d367 100644 --- a/docs/php/builtins/string/number_format.md +++ b/docs/php/builtins/string/number_format.md @@ -2,7 +2,7 @@ title: "number_format()" description: "Formats a number with grouped thousands." sidebar: - order: 373 + order: 377 --- ## number_format() @@ -21,6 +21,11 @@ Formats a number with grouped thousands. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ord.md b/docs/php/builtins/string/ord.md index a645846562..32e73ff2ac 100644 --- a/docs/php/builtins/string/ord.md +++ b/docs/php/builtins/string/ord.md @@ -2,7 +2,7 @@ title: "ord()" description: "Returns the ASCII value of the first character of a string." sidebar: - order: 374 + order: 378 --- ## ord() @@ -18,6 +18,11 @@ Returns the ASCII value of the first character of a string. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ord.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ord.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/printf.md b/docs/php/builtins/string/printf.md index 156332b12c..c0cf42b38d 100644 --- a/docs/php/builtins/string/printf.md +++ b/docs/php/builtins/string/printf.md @@ -2,7 +2,7 @@ title: "printf()" description: "Outputs a formatted string." sidebar: - order: 375 + order: 379 --- ## printf() @@ -19,6 +19,11 @@ Outputs a formatted string. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/rawurldecode.md b/docs/php/builtins/string/rawurldecode.md index 4202a3459b..6b283f1d50 100644 --- a/docs/php/builtins/string/rawurldecode.md +++ b/docs/php/builtins/string/rawurldecode.md @@ -2,7 +2,7 @@ title: "rawurldecode()" description: "Decodes an RFC 3986 percent-encoded string without treating '+' as a space." sidebar: - order: 376 + order: 380 --- ## rawurldecode() @@ -18,6 +18,11 @@ Decodes an RFC 3986 percent-encoded string without treating '+' as a space. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/rawurlencode.md b/docs/php/builtins/string/rawurlencode.md index 52f27ea5cb..ba0bbe5bb9 100644 --- a/docs/php/builtins/string/rawurlencode.md +++ b/docs/php/builtins/string/rawurlencode.md @@ -2,7 +2,7 @@ title: "rawurlencode()" description: "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces)." sidebar: - order: 377 + order: 381 --- ## rawurlencode() @@ -18,6 +18,11 @@ URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces). **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/rtrim.md b/docs/php/builtins/string/rtrim.md index e8cc673ded..028e86cda6 100644 --- a/docs/php/builtins/string/rtrim.md +++ b/docs/php/builtins/string/rtrim.md @@ -2,7 +2,7 @@ title: "rtrim()" description: "Strips whitespace (or other characters) from the end of a string." sidebar: - order: 378 + order: 382 --- ## rtrim() @@ -19,6 +19,11 @@ Strips whitespace (or other characters) from the end of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/sha1.md b/docs/php/builtins/string/sha1.md index aa07710d1d..5a374b0e21 100644 --- a/docs/php/builtins/string/sha1.md +++ b/docs/php/builtins/string/sha1.md @@ -2,7 +2,7 @@ title: "sha1()" description: "Calculates the SHA-1 hash of a string." sidebar: - order: 379 + order: 383 --- ## sha1() @@ -19,6 +19,11 @@ Calculates the SHA-1 hash of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/sha1.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/sha1.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/sprintf.md b/docs/php/builtins/string/sprintf.md index d4f6da2cf2..25eb470c99 100644 --- a/docs/php/builtins/string/sprintf.md +++ b/docs/php/builtins/string/sprintf.md @@ -2,7 +2,7 @@ title: "sprintf()" description: "Returns a formatted string." sidebar: - order: 380 + order: 384 --- ## sprintf() @@ -19,6 +19,11 @@ Returns a formatted string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/sscanf.md b/docs/php/builtins/string/sscanf.md index 7f3206257d..785fb4b3c7 100644 --- a/docs/php/builtins/string/sscanf.md +++ b/docs/php/builtins/string/sscanf.md @@ -2,7 +2,7 @@ title: "sscanf()" description: "Parses a string according to a format." sidebar: - order: 381 + order: 385 --- ## sscanf() @@ -20,6 +20,11 @@ Parses a string according to a format. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_contains.md b/docs/php/builtins/string/str_contains.md index 7f64d495eb..de8fd5a766 100644 --- a/docs/php/builtins/string/str_contains.md +++ b/docs/php/builtins/string/str_contains.md @@ -2,7 +2,7 @@ title: "str_contains()" description: "Determines if a string contains a given substring." sidebar: - order: 382 + order: 386 --- ## str_contains() @@ -19,6 +19,11 @@ Determines if a string contains a given substring. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_ends_with.md b/docs/php/builtins/string/str_ends_with.md index 7a05ef2b75..164376bb04 100644 --- a/docs/php/builtins/string/str_ends_with.md +++ b/docs/php/builtins/string/str_ends_with.md @@ -2,7 +2,7 @@ title: "str_ends_with()" description: "Checks if a string ends with a given substring." sidebar: - order: 383 + order: 387 --- ## str_ends_with() @@ -19,6 +19,11 @@ Checks if a string ends with a given substring. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_ireplace.md b/docs/php/builtins/string/str_ireplace.md index 75706424ad..e9bba3b29c 100644 --- a/docs/php/builtins/string/str_ireplace.md +++ b/docs/php/builtins/string/str_ireplace.md @@ -2,7 +2,7 @@ title: "str_ireplace()" description: "Case-insensitive version of str_replace()." sidebar: - order: 384 + order: 388 --- ## str_ireplace() @@ -21,6 +21,11 @@ Case-insensitive version of str_replace(). **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_pad.md b/docs/php/builtins/string/str_pad.md index 5abc77c431..49d4be8770 100644 --- a/docs/php/builtins/string/str_pad.md +++ b/docs/php/builtins/string/str_pad.md @@ -2,7 +2,7 @@ title: "str_pad()" description: "Pads a string to a certain length with another string." sidebar: - order: 385 + order: 389 --- ## str_pad() @@ -21,6 +21,11 @@ Pads a string to a certain length with another string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_repeat.md b/docs/php/builtins/string/str_repeat.md index a3f9a4c0d9..a8ce358b4c 100644 --- a/docs/php/builtins/string/str_repeat.md +++ b/docs/php/builtins/string/str_repeat.md @@ -2,7 +2,7 @@ title: "str_repeat()" description: "Repeats a string a given number of times." sidebar: - order: 386 + order: 390 --- ## str_repeat() @@ -19,6 +19,11 @@ Repeats a string a given number of times. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_replace.md b/docs/php/builtins/string/str_replace.md index 6d21c42e23..d018238fd8 100644 --- a/docs/php/builtins/string/str_replace.md +++ b/docs/php/builtins/string/str_replace.md @@ -2,7 +2,7 @@ title: "str_replace()" description: "Replaces all occurrences of a search string with a replacement string." sidebar: - order: 387 + order: 391 --- ## str_replace() @@ -21,6 +21,11 @@ Replaces all occurrences of a search string with a replacement string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_split.md b/docs/php/builtins/string/str_split.md index 9e121f6f6d..3d6d9432f5 100644 --- a/docs/php/builtins/string/str_split.md +++ b/docs/php/builtins/string/str_split.md @@ -2,7 +2,7 @@ title: "str_split()" description: "Converts a string into an array of chunks of the given length." sidebar: - order: 388 + order: 392 --- ## str_split() @@ -19,6 +19,11 @@ Converts a string into an array of chunks of the given length. **Returns**: `array` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_split.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_split.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/str_starts_with.md b/docs/php/builtins/string/str_starts_with.md index 9b653fa120..7e4c58141a 100644 --- a/docs/php/builtins/string/str_starts_with.md +++ b/docs/php/builtins/string/str_starts_with.md @@ -2,7 +2,7 @@ title: "str_starts_with()" description: "Checks if a string starts with a given substring." sidebar: - order: 389 + order: 393 --- ## str_starts_with() @@ -19,6 +19,11 @@ Checks if a string starts with a given substring. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strcasecmp.md b/docs/php/builtins/string/strcasecmp.md index fbfeeaa353..8cf921556e 100644 --- a/docs/php/builtins/string/strcasecmp.md +++ b/docs/php/builtins/string/strcasecmp.md @@ -2,7 +2,7 @@ title: "strcasecmp()" description: "Binary safe case-insensitive string comparison. Returns negative, zero, or positive." sidebar: - order: 390 + order: 394 --- ## strcasecmp() @@ -19,6 +19,11 @@ Binary safe case-insensitive string comparison. Returns negative, zero, or posit **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strcmp.md b/docs/php/builtins/string/strcmp.md index 279f8625bb..04ba651fe6 100644 --- a/docs/php/builtins/string/strcmp.md +++ b/docs/php/builtins/string/strcmp.md @@ -2,7 +2,7 @@ title: "strcmp()" description: "Binary safe string comparison. Returns negative, zero, or positive." sidebar: - order: 391 + order: 395 --- ## strcmp() @@ -19,6 +19,11 @@ Binary safe string comparison. Returns negative, zero, or positive. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/stripslashes.md b/docs/php/builtins/string/stripslashes.md index 2a22f56a34..e75dee9b8a 100644 --- a/docs/php/builtins/string/stripslashes.md +++ b/docs/php/builtins/string/stripslashes.md @@ -2,7 +2,7 @@ title: "stripslashes()" description: "Removes backslashes from a string previously escaped by addslashes." sidebar: - order: 392 + order: 396 --- ## stripslashes() @@ -18,6 +18,11 @@ Removes backslashes from a string previously escaped by addslashes. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strlen.md b/docs/php/builtins/string/strlen.md index 27592d4259..2447dc287c 100644 --- a/docs/php/builtins/string/strlen.md +++ b/docs/php/builtins/string/strlen.md @@ -2,7 +2,7 @@ title: "strlen()" description: "Returns the length of a string." sidebar: - order: 393 + order: 397 --- ## strlen() @@ -18,6 +18,11 @@ Returns the length of a string. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strlen.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strlen.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strpos.md b/docs/php/builtins/string/strpos.md index 6c5e3ef75b..8d25bdb870 100644 --- a/docs/php/builtins/string/strpos.md +++ b/docs/php/builtins/string/strpos.md @@ -2,7 +2,7 @@ title: "strpos()" description: "Finds the numeric position of the first occurrence of a substring." sidebar: - order: 394 + order: 398 --- ## strpos() @@ -20,6 +20,11 @@ Finds the numeric position of the first occurrence of a substring. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strpos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strpos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strrev.md b/docs/php/builtins/string/strrev.md index d867d1be94..cc53004b0a 100644 --- a/docs/php/builtins/string/strrev.md +++ b/docs/php/builtins/string/strrev.md @@ -2,7 +2,7 @@ title: "strrev()" description: "Reverses a string." sidebar: - order: 395 + order: 399 --- ## strrev() @@ -18,6 +18,11 @@ Reverses a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strrev.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strrev.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strrpos.md b/docs/php/builtins/string/strrpos.md index 06cca6fde3..a3e1d06704 100644 --- a/docs/php/builtins/string/strrpos.md +++ b/docs/php/builtins/string/strrpos.md @@ -2,7 +2,7 @@ title: "strrpos()" description: "Finds the numeric position of the last occurrence of a substring." sidebar: - order: 396 + order: 400 --- ## strrpos() @@ -20,6 +20,11 @@ Finds the numeric position of the last occurrence of a substring. **Returns**: `mixed` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strstr.md b/docs/php/builtins/string/strstr.md index 94ff9bc781..02a3374550 100644 --- a/docs/php/builtins/string/strstr.md +++ b/docs/php/builtins/string/strstr.md @@ -2,7 +2,7 @@ title: "strstr()" description: "Returns the portion of a string starting at the first occurrence of a substring." sidebar: - order: 397 + order: 401 --- ## strstr() @@ -20,6 +20,11 @@ Returns the portion of a string starting at the first occurrence of a substring. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strstr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strstr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strtolower.md b/docs/php/builtins/string/strtolower.md index 0f3edc503b..2a10ffbd44 100644 --- a/docs/php/builtins/string/strtolower.md +++ b/docs/php/builtins/string/strtolower.md @@ -2,7 +2,7 @@ title: "strtolower()" description: "Converts a string to lowercase." sidebar: - order: 398 + order: 402 --- ## strtolower() @@ -18,6 +18,11 @@ Converts a string to lowercase. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/strtoupper.md b/docs/php/builtins/string/strtoupper.md index e61f7b42c1..931bdc74d2 100644 --- a/docs/php/builtins/string/strtoupper.md +++ b/docs/php/builtins/string/strtoupper.md @@ -2,7 +2,7 @@ title: "strtoupper()" description: "Converts a string to uppercase." sidebar: - order: 399 + order: 403 --- ## strtoupper() @@ -18,6 +18,11 @@ Converts a string to uppercase. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/substr.md b/docs/php/builtins/string/substr.md index e85c4bcf62..f59589b1fb 100644 --- a/docs/php/builtins/string/substr.md +++ b/docs/php/builtins/string/substr.md @@ -2,7 +2,7 @@ title: "substr()" description: "Returns a portion of a string specified by the offset and length." sidebar: - order: 400 + order: 404 --- ## substr() @@ -20,6 +20,11 @@ Returns a portion of a string specified by the offset and length. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/substr.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/substr.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/substr_replace.md b/docs/php/builtins/string/substr_replace.md index 687dde47e5..93fbe68cc9 100644 --- a/docs/php/builtins/string/substr_replace.md +++ b/docs/php/builtins/string/substr_replace.md @@ -2,7 +2,7 @@ title: "substr_replace()" description: "Replaces text within a portion of a string." sidebar: - order: 401 + order: 405 --- ## substr_replace() @@ -21,6 +21,11 @@ Replaces text within a portion of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/trim.md b/docs/php/builtins/string/trim.md index 6466fbe3e8..8a7b53e39a 100644 --- a/docs/php/builtins/string/trim.md +++ b/docs/php/builtins/string/trim.md @@ -2,7 +2,7 @@ title: "trim()" description: "Strips whitespace (or other characters) from the beginning and end of a string." sidebar: - order: 402 + order: 406 --- ## trim() @@ -19,6 +19,11 @@ Strips whitespace (or other characters) from the beginning and end of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/trim.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/trim.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ucfirst.md b/docs/php/builtins/string/ucfirst.md index 5ea51801e0..761cf864f3 100644 --- a/docs/php/builtins/string/ucfirst.md +++ b/docs/php/builtins/string/ucfirst.md @@ -2,7 +2,7 @@ title: "ucfirst()" description: "Uppercases the first character of a string." sidebar: - order: 403 + order: 407 --- ## ucfirst() @@ -18,6 +18,11 @@ Uppercases the first character of a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/ucwords.md b/docs/php/builtins/string/ucwords.md index 28ae2cf3d8..e19ff6ae0f 100644 --- a/docs/php/builtins/string/ucwords.md +++ b/docs/php/builtins/string/ucwords.md @@ -2,7 +2,7 @@ title: "ucwords()" description: "Uppercases the first character of each word in a string." sidebar: - order: 404 + order: 408 --- ## ucwords() @@ -19,6 +19,11 @@ Uppercases the first character of each word in a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/urldecode.md b/docs/php/builtins/string/urldecode.md index 61018c9334..c62400f0dd 100644 --- a/docs/php/builtins/string/urldecode.md +++ b/docs/php/builtins/string/urldecode.md @@ -2,7 +2,7 @@ title: "urldecode()" description: "Decodes a URL-encoded string, including '+' as a space." sidebar: - order: 405 + order: 409 --- ## urldecode() @@ -18,6 +18,11 @@ Decodes a URL-encoded string, including '+' as a space. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/urlencode.md b/docs/php/builtins/string/urlencode.md index 2130cb43b5..f851695738 100644 --- a/docs/php/builtins/string/urlencode.md +++ b/docs/php/builtins/string/urlencode.md @@ -2,7 +2,7 @@ title: "urlencode()" description: "URL-encodes a string using application/x-www-form-urlencoded rules." sidebar: - order: 406 + order: 410 --- ## urlencode() @@ -18,6 +18,11 @@ URL-encodes a string using application/x-www-form-urlencoded rules. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/vprintf.md b/docs/php/builtins/string/vprintf.md index 57b8496cff..ac88bfa994 100644 --- a/docs/php/builtins/string/vprintf.md +++ b/docs/php/builtins/string/vprintf.md @@ -2,7 +2,7 @@ title: "vprintf()" description: "Outputs a formatted string using an array of values." sidebar: - order: 407 + order: 411 --- ## vprintf() @@ -19,6 +19,11 @@ Outputs a formatted string using an array of values. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/vsprintf.md b/docs/php/builtins/string/vsprintf.md index 3f180b06ed..7aec19ae51 100644 --- a/docs/php/builtins/string/vsprintf.md +++ b/docs/php/builtins/string/vsprintf.md @@ -2,7 +2,7 @@ title: "vsprintf()" description: "Returns a formatted string using an array of values." sidebar: - order: 408 + order: 412 --- ## vsprintf() @@ -19,6 +19,11 @@ Returns a formatted string using an array of values. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/string/wordwrap.md b/docs/php/builtins/string/wordwrap.md index 765a9f2ce3..afae05ae8d 100644 --- a/docs/php/builtins/string/wordwrap.md +++ b/docs/php/builtins/string/wordwrap.md @@ -2,7 +2,7 @@ title: "wordwrap()" description: "Wraps a string to a given number of characters." sidebar: - order: 409 + order: 413 --- ## wordwrap() @@ -21,6 +21,11 @@ Wraps a string to a given number of characters. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type.md b/docs/php/builtins/type.md index 75386c75ec..49c9af6645 100644 --- a/docs/php/builtins/type.md +++ b/docs/php/builtins/type.md @@ -7,28 +7,28 @@ sidebar: ## Type builtins -| Function | Signature | Returns | -|---|---|---| -| [`boolval()`](./type/boolval.md) | `(mixed $value): bool` | `bool` | -| [`ctype_alnum()`](./type/ctype_alnum.md) | `(string $text): bool` | `bool` | -| [`ctype_alpha()`](./type/ctype_alpha.md) | `(string $text): bool` | `bool` | -| [`ctype_digit()`](./type/ctype_digit.md) | `(string $text): bool` | `bool` | -| [`ctype_space()`](./type/ctype_space.md) | `(string $text): bool` | `bool` | -| [`floatval()`](./type/floatval.md) | `(mixed $value): float` | `float` | -| [`get_resource_id()`](./type/get_resource_id.md) | `(resource $resource): int` | `int` | -| [`get_resource_type()`](./type/get_resource_type.md) | `(resource $resource): string` | `string` | -| [`gettype()`](./type/gettype.md) | `(mixed $value): string` | `string` | -| [`intval()`](./type/intval.md) | `(mixed $value): int` | `int` | -| [`is_array()`](./type/is_array.md) | `(mixed $value): bool` | `bool` | -| [`is_bool()`](./type/is_bool.md) | `(mixed $value): bool` | `bool` | -| [`is_callable()`](./type/is_callable.md) | `(mixed $value): bool` | `bool` | -| [`is_float()`](./type/is_float.md) | `(mixed $value): bool` | `bool` | -| [`is_int()`](./type/is_int.md) | `(mixed $value): bool` | `bool` | -| [`is_iterable()`](./type/is_iterable.md) | `(mixed $value): bool` | `bool` | -| [`is_null()`](./type/is_null.md) | `(mixed $value): bool` | `bool` | -| [`is_numeric()`](./type/is_numeric.md) | `(mixed $value): bool` | `bool` | -| [`is_object()`](./type/is_object.md) | `(mixed $value): bool` | `bool` | -| [`is_resource()`](./type/is_resource.md) | `(mixed $value): bool` | `bool` | -| [`is_scalar()`](./type/is_scalar.md) | `(mixed $value): bool` | `bool` | -| [`is_string()`](./type/is_string.md) | `(mixed $value): bool` | `bool` | -| [`settype()`](./type/settype.md) | `(mixed $var, string $type): bool` | `bool` | +| Function | Signature | Returns | AOT | eval() | +|---|---|---|:-:|:-:| +| [`boolval()`](./type/boolval.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`ctype_alnum()`](./type/ctype_alnum.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_alpha()`](./type/ctype_alpha.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_digit()`](./type/ctype_digit.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`ctype_space()`](./type/ctype_space.md) | `(string $text): bool` | `bool` | ✓ | ✓ | +| [`floatval()`](./type/floatval.md) | `(mixed $value): float` | `float` | ✓ | ✓ | +| [`get_resource_id()`](./type/get_resource_id.md) | `(resource $resource): int` | `int` | ✓ | ✓ | +| [`get_resource_type()`](./type/get_resource_type.md) | `(resource $resource): string` | `string` | ✓ | ✓ | +| [`gettype()`](./type/gettype.md) | `(mixed $value): string` | `string` | ✓ | ✓ | +| [`intval()`](./type/intval.md) | `(mixed $value): int` | `int` | ✓ | ✓ | +| [`is_array()`](./type/is_array.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_bool()`](./type/is_bool.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_callable()`](./type/is_callable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_float()`](./type/is_float.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_int()`](./type/is_int.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_iterable()`](./type/is_iterable.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_null()`](./type/is_null.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_numeric()`](./type/is_numeric.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_object()`](./type/is_object.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_resource()`](./type/is_resource.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_scalar()`](./type/is_scalar.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`is_string()`](./type/is_string.md) | `(mixed $value): bool` | `bool` | ✓ | ✓ | +| [`settype()`](./type/settype.md) | `(mixed $var, string $type): bool` | `bool` | ✓ | ✓ | diff --git a/docs/php/builtins/type/boolval.md b/docs/php/builtins/type/boolval.md index b51be25e0e..c708d4f738 100644 --- a/docs/php/builtins/type/boolval.md +++ b/docs/php/builtins/type/boolval.md @@ -2,7 +2,7 @@ title: "boolval()" description: "Returns the boolean value of a variable." sidebar: - order: 410 + order: 414 --- ## boolval() @@ -18,6 +18,11 @@ Returns the boolean value of a variable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/boolval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/boolval.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/ctype_alnum.md b/docs/php/builtins/type/ctype_alnum.md index 3b0f4ea08e..e0009bd063 100644 --- a/docs/php/builtins/type/ctype_alnum.md +++ b/docs/php/builtins/type/ctype_alnum.md @@ -2,7 +2,7 @@ title: "ctype_alnum()" description: "Checks if all characters in the string are alphanumeric." sidebar: - order: 411 + order: 415 --- ## ctype_alnum() @@ -18,6 +18,11 @@ Checks if all characters in the string are alphanumeric. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/ctype_alpha.md b/docs/php/builtins/type/ctype_alpha.md index dcec8ca7e7..1c78690cc1 100644 --- a/docs/php/builtins/type/ctype_alpha.md +++ b/docs/php/builtins/type/ctype_alpha.md @@ -2,7 +2,7 @@ title: "ctype_alpha()" description: "Checks if all characters in the string are alphabetic." sidebar: - order: 412 + order: 416 --- ## ctype_alpha() @@ -18,6 +18,11 @@ Checks if all characters in the string are alphabetic. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/ctype_digit.md b/docs/php/builtins/type/ctype_digit.md index 57dd9ec6df..0639b02ebc 100644 --- a/docs/php/builtins/type/ctype_digit.md +++ b/docs/php/builtins/type/ctype_digit.md @@ -2,7 +2,7 @@ title: "ctype_digit()" description: "Checks if all characters in the string are digits." sidebar: - order: 413 + order: 417 --- ## ctype_digit() @@ -18,6 +18,11 @@ Checks if all characters in the string are digits. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/ctype_space.md b/docs/php/builtins/type/ctype_space.md index ed18d53168..3825e43c1c 100644 --- a/docs/php/builtins/type/ctype_space.md +++ b/docs/php/builtins/type/ctype_space.md @@ -2,7 +2,7 @@ title: "ctype_space()" description: "Checks if all characters in the string are whitespace characters." sidebar: - order: 414 + order: 418 --- ## ctype_space() @@ -18,6 +18,11 @@ Checks if all characters in the string are whitespace characters. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/floatval.md b/docs/php/builtins/type/floatval.md index 3bf86190c7..089f094671 100644 --- a/docs/php/builtins/type/floatval.md +++ b/docs/php/builtins/type/floatval.md @@ -2,7 +2,7 @@ title: "floatval()" description: "Returns the float value of a variable." sidebar: - order: 415 + order: 419 --- ## floatval() @@ -18,6 +18,11 @@ Returns the float value of a variable. **Returns**: `float` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/floatval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/floatval.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/get_resource_id.md b/docs/php/builtins/type/get_resource_id.md index b6e5f9609b..1a2d929fd5 100644 --- a/docs/php/builtins/type/get_resource_id.md +++ b/docs/php/builtins/type/get_resource_id.md @@ -2,7 +2,7 @@ title: "get_resource_id()" description: "Returns an integer identifier for the given resource." sidebar: - order: 416 + order: 420 --- ## get_resource_id() @@ -18,6 +18,11 @@ Returns an integer identifier for the given resource. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/get_resource_type.md b/docs/php/builtins/type/get_resource_type.md index f9ae6868cc..62c4d71e3a 100644 --- a/docs/php/builtins/type/get_resource_type.md +++ b/docs/php/builtins/type/get_resource_type.md @@ -2,7 +2,7 @@ title: "get_resource_type()" description: "Returns the type of a resource." sidebar: - order: 417 + order: 421 --- ## get_resource_type() @@ -18,6 +18,11 @@ Returns the type of a resource. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/gettype.md b/docs/php/builtins/type/gettype.md index ddc8b75548..fd840fb77d 100644 --- a/docs/php/builtins/type/gettype.md +++ b/docs/php/builtins/type/gettype.md @@ -2,7 +2,7 @@ title: "gettype()" description: "Returns the type of a variable as a string." sidebar: - order: 418 + order: 422 --- ## gettype() @@ -18,6 +18,11 @@ Returns the type of a variable as a string. **Returns**: `string` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/gettype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/gettype.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/intval.md b/docs/php/builtins/type/intval.md index d6f8629483..8c584777c9 100644 --- a/docs/php/builtins/type/intval.md +++ b/docs/php/builtins/type/intval.md @@ -2,7 +2,7 @@ title: "intval()" description: "Returns the integer value of a variable." sidebar: - order: 419 + order: 423 --- ## intval() @@ -18,6 +18,11 @@ Returns the integer value of a variable. **Returns**: `int` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/intval.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/intval.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_array.md b/docs/php/builtins/type/is_array.md index f096891afb..1fdb21c808 100644 --- a/docs/php/builtins/type/is_array.md +++ b/docs/php/builtins/type/is_array.md @@ -2,7 +2,7 @@ title: "is_array()" description: "Checks whether a variable is an array." sidebar: - order: 420 + order: 424 --- ## is_array() @@ -18,6 +18,11 @@ Checks whether a variable is an array. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_array.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_array.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_bool.md b/docs/php/builtins/type/is_bool.md index fddf5d2781..fd8c2600af 100644 --- a/docs/php/builtins/type/is_bool.md +++ b/docs/php/builtins/type/is_bool.md @@ -2,7 +2,7 @@ title: "is_bool()" description: "Checks whether a variable is a boolean." sidebar: - order: 421 + order: 425 --- ## is_bool() @@ -18,6 +18,11 @@ Checks whether a variable is a boolean. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_callable.md b/docs/php/builtins/type/is_callable.md index 50e144beb0..b5dfc3c88d 100644 --- a/docs/php/builtins/type/is_callable.md +++ b/docs/php/builtins/type/is_callable.md @@ -2,7 +2,7 @@ title: "is_callable()" description: "Checks whether a variable can be called as a function." sidebar: - order: 422 + order: 426 --- ## is_callable() @@ -18,6 +18,11 @@ Checks whether a variable can be called as a function. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_float.md b/docs/php/builtins/type/is_float.md index 1cdf014f21..e18e41b2f9 100644 --- a/docs/php/builtins/type/is_float.md +++ b/docs/php/builtins/type/is_float.md @@ -2,7 +2,7 @@ title: "is_float()" description: "Checks whether a variable is a floating-point number." sidebar: - order: 423 + order: 427 --- ## is_float() @@ -18,6 +18,11 @@ Checks whether a variable is a floating-point number. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_float.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_float.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_int.md b/docs/php/builtins/type/is_int.md index b6ab7eb4b7..56569d6bd5 100644 --- a/docs/php/builtins/type/is_int.md +++ b/docs/php/builtins/type/is_int.md @@ -2,7 +2,7 @@ title: "is_int()" description: "Checks whether a variable is an integer." sidebar: - order: 424 + order: 428 --- ## is_int() @@ -18,6 +18,11 @@ Checks whether a variable is an integer. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_int.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_int.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_iterable.md b/docs/php/builtins/type/is_iterable.md index c71165da23..931aae2140 100644 --- a/docs/php/builtins/type/is_iterable.md +++ b/docs/php/builtins/type/is_iterable.md @@ -2,7 +2,7 @@ title: "is_iterable()" description: "Checks whether a variable is iterable." sidebar: - order: 425 + order: 429 --- ## is_iterable() @@ -18,6 +18,11 @@ Checks whether a variable is iterable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_null.md b/docs/php/builtins/type/is_null.md index 2722731bdc..75062324ff 100644 --- a/docs/php/builtins/type/is_null.md +++ b/docs/php/builtins/type/is_null.md @@ -2,7 +2,7 @@ title: "is_null()" description: "Checks whether a variable is null." sidebar: - order: 426 + order: 430 --- ## is_null() @@ -18,6 +18,11 @@ Checks whether a variable is null. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_null.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_null.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_numeric.md b/docs/php/builtins/type/is_numeric.md index b45733be8f..ebe68cb067 100644 --- a/docs/php/builtins/type/is_numeric.md +++ b/docs/php/builtins/type/is_numeric.md @@ -2,7 +2,7 @@ title: "is_numeric()" description: "Checks whether a variable is a number or a numeric string." sidebar: - order: 427 + order: 431 --- ## is_numeric() @@ -18,6 +18,11 @@ Checks whether a variable is a number or a numeric string. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_object.md b/docs/php/builtins/type/is_object.md index a093703491..b858336723 100644 --- a/docs/php/builtins/type/is_object.md +++ b/docs/php/builtins/type/is_object.md @@ -2,7 +2,7 @@ title: "is_object()" description: "Checks whether a variable is an object." sidebar: - order: 428 + order: 432 --- ## is_object() @@ -18,6 +18,11 @@ Checks whether a variable is an object. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_object.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_object.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_resource.md b/docs/php/builtins/type/is_resource.md index 3821445800..3680361239 100644 --- a/docs/php/builtins/type/is_resource.md +++ b/docs/php/builtins/type/is_resource.md @@ -2,7 +2,7 @@ title: "is_resource()" description: "Checks whether a variable is a resource." sidebar: - order: 429 + order: 433 --- ## is_resource() @@ -18,6 +18,11 @@ Checks whether a variable is a resource. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_scalar.md b/docs/php/builtins/type/is_scalar.md index 41e0acf8db..65aa1759aa 100644 --- a/docs/php/builtins/type/is_scalar.md +++ b/docs/php/builtins/type/is_scalar.md @@ -2,7 +2,7 @@ title: "is_scalar()" description: "Checks whether a variable is a scalar." sidebar: - order: 430 + order: 434 --- ## is_scalar() @@ -18,6 +18,11 @@ Checks whether a variable is a scalar. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/is_string.md b/docs/php/builtins/type/is_string.md index 60a624402d..c86c981aa9 100644 --- a/docs/php/builtins/type/is_string.md +++ b/docs/php/builtins/type/is_string.md @@ -2,7 +2,7 @@ title: "is_string()" description: "Checks whether a variable is a string." sidebar: - order: 431 + order: 435 --- ## is_string() @@ -18,6 +18,11 @@ Checks whether a variable is a string. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/is_string.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/is_string.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/docs/php/builtins/type/settype.md b/docs/php/builtins/type/settype.md index 698862b041..609df86682 100644 --- a/docs/php/builtins/type/settype.md +++ b/docs/php/builtins/type/settype.md @@ -2,7 +2,7 @@ title: "settype()" description: "Sets the type of a variable." sidebar: - order: 432 + order: 436 --- ## settype() @@ -19,6 +19,11 @@ Sets the type of a variable. **Returns**: `bool` +## Availability + +- **Compiled (AOT)**: supported by the Elephc code generator. +- **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/types/settype.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/types/settype.rs)). + _No examples yet — check `examples/` and `showcases/` for usage patterns._ diff --git a/scripts/docs/builtin_registry.json b/scripts/docs/builtin_registry.json index 4aebd39aad..f708adbdf2 100644 --- a/scripts/docs/builtin_registry.json +++ b/scripts/docs/builtin_registry.json @@ -3,6 +3,11 @@ "area": "Misc", "canonical_name": "__elephc_gmmktime_raw", "description": "Internal raw gmmktime alias used by the synthetic DateTime body.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -80,6 +85,11 @@ "area": "Misc", "canonical_name": "__elephc_mktime_raw", "description": "Internal raw mktime alias used by the synthetic DateTime body.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -157,6 +167,11 @@ "area": "IO", "canonical_name": "__elephc_phar_bzip2_archive", "description": "Compresses a PHAR archive using bzip2.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -195,6 +210,11 @@ "area": "IO", "canonical_name": "__elephc_phar_decompress_archive", "description": "Decompresses a PHAR archive to a new path.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -233,6 +253,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_file_metadata", "description": "Reads the serialized per-file metadata blob.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -270,6 +295,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_metadata", "description": "Reads the serialized PHAR-level metadata blob.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -307,6 +337,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_signature_hash", "description": "Returns the PHAR signature hash bytes.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -344,6 +379,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_signature_type", "description": "Returns the PHAR signature algorithm name.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -381,6 +421,11 @@ "area": "IO", "canonical_name": "__elephc_phar_get_stub", "description": "Reads the PHAR stub script.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -418,6 +463,11 @@ "area": "IO", "canonical_name": "__elephc_phar_gzip_archive", "description": "Compresses a PHAR archive using gzip.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -456,6 +506,11 @@ "area": "IO", "canonical_name": "__elephc_phar_list_entries", "description": "Lists the file paths within a PHAR archive.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -494,6 +549,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_compression", "description": "Sets the compression algorithm for a PHAR archive.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -539,6 +599,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_file_metadata", "description": "Writes the serialized per-file metadata blob.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -585,6 +650,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_metadata", "description": "Writes the serialized PHAR-level metadata blob.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -629,6 +699,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_stub", "description": "Writes the PHAR stub script.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -673,6 +748,11 @@ "area": "IO", "canonical_name": "__elephc_phar_set_zip_password", "description": "Sets the encryption password for a PHAR ZIP archive.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -711,6 +791,11 @@ "area": "IO", "canonical_name": "__elephc_phar_sign_hash", "description": "Signs a PHAR archive with the given hash algorithm.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -755,6 +840,11 @@ "area": "IO", "canonical_name": "__elephc_phar_sign_openssl", "description": "Signs a PHAR archive using an OpenSSL private key.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -799,6 +889,11 @@ "area": "Misc", "canonical_name": "__elephc_strtotime_raw", "description": "Internal raw strtotime alias returning a plain integer.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": false, "is_internal": true, "lowering": { @@ -846,6 +941,27 @@ "area": "Math", "canonical_name": "abs", "description": "Absolute value.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/abs.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -885,6 +1001,27 @@ "area": "Math", "canonical_name": "acos", "description": "Returns the arccosine of a number in radians.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/acos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -922,6 +1059,27 @@ "area": "String", "canonical_name": "addslashes", "description": "Adds backslashes before characters that need to be escaped.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/addslashes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -961,6 +1119,11 @@ "area": "Array", "canonical_name": "array_all", "description": "Returns true when every array element satisfies the predicate callback.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1008,6 +1171,11 @@ "area": "Array", "canonical_name": "array_any", "description": "Returns true when at least one array element satisfies the predicate callback.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1054,6 +1222,33 @@ "area": "Array", "canonical_name": "array_chunk", "description": "Splits an array into chunks of the given size.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_chunk.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1098,6 +1293,33 @@ "area": "Array", "canonical_name": "array_column", "description": "Returns the values from a single column of an array of arrays.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_column.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "column_key", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1142,6 +1364,33 @@ "area": "Array", "canonical_name": "array_combine", "description": "Creates an array by using one array for keys and another for values.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_combine.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "keys", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "values", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1186,6 +1435,27 @@ "area": "Array", "canonical_name": "array_diff", "description": "Computes the difference of arrays.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_diff.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1229,6 +1499,11 @@ "area": "Array", "canonical_name": "array_diff_assoc", "description": "Computes the difference of arrays with additional index check.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1268,6 +1543,27 @@ "area": "Array", "canonical_name": "array_diff_key", "description": "Computes the difference of arrays using keys for comparison.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_diff_key.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1308,6 +1604,39 @@ "area": "Array", "canonical_name": "array_fill", "description": "Fill an array with values.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_fill.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "start_index", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "count", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1359,6 +1688,33 @@ "area": "Array", "canonical_name": "array_fill_keys", "description": "Fill an array with values, specifying keys.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_fill_keys.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "keys", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1403,6 +1759,39 @@ "area": "Array", "canonical_name": "array_filter", "description": "Filters elements of an array using a callback function.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_filter.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "callback", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "mode", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1457,6 +1846,11 @@ "area": "Array", "canonical_name": "array_find", "description": "Returns the first element satisfying a predicate callback, or null.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1503,6 +1897,27 @@ "area": "Array", "canonical_name": "array_flip", "description": "Exchanges all keys with their associated values in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_flip.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1540,6 +1955,27 @@ "area": "Array", "canonical_name": "array_intersect", "description": "Computes the intersection of arrays.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_intersect.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1582,6 +2018,11 @@ "area": "Array", "canonical_name": "array_intersect_assoc", "description": "Computes the intersection of arrays with additional index check.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1624,6 +2065,27 @@ "area": "Array", "canonical_name": "array_intersect_key", "description": "Computes the intersection of arrays using keys for comparison.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_intersect_key.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1663,6 +2125,11 @@ "area": "Array", "canonical_name": "array_is_list", "description": "Checks whether an array is a list (sequential 0-based integer keys).", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1706,6 +2173,33 @@ "area": "Array", "canonical_name": "array_key_exists", "description": "Checks if the given key or index exists in the array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_key_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "key", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1752,6 +2246,11 @@ "area": "Array", "canonical_name": "array_key_first", "description": "Gets the first key of an array.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1792,6 +2291,11 @@ "area": "Array", "canonical_name": "array_key_last", "description": "Gets the last key of an array.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1832,6 +2336,27 @@ "area": "Array", "canonical_name": "array_keys", "description": "Returns all the keys of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_keys.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1869,6 +2394,33 @@ "area": "Array", "canonical_name": "array_map", "description": "Applies a callback to the elements of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_map.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1913,6 +2465,20 @@ "area": "Array", "canonical_name": "array_merge", "description": "Merges the elements of two arrays.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_merge.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": "arrays" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1945,6 +2511,11 @@ "area": "Array", "canonical_name": "array_merge_recursive", "description": "Recursively merges two arrays, combining scalar collisions into lists.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -1978,6 +2549,11 @@ "area": "Array", "canonical_name": "array_multisort", "description": "Sorts multiple arrays or multi-dimensional arrays.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2025,10 +2601,43 @@ "area": "Array", "canonical_name": "array_pad", "description": "Pads an array to the specified length with a value.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_pad.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/arrays.rs", "codegen_function": "lower_array_pad", @@ -2076,6 +2685,26 @@ "area": "Array", "canonical_name": "array_pop", "description": "Pops the element off the end of array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_pop.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2116,6 +2745,27 @@ "area": "Array", "canonical_name": "array_product", "description": "Calculate the product of values in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_product.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2155,6 +2805,26 @@ "area": "Array", "canonical_name": "array_push", "description": "Pushes one or more elements onto the end of array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_push.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2192,6 +2862,27 @@ "area": "Array", "canonical_name": "array_rand", "description": "Pick one or more random keys out of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_rand.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2232,6 +2923,39 @@ "area": "Array", "canonical_name": "array_reduce", "description": "Iteratively reduces an array to a single value using a callback function.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_reduce.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "initial", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2285,6 +3009,11 @@ "area": "Array", "canonical_name": "array_replace", "description": "Replaces elements from passed arrays into the first array.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2333,6 +3062,11 @@ "area": "Array", "canonical_name": "array_replace_recursive", "description": "Replaces elements from passed arrays into the first array recursively.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2380,6 +3114,33 @@ "area": "Array", "canonical_name": "array_reverse", "description": "Returns an array with the elements in reverse order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_reverse.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "preserve_keys", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2417,6 +3178,39 @@ "area": "Array", "canonical_name": "array_search", "description": "Searches the array for a given value and returns the first corresponding key if successful.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_search.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "strict", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2468,6 +3262,26 @@ "area": "Array", "canonical_name": "array_shift", "description": "Shifts an element off the beginning of array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_shift.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2505,6 +3319,39 @@ "area": "Array", "canonical_name": "array_slice", "description": "Extracts a slice of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_slice.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2556,6 +3403,44 @@ "area": "Array", "canonical_name": "array_splice", "description": "Removes a portion of the array and replaces it with something else.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_splice.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + }, + { + "by_ref": false, + "default": "[]", + "name": "replacement", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2607,6 +3492,27 @@ "area": "Array", "canonical_name": "array_sum", "description": "Calculate the sum of values in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_sum.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2647,6 +3553,11 @@ "area": "Array", "canonical_name": "array_udiff", "description": "Computes the difference of arrays using a callback comparator.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2698,6 +3609,11 @@ "area": "Array", "canonical_name": "array_uintersect", "description": "Computes the intersection of arrays using a callback comparator.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2749,6 +3665,27 @@ "area": "Array", "canonical_name": "array_unique", "description": "Removes duplicate values from an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_unique.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2789,6 +3726,26 @@ "area": "Array", "canonical_name": "array_unshift", "description": "Prepends one or more elements to the beginning of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_unshift.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2826,6 +3783,27 @@ "area": "Array", "canonical_name": "array_values", "description": "Returns all the values of an array, re-indexed numerically.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_values.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2863,6 +3841,32 @@ "area": "Array", "canonical_name": "array_walk", "description": "Applies a user function to every member of an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/array_walk.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2909,6 +3913,11 @@ "area": "Array", "canonical_name": "array_walk_recursive", "description": "Applies a user function recursively to every member of an array.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -2957,6 +3966,26 @@ "area": "Array", "canonical_name": "arsort", "description": "Sorts an array in descending order and maintains index association.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/arsort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3000,6 +4029,27 @@ "area": "Math", "canonical_name": "asin", "description": "Returns the arcsine of a number in radians.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/asin.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3037,8 +4087,28 @@ "area": "Array", "canonical_name": "asort", "description": "Sorts an array and maintains index association.", - "in_catalog": true, - "is_internal": false, + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/asort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, @@ -3081,6 +4151,27 @@ "area": "Math", "canonical_name": "atan", "description": "Returns the arctangent of a number in radians.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/atan.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3118,6 +4209,33 @@ "area": "Math", "canonical_name": "atan2", "description": "Returns the arc tangent of two variables.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/atan2.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "y", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "x", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3162,6 +4280,27 @@ "area": "String", "canonical_name": "base64_decode", "description": "Decodes a Base64-encoded string back into its original data.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/base64_decode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3201,6 +4340,27 @@ "area": "String", "canonical_name": "base64_encode", "description": "Encodes binary data into a Base64 string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/base64_encode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3240,6 +4400,33 @@ "area": "Filesystem", "canonical_name": "basename", "description": "Returns the trailing name component of a path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/basename.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + }, + { + "by_ref": false, + "default": "\"\"", + "name": "suffix", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3284,6 +4471,27 @@ "area": "String", "canonical_name": "bin2hex", "description": "Converts binary data into its hexadecimal string representation.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/bin2hex.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3323,6 +4531,27 @@ "area": "Type", "canonical_name": "boolval", "description": "Returns the boolean value of a variable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/boolval.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3362,6 +4591,27 @@ "area": "Buffer", "canonical_name": "buffer_free", "description": "Lowers `buffer_free()` through the direct buffer opcode helper.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_free.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "buffer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3399,6 +4649,27 @@ "area": "Buffer", "canonical_name": "buffer_len", "description": "Lowers `buffer_len()` through the direct buffer opcode helper.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_len.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "buffer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3436,6 +4707,27 @@ "area": "Misc", "canonical_name": "buffer_new", "description": "", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3471,6 +4763,27 @@ "area": "Array", "canonical_name": "call_user_func", "description": "Calls a callback with the given arguments.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/call_user_func.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "args" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3511,6 +4824,33 @@ "area": "Array", "canonical_name": "call_user_func_array", "description": "Calls a callback with an array of parameters.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/call_user_func_array.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "args", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3558,6 +4898,27 @@ "area": "Math", "canonical_name": "ceil", "description": "Rounds a number up to the nearest integer.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/ceil.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3595,6 +4956,27 @@ "area": "Filesystem", "canonical_name": "chdir", "description": "Changes the current directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/chdir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3638,6 +5020,39 @@ "area": "Date", "canonical_name": "checkdate", "description": "Validates a Gregorian date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/checkdate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "month", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "day", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "year", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3695,6 +5110,33 @@ "area": "Filesystem", "canonical_name": "chgrp", "description": "Changes file group.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/chgrp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "group", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3741,6 +5183,33 @@ "area": "Filesystem", "canonical_name": "chmod", "description": "Changes file mode.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/chmod.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "permissions", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3785,6 +5254,33 @@ "area": "String", "canonical_name": "chop", "description": "Alias of rtrim: strips whitespace (or other characters) from the end of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/chop.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\n\\r\\t\\u{b}\\u{c}\\u{0}\"", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3829,6 +5325,33 @@ "area": "Filesystem", "canonical_name": "chown", "description": "Changes file owner.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/chown.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "user", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3875,6 +5398,27 @@ "area": "String", "canonical_name": "chr", "description": "Returns a one-character string from the given byte code point.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/chr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "codepoint", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -3914,7 +5458,40 @@ "area": "Math", "canonical_name": "clamp", "description": "Clamps a value to be within a specified range.", - "in_catalog": true, + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/clamp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, @@ -3965,6 +5542,39 @@ "area": "Class", "canonical_name": "class_alias", "description": "Creates an alias for a class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_alias.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "alias", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4016,6 +5626,33 @@ "area": "Class", "canonical_name": "class_attribute_args", "description": "Returns the constructor arguments of a named attribute applied to a class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_args.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class_name", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "attribute_name", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4060,6 +5697,27 @@ "area": "Class", "canonical_name": "class_attribute_names", "description": "Returns the list of attribute names applied to a class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_attribute_names.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class_name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4097,6 +5755,33 @@ "area": "Class", "canonical_name": "class_exists", "description": "Checks whether the given class has been defined.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4141,6 +5826,27 @@ "area": "Class", "canonical_name": "class_get_attributes", "description": "Returns an array of ReflectionAttribute objects for all attributes of a class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_get_attributes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class_name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4178,6 +5884,33 @@ "area": "Class", "canonical_name": "class_implements", "description": "Returns the interfaces which are implemented by the given class or its parents.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_implements.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4222,6 +5955,33 @@ "area": "Class", "canonical_name": "class_parents", "description": "Returns the parent classes of the given class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_parents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4266,6 +6026,33 @@ "area": "Class", "canonical_name": "class_uses", "description": "Returns the traits used by the given class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/class_uses.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4310,6 +6097,33 @@ "area": "Filesystem", "canonical_name": "clearstatcache", "description": "Clears file status cache.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/clearstatcache.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "false", + "name": "clear_realpath_cache", + "optional": true + }, + { + "by_ref": false, + "default": "\"\"", + "name": "filename", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4356,6 +6170,27 @@ "area": "IO", "canonical_name": "closedir", "description": "Closes directory handle.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/closedir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "dir_handle", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4398,6 +6233,33 @@ "area": "Filesystem", "canonical_name": "copy", "description": "Copies a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/copy.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "from", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "to", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4447,6 +6309,27 @@ "area": "Math", "canonical_name": "cos", "description": "Returns the cosine of a number (radians).", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/cos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4484,6 +6367,27 @@ "area": "Math", "canonical_name": "cosh", "description": "Returns the hyperbolic cosine of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/cosh.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4521,6 +6425,33 @@ "area": "Array", "canonical_name": "count", "description": "Counts all elements in an array or Countable object.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/count.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "mode", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4571,6 +6502,27 @@ "area": "String", "canonical_name": "crc32", "description": "Calculates the CRC32 polynomial of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/crc32.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4613,6 +6565,27 @@ "area": "Type", "canonical_name": "ctype_alnum", "description": "Checks if all characters in the string are alphanumeric.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_alnum.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "text", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4650,6 +6623,27 @@ "area": "Type", "canonical_name": "ctype_alpha", "description": "Checks if all characters in the string are alphabetic.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_alpha.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "text", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4687,6 +6681,27 @@ "area": "Type", "canonical_name": "ctype_digit", "description": "Checks if all characters in the string are digits.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_digit.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "text", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4724,12 +6739,33 @@ "area": "Type", "canonical_name": "ctype_space", "description": "Checks if all characters in the string are whitespace characters.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, - "checker_line": null, - "codegen_file": "src/codegen/lower_inst/builtins/ctype.rs", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ctype_space.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "text", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/ctype.rs", "codegen_function": "lower_ctype_space", "codegen_line": 35, "notes": [ @@ -4761,6 +6797,33 @@ "area": "Date", "canonical_name": "date", "description": "Formats a local time/date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4808,6 +6871,20 @@ "area": "Date", "canonical_name": "date_default_timezone_get", "description": "Gets the default timezone.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_get.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4842,6 +6919,27 @@ "area": "Date", "canonical_name": "date_default_timezone_set", "description": "Sets the default timezone.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/date_default_timezone_set.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "timezoneId", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4887,6 +6985,33 @@ "area": "Misc", "canonical_name": "define", "description": "Defines a named constant at runtime.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/define.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "constant_name", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4931,6 +7056,27 @@ "area": "Misc", "canonical_name": "defined", "description": "Checks whether a given named constant exists.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/defined.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "constant_name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -4968,6 +7114,27 @@ "area": "Math", "canonical_name": "deg2rad", "description": "Converts a degree value to radians.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/deg2rad.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5005,6 +7172,27 @@ "area": "Process", "canonical_name": "die", "description": "", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/die.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "0", + "name": "status", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5040,6 +7228,33 @@ "area": "Filesystem", "canonical_name": "dirname", "description": "Returns a parent directory's path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/dirname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + }, + { + "by_ref": false, + "default": "1", + "name": "levels", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5087,6 +7302,27 @@ "area": "Filesystem", "canonical_name": "disk_free_space", "description": "Returns available space on filesystem or disk partition.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/disk_free_space.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5126,6 +7362,27 @@ "area": "Filesystem", "canonical_name": "disk_total_space", "description": "Returns the total size of a filesystem or disk partition.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/disk_total_space.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5165,6 +7422,27 @@ "area": "Misc", "canonical_name": "empty", "description": "Determines whether a variable is considered empty.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/empty.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5204,6 +7482,33 @@ "area": "Class", "canonical_name": "enum_exists", "description": "Checks if the enum has been defined.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/enum_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "enum", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5248,6 +7553,27 @@ "area": "Process", "canonical_name": "exec", "description": "Executes an external program and returns the last line of output.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/exec.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5285,6 +7611,27 @@ "area": "Process", "canonical_name": "exit", "description": "", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/exit.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "0", + "name": "status", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5320,6 +7667,27 @@ "area": "Math", "canonical_name": "exp", "description": "Returns e raised to the power of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/exp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5357,6 +7725,39 @@ "area": "String", "canonical_name": "explode", "description": "Splits a string by a separator into an array of substrings.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/explode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "separator", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "9223372036854775807", + "name": "limit", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5411,6 +7812,27 @@ "area": "IO", "canonical_name": "fclose", "description": "Closes an open file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fclose.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5448,6 +7870,27 @@ "area": "IO", "canonical_name": "fdatasync", "description": "Synchronizes data (but not meta-data) to file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fdatasync.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5487,6 +7930,33 @@ "area": "Math", "canonical_name": "fdiv", "description": "Divides two numbers, according to IEEE 754.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/fdiv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "num2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5531,6 +8001,27 @@ "area": "IO", "canonical_name": "feof", "description": "Tests for end-of-file on a file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/feof.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5571,6 +8062,27 @@ "area": "IO", "canonical_name": "fflush", "description": "Flushes the output to a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fflush.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5610,9 +8122,30 @@ "area": "IO", "canonical_name": "fgetc", "description": "Gets a character from the given file pointer.", - "in_catalog": true, - "is_internal": false, - "lowering": { + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fgetc.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/io.rs", @@ -5650,6 +8183,39 @@ "area": "IO", "canonical_name": "fgetcsv", "description": "Gets line from file pointer and parse for CSV fields.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fgetcsv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + }, + { + "by_ref": false, + "default": "\",\"", + "name": "separator", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5704,6 +8270,27 @@ "area": "IO", "canonical_name": "fgets", "description": "Gets line from file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fgets.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5744,6 +8331,27 @@ "area": "IO", "canonical_name": "file", "description": "Reads an entire file into an array.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/file.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5784,6 +8392,27 @@ "area": "Filesystem", "canonical_name": "file_exists", "description": "Checks whether a file or directory exists.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/file_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5824,6 +8453,27 @@ "area": "IO", "canonical_name": "file_get_contents", "description": "Reads an entire file into a string.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/file_get_contents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5864,6 +8514,33 @@ "area": "IO", "canonical_name": "file_put_contents", "description": "Writes data to a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/file_put_contents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5911,6 +8588,27 @@ "area": "Filesystem", "canonical_name": "fileatime", "description": "Gets last access time of file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fileatime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5953,6 +8651,27 @@ "area": "Filesystem", "canonical_name": "filectime", "description": "Gets inode change time of file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filectime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -5995,6 +8714,27 @@ "area": "Filesystem", "canonical_name": "filegroup", "description": "Gets file group.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filegroup.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6037,6 +8777,27 @@ "area": "Filesystem", "canonical_name": "fileinode", "description": "Gets file inode.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fileinode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6079,6 +8840,27 @@ "area": "Filesystem", "canonical_name": "filemtime", "description": "Gets file modification time.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filemtime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6122,6 +8904,27 @@ "area": "Filesystem", "canonical_name": "fileowner", "description": "Gets file owner.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fileowner.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6163,6 +8966,27 @@ "area": "Filesystem", "canonical_name": "fileperms", "description": "Gets file permissions.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fileperms.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6205,6 +9029,27 @@ "area": "Filesystem", "canonical_name": "filesize", "description": "Gets file size.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filesize.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6247,6 +9092,27 @@ "area": "Filesystem", "canonical_name": "filetype", "description": "Gets file type.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/filetype.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6288,6 +9154,27 @@ "area": "Type", "canonical_name": "floatval", "description": "Returns the float value of a variable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/floatval.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6328,6 +9215,38 @@ "area": "IO", "canonical_name": "flock", "description": "Portable advisory file locking.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/flock.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "operation", + "optional": false + }, + { + "by_ref": true, + "default": "null", + "name": "would_block", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6379,6 +9298,27 @@ "area": "Math", "canonical_name": "floor", "description": "Rounds a number down to the nearest integer.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/floor.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6416,6 +9356,33 @@ "area": "Math", "canonical_name": "fmod", "description": "Returns the floating point remainder of the division of the arguments.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/fmod.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "num2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6460,6 +9427,39 @@ "area": "Filesystem", "canonical_name": "fnmatch", "description": "Matches a filename against a pattern.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fnmatch.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6511,10 +9511,49 @@ "area": "IO", "canonical_name": "fopen", "description": "Opens file or URL.", - "in_catalog": true, - "is_internal": false, - "lowering": { - "checker_file": null, + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fopen.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "mode", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "use_include_path", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "context", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, "checker_line": null, "codegen_file": "src/codegen/lower_inst/builtins/io.rs", "codegen_function": "lower_fopen", @@ -6571,6 +9610,27 @@ "area": "IO", "canonical_name": "fpassthru", "description": "Output all remaining data on a file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fpassthru.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6611,6 +9671,33 @@ "area": "IO", "canonical_name": "fprintf", "description": "Write a formatted string to a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6657,6 +9744,45 @@ "area": "IO", "canonical_name": "fputcsv", "description": "Format line as CSV and write to file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fputcsv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "fields", + "optional": false + }, + { + "by_ref": false, + "default": "\",\"", + "name": "separator", + "optional": true + }, + { + "by_ref": false, + "default": "\"\\\"\"", + "name": "enclosure", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6717,6 +9843,33 @@ "area": "IO", "canonical_name": "fread", "description": "Binary-safe file read.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fread.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6763,6 +9916,33 @@ "area": "IO", "canonical_name": "fscanf", "description": "Parses input from a file according to a format.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fscanf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": "vars" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6810,6 +9990,39 @@ "area": "IO", "canonical_name": "fseek", "description": "Seeks on a file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fseek.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "whence", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6861,6 +10074,50 @@ "area": "Streams", "canonical_name": "fsockopen", "description": "Open Internet or Unix domain socket connection.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fsockopen.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hostname", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "port", + "optional": false + }, + { + "by_ref": true, + "default": "null", + "name": "error_code", + "optional": true + }, + { + "by_ref": true, + "default": "null", + "name": "error_message", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "timeout", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6926,6 +10183,27 @@ "area": "IO", "canonical_name": "fstat", "description": "Gets information about a file using an open file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fstat.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -6965,6 +10243,27 @@ "area": "IO", "canonical_name": "fsync", "description": "Synchronizes changes to the file (including meta-data).", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fsync.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7005,6 +10304,27 @@ "area": "IO", "canonical_name": "ftell", "description": "Returns the current position of the file read/write pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/ftell.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7044,6 +10364,33 @@ "area": "IO", "canonical_name": "ftruncate", "description": "Truncates a file to a given length.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/ftruncate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7088,6 +10435,27 @@ "area": "Class", "canonical_name": "function_exists", "description": "Returns true if the given function has been defined.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/function_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "function", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7130,6 +10498,33 @@ "area": "IO", "canonical_name": "fwrite", "description": "Binary-safe file write.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/fwrite.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7174,25 +10569,87 @@ }, { "area": "Class", - "canonical_name": "get_class", - "description": "Returns the name of the class of an object.", + "canonical_name": "get_called_class", + "description": "get_called_class() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": true, "in_catalog": true, "is_internal": false, "lowering": { "checker_file": null, "checker_line": null, - "codegen_file": "src/codegen/lower_inst/builtins/types.rs", - "codegen_function": "lower_class_name_lookup", - "codegen_line": 331, - "notes": [ - "Lowers `get_class()` and `get_parent_class()` through static or dynamic class metadata." - ], + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], "runtime_helpers": [], "sig_arm": null, - "sig_file": "src/builtins/callables/get_class.rs", + "sig_file": null, "sig_line": null }, - "name": "get_class", + "name": "get_called_class", + "sig": { + "params": [], + "return_type": "mixed", + "variadic": null + }, + "slug": "get_called_class", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "get_class", + "description": "Returns the name of the class of an object.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_class.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "object", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": "src/codegen/lower_inst/builtins/types.rs", + "codegen_function": "lower_class_name_lookup", + "codegen_line": 331, + "notes": [ + "Lowers `get_class()` and `get_parent_class()` through static or dynamic class metadata." + ], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": "src/builtins/callables/get_class.rs", + "sig_line": null + }, + "name": "get_class", "sig": { "params": [ { @@ -7209,10 +10666,136 @@ "slug": "get_class", "sub_area": "Class" }, + { + "area": "Class", + "canonical_name": "get_class_methods", + "description": "get_class_methods() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": true, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": null, + "sig_line": null + }, + "name": "get_class_methods", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false, + "type": "mixed" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "get_class_methods", + "sub_area": "Class" + }, + { + "area": "Class", + "canonical_name": "get_class_vars", + "description": "get_class_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": true, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": null, + "sig_line": null + }, + "name": "get_class_vars", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false, + "type": "mixed" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "get_class_vars", + "sub_area": "Class" + }, { "area": "Class", "canonical_name": "get_declared_classes", "description": "Returns an array of the names of the defined classes.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_classes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7242,6 +10825,20 @@ "area": "Class", "canonical_name": "get_declared_interfaces", "description": "Returns an array of all declared interfaces.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_interfaces.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7271,6 +10868,20 @@ "area": "Class", "canonical_name": "get_declared_traits", "description": "Returns an array of all declared traits.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_declared_traits.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7296,10 +10907,87 @@ "slug": "get_declared_traits", "sub_area": "Class" }, + { + "area": "Class", + "canonical_name": "get_object_vars", + "description": "get_object_vars() is available inside eval'd code via the magician interpreter; compiled (AOT) code does not support it yet.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": true, + "in_catalog": true, + "is_internal": false, + "lowering": { + "checker_file": null, + "checker_line": null, + "codegen_file": null, + "codegen_function": null, + "codegen_line": null, + "notes": [], + "runtime_helpers": [], + "sig_arm": null, + "sig_file": null, + "sig_line": null + }, + "name": "get_object_vars", + "sig": { + "params": [ + { + "by_ref": false, + "default": null, + "name": "object", + "optional": false, + "type": "mixed" + } + ], + "return_type": "mixed", + "variadic": null + }, + "slug": "get_object_vars", + "sub_area": "Class" + }, { "area": "Class", "canonical_name": "get_parent_class", "description": "Returns the name of the parent class of an object or class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_parent_class.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "object_or_class", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7337,6 +11025,27 @@ "area": "Type", "canonical_name": "get_resource_id", "description": "Returns an integer identifier for the given resource.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_id.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "resource", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7374,6 +11083,27 @@ "area": "Type", "canonical_name": "get_resource_type", "description": "Returns the type of a resource.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/get_resource_type.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "resource", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7411,6 +11141,20 @@ "area": "Filesystem", "canonical_name": "getcwd", "description": "Gets the current working directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/getcwd.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7443,6 +11187,27 @@ "area": "Date", "canonical_name": "getdate", "description": "Returns date/time information.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/getdate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7487,6 +11252,27 @@ "area": "Filesystem", "canonical_name": "getenv", "description": "Gets the value of an environment variable.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getenv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "name", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7526,6 +11312,27 @@ "area": "IO", "canonical_name": "gethostbyaddr", "description": "Gets the Internet host name corresponding to a given IP address.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyaddr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7566,6 +11373,27 @@ "area": "IO", "canonical_name": "gethostbyname", "description": "Gets the IPv4 address corresponding to the given Internet host name.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/gethostbyname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hostname", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7606,6 +11434,20 @@ "area": "IO", "canonical_name": "gethostname", "description": "Gets the standard host name for the local machine.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/gethostname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7639,6 +11481,27 @@ "area": "IO", "canonical_name": "getprotobyname", "description": "Gets the protocol number associated with the given protocol name.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getprotobyname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7678,6 +11541,27 @@ "area": "IO", "canonical_name": "getprotobynumber", "description": "Gets the protocol name associated with the given protocol number.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getprotobynumber.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7717,6 +11601,33 @@ "area": "IO", "canonical_name": "getservbyname", "description": "Gets port number associated with an Internet service and protocol.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getservbyname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "service", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7763,6 +11674,33 @@ "area": "IO", "canonical_name": "getservbyport", "description": "Gets the Internet service that corresponds to a port and protocol.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/getservbyport.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "port", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7809,6 +11747,27 @@ "area": "Type", "canonical_name": "gettype", "description": "Returns the type of a variable as a string.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/gettype.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7846,6 +11805,27 @@ "area": "Filesystem", "canonical_name": "glob", "description": "Finds pathnames matching a pattern.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/glob.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7885,6 +11865,33 @@ "area": "Date", "canonical_name": "gmdate", "description": "Formats a GMT/UTC date and time.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/gmdate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -7934,6 +11941,57 @@ "area": "Date", "canonical_name": "gmmktime", "description": "Returns the Unix timestamp for a GMT date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/gmmktime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hour", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "minute", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "second", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "month", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "day", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "year", + "optional": false + } + ], + "required_param_count": 6, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8012,6 +12070,27 @@ "area": "String", "canonical_name": "grapheme_strrev", "description": "Reverses a string by grapheme cluster, returning false on failure.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/grapheme_strrev.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8052,6 +12131,33 @@ "area": "String", "canonical_name": "gzcompress", "description": "Compress a string using the ZLIB data format.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/gzcompress.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "-1", + "name": "level", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8096,6 +12202,33 @@ "area": "String", "canonical_name": "gzdeflate", "description": "Deflate a string using the DEFLATE data format.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/gzdeflate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "-1", + "name": "level", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8140,6 +12273,33 @@ "area": "String", "canonical_name": "gzinflate", "description": "Inflate a deflated string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/gzinflate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "max_length", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8184,6 +12344,33 @@ "area": "String", "canonical_name": "gzuncompress", "description": "Uncompress a compressed string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/gzuncompress.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "max_length", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8230,6 +12417,39 @@ "area": "String", "canonical_name": "hash", "description": "Generates a hash value using the given algorithm.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8283,6 +12503,20 @@ "area": "String", "canonical_name": "hash_algos", "description": "Returns an array of supported hashing algorithm names.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_algos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8315,6 +12549,27 @@ "area": "String", "canonical_name": "hash_copy", "description": "Copies the state of an incremental hashing context.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_copy.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8357,6 +12612,33 @@ "area": "String", "canonical_name": "hash_equals", "description": "Compares two strings using a constant-time algorithm.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_equals.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "known_string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "user_string", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8405,6 +12687,39 @@ "area": "IO", "canonical_name": "hash_file", "description": "Generates a hash value using the contents of a given file.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_file.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8456,6 +12771,33 @@ "area": "String", "canonical_name": "hash_final", "description": "Finalizes an incremental hash and returns the digest string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_final.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8502,6 +12844,45 @@ "area": "String", "canonical_name": "hash_hmac", "description": "Generates a keyed hash value using the HMAC method.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_hmac.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "key", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8563,6 +12944,39 @@ "area": "String", "canonical_name": "hash_init", "description": "Initialize an incremental hashing context.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_init.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "algo", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "\"\"", + "name": "key", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8616,6 +13030,33 @@ "area": "String", "canonical_name": "hash_update", "description": "Pumps data into an active incremental hashing context.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hash_update.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8662,6 +13103,39 @@ "area": "Misc", "canonical_name": "header", "description": "Sends a raw HTTP header.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/header.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "header", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "replace", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "response_code", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8720,6 +13194,27 @@ "area": "String", "canonical_name": "hex2bin", "description": "Decodes a hexadecimal string back into its binary representation.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/hex2bin.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8759,6 +13254,27 @@ "area": "Date", "canonical_name": "hrtime", "description": "Returns the current high-resolution time.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/hrtime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "false", + "name": "as_number", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8803,6 +13319,27 @@ "area": "String", "canonical_name": "html_entity_decode", "description": "Converts HTML entities in a string back into their corresponding characters.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/html_entity_decode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8842,6 +13379,39 @@ "area": "String", "canonical_name": "htmlentities", "description": "Converts all applicable characters in a string into their HTML entities.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/htmlentities.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "11", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "\"UTF-8\"", + "name": "encoding", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8902,6 +13472,39 @@ "area": "String", "canonical_name": "htmlspecialchars", "description": "Converts the HTML special characters in a string into their entities.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/htmlspecialchars.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "11", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "\"UTF-8\"", + "name": "encoding", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -8962,6 +13565,27 @@ "area": "Misc", "canonical_name": "http_response_code", "description": "Gets or sets the HTTP response code.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/http_response_code.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "0", + "name": "response_code", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9005,6 +13629,33 @@ "area": "Math", "canonical_name": "hypot", "description": "Calculates the length of the hypotenuse of a right-angle triangle.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/hypot.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "x", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "y", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9049,6 +13700,33 @@ "area": "String", "canonical_name": "implode", "description": "Joins array elements into a single string using a separator.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/implode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "separator", + "optional": true + }, + { + "by_ref": false, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9093,6 +13771,39 @@ "area": "Array", "canonical_name": "in_array", "description": "Checks if a value exists in an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/in_array.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "strict", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9144,6 +13855,27 @@ "area": "String", "canonical_name": "inet_ntop", "description": "Converts a packed internet address to a human-readable representation.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/inet_ntop.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9183,6 +13915,27 @@ "area": "String", "canonical_name": "inet_pton", "description": "Converts a human-readable IP address to its packed in_addr representation.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/inet_pton.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9222,6 +13975,33 @@ "area": "Math", "canonical_name": "intdiv", "description": "Integer division.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/intdiv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "num2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9266,6 +14046,33 @@ "area": "Class", "canonical_name": "interface_exists", "description": "Checks if the interface has been defined.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/interface_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "interface", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9310,6 +14117,27 @@ "area": "Type", "canonical_name": "intval", "description": "Returns the integer value of a variable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/intval.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9350,6 +14178,27 @@ "area": "String", "canonical_name": "ip2long", "description": "Converts a string containing an IPv4 address into a long integer.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/ip2long.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9390,6 +14239,39 @@ "area": "Class", "canonical_name": "is_a", "description": "Checks whether an object is of a given type or has it as one of its parents.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/is_a.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "allow_string", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9441,6 +14323,27 @@ "area": "Type", "canonical_name": "is_array", "description": "Checks whether a variable is an array.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_array.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9480,6 +14383,27 @@ "area": "Type", "canonical_name": "is_bool", "description": "Checks whether a variable is a boolean.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_bool.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9517,6 +14441,39 @@ "area": "Type", "canonical_name": "is_callable", "description": "Checks whether a variable can be called as a function.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/is_callable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "syntax_only", + "optional": true + }, + { + "by_ref": true, + "default": "null", + "name": "callable_name", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9557,6 +14514,27 @@ "area": "Filesystem", "canonical_name": "is_dir", "description": "Tells whether the filename is a directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_dir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9598,6 +14576,27 @@ "area": "Filesystem", "canonical_name": "is_executable", "description": "Tells whether the filename is executable.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_executable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9640,6 +14639,27 @@ "area": "Filesystem", "canonical_name": "is_file", "description": "Tells whether the filename is a regular file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_file.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9681,6 +14701,27 @@ "area": "Math", "canonical_name": "is_finite", "description": "Checks whether a float is finite.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_finite.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9718,6 +14759,27 @@ "area": "Type", "canonical_name": "is_float", "description": "Checks whether a variable is a floating-point number.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_float.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9755,6 +14817,27 @@ "area": "Math", "canonical_name": "is_infinite", "description": "Checks whether a float is infinite.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_infinite.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9792,6 +14875,27 @@ "area": "Type", "canonical_name": "is_int", "description": "Checks whether a variable is an integer.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_int.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9829,6 +14933,27 @@ "area": "Type", "canonical_name": "is_iterable", "description": "Checks whether a variable is iterable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_iterable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9866,6 +14991,27 @@ "area": "Filesystem", "canonical_name": "is_link", "description": "Tells whether the filename is a symbolic link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_link.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9908,6 +15054,27 @@ "area": "Math", "canonical_name": "is_nan", "description": "Checks whether a float is NAN.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_nan.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9945,6 +15112,27 @@ "area": "Type", "canonical_name": "is_null", "description": "Checks whether a variable is null.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_null.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -9982,6 +15170,27 @@ "area": "Type", "canonical_name": "is_numeric", "description": "Checks whether a variable is a number or a numeric string.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_numeric.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10019,6 +15228,27 @@ "area": "Type", "canonical_name": "is_object", "description": "Checks whether a variable is an object.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_object.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10057,6 +15287,27 @@ "area": "Filesystem", "canonical_name": "is_readable", "description": "Tells whether the filename is readable.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_readable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10098,6 +15349,27 @@ "area": "Type", "canonical_name": "is_resource", "description": "Checks whether a variable is a resource.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_resource.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10135,6 +15407,27 @@ "area": "Type", "canonical_name": "is_scalar", "description": "Checks whether a variable is a scalar.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_scalar.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10174,6 +15467,27 @@ "area": "Type", "canonical_name": "is_string", "description": "Checks whether a variable is a string.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/is_string.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10211,6 +15525,39 @@ "area": "Class", "canonical_name": "is_subclass_of", "description": "Checks if the object has a given class as one of its parents or implements it.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/is_subclass_of.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object_or_class", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "allow_string", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10262,6 +15609,27 @@ "area": "Filesystem", "canonical_name": "is_writable", "description": "Tells whether the filename is writable.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_writable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10296,13 +15664,34 @@ "return_type": "bool", "variadic": null }, - "slug": "is_writable", - "sub_area": "Filesystem" - }, - { - "area": "Filesystem", - "canonical_name": "is_writeable", - "description": "Tells whether the filename is writable (alias of is_writable).", + "slug": "is_writable", + "sub_area": "Filesystem" + }, + { + "area": "Filesystem", + "canonical_name": "is_writeable", + "description": "Tells whether the filename is writable (alias of is_writable).", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/is_writeable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10344,6 +15733,27 @@ "area": "Misc", "canonical_name": "isset", "description": "Determines whether a variable is set and is not null.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/isset.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "var", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "vars" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10381,6 +15791,39 @@ "area": "SPL", "canonical_name": "iterator_apply", "description": "Call a function for every element in an iterator.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/iterator_apply.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "iterator", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "args", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10432,6 +15875,27 @@ "area": "SPL", "canonical_name": "iterator_count", "description": "Count the elements in an iterator.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/iterator_count.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "iterator", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10469,6 +15933,33 @@ "area": "SPL", "canonical_name": "iterator_to_array", "description": "Copy the iterator into an array.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/iterator_to_array.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "iterator", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "preserve_keys", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10513,6 +16004,45 @@ "area": "JSON", "canonical_name": "json_decode", "description": "Decodes a JSON string.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_decode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "json", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "associative", + "optional": true + }, + { + "by_ref": false, + "default": "512", + "name": "depth", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10573,6 +16103,39 @@ "area": "JSON", "canonical_name": "json_encode", "description": "Returns the JSON representation of a value.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_encode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "512", + "name": "depth", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10624,6 +16187,20 @@ "area": "JSON", "canonical_name": "json_last_error", "description": "Returns the last error (if any) occurred during the last JSON encoding/decoding.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_last_error.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10655,6 +16232,20 @@ "area": "JSON", "canonical_name": "json_last_error_msg", "description": "Returns the error string of the last json_encode() or json_decode() call.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_last_error_msg.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10687,6 +16278,39 @@ "area": "JSON", "canonical_name": "json_validate", "description": "Checks if a string contains valid JSON.", + "eval": { + "area": "json", + "home_file": "crates/elephc-magician/src/interpreter/builtins/json/json_validate.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "json", + "optional": false + }, + { + "by_ref": false, + "default": "512", + "name": "depth", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10740,6 +16364,26 @@ "area": "Array", "canonical_name": "krsort", "description": "Sorts an array by key in descending order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/krsort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10781,6 +16425,26 @@ "area": "Array", "canonical_name": "ksort", "description": "Sorts an array by key in ascending order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/ksort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10823,6 +16487,27 @@ "area": "String", "canonical_name": "lcfirst", "description": "Lowercases the first character of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/lcfirst.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10862,6 +16547,33 @@ "area": "Filesystem", "canonical_name": "lchgrp", "description": "Changes group ownership of a symlink.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/lchgrp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "group", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10908,6 +16620,33 @@ "area": "Filesystem", "canonical_name": "lchown", "description": "Changes user ownership of a symlink.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/lchown.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "user", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -10954,6 +16693,33 @@ "area": "Filesystem", "canonical_name": "link", "description": "Creates a hard link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/link.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "target", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "link", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11003,6 +16769,27 @@ "area": "Filesystem", "canonical_name": "linkinfo", "description": "Gets information about a link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/linkinfo.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11045,6 +16832,33 @@ "area": "Date", "canonical_name": "localtime", "description": "Returns the local time.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/localtime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "timestamp", + "optional": true + }, + { + "by_ref": false, + "default": "false", + "name": "associative", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11097,6 +16911,33 @@ "area": "Math", "canonical_name": "log", "description": "Natural logarithm.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/log.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": "2.718281828459045", + "name": "base", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11141,6 +16982,27 @@ "area": "Math", "canonical_name": "log10", "description": "Returns the base-10 logarithm of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/log10.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11178,6 +17040,27 @@ "area": "Math", "canonical_name": "log2", "description": "Returns the base-2 logarithm of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/log2.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11215,6 +17098,27 @@ "area": "String", "canonical_name": "long2ip", "description": "Converts an IPv4 address from long integer to dotted string notation.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/long2ip.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "ip", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11255,6 +17159,27 @@ "area": "Filesystem", "canonical_name": "lstat", "description": "Gives information about a file or symbolic link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/lstat.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11295,6 +17220,33 @@ "area": "String", "canonical_name": "ltrim", "description": "Strips whitespace (or other characters) from the beginning of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ltrim.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\n\\r\\t\\u{b}\\u{c}\\u{0}\"", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11339,6 +17291,27 @@ "area": "Math", "canonical_name": "max", "description": "Find highest value.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/max.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11376,6 +17349,39 @@ "area": "Regex", "canonical_name": "mb_ereg_match", "description": "Tests whether a regex pattern matches the beginning of a string (multibyte).", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/mb_ereg_match.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11431,6 +17437,33 @@ "area": "String", "canonical_name": "md5", "description": "Calculates the MD5 hash of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/md5.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11479,6 +17512,27 @@ "area": "Date", "canonical_name": "microtime", "description": "Returns the current Unix timestamp with microseconds.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/microtime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "false", + "name": "as_float", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11527,6 +17581,27 @@ "area": "Math", "canonical_name": "min", "description": "Find lowest value.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/min.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11564,6 +17639,27 @@ "area": "Filesystem", "canonical_name": "mkdir", "description": "Makes a directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/mkdir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11607,6 +17703,57 @@ "area": "Date", "canonical_name": "mktime", "description": "Returns the Unix timestamp for a date.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/mktime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hour", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "minute", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "second", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "month", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "day", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "year", + "optional": false + } + ], + "required_param_count": 6, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11683,6 +17830,33 @@ "area": "Math", "canonical_name": "mt_rand", "description": "Generate a random value via the Mersenne Twister Random Number Generator.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/mt_rand.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11729,6 +17903,26 @@ "area": "Array", "canonical_name": "natcasesort", "description": "Sorts an array using a case-insensitive natural order algorithm.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/natcasesort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11768,6 +17962,26 @@ "area": "Array", "canonical_name": "natsort", "description": "Sorts an array using a natural order algorithm.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/natsort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11808,6 +18022,33 @@ "area": "String", "canonical_name": "nl2br", "description": "Inserts HTML line breaks before newlines in a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/nl2br.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "use_xhtml", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11847,6 +18088,45 @@ "area": "String", "canonical_name": "number_format", "description": "Formats a number with grouped thousands.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/number_format.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "decimals", + "optional": true + }, + { + "by_ref": false, + "default": "\".\"", + "name": "decimal_separator", + "optional": true + }, + { + "by_ref": false, + "default": "\",\"", + "name": "thousands_separator", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11907,6 +18187,27 @@ "area": "IO", "canonical_name": "opendir", "description": "Open directory handle.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/opendir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11948,6 +18249,27 @@ "area": "String", "canonical_name": "ord", "description": "Returns the ASCII value of the first character of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ord.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "character", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -11985,6 +18307,27 @@ "area": "Process", "canonical_name": "passthru", "description": "Executes an external program and passes its output directly.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/passthru.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12025,6 +18368,33 @@ "area": "Filesystem", "canonical_name": "pathinfo", "description": "Returns information about a file path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/pathinfo.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + }, + { + "by_ref": false, + "default": "15", + "name": "flags", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12071,6 +18441,27 @@ "area": "Process", "canonical_name": "pclose", "description": "Closes process file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/pclose.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "handle", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12110,6 +18501,50 @@ "area": "Streams", "canonical_name": "pfsockopen", "description": "Open persistent Internet or Unix domain socket connection.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/pfsockopen.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "hostname", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "port", + "optional": false + }, + { + "by_ref": true, + "default": "null", + "name": "error_code", + "optional": true + }, + { + "by_ref": true, + "default": "null", + "name": "error_message", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "timeout", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12175,6 +18610,27 @@ "area": "Misc", "canonical_name": "php_uname", "description": "Returns information about the operating system PHP is running on.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/php_uname.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "\"a\"", + "name": "mode", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12214,6 +18670,20 @@ "area": "Misc", "canonical_name": "phpversion", "description": "Returns the current PHP version information.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/phpversion.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12243,6 +18713,20 @@ "area": "Math", "canonical_name": "pi", "description": "Gets value of pi.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/pi.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12272,6 +18756,33 @@ "area": "Process", "canonical_name": "popen", "description": "Opens process file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/popen.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "mode", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12318,6 +18829,33 @@ "area": "Math", "canonical_name": "pow", "description": "Exponential expression.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/pow.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "exponent", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12362,6 +18900,45 @@ "area": "Regex", "canonical_name": "preg_match", "description": "Performs a regular expression match.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_match.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": true, + "default": "[]", + "name": "matches", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12416,6 +18993,45 @@ "area": "Regex", "canonical_name": "preg_match_all", "description": "Performs a global regular expression match and returns the number of matches.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_match_all.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": true, + "default": "[]", + "name": "matches", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12463,6 +19079,39 @@ "area": "Regex", "canonical_name": "preg_replace", "description": "Performs a regular expression search and replace.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_replace.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "replacement", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12516,6 +19165,39 @@ "area": "Regex", "canonical_name": "preg_replace_callback", "description": "Performs a regular expression search and replace using a callback.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_replace_callback.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12569,6 +19251,45 @@ "area": "Regex", "canonical_name": "preg_split", "description": "Splits a string by a regular expression.", + "eval": { + "area": "regex", + "home_file": "crates/elephc-magician/src/interpreter/builtins/regex/preg_split.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pattern", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": false, + "default": "-1", + "name": "limit", + "optional": true + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12629,6 +19350,33 @@ "area": "Misc", "canonical_name": "print_r", "description": "Prints human-readable information about a variable.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/print_r.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "return", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12684,6 +19432,27 @@ "area": "String", "canonical_name": "printf", "description": "Outputs a formatted string.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/printf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12723,6 +19492,27 @@ "area": "Pointer", "canonical_name": "ptr", "description": "Returns a raw pointer to the given variable.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12760,6 +19550,27 @@ "area": "Pointer", "canonical_name": "ptr_get", "description": "Reads one machine word through a raw pointer and returns it as an integer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_get.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12797,6 +19608,27 @@ "area": "Pointer", "canonical_name": "ptr_is_null", "description": "Returns true if the pointer is null.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_is_null.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12834,6 +19666,20 @@ "area": "Pointer", "canonical_name": "ptr_null", "description": "Returns a null raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_null.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12863,6 +19709,33 @@ "area": "Pointer", "canonical_name": "ptr_offset", "description": "Returns a new pointer offset from the given pointer by the given byte count.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_offset.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12907,6 +19780,27 @@ "area": "Pointer", "canonical_name": "ptr_read16", "description": "Reads one unsigned 16-bit word through a raw pointer and returns it as an integer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read16.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12946,6 +19840,27 @@ "area": "Pointer", "canonical_name": "ptr_read32", "description": "Reads one unsigned 32-bit word through a raw pointer and returns it as an integer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read32.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -12985,6 +19900,27 @@ "area": "Pointer", "canonical_name": "ptr_read8", "description": "Reads one unsigned byte through a raw pointer and returns it as an integer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read8.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13022,6 +19958,33 @@ "area": "Pointer", "canonical_name": "ptr_read_string", "description": "Copies raw bytes from a pointer into a PHP string of the given length.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_read_string.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13068,6 +20031,33 @@ "area": "Pointer", "canonical_name": "ptr_set", "description": "Writes one machine word through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_set.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13112,6 +20102,27 @@ "area": "Pointer", "canonical_name": "ptr_sizeof", "description": "Returns the byte size of the named pointer target type.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_sizeof.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "type", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13149,6 +20160,33 @@ "area": "Pointer", "canonical_name": "ptr_write16", "description": "Writes one 16-bit word through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write16.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13195,6 +20233,33 @@ "area": "Pointer", "canonical_name": "ptr_write32", "description": "Writes one 32-bit word through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write32.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13241,6 +20306,33 @@ "area": "Pointer", "canonical_name": "ptr_write8", "description": "Writes one byte through a raw pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write8.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13285,6 +20377,33 @@ "area": "Pointer", "canonical_name": "ptr_write_string", "description": "Copies PHP string bytes into raw memory at the given pointer.", + "eval": { + "area": "raw_memory", + "home_file": "crates/elephc-magician/src/interpreter/builtins/raw_memory/ptr_write_string.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "pointer", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13331,6 +20450,27 @@ "area": "Filesystem", "canonical_name": "putenv", "description": "Sets an environment variable.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/putenv.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "assignment", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13370,6 +20510,27 @@ "area": "Math", "canonical_name": "rad2deg", "description": "Converts a radian value to degrees.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/rad2deg.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13407,6 +20568,33 @@ "area": "Math", "canonical_name": "rand", "description": "Generate a random integer.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/rand.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13453,6 +20641,33 @@ "area": "Math", "canonical_name": "random_int", "description": "Get a cryptographically secure, uniformly selected integer.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/random_int.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "min", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "max", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13497,6 +20712,33 @@ "area": "Array", "canonical_name": "range", "description": "Create an array containing a range of elements.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/range.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "start", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "end", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13544,6 +20786,27 @@ "area": "String", "canonical_name": "rawurldecode", "description": "Decodes an RFC 3986 percent-encoded string without treating '+' as a space.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/rawurldecode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13583,6 +20846,27 @@ "area": "String", "canonical_name": "rawurlencode", "description": "URL-encodes a string using RFC 3986 percent-encoding (no '+' for spaces).", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/rawurlencode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13622,6 +20906,27 @@ "area": "IO", "canonical_name": "readdir", "description": "Read entry from directory handle.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/readdir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "dir_handle", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13664,6 +20969,27 @@ "area": "Filesystem", "canonical_name": "readfile", "description": "Outputs a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/readfile.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13701,6 +21027,27 @@ "area": "Process", "canonical_name": "readline", "description": "Reads a line from the user's terminal.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/readline.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "prompt", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13740,6 +21087,27 @@ "area": "Filesystem", "canonical_name": "readlink", "description": "Returns the target of a symbolic link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/readlink.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13782,6 +21150,27 @@ "area": "Filesystem", "canonical_name": "realpath", "description": "Returns canonicalized absolute pathname.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/realpath.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "path", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13821,6 +21210,20 @@ "area": "Filesystem", "canonical_name": "realpath_cache_get", "description": "Returns realpath cache entries.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_get.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13850,6 +21253,20 @@ "area": "Filesystem", "canonical_name": "realpath_cache_size", "description": "Returns the amount of memory used by the realpath cache.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/realpath_cache_size.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13879,6 +21296,33 @@ "area": "Filesystem", "canonical_name": "rename", "description": "Renames a file or directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/rename.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "from", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "to", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13927,6 +21371,27 @@ "area": "IO", "canonical_name": "rewind", "description": "Rewind the position of a file pointer.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/rewind.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -13964,6 +21429,27 @@ "area": "IO", "canonical_name": "rewinddir", "description": "Rewind directory handle.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/rewinddir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "dir_handle", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14004,6 +21490,27 @@ "area": "Filesystem", "canonical_name": "rmdir", "description": "Removes a directory.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/rmdir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14047,6 +21554,33 @@ "area": "Math", "canonical_name": "round", "description": "Rounds a float.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/round.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "precision", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14091,6 +21625,26 @@ "area": "Array", "canonical_name": "rsort", "description": "Sorts an array in descending order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/rsort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14136,6 +21690,33 @@ "area": "String", "canonical_name": "rtrim", "description": "Strips whitespace (or other characters) from the end of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/rtrim.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\n\\r\\t\\u{b}\\u{c}\\u{0}\"", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14180,6 +21761,27 @@ "area": "Filesystem", "canonical_name": "scandir", "description": "Lists files and directories inside the specified path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/scandir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14220,6 +21822,11 @@ "area": "Misc", "canonical_name": "serialize", "description": "Generates a storable representation of a value.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14264,6 +21871,32 @@ "area": "Type", "canonical_name": "settype", "description": "Sets the type of a variable.", + "eval": { + "area": "types", + "home_file": "crates/elephc-magician/src/interpreter/builtins/types/settype.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "var", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "type", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14308,6 +21941,33 @@ "area": "String", "canonical_name": "sha1", "description": "Calculates the SHA-1 hash of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/sha1.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "binary", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14355,6 +22015,27 @@ "area": "Process", "canonical_name": "shell_exec", "description": "Executes a command via the shell and returns the complete output as a string.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/shell_exec.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14392,6 +22073,26 @@ "area": "Array", "canonical_name": "shuffle", "description": "Shuffles an array into random order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/shuffle.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14431,6 +22132,27 @@ "area": "Math", "canonical_name": "sin", "description": "Returns the sine of a number (radians).", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/sin.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14468,6 +22190,27 @@ "area": "Math", "canonical_name": "sinh", "description": "Returns the hyperbolic sine of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/sinh.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14505,6 +22248,27 @@ "area": "Process", "canonical_name": "sleep", "description": "Delays execution for a number of seconds.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/sleep.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "seconds", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14544,6 +22308,26 @@ "area": "Array", "canonical_name": "sort", "description": "Sorts an array in ascending order.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/sort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14583,13 +22367,40 @@ "return_type": "bool", "variadic": null }, - "slug": "sort", - "sub_area": "Array" - }, - { - "area": "SPL", - "canonical_name": "spl_autoload", - "description": "Default implementation for __autoload().", + "slug": "sort", + "sub_area": "Array" + }, + { + "area": "SPL", + "canonical_name": "spl_autoload", + "description": "Default implementation for __autoload().", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "file_extensions", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14634,6 +22445,27 @@ "area": "SPL", "canonical_name": "spl_autoload_call", "description": "Try all registered __autoload() functions to load the requested class.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_call.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14671,6 +22503,27 @@ "area": "SPL", "canonical_name": "spl_autoload_extensions", "description": "Register and return default file extensions for spl_autoload.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_extensions.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "file_extensions", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14708,6 +22561,20 @@ "area": "SPL", "canonical_name": "spl_autoload_functions", "description": "Return all registered __autoload() functions.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_functions.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14737,6 +22604,39 @@ "area": "SPL", "canonical_name": "spl_autoload_register", "description": "Register given function as __autoload() implementation.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_register.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "callback", + "optional": true + }, + { + "by_ref": false, + "default": "true", + "name": "throw", + "optional": true + }, + { + "by_ref": false, + "default": "false", + "name": "prepend", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14788,6 +22688,27 @@ "area": "SPL", "canonical_name": "spl_autoload_unregister", "description": "Unregister given function as __autoload() implementation.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_autoload_unregister.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14825,6 +22746,20 @@ "area": "SPL", "canonical_name": "spl_classes", "description": "Return available SPL classes.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_classes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14856,6 +22791,27 @@ "area": "SPL", "canonical_name": "spl_object_hash", "description": "Return hash id for given object.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_hash.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14895,6 +22851,27 @@ "area": "SPL", "canonical_name": "spl_object_id", "description": "Return the integer object handle for given object.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/spl_object_id.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "object", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14934,6 +22911,27 @@ "area": "String", "canonical_name": "sprintf", "description": "Returns a formatted string.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/sprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -14973,6 +22971,27 @@ "area": "Math", "canonical_name": "sqrt", "description": "Returns the square root of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/sqrt.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15010,6 +23029,33 @@ "area": "String", "canonical_name": "sscanf", "description": "Parses a string according to a format.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/sscanf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": "vars" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15057,6 +23103,27 @@ "area": "Filesystem", "canonical_name": "stat", "description": "Gives information about a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stat.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15098,6 +23165,33 @@ "area": "String", "canonical_name": "str_contains", "description": "Determines if a string contains a given substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_contains.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15144,6 +23238,33 @@ "area": "String", "canonical_name": "str_ends_with", "description": "Checks if a string ends with a given substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_ends_with.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15190,6 +23311,45 @@ "area": "String", "canonical_name": "str_ireplace", "description": "Case-insensitive version of str_replace().", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_ireplace.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "search", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "replace", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "count", + "optional": true + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15248,6 +23408,45 @@ "area": "String", "canonical_name": "str_pad", "description": "Pads a string to a certain length with another string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_pad.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + }, + { + "by_ref": false, + "default": "\" \"", + "name": "pad_string", + "optional": true + }, + { + "by_ref": false, + "default": "1", + "name": "pad_type", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15308,6 +23507,33 @@ "area": "String", "canonical_name": "str_repeat", "description": "Repeats a string a given number of times.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_repeat.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "times", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15338,22 +23564,61 @@ }, { "by_ref": false, - "default": null, - "name": "times", - "optional": false, - "type": "int" + "default": null, + "name": "times", + "optional": false, + "type": "int" + } + ], + "return_type": "string", + "variadic": null + }, + "slug": "str_repeat", + "sub_area": "String" + }, + { + "area": "String", + "canonical_name": "str_replace", + "description": "Replaces all occurrences of a search string with a replacement string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_replace.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "search", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "replace", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "subject", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "count", + "optional": true } ], - "return_type": "string", + "required_param_count": 3, + "supported": true, "variadic": null }, - "slug": "str_repeat", - "sub_area": "String" - }, - { - "area": "String", - "canonical_name": "str_replace", - "description": "Replaces all occurrences of a search string with a replacement string.", + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15412,6 +23677,33 @@ "area": "String", "canonical_name": "str_split", "description": "Converts a string into an array of chunks of the given length.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_split.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "1", + "name": "length", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15458,6 +23750,33 @@ "area": "String", "canonical_name": "str_starts_with", "description": "Checks if a string starts with a given substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/str_starts_with.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15504,6 +23823,33 @@ "area": "String", "canonical_name": "strcasecmp", "description": "Binary safe case-insensitive string comparison. Returns negative, zero, or positive.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strcasecmp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "string2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15550,6 +23896,33 @@ "area": "String", "canonical_name": "strcmp", "description": "Binary safe string comparison. Returns negative, zero, or positive.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strcmp.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string1", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "string2", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15596,6 +23969,33 @@ "area": "Streams", "canonical_name": "stream_bucket_append", "description": "Appends a bucket to the brigade.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_append.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "brigade", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "bucket", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15640,6 +24040,27 @@ "area": "IO", "canonical_name": "stream_bucket_make_writeable", "description": "Returns a bucket object from the brigade for use in a stream filter.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_make_writeable.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "brigade", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15679,6 +24100,33 @@ "area": "IO", "canonical_name": "stream_bucket_new", "description": "Creates a new bucket for use in a stream filter.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_new.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "buffer", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15723,6 +24171,33 @@ "area": "Streams", "canonical_name": "stream_bucket_prepend", "description": "Prepends a bucket to the brigade.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_bucket_prepend.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "brigade", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "bucket", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15767,6 +24242,33 @@ "area": "IO", "canonical_name": "stream_context_create", "description": "Creates a stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_create.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "params", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15811,6 +24313,27 @@ "area": "IO", "canonical_name": "stream_context_get_default", "description": "Retrieves the default stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_default.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "options", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15848,6 +24371,27 @@ "area": "IO", "canonical_name": "stream_context_get_options", "description": "Retrieves options for the specified stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_options.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15888,6 +24432,27 @@ "area": "IO", "canonical_name": "stream_context_get_params", "description": "Retrieves parameters from the specified stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_get_params.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15925,6 +24490,27 @@ "area": "IO", "canonical_name": "stream_context_set_default", "description": "Sets the default stream context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_default.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "options", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -15962,6 +24548,45 @@ "area": "IO", "canonical_name": "stream_context_set_option", "description": "Sets an option on the specified context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_option.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "wrapper_or_options", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "option_name", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "value", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16020,6 +24645,33 @@ "area": "IO", "canonical_name": "stream_context_set_params", "description": "Sets parameters on the specified context.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_context_set_params.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "context", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "params", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16048,22 +24700,61 @@ }, { "by_ref": false, - "default": null, - "name": "params", - "optional": false, - "type": "array" + "default": null, + "name": "params", + "optional": false, + "type": "array" + } + ], + "return_type": "bool", + "variadic": null + }, + "slug": "stream_context_set_params", + "sub_area": "IO" + }, + { + "area": "IO", + "canonical_name": "stream_copy_to_stream", + "description": "Copies data from one stream to another.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_copy_to_stream.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "from", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "to", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + }, + { + "by_ref": false, + "default": "-1", + "name": "offset", + "optional": true } ], - "return_type": "bool", + "required_param_count": 2, + "supported": true, "variadic": null }, - "slug": "stream_context_set_params", - "sub_area": "IO" - }, - { - "area": "IO", - "canonical_name": "stream_copy_to_stream", - "description": "Copies data from one stream to another.", + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16122,6 +24813,45 @@ "area": "Streams", "canonical_name": "stream_filter_append", "description": "Attaches a filter to a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_append.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "filtername", + "optional": false + }, + { + "by_ref": false, + "default": "3", + "name": "read_write", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "params", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16180,6 +24910,45 @@ "area": "Streams", "canonical_name": "stream_filter_prepend", "description": "Attaches a filter to a stream (prepend).", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_prepend.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "filtername", + "optional": false + }, + { + "by_ref": false, + "default": "3", + "name": "read_write", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "params", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16238,6 +25007,33 @@ "area": "IO", "canonical_name": "stream_filter_register", "description": "Registers a user-defined stream filter.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_register.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filter_name", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16284,6 +25080,27 @@ "area": "IO", "canonical_name": "stream_filter_remove", "description": "Removes a filter from a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_filter_remove.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream_filter", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16323,6 +25140,39 @@ "area": "IO", "canonical_name": "stream_get_contents", "description": "Reads remainder of a stream into a string.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_contents.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + }, + { + "by_ref": false, + "default": "-1", + "name": "offset", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16374,6 +25224,20 @@ "area": "IO", "canonical_name": "stream_get_filters", "description": "Retrieves list of registered filters.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_get_filters.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16403,6 +25267,39 @@ "area": "IO", "canonical_name": "stream_get_line", "description": "Gets line from stream resource up to a given delimiter.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_line.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + }, + { + "by_ref": false, + "default": "\"\"", + "name": "ending", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16454,6 +25351,27 @@ "area": "IO", "canonical_name": "stream_get_meta_data", "description": "Retrieves metadata from streams/file pointers.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_get_meta_data.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16493,6 +25411,20 @@ "area": "IO", "canonical_name": "stream_get_transports", "description": "Retrieves list of registered socket transports.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_get_transports.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16522,6 +25454,20 @@ "area": "IO", "canonical_name": "stream_get_wrappers", "description": "Retrieves list of registered streams.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_get_wrappers.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16551,6 +25497,27 @@ "area": "IO", "canonical_name": "stream_is_local", "description": "Checks if a stream is a local stream.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_is_local.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16588,6 +25555,27 @@ "area": "IO", "canonical_name": "stream_isatty", "description": "Checks if a stream is a TTY.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_isatty.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16627,6 +25615,27 @@ "area": "IO", "canonical_name": "stream_resolve_include_path", "description": "Resolves filename against the include path.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_resolve_include_path.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16667,6 +25676,50 @@ "area": "IO", "canonical_name": "stream_select", "description": "Runs the equivalent of the select() system call on the given arrays of streams.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_select.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "read", + "optional": false + }, + { + "by_ref": true, + "default": null, + "name": "write", + "optional": false + }, + { + "by_ref": true, + "default": null, + "name": "except", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "seconds", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "microseconds", + "optional": true + } + ], + "required_param_count": 4, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16732,6 +25785,33 @@ "area": "IO", "canonical_name": "stream_set_blocking", "description": "Sets blocking/non-blocking mode on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_blocking.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "enable", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16779,6 +25859,33 @@ "area": "IO", "canonical_name": "stream_set_chunk_size", "description": "Sets the read chunk size on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_chunk_size.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16823,6 +25930,33 @@ "area": "IO", "canonical_name": "stream_set_read_buffer", "description": "Sets the read file buffering on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_read_buffer.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16867,6 +26001,39 @@ "area": "IO", "canonical_name": "stream_set_timeout", "description": "Sets timeout period on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_timeout.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "seconds", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "microseconds", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16918,6 +26085,33 @@ "area": "IO", "canonical_name": "stream_set_write_buffer", "description": "Sets the write file buffering on a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_set_write_buffer.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "size", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -16962,6 +26156,38 @@ "area": "IO", "canonical_name": "stream_socket_accept", "description": "Accept a connection on a socket created by stream_socket_server().", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_accept.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "socket", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "timeout", + "optional": true + }, + { + "by_ref": true, + "default": "null", + "name": "peer_name", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17015,6 +26241,27 @@ "area": "IO", "canonical_name": "stream_socket_client", "description": "Open Internet or Unix domain socket connection.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_client.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "address", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17055,6 +26302,45 @@ "area": "IO", "canonical_name": "stream_socket_enable_crypto", "description": "Turns encryption on/off on an already connected socket.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_enable_crypto.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "enable", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "crypto_method", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "session_stream", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17113,6 +26399,33 @@ "area": "IO", "canonical_name": "stream_socket_get_name", "description": "Retrieve the name of the local or remote sockets.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_get_name.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "socket", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "remote", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17159,6 +26472,39 @@ "area": "IO", "canonical_name": "stream_socket_pair", "description": "Creates a pair of connected, indistinguishable socket streams.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_pair.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "domain", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "type", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17212,6 +26558,44 @@ "area": "IO", "canonical_name": "stream_socket_recvfrom", "description": "Receives data from a socket, connected or not.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_recvfrom.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "socket", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "length", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + }, + { + "by_ref": true, + "default": "\"\"", + "name": "address", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17270,6 +26654,45 @@ "area": "IO", "canonical_name": "stream_socket_sendto", "description": "Sends a message to a socket, whether it is connected or not.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_sendto.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "socket", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "data", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + }, + { + "by_ref": false, + "default": "\"\"", + "name": "address", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17328,6 +26751,27 @@ "area": "IO", "canonical_name": "stream_socket_server", "description": "Create an Internet or Unix domain server socket.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_server.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "address", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17367,6 +26811,33 @@ "area": "IO", "canonical_name": "stream_socket_shutdown", "description": "Shutdown a full-duplex connection.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_socket_shutdown.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "mode", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17413,6 +26884,27 @@ "area": "IO", "canonical_name": "stream_supports_lock", "description": "Tells whether the stream supports locking.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stream_supports_lock.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17452,6 +26944,39 @@ "area": "IO", "canonical_name": "stream_wrapper_register", "description": "Registers a URL wrapper implemented as a PHP class.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_register.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "class", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "flags", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17505,6 +27030,27 @@ "area": "IO", "canonical_name": "stream_wrapper_restore", "description": "Restores a previously unregistered built-in wrapper.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_restore.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17542,6 +27088,27 @@ "area": "IO", "canonical_name": "stream_wrapper_unregister", "description": "Unregisters a previously registered URL wrapper.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/stream_wrapper_unregister.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "protocol", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17581,6 +27148,27 @@ "area": "String", "canonical_name": "stripslashes", "description": "Removes backslashes from a string previously escaped by addslashes.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/stripslashes.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17620,6 +27208,27 @@ "area": "String", "canonical_name": "strlen", "description": "Returns the length of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strlen.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17659,6 +27268,39 @@ "area": "String", "canonical_name": "strpos", "description": "Finds the numeric position of the first occurrence of a substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strpos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17710,6 +27352,27 @@ "area": "String", "canonical_name": "strrev", "description": "Reverses a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strrev.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17749,6 +27412,39 @@ "area": "String", "canonical_name": "strrpos", "description": "Finds the numeric position of the last occurrence of a substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strrpos.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": "0", + "name": "offset", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17800,6 +27496,39 @@ "area": "String", "canonical_name": "strstr", "description": "Returns the portion of a string starting at the first occurrence of a substring.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strstr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "haystack", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "needle", + "optional": false + }, + { + "by_ref": false, + "default": "false", + "name": "before_needle", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17851,6 +27580,27 @@ "area": "String", "canonical_name": "strtolower", "description": "Converts a string to lowercase.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strtolower.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17890,6 +27640,33 @@ "area": "Date", "canonical_name": "strtotime", "description": "Parses an English textual datetime description into a Unix timestamp.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/strtotime.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "datetime", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "baseTimestamp", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17942,6 +27719,27 @@ "area": "String", "canonical_name": "strtoupper", "description": "Converts a string to uppercase.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/strtoupper.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -17981,6 +27779,39 @@ "area": "String", "canonical_name": "substr", "description": "Returns a portion of a string specified by the offset and length.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/substr.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18034,6 +27865,45 @@ "area": "String", "canonical_name": "substr_replace", "description": "Replaces text within a portion of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/substr_replace.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "replace", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "offset", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "length", + "optional": true + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18095,6 +27965,33 @@ "area": "Filesystem", "canonical_name": "symlink", "description": "Creates a symbolic link.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/symlink.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "target", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "link", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18144,6 +28041,20 @@ "area": "Filesystem", "canonical_name": "sys_get_temp_dir", "description": "Returns the directory path used for temporary files.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/sys_get_temp_dir.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18175,6 +28086,27 @@ "area": "Process", "canonical_name": "system", "description": "Executes an external program and displays the output.", + "eval": { + "area": "network_env", + "home_file": "crates/elephc-magician/src/interpreter/builtins/network_env/system.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "command", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18214,6 +28146,27 @@ "area": "Math", "canonical_name": "tan", "description": "Returns the tangent of a number (radians).", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/tan.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18251,6 +28204,27 @@ "area": "Math", "canonical_name": "tanh", "description": "Returns the hyperbolic tangent of a number.", + "eval": { + "area": "math", + "home_file": "crates/elephc-magician/src/interpreter/builtins/math/tanh.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "num", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18288,6 +28262,33 @@ "area": "Filesystem", "canonical_name": "tempnam", "description": "Creates a file with a unique filename.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/tempnam.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "directory", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "prefix", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18336,6 +28337,20 @@ "area": "Date", "canonical_name": "time", "description": "Returns the current Unix timestamp.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/time.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18367,6 +28382,20 @@ "area": "Filesystem", "canonical_name": "tmpfile", "description": "Creates a temporary file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/tmpfile.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18393,13 +28422,46 @@ "return_type": "mixed", "variadic": null }, - "slug": "tmpfile", - "sub_area": "Filesystem" - }, - { - "area": "Filesystem", - "canonical_name": "touch", - "description": "Sets access and modification time of a file.", + "slug": "tmpfile", + "sub_area": "Filesystem" + }, + { + "area": "Filesystem", + "canonical_name": "touch", + "description": "Sets access and modification time of a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/touch.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + }, + { + "by_ref": false, + "default": "null", + "name": "mtime", + "optional": true + }, + { + "by_ref": false, + "default": "null", + "name": "atime", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18451,6 +28513,33 @@ "area": "Class", "canonical_name": "trait_exists", "description": "Checks whether the trait exists.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/trait_exists.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "trait", + "optional": false + }, + { + "by_ref": false, + "default": "true", + "name": "autoload", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18495,6 +28584,33 @@ "area": "String", "canonical_name": "trim", "description": "Strips whitespace (or other characters) from the beginning and end of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/trim.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\n\\r\\t\\u{b}\\u{c}\\u{0}\"", + "name": "characters", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18539,6 +28655,32 @@ "area": "Array", "canonical_name": "uasort", "description": "Sorts an array with a user-defined comparison function and maintains index association.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/uasort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18585,6 +28727,27 @@ "area": "String", "canonical_name": "ucfirst", "description": "Uppercases the first character of a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ucfirst.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18624,6 +28787,33 @@ "area": "String", "canonical_name": "ucwords", "description": "Uppercases the first character of each word in a string.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/ucwords.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "\" \\t\\r\\n\\u{c}\\u{b}\"", + "name": "separators", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18670,6 +28860,32 @@ "area": "Array", "canonical_name": "uksort", "description": "Sorts an array by keys using a user-defined comparison function.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/uksort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18716,6 +28932,27 @@ "area": "Filesystem", "canonical_name": "umask", "description": "Changes the current umask.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/umask.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": "null", + "name": "mask", + "optional": true + } + ], + "required_param_count": 0, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18755,6 +28992,27 @@ "area": "Filesystem", "canonical_name": "unlink", "description": "Deletes a file.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/unlink.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "filename", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18796,6 +29054,11 @@ "area": "Misc", "canonical_name": "unserialize", "description": "Creates a PHP value from a stored representation.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18847,6 +29110,27 @@ "area": "Misc", "canonical_name": "unset", "description": "Unsets the given variables.", + "eval": { + "area": "symbols", + "home_file": "crates/elephc-magician/src/interpreter/builtins/symbols/unset.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "var", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "vars" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18884,6 +29168,27 @@ "area": "String", "canonical_name": "urldecode", "description": "Decodes a URL-encoded string, including '+' as a space.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/urldecode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18923,6 +29228,27 @@ "area": "String", "canonical_name": "urlencode", "description": "URL-encodes a string using application/x-www-form-urlencoded rules.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/urlencode.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -18962,6 +29288,27 @@ "area": "Process", "canonical_name": "usleep", "description": "Delays execution for a number of microseconds.", + "eval": { + "area": "time", + "home_file": "crates/elephc-magician/src/interpreter/builtins/time/usleep.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "microseconds", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19001,6 +29348,32 @@ "area": "Array", "canonical_name": "usort", "description": "Sorts an array by values using a user-defined comparison function.", + "eval": { + "area": "array", + "home_file": "crates/elephc-magician/src/interpreter/builtins/array/usort.rs", + "hooks": [ + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": true, + "default": null, + "name": "array", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "callback", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19047,6 +29420,27 @@ "area": "Misc", "canonical_name": "var_dump", "description": "Dumps information about a variable, including its type and value.", + "eval": { + "area": "core", + "home_file": "crates/elephc-magician/src/interpreter/builtins/core/var_dump.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "value", + "optional": false + } + ], + "required_param_count": 1, + "supported": true, + "variadic": "values" + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19085,6 +29479,39 @@ "area": "IO", "canonical_name": "vfprintf", "description": "Write a formatted string to a stream.", + "eval": { + "area": "filesystem", + "home_file": "crates/elephc-magician/src/interpreter/builtins/filesystem/vfprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "stream", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "values", + "optional": false + } + ], + "required_param_count": 3, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19139,6 +29566,33 @@ "area": "String", "canonical_name": "vprintf", "description": "Outputs a formatted string using an array of values.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/vprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "values", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19185,6 +29639,33 @@ "area": "String", "canonical_name": "vsprintf", "description": "Returns a formatted string using an array of values.", + "eval": { + "area": "formatting", + "home_file": "crates/elephc-magician/src/interpreter/builtins/formatting/vsprintf.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "format", + "optional": false + }, + { + "by_ref": false, + "default": null, + "name": "values", + "optional": false + } + ], + "required_param_count": 2, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19231,6 +29712,45 @@ "area": "String", "canonical_name": "wordwrap", "description": "Wraps a string to a given number of characters.", + "eval": { + "area": "string", + "home_file": "crates/elephc-magician/src/interpreter/builtins/string/wordwrap.rs", + "hooks": [ + "direct", + "values" + ], + "kind": "registry", + "params": [ + { + "by_ref": false, + "default": null, + "name": "string", + "optional": false + }, + { + "by_ref": false, + "default": "75", + "name": "width", + "optional": true + }, + { + "by_ref": false, + "default": "\"\\n\"", + "name": "break", + "optional": true + }, + { + "by_ref": false, + "default": "false", + "name": "cut_long_words", + "optional": true + } + ], + "required_param_count": 1, + "supported": true, + "variadic": null + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19292,6 +29812,11 @@ "area": "Pointer", "canonical_name": "zval_free", "description": "Frees a PHP zval pointer allocated by `zval_pack`.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19331,6 +29856,11 @@ "area": "Pointer", "canonical_name": "zval_pack", "description": "Packs an elephc runtime value into a heap-allocated PHP zval pointer.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19379,6 +29909,11 @@ "area": "Pointer", "canonical_name": "zval_type", "description": "Returns the PHP zval type byte for a zval pointer.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { @@ -19419,6 +29954,11 @@ "area": "Pointer", "canonical_name": "zval_unpack", "description": "Unpacks a PHP zval pointer into an owned elephc Mixed value.", + "eval": { + "kind": "none", + "supported": false + }, + "eval_only": false, "in_catalog": true, "is_internal": false, "lowering": { From 92378b590f8ffe86aa084289208fc996dcd35d0a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sat, 11 Jul 2026 21:54:43 +0200 Subject: [PATCH 1193/1208] fix(ci): address eval regressions and slow tests --- .config/nextest.toml | 35 +++++++++++++++++++ .github/workflows/ci.yml | 6 ++-- .../interpreter/builtins/registry/callable.rs | 20 +++++++++++ .../src/interpreter/dynamic_functions.rs | 35 +++++++++++++++---- .../src/interpreter/statements.rs | 4 +-- src/codegen/lower_inst/builtins/eval.rs | 4 +++ src/codegen/lower_inst/objects.rs | 4 +++ 7 files changed, 96 insertions(+), 12 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 49ebc2c48c..6d62b9a48b 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -56,3 +56,38 @@ slow-timeout = { period = "180s", terminate-after = 1 } [[profile.ci.overrides]] filter = 'test(lowers_examples_corpus)' slow-timeout = { period = "180s", terminate-after = 1 } + +# These eval/codegen regression tests intentionally exercise several complete +# compile-link-run programs in one test. They exceed the 60s global guard on +# one or more supported runners without being hung. +[[profile.default.overrides]] +filter = 'test(ir_backend_handles_scalar_builtins)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.ci.overrides]] +filter = 'test(ir_backend_handles_scalar_builtins)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.default.overrides]] +filter = 'test(test_eval_rejects_invalid_property_and_parameter_type_atoms)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.ci.overrides]] +filter = 'test(test_eval_rejects_invalid_property_and_parameter_type_atoms)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.default.overrides]] +filter = 'test(test_eval_declared_inherited_property_redeclaration_contracts)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.ci.overrides]] +filter = 'test(test_eval_declared_inherited_property_redeclaration_contracts)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.default.overrides]] +filter = 'test(test_eval_aot_callable_named_ref_arg_prep_fatal_cleans_up_stack)' +slow-timeout = { period = "180s", terminate-after = 1 } + +[[profile.ci.overrides]] +filter = 'test(test_eval_aot_callable_named_ref_arg_prep_fatal_cleans_up_stack)' +slow-timeout = { period = "180s", terminate-after = 1 } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eaa09e84d5..6a993c6b02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -233,7 +233,7 @@ jobs: name: Non-Codegen Tests (macos-aarch64) needs: build-archive-macos-aarch64 runs-on: macos-14 - timeout-minutes: 30 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 @@ -262,7 +262,7 @@ jobs: name: Non-Codegen Tests (linux-x86_64) needs: build-archive-linux-x86_64 runs-on: ubuntu-24.04 - timeout-minutes: 30 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 @@ -302,7 +302,7 @@ jobs: name: Non-Codegen Tests (linux-aarch64) needs: build-archive-linux-aarch64 runs-on: ubuntu-24.04-arm - timeout-minutes: 30 + timeout-minutes: 45 steps: - uses: actions/checkout@v4 diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index c72eab8d75..414f954117 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -1070,6 +1070,16 @@ fn eval_object_method_call_user_func_result( if method.is_static() || method.is_abstract() { return Ok(None); } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { + return eval_magic_instance_method_call( + object, + &called_class_name, + method_name, + evaluated_args, + context, + values, + ); + } let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); return eval_dynamic_method_with_values_and_ref_mode( &declaring_class, @@ -1283,6 +1293,16 @@ fn eval_static_method_call_user_func_result( if !method.is_static() || method.is_abstract() { return Ok(None); } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { + return eval_magic_static_method_call( + &dispatch_class, + &called_class, + method_name, + evaluated_args, + context, + values, + ); + } let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); return eval_dynamic_static_method_with_values_and_ref_mode( &declaring_class, diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index ffda7c1100..8cc9141b2d 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -803,8 +803,15 @@ fn stage_native_function_invoker_args( continue; } let original = bound_arg.value; - let mut slot = Box::new(original); - let marker = values.invoker_ref_cell(slot.as_mut() as *mut RuntimeCellHandle)?; + let retained = values.retain(original)?; + let mut slot = Box::new(retained); + let marker = match values.invoker_ref_cell(slot.as_mut() as *mut RuntimeCellHandle) { + Ok(marker) => marker, + Err(status) => { + values.release(retained)?; + return Err(status); + } + }; invoker_values.push(marker); ref_slots.push(BoundNativeFunctionRefSlot::Mixed { original, @@ -2438,8 +2445,10 @@ fn cleanup_native_function_ref_args( values.release_raw_heap_word(*original)?; } } - BoundNativeFunctionRefSlot::Mixed { .. } - | BoundNativeFunctionRefSlot::RawWord { .. } => {} + BoundNativeFunctionRefSlot::Mixed { slot, .. } => { + values.release(**slot)?; + } + BoundNativeFunctionRefSlot::RawWord { .. } => {} } } Ok(()) @@ -2460,22 +2469,34 @@ fn write_back_native_function_ref_args( } => { let value = **slot; if value == *original { + values.release(value)?; continue; } let Some(target) = target else { + values.release(value)?; continue; }; - let current = eval_reference_target_value(target, context, values)?; + let current = match eval_reference_target_value(target, context, values) { + Ok(current) => current, + Err(status) => { + values.release(value)?; + return Err(status); + } + }; if current == value { + values.release(value)?; continue; } - eval_write_direct_ref_target( + if let Err(status) = eval_write_direct_ref_target( target, value, context, values, Some(ScopeCellOwnership::Owned), - )?; + ) { + values.release(value)?; + return Err(status); + } } BoundNativeFunctionRefSlot::RawWord { tag, diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index c6fd196a12..99a126dd16 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -9775,7 +9775,7 @@ pub(in crate::interpreter) fn eval_dynamic_class_native_method_metadata( } /// Dispatches a missing or inaccessible eval instance method through `__call()`. -fn eval_magic_instance_method_call( +pub(in crate::interpreter) fn eval_magic_instance_method_call( object: RuntimeCellHandle, called_class_name: &str, method_name: &str, @@ -9803,7 +9803,7 @@ fn eval_magic_instance_method_call( } /// Dispatches a missing or inaccessible eval static method through `__callStatic()`. -fn eval_magic_static_method_call( +pub(in crate::interpreter) fn eval_magic_static_method_call( class_name: &str, called_class_name: &str, method_name: &str, diff --git a/src/codegen/lower_inst/builtins/eval.rs b/src/codegen/lower_inst/builtins/eval.rs index c3a8415c18..ed711e9350 100644 --- a/src/codegen/lower_inst/builtins/eval.rs +++ b/src/codegen/lower_inst/builtins/eval.rs @@ -4814,6 +4814,10 @@ fn emit_eval_literal_aot_scope_load(ctx: &mut FunctionContext<'_>, name: &str, s emit_branch_if_scope_entry_missing_at(ctx, out_flags_offset, &missing); let result_reg = abi::int_result_reg(ctx.emitter); abi::emit_load_temporary_stack_slot(ctx.emitter, result_reg, out_cell_offset); + let retain_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if retain_arg != result_reg { + abi::emit_reg_move(ctx.emitter, retain_arg, result_reg); + } let retain = ctx .emitter .target diff --git a/src/codegen/lower_inst/objects.rs b/src/codegen/lower_inst/objects.rs index ee1f88c5e0..80e0bba195 100644 --- a/src/codegen/lower_inst/objects.rs +++ b/src/codegen/lower_inst/objects.rs @@ -4321,6 +4321,10 @@ fn emit_clone_dynamic_property_hash( } abi::emit_push_reg(ctx.emitter, source_reg); abi::emit_push_reg(ctx.emitter, dest_reg); + let hash_arg = abi::int_arg_reg_name(ctx.emitter.target, 0); + if hash_arg != result_reg { + abi::emit_reg_move(ctx.emitter, hash_arg, result_reg); + } abi::emit_call_label(ctx.emitter, "__rt_hash_clone_shallow"); abi::emit_pop_reg(ctx.emitter, dest_reg); abi::emit_pop_reg(ctx.emitter, source_reg); From 3cd115baa30c214da985acc9bab1edfc16d1c218 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 12 Jul 2026 09:25:34 +0200 Subject: [PATCH 1194/1208] fix(eval): preserve invokable object callback access --- .../interpreter/builtins/registry/callable.rs | 31 +++++++++++++++++-- .../src/interpreter/tests/dynamic_calls.rs | 13 ++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index 414f954117..ff4c484f6e 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -13,6 +13,15 @@ use super::*; +/// Distinguishes PHP's invokable-object callback form from an explicit method callback. +#[derive(Clone, Copy, PartialEq, Eq)] +enum EvalObjectCallbackKind { + /// A bare object callback whose `__invoke` magic method is callable regardless of visibility. + InvokableObject, + /// An explicit object-method callback that must pass normal visibility validation. + Method, +} + /// Dispatches `call_user_func_array` with optional lexical scope for special class receivers. pub(in crate::interpreter) fn eval_call_user_func_array_with_values_from_scope( callback: RuntimeCellHandle, @@ -685,6 +694,7 @@ fn eval_evaluated_callable_with_call_user_func_values( None => eval_object_method_with_call_user_func_values( *object, method, + EvalObjectCallbackKind::Method, evaluated_args, context, values, @@ -793,6 +803,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_by_value_call_args( None => eval_object_method_with_call_user_func_args( *object, method, + EvalObjectCallbackKind::Method, evaluated_args, context, values, @@ -980,6 +991,7 @@ fn eval_invokable_object_with_call_user_func_values( eval_object_method_with_call_user_func_values( object, "__invoke", + EvalObjectCallbackKind::InvokableObject, evaluated_args, context, values, @@ -993,13 +1005,21 @@ fn eval_invokable_object_with_call_user_func_args( context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, ) -> Result { - eval_object_method_with_call_user_func_args(object, "__invoke", evaluated_args, context, values) + eval_object_method_with_call_user_func_args( + object, + "__invoke", + EvalObjectCallbackKind::InvokableObject, + evaluated_args, + context, + values, + ) } /// Invokes an object-method callable through `call_user_func()` by-value semantics. fn eval_object_method_with_call_user_func_values( object: RuntimeCellHandle, method: &str, + callback_kind: EvalObjectCallbackKind, evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -1008,6 +1028,7 @@ fn eval_object_method_with_call_user_func_values( if let Some(result) = eval_object_method_call_user_func_result( object, method, + callback_kind, evaluated_args.clone(), context, values, @@ -1021,6 +1042,7 @@ fn eval_object_method_with_call_user_func_values( fn eval_object_method_with_call_user_func_args( object: RuntimeCellHandle, method: &str, + callback_kind: EvalObjectCallbackKind, evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -1028,6 +1050,7 @@ fn eval_object_method_with_call_user_func_args( if let Some(result) = eval_object_method_call_user_func_result( object, method, + callback_kind, evaluated_args.clone(), context, values, @@ -1041,6 +1064,7 @@ fn eval_object_method_with_call_user_func_args( fn eval_object_method_call_user_func_result( object: RuntimeCellHandle, method_name: &str, + callback_kind: EvalObjectCallbackKind, evaluated_args: Vec, context: &mut ElephcEvalContext, values: &mut impl RuntimeValueOps, @@ -1070,7 +1094,9 @@ fn eval_object_method_call_user_func_result( if method.is_static() || method.is_abstract() { return Ok(None); } - if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { + if callback_kind == EvalObjectCallbackKind::Method + && validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + { return eval_magic_instance_method_call( object, &called_class_name, @@ -1481,6 +1507,7 @@ pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( None => eval_object_method_with_call_user_func_args( *object, method, + EvalObjectCallbackKind::Method, evaluated_args, context, values, diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs index e74f85e1ab..8aca8ba05a 100644 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs @@ -512,6 +512,11 @@ class EvalPrivateCallbackArray { return "bad"; } } +class EvalPrivateInvokeCallbackArray { + private function __invoke() { + return "bad"; + } +} class EvalInstanceCallbackArray { public function inst() { return "bad"; @@ -539,6 +544,13 @@ try { echo get_class($e) . ":" . $e->getMessage(); } echo "|"; +try { + call_user_func([new EvalPrivateInvokeCallbackArray(), "__invoke"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; try { call_user_func(["EvalInstanceCallbackArray", "inst"]); echo "bad"; @@ -558,6 +570,7 @@ return true;"#, "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"MiSsInG\"|\ TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"missing\"|\ TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateCallbackArray::hidden()|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateInvokeCallbackArray::__invoke()|\ TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, non-static method EvalInstanceCallbackArray::inst() cannot be called statically" ); assert_eq!(values.get(result), FakeValue::Bool(true)); From 4d89bcdd01048a261993aed99741e85b63db9958 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 12 Jul 2026 09:25:42 +0200 Subject: [PATCH 1195/1208] fix(ci): trust archived bridge staticlibs in shards --- .config/nextest.toml | 3 +++ .github/workflows/ci.yml | 19 ++++++++++++++++--- tests/codegen/support/runner.rs | 15 ++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 6d62b9a48b..ba6a5dceea 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -32,6 +32,9 @@ relative-to = "target" path = "debug/libelephc_image.a" relative-to = "target" [[profile.ci.archive.include]] +path = "debug/libelephc_magician.a" +relative-to = "target" +[[profile.ci.archive.include]] path = "debug/libelephc_web.a" relative-to = "target" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a993c6b02..07da5f972e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ env: -p elephc-phar -p elephc-tz -p elephc-image + -p elephc-magician -p elephc-web jobs: @@ -66,7 +67,8 @@ jobs: # staticlibs: libelephc_tls.a (https:// / TLS), libelephc_pdo.a (PDO), # libelephc_crypto.a (hash / HMAC family), libelephc_phar.a (PHAR), # libelephc_tz.a (DateTimeZone introspection), libelephc_image.a - # (GD / Exif / Imagick / Gmagick / Cairo), and libelephc_web.a. Build + # (GD / Exif / Imagick / Gmagick / Cairo), libelephc_magician.a (eval), + # and libelephc_web.a. Build # them before archiving so the archive can capture the .a files (see the # archive include list in .config/nextest.toml). run: cargo build $BRIDGE_CRATES @@ -137,7 +139,8 @@ jobs: # staticlibs: libelephc_tls.a (https:// / TLS), libelephc_pdo.a (PDO), # libelephc_crypto.a (hash / HMAC family), libelephc_phar.a (PHAR), # libelephc_tz.a (DateTimeZone introspection), libelephc_image.a - # (GD / Exif / Imagick / Gmagick / Cairo), and libelephc_web.a. Build + # (GD / Exif / Imagick / Gmagick / Cairo), libelephc_magician.a (eval), + # and libelephc_web.a. Build # them before archiving so the archive can capture the .a files (see the # archive include list in .config/nextest.toml). run: cargo build $BRIDGE_CRATES @@ -208,7 +211,8 @@ jobs: # staticlibs: libelephc_tls.a (https:// / TLS), libelephc_pdo.a (PDO), # libelephc_crypto.a (hash / HMAC family), libelephc_phar.a (PHAR), # libelephc_tz.a (DateTimeZone introspection), libelephc_image.a - # (GD / Exif / Imagick / Gmagick / Cairo), and libelephc_web.a. Build + # (GD / Exif / Imagick / Gmagick / Cairo), libelephc_magician.a (eval), + # and libelephc_web.a. Build # them before archiving so the archive can capture the .a files (see the # archive include list in .config/nextest.toml). run: cargo build $BRIDGE_CRATES @@ -363,6 +367,9 @@ jobs: path: ${{ runner.temp }} - name: Run codegen test shard + env: + CARGO_NET_OFFLINE: "true" + ELEPHC_TEST_PREBUILT_BRIDGES: "1" run: | cargo nextest run --profile ci \ --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ @@ -408,6 +415,9 @@ jobs: path: ${{ runner.temp }} - name: Run codegen test shard + env: + CARGO_NET_OFFLINE: "true" + ELEPHC_TEST_PREBUILT_BRIDGES: "1" run: | cargo nextest run --profile ci \ --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ @@ -453,6 +463,9 @@ jobs: path: ${{ runner.temp }} - name: Run codegen test shard + env: + CARGO_NET_OFFLINE: "true" + ELEPHC_TEST_PREBUILT_BRIDGES: "1" run: | cargo nextest run --profile ci \ --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ diff --git a/tests/codegen/support/runner.rs b/tests/codegen/support/runner.rs index a499daeb97..9d0c9583e1 100644 --- a/tests/codegen/support/runner.rs +++ b/tests/codegen/support/runner.rs @@ -6,6 +6,8 @@ //! //! Key details: //! - Handles platform-specific linker flags, qemu ARM64 execution, and runtime object caching. +//! - Archived CI shards trust the bridge staticlibs packaged by the build job, +//! avoiding source-mtime rebuilds and network access on test runners. //! - Per-test assembly is fed to `as` over stdin so no intermediate `test.s` //! file is written, which shaves ~1/3 of the file-system events the macOS //! `syspolicyd` / on-access AV scans inspect during a full `cargo test`. @@ -198,12 +200,16 @@ fn ensure_bridge_staticlibs(actual_link_libs: &[&str], bridge_staticlib_dir: &st /// Reports whether a bridge staticlib is missing or older than its package /// sources. This keeps codegen tests from linking stale bridge archives after a -/// bridge crate changes inside the same worktree. +/// bridge crate changes inside the same worktree. Archived CI runs can declare +/// existing build-job artifacts authoritative through `ELEPHC_TEST_PREBUILT_BRIDGES`. fn bridge_staticlib_needs_build(archive_path: &Path, package: &str) -> bool { let archive_mtime = match archive_path.metadata().and_then(|meta| meta.modified()) { Ok(mtime) => mtime, Err(_) => return true, }; + if prebuilt_bridge_staticlibs_are_trusted() { + return false; + } let package_dir = Path::new(env!("CARGO_MANIFEST_DIR")) .join("crates") .join(package); @@ -213,6 +219,13 @@ fn bridge_staticlib_needs_build(archive_path: &Path, package: &str) -> bool { || source_tree_newer_than(&package_dir.join("src"), archive_mtime) } +/// Returns whether this test process should trust existing bridge archives without mtime checks. +fn prebuilt_bridge_staticlibs_are_trusted() -> bool { + std::env::var("ELEPHC_TEST_PREBUILT_BRIDGES").is_ok_and(|value| { + value == "1" || value.eq_ignore_ascii_case("true") + }) +} + /// Reports whether an existing source path was modified after `archive_mtime`. /// Missing optional files such as `build.rs` do not force a rebuild. fn source_path_newer_than(path: &Path, archive_mtime: std::time::SystemTime) -> bool { From 0bc7fc8cdff37a79fda63b987c75556ee665ceaa Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 12 Jul 2026 10:04:32 +0200 Subject: [PATCH 1196/1208] fix(ci): shard eval codegen tests separately --- .github/workflows/ci.yml | 142 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 139 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07da5f972e..896a8f2abf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -374,7 +374,7 @@ jobs: cargo nextest run --profile ci \ --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ --workspace-remap . \ - -E 'binary(codegen_tests)' \ + -E 'binary(codegen_tests) and not test(~codegen::eval)' \ --partition hash:${{ matrix.shard }}/16 \ --no-fail-fast --retries 1 --flaky-result pass \ -j 1 @@ -422,7 +422,7 @@ jobs: cargo nextest run --profile ci \ --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ --workspace-remap . \ - -E 'binary(codegen_tests)' \ + -E 'binary(codegen_tests) and not test(~codegen::eval)' \ --partition hash:${{ matrix.shard }}/16 \ --no-fail-fast --retries 1 --flaky-result pass \ -j 1 @@ -470,11 +470,147 @@ jobs: cargo nextest run --profile ci \ --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ --workspace-remap . \ - -E 'binary(codegen_tests)' \ + -E 'binary(codegen_tests) and not test(~codegen::eval)' \ --partition hash:${{ matrix.shard }}/16 \ --no-fail-fast --retries 1 --flaky-result pass \ -j 1 + # Eval integration tests compile, link, and run full programs while exercising + # eval/magician paths, so keep them out of the ordinary codegen shards. Two + # workers per shard reduce wall time without increasing the number of test runs. + eval-codegen-tests-macos-aarch64: + name: Eval Codegen Tests (macos-aarch64 ${{ matrix.shard }}/16) + needs: build-archive-macos-aarch64 + runs-on: macos-14 + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + steps: + - uses: actions/checkout@v4 + + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + + - name: Install native test dependencies + run: brew install pcre2 + + - name: Download test archive + uses: actions/download-artifact@v4 + with: + name: nextest-archive-macos-aarch64 + path: ${{ runner.temp }} + + - name: Run eval codegen test shard + env: + CARGO_NET_OFFLINE: "true" + ELEPHC_TEST_PREBUILT_BRIDGES: "1" + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'binary(codegen_tests) and test(~codegen::eval)' \ + --partition hash:${{ matrix.shard }}/16 \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 2 + + eval-codegen-tests-linux-x86_64: + name: Eval Codegen Tests (linux-x86_64 ${{ matrix.shard }}/16) + needs: build-archive-linux-x86_64 + runs-on: ubuntu-24.04 + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + steps: + - uses: actions/checkout@v4 + + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + + - name: Install native test dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + binutils \ + build-essential \ + file \ + libbz2-dev \ + libpcre2-dev \ + libssl-dev \ + pkg-config \ + tzdata \ + zlib1g-dev + + - name: Download test archive + uses: actions/download-artifact@v4 + with: + name: nextest-archive-linux-x86_64 + path: ${{ runner.temp }} + + - name: Run eval codegen test shard + env: + CARGO_NET_OFFLINE: "true" + ELEPHC_TEST_PREBUILT_BRIDGES: "1" + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'binary(codegen_tests) and test(~codegen::eval)' \ + --partition hash:${{ matrix.shard }}/16 \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 2 + + eval-codegen-tests-linux-aarch64: + name: Eval Codegen Tests (linux-aarch64 ${{ matrix.shard }}/16) + needs: build-archive-linux-aarch64 + runs-on: ubuntu-24.04-arm + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + steps: + - uses: actions/checkout@v4 + + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + + - name: Install native test dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + binutils \ + build-essential \ + file \ + libbz2-dev \ + libpcre2-dev \ + libssl-dev \ + pkg-config \ + tzdata \ + zlib1g-dev + + - name: Download test archive + uses: actions/download-artifact@v4 + with: + name: nextest-archive-linux-aarch64 + path: ${{ runner.temp }} + + - name: Run eval codegen test shard + env: + CARGO_NET_OFFLINE: "true" + ELEPHC_TEST_PREBUILT_BRIDGES: "1" + run: | + cargo nextest run --profile ci \ + --archive-file "$RUNNER_TEMP/nextest-archive.tar.zst" \ + --workspace-remap . \ + -E 'binary(codegen_tests) and test(~codegen::eval)' \ + --partition hash:${{ matrix.shard }}/16 \ + --no-fail-fast --retries 1 --flaky-result pass \ + -j 2 + image-api-sync: name: Image API stubs in sync runs-on: ubuntu-latest From 17b31aad99aaa90bc4dc326b19a1dfd71d2c5018 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 12 Jul 2026 13:52:36 +0200 Subject: [PATCH 1197/1208] fix(ci): resolve archived bridge staticlibs --- tests/codegen/support/runner.rs | 104 ++++++++++++++++++++++++++++---- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/tests/codegen/support/runner.rs b/tests/codegen/support/runner.rs index 9d0c9583e1..b6c02b8506 100644 --- a/tests/codegen/support/runner.rs +++ b/tests/codegen/support/runner.rs @@ -162,14 +162,13 @@ fn requested_bridge_staticlibs<'a>(actual_link_libs: &[&str]) -> Vec<&'a TestBri } /// Builds any requested bridge staticlibs missing from the debug target directory. -fn ensure_bridge_staticlibs(actual_link_libs: &[&str], bridge_staticlib_dir: &str) { +fn ensure_bridge_staticlibs(actual_link_libs: &[&str], bridge_staticlib_dir: &Path) { let _guard = BRIDGE_STATICLIB_BUILD_LOCK .get_or_init(|| Mutex::new(())) .lock() .expect("bridge staticlib build lock poisoned"); for bridge in requested_bridge_staticlibs(actual_link_libs) { - let archive_path = - Path::new(bridge_staticlib_dir).join(format!("lib{}.a", bridge.lib_name)); + let archive_path = bridge_staticlib_dir.join(format!("lib{}.a", bridge.lib_name)); if !bridge_staticlib_needs_build(&archive_path, bridge.package) { continue; } @@ -226,6 +225,91 @@ fn prebuilt_bridge_staticlibs_are_trusted() -> bool { }) } +/// Resolves the debug directory containing bridge archives for the current test process. +fn bridge_staticlib_dir() -> std::path::PathBuf { + let cargo_target_dir = std::env::var_os("CARGO_TARGET_DIR"); + let current_exe = std::env::current_exe().ok(); + bridge_staticlib_dir_for( + cargo_target_dir.as_deref(), + current_exe.as_deref(), + Path::new(env!("CARGO_MANIFEST_DIR")), + prebuilt_bridge_staticlibs_are_trusted(), + ) +} + +/// Selects a bridge archive directory from an explicit target, archive executable, or workspace. +fn bridge_staticlib_dir_for( + cargo_target_dir: Option<&std::ffi::OsStr>, + current_exe: Option<&Path>, + manifest_dir: &Path, + trust_prebuilt: bool, +) -> std::path::PathBuf { + if let Some(target_dir) = cargo_target_dir.filter(|dir| !dir.is_empty()) { + return std::path::PathBuf::from(target_dir).join("debug"); + } + + if trust_prebuilt { + if let Some(executable_dir) = current_exe.and_then(Path::parent) { + let debug_dir = if executable_dir.ends_with("deps") { + executable_dir.parent().unwrap_or(executable_dir) + } else { + executable_dir + }; + return debug_dir.to_path_buf(); + } + } + + manifest_dir.join("target/debug") +} + +#[cfg(test)] +mod bridge_staticlib_dir_tests { + use super::*; + + /// Verifies archived tests resolve bridge libraries beside the extracted test binary. + #[test] + fn archived_tests_use_extracted_target_debug_directory() { + let resolved = bridge_staticlib_dir_for( + None, + Some(Path::new( + "/tmp/nextest-archive/target/debug/deps/codegen_tests-hash", + )), + Path::new("/workspace/elephc"), + true, + ); + + assert_eq!(resolved, Path::new("/tmp/nextest-archive/target/debug")); + } + + /// Verifies an explicit Cargo target directory remains authoritative in Docker runs. + #[test] + fn cargo_target_dir_overrides_archived_executable_directory() { + let resolved = bridge_staticlib_dir_for( + Some(std::ffi::OsStr::new("/shared/target")), + Some(Path::new( + "/tmp/nextest-archive/target/debug/deps/codegen_tests-hash", + )), + Path::new("/workspace/elephc"), + true, + ); + + assert_eq!(resolved, Path::new("/shared/target/debug")); + } + + /// Verifies ordinary local tests continue to use the workspace debug directory. + #[test] + fn local_tests_use_workspace_target_debug_directory() { + let resolved = bridge_staticlib_dir_for( + None, + Some(Path::new("/workspace/target/debug/deps/codegen_tests-hash")), + Path::new("/workspace/elephc"), + false, + ); + + assert_eq!(resolved, Path::new("/workspace/elephc/target/debug")); + } +} + /// Reports whether an existing source path was modified after `archive_mtime`. /// Missing optional files such as `build.rs` do not force a rebuild. fn source_path_newer_than(path: &Path, archive_mtime: std::time::SystemTime) -> bool { @@ -280,13 +364,11 @@ pub(crate) fn link_binary( // Bridge staticlibs live in `/debug` alongside the test binaries; // surface that directory automatically whenever a compiled program links a // known bridge, so tests get robust absolute `-L` paths instead of depending - // on cwd-relative lookup. Docker scripts override CARGO_TARGET_DIR to point - // at a shared volume, so honour that envvar before falling back in-tree. + // on cwd-relative lookup. Docker scripts override CARGO_TARGET_DIR, archived + // shards derive it from their extracted executable, and local tests fall + // back to the workspace target directory. let needs_bridge_staticlib = !requested_bridge_staticlibs(&actual_link_libs).is_empty(); - let bridge_staticlib_dir = match std::env::var("CARGO_TARGET_DIR") { - Ok(dir) if !dir.is_empty() => format!("{}/debug", dir), - _ => format!("{}/target/debug", env!("CARGO_MANIFEST_DIR")), - }; + let bridge_staticlib_dir = bridge_staticlib_dir(); if needs_bridge_staticlib { ensure_bridge_staticlibs(&actual_link_libs, &bridge_staticlib_dir); } @@ -307,7 +389,7 @@ pub(crate) fn link_binary( get_sdk_version(), ]); if needs_bridge_staticlib { - ld_cmd.arg(format!("-L{}", bridge_staticlib_dir)); + ld_cmd.arg(format!("-L{}", bridge_staticlib_dir.display())); } for path in extra_link_paths { ld_cmd.arg(format!("-L{}", path)); @@ -343,7 +425,7 @@ pub(crate) fn link_binary( ld_cmd.arg("-Wl,--no-as-needed"); } if needs_bridge_staticlib { - ld_cmd.arg(format!("-L{}", bridge_staticlib_dir)); + ld_cmd.arg(format!("-L{}", bridge_staticlib_dir.display())); } for path in extra_link_paths { ld_cmd.arg(format!("-L{}", path)); From 6032afb77d94d5cdc9939c59ae7083d63662df77 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Sun, 12 Jul 2026 22:16:02 +0200 Subject: [PATCH 1198/1208] fix(codegen): gate reflection lowering by reachability --- src/codegen/mod.rs | 26 +- src/codegen_support/runtime/data/user.rs | 53 ++-- src/ir_lower/mod.rs | 1 + src/ir_lower/program.rs | 140 ++------- src/ir_lower/reflection.rs | 349 +++++++++++++++++++++++ src/ir_lower/tests/mod.rs | 32 +++ tests/codegen/mod.rs | 1 + tests/codegen/runtime_reachability.rs | 35 +++ 8 files changed, 494 insertions(+), 143 deletions(-) create mode 100644 src/ir_lower/reflection.rs create mode 100644 tests/codegen/runtime_reachability.rs diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index f3b461d2f2..f806a4b648 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -274,12 +274,11 @@ fn finalize_user_asm( &runtime_classes, &module.enum_infos, Some(&allowed_class_names), + module.required_runtime_features.eval_bridge, // The source path feeds eval Reflection source-location hooks only; // embedding it in native-only programs leaks the build path into the // assembly (and trips needle-based optimizer asm asserts). - if module.required_runtime_features.eval_bridge - || module.required_runtime_features.eval_scope - { + if module.required_runtime_features.eval_bridge { module.source_path.as_deref() } else { None @@ -512,6 +511,12 @@ fn intrinsic_method_wrapper_specs(module: &Module) -> Vec HashMap { let emitted_methods = emitted_class_method_keys(module); + let emitted_property_init_thunks = module + .functions + .iter() + .filter(|function| is_property_init_thunk_function(function)) + .map(|function| function.name.as_str()) + .collect::>(); let mut classes = module.class_infos.clone(); for class_info in classes.values_mut() { class_info @@ -524,6 +529,10 @@ fn runtime_class_infos(module: &Module) -> HashMap { .retain(|method_name, impl_class| { emitted_methods.contains(&(impl_class.clone(), method_name.clone(), true)) }); + let property_init_thunk = format!("_class_propinit_{}", class_info.class_id); + if !emitted_property_init_thunks.contains(property_init_thunk.as_str()) { + class_info.defaults.fill(None); + } } classes } @@ -827,6 +836,7 @@ fn seed_builtin_reflection_class_names(module: &Module, names: &mut HashSet, enums: &HashMap, allowed_class_names: Option<&HashSet>, + emit_eval_reflection_metadata: bool, source_path: Option<&str>, ) -> String { let mut out = String::new(); @@ -508,31 +509,35 @@ pub(crate) fn emit_runtime_data_user( } out.push_str(".p2align 3\n"); emit_static_callable_method_data(&mut out, &sorted_classes); - out.push_str(".p2align 3\n"); - emit_eval_reflection_source_file_data(&mut out, source_path); + if emit_eval_reflection_metadata { + out.push_str(".p2align 3\n"); + emit_eval_reflection_source_file_data(&mut out, source_path); + } out.push_str(".p2align 3\n"); emit_classes_by_name_table(&mut out, &sorted_classes); - out.push_str(".p2align 3\n"); - emit_eval_reflection_method_lookup_data(&mut out, &sorted_classes, &sorted_interfaces); - out.push_str(".p2align 3\n"); - emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); - out.push_str(".p2align 3\n"); - emit_eval_reflection_class_lookup_data( - &mut out, - &sorted_classes, - &sorted_interfaces, - declared_trait_source_lines, - ); - out.push_str(".p2align 3\n"); - emit_eval_reflection_class_interface_lookup_data(&mut out, &sorted_classes, interfaces); - out.push_str(".p2align 3\n"); - emit_eval_reflection_class_trait_lookup_data( - &mut out, - &sorted_classes, - declared_trait_uses, - ); - out.push_str(".p2align 3\n"); - emit_eval_reflection_class_trait_alias_lookup_data(&mut out, &sorted_classes); + if emit_eval_reflection_metadata { + out.push_str(".p2align 3\n"); + emit_eval_reflection_method_lookup_data(&mut out, &sorted_classes, &sorted_interfaces); + out.push_str(".p2align 3\n"); + emit_eval_reflection_property_lookup_data(&mut out, &sorted_classes); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_lookup_data( + &mut out, + &sorted_classes, + &sorted_interfaces, + declared_trait_source_lines, + ); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_interface_lookup_data(&mut out, &sorted_classes, interfaces); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_trait_lookup_data( + &mut out, + &sorted_classes, + declared_trait_uses, + ); + out.push_str(".p2align 3\n"); + emit_eval_reflection_class_trait_alias_lookup_data(&mut out, &sorted_classes); + } // -- class-level PHP 8 attribute metadata table -- // Per-class layout: count followed by (name_ptr, name_len) pairs. @@ -2450,6 +2455,7 @@ mod tests { &classes, &HashMap::new(), Some(&allowed_class_names), + false, None, ); @@ -2480,6 +2486,7 @@ mod tests { &classes, &HashMap::new(), None, + false, None, ); diff --git a/src/ir_lower/mod.rs b/src/ir_lower/mod.rs index c3e83c72a0..febfa3e545 100644 --- a/src/ir_lower/mod.rs +++ b/src/ir_lower/mod.rs @@ -18,6 +18,7 @@ mod fibers; mod function; mod ownership; mod program; +mod reflection; mod stmt; #[cfg(test)] diff --git a/src/ir_lower/program.rs b/src/ir_lower/program.rs index 09002d8536..ff76839262 100644 --- a/src/ir_lower/program.rs +++ b/src/ir_lower/program.rs @@ -54,7 +54,6 @@ pub(crate) fn lower( &fiber_return_sigs, ); lower_property_init_thunks(&mut module, check_result, &constants, &fiber_return_sigs); - lower_builtin_reflection_methods(&mut module, check_result, &constants, &fiber_return_sigs); function::lower_main( program, &mut module, @@ -62,6 +61,14 @@ pub(crate) fn lower( &constants, &fiber_return_sigs, ); + lower_literal_eval_aot_functions(&mut module, check_result, &constants, &fiber_return_sigs); + include_lowered_runtime_features(&mut module); + super::reflection::lower_referenced_builtin_methods( + &mut module, + check_result, + &constants, + &fiber_return_sigs, + ); lower_referenced_builtin_spl_methods(&mut module, check_result, &constants, &fiber_return_sigs); builtin_datetime::lower_referenced_builtin_datetime_methods( &mut module, @@ -69,7 +76,6 @@ pub(crate) fn lower( &constants, &fiber_return_sigs, ); - lower_literal_eval_aot_functions(&mut module, check_result, &constants, &fiber_return_sigs); include_lowered_runtime_features(&mut module); validate_module(&module)?; Ok(module) @@ -349,7 +355,7 @@ fn expr_exposes_dynamic_param(expr: &Expr, dynamic_params: &HashSet) -> } /// Adds optional runtime features referenced by synthetic or lowered EIR functions. -fn include_lowered_runtime_features(module: &mut Module) { +pub(super) fn include_lowered_runtime_features(module: &mut Module) { let features = lowered_runtime_features(module); module.required_runtime_features.regex |= features.regex; module.required_runtime_features.phar_archive |= features.phar_archive; @@ -944,7 +950,7 @@ fn eval_literal_fragment_from_inst( } /// Iterates every function-like body already materialized into the EIR module. -fn all_lowered_functions(module: &Module) -> impl Iterator { +pub(super) fn all_lowered_functions(module: &Module) -> impl Iterator { module .functions .iter() @@ -1095,6 +1101,9 @@ fn lower_property_init_thunks( let mut classes = check_result.classes.iter().collect::>(); classes.sort_by_key(|(_, class_info)| class_info.class_id); for (class_name, class_info) in classes { + if super::reflection::canonical_builtin_reflection_class_name(class_name).is_some() { + continue; + } function::lower_property_init_thunk( class_name, class_info, @@ -1847,113 +1856,6 @@ fn lower_methods_for_class_like( } } -/// Lowers the synthetic reflection methods injected by the checker. -fn lower_builtin_reflection_methods( - module: &mut Module, - check_result: &CheckResult, - constants: &std::collections::HashMap, - fiber_return_sigs: &std::collections::HashMap, -) { - for class_name in [ - "ReflectionAttribute", - "ReflectionClass", - "ReflectionObject", - "ReflectionEnum", - "ReflectionClassConstant", - "ReflectionEnumBackedCase", - "ReflectionEnumUnitCase", - "ReflectionFunction", - "ReflectionMethod", - "ReflectionNamedType", - "ReflectionParameter", - "ReflectionProperty", - "ReflectionUnionType", - "ReflectionIntersectionType", - ] { - lower_builtin_reflection_class_methods( - class_name, - module, - check_result, - constants, - fiber_return_sigs, - ); - } -} - -/// Lowers all concrete synthetic methods for one builtin reflection class. -fn lower_builtin_reflection_class_methods( - class_name: &str, - module: &mut Module, - check_result: &CheckResult, - constants: &std::collections::HashMap, - fiber_return_sigs: &std::collections::HashMap, -) { - let Some(class_info) = check_result.classes.get(class_name) else { - return; - }; - let before = module.class_methods.len(); - for method in &class_info.method_decls { - if !method.has_body { - continue; - } - let generated_body; - let method_key = crate::names::php_symbol_key(&method.name); - let body = if class_name == "ReflectionAttribute" && method_key == "newinstance" { - let function_attrs = function_attribute_sources(module); - generated_body = - crate::codegen::reflection::build_attribute_new_instance_body_with_extra( - &check_result.classes, - &function_attrs, - ); - generated_body.as_slice() - } else if class_name == "ReflectionAttribute" && method_key == "getarguments" { - // Materialize captured attribute arguments through the normal array - // lowering (named arguments and associative arrays included) rather - // than a bespoke codegen path. - let function_attrs = function_attribute_sources(module); - generated_body = crate::codegen::reflection::build_attribute_get_arguments_body_with_extra( - &check_result.classes, - &function_attrs, - ); - generated_body.as_slice() - } else { - &method.body - }; - function::lower_class_method( - class_name, - &method.name, - method.is_static, - &method.params, - method.return_type.as_ref(), - body, - module, - check_result, - constants, - fiber_return_sigs, - ); - } - for method in module.class_methods.iter_mut().skip(before) { - method.flags.is_synthetic = true; - } -} - -/// Returns reflection-visible top-level function attribute metadata sources. -fn function_attribute_sources( - module: &Module, -) -> Vec> { - module - .functions - .iter() - .filter(|function| !function.attribute_names.is_empty()) - .map(|function| { - ( - function.attribute_names.as_slice(), - function.attribute_args.as_slice(), - ) - }) - .collect() -} - /// Lowers the small builtin SPL method slice currently consumed by the EIR backend. fn lower_referenced_builtin_spl_methods( module: &mut Module, @@ -2253,7 +2155,10 @@ fn push_supported_builtin_spl_method_for_receiver( } /// Returns the class-name immediate attached to an instruction. -fn class_data_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { +pub(super) fn class_data_name<'a>( + module: &'a Module, + inst: &crate::ir::Instruction, +) -> Option<&'a str> { let Some(Immediate::Data(data)) = inst.immediate else { return None; }; @@ -2265,7 +2170,7 @@ fn class_data_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Opt } /// Parses dynamic object factory fallback and required-parent metadata. -fn dynamic_object_new_metadata_names<'a>( +pub(super) fn dynamic_object_new_metadata_names<'a>( module: &'a Module, inst: &crate::ir::Instruction, ) -> Option<(&'a str, &'a str)> { @@ -2273,7 +2178,10 @@ fn dynamic_object_new_metadata_names<'a>( } /// Returns the string immediate attached to an instruction. -fn string_data_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Option<&'a str> { +pub(super) fn string_data_name<'a>( + module: &'a Module, + inst: &crate::ir::Instruction, +) -> Option<&'a str> { let Some(Immediate::Data(data)) = inst.immediate else { return None; }; @@ -2285,7 +2193,7 @@ fn string_data_name<'a>(module: &'a Module, inst: &crate::ir::Instruction) -> Op } /// Normalizes a PHP method name for metadata lookups. -fn php_method_key(method_name: &str) -> String { +pub(super) fn php_method_key(method_name: &str) -> String { crate::names::php_symbol_key(method_name) } @@ -3045,7 +2953,7 @@ fn lower_builtin_spl_method( } /// Returns true when `module.class_methods` already contains a class-method body. -fn class_method_already_lowered( +pub(super) fn class_method_already_lowered( module: &Module, class_name: &str, method_key: &str, diff --git a/src/ir_lower/reflection.rs b/src/ir_lower/reflection.rs new file mode 100644 index 0000000000..40bdfb83e6 --- /dev/null +++ b/src/ir_lower/reflection.rs @@ -0,0 +1,349 @@ +//! Purpose: +//! Selects and lowers the synthetic builtin Reflection surface reachable from EIR. +//! +//! Called from: +//! - `crate::ir_lower::program::lower()` after user functions and literal eval AOT bodies. +//! +//! Key details: +//! - Native programs lower Reflection classes to a fixed point from EIR types and calls. +//! - Dynamic eval keeps the full Reflection surface because names are resolved at runtime. + +use std::collections::{BTreeSet, HashMap}; + +use crate::ir::{Function, Module, Op}; +use crate::parser::ast::ExprKind; +use crate::types::{CheckResult, FunctionSig, PhpType}; + +use super::function; +use super::program::{ + all_lowered_functions, class_data_name, class_method_already_lowered, + dynamic_object_new_metadata_names, include_lowered_runtime_features, php_method_key, + string_data_name, +}; + +/// Builtin Reflection classes whose concrete method bodies can be lowered into EIR. +const BUILTIN_REFLECTION_CLASS_NAMES: &[&str] = &[ + "ReflectionAttribute", + "ReflectionClass", + "ReflectionObject", + "ReflectionEnum", + "ReflectionClassConstant", + "ReflectionEnumBackedCase", + "ReflectionEnumUnitCase", + "ReflectionFunction", + "ReflectionMethod", + "ReflectionNamedType", + "ReflectionParameter", + "ReflectionProperty", + "ReflectionUnionType", + "ReflectionIntersectionType", +]; + +/// Lowers only synthetic Reflection classes reachable from native EIR, or the full +/// surface when the dynamic eval bridge can construct and invoke them by name. +pub(super) fn lower_referenced_builtin_methods( + module: &mut Module, + check_result: &CheckResult, + constants: &HashMap, + fiber_return_sigs: &HashMap, +) { + loop { + let classes = referenced_builtin_reflection_classes(module); + let before = module.class_methods.len(); + for class_name in classes { + lower_builtin_reflection_property_init_thunk( + &class_name, + module, + check_result, + constants, + fiber_return_sigs, + ); + lower_builtin_reflection_class_methods( + &class_name, + module, + check_result, + constants, + fiber_return_sigs, + ); + } + if module.class_methods.len() == before { + break; + } + include_lowered_runtime_features(module); + } +} + +/// Lowers a selected Reflection class's property-default thunk once it becomes reachable. +fn lower_builtin_reflection_property_init_thunk( + class_name: &str, + module: &mut Module, + check_result: &CheckResult, + constants: &HashMap, + fiber_return_sigs: &HashMap, +) { + let Some(class_info) = check_result.classes.get(class_name) else { + return; + }; + let function_name = format!("_class_propinit_{}", class_info.class_id); + if module + .functions + .iter() + .any(|function| function.name == function_name) + { + return; + } + function::lower_property_init_thunk( + class_name, + class_info, + module, + check_result, + constants, + fiber_return_sigs, + ); +} + +/// Collects Reflection class owners reachable from lowered values and calls. +fn referenced_builtin_reflection_classes(module: &Module) -> BTreeSet { + let mut classes = BTreeSet::new(); + if module.required_runtime_features.eval_bridge { + for class_name in BUILTIN_REFLECTION_CLASS_NAMES { + insert_builtin_reflection_class(module, class_name, &mut classes); + } + return classes; + } + + for function in all_lowered_functions(module) { + collect_builtin_reflection_class_from_type( + module, + &function.return_php_type, + &mut classes, + ); + for param in &function.params { + collect_builtin_reflection_class_from_type(module, ¶m.php_type, &mut classes); + } + for local in &function.locals { + collect_builtin_reflection_class_from_type(module, &local.php_type, &mut classes); + } + for value in &function.values { + collect_builtin_reflection_class_from_type(module, &value.php_type, &mut classes); + } + + for inst in &function.instructions { + match inst.op { + Op::ObjectNew => { + if let Some(class_name) = class_data_name(module, inst) { + insert_builtin_reflection_class(module, class_name, &mut classes); + } + } + Op::DynamicObjectNew => { + if let Some((fallback_class, required_parent)) = + dynamic_object_new_metadata_names(module, inst) + { + insert_builtin_reflection_class(module, fallback_class, &mut classes); + insert_builtin_reflection_class(module, required_parent, &mut classes); + } + } + Op::StaticMethodCall => { + if let Some((class_name, _)) = + string_data_name(module, inst).and_then(|name| name.rsplit_once("::")) + { + insert_builtin_reflection_class(module, class_name, &mut classes); + } + } + Op::MethodCall | Op::NullsafeMethodCall => { + collect_dynamic_reflection_method_candidates( + module, + function, + inst, + &mut classes, + ); + } + _ => {} + } + } + } + classes +} + +/// Adds Reflection implementations that a mixed/union method receiver may dispatch to. +fn collect_dynamic_reflection_method_candidates( + module: &Module, + function: &Function, + inst: &crate::ir::Instruction, + classes: &mut BTreeSet, +) { + let Some(receiver) = inst.operands.first().copied() else { + return; + }; + let Some(receiver_type) = function.value(receiver).map(|value| value.php_type.codegen_repr()) + else { + return; + }; + if !matches!(receiver_type, PhpType::Mixed | PhpType::Union(_)) { + return; + } + let Some(method_name) = string_data_name(module, inst) else { + return; + }; + let method_key = php_method_key(method_name); + for (class_name, class_info) in &module.class_infos { + if class_info.methods.contains_key(&method_key) { + insert_builtin_reflection_class(module, class_name, classes); + } + } +} + +/// Adds Reflection class names nested in one EIR-visible PHP type. +fn collect_builtin_reflection_class_from_type( + module: &Module, + php_type: &PhpType, + classes: &mut BTreeSet, +) { + match php_type { + PhpType::Object(class_name) => { + insert_builtin_reflection_class(module, class_name, classes); + } + PhpType::Array(element) | PhpType::Buffer(element) => { + collect_builtin_reflection_class_from_type(module, element, classes); + } + PhpType::AssocArray { key, value } => { + collect_builtin_reflection_class_from_type(module, key, classes); + collect_builtin_reflection_class_from_type(module, value, classes); + } + PhpType::Union(members) => { + for member in members { + collect_builtin_reflection_class_from_type(module, member, classes); + } + } + PhpType::Pointer(Some(class_name)) => { + insert_builtin_reflection_class(module, class_name, classes); + } + PhpType::Int + | PhpType::Float + | PhpType::Str + | PhpType::Bool + | PhpType::False + | PhpType::Void + | PhpType::Never + | PhpType::Iterable + | PhpType::Mixed + | PhpType::Callable + | PhpType::Packed(_) + | PhpType::Pointer(None) + | PhpType::Resource(_) + | PhpType::TaggedScalar => {} + } +} + +/// Inserts one canonical Reflection class plus Reflection ancestors and method owners. +fn insert_builtin_reflection_class( + module: &Module, + class_name: &str, + classes: &mut BTreeSet, +) { + let Some(canonical) = canonical_builtin_reflection_class_name(class_name) else { + return; + }; + if !module.class_infos.contains_key(canonical) || !classes.insert(canonical.to_string()) { + return; + } + let Some(class_info) = module.class_infos.get(canonical) else { + return; + }; + let dependencies = class_info + .parent + .iter() + .chain(class_info.method_impl_classes.values()) + .chain(class_info.static_method_impl_classes.values()) + .cloned() + .collect::>(); + for dependency in dependencies { + insert_builtin_reflection_class(module, &dependency, classes); + } +} + +/// Resolves a class spelling to the canonical builtin Reflection name. +pub(super) fn canonical_builtin_reflection_class_name(class_name: &str) -> Option<&'static str> { + let key = crate::names::php_symbol_key(class_name.trim_start_matches('\\')); + BUILTIN_REFLECTION_CLASS_NAMES + .iter() + .copied() + .find(|candidate| crate::names::php_symbol_key(candidate) == key) +} + +/// Lowers all concrete synthetic methods for one builtin reflection class. +fn lower_builtin_reflection_class_methods( + class_name: &str, + module: &mut Module, + check_result: &CheckResult, + constants: &HashMap, + fiber_return_sigs: &HashMap, +) { + let Some(class_info) = check_result.classes.get(class_name) else { + return; + }; + let before = module.class_methods.len(); + for method in &class_info.method_decls { + if !method.has_body { + continue; + } + let generated_body; + let method_key = crate::names::php_symbol_key(&method.name); + if class_method_already_lowered(module, class_name, &method_key, method.is_static) { + continue; + } + let body = if class_name == "ReflectionAttribute" && method_key == "newinstance" { + let function_attrs = function_attribute_sources(module); + generated_body = + crate::codegen::reflection::build_attribute_new_instance_body_with_extra( + &check_result.classes, + &function_attrs, + ); + generated_body.as_slice() + } else if class_name == "ReflectionAttribute" && method_key == "getarguments" { + // Materialize captured attribute arguments through the normal array + // lowering (named arguments and associative arrays included) rather + // than a bespoke codegen path. + let function_attrs = function_attribute_sources(module); + generated_body = crate::codegen::reflection::build_attribute_get_arguments_body_with_extra( + &check_result.classes, + &function_attrs, + ); + generated_body.as_slice() + } else { + &method.body + }; + function::lower_class_method( + class_name, + &method.name, + method.is_static, + &method.params, + method.return_type.as_ref(), + body, + module, + check_result, + constants, + fiber_return_sigs, + ); + } + for method in module.class_methods.iter_mut().skip(before) { + method.flags.is_synthetic = true; + } +} + +/// Returns reflection-visible top-level function attribute metadata sources. +fn function_attribute_sources( + module: &Module, +) -> Vec> { + module + .functions + .iter() + .filter(|function| !function.attribute_names.is_empty()) + .map(|function| { + ( + function.attribute_names.as_slice(), + function.attribute_args.as_slice(), + ) + }) + .collect() +} diff --git a/src/ir_lower/tests/mod.rs b/src/ir_lower/tests/mod.rs index bc4202ef04..77551ba2c8 100644 --- a/src/ir_lower/tests/mod.rs +++ b/src/ir_lower/tests/mod.rs @@ -105,6 +105,38 @@ echo $counter->value(); assert!(text.contains("flags(method)"), "missing method flag: {text}"); } +/// Verifies a native program without Reflection references does not lower the synthetic surface. +#[test] +fn plain_program_omits_unreferenced_builtin_reflection_methods() { + let module = lower_source("getName(); +"#, + ); + let method_names = module + .class_methods + .iter() + .map(|function| function.name.as_str()) + .collect::>(); + assert!(method_names.contains("ReflectionClass::__construct")); + assert!(method_names.contains("ReflectionClass::getName")); +} + /// Verifies mixed float/integer comparisons coerce both operands before `fcmp`. #[test] fn float_comparison_coerces_integer_operand() { diff --git a/tests/codegen/mod.rs b/tests/codegen/mod.rs index c537559f14..3bb13b0a29 100644 --- a/tests/codegen/mod.rs +++ b/tests/codegen/mod.rs @@ -50,6 +50,7 @@ mod objects; mod destructors; mod references; mod runtime_gc; +mod runtime_reachability; mod math; mod misc; mod pointers; diff --git a/tests/codegen/runtime_reachability.rs b/tests/codegen/runtime_reachability.rs new file mode 100644 index 0000000000..85c7d4f8f4 --- /dev/null +++ b/tests/codegen/runtime_reachability.rs @@ -0,0 +1,35 @@ +//! Purpose: +//! Regression coverage for feature-gated runtime and synthetic builtin reachability. +//! +//! Called from: +//! - `cargo test` through the codegen integration-test harness. +//! +//! Key details: +//! - Plain native programs must not carry the optional eval Reflection surface. + +use crate::support::{compile_source_to_asm_with_options, fs, make_cli_test_dir}; + +/// Verifies a program without eval or Reflection omits their synthetic methods and metadata. +#[test] +fn test_plain_program_omits_unreferenced_reflection_surface() { + let dir = make_cli_test_dir("elephc_plain_runtime_reachability"); + let (user_asm, _runtime_asm, required_libraries) = + compile_source_to_asm_with_options(" Date: Sun, 12 Jul 2026 23:03:31 +0200 Subject: [PATCH 1199/1208] fix(codegen): retain reflection metadata for eval scope --- src/codegen/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index f806a4b648..2c50001126 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -201,6 +201,8 @@ fn finalize_user_asm( exported_functions: &HashMap, ) -> String { let eval_bridge = module.required_runtime_features.eval_bridge; + let emit_eval_reflection_metadata = + eval_bridge || module.required_runtime_features.eval_scope; if eval_bridge { eval_property_helpers::emit_eval_property_helpers(module, &mut emitter, &mut data); eval_static_property_helpers::emit_eval_static_property_helpers( @@ -274,11 +276,11 @@ fn finalize_user_asm( &runtime_classes, &module.enum_infos, Some(&allowed_class_names), - module.required_runtime_features.eval_bridge, + emit_eval_reflection_metadata, // The source path feeds eval Reflection source-location hooks only; // embedding it in native-only programs leaks the build path into the // assembly (and trips needle-based optimizer asm asserts). - if module.required_runtime_features.eval_bridge { + if emit_eval_reflection_metadata { module.source_path.as_deref() } else { None From 8eca4ccd389e9a376c2d29ebf8945c485c1bd9ab Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 13 Jul 2026 00:16:32 +0200 Subject: [PATCH 1200/1208] test(eval): split reserved class-like name cases --- tests/codegen/eval.rs | 51 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/tests/codegen/eval.rs b/tests/codegen/eval.rs index cf27531b7a..c9fc08152a 100644 --- a/tests/codegen/eval.rs +++ b/tests/codegen/eval.rs @@ -19773,32 +19773,63 @@ eval('enum EvalBadEnumConstName { } } -/// Verifies eval rejects PHP-reserved class-like declaration names. +/// Asserts one eval fragment rejects a PHP-reserved class-like declaration name. +fn assert_eval_declared_reserved_class_like_name_fails(source: &str) { + let err = compile_and_run_expect_failure(source); + assert!( + err.contains("Fatal error: eval() fragment uses an unsupported construct"), + "stderr did not contain eval unsupported-construct diagnostic: {err}" + ); +} + +/// Verifies eval rejects the reserved `match` class declaration name. #[test] -fn test_eval_declared_reserved_class_like_name_fails() { - for source in [ +fn test_eval_declared_reserved_match_class_name_fails() { + assert_eval_declared_reserved_class_like_name_fails( r#" Date: Mon, 13 Jul 2026 13:26:36 +0200 Subject: [PATCH 1201/1208] refactor(magician): split context and eval IR modules --- crates/elephc-magician/src/context.rs | 4090 +---------------- .../src/context/alias_metadata.rs | 60 + .../src/context/class_metadata.rs | 750 +++ .../src/context/classes_aliases.rs | 375 ++ .../src/context/classlike_objects.rs | 475 ++ .../src/context/closure_metadata.rs | 114 + crates/elephc-magician/src/context/core.rs | 239 + .../elephc-magician/src/context/functions.rs | 203 + .../src/context/global_registry.rs | 198 + .../src/context/native_defaults.rs | 235 + .../src/context/native_function.rs | 180 + .../src/context/native_metadata.rs | 263 ++ .../src/context/native_signatures.rs | 340 ++ .../src/context/normalization.rs | 118 + .../src/context/reference_metadata.rs | 93 + .../src/context/reflection_registry.rs | 167 + .../src/context/runtime_state.rs | 425 ++ crates/elephc-magician/src/eval_ir.rs | 2880 +----------- .../elephc-magician/src/eval_ir/attributes.rs | 82 + .../elephc-magician/src/eval_ir/callable.rs | 266 ++ crates/elephc-magician/src/eval_ir/classes.rs | 507 ++ crates/elephc-magician/src/eval_ir/enums.rs | 236 + .../src/eval_ir/expressions.rs | 343 ++ .../elephc-magician/src/eval_ir/interfaces.rs | 444 ++ crates/elephc-magician/src/eval_ir/methods.rs | 301 ++ crates/elephc-magician/src/eval_ir/program.rs | 74 + .../elephc-magician/src/eval_ir/properties.rs | 284 ++ .../elephc-magician/src/eval_ir/statements.rs | 278 ++ crates/elephc-magician/src/eval_ir/traits.rs | 144 + 29 files changed, 7248 insertions(+), 6916 deletions(-) create mode 100644 crates/elephc-magician/src/context/alias_metadata.rs create mode 100644 crates/elephc-magician/src/context/class_metadata.rs create mode 100644 crates/elephc-magician/src/context/classes_aliases.rs create mode 100644 crates/elephc-magician/src/context/classlike_objects.rs create mode 100644 crates/elephc-magician/src/context/closure_metadata.rs create mode 100644 crates/elephc-magician/src/context/core.rs create mode 100644 crates/elephc-magician/src/context/functions.rs create mode 100644 crates/elephc-magician/src/context/global_registry.rs create mode 100644 crates/elephc-magician/src/context/native_defaults.rs create mode 100644 crates/elephc-magician/src/context/native_function.rs create mode 100644 crates/elephc-magician/src/context/native_metadata.rs create mode 100644 crates/elephc-magician/src/context/native_signatures.rs create mode 100644 crates/elephc-magician/src/context/normalization.rs create mode 100644 crates/elephc-magician/src/context/reference_metadata.rs create mode 100644 crates/elephc-magician/src/context/reflection_registry.rs create mode 100644 crates/elephc-magician/src/context/runtime_state.rs create mode 100644 crates/elephc-magician/src/eval_ir/attributes.rs create mode 100644 crates/elephc-magician/src/eval_ir/callable.rs create mode 100644 crates/elephc-magician/src/eval_ir/classes.rs create mode 100644 crates/elephc-magician/src/eval_ir/enums.rs create mode 100644 crates/elephc-magician/src/eval_ir/expressions.rs create mode 100644 crates/elephc-magician/src/eval_ir/interfaces.rs create mode 100644 crates/elephc-magician/src/eval_ir/methods.rs create mode 100644 crates/elephc-magician/src/eval_ir/program.rs create mode 100644 crates/elephc-magician/src/eval_ir/properties.rs create mode 100644 crates/elephc-magician/src/eval_ir/statements.rs create mode 100644 crates/elephc-magician/src/eval_ir/traits.rs diff --git a/crates/elephc-magician/src/context.rs b/crates/elephc-magician/src/context.rs index ab910f39d0..ccc782c6cf 100644 --- a/crates/elephc-magician/src/context.rs +++ b/crates/elephc-magician/src/context.rs @@ -1,7 +1,6 @@ //! Purpose: -//! Declares the opaque process-level eval context handle. -//! The full implementation will hold dynamic function, class, constant, and -//! class-like, constant, builtin registries plus runtime hooks. +//! Declares the opaque process-level eval context handle and shared metadata types. +//! Registry and runtime-state method families live in focused child modules. //! //! Called from: //! - `crate::abi` @@ -11,6 +10,23 @@ //! - The handle is intentionally opaque to generated code. //! - No Rust-owned layout is promised across the C ABI. +mod alias_metadata; +mod class_metadata; +mod classes_aliases; +mod classlike_objects; +mod closure_metadata; +mod core; +mod functions; +mod global_registry; +mod native_defaults; +mod native_function; +mod native_metadata; +mod native_signatures; +mod normalization; +mod reference_metadata; +mod reflection_registry; +mod runtime_state; + use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::ffi::c_void; @@ -27,6 +43,15 @@ use crate::scope::ElephcEvalScope; use crate::stream_resources::EvalStreamResources; use crate::value::{RuntimeCell, RuntimeCellHandle}; +pub use alias_metadata::*; +pub use closure_metadata::*; +pub use core::*; +pub(crate) use global_registry::*; +pub use native_defaults::*; +pub use native_function::*; +use normalization::*; +pub use reference_metadata::*; + #[cfg(not(test))] static GLOBAL_EVAL_CLASSES: OnceLock> = OnceLock::new(); @@ -34,4062 +59,3 @@ thread_local! { static NATIVE_FRAME_CALLED_CLASS_OVERRIDES: RefCell> = RefCell::new(Vec::new()); } - -/// Late-static override installed while eval dispatches into a generated/AOT frame. -#[derive(Clone)] -struct NativeFrameCalledClassOverride { - #[cfg_attr(test, allow(dead_code))] - context: *mut ElephcEvalContext, - frame_class: String, - called_class: String, -} - -/// Scoped guard that removes one native-frame called-class override on drop. -pub(crate) struct NativeFrameCalledClassOverrideGuard; - -/// Installs a late-static called-class override for a generated/AOT frame call. -pub(crate) fn push_native_frame_called_class_override( - context: *mut ElephcEvalContext, - frame_class: &str, - called_class: &str, -) -> NativeFrameCalledClassOverrideGuard { - NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { - overrides - .borrow_mut() - .push(NativeFrameCalledClassOverride { - context, - frame_class: frame_class.trim_start_matches('\\').to_string(), - called_class: called_class.trim_start_matches('\\').to_string(), - }); - }); - NativeFrameCalledClassOverrideGuard -} - -impl Drop for NativeFrameCalledClassOverrideGuard { - /// Removes the most recent native-frame called-class override. - fn drop(&mut self) { - NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { - overrides.borrow_mut().pop(); - }); - } -} - -/// Returns the active thread-local late-static override for one generated/AOT frame. -fn native_frame_called_class_override( - frame_class: &str, - called_class: &str, -) -> Option { - let frame_class = frame_class.trim_start_matches('\\'); - let called_class = called_class.trim_start_matches('\\'); - if frame_class.is_empty() || !called_class.eq_ignore_ascii_case(frame_class) { - return None; - } - native_frame_called_class_override_for_frame(frame_class) -} - -/// Returns the active called-class override for one generated/AOT frame class. -pub(crate) fn native_frame_called_class_override_for_frame(frame_class: &str) -> Option { - let frame_class = frame_class.trim_start_matches('\\'); - if frame_class.is_empty() { - return None; - } - NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { - overrides - .borrow() - .iter() - .rev() - .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) - .map(|entry| entry.called_class.clone()) - }) -} - -/// Returns the active called-class override bytes for one generated/AOT frame class. -pub(crate) fn native_frame_called_class_override_bytes( - frame_class: &str, -) -> Option<(*const u8, usize)> { - let frame_class = frame_class.trim_start_matches('\\'); - if frame_class.is_empty() { - return None; - } - NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { - overrides - .borrow() - .iter() - .rev() - .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) - .map(|entry| (entry.called_class.as_ptr(), entry.called_class.len())) - }) -} - -/// Returns the active eval context and called class for one generated/AOT frame. -#[cfg_attr(test, allow(dead_code))] -pub(crate) fn native_frame_called_class_override_context( - frame_class: &str, -) -> Option<(*mut ElephcEvalContext, String)> { - let frame_class = frame_class.trim_start_matches('\\'); - if frame_class.is_empty() { - return None; - } - NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { - overrides - .borrow() - .iter() - .rev() - .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) - .map(|entry| (entry.context, entry.called_class.clone())) - }) -} - -#[cfg(not(test))] -#[derive(Default)] -struct GlobalEvalClassRegistry { - classes: HashMap, - declared_class_names: Vec, - interfaces: HashMap, - declared_interface_names: Vec, - traits: HashMap, - declared_trait_names: Vec, - enums: HashMap, - declared_enum_names: Vec, - aliases: HashMap, -} - -/// Returns the process-local eval class registry for generated-code eval contexts. -#[cfg(not(test))] -fn global_eval_classes() -> &'static Mutex { - GLOBAL_EVAL_CLASSES.get_or_init(|| Mutex::new(GlobalEvalClassRegistry::default())) -} - -/// Records one eval-declared class so later eval contexts can see PHP-global metadata. -#[cfg(not(test))] -fn register_global_eval_class(class: &EvalClass) { - let key = normalize_class_name(class.name()); - if let Ok(mut registry) = global_eval_classes().lock() { - if !registry.classes.contains_key(&key) { - registry.declared_class_names.push(class.name().to_string()); - } - registry.classes.insert(key, class.clone()); - } -} - -/// Records one eval-declared interface so later eval contexts can see PHP-global metadata. -#[cfg(not(test))] -fn register_global_eval_interface(interface: &EvalInterface) { - let key = normalize_class_name(interface.name()); - if let Ok(mut registry) = global_eval_classes().lock() { - if !registry.interfaces.contains_key(&key) { - registry - .declared_interface_names - .push(interface.name().to_string()); - } - registry.interfaces.insert(key, interface.clone()); - } -} - -/// Records one eval-declared trait so later eval contexts can see PHP-global metadata. -#[cfg(not(test))] -fn register_global_eval_trait(trait_decl: &EvalTrait) { - let key = normalize_class_name(trait_decl.name()); - if let Ok(mut registry) = global_eval_classes().lock() { - if !registry.traits.contains_key(&key) { - registry - .declared_trait_names - .push(trait_decl.name().to_string()); - } - registry.traits.insert(key, trait_decl.clone()); - } -} - -/// Records one eval-declared enum so later eval contexts can see PHP-global metadata. -#[cfg(not(test))] -fn register_global_eval_enum(enum_decl: &EvalEnum) { - let key = normalize_class_name(enum_decl.name()); - if let Ok(mut registry) = global_eval_classes().lock() { - if !registry.enums.contains_key(&key) { - registry - .declared_enum_names - .push(enum_decl.name().trim_start_matches('\\').to_string()); - } - registry.enums.insert(key, enum_decl.clone()); - } -} - -/// Records one eval-defined class-like alias for later generated eval contexts. -#[cfg(not(test))] -fn register_global_eval_alias(alias_name: &str, alias: &EvalClassAlias) { - let key = normalize_class_name(alias_name); - if let Ok(mut registry) = global_eval_classes().lock() { - registry.aliases.insert(key, alias.clone()); - } -} - -/// Native descriptor-invoker ABI registered by generated code for AOT functions. -pub type NativeFunctionInvoker = - unsafe extern "C" fn(*mut c_void, *mut RuntimeCell) -> *mut RuntimeCell; - -/// Snapshot of eval execution stacks used to restore caller-sensitive access checks. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ElephcEvalExecutionScope { - function_stack: Vec, - class_stack: Vec, - called_class_stack: Vec, -} - -/// PHP-visible magic-constant names for the current eval execution frame. -#[derive(Debug, Clone, PartialEq, Eq)] -struct EvalMagicScope { - function_name: String, - method_name: String, - class_name: String, - trait_name: String, -} - -/// Caller-side storage target that can remain linked to an eval object property. -#[derive(Clone)] -pub enum EvalReferenceTarget { - Variable { - scope: *mut ElephcEvalScope, - name: String, - }, - ArrayElement { - scope: *mut ElephcEvalScope, - array_name: String, - index: RuntimeCellHandle, - }, - NestedArrayElement { - array_target: Box, - index: RuntimeCellHandle, - }, - ObjectProperty { - object: RuntimeCellHandle, - property: String, - access_scope: ElephcEvalExecutionScope, - }, - StaticProperty { - class_name: String, - property: String, - access_scope: ElephcEvalExecutionScope, - }, - Cell { - cell: RuntimeCellHandle, - }, - InvokerSlot { - slot: usize, - source_tag: u64, - }, -} - -/// Normalized PHP array key used for eval-side reference metadata. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum EvalArrayReferenceKey { - Int(i64), - String(Vec), -} - -/// Late-static dispatch metadata attached to eval-created static callable arrays. -#[derive(Clone)] -struct EvalStaticCallableMetadata { - class_name: String, - method: String, - called_class: String, - native_class: Option, - bridge_scope: Option, -} - -/// Native instance-method dispatch metadata attached to eval-created method callables. -#[derive(Clone)] -struct EvalObjectCallableMetadata { - object: usize, - method: String, - called_class: String, - native_class: String, - bridge_scope: String, -} - -/// Callable target represented by a PHP-visible eval `Closure` object. -#[derive(Clone)] -pub enum EvalClosureObjectTarget { - Named(String), - BoundNamed { - name: String, - bound_this: Option, - bound_scope: Option, - }, - InvokableObject { - object: RuntimeCellHandle, - }, - ObjectMethod { - object: RuntimeCellHandle, - method: String, - called_class: Option, - native_class: Option, - bridge_scope: Option, - }, - StaticMethod { - class_name: String, - method: String, - called_class: Option, - native_class: Option, - bridge_scope: Option, - }, -} - -/// Runtime value captured by an eval closure literal. -#[derive(Clone)] -pub struct EvalClosureCaptureBinding { - name: String, - value: RuntimeCellHandle, - by_ref_target: Option, -} - -impl EvalClosureCaptureBinding { - /// Creates one captured runtime value with optional caller-side by-reference storage. - pub fn new( - name: impl Into, - value: RuntimeCellHandle, - by_ref_target: Option, - ) -> Self { - Self { - name: name.into(), - value, - by_ref_target, - } - } - - /// Returns the captured variable name without the leading `$`. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the runtime cell captured by the closure. - pub const fn value(&self) -> RuntimeCellHandle { - self.value - } - - /// Returns caller-side writeback metadata for by-reference captures. - pub fn by_ref_target(&self) -> Option<&EvalReferenceTarget> { - self.by_ref_target.as_ref() - } -} - -/// One eval closure instance retained by a synthetic callable name. -#[derive(Clone)] -pub struct EvalClosure { - function: EvalFunction, - captures: Vec, - is_static: bool, -} - -impl EvalClosure { - /// Creates one closure instance from its function body and captured values. - pub fn new( - function: EvalFunction, - captures: Vec, - is_static: bool, - ) -> Self { - Self { - function, - captures, - is_static, - } - } - - /// Returns the executable eval function payload for this closure. - pub fn function(&self) -> &EvalFunction { - &self.function - } - - /// Returns the captured runtime values attached to this closure instance. - pub fn captures(&self) -> &[EvalClosureCaptureBinding] { - &self.captures - } - - /// Returns whether this closure was declared with PHP's `static function` form. - pub const fn is_static(&self) -> bool { - self.is_static - } -} - -/// Native AOT function callback metadata visible to runtime eval fragments. -#[derive(Clone)] -pub struct NativeFunction { - descriptor: *mut c_void, - invoker: NativeFunctionInvoker, - param_count: usize, - param_names: Vec, - param_types: Vec>, - param_defaults: Vec>, - param_by_ref: Vec, - variadic_index: Option, - return_type: Option, - bridge_supported: bool, -} - -impl NativeFunction { - /// Creates callback metadata for a descriptor-compatible AOT function. - pub fn new( - descriptor: *mut c_void, - invoker: NativeFunctionInvoker, - param_count: usize, - ) -> Self { - Self { - descriptor, - invoker, - param_count, - param_names: Vec::new(), - param_types: Vec::new(), - param_defaults: Vec::new(), - param_by_ref: Vec::new(), - variadic_index: None, - return_type: None, - bridge_supported: true, - } - } - - /// Returns the visible positional parameter count accepted by this callback. - pub const fn param_count(&self) -> usize { - self.param_count - } - - /// Records the PHP parameter name for one positional callback slot. - pub fn set_param_name(&mut self, index: usize, name: impl Into) -> bool { - if index >= self.param_count { - return false; - } - if self.param_names.len() < self.param_count { - self.param_names.resize(self.param_count, String::new()); - } - self.param_names[index] = name.into(); - true - } - - /// Returns the PHP-visible parameter names registered for this callback. - pub fn param_names(&self) -> &[String] { - &self.param_names - } - - /// Records the PHP declared type metadata for one positional callback slot. - pub fn set_param_type(&mut self, index: usize, param_type: EvalParameterType) -> bool { - if index >= self.param_count { - return false; - } - if self.param_types.len() < self.param_count { - self.param_types.resize(self.param_count, None); - } - self.param_types[index] = Some(param_type); - true - } - - /// Returns PHP declared parameter types registered for this callback. - pub fn param_types(&self) -> &[Option] { - &self.param_types - } - - /// Returns the registered declared type for one parameter slot, if any. - pub fn param_type(&self, index: usize) -> Option<&EvalParameterType> { - self.param_types.get(index).and_then(Option::as_ref) - } - - /// Records a PHP default value for one positional callback slot. - pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { - if index >= self.param_count { - return false; - } - if self.param_defaults.len() < self.param_count { - self.param_defaults.resize(self.param_count, None); - } - self.param_defaults[index] = Some(default); - true - } - - /// Returns the registered default for one parameter slot, if any. - pub fn param_default(&self, index: usize) -> Option<&NativeCallableDefault> { - self.param_defaults.get(index).and_then(Option::as_ref) - } - - /// Records whether one positional callback parameter is by-reference. - pub fn set_param_by_ref(&mut self, index: usize, by_ref: bool) -> bool { - if index >= self.param_count { - return false; - } - if self.param_by_ref.len() < self.param_count { - self.param_by_ref.resize(self.param_count, false); - } - self.param_by_ref[index] = by_ref; - true - } - - /// Records which positional callback parameter is variadic. - pub fn set_variadic_index(&mut self, index: usize) -> bool { - if index >= self.param_count { - return false; - } - self.variadic_index = Some(index); - true - } - - /// Records the PHP declared return type metadata for this callback. - pub fn set_return_type(&mut self, return_type: EvalParameterType) { - self.return_type = Some(return_type); - } - - /// Records whether eval may dispatch this callback through the generated bridge. - pub fn set_bridge_supported(&mut self, supported: bool) { - self.bridge_supported = supported; - } - - /// Returns whether one registered parameter is by-reference. - pub fn param_by_ref(&self, index: usize) -> bool { - self.param_by_ref.get(index).copied().unwrap_or(false) - } - - /// Returns whether one registered parameter is the variadic parameter. - pub fn param_variadic(&self, index: usize) -> bool { - self.variadic_index == Some(index) - } - - /// Returns the registered declared return type, if any. - pub fn return_type(&self) -> Option<&EvalParameterType> { - self.return_type.as_ref() - } - - /// Returns whether eval may dispatch this callback through the generated bridge. - pub const fn bridge_supported(&self) -> bool { - self.bridge_supported - } - - /// Returns the minimum number of required parameters implied by defaults. - pub fn required_param_count(&self) -> usize { - if let Some(index) = self.variadic_index { - return (0..index) - .rfind(|position| self.param_default(*position).is_none()) - .map_or(0, |position| position + 1); - } - (0..self.param_count) - .rfind(|index| self.param_default(*index).is_none()) - .map_or(0, |index| index + 1) - } - - /// Invokes the descriptor-compatible callback with a boxed Mixed arg array. - /// - /// # Safety - /// `arg_array` must be a boxed Mixed indexed array whose elements are boxed - /// Mixed cells following the descriptor-invoker ABI. - pub unsafe fn call(&self, arg_array: RuntimeCellHandle) -> RuntimeCellHandle { - RuntimeCellHandle::from_raw((self.invoker)(self.descriptor, arg_array.as_ptr())) - } -} - -/// Default value for a native AOT callable parameter visible to eval fragments. -#[derive(Clone, Debug, PartialEq)] -pub enum NativeCallableDefault { - Null, - Bool(bool), - Int(i64), - Float(f64), - String(String), - EmptyArray, - Array(Vec), - Object { - class_name: String, - args: Vec, - }, -} - -/// One element in an array-valued native AOT callable default. -#[derive(Clone, Debug, PartialEq)] -pub struct NativeCallableArrayDefaultElement { - pub key: Option, - pub value: NativeCallableDefault, -} - -impl NativeCallableArrayDefaultElement { - /// Creates one auto-indexed element for an array-valued default. - pub fn positional(value: NativeCallableDefault) -> Self { - Self { key: None, value } - } - - /// Creates one explicitly keyed element for an array-valued default. - pub fn keyed(key: NativeCallableArrayDefaultKey, value: NativeCallableDefault) -> Self { - Self { - key: Some(key), - value, - } - } -} - -/// Static PHP array key retained for an array-valued native AOT callable default. -#[derive(Clone, Debug, PartialEq)] -pub enum NativeCallableArrayDefaultKey { - Int(i64), - String(String), -} - -/// Constructor argument for an object-valued native AOT callable default. -#[derive(Clone, Debug, PartialEq)] -pub struct NativeCallableObjectDefaultArg { - pub name: Option, - pub value: NativeCallableDefault, -} - -impl NativeCallableObjectDefaultArg { - /// Creates one positional constructor argument for an object-valued default. - pub fn positional(value: NativeCallableDefault) -> Self { - Self { name: None, value } - } - - /// Creates one named constructor argument for an object-valued default. - pub fn named(name: impl Into, value: NativeCallableDefault) -> Self { - Self { - name: Some(name.into()), - value, - } - } -} - -/// Native AOT method or constructor signature metadata visible to eval fragments. -#[derive(Clone)] -pub struct NativeCallableSignature { - param_count: usize, - param_names: Vec, - param_types: Vec>, - param_defaults: Vec>, - param_by_ref: Vec, - variadic_index: Option, - return_type: Option, - bridge_supported: bool, -} - -impl NativeCallableSignature { - /// Creates signature metadata with the visible positional parameter count. - pub const fn new(param_count: usize) -> Self { - Self { - param_count, - param_names: Vec::new(), - param_types: Vec::new(), - param_defaults: Vec::new(), - param_by_ref: Vec::new(), - variadic_index: None, - return_type: None, - bridge_supported: true, - } - } - - /// Returns the visible positional parameter count accepted by this callable. - pub const fn param_count(&self) -> usize { - self.param_count - } - - /// Records the PHP parameter name for one positional callable slot. - pub fn set_param_name(&mut self, index: usize, name: impl Into) -> bool { - if index >= self.param_count { - return false; - } - if self.param_names.len() < self.param_count { - self.param_names.resize(self.param_count, String::new()); - } - self.param_names[index] = name.into(); - true - } - - /// Records the PHP declared type metadata for one positional callable slot. - pub fn set_param_type(&mut self, index: usize, param_type: EvalParameterType) -> bool { - if index >= self.param_count { - return false; - } - if self.param_types.len() < self.param_count { - self.param_types.resize(self.param_count, None); - } - self.param_types[index] = Some(param_type); - true - } - - /// Records a PHP scalar default value for one positional callable slot. - pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { - if index >= self.param_count { - return false; - } - if self.param_defaults.len() < self.param_count { - self.param_defaults.resize(self.param_count, None); - } - self.param_defaults[index] = Some(default); - true - } - - /// Records whether one positional callable parameter is by-reference. - pub fn set_param_by_ref(&mut self, index: usize, by_ref: bool) -> bool { - if index >= self.param_count { - return false; - } - if self.param_by_ref.len() < self.param_count { - self.param_by_ref.resize(self.param_count, false); - } - self.param_by_ref[index] = by_ref; - true - } - - /// Records which positional callable parameter is variadic. - pub fn set_variadic_index(&mut self, index: usize) -> bool { - if index >= self.param_count { - return false; - } - self.variadic_index = Some(index); - true - } - - /// Records the PHP declared return type metadata for this callable. - pub fn set_return_type(&mut self, return_type: EvalParameterType) { - self.return_type = Some(return_type); - } - - /// Records whether eval may dispatch this callable through the generated bridge. - pub fn set_bridge_supported(&mut self, supported: bool) { - self.bridge_supported = supported; - } - - /// Returns the PHP-visible parameter names registered for this callable. - pub fn param_names(&self) -> &[String] { - &self.param_names - } - - /// Returns PHP declared parameter types registered for this callable. - pub fn param_types(&self) -> &[Option] { - &self.param_types - } - - /// Returns the registered declared type for one parameter slot, if any. - pub fn param_type(&self, index: usize) -> Option<&EvalParameterType> { - self.param_types.get(index).and_then(Option::as_ref) - } - - /// Returns the PHP-visible scalar parameter defaults registered for this callable. - pub fn param_defaults(&self) -> &[Option] { - &self.param_defaults - } - - /// Returns the registered scalar default for one parameter slot, if any. - pub fn param_default(&self, index: usize) -> Option<&NativeCallableDefault> { - self.param_defaults.get(index).and_then(Option::as_ref) - } - - /// Returns whether one registered parameter is by-reference. - pub fn param_by_ref(&self, index: usize) -> bool { - self.param_by_ref.get(index).copied().unwrap_or(false) - } - - /// Returns whether one registered parameter is the variadic parameter. - pub fn param_variadic(&self, index: usize) -> bool { - self.variadic_index == Some(index) - } - - /// Returns whether eval may dispatch this callable through the generated bridge. - pub const fn bridge_supported(&self) -> bool { - self.bridge_supported - } - - /// Returns the minimum number of arguments required by registered defaults. - pub fn required_param_count(&self) -> usize { - if let Some(index) = self.variadic_index { - return (0..index) - .rfind(|position| self.param_default(*position).is_none()) - .map_or(0, |position| position + 1); - } - (0..self.param_count) - .rfind(|index| self.param_default(*index).is_none()) - .map_or(0, |index| index + 1) - } - - /// Returns the registered declared return type metadata, if any. - pub fn return_type(&self) -> Option<&EvalParameterType> { - self.return_type.as_ref() - } -} - -/// PHP class-like declaration kind targeted by a dynamic `class_alias()`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum EvalClassAliasKind { - Class, - Interface, - Trait, - Enum, -} - -/// Dynamic alias target and kind recorded for eval-visible class-like symbols. -#[derive(Debug, Clone, PartialEq, Eq)] -struct EvalClassAlias { - target: String, - kind: EvalClassAliasKind, -} - -/// Metadata attached to one synthetic eval `ReflectionAttribute` object. -#[derive(Clone)] -pub struct EvalReflectionAttributeMetadata { - attribute: EvalAttribute, - target: u64, - repeated: bool, -} - -impl EvalReflectionAttributeMetadata { - /// Creates metadata for a materialized `ReflectionAttribute` object. - pub fn new(attribute: EvalAttribute, target: u64, repeated: bool) -> Self { - Self { - attribute, - target, - repeated, - } - } - - /// Returns the underlying eval-retained attribute metadata. - pub const fn attribute(&self) -> &EvalAttribute { - &self.attribute - } - - /// Returns the PHP `Attribute::TARGET_*` bitmask for this reflected owner. - pub const fn target(&self) -> u64 { - self.target - } - - /// Returns whether this owner has multiple attributes with the same name. - pub const fn is_repeated(&self) -> bool { - self.repeated - } -} - -/// Process-level eval context passed opaquely across the C ABI. -/// -/// Generated code never inspects this layout directly; it only passes pointers -/// back to the eval bridge. Keeping a concrete Rust type here lets the bridge -/// grow dynamic registries without exposing them to generated assembly. -pub struct ElephcEvalContext { - abi_version: u32, - classes: HashMap, - class_aliases: HashMap, - declared_class_names: Vec, - interfaces: HashMap, - declared_interface_names: Vec, - traits: HashMap, - declared_trait_names: Vec, - enums: HashMap, - declared_enum_names: Vec, - enum_cases: HashMap<(String, String), RuntimeCellHandle>, - enum_case_values: HashMap<(String, String), RuntimeCellHandle>, - constants: HashMap, - functions: HashMap, - closures: HashMap, - closure_objects: HashMap, - next_closure_id: usize, - native_functions: HashMap, - native_methods: HashMap<(String, String), NativeCallableSignature>, - native_static_methods: HashMap<(String, String), NativeCallableSignature>, - native_constructors: HashMap, - native_class_parents: HashMap, - native_class_attributes: HashMap>, - native_method_attributes: HashMap<(String, String), Vec>, - native_constant_attributes: HashMap<(String, String), Vec>, - native_interface_properties: HashMap>, - native_abstract_properties: HashMap>, - native_property_types: HashMap<(String, String), EvalParameterType>, - native_property_defaults: HashMap<(String, String), NativeCallableDefault>, - native_property_attributes: HashMap<(String, String), Vec>, - static_locals: HashMap<(String, String), RuntimeCellHandle>, - static_properties: HashMap<(String, String), RuntimeCellHandle>, - static_property_aliases: HashMap<(String, String), EvalReferenceTarget>, - class_constants: HashMap<(String, String), RuntimeCellHandle>, - included_files: HashSet, - dynamic_objects: HashMap, - dynamic_destructing_objects: HashSet, - dynamic_destructed_objects: HashSet, - dynamic_property_aliases: HashMap<(u64, String), EvalReferenceTarget>, - array_element_aliases: HashMap<(usize, EvalArrayReferenceKey), EvalReferenceTarget>, - dynamic_initialized_properties: HashSet<(u64, String)>, - eval_reflection_attributes: HashMap, - eval_reflection_classes: HashMap, - eval_reflection_functions: HashMap, - eval_reflection_function_closure_targets: HashMap, - eval_reflection_methods: HashMap, - eval_reflection_properties: HashMap, - eval_dynamic_reflection_properties: HashSet, - eval_reflection_class_constants: HashMap, - eval_static_callables: HashMap, - eval_object_callables: HashMap, - global_scope: Option<*mut ElephcEvalScope>, - function_stack: Vec, - class_stack: Vec, - called_class_stack: Vec, - magic_stack: Vec, - pending_throw: Option, - spl_autoload_extensions: String, - streams: EvalStreamResources, - json_last_error: i64, - json_last_error_msg: String, - default_timezone: String, - http_response_code: i64, - call_file: String, - call_dir: String, - call_line: i64, - file_magic_override: Option, -} - -impl ElephcEvalContext { - /// Creates a context using the current eval bridge ABI version. - pub fn new() -> Self { - Self { - abi_version: ABI_VERSION, - classes: HashMap::new(), - class_aliases: HashMap::new(), - declared_class_names: Vec::new(), - interfaces: HashMap::new(), - declared_interface_names: Vec::new(), - traits: HashMap::new(), - declared_trait_names: Vec::new(), - enums: HashMap::new(), - declared_enum_names: Vec::new(), - enum_cases: HashMap::new(), - enum_case_values: HashMap::new(), - constants: HashMap::new(), - functions: HashMap::new(), - closures: HashMap::new(), - closure_objects: HashMap::new(), - next_closure_id: 0, - native_functions: HashMap::new(), - native_methods: HashMap::new(), - native_static_methods: HashMap::new(), - native_constructors: HashMap::new(), - native_class_parents: HashMap::new(), - native_class_attributes: HashMap::new(), - native_method_attributes: HashMap::new(), - native_constant_attributes: HashMap::new(), - native_interface_properties: HashMap::new(), - native_abstract_properties: HashMap::new(), - native_property_types: HashMap::new(), - native_property_defaults: HashMap::new(), - native_property_attributes: HashMap::new(), - static_locals: HashMap::new(), - static_properties: HashMap::new(), - static_property_aliases: HashMap::new(), - class_constants: HashMap::new(), - included_files: HashSet::new(), - dynamic_objects: HashMap::new(), - dynamic_destructing_objects: HashSet::new(), - dynamic_destructed_objects: HashSet::new(), - dynamic_property_aliases: HashMap::new(), - array_element_aliases: HashMap::new(), - dynamic_initialized_properties: HashSet::new(), - eval_reflection_attributes: HashMap::new(), - eval_reflection_classes: HashMap::new(), - eval_reflection_functions: HashMap::new(), - eval_reflection_function_closure_targets: HashMap::new(), - eval_reflection_methods: HashMap::new(), - eval_reflection_properties: HashMap::new(), - eval_dynamic_reflection_properties: HashSet::new(), - eval_reflection_class_constants: HashMap::new(), - eval_static_callables: HashMap::new(), - eval_object_callables: HashMap::new(), - global_scope: None, - function_stack: Vec::new(), - class_stack: Vec::new(), - called_class_stack: Vec::new(), - magic_stack: Vec::new(), - pending_throw: None, - spl_autoload_extensions: String::from(".inc,.php"), - streams: EvalStreamResources::default(), - json_last_error: 0, - json_last_error_msg: String::from("No error"), - default_timezone: String::from("UTC"), - http_response_code: 200, - call_file: String::new(), - call_dir: String::new(), - call_line: 0, - file_magic_override: None, - } - } - - /// Creates a context with an explicit ABI version for compatibility tests. - #[cfg(test)] - pub fn for_abi_version(abi_version: u32) -> Self { - Self { - abi_version, - classes: HashMap::new(), - class_aliases: HashMap::new(), - declared_class_names: Vec::new(), - interfaces: HashMap::new(), - declared_interface_names: Vec::new(), - traits: HashMap::new(), - declared_trait_names: Vec::new(), - enums: HashMap::new(), - declared_enum_names: Vec::new(), - enum_cases: HashMap::new(), - enum_case_values: HashMap::new(), - constants: HashMap::new(), - functions: HashMap::new(), - closures: HashMap::new(), - closure_objects: HashMap::new(), - next_closure_id: 0, - native_functions: HashMap::new(), - native_methods: HashMap::new(), - native_static_methods: HashMap::new(), - native_constructors: HashMap::new(), - native_class_parents: HashMap::new(), - native_class_attributes: HashMap::new(), - native_method_attributes: HashMap::new(), - native_constant_attributes: HashMap::new(), - native_interface_properties: HashMap::new(), - native_abstract_properties: HashMap::new(), - native_property_types: HashMap::new(), - native_property_defaults: HashMap::new(), - native_property_attributes: HashMap::new(), - static_locals: HashMap::new(), - static_properties: HashMap::new(), - static_property_aliases: HashMap::new(), - class_constants: HashMap::new(), - included_files: HashSet::new(), - dynamic_objects: HashMap::new(), - dynamic_destructing_objects: HashSet::new(), - dynamic_destructed_objects: HashSet::new(), - dynamic_property_aliases: HashMap::new(), - array_element_aliases: HashMap::new(), - dynamic_initialized_properties: HashSet::new(), - eval_reflection_attributes: HashMap::new(), - eval_reflection_classes: HashMap::new(), - eval_reflection_functions: HashMap::new(), - eval_reflection_function_closure_targets: HashMap::new(), - eval_reflection_methods: HashMap::new(), - eval_reflection_properties: HashMap::new(), - eval_dynamic_reflection_properties: HashSet::new(), - eval_reflection_class_constants: HashMap::new(), - eval_static_callables: HashMap::new(), - eval_object_callables: HashMap::new(), - global_scope: None, - function_stack: Vec::new(), - class_stack: Vec::new(), - called_class_stack: Vec::new(), - magic_stack: Vec::new(), - pending_throw: None, - spl_autoload_extensions: String::from(".inc,.php"), - streams: EvalStreamResources::default(), - json_last_error: 0, - json_last_error_msg: String::from("No error"), - default_timezone: String::from("UTC"), - http_response_code: 200, - call_file: String::new(), - call_dir: String::new(), - call_line: 0, - file_magic_override: None, - } - } - - /// Returns the ABI version this context was created for. - pub const fn abi_version(&self) -> u32 { - self.abi_version - } - - /// Defines an eval-declared class, failing if this context already has it. - pub fn define_class(&mut self, class: EvalClass) -> bool { - let key = normalize_class_name(class.name()); - if self.classes.contains_key(&key) - || self.class_aliases.contains_key(&key) - || self.interfaces.contains_key(&key) - || self.traits.contains_key(&key) - || self.enums.contains_key(&key) - { - return false; - } - self.declared_class_names.push(class.name().to_string()); - #[cfg(not(test))] - register_global_eval_class(&class); - self.classes.insert(key, class); - true - } - - /// Imports eval-declared process-global class-like metadata not yet known by this context. - #[cfg(not(test))] - pub fn sync_global_eval_classes(&mut self) { - let Ok(registry) = global_eval_classes().lock() else { - return; - }; - for name in ®istry.declared_class_names { - let key = normalize_class_name(name); - if self.classes.contains_key(&key) - || self.interfaces.contains_key(&key) - || self.traits.contains_key(&key) - || self.enums.contains_key(&key) - || self.class_aliases.contains_key(&key) - { - continue; - } - let Some(class) = registry.classes.get(&key).cloned() else { - continue; - }; - self.declared_class_names.push(class.name().to_string()); - self.classes.insert(key, class); - } - for name in ®istry.declared_interface_names { - let key = normalize_class_name(name); - if self.interfaces.contains_key(&key) - || self.classes.contains_key(&key) - || self.traits.contains_key(&key) - || self.enums.contains_key(&key) - || self.class_aliases.contains_key(&key) - { - continue; - } - let Some(interface) = registry.interfaces.get(&key).cloned() else { - continue; - }; - self.declared_interface_names - .push(interface.name().to_string()); - self.interfaces.insert(key, interface); - } - for name in ®istry.declared_trait_names { - let key = normalize_class_name(name); - if self.traits.contains_key(&key) - || self.classes.contains_key(&key) - || self.interfaces.contains_key(&key) - || self.enums.contains_key(&key) - || self.class_aliases.contains_key(&key) - { - continue; - } - let Some(trait_decl) = registry.traits.get(&key).cloned() else { - continue; - }; - self.declared_trait_names - .push(trait_decl.name().to_string()); - self.traits.insert(key, trait_decl); - } - for name in ®istry.declared_enum_names { - let key = normalize_class_name(name); - if self.enums.contains_key(&key) - || self.classes.contains_key(&key) - || self.interfaces.contains_key(&key) - || self.traits.contains_key(&key) - || self.class_aliases.contains_key(&key) - { - continue; - } - let Some(enum_decl) = registry.enums.get(&key).cloned() else { - continue; - }; - self.declared_enum_names - .push(enum_decl.name().trim_start_matches('\\').to_string()); - self.declared_class_names - .push(enum_decl.name().trim_start_matches('\\').to_string()); - self.classes - .insert(key.clone(), enum_decl.as_class_metadata()); - self.enums.insert(key, enum_decl); - } - for (key, alias) in ®istry.aliases { - if self.classes.contains_key(key) - || self.interfaces.contains_key(key) - || self.traits.contains_key(key) - || self.enums.contains_key(key) - || self.class_aliases.contains_key(key) - { - continue; - } - self.class_aliases.insert(key.clone(), alias.clone()); - } - } - - /// Returns true when this eval context has a dynamic class or alias with the requested name. - pub fn has_class(&self, name: &str) -> bool { - let key = normalize_class_name(name); - self.classes.contains_key(&key) - || self.class_aliases.get(&key).is_some_and(|alias| { - matches!( - alias.kind, - EvalClassAliasKind::Class | EvalClassAliasKind::Enum - ) - }) - } - - /// Returns a dynamic eval class by PHP case-insensitive class name or alias. - pub fn class(&self, name: &str) -> Option<&EvalClass> { - let key = normalize_class_name(name); - if let Some(class) = self.classes.get(&key) { - return Some(class); - } - let alias = self.class_aliases.get(&key)?; - if !matches!( - alias.kind, - EvalClassAliasKind::Class | EvalClassAliasKind::Enum - ) { - return None; - } - self.classes.get(&normalize_class_name(&alias.target)) - } - - /// Resolves a PHP class name or alias to the canonical target spelling stored by eval. - pub fn resolve_class_name(&self, name: &str) -> Option { - let key = normalize_class_name(name); - if let Some(class) = self.classes.get(&key) { - return Some(class.name().to_string()); - } - self.class_aliases.get(&key).and_then(|alias| { - matches!( - alias.kind, - EvalClassAliasKind::Class | EvalClassAliasKind::Enum - ) - .then(|| alias.target.clone()) - }) - } - - /// Registers one eval-created static callable array with late-static dispatch metadata. - pub fn register_eval_static_callable( - &mut self, - callable: RuntimeCellHandle, - class_name: &str, - method: &str, - called_class: &str, - native_dispatch: Option<(&str, &str)>, - ) { - let (native_class, bridge_scope) = native_dispatch - .map(|(native_class, bridge_scope)| { - ( - Some(native_class.trim_start_matches('\\').to_string()), - Some(bridge_scope.trim_start_matches('\\').to_string()), - ) - }) - .unwrap_or((None, None)); - self.eval_static_callables.insert( - callable.as_ptr() as usize, - EvalStaticCallableMetadata { - class_name: class_name.trim_start_matches('\\').to_string(), - method: method.to_string(), - called_class: called_class.trim_start_matches('\\').to_string(), - native_class, - bridge_scope, - }, - ); - } - - /// Returns the captured late-static called class for one matching static callable array. - pub fn eval_static_callable_called_class( - &self, - callable: RuntimeCellHandle, - class_name: &str, - method: &str, - ) -> Option<&str> { - let metadata = self.eval_static_callables.get(&(callable.as_ptr() as usize))?; - let class_name = class_name.trim_start_matches('\\'); - (metadata.class_name.eq_ignore_ascii_case(class_name) - && metadata.method.eq_ignore_ascii_case(method)) - .then_some(metadata.called_class.as_str()) - } - - /// Returns native method bridge metadata captured for one static callable array. - pub fn eval_static_callable_native_dispatch( - &self, - callable: RuntimeCellHandle, - class_name: &str, - method: &str, - ) -> Option<(&str, &str)> { - let metadata = self.eval_static_callables.get(&(callable.as_ptr() as usize))?; - let class_name = class_name.trim_start_matches('\\'); - if !metadata.class_name.eq_ignore_ascii_case(class_name) - || !metadata.method.eq_ignore_ascii_case(method) - { - return None; - } - Some(( - metadata.native_class.as_deref()?, - metadata.bridge_scope.as_deref()?, - )) - } - - /// Registers one eval-created object method callable with native bridge metadata. - pub fn register_eval_object_callable( - &mut self, - callable: RuntimeCellHandle, - object: RuntimeCellHandle, - method: &str, - called_class: &str, - native_class: &str, - bridge_scope: &str, - ) { - self.eval_object_callables.insert( - callable.as_ptr() as usize, - EvalObjectCallableMetadata { - object: object.as_ptr() as usize, - method: method.to_string(), - called_class: called_class.trim_start_matches('\\').to_string(), - native_class: native_class.trim_start_matches('\\').to_string(), - bridge_scope: bridge_scope.trim_start_matches('\\').to_string(), - }, - ); - } - - /// Returns native method bridge metadata captured for one object callable array. - pub fn eval_object_callable_native_dispatch( - &self, - callable: RuntimeCellHandle, - object: RuntimeCellHandle, - method: &str, - ) -> Option<(&str, &str, &str)> { - let metadata = self - .eval_object_callables - .get(&(callable.as_ptr() as usize))?; - (metadata.object == object.as_ptr() as usize - && metadata.method.eq_ignore_ascii_case(method)) - .then_some(( - metadata.native_class.as_str(), - metadata.bridge_scope.as_str(), - metadata.called_class.as_str(), - )) - } - - /// Resolves a PHP class-like name to eval class, interface, trait, or alias spelling. - pub fn resolve_class_like_name(&self, name: &str) -> Option { - let key = normalize_class_name(name); - if let Some(class) = self.classes.get(&key) { - return Some(class.name().to_string()); - } - if let Some(interface) = self.interfaces.get(&key) { - return Some(interface.name().to_string()); - } - if let Some(trait_decl) = self.traits.get(&key) { - return Some(trait_decl.name().to_string()); - } - if let Some(enum_decl) = self.enums.get(&key) { - return Some(enum_decl.name().to_string()); - } - self.class_aliases - .get(&key) - .map(|alias| alias.target.clone()) - } - - /// Defines an alias for an eval-declared class or an already known alias. - pub fn define_class_alias(&mut self, original: &str, alias: &str) -> bool { - let Some((target, kind)) = self.resolve_class_like_alias_target(original) else { - return false; - }; - self.define_class_alias_with_kind(&target, alias, kind) - } - - /// Defines an alias for a runtime-visible class whose metadata lives outside eval. - pub fn define_external_class_alias(&mut self, original: &str, alias: &str) -> bool { - self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Class) - } - - /// Defines an alias for a runtime-visible interface whose metadata lives outside eval. - pub fn define_external_interface_alias(&mut self, original: &str, alias: &str) -> bool { - self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Interface) - } - - /// Defines an alias for a runtime-visible trait whose metadata lives outside eval. - pub fn define_external_trait_alias(&mut self, original: &str, alias: &str) -> bool { - self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Trait) - } - - /// Defines an alias for a runtime-visible enum whose metadata lives outside eval. - pub fn define_external_enum_alias(&mut self, original: &str, alias: &str) -> bool { - self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Enum) - } - - /// Resolves the canonical target and declaration kind for a class-like alias source. - fn resolve_class_like_alias_target( - &self, - original: &str, - ) -> Option<(String, EvalClassAliasKind)> { - let key = normalize_class_name(original); - if let Some(enum_decl) = self.enums.get(&key) { - return Some((enum_decl.name().to_string(), EvalClassAliasKind::Enum)); - } - if let Some(class) = self.classes.get(&key) { - return Some((class.name().to_string(), EvalClassAliasKind::Class)); - } - if let Some(interface) = self.interfaces.get(&key) { - return Some((interface.name().to_string(), EvalClassAliasKind::Interface)); - } - if let Some(trait_decl) = self.traits.get(&key) { - return Some((trait_decl.name().to_string(), EvalClassAliasKind::Trait)); - } - self.class_aliases - .get(&key) - .map(|alias| (alias.target.clone(), alias.kind)) - } - - /// Defines one class-like alias after the caller has resolved the target kind. - fn define_class_alias_with_kind( - &mut self, - original: &str, - alias: &str, - kind: EvalClassAliasKind, - ) -> bool { - let alias_key = normalize_class_name(alias); - if alias_key.is_empty() - || self.classes.contains_key(&alias_key) - || self.interfaces.contains_key(&alias_key) - || self.traits.contains_key(&alias_key) - || self.enums.contains_key(&alias_key) - || self.class_aliases.contains_key(&alias_key) - { - return false; - } - let alias_record = EvalClassAlias { - target: original.trim_start_matches('\\').to_string(), - kind, - }; - #[cfg(not(test))] - register_global_eval_alias(alias, &alias_record); - self.class_aliases.insert(alias_key, alias_record); - true - } - - /// Returns class names declared through eval or registered from generated metadata. - pub fn declared_class_names(&self) -> &[String] { - &self.declared_class_names - } - - /// Registers a runtime-visible class or enum declaration name for `get_declared_classes()`. - pub fn define_external_declared_class_name(&mut self, name: &str) -> bool { - push_external_declared_name(&mut self.declared_class_names, name) - } - - /// Defines an eval-declared interface, failing if this context already has the name. - pub fn define_interface(&mut self, interface: EvalInterface) -> bool { - let key = normalize_class_name(interface.name()); - if self.interfaces.contains_key(&key) - || self.classes.contains_key(&key) - || self.traits.contains_key(&key) - || self.enums.contains_key(&key) - || self.class_aliases.contains_key(&key) - { - return false; - } - self.declared_interface_names - .push(interface.name().to_string()); - #[cfg(not(test))] - register_global_eval_interface(&interface); - self.interfaces.insert(key, interface); - true - } - - /// Returns true when this eval context has a dynamic interface with the requested name. - pub fn has_interface(&self, name: &str) -> bool { - let key = normalize_class_name(name); - self.interfaces.contains_key(&key) - || self - .class_aliases - .get(&key) - .is_some_and(|alias| alias.kind == EvalClassAliasKind::Interface) - } - - /// Returns a dynamic eval interface by PHP case-insensitive interface name. - pub fn interface(&self, name: &str) -> Option<&EvalInterface> { - let key = normalize_class_name(name); - if let Some(interface) = self.interfaces.get(&key) { - return Some(interface); - } - let alias = self.class_aliases.get(&key)?; - (alias.kind == EvalClassAliasKind::Interface) - .then(|| self.interfaces.get(&normalize_class_name(&alias.target))) - .flatten() - } - - /// Returns interface names declared through eval or registered from generated metadata. - pub fn declared_interface_names(&self) -> &[String] { - &self.declared_interface_names - } - - /// Registers a runtime-visible interface declaration name for `get_declared_interfaces()`. - pub fn define_external_declared_interface_name(&mut self, name: &str) -> bool { - push_external_declared_name(&mut self.declared_interface_names, name) - } - - /// Defines an eval-declared trait, failing if this context already has the name. - pub fn define_trait(&mut self, trait_decl: EvalTrait) -> bool { - let key = normalize_class_name(trait_decl.name()); - if self.traits.contains_key(&key) - || self.classes.contains_key(&key) - || self.interfaces.contains_key(&key) - || self.enums.contains_key(&key) - || self.class_aliases.contains_key(&key) - { - return false; - } - self.declared_trait_names - .push(trait_decl.name().to_string()); - #[cfg(not(test))] - register_global_eval_trait(&trait_decl); - self.traits.insert(key, trait_decl); - true - } - - /// Returns true when this eval context has a dynamic trait with the requested name. - pub fn has_trait(&self, name: &str) -> bool { - let key = normalize_class_name(name); - self.traits.contains_key(&key) - || self - .class_aliases - .get(&key) - .is_some_and(|alias| alias.kind == EvalClassAliasKind::Trait) - } - - /// Returns a dynamic eval trait by PHP case-insensitive trait name. - pub fn trait_decl(&self, name: &str) -> Option<&EvalTrait> { - let key = normalize_class_name(name); - if let Some(trait_decl) = self.traits.get(&key) { - return Some(trait_decl); - } - let alias = self.class_aliases.get(&key)?; - (alias.kind == EvalClassAliasKind::Trait) - .then(|| self.traits.get(&normalize_class_name(&alias.target))) - .flatten() - } - - /// Returns trait names declared through eval or registered from generated metadata. - pub fn declared_trait_names(&self) -> &[String] { - &self.declared_trait_names - } - - /// Registers a runtime-visible trait declaration name for `get_declared_traits()`. - pub fn define_external_declared_trait_name(&mut self, name: &str) -> bool { - push_external_declared_name(&mut self.declared_trait_names, name) - } - - /// Defines an eval-declared enum plus class-shaped metadata for dispatch. - pub fn define_enum(&mut self, enum_decl: EvalEnum) -> bool { - let key = normalize_class_name(enum_decl.name()); - if self.enums.contains_key(&key) - || self.classes.contains_key(&key) - || self.interfaces.contains_key(&key) - || self.traits.contains_key(&key) - || self.class_aliases.contains_key(&key) - { - return false; - } - self.declared_enum_names - .push(enum_decl.name().trim_start_matches('\\').to_string()); - self.declared_class_names - .push(enum_decl.name().trim_start_matches('\\').to_string()); - #[cfg(not(test))] - register_global_eval_enum(&enum_decl); - self.classes - .insert(key.clone(), enum_decl.as_class_metadata()); - self.enums.insert(key, enum_decl); - true - } - - /// Returns true when this eval context has a dynamic enum with the requested name. - pub fn has_enum(&self, name: &str) -> bool { - let key = normalize_class_name(name); - self.enums.contains_key(&key) - || self - .class_aliases - .get(&key) - .is_some_and(|alias| alias.kind == EvalClassAliasKind::Enum) - } - - /// Returns a dynamic eval enum by PHP case-insensitive enum name. - pub fn enum_decl(&self, name: &str) -> Option<&EvalEnum> { - let key = normalize_class_name(name); - if let Some(enum_decl) = self.enums.get(&key) { - return Some(enum_decl); - } - let alias = self.class_aliases.get(&key)?; - (alias.kind == EvalClassAliasKind::Enum) - .then(|| self.enums.get(&normalize_class_name(&alias.target))) - .flatten() - } - - /// Resolves an enum name or enum alias to the canonical eval enum spelling. - pub fn resolve_enum_name(&self, name: &str) -> Option { - let key = normalize_class_name(name); - if let Some(enum_decl) = self.enums.get(&key) { - return Some(enum_decl.name().to_string()); - } - self.class_aliases.get(&key).and_then(|alias| { - (alias.kind == EvalClassAliasKind::Enum).then(|| alias.target.clone()) - }) - } - - /// Returns enum names declared through eval in PHP-visible order. - pub fn declared_enum_names(&self) -> &[String] { - &self.declared_enum_names - } - - /// Returns a materialized singleton case object for one eval enum case. - pub fn enum_case(&self, enum_name: &str, case_name: &str) -> Option { - let enum_name = self - .resolve_enum_name(enum_name) - .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); - self.enum_cases - .get(&( - normalize_class_name(&enum_name), - normalize_enum_case_name(case_name), - )) - .copied() - } - - /// Stores a materialized singleton case object and returns any replaced distinct cell. - pub fn set_enum_case( - &mut self, - enum_name: &str, - case_name: &str, - cell: RuntimeCellHandle, - ) -> Option { - let previous = self.enum_cases.insert( - ( - normalize_class_name(enum_name), - normalize_enum_case_name(case_name), - ), - cell, - ); - previous.filter(|previous| *previous != cell) - } - - /// Returns a materialized backing value for one eval backed-enum case. - pub fn enum_case_value(&self, enum_name: &str, case_name: &str) -> Option { - let enum_name = self - .resolve_enum_name(enum_name) - .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); - self.enum_case_values - .get(&( - normalize_class_name(&enum_name), - normalize_enum_case_name(case_name), - )) - .copied() - } - - /// Stores a materialized backing value and returns any replaced distinct cell. - pub fn set_enum_case_value( - &mut self, - enum_name: &str, - case_name: &str, - cell: RuntimeCellHandle, - ) -> Option { - let previous = self.enum_case_values.insert( - ( - normalize_class_name(enum_name), - normalize_enum_case_name(case_name), - ), - cell, - ); - previous.filter(|previous| *previous != cell) - } - - /// Records that one runtime object handle was created for an eval-declared class. - pub fn register_dynamic_object(&mut self, identity: u64, class_name: &str) { - let class_name = self - .resolve_class_name(class_name) - .unwrap_or_else(|| class_name.to_string()); - self.dynamic_objects - .insert(identity, normalize_class_name(&class_name)); - crate::ffi::dynamic_destructors::register_dynamic_object_context(identity, self as *mut Self); - self.dynamic_destructing_objects.remove(&identity); - self.dynamic_destructed_objects.remove(&identity); - self.dynamic_initialized_properties - .retain(|(object, _)| *object != identity); - } - - /// Removes one dynamic object identity and all per-object eval metadata. - pub fn forget_dynamic_object(&mut self, identity: u64) { - self.dynamic_objects.remove(&identity); - self.closure_objects.remove(&identity); - self.dynamic_destructing_objects.remove(&identity); - self.dynamic_destructed_objects.remove(&identity); - self.dynamic_property_aliases - .retain(|(object, _), _| *object != identity); - self.dynamic_initialized_properties - .retain(|(object, _)| *object != identity); - crate::ffi::dynamic_destructors::unregister_dynamic_object(identity); - } - - /// Removes this context from the process-local dynamic object destructor registry. - pub fn unregister_dynamic_object_context(&self) { - crate::ffi::dynamic_destructors::unregister_dynamic_objects_for_context( - self as *const Self as *mut Self, - ); - } - - /// Returns the dynamic eval class metadata associated with one object identity. - pub fn dynamic_object_class(&self, identity: u64) -> Option<&EvalClass> { - if let Some(class_key) = self.dynamic_objects.get(&identity) { - return self.classes.get(class_key); - } - #[cfg(not(test))] - { - let owner = crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity)?; - let owner = unsafe { owner.as_ref()? }; - if owner.abi_version() != ABI_VERSION { - return None; - } - let class_key = owner.dynamic_objects.get(&identity)?; - self.classes.get(class_key) - } - #[cfg(test)] - { - None - } - } - - /// Returns the PHP-visible eval class name associated with one dynamic object identity. - pub fn dynamic_object_class_name(&self, identity: u64) -> Option { - if self.closure_objects.contains_key(&identity) { - return Some(String::from("Closure")); - } - if let Some(class) = self.dynamic_object_class(identity) { - return Some(class.name().trim_start_matches('\\').to_string()); - } - #[cfg(not(test))] - { - let owner = crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity)?; - let owner = unsafe { owner.as_ref()? }; - if owner.abi_version() != ABI_VERSION { - return None; - } - owner - .dynamic_object_class(identity) - .map(|class| class.name().trim_start_matches('\\').to_string()) - } - #[cfg(test)] - { - None - } - } - - /// Marks one dynamic object's destructor as active if it has not already run. - pub fn begin_dynamic_object_destructor(&mut self, identity: u64) -> bool { - if self.dynamic_destructed_objects.contains(&identity) { - return false; - } - if !self.dynamic_destructing_objects.insert(identity) { - return false; - } - self.dynamic_destructed_objects.insert(identity); - true - } - - /// Clears the active destructor guard for one dynamic object identity. - pub fn finish_dynamic_object_destructor(&mut self, identity: u64) { - self.dynamic_destructing_objects.remove(&identity); - } - - /// Returns whether one dynamic object identity was registered with a class-like name. - pub fn dynamic_object_is_class(&self, identity: u64, class_name: &str) -> bool { - let class_name = normalize_class_name(class_name); - if class_name == "closure" && self.closure_objects.contains_key(&identity) { - return true; - } - if self - .dynamic_objects - .get(&identity) - .is_some_and(|class_key| class_key == &class_name) - { - return true; - } - #[cfg(not(test))] - { - let Some(owner) = - crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity) - else { - return false; - }; - let Some(owner) = (unsafe { owner.as_ref() }) else { - return false; - }; - if owner.abi_version() != ABI_VERSION { - return false; - } - owner - .dynamic_objects - .get(&identity) - .is_some_and(|class_key| class_key == &class_name) - } - #[cfg(test)] - { - false - } - } - - /// Binds one eval object property slot to a persistent PHP reference target. - pub fn bind_dynamic_property_alias( - &mut self, - identity: u64, - storage_property_name: &str, - target: EvalReferenceTarget, - ) -> Option { - self.dynamic_property_aliases - .insert((identity, storage_property_name.to_string()), target) - } - - /// Returns the persistent reference target bound to one eval object property slot. - pub fn dynamic_property_alias( - &self, - identity: u64, - storage_property_name: &str, - ) -> Option<&EvalReferenceTarget> { - self.dynamic_property_aliases - .get(&(identity, storage_property_name.to_string())) - } - - /// Removes the persistent reference target for one eval object property slot. - pub fn remove_dynamic_property_alias( - &mut self, - identity: u64, - storage_property_name: &str, - ) -> Option { - self.dynamic_property_aliases - .remove(&(identity, storage_property_name.to_string())) - } - - /// Binds one runtime array element slot to a PHP reference target. - pub fn bind_array_element_alias( - &mut self, - array: RuntimeCellHandle, - key: EvalArrayReferenceKey, - target: EvalReferenceTarget, - ) -> Option { - self.array_element_aliases - .insert((array.as_ptr() as usize, key), target) - } - - /// Returns the persistent reference target bound to one runtime array element slot. - pub fn array_element_alias( - &self, - array: RuntimeCellHandle, - key: &EvalArrayReferenceKey, - ) -> Option<&EvalReferenceTarget> { - self.array_element_aliases - .get(&(array.as_ptr() as usize, key.clone())) - } - - /// Marks one eval object storage slot as initialized. - pub fn mark_dynamic_property_initialized( - &mut self, - identity: u64, - storage_property_name: &str, - ) { - self.dynamic_initialized_properties - .insert((identity, storage_property_name.to_string())); - } - - /// Marks one eval object storage slot as uninitialized. - pub fn mark_dynamic_property_uninitialized( - &mut self, - identity: u64, - storage_property_name: &str, - ) { - self.dynamic_initialized_properties - .remove(&(identity, storage_property_name.to_string())); - } - - /// Returns whether one eval object storage slot is known to be initialized. - pub fn dynamic_property_is_initialized( - &self, - identity: u64, - storage_property_name: &str, - ) -> bool { - self.dynamic_initialized_properties - .contains(&(identity, storage_property_name.to_string())) - } - - /// Copies persistent property aliases from a source object identity to a clone identity. - pub fn clone_dynamic_property_aliases(&mut self, source_identity: u64, clone_identity: u64) { - let aliases = self - .dynamic_property_aliases - .iter() - .filter_map(|((identity, property), target)| { - (*identity == source_identity).then(|| (property.clone(), target.clone())) - }) - .collect::>(); - for (property, target) in aliases { - self.bind_dynamic_property_alias(clone_identity, &property, target); - } - let initialized = self - .dynamic_initialized_properties - .iter() - .filter_map(|(identity, property)| { - (*identity == source_identity).then(|| property.clone()) - }) - .collect::>(); - for property in initialized { - self.mark_dynamic_property_initialized(clone_identity, &property); - } - } - - /// Records eval-declared attribute metadata for one synthetic ReflectionAttribute object. - pub fn register_eval_reflection_attribute( - &mut self, - identity: u64, - attribute: EvalAttribute, - target: u64, - repeated: bool, - ) { - self.eval_reflection_attributes.insert( - identity, - EvalReflectionAttributeMetadata::new(attribute, target, repeated), - ); - } - - /// Returns eval-declared attribute metadata attached to a synthetic ReflectionAttribute. - pub fn eval_reflection_attribute( - &self, - identity: u64, - ) -> Option<&EvalReflectionAttributeMetadata> { - self.eval_reflection_attributes.get(&identity) - } - - /// Records reflected class metadata for one synthetic ReflectionClass object. - pub fn register_eval_reflection_class(&mut self, identity: u64, class_name: &str) { - self.eval_reflection_classes - .insert(identity, class_name.trim_start_matches('\\').to_string()); - } - - /// Returns the reflected class name attached to a synthetic ReflectionClass. - pub fn eval_reflection_class_name(&self, identity: u64) -> Option<&str> { - self.eval_reflection_classes - .get(&identity) - .map(String::as_str) - } - - /// Records reflected function metadata for one synthetic ReflectionFunction object. - pub fn register_eval_reflection_function(&mut self, identity: u64, function_name: &str) { - self.eval_reflection_functions - .insert(identity, function_name.trim_start_matches('\\').to_string()); - } - - /// Returns the reflected function name attached to a synthetic ReflectionFunction. - pub fn eval_reflection_function_name(&self, identity: u64) -> Option<&str> { - self.eval_reflection_functions - .get(&identity) - .map(String::as_str) - } - - /// Records the callable target behind a `Closure` reflected as a function. - pub fn register_eval_reflection_function_closure_target( - &mut self, - identity: u64, - target: EvalClosureObjectTarget, - ) { - self.eval_reflection_function_closure_targets - .insert(identity, target); - } - - /// Returns the callable target retained for a reflected `Closure` object. - pub fn eval_reflection_function_closure_target( - &self, - identity: u64, - ) -> Option<&EvalClosureObjectTarget> { - self.eval_reflection_function_closure_targets - .get(&identity) - } - - /// Records reflected method metadata for one synthetic ReflectionMethod object. - pub fn register_eval_reflection_method( - &mut self, - identity: u64, - declaring_class: &str, - method_name: &str, - ) { - self.eval_reflection_methods.insert( - identity, - ( - declaring_class.trim_start_matches('\\').to_string(), - method_name.to_string(), - ), - ); - } - - /// Returns the declaring class and method name attached to a synthetic ReflectionMethod. - pub fn eval_reflection_method(&self, identity: u64) -> Option<(&str, &str)> { - self.eval_reflection_methods - .get(&identity) - .map(|(class, method)| (class.as_str(), method.as_str())) - } - - /// Records reflected property metadata for one synthetic ReflectionProperty object. - pub fn register_eval_reflection_property( - &mut self, - identity: u64, - declaring_class: &str, - property_name: &str, - ) { - self.eval_reflection_properties.insert( - identity, - ( - declaring_class.trim_start_matches('\\').to_string(), - property_name.to_string(), - ), - ); - self.eval_dynamic_reflection_properties.remove(&identity); - } - - /// Records reflected dynamic-property metadata for one synthetic ReflectionProperty object. - pub fn register_eval_dynamic_reflection_property( - &mut self, - identity: u64, - declaring_class: &str, - property_name: &str, - ) { - self.register_eval_reflection_property(identity, declaring_class, property_name); - self.eval_dynamic_reflection_properties.insert(identity); - } - - /// Returns the declaring class and property name attached to a synthetic ReflectionProperty. - pub fn eval_reflection_property(&self, identity: u64) -> Option<(&str, &str)> { - self.eval_reflection_properties - .get(&identity) - .map(|(class, property)| (class.as_str(), property.as_str())) - } - - /// Returns whether a synthetic ReflectionProperty represents a dynamic property. - pub fn eval_reflection_property_is_dynamic(&self, identity: u64) -> bool { - self.eval_dynamic_reflection_properties.contains(&identity) - } - - /// Records reflected class constant or enum case metadata for one synthetic object. - pub fn register_eval_reflection_class_constant( - &mut self, - identity: u64, - declaring_class: &str, - constant_name: &str, - owner_kind: u64, - ) { - self.eval_reflection_class_constants.insert( - identity, - ( - declaring_class.trim_start_matches('\\').to_string(), - constant_name.to_string(), - owner_kind, - ), - ); - } - - /// Returns the declaring class, name, and reflection owner kind for a synthetic constant. - pub fn eval_reflection_class_constant(&self, identity: u64) -> Option<(&str, &str, u64)> { - self.eval_reflection_class_constants - .get(&identity) - .map(|(class, constant, owner_kind)| (class.as_str(), constant.as_str(), *owner_kind)) - } - - /// Returns eval-declared class metadata from parent to child for construction. - pub fn class_chain(&self, name: &str) -> Vec { - let mut chain = Vec::new(); - let mut seen = HashSet::new(); - self.collect_class_chain(name, &mut chain, &mut seen); - chain - } - - /// Collects one eval-declared class ancestry chain without following cycles. - fn collect_class_chain( - &self, - name: &str, - chain: &mut Vec, - seen: &mut HashSet, - ) { - let key = normalize_class_name(name); - if !seen.insert(key.clone()) { - return; - } - let Some(class) = self.classes.get(&key) else { - return; - }; - if let Some(parent) = class.parent() { - self.collect_class_chain(parent, chain, seen); - } - chain.push(class.clone()); - } - - /// Finds a method in an eval-declared class or its eval-declared parents. - pub fn class_method( - &self, - class_name: &str, - method_name: &str, - ) -> Option<(String, EvalClassMethod)> { - let mut current_name = self.resolve_class_name(class_name)?; - let mut seen = HashSet::new(); - loop { - let key = normalize_class_name(¤t_name); - if !seen.insert(key.clone()) { - return None; - } - let class = self.classes.get(&key)?; - if let Some(method) = class.method(method_name) { - return Some((class.name().to_string(), method.clone())); - } - current_name = class.parent()?.to_string(); - } - } - - /// Finds a method declared directly by one eval-declared class. - pub fn class_own_method( - &self, - class_name: &str, - method_name: &str, - ) -> Option<(String, EvalClassMethod)> { - let class = self.class(class_name)?; - class - .method(method_name) - .map(|method| (class.name().to_string(), method.clone())) - } - - /// Finds a class-like constant on an eval class, interface, trait, or inherited relation. - pub fn class_constant( - &self, - class_name: &str, - constant_name: &str, - ) -> Option<(String, EvalClassConstant)> { - if self.has_class(class_name) { - return self.class_or_interface_constant(class_name, constant_name); - } - if self.has_interface(class_name) { - return self.interface_constant(class_name, constant_name); - } - if let Some(trait_decl) = self.trait_decl(class_name) { - if let Some(constant) = trait_decl.constant(constant_name) { - return Some((trait_decl.name().to_string(), constant.clone())); - } - } - None - } - - /// Finds a class constant in an eval-declared class, parents, or implemented interfaces. - fn class_or_interface_constant( - &self, - class_name: &str, - constant_name: &str, - ) -> Option<(String, EvalClassConstant)> { - let mut current_name = self.resolve_class_name(class_name)?; - let mut seen = HashSet::new(); - loop { - let key = normalize_class_name(¤t_name); - if !seen.insert(key.clone()) { - return None; - } - let class = self.classes.get(&key)?; - if let Some(constant) = class.constant(constant_name) { - return Some((class.name().to_string(), constant.clone())); - } - if let Some(parent) = class.parent() { - current_name = parent.to_string(); - } else { - break; - } - } - for interface_name in self.class_interface_names(class_name) { - if let Some(found) = self.interface_constant(&interface_name, constant_name) { - return Some(found); - } - } - None - } - - /// Finds a constant declared on an eval interface or inherited parent interface. - pub fn interface_constant( - &self, - interface_name: &str, - constant_name: &str, - ) -> Option<(String, EvalClassConstant)> { - let interface = self.interface(interface_name)?; - if let Some(constant) = interface.constant(constant_name) { - return Some((interface.name().to_string(), constant.clone())); - } - for parent in interface.parents() { - if let Some(found) = self.interface_constant(parent, constant_name) { - return Some(found); - } - } - None - } - - /// Finds a class constant declared directly by one eval-declared class. - pub fn class_own_constant( - &self, - class_name: &str, - constant_name: &str, - ) -> Option<(String, EvalClassConstant)> { - let class = self.class(class_name)?; - class - .constant(constant_name) - .map(|constant| (class.name().to_string(), constant.clone())) - } - - /// Finds a property in an eval-declared class or its eval-declared parents. - pub fn class_property( - &self, - class_name: &str, - property_name: &str, - ) -> Option<(String, EvalClassProperty)> { - let mut current_name = self.resolve_class_name(class_name)?; - let mut seen = HashSet::new(); - loop { - let key = normalize_class_name(¤t_name); - if !seen.insert(key.clone()) { - return None; - } - let class = self.classes.get(&key)?; - if let Some(property) = class - .properties() - .iter() - .find(|property| property.name() == property_name) - { - return Some((class.name().to_string(), property.clone())); - } - current_name = class.parent()?.to_string(); - } - } - - /// Finds a property declared directly by one eval-declared class. - pub fn class_own_property( - &self, - class_name: &str, - property_name: &str, - ) -> Option<(String, EvalClassProperty)> { - let class = self.class(class_name)?; - class - .properties() - .iter() - .find(|property| property.name() == property_name) - .map(|property| (class.name().to_string(), property.clone())) - } - - /// Returns direct and inherited parent class names for an eval-declared class. - pub fn class_parent_names(&self, class_name: &str) -> Vec { - let mut parents = Vec::new(); - let mut current = self - .class(class_name) - .and_then(EvalClass::parent) - .map(str::to_string) - .or_else(|| self.native_class_parent(class_name).map(str::to_string)); - let mut seen = HashSet::new(); - while let Some(parent) = current { - let parent = self - .resolve_class_name(&parent) - .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); - let key = normalize_class_name(&parent); - if !seen.insert(key) { - break; - } - if let Some(parent_class) = self.class(&parent) { - parents.push(parent_class.name().trim_start_matches('\\').to_string()); - current = parent_class - .parent() - .map(str::to_string) - .or_else(|| self.native_class_parent(parent_class.name()).map(str::to_string)); - } else { - parents.push(parent); - current = parents - .last() - .and_then(|parent| self.native_class_parent(parent)) - .map(str::to_string); - } - } - parents - } - - /// Returns the nearest runtime/AOT parent backing an eval class hierarchy. - pub fn class_native_parent_name(&self, class_name: &str) -> Option { - let mut current = self - .resolve_class_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - let mut seen = HashSet::new(); - loop { - if !seen.insert(normalize_class_name(¤t)) { - return None; - } - let parent = self - .class(¤t) - .and_then(EvalClass::parent) - .map(str::to_string) - .or_else(|| self.native_class_parent(¤t).map(str::to_string))?; - let parent = self - .resolve_class_name(&parent) - .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); - if self.class(&parent).is_none() { - return Some(parent); - } - current = parent; - } - } - - /// Returns direct and inherited interface names for an eval-declared class. - pub fn class_interface_names(&self, class_name: &str) -> Vec { - let mut interfaces = Vec::new(); - let mut seen = HashSet::new(); - let is_enum = self.enum_decl(class_name).is_some(); - for class in self.class_chain(class_name) { - for interface in class.interfaces() { - push_unique_class_name(interface, &mut interfaces, &mut seen); - self.collect_class_interface_parent_names( - interface, - is_enum, - &mut interfaces, - &mut seen, - ); - } - } - if let Some(enum_decl) = self.enum_decl(class_name) { - push_unique_class_name("UnitEnum", &mut interfaces, &mut seen); - if enum_decl.backing_type().is_some() { - push_unique_class_name("BackedEnum", &mut interfaces, &mut seen); - } - } - interfaces - } - - /// Collects interface parents while preserving PHP enum marker interface ordering. - fn collect_class_interface_parent_names( - &self, - interface_name: &str, - skip_enum_markers: bool, - names: &mut Vec, - seen: &mut HashSet, - ) { - let Some(interface) = self.interface(interface_name) else { - return; - }; - for parent in interface.parents() { - if skip_enum_markers && is_php_enum_marker_interface(parent) { - continue; - } - push_unique_class_name(parent, names, seen); - self.collect_class_interface_parent_names(parent, skip_enum_markers, names, seen); - } - } - - /// Returns trait names used directly by an eval-declared class. - pub fn class_trait_names(&self, class_name: &str) -> Vec { - self.class(class_name).map_or_else(Vec::new, |class| { - let mut traits = Vec::new(); - let mut seen = HashSet::new(); - for trait_name in class.traits() { - push_unique_class_name(trait_name, &mut traits, &mut seen); - } - traits - }) - } - - /// Returns trait method aliases declared directly by an eval-declared class. - pub fn class_trait_aliases(&self, class_name: &str) -> Vec<(String, String)> { - let Some(class) = self.class(class_name) else { - return Vec::new(); - }; - let mut aliases = Vec::new(); - for adaptation in class.trait_adaptations() { - let EvalTraitAdaptation::Alias { - trait_name, - method, - alias: Some(alias), - .. - } = adaptation - else { - continue; - }; - let Some(source_trait) = - self.class_trait_alias_source(class, trait_name.as_deref(), method) - else { - continue; - }; - aliases.push((alias.clone(), format!("{source_trait}::{method}"))); - } - aliases - } - - /// Returns trait names used directly by an eval-declared trait. - pub fn trait_trait_names(&self, trait_name: &str) -> Vec { - self.trait_decl(trait_name).map_or_else(Vec::new, |trait_decl| { - let mut traits = Vec::new(); - let mut seen = HashSet::new(); - for used_trait in trait_decl.traits() { - push_unique_class_name(used_trait, &mut traits, &mut seen); - } - traits - }) - } - - /// Returns trait method aliases declared directly by an eval-declared trait. - pub fn trait_trait_aliases(&self, trait_name: &str) -> Vec<(String, String)> { - let Some(trait_decl) = self.trait_decl(trait_name) else { - return Vec::new(); - }; - let mut aliases = Vec::new(); - for adaptation in trait_decl.trait_adaptations() { - let EvalTraitAdaptation::Alias { - trait_name, - method, - alias: Some(alias), - .. - } = adaptation - else { - continue; - }; - let Some(source_trait) = - self.trait_trait_alias_source(trait_decl, trait_name.as_deref(), method) - else { - continue; - }; - aliases.push((alias.clone(), format!("{source_trait}::{method}"))); - } - aliases - } - - /// Resolves the trait name shown in `ReflectionClass::getTraitAliases()`. - fn class_trait_alias_source( - &self, - class: &EvalClass, - explicit_trait: Option<&str>, - method: &str, - ) -> Option { - if let Some(trait_name) = explicit_trait { - return Some( - self.trait_decl(trait_name) - .map_or(trait_name, EvalTrait::name) - .trim_start_matches('\\') - .to_string(), - ); - } - class.traits().iter().find_map(|trait_name| { - let trait_decl = self.trait_decl(trait_name)?; - trait_decl - .methods() - .iter() - .any(|candidate| candidate.name().eq_ignore_ascii_case(method)) - .then(|| trait_decl.name().trim_start_matches('\\').to_string()) - }) - } - - /// Resolves the trait name shown for a trait's internal `getTraitAliases()`. - fn trait_trait_alias_source( - &self, - trait_decl: &EvalTrait, - explicit_trait: Option<&str>, - method: &str, - ) -> Option { - if let Some(trait_name) = explicit_trait { - return Some( - self.trait_decl(trait_name) - .map_or(trait_name, EvalTrait::name) - .trim_start_matches('\\') - .to_string(), - ); - } - trait_decl.traits().iter().find_map(|trait_name| { - let used_trait_decl = self.trait_decl(trait_name)?; - used_trait_decl - .methods() - .iter() - .any(|candidate| candidate.name().eq_ignore_ascii_case(method)) - .then(|| used_trait_decl.name().trim_start_matches('\\').to_string()) - }) - } - - /// Returns PHP case-insensitive method names visible to `ReflectionClass::hasMethod()`. - pub fn class_method_names(&self, class_name: &str) -> Vec { - let mut names = Vec::new(); - let mut seen = HashSet::new(); - for class in self.class_chain(class_name).into_iter().rev() { - for method in class.methods() { - push_unique_method_name(method.name(), &mut names, &mut seen); - } - if let Some(enum_decl) = self.enum_decl(class.name()) { - push_unique_method_name("cases", &mut names, &mut seen); - if enum_decl.backing_type().is_some() { - push_unique_method_name("from", &mut names, &mut seen); - push_unique_method_name("tryFrom", &mut names, &mut seen); - } - } - } - names - } - - /// Returns PHP case-sensitive property names visible to `ReflectionClass::hasProperty()`. - pub fn class_property_names(&self, class_name: &str) -> Vec { - let reflected_name = self - .resolve_class_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - let mut names = Vec::new(); - let mut seen = HashSet::new(); - if let Some(enum_decl) = self.enum_decl(&reflected_name) { - push_unique_property_name("name", &mut names, &mut seen); - if enum_decl.backing_type().is_some() { - push_unique_property_name("value", &mut names, &mut seen); - } - } - for class in self.class_chain(&reflected_name) { - let declaring_is_reflected = same_class_name(class.name(), &reflected_name); - for property in class.properties() { - if property.visibility() == EvalVisibility::Private && !declaring_is_reflected { - continue; - } - push_unique_property_name(property.name(), &mut names, &mut seen); - } - } - names - } - - /// Returns PHP case-sensitive constant names visible to `ReflectionClass::hasConstant()`. - pub fn class_constant_names(&self, class_name: &str) -> Vec { - let reflected_name = self - .resolve_class_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - let mut names = Vec::new(); - let mut seen = HashSet::new(); - if let Some(enum_decl) = self.enum_decl(&reflected_name) { - for case in enum_decl.cases() { - push_unique_constant_name(case.name(), &mut names, &mut seen); - } - } - for class in self.class_chain(&reflected_name).into_iter().rev() { - for constant in class.constants() { - push_unique_constant_name(constant.name(), &mut names, &mut seen); - } - for interface_name in class.interfaces() { - for constant in self.interface_constant_names(interface_name) { - push_unique_constant_name(&constant, &mut names, &mut seen); - } - } - } - names - } - - /// Returns PHP case-insensitive method names declared by an eval interface hierarchy. - pub fn interface_method_names(&self, interface_name: &str) -> Vec { - let mut names = Vec::new(); - let mut seen = HashSet::new(); - for method in self.interface_method_requirements(interface_name) { - push_unique_method_name(method.name(), &mut names, &mut seen); - } - names - } - - /// Returns PHP case-sensitive property names declared by an eval interface hierarchy. - pub fn interface_property_names(&self, interface_name: &str) -> Vec { - let mut names = Vec::new(); - let mut seen = HashSet::new(); - for property in self.interface_property_requirements(interface_name) { - push_unique_property_name(property.name(), &mut names, &mut seen); - } - names - } - - /// Returns PHP case-sensitive constant names declared by an eval interface hierarchy. - pub fn interface_constant_names(&self, interface_name: &str) -> Vec { - let mut names = Vec::new(); - let mut seen = HashSet::new(); - self.collect_interface_constant_names(interface_name, &mut names, &mut seen); - names - } - - /// Collects eval interface constants without duplicating inherited names. - fn collect_interface_constant_names( - &self, - interface_name: &str, - names: &mut Vec, - seen: &mut HashSet, - ) { - let Some(interface) = self.interface(interface_name) else { - return; - }; - for parent in interface.parents() { - self.collect_interface_constant_names(parent, names, seen); - } - for constant in interface.constants() { - push_unique_constant_name(constant.name(), names, seen); - } - } - - /// Returns PHP case-insensitive direct method names declared by an eval trait. - pub fn trait_method_names(&self, trait_name: &str) -> Vec { - let Some(trait_decl) = self.trait_decl(trait_name) else { - return Vec::new(); - }; - let mut names = Vec::new(); - let mut seen = HashSet::new(); - for method in trait_decl.methods() { - push_unique_method_name(method.name(), &mut names, &mut seen); - } - names - } - - /// Returns PHP case-sensitive direct property names declared by an eval trait. - pub fn trait_property_names(&self, trait_name: &str) -> Vec { - let Some(trait_decl) = self.trait_decl(trait_name) else { - return Vec::new(); - }; - let mut names = Vec::new(); - let mut seen = HashSet::new(); - for property in trait_decl.properties() { - push_unique_property_name(property.name(), &mut names, &mut seen); - } - names - } - - /// Returns PHP case-sensitive direct constant names declared by an eval trait. - pub fn trait_constant_names(&self, trait_name: &str) -> Vec { - let Some(trait_decl) = self.trait_decl(trait_name) else { - return Vec::new(); - }; - let mut names = Vec::new(); - let mut seen = HashSet::new(); - for constant in trait_decl.constants() { - push_unique_constant_name(constant.name(), &mut names, &mut seen); - } - names - } - - /// Returns parent interface names for an eval-declared interface. - pub fn interface_parent_names(&self, interface_name: &str) -> Vec { - let mut parents = Vec::new(); - let mut seen = HashSet::new(); - self.collect_interface_parent_names(interface_name, &mut parents, &mut seen); - parents - } - - /// Collects eval-declared interface parents without following cycles. - fn collect_interface_parent_names( - &self, - interface_name: &str, - names: &mut Vec, - seen: &mut HashSet, - ) { - let Some(interface) = self.interface(interface_name) else { - return; - }; - for parent in interface.parents() { - push_unique_class_name(parent, names, seen); - self.collect_interface_parent_names(parent, names, seen); - } - } - - /// Returns direct and inherited method requirements for an eval interface. - pub fn interface_method_requirements(&self, interface_name: &str) -> Vec { - self.interface_method_requirements_with_owners(interface_name) - .into_iter() - .map(|(_, method)| method) - .collect() - } - - /// Returns direct and inherited method requirements with their declaring interface. - pub fn interface_method_requirements_with_owners( - &self, - interface_name: &str, - ) -> Vec<(String, EvalInterfaceMethod)> { - let mut methods = Vec::new(); - let mut seen_interfaces = HashSet::new(); - let mut seen_methods = HashSet::new(); - self.collect_interface_method_requirements( - interface_name, - &mut methods, - &mut seen_interfaces, - &mut seen_methods, - ); - methods - } - - /// Collects eval interface methods without duplicating inherited method names. - fn collect_interface_method_requirements( - &self, - interface_name: &str, - methods: &mut Vec<(String, EvalInterfaceMethod)>, - seen_interfaces: &mut HashSet, - seen_methods: &mut HashSet, - ) { - let key = normalize_class_name(interface_name); - if !seen_interfaces.insert(key) { - return; - } - let Some(interface) = self.interface(interface_name) else { - return; - }; - for parent in interface.parents() { - self.collect_interface_method_requirements( - parent, - methods, - seen_interfaces, - seen_methods, - ); - } - for method in interface.methods() { - let key = method.name().to_ascii_lowercase(); - if seen_methods.insert(key) { - methods.push((interface.name().to_string(), method.clone())); - } - } - } - - /// Returns direct and inherited property contracts for an eval interface. - pub fn interface_property_requirements( - &self, - interface_name: &str, - ) -> Vec { - self.interface_property_requirements_with_owners(interface_name) - .into_iter() - .map(|(_, property)| property) - .collect() - } - - /// Returns direct and inherited property contracts with their declaring interface. - pub fn interface_property_requirements_with_owners( - &self, - interface_name: &str, - ) -> Vec<(String, EvalInterfaceProperty)> { - let mut properties = Vec::new(); - let mut seen_interfaces = HashSet::new(); - self.collect_interface_property_requirements( - interface_name, - &mut properties, - &mut seen_interfaces, - ); - properties - } - - /// Collects eval interface property contracts, merging duplicate inherited names. - fn collect_interface_property_requirements( - &self, - interface_name: &str, - properties: &mut Vec<(String, EvalInterfaceProperty)>, - seen_interfaces: &mut HashSet, - ) { - let key = normalize_class_name(interface_name); - if !seen_interfaces.insert(key) { - return; - } - let Some(interface) = self.interface(interface_name) else { - return; - }; - for parent in interface.parents() { - self.collect_interface_property_requirements(parent, properties, seen_interfaces); - } - for property in interface.properties() { - if let Some((_, existing)) = properties - .iter_mut() - .find(|(_, existing)| existing.name() == property.name()) - { - *existing = existing.merged_with(property); - } else { - properties.push((interface.name().to_string(), property.clone())); - } - } - } - - /// Returns whether an eval-declared class satisfies one class/interface target. - pub fn class_is_a(&self, class_name: &str, target: &str, exclude_self: bool) -> bool { - let Some(class) = self.class(class_name) else { - return false; - }; - let target = normalize_class_name( - &self - .resolve_class_like_name(target) - .unwrap_or_else(|| target.trim_start_matches('\\').to_string()), - ); - if !exclude_self && normalize_class_name(class.name()) == target { - return true; - } - if target == normalize_class_name("Stringable") - && self.class_has_valid_tostring(class.name()) - { - return true; - } - self.class_parent_names(class.name()) - .iter() - .any(|parent| normalize_class_name(parent) == target) - || self - .class_interface_names(class.name()) - .iter() - .any(|interface| normalize_class_name(interface) == target) - } - - /// Returns whether one eval class exposes a PHP-compatible `__toString()` method. - fn class_has_valid_tostring(&self, class_name: &str) -> bool { - self.class_method(class_name, "__toString") - .is_some_and(|(_, method)| { - method.visibility() == EvalVisibility::Public - && !method.is_static() - && !method.is_abstract() - && method.params().is_empty() - }) - } - - /// Defines an eval dynamic constant value, failing if the name is invalid or already present. - pub fn define_constant(&mut self, name: &str, value: RuntimeCellHandle) -> bool { - let key = normalize_constant_name(name); - if key.is_empty() || self.constants.contains_key(&key) { - return false; - } - self.constants.insert(key, value); - true - } - - /// Returns true when this eval context has a dynamic constant with the requested name. - pub fn has_constant(&self, name: &str) -> bool { - self.constants.contains_key(&normalize_constant_name(name)) - } - - /// Returns an eval dynamic constant value by case-sensitive PHP constant name. - pub fn constant(&self, name: &str) -> Option { - self.constants.get(&normalize_constant_name(name)).copied() - } - - /// Defines a dynamic user function, failing if the name already exists. - pub fn define_function( - &mut self, - name: impl Into, - function: EvalFunction, - ) -> Result<(), EvalFunction> { - let name = name.into(); - if self.functions.contains_key(&name) || self.native_functions.contains_key(&name) { - return Err(function); - } - self.functions.insert(name, function); - Ok(()) - } - - /// Stores one eval closure instance under a context-local synthetic callable name. - pub fn define_closure(&mut self, closure: EvalClosure) -> String { - let name = format!("{{closure:eval:{}}}", self.next_closure_id); - self.next_closure_id += 1; - self.closures.insert(name.clone(), closure); - name - } - - /// Associates a PHP `Closure` object identity with an eval closure callable name. - pub fn register_closure_object(&mut self, identity: u64, closure_name: &str) { - self.register_closure_object_target( - identity, - EvalClosureObjectTarget::Named(closure_name.to_string()), - ); - } - - /// Associates a PHP `Closure` object identity with any eval callable target. - pub fn register_closure_object_target( - &mut self, - identity: u64, - target: EvalClosureObjectTarget, - ) { - self.closure_objects.insert(identity, target); - } - - /// Returns the callable target bound to a PHP `Closure` object. - pub fn closure_object_target(&self, identity: u64) -> Option<&EvalClosureObjectTarget> { - self.closure_objects.get(&identity) - } - - /// Returns the eval closure callable name bound to a literal PHP `Closure` object. - pub fn closure_object_name(&self, identity: u64) -> Option<&str> { - self.closure_objects - .get(&identity) - .and_then(|target| match target { - EvalClosureObjectTarget::Named(name) - | EvalClosureObjectTarget::BoundNamed { name, .. } => Some(name.as_str()), - _ => None, - }) - } - - /// Defines a generated native function callback, failing if the name already exists. - pub fn define_native_function( - &mut self, - name: impl Into, - function: NativeFunction, - ) -> Result<(), NativeFunction> { - let name = name.into(); - if self.functions.contains_key(&name) || self.native_functions.contains_key(&name) { - return Err(function); - } - self.native_functions.insert(name, function); - Ok(()) - } - - /// Returns a dynamic user function by its lowercase PHP function name. - pub fn function(&self, name: &str) -> Option<&EvalFunction> { - self.functions.get(name) - } - - /// Returns a dynamic eval closure by its synthetic callable name. - pub fn closure(&self, name: &str) -> Option<&EvalClosure> { - self.closures.get(name) - } - - /// Returns a native AOT function callback by its lowercase PHP function name. - pub fn native_function(&self, name: &str) -> Option { - self.native_functions.get(name).cloned() - } - - /// Records one parameter name for an already registered native AOT callback. - pub fn define_native_function_param( - &mut self, - function_name: &str, - index: usize, - param_name: impl Into, - ) -> bool { - self.native_functions - .get_mut(function_name) - .is_some_and(|function| function.set_param_name(index, param_name)) - } - - /// Records one parameter type for an already registered native AOT callback. - pub fn define_native_function_param_type( - &mut self, - function_name: &str, - index: usize, - param_type: EvalParameterType, - ) -> bool { - self.native_functions - .get_mut(function_name) - .is_some_and(|function| function.set_param_type(index, param_type)) - } - - /// Records one parameter default for an already registered native AOT callback. - pub fn define_native_function_param_default( - &mut self, - function_name: &str, - index: usize, - default: NativeCallableDefault, - ) -> bool { - self.native_functions - .get_mut(function_name) - .is_some_and(|function| function.set_param_default(index, default)) - } - - /// Records whether one native AOT callback parameter is by-reference. - pub fn define_native_function_param_by_ref( - &mut self, - function_name: &str, - index: usize, - by_ref: bool, - ) -> bool { - self.native_functions - .get_mut(function_name) - .is_some_and(|function| function.set_param_by_ref(index, by_ref)) - } - - /// Records which native AOT callback parameter is variadic. - pub fn define_native_function_variadic_param( - &mut self, - function_name: &str, - index: usize, - ) -> bool { - self.native_functions - .get_mut(function_name) - .is_some_and(|function| function.set_variadic_index(index)) - } - - /// Records one native AOT callback return type. - pub fn define_native_function_return_type( - &mut self, - function_name: &str, - return_type: EvalParameterType, - ) -> bool { - self.native_functions - .get_mut(function_name) - .is_some_and(|function| { - function.set_return_type(return_type); - true - }) - } - - /// Records whether eval may dispatch a native AOT callback through its bridge. - pub fn define_native_function_bridge_supported( - &mut self, - function_name: &str, - supported: bool, - ) -> bool { - self.native_functions - .get_mut(function_name) - .is_some_and(|function| { - function.set_bridge_supported(supported); - true - }) - } - - /// Defines native AOT instance-method signature metadata for eval named-argument binding. - pub fn define_native_method_signature( - &mut self, - class_name: &str, - method_name: &str, - signature: NativeCallableSignature, - ) -> bool { - self.native_methods - .insert(native_method_key(class_name, method_name), signature) - .is_none() - } - - /// Defines native AOT static-method signature metadata for eval named-argument binding. - pub fn define_native_static_method_signature( - &mut self, - class_name: &str, - method_name: &str, - signature: NativeCallableSignature, - ) -> bool { - self.native_static_methods - .insert(native_method_key(class_name, method_name), signature) - .is_none() - } - - /// Defines native AOT constructor signature metadata for eval named-argument binding. - pub fn define_native_constructor_signature( - &mut self, - class_name: &str, - signature: NativeCallableSignature, - ) -> bool { - self.native_constructors - .insert(normalize_class_name(class_name), signature) - .is_none() - } - - /// Records one parameter name for registered native AOT instance-method metadata. - pub fn define_native_method_param( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - param_name: impl Into, - ) -> bool { - self.native_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_param_name(index, param_name)) - } - - /// Records one parameter type for registered native AOT instance-method metadata. - pub fn define_native_method_param_type( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - param_type: EvalParameterType, - ) -> bool { - self.native_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_param_type(index, param_type)) - } - - /// Records one parameter default for registered native AOT instance-method metadata. - pub fn define_native_method_param_default( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - default: NativeCallableDefault, - ) -> bool { - self.native_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_param_default(index, default)) - } - - /// Records whether one native AOT instance-method parameter is by-reference. - pub fn define_native_method_param_by_ref( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - by_ref: bool, - ) -> bool { - self.native_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) - } - - /// Records which native AOT instance-method parameter is variadic. - pub fn define_native_method_variadic_param( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - ) -> bool { - self.native_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_variadic_index(index)) - } - - /// Records whether eval may dispatch one native AOT instance method. - pub fn define_native_method_bridge_supported( - &mut self, - class_name: &str, - method_name: &str, - supported: bool, - ) -> bool { - self.native_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| { - signature.set_bridge_supported(supported); - true - }) - } - - /// Records one return type for registered native AOT instance-method metadata. - pub fn define_native_method_return_type( - &mut self, - class_name: &str, - method_name: &str, - return_type: EvalParameterType, - ) -> bool { - self.native_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| { - signature.set_return_type(return_type); - true - }) - } - - /// Records one parameter name for registered native AOT static-method metadata. - pub fn define_native_static_method_param( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - param_name: impl Into, - ) -> bool { - self.native_static_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_param_name(index, param_name)) - } - - /// Records one parameter type for registered native AOT static-method metadata. - pub fn define_native_static_method_param_type( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - param_type: EvalParameterType, - ) -> bool { - self.native_static_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_param_type(index, param_type)) - } - - /// Records one parameter default for registered native AOT static-method metadata. - pub fn define_native_static_method_param_default( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - default: NativeCallableDefault, - ) -> bool { - self.native_static_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_param_default(index, default)) - } - - /// Records whether one native AOT static-method parameter is by-reference. - pub fn define_native_static_method_param_by_ref( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - by_ref: bool, - ) -> bool { - self.native_static_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) - } - - /// Records which native AOT static-method parameter is variadic. - pub fn define_native_static_method_variadic_param( - &mut self, - class_name: &str, - method_name: &str, - index: usize, - ) -> bool { - self.native_static_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| signature.set_variadic_index(index)) - } - - /// Records whether eval may dispatch one native AOT static method. - pub fn define_native_static_method_bridge_supported( - &mut self, - class_name: &str, - method_name: &str, - supported: bool, - ) -> bool { - self.native_static_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| { - signature.set_bridge_supported(supported); - true - }) - } - - /// Records one return type for registered native AOT static-method metadata. - pub fn define_native_static_method_return_type( - &mut self, - class_name: &str, - method_name: &str, - return_type: EvalParameterType, - ) -> bool { - self.native_static_methods - .get_mut(&native_method_key(class_name, method_name)) - .is_some_and(|signature| { - signature.set_return_type(return_type); - true - }) - } - - /// Records one parameter name for registered native AOT constructor metadata. - pub fn define_native_constructor_param( - &mut self, - class_name: &str, - index: usize, - param_name: impl Into, - ) -> bool { - self.native_constructors - .get_mut(&normalize_class_name(class_name)) - .is_some_and(|signature| signature.set_param_name(index, param_name)) - } - - /// Records one parameter type for registered native AOT constructor metadata. - pub fn define_native_constructor_param_type( - &mut self, - class_name: &str, - index: usize, - param_type: EvalParameterType, - ) -> bool { - self.native_constructors - .get_mut(&normalize_class_name(class_name)) - .is_some_and(|signature| signature.set_param_type(index, param_type)) - } - - /// Records one parameter default for registered native AOT constructor metadata. - pub fn define_native_constructor_param_default( - &mut self, - class_name: &str, - index: usize, - default: NativeCallableDefault, - ) -> bool { - self.native_constructors - .get_mut(&normalize_class_name(class_name)) - .is_some_and(|signature| signature.set_param_default(index, default)) - } - - /// Records whether one native AOT constructor parameter is by-reference. - pub fn define_native_constructor_param_by_ref( - &mut self, - class_name: &str, - index: usize, - by_ref: bool, - ) -> bool { - self.native_constructors - .get_mut(&normalize_class_name(class_name)) - .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) - } - - /// Records which native AOT constructor parameter is variadic. - pub fn define_native_constructor_variadic_param( - &mut self, - class_name: &str, - index: usize, - ) -> bool { - self.native_constructors - .get_mut(&normalize_class_name(class_name)) - .is_some_and(|signature| signature.set_variadic_index(index)) - } - - /// Records whether eval may dispatch one native AOT constructor. - pub fn define_native_constructor_bridge_supported( - &mut self, - class_name: &str, - supported: bool, - ) -> bool { - self.native_constructors - .get_mut(&normalize_class_name(class_name)) - .is_some_and(|signature| { - signature.set_bridge_supported(supported); - true - }) - } - - /// Returns native AOT instance-method signature metadata by PHP class and method name. - pub fn native_method_signature( - &self, - class_name: &str, - method_name: &str, - ) -> Option { - self.native_methods - .get(&native_method_key(class_name, method_name)) - .cloned() - } - - /// Returns native AOT static-method signature metadata by PHP class and method name. - pub fn native_static_method_signature( - &self, - class_name: &str, - method_name: &str, - ) -> Option { - self.native_static_methods - .get(&native_method_key(class_name, method_name)) - .cloned() - } - - /// Returns native AOT constructor signature metadata by PHP class name. - pub fn native_constructor_signature( - &self, - class_name: &str, - ) -> Option { - self.native_constructors - .get(&normalize_class_name(class_name)) - .cloned() - } - - /// Defines generated AOT parent metadata for eval `parent::` resolution. - pub fn define_native_class_parent(&mut self, class_name: &str, parent_name: &str) -> bool { - let class_key = normalize_class_name(class_name); - let parent_name = parent_name.trim_start_matches('\\'); - if class_key.is_empty() || parent_name.is_empty() { - return false; - } - self.native_class_parents - .insert(class_key, parent_name.to_string()) - .is_none() - } - - /// Returns generated AOT parent metadata by PHP class name. - pub fn native_class_parent(&self, class_name: &str) -> Option<&str> { - self.native_class_parents - .get(&normalize_class_name(class_name)) - .map(String::as_str) - } - - /// Appends generated AOT class attribute metadata for eval reflection. - pub fn define_native_class_attribute( - &mut self, - class_name: &str, - attribute: EvalAttribute, - ) -> bool { - let key = normalize_class_name(class_name); - if key.is_empty() { - return false; - } - self.native_class_attributes - .entry(key) - .or_default() - .push(attribute); - true - } - - /// Returns generated AOT class attribute metadata by PHP class name. - pub fn native_class_attributes(&self, class_name: &str) -> Vec { - self.native_class_attributes - .get(&normalize_class_name(class_name)) - .cloned() - .unwrap_or_default() - } - - /// Appends generated AOT method attribute metadata for eval reflection. - pub fn define_native_method_attribute( - &mut self, - class_name: &str, - method_name: &str, - attribute: EvalAttribute, - ) -> bool { - let key = native_method_key(class_name, method_name); - if key.0.is_empty() || key.1.is_empty() { - return false; - } - self.native_method_attributes - .entry(key) - .or_default() - .push(attribute); - true - } - - /// Returns generated AOT method attribute metadata by PHP class and method name. - pub fn native_method_attributes( - &self, - class_name: &str, - method_name: &str, - ) -> Vec { - self.native_method_attributes - .get(&native_method_key(class_name, method_name)) - .cloned() - .unwrap_or_default() - } - - /// Appends generated AOT class-constant attribute metadata for eval reflection. - pub fn define_native_constant_attribute( - &mut self, - class_name: &str, - constant_name: &str, - attribute: EvalAttribute, - ) -> bool { - let key = native_constant_key(class_name, constant_name); - if key.0.is_empty() || key.1.is_empty() { - return false; - } - self.native_constant_attributes - .entry(key) - .or_default() - .push(attribute); - true - } - - /// Returns generated AOT class-constant attribute metadata by PHP class and constant name. - pub fn native_constant_attributes( - &self, - class_name: &str, - constant_name: &str, - ) -> Vec { - self.native_constant_attributes - .get(&native_constant_key(class_name, constant_name)) - .cloned() - .unwrap_or_default() - } - - /// Defines generated AOT interface property-hook metadata for eval validation. - pub fn define_native_interface_property_requirement( - &mut self, - interface_name: &str, - declaring_interface_name: &str, - property: EvalInterfaceProperty, - ) -> bool { - let key = normalize_class_name(interface_name); - let owner = declaring_interface_name.trim_start_matches('\\').to_string(); - if key.is_empty() || owner.is_empty() || property.name().is_empty() { - return false; - } - let requirements = self.native_interface_properties.entry(key).or_default(); - if requirements.iter().any(|(_, existing)| existing.name() == property.name()) { - return false; - } - requirements.push((owner, property)); - true - } - - /// Returns generated AOT interface property-hook metadata by interface name. - pub fn native_interface_property_requirements( - &self, - interface_name: &str, - ) -> Vec<(String, EvalInterfaceProperty)> { - self.native_interface_properties - .get(&normalize_class_name(interface_name)) - .cloned() - .unwrap_or_default() - } - - /// Defines generated AOT abstract class property-hook metadata for eval validation. - pub fn define_native_abstract_property_requirement( - &mut self, - class_name: &str, - declaring_class_name: &str, - property: EvalInterfaceProperty, - ) -> bool { - let key = normalize_class_name(class_name); - let owner = declaring_class_name.trim_start_matches('\\').to_string(); - if key.is_empty() || owner.is_empty() || property.name().is_empty() { - return false; - } - let requirements = self.native_abstract_properties.entry(key).or_default(); - if requirements - .iter() - .any(|(_, existing)| existing.name() == property.name()) - { - return false; - } - requirements.push((owner, property)); - true - } - - /// Returns generated AOT abstract class property-hook metadata by class name. - pub fn native_abstract_property_requirements( - &self, - class_name: &str, - ) -> Vec<(String, EvalInterfaceProperty)> { - self.native_abstract_properties - .get(&normalize_class_name(class_name)) - .cloned() - .unwrap_or_default() - } - - /// Defines generated AOT property type metadata for eval reflection. - pub fn define_native_property_type( - &mut self, - class_name: &str, - property_name: &str, - property_type: EvalParameterType, - ) -> bool { - let key = native_property_key(class_name, property_name); - if key.0.is_empty() || key.1.is_empty() { - return false; - } - self.native_property_types - .insert(key, property_type) - .is_none() - } - - /// Returns generated AOT property type metadata by PHP class and property name. - pub fn native_property_type( - &self, - class_name: &str, - property_name: &str, - ) -> Option { - self.native_property_types - .get(&native_property_key(class_name, property_name)) - .cloned() - } - - /// Defines generated AOT property default metadata for eval reflection. - pub fn define_native_property_default( - &mut self, - class_name: &str, - property_name: &str, - default: NativeCallableDefault, - ) -> bool { - let key = native_property_key(class_name, property_name); - if key.0.is_empty() || key.1.is_empty() { - return false; - } - self.native_property_defaults.insert(key, default).is_none() - } - - /// Returns generated AOT property default metadata by PHP class and property name. - pub fn native_property_default( - &self, - class_name: &str, - property_name: &str, - ) -> Option { - self.native_property_defaults - .get(&native_property_key(class_name, property_name)) - .cloned() - } - - /// Appends generated AOT property attribute metadata for eval reflection. - pub fn define_native_property_attribute( - &mut self, - class_name: &str, - property_name: &str, - attribute: EvalAttribute, - ) -> bool { - let key = native_property_key(class_name, property_name); - if key.0.is_empty() || key.1.is_empty() { - return false; - } - self.native_property_attributes - .entry(key) - .or_default() - .push(attribute); - true - } - - /// Returns generated AOT property attribute metadata by PHP class and property name. - pub fn native_property_attributes( - &self, - class_name: &str, - property_name: &str, - ) -> Vec { - self.native_property_attributes - .get(&native_property_key(class_name, property_name)) - .cloned() - .unwrap_or_default() - } - - /// Returns true when the context has a dynamic or native function with this lowercase PHP name. - pub fn has_function(&self, name: &str) -> bool { - self.functions.contains_key(name) || self.native_functions.contains_key(name) - } - - /// Returns true when the context has a closure registered under this synthetic name. - pub fn has_closure(&self, name: &str) -> bool { - self.closures.contains_key(name) - } - - /// Returns a stored static local cell for an eval-declared function. - pub fn static_local(&self, function_name: &str, name: &str) -> Option { - self.static_locals - .get(&(function_name.to_string(), name.to_string())) - .copied() - } - - /// Stores one static local cell and returns any replaced distinct cell. - pub fn set_static_local( - &mut self, - function_name: impl Into, - name: impl Into, - cell: RuntimeCellHandle, - ) -> Option { - let previous = self - .static_locals - .insert((function_name.into(), name.into()), cell); - previous.filter(|previous| *previous != cell) - } - - /// Returns a stored static property cell for an eval-declared class. - pub fn static_property(&self, class_name: &str, name: &str) -> Option { - self.static_properties - .get(&(normalize_class_name(class_name), name.to_string())) - .copied() - } - - /// Stores one eval static property cell and returns any replaced distinct cell. - pub fn set_static_property( - &mut self, - class_name: &str, - name: impl Into, - cell: RuntimeCellHandle, - ) -> Option { - let previous = self - .static_properties - .insert((normalize_class_name(class_name), name.into()), cell); - previous.filter(|previous| *previous != cell) - } - - /// Binds one eval static property slot to a persistent PHP reference target. - pub fn bind_static_property_alias( - &mut self, - class_name: &str, - name: &str, - target: EvalReferenceTarget, - ) -> Option { - self.static_property_aliases - .insert((normalize_class_name(class_name), name.to_string()), target) - } - - /// Returns the persistent reference target bound to one eval static property slot. - pub fn static_property_alias( - &self, - class_name: &str, - name: &str, - ) -> Option<&EvalReferenceTarget> { - self.static_property_aliases - .get(&(normalize_class_name(class_name), name.to_string())) - } - - /// Returns a materialized eval class constant cell. - pub fn class_constant_cell(&self, class_name: &str, name: &str) -> Option { - self.class_constants - .get(&(normalize_class_name(class_name), name.to_string())) - .copied() - } - - /// Stores one eval class constant cell and returns any replaced distinct cell. - pub fn set_class_constant_cell( - &mut self, - class_name: &str, - name: impl Into, - cell: RuntimeCellHandle, - ) -> Option { - let previous = self - .class_constants - .insert((normalize_class_name(class_name), name.into()), cell); - previous.filter(|previous| *previous != cell) - } - - /// Returns true when an eval include key was already loaded by this context. - pub fn has_included_file(&self, path: &str) -> bool { - self.included_files.contains(path) - } - - /// Records one successfully loaded eval include key for include_once/require_once. - pub fn mark_included_file(&mut self, path: impl Into) { - self.included_files.insert(path.into()); - } - - /// Stores the non-owned global scope handle used by eval `global` aliases. - pub fn set_global_scope(&mut self, scope: *mut ElephcEvalScope) -> bool { - if scope.is_null() { - self.global_scope = None; - false - } else { - self.global_scope = Some(scope); - true - } - } - - /// Returns the non-owned global scope handle for eval `global` aliases. - pub fn global_scope_ptr(&self) -> Option<*mut ElephcEvalScope> { - self.global_scope - } - - /// Pushes an eval-executed function name for magic-constant resolution. - pub fn push_function(&mut self, name: impl Into) { - self.function_stack.push(name.into()); - } - - /// Pops the current eval-executed function name after its body completes. - pub fn pop_function(&mut self) { - self.function_stack.pop(); - } - - /// Returns the current eval-executed function name, if execution is inside one. - pub fn current_function(&self) -> Option<&str> { - self.function_stack.last().map(String::as_str) - } - - /// Pushes the eval class whose method is currently executing. - pub fn push_class_scope(&mut self, name: impl Into) { - self.class_stack.push(name.into()); - } - - /// Pops the current eval class method scope. - pub fn pop_class_scope(&mut self) { - self.class_stack.pop(); - } - - /// Returns the current eval class scope, if execution is inside a method. - pub fn current_class_scope(&self) -> Option<&str> { - self.class_stack.last().map(String::as_str) - } - - /// Pushes the class name used to dispatch the current eval method call. - pub fn push_called_class_scope(&mut self, name: impl Into) { - self.called_class_stack.push(name.into()); - } - - /// Pops the current late-static-bound eval class scope. - pub fn pop_called_class_scope(&mut self) { - self.called_class_stack.pop(); - } - - /// Returns the current late-static-bound eval class scope, if execution is inside a method. - pub fn current_called_class_scope(&self) -> Option<&str> { - self.called_class_stack.last().map(String::as_str) - } - - /// Returns a dynamic called-class override for a generated/AOT frame entering eval. - pub fn native_frame_called_class_override( - &self, - class_name: &str, - called_class_name: &str, - ) -> Option { - let class_name = class_name.trim_start_matches('\\'); - let called_class_name = called_class_name.trim_start_matches('\\'); - if class_name.is_empty() || !called_class_name.eq_ignore_ascii_case(class_name) { - return None; - } - if let Some(called_class) = - native_frame_called_class_override(class_name, called_class_name) - { - return Some(called_class); - } - let active = self.current_called_class_scope()?.trim_start_matches('\\'); - if active.is_empty() || active.eq_ignore_ascii_case(class_name) { - return None; - } - let active = self - .resolve_class_name(active) - .unwrap_or_else(|| active.to_string()); - self.class_parent_names(&active) - .iter() - .any(|parent| parent.eq_ignore_ascii_case(class_name)) - .then_some(active) - } - - /// Pushes PHP-visible method magic constants for the current eval method frame. - pub fn push_method_magic_scope(&mut self, class_name: &str, method: &EvalClassMethod) { - self.magic_stack.push(EvalMagicScope { - function_name: method.magic_function_name().to_string(), - method_name: method.magic_method_name(class_name), - class_name: class_name.trim_start_matches('\\').to_string(), - trait_name: method - .trait_origin() - .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) - .unwrap_or_default(), - }); - } - - /// Pushes PHP-visible class-like member magic constants for default expressions. - pub fn push_class_like_member_magic_scope( - &mut self, - class_name: &str, - trait_name: Option<&str>, - ) { - self.magic_stack.push(EvalMagicScope { - function_name: String::new(), - method_name: String::new(), - class_name: class_name.trim_start_matches('\\').to_string(), - trait_name: trait_name - .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) - .unwrap_or_default(), - }); - } - - /// Pushes PHP-visible callable magic constants for reflected parameter defaults. - pub fn push_callable_magic_scope( - &mut self, - function_name: &str, - method_name: &str, - class_name: Option<&str>, - trait_name: Option<&str>, - ) { - self.magic_stack.push(EvalMagicScope { - function_name: function_name.to_string(), - method_name: method_name.to_string(), - class_name: class_name - .map(|class_name| class_name.trim_start_matches('\\').to_string()) - .unwrap_or_default(), - trait_name: trait_name - .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) - .unwrap_or_default(), - }); - } - - /// Pops the current PHP-visible eval magic-constant scope. - pub fn pop_magic_scope(&mut self) { - self.magic_stack.pop(); - } - - /// Returns the PHP `__FUNCTION__` value for the current eval frame. - pub fn current_magic_function(&self) -> Option<&str> { - self.magic_stack - .last() - .map(|scope| scope.function_name.as_str()) - } - - /// Returns the PHP `__METHOD__` value for the current eval method frame. - pub fn current_magic_method(&self) -> Option<&str> { - self.magic_stack - .last() - .map(|scope| scope.method_name.as_str()) - } - - /// Returns the PHP `__CLASS__` value for the current eval method frame. - pub fn current_magic_class(&self) -> Option<&str> { - self.magic_stack - .last() - .map(|scope| scope.class_name.as_str()) - } - - /// Returns the PHP `__TRAIT__` value for the current eval method frame. - pub fn current_magic_trait(&self) -> Option<&str> { - self.magic_stack - .last() - .map(|scope| scope.trait_name.as_str()) - } - - /// Captures the current eval execution stacks for later caller-context-sensitive work. - pub fn execution_scope(&self) -> ElephcEvalExecutionScope { - ElephcEvalExecutionScope { - function_stack: self.function_stack.clone(), - class_stack: self.class_stack.clone(), - called_class_stack: self.called_class_stack.clone(), - } - } - - /// Replaces eval execution stacks and returns the previous stacks for restoration. - pub fn replace_execution_scope( - &mut self, - scope: ElephcEvalExecutionScope, - ) -> ElephcEvalExecutionScope { - ElephcEvalExecutionScope { - function_stack: std::mem::replace(&mut self.function_stack, scope.function_stack), - class_stack: std::mem::replace(&mut self.class_stack, scope.class_stack), - called_class_stack: std::mem::replace( - &mut self.called_class_stack, - scope.called_class_stack, - ), - } - } - - /// Records a Throwable cell that escaped from an eval-executed function call. - pub fn set_pending_throw(&mut self, value: RuntimeCellHandle) { - self.pending_throw = Some(value); - } - - /// Returns and clears the Throwable cell currently escaping through eval. - pub fn take_pending_throw(&mut self) -> Option { - self.pending_throw.take() - } - - /// Returns the eval-local SPL autoload extension list. - pub fn spl_autoload_extensions(&self) -> &str { - &self.spl_autoload_extensions - } - - /// Replaces the eval-local SPL autoload extension list. - pub fn set_spl_autoload_extensions(&mut self, extensions: impl Into) { - self.spl_autoload_extensions = extensions.into(); - } - - /// Returns the eval-local stream resource table. - pub(crate) fn stream_resources(&self) -> &EvalStreamResources { - &self.streams - } - - /// Returns mutable access to the eval-local stream resource table. - pub(crate) fn stream_resources_mut(&mut self) -> &mut EvalStreamResources { - &mut self.streams - } - - /// Clears the eval-local JSON error state after a successful JSON operation. - pub fn clear_json_error(&mut self) { - self.json_last_error = 0; - self.json_last_error_msg.clear(); - self.json_last_error_msg.push_str("No error"); - } - - /// Records the eval-local JSON error state for `json_last_error*()` calls. - pub fn set_json_error(&mut self, code: i64, message: impl Into) { - self.json_last_error = code; - self.json_last_error_msg = message.into(); - } - - /// Returns the PHP `JSON_ERROR_*` code for the last eval JSON operation. - pub const fn json_last_error(&self) -> i64 { - self.json_last_error - } - - /// Returns the PHP message for the last eval JSON operation. - pub fn json_last_error_msg(&self) -> &str { - &self.json_last_error_msg - } - - /// Returns the eval-local PHP default timezone identifier. - pub fn default_timezone(&self) -> &str { - &self.default_timezone - } - - /// Replaces the eval-local PHP default timezone identifier. - pub fn set_default_timezone(&mut self, timezone: impl Into) { - self.default_timezone = timezone.into(); - } - - /// Returns the eval-local HTTP response code used by web-facing builtins. - pub const fn http_response_code(&self) -> i64 { - self.http_response_code - } - - /// Applies a new eval-local HTTP response code and returns the previous one. - pub fn replace_http_response_code(&mut self, response_code: i64) -> i64 { - let previous = self.http_response_code; - if response_code > 0 { - self.http_response_code = response_code; - } - previous - } - - /// Updates the source file, directory, and line for the current eval call site. - pub fn set_call_site(&mut self, file: impl Into, dir: impl Into, line: i64) { - self.call_file = file.into(); - self.call_dir = dir.into(); - self.call_line = line; - self.file_magic_override = None; - } - - /// Returns a copy of the current call-site metadata for temporary overrides. - pub fn call_site(&self) -> (String, String, i64, Option) { - ( - self.call_file.clone(), - self.call_dir.clone(), - self.call_line, - self.file_magic_override.clone(), - ) - } - - /// Overrides `__FILE__` while executing an actual file through eval include. - pub fn set_file_magic_override(&mut self, file: Option) { - self.file_magic_override = file; - } - - /// Returns the source directory associated with the current eval call site. - pub fn call_dir(&self) -> &str { - &self.call_dir - } - - /// Returns PHP's `__FILE__` string for code currently running inside eval. - pub fn eval_file_magic(&self) -> String { - if let Some(file) = &self.file_magic_override { - return file.clone(); - } - if self.call_file.is_empty() { - return String::new(); - } - format!("{}({}) : eval()'d code", self.call_file, self.call_line) - } -} - -impl Default for ElephcEvalContext { - /// Creates the default process-level eval context. - fn default() -> Self { - Self::new() - } -} - -/// Normalizes PHP class names for the eval dynamic class registry. -fn normalize_class_name(name: &str) -> String { - name.trim_start_matches('\\').to_ascii_lowercase() -} - -/// Adds an external declaration name once while preserving PHP-visible spelling. -fn push_external_declared_name(names: &mut Vec, name: &str) -> bool { - let visible_name = name.trim_start_matches('\\'); - let key = normalize_class_name(visible_name); - if key.is_empty() { - return false; - } - if !names - .iter() - .any(|existing| normalize_class_name(existing) == key) - { - names.push(visible_name.to_string()); - } - true -} - -/// Normalizes PHP enum case names for case-sensitive eval enum lookup. -fn normalize_enum_case_name(name: &str) -> String { - name.to_string() -} - -/// Normalizes PHP method names for case-insensitive native metadata lookup. -fn normalize_method_name(name: &str) -> String { - name.to_ascii_lowercase() -} - -/// Builds the folded native method metadata key used for eval argument binding. -fn native_method_key(class_name: &str, method_name: &str) -> (String, String) { - ( - normalize_class_name(class_name), - normalize_method_name(method_name), - ) -} - -/// Builds the folded native property metadata key used for eval reflection. -fn native_property_key(class_name: &str, property_name: &str) -> (String, String) { - ( - normalize_class_name(class_name), - property_name.trim_start_matches('$').to_string(), - ) -} - -/// Builds the case-sensitive native class-constant metadata key used for eval reflection. -fn native_constant_key(class_name: &str, constant_name: &str) -> (String, String) { - ( - normalize_class_name(class_name), - constant_name.to_string(), - ) -} - -/// Pushes a PHP class-like name once, preserving the first visible spelling. -fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSet) { - let key = normalize_class_name(name); - if seen.insert(key) { - names.push(name.trim_start_matches('\\').to_string()); - } -} - -/// Returns whether two PHP class-like names resolve to the same normalized spelling. -fn same_class_name(left: &str, right: &str) -> bool { - normalize_class_name(left) == normalize_class_name(right) -} - -/// Returns whether a class-like name is one of PHP's native enum marker interfaces. -fn is_php_enum_marker_interface(name: &str) -> bool { - let name = name.trim_start_matches('\\'); - name.eq_ignore_ascii_case("UnitEnum") || name.eq_ignore_ascii_case("BackedEnum") -} - -/// Pushes a case-insensitive PHP method name once for ReflectionClass metadata. -fn push_unique_method_name(name: &str, names: &mut Vec, seen: &mut HashSet) { - let key = normalize_method_name(name); - if seen.insert(key) { - names.push(name.trim_start_matches('\\').to_string()); - } -} - -/// Pushes a case-sensitive PHP property name once for ReflectionClass metadata. -fn push_unique_property_name(name: &str, names: &mut Vec, seen: &mut HashSet) { - if seen.insert(name.to_string()) { - names.push(name.to_string()); - } -} - -/// Pushes a case-sensitive PHP class constant name once for ReflectionClass metadata. -fn push_unique_constant_name(name: &str, names: &mut Vec, seen: &mut HashSet) { - if seen.insert(name.to_string()) { - names.push(name.to_string()); - } -} - -/// Normalizes PHP constant names for case-sensitive eval dynamic probes. -fn normalize_constant_name(name: &str) -> String { - name.trim_start_matches('\\').to_string() -} diff --git a/crates/elephc-magician/src/context/alias_metadata.rs b/crates/elephc-magician/src/context/alias_metadata.rs new file mode 100644 index 0000000000..889c796e84 --- /dev/null +++ b/crates/elephc-magician/src/context/alias_metadata.rs @@ -0,0 +1,60 @@ +//! Purpose: +//! Defines class-alias kinds and synthetic ReflectionAttribute metadata. +//! +//! Called from: +//! - Class alias registration and ReflectionAttribute construction. +//! +//! Key details: +//! - Alias kind prevents cross-kind lookup while attribute target/repetition stays attached to identity. + +use super::*; + +/// PHP class-like declaration kind targeted by a dynamic `class_alias()`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum EvalClassAliasKind { + Class, + Interface, + Trait, + Enum, +} + +/// Dynamic alias target and kind recorded for eval-visible class-like symbols. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct EvalClassAlias { + pub(super) target: String, + pub(super) kind: EvalClassAliasKind, +} + +/// Metadata attached to one synthetic eval `ReflectionAttribute` object. +#[derive(Clone)] +pub struct EvalReflectionAttributeMetadata { + pub(super) attribute: EvalAttribute, + pub(super) target: u64, + pub(super) repeated: bool, +} + +impl EvalReflectionAttributeMetadata { + /// Creates metadata for a materialized `ReflectionAttribute` object. + pub fn new(attribute: EvalAttribute, target: u64, repeated: bool) -> Self { + Self { + attribute, + target, + repeated, + } + } + + /// Returns the underlying eval-retained attribute metadata. + pub const fn attribute(&self) -> &EvalAttribute { + &self.attribute + } + + /// Returns the PHP `Attribute::TARGET_*` bitmask for this reflected owner. + pub const fn target(&self) -> u64 { + self.target + } + + /// Returns whether this owner has multiple attributes with the same name. + pub const fn is_repeated(&self) -> bool { + self.repeated + } +} diff --git a/crates/elephc-magician/src/context/class_metadata.rs b/crates/elephc-magician/src/context/class_metadata.rs new file mode 100644 index 0000000000..070feadbd5 --- /dev/null +++ b/crates/elephc-magician/src/context/class_metadata.rs @@ -0,0 +1,750 @@ +//! Purpose: +//! Resolves class chains, inherited members, traits, interfaces, and compatibility requirements. +//! +//! Called from: +//! - Declaration validation, Reflection metadata, and runtime class checks. +//! +//! Key details: +//! - Traversals are cycle-safe and preserve PHP case-insensitive class-like semantics. + +use super::*; + +impl ElephcEvalContext { + /// Returns eval-declared class metadata from parent to child for construction. + pub fn class_chain(&self, name: &str) -> Vec { + let mut chain = Vec::new(); + let mut seen = HashSet::new(); + self.collect_class_chain(name, &mut chain, &mut seen); + chain + } + + /// Collects one eval-declared class ancestry chain without following cycles. + pub(super) fn collect_class_chain( + &self, + name: &str, + chain: &mut Vec, + seen: &mut HashSet, + ) { + let key = normalize_class_name(name); + if !seen.insert(key.clone()) { + return; + } + let Some(class) = self.classes.get(&key) else { + return; + }; + if let Some(parent) = class.parent() { + self.collect_class_chain(parent, chain, seen); + } + chain.push(class.clone()); + } + + /// Finds a method in an eval-declared class or its eval-declared parents. + pub fn class_method( + &self, + class_name: &str, + method_name: &str, + ) -> Option<(String, EvalClassMethod)> { + let mut current_name = self.resolve_class_name(class_name)?; + let mut seen = HashSet::new(); + loop { + let key = normalize_class_name(¤t_name); + if !seen.insert(key.clone()) { + return None; + } + let class = self.classes.get(&key)?; + if let Some(method) = class.method(method_name) { + return Some((class.name().to_string(), method.clone())); + } + current_name = class.parent()?.to_string(); + } + } + + /// Finds a method declared directly by one eval-declared class. + pub fn class_own_method( + &self, + class_name: &str, + method_name: &str, + ) -> Option<(String, EvalClassMethod)> { + let class = self.class(class_name)?; + class + .method(method_name) + .map(|method| (class.name().to_string(), method.clone())) + } + + /// Finds a class-like constant on an eval class, interface, trait, or inherited relation. + pub fn class_constant( + &self, + class_name: &str, + constant_name: &str, + ) -> Option<(String, EvalClassConstant)> { + if self.has_class(class_name) { + return self.class_or_interface_constant(class_name, constant_name); + } + if self.has_interface(class_name) { + return self.interface_constant(class_name, constant_name); + } + if let Some(trait_decl) = self.trait_decl(class_name) { + if let Some(constant) = trait_decl.constant(constant_name) { + return Some((trait_decl.name().to_string(), constant.clone())); + } + } + None + } + + /// Finds a class constant in an eval-declared class, parents, or implemented interfaces. + pub(super) fn class_or_interface_constant( + &self, + class_name: &str, + constant_name: &str, + ) -> Option<(String, EvalClassConstant)> { + let mut current_name = self.resolve_class_name(class_name)?; + let mut seen = HashSet::new(); + loop { + let key = normalize_class_name(¤t_name); + if !seen.insert(key.clone()) { + return None; + } + let class = self.classes.get(&key)?; + if let Some(constant) = class.constant(constant_name) { + return Some((class.name().to_string(), constant.clone())); + } + if let Some(parent) = class.parent() { + current_name = parent.to_string(); + } else { + break; + } + } + for interface_name in self.class_interface_names(class_name) { + if let Some(found) = self.interface_constant(&interface_name, constant_name) { + return Some(found); + } + } + None + } + + /// Finds a constant declared on an eval interface or inherited parent interface. + pub fn interface_constant( + &self, + interface_name: &str, + constant_name: &str, + ) -> Option<(String, EvalClassConstant)> { + let interface = self.interface(interface_name)?; + if let Some(constant) = interface.constant(constant_name) { + return Some((interface.name().to_string(), constant.clone())); + } + for parent in interface.parents() { + if let Some(found) = self.interface_constant(parent, constant_name) { + return Some(found); + } + } + None + } + + /// Finds a class constant declared directly by one eval-declared class. + pub fn class_own_constant( + &self, + class_name: &str, + constant_name: &str, + ) -> Option<(String, EvalClassConstant)> { + let class = self.class(class_name)?; + class + .constant(constant_name) + .map(|constant| (class.name().to_string(), constant.clone())) + } + + /// Finds a property in an eval-declared class or its eval-declared parents. + pub fn class_property( + &self, + class_name: &str, + property_name: &str, + ) -> Option<(String, EvalClassProperty)> { + let mut current_name = self.resolve_class_name(class_name)?; + let mut seen = HashSet::new(); + loop { + let key = normalize_class_name(¤t_name); + if !seen.insert(key.clone()) { + return None; + } + let class = self.classes.get(&key)?; + if let Some(property) = class + .properties() + .iter() + .find(|property| property.name() == property_name) + { + return Some((class.name().to_string(), property.clone())); + } + current_name = class.parent()?.to_string(); + } + } + + /// Finds a property declared directly by one eval-declared class. + pub fn class_own_property( + &self, + class_name: &str, + property_name: &str, + ) -> Option<(String, EvalClassProperty)> { + let class = self.class(class_name)?; + class + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| (class.name().to_string(), property.clone())) + } + + /// Returns direct and inherited parent class names for an eval-declared class. + pub fn class_parent_names(&self, class_name: &str) -> Vec { + let mut parents = Vec::new(); + let mut current = self + .class(class_name) + .and_then(EvalClass::parent) + .map(str::to_string) + .or_else(|| self.native_class_parent(class_name).map(str::to_string)); + let mut seen = HashSet::new(); + while let Some(parent) = current { + let parent = self + .resolve_class_name(&parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); + let key = normalize_class_name(&parent); + if !seen.insert(key) { + break; + } + if let Some(parent_class) = self.class(&parent) { + parents.push(parent_class.name().trim_start_matches('\\').to_string()); + current = parent_class + .parent() + .map(str::to_string) + .or_else(|| self.native_class_parent(parent_class.name()).map(str::to_string)); + } else { + parents.push(parent); + current = parents + .last() + .and_then(|parent| self.native_class_parent(parent)) + .map(str::to_string); + } + } + parents + } + + /// Returns the nearest runtime/AOT parent backing an eval class hierarchy. + pub fn class_native_parent_name(&self, class_name: &str) -> Option { + let mut current = self + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let mut seen = HashSet::new(); + loop { + if !seen.insert(normalize_class_name(¤t)) { + return None; + } + let parent = self + .class(¤t) + .and_then(EvalClass::parent) + .map(str::to_string) + .or_else(|| self.native_class_parent(¤t).map(str::to_string))?; + let parent = self + .resolve_class_name(&parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); + if self.class(&parent).is_none() { + return Some(parent); + } + current = parent; + } + } + + /// Returns direct and inherited interface names for an eval-declared class. + pub fn class_interface_names(&self, class_name: &str) -> Vec { + let mut interfaces = Vec::new(); + let mut seen = HashSet::new(); + let is_enum = self.enum_decl(class_name).is_some(); + for class in self.class_chain(class_name) { + for interface in class.interfaces() { + push_unique_class_name(interface, &mut interfaces, &mut seen); + self.collect_class_interface_parent_names( + interface, + is_enum, + &mut interfaces, + &mut seen, + ); + } + } + if let Some(enum_decl) = self.enum_decl(class_name) { + push_unique_class_name("UnitEnum", &mut interfaces, &mut seen); + if enum_decl.backing_type().is_some() { + push_unique_class_name("BackedEnum", &mut interfaces, &mut seen); + } + } + interfaces + } + + /// Collects interface parents while preserving PHP enum marker interface ordering. + pub(super) fn collect_class_interface_parent_names( + &self, + interface_name: &str, + skip_enum_markers: bool, + names: &mut Vec, + seen: &mut HashSet, + ) { + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + if skip_enum_markers && is_php_enum_marker_interface(parent) { + continue; + } + push_unique_class_name(parent, names, seen); + self.collect_class_interface_parent_names(parent, skip_enum_markers, names, seen); + } + } + + /// Returns trait names used directly by an eval-declared class. + pub fn class_trait_names(&self, class_name: &str) -> Vec { + self.class(class_name).map_or_else(Vec::new, |class| { + let mut traits = Vec::new(); + let mut seen = HashSet::new(); + for trait_name in class.traits() { + push_unique_class_name(trait_name, &mut traits, &mut seen); + } + traits + }) + } + + /// Returns trait method aliases declared directly by an eval-declared class. + pub fn class_trait_aliases(&self, class_name: &str) -> Vec<(String, String)> { + let Some(class) = self.class(class_name) else { + return Vec::new(); + }; + let mut aliases = Vec::new(); + for adaptation in class.trait_adaptations() { + let EvalTraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + .. + } = adaptation + else { + continue; + }; + let Some(source_trait) = + self.class_trait_alias_source(class, trait_name.as_deref(), method) + else { + continue; + }; + aliases.push((alias.clone(), format!("{source_trait}::{method}"))); + } + aliases + } + + /// Returns trait names used directly by an eval-declared trait. + pub fn trait_trait_names(&self, trait_name: &str) -> Vec { + self.trait_decl(trait_name).map_or_else(Vec::new, |trait_decl| { + let mut traits = Vec::new(); + let mut seen = HashSet::new(); + for used_trait in trait_decl.traits() { + push_unique_class_name(used_trait, &mut traits, &mut seen); + } + traits + }) + } + + /// Returns trait method aliases declared directly by an eval-declared trait. + pub fn trait_trait_aliases(&self, trait_name: &str) -> Vec<(String, String)> { + let Some(trait_decl) = self.trait_decl(trait_name) else { + return Vec::new(); + }; + let mut aliases = Vec::new(); + for adaptation in trait_decl.trait_adaptations() { + let EvalTraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + .. + } = adaptation + else { + continue; + }; + let Some(source_trait) = + self.trait_trait_alias_source(trait_decl, trait_name.as_deref(), method) + else { + continue; + }; + aliases.push((alias.clone(), format!("{source_trait}::{method}"))); + } + aliases + } + + /// Resolves the trait name shown in `ReflectionClass::getTraitAliases()`. + pub(super) fn class_trait_alias_source( + &self, + class: &EvalClass, + explicit_trait: Option<&str>, + method: &str, + ) -> Option { + if let Some(trait_name) = explicit_trait { + return Some( + self.trait_decl(trait_name) + .map_or(trait_name, EvalTrait::name) + .trim_start_matches('\\') + .to_string(), + ); + } + class.traits().iter().find_map(|trait_name| { + let trait_decl = self.trait_decl(trait_name)?; + trait_decl + .methods() + .iter() + .any(|candidate| candidate.name().eq_ignore_ascii_case(method)) + .then(|| trait_decl.name().trim_start_matches('\\').to_string()) + }) + } + + /// Resolves the trait name shown for a trait's internal `getTraitAliases()`. + pub(super) fn trait_trait_alias_source( + &self, + trait_decl: &EvalTrait, + explicit_trait: Option<&str>, + method: &str, + ) -> Option { + if let Some(trait_name) = explicit_trait { + return Some( + self.trait_decl(trait_name) + .map_or(trait_name, EvalTrait::name) + .trim_start_matches('\\') + .to_string(), + ); + } + trait_decl.traits().iter().find_map(|trait_name| { + let used_trait_decl = self.trait_decl(trait_name)?; + used_trait_decl + .methods() + .iter() + .any(|candidate| candidate.name().eq_ignore_ascii_case(method)) + .then(|| used_trait_decl.name().trim_start_matches('\\').to_string()) + }) + } + + /// Returns PHP case-insensitive method names visible to `ReflectionClass::hasMethod()`. + pub fn class_method_names(&self, class_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for class in self.class_chain(class_name).into_iter().rev() { + for method in class.methods() { + push_unique_method_name(method.name(), &mut names, &mut seen); + } + if let Some(enum_decl) = self.enum_decl(class.name()) { + push_unique_method_name("cases", &mut names, &mut seen); + if enum_decl.backing_type().is_some() { + push_unique_method_name("from", &mut names, &mut seen); + push_unique_method_name("tryFrom", &mut names, &mut seen); + } + } + } + names + } + + /// Returns PHP case-sensitive property names visible to `ReflectionClass::hasProperty()`. + pub fn class_property_names(&self, class_name: &str) -> Vec { + let reflected_name = self + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let mut names = Vec::new(); + let mut seen = HashSet::new(); + if let Some(enum_decl) = self.enum_decl(&reflected_name) { + push_unique_property_name("name", &mut names, &mut seen); + if enum_decl.backing_type().is_some() { + push_unique_property_name("value", &mut names, &mut seen); + } + } + for class in self.class_chain(&reflected_name) { + let declaring_is_reflected = same_class_name(class.name(), &reflected_name); + for property in class.properties() { + if property.visibility() == EvalVisibility::Private && !declaring_is_reflected { + continue; + } + push_unique_property_name(property.name(), &mut names, &mut seen); + } + } + names + } + + /// Returns PHP case-sensitive constant names visible to `ReflectionClass::hasConstant()`. + pub fn class_constant_names(&self, class_name: &str) -> Vec { + let reflected_name = self + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let mut names = Vec::new(); + let mut seen = HashSet::new(); + if let Some(enum_decl) = self.enum_decl(&reflected_name) { + for case in enum_decl.cases() { + push_unique_constant_name(case.name(), &mut names, &mut seen); + } + } + for class in self.class_chain(&reflected_name).into_iter().rev() { + for constant in class.constants() { + push_unique_constant_name(constant.name(), &mut names, &mut seen); + } + for interface_name in class.interfaces() { + for constant in self.interface_constant_names(interface_name) { + push_unique_constant_name(&constant, &mut names, &mut seen); + } + } + } + names + } + + /// Returns PHP case-insensitive method names declared by an eval interface hierarchy. + pub fn interface_method_names(&self, interface_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for method in self.interface_method_requirements(interface_name) { + push_unique_method_name(method.name(), &mut names, &mut seen); + } + names + } + + /// Returns PHP case-sensitive property names declared by an eval interface hierarchy. + pub fn interface_property_names(&self, interface_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for property in self.interface_property_requirements(interface_name) { + push_unique_property_name(property.name(), &mut names, &mut seen); + } + names + } + + /// Returns PHP case-sensitive constant names declared by an eval interface hierarchy. + pub fn interface_constant_names(&self, interface_name: &str) -> Vec { + let mut names = Vec::new(); + let mut seen = HashSet::new(); + self.collect_interface_constant_names(interface_name, &mut names, &mut seen); + names + } + + /// Collects eval interface constants without duplicating inherited names. + pub(super) fn collect_interface_constant_names( + &self, + interface_name: &str, + names: &mut Vec, + seen: &mut HashSet, + ) { + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + self.collect_interface_constant_names(parent, names, seen); + } + for constant in interface.constants() { + push_unique_constant_name(constant.name(), names, seen); + } + } + + /// Returns PHP case-insensitive direct method names declared by an eval trait. + pub fn trait_method_names(&self, trait_name: &str) -> Vec { + let Some(trait_decl) = self.trait_decl(trait_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for method in trait_decl.methods() { + push_unique_method_name(method.name(), &mut names, &mut seen); + } + names + } + + /// Returns PHP case-sensitive direct property names declared by an eval trait. + pub fn trait_property_names(&self, trait_name: &str) -> Vec { + let Some(trait_decl) = self.trait_decl(trait_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for property in trait_decl.properties() { + push_unique_property_name(property.name(), &mut names, &mut seen); + } + names + } + + /// Returns PHP case-sensitive direct constant names declared by an eval trait. + pub fn trait_constant_names(&self, trait_name: &str) -> Vec { + let Some(trait_decl) = self.trait_decl(trait_name) else { + return Vec::new(); + }; + let mut names = Vec::new(); + let mut seen = HashSet::new(); + for constant in trait_decl.constants() { + push_unique_constant_name(constant.name(), &mut names, &mut seen); + } + names + } + + /// Returns parent interface names for an eval-declared interface. + pub fn interface_parent_names(&self, interface_name: &str) -> Vec { + let mut parents = Vec::new(); + let mut seen = HashSet::new(); + self.collect_interface_parent_names(interface_name, &mut parents, &mut seen); + parents + } + + /// Collects eval-declared interface parents without following cycles. + pub(super) fn collect_interface_parent_names( + &self, + interface_name: &str, + names: &mut Vec, + seen: &mut HashSet, + ) { + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + push_unique_class_name(parent, names, seen); + self.collect_interface_parent_names(parent, names, seen); + } + } + + /// Returns direct and inherited method requirements for an eval interface. + pub fn interface_method_requirements(&self, interface_name: &str) -> Vec { + self.interface_method_requirements_with_owners(interface_name) + .into_iter() + .map(|(_, method)| method) + .collect() + } + + /// Returns direct and inherited method requirements with their declaring interface. + pub fn interface_method_requirements_with_owners( + &self, + interface_name: &str, + ) -> Vec<(String, EvalInterfaceMethod)> { + let mut methods = Vec::new(); + let mut seen_interfaces = HashSet::new(); + let mut seen_methods = HashSet::new(); + self.collect_interface_method_requirements( + interface_name, + &mut methods, + &mut seen_interfaces, + &mut seen_methods, + ); + methods + } + + /// Collects eval interface methods without duplicating inherited method names. + pub(super) fn collect_interface_method_requirements( + &self, + interface_name: &str, + methods: &mut Vec<(String, EvalInterfaceMethod)>, + seen_interfaces: &mut HashSet, + seen_methods: &mut HashSet, + ) { + let key = normalize_class_name(interface_name); + if !seen_interfaces.insert(key) { + return; + } + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + self.collect_interface_method_requirements( + parent, + methods, + seen_interfaces, + seen_methods, + ); + } + for method in interface.methods() { + let key = method.name().to_ascii_lowercase(); + if seen_methods.insert(key) { + methods.push((interface.name().to_string(), method.clone())); + } + } + } + + /// Returns direct and inherited property contracts for an eval interface. + pub fn interface_property_requirements( + &self, + interface_name: &str, + ) -> Vec { + self.interface_property_requirements_with_owners(interface_name) + .into_iter() + .map(|(_, property)| property) + .collect() + } + + /// Returns direct and inherited property contracts with their declaring interface. + pub fn interface_property_requirements_with_owners( + &self, + interface_name: &str, + ) -> Vec<(String, EvalInterfaceProperty)> { + let mut properties = Vec::new(); + let mut seen_interfaces = HashSet::new(); + self.collect_interface_property_requirements( + interface_name, + &mut properties, + &mut seen_interfaces, + ); + properties + } + + /// Collects eval interface property contracts, merging duplicate inherited names. + pub(super) fn collect_interface_property_requirements( + &self, + interface_name: &str, + properties: &mut Vec<(String, EvalInterfaceProperty)>, + seen_interfaces: &mut HashSet, + ) { + let key = normalize_class_name(interface_name); + if !seen_interfaces.insert(key) { + return; + } + let Some(interface) = self.interface(interface_name) else { + return; + }; + for parent in interface.parents() { + self.collect_interface_property_requirements(parent, properties, seen_interfaces); + } + for property in interface.properties() { + if let Some((_, existing)) = properties + .iter_mut() + .find(|(_, existing)| existing.name() == property.name()) + { + *existing = existing.merged_with(property); + } else { + properties.push((interface.name().to_string(), property.clone())); + } + } + } + + /// Returns whether an eval-declared class satisfies one class/interface target. + pub fn class_is_a(&self, class_name: &str, target: &str, exclude_self: bool) -> bool { + let Some(class) = self.class(class_name) else { + return false; + }; + let target = normalize_class_name( + &self + .resolve_class_like_name(target) + .unwrap_or_else(|| target.trim_start_matches('\\').to_string()), + ); + if !exclude_self && normalize_class_name(class.name()) == target { + return true; + } + if target == normalize_class_name("Stringable") + && self.class_has_valid_tostring(class.name()) + { + return true; + } + self.class_parent_names(class.name()) + .iter() + .any(|parent| normalize_class_name(parent) == target) + || self + .class_interface_names(class.name()) + .iter() + .any(|interface| normalize_class_name(interface) == target) + } + + /// Returns whether one eval class exposes a PHP-compatible `__toString()` method. + pub(super) fn class_has_valid_tostring(&self, class_name: &str) -> bool { + self.class_method(class_name, "__toString") + .is_some_and(|(_, method)| { + method.visibility() == EvalVisibility::Public + && !method.is_static() + && !method.is_abstract() + && method.params().is_empty() + }) + } +} diff --git a/crates/elephc-magician/src/context/classes_aliases.rs b/crates/elephc-magician/src/context/classes_aliases.rs new file mode 100644 index 0000000000..821d822fec --- /dev/null +++ b/crates/elephc-magician/src/context/classes_aliases.rs @@ -0,0 +1,375 @@ +//! Purpose: +//! Registers eval classes, external declarations, aliases, and callable class metadata. +//! +//! Called from: +//! - Class declaration execution and callable construction. +//! +//! Key details: +//! - Alias kinds and global class snapshots preserve case-insensitive PHP lookup. + +use super::*; + +impl ElephcEvalContext { + /// Defines an eval-declared class, failing if this context already has it. + pub fn define_class(&mut self, class: EvalClass) -> bool { + let key = normalize_class_name(class.name()); + if self.classes.contains_key(&key) + || self.class_aliases.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.traits.contains_key(&key) + || self.enums.contains_key(&key) + { + return false; + } + self.declared_class_names.push(class.name().to_string()); + #[cfg(not(test))] + register_global_eval_class(&class); + self.classes.insert(key, class); + true + } + + /// Imports eval-declared process-global class-like metadata not yet known by this context. + #[cfg(not(test))] + pub fn sync_global_eval_classes(&mut self) { + let Ok(registry) = global_eval_classes().lock() else { + return; + }; + for name in ®istry.declared_class_names { + let key = normalize_class_name(name); + if self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.traits.contains_key(&key) + || self.enums.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + continue; + } + let Some(class) = registry.classes.get(&key).cloned() else { + continue; + }; + self.declared_class_names.push(class.name().to_string()); + self.classes.insert(key, class); + } + for name in ®istry.declared_interface_names { + let key = normalize_class_name(name); + if self.interfaces.contains_key(&key) + || self.classes.contains_key(&key) + || self.traits.contains_key(&key) + || self.enums.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + continue; + } + let Some(interface) = registry.interfaces.get(&key).cloned() else { + continue; + }; + self.declared_interface_names + .push(interface.name().to_string()); + self.interfaces.insert(key, interface); + } + for name in ®istry.declared_trait_names { + let key = normalize_class_name(name); + if self.traits.contains_key(&key) + || self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.enums.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + continue; + } + let Some(trait_decl) = registry.traits.get(&key).cloned() else { + continue; + }; + self.declared_trait_names + .push(trait_decl.name().to_string()); + self.traits.insert(key, trait_decl); + } + for name in ®istry.declared_enum_names { + let key = normalize_class_name(name); + if self.enums.contains_key(&key) + || self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.traits.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + continue; + } + let Some(enum_decl) = registry.enums.get(&key).cloned() else { + continue; + }; + self.declared_enum_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + self.declared_class_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + self.classes + .insert(key.clone(), enum_decl.as_class_metadata()); + self.enums.insert(key, enum_decl); + } + for (key, alias) in ®istry.aliases { + if self.classes.contains_key(key) + || self.interfaces.contains_key(key) + || self.traits.contains_key(key) + || self.enums.contains_key(key) + || self.class_aliases.contains_key(key) + { + continue; + } + self.class_aliases.insert(key.clone(), alias.clone()); + } + } + + /// Returns true when this eval context has a dynamic class or alias with the requested name. + pub fn has_class(&self, name: &str) -> bool { + let key = normalize_class_name(name); + self.classes.contains_key(&key) + || self.class_aliases.get(&key).is_some_and(|alias| { + matches!( + alias.kind, + EvalClassAliasKind::Class | EvalClassAliasKind::Enum + ) + }) + } + + /// Returns a dynamic eval class by PHP case-insensitive class name or alias. + pub fn class(&self, name: &str) -> Option<&EvalClass> { + let key = normalize_class_name(name); + if let Some(class) = self.classes.get(&key) { + return Some(class); + } + let alias = self.class_aliases.get(&key)?; + if !matches!( + alias.kind, + EvalClassAliasKind::Class | EvalClassAliasKind::Enum + ) { + return None; + } + self.classes.get(&normalize_class_name(&alias.target)) + } + + /// Resolves a PHP class name or alias to the canonical target spelling stored by eval. + pub fn resolve_class_name(&self, name: &str) -> Option { + let key = normalize_class_name(name); + if let Some(class) = self.classes.get(&key) { + return Some(class.name().to_string()); + } + self.class_aliases.get(&key).and_then(|alias| { + matches!( + alias.kind, + EvalClassAliasKind::Class | EvalClassAliasKind::Enum + ) + .then(|| alias.target.clone()) + }) + } + + /// Registers one eval-created static callable array with late-static dispatch metadata. + pub fn register_eval_static_callable( + &mut self, + callable: RuntimeCellHandle, + class_name: &str, + method: &str, + called_class: &str, + native_dispatch: Option<(&str, &str)>, + ) { + let (native_class, bridge_scope) = native_dispatch + .map(|(native_class, bridge_scope)| { + ( + Some(native_class.trim_start_matches('\\').to_string()), + Some(bridge_scope.trim_start_matches('\\').to_string()), + ) + }) + .unwrap_or((None, None)); + self.eval_static_callables.insert( + callable.as_ptr() as usize, + EvalStaticCallableMetadata { + class_name: class_name.trim_start_matches('\\').to_string(), + method: method.to_string(), + called_class: called_class.trim_start_matches('\\').to_string(), + native_class, + bridge_scope, + }, + ); + } + + /// Returns the captured late-static called class for one matching static callable array. + pub fn eval_static_callable_called_class( + &self, + callable: RuntimeCellHandle, + class_name: &str, + method: &str, + ) -> Option<&str> { + let metadata = self.eval_static_callables.get(&(callable.as_ptr() as usize))?; + let class_name = class_name.trim_start_matches('\\'); + (metadata.class_name.eq_ignore_ascii_case(class_name) + && metadata.method.eq_ignore_ascii_case(method)) + .then_some(metadata.called_class.as_str()) + } + + /// Returns native method bridge metadata captured for one static callable array. + pub fn eval_static_callable_native_dispatch( + &self, + callable: RuntimeCellHandle, + class_name: &str, + method: &str, + ) -> Option<(&str, &str)> { + let metadata = self.eval_static_callables.get(&(callable.as_ptr() as usize))?; + let class_name = class_name.trim_start_matches('\\'); + if !metadata.class_name.eq_ignore_ascii_case(class_name) + || !metadata.method.eq_ignore_ascii_case(method) + { + return None; + } + Some(( + metadata.native_class.as_deref()?, + metadata.bridge_scope.as_deref()?, + )) + } + + /// Registers one eval-created object method callable with native bridge metadata. + pub fn register_eval_object_callable( + &mut self, + callable: RuntimeCellHandle, + object: RuntimeCellHandle, + method: &str, + called_class: &str, + native_class: &str, + bridge_scope: &str, + ) { + self.eval_object_callables.insert( + callable.as_ptr() as usize, + EvalObjectCallableMetadata { + object: object.as_ptr() as usize, + method: method.to_string(), + called_class: called_class.trim_start_matches('\\').to_string(), + native_class: native_class.trim_start_matches('\\').to_string(), + bridge_scope: bridge_scope.trim_start_matches('\\').to_string(), + }, + ); + } + + /// Returns native method bridge metadata captured for one object callable array. + pub fn eval_object_callable_native_dispatch( + &self, + callable: RuntimeCellHandle, + object: RuntimeCellHandle, + method: &str, + ) -> Option<(&str, &str, &str)> { + let metadata = self + .eval_object_callables + .get(&(callable.as_ptr() as usize))?; + (metadata.object == object.as_ptr() as usize + && metadata.method.eq_ignore_ascii_case(method)) + .then_some(( + metadata.native_class.as_str(), + metadata.bridge_scope.as_str(), + metadata.called_class.as_str(), + )) + } + + /// Resolves a PHP class-like name to eval class, interface, trait, or alias spelling. + pub fn resolve_class_like_name(&self, name: &str) -> Option { + let key = normalize_class_name(name); + if let Some(class) = self.classes.get(&key) { + return Some(class.name().to_string()); + } + if let Some(interface) = self.interfaces.get(&key) { + return Some(interface.name().to_string()); + } + if let Some(trait_decl) = self.traits.get(&key) { + return Some(trait_decl.name().to_string()); + } + if let Some(enum_decl) = self.enums.get(&key) { + return Some(enum_decl.name().to_string()); + } + self.class_aliases + .get(&key) + .map(|alias| alias.target.clone()) + } + + /// Defines an alias for an eval-declared class or an already known alias. + pub fn define_class_alias(&mut self, original: &str, alias: &str) -> bool { + let Some((target, kind)) = self.resolve_class_like_alias_target(original) else { + return false; + }; + self.define_class_alias_with_kind(&target, alias, kind) + } + + /// Defines an alias for a runtime-visible class whose metadata lives outside eval. + pub fn define_external_class_alias(&mut self, original: &str, alias: &str) -> bool { + self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Class) + } + + /// Defines an alias for a runtime-visible interface whose metadata lives outside eval. + pub fn define_external_interface_alias(&mut self, original: &str, alias: &str) -> bool { + self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Interface) + } + + /// Defines an alias for a runtime-visible trait whose metadata lives outside eval. + pub fn define_external_trait_alias(&mut self, original: &str, alias: &str) -> bool { + self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Trait) + } + + /// Defines an alias for a runtime-visible enum whose metadata lives outside eval. + pub fn define_external_enum_alias(&mut self, original: &str, alias: &str) -> bool { + self.define_class_alias_with_kind(original, alias, EvalClassAliasKind::Enum) + } + + /// Resolves the canonical target and declaration kind for a class-like alias source. + pub(super) fn resolve_class_like_alias_target( + &self, + original: &str, + ) -> Option<(String, EvalClassAliasKind)> { + let key = normalize_class_name(original); + if let Some(enum_decl) = self.enums.get(&key) { + return Some((enum_decl.name().to_string(), EvalClassAliasKind::Enum)); + } + if let Some(class) = self.classes.get(&key) { + return Some((class.name().to_string(), EvalClassAliasKind::Class)); + } + if let Some(interface) = self.interfaces.get(&key) { + return Some((interface.name().to_string(), EvalClassAliasKind::Interface)); + } + if let Some(trait_decl) = self.traits.get(&key) { + return Some((trait_decl.name().to_string(), EvalClassAliasKind::Trait)); + } + self.class_aliases + .get(&key) + .map(|alias| (alias.target.clone(), alias.kind)) + } + + /// Defines one class-like alias after the caller has resolved the target kind. + pub(super) fn define_class_alias_with_kind( + &mut self, + original: &str, + alias: &str, + kind: EvalClassAliasKind, + ) -> bool { + let alias_key = normalize_class_name(alias); + if alias_key.is_empty() + || self.classes.contains_key(&alias_key) + || self.interfaces.contains_key(&alias_key) + || self.traits.contains_key(&alias_key) + || self.enums.contains_key(&alias_key) + || self.class_aliases.contains_key(&alias_key) + { + return false; + } + let alias_record = EvalClassAlias { + target: original.trim_start_matches('\\').to_string(), + kind, + }; + #[cfg(not(test))] + register_global_eval_alias(alias, &alias_record); + self.class_aliases.insert(alias_key, alias_record); + true + } + + /// Returns class names declared through eval or registered from generated metadata. + pub fn declared_class_names(&self) -> &[String] { + &self.declared_class_names + } + + /// Registers a runtime-visible class or enum declaration name for `get_declared_classes()`. + pub fn define_external_declared_class_name(&mut self, name: &str) -> bool { + push_external_declared_name(&mut self.declared_class_names, name) + } +} diff --git a/crates/elephc-magician/src/context/classlike_objects.rs b/crates/elephc-magician/src/context/classlike_objects.rs new file mode 100644 index 0000000000..46c6ce3aa1 --- /dev/null +++ b/crates/elephc-magician/src/context/classlike_objects.rs @@ -0,0 +1,475 @@ +//! Purpose: +//! Manages interfaces, traits, enums, dynamic objects, and runtime property aliases. +//! +//! Called from: +//! - Class-like declaration execution and object/property runtime paths. +//! +//! Key details: +//! - Enum cases, destructor state, aliases, and initialized-property markers stay context-owned. + +use super::*; + +impl ElephcEvalContext { + /// Defines an eval-declared interface, failing if this context already has the name. + pub fn define_interface(&mut self, interface: EvalInterface) -> bool { + let key = normalize_class_name(interface.name()); + if self.interfaces.contains_key(&key) + || self.classes.contains_key(&key) + || self.traits.contains_key(&key) + || self.enums.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + return false; + } + self.declared_interface_names + .push(interface.name().to_string()); + #[cfg(not(test))] + register_global_eval_interface(&interface); + self.interfaces.insert(key, interface); + true + } + + /// Returns true when this eval context has a dynamic interface with the requested name. + pub fn has_interface(&self, name: &str) -> bool { + let key = normalize_class_name(name); + self.interfaces.contains_key(&key) + || self + .class_aliases + .get(&key) + .is_some_and(|alias| alias.kind == EvalClassAliasKind::Interface) + } + + /// Returns a dynamic eval interface by PHP case-insensitive interface name. + pub fn interface(&self, name: &str) -> Option<&EvalInterface> { + let key = normalize_class_name(name); + if let Some(interface) = self.interfaces.get(&key) { + return Some(interface); + } + let alias = self.class_aliases.get(&key)?; + (alias.kind == EvalClassAliasKind::Interface) + .then(|| self.interfaces.get(&normalize_class_name(&alias.target))) + .flatten() + } + + /// Returns interface names declared through eval or registered from generated metadata. + pub fn declared_interface_names(&self) -> &[String] { + &self.declared_interface_names + } + + /// Registers a runtime-visible interface declaration name for `get_declared_interfaces()`. + pub fn define_external_declared_interface_name(&mut self, name: &str) -> bool { + push_external_declared_name(&mut self.declared_interface_names, name) + } + + /// Defines an eval-declared trait, failing if this context already has the name. + pub fn define_trait(&mut self, trait_decl: EvalTrait) -> bool { + let key = normalize_class_name(trait_decl.name()); + if self.traits.contains_key(&key) + || self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.enums.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + return false; + } + self.declared_trait_names + .push(trait_decl.name().to_string()); + #[cfg(not(test))] + register_global_eval_trait(&trait_decl); + self.traits.insert(key, trait_decl); + true + } + + /// Returns true when this eval context has a dynamic trait with the requested name. + pub fn has_trait(&self, name: &str) -> bool { + let key = normalize_class_name(name); + self.traits.contains_key(&key) + || self + .class_aliases + .get(&key) + .is_some_and(|alias| alias.kind == EvalClassAliasKind::Trait) + } + + /// Returns a dynamic eval trait by PHP case-insensitive trait name. + pub fn trait_decl(&self, name: &str) -> Option<&EvalTrait> { + let key = normalize_class_name(name); + if let Some(trait_decl) = self.traits.get(&key) { + return Some(trait_decl); + } + let alias = self.class_aliases.get(&key)?; + (alias.kind == EvalClassAliasKind::Trait) + .then(|| self.traits.get(&normalize_class_name(&alias.target))) + .flatten() + } + + /// Returns trait names declared through eval or registered from generated metadata. + pub fn declared_trait_names(&self) -> &[String] { + &self.declared_trait_names + } + + /// Registers a runtime-visible trait declaration name for `get_declared_traits()`. + pub fn define_external_declared_trait_name(&mut self, name: &str) -> bool { + push_external_declared_name(&mut self.declared_trait_names, name) + } + + /// Defines an eval-declared enum plus class-shaped metadata for dispatch. + pub fn define_enum(&mut self, enum_decl: EvalEnum) -> bool { + let key = normalize_class_name(enum_decl.name()); + if self.enums.contains_key(&key) + || self.classes.contains_key(&key) + || self.interfaces.contains_key(&key) + || self.traits.contains_key(&key) + || self.class_aliases.contains_key(&key) + { + return false; + } + self.declared_enum_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + self.declared_class_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + #[cfg(not(test))] + register_global_eval_enum(&enum_decl); + self.classes + .insert(key.clone(), enum_decl.as_class_metadata()); + self.enums.insert(key, enum_decl); + true + } + + /// Returns true when this eval context has a dynamic enum with the requested name. + pub fn has_enum(&self, name: &str) -> bool { + let key = normalize_class_name(name); + self.enums.contains_key(&key) + || self + .class_aliases + .get(&key) + .is_some_and(|alias| alias.kind == EvalClassAliasKind::Enum) + } + + /// Returns a dynamic eval enum by PHP case-insensitive enum name. + pub fn enum_decl(&self, name: &str) -> Option<&EvalEnum> { + let key = normalize_class_name(name); + if let Some(enum_decl) = self.enums.get(&key) { + return Some(enum_decl); + } + let alias = self.class_aliases.get(&key)?; + (alias.kind == EvalClassAliasKind::Enum) + .then(|| self.enums.get(&normalize_class_name(&alias.target))) + .flatten() + } + + /// Resolves an enum name or enum alias to the canonical eval enum spelling. + pub fn resolve_enum_name(&self, name: &str) -> Option { + let key = normalize_class_name(name); + if let Some(enum_decl) = self.enums.get(&key) { + return Some(enum_decl.name().to_string()); + } + self.class_aliases.get(&key).and_then(|alias| { + (alias.kind == EvalClassAliasKind::Enum).then(|| alias.target.clone()) + }) + } + + /// Returns enum names declared through eval in PHP-visible order. + pub fn declared_enum_names(&self) -> &[String] { + &self.declared_enum_names + } + + /// Returns a materialized singleton case object for one eval enum case. + pub fn enum_case(&self, enum_name: &str, case_name: &str) -> Option { + let enum_name = self + .resolve_enum_name(enum_name) + .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); + self.enum_cases + .get(&( + normalize_class_name(&enum_name), + normalize_enum_case_name(case_name), + )) + .copied() + } + + /// Stores a materialized singleton case object and returns any replaced distinct cell. + pub fn set_enum_case( + &mut self, + enum_name: &str, + case_name: &str, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self.enum_cases.insert( + ( + normalize_class_name(enum_name), + normalize_enum_case_name(case_name), + ), + cell, + ); + previous.filter(|previous| *previous != cell) + } + + /// Returns a materialized backing value for one eval backed-enum case. + pub fn enum_case_value(&self, enum_name: &str, case_name: &str) -> Option { + let enum_name = self + .resolve_enum_name(enum_name) + .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); + self.enum_case_values + .get(&( + normalize_class_name(&enum_name), + normalize_enum_case_name(case_name), + )) + .copied() + } + + /// Stores a materialized backing value and returns any replaced distinct cell. + pub fn set_enum_case_value( + &mut self, + enum_name: &str, + case_name: &str, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self.enum_case_values.insert( + ( + normalize_class_name(enum_name), + normalize_enum_case_name(case_name), + ), + cell, + ); + previous.filter(|previous| *previous != cell) + } + + /// Records that one runtime object handle was created for an eval-declared class. + pub fn register_dynamic_object(&mut self, identity: u64, class_name: &str) { + let class_name = self + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.to_string()); + self.dynamic_objects + .insert(identity, normalize_class_name(&class_name)); + crate::ffi::dynamic_destructors::register_dynamic_object_context(identity, self as *mut Self); + self.dynamic_destructing_objects.remove(&identity); + self.dynamic_destructed_objects.remove(&identity); + self.dynamic_initialized_properties + .retain(|(object, _)| *object != identity); + } + + /// Removes one dynamic object identity and all per-object eval metadata. + pub fn forget_dynamic_object(&mut self, identity: u64) { + self.dynamic_objects.remove(&identity); + self.closure_objects.remove(&identity); + self.dynamic_destructing_objects.remove(&identity); + self.dynamic_destructed_objects.remove(&identity); + self.dynamic_property_aliases + .retain(|(object, _), _| *object != identity); + self.dynamic_initialized_properties + .retain(|(object, _)| *object != identity); + crate::ffi::dynamic_destructors::unregister_dynamic_object(identity); + } + + /// Removes this context from the process-local dynamic object destructor registry. + pub fn unregister_dynamic_object_context(&self) { + crate::ffi::dynamic_destructors::unregister_dynamic_objects_for_context( + self as *const Self as *mut Self, + ); + } + + /// Returns the dynamic eval class metadata associated with one object identity. + pub fn dynamic_object_class(&self, identity: u64) -> Option<&EvalClass> { + if let Some(class_key) = self.dynamic_objects.get(&identity) { + return self.classes.get(class_key); + } + #[cfg(not(test))] + { + let owner = crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity)?; + let owner = unsafe { owner.as_ref()? }; + if owner.abi_version() != ABI_VERSION { + return None; + } + let class_key = owner.dynamic_objects.get(&identity)?; + self.classes.get(class_key) + } + #[cfg(test)] + { + None + } + } + + /// Returns the PHP-visible eval class name associated with one dynamic object identity. + pub fn dynamic_object_class_name(&self, identity: u64) -> Option { + if self.closure_objects.contains_key(&identity) { + return Some(String::from("Closure")); + } + if let Some(class) = self.dynamic_object_class(identity) { + return Some(class.name().trim_start_matches('\\').to_string()); + } + #[cfg(not(test))] + { + let owner = crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity)?; + let owner = unsafe { owner.as_ref()? }; + if owner.abi_version() != ABI_VERSION { + return None; + } + owner + .dynamic_object_class(identity) + .map(|class| class.name().trim_start_matches('\\').to_string()) + } + #[cfg(test)] + { + None + } + } + + /// Marks one dynamic object's destructor as active if it has not already run. + pub fn begin_dynamic_object_destructor(&mut self, identity: u64) -> bool { + if self.dynamic_destructed_objects.contains(&identity) { + return false; + } + if !self.dynamic_destructing_objects.insert(identity) { + return false; + } + self.dynamic_destructed_objects.insert(identity); + true + } + + /// Clears the active destructor guard for one dynamic object identity. + pub fn finish_dynamic_object_destructor(&mut self, identity: u64) { + self.dynamic_destructing_objects.remove(&identity); + } + + /// Returns whether one dynamic object identity was registered with a class-like name. + pub fn dynamic_object_is_class(&self, identity: u64, class_name: &str) -> bool { + let class_name = normalize_class_name(class_name); + if class_name == "closure" && self.closure_objects.contains_key(&identity) { + return true; + } + if self + .dynamic_objects + .get(&identity) + .is_some_and(|class_key| class_key == &class_name) + { + return true; + } + #[cfg(not(test))] + { + let Some(owner) = + crate::ffi::dynamic_destructors::dynamic_object_owner_context(identity) + else { + return false; + }; + let Some(owner) = (unsafe { owner.as_ref() }) else { + return false; + }; + if owner.abi_version() != ABI_VERSION { + return false; + } + owner + .dynamic_objects + .get(&identity) + .is_some_and(|class_key| class_key == &class_name) + } + #[cfg(test)] + { + false + } + } + + /// Binds one eval object property slot to a persistent PHP reference target. + pub fn bind_dynamic_property_alias( + &mut self, + identity: u64, + storage_property_name: &str, + target: EvalReferenceTarget, + ) -> Option { + self.dynamic_property_aliases + .insert((identity, storage_property_name.to_string()), target) + } + + /// Returns the persistent reference target bound to one eval object property slot. + pub fn dynamic_property_alias( + &self, + identity: u64, + storage_property_name: &str, + ) -> Option<&EvalReferenceTarget> { + self.dynamic_property_aliases + .get(&(identity, storage_property_name.to_string())) + } + + /// Removes the persistent reference target for one eval object property slot. + pub fn remove_dynamic_property_alias( + &mut self, + identity: u64, + storage_property_name: &str, + ) -> Option { + self.dynamic_property_aliases + .remove(&(identity, storage_property_name.to_string())) + } + + /// Binds one runtime array element slot to a PHP reference target. + pub fn bind_array_element_alias( + &mut self, + array: RuntimeCellHandle, + key: EvalArrayReferenceKey, + target: EvalReferenceTarget, + ) -> Option { + self.array_element_aliases + .insert((array.as_ptr() as usize, key), target) + } + + /// Returns the persistent reference target bound to one runtime array element slot. + pub fn array_element_alias( + &self, + array: RuntimeCellHandle, + key: &EvalArrayReferenceKey, + ) -> Option<&EvalReferenceTarget> { + self.array_element_aliases + .get(&(array.as_ptr() as usize, key.clone())) + } + + /// Marks one eval object storage slot as initialized. + pub fn mark_dynamic_property_initialized( + &mut self, + identity: u64, + storage_property_name: &str, + ) { + self.dynamic_initialized_properties + .insert((identity, storage_property_name.to_string())); + } + + /// Marks one eval object storage slot as uninitialized. + pub fn mark_dynamic_property_uninitialized( + &mut self, + identity: u64, + storage_property_name: &str, + ) { + self.dynamic_initialized_properties + .remove(&(identity, storage_property_name.to_string())); + } + + /// Returns whether one eval object storage slot is known to be initialized. + pub fn dynamic_property_is_initialized( + &self, + identity: u64, + storage_property_name: &str, + ) -> bool { + self.dynamic_initialized_properties + .contains(&(identity, storage_property_name.to_string())) + } + + /// Copies persistent property aliases from a source object identity to a clone identity. + pub fn clone_dynamic_property_aliases(&mut self, source_identity: u64, clone_identity: u64) { + let aliases = self + .dynamic_property_aliases + .iter() + .filter_map(|((identity, property), target)| { + (*identity == source_identity).then(|| (property.clone(), target.clone())) + }) + .collect::>(); + for (property, target) in aliases { + self.bind_dynamic_property_alias(clone_identity, &property, target); + } + let initialized = self + .dynamic_initialized_properties + .iter() + .filter_map(|(identity, property)| { + (*identity == source_identity).then(|| property.clone()) + }) + .collect::>(); + for property in initialized { + self.mark_dynamic_property_initialized(clone_identity, &property); + } + } +} diff --git a/crates/elephc-magician/src/context/closure_metadata.rs b/crates/elephc-magician/src/context/closure_metadata.rs new file mode 100644 index 0000000000..b403248a3f --- /dev/null +++ b/crates/elephc-magician/src/context/closure_metadata.rs @@ -0,0 +1,114 @@ +//! Purpose: +//! Defines normalized closure targets, capture bindings, and eval closure metadata. +//! +//! Called from: +//! - Closure construction, binding, Reflection, and callable dispatch. +//! +//! Key details: +//! - Bound receivers/scopes and by-reference captures remain explicit runtime metadata. + +use super::*; + +/// Callable target represented by a PHP-visible eval `Closure` object. +#[derive(Clone)] +pub enum EvalClosureObjectTarget { + Named(String), + BoundNamed { + name: String, + bound_this: Option, + bound_scope: Option, + }, + InvokableObject { + object: RuntimeCellHandle, + }, + ObjectMethod { + object: RuntimeCellHandle, + method: String, + called_class: Option, + native_class: Option, + bridge_scope: Option, + }, + StaticMethod { + class_name: String, + method: String, + called_class: Option, + native_class: Option, + bridge_scope: Option, + }, +} + +/// Runtime value captured by an eval closure literal. +#[derive(Clone)] +pub struct EvalClosureCaptureBinding { + pub(super) name: String, + pub(super) value: RuntimeCellHandle, + pub(super) by_ref_target: Option, +} + +impl EvalClosureCaptureBinding { + /// Creates one captured runtime value with optional caller-side by-reference storage. + pub fn new( + name: impl Into, + value: RuntimeCellHandle, + by_ref_target: Option, + ) -> Self { + Self { + name: name.into(), + value, + by_ref_target, + } + } + + /// Returns the captured variable name without the leading `$`. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the runtime cell captured by the closure. + pub const fn value(&self) -> RuntimeCellHandle { + self.value + } + + /// Returns caller-side writeback metadata for by-reference captures. + pub fn by_ref_target(&self) -> Option<&EvalReferenceTarget> { + self.by_ref_target.as_ref() + } +} + +/// One eval closure instance retained by a synthetic callable name. +#[derive(Clone)] +pub struct EvalClosure { + pub(super) function: EvalFunction, + pub(super) captures: Vec, + pub(super) is_static: bool, +} + +impl EvalClosure { + /// Creates one closure instance from its function body and captured values. + pub fn new( + function: EvalFunction, + captures: Vec, + is_static: bool, + ) -> Self { + Self { + function, + captures, + is_static, + } + } + + /// Returns the executable eval function payload for this closure. + pub fn function(&self) -> &EvalFunction { + &self.function + } + + /// Returns the captured runtime values attached to this closure instance. + pub fn captures(&self) -> &[EvalClosureCaptureBinding] { + &self.captures + } + + /// Returns whether this closure was declared with PHP's `static function` form. + pub const fn is_static(&self) -> bool { + self.is_static + } +} diff --git a/crates/elephc-magician/src/context/core.rs b/crates/elephc-magician/src/context/core.rs new file mode 100644 index 0000000000..e757874fb1 --- /dev/null +++ b/crates/elephc-magician/src/context/core.rs @@ -0,0 +1,239 @@ +//! Purpose: +//! Defines the opaque eval context storage layout and initializes all registries. +//! +//! Called from: +//! - Public context construction and every context method family. +//! +//! Key details: +//! - Generated code only passes this value opaquely; Rust owns every internal collection. + +use super::*; + +/// Process-level eval context passed opaquely across the C ABI. +/// +/// Generated code never inspects this layout directly; it only passes pointers +/// back to the eval bridge. Keeping a concrete Rust type here lets the bridge +/// grow dynamic registries without exposing them to generated assembly. +pub struct ElephcEvalContext { + pub(super) abi_version: u32, + pub(super) classes: HashMap, + pub(super) class_aliases: HashMap, + pub(super) declared_class_names: Vec, + pub(super) interfaces: HashMap, + pub(super) declared_interface_names: Vec, + pub(super) traits: HashMap, + pub(super) declared_trait_names: Vec, + pub(super) enums: HashMap, + pub(super) declared_enum_names: Vec, + pub(super) enum_cases: HashMap<(String, String), RuntimeCellHandle>, + pub(super) enum_case_values: HashMap<(String, String), RuntimeCellHandle>, + pub(super) constants: HashMap, + pub(super) functions: HashMap, + pub(super) closures: HashMap, + pub(super) closure_objects: HashMap, + pub(super) next_closure_id: usize, + pub(super) native_functions: HashMap, + pub(super) native_methods: HashMap<(String, String), NativeCallableSignature>, + pub(super) native_static_methods: HashMap<(String, String), NativeCallableSignature>, + pub(super) native_constructors: HashMap, + pub(super) native_class_parents: HashMap, + pub(super) native_class_attributes: HashMap>, + pub(super) native_method_attributes: HashMap<(String, String), Vec>, + pub(super) native_constant_attributes: HashMap<(String, String), Vec>, + pub(super) native_interface_properties: HashMap>, + pub(super) native_abstract_properties: HashMap>, + pub(super) native_property_types: HashMap<(String, String), EvalParameterType>, + pub(super) native_property_defaults: HashMap<(String, String), NativeCallableDefault>, + pub(super) native_property_attributes: HashMap<(String, String), Vec>, + pub(super) static_locals: HashMap<(String, String), RuntimeCellHandle>, + pub(super) static_properties: HashMap<(String, String), RuntimeCellHandle>, + pub(super) static_property_aliases: HashMap<(String, String), EvalReferenceTarget>, + pub(super) class_constants: HashMap<(String, String), RuntimeCellHandle>, + pub(super) included_files: HashSet, + pub(super) dynamic_objects: HashMap, + pub(super) dynamic_destructing_objects: HashSet, + pub(super) dynamic_destructed_objects: HashSet, + pub(super) dynamic_property_aliases: HashMap<(u64, String), EvalReferenceTarget>, + pub(super) array_element_aliases: HashMap<(usize, EvalArrayReferenceKey), EvalReferenceTarget>, + pub(super) dynamic_initialized_properties: HashSet<(u64, String)>, + pub(super) eval_reflection_attributes: HashMap, + pub(super) eval_reflection_classes: HashMap, + pub(super) eval_reflection_functions: HashMap, + pub(super) eval_reflection_function_closure_targets: HashMap, + pub(super) eval_reflection_methods: HashMap, + pub(super) eval_reflection_properties: HashMap, + pub(super) eval_dynamic_reflection_properties: HashSet, + pub(super) eval_reflection_class_constants: HashMap, + pub(super) eval_static_callables: HashMap, + pub(super) eval_object_callables: HashMap, + pub(super) global_scope: Option<*mut ElephcEvalScope>, + pub(super) function_stack: Vec, + pub(super) class_stack: Vec, + pub(super) called_class_stack: Vec, + pub(super) magic_stack: Vec, + pub(super) pending_throw: Option, + pub(super) spl_autoload_extensions: String, + pub(super) streams: EvalStreamResources, + pub(super) json_last_error: i64, + pub(super) json_last_error_msg: String, + pub(super) default_timezone: String, + pub(super) http_response_code: i64, + pub(super) call_file: String, + pub(super) call_dir: String, + pub(super) call_line: i64, + pub(super) file_magic_override: Option, +} + +impl ElephcEvalContext { + /// Creates a context using the current eval bridge ABI version. + pub fn new() -> Self { + Self { + abi_version: ABI_VERSION, + classes: HashMap::new(), + class_aliases: HashMap::new(), + declared_class_names: Vec::new(), + interfaces: HashMap::new(), + declared_interface_names: Vec::new(), + traits: HashMap::new(), + declared_trait_names: Vec::new(), + enums: HashMap::new(), + declared_enum_names: Vec::new(), + enum_cases: HashMap::new(), + enum_case_values: HashMap::new(), + constants: HashMap::new(), + functions: HashMap::new(), + closures: HashMap::new(), + closure_objects: HashMap::new(), + next_closure_id: 0, + native_functions: HashMap::new(), + native_methods: HashMap::new(), + native_static_methods: HashMap::new(), + native_constructors: HashMap::new(), + native_class_parents: HashMap::new(), + native_class_attributes: HashMap::new(), + native_method_attributes: HashMap::new(), + native_constant_attributes: HashMap::new(), + native_interface_properties: HashMap::new(), + native_abstract_properties: HashMap::new(), + native_property_types: HashMap::new(), + native_property_defaults: HashMap::new(), + native_property_attributes: HashMap::new(), + static_locals: HashMap::new(), + static_properties: HashMap::new(), + static_property_aliases: HashMap::new(), + class_constants: HashMap::new(), + included_files: HashSet::new(), + dynamic_objects: HashMap::new(), + dynamic_destructing_objects: HashSet::new(), + dynamic_destructed_objects: HashSet::new(), + dynamic_property_aliases: HashMap::new(), + array_element_aliases: HashMap::new(), + dynamic_initialized_properties: HashSet::new(), + eval_reflection_attributes: HashMap::new(), + eval_reflection_classes: HashMap::new(), + eval_reflection_functions: HashMap::new(), + eval_reflection_function_closure_targets: HashMap::new(), + eval_reflection_methods: HashMap::new(), + eval_reflection_properties: HashMap::new(), + eval_dynamic_reflection_properties: HashSet::new(), + eval_reflection_class_constants: HashMap::new(), + eval_static_callables: HashMap::new(), + eval_object_callables: HashMap::new(), + global_scope: None, + function_stack: Vec::new(), + class_stack: Vec::new(), + called_class_stack: Vec::new(), + magic_stack: Vec::new(), + pending_throw: None, + spl_autoload_extensions: String::from(".inc,.php"), + streams: EvalStreamResources::default(), + json_last_error: 0, + json_last_error_msg: String::from("No error"), + default_timezone: String::from("UTC"), + http_response_code: 200, + call_file: String::new(), + call_dir: String::new(), + call_line: 0, + file_magic_override: None, + } + } + + /// Creates a context with an explicit ABI version for compatibility tests. + #[cfg(test)] + pub fn for_abi_version(abi_version: u32) -> Self { + Self { + abi_version, + classes: HashMap::new(), + class_aliases: HashMap::new(), + declared_class_names: Vec::new(), + interfaces: HashMap::new(), + declared_interface_names: Vec::new(), + traits: HashMap::new(), + declared_trait_names: Vec::new(), + enums: HashMap::new(), + declared_enum_names: Vec::new(), + enum_cases: HashMap::new(), + enum_case_values: HashMap::new(), + constants: HashMap::new(), + functions: HashMap::new(), + closures: HashMap::new(), + closure_objects: HashMap::new(), + next_closure_id: 0, + native_functions: HashMap::new(), + native_methods: HashMap::new(), + native_static_methods: HashMap::new(), + native_constructors: HashMap::new(), + native_class_parents: HashMap::new(), + native_class_attributes: HashMap::new(), + native_method_attributes: HashMap::new(), + native_constant_attributes: HashMap::new(), + native_interface_properties: HashMap::new(), + native_abstract_properties: HashMap::new(), + native_property_types: HashMap::new(), + native_property_defaults: HashMap::new(), + native_property_attributes: HashMap::new(), + static_locals: HashMap::new(), + static_properties: HashMap::new(), + static_property_aliases: HashMap::new(), + class_constants: HashMap::new(), + included_files: HashSet::new(), + dynamic_objects: HashMap::new(), + dynamic_destructing_objects: HashSet::new(), + dynamic_destructed_objects: HashSet::new(), + dynamic_property_aliases: HashMap::new(), + array_element_aliases: HashMap::new(), + dynamic_initialized_properties: HashSet::new(), + eval_reflection_attributes: HashMap::new(), + eval_reflection_classes: HashMap::new(), + eval_reflection_functions: HashMap::new(), + eval_reflection_function_closure_targets: HashMap::new(), + eval_reflection_methods: HashMap::new(), + eval_reflection_properties: HashMap::new(), + eval_dynamic_reflection_properties: HashSet::new(), + eval_reflection_class_constants: HashMap::new(), + eval_static_callables: HashMap::new(), + eval_object_callables: HashMap::new(), + global_scope: None, + function_stack: Vec::new(), + class_stack: Vec::new(), + called_class_stack: Vec::new(), + magic_stack: Vec::new(), + pending_throw: None, + spl_autoload_extensions: String::from(".inc,.php"), + streams: EvalStreamResources::default(), + json_last_error: 0, + json_last_error_msg: String::from("No error"), + default_timezone: String::from("UTC"), + http_response_code: 200, + call_file: String::new(), + call_dir: String::new(), + call_line: 0, + file_magic_override: None, + } + } + + /// Returns the ABI version this context was created for. + pub const fn abi_version(&self) -> u32 { + self.abi_version + } +} diff --git a/crates/elephc-magician/src/context/functions.rs b/crates/elephc-magician/src/context/functions.rs new file mode 100644 index 0000000000..938128d675 --- /dev/null +++ b/crates/elephc-magician/src/context/functions.rs @@ -0,0 +1,203 @@ +//! Purpose: +//! Registers dynamic constants, functions, closures, and native function metadata. +//! +//! Called from: +//! - Declaration execution, closure creation, and dynamic function dispatch. +//! +//! Key details: +//! - Synthetic closure identities and native parameter metadata remain context-local. + +use super::*; + +impl ElephcEvalContext { + /// Defines an eval dynamic constant value, failing if the name is invalid or already present. + pub fn define_constant(&mut self, name: &str, value: RuntimeCellHandle) -> bool { + let key = normalize_constant_name(name); + if key.is_empty() || self.constants.contains_key(&key) { + return false; + } + self.constants.insert(key, value); + true + } + + /// Returns true when this eval context has a dynamic constant with the requested name. + pub fn has_constant(&self, name: &str) -> bool { + self.constants.contains_key(&normalize_constant_name(name)) + } + + /// Returns an eval dynamic constant value by case-sensitive PHP constant name. + pub fn constant(&self, name: &str) -> Option { + self.constants.get(&normalize_constant_name(name)).copied() + } + + /// Defines a dynamic user function, failing if the name already exists. + pub fn define_function( + &mut self, + name: impl Into, + function: EvalFunction, + ) -> Result<(), EvalFunction> { + let name = name.into(); + if self.functions.contains_key(&name) || self.native_functions.contains_key(&name) { + return Err(function); + } + self.functions.insert(name, function); + Ok(()) + } + + /// Stores one eval closure instance under a context-local synthetic callable name. + pub fn define_closure(&mut self, closure: EvalClosure) -> String { + let name = format!("{{closure:eval:{}}}", self.next_closure_id); + self.next_closure_id += 1; + self.closures.insert(name.clone(), closure); + name + } + + /// Associates a PHP `Closure` object identity with an eval closure callable name. + pub fn register_closure_object(&mut self, identity: u64, closure_name: &str) { + self.register_closure_object_target( + identity, + EvalClosureObjectTarget::Named(closure_name.to_string()), + ); + } + + /// Associates a PHP `Closure` object identity with any eval callable target. + pub fn register_closure_object_target( + &mut self, + identity: u64, + target: EvalClosureObjectTarget, + ) { + self.closure_objects.insert(identity, target); + } + + /// Returns the callable target bound to a PHP `Closure` object. + pub fn closure_object_target(&self, identity: u64) -> Option<&EvalClosureObjectTarget> { + self.closure_objects.get(&identity) + } + + /// Returns the eval closure callable name bound to a literal PHP `Closure` object. + pub fn closure_object_name(&self, identity: u64) -> Option<&str> { + self.closure_objects + .get(&identity) + .and_then(|target| match target { + EvalClosureObjectTarget::Named(name) + | EvalClosureObjectTarget::BoundNamed { name, .. } => Some(name.as_str()), + _ => None, + }) + } + + /// Defines a generated native function callback, failing if the name already exists. + pub fn define_native_function( + &mut self, + name: impl Into, + function: NativeFunction, + ) -> Result<(), NativeFunction> { + let name = name.into(); + if self.functions.contains_key(&name) || self.native_functions.contains_key(&name) { + return Err(function); + } + self.native_functions.insert(name, function); + Ok(()) + } + + /// Returns a dynamic user function by its lowercase PHP function name. + pub fn function(&self, name: &str) -> Option<&EvalFunction> { + self.functions.get(name) + } + + /// Returns a dynamic eval closure by its synthetic callable name. + pub fn closure(&self, name: &str) -> Option<&EvalClosure> { + self.closures.get(name) + } + + /// Returns a native AOT function callback by its lowercase PHP function name. + pub fn native_function(&self, name: &str) -> Option { + self.native_functions.get(name).cloned() + } + + /// Records one parameter name for an already registered native AOT callback. + pub fn define_native_function_param( + &mut self, + function_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_param_name(index, param_name)) + } + + /// Records one parameter type for an already registered native AOT callback. + pub fn define_native_function_param_type( + &mut self, + function_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_param_type(index, param_type)) + } + + /// Records one parameter default for an already registered native AOT callback. + pub fn define_native_function_param_default( + &mut self, + function_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_param_default(index, default)) + } + + /// Records whether one native AOT callback parameter is by-reference. + pub fn define_native_function_param_by_ref( + &mut self, + function_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT callback parameter is variadic. + pub fn define_native_function_variadic_param( + &mut self, + function_name: &str, + index: usize, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| function.set_variadic_index(index)) + } + + /// Records one native AOT callback return type. + pub fn define_native_function_return_type( + &mut self, + function_name: &str, + return_type: EvalParameterType, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| { + function.set_return_type(return_type); + true + }) + } + + /// Records whether eval may dispatch a native AOT callback through its bridge. + pub fn define_native_function_bridge_supported( + &mut self, + function_name: &str, + supported: bool, + ) -> bool { + self.native_functions + .get_mut(function_name) + .is_some_and(|function| { + function.set_bridge_supported(supported); + true + }) + } +} diff --git a/crates/elephc-magician/src/context/global_registry.rs b/crates/elephc-magician/src/context/global_registry.rs new file mode 100644 index 0000000000..bc8d55bdb6 --- /dev/null +++ b/crates/elephc-magician/src/context/global_registry.rs @@ -0,0 +1,198 @@ +//! Purpose: +//! Manages native-frame called-class overrides and the process-global eval class snapshot. +//! +//! Called from: +//! - Native bridge entry points and per-context class-like synchronization. +//! +//! Key details: +//! - Overrides are thread-local guards; global declarations are mutex-protected outside tests. + +use super::*; + +/// Late-static override installed while eval dispatches into a generated/AOT frame. +#[derive(Clone)] +pub(super) struct NativeFrameCalledClassOverride { + #[cfg_attr(test, allow(dead_code))] + context: *mut ElephcEvalContext, + frame_class: String, + called_class: String, +} + +/// Scoped guard that removes one native-frame called-class override on drop. +pub(crate) struct NativeFrameCalledClassOverrideGuard; + +/// Installs a late-static called-class override for a generated/AOT frame call. +pub(crate) fn push_native_frame_called_class_override( + context: *mut ElephcEvalContext, + frame_class: &str, + called_class: &str, +) -> NativeFrameCalledClassOverrideGuard { + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides + .borrow_mut() + .push(NativeFrameCalledClassOverride { + context, + frame_class: frame_class.trim_start_matches('\\').to_string(), + called_class: called_class.trim_start_matches('\\').to_string(), + }); + }); + NativeFrameCalledClassOverrideGuard +} + +impl Drop for NativeFrameCalledClassOverrideGuard { + /// Removes the most recent native-frame called-class override. + fn drop(&mut self) { + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides.borrow_mut().pop(); + }); + } +} + +/// Returns the active thread-local late-static override for one generated/AOT frame. +pub(super) fn native_frame_called_class_override( + frame_class: &str, + called_class: &str, +) -> Option { + let frame_class = frame_class.trim_start_matches('\\'); + let called_class = called_class.trim_start_matches('\\'); + if frame_class.is_empty() || !called_class.eq_ignore_ascii_case(frame_class) { + return None; + } + native_frame_called_class_override_for_frame(frame_class) +} + +/// Returns the active called-class override for one generated/AOT frame class. +pub(crate) fn native_frame_called_class_override_for_frame(frame_class: &str) -> Option { + let frame_class = frame_class.trim_start_matches('\\'); + if frame_class.is_empty() { + return None; + } + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides + .borrow() + .iter() + .rev() + .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) + .map(|entry| entry.called_class.clone()) + }) +} + +/// Returns the active called-class override bytes for one generated/AOT frame class. +pub(crate) fn native_frame_called_class_override_bytes( + frame_class: &str, +) -> Option<(*const u8, usize)> { + let frame_class = frame_class.trim_start_matches('\\'); + if frame_class.is_empty() { + return None; + } + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides + .borrow() + .iter() + .rev() + .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) + .map(|entry| (entry.called_class.as_ptr(), entry.called_class.len())) + }) +} + +/// Returns the active eval context and called class for one generated/AOT frame. +#[cfg_attr(test, allow(dead_code))] +pub(crate) fn native_frame_called_class_override_context( + frame_class: &str, +) -> Option<(*mut ElephcEvalContext, String)> { + let frame_class = frame_class.trim_start_matches('\\'); + if frame_class.is_empty() { + return None; + } + NATIVE_FRAME_CALLED_CLASS_OVERRIDES.with(|overrides| { + overrides + .borrow() + .iter() + .rev() + .find(|entry| entry.frame_class.eq_ignore_ascii_case(frame_class)) + .map(|entry| (entry.context, entry.called_class.clone())) + }) +} + +#[cfg(not(test))] +#[derive(Default)] +pub(super) struct GlobalEvalClassRegistry { + pub(super) classes: HashMap, + pub(super) declared_class_names: Vec, + pub(super) interfaces: HashMap, + pub(super) declared_interface_names: Vec, + pub(super) traits: HashMap, + pub(super) declared_trait_names: Vec, + pub(super) enums: HashMap, + pub(super) declared_enum_names: Vec, + pub(super) aliases: HashMap, +} + +/// Returns the process-local eval class registry for generated-code eval contexts. +#[cfg(not(test))] +pub(super) fn global_eval_classes() -> &'static Mutex { + GLOBAL_EVAL_CLASSES.get_or_init(|| Mutex::new(GlobalEvalClassRegistry::default())) +} + +/// Records one eval-declared class so later eval contexts can see PHP-global metadata. +#[cfg(not(test))] +pub(super) fn register_global_eval_class(class: &EvalClass) { + let key = normalize_class_name(class.name()); + if let Ok(mut registry) = global_eval_classes().lock() { + if !registry.classes.contains_key(&key) { + registry.declared_class_names.push(class.name().to_string()); + } + registry.classes.insert(key, class.clone()); + } +} + +/// Records one eval-declared interface so later eval contexts can see PHP-global metadata. +#[cfg(not(test))] +pub(super) fn register_global_eval_interface(interface: &EvalInterface) { + let key = normalize_class_name(interface.name()); + if let Ok(mut registry) = global_eval_classes().lock() { + if !registry.interfaces.contains_key(&key) { + registry + .declared_interface_names + .push(interface.name().to_string()); + } + registry.interfaces.insert(key, interface.clone()); + } +} + +/// Records one eval-declared trait so later eval contexts can see PHP-global metadata. +#[cfg(not(test))] +pub(super) fn register_global_eval_trait(trait_decl: &EvalTrait) { + let key = normalize_class_name(trait_decl.name()); + if let Ok(mut registry) = global_eval_classes().lock() { + if !registry.traits.contains_key(&key) { + registry + .declared_trait_names + .push(trait_decl.name().to_string()); + } + registry.traits.insert(key, trait_decl.clone()); + } +} + +/// Records one eval-declared enum so later eval contexts can see PHP-global metadata. +#[cfg(not(test))] +pub(super) fn register_global_eval_enum(enum_decl: &EvalEnum) { + let key = normalize_class_name(enum_decl.name()); + if let Ok(mut registry) = global_eval_classes().lock() { + if !registry.enums.contains_key(&key) { + registry + .declared_enum_names + .push(enum_decl.name().trim_start_matches('\\').to_string()); + } + registry.enums.insert(key, enum_decl.clone()); + } +} + +/// Records one eval-defined class-like alias for later generated eval contexts. +#[cfg(not(test))] +pub(super) fn register_global_eval_alias(alias_name: &str, alias: &EvalClassAlias) { + let key = normalize_class_name(alias_name); + if let Ok(mut registry) = global_eval_classes().lock() { + registry.aliases.insert(key, alias.clone()); + } +} diff --git a/crates/elephc-magician/src/context/native_defaults.rs b/crates/elephc-magician/src/context/native_defaults.rs new file mode 100644 index 0000000000..c121e29bf7 --- /dev/null +++ b/crates/elephc-magician/src/context/native_defaults.rs @@ -0,0 +1,235 @@ +//! Purpose: +//! Defines native callable defaults and reusable instance/static/constructor signatures. +//! +//! Called from: +//! - FFI registration, argument binding, Reflection, and default materialization. +//! +//! Key details: +//! - Scalar, array, and object defaults preserve keyed/named structure without runtime cells. + +use super::*; + +/// Default value for a native AOT callable parameter visible to eval fragments. +#[derive(Clone, Debug, PartialEq)] +pub enum NativeCallableDefault { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), + EmptyArray, + Array(Vec), + Object { + class_name: String, + args: Vec, + }, +} + +/// One element in an array-valued native AOT callable default. +#[derive(Clone, Debug, PartialEq)] +pub struct NativeCallableArrayDefaultElement { + pub key: Option, + pub value: NativeCallableDefault, +} + +impl NativeCallableArrayDefaultElement { + /// Creates one auto-indexed element for an array-valued default. + pub fn positional(value: NativeCallableDefault) -> Self { + Self { key: None, value } + } + + /// Creates one explicitly keyed element for an array-valued default. + pub fn keyed(key: NativeCallableArrayDefaultKey, value: NativeCallableDefault) -> Self { + Self { + key: Some(key), + value, + } + } +} + +/// Static PHP array key retained for an array-valued native AOT callable default. +#[derive(Clone, Debug, PartialEq)] +pub enum NativeCallableArrayDefaultKey { + Int(i64), + String(String), +} + +/// Constructor argument for an object-valued native AOT callable default. +#[derive(Clone, Debug, PartialEq)] +pub struct NativeCallableObjectDefaultArg { + pub name: Option, + pub value: NativeCallableDefault, +} + +impl NativeCallableObjectDefaultArg { + /// Creates one positional constructor argument for an object-valued default. + pub fn positional(value: NativeCallableDefault) -> Self { + Self { name: None, value } + } + + /// Creates one named constructor argument for an object-valued default. + pub fn named(name: impl Into, value: NativeCallableDefault) -> Self { + Self { + name: Some(name.into()), + value, + } + } +} + +/// Native AOT method or constructor signature metadata visible to eval fragments. +#[derive(Clone)] +pub struct NativeCallableSignature { + pub(super) param_count: usize, + pub(super) param_names: Vec, + pub(super) param_types: Vec>, + pub(super) param_defaults: Vec>, + pub(super) param_by_ref: Vec, + pub(super) variadic_index: Option, + pub(super) return_type: Option, + pub(super) bridge_supported: bool, +} + +impl NativeCallableSignature { + /// Creates signature metadata with the visible positional parameter count. + pub const fn new(param_count: usize) -> Self { + Self { + param_count, + param_names: Vec::new(), + param_types: Vec::new(), + param_defaults: Vec::new(), + param_by_ref: Vec::new(), + variadic_index: None, + return_type: None, + bridge_supported: true, + } + } + + /// Returns the visible positional parameter count accepted by this callable. + pub const fn param_count(&self) -> usize { + self.param_count + } + + /// Records the PHP parameter name for one positional callable slot. + pub fn set_param_name(&mut self, index: usize, name: impl Into) -> bool { + if index >= self.param_count { + return false; + } + if self.param_names.len() < self.param_count { + self.param_names.resize(self.param_count, String::new()); + } + self.param_names[index] = name.into(); + true + } + + /// Records the PHP declared type metadata for one positional callable slot. + pub fn set_param_type(&mut self, index: usize, param_type: EvalParameterType) -> bool { + if index >= self.param_count { + return false; + } + if self.param_types.len() < self.param_count { + self.param_types.resize(self.param_count, None); + } + self.param_types[index] = Some(param_type); + true + } + + /// Records a PHP scalar default value for one positional callable slot. + pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { + if index >= self.param_count { + return false; + } + if self.param_defaults.len() < self.param_count { + self.param_defaults.resize(self.param_count, None); + } + self.param_defaults[index] = Some(default); + true + } + + /// Records whether one positional callable parameter is by-reference. + pub fn set_param_by_ref(&mut self, index: usize, by_ref: bool) -> bool { + if index >= self.param_count { + return false; + } + if self.param_by_ref.len() < self.param_count { + self.param_by_ref.resize(self.param_count, false); + } + self.param_by_ref[index] = by_ref; + true + } + + /// Records which positional callable parameter is variadic. + pub fn set_variadic_index(&mut self, index: usize) -> bool { + if index >= self.param_count { + return false; + } + self.variadic_index = Some(index); + true + } + + /// Records the PHP declared return type metadata for this callable. + pub fn set_return_type(&mut self, return_type: EvalParameterType) { + self.return_type = Some(return_type); + } + + /// Records whether eval may dispatch this callable through the generated bridge. + pub fn set_bridge_supported(&mut self, supported: bool) { + self.bridge_supported = supported; + } + + /// Returns the PHP-visible parameter names registered for this callable. + pub fn param_names(&self) -> &[String] { + &self.param_names + } + + /// Returns PHP declared parameter types registered for this callable. + pub fn param_types(&self) -> &[Option] { + &self.param_types + } + + /// Returns the registered declared type for one parameter slot, if any. + pub fn param_type(&self, index: usize) -> Option<&EvalParameterType> { + self.param_types.get(index).and_then(Option::as_ref) + } + + /// Returns the PHP-visible scalar parameter defaults registered for this callable. + pub fn param_defaults(&self) -> &[Option] { + &self.param_defaults + } + + /// Returns the registered scalar default for one parameter slot, if any. + pub fn param_default(&self, index: usize) -> Option<&NativeCallableDefault> { + self.param_defaults.get(index).and_then(Option::as_ref) + } + + /// Returns whether one registered parameter is by-reference. + pub fn param_by_ref(&self, index: usize) -> bool { + self.param_by_ref.get(index).copied().unwrap_or(false) + } + + /// Returns whether one registered parameter is the variadic parameter. + pub fn param_variadic(&self, index: usize) -> bool { + self.variadic_index == Some(index) + } + + /// Returns whether eval may dispatch this callable through the generated bridge. + pub const fn bridge_supported(&self) -> bool { + self.bridge_supported + } + + /// Returns the minimum number of arguments required by registered defaults. + pub fn required_param_count(&self) -> usize { + if let Some(index) = self.variadic_index { + return (0..index) + .rfind(|position| self.param_default(*position).is_none()) + .map_or(0, |position| position + 1); + } + (0..self.param_count) + .rfind(|index| self.param_default(*index).is_none()) + .map_or(0, |index| index + 1) + } + + /// Returns the registered declared return type metadata, if any. + pub fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } +} diff --git a/crates/elephc-magician/src/context/native_function.rs b/crates/elephc-magician/src/context/native_function.rs new file mode 100644 index 0000000000..b07ead6117 --- /dev/null +++ b/crates/elephc-magician/src/context/native_function.rs @@ -0,0 +1,180 @@ +//! Purpose: +//! Defines registered native function descriptors and their callable signatures. +//! +//! Called from: +//! - FFI registration and native function binding/dispatch. +//! +//! Key details: +//! - Parameter metadata, bridge support, required arity, and return type travel with the invoker. + +use super::*; + +/// Native AOT function callback metadata visible to runtime eval fragments. +#[derive(Clone)] +pub struct NativeFunction { + pub(super) descriptor: *mut c_void, + pub(super) invoker: NativeFunctionInvoker, + pub(super) param_count: usize, + pub(super) param_names: Vec, + pub(super) param_types: Vec>, + pub(super) param_defaults: Vec>, + pub(super) param_by_ref: Vec, + pub(super) variadic_index: Option, + pub(super) return_type: Option, + pub(super) bridge_supported: bool, +} + +impl NativeFunction { + /// Creates callback metadata for a descriptor-compatible AOT function. + pub fn new( + descriptor: *mut c_void, + invoker: NativeFunctionInvoker, + param_count: usize, + ) -> Self { + Self { + descriptor, + invoker, + param_count, + param_names: Vec::new(), + param_types: Vec::new(), + param_defaults: Vec::new(), + param_by_ref: Vec::new(), + variadic_index: None, + return_type: None, + bridge_supported: true, + } + } + + /// Returns the visible positional parameter count accepted by this callback. + pub const fn param_count(&self) -> usize { + self.param_count + } + + /// Records the PHP parameter name for one positional callback slot. + pub fn set_param_name(&mut self, index: usize, name: impl Into) -> bool { + if index >= self.param_count { + return false; + } + if self.param_names.len() < self.param_count { + self.param_names.resize(self.param_count, String::new()); + } + self.param_names[index] = name.into(); + true + } + + /// Returns the PHP-visible parameter names registered for this callback. + pub fn param_names(&self) -> &[String] { + &self.param_names + } + + /// Records the PHP declared type metadata for one positional callback slot. + pub fn set_param_type(&mut self, index: usize, param_type: EvalParameterType) -> bool { + if index >= self.param_count { + return false; + } + if self.param_types.len() < self.param_count { + self.param_types.resize(self.param_count, None); + } + self.param_types[index] = Some(param_type); + true + } + + /// Returns PHP declared parameter types registered for this callback. + pub fn param_types(&self) -> &[Option] { + &self.param_types + } + + /// Returns the registered declared type for one parameter slot, if any. + pub fn param_type(&self, index: usize) -> Option<&EvalParameterType> { + self.param_types.get(index).and_then(Option::as_ref) + } + + /// Records a PHP default value for one positional callback slot. + pub fn set_param_default(&mut self, index: usize, default: NativeCallableDefault) -> bool { + if index >= self.param_count { + return false; + } + if self.param_defaults.len() < self.param_count { + self.param_defaults.resize(self.param_count, None); + } + self.param_defaults[index] = Some(default); + true + } + + /// Returns the registered default for one parameter slot, if any. + pub fn param_default(&self, index: usize) -> Option<&NativeCallableDefault> { + self.param_defaults.get(index).and_then(Option::as_ref) + } + + /// Records whether one positional callback parameter is by-reference. + pub fn set_param_by_ref(&mut self, index: usize, by_ref: bool) -> bool { + if index >= self.param_count { + return false; + } + if self.param_by_ref.len() < self.param_count { + self.param_by_ref.resize(self.param_count, false); + } + self.param_by_ref[index] = by_ref; + true + } + + /// Records which positional callback parameter is variadic. + pub fn set_variadic_index(&mut self, index: usize) -> bool { + if index >= self.param_count { + return false; + } + self.variadic_index = Some(index); + true + } + + /// Records the PHP declared return type metadata for this callback. + pub fn set_return_type(&mut self, return_type: EvalParameterType) { + self.return_type = Some(return_type); + } + + /// Records whether eval may dispatch this callback through the generated bridge. + pub fn set_bridge_supported(&mut self, supported: bool) { + self.bridge_supported = supported; + } + + /// Returns whether one registered parameter is by-reference. + pub fn param_by_ref(&self, index: usize) -> bool { + self.param_by_ref.get(index).copied().unwrap_or(false) + } + + /// Returns whether one registered parameter is the variadic parameter. + pub fn param_variadic(&self, index: usize) -> bool { + self.variadic_index == Some(index) + } + + /// Returns the registered declared return type, if any. + pub fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + + /// Returns whether eval may dispatch this callback through the generated bridge. + pub const fn bridge_supported(&self) -> bool { + self.bridge_supported + } + + /// Returns the minimum number of required parameters implied by defaults. + pub fn required_param_count(&self) -> usize { + if let Some(index) = self.variadic_index { + return (0..index) + .rfind(|position| self.param_default(*position).is_none()) + .map_or(0, |position| position + 1); + } + (0..self.param_count) + .rfind(|index| self.param_default(*index).is_none()) + .map_or(0, |index| index + 1) + } + + /// Invokes the descriptor-compatible callback with a boxed Mixed arg array. + /// + /// # Safety + /// `arg_array` must be a boxed Mixed indexed array whose elements are boxed + /// Mixed cells following the descriptor-invoker ABI. + pub unsafe fn call(&self, arg_array: RuntimeCellHandle) -> RuntimeCellHandle { + RuntimeCellHandle::from_raw((self.invoker)(self.descriptor, arg_array.as_ptr())) + } +} diff --git a/crates/elephc-magician/src/context/native_metadata.rs b/crates/elephc-magician/src/context/native_metadata.rs new file mode 100644 index 0000000000..b0be4a30c6 --- /dev/null +++ b/crates/elephc-magician/src/context/native_metadata.rs @@ -0,0 +1,263 @@ +//! Purpose: +//! Stores generated class hierarchy, attributes, properties, and abstract contract metadata. +//! +//! Called from: +//! - FFI registration, declaration validation, Reflection, and property access. +//! +//! Key details: +//! - Native member keys are normalized consistently with eval class-like lookups. + +use super::*; + +impl ElephcEvalContext { + /// Defines generated AOT parent metadata for eval `parent::` resolution. + pub fn define_native_class_parent(&mut self, class_name: &str, parent_name: &str) -> bool { + let class_key = normalize_class_name(class_name); + let parent_name = parent_name.trim_start_matches('\\'); + if class_key.is_empty() || parent_name.is_empty() { + return false; + } + self.native_class_parents + .insert(class_key, parent_name.to_string()) + .is_none() + } + + /// Returns generated AOT parent metadata by PHP class name. + pub fn native_class_parent(&self, class_name: &str) -> Option<&str> { + self.native_class_parents + .get(&normalize_class_name(class_name)) + .map(String::as_str) + } + + /// Appends generated AOT class attribute metadata for eval reflection. + pub fn define_native_class_attribute( + &mut self, + class_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = normalize_class_name(class_name); + if key.is_empty() { + return false; + } + self.native_class_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT class attribute metadata by PHP class name. + pub fn native_class_attributes(&self, class_name: &str) -> Vec { + self.native_class_attributes + .get(&normalize_class_name(class_name)) + .cloned() + .unwrap_or_default() + } + + /// Appends generated AOT method attribute metadata for eval reflection. + pub fn define_native_method_attribute( + &mut self, + class_name: &str, + method_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_method_key(class_name, method_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_method_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT method attribute metadata by PHP class and method name. + pub fn native_method_attributes( + &self, + class_name: &str, + method_name: &str, + ) -> Vec { + self.native_method_attributes + .get(&native_method_key(class_name, method_name)) + .cloned() + .unwrap_or_default() + } + + /// Appends generated AOT class-constant attribute metadata for eval reflection. + pub fn define_native_constant_attribute( + &mut self, + class_name: &str, + constant_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_constant_key(class_name, constant_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_constant_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT class-constant attribute metadata by PHP class and constant name. + pub fn native_constant_attributes( + &self, + class_name: &str, + constant_name: &str, + ) -> Vec { + self.native_constant_attributes + .get(&native_constant_key(class_name, constant_name)) + .cloned() + .unwrap_or_default() + } + + /// Defines generated AOT interface property-hook metadata for eval validation. + pub fn define_native_interface_property_requirement( + &mut self, + interface_name: &str, + declaring_interface_name: &str, + property: EvalInterfaceProperty, + ) -> bool { + let key = normalize_class_name(interface_name); + let owner = declaring_interface_name.trim_start_matches('\\').to_string(); + if key.is_empty() || owner.is_empty() || property.name().is_empty() { + return false; + } + let requirements = self.native_interface_properties.entry(key).or_default(); + if requirements.iter().any(|(_, existing)| existing.name() == property.name()) { + return false; + } + requirements.push((owner, property)); + true + } + + /// Returns generated AOT interface property-hook metadata by interface name. + pub fn native_interface_property_requirements( + &self, + interface_name: &str, + ) -> Vec<(String, EvalInterfaceProperty)> { + self.native_interface_properties + .get(&normalize_class_name(interface_name)) + .cloned() + .unwrap_or_default() + } + + /// Defines generated AOT abstract class property-hook metadata for eval validation. + pub fn define_native_abstract_property_requirement( + &mut self, + class_name: &str, + declaring_class_name: &str, + property: EvalInterfaceProperty, + ) -> bool { + let key = normalize_class_name(class_name); + let owner = declaring_class_name.trim_start_matches('\\').to_string(); + if key.is_empty() || owner.is_empty() || property.name().is_empty() { + return false; + } + let requirements = self.native_abstract_properties.entry(key).or_default(); + if requirements + .iter() + .any(|(_, existing)| existing.name() == property.name()) + { + return false; + } + requirements.push((owner, property)); + true + } + + /// Returns generated AOT abstract class property-hook metadata by class name. + pub fn native_abstract_property_requirements( + &self, + class_name: &str, + ) -> Vec<(String, EvalInterfaceProperty)> { + self.native_abstract_properties + .get(&normalize_class_name(class_name)) + .cloned() + .unwrap_or_default() + } + + /// Defines generated AOT property type metadata for eval reflection. + pub fn define_native_property_type( + &mut self, + class_name: &str, + property_name: &str, + property_type: EvalParameterType, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_types + .insert(key, property_type) + .is_none() + } + + /// Returns generated AOT property type metadata by PHP class and property name. + pub fn native_property_type( + &self, + class_name: &str, + property_name: &str, + ) -> Option { + self.native_property_types + .get(&native_property_key(class_name, property_name)) + .cloned() + } + + /// Defines generated AOT property default metadata for eval reflection. + pub fn define_native_property_default( + &mut self, + class_name: &str, + property_name: &str, + default: NativeCallableDefault, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_defaults.insert(key, default).is_none() + } + + /// Returns generated AOT property default metadata by PHP class and property name. + pub fn native_property_default( + &self, + class_name: &str, + property_name: &str, + ) -> Option { + self.native_property_defaults + .get(&native_property_key(class_name, property_name)) + .cloned() + } + + /// Appends generated AOT property attribute metadata for eval reflection. + pub fn define_native_property_attribute( + &mut self, + class_name: &str, + property_name: &str, + attribute: EvalAttribute, + ) -> bool { + let key = native_property_key(class_name, property_name); + if key.0.is_empty() || key.1.is_empty() { + return false; + } + self.native_property_attributes + .entry(key) + .or_default() + .push(attribute); + true + } + + /// Returns generated AOT property attribute metadata by PHP class and property name. + pub fn native_property_attributes( + &self, + class_name: &str, + property_name: &str, + ) -> Vec { + self.native_property_attributes + .get(&native_property_key(class_name, property_name)) + .cloned() + .unwrap_or_default() + } +} diff --git a/crates/elephc-magician/src/context/native_signatures.rs b/crates/elephc-magician/src/context/native_signatures.rs new file mode 100644 index 0000000000..82a9b3a123 --- /dev/null +++ b/crates/elephc-magician/src/context/native_signatures.rs @@ -0,0 +1,340 @@ +//! Purpose: +//! Registers generated instance, static, and constructor callable signatures. +//! +//! Called from: +//! - FFI registration and native argument binding. +//! +//! Key details: +//! - Parameter names, types, defaults, by-ref flags, variadics, and return types share one shape. + +use super::*; + +impl ElephcEvalContext { + /// Defines native AOT instance-method signature metadata for eval named-argument binding. + pub fn define_native_method_signature( + &mut self, + class_name: &str, + method_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_methods + .insert(native_method_key(class_name, method_name), signature) + .is_none() + } + + /// Defines native AOT static-method signature metadata for eval named-argument binding. + pub fn define_native_static_method_signature( + &mut self, + class_name: &str, + method_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_static_methods + .insert(native_method_key(class_name, method_name), signature) + .is_none() + } + + /// Defines native AOT constructor signature metadata for eval named-argument binding. + pub fn define_native_constructor_signature( + &mut self, + class_name: &str, + signature: NativeCallableSignature, + ) -> bool { + self.native_constructors + .insert(normalize_class_name(class_name), signature) + .is_none() + } + + /// Records one parameter name for registered native AOT instance-method metadata. + pub fn define_native_method_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Records one parameter type for registered native AOT instance-method metadata. + pub fn define_native_method_param_type( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + + /// Records one parameter default for registered native AOT instance-method metadata. + pub fn define_native_method_param_default( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + + /// Records whether one native AOT instance-method parameter is by-reference. + pub fn define_native_method_param_by_ref( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT instance-method parameter is variadic. + pub fn define_native_method_variadic_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT instance method. + pub fn define_native_method_bridge_supported( + &mut self, + class_name: &str, + method_name: &str, + supported: bool, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + + /// Records one return type for registered native AOT instance-method metadata. + pub fn define_native_method_return_type( + &mut self, + class_name: &str, + method_name: &str, + return_type: EvalParameterType, + ) -> bool { + self.native_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_return_type(return_type); + true + }) + } + + /// Records one parameter name for registered native AOT static-method metadata. + pub fn define_native_static_method_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Records one parameter type for registered native AOT static-method metadata. + pub fn define_native_static_method_param_type( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + + /// Records one parameter default for registered native AOT static-method metadata. + pub fn define_native_static_method_param_default( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + + /// Records whether one native AOT static-method parameter is by-reference. + pub fn define_native_static_method_param_by_ref( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT static-method parameter is variadic. + pub fn define_native_static_method_variadic_param( + &mut self, + class_name: &str, + method_name: &str, + index: usize, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT static method. + pub fn define_native_static_method_bridge_supported( + &mut self, + class_name: &str, + method_name: &str, + supported: bool, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + + /// Records one return type for registered native AOT static-method metadata. + pub fn define_native_static_method_return_type( + &mut self, + class_name: &str, + method_name: &str, + return_type: EvalParameterType, + ) -> bool { + self.native_static_methods + .get_mut(&native_method_key(class_name, method_name)) + .is_some_and(|signature| { + signature.set_return_type(return_type); + true + }) + } + + /// Records one parameter name for registered native AOT constructor metadata. + pub fn define_native_constructor_param( + &mut self, + class_name: &str, + index: usize, + param_name: impl Into, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_name(index, param_name)) + } + + /// Records one parameter type for registered native AOT constructor metadata. + pub fn define_native_constructor_param_type( + &mut self, + class_name: &str, + index: usize, + param_type: EvalParameterType, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_type(index, param_type)) + } + + /// Records one parameter default for registered native AOT constructor metadata. + pub fn define_native_constructor_param_default( + &mut self, + class_name: &str, + index: usize, + default: NativeCallableDefault, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_default(index, default)) + } + + /// Records whether one native AOT constructor parameter is by-reference. + pub fn define_native_constructor_param_by_ref( + &mut self, + class_name: &str, + index: usize, + by_ref: bool, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_param_by_ref(index, by_ref)) + } + + /// Records which native AOT constructor parameter is variadic. + pub fn define_native_constructor_variadic_param( + &mut self, + class_name: &str, + index: usize, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| signature.set_variadic_index(index)) + } + + /// Records whether eval may dispatch one native AOT constructor. + pub fn define_native_constructor_bridge_supported( + &mut self, + class_name: &str, + supported: bool, + ) -> bool { + self.native_constructors + .get_mut(&normalize_class_name(class_name)) + .is_some_and(|signature| { + signature.set_bridge_supported(supported); + true + }) + } + + /// Returns native AOT instance-method signature metadata by PHP class and method name. + pub fn native_method_signature( + &self, + class_name: &str, + method_name: &str, + ) -> Option { + self.native_methods + .get(&native_method_key(class_name, method_name)) + .cloned() + } + + /// Returns native AOT static-method signature metadata by PHP class and method name. + pub fn native_static_method_signature( + &self, + class_name: &str, + method_name: &str, + ) -> Option { + self.native_static_methods + .get(&native_method_key(class_name, method_name)) + .cloned() + } + + /// Returns native AOT constructor signature metadata by PHP class name. + pub fn native_constructor_signature( + &self, + class_name: &str, + ) -> Option { + self.native_constructors + .get(&normalize_class_name(class_name)) + .cloned() + } +} diff --git a/crates/elephc-magician/src/context/normalization.rs b/crates/elephc-magician/src/context/normalization.rs new file mode 100644 index 0000000000..a8b4583e4e --- /dev/null +++ b/crates/elephc-magician/src/context/normalization.rs @@ -0,0 +1,118 @@ +//! Purpose: +//! Provides default context construction and normalized registry-key helpers. +//! +//! Called from: +//! - Context registries and public `Default` construction. +//! +//! Key details: +//! - PHP class, method, property, constant, and enum-case names use their required casing rules. + +use super::*; + +impl Default for ElephcEvalContext { + /// Creates the default process-level eval context. + fn default() -> Self { + Self::new() + } +} + +/// Normalizes PHP class names for the eval dynamic class registry. +pub(super) fn normalize_class_name(name: &str) -> String { + name.trim_start_matches('\\').to_ascii_lowercase() +} + +/// Adds an external declaration name once while preserving PHP-visible spelling. +pub(super) fn push_external_declared_name(names: &mut Vec, name: &str) -> bool { + let visible_name = name.trim_start_matches('\\'); + let key = normalize_class_name(visible_name); + if key.is_empty() { + return false; + } + if !names + .iter() + .any(|existing| normalize_class_name(existing) == key) + { + names.push(visible_name.to_string()); + } + true +} + +/// Normalizes PHP enum case names for case-sensitive eval enum lookup. +pub(super) fn normalize_enum_case_name(name: &str) -> String { + name.to_string() +} + +/// Normalizes PHP method names for case-insensitive native metadata lookup. +pub(super) fn normalize_method_name(name: &str) -> String { + name.to_ascii_lowercase() +} + +/// Builds the folded native method metadata key used for eval argument binding. +pub(super) fn native_method_key(class_name: &str, method_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + normalize_method_name(method_name), + ) +} + +/// Builds the folded native property metadata key used for eval reflection. +pub(super) fn native_property_key(class_name: &str, property_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + property_name.trim_start_matches('$').to_string(), + ) +} + +/// Builds the case-sensitive native class-constant metadata key used for eval reflection. +pub(super) fn native_constant_key(class_name: &str, constant_name: &str) -> (String, String) { + ( + normalize_class_name(class_name), + constant_name.to_string(), + ) +} + +/// Pushes a PHP class-like name once, preserving the first visible spelling. +pub(super) fn push_unique_class_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + let key = normalize_class_name(name); + if seen.insert(key) { + names.push(name.trim_start_matches('\\').to_string()); + } +} + +/// Returns whether two PHP class-like names resolve to the same normalized spelling. +pub(super) fn same_class_name(left: &str, right: &str) -> bool { + normalize_class_name(left) == normalize_class_name(right) +} + +/// Returns whether a class-like name is one of PHP's native enum marker interfaces. +pub(super) fn is_php_enum_marker_interface(name: &str) -> bool { + let name = name.trim_start_matches('\\'); + name.eq_ignore_ascii_case("UnitEnum") || name.eq_ignore_ascii_case("BackedEnum") +} + +/// Pushes a case-insensitive PHP method name once for ReflectionClass metadata. +pub(super) fn push_unique_method_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + let key = normalize_method_name(name); + if seen.insert(key) { + names.push(name.trim_start_matches('\\').to_string()); + } +} + +/// Pushes a case-sensitive PHP property name once for ReflectionClass metadata. +pub(super) fn push_unique_property_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + if seen.insert(name.to_string()) { + names.push(name.to_string()); + } +} + +/// Pushes a case-sensitive PHP class constant name once for ReflectionClass metadata. +pub(super) fn push_unique_constant_name(name: &str, names: &mut Vec, seen: &mut HashSet) { + if seen.insert(name.to_string()) { + names.push(name.to_string()); + } +} + +/// Normalizes PHP constant names for case-sensitive eval dynamic probes. +pub(super) fn normalize_constant_name(name: &str) -> String { + name.trim_start_matches('\\').to_string() +} diff --git a/crates/elephc-magician/src/context/reference_metadata.rs b/crates/elephc-magician/src/context/reference_metadata.rs new file mode 100644 index 0000000000..378eb748c5 --- /dev/null +++ b/crates/elephc-magician/src/context/reference_metadata.rs @@ -0,0 +1,93 @@ +//! Purpose: +//! Defines callable ABI aliases, execution-scope snapshots, and reference target shapes. +//! +//! Called from: +//! - Argument binding, reference writeback, object properties, and native invokers. +//! +//! Key details: +//! - Reference targets retain the exact caller-side storage and access scope needed for writeback. + +use super::*; + +/// Native descriptor-invoker ABI registered by generated code for AOT functions. +pub type NativeFunctionInvoker = + unsafe extern "C" fn(*mut c_void, *mut RuntimeCell) -> *mut RuntimeCell; + +/// Snapshot of eval execution stacks used to restore caller-sensitive access checks. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ElephcEvalExecutionScope { + pub(super) function_stack: Vec, + pub(super) class_stack: Vec, + pub(super) called_class_stack: Vec, +} + +/// PHP-visible magic-constant names for the current eval execution frame. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct EvalMagicScope { + pub(super) function_name: String, + pub(super) method_name: String, + pub(super) class_name: String, + pub(super) trait_name: String, +} + +/// Caller-side storage target that can remain linked to an eval object property. +#[derive(Clone)] +pub enum EvalReferenceTarget { + Variable { + scope: *mut ElephcEvalScope, + name: String, + }, + ArrayElement { + scope: *mut ElephcEvalScope, + array_name: String, + index: RuntimeCellHandle, + }, + NestedArrayElement { + array_target: Box, + index: RuntimeCellHandle, + }, + ObjectProperty { + object: RuntimeCellHandle, + property: String, + access_scope: ElephcEvalExecutionScope, + }, + StaticProperty { + class_name: String, + property: String, + access_scope: ElephcEvalExecutionScope, + }, + Cell { + cell: RuntimeCellHandle, + }, + InvokerSlot { + slot: usize, + source_tag: u64, + }, +} + +/// Normalized PHP array key used for eval-side reference metadata. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum EvalArrayReferenceKey { + Int(i64), + String(Vec), +} + +/// Late-static dispatch metadata attached to eval-created static callable arrays. +#[derive(Clone)] +pub(super) struct EvalStaticCallableMetadata { + pub(super) class_name: String, + pub(super) method: String, + pub(super) called_class: String, + pub(super) native_class: Option, + pub(super) bridge_scope: Option, +} + +/// Native instance-method dispatch metadata attached to eval-created method callables. +#[derive(Clone)] +pub(super) struct EvalObjectCallableMetadata { + pub(super) object: usize, + pub(super) method: String, + pub(super) called_class: String, + pub(super) native_class: String, + pub(super) bridge_scope: String, +} diff --git a/crates/elephc-magician/src/context/reflection_registry.rs b/crates/elephc-magician/src/context/reflection_registry.rs new file mode 100644 index 0000000000..34a4caf066 --- /dev/null +++ b/crates/elephc-magician/src/context/reflection_registry.rs @@ -0,0 +1,167 @@ +//! Purpose: +//! Registers synthetic Reflection owner identities and their eval metadata targets. +//! +//! Called from: +//! - Reflection object construction and reflected method dispatch. +//! +//! Key details: +//! - Class, function, method, property, constant, and closure targets use stable runtime identities. + +use super::*; + +impl ElephcEvalContext { + /// Records eval-declared attribute metadata for one synthetic ReflectionAttribute object. + pub fn register_eval_reflection_attribute( + &mut self, + identity: u64, + attribute: EvalAttribute, + target: u64, + repeated: bool, + ) { + self.eval_reflection_attributes.insert( + identity, + EvalReflectionAttributeMetadata::new(attribute, target, repeated), + ); + } + + /// Returns eval-declared attribute metadata attached to a synthetic ReflectionAttribute. + pub fn eval_reflection_attribute( + &self, + identity: u64, + ) -> Option<&EvalReflectionAttributeMetadata> { + self.eval_reflection_attributes.get(&identity) + } + + /// Records reflected class metadata for one synthetic ReflectionClass object. + pub fn register_eval_reflection_class(&mut self, identity: u64, class_name: &str) { + self.eval_reflection_classes + .insert(identity, class_name.trim_start_matches('\\').to_string()); + } + + /// Returns the reflected class name attached to a synthetic ReflectionClass. + pub fn eval_reflection_class_name(&self, identity: u64) -> Option<&str> { + self.eval_reflection_classes + .get(&identity) + .map(String::as_str) + } + + /// Records reflected function metadata for one synthetic ReflectionFunction object. + pub fn register_eval_reflection_function(&mut self, identity: u64, function_name: &str) { + self.eval_reflection_functions + .insert(identity, function_name.trim_start_matches('\\').to_string()); + } + + /// Returns the reflected function name attached to a synthetic ReflectionFunction. + pub fn eval_reflection_function_name(&self, identity: u64) -> Option<&str> { + self.eval_reflection_functions + .get(&identity) + .map(String::as_str) + } + + /// Records the callable target behind a `Closure` reflected as a function. + pub fn register_eval_reflection_function_closure_target( + &mut self, + identity: u64, + target: EvalClosureObjectTarget, + ) { + self.eval_reflection_function_closure_targets + .insert(identity, target); + } + + /// Returns the callable target retained for a reflected `Closure` object. + pub fn eval_reflection_function_closure_target( + &self, + identity: u64, + ) -> Option<&EvalClosureObjectTarget> { + self.eval_reflection_function_closure_targets + .get(&identity) + } + + /// Records reflected method metadata for one synthetic ReflectionMethod object. + pub fn register_eval_reflection_method( + &mut self, + identity: u64, + declaring_class: &str, + method_name: &str, + ) { + self.eval_reflection_methods.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + method_name.to_string(), + ), + ); + } + + /// Returns the declaring class and method name attached to a synthetic ReflectionMethod. + pub fn eval_reflection_method(&self, identity: u64) -> Option<(&str, &str)> { + self.eval_reflection_methods + .get(&identity) + .map(|(class, method)| (class.as_str(), method.as_str())) + } + + /// Records reflected property metadata for one synthetic ReflectionProperty object. + pub fn register_eval_reflection_property( + &mut self, + identity: u64, + declaring_class: &str, + property_name: &str, + ) { + self.eval_reflection_properties.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + property_name.to_string(), + ), + ); + self.eval_dynamic_reflection_properties.remove(&identity); + } + + /// Records reflected dynamic-property metadata for one synthetic ReflectionProperty object. + pub fn register_eval_dynamic_reflection_property( + &mut self, + identity: u64, + declaring_class: &str, + property_name: &str, + ) { + self.register_eval_reflection_property(identity, declaring_class, property_name); + self.eval_dynamic_reflection_properties.insert(identity); + } + + /// Returns the declaring class and property name attached to a synthetic ReflectionProperty. + pub fn eval_reflection_property(&self, identity: u64) -> Option<(&str, &str)> { + self.eval_reflection_properties + .get(&identity) + .map(|(class, property)| (class.as_str(), property.as_str())) + } + + /// Returns whether a synthetic ReflectionProperty represents a dynamic property. + pub fn eval_reflection_property_is_dynamic(&self, identity: u64) -> bool { + self.eval_dynamic_reflection_properties.contains(&identity) + } + + /// Records reflected class constant or enum case metadata for one synthetic object. + pub fn register_eval_reflection_class_constant( + &mut self, + identity: u64, + declaring_class: &str, + constant_name: &str, + owner_kind: u64, + ) { + self.eval_reflection_class_constants.insert( + identity, + ( + declaring_class.trim_start_matches('\\').to_string(), + constant_name.to_string(), + owner_kind, + ), + ); + } + + /// Returns the declaring class, name, and reflection owner kind for a synthetic constant. + pub fn eval_reflection_class_constant(&self, identity: u64) -> Option<(&str, &str, u64)> { + self.eval_reflection_class_constants + .get(&identity) + .map(|(class, constant, owner_kind)| (class.as_str(), constant.as_str(), *owner_kind)) + } +} diff --git a/crates/elephc-magician/src/context/runtime_state.rs b/crates/elephc-magician/src/context/runtime_state.rs new file mode 100644 index 0000000000..10f4d62111 --- /dev/null +++ b/crates/elephc-magician/src/context/runtime_state.rs @@ -0,0 +1,425 @@ +//! Purpose: +//! Manages mutable eval runtime state, execution scopes, resources, and call-site metadata. +//! +//! Called from: +//! - Interpreter execution and builtins requiring process-level eval state. +//! +//! Key details: +//! - Static cells, include state, scope stacks, errors, timezone, HTTP status, and magic paths live here. + +use super::*; + +impl ElephcEvalContext { + /// Returns true when the context has a dynamic or native function with this lowercase PHP name. + pub fn has_function(&self, name: &str) -> bool { + self.functions.contains_key(name) || self.native_functions.contains_key(name) + } + + /// Returns true when the context has a closure registered under this synthetic name. + pub fn has_closure(&self, name: &str) -> bool { + self.closures.contains_key(name) + } + + /// Returns a stored static local cell for an eval-declared function. + pub fn static_local(&self, function_name: &str, name: &str) -> Option { + self.static_locals + .get(&(function_name.to_string(), name.to_string())) + .copied() + } + + /// Stores one static local cell and returns any replaced distinct cell. + pub fn set_static_local( + &mut self, + function_name: impl Into, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .static_locals + .insert((function_name.into(), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + + /// Returns a stored static property cell for an eval-declared class. + pub fn static_property(&self, class_name: &str, name: &str) -> Option { + self.static_properties + .get(&(normalize_class_name(class_name), name.to_string())) + .copied() + } + + /// Stores one eval static property cell and returns any replaced distinct cell. + pub fn set_static_property( + &mut self, + class_name: &str, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .static_properties + .insert((normalize_class_name(class_name), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + + /// Binds one eval static property slot to a persistent PHP reference target. + pub fn bind_static_property_alias( + &mut self, + class_name: &str, + name: &str, + target: EvalReferenceTarget, + ) -> Option { + self.static_property_aliases + .insert((normalize_class_name(class_name), name.to_string()), target) + } + + /// Returns the persistent reference target bound to one eval static property slot. + pub fn static_property_alias( + &self, + class_name: &str, + name: &str, + ) -> Option<&EvalReferenceTarget> { + self.static_property_aliases + .get(&(normalize_class_name(class_name), name.to_string())) + } + + /// Returns a materialized eval class constant cell. + pub fn class_constant_cell(&self, class_name: &str, name: &str) -> Option { + self.class_constants + .get(&(normalize_class_name(class_name), name.to_string())) + .copied() + } + + /// Stores one eval class constant cell and returns any replaced distinct cell. + pub fn set_class_constant_cell( + &mut self, + class_name: &str, + name: impl Into, + cell: RuntimeCellHandle, + ) -> Option { + let previous = self + .class_constants + .insert((normalize_class_name(class_name), name.into()), cell); + previous.filter(|previous| *previous != cell) + } + + /// Returns true when an eval include key was already loaded by this context. + pub fn has_included_file(&self, path: &str) -> bool { + self.included_files.contains(path) + } + + /// Records one successfully loaded eval include key for include_once/require_once. + pub fn mark_included_file(&mut self, path: impl Into) { + self.included_files.insert(path.into()); + } + + /// Stores the non-owned global scope handle used by eval `global` aliases. + pub fn set_global_scope(&mut self, scope: *mut ElephcEvalScope) -> bool { + if scope.is_null() { + self.global_scope = None; + false + } else { + self.global_scope = Some(scope); + true + } + } + + /// Returns the non-owned global scope handle for eval `global` aliases. + pub fn global_scope_ptr(&self) -> Option<*mut ElephcEvalScope> { + self.global_scope + } + + /// Pushes an eval-executed function name for magic-constant resolution. + pub fn push_function(&mut self, name: impl Into) { + self.function_stack.push(name.into()); + } + + /// Pops the current eval-executed function name after its body completes. + pub fn pop_function(&mut self) { + self.function_stack.pop(); + } + + /// Returns the current eval-executed function name, if execution is inside one. + pub fn current_function(&self) -> Option<&str> { + self.function_stack.last().map(String::as_str) + } + + /// Pushes the eval class whose method is currently executing. + pub fn push_class_scope(&mut self, name: impl Into) { + self.class_stack.push(name.into()); + } + + /// Pops the current eval class method scope. + pub fn pop_class_scope(&mut self) { + self.class_stack.pop(); + } + + /// Returns the current eval class scope, if execution is inside a method. + pub fn current_class_scope(&self) -> Option<&str> { + self.class_stack.last().map(String::as_str) + } + + /// Pushes the class name used to dispatch the current eval method call. + pub fn push_called_class_scope(&mut self, name: impl Into) { + self.called_class_stack.push(name.into()); + } + + /// Pops the current late-static-bound eval class scope. + pub fn pop_called_class_scope(&mut self) { + self.called_class_stack.pop(); + } + + /// Returns the current late-static-bound eval class scope, if execution is inside a method. + pub fn current_called_class_scope(&self) -> Option<&str> { + self.called_class_stack.last().map(String::as_str) + } + + /// Returns a dynamic called-class override for a generated/AOT frame entering eval. + pub fn native_frame_called_class_override( + &self, + class_name: &str, + called_class_name: &str, + ) -> Option { + let class_name = class_name.trim_start_matches('\\'); + let called_class_name = called_class_name.trim_start_matches('\\'); + if class_name.is_empty() || !called_class_name.eq_ignore_ascii_case(class_name) { + return None; + } + if let Some(called_class) = + native_frame_called_class_override(class_name, called_class_name) + { + return Some(called_class); + } + let active = self.current_called_class_scope()?.trim_start_matches('\\'); + if active.is_empty() || active.eq_ignore_ascii_case(class_name) { + return None; + } + let active = self + .resolve_class_name(active) + .unwrap_or_else(|| active.to_string()); + self.class_parent_names(&active) + .iter() + .any(|parent| parent.eq_ignore_ascii_case(class_name)) + .then_some(active) + } + + /// Pushes PHP-visible method magic constants for the current eval method frame. + pub fn push_method_magic_scope(&mut self, class_name: &str, method: &EvalClassMethod) { + self.magic_stack.push(EvalMagicScope { + function_name: method.magic_function_name().to_string(), + method_name: method.magic_method_name(class_name), + class_name: class_name.trim_start_matches('\\').to_string(), + trait_name: method + .trait_origin() + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + + /// Pushes PHP-visible class-like member magic constants for default expressions. + pub fn push_class_like_member_magic_scope( + &mut self, + class_name: &str, + trait_name: Option<&str>, + ) { + self.magic_stack.push(EvalMagicScope { + function_name: String::new(), + method_name: String::new(), + class_name: class_name.trim_start_matches('\\').to_string(), + trait_name: trait_name + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + + /// Pushes PHP-visible callable magic constants for reflected parameter defaults. + pub fn push_callable_magic_scope( + &mut self, + function_name: &str, + method_name: &str, + class_name: Option<&str>, + trait_name: Option<&str>, + ) { + self.magic_stack.push(EvalMagicScope { + function_name: function_name.to_string(), + method_name: method_name.to_string(), + class_name: class_name + .map(|class_name| class_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + trait_name: trait_name + .map(|trait_name| trait_name.trim_start_matches('\\').to_string()) + .unwrap_or_default(), + }); + } + + /// Pops the current PHP-visible eval magic-constant scope. + pub fn pop_magic_scope(&mut self) { + self.magic_stack.pop(); + } + + /// Returns the PHP `__FUNCTION__` value for the current eval frame. + pub fn current_magic_function(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.function_name.as_str()) + } + + /// Returns the PHP `__METHOD__` value for the current eval method frame. + pub fn current_magic_method(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.method_name.as_str()) + } + + /// Returns the PHP `__CLASS__` value for the current eval method frame. + pub fn current_magic_class(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.class_name.as_str()) + } + + /// Returns the PHP `__TRAIT__` value for the current eval method frame. + pub fn current_magic_trait(&self) -> Option<&str> { + self.magic_stack + .last() + .map(|scope| scope.trait_name.as_str()) + } + + /// Captures the current eval execution stacks for later caller-context-sensitive work. + pub fn execution_scope(&self) -> ElephcEvalExecutionScope { + ElephcEvalExecutionScope { + function_stack: self.function_stack.clone(), + class_stack: self.class_stack.clone(), + called_class_stack: self.called_class_stack.clone(), + } + } + + /// Replaces eval execution stacks and returns the previous stacks for restoration. + pub fn replace_execution_scope( + &mut self, + scope: ElephcEvalExecutionScope, + ) -> ElephcEvalExecutionScope { + ElephcEvalExecutionScope { + function_stack: std::mem::replace(&mut self.function_stack, scope.function_stack), + class_stack: std::mem::replace(&mut self.class_stack, scope.class_stack), + called_class_stack: std::mem::replace( + &mut self.called_class_stack, + scope.called_class_stack, + ), + } + } + + /// Records a Throwable cell that escaped from an eval-executed function call. + pub fn set_pending_throw(&mut self, value: RuntimeCellHandle) { + self.pending_throw = Some(value); + } + + /// Returns and clears the Throwable cell currently escaping through eval. + pub fn take_pending_throw(&mut self) -> Option { + self.pending_throw.take() + } + + /// Returns the eval-local SPL autoload extension list. + pub fn spl_autoload_extensions(&self) -> &str { + &self.spl_autoload_extensions + } + + /// Replaces the eval-local SPL autoload extension list. + pub fn set_spl_autoload_extensions(&mut self, extensions: impl Into) { + self.spl_autoload_extensions = extensions.into(); + } + + /// Returns the eval-local stream resource table. + pub(crate) fn stream_resources(&self) -> &EvalStreamResources { + &self.streams + } + + /// Returns mutable access to the eval-local stream resource table. + pub(crate) fn stream_resources_mut(&mut self) -> &mut EvalStreamResources { + &mut self.streams + } + + /// Clears the eval-local JSON error state after a successful JSON operation. + pub fn clear_json_error(&mut self) { + self.json_last_error = 0; + self.json_last_error_msg.clear(); + self.json_last_error_msg.push_str("No error"); + } + + /// Records the eval-local JSON error state for `json_last_error*()` calls. + pub fn set_json_error(&mut self, code: i64, message: impl Into) { + self.json_last_error = code; + self.json_last_error_msg = message.into(); + } + + /// Returns the PHP `JSON_ERROR_*` code for the last eval JSON operation. + pub const fn json_last_error(&self) -> i64 { + self.json_last_error + } + + /// Returns the PHP message for the last eval JSON operation. + pub fn json_last_error_msg(&self) -> &str { + &self.json_last_error_msg + } + + /// Returns the eval-local PHP default timezone identifier. + pub fn default_timezone(&self) -> &str { + &self.default_timezone + } + + /// Replaces the eval-local PHP default timezone identifier. + pub fn set_default_timezone(&mut self, timezone: impl Into) { + self.default_timezone = timezone.into(); + } + + /// Returns the eval-local HTTP response code used by web-facing builtins. + pub const fn http_response_code(&self) -> i64 { + self.http_response_code + } + + /// Applies a new eval-local HTTP response code and returns the previous one. + pub fn replace_http_response_code(&mut self, response_code: i64) -> i64 { + let previous = self.http_response_code; + if response_code > 0 { + self.http_response_code = response_code; + } + previous + } + + /// Updates the source file, directory, and line for the current eval call site. + pub fn set_call_site(&mut self, file: impl Into, dir: impl Into, line: i64) { + self.call_file = file.into(); + self.call_dir = dir.into(); + self.call_line = line; + self.file_magic_override = None; + } + + /// Returns a copy of the current call-site metadata for temporary overrides. + pub fn call_site(&self) -> (String, String, i64, Option) { + ( + self.call_file.clone(), + self.call_dir.clone(), + self.call_line, + self.file_magic_override.clone(), + ) + } + + /// Overrides `__FILE__` while executing an actual file through eval include. + pub fn set_file_magic_override(&mut self, file: Option) { + self.file_magic_override = file; + } + + /// Returns the source directory associated with the current eval call site. + pub fn call_dir(&self) -> &str { + &self.call_dir + } + + /// Returns PHP's `__FILE__` string for code currently running inside eval. + pub fn eval_file_magic(&self) -> String { + if let Some(file) = &self.file_magic_override { + return file.clone(); + } + if self.call_file.is_empty() { + return String::new(); + } + format!("{}({}) : eval()'d code", self.call_file, self.call_line) + } +} diff --git a/crates/elephc-magician/src/eval_ir.rs b/crates/elephc-magician/src/eval_ir.rs index 692da5c685..d88dd49c29 100644 --- a/crates/elephc-magician/src/eval_ir.rs +++ b/crates/elephc-magician/src/eval_ir.rs @@ -1,2863 +1,35 @@ //! Purpose: -//! Defines the dynamic EvalIR used by runtime `eval()` fragments. -//! EvalIR models by-name variable operations and expression shape without -//! introducing an independent runtime value representation. +//! Defines and re-exports the dynamic EvalIR used by runtime `eval()` fragments. +//! Node families live in focused child modules without changing the public model. //! //! Called from: //! - `crate::parser::parse_fragment()` -//! - Future `crate::interpreter` execution. +//! - `crate::interpreter` execution. //! //! Key details: //! - Runtime execution must turn constants into elephc runtime cells through //! value-bridge hooks; EvalIR constants are syntax data, not owned PHP values. -/// Parsed eval fragment lowered into dynamic by-name statements. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalProgram { - source_len: usize, - statements: Vec, -} - -impl EvalProgram { - /// Creates an EvalIR program for a source fragment and statement list. - pub fn new(source_len: usize, statements: Vec) -> Self { - Self { - source_len, - statements, - } - } - - /// Returns the byte length of the parsed eval fragment. - pub const fn source_len(&self) -> usize { - self.source_len - } - - /// Returns the ordered EvalIR statements in source order. - pub fn statements(&self) -> &[EvalStmt] { - &self.statements - } - - /// Consumes the program and returns its statement list. - pub fn into_statements(self) -> Vec { - self.statements - } -} - -/// One source range inside the current eval fragment. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EvalSourceLocation { - start_line: i64, - end_line: i64, -} - -impl EvalSourceLocation { - /// Creates a source range using one-based eval-fragment line numbers. - pub const fn new(start_line: i64, end_line: i64) -> Self { - Self { - start_line, - end_line, - } - } - - /// Creates a single-line source range. - pub const fn single_line(line: i64) -> Self { - Self::new(line, line) - } - - /// Returns the one-based line where the declaration starts. - pub const fn start_line(&self) -> i64 { - self.start_line - } - - /// Returns the one-based line where the declaration ends. - pub const fn end_line(&self) -> i64 { - self.end_line - } -} - -/// Dynamic eval statements that operate on a materialized activation scope. -#[derive(Debug, Clone, PartialEq)] -pub enum EvalStmt { - ArrayAppendVar { - name: String, - value: EvalExpr, - }, - ArraySetVar { - name: String, - index: EvalExpr, - value: EvalExpr, - }, - Break, - Continue, - DoWhile { - body: Vec, - condition: EvalExpr, - }, - Echo(EvalExpr), - For { - init: Vec, - condition: Option, - update: Vec, - body: Vec, - }, - ClassDecl(EvalClass), - EnumDecl(EvalEnum), - InterfaceDecl(EvalInterface), - TraitDecl(EvalTrait), - Foreach { - array: EvalExpr, - key_name: Option, - value_name: String, - body: Vec, - }, - FunctionDecl { - name: String, - source_location: Option, - attributes: Vec, - params: Vec, - parameter_attributes: Vec>, - parameter_types: Vec>, - parameter_defaults: Vec>, - parameter_is_by_ref: Vec, - parameter_is_variadic: Vec, - return_type: Option, - body: Vec, - }, - Global { - vars: Vec, - }, - If { - condition: EvalExpr, - then_branch: Vec, - else_branch: Vec, - }, - Return(Option), - ReferenceAssign { - target: String, - source: String, - }, - PropertyReferenceBind { - object: EvalExpr, - property: String, - source: String, - }, - DynamicPropertyReferenceBind { - object: EvalExpr, - property: EvalExpr, - source: String, - }, - DynamicPropertySet { - object: EvalExpr, - property: EvalExpr, - value: EvalExpr, - }, - DynamicPropertyArrayAppend { - object: EvalExpr, - property: EvalExpr, - value: EvalExpr, - }, - DynamicPropertyArraySet { - object: EvalExpr, - property: EvalExpr, - index: EvalExpr, - op: Option, - value: EvalExpr, - }, - DynamicPropertyCompoundAssign { - object: EvalExpr, - property: EvalExpr, - op: EvalBinOp, - value: EvalExpr, - }, - DynamicPropertyIncDec { - object: EvalExpr, - property: EvalExpr, - increment: bool, - }, - PropertySet { - object: EvalExpr, - property: String, - value: EvalExpr, - }, - PropertyArrayAppend { - object: EvalExpr, - property: String, - value: EvalExpr, - }, - PropertyArraySet { - object: EvalExpr, - property: String, - index: EvalExpr, - op: Option, - value: EvalExpr, - }, - PropertyCompoundAssign { - object: EvalExpr, - property: String, - op: EvalBinOp, - value: EvalExpr, - }, - PropertyIncDec { - object: EvalExpr, - property: String, - increment: bool, - }, - StaticPropertySet { - class_name: String, - property: String, - value: EvalExpr, - }, - StaticPropertyReferenceBind { - class_name: String, - property: String, - source: String, - }, - StaticPropertyArrayAppend { - class_name: String, - property: String, - value: EvalExpr, - }, - StaticPropertyArraySet { - class_name: String, - property: String, - index: EvalExpr, - op: Option, - value: EvalExpr, - }, - StaticPropertyIncDec { - class_name: String, - property: String, - increment: bool, - }, - DynamicStaticPropertySet { - class_name: EvalExpr, - property: String, - value: EvalExpr, - }, - DynamicStaticPropertyReferenceBind { - class_name: EvalExpr, - property: String, - source: String, - }, - DynamicStaticPropertyArrayAppend { - class_name: EvalExpr, - property: String, - value: EvalExpr, - }, - DynamicStaticPropertyArraySet { - class_name: EvalExpr, - property: String, - index: EvalExpr, - op: Option, - value: EvalExpr, - }, - DynamicStaticPropertyIncDec { - class_name: EvalExpr, - property: String, - increment: bool, - }, - DynamicStaticPropertyNameSet { - class_name: EvalExpr, - property: EvalExpr, - value: EvalExpr, - }, - DynamicStaticPropertyNameReferenceBind { - class_name: EvalExpr, - property: EvalExpr, - source: String, - }, - DynamicStaticPropertyNameArrayAppend { - class_name: EvalExpr, - property: EvalExpr, - value: EvalExpr, - }, - DynamicStaticPropertyNameArraySet { - class_name: EvalExpr, - property: EvalExpr, - index: EvalExpr, - op: Option, - value: EvalExpr, - }, - DynamicStaticPropertyNameIncDec { - class_name: EvalExpr, - property: EvalExpr, - increment: bool, - }, - StaticVar { - name: String, - init: EvalExpr, - }, - StoreVar { - name: String, - value: EvalExpr, - }, - Switch { - expr: EvalExpr, - cases: Vec, - }, - Throw(EvalExpr), - Try { - body: Vec, - catches: Vec, - finally_body: Vec, - }, - UnsetArrayElement { - array: EvalExpr, - index: EvalExpr, - }, - UnsetProperty { - object: EvalExpr, - property: String, - }, - UnsetDynamicProperty { - object: EvalExpr, - property: EvalExpr, - }, - UnsetStaticProperty { - class_name: String, - property: String, - }, - UnsetDynamicStaticProperty { - class_name: EvalExpr, - property: String, - }, - UnsetDynamicStaticPropertyName { - class_name: EvalExpr, - property: EvalExpr, - }, - UnsetVar { - name: String, - }, - While { - condition: EvalExpr, - body: Vec, - }, - Expr(EvalExpr), -} - -/// One `catch` block attached to an eval `try` statement. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalCatch { - pub class_names: Vec, - pub var_name: Option, - pub body: Vec, -} - -/// One lexical variable captured by a runtime eval closure literal. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalClosureCapture { - name: String, - by_ref: bool, -} - -impl EvalClosureCapture { - /// Creates one closure capture with its source variable name and reference mode. - pub fn new(name: impl Into, by_ref: bool) -> Self { - Self { - name: name.into(), - by_ref, - } - } - - /// Returns the captured variable name without the leading `$`. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns whether this capture was declared as `use (&$name)`. - pub const fn by_ref(&self) -> bool { - self.by_ref - } -} - -/// Runtime user function declared by an eval fragment. -#[derive(Debug, Clone)] -pub struct EvalFunction { - name: String, - source_location: Option, - attributes: Vec, - params: Vec, - parameter_attributes: Vec>, - parameter_types: Vec>, - parameter_defaults: Vec>, - parameter_is_by_ref: Vec, - parameter_is_variadic: Vec, - return_type: Option, - body: Vec, -} - -impl PartialEq for EvalFunction { - /// Compares function metadata while ignoring retained source-location decoration. - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.attributes == other.attributes - && self.params == other.params - && self.parameter_attributes == other.parameter_attributes - && self.parameter_types == other.parameter_types - && self.parameter_defaults == other.parameter_defaults - && self.parameter_is_by_ref == other.parameter_is_by_ref - && self.parameter_is_variadic == other.parameter_is_variadic - && self.return_type == other.return_type - && self.body == other.body - } -} - -impl EvalFunction { - /// Creates a dynamic eval function with source-order parameters and body. - pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { - let parameter_attributes = vec![Vec::new(); params.len()]; - let parameter_types = vec![None; params.len()]; - let parameter_defaults = vec![None; params.len()]; - let parameter_is_by_ref = vec![false; params.len()]; - let parameter_is_variadic = vec![false; params.len()]; - Self { - name: name.into(), - source_location: None, - attributes: Vec::new(), - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - return_type: None, - body, - } - } - - /// Returns a copy of this function with source-location metadata attached. - pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { - self.source_location = Some(source_location); - self - } - - /// Returns a copy of this function with declaration attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns a copy of this function with source-order parameter attributes. - pub fn with_parameter_attributes( - mut self, - parameter_attributes: Vec>, - ) -> Self { - self.parameter_attributes = parameter_attributes; - self - } - - /// Returns a copy of this function with source-order parameter type metadata. - pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { - self.parameter_types = parameter_types; - self - } - - /// Returns a copy of this function with source-order default expressions. - pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { - self.parameter_defaults = parameter_defaults; - self - } - - /// Returns a copy of this function with source-order by-reference flags. - pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { - self.parameter_is_by_ref = parameter_is_by_ref; - self - } - - /// Returns a copy of this function with source-order variadic flags. - pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { - self.parameter_is_variadic = parameter_is_variadic; - self - } - - /// Returns a copy of this function with retained return type metadata. - pub fn with_return_type(mut self, return_type: Option) -> Self { - self.return_type = return_type; - self - } - - /// Returns the original source spelling of this eval-declared function name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns eval-fragment source-location metadata, when retained. - pub const fn source_location(&self) -> Option { - self.source_location - } - - /// Returns attributes declared directly on this eval function. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns source-order parameter names without leading `$`. - pub fn params(&self) -> &[String] { - &self.params - } - - /// Returns source-order parameter attributes. - pub fn parameter_attributes(&self) -> &[Vec] { - &self.parameter_attributes - } - - /// Returns source-order parameter type metadata. - pub fn parameter_types(&self) -> &[Option] { - &self.parameter_types - } - - /// Returns default expressions declared for each source-order parameter. - pub fn parameter_defaults(&self) -> &[Option] { - &self.parameter_defaults - } - - /// Returns source-order flags for whether each parameter was declared by reference. - pub fn parameter_is_by_ref(&self) -> &[bool] { - &self.parameter_is_by_ref - } - - /// Returns source-order flags for whether each parameter was declared variadic. - pub fn parameter_is_variadic(&self) -> &[bool] { - &self.parameter_is_variadic - } - - /// Returns retained return type metadata, if the function declared one. - pub const fn return_type(&self) -> Option<&EvalParameterType> { - self.return_type.as_ref() - } - - /// Returns the dynamic EvalIR statements that form the function body. - pub fn body(&self) -> &[EvalStmt] { - &self.body - } -} - -/// One supported eval method parameter type atom. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum EvalParameterTypeVariant { - Array, - Bool, - Callable, - Class(String), - Float, - Int, - Iterable, - Mixed, - Never, - Object, - String, - Void, -} - -/// How multiple eval parameter type atoms combine. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EvalParameterTypeKind { - Union, - Intersection, -} - -/// Type metadata retained for one eval method parameter. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EvalParameterType { - variants: Vec, - allows_null: bool, - kind: EvalParameterTypeKind, -} - -impl EvalParameterType { - /// Creates one eval method parameter type from union variants and nullability. - pub fn new(variants: Vec, allows_null: bool) -> Self { - Self { - variants, - allows_null, - kind: EvalParameterTypeKind::Union, - } - } - - /// Creates one eval method parameter type from intersection variants. - pub fn intersection(variants: Vec) -> Self { - Self { - variants, - allows_null: false, - kind: EvalParameterTypeKind::Intersection, - } - } - - /// Returns the non-null type atoms in source order. - pub fn variants(&self) -> &[EvalParameterTypeVariant] { - &self.variants - } - - /// Returns whether the type explicitly accepts PHP null. - pub const fn allows_null(&self) -> bool { - self.allows_null - } - - /// Returns whether all variants must match the value. - pub const fn is_intersection(&self) -> bool { - matches!(self.kind, EvalParameterTypeKind::Intersection) - } -} - -/// Literal attribute argument metadata retained by eval declarations. -#[derive(Debug, Clone, PartialEq)] -pub enum EvalAttributeArg { - String(String), - Int(i64), - Float(u64), - Bool(bool), - Null, - Array(Vec), - Named { - name: String, - value: Box, - }, - IntKeyed { - key: i64, - value: Box, - }, -} - -impl EvalAttributeArg { - /// Returns the PHP named-argument key when this attribute arg is named. - pub fn name(&self) -> Option<&str> { - match self { - EvalAttributeArg::Named { name, .. } => Some(name), - _ => None, - } - } - - /// Returns the PHP integer array key when this attribute arg is int-keyed. - pub fn int_key(&self) -> Option { - match self { - EvalAttributeArg::IntKeyed { key, .. } => Some(*key), - _ => None, - } - } - - /// Returns the scalar payload, unwrapping a named or int-keyed wrapper. - pub fn value(&self) -> &EvalAttributeArg { - match self { - EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { - value - } - _ => self, - } - } -} - -/// Attribute metadata retained for eval class-like declarations. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalAttribute { - name: String, - args: Option>, -} - -impl EvalAttribute { - /// Creates one eval attribute metadata entry. - pub fn new(name: impl Into, args: Option>) -> Self { - Self { - name: name.into(), - args, - } - } - - /// Returns the resolved PHP-visible attribute class name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns supported literal positional args, or `None` for unsupported metadata. - pub fn args(&self) -> Option<&[EvalAttributeArg]> { - self.args.as_deref() - } -} - -/// Runtime enum declared by an eval fragment. -#[derive(Debug, Clone)] -pub struct EvalEnum { - name: String, - source_location: Option, - backing_type: Option, - interfaces: Vec, - attributes: Vec, - traits: Vec, - trait_adaptations: Vec, - cases: Vec, - constants: Vec, - methods: Vec, -} - -impl PartialEq for EvalEnum { - /// Compares enum metadata while ignoring retained source-location decoration. - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.backing_type == other.backing_type - && self.interfaces == other.interfaces - && self.attributes == other.attributes - && self.traits == other.traits - && self.trait_adaptations == other.trait_adaptations - && self.cases == other.cases - && self.constants == other.constants - && self.methods == other.methods - } -} - -impl EvalEnum { - /// Creates a dynamic eval enum with cases and optional backing type. - pub fn new( - name: impl Into, - backing_type: Option, - cases: Vec, - ) -> Self { - Self::with_members( - name, - backing_type, - Vec::new(), - cases, - Vec::new(), - Vec::new(), - ) - } - - /// Creates a dynamic eval enum with interfaces, cases, constants, and methods. - pub fn with_members( - name: impl Into, - backing_type: Option, - interfaces: Vec, - cases: Vec, - constants: Vec, - methods: Vec, - ) -> Self { - Self::with_members_traits_adaptations( - name, - backing_type, - interfaces, - cases, - constants, - methods, - Vec::new(), - Vec::new(), - ) - } - - /// Creates a dynamic eval enum with traits, adaptations, cases, constants, and methods. - pub fn with_members_traits_adaptations( - name: impl Into, - backing_type: Option, - interfaces: Vec, - cases: Vec, - constants: Vec, - methods: Vec, - traits: Vec, - trait_adaptations: Vec, - ) -> Self { - Self { - name: name.into(), - source_location: None, - backing_type, - interfaces, - attributes: Vec::new(), - traits, - trait_adaptations, - cases, - constants, - methods, - } - } - - /// Returns a copy of this enum with source-location metadata attached. - pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { - self.source_location = Some(source_location); - self - } - - /// Returns a copy of this enum with class-like attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns the original source spelling of this eval-declared enum name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns eval-fragment source-location metadata, when retained. - pub const fn source_location(&self) -> Option { - self.source_location - } - - /// Returns the optional scalar backing type for this enum. - pub const fn backing_type(&self) -> Option { - self.backing_type - } - - /// Returns interface names implemented directly by this eval enum. - pub fn interfaces(&self) -> &[String] { - &self.interfaces - } - - /// Returns attributes declared directly on this eval enum. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns trait names used directly by this eval enum. - pub fn traits(&self) -> &[String] { - &self.traits - } - - /// Returns trait adaptations declared directly by this eval enum. - pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { - &self.trait_adaptations - } - - /// Returns cases declared directly by this eval enum. - pub fn cases(&self) -> &[EvalEnumCase] { - &self.cases - } - - /// Returns one enum case by PHP case-sensitive case name. - pub fn case(&self, name: &str) -> Option<&EvalEnumCase> { - self.cases().iter().find(|case| case.name() == name) - } - - /// Returns constants declared directly by this eval enum. - pub fn constants(&self) -> &[EvalClassConstant] { - &self.constants - } - - /// Returns methods declared directly by this eval enum. - pub fn methods(&self) -> &[EvalClassMethod] { - &self.methods - } - - /// Builds class-shaped metadata used for enum method and relation dispatch. - pub fn as_class_metadata(&self) -> EvalClass { - EvalClass::with_modifiers_traits_adaptations_and_constants( - self.name.clone(), - false, - true, - None, - self.interfaces.clone(), - self.traits.clone(), - self.trait_adaptations.clone(), - self.constants.clone(), - Vec::new(), - self.methods.clone(), - ) - .with_attributes(self.attributes.clone()) - .with_source_location_option(self.source_location) - } -} - -/// Scalar backing type for a runtime eval enum. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EvalEnumBackingType { - Int, - String, -} - -/// One case declared by a runtime eval enum. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalEnumCase { - name: String, - attributes: Vec, - value: Option, -} - -impl EvalEnumCase { - /// Creates an eval enum case with an optional backing value expression. - pub fn new(name: impl Into, value: Option) -> Self { - Self { - name: name.into(), - attributes: Vec::new(), - value, - } - } - - /// Returns a copy of this enum case with declaration attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns the PHP-visible enum case name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns attributes declared directly on this enum case. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns the optional backing value expression. - pub fn value(&self) -> Option<&EvalExpr> { - self.value.as_ref() - } -} - -/// Runtime interface declared by an eval fragment. -#[derive(Debug, Clone)] -pub struct EvalInterface { - name: String, - source_location: Option, - parents: Vec, - attributes: Vec, - constants: Vec, - properties: Vec, - methods: Vec, -} - -impl PartialEq for EvalInterface { - /// Compares interface metadata while ignoring retained source-location decoration. - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.parents == other.parents - && self.attributes == other.attributes - && self.constants == other.constants - && self.properties == other.properties - && self.methods == other.methods - } -} - -impl EvalInterface { - /// Creates a dynamic eval interface with optional parent interfaces and methods. - pub fn new( - name: impl Into, - parents: Vec, - methods: Vec, - ) -> Self { - Self::with_constants(name, parents, Vec::new(), methods) - } - - /// Creates a dynamic eval interface with optional parent interfaces, constants, and methods. - pub fn with_constants( - name: impl Into, - parents: Vec, - constants: Vec, - methods: Vec, - ) -> Self { - Self::with_constants_and_properties(name, parents, constants, Vec::new(), methods) - } - - /// Creates a dynamic eval interface with constants, property contracts, and methods. - pub fn with_constants_and_properties( - name: impl Into, - parents: Vec, - constants: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self { - name: name.into(), - source_location: None, - parents, - attributes: Vec::new(), - constants, - properties, - methods, - } - } - - /// Returns a copy of this interface with source-location metadata attached. - pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { - self.source_location = Some(source_location); - self - } - - /// Returns a copy of this interface with class-like attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns the original source spelling of this eval-declared interface name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns eval-fragment source-location metadata, when retained. - pub const fn source_location(&self) -> Option { - self.source_location - } - - /// Returns interface names extended directly by this eval interface. - pub fn parents(&self) -> &[String] { - &self.parents - } - - /// Returns attributes declared directly on this eval interface. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns constants declared directly by this eval interface. - pub fn constants(&self) -> &[EvalClassConstant] { - &self.constants - } - - /// Returns one interface constant by PHP case-sensitive constant name. - pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { - self.constants() - .iter() - .find(|constant| constant.name() == name) - } - - /// Returns property hook contracts declared directly by this eval interface. - pub fn properties(&self) -> &[EvalInterfaceProperty] { - &self.properties - } - - /// Returns method signatures declared directly by this eval interface. - pub fn methods(&self) -> &[EvalInterfaceMethod] { - &self.methods - } -} - -/// Property hook contract metadata for a runtime eval interface. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalInterfaceProperty { - name: String, - attributes: Vec, - property_type: Option, - set_visibility: Option, - requires_get: bool, - requires_set: bool, -} - -impl EvalInterfaceProperty { - /// Creates one eval interface property contract. - pub fn new(name: impl Into, requires_get: bool, requires_set: bool) -> Self { - Self { - name: name.into(), - attributes: Vec::new(), - property_type: None, - set_visibility: None, - requires_get, - requires_set, - } - } - - /// Returns a copy of this interface property with retained type metadata. - pub fn with_type(mut self, property_type: Option) -> Self { - self.property_type = property_type; - self - } - - /// Returns a copy of this interface property with PHP asymmetric write visibility metadata. - pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { - self.set_visibility = set_visibility; - self - } - - /// Returns a copy of this interface property with declaration attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns the PHP-visible property name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns attributes declared directly on this interface property. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns retained PHP type metadata for this interface property contract. - pub fn property_type(&self) -> Option<&EvalParameterType> { - self.property_type.as_ref() - } - - /// Returns the PHP asymmetric write visibility declared by this contract, if any. - pub const fn set_visibility(&self) -> Option { - self.set_visibility - } - - /// Returns the visibility required for writes by this property contract. - pub const fn write_visibility(&self) -> EvalVisibility { - match self.set_visibility { - Some(visibility) => visibility, - None => EvalVisibility::Public, - } - } - - /// Returns whether the interface requires the property to be readable. - pub const fn requires_get(&self) -> bool { - self.requires_get - } - - /// Returns whether the interface requires the property to be writable. - pub const fn requires_set(&self) -> bool { - self.requires_set - } - - /// Returns a merged contract containing either side's get/set requirements. - pub fn merged_with(&self, other: &Self) -> Self { - Self { - name: self.name.clone(), - attributes: self.attributes.clone(), - property_type: self - .property_type - .clone() - .or_else(|| other.property_type.clone()), - set_visibility: merge_eval_property_set_visibility( - self.set_visibility, - other.set_visibility, - ), - requires_get: self.requires_get || other.requires_get, - requires_set: self.requires_set || other.requires_set, - } - } -} - -/// Merges interface property set-visibility contracts by keeping the stricter write requirement. -const fn merge_eval_property_set_visibility( - left: Option, - right: Option, -) -> Option { - let left = match left { - Some(visibility) => visibility, - None => EvalVisibility::Public, - }; - let right = match right { - Some(visibility) => visibility, - None => EvalVisibility::Public, - }; - let merged = if eval_visibility_rank(left) < eval_visibility_rank(right) { - left - } else { - right - }; - match merged { - EvalVisibility::Public => None, - visibility => Some(visibility), - } -} - -/// Returns a comparable visibility rank where smaller means more restrictive. -const fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { - match visibility { - EvalVisibility::Private => 1, - EvalVisibility::Protected => 2, - EvalVisibility::Public => 3, - } -} - -/// Method signature metadata for a runtime eval interface. -#[derive(Debug, Clone)] -pub struct EvalInterfaceMethod { - name: String, - source_location: Option, - attributes: Vec, - is_static: bool, - params: Vec, - parameter_attributes: Vec>, - parameter_has_types: Vec, - parameter_types: Vec>, - parameter_defaults: Vec>, - parameter_is_by_ref: Vec, - parameter_is_variadic: Vec, - return_type: Option, -} - -impl PartialEq for EvalInterfaceMethod { - /// Compares interface method metadata while ignoring retained source-location decoration. - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.attributes == other.attributes - && self.is_static == other.is_static - && self.params == other.params - && self.parameter_attributes == other.parameter_attributes - && self.parameter_has_types == other.parameter_has_types - && self.parameter_types == other.parameter_types - && self.parameter_defaults == other.parameter_defaults - && self.parameter_is_by_ref == other.parameter_is_by_ref - && self.parameter_is_variadic == other.parameter_is_variadic - && self.return_type == other.return_type - } -} - -impl EvalInterfaceMethod { - /// Creates one dynamic eval interface method signature. - pub fn new(name: impl Into, params: Vec) -> Self { - let parameter_has_types = vec![false; params.len()]; - let parameter_attributes = vec![Vec::new(); params.len()]; - let parameter_types = vec![None; params.len()]; - let parameter_defaults = vec![None; params.len()]; - let parameter_is_by_ref = vec![false; params.len()]; - let parameter_is_variadic = vec![false; params.len()]; - Self { - name: name.into(), - source_location: None, - attributes: Vec::new(), - is_static: false, - params, - parameter_attributes, - parameter_has_types, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - return_type: None, - } - } - - /// Returns a copy of this interface method with source-location metadata attached. - pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { - self.source_location = Some(source_location); - self - } - - /// Returns a copy of this interface method with its static modifier flag set. - pub fn with_static(mut self, is_static: bool) -> Self { - self.is_static = is_static; - self - } - - /// Returns a copy of this interface method with declaration attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns a copy of this interface method with parameter type-presence flags. - pub fn with_parameter_type_flags(mut self, parameter_has_types: Vec) -> Self { - self.parameter_has_types = parameter_has_types; - self - } - - /// Returns a copy of this interface method with source-order parameter attributes. - pub fn with_parameter_attributes( - mut self, - parameter_attributes: Vec>, - ) -> Self { - self.parameter_attributes = parameter_attributes; - self - } - - /// Returns a copy of this interface method with source-order parameter type metadata. - pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { - self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); - self.parameter_types = parameter_types; - self - } - - /// Returns a copy of this interface method with source-order default expressions. - pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { - self.parameter_defaults = parameter_defaults; - self - } - - /// Returns a copy of this interface method with source-order by-reference flags. - pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { - self.parameter_is_by_ref = parameter_is_by_ref; - self - } - - /// Returns a copy of this interface method with source-order variadic flags. - pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { - self.parameter_is_variadic = parameter_is_variadic; - self - } - - /// Returns a copy of this interface method with retained return type metadata. - pub fn with_return_type(mut self, return_type: Option) -> Self { - self.return_type = return_type; - self - } - - /// Returns the PHP-visible method name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns eval-fragment source-location metadata, when retained. - pub const fn source_location(&self) -> Option { - self.source_location - } - - /// Returns attributes declared directly on this interface method. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns whether this interface method was declared `static`. - pub const fn is_static(&self) -> bool { - self.is_static - } - - /// Returns source-order parameter names without leading `$`. - pub fn params(&self) -> &[String] { - &self.params - } - - /// Returns source-order parameter attributes. - pub fn parameter_attributes(&self) -> &[Vec] { - &self.parameter_attributes - } - - /// Returns source-order flags for whether each parameter declared a type. - pub fn parameter_has_types(&self) -> &[bool] { - &self.parameter_has_types - } - - /// Returns source-order parameter type metadata. - pub fn parameter_types(&self) -> &[Option] { - &self.parameter_types - } - - /// Returns default expressions declared for each source-order parameter. - pub fn parameter_defaults(&self) -> &[Option] { - &self.parameter_defaults - } - - /// Returns source-order flags for whether each parameter was declared by reference. - pub fn parameter_is_by_ref(&self) -> &[bool] { - &self.parameter_is_by_ref - } - - /// Returns source-order flags for whether each parameter was declared variadic. - pub fn parameter_is_variadic(&self) -> &[bool] { - &self.parameter_is_variadic - } - - /// Returns retained return type metadata, if the method declared one. - pub const fn return_type(&self) -> Option<&EvalParameterType> { - self.return_type.as_ref() - } -} - -/// Runtime class declared by an eval fragment. -#[derive(Debug, Clone)] -pub struct EvalClass { - name: String, - source_location: Option, - is_abstract: bool, - is_final: bool, - is_readonly_class: bool, - is_anonymous: bool, - parent: Option, - interfaces: Vec, - attributes: Vec, - traits: Vec, - trait_adaptations: Vec, - constants: Vec, - properties: Vec, - methods: Vec, -} - -impl PartialEq for EvalClass { - /// Compares class metadata while ignoring retained source-location decoration. - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.is_abstract == other.is_abstract - && self.is_final == other.is_final - && self.is_readonly_class == other.is_readonly_class - && self.is_anonymous == other.is_anonymous - && self.parent == other.parent - && self.interfaces == other.interfaces - && self.attributes == other.attributes - && self.traits == other.traits - && self.trait_adaptations == other.trait_adaptations - && self.constants == other.constants - && self.properties == other.properties - && self.methods == other.methods - } -} - -impl EvalClass { - /// Creates a dynamic eval class with public properties and methods, and no relations. - pub fn new( - name: impl Into, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_modifiers(name, false, false, None, Vec::new(), properties, methods) - } - - /// Creates a dynamic eval class with optional parent and implemented interfaces. - pub fn with_relations( - name: impl Into, - parent: Option, - interfaces: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_modifiers(name, false, false, parent, interfaces, properties, methods) - } - - /// Creates a dynamic eval class with optional modifiers, parent, and interfaces. - pub fn with_modifiers( - name: impl Into, - is_abstract: bool, - is_final: bool, - parent: Option, - interfaces: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_class_modifiers( - name, - is_abstract, - is_final, - false, - parent, - interfaces, - properties, - methods, - ) - } - - /// Creates a dynamic eval class with class modifiers, optional parent, and interfaces. - pub fn with_class_modifiers( - name: impl Into, - is_abstract: bool, - is_final: bool, - is_readonly_class: bool, - parent: Option, - interfaces: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_class_modifiers_and_traits( - name, - is_abstract, - is_final, - is_readonly_class, - parent, - interfaces, - Vec::new(), - properties, - methods, - ) - } - - /// Creates a dynamic eval class with optional modifiers, relations, and trait uses. - pub fn with_modifiers_and_traits( - name: impl Into, - is_abstract: bool, - is_final: bool, - parent: Option, - interfaces: Vec, - traits: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_class_modifiers_and_traits( - name, - is_abstract, - is_final, - false, - parent, - interfaces, - traits, - properties, - methods, - ) - } - - /// Creates a dynamic eval class with class modifiers, relations, and trait uses. - pub fn with_class_modifiers_and_traits( - name: impl Into, - is_abstract: bool, - is_final: bool, - is_readonly_class: bool, - parent: Option, - interfaces: Vec, - traits: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_class_modifiers_traits_and_constants( - name, - is_abstract, - is_final, - is_readonly_class, - parent, - interfaces, - traits, - Vec::new(), - properties, - methods, - ) - } - - /// Creates a dynamic eval class with modifiers, relations, trait uses, constants, and members. - pub fn with_modifiers_traits_and_constants( - name: impl Into, - is_abstract: bool, - is_final: bool, - parent: Option, - interfaces: Vec, - traits: Vec, - constants: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_class_modifiers_traits_and_constants( - name, - is_abstract, - is_final, - false, - parent, - interfaces, - traits, - constants, - properties, - methods, - ) - } - - /// Creates a dynamic eval class with class modifiers, relations, trait uses, constants, and members. - pub fn with_class_modifiers_traits_and_constants( - name: impl Into, - is_abstract: bool, - is_final: bool, - is_readonly_class: bool, - parent: Option, - interfaces: Vec, - traits: Vec, - constants: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_class_modifiers_traits_adaptations_and_constants( - name, - is_abstract, - is_final, - is_readonly_class, - parent, - interfaces, - traits, - Vec::new(), - constants, - properties, - methods, - ) - } - - /// Creates a dynamic eval class with modifiers, relations, trait adaptations, constants, and members. - pub fn with_modifiers_traits_adaptations_and_constants( - name: impl Into, - is_abstract: bool, - is_final: bool, - parent: Option, - interfaces: Vec, - traits: Vec, - trait_adaptations: Vec, - constants: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_class_modifiers_traits_adaptations_and_constants( - name, - is_abstract, - is_final, - false, - parent, - interfaces, - traits, - trait_adaptations, - constants, - properties, - methods, - ) - } - - /// Creates a dynamic eval class with all class modifiers, relations, adaptations, constants, and members. - pub fn with_class_modifiers_traits_adaptations_and_constants( - name: impl Into, - is_abstract: bool, - is_final: bool, - is_readonly_class: bool, - parent: Option, - interfaces: Vec, - traits: Vec, - trait_adaptations: Vec, - constants: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self { - name: name.into(), - source_location: None, - is_abstract, - is_final, - is_readonly_class, - is_anonymous: false, - parent, - interfaces, - attributes: Vec::new(), - traits, - trait_adaptations, - constants, - properties, - methods, - } - } - - /// Returns a copy of this class with source-location metadata attached. - pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { - self.source_location = Some(source_location); - self - } - - /// Returns a copy of this class with optional source-location metadata attached. - pub const fn with_source_location_option( - mut self, - source_location: Option, - ) -> Self { - self.source_location = source_location; - self - } - - /// Returns a copy of this class with class-like attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Marks this eval class metadata as an anonymous class expression result. - pub fn with_anonymous(mut self) -> Self { - self.is_anonymous = true; - self - } - - /// Marks instance properties readonly when this metadata represents a `readonly class`. - pub fn with_readonly_properties(mut self) -> Self { - if self.is_readonly_class { - for property in &mut self.properties { - if !property.is_static { - property.is_readonly = true; - } - } - } - self - } - - /// Returns the original source spelling of this eval-declared class name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns eval-fragment source-location metadata, when retained. - pub const fn source_location(&self) -> Option { - self.source_location - } - - /// Returns whether this eval-declared class was declared `abstract`. - pub const fn is_abstract(&self) -> bool { - self.is_abstract - } - - /// Returns whether this eval-declared class was declared `final`. - pub const fn is_final(&self) -> bool { - self.is_final - } - - /// Returns whether this eval-declared class was declared `readonly`. - pub const fn is_readonly_class(&self) -> bool { - self.is_readonly_class - } - - /// Returns whether this eval class came from a `new class {}` expression. - pub const fn is_anonymous(&self) -> bool { - self.is_anonymous - } - - /// Returns the parent class name declared by this eval class, when present. - pub fn parent(&self) -> Option<&str> { - self.parent.as_deref() - } - - /// Returns interface names implemented directly by this eval class. - pub fn interfaces(&self) -> &[String] { - &self.interfaces - } - - /// Returns attributes declared directly on this eval class. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns trait names used directly by this eval class. - pub fn traits(&self) -> &[String] { - &self.traits - } - - /// Returns trait adaptations declared on this eval class. - pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { - &self.trait_adaptations - } - - /// Returns class constants declared directly by this eval class. - pub fn constants(&self) -> &[EvalClassConstant] { - &self.constants - } - - /// Returns public properties declared directly by this eval class. - pub fn properties(&self) -> &[EvalClassProperty] { - &self.properties - } - - /// Returns public methods declared directly by this eval class. - pub fn methods(&self) -> &[EvalClassMethod] { - &self.methods - } - - /// Returns a public method by PHP case-insensitive method name. - pub fn method(&self, name: &str) -> Option<&EvalClassMethod> { - self.methods() - .iter() - .find(|method| method.name().eq_ignore_ascii_case(name)) - } - - /// Returns a class constant by PHP case-sensitive constant name. - pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { - self.constants() - .iter() - .find(|constant| constant.name() == name) - } -} - -/// Adaptation rule declared in a runtime eval class `use Trait { ... }` block. -#[derive(Debug, Clone, PartialEq)] -pub enum EvalTraitAdaptation { - Alias { - trait_name: Option, - method: String, - alias: Option, - visibility: Option, - }, - InsteadOf { - trait_name: Option, - method: String, - instead_of: Vec, - }, -} - -/// Constant metadata for a runtime eval class. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalClassConstant { - name: String, - trait_origin: Option, - attributes: Vec, - visibility: EvalVisibility, - is_final: bool, - value: EvalExpr, -} - -impl EvalClassConstant { - /// Creates a public eval class constant with a value expression. - pub fn new(name: impl Into, value: EvalExpr) -> Self { - Self::with_visibility(name, EvalVisibility::Public, value) - } - - /// Creates an eval class constant with explicit PHP visibility. - pub fn with_visibility( - name: impl Into, - visibility: EvalVisibility, - value: EvalExpr, - ) -> Self { - Self::with_visibility_and_final(name, visibility, false, value) - } - - /// Creates an eval class constant with explicit PHP visibility and finality. - pub fn with_visibility_and_final( - name: impl Into, - visibility: EvalVisibility, - is_final: bool, - value: EvalExpr, - ) -> Self { - Self { - name: name.into(), - trait_origin: None, - attributes: Vec::new(), - visibility, - is_final, - value, - } - } - - /// Returns a copy of this class constant with declaration attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns a copy of this constant with its declaring trait retained for magic constants. - pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { - if self.trait_origin.is_none() { - self.trait_origin = Some(trait_name.into()); - } - self - } - - /// Returns the PHP-visible class constant name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the trait that originally declared this imported constant, if any. - pub fn trait_origin(&self) -> Option<&str> { - self.trait_origin.as_deref() - } - - /// Returns attributes declared directly on this class constant. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns the PHP visibility declared for this constant. - pub const fn visibility(&self) -> EvalVisibility { - self.visibility - } - - /// Returns whether this class constant was declared `final`. - pub const fn is_final(&self) -> bool { - self.is_final - } - - /// Returns the constant initializer expression. - pub fn value(&self) -> &EvalExpr { - &self.value - } -} - -/// Runtime trait declared by an eval fragment. -#[derive(Debug, Clone)] -pub struct EvalTrait { - name: String, - source_location: Option, - attributes: Vec, - traits: Vec, - trait_adaptations: Vec, - constants: Vec, - properties: Vec, - methods: Vec, -} - -impl PartialEq for EvalTrait { - /// Compares trait metadata while ignoring retained source-location decoration. - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.attributes == other.attributes - && self.traits == other.traits - && self.trait_adaptations == other.trait_adaptations - && self.constants == other.constants - && self.properties == other.properties - && self.methods == other.methods - } -} - -impl EvalTrait { - /// Creates a dynamic eval trait with public properties and methods. - pub fn new( - name: impl Into, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_constants(name, Vec::new(), properties, methods) - } - - /// Creates a dynamic eval trait with constants, properties, and methods. - pub fn with_constants( - name: impl Into, - constants: Vec, - properties: Vec, - methods: Vec, - ) -> Self { - Self::with_constants_traits_adaptations( - name, - constants, - properties, - methods, - Vec::new(), - Vec::new(), - ) - } - - /// Creates a dynamic eval trait with trait uses, adaptations, constants, properties, and methods. - pub fn with_constants_traits_adaptations( - name: impl Into, - constants: Vec, - properties: Vec, - methods: Vec, - traits: Vec, - trait_adaptations: Vec, - ) -> Self { - Self { - name: name.into(), - source_location: None, - attributes: Vec::new(), - traits, - trait_adaptations, - constants, - properties, - methods, - } - } - - /// Returns a copy of this trait with source-location metadata attached. - pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { - self.source_location = Some(source_location); - self - } - - /// Returns a copy of this trait with class-like attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns the original source spelling of this eval-declared trait name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns eval-fragment source-location metadata, when retained. - pub const fn source_location(&self) -> Option { - self.source_location - } - - /// Returns attributes declared directly on this eval trait. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns trait names used directly by this eval trait. - pub fn traits(&self) -> &[String] { - &self.traits - } - - /// Returns trait adaptations declared directly by this eval trait. - pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { - &self.trait_adaptations - } - - /// Returns constants declared directly by this eval trait. - pub fn constants(&self) -> &[EvalClassConstant] { - &self.constants - } - - /// Returns one trait constant by PHP case-sensitive constant name. - pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { - self.constants() - .iter() - .find(|constant| constant.name() == name) - } - - /// Returns public properties declared directly by this eval trait. - pub fn properties(&self) -> &[EvalClassProperty] { - &self.properties - } - - /// Returns public methods declared directly by this eval trait. - pub fn methods(&self) -> &[EvalClassMethod] { - &self.methods - } -} - -/// Public property metadata for a runtime eval class. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalClassProperty { - name: String, - trait_origin: Option, - attributes: Vec, - property_type: Option, - set_hook_type: Option, - visibility: EvalVisibility, - set_visibility: Option, - is_static: bool, - is_final: bool, - is_readonly: bool, - is_promoted: bool, - is_abstract: bool, - has_get_hook: bool, - has_set_hook: bool, - requires_get_hook: bool, - requires_set_hook: bool, - is_virtual: bool, - default: Option, -} - -impl EvalClassProperty { - /// Creates a public eval class property with an optional initializer. - pub fn new(name: impl Into, default: Option) -> Self { - Self::with_visibility(name, EvalVisibility::Public, default) - } - - /// Creates an eval class property with explicit PHP visibility. - pub fn with_visibility( - name: impl Into, - visibility: EvalVisibility, - default: Option, - ) -> Self { - Self::with_visibility_and_static(name, visibility, false, default) - } - - /// Creates an eval class property with explicit PHP visibility and static metadata. - pub fn with_visibility_and_static( - name: impl Into, - visibility: EvalVisibility, - is_static: bool, - default: Option, - ) -> Self { - Self::with_visibility_static_and_readonly(name, visibility, is_static, false, default) - } - - /// Creates an eval class property with explicit storage and readonly metadata. - pub fn with_visibility_static_and_readonly( - name: impl Into, - visibility: EvalVisibility, - is_static: bool, - is_readonly: bool, - default: Option, - ) -> Self { - Self::with_visibility_static_final_and_readonly( - name, - visibility, - is_static, - false, - is_readonly, - default, - ) - } - - /// Creates an eval class property with explicit storage and modifier metadata. - pub fn with_visibility_static_final_and_readonly( - name: impl Into, - visibility: EvalVisibility, - is_static: bool, - is_final: bool, - is_readonly: bool, - default: Option, - ) -> Self { - Self { - name: name.into(), - trait_origin: None, - attributes: Vec::new(), - property_type: None, - set_hook_type: None, - visibility, - set_visibility: None, - is_static, - is_final, - is_readonly, - is_promoted: false, - is_abstract: false, - has_get_hook: false, - has_set_hook: false, - requires_get_hook: false, - requires_set_hook: false, - is_virtual: false, - default, - } - } - - /// Returns a copy of this property marked with concrete get/set hook metadata. - pub const fn with_hooks(mut self, has_get_hook: bool, has_set_hook: bool) -> Self { - self.has_get_hook = has_get_hook; - self.has_set_hook = has_set_hook; - self.is_virtual = has_get_hook || has_set_hook; - self - } - - /// Returns a copy of this property with explicit hook virtuality metadata. - pub const fn with_virtual(mut self, is_virtual: bool) -> Self { - self.is_virtual = is_virtual; - self - } - - /// Returns a copy of this property marked as an abstract hook contract. - pub const fn with_abstract_hook_contract( - mut self, - requires_get_hook: bool, - requires_set_hook: bool, - ) -> Self { - self.is_abstract = true; - self.requires_get_hook = requires_get_hook; - self.requires_set_hook = requires_set_hook; - self.is_virtual = true; - self - } - - /// Returns a copy of this property with declaration attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns a copy of this property with its declaring trait retained for magic constants. - pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { - if self.trait_origin.is_none() { - self.trait_origin = Some(trait_name.into()); - } - self - } - - /// Returns a copy of this property with retained type metadata. - pub fn with_type(mut self, property_type: Option) -> Self { - self.property_type = property_type; - self - } - - /// Returns a copy of this property with retained explicit set-hook parameter type metadata. - pub fn with_set_hook_type(mut self, set_hook_type: Option) -> Self { - self.set_hook_type = set_hook_type; - self - } - - /// Returns a copy of this property with PHP asymmetric write visibility metadata. - pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { - self.set_visibility = set_visibility; - self - } - - /// Returns a copy of this property marked as coming from constructor promotion. - pub const fn with_promoted(mut self) -> Self { - self.is_promoted = true; - self - } - - /// Returns the PHP-visible property name without `$`. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns the trait that originally declared this imported property, if any. - pub fn trait_origin(&self) -> Option<&str> { - self.trait_origin.as_deref() - } - - /// Returns attributes declared directly on this class property. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns retained PHP type metadata for this property. - pub fn property_type(&self) -> Option<&EvalParameterType> { - self.property_type.as_ref() - } - - /// Returns retained PHP type metadata for an explicit set-hook parameter. - pub fn set_hook_type(&self) -> Option<&EvalParameterType> { - self.set_hook_type.as_ref() - } - - /// Returns the PHP-visible type accepted by property writes. - pub fn settable_type(&self) -> Option<&EvalParameterType> { - self.set_hook_type().or_else(|| self.property_type()) - } - - /// Returns the PHP visibility declared for this property. - pub const fn visibility(&self) -> EvalVisibility { - self.visibility - } - - /// Returns the PHP asymmetric write visibility, if it differs from read visibility. - pub const fn set_visibility(&self) -> Option { - self.set_visibility - } - - /// Returns the visibility that applies to writes for this property. - pub const fn write_visibility(&self) -> EvalVisibility { - match self.set_visibility { - Some(visibility) => visibility, - None => self.visibility, - } - } - - /// Returns whether this property was declared `static`. - pub const fn is_static(&self) -> bool { - self.is_static - } - - /// Returns whether this property was declared `final`. - pub const fn is_final(&self) -> bool { - self.is_final - } - - /// Returns whether this property was declared `readonly`. - pub const fn is_readonly(&self) -> bool { - self.is_readonly - } - - /// Returns whether this property came from constructor property promotion. - pub const fn is_promoted(&self) -> bool { - self.is_promoted - } - - /// Returns whether this property is an abstract property hook contract. - pub const fn is_abstract(&self) -> bool { - self.is_abstract - } - - /// Returns whether this property has a concrete get hook accessor. - pub const fn has_get_hook(&self) -> bool { - self.has_get_hook - } - - /// Returns whether this property has a concrete set hook accessor. - pub const fn has_set_hook(&self) -> bool { - self.has_set_hook - } - - /// Returns whether this abstract property contract requires read access. - pub const fn requires_get_hook(&self) -> bool { - self.requires_get_hook - } - - /// Returns whether this abstract property contract requires write access. - pub const fn requires_set_hook(&self) -> bool { - self.requires_set_hook - } - - /// Returns whether this property is virtual instead of backed by object storage. - pub const fn is_virtual(&self) -> bool { - self.is_virtual - } - - /// Returns the property initializer expression, when one was declared. - pub fn default(&self) -> Option<&EvalExpr> { - self.default.as_ref() - } -} - -/// PHP visibility for eval-declared object members. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EvalVisibility { - Public, - Protected, - Private, -} - -/// Public method metadata for a runtime eval class. -#[derive(Debug, Clone)] -pub struct EvalClassMethod { - name: String, - trait_origin: Option, - trait_origin_method: Option, - source_location: Option, - attributes: Vec, - visibility: EvalVisibility, - is_static: bool, - is_abstract: bool, - is_final: bool, - params: Vec, - parameter_attributes: Vec>, - parameter_has_types: Vec, - parameter_types: Vec>, - parameter_defaults: Vec>, - parameter_is_by_ref: Vec, - parameter_is_variadic: Vec, - return_type: Option, - body: Vec, -} - -impl PartialEq for EvalClassMethod { - /// Compares class method metadata while ignoring retained source-location decoration. - fn eq(&self, other: &Self) -> bool { - self.name == other.name - && self.trait_origin == other.trait_origin - && self.trait_origin_method == other.trait_origin_method - && self.attributes == other.attributes - && self.visibility == other.visibility - && self.is_static == other.is_static - && self.is_abstract == other.is_abstract - && self.is_final == other.is_final - && self.params == other.params - && self.parameter_attributes == other.parameter_attributes - && self.parameter_has_types == other.parameter_has_types - && self.parameter_types == other.parameter_types - && self.parameter_defaults == other.parameter_defaults - && self.parameter_is_by_ref == other.parameter_is_by_ref - && self.parameter_is_variadic == other.parameter_is_variadic - && self.return_type == other.return_type - && self.body == other.body - } -} - -impl EvalClassMethod { - /// Creates a public eval class method with source-order parameters and body. - pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { - Self::with_modifiers(name, false, false, params, body) - } - - /// Creates a public eval class method with optional abstract/final modifiers. - pub fn with_modifiers( - name: impl Into, - is_abstract: bool, - is_final: bool, - params: Vec, - body: Vec, - ) -> Self { - Self::with_visibility_and_modifiers( - name, - EvalVisibility::Public, - false, - is_abstract, - is_final, - params, - body, - ) - } - - /// Creates an eval class method with explicit visibility and optional modifiers. - pub fn with_visibility_and_modifiers( - name: impl Into, - visibility: EvalVisibility, - is_static: bool, - is_abstract: bool, - is_final: bool, - params: Vec, - body: Vec, - ) -> Self { - let parameter_has_types = vec![false; params.len()]; - let parameter_attributes = vec![Vec::new(); params.len()]; - let parameter_types = vec![None; params.len()]; - let parameter_defaults = vec![None; params.len()]; - let parameter_is_by_ref = vec![false; params.len()]; - let parameter_is_variadic = vec![false; params.len()]; - Self { - name: name.into(), - trait_origin: None, - trait_origin_method: None, - source_location: None, - attributes: Vec::new(), - visibility, - is_static, - is_abstract, - is_final, - params, - parameter_attributes, - parameter_has_types, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - return_type: None, - body, - } - } - - /// Returns a copy of this method with source-location metadata attached. - pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { - self.source_location = Some(source_location); - self - } - - /// Returns the PHP-visible method name. - pub fn name(&self) -> &str { - &self.name - } - - /// Returns a copy of this method with its declaring trait retained for magic constants. - pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { - if self.trait_origin.is_none() { - self.trait_origin = Some(trait_name.into()); - self.trait_origin_method = Some(self.name.clone()); - } - self - } - - /// Returns the trait that originally declared this imported method, if any. - pub fn trait_origin(&self) -> Option<&str> { - self.trait_origin.as_deref() - } - - /// Returns the PHP `__FUNCTION__` value for this method body. - pub fn magic_function_name(&self) -> &str { - self.trait_origin_method.as_deref().unwrap_or(&self.name) - } - - /// Returns the PHP `__METHOD__` value for this method body. - pub fn magic_method_name(&self, class_name: &str) -> String { - let owner = self.trait_origin().unwrap_or(class_name); - format!( - "{}::{}", - owner.trim_start_matches('\\'), - self.magic_function_name() - ) - } - - /// Returns eval-fragment source-location metadata, when retained. - pub const fn source_location(&self) -> Option { - self.source_location - } - - /// Returns a copy of this method with declaration attributes attached. - pub fn with_attributes(mut self, attributes: Vec) -> Self { - self.attributes = attributes; - self - } - - /// Returns a copy of this method with source-order parameter type-presence flags. - pub fn with_parameter_type_flags(mut self, parameter_has_types: Vec) -> Self { - self.parameter_has_types = parameter_has_types; - self - } - - /// Returns a copy of this method with source-order parameter attributes. - pub fn with_parameter_attributes( - mut self, - parameter_attributes: Vec>, - ) -> Self { - self.parameter_attributes = parameter_attributes; - self - } - - /// Returns a copy of this method with source-order parameter type metadata. - pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { - self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); - self.parameter_types = parameter_types; - self - } - - /// Returns a copy of this method with source-order default expressions. - pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { - self.parameter_defaults = parameter_defaults; - self - } - - /// Returns a copy of this method with source-order by-reference flags. - pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { - self.parameter_is_by_ref = parameter_is_by_ref; - self - } - - /// Returns a copy of this method with source-order variadic flags. - pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { - self.parameter_is_variadic = parameter_is_variadic; - self - } - - /// Returns a copy of this method with retained return type metadata. - pub fn with_return_type(mut self, return_type: Option) -> Self { - self.return_type = return_type; - self - } - - /// Returns attributes declared directly on this class method. - pub fn attributes(&self) -> &[EvalAttribute] { - &self.attributes - } - - /// Returns a copy of this method with a different PHP-visible name. - pub fn renamed(&self, name: impl Into) -> Self { - let mut method = self.clone(); - method.name = name.into(); - method - } - - /// Returns a copy of this method with a different PHP visibility. - pub fn with_visibility_override(&self, visibility: EvalVisibility) -> Self { - let mut method = self.clone(); - method.visibility = visibility; - method - } - - /// Returns the PHP visibility declared for this method. - pub const fn visibility(&self) -> EvalVisibility { - self.visibility - } - - /// Returns whether this method was declared `static`. - pub const fn is_static(&self) -> bool { - self.is_static - } - - /// Returns whether this eval-declared method was declared `abstract`. - pub const fn is_abstract(&self) -> bool { - self.is_abstract - } - - /// Returns whether this eval-declared method was declared `final`. - pub const fn is_final(&self) -> bool { - self.is_final - } - - /// Returns source-order parameter names without leading `$`. - pub fn params(&self) -> &[String] { - &self.params - } - - /// Returns source-order parameter attributes. - pub fn parameter_attributes(&self) -> &[Vec] { - &self.parameter_attributes - } - - /// Returns source-order flags for whether each parameter declared a type. - pub fn parameter_has_types(&self) -> &[bool] { - &self.parameter_has_types - } - - /// Returns source-order parameter type metadata. - pub fn parameter_types(&self) -> &[Option] { - &self.parameter_types - } - - /// Returns default expressions declared for each source-order parameter. - pub fn parameter_defaults(&self) -> &[Option] { - &self.parameter_defaults - } - - /// Returns source-order flags for whether each parameter was declared by reference. - pub fn parameter_is_by_ref(&self) -> &[bool] { - &self.parameter_is_by_ref - } - - /// Returns source-order flags for whether each parameter was declared variadic. - pub fn parameter_is_variadic(&self) -> &[bool] { - &self.parameter_is_variadic - } - - /// Returns retained return type metadata, if the method declared one. - pub const fn return_type(&self) -> Option<&EvalParameterType> { - self.return_type.as_ref() - } - - /// Returns the dynamic EvalIR statements that form the method body. - pub fn body(&self) -> &[EvalStmt] { - &self.body - } -} - -/// Dynamic eval expressions evaluated by the interpreter against runtime cells. -#[derive(Debug, Clone, PartialEq)] -pub enum EvalExpr { - Array(Vec), - ArrayGet { - array: Box, - index: Box, - }, - Call { - name: String, - args: Vec, - }, - Cast { - target: EvalCastType, - expr: Box, - }, - Const(EvalConst), - ConstFetch(String), - Closure { - function: EvalFunction, - captures: Vec, - is_static: bool, - }, - FunctionCallable { - name: String, - fallback_name: Option, - }, - InvokableCallable { - object: Box, - }, - MethodCallable { - object: Box, - method: Box, - }, - StaticMethodCallable { - class_name: String, - method: Box, - }, - DynamicStaticMethodCallable { - class_name: Box, - method: Box, - }, - DynamicCall { - callee: Box, - args: Vec, - }, - DynamicMethodCall { - object: Box, - method: Box, - args: Vec, - }, - DynamicNewObject { - class_name: Box, - args: Vec, - }, - DynamicPropertyGet { - object: Box, - property: Box, - }, - DynamicStaticMethodCall { - class_name: Box, - method: Box, - args: Vec, - }, - DynamicStaticPropertyGet { - class_name: Box, - property: String, - }, - DynamicStaticPropertyNameGet { - class_name: Box, - property: Box, - }, - DynamicClassConstantFetch { - class_name: Box, - constant: String, - }, - DynamicClassConstantNameFetch { - class_name: Box, - constant: Box, - }, - DynamicClassNameFetch { - class_name: Box, - }, - Include { - path: Box, - required: bool, - once: bool, - }, - InstanceOf { - value: Box, - target: EvalInstanceOfTarget, - }, - LoadVar(String), - Match { - subject: Box, - arms: Vec, - default: Option>, - }, - Clone(Box), - NamespacedCall { - name: String, - fallback_name: String, - args: Vec, - }, - NamespacedConstFetch { - name: String, - fallback_name: String, - }, - MethodCall { - object: Box, - method: String, - args: Vec, - }, - NullsafeMethodCall { - object: Box, - method: String, - args: Vec, - }, - NullsafeDynamicMethodCall { - object: Box, - method: Box, - args: Vec, - }, - Magic(EvalMagicConst), - NewObject { - class_name: String, - args: Vec, - }, - NewAnonymousClass { - class: EvalClass, - args: Vec, - }, - StaticMethodCall { - class_name: String, - method: String, - args: Vec, - }, - StaticPropertyGet { - class_name: String, - property: String, - }, - ClassConstantFetch { - class_name: String, - constant: String, - }, - ClassNameFetch { - class_name: String, - }, - NullCoalesce { - value: Box, - default: Box, - }, - NullsafePropertyGet { - object: Box, - property: String, - }, - NullsafeDynamicPropertyGet { - object: Box, - property: Box, - }, - PropertyGet { - object: Box, - property: String, - }, - Print(Box), - Ternary { - condition: Box, - then_branch: Option>, - else_branch: Box, - }, - Unary { - op: EvalUnaryOp, - expr: Box, - }, - Binary { - op: EvalBinOp, - left: Box, - right: Box, - }, -} - -/// The right-hand side accepted by PHP's `instanceof` operator. -#[derive(Debug, Clone, PartialEq)] -pub enum EvalInstanceOfTarget { - ClassName(String), - Expr(Box), -} - -/// One source-order function or method call argument parsed from eval code. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalCallArg { - name: Option, - spread: bool, - value: EvalExpr, -} - -impl EvalCallArg { - /// Creates a positional call argument from a value expression. - pub fn positional(value: EvalExpr) -> Self { - Self { - name: None, - spread: false, - value, - } - } - - /// Creates a named call argument from a parameter name and value expression. - pub fn named(name: impl Into, value: EvalExpr) -> Self { - Self { - name: Some(name.into()), - spread: false, - value, - } - } - - /// Creates an unpacking call argument from an array expression. - pub fn spread(value: EvalExpr) -> Self { - Self { - name: None, - spread: true, - value, - } - } - - /// Returns the source argument name without `$`, if the argument was named. - pub fn name(&self) -> Option<&str> { - self.name.as_deref() - } - - /// Returns true when this argument came from `...expr` unpacking syntax. - pub const fn is_spread(&self) -> bool { - self.spread - } - - /// Returns the expression that computes this argument's runtime value. - pub const fn value(&self) -> &EvalExpr { - &self.value - } -} - -/// One element in a PHP array literal parsed from an eval fragment. -#[derive(Debug, Clone, PartialEq)] -pub enum EvalArrayElement { - Value(EvalExpr), - Reference(EvalExpr), - KeyValue { key: EvalExpr, value: EvalExpr }, - KeyReference { key: EvalExpr, value: EvalExpr }, -} - -/// One ordered arm in a PHP `match` expression parsed from an eval fragment. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalMatchArm { - pub patterns: Vec, - pub value: EvalExpr, -} - -/// One ordered case arm in a PHP switch parsed from an eval fragment. -#[derive(Debug, Clone, PartialEq)] -pub struct EvalSwitchCase { - pub condition: Option, - pub body: Vec, -} - -/// Literal syntax supported by the initial EvalIR parser. -#[derive(Debug, Clone, PartialEq)] -pub enum EvalConst { - Null, - Bool(bool), - Int(i64), - Float(f64), - String(String), -} - -/// PHP magic constants supported by runtime eval fragments. -#[derive(Debug, Clone, PartialEq)] -pub enum EvalMagicConst { - File, - Dir, - Line(i64), - Function, - Class, - Method, - Namespace, - Trait, -} - -/// Binary operations supported by the initial EvalIR parser. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EvalBinOp { - Add, - Sub, - Mul, - Div, - Mod, - Pow, - BitAnd, - BitOr, - BitXor, - ShiftLeft, - ShiftRight, - Concat, - LogicalAnd, - LogicalOr, - LogicalXor, - LooseEq, - LooseNotEq, - StrictEq, - StrictNotEq, - Lt, - LtEq, - Gt, - GtEq, - Spaceship, -} - -/// Scalar cast targets supported by runtime eval expressions. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EvalCastType { - Int, - Float, - String, - Bool, -} - -/// Unary operations supported by the initial EvalIR parser. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EvalUnaryOp { - Plus, - Negate, - LogicalNot, - BitNot, -} +mod attributes; +mod callable; +mod classes; +mod enums; +mod expressions; +mod interfaces; +mod methods; +mod program; +mod properties; +mod statements; +mod traits; + +pub use attributes::*; +pub use callable::*; +pub use classes::*; +pub use enums::*; +pub use expressions::*; +pub use interfaces::*; +pub use methods::*; +pub use program::*; +pub use properties::*; +pub use statements::*; +pub use traits::*; diff --git a/crates/elephc-magician/src/eval_ir/attributes.rs b/crates/elephc-magician/src/eval_ir/attributes.rs new file mode 100644 index 0000000000..fdbb271ed3 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/attributes.rs @@ -0,0 +1,82 @@ +//! Purpose: +//! Defines eval attribute declarations and literal argument metadata. +//! +//! Called from: +//! - Class-like/callable parsing, validation, context registration, and Reflection. +//! +//! Key details: +//! - Attribute arguments remain syntax values until explicitly materialized by the interpreter. + +/// Literal attribute argument metadata retained by eval declarations. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalAttributeArg { + String(String), + Int(i64), + Float(u64), + Bool(bool), + Null, + Array(Vec), + Named { + name: String, + value: Box, + }, + IntKeyed { + key: i64, + value: Box, + }, +} + +impl EvalAttributeArg { + /// Returns the PHP named-argument key when this attribute arg is named. + pub fn name(&self) -> Option<&str> { + match self { + EvalAttributeArg::Named { name, .. } => Some(name), + _ => None, + } + } + + /// Returns the PHP integer array key when this attribute arg is int-keyed. + pub fn int_key(&self) -> Option { + match self { + EvalAttributeArg::IntKeyed { key, .. } => Some(*key), + _ => None, + } + } + + /// Returns the scalar payload, unwrapping a named or int-keyed wrapper. + pub fn value(&self) -> &EvalAttributeArg { + match self { + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + value + } + _ => self, + } + } +} + +/// Attribute metadata retained for eval class-like declarations. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalAttribute { + name: String, + args: Option>, +} + +impl EvalAttribute { + /// Creates one eval attribute metadata entry. + pub fn new(name: impl Into, args: Option>) -> Self { + Self { + name: name.into(), + args, + } + } + + /// Returns the resolved PHP-visible attribute class name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns supported literal positional args, or `None` for unsupported metadata. + pub fn args(&self) -> Option<&[EvalAttributeArg]> { + self.args.as_deref() + } +} diff --git a/crates/elephc-magician/src/eval_ir/callable.rs b/crates/elephc-magician/src/eval_ir/callable.rs new file mode 100644 index 0000000000..2ed0e18046 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/callable.rs @@ -0,0 +1,266 @@ +//! Purpose: +//! Defines closure captures, functions, and callable parameter type metadata. +//! +//! Called from: +//! - Function/closure parsing, dynamic binding, type checks, and Reflection. +//! +//! Key details: +//! - Parameter names, types, defaults, by-ref flags, and variadics remain index-aligned. + +use super::*; + +/// One lexical variable captured by a runtime eval closure literal. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClosureCapture { + name: String, + by_ref: bool, +} + +impl EvalClosureCapture { + /// Creates one closure capture with its source variable name and reference mode. + pub fn new(name: impl Into, by_ref: bool) -> Self { + Self { + name: name.into(), + by_ref, + } + } + + /// Returns the captured variable name without the leading `$`. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns whether this capture was declared as `use (&$name)`. + pub const fn by_ref(&self) -> bool { + self.by_ref + } +} + +/// Runtime user function declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalFunction { + name: String, + source_location: Option, + attributes: Vec, + params: Vec, + parameter_attributes: Vec>, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + return_type: Option, + body: Vec, +} + +impl PartialEq for EvalFunction { + /// Compares function metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + && self.body == other.body + } +} + +impl EvalFunction { + /// Creates a dynamic eval function with source-order parameters and body. + pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { + let parameter_attributes = vec![Vec::new(); params.len()]; + let parameter_types = vec![None; params.len()]; + let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; + Self { + name: name.into(), + source_location: None, + attributes: Vec::new(), + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type: None, + body, + } + } + + /// Returns a copy of this function with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this function with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this function with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + + /// Returns a copy of this function with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_types = parameter_types; + self + } + + /// Returns a copy of this function with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + + /// Returns a copy of this function with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + + /// Returns a copy of this function with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + + /// Returns a copy of this function with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + + /// Returns the original source spelling of this eval-declared function name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns attributes declared directly on this eval function. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } + + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } + + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } + + /// Returns retained return type metadata, if the function declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + + /// Returns the dynamic EvalIR statements that form the function body. + pub fn body(&self) -> &[EvalStmt] { + &self.body + } +} + +/// One supported eval method parameter type atom. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EvalParameterTypeVariant { + Array, + Bool, + Callable, + Class(String), + Float, + Int, + Iterable, + Mixed, + Never, + Object, + String, + Void, +} + +/// How multiple eval parameter type atoms combine. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalParameterTypeKind { + Union, + Intersection, +} + +/// Type metadata retained for one eval method parameter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EvalParameterType { + variants: Vec, + allows_null: bool, + kind: EvalParameterTypeKind, +} + +impl EvalParameterType { + /// Creates one eval method parameter type from union variants and nullability. + pub fn new(variants: Vec, allows_null: bool) -> Self { + Self { + variants, + allows_null, + kind: EvalParameterTypeKind::Union, + } + } + + /// Creates one eval method parameter type from intersection variants. + pub fn intersection(variants: Vec) -> Self { + Self { + variants, + allows_null: false, + kind: EvalParameterTypeKind::Intersection, + } + } + + /// Returns the non-null type atoms in source order. + pub fn variants(&self) -> &[EvalParameterTypeVariant] { + &self.variants + } + + /// Returns whether the type explicitly accepts PHP null. + pub const fn allows_null(&self) -> bool { + self.allows_null + } + + /// Returns whether all variants must match the value. + pub const fn is_intersection(&self) -> bool { + matches!(self.kind, EvalParameterTypeKind::Intersection) + } +} diff --git a/crates/elephc-magician/src/eval_ir/classes.rs b/crates/elephc-magician/src/eval_ir/classes.rs new file mode 100644 index 0000000000..3f11e8e8e1 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/classes.rs @@ -0,0 +1,507 @@ +//! Purpose: +//! Defines eval classes, trait adaptations, and class constants. +//! +//! Called from: +//! - Class parser, declaration validation, context lookup, object construction, and Reflection. +//! +//! Key details: +//! - Parent/interface/trait composition and member lists remain one class declaration model. + +use super::*; + +/// Runtime class declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalClass { + name: String, + source_location: Option, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + is_anonymous: bool, + parent: Option, + interfaces: Vec, + attributes: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, +} + +impl PartialEq for EvalClass { + /// Compares class metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.is_abstract == other.is_abstract + && self.is_final == other.is_final + && self.is_readonly_class == other.is_readonly_class + && self.is_anonymous == other.is_anonymous + && self.parent == other.parent + && self.interfaces == other.interfaces + && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + +impl EvalClass { + /// Creates a dynamic eval class with public properties and methods, and no relations. + pub fn new( + name: impl Into, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_modifiers(name, false, false, None, Vec::new(), properties, methods) + } + + /// Creates a dynamic eval class with optional parent and implemented interfaces. + pub fn with_relations( + name: impl Into, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_modifiers(name, false, false, parent, interfaces, properties, methods) + } + + /// Creates a dynamic eval class with optional modifiers, parent, and interfaces. + pub fn with_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, optional parent, and interfaces. + pub fn with_class_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_and_traits( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + Vec::new(), + properties, + methods, + ) + } + + /// Creates a dynamic eval class with optional modifiers, relations, and trait uses. + pub fn with_modifiers_and_traits( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_and_traits( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + traits, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, relations, and trait uses. + pub fn with_class_modifiers_and_traits( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + traits, + Vec::new(), + properties, + methods, + ) + } + + /// Creates a dynamic eval class with modifiers, relations, trait uses, constants, and members. + pub fn with_modifiers_traits_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_and_constants( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + traits, + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with class modifiers, relations, trait uses, constants, and members. + pub fn with_class_modifiers_traits_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + traits, + Vec::new(), + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with modifiers, relations, trait adaptations, constants, and members. + pub fn with_modifiers_traits_adaptations_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + false, + parent, + interfaces, + traits, + trait_adaptations, + constants, + properties, + methods, + ) + } + + /// Creates a dynamic eval class with all class modifiers, relations, adaptations, constants, and members. + pub fn with_class_modifiers_traits_adaptations_and_constants( + name: impl Into, + is_abstract: bool, + is_final: bool, + is_readonly_class: bool, + parent: Option, + interfaces: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self { + name: name.into(), + source_location: None, + is_abstract, + is_final, + is_readonly_class, + is_anonymous: false, + parent, + interfaces, + attributes: Vec::new(), + traits, + trait_adaptations, + constants, + properties, + methods, + } + } + + /// Returns a copy of this class with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this class with optional source-location metadata attached. + pub const fn with_source_location_option( + mut self, + source_location: Option, + ) -> Self { + self.source_location = source_location; + self + } + + /// Returns a copy of this class with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Marks this eval class metadata as an anonymous class expression result. + pub fn with_anonymous(mut self) -> Self { + self.is_anonymous = true; + self + } + + /// Marks instance properties readonly when this metadata represents a `readonly class`. + pub fn with_readonly_properties(mut self) -> Self { + if self.is_readonly_class { + for property in &mut self.properties { + if !property.is_static { + property.is_readonly = true; + } + } + } + self + } + + /// Returns the original source spelling of this eval-declared class name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns whether this eval-declared class was declared `abstract`. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + + /// Returns whether this eval-declared class was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + + /// Returns whether this eval-declared class was declared `readonly`. + pub const fn is_readonly_class(&self) -> bool { + self.is_readonly_class + } + + /// Returns whether this eval class came from a `new class {}` expression. + pub const fn is_anonymous(&self) -> bool { + self.is_anonymous + } + + /// Returns the parent class name declared by this eval class, when present. + pub fn parent(&self) -> Option<&str> { + self.parent.as_deref() + } + + /// Returns interface names implemented directly by this eval class. + pub fn interfaces(&self) -> &[String] { + &self.interfaces + } + + /// Returns attributes declared directly on this eval class. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns trait names used directly by this eval class. + pub fn traits(&self) -> &[String] { + &self.traits + } + + /// Returns trait adaptations declared on this eval class. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + + /// Returns class constants declared directly by this eval class. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns public properties declared directly by this eval class. + pub fn properties(&self) -> &[EvalClassProperty] { + &self.properties + } + + /// Returns public methods declared directly by this eval class. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } + + /// Returns a public method by PHP case-insensitive method name. + pub fn method(&self, name: &str) -> Option<&EvalClassMethod> { + self.methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(name)) + } + + /// Returns a class constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } +} + +/// Adaptation rule declared in a runtime eval class `use Trait { ... }` block. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalTraitAdaptation { + Alias { + trait_name: Option, + method: String, + alias: Option, + visibility: Option, + }, + InsteadOf { + trait_name: Option, + method: String, + instead_of: Vec, + }, +} + +/// Constant metadata for a runtime eval class. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClassConstant { + name: String, + trait_origin: Option, + attributes: Vec, + visibility: EvalVisibility, + is_final: bool, + value: EvalExpr, +} + +impl EvalClassConstant { + /// Creates a public eval class constant with a value expression. + pub fn new(name: impl Into, value: EvalExpr) -> Self { + Self::with_visibility(name, EvalVisibility::Public, value) + } + + /// Creates an eval class constant with explicit PHP visibility. + pub fn with_visibility( + name: impl Into, + visibility: EvalVisibility, + value: EvalExpr, + ) -> Self { + Self::with_visibility_and_final(name, visibility, false, value) + } + + /// Creates an eval class constant with explicit PHP visibility and finality. + pub fn with_visibility_and_final( + name: impl Into, + visibility: EvalVisibility, + is_final: bool, + value: EvalExpr, + ) -> Self { + Self { + name: name.into(), + trait_origin: None, + attributes: Vec::new(), + visibility, + is_final, + value, + } + } + + /// Returns a copy of this class constant with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this constant with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + } + self + } + + /// Returns the PHP-visible class constant name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the trait that originally declared this imported constant, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + + /// Returns attributes declared directly on this class constant. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns the PHP visibility declared for this constant. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + + /// Returns whether this class constant was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + + /// Returns the constant initializer expression. + pub fn value(&self) -> &EvalExpr { + &self.value + } +} diff --git a/crates/elephc-magician/src/eval_ir/enums.rs b/crates/elephc-magician/src/eval_ir/enums.rs new file mode 100644 index 0000000000..6ac858c8f2 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/enums.rs @@ -0,0 +1,236 @@ +//! Purpose: +//! Defines eval enum declarations, backing types, and case metadata. +//! +//! Called from: +//! - Enum parser, declaration execution, context lookup, and Reflection. +//! +//! Key details: +//! - Unit/backed cases, traits, interfaces, methods, and source metadata stay together. + +use super::*; + +/// Runtime enum declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalEnum { + name: String, + source_location: Option, + backing_type: Option, + interfaces: Vec, + attributes: Vec, + traits: Vec, + trait_adaptations: Vec, + cases: Vec, + constants: Vec, + methods: Vec, +} + +impl PartialEq for EvalEnum { + /// Compares enum metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.backing_type == other.backing_type + && self.interfaces == other.interfaces + && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations + && self.cases == other.cases + && self.constants == other.constants + && self.methods == other.methods + } +} + +impl EvalEnum { + /// Creates a dynamic eval enum with cases and optional backing type. + pub fn new( + name: impl Into, + backing_type: Option, + cases: Vec, + ) -> Self { + Self::with_members( + name, + backing_type, + Vec::new(), + cases, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval enum with interfaces, cases, constants, and methods. + pub fn with_members( + name: impl Into, + backing_type: Option, + interfaces: Vec, + cases: Vec, + constants: Vec, + methods: Vec, + ) -> Self { + Self::with_members_traits_adaptations( + name, + backing_type, + interfaces, + cases, + constants, + methods, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval enum with traits, adaptations, cases, constants, and methods. + pub fn with_members_traits_adaptations( + name: impl Into, + backing_type: Option, + interfaces: Vec, + cases: Vec, + constants: Vec, + methods: Vec, + traits: Vec, + trait_adaptations: Vec, + ) -> Self { + Self { + name: name.into(), + source_location: None, + backing_type, + interfaces, + attributes: Vec::new(), + traits, + trait_adaptations, + cases, + constants, + methods, + } + } + + /// Returns a copy of this enum with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this enum with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the original source spelling of this eval-declared enum name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns the optional scalar backing type for this enum. + pub const fn backing_type(&self) -> Option { + self.backing_type + } + + /// Returns interface names implemented directly by this eval enum. + pub fn interfaces(&self) -> &[String] { + &self.interfaces + } + + /// Returns attributes declared directly on this eval enum. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns trait names used directly by this eval enum. + pub fn traits(&self) -> &[String] { + &self.traits + } + + /// Returns trait adaptations declared directly by this eval enum. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + + /// Returns cases declared directly by this eval enum. + pub fn cases(&self) -> &[EvalEnumCase] { + &self.cases + } + + /// Returns one enum case by PHP case-sensitive case name. + pub fn case(&self, name: &str) -> Option<&EvalEnumCase> { + self.cases().iter().find(|case| case.name() == name) + } + + /// Returns constants declared directly by this eval enum. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns methods declared directly by this eval enum. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } + + /// Builds class-shaped metadata used for enum method and relation dispatch. + pub fn as_class_metadata(&self) -> EvalClass { + EvalClass::with_modifiers_traits_adaptations_and_constants( + self.name.clone(), + false, + true, + None, + self.interfaces.clone(), + self.traits.clone(), + self.trait_adaptations.clone(), + self.constants.clone(), + Vec::new(), + self.methods.clone(), + ) + .with_attributes(self.attributes.clone()) + .with_source_location_option(self.source_location) + } +} + +/// Scalar backing type for a runtime eval enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalEnumBackingType { + Int, + String, +} + +/// One case declared by a runtime eval enum. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalEnumCase { + name: String, + attributes: Vec, + value: Option, +} + +impl EvalEnumCase { + /// Creates an eval enum case with an optional backing value expression. + pub fn new(name: impl Into, value: Option) -> Self { + Self { + name: name.into(), + attributes: Vec::new(), + value, + } + } + + /// Returns a copy of this enum case with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the PHP-visible enum case name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns attributes declared directly on this enum case. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns the optional backing value expression. + pub fn value(&self) -> Option<&EvalExpr> { + self.value.as_ref() + } +} diff --git a/crates/elephc-magician/src/eval_ir/expressions.rs b/crates/elephc-magician/src/eval_ir/expressions.rs new file mode 100644 index 0000000000..f1ea0c7179 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/expressions.rs @@ -0,0 +1,343 @@ +//! Purpose: +//! Defines EvalIR expressions, calls, arrays, constants, operators, and match/switch values. +//! +//! Called from: +//! - Expression parser, statement nodes, optimizer-free eval execution, and default metadata. +//! +//! Key details: +//! - Expression nodes describe syntax and evaluation order without owning runtime cells. + +use super::*; + +/// Dynamic eval expressions evaluated by the interpreter against runtime cells. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalExpr { + Array(Vec), + ArrayGet { + array: Box, + index: Box, + }, + Call { + name: String, + args: Vec, + }, + Cast { + target: EvalCastType, + expr: Box, + }, + Const(EvalConst), + ConstFetch(String), + Closure { + function: EvalFunction, + captures: Vec, + is_static: bool, + }, + FunctionCallable { + name: String, + fallback_name: Option, + }, + InvokableCallable { + object: Box, + }, + MethodCallable { + object: Box, + method: Box, + }, + StaticMethodCallable { + class_name: String, + method: Box, + }, + DynamicStaticMethodCallable { + class_name: Box, + method: Box, + }, + DynamicCall { + callee: Box, + args: Vec, + }, + DynamicMethodCall { + object: Box, + method: Box, + args: Vec, + }, + DynamicNewObject { + class_name: Box, + args: Vec, + }, + DynamicPropertyGet { + object: Box, + property: Box, + }, + DynamicStaticMethodCall { + class_name: Box, + method: Box, + args: Vec, + }, + DynamicStaticPropertyGet { + class_name: Box, + property: String, + }, + DynamicStaticPropertyNameGet { + class_name: Box, + property: Box, + }, + DynamicClassConstantFetch { + class_name: Box, + constant: String, + }, + DynamicClassConstantNameFetch { + class_name: Box, + constant: Box, + }, + DynamicClassNameFetch { + class_name: Box, + }, + Include { + path: Box, + required: bool, + once: bool, + }, + InstanceOf { + value: Box, + target: EvalInstanceOfTarget, + }, + LoadVar(String), + Match { + subject: Box, + arms: Vec, + default: Option>, + }, + Clone(Box), + NamespacedCall { + name: String, + fallback_name: String, + args: Vec, + }, + NamespacedConstFetch { + name: String, + fallback_name: String, + }, + MethodCall { + object: Box, + method: String, + args: Vec, + }, + NullsafeMethodCall { + object: Box, + method: String, + args: Vec, + }, + NullsafeDynamicMethodCall { + object: Box, + method: Box, + args: Vec, + }, + Magic(EvalMagicConst), + NewObject { + class_name: String, + args: Vec, + }, + NewAnonymousClass { + class: EvalClass, + args: Vec, + }, + StaticMethodCall { + class_name: String, + method: String, + args: Vec, + }, + StaticPropertyGet { + class_name: String, + property: String, + }, + ClassConstantFetch { + class_name: String, + constant: String, + }, + ClassNameFetch { + class_name: String, + }, + NullCoalesce { + value: Box, + default: Box, + }, + NullsafePropertyGet { + object: Box, + property: String, + }, + NullsafeDynamicPropertyGet { + object: Box, + property: Box, + }, + PropertyGet { + object: Box, + property: String, + }, + Print(Box), + Ternary { + condition: Box, + then_branch: Option>, + else_branch: Box, + }, + Unary { + op: EvalUnaryOp, + expr: Box, + }, + Binary { + op: EvalBinOp, + left: Box, + right: Box, + }, +} + +/// The right-hand side accepted by PHP's `instanceof` operator. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalInstanceOfTarget { + ClassName(String), + Expr(Box), +} + +/// One source-order function or method call argument parsed from eval code. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalCallArg { + name: Option, + spread: bool, + value: EvalExpr, +} + +impl EvalCallArg { + /// Creates a positional call argument from a value expression. + pub fn positional(value: EvalExpr) -> Self { + Self { + name: None, + spread: false, + value, + } + } + + /// Creates a named call argument from a parameter name and value expression. + pub fn named(name: impl Into, value: EvalExpr) -> Self { + Self { + name: Some(name.into()), + spread: false, + value, + } + } + + /// Creates an unpacking call argument from an array expression. + pub fn spread(value: EvalExpr) -> Self { + Self { + name: None, + spread: true, + value, + } + } + + /// Returns the source argument name without `$`, if the argument was named. + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Returns true when this argument came from `...expr` unpacking syntax. + pub const fn is_spread(&self) -> bool { + self.spread + } + + /// Returns the expression that computes this argument's runtime value. + pub const fn value(&self) -> &EvalExpr { + &self.value + } +} + +/// One element in a PHP array literal parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalArrayElement { + Value(EvalExpr), + Reference(EvalExpr), + KeyValue { key: EvalExpr, value: EvalExpr }, + KeyReference { key: EvalExpr, value: EvalExpr }, +} + +/// One ordered arm in a PHP `match` expression parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalMatchArm { + pub patterns: Vec, + pub value: EvalExpr, +} + +/// One ordered case arm in a PHP switch parsed from an eval fragment. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalSwitchCase { + pub condition: Option, + pub body: Vec, +} + +/// Literal syntax supported by the initial EvalIR parser. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalConst { + Null, + Bool(bool), + Int(i64), + Float(f64), + String(String), +} + +/// PHP magic constants supported by runtime eval fragments. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalMagicConst { + File, + Dir, + Line(i64), + Function, + Class, + Method, + Namespace, + Trait, +} + +/// Binary operations supported by the initial EvalIR parser. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalBinOp { + Add, + Sub, + Mul, + Div, + Mod, + Pow, + BitAnd, + BitOr, + BitXor, + ShiftLeft, + ShiftRight, + Concat, + LogicalAnd, + LogicalOr, + LogicalXor, + LooseEq, + LooseNotEq, + StrictEq, + StrictNotEq, + Lt, + LtEq, + Gt, + GtEq, + Spaceship, +} + +/// Scalar cast targets supported by runtime eval expressions. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalCastType { + Int, + Float, + String, + Bool, +} + +/// Unary operations supported by the initial EvalIR parser. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalUnaryOp { + Plus, + Negate, + LogicalNot, + BitNot, +} diff --git a/crates/elephc-magician/src/eval_ir/interfaces.rs b/crates/elephc-magician/src/eval_ir/interfaces.rs new file mode 100644 index 0000000000..f982a5892d --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/interfaces.rs @@ -0,0 +1,444 @@ +//! Purpose: +//! Defines eval interfaces, methods, properties, and hook contracts. +//! +//! Called from: +//! - Interface parser, declaration validation, context lookup, and Reflection. +//! +//! Key details: +//! - Parent interfaces and member contracts retain types, visibility, and source metadata. + +use super::*; + +/// Runtime interface declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalInterface { + name: String, + source_location: Option, + parents: Vec, + attributes: Vec, + constants: Vec, + properties: Vec, + methods: Vec, +} + +impl PartialEq for EvalInterface { + /// Compares interface metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.parents == other.parents + && self.attributes == other.attributes + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + +impl EvalInterface { + /// Creates a dynamic eval interface with optional parent interfaces and methods. + pub fn new( + name: impl Into, + parents: Vec, + methods: Vec, + ) -> Self { + Self::with_constants(name, parents, Vec::new(), methods) + } + + /// Creates a dynamic eval interface with optional parent interfaces, constants, and methods. + pub fn with_constants( + name: impl Into, + parents: Vec, + constants: Vec, + methods: Vec, + ) -> Self { + Self::with_constants_and_properties(name, parents, constants, Vec::new(), methods) + } + + /// Creates a dynamic eval interface with constants, property contracts, and methods. + pub fn with_constants_and_properties( + name: impl Into, + parents: Vec, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self { + name: name.into(), + source_location: None, + parents, + attributes: Vec::new(), + constants, + properties, + methods, + } + } + + /// Returns a copy of this interface with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this interface with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the original source spelling of this eval-declared interface name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns interface names extended directly by this eval interface. + pub fn parents(&self) -> &[String] { + &self.parents + } + + /// Returns attributes declared directly on this eval interface. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns constants declared directly by this eval interface. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns one interface constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } + + /// Returns property hook contracts declared directly by this eval interface. + pub fn properties(&self) -> &[EvalInterfaceProperty] { + &self.properties + } + + /// Returns method signatures declared directly by this eval interface. + pub fn methods(&self) -> &[EvalInterfaceMethod] { + &self.methods + } +} + +/// Property hook contract metadata for a runtime eval interface. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalInterfaceProperty { + name: String, + attributes: Vec, + property_type: Option, + set_visibility: Option, + requires_get: bool, + requires_set: bool, +} + +impl EvalInterfaceProperty { + /// Creates one eval interface property contract. + pub fn new(name: impl Into, requires_get: bool, requires_set: bool) -> Self { + Self { + name: name.into(), + attributes: Vec::new(), + property_type: None, + set_visibility: None, + requires_get, + requires_set, + } + } + + /// Returns a copy of this interface property with retained type metadata. + pub fn with_type(mut self, property_type: Option) -> Self { + self.property_type = property_type; + self + } + + /// Returns a copy of this interface property with PHP asymmetric write visibility metadata. + pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { + self.set_visibility = set_visibility; + self + } + + /// Returns a copy of this interface property with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the PHP-visible property name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns attributes declared directly on this interface property. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns retained PHP type metadata for this interface property contract. + pub fn property_type(&self) -> Option<&EvalParameterType> { + self.property_type.as_ref() + } + + /// Returns the PHP asymmetric write visibility declared by this contract, if any. + pub const fn set_visibility(&self) -> Option { + self.set_visibility + } + + /// Returns the visibility required for writes by this property contract. + pub const fn write_visibility(&self) -> EvalVisibility { + match self.set_visibility { + Some(visibility) => visibility, + None => EvalVisibility::Public, + } + } + + /// Returns whether the interface requires the property to be readable. + pub const fn requires_get(&self) -> bool { + self.requires_get + } + + /// Returns whether the interface requires the property to be writable. + pub const fn requires_set(&self) -> bool { + self.requires_set + } + + /// Returns a merged contract containing either side's get/set requirements. + pub fn merged_with(&self, other: &Self) -> Self { + Self { + name: self.name.clone(), + attributes: self.attributes.clone(), + property_type: self + .property_type + .clone() + .or_else(|| other.property_type.clone()), + set_visibility: merge_eval_property_set_visibility( + self.set_visibility, + other.set_visibility, + ), + requires_get: self.requires_get || other.requires_get, + requires_set: self.requires_set || other.requires_set, + } + } +} + +/// Merges interface property set-visibility contracts by keeping the stricter write requirement. +const fn merge_eval_property_set_visibility( + left: Option, + right: Option, +) -> Option { + let left = match left { + Some(visibility) => visibility, + None => EvalVisibility::Public, + }; + let right = match right { + Some(visibility) => visibility, + None => EvalVisibility::Public, + }; + let merged = if eval_visibility_rank(left) < eval_visibility_rank(right) { + left + } else { + right + }; + match merged { + EvalVisibility::Public => None, + visibility => Some(visibility), + } +} + +/// Returns a comparable visibility rank where smaller means more restrictive. +const fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} + +/// Method signature metadata for a runtime eval interface. +#[derive(Debug, Clone)] +pub struct EvalInterfaceMethod { + name: String, + source_location: Option, + attributes: Vec, + is_static: bool, + params: Vec, + parameter_attributes: Vec>, + parameter_has_types: Vec, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + return_type: Option, +} + +impl PartialEq for EvalInterfaceMethod { + /// Compares interface method metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.is_static == other.is_static + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_has_types == other.parameter_has_types + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + } +} + +impl EvalInterfaceMethod { + /// Creates one dynamic eval interface method signature. + pub fn new(name: impl Into, params: Vec) -> Self { + let parameter_has_types = vec![false; params.len()]; + let parameter_attributes = vec![Vec::new(); params.len()]; + let parameter_types = vec![None; params.len()]; + let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; + Self { + name: name.into(), + source_location: None, + attributes: Vec::new(), + is_static: false, + params, + parameter_attributes, + parameter_has_types, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type: None, + } + } + + /// Returns a copy of this interface method with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this interface method with its static modifier flag set. + pub fn with_static(mut self, is_static: bool) -> Self { + self.is_static = is_static; + self + } + + /// Returns a copy of this interface method with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this interface method with parameter type-presence flags. + pub fn with_parameter_type_flags(mut self, parameter_has_types: Vec) -> Self { + self.parameter_has_types = parameter_has_types; + self + } + + /// Returns a copy of this interface method with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + + /// Returns a copy of this interface method with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); + self.parameter_types = parameter_types; + self + } + + /// Returns a copy of this interface method with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + + /// Returns a copy of this interface method with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + + /// Returns a copy of this interface method with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + + /// Returns a copy of this interface method with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + + /// Returns the PHP-visible method name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns attributes declared directly on this interface method. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns whether this interface method was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } + + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + + /// Returns source-order flags for whether each parameter declared a type. + pub fn parameter_has_types(&self) -> &[bool] { + &self.parameter_has_types + } + + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } + + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } + + /// Returns retained return type metadata, if the method declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } +} diff --git a/crates/elephc-magician/src/eval_ir/methods.rs b/crates/elephc-magician/src/eval_ir/methods.rs new file mode 100644 index 0000000000..2a796ff8d9 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/methods.rs @@ -0,0 +1,301 @@ +//! Purpose: +//! Defines eval class methods and their complete callable metadata. +//! +//! Called from: +//! - Class-like parsing, validation, dynamic invocation, closures, and Reflection. +//! +//! Key details: +//! - Parameters, statics, visibility, hook bodies, return types, and source metadata remain aligned. + +use super::*; + +/// Public method metadata for a runtime eval class. +#[derive(Debug, Clone)] +pub struct EvalClassMethod { + name: String, + trait_origin: Option, + trait_origin_method: Option, + source_location: Option, + attributes: Vec, + visibility: EvalVisibility, + is_static: bool, + is_abstract: bool, + is_final: bool, + params: Vec, + parameter_attributes: Vec>, + parameter_has_types: Vec, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + return_type: Option, + body: Vec, +} + +impl PartialEq for EvalClassMethod { + /// Compares class method metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.trait_origin == other.trait_origin + && self.trait_origin_method == other.trait_origin_method + && self.attributes == other.attributes + && self.visibility == other.visibility + && self.is_static == other.is_static + && self.is_abstract == other.is_abstract + && self.is_final == other.is_final + && self.params == other.params + && self.parameter_attributes == other.parameter_attributes + && self.parameter_has_types == other.parameter_has_types + && self.parameter_types == other.parameter_types + && self.parameter_defaults == other.parameter_defaults + && self.parameter_is_by_ref == other.parameter_is_by_ref + && self.parameter_is_variadic == other.parameter_is_variadic + && self.return_type == other.return_type + && self.body == other.body + } +} + +impl EvalClassMethod { + /// Creates a public eval class method with source-order parameters and body. + pub fn new(name: impl Into, params: Vec, body: Vec) -> Self { + Self::with_modifiers(name, false, false, params, body) + } + + /// Creates a public eval class method with optional abstract/final modifiers. + pub fn with_modifiers( + name: impl Into, + is_abstract: bool, + is_final: bool, + params: Vec, + body: Vec, + ) -> Self { + Self::with_visibility_and_modifiers( + name, + EvalVisibility::Public, + false, + is_abstract, + is_final, + params, + body, + ) + } + + /// Creates an eval class method with explicit visibility and optional modifiers. + pub fn with_visibility_and_modifiers( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + is_abstract: bool, + is_final: bool, + params: Vec, + body: Vec, + ) -> Self { + let parameter_has_types = vec![false; params.len()]; + let parameter_attributes = vec![Vec::new(); params.len()]; + let parameter_types = vec![None; params.len()]; + let parameter_defaults = vec![None; params.len()]; + let parameter_is_by_ref = vec![false; params.len()]; + let parameter_is_variadic = vec![false; params.len()]; + Self { + name: name.into(), + trait_origin: None, + trait_origin_method: None, + source_location: None, + attributes: Vec::new(), + visibility, + is_static, + is_abstract, + is_final, + params, + parameter_attributes, + parameter_has_types, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type: None, + body, + } + } + + /// Returns a copy of this method with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns the PHP-visible method name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns a copy of this method with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + self.trait_origin_method = Some(self.name.clone()); + } + self + } + + /// Returns the trait that originally declared this imported method, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + + /// Returns the PHP `__FUNCTION__` value for this method body. + pub fn magic_function_name(&self) -> &str { + self.trait_origin_method.as_deref().unwrap_or(&self.name) + } + + /// Returns the PHP `__METHOD__` value for this method body. + pub fn magic_method_name(&self, class_name: &str) -> String { + let owner = self.trait_origin().unwrap_or(class_name); + format!( + "{}::{}", + owner.trim_start_matches('\\'), + self.magic_function_name() + ) + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns a copy of this method with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this method with source-order parameter type-presence flags. + pub fn with_parameter_type_flags(mut self, parameter_has_types: Vec) -> Self { + self.parameter_has_types = parameter_has_types; + self + } + + /// Returns a copy of this method with source-order parameter attributes. + pub fn with_parameter_attributes( + mut self, + parameter_attributes: Vec>, + ) -> Self { + self.parameter_attributes = parameter_attributes; + self + } + + /// Returns a copy of this method with source-order parameter type metadata. + pub fn with_parameter_types(mut self, parameter_types: Vec>) -> Self { + self.parameter_has_types = parameter_types.iter().map(Option::is_some).collect(); + self.parameter_types = parameter_types; + self + } + + /// Returns a copy of this method with source-order default expressions. + pub fn with_parameter_defaults(mut self, parameter_defaults: Vec>) -> Self { + self.parameter_defaults = parameter_defaults; + self + } + + /// Returns a copy of this method with source-order by-reference flags. + pub fn with_parameter_by_ref_flags(mut self, parameter_is_by_ref: Vec) -> Self { + self.parameter_is_by_ref = parameter_is_by_ref; + self + } + + /// Returns a copy of this method with source-order variadic flags. + pub fn with_parameter_variadic_flags(mut self, parameter_is_variadic: Vec) -> Self { + self.parameter_is_variadic = parameter_is_variadic; + self + } + + /// Returns a copy of this method with retained return type metadata. + pub fn with_return_type(mut self, return_type: Option) -> Self { + self.return_type = return_type; + self + } + + /// Returns attributes declared directly on this class method. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns a copy of this method with a different PHP-visible name. + pub fn renamed(&self, name: impl Into) -> Self { + let mut method = self.clone(); + method.name = name.into(); + method + } + + /// Returns a copy of this method with a different PHP visibility. + pub fn with_visibility_override(&self, visibility: EvalVisibility) -> Self { + let mut method = self.clone(); + method.visibility = visibility; + method + } + + /// Returns the PHP visibility declared for this method. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + + /// Returns whether this method was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + + /// Returns whether this eval-declared method was declared `abstract`. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + + /// Returns whether this eval-declared method was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + + /// Returns source-order parameter names without leading `$`. + pub fn params(&self) -> &[String] { + &self.params + } + + /// Returns source-order parameter attributes. + pub fn parameter_attributes(&self) -> &[Vec] { + &self.parameter_attributes + } + + /// Returns source-order flags for whether each parameter declared a type. + pub fn parameter_has_types(&self) -> &[bool] { + &self.parameter_has_types + } + + /// Returns source-order parameter type metadata. + pub fn parameter_types(&self) -> &[Option] { + &self.parameter_types + } + + /// Returns default expressions declared for each source-order parameter. + pub fn parameter_defaults(&self) -> &[Option] { + &self.parameter_defaults + } + + /// Returns source-order flags for whether each parameter was declared by reference. + pub fn parameter_is_by_ref(&self) -> &[bool] { + &self.parameter_is_by_ref + } + + /// Returns source-order flags for whether each parameter was declared variadic. + pub fn parameter_is_variadic(&self) -> &[bool] { + &self.parameter_is_variadic + } + + /// Returns retained return type metadata, if the method declared one. + pub const fn return_type(&self) -> Option<&EvalParameterType> { + self.return_type.as_ref() + } + + /// Returns the dynamic EvalIR statements that form the method body. + pub fn body(&self) -> &[EvalStmt] { + &self.body + } +} diff --git a/crates/elephc-magician/src/eval_ir/program.rs b/crates/elephc-magician/src/eval_ir/program.rs new file mode 100644 index 0000000000..3f1aa4cba8 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/program.rs @@ -0,0 +1,74 @@ +//! Purpose: +//! Defines parsed EvalIR programs and source-location metadata. +//! +//! Called from: +//! - Parser entry points, declaration metadata, Reflection, and interpreter execution. +//! +//! Key details: +//! - Source offsets and file/line ranges remain syntax metadata, not runtime cells. + +use super::*; + +/// Parsed eval fragment lowered into dynamic by-name statements. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalProgram { + source_len: usize, + statements: Vec, +} + +impl EvalProgram { + /// Creates an EvalIR program for a source fragment and statement list. + pub fn new(source_len: usize, statements: Vec) -> Self { + Self { + source_len, + statements, + } + } + + /// Returns the byte length of the parsed eval fragment. + pub const fn source_len(&self) -> usize { + self.source_len + } + + /// Returns the ordered EvalIR statements in source order. + pub fn statements(&self) -> &[EvalStmt] { + &self.statements + } + + /// Consumes the program and returns its statement list. + pub fn into_statements(self) -> Vec { + self.statements + } +} + +/// One source range inside the current eval fragment. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EvalSourceLocation { + start_line: i64, + end_line: i64, +} + +impl EvalSourceLocation { + /// Creates a source range using one-based eval-fragment line numbers. + pub const fn new(start_line: i64, end_line: i64) -> Self { + Self { + start_line, + end_line, + } + } + + /// Creates a single-line source range. + pub const fn single_line(line: i64) -> Self { + Self::new(line, line) + } + + /// Returns the one-based line where the declaration starts. + pub const fn start_line(&self) -> i64 { + self.start_line + } + + /// Returns the one-based line where the declaration ends. + pub const fn end_line(&self) -> i64 { + self.end_line + } +} diff --git a/crates/elephc-magician/src/eval_ir/properties.rs b/crates/elephc-magician/src/eval_ir/properties.rs new file mode 100644 index 0000000000..8a87e48a3d --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/properties.rs @@ -0,0 +1,284 @@ +//! Purpose: +//! Defines eval class properties, hook metadata, defaults, and visibility. +//! +//! Called from: +//! - Class-like parsing, validation, object storage, property access, and Reflection. +//! +//! Key details: +//! - Readonly, promotion, asymmetric set visibility, backing slots, and hooks stay coherent. + +use super::*; + +/// Public property metadata for a runtime eval class. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalClassProperty { + name: String, + trait_origin: Option, + attributes: Vec, + property_type: Option, + set_hook_type: Option, + visibility: EvalVisibility, + set_visibility: Option, + pub(super) is_static: bool, + is_final: bool, + pub(super) is_readonly: bool, + is_promoted: bool, + is_abstract: bool, + has_get_hook: bool, + has_set_hook: bool, + requires_get_hook: bool, + requires_set_hook: bool, + is_virtual: bool, + default: Option, +} + +impl EvalClassProperty { + /// Creates a public eval class property with an optional initializer. + pub fn new(name: impl Into, default: Option) -> Self { + Self::with_visibility(name, EvalVisibility::Public, default) + } + + /// Creates an eval class property with explicit PHP visibility. + pub fn with_visibility( + name: impl Into, + visibility: EvalVisibility, + default: Option, + ) -> Self { + Self::with_visibility_and_static(name, visibility, false, default) + } + + /// Creates an eval class property with explicit PHP visibility and static metadata. + pub fn with_visibility_and_static( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + default: Option, + ) -> Self { + Self::with_visibility_static_and_readonly(name, visibility, is_static, false, default) + } + + /// Creates an eval class property with explicit storage and readonly metadata. + pub fn with_visibility_static_and_readonly( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + is_readonly: bool, + default: Option, + ) -> Self { + Self::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + false, + is_readonly, + default, + ) + } + + /// Creates an eval class property with explicit storage and modifier metadata. + pub fn with_visibility_static_final_and_readonly( + name: impl Into, + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_readonly: bool, + default: Option, + ) -> Self { + Self { + name: name.into(), + trait_origin: None, + attributes: Vec::new(), + property_type: None, + set_hook_type: None, + visibility, + set_visibility: None, + is_static, + is_final, + is_readonly, + is_promoted: false, + is_abstract: false, + has_get_hook: false, + has_set_hook: false, + requires_get_hook: false, + requires_set_hook: false, + is_virtual: false, + default, + } + } + + /// Returns a copy of this property marked with concrete get/set hook metadata. + pub const fn with_hooks(mut self, has_get_hook: bool, has_set_hook: bool) -> Self { + self.has_get_hook = has_get_hook; + self.has_set_hook = has_set_hook; + self.is_virtual = has_get_hook || has_set_hook; + self + } + + /// Returns a copy of this property with explicit hook virtuality metadata. + pub const fn with_virtual(mut self, is_virtual: bool) -> Self { + self.is_virtual = is_virtual; + self + } + + /// Returns a copy of this property marked as an abstract hook contract. + pub const fn with_abstract_hook_contract( + mut self, + requires_get_hook: bool, + requires_set_hook: bool, + ) -> Self { + self.is_abstract = true; + self.requires_get_hook = requires_get_hook; + self.requires_set_hook = requires_set_hook; + self.is_virtual = true; + self + } + + /// Returns a copy of this property with declaration attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns a copy of this property with its declaring trait retained for magic constants. + pub fn with_trait_origin(mut self, trait_name: impl Into) -> Self { + if self.trait_origin.is_none() { + self.trait_origin = Some(trait_name.into()); + } + self + } + + /// Returns a copy of this property with retained type metadata. + pub fn with_type(mut self, property_type: Option) -> Self { + self.property_type = property_type; + self + } + + /// Returns a copy of this property with retained explicit set-hook parameter type metadata. + pub fn with_set_hook_type(mut self, set_hook_type: Option) -> Self { + self.set_hook_type = set_hook_type; + self + } + + /// Returns a copy of this property with PHP asymmetric write visibility metadata. + pub const fn with_set_visibility(mut self, set_visibility: Option) -> Self { + self.set_visibility = set_visibility; + self + } + + /// Returns a copy of this property marked as coming from constructor promotion. + pub const fn with_promoted(mut self) -> Self { + self.is_promoted = true; + self + } + + /// Returns the PHP-visible property name without `$`. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns the trait that originally declared this imported property, if any. + pub fn trait_origin(&self) -> Option<&str> { + self.trait_origin.as_deref() + } + + /// Returns attributes declared directly on this class property. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns retained PHP type metadata for this property. + pub fn property_type(&self) -> Option<&EvalParameterType> { + self.property_type.as_ref() + } + + /// Returns retained PHP type metadata for an explicit set-hook parameter. + pub fn set_hook_type(&self) -> Option<&EvalParameterType> { + self.set_hook_type.as_ref() + } + + /// Returns the PHP-visible type accepted by property writes. + pub fn settable_type(&self) -> Option<&EvalParameterType> { + self.set_hook_type().or_else(|| self.property_type()) + } + + /// Returns the PHP visibility declared for this property. + pub const fn visibility(&self) -> EvalVisibility { + self.visibility + } + + /// Returns the PHP asymmetric write visibility, if it differs from read visibility. + pub const fn set_visibility(&self) -> Option { + self.set_visibility + } + + /// Returns the visibility that applies to writes for this property. + pub const fn write_visibility(&self) -> EvalVisibility { + match self.set_visibility { + Some(visibility) => visibility, + None => self.visibility, + } + } + + /// Returns whether this property was declared `static`. + pub const fn is_static(&self) -> bool { + self.is_static + } + + /// Returns whether this property was declared `final`. + pub const fn is_final(&self) -> bool { + self.is_final + } + + /// Returns whether this property was declared `readonly`. + pub const fn is_readonly(&self) -> bool { + self.is_readonly + } + + /// Returns whether this property came from constructor property promotion. + pub const fn is_promoted(&self) -> bool { + self.is_promoted + } + + /// Returns whether this property is an abstract property hook contract. + pub const fn is_abstract(&self) -> bool { + self.is_abstract + } + + /// Returns whether this property has a concrete get hook accessor. + pub const fn has_get_hook(&self) -> bool { + self.has_get_hook + } + + /// Returns whether this property has a concrete set hook accessor. + pub const fn has_set_hook(&self) -> bool { + self.has_set_hook + } + + /// Returns whether this abstract property contract requires read access. + pub const fn requires_get_hook(&self) -> bool { + self.requires_get_hook + } + + /// Returns whether this abstract property contract requires write access. + pub const fn requires_set_hook(&self) -> bool { + self.requires_set_hook + } + + /// Returns whether this property is virtual instead of backed by object storage. + pub const fn is_virtual(&self) -> bool { + self.is_virtual + } + + /// Returns the property initializer expression, when one was declared. + pub fn default(&self) -> Option<&EvalExpr> { + self.default.as_ref() + } +} + +/// PHP visibility for eval-declared object members. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvalVisibility { + Public, + Protected, + Private, +} diff --git a/crates/elephc-magician/src/eval_ir/statements.rs b/crates/elephc-magician/src/eval_ir/statements.rs new file mode 100644 index 0000000000..3750af8502 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/statements.rs @@ -0,0 +1,278 @@ +//! Purpose: +//! Defines EvalIR statement and catch-clause variants. +//! +//! Called from: +//! - Statement parser and interpreter statement dispatcher. +//! +//! Key details: +//! - Statements encode explicit mutation and structured control flow without runtime ownership. + +use super::*; + +/// Dynamic eval statements that operate on a materialized activation scope. +#[derive(Debug, Clone, PartialEq)] +pub enum EvalStmt { + ArrayAppendVar { + name: String, + value: EvalExpr, + }, + ArraySetVar { + name: String, + index: EvalExpr, + value: EvalExpr, + }, + Break, + Continue, + DoWhile { + body: Vec, + condition: EvalExpr, + }, + Echo(EvalExpr), + For { + init: Vec, + condition: Option, + update: Vec, + body: Vec, + }, + ClassDecl(EvalClass), + EnumDecl(EvalEnum), + InterfaceDecl(EvalInterface), + TraitDecl(EvalTrait), + Foreach { + array: EvalExpr, + key_name: Option, + value_name: String, + body: Vec, + }, + FunctionDecl { + name: String, + source_location: Option, + attributes: Vec, + params: Vec, + parameter_attributes: Vec>, + parameter_types: Vec>, + parameter_defaults: Vec>, + parameter_is_by_ref: Vec, + parameter_is_variadic: Vec, + return_type: Option, + body: Vec, + }, + Global { + vars: Vec, + }, + If { + condition: EvalExpr, + then_branch: Vec, + else_branch: Vec, + }, + Return(Option), + ReferenceAssign { + target: String, + source: String, + }, + PropertyReferenceBind { + object: EvalExpr, + property: String, + source: String, + }, + DynamicPropertyReferenceBind { + object: EvalExpr, + property: EvalExpr, + source: String, + }, + DynamicPropertySet { + object: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicPropertyArrayAppend { + object: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicPropertyArraySet { + object: EvalExpr, + property: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + DynamicPropertyCompoundAssign { + object: EvalExpr, + property: EvalExpr, + op: EvalBinOp, + value: EvalExpr, + }, + DynamicPropertyIncDec { + object: EvalExpr, + property: EvalExpr, + increment: bool, + }, + PropertySet { + object: EvalExpr, + property: String, + value: EvalExpr, + }, + PropertyArrayAppend { + object: EvalExpr, + property: String, + value: EvalExpr, + }, + PropertyArraySet { + object: EvalExpr, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + PropertyCompoundAssign { + object: EvalExpr, + property: String, + op: EvalBinOp, + value: EvalExpr, + }, + PropertyIncDec { + object: EvalExpr, + property: String, + increment: bool, + }, + StaticPropertySet { + class_name: String, + property: String, + value: EvalExpr, + }, + StaticPropertyReferenceBind { + class_name: String, + property: String, + source: String, + }, + StaticPropertyArrayAppend { + class_name: String, + property: String, + value: EvalExpr, + }, + StaticPropertyArraySet { + class_name: String, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + StaticPropertyIncDec { + class_name: String, + property: String, + increment: bool, + }, + DynamicStaticPropertySet { + class_name: EvalExpr, + property: String, + value: EvalExpr, + }, + DynamicStaticPropertyReferenceBind { + class_name: EvalExpr, + property: String, + source: String, + }, + DynamicStaticPropertyArrayAppend { + class_name: EvalExpr, + property: String, + value: EvalExpr, + }, + DynamicStaticPropertyArraySet { + class_name: EvalExpr, + property: String, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + DynamicStaticPropertyIncDec { + class_name: EvalExpr, + property: String, + increment: bool, + }, + DynamicStaticPropertyNameSet { + class_name: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr, + property: EvalExpr, + source: String, + }, + DynamicStaticPropertyNameArrayAppend { + class_name: EvalExpr, + property: EvalExpr, + value: EvalExpr, + }, + DynamicStaticPropertyNameArraySet { + class_name: EvalExpr, + property: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, + }, + DynamicStaticPropertyNameIncDec { + class_name: EvalExpr, + property: EvalExpr, + increment: bool, + }, + StaticVar { + name: String, + init: EvalExpr, + }, + StoreVar { + name: String, + value: EvalExpr, + }, + Switch { + expr: EvalExpr, + cases: Vec, + }, + Throw(EvalExpr), + Try { + body: Vec, + catches: Vec, + finally_body: Vec, + }, + UnsetArrayElement { + array: EvalExpr, + index: EvalExpr, + }, + UnsetProperty { + object: EvalExpr, + property: String, + }, + UnsetDynamicProperty { + object: EvalExpr, + property: EvalExpr, + }, + UnsetStaticProperty { + class_name: String, + property: String, + }, + UnsetDynamicStaticProperty { + class_name: EvalExpr, + property: String, + }, + UnsetDynamicStaticPropertyName { + class_name: EvalExpr, + property: EvalExpr, + }, + UnsetVar { + name: String, + }, + While { + condition: EvalExpr, + body: Vec, + }, + Expr(EvalExpr), +} + +/// One `catch` block attached to an eval `try` statement. +#[derive(Debug, Clone, PartialEq)] +pub struct EvalCatch { + pub class_names: Vec, + pub var_name: Option, + pub body: Vec, +} diff --git a/crates/elephc-magician/src/eval_ir/traits.rs b/crates/elephc-magician/src/eval_ir/traits.rs new file mode 100644 index 0000000000..7ffd2f14f8 --- /dev/null +++ b/crates/elephc-magician/src/eval_ir/traits.rs @@ -0,0 +1,144 @@ +//! Purpose: +//! Defines eval trait declarations and composed member metadata. +//! +//! Called from: +//! - Trait parser, trait composition, context lookup, and Reflection. +//! +//! Key details: +//! - Nested traits, adaptations, methods, properties, constants, and attributes stay grouped. + +use super::*; + +/// Runtime trait declared by an eval fragment. +#[derive(Debug, Clone)] +pub struct EvalTrait { + name: String, + source_location: Option, + attributes: Vec, + traits: Vec, + trait_adaptations: Vec, + constants: Vec, + properties: Vec, + methods: Vec, +} + +impl PartialEq for EvalTrait { + /// Compares trait metadata while ignoring retained source-location decoration. + fn eq(&self, other: &Self) -> bool { + self.name == other.name + && self.attributes == other.attributes + && self.traits == other.traits + && self.trait_adaptations == other.trait_adaptations + && self.constants == other.constants + && self.properties == other.properties + && self.methods == other.methods + } +} + +impl EvalTrait { + /// Creates a dynamic eval trait with public properties and methods. + pub fn new( + name: impl Into, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_constants(name, Vec::new(), properties, methods) + } + + /// Creates a dynamic eval trait with constants, properties, and methods. + pub fn with_constants( + name: impl Into, + constants: Vec, + properties: Vec, + methods: Vec, + ) -> Self { + Self::with_constants_traits_adaptations( + name, + constants, + properties, + methods, + Vec::new(), + Vec::new(), + ) + } + + /// Creates a dynamic eval trait with trait uses, adaptations, constants, properties, and methods. + pub fn with_constants_traits_adaptations( + name: impl Into, + constants: Vec, + properties: Vec, + methods: Vec, + traits: Vec, + trait_adaptations: Vec, + ) -> Self { + Self { + name: name.into(), + source_location: None, + attributes: Vec::new(), + traits, + trait_adaptations, + constants, + properties, + methods, + } + } + + /// Returns a copy of this trait with source-location metadata attached. + pub const fn with_source_location(mut self, source_location: EvalSourceLocation) -> Self { + self.source_location = Some(source_location); + self + } + + /// Returns a copy of this trait with class-like attributes attached. + pub fn with_attributes(mut self, attributes: Vec) -> Self { + self.attributes = attributes; + self + } + + /// Returns the original source spelling of this eval-declared trait name. + pub fn name(&self) -> &str { + &self.name + } + + /// Returns eval-fragment source-location metadata, when retained. + pub const fn source_location(&self) -> Option { + self.source_location + } + + /// Returns attributes declared directly on this eval trait. + pub fn attributes(&self) -> &[EvalAttribute] { + &self.attributes + } + + /// Returns trait names used directly by this eval trait. + pub fn traits(&self) -> &[String] { + &self.traits + } + + /// Returns trait adaptations declared directly by this eval trait. + pub fn trait_adaptations(&self) -> &[EvalTraitAdaptation] { + &self.trait_adaptations + } + + /// Returns constants declared directly by this eval trait. + pub fn constants(&self) -> &[EvalClassConstant] { + &self.constants + } + + /// Returns one trait constant by PHP case-sensitive constant name. + pub fn constant(&self, name: &str) -> Option<&EvalClassConstant> { + self.constants() + .iter() + .find(|constant| constant.name() == name) + } + + /// Returns public properties declared directly by this eval trait. + pub fn properties(&self) -> &[EvalClassProperty] { + &self.properties + } + + /// Returns public methods declared directly by this eval trait. + pub fn methods(&self) -> &[EvalClassMethod] { + &self.methods + } +} From b843ffe076eae5ed61834cba9a8fd848c731935a Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 13 Jul 2026 13:26:46 +0200 Subject: [PATCH 1202/1208] refactor(magician): split FFI implementation and tests --- .../src/ffi/native_functions.rs | 599 +---- .../src/ffi/native_functions/public_abi.rs | 269 ++ .../src/ffi/native_functions/registration.rs | 350 +++ .../elephc-magician/src/ffi/native_methods.rs | 2350 +---------------- .../ffi/native_methods/attribute_decoder.rs | 131 + .../ffi/native_methods/callable_metadata.rs | 270 ++ .../constructor_registration.rs | 291 ++ .../ffi/native_methods/method_registration.rs | 532 ++++ .../native_methods/property_registration.rs | 210 ++ .../src/ffi/native_methods/public_abi.rs | 969 +++++++ crates/elephc-magician/src/ffi/tests.rs | 1651 ------------ .../src/ffi/tests/callable_defaults.rs | 228 ++ .../src/ffi/tests/class_metadata.rs | 346 +++ .../src/ffi/tests/context_symbols.rs | 312 +++ crates/elephc-magician/src/ffi/tests/mod.rs | 223 ++ .../src/ffi/tests/native_functions.rs | 144 + .../src/ffi/tests/native_methods.rs | 296 +++ .../src/ffi/tests/scope_execution.rs | 175 ++ 18 files changed, 4769 insertions(+), 4577 deletions(-) create mode 100644 crates/elephc-magician/src/ffi/native_functions/public_abi.rs create mode 100644 crates/elephc-magician/src/ffi/native_functions/registration.rs create mode 100644 crates/elephc-magician/src/ffi/native_methods/attribute_decoder.rs create mode 100644 crates/elephc-magician/src/ffi/native_methods/callable_metadata.rs create mode 100644 crates/elephc-magician/src/ffi/native_methods/constructor_registration.rs create mode 100644 crates/elephc-magician/src/ffi/native_methods/method_registration.rs create mode 100644 crates/elephc-magician/src/ffi/native_methods/property_registration.rs create mode 100644 crates/elephc-magician/src/ffi/native_methods/public_abi.rs delete mode 100644 crates/elephc-magician/src/ffi/tests.rs create mode 100644 crates/elephc-magician/src/ffi/tests/callable_defaults.rs create mode 100644 crates/elephc-magician/src/ffi/tests/class_metadata.rs create mode 100644 crates/elephc-magician/src/ffi/tests/context_symbols.rs create mode 100644 crates/elephc-magician/src/ffi/tests/mod.rs create mode 100644 crates/elephc-magician/src/ffi/tests/native_functions.rs create mode 100644 crates/elephc-magician/src/ffi/tests/native_methods.rs create mode 100644 crates/elephc-magician/src/ffi/tests/scope_execution.rs diff --git a/crates/elephc-magician/src/ffi/native_functions.rs b/crates/elephc-magician/src/ffi/native_functions.rs index 5b14d47895..a2fbf76b11 100644 --- a/crates/elephc-magician/src/ffi/native_functions.rs +++ b/crates/elephc-magician/src/ffi/native_functions.rs @@ -19,599 +19,8 @@ use crate::abi::{ElephcEvalContext, ABI_VERSION}; use crate::context::{NativeCallableDefault, NativeFunction, NativeFunctionInvoker}; use std::ffi::c_void; -/// Registers a generated native PHP function callback in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for -/// `name_len` bytes when `name_len > 0`. `descriptor` and `invoker` must follow -/// the descriptor-invoker ABI emitted by generated code. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - descriptor: *mut c_void, - invoker: Option, - param_count: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_inner(ctx, name_ptr, name_len, descriptor, invoker, param_count) - }) - .unwrap_or(0) -} +mod public_abi; +mod registration; -/// Registers one generated native PHP function parameter name in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function and parameter name -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_param( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_param_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - param_name_ptr, - param_name_len, - ) - }) - .unwrap_or(0) -} - -/// Registers whether a generated native PHP function can be invoked through its bridge. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function name must be readable for -/// its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_bridge_support( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - supported: i32, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_bridge_support_inner( - ctx, - function_name_ptr, - function_name_len, - supported, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP function parameter's by-ref and variadic flags. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function name must be readable for -/// its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_param_flags( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - is_by_ref: i32, - is_variadic: i32, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_param_flags_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - is_by_ref, - is_variadic, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP function parameter type in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function name and type-spec -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_param_type( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_param_type_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - type_spec_ptr, - type_spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP function return type in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function name and type-spec -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_return_type( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_return_type_inner( - ctx, - function_name_ptr, - function_name_len, - type_spec_ptr, - type_spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP function scalar parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function name must be readable for -/// its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_scalar( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_param_default_scalar_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - default_kind, - default_payload, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP function string parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function name and default string -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_string( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_param_default_string_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - default_ptr, - default_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP function object parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function name and encoded default -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_object( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_param_default_object_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP function array parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Function name and encoded default -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_array( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_function_param_default_array_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Runs the native registration ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function`; invalid handles, names, or -/// callback pointers fail closed as `false`. -unsafe fn register_native_function_inner( - ctx: *mut ElephcEvalContext, - name_ptr: *const u8, - name_len: u64, - descriptor: *mut c_void, - invoker: Option, - param_count: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION || descriptor.is_null() { - return 0; - } - let Some(invoker) = invoker else { - return 0; - }; - let Ok(name) = abi_name_to_string(name_ptr, name_len) else { - return 0; - }; - let Ok(param_count) = usize::try_from(param_count) else { - return 0; - }; - let function = NativeFunction::new(descriptor, invoker, param_count); - i32::from( - context - .define_native_function(name.to_ascii_lowercase(), function) - .is_ok(), - ) -} - -/// Runs the native parameter-name registration ABI body after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_param`; invalid handles, -/// names, or indexes fail closed as `false`. -unsafe fn register_native_function_param_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { - return 0; - }; - let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - i32::from(context.define_native_function_param( - &function_name.to_ascii_lowercase(), - param_index, - param_name, - )) -} - -/// Runs native function bridge-support registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_bridge_support`; invalid -/// handles or names fail closed as `false`. -unsafe fn register_native_function_bridge_support_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - supported: i32, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { - return 0; - }; - i32::from(context.define_native_function_bridge_supported( - &function_name.to_ascii_lowercase(), - supported != 0, - )) -} - -/// Runs native function parameter-flags registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_param_flags`; invalid -/// handles, names, or indexes fail closed as `false`. -unsafe fn register_native_function_param_flags_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - is_by_ref: i32, - is_variadic: i32, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - let function_name = function_name.to_ascii_lowercase(); - if !context.define_native_function_param_by_ref(&function_name, param_index, is_by_ref != 0) { - return 0; - } - if is_variadic != 0 { - return i32::from(context.define_native_function_variadic_param( - &function_name, - param_index, - )); - } - 1 -} - -/// Runs native function parameter-type registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_param_type`; invalid handles, -/// names, indexes, or type specs fail closed as `false`. -unsafe fn register_native_function_param_type_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - let Some(param_type) = native_callable_type_from_abi( - type_spec_ptr, - type_spec_len, - NativeCallableTypePosition::Parameter, - ) else { - return 0; - }; - i32::from(context.define_native_function_param_type( - &function_name.to_ascii_lowercase(), - param_index, - param_type, - )) -} - -/// Runs native function return-type registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_return_type`; invalid handles, -/// names, or type specs fail closed as `false`. -unsafe fn register_native_function_return_type_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { - return 0; - }; - let Some(return_type) = native_callable_type_from_abi( - type_spec_ptr, - type_spec_len, - NativeCallableTypePosition::Return, - ) else { - return 0; - }; - i32::from(context.define_native_function_return_type( - &function_name.to_ascii_lowercase(), - return_type, - )) -} - -/// Runs native function scalar-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_param_default_scalar`; invalid -/// handles, names, indexes, or default kinds fail closed as `false`. -unsafe fn register_native_function_param_default_scalar_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { - return 0; - }; - register_native_function_param_default_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - default, - ) -} - -/// Runs native function string-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_param_default_string`; invalid -/// handles, names, or indexes fail closed as `false`. -unsafe fn register_native_function_param_default_string_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - let Ok(default) = abi_name_to_string(default_ptr, default_len) else { - return 0; - }; - register_native_function_param_default_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - NativeCallableDefault::String(default), - ) -} - -/// Runs native function object-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_param_default_object`; invalid -/// handles, names, indexes, or object specs fail closed as `false`. -unsafe fn register_native_function_param_default_object_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { - return 0; - }; - register_native_function_param_default_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - default, - ) -} - -/// Runs native function array-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_function_param_default_array`; invalid -/// handles, names, indexes, or array specs fail closed as `false`. -unsafe fn register_native_function_param_default_array_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { - return 0; - }; - register_native_function_param_default_inner( - ctx, - function_name_ptr, - function_name_len, - param_index, - default, - ) -} - -/// Records a native function parameter default by folded function name. -/// -/// # Safety -/// `ctx` and `function_name_ptr` must be valid for their declared use; callers -/// are the exported ABI wrappers above. -unsafe fn register_native_function_param_default_inner( - ctx: *mut ElephcEvalContext, - function_name_ptr: *const u8, - function_name_len: u64, - param_index: u64, - default: NativeCallableDefault, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - i32::from(context.define_native_function_param_default( - &function_name.to_ascii_lowercase(), - param_index, - default, - )) -} +pub use public_abi::*; +use registration::*; diff --git a/crates/elephc-magician/src/ffi/native_functions/public_abi.rs b/crates/elephc-magician/src/ffi/native_functions/public_abi.rs new file mode 100644 index 0000000000..fa0d8304db --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_functions/public_abi.rs @@ -0,0 +1,269 @@ +//! Purpose: +//! Exposes C ABI entry points for generated native PHP function signatures, +//! types, flags, bridge support, and defaults. +//! +//! Called from: +//! - Generated EIR backend assembly during native function registration. +//! +//! Key details: +//! - Every entry point installs a panic boundary before internal validation. + +use super::*; + +/// Registers a generated native PHP function callback in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `name_ptr` must be readable for +/// `name_len` bytes when `name_len > 0`. `descriptor` and `invoker` must follow +/// the descriptor-invoker ABI emitted by generated code. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + descriptor: *mut c_void, + invoker: Option, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_inner(ctx, name_ptr, name_len, descriptor, invoker, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers whether a generated native PHP function can be invoked through its bridge. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_bridge_support( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_bridge_support_inner( + ctx, + function_name_ptr, + function_name_len, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter's by-ref and variadic flags. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_flags( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_flags_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function parameter type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and type-spec +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_type( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_type_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function return type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and type-spec +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_return_type( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_return_type_inner( + ctx, + function_name_ptr, + function_name_len, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_scalar( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_scalar_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_string( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_string_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_object( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_object_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP function array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Function name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_function_param_default_array( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_function_param_default_array_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} diff --git a/crates/elephc-magician/src/ffi/native_functions/registration.rs b/crates/elephc-magician/src/ffi/native_functions/registration.rs new file mode 100644 index 0000000000..f5c1fcc701 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_functions/registration.rs @@ -0,0 +1,350 @@ +//! Purpose: +//! Validates and records generated native function callbacks and callable +//! metadata after the public ABI panic boundary. +//! +//! Called from: +//! - `super::public_abi` registration entry points. +//! +//! Key details: +//! - Invalid handles, names, descriptors, types, defaults, or indexes fail closed. + +use super::*; + +/// Runs the native registration ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function`; invalid handles, names, or +/// callback pointers fail closed as `false`. +pub(super) unsafe fn register_native_function_inner( + ctx: *mut ElephcEvalContext, + name_ptr: *const u8, + name_len: u64, + descriptor: *mut c_void, + invoker: Option, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION || descriptor.is_null() { + return 0; + } + let Some(invoker) = invoker else { + return 0; + }; + let Ok(name) = abi_name_to_string(name_ptr, name_len) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + let function = NativeFunction::new(descriptor, invoker, param_count); + i32::from( + context + .define_native_function(name.to_ascii_lowercase(), function) + .is_ok(), + ) +} + +/// Runs the native parameter-name registration ABI body after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param`; invalid handles, +/// names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_function_param_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_function_param( + &function_name.to_ascii_lowercase(), + param_index, + param_name, + )) +} + +/// Runs native function bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_bridge_support`; invalid +/// handles or names fail closed as `false`. +pub(super) unsafe fn register_native_function_bridge_support_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + i32::from(context.define_native_function_bridge_supported( + &function_name.to_ascii_lowercase(), + supported != 0, + )) +} + +/// Runs native function parameter-flags registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_flags`; invalid +/// handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_function_param_flags_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let function_name = function_name.to_ascii_lowercase(); + if !context.define_native_function_param_by_ref(&function_name, param_index, is_by_ref != 0) { + return 0; + } + if is_variadic != 0 { + return i32::from(context.define_native_function_variadic_param( + &function_name, + param_index, + )); + } + 1 +} + +/// Runs native function parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_type`; invalid handles, +/// names, indexes, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_function_param_type_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_function_param_type( + &function_name.to_ascii_lowercase(), + param_index, + param_type, + )) +} + +/// Runs native function return-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_return_type`; invalid handles, +/// names, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_function_return_type_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Some(return_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Return, + ) else { + return 0; + }; + i32::from(context.define_native_function_return_type( + &function_name.to_ascii_lowercase(), + return_type, + )) +} + +/// Runs native function scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_scalar`; invalid +/// handles, names, indexes, or default kinds fail closed as `false`. +pub(super) unsafe fn register_native_function_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Runs native function string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_string`; invalid +/// handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_function_param_default_string_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Runs native function object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_object`; invalid +/// handles, names, indexes, or object specs fail closed as `false`. +pub(super) unsafe fn register_native_function_param_default_object_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Runs native function array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_function_param_default_array`; invalid +/// handles, names, indexes, or array specs fail closed as `false`. +pub(super) unsafe fn register_native_function_param_default_array_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_function_param_default_inner( + ctx, + function_name_ptr, + function_name_len, + param_index, + default, + ) +} + +/// Records a native function parameter default by folded function name. +/// +/// # Safety +/// `ctx` and `function_name_ptr` must be valid for their declared use; callers +/// are the exported ABI wrappers above. +pub(super) unsafe fn register_native_function_param_default_inner( + ctx: *mut ElephcEvalContext, + function_name_ptr: *const u8, + function_name_len: u64, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(function_name) = abi_name_to_string(function_name_ptr, function_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_function_param_default( + &function_name.to_ascii_lowercase(), + param_index, + default, + )) +} diff --git a/crates/elephc-magician/src/ffi/native_methods.rs b/crates/elephc-magician/src/ffi/native_methods.rs index c62360bbc0..5849488e1d 100644 --- a/crates/elephc-magician/src/ffi/native_methods.rs +++ b/crates/elephc-magician/src/ffi/native_methods.rs @@ -22,6 +22,25 @@ use crate::eval_ir::{ EvalParameterTypeVariant, }; +mod attribute_decoder; +mod callable_metadata; +mod constructor_registration; +mod method_registration; +mod property_registration; +mod public_abi; + +use attribute_decoder::*; +use callable_metadata::*; +use constructor_registration::*; +use method_registration::*; +use property_registration::*; +pub use public_abi::*; + +pub(in crate::ffi) use callable_metadata::{ + native_callable_array_default, native_callable_object_default, native_callable_scalar_default, + native_callable_type_from_abi, +}; + const NATIVE_DEFAULT_NULL: u64 = 0; const NATIVE_DEFAULT_BOOL: u64 = 1; const NATIVE_DEFAULT_INT: u64 = 2; @@ -57,2334 +76,3 @@ pub(super) enum NativeCallableTypePosition { Parameter, Return, } - -/// Registers a generated native PHP method signature in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `method_key_ptr` must point to a -/// readable `ClassName::methodName` byte string. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_count: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_inner(ctx, method_key_ptr, method_key_len, false, param_count) - }) - .unwrap_or(0) -} - -/// Registers a generated native PHP static-method signature in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `method_key_ptr` must point to a -/// readable `ClassName::methodName` byte string. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_count: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_inner(ctx, method_key_ptr, method_key_len, true, param_count) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP interface property contract in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a -/// readable `InterfaceName::DeclaringInterface::propertyName` byte string, and -/// `type_spec_ptr` must point to a readable generated type-spec byte string. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_interface_property( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, - flags: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_interface_property_inner( - ctx, - property_key_ptr, - property_key_len, - type_spec_ptr, - type_spec_len, - flags, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP abstract class property contract in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a -/// readable `ClassName::DeclaringClass::propertyName` byte string, and -/// `type_spec_ptr` must point to a readable generated type-spec byte string. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_abstract_property( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, - flags: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_abstract_property_inner( - ctx, - property_key_ptr, - property_key_len, - type_spec_ptr, - type_spec_len, - flags, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP method parameter name in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and parameter name -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_param( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_inner( - ctx, - method_key_ptr, - method_key_len, - false, - param_index, - param_name_ptr, - param_name_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP static-method parameter name in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and parameter name -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_inner( - ctx, - method_key_ptr, - method_key_len, - true, - param_index, - param_name_ptr, - param_name_len, - ) - }) - .unwrap_or(0) -} - -/// Registers generated native PHP method parameter flags in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The method key must be readable -/// for its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_param_flags( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - is_by_ref: i32, - is_variadic: i32, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_flags_inner( - ctx, - method_key_ptr, - method_key_len, - false, - param_index, - is_by_ref, - is_variadic, - ) - }) - .unwrap_or(0) -} - -/// Registers generated native PHP static-method parameter flags in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The method key must be readable -/// for its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_flags( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - is_by_ref: i32, - is_variadic: i32, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_flags_inner( - ctx, - method_key_ptr, - method_key_len, - true, - param_index, - is_by_ref, - is_variadic, - ) - }) - .unwrap_or(0) -} - -/// Registers whether generated eval may dispatch a native PHP method. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The method key must be readable -/// for its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_bridge_support( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - supported: i32, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_bridge_support_inner( - ctx, - method_key_ptr, - method_key_len, - false, - supported, - ) - }) - .unwrap_or(0) -} - -/// Registers whether generated eval may dispatch a native PHP static method. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The method key must be readable -/// for its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_bridge_support( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - supported: i32, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_bridge_support_inner( - ctx, - method_key_ptr, - method_key_len, - true, - supported, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP method parameter declared type in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and type-spec pointers -/// must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_param_type( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_type_inner( - ctx, - method_key_ptr, - method_key_len, - false, - param_index, - type_spec_ptr, - type_spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP static-method parameter declared type. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and type-spec pointers -/// must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_type( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_type_inner( - ctx, - method_key_ptr, - method_key_len, - true, - param_index, - type_spec_ptr, - type_spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP method declared return type in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and type-spec pointers -/// must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_return_type( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_return_type_inner( - ctx, - method_key_ptr, - method_key_len, - false, - type_spec_ptr, - type_spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP static-method declared return type. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and type-spec pointers -/// must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_return_type( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_return_type_inner( - ctx, - method_key_ptr, - method_key_len, - true, - type_spec_ptr, - type_spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP method scalar parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key must be readable for -/// its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_scalar( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_default_scalar_inner( - ctx, - method_key_ptr, - method_key_len, - false, - param_index, - default_kind, - default_payload, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP static-method scalar parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key must be readable for -/// its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_scalar( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_default_scalar_inner( - ctx, - method_key_ptr, - method_key_len, - true, - param_index, - default_kind, - default_payload, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP method string parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and default string -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_string( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_default_string_inner( - ctx, - method_key_ptr, - method_key_len, - false, - param_index, - default_ptr, - default_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP static-method string parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and default string -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_string( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_default_string_inner( - ctx, - method_key_ptr, - method_key_len, - true, - param_index, - default_ptr, - default_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP method object parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and encoded default -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_object( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_default_object_inner( - ctx, - method_key_ptr, - method_key_len, - false, - param_index, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP static-method object parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and encoded default -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_object( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_default_object_inner( - ctx, - method_key_ptr, - method_key_len, - true, - param_index, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP method array parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and encoded default -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_array( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_default_array_inner( - ctx, - method_key_ptr, - method_key_len, - false, - param_index, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP static-method array parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Method key and encoded default -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_array( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_method_param_default_array_inner( - ctx, - method_key_ptr, - method_key_len, - true, - param_index, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers a generated native PHP constructor signature in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `class_name_ptr` must be readable -/// for `class_name_len` bytes. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_count: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_inner(ctx, class_name_ptr, class_name_len, param_count) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP constructor parameter name in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Class and parameter name pointers -/// must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_param_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - param_name_ptr, - param_name_len, - ) - }) - .unwrap_or(0) -} - -/// Registers generated native PHP constructor parameter flags in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The class name must be readable -/// for its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_flags( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - is_by_ref: i32, - is_variadic: i32, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_param_flags_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - is_by_ref, - is_variadic, - ) - }) - .unwrap_or(0) -} - -/// Registers whether generated eval may dispatch a native PHP constructor. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The class name must be readable -/// for its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor_bridge_support( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - supported: i32, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_bridge_support_inner( - ctx, - class_name_ptr, - class_name_len, - supported, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP constructor parameter declared type. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Class and type-spec pointers must -/// be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_type( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_param_type_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - type_spec_ptr, - type_spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP constructor scalar parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Class name must be readable for -/// its declared byte length. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_scalar( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_param_default_scalar_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - default_kind, - default_payload, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP constructor string parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Class name and default string -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_string( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_param_default_string_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - default_ptr, - default_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP constructor object parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Class name and encoded default -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_object( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_param_default_object_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP constructor array parameter default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Class name and encoded default -/// pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_array( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_constructor_param_default_array_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers generated native PHP parent-class metadata in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. Class and parent name pointers -/// must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_class_parent( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - parent_name_ptr: *const u8, - parent_name_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_class_parent_inner( - ctx, - class_name_ptr, - class_name_len, - parent_name_ptr, - parent_name_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP property type in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The property key must be a -/// readable `ClassName::propertyName` byte string, and the type spec must be a -/// readable generated type-spec byte string. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_property_type( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_property_type_inner( - ctx, - property_key_ptr, - property_key_len, - type_spec_ptr, - type_spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP property scalar default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The property key must be a -/// readable `ClassName::propertyName` byte string. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_property_default_scalar( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_property_default_scalar_inner( - ctx, - property_key_ptr, - property_key_len, - default_kind, - default_payload, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP property string default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The property key and default -/// string pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_property_default_string( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_property_default_string_inner( - ctx, - property_key_ptr, - property_key_len, - default_ptr, - default_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP property array default in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. The property key and encoded -/// default pointers must be readable for their declared byte lengths. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_property_default_array( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_property_default_array_inner( - ctx, - property_key_ptr, - property_key_len, - spec_ptr, - spec_len, - ) - }) - .unwrap_or(0) -} - -/// Registers one generated native PHP class/member attribute in an eval context. -/// -/// # Safety -/// `ctx` must be a valid eval context handle. `record_ptr` must point to one -/// readable binary member-attribute metadata record. -#[no_mangle] -pub unsafe extern "C" fn __elephc_eval_register_native_member_attribute( - ctx: *mut ElephcEvalContext, - record_ptr: *const u8, - record_len: u64, -) -> i32 { - std::panic::catch_unwind(|| unsafe { - register_native_member_attribute_inner(ctx, record_ptr, record_len) - }) - .unwrap_or(0) -} - -/// Runs native method registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method`; invalid handles, names, or -/// counts fail closed as `false`. -unsafe fn register_native_method_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_count: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { - return 0; - }; - let Some((class_name, method_name)) = split_method_key(&method_key) else { - return 0; - }; - let Ok(param_count) = usize::try_from(param_count) else { - return 0; - }; - let signature = NativeCallableSignature::new(param_count); - if is_static { - i32::from(context.define_native_static_method_signature(class_name, method_name, signature)) - } else { - i32::from(context.define_native_method_signature(class_name, method_name, signature)) - } -} - -/// Runs native interface property registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_interface_property`; invalid handles, -/// names, flags, or type specs fail closed as `false`. -unsafe fn register_native_interface_property_inner( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, - flags: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { - return 0; - }; - let Some((interface_name, declaring_interface_name, property_name)) = - split_three_part_property_key(&property_key) - else { - return 0; - }; - let requires_get = flags & NATIVE_PROPERTY_REQUIRES_GET != 0; - let requires_set = flags & NATIVE_PROPERTY_REQUIRES_SET != 0; - if !requires_get && !requires_set { - return 0; - } - let Some(property_type) = native_callable_type_from_abi( - type_spec_ptr, - type_spec_len, - NativeCallableTypePosition::Parameter, - ) else { - return 0; - }; - let property = EvalInterfaceProperty::new(property_name, requires_get, requires_set) - .with_type(Some(property_type)); - i32::from(context.define_native_interface_property_requirement( - interface_name, - declaring_interface_name, - property, - )) -} - -/// Runs native abstract property registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_abstract_property`; invalid handles, -/// names, flags, or type specs fail closed as `false`. -unsafe fn register_native_abstract_property_inner( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, - flags: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { - return 0; - }; - let Some((class_name, declaring_class_name, property_name)) = - split_three_part_property_key(&property_key) - else { - return 0; - }; - let requires_get = flags & NATIVE_PROPERTY_REQUIRES_GET != 0; - let requires_set = flags & NATIVE_PROPERTY_REQUIRES_SET != 0; - if !requires_get && !requires_set { - return 0; - } - let Some(property_type) = native_callable_type_from_abi( - type_spec_ptr, - type_spec_len, - NativeCallableTypePosition::Parameter, - ) else { - return 0; - }; - let property = EvalInterfaceProperty::new(property_name, requires_get, requires_set) - .with_type(Some(property_type)); - i32::from(context.define_native_abstract_property_requirement( - class_name, - declaring_class_name, - property, - )) -} - -/// Runs native method parameter registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_param`; invalid handles, names, -/// or indexes fail closed as `false`. -unsafe fn register_native_method_param_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { - return 0; - }; - let Some((class_name, method_name)) = split_method_key(&method_key) else { - return 0; - }; - let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - if is_static { - i32::from(context.define_native_static_method_param( - class_name, - method_name, - param_index, - param_name, - )) - } else { - i32::from(context.define_native_method_param( - class_name, - method_name, - param_index, - param_name, - )) - } -} - -/// Runs native method parameter-flag registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_param_flags`; invalid handles, -/// names, or indexes fail closed as `false`. -unsafe fn register_native_method_param_flags_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_index: u64, - is_by_ref: i32, - is_variadic: i32, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { - return 0; - }; - let Some((class_name, method_name)) = split_method_key(&method_key) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - let by_ref_registered = if is_static { - context.define_native_static_method_param_by_ref( - class_name, - method_name, - param_index, - is_by_ref != 0, - ) - } else { - context.define_native_method_param_by_ref( - class_name, - method_name, - param_index, - is_by_ref != 0, - ) - }; - if !by_ref_registered { - return 0; - } - if is_variadic == 0 { - return 1; - } - i32::from(if is_static { - context.define_native_static_method_variadic_param(class_name, method_name, param_index) - } else { - context.define_native_method_variadic_param(class_name, method_name, param_index) - }) -} - -/// Runs native method bridge-support registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_bridge_support`; invalid -/// handles or names fail closed as `false`. -unsafe fn register_native_method_bridge_support_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - supported: i32, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { - return 0; - }; - let Some((class_name, method_name)) = split_method_key(&method_key) else { - return 0; - }; - i32::from(if is_static { - context.define_native_static_method_bridge_supported( - class_name, - method_name, - supported != 0, - ) - } else { - context.define_native_method_bridge_supported(class_name, method_name, supported != 0) - }) -} - -/// Runs native method parameter-type registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_param_type`; invalid handles, -/// names, indexes, or type specs fail closed as `false`. -unsafe fn register_native_method_param_type_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_index: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { - return 0; - }; - let Some((class_name, method_name)) = split_method_key(&method_key) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - let Some(param_type) = native_callable_type_from_abi( - type_spec_ptr, - type_spec_len, - NativeCallableTypePosition::Parameter, - ) else { - return 0; - }; - if is_static { - i32::from(context.define_native_static_method_param_type( - class_name, - method_name, - param_index, - param_type, - )) - } else { - i32::from(context.define_native_method_param_type( - class_name, - method_name, - param_index, - param_type, - )) - } -} - -/// Runs native method return-type registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_return_type`; invalid handles, -/// names, or type specs fail closed as `false`. -unsafe fn register_native_method_return_type_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { - return 0; - }; - let Some((class_name, method_name)) = split_method_key(&method_key) else { - return 0; - }; - let Some(return_type) = native_callable_type_from_abi( - type_spec_ptr, - type_spec_len, - NativeCallableTypePosition::Return, - ) else { - return 0; - }; - if is_static { - i32::from(context.define_native_static_method_return_type( - class_name, - method_name, - return_type, - )) - } else { - i32::from(context.define_native_method_return_type(class_name, method_name, return_type)) - } -} - -/// Runs native method scalar-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_param_default_scalar`; invalid -/// handles, names, indexes, or default kinds fail closed as `false`. -unsafe fn register_native_method_param_default_scalar_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_index: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { - return 0; - }; - register_native_method_param_default_inner( - ctx, - method_key_ptr, - method_key_len, - is_static, - param_index, - default, - ) -} - -/// Runs native method string-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_param_default_string`; invalid -/// handles, names, or indexes fail closed as `false`. -unsafe fn register_native_method_param_default_string_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_index: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - let Ok(default) = abi_name_to_string(default_ptr, default_len) else { - return 0; - }; - register_native_method_param_default_inner( - ctx, - method_key_ptr, - method_key_len, - is_static, - param_index, - NativeCallableDefault::String(default), - ) -} - -/// Runs native method object-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_param_default_object`; invalid -/// handles, names, indexes, or object specs fail closed as `false`. -unsafe fn register_native_method_param_default_object_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { - return 0; - }; - register_native_method_param_default_inner( - ctx, - method_key_ptr, - method_key_len, - is_static, - param_index, - default, - ) -} - -/// Runs native method array-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_method_param_default_array`; invalid -/// handles, names, indexes, or array specs fail closed as `false`. -unsafe fn register_native_method_param_default_array_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { - return 0; - }; - register_native_method_param_default_inner( - ctx, - method_key_ptr, - method_key_len, - is_static, - param_index, - default, - ) -} - -/// Records a native method parameter default in the selected instance/static table. -/// -/// # Safety -/// `ctx` and `method_key_ptr` must be valid for their declared use; callers are -/// the exported ABI wrappers above. -unsafe fn register_native_method_param_default_inner( - ctx: *mut ElephcEvalContext, - method_key_ptr: *const u8, - method_key_len: u64, - is_static: bool, - param_index: u64, - default: NativeCallableDefault, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { - return 0; - }; - let Some((class_name, method_name)) = split_method_key(&method_key) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - if is_static { - i32::from(context.define_native_static_method_param_default( - class_name, - method_name, - param_index, - default, - )) - } else { - i32::from(context.define_native_method_param_default( - class_name, - method_name, - param_index, - default, - )) - } -} - -/// Runs native constructor registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor`; invalid handles, names, -/// or counts fail closed as `false`. -unsafe fn register_native_constructor_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_count: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { - return 0; - }; - let Ok(param_count) = usize::try_from(param_count) else { - return 0; - }; - i32::from(context.define_native_constructor_signature( - &class_name, - NativeCallableSignature::new(param_count), - )) -} - -/// Runs native constructor parameter registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor_param`; invalid handles, -/// names, or indexes fail closed as `false`. -unsafe fn register_native_constructor_param_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - param_name_ptr: *const u8, - param_name_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { - return 0; - }; - let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - i32::from(context.define_native_constructor_param(&class_name, param_index, param_name)) -} - -/// Runs native constructor parameter-flag registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor_param_flags`; invalid -/// handles, names, or indexes fail closed as `false`. -unsafe fn register_native_constructor_param_flags_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - is_by_ref: i32, - is_variadic: i32, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - if !context.define_native_constructor_param_by_ref(&class_name, param_index, is_by_ref != 0) { - return 0; - } - if is_variadic == 0 { - return 1; - } - i32::from(context.define_native_constructor_variadic_param(&class_name, param_index)) -} - -/// Runs native constructor bridge-support registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor_bridge_support`; invalid -/// handles or names fail closed as `false`. -unsafe fn register_native_constructor_bridge_support_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - supported: i32, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { - return 0; - }; - i32::from(context.define_native_constructor_bridge_supported(&class_name, supported != 0)) -} - -/// Runs native constructor parameter-type registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor_param_type`; invalid -/// handles, names, indexes, or type specs fail closed as `false`. -unsafe fn register_native_constructor_param_type_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - let Some(param_type) = native_callable_type_from_abi( - type_spec_ptr, - type_spec_len, - NativeCallableTypePosition::Parameter, - ) else { - return 0; - }; - i32::from(context.define_native_constructor_param_type(&class_name, param_index, param_type)) -} - -/// Runs native constructor scalar-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor_param_default_scalar`; -/// invalid handles, names, indexes, or default kinds fail closed as `false`. -unsafe fn register_native_constructor_param_default_scalar_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { - return 0; - }; - register_native_constructor_param_default_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - default, - ) -} - -/// Runs native constructor string-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor_param_default_string`; -/// invalid handles, names, or indexes fail closed as `false`. -unsafe fn register_native_constructor_param_default_string_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - let Ok(default) = abi_name_to_string(default_ptr, default_len) else { - return 0; - }; - register_native_constructor_param_default_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - NativeCallableDefault::String(default), - ) -} - -/// Runs native constructor object-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor_param_default_object`; -/// invalid handles, names, indexes, or object specs fail closed as `false`. -unsafe fn register_native_constructor_param_default_object_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { - return 0; - }; - register_native_constructor_param_default_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - default, - ) -} - -/// Runs native constructor array-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_constructor_param_default_array`; -/// invalid handles, names, indexes, or array specs fail closed as `false`. -unsafe fn register_native_constructor_param_default_array_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { - return 0; - }; - register_native_constructor_param_default_inner( - ctx, - class_name_ptr, - class_name_len, - param_index, - default, - ) -} - -/// Records a native constructor parameter default in the constructor signature table. -/// -/// # Safety -/// `ctx` and `class_name_ptr` must be valid for their declared use; callers are -/// the exported ABI wrappers above. -unsafe fn register_native_constructor_param_default_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - param_index: u64, - default: NativeCallableDefault, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { - return 0; - }; - let Ok(param_index) = usize::try_from(param_index) else { - return 0; - }; - i32::from(context.define_native_constructor_param_default(&class_name, param_index, default)) -} - -/// Runs native parent-class registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_class_parent`; invalid handles or -/// names fail closed as `false`. -unsafe fn register_native_class_parent_inner( - ctx: *mut ElephcEvalContext, - class_name_ptr: *const u8, - class_name_len: u64, - parent_name_ptr: *const u8, - parent_name_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { - return 0; - }; - let Ok(parent_name) = abi_name_to_string(parent_name_ptr, parent_name_len) else { - return 0; - }; - i32::from(context.define_native_class_parent(&class_name, &parent_name)) -} - -/// Runs native property-type registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_property_type`; invalid handles, -/// names, or type specs fail closed as `false`. -unsafe fn register_native_property_type_inner( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - type_spec_ptr: *const u8, - type_spec_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { - return 0; - }; - let Some((class_name, property_name)) = split_property_key(&property_key) else { - return 0; - }; - let Some(property_type) = native_callable_type_from_abi( - type_spec_ptr, - type_spec_len, - NativeCallableTypePosition::Parameter, - ) else { - return 0; - }; - i32::from(context.define_native_property_type(class_name, property_name, property_type)) -} - -/// Runs native property scalar-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_property_default_scalar`; invalid -/// handles, names, or default kinds fail closed as `false`. -unsafe fn register_native_property_default_scalar_inner( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - default_kind: u64, - default_payload: u64, -) -> i32 { - let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { - return 0; - }; - register_native_property_default_inner(ctx, property_key_ptr, property_key_len, default) -} - -/// Runs native property string-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_property_default_string`; invalid -/// handles, names, or string buffers fail closed as `false`. -unsafe fn register_native_property_default_string_inner( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - default_ptr: *const u8, - default_len: u64, -) -> i32 { - let Ok(default) = abi_name_to_string(default_ptr, default_len) else { - return 0; - }; - register_native_property_default_inner( - ctx, - property_key_ptr, - property_key_len, - NativeCallableDefault::String(default), - ) -} - -/// Runs native property array-default registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_property_default_array`; invalid -/// handles, names, or array specs fail closed as `false`. -unsafe fn register_native_property_default_array_inner( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - spec_ptr: *const u8, - spec_len: u64, -) -> i32 { - let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { - return 0; - }; - register_native_property_default_inner(ctx, property_key_ptr, property_key_len, default) -} - -/// Records a native property default in the property metadata table. -/// -/// # Safety -/// `ctx` and `property_key_ptr` must be valid for their declared use; callers -/// are the exported ABI wrappers above. -unsafe fn register_native_property_default_inner( - ctx: *mut ElephcEvalContext, - property_key_ptr: *const u8, - property_key_len: u64, - default: NativeCallableDefault, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { - return 0; - }; - let Some((class_name, property_name)) = split_property_key(&property_key) else { - return 0; - }; - i32::from(context.define_native_property_default(class_name, property_name, default)) -} - -/// Runs native member-attribute registration after installing a panic boundary. -/// -/// # Safety -/// Mirrors `__elephc_eval_register_native_member_attribute`; invalid handles or -/// binary records fail closed as `false`. -unsafe fn register_native_member_attribute_inner( - ctx: *mut ElephcEvalContext, - record_ptr: *const u8, - record_len: u64, -) -> i32 { - let Some(context) = ctx.as_mut() else { - return 0; - }; - if context.abi_version() != ABI_VERSION { - return 0; - } - let Some(record) = native_member_attribute_record_from_abi(record_ptr, record_len) else { - return 0; - }; - match record.owner_kind { - NATIVE_MEMBER_ATTRIBUTE_CLASS => { - i32::from(context.define_native_class_attribute(&record.member_key, record.attribute)) - } - NATIVE_MEMBER_ATTRIBUTE_METHOD - | NATIVE_MEMBER_ATTRIBUTE_PROPERTY - | NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => { - let Some((class_name, member_name)) = split_method_key(&record.member_key) else { - return 0; - }; - match record.owner_kind { - NATIVE_MEMBER_ATTRIBUTE_METHOD => i32::from(context.define_native_method_attribute( - class_name, - member_name, - record.attribute, - )), - NATIVE_MEMBER_ATTRIBUTE_PROPERTY => i32::from( - context.define_native_property_attribute(class_name, member_name, record.attribute), - ), - NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => { - i32::from(context.define_native_constant_attribute( - class_name, - member_name, - record.attribute, - )) - } - _ => 0, - } - } - _ => 0, - } -} - -/// Decoded native member-attribute metadata record. -struct NativeMemberAttributeRecord { - owner_kind: u8, - member_key: String, - attribute: EvalAttribute, -} - -/// Decodes one generated native member-attribute metadata record. -fn native_member_attribute_record_from_abi( - record_ptr: *const u8, - record_len: u64, -) -> Option { - if record_ptr.is_null() || record_len == 0 { - return None; - } - let record_len = usize::try_from(record_len).ok()?; - let bytes = unsafe { std::slice::from_raw_parts(record_ptr, record_len) }; - let mut offset = 0usize; - let owner_kind = native_attribute_take_u8(bytes, &mut offset)?; - let member_key = native_attribute_take_string(bytes, &mut offset)?; - let attribute_name = native_attribute_take_string(bytes, &mut offset)?; - let args = native_attribute_take_args(bytes, &mut offset)?; - (offset == bytes.len()).then_some(NativeMemberAttributeRecord { - owner_kind, - member_key, - attribute: EvalAttribute::new(attribute_name, args), - }) -} - -/// Decodes the optional argument vector from a native attribute record. -fn native_attribute_take_args( - bytes: &[u8], - offset: &mut usize, -) -> Option>> { - match native_attribute_take_u8(bytes, offset)? { - NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED => Some(None), - NATIVE_ATTRIBUTE_ARGS_SUPPORTED => { - let count = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; - let mut args = Vec::with_capacity(count); - for _ in 0..count { - args.push(native_attribute_take_arg(bytes, offset)?); - } - Some(Some(args)) - } - _ => None, - } -} - -/// Decodes one literal argument from a native attribute record. -fn native_attribute_take_arg(bytes: &[u8], offset: &mut usize) -> Option { - match native_attribute_take_u8(bytes, offset)? { - NATIVE_ATTRIBUTE_ARG_NULL => Some(EvalAttributeArg::Null), - NATIVE_ATTRIBUTE_ARG_BOOL => Some(EvalAttributeArg::Bool( - native_attribute_take_u8(bytes, offset)? != 0, - )), - NATIVE_ATTRIBUTE_ARG_INT => Some(EvalAttributeArg::Int(native_attribute_take_i64( - bytes, offset, - )?)), - NATIVE_ATTRIBUTE_ARG_FLOAT => Some(EvalAttributeArg::Float( - native_attribute_take_u64(bytes, offset)?, - )), - NATIVE_ATTRIBUTE_ARG_STRING => { - native_attribute_take_string(bytes, offset).map(EvalAttributeArg::String) - } - NATIVE_ATTRIBUTE_ARG_NAMED => { - let name = native_attribute_take_string(bytes, offset)?; - let value = native_attribute_take_arg(bytes, offset)?; - Some(EvalAttributeArg::Named { - name, - value: Box::new(value), - }) - } - NATIVE_ATTRIBUTE_ARG_ARRAY => { - let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; - let mut elements = Vec::with_capacity(len); - for _ in 0..len { - elements.push(native_attribute_take_arg(bytes, offset)?); - } - Some(EvalAttributeArg::Array(elements)) - } - _ => None, - } -} - -/// Reads one UTF-8 string with a little-endian u32 byte length prefix. -fn native_attribute_take_string(bytes: &[u8], offset: &mut usize) -> Option { - let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; - let chunk = native_attribute_take_bytes(bytes, offset, len)?; - std::str::from_utf8(chunk).ok().map(str::to_string) -} - -/// Reads one little-endian i64 from a native attribute record. -fn native_attribute_take_i64(bytes: &[u8], offset: &mut usize) -> Option { - let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; - Some(i64::from_le_bytes(chunk.try_into().ok()?)) -} - -/// Reads one little-endian u32 from a native attribute record. -fn native_attribute_take_u32(bytes: &[u8], offset: &mut usize) -> Option { - let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; - Some(u32::from_le_bytes(chunk.try_into().ok()?)) -} - -/// Reads one byte from a native attribute record. -fn native_attribute_take_u8(bytes: &[u8], offset: &mut usize) -> Option { - native_attribute_take_bytes(bytes, offset, 1).map(|chunk| chunk[0]) -} - -/// Reads one bounded byte slice and advances the decode offset. -fn native_attribute_take_bytes<'a>( - bytes: &'a [u8], - offset: &mut usize, - len: usize, -) -> Option<&'a [u8]> { - let end = offset.checked_add(len)?; - let chunk = bytes.get(*offset..end)?; - *offset = end; - Some(chunk) -} - -/// Decodes tagged default kind/payload ABI fields into native callable metadata. -pub(super) fn native_callable_scalar_default( - default_kind: u64, - default_payload: u64, -) -> Option { - match default_kind { - NATIVE_DEFAULT_NULL => Some(NativeCallableDefault::Null), - NATIVE_DEFAULT_BOOL => Some(NativeCallableDefault::Bool(default_payload != 0)), - NATIVE_DEFAULT_INT => Some(NativeCallableDefault::Int(default_payload as i64)), - NATIVE_DEFAULT_FLOAT => Some(NativeCallableDefault::Float(f64::from_bits( - default_payload, - ))), - NATIVE_DEFAULT_EMPTY_ARRAY => Some(NativeCallableDefault::EmptyArray), - _ => None, - } -} - -/// Decodes an object-valued native callable default from a generated binary spec. -/// -/// # Safety -/// `spec_ptr` must be readable for `spec_len` bytes when non-null. -pub(super) unsafe fn native_callable_object_default( - spec_ptr: *const u8, - spec_len: u64, -) -> Option { - let len = usize::try_from(spec_len).ok()?; - let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; - let mut offset = 0; - let default = native_callable_object_default_from_bytes(bytes, &mut offset)?; - (offset == bytes.len()).then_some(default) -} - -/// Decodes an array-valued native callable default from a generated binary spec. -/// -/// # Safety -/// `spec_ptr` must be readable for `spec_len` bytes when non-null. -pub(super) unsafe fn native_callable_array_default( - spec_ptr: *const u8, - spec_len: u64, -) -> Option { - let len = usize::try_from(spec_len).ok()?; - let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; - let mut offset = 0; - let default = native_callable_array_default_from_bytes(bytes, &mut offset)?; - (offset == bytes.len()).then_some(default) -} - -/// Decodes an object-valued native callable default from a generated binary spec slice. -fn native_callable_object_default_from_bytes( - bytes: &[u8], - offset: &mut usize, -) -> Option { - let class_name = native_attribute_take_string(bytes, offset)?; - let arg_count = usize::from(native_attribute_take_u8(bytes, offset)?); - if arg_count > MAX_NATIVE_OBJECT_DEFAULT_ARGS { - return None; - } - let mut args = Vec::with_capacity(arg_count); - for _ in 0..arg_count { - args.push(native_callable_object_default_arg(bytes, offset)?); - } - Some(NativeCallableDefault::Object { class_name, args }) -} - -/// Decodes an array-valued native callable default from a generated binary spec slice. -fn native_callable_array_default_from_bytes( - bytes: &[u8], - offset: &mut usize, -) -> Option { - let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; - let mut elements = Vec::with_capacity(len); - for _ in 0..len { - elements.push(native_callable_array_default_element(bytes, offset)?); - } - Some(NativeCallableDefault::Array(elements)) -} - -/// Decodes one array-default element and its optional static key. -fn native_callable_array_default_element( - bytes: &[u8], - offset: &mut usize, -) -> Option { - let key = match native_attribute_take_u8(bytes, offset)? { - NATIVE_ARRAY_DEFAULT_KEY_AUTO => None, - NATIVE_ARRAY_DEFAULT_KEY_INT => { - Some(NativeCallableArrayDefaultKey::Int(native_attribute_take_i64( - bytes, offset, - )?)) - } - NATIVE_ARRAY_DEFAULT_KEY_STRING => Some(NativeCallableArrayDefaultKey::String( - native_attribute_take_string(bytes, offset)?, - )), - _ => return None, - }; - let value = native_callable_object_default_arg_value(bytes, offset)?; - Some(NativeCallableArrayDefaultElement { key, value }) -} - -/// Decodes one object-default constructor argument from a generated binary spec. -fn native_callable_object_default_arg( - bytes: &[u8], - offset: &mut usize, -) -> Option { - let tag = native_attribute_take_u8(bytes, offset)?; - if tag == NATIVE_OBJECT_DEFAULT_ARG_NAMED { - let name = native_attribute_take_string(bytes, offset)?; - let value = native_callable_object_default_arg_value(bytes, offset)?; - return Some(NativeCallableObjectDefaultArg::named(name, value)); - } - native_callable_object_default_arg_value_for_tag(tag, bytes, offset) - .map(NativeCallableObjectDefaultArg::positional) -} - -/// Decodes one object-default constructor argument value from a generated binary spec. -fn native_callable_object_default_arg_value( - bytes: &[u8], - offset: &mut usize, -) -> Option { - let tag = native_attribute_take_u8(bytes, offset)?; - native_callable_object_default_arg_value_for_tag(tag, bytes, offset) -} - -/// Decodes one tagged object-default constructor argument value. -fn native_callable_object_default_arg_value_for_tag( - tag: u8, - bytes: &[u8], - offset: &mut usize, -) -> Option { - match tag { - NATIVE_OBJECT_DEFAULT_ARG_SCALAR => { - let kind = native_attribute_take_u64(bytes, offset)?; - let payload = native_attribute_take_u64(bytes, offset)?; - native_callable_scalar_default(kind, payload) - } - NATIVE_OBJECT_DEFAULT_ARG_STRING => { - native_attribute_take_string(bytes, offset).map(NativeCallableDefault::String) - } - NATIVE_OBJECT_DEFAULT_ARG_OBJECT => native_callable_object_default_from_bytes(bytes, offset), - NATIVE_OBJECT_DEFAULT_ARG_ARRAY => native_callable_array_default_from_bytes(bytes, offset), - _ => None, - } -} - -/// Reads one little-endian u64 from a native binary metadata record. -fn native_attribute_take_u64(bytes: &[u8], offset: &mut usize) -> Option { - let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; - Some(u64::from_le_bytes(chunk.try_into().ok()?)) -} - -/// Decodes one generated type-spec string into eval Reflection type metadata. -pub(super) fn native_callable_type_from_abi( - type_spec_ptr: *const u8, - type_spec_len: u64, - position: NativeCallableTypePosition, -) -> Option { - let type_spec = abi_name_to_string(type_spec_ptr, type_spec_len).ok()?; - native_callable_type_from_spec(&type_spec, position) -} - -/// Parses the compact generated type syntax used by native signature registration. -fn native_callable_type_from_spec( - type_spec: &str, - position: NativeCallableTypePosition, -) -> Option { - let type_spec = type_spec.trim(); - if type_spec.is_empty() { - return None; - } - let nullable_shorthand = type_spec.strip_prefix('?'); - let (type_spec, mut allows_null) = match nullable_shorthand { - Some(inner) => (inner, true), - None => (type_spec, false), - }; - if type_spec.contains('&') { - if allows_null || type_spec.contains('|') { - return None; - } - let variants = type_spec - .split('&') - .map(|member| native_callable_type_variant(member, position)) - .collect::>>()?; - if variants.iter().any(Option::is_none) { - return None; - } - return Some(EvalParameterType::intersection( - variants.into_iter().flatten().collect(), - )); - } - let mut variants = Vec::new(); - for member in type_spec.split('|') { - match native_callable_type_variant(member, position)? { - Some(variant) => variants.push(variant), - None => allows_null = true, - } - } - if variants.is_empty() { - return None; - } - Some(EvalParameterType::new(variants, allows_null)) -} - -/// Converts one generated type member name into eval type metadata. -fn native_callable_type_variant( - member: &str, - position: NativeCallableTypePosition, -) -> Option> { - let member = member.trim(); - if member.is_empty() { - return None; - } - let lower = member.trim_start_matches('\\').to_ascii_lowercase(); - let variant = match lower.as_str() { - "array" => EvalParameterTypeVariant::Array, - "bool" => EvalParameterTypeVariant::Bool, - "callable" => EvalParameterTypeVariant::Callable, - "float" => EvalParameterTypeVariant::Float, - "int" => EvalParameterTypeVariant::Int, - "iterable" => EvalParameterTypeVariant::Iterable, - "mixed" => EvalParameterTypeVariant::Mixed, - "never" if matches!(position, NativeCallableTypePosition::Return) => { - EvalParameterTypeVariant::Never - } - "null" => return Some(None), - "object" => EvalParameterTypeVariant::Object, - "string" => EvalParameterTypeVariant::String, - "void" if matches!(position, NativeCallableTypePosition::Return) => { - EvalParameterTypeVariant::Void - } - "void" | "never" => return None, - "self" | "parent" | "static" => EvalParameterTypeVariant::Class(lower), - _ => EvalParameterTypeVariant::Class(member.trim_start_matches('\\').to_string()), - }; - Some(Some(variant)) -} - -/// Splits one generated `ClassName::methodName` metadata key into class and method pieces. -fn split_method_key(method_key: &str) -> Option<(&str, &str)> { - let (class_name, method_name) = method_key.rsplit_once("::")?; - (!class_name.is_empty() && !method_name.is_empty()).then_some((class_name, method_name)) -} - -/// Splits one generated `ClassName::propertyName` metadata key into class and property pieces. -fn split_property_key(property_key: &str) -> Option<(&str, &str)> { - split_method_key(property_key) -} - -/// Splits `ClassLike::DeclaringClassLike::propertyName` property metadata keys. -fn split_three_part_property_key(property_key: &str) -> Option<(&str, &str, &str)> { - let (owner_key, property_name) = property_key.rsplit_once("::")?; - let (class_like_name, declaring_class_like_name) = owner_key.rsplit_once("::")?; - (!class_like_name.is_empty() - && !declaring_class_like_name.is_empty() - && !property_name.is_empty()) - .then_some((class_like_name, declaring_class_like_name, property_name)) -} diff --git a/crates/elephc-magician/src/ffi/native_methods/attribute_decoder.rs b/crates/elephc-magician/src/ffi/native_methods/attribute_decoder.rs new file mode 100644 index 0000000000..cf3a6709e7 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/attribute_decoder.rs @@ -0,0 +1,131 @@ +//! Purpose: +//! Decodes bounded binary member-attribute records emitted by generated native +//! code into eval attribute metadata. +//! +//! Called from: +//! - `super::property_registration` while registering class-member attributes. +//! +//! Key details: +//! - Every read is bounds checked and malformed or trailing data is rejected. + +use super::*; + +/// Decoded native member-attribute metadata record. +pub(super) struct NativeMemberAttributeRecord { + pub(super) owner_kind: u8, + pub(super) member_key: String, + pub(super) attribute: EvalAttribute, +} + +/// Decodes one generated native member-attribute metadata record. +pub(super) fn native_member_attribute_record_from_abi( + record_ptr: *const u8, + record_len: u64, +) -> Option { + if record_ptr.is_null() || record_len == 0 { + return None; + } + let record_len = usize::try_from(record_len).ok()?; + let bytes = unsafe { std::slice::from_raw_parts(record_ptr, record_len) }; + let mut offset = 0usize; + let owner_kind = native_attribute_take_u8(bytes, &mut offset)?; + let member_key = native_attribute_take_string(bytes, &mut offset)?; + let attribute_name = native_attribute_take_string(bytes, &mut offset)?; + let args = native_attribute_take_args(bytes, &mut offset)?; + (offset == bytes.len()).then_some(NativeMemberAttributeRecord { + owner_kind, + member_key, + attribute: EvalAttribute::new(attribute_name, args), + }) +} + +/// Decodes the optional argument vector from a native attribute record. +fn native_attribute_take_args( + bytes: &[u8], + offset: &mut usize, +) -> Option>> { + match native_attribute_take_u8(bytes, offset)? { + NATIVE_ATTRIBUTE_ARGS_UNSUPPORTED => Some(None), + NATIVE_ATTRIBUTE_ARGS_SUPPORTED => { + let count = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut args = Vec::with_capacity(count); + for _ in 0..count { + args.push(native_attribute_take_arg(bytes, offset)?); + } + Some(Some(args)) + } + _ => None, + } +} + +/// Decodes one literal argument from a native attribute record. +fn native_attribute_take_arg(bytes: &[u8], offset: &mut usize) -> Option { + match native_attribute_take_u8(bytes, offset)? { + NATIVE_ATTRIBUTE_ARG_NULL => Some(EvalAttributeArg::Null), + NATIVE_ATTRIBUTE_ARG_BOOL => Some(EvalAttributeArg::Bool( + native_attribute_take_u8(bytes, offset)? != 0, + )), + NATIVE_ATTRIBUTE_ARG_INT => Some(EvalAttributeArg::Int(native_attribute_take_i64( + bytes, offset, + )?)), + NATIVE_ATTRIBUTE_ARG_FLOAT => Some(EvalAttributeArg::Float( + native_attribute_take_u64(bytes, offset)?, + )), + NATIVE_ATTRIBUTE_ARG_STRING => { + native_attribute_take_string(bytes, offset).map(EvalAttributeArg::String) + } + NATIVE_ATTRIBUTE_ARG_NAMED => { + let name = native_attribute_take_string(bytes, offset)?; + let value = native_attribute_take_arg(bytes, offset)?; + Some(EvalAttributeArg::Named { + name, + value: Box::new(value), + }) + } + NATIVE_ATTRIBUTE_ARG_ARRAY => { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut elements = Vec::with_capacity(len); + for _ in 0..len { + elements.push(native_attribute_take_arg(bytes, offset)?); + } + Some(EvalAttributeArg::Array(elements)) + } + _ => None, + } +} + +/// Reads one UTF-8 string with a little-endian u32 byte length prefix. +pub(super) fn native_attribute_take_string(bytes: &[u8], offset: &mut usize) -> Option { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let chunk = native_attribute_take_bytes(bytes, offset, len)?; + std::str::from_utf8(chunk).ok().map(str::to_string) +} + +/// Reads one little-endian i64 from a native attribute record. +pub(super) fn native_attribute_take_i64(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(i64::from_le_bytes(chunk.try_into().ok()?)) +} + +/// Reads one little-endian u32 from a native attribute record. +pub(super) fn native_attribute_take_u32(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(u32::from_le_bytes(chunk.try_into().ok()?)) +} + +/// Reads one byte from a native attribute record. +pub(super) fn native_attribute_take_u8(bytes: &[u8], offset: &mut usize) -> Option { + native_attribute_take_bytes(bytes, offset, 1).map(|chunk| chunk[0]) +} + +/// Reads one bounded byte slice and advances the decode offset. +pub(super) fn native_attribute_take_bytes<'a>( + bytes: &'a [u8], + offset: &mut usize, + len: usize, +) -> Option<&'a [u8]> { + let end = offset.checked_add(len)?; + let chunk = bytes.get(*offset..end)?; + *offset = end; + Some(chunk) +} diff --git a/crates/elephc-magician/src/ffi/native_methods/callable_metadata.rs b/crates/elephc-magician/src/ffi/native_methods/callable_metadata.rs new file mode 100644 index 0000000000..b7ba197da6 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/callable_metadata.rs @@ -0,0 +1,270 @@ +//! Purpose: +//! Decodes generated callable type and default-value ABI metadata and splits +//! class-member registration keys. +//! +//! Called from: +//! - Native method and constructor registration helpers in this module tree. +//! - `crate::ffi::native_functions` for shared callable metadata decoding. +//! +//! Key details: +//! - Nested object and array defaults are decoded recursively with strict bounds. + +use super::*; + +/// Decodes tagged default kind/payload ABI fields into native callable metadata. +pub(in crate::ffi) fn native_callable_scalar_default( + default_kind: u64, + default_payload: u64, +) -> Option { + match default_kind { + NATIVE_DEFAULT_NULL => Some(NativeCallableDefault::Null), + NATIVE_DEFAULT_BOOL => Some(NativeCallableDefault::Bool(default_payload != 0)), + NATIVE_DEFAULT_INT => Some(NativeCallableDefault::Int(default_payload as i64)), + NATIVE_DEFAULT_FLOAT => Some(NativeCallableDefault::Float(f64::from_bits( + default_payload, + ))), + NATIVE_DEFAULT_EMPTY_ARRAY => Some(NativeCallableDefault::EmptyArray), + _ => None, + } +} + +/// Decodes an object-valued native callable default from a generated binary spec. +/// +/// # Safety +/// `spec_ptr` must be readable for `spec_len` bytes when non-null. +pub(in crate::ffi) unsafe fn native_callable_object_default( + spec_ptr: *const u8, + spec_len: u64, +) -> Option { + let len = usize::try_from(spec_len).ok()?; + let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; + let mut offset = 0; + let default = native_callable_object_default_from_bytes(bytes, &mut offset)?; + (offset == bytes.len()).then_some(default) +} + +/// Decodes an array-valued native callable default from a generated binary spec. +/// +/// # Safety +/// `spec_ptr` must be readable for `spec_len` bytes when non-null. +pub(in crate::ffi) unsafe fn native_callable_array_default( + spec_ptr: *const u8, + spec_len: u64, +) -> Option { + let len = usize::try_from(spec_len).ok()?; + let bytes = (!spec_ptr.is_null()).then(|| std::slice::from_raw_parts(spec_ptr, len))?; + let mut offset = 0; + let default = native_callable_array_default_from_bytes(bytes, &mut offset)?; + (offset == bytes.len()).then_some(default) +} + +/// Decodes an object-valued native callable default from a generated binary spec slice. +fn native_callable_object_default_from_bytes( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let class_name = native_attribute_take_string(bytes, offset)?; + let arg_count = usize::from(native_attribute_take_u8(bytes, offset)?); + if arg_count > MAX_NATIVE_OBJECT_DEFAULT_ARGS { + return None; + } + let mut args = Vec::with_capacity(arg_count); + for _ in 0..arg_count { + args.push(native_callable_object_default_arg(bytes, offset)?); + } + Some(NativeCallableDefault::Object { class_name, args }) +} + +/// Decodes an array-valued native callable default from a generated binary spec slice. +fn native_callable_array_default_from_bytes( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let len = usize::try_from(native_attribute_take_u32(bytes, offset)?).ok()?; + let mut elements = Vec::with_capacity(len); + for _ in 0..len { + elements.push(native_callable_array_default_element(bytes, offset)?); + } + Some(NativeCallableDefault::Array(elements)) +} + +/// Decodes one array-default element and its optional static key. +fn native_callable_array_default_element( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let key = match native_attribute_take_u8(bytes, offset)? { + NATIVE_ARRAY_DEFAULT_KEY_AUTO => None, + NATIVE_ARRAY_DEFAULT_KEY_INT => { + Some(NativeCallableArrayDefaultKey::Int(native_attribute_take_i64( + bytes, offset, + )?)) + } + NATIVE_ARRAY_DEFAULT_KEY_STRING => Some(NativeCallableArrayDefaultKey::String( + native_attribute_take_string(bytes, offset)?, + )), + _ => return None, + }; + let value = native_callable_object_default_arg_value(bytes, offset)?; + Some(NativeCallableArrayDefaultElement { key, value }) +} + +/// Decodes one object-default constructor argument from a generated binary spec. +fn native_callable_object_default_arg( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let tag = native_attribute_take_u8(bytes, offset)?; + if tag == NATIVE_OBJECT_DEFAULT_ARG_NAMED { + let name = native_attribute_take_string(bytes, offset)?; + let value = native_callable_object_default_arg_value(bytes, offset)?; + return Some(NativeCallableObjectDefaultArg::named(name, value)); + } + native_callable_object_default_arg_value_for_tag(tag, bytes, offset) + .map(NativeCallableObjectDefaultArg::positional) +} + +/// Decodes one object-default constructor argument value from a generated binary spec. +fn native_callable_object_default_arg_value( + bytes: &[u8], + offset: &mut usize, +) -> Option { + let tag = native_attribute_take_u8(bytes, offset)?; + native_callable_object_default_arg_value_for_tag(tag, bytes, offset) +} + +/// Decodes one tagged object-default constructor argument value. +fn native_callable_object_default_arg_value_for_tag( + tag: u8, + bytes: &[u8], + offset: &mut usize, +) -> Option { + match tag { + NATIVE_OBJECT_DEFAULT_ARG_SCALAR => { + let kind = native_attribute_take_u64(bytes, offset)?; + let payload = native_attribute_take_u64(bytes, offset)?; + native_callable_scalar_default(kind, payload) + } + NATIVE_OBJECT_DEFAULT_ARG_STRING => { + native_attribute_take_string(bytes, offset).map(NativeCallableDefault::String) + } + NATIVE_OBJECT_DEFAULT_ARG_OBJECT => native_callable_object_default_from_bytes(bytes, offset), + NATIVE_OBJECT_DEFAULT_ARG_ARRAY => native_callable_array_default_from_bytes(bytes, offset), + _ => None, + } +} + +/// Reads one little-endian u64 from a native binary metadata record. +pub(super) fn native_attribute_take_u64(bytes: &[u8], offset: &mut usize) -> Option { + let chunk = native_attribute_take_bytes(bytes, offset, std::mem::size_of::())?; + Some(u64::from_le_bytes(chunk.try_into().ok()?)) +} + +/// Decodes one generated type-spec string into eval Reflection type metadata. +pub(in crate::ffi) fn native_callable_type_from_abi( + type_spec_ptr: *const u8, + type_spec_len: u64, + position: NativeCallableTypePosition, +) -> Option { + let type_spec = abi_name_to_string(type_spec_ptr, type_spec_len).ok()?; + native_callable_type_from_spec(&type_spec, position) +} + +/// Parses the compact generated type syntax used by native signature registration. +fn native_callable_type_from_spec( + type_spec: &str, + position: NativeCallableTypePosition, +) -> Option { + let type_spec = type_spec.trim(); + if type_spec.is_empty() { + return None; + } + let nullable_shorthand = type_spec.strip_prefix('?'); + let (type_spec, mut allows_null) = match nullable_shorthand { + Some(inner) => (inner, true), + None => (type_spec, false), + }; + if type_spec.contains('&') { + if allows_null || type_spec.contains('|') { + return None; + } + let variants = type_spec + .split('&') + .map(|member| native_callable_type_variant(member, position)) + .collect::>>()?; + if variants.iter().any(Option::is_none) { + return None; + } + return Some(EvalParameterType::intersection( + variants.into_iter().flatten().collect(), + )); + } + let mut variants = Vec::new(); + for member in type_spec.split('|') { + match native_callable_type_variant(member, position)? { + Some(variant) => variants.push(variant), + None => allows_null = true, + } + } + if variants.is_empty() { + return None; + } + Some(EvalParameterType::new(variants, allows_null)) +} + +/// Converts one generated type member name into eval type metadata. +fn native_callable_type_variant( + member: &str, + position: NativeCallableTypePosition, +) -> Option> { + let member = member.trim(); + if member.is_empty() { + return None; + } + let lower = member.trim_start_matches('\\').to_ascii_lowercase(); + let variant = match lower.as_str() { + "array" => EvalParameterTypeVariant::Array, + "bool" => EvalParameterTypeVariant::Bool, + "callable" => EvalParameterTypeVariant::Callable, + "float" => EvalParameterTypeVariant::Float, + "int" => EvalParameterTypeVariant::Int, + "iterable" => EvalParameterTypeVariant::Iterable, + "mixed" => EvalParameterTypeVariant::Mixed, + "never" if matches!(position, NativeCallableTypePosition::Return) => { + EvalParameterTypeVariant::Never + } + "null" => return Some(None), + "object" => EvalParameterTypeVariant::Object, + "string" => EvalParameterTypeVariant::String, + "void" if matches!(position, NativeCallableTypePosition::Return) => { + EvalParameterTypeVariant::Void + } + "void" | "never" => return None, + "self" | "parent" | "static" => EvalParameterTypeVariant::Class(lower), + _ => EvalParameterTypeVariant::Class(member.trim_start_matches('\\').to_string()), + }; + Some(Some(variant)) +} + +/// Splits one generated `ClassName::methodName` metadata key into class and method pieces. +pub(super) fn split_method_key(method_key: &str) -> Option<(&str, &str)> { + let (class_name, method_name) = method_key.rsplit_once("::")?; + (!class_name.is_empty() && !method_name.is_empty()).then_some((class_name, method_name)) +} + +/// Splits one generated `ClassName::propertyName` metadata key into class and property pieces. +pub(super) fn split_property_key(property_key: &str) -> Option<(&str, &str)> { + split_method_key(property_key) +} + +/// Splits `ClassLike::DeclaringClassLike::propertyName` property metadata keys. +pub(super) fn split_three_part_property_key( + property_key: &str, +) -> Option<(&str, &str, &str)> { + let (owner_key, property_name) = property_key.rsplit_once("::")?; + let (class_like_name, declaring_class_like_name) = owner_key.rsplit_once("::")?; + (!class_like_name.is_empty() + && !declaring_class_like_name.is_empty() + && !property_name.is_empty()) + .then_some((class_like_name, declaring_class_like_name, property_name)) +} diff --git a/crates/elephc-magician/src/ffi/native_methods/constructor_registration.rs b/crates/elephc-magician/src/ffi/native_methods/constructor_registration.rs new file mode 100644 index 0000000000..28ff90acec --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/constructor_registration.rs @@ -0,0 +1,291 @@ +//! Purpose: +//! Validates and records generated constructor signatures, parameter metadata, +//! bridge support, and default values. +//! +//! Called from: +//! - `super::public_abi` constructor registration entry points. +//! +//! Key details: +//! - Registration rejects malformed names, unsupported defaults, and indexes +//! outside the declared constructor signature. + +use super::*; + +/// Runs native constructor registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor`; invalid handles, names, +/// or counts fail closed as `false`. +pub(super) unsafe fn register_native_constructor_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + i32::from(context.define_native_constructor_signature( + &class_name, + NativeCallableSignature::new(param_count), + )) +} + +/// Runs native constructor parameter registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param`; invalid handles, +/// names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_constructor_param(&class_name, param_index, param_name)) +} + +/// Runs native constructor parameter-flag registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_flags`; invalid +/// handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_flags_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if !context.define_native_constructor_param_by_ref(&class_name, param_index, is_by_ref != 0) { + return 0; + } + if is_variadic == 0 { + return 1; + } + i32::from(context.define_native_constructor_variadic_param(&class_name, param_index)) +} + +/// Runs native constructor bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_bridge_support`; invalid +/// handles or names fail closed as `false`. +pub(super) unsafe fn register_native_constructor_bridge_support_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + i32::from(context.define_native_constructor_bridge_supported(&class_name, supported != 0)) +} + +/// Runs native constructor parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_type`; invalid +/// handles, names, indexes, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_type_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_constructor_param_type(&class_name, param_index, param_type)) +} + +/// Runs native constructor scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_scalar`; +/// invalid handles, names, indexes, or default kinds fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + +/// Runs native constructor string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_string`; +/// invalid handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_default_string_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Runs native constructor object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_object`; +/// invalid handles, names, indexes, or object specs fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_default_object_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + +/// Runs native constructor array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_constructor_param_default_array`; +/// invalid handles, names, indexes, or array specs fail closed as `false`. +pub(super) unsafe fn register_native_constructor_param_default_array_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_constructor_param_default_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default, + ) +} + +/// Records a native constructor parameter default in the constructor signature table. +/// +/// # Safety +/// `ctx` and `class_name_ptr` must be valid for their declared use; callers are +/// the exported ABI wrappers above. +pub(super) unsafe fn register_native_constructor_param_default_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + i32::from(context.define_native_constructor_param_default(&class_name, param_index, default)) +} diff --git a/crates/elephc-magician/src/ffi/native_methods/method_registration.rs b/crates/elephc-magician/src/ffi/native_methods/method_registration.rs new file mode 100644 index 0000000000..5af44e38f9 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/method_registration.rs @@ -0,0 +1,532 @@ +//! Purpose: +//! Validates and records generated method, interface-property, and abstract- +//! property metadata after the public ABI panic boundary. +//! +//! Called from: +//! - `super::public_abi` registration entry points. +//! +//! Key details: +//! - Invalid handles, names, types, defaults, or parameter indexes fail closed. + +use super::*; + +/// Runs native method registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method`; invalid handles, names, or +/// counts fail closed as `false`. +pub(super) unsafe fn register_native_method_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_count: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_count) = usize::try_from(param_count) else { + return 0; + }; + let signature = NativeCallableSignature::new(param_count); + if is_static { + i32::from(context.define_native_static_method_signature(class_name, method_name, signature)) + } else { + i32::from(context.define_native_method_signature(class_name, method_name, signature)) + } +} + +/// Runs native interface property registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_interface_property`; invalid handles, +/// names, flags, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_interface_property_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((interface_name, declaring_interface_name, property_name)) = + split_three_part_property_key(&property_key) + else { + return 0; + }; + let requires_get = flags & NATIVE_PROPERTY_REQUIRES_GET != 0; + let requires_set = flags & NATIVE_PROPERTY_REQUIRES_SET != 0; + if !requires_get && !requires_set { + return 0; + } + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + let property = EvalInterfaceProperty::new(property_name, requires_get, requires_set) + .with_type(Some(property_type)); + i32::from(context.define_native_interface_property_requirement( + interface_name, + declaring_interface_name, + property, + )) +} + +/// Runs native abstract property registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_abstract_property`; invalid handles, +/// names, flags, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_abstract_property_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, declaring_class_name, property_name)) = + split_three_part_property_key(&property_key) + else { + return 0; + }; + let requires_get = flags & NATIVE_PROPERTY_REQUIRES_GET != 0; + let requires_set = flags & NATIVE_PROPERTY_REQUIRES_SET != 0; + if !requires_get && !requires_set { + return 0; + } + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + let property = EvalInterfaceProperty::new(property_name, requires_get, requires_set) + .with_type(Some(property_type)); + i32::from(context.define_native_abstract_property_requirement( + class_name, + declaring_class_name, + property, + )) +} + +/// Runs native method parameter registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param`; invalid handles, names, +/// or indexes fail closed as `false`. +pub(super) unsafe fn register_native_method_param_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_name) = abi_name_to_string(param_name_ptr, param_name_len) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param( + class_name, + method_name, + param_index, + param_name, + )) + } else { + i32::from(context.define_native_method_param( + class_name, + method_name, + param_index, + param_name, + )) + } +} + +/// Runs native method parameter-flag registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_flags`; invalid handles, +/// names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_method_param_flags_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let by_ref_registered = if is_static { + context.define_native_static_method_param_by_ref( + class_name, + method_name, + param_index, + is_by_ref != 0, + ) + } else { + context.define_native_method_param_by_ref( + class_name, + method_name, + param_index, + is_by_ref != 0, + ) + }; + if !by_ref_registered { + return 0; + } + if is_variadic == 0 { + return 1; + } + i32::from(if is_static { + context.define_native_static_method_variadic_param(class_name, method_name, param_index) + } else { + context.define_native_method_variadic_param(class_name, method_name, param_index) + }) +} + +/// Runs native method bridge-support registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_bridge_support`; invalid +/// handles or names fail closed as `false`. +pub(super) unsafe fn register_native_method_bridge_support_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + supported: i32, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + i32::from(if is_static { + context.define_native_static_method_bridge_supported( + class_name, + method_name, + supported != 0, + ) + } else { + context.define_native_method_bridge_supported(class_name, method_name, supported != 0) + }) +} + +/// Runs native method parameter-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_type`; invalid handles, +/// names, indexes, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_method_param_type_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + let Some(param_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param_type( + class_name, + method_name, + param_index, + param_type, + )) + } else { + i32::from(context.define_native_method_param_type( + class_name, + method_name, + param_index, + param_type, + )) + } +} + +/// Runs native method return-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_return_type`; invalid handles, +/// names, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_method_return_type_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Some(return_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Return, + ) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_return_type( + class_name, + method_name, + return_type, + )) + } else { + i32::from(context.define_native_method_return_type(class_name, method_name, return_type)) + } +} + +/// Runs native method scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_scalar`; invalid +/// handles, names, indexes, or default kinds fail closed as `false`. +pub(super) unsafe fn register_native_method_param_default_scalar_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + +/// Runs native method string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_string`; invalid +/// handles, names, or indexes fail closed as `false`. +pub(super) unsafe fn register_native_method_param_default_string_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + NativeCallableDefault::String(default), + ) +} + +/// Runs native method object-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_object`; invalid +/// handles, names, indexes, or object specs fail closed as `false`. +pub(super) unsafe fn register_native_method_param_default_object_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_object_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + +/// Runs native method array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_method_param_default_array`; invalid +/// handles, names, indexes, or array specs fail closed as `false`. +pub(super) unsafe fn register_native_method_param_default_array_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_method_param_default_inner( + ctx, + method_key_ptr, + method_key_len, + is_static, + param_index, + default, + ) +} + +/// Records a native method parameter default in the selected instance/static table. +/// +/// # Safety +/// `ctx` and `method_key_ptr` must be valid for their declared use; callers are +/// the exported ABI wrappers above. +pub(super) unsafe fn register_native_method_param_default_inner( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + is_static: bool, + param_index: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(method_key) = abi_name_to_string(method_key_ptr, method_key_len) else { + return 0; + }; + let Some((class_name, method_name)) = split_method_key(&method_key) else { + return 0; + }; + let Ok(param_index) = usize::try_from(param_index) else { + return 0; + }; + if is_static { + i32::from(context.define_native_static_method_param_default( + class_name, + method_name, + param_index, + default, + )) + } else { + i32::from(context.define_native_method_param_default( + class_name, + method_name, + param_index, + default, + )) + } +} diff --git a/crates/elephc-magician/src/ffi/native_methods/property_registration.rs b/crates/elephc-magician/src/ffi/native_methods/property_registration.rs new file mode 100644 index 0000000000..fc6a7d2152 --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/property_registration.rs @@ -0,0 +1,210 @@ +//! Purpose: +//! Validates and records native class parents, property contracts and defaults, +//! and generated class-member attributes. +//! +//! Called from: +//! - `super::public_abi` property and class metadata registration entry points. +//! +//! Key details: +//! - Property keys are decoded into their declaring class-like components before +//! context mutation. + +use super::*; + +/// Runs native parent-class registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_class_parent`; invalid handles or +/// names fail closed as `false`. +pub(super) unsafe fn register_native_class_parent_inner( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + parent_name_ptr: *const u8, + parent_name_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(class_name) = abi_name_to_string(class_name_ptr, class_name_len) else { + return 0; + }; + let Ok(parent_name) = abi_name_to_string(parent_name_ptr, parent_name_len) else { + return 0; + }; + i32::from(context.define_native_class_parent(&class_name, &parent_name)) +} + +/// Runs native property-type registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_type`; invalid handles, +/// names, or type specs fail closed as `false`. +pub(super) unsafe fn register_native_property_type_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, property_name)) = split_property_key(&property_key) else { + return 0; + }; + let Some(property_type) = native_callable_type_from_abi( + type_spec_ptr, + type_spec_len, + NativeCallableTypePosition::Parameter, + ) else { + return 0; + }; + i32::from(context.define_native_property_type(class_name, property_name, property_type)) +} + +/// Runs native property scalar-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_scalar`; invalid +/// handles, names, or default kinds fail closed as `false`. +pub(super) unsafe fn register_native_property_default_scalar_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + let Some(default) = native_callable_scalar_default(default_kind, default_payload) else { + return 0; + }; + register_native_property_default_inner(ctx, property_key_ptr, property_key_len, default) +} + +/// Runs native property string-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_string`; invalid +/// handles, names, or string buffers fail closed as `false`. +pub(super) unsafe fn register_native_property_default_string_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + let Ok(default) = abi_name_to_string(default_ptr, default_len) else { + return 0; + }; + register_native_property_default_inner( + ctx, + property_key_ptr, + property_key_len, + NativeCallableDefault::String(default), + ) +} + +/// Runs native property array-default registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_property_default_array`; invalid +/// handles, names, or array specs fail closed as `false`. +pub(super) unsafe fn register_native_property_default_array_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + let Some(default) = native_callable_array_default(spec_ptr, spec_len) else { + return 0; + }; + register_native_property_default_inner(ctx, property_key_ptr, property_key_len, default) +} + +/// Records a native property default in the property metadata table. +/// +/// # Safety +/// `ctx` and `property_key_ptr` must be valid for their declared use; callers +/// are the exported ABI wrappers above. +pub(super) unsafe fn register_native_property_default_inner( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default: NativeCallableDefault, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Ok(property_key) = abi_name_to_string(property_key_ptr, property_key_len) else { + return 0; + }; + let Some((class_name, property_name)) = split_property_key(&property_key) else { + return 0; + }; + i32::from(context.define_native_property_default(class_name, property_name, default)) +} + +/// Runs native member-attribute registration after installing a panic boundary. +/// +/// # Safety +/// Mirrors `__elephc_eval_register_native_member_attribute`; invalid handles or +/// binary records fail closed as `false`. +pub(super) unsafe fn register_native_member_attribute_inner( + ctx: *mut ElephcEvalContext, + record_ptr: *const u8, + record_len: u64, +) -> i32 { + let Some(context) = ctx.as_mut() else { + return 0; + }; + if context.abi_version() != ABI_VERSION { + return 0; + } + let Some(record) = native_member_attribute_record_from_abi(record_ptr, record_len) else { + return 0; + }; + match record.owner_kind { + NATIVE_MEMBER_ATTRIBUTE_CLASS => { + i32::from(context.define_native_class_attribute(&record.member_key, record.attribute)) + } + NATIVE_MEMBER_ATTRIBUTE_METHOD + | NATIVE_MEMBER_ATTRIBUTE_PROPERTY + | NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => { + let Some((class_name, member_name)) = split_method_key(&record.member_key) else { + return 0; + }; + match record.owner_kind { + NATIVE_MEMBER_ATTRIBUTE_METHOD => i32::from(context.define_native_method_attribute( + class_name, + member_name, + record.attribute, + )), + NATIVE_MEMBER_ATTRIBUTE_PROPERTY => i32::from( + context.define_native_property_attribute(class_name, member_name, record.attribute), + ), + NATIVE_MEMBER_ATTRIBUTE_CLASS_CONSTANT => { + i32::from(context.define_native_constant_attribute( + class_name, + member_name, + record.attribute, + )) + } + _ => 0, + } + } + _ => 0, + } +} diff --git a/crates/elephc-magician/src/ffi/native_methods/public_abi.rs b/crates/elephc-magician/src/ffi/native_methods/public_abi.rs new file mode 100644 index 0000000000..2e9bb9c38a --- /dev/null +++ b/crates/elephc-magician/src/ffi/native_methods/public_abi.rs @@ -0,0 +1,969 @@ +//! Purpose: +//! Exposes the C ABI entry points that register generated method, constructor, +//! property, interface, and attribute metadata in an eval context. +//! +//! Called from: +//! - Generated EIR backend assembly during native fragment registration. +//! +//! Key details: +//! - Every entry point installs a panic boundary and delegates validation to a +//! focused registration helper. + +use super::*; + +/// Registers a generated native PHP method signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `method_key_ptr` must point to a +/// readable `ClassName::methodName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_inner(ctx, method_key_ptr, method_key_len, false, param_count) + }) + .unwrap_or(0) +} + +/// Registers a generated native PHP static-method signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `method_key_ptr` must point to a +/// readable `ClassName::methodName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_inner(ctx, method_key_ptr, method_key_len, true, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP interface property contract in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a +/// readable `InterfaceName::DeclaringInterface::propertyName` byte string, and +/// `type_spec_ptr` must point to a readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_interface_property( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_interface_property_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + flags, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP abstract class property contract in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `property_key_ptr` must point to a +/// readable `ClassName::DeclaringClass::propertyName` byte string, and +/// `type_spec_ptr` must point to a readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_abstract_property( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, + flags: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_abstract_property_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + flags, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and parameter name +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP method parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_flags( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_flags_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP static-method parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_flags( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_flags_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP method. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_bridge_support( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_bridge_support_inner( + ctx, + method_key_ptr, + method_key_len, + false, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP static method. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The method key must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_bridge_support( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_bridge_support_inner( + ctx, + method_key_ptr, + method_key_len, + true, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method parameter declared type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_type_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method parameter declared type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_type_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method declared return type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_return_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_return_type_inner( + ctx, + method_key_ptr, + method_key_len, + false, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method declared return type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and type-spec pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_return_type( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_return_type_inner( + ctx, + method_key_ptr, + method_key_len, + true, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_scalar( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_scalar_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_scalar( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_scalar_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_string( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_string_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_string( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_string_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_object( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_object_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_object( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_object_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP method array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_method_param_default_array( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_array_inner( + ctx, + method_key_ptr, + method_key_len, + false, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP static-method array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Method key and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_static_method_param_default_array( + ctx: *mut ElephcEvalContext, + method_key_ptr: *const u8, + method_key_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_method_param_default_array_inner( + ctx, + method_key_ptr, + method_key_len, + true, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers a generated native PHP constructor signature in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `class_name_ptr` must be readable +/// for `class_name_len` bytes. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_count: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_inner(ctx, class_name_ptr, class_name_len, param_count) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor parameter name in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and parameter name pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + param_name_ptr: *const u8, + param_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + param_name_ptr, + param_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP constructor parameter flags in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class name must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_flags( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + is_by_ref: i32, + is_variadic: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_flags_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + is_by_ref, + is_variadic, + ) + }) + .unwrap_or(0) +} + +/// Registers whether generated eval may dispatch a native PHP constructor. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The class name must be readable +/// for its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_bridge_support( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + supported: i32, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_bridge_support_inner( + ctx, + class_name_ptr, + class_name_len, + supported, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor parameter declared type. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and type-spec pointers must +/// be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_type( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_type_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor scalar parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name must be readable for +/// its declared byte length. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_scalar( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_scalar_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor string parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and default string +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_string( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_string_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor object parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_object( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_object_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP constructor array parameter default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class name and encoded default +/// pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_constructor_param_default_array( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + param_index: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_constructor_param_default_array_inner( + ctx, + class_name_ptr, + class_name_len, + param_index, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers generated native PHP parent-class metadata in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. Class and parent name pointers +/// must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_class_parent( + ctx: *mut ElephcEvalContext, + class_name_ptr: *const u8, + class_name_len: u64, + parent_name_ptr: *const u8, + parent_name_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_class_parent_inner( + ctx, + class_name_ptr, + class_name_len, + parent_name_ptr, + parent_name_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property type in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key must be a +/// readable `ClassName::propertyName` byte string, and the type spec must be a +/// readable generated type-spec byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_type( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + type_spec_ptr: *const u8, + type_spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_type_inner( + ctx, + property_key_ptr, + property_key_len, + type_spec_ptr, + type_spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property scalar default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key must be a +/// readable `ClassName::propertyName` byte string. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_scalar( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_kind: u64, + default_payload: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_scalar_inner( + ctx, + property_key_ptr, + property_key_len, + default_kind, + default_payload, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property string default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key and default +/// string pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_string( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + default_ptr: *const u8, + default_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_string_inner( + ctx, + property_key_ptr, + property_key_len, + default_ptr, + default_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP property array default in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. The property key and encoded +/// default pointers must be readable for their declared byte lengths. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_property_default_array( + ctx: *mut ElephcEvalContext, + property_key_ptr: *const u8, + property_key_len: u64, + spec_ptr: *const u8, + spec_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_property_default_array_inner( + ctx, + property_key_ptr, + property_key_len, + spec_ptr, + spec_len, + ) + }) + .unwrap_or(0) +} + +/// Registers one generated native PHP class/member attribute in an eval context. +/// +/// # Safety +/// `ctx` must be a valid eval context handle. `record_ptr` must point to one +/// readable binary member-attribute metadata record. +#[no_mangle] +pub unsafe extern "C" fn __elephc_eval_register_native_member_attribute( + ctx: *mut ElephcEvalContext, + record_ptr: *const u8, + record_len: u64, +) -> i32 { + std::panic::catch_unwind(|| unsafe { + register_native_member_attribute_inner(ctx, record_ptr, record_len) + }) + .unwrap_or(0) +} diff --git a/crates/elephc-magician/src/ffi/tests.rs b/crates/elephc-magician/src/ffi/tests.rs deleted file mode 100644 index 5848d7bd4a..0000000000 --- a/crates/elephc-magician/src/ffi/tests.rs +++ /dev/null @@ -1,1651 +0,0 @@ -//! Purpose: -//! Unit tests for the eval C ABI layer. -//! They validate handle allocation, stable status codes, scope flags, and -//! dynamic symbol registration without requiring generated runtime assembly. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - Execute remains a controlled unsupported stub in crate unit tests. -//! - Fake runtime-cell pointers are never dereferenced by these tests. - -use super::context::*; -use super::declared_symbols::*; -use super::execute::*; -use super::native_functions::*; -use super::native_methods::*; -use super::scope::*; -use super::symbols::*; -use crate::abi::{ - ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_DIRTY, - SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, -}; -use crate::context::{ - NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, NativeCallableDefault, - NativeCallableObjectDefaultArg, push_native_frame_called_class_override, -}; -use crate::errors::EvalStatus; -use crate::eval_ir::{EvalAttributeArg, EvalParameterTypeVariant}; -use crate::value::{RuntimeCell, RuntimeCellHandle}; -use std::ffi::c_void; - -const TEST_NATIVE_DEFAULT_NULL: u64 = 0; -const TEST_NATIVE_DEFAULT_BOOL: u64 = 1; -const TEST_NATIVE_DEFAULT_INT: u64 = 2; -const TEST_NATIVE_DEFAULT_FLOAT: u64 = 3; -const TEST_NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; -const TEST_NATIVE_PROPERTY_REQUIRES_GET: u64 = 1; -const TEST_NATIVE_PROPERTY_REQUIRES_SET: u64 = 2; -const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; -const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; -const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; -const TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; -const TEST_NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; -const TEST_NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; -const TEST_NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; -const TEST_NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; -const TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; - -/// Test native invoker placeholder used only to validate ABI registration. -unsafe extern "C" fn fake_native_invoker( - _descriptor: *mut c_void, - _args: *mut RuntimeCell, -) -> *mut RuntimeCell { - std::ptr::null_mut() -} - -/// Builds one native member-attribute ABI record for registration tests. -fn native_member_attribute_record( - owner_kind: u8, - member_key: &str, - attribute_name: &str, - args: Option<&[EvalAttributeArg]>, -) -> Vec { - let mut record = Vec::new(); - record.push(owner_kind); - native_member_attribute_push_string(&mut record, member_key); - native_member_attribute_push_string(&mut record, attribute_name); - match args { - Some(args) => { - record.push(1); - record.extend_from_slice(&(args.len() as u32).to_le_bytes()); - for arg in args { - native_member_attribute_push_arg(&mut record, arg); - } - } - None => record.push(0), - } - record -} - -/// Appends one test attribute argument to a native member-attribute ABI record. -fn native_member_attribute_push_arg(record: &mut Vec, arg: &EvalAttributeArg) { - match arg { - EvalAttributeArg::Null => record.push(0), - EvalAttributeArg::Bool(value) => { - record.push(1); - record.push(u8::from(*value)); - } - EvalAttributeArg::Int(value) => { - record.push(2); - record.extend_from_slice(&value.to_le_bytes()); - } - EvalAttributeArg::Float(bits) => { - record.push(5); - record.extend_from_slice(&bits.to_le_bytes()); - } - EvalAttributeArg::String(value) => { - record.push(3); - native_member_attribute_push_string(record, value); - } - EvalAttributeArg::Named { name, value } => { - record.push(4); - native_member_attribute_push_string(record, name); - native_member_attribute_push_arg(record, value); - } - EvalAttributeArg::IntKeyed { .. } => { - panic!("native attribute test ABI does not encode int-keyed array arguments") - } - EvalAttributeArg::Array(elements) => { - record.push(6); - record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); - for element in elements { - native_member_attribute_push_arg(record, element); - } - } - } -} - -/// Appends one length-prefixed string to a native member-attribute ABI record. -fn native_member_attribute_push_string(record: &mut Vec, value: &str) { - record.extend_from_slice(&(value.len() as u32).to_le_bytes()); - record.extend_from_slice(value.as_bytes()); -} - -/// Builds one object-valued native parameter default ABI record for registration tests. -fn native_object_default_record( - class_name: &str, - args: &[NativeCallableObjectDefaultArg], -) -> Vec { - let mut record = Vec::new(); - native_member_attribute_push_string(&mut record, class_name); - record.push(args.len() as u8); - for arg in args { - native_object_default_push_arg(&mut record, arg); - } - record -} - -/// Builds one array-valued native parameter default ABI record for registration tests. -fn native_array_default_record(elements: &[NativeCallableArrayDefaultElement]) -> Vec { - let mut record = Vec::new(); - record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); - for element in elements { - native_array_default_push_element(&mut record, element); - } - record -} - -/// Appends one array-default element and optional static key to a default record. -fn native_array_default_push_element( - record: &mut Vec, - element: &NativeCallableArrayDefaultElement, -) { - match &element.key { - Some(NativeCallableArrayDefaultKey::Int(value)) => { - record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_INT); - record.extend_from_slice(&value.to_le_bytes()); - } - Some(NativeCallableArrayDefaultKey::String(value)) => { - record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_STRING); - native_member_attribute_push_string(record, value); - } - None => record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_AUTO), - } - native_object_default_push_arg_value(record, &element.value); -} - -/// Appends one object-default constructor argument to a native parameter default record. -fn native_object_default_push_arg(record: &mut Vec, arg: &NativeCallableObjectDefaultArg) { - if let Some(name) = &arg.name { - record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED); - native_member_attribute_push_string(record, name); - } - native_object_default_push_arg_value(record, &arg.value); -} - -/// Appends one object-default constructor argument value to a native parameter default record. -fn native_object_default_push_arg_value(record: &mut Vec, value: &NativeCallableDefault) { - match value { - NativeCallableDefault::Null => { - native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_NULL, 0) - } - NativeCallableDefault::Bool(value) => { - native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_BOOL, u64::from(*value)) - } - NativeCallableDefault::Int(value) => { - native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_INT, *value as u64) - } - NativeCallableDefault::Float(value) => { - native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_FLOAT, value.to_bits()) - } - NativeCallableDefault::String(value) => { - record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING); - native_member_attribute_push_string(record, value); - } - NativeCallableDefault::EmptyArray => { - native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_EMPTY_ARRAY, 0) - } - NativeCallableDefault::Object { class_name, args } => { - record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT); - record.extend_from_slice(&native_object_default_record(class_name, args)); - } - NativeCallableDefault::Array(elements) => { - record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_ARRAY); - record.extend_from_slice(&native_array_default_record(elements)); - } - } -} - -/// Appends one scalar object-default constructor argument to a native parameter default record. -fn native_object_default_push_scalar(record: &mut Vec, kind: u64, payload: u64) { - record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR); - record.extend_from_slice(&kind.to_le_bytes()); - record.extend_from_slice(&payload.to_le_bytes()); -} - -/// Verifies the exported version entry point reports the crate ABI constant. -#[test] -fn abi_version_matches_constant() { - assert_eq!(__elephc_eval_abi_version(), ABI_VERSION); -} - -/// Verifies the initial execute stub clears result storage and returns the -/// documented unsupported status instead of panicking or succeeding. -#[test] -fn execute_stub_returns_unsupported_and_clears_result() { - let mut result = ElephcEvalResult { - kind: 99, - value_cell: 1usize as *mut std::ffi::c_void, - error: 2usize as *mut std::ffi::c_void, - }; - let status = unsafe { - __elephc_eval_execute( - std::ptr::null_mut(), - std::ptr::null_mut(), - b"$x = 1;".as_ptr(), - 7, - &mut result, - ) - }; - assert_eq!(status, EvalStatus::UnsupportedConstruct.code()); - assert_eq!(result.kind, 0); - assert!(result.value_cell.is_null()); - assert!(result.error.is_null()); -} - -/// Verifies context allocation returns a current-version opaque handle. -#[test] -fn context_new_returns_current_version_handle() { - let ctx = __elephc_eval_context_new(); - assert!(!ctx.is_null()); - let version = unsafe { (*ctx).abi_version() }; - unsafe { - __elephc_eval_context_free(ctx); - } - assert_eq!(version, ABI_VERSION); -} - -/// Verifies call-site metadata can be set through the stable context ABI. -#[test] -fn context_set_call_site_records_file_dir_and_line() { - let mut ctx = ElephcEvalContext::new(); - let file = b"/tmp/source.php"; - let dir = b"/tmp"; - - let status = unsafe { - __elephc_eval_context_set_call_site( - &mut ctx, - file.as_ptr(), - file.len() as u64, - dir.as_ptr(), - dir.len() as u64, - 9, - ) - }; - - assert_eq!(status, EvalStatus::Ok.code()); - assert_eq!(ctx.call_dir(), "/tmp"); - assert_eq!(ctx.eval_file_magic(), "/tmp/source.php(9) : eval()'d code"); -} - -/// Verifies the context ABI records a non-owned global scope handle. -#[test] -fn context_set_global_scope_records_handle() { - let mut ctx = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - - let status = unsafe { __elephc_eval_context_set_global_scope(&mut ctx, &mut scope) }; - - assert_eq!(status, EvalStatus::Ok.code()); - assert_eq!( - ctx.global_scope_ptr(), - Some(&mut scope as *mut ElephcEvalScope) - ); -} - -/// Verifies generated class scopes are pushed and popped through the context ABI. -#[test] -fn context_push_class_scope_records_self_and_called_class() { - let mut ctx = ElephcEvalContext::new(); - let class_name = b"AotBase"; - let called_class_name = b"AotChild"; - - let push_status = unsafe { - __elephc_eval_context_push_class_scope( - &mut ctx, - class_name.as_ptr(), - class_name.len() as u64, - called_class_name.as_ptr(), - called_class_name.len() as u64, - ) - }; - - assert_eq!(push_status, EvalStatus::Ok.code()); - assert_eq!(ctx.current_class_scope(), Some("AotBase")); - assert_eq!(ctx.current_called_class_scope(), Some("AotChild")); - - let pop_status = unsafe { __elephc_eval_context_pop_class_scope(&mut ctx) }; - - assert_eq!(pop_status, EvalStatus::Ok.code()); - assert_eq!(ctx.current_class_scope(), None); - assert_eq!(ctx.current_called_class_scope(), None); -} - -/// Verifies generated frames can query eval late-static overrides without a context handle. -#[test] -fn native_frame_called_class_override_reports_thread_local_scope() { - let class_name = b"AotBase"; - let mut out_ptr = std::ptr::null(); - let mut out_len = 0; - - let missing = unsafe { - __elephc_eval_native_frame_called_class_override( - class_name.as_ptr(), - class_name.len() as u64, - &mut out_ptr, - &mut out_len, - ) - }; - - assert_eq!(missing, 0); - assert!(out_ptr.is_null()); - assert_eq!(out_len, 0); - - { - let _guard = push_native_frame_called_class_override( - std::ptr::null_mut(), - "AotBase", - "EvalChild", - ); - - let found = unsafe { - __elephc_eval_native_frame_called_class_override( - class_name.as_ptr(), - class_name.len() as u64, - &mut out_ptr, - &mut out_len, - ) - }; - - assert_eq!(found, 1); - let bytes = unsafe { std::slice::from_raw_parts(out_ptr, out_len as usize) }; - assert_eq!(bytes, b"EvalChild"); - } - - let after_drop = unsafe { - __elephc_eval_native_frame_called_class_override( - class_name.as_ptr(), - class_name.len() as u64, - &mut out_ptr, - &mut out_len, - ) - }; - - assert_eq!(after_drop, 0); - assert!(out_ptr.is_null()); - assert_eq!(out_len, 0); -} - -/// Verifies generated declaration-name metadata is exposed through eval lists. -#[test] -fn register_declared_symbol_names_records_visible_metadata() { - let mut ctx = ElephcEvalContext::new(); - let class_name = b"\\AotDeclaredClass"; - let class_duplicate = b"aotdeclaredclass"; - let interface_name = b"AotDeclaredInterface"; - let trait_name = b"AotDeclaredTrait"; - let empty_name = b""; - - let class_registered = unsafe { - __elephc_eval_register_declared_class_name( - &mut ctx, - class_name.as_ptr(), - class_name.len() as u64, - ) - }; - let duplicate_registered = unsafe { - __elephc_eval_register_declared_class_name( - &mut ctx, - class_duplicate.as_ptr(), - class_duplicate.len() as u64, - ) - }; - let interface_registered = unsafe { - __elephc_eval_register_declared_interface_name( - &mut ctx, - interface_name.as_ptr(), - interface_name.len() as u64, - ) - }; - let trait_registered = unsafe { - __elephc_eval_register_declared_trait_name( - &mut ctx, - trait_name.as_ptr(), - trait_name.len() as u64, - ) - }; - let empty_rejected = unsafe { - __elephc_eval_register_declared_trait_name( - &mut ctx, - empty_name.as_ptr(), - empty_name.len() as u64, - ) - }; - - assert_eq!(class_registered, 1); - assert_eq!(duplicate_registered, 1); - assert_eq!(interface_registered, 1); - assert_eq!(trait_registered, 1); - assert_eq!(empty_rejected, 0); - assert_eq!(ctx.declared_class_names(), &["AotDeclaredClass".to_string()]); - assert_eq!( - ctx.declared_interface_names(), - &["AotDeclaredInterface".to_string()] - ); - assert_eq!(ctx.declared_trait_names(), &["AotDeclaredTrait".to_string()]); -} - -/// Verifies the function-exists ABI probes eval-declared functions by folded name. -#[test] -fn function_exists_reports_declared_eval_function() { - let mut ctx = ElephcEvalContext::new(); - ctx.define_function( - "dyn_probe", - crate::eval_ir::EvalFunction::new("dyn_probe", Vec::new(), Vec::new()), - ) - .expect("first dynamic function declaration should succeed"); - let existing = b"DYN_PROBE"; - let missing = b"missing"; - - let existing_result = - unsafe { __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; - let missing_result = - unsafe { __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; - - assert_eq!(existing_result, 1); - assert_eq!(missing_result, 0); -} - -/// Verifies the constant-exists ABI probes eval-defined constants by PHP name. -#[test] -fn constant_exists_reports_defined_eval_constant() { - let mut ctx = ElephcEvalContext::new(); - let value = RuntimeCellHandle::from_raw(1usize as *mut RuntimeCell); - assert!(ctx.define_constant("DynConstProbe", value)); - let existing = b"DynConstProbe"; - let qualified = b"\\DynConstProbe"; - let wrong_case = b"dynconstprobe"; - let missing = b"missing"; - - let existing_result = - unsafe { __elephc_eval_constant_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; - let qualified_result = - unsafe { __elephc_eval_constant_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) }; - let wrong_case_result = unsafe { - __elephc_eval_constant_exists(&ctx, wrong_case.as_ptr(), wrong_case.len() as u64) - }; - let missing_result = - unsafe { __elephc_eval_constant_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; - - assert_eq!(existing_result, 1); - assert_eq!(qualified_result, 1); - assert_eq!(wrong_case_result, 0); - assert_eq!(missing_result, 0); -} - -/// Verifies the dynamic-class-exists ABI probes eval-declared classes by folded PHP name. -#[test] -fn dynamic_class_exists_reports_declared_eval_class() { - let mut ctx = ElephcEvalContext::new(); - assert!(ctx.define_class(crate::eval_ir::EvalClass::new( - "DynClassProbe", - Vec::new(), - Vec::new() - ))); - let existing = b"DynClassProbe"; - let qualified = b"\\DynClassProbe"; - let folded = b"dynclassprobe"; - let missing = b"missing"; - - let existing_result = unsafe { - __elephc_eval_dynamic_class_exists(&ctx, existing.as_ptr(), existing.len() as u64) - }; - let qualified_result = unsafe { - __elephc_eval_dynamic_class_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) - }; - let folded_result = - unsafe { __elephc_eval_dynamic_class_exists(&ctx, folded.as_ptr(), folded.len() as u64) }; - let missing_result = - unsafe { __elephc_eval_dynamic_class_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; - - assert_eq!(existing_result, 1); - assert_eq!(qualified_result, 1); - assert_eq!(folded_result, 1); - assert_eq!(missing_result, 0); -} - -/// Verifies native AOT registration records function parameter metadata and defaults. -#[test] -fn register_native_function_reports_function_exists() { - let mut ctx = ElephcEvalContext::new(); - let name = b"NATIVE_PROBE"; - let param = b"value"; - let variadic = b"items"; - let param_type = b"?string"; - let return_type = b"int"; - let descriptor = 1usize as *mut c_void; - - let registered = unsafe { - __elephc_eval_register_native_function( - &mut ctx, - name.as_ptr(), - name.len() as u64, - descriptor, - Some(fake_native_invoker), - 2, - ) - }; - let param_registered = unsafe { - __elephc_eval_register_native_function_param( - &mut ctx, - name.as_ptr(), - name.len() as u64, - 0, - param.as_ptr(), - param.len() as u64, - ) - }; - let variadic_param_registered = unsafe { - __elephc_eval_register_native_function_param( - &mut ctx, - name.as_ptr(), - name.len() as u64, - 1, - variadic.as_ptr(), - variadic.len() as u64, - ) - }; - let bridge_support_registered = unsafe { - __elephc_eval_register_native_function_bridge_support( - &mut ctx, - name.as_ptr(), - name.len() as u64, - 0, - ) - }; - let param_flags_registered = unsafe { - __elephc_eval_register_native_function_param_flags( - &mut ctx, - name.as_ptr(), - name.len() as u64, - 0, - 1, - 0, - ) - }; - let variadic_flags_registered = unsafe { - __elephc_eval_register_native_function_param_flags( - &mut ctx, - name.as_ptr(), - name.len() as u64, - 1, - 0, - 1, - ) - }; - let param_type_registered = unsafe { - __elephc_eval_register_native_function_param_type( - &mut ctx, - name.as_ptr(), - name.len() as u64, - 0, - param_type.as_ptr(), - param_type.len() as u64, - ) - }; - let return_type_registered = unsafe { - __elephc_eval_register_native_function_return_type( - &mut ctx, - name.as_ptr(), - name.len() as u64, - return_type.as_ptr(), - return_type.len() as u64, - ) - }; - let param_default_registered = unsafe { - __elephc_eval_register_native_function_param_default_scalar( - &mut ctx, - name.as_ptr(), - name.len() as u64, - 0, - TEST_NATIVE_DEFAULT_INT, - 42, - ) - }; - let exists = unsafe { __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) }; - - assert_eq!(registered, 1); - let native = ctx - .native_function("native_probe") - .expect("native function should be registered"); - - assert_eq!(param_registered, 1); - assert_eq!(variadic_param_registered, 1); - assert_eq!(bridge_support_registered, 1); - assert_eq!(param_flags_registered, 1); - assert_eq!(variadic_flags_registered, 1); - assert_eq!(param_type_registered, 1); - assert_eq!(return_type_registered, 1); - assert_eq!(param_default_registered, 1); - assert_eq!(exists, 1); - assert_eq!( - native.param_names(), - &["value".to_string(), "items".to_string()] - ); - let native_type = native.param_type(0).expect("native parameter type"); - assert!(native_type.allows_null()); - assert_eq!(native_type.variants(), &[EvalParameterTypeVariant::String]); - assert!(native.param_by_ref(0)); - assert!(!native.param_variadic(0)); - assert!(native.param_variadic(1)); - assert!(!native.bridge_supported()); - assert_eq!(native.required_param_count(), 0); - assert_eq!( - native.return_type().expect("native return type").variants(), - &[EvalParameterTypeVariant::Int] - ); - assert_eq!(native.param_default(0), Some(&NativeCallableDefault::Int(42))); -} - -/// Verifies native AOT method registration records instance/static/constructor parameters. -#[test] -fn register_native_methods_record_signature_metadata() { - let mut ctx = ElephcEvalContext::new(); - let method = b"KnownClass::join"; - let static_method = b"KnownClass::sum"; - let class = b"KnownClass"; - let left = b"left"; - let right = b"right"; - let value = b"value"; - let method_type = b"int|string|null"; - let static_type = b"?string"; - let constructor_type = b"KnownDep"; - let return_type = b"bool"; - - let method_registered = unsafe { - __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 2) - }; - let method_param_registered = unsafe { - __elephc_eval_register_native_method_param( - &mut ctx, - method.as_ptr(), - method.len() as u64, - 1, - right.as_ptr(), - right.len() as u64, - ) - }; - let method_param_type_registered = unsafe { - __elephc_eval_register_native_method_param_type( - &mut ctx, - method.as_ptr(), - method.len() as u64, - 0, - method_type.as_ptr(), - method_type.len() as u64, - ) - }; - let method_param_flags_registered = unsafe { - __elephc_eval_register_native_method_param_flags( - &mut ctx, - method.as_ptr(), - method.len() as u64, - 1, - 1, - 0, - ) - }; - let static_registered = unsafe { - __elephc_eval_register_native_static_method( - &mut ctx, - static_method.as_ptr(), - static_method.len() as u64, - 2, - ) - }; - let static_param_registered = unsafe { - __elephc_eval_register_native_static_method_param( - &mut ctx, - static_method.as_ptr(), - static_method.len() as u64, - 0, - left.as_ptr(), - left.len() as u64, - ) - }; - let static_param_type_registered = unsafe { - __elephc_eval_register_native_static_method_param_type( - &mut ctx, - static_method.as_ptr(), - static_method.len() as u64, - 0, - static_type.as_ptr(), - static_type.len() as u64, - ) - }; - let static_param_flags_registered = unsafe { - __elephc_eval_register_native_static_method_param_flags( - &mut ctx, - static_method.as_ptr(), - static_method.len() as u64, - 1, - 0, - 1, - ) - }; - let static_return_type_registered = unsafe { - __elephc_eval_register_native_static_method_return_type( - &mut ctx, - static_method.as_ptr(), - static_method.len() as u64, - return_type.as_ptr(), - return_type.len() as u64, - ) - }; - let constructor_registered = unsafe { - __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) - }; - let constructor_param_registered = unsafe { - __elephc_eval_register_native_constructor_param( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - value.as_ptr(), - value.len() as u64, - ) - }; - let constructor_param_type_registered = unsafe { - __elephc_eval_register_native_constructor_param_type( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - constructor_type.as_ptr(), - constructor_type.len() as u64, - ) - }; - let constructor_param_flags_registered = unsafe { - __elephc_eval_register_native_constructor_param_flags( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - 1, - 1, - ) - }; - let constructor_bridge_support_registered = unsafe { - __elephc_eval_register_native_constructor_bridge_support( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - ) - }; - let method_default_registered = unsafe { - __elephc_eval_register_native_method_param_default_string( - &mut ctx, - method.as_ptr(), - method.len() as u64, - 1, - right.as_ptr(), - right.len() as u64, - ) - }; - let static_default_registered = unsafe { - __elephc_eval_register_native_static_method_param_default_scalar( - &mut ctx, - static_method.as_ptr(), - static_method.len() as u64, - 0, - 2, - 42, - ) - }; - let static_empty_array_default_registered = unsafe { - __elephc_eval_register_native_static_method_param_default_scalar( - &mut ctx, - static_method.as_ptr(), - static_method.len() as u64, - 1, - NATIVE_DEFAULT_EMPTY_ARRAY, - 0, - ) - }; - let constructor_default_registered = unsafe { - __elephc_eval_register_native_constructor_param_default_scalar( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - 1, - 1, - ) - }; - - assert_eq!(method_registered, 1); - assert_eq!(method_param_registered, 1); - assert_eq!(method_param_type_registered, 1); - assert_eq!(method_param_flags_registered, 1); - assert_eq!(static_registered, 1); - assert_eq!(static_param_registered, 1); - assert_eq!(static_param_type_registered, 1); - assert_eq!(static_param_flags_registered, 1); - assert_eq!(static_return_type_registered, 1); - assert_eq!(constructor_registered, 1); - assert_eq!(constructor_param_registered, 1); - assert_eq!(constructor_param_type_registered, 1); - assert_eq!(constructor_param_flags_registered, 1); - assert_eq!(constructor_bridge_support_registered, 1); - assert_eq!(method_default_registered, 1); - assert_eq!(static_default_registered, 1); - assert_eq!(static_empty_array_default_registered, 1); - assert_eq!(constructor_default_registered, 1); - assert_eq!( - ctx.native_method_signature("knownclass", "JOIN") - .expect("method metadata") - .param_names(), - &["".to_string(), "right".to_string()] - ); - let method_signature = ctx - .native_method_signature("knownclass", "JOIN") - .expect("method metadata"); - let method_type = method_signature - .param_type(0) - .expect("method parameter type"); - assert!(method_type.allows_null()); - assert_eq!( - method_type.variants(), - &[ - EvalParameterTypeVariant::Int, - EvalParameterTypeVariant::String - ] - ); - assert!(method_signature.param_by_ref(1)); - assert!(!method_signature.param_variadic(1)); - assert_eq!( - ctx.native_static_method_signature("KnownClass", "SUM") - .expect("static method metadata") - .param_names(), - &["left".to_string(), "".to_string()] - ); - let static_signature = ctx - .native_static_method_signature("KnownClass", "SUM") - .expect("static method metadata"); - let static_type = static_signature - .param_type(0) - .expect("static method parameter type"); - assert!(static_type.allows_null()); - assert_eq!(static_type.variants(), &[EvalParameterTypeVariant::String]); - assert!(!static_signature.param_by_ref(1)); - assert!(static_signature.param_variadic(1)); - assert_eq!( - static_signature - .return_type() - .expect("static return type") - .variants(), - &[EvalParameterTypeVariant::Bool] - ); - assert_eq!( - ctx.native_constructor_signature("knownclass") - .expect("constructor metadata") - .param_names(), - &["value".to_string()] - ); - let constructor_signature = ctx - .native_constructor_signature("knownclass") - .expect("constructor metadata"); - assert_eq!( - constructor_signature - .param_type(0) - .expect("constructor parameter type") - .variants(), - &[EvalParameterTypeVariant::Class("KnownDep".to_string())] - ); - assert!(constructor_signature.param_by_ref(0)); - assert!(constructor_signature.param_variadic(0)); - assert!(!constructor_signature.bridge_supported()); - assert_eq!( - ctx.native_method_signature("knownclass", "JOIN") - .expect("method metadata") - .param_default(1), - Some(&NativeCallableDefault::String("right".to_string())) - ); - assert_eq!( - ctx.native_static_method_signature("KnownClass", "SUM") - .expect("static method metadata") - .param_default(0), - Some(&NativeCallableDefault::Int(42)) - ); - assert_eq!( - ctx.native_static_method_signature("KnownClass", "SUM") - .expect("static method metadata") - .param_default(1), - Some(&NativeCallableDefault::EmptyArray) - ); - assert_eq!( - ctx.native_constructor_signature("knownclass") - .expect("constructor metadata") - .param_default(0), - Some(&NativeCallableDefault::Bool(true)) - ); -} - -/// Verifies native AOT object defaults can carry nested object constructor args. -#[test] -fn register_native_object_default_decodes_nested_objects() { - let mut ctx = ElephcEvalContext::new(); - let class = b"KnownClass"; - let nested_args = vec![NativeCallableObjectDefaultArg::positional( - NativeCallableDefault::Object { - class_name: "InnerDefault".to_string(), - args: vec![NativeCallableObjectDefaultArg::positional( - NativeCallableDefault::String("leaf".to_string()), - )], - }, - )]; - let expected_default = NativeCallableDefault::Object { - class_name: "OuterDefault".to_string(), - args: nested_args.clone(), - }; - let spec = native_object_default_record("OuterDefault", &nested_args); - - let constructor_registered = unsafe { - __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) - }; - let default_registered = unsafe { - __elephc_eval_register_native_constructor_param_default_object( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - spec.as_ptr(), - spec.len() as u64, - ) - }; - - assert_eq!(constructor_registered, 1); - assert_eq!(default_registered, 1); - assert_eq!( - ctx.native_constructor_signature("knownclass") - .expect("constructor metadata") - .param_default(0), - Some(&expected_default) - ); -} - -/// Verifies native AOT object defaults can carry named constructor args. -#[test] -fn register_native_object_default_decodes_named_args() { - let mut ctx = ElephcEvalContext::new(); - let class = b"KnownClass"; - let args = vec![ - NativeCallableObjectDefaultArg::named( - "right", - NativeCallableDefault::String("R".to_string()), - ), - NativeCallableObjectDefaultArg::named( - "left", - NativeCallableDefault::String("L".to_string()), - ), - ]; - let expected_default = NativeCallableDefault::Object { - class_name: "NamedDefault".to_string(), - args: args.clone(), - }; - let spec = native_object_default_record("NamedDefault", &args); - - let constructor_registered = unsafe { - __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) - }; - let default_registered = unsafe { - __elephc_eval_register_native_constructor_param_default_object( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - spec.as_ptr(), - spec.len() as u64, - ) - }; - - assert_eq!(constructor_registered, 1); - assert_eq!(default_registered, 1); - assert_eq!( - ctx.native_constructor_signature("knownclass") - .expect("constructor metadata") - .param_default(0), - Some(&expected_default) - ); -} - -/// Verifies native AOT object defaults decode the full u8 constructor argument range. -#[test] -fn register_native_object_default_decodes_full_u8_arg_count() { - let mut ctx = ElephcEvalContext::new(); - let class = b"KnownClass"; - let args = (0..TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS) - .map(|index| { - let value = NativeCallableDefault::String(format!("arg{}", index)); - NativeCallableObjectDefaultArg::positional(value) - }) - .collect::>(); - let expected_default = NativeCallableDefault::Object { - class_name: "LargeDefault".to_string(), - args: args.clone(), - }; - let spec = native_object_default_record("LargeDefault", &args); - - let constructor_registered = unsafe { - __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) - }; - let default_registered = unsafe { - __elephc_eval_register_native_constructor_param_default_object( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - spec.as_ptr(), - spec.len() as u64, - ) - }; - - assert_eq!(constructor_registered, 1); - assert_eq!(default_registered, 1); - assert_eq!( - ctx.native_constructor_signature("knownclass") - .expect("constructor metadata") - .param_default(0), - Some(&expected_default) - ); -} - -/// Verifies native AOT array defaults can carry keyed and nested default values. -#[test] -fn register_native_array_default_decodes_nested_values() { - let mut ctx = ElephcEvalContext::new(); - let method = b"KnownClass::items"; - let class = b"KnownClass"; - let property = b"KnownClass::items"; - let elements = vec![ - NativeCallableArrayDefaultElement::keyed( - NativeCallableArrayDefaultKey::String("left".to_string()), - NativeCallableDefault::String("L".to_string()), - ), - NativeCallableArrayDefaultElement::keyed( - NativeCallableArrayDefaultKey::Int(2), - NativeCallableDefault::Array(vec![NativeCallableArrayDefaultElement::positional( - NativeCallableDefault::Int(7), - )]), - ), - NativeCallableArrayDefaultElement::positional(NativeCallableDefault::Object { - class_name: "ArrayDefaultDep".to_string(), - args: vec![NativeCallableObjectDefaultArg::named( - "label", - NativeCallableDefault::String("dep".to_string()), - )], - }), - ]; - let expected_default = NativeCallableDefault::Array(elements.clone()); - let spec = native_array_default_record(&elements); - - let method_registered = unsafe { - __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 1) - }; - let method_default_registered = unsafe { - __elephc_eval_register_native_method_param_default_array( - &mut ctx, - method.as_ptr(), - method.len() as u64, - 0, - spec.as_ptr(), - spec.len() as u64, - ) - }; - let constructor_registered = unsafe { - __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) - }; - let constructor_default_registered = unsafe { - __elephc_eval_register_native_constructor_param_default_array( - &mut ctx, - class.as_ptr(), - class.len() as u64, - 0, - spec.as_ptr(), - spec.len() as u64, - ) - }; - let property_default_registered = unsafe { - __elephc_eval_register_native_property_default_array( - &mut ctx, - property.as_ptr(), - property.len() as u64, - spec.as_ptr(), - spec.len() as u64, - ) - }; - - assert_eq!(method_registered, 1); - assert_eq!(method_default_registered, 1); - assert_eq!(constructor_registered, 1); - assert_eq!(constructor_default_registered, 1); - assert_eq!(property_default_registered, 1); - assert_eq!( - ctx.native_method_signature("knownclass", "ITEMS") - .expect("method metadata") - .param_default(0), - Some(&expected_default) - ); - assert_eq!( - ctx.native_constructor_signature("knownclass") - .expect("constructor metadata") - .param_default(0), - Some(&expected_default) - ); - assert_eq!( - ctx.native_property_default("KnownClass", "items"), - Some(expected_default) - ); -} - -/// Verifies native AOT parent metadata is available for eval static-scope resolution. -#[test] -fn register_native_class_parent_records_metadata() { - let mut ctx = ElephcEvalContext::new(); - let class = b"KnownChild"; - let parent = b"KnownParent"; - - let registered = unsafe { - __elephc_eval_register_native_class_parent( - &mut ctx, - class.as_ptr(), - class.len() as u64, - parent.as_ptr(), - parent.len() as u64, - ) - }; - - assert_eq!(registered, 1); - assert_eq!(ctx.native_class_parent("knownchild"), Some("KnownParent")); -} - -/// Verifies native AOT property type metadata is available to eval reflection. -#[test] -fn register_native_property_type_records_metadata() { - let mut ctx = ElephcEvalContext::new(); - let property = b"KnownClass::name"; - let property_type = b"?KnownDep"; - let invalid_property = b"KnownClass::bad"; - let invalid_type = b"void"; - - let registered = unsafe { - __elephc_eval_register_native_property_type( - &mut ctx, - property.as_ptr(), - property.len() as u64, - property_type.as_ptr(), - property_type.len() as u64, - ) - }; - let invalid_registered = unsafe { - __elephc_eval_register_native_property_type( - &mut ctx, - invalid_property.as_ptr(), - invalid_property.len() as u64, - invalid_type.as_ptr(), - invalid_type.len() as u64, - ) - }; - - assert_eq!(registered, 1); - let property_type = ctx - .native_property_type("knownclass", "name") - .expect("property type metadata"); - assert!(property_type.allows_null()); - assert_eq!( - property_type.variants(), - &[EvalParameterTypeVariant::Class("KnownDep".to_string())] - ); - assert_eq!(invalid_registered, 0); - assert!(ctx.native_property_type("KnownClass", "bad").is_none()); -} - -/// Verifies native AOT interface property contracts are available to eval validation. -#[test] -fn register_native_interface_property_records_metadata() { - let mut ctx = ElephcEvalContext::new(); - let property = b"KnownContract::KnownParentContract::name"; - let property_type = b"string"; - let invalid_property = b"KnownContract::KnownParentContract::bad"; - let invalid_type = b"void"; - - let registered = unsafe { - __elephc_eval_register_native_interface_property( - &mut ctx, - property.as_ptr(), - property.len() as u64, - property_type.as_ptr(), - property_type.len() as u64, - TEST_NATIVE_PROPERTY_REQUIRES_GET | TEST_NATIVE_PROPERTY_REQUIRES_SET, - ) - }; - let invalid_registered = unsafe { - __elephc_eval_register_native_interface_property( - &mut ctx, - invalid_property.as_ptr(), - invalid_property.len() as u64, - invalid_type.as_ptr(), - invalid_type.len() as u64, - TEST_NATIVE_PROPERTY_REQUIRES_GET, - ) - }; - - let requirements = ctx.native_interface_property_requirements("knowncontract"); - assert_eq!(registered, 1); - assert_eq!(requirements.len(), 1); - assert_eq!(requirements[0].0, "KnownParentContract"); - assert_eq!(requirements[0].1.name(), "name"); - assert!(requirements[0].1.requires_get()); - assert!(requirements[0].1.requires_set()); - assert_eq!( - requirements[0].1.property_type().map(|ty| ty.variants()), - Some(&[EvalParameterTypeVariant::String][..]) - ); - assert_eq!(invalid_registered, 0); - assert!(ctx - .native_interface_property_requirements("KnownContract") - .iter() - .all(|(_, property)| property.name() != "bad")); -} - -/// Verifies native AOT abstract class property contracts are available to eval validation. -#[test] -fn register_native_abstract_property_records_metadata() { - let mut ctx = ElephcEvalContext::new(); - let property = b"KnownClass::KnownParent::name"; - let property_type = b"string"; - let invalid_property = b"KnownClass::KnownParent::bad"; - let invalid_type = b"void"; - - let registered = unsafe { - __elephc_eval_register_native_abstract_property( - &mut ctx, - property.as_ptr(), - property.len() as u64, - property_type.as_ptr(), - property_type.len() as u64, - TEST_NATIVE_PROPERTY_REQUIRES_GET | TEST_NATIVE_PROPERTY_REQUIRES_SET, - ) - }; - let invalid_registered = unsafe { - __elephc_eval_register_native_abstract_property( - &mut ctx, - invalid_property.as_ptr(), - invalid_property.len() as u64, - invalid_type.as_ptr(), - invalid_type.len() as u64, - TEST_NATIVE_PROPERTY_REQUIRES_GET, - ) - }; - - let requirements = ctx.native_abstract_property_requirements("knownclass"); - assert_eq!(registered, 1); - assert_eq!(requirements.len(), 1); - assert_eq!(requirements[0].0, "KnownParent"); - assert_eq!(requirements[0].1.name(), "name"); - assert!(requirements[0].1.requires_get()); - assert!(requirements[0].1.requires_set()); - assert_eq!( - requirements[0].1.property_type().map(|ty| ty.variants()), - Some(&[EvalParameterTypeVariant::String][..]) - ); - assert_eq!(invalid_registered, 0); - assert!(ctx - .native_abstract_property_requirements("KnownClass") - .iter() - .all(|(_, property)| property.name() != "bad")); -} - -/// Verifies native AOT property default metadata is available to eval reflection. -#[test] -fn register_native_property_default_records_metadata() { - let mut ctx = ElephcEvalContext::new(); - let count = b"KnownClass::count"; - let label = b"KnownClass::label"; - let invalid = b"KnownClass::invalid"; - let label_value = b"ok"; - - let scalar_registered = unsafe { - __elephc_eval_register_native_property_default_scalar( - &mut ctx, - count.as_ptr(), - count.len() as u64, - 2, - 42, - ) - }; - let string_registered = unsafe { - __elephc_eval_register_native_property_default_string( - &mut ctx, - label.as_ptr(), - label.len() as u64, - label_value.as_ptr(), - label_value.len() as u64, - ) - }; - let invalid_registered = unsafe { - __elephc_eval_register_native_property_default_scalar( - &mut ctx, - invalid.as_ptr(), - invalid.len() as u64, - 99, - 0, - ) - }; - - assert_eq!(scalar_registered, 1); - assert_eq!( - ctx.native_property_default("knownclass", "count"), - Some(NativeCallableDefault::Int(42)) - ); - assert_eq!(string_registered, 1); - assert_eq!( - ctx.native_property_default("KnownClass", "label"), - Some(NativeCallableDefault::String("ok".to_string())) - ); - assert_eq!(invalid_registered, 0); - assert!(ctx - .native_property_default("KnownClass", "invalid") - .is_none()); -} - -/// Verifies native AOT member attributes are available to eval reflection. -#[test] -fn register_native_member_attribute_records_metadata() { - let mut ctx = ElephcEvalContext::new(); - let method_record = native_member_attribute_record( - 0, - "KnownClass::run", - "Route", - Some(&[ - EvalAttributeArg::String("api".to_string()), - EvalAttributeArg::Named { - name: "path".to_string(), - value: Box::new(EvalAttributeArg::String("/users".to_string())), - }, - EvalAttributeArg::Int(7), - EvalAttributeArg::Float(1.5f64.to_bits()), - EvalAttributeArg::Array(vec![ - EvalAttributeArg::Int(1), - EvalAttributeArg::String("two".to_string()), - ]), - EvalAttributeArg::Bool(true), - EvalAttributeArg::Null, - ]), - ); - let property_record = native_member_attribute_record(1, "KnownClass::id", "Column", None); - let constant_record = native_member_attribute_record( - 2, - "KnownClass::LIMIT", - "Limit", - Some(&[EvalAttributeArg::Int(100)]), - ); - let class_record = native_member_attribute_record( - 3, - "KnownClass", - "Entity", - Some(&[EvalAttributeArg::String("model".to_string())]), - ); - let invalid_record = [99, 0, 0, 0, 0]; - - let method_registered = unsafe { - __elephc_eval_register_native_member_attribute( - &mut ctx, - method_record.as_ptr(), - method_record.len() as u64, - ) - }; - let property_registered = unsafe { - __elephc_eval_register_native_member_attribute( - &mut ctx, - property_record.as_ptr(), - property_record.len() as u64, - ) - }; - let constant_registered = unsafe { - __elephc_eval_register_native_member_attribute( - &mut ctx, - constant_record.as_ptr(), - constant_record.len() as u64, - ) - }; - let class_registered = unsafe { - __elephc_eval_register_native_member_attribute( - &mut ctx, - class_record.as_ptr(), - class_record.len() as u64, - ) - }; - let invalid_registered = unsafe { - __elephc_eval_register_native_member_attribute( - &mut ctx, - invalid_record.as_ptr(), - invalid_record.len() as u64, - ) - }; - - assert_eq!(method_registered, 1); - let method_attributes = ctx.native_method_attributes("knownclass", "RUN"); - assert_eq!(method_attributes.len(), 1); - assert_eq!(method_attributes[0].name(), "Route"); - assert_eq!( - method_attributes[0].args(), - Some( - [ - EvalAttributeArg::String("api".to_string()), - EvalAttributeArg::Named { - name: "path".to_string(), - value: Box::new(EvalAttributeArg::String("/users".to_string())), - }, - EvalAttributeArg::Int(7), - EvalAttributeArg::Float(1.5f64.to_bits()), - EvalAttributeArg::Array(vec![ - EvalAttributeArg::Int(1), - EvalAttributeArg::String("two".to_string()), - ]), - EvalAttributeArg::Bool(true), - EvalAttributeArg::Null, - ] - .as_slice() - ) - ); - assert_eq!(property_registered, 1); - let property_attributes = ctx.native_property_attributes("KnownClass", "id"); - assert_eq!(property_attributes.len(), 1); - assert_eq!(property_attributes[0].name(), "Column"); - assert!(property_attributes[0].args().is_none()); - assert_eq!(constant_registered, 1); - let constant_attributes = ctx.native_constant_attributes("KnownClass", "LIMIT"); - assert_eq!(constant_attributes.len(), 1); - assert_eq!(constant_attributes[0].name(), "Limit"); - assert_eq!( - constant_attributes[0].args(), - Some([EvalAttributeArg::Int(100)].as_slice()) - ); - assert_eq!(class_registered, 1); - let class_attributes = ctx.native_class_attributes("knownclass"); - assert_eq!(class_attributes.len(), 1); - assert_eq!(class_attributes[0].name(), "Entity"); - assert_eq!( - class_attributes[0].args(), - Some([EvalAttributeArg::String("model".to_string())].as_slice()) - ); - assert_eq!(invalid_registered, 0); -} - -/// Verifies scope allocation returns an empty opaque activation scope handle. -#[test] -fn scope_new_returns_empty_handle() { - let scope = __elephc_eval_scope_new(); - assert!(!scope.is_null()); - let generation = unsafe { (*scope).generation() }; - unsafe { - __elephc_eval_scope_free(scope); - } - assert_eq!(generation, 0); -} - -/// Verifies execute rejects contexts whose ABI version no longer matches. -#[test] -fn execute_rejects_mismatched_context_version() { - let mut ctx = ElephcEvalContext::for_abi_version(ABI_VERSION + 1); - let status = unsafe { - __elephc_eval_execute( - &mut ctx, - std::ptr::null_mut(), - std::ptr::null(), - 0, - std::ptr::null_mut(), - ) - }; - - assert_eq!(status, EvalStatus::AbiMismatch.code()); -} - -/// Verifies execute maps invalid eval fragments to the stable parse status. -#[test] -fn execute_rejects_php_opening_tags_as_parse_errors() { - let code = b">(); + let expected_default = NativeCallableDefault::Object { + class_name: "LargeDefault".to_string(), + args: args.clone(), + }; + let spec = native_object_default_record("LargeDefault", &args); + + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_object( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(constructor_registered, 1); + assert_eq!(default_registered, 1); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); +} + +/// Verifies native AOT array defaults can carry keyed and nested default values. +#[test] +fn register_native_array_default_decodes_nested_values() { + let mut ctx = ElephcEvalContext::new(); + let method = b"KnownClass::items"; + let class = b"KnownClass"; + let property = b"KnownClass::items"; + let elements = vec![ + NativeCallableArrayDefaultElement::keyed( + NativeCallableArrayDefaultKey::String("left".to_string()), + NativeCallableDefault::String("L".to_string()), + ), + NativeCallableArrayDefaultElement::keyed( + NativeCallableArrayDefaultKey::Int(2), + NativeCallableDefault::Array(vec![NativeCallableArrayDefaultElement::positional( + NativeCallableDefault::Int(7), + )]), + ), + NativeCallableArrayDefaultElement::positional(NativeCallableDefault::Object { + class_name: "ArrayDefaultDep".to_string(), + args: vec![NativeCallableObjectDefaultArg::named( + "label", + NativeCallableDefault::String("dep".to_string()), + )], + }), + ]; + let expected_default = NativeCallableDefault::Array(elements.clone()); + let spec = native_array_default_record(&elements); + + let method_registered = unsafe { + __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 1) + }; + let method_default_registered = unsafe { + __elephc_eval_register_native_method_param_default_array( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let constructor_default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_array( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + spec.as_ptr(), + spec.len() as u64, + ) + }; + let property_default_registered = unsafe { + __elephc_eval_register_native_property_default_array( + &mut ctx, + property.as_ptr(), + property.len() as u64, + spec.as_ptr(), + spec.len() as u64, + ) + }; + + assert_eq!(method_registered, 1); + assert_eq!(method_default_registered, 1); + assert_eq!(constructor_registered, 1); + assert_eq!(constructor_default_registered, 1); + assert_eq!(property_default_registered, 1); + assert_eq!( + ctx.native_method_signature("knownclass", "ITEMS") + .expect("method metadata") + .param_default(0), + Some(&expected_default) + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&expected_default) + ); + assert_eq!( + ctx.native_property_default("KnownClass", "items"), + Some(expected_default) + ); +} diff --git a/crates/elephc-magician/src/ffi/tests/class_metadata.rs b/crates/elephc-magician/src/ffi/tests/class_metadata.rs new file mode 100644 index 0000000000..f4e751457e --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/class_metadata.rs @@ -0,0 +1,346 @@ +//! Purpose: +//! Tests native parent, property, interface, abstract-property, default, and +//! attribute metadata registration. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Every metadata family is read back through `ElephcEvalContext`. + +use super::*; + +/// Verifies native AOT parent metadata is available for eval static-scope resolution. +#[test] +fn register_native_class_parent_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let class = b"KnownChild"; + let parent = b"KnownParent"; + + let registered = unsafe { + __elephc_eval_register_native_class_parent( + &mut ctx, + class.as_ptr(), + class.len() as u64, + parent.as_ptr(), + parent.len() as u64, + ) + }; + + assert_eq!(registered, 1); + assert_eq!(ctx.native_class_parent("knownchild"), Some("KnownParent")); +} + +/// Verifies native AOT property type metadata is available to eval reflection. +#[test] +fn register_native_property_type_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownClass::name"; + let property_type = b"?KnownDep"; + let invalid_property = b"KnownClass::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_property_type( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_property_type( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + ) + }; + + assert_eq!(registered, 1); + let property_type = ctx + .native_property_type("knownclass", "name") + .expect("property type metadata"); + assert!(property_type.allows_null()); + assert_eq!( + property_type.variants(), + &[EvalParameterTypeVariant::Class("KnownDep".to_string())] + ); + assert_eq!(invalid_registered, 0); + assert!(ctx.native_property_type("KnownClass", "bad").is_none()); +} + +/// Verifies native AOT interface property contracts are available to eval validation. +#[test] +fn register_native_interface_property_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownContract::KnownParentContract::name"; + let property_type = b"string"; + let invalid_property = b"KnownContract::KnownParentContract::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_interface_property( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET | TEST_NATIVE_PROPERTY_REQUIRES_SET, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_interface_property( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET, + ) + }; + + let requirements = ctx.native_interface_property_requirements("knowncontract"); + assert_eq!(registered, 1); + assert_eq!(requirements.len(), 1); + assert_eq!(requirements[0].0, "KnownParentContract"); + assert_eq!(requirements[0].1.name(), "name"); + assert!(requirements[0].1.requires_get()); + assert!(requirements[0].1.requires_set()); + assert_eq!( + requirements[0].1.property_type().map(|ty| ty.variants()), + Some(&[EvalParameterTypeVariant::String][..]) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_interface_property_requirements("KnownContract") + .iter() + .all(|(_, property)| property.name() != "bad")); +} + +/// Verifies native AOT abstract class property contracts are available to eval validation. +#[test] +fn register_native_abstract_property_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let property = b"KnownClass::KnownParent::name"; + let property_type = b"string"; + let invalid_property = b"KnownClass::KnownParent::bad"; + let invalid_type = b"void"; + + let registered = unsafe { + __elephc_eval_register_native_abstract_property( + &mut ctx, + property.as_ptr(), + property.len() as u64, + property_type.as_ptr(), + property_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET | TEST_NATIVE_PROPERTY_REQUIRES_SET, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_abstract_property( + &mut ctx, + invalid_property.as_ptr(), + invalid_property.len() as u64, + invalid_type.as_ptr(), + invalid_type.len() as u64, + TEST_NATIVE_PROPERTY_REQUIRES_GET, + ) + }; + + let requirements = ctx.native_abstract_property_requirements("knownclass"); + assert_eq!(registered, 1); + assert_eq!(requirements.len(), 1); + assert_eq!(requirements[0].0, "KnownParent"); + assert_eq!(requirements[0].1.name(), "name"); + assert!(requirements[0].1.requires_get()); + assert!(requirements[0].1.requires_set()); + assert_eq!( + requirements[0].1.property_type().map(|ty| ty.variants()), + Some(&[EvalParameterTypeVariant::String][..]) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_abstract_property_requirements("KnownClass") + .iter() + .all(|(_, property)| property.name() != "bad")); +} + +/// Verifies native AOT property default metadata is available to eval reflection. +#[test] +fn register_native_property_default_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let count = b"KnownClass::count"; + let label = b"KnownClass::label"; + let invalid = b"KnownClass::invalid"; + let label_value = b"ok"; + + let scalar_registered = unsafe { + __elephc_eval_register_native_property_default_scalar( + &mut ctx, + count.as_ptr(), + count.len() as u64, + 2, + 42, + ) + }; + let string_registered = unsafe { + __elephc_eval_register_native_property_default_string( + &mut ctx, + label.as_ptr(), + label.len() as u64, + label_value.as_ptr(), + label_value.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_property_default_scalar( + &mut ctx, + invalid.as_ptr(), + invalid.len() as u64, + 99, + 0, + ) + }; + + assert_eq!(scalar_registered, 1); + assert_eq!( + ctx.native_property_default("knownclass", "count"), + Some(NativeCallableDefault::Int(42)) + ); + assert_eq!(string_registered, 1); + assert_eq!( + ctx.native_property_default("KnownClass", "label"), + Some(NativeCallableDefault::String("ok".to_string())) + ); + assert_eq!(invalid_registered, 0); + assert!(ctx + .native_property_default("KnownClass", "invalid") + .is_none()); +} + +/// Verifies native AOT member attributes are available to eval reflection. +#[test] +fn register_native_member_attribute_records_metadata() { + let mut ctx = ElephcEvalContext::new(); + let method_record = native_member_attribute_record( + 0, + "KnownClass::run", + "Route", + Some(&[ + EvalAttributeArg::String("api".to_string()), + EvalAttributeArg::Named { + name: "path".to_string(), + value: Box::new(EvalAttributeArg::String("/users".to_string())), + }, + EvalAttributeArg::Int(7), + EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::Int(1), + EvalAttributeArg::String("two".to_string()), + ]), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + ]), + ); + let property_record = native_member_attribute_record(1, "KnownClass::id", "Column", None); + let constant_record = native_member_attribute_record( + 2, + "KnownClass::LIMIT", + "Limit", + Some(&[EvalAttributeArg::Int(100)]), + ); + let class_record = native_member_attribute_record( + 3, + "KnownClass", + "Entity", + Some(&[EvalAttributeArg::String("model".to_string())]), + ); + let invalid_record = [99, 0, 0, 0, 0]; + + let method_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + method_record.as_ptr(), + method_record.len() as u64, + ) + }; + let property_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + property_record.as_ptr(), + property_record.len() as u64, + ) + }; + let constant_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + constant_record.as_ptr(), + constant_record.len() as u64, + ) + }; + let class_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + class_record.as_ptr(), + class_record.len() as u64, + ) + }; + let invalid_registered = unsafe { + __elephc_eval_register_native_member_attribute( + &mut ctx, + invalid_record.as_ptr(), + invalid_record.len() as u64, + ) + }; + + assert_eq!(method_registered, 1); + let method_attributes = ctx.native_method_attributes("knownclass", "RUN"); + assert_eq!(method_attributes.len(), 1); + assert_eq!(method_attributes[0].name(), "Route"); + assert_eq!( + method_attributes[0].args(), + Some( + [ + EvalAttributeArg::String("api".to_string()), + EvalAttributeArg::Named { + name: "path".to_string(), + value: Box::new(EvalAttributeArg::String("/users".to_string())), + }, + EvalAttributeArg::Int(7), + EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::Int(1), + EvalAttributeArg::String("two".to_string()), + ]), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + ] + .as_slice() + ) + ); + assert_eq!(property_registered, 1); + let property_attributes = ctx.native_property_attributes("KnownClass", "id"); + assert_eq!(property_attributes.len(), 1); + assert_eq!(property_attributes[0].name(), "Column"); + assert!(property_attributes[0].args().is_none()); + assert_eq!(constant_registered, 1); + let constant_attributes = ctx.native_constant_attributes("KnownClass", "LIMIT"); + assert_eq!(constant_attributes.len(), 1); + assert_eq!(constant_attributes[0].name(), "Limit"); + assert_eq!( + constant_attributes[0].args(), + Some([EvalAttributeArg::Int(100)].as_slice()) + ); + assert_eq!(class_registered, 1); + let class_attributes = ctx.native_class_attributes("knownclass"); + assert_eq!(class_attributes.len(), 1); + assert_eq!(class_attributes[0].name(), "Entity"); + assert_eq!( + class_attributes[0].args(), + Some([EvalAttributeArg::String("model".to_string())].as_slice()) + ); + assert_eq!(invalid_registered, 0); +} diff --git a/crates/elephc-magician/src/ffi/tests/context_symbols.rs b/crates/elephc-magician/src/ffi/tests/context_symbols.rs new file mode 100644 index 0000000000..28c626eb88 --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/context_symbols.rs @@ -0,0 +1,312 @@ +//! Purpose: +//! Tests eval ABI versioning, context state, call-site scopes, and declared +//! symbol visibility. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Context and symbol registration are exercised without generated assembly. + +use super::*; + +/// Verifies the exported version entry point reports the crate ABI constant. +#[test] +fn abi_version_matches_constant() { + assert_eq!(__elephc_eval_abi_version(), ABI_VERSION); +} + +/// Verifies the initial execute stub clears result storage and returns the +/// documented unsupported status instead of panicking or succeeding. +#[test] +fn execute_stub_returns_unsupported_and_clears_result() { + let mut result = ElephcEvalResult { + kind: 99, + value_cell: 1usize as *mut std::ffi::c_void, + error: 2usize as *mut std::ffi::c_void, + }; + let status = unsafe { + __elephc_eval_execute( + std::ptr::null_mut(), + std::ptr::null_mut(), + b"$x = 1;".as_ptr(), + 7, + &mut result, + ) + }; + assert_eq!(status, EvalStatus::UnsupportedConstruct.code()); + assert_eq!(result.kind, 0); + assert!(result.value_cell.is_null()); + assert!(result.error.is_null()); +} + +/// Verifies context allocation returns a current-version opaque handle. +#[test] +fn context_new_returns_current_version_handle() { + let ctx = __elephc_eval_context_new(); + assert!(!ctx.is_null()); + let version = unsafe { (*ctx).abi_version() }; + unsafe { + __elephc_eval_context_free(ctx); + } + assert_eq!(version, ABI_VERSION); +} + +/// Verifies call-site metadata can be set through the stable context ABI. +#[test] +fn context_set_call_site_records_file_dir_and_line() { + let mut ctx = ElephcEvalContext::new(); + let file = b"/tmp/source.php"; + let dir = b"/tmp"; + + let status = unsafe { + __elephc_eval_context_set_call_site( + &mut ctx, + file.as_ptr(), + file.len() as u64, + dir.as_ptr(), + dir.len() as u64, + 9, + ) + }; + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!(ctx.call_dir(), "/tmp"); + assert_eq!(ctx.eval_file_magic(), "/tmp/source.php(9) : eval()'d code"); +} + +/// Verifies the context ABI records a non-owned global scope handle. +#[test] +fn context_set_global_scope_records_handle() { + let mut ctx = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + + let status = unsafe { __elephc_eval_context_set_global_scope(&mut ctx, &mut scope) }; + + assert_eq!(status, EvalStatus::Ok.code()); + assert_eq!( + ctx.global_scope_ptr(), + Some(&mut scope as *mut ElephcEvalScope) + ); +} + +/// Verifies generated class scopes are pushed and popped through the context ABI. +#[test] +fn context_push_class_scope_records_self_and_called_class() { + let mut ctx = ElephcEvalContext::new(); + let class_name = b"AotBase"; + let called_class_name = b"AotChild"; + + let push_status = unsafe { + __elephc_eval_context_push_class_scope( + &mut ctx, + class_name.as_ptr(), + class_name.len() as u64, + called_class_name.as_ptr(), + called_class_name.len() as u64, + ) + }; + + assert_eq!(push_status, EvalStatus::Ok.code()); + assert_eq!(ctx.current_class_scope(), Some("AotBase")); + assert_eq!(ctx.current_called_class_scope(), Some("AotChild")); + + let pop_status = unsafe { __elephc_eval_context_pop_class_scope(&mut ctx) }; + + assert_eq!(pop_status, EvalStatus::Ok.code()); + assert_eq!(ctx.current_class_scope(), None); + assert_eq!(ctx.current_called_class_scope(), None); +} + +/// Verifies generated frames can query eval late-static overrides without a context handle. +#[test] +fn native_frame_called_class_override_reports_thread_local_scope() { + let class_name = b"AotBase"; + let mut out_ptr = std::ptr::null(); + let mut out_len = 0; + + let missing = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(missing, 0); + assert!(out_ptr.is_null()); + assert_eq!(out_len, 0); + + { + let _guard = push_native_frame_called_class_override( + std::ptr::null_mut(), + "AotBase", + "EvalChild", + ); + + let found = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(found, 1); + let bytes = unsafe { std::slice::from_raw_parts(out_ptr, out_len as usize) }; + assert_eq!(bytes, b"EvalChild"); + } + + let after_drop = unsafe { + __elephc_eval_native_frame_called_class_override( + class_name.as_ptr(), + class_name.len() as u64, + &mut out_ptr, + &mut out_len, + ) + }; + + assert_eq!(after_drop, 0); + assert!(out_ptr.is_null()); + assert_eq!(out_len, 0); +} + +/// Verifies generated declaration-name metadata is exposed through eval lists. +#[test] +fn register_declared_symbol_names_records_visible_metadata() { + let mut ctx = ElephcEvalContext::new(); + let class_name = b"\\AotDeclaredClass"; + let class_duplicate = b"aotdeclaredclass"; + let interface_name = b"AotDeclaredInterface"; + let trait_name = b"AotDeclaredTrait"; + let empty_name = b""; + + let class_registered = unsafe { + __elephc_eval_register_declared_class_name( + &mut ctx, + class_name.as_ptr(), + class_name.len() as u64, + ) + }; + let duplicate_registered = unsafe { + __elephc_eval_register_declared_class_name( + &mut ctx, + class_duplicate.as_ptr(), + class_duplicate.len() as u64, + ) + }; + let interface_registered = unsafe { + __elephc_eval_register_declared_interface_name( + &mut ctx, + interface_name.as_ptr(), + interface_name.len() as u64, + ) + }; + let trait_registered = unsafe { + __elephc_eval_register_declared_trait_name( + &mut ctx, + trait_name.as_ptr(), + trait_name.len() as u64, + ) + }; + let empty_rejected = unsafe { + __elephc_eval_register_declared_trait_name( + &mut ctx, + empty_name.as_ptr(), + empty_name.len() as u64, + ) + }; + + assert_eq!(class_registered, 1); + assert_eq!(duplicate_registered, 1); + assert_eq!(interface_registered, 1); + assert_eq!(trait_registered, 1); + assert_eq!(empty_rejected, 0); + assert_eq!(ctx.declared_class_names(), &["AotDeclaredClass".to_string()]); + assert_eq!( + ctx.declared_interface_names(), + &["AotDeclaredInterface".to_string()] + ); + assert_eq!(ctx.declared_trait_names(), &["AotDeclaredTrait".to_string()]); +} + +/// Verifies the function-exists ABI probes eval-declared functions by folded name. +#[test] +fn function_exists_reports_declared_eval_function() { + let mut ctx = ElephcEvalContext::new(); + ctx.define_function( + "dyn_probe", + crate::eval_ir::EvalFunction::new("dyn_probe", Vec::new(), Vec::new()), + ) + .expect("first dynamic function declaration should succeed"); + let existing = b"DYN_PROBE"; + let missing = b"missing"; + + let existing_result = + unsafe { __elephc_eval_function_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; + let missing_result = + unsafe { __elephc_eval_function_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(missing_result, 0); +} + +/// Verifies the constant-exists ABI probes eval-defined constants by PHP name. +#[test] +fn constant_exists_reports_defined_eval_constant() { + let mut ctx = ElephcEvalContext::new(); + let value = RuntimeCellHandle::from_raw(1usize as *mut RuntimeCell); + assert!(ctx.define_constant("DynConstProbe", value)); + let existing = b"DynConstProbe"; + let qualified = b"\\DynConstProbe"; + let wrong_case = b"dynconstprobe"; + let missing = b"missing"; + + let existing_result = + unsafe { __elephc_eval_constant_exists(&ctx, existing.as_ptr(), existing.len() as u64) }; + let qualified_result = + unsafe { __elephc_eval_constant_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) }; + let wrong_case_result = unsafe { + __elephc_eval_constant_exists(&ctx, wrong_case.as_ptr(), wrong_case.len() as u64) + }; + let missing_result = + unsafe { __elephc_eval_constant_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(qualified_result, 1); + assert_eq!(wrong_case_result, 0); + assert_eq!(missing_result, 0); +} + +/// Verifies the dynamic-class-exists ABI probes eval-declared classes by folded PHP name. +#[test] +fn dynamic_class_exists_reports_declared_eval_class() { + let mut ctx = ElephcEvalContext::new(); + assert!(ctx.define_class(crate::eval_ir::EvalClass::new( + "DynClassProbe", + Vec::new(), + Vec::new() + ))); + let existing = b"DynClassProbe"; + let qualified = b"\\DynClassProbe"; + let folded = b"dynclassprobe"; + let missing = b"missing"; + + let existing_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, existing.as_ptr(), existing.len() as u64) + }; + let qualified_result = unsafe { + __elephc_eval_dynamic_class_exists(&ctx, qualified.as_ptr(), qualified.len() as u64) + }; + let folded_result = + unsafe { __elephc_eval_dynamic_class_exists(&ctx, folded.as_ptr(), folded.len() as u64) }; + let missing_result = + unsafe { __elephc_eval_dynamic_class_exists(&ctx, missing.as_ptr(), missing.len() as u64) }; + + assert_eq!(existing_result, 1); + assert_eq!(qualified_result, 1); + assert_eq!(folded_result, 1); + assert_eq!(missing_result, 0); +} diff --git a/crates/elephc-magician/src/ffi/tests/mod.rs b/crates/elephc-magician/src/ffi/tests/mod.rs new file mode 100644 index 0000000000..13aee70dbe --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/mod.rs @@ -0,0 +1,223 @@ +//! Purpose: +//! Unit tests for the eval C ABI layer. +//! They validate handle allocation, stable status codes, scope flags, and +//! dynamic symbol registration without requiring generated runtime assembly. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Execute remains a controlled unsupported stub in crate unit tests. +//! - Fake runtime-cell pointers are never dereferenced by these tests. + +use super::context::*; +use super::declared_symbols::*; +use super::execute::*; +use super::native_functions::*; +use super::native_methods::*; +use super::scope::*; +use super::symbols::*; +use crate::abi::{ + ElephcEvalContext, ElephcEvalResult, ElephcEvalScope, ABI_VERSION, SCOPE_FLAG_DIRTY, + SCOPE_FLAG_OWNED, SCOPE_FLAG_PRESENT, SCOPE_FLAG_UNSET, +}; +use crate::context::{ + NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, NativeCallableDefault, + NativeCallableObjectDefaultArg, push_native_frame_called_class_override, +}; +use crate::errors::EvalStatus; +use crate::eval_ir::{EvalAttributeArg, EvalParameterTypeVariant}; +use crate::value::{RuntimeCell, RuntimeCellHandle}; +use std::ffi::c_void; + +mod callable_defaults; +mod class_metadata; +mod context_symbols; +mod native_functions; +mod native_methods; +mod scope_execution; + +const TEST_NATIVE_DEFAULT_NULL: u64 = 0; +const TEST_NATIVE_DEFAULT_BOOL: u64 = 1; +const TEST_NATIVE_DEFAULT_INT: u64 = 2; +const TEST_NATIVE_DEFAULT_FLOAT: u64 = 3; +const TEST_NATIVE_DEFAULT_EMPTY_ARRAY: u64 = 4; +const TEST_NATIVE_PROPERTY_REQUIRES_GET: u64 = 1; +const TEST_NATIVE_PROPERTY_REQUIRES_SET: u64 = 2; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR: u8 = 0; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING: u8 = 1; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT: u8 = 2; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED: u8 = 3; +const TEST_NATIVE_OBJECT_DEFAULT_ARG_ARRAY: u8 = 4; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_AUTO: u8 = 0; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_INT: u8 = 1; +const TEST_NATIVE_ARRAY_DEFAULT_KEY_STRING: u8 = 2; +const TEST_MAX_NATIVE_OBJECT_DEFAULT_ARGS: usize = u8::MAX as usize; + +/// Test native invoker placeholder used only to validate ABI registration. +unsafe extern "C" fn fake_native_invoker( + _descriptor: *mut c_void, + _args: *mut RuntimeCell, +) -> *mut RuntimeCell { + std::ptr::null_mut() +} + +/// Builds one native member-attribute ABI record for registration tests. +fn native_member_attribute_record( + owner_kind: u8, + member_key: &str, + attribute_name: &str, + args: Option<&[EvalAttributeArg]>, +) -> Vec { + let mut record = Vec::new(); + record.push(owner_kind); + native_member_attribute_push_string(&mut record, member_key); + native_member_attribute_push_string(&mut record, attribute_name); + match args { + Some(args) => { + record.push(1); + record.extend_from_slice(&(args.len() as u32).to_le_bytes()); + for arg in args { + native_member_attribute_push_arg(&mut record, arg); + } + } + None => record.push(0), + } + record +} + +/// Appends one test attribute argument to a native member-attribute ABI record. +fn native_member_attribute_push_arg(record: &mut Vec, arg: &EvalAttributeArg) { + match arg { + EvalAttributeArg::Null => record.push(0), + EvalAttributeArg::Bool(value) => { + record.push(1); + record.push(u8::from(*value)); + } + EvalAttributeArg::Int(value) => { + record.push(2); + record.extend_from_slice(&value.to_le_bytes()); + } + EvalAttributeArg::Float(bits) => { + record.push(5); + record.extend_from_slice(&bits.to_le_bytes()); + } + EvalAttributeArg::String(value) => { + record.push(3); + native_member_attribute_push_string(record, value); + } + EvalAttributeArg::Named { name, value } => { + record.push(4); + native_member_attribute_push_string(record, name); + native_member_attribute_push_arg(record, value); + } + EvalAttributeArg::IntKeyed { .. } => { + panic!("native attribute test ABI does not encode int-keyed array arguments") + } + EvalAttributeArg::Array(elements) => { + record.push(6); + record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); + for element in elements { + native_member_attribute_push_arg(record, element); + } + } + } +} + +/// Appends one length-prefixed string to a native member-attribute ABI record. +fn native_member_attribute_push_string(record: &mut Vec, value: &str) { + record.extend_from_slice(&(value.len() as u32).to_le_bytes()); + record.extend_from_slice(value.as_bytes()); +} + +/// Builds one object-valued native parameter default ABI record for registration tests. +fn native_object_default_record( + class_name: &str, + args: &[NativeCallableObjectDefaultArg], +) -> Vec { + let mut record = Vec::new(); + native_member_attribute_push_string(&mut record, class_name); + record.push(args.len() as u8); + for arg in args { + native_object_default_push_arg(&mut record, arg); + } + record +} + +/// Builds one array-valued native parameter default ABI record for registration tests. +fn native_array_default_record(elements: &[NativeCallableArrayDefaultElement]) -> Vec { + let mut record = Vec::new(); + record.extend_from_slice(&(elements.len() as u32).to_le_bytes()); + for element in elements { + native_array_default_push_element(&mut record, element); + } + record +} + +/// Appends one array-default element and optional static key to a default record. +fn native_array_default_push_element( + record: &mut Vec, + element: &NativeCallableArrayDefaultElement, +) { + match &element.key { + Some(NativeCallableArrayDefaultKey::Int(value)) => { + record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_INT); + record.extend_from_slice(&value.to_le_bytes()); + } + Some(NativeCallableArrayDefaultKey::String(value)) => { + record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_STRING); + native_member_attribute_push_string(record, value); + } + None => record.push(TEST_NATIVE_ARRAY_DEFAULT_KEY_AUTO), + } + native_object_default_push_arg_value(record, &element.value); +} + +/// Appends one object-default constructor argument to a native parameter default record. +fn native_object_default_push_arg(record: &mut Vec, arg: &NativeCallableObjectDefaultArg) { + if let Some(name) = &arg.name { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_NAMED); + native_member_attribute_push_string(record, name); + } + native_object_default_push_arg_value(record, &arg.value); +} + +/// Appends one object-default constructor argument value to a native parameter default record. +fn native_object_default_push_arg_value(record: &mut Vec, value: &NativeCallableDefault) { + match value { + NativeCallableDefault::Null => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_NULL, 0) + } + NativeCallableDefault::Bool(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_BOOL, u64::from(*value)) + } + NativeCallableDefault::Int(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_INT, *value as u64) + } + NativeCallableDefault::Float(value) => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_FLOAT, value.to_bits()) + } + NativeCallableDefault::String(value) => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_STRING); + native_member_attribute_push_string(record, value); + } + NativeCallableDefault::EmptyArray => { + native_object_default_push_scalar(record, TEST_NATIVE_DEFAULT_EMPTY_ARRAY, 0) + } + NativeCallableDefault::Object { class_name, args } => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_OBJECT); + record.extend_from_slice(&native_object_default_record(class_name, args)); + } + NativeCallableDefault::Array(elements) => { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_ARRAY); + record.extend_from_slice(&native_array_default_record(elements)); + } + } +} + +/// Appends one scalar object-default constructor argument to a native parameter default record. +fn native_object_default_push_scalar(record: &mut Vec, kind: u64, payload: u64) { + record.push(TEST_NATIVE_OBJECT_DEFAULT_ARG_SCALAR); + record.extend_from_slice(&kind.to_le_bytes()); + record.extend_from_slice(&payload.to_le_bytes()); +} diff --git a/crates/elephc-magician/src/ffi/tests/native_functions.rs b/crates/elephc-magician/src/ffi/tests/native_functions.rs new file mode 100644 index 0000000000..4f807d9d75 --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/native_functions.rs @@ -0,0 +1,144 @@ +//! Purpose: +//! Tests native function signature, parameter, type, and default registration +//! through the eval C ABI. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Registration metadata is read back from the same eval context. + +use super::*; + +/// Verifies native AOT registration records function parameter metadata and defaults. +#[test] +fn register_native_function_reports_function_exists() { + let mut ctx = ElephcEvalContext::new(); + let name = b"NATIVE_PROBE"; + let param = b"value"; + let variadic = b"items"; + let param_type = b"?string"; + let return_type = b"int"; + let descriptor = 1usize as *mut c_void; + + let registered = unsafe { + __elephc_eval_register_native_function( + &mut ctx, + name.as_ptr(), + name.len() as u64, + descriptor, + Some(fake_native_invoker), + 2, + ) + }; + let param_registered = unsafe { + __elephc_eval_register_native_function_param( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + param.as_ptr(), + param.len() as u64, + ) + }; + let variadic_param_registered = unsafe { + __elephc_eval_register_native_function_param( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 1, + variadic.as_ptr(), + variadic.len() as u64, + ) + }; + let bridge_support_registered = unsafe { + __elephc_eval_register_native_function_bridge_support( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + ) + }; + let param_flags_registered = unsafe { + __elephc_eval_register_native_function_param_flags( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + 1, + 0, + ) + }; + let variadic_flags_registered = unsafe { + __elephc_eval_register_native_function_param_flags( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 1, + 0, + 1, + ) + }; + let param_type_registered = unsafe { + __elephc_eval_register_native_function_param_type( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + param_type.as_ptr(), + param_type.len() as u64, + ) + }; + let return_type_registered = unsafe { + __elephc_eval_register_native_function_return_type( + &mut ctx, + name.as_ptr(), + name.len() as u64, + return_type.as_ptr(), + return_type.len() as u64, + ) + }; + let param_default_registered = unsafe { + __elephc_eval_register_native_function_param_default_scalar( + &mut ctx, + name.as_ptr(), + name.len() as u64, + 0, + TEST_NATIVE_DEFAULT_INT, + 42, + ) + }; + let exists = unsafe { __elephc_eval_function_exists(&ctx, b"native_probe".as_ptr(), 12) }; + + assert_eq!(registered, 1); + let native = ctx + .native_function("native_probe") + .expect("native function should be registered"); + + assert_eq!(param_registered, 1); + assert_eq!(variadic_param_registered, 1); + assert_eq!(bridge_support_registered, 1); + assert_eq!(param_flags_registered, 1); + assert_eq!(variadic_flags_registered, 1); + assert_eq!(param_type_registered, 1); + assert_eq!(return_type_registered, 1); + assert_eq!(param_default_registered, 1); + assert_eq!(exists, 1); + assert_eq!( + native.param_names(), + &["value".to_string(), "items".to_string()] + ); + let native_type = native.param_type(0).expect("native parameter type"); + assert!(native_type.allows_null()); + assert_eq!(native_type.variants(), &[EvalParameterTypeVariant::String]); + assert!(native.param_by_ref(0)); + assert!(!native.param_variadic(0)); + assert!(native.param_variadic(1)); + assert!(!native.bridge_supported()); + assert_eq!(native.required_param_count(), 0); + assert_eq!( + native.return_type().expect("native return type").variants(), + &[EvalParameterTypeVariant::Int] + ); + assert_eq!(native.param_default(0), Some(&NativeCallableDefault::Int(42))); +} diff --git a/crates/elephc-magician/src/ffi/tests/native_methods.rs b/crates/elephc-magician/src/ffi/tests/native_methods.rs new file mode 100644 index 0000000000..7ca141d9ac --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/native_methods.rs @@ -0,0 +1,296 @@ +//! Purpose: +//! Tests native instance, static, constructor, interface, and abstract member +//! signature registration through the eval C ABI. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Parameters, flags, types, defaults, and bridge support are checked together. + +use super::*; + +/// Verifies native AOT method registration records instance/static/constructor parameters. +#[test] +fn register_native_methods_record_signature_metadata() { + let mut ctx = ElephcEvalContext::new(); + let method = b"KnownClass::join"; + let static_method = b"KnownClass::sum"; + let class = b"KnownClass"; + let left = b"left"; + let right = b"right"; + let value = b"value"; + let method_type = b"int|string|null"; + let static_type = b"?string"; + let constructor_type = b"KnownDep"; + let return_type = b"bool"; + + let method_registered = unsafe { + __elephc_eval_register_native_method(&mut ctx, method.as_ptr(), method.len() as u64, 2) + }; + let method_param_registered = unsafe { + __elephc_eval_register_native_method_param( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + right.as_ptr(), + right.len() as u64, + ) + }; + let method_param_type_registered = unsafe { + __elephc_eval_register_native_method_param_type( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 0, + method_type.as_ptr(), + method_type.len() as u64, + ) + }; + let method_param_flags_registered = unsafe { + __elephc_eval_register_native_method_param_flags( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + 1, + 0, + ) + }; + let static_registered = unsafe { + __elephc_eval_register_native_static_method( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 2, + ) + }; + let static_param_registered = unsafe { + __elephc_eval_register_native_static_method_param( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + left.as_ptr(), + left.len() as u64, + ) + }; + let static_param_type_registered = unsafe { + __elephc_eval_register_native_static_method_param_type( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + static_type.as_ptr(), + static_type.len() as u64, + ) + }; + let static_param_flags_registered = unsafe { + __elephc_eval_register_native_static_method_param_flags( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 1, + 0, + 1, + ) + }; + let static_return_type_registered = unsafe { + __elephc_eval_register_native_static_method_return_type( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + return_type.as_ptr(), + return_type.len() as u64, + ) + }; + let constructor_registered = unsafe { + __elephc_eval_register_native_constructor(&mut ctx, class.as_ptr(), class.len() as u64, 1) + }; + let constructor_param_registered = unsafe { + __elephc_eval_register_native_constructor_param( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + value.as_ptr(), + value.len() as u64, + ) + }; + let constructor_param_type_registered = unsafe { + __elephc_eval_register_native_constructor_param_type( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + constructor_type.as_ptr(), + constructor_type.len() as u64, + ) + }; + let constructor_param_flags_registered = unsafe { + __elephc_eval_register_native_constructor_param_flags( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + 1, + 1, + ) + }; + let constructor_bridge_support_registered = unsafe { + __elephc_eval_register_native_constructor_bridge_support( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + ) + }; + let method_default_registered = unsafe { + __elephc_eval_register_native_method_param_default_string( + &mut ctx, + method.as_ptr(), + method.len() as u64, + 1, + right.as_ptr(), + right.len() as u64, + ) + }; + let static_default_registered = unsafe { + __elephc_eval_register_native_static_method_param_default_scalar( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 0, + 2, + 42, + ) + }; + let static_empty_array_default_registered = unsafe { + __elephc_eval_register_native_static_method_param_default_scalar( + &mut ctx, + static_method.as_ptr(), + static_method.len() as u64, + 1, + NATIVE_DEFAULT_EMPTY_ARRAY, + 0, + ) + }; + let constructor_default_registered = unsafe { + __elephc_eval_register_native_constructor_param_default_scalar( + &mut ctx, + class.as_ptr(), + class.len() as u64, + 0, + 1, + 1, + ) + }; + + assert_eq!(method_registered, 1); + assert_eq!(method_param_registered, 1); + assert_eq!(method_param_type_registered, 1); + assert_eq!(method_param_flags_registered, 1); + assert_eq!(static_registered, 1); + assert_eq!(static_param_registered, 1); + assert_eq!(static_param_type_registered, 1); + assert_eq!(static_param_flags_registered, 1); + assert_eq!(static_return_type_registered, 1); + assert_eq!(constructor_registered, 1); + assert_eq!(constructor_param_registered, 1); + assert_eq!(constructor_param_type_registered, 1); + assert_eq!(constructor_param_flags_registered, 1); + assert_eq!(constructor_bridge_support_registered, 1); + assert_eq!(method_default_registered, 1); + assert_eq!(static_default_registered, 1); + assert_eq!(static_empty_array_default_registered, 1); + assert_eq!(constructor_default_registered, 1); + assert_eq!( + ctx.native_method_signature("knownclass", "JOIN") + .expect("method metadata") + .param_names(), + &["".to_string(), "right".to_string()] + ); + let method_signature = ctx + .native_method_signature("knownclass", "JOIN") + .expect("method metadata"); + let method_type = method_signature + .param_type(0) + .expect("method parameter type"); + assert!(method_type.allows_null()); + assert_eq!( + method_type.variants(), + &[ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ] + ); + assert!(method_signature.param_by_ref(1)); + assert!(!method_signature.param_variadic(1)); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_names(), + &["left".to_string(), "".to_string()] + ); + let static_signature = ctx + .native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata"); + let static_type = static_signature + .param_type(0) + .expect("static method parameter type"); + assert!(static_type.allows_null()); + assert_eq!(static_type.variants(), &[EvalParameterTypeVariant::String]); + assert!(!static_signature.param_by_ref(1)); + assert!(static_signature.param_variadic(1)); + assert_eq!( + static_signature + .return_type() + .expect("static return type") + .variants(), + &[EvalParameterTypeVariant::Bool] + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_names(), + &["value".to_string()] + ); + let constructor_signature = ctx + .native_constructor_signature("knownclass") + .expect("constructor metadata"); + assert_eq!( + constructor_signature + .param_type(0) + .expect("constructor parameter type") + .variants(), + &[EvalParameterTypeVariant::Class("KnownDep".to_string())] + ); + assert!(constructor_signature.param_by_ref(0)); + assert!(constructor_signature.param_variadic(0)); + assert!(!constructor_signature.bridge_supported()); + assert_eq!( + ctx.native_method_signature("knownclass", "JOIN") + .expect("method metadata") + .param_default(1), + Some(&NativeCallableDefault::String("right".to_string())) + ); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_default(0), + Some(&NativeCallableDefault::Int(42)) + ); + assert_eq!( + ctx.native_static_method_signature("KnownClass", "SUM") + .expect("static method metadata") + .param_default(1), + Some(&NativeCallableDefault::EmptyArray) + ); + assert_eq!( + ctx.native_constructor_signature("knownclass") + .expect("constructor metadata") + .param_default(0), + Some(&NativeCallableDefault::Bool(true)) + ); +} diff --git a/crates/elephc-magician/src/ffi/tests/scope_execution.rs b/crates/elephc-magician/src/ffi/tests/scope_execution.rs new file mode 100644 index 0000000000..02eecdc1d5 --- /dev/null +++ b/crates/elephc-magician/src/ffi/tests/scope_execution.rs @@ -0,0 +1,175 @@ +//! Purpose: +//! Tests eval scope allocation, execute validation, scope cell flags, aliases, +//! unset, and dirty-state handling. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Fake runtime-cell pointers are stored and compared but never dereferenced. + +use super::*; + +/// Verifies scope allocation returns an empty opaque activation scope handle. +#[test] +fn scope_new_returns_empty_handle() { + let scope = __elephc_eval_scope_new(); + assert!(!scope.is_null()); + let generation = unsafe { (*scope).generation() }; + unsafe { + __elephc_eval_scope_free(scope); + } + assert_eq!(generation, 0); +} + +/// Verifies execute rejects contexts whose ABI version no longer matches. +#[test] +fn execute_rejects_mismatched_context_version() { + let mut ctx = ElephcEvalContext::for_abi_version(ABI_VERSION + 1); + let status = unsafe { + __elephc_eval_execute( + &mut ctx, + std::ptr::null_mut(), + std::ptr::null(), + 0, + std::ptr::null_mut(), + ) + }; + + assert_eq!(status, EvalStatus::AbiMismatch.code()); +} + +/// Verifies execute maps invalid eval fragments to the stable parse status. +#[test] +fn execute_rejects_php_opening_tags_as_parse_errors() { + let code = b" Date: Mon, 13 Jul 2026 13:26:58 +0200 Subject: [PATCH 1203/1208] refactor(magician): split interpreter dispatch modules --- .../interpreter/builtins/registry/callable.rs | 1243 +- .../registry/callable/array_dispatch.rs | 206 + .../builtins/registry/callable/execution.rs | 440 + .../registry/callable/object_dispatch.rs | 376 + .../registry/callable/static_dispatch.rs | 243 + .../src/interpreter/dynamic_functions.rs | 2212 +-- .../dynamic_functions/closure_execution.rs | 655 + .../dynamic_functions/function_binding.rs | 515 + .../dynamic_functions/method_binding.rs | 816 + .../dynamic_functions/native_execution.rs | 253 + .../src/interpreter/reflection.rs | 10358 +------------ .../interpreter/reflection/callable_api.rs | 610 + .../src/interpreter/reflection/class_api.rs | 899 ++ .../reflection/class_construction.rs | 440 + .../interpreter/reflection/class_lookup.rs | 814 + .../reflection/class_member_api.rs | 435 + .../reflection/constant_construction.rs | 280 + .../src/interpreter/reflection/flags.rs | 157 + .../src/interpreter/reflection/formatting.rs | 565 + .../reflection/function_construction.rs | 270 + .../reflection/function_metadata.rs | 934 ++ .../src/interpreter/reflection/invocation.rs | 270 + .../src/interpreter/reflection/member_api.rs | 558 + .../reflection/member_construction.rs | 1058 ++ .../interpreter/reflection/member_metadata.rs | 637 + .../reflection/owner_materialization.rs | 1139 ++ .../reflection/parameter_construction.rs | 280 + .../reflection/parameter_metadata.rs | 348 + .../interpreter/reflection/property_access.rs | 421 + .../reflection/property_helpers.rs | 393 + .../src/interpreter/statements.rs | 12519 +--------------- .../statements/abstract_requirements.rs | 508 + .../interpreter/statements/array_updates.rs | 590 + .../statements/attributes_magic_validation.rs | 620 + .../statements/callable_objects.rs | 242 + .../statements/class_declarations.rs | 123 + .../statements/class_resolution.rs | 512 + .../interpreter/statements/closure_binding.rs | 451 + .../src/interpreter/statements/dispatch.rs | 617 + .../statements/dynamic_method_execution.rs | 283 + .../statements/enum_declarations.rs | 392 + .../src/interpreter/statements/exceptions.rs | 147 + .../statements/instance_property_access.rs | 809 + .../statements/interface_contracts.rs | 736 + .../statements/interface_member_validation.rs | 360 + .../interpreter/statements/loop_statements.rs | 353 + .../interpreter/statements/method_dispatch.rs | 863 ++ .../statements/native_argument_binding.rs | 420 + .../statements/native_constructor_defaults.rs | 251 + .../statements/native_method_execution.rs | 480 + .../statements/native_static_dispatch.rs | 413 + .../property_constant_validation.rs | 371 + .../statements/property_validation.rs | 443 + .../statements/reference_writeback.rs | 509 + .../statements/reflection_instantiation.rs | 384 + .../statements/static_method_dispatch.rs | 240 + .../statements/static_property_access.rs | 827 + .../statements/trait_declarations.rs | 791 + 58 files changed, 26875 insertions(+), 26204 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/callable/array_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/callable/execution.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/callable/object_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/builtins/registry/callable/static_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/dynamic_functions/closure_execution.rs create mode 100644 crates/elephc-magician/src/interpreter/dynamic_functions/function_binding.rs create mode 100644 crates/elephc-magician/src/interpreter/dynamic_functions/method_binding.rs create mode 100644 crates/elephc-magician/src/interpreter/dynamic_functions/native_execution.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/callable_api.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/class_api.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/class_construction.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/class_lookup.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/class_member_api.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/constant_construction.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/flags.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/formatting.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/function_construction.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/function_metadata.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/invocation.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/member_api.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/member_construction.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/member_metadata.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/owner_materialization.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/parameter_construction.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/parameter_metadata.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/property_access.rs create mode 100644 crates/elephc-magician/src/interpreter/reflection/property_helpers.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/abstract_requirements.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/array_updates.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/attributes_magic_validation.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/callable_objects.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/class_declarations.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/class_resolution.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/closure_binding.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/dynamic_method_execution.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/enum_declarations.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/exceptions.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/instance_property_access.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/interface_contracts.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/interface_member_validation.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/loop_statements.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/method_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/native_argument_binding.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/native_constructor_defaults.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/native_method_execution.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/native_static_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/property_constant_validation.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/property_validation.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/reference_writeback.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/reflection_instantiation.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/static_method_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/static_property_access.rs create mode 100644 crates/elephc-magician/src/interpreter/statements/trait_declarations.rs diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs index ff4c484f6e..956c61dd68 100644 --- a/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable.rs @@ -1,5 +1,6 @@ //! Purpose: -//! call_user_func and callable normalization helpers. +//! Resolves PHP callbacks into normalized callable targets. +//! Invocation strategies live in focused child modules. //! //! Called from: //! - `crate::interpreter::builtins::registry` re-exports. @@ -7,12 +8,21 @@ //! Key details: //! - Helpers are scoped to the eval interpreter and operate on already parsed //! EvalIR call metadata or evaluated runtime-cell handles. -//! - This file is a deliberate >500 LoC single-scope callable engine: splitting -//! normalization, ref-target preservation, and invocation would spread one PHP -//! callable state machine across artificial modules. +//! - Callback normalization remains centralized while dispatch is grouped by +//! evaluated, object, static, and call-array entry surfaces. + +mod array_dispatch; +mod execution; +mod object_dispatch; +mod static_dispatch; use super::*; +pub(in crate::interpreter) use array_dispatch::*; +pub(in crate::interpreter) use execution::*; +use object_dispatch::*; +use static_dispatch::*; + /// Distinguishes PHP's invokable-object callback form from an explicit method callback. #[derive(Clone, Copy, PartialEq, Eq)] enum EvalObjectCallbackKind { @@ -422,1228 +432,3 @@ pub(in crate::interpreter) fn eval_string_callable( display_name, }) } - -/// Invokes an already normalized callback with source-order positional values. -pub(in crate::interpreter) fn eval_evaluated_callable_with_values( - callback: &EvaluatedCallable, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named { name, .. } => { - if let Some(closure) = context.closure(name).cloned() { - return eval_closure_with_evaluated_args( - &closure, - positional_args(evaluated_args), - context, - values, - ); - } - eval_callable_with_values(name, evaluated_args, context, values) - } - EvaluatedCallable::BoundClosure { - name, - bound_this, - bound_scope, - } => { - let Some(closure) = context.closure(name).cloned() else { - return eval_callable_with_values(name, evaluated_args, context, values); - }; - eval_bound_closure_with_call_args( - &closure, - *bound_this, - bound_scope.clone(), - positional_args(evaluated_args), - context, - values, - ) - } - EvaluatedCallable::InvokableObject { object } => { - eval_invokable_object_call_result( - *object, - positional_args(evaluated_args), - context, - values, - ) - } - EvaluatedCallable::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - } => match native_class { - Some(native_class) => eval_native_method_with_evaluated_args_unchecked_bridge_scope( - *object, - native_class, - method, - positional_args(evaluated_args), - bridge_scope.as_deref(), - called_class.as_deref(), - context, - values, - ), - None => eval_method_call_result(*object, method, evaluated_args, context, values), - }, - EvaluatedCallable::StaticMethod { - class_name, - method, - called_class, - native_class, - bridge_scope, - } => match native_class { - Some(native_class) => { - eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( - native_class, - method, - positional_args(evaluated_args), - bridge_scope.as_deref(), - called_class.as_deref(), - context, - values, - ) - } - None => match called_class { - Some(called_class) => eval_static_method_call_result_with_called_class( - class_name, - called_class, - method, - positional_args(evaluated_args), - context, - values, - ), - None if eval_callable_array_receiver_is_special_class_name(class_name) => { - eval_throw_class_not_found_error(class_name, context, values) - } - None => eval_static_method_call_result( - class_name, - method, - positional_args(evaluated_args), - context, - values, - ), - }, - }, - } -} - -/// Invokes a bound eval closure with either `$this` or only an explicit class scope. -fn eval_bound_closure_with_call_args( - closure: &EvalClosure, - bound_this: Option, - bound_scope: Option, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match bound_this { - Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope( - closure, - this_object, - bound_scope, - evaluated_args, - context, - values, - ), - None => eval_closure_with_evaluated_args_and_bound_scope( - closure, - bound_scope, - evaluated_args, - context, - values, - ), - } -} - -/// Invokes a bound eval closure with caller-selected by-reference binding flags. -fn eval_bound_closure_with_call_args_ref_flags( - closure: &EvalClosure, - bound_this: Option, - bound_scope: Option, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match bound_this { - Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope_ref_flags( - closure, - this_object, - bound_scope, - parameter_is_by_ref, - evaluated_args, - context, - values, - ), - None => eval_closure_with_evaluated_args_and_bound_scope_ref_flags( - closure, - bound_scope, - parameter_is_by_ref, - evaluated_args, - context, - values, - ), - } -} - -/// Invokes a bound eval closure with caller-selected by-reference binding mode. -fn eval_bound_closure_with_call_args_ref_mode( - closure: &EvalClosure, - bound_this: Option, - bound_scope: Option, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match bound_this { - Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope_ref_mode( - closure, - this_object, - bound_scope, - parameter_is_by_ref, - evaluated_args, - by_ref_mode, - context, - values, - ), - None => eval_closure_with_evaluated_args_and_bound_scope_ref_mode( - closure, - bound_scope, - parameter_is_by_ref, - evaluated_args, - by_ref_mode, - context, - values, - ), - } -} - -/// Invokes a normalized callback through `call_user_func()` by-value argument semantics. -fn eval_evaluated_callable_with_call_user_func_values( - callback: &EvaluatedCallable, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named { name, .. } => { - eval_named_callable_with_call_user_func_values(name, evaluated_args, context, values) - } - EvaluatedCallable::BoundClosure { - name, - bound_this, - bound_scope, - } => { - let Some(closure) = context.closure(name).cloned() else { - return eval_named_callable_with_call_user_func_values( - name, - evaluated_args, - context, - values, - ); - }; - let evaluated_args = positional_args(evaluated_args); - let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( - closure.function().name(), - closure.function().params(), - closure.function().parameter_is_by_ref(), - closure.function().parameter_is_variadic(), - evaluated_args.len(), - values, - )?; - eval_bound_closure_with_call_args_ref_flags( - &closure, - *bound_this, - bound_scope.clone(), - ¶meter_is_by_ref, - evaluated_args, - context, - values, - ) - } - EvaluatedCallable::InvokableObject { object } => { - eval_invokable_object_with_call_user_func_values( - *object, - evaluated_args, - context, - values, - ) - } - EvaluatedCallable::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - } => match native_class { - Some(native_class) => { - eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - *object, - native_class, - method, - positional_args(evaluated_args), - bridge_scope.as_deref(), - called_class.as_deref(), - context, - values, - ) - } - None => eval_object_method_with_call_user_func_values( - *object, - method, - EvalObjectCallbackKind::Method, - evaluated_args, - context, - values, - ), - }, - EvaluatedCallable::StaticMethod { - class_name, - method, - called_class, - native_class, - bridge_scope, - } => match native_class { - Some(native_class) => { - eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - native_class, - method, - positional_args(evaluated_args), - bridge_scope.as_deref(), - called_class.as_deref(), - context, - values, - ) - } - None => eval_static_method_with_call_user_func_values( - class_name, - method, - called_class.as_deref(), - evaluated_args, - context, - values, - ), - }, - } -} - -/// Invokes a normalized callback with by-value semantics while preserving named args. -pub(in crate::interpreter) fn eval_evaluated_callable_with_by_value_call_args( - callback: &EvaluatedCallable, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = eval_clear_evaluated_arg_ref_targets(evaluated_args); - match callback { - EvaluatedCallable::Named { name, .. } => { - eval_named_callable_with_call_user_func_args(name, evaluated_args, context, values) - } - EvaluatedCallable::BoundClosure { - name, - bound_this, - bound_scope, - } => { - let Some(closure) = context.closure(name).cloned() else { - return eval_named_callable_with_call_user_func_args( - name, - evaluated_args, - context, - values, - ); - }; - let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( - closure.function().name(), - closure.function().params(), - closure.function().parameter_is_by_ref(), - closure.function().parameter_is_variadic(), - evaluated_args.len(), - values, - )?; - eval_bound_closure_with_call_args_ref_flags( - &closure, - *bound_this, - bound_scope.clone(), - ¶meter_is_by_ref, - evaluated_args, - context, - values, - ) - } - EvaluatedCallable::InvokableObject { object } => { - eval_invokable_object_with_call_user_func_args( - *object, - evaluated_args, - context, - values, - ) - } - EvaluatedCallable::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - } => match native_class { - Some(native_class) => { - eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - *object, - native_class, - method, - evaluated_args, - bridge_scope.as_deref(), - called_class.as_deref(), - context, - values, - ) - } - None => eval_object_method_with_call_user_func_args( - *object, - method, - EvalObjectCallbackKind::Method, - evaluated_args, - context, - values, - ), - }, - EvaluatedCallable::StaticMethod { - class_name, - method, - called_class, - native_class, - bridge_scope, - } => match native_class { - Some(native_class) => { - eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - native_class, - method, - evaluated_args, - bridge_scope.as_deref(), - called_class.as_deref(), - context, - values, - ) - } - None => eval_static_method_with_call_user_func_args( - class_name, - method, - called_class.as_deref(), - evaluated_args, - context, - values, - ), - }, - } -} - -/// Removes caller writeback targets before a by-value callable API dispatch. -fn eval_clear_evaluated_arg_ref_targets( - evaluated_args: Vec, -) -> Vec { - evaluated_args - .into_iter() - .map(|arg| EvaluatedCallArg { - name: arg.name, - value: arg.value, - ref_target: None, - }) - .collect() -} - -/// Invokes a named callable through `call_user_func()` and warns for by-ref parameters. -fn eval_named_callable_with_call_user_func_values( - name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { - return Ok(result); - } - if let Some(closure) = context.closure(name).cloned() { - let evaluated_args = positional_args(evaluated_args); - let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( - closure.function().name(), - closure.function().params(), - closure.function().parameter_is_by_ref(), - closure.function().parameter_is_variadic(), - evaluated_args.len(), - values, - )?; - return eval_closure_with_evaluated_args_and_bound_scope_ref_flags( - &closure, - None, - ¶meter_is_by_ref, - evaluated_args, - context, - values, - ); - } - if let Some(function) = context.function(name).cloned() { - let evaluated_args = positional_args(evaluated_args); - let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( - function.name(), - function.params(), - function.parameter_is_by_ref(), - function.parameter_is_variadic(), - evaluated_args.len(), - values, - )?; - return eval_dynamic_function_with_evaluated_args_and_ref_flags( - &function, - ¶meter_is_by_ref, - evaluated_args, - context, - values, - ); - } - if let Some(function) = context.native_function(name) { - let evaluated_args = positional_args(evaluated_args); - let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( - name, - &function, - evaluated_args, - context, - values, - )?; - return eval_native_function_with_values(function, evaluated_args, context, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Invokes a named callable through by-value callable semantics with named args. -fn eval_named_callable_with_call_user_func_args( - name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if evaluated_args - .iter() - .all(|arg| arg.name.is_none() && arg.ref_target.is_none()) - { - let evaluated_values = evaluated_args.into_iter().map(|arg| arg.value).collect(); - return eval_named_callable_with_call_user_func_values( - name, - evaluated_values, - context, - values, - ); - } - if let Some(closure) = context.closure(name).cloned() { - let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( - closure.function().name(), - closure.function().params(), - closure.function().parameter_is_by_ref(), - closure.function().parameter_is_variadic(), - evaluated_args.len(), - values, - )?; - return eval_closure_with_evaluated_args_and_bound_scope_ref_flags( - &closure, - None, - ¶meter_is_by_ref, - evaluated_args, - context, - values, - ); - } - if let Some(function) = context.function(name).cloned() { - let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( - function.name(), - function.params(), - function.parameter_is_by_ref(), - function.parameter_is_variadic(), - evaluated_args.len(), - values, - )?; - return eval_dynamic_function_with_evaluated_args_and_ref_flags( - &function, - ¶meter_is_by_ref, - evaluated_args, - context, - values, - ); - } - if let Some(function) = context.native_function(name) { - let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( - name, - &function, - evaluated_args, - context, - values, - )?; - return eval_native_function_with_values(function, evaluated_args, context, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Invokes an invokable object through `call_user_func()` by-value argument semantics. -fn eval_invokable_object_with_call_user_func_values( - object: RuntimeCellHandle, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_object_method_with_call_user_func_values( - object, - "__invoke", - EvalObjectCallbackKind::InvokableObject, - evaluated_args, - context, - values, - ) -} - -/// Invokes an invokable object through by-value callable semantics with named args. -fn eval_invokable_object_with_call_user_func_args( - object: RuntimeCellHandle, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_object_method_with_call_user_func_args( - object, - "__invoke", - EvalObjectCallbackKind::InvokableObject, - evaluated_args, - context, - values, - ) -} - -/// Invokes an object-method callable through `call_user_func()` by-value semantics. -fn eval_object_method_with_call_user_func_values( - object: RuntimeCellHandle, - method: &str, - callback_kind: EvalObjectCallbackKind, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = positional_args(evaluated_args); - if let Some(result) = eval_object_method_call_user_func_result( - object, - method, - callback_kind, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) -} - -/// Invokes an object-method callable through by-value callable semantics with named args. -fn eval_object_method_with_call_user_func_args( - object: RuntimeCellHandle, - method: &str, - callback_kind: EvalObjectCallbackKind, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_object_method_call_user_func_result( - object, - method, - callback_kind, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) -} - -/// Attempts call-user-func by-value dispatch for eval-declared or generated object methods. -fn eval_object_method_call_user_func_result( - object: RuntimeCellHandle, - method_name: &str, - callback_kind: EvalObjectCallbackKind, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Ok(identity) = values.object_identity(object) else { - return eval_native_object_method_call_user_func_result( - object, - method_name, - evaluated_args, - context, - values, - ); - }; - let Some(class) = context.dynamic_object_class(identity) else { - return eval_native_object_method_call_user_func_result( - object, - method_name, - evaluated_args, - context, - values, - ); - }; - let called_class_name = class.name().to_string(); - if let Some((declaring_class, method)) = - eval_dynamic_method_for_call(&called_class_name, method_name, context) - { - if method.is_static() || method.is_abstract() { - return Ok(None); - } - if callback_kind == EvalObjectCallbackKind::Method - && validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() - { - return eval_magic_instance_method_call( - object, - &called_class_name, - method_name, - evaluated_args, - context, - values, - ); - } - let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); - return eval_dynamic_method_with_values_and_ref_mode( - &declaring_class, - &called_class_name, - &method, - object, - method.parameter_is_by_ref(), - evaluated_args, - EvalByRefBindingMode::WarnByValue { - callable_name: &callable_name, - }, - context, - values, - ) - .map(Some); - } - let Some(parent) = context.class_native_parent_name(&called_class_name) else { - return Ok(None); - }; - eval_native_object_method_call_user_func_result_for_class( - object, - &parent, - method_name, - Some(&called_class_name), - evaluated_args, - context, - values, - ) -} - -/// Attempts call-user-func by-value dispatch for a generated/AOT object method. -fn eval_native_object_method_call_user_func_result( - object: RuntimeCellHandle, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let class_name = runtime_object_class_name(object, values)?; - eval_native_object_method_call_user_func_result_for_class( - object, - &class_name, - method_name, - Some(&class_name), - evaluated_args, - context, - values, - ) -} - -/// Attempts generated/AOT object-method dispatch for one resolved receiver class. -fn eval_native_object_method_call_user_func_result_for_class( - object: RuntimeCellHandle, - class_name: &str, - method_name: &str, - called_class_scope: Option<&str>, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(shadow_scope) = - eval_private_scope_shadow_bridge_scope(class_name, method_name, context, values)? - { - // The calling scope's own private method shadows any override on the - // receiver's class (PHP private-method shadowing); dispatch with the - // scope string so the native shadow slot selects it directly. - return eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - object, - class_name, - method_name, - evaluated_args, - Some(&shadow_scope), - called_class_scope, - context, - values, - ) - .map(Some); - } - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? - else { - return Ok(None); - }; - if is_static || is_abstract { - return Ok(None); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() - && eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? - .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract) - { - return eval_native_magic_instance_method_call( - object, - class_name, - method_name, - evaluated_args, - context, - values, - ) - .map(Some); - } - eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - object, - class_name, - method_name, - evaluated_args, - Some(&declaring_class), - called_class_scope, - context, - values, - ) - .map(Some) -} - -/// Invokes a static-method callable through `call_user_func()` by-value semantics. -fn eval_static_method_with_call_user_func_values( - class_name: &str, - method_name: &str, - called_class: Option<&str>, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = positional_args(evaluated_args); - if let Some(result) = eval_static_method_call_user_func_result( - class_name, - method_name, - called_class, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - match called_class { - Some(called_class) => eval_static_method_call_result_with_called_class( - class_name, - called_class, - method_name, - evaluated_args, - context, - values, - ), - None if eval_callable_array_receiver_is_special_class_name(class_name) => { - eval_throw_class_not_found_error(class_name, context, values) - } - None => eval_static_method_call_result( - class_name, - method_name, - evaluated_args, - context, - values, - ), - } -} - -/// Invokes a static-method callable through by-value callable semantics with named args. -fn eval_static_method_with_call_user_func_args( - class_name: &str, - method_name: &str, - called_class: Option<&str>, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_static_method_call_user_func_result( - class_name, - method_name, - called_class, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - match called_class { - Some(called_class) => eval_static_method_call_result_with_called_class( - class_name, - called_class, - method_name, - evaluated_args, - context, - values, - ), - None if eval_callable_array_receiver_is_special_class_name(class_name) => { - eval_throw_class_not_found_error(class_name, context, values) - } - None => eval_static_method_call_result( - class_name, - method_name, - evaluated_args, - context, - values, - ), - } -} - -/// Attempts call-user-func by-value dispatch for eval-declared or generated static methods. -fn eval_static_method_call_user_func_result( - class_name: &str, - method_name: &str, - called_class: Option<&str>, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let dispatch_class = resolve_eval_static_member_class_name(class_name, context)?; - let called_class = called_class.unwrap_or(&dispatch_class).to_string(); - if let Some((declaring_class, method)) = - eval_dynamic_static_method_for_call(&dispatch_class, method_name, context) - { - if !method.is_static() || method.is_abstract() { - return Ok(None); - } - if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { - return eval_magic_static_method_call( - &dispatch_class, - &called_class, - method_name, - evaluated_args, - context, - values, - ); - } - let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); - return eval_dynamic_static_method_with_values_and_ref_mode( - &declaring_class, - &called_class, - &method, - method.parameter_is_by_ref(), - evaluated_args, - EvalByRefBindingMode::WarnByValue { - callable_name: &callable_name, - }, - context, - values, - ) - .map(Some); - } - let native_class = if context.has_class(&dispatch_class) { - let Some(parent) = context.class_native_parent_name(&dispatch_class) else { - return Ok(None); - }; - parent - } else if context.has_interface(&dispatch_class) - || context.has_trait(&dispatch_class) - || context.has_enum(&dispatch_class) - { - return Ok(None); - } else { - dispatch_class.clone() - }; - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy( - &native_class, - method_name, - context, - values, - )? - else { - if context - .native_static_method_signature(&native_class, method_name) - .is_some() - { - return eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - &native_class, - method_name, - evaluated_args, - None, - Some(&called_class), - context, - values, - ) - .map(Some); - } - return Ok(None); - }; - if !is_static || is_abstract { - return Ok(None); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() - && eval_aot_method_dispatch_metadata_in_hierarchy( - &native_class, - "__callStatic", - context, - values, - )? - .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract) - { - return eval_native_magic_static_method_call( - &native_class, - method_name, - evaluated_args, - context, - values, - ) - .map(Some); - } - eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - &native_class, - method_name, - evaluated_args, - Some(&declaring_class), - Some(&called_class), - context, - values, - ) - .map(Some) -} - -/// Builds by-value binding flags for `call_user_func()` and emits PHP by-ref warnings. -fn eval_call_user_func_by_value_ref_flags( - callable_name: &str, - params: &[String], - parameter_is_by_ref: &[bool], - parameter_is_variadic: &[bool], - supplied_count: usize, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let variadic_index = parameter_is_variadic - .iter() - .position(|is_variadic| *is_variadic); - for arg_index in 0..supplied_count { - let param_index = if variadic_index.is_some_and(|index| arg_index >= index) { - variadic_index.ok_or(EvalStatus::RuntimeFatal)? - } else { - arg_index - }; - if !parameter_is_by_ref - .get(param_index) - .copied() - .unwrap_or(false) - { - continue; - } - let param_name = params - .get(param_index) - .map(String::as_str) - .unwrap_or("arg"); - values.warning(&format!( - "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", - arg_index + 1 - ))?; - } - Ok(vec![false; params.len()]) -} - -/// Invokes an already normalized callback with optional named-argument metadata. -pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( - callback: &EvaluatedCallable, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match callback { - EvaluatedCallable::Named { name, .. } => { - eval_callable_with_call_array_args(name, evaluated_args, context, values) - } - EvaluatedCallable::BoundClosure { - name, - bound_this, - bound_scope, - } => { - let Some(closure) = context.closure(name).cloned() else { - return eval_callable_with_call_array_args(name, evaluated_args, context, values); - }; - eval_bound_closure_with_call_args_ref_mode( - &closure, - *bound_this, - bound_scope.clone(), - closure.function().parameter_is_by_ref(), - evaluated_args, - EvalByRefBindingMode::WarnByValue { - callable_name: closure.function().name(), - }, - context, - values, - ) - } - EvaluatedCallable::InvokableObject { object } => { - eval_invokable_object_with_call_user_func_args(*object, evaluated_args, context, values) - } - EvaluatedCallable::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - } => match native_class { - Some(native_class) => eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - *object, - native_class, - method, - evaluated_args, - bridge_scope.as_deref(), - called_class.as_deref(), - context, - values, - ), - None => eval_object_method_with_call_user_func_args( - *object, - method, - EvalObjectCallbackKind::Method, - evaluated_args, - context, - values, - ), - }, - EvaluatedCallable::StaticMethod { - class_name, - method, - called_class, - native_class, - bridge_scope, - } => match native_class { - Some(native_class) => { - eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - native_class, - method, - evaluated_args, - bridge_scope.as_deref(), - called_class.as_deref(), - context, - values, - ) - } - None => match called_class { - Some(called_class) => eval_static_method_with_call_user_func_args( - class_name, - method, - Some(called_class), - evaluated_args, - context, - values, - ), - None if eval_callable_array_receiver_is_special_class_name(class_name) => { - eval_throw_class_not_found_error(class_name, context, values) - } - None => eval_static_method_with_call_user_func_args( - class_name, - method, - None, - evaluated_args, - context, - values, - ), - }, - }, - } -} - -/// Invokes a PHP-visible callable name with source-order positional values. -pub(in crate::interpreter) fn eval_callable_with_values( - name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { - return Ok(result); - } - if let Some(closure) = context.closure(name).cloned() { - return eval_closure_with_evaluated_args( - &closure, - positional_args(evaluated_args), - context, - values, - ); - } - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function_with_values(&function, evaluated_args, context, values); - } - if let Some(function) = context.native_function(name) { - let evaluated_args = positional_args(evaluated_args); - let evaluated_args = - bind_evaluated_native_function_args(&function, evaluated_args, context, values)?; - return eval_native_function_with_values(function, evaluated_args, context, values); - } - Err(EvalStatus::UnsupportedConstruct) -} - -/// Invokes a callable with arguments that may carry `call_user_func_array` names. -pub(in crate::interpreter) fn eval_callable_with_call_array_args( - name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = - eval_date_procedural_alias_with_evaluated_args(name, evaluated_args.clone(), context, values)? - { - return Ok(result); - } - if let Some(result) = - eval_mutating_builtin_with_call_array_args(name, &evaluated_args, context, values)? - { - return Ok(result); - } - if eval_php_visible_builtin_exists(name) { - let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; - let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { - return Err(EvalStatus::UnsupportedConstruct); - }; - return Ok(result); - } - if let Some(closure) = context.closure(name).cloned() { - return eval_closure_with_evaluated_args_and_bound_scope_ref_mode( - &closure, - None, - closure.function().parameter_is_by_ref(), - evaluated_args, - EvalByRefBindingMode::WarnByValue { - callable_name: closure.function().name(), - }, - context, - values, - ); - } - if let Some(function) = context.function(name).cloned() { - return eval_dynamic_function_with_evaluated_args_and_ref_mode( - &function, - function.parameter_is_by_ref(), - evaluated_args, - EvalByRefBindingMode::WarnByValue { - callable_name: function.name(), - }, - context, - values, - ); - } - if let Some(function) = context.native_function(name) { - let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( - name, - &function, - evaluated_args, - context, - values, - )?; - return eval_native_function_with_values(function, evaluated_args, context, values); - } - Err(EvalStatus::UnsupportedConstruct) -} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable/array_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable/array_dispatch.rs new file mode 100644 index 0000000000..9b6fcdd4ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable/array_dispatch.rs @@ -0,0 +1,206 @@ +//! Purpose: +//! Executes normalized callbacks from evaluated call-array arguments or values. +//! +//! Called from: +//! - `call_user_func_array` and internal callable-with-values helpers. +//! +//! Key details: +//! - Named metadata and call-array ordering are preserved before target dispatch. + +use super::*; + +/// Invokes an already normalized callback with optional named-argument metadata. +pub(in crate::interpreter) fn eval_evaluated_callable_with_call_array_args( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => { + eval_callable_with_call_array_args(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let Some(closure) = context.closure(name).cloned() else { + return eval_callable_with_call_array_args(name, evaluated_args, context, values); + }; + eval_bound_closure_with_call_args_ref_mode( + &closure, + *bound_this, + bound_scope.clone(), + closure.function().parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_with_call_user_func_args(*object, evaluated_args, context, values) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + *object, + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ), + None => eval_object_method_with_call_user_func_args( + *object, + method, + EvalObjectCallbackKind::Method, + evaluated_args, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => match called_class { + Some(called_class) => eval_static_method_with_call_user_func_args( + class_name, + method, + Some(called_class), + evaluated_args, + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_with_call_user_func_args( + class_name, + method, + None, + evaluated_args, + context, + values, + ), + }, + }, + } +} + +/// Invokes a PHP-visible callable name with source-order positional values. +pub(in crate::interpreter) fn eval_callable_with_values( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { + return Ok(result); + } + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args( + &closure, + positional_args(evaluated_args), + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function_with_values(&function, evaluated_args, context, values); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = positional_args(evaluated_args); + let evaluated_args = + bind_evaluated_native_function_args(&function, evaluated_args, context, values)?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Invokes a callable with arguments that may carry `call_user_func_array` names. +pub(in crate::interpreter) fn eval_callable_with_call_array_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = + eval_date_procedural_alias_with_evaluated_args(name, evaluated_args.clone(), context, values)? + { + return Ok(result); + } + if let Some(result) = + eval_mutating_builtin_with_call_array_args(name, &evaluated_args, context, values)? + { + return Ok(result); + } + if eval_php_visible_builtin_exists(name) { + let evaluated_args = bind_evaluated_builtin_args(name, evaluated_args, values)?; + let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? else { + return Err(EvalStatus::UnsupportedConstruct); + }; + return Ok(result); + } + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + &closure, + None, + closure.function().parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + return eval_dynamic_function_with_evaluated_args_and_ref_mode( + &function, + function.parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: function.name(), + }, + context, + values, + ); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable/execution.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable/execution.rs new file mode 100644 index 0000000000..aedacc4159 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable/execution.rs @@ -0,0 +1,440 @@ +//! Purpose: +//! Executes normalized callables from already evaluated positional values. +//! +//! Called from: +//! - `call_user_func`, direct callable invocation, and callback builtins. +//! +//! Key details: +//! - Closure binding and by-value reference warnings preserve PHP call semantics. + +use super::*; + +/// Invokes an already normalized callback with source-order positional values. +pub(in crate::interpreter) fn eval_evaluated_callable_with_values( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => { + if let Some(closure) = context.closure(name).cloned() { + return eval_closure_with_evaluated_args( + &closure, + positional_args(evaluated_args), + context, + values, + ); + } + eval_callable_with_values(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let Some(closure) = context.closure(name).cloned() else { + return eval_callable_with_values(name, evaluated_args, context, values); + }; + eval_bound_closure_with_call_args( + &closure, + *bound_this, + bound_scope.clone(), + positional_args(evaluated_args), + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_call_result( + *object, + positional_args(evaluated_args), + context, + values, + ) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => eval_native_method_with_evaluated_args_unchecked_bridge_scope( + *object, + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ), + None => eval_method_call_result(*object, method, evaluated_args, context, values), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method, + positional_args(evaluated_args), + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_call_result( + class_name, + method, + positional_args(evaluated_args), + context, + values, + ), + }, + }, + } +} + +/// Invokes a bound eval closure with either `$this` or only an explicit class scope. +pub(super) fn eval_bound_closure_with_call_args( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope( + closure, + this_object, + bound_scope, + evaluated_args, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope( + closure, + bound_scope, + evaluated_args, + context, + values, + ), + } +} + +/// Invokes a bound eval closure with caller-selected by-reference binding flags. +pub(super) fn eval_bound_closure_with_call_args_ref_flags( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope_ref_flags( + closure, + this_object, + bound_scope, + parameter_is_by_ref, + evaluated_args, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + closure, + bound_scope, + parameter_is_by_ref, + evaluated_args, + context, + values, + ), + } +} + +/// Invokes a bound eval closure with caller-selected by-reference binding mode. +pub(super) fn eval_bound_closure_with_call_args_ref_mode( + closure: &EvalClosure, + bound_this: Option, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match bound_this { + Some(this_object) => eval_closure_with_evaluated_args_and_bound_this_scope_ref_mode( + closure, + this_object, + bound_scope, + parameter_is_by_ref, + evaluated_args, + by_ref_mode, + context, + values, + ), + None => eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + closure, + bound_scope, + parameter_is_by_ref, + evaluated_args, + by_ref_mode, + context, + values, + ), + } +} + +/// Invokes a normalized callback through `call_user_func()` by-value argument semantics. +pub(super) fn eval_evaluated_callable_with_call_user_func_values( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match callback { + EvaluatedCallable::Named { name, .. } => { + eval_named_callable_with_call_user_func_values(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let Some(closure) = context.closure(name).cloned() else { + return eval_named_callable_with_call_user_func_values( + name, + evaluated_args, + context, + values, + ); + }; + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + eval_bound_closure_with_call_args_ref_flags( + &closure, + *bound_this, + bound_scope.clone(), + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_with_call_user_func_values( + *object, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + *object, + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_object_method_with_call_user_func_values( + *object, + method, + EvalObjectCallbackKind::Method, + evaluated_args, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + native_class, + method, + positional_args(evaluated_args), + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_static_method_with_call_user_func_values( + class_name, + method, + called_class.as_deref(), + evaluated_args, + context, + values, + ), + }, + } +} + +/// Invokes a normalized callback with by-value semantics while preserving named args. +pub(in crate::interpreter) fn eval_evaluated_callable_with_by_value_call_args( + callback: &EvaluatedCallable, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = eval_clear_evaluated_arg_ref_targets(evaluated_args); + match callback { + EvaluatedCallable::Named { name, .. } => { + eval_named_callable_with_call_user_func_args(name, evaluated_args, context, values) + } + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => { + let Some(closure) = context.closure(name).cloned() else { + return eval_named_callable_with_call_user_func_args( + name, + evaluated_args, + context, + values, + ); + }; + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + eval_bound_closure_with_call_args_ref_flags( + &closure, + *bound_this, + bound_scope.clone(), + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::InvokableObject { object } => { + eval_invokable_object_with_call_user_func_args( + *object, + evaluated_args, + context, + values, + ) + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + *object, + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_object_method_with_call_user_func_args( + *object, + method, + EvalObjectCallbackKind::Method, + evaluated_args, + context, + values, + ), + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => match native_class { + Some(native_class) => { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + native_class, + method, + evaluated_args, + bridge_scope.as_deref(), + called_class.as_deref(), + context, + values, + ) + } + None => eval_static_method_with_call_user_func_args( + class_name, + method, + called_class.as_deref(), + evaluated_args, + context, + values, + ), + }, + } +} + +/// Removes caller writeback targets before a by-value callable API dispatch. +pub(super) fn eval_clear_evaluated_arg_ref_targets( + evaluated_args: Vec, +) -> Vec { + evaluated_args + .into_iter() + .map(|arg| EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + }) + .collect() +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable/object_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable/object_dispatch.rs new file mode 100644 index 0000000000..c1ecd92752 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable/object_dispatch.rs @@ -0,0 +1,376 @@ +//! Purpose: +//! Dispatches named, invokable-object, and object-method callbacks. +//! +//! Called from: +//! - Callable execution after callback normalization. +//! +//! Key details: +//! - Native and eval object methods share call_user_func by-value handling. + +use super::*; + +/// Invokes a named callable through `call_user_func()` and warns for by-ref parameters. +pub(super) fn eval_named_callable_with_call_user_func_values( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_builtin_with_values(name, &evaluated_args, context, values)? { + return Ok(result); + } + if let Some(closure) = context.closure(name).cloned() { + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + &closure, + None, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + let evaluated_args = positional_args(evaluated_args); + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + function.name(), + function.params(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_dynamic_function_with_evaluated_args_and_ref_flags( + &function, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = positional_args(evaluated_args); + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Invokes a named callable through by-value callable semantics with named args. +pub(super) fn eval_named_callable_with_call_user_func_args( + name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if evaluated_args + .iter() + .all(|arg| arg.name.is_none() && arg.ref_target.is_none()) + { + let evaluated_values = evaluated_args.into_iter().map(|arg| arg.value).collect(); + return eval_named_callable_with_call_user_func_values( + name, + evaluated_values, + context, + values, + ); + } + if let Some(closure) = context.closure(name).cloned() { + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + closure.function().name(), + closure.function().params(), + closure.function().parameter_is_by_ref(), + closure.function().parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + &closure, + None, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.function(name).cloned() { + let parameter_is_by_ref = eval_call_user_func_by_value_ref_flags( + function.name(), + function.params(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + evaluated_args.len(), + values, + )?; + return eval_dynamic_function_with_evaluated_args_and_ref_flags( + &function, + ¶meter_is_by_ref, + evaluated_args, + context, + values, + ); + } + if let Some(function) = context.native_function(name) { + let evaluated_args = bind_evaluated_native_function_args_for_call_user_func( + name, + &function, + evaluated_args, + context, + values, + )?; + return eval_native_function_with_values(function, evaluated_args, context, values); + } + Err(EvalStatus::UnsupportedConstruct) +} + +/// Invokes an invokable object through `call_user_func()` by-value argument semantics. +pub(super) fn eval_invokable_object_with_call_user_func_values( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_object_method_with_call_user_func_values( + object, + "__invoke", + EvalObjectCallbackKind::InvokableObject, + evaluated_args, + context, + values, + ) +} + +/// Invokes an invokable object through by-value callable semantics with named args. +pub(super) fn eval_invokable_object_with_call_user_func_args( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_object_method_with_call_user_func_args( + object, + "__invoke", + EvalObjectCallbackKind::InvokableObject, + evaluated_args, + context, + values, + ) +} + +/// Invokes an object-method callable through `call_user_func()` by-value semantics. +pub(super) fn eval_object_method_with_call_user_func_values( + object: RuntimeCellHandle, + method: &str, + callback_kind: EvalObjectCallbackKind, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = positional_args(evaluated_args); + if let Some(result) = eval_object_method_call_user_func_result( + object, + method, + callback_kind, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) +} + +/// Invokes an object-method callable through by-value callable semantics with named args. +pub(super) fn eval_object_method_with_call_user_func_args( + object: RuntimeCellHandle, + method: &str, + callback_kind: EvalObjectCallbackKind, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_object_method_call_user_func_result( + object, + method, + callback_kind, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + eval_method_call_result_with_evaluated_args(object, method, evaluated_args, context, values) +} + +/// Attempts call-user-func by-value dispatch for eval-declared or generated object methods. +pub(super) fn eval_object_method_call_user_func_result( + object: RuntimeCellHandle, + method_name: &str, + callback_kind: EvalObjectCallbackKind, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return eval_native_object_method_call_user_func_result( + object, + method_name, + evaluated_args, + context, + values, + ); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return eval_native_object_method_call_user_func_result( + object, + method_name, + evaluated_args, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_method_for_call(&called_class_name, method_name, context) + { + if method.is_static() || method.is_abstract() { + return Ok(None); + } + if callback_kind == EvalObjectCallbackKind::Method + && validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() + { + return eval_magic_instance_method_call( + object, + &called_class_name, + method_name, + evaluated_args, + context, + values, + ); + } + let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); + return eval_dynamic_method_with_values_and_ref_mode( + &declaring_class, + &called_class_name, + &method, + object, + method.parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) + .map(Some); + } + let Some(parent) = context.class_native_parent_name(&called_class_name) else { + return Ok(None); + }; + eval_native_object_method_call_user_func_result_for_class( + object, + &parent, + method_name, + Some(&called_class_name), + evaluated_args, + context, + values, + ) +} + +/// Attempts call-user-func by-value dispatch for a generated/AOT object method. +pub(super) fn eval_native_object_method_call_user_func_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = runtime_object_class_name(object, values)?; + eval_native_object_method_call_user_func_result_for_class( + object, + &class_name, + method_name, + Some(&class_name), + evaluated_args, + context, + values, + ) +} + +/// Attempts generated/AOT object-method dispatch for one resolved receiver class. +pub(super) fn eval_native_object_method_call_user_func_result_for_class( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + called_class_scope: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(shadow_scope) = + eval_private_scope_shadow_bridge_scope(class_name, method_name, context, values)? + { + // The calling scope's own private method shadows any override on the + // receiver's class (PHP private-method shadowing); dispatch with the + // scope string so the native shadow slot selects it directly. + return eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&shadow_scope), + called_class_scope, + context, + values, + ) + .map(Some); + } + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() + && eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract) + { + return eval_native_magic_instance_method_call( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + called_class_scope, + context, + values, + ) + .map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/builtins/registry/callable/static_dispatch.rs b/crates/elephc-magician/src/interpreter/builtins/registry/callable/static_dispatch.rs new file mode 100644 index 0000000000..1969a3c7eb --- /dev/null +++ b/crates/elephc-magician/src/interpreter/builtins/registry/callable/static_dispatch.rs @@ -0,0 +1,243 @@ +//! Purpose: +//! Dispatches normalized static-method callbacks through call_user_func semantics. +//! +//! Called from: +//! - Callable execution for class-string and special-scope static targets. +//! +//! Key details: +//! - Called-class propagation and by-reference warning flags stay paired. + +use super::*; + +/// Invokes a static-method callable through `call_user_func()` by-value semantics. +pub(super) fn eval_static_method_with_call_user_func_values( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = positional_args(evaluated_args); + if let Some(result) = eval_static_method_call_user_func_result( + class_name, + method_name, + called_class, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method_name, + evaluated_args, + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_call_result( + class_name, + method_name, + evaluated_args, + context, + values, + ), + } +} + +/// Invokes a static-method callable through by-value callable semantics with named args. +pub(super) fn eval_static_method_with_call_user_func_args( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_static_method_call_user_func_result( + class_name, + method_name, + called_class, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + match called_class { + Some(called_class) => eval_static_method_call_result_with_called_class( + class_name, + called_class, + method_name, + evaluated_args, + context, + values, + ), + None if eval_callable_array_receiver_is_special_class_name(class_name) => { + eval_throw_class_not_found_error(class_name, context, values) + } + None => eval_static_method_call_result( + class_name, + method_name, + evaluated_args, + context, + values, + ), + } +} + +/// Attempts call-user-func by-value dispatch for eval-declared or generated static methods. +pub(super) fn eval_static_method_call_user_func_result( + class_name: &str, + method_name: &str, + called_class: Option<&str>, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let dispatch_class = resolve_eval_static_member_class_name(class_name, context)?; + let called_class = called_class.unwrap_or(&dispatch_class).to_string(); + if let Some((declaring_class, method)) = + eval_dynamic_static_method_for_call(&dispatch_class, method_name, context) + { + if !method.is_static() || method.is_abstract() { + return Ok(None); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { + return eval_magic_static_method_call( + &dispatch_class, + &called_class, + method_name, + evaluated_args, + context, + values, + ); + } + let callable_name = format!("{}::{}", declaring_class.trim_start_matches('\\'), method.name()); + return eval_dynamic_static_method_with_values_and_ref_mode( + &declaring_class, + &called_class, + &method, + method.parameter_is_by_ref(), + evaluated_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) + .map(Some); + } + let native_class = if context.has_class(&dispatch_class) { + let Some(parent) = context.class_native_parent_name(&dispatch_class) else { + return Ok(None); + }; + parent + } else if context.has_interface(&dispatch_class) + || context.has_trait(&dispatch_class) + || context.has_enum(&dispatch_class) + { + return Ok(None); + } else { + dispatch_class.clone() + }; + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + method_name, + context, + values, + )? + else { + if context + .native_static_method_signature(&native_class, method_name) + .is_some() + { + return eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + &native_class, + method_name, + evaluated_args, + None, + Some(&called_class), + context, + values, + ) + .map(Some); + } + return Ok(None); + }; + if !is_static || is_abstract { + return Ok(None); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() + && eval_aot_method_dispatch_metadata_in_hierarchy( + &native_class, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract) + { + return eval_native_magic_static_method_call( + &native_class, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + &native_class, + method_name, + evaluated_args, + Some(&declaring_class), + Some(&called_class), + context, + values, + ) + .map(Some) +} + +/// Builds by-value binding flags for `call_user_func()` and emits PHP by-ref warnings. +pub(super) fn eval_call_user_func_by_value_ref_flags( + callable_name: &str, + params: &[String], + parameter_is_by_ref: &[bool], + parameter_is_variadic: &[bool], + supplied_count: usize, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let variadic_index = parameter_is_variadic + .iter() + .position(|is_variadic| *is_variadic); + for arg_index in 0..supplied_count { + let param_index = if variadic_index.is_some_and(|index| arg_index >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + arg_index + }; + if !parameter_is_by_ref + .get(param_index) + .copied() + .unwrap_or(false) + { + continue; + } + let param_name = params + .get(param_index) + .map(String::as_str) + .unwrap_or("arg"); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + arg_index + 1 + ))?; + } + Ok(vec![false; params.len()]) +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions.rs b/crates/elephc-magician/src/interpreter/dynamic_functions.rs index 8cc9141b2d..a4f5ba50b9 100644 --- a/crates/elephc-magician/src/interpreter/dynamic_functions.rs +++ b/crates/elephc-magician/src/interpreter/dynamic_functions.rs @@ -1,5 +1,6 @@ //! Purpose: -//! Evaluates user-declared and native dynamic functions, including named/spread argument binding. +//! Coordinates call-argument evaluation for user-declared and native functions. +//! Binding and execution paths live in focused child modules. //! //! Called from: //! - `crate::interpreter::eval_call()` and dynamic callable dispatch helpers. @@ -8,9 +9,19 @@ //! - PHP source evaluation order is preserved before argument binding. //! - Static locals are persisted through `ElephcEvalContext` after function execution. +mod closure_execution; +mod function_binding; +mod method_binding; +mod native_execution; + use super::*; use std::ffi::c_void; +pub(in crate::interpreter) use closure_execution::*; +pub(in crate::interpreter) use function_binding::*; +pub(in crate::interpreter) use method_binding::*; +pub(in crate::interpreter) use native_execution::*; + /// Evaluates an eval-declared user function with PHP-style argument binding. pub(in crate::interpreter) fn eval_dynamic_function( function: &EvalFunction, @@ -391,2202 +402,3 @@ fn eval_invoker_ref_slot_value( _ => Err(EvalStatus::RuntimeFatal), } } - -/// Binds evaluated positional and named values to declared parameter order. -pub(in crate::interpreter) fn bind_evaluated_function_args( - params: &[String], - evaluated_args: Vec, -) -> Result, EvalStatus> { - let mut bound_args = vec![None; params.len()]; - let mut next_positional = 0; - - for arg in evaluated_args { - if let Some(name) = arg.name { - bind_dynamic_named_arg(params, &mut bound_args, &name, arg.value)?; - } else { - bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; - } - } - - bound_args - .into_iter() - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Binds already evaluated native AOT function args and fills omitted defaults. -pub(in crate::interpreter) fn bind_evaluated_native_function_args( - function: &NativeFunction, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - bind_evaluated_native_function_args_with_mode( - function, - evaluated_args, - EvalByRefBindingMode::RequireTarget, - context, - values, - ) -} - -/// Binds native AOT function args for `call_user_func()` by-value by-ref degradation. -pub(in crate::interpreter) fn bind_evaluated_native_function_args_for_call_user_func( - callable_name: &str, - function: &NativeFunction, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - bind_evaluated_native_function_args_with_mode( - function, - evaluated_args, - EvalByRefBindingMode::WarnByValue { callable_name }, - context, - values, - ) -} - -/// Binds already evaluated native AOT function args using the selected by-reference mode. -fn bind_evaluated_native_function_args_with_mode( - function: &NativeFunction, - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if native_function_variadic_index(function).is_some() { - return bind_evaluated_native_variadic_function_args( - function, - evaluated_args, - by_ref_mode, - context, - values, - ); - } - let mut bound_args = vec![None; function.param_count()]; - let has_param_names = function.param_names().len() == function.param_count(); - let mut next_positional = 0; - - for arg in evaluated_args { - if let Some(name) = arg.name { - if !has_param_names { - return Err(EvalStatus::RuntimeFatal); - } - bind_native_function_named_arg( - function, - None, - &mut bound_args, - &name, - arg.value, - arg.ref_target, - by_ref_mode, - values, - )?; - } else { - bind_native_function_positional_arg( - function, - &mut bound_args, - None, - &mut next_positional, - arg.value, - arg.ref_target, - by_ref_mode, - values, - )?; - } - } - - for (position, bound) in bound_args.iter_mut().enumerate() { - if bound.is_some() { - continue; - } - if position < function.required_param_count() { - return Err(EvalStatus::RuntimeFatal); - } - let Some(default) = function.param_default(position) else { - return Err(EvalStatus::RuntimeFatal); - }; - *bound = Some(BoundMethodArg { - value: materialize_native_callable_default(default, context, values)?, - ref_target: None, - variadic_ref_targets: Vec::new(), - }); - } - - let mut bound_args = bound_args - .into_iter() - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal)?; - apply_native_function_arg_types(function, None, &mut bound_args, context, values)?; - stage_native_function_invoker_args(function, None, bound_args, by_ref_mode, values) -} - -/// Binds a native AOT variadic function while keeping the raw invoker argument layout. -fn bind_evaluated_native_variadic_function_args( - function: &NativeFunction, - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let variadic_index = native_function_variadic_index(function).ok_or(EvalStatus::RuntimeFatal)?; - let has_param_names = function.param_names().len() == function.param_count(); - let mut regular_args = vec![None; variadic_index]; - let mut variadic_args = Vec::new(); - let mut next_positional = 0; - - for arg in evaluated_args { - if let Some(name) = arg.name { - if !has_param_names { - return Err(EvalStatus::RuntimeFatal); - } - if native_function_regular_param_index(function, variadic_index, &name).is_none() { - return Err(EvalStatus::RuntimeFatal); - } - bind_native_function_named_arg( - function, - Some(variadic_index), - &mut regular_args, - &name, - arg.value, - arg.ref_target, - by_ref_mode, - values, - )?; - } else if next_positional < variadic_index { - bind_native_function_positional_arg( - function, - &mut regular_args, - Some(variadic_index), - &mut next_positional, - arg.value, - arg.ref_target, - by_ref_mode, - values, - )?; - } else { - let ref_target = native_function_parameter_ref_target( - function, - Some(variadic_index), - arg.ref_target, - by_ref_mode, - values, - )?; - variadic_args.push(BoundMethodArg { - value: arg.value, - ref_target, - variadic_ref_targets: Vec::new(), - }); - } - } - - for (position, bound) in regular_args.iter_mut().enumerate() { - if bound.is_some() { - continue; - } - if position < function.required_param_count() { - return Err(EvalStatus::RuntimeFatal); - } - let Some(default) = function.param_default(position) else { - return Err(EvalStatus::RuntimeFatal); - }; - *bound = Some(BoundMethodArg { - value: materialize_native_callable_default(default, context, values)?, - ref_target: None, - variadic_ref_targets: Vec::new(), - }); - } - - let mut bound_args = regular_args - .into_iter() - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal)?; - bound_args.extend(variadic_args); - apply_native_function_arg_types( - function, - Some(variadic_index), - &mut bound_args, - context, - values, - )?; - stage_native_function_invoker_args( - function, - Some(variadic_index), - bound_args, - by_ref_mode, - values, - ) -} - -/// Applies registered native AOT function parameter types after argument binding. -fn apply_native_function_arg_types( - function: &NativeFunction, - variadic_index: Option, - bound_args: &mut [BoundMethodArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for (position, bound_arg) in bound_args.iter_mut().enumerate() { - let param_index = if variadic_index.is_some_and(|index| position >= index) { - variadic_index.ok_or(EvalStatus::RuntimeFatal)? - } else { - position - }; - let Some(param_type) = function.param_type(param_index) else { - continue; - }; - bound_arg.value = eval_method_parameter_value(param_type, bound_arg.value, context, values)?; - } - Ok(()) -} - -/// Binds one named native AOT function argument to a non-variadic parameter slot. -fn bind_native_function_named_arg( - function: &NativeFunction, - variadic_index: Option, - bound_args: &mut [Option], - name: &str, - value: RuntimeCellHandle, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(param_index) = native_function_named_param_index(function, variadic_index, name) else { - return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let ref_target = - native_function_parameter_ref_target(function, Some(param_index), ref_target, by_ref_mode, values)?; - bound_args[param_index] = Some(BoundMethodArg { - value, - ref_target, - variadic_ref_targets: Vec::new(), - }); - Ok(()) -} - -/// Binds one positional native AOT function argument to the next fixed parameter. -fn bind_native_function_positional_arg( - function: &NativeFunction, - bound_args: &mut [Option], - variadic_index: Option, - next_positional: &mut usize, - value: RuntimeCellHandle, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let param_index = *next_positional; - if variadic_index.is_some_and(|index| param_index >= index) - || param_index >= bound_args.len() - || bound_args[param_index].is_some() - { - return Err(EvalStatus::RuntimeFatal); - } - let ref_target = - native_function_parameter_ref_target(function, Some(param_index), ref_target, by_ref_mode, values)?; - bound_args[param_index] = Some(BoundMethodArg { - value, - ref_target, - variadic_ref_targets: Vec::new(), - }); - *next_positional += 1; - Ok(()) -} - -/// Returns the caller writeback target required by a native function by-reference parameter. -fn native_function_parameter_ref_target( - function: &NativeFunction, - param_index: Option, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(param_index) = param_index else { - return Ok(None); - }; - if !function.param_by_ref(param_index) { - return Ok(None); - } - if let Some(ref_target) = ref_target { - return Ok(Some(ref_target)); - } - match by_ref_mode { - EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), - EvalByRefBindingMode::WarnByValue { callable_name } => { - let param_name = native_function_param_warning_name(function, param_index); - values.warning(&format!( - "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", - param_index + 1 - ))?; - Ok(None) - } - } -} - -/// Converts bound values into descriptor-invoker arguments, staging by-reference slots. -fn stage_native_function_invoker_args( - function: &NativeFunction, - variadic_index: Option, - bound_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut invoker_values = Vec::with_capacity(bound_args.len()); - let mut ref_slots = Vec::new(); - for (position, bound_arg) in bound_args.into_iter().enumerate() { - let param_index = if variadic_index.is_some_and(|index| position >= index) { - variadic_index.ok_or(EvalStatus::RuntimeFatal)? - } else { - position - }; - if !function.param_by_ref(param_index) { - invoker_values.push(bound_arg.value); - continue; - } - let target = match (bound_arg.ref_target, by_ref_mode) { - (Some(target), _) => Some(target), - (None, EvalByRefBindingMode::WarnByValue { .. }) => None, - (None, EvalByRefBindingMode::RequireTarget) => return Err(EvalStatus::RuntimeFatal), - }; - if let Some(raw_ref_kind) = native_function_raw_ref_kind(function.param_type(param_index)) { - match raw_ref_kind { - NativeFunctionRawRefKind::Scalar { tag } => { - let original = values.raw_value_word(bound_arg.value)?; - let mut slot = Box::new(original); - let marker = - values.invoker_raw_ref_cell(slot.as_mut() as *mut u64 as *mut c_void, tag)?; - invoker_values.push(marker); - ref_slots.push(BoundNativeFunctionRefSlot::RawWord { - tag, - original, - slot, - target, - }); - } - NativeFunctionRawRefKind::String => { - let original_ptr = values.raw_value_word(bound_arg.value)?; - let original_len = values.raw_value_high_word(bound_arg.value)?; - let retained = values.retain_raw_string_words(original_ptr, original_len)?; - let mut slot = Box::new([retained.0, retained.1]); - let marker = values.invoker_raw_ref_cell( - slot.as_mut() as *mut [u64; 2] as *mut c_void, - EVAL_TAG_STRING, - )?; - invoker_values.push(marker); - ref_slots.push(BoundNativeFunctionRefSlot::RawString { - original: [retained.0, retained.1], - slot, - target, - }); - } - NativeFunctionRawRefKind::OwnedHeap => { - let source_tag = values.type_tag(bound_arg.value)?; - let original = values.raw_value_word(bound_arg.value)?; - let retained = values.retain_raw_heap_word(original)?; - let mut slot = Box::new(retained); - let marker = values.invoker_raw_ref_cell( - slot.as_mut() as *mut u64 as *mut c_void, - source_tag, - )?; - invoker_values.push(marker); - ref_slots.push(BoundNativeFunctionRefSlot::OwnedRawWord { - original, - slot, - target, - }); - } - } - continue; - } - let original = bound_arg.value; - let retained = values.retain(original)?; - let mut slot = Box::new(retained); - let marker = match values.invoker_ref_cell(slot.as_mut() as *mut RuntimeCellHandle) { - Ok(marker) => marker, - Err(status) => { - values.release(retained)?; - return Err(status); - } - }; - invoker_values.push(marker); - ref_slots.push(BoundNativeFunctionRefSlot::Mixed { - original, - slot, - target, - }); - } - Ok(BoundNativeFunctionArgs { - values: invoker_values, - ref_slots, - }) -} - -/// Returns the PHP parameter name used in by-reference warning diagnostics. -fn native_function_param_warning_name(function: &NativeFunction, param_index: usize) -> String { - function - .param_names() - .get(param_index) - .filter(|name| !name.is_empty()) - .cloned() - .unwrap_or_else(|| format!("arg{}", param_index + 1)) -} - -/// Describes native function by-reference parameters that can use typed raw slots. -enum NativeFunctionRawRefKind { - Scalar { tag: u64 }, - String, - OwnedHeap, -} - -/// Returns the raw-slot strategy for one supported by-reference parameter. -fn native_function_raw_ref_kind(param_type: Option<&EvalParameterType>) -> Option { - let param_type = param_type?; - if param_type.allows_null() - || param_type.is_intersection() - || param_type.variants().len() != 1 - { - return None; - } - match param_type.variants().first()? { - EvalParameterTypeVariant::Array - | EvalParameterTypeVariant::Class(_) - | EvalParameterTypeVariant::Iterable - | EvalParameterTypeVariant::Object => Some(NativeFunctionRawRefKind::OwnedHeap), - EvalParameterTypeVariant::Bool => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_BOOL }), - EvalParameterTypeVariant::Float => { - Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_FLOAT }) - } - EvalParameterTypeVariant::Int => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_INT }), - EvalParameterTypeVariant::String => Some(NativeFunctionRawRefKind::String), - _ => None, - } -} - -/// Returns the variadic parameter index for a native AOT function, if registered. -fn native_function_variadic_index(function: &NativeFunction) -> Option { - (0..function.param_count()).find(|index| function.param_variadic(*index)) -} - -/// Returns the native function parameter index for one named argument. -fn native_function_named_param_index( - function: &NativeFunction, - variadic_index: Option, - name: &str, -) -> Option { - function - .param_names() - .iter() - .enumerate() - .position(|(index, param)| Some(index) != variadic_index && param == name) -} - -/// Returns the non-variadic native function parameter index for one named argument. -fn native_function_regular_param_index( - function: &NativeFunction, - variadic_index: usize, - name: &str, -) -> Option { - function - .param_names() - .iter() - .enumerate() - .position(|(index, param)| index < variadic_index && param == name) -} - -/// Binds evaluated method arguments using a selected by-reference target policy. -pub(in crate::interpreter) fn bind_evaluated_method_args_with_ref_mode( - params: &[String], - parameter_types: &[Option], - parameter_defaults: &[Option], - parameter_is_by_ref: &[bool], - parameter_is_variadic: &[bool], - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut bound_args = vec![None; params.len()]; - let variadic_index = parameter_is_variadic - .iter() - .position(|is_variadic| *is_variadic); - let required_count = - method_required_param_count(params.len(), parameter_defaults, parameter_is_variadic); - let mut next_positional = 0; - let mut next_variadic_index = 0_i64; - let mut variadic_named_args = std::collections::HashSet::new(); - - if let Some(index) = variadic_index { - let array = if evaluated_args_contain_named_variadic_values( - params, - variadic_index, - &evaluated_args, - ) { - values.assoc_new(evaluated_args.len())? - } else { - values.array_new(evaluated_args.len())? - }; - bound_args[index] = Some(BoundMethodArg { - value: array, - ref_target: None, - variadic_ref_targets: Vec::new(), - }); - } - - for arg in evaluated_args { - if let Some(name) = arg.name { - bind_dynamic_named_method_arg( - params, - parameter_types, - parameter_is_by_ref, - variadic_index, - &mut bound_args, - &name, - arg.value, - arg.ref_target, - by_ref_mode, - &mut variadic_named_args, - context, - values, - )?; - } else { - bind_dynamic_positional_method_arg( - params, - &mut bound_args, - parameter_types, - parameter_is_by_ref, - variadic_index, - &mut next_positional, - &mut next_variadic_index, - arg.value, - arg.ref_target, - by_ref_mode, - context, - values, - )?; - } - } - - for (position, value) in bound_args.iter_mut().enumerate() { - if Some(position) == variadic_index { - continue; - } - if value.is_none() { - if position < required_count { - return Err(EvalStatus::RuntimeFatal); - } - let Some(Some(default)) = parameter_defaults.get(position) else { - return Err(EvalStatus::RuntimeFatal); - }; - *value = Some(BoundMethodArg { - value: eval_method_parameter_default(default, context, values)?, - ref_target: None, - variadic_ref_targets: Vec::new(), - }); - } - if let Some(param_type) = parameter_types.get(position).and_then(Option::as_ref) { - let bound = value.as_mut().ok_or(EvalStatus::RuntimeFatal)?; - bound.value = eval_method_parameter_value(param_type, bound.value, context, values)?; - } - } - - bound_args - .into_iter() - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Returns the minimum argument count for a PHP method signature. -fn method_required_param_count( - param_count: usize, - defaults: &[Option], - variadics: &[bool], -) -> usize { - let fixed_count = variadics - .iter() - .position(|is_variadic| *is_variadic) - .unwrap_or(param_count); - (0..fixed_count) - .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) - .map_or(0, |position| position + 1) -} - -/// Returns true when evaluated args contain named values captured by a variadic parameter. -fn evaluated_args_contain_named_variadic_values( - params: &[String], - variadic_index: Option, - evaluated_args: &[EvaluatedCallArg], -) -> bool { - let Some(variadic_index) = variadic_index else { - return false; - }; - evaluated_args.iter().any(|arg| { - arg.name.as_ref().is_some_and(|name| { - regular_method_param_index(params, Some(variadic_index), name).is_none() - }) - }) -} - -/// Binds one positional method argument to a fixed parameter or variadic array. -fn bind_dynamic_positional_method_arg( - params: &[String], - bound_args: &mut [Option], - parameter_types: &[Option], - parameter_is_by_ref: &[bool], - variadic_index: Option, - next_positional: &mut usize, - next_variadic_index: &mut i64, - value: RuntimeCellHandle, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if variadic_index.is_some_and(|index| *next_positional >= index) { - let argument_number = variadic_index - .and_then(|index| { - usize::try_from(*next_variadic_index) - .ok() - .and_then(|offset| index.checked_add(offset)) - }) - .and_then(|index| index.checked_add(1)) - .ok_or(EvalStatus::RuntimeFatal)?; - let key = values.int(*next_variadic_index)?; - *next_variadic_index = next_variadic_index - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - let value = eval_variadic_method_parameter_value( - parameter_types, - variadic_index, - value, - context, - values, - )?; - let ref_target = method_parameter_ref_target( - params, - parameter_is_by_ref, - variadic_index, - argument_number, - ref_target, - by_ref_mode, - values, - )?; - return bind_dynamic_variadic_arg( - bound_args, - variadic_index, - key, - value, - ref_target, - values, - ); - } - let param_index = *next_positional; - if param_index >= bound_args.len() || bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let ref_target = method_parameter_ref_target( - params, - parameter_is_by_ref, - Some(param_index), - param_index + 1, - ref_target, - by_ref_mode, - values, - )?; - bound_args[param_index] = Some(BoundMethodArg { - value, - ref_target, - variadic_ref_targets: Vec::new(), - }); - *next_positional += 1; - Ok(()) -} - -/// Binds one named method argument to a fixed parameter or variadic array. -fn bind_dynamic_named_method_arg( - params: &[String], - parameter_types: &[Option], - parameter_is_by_ref: &[bool], - variadic_index: Option, - bound_args: &mut [Option], - name: &str, - value: RuntimeCellHandle, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - variadic_named_args: &mut std::collections::HashSet, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if let Some(param_index) = regular_method_param_index(params, variadic_index, name) { - if bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let ref_target = method_parameter_ref_target( - params, - parameter_is_by_ref, - Some(param_index), - param_index + 1, - ref_target, - by_ref_mode, - values, - )?; - bound_args[param_index] = Some(BoundMethodArg { - value, - ref_target, - variadic_ref_targets: Vec::new(), - }); - return Ok(()); - } - if variadic_index.is_none() || !variadic_named_args.insert(name.to_string()) { - return Err(EvalStatus::RuntimeFatal); - } - let key = values.string(name)?; - let value = eval_variadic_method_parameter_value( - parameter_types, - variadic_index, - value, - context, - values, - )?; - let argument_number = variadic_index - .and_then(|index| index.checked_add(1)) - .ok_or(EvalStatus::RuntimeFatal)?; - let ref_target = method_parameter_ref_target( - params, - parameter_is_by_ref, - variadic_index, - argument_number, - ref_target, - by_ref_mode, - values, - )?; - bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, ref_target, values) -} - -/// Returns the caller writeback target required by a by-reference method parameter. -fn method_parameter_ref_target( - params: &[String], - parameter_is_by_ref: &[bool], - param_index: Option, - argument_number: usize, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(param_index) = param_index else { - return Ok(None); - }; - if !parameter_is_by_ref - .get(param_index) - .copied() - .unwrap_or(false) - { - return Ok(None); - } - if let Some(ref_target) = ref_target { - return Ok(Some(ref_target)); - } - match by_ref_mode { - EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), - EvalByRefBindingMode::WarnByValue { callable_name } => { - let param_name = params - .get(param_index) - .map(String::as_str) - .unwrap_or("arg"); - values.warning(&format!( - "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", - argument_number - ))?; - Ok(None) - } - } -} - -/// Returns the by-reference flags that should be installed into the callee scope. -pub(in crate::interpreter) fn method_scope_parameter_ref_flags( - parameter_is_by_ref: &[bool], - bound_args: &[BoundMethodArg], - by_ref_mode: EvalByRefBindingMode<'_>, -) -> Vec { - if matches!(by_ref_mode, EvalByRefBindingMode::RequireTarget) { - return parameter_is_by_ref.to_vec(); - } - parameter_is_by_ref - .iter() - .enumerate() - .map(|(position, is_by_ref)| { - *is_by_ref - && bound_args.get(position).is_some_and(|arg| { - arg.ref_target.is_some() || !arg.variadic_ref_targets.is_empty() - }) - }) - .collect() -} - -/// Applies a variadic parameter type to one captured argument value. -fn eval_variadic_method_parameter_value( - parameter_types: &[Option], - variadic_index: Option, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(param_type) = - variadic_index.and_then(|index| parameter_types.get(index).and_then(Option::as_ref)) - else { - return Ok(value); - }; - eval_method_parameter_value(param_type, value, context, values) -} - -/// Returns the matching non-variadic parameter index for one PHP named argument. -fn regular_method_param_index( - params: &[String], - variadic_index: Option, - name: &str, -) -> Option { - params - .iter() - .enumerate() - .position(|(index, param)| Some(index) != variadic_index && param == name) -} - -/// Appends one value into the method variadic array. -fn bind_dynamic_variadic_arg( - bound_args: &mut [Option], - variadic_index: Option, - key: RuntimeCellHandle, - value: RuntimeCellHandle, - ref_target: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; - let bound = bound_args[index].as_mut().ok_or(EvalStatus::RuntimeFatal)?; - bound.value = values.array_set(bound.value, key, value)?; - if let Some(ref_target) = ref_target { - bound.variadic_ref_targets.push((key, ref_target)); - } - Ok(()) -} - -/// Applies one eval method parameter type to a bound runtime value. -pub(in crate::interpreter) fn eval_method_parameter_value( - param_type: &EvalParameterType, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_method_parameter_type_accepts_exact(param_type, value, context, values)? { - return Ok(value); - } - if param_type.is_intersection() { - return Err(EvalStatus::RuntimeFatal); - } - for variant in param_type.variants() { - if let Some(coerced) = - eval_method_parameter_scalar_coercion(variant, value, context, values)? - { - return Ok(coerced); - } - } - Err(EvalStatus::RuntimeFatal) -} - -/// Returns whether a value satisfies one eval parameter type without scalar coercion. -fn eval_method_parameter_type_accepts_exact( - param_type: &EvalParameterType, - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - if tag == EVAL_TAG_NULL && param_type.allows_null() { - return Ok(true); - } - if param_type.is_intersection() { - for variant in param_type.variants() { - if !eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { - return Ok(false); - } - } - return Ok(true); - } - for variant in param_type.variants() { - if eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { - return Ok(true); - } - } - Ok(false) -} - -/// Returns whether a value exactly satisfies one non-null eval parameter type atom. -fn eval_method_parameter_variant_accepts_exact( - variant: &EvalParameterTypeVariant, - value: RuntimeCellHandle, - tag: u64, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match variant { - EvalParameterTypeVariant::Array => Ok(matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC)), - EvalParameterTypeVariant::Bool => Ok(tag == EVAL_TAG_BOOL), - EvalParameterTypeVariant::Callable => Ok(matches!( - tag, - EVAL_TAG_STRING | EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT - )), - EvalParameterTypeVariant::Class(class_name) => { - eval_method_parameter_class_accepts(value, tag, class_name, context, values) - } - EvalParameterTypeVariant::Float => Ok(tag == EVAL_TAG_FLOAT), - EvalParameterTypeVariant::Int => Ok(tag == EVAL_TAG_INT), - EvalParameterTypeVariant::Iterable => { - if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Ok(true); - } - if eval_method_parameter_class_accepts(value, tag, "Traversable", context, values)? { - return Ok(true); - } - eval_method_parameter_class_accepts(value, tag, "Iterator", context, values) - } - EvalParameterTypeVariant::Mixed => Ok(true), - EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void => Ok(false), - EvalParameterTypeVariant::Object => Ok(tag == EVAL_TAG_OBJECT), - EvalParameterTypeVariant::String => Ok(tag == EVAL_TAG_STRING), - } -} - -/// Returns whether an object value satisfies one class/interface parameter target. -fn eval_method_parameter_class_accepts( - value: RuntimeCellHandle, - tag: u64, - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if tag != EVAL_TAG_OBJECT { - return Ok(false); - } - let target = eval_method_parameter_runtime_class_name(class_name, context)?; - let identity = values.object_identity(value)?; - if let Some(class) = context.dynamic_object_class(identity) { - return Ok(context.class_is_a(class.name(), &target, false)); - } - values.object_is_a(value, &target, false) -} - -/// Resolves late-bound class keywords inside eval method parameter type checks. -fn eval_method_parameter_runtime_class_name( - class_name: &str, - context: &ElephcEvalContext, -) -> Result { - match class_name.to_ascii_lowercase().as_str() { - "self" | "static" => context - .current_class_scope() - .map(str::to_string) - .ok_or(EvalStatus::RuntimeFatal), - "parent" => { - let current = context - .current_class_scope() - .ok_or(EvalStatus::RuntimeFatal)?; - context - .class(current) - .and_then(EvalClass::parent) - .map(str::to_string) - .ok_or(EvalStatus::RuntimeFatal) - } - _ => Ok(context - .resolve_class_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), - } -} - -/// Applies PHP weak-mode scalar coercion for supported scalar parameter types. -pub(in crate::interpreter) fn eval_method_parameter_scalar_coercion( - variant: &EvalParameterTypeVariant, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let tag = values.type_tag(value)?; - match variant { - EvalParameterTypeVariant::Bool if eval_method_scalar_coercible_tag(tag) => { - values.cast_bool(value).map(Some) - } - EvalParameterTypeVariant::Float - if eval_method_numeric_coercible_value(value, tag, values)? => - { - values.cast_float(value).map(Some) - } - EvalParameterTypeVariant::Int - if eval_method_numeric_coercible_value(value, tag, values)? => - { - values.cast_int(value).map(Some) - } - EvalParameterTypeVariant::String if eval_method_scalar_coercible_tag(tag) => { - values.cast_string(value).map(Some) - } - EvalParameterTypeVariant::String if tag == EVAL_TAG_OBJECT => { - let coerced = eval_dynamic_object_string_context_value(value, context, values)?; - if values.type_tag(coerced)? == EVAL_TAG_STRING { - Ok(Some(coerced)) - } else { - Ok(None) - } - } - _ => Ok(None), - } -} - -/// Converts objects in string contexts through the applicable `__toString()` dispatch path. -pub(in crate::interpreter) fn eval_string_context_value( - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? != EVAL_TAG_OBJECT { - return Ok(value); - } - eval_dynamic_object_string_context_value(value, context, values) -} - -/// Invokes `__toString()` for eval-declared objects or throws PHP's missing-hook error. -fn eval_dynamic_object_string_context_value( - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(value)?; - let Some(class) = context.dynamic_object_class(identity) else { - return eval_runtime_object_string_context_value(value, context, values); - }; - let called_class_name = class.name().to_string(); - let Some((declaring_class, method)) = context.class_method(&called_class_name, "__toString") - else { - return eval_throw_object_to_string_error(&called_class_name, context, values); - }; - if method.visibility() != EvalVisibility::Public - || method.is_static() - || method.is_abstract() - || !method.params().is_empty() - { - return Err(EvalStatus::RuntimeFatal); - } - let result = eval_dynamic_method_with_values( - &declaring_class, - &called_class_name, - &method, - value, - Vec::new(), - context, - values, - )?; - eval_tostring_result_to_string(result, values) -} - -/// Invokes the interpreter method dispatcher for AOT/native objects in string contexts. -fn eval_runtime_object_string_context_value( - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(value)?; - let class_name = runtime_object_class_name(value, values)?; - if !eval_runtime_object_has_interpreter_tostring(identity, &class_name, context) - && eval_aot_method_dispatch_metadata_in_hierarchy( - &class_name, - "__toString", - context, - values, - )? - .is_none() - { - return eval_throw_object_to_string_error(&class_name, context, values); - } - let result = eval_method_call_result_with_evaluated_args( - value, - "__toString", - Vec::new(), - context, - values, - )?; - eval_tostring_result_to_string(result, values) -} - -/// Returns whether eval owns a synthetic `__toString()` implementation for the runtime object. -fn eval_runtime_object_has_interpreter_tostring( - identity: u64, - class_name: &str, - context: &ElephcEvalContext, -) -> bool { - context.eval_reflection_class_name(identity).is_some() - || context.eval_reflection_function_name(identity).is_some() - || context.eval_reflection_method(identity).is_some() - || context.eval_reflection_property(identity).is_some() - || context.eval_reflection_class_constant(identity).is_some() - || eval_runtime_class_name_has_reflection_tostring(class_name) -} - -/// Returns whether a synthetic reflection class has `__toString()` handled by eval dispatch. -fn eval_runtime_class_name_has_reflection_tostring(class_name: &str) -> bool { - let class_name = class_name.trim_start_matches('\\'); - [ - "ReflectionParameter", - "ReflectionNamedType", - "ReflectionUnionType", - "ReflectionIntersectionType", - ] - .iter() - .any(|reflection_class| class_name.eq_ignore_ascii_case(reflection_class)) -} - -/// Throws PHP's catchable object-to-string conversion error. -fn eval_throw_object_to_string_error( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Object of class {} could not be converted to string", - class_name.trim_start_matches('\\') - ), - context, - values, - ) -} - -/// Normalizes one `__toString()` result to a boxed string cell. -fn eval_tostring_result_to_string( - result: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(result)? == EVAL_TAG_STRING { - return Ok(result); - } - let coerced = values.cast_string(result)?; - values.release(result)?; - Ok(coerced) -} - -/// Returns whether a runtime tag can be weakly coerced to string/bool parameters. -fn eval_method_scalar_coercible_tag(tag: u64) -> bool { - matches!( - tag, - EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_STRING | EVAL_TAG_BOOL - ) -} - -/// Returns whether a runtime value can be weakly coerced to a numeric parameter. -fn eval_method_numeric_coercible_value( - value: RuntimeCellHandle, - tag: u64, - values: &mut impl RuntimeValueOps, -) -> Result { - match tag { - EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL => Ok(true), - EVAL_TAG_STRING => Ok(eval_is_numeric_string(&values.string_bytes(value)?)), - _ => Ok(false), - } -} - -/// Materializes a supported eval method parameter default expression. -pub(in crate::interpreter) fn eval_method_parameter_default( - default: &EvalExpr, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !eval_method_default_expr_is_supported(default) { - return Err(EvalStatus::UnsupportedConstruct); - } - let mut default_scope = ElephcEvalScope::new(); - eval_expr(default, context, &mut default_scope, values) -} - -/// Returns whether an EvalIR expression can be safely evaluated as a method default. -fn eval_method_default_expr_is_supported(expr: &EvalExpr) -> bool { - match expr { - EvalExpr::Array(elements) => elements - .iter() - .all(eval_method_default_array_element_is_supported), - EvalExpr::Const(_) | EvalExpr::Magic(_) => true, - EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, - EvalExpr::ClassConstantFetch { class_name, .. } - | EvalExpr::ClassNameFetch { class_name } => { - eval_method_default_class_receiver_is_supported(class_name) - } - EvalExpr::NewObject { class_name, args } => { - eval_method_default_class_receiver_is_supported(class_name) - && args.iter().all(eval_method_default_call_arg_is_supported) - } - EvalExpr::NewAnonymousClass { .. } => false, - EvalExpr::NullCoalesce { value, default } => { - eval_method_default_expr_is_supported(value) - && eval_method_default_expr_is_supported(default) - } - EvalExpr::Ternary { - condition, - then_branch, - else_branch, - } => { - eval_method_default_expr_is_supported(condition) - && then_branch - .as_deref() - .is_none_or(eval_method_default_expr_is_supported) - && eval_method_default_expr_is_supported(else_branch) - } - EvalExpr::Cast { expr, .. } => eval_method_default_expr_is_supported(expr), - EvalExpr::Unary { expr, .. } => eval_method_default_expr_is_supported(expr), - EvalExpr::Binary { left, right, .. } => { - eval_method_default_expr_is_supported(left) - && eval_method_default_expr_is_supported(right) - } - _ => false, - } -} - -/// Returns whether one object-construction argument is safe inside a method default. -fn eval_method_default_call_arg_is_supported(arg: &EvalCallArg) -> bool { - !arg.is_spread() && eval_method_default_expr_is_supported(arg.value()) -} - -/// Returns whether one array default element contains only supported constant expressions. -fn eval_method_default_array_element_is_supported(element: &EvalArrayElement) -> bool { - match element { - EvalArrayElement::Value(value) => eval_method_default_expr_is_supported(value), - EvalArrayElement::Reference(_) => false, - EvalArrayElement::KeyValue { key, value } => { - eval_method_default_expr_is_supported(key) - && eval_method_default_expr_is_supported(value) - } - EvalArrayElement::KeyReference { .. } => false, - } -} - -/// Returns whether a class-like receiver is legal in a compile-time method default. -fn eval_method_default_class_receiver_is_supported(class_name: &str) -> bool { - !class_name - .trim_start_matches('\\') - .eq_ignore_ascii_case("static") -} - -/// Binds one positional dynamic-call value to the next declared parameter slot. -pub(in crate::interpreter) fn bind_dynamic_positional_arg( - bound_args: &mut [Option], - next_positional: &mut usize, - value: RuntimeCellHandle, -) -> Result<(), EvalStatus> { - if *next_positional >= bound_args.len() || bound_args[*next_positional].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[*next_positional] = Some(value); - *next_positional += 1; - Ok(()) -} - -/// Binds one named dynamic-call value to the matching declared parameter slot. -pub(in crate::interpreter) fn bind_dynamic_named_arg( - params: &[String], - bound_args: &mut [Option], - name: &str, - value: RuntimeCellHandle, -) -> Result<(), EvalStatus> { - let Some(param_index) = params.iter().position(|param| param == name) else { - return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[param_index] = Some(value); - Ok(()) -} - -/// Evaluates an eval-declared function after its positional arguments are prepared. -pub(super) fn eval_dynamic_function_with_values( - function: &EvalFunction, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = evaluated_args - .into_iter() - .map(|value| EvaluatedCallArg { - name: None, - value, - ref_target: None, - }) - .collect(); - eval_dynamic_function_with_evaluated_args(function, evaluated_args, context, values) -} - -/// Evaluates an eval-declared function after call arguments preserve names and ref targets. -pub(super) fn eval_dynamic_function_with_evaluated_args( - function: &EvalFunction, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_dynamic_function_with_evaluated_args_and_ref_flags( - function, - function.parameter_is_by_ref(), - evaluated_args, - context, - values, - ) -} - -/// Evaluates an eval-declared function with caller-selected by-ref binding flags. -pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_flags( - function: &EvalFunction, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_dynamic_function_with_evaluated_args_and_ref_mode( - function, - parameter_is_by_ref, - evaluated_args, - EvalByRefBindingMode::RequireTarget, - context, - values, - ) -} - -/// Evaluates an eval-declared function with caller-selected by-ref mode. -pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_mode( - function: &EvalFunction, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let static_names = static_var_names(function.body()); - context.push_function(function.name()); - let evaluated_args = match bind_evaluated_method_args_with_ref_mode( - function.params(), - function.parameter_types(), - function.parameter_defaults(), - parameter_is_by_ref, - function.parameter_is_variadic(), - evaluated_args, - by_ref_mode, - context, - values, - ) { - Ok(args) => args, - Err(status) => { - context.pop_function(); - return Err(status); - } - }; - let scope_parameter_is_by_ref = - method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); - let mut function_scope = ElephcEvalScope::new(); - bind_method_scope_args( - &mut function_scope, - function.params(), - &scope_parameter_is_by_ref, - &evaluated_args, - ); - let result = execute_statements(function.body(), context, &mut function_scope, values); - let persist_result = persist_static_locals( - context, - function.name(), - &static_names, - &function_scope, - values, - ); - let writeback_result = write_back_method_ref_args( - function.params(), - &evaluated_args, - &function_scope, - context, - values, - ); - let return_result = match (persist_result, writeback_result, result) { - (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), - (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( - function.return_type(), - None, - None, - control, - context, - values, - ), - }; - context.pop_function(); - return_result -} - -/// Evaluates one runtime eval closure after callback arguments preserve names and ref targets. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args( - closure: &EvalClosure, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_closure_with_optional_binding( - closure, - function_ref_flags(closure), - EvalByRefBindingMode::RequireTarget, - None, - evaluated_args, - context, - values, - ) -} - -/// Evaluates one runtime eval closure with `$this` and an optional binding scope. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope( - closure: &EvalClosure, - bound_this: RuntimeCellHandle, - bound_scope: Option, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if closure.is_static() { - values.warning("Cannot bind an instance to a static closure")?; - return values.null(); - } - let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; - let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); - eval_closure_with_optional_binding( - closure, - function_ref_flags(closure), - EvalByRefBindingMode::RequireTarget, - Some(EvalClosureBinding { - this_object: Some(bound_this), - class_scope, - called_class, - }), - evaluated_args, - context, - values, - ) -} - -/// Evaluates a runtime eval closure with `$this`, scope, and caller-selected by-ref flags. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope_ref_flags( - closure: &EvalClosure, - bound_this: RuntimeCellHandle, - bound_scope: Option, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if closure.is_static() { - values.warning("Cannot bind an instance to a static closure")?; - return values.null(); - } - let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; - let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); - eval_closure_with_optional_binding( - closure, - parameter_is_by_ref, - EvalByRefBindingMode::RequireTarget, - Some(EvalClosureBinding { - this_object: Some(bound_this), - class_scope, - called_class, - }), - evaluated_args, - context, - values, - ) -} - -/// Evaluates a runtime eval closure with `$this`, scope, and caller-selected by-ref mode. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope_ref_mode( - closure: &EvalClosure, - bound_this: RuntimeCellHandle, - bound_scope: Option, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if closure.is_static() { - values.warning("Cannot bind an instance to a static closure")?; - return values.null(); - } - let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; - let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); - eval_closure_with_optional_binding( - closure, - parameter_is_by_ref, - by_ref_mode, - Some(EvalClosureBinding { - this_object: Some(bound_this), - class_scope, - called_class, - }), - evaluated_args, - context, - values, - ) -} - -/// Evaluates one runtime eval closure with a class scope but no `$this` binding. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope( - closure: &EvalClosure, - bound_scope: Option, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(class_scope) = bound_scope else { - return eval_closure_with_evaluated_args(closure, evaluated_args, context, values); - }; - eval_closure_with_optional_binding( - closure, - function_ref_flags(closure), - EvalByRefBindingMode::RequireTarget, - Some(EvalClosureBinding { - this_object: None, - called_class: class_scope.clone(), - class_scope, - }), - evaluated_args, - context, - values, - ) -} - -/// Evaluates a runtime eval closure with scope-only binding and caller-selected by-ref flags. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_ref_flags( - closure: &EvalClosure, - bound_scope: Option, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(class_scope) = bound_scope else { - return eval_closure_with_optional_binding( - closure, - parameter_is_by_ref, - EvalByRefBindingMode::RequireTarget, - None, - evaluated_args, - context, - values, - ); - }; - eval_closure_with_optional_binding( - closure, - parameter_is_by_ref, - EvalByRefBindingMode::RequireTarget, - Some(EvalClosureBinding { - this_object: None, - called_class: class_scope.clone(), - class_scope, - }), - evaluated_args, - context, - values, - ) -} - -/// Evaluates a scope-only runtime eval closure with caller-selected by-ref mode. -pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_ref_mode( - closure: &EvalClosure, - bound_scope: Option, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(class_scope) = bound_scope else { - return eval_closure_with_optional_binding( - closure, - parameter_is_by_ref, - by_ref_mode, - None, - evaluated_args, - context, - values, - ); - }; - eval_closure_with_optional_binding( - closure, - parameter_is_by_ref, - by_ref_mode, - Some(EvalClosureBinding { - this_object: None, - called_class: class_scope.clone(), - class_scope, - }), - evaluated_args, - context, - values, - ) -} - -/// Class binding metadata for a runtime eval closure invocation. -struct EvalClosureBinding { - this_object: Option, - class_scope: String, - called_class: String, -} - -/// Returns the closure function's declared by-reference parameter flags. -fn function_ref_flags(closure: &EvalClosure) -> &[bool] { - closure.function().parameter_is_by_ref() -} - -/// Evaluates one runtime eval closure with optional class and `$this` binding metadata. -fn eval_closure_with_optional_binding( - closure: &EvalClosure, - parameter_is_by_ref: &[bool], - by_ref_mode: EvalByRefBindingMode<'_>, - binding: Option, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let function = closure.function(); - let static_names = static_var_names(function.body()); - let bound_class_pushed = binding.is_some(); - context.push_function(function.name()); - if let Some(binding) = &binding { - context.push_class_scope(binding.class_scope.clone()); - context.push_called_class_scope(binding.called_class.clone()); - } - let evaluated_args = match bind_evaluated_method_args_with_ref_mode( - function.params(), - function.parameter_types(), - function.parameter_defaults(), - parameter_is_by_ref, - function.parameter_is_variadic(), - evaluated_args, - by_ref_mode, - context, - values, - ) { - Ok(args) => args, - Err(status) => { - if bound_class_pushed { - context.pop_called_class_scope(); - context.pop_class_scope(); - } - context.pop_function(); - return Err(status); - } - }; - let mut function_scope = ElephcEvalScope::new(); - bind_closure_captures(&mut function_scope, closure.captures()); - if let Some(object) = binding.and_then(|binding| binding.this_object) { - function_scope.set("this", object, ScopeCellOwnership::Borrowed); - } - let scope_parameter_is_by_ref = - method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); - bind_method_scope_args( - &mut function_scope, - function.params(), - &scope_parameter_is_by_ref, - &evaluated_args, - ); - let result = execute_statements(function.body(), context, &mut function_scope, values); - let persist_result = persist_static_locals( - context, - function.name(), - &static_names, - &function_scope, - values, - ); - let capture_writeback_result = - write_back_closure_ref_captures(closure.captures(), &function_scope, context, values); - let arg_writeback_result = write_back_method_ref_args( - function.params(), - &evaluated_args, - &function_scope, - context, - values, - ); - let return_result = match ( - persist_result, - capture_writeback_result, - arg_writeback_result, - result, - ) { - (Err(status), _, _, _) - | (_, Err(status), _, _) - | (_, _, Err(status), _) - | (_, _, _, Err(status)) => Err(status), - (Ok(()), Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( - function.return_type(), - None, - None, - control, - context, - values, - ), - }; - if bound_class_pushed { - context.pop_called_class_scope(); - context.pop_class_scope(); - } - context.pop_function(); - return_result -} - -/// Returns the PHP class name used as the bound scope for `Closure::call()`. -pub(in crate::interpreter) fn eval_closure_bound_object_class_name( - object: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - if let Ok(identity) = values.object_identity(object) { - if let Some(class) = context.dynamic_object_class(identity) { - return Ok(class.name().to_string()); - } - } - runtime_object_class_name(object, values) -} - -/// Seeds one closure activation scope with values captured when the closure was created. -fn bind_closure_captures( - function_scope: &mut ElephcEvalScope, - captures: &[EvalClosureCaptureBinding], -) { - for capture in captures { - if let Some(target) = capture.by_ref_target().cloned() { - function_scope.set_reference( - capture.name().to_string(), - capture.name().to_string(), - capture.value(), - ScopeCellOwnership::Borrowed, - ); - function_scope.set_reference_target(capture.name().to_string(), target); - } else { - function_scope.set( - capture.name().to_string(), - capture.value(), - ScopeCellOwnership::Borrowed, - ); - } - } -} - -/// Writes modified by-reference closure captures back to their defining caller targets. -fn write_back_closure_ref_captures( - captures: &[EvalClosureCaptureBinding], - function_scope: &ElephcEvalScope, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for capture in captures { - let Some(target) = capture.by_ref_target() else { - continue; - }; - let Some(entry) = function_scope - .entry(capture.name()) - .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) - else { - continue; - }; - write_back_method_ref_target(target, entry.cell(), context, values)?; - } - Ok(()) -} - -/// Persists static local variables from one eval-declared function activation. -pub(super) fn persist_static_locals( - context: &mut ElephcEvalContext, - function_name: &str, - names: &[String], - scope: &ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for name in names { - if let Some(cell) = scope.visible_cell(name) { - if let Some(replaced) = - context.set_static_local(function_name.to_string(), name.clone(), cell) - { - values.release(replaced)?; - } - } - } - Ok(()) -} - -/// One source-order static local declaration and its initializer expression. -#[derive(Clone)] -pub(in crate::interpreter) struct EvalStaticVarInitializer { - pub name: String, - pub init: EvalExpr, -} - -/// Returns the distinct static local names declared anywhere in an eval function body. -pub(in crate::interpreter) fn static_var_names(body: &[EvalStmt]) -> Vec { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - visit_static_var_declarations(body, &mut seen, &mut |name, _| { - names.push(name.to_string()); - }); - names -} - -/// Returns static local declarations and initializers in first-seen source order. -pub(in crate::interpreter) fn static_var_initializers( - body: &[EvalStmt], -) -> Vec { - let mut vars = Vec::new(); - let mut seen = std::collections::HashSet::new(); - visit_static_var_declarations(body, &mut seen, &mut |name, init| { - vars.push(EvalStaticVarInitializer { - name: name.to_string(), - init: init.clone(), - }); - }); - vars -} - -/// Visits distinct static local declarations in first-seen source order. -fn visit_static_var_declarations( - body: &[EvalStmt], - seen: &mut std::collections::HashSet, - visitor: &mut impl FnMut(&str, &EvalExpr), -) { - for stmt in body { - match stmt { - EvalStmt::StaticVar { name, init } => { - if seen.insert(name.clone()) { - visitor(name, init); - } - } - EvalStmt::DoWhile { body, .. } - | EvalStmt::Foreach { body, .. } - | EvalStmt::For { body, .. } - | EvalStmt::While { body, .. } => visit_static_var_declarations(body, seen, visitor), - EvalStmt::FunctionDecl { .. } => {} - EvalStmt::If { - then_branch, - else_branch, - .. - } => { - visit_static_var_declarations(then_branch, seen, visitor); - visit_static_var_declarations(else_branch, seen, visitor); - } - EvalStmt::Switch { cases, .. } => { - for case in cases { - visit_static_var_declarations(&case.body, seen, visitor); - } - } - EvalStmt::Try { - body, - catches, - finally_body, - } => { - visit_static_var_declarations(body, seen, visitor); - for catch in catches { - visit_static_var_declarations(&catch.body, seen, visitor); - } - visit_static_var_declarations(finally_body, seen, visitor); - } - EvalStmt::ArrayAppendVar { .. } - | EvalStmt::ArraySetVar { .. } - | EvalStmt::Break - | EvalStmt::ClassDecl(_) - | EvalStmt::Continue - | EvalStmt::Echo(_) - | EvalStmt::EnumDecl(_) - | EvalStmt::Expr(_) - | EvalStmt::Global { .. } - | EvalStmt::InterfaceDecl(_) - | EvalStmt::DynamicPropertyArrayAppend { .. } - | EvalStmt::DynamicPropertyArraySet { .. } - | EvalStmt::DynamicPropertyCompoundAssign { .. } - | EvalStmt::DynamicPropertyIncDec { .. } - | EvalStmt::DynamicPropertyReferenceBind { .. } - | EvalStmt::DynamicPropertySet { .. } - | EvalStmt::DynamicStaticPropertyArrayAppend { .. } - | EvalStmt::DynamicStaticPropertyArraySet { .. } - | EvalStmt::DynamicStaticPropertyIncDec { .. } - | EvalStmt::DynamicStaticPropertyReferenceBind { .. } - | EvalStmt::DynamicStaticPropertyNameArrayAppend { .. } - | EvalStmt::DynamicStaticPropertyNameArraySet { .. } - | EvalStmt::DynamicStaticPropertyNameIncDec { .. } - | EvalStmt::DynamicStaticPropertyNameReferenceBind { .. } - | EvalStmt::DynamicStaticPropertyNameSet { .. } - | EvalStmt::DynamicStaticPropertySet { .. } - | EvalStmt::PropertyReferenceBind { .. } - | EvalStmt::PropertyArrayAppend { .. } - | EvalStmt::PropertyArraySet { .. } - | EvalStmt::PropertyCompoundAssign { .. } - | EvalStmt::PropertyIncDec { .. } - | EvalStmt::PropertySet { .. } - | EvalStmt::ReferenceAssign { .. } - | EvalStmt::Return(_) - | EvalStmt::StaticPropertyArrayAppend { .. } - | EvalStmt::StaticPropertyArraySet { .. } - | EvalStmt::StaticPropertyIncDec { .. } - | EvalStmt::StaticPropertyReferenceBind { .. } - | EvalStmt::StaticPropertySet { .. } - | EvalStmt::StoreVar { .. } - | EvalStmt::Throw(_) - | EvalStmt::TraitDecl(_) - | EvalStmt::UnsetArrayElement { .. } - | EvalStmt::UnsetDynamicProperty { .. } - | EvalStmt::UnsetDynamicStaticProperty { .. } - | EvalStmt::UnsetDynamicStaticPropertyName { .. } - | EvalStmt::UnsetProperty { .. } - | EvalStmt::UnsetStaticProperty { .. } - | EvalStmt::UnsetVar { .. } => {} - } - } -} - -/// Evaluates a registered AOT function through its descriptor-compatible invoker. -pub(super) fn eval_native_function( - function: NativeFunction, - args: &[EvalCallArg], - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let evaluated_args = - eval_native_function_call_args(&function, args, context, caller_scope, values)?; - eval_native_function_with_values(function, evaluated_args, context, values) -} - -/// Invokes a registered AOT function after its arguments have been bound and staged. -pub(super) fn eval_native_function_with_values( - function: NativeFunction, - bound_args: BoundNativeFunctionArgs, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !function.bridge_supported() { - return Err(EvalStatus::RuntimeFatal); - } - let variadic_index = native_function_variadic_index(&function); - if variadic_index.is_none() && bound_args.values.len() != function.param_count() { - return Err(EvalStatus::RuntimeFatal); - } - if let Some(variadic_index) = variadic_index { - if bound_args.values.len() < function.required_param_count().min(variadic_index) { - return Err(EvalStatus::RuntimeFatal); - } - } - let arg_array = match build_native_function_arg_array(&bound_args, values) { - Ok(arg_array) => arg_array, - Err(status) => { - cleanup_native_function_ref_args(&bound_args, values)?; - return Err(status); - } - }; - let result = unsafe { function.call(arg_array) }; - if let Err(status) = values.release(arg_array) { - cleanup_native_function_ref_args(&bound_args, values)?; - return Err(status); - } - let result = values.native_call_result(result); - let writeback = write_back_native_function_ref_args(&bound_args, context, values); - match (result, writeback) { - (Err(status), _) | (_, Err(status)) => Err(status), - (Ok(result), Ok(())) => { - eval_declared_native_return_value(function.return_type(), None, None, result, context, values) - } - } -} - -/// Builds the positional runtime array passed to descriptor-compatible native invokers. -fn build_native_function_arg_array( - bound_args: &BoundNativeFunctionArgs, - values: &mut impl RuntimeValueOps, -) -> Result { - let arg_array = values.array_new(bound_args.values.len())?; - for (index, value) in bound_args.values.iter().copied().enumerate() { - let index = match values.int(index as i64) { - Ok(index) => index, - Err(status) => { - values.release(arg_array)?; - return Err(status); - } - }; - if let Err(status) = values.array_set(arg_array, index, value) { - values.release(arg_array)?; - return Err(status); - } - } - Ok(arg_array) -} - -/// Releases retained raw native-function by-reference staging slots without writeback. -fn cleanup_native_function_ref_args( - bound_args: &BoundNativeFunctionArgs, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for ref_slot in &bound_args.ref_slots { - match ref_slot { - BoundNativeFunctionRefSlot::RawString { original, slot, .. } => { - let words = **slot; - values.release_raw_string_words(words[0], words[1])?; - if words[0] != original[0] { - values.release_raw_string_words(original[0], original[1])?; - } - } - BoundNativeFunctionRefSlot::OwnedRawWord { original, slot, .. } => { - let word = **slot; - values.release_raw_heap_word(word)?; - if word != *original { - values.release_raw_heap_word(*original)?; - } - } - BoundNativeFunctionRefSlot::Mixed { slot, .. } => { - values.release(**slot)?; - } - BoundNativeFunctionRefSlot::RawWord { .. } => {} - } - } - Ok(()) -} - -/// Writes changed staged native-function by-reference slots back to eval caller targets. -fn write_back_native_function_ref_args( - bound_args: &BoundNativeFunctionArgs, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for ref_slot in &bound_args.ref_slots { - match ref_slot { - BoundNativeFunctionRefSlot::Mixed { - original, - slot, - target, - } => { - let value = **slot; - if value == *original { - values.release(value)?; - continue; - } - let Some(target) = target else { - values.release(value)?; - continue; - }; - let current = match eval_reference_target_value(target, context, values) { - Ok(current) => current, - Err(status) => { - values.release(value)?; - return Err(status); - } - }; - if current == value { - values.release(value)?; - continue; - } - if let Err(status) = eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - ) { - values.release(value)?; - return Err(status); - } - } - BoundNativeFunctionRefSlot::RawWord { - tag, - original, - slot, - target, - } => { - let word = **slot; - if word == *original { - continue; - } - let Some(target) = target else { - continue; - }; - let value = values.raw_word_value(*tag, word)?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - BoundNativeFunctionRefSlot::RawString { - original, - slot, - target, - } => { - let words = **slot; - if target.is_none() { - values.release_raw_string_words(words[0], words[1])?; - if words[0] != original[0] { - values.release_raw_string_words(original[0], original[1])?; - } - continue; - } - let Some(target) = target else { - return Err(EvalStatus::RuntimeFatal); - }; - if words == *original { - values.release_raw_string_words(words[0], words[1])?; - continue; - } - let value = values.raw_string_value(words[0], words[1]); - values.release_raw_string_words(words[0], words[1])?; - if words[0] != original[0] { - values.release_raw_string_words(original[0], original[1])?; - } - let value = value?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - BoundNativeFunctionRefSlot::OwnedRawWord { - original, - slot, - target, - } => { - let word = **slot; - if target.is_none() { - values.release_raw_heap_word(word)?; - if word != *original { - values.release_raw_heap_word(*original)?; - } - continue; - } - let Some(target) = target else { - return Err(EvalStatus::RuntimeFatal); - }; - if word == *original { - values.release_raw_heap_word(word)?; - continue; - } - let value = values.raw_heap_word_value(word); - values.release_raw_heap_word(word)?; - values.release_raw_heap_word(*original)?; - let value = value?; - eval_write_direct_ref_target( - target, - value, - context, - values, - Some(ScopeCellOwnership::Owned), - )?; - } - } - } - Ok(()) -} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions/closure_execution.rs b/crates/elephc-magician/src/interpreter/dynamic_functions/closure_execution.rs new file mode 100644 index 0000000000..8da6060684 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions/closure_execution.rs @@ -0,0 +1,655 @@ +//! Purpose: +//! Executes eval-declared functions and closures with bound scope and captures. +//! +//! Called from: +//! - Dynamic function dispatch and callable/Closure invocation paths. +//! +//! Key details: +//! - Bound `$this`, class scope, reference captures, and static locals survive execution. + +use super::*; + +/// Evaluates an eval-declared function after its positional arguments are prepared. +pub(in crate::interpreter) fn eval_dynamic_function_with_values( + function: &EvalFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = evaluated_args + .into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect(); + eval_dynamic_function_with_evaluated_args(function, evaluated_args, context, values) +} + +/// Evaluates an eval-declared function after call arguments preserve names and ref targets. +pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args( + function: &EvalFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_function_with_evaluated_args_and_ref_flags( + function, + function.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Evaluates an eval-declared function with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_flags( + function: &EvalFunction, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_function_with_evaluated_args_and_ref_mode( + function, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Evaluates an eval-declared function with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_function_with_evaluated_args_and_ref_mode( + function: &EvalFunction, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let static_names = static_var_names(function.body()); + context.push_function(function.name()); + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( + function.params(), + function.parameter_types(), + function.parameter_defaults(), + parameter_is_by_ref, + function.parameter_is_variadic(), + evaluated_args, + by_ref_mode, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + context.pop_function(); + return Err(status); + } + }; + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); + let mut function_scope = ElephcEvalScope::new(); + bind_method_scope_args( + &mut function_scope, + function.params(), + &scope_parameter_is_by_ref, + &evaluated_args, + ); + let result = execute_statements(function.body(), context, &mut function_scope, values); + let persist_result = persist_static_locals( + context, + function.name(), + &static_names, + &function_scope, + values, + ); + let writeback_result = write_back_method_ref_args( + function.params(), + &evaluated_args, + &function_scope, + context, + values, + ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + function.return_type(), + None, + None, + control, + context, + values, + ), + }; + context.pop_function(); + return_result +} + +/// Evaluates one runtime eval closure after callback arguments preserve names and ref targets. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args( + closure: &EvalClosure, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_closure_with_optional_binding( + closure, + function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, + None, + evaluated_args, + context, + values, + ) +} + +/// Evaluates one runtime eval closure with `$this` and an optional binding scope. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); + eval_closure_with_optional_binding( + closure, + function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a runtime eval closure with `$this`, scope, and caller-selected by-ref flags. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope_ref_flags( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a runtime eval closure with `$this`, scope, and caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_this_scope_ref_mode( + closure: &EvalClosure, + bound_this: RuntimeCellHandle, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if closure.is_static() { + values.warning("Cannot bind an instance to a static closure")?; + return values.null(); + } + let called_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + let class_scope = bound_scope.unwrap_or_else(|| called_class.clone()); + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, + Some(EvalClosureBinding { + this_object: Some(bound_this), + class_scope, + called_class, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates one runtime eval closure with a class scope but no `$this` binding. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope( + closure: &EvalClosure, + bound_scope: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_evaluated_args(closure, evaluated_args, context, values); + }; + eval_closure_with_optional_binding( + closure, + function_ref_flags(closure), + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a runtime eval closure with scope-only binding and caller-selected by-ref flags. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_ref_flags( + closure: &EvalClosure, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, + None, + evaluated_args, + context, + values, + ); + }; + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + EvalByRefBindingMode::RequireTarget, + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), + evaluated_args, + context, + values, + ) +} + +/// Evaluates a scope-only runtime eval closure with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + closure: &EvalClosure, + bound_scope: Option, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_scope) = bound_scope else { + return eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, + None, + evaluated_args, + context, + values, + ); + }; + eval_closure_with_optional_binding( + closure, + parameter_is_by_ref, + by_ref_mode, + Some(EvalClosureBinding { + this_object: None, + called_class: class_scope.clone(), + class_scope, + }), + evaluated_args, + context, + values, + ) +} + +/// Class binding metadata for a runtime eval closure invocation. +struct EvalClosureBinding { + this_object: Option, + class_scope: String, + called_class: String, +} + +/// Returns the closure function's declared by-reference parameter flags. +fn function_ref_flags(closure: &EvalClosure) -> &[bool] { + closure.function().parameter_is_by_ref() +} + +/// Evaluates one runtime eval closure with optional class and `$this` binding metadata. +fn eval_closure_with_optional_binding( + closure: &EvalClosure, + parameter_is_by_ref: &[bool], + by_ref_mode: EvalByRefBindingMode<'_>, + binding: Option, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let function = closure.function(); + let static_names = static_var_names(function.body()); + let bound_class_pushed = binding.is_some(); + context.push_function(function.name()); + if let Some(binding) = &binding { + context.push_class_scope(binding.class_scope.clone()); + context.push_called_class_scope(binding.called_class.clone()); + } + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( + function.params(), + function.parameter_types(), + function.parameter_defaults(), + parameter_is_by_ref, + function.parameter_is_variadic(), + evaluated_args, + by_ref_mode, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + if bound_class_pushed { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + context.pop_function(); + return Err(status); + } + }; + let mut function_scope = ElephcEvalScope::new(); + bind_closure_captures(&mut function_scope, closure.captures()); + if let Some(object) = binding.and_then(|binding| binding.this_object) { + function_scope.set("this", object, ScopeCellOwnership::Borrowed); + } + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); + bind_method_scope_args( + &mut function_scope, + function.params(), + &scope_parameter_is_by_ref, + &evaluated_args, + ); + let result = execute_statements(function.body(), context, &mut function_scope, values); + let persist_result = persist_static_locals( + context, + function.name(), + &static_names, + &function_scope, + values, + ); + let capture_writeback_result = + write_back_closure_ref_captures(closure.captures(), &function_scope, context, values); + let arg_writeback_result = write_back_method_ref_args( + function.params(), + &evaluated_args, + &function_scope, + context, + values, + ); + let return_result = match ( + persist_result, + capture_writeback_result, + arg_writeback_result, + result, + ) { + (Err(status), _, _, _) + | (_, Err(status), _, _) + | (_, _, Err(status), _) + | (_, _, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + function.return_type(), + None, + None, + control, + context, + values, + ), + }; + if bound_class_pushed { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + context.pop_function(); + return_result +} + +/// Returns the PHP class name used as the bound scope for `Closure::call()`. +pub(in crate::interpreter) fn eval_closure_bound_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + } + runtime_object_class_name(object, values) +} + +/// Seeds one closure activation scope with values captured when the closure was created. +fn bind_closure_captures( + function_scope: &mut ElephcEvalScope, + captures: &[EvalClosureCaptureBinding], +) { + for capture in captures { + if let Some(target) = capture.by_ref_target().cloned() { + function_scope.set_reference( + capture.name().to_string(), + capture.name().to_string(), + capture.value(), + ScopeCellOwnership::Borrowed, + ); + function_scope.set_reference_target(capture.name().to_string(), target); + } else { + function_scope.set( + capture.name().to_string(), + capture.value(), + ScopeCellOwnership::Borrowed, + ); + } + } +} + +/// Writes modified by-reference closure captures back to their defining caller targets. +fn write_back_closure_ref_captures( + captures: &[EvalClosureCaptureBinding], + function_scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for capture in captures { + let Some(target) = capture.by_ref_target() else { + continue; + }; + let Some(entry) = function_scope + .entry(capture.name()) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + continue; + }; + write_back_method_ref_target(target, entry.cell(), context, values)?; + } + Ok(()) +} + +/// Persists static local variables from one eval-declared function activation. +pub(in crate::interpreter) fn persist_static_locals( + context: &mut ElephcEvalContext, + function_name: &str, + names: &[String], + scope: &ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for name in names { + if let Some(cell) = scope.visible_cell(name) { + if let Some(replaced) = + context.set_static_local(function_name.to_string(), name.clone(), cell) + { + values.release(replaced)?; + } + } + } + Ok(()) +} + +/// One source-order static local declaration and its initializer expression. +#[derive(Clone)] +pub(in crate::interpreter) struct EvalStaticVarInitializer { + pub name: String, + pub init: EvalExpr, +} + +/// Returns the distinct static local names declared anywhere in an eval function body. +pub(in crate::interpreter) fn static_var_names(body: &[EvalStmt]) -> Vec { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + visit_static_var_declarations(body, &mut seen, &mut |name, _| { + names.push(name.to_string()); + }); + names +} + +/// Returns static local declarations and initializers in first-seen source order. +pub(in crate::interpreter) fn static_var_initializers( + body: &[EvalStmt], +) -> Vec { + let mut vars = Vec::new(); + let mut seen = std::collections::HashSet::new(); + visit_static_var_declarations(body, &mut seen, &mut |name, init| { + vars.push(EvalStaticVarInitializer { + name: name.to_string(), + init: init.clone(), + }); + }); + vars +} + +/// Visits distinct static local declarations in first-seen source order. +fn visit_static_var_declarations( + body: &[EvalStmt], + seen: &mut std::collections::HashSet, + visitor: &mut impl FnMut(&str, &EvalExpr), +) { + for stmt in body { + match stmt { + EvalStmt::StaticVar { name, init } => { + if seen.insert(name.clone()) { + visitor(name, init); + } + } + EvalStmt::DoWhile { body, .. } + | EvalStmt::Foreach { body, .. } + | EvalStmt::For { body, .. } + | EvalStmt::While { body, .. } => visit_static_var_declarations(body, seen, visitor), + EvalStmt::FunctionDecl { .. } => {} + EvalStmt::If { + then_branch, + else_branch, + .. + } => { + visit_static_var_declarations(then_branch, seen, visitor); + visit_static_var_declarations(else_branch, seen, visitor); + } + EvalStmt::Switch { cases, .. } => { + for case in cases { + visit_static_var_declarations(&case.body, seen, visitor); + } + } + EvalStmt::Try { + body, + catches, + finally_body, + } => { + visit_static_var_declarations(body, seen, visitor); + for catch in catches { + visit_static_var_declarations(&catch.body, seen, visitor); + } + visit_static_var_declarations(finally_body, seen, visitor); + } + EvalStmt::ArrayAppendVar { .. } + | EvalStmt::ArraySetVar { .. } + | EvalStmt::Break + | EvalStmt::ClassDecl(_) + | EvalStmt::Continue + | EvalStmt::Echo(_) + | EvalStmt::EnumDecl(_) + | EvalStmt::Expr(_) + | EvalStmt::Global { .. } + | EvalStmt::InterfaceDecl(_) + | EvalStmt::DynamicPropertyArrayAppend { .. } + | EvalStmt::DynamicPropertyArraySet { .. } + | EvalStmt::DynamicPropertyCompoundAssign { .. } + | EvalStmt::DynamicPropertyIncDec { .. } + | EvalStmt::DynamicPropertyReferenceBind { .. } + | EvalStmt::DynamicPropertySet { .. } + | EvalStmt::DynamicStaticPropertyArrayAppend { .. } + | EvalStmt::DynamicStaticPropertyArraySet { .. } + | EvalStmt::DynamicStaticPropertyIncDec { .. } + | EvalStmt::DynamicStaticPropertyReferenceBind { .. } + | EvalStmt::DynamicStaticPropertyNameArrayAppend { .. } + | EvalStmt::DynamicStaticPropertyNameArraySet { .. } + | EvalStmt::DynamicStaticPropertyNameIncDec { .. } + | EvalStmt::DynamicStaticPropertyNameReferenceBind { .. } + | EvalStmt::DynamicStaticPropertyNameSet { .. } + | EvalStmt::DynamicStaticPropertySet { .. } + | EvalStmt::PropertyReferenceBind { .. } + | EvalStmt::PropertyArrayAppend { .. } + | EvalStmt::PropertyArraySet { .. } + | EvalStmt::PropertyCompoundAssign { .. } + | EvalStmt::PropertyIncDec { .. } + | EvalStmt::PropertySet { .. } + | EvalStmt::ReferenceAssign { .. } + | EvalStmt::Return(_) + | EvalStmt::StaticPropertyArrayAppend { .. } + | EvalStmt::StaticPropertyArraySet { .. } + | EvalStmt::StaticPropertyIncDec { .. } + | EvalStmt::StaticPropertyReferenceBind { .. } + | EvalStmt::StaticPropertySet { .. } + | EvalStmt::StoreVar { .. } + | EvalStmt::Throw(_) + | EvalStmt::TraitDecl(_) + | EvalStmt::UnsetArrayElement { .. } + | EvalStmt::UnsetDynamicProperty { .. } + | EvalStmt::UnsetDynamicStaticProperty { .. } + | EvalStmt::UnsetDynamicStaticPropertyName { .. } + | EvalStmt::UnsetProperty { .. } + | EvalStmt::UnsetStaticProperty { .. } + | EvalStmt::UnsetVar { .. } => {} + } + } +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions/function_binding.rs b/crates/elephc-magician/src/interpreter/dynamic_functions/function_binding.rs new file mode 100644 index 0000000000..8905fe950b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions/function_binding.rs @@ -0,0 +1,515 @@ +//! Purpose: +//! Binds evaluated arguments for eval-declared and native functions. +//! +//! Called from: +//! - Dynamic function and callable dispatch after source-order argument evaluation. +//! +//! Key details: +//! - Named, variadic, by-reference, and raw native arguments preserve PHP binding rules. + +use super::*; + +/// Binds evaluated positional and named values to declared parameter order. +pub(in crate::interpreter) fn bind_evaluated_function_args( + params: &[String], + evaluated_args: Vec, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_dynamic_named_arg(params, &mut bound_args, &name, arg.value)?; + } else { + bind_dynamic_positional_arg(&mut bound_args, &mut next_positional, arg.value)?; + } + } + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Binds already evaluated native AOT function args and fills omitted defaults. +pub(in crate::interpreter) fn bind_evaluated_native_function_args( + function: &NativeFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + bind_evaluated_native_function_args_with_mode( + function, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Binds native AOT function args for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn bind_evaluated_native_function_args_for_call_user_func( + callable_name: &str, + function: &NativeFunction, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + bind_evaluated_native_function_args_with_mode( + function, + evaluated_args, + EvalByRefBindingMode::WarnByValue { callable_name }, + context, + values, + ) +} + +/// Binds already evaluated native AOT function args using the selected by-reference mode. +fn bind_evaluated_native_function_args_with_mode( + function: &NativeFunction, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if native_function_variadic_index(function).is_some() { + return bind_evaluated_native_variadic_function_args( + function, + evaluated_args, + by_ref_mode, + context, + values, + ); + } + let mut bound_args = vec![None; function.param_count()]; + let has_param_names = function.param_names().len() == function.param_count(); + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + if !has_param_names { + return Err(EvalStatus::RuntimeFatal); + } + bind_native_function_named_arg( + function, + None, + &mut bound_args, + &name, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } else { + bind_native_function_positional_arg( + function, + &mut bound_args, + None, + &mut next_positional, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } + } + + for (position, bound) in bound_args.iter_mut().enumerate() { + if bound.is_some() { + continue; + } + if position < function.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = function.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *bound = Some(BoundMethodArg { + value: materialize_native_callable_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + let mut bound_args = bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal)?; + apply_native_function_arg_types(function, None, &mut bound_args, context, values)?; + stage_native_function_invoker_args(function, None, bound_args, by_ref_mode, values) +} + +/// Binds a native AOT variadic function while keeping the raw invoker argument layout. +fn bind_evaluated_native_variadic_function_args( + function: &NativeFunction, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let variadic_index = native_function_variadic_index(function).ok_or(EvalStatus::RuntimeFatal)?; + let has_param_names = function.param_names().len() == function.param_count(); + let mut regular_args = vec![None; variadic_index]; + let mut variadic_args = Vec::new(); + let mut next_positional = 0; + + for arg in evaluated_args { + if let Some(name) = arg.name { + if !has_param_names { + return Err(EvalStatus::RuntimeFatal); + } + if native_function_regular_param_index(function, variadic_index, &name).is_none() { + return Err(EvalStatus::RuntimeFatal); + } + bind_native_function_named_arg( + function, + Some(variadic_index), + &mut regular_args, + &name, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } else if next_positional < variadic_index { + bind_native_function_positional_arg( + function, + &mut regular_args, + Some(variadic_index), + &mut next_positional, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } else { + let ref_target = native_function_parameter_ref_target( + function, + Some(variadic_index), + arg.ref_target, + by_ref_mode, + values, + )?; + variadic_args.push(BoundMethodArg { + value: arg.value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + } + } + + for (position, bound) in regular_args.iter_mut().enumerate() { + if bound.is_some() { + continue; + } + if position < function.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = function.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *bound = Some(BoundMethodArg { + value: materialize_native_callable_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + let mut bound_args = regular_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal)?; + bound_args.extend(variadic_args); + apply_native_function_arg_types( + function, + Some(variadic_index), + &mut bound_args, + context, + values, + )?; + stage_native_function_invoker_args( + function, + Some(variadic_index), + bound_args, + by_ref_mode, + values, + ) +} + +/// Applies registered native AOT function parameter types after argument binding. +fn apply_native_function_arg_types( + function: &NativeFunction, + variadic_index: Option, + bound_args: &mut [BoundMethodArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, bound_arg) in bound_args.iter_mut().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + let Some(param_type) = function.param_type(param_index) else { + continue; + }; + bound_arg.value = eval_method_parameter_value(param_type, bound_arg.value, context, values)?; + } + Ok(()) +} + +/// Binds one named native AOT function argument to a non-variadic parameter slot. +fn bind_native_function_named_arg( + function: &NativeFunction, + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(param_index) = native_function_named_param_index(function, variadic_index, name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = + native_function_parameter_ref_target(function, Some(param_index), ref_target, by_ref_mode, values)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + Ok(()) +} + +/// Binds one positional native AOT function argument to the next fixed parameter. +fn bind_native_function_positional_arg( + function: &NativeFunction, + bound_args: &mut [Option], + variadic_index: Option, + next_positional: &mut usize, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let param_index = *next_positional; + if variadic_index.is_some_and(|index| param_index >= index) + || param_index >= bound_args.len() + || bound_args[param_index].is_some() + { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = + native_function_parameter_ref_target(function, Some(param_index), ref_target, by_ref_mode, values)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) +} + +/// Returns the caller writeback target required by a native function by-reference parameter. +fn native_function_parameter_ref_target( + function: &NativeFunction, + param_index: Option, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !function.param_by_ref(param_index) { + return Ok(None); + } + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = native_function_param_warning_name(function, param_index); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + param_index + 1 + ))?; + Ok(None) + } + } +} + +/// Converts bound values into descriptor-invoker arguments, staging by-reference slots. +fn stage_native_function_invoker_args( + function: &NativeFunction, + variadic_index: Option, + bound_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut invoker_values = Vec::with_capacity(bound_args.len()); + let mut ref_slots = Vec::new(); + for (position, bound_arg) in bound_args.into_iter().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + if !function.param_by_ref(param_index) { + invoker_values.push(bound_arg.value); + continue; + } + let target = match (bound_arg.ref_target, by_ref_mode) { + (Some(target), _) => Some(target), + (None, EvalByRefBindingMode::WarnByValue { .. }) => None, + (None, EvalByRefBindingMode::RequireTarget) => return Err(EvalStatus::RuntimeFatal), + }; + if let Some(raw_ref_kind) = native_function_raw_ref_kind(function.param_type(param_index)) { + match raw_ref_kind { + NativeFunctionRawRefKind::Scalar { tag } => { + let original = values.raw_value_word(bound_arg.value)?; + let mut slot = Box::new(original); + let marker = + values.invoker_raw_ref_cell(slot.as_mut() as *mut u64 as *mut c_void, tag)?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::RawWord { + tag, + original, + slot, + target, + }); + } + NativeFunctionRawRefKind::String => { + let original_ptr = values.raw_value_word(bound_arg.value)?; + let original_len = values.raw_value_high_word(bound_arg.value)?; + let retained = values.retain_raw_string_words(original_ptr, original_len)?; + let mut slot = Box::new([retained.0, retained.1]); + let marker = values.invoker_raw_ref_cell( + slot.as_mut() as *mut [u64; 2] as *mut c_void, + EVAL_TAG_STRING, + )?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::RawString { + original: [retained.0, retained.1], + slot, + target, + }); + } + NativeFunctionRawRefKind::OwnedHeap => { + let source_tag = values.type_tag(bound_arg.value)?; + let original = values.raw_value_word(bound_arg.value)?; + let retained = values.retain_raw_heap_word(original)?; + let mut slot = Box::new(retained); + let marker = values.invoker_raw_ref_cell( + slot.as_mut() as *mut u64 as *mut c_void, + source_tag, + )?; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::OwnedRawWord { + original, + slot, + target, + }); + } + } + continue; + } + let original = bound_arg.value; + let retained = values.retain(original)?; + let mut slot = Box::new(retained); + let marker = match values.invoker_ref_cell(slot.as_mut() as *mut RuntimeCellHandle) { + Ok(marker) => marker, + Err(status) => { + values.release(retained)?; + return Err(status); + } + }; + invoker_values.push(marker); + ref_slots.push(BoundNativeFunctionRefSlot::Mixed { + original, + slot, + target, + }); + } + Ok(BoundNativeFunctionArgs { + values: invoker_values, + ref_slots, + }) +} + +/// Returns the PHP parameter name used in by-reference warning diagnostics. +fn native_function_param_warning_name(function: &NativeFunction, param_index: usize) -> String { + function + .param_names() + .get(param_index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", param_index + 1)) +} + +/// Describes native function by-reference parameters that can use typed raw slots. +enum NativeFunctionRawRefKind { + Scalar { tag: u64 }, + String, + OwnedHeap, +} + +/// Returns the raw-slot strategy for one supported by-reference parameter. +fn native_function_raw_ref_kind(param_type: Option<&EvalParameterType>) -> Option { + let param_type = param_type?; + if param_type.allows_null() + || param_type.is_intersection() + || param_type.variants().len() != 1 + { + return None; + } + match param_type.variants().first()? { + EvalParameterTypeVariant::Array + | EvalParameterTypeVariant::Class(_) + | EvalParameterTypeVariant::Iterable + | EvalParameterTypeVariant::Object => Some(NativeFunctionRawRefKind::OwnedHeap), + EvalParameterTypeVariant::Bool => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_BOOL }), + EvalParameterTypeVariant::Float => { + Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_FLOAT }) + } + EvalParameterTypeVariant::Int => Some(NativeFunctionRawRefKind::Scalar { tag: EVAL_TAG_INT }), + EvalParameterTypeVariant::String => Some(NativeFunctionRawRefKind::String), + _ => None, + } +} + +/// Returns the variadic parameter index for a native AOT function, if registered. +pub(super) fn native_function_variadic_index(function: &NativeFunction) -> Option { + (0..function.param_count()).find(|index| function.param_variadic(*index)) +} + +/// Returns the native function parameter index for one named argument. +fn native_function_named_param_index( + function: &NativeFunction, + variadic_index: Option, + name: &str, +) -> Option { + function + .param_names() + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + +/// Returns the non-variadic native function parameter index for one named argument. +fn native_function_regular_param_index( + function: &NativeFunction, + variadic_index: usize, + name: &str, +) -> Option { + function + .param_names() + .iter() + .enumerate() + .position(|(index, param)| index < variadic_index && param == name) +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions/method_binding.rs b/crates/elephc-magician/src/interpreter/dynamic_functions/method_binding.rs new file mode 100644 index 0000000000..979ce94260 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions/method_binding.rs @@ -0,0 +1,816 @@ +//! Purpose: +//! Binds and coerces evaluated method arguments against eval signatures. +//! +//! Called from: +//! - Dynamic instance, static, closure, and reflected method invocation. +//! +//! Key details: +//! - Reference targets, scalar coercion, defaults, and object type checks stay aligned by index. + +use super::*; + +/// Binds evaluated method arguments using a selected by-reference target policy. +pub(in crate::interpreter) fn bind_evaluated_method_args_with_ref_mode( + params: &[String], + parameter_types: &[Option], + parameter_defaults: &[Option], + parameter_is_by_ref: &[bool], + parameter_is_variadic: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let variadic_index = parameter_is_variadic + .iter() + .position(|is_variadic| *is_variadic); + let required_count = + method_required_param_count(params.len(), parameter_defaults, parameter_is_variadic); + let mut next_positional = 0; + let mut next_variadic_index = 0_i64; + let mut variadic_named_args = std::collections::HashSet::new(); + + if let Some(index) = variadic_index { + let array = if evaluated_args_contain_named_variadic_values( + params, + variadic_index, + &evaluated_args, + ) { + values.assoc_new(evaluated_args.len())? + } else { + values.array_new(evaluated_args.len())? + }; + bound_args[index] = Some(BoundMethodArg { + value: array, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + for arg in evaluated_args { + if let Some(name) = arg.name { + bind_dynamic_named_method_arg( + params, + parameter_types, + parameter_is_by_ref, + variadic_index, + &mut bound_args, + &name, + arg.value, + arg.ref_target, + by_ref_mode, + &mut variadic_named_args, + context, + values, + )?; + } else { + bind_dynamic_positional_method_arg( + params, + &mut bound_args, + parameter_types, + parameter_is_by_ref, + variadic_index, + &mut next_positional, + &mut next_variadic_index, + arg.value, + arg.ref_target, + by_ref_mode, + context, + values, + )?; + } + } + + for (position, value) in bound_args.iter_mut().enumerate() { + if Some(position) == variadic_index { + continue; + } + if value.is_none() { + if position < required_count { + return Err(EvalStatus::RuntimeFatal); + } + let Some(Some(default)) = parameter_defaults.get(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *value = Some(BoundMethodArg { + value: eval_method_parameter_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + if let Some(param_type) = parameter_types.get(position).and_then(Option::as_ref) { + let bound = value.as_mut().ok_or(EvalStatus::RuntimeFatal)?; + bound.value = eval_method_parameter_value(param_type, bound.value, context, values)?; + } + } + + bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns the minimum argument count for a PHP method signature. +fn method_required_param_count( + param_count: usize, + defaults: &[Option], + variadics: &[bool], +) -> usize { + let fixed_count = variadics + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(param_count); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + +/// Returns true when evaluated args contain named values captured by a variadic parameter. +fn evaluated_args_contain_named_variadic_values( + params: &[String], + variadic_index: Option, + evaluated_args: &[EvaluatedCallArg], +) -> bool { + let Some(variadic_index) = variadic_index else { + return false; + }; + evaluated_args.iter().any(|arg| { + arg.name.as_ref().is_some_and(|name| { + regular_method_param_index(params, Some(variadic_index), name).is_none() + }) + }) +} + +/// Binds one positional method argument to a fixed parameter or variadic array. +fn bind_dynamic_positional_method_arg( + params: &[String], + bound_args: &mut [Option], + parameter_types: &[Option], + parameter_is_by_ref: &[bool], + variadic_index: Option, + next_positional: &mut usize, + next_variadic_index: &mut i64, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if variadic_index.is_some_and(|index| *next_positional >= index) { + let argument_number = variadic_index + .and_then(|index| { + usize::try_from(*next_variadic_index) + .ok() + .and_then(|offset| index.checked_add(offset)) + }) + .and_then(|index| index.checked_add(1)) + .ok_or(EvalStatus::RuntimeFatal)?; + let key = values.int(*next_variadic_index)?; + *next_variadic_index = next_variadic_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + let value = eval_variadic_method_parameter_value( + parameter_types, + variadic_index, + value, + context, + values, + )?; + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + variadic_index, + argument_number, + ref_target, + by_ref_mode, + values, + )?; + return bind_dynamic_variadic_arg( + bound_args, + variadic_index, + key, + value, + ref_target, + values, + ); + } + let param_index = *next_positional; + if param_index >= bound_args.len() || bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + Some(param_index), + param_index + 1, + ref_target, + by_ref_mode, + values, + )?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) +} + +/// Binds one named method argument to a fixed parameter or variadic array. +fn bind_dynamic_named_method_arg( + params: &[String], + parameter_types: &[Option], + parameter_is_by_ref: &[bool], + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + variadic_named_args: &mut std::collections::HashSet, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(param_index) = regular_method_param_index(params, variadic_index, name) { + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + Some(param_index), + param_index + 1, + ref_target, + by_ref_mode, + values, + )?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + return Ok(()); + } + if variadic_index.is_none() || !variadic_named_args.insert(name.to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + let key = values.string(name)?; + let value = eval_variadic_method_parameter_value( + parameter_types, + variadic_index, + value, + context, + values, + )?; + let argument_number = variadic_index + .and_then(|index| index.checked_add(1)) + .ok_or(EvalStatus::RuntimeFatal)?; + let ref_target = method_parameter_ref_target( + params, + parameter_is_by_ref, + variadic_index, + argument_number, + ref_target, + by_ref_mode, + values, + )?; + bind_dynamic_variadic_arg(bound_args, variadic_index, key, value, ref_target, values) +} + +/// Returns the caller writeback target required by a by-reference method parameter. +fn method_parameter_ref_target( + params: &[String], + parameter_is_by_ref: &[bool], + param_index: Option, + argument_number: usize, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !parameter_is_by_ref + .get(param_index) + .copied() + .unwrap_or(false) + { + return Ok(None); + } + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = params + .get(param_index) + .map(String::as_str) + .unwrap_or("arg"); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + argument_number + ))?; + Ok(None) + } + } +} + +/// Returns the by-reference flags that should be installed into the callee scope. +pub(in crate::interpreter) fn method_scope_parameter_ref_flags( + parameter_is_by_ref: &[bool], + bound_args: &[BoundMethodArg], + by_ref_mode: EvalByRefBindingMode<'_>, +) -> Vec { + if matches!(by_ref_mode, EvalByRefBindingMode::RequireTarget) { + return parameter_is_by_ref.to_vec(); + } + parameter_is_by_ref + .iter() + .enumerate() + .map(|(position, is_by_ref)| { + *is_by_ref + && bound_args.get(position).is_some_and(|arg| { + arg.ref_target.is_some() || !arg.variadic_ref_targets.is_empty() + }) + }) + .collect() +} + +/// Applies a variadic parameter type to one captured argument value. +fn eval_variadic_method_parameter_value( + parameter_types: &[Option], + variadic_index: Option, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(param_type) = + variadic_index.and_then(|index| parameter_types.get(index).and_then(Option::as_ref)) + else { + return Ok(value); + }; + eval_method_parameter_value(param_type, value, context, values) +} + +/// Returns the matching non-variadic parameter index for one PHP named argument. +fn regular_method_param_index( + params: &[String], + variadic_index: Option, + name: &str, +) -> Option { + params + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + +/// Appends one value into the method variadic array. +fn bind_dynamic_variadic_arg( + bound_args: &mut [Option], + variadic_index: Option, + key: RuntimeCellHandle, + value: RuntimeCellHandle, + ref_target: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; + let bound = bound_args[index].as_mut().ok_or(EvalStatus::RuntimeFatal)?; + bound.value = values.array_set(bound.value, key, value)?; + if let Some(ref_target) = ref_target { + bound.variadic_ref_targets.push((key, ref_target)); + } + Ok(()) +} + +/// Applies one eval method parameter type to a bound runtime value. +pub(in crate::interpreter) fn eval_method_parameter_value( + param_type: &EvalParameterType, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_method_parameter_type_accepts_exact(param_type, value, context, values)? { + return Ok(value); + } + if param_type.is_intersection() { + return Err(EvalStatus::RuntimeFatal); + } + for variant in param_type.variants() { + if let Some(coerced) = + eval_method_parameter_scalar_coercion(variant, value, context, values)? + { + return Ok(coerced); + } + } + Err(EvalStatus::RuntimeFatal) +} + +/// Returns whether a value satisfies one eval parameter type without scalar coercion. +fn eval_method_parameter_type_accepts_exact( + param_type: &EvalParameterType, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + if tag == EVAL_TAG_NULL && param_type.allows_null() { + return Ok(true); + } + if param_type.is_intersection() { + for variant in param_type.variants() { + if !eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { + return Ok(false); + } + } + return Ok(true); + } + for variant in param_type.variants() { + if eval_method_parameter_variant_accepts_exact(variant, value, tag, context, values)? { + return Ok(true); + } + } + Ok(false) +} + +/// Returns whether a value exactly satisfies one non-null eval parameter type atom. +fn eval_method_parameter_variant_accepts_exact( + variant: &EvalParameterTypeVariant, + value: RuntimeCellHandle, + tag: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match variant { + EvalParameterTypeVariant::Array => Ok(matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC)), + EvalParameterTypeVariant::Bool => Ok(tag == EVAL_TAG_BOOL), + EvalParameterTypeVariant::Callable => Ok(matches!( + tag, + EVAL_TAG_STRING | EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT + )), + EvalParameterTypeVariant::Class(class_name) => { + eval_method_parameter_class_accepts(value, tag, class_name, context, values) + } + EvalParameterTypeVariant::Float => Ok(tag == EVAL_TAG_FLOAT), + EvalParameterTypeVariant::Int => Ok(tag == EVAL_TAG_INT), + EvalParameterTypeVariant::Iterable => { + if matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Ok(true); + } + if eval_method_parameter_class_accepts(value, tag, "Traversable", context, values)? { + return Ok(true); + } + eval_method_parameter_class_accepts(value, tag, "Iterator", context, values) + } + EvalParameterTypeVariant::Mixed => Ok(true), + EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void => Ok(false), + EvalParameterTypeVariant::Object => Ok(tag == EVAL_TAG_OBJECT), + EvalParameterTypeVariant::String => Ok(tag == EVAL_TAG_STRING), + } +} + +/// Returns whether an object value satisfies one class/interface parameter target. +fn eval_method_parameter_class_accepts( + value: RuntimeCellHandle, + tag: u64, + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if tag != EVAL_TAG_OBJECT { + return Ok(false); + } + let target = eval_method_parameter_runtime_class_name(class_name, context)?; + let identity = values.object_identity(value)?; + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(context.class_is_a(class.name(), &target, false)); + } + values.object_is_a(value, &target, false) +} + +/// Resolves late-bound class keywords inside eval method parameter type checks. +fn eval_method_parameter_runtime_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "static" => context + .current_class_scope() + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "parent" => { + let current = context + .current_class_scope() + .ok_or(EvalStatus::RuntimeFatal)?; + context + .class(current) + .and_then(EvalClass::parent) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal) + } + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Applies PHP weak-mode scalar coercion for supported scalar parameter types. +pub(in crate::interpreter) fn eval_method_parameter_scalar_coercion( + variant: &EvalParameterTypeVariant, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let tag = values.type_tag(value)?; + match variant { + EvalParameterTypeVariant::Bool if eval_method_scalar_coercible_tag(tag) => { + values.cast_bool(value).map(Some) + } + EvalParameterTypeVariant::Float + if eval_method_numeric_coercible_value(value, tag, values)? => + { + values.cast_float(value).map(Some) + } + EvalParameterTypeVariant::Int + if eval_method_numeric_coercible_value(value, tag, values)? => + { + values.cast_int(value).map(Some) + } + EvalParameterTypeVariant::String if eval_method_scalar_coercible_tag(tag) => { + values.cast_string(value).map(Some) + } + EvalParameterTypeVariant::String if tag == EVAL_TAG_OBJECT => { + let coerced = eval_dynamic_object_string_context_value(value, context, values)?; + if values.type_tag(coerced)? == EVAL_TAG_STRING { + Ok(Some(coerced)) + } else { + Ok(None) + } + } + _ => Ok(None), + } +} + +/// Converts objects in string contexts through the applicable `__toString()` dispatch path. +pub(in crate::interpreter) fn eval_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Ok(value); + } + eval_dynamic_object_string_context_value(value, context, values) +} + +/// Invokes `__toString()` for eval-declared objects or throws PHP's missing-hook error. +fn eval_dynamic_object_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(value)?; + let Some(class) = context.dynamic_object_class(identity) else { + return eval_runtime_object_string_context_value(value, context, values); + }; + let called_class_name = class.name().to_string(); + let Some((declaring_class, method)) = context.class_method(&called_class_name, "__toString") + else { + return eval_throw_object_to_string_error(&called_class_name, context, values); + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() + || method.is_abstract() + || !method.params().is_empty() + { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + value, + Vec::new(), + context, + values, + )?; + eval_tostring_result_to_string(result, values) +} + +/// Invokes the interpreter method dispatcher for AOT/native objects in string contexts. +fn eval_runtime_object_string_context_value( + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(value)?; + let class_name = runtime_object_class_name(value, values)?; + if !eval_runtime_object_has_interpreter_tostring(identity, &class_name, context) + && eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__toString", + context, + values, + )? + .is_none() + { + return eval_throw_object_to_string_error(&class_name, context, values); + } + let result = eval_method_call_result_with_evaluated_args( + value, + "__toString", + Vec::new(), + context, + values, + )?; + eval_tostring_result_to_string(result, values) +} + +/// Returns whether eval owns a synthetic `__toString()` implementation for the runtime object. +fn eval_runtime_object_has_interpreter_tostring( + identity: u64, + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context.eval_reflection_class_name(identity).is_some() + || context.eval_reflection_function_name(identity).is_some() + || context.eval_reflection_method(identity).is_some() + || context.eval_reflection_property(identity).is_some() + || context.eval_reflection_class_constant(identity).is_some() + || eval_runtime_class_name_has_reflection_tostring(class_name) +} + +/// Returns whether a synthetic reflection class has `__toString()` handled by eval dispatch. +fn eval_runtime_class_name_has_reflection_tostring(class_name: &str) -> bool { + let class_name = class_name.trim_start_matches('\\'); + [ + "ReflectionParameter", + "ReflectionNamedType", + "ReflectionUnionType", + "ReflectionIntersectionType", + ] + .iter() + .any(|reflection_class| class_name.eq_ignore_ascii_case(reflection_class)) +} + +/// Throws PHP's catchable object-to-string conversion error. +fn eval_throw_object_to_string_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Object of class {} could not be converted to string", + class_name.trim_start_matches('\\') + ), + context, + values, + ) +} + +/// Normalizes one `__toString()` result to a boxed string cell. +fn eval_tostring_result_to_string( + result: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(result)? == EVAL_TAG_STRING { + return Ok(result); + } + let coerced = values.cast_string(result)?; + values.release(result)?; + Ok(coerced) +} + +/// Returns whether a runtime tag can be weakly coerced to string/bool parameters. +fn eval_method_scalar_coercible_tag(tag: u64) -> bool { + matches!( + tag, + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_STRING | EVAL_TAG_BOOL + ) +} + +/// Returns whether a runtime value can be weakly coerced to a numeric parameter. +fn eval_method_numeric_coercible_value( + value: RuntimeCellHandle, + tag: u64, + values: &mut impl RuntimeValueOps, +) -> Result { + match tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL => Ok(true), + EVAL_TAG_STRING => Ok(eval_is_numeric_string(&values.string_bytes(value)?)), + _ => Ok(false), + } +} + +/// Materializes a supported eval method parameter default expression. +pub(in crate::interpreter) fn eval_method_parameter_default( + default: &EvalExpr, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_method_default_expr_is_supported(default) { + return Err(EvalStatus::UnsupportedConstruct); + } + let mut default_scope = ElephcEvalScope::new(); + eval_expr(default, context, &mut default_scope, values) +} + +/// Returns whether an EvalIR expression can be safely evaluated as a method default. +fn eval_method_default_expr_is_supported(expr: &EvalExpr) -> bool { + match expr { + EvalExpr::Array(elements) => elements + .iter() + .all(eval_method_default_array_element_is_supported), + EvalExpr::Const(_) | EvalExpr::Magic(_) => true, + EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, + EvalExpr::ClassConstantFetch { class_name, .. } + | EvalExpr::ClassNameFetch { class_name } => { + eval_method_default_class_receiver_is_supported(class_name) + } + EvalExpr::NewObject { class_name, args } => { + eval_method_default_class_receiver_is_supported(class_name) + && args.iter().all(eval_method_default_call_arg_is_supported) + } + EvalExpr::NewAnonymousClass { .. } => false, + EvalExpr::NullCoalesce { value, default } => { + eval_method_default_expr_is_supported(value) + && eval_method_default_expr_is_supported(default) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_method_default_expr_is_supported(condition) + && then_branch + .as_deref() + .is_none_or(eval_method_default_expr_is_supported) + && eval_method_default_expr_is_supported(else_branch) + } + EvalExpr::Cast { expr, .. } => eval_method_default_expr_is_supported(expr), + EvalExpr::Unary { expr, .. } => eval_method_default_expr_is_supported(expr), + EvalExpr::Binary { left, right, .. } => { + eval_method_default_expr_is_supported(left) + && eval_method_default_expr_is_supported(right) + } + _ => false, + } +} + +/// Returns whether one object-construction argument is safe inside a method default. +fn eval_method_default_call_arg_is_supported(arg: &EvalCallArg) -> bool { + !arg.is_spread() && eval_method_default_expr_is_supported(arg.value()) +} + +/// Returns whether one array default element contains only supported constant expressions. +fn eval_method_default_array_element_is_supported(element: &EvalArrayElement) -> bool { + match element { + EvalArrayElement::Value(value) => eval_method_default_expr_is_supported(value), + EvalArrayElement::Reference(_) => false, + EvalArrayElement::KeyValue { key, value } => { + eval_method_default_expr_is_supported(key) + && eval_method_default_expr_is_supported(value) + } + EvalArrayElement::KeyReference { .. } => false, + } +} + +/// Returns whether a class-like receiver is legal in a compile-time method default. +fn eval_method_default_class_receiver_is_supported(class_name: &str) -> bool { + !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("static") +} + +/// Binds one positional dynamic-call value to the next declared parameter slot. +pub(in crate::interpreter) fn bind_dynamic_positional_arg( + bound_args: &mut [Option], + next_positional: &mut usize, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + if *next_positional >= bound_args.len() || bound_args[*next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[*next_positional] = Some(value); + *next_positional += 1; + Ok(()) +} + +/// Binds one named dynamic-call value to the matching declared parameter slot. +pub(in crate::interpreter) fn bind_dynamic_named_arg( + params: &[String], + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + let Some(param_index) = params.iter().position(|param| param == name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[param_index] = Some(value); + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/dynamic_functions/native_execution.rs b/crates/elephc-magician/src/interpreter/dynamic_functions/native_execution.rs new file mode 100644 index 0000000000..0bcc35274f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/dynamic_functions/native_execution.rs @@ -0,0 +1,253 @@ +//! Purpose: +//! Executes registered native functions and balances temporary argument arrays. +//! +//! Called from: +//! - Dynamic function dispatch after native signature binding. +//! +//! Key details: +//! - By-reference writeback and temporary runtime-cell ownership are handled together. + +use super::*; + +/// Evaluates a registered AOT function through its descriptor-compatible invoker. +pub(in crate::interpreter) fn eval_native_function( + function: NativeFunction, + args: &[EvalCallArg], + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let evaluated_args = + eval_native_function_call_args(&function, args, context, caller_scope, values)?; + eval_native_function_with_values(function, evaluated_args, context, values) +} + +/// Invokes a registered AOT function after its arguments have been bound and staged. +pub(in crate::interpreter) fn eval_native_function_with_values( + function: NativeFunction, + bound_args: BoundNativeFunctionArgs, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !function.bridge_supported() { + return Err(EvalStatus::RuntimeFatal); + } + let variadic_index = native_function_variadic_index(&function); + if variadic_index.is_none() && bound_args.values.len() != function.param_count() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(variadic_index) = variadic_index { + if bound_args.values.len() < function.required_param_count().min(variadic_index) { + return Err(EvalStatus::RuntimeFatal); + } + } + let arg_array = match build_native_function_arg_array(&bound_args, values) { + Ok(arg_array) => arg_array, + Err(status) => { + cleanup_native_function_ref_args(&bound_args, values)?; + return Err(status); + } + }; + let result = unsafe { function.call(arg_array) }; + if let Err(status) = values.release(arg_array) { + cleanup_native_function_ref_args(&bound_args, values)?; + return Err(status); + } + let result = values.native_call_result(result); + let writeback = write_back_native_function_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => { + eval_declared_native_return_value(function.return_type(), None, None, result, context, values) + } + } +} + +/// Builds the positional runtime array passed to descriptor-compatible native invokers. +fn build_native_function_arg_array( + bound_args: &BoundNativeFunctionArgs, + values: &mut impl RuntimeValueOps, +) -> Result { + let arg_array = values.array_new(bound_args.values.len())?; + for (index, value) in bound_args.values.iter().copied().enumerate() { + let index = match values.int(index as i64) { + Ok(index) => index, + Err(status) => { + values.release(arg_array)?; + return Err(status); + } + }; + if let Err(status) = values.array_set(arg_array, index, value) { + values.release(arg_array)?; + return Err(status); + } + } + Ok(arg_array) +} + +/// Releases retained raw native-function by-reference staging slots without writeback. +fn cleanup_native_function_ref_args( + bound_args: &BoundNativeFunctionArgs, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for ref_slot in &bound_args.ref_slots { + match ref_slot { + BoundNativeFunctionRefSlot::RawString { original, slot, .. } => { + let words = **slot; + values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } + } + BoundNativeFunctionRefSlot::OwnedRawWord { original, slot, .. } => { + let word = **slot; + values.release_raw_heap_word(word)?; + if word != *original { + values.release_raw_heap_word(*original)?; + } + } + BoundNativeFunctionRefSlot::Mixed { slot, .. } => { + values.release(**slot)?; + } + BoundNativeFunctionRefSlot::RawWord { .. } => {} + } + } + Ok(()) +} + +/// Writes changed staged native-function by-reference slots back to eval caller targets. +fn write_back_native_function_ref_args( + bound_args: &BoundNativeFunctionArgs, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for ref_slot in &bound_args.ref_slots { + match ref_slot { + BoundNativeFunctionRefSlot::Mixed { + original, + slot, + target, + } => { + let value = **slot; + if value == *original { + values.release(value)?; + continue; + } + let Some(target) = target else { + values.release(value)?; + continue; + }; + let current = match eval_reference_target_value(target, context, values) { + Ok(current) => current, + Err(status) => { + values.release(value)?; + return Err(status); + } + }; + if current == value { + values.release(value)?; + continue; + } + if let Err(status) = eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + ) { + values.release(value)?; + return Err(status); + } + } + BoundNativeFunctionRefSlot::RawWord { + tag, + original, + slot, + target, + } => { + let word = **slot; + if word == *original { + continue; + } + let Some(target) = target else { + continue; + }; + let value = values.raw_word_value(*tag, word)?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + BoundNativeFunctionRefSlot::RawString { + original, + slot, + target, + } => { + let words = **slot; + if target.is_none() { + values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } + continue; + } + let Some(target) = target else { + return Err(EvalStatus::RuntimeFatal); + }; + if words == *original { + values.release_raw_string_words(words[0], words[1])?; + continue; + } + let value = values.raw_string_value(words[0], words[1]); + values.release_raw_string_words(words[0], words[1])?; + if words[0] != original[0] { + values.release_raw_string_words(original[0], original[1])?; + } + let value = value?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + BoundNativeFunctionRefSlot::OwnedRawWord { + original, + slot, + target, + } => { + let word = **slot; + if target.is_none() { + values.release_raw_heap_word(word)?; + if word != *original { + values.release_raw_heap_word(*original)?; + } + continue; + } + let Some(target) = target else { + return Err(EvalStatus::RuntimeFatal); + }; + if word == *original { + values.release_raw_heap_word(word)?; + continue; + } + let value = values.raw_heap_word_value(word); + values.release_raw_heap_word(word)?; + values.release_raw_heap_word(*original)?; + let value = value?; + eval_write_direct_ref_target( + target, + value, + context, + values, + Some(ScopeCellOwnership::Owned), + )?; + } + } + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/reflection.rs b/crates/elephc-magician/src/interpreter/reflection.rs index 5536687417..a82462c6f3 100644 --- a/crates/elephc-magician/src/interpreter/reflection.rs +++ b/crates/elephc-magician/src/interpreter/reflection.rs @@ -1,15 +1,36 @@ //! Purpose: -//! Handles eval-aware construction of builtin reflection owner objects. -//! These objects need private metadata slots populated from eval-declared class -//! metadata, which ordinary public property writes cannot express. +//! Coordinates eval-aware Reflection dispatch and shared metadata shapes. +//! Owner-specific APIs, construction, lookup, formatting, and runtime access +//! live in focused child modules. //! //! Called from: //! - `crate::interpreter::expressions::eval_expr()` for `new Reflection*`. +//! - `crate::interpreter::statements` for Reflection method dispatch. //! //! Key details: -//! - Eval-declared classes/interfaces/traits/enums are materialized from dynamic metadata. +//! - Shared metadata types stay here so every Reflection owner uses one contract. //! - Generated/AOT targets use focused runtime hooks for supported point lookups. +mod callable_api; +mod class_api; +mod class_construction; +mod class_lookup; +mod class_member_api; +mod constant_construction; +mod flags; +mod formatting; +mod function_construction; +mod function_metadata; +mod invocation; +mod member_api; +mod member_construction; +mod member_metadata; +mod owner_materialization; +mod parameter_construction; +mod parameter_metadata; +mod property_access; +mod property_helpers; + use super::*; use crate::context::{ NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, @@ -17,6 +38,26 @@ use crate::context::{ }; use crate::eval_ir::EvalSourceLocation; +pub(in crate::interpreter) use callable_api::*; +pub(in crate::interpreter) use class_api::*; +pub(in crate::interpreter) use class_construction::*; +use class_lookup::*; +pub(in crate::interpreter) use class_member_api::*; +use constant_construction::*; +use flags::*; +use formatting::*; +use function_construction::*; +use function_metadata::*; +use invocation::*; +pub(in crate::interpreter) use member_api::*; +pub(in crate::interpreter) use member_construction::*; +use member_metadata::*; +use owner_materialization::*; +use parameter_construction::*; +use parameter_metadata::*; +use property_access::*; +use property_helpers::*; + const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; const EVAL_REFLECTION_CLASS_FLAG_INTERFACE: u64 = 4; @@ -306,10312 +347,3 @@ pub(in crate::interpreter) fn eval_reflection_owner_new_object( None => Ok(None), } } - -/// Handles eval-backed `ReflectionClass::implementsInterface()` calls. -pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("implementsInterface") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("interface")], evaluated_args)?; - let interface_name = eval_reflection_string_arg(args[0], values)?; - if !eval_reflection_interface_exists(&interface_name, context, values)? { - if eval_reflection_non_interface_exists(&interface_name, context, values)? { - return eval_throw_reflection_exception( - &format!("{} is not an interface", interface_name), - context, - values, - ); - } - return eval_throw_reflection_exception( - &format!("Interface \"{}\" does not exist", interface_name), - context, - values, - ); - } - let result = if eval_reflection_class_like_exists(&reflected_name, context) { - eval_reflection_class_implements_interface_name( - &reflected_name, - &interface_name, - context, - values, - )? - } else if eval_runtime_interface_exists(&reflected_name, values)? { - eval_reflection_same_class_like_name(&reflected_name, &interface_name) - } else { - let reflected_class = values.string(&reflected_name)?; - let result = values.object_is_a(reflected_class, &interface_name, false); - values.release(reflected_class)?; - result? - }; - values.bool_value(result).map(Some) -} - -/// Handles eval-backed `ReflectionClass::isSubclassOf()` calls. -pub(in crate::interpreter) fn eval_reflection_class_is_subclass_of_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("isSubclassOf") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("class")], evaluated_args)?; - let target_name = eval_reflection_string_arg(args[0], values)?; - if !eval_reflection_class_like_exists(&target_name, context) - && !values.class_exists(&target_name)? - && !eval_runtime_interface_exists(&target_name, values)? - && !values.trait_exists(&target_name)? - && !values.enum_exists(&target_name)? - { - return eval_throw_reflection_exception( - &format!("Class \"{}\" does not exist", target_name), - context, - values, - ); - } - let result = if eval_reflection_class_like_exists(&reflected_name, context) { - eval_reflection_class_is_subclass_of_name( - &reflected_name, - &target_name, - context, - values, - )? - } else { - let reflected_class = values.string(&reflected_name)?; - let result = values.object_is_a(reflected_class, &target_name, true)?; - values.release(reflected_class)?; - result - }; - values.bool_value(result).map(Some) -} - -/// Handles eval-backed `ReflectionClass::isInstance()` calls. -pub(in crate::interpreter) fn eval_reflection_class_is_instance_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("isInstance") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; - let object = args[0]; - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let result = dynamic_object_is_a(object, &reflected_name, false, context, values)? - .map_or_else(|| values.object_is_a(object, &reflected_name, false), Ok)?; - values.bool_value(result).map(Some) -} - -/// Handles eval-backed `ReflectionClass` source-location metadata calls. -pub(in crate::interpreter) fn eval_reflection_class_source_location_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let method_key = method_name.to_ascii_lowercase(); - if !matches!( - method_key.as_str(), - "getfilename" | "getstartline" | "getendline" - ) { - return Ok(None); - } - let Some(reflected_name) = context.eval_reflection_class_name(identity) else { - return Ok(None); - }; - let (source_file, source_location) = - if let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) { - (None, metadata.source_location) - } else { - eval_reflection_aot_class_source_metadata(reflected_name, values)? - }; - eval_reflection_source_location_result( - method_key.as_str(), - source_file.as_deref(), - source_location, - evaluated_args, - context, - values, - ) -} - -/// Returns AOT source-file and line metadata for a generated ReflectionClass. -fn eval_reflection_aot_class_source_metadata( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result<(Option, Option), EvalStatus> { - let Some(flags) = values.reflection_class_flags(class_name.trim_start_matches('\\'))? else { - return Ok((None, None)); - }; - let Some(source_location) = eval_reflection_aot_class_source_location_from_flags(flags) else { - return Ok((None, None)); - }; - let Some(source_file) = values.reflection_source_file()? else { - return Ok((None, None)); - }; - Ok((Some(source_file), Some(source_location))) -} - -/// Decodes AOT ReflectionClass source lines packed into high flag bits. -fn eval_reflection_aot_class_source_location_from_flags(flags: u64) -> Option { - let start_line = ((flags >> EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT) - & EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK) as i64; - let end_line = ((flags >> EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT) - & EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK) as i64; - (start_line > 0 && end_line >= start_line) - .then(|| EvalSourceLocation::new(start_line, end_line)) -} - -/// Handles eval-backed `ReflectionClass` scalar metadata methods. -pub(in crate::interpreter) fn eval_reflection_class_basic_metadata_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { - return Ok(None); - }; - let method_key = method_name.to_ascii_lowercase(); - match method_key.as_str() { - "getname" => { - eval_reflection_bind_no_args(evaluated_args)?; - values.string(&metadata.resolved_name).map(Some) - } - "getshortname" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .string(&eval_reflection_short_name(&metadata.resolved_name)) - .map(Some) - } - "getnamespacename" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .string(&eval_reflection_namespace_name(&metadata.resolved_name)) - .map(Some) - } - "innamespace" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .bool_value(!eval_reflection_namespace_name(&metadata.resolved_name).is_empty()) - .map(Some) - } - "getinterfacenames" => { - eval_reflection_bind_no_args(evaluated_args)?; - let interface_names = - eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; - eval_reflection_string_array_result(&interface_names, values).map(Some) - } - "gettraitnames" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_string_array_result(&metadata.trait_names, values).map(Some) - } - "getparentclass" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_related_class_result( - EVAL_REFLECTION_OWNER_CLASS, - metadata.parent_class_name.as_deref(), - true, - context, - values, - ) - .map(Some) - } - "getconstructor" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_constructor_object_result( - EVAL_REFLECTION_OWNER_CLASS, - &metadata.resolved_name, - true, - context, - values, - ) - .map(Some) - } - "getmodifiers" => { - eval_reflection_bind_no_args(evaluated_args)?; - values.int(metadata.modifiers as i64).map(Some) - } - "isfinal" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_FINAL, - evaluated_args, - values, - ), - "isabstract" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_ABSTRACT, - evaluated_args, - values, - ), - "isinterface" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_INTERFACE, - evaluated_args, - values, - ), - "istrait" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_TRAIT, - evaluated_args, - values, - ), - "isenum" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_ENUM, - evaluated_args, - values, - ), - "isreadonly" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_READONLY, - evaluated_args, - values, - ), - "isanonymous" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS, - evaluated_args, - values, - ), - "isinstantiable" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE, - evaluated_args, - values, - ), - "iscloneable" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_CLONEABLE, - evaluated_args, - values, - ), - "isiterable" | "isiterateable" => { - let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; - eval_reflection_class_flag_result( - flags, - EVAL_REFLECTION_CLASS_FLAG_ITERABLE, - evaluated_args, - values, - ) - } - "isinternal" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_INTERNAL, - evaluated_args, - values, - ), - "isuserdefined" => eval_reflection_class_flag_result( - metadata.flags, - EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, - evaluated_args, - values, - ), - _ => Ok(None), - } -} - -/// Handles `ReflectionClass::__toString()` calls for eval-visible class metadata. -pub(in crate::interpreter) fn eval_reflection_class_to_string_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("__toString") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - eval_reflection_bind_no_args(evaluated_args)?; - let rendered = eval_reflection_class_to_string(&reflected_name, context, values)?; - values.string(&rendered).map(Some) -} - -/// Returns one boolean ReflectionClass flag after validating a no-arg call. -fn eval_reflection_class_flag_result( - flags: u64, - flag: u64, - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - eval_reflection_bind_no_args(evaluated_args)?; - values.bool_value(flags & flag != 0).map(Some) -} - -/// Handles eval-backed `ReflectionClass::hasMethod()` calls. -pub(in crate::interpreter) fn eval_reflection_class_has_method_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("hasMethod") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; - let requested_name = eval_reflection_string_arg(args[0], values)?; - let exists = - if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { - metadata - .method_names - .iter() - .any(|name| name.eq_ignore_ascii_case(&requested_name)) - } else { - eval_reflection_aot_method_metadata_if_exists(&reflected_name, &requested_name, values)? - .is_some() - }; - values.bool_value(exists).map(Some) -} - -/// Handles eval-backed `ReflectionClass::hasProperty()` and inherited `ReflectionObject` calls. -pub(in crate::interpreter) fn eval_reflection_class_has_property_result( - object: RuntimeCellHandle, - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("hasProperty") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; - let property_name = eval_reflection_string_arg(args[0], values)?; - let mut exists = - if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { - metadata - .property_names - .iter() - .any(|name| name == &property_name) - } else { - eval_reflection_aot_property_metadata_if_exists( - &reflected_name, - &property_name, - context, - values, - )? - .is_some() - || eval_reflection_native_interface_property_requirement( - &reflected_name, - &property_name, - context, - ) - .is_some() - }; - if !exists { - if let Some(dynamic_object) = - eval_reflection_object_reflected_object(object, context, values)? - { - let dynamic_exists = eval_reflection_object_dynamic_property_exists( - dynamic_object, - &property_name, - values, - ); - values.release(dynamic_object)?; - exists = dynamic_exists?; - } - } - values.bool_value(exists).map(Some) -} - -/// Handles eval-backed `ReflectionClass::hasConstant()` calls. -pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("hasConstant") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; - let constant_name = eval_reflection_string_arg(args[0], values)?; - let constant_names = eval_reflection_constant_names(&reflected_name, context, values)?; - values - .bool_value(constant_names.iter().any(|name| name == &constant_name)) - .map(Some) -} - -/// Handles eval-backed `ReflectionEnum` methods that are not inherited from `ReflectionClass`. -pub(in crate::interpreter) fn eval_reflection_enum_methods_result( - object: RuntimeCellHandle, - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !eval_reflection_object_has_class(object, "ReflectionEnum", values)? { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let Some(enum_name) = context.resolve_enum_name(&reflected_name) else { - return Ok(None); - }; - match method_name.to_ascii_lowercase().as_str() { - "hascase" => { - let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; - let requested_name = eval_reflection_string_arg(args[0], values)?; - let exists = context - .enum_decl(&enum_name) - .and_then(|enum_decl| enum_decl.case(&requested_name)) - .is_some(); - values.bool_value(exists).map(Some) - } - "getcase" => { - let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; - let requested_name = eval_reflection_string_arg(args[0], values)?; - let owner_kind = eval_reflection_enum_case_owner_kind(&enum_name, context)?; - let result = eval_reflection_enum_case_object_result( - owner_kind, - &enum_name, - &requested_name, - context, - values, - )?; - Ok(Some(result)) - } - "getcases" => { - eval_reflection_bind_no_args(evaluated_args)?; - let (case_names, owner_kind) = { - let enum_decl = context.enum_decl(&enum_name).ok_or(EvalStatus::RuntimeFatal)?; - let case_names = enum_decl - .cases() - .iter() - .map(|case| case.name().to_string()) - .collect::>(); - (case_names, eval_reflection_enum_case_owner_kind(&enum_name, context)?) - }; - let mut result = values.array_new(case_names.len())?; - for (index, case_name) in case_names.iter().enumerate() { - let case_object = eval_reflection_enum_case_object_result( - owner_kind, &enum_name, case_name, context, values, - )?; - let key = values.int(index as i64)?; - result = values.array_set(result, key, case_object)?; - } - Ok(Some(result)) - } - "isbacked" => { - eval_reflection_bind_no_args(evaluated_args)?; - let is_backed = context - .enum_decl(&enum_name) - .and_then(EvalEnum::backing_type) - .is_some(); - values.bool_value(is_backed).map(Some) - } - "getbackingtype" => { - eval_reflection_bind_no_args(evaluated_args)?; - let backing_type = context - .enum_decl(&enum_name) - .and_then(EvalEnum::backing_type); - let Some(backing_type) = backing_type else { - return values.null().map(Some); - }; - let metadata = eval_reflection_enum_backing_type_metadata(backing_type); - eval_reflection_type_object_result(&metadata, values).map(Some) - } - _ => Ok(None), - } -} - -/// Handles eval-backed `ReflectionClass::getInterfaces()` and `getTraits()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let relation_kind = if method_name.eq_ignore_ascii_case("getInterfaces") { - "interfaces" - } else if method_name.eq_ignore_ascii_case("getTraits") { - "traits" - } else { - return Ok(None); - }; - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let names = - if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { - if relation_kind == "interfaces" { - eval_reflection_eval_metadata_interface_names(&metadata, context, values)? - } else { - metadata.trait_names - } - } else if relation_kind == "interfaces" { - eval_reflection_aot_class_interface_names(&reflected_name, values)? - } else { - eval_reflection_aot_class_trait_names(&reflected_name, values)? - }; - eval_reflection_class_object_map_result(&names, context, values).map(Some) -} - -/// Handles eval-backed `ReflectionClass::getTraitAliases()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_trait_aliases_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getTraitAliases") { - return Ok(None); - } - eval_reflection_bind_no_args(evaluated_args)?; - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let aliases = if context.trait_decl(&reflected_name).is_some() { - context.trait_trait_aliases(&reflected_name) - } else if eval_reflection_class_like_exists(&reflected_name, context) { - context.class_trait_aliases(&reflected_name) - } else { - eval_reflection_aot_class_trait_aliases(&reflected_name, values)? - }; - eval_reflection_string_assoc_result(aliases, values).map(Some) -} - -/// Handles eval-backed `ReflectionClass::getConstant()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_constant_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getConstant") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; - let constant_name = eval_reflection_string_arg(args[0], values)?; - if let Some(value) = - eval_reflection_constant_value(&reflected_name, &constant_name, context, values)? - { - return Ok(Some(value)); - } - values.bool_value(false).map(Some) -} - -/// Handles eval-backed `ReflectionClass::getConstants()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getConstants") { - return Ok(None); - } - let filter = eval_reflection_member_filter(evaluated_args, values)?; - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let names = eval_reflection_constant_names(&reflected_name, context, values)?; - let mut result = values.assoc_new(names.len())?; - for name in names { - if !eval_reflection_constant_matches_filter(&reflected_name, &name, filter, context, values)? - { - continue; - } - let Some(value) = eval_reflection_constant_value(&reflected_name, &name, context, values)? - else { - continue; - }; - let key = values.string(&name)?; - result = values.array_set(result, key, value)?; - } - Ok(Some(result)) -} - -/// Handles eval-backed `ReflectionClass::getDefaultProperties()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_default_properties_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getDefaultProperties") { - return Ok(None); - } - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let property_names = eval_reflection_default_property_names(&reflected_name, context, values)?; - let mut result = values.assoc_new(property_names.len())?; - for name in property_names { - let Some(member) = - eval_reflection_default_property_metadata(&reflected_name, &name, context, values)? - else { - continue; - }; - let Some(default) = member.default_value.as_ref() else { - continue; - }; - let key = values.string(&name)?; - let value = eval_reflection_member_default_value(&member, default, context, values)?; - result = values.array_set(result, key, value)?; - } - Ok(Some(result)) -} - -/// Handles eval-backed `ReflectionClass::getStaticProperties()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_static_properties_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getStaticProperties") { - return Ok(None); - } - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let property_names = eval_reflection_static_property_names(&reflected_name, context, values)?; - let mut result = values.assoc_new(property_names.len())?; - for name in property_names { - let Some(value) = - eval_reflection_static_property_value(&reflected_name, &name, context, values)? - else { - continue; - }; - let key = values.string(&name)?; - result = values.array_set(result, key, value)?; - } - Ok(Some(result)) -} - -/// Handles eval-backed `ReflectionClass::getStaticPropertyValue()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_static_property_value_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getStaticPropertyValue") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let (property_name, default_value) = - eval_reflection_static_property_value_args(evaluated_args)?; - let property_name = eval_reflection_string_arg(property_name, values)?; - if let Some(value) = - eval_reflection_static_property_value(&reflected_name, &property_name, context, values)? - { - return Ok(Some(value)); - } - if let Some(default_value) = default_value { - return Ok(Some(default_value)); - } - eval_throw_reflection_exception( - &format!( - "Property {}::${} does not exist", - reflected_name, property_name - ), - context, - values, - ) -} - -/// Handles eval-backed `ReflectionClass::setStaticPropertyValue()` calls. -pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("setStaticPropertyValue") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args( - &[String::from("name"), String::from("value")], - evaluated_args, - )?; - let property_name = eval_reflection_string_arg(args[0], values)?; - let Some(member) = - eval_reflection_static_property_metadata(&reflected_name, &property_name, context, values)? - else { - return eval_reflection_static_property_missing_for_set( - &reflected_name, - &property_name, - context, - values, - ); - }; - if !member.is_static { - return eval_reflection_static_property_missing_for_set( - &reflected_name, - &property_name, - context, - values, - ); - } - if eval_reflection_class_like_exists(&reflected_name, context) { - let declaring_class = member - .declaring_class_name - .as_deref() - .ok_or(EvalStatus::RuntimeFatal)?; - if let Some(replaced) = - context.set_static_property(declaring_class, &property_name, args[1]) - { - values.release(replaced)?; - } - } else { - let declaring_class = member - .declaring_class_name - .as_deref() - .unwrap_or(reflected_name.as_str()); - let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { - values.static_property_set(&reflected_name, &property_name, args[1]) - })?; - if updated { - return values.null().map(Some); - } - return eval_reflection_static_property_missing_for_set( - &reflected_name, - &property_name, - context, - values, - ); - } - values.null().map(Some) -} - -/// Handles eval-backed `ReflectionMethod::invoke()` and `invokeArgs()` calls. -pub(in crate::interpreter) fn eval_reflection_method_invoke_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let is_invoke = method_name.eq_ignore_ascii_case("invoke"); - let is_invoke_args = method_name.eq_ignore_ascii_case("invokeArgs"); - if !is_invoke && !is_invoke_args { - return Ok(None); - } - let Some((declaring_class, reflected_method)) = context - .eval_reflection_method(identity) - .map(|(declaring_class, method)| (declaring_class.to_string(), method.to_string())) - else { - return Ok(None); - }; - let (object, method_args) = if is_invoke { - eval_reflection_method_invoke_args(evaluated_args)? - } else { - eval_reflection_method_invoke_args_array(evaluated_args, context, values)? - }; - eval_reflection_method_invoke_dispatch( - &declaring_class, - &reflected_method, - object, - method_args, - context, - values, - ) - .map(Some) -} - -/// Handles eval-backed `ReflectionFunction::invoke()` and `invokeArgs()` calls. -pub(in crate::interpreter) fn eval_reflection_function_invoke_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let is_invoke = method_name.eq_ignore_ascii_case("invoke"); - let is_invoke_args = method_name.eq_ignore_ascii_case("invokeArgs"); - if !is_invoke && !is_invoke_args { - return Ok(None); - } - let Some(function_name) = context - .eval_reflection_function_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let function_args = if is_invoke { - evaluated_args - .into_iter() - .map(eval_reflection_method_forwarded_value_arg) - .collect() - } else { - eval_reflection_function_invoke_args_array(evaluated_args, context, values)? - }; - eval_reflection_function_invoke_dispatch(&function_name, function_args, context, values) - .map(Some) -} - -/// Handles eval-backed ReflectionFunctionAbstract name/origin metadata calls. -pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { - return Ok(None); - }; - let method_key = method_name.to_ascii_lowercase(); - match method_key.as_str() { - "getshortname" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .string(&eval_reflection_function_method_short_name(&target)) - .map(Some) - } - "getnamespacename" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .string(&eval_reflection_function_method_namespace_name(&target)) - .map(Some) - } - "innamespace" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .bool_value(!eval_reflection_function_method_namespace_name(&target).is_empty()) - .map(Some) - } - "getfilename" | "getstartline" | "getendline" => { - let (source_file, source_location) = - eval_reflection_function_method_source_location(&target); - eval_reflection_source_location_result( - method_key.as_str(), - source_file, - source_location, - evaluated_args, - context, - values, - ) - } - "isinternal" | "returnsreference" | "isgenerator" | "hastentativereturntype" => { - eval_reflection_false_metadata_result(evaluated_args, values) - } - "isclosure" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .bool_value(eval_reflection_function_method_is_closure(&target)) - .map(Some) - } - "isdeprecated" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .bool_value(eval_reflection_function_method_is_deprecated(&target)) - .map(Some) - } - "isanonymous" => match target { - EvalReflectionFunctionMethodTarget::Function { .. } => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .bool_value(eval_reflection_function_method_is_closure(&target)) - .map(Some) - } - EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), - }, - "hasreturntype" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .bool_value(eval_reflection_function_method_return_type(&target).is_some()) - .map(Some) - } - "isuserdefined" => { - eval_reflection_bind_no_args(evaluated_args)?; - values.bool_value(true).map(Some) - } - "isvariadic" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .bool_value(eval_reflection_function_method_is_variadic(&target)) - .map(Some) - } - "isstatic" => { - eval_reflection_bind_no_args(evaluated_args)?; - values - .bool_value(eval_reflection_function_method_is_static(&target)) - .map(Some) - } - "isdisabled" => match target { - EvalReflectionFunctionMethodTarget::Function { .. } => { - eval_reflection_false_metadata_result(evaluated_args, values) - } - EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), - }, - "getreturntype" => { - eval_reflection_bind_no_args(evaluated_args)?; - match eval_reflection_function_method_return_type(&target) { - Some(type_metadata) => { - eval_reflection_type_object_result(type_metadata, values).map(Some) - } - None => values.null().map(Some), - } - } - "gettentativereturntype" => { - eval_reflection_bind_no_args(evaluated_args)?; - values.null().map(Some) - } - "getstaticvariables" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_function_method_static_variables_result(&target, context, values) - .map(Some) - } - "getclosureusedvariables" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_function_closure_used_variables_result(&target, values).map(Some) - } - "getclosurethis" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_function_closure_this_result(&target, values).map(Some) - } - "getclosurescopeclass" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_function_closure_scope_class_result(&target, context, values) - .map(Some) - } - "getclosurecalledclass" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_function_closure_called_class_result(&target, context, values) - .map(Some) - } - _ => Ok(None), - } -} - -/// Handles eval-backed `ReflectionFunction::__toString()` and `ReflectionMethod::__toString()`. -pub(in crate::interpreter) fn eval_reflection_function_method_to_string_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("__toString") { - return Ok(None); - } - let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { - return Ok(None); - }; - eval_reflection_bind_no_args(evaluated_args)?; - let rendered = eval_reflection_function_method_to_string(&target); - values.string(&rendered).map(Some) -} - -/// Handles eval-backed `ReflectionParameter::isArray()` and `isCallable()` calls. -pub(in crate::interpreter) fn eval_reflection_parameter_legacy_type_predicate_result( - object: RuntimeCellHandle, - method_name: &str, - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(expected_type) = eval_reflection_parameter_legacy_type_name(method_name) else { - return Ok(None); - }; - if !eval_reflection_object_has_class(object, "ReflectionParameter", values)? { - return Ok(None); - } - eval_reflection_bind_no_args(evaluated_args)?; - let type_value = values.method_call(object, "getType", Vec::new())?; - if values.is_null(type_value)? { - return values.bool_value(false).map(Some); - } - if !eval_reflection_object_has_class(type_value, "ReflectionNamedType", values)? { - return values.bool_value(false).map(Some); - } - let name = values.method_call(type_value, "getName", Vec::new())?; - let bytes = values.string_bytes(name)?; - let name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - values - .bool_value(name.eq_ignore_ascii_case(expected_type)) - .map(Some) -} - -/// Handles eval-backed `ReflectionParameter::__toString()` calls. -pub(in crate::interpreter) fn eval_reflection_parameter_to_string_result( - object: RuntimeCellHandle, - method_name: &str, - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("__toString") { - return Ok(None); - } - if !eval_reflection_object_has_class(object, "ReflectionParameter", values)? { - return Ok(None); - } - eval_reflection_bind_no_args(evaluated_args)?; - let rendered = eval_reflection_parameter_object_to_string(object, values)?; - values.string(&rendered).map(Some) -} - -/// Handles eval-backed `ReflectionType::__toString()` calls. -pub(in crate::interpreter) fn eval_reflection_type_to_string_result( - object: RuntimeCellHandle, - method_name: &str, - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("__toString") { - return Ok(None); - } - let Some(rendered) = eval_reflection_type_object_to_string(object, values)? else { - return Ok(None); - }; - eval_reflection_bind_no_args(evaluated_args)?; - values.string(&rendered).map(Some) -} - -/// Formats one ReflectionParameter object through its public metadata methods. -fn eval_reflection_parameter_object_to_string( - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let position = eval_reflection_no_arg_int_method(object, "getPosition", values)?; - let name = eval_reflection_no_arg_string_method(object, "getName", values)?; - let is_optional = eval_reflection_no_arg_bool_method(object, "isOptional", values)?; - let is_passed_by_reference = - eval_reflection_no_arg_bool_method(object, "isPassedByReference", values)?; - let is_variadic = eval_reflection_no_arg_bool_method(object, "isVariadic", values)?; - let type_value = values.method_call(object, "getType", Vec::new())?; - let type_text = if values.is_null(type_value)? { - None - } else { - eval_reflection_type_object_to_string(type_value, values)? - }; - - let mut signature_parts = Vec::new(); - if let Some(type_text) = type_text { - signature_parts.push(type_text); - } - let mut variable = String::new(); - if is_passed_by_reference { - variable.push('&'); - } - if is_variadic { - variable.push_str("..."); - } - variable.push('$'); - variable.push_str(&name); - signature_parts.push(variable); - let requiredness = if is_optional { "optional" } else { "required" }; - let default = eval_reflection_parameter_object_default_to_string(object, values)? - .map(|value| format!(" = {value}")) - .unwrap_or_default(); - - Ok(format!( - "Parameter #{} [ <{}> {}{} ]", - position, - requiredness, - signature_parts.join(" "), - default - )) -} - -/// Formats a ReflectionParameter default through its public metadata methods. -fn eval_reflection_parameter_object_default_to_string( - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !eval_reflection_no_arg_bool_method(object, "isDefaultValueAvailable", values)? { - return Ok(None); - } - if eval_reflection_no_arg_bool_method(object, "isDefaultValueConstant", values)? { - let constant_name = - eval_reflection_no_arg_string_method(object, "getDefaultValueConstantName", values)?; - if !constant_name.is_empty() { - return Ok(Some(constant_name)); - } - } - let default_value = values.method_call(object, "getDefaultValue", Vec::new())?; - eval_reflection_runtime_default_value_to_string(default_value, values).map(Some) -} - -/// Formats one materialized scalar-ish default value for reflection string output. -fn eval_reflection_runtime_default_value_to_string( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(match values.type_tag(value)? { - EVAL_TAG_NULL => String::from("NULL"), - EVAL_TAG_BOOL => { - if values.truthy(value)? { - String::from("true") - } else { - String::from("false") - } - } - EVAL_TAG_INT | EVAL_TAG_FLOAT => String::from_utf8_lossy(&values.string_bytes(value)?) - .into_owned(), - EVAL_TAG_STRING => { - let value = String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(); - format!("'{value}'") - } - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC if values.array_len(value)? == 0 => String::from("[]"), - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("Array"), - EVAL_TAG_OBJECT => String::from("Object"), - _ => String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(), - }) -} - -/// Calls one no-arg Reflection method and returns its string result. -fn eval_reflection_no_arg_string_method( - object: RuntimeCellHandle, - method: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.method_call(object, method, Vec::new())?; - eval_reflection_string_arg(value, values) -} - -/// Calls one no-arg Reflection method and returns its bool result. -fn eval_reflection_no_arg_bool_method( - object: RuntimeCellHandle, - method: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.method_call(object, method, Vec::new())?; - if values.type_tag(value)? != EVAL_TAG_BOOL { - return Err(EvalStatus::RuntimeFatal); - } - values.truthy(value) -} - -/// Calls one no-arg Reflection method and returns its int result. -fn eval_reflection_no_arg_int_method( - object: RuntimeCellHandle, - method: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.method_call(object, method, Vec::new())?; - eval_int_value(value, values) -} - -/// Formats one eval-visible ReflectionType object if the value is a retained type object. -fn eval_reflection_type_object_to_string( - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let type_kind = if eval_reflection_object_has_class(object, "ReflectionNamedType", values)? { - Some(("ReflectionNamedType", "")) - } else if eval_reflection_object_has_class(object, "ReflectionUnionType", values)? { - Some(("ReflectionUnionType", "|")) - } else if eval_reflection_object_has_class(object, "ReflectionIntersectionType", values)? { - Some(("ReflectionIntersectionType", "&")) - } else { - None - }; - let Some((class_name, separator)) = type_kind else { - return Ok(None); - }; - let rendered = if class_name == "ReflectionNamedType" { - eval_reflection_named_type_to_string(object, values)? - } else { - eval_reflection_composite_type_to_string(object, separator, values)? - }; - Ok(Some(rendered)) -} - -/// Formats one eval-visible ReflectionNamedType object from its public methods. -fn eval_reflection_named_type_to_string( - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let name = eval_reflection_type_method_string(object, "getName", values)?; - let allows_null = eval_reflection_type_method_bool(object, "allowsNull", values)?; - if allows_null && name != "mixed" { - Ok(format!("?{name}")) - } else { - Ok(name) - } -} - -/// Formats one eval-visible ReflectionUnionType or ReflectionIntersectionType object. -fn eval_reflection_composite_type_to_string( - object: RuntimeCellHandle, - separator: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let types = values.method_call(object, "getTypes", Vec::new())?; - let mut names = Vec::new(); - for position in 0..values.array_len(types)? { - let key = values.array_iter_key(types, position)?; - let member = values.array_get(types, key)?; - names.push(eval_reflection_type_method_string(member, "getName", values)?); - } - if separator == "|" && eval_reflection_type_method_bool(object, "allowsNull", values)? { - names.push(String::from("null")); - } - Ok(names.join(separator)) -} - -/// Calls one no-arg ReflectionType method and returns its string result. -fn eval_reflection_type_method_string( - object: RuntimeCellHandle, - method: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.method_call(object, method, Vec::new())?; - let bytes = values.string_bytes(value)?; - String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Calls one no-arg ReflectionType method and returns its bool result. -fn eval_reflection_type_method_bool( - object: RuntimeCellHandle, - method: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let value = values.method_call(object, method, Vec::new())?; - if values.type_tag(value)? != EVAL_TAG_BOOL { - return Err(EvalStatus::RuntimeFatal); - } - values.truthy(value) -} - -/// Maps a legacy ReflectionParameter predicate method to its target named type. -fn eval_reflection_parameter_legacy_type_name(method_name: &str) -> Option<&'static str> { - if method_name.eq_ignore_ascii_case("isArray") { - Some("array") - } else if method_name.eq_ignore_ascii_case("isCallable") { - Some("callable") - } else { - None - } -} - -/// Returns whether one runtime object cell has the requested PHP class name. -fn eval_reflection_object_has_class( - object: RuntimeCellHandle, - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { - return Ok(false); - } - let actual = values.object_class_name(object)?; - let bytes = values.string_bytes(actual); - values.release(actual)?; - let actual = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(actual - .trim_start_matches('\\') - .eq_ignore_ascii_case(class_name)) -} - -/// Handles eval-backed `ReflectionMethod::hasPrototype()` and `getPrototype()` calls. -pub(in crate::interpreter) fn eval_reflection_method_prototype_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let is_has_prototype = method_name.eq_ignore_ascii_case("hasPrototype"); - let is_get_prototype = method_name.eq_ignore_ascii_case("getPrototype"); - if !is_has_prototype && !is_get_prototype { - return Ok(None); - } - let Some((declaring_class, reflected_method)) = - context - .eval_reflection_method(identity) - .map(|(declaring_class, method_name)| { - (declaring_class.to_string(), method_name.to_string()) - }) - else { - return Ok(None); - }; - eval_reflection_bind_no_args(evaluated_args)?; - let Some((prototype_class, prototype_method)) = eval_reflection_method_prototype_target( - &declaring_class, - &reflected_method, - context, - values, - )? - else { - if is_has_prototype { - return values.bool_value(false).map(Some); - } - return eval_throw_reflection_exception( - &format!( - "Method {}::{} does not have a prototype", - declaring_class, reflected_method - ), - context, - values, - ); - }; - if is_has_prototype { - return values.bool_value(true).map(Some); - } - let Some(metadata) = - eval_reflection_prototype_method_metadata(&prototype_class, &prototype_method, context, values)? - else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_METHOD, - &prototype_method, - &metadata, - context, - values, - ) - .map(Some) -} - -/// Handles PHP's no-op `ReflectionMethod/Property::setAccessible()` calls. -pub(in crate::interpreter) fn eval_reflection_set_accessible_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("setAccessible") { - return Ok(None); - } - if context.eval_reflection_method(identity).is_none() - && context.eval_reflection_property(identity).is_none() - { - return Ok(None); - } - let _ = bind_evaluated_function_args(&[String::from("accessible")], evaluated_args)?; - values.null().map(Some) -} - -/// Handles eval-backed `ReflectionProperty` hook-inspection calls. -pub(in crate::interpreter) fn eval_reflection_property_hooks_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, property_name)) = - context - .eval_reflection_property(identity) - .map(|(declaring_class, property_name)| { - (declaring_class.to_string(), property_name.to_string()) - }) - else { - return Ok(None); - }; - let Some((property_class, property)) = - eval_reflection_property_for_hooks(&declaring_class, &property_name, context) - else { - return Ok(None); - }; - match method_name.to_ascii_lowercase().as_str() { - "hashooks" => { - eval_reflection_bind_no_args(evaluated_args)?; - let has_hooks = !eval_reflection_property_hook_kinds(&property).is_empty(); - values.bool_value(has_hooks).map(Some) - } - "hashook" => { - let hook = eval_reflection_property_hook_arg(evaluated_args, context, values)?; - values - .bool_value(eval_reflection_property_has_hook(&property, hook)) - .map(Some) - } - "gethook" => { - let hook = eval_reflection_property_hook_arg(evaluated_args, context, values)?; - if !eval_reflection_property_has_hook(&property, hook) { - return values.null().map(Some); - } - eval_reflection_property_hook_method_object( - &property_class, - &property, - hook, - context, - values, - ) - .map(Some) - } - "gethooks" => { - eval_reflection_bind_no_args(evaluated_args)?; - eval_reflection_property_hook_method_array(&property_class, &property, context, values) - .map(Some) - } - _ => Ok(None), - } -} - -/// Handles eval-backed `ReflectionProperty::getValue()` calls. -pub(in crate::interpreter) fn eval_reflection_property_get_value_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getValue") { - return Ok(None); - } - let Some((declaring_class, property_name)) = - context - .eval_reflection_property(identity) - .map(|(declaring_class, property_name)| { - (declaring_class.to_string(), property_name.to_string()) - }) - else { - return Ok(None); - }; - let object = eval_reflection_property_get_value_arg(evaluated_args)?; - if context.eval_reflection_property_is_dynamic(identity) { - let object = object.ok_or(EvalStatus::RuntimeFatal)?; - return eval_reflection_dynamic_property_get_value( - &declaring_class, - &property_name, - object, - context, - values, - ) - .map(Some); - } - let Some(member) = - eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? - else { - return Err(EvalStatus::RuntimeFatal); - }; - if member.is_static { - return eval_reflection_static_property_value( - &declaring_class, - &property_name, - context, - values, - )? - .map(Some) - .ok_or(EvalStatus::RuntimeFatal); - } - let object = object.ok_or(EvalStatus::RuntimeFatal)?; - if eval_reflection_class_like_exists(&declaring_class, context) { - eval_reflection_instance_property_get_value( - &declaring_class, - &property_name, - object, - context, - values, - ) - } else { - eval_reflection_aot_instance_property_get_value( - &declaring_class, - &property_name, - object, - context, - values, - ) - } - .map(Some) -} - -/// Handles eval-backed `ReflectionProperty::setValue()` calls. -pub(in crate::interpreter) fn eval_reflection_property_set_value_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("setValue") { - return Ok(None); - } - let Some((declaring_class, property_name)) = - context - .eval_reflection_property(identity) - .map(|(declaring_class, property_name)| { - (declaring_class.to_string(), property_name.to_string()) - }) - else { - return Ok(None); - }; - let (object_or_value, value) = eval_reflection_property_set_value_args(evaluated_args)?; - if context.eval_reflection_property_is_dynamic(identity) { - let value = value.ok_or(EvalStatus::RuntimeFatal)?; - eval_reflection_dynamic_property_set_value( - &declaring_class, - &property_name, - object_or_value, - value, - context, - values, - )?; - return values.null().map(Some); - } - let Some(member) = - eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? - else { - return Err(EvalStatus::RuntimeFatal); - }; - if member.is_static { - let value = value.unwrap_or(object_or_value); - if eval_reflection_class_like_exists(&declaring_class, context) { - let declaring_class = member - .declaring_class_name - .as_deref() - .ok_or(EvalStatus::RuntimeFatal)?; - if let Some(replaced) = - context.set_static_property(declaring_class, &property_name, value) - { - values.release(replaced)?; - } - } else { - let declaring_class = member - .declaring_class_name - .as_deref() - .unwrap_or(declaring_class.as_str()); - let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { - values.static_property_set(declaring_class, &property_name, value) - })?; - if !updated { - return Err(EvalStatus::RuntimeFatal); - } - } - return values.null().map(Some); - } - let value = value.ok_or(EvalStatus::RuntimeFatal)?; - if eval_reflection_class_like_exists(&declaring_class, context) { - eval_reflection_instance_property_set_value( - &declaring_class, - &property_name, - object_or_value, - value, - context, - values, - )?; - } else { - eval_reflection_aot_instance_property_set_value( - &declaring_class, - &property_name, - object_or_value, - value, - context, - values, - )?; - } - values.null().map(Some) -} - -/// Handles `ReflectionProperty::isInitialized()` calls for eval and generated/AOT properties. -pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("isInitialized") { - return Ok(None); - } - let Some((declaring_class, property_name)) = - context - .eval_reflection_property(identity) - .map(|(declaring_class, property_name)| { - (declaring_class.to_string(), property_name.to_string()) - }) - else { - return Ok(None); - }; - let object = eval_reflection_property_get_value_arg(evaluated_args)?; - if context.eval_reflection_property_is_dynamic(identity) { - let object = object.ok_or(EvalStatus::RuntimeFatal)?; - return eval_reflection_dynamic_property_is_initialized( - &declaring_class, - &property_name, - object, - context, - values, - ) - .and_then(|initialized| values.bool_value(initialized)) - .map(Some); - } - let Some(member) = - eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? - else { - return Err(EvalStatus::RuntimeFatal); - }; - if member.is_static { - let declaring_class = member - .declaring_class_name - .as_deref() - .ok_or(EvalStatus::RuntimeFatal)?; - let initialized = if eval_reflection_class_like_exists(declaring_class, context) { - context - .static_property(declaring_class, &property_name) - .is_some() - } else { - eval_reflection_aot_static_property_is_initialized( - declaring_class, - &property_name, - context, - values, - )? - }; - return values.bool_value(initialized).map(Some); - } - let object = object.ok_or(EvalStatus::RuntimeFatal)?; - if eval_reflection_class_like_exists(&declaring_class, context) { - eval_reflection_instance_property_is_initialized( - &declaring_class, - &property_name, - object, - context, - values, - ) - } else { - eval_reflection_aot_instance_property_is_initialized( - &declaring_class, - &property_name, - object, - context, - values, - ) - } - .and_then(|initialized| values.bool_value(initialized)) - .map(Some) -} - -/// Handles `ReflectionProperty::isLazy()` and `skipLazyInitialization()` calls. -pub(in crate::interpreter) fn eval_reflection_property_lazy_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, property_name)) = - context - .eval_reflection_property(identity) - .map(|(declaring_class, property_name)| { - (declaring_class.to_string(), property_name.to_string()) - }) - else { - return Ok(None); - }; - if method_name.eq_ignore_ascii_case("isLazy") { - let object = eval_reflection_property_raw_value_arg(evaluated_args)?; - if context.eval_reflection_property_is_dynamic(identity) { - eval_reflection_dynamic_property_validate_object( - &declaring_class, - object, - context, - values, - )?; - return values.bool_value(false).map(Some); - } - if eval_reflection_class_like_exists(&declaring_class, context) { - eval_reflection_property_validate_object(&declaring_class, object, context, values)?; - } else { - eval_reflection_aot_instance_property_validate_object( - &declaring_class, - object, - context, - values, - )?; - } - return values.bool_value(false).map(Some); - } - if method_name.eq_ignore_ascii_case("skipLazyInitialization") { - let object = eval_reflection_property_raw_value_arg(evaluated_args)?; - if context.eval_reflection_property_is_dynamic(identity) { - eval_reflection_dynamic_property_validate_object( - &declaring_class, - object, - context, - values, - )?; - return Err(EvalStatus::RuntimeFatal); - } - if eval_reflection_class_like_exists(&declaring_class, context) { - let (_, property) = eval_reflection_instance_property_target( - &declaring_class, - &property_name, - object, - context, - values, - )?; - if property.is_virtual() { - return Err(EvalStatus::RuntimeFatal); - } - } else { - eval_reflection_aot_instance_property_validate_object( - &declaring_class, - object, - context, - values, - )?; - } - return values.null().map(Some); - } - Ok(None) -} - -/// Handles eval-backed `ReflectionProperty::__toString()` calls. -pub(in crate::interpreter) fn eval_reflection_property_to_string_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("__toString") { - return Ok(None); - } - let Some((declaring_class, property_name)) = - context - .eval_reflection_property(identity) - .map(|(declaring_class, property_name)| { - (declaring_class.to_string(), property_name.to_string()) - }) - else { - return Ok(None); - }; - eval_reflection_bind_no_args(evaluated_args)?; - if context.eval_reflection_property_is_dynamic(identity) { - let member = eval_reflection_dynamic_property_metadata(&declaring_class); - let text = eval_reflection_property_to_string(&property_name, &member); - return values.string(&text).map(Some); - } - let member = - eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - let text = eval_reflection_property_to_string(&property_name, &member); - values.string(&text).map(Some) -} - -/// Handles eval-backed `ReflectionClassConstant` and enum-case `__toString()` calls. -pub(in crate::interpreter) fn eval_reflection_class_constant_to_string_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("__toString") { - return Ok(None); - } - let Some((declaring_class, constant_name, owner_kind)) = - context - .eval_reflection_class_constant(identity) - .map(|(declaring_class, constant_name, owner_kind)| { - ( - declaring_class.to_string(), - constant_name.to_string(), - owner_kind, - ) - }) - else { - return Ok(None); - }; - eval_reflection_bind_no_args(evaluated_args)?; - let text = eval_reflection_class_constant_to_string( - &declaring_class, - &constant_name, - owner_kind, - context, - values, - )?; - values.string(&text).map(Some) -} - -/// Handles `ReflectionEnumUnitCase::getEnum()` and `ReflectionEnumBackedCase::getEnum()`. -pub(in crate::interpreter) fn eval_reflection_enum_case_get_enum_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getEnum") { - return Ok(None); - } - eval_reflection_bind_no_args(evaluated_args)?; - let Some((declaring_class, _, owner_kind)) = context - .eval_reflection_class_constant(identity) - .map(|(class, constant, owner_kind)| (class.to_string(), constant.to_string(), owner_kind)) - else { - return Ok(None); - }; - if !matches!( - owner_kind, - EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE - ) { - return Ok(None); - } - eval_reflection_enum_object_result(&declaring_class, context, values).map(Some) -} - -/// Handles `ReflectionProperty::getRawValue()` and raw write calls. -pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, property_name)) = - context - .eval_reflection_property(identity) - .map(|(declaring_class, property_name)| { - (declaring_class.to_string(), property_name.to_string()) - }) - else { - return Ok(None); - }; - if method_name.eq_ignore_ascii_case("getRawValue") { - let object = eval_reflection_property_raw_value_arg(evaluated_args)?; - if context.eval_reflection_property_is_dynamic(identity) { - return eval_reflection_dynamic_property_get_value( - &declaring_class, - &property_name, - object, - context, - values, - ) - .map(Some); - } - return if eval_reflection_class_like_exists(&declaring_class, context) { - eval_reflection_instance_property_get_raw_value( - &declaring_class, - &property_name, - object, - context, - values, - ) - } else { - eval_reflection_aot_instance_property_get_value( - &declaring_class, - &property_name, - object, - context, - values, - ) - } - .map(Some); - } - if method_name.eq_ignore_ascii_case("setRawValue") - || method_name.eq_ignore_ascii_case("setRawValueWithoutLazyInitialization") - { - let (object, value) = eval_reflection_property_set_raw_value_args(evaluated_args)?; - if context.eval_reflection_property_is_dynamic(identity) { - eval_reflection_dynamic_property_set_value( - &declaring_class, - &property_name, - object, - value, - context, - values, - )?; - return values.null().map(Some); - } - if eval_reflection_class_like_exists(&declaring_class, context) { - eval_reflection_instance_property_set_raw_value( - &declaring_class, - &property_name, - object, - value, - context, - values, - )?; - } else { - eval_reflection_aot_instance_property_set_value( - &declaring_class, - &property_name, - object, - value, - context, - values, - )?; - } - return values.null().map(Some); - } - Ok(None) -} - -/// Handles eval-backed `ReflectionClass::getReflectionConstant()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getReflectionConstant") { - return Ok(None); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; - let requested_name = eval_reflection_string_arg(args[0], values)?; - if !eval_reflection_constant_names(&reflected_name, context, values)? - .iter() - .any(|name| name == &requested_name) - { - return values.bool_value(false).map(Some); - } - eval_reflection_class_constant_object_result(&reflected_name, &requested_name, context, values) - .map(Some) -} - -/// Handles eval-backed `ReflectionClass::getReflectionConstants()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("getReflectionConstants") { - return Ok(None); - } - let filter = eval_reflection_member_filter(evaluated_args, values)?; - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let names = eval_reflection_constant_names(&reflected_name, context, values)?; - let mut result = values.array_new(names.len())?; - let mut index = 0; - for name in &names { - if !eval_reflection_constant_matches_filter(reflected_name.as_str(), name, filter, context, values)? - { - continue; - } - let object = - eval_reflection_class_constant_object_result(&reflected_name, name, context, values)?; - let key = values.int(index)?; - result = values.array_set(result, key, object)?; - index += 1; - } - Ok(Some(result)) -} - -/// Handles eval-backed `ReflectionClass::getMethods()` and `getProperties()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_members_result( - object: RuntimeCellHandle, - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let owner_kind = if method_name.eq_ignore_ascii_case("getMethods") { - EVAL_REFLECTION_OWNER_METHOD - } else if method_name.eq_ignore_ascii_case("getProperties") { - EVAL_REFLECTION_OWNER_PROPERTY - } else { - return Ok(None); - }; - let filter = eval_reflection_member_filter(evaluated_args, values)?; - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { - let names = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - metadata.method_names - } else { - metadata.property_names - }; - return eval_reflection_member_object_array_result( - owner_kind, - &reflected_name, - &names, - filter, - context, - values, - ) - .and_then(|result| { - eval_reflection_object_dynamic_property_array_result( - object, - owner_kind, - &reflected_name, - filter, - result, - context, - values, - ) - }) - .map(Some); - } - let native_interface_property_names = - if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { - eval_reflection_native_interface_property_names(&reflected_name, context) - } else { - Vec::new() - }; - let names = if native_interface_property_names.is_empty() { - eval_reflection_aot_member_names(owner_kind, &reflected_name, values)? - } else { - native_interface_property_names - }; - eval_reflection_aot_member_object_array_result( - owner_kind, - &reflected_name, - &names, - filter, - context, - values, - ) - .and_then(|result| { - eval_reflection_object_dynamic_property_array_result( - object, - owner_kind, - &reflected_name, - filter, - result, - context, - values, - ) - }) - .map(Some) -} - -/// Handles eval-backed `ReflectionClass::getMethod()` and `getProperty()` calls. -pub(in crate::interpreter) fn eval_reflection_class_get_member_result( - object: RuntimeCellHandle, - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let owner_kind = if method_name.eq_ignore_ascii_case("getMethod") { - EVAL_REFLECTION_OWNER_METHOD - } else if method_name.eq_ignore_ascii_case("getProperty") { - EVAL_REFLECTION_OWNER_PROPERTY - } else { - return Ok(None); - }; - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; - let requested_name = eval_reflection_string_arg(args[0], values)?; - let Some(member_name) = - eval_reflection_member_name(owner_kind, &reflected_name, &requested_name, context) - else { - if owner_kind == EVAL_REFLECTION_OWNER_METHOD - && !eval_reflection_class_like_exists(&reflected_name, context) - { - if let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( - &reflected_name, - &requested_name, - context, - values, - )? { - let member_name = requested_name.to_ascii_lowercase(); - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_METHOD, - &member_name, - &member, - context, - values, - ) - .map(Some); - } - } - if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY - && !eval_reflection_class_like_exists(&reflected_name, context) - { - if let Some((declaring_class, property)) = - eval_reflection_native_interface_property_requirement( - &reflected_name, - &requested_name, - context, - ) - { - let member = eval_reflection_interface_property_metadata(declaring_class, &property); - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - &requested_name, - &member, - context, - values, - ) - .map(Some); - } - if let Some(member) = eval_reflection_aot_property_metadata_if_exists( - &reflected_name, - &requested_name, - context, - values, - )? { - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - &requested_name, - &member, - context, - values, - ) - .map(Some); - } - } - if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { - if let Some(dynamic_object) = - eval_reflection_object_reflected_object(object, context, values)? - { - let exists = eval_reflection_object_dynamic_property_exists( - dynamic_object, - &requested_name, - values, - ); - values.release(dynamic_object)?; - if exists? { - let member = eval_reflection_dynamic_property_metadata(&reflected_name); - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - &requested_name, - &member, - context, - values, - ) - .map(Some); - } - } - } - let message_name = eval_reflection_class_like_attributes(&reflected_name, context) - .map(|metadata| metadata.resolved_name) - .unwrap_or_else(|| reflected_name.clone()); - let message = - eval_reflection_missing_member_message(owner_kind, &message_name, &requested_name); - return eval_throw_reflection_exception(&message, context, values); - }; - let member = - eval_reflection_member_metadata(owner_kind, &reflected_name, &member_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; - eval_reflection_member_object_result(owner_kind, &member_name, &member, context, values) - .map(Some) -} - -/// Returns generated/AOT constant names visible through eval ReflectionClass. -fn eval_reflection_aot_constant_names( - reflected_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = reflected_name.trim_start_matches('\\'); - let names_array = values.reflection_constant_names(runtime_class_name)?; - let names = eval_reflection_string_array_to_vec(names_array, values)?; - values.release(names_array)?; - Ok(names) -} - -/// Returns constant names from eval metadata or generated/AOT runtime metadata. -fn eval_reflection_constant_names( - reflected_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if context.has_interface(reflected_name) { - Ok(context.interface_constant_names(reflected_name)) - } else if context.has_trait(reflected_name) { - Ok(context.trait_constant_names(reflected_name)) - } else if context.has_class(reflected_name) || context.has_enum(reflected_name) { - Ok(context.class_constant_names(reflected_name)) - } else { - eval_reflection_aot_constant_names(reflected_name, values) - } -} - -/// Returns a materialized eval constant value for Reflection without visibility checks. -fn eval_reflection_eval_constant_value( - reflected_name: &str, - constant_name: &str, - context: &ElephcEvalContext, -) -> Option { - if let Some(case) = context.enum_case(reflected_name, constant_name) { - return Some(case); - } - let (declaring_class, constant) = context.class_constant(reflected_name, constant_name)?; - context.class_constant_cell(&declaring_class, constant.name()) -} - -/// Returns a materialized eval or AOT constant value for Reflection without visibility checks. -fn eval_reflection_constant_value( - reflected_name: &str, - constant_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if eval_reflection_class_like_exists(reflected_name, context) { - return Ok(eval_reflection_eval_constant_value( - reflected_name, - constant_name, - context, - )); - } - let runtime_class_name = reflected_name.trim_start_matches('\\'); - values.reflection_constant_value(runtime_class_name, constant_name) -} - -/// Builds one eval-backed `ReflectionClassConstant` object for a visible constant name. -fn eval_reflection_class_constant_object_result( - reflected_name: &str, - constant_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (declaring_class_name, attributes, visibility, is_final, is_enum_case) = - eval_reflection_class_constant_metadata(reflected_name, constant_name, context, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - let constant_value = - eval_reflection_constant_value(reflected_name, constant_name, context, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); - if is_enum_case { - flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; - } - let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); - eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_CLASS_CONSTANT, - constant_name, - &attributes, - &[], - &[], - &[], - &[], - Some(&declaring_class_name), - &[], - None, - None, - None, - None, - flags, - modifiers, - 0, - Some(constant_value), - None, - context, - values, - ) -} - -/// Returns whether one class constant passes an optional `ReflectionClassConstant` filter. -fn eval_reflection_constant_matches_filter( - reflected_name: &str, - constant_name: &str, - filter: Option, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(filter) = filter else { - return Ok(true); - }; - Ok(eval_reflection_class_constant_metadata(reflected_name, constant_name, context, values)? - .is_some_and(|(_, _, visibility, is_final, _)| { - eval_reflection_class_constant_modifiers(visibility, is_final) & filter != 0 - })) -} - -/// Resolves the declared member spelling for eval `ReflectionClass` single-member lookups. -fn eval_reflection_member_name( - owner_kind: u64, - reflected_name: &str, - requested_name: &str, - context: &ElephcEvalContext, -) -> Option { - let metadata = eval_reflection_class_like_attributes(reflected_name, context)?; - let names = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - metadata.method_names - } else { - metadata.property_names - }; - names.into_iter().find(|name| match owner_kind { - EVAL_REFLECTION_OWNER_METHOD => name.eq_ignore_ascii_case(requested_name), - EVAL_REFLECTION_OWNER_PROPERTY => name == requested_name, - _ => false, - }) -} - -/// Builds PHP-compatible missing-member messages for eval ReflectionClass lookups. -fn eval_reflection_missing_member_message( - owner_kind: u64, - reflected_name: &str, - requested_name: &str, -) -> String { - if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - format!( - "Method {}::{}() does not exist", - reflected_name, requested_name - ) - } else { - format!( - "Property {}::${} does not exist", - reflected_name, requested_name - ) - } -} - -/// Builds an eval-backed `ReflectionClass` object when the reflected class-like exists in eval. -fn eval_reflection_class_new( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; - let class_name = eval_reflection_class_target_name(args[0], context, values)?; - let reflected_name = context - .resolve_class_like_name(&class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - eval_reflection_class_owner_object_result( - EVAL_REFLECTION_OWNER_CLASS, - &reflected_name, - context, - values, - ) -} - -/// Builds an eval-backed `ReflectionObject` from an object instance. -fn eval_reflection_object_new( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; - let tag = values.type_tag(args[0])?; - if tag != EVAL_TAG_OBJECT { - return eval_throw_type_error( - &format!( - "ReflectionObject::__construct(): Argument #1 ($object) must be of type object, {} given", - eval_reflection_type_error_type_name(tag) - ), - context, - values, - ); - } - let reflected_name = eval_reflection_object_class_name(args[0], context, values)?; - let Some(object) = eval_reflection_class_owner_object_result( - EVAL_REFLECTION_OWNER_OBJECT, - &reflected_name, - context, - values, - )? - else { - return Err(EvalStatus::RuntimeFatal); - }; - eval_reflection_with_declaring_class_scope("ReflectionObject", context, |_| { - values.property_set(object, "__object", args[0]) - })?; - Ok(Some(object)) -} - -/// Materializes class metadata for `ReflectionClass` or `ReflectionObject`. -fn eval_reflection_class_owner_object_result( - owner_kind: u64, - reflected_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) else { - if reflected_name - .trim_start_matches('\\') - .eq_ignore_ascii_case("Closure") - { - return eval_reflection_builtin_closure_class_object_result( - owner_kind, context, values, - ) - .map(Some); - } - let Some((flags, modifiers)) = eval_reflection_aot_class_flags(reflected_name, values)? - else { - return Ok(None); - }; - let method_names = eval_reflection_aot_member_names( - EVAL_REFLECTION_OWNER_METHOD, - reflected_name, - values, - )?; - let property_names = eval_reflection_aot_member_names( - EVAL_REFLECTION_OWNER_PROPERTY, - reflected_name, - values, - )?; - let interface_names = eval_reflection_aot_class_interface_names(reflected_name, values)?; - let trait_names = eval_reflection_aot_class_trait_names(reflected_name, values)?; - let parent_class_name = eval_reflection_aot_parent_class_name(reflected_name, values)?; - let attributes = context.native_class_attributes(reflected_name); - return eval_reflection_owner_object( - owner_kind, - reflected_name, - &attributes, - &interface_names, - &trait_names, - &method_names, - &property_names, - parent_class_name.as_deref(), - &[], - None, - None, - None, - None, - flags, - modifiers, - 0, - None, - None, - context, - values, - ) - .map(Some); - }; - let interface_names = - eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; - let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; - eval_reflection_owner_object( - owner_kind, - &metadata.resolved_name, - &metadata.attributes, - &interface_names, - &metadata.trait_names, - &metadata.method_names, - &metadata.property_names, - metadata.parent_class_name.as_deref(), - &[], - None, - None, - None, - None, - flags, - metadata.modifiers, - 0, - None, - None, - context, - values, - ) - .map(Some) -} - -/// Builds the minimal ReflectionClass metadata object for PHP's builtin Closure class. -fn eval_reflection_builtin_closure_class_object_result( - owner_kind: u64, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let flags = EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_INTERNAL; - let modifiers = eval_reflection_class_modifiers(true, false, false, false); - eval_reflection_owner_object( - owner_kind, - "Closure", - &[], - &[], - &[], - &[], - &[], - None, - &[], - None, - None, - None, - None, - flags, - modifiers, - 0, - None, - None, - context, - values, - ) -} - -/// Builds an eval-backed `ReflectionEnum` object for a declared enum. -fn eval_reflection_enum_new( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; - let class_name = eval_reflection_class_target_name(args[0], context, values)?; - let reflected_name = context - .resolve_enum_name(&class_name) - .or_else(|| context.resolve_class_like_name(&class_name)) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - if context.enum_decl(&reflected_name).is_some() { - return eval_reflection_enum_object_result(&reflected_name, context, values).map(Some); - } - if eval_reflection_class_like_exists(&reflected_name, context) { - return eval_throw_reflection_exception( - &format!("Class \"{}\" is not an enum", reflected_name), - context, - values, - ); - } - if eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { - return Ok(None); - } - eval_throw_reflection_exception( - &format!("Class \"{}\" does not exist", reflected_name), - context, - values, - ) -} - -/// Materializes one eval-backed `ReflectionEnum` owner object. -fn eval_reflection_enum_object_result( - enum_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let reflected_name = context - .resolve_enum_name(enum_name) - .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); - let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { - return Err(EvalStatus::RuntimeFatal); - }; - if !context.has_enum(&metadata.resolved_name) { - return Err(EvalStatus::RuntimeFatal); - } - eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_ENUM, - &metadata.resolved_name, - &metadata.attributes, - &metadata.interface_names, - &metadata.trait_names, - &metadata.method_names, - &metadata.property_names, - metadata.parent_class_name.as_deref(), - &[], - None, - None, - None, - None, - metadata.flags, - metadata.modifiers, - 0, - None, - None, - context, - values, - ) -} - -/// Resolves a ReflectionClass constructor target from a class-name string or object. -fn eval_reflection_class_target_name( - target: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(target)? == EVAL_TAG_OBJECT { - return eval_reflection_object_class_name(target, context, values); - } - eval_reflection_string_arg(target, values) -} - -/// Returns generated/AOT class flags for synthetic ReflectionClass fallback objects. -fn eval_reflection_aot_class_flags( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let is_class = values.class_exists(runtime_class_name)?; - let is_interface = eval_runtime_interface_exists(runtime_class_name, values)?; - let is_trait = values.trait_exists(runtime_class_name)?; - let is_enum = values.enum_exists(runtime_class_name)?; - if !(is_class || is_interface || is_trait || is_enum) { - return Ok(None); - } - let mut flags = 0; - if eval_reflection_class_like_is_internal(runtime_class_name) { - flags |= EVAL_REFLECTION_CLASS_FLAG_INTERNAL; - } else { - flags |= EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; - } - let mut class_flags = values.reflection_class_flags(runtime_class_name)?.unwrap_or(0); - if is_enum { - class_flags &= !EVAL_REFLECTION_CLASS_FLAG_READONLY; - } - flags |= class_flags - & (EVAL_REFLECTION_CLASS_FLAG_FINAL - | EVAL_REFLECTION_CLASS_FLAG_ABSTRACT - | EVAL_REFLECTION_CLASS_FLAG_READONLY); - if is_interface { - flags |= EVAL_REFLECTION_CLASS_FLAG_INTERFACE; - } - if is_trait { - flags |= EVAL_REFLECTION_CLASS_FLAG_TRAIT; - } - if is_enum { - flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM; - } - if eval_reflection_builtin_class_is_iterable(runtime_class_name) { - flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; - } - if is_class && !is_enum && flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT == 0 { - if eval_reflection_aot_lifecycle_method_allows_public_reflection( - runtime_class_name, - "__construct", - values, - )? { - flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; - } - if eval_reflection_aot_lifecycle_method_allows_public_reflection( - runtime_class_name, - "__clone", - values, - )? { - flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; - } - } - let modifiers = eval_reflection_class_modifiers( - flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0, - flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0, - flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0, - is_enum, - ); - Ok(Some((flags, modifiers))) -} - -/// Returns AOT class modifiers relevant to validating an eval `extends` clause. -pub(in crate::interpreter) fn eval_reflection_aot_class_inheritance_modifiers( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { - return Ok(None); - }; - if flags - & (EVAL_REFLECTION_CLASS_FLAG_INTERFACE - | EVAL_REFLECTION_CLASS_FLAG_TRAIT - | EVAL_REFLECTION_CLASS_FLAG_ENUM) - != 0 - { - return Ok(None); - } - Ok(Some(( - flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0, - flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0, - ))) -} - -/// Returns the catchable error for generated/AOT allocation without constructor, if any. -pub(in crate::interpreter) fn eval_reflection_aot_class_without_constructor_error( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { - return Ok(None); - }; - Ok(eval_reflection_non_instantiable_error_message( - class_name, flags, - )) -} - -/// Returns the catchable error for generated/AOT public ReflectionClass construction, if any. -pub(in crate::interpreter) fn eval_reflection_aot_class_public_instantiation_error( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { - return Ok(None); - }; - if let Some(message) = eval_reflection_non_instantiable_error_message(class_name, flags) { - return Ok(Some(EvalReflectionInstantiationError::ThrowableError(message))); - } - if flags & EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE == 0 { - return Ok(Some( - EvalReflectionInstantiationError::ReflectionException(format!( - "Access to non-public constructor of class {}", - class_name.trim_start_matches('\\') - )), - )); - } - Ok(None) -} - -/// Builds PHP's non-instantiable class-like message from ReflectionClass flags. -fn eval_reflection_non_instantiable_error_message(class_name: &str, flags: u64) -> Option { - let class_name = class_name.trim_start_matches('\\'); - if flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0 { - return Some(format!("Cannot instantiate abstract class {}", class_name)); - } - if flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { - return Some(format!("Cannot instantiate interface {}", class_name)); - } - if flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { - return Some(format!("Cannot instantiate trait {}", class_name)); - } - if flags & EVAL_REFLECTION_CLASS_FLAG_ENUM != 0 { - return Some(format!("Cannot instantiate enum {}", class_name)); - } - None -} - -/// Returns whether an absent or public AOT lifecycle method allows public reflection. -fn eval_reflection_aot_lifecycle_method_allows_public_reflection( - class_name: &str, - method_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { - return Ok(true); - }; - Ok(flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC != 0 - && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0) -} - -/// Returns AOT constructor access metadata when the constructor is not public. -pub(in crate::interpreter) fn eval_reflection_aot_non_public_constructor( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let Some(flags) = values.reflection_method_flags(runtime_class_name, "__construct")? else { - return Ok(None); - }; - let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - return Ok(None); - }; - let declaring_class = values - .reflection_method_declaring_class(runtime_class_name, "__construct")? - .unwrap_or_else(|| runtime_class_name.to_string()); - Ok(Some((declaring_class, visibility))) -} - -/// Builds an eval-backed `ReflectionFunction` object for eval or registered native functions. -fn eval_reflection_function_new( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args(&[String::from("function")], evaluated_args)?; - let closure_target = eval_reflection_function_closure_target_arg(args[0], context, values)?; - let requested_name = match closure_target.as_ref() { - Some(target) => eval_reflection_function_closure_target_name(target), - None => eval_reflection_function_name_arg(args[0], context, values)?, - }; - let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); - if let Some(closure) = context.closure(&requested_name).cloned() { - let function = closure.function(); - let required_parameter_count = eval_reflection_required_parameter_count( - function.parameter_defaults(), - function.parameter_is_variadic(), - ); - let parameters = eval_reflection_function_parameters( - function.name(), - function.params(), - function.attributes().to_vec(), - function.parameter_attributes(), - function.parameter_types(), - function.parameter_defaults(), - function.parameter_is_by_ref(), - function.parameter_is_variadic(), - ); - return eval_reflection_function_object_result( - &requested_name, - function.attributes(), - ¶meters, - required_parameter_count, - context, - values, - ) - .and_then(|object| { - eval_reflection_attach_function_closure_target( - object, - closure_target, - context, - values, - ) - }) - .map(Some); - } - if let Some(function) = context.function(&lookup_name).cloned() { - let required_parameter_count = eval_reflection_required_parameter_count( - function.parameter_defaults(), - function.parameter_is_variadic(), - ); - let parameters = eval_reflection_function_parameters( - function.name(), - function.params(), - function.attributes().to_vec(), - function.parameter_attributes(), - function.parameter_types(), - function.parameter_defaults(), - function.parameter_is_by_ref(), - function.parameter_is_variadic(), - ); - return eval_reflection_function_object_result( - function.name(), - function.attributes(), - ¶meters, - required_parameter_count, - context, - values, - ) - .and_then(|object| { - eval_reflection_attach_function_closure_target( - object, - closure_target, - context, - values, - ) - }) - .map(Some); - } - if let Some(function) = context.native_function(&lookup_name) { - let reflected_name = requested_name.trim_start_matches('\\'); - let required_parameter_count = function.required_param_count(); - let parameters = eval_reflection_native_function_parameters(reflected_name, &function); - return eval_reflection_function_object_result( - reflected_name, - &[], - ¶meters, - required_parameter_count, - context, - values, - ) - .and_then(|object| { - eval_reflection_attach_function_closure_target( - object, - closure_target, - context, - values, - ) - }) - .map(Some); - } - if closure_target.is_some() { - return eval_reflection_function_object_result( - &requested_name, - &[], - &[], - 0, - context, - values, - ) - .and_then(|object| { - eval_reflection_attach_function_closure_target( - object, - closure_target, - context, - values, - ) - }) - .map(Some); - } - Ok(None) -} - -/// Returns the retained callable target when a ReflectionFunction argument is a Closure object. -fn eval_reflection_function_closure_target_arg( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.type_tag(value)? != EVAL_TAG_OBJECT { - return Ok(None); - } - let identity = values.object_identity(value)?; - Ok(context.closure_object_target(identity).cloned()) -} - -/// Returns the function-like name exposed for a Closure-backed ReflectionFunction. -fn eval_reflection_function_closure_target_name(target: &EvalClosureObjectTarget) -> String { - match target { - EvalClosureObjectTarget::Named(name) - | EvalClosureObjectTarget::BoundNamed { name, .. } => name.clone(), - EvalClosureObjectTarget::InvokableObject { .. } => String::from("__invoke"), - EvalClosureObjectTarget::ObjectMethod { method, .. } - | EvalClosureObjectTarget::StaticMethod { method, .. } => method.clone(), - } -} - -/// Attaches original Closure target metadata to a synthetic ReflectionFunction object. -fn eval_reflection_attach_function_closure_target( - object: RuntimeCellHandle, - closure_target: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(closure_target) = closure_target else { - return Ok(object); - }; - let identity = values.object_identity(object)?; - context.register_eval_reflection_function_closure_target(identity, closure_target); - Ok(object) -} - -/// Returns parameter names for a registered native function, filling missing bridge names. -fn eval_reflection_native_function_parameter_names(function: &NativeFunction) -> Vec { - (0..function.param_count()) - .map(|index| { - function - .param_names() - .get(index) - .filter(|name| !name.is_empty()) - .cloned() - .unwrap_or_else(|| format!("arg{}", index)) - }) - .collect() -} - -/// Builds ReflectionParameter metadata for one registered native AOT function. -fn eval_reflection_native_function_parameters( - function_name: &str, - function: &NativeFunction, -) -> Vec { - let parameter_names = eval_reflection_native_function_parameter_names(function); - let parameter_count = parameter_names.len(); - let parameter_attributes = vec![Vec::new(); parameter_count]; - let parameter_types = eval_reflection_native_function_parameter_types(function); - let parameter_defaults = eval_reflection_native_function_parameter_defaults(function); - let parameter_is_by_ref = (0..parameter_count) - .map(|index| function.param_by_ref(index)) - .collect::>(); - let parameter_is_variadic = (0..parameter_count) - .map(|index| function.param_variadic(index)) - .collect::>(); - eval_reflection_function_parameters( - function_name, - ¶meter_names, - Vec::new(), - ¶meter_attributes, - ¶meter_types, - ¶meter_defaults, - ¶meter_is_by_ref, - ¶meter_is_variadic, - ) -} - -/// Converts registered native function parameter types into reflection metadata input. -fn eval_reflection_native_function_parameter_types( - function: &NativeFunction, -) -> Vec> { - (0..function.param_count()) - .map(|index| function.param_type(index).cloned()) - .collect() -} - -/// Converts registered native function defaults into eval constant expressions. -fn eval_reflection_native_function_parameter_defaults( - function: &NativeFunction, -) -> Vec> { - (0..function.param_count()) - .map(|index| { - function - .param_default(index) - .map(eval_reflection_native_callable_default_expr) - }) - .collect() -} - -/// Builds one `ReflectionFunction` object from retained eval function metadata. -fn eval_reflection_function_object_result( - function_name: &str, - attributes: &[EvalAttribute], - parameters: &[EvalReflectionParameterMetadata], - required_parameter_count: usize, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_FUNCTION, - function_name, - attributes, - &[], - &[], - &[], - &[], - None, - parameters, - None, - None, - None, - None, - eval_reflection_callable_flags(attributes), - required_parameter_count as u64, - 0, - None, - None, - context, - values, - ) -} - -/// Builds an eval-backed `ReflectionParameter` object for a function or method parameter. -fn eval_reflection_parameter_new( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args( - &[String::from("function"), String::from("param")], - evaluated_args, - )?; - let selector = eval_reflection_parameter_selector(args[1], values)?; - let Some(parameter) = - eval_reflection_parameter_constructor_metadata(args[0], selector.clone(), context, values)? - else { - return eval_reflection_parameter_constructor_error(args[0], &selector, context, values); - }; - eval_reflection_parameter_object_result(¶meter, context, values).map(Some) -} - -/// Throws the PHP constructor error for eval-backed `ReflectionParameter` misses. -fn eval_reflection_parameter_constructor_error( - target: RuntimeCellHandle, - selector: &EvalReflectionParameterSelector, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.is_array_like(target)? { - return eval_reflection_method_parameter_constructor_error(target, selector, context, values); - } - if values.type_tag(target)? == EVAL_TAG_STRING { - return eval_reflection_function_parameter_constructor_error( - target, selector, context, values, - ); - } - Err(EvalStatus::RuntimeFatal) -} - -/// Throws the PHP constructor error for eval-backed function parameter misses. -fn eval_reflection_function_parameter_constructor_error( - target: RuntimeCellHandle, - selector: &EvalReflectionParameterSelector, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let requested_name = eval_reflection_string_arg(target, values)?; - let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); - if context.function(&lookup_name).is_some() || context.native_function(&lookup_name).is_some() { - return eval_reflection_parameter_selector_error(selector, context, values); - } - Ok(None) -} - -/// Throws the PHP constructor error for eval-backed method parameter misses. -fn eval_reflection_method_parameter_constructor_error( - target: RuntimeCellHandle, - selector: &EvalReflectionParameterSelector, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.array_len(target)? != 2 { - return Err(EvalStatus::RuntimeFatal); - } - let zero = values.int(0)?; - let one = values.int(1)?; - let receiver = values.array_get(target, zero)?; - let method = values.array_get(target, one)?; - let method_name = eval_reflection_string_arg(method, values)?; - let class_name = match values.type_tag(receiver)? { - EVAL_TAG_OBJECT => eval_reflection_object_class_name(receiver, context, values)?, - EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - let reflected_name = context - .resolve_class_like_name(&class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - if eval_reflection_class_like_exists(&reflected_name, context) { - let Some(reflected_method_name) = eval_reflection_member_name( - EVAL_REFLECTION_OWNER_METHOD, - &reflected_name, - &method_name, - context, - ) else { - return eval_throw_reflection_exception( - &format!("Method {}::{}() does not exist", reflected_name, method_name), - context, - values, - ); - }; - if eval_reflection_method_metadata(&reflected_name, &reflected_method_name, context) - .is_some() - { - return eval_reflection_parameter_selector_error(selector, context, values); - } - return Err(EvalStatus::RuntimeFatal); - } - if eval_reflection_aot_method_metadata_with_signature_if_exists( - &reflected_name, - &method_name, - context, - values, - )? - .is_some() - { - return eval_reflection_parameter_selector_error(selector, context, values); - } - Ok(None) -} - -/// Throws PHP's selector-specific ReflectionParameter constructor error. -fn eval_reflection_parameter_selector_error( - selector: &EvalReflectionParameterSelector, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - match selector { - EvalReflectionParameterSelector::Name(_) => eval_throw_reflection_exception( - "The parameter specified by its name could not be found", - context, - values, - ), - EvalReflectionParameterSelector::Position(position) if *position < 0 => { - eval_throw_value_error( - "ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0", - context, - values, - ) - } - EvalReflectionParameterSelector::Position(_) => eval_throw_reflection_exception( - "The parameter specified by its offset could not be found", - context, - values, - ), - } -} - -/// Resolves `ReflectionParameter` constructor target metadata. -fn eval_reflection_parameter_constructor_metadata( - target: RuntimeCellHandle, - selector: EvalReflectionParameterSelector, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.is_array_like(target)? { - return eval_reflection_method_parameter_metadata(target, selector, context, values); - } - if values.type_tag(target)? == EVAL_TAG_STRING { - return eval_reflection_function_parameter_metadata(target, selector, context, values); - } - Err(EvalStatus::RuntimeFatal) -} - -/// Builds selected parameter metadata for an eval or native free function. -fn eval_reflection_function_parameter_metadata( - target: RuntimeCellHandle, - selector: EvalReflectionParameterSelector, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let requested_name = eval_reflection_string_arg(target, values)?; - let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); - if let Some(function) = context.function(&lookup_name).cloned() { - let parameters = eval_reflection_function_parameters( - function.name(), - function.params(), - function.attributes().to_vec(), - function.parameter_attributes(), - function.parameter_types(), - function.parameter_defaults(), - function.parameter_is_by_ref(), - function.parameter_is_variadic(), - ); - return Ok(eval_reflection_parameter_for_selector(parameters, selector)); - } - if let Some(function) = context.native_function(&lookup_name) { - let reflected_name = requested_name.trim_start_matches('\\'); - let parameters = eval_reflection_native_function_parameters(reflected_name, &function); - return Ok(eval_reflection_parameter_for_selector(parameters, selector)); - } - Ok(None) -} - -/// Builds selected parameter metadata for an eval or generated/AOT method target. -fn eval_reflection_method_parameter_metadata( - target: RuntimeCellHandle, - selector: EvalReflectionParameterSelector, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.array_len(target)? != 2 { - return Err(EvalStatus::RuntimeFatal); - } - let zero = values.int(0)?; - let one = values.int(1)?; - let receiver = values.array_get(target, zero)?; - let method = values.array_get(target, one)?; - let method_name = eval_reflection_string_arg(method, values)?; - let class_name = match values.type_tag(receiver)? { - EVAL_TAG_OBJECT => eval_reflection_object_class_name(receiver, context, values)?, - EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, - _ => return Err(EvalStatus::RuntimeFatal), - }; - let reflected_name = context - .resolve_class_like_name(&class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - let member = if eval_reflection_class_like_exists(&reflected_name, context) { - let reflected_method_name = eval_reflection_member_name( - EVAL_REFLECTION_OWNER_METHOD, - &reflected_name, - &method_name, - context, - ); - let Some(reflected_method_name) = reflected_method_name else { - return Ok(None); - }; - let Some(method) = - eval_reflection_method_metadata(&reflected_name, &reflected_method_name, context) - else { - return Ok(None); - }; - method - } else { - let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( - &reflected_name, - &method_name, - context, - values, - )? - else { - return Ok(None); - }; - member - }; - Ok(eval_reflection_parameter_for_selector( - member.parameters, - selector, - )) -} - -/// Converts a `ReflectionParameter` selector runtime value to a supported selector. -fn eval_reflection_parameter_selector( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - match values.type_tag(value)? { - EVAL_TAG_STRING => { - eval_reflection_string_arg(value, values).map(EvalReflectionParameterSelector::Name) - } - EVAL_TAG_INT => { - eval_int_value(value, values).map(EvalReflectionParameterSelector::Position) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Selects a parameter by PHP name or zero-based position. -fn eval_reflection_parameter_for_selector( - parameters: Vec, - selector: EvalReflectionParameterSelector, -) -> Option { - match selector { - EvalReflectionParameterSelector::Name(name) => parameters - .into_iter() - .find(|parameter| parameter.name == name), - EvalReflectionParameterSelector::Position(position) if position >= 0 => { - parameters.into_iter().nth(position as usize) - } - EvalReflectionParameterSelector::Position(_) => None, - } -} - -/// Handles eval-backed `ReflectionMethod::createFromMethodName()` static calls. -pub(in crate::interpreter) fn eval_reflection_method_create_from_method_name_result( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !class_name - .trim_start_matches('\\') - .eq_ignore_ascii_case("ReflectionMethod") - || !method_name.eq_ignore_ascii_case("createFromMethodName") - { - return Ok(None); - } - let args = bind_evaluated_function_args(&[String::from("method")], evaluated_args)?; - let target = eval_reflection_string_arg(args[0], values)?; - let Some((class_name, method_name)) = eval_reflection_method_target_parts(&target) else { - return eval_throw_reflection_exception( - "ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name", - context, - values, - ); - }; - eval_reflection_method_object_result_or_throw( - &class_name, - &method_name, - context, - values, - ) -} - -/// Splits PHP's `ClassName::methodName` reflection-method target string. -fn eval_reflection_method_target_parts(target: &str) -> Option<(String, String)> { - let Some((class_name, method_name)) = target.rsplit_once("::") else { - return None; - }; - if class_name.is_empty() || method_name.is_empty() { - return None; - } - Some((class_name.to_string(), method_name.to_string())) -} - -/// Extracts the deprecated one-argument `ReflectionMethod("Class::method")` target. -fn eval_reflection_method_single_target_arg( - evaluated_args: Vec, -) -> Result { - let mut args = evaluated_args.into_iter(); - let Some(arg) = args.next() else { - return Err(EvalStatus::RuntimeFatal); - }; - if args.next().is_some() { - return Err(EvalStatus::RuntimeFatal); - } - if let Some(name) = arg.name.as_deref() { - if !matches!(name, "class_name" | "objectOrMethod") { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(arg.value) -} - -/// Builds a `ReflectionMethod` object when the reflected method exists in eval or AOT metadata. -fn eval_reflection_method_object_result_if_exists( - class_name: &str, - requested_method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let reflected_name = context - .resolve_class_like_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - if !eval_reflection_class_like_exists(&reflected_name, context) { - if let Some(method) = eval_reflection_aot_method_metadata_with_signature_if_exists( - &reflected_name, - requested_method_name, - context, - values, - )? { - let method_name = requested_method_name.to_ascii_lowercase(); - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_METHOD, - &method_name, - &method, - context, - values, - ) - .map(Some); - } - return Ok(None); - } - let method_name = eval_reflection_member_name( - EVAL_REFLECTION_OWNER_METHOD, - &reflected_name, - requested_method_name, - context, - ); - let Some(method_name) = method_name else { - return Ok(None); - }; - let Some(method) = eval_reflection_method_metadata(&reflected_name, &method_name, context) - else { - return Ok(None); - }; - eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_METHOD, - &method_name, - &method, - context, - values, - ) - .map(Some) -} - -/// Builds a `ReflectionMethod` object or throws PHP's catchable reflection error. -fn eval_reflection_method_object_result_or_throw( - class_name: &str, - requested_method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(result) = eval_reflection_method_object_result_if_exists( - class_name, - requested_method_name, - context, - values, - )? { - return Ok(Some(result)); - } - let reflected_name = context - .resolve_class_like_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { - return eval_throw_reflection_exception( - &format!("Class \"{}\" does not exist", reflected_name), - context, - values, - ); - } - eval_throw_reflection_exception( - &format!( - "Method {}::{}() does not exist", - reflected_name, requested_method_name - ), - context, - values, - ) -} - -/// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. -fn eval_reflection_method_new( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if evaluated_args.len() == 1 { - let target = eval_reflection_method_single_target_arg(evaluated_args)?; - let target = eval_reflection_string_arg(target, values)?; - let Some((class_name, method_name)) = eval_reflection_method_target_parts(&target) else { - return eval_throw_reflection_exception( - "ReflectionMethod::__construct(): Argument #1 ($objectOrMethod) must be a valid method name", - context, - values, - ); - }; - return eval_reflection_method_object_result_or_throw( - &class_name, - &method_name, - context, - values, - ); - } - let args = bind_evaluated_function_args( - &[String::from("class_name"), String::from("method_name")], - evaluated_args, - )?; - let class_name = eval_reflection_class_target_name(args[0], context, values)?; - let requested_method_name = eval_reflection_string_arg(args[1], values)?; - eval_reflection_method_object_result_or_throw( - &class_name, - &requested_method_name, - context, - values, - ) -} - -/// Builds an eval-backed `ReflectionProperty` object when the reflected property exists in eval. -fn eval_reflection_property_new( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args( - &[String::from("class_name"), String::from("property_name")], - evaluated_args, - )?; - let property_name = eval_reflection_string_arg(args[1], values)?; - if values.type_tag(args[0])? == EVAL_TAG_OBJECT { - return eval_reflection_property_new_for_object(args[0], &property_name, context, values); - } - let class_name = eval_reflection_string_arg(args[0], values)?; - eval_reflection_property_object_result_or_throw(&class_name, &property_name, context, values) -} - -/// Builds a `ReflectionProperty` object when the reflected property exists. -fn eval_reflection_property_object_result_if_exists( - class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let reflected_name = context - .resolve_class_like_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - if !eval_reflection_class_like_exists(&reflected_name, context) { - if let Some((declaring_class, property)) = - eval_reflection_native_interface_property_requirement( - &reflected_name, - property_name, - context, - ) - { - let property = eval_reflection_interface_property_metadata(declaring_class, &property); - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - property_name, - &property, - context, - values, - ) - .map(Some); - } - let Some(property) = eval_reflection_aot_property_metadata_if_exists( - &reflected_name, - property_name, - context, - values, - )? - else { - return Ok(None); - }; - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - property_name, - &property, - context, - values, - ) - .map(Some); - } - let Some(property) = eval_reflection_property_metadata(&reflected_name, property_name, context) - else { - return Ok(None); - }; - eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - property_name, - &property, - context, - values, - ) - .map(Some) -} - -/// Builds a `ReflectionProperty` object or throws PHP's catchable reflection error. -fn eval_reflection_property_object_result_or_throw( - class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(result) = eval_reflection_property_object_result_if_exists( - class_name, - property_name, - context, - values, - )? { - return Ok(Some(result)); - } - let reflected_name = context - .resolve_class_like_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { - return eval_throw_reflection_exception( - &format!("Class \"{}\" does not exist", reflected_name), - context, - values, - ); - } - let message = eval_reflection_missing_member_message( - EVAL_REFLECTION_OWNER_PROPERTY, - &reflected_name, - property_name, - ); - eval_throw_reflection_exception(&message, context, values) -} - -/// Builds a ReflectionProperty from an object argument, including dynamic properties. -fn eval_reflection_property_new_for_object( - object: RuntimeCellHandle, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let class_name = eval_reflection_object_class_name(object, context, values)?; - if let Some(property) = eval_reflection_property_metadata(&class_name, property_name, context) { - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - property_name, - &property, - context, - values, - ) - .map(Some); - } - if !eval_reflection_object_dynamic_property_exists(object, property_name, values)? { - let message = eval_reflection_missing_member_message( - EVAL_REFLECTION_OWNER_PROPERTY, - &class_name, - property_name, - ); - return eval_throw_reflection_exception(&message, context, values); - } - let property = eval_reflection_dynamic_property_metadata(&class_name); - eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - property_name, - &property, - context, - values, - ) - .map(Some) -} - -/// Returns the class name for an object passed to a Reflection constructor. -fn eval_reflection_object_class_name( - object: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - if let Some(class_name) = context.dynamic_object_class_name(identity) { - return Ok(class_name); - } - let class_name = values.object_class_name(object)?; - let bytes = values.string_bytes(class_name); - values.release(class_name)?; - let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(class_name.trim_start_matches('\\').to_string()) -} - -/// Returns whether one object has a public dynamic property by exact PHP name. -fn eval_reflection_object_dynamic_property_exists( - object: RuntimeCellHandle, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - if property_name.contains('\0') { - return Ok(false); - } - let property_count = values.object_property_len(object)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - if key_bytes? == property_name.as_bytes() { - return Ok(true); - } - } - Ok(false) -} - -/// Returns the object captured by a `ReflectionObject` instance, when present. -fn eval_reflection_object_reflected_object( - reflection_object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !eval_reflection_object_has_class(reflection_object, "ReflectionObject", values)? { - return Ok(None); - } - let object = eval_reflection_with_declaring_class_scope("ReflectionObject", context, |_| { - values.property_get(reflection_object, "__object") - })?; - if values.type_tag(object)? == EVAL_TAG_OBJECT { - Ok(Some(object)) - } else { - Ok(None) - } -} - -/// Appends dynamic public properties to `ReflectionObject::getProperties()` results. -fn eval_reflection_object_dynamic_property_array_result( - reflection_object: RuntimeCellHandle, - owner_kind: u64, - reflected_name: &str, - filter: Option, - mut result: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if owner_kind != EVAL_REFLECTION_OWNER_PROPERTY { - return Ok(result); - } - let Some(object) = eval_reflection_object_reflected_object(reflection_object, context, values)? - else { - return Ok(result); - }; - let property_names = - eval_reflection_object_dynamic_property_names(object, reflected_name, context, values); - values.release(object)?; - let property_names = property_names?; - for name in property_names { - let member = eval_reflection_dynamic_property_metadata(reflected_name); - if !eval_reflection_member_matches_filter(&member, filter) { - continue; - } - let member_object = eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_PROPERTY, - &name, - &member, - context, - values, - )?; - let next_index = values.array_len(result)? as i64; - let key = values.int(next_index)?; - result = values.array_set(result, key, member_object)?; - } - Ok(result) -} - -/// Enumerates public dynamic property names on the object behind `ReflectionObject`. -fn eval_reflection_object_dynamic_property_names( - object: RuntimeCellHandle, - reflected_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let property_count = values.object_property_len(object)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - let property_name = - String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; - if !eval_reflection_dynamic_property_name_is_visible( - reflected_name, - &property_name, - context, - ) { - continue; - } - if !names.iter().any(|name| name == &property_name) { - names.push(property_name); - } - } - Ok(names) -} - -/// Returns true when an object-storage name represents a public dynamic property. -fn eval_reflection_dynamic_property_name_is_visible( - reflected_name: &str, - property_name: &str, - context: &ElephcEvalContext, -) -> bool { - !property_name.contains('\0') - && eval_reflection_member_name( - EVAL_REFLECTION_OWNER_PROPERTY, - reflected_name, - property_name, - context, - ) - .is_none() -} - -/// Builds PHP reflection metadata for a public dynamic object property. -fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflectionMemberMetadata { - EvalReflectionMemberMetadata { - declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), - source_file: None, - source_location: None, - attributes: Vec::new(), - visibility: EvalVisibility::Public, - is_static: false, - is_final: false, - is_abstract: false, - is_readonly: false, - is_promoted: false, - is_dynamic: true, - modifiers: eval_reflection_property_modifiers( - EvalVisibility::Public, - None, - false, - false, - false, - false, - false, - ), - type_metadata: None, - settable_type_metadata: None, - return_type_metadata: None, - default_value: None, - default_value_trait_origin: None, - required_parameter_count: 0, - parameters: Vec::new(), - } -} - -/// Returns generated AOT ReflectionMethod metadata when the runtime table has a matching row. -fn eval_reflection_aot_method_metadata_if_exists( - class_name: &str, - method_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { - return Ok(None); - }; - let declaring_class_name = values - .reflection_method_declaring_class(runtime_class_name, method_name)? - .unwrap_or_else(|| runtime_class_name.to_string()); - Ok(Some(eval_reflection_aot_method_metadata( - &declaring_class_name, - method_name, - flags, - Vec::new(), - None, - None, - None, - ))) -} - -/// Returns generated AOT ReflectionMethod metadata with registered signature details. -fn eval_reflection_aot_method_metadata_with_signature_if_exists( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { - return Ok(None); - }; - let declaring_class_name = values - .reflection_method_declaring_class(runtime_class_name, method_name)? - .unwrap_or_else(|| runtime_class_name.to_string()); - let mut signature = - eval_reflection_aot_method_signature(&declaring_class_name, method_name, flags, context); - if signature.is_none() && declaring_class_name != runtime_class_name { - signature = - eval_reflection_aot_method_signature(runtime_class_name, method_name, flags, context); - } - let attributes = eval_reflection_aot_method_attributes( - runtime_class_name, - &declaring_class_name, - method_name, - context, - ); - let (source_file, source_location) = - eval_reflection_aot_method_source_metadata(flags, values)?; - Ok(Some(eval_reflection_aot_method_metadata( - &declaring_class_name, - method_name, - flags, - attributes, - source_file, - source_location, - signature.as_ref(), - ))) -} - -/// Returns AOT source-file and line metadata encoded in ReflectionMethod flags. -fn eval_reflection_aot_method_source_metadata( - flags: u64, - values: &mut impl RuntimeValueOps, -) -> Result<(Option, Option), EvalStatus> { - let Some(source_location) = eval_reflection_aot_method_source_location_from_flags(flags) else { - return Ok((None, None)); - }; - let Some(source_file) = values.reflection_source_file()? else { - return Ok((None, None)); - }; - Ok((Some(source_file), Some(source_location))) -} - -/// Decodes AOT ReflectionMethod source lines packed into high flag bits. -fn eval_reflection_aot_method_source_location_from_flags( - flags: u64, -) -> Option { - let start_line = - ((flags >> EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT) - & EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK) as i64; - let end_line = - ((flags >> EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT) - & EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK) as i64; - (start_line > 0 && end_line >= start_line) - .then(|| EvalSourceLocation::new(start_line, end_line)) -} - -/// Returns generated/AOT method dispatch metadata for interpreter-only runtime decisions. -pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata( - class_name: &str, - method_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - Ok( - eval_reflection_aot_method_metadata_if_exists(class_name, method_name, values)?.map( - |member| { - ( - member - .declaring_class_name - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()), - member.visibility, - member.is_static, - member.is_abstract, - ) - }, - ), - ) -} - -/// Converts AOT method flag metadata into the eval ReflectionMethod shape. -fn eval_reflection_aot_method_metadata( - class_name: &str, - method_name: &str, - flags: u64, - attributes: Vec, - source_file: Option, - source_location: Option, - signature: Option<&NativeCallableSignature>, -) -> EvalReflectionMemberMetadata { - let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - }; - let required_parameter_count = - signature.map_or(0, NativeCallableSignature::required_param_count); - let parameters = signature.map_or_else(Vec::new, |signature| { - eval_reflection_native_callable_parameters(class_name, method_name, flags, signature) - }); - let return_type_metadata = signature - .and_then(NativeCallableSignature::return_type) - .and_then(eval_reflection_parameter_type_metadata); - EvalReflectionMemberMetadata { - declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), - source_file, - source_location, - attributes, - visibility, - is_static: flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, - is_final: flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, - is_abstract: flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0, - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_method_modifiers_from_flags(flags), - type_metadata: None, - settable_type_metadata: None, - return_type_metadata, - default_value: None, - default_value_trait_origin: None, - required_parameter_count, - parameters, - } -} - -/// Returns registered generated/AOT method attributes for one reflected method. -fn eval_reflection_aot_method_attributes( - runtime_class_name: &str, - declaring_class_name: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Vec { - let attributes = context.native_method_attributes(declaring_class_name, method_name); - if !attributes.is_empty() || declaring_class_name == runtime_class_name { - return attributes; - } - context.native_method_attributes(runtime_class_name, method_name) -} - -/// Selects the registered native signature for an AOT method-like member. -fn eval_reflection_aot_method_signature( - class_name: &str, - method_name: &str, - flags: u64, - context: &ElephcEvalContext, -) -> Option { - if method_name.eq_ignore_ascii_case("__construct") { - return context.native_constructor_signature(class_name); - } - if flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0 { - context.native_static_method_signature(class_name, method_name) - } else { - context.native_method_signature(class_name, method_name) - } -} - -/// Builds ReflectionParameter metadata for one registered native AOT signature. -fn eval_reflection_native_callable_parameters( - declaring_class_name: &str, - method_name: &str, - flags: u64, - signature: &NativeCallableSignature, -) -> Vec { - let names = eval_reflection_native_callable_parameter_names(signature); - let parameter_count = names.len(); - let parameter_types = eval_reflection_native_callable_parameter_types(signature); - let has_type_flags = parameter_types - .iter() - .map(Option::is_some) - .collect::>(); - let parameter_attributes = vec![Vec::new(); parameter_count]; - let defaults = eval_reflection_native_callable_parameter_defaults(signature); - let by_ref_flags = (0..parameter_count) - .map(|index| signature.param_by_ref(index)) - .collect::>(); - let variadic_flags = (0..parameter_count) - .map(|index| signature.param_variadic(index)) - .collect::>(); - let declaring_function = EvalReflectionDeclaringFunctionMetadata { - name: method_name.to_ascii_lowercase(), - declaring_class_name: Some(declaring_class_name.trim_start_matches('\\').to_string()), - magic_scope: None, - attributes: Vec::new(), - flags, - required_parameter_count: signature.required_param_count(), - }; - eval_reflection_parameters_from_names_and_type_flags( - Some(declaring_class_name.trim_start_matches('\\')), - Some(&declaring_function), - &names, - &has_type_flags, - ¶meter_types, - ¶meter_attributes, - &defaults, - &by_ref_flags, - &variadic_flags, - &[], - ) -} - -/// Returns declared parameter type metadata for a registered native callable. -fn eval_reflection_native_callable_parameter_types( - signature: &NativeCallableSignature, -) -> Vec> { - (0..signature.param_count()) - .map(|index| signature.param_type(index).cloned()) - .collect() -} - -/// Returns parameter names for a registered native callable, filling missing bridge names. -fn eval_reflection_native_callable_parameter_names( - signature: &NativeCallableSignature, -) -> Vec { - (0..signature.param_count()) - .map(|index| { - signature - .param_names() - .get(index) - .filter(|name| !name.is_empty()) - .cloned() - .unwrap_or_else(|| format!("arg{}", index)) - }) - .collect() -} - -/// Converts registered scalar native defaults into eval constant expressions. -fn eval_reflection_native_callable_parameter_defaults( - signature: &NativeCallableSignature, -) -> Vec> { - (0..signature.param_count()) - .map(|index| { - signature - .param_default(index) - .map(eval_reflection_native_callable_default_expr) - }) - .collect() -} - -/// Converts one registered native default into an eval constant expression. -fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) -> EvalExpr { - match default { - NativeCallableDefault::Null => EvalExpr::Const(EvalConst::Null), - NativeCallableDefault::Bool(value) => EvalExpr::Const(EvalConst::Bool(*value)), - NativeCallableDefault::Int(value) => EvalExpr::Const(EvalConst::Int(*value)), - NativeCallableDefault::Float(value) => EvalExpr::Const(EvalConst::Float(*value)), - NativeCallableDefault::String(value) => EvalExpr::Const(EvalConst::String(value.clone())), - NativeCallableDefault::EmptyArray => EvalExpr::Array(Vec::new()), - NativeCallableDefault::Array(elements) => EvalExpr::Array( - elements - .iter() - .map(eval_reflection_native_callable_default_array_element) - .collect(), - ), - NativeCallableDefault::Object { class_name, args } => EvalExpr::NewObject { - class_name: class_name.clone(), - args: args - .iter() - .map(eval_reflection_native_callable_default_arg) - .collect(), - }, - } -} - -/// Converts one native array-default element into an eval array literal element. -fn eval_reflection_native_callable_default_array_element( - element: &NativeCallableArrayDefaultElement, -) -> EvalArrayElement { - let value = eval_reflection_native_callable_default_expr(&element.value); - match &element.key { - Some(NativeCallableArrayDefaultKey::Int(key)) => EvalArrayElement::KeyValue { - key: EvalExpr::Const(EvalConst::Int(*key)), - value, - }, - Some(NativeCallableArrayDefaultKey::String(key)) => EvalArrayElement::KeyValue { - key: EvalExpr::Const(EvalConst::String(key.clone())), - value, - }, - None => EvalArrayElement::Value(value), - } -} - -/// Converts one native object-default constructor argument into an eval call arg. -fn eval_reflection_native_callable_default_arg(arg: &NativeCallableObjectDefaultArg) -> EvalCallArg { - let value = eval_reflection_native_callable_default_expr(&arg.value); - match &arg.name { - Some(name) => EvalCallArg::named(name, value), - None => EvalCallArg::positional(value), - } -} - -/// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. -fn eval_reflection_aot_property_metadata_if_exists( - class_name: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { - return Ok(None); - }; - let declaring_class_name = values - .reflection_property_declaring_class(runtime_class_name, property_name)? - .unwrap_or_else(|| runtime_class_name.to_string()); - let type_metadata = eval_reflection_aot_property_type_metadata( - runtime_class_name, - &declaring_class_name, - property_name, - context, - ); - let default_value = eval_reflection_aot_property_default_value( - runtime_class_name, - &declaring_class_name, - property_name, - context, - ); - let attributes = eval_reflection_aot_property_attributes( - runtime_class_name, - &declaring_class_name, - property_name, - context, - ); - Ok(Some(eval_reflection_aot_property_metadata( - &declaring_class_name, - flags, - attributes, - type_metadata, - default_value, - ))) -} - -/// Returns generated/AOT property access metadata for an exact class/property pair. -pub(in crate::interpreter) fn eval_reflection_aot_property_access_metadata( - class_name: &str, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { - return Ok(None); - }; - let declaring_class = values - .reflection_property_declaring_class(runtime_class_name, property_name)? - .unwrap_or_else(|| runtime_class_name.to_string()); - Ok(Some(eval_reflection_aot_property_access_metadata_from_flags( - declaring_class, - flags, - ))) -} - -/// Returns generated/AOT static property metadata from a class or native parent chain. -pub(in crate::interpreter) fn eval_reflection_aot_static_property_access_metadata( - class_name: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut current = class_name.trim_start_matches('\\').to_string(); - let mut seen = std::collections::HashSet::new(); - loop { - if !seen.insert(current.to_ascii_lowercase()) { - return Ok(None); - } - if let Some(metadata) = - eval_reflection_aot_property_access_metadata(¤t, property_name, values)? - { - return Ok(Some(metadata)); - } - let Some(parent) = context.native_class_parent(¤t) else { - return Ok(None); - }; - current = parent.to_string(); - } -} - -/// Converts AOT ReflectionProperty flags into access-check metadata. -fn eval_reflection_aot_property_access_metadata_from_flags( - declaring_class: String, - flags: u64, -) -> (String, EvalVisibility, EvalVisibility, bool) { - let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - }; - let write_visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { - EvalVisibility::Protected - } else { - visibility - }; - ( - declaring_class, - visibility, - write_visibility, - flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, - ) -} - -/// Returns registered generated/AOT property type metadata for one reflected property. -fn eval_reflection_aot_property_type_metadata( - runtime_class_name: &str, - declaring_class_name: &str, - property_name: &str, - context: &ElephcEvalContext, -) -> Option { - context - .native_property_type(declaring_class_name, property_name) - .or_else(|| context.native_property_type(runtime_class_name, property_name)) - .as_ref() - .and_then(eval_reflection_parameter_type_metadata) -} - -/// Returns registered generated/AOT property default metadata for one reflected property. -fn eval_reflection_aot_property_default_value( - runtime_class_name: &str, - declaring_class_name: &str, - property_name: &str, - context: &ElephcEvalContext, -) -> Option { - context - .native_property_default(declaring_class_name, property_name) - .or_else(|| context.native_property_default(runtime_class_name, property_name)) - .as_ref() - .map(eval_reflection_native_callable_default_expr) -} - -/// Returns registered generated/AOT property attributes for one reflected property. -fn eval_reflection_aot_property_attributes( - runtime_class_name: &str, - declaring_class_name: &str, - property_name: &str, - context: &ElephcEvalContext, -) -> Vec { - let attributes = context.native_property_attributes(declaring_class_name, property_name); - if !attributes.is_empty() || declaring_class_name == runtime_class_name { - return attributes; - } - context.native_property_attributes(runtime_class_name, property_name) -} - -/// Converts AOT property flag metadata into the eval ReflectionProperty shape. -fn eval_reflection_aot_property_metadata( - class_name: &str, - flags: u64, - attributes: Vec, - type_metadata: Option, - default_value: Option, -) -> EvalReflectionMemberMetadata { - let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - }; - let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - let is_final = flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0; - let is_abstract = flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0; - let is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; - let is_virtual = flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL != 0; - let mut modifiers = eval_reflection_property_modifiers( - visibility, - None, - is_static, - is_final, - is_abstract, - is_readonly, - is_virtual, - ); - if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { - modifiers |= 32 | 4096; - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { - modifiers |= 2048; - } - let settable_type_metadata = type_metadata.clone(); - EvalReflectionMemberMetadata { - declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), - source_file: None, - source_location: None, - attributes, - visibility, - is_static, - is_final, - is_abstract, - is_readonly, - is_promoted: flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED != 0, - is_dynamic: false, - modifiers, - type_metadata, - settable_type_metadata, - return_type_metadata: None, - default_value, - default_value_trait_origin: None, - required_parameter_count: 0, - parameters: Vec::new(), - } -} - -/// Builds an eval-backed `ReflectionClassConstant` object for a class constant or enum case. -fn eval_reflection_class_constant_new( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args( - &[String::from("class_name"), String::from("constant_name")], - evaluated_args, - )?; - let class_name = eval_reflection_string_arg(args[0], values)?; - let constant_name = eval_reflection_string_arg(args[1], values)?; - eval_reflection_class_constant_object_result_or_throw( - &class_name, - &constant_name, - context, - values, - ) -} - -/// Builds a `ReflectionClassConstant` object or throws PHP's catchable error. -fn eval_reflection_class_constant_object_result_or_throw( - class_name: &str, - constant_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class_name, attributes, visibility, is_final, is_enum_case)) = - eval_reflection_class_constant_metadata(class_name, constant_name, context, values)? - else { - return eval_reflection_missing_class_constant_exception( - class_name, - constant_name, - context, - values, - ); - }; - let constant_value = eval_reflection_constant_value(class_name, constant_name, context, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); - if is_enum_case { - flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; - } - let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); - eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_CLASS_CONSTANT, - constant_name, - &attributes, - &[], - &[], - &[], - &[], - Some(&declaring_class_name), - &[], - None, - None, - None, - None, - flags, - modifiers, - 0, - Some(constant_value), - None, - context, - values, - ) - .map(Some) -} - -/// Throws the catchable ReflectionException for a missing class-like constant. -fn eval_reflection_missing_class_constant_exception( - class_name: &str, - constant_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let reflected_name = context - .resolve_class_like_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { - return eval_throw_reflection_exception( - &format!("Class \"{}\" does not exist", reflected_name), - context, - values, - ); - } - eval_throw_reflection_exception( - &format!("Constant {}::{} does not exist", reflected_name, constant_name), - context, - values, - ) -} - -/// Builds an eval-backed ReflectionEnumUnitCase/BackedCase object for an enum case. -fn eval_reflection_enum_case_new( - owner_kind: u64, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args( - &[String::from("class_name"), String::from("constant_name")], - evaluated_args, - )?; - let enum_name = eval_reflection_string_arg(args[0], values)?; - let case_name = eval_reflection_string_arg(args[1], values)?; - let Some(enum_decl) = context.enum_decl(&enum_name) else { - if eval_reflection_class_constant_metadata(&enum_name, &case_name, context, values)? - .is_some() - { - return eval_reflection_not_enum_case_exception( - &enum_name, &case_name, context, values, - ); - } - if !eval_reflection_class_like_exists(&enum_name, context) - && eval_reflection_class_like_or_runtime_exists(&enum_name, context, values)? - { - return Ok(None); - } - return eval_reflection_missing_class_constant_exception( - &enum_name, &case_name, context, values, - ); - }; - let declaring_class_name = enum_decl.name().to_string(); - let has_case = enum_decl.case(&case_name).is_some(); - let is_backed = enum_decl.backing_type().is_some(); - if !has_case { - if eval_reflection_class_constant_metadata( - &declaring_class_name, - &case_name, - context, - values, - )? - .is_some() - { - return eval_reflection_not_enum_case_exception( - &declaring_class_name, - &case_name, - context, - values, - ); - } - return eval_reflection_missing_class_constant_exception( - &declaring_class_name, - &case_name, - context, - values, - ); - } - if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && !is_backed { - return eval_throw_reflection_exception( - &format!("Enum case {}::{} is not a backed case", declaring_class_name, case_name), - context, - values, - ); - } - eval_reflection_enum_case_object_result( - owner_kind, - &declaring_class_name, - &case_name, - context, - values, - ) - .map(Some) -} - -/// Throws the catchable ReflectionException for a constant that is not an enum case. -fn eval_reflection_not_enum_case_exception( - class_name: &str, - constant_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let reflected_name = context - .resolve_class_like_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - eval_throw_reflection_exception( - &format!("Constant {}::{} is not a case", reflected_name, constant_name), - context, - values, - ) -} - -/// Builds one eval-backed enum-case reflection owner object. -fn eval_reflection_enum_case_object_result( - owner_kind: u64, - enum_name: &str, - case_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (declaring_class_name, attributes, is_backed) = { - let enum_decl = context.enum_decl(enum_name).ok_or(EvalStatus::RuntimeFatal)?; - let case = enum_decl.case(case_name).ok_or(EvalStatus::RuntimeFatal)?; - ( - enum_decl.name().to_string(), - case.attributes().to_vec(), - enum_decl.backing_type().is_some(), - ) - }; - if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && !is_backed { - return Err(EvalStatus::RuntimeFatal); - } - let case_value = context - .enum_case(&declaring_class_name, case_name) - .ok_or(EvalStatus::RuntimeFatal)?; - let backing_value = if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { - Some( - context - .enum_case_value(&declaring_class_name, case_name) - .ok_or(EvalStatus::RuntimeFatal)?, - ) - } else { - None - }; - let flags = eval_reflection_member_flags(EvalVisibility::Public, false, false, false, false) - | EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; - let modifiers = eval_reflection_class_constant_modifiers(EvalVisibility::Public, false); - eval_reflection_owner_object( - owner_kind, - case_name, - &attributes, - &[], - &[], - &[], - &[], - Some(&declaring_class_name), - &[], - None, - None, - None, - None, - flags, - modifiers, - 0, - Some(case_value), - backing_value, - context, - values, - ) -} - -/// Selects the concrete enum-case reflector class for one enum. -fn eval_reflection_enum_case_owner_kind( - enum_name: &str, - context: &ElephcEvalContext, -) -> Result { - let enum_decl = context.enum_decl(enum_name).ok_or(EvalStatus::RuntimeFatal)?; - Ok(if enum_decl.backing_type().is_some() { - EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE - } else { - EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE - }) -} - -/// Builds `ReflectionNamedType` metadata for an enum backing type. -fn eval_reflection_enum_backing_type_metadata( - backing_type: EvalEnumBackingType, -) -> EvalReflectionParameterTypeMetadata { - let name = match backing_type { - EvalEnumBackingType::Int => "int", - EvalEnumBackingType::String => "string", - }; - EvalReflectionParameterTypeMetadata { - kind: EvalReflectionParameterTypeKind::Named(eval_reflection_builtin_named_type( - name, false, - )), - } -} - -/// Materializes one Reflection owner object and transfers the temporary attribute array. -fn eval_reflection_owner_object( - owner_kind: u64, - reflected_name: &str, - attributes: &[EvalAttribute], - interface_names: &[String], - trait_names: &[String], - method_names: &[String], - property_names: &[String], - parent_class_name: Option<&str>, - parameter_metadata: &[EvalReflectionParameterMetadata], - type_metadata: Option<&EvalReflectionParameterTypeMetadata>, - settable_type_metadata: Option<&EvalReflectionParameterTypeMetadata>, - default_value: Option<&EvalExpr>, - default_value_trait_origin: Option<&str>, - flags: u64, - modifiers: u64, - method_modifiers: u64, - constant_value: Option, - backing_value: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_reflection_owner_object_with_members( - owner_kind, - reflected_name, - attributes, - interface_names, - trait_names, - method_names, - property_names, - parent_class_name, - parameter_metadata, - type_metadata, - settable_type_metadata, - default_value, - default_value_trait_origin, - flags, - modifiers, - method_modifiers, - constant_value, - backing_value, - true, - context, - values, - ) -} - -/// Materializes one Reflection owner object with optional nested class member objects. -fn eval_reflection_owner_object_with_members( - owner_kind: u64, - reflected_name: &str, - attributes: &[EvalAttribute], - interface_names: &[String], - trait_names: &[String], - method_names: &[String], - property_names: &[String], - parent_class_name: Option<&str>, - parameter_metadata: &[EvalReflectionParameterMetadata], - type_metadata: Option<&EvalReflectionParameterTypeMetadata>, - settable_type_metadata: Option<&EvalReflectionParameterTypeMetadata>, - default_value: Option<&EvalExpr>, - default_value_trait_origin: Option<&str>, - flags: u64, - modifiers: u64, - method_modifiers: u64, - constant_value: Option, - backing_value: Option, - include_class_members: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let attrs = eval_reflection_attribute_array_result( - attributes, - eval_reflection_attribute_target(owner_kind), - context, - values, - )?; - let interface_names_array = eval_reflection_string_array_result(interface_names, values)?; - let trait_names_array = eval_reflection_string_array_result(trait_names, values)?; - let method_names_array = eval_reflection_string_array_result(method_names, values)?; - let property_names_array = eval_reflection_string_array_result(property_names, values)?; - let class_metadata_owner = eval_reflection_owner_uses_class_metadata(owner_kind); - let is_eval_class = class_metadata_owner - && eval_reflection_class_like_exists(reflected_name, context); - let method_objects = if class_metadata_owner && include_class_members { - if is_eval_class { - eval_reflection_member_object_array_result( - EVAL_REFLECTION_OWNER_METHOD, - reflected_name, - method_names, - None, - context, - values, - )? - } else { - eval_reflection_aot_member_object_array_result( - EVAL_REFLECTION_OWNER_METHOD, - reflected_name, - method_names, - None, - context, - values, - )? - } - } else if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION - ) { - eval_reflection_parameter_object_array_result(parameter_metadata, context, values)? - } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { - match type_metadata { - Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, - None => values.null()?, - } - } else { - values.array_new(0)? - }; - let property_objects = if class_metadata_owner && include_class_members { - if is_eval_class { - eval_reflection_member_object_array_result( - EVAL_REFLECTION_OWNER_PROPERTY, - reflected_name, - property_names, - None, - context, - values, - )? - } else { - eval_reflection_aot_member_object_array_result( - EVAL_REFLECTION_OWNER_PROPERTY, - reflected_name, - property_names, - None, - context, - values, - )? - } - } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { - match default_value { - Some(default) => eval_reflection_class_like_default_value( - parent_class_name, - default_value_trait_origin, - default, - context, - values, - )?, - None => values.null()?, - } - } else { - values.array_new(0)? - }; - let parent_class = eval_reflection_related_class_result( - owner_kind, - parent_class_name, - include_class_members, - context, - values, - )?; - let constructor = eval_reflection_constructor_object_result( - owner_kind, - reflected_name, - include_class_members, - context, - values, - )?; - let (constant_value_cell, release_constant_value) = if owner_kind - == EVAL_REFLECTION_OWNER_PROPERTY - { - match settable_type_metadata { - Some(type_metadata) => ( - eval_reflection_type_object_result(type_metadata, values)?, - true, - ), - None => (values.null()?, true), - } - } else { - match constant_value { - Some(value) => (value, false), - None => (values.null()?, true), - } - }; - let (backing_value_cell, release_backing_value) = match backing_value { - Some(value) => (value, false), - None => (values.null()?, true), - }; - let object = values.reflection_owner_new( - owner_kind, - reflected_name, - attrs, - interface_names_array, - trait_names_array, - method_names_array, - property_names_array, - method_objects, - property_objects, - parent_class, - flags, - modifiers, - method_modifiers, - constant_value_cell, - backing_value_cell, - constructor, - )?; - if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM - ) { - let identity = values.object_identity(object)?; - context.register_eval_reflection_class(identity, reflected_name); - } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - if let Some(declaring_class) = parent_class_name { - let identity = values.object_identity(object)?; - context.register_eval_reflection_method(identity, declaring_class, reflected_name); - } - } else if owner_kind == EVAL_REFLECTION_OWNER_FUNCTION { - let identity = values.object_identity(object)?; - context.register_eval_reflection_function(identity, reflected_name); - } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { - if let Some(declaring_class) = parent_class_name { - if flags & EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC != 0 { - let identity = values.object_identity(object)?; - context.register_eval_dynamic_reflection_property( - identity, - declaring_class, - reflected_name, - ); - } else { - let identity = values.object_identity(object)?; - context.register_eval_reflection_property( - identity, - declaring_class, - reflected_name, - ); - } - } - } else if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_CLASS_CONSTANT - | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE - | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE - ) { - if let Some(declaring_class) = parent_class_name { - let identity = values.object_identity(object)?; - context.register_eval_reflection_class_constant( - identity, - declaring_class, - reflected_name, - owner_kind, - ); - } - } - values.release(attrs)?; - values.release(interface_names_array)?; - values.release(trait_names_array)?; - values.release(method_names_array)?; - values.release(property_names_array)?; - values.release(method_objects)?; - values.release(property_objects)?; - values.release(parent_class)?; - values.release(constructor)?; - if release_constant_value { - values.release(constant_value_cell)?; - } - if release_backing_value { - values.release(backing_value_cell)?; - } - Ok(object) -} - -/// Builds the `ReflectionClass|false` value stored in parent or declaring-class slots. -fn eval_reflection_related_class_result( - owner_kind: u64, - related_class_name: Option<&str>, - include_class_members: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(related_class_name) = related_class_name else { - return values.bool_value(false); - }; - if eval_reflection_owner_uses_class_metadata(owner_kind) && include_class_members { - return eval_reflection_full_class_object_result(related_class_name, context, values); - } - if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_METHOD - | EVAL_REFLECTION_OWNER_PROPERTY - | EVAL_REFLECTION_OWNER_CLASS_CONSTANT - | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE - | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE - ) { - return eval_reflection_shallow_class_object_result(related_class_name, context, values); - } - values.bool_value(false) -} - -/// Builds a full `ReflectionClass` object for parent-class metadata. -fn eval_reflection_full_class_object_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if class_name - .trim_start_matches('\\') - .eq_ignore_ascii_case("Closure") - { - return eval_reflection_builtin_closure_class_object_result( - EVAL_REFLECTION_OWNER_CLASS, - context, - values, - ); - } - let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { - let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { - return values.bool_value(false); - }; - let runtime_class_name = class_name.trim_start_matches('\\'); - let method_names = eval_reflection_aot_member_names( - EVAL_REFLECTION_OWNER_METHOD, - runtime_class_name, - values, - )?; - let property_names = eval_reflection_aot_member_names( - EVAL_REFLECTION_OWNER_PROPERTY, - runtime_class_name, - values, - )?; - let interface_names = - eval_reflection_aot_class_interface_names(runtime_class_name, values)?; - let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; - let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; - let attributes = context.native_class_attributes(runtime_class_name); - return eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_CLASS, - runtime_class_name, - &attributes, - &interface_names, - &trait_names, - &method_names, - &property_names, - parent_class_name.as_deref(), - &[], - None, - None, - None, - None, - flags, - modifiers, - 0, - None, - None, - context, - values, - ); - }; - let interface_names = - eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; - let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; - eval_reflection_owner_object( - EVAL_REFLECTION_OWNER_CLASS, - &metadata.resolved_name, - &metadata.attributes, - &interface_names, - &metadata.trait_names, - &metadata.method_names, - &metadata.property_names, - metadata.parent_class_name.as_deref(), - &[], - None, - None, - None, - None, - flags, - metadata.modifiers, - 0, - None, - None, - context, - values, - ) -} - -/// Builds a shallow `ReflectionClass` object for member declaring-class metadata. -fn eval_reflection_shallow_class_object_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { - let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { - return values.bool_value(false); - }; - let interface_names = eval_reflection_aot_class_interface_names(class_name, values)?; - let trait_names = eval_reflection_aot_class_trait_names(class_name, values)?; - let attributes = context.native_class_attributes(class_name); - return eval_reflection_owner_object_with_members( - EVAL_REFLECTION_OWNER_CLASS, - class_name.trim_start_matches('\\'), - &attributes, - &interface_names, - &trait_names, - &[], - &[], - None, - &[], - None, - None, - None, - None, - flags, - modifiers, - 0, - None, - None, - false, - context, - values, - ); - }; - let interface_names = - eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; - let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; - eval_reflection_owner_object_with_members( - EVAL_REFLECTION_OWNER_CLASS, - &metadata.resolved_name, - &metadata.attributes, - &interface_names, - &metadata.trait_names, - &[], - &[], - None, - &[], - None, - None, - None, - None, - flags, - metadata.modifiers, - 0, - None, - None, - false, - context, - values, - ) -} - -/// Returns the generated/AOT parent class name for a reflected class, if any. -fn eval_reflection_aot_parent_class_name( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let class_cell = values.string(runtime_class_name)?; - let parent_cell = match values.parent_class_name(class_cell) { - Ok(parent_cell) => parent_cell, - Err(err) => { - values.release(class_cell)?; - return Err(err); - } - }; - values.release(class_cell)?; - let parent_bytes = match values.string_bytes(parent_cell) { - Ok(parent_bytes) => parent_bytes, - Err(err) => { - values.release(parent_cell)?; - return Err(err); - } - }; - values.release(parent_cell)?; - let parent_name = String::from_utf8(parent_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - if parent_name.is_empty() { - Ok(None) - } else { - Ok(Some(parent_name)) - } -} - -/// Builds an indexed PHP string array for ReflectionClass metadata names. -fn eval_reflection_string_array_result( - names: &[String], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.string_array_new(names.len())?; - for name in names { - result = values.string_array_push(result, name)?; - } - Ok(result) -} - -/// Builds a string-keyed PHP associative array from owned string pairs. -fn eval_reflection_string_assoc_result( - pairs: Vec<(String, String)>, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(pairs.len())?; - for (key, value) in pairs { - let key = values.string(&key)?; - let value = values.string(&value)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Builds a name-keyed PHP array of full ReflectionClass objects. -fn eval_reflection_class_object_map_result( - names: &[String], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.assoc_new(names.len())?; - for name in names { - let key = values.string(name)?; - let object = eval_reflection_full_class_object_result(name, context, values)?; - result = values.array_set(result, key, object)?; - } - Ok(result) -} - -/// Maps a synthetic reflection owner kind to PHP's `Attribute::TARGET_*` bitmask. -fn eval_reflection_attribute_target(owner_kind: u64) -> u64 { - match owner_kind { - EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM => { - EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS - } - EVAL_REFLECTION_OWNER_FUNCTION => EVAL_REFLECTION_ATTRIBUTE_TARGET_FUNCTION, - EVAL_REFLECTION_OWNER_METHOD => EVAL_REFLECTION_ATTRIBUTE_TARGET_METHOD, - EVAL_REFLECTION_OWNER_PROPERTY => EVAL_REFLECTION_ATTRIBUTE_TARGET_PROPERTY, - EVAL_REFLECTION_OWNER_CLASS_CONSTANT - | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE - | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT, - _ => 0, - } -} - -/// Returns whether a synthetic owner stores `ReflectionClass`-style metadata. -fn eval_reflection_owner_uses_class_metadata(owner_kind: u64) -> bool { - matches!( - owner_kind, - EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT - ) -} - -/// Builds an indexed array of populated ReflectionParameter objects. -fn eval_reflection_parameter_object_array_result( - parameters: &[EvalReflectionParameterMetadata], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(parameters.len())?; - for parameter in parameters { - let parameter_object = eval_reflection_parameter_object_result(parameter, context, values)?; - let key = values.int(parameter.position as i64)?; - result = values.array_set(result, key, parameter_object)?; - } - Ok(result) -} - -/// Materializes one ReflectionParameter object through the shared reflection helper. -fn eval_reflection_parameter_object_result( - parameter: &EvalReflectionParameterMetadata, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let attrs = eval_reflection_attribute_array_result( - ¶meter.attributes, - EVAL_REFLECTION_ATTRIBUTE_TARGET_PARAMETER, - context, - values, - )?; - let declaring_function = match parameter.declaring_function.as_ref() { - Some(metadata) => { - eval_reflection_declaring_function_object_result(metadata, context, values)? - } - None => values.null()?, - }; - let trait_names = values.array_new(0)?; - let method_names = values.array_new(0)?; - let property_names = values.array_new(0)?; - let method_objects = values.array_new(0)?; - let parent_class = match parameter.declaring_class_name.as_deref() { - Some(declaring_class_name) => { - eval_reflection_shallow_class_object_result(declaring_class_name, context, values)? - } - None => values.null()?, - }; - let type_value = match parameter.type_metadata.as_ref() { - Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, - None => values.null()?, - }; - let class_value = eval_reflection_parameter_class_value(parameter, context, values)?; - let default_value = eval_reflection_parameter_default_value(parameter, context, values)?; - let default_value_constant_name = match parameter.default_value_constant_name.as_deref() { - Some(name) => values.string(name)?, - None => values.null()?, - }; - let constructor = values.null()?; - let flags = eval_reflection_parameter_flags(parameter); - let object = values.reflection_owner_new( - EVAL_REFLECTION_OWNER_PARAMETER, - ¶meter.name, - attrs, - declaring_function, - trait_names, - method_names, - property_names, - type_value, - default_value, - parent_class, - flags, - parameter.position as u64, - 0, - default_value_constant_name, - class_value, - constructor, - )?; - values.release(attrs)?; - values.release(declaring_function)?; - values.release(trait_names)?; - values.release(method_names)?; - values.release(property_names)?; - values.release(method_objects)?; - values.release(type_value)?; - values.release(default_value)?; - values.release(parent_class)?; - values.release(default_value_constant_name)?; - values.release(class_value)?; - values.release(constructor)?; - Ok(object) -} - -/// Materializes the legacy ReflectionParameter::getClass() value for known named object types. -fn eval_reflection_parameter_class_value( - parameter: &EvalReflectionParameterMetadata, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match eval_reflection_parameter_class_name(parameter) { - Some(class_name) => eval_reflection_shallow_class_object_result(class_name, context, values), - None => values.null(), - } -} - -/// Returns the retained object type name used by ReflectionParameter::getClass(). -fn eval_reflection_parameter_class_name( - parameter: &EvalReflectionParameterMetadata, -) -> Option<&str> { - match ¶meter.type_metadata.as_ref()?.kind { - EvalReflectionParameterTypeKind::Named(named_type) if !named_type.is_builtin => { - Some(named_type.name.as_str()) - } - _ => None, - } -} - -/// Materializes one ReflectionParameter default using declaring class and magic scopes. -fn eval_reflection_parameter_default_value( - parameter: &EvalReflectionParameterMetadata, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(default) = parameter.default_value.as_ref() else { - return values.null(); - }; - if let Some(class_name) = parameter.declaring_class_name.as_deref() { - context.push_class_scope(class_name.to_string()); - context.push_called_class_scope(class_name.to_string()); - } - let magic_scope = parameter - .declaring_function - .as_ref() - .and_then(|function| function.magic_scope.as_ref()); - if let Some(magic_scope) = magic_scope { - context.push_callable_magic_scope( - &magic_scope.function_name, - &magic_scope.method_name, - magic_scope.class_name.as_deref(), - magic_scope.trait_name.as_deref(), - ); - } - let result = eval_method_parameter_default(default, context, values); - if magic_scope.is_some() { - context.pop_magic_scope(); - } - if parameter.declaring_class_name.is_some() { - context.pop_called_class_scope(); - context.pop_class_scope(); - } - result -} - -/// Evaluates one reflected property default with its declaring class-like magic scope. -fn eval_reflection_member_default_value( - member: &EvalReflectionMemberMetadata, - default: &EvalExpr, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_reflection_class_like_default_value( - member.declaring_class_name.as_deref(), - member.default_value_trait_origin.as_deref(), - default, - context, - values, - ) -} - -/// Evaluates one class-like default expression with PHP `__CLASS__` and `__TRAIT__`. -fn eval_reflection_class_like_default_value( - declaring_class: Option<&str>, - trait_origin: Option<&str>, - default: &EvalExpr, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(declaring_class) = declaring_class else { - return eval_method_parameter_default(default, context, values); - }; - let trait_name = - trait_origin.or_else(|| context.has_trait(declaring_class).then_some(declaring_class)); - context.push_class_like_member_magic_scope(declaring_class, trait_name); - let result = eval_method_parameter_default(default, context, values); - context.pop_magic_scope(); - result -} - -/// Builds a shallow ReflectionMethod object for a parameter's declaring function metadata. -fn eval_reflection_declaring_function_object_result( - metadata: &EvalReflectionDeclaringFunctionMetadata, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let owner_kind = if metadata.declaring_class_name.is_some() { - EVAL_REFLECTION_OWNER_METHOD - } else { - EVAL_REFLECTION_OWNER_FUNCTION - }; - let method_modifiers = if metadata.declaring_class_name.is_some() { - eval_reflection_method_modifiers_from_flags(metadata.flags) - } else { - 0 - }; - eval_reflection_owner_object( - owner_kind, - &metadata.name, - &metadata.attributes, - &[], - &[], - &[], - &[], - metadata.declaring_class_name.as_deref(), - &[], - None, - None, - None, - None, - metadata.flags, - metadata.required_parameter_count as u64, - method_modifiers, - None, - None, - context, - values, - ) -} - -/// Materializes one parameter ReflectionType object through the shared reflection helper. -fn eval_reflection_type_object_result( - type_metadata: &EvalReflectionParameterTypeMetadata, - values: &mut impl RuntimeValueOps, -) -> Result { - match &type_metadata.kind { - EvalReflectionParameterTypeKind::Named(named_type) => { - eval_reflection_named_type_object_result(named_type, values) - } - EvalReflectionParameterTypeKind::Union(union_type) => { - eval_reflection_union_type_object_result(union_type, values) - } - EvalReflectionParameterTypeKind::Intersection(intersection_type) => { - eval_reflection_intersection_type_object_result(intersection_type, values) - } - } -} - -/// Materializes one ReflectionNamedType object through the shared reflection helper. -fn eval_reflection_named_type_object_result( - type_metadata: &EvalReflectionNamedTypeMetadata, - values: &mut impl RuntimeValueOps, -) -> Result { - let attrs = values.array_new(0)?; - let interface_names = values.array_new(0)?; - let trait_names = values.array_new(0)?; - let method_names = values.array_new(0)?; - let property_names = values.array_new(0)?; - let method_objects = values.array_new(0)?; - let property_objects = values.array_new(0)?; - let parent_class = values.bool_value(false)?; - let constant_value = values.null()?; - let backing_value = values.null()?; - let flags = eval_reflection_named_type_flags(type_metadata); - let object = values.reflection_owner_new( - EVAL_REFLECTION_OWNER_NAMED_TYPE, - &type_metadata.name, - attrs, - interface_names, - trait_names, - method_names, - property_names, - method_objects, - property_objects, - parent_class, - flags, - 0, - 0, - constant_value, - backing_value, - constant_value, - )?; - values.release(attrs)?; - values.release(interface_names)?; - values.release(trait_names)?; - values.release(method_names)?; - values.release(property_names)?; - values.release(method_objects)?; - values.release(property_objects)?; - values.release(parent_class)?; - values.release(constant_value)?; - values.release(backing_value)?; - Ok(object) -} - -/// Materializes one ReflectionUnionType object through the shared reflection helper. -fn eval_reflection_union_type_object_result( - type_metadata: &EvalReflectionUnionTypeMetadata, - values: &mut impl RuntimeValueOps, -) -> Result { - let attrs = values.array_new(0)?; - let interface_names = values.array_new(0)?; - let trait_names = values.array_new(0)?; - let method_names = values.array_new(0)?; - let property_names = values.array_new(0)?; - let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; - let property_objects = values.array_new(0)?; - let parent_class = values.bool_value(false)?; - let constant_value = values.null()?; - let backing_value = values.null()?; - let flags = eval_reflection_union_type_flags(type_metadata); - let object = values.reflection_owner_new( - EVAL_REFLECTION_OWNER_UNION_TYPE, - "", - attrs, - interface_names, - trait_names, - method_names, - property_names, - types, - property_objects, - parent_class, - flags, - 0, - 0, - constant_value, - backing_value, - constant_value, - )?; - values.release(attrs)?; - values.release(interface_names)?; - values.release(trait_names)?; - values.release(method_names)?; - values.release(property_names)?; - values.release(types)?; - values.release(property_objects)?; - values.release(parent_class)?; - values.release(constant_value)?; - values.release(backing_value)?; - Ok(object) -} - -/// Materializes one ReflectionIntersectionType object through the shared reflection helper. -fn eval_reflection_intersection_type_object_result( - type_metadata: &EvalReflectionIntersectionTypeMetadata, - values: &mut impl RuntimeValueOps, -) -> Result { - let attrs = values.array_new(0)?; - let interface_names = values.array_new(0)?; - let trait_names = values.array_new(0)?; - let method_names = values.array_new(0)?; - let property_names = values.array_new(0)?; - let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; - let property_objects = values.array_new(0)?; - let parent_class = values.bool_value(false)?; - let constant_value = values.null()?; - let backing_value = values.null()?; - let object = values.reflection_owner_new( - EVAL_REFLECTION_OWNER_INTERSECTION_TYPE, - "", - attrs, - interface_names, - trait_names, - method_names, - property_names, - types, - property_objects, - parent_class, - 0, - 0, - 0, - constant_value, - backing_value, - constant_value, - )?; - values.release(attrs)?; - values.release(interface_names)?; - values.release(trait_names)?; - values.release(method_names)?; - values.release(property_names)?; - values.release(types)?; - values.release(property_objects)?; - values.release(parent_class)?; - values.release(constant_value)?; - values.release(backing_value)?; - Ok(object) -} - -/// Builds an indexed array of populated ReflectionNamedType objects. -fn eval_reflection_named_type_object_array_result( - types: &[EvalReflectionNamedTypeMetadata], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(types.len())?; - for (position, type_metadata) in types.iter().enumerate() { - let type_object = eval_reflection_named_type_object_result(type_metadata, values)?; - let key = values.int(position as i64)?; - result = values.array_set(result, key, type_object)?; - } - Ok(result) -} - -/// Builds the `ReflectionMethod|null` value stored in ReflectionClass::__constructor. -fn eval_reflection_constructor_object_result( - owner_kind: u64, - class_name: &str, - include_class_members: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !eval_reflection_owner_uses_class_metadata(owner_kind) || !include_class_members { - return values.null(); - } - let Some(member) = eval_reflection_method_metadata(class_name, "__construct", context) else { - if !eval_reflection_class_like_exists(class_name, context) { - if let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( - class_name, - "__construct", - context, - values, - )? { - return eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_METHOD, - "__construct", - &member, - context, - values, - ); - } - } - return values.null(); - }; - eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_METHOD, - "__construct", - &member, - context, - values, - ) -} - -/// Materializes one eval-backed ReflectionMethod or ReflectionProperty object. -fn eval_reflection_member_object_result( - owner_kind: u64, - reflected_name: &str, - member: &EvalReflectionMemberMetadata, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut flags = eval_reflection_member_flags( - member.visibility, - member.is_static, - member.is_final, - member.is_abstract, - member.is_readonly, - ); - if member.default_value.is_some() { - flags |= EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE; - } - if member.is_promoted { - flags |= EVAL_REFLECTION_MEMBER_FLAG_PROMOTED; - } - if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 512) != 0 { - flags |= EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL; - } - if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 4096) != 0 { - flags |= EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET | EVAL_REFLECTION_MEMBER_FLAG_FINAL; - } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY - && (member.modifiers & 2048) != 0 - && !member.is_readonly - { - flags |= EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET; - } - if member.is_dynamic { - flags |= EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC; - } - if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - flags |= eval_reflection_callable_flags(&member.attributes); - } - let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - member.required_parameter_count as u64 - } else { - member.modifiers - }; - let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - member.modifiers - } else { - 0 - }; - eval_reflection_owner_object( - owner_kind, - reflected_name, - &member.attributes, - &[], - &[], - &[], - &[], - member.declaring_class_name.as_deref(), - &member.parameters, - member.type_metadata.as_ref(), - member.settable_type_metadata.as_ref(), - member.default_value.as_ref(), - member.default_value_trait_origin.as_deref(), - flags, - owner_modifiers, - method_modifiers, - None, - None, - context, - values, - ) -} - -/// Builds an indexed array of ReflectionMethod or ReflectionProperty objects for a ReflectionClass. -fn eval_reflection_member_object_array_result( - owner_kind: u64, - class_name: &str, - names: &[String], - filter: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(names.len())?; - let mut index = 0; - for name in names { - let Some(member) = eval_reflection_member_metadata(owner_kind, class_name, name, context) - else { - continue; - }; - if !eval_reflection_member_matches_filter(&member, filter) { - continue; - } - let member_object = - eval_reflection_member_object_result(owner_kind, name, &member, context, values)?; - let key = values.int(index)?; - result = values.array_set(result, key, member_object)?; - index += 1; - } - Ok(result) -} - -/// Builds an indexed array of AOT ReflectionMethod or ReflectionProperty objects for a class. -fn eval_reflection_aot_member_object_array_result( - owner_kind: u64, - class_name: &str, - names: &[String], - filter: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = values.array_new(names.len())?; - let mut index = 0; - for name in names { - let member = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - eval_reflection_aot_method_metadata_with_signature_if_exists( - class_name, name, context, values, - )? - } else { - eval_reflection_native_interface_property_requirement(class_name, name, context) - .map(|(declaring_class, property)| { - eval_reflection_interface_property_metadata(declaring_class, &property) - }) - .or(eval_reflection_aot_property_metadata_if_exists( - class_name, name, context, values, - )?) - }; - let Some(member) = member else { - continue; - }; - if !eval_reflection_member_matches_filter(&member, filter) { - continue; - } - let reflected_name = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - name.to_ascii_lowercase() - } else { - name.clone() - }; - let member_object = eval_reflection_member_object_result( - owner_kind, - &reflected_name, - &member, - context, - values, - )?; - let key = values.int(index)?; - result = values.array_set(result, key, member_object)?; - index += 1; - } - Ok(result) -} - -/// Returns true when a ReflectionClass member passes an optional modifier filter. -fn eval_reflection_member_matches_filter( - member: &EvalReflectionMemberMetadata, - filter: Option, -) -> bool { - match filter { - Some(filter) => member.modifiers & filter != 0, - None => true, - } -} - -/// Parses the optional ReflectionClass member filter argument. -fn eval_reflection_member_filter( - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut filter = None; - for arg in evaluated_args { - if let Some(name) = arg.name.as_deref() { - if name != "filter" { - return Err(EvalStatus::RuntimeFatal); - } - } - if filter.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - filter = Some(arg.value); - } - let Some(filter) = filter else { - return Ok(None); - }; - if values.is_null(filter)? { - return Ok(None); - } - let cast_filter = values.cast_int(filter)?; - let bytes = values.string_bytes(cast_filter)?; - values.release(cast_filter)?; - let text = std::str::from_utf8(&bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - text.parse::() - .map(|value| Some(value as u64)) - .map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Returns generated AOT member names for one reflected class. -fn eval_reflection_aot_member_names( - owner_kind: u64, - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let names_array = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - values.reflection_method_names(runtime_class_name)? - } else { - values.reflection_property_names(runtime_class_name)? - }; - let names = eval_reflection_string_array_to_vec(names_array, values)?; - values.release(names_array)?; - Ok(names) -} - -/// Returns generated AOT interface names for one reflected class-like symbol. -fn eval_reflection_aot_class_interface_names( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let names_array = values.reflection_class_interface_names(runtime_class_name)?; - let names = eval_reflection_string_array_to_vec(names_array, values)?; - values.release(names_array)?; - Ok(names) -} - -/// Returns eval metadata interface names expanded with generated/AOT ancestors. -fn eval_reflection_eval_metadata_interface_names( - metadata: &EvalReflectionClassMetadata, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if context.has_class(&metadata.resolved_name) || context.has_enum(&metadata.resolved_name) { - eval_reflection_eval_class_interface_names(&metadata.resolved_name, context, values) - } else if context.has_interface(&metadata.resolved_name) { - eval_reflection_eval_interface_parent_names(&metadata.resolved_name, context, values) - } else { - Ok(metadata.interface_names.clone()) - } -} - -/// Returns eval metadata flags corrected for generated/AOT inherited interfaces. -fn eval_reflection_eval_metadata_flags( - metadata: &EvalReflectionClassMetadata, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut flags = metadata.flags; - if flags & EVAL_REFLECTION_CLASS_FLAG_ITERABLE == 0 - && flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT == 0 - && context.has_class(&metadata.resolved_name) - && eval_reflection_interface_names_include_iterable( - &eval_reflection_eval_class_interface_names(&metadata.resolved_name, context, values)?, - ) - { - flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; - } - Ok(flags) -} - -/// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. -fn eval_reflection_eval_class_interface_names( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - if let Some(parent) = context.class_native_parent_name(class_name) { - for name in eval_reflection_aot_class_interface_names(&parent, values)? { - eval_reflection_push_unique_class_name(name, &mut names, &mut seen); - } - } - for name in context.class_interface_names(class_name) { - eval_reflection_push_unique_class_name(name.clone(), &mut names, &mut seen); - if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { - for parent in eval_reflection_aot_class_interface_names(&name, values)? { - eval_reflection_push_unique_class_name(parent, &mut names, &mut seen); - } - } - } - Ok(names) -} - -/// Returns eval interface parents plus inherited generated/AOT interface parents. -fn eval_reflection_eval_interface_parent_names( - interface_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut names = Vec::new(); - let mut seen = std::collections::HashSet::new(); - for name in context.interface_parent_names(interface_name) { - eval_reflection_push_unique_class_name(name.clone(), &mut names, &mut seen); - if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { - for parent in eval_reflection_aot_class_interface_names(&name, values)? { - eval_reflection_push_unique_class_name(parent, &mut names, &mut seen); - } - } - } - Ok(names) -} - -/// Returns whether one interface list includes PHP iterable marker interfaces. -fn eval_reflection_interface_names_include_iterable(interface_names: &[String]) -> bool { - interface_names.iter().any(|name| { - name.eq_ignore_ascii_case("Iterator") || name.eq_ignore_ascii_case("IteratorAggregate") - }) -} - -/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. -fn eval_reflection_push_unique_class_name( - name: String, - names: &mut Vec, - seen: &mut std::collections::HashSet, -) { - if seen.insert(name.to_ascii_lowercase()) { - names.push(name); - } -} - -/// Returns generated AOT trait names for one reflected class-like symbol. -fn eval_reflection_aot_class_trait_names( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let names_array = values.reflection_class_trait_names(runtime_class_name)?; - let names = eval_reflection_string_array_to_vec(names_array, values)?; - values.release(names_array)?; - Ok(names) -} - -/// Returns generated AOT trait aliases for one reflected class-like symbol. -fn eval_reflection_aot_class_trait_aliases( - class_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let runtime_class_name = class_name.trim_start_matches('\\'); - let alias_names_array = values.reflection_class_trait_alias_names(runtime_class_name)?; - let alias_names = eval_reflection_string_array_to_vec(alias_names_array, values)?; - values.release(alias_names_array)?; - let alias_sources_array = values.reflection_class_trait_alias_sources(runtime_class_name)?; - let alias_sources = eval_reflection_string_array_to_vec(alias_sources_array, values)?; - values.release(alias_sources_array)?; - if alias_names.len() != alias_sources.len() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(alias_names.into_iter().zip(alias_sources).collect()) -} - -/// Copies a runtime string array into Rust-owned strings for reflection metadata assembly. -fn eval_reflection_string_array_to_vec( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut result = Vec::with_capacity(len); - for position in 0..len { - let key = values.int(position as i64)?; - let value = values.array_get(array, key)?; - result.push(eval_reflection_string_arg(value, values)?); - } - Ok(result) -} - -/// Returns member metadata for one ReflectionClass member-array entry. -fn eval_reflection_member_metadata( - owner_kind: u64, - class_name: &str, - name: &str, - context: &ElephcEvalContext, -) -> Option { - match owner_kind { - EVAL_REFLECTION_OWNER_METHOD => eval_reflection_method_metadata(class_name, name, context), - EVAL_REFLECTION_OWNER_PROPERTY => { - eval_reflection_property_metadata(class_name, name, context) - } - _ => None, - } -} - -/// Returns the eval-retained class-like attributes plus canonical reflected name. -fn eval_reflection_class_like_attributes( - name: &str, - context: &ElephcEvalContext, -) -> Option { - if let Some(class) = context.class(name) { - let is_enum = context.has_enum(class.name()); - let mut flags = EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; - if class.is_final() { - flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL; - } - if class.is_abstract() { - flags |= EVAL_REFLECTION_CLASS_FLAG_ABSTRACT; - } - if is_enum { - flags |= EVAL_REFLECTION_CLASS_FLAG_ENUM; - } - if class.is_readonly_class() && !is_enum { - flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; - } - if eval_reflection_class_is_instantiable(class, is_enum, context) { - flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; - } - if eval_reflection_class_is_cloneable(class, is_enum, context) { - flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; - } - if eval_reflection_class_is_iterable(class, is_enum, context) { - flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; - } - if class.is_anonymous() { - flags |= EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS; - } - let modifiers = eval_reflection_class_modifiers( - class.is_final(), - class.is_abstract(), - class.is_readonly_class(), - is_enum, - ); - return Some(EvalReflectionClassMetadata { - resolved_name: class.name().trim_start_matches('\\').to_string(), - source_location: class.source_location(), - attributes: class.attributes().to_vec(), - interface_names: context.class_interface_names(class.name()), - trait_names: context.class_trait_names(class.name()), - method_names: context.class_method_names(class.name()), - property_names: context.class_property_names(class.name()), - parent_class_name: eval_reflection_parent_class_name(class, context), - flags, - modifiers, - }); - } - if let Some(interface) = context.interface(name) { - return Some(EvalReflectionClassMetadata { - resolved_name: interface.name().trim_start_matches('\\').to_string(), - source_location: interface.source_location(), - attributes: interface.attributes().to_vec(), - interface_names: context.interface_parent_names(interface.name()), - trait_names: Vec::new(), - method_names: context.interface_method_names(interface.name()), - property_names: context.interface_property_names(interface.name()), - parent_class_name: None, - flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, - modifiers: 0, - }); - } - if let Some(trait_decl) = context.trait_decl(name) { - return Some(EvalReflectionClassMetadata { - resolved_name: trait_decl.name().trim_start_matches('\\').to_string(), - source_location: trait_decl.source_location(), - attributes: trait_decl.attributes().to_vec(), - interface_names: Vec::new(), - trait_names: context.trait_trait_names(trait_decl.name()), - method_names: context.trait_method_names(trait_decl.name()), - property_names: context.trait_property_names(trait_decl.name()), - parent_class_name: None, - flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, - modifiers: 0, - }); - } - context - .enum_decl(name) - .map(|enum_decl| EvalReflectionClassMetadata { - resolved_name: enum_decl.name().trim_start_matches('\\').to_string(), - source_location: enum_decl.source_location(), - attributes: enum_decl.attributes().to_vec(), - interface_names: context.class_interface_names(enum_decl.name()), - trait_names: context.class_trait_names(enum_decl.name()), - method_names: context.class_method_names(enum_decl.name()), - property_names: context.class_property_names(enum_decl.name()), - parent_class_name: None, - flags: EVAL_REFLECTION_CLASS_FLAG_FINAL - | EVAL_REFLECTION_CLASS_FLAG_ENUM - | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, - modifiers: 32, - }) -} - -/// Returns the PHP-visible parent class name for ReflectionClass metadata. -fn eval_reflection_parent_class_name( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Option { - context.class_parent_names(class.name()).into_iter().next() -} - -/// Returns PHP's `ReflectionClass::isInstantiable()` value for eval class metadata. -fn eval_reflection_class_is_instantiable( - class: &EvalClass, - is_enum: bool, - context: &ElephcEvalContext, -) -> bool { - if class.is_abstract() || is_enum { - return false; - } - context - .class_method(class.name(), "__construct") - .map(|(_, method)| method.visibility() == EvalVisibility::Public) - .unwrap_or(true) -} - -/// Returns PHP's `ReflectionClass::isCloneable()` value for eval class metadata. -fn eval_reflection_class_is_cloneable( - class: &EvalClass, - is_enum: bool, - context: &ElephcEvalContext, -) -> bool { - if class.is_abstract() || is_enum { - return false; - } - context - .class_method(class.name(), "__clone") - .map(|(_, method)| method.visibility() == EvalVisibility::Public) - .unwrap_or(true) -} - -/// Returns PHP's `ReflectionClass::isIterable()` value for eval class metadata. -fn eval_reflection_class_is_iterable( - class: &EvalClass, - is_enum: bool, - context: &ElephcEvalContext, -) -> bool { - if class.is_abstract() || is_enum { - return false; - } - context - .class_interface_names(class.name()) - .iter() - .any(|name| { - name.eq_ignore_ascii_case("Iterator") || name.eq_ignore_ascii_case("IteratorAggregate") - }) -} - -/// Returns PHP's `ReflectionClass::isIterable()` value for compiler-injected class names. -fn eval_reflection_builtin_class_is_iterable(class_name: &str) -> bool { - matches!( - class_name - .trim_start_matches('\\') - .to_ascii_lowercase() - .as_str(), - "__elephcappenditeratorarrayiterator" - | "appenditerator" - | "arrayiterator" - | "arrayobject" - | "cachingiterator" - | "callbackfilteriterator" - | "directoryiterator" - | "emptyiterator" - | "filesystemiterator" - | "generator" - | "globiterator" - | "infiniteiterator" - | "internaliterator" - | "iteratoriterator" - | "limititerator" - | "multipleiterator" - | "norewinditerator" - | "parentiterator" - | "recursivearrayiterator" - | "recursivecachingiterator" - | "recursivecallbackfilteriterator" - | "recursivedirectoryiterator" - | "recursiveiteratoriterator" - | "recursiveregexiterator" - | "regexiterator" - | "spldoublylinkedlist" - | "splfixedarray" - | "splfileobject" - | "splmaxheap" - | "splminheap" - | "splobjectstorage" - | "splpriorityqueue" - | "splqueue" - | "splstack" - | "spltempfileobject" - ) -} - -/// Returns whether one reflected class-like name belongs to compiler-injected metadata. -fn eval_reflection_class_like_is_internal(class_name: &str) -> bool { - let trimmed = class_name.trim_start_matches('\\'); - if EVAL_SPL_CLASS_NAMES - .iter() - .any(|candidate| candidate.eq_ignore_ascii_case(trimmed)) - { - return true; - } - matches!( - trimmed.to_ascii_lowercase().as_str(), - "__elephcappenditeratorarrayiterator" - | "fiber" - | "fibererror" - | "generator" - | "internaliterator" - | "jsonexception" - | "phar" - | "phardata" - | "pharfileinfo" - | "php_user_filter" - | "reflectionattribute" - | "reflectionclass" - | "reflectionclassconstant" - | "reflectionenum" - | "reflectionenumbackedcase" - | "reflectionenumunitcase" - | "reflectionexception" - | "reflectionfunction" - | "reflectionintersectiontype" - | "reflectionmethod" - | "reflectionnamedtype" - | "reflectionparameter" - | "reflectionproperty" - | "reflectionuniontype" - | "sortdirection" - | "splheap" - | "splmaxheap" - | "splminheap" - | "splobjectstorage" - | "splpriorityqueue" - | "stdclass" - ) -} - -/// Computes PHP's `ReflectionClass::getModifiers()` bitmask for eval metadata. -fn eval_reflection_class_modifiers( - is_final: bool, - is_abstract: bool, - is_readonly_class: bool, - is_enum: bool, -) -> u64 { - let mut modifiers = 0; - if is_final { - modifiers |= 32; - } - if is_abstract { - modifiers |= 64; - } - if is_readonly_class && !is_enum { - modifiers |= 65_536; - } - modifiers -} - -/// Computes PHP's `ReflectionClassConstant::getModifiers()` bitmask for eval metadata. -fn eval_reflection_class_constant_modifiers(visibility: EvalVisibility, is_final: bool) -> u64 { - let mut modifiers = match visibility { - EvalVisibility::Public => 1, - EvalVisibility::Protected => 2, - EvalVisibility::Private => 4, - }; - if is_final { - modifiers |= 32; - } - modifiers -} - -/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask for eval metadata. -fn eval_reflection_method_modifiers( - visibility: EvalVisibility, - is_static: bool, - is_final: bool, - is_abstract: bool, -) -> u64 { - let mut modifiers = match visibility { - EvalVisibility::Public => 1, - EvalVisibility::Protected => 2, - EvalVisibility::Private => 4, - }; - if is_static { - modifiers |= 16; - } - if is_final { - modifiers |= 32; - } - if is_abstract { - modifiers |= 64; - } - modifiers -} - -/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask for eval metadata. -fn eval_reflection_property_modifiers( - visibility: EvalVisibility, - set_visibility: Option, - is_static: bool, - is_final: bool, - is_abstract: bool, - is_readonly: bool, - is_virtual: bool, -) -> u64 { - let mut modifiers = match visibility { - EvalVisibility::Public => 1, - EvalVisibility::Protected => 2, - EvalVisibility::Private => 4, - }; - if is_static { - modifiers |= 16; - } - if is_final { - modifiers |= 32; - } - if is_abstract { - modifiers |= 64; - } - if is_readonly { - modifiers |= 128; - } - if is_virtual { - modifiers |= 512; - } - match set_visibility { - Some(EvalVisibility::Private) => modifiers |= 32 | 4096, - Some(EvalVisibility::Protected) => modifiers |= 2048, - _ if is_readonly && visibility == EvalVisibility::Public => modifiers |= 2048, - _ => {} - } - modifiers -} - -/// Formats one reflected class-like symbol similarly to PHP's `__toString()` output. -fn eval_reflection_class_to_string( - reflected_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let metadata = eval_reflection_class_to_string_metadata(reflected_name, context, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - let constant_lines = - eval_reflection_class_constant_string_lines(&metadata.resolved_name, context, values)?; - let property_lines = - eval_reflection_class_property_string_lines(&metadata, context, values)?; - let method_lines = eval_reflection_class_method_string_lines(&metadata, context, values)?; - - let mut rendered = format!("{} {{\n", eval_reflection_class_to_string_header(&metadata)); - eval_reflection_class_append_string_section(&mut rendered, "Constants", &constant_lines); - eval_reflection_class_append_string_section(&mut rendered, "Properties", &property_lines); - eval_reflection_class_append_string_section(&mut rendered, "Methods", &method_lines); - rendered.push_str("}\n"); - Ok(rendered) -} - -/// Returns eval or AOT class metadata for a ReflectionClass string dump. -fn eval_reflection_class_to_string_metadata( - reflected_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(mut metadata) = eval_reflection_class_like_attributes(reflected_name, context) { - metadata.interface_names = - eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; - metadata.flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; - return Ok(Some(metadata)); - } - let runtime_class_name = reflected_name.trim_start_matches('\\'); - let Some((flags, modifiers)) = eval_reflection_aot_class_flags(runtime_class_name, values)? - else { - return Ok(None); - }; - let method_names = eval_reflection_aot_member_names( - EVAL_REFLECTION_OWNER_METHOD, - runtime_class_name, - values, - )?; - let native_interface_property_names = - eval_reflection_native_interface_property_names(runtime_class_name, context); - let property_names = if native_interface_property_names.is_empty() { - eval_reflection_aot_member_names( - EVAL_REFLECTION_OWNER_PROPERTY, - runtime_class_name, - values, - )? - } else { - native_interface_property_names - }; - let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; - let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; - let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; - Ok(Some(EvalReflectionClassMetadata { - resolved_name: runtime_class_name.to_string(), - source_location: None, - attributes: context.native_class_attributes(runtime_class_name), - flags, - modifiers, - interface_names, - trait_names, - method_names, - property_names, - parent_class_name, - })) -} - -/// Returns the PHP-like header line for `ReflectionClass::__toString()`. -fn eval_reflection_class_to_string_header(metadata: &EvalReflectionClassMetadata) -> String { - let origin = if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_INTERNAL != 0 { - "" - } else { - "" - }; - let kind = eval_reflection_class_to_string_kind(metadata.flags); - let mut parts = Vec::new(); - if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { - parts.push(String::from("interface")); - parts.push(metadata.resolved_name.clone()); - if !metadata.interface_names.is_empty() { - parts.push(String::from("extends")); - parts.push(metadata.interface_names.join(", ")); - } - } else if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { - parts.push(String::from("trait")); - parts.push(metadata.resolved_name.clone()); - } else { - if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0 { - parts.push(String::from("abstract")); - } - if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0 { - parts.push(String::from("final")); - } - if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0 { - parts.push(String::from("readonly")); - } - parts.push(String::from("class")); - parts.push(metadata.resolved_name.clone()); - if let Some(parent_class_name) = metadata.parent_class_name.as_ref() { - parts.push(String::from("extends")); - parts.push(parent_class_name.clone()); - } - if !metadata.interface_names.is_empty() { - parts.push(String::from("implements")); - parts.push(metadata.interface_names.join(", ")); - } - } - format!("{kind} [ {origin} {} ]", parts.join(" ")) -} - -/// Returns the ReflectionClass string header owner kind label. -fn eval_reflection_class_to_string_kind(flags: u64) -> &'static str { - if flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { - "Interface" - } else if flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { - "Trait" - } else { - "Class" - } -} - -/// Formats all constants visible to `ReflectionClass::__toString()`. -fn eval_reflection_class_constant_string_lines( - reflected_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let constant_names = eval_reflection_constant_names(reflected_name, context, values)?; - let mut lines = Vec::with_capacity(constant_names.len()); - for constant_name in constant_names { - let line = eval_reflection_class_constant_to_string( - reflected_name, - &constant_name, - EVAL_REFLECTION_OWNER_CLASS_CONSTANT, - context, - values, - )?; - lines.push(line.trim_end_matches('\n').to_string()); - } - Ok(lines) -} - -/// Formats all properties visible to `ReflectionClass::__toString()`. -fn eval_reflection_class_property_string_lines( - metadata: &EvalReflectionClassMetadata, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut lines = Vec::with_capacity(metadata.property_names.len()); - for property_name in &metadata.property_names { - let Some(member) = eval_reflection_reflected_property_metadata( - &metadata.resolved_name, - property_name, - context, - values, - )? - else { - continue; - }; - lines.push(eval_reflection_property_to_string(property_name, &member)); - } - Ok(lines) -} - -/// Formats all methods visible to `ReflectionClass::__toString()`. -fn eval_reflection_class_method_string_lines( - metadata: &EvalReflectionClassMetadata, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut lines = Vec::with_capacity(metadata.method_names.len()); - for method_name in &metadata.method_names { - let member = - if let Some(member) = - eval_reflection_method_metadata(&metadata.resolved_name, method_name, context) - { - Some(member) - } else { - eval_reflection_aot_method_metadata_with_signature_if_exists( - &metadata.resolved_name, - method_name, - context, - values, - )? - }; - let Some(member) = member else { - continue; - }; - lines.push(eval_reflection_method_summary_to_string(method_name, &member)); - } - Ok(lines) -} - -/// Appends one named section to a ReflectionClass string dump. -fn eval_reflection_class_append_string_section( - rendered: &mut String, - label: &str, - lines: &[String], -) { - rendered.push_str(&format!(" - {label} [{}] {{\n", lines.len())); - for line in lines { - rendered.push_str(" "); - rendered.push_str(line); - rendered.push('\n'); - } - rendered.push_str(" }\n"); -} - -/// Formats one reflected method line for `ReflectionClass::__toString()`. -fn eval_reflection_method_summary_to_string( - method_name: &str, - member: &EvalReflectionMemberMetadata, -) -> String { - let mut parts = Vec::new(); - if member.is_abstract { - parts.push(String::from("abstract")); - } - if member.is_final { - parts.push(String::from("final")); - } - if member.is_static { - parts.push(String::from("static")); - } - parts.push(eval_reflection_visibility_label(member.visibility).to_string()); - parts.push(String::from("method")); - parts.push(method_name.to_string()); - format!("Method [ {} ]", parts.join(" ")) -} - -/// Formats one reflected function or method similarly to PHP's `__toString()` output. -fn eval_reflection_function_method_to_string( - target: &EvalReflectionFunctionMethodTarget, -) -> String { - let mut rendered = format!( - "{} {{\n - Parameters [{}] {{\n", - eval_reflection_function_method_header(target), - eval_reflection_function_method_parameters(target).len() - ); - for parameter in eval_reflection_function_method_parameters(target) { - rendered.push_str(" "); - rendered.push_str(&eval_reflection_function_method_parameter_to_string(parameter)); - rendered.push('\n'); - } - rendered.push_str(" }\n"); - if let Some(return_type) = eval_reflection_function_method_return_type(target) { - rendered.push_str(" - Return [ "); - rendered.push_str(&eval_reflection_type_metadata_to_string(return_type)); - rendered.push_str(" ]\n"); - } - rendered.push_str("}\n"); - rendered -} - -/// Returns the PHP-like header line for a reflected function or method. -fn eval_reflection_function_method_header( - target: &EvalReflectionFunctionMethodTarget, -) -> String { - match target { - EvalReflectionFunctionMethodTarget::Function { name, .. } => { - format!( - "Function [ function {} ]", - name.trim_start_matches('\\') - ) - } - EvalReflectionFunctionMethodTarget::Method { - name, - visibility, - is_static, - is_final, - is_abstract, - .. - } => { - let mut parts = Vec::new(); - if *is_abstract { - parts.push(String::from("abstract")); - } - if *is_final { - parts.push(String::from("final")); - } - if *is_static { - parts.push(String::from("static")); - } - if let Some(visibility) = visibility { - parts.push(eval_reflection_visibility_label(*visibility).to_string()); - } - parts.push(String::from("method")); - parts.push(name.clone()); - format!("Method [ {} ]", parts.join(" ")) - } - } -} - -/// Returns retained parameters for a reflected function or method target. -fn eval_reflection_function_method_parameters( - target: &EvalReflectionFunctionMethodTarget, -) -> &[EvalReflectionParameterMetadata] { - match target { - EvalReflectionFunctionMethodTarget::Function { parameters, .. } - | EvalReflectionFunctionMethodTarget::Method { parameters, .. } => parameters, - } -} - -/// Formats one parameter line for function-like `__toString()` output. -fn eval_reflection_function_method_parameter_to_string( - parameter: &EvalReflectionParameterMetadata, -) -> String { - let requiredness = if parameter.is_optional { - "optional" - } else { - "required" - }; - let mut signature_parts = Vec::new(); - if let Some(type_metadata) = parameter.type_metadata.as_ref() { - signature_parts.push(eval_reflection_type_metadata_to_string(type_metadata)); - } - let mut variable = String::new(); - if parameter.is_passed_by_reference { - variable.push('&'); - } - if parameter.is_variadic { - variable.push_str("..."); - } - variable.push('$'); - variable.push_str(¶meter.name); - signature_parts.push(variable); - let default = parameter - .default_value - .as_ref() - .and_then(eval_reflection_default_expr_to_string) - .map(|value| format!(" = {value}")) - .unwrap_or_default(); - format!( - "Parameter #{} [ <{}> {}{} ]", - parameter.position, - requiredness, - signature_parts.join(" "), - default - ) -} - -/// Formats one reflected property similarly to PHP's `ReflectionProperty::__toString()`. -fn eval_reflection_property_to_string( - property_name: &str, - member: &EvalReflectionMemberMetadata, -) -> String { - if member.is_dynamic { - return format!("Property [ public ${property_name} ]\n"); - } - let mut parts = Vec::new(); - if member.is_abstract { - parts.push(String::from("abstract")); - } - if member.is_final { - parts.push(String::from("final")); - } - parts.push(eval_reflection_visibility_label(member.visibility).to_string()); - if member.is_static { - parts.push(String::from("static")); - } - if member.is_readonly { - parts.push(String::from("readonly")); - } - if let Some(type_name) = member - .type_metadata - .as_ref() - .map(eval_reflection_type_metadata_to_string) - { - parts.push(type_name); - } - parts.push(format!("${property_name}")); - - let default = if member.modifiers & 512 != 0 { - String::new() - } else { - member - .default_value - .as_ref() - .and_then(eval_reflection_default_expr_to_string) - .map(|value| format!(" = {value}")) - .unwrap_or_default() - }; - format!("Property [ {}{} ]", parts.join(" "), default) -} - -/// Formats one class constant or enum case like PHP's `ReflectionClassConstant::__toString()`. -fn eval_reflection_class_constant_to_string( - declaring_class: &str, - constant_name: &str, - owner_kind: u64, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (_, _, visibility, is_final, is_enum_case) = - eval_reflection_class_constant_metadata(declaring_class, constant_name, context, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - let value = eval_reflection_constant_value(declaring_class, constant_name, context, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - let mut parts = Vec::new(); - if is_final { - parts.push(String::from("final")); - } - parts.push(eval_reflection_visibility_label(visibility).to_string()); - parts.push(eval_reflection_class_constant_type_label( - declaring_class, - value, - is_enum_case - || matches!( - owner_kind, - EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE - ), - values, - )?); - parts.push(constant_name.to_string()); - let value = eval_reflection_class_constant_display_value(value, values)?; - Ok(format!("Constant [ {} ] {{ {} }}\n", parts.join(" "), value)) -} - -/// Returns the type label PHP prints for a reflected class constant value. -fn eval_reflection_class_constant_type_label( - declaring_class: &str, - value: RuntimeCellHandle, - is_enum_case: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - if is_enum_case { - return Ok(declaring_class.trim_start_matches('\\').to_string()); - } - Ok(match values.type_tag(value)? { - EVAL_TAG_INT => String::from("int"), - EVAL_TAG_STRING => String::from("string"), - EVAL_TAG_FLOAT => String::from("float"), - EVAL_TAG_BOOL => String::from("bool"), - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("array"), - EVAL_TAG_OBJECT => String::from("object"), - EVAL_TAG_NULL => String::from("null"), - _ => String::from("mixed"), - }) -} - -/// Returns the value display PHP prints inside ReflectionClassConstant braces. -fn eval_reflection_class_constant_display_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(match values.type_tag(value)? { - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("Array"), - EVAL_TAG_OBJECT => String::from("Object"), - EVAL_TAG_NULL => String::new(), - _ => String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(), - }) -} - -/// Returns PHP's lowercase label for one reflected visibility. -fn eval_reflection_visibility_label(visibility: EvalVisibility) -> &'static str { - match visibility { - EvalVisibility::Public => "public", - EvalVisibility::Protected => "protected", - EvalVisibility::Private => "private", - } -} - -/// Formats retained ReflectionType metadata for `ReflectionProperty::__toString()`. -fn eval_reflection_type_metadata_to_string( - type_metadata: &EvalReflectionParameterTypeMetadata, -) -> String { - match &type_metadata.kind { - EvalReflectionParameterTypeKind::Named(named) => { - if named.allows_null && named.name != "mixed" { - format!("?{}", named.name) - } else { - named.name.clone() - } - } - EvalReflectionParameterTypeKind::Union(union) => { - let mut names = union - .types - .iter() - .map(|type_metadata| type_metadata.name.clone()) - .collect::>(); - if union.allows_null && names.iter().all(|name| name != "null") { - names.push(String::from("null")); - } - names.join("|") - } - EvalReflectionParameterTypeKind::Intersection(intersection) => intersection - .types - .iter() - .map(|type_metadata| type_metadata.name.clone()) - .collect::>() - .join("&"), - } -} - -/// Formats retained literal defaults for `ReflectionProperty::__toString()`. -fn eval_reflection_default_expr_to_string(default: &EvalExpr) -> Option { - match default { - EvalExpr::Const(EvalConst::Null) => Some(String::from("NULL")), - EvalExpr::Const(EvalConst::Bool(value)) => Some(value.to_string()), - EvalExpr::Const(EvalConst::Int(value)) => Some(value.to_string()), - EvalExpr::Const(EvalConst::Float(value)) => Some(value.to_string()), - EvalExpr::Const(EvalConst::String(value)) => Some(format!("'{value}'")), - EvalExpr::Unary { - op: EvalUnaryOp::Plus, - expr, - } => eval_reflection_default_expr_to_string(expr), - EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr, - } => eval_reflection_default_expr_to_string(expr).map(|value| format!("-{value}")), - EvalExpr::ConstFetch(name) => Some(name.clone()), - EvalExpr::NamespacedConstFetch { name, .. } => Some(name.clone()), - EvalExpr::ClassConstantFetch { - class_name, - constant, - } => Some(format!("{class_name}::{constant}")), - _ => None, - } -} - -/// Returns whether eval retained this property as virtual rather than backed. -fn eval_reflection_property_is_virtual(property: &EvalClassProperty) -> bool { - property.is_virtual() -} - -/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from eval member flags. -fn eval_reflection_method_modifiers_from_flags(flags: u64) -> u64 { - let mut modifiers = 0; - if (flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0 { - modifiers |= 1; - } - if (flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0 { - modifiers |= 2; - } - if (flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0 { - modifiers |= 4; - } - if (flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC) != 0 { - modifiers |= 16; - } - if (flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0 { - modifiers |= 32; - } - if (flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0 { - modifiers |= 64; - } - modifiers -} - -/// Returns declaring class, attributes, visibility, finality, and enum-case kind. -fn eval_reflection_class_constant_metadata( - class_name: &str, - constant_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalVisibility, bool, bool)>, EvalStatus> { - if let Some(enum_decl) = context.enum_decl(class_name) { - if let Some(case) = enum_decl.case(constant_name) { - return Ok(Some(( - enum_decl.name().to_string(), - case.attributes().to_vec(), - EvalVisibility::Public, - false, - true, - ))); - } - } - if let Some(metadata) = context - .class_constant(class_name, constant_name) - .map(|(declaring_class, constant)| { - ( - declaring_class, - constant.attributes().to_vec(), - constant.visibility(), - constant.is_final(), - false, - ) - }) { - return Ok(Some(metadata)); - } - let runtime_class_name = class_name.trim_start_matches('\\'); - let Some(flags) = values.reflection_constant_flags(runtime_class_name, constant_name)? else { - return Ok(None); - }; - let declaring_class = values - .reflection_constant_declaring_class(runtime_class_name, constant_name)? - .unwrap_or_else(|| runtime_class_name.to_string()); - let attributes = eval_reflection_aot_constant_attributes( - runtime_class_name, - &declaring_class, - constant_name, - context, - ); - let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - }; - Ok(Some(( - declaring_class, - attributes, - visibility, - flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, - flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE != 0, - ))) -} - -/// Returns registered generated/AOT class-constant attributes for one reflected constant. -fn eval_reflection_aot_constant_attributes( - runtime_class_name: &str, - declaring_class_name: &str, - constant_name: &str, - context: &ElephcEvalContext, -) -> Vec { - let attributes = context.native_constant_attributes(declaring_class_name, constant_name); - if !attributes.is_empty() || declaring_class_name == runtime_class_name { - return attributes; - } - context.native_constant_attributes(runtime_class_name, constant_name) -} - -/// Returns true when a name resolves to an eval-declared class-like symbol. -fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> bool { - context.has_class(name) - || context.has_interface(name) - || context.has_trait(name) - || context.has_enum(name) -} - -/// Returns true when a name resolves to eval or runtime class-like metadata. -fn eval_reflection_class_like_or_runtime_exists( - name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(eval_reflection_class_like_exists(name, context) - || values.class_exists(name)? - || eval_runtime_interface_exists(name, values)? - || values.trait_exists(name)? - || values.enum_exists(name)?) -} - -/// Returns true when one name exists as an eval or runtime interface. -fn eval_reflection_interface_exists( - name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) -} - -/// Returns true when one name exists as a non-interface class-like symbol. -fn eval_reflection_non_interface_exists( - name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if context.has_class(name) - || context.has_trait(name) - || context.has_enum(name) - || values.class_exists(name)? - || values.trait_exists(name)? - { - return Ok(true); - } - values.enum_exists(name) -} - -/// Returns true when reflected eval metadata implements or extends an interface name. -fn eval_reflection_class_implements_interface_name( - reflected_name: &str, - interface_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if context.has_interface(reflected_name) { - if eval_reflection_same_class_like_name(reflected_name, interface_name) { - return Ok(true); - } - return Ok(eval_reflection_eval_interface_parent_names( - reflected_name, - context, - values, - )? - .iter() - .any(|parent| eval_reflection_same_class_like_name(parent, interface_name))); - } - if context.has_class(reflected_name) || context.has_enum(reflected_name) { - return Ok(eval_reflection_eval_class_interface_names(reflected_name, context, values)? - .iter() - .any(|interface| eval_reflection_same_class_like_name(interface, interface_name))); - } - Ok(false) -} - -/// Returns true when reflected eval metadata is a subclass or subinterface of a target. -fn eval_reflection_class_is_subclass_of_name( - reflected_name: &str, - target_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if context.has_interface(reflected_name) { - return Ok(eval_reflection_eval_interface_parent_names( - reflected_name, - context, - values, - )? - .iter() - .any(|parent| eval_reflection_same_class_like_name(parent, target_name))); - } - if context.has_class(reflected_name) || context.has_enum(reflected_name) { - if context.class_is_a(reflected_name, target_name, true) { - return Ok(true); - } - return Ok(eval_reflection_eval_class_interface_names(reflected_name, context, values)? - .iter() - .any(|interface| eval_reflection_same_class_like_name(interface, target_name))); - } - Ok(false) -} - -/// Returns true when two PHP class-like names match case-insensitively. -fn eval_reflection_same_class_like_name(left: &str, right: &str) -> bool { - left.trim_start_matches('\\') - .eq_ignore_ascii_case(right.trim_start_matches('\\')) -} - -/// Creates a catchable `ReflectionException` and propagates it through eval throw state. -fn eval_throw_reflection_exception( - message: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let exception = values.new_object("ReflectionException")?; - let message = values.string(message)?; - let code = values.int(0)?; - values.construct_object(exception, vec![message, code])?; - context.set_pending_throw(exception); - Err(EvalStatus::UncaughtThrowable) -} - -/// Creates a catchable `ValueError` and propagates it through eval throw state. -fn eval_throw_value_error( - message: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let exception = values.new_object("ValueError")?; - let message = values.string(message)?; - let code = values.int(0)?; - values.construct_object(exception, vec![message, code])?; - context.set_pending_throw(exception); - Err(EvalStatus::UncaughtThrowable) -} - -/// Creates a catchable `TypeError` and propagates it through eval throw state. -fn eval_throw_type_error( - message: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let exception = values.new_object("TypeError")?; - let message = values.string(message)?; - let code = values.int(0)?; - values.construct_object(exception, vec![message, code])?; - context.set_pending_throw(exception); - Err(EvalStatus::UncaughtThrowable) -} - -/// Returns PHP's type name spelling used in argument type error messages. -fn eval_reflection_type_error_type_name(tag: u64) -> &'static str { - match tag { - EVAL_TAG_INT => "int", - EVAL_TAG_STRING => "string", - EVAL_TAG_FLOAT => "float", - EVAL_TAG_BOOL => "bool", - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", - EVAL_TAG_NULL => "null", - EVAL_TAG_RESOURCE => "resource", - EVAL_TAG_OBJECT => "object", - _ => "unknown", - } -} - -/// Returns method metadata for a method-like member on an eval class-like symbol. -fn eval_reflection_method_metadata( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Option { - if context.has_class(class_name) || context.has_enum(class_name) { - if let Some((declaring_class, method)) = context.class_method(class_name, method_name) { - let required_parameter_count = eval_reflection_required_parameter_count( - method.parameter_defaults(), - method.parameter_is_variadic(), - ); - let mut flags = eval_reflection_member_flags( - method.visibility(), - method.is_static(), - method.is_final(), - method.is_abstract(), - false, - ); - flags |= eval_reflection_callable_flags(method.attributes()); - let return_type_metadata = method - .return_type() - .and_then(eval_reflection_parameter_type_metadata); - let declaring_function = EvalReflectionDeclaringFunctionMetadata { - name: method.name().to_string(), - declaring_class_name: Some(declaring_class.clone()), - magic_scope: Some(eval_reflection_eval_method_parameter_magic_scope( - &declaring_class, - &method, - None, - )), - attributes: method.attributes().to_vec(), - flags, - required_parameter_count, - }; - let promoted_parameter_names = eval_reflection_promoted_parameter_names( - &declaring_class, - method.name(), - context, - ); - let parameters = eval_reflection_parameters_from_names_and_type_flags( - Some(declaring_class.as_str()), - Some(&declaring_function), - method.params(), - method.parameter_has_types(), - method.parameter_types(), - method.parameter_attributes(), - method.parameter_defaults(), - method.parameter_is_by_ref(), - method.parameter_is_variadic(), - &promoted_parameter_names, - ); - return Some(EvalReflectionMemberMetadata { - declaring_class_name: Some(declaring_class), - source_file: None, - source_location: method.source_location(), - attributes: method.attributes().to_vec(), - visibility: method.visibility(), - is_static: method.is_static(), - is_final: method.is_final(), - is_abstract: method.is_abstract(), - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_method_modifiers( - method.visibility(), - method.is_static(), - method.is_final(), - method.is_abstract(), - ), - type_metadata: None, - settable_type_metadata: None, - return_type_metadata, - default_value: None, - default_value_trait_origin: None, - required_parameter_count, - parameters, - }); - } - return eval_reflection_enum_synthetic_method_metadata(class_name, method_name, context); - } - if context.has_interface(class_name) { - return context - .interface_method_requirements(class_name) - .into_iter() - .find(|method| method.name().eq_ignore_ascii_case(method_name)) - .map(|method| { - let required_parameter_count = eval_reflection_required_parameter_count( - method.parameter_defaults(), - method.parameter_is_variadic(), - ); - let mut flags = eval_reflection_member_flags( - EvalVisibility::Public, - method.is_static(), - false, - true, - false, - ); - flags |= eval_reflection_callable_flags(method.attributes()); - let return_type_metadata = method - .return_type() - .and_then(eval_reflection_parameter_type_metadata); - let declaring_function = EvalReflectionDeclaringFunctionMetadata { - name: method.name().to_string(), - declaring_class_name: Some(class_name.to_string()), - magic_scope: Some(eval_reflection_method_parameter_magic_scope( - class_name, - method.name(), - &format!("{}::{}", class_name.trim_start_matches('\\'), method.name()), - None, - )), - attributes: method.attributes().to_vec(), - flags, - required_parameter_count, - }; - let parameters = eval_reflection_parameters_from_names_and_type_flags( - Some(class_name), - Some(&declaring_function), - method.params(), - method.parameter_has_types(), - method.parameter_types(), - method.parameter_attributes(), - method.parameter_defaults(), - method.parameter_is_by_ref(), - method.parameter_is_variadic(), - &[], - ); - EvalReflectionMemberMetadata { - declaring_class_name: Some(class_name.to_string()), - source_file: None, - source_location: method.source_location(), - attributes: method.attributes().to_vec(), - visibility: EvalVisibility::Public, - is_static: method.is_static(), - is_final: false, - is_abstract: true, - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_method_modifiers( - EvalVisibility::Public, - method.is_static(), - false, - true, - ), - type_metadata: None, - settable_type_metadata: None, - return_type_metadata, - default_value: None, - default_value_trait_origin: None, - required_parameter_count, - parameters, - } - }); - } - context.trait_decl(class_name).and_then(|trait_decl| { - trait_decl - .methods() - .iter() - .find(|method| method.name().eq_ignore_ascii_case(method_name)) - .map(|method| { - let required_parameter_count = eval_reflection_required_parameter_count( - method.parameter_defaults(), - method.parameter_is_variadic(), - ); - let mut flags = eval_reflection_member_flags( - method.visibility(), - method.is_static(), - method.is_final(), - method.is_abstract(), - false, - ); - flags |= eval_reflection_callable_flags(method.attributes()); - let return_type_metadata = method - .return_type() - .and_then(eval_reflection_parameter_type_metadata); - let declaring_function = EvalReflectionDeclaringFunctionMetadata { - name: method.name().to_string(), - declaring_class_name: Some(trait_decl.name().to_string()), - magic_scope: Some(eval_reflection_eval_method_parameter_magic_scope( - trait_decl.name(), - method, - Some(trait_decl.name()), - )), - attributes: method.attributes().to_vec(), - flags, - required_parameter_count, - }; - let promoted_parameter_names = - eval_reflection_promoted_trait_parameter_names(trait_decl, method.name()); - let parameters = eval_reflection_parameters_from_names_and_type_flags( - Some(trait_decl.name()), - Some(&declaring_function), - method.params(), - method.parameter_has_types(), - method.parameter_types(), - method.parameter_attributes(), - method.parameter_defaults(), - method.parameter_is_by_ref(), - method.parameter_is_variadic(), - &promoted_parameter_names, - ); - EvalReflectionMemberMetadata { - declaring_class_name: Some(trait_decl.name().to_string()), - source_file: None, - source_location: method.source_location(), - attributes: method.attributes().to_vec(), - visibility: method.visibility(), - is_static: method.is_static(), - is_final: method.is_final(), - is_abstract: method.is_abstract(), - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_method_modifiers( - method.visibility(), - method.is_static(), - method.is_final(), - method.is_abstract(), - ), - type_metadata: None, - settable_type_metadata: None, - return_type_metadata, - default_value: None, - default_value_trait_origin: None, - required_parameter_count, - parameters, - } - }) - }) -} - -/// Builds ReflectionMethod metadata for PHP's enum-provided synthetic methods. -fn eval_reflection_enum_synthetic_method_metadata( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Option { - let synthetic_name = eval_enum_static_builtin_applies(class_name, method_name, context)?; - let enum_decl = context.enum_decl(class_name)?; - let declaring_class_name = enum_decl.name().trim_start_matches('\\').to_string(); - let flags = eval_reflection_member_flags(EvalVisibility::Public, true, false, false, false); - let (parameter_names, parameter_types, return_type_metadata) = match synthetic_name { - "cases" => ( - Vec::new(), - Vec::new(), - Some(eval_reflection_parameter_type_metadata(&EvalParameterType::new( - vec![EvalParameterTypeVariant::Array], - false, - ))?), - ), - "from" | "tryFrom" => { - let return_type = EvalParameterType::new( - vec![EvalParameterTypeVariant::Class(String::from("static"))], - synthetic_name == "tryFrom", - ); - ( - vec![String::from("value")], - vec![Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::String, EvalParameterTypeVariant::Int], - false, - ))], - Some(eval_reflection_parameter_type_metadata(&return_type)?), - ) - } - _ => return None, - }; - let parameter_count = parameter_names.len(); - let parameter_has_types = parameter_types - .iter() - .map(Option::is_some) - .collect::>(); - let parameter_attributes = vec![Vec::new(); parameter_count]; - let parameter_defaults = vec![None; parameter_count]; - let parameter_is_by_ref = vec![false; parameter_count]; - let parameter_is_variadic = vec![false; parameter_count]; - let required_parameter_count = - eval_reflection_required_parameter_count(¶meter_defaults, ¶meter_is_variadic); - let declaring_function = EvalReflectionDeclaringFunctionMetadata { - name: synthetic_name.to_string(), - declaring_class_name: Some(declaring_class_name.clone()), - magic_scope: None, - attributes: Vec::new(), - flags, - required_parameter_count, - }; - let parameters = eval_reflection_parameters_from_names_and_type_flags( - Some(&declaring_class_name), - Some(&declaring_function), - ¶meter_names, - ¶meter_has_types, - ¶meter_types, - ¶meter_attributes, - ¶meter_defaults, - ¶meter_is_by_ref, - ¶meter_is_variadic, - &[], - ); - Some(EvalReflectionMemberMetadata { - declaring_class_name: Some(declaring_class_name), - source_file: None, - source_location: None, - attributes: Vec::new(), - visibility: EvalVisibility::Public, - is_static: true, - is_final: false, - is_abstract: false, - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_method_modifiers(EvalVisibility::Public, true, false, false), - type_metadata: None, - settable_type_metadata: None, - return_type_metadata, - default_value: None, - default_value_trait_origin: None, - required_parameter_count, - parameters, - }) -} - -/// Returns property metadata for a property-like member on an eval class-like symbol. -fn eval_reflection_property_metadata( - class_name: &str, - property_name: &str, - context: &ElephcEvalContext, -) -> Option { - if context.has_class(class_name) || context.has_enum(class_name) { - return context.class_property(class_name, property_name).map( - |(declaring_class, property)| { - let default_value = eval_reflection_property_default_value(&property); - EvalReflectionMemberMetadata { - declaring_class_name: Some(declaring_class), - source_file: None, - source_location: None, - attributes: property.attributes().to_vec(), - visibility: property.visibility(), - is_static: property.is_static(), - is_final: property.is_final(), - is_abstract: property.is_abstract(), - is_readonly: property.is_readonly(), - is_promoted: property.is_promoted(), - is_dynamic: false, - modifiers: eval_reflection_property_modifiers( - property.visibility(), - property.set_visibility(), - property.is_static(), - property.is_final(), - property.is_abstract(), - property.is_readonly(), - eval_reflection_property_is_virtual(&property), - ), - type_metadata: property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - settable_type_metadata: property - .settable_type() - .and_then(eval_reflection_parameter_type_metadata), - return_type_metadata: None, - default_value, - default_value_trait_origin: property.trait_origin().map(str::to_string), - required_parameter_count: 0, - parameters: Vec::new(), - } - }, - ); - } - if context.has_interface(class_name) { - return context - .interface_property_requirements(class_name) - .into_iter() - .find(|property| property.name() == property_name) - .map(|property| { - eval_reflection_interface_property_metadata(class_name.to_string(), &property) - }); - } - if let Some((declaring_class, property)) = - eval_reflection_native_interface_property_requirement(class_name, property_name, context) - { - return Some(eval_reflection_interface_property_metadata( - declaring_class, - &property, - )); - } - context.trait_decl(class_name).and_then(|trait_decl| { - trait_decl - .properties() - .iter() - .find(|property| property.name() == property_name) - .map(|property| { - let default_value = eval_reflection_property_default_value(property); - EvalReflectionMemberMetadata { - declaring_class_name: Some(trait_decl.name().to_string()), - source_file: None, - source_location: None, - attributes: property.attributes().to_vec(), - visibility: property.visibility(), - is_static: property.is_static(), - is_final: property.is_final(), - is_abstract: property.is_abstract(), - is_readonly: property.is_readonly(), - is_promoted: property.is_promoted(), - is_dynamic: false, - modifiers: eval_reflection_property_modifiers( - property.visibility(), - property.set_visibility(), - property.is_static(), - property.is_final(), - property.is_abstract(), - property.is_readonly(), - eval_reflection_property_is_virtual(property), - ), - type_metadata: property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - settable_type_metadata: property - .settable_type() - .and_then(eval_reflection_parameter_type_metadata), - return_type_metadata: None, - default_value, - default_value_trait_origin: property - .trait_origin() - .map(str::to_string) - .or_else(|| Some(trait_decl.name().to_string())), - required_parameter_count: 0, - parameters: Vec::new(), - } - }) - }) -} - -/// Returns property metadata for a property contract declared on an interface. -fn eval_reflection_interface_property_metadata( - declaring_class: String, - property: &EvalInterfaceProperty, -) -> EvalReflectionMemberMetadata { - EvalReflectionMemberMetadata { - declaring_class_name: Some(declaring_class), - source_file: None, - source_location: None, - attributes: property.attributes().to_vec(), - visibility: EvalVisibility::Public, - is_static: false, - is_final: false, - is_abstract: true, - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_property_modifiers( - EvalVisibility::Public, - property.set_visibility(), - false, - false, - true, - false, - true, - ), - type_metadata: property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - settable_type_metadata: property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - return_type_metadata: None, - default_value: None, - default_value_trait_origin: None, - required_parameter_count: 0, - parameters: Vec::new(), - } -} - -/// Returns property names that can contribute to `ReflectionClass::getDefaultProperties()`. -fn eval_reflection_default_property_names( - reflected_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if context.has_class(reflected_name) - || context.has_enum(reflected_name) - || context.has_trait(reflected_name) - || context.has_interface(reflected_name) - { - return Ok(eval_reflection_eval_property_names(reflected_name, context)); - } - eval_reflection_aot_member_names(EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, values) -} - -/// Returns eval or generated/AOT property metadata for default-property materialization. -fn eval_reflection_default_property_metadata( - reflected_name: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) { - return Ok(Some(member)); - } - if let Some((declaring_class, property)) = - eval_reflection_native_interface_property_requirement( - reflected_name, - property_name, - context, - ) - { - return Ok(Some(eval_reflection_interface_property_metadata( - declaring_class, - &property, - ))); - } - eval_reflection_aot_property_metadata_if_exists(reflected_name, property_name, context, values) -} - -/// Returns eval or generated/AOT metadata for a materialized `ReflectionProperty`. -fn eval_reflection_reflected_property_metadata( - declaring_class: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(member) = eval_reflection_property_metadata(declaring_class, property_name, context) { - return Ok(Some(member)); - } - if let Some((declaring_class, property)) = - eval_reflection_native_interface_property_requirement( - declaring_class, - property_name, - context, - ) - { - return Ok(Some(eval_reflection_interface_property_metadata( - declaring_class, - &property, - ))); - } - eval_reflection_aot_property_metadata_if_exists(declaring_class, property_name, context, values) -} - -/// Returns eval-declared property names for reflection APIs that do not use AOT lists. -fn eval_reflection_eval_property_names( - reflected_name: &str, - context: &ElephcEvalContext, -) -> Vec { - if context.has_trait(reflected_name) { - return context.trait_property_names(reflected_name); - } - if context.has_interface(reflected_name) { - return context.interface_property_names(reflected_name); - } - context.class_property_names(reflected_name) -} - -/// Returns property names that can contribute to `ReflectionClass::getStaticProperties()`. -fn eval_reflection_static_property_names( - reflected_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if eval_reflection_class_like_exists(reflected_name, context) { - return Ok(eval_reflection_eval_property_names(reflected_name, context) - .into_iter() - .filter(|name| { - eval_reflection_property_metadata(reflected_name, name, context) - .is_some_and(|property| property.is_static) - }) - .collect()); - } - let names = - eval_reflection_aot_member_names(EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, values)?; - let mut result = Vec::new(); - for name in names { - if eval_reflection_aot_property_metadata_if_exists(reflected_name, &name, context, values)? - .is_some_and(|property| property.is_static) - { - result.push(name); - } - } - Ok(result) -} - -/// Returns eval or generated/AOT property metadata for static-property reflection. -fn eval_reflection_static_property_metadata( - reflected_name: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - eval_reflection_reflected_property_metadata(reflected_name, property_name, context, values) -} - -/// Returns the current eval or generated/AOT static property value. -fn eval_reflection_static_property_value( - reflected_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(member) = - eval_reflection_static_property_metadata(reflected_name, property_name, context, values)? - else { - return Ok(None); - }; - if !member.is_static { - return Ok(None); - } - if eval_reflection_class_like_exists(reflected_name, context) { - let declaring_class = member - .declaring_class_name - .as_deref() - .ok_or(EvalStatus::RuntimeFatal)?; - if let Some(value) = context.static_property(declaring_class, property_name) { - return Ok(Some(value)); - } - return member - .default_value - .as_ref() - .map(|default| eval_reflection_member_default_value(&member, default, context, values)) - .transpose(); - } - let declaring_class = member - .declaring_class_name - .as_deref() - .unwrap_or(reflected_name); - eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { - values.static_property_get(reflected_name, property_name) - }) -} - -/// Binds `getStaticPropertyValue()` arguments while preserving whether a default was supplied. -fn eval_reflection_static_property_value_args( - evaluated_args: Vec, -) -> Result<(RuntimeCellHandle, Option), EvalStatus> { - let params = [String::from("name"), String::from("default")]; - let mut bound_args = [None, None]; - let mut next_positional = 0; - for arg in evaluated_args { - if let Some(name) = arg.name { - let Some(position) = params.iter().position(|param| param == &name) else { - return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[position].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[position] = Some(arg.value); - } else { - while next_positional < bound_args.len() && bound_args[next_positional].is_some() { - next_positional += 1; - } - if next_positional >= bound_args.len() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[next_positional] = Some(arg.value); - next_positional += 1; - } - } - let property_name = bound_args[0].ok_or(EvalStatus::RuntimeFatal)?; - Ok((property_name, bound_args[1])) -} - -/// Binds the optional `ReflectionProperty::getValue()` object argument. -fn eval_reflection_property_get_value_arg( - evaluated_args: Vec, -) -> Result, EvalStatus> { - let params = [String::from("object")]; - let mut bound_arg = None; - for arg in evaluated_args { - if let Some(name) = arg.name { - if params.iter().all(|param| param != &name) || bound_arg.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_arg = Some(arg.value); - } else if bound_arg.is_none() { - bound_arg = Some(arg.value); - } else { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(bound_arg) -} - -/// Binds `ReflectionProperty::setValue()` arguments while allowing PHP's static shorthand. -fn eval_reflection_property_set_value_args( - evaluated_args: Vec, -) -> Result<(RuntimeCellHandle, Option), EvalStatus> { - let params = [String::from("objectOrValue"), String::from("value")]; - let mut bound_args = [None, None]; - let mut next_positional = 0; - for arg in evaluated_args { - if let Some(name) = arg.name { - let Some(position) = params.iter().position(|param| param == &name) else { - return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[position].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[position] = Some(arg.value); - } else { - while next_positional < bound_args.len() && bound_args[next_positional].is_some() { - next_positional += 1; - } - if next_positional >= bound_args.len() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[next_positional] = Some(arg.value); - next_positional += 1; - } - } - let object_or_value = bound_args[0].ok_or(EvalStatus::RuntimeFatal)?; - Ok((object_or_value, bound_args[1])) -} - -/// Binds the required object argument for `ReflectionProperty::getRawValue()`. -fn eval_reflection_property_raw_value_arg( - evaluated_args: Vec, -) -> Result { - let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; - Ok(args[0]) -} - -/// Binds the object and value arguments for `ReflectionProperty::setRawValue()`. -fn eval_reflection_property_set_raw_value_args( - evaluated_args: Vec, -) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { - let args = bind_evaluated_function_args( - &[String::from("object"), String::from("value")], - evaluated_args, - )?; - Ok((args[0], args[1])) -} - -/// Returns the eval property metadata eligible for ReflectionProperty hook APIs. -fn eval_reflection_property_for_hooks( - declaring_class: &str, - property_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, EvalClassProperty)> { - if context.has_class(declaring_class) || context.has_enum(declaring_class) { - return context.class_property(declaring_class, property_name); - } - context - .trait_decl(declaring_class) - .and_then(|trait_decl| { - trait_decl - .properties() - .iter() - .find(|property| property.name() == property_name) - .map(|property| (trait_decl.name().to_string(), property.clone())) - }) - .or_else(|| { - context - .interface_property_requirements(declaring_class) - .into_iter() - .find(|property| property.name() == property_name) - .map(|property| { - let property = EvalClassProperty::new(property.name(), None) - .with_type(property.property_type().cloned()) - .with_attributes(property.attributes().to_vec()) - .with_abstract_hook_contract( - property.requires_get(), - property.requires_set(), - ); - (declaring_class.to_string(), property) - }) - }) - .or_else(|| { - eval_reflection_native_interface_property_requirement( - declaring_class, - property_name, - context, - ) - .map(|(owner, property)| { - let property = EvalClassProperty::new(property.name(), None) - .with_type(property.property_type().cloned()) - .with_attributes(property.attributes().to_vec()) - .with_abstract_hook_contract(property.requires_get(), property.requires_set()); - (owner, property) - }) - }) -} - -/// Returns one generated/AOT interface property contract registered for eval reflection. -fn eval_reflection_native_interface_property_requirement( - interface_name: &str, - property_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, EvalInterfaceProperty)> { - context - .native_interface_property_requirements(interface_name) - .into_iter() - .find(|(_, property)| property.name() == property_name) -} - -/// Returns generated/AOT interface property names registered for eval reflection. -fn eval_reflection_native_interface_property_names( - interface_name: &str, - context: &ElephcEvalContext, -) -> Vec { - context - .native_interface_property_requirements(interface_name) - .into_iter() - .map(|(_, property)| property.name().to_string()) - .collect() -} - -/// Binds the `PropertyHookType $type` argument used by ReflectionProperty hook APIs. -fn eval_reflection_property_hook_arg( - evaluated_args: Vec, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let args = bind_evaluated_function_args(&[String::from("type")], evaluated_args)?; - eval_reflection_property_hook_type(args[0], context, values) -} - -/// Converts one synthetic `PropertyHookType` object into an eval reflection hook kind. -fn eval_reflection_property_hook_type( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let identity = values.object_identity(value)?; - if !context.dynamic_object_is_class(identity, "PropertyHookType") { - return Err(EvalStatus::RuntimeFatal); - } - let hook_value = values.property_get(value, "value")?; - match eval_reflection_string_arg(hook_value, values)?.as_str() { - "get" => Ok(EvalReflectionPropertyHook::Get), - "set" => Ok(EvalReflectionPropertyHook::Set), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns concrete hook kinds declared on one eval property. -fn eval_reflection_property_hook_kinds( - property: &EvalClassProperty, -) -> Vec { - let mut hooks = Vec::new(); - if property.has_get_hook() || property.requires_get_hook() { - hooks.push(EvalReflectionPropertyHook::Get); - } - if property.has_set_hook() || property.requires_set_hook() { - hooks.push(EvalReflectionPropertyHook::Set); - } - hooks -} - -/// Returns whether one eval property exposes the requested concrete hook. -fn eval_reflection_property_has_hook( - property: &EvalClassProperty, - hook: EvalReflectionPropertyHook, -) -> bool { - match hook { - EvalReflectionPropertyHook::Get => property.has_get_hook() || property.requires_get_hook(), - EvalReflectionPropertyHook::Set => property.has_set_hook() || property.requires_set_hook(), - } -} - -/// Builds PHP's string-keyed ReflectionMethod map returned by `getHooks()`. -fn eval_reflection_property_hook_method_array( - declaring_class: &str, - property: &EvalClassProperty, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let hooks = eval_reflection_property_hook_kinds(property); - let mut result = values.assoc_new(hooks.len())?; - for hook in hooks { - let key = values.string(hook.key())?; - let method = eval_reflection_property_hook_method_object( - declaring_class, - property, - hook, - context, - values, - )?; - result = values.array_set(result, key, method)?; - } - Ok(result) -} - -/// Materializes a ReflectionMethod object for one concrete property hook. -fn eval_reflection_property_hook_method_object( - declaring_class: &str, - property: &EvalClassProperty, - hook: EvalReflectionPropertyHook, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let metadata = eval_reflection_property_hook_method_metadata(declaring_class, property, hook); - eval_reflection_member_object_result( - EVAL_REFLECTION_OWNER_METHOD, - &hook.reflected_method_name(property.name()), - &metadata, - context, - values, - ) -} - -/// Builds ReflectionMethod metadata for one eval property hook accessor. -fn eval_reflection_property_hook_method_metadata( - declaring_class: &str, - property: &EvalClassProperty, - hook: EvalReflectionPropertyHook, -) -> EvalReflectionMemberMetadata { - let parameters = eval_reflection_property_hook_parameters(declaring_class, property, hook); - let required_parameter_count = parameters.len(); - EvalReflectionMemberMetadata { - declaring_class_name: Some(declaring_class.to_string()), - source_file: None, - source_location: None, - attributes: Vec::new(), - visibility: property.visibility(), - is_static: false, - is_final: false, - is_abstract: property.is_abstract(), - is_readonly: false, - is_promoted: false, - is_dynamic: false, - modifiers: eval_reflection_method_modifiers( - property.visibility(), - false, - false, - property.is_abstract(), - ), - type_metadata: None, - settable_type_metadata: None, - return_type_metadata: eval_reflection_property_hook_return_type(property, hook), - default_value: None, - default_value_trait_origin: None, - required_parameter_count, - parameters, - } -} - -/// Builds the synthetic setter parameter metadata exposed by PHP hook reflection. -fn eval_reflection_property_hook_parameters( - declaring_class: &str, - property: &EvalClassProperty, - hook: EvalReflectionPropertyHook, -) -> Vec { - if !matches!(hook, EvalReflectionPropertyHook::Set) { - return Vec::new(); - } - let type_metadata = property - .settable_type() - .and_then(eval_reflection_parameter_type_metadata); - let has_type = type_metadata.is_some(); - let is_array_type = eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); - let is_callable_type = - eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); - let declaring_function = EvalReflectionDeclaringFunctionMetadata { - name: hook.reflected_method_name(property.name()), - declaring_class_name: Some(declaring_class.to_string()), - magic_scope: None, - attributes: Vec::new(), - flags: eval_reflection_member_flags(property.visibility(), false, false, false, false), - required_parameter_count: 1, - }; - vec![EvalReflectionParameterMetadata { - name: "value".to_string(), - declaring_class_name: Some(declaring_class.to_string()), - declaring_function: Some(declaring_function), - attributes: Vec::new(), - position: 0, - is_optional: false, - is_variadic: false, - is_passed_by_reference: false, - is_promoted: false, - has_type, - allows_null: type_metadata - .as_ref() - .is_some_and(eval_reflection_type_allows_null), - is_array_type, - is_callable_type, - type_metadata, - default_value: None, - default_value_constant_name: None, - }] -} - -/// Returns the ReflectionMethod return type metadata for a property hook. -fn eval_reflection_property_hook_return_type( - property: &EvalClassProperty, - hook: EvalReflectionPropertyHook, -) -> Option { - match hook { - EvalReflectionPropertyHook::Get => property - .property_type() - .and_then(eval_reflection_parameter_type_metadata), - EvalReflectionPropertyHook::Set => Some(EvalReflectionParameterTypeMetadata { - kind: EvalReflectionParameterTypeKind::Named(eval_reflection_builtin_named_type( - "void", false, - )), - }), - } -} - -/// Maps PHP-visible property-hook method names back to eval's synthetic method names. -fn eval_reflection_property_hook_synthetic_method_name(method_name: &str) -> Option { - let body = method_name.strip_prefix('$')?; - let (property_name, hook_name) = body.rsplit_once("::")?; - match hook_name { - "get" => Some(EvalReflectionPropertyHook::Get.synthetic_method_name(property_name)), - "set" => Some(EvalReflectionPropertyHook::Set.synthetic_method_name(property_name)), - _ => None, - } -} - -/// Binds `ReflectionMethod::invoke()` arguments and preserves forwarded named args. -fn eval_reflection_method_invoke_args( - evaluated_args: Vec, -) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { - let mut object = None; - let mut method_args = Vec::new(); - for arg in evaluated_args { - if matches!(arg.name.as_deref(), Some("object")) { - if object.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - object = Some(arg.value); - } else if object.is_none() && arg.name.is_none() { - object = Some(arg.value); - } else { - method_args.push(eval_reflection_method_forwarded_value_arg(arg)); - } - } - object - .map(|object| (object, method_args)) - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Converts a variadic `invoke()` argument into a by-value forwarded method argument. -fn eval_reflection_method_forwarded_value_arg(arg: EvaluatedCallArg) -> EvaluatedCallArg { - EvaluatedCallArg { - name: arg.name, - value: arg.value, - ref_target: None, - } -} - -/// Binds `ReflectionMethod::invokeArgs()` and expands its PHP argument array. -fn eval_reflection_method_invoke_args_array( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { - let args = bind_evaluated_function_args( - &[String::from("object"), String::from("args")], - evaluated_args, - )?; - let method_args = eval_array_call_arg_values(args[1], context, values)?; - Ok((args[0], method_args)) -} - -/// Binds `ReflectionFunction::invokeArgs()` and expands its PHP argument array. -fn eval_reflection_function_invoke_args_array( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; - eval_array_call_arg_values(args[0], context, values) -} - -/// Dispatches one reflected function invocation through eval or registered native functions. -fn eval_reflection_function_invoke_dispatch( - function_name: &str, - function_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(closure) = context.closure(function_name).cloned() { - return eval_closure_with_evaluated_args_and_bound_scope_ref_mode( - &closure, - None, - closure.function().parameter_is_by_ref(), - function_args, - EvalByRefBindingMode::WarnByValue { - callable_name: closure.function().name(), - }, - context, - values, - ); - } - let function_key = function_name.to_ascii_lowercase(); - if let Some(function) = context.function(&function_key).cloned() { - return eval_dynamic_function_with_evaluated_args_and_ref_mode( - &function, - function.parameter_is_by_ref(), - function_args, - EvalByRefBindingMode::WarnByValue { - callable_name: function.name(), - }, - context, - values, - ); - } - eval_callable_with_call_array_args(&function_key, function_args, context, values) -} - -/// Dispatches one reflected method invocation through eval or AOT bridges. -fn eval_reflection_method_invoke_dispatch( - declaring_class: &str, - method_name: &str, - object: RuntimeCellHandle, - method_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let lookup_method_name = eval_reflection_property_hook_synthetic_method_name(method_name) - .unwrap_or_else(|| method_name.to_string()); - if let Some((method_class, method)) = context.class_method(declaring_class, &lookup_method_name) - { - if method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - let callable_name = format!( - "{}::{}", - method_class.trim_start_matches('\\'), - method.name() - ); - if method.is_static() { - return eval_dynamic_static_method_with_values_and_ref_mode( - &method_class, - &method_class, - &method, - method.parameter_is_by_ref(), - method_args, - EvalByRefBindingMode::WarnByValue { - callable_name: &callable_name, - }, - context, - values, - ); - } - let called_class = - eval_reflection_method_instance_called_class(declaring_class, object, context, values)?; - return eval_dynamic_method_with_values_and_ref_mode( - &method_class, - &called_class, - &method, - object, - method.parameter_is_by_ref(), - method_args, - EvalByRefBindingMode::WarnByValue { - callable_name: &callable_name, - }, - context, - values, - ); - } - if eval_enum_static_builtin_applies(declaring_class, &lookup_method_name, context).is_some() { - return eval_enum_builtin_static_method_result( - declaring_class, - &lookup_method_name, - method_args, - context, - values, - ); - } - eval_reflection_aot_method_invoke_dispatch( - declaring_class, - method_name, - object, - method_args, - context, - values, - ) -} - -/// Returns the runtime class name for an eval object used as a reflected receiver. -fn eval_reflection_method_instance_called_class( - declaring_class: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let identity = values.object_identity(object)?; - let Some(object_class_name) = context - .dynamic_object_class(identity) - .map(|class| class.name().to_string()) - else { - return Err(EvalStatus::RuntimeFatal); - }; - if !context.class_is_a(&object_class_name, declaring_class, false) { - eval_throw_reflection_exception( - "Given object is not an instance of the class this method was declared in", - context, - values, - )?; - return Err(EvalStatus::UncaughtThrowable); - } - Ok(object_class_name) -} - -/// Invokes one reflected generated/AOT method when it fits the bridge slice. -fn eval_reflection_aot_method_invoke_dispatch( - declaring_class: &str, - method_name: &str, - object: RuntimeCellHandle, - method_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let member = - eval_reflection_aot_method_metadata_if_exists(declaring_class, method_name, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - if member.is_abstract { - return Err(EvalStatus::RuntimeFatal); - } - if member.is_static { - return eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { - eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - declaring_class, - method_name, - method_args, - Some(declaring_class), - Some(declaring_class), - context, - values, - ) - }); - } - if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let is_instance = dynamic_object_is_a(object, declaring_class, false, context, values)? - .map_or_else(|| values.object_is_a(object, declaring_class, false), Ok)?; - if !is_instance { - eval_throw_reflection_exception( - "Given object is not an instance of the class this method was declared in", - context, - values, - )?; - return Err(EvalStatus::UncaughtThrowable); - } - let called_class = eval_reflection_object_class_name(object, context, values)?; - eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { - eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - object, - declaring_class, - method_name, - method_args, - Some(declaring_class), - Some(&called_class), - context, - values, - ) - }) -} - -/// Runs a reflected AOT invocation with the declaring class as visibility scope. -fn eval_reflection_with_declaring_class_scope( - declaring_class: &str, - context: &mut ElephcEvalContext, - action: impl FnOnce(&mut ElephcEvalContext) -> Result, -) -> Result { - context.push_class_scope(declaring_class.to_string()); - context.push_called_class_scope(declaring_class.to_string()); - let result = action(context); - context.pop_called_class_scope(); - context.pop_class_scope(); - result -} - -/// Reads one eval instance property through ReflectionProperty semantics. -fn eval_reflection_instance_property_get_value( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (object_class_name, property) = eval_reflection_instance_property_target( - declaring_class, - property_name, - object, - context, - values, - )?; - if property.has_get_hook() - && !current_eval_property_hook_is( - declaring_class, - property.name(), - &property_hook_get_method(property.name()), - context, - ) - { - let (hook_class, hook_method) = context - .class_method( - &object_class_name, - &property_hook_get_method(property.name()), - ) - .ok_or(EvalStatus::RuntimeFatal)?; - return eval_dynamic_method_with_values( - &hook_class, - &object_class_name, - &hook_method, - object, - Vec::new(), - context, - values, - ); - } - let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); - values.property_get(object, &storage_property_name) -} - -/// Writes one eval instance property through ReflectionProperty semantics. -fn eval_reflection_instance_property_set_value( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let (object_class_name, property) = eval_reflection_instance_property_target( - declaring_class, - property_name, - object, - context, - values, - )?; - validate_eval_reflection_property_write(declaring_class, &property, context)?; - if property.has_set_hook() { - if !current_eval_property_hook_is( - declaring_class, - property.name(), - &property_hook_set_method(property.name()), - context, - ) { - let (hook_class, hook_method) = context - .class_method( - &object_class_name, - &property_hook_set_method(property.name()), - ) - .ok_or(EvalStatus::RuntimeFatal)?; - let hook_result = eval_dynamic_method_with_values( - &hook_class, - &object_class_name, - &hook_method, - object, - vec![EvaluatedCallArg { - name: None, - value, - ref_target: None, - }], - context, - values, - )?; - values.release(hook_result)?; - return Ok(()); - } - } else if property.has_get_hook() { - return Err(EvalStatus::RuntimeFatal); - } - let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); - values.property_set(object, &storage_property_name, value)?; - let identity = values.object_identity(object)?; - context.mark_dynamic_property_initialized(identity, &storage_property_name); - Ok(()) -} - -/// Reads one generated/AOT instance property through ReflectionProperty semantics. -fn eval_reflection_aot_instance_property_get_value( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_reflection_aot_instance_property_validate_object( - declaring_class, - object, - context, - values, - )?; - eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { - values.property_get(object, property_name) - }) -} - -/// Writes one generated/AOT instance property through ReflectionProperty semantics. -fn eval_reflection_aot_instance_property_set_value( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - eval_reflection_aot_instance_property_validate_object( - declaring_class, - object, - context, - values, - )?; - eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { - values.property_set(object, property_name, value) - }) -} - -/// Checks one generated/AOT instance property initialization marker through ReflectionProperty. -fn eval_reflection_aot_instance_property_is_initialized( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_reflection_aot_instance_property_validate_object( - declaring_class, - object, - context, - values, - )?; - eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { - values.property_is_initialized(object, property_name) - }) -} - -/// Checks one generated/AOT static property initialization marker through ReflectionProperty. -fn eval_reflection_aot_static_property_is_initialized( - declaring_class: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { - values.static_property_is_initialized(declaring_class, property_name) - }) -} - -/// Verifies a generated/AOT ReflectionProperty instance target is compatible. -fn eval_reflection_aot_instance_property_validate_object( - declaring_class: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - let is_instance = dynamic_object_is_a(object, declaring_class, false, context, values)? - .map_or_else(|| values.object_is_a(object, declaring_class, false), Ok)?; - if is_instance { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Returns whether one eval instance property is initialized for ReflectionProperty. -fn eval_reflection_instance_property_is_initialized( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (_, property) = eval_reflection_instance_property_target( - declaring_class, - property_name, - object, - context, - values, - )?; - if property.is_virtual() { - return Ok(true); - } - let identity = values.object_identity(object)?; - let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); - Ok(context.dynamic_property_is_initialized(identity, &storage_property_name)) -} - -/// Reads one eval instance property through ReflectionProperty raw-storage semantics. -fn eval_reflection_instance_property_get_raw_value( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (_, property) = eval_reflection_instance_property_target( - declaring_class, - property_name, - object, - context, - values, - )?; - if property.is_virtual() { - return Err(EvalStatus::RuntimeFatal); - } - let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); - values.property_get(object, &storage_property_name) -} - -/// Writes one eval instance property through ReflectionProperty raw-storage semantics. -fn eval_reflection_instance_property_set_raw_value( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let (_, property) = eval_reflection_instance_property_target( - declaring_class, - property_name, - object, - context, - values, - )?; - if property.is_virtual() { - return Err(EvalStatus::RuntimeFatal); - } - validate_eval_reflection_property_write(declaring_class, &property, context)?; - let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); - values.property_set(object, &storage_property_name, value)?; - let identity = values.object_identity(object)?; - context.mark_dynamic_property_initialized(identity, &storage_property_name); - Ok(()) -} - -/// Reads a public dynamic property through ReflectionProperty semantics. -fn eval_reflection_dynamic_property_get_value( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; - values.property_get(object, property_name) -} - -/// Writes a public dynamic property through ReflectionProperty semantics. -fn eval_reflection_dynamic_property_set_value( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; - values.property_set(object, property_name, value)?; - let identity = values.object_identity(object)?; - context.mark_dynamic_property_initialized(identity, property_name); - Ok(()) -} - -/// Returns whether a public dynamic property currently exists on the target object. -fn eval_reflection_dynamic_property_is_initialized( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; - eval_reflection_object_dynamic_property_exists(object, property_name, values) -} - -/// Validates the object argument used by dynamic ReflectionProperty operations. -fn eval_reflection_dynamic_property_validate_object( - declaring_class: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let object_class_name = eval_reflection_object_class_name(object, context, values)?; - if eval_reflection_class_like_exists(declaring_class, context) { - if context.class_is_a(&object_class_name, declaring_class, false) { - return Ok(()); - } - } else if object_class_name - .trim_start_matches('\\') - .eq_ignore_ascii_case(declaring_class.trim_start_matches('\\')) - { - return Ok(()); - } - Err(EvalStatus::RuntimeFatal) -} - -/// Validates the object argument shared by non-mutating ReflectionProperty instance APIs. -fn eval_reflection_property_validate_object( - declaring_class: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let identity = values.object_identity(object)?; - let object_class_name = context - .dynamic_object_class(identity) - .map(|class| class.name().to_string()) - .ok_or(EvalStatus::RuntimeFatal)?; - if !context.class_is_a(&object_class_name, declaring_class, false) { - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Resolves and validates the object/property pair targeted by ReflectionProperty. -fn eval_reflection_instance_property_target( - declaring_class: &str, - property_name: &str, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(String, EvalClassProperty), EvalStatus> { - let identity = values.object_identity(object)?; - let object_class_name = context - .dynamic_object_class(identity) - .map(|class| class.name().to_string()) - .ok_or(EvalStatus::RuntimeFatal)?; - if !context.class_is_a(&object_class_name, declaring_class, false) { - return Err(EvalStatus::RuntimeFatal); - } - let (_, property) = context - .class_own_property(declaring_class, property_name) - .ok_or(EvalStatus::RuntimeFatal)?; - if property.is_static() || property.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - Ok((object_class_name, property)) -} - -/// Rejects writes to eval properties ReflectionProperty is not allowed to mutate. -fn validate_eval_reflection_property_write( - declaring_class: &str, - property: &EvalClassProperty, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - if !property.is_readonly() { - return Ok(()); - } - current_eval_property_hook_is( - declaring_class, - property.name(), - &property_hook_set_method(property.name()), - context, - ) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Throws PHP's `ReflectionException` for invalid static-property writes. -fn eval_reflection_static_property_missing_for_set( - reflected_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - eval_throw_reflection_exception( - &format!( - "Class {} does not have a property named {}", - reflected_name, property_name - ), - context, - values, - ) -} - -/// Returns ReflectionProperty default metadata for concrete eval properties. -fn eval_reflection_property_default_value(property: &EvalClassProperty) -> Option { - if let Some(default) = property.default() { - return Some(default.clone()); - } - if property.is_abstract() || property.property_type().is_some() { - return None; - } - Some(EvalExpr::Const(EvalConst::Null)) -} - -/// Returns PHP's required parameter count for a reflected method signature. -fn eval_reflection_required_parameter_count( - defaults: &[Option], - variadic_flags: &[bool], -) -> usize { - let fixed_count = variadic_flags - .iter() - .position(|is_variadic| *is_variadic) - .unwrap_or(defaults.len()); - (0..fixed_count) - .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) - .map_or(0, |position| position + 1) -} - -/// Builds PHP magic scope metadata for a reflected function parameter default. -fn eval_reflection_function_parameter_magic_scope( - function_name: &str, -) -> EvalReflectionParameterMagicScope { - EvalReflectionParameterMagicScope { - function_name: function_name.to_string(), - method_name: function_name.to_string(), - class_name: None, - trait_name: None, - } -} - -/// Builds PHP magic scope metadata for a reflected method parameter default. -fn eval_reflection_method_parameter_magic_scope( - class_name: &str, - function_name: &str, - method_name: &str, - trait_name: Option<&str>, -) -> EvalReflectionParameterMagicScope { - EvalReflectionParameterMagicScope { - function_name: function_name.to_string(), - method_name: method_name.to_string(), - class_name: Some(class_name.trim_start_matches('\\').to_string()), - trait_name: trait_name.map(|trait_name| trait_name.trim_start_matches('\\').to_string()), - } -} - -/// Builds PHP magic scope metadata for an eval method's reflected parameter default. -fn eval_reflection_eval_method_parameter_magic_scope( - class_name: &str, - method: &EvalClassMethod, - fallback_trait_name: Option<&str>, -) -> EvalReflectionParameterMagicScope { - eval_reflection_method_parameter_magic_scope( - class_name, - method.magic_function_name(), - &method.magic_method_name(class_name), - method - .trait_origin() - .or(fallback_trait_name), - ) -} - -/// Builds parameter reflection metadata from eval parameter names and type flags. -fn eval_reflection_parameters_from_names_and_type_flags( - declaring_class_name: Option<&str>, - declaring_function: Option<&EvalReflectionDeclaringFunctionMetadata>, - names: &[String], - has_type_flags: &[bool], - parameter_types: &[Option], - parameter_attributes: &[Vec], - defaults: &[Option], - by_ref_flags: &[bool], - variadic_flags: &[bool], - promoted_parameter_names: &[String], -) -> Vec { - names - .iter() - .enumerate() - .map(|(position, name)| { - let has_type = has_type_flags.get(position).copied().unwrap_or(false); - let default_value = defaults.get(position).and_then(Clone::clone); - let default_value_constant_name = default_value - .as_ref() - .and_then(eval_reflection_default_constant_name); - let type_metadata = parameter_types - .get(position) - .and_then(Option::as_ref) - .and_then(eval_reflection_parameter_type_metadata) - .filter(|_| has_type); - let is_array_type = - eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); - let is_callable_type = - eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); - EvalReflectionParameterMetadata { - name: name.clone(), - declaring_class_name: declaring_class_name.map(str::to_string), - declaring_function: declaring_function.cloned(), - attributes: parameter_attributes - .get(position) - .cloned() - .unwrap_or_default(), - position, - is_optional: defaults.get(position).is_some_and(Option::is_some) - || variadic_flags.get(position).copied().unwrap_or(false), - is_variadic: variadic_flags.get(position).copied().unwrap_or(false), - is_passed_by_reference: by_ref_flags.get(position).copied().unwrap_or(false), - is_promoted: promoted_parameter_names - .iter() - .any(|promoted_name| promoted_name == name), - has_type, - allows_null: eval_reflection_parameter_allows_null( - has_type, - type_metadata.as_ref(), - default_value.as_ref(), - ), - is_array_type, - is_callable_type, - type_metadata, - default_value, - default_value_constant_name, - } - }) - .collect() -} - -/// Returns whether retained parameter metadata is one named type with the requested name. -fn eval_reflection_parameter_has_named_type( - type_metadata: Option<&EvalReflectionParameterTypeMetadata>, - expected_name: &str, -) -> bool { - matches!( - type_metadata, - Some(EvalReflectionParameterTypeMetadata { - kind: EvalReflectionParameterTypeKind::Named(named) - }) if named.name.eq_ignore_ascii_case(expected_name) - ) -} - -/// Returns PHP's ReflectionParameter default-constant name for retained eval defaults. -fn eval_reflection_default_constant_name(default: &EvalExpr) -> Option { - match default { - EvalExpr::ConstFetch(name) => Some(name.clone()), - EvalExpr::NamespacedConstFetch { name, .. } => Some(name.clone()), - EvalExpr::ClassConstantFetch { - class_name, - constant, - } => Some(format!("{}::{}", class_name, constant)), - _ => None, - } -} - -/// Builds ReflectionParameter metadata for eval-declared or native free functions. -fn eval_reflection_function_parameters( - function_name: &str, - names: &[String], - function_attributes: Vec, - parameter_attributes: &[Vec], - parameter_types: &[Option], - defaults: &[Option], - by_ref_flags: &[bool], - variadic_flags: &[bool], -) -> Vec { - let has_type_flags = parameter_types - .iter() - .map(Option::is_some) - .collect::>(); - let flags = eval_reflection_callable_flags(&function_attributes); - let declaring_function = EvalReflectionDeclaringFunctionMetadata { - name: function_name.to_string(), - declaring_class_name: None, - magic_scope: Some(eval_reflection_function_parameter_magic_scope(function_name)), - attributes: function_attributes, - flags, - required_parameter_count: eval_reflection_required_parameter_count( - defaults, - variadic_flags, - ), - }; - eval_reflection_parameters_from_names_and_type_flags( - None, - Some(&declaring_function), - names, - &has_type_flags, - parameter_types, - parameter_attributes, - defaults, - by_ref_flags, - variadic_flags, - &[], - ) -} - -/// Returns promoted constructor parameter names for one eval class method. -fn eval_reflection_promoted_parameter_names( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Vec { - if !method_name.eq_ignore_ascii_case("__construct") { - return Vec::new(); - } - context - .class(class_name) - .map(eval_reflection_promoted_property_names) - .unwrap_or_default() -} - -/// Returns promoted constructor parameter names for one eval trait method. -fn eval_reflection_promoted_trait_parameter_names( - trait_decl: &EvalTrait, - method_name: &str, -) -> Vec { - if method_name.eq_ignore_ascii_case("__construct") { - eval_reflection_promoted_property_names_from_slice(trait_decl.properties()) - } else { - Vec::new() - } -} - -/// Returns property names marked as constructor-promoted in one eval class. -fn eval_reflection_promoted_property_names(class: &EvalClass) -> Vec { - eval_reflection_promoted_property_names_from_slice(class.properties()) -} - -/// Returns property names marked as constructor-promoted in one property list. -fn eval_reflection_promoted_property_names_from_slice( - properties: &[EvalClassProperty], -) -> Vec { - properties - .iter() - .filter(|property| property.is_promoted()) - .map(|property| property.name().to_string()) - .collect() -} - -/// Converts eval parameter type metadata into the supported ReflectionType subset. -fn eval_reflection_parameter_type_metadata( - parameter_type: &EvalParameterType, -) -> Option { - let variants = parameter_type.variants(); - if variants.is_empty() { - return None; - } - let allows_null = parameter_type.allows_null(); - let mut types = variants - .iter() - .map(|variant| eval_reflection_named_type_variant_metadata(variant, false)) - .collect::>>()?; - if types.len() == 1 { - let mut named = types.pop()?; - named.allows_null = allows_null; - return Some(EvalReflectionParameterTypeMetadata { - kind: EvalReflectionParameterTypeKind::Named(named), - }); - } - if parameter_type.is_intersection() { - return Some(EvalReflectionParameterTypeMetadata { - kind: EvalReflectionParameterTypeKind::Intersection( - EvalReflectionIntersectionTypeMetadata { types }, - ), - }); - } - Some(EvalReflectionParameterTypeMetadata { - kind: EvalReflectionParameterTypeKind::Union(EvalReflectionUnionTypeMetadata { - types, - allows_null, - }), - }) -} - -/// Returns PHP's `ReflectionParameter::allowsNull()` value for retained metadata. -fn eval_reflection_parameter_allows_null( - has_type: bool, - type_metadata: Option<&EvalReflectionParameterTypeMetadata>, - default_value: Option<&EvalExpr>, -) -> bool { - !has_type - || default_value.is_some_and(|default| matches!(default, EvalExpr::Const(EvalConst::Null))) - || type_metadata.is_some_and(eval_reflection_type_allows_null) -} - -/// Returns whether one retained ReflectionType metadata value accepts null. -fn eval_reflection_type_allows_null(type_metadata: &EvalReflectionParameterTypeMetadata) -> bool { - match &type_metadata.kind { - EvalReflectionParameterTypeKind::Named(named_type) => named_type.allows_null, - EvalReflectionParameterTypeKind::Union(union_type) => union_type.allows_null, - EvalReflectionParameterTypeKind::Intersection(_) => false, - } -} - -/// Converts one eval parameter type variant into `ReflectionNamedType` metadata. -fn eval_reflection_named_type_variant_metadata( - variant: &EvalParameterTypeVariant, - allows_null: bool, -) -> Option { - match variant { - EvalParameterTypeVariant::Array => { - Some(eval_reflection_builtin_named_type("array", allows_null)) - } - EvalParameterTypeVariant::Bool => { - Some(eval_reflection_builtin_named_type("bool", allows_null)) - } - EvalParameterTypeVariant::Callable => { - Some(eval_reflection_builtin_named_type("callable", allows_null)) - } - EvalParameterTypeVariant::Class(name) => Some(EvalReflectionNamedTypeMetadata { - name: name.clone(), - allows_null, - is_builtin: false, - }), - EvalParameterTypeVariant::Float => { - Some(eval_reflection_builtin_named_type("float", allows_null)) - } - EvalParameterTypeVariant::Int => { - Some(eval_reflection_builtin_named_type("int", allows_null)) - } - EvalParameterTypeVariant::Iterable => { - Some(eval_reflection_builtin_named_type("iterable", allows_null)) - } - EvalParameterTypeVariant::Mixed => Some(eval_reflection_builtin_named_type("mixed", true)), - EvalParameterTypeVariant::Never => Some(eval_reflection_builtin_named_type("never", false)), - EvalParameterTypeVariant::Object => { - Some(eval_reflection_builtin_named_type("object", allows_null)) - } - EvalParameterTypeVariant::String => { - Some(eval_reflection_builtin_named_type("string", allows_null)) - } - EvalParameterTypeVariant::Void => Some(eval_reflection_builtin_named_type("void", false)), - } -} - -/// Builds metadata for one builtin eval `ReflectionNamedType`. -fn eval_reflection_builtin_named_type( - name: &str, - allows_null: bool, -) -> EvalReflectionNamedTypeMetadata { - EvalReflectionNamedTypeMetadata { - name: name.to_string(), - allows_null, - is_builtin: true, - } -} - -/// Returns function or method metadata registered for a synthetic reflection owner object. -fn eval_reflection_function_method_target( - identity: u64, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(name) = context.eval_reflection_function_name(identity) { - let closure_target = context - .eval_reflection_function_closure_target(identity) - .cloned(); - if let Some(closure) = context.closure(name) { - let function = closure.function(); - let is_variadic = function.parameter_is_variadic().iter().any(|flag| *flag); - let parameters = eval_reflection_function_parameters( - function.name(), - function.params(), - function.attributes().to_vec(), - function.parameter_attributes(), - function.parameter_types(), - function.parameter_defaults(), - function.parameter_is_by_ref(), - function.parameter_is_variadic(), - ); - let source_location = function.source_location(); - let return_type_metadata = function - .return_type() - .and_then(eval_reflection_parameter_type_metadata); - let static_key = Some(function.name().to_string()); - let static_variables = static_var_initializers(function.body()); - let is_deprecated = - eval_reflection_attributes_include_deprecated(function.attributes()); - return Ok(Some(EvalReflectionFunctionMethodTarget::Function { - name: name.to_string(), - static_key, - static_variables, - closure_captures: closure.captures().to_vec(), - parameters, - source_location, - closure_target, - is_variadic, - is_static: closure.is_static(), - is_closure: true, - is_deprecated, - return_type_metadata, - })); - } - let lookup_name = name.to_ascii_lowercase(); - if let Some(function) = context.function(&lookup_name) { - let is_variadic = function - .parameter_is_variadic() - .iter() - .any(|flag| *flag); - let parameters = eval_reflection_function_parameters( - function.name(), - function.params(), - function.attributes().to_vec(), - function.parameter_attributes(), - function.parameter_types(), - function.parameter_defaults(), - function.parameter_is_by_ref(), - function.parameter_is_variadic(), - ); - let source_location = function.source_location(); - let return_type_metadata = function - .return_type() - .and_then(eval_reflection_parameter_type_metadata); - let static_key = Some(function.name().to_string()); - let static_variables = static_var_initializers(function.body()); - let is_deprecated = - eval_reflection_attributes_include_deprecated(function.attributes()); - return Ok(Some(EvalReflectionFunctionMethodTarget::Function { - name: name.to_string(), - static_key, - static_variables, - closure_captures: Vec::new(), - parameters, - source_location, - closure_target: closure_target.clone(), - is_variadic, - is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), - is_closure: closure_target.is_some(), - is_deprecated, - return_type_metadata, - })); - } - if let Some(function) = context.native_function(&lookup_name) { - let parameters = eval_reflection_native_function_parameters(name, &function); - let is_variadic = - (0..function.param_count()).any(|index| function.param_variadic(index)); - let return_type_metadata = function - .return_type() - .and_then(eval_reflection_parameter_type_metadata); - return Ok(Some(EvalReflectionFunctionMethodTarget::Function { - name: name.to_string(), - static_key: None, - static_variables: Vec::new(), - closure_captures: Vec::new(), - parameters, - source_location: None, - closure_target: closure_target.clone(), - is_variadic, - is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), - is_closure: closure_target.is_some(), - is_deprecated: false, - return_type_metadata, - })); - } - return Ok(Some(EvalReflectionFunctionMethodTarget::Function { - name: name.to_string(), - static_key: None, - static_variables: Vec::new(), - closure_captures: Vec::new(), - parameters: Vec::new(), - source_location: None, - closure_target: closure_target.clone(), - is_variadic: false, - is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), - is_closure: closure_target.is_some(), - is_deprecated: false, - return_type_metadata: None, - })); - } - let Some((declaring_class, method_name)) = context.eval_reflection_method(identity) else { - return Ok(None); - }; - let method_metadata = if let Some(method_metadata) = - eval_reflection_method_metadata(declaring_class, method_name, context) - { - Some(method_metadata) - } else { - eval_reflection_aot_method_metadata_with_signature_if_exists( - declaring_class, - method_name, - context, - values, - )? - }; - let ( - parameters, - source_file, - source_location, - visibility, - is_variadic, - is_static, - is_final, - is_abstract, - is_deprecated, - return_type_metadata, - ) = match method_metadata { - Some(method) => { - let is_variadic = method - .parameters - .iter() - .any(|parameter| parameter.is_variadic); - let is_deprecated = - eval_reflection_attributes_include_deprecated(&method.attributes); - ( - method.parameters, - method.source_file, - method.source_location, - Some(method.visibility), - is_variadic, - method.is_static, - method.is_final, - method.is_abstract, - is_deprecated, - method.return_type_metadata, - ) - } - None => ( - Vec::new(), - None, - None, - None, - false, - false, - false, - false, - false, - None, - ), - }; - let static_method = - eval_reflection_eval_method_static_target(declaring_class, method_name, context); - let declaring_class = static_method - .as_ref() - .map(|(declaring_class, _)| declaring_class.clone()); - let static_key = static_method - .as_ref() - .map(|(declaring_class, method)| eval_method_static_local_key(declaring_class, method.name())); - let static_variables = static_method - .as_ref() - .map(|(_, method)| static_var_initializers(method.body())) - .unwrap_or_default(); - Ok(Some(EvalReflectionFunctionMethodTarget::Method { - declaring_class, - name: method_name.to_string(), - static_key, - static_variables, - parameters, - source_file, - source_location, - visibility, - is_variadic, - is_static, - is_final, - is_abstract, - is_deprecated, - return_type_metadata, - })) -} - -/// Returns an eval method body that can contribute ReflectionMethod static locals. -fn eval_reflection_eval_method_static_target( - declaring_class: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, EvalClassMethod)> { - if context.has_class(declaring_class) || context.has_enum(declaring_class) { - return context.class_method(declaring_class, method_name); - } - let trait_decl = context.trait_decl(declaring_class)?; - trait_decl - .methods() - .iter() - .find(|method| method.name().eq_ignore_ascii_case(method_name)) - .map(|method| (trait_decl.name().to_string(), method.clone())) -} - -/// Builds the static-local storage key shared by method execution and reflection. -fn eval_method_static_local_key(class_name: &str, method_name: &str) -> String { - format!("{}::{}", class_name.trim_start_matches('\\'), method_name) -} - -/// Builds the associative `getStaticVariables()` result for eval-backed reflection. -fn eval_reflection_function_method_static_variables_result( - target: &EvalReflectionFunctionMethodTarget, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (Some(static_key), static_variables, declaring_class) = - eval_reflection_function_method_static_target(target) - else { - return values.array_new(0); - }; - let mut result = values.assoc_new(static_variables.len())?; - for variable in static_variables { - let key = values.string(&variable.name)?; - let value = eval_reflection_static_local_value( - static_key, - variable, - declaring_class, - context, - values, - )?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Returns static-local storage details retained for a reflected eval function or method. -fn eval_reflection_function_method_static_target( - target: &EvalReflectionFunctionMethodTarget, -) -> ( - Option<&str>, - &[EvalStaticVarInitializer], - Option<&str>, -) { - match target { - EvalReflectionFunctionMethodTarget::Function { - static_key, - static_variables, - .. - } => (static_key.as_deref(), static_variables, None), - EvalReflectionFunctionMethodTarget::Method { - declaring_class, - static_key, - static_variables, - .. - } => ( - static_key.as_deref(), - static_variables, - declaring_class.as_deref(), - ), - } -} - -/// Returns the retained current static value or initializes it for reflection. -fn eval_reflection_static_local_value( - static_key: &str, - variable: &EvalStaticVarInitializer, - declaring_class: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(value) = context.static_local(static_key, &variable.name) { - return values.retain(value); - } - let value = eval_reflection_static_local_initializer_value( - static_key, - &variable.init, - declaring_class, - context, - values, - )?; - if let Some(replaced) = - context.set_static_local(static_key.to_string(), variable.name.clone(), value) - { - values.release(replaced)?; - } - values.retain(value) -} - -/// Evaluates a static-local initializer with PHP magic class/function context. -fn eval_reflection_static_local_initializer_value( - static_key: &str, - init: &EvalExpr, - declaring_class: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(declaring_class) = declaring_class { - context.push_class_scope(declaring_class.to_string()); - context.push_called_class_scope(declaring_class.to_string()); - } - context.push_function(static_key.to_string()); - let mut scope = ElephcEvalScope::new(); - let result = eval_expr(init, context, &mut scope, values); - for cell in scope.drain_owned_cells() { - values.release(cell)?; - } - context.pop_function(); - if declaring_class.is_some() { - context.pop_called_class_scope(); - context.pop_class_scope(); - } - result -} - -/// Validates that a synthetic reflection metadata call received no arguments. -fn eval_reflection_bind_no_args(evaluated_args: Vec) -> Result<(), EvalStatus> { - let _ = bind_evaluated_function_args(&[], evaluated_args)?; - Ok(()) -} - -/// Returns a no-argument reflection metadata predicate result that is always false. -fn eval_reflection_false_metadata_result( - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - eval_reflection_bind_no_args(evaluated_args)?; - values.bool_value(false).map(Some) -} - -/// Returns source file or line metadata for eval-backed reflection objects. -fn eval_reflection_source_location_result( - method_key: &str, - source_file: Option<&str>, - source_location: Option, - evaluated_args: Vec, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - eval_reflection_bind_no_args(evaluated_args)?; - let Some(source_location) = source_location else { - return values.bool_value(false).map(Some); - }; - match method_key { - "getfilename" => { - let eval_file; - let file = if let Some(source_file) = source_file { - source_file - } else { - eval_file = context.eval_file_magic(); - &eval_file - }; - values.string(file).map(Some) - } - "getstartline" => values.int(source_location.start_line()).map(Some), - "getendline" => values.int(source_location.end_line()).map(Some), - _ => Ok(None), - } -} - -/// Returns PHP's short name for a ReflectionFunction or ReflectionMethod target. -fn eval_reflection_function_method_short_name( - target: &EvalReflectionFunctionMethodTarget, -) -> String { - match target { - EvalReflectionFunctionMethodTarget::Function { name, .. } => { - eval_reflection_short_name(name) - } - EvalReflectionFunctionMethodTarget::Method { name, .. } => name.clone(), - } -} - -/// Returns eval-fragment source metadata for a ReflectionFunction or ReflectionMethod target. -fn eval_reflection_function_method_source_location( - target: &EvalReflectionFunctionMethodTarget, -) -> (Option<&str>, Option) { - match target { - EvalReflectionFunctionMethodTarget::Function { - source_location, .. - } => (None, *source_location), - EvalReflectionFunctionMethodTarget::Method { - source_file, - source_location, .. - } => (source_file.as_deref(), *source_location), - } -} - -/// Returns PHP's namespace name for a ReflectionFunction or ReflectionMethod target. -fn eval_reflection_function_method_namespace_name( - target: &EvalReflectionFunctionMethodTarget, -) -> String { - match target { - EvalReflectionFunctionMethodTarget::Function { name, .. } => { - eval_reflection_namespace_name(name) - } - EvalReflectionFunctionMethodTarget::Method { .. } => String::new(), - } -} - -/// Returns whether the reflected function or method has a variadic parameter. -fn eval_reflection_function_method_is_variadic( - target: &EvalReflectionFunctionMethodTarget, -) -> bool { - match target { - EvalReflectionFunctionMethodTarget::Function { is_variadic, .. } - | EvalReflectionFunctionMethodTarget::Method { is_variadic, .. } => *is_variadic, - } -} - -/// Returns whether the reflected function-like target is an eval closure literal. -fn eval_reflection_function_method_is_closure( - target: &EvalReflectionFunctionMethodTarget, -) -> bool { - match target { - EvalReflectionFunctionMethodTarget::Function { is_closure, .. } => *is_closure, - EvalReflectionFunctionMethodTarget::Method { .. } => false, - } -} - -/// Returns whether the reflected function-like target is static. -fn eval_reflection_function_method_is_static(target: &EvalReflectionFunctionMethodTarget) -> bool { - match target { - EvalReflectionFunctionMethodTarget::Function { is_static, .. } - | EvalReflectionFunctionMethodTarget::Method { is_static, .. } => *is_static, - } -} - -/// Returns whether retained Closure target metadata represents a static callable. -fn eval_reflection_closure_target_is_static(target: Option<&EvalClosureObjectTarget>) -> bool { - matches!(target, Some(EvalClosureObjectTarget::StaticMethod { .. })) -} - -/// Returns whether the reflected function-like target carries `#[Deprecated]`. -fn eval_reflection_function_method_is_deprecated( - target: &EvalReflectionFunctionMethodTarget, -) -> bool { - match target { - EvalReflectionFunctionMethodTarget::Function { is_deprecated, .. } - | EvalReflectionFunctionMethodTarget::Method { is_deprecated, .. } => *is_deprecated, - } -} - -/// Builds `ReflectionFunction::getClosureUsedVariables()` for eval closure targets. -fn eval_reflection_function_closure_used_variables_result( - target: &EvalReflectionFunctionMethodTarget, - values: &mut impl RuntimeValueOps, -) -> Result { - let EvalReflectionFunctionMethodTarget::Function { - is_closure: true, - closure_captures, - .. - } = target - else { - return values.array_new(0); - }; - let mut result = values.assoc_new(closure_captures.len())?; - for capture in closure_captures { - let key = values.string(capture.name())?; - let value = values.retain(capture.value())?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Builds `ReflectionFunction::getClosureThis()` from retained Closure target metadata. -fn eval_reflection_function_closure_this_result( - target: &EvalReflectionFunctionMethodTarget, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(target) = eval_reflection_function_closure_target(target) else { - return values.null(); - }; - match target { - EvalClosureObjectTarget::BoundNamed { - bound_this: Some(object), - .. - } - | EvalClosureObjectTarget::InvokableObject { object } - | EvalClosureObjectTarget::ObjectMethod { object, .. } => values.retain(*object), - EvalClosureObjectTarget::Named(_) - | EvalClosureObjectTarget::BoundNamed { - bound_this: None, .. - } - | EvalClosureObjectTarget::StaticMethod { .. } => values.null(), - } -} - -/// Builds `ReflectionFunction::getClosureScopeClass()` from retained Closure metadata. -fn eval_reflection_function_closure_scope_class_result( - target: &EvalReflectionFunctionMethodTarget, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = - eval_reflection_function_closure_scope_class_name(target, context, values)?; - eval_reflection_function_closure_class_object_result(class_name, context, values) -} - -/// Builds `ReflectionFunction::getClosureCalledClass()` from retained Closure metadata. -fn eval_reflection_function_closure_called_class_result( - target: &EvalReflectionFunctionMethodTarget, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = - eval_reflection_function_closure_called_class_name(target, context, values)?; - eval_reflection_function_closure_class_object_result(class_name, context, values) -} - -/// Returns the retained callable target for a Closure-backed ReflectionFunction. -fn eval_reflection_function_closure_target( - target: &EvalReflectionFunctionMethodTarget, -) -> Option<&EvalClosureObjectTarget> { - match target { - EvalReflectionFunctionMethodTarget::Function { closure_target, .. } => { - closure_target.as_ref() - } - EvalReflectionFunctionMethodTarget::Method { .. } => None, - } -} - -/// Resolves the PHP closure scope class name for retained Closure target metadata. -fn eval_reflection_function_closure_scope_class_name( - target: &EvalReflectionFunctionMethodTarget, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(target) = eval_reflection_function_closure_target(target) else { - return Ok(None); - }; - match target { - EvalClosureObjectTarget::Named(_) => Ok(None), - EvalClosureObjectTarget::BoundNamed { - name, - bound_this, - bound_scope, - } => { - if context.closure(name).is_none() { - return Ok(bound_this.map(|_| String::from("Closure"))); - } - if let Some(bound_scope) = bound_scope { - return Ok(Some(bound_scope.clone())); - } - match bound_this { - Some(object) => eval_closure_bound_object_class_name(*object, context, values) - .map(Some), - None => Ok(None), - } - } - EvalClosureObjectTarget::InvokableObject { object } - | EvalClosureObjectTarget::ObjectMethod { object, .. } => { - eval_closure_bound_object_class_name(*object, context, values).map(Some) - } - EvalClosureObjectTarget::StaticMethod { class_name, .. } => { - Ok(Some(class_name.trim_start_matches('\\').to_string())) - } - } -} - -/// Resolves the PHP closure called class name for retained Closure target metadata. -fn eval_reflection_function_closure_called_class_name( - target: &EvalReflectionFunctionMethodTarget, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(target) = eval_reflection_function_closure_target(target) else { - return Ok(None); - }; - match target { - EvalClosureObjectTarget::Named(_) => Ok(None), - EvalClosureObjectTarget::BoundNamed { - bound_this, - bound_scope, - .. - } => match bound_this { - Some(object) => eval_closure_bound_object_class_name(*object, context, values) - .map(Some), - None => Ok(bound_scope.clone()), - }, - EvalClosureObjectTarget::InvokableObject { object } => { - eval_closure_bound_object_class_name(*object, context, values).map(Some) - } - EvalClosureObjectTarget::ObjectMethod { - object, - called_class, - .. - } => match called_class { - Some(called_class) => Ok(Some(called_class.clone())), - None => eval_closure_bound_object_class_name(*object, context, values).map(Some), - }, - EvalClosureObjectTarget::StaticMethod { - class_name, - called_class, - .. - } => Ok(Some( - called_class - .as_deref() - .unwrap_or(class_name) - .trim_start_matches('\\') - .to_string(), - )), - } -} - -/// Materializes a nullable ReflectionClass result for Closure scope metadata. -fn eval_reflection_function_closure_class_object_result( - class_name: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(class_name) = class_name else { - return values.null(); - }; - eval_reflection_full_class_object_result(&class_name, context, values) -} - -/// Returns the retained return type metadata for a reflected function or method. -fn eval_reflection_function_method_return_type( - target: &EvalReflectionFunctionMethodTarget, -) -> Option<&EvalReflectionParameterTypeMetadata> { - match target { - EvalReflectionFunctionMethodTarget::Function { - return_type_metadata, - .. - } - | EvalReflectionFunctionMethodTarget::Method { - return_type_metadata, - .. - } => return_type_metadata.as_ref(), - } -} - -/// Returns the final namespace segment-free name component from a PHP symbol name. -fn eval_reflection_short_name(name: &str) -> String { - let name = name.trim_start_matches('\\'); - name.rsplit_once('\\').map_or_else( - || name.to_string(), - |(_, short_name)| short_name.to_string(), - ) -} - -/// Returns the namespace prefix from a PHP function name, or an empty string. -fn eval_reflection_namespace_name(name: &str) -> String { - name.trim_start_matches('\\') - .rsplit_once('\\') - .map_or_else(String::new, |(namespace_name, _)| { - namespace_name.to_string() - }) -} - -/// Builds ReflectionMethod metadata for a resolved eval or AOT prototype target. -fn eval_reflection_prototype_method_metadata( - prototype_class: &str, - prototype_method: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if let Some(metadata) = - eval_reflection_method_metadata(prototype_class, prototype_method, context) - { - return Ok(Some(metadata)); - } - eval_reflection_aot_method_metadata_with_signature_if_exists( - prototype_class, - prototype_method, - context, - values, - ) -} - -/// Finds the PHP ReflectionMethod prototype target for an eval or AOT method. -fn eval_reflection_method_prototype_target( - declaring_class: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if context.has_class(declaring_class) || context.has_enum(declaring_class) { - let want_static = eval_reflection_method_metadata(declaring_class, method_name, context) - .map_or(false, |metadata| metadata.is_static); - if let Some(prototype) = - eval_reflection_parent_method_prototype_target(declaring_class, method_name, context) - { - return Ok(Some(prototype)); - } - if let Some(prototype) = eval_reflection_eval_aot_parent_method_prototype_target( - declaring_class, - method_name, - context, - want_static, - values, - )? { - return Ok(Some(prototype)); - } - if let Some(prototype) = - eval_reflection_interface_method_prototype_target(declaring_class, method_name, context) - { - return Ok(Some(prototype)); - } - return eval_reflection_aot_interface_method_prototype_target_for_eval( - declaring_class, - method_name, - context, - want_static, - values, - ); - } - eval_reflection_aot_method_prototype_target(declaring_class, method_name, values) -} - -/// Finds the PHP ReflectionMethod prototype target for a generated/AOT method. -fn eval_reflection_aot_method_prototype_target( - declaring_class: &str, - method_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(flags) = values.reflection_method_flags(declaring_class, method_name)? else { - return Ok(None); - }; - if let Some(prototype) = - eval_reflection_aot_parent_method_prototype_target(declaring_class, method_name, flags, values)? - { - return Ok(Some(prototype)); - } - eval_reflection_aot_interface_method_prototype_target( - declaring_class, - method_name, - flags, - values, - ) -} - -/// Finds the nearest generated/AOT parent-class method that can act as prototype. -fn eval_reflection_aot_parent_method_prototype_target( - declaring_class: &str, - method_name: &str, - method_flags: u64, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let want_static = method_flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - let mut current = eval_reflection_aot_parent_class_name(declaring_class, values)?; - let mut seen = std::collections::HashSet::new(); - while let Some(parent_class) = current { - if !seen.insert(parent_class.to_ascii_lowercase()) { - break; - } - if let Some(prototype) = - eval_reflection_aot_method_candidate(&parent_class, method_name, want_static, values)? - { - return Ok(Some(prototype)); - } - current = eval_reflection_aot_parent_class_name(&parent_class, values)?; - } - Ok(None) -} - -/// Finds the first generated/AOT interface method that can act as prototype. -fn eval_reflection_aot_interface_method_prototype_target( - declaring_class: &str, - method_name: &str, - method_flags: u64, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let want_static = method_flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - for interface_name in eval_reflection_aot_class_interface_names(declaring_class, values)? { - if let Some(prototype) = - eval_reflection_aot_method_candidate(&interface_name, method_name, want_static, values)? - { - return Ok(Some(prototype)); - } - } - Ok(None) -} - -/// Returns one generated/AOT method prototype candidate if staticness and visibility match. -fn eval_reflection_aot_method_candidate( - class_name: &str, - method_name: &str, - want_static: bool, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { - return Ok(None); - }; - let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - let is_private = flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0; - if is_static != want_static || is_private { - return Ok(None); - } - let declaring_class = values - .reflection_method_declaring_class(class_name, method_name)? - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); - Ok(Some((declaring_class, method_name.to_ascii_lowercase()))) -} - -/// Finds the nearest parent-class method prototype for an eval-declared override. -fn eval_reflection_parent_method_prototype_target( - declaring_class: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, String)> { - for parent_class in context.class_parent_names(declaring_class) { - if let Some((prototype_class, prototype_method)) = - context.class_own_method(&parent_class, method_name) - { - return Some((prototype_class, prototype_method.name().to_string())); - } - } - None -} - -/// Finds the nearest generated/AOT parent-class method prototype for an eval method. -fn eval_reflection_eval_aot_parent_method_prototype_target( - declaring_class: &str, - method_name: &str, - context: &ElephcEvalContext, - want_static: bool, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(parent_class) = context.class_native_parent_name(declaring_class) else { - return Ok(None); - }; - eval_reflection_aot_method_candidate(&parent_class, method_name, want_static, values) -} - -/// Finds the interface method prototype for an eval-declared class method. -fn eval_reflection_interface_method_prototype_target( - declaring_class: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, String)> { - let mut seen = std::collections::HashSet::new(); - for interface_name in context.class_interface_names(declaring_class) { - if let Some(prototype) = eval_reflection_interface_declared_method_target( - &interface_name, - method_name, - context, - &mut seen, - ) { - return Some(prototype); - } - } - None -} - -/// Finds an AOT interface method prototype for an eval-declared class method. -fn eval_reflection_aot_interface_method_prototype_target_for_eval( - declaring_class: &str, - method_name: &str, - context: &ElephcEvalContext, - want_static: bool, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - for interface_name in eval_reflection_eval_class_interface_names( - declaring_class, - context, - values, - )? { - if context.has_interface(&interface_name) { - continue; - } - if let Some(prototype) = - eval_reflection_aot_method_candidate(&interface_name, method_name, want_static, values)? - { - return Ok(Some(prototype)); - } - } - Ok(None) -} - -/// Finds the interface that actually declares a method in an interface hierarchy. -fn eval_reflection_interface_declared_method_target( - interface_name: &str, - method_name: &str, - context: &ElephcEvalContext, - seen: &mut std::collections::HashSet, -) -> Option<(String, String)> { - let interface = context.interface(interface_name)?; - if !seen.insert(interface.name().to_ascii_lowercase()) { - return None; - } - if let Some(method) = interface - .methods() - .iter() - .find(|method| method.name().eq_ignore_ascii_case(method_name)) - { - return Some((interface.name().to_string(), method.name().to_string())); - } - for parent in interface.parents() { - if let Some(prototype) = - eval_reflection_interface_declared_method_target(parent, method_name, context, seen) - { - return Some(prototype); - } - } - None -} - -/// Packs ReflectionMethod/ReflectionProperty predicate flags for the runtime owner factory. -fn eval_reflection_member_flags( - visibility: EvalVisibility, - is_static: bool, - is_final: bool, - is_abstract: bool, - is_readonly: bool, -) -> u64 { - let mut flags = 0; - if is_static { - flags |= EVAL_REFLECTION_MEMBER_FLAG_STATIC; - } - match visibility { - EvalVisibility::Public => flags |= EVAL_REFLECTION_MEMBER_FLAG_PUBLIC, - EvalVisibility::Protected => flags |= EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, - EvalVisibility::Private => flags |= EVAL_REFLECTION_MEMBER_FLAG_PRIVATE, - } - if is_final { - flags |= EVAL_REFLECTION_MEMBER_FLAG_FINAL; - } - if is_abstract { - flags |= EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT; - } - if is_readonly { - flags |= EVAL_REFLECTION_MEMBER_FLAG_READONLY; - } - flags -} - -/// Packs callable-only ReflectionFunctionAbstract predicate flags. -fn eval_reflection_callable_flags(attributes: &[EvalAttribute]) -> u64 { - if eval_reflection_attributes_include_deprecated(attributes) { - EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED - } else { - 0 - } -} - -/// Returns whether an attribute list contains PHP's global `#[Deprecated]` marker. -fn eval_reflection_attributes_include_deprecated(attributes: &[EvalAttribute]) -> bool { - attributes - .iter() - .any(|attribute| attribute.name().eq_ignore_ascii_case("Deprecated")) -} - -/// Packs ReflectionParameter predicate flags for the runtime parameter factory. -fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) -> u64 { - let mut flags = 0; - if parameter.is_optional { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL; - } - if parameter.is_variadic { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC; - } - if parameter.is_passed_by_reference { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_BY_REF; - } - if parameter.is_promoted { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED; - } - if parameter.has_type { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE; - } - if parameter.default_value.is_some() { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE; - } - if parameter.allows_null { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL; - } - if parameter.default_value_constant_name.is_some() { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT; - } - if parameter.is_array_type { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE; - } - if parameter.is_callable_type { - flags |= EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE; - } - flags -} - -/// Packs ReflectionNamedType predicate flags for the runtime type factory. -fn eval_reflection_named_type_flags(type_metadata: &EvalReflectionNamedTypeMetadata) -> u64 { - let mut flags = 0; - if type_metadata.allows_null { - flags |= EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL; - } - if type_metadata.is_builtin { - flags |= EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN; - } - flags -} - -/// Packs ReflectionUnionType predicate flags for the runtime type factory. -fn eval_reflection_union_type_flags(type_metadata: &EvalReflectionUnionTypeMetadata) -> u64 { - if type_metadata.allows_null { - EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL - } else { - 0 - } -} - -/// Converts a ReflectionFunction argument into a function or eval-closure name. -fn eval_reflection_function_name_arg( - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(value)? == EVAL_TAG_OBJECT { - let identity = values.object_identity(value)?; - if let Some(name) = context.closure_object_name(identity) { - return Ok(name.to_string()); - } - } - eval_reflection_string_arg(value, values) -} - -/// Converts one reflection constructor argument to a Rust UTF-8 string. -fn eval_reflection_string_arg( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Maps a PHP reflection owner class name to the helper owner kind. -fn reflection_owner_kind(class_name: &str) -> Option { - match class_name - .trim_start_matches('\\') - .to_ascii_lowercase() - .as_str() - { - "reflectionclass" => Some(EVAL_REFLECTION_OWNER_CLASS), - "reflectionobject" => Some(EVAL_REFLECTION_OWNER_OBJECT), - "reflectionenum" => Some(EVAL_REFLECTION_OWNER_ENUM), - "reflectionfunction" => Some(EVAL_REFLECTION_OWNER_FUNCTION), - "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), - "reflectionproperty" => Some(EVAL_REFLECTION_OWNER_PROPERTY), - "reflectionparameter" => Some(EVAL_REFLECTION_OWNER_PARAMETER), - "reflectionclassconstant" => Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT), - "reflectionenumunitcase" => Some(EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE), - "reflectionenumbackedcase" => Some(EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE), - _ => None, - } -} diff --git a/crates/elephc-magician/src/interpreter/reflection/callable_api.rs b/crates/elephc-magician/src/interpreter/reflection/callable_api.rs new file mode 100644 index 0000000000..742385ba17 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/callable_api.rs @@ -0,0 +1,610 @@ +//! Purpose: +//! Implements callable, parameter, and type Reflection method dispatch. +//! +//! Called from: +//! - `crate::interpreter::statements` for ReflectionFunctionAbstract owners. +//! +//! Key details: +//! - Invocation, metadata predicates, prototypes, and PHP string forms share one target lookup. + +use super::*; + +/// Handles eval-backed `ReflectionMethod::invoke()` and `invokeArgs()` calls. +pub(in crate::interpreter) fn eval_reflection_method_invoke_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_invoke = method_name.eq_ignore_ascii_case("invoke"); + let is_invoke_args = method_name.eq_ignore_ascii_case("invokeArgs"); + if !is_invoke && !is_invoke_args { + return Ok(None); + } + let Some((declaring_class, reflected_method)) = context + .eval_reflection_method(identity) + .map(|(declaring_class, method)| (declaring_class.to_string(), method.to_string())) + else { + return Ok(None); + }; + let (object, method_args) = if is_invoke { + eval_reflection_method_invoke_args(evaluated_args)? + } else { + eval_reflection_method_invoke_args_array(evaluated_args, context, values)? + }; + eval_reflection_method_invoke_dispatch( + &declaring_class, + &reflected_method, + object, + method_args, + context, + values, + ) + .map(Some) +} + +/// Handles eval-backed `ReflectionFunction::invoke()` and `invokeArgs()` calls. +pub(in crate::interpreter) fn eval_reflection_function_invoke_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_invoke = method_name.eq_ignore_ascii_case("invoke"); + let is_invoke_args = method_name.eq_ignore_ascii_case("invokeArgs"); + if !is_invoke && !is_invoke_args { + return Ok(None); + } + let Some(function_name) = context + .eval_reflection_function_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let function_args = if is_invoke { + evaluated_args + .into_iter() + .map(eval_reflection_method_forwarded_value_arg) + .collect() + } else { + eval_reflection_function_invoke_args_array(evaluated_args, context, values)? + }; + eval_reflection_function_invoke_dispatch(&function_name, function_args, context, values) + .map(Some) +} + +/// Handles eval-backed ReflectionFunctionAbstract name/origin metadata calls. +pub(in crate::interpreter) fn eval_reflection_function_method_metadata_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { + return Ok(None); + }; + let method_key = method_name.to_ascii_lowercase(); + match method_key.as_str() { + "getshortname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_function_method_short_name(&target)) + .map(Some) + } + "getnamespacename" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_function_method_namespace_name(&target)) + .map(Some) + } + "innamespace" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(!eval_reflection_function_method_namespace_name(&target).is_empty()) + .map(Some) + } + "getfilename" | "getstartline" | "getendline" => { + let (source_file, source_location) = + eval_reflection_function_method_source_location(&target); + eval_reflection_source_location_result( + method_key.as_str(), + source_file, + source_location, + evaluated_args, + context, + values, + ) + } + "isinternal" | "returnsreference" | "isgenerator" | "hastentativereturntype" => { + eval_reflection_false_metadata_result(evaluated_args, values) + } + "isclosure" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_closure(&target)) + .map(Some) + } + "isdeprecated" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_deprecated(&target)) + .map(Some) + } + "isanonymous" => match target { + EvalReflectionFunctionMethodTarget::Function { .. } => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_closure(&target)) + .map(Some) + } + EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), + }, + "hasreturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_return_type(&target).is_some()) + .map(Some) + } + "isuserdefined" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(true).map(Some) + } + "isvariadic" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_variadic(&target)) + .map(Some) + } + "isstatic" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(eval_reflection_function_method_is_static(&target)) + .map(Some) + } + "isdisabled" => match target { + EvalReflectionFunctionMethodTarget::Function { .. } => { + eval_reflection_false_metadata_result(evaluated_args, values) + } + EvalReflectionFunctionMethodTarget::Method { .. } => Ok(None), + }, + "getreturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + match eval_reflection_function_method_return_type(&target) { + Some(type_metadata) => { + eval_reflection_type_object_result(type_metadata, values).map(Some) + } + None => values.null().map(Some), + } + } + "gettentativereturntype" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.null().map(Some) + } + "getstaticvariables" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_method_static_variables_result(&target, context, values) + .map(Some) + } + "getclosureusedvariables" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_used_variables_result(&target, values).map(Some) + } + "getclosurethis" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_this_result(&target, values).map(Some) + } + "getclosurescopeclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_scope_class_result(&target, context, values) + .map(Some) + } + "getclosurecalledclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_function_closure_called_class_result(&target, context, values) + .map(Some) + } + _ => Ok(None), + } +} + +/// Handles eval-backed `ReflectionFunction::__toString()` and `ReflectionMethod::__toString()`. +pub(in crate::interpreter) fn eval_reflection_function_method_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some(target) = eval_reflection_function_method_target(identity, context, values)? else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_function_method_to_string(&target); + values.string(&rendered).map(Some) +} + +/// Handles eval-backed `ReflectionParameter::isArray()` and `isCallable()` calls. +pub(in crate::interpreter) fn eval_reflection_parameter_legacy_type_predicate_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(expected_type) = eval_reflection_parameter_legacy_type_name(method_name) else { + return Ok(None); + }; + if !eval_reflection_object_has_class(object, "ReflectionParameter", values)? { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let type_value = values.method_call(object, "getType", Vec::new())?; + if values.is_null(type_value)? { + return values.bool_value(false).map(Some); + } + if !eval_reflection_object_has_class(type_value, "ReflectionNamedType", values)? { + return values.bool_value(false).map(Some); + } + let name = values.method_call(type_value, "getName", Vec::new())?; + let bytes = values.string_bytes(name)?; + let name = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + values + .bool_value(name.eq_ignore_ascii_case(expected_type)) + .map(Some) +} + +/// Handles eval-backed `ReflectionParameter::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_parameter_to_string_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + if !eval_reflection_object_has_class(object, "ReflectionParameter", values)? { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_parameter_object_to_string(object, values)?; + values.string(&rendered).map(Some) +} + +/// Handles eval-backed `ReflectionType::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_type_to_string_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some(rendered) = eval_reflection_type_object_to_string(object, values)? else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + values.string(&rendered).map(Some) +} + +/// Formats one ReflectionParameter object through its public metadata methods. +pub(super) fn eval_reflection_parameter_object_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let position = eval_reflection_no_arg_int_method(object, "getPosition", values)?; + let name = eval_reflection_no_arg_string_method(object, "getName", values)?; + let is_optional = eval_reflection_no_arg_bool_method(object, "isOptional", values)?; + let is_passed_by_reference = + eval_reflection_no_arg_bool_method(object, "isPassedByReference", values)?; + let is_variadic = eval_reflection_no_arg_bool_method(object, "isVariadic", values)?; + let type_value = values.method_call(object, "getType", Vec::new())?; + let type_text = if values.is_null(type_value)? { + None + } else { + eval_reflection_type_object_to_string(type_value, values)? + }; + + let mut signature_parts = Vec::new(); + if let Some(type_text) = type_text { + signature_parts.push(type_text); + } + let mut variable = String::new(); + if is_passed_by_reference { + variable.push('&'); + } + if is_variadic { + variable.push_str("..."); + } + variable.push('$'); + variable.push_str(&name); + signature_parts.push(variable); + let requiredness = if is_optional { "optional" } else { "required" }; + let default = eval_reflection_parameter_object_default_to_string(object, values)? + .map(|value| format!(" = {value}")) + .unwrap_or_default(); + + Ok(format!( + "Parameter #{} [ <{}> {}{} ]", + position, + requiredness, + signature_parts.join(" "), + default + )) +} + +/// Formats a ReflectionParameter default through its public metadata methods. +pub(super) fn eval_reflection_parameter_object_default_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_no_arg_bool_method(object, "isDefaultValueAvailable", values)? { + return Ok(None); + } + if eval_reflection_no_arg_bool_method(object, "isDefaultValueConstant", values)? { + let constant_name = + eval_reflection_no_arg_string_method(object, "getDefaultValueConstantName", values)?; + if !constant_name.is_empty() { + return Ok(Some(constant_name)); + } + } + let default_value = values.method_call(object, "getDefaultValue", Vec::new())?; + eval_reflection_runtime_default_value_to_string(default_value, values).map(Some) +} + +/// Formats one materialized scalar-ish default value for reflection string output. +pub(super) fn eval_reflection_runtime_default_value_to_string( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(match values.type_tag(value)? { + EVAL_TAG_NULL => String::from("NULL"), + EVAL_TAG_BOOL => { + if values.truthy(value)? { + String::from("true") + } else { + String::from("false") + } + } + EVAL_TAG_INT | EVAL_TAG_FLOAT => String::from_utf8_lossy(&values.string_bytes(value)?) + .into_owned(), + EVAL_TAG_STRING => { + let value = String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(); + format!("'{value}'") + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC if values.array_len(value)? == 0 => String::from("[]"), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("Array"), + EVAL_TAG_OBJECT => String::from("Object"), + _ => String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(), + }) +} + +/// Calls one no-arg Reflection method and returns its string result. +pub(super) fn eval_reflection_no_arg_string_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + eval_reflection_string_arg(value, values) +} + +/// Calls one no-arg Reflection method and returns its bool result. +pub(super) fn eval_reflection_no_arg_bool_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + if values.type_tag(value)? != EVAL_TAG_BOOL { + return Err(EvalStatus::RuntimeFatal); + } + values.truthy(value) +} + +/// Calls one no-arg Reflection method and returns its int result. +pub(super) fn eval_reflection_no_arg_int_method( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + eval_int_value(value, values) +} + +/// Formats one eval-visible ReflectionType object if the value is a retained type object. +pub(super) fn eval_reflection_type_object_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let type_kind = if eval_reflection_object_has_class(object, "ReflectionNamedType", values)? { + Some(("ReflectionNamedType", "")) + } else if eval_reflection_object_has_class(object, "ReflectionUnionType", values)? { + Some(("ReflectionUnionType", "|")) + } else if eval_reflection_object_has_class(object, "ReflectionIntersectionType", values)? { + Some(("ReflectionIntersectionType", "&")) + } else { + None + }; + let Some((class_name, separator)) = type_kind else { + return Ok(None); + }; + let rendered = if class_name == "ReflectionNamedType" { + eval_reflection_named_type_to_string(object, values)? + } else { + eval_reflection_composite_type_to_string(object, separator, values)? + }; + Ok(Some(rendered)) +} + +/// Formats one eval-visible ReflectionNamedType object from its public methods. +pub(super) fn eval_reflection_named_type_to_string( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let name = eval_reflection_type_method_string(object, "getName", values)?; + let allows_null = eval_reflection_type_method_bool(object, "allowsNull", values)?; + if allows_null && name != "mixed" { + Ok(format!("?{name}")) + } else { + Ok(name) + } +} + +/// Formats one eval-visible ReflectionUnionType or ReflectionIntersectionType object. +pub(super) fn eval_reflection_composite_type_to_string( + object: RuntimeCellHandle, + separator: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let types = values.method_call(object, "getTypes", Vec::new())?; + let mut names = Vec::new(); + for position in 0..values.array_len(types)? { + let key = values.array_iter_key(types, position)?; + let member = values.array_get(types, key)?; + names.push(eval_reflection_type_method_string(member, "getName", values)?); + } + if separator == "|" && eval_reflection_type_method_bool(object, "allowsNull", values)? { + names.push(String::from("null")); + } + Ok(names.join(separator)) +} + +/// Calls one no-arg ReflectionType method and returns its string result. +pub(super) fn eval_reflection_type_method_string( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Calls one no-arg ReflectionType method and returns its bool result. +pub(super) fn eval_reflection_type_method_bool( + object: RuntimeCellHandle, + method: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let value = values.method_call(object, method, Vec::new())?; + if values.type_tag(value)? != EVAL_TAG_BOOL { + return Err(EvalStatus::RuntimeFatal); + } + values.truthy(value) +} + +/// Maps a legacy ReflectionParameter predicate method to its target named type. +pub(super) fn eval_reflection_parameter_legacy_type_name(method_name: &str) -> Option<&'static str> { + if method_name.eq_ignore_ascii_case("isArray") { + Some("array") + } else if method_name.eq_ignore_ascii_case("isCallable") { + Some("callable") + } else { + None + } +} + +/// Returns whether one runtime object cell has the requested PHP class name. +pub(super) fn eval_reflection_object_has_class( + object: RuntimeCellHandle, + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Ok(false); + } + let actual = values.object_class_name(object)?; + let bytes = values.string_bytes(actual); + values.release(actual)?; + let actual = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(actual + .trim_start_matches('\\') + .eq_ignore_ascii_case(class_name)) +} + +/// Handles eval-backed `ReflectionMethod::hasPrototype()` and `getPrototype()` calls. +pub(in crate::interpreter) fn eval_reflection_method_prototype_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let is_has_prototype = method_name.eq_ignore_ascii_case("hasPrototype"); + let is_get_prototype = method_name.eq_ignore_ascii_case("getPrototype"); + if !is_has_prototype && !is_get_prototype { + return Ok(None); + } + let Some((declaring_class, reflected_method)) = + context + .eval_reflection_method(identity) + .map(|(declaring_class, method_name)| { + (declaring_class.to_string(), method_name.to_string()) + }) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let Some((prototype_class, prototype_method)) = eval_reflection_method_prototype_target( + &declaring_class, + &reflected_method, + context, + values, + )? + else { + if is_has_prototype { + return values.bool_value(false).map(Some); + } + return eval_throw_reflection_exception( + &format!( + "Method {}::{} does not have a prototype", + declaring_class, reflected_method + ), + context, + values, + ); + }; + if is_has_prototype { + return values.bool_value(true).map(Some); + } + let Some(metadata) = + eval_reflection_prototype_method_metadata(&prototype_class, &prototype_method, context, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &prototype_method, + &metadata, + context, + values, + ) + .map(Some) +} + +/// Handles PHP's no-op `ReflectionMethod/Property::setAccessible()` calls. +pub(in crate::interpreter) fn eval_reflection_set_accessible_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setAccessible") { + return Ok(None); + } + if context.eval_reflection_method(identity).is_none() + && context.eval_reflection_property(identity).is_none() + { + return Ok(None); + } + let _ = bind_evaluated_function_args(&[String::from("accessible")], evaluated_args)?; + values.null().map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/class_api.rs b/crates/elephc-magician/src/interpreter/reflection/class_api.rs new file mode 100644 index 0000000000..4a10aa3f39 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/class_api.rs @@ -0,0 +1,899 @@ +//! Purpose: +//! Implements eval-backed `ReflectionClass` query and static-property APIs. +//! It keeps the public method dispatch layer separate from owner construction. +//! +//! Called from: +//! - `crate::interpreter::statements` while dispatching Reflection methods. +//! +//! Key details: +//! - Queries combine eval declarations with focused AOT metadata hooks. +//! - PHP-visible errors and missing-member results are preserved at this boundary. + +use super::*; + +/// Handles eval-backed `ReflectionClass::implementsInterface()` calls. +pub(in crate::interpreter) fn eval_reflection_class_implements_interface_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("implementsInterface") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("interface")], evaluated_args)?; + let interface_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_interface_exists(&interface_name, context, values)? { + if eval_reflection_non_interface_exists(&interface_name, context, values)? { + return eval_throw_reflection_exception( + &format!("{} is not an interface", interface_name), + context, + values, + ); + } + return eval_throw_reflection_exception( + &format!("Interface \"{}\" does not exist", interface_name), + context, + values, + ); + } + let result = if eval_reflection_class_like_exists(&reflected_name, context) { + eval_reflection_class_implements_interface_name( + &reflected_name, + &interface_name, + context, + values, + )? + } else if eval_runtime_interface_exists(&reflected_name, values)? { + eval_reflection_same_class_like_name(&reflected_name, &interface_name) + } else { + let reflected_class = values.string(&reflected_name)?; + let result = values.object_is_a(reflected_class, &interface_name, false); + values.release(reflected_class)?; + result? + }; + values.bool_value(result).map(Some) +} + +/// Handles eval-backed `ReflectionClass::isSubclassOf()` calls. +pub(in crate::interpreter) fn eval_reflection_class_is_subclass_of_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isSubclassOf") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("class")], evaluated_args)?; + let target_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_class_like_exists(&target_name, context) + && !values.class_exists(&target_name)? + && !eval_runtime_interface_exists(&target_name, values)? + && !values.trait_exists(&target_name)? + && !values.enum_exists(&target_name)? + { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", target_name), + context, + values, + ); + } + let result = if eval_reflection_class_like_exists(&reflected_name, context) { + eval_reflection_class_is_subclass_of_name( + &reflected_name, + &target_name, + context, + values, + )? + } else { + let reflected_class = values.string(&reflected_name)?; + let result = values.object_is_a(reflected_class, &target_name, true)?; + values.release(reflected_class)?; + result + }; + values.bool_value(result).map(Some) +} + +/// Handles eval-backed `ReflectionClass::isInstance()` calls. +pub(in crate::interpreter) fn eval_reflection_class_is_instance_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isInstance") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + let object = args[0]; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let result = dynamic_object_is_a(object, &reflected_name, false, context, values)? + .map_or_else(|| values.object_is_a(object, &reflected_name, false), Ok)?; + values.bool_value(result).map(Some) +} + +/// Handles eval-backed `ReflectionClass` source-location metadata calls. +pub(in crate::interpreter) fn eval_reflection_class_source_location_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method_key = method_name.to_ascii_lowercase(); + if !matches!( + method_key.as_str(), + "getfilename" | "getstartline" | "getendline" + ) { + return Ok(None); + } + let Some(reflected_name) = context.eval_reflection_class_name(identity) else { + return Ok(None); + }; + let (source_file, source_location) = + if let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) { + (None, metadata.source_location) + } else { + eval_reflection_aot_class_source_metadata(reflected_name, values)? + }; + eval_reflection_source_location_result( + method_key.as_str(), + source_file.as_deref(), + source_location, + evaluated_args, + context, + values, + ) +} + +/// Returns AOT source-file and line metadata for a generated ReflectionClass. +fn eval_reflection_aot_class_source_metadata( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, Option), EvalStatus> { + let Some(flags) = values.reflection_class_flags(class_name.trim_start_matches('\\'))? else { + return Ok((None, None)); + }; + let Some(source_location) = eval_reflection_aot_class_source_location_from_flags(flags) else { + return Ok((None, None)); + }; + let Some(source_file) = values.reflection_source_file()? else { + return Ok((None, None)); + }; + Ok((Some(source_file), Some(source_location))) +} + +/// Decodes AOT ReflectionClass source lines packed into high flag bits. +fn eval_reflection_aot_class_source_location_from_flags(flags: u64) -> Option { + let start_line = ((flags >> EVAL_REFLECTION_CLASS_SOURCE_START_SHIFT) + & EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK) as i64; + let end_line = ((flags >> EVAL_REFLECTION_CLASS_SOURCE_END_SHIFT) + & EVAL_REFLECTION_CLASS_SOURCE_LINE_MASK) as i64; + (start_line > 0 && end_line >= start_line) + .then(|| EvalSourceLocation::new(start_line, end_line)) +} + +/// Handles eval-backed `ReflectionClass` scalar metadata methods. +pub(in crate::interpreter) fn eval_reflection_class_basic_metadata_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { + return Ok(None); + }; + let method_key = method_name.to_ascii_lowercase(); + match method_key.as_str() { + "getname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.string(&metadata.resolved_name).map(Some) + } + "getshortname" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_short_name(&metadata.resolved_name)) + .map(Some) + } + "getnamespacename" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .string(&eval_reflection_namespace_name(&metadata.resolved_name)) + .map(Some) + } + "innamespace" => { + eval_reflection_bind_no_args(evaluated_args)?; + values + .bool_value(!eval_reflection_namespace_name(&metadata.resolved_name).is_empty()) + .map(Some) + } + "getinterfacenames" => { + eval_reflection_bind_no_args(evaluated_args)?; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + eval_reflection_string_array_result(&interface_names, values).map(Some) + } + "gettraitnames" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_string_array_result(&metadata.trait_names, values).map(Some) + } + "getparentclass" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_related_class_result( + EVAL_REFLECTION_OWNER_CLASS, + metadata.parent_class_name.as_deref(), + true, + context, + values, + ) + .map(Some) + } + "getconstructor" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_constructor_object_result( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + true, + context, + values, + ) + .map(Some) + } + "getmodifiers" => { + eval_reflection_bind_no_args(evaluated_args)?; + values.int(metadata.modifiers as i64).map(Some) + } + "isfinal" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_FINAL, + evaluated_args, + values, + ), + "isabstract" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ABSTRACT, + evaluated_args, + values, + ), + "isinterface" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INTERFACE, + evaluated_args, + values, + ), + "istrait" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_TRAIT, + evaluated_args, + values, + ), + "isenum" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ENUM, + evaluated_args, + values, + ), + "isreadonly" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_READONLY, + evaluated_args, + values, + ), + "isanonymous" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS, + evaluated_args, + values, + ), + "isinstantiable" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE, + evaluated_args, + values, + ), + "iscloneable" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_CLONEABLE, + evaluated_args, + values, + ), + "isiterable" | "isiterateable" => { + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_class_flag_result( + flags, + EVAL_REFLECTION_CLASS_FLAG_ITERABLE, + evaluated_args, + values, + ) + } + "isinternal" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_INTERNAL, + evaluated_args, + values, + ), + "isuserdefined" => eval_reflection_class_flag_result( + metadata.flags, + EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + evaluated_args, + values, + ), + _ => Ok(None), + } +} + +/// Handles `ReflectionClass::__toString()` calls for eval-visible class metadata. +pub(in crate::interpreter) fn eval_reflection_class_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let rendered = eval_reflection_class_to_string(&reflected_name, context, values)?; + values.string(&rendered).map(Some) +} + +/// Returns one boolean ReflectionClass flag after validating a no-arg call. +fn eval_reflection_class_flag_result( + flags: u64, + flag: u64, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(flags & flag != 0).map(Some) +} + +/// Handles eval-backed `ReflectionClass::hasMethod()` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_method_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasMethod") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let exists = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + metadata + .method_names + .iter() + .any(|name| name.eq_ignore_ascii_case(&requested_name)) + } else { + eval_reflection_aot_method_metadata_if_exists(&reflected_name, &requested_name, values)? + .is_some() + }; + values.bool_value(exists).map(Some) +} + +/// Handles eval-backed `ReflectionClass::hasProperty()` and inherited `ReflectionObject` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_property_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasProperty") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let property_name = eval_reflection_string_arg(args[0], values)?; + let mut exists = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + metadata + .property_names + .iter() + .any(|name| name == &property_name) + } else { + eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + &property_name, + context, + values, + )? + .is_some() + || eval_reflection_native_interface_property_requirement( + &reflected_name, + &property_name, + context, + ) + .is_some() + }; + if !exists { + if let Some(dynamic_object) = + eval_reflection_object_reflected_object(object, context, values)? + { + let dynamic_exists = eval_reflection_object_dynamic_property_exists( + dynamic_object, + &property_name, + values, + ); + values.release(dynamic_object)?; + exists = dynamic_exists?; + } + } + values.bool_value(exists).map(Some) +} + +/// Handles eval-backed `ReflectionClass::hasConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_has_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("hasConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let constant_name = eval_reflection_string_arg(args[0], values)?; + let constant_names = eval_reflection_constant_names(&reflected_name, context, values)?; + values + .bool_value(constant_names.iter().any(|name| name == &constant_name)) + .map(Some) +} + +/// Handles eval-backed `ReflectionEnum` methods that are not inherited from `ReflectionClass`. +pub(in crate::interpreter) fn eval_reflection_enum_methods_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_object_has_class(object, "ReflectionEnum", values)? { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let Some(enum_name) = context.resolve_enum_name(&reflected_name) else { + return Ok(None); + }; + match method_name.to_ascii_lowercase().as_str() { + "hascase" => { + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let exists = context + .enum_decl(&enum_name) + .and_then(|enum_decl| enum_decl.case(&requested_name)) + .is_some(); + values.bool_value(exists).map(Some) + } + "getcase" => { + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let owner_kind = eval_reflection_enum_case_owner_kind(&enum_name, context)?; + let result = eval_reflection_enum_case_object_result( + owner_kind, + &enum_name, + &requested_name, + context, + values, + )?; + Ok(Some(result)) + } + "getcases" => { + eval_reflection_bind_no_args(evaluated_args)?; + let (case_names, owner_kind) = { + let enum_decl = context.enum_decl(&enum_name).ok_or(EvalStatus::RuntimeFatal)?; + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + (case_names, eval_reflection_enum_case_owner_kind(&enum_name, context)?) + }; + let mut result = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let case_object = eval_reflection_enum_case_object_result( + owner_kind, &enum_name, case_name, context, values, + )?; + let key = values.int(index as i64)?; + result = values.array_set(result, key, case_object)?; + } + Ok(Some(result)) + } + "isbacked" => { + eval_reflection_bind_no_args(evaluated_args)?; + let is_backed = context + .enum_decl(&enum_name) + .and_then(EvalEnum::backing_type) + .is_some(); + values.bool_value(is_backed).map(Some) + } + "getbackingtype" => { + eval_reflection_bind_no_args(evaluated_args)?; + let backing_type = context + .enum_decl(&enum_name) + .and_then(EvalEnum::backing_type); + let Some(backing_type) = backing_type else { + return values.null().map(Some); + }; + let metadata = eval_reflection_enum_backing_type_metadata(backing_type); + eval_reflection_type_object_result(&metadata, values).map(Some) + } + _ => Ok(None), + } +} + +/// Handles eval-backed `ReflectionClass::getInterfaces()` and `getTraits()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_relation_objects_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let relation_kind = if method_name.eq_ignore_ascii_case("getInterfaces") { + "interfaces" + } else if method_name.eq_ignore_ascii_case("getTraits") { + "traits" + } else { + return Ok(None); + }; + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + if relation_kind == "interfaces" { + eval_reflection_eval_metadata_interface_names(&metadata, context, values)? + } else { + metadata.trait_names + } + } else if relation_kind == "interfaces" { + eval_reflection_aot_class_interface_names(&reflected_name, values)? + } else { + eval_reflection_aot_class_trait_names(&reflected_name, values)? + }; + eval_reflection_class_object_map_result(&names, context, values).map(Some) +} + +/// Handles eval-backed `ReflectionClass::getTraitAliases()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_trait_aliases_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getTraitAliases") { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let aliases = if context.trait_decl(&reflected_name).is_some() { + context.trait_trait_aliases(&reflected_name) + } else if eval_reflection_class_like_exists(&reflected_name, context) { + context.class_trait_aliases(&reflected_name) + } else { + eval_reflection_aot_class_trait_aliases(&reflected_name, values)? + }; + eval_reflection_string_assoc_result(aliases, values).map(Some) +} + +/// Handles eval-backed `ReflectionClass::getConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let constant_name = eval_reflection_string_arg(args[0], values)?; + if let Some(value) = + eval_reflection_constant_value(&reflected_name, &constant_name, context, values)? + { + return Ok(Some(value)); + } + values.bool_value(false).map(Some) +} + +/// Handles eval-backed `ReflectionClass::getConstants()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_constants_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getConstants") { + return Ok(None); + } + let filter = eval_reflection_member_filter(evaluated_args, values)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = eval_reflection_constant_names(&reflected_name, context, values)?; + let mut result = values.assoc_new(names.len())?; + for name in names { + if !eval_reflection_constant_matches_filter(&reflected_name, &name, filter, context, values)? + { + continue; + } + let Some(value) = eval_reflection_constant_value(&reflected_name, &name, context, values)? + else { + continue; + }; + let key = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getDefaultProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_default_properties_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getDefaultProperties") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let property_names = eval_reflection_default_property_names(&reflected_name, context, values)?; + let mut result = values.assoc_new(property_names.len())?; + for name in property_names { + let Some(member) = + eval_reflection_default_property_metadata(&reflected_name, &name, context, values)? + else { + continue; + }; + let Some(default) = member.default_value.as_ref() else { + continue; + }; + let key = values.string(&name)?; + let value = eval_reflection_member_default_value(&member, default, context, values)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getStaticProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_static_properties_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getStaticProperties") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let property_names = eval_reflection_static_property_names(&reflected_name, context, values)?; + let mut result = values.assoc_new(property_names.len())?; + for name in property_names { + let Some(value) = + eval_reflection_static_property_value(&reflected_name, &name, context, values)? + else { + continue; + }; + let key = values.string(&name)?; + result = values.array_set(result, key, value)?; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getStaticPropertyValue()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_static_property_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getStaticPropertyValue") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let (property_name, default_value) = + eval_reflection_static_property_value_args(evaluated_args)?; + let property_name = eval_reflection_string_arg(property_name, values)?; + if let Some(value) = + eval_reflection_static_property_value(&reflected_name, &property_name, context, values)? + { + return Ok(Some(value)); + } + if let Some(default_value) = default_value { + return Ok(Some(default_value)); + } + eval_throw_reflection_exception( + &format!( + "Property {}::${} does not exist", + reflected_name, property_name + ), + context, + values, + ) +} + +/// Handles eval-backed `ReflectionClass::setStaticPropertyValue()` calls. +pub(in crate::interpreter) fn eval_reflection_class_set_static_property_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setStaticPropertyValue") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args( + &[String::from("name"), String::from("value")], + evaluated_args, + )?; + let property_name = eval_reflection_string_arg(args[0], values)?; + let Some(member) = + eval_reflection_static_property_metadata(&reflected_name, &property_name, context, values)? + else { + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); + }; + if !member.is_static { + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); + } + if eval_reflection_class_like_exists(&reflected_name, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(replaced) = + context.set_static_property(declaring_class, &property_name, args[1]) + { + values.release(replaced)?; + } + } else { + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(reflected_name.as_str()); + let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.static_property_set(&reflected_name, &property_name, args[1]) + })?; + if updated { + return values.null().map(Some); + } + return eval_reflection_static_property_missing_for_set( + &reflected_name, + &property_name, + context, + values, + ); + } + values.null().map(Some) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/class_construction.rs b/crates/elephc-magician/src/interpreter/reflection/class_construction.rs new file mode 100644 index 0000000000..8c61840608 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/class_construction.rs @@ -0,0 +1,440 @@ +//! Purpose: +//! Constructs Reflection owners for eval and AOT class-like targets. +//! It also centralizes instantiability and constructor visibility metadata. +//! +//! Called from: +//! - `crate::interpreter::reflection` for ReflectionClass/Object/Enum construction. +//! +//! Key details: +//! - Class-like flags merge eval declarations with focused AOT runtime metadata. + +use super::*; + +/// Builds an eval-backed `ReflectionClass` object when the reflected class-like exists in eval. +pub(super) fn eval_reflection_class_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + eval_reflection_class_owner_object_result( + EVAL_REFLECTION_OWNER_CLASS, + &reflected_name, + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionObject` from an object instance. +pub(super) fn eval_reflection_object_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + let tag = values.type_tag(args[0])?; + if tag != EVAL_TAG_OBJECT { + return super::class_lookup::eval_throw_type_error( + &format!( + "ReflectionObject::__construct(): Argument #1 ($object) must be of type object, {} given", + eval_reflection_type_error_type_name(tag) + ), + context, + values, + ); + } + let reflected_name = eval_reflection_object_class_name(args[0], context, values)?; + let Some(object) = eval_reflection_class_owner_object_result( + EVAL_REFLECTION_OWNER_OBJECT, + &reflected_name, + context, + values, + )? + else { + return Err(EvalStatus::RuntimeFatal); + }; + eval_reflection_with_declaring_class_scope("ReflectionObject", context, |_| { + values.property_set(object, "__object", args[0]) + })?; + Ok(Some(object)) +} + +/// Materializes class metadata for `ReflectionClass` or `ReflectionObject`. +pub(super) fn eval_reflection_class_owner_object_result( + owner_kind: u64, + reflected_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(metadata) = eval_reflection_class_like_attributes(reflected_name, context) else { + if reflected_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return eval_reflection_builtin_closure_class_object_result( + owner_kind, context, values, + ) + .map(Some); + } + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(reflected_name, values)? + else { + return Ok(None); + }; + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + values, + )?; + let property_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + values, + )?; + let interface_names = eval_reflection_aot_class_interface_names(reflected_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(reflected_name, values)?; + let parent_class_name = eval_reflection_aot_parent_class_name(reflected_name, values)?; + let attributes = context.native_class_attributes(reflected_name); + return eval_reflection_owner_object( + owner_kind, + reflected_name, + &attributes, + &interface_names, + &trait_names, + &method_names, + &property_names, + parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + context, + values, + ) + .map(Some); + }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_owner_object( + owner_kind, + &metadata.resolved_name, + &metadata.attributes, + &interface_names, + &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, + metadata.parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + flags, + metadata.modifiers, + 0, + None, + None, + context, + values, + ) + .map(Some) +} + +/// Builds the minimal ReflectionClass metadata object for PHP's builtin Closure class. +pub(super) fn eval_reflection_builtin_closure_class_object_result( + owner_kind: u64, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let flags = EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_INTERNAL; + let modifiers = eval_reflection_class_modifiers(true, false, false, false); + eval_reflection_owner_object( + owner_kind, + "Closure", + &[], + &[], + &[], + &[], + &[], + None, + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionEnum` object for a declared enum. +pub(super) fn eval_reflection_enum_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("class_name")], evaluated_args)?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; + let reflected_name = context + .resolve_enum_name(&class_name) + .or_else(|| context.resolve_class_like_name(&class_name)) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if context.enum_decl(&reflected_name).is_some() { + return eval_reflection_enum_object_result(&reflected_name, context, values).map(Some); + } + if eval_reflection_class_like_exists(&reflected_name, context) { + return eval_throw_reflection_exception( + &format!("Class \"{}\" is not an enum", reflected_name), + context, + values, + ); + } + if eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return Ok(None); + } + eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ) +} + +/// Materializes one eval-backed `ReflectionEnum` owner object. +pub(super) fn eval_reflection_enum_object_result( + enum_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let reflected_name = context + .resolve_enum_name(enum_name) + .unwrap_or_else(|| enum_name.trim_start_matches('\\').to_string()); + let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) else { + return Err(EvalStatus::RuntimeFatal); + }; + if !context.has_enum(&metadata.resolved_name) { + return Err(EvalStatus::RuntimeFatal); + } + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_ENUM, + &metadata.resolved_name, + &metadata.attributes, + &metadata.interface_names, + &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, + metadata.parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + metadata.flags, + metadata.modifiers, + 0, + None, + None, + context, + values, + ) +} + +/// Resolves a ReflectionClass constructor target from a class-name string or object. +pub(super) fn eval_reflection_class_target_name( + target: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(target)? == EVAL_TAG_OBJECT { + return eval_reflection_object_class_name(target, context, values); + } + eval_reflection_string_arg(target, values) +} + +/// Returns generated/AOT class flags for synthetic ReflectionClass fallback objects. +pub(super) fn eval_reflection_aot_class_flags( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let is_class = values.class_exists(runtime_class_name)?; + let is_interface = eval_runtime_interface_exists(runtime_class_name, values)?; + let is_trait = values.trait_exists(runtime_class_name)?; + let is_enum = values.enum_exists(runtime_class_name)?; + if !(is_class || is_interface || is_trait || is_enum) { + return Ok(None); + } + let mut flags = 0; + if eval_reflection_class_like_is_internal(runtime_class_name) { + flags |= EVAL_REFLECTION_CLASS_FLAG_INTERNAL; + } else { + flags |= EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; + } + let mut class_flags = values.reflection_class_flags(runtime_class_name)?.unwrap_or(0); + if is_enum { + class_flags &= !EVAL_REFLECTION_CLASS_FLAG_READONLY; + } + flags |= class_flags + & (EVAL_REFLECTION_CLASS_FLAG_FINAL + | EVAL_REFLECTION_CLASS_FLAG_ABSTRACT + | EVAL_REFLECTION_CLASS_FLAG_READONLY); + if is_interface { + flags |= EVAL_REFLECTION_CLASS_FLAG_INTERFACE; + } + if is_trait { + flags |= EVAL_REFLECTION_CLASS_FLAG_TRAIT; + } + if is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL | EVAL_REFLECTION_CLASS_FLAG_ENUM; + } + if eval_reflection_builtin_class_is_iterable(runtime_class_name) { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } + if is_class && !is_enum && flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT == 0 { + if eval_reflection_aot_lifecycle_method_allows_public_reflection( + runtime_class_name, + "__construct", + values, + )? { + flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; + } + if eval_reflection_aot_lifecycle_method_allows_public_reflection( + runtime_class_name, + "__clone", + values, + )? { + flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; + } + } + let modifiers = eval_reflection_class_modifiers( + flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0, + is_enum, + ); + Ok(Some((flags, modifiers))) +} + +/// Returns AOT class modifiers relevant to validating an eval `extends` clause. +pub(in crate::interpreter) fn eval_reflection_aot_class_inheritance_modifiers( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + if flags + & (EVAL_REFLECTION_CLASS_FLAG_INTERFACE + | EVAL_REFLECTION_CLASS_FLAG_TRAIT + | EVAL_REFLECTION_CLASS_FLAG_ENUM) + != 0 + { + return Ok(None); + } + Ok(Some(( + flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0, + ))) +} + +/// Returns the catchable error for generated/AOT allocation without constructor, if any. +pub(in crate::interpreter) fn eval_reflection_aot_class_without_constructor_error( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + Ok(eval_reflection_non_instantiable_error_message( + class_name, flags, + )) +} + +/// Returns the catchable error for generated/AOT public ReflectionClass construction, if any. +pub(in crate::interpreter) fn eval_reflection_aot_class_public_instantiation_error( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((flags, _)) = eval_reflection_aot_class_flags(class_name, values)? else { + return Ok(None); + }; + if let Some(message) = eval_reflection_non_instantiable_error_message(class_name, flags) { + return Ok(Some(EvalReflectionInstantiationError::ThrowableError(message))); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE == 0 { + return Ok(Some( + EvalReflectionInstantiationError::ReflectionException(format!( + "Access to non-public constructor of class {}", + class_name.trim_start_matches('\\') + )), + )); + } + Ok(None) +} + +/// Builds PHP's non-instantiable class-like message from ReflectionClass flags. +pub(super) fn eval_reflection_non_instantiable_error_message(class_name: &str, flags: u64) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0 { + return Some(format!("Cannot instantiate abstract class {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + return Some(format!("Cannot instantiate interface {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + return Some(format!("Cannot instantiate trait {}", class_name)); + } + if flags & EVAL_REFLECTION_CLASS_FLAG_ENUM != 0 { + return Some(format!("Cannot instantiate enum {}", class_name)); + } + None +} + +/// Returns whether an absent or public AOT lifecycle method allows public reflection. +pub(super) fn eval_reflection_aot_lifecycle_method_allows_public_reflection( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(true); + }; + Ok(flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC != 0 + && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0) +} + +/// Returns AOT constructor access metadata when the constructor is not public. +pub(in crate::interpreter) fn eval_reflection_aot_non_public_constructor( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, "__construct")? else { + return Ok(None); + }; + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + return Ok(None); + }; + let declaring_class = values + .reflection_method_declaring_class(runtime_class_name, "__construct")? + .unwrap_or_else(|| runtime_class_name.to_string()); + Ok(Some((declaring_class, visibility))) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/class_lookup.rs b/crates/elephc-magician/src/interpreter/reflection/class_lookup.rs new file mode 100644 index 0000000000..c48ed1ba4f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/class_lookup.rs @@ -0,0 +1,814 @@ +//! Purpose: +//! Resolves class-like relations, members, attributes, flags, and lookup errors. +//! +//! Called from: +//! - Reflection class APIs, constructors, formatters, and member metadata builders. +//! +//! Key details: +//! - Eval declarations, aliases, interfaces, traits, and AOT metadata use case-insensitive lookup. + +use super::*; + +/// Returns true when a ReflectionClass member passes an optional modifier filter. +pub(super) fn eval_reflection_member_matches_filter( + member: &EvalReflectionMemberMetadata, + filter: Option, +) -> bool { + match filter { + Some(filter) => member.modifiers & filter != 0, + None => true, + } +} + +/// Parses the optional ReflectionClass member filter argument. +pub(super) fn eval_reflection_member_filter( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut filter = None; + for arg in evaluated_args { + if let Some(name) = arg.name.as_deref() { + if name != "filter" { + return Err(EvalStatus::RuntimeFatal); + } + } + if filter.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + filter = Some(arg.value); + } + let Some(filter) = filter else { + return Ok(None); + }; + if values.is_null(filter)? { + return Ok(None); + } + let cast_filter = values.cast_int(filter)?; + let bytes = values.string_bytes(cast_filter)?; + values.release(cast_filter)?; + let text = std::str::from_utf8(&bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + text.parse::() + .map(|value| Some(value as u64)) + .map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Returns generated AOT member names for one reflected class. +pub(super) fn eval_reflection_aot_member_names( + owner_kind: u64, + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + values.reflection_method_names(runtime_class_name)? + } else { + values.reflection_property_names(runtime_class_name)? + }; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns generated AOT interface names for one reflected class-like symbol. +pub(super) fn eval_reflection_aot_class_interface_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = values.reflection_class_interface_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns eval metadata interface names expanded with generated/AOT ancestors. +pub(super) fn eval_reflection_eval_metadata_interface_names( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(&metadata.resolved_name) || context.has_enum(&metadata.resolved_name) { + eval_reflection_eval_class_interface_names(&metadata.resolved_name, context, values) + } else if context.has_interface(&metadata.resolved_name) { + eval_reflection_eval_interface_parent_names(&metadata.resolved_name, context, values) + } else { + Ok(metadata.interface_names.clone()) + } +} + +/// Returns eval metadata flags corrected for generated/AOT inherited interfaces. +pub(super) fn eval_reflection_eval_metadata_flags( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut flags = metadata.flags; + if flags & EVAL_REFLECTION_CLASS_FLAG_ITERABLE == 0 + && flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT == 0 + && context.has_class(&metadata.resolved_name) + && eval_reflection_interface_names_include_iterable( + &eval_reflection_eval_class_interface_names(&metadata.resolved_name, context, values)?, + ) + { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } + Ok(flags) +} + +/// Returns eval class interfaces plus interfaces inherited from generated/AOT parents. +pub(super) fn eval_reflection_eval_class_interface_names( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = context.class_native_parent_name(class_name) { + for name in eval_reflection_aot_class_interface_names(&parent, values)? { + eval_reflection_push_unique_class_name(name, &mut names, &mut seen); + } + } + for name in context.class_interface_names(class_name) { + eval_reflection_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_reflection_aot_class_interface_names(&name, values)? { + eval_reflection_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns eval interface parents plus inherited generated/AOT interface parents. +pub(super) fn eval_reflection_eval_interface_parent_names( + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for name in context.interface_parent_names(interface_name) { + eval_reflection_push_unique_class_name(name.clone(), &mut names, &mut seen); + if !context.has_interface(&name) && eval_runtime_interface_exists(&name, values)? { + for parent in eval_reflection_aot_class_interface_names(&name, values)? { + eval_reflection_push_unique_class_name(parent, &mut names, &mut seen); + } + } + } + Ok(names) +} + +/// Returns whether one interface list includes PHP iterable marker interfaces. +pub(super) fn eval_reflection_interface_names_include_iterable(interface_names: &[String]) -> bool { + interface_names.iter().any(|name| { + name.eq_ignore_ascii_case("Iterator") || name.eq_ignore_ascii_case("IteratorAggregate") + }) +} + +/// Appends one class-like name while preserving PHP's case-insensitive uniqueness. +pub(super) fn eval_reflection_push_unique_class_name( + name: String, + names: &mut Vec, + seen: &mut std::collections::HashSet, +) { + if seen.insert(name.to_ascii_lowercase()) { + names.push(name); + } +} + +/// Returns generated AOT trait names for one reflected class-like symbol. +pub(super) fn eval_reflection_aot_class_trait_names( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let names_array = values.reflection_class_trait_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns generated AOT trait aliases for one reflected class-like symbol. +pub(super) fn eval_reflection_aot_class_trait_aliases( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let alias_names_array = values.reflection_class_trait_alias_names(runtime_class_name)?; + let alias_names = eval_reflection_string_array_to_vec(alias_names_array, values)?; + values.release(alias_names_array)?; + let alias_sources_array = values.reflection_class_trait_alias_sources(runtime_class_name)?; + let alias_sources = eval_reflection_string_array_to_vec(alias_sources_array, values)?; + values.release(alias_sources_array)?; + if alias_names.len() != alias_sources.len() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(alias_names.into_iter().zip(alias_sources).collect()) +} + +/// Copies a runtime string array into Rust-owned strings for reflection metadata assembly. +pub(super) fn eval_reflection_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_reflection_string_arg(value, values)?); + } + Ok(result) +} + +/// Returns member metadata for one ReflectionClass member-array entry. +pub(super) fn eval_reflection_member_metadata( + owner_kind: u64, + class_name: &str, + name: &str, + context: &ElephcEvalContext, +) -> Option { + match owner_kind { + EVAL_REFLECTION_OWNER_METHOD => eval_reflection_method_metadata(class_name, name, context), + EVAL_REFLECTION_OWNER_PROPERTY => { + eval_reflection_property_metadata(class_name, name, context) + } + _ => None, + } +} + +/// Returns the eval-retained class-like attributes plus canonical reflected name. +pub(super) fn eval_reflection_class_like_attributes( + name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(class) = context.class(name) { + let is_enum = context.has_enum(class.name()); + let mut flags = EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED; + if class.is_final() { + flags |= EVAL_REFLECTION_CLASS_FLAG_FINAL; + } + if class.is_abstract() { + flags |= EVAL_REFLECTION_CLASS_FLAG_ABSTRACT; + } + if is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_ENUM; + } + if class.is_readonly_class() && !is_enum { + flags |= EVAL_REFLECTION_CLASS_FLAG_READONLY; + } + if eval_reflection_class_is_instantiable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_INSTANTIABLE; + } + if eval_reflection_class_is_cloneable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_CLONEABLE; + } + if eval_reflection_class_is_iterable(class, is_enum, context) { + flags |= EVAL_REFLECTION_CLASS_FLAG_ITERABLE; + } + if class.is_anonymous() { + flags |= EVAL_REFLECTION_CLASS_FLAG_ANONYMOUS; + } + let modifiers = eval_reflection_class_modifiers( + class.is_final(), + class.is_abstract(), + class.is_readonly_class(), + is_enum, + ); + return Some(EvalReflectionClassMetadata { + resolved_name: class.name().trim_start_matches('\\').to_string(), + source_location: class.source_location(), + attributes: class.attributes().to_vec(), + interface_names: context.class_interface_names(class.name()), + trait_names: context.class_trait_names(class.name()), + method_names: context.class_method_names(class.name()), + property_names: context.class_property_names(class.name()), + parent_class_name: eval_reflection_parent_class_name(class, context), + flags, + modifiers, + }); + } + if let Some(interface) = context.interface(name) { + return Some(EvalReflectionClassMetadata { + resolved_name: interface.name().trim_start_matches('\\').to_string(), + source_location: interface.source_location(), + attributes: interface.attributes().to_vec(), + interface_names: context.interface_parent_names(interface.name()), + trait_names: Vec::new(), + method_names: context.interface_method_names(interface.name()), + property_names: context.interface_property_names(interface.name()), + parent_class_name: None, + flags: EVAL_REFLECTION_CLASS_FLAG_INTERFACE | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + modifiers: 0, + }); + } + if let Some(trait_decl) = context.trait_decl(name) { + return Some(EvalReflectionClassMetadata { + resolved_name: trait_decl.name().trim_start_matches('\\').to_string(), + source_location: trait_decl.source_location(), + attributes: trait_decl.attributes().to_vec(), + interface_names: Vec::new(), + trait_names: context.trait_trait_names(trait_decl.name()), + method_names: context.trait_method_names(trait_decl.name()), + property_names: context.trait_property_names(trait_decl.name()), + parent_class_name: None, + flags: EVAL_REFLECTION_CLASS_FLAG_TRAIT | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + modifiers: 0, + }); + } + context + .enum_decl(name) + .map(|enum_decl| EvalReflectionClassMetadata { + resolved_name: enum_decl.name().trim_start_matches('\\').to_string(), + source_location: enum_decl.source_location(), + attributes: enum_decl.attributes().to_vec(), + interface_names: context.class_interface_names(enum_decl.name()), + trait_names: context.class_trait_names(enum_decl.name()), + method_names: context.class_method_names(enum_decl.name()), + property_names: context.class_property_names(enum_decl.name()), + parent_class_name: None, + flags: EVAL_REFLECTION_CLASS_FLAG_FINAL + | EVAL_REFLECTION_CLASS_FLAG_ENUM + | EVAL_REFLECTION_CLASS_FLAG_USER_DEFINED, + modifiers: 32, + }) +} + +/// Returns the PHP-visible parent class name for ReflectionClass metadata. +pub(super) fn eval_reflection_parent_class_name( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Option { + context.class_parent_names(class.name()).into_iter().next() +} + +/// Returns PHP's `ReflectionClass::isInstantiable()` value for eval class metadata. +pub(super) fn eval_reflection_class_is_instantiable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_method(class.name(), "__construct") + .map(|(_, method)| method.visibility() == EvalVisibility::Public) + .unwrap_or(true) +} + +/// Returns PHP's `ReflectionClass::isCloneable()` value for eval class metadata. +pub(super) fn eval_reflection_class_is_cloneable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_method(class.name(), "__clone") + .map(|(_, method)| method.visibility() == EvalVisibility::Public) + .unwrap_or(true) +} + +/// Returns PHP's `ReflectionClass::isIterable()` value for eval class metadata. +pub(super) fn eval_reflection_class_is_iterable( + class: &EvalClass, + is_enum: bool, + context: &ElephcEvalContext, +) -> bool { + if class.is_abstract() || is_enum { + return false; + } + context + .class_interface_names(class.name()) + .iter() + .any(|name| { + name.eq_ignore_ascii_case("Iterator") || name.eq_ignore_ascii_case("IteratorAggregate") + }) +} + +/// Returns PHP's `ReflectionClass::isIterable()` value for compiler-injected class names. +pub(super) fn eval_reflection_builtin_class_is_iterable(class_name: &str) -> bool { + matches!( + class_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str(), + "__elephcappenditeratorarrayiterator" + | "appenditerator" + | "arrayiterator" + | "arrayobject" + | "cachingiterator" + | "callbackfilteriterator" + | "directoryiterator" + | "emptyiterator" + | "filesystemiterator" + | "generator" + | "globiterator" + | "infiniteiterator" + | "internaliterator" + | "iteratoriterator" + | "limititerator" + | "multipleiterator" + | "norewinditerator" + | "parentiterator" + | "recursivearrayiterator" + | "recursivecachingiterator" + | "recursivecallbackfilteriterator" + | "recursivedirectoryiterator" + | "recursiveiteratoriterator" + | "recursiveregexiterator" + | "regexiterator" + | "spldoublylinkedlist" + | "splfixedarray" + | "splfileobject" + | "splmaxheap" + | "splminheap" + | "splobjectstorage" + | "splpriorityqueue" + | "splqueue" + | "splstack" + | "spltempfileobject" + ) +} + +/// Returns whether one reflected class-like name belongs to compiler-injected metadata. +pub(super) fn eval_reflection_class_like_is_internal(class_name: &str) -> bool { + let trimmed = class_name.trim_start_matches('\\'); + if EVAL_SPL_CLASS_NAMES + .iter() + .any(|candidate| candidate.eq_ignore_ascii_case(trimmed)) + { + return true; + } + matches!( + trimmed.to_ascii_lowercase().as_str(), + "__elephcappenditeratorarrayiterator" + | "fiber" + | "fibererror" + | "generator" + | "internaliterator" + | "jsonexception" + | "phar" + | "phardata" + | "pharfileinfo" + | "php_user_filter" + | "reflectionattribute" + | "reflectionclass" + | "reflectionclassconstant" + | "reflectionenum" + | "reflectionenumbackedcase" + | "reflectionenumunitcase" + | "reflectionexception" + | "reflectionfunction" + | "reflectionintersectiontype" + | "reflectionmethod" + | "reflectionnamedtype" + | "reflectionparameter" + | "reflectionproperty" + | "reflectionuniontype" + | "sortdirection" + | "splheap" + | "splmaxheap" + | "splminheap" + | "splobjectstorage" + | "splpriorityqueue" + | "stdclass" + ) +} + +/// Computes PHP's `ReflectionClass::getModifiers()` bitmask for eval metadata. +pub(super) fn eval_reflection_class_modifiers( + is_final: bool, + is_abstract: bool, + is_readonly_class: bool, + is_enum: bool, +) -> u64 { + let mut modifiers = 0; + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly_class && !is_enum { + modifiers |= 65_536; + } + modifiers +} + +/// Computes PHP's `ReflectionClassConstant::getModifiers()` bitmask for eval metadata. +pub(super) fn eval_reflection_class_constant_modifiers(visibility: EvalVisibility, is_final: bool) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_final { + modifiers |= 32; + } + modifiers +} + +/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask for eval metadata. +pub(super) fn eval_reflection_method_modifiers( + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, +) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + modifiers +} + +/// Computes PHP's `ReflectionProperty::getModifiers()` bitmask for eval metadata. +pub(super) fn eval_reflection_property_modifiers( + visibility: EvalVisibility, + set_visibility: Option, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, + is_virtual: bool, +) -> u64 { + let mut modifiers = match visibility { + EvalVisibility::Public => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Private => 4, + }; + if is_static { + modifiers |= 16; + } + if is_final { + modifiers |= 32; + } + if is_abstract { + modifiers |= 64; + } + if is_readonly { + modifiers |= 128; + } + if is_virtual { + modifiers |= 512; + } + match set_visibility { + Some(EvalVisibility::Private) => modifiers |= 32 | 4096, + Some(EvalVisibility::Protected) => modifiers |= 2048, + _ if is_readonly && visibility == EvalVisibility::Public => modifiers |= 2048, + _ => {} + } + modifiers +} + +/// Returns declaring class, attributes, visibility, finality, and enum-case kind. +pub(super) fn eval_reflection_class_constant_metadata( + class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalVisibility, bool, bool)>, EvalStatus> { + if let Some(enum_decl) = context.enum_decl(class_name) { + if let Some(case) = enum_decl.case(constant_name) { + return Ok(Some(( + enum_decl.name().to_string(), + case.attributes().to_vec(), + EvalVisibility::Public, + false, + true, + ))); + } + } + if let Some(metadata) = context + .class_constant(class_name, constant_name) + .map(|(declaring_class, constant)| { + ( + declaring_class, + constant.attributes().to_vec(), + constant.visibility(), + constant.is_final(), + false, + ) + }) { + return Ok(Some(metadata)); + } + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_constant_flags(runtime_class_name, constant_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_constant_declaring_class(runtime_class_name, constant_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + let attributes = eval_reflection_aot_constant_attributes( + runtime_class_name, + &declaring_class, + constant_name, + context, + ); + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + Ok(Some(( + declaring_class, + attributes, + visibility, + flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, + flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE != 0, + ))) +} + +/// Returns registered generated/AOT class-constant attributes for one reflected constant. +pub(super) fn eval_reflection_aot_constant_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_constant_attributes(declaring_class_name, constant_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_constant_attributes(runtime_class_name, constant_name) +} + +/// Returns true when a name resolves to an eval-declared class-like symbol. +pub(super) fn eval_reflection_class_like_exists(name: &str, context: &ElephcEvalContext) -> bool { + context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || context.has_enum(name) +} + +/// Returns true when a name resolves to eval or runtime class-like metadata. +pub(super) fn eval_reflection_class_like_or_runtime_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_reflection_class_like_exists(name, context) + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.trait_exists(name)? + || values.enum_exists(name)?) +} + +/// Returns true when one name exists as an eval or runtime interface. +pub(super) fn eval_reflection_interface_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(context.has_interface(name) || eval_runtime_interface_exists(name, values)?) +} + +/// Returns true when one name exists as a non-interface class-like symbol. +pub(super) fn eval_reflection_non_interface_exists( + name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_class(name) + || context.has_trait(name) + || context.has_enum(name) + || values.class_exists(name)? + || values.trait_exists(name)? + { + return Ok(true); + } + values.enum_exists(name) +} + +/// Returns true when reflected eval metadata implements or extends an interface name. +pub(super) fn eval_reflection_class_implements_interface_name( + reflected_name: &str, + interface_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_interface(reflected_name) { + if eval_reflection_same_class_like_name(reflected_name, interface_name) { + return Ok(true); + } + return Ok(eval_reflection_eval_interface_parent_names( + reflected_name, + context, + values, + )? + .iter() + .any(|parent| eval_reflection_same_class_like_name(parent, interface_name))); + } + if context.has_class(reflected_name) || context.has_enum(reflected_name) { + return Ok(eval_reflection_eval_class_interface_names(reflected_name, context, values)? + .iter() + .any(|interface| eval_reflection_same_class_like_name(interface, interface_name))); + } + Ok(false) +} + +/// Returns true when reflected eval metadata is a subclass or subinterface of a target. +pub(super) fn eval_reflection_class_is_subclass_of_name( + reflected_name: &str, + target_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if context.has_interface(reflected_name) { + return Ok(eval_reflection_eval_interface_parent_names( + reflected_name, + context, + values, + )? + .iter() + .any(|parent| eval_reflection_same_class_like_name(parent, target_name))); + } + if context.has_class(reflected_name) || context.has_enum(reflected_name) { + if context.class_is_a(reflected_name, target_name, true) { + return Ok(true); + } + return Ok(eval_reflection_eval_class_interface_names(reflected_name, context, values)? + .iter() + .any(|interface| eval_reflection_same_class_like_name(interface, target_name))); + } + Ok(false) +} + +/// Returns true when two PHP class-like names match case-insensitively. +pub(super) fn eval_reflection_same_class_like_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Creates a catchable `ReflectionException` and propagates it through eval throw state. +pub(super) fn eval_throw_reflection_exception( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ReflectionException")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Creates a catchable `ValueError` and propagates it through eval throw state. +pub(super) fn eval_throw_value_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ValueError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Creates a catchable `TypeError` and propagates it through eval throw state. +pub(super) fn eval_throw_type_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("TypeError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Returns PHP's type name spelling used in argument type error messages. +pub(super) fn eval_reflection_type_error_type_name(tag: u64) -> &'static str { + match tag { + EVAL_TAG_INT => "int", + EVAL_TAG_STRING => "string", + EVAL_TAG_FLOAT => "float", + EVAL_TAG_BOOL => "bool", + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => "array", + EVAL_TAG_NULL => "null", + EVAL_TAG_RESOURCE => "resource", + EVAL_TAG_OBJECT => "object", + _ => "unknown", + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/class_member_api.rs b/crates/elephc-magician/src/interpreter/reflection/class_member_api.rs new file mode 100644 index 0000000000..9106a29e0b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/class_member_api.rs @@ -0,0 +1,435 @@ +//! Purpose: +//! Implements ReflectionClass member and reflected-constant collection APIs. +//! +//! Called from: +//! - `crate::interpreter::statements` for ReflectionClass member dispatch. +//! +//! Key details: +//! - Eval and AOT members share filtering, object construction, and missing-member errors. + +use super::*; + +/// Handles eval-backed `ReflectionClass::getReflectionConstant()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constant_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getReflectionConstant") { + return Ok(None); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + if !eval_reflection_constant_names(&reflected_name, context, values)? + .iter() + .any(|name| name == &requested_name) + { + return values.bool_value(false).map(Some); + } + eval_reflection_class_constant_object_result(&reflected_name, &requested_name, context, values) + .map(Some) +} + +/// Handles eval-backed `ReflectionClass::getReflectionConstants()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_reflection_constants_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getReflectionConstants") { + return Ok(None); + } + let filter = eval_reflection_member_filter(evaluated_args, values)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let names = eval_reflection_constant_names(&reflected_name, context, values)?; + let mut result = values.array_new(names.len())?; + let mut index = 0; + for name in &names { + if !eval_reflection_constant_matches_filter(reflected_name.as_str(), name, filter, context, values)? + { + continue; + } + let object = + eval_reflection_class_constant_object_result(&reflected_name, name, context, values)?; + let key = values.int(index)?; + result = values.array_set(result, key, object)?; + index += 1; + } + Ok(Some(result)) +} + +/// Handles eval-backed `ReflectionClass::getMethods()` and `getProperties()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_members_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let owner_kind = if method_name.eq_ignore_ascii_case("getMethods") { + EVAL_REFLECTION_OWNER_METHOD + } else if method_name.eq_ignore_ascii_case("getProperties") { + EVAL_REFLECTION_OWNER_PROPERTY + } else { + return Ok(None); + }; + let filter = eval_reflection_member_filter(evaluated_args, values)?; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(metadata) = eval_reflection_class_like_attributes(&reflected_name, context) { + let names = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + metadata.method_names + } else { + metadata.property_names + }; + return eval_reflection_member_object_array_result( + owner_kind, + &reflected_name, + &names, + filter, + context, + values, + ) + .and_then(|result| { + eval_reflection_object_dynamic_property_array_result( + object, + owner_kind, + &reflected_name, + filter, + result, + context, + values, + ) + }) + .map(Some); + } + let native_interface_property_names = + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + eval_reflection_native_interface_property_names(&reflected_name, context) + } else { + Vec::new() + }; + let names = if native_interface_property_names.is_empty() { + eval_reflection_aot_member_names(owner_kind, &reflected_name, values)? + } else { + native_interface_property_names + }; + eval_reflection_aot_member_object_array_result( + owner_kind, + &reflected_name, + &names, + filter, + context, + values, + ) + .and_then(|result| { + eval_reflection_object_dynamic_property_array_result( + object, + owner_kind, + &reflected_name, + filter, + result, + context, + values, + ) + }) + .map(Some) +} + +/// Handles eval-backed `ReflectionClass::getMethod()` and `getProperty()` calls. +pub(in crate::interpreter) fn eval_reflection_class_get_member_result( + object: RuntimeCellHandle, + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let owner_kind = if method_name.eq_ignore_ascii_case("getMethod") { + EVAL_REFLECTION_OWNER_METHOD + } else if method_name.eq_ignore_ascii_case("getProperty") { + EVAL_REFLECTION_OWNER_PROPERTY + } else { + return Ok(None); + }; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + let args = bind_evaluated_function_args(&[String::from("name")], evaluated_args)?; + let requested_name = eval_reflection_string_arg(args[0], values)?; + let Some(member_name) = + eval_reflection_member_name(owner_kind, &reflected_name, &requested_name, context) + else { + if owner_kind == EVAL_REFLECTION_OWNER_METHOD + && !eval_reflection_class_like_exists(&reflected_name, context) + { + if let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + &requested_name, + context, + values, + )? { + let member_name = requested_name.to_ascii_lowercase(); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &member_name, + &member, + context, + values, + ) + .map(Some); + } + } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + && !eval_reflection_class_like_exists(&reflected_name, context) + { + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + &reflected_name, + &requested_name, + context, + ) + { + let member = eval_reflection_interface_property_metadata(declaring_class, &property); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } + if let Some(member) = eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + &requested_name, + context, + values, + )? { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } + } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + if let Some(dynamic_object) = + eval_reflection_object_reflected_object(object, context, values)? + { + let exists = eval_reflection_object_dynamic_property_exists( + dynamic_object, + &requested_name, + values, + ); + values.release(dynamic_object)?; + if exists? { + let member = eval_reflection_dynamic_property_metadata(&reflected_name); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &requested_name, + &member, + context, + values, + ) + .map(Some); + } + } + } + let message_name = eval_reflection_class_like_attributes(&reflected_name, context) + .map(|metadata| metadata.resolved_name) + .unwrap_or_else(|| reflected_name.clone()); + let message = + eval_reflection_missing_member_message(owner_kind, &message_name, &requested_name); + return eval_throw_reflection_exception(&message, context, values); + }; + let member = + eval_reflection_member_metadata(owner_kind, &reflected_name, &member_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_member_object_result(owner_kind, &member_name, &member, context, values) + .map(Some) +} + +/// Returns generated/AOT constant names visible through eval ReflectionClass. +pub(super) fn eval_reflection_aot_constant_names( + reflected_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = reflected_name.trim_start_matches('\\'); + let names_array = values.reflection_constant_names(runtime_class_name)?; + let names = eval_reflection_string_array_to_vec(names_array, values)?; + values.release(names_array)?; + Ok(names) +} + +/// Returns constant names from eval metadata or generated/AOT runtime metadata. +pub(super) fn eval_reflection_constant_names( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_interface(reflected_name) { + Ok(context.interface_constant_names(reflected_name)) + } else if context.has_trait(reflected_name) { + Ok(context.trait_constant_names(reflected_name)) + } else if context.has_class(reflected_name) || context.has_enum(reflected_name) { + Ok(context.class_constant_names(reflected_name)) + } else { + eval_reflection_aot_constant_names(reflected_name, values) + } +} + +/// Returns a materialized eval constant value for Reflection without visibility checks. +pub(super) fn eval_reflection_eval_constant_value( + reflected_name: &str, + constant_name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(case) = context.enum_case(reflected_name, constant_name) { + return Some(case); + } + let (declaring_class, constant) = context.class_constant(reflected_name, constant_name)?; + context.class_constant_cell(&declaring_class, constant.name()) +} + +/// Returns a materialized eval or AOT constant value for Reflection without visibility checks. +pub(super) fn eval_reflection_constant_value( + reflected_name: &str, + constant_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if eval_reflection_class_like_exists(reflected_name, context) { + return Ok(eval_reflection_eval_constant_value( + reflected_name, + constant_name, + context, + )); + } + let runtime_class_name = reflected_name.trim_start_matches('\\'); + values.reflection_constant_value(runtime_class_name, constant_name) +} + +/// Builds one eval-backed `ReflectionClassConstant` object for a visible constant name. +pub(super) fn eval_reflection_class_constant_object_result( + reflected_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (declaring_class_name, attributes, visibility, is_final, is_enum_case) = + eval_reflection_class_constant_metadata(reflected_name, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let constant_value = + eval_reflection_constant_value(reflected_name, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + if is_enum_case { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + } + let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + constant_name, + &attributes, + &[], + &[], + &[], + &[], + Some(&declaring_class_name), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + Some(constant_value), + None, + context, + values, + ) +} + +/// Returns whether one class constant passes an optional `ReflectionClassConstant` filter. +pub(super) fn eval_reflection_constant_matches_filter( + reflected_name: &str, + constant_name: &str, + filter: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(filter) = filter else { + return Ok(true); + }; + Ok(eval_reflection_class_constant_metadata(reflected_name, constant_name, context, values)? + .is_some_and(|(_, _, visibility, is_final, _)| { + eval_reflection_class_constant_modifiers(visibility, is_final) & filter != 0 + })) +} + +/// Resolves the declared member spelling for eval `ReflectionClass` single-member lookups. +pub(super) fn eval_reflection_member_name( + owner_kind: u64, + reflected_name: &str, + requested_name: &str, + context: &ElephcEvalContext, +) -> Option { + let metadata = eval_reflection_class_like_attributes(reflected_name, context)?; + let names = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + metadata.method_names + } else { + metadata.property_names + }; + names.into_iter().find(|name| match owner_kind { + EVAL_REFLECTION_OWNER_METHOD => name.eq_ignore_ascii_case(requested_name), + EVAL_REFLECTION_OWNER_PROPERTY => name == requested_name, + _ => false, + }) +} + +/// Builds PHP-compatible missing-member messages for eval ReflectionClass lookups. +pub(super) fn eval_reflection_missing_member_message( + owner_kind: u64, + reflected_name: &str, + requested_name: &str, +) -> String { + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + format!( + "Method {}::{}() does not exist", + reflected_name, requested_name + ) + } else { + format!( + "Property {}::${} does not exist", + reflected_name, requested_name + ) + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/constant_construction.rs b/crates/elephc-magician/src/interpreter/reflection/constant_construction.rs new file mode 100644 index 0000000000..c7b511efc7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/constant_construction.rs @@ -0,0 +1,280 @@ +//! Purpose: +//! Constructs reflected class constants and enum cases. +//! +//! Called from: +//! - `crate::interpreter::reflection` for ReflectionClassConstant and enum-case owners. +//! +//! Key details: +//! - Missing constants and non-case targets preserve PHP exception categories. + +use super::*; + +/// Builds an eval-backed `ReflectionClassConstant` object for a class constant or enum case. +pub(super) fn eval_reflection_class_constant_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("constant_name")], + evaluated_args, + )?; + let class_name = eval_reflection_string_arg(args[0], values)?; + let constant_name = eval_reflection_string_arg(args[1], values)?; + eval_reflection_class_constant_object_result_or_throw( + &class_name, + &constant_name, + context, + values, + ) +} + +/// Builds a `ReflectionClassConstant` object or throws PHP's catchable error. +pub(super) fn eval_reflection_class_constant_object_result_or_throw( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class_name, attributes, visibility, is_final, is_enum_case)) = + eval_reflection_class_constant_metadata(class_name, constant_name, context, values)? + else { + return eval_reflection_missing_class_constant_exception( + class_name, + constant_name, + context, + values, + ); + }; + let constant_value = eval_reflection_constant_value(class_name, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let mut flags = eval_reflection_member_flags(visibility, false, is_final, false, false); + if is_enum_case { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + } + let modifiers = eval_reflection_class_constant_modifiers(visibility, is_final); + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + constant_name, + &attributes, + &[], + &[], + &[], + &[], + Some(&declaring_class_name), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + Some(constant_value), + None, + context, + values, + ) + .map(Some) +} + +/// Throws the catchable ReflectionException for a missing class-like constant. +pub(super) fn eval_reflection_missing_class_constant_exception( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + eval_throw_reflection_exception( + &format!("Constant {}::{} does not exist", reflected_name, constant_name), + context, + values, + ) +} + +/// Builds an eval-backed ReflectionEnumUnitCase/BackedCase object for an enum case. +pub(super) fn eval_reflection_enum_case_new( + owner_kind: u64, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("constant_name")], + evaluated_args, + )?; + let enum_name = eval_reflection_string_arg(args[0], values)?; + let case_name = eval_reflection_string_arg(args[1], values)?; + let Some(enum_decl) = context.enum_decl(&enum_name) else { + if eval_reflection_class_constant_metadata(&enum_name, &case_name, context, values)? + .is_some() + { + return eval_reflection_not_enum_case_exception( + &enum_name, &case_name, context, values, + ); + } + if !eval_reflection_class_like_exists(&enum_name, context) + && eval_reflection_class_like_or_runtime_exists(&enum_name, context, values)? + { + return Ok(None); + } + return eval_reflection_missing_class_constant_exception( + &enum_name, &case_name, context, values, + ); + }; + let declaring_class_name = enum_decl.name().to_string(); + let has_case = enum_decl.case(&case_name).is_some(); + let is_backed = enum_decl.backing_type().is_some(); + if !has_case { + if eval_reflection_class_constant_metadata( + &declaring_class_name, + &case_name, + context, + values, + )? + .is_some() + { + return eval_reflection_not_enum_case_exception( + &declaring_class_name, + &case_name, + context, + values, + ); + } + return eval_reflection_missing_class_constant_exception( + &declaring_class_name, + &case_name, + context, + values, + ); + } + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && !is_backed { + return eval_throw_reflection_exception( + &format!("Enum case {}::{} is not a backed case", declaring_class_name, case_name), + context, + values, + ); + } + eval_reflection_enum_case_object_result( + owner_kind, + &declaring_class_name, + &case_name, + context, + values, + ) + .map(Some) +} + +/// Throws the catchable ReflectionException for a constant that is not an enum case. +pub(super) fn eval_reflection_not_enum_case_exception( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + eval_throw_reflection_exception( + &format!("Constant {}::{} is not a case", reflected_name, constant_name), + context, + values, + ) +} + +/// Builds one eval-backed enum-case reflection owner object. +pub(super) fn eval_reflection_enum_case_object_result( + owner_kind: u64, + enum_name: &str, + case_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (declaring_class_name, attributes, is_backed) = { + let enum_decl = context.enum_decl(enum_name).ok_or(EvalStatus::RuntimeFatal)?; + let case = enum_decl.case(case_name).ok_or(EvalStatus::RuntimeFatal)?; + ( + enum_decl.name().to_string(), + case.attributes().to_vec(), + enum_decl.backing_type().is_some(), + ) + }; + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE && !is_backed { + return Err(EvalStatus::RuntimeFatal); + } + let case_value = context + .enum_case(&declaring_class_name, case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let backing_value = if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { + Some( + context + .enum_case_value(&declaring_class_name, case_name) + .ok_or(EvalStatus::RuntimeFatal)?, + ) + } else { + None + }; + let flags = eval_reflection_member_flags(EvalVisibility::Public, false, false, false, false) + | EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE; + let modifiers = eval_reflection_class_constant_modifiers(EvalVisibility::Public, false); + eval_reflection_owner_object( + owner_kind, + case_name, + &attributes, + &[], + &[], + &[], + &[], + Some(&declaring_class_name), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + Some(case_value), + backing_value, + context, + values, + ) +} + +/// Selects the concrete enum-case reflector class for one enum. +pub(super) fn eval_reflection_enum_case_owner_kind( + enum_name: &str, + context: &ElephcEvalContext, +) -> Result { + let enum_decl = context.enum_decl(enum_name).ok_or(EvalStatus::RuntimeFatal)?; + Ok(if enum_decl.backing_type().is_some() { + EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + } else { + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + }) +} + +/// Builds `ReflectionNamedType` metadata for an enum backing type. +pub(super) fn eval_reflection_enum_backing_type_metadata( + backing_type: EvalEnumBackingType, +) -> EvalReflectionParameterTypeMetadata { + let name = match backing_type { + EvalEnumBackingType::Int => "int", + EvalEnumBackingType::String => "string", + }; + EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(eval_reflection_builtin_named_type( + name, false, + )), + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/flags.rs b/crates/elephc-magician/src/interpreter/reflection/flags.rs new file mode 100644 index 0000000000..9b97c031e9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/flags.rs @@ -0,0 +1,157 @@ +//! Purpose: +//! Packs Reflection runtime flags and parses common constructor arguments. +//! +//! Called from: +//! - Reflection owner factories and metadata materializers. +//! +//! Key details: +//! - Bit layouts mirror the runtime owner factory contract and remain centralized here. + +use super::*; + +/// Packs ReflectionMethod/ReflectionProperty predicate flags for the runtime owner factory. +pub(super) fn eval_reflection_member_flags( + visibility: EvalVisibility, + is_static: bool, + is_final: bool, + is_abstract: bool, + is_readonly: bool, +) -> u64 { + let mut flags = 0; + if is_static { + flags |= EVAL_REFLECTION_MEMBER_FLAG_STATIC; + } + match visibility { + EvalVisibility::Public => flags |= EVAL_REFLECTION_MEMBER_FLAG_PUBLIC, + EvalVisibility::Protected => flags |= EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, + EvalVisibility::Private => flags |= EVAL_REFLECTION_MEMBER_FLAG_PRIVATE, + } + if is_final { + flags |= EVAL_REFLECTION_MEMBER_FLAG_FINAL; + } + if is_abstract { + flags |= EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT; + } + if is_readonly { + flags |= EVAL_REFLECTION_MEMBER_FLAG_READONLY; + } + flags +} + +/// Packs callable-only ReflectionFunctionAbstract predicate flags. +pub(super) fn eval_reflection_callable_flags(attributes: &[EvalAttribute]) -> u64 { + if eval_reflection_attributes_include_deprecated(attributes) { + EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED + } else { + 0 + } +} + +/// Returns whether an attribute list contains PHP's global `#[Deprecated]` marker. +pub(super) fn eval_reflection_attributes_include_deprecated(attributes: &[EvalAttribute]) -> bool { + attributes + .iter() + .any(|attribute| attribute.name().eq_ignore_ascii_case("Deprecated")) +} + +/// Packs ReflectionParameter predicate flags for the runtime parameter factory. +pub(super) fn eval_reflection_parameter_flags(parameter: &EvalReflectionParameterMetadata) -> u64 { + let mut flags = 0; + if parameter.is_optional { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL; + } + if parameter.is_variadic { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC; + } + if parameter.is_passed_by_reference { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_BY_REF; + } + if parameter.is_promoted { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED; + } + if parameter.has_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE; + } + if parameter.default_value.is_some() { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE; + } + if parameter.allows_null { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL; + } + if parameter.default_value_constant_name.is_some() { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT; + } + if parameter.is_array_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE; + } + if parameter.is_callable_type { + flags |= EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE; + } + flags +} + +/// Packs ReflectionNamedType predicate flags for the runtime type factory. +pub(super) fn eval_reflection_named_type_flags(type_metadata: &EvalReflectionNamedTypeMetadata) -> u64 { + let mut flags = 0; + if type_metadata.allows_null { + flags |= EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL; + } + if type_metadata.is_builtin { + flags |= EVAL_REFLECTION_NAMED_TYPE_FLAG_BUILTIN; + } + flags +} + +/// Packs ReflectionUnionType predicate flags for the runtime type factory. +pub(super) fn eval_reflection_union_type_flags(type_metadata: &EvalReflectionUnionTypeMetadata) -> u64 { + if type_metadata.allows_null { + EVAL_REFLECTION_NAMED_TYPE_FLAG_ALLOWS_NULL + } else { + 0 + } +} + +/// Converts a ReflectionFunction argument into a function or eval-closure name. +pub(super) fn eval_reflection_function_name_arg( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? == EVAL_TAG_OBJECT { + let identity = values.object_identity(value)?; + if let Some(name) = context.closure_object_name(identity) { + return Ok(name.to_string()); + } + } + eval_reflection_string_arg(value, values) +} + +/// Converts one reflection constructor argument to a Rust UTF-8 string. +pub(super) fn eval_reflection_string_arg( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Maps a PHP reflection owner class name to the helper owner kind. +pub(super) fn reflection_owner_kind(class_name: &str) -> Option { + match class_name + .trim_start_matches('\\') + .to_ascii_lowercase() + .as_str() + { + "reflectionclass" => Some(EVAL_REFLECTION_OWNER_CLASS), + "reflectionobject" => Some(EVAL_REFLECTION_OWNER_OBJECT), + "reflectionenum" => Some(EVAL_REFLECTION_OWNER_ENUM), + "reflectionfunction" => Some(EVAL_REFLECTION_OWNER_FUNCTION), + "reflectionmethod" => Some(EVAL_REFLECTION_OWNER_METHOD), + "reflectionproperty" => Some(EVAL_REFLECTION_OWNER_PROPERTY), + "reflectionparameter" => Some(EVAL_REFLECTION_OWNER_PARAMETER), + "reflectionclassconstant" => Some(EVAL_REFLECTION_OWNER_CLASS_CONSTANT), + "reflectionenumunitcase" => Some(EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE), + "reflectionenumbackedcase" => Some(EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE), + _ => None, + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/formatting.rs b/crates/elephc-magician/src/interpreter/reflection/formatting.rs new file mode 100644 index 0000000000..b0a93d6153 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/formatting.rs @@ -0,0 +1,565 @@ +//! Purpose: +//! Formats eval-backed Reflection owners using PHP-compatible string layouts. +//! This module owns presentation only; metadata lookup remains in the parent. +//! +//! Called from: +//! - `crate::interpreter::reflection` for Reflection `__toString()` methods. +//! +//! Key details: +//! - Class, callable, property, constant, parameter, and type formatting share +//! the same visibility, modifier, and default-value conventions. + +use super::*; + +/// Formats one reflected class-like symbol similarly to PHP's `__toString()` output. +pub(super) fn eval_reflection_class_to_string( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let metadata = eval_reflection_class_to_string_metadata(reflected_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let constant_lines = + eval_reflection_class_constant_string_lines(&metadata.resolved_name, context, values)?; + let property_lines = + eval_reflection_class_property_string_lines(&metadata, context, values)?; + let method_lines = eval_reflection_class_method_string_lines(&metadata, context, values)?; + + let mut rendered = format!("{} {{\n", eval_reflection_class_to_string_header(&metadata)); + eval_reflection_class_append_string_section(&mut rendered, "Constants", &constant_lines); + eval_reflection_class_append_string_section(&mut rendered, "Properties", &property_lines); + eval_reflection_class_append_string_section(&mut rendered, "Methods", &method_lines); + rendered.push_str("}\n"); + Ok(rendered) +} + +/// Returns eval or AOT class metadata for a ReflectionClass string dump. +pub(super) fn eval_reflection_class_to_string_metadata( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(mut metadata) = eval_reflection_class_like_attributes(reflected_name, context) { + metadata.interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + metadata.flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + return Ok(Some(metadata)); + } + let runtime_class_name = reflected_name.trim_start_matches('\\'); + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(runtime_class_name, values)? + else { + return Ok(None); + }; + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + runtime_class_name, + values, + )?; + let native_interface_property_names = + eval_reflection_native_interface_property_names(runtime_class_name, context); + let property_names = if native_interface_property_names.is_empty() { + eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + runtime_class_name, + values, + )? + } else { + native_interface_property_names + }; + let interface_names = eval_reflection_aot_class_interface_names(runtime_class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; + let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; + Ok(Some(EvalReflectionClassMetadata { + resolved_name: runtime_class_name.to_string(), + source_location: None, + attributes: context.native_class_attributes(runtime_class_name), + flags, + modifiers, + interface_names, + trait_names, + method_names, + property_names, + parent_class_name, + })) +} + +/// Returns the PHP-like header line for `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_class_to_string_header(metadata: &EvalReflectionClassMetadata) -> String { + let origin = if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_INTERNAL != 0 { + "" + } else { + "" + }; + let kind = eval_reflection_class_to_string_kind(metadata.flags); + let mut parts = Vec::new(); + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + parts.push(String::from("interface")); + parts.push(metadata.resolved_name.clone()); + if !metadata.interface_names.is_empty() { + parts.push(String::from("extends")); + parts.push(metadata.interface_names.join(", ")); + } + } else if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + parts.push(String::from("trait")); + parts.push(metadata.resolved_name.clone()); + } else { + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_ABSTRACT != 0 { + parts.push(String::from("abstract")); + } + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_FINAL != 0 { + parts.push(String::from("final")); + } + if metadata.flags & EVAL_REFLECTION_CLASS_FLAG_READONLY != 0 { + parts.push(String::from("readonly")); + } + parts.push(String::from("class")); + parts.push(metadata.resolved_name.clone()); + if let Some(parent_class_name) = metadata.parent_class_name.as_ref() { + parts.push(String::from("extends")); + parts.push(parent_class_name.clone()); + } + if !metadata.interface_names.is_empty() { + parts.push(String::from("implements")); + parts.push(metadata.interface_names.join(", ")); + } + } + format!("{kind} [ {origin} {} ]", parts.join(" ")) +} + +/// Returns the ReflectionClass string header owner kind label. +pub(super) fn eval_reflection_class_to_string_kind(flags: u64) -> &'static str { + if flags & EVAL_REFLECTION_CLASS_FLAG_INTERFACE != 0 { + "Interface" + } else if flags & EVAL_REFLECTION_CLASS_FLAG_TRAIT != 0 { + "Trait" + } else { + "Class" + } +} + +/// Formats all constants visible to `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_class_constant_string_lines( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let constant_names = eval_reflection_constant_names(reflected_name, context, values)?; + let mut lines = Vec::with_capacity(constant_names.len()); + for constant_name in constant_names { + let line = eval_reflection_class_constant_to_string( + reflected_name, + &constant_name, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT, + context, + values, + )?; + lines.push(line.trim_end_matches('\n').to_string()); + } + Ok(lines) +} + +/// Formats all properties visible to `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_class_property_string_lines( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut lines = Vec::with_capacity(metadata.property_names.len()); + for property_name in &metadata.property_names { + let Some(member) = eval_reflection_reflected_property_metadata( + &metadata.resolved_name, + property_name, + context, + values, + )? + else { + continue; + }; + lines.push(eval_reflection_property_to_string(property_name, &member)); + } + Ok(lines) +} + +/// Formats all methods visible to `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_class_method_string_lines( + metadata: &EvalReflectionClassMetadata, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut lines = Vec::with_capacity(metadata.method_names.len()); + for method_name in &metadata.method_names { + let member = + if let Some(member) = + eval_reflection_method_metadata(&metadata.resolved_name, method_name, context) + { + Some(member) + } else { + eval_reflection_aot_method_metadata_with_signature_if_exists( + &metadata.resolved_name, + method_name, + context, + values, + )? + }; + let Some(member) = member else { + continue; + }; + lines.push(eval_reflection_method_summary_to_string(method_name, &member)); + } + Ok(lines) +} + +/// Appends one named section to a ReflectionClass string dump. +pub(super) fn eval_reflection_class_append_string_section( + rendered: &mut String, + label: &str, + lines: &[String], +) { + rendered.push_str(&format!(" - {label} [{}] {{\n", lines.len())); + for line in lines { + rendered.push_str(" "); + rendered.push_str(line); + rendered.push('\n'); + } + rendered.push_str(" }\n"); +} + +/// Formats one reflected method line for `ReflectionClass::__toString()`. +pub(super) fn eval_reflection_method_summary_to_string( + method_name: &str, + member: &EvalReflectionMemberMetadata, +) -> String { + let mut parts = Vec::new(); + if member.is_abstract { + parts.push(String::from("abstract")); + } + if member.is_final { + parts.push(String::from("final")); + } + if member.is_static { + parts.push(String::from("static")); + } + parts.push(eval_reflection_visibility_label(member.visibility).to_string()); + parts.push(String::from("method")); + parts.push(method_name.to_string()); + format!("Method [ {} ]", parts.join(" ")) +} + +/// Formats one reflected function or method similarly to PHP's `__toString()` output. +pub(super) fn eval_reflection_function_method_to_string( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + let mut rendered = format!( + "{} {{\n - Parameters [{}] {{\n", + eval_reflection_function_method_header(target), + eval_reflection_function_method_parameters(target).len() + ); + for parameter in eval_reflection_function_method_parameters(target) { + rendered.push_str(" "); + rendered.push_str(&eval_reflection_function_method_parameter_to_string(parameter)); + rendered.push('\n'); + } + rendered.push_str(" }\n"); + if let Some(return_type) = eval_reflection_function_method_return_type(target) { + rendered.push_str(" - Return [ "); + rendered.push_str(&eval_reflection_type_metadata_to_string(return_type)); + rendered.push_str(" ]\n"); + } + rendered.push_str("}\n"); + rendered +} + +/// Returns the PHP-like header line for a reflected function or method. +pub(super) fn eval_reflection_function_method_header( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + format!( + "Function [ function {} ]", + name.trim_start_matches('\\') + ) + } + EvalReflectionFunctionMethodTarget::Method { + name, + visibility, + is_static, + is_final, + is_abstract, + .. + } => { + let mut parts = Vec::new(); + if *is_abstract { + parts.push(String::from("abstract")); + } + if *is_final { + parts.push(String::from("final")); + } + if *is_static { + parts.push(String::from("static")); + } + if let Some(visibility) = visibility { + parts.push(eval_reflection_visibility_label(*visibility).to_string()); + } + parts.push(String::from("method")); + parts.push(name.clone()); + format!("Method [ {} ]", parts.join(" ")) + } + } +} + +/// Returns retained parameters for a reflected function or method target. +pub(super) fn eval_reflection_function_method_parameters( + target: &EvalReflectionFunctionMethodTarget, +) -> &[EvalReflectionParameterMetadata] { + match target { + EvalReflectionFunctionMethodTarget::Function { parameters, .. } + | EvalReflectionFunctionMethodTarget::Method { parameters, .. } => parameters, + } +} + +/// Formats one parameter line for function-like `__toString()` output. +pub(super) fn eval_reflection_function_method_parameter_to_string( + parameter: &EvalReflectionParameterMetadata, +) -> String { + let requiredness = if parameter.is_optional { + "optional" + } else { + "required" + }; + let mut signature_parts = Vec::new(); + if let Some(type_metadata) = parameter.type_metadata.as_ref() { + signature_parts.push(eval_reflection_type_metadata_to_string(type_metadata)); + } + let mut variable = String::new(); + if parameter.is_passed_by_reference { + variable.push('&'); + } + if parameter.is_variadic { + variable.push_str("..."); + } + variable.push('$'); + variable.push_str(¶meter.name); + signature_parts.push(variable); + let default = parameter + .default_value + .as_ref() + .and_then(eval_reflection_default_expr_to_string) + .map(|value| format!(" = {value}")) + .unwrap_or_default(); + format!( + "Parameter #{} [ <{}> {}{} ]", + parameter.position, + requiredness, + signature_parts.join(" "), + default + ) +} + +/// Formats one reflected property similarly to PHP's `ReflectionProperty::__toString()`. +pub(super) fn eval_reflection_property_to_string( + property_name: &str, + member: &EvalReflectionMemberMetadata, +) -> String { + if member.is_dynamic { + return format!("Property [ public ${property_name} ]\n"); + } + let mut parts = Vec::new(); + if member.is_abstract { + parts.push(String::from("abstract")); + } + if member.is_final { + parts.push(String::from("final")); + } + parts.push(eval_reflection_visibility_label(member.visibility).to_string()); + if member.is_static { + parts.push(String::from("static")); + } + if member.is_readonly { + parts.push(String::from("readonly")); + } + if let Some(type_name) = member + .type_metadata + .as_ref() + .map(eval_reflection_type_metadata_to_string) + { + parts.push(type_name); + } + parts.push(format!("${property_name}")); + + let default = if member.modifiers & 512 != 0 { + String::new() + } else { + member + .default_value + .as_ref() + .and_then(eval_reflection_default_expr_to_string) + .map(|value| format!(" = {value}")) + .unwrap_or_default() + }; + format!("Property [ {}{} ]", parts.join(" "), default) +} + +/// Formats one class constant or enum case like PHP's `ReflectionClassConstant::__toString()`. +pub(super) fn eval_reflection_class_constant_to_string( + declaring_class: &str, + constant_name: &str, + owner_kind: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, _, visibility, is_final, is_enum_case) = + eval_reflection_class_constant_metadata(declaring_class, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let value = eval_reflection_constant_value(declaring_class, constant_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let mut parts = Vec::new(); + if is_final { + parts.push(String::from("final")); + } + parts.push(eval_reflection_visibility_label(visibility).to_string()); + parts.push(eval_reflection_class_constant_type_label( + declaring_class, + value, + is_enum_case + || matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ), + values, + )?); + parts.push(constant_name.to_string()); + let value = eval_reflection_class_constant_display_value(value, values)?; + Ok(format!("Constant [ {} ] {{ {} }}\n", parts.join(" "), value)) +} + +/// Returns the type label PHP prints for a reflected class constant value. +pub(super) fn eval_reflection_class_constant_type_label( + declaring_class: &str, + value: RuntimeCellHandle, + is_enum_case: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + if is_enum_case { + return Ok(declaring_class.trim_start_matches('\\').to_string()); + } + Ok(match values.type_tag(value)? { + EVAL_TAG_INT => String::from("int"), + EVAL_TAG_STRING => String::from("string"), + EVAL_TAG_FLOAT => String::from("float"), + EVAL_TAG_BOOL => String::from("bool"), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("array"), + EVAL_TAG_OBJECT => String::from("object"), + EVAL_TAG_NULL => String::from("null"), + _ => String::from("mixed"), + }) +} + +/// Returns the value display PHP prints inside ReflectionClassConstant braces. +pub(super) fn eval_reflection_class_constant_display_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(match values.type_tag(value)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => String::from("Array"), + EVAL_TAG_OBJECT => String::from("Object"), + EVAL_TAG_NULL => String::new(), + _ => String::from_utf8_lossy(&values.string_bytes(value)?).into_owned(), + }) +} + +/// Returns PHP's lowercase label for one reflected visibility. +pub(super) fn eval_reflection_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} + +/// Formats retained ReflectionType metadata for `ReflectionProperty::__toString()`. +pub(super) fn eval_reflection_type_metadata_to_string( + type_metadata: &EvalReflectionParameterTypeMetadata, +) -> String { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named) => { + if named.allows_null && named.name != "mixed" { + format!("?{}", named.name) + } else { + named.name.clone() + } + } + EvalReflectionParameterTypeKind::Union(union) => { + let mut names = union + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>(); + if union.allows_null && names.iter().all(|name| name != "null") { + names.push(String::from("null")); + } + names.join("|") + } + EvalReflectionParameterTypeKind::Intersection(intersection) => intersection + .types + .iter() + .map(|type_metadata| type_metadata.name.clone()) + .collect::>() + .join("&"), + } +} + +/// Formats retained literal defaults for `ReflectionProperty::__toString()`. +pub(super) fn eval_reflection_default_expr_to_string(default: &EvalExpr) -> Option { + match default { + EvalExpr::Const(EvalConst::Null) => Some(String::from("NULL")), + EvalExpr::Const(EvalConst::Bool(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::Int(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::Float(value)) => Some(value.to_string()), + EvalExpr::Const(EvalConst::String(value)) => Some(format!("'{value}'")), + EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr, + } => eval_reflection_default_expr_to_string(expr), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => eval_reflection_default_expr_to_string(expr).map(|value| format!("-{value}")), + EvalExpr::ConstFetch(name) => Some(name.clone()), + EvalExpr::NamespacedConstFetch { name, .. } => Some(name.clone()), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => Some(format!("{class_name}::{constant}")), + _ => None, + } +} + +/// Returns whether eval retained this property as virtual rather than backed. +pub(super) fn eval_reflection_property_is_virtual(property: &EvalClassProperty) -> bool { + property.is_virtual() +} + +/// Computes PHP's `ReflectionMethod::getModifiers()` bitmask from eval member flags. +pub(super) fn eval_reflection_method_modifiers_from_flags(flags: u64) -> u64 { + let mut modifiers = 0; + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0 { + modifiers |= 1; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0 { + modifiers |= 2; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0 { + modifiers |= 4; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC) != 0 { + modifiers |= 16; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0 { + modifiers |= 32; + } + if (flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0 { + modifiers |= 64; + } + modifiers +} diff --git a/crates/elephc-magician/src/interpreter/reflection/function_construction.rs b/crates/elephc-magician/src/interpreter/reflection/function_construction.rs new file mode 100644 index 0000000000..3bfd265481 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/function_construction.rs @@ -0,0 +1,270 @@ +//! Purpose: +//! Constructs `ReflectionFunction` owners for eval, closure, and native targets. +//! +//! Called from: +//! - `crate::interpreter::reflection::eval_reflection_owner_new_object()`. +//! +//! Key details: +//! - Closure targets and native parameter/default metadata are attached once here. + +use super::*; + +/// Builds an eval-backed `ReflectionFunction` object for eval or registered native functions. +pub(super) fn eval_reflection_function_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("function")], evaluated_args)?; + let closure_target = eval_reflection_function_closure_target_arg(args[0], context, values)?; + let requested_name = match closure_target.as_ref() { + Some(target) => eval_reflection_function_closure_target_name(target), + None => eval_reflection_function_name_arg(args[0], context, values)?, + }; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if let Some(closure) = context.closure(&requested_name).cloned() { + let function = closure.function(); + let required_parameter_count = eval_reflection_required_parameter_count( + function.parameter_defaults(), + function.parameter_is_variadic(), + ); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + return eval_reflection_function_object_result( + &requested_name, + function.attributes(), + ¶meters, + required_parameter_count, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + if let Some(function) = context.function(&lookup_name).cloned() { + let required_parameter_count = eval_reflection_required_parameter_count( + function.parameter_defaults(), + function.parameter_is_variadic(), + ); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + return eval_reflection_function_object_result( + function.name(), + function.attributes(), + ¶meters, + required_parameter_count, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + if let Some(function) = context.native_function(&lookup_name) { + let reflected_name = requested_name.trim_start_matches('\\'); + let required_parameter_count = function.required_param_count(); + let parameters = eval_reflection_native_function_parameters(reflected_name, &function); + return eval_reflection_function_object_result( + reflected_name, + &[], + ¶meters, + required_parameter_count, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + if closure_target.is_some() { + return eval_reflection_function_object_result( + &requested_name, + &[], + &[], + 0, + context, + values, + ) + .and_then(|object| { + eval_reflection_attach_function_closure_target( + object, + closure_target, + context, + values, + ) + }) + .map(Some); + } + Ok(None) +} + +/// Returns the retained callable target when a ReflectionFunction argument is a Closure object. +pub(super) fn eval_reflection_function_closure_target_arg( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Ok(None); + } + let identity = values.object_identity(value)?; + Ok(context.closure_object_target(identity).cloned()) +} + +/// Returns the function-like name exposed for a Closure-backed ReflectionFunction. +pub(super) fn eval_reflection_function_closure_target_name(target: &EvalClosureObjectTarget) -> String { + match target { + EvalClosureObjectTarget::Named(name) + | EvalClosureObjectTarget::BoundNamed { name, .. } => name.clone(), + EvalClosureObjectTarget::InvokableObject { .. } => String::from("__invoke"), + EvalClosureObjectTarget::ObjectMethod { method, .. } + | EvalClosureObjectTarget::StaticMethod { method, .. } => method.clone(), + } +} + +/// Attaches original Closure target metadata to a synthetic ReflectionFunction object. +pub(super) fn eval_reflection_attach_function_closure_target( + object: RuntimeCellHandle, + closure_target: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(closure_target) = closure_target else { + return Ok(object); + }; + let identity = values.object_identity(object)?; + context.register_eval_reflection_function_closure_target(identity, closure_target); + Ok(object) +} + +/// Returns parameter names for a registered native function, filling missing bridge names. +pub(super) fn eval_reflection_native_function_parameter_names(function: &NativeFunction) -> Vec { + (0..function.param_count()) + .map(|index| { + function + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", index)) + }) + .collect() +} + +/// Builds ReflectionParameter metadata for one registered native AOT function. +pub(super) fn eval_reflection_native_function_parameters( + function_name: &str, + function: &NativeFunction, +) -> Vec { + let parameter_names = eval_reflection_native_function_parameter_names(function); + let parameter_count = parameter_names.len(); + let parameter_attributes = vec![Vec::new(); parameter_count]; + let parameter_types = eval_reflection_native_function_parameter_types(function); + let parameter_defaults = eval_reflection_native_function_parameter_defaults(function); + let parameter_is_by_ref = (0..parameter_count) + .map(|index| function.param_by_ref(index)) + .collect::>(); + let parameter_is_variadic = (0..parameter_count) + .map(|index| function.param_variadic(index)) + .collect::>(); + eval_reflection_function_parameters( + function_name, + ¶meter_names, + Vec::new(), + ¶meter_attributes, + ¶meter_types, + ¶meter_defaults, + ¶meter_is_by_ref, + ¶meter_is_variadic, + ) +} + +/// Converts registered native function parameter types into reflection metadata input. +pub(super) fn eval_reflection_native_function_parameter_types( + function: &NativeFunction, +) -> Vec> { + (0..function.param_count()) + .map(|index| function.param_type(index).cloned()) + .collect() +} + +/// Converts registered native function defaults into eval constant expressions. +pub(super) fn eval_reflection_native_function_parameter_defaults( + function: &NativeFunction, +) -> Vec> { + (0..function.param_count()) + .map(|index| { + function + .param_default(index) + .map(eval_reflection_native_callable_default_expr) + }) + .collect() +} + +/// Builds one `ReflectionFunction` object from retained eval function metadata. +pub(super) fn eval_reflection_function_object_result( + function_name: &str, + attributes: &[EvalAttribute], + parameters: &[EvalReflectionParameterMetadata], + required_parameter_count: usize, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_FUNCTION, + function_name, + attributes, + &[], + &[], + &[], + &[], + None, + parameters, + None, + None, + None, + None, + eval_reflection_callable_flags(attributes), + required_parameter_count as u64, + 0, + None, + None, + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/function_metadata.rs b/crates/elephc-magician/src/interpreter/reflection/function_metadata.rs new file mode 100644 index 0000000000..ff251b00d2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/function_metadata.rs @@ -0,0 +1,934 @@ +//! Purpose: +//! Resolves metadata shared by `ReflectionFunction` and `ReflectionMethod`. +//! It covers callable identity, source data, closures, statics, and prototypes. +//! +//! Called from: +//! - `crate::interpreter::reflection` while answering callable Reflection APIs. +//! +//! Key details: +//! - Eval, native, closure, and AOT method targets converge on one metadata shape. +//! - Prototype traversal preserves parent and interface declaration order. + +use super::*; + +/// Returns function or method metadata registered for a synthetic reflection owner object. +pub(super) fn eval_reflection_function_method_target( + identity: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(name) = context.eval_reflection_function_name(identity) { + let closure_target = context + .eval_reflection_function_closure_target(identity) + .cloned(); + if let Some(closure) = context.closure(name) { + let function = closure.function(); + let is_variadic = function.parameter_is_variadic().iter().any(|flag| *flag); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + let source_location = function.source_location(); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let static_key = Some(function.name().to_string()); + let static_variables = static_var_initializers(function.body()); + let is_deprecated = + eval_reflection_attributes_include_deprecated(function.attributes()); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key, + static_variables, + closure_captures: closure.captures().to_vec(), + parameters, + source_location, + closure_target, + is_variadic, + is_static: closure.is_static(), + is_closure: true, + is_deprecated, + return_type_metadata, + })); + } + let lookup_name = name.to_ascii_lowercase(); + if let Some(function) = context.function(&lookup_name) { + let is_variadic = function + .parameter_is_variadic() + .iter() + .any(|flag| *flag); + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + let source_location = function.source_location(); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let static_key = Some(function.name().to_string()); + let static_variables = static_var_initializers(function.body()); + let is_deprecated = + eval_reflection_attributes_include_deprecated(function.attributes()); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key, + static_variables, + closure_captures: Vec::new(), + parameters, + source_location, + closure_target: closure_target.clone(), + is_variadic, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), + is_deprecated, + return_type_metadata, + })); + } + if let Some(function) = context.native_function(&lookup_name) { + let parameters = eval_reflection_native_function_parameters(name, &function); + let is_variadic = + (0..function.param_count()).any(|index| function.param_variadic(index)); + let return_type_metadata = function + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key: None, + static_variables: Vec::new(), + closure_captures: Vec::new(), + parameters, + source_location: None, + closure_target: closure_target.clone(), + is_variadic, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), + is_deprecated: false, + return_type_metadata, + })); + } + return Ok(Some(EvalReflectionFunctionMethodTarget::Function { + name: name.to_string(), + static_key: None, + static_variables: Vec::new(), + closure_captures: Vec::new(), + parameters: Vec::new(), + source_location: None, + closure_target: closure_target.clone(), + is_variadic: false, + is_static: eval_reflection_closure_target_is_static(closure_target.as_ref()), + is_closure: closure_target.is_some(), + is_deprecated: false, + return_type_metadata: None, + })); + } + let Some((declaring_class, method_name)) = context.eval_reflection_method(identity) else { + return Ok(None); + }; + let method_metadata = if let Some(method_metadata) = + eval_reflection_method_metadata(declaring_class, method_name, context) + { + Some(method_metadata) + } else { + eval_reflection_aot_method_metadata_with_signature_if_exists( + declaring_class, + method_name, + context, + values, + )? + }; + let ( + parameters, + source_file, + source_location, + visibility, + is_variadic, + is_static, + is_final, + is_abstract, + is_deprecated, + return_type_metadata, + ) = match method_metadata { + Some(method) => { + let is_variadic = method + .parameters + .iter() + .any(|parameter| parameter.is_variadic); + let is_deprecated = + eval_reflection_attributes_include_deprecated(&method.attributes); + ( + method.parameters, + method.source_file, + method.source_location, + Some(method.visibility), + is_variadic, + method.is_static, + method.is_final, + method.is_abstract, + is_deprecated, + method.return_type_metadata, + ) + } + None => ( + Vec::new(), + None, + None, + None, + false, + false, + false, + false, + false, + None, + ), + }; + let static_method = + eval_reflection_eval_method_static_target(declaring_class, method_name, context); + let declaring_class = static_method + .as_ref() + .map(|(declaring_class, _)| declaring_class.clone()); + let static_key = static_method + .as_ref() + .map(|(declaring_class, method)| eval_method_static_local_key(declaring_class, method.name())); + let static_variables = static_method + .as_ref() + .map(|(_, method)| static_var_initializers(method.body())) + .unwrap_or_default(); + Ok(Some(EvalReflectionFunctionMethodTarget::Method { + declaring_class, + name: method_name.to_string(), + static_key, + static_variables, + parameters, + source_file, + source_location, + visibility, + is_variadic, + is_static, + is_final, + is_abstract, + is_deprecated, + return_type_metadata, + })) +} + +/// Returns an eval method body that can contribute ReflectionMethod static locals. +pub(super) fn eval_reflection_eval_method_static_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + return context.class_method(declaring_class, method_name); + } + let trait_decl = context.trait_decl(declaring_class)?; + trait_decl + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| (trait_decl.name().to_string(), method.clone())) +} + +/// Builds the static-local storage key shared by method execution and reflection. +pub(super) fn eval_method_static_local_key(class_name: &str, method_name: &str) -> String { + format!("{}::{}", class_name.trim_start_matches('\\'), method_name) +} + +/// Builds the associative `getStaticVariables()` result for eval-backed reflection. +pub(super) fn eval_reflection_function_method_static_variables_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (Some(static_key), static_variables, declaring_class) = + eval_reflection_function_method_static_target(target) + else { + return values.array_new(0); + }; + let mut result = values.assoc_new(static_variables.len())?; + for variable in static_variables { + let key = values.string(&variable.name)?; + let value = eval_reflection_static_local_value( + static_key, + variable, + declaring_class, + context, + values, + )?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Returns static-local storage details retained for a reflected eval function or method. +pub(super) fn eval_reflection_function_method_static_target( + target: &EvalReflectionFunctionMethodTarget, +) -> ( + Option<&str>, + &[EvalStaticVarInitializer], + Option<&str>, +) { + match target { + EvalReflectionFunctionMethodTarget::Function { + static_key, + static_variables, + .. + } => (static_key.as_deref(), static_variables, None), + EvalReflectionFunctionMethodTarget::Method { + declaring_class, + static_key, + static_variables, + .. + } => ( + static_key.as_deref(), + static_variables, + declaring_class.as_deref(), + ), + } +} + +/// Returns the retained current static value or initializes it for reflection. +pub(super) fn eval_reflection_static_local_value( + static_key: &str, + variable: &EvalStaticVarInitializer, + declaring_class: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = context.static_local(static_key, &variable.name) { + return values.retain(value); + } + let value = eval_reflection_static_local_initializer_value( + static_key, + &variable.init, + declaring_class, + context, + values, + )?; + if let Some(replaced) = + context.set_static_local(static_key.to_string(), variable.name.clone(), value) + { + values.release(replaced)?; + } + values.retain(value) +} + +/// Evaluates a static-local initializer with PHP magic class/function context. +pub(super) fn eval_reflection_static_local_initializer_value( + static_key: &str, + init: &EvalExpr, + declaring_class: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(declaring_class) = declaring_class { + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + } + context.push_function(static_key.to_string()); + let mut scope = ElephcEvalScope::new(); + let result = eval_expr(init, context, &mut scope, values); + for cell in scope.drain_owned_cells() { + values.release(cell)?; + } + context.pop_function(); + if declaring_class.is_some() { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + result +} + +/// Validates that a synthetic reflection metadata call received no arguments. +pub(super) fn eval_reflection_bind_no_args(evaluated_args: Vec) -> Result<(), EvalStatus> { + let _ = bind_evaluated_function_args(&[], evaluated_args)?; + Ok(()) +} + +/// Returns a no-argument reflection metadata predicate result that is always false. +pub(super) fn eval_reflection_false_metadata_result( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + values.bool_value(false).map(Some) +} + +/// Returns source file or line metadata for eval-backed reflection objects. +pub(super) fn eval_reflection_source_location_result( + method_key: &str, + source_file: Option<&str>, + source_location: Option, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_bind_no_args(evaluated_args)?; + let Some(source_location) = source_location else { + return values.bool_value(false).map(Some); + }; + match method_key { + "getfilename" => { + let eval_file; + let file = if let Some(source_file) = source_file { + source_file + } else { + eval_file = context.eval_file_magic(); + &eval_file + }; + values.string(file).map(Some) + } + "getstartline" => values.int(source_location.start_line()).map(Some), + "getendline" => values.int(source_location.end_line()).map(Some), + _ => Ok(None), + } +} + +/// Returns PHP's short name for a ReflectionFunction or ReflectionMethod target. +pub(super) fn eval_reflection_function_method_short_name( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + eval_reflection_short_name(name) + } + EvalReflectionFunctionMethodTarget::Method { name, .. } => name.clone(), + } +} + +/// Returns eval-fragment source metadata for a ReflectionFunction or ReflectionMethod target. +pub(super) fn eval_reflection_function_method_source_location( + target: &EvalReflectionFunctionMethodTarget, +) -> (Option<&str>, Option) { + match target { + EvalReflectionFunctionMethodTarget::Function { + source_location, .. + } => (None, *source_location), + EvalReflectionFunctionMethodTarget::Method { + source_file, + source_location, .. + } => (source_file.as_deref(), *source_location), + } +} + +/// Returns PHP's namespace name for a ReflectionFunction or ReflectionMethod target. +pub(super) fn eval_reflection_function_method_namespace_name( + target: &EvalReflectionFunctionMethodTarget, +) -> String { + match target { + EvalReflectionFunctionMethodTarget::Function { name, .. } => { + eval_reflection_namespace_name(name) + } + EvalReflectionFunctionMethodTarget::Method { .. } => String::new(), + } +} + +/// Returns whether the reflected function or method has a variadic parameter. +pub(super) fn eval_reflection_function_method_is_variadic( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_variadic, .. } + | EvalReflectionFunctionMethodTarget::Method { is_variadic, .. } => *is_variadic, + } +} + +/// Returns whether the reflected function-like target is an eval closure literal. +pub(super) fn eval_reflection_function_method_is_closure( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_closure, .. } => *is_closure, + EvalReflectionFunctionMethodTarget::Method { .. } => false, + } +} + +/// Returns whether the reflected function-like target is static. +pub(super) fn eval_reflection_function_method_is_static(target: &EvalReflectionFunctionMethodTarget) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_static, .. } + | EvalReflectionFunctionMethodTarget::Method { is_static, .. } => *is_static, + } +} + +/// Returns whether retained Closure target metadata represents a static callable. +pub(super) fn eval_reflection_closure_target_is_static(target: Option<&EvalClosureObjectTarget>) -> bool { + matches!(target, Some(EvalClosureObjectTarget::StaticMethod { .. })) +} + +/// Returns whether the reflected function-like target carries `#[Deprecated]`. +pub(super) fn eval_reflection_function_method_is_deprecated( + target: &EvalReflectionFunctionMethodTarget, +) -> bool { + match target { + EvalReflectionFunctionMethodTarget::Function { is_deprecated, .. } + | EvalReflectionFunctionMethodTarget::Method { is_deprecated, .. } => *is_deprecated, + } +} + +/// Builds `ReflectionFunction::getClosureUsedVariables()` for eval closure targets. +pub(super) fn eval_reflection_function_closure_used_variables_result( + target: &EvalReflectionFunctionMethodTarget, + values: &mut impl RuntimeValueOps, +) -> Result { + let EvalReflectionFunctionMethodTarget::Function { + is_closure: true, + closure_captures, + .. + } = target + else { + return values.array_new(0); + }; + let mut result = values.assoc_new(closure_captures.len())?; + for capture in closure_captures { + let key = values.string(capture.name())?; + let value = values.retain(capture.value())?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds `ReflectionFunction::getClosureThis()` from retained Closure target metadata. +pub(super) fn eval_reflection_function_closure_this_result( + target: &EvalReflectionFunctionMethodTarget, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(target) = eval_reflection_function_closure_target(target) else { + return values.null(); + }; + match target { + EvalClosureObjectTarget::BoundNamed { + bound_this: Some(object), + .. + } + | EvalClosureObjectTarget::InvokableObject { object } + | EvalClosureObjectTarget::ObjectMethod { object, .. } => values.retain(*object), + EvalClosureObjectTarget::Named(_) + | EvalClosureObjectTarget::BoundNamed { + bound_this: None, .. + } + | EvalClosureObjectTarget::StaticMethod { .. } => values.null(), + } +} + +/// Builds `ReflectionFunction::getClosureScopeClass()` from retained Closure metadata. +pub(super) fn eval_reflection_function_closure_scope_class_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = + eval_reflection_function_closure_scope_class_name(target, context, values)?; + eval_reflection_function_closure_class_object_result(class_name, context, values) +} + +/// Builds `ReflectionFunction::getClosureCalledClass()` from retained Closure metadata. +pub(super) fn eval_reflection_function_closure_called_class_result( + target: &EvalReflectionFunctionMethodTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = + eval_reflection_function_closure_called_class_name(target, context, values)?; + eval_reflection_function_closure_class_object_result(class_name, context, values) +} + +/// Returns the retained callable target for a Closure-backed ReflectionFunction. +pub(super) fn eval_reflection_function_closure_target( + target: &EvalReflectionFunctionMethodTarget, +) -> Option<&EvalClosureObjectTarget> { + match target { + EvalReflectionFunctionMethodTarget::Function { closure_target, .. } => { + closure_target.as_ref() + } + EvalReflectionFunctionMethodTarget::Method { .. } => None, + } +} + +/// Resolves the PHP closure scope class name for retained Closure target metadata. +pub(super) fn eval_reflection_function_closure_scope_class_name( + target: &EvalReflectionFunctionMethodTarget, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_closure_target(target) else { + return Ok(None); + }; + match target { + EvalClosureObjectTarget::Named(_) => Ok(None), + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + } => { + if context.closure(name).is_none() { + return Ok(bound_this.map(|_| String::from("Closure"))); + } + if let Some(bound_scope) = bound_scope { + return Ok(Some(bound_scope.clone())); + } + match bound_this { + Some(object) => eval_closure_bound_object_class_name(*object, context, values) + .map(Some), + None => Ok(None), + } + } + EvalClosureObjectTarget::InvokableObject { object } + | EvalClosureObjectTarget::ObjectMethod { object, .. } => { + eval_closure_bound_object_class_name(*object, context, values).map(Some) + } + EvalClosureObjectTarget::StaticMethod { class_name, .. } => { + Ok(Some(class_name.trim_start_matches('\\').to_string())) + } + } +} + +/// Resolves the PHP closure called class name for retained Closure target metadata. +pub(super) fn eval_reflection_function_closure_called_class_name( + target: &EvalReflectionFunctionMethodTarget, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(target) = eval_reflection_function_closure_target(target) else { + return Ok(None); + }; + match target { + EvalClosureObjectTarget::Named(_) => Ok(None), + EvalClosureObjectTarget::BoundNamed { + bound_this, + bound_scope, + .. + } => match bound_this { + Some(object) => eval_closure_bound_object_class_name(*object, context, values) + .map(Some), + None => Ok(bound_scope.clone()), + }, + EvalClosureObjectTarget::InvokableObject { object } => { + eval_closure_bound_object_class_name(*object, context, values).map(Some) + } + EvalClosureObjectTarget::ObjectMethod { + object, + called_class, + .. + } => match called_class { + Some(called_class) => Ok(Some(called_class.clone())), + None => eval_closure_bound_object_class_name(*object, context, values).map(Some), + }, + EvalClosureObjectTarget::StaticMethod { + class_name, + called_class, + .. + } => Ok(Some( + called_class + .as_deref() + .unwrap_or(class_name) + .trim_start_matches('\\') + .to_string(), + )), + } +} + +/// Materializes a nullable ReflectionClass result for Closure scope metadata. +pub(super) fn eval_reflection_function_closure_class_object_result( + class_name: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = class_name else { + return values.null(); + }; + eval_reflection_full_class_object_result(&class_name, context, values) +} + +/// Returns the retained return type metadata for a reflected function or method. +pub(super) fn eval_reflection_function_method_return_type( + target: &EvalReflectionFunctionMethodTarget, +) -> Option<&EvalReflectionParameterTypeMetadata> { + match target { + EvalReflectionFunctionMethodTarget::Function { + return_type_metadata, + .. + } + | EvalReflectionFunctionMethodTarget::Method { + return_type_metadata, + .. + } => return_type_metadata.as_ref(), + } +} + +/// Returns the final namespace segment-free name component from a PHP symbol name. +pub(super) fn eval_reflection_short_name(name: &str) -> String { + let name = name.trim_start_matches('\\'); + name.rsplit_once('\\').map_or_else( + || name.to_string(), + |(_, short_name)| short_name.to_string(), + ) +} + +/// Returns the namespace prefix from a PHP function name, or an empty string. +pub(super) fn eval_reflection_namespace_name(name: &str) -> String { + name.trim_start_matches('\\') + .rsplit_once('\\') + .map_or_else(String::new, |(namespace_name, _)| { + namespace_name.to_string() + }) +} + +/// Builds ReflectionMethod metadata for a resolved eval or AOT prototype target. +pub(super) fn eval_reflection_prototype_method_metadata( + prototype_class: &str, + prototype_method: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(metadata) = + eval_reflection_method_metadata(prototype_class, prototype_method, context) + { + return Ok(Some(metadata)); + } + eval_reflection_aot_method_metadata_with_signature_if_exists( + prototype_class, + prototype_method, + context, + values, + ) +} + +/// Finds the PHP ReflectionMethod prototype target for an eval or AOT method. +pub(super) fn eval_reflection_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + let want_static = eval_reflection_method_metadata(declaring_class, method_name, context) + .map_or(false, |metadata| metadata.is_static); + if let Some(prototype) = + eval_reflection_parent_method_prototype_target(declaring_class, method_name, context) + { + return Ok(Some(prototype)); + } + if let Some(prototype) = eval_reflection_eval_aot_parent_method_prototype_target( + declaring_class, + method_name, + context, + want_static, + values, + )? { + return Ok(Some(prototype)); + } + if let Some(prototype) = + eval_reflection_interface_method_prototype_target(declaring_class, method_name, context) + { + return Ok(Some(prototype)); + } + return eval_reflection_aot_interface_method_prototype_target_for_eval( + declaring_class, + method_name, + context, + want_static, + values, + ); + } + eval_reflection_aot_method_prototype_target(declaring_class, method_name, values) +} + +/// Finds the PHP ReflectionMethod prototype target for a generated/AOT method. +pub(super) fn eval_reflection_aot_method_prototype_target( + declaring_class: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(declaring_class, method_name)? else { + return Ok(None); + }; + if let Some(prototype) = + eval_reflection_aot_parent_method_prototype_target(declaring_class, method_name, flags, values)? + { + return Ok(Some(prototype)); + } + eval_reflection_aot_interface_method_prototype_target( + declaring_class, + method_name, + flags, + values, + ) +} + +/// Finds the nearest generated/AOT parent-class method that can act as prototype. +pub(super) fn eval_reflection_aot_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + method_flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let want_static = method_flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let mut current = eval_reflection_aot_parent_class_name(declaring_class, values)?; + let mut seen = std::collections::HashSet::new(); + while let Some(parent_class) = current { + if !seen.insert(parent_class.to_ascii_lowercase()) { + break; + } + if let Some(prototype) = + eval_reflection_aot_method_candidate(&parent_class, method_name, want_static, values)? + { + return Ok(Some(prototype)); + } + current = eval_reflection_aot_parent_class_name(&parent_class, values)?; + } + Ok(None) +} + +/// Finds the first generated/AOT interface method that can act as prototype. +pub(super) fn eval_reflection_aot_interface_method_prototype_target( + declaring_class: &str, + method_name: &str, + method_flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let want_static = method_flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + for interface_name in eval_reflection_aot_class_interface_names(declaring_class, values)? { + if let Some(prototype) = + eval_reflection_aot_method_candidate(&interface_name, method_name, want_static, values)? + { + return Ok(Some(prototype)); + } + } + Ok(None) +} + +/// Returns one generated/AOT method prototype candidate if staticness and visibility match. +pub(super) fn eval_reflection_aot_method_candidate( + class_name: &str, + method_name: &str, + want_static: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(class_name, method_name)? else { + return Ok(None); + }; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let is_private = flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0; + if is_static != want_static || is_private { + return Ok(None); + } + let declaring_class = values + .reflection_method_declaring_class(class_name, method_name)? + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + Ok(Some((declaring_class, method_name.to_ascii_lowercase()))) +} + +/// Finds the nearest parent-class method prototype for an eval-declared override. +pub(super) fn eval_reflection_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, String)> { + for parent_class in context.class_parent_names(declaring_class) { + if let Some((prototype_class, prototype_method)) = + context.class_own_method(&parent_class, method_name) + { + return Some((prototype_class, prototype_method.name().to_string())); + } + } + None +} + +/// Finds the nearest generated/AOT parent-class method prototype for an eval method. +pub(super) fn eval_reflection_eval_aot_parent_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, + want_static: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent_class) = context.class_native_parent_name(declaring_class) else { + return Ok(None); + }; + eval_reflection_aot_method_candidate(&parent_class, method_name, want_static, values) +} + +/// Finds the interface method prototype for an eval-declared class method. +pub(super) fn eval_reflection_interface_method_prototype_target( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, String)> { + let mut seen = std::collections::HashSet::new(); + for interface_name in context.class_interface_names(declaring_class) { + if let Some(prototype) = eval_reflection_interface_declared_method_target( + &interface_name, + method_name, + context, + &mut seen, + ) { + return Some(prototype); + } + } + None +} + +/// Finds an AOT interface method prototype for an eval-declared class method. +pub(super) fn eval_reflection_aot_interface_method_prototype_target_for_eval( + declaring_class: &str, + method_name: &str, + context: &ElephcEvalContext, + want_static: bool, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + for interface_name in eval_reflection_eval_class_interface_names( + declaring_class, + context, + values, + )? { + if context.has_interface(&interface_name) { + continue; + } + if let Some(prototype) = + eval_reflection_aot_method_candidate(&interface_name, method_name, want_static, values)? + { + return Ok(Some(prototype)); + } + } + Ok(None) +} + +/// Finds the interface that actually declares a method in an interface hierarchy. +pub(super) fn eval_reflection_interface_declared_method_target( + interface_name: &str, + method_name: &str, + context: &ElephcEvalContext, + seen: &mut std::collections::HashSet, +) -> Option<(String, String)> { + let interface = context.interface(interface_name)?; + if !seen.insert(interface.name().to_ascii_lowercase()) { + return None; + } + if let Some(method) = interface + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + { + return Some((interface.name().to_string(), method.name().to_string())); + } + for parent in interface.parents() { + if let Some(prototype) = + eval_reflection_interface_declared_method_target(parent, method_name, context, seen) + { + return Some(prototype); + } + } + None +} diff --git a/crates/elephc-magician/src/interpreter/reflection/invocation.rs b/crates/elephc-magician/src/interpreter/reflection/invocation.rs new file mode 100644 index 0000000000..97ad08a120 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/invocation.rs @@ -0,0 +1,270 @@ +//! Purpose: +//! Invokes reflected eval, closure, native, and AOT functions or methods. +//! +//! Called from: +//! - Callable Reflection APIs after PHP argument normalization. +//! +//! Key details: +//! - Forwarded named arguments and declaring-class scope are preserved across dispatch paths. + +use super::*; + +/// Binds `ReflectionMethod::invoke()` arguments and preserves forwarded named args. +pub(super) fn eval_reflection_method_invoke_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let mut object = None; + let mut method_args = Vec::new(); + for arg in evaluated_args { + if matches!(arg.name.as_deref(), Some("object")) { + if object.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + object = Some(arg.value); + } else if object.is_none() && arg.name.is_none() { + object = Some(arg.value); + } else { + method_args.push(eval_reflection_method_forwarded_value_arg(arg)); + } + } + object + .map(|object| (object, method_args)) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts a variadic `invoke()` argument into a by-value forwarded method argument. +pub(super) fn eval_reflection_method_forwarded_value_arg(arg: EvaluatedCallArg) -> EvaluatedCallArg { + EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + } +} + +/// Binds `ReflectionMethod::invokeArgs()` and expands its PHP argument array. +pub(super) fn eval_reflection_method_invoke_args_array( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("object"), String::from("args")], + evaluated_args, + )?; + let method_args = eval_array_call_arg_values(args[1], context, values)?; + Ok((args[0], method_args)) +} + +/// Binds `ReflectionFunction::invokeArgs()` and expands its PHP argument array. +pub(super) fn eval_reflection_function_invoke_args_array( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; + eval_array_call_arg_values(args[0], context, values) +} + +/// Dispatches one reflected function invocation through eval or registered native functions. +pub(super) fn eval_reflection_function_invoke_dispatch( + function_name: &str, + function_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(closure) = context.closure(function_name).cloned() { + return eval_closure_with_evaluated_args_and_bound_scope_ref_mode( + &closure, + None, + closure.function().parameter_is_by_ref(), + function_args, + EvalByRefBindingMode::WarnByValue { + callable_name: closure.function().name(), + }, + context, + values, + ); + } + let function_key = function_name.to_ascii_lowercase(); + if let Some(function) = context.function(&function_key).cloned() { + return eval_dynamic_function_with_evaluated_args_and_ref_mode( + &function, + function.parameter_is_by_ref(), + function_args, + EvalByRefBindingMode::WarnByValue { + callable_name: function.name(), + }, + context, + values, + ); + } + eval_callable_with_call_array_args(&function_key, function_args, context, values) +} + +/// Dispatches one reflected method invocation through eval or AOT bridges. +pub(super) fn eval_reflection_method_invoke_dispatch( + declaring_class: &str, + method_name: &str, + object: RuntimeCellHandle, + method_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let lookup_method_name = eval_reflection_property_hook_synthetic_method_name(method_name) + .unwrap_or_else(|| method_name.to_string()); + if let Some((method_class, method)) = context.class_method(declaring_class, &lookup_method_name) + { + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let callable_name = format!( + "{}::{}", + method_class.trim_start_matches('\\'), + method.name() + ); + if method.is_static() { + return eval_dynamic_static_method_with_values_and_ref_mode( + &method_class, + &method_class, + &method, + method.parameter_is_by_ref(), + method_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ); + } + let called_class = + eval_reflection_method_instance_called_class(declaring_class, object, context, values)?; + return eval_dynamic_method_with_values_and_ref_mode( + &method_class, + &called_class, + &method, + object, + method.parameter_is_by_ref(), + method_args, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ); + } + if eval_enum_static_builtin_applies(declaring_class, &lookup_method_name, context).is_some() { + return eval_enum_builtin_static_method_result( + declaring_class, + &lookup_method_name, + method_args, + context, + values, + ); + } + eval_reflection_aot_method_invoke_dispatch( + declaring_class, + method_name, + object, + method_args, + context, + values, + ) +} + +/// Returns the runtime class name for an eval object used as a reflected receiver. +pub(super) fn eval_reflection_method_instance_called_class( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(object)?; + let Some(object_class_name) = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if !context.class_is_a(&object_class_name, declaring_class, false) { + eval_throw_reflection_exception( + "Given object is not an instance of the class this method was declared in", + context, + values, + )?; + return Err(EvalStatus::UncaughtThrowable); + } + Ok(object_class_name) +} + +/// Invokes one reflected generated/AOT method when it fits the bridge slice. +pub(super) fn eval_reflection_aot_method_invoke_dispatch( + declaring_class: &str, + method_name: &str, + object: RuntimeCellHandle, + method_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let member = + eval_reflection_aot_method_metadata_if_exists(declaring_class, method_name, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + if member.is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if member.is_static { + return eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { + eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + declaring_class, + method_name, + method_args, + Some(declaring_class), + Some(declaring_class), + context, + values, + ) + }); + } + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let is_instance = dynamic_object_is_a(object, declaring_class, false, context, values)? + .map_or_else(|| values.object_is_a(object, declaring_class, false), Ok)?; + if !is_instance { + eval_throw_reflection_exception( + "Given object is not an instance of the class this method was declared in", + context, + values, + )?; + return Err(EvalStatus::UncaughtThrowable); + } + let called_class = eval_reflection_object_class_name(object, context, values)?; + eval_reflection_with_declaring_class_scope(declaring_class, context, |context| { + eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object, + declaring_class, + method_name, + method_args, + Some(declaring_class), + Some(&called_class), + context, + values, + ) + }) +} + +/// Runs a reflected AOT invocation with the declaring class as visibility scope. +pub(super) fn eval_reflection_with_declaring_class_scope( + declaring_class: &str, + context: &mut ElephcEvalContext, + action: impl FnOnce(&mut ElephcEvalContext) -> Result, +) -> Result { + context.push_class_scope(declaring_class.to_string()); + context.push_called_class_scope(declaring_class.to_string()); + let result = action(context); + context.pop_called_class_scope(); + context.pop_class_scope(); + result +} diff --git a/crates/elephc-magician/src/interpreter/reflection/member_api.rs b/crates/elephc-magician/src/interpreter/reflection/member_api.rs new file mode 100644 index 0000000000..cfd41e0883 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/member_api.rs @@ -0,0 +1,558 @@ +//! Purpose: +//! Implements Reflection APIs for properties, constants, and enum cases. +//! +//! Called from: +//! - `crate::interpreter::statements` for non-callable reflected members. +//! +//! Key details: +//! - Property access, hooks, laziness, raw values, and owner-specific formatting dispatch here. + +use super::*; + +/// Handles eval-backed `ReflectionProperty` hook-inspection calls. +pub(in crate::interpreter) fn eval_reflection_property_hooks_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let Some((property_class, property)) = + eval_reflection_property_for_hooks(&declaring_class, &property_name, context) + else { + return Ok(None); + }; + match method_name.to_ascii_lowercase().as_str() { + "hashooks" => { + eval_reflection_bind_no_args(evaluated_args)?; + let has_hooks = !eval_reflection_property_hook_kinds(&property).is_empty(); + values.bool_value(has_hooks).map(Some) + } + "hashook" => { + let hook = eval_reflection_property_hook_arg(evaluated_args, context, values)?; + values + .bool_value(eval_reflection_property_has_hook(&property, hook)) + .map(Some) + } + "gethook" => { + let hook = eval_reflection_property_hook_arg(evaluated_args, context, values)?; + if !eval_reflection_property_has_hook(&property, hook) { + return values.null().map(Some); + } + eval_reflection_property_hook_method_object( + &property_class, + &property, + hook, + context, + values, + ) + .map(Some) + } + "gethooks" => { + eval_reflection_bind_no_args(evaluated_args)?; + eval_reflection_property_hook_method_array(&property_class, &property, context, values) + .map(Some) + } + _ => Ok(None), + } +} + +/// Handles eval-backed `ReflectionProperty::getValue()` calls. +pub(in crate::interpreter) fn eval_reflection_property_get_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getValue") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let object = eval_reflection_property_get_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + return eval_reflection_dynamic_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + .map(Some); + } + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if member.is_static { + return eval_reflection_static_property_value( + &declaring_class, + &property_name, + context, + values, + )? + .map(Some) + .ok_or(EvalStatus::RuntimeFatal); + } + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } + .map(Some) +} + +/// Handles eval-backed `ReflectionProperty::setValue()` calls. +pub(in crate::interpreter) fn eval_reflection_property_set_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("setValue") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let (object_or_value, value) = eval_reflection_property_set_value_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let value = value.ok_or(EvalStatus::RuntimeFatal)?; + eval_reflection_dynamic_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + return values.null().map(Some); + } + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if member.is_static { + let value = value.unwrap_or(object_or_value); + if eval_reflection_class_like_exists(&declaring_class, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(replaced) = + context.set_static_property(declaring_class, &property_name, value) + { + values.release(replaced)?; + } + } else { + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(declaring_class.as_str()); + let updated = eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.static_property_set(declaring_class, &property_name, value) + })?; + if !updated { + return Err(EvalStatus::RuntimeFatal); + } + } + return values.null().map(Some); + } + let value = value.ok_or(EvalStatus::RuntimeFatal)?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + } else { + eval_reflection_aot_instance_property_set_value( + &declaring_class, + &property_name, + object_or_value, + value, + context, + values, + )?; + } + values.null().map(Some) +} + +/// Handles `ReflectionProperty::isInitialized()` calls for eval and generated/AOT properties. +pub(in crate::interpreter) fn eval_reflection_property_is_initialized_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("isInitialized") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + let object = eval_reflection_property_get_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + return eval_reflection_dynamic_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + .and_then(|initialized| values.bool_value(initialized)) + .map(Some); + } + let Some(member) = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if member.is_static { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + let initialized = if eval_reflection_class_like_exists(declaring_class, context) { + context + .static_property(declaring_class, &property_name) + .is_some() + } else { + eval_reflection_aot_static_property_is_initialized( + declaring_class, + &property_name, + context, + values, + )? + }; + return values.bool_value(initialized).map(Some); + } + let object = object.ok_or(EvalStatus::RuntimeFatal)?; + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_is_initialized( + &declaring_class, + &property_name, + object, + context, + values, + ) + } + .and_then(|initialized| values.bool_value(initialized)) + .map(Some) +} + +/// Handles `ReflectionProperty::isLazy()` and `skipLazyInitialization()` calls. +pub(in crate::interpreter) fn eval_reflection_property_lazy_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + if method_name.eq_ignore_ascii_case("isLazy") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + return values.bool_value(false).map(Some); + } + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_property_validate_object(&declaring_class, object, context, values)?; + } else { + eval_reflection_aot_instance_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + } + return values.bool_value(false).map(Some); + } + if method_name.eq_ignore_ascii_case("skipLazyInitialization") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + return Err(EvalStatus::RuntimeFatal); + } + if eval_reflection_class_like_exists(&declaring_class, context) { + let (_, property) = eval_reflection_instance_property_target( + &declaring_class, + &property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + } else { + eval_reflection_aot_instance_property_validate_object( + &declaring_class, + object, + context, + values, + )?; + } + return values.null().map(Some); + } + Ok(None) +} + +/// Handles eval-backed `ReflectionProperty::__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_property_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + let member = eval_reflection_dynamic_property_metadata(&declaring_class); + let text = eval_reflection_property_to_string(&property_name, &member); + return values.string(&text).map(Some); + } + let member = + eval_reflection_reflected_property_metadata(&declaring_class, &property_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + let text = eval_reflection_property_to_string(&property_name, &member); + values.string(&text).map(Some) +} + +/// Handles eval-backed `ReflectionClassConstant` and enum-case `__toString()` calls. +pub(in crate::interpreter) fn eval_reflection_class_constant_to_string_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("__toString") { + return Ok(None); + } + let Some((declaring_class, constant_name, owner_kind)) = + context + .eval_reflection_class_constant(identity) + .map(|(declaring_class, constant_name, owner_kind)| { + ( + declaring_class.to_string(), + constant_name.to_string(), + owner_kind, + ) + }) + else { + return Ok(None); + }; + eval_reflection_bind_no_args(evaluated_args)?; + let text = eval_reflection_class_constant_to_string( + &declaring_class, + &constant_name, + owner_kind, + context, + values, + )?; + values.string(&text).map(Some) +} + +/// Handles `ReflectionEnumUnitCase::getEnum()` and `ReflectionEnumBackedCase::getEnum()`. +pub(in crate::interpreter) fn eval_reflection_enum_case_get_enum_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("getEnum") { + return Ok(None); + } + eval_reflection_bind_no_args(evaluated_args)?; + let Some((declaring_class, _, owner_kind)) = context + .eval_reflection_class_constant(identity) + .map(|(class, constant, owner_kind)| (class.to_string(), constant.to_string(), owner_kind)) + else { + return Ok(None); + }; + if !matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + return Ok(None); + } + eval_reflection_enum_object_result(&declaring_class, context, values).map(Some) +} + +/// Handles `ReflectionProperty::getRawValue()` and raw write calls. +pub(in crate::interpreter) fn eval_reflection_property_raw_value_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, property_name)) = + context + .eval_reflection_property(identity) + .map(|(declaring_class, property_name)| { + (declaring_class.to_string(), property_name.to_string()) + }) + else { + return Ok(None); + }; + if method_name.eq_ignore_ascii_case("getRawValue") { + let object = eval_reflection_property_raw_value_arg(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + return eval_reflection_dynamic_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + .map(Some); + } + return if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_get_raw_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } else { + eval_reflection_aot_instance_property_get_value( + &declaring_class, + &property_name, + object, + context, + values, + ) + } + .map(Some); + } + if method_name.eq_ignore_ascii_case("setRawValue") + || method_name.eq_ignore_ascii_case("setRawValueWithoutLazyInitialization") + { + let (object, value) = eval_reflection_property_set_raw_value_args(evaluated_args)?; + if context.eval_reflection_property_is_dynamic(identity) { + eval_reflection_dynamic_property_set_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + return values.null().map(Some); + } + if eval_reflection_class_like_exists(&declaring_class, context) { + eval_reflection_instance_property_set_raw_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + } else { + eval_reflection_aot_instance_property_set_value( + &declaring_class, + &property_name, + object, + value, + context, + values, + )?; + } + return values.null().map(Some); + } + Ok(None) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/member_construction.rs b/crates/elephc-magician/src/interpreter/reflection/member_construction.rs new file mode 100644 index 0000000000..94f28f77de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/member_construction.rs @@ -0,0 +1,1058 @@ +//! Purpose: +//! Constructs ReflectionMethod and ReflectionProperty owners and their AOT metadata. +//! +//! Called from: +//! - `crate::interpreter::reflection` and reflected static method dispatch. +//! +//! Key details: +//! - Eval, object, dynamic-property, native, and AOT targets converge here. +//! - Native callable defaults are converted into eval expressions without execution. + +use super::*; + +/// Handles eval-backed `ReflectionMethod::createFromMethodName()` static calls. +pub(in crate::interpreter) fn eval_reflection_method_create_from_method_name_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("ReflectionMethod") + || !method_name.eq_ignore_ascii_case("createFromMethodName") + { + return Ok(None); + } + let args = bind_evaluated_function_args(&[String::from("method")], evaluated_args)?; + let target = eval_reflection_string_arg(args[0], values)?; + let Some((class_name, method_name)) = eval_reflection_method_target_parts(&target) else { + return eval_throw_reflection_exception( + "ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name", + context, + values, + ); + }; + eval_reflection_method_object_result_or_throw( + &class_name, + &method_name, + context, + values, + ) +} + +/// Splits PHP's `ClassName::methodName` reflection-method target string. +pub(super) fn eval_reflection_method_target_parts(target: &str) -> Option<(String, String)> { + let Some((class_name, method_name)) = target.rsplit_once("::") else { + return None; + }; + if class_name.is_empty() || method_name.is_empty() { + return None; + } + Some((class_name.to_string(), method_name.to_string())) +} + +/// Extracts the deprecated one-argument `ReflectionMethod("Class::method")` target. +pub(super) fn eval_reflection_method_single_target_arg( + evaluated_args: Vec, +) -> Result { + let mut args = evaluated_args.into_iter(); + let Some(arg) = args.next() else { + return Err(EvalStatus::RuntimeFatal); + }; + if args.next().is_some() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(name) = arg.name.as_deref() { + if !matches!(name, "class_name" | "objectOrMethod") { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(arg.value) +} + +/// Builds a `ReflectionMethod` object when the reflected method exists in eval or AOT metadata. +pub(super) fn eval_reflection_method_object_result_if_exists( + class_name: &str, + requested_method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_exists(&reflected_name, context) { + if let Some(method) = eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + requested_method_name, + context, + values, + )? { + let method_name = requested_method_name.to_ascii_lowercase(); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &method_name, + &method, + context, + values, + ) + .map(Some); + } + return Ok(None); + } + let method_name = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &reflected_name, + requested_method_name, + context, + ); + let Some(method_name) = method_name else { + return Ok(None); + }; + let Some(method) = eval_reflection_method_metadata(&reflected_name, &method_name, context) + else { + return Ok(None); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &method_name, + &method, + context, + values, + ) + .map(Some) +} + +/// Builds a `ReflectionMethod` object or throws PHP's catchable reflection error. +pub(super) fn eval_reflection_method_object_result_or_throw( + class_name: &str, + requested_method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(result) = eval_reflection_method_object_result_if_exists( + class_name, + requested_method_name, + context, + values, + )? { + return Ok(Some(result)); + } + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + eval_throw_reflection_exception( + &format!( + "Method {}::{}() does not exist", + reflected_name, requested_method_name + ), + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionMethod` object when the reflected method exists in eval. +pub(super) fn eval_reflection_method_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if evaluated_args.len() == 1 { + let target = eval_reflection_method_single_target_arg(evaluated_args)?; + let target = eval_reflection_string_arg(target, values)?; + let Some((class_name, method_name)) = eval_reflection_method_target_parts(&target) else { + return eval_throw_reflection_exception( + "ReflectionMethod::__construct(): Argument #1 ($objectOrMethod) must be a valid method name", + context, + values, + ); + }; + return eval_reflection_method_object_result_or_throw( + &class_name, + &method_name, + context, + values, + ); + } + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("method_name")], + evaluated_args, + )?; + let class_name = eval_reflection_class_target_name(args[0], context, values)?; + let requested_method_name = eval_reflection_string_arg(args[1], values)?; + eval_reflection_method_object_result_or_throw( + &class_name, + &requested_method_name, + context, + values, + ) +} + +/// Builds an eval-backed `ReflectionProperty` object when the reflected property exists in eval. +pub(super) fn eval_reflection_property_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("class_name"), String::from("property_name")], + evaluated_args, + )?; + let property_name = eval_reflection_string_arg(args[1], values)?; + if values.type_tag(args[0])? == EVAL_TAG_OBJECT { + return eval_reflection_property_new_for_object(args[0], &property_name, context, values); + } + let class_name = eval_reflection_string_arg(args[0], values)?; + eval_reflection_property_object_result_or_throw(&class_name, &property_name, context, values) +} + +/// Builds a `ReflectionProperty` object when the reflected property exists. +pub(super) fn eval_reflection_property_object_result_if_exists( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_exists(&reflected_name, context) { + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + &reflected_name, + property_name, + context, + ) + { + let property = eval_reflection_interface_property_metadata(declaring_class, &property); + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); + } + let Some(property) = eval_reflection_aot_property_metadata_if_exists( + &reflected_name, + property_name, + context, + values, + )? + else { + return Ok(None); + }; + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); + } + let Some(property) = eval_reflection_property_metadata(&reflected_name, property_name, context) + else { + return Ok(None); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some) +} + +/// Builds a `ReflectionProperty` object or throws PHP's catchable reflection error. +pub(super) fn eval_reflection_property_object_result_or_throw( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(result) = eval_reflection_property_object_result_if_exists( + class_name, + property_name, + context, + values, + )? { + return Ok(Some(result)); + } + let reflected_name = context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if !eval_reflection_class_like_or_runtime_exists(&reflected_name, context, values)? { + return eval_throw_reflection_exception( + &format!("Class \"{}\" does not exist", reflected_name), + context, + values, + ); + } + let message = eval_reflection_missing_member_message( + EVAL_REFLECTION_OWNER_PROPERTY, + &reflected_name, + property_name, + ); + eval_throw_reflection_exception(&message, context, values) +} + +/// Builds a ReflectionProperty from an object argument, including dynamic properties. +pub(super) fn eval_reflection_property_new_for_object( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = eval_reflection_object_class_name(object, context, values)?; + if let Some(property) = eval_reflection_property_metadata(&class_name, property_name, context) { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some); + } + if !eval_reflection_object_dynamic_property_exists(object, property_name, values)? { + let message = eval_reflection_missing_member_message( + EVAL_REFLECTION_OWNER_PROPERTY, + &class_name, + property_name, + ); + return eval_throw_reflection_exception(&message, context, values); + } + let property = eval_reflection_dynamic_property_metadata(&class_name); + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + property_name, + &property, + context, + values, + ) + .map(Some) +} + +/// Returns the class name for an object passed to a Reflection constructor. +pub(super) fn eval_reflection_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + if let Some(class_name) = context.dynamic_object_class_name(identity) { + return Ok(class_name); + } + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + let class_name = String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(class_name.trim_start_matches('\\').to_string()) +} + +/// Returns whether one object has a public dynamic property by exact PHP name. +pub(super) fn eval_reflection_object_dynamic_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + if property_name.contains('\0') { + return Ok(false); + } + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} + +/// Returns the object captured by a `ReflectionObject` instance, when present. +pub(super) fn eval_reflection_object_reflected_object( + reflection_object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !eval_reflection_object_has_class(reflection_object, "ReflectionObject", values)? { + return Ok(None); + } + let object = eval_reflection_with_declaring_class_scope("ReflectionObject", context, |_| { + values.property_get(reflection_object, "__object") + })?; + if values.type_tag(object)? == EVAL_TAG_OBJECT { + Ok(Some(object)) + } else { + Ok(None) + } +} + +/// Appends dynamic public properties to `ReflectionObject::getProperties()` results. +pub(super) fn eval_reflection_object_dynamic_property_array_result( + reflection_object: RuntimeCellHandle, + owner_kind: u64, + reflected_name: &str, + filter: Option, + mut result: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if owner_kind != EVAL_REFLECTION_OWNER_PROPERTY { + return Ok(result); + } + let Some(object) = eval_reflection_object_reflected_object(reflection_object, context, values)? + else { + return Ok(result); + }; + let property_names = + eval_reflection_object_dynamic_property_names(object, reflected_name, context, values); + values.release(object)?; + let property_names = property_names?; + for name in property_names { + let member = eval_reflection_dynamic_property_metadata(reflected_name); + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } + let member_object = eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_PROPERTY, + &name, + &member, + context, + values, + )?; + let next_index = values.array_len(result)? as i64; + let key = values.int(next_index)?; + result = values.array_set(result, key, member_object)?; + } + Ok(result) +} + +/// Enumerates public dynamic property names on the object behind `ReflectionObject`. +pub(super) fn eval_reflection_object_dynamic_property_names( + object: RuntimeCellHandle, + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut names = Vec::new(); + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + let property_name = + String::from_utf8(key_bytes?).map_err(|_| EvalStatus::RuntimeFatal)?; + if !eval_reflection_dynamic_property_name_is_visible( + reflected_name, + &property_name, + context, + ) { + continue; + } + if !names.iter().any(|name| name == &property_name) { + names.push(property_name); + } + } + Ok(names) +} + +/// Returns true when an object-storage name represents a public dynamic property. +pub(super) fn eval_reflection_dynamic_property_name_is_visible( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> bool { + !property_name.contains('\0') + && eval_reflection_member_name( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_name, + context, + ) + .is_none() +} + +/// Builds PHP reflection metadata for a public dynamic object property. +pub(super) fn eval_reflection_dynamic_property_metadata(class_name: &str) -> EvalReflectionMemberMetadata { + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_file: None, + source_location: None, + attributes: Vec::new(), + visibility: EvalVisibility::Public, + is_static: false, + is_final: false, + is_abstract: false, + is_readonly: false, + is_promoted: false, + is_dynamic: true, + modifiers: eval_reflection_property_modifiers( + EvalVisibility::Public, + None, + false, + false, + false, + false, + false, + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata: None, + default_value: None, + default_value_trait_origin: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} + +/// Returns generated AOT ReflectionMethod metadata when the runtime table has a matching row. +pub(super) fn eval_reflection_aot_method_metadata_if_exists( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { + return Ok(None); + }; + let declaring_class_name = values + .reflection_method_declaring_class(runtime_class_name, method_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + Ok(Some(eval_reflection_aot_method_metadata( + &declaring_class_name, + method_name, + flags, + Vec::new(), + None, + None, + None, + ))) +} + +/// Returns generated AOT ReflectionMethod metadata with registered signature details. +pub(super) fn eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_method_flags(runtime_class_name, method_name)? else { + return Ok(None); + }; + let declaring_class_name = values + .reflection_method_declaring_class(runtime_class_name, method_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + let mut signature = + eval_reflection_aot_method_signature(&declaring_class_name, method_name, flags, context); + if signature.is_none() && declaring_class_name != runtime_class_name { + signature = + eval_reflection_aot_method_signature(runtime_class_name, method_name, flags, context); + } + let attributes = eval_reflection_aot_method_attributes( + runtime_class_name, + &declaring_class_name, + method_name, + context, + ); + let (source_file, source_location) = + eval_reflection_aot_method_source_metadata(flags, values)?; + Ok(Some(eval_reflection_aot_method_metadata( + &declaring_class_name, + method_name, + flags, + attributes, + source_file, + source_location, + signature.as_ref(), + ))) +} + +/// Returns AOT source-file and line metadata encoded in ReflectionMethod flags. +pub(super) fn eval_reflection_aot_method_source_metadata( + flags: u64, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, Option), EvalStatus> { + let Some(source_location) = eval_reflection_aot_method_source_location_from_flags(flags) else { + return Ok((None, None)); + }; + let Some(source_file) = values.reflection_source_file()? else { + return Ok((None, None)); + }; + Ok((Some(source_file), Some(source_location))) +} + +/// Decodes AOT ReflectionMethod source lines packed into high flag bits. +pub(super) fn eval_reflection_aot_method_source_location_from_flags( + flags: u64, +) -> Option { + let start_line = + ((flags >> EVAL_REFLECTION_METHOD_SOURCE_START_SHIFT) + & EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK) as i64; + let end_line = + ((flags >> EVAL_REFLECTION_METHOD_SOURCE_END_SHIFT) + & EVAL_REFLECTION_METHOD_SOURCE_LINE_MASK) as i64; + (start_line > 0 && end_line >= start_line) + .then(|| EvalSourceLocation::new(start_line, end_line)) +} + +/// Returns generated/AOT method dispatch metadata for interpreter-only runtime decisions. +pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + Ok( + eval_reflection_aot_method_metadata_if_exists(class_name, method_name, values)?.map( + |member| { + ( + member + .declaring_class_name + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()), + member.visibility, + member.is_static, + member.is_abstract, + ) + }, + ), + ) +} + +/// Converts AOT method flag metadata into the eval ReflectionMethod shape. +pub(super) fn eval_reflection_aot_method_metadata( + class_name: &str, + method_name: &str, + flags: u64, + attributes: Vec, + source_file: Option, + source_location: Option, + signature: Option<&NativeCallableSignature>, +) -> EvalReflectionMemberMetadata { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + let required_parameter_count = + signature.map_or(0, NativeCallableSignature::required_param_count); + let parameters = signature.map_or_else(Vec::new, |signature| { + eval_reflection_native_callable_parameters(class_name, method_name, flags, signature) + }); + let return_type_metadata = signature + .and_then(NativeCallableSignature::return_type) + .and_then(eval_reflection_parameter_type_metadata); + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_file, + source_location, + attributes, + visibility, + is_static: flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, + is_final: flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0, + is_abstract: flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers_from_flags(flags), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + } +} + +/// Returns registered generated/AOT method attributes for one reflected method. +pub(super) fn eval_reflection_aot_method_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_method_attributes(declaring_class_name, method_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_method_attributes(runtime_class_name, method_name) +} + +/// Selects the registered native signature for an AOT method-like member. +pub(super) fn eval_reflection_aot_method_signature( + class_name: &str, + method_name: &str, + flags: u64, + context: &ElephcEvalContext, +) -> Option { + if method_name.eq_ignore_ascii_case("__construct") { + return context.native_constructor_signature(class_name); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0 { + context.native_static_method_signature(class_name, method_name) + } else { + context.native_method_signature(class_name, method_name) + } +} + +/// Builds ReflectionParameter metadata for one registered native AOT signature. +pub(super) fn eval_reflection_native_callable_parameters( + declaring_class_name: &str, + method_name: &str, + flags: u64, + signature: &NativeCallableSignature, +) -> Vec { + let names = eval_reflection_native_callable_parameter_names(signature); + let parameter_count = names.len(); + let parameter_types = eval_reflection_native_callable_parameter_types(signature); + let has_type_flags = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); + let parameter_attributes = vec![Vec::new(); parameter_count]; + let defaults = eval_reflection_native_callable_parameter_defaults(signature); + let by_ref_flags = (0..parameter_count) + .map(|index| signature.param_by_ref(index)) + .collect::>(); + let variadic_flags = (0..parameter_count) + .map(|index| signature.param_variadic(index)) + .collect::>(); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method_name.to_ascii_lowercase(), + declaring_class_name: Some(declaring_class_name.trim_start_matches('\\').to_string()), + magic_scope: None, + attributes: Vec::new(), + flags, + required_parameter_count: signature.required_param_count(), + }; + eval_reflection_parameters_from_names_and_type_flags( + Some(declaring_class_name.trim_start_matches('\\')), + Some(&declaring_function), + &names, + &has_type_flags, + ¶meter_types, + ¶meter_attributes, + &defaults, + &by_ref_flags, + &variadic_flags, + &[], + ) +} + +/// Returns declared parameter type metadata for a registered native callable. +pub(super) fn eval_reflection_native_callable_parameter_types( + signature: &NativeCallableSignature, +) -> Vec> { + (0..signature.param_count()) + .map(|index| signature.param_type(index).cloned()) + .collect() +} + +/// Returns parameter names for a registered native callable, filling missing bridge names. +pub(super) fn eval_reflection_native_callable_parameter_names( + signature: &NativeCallableSignature, +) -> Vec { + (0..signature.param_count()) + .map(|index| { + signature + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", index)) + }) + .collect() +} + +/// Converts registered scalar native defaults into eval constant expressions. +pub(super) fn eval_reflection_native_callable_parameter_defaults( + signature: &NativeCallableSignature, +) -> Vec> { + (0..signature.param_count()) + .map(|index| { + signature + .param_default(index) + .map(eval_reflection_native_callable_default_expr) + }) + .collect() +} + +/// Converts one registered native default into an eval constant expression. +pub(super) fn eval_reflection_native_callable_default_expr(default: &NativeCallableDefault) -> EvalExpr { + match default { + NativeCallableDefault::Null => EvalExpr::Const(EvalConst::Null), + NativeCallableDefault::Bool(value) => EvalExpr::Const(EvalConst::Bool(*value)), + NativeCallableDefault::Int(value) => EvalExpr::Const(EvalConst::Int(*value)), + NativeCallableDefault::Float(value) => EvalExpr::Const(EvalConst::Float(*value)), + NativeCallableDefault::String(value) => EvalExpr::Const(EvalConst::String(value.clone())), + NativeCallableDefault::EmptyArray => EvalExpr::Array(Vec::new()), + NativeCallableDefault::Array(elements) => EvalExpr::Array( + elements + .iter() + .map(eval_reflection_native_callable_default_array_element) + .collect(), + ), + NativeCallableDefault::Object { class_name, args } => EvalExpr::NewObject { + class_name: class_name.clone(), + args: args + .iter() + .map(eval_reflection_native_callable_default_arg) + .collect(), + }, + } +} + +/// Converts one native array-default element into an eval array literal element. +pub(super) fn eval_reflection_native_callable_default_array_element( + element: &NativeCallableArrayDefaultElement, +) -> EvalArrayElement { + let value = eval_reflection_native_callable_default_expr(&element.value); + match &element.key { + Some(NativeCallableArrayDefaultKey::Int(key)) => EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::Int(*key)), + value, + }, + Some(NativeCallableArrayDefaultKey::String(key)) => EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String(key.clone())), + value, + }, + None => EvalArrayElement::Value(value), + } +} + +/// Converts one native object-default constructor argument into an eval call arg. +pub(super) fn eval_reflection_native_callable_default_arg(arg: &NativeCallableObjectDefaultArg) -> EvalCallArg { + let value = eval_reflection_native_callable_default_expr(&arg.value); + match &arg.name { + Some(name) => EvalCallArg::named(name, value), + None => EvalCallArg::positional(value), + } +} + +/// Returns generated AOT ReflectionProperty metadata when the runtime table has a matching row. +pub(super) fn eval_reflection_aot_property_metadata_if_exists( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { + return Ok(None); + }; + let declaring_class_name = values + .reflection_property_declaring_class(runtime_class_name, property_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + let type_metadata = eval_reflection_aot_property_type_metadata( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); + let default_value = eval_reflection_aot_property_default_value( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); + let attributes = eval_reflection_aot_property_attributes( + runtime_class_name, + &declaring_class_name, + property_name, + context, + ); + Ok(Some(eval_reflection_aot_property_metadata( + &declaring_class_name, + flags, + attributes, + type_metadata, + default_value, + ))) +} + +/// Returns generated/AOT property access metadata for an exact class/property pair. +pub(in crate::interpreter) fn eval_reflection_aot_property_access_metadata( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(runtime_class_name, property_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_property_declaring_class(runtime_class_name, property_name)? + .unwrap_or_else(|| runtime_class_name.to_string()); + Ok(Some(eval_reflection_aot_property_access_metadata_from_flags( + declaring_class, + flags, + ))) +} + +/// Returns generated/AOT static property metadata from a class or native parent chain. +pub(in crate::interpreter) fn eval_reflection_aot_static_property_access_metadata( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return Ok(None); + } + if let Some(metadata) = + eval_reflection_aot_property_access_metadata(¤t, property_name, values)? + { + return Ok(Some(metadata)); + } + let Some(parent) = context.native_class_parent(¤t) else { + return Ok(None); + }; + current = parent.to_string(); + } +} + +/// Converts AOT ReflectionProperty flags into access-check metadata. +pub(super) fn eval_reflection_aot_property_access_metadata_from_flags( + declaring_class: String, + flags: u64, +) -> (String, EvalVisibility, EvalVisibility, bool) { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + let write_visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + EvalVisibility::Protected + } else { + visibility + }; + ( + declaring_class, + visibility, + write_visibility, + flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0, + ) +} + +/// Returns registered generated/AOT property type metadata for one reflected property. +pub(super) fn eval_reflection_aot_property_type_metadata( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + context + .native_property_type(declaring_class_name, property_name) + .or_else(|| context.native_property_type(runtime_class_name, property_name)) + .as_ref() + .and_then(eval_reflection_parameter_type_metadata) +} + +/// Returns registered generated/AOT property default metadata for one reflected property. +pub(super) fn eval_reflection_aot_property_default_value( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + context + .native_property_default(declaring_class_name, property_name) + .or_else(|| context.native_property_default(runtime_class_name, property_name)) + .as_ref() + .map(eval_reflection_native_callable_default_expr) +} + +/// Returns registered generated/AOT property attributes for one reflected property. +pub(super) fn eval_reflection_aot_property_attributes( + runtime_class_name: &str, + declaring_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Vec { + let attributes = context.native_property_attributes(declaring_class_name, property_name); + if !attributes.is_empty() || declaring_class_name == runtime_class_name { + return attributes; + } + context.native_property_attributes(runtime_class_name, property_name) +} + +/// Converts AOT property flag metadata into the eval ReflectionProperty shape. +pub(super) fn eval_reflection_aot_property_metadata( + class_name: &str, + flags: u64, + attributes: Vec, + type_metadata: Option, + default_value: Option, +) -> EvalReflectionMemberMetadata { + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let is_final = flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0; + let is_abstract = flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT != 0; + let is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; + let is_virtual = flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL != 0; + let mut modifiers = eval_reflection_property_modifiers( + visibility, + None, + is_static, + is_final, + is_abstract, + is_readonly, + is_virtual, + ); + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + modifiers |= 32 | 4096; + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + modifiers |= 2048; + } + let settable_type_metadata = type_metadata.clone(); + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.trim_start_matches('\\').to_string()), + source_file: None, + source_location: None, + attributes, + visibility, + is_static, + is_final, + is_abstract, + is_readonly, + is_promoted: flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED != 0, + is_dynamic: false, + modifiers, + type_metadata, + settable_type_metadata, + return_type_metadata: None, + default_value, + default_value_trait_origin: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/member_metadata.rs b/crates/elephc-magician/src/interpreter/reflection/member_metadata.rs new file mode 100644 index 0000000000..1a15641eb5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/member_metadata.rs @@ -0,0 +1,637 @@ +//! Purpose: +//! Builds reflected method and property metadata for eval and native class-like targets. +//! +//! Called from: +//! - Reflection owner construction, class member APIs, and property access. +//! +//! Key details: +//! - Trait composition, enum synthetic methods, defaults, and visibility are resolved here. + +use super::*; + +/// Returns method metadata for a method-like member on an eval class-like symbol. +pub(super) fn eval_reflection_method_metadata( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option { + if context.has_class(class_name) || context.has_enum(class_name) { + if let Some((declaring_class, method)) = context.class_method(class_name, method_name) { + let required_parameter_count = eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ); + let mut flags = eval_reflection_member_flags( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + false, + ); + flags |= eval_reflection_callable_flags(method.attributes()); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(declaring_class.clone()), + magic_scope: Some(eval_reflection_eval_method_parameter_magic_scope( + &declaring_class, + &method, + None, + )), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let promoted_parameter_names = eval_reflection_promoted_parameter_names( + &declaring_class, + method.name(), + context, + ); + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(declaring_class.as_str()), + Some(&declaring_function), + method.params(), + method.parameter_has_types(), + method.parameter_types(), + method.parameter_attributes(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + &promoted_parameter_names, + ); + return Some(EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + source_file: None, + source_location: method.source_location(), + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + }); + } + return eval_reflection_enum_synthetic_method_metadata(class_name, method_name, context); + } + if context.has_interface(class_name) { + return context + .interface_method_requirements(class_name) + .into_iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| { + let required_parameter_count = eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ); + let mut flags = eval_reflection_member_flags( + EvalVisibility::Public, + method.is_static(), + false, + true, + false, + ); + flags |= eval_reflection_callable_flags(method.attributes()); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(class_name.to_string()), + magic_scope: Some(eval_reflection_method_parameter_magic_scope( + class_name, + method.name(), + &format!("{}::{}", class_name.trim_start_matches('\\'), method.name()), + None, + )), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(class_name), + Some(&declaring_function), + method.params(), + method.parameter_has_types(), + method.parameter_types(), + method.parameter_attributes(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + &[], + ); + EvalReflectionMemberMetadata { + declaring_class_name: Some(class_name.to_string()), + source_file: None, + source_location: method.source_location(), + attributes: method.attributes().to_vec(), + visibility: EvalVisibility::Public, + is_static: method.is_static(), + is_final: false, + is_abstract: true, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( + EvalVisibility::Public, + method.is_static(), + false, + true, + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + } + }); + } + context.trait_decl(class_name).and_then(|trait_decl| { + trait_decl + .methods() + .iter() + .find(|method| method.name().eq_ignore_ascii_case(method_name)) + .map(|method| { + let required_parameter_count = eval_reflection_required_parameter_count( + method.parameter_defaults(), + method.parameter_is_variadic(), + ); + let mut flags = eval_reflection_member_flags( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + false, + ); + flags |= eval_reflection_callable_flags(method.attributes()); + let return_type_metadata = method + .return_type() + .and_then(eval_reflection_parameter_type_metadata); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: method.name().to_string(), + declaring_class_name: Some(trait_decl.name().to_string()), + magic_scope: Some(eval_reflection_eval_method_parameter_magic_scope( + trait_decl.name(), + method, + Some(trait_decl.name()), + )), + attributes: method.attributes().to_vec(), + flags, + required_parameter_count, + }; + let promoted_parameter_names = + eval_reflection_promoted_trait_parameter_names(trait_decl, method.name()); + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(trait_decl.name()), + Some(&declaring_function), + method.params(), + method.parameter_has_types(), + method.parameter_types(), + method.parameter_attributes(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + &promoted_parameter_names, + ); + EvalReflectionMemberMetadata { + declaring_class_name: Some(trait_decl.name().to_string()), + source_file: None, + source_location: method.source_location(), + attributes: method.attributes().to_vec(), + visibility: method.visibility(), + is_static: method.is_static(), + is_final: method.is_final(), + is_abstract: method.is_abstract(), + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( + method.visibility(), + method.is_static(), + method.is_final(), + method.is_abstract(), + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + } + }) + }) +} + +/// Builds ReflectionMethod metadata for PHP's enum-provided synthetic methods. +pub(super) fn eval_reflection_enum_synthetic_method_metadata( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option { + let synthetic_name = eval_enum_static_builtin_applies(class_name, method_name, context)?; + let enum_decl = context.enum_decl(class_name)?; + let declaring_class_name = enum_decl.name().trim_start_matches('\\').to_string(); + let flags = eval_reflection_member_flags(EvalVisibility::Public, true, false, false, false); + let (parameter_names, parameter_types, return_type_metadata) = match synthetic_name { + "cases" => ( + Vec::new(), + Vec::new(), + Some(eval_reflection_parameter_type_metadata(&EvalParameterType::new( + vec![EvalParameterTypeVariant::Array], + false, + ))?), + ), + "from" | "tryFrom" => { + let return_type = EvalParameterType::new( + vec![EvalParameterTypeVariant::Class(String::from("static"))], + synthetic_name == "tryFrom", + ); + ( + vec![String::from("value")], + vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String, EvalParameterTypeVariant::Int], + false, + ))], + Some(eval_reflection_parameter_type_metadata(&return_type)?), + ) + } + _ => return None, + }; + let parameter_count = parameter_names.len(); + let parameter_has_types = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); + let parameter_attributes = vec![Vec::new(); parameter_count]; + let parameter_defaults = vec![None; parameter_count]; + let parameter_is_by_ref = vec![false; parameter_count]; + let parameter_is_variadic = vec![false; parameter_count]; + let required_parameter_count = + eval_reflection_required_parameter_count(¶meter_defaults, ¶meter_is_variadic); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: synthetic_name.to_string(), + declaring_class_name: Some(declaring_class_name.clone()), + magic_scope: None, + attributes: Vec::new(), + flags, + required_parameter_count, + }; + let parameters = eval_reflection_parameters_from_names_and_type_flags( + Some(&declaring_class_name), + Some(&declaring_function), + ¶meter_names, + ¶meter_has_types, + ¶meter_types, + ¶meter_attributes, + ¶meter_defaults, + ¶meter_is_by_ref, + ¶meter_is_variadic, + &[], + ); + Some(EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class_name), + source_file: None, + source_location: None, + attributes: Vec::new(), + visibility: EvalVisibility::Public, + is_static: true, + is_final: false, + is_abstract: false, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers(EvalVisibility::Public, true, false, false), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata, + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + }) +} + +/// Returns property metadata for a property-like member on an eval class-like symbol. +pub(super) fn eval_reflection_property_metadata( + class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option { + if context.has_class(class_name) || context.has_enum(class_name) { + return context.class_property(class_name, property_name).map( + |(declaring_class, property)| { + let default_value = eval_reflection_property_default_value(&property); + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + source_file: None, + source_location: None, + attributes: property.attributes().to_vec(), + visibility: property.visibility(), + is_static: property.is_static(), + is_final: property.is_final(), + is_abstract: property.is_abstract(), + is_readonly: property.is_readonly(), + is_promoted: property.is_promoted(), + is_dynamic: false, + modifiers: eval_reflection_property_modifiers( + property.visibility(), + property.set_visibility(), + property.is_static(), + property.is_final(), + property.is_abstract(), + property.is_readonly(), + eval_reflection_property_is_virtual(&property), + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .settable_type() + .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, + default_value, + default_value_trait_origin: property.trait_origin().map(str::to_string), + required_parameter_count: 0, + parameters: Vec::new(), + } + }, + ); + } + if context.has_interface(class_name) { + return context + .interface_property_requirements(class_name) + .into_iter() + .find(|property| property.name() == property_name) + .map(|property| { + eval_reflection_interface_property_metadata(class_name.to_string(), &property) + }); + } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement(class_name, property_name, context) + { + return Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + )); + } + context.trait_decl(class_name).and_then(|trait_decl| { + trait_decl + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| { + let default_value = eval_reflection_property_default_value(property); + EvalReflectionMemberMetadata { + declaring_class_name: Some(trait_decl.name().to_string()), + source_file: None, + source_location: None, + attributes: property.attributes().to_vec(), + visibility: property.visibility(), + is_static: property.is_static(), + is_final: property.is_final(), + is_abstract: property.is_abstract(), + is_readonly: property.is_readonly(), + is_promoted: property.is_promoted(), + is_dynamic: false, + modifiers: eval_reflection_property_modifiers( + property.visibility(), + property.set_visibility(), + property.is_static(), + property.is_final(), + property.is_abstract(), + property.is_readonly(), + eval_reflection_property_is_virtual(property), + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .settable_type() + .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, + default_value, + default_value_trait_origin: property + .trait_origin() + .map(str::to_string) + .or_else(|| Some(trait_decl.name().to_string())), + required_parameter_count: 0, + parameters: Vec::new(), + } + }) + }) +} + +/// Returns property metadata for a property contract declared on an interface. +pub(super) fn eval_reflection_interface_property_metadata( + declaring_class: String, + property: &EvalInterfaceProperty, +) -> EvalReflectionMemberMetadata { + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class), + source_file: None, + source_location: None, + attributes: property.attributes().to_vec(), + visibility: EvalVisibility::Public, + is_static: false, + is_final: false, + is_abstract: true, + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_property_modifiers( + EvalVisibility::Public, + property.set_visibility(), + false, + false, + true, + false, + true, + ), + type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + settable_type_metadata: property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + return_type_metadata: None, + default_value: None, + default_value_trait_origin: None, + required_parameter_count: 0, + parameters: Vec::new(), + } +} + +/// Returns property names that can contribute to `ReflectionClass::getDefaultProperties()`. +pub(super) fn eval_reflection_default_property_names( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if context.has_class(reflected_name) + || context.has_enum(reflected_name) + || context.has_trait(reflected_name) + || context.has_interface(reflected_name) + { + return Ok(eval_reflection_eval_property_names(reflected_name, context)); + } + eval_reflection_aot_member_names(EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, values) +} + +/// Returns eval or generated/AOT property metadata for default-property materialization. +pub(super) fn eval_reflection_default_property_metadata( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(member) = eval_reflection_property_metadata(reflected_name, property_name, context) { + return Ok(Some(member)); + } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + reflected_name, + property_name, + context, + ) + { + return Ok(Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + ))); + } + eval_reflection_aot_property_metadata_if_exists(reflected_name, property_name, context, values) +} + +/// Returns eval or generated/AOT metadata for a materialized `ReflectionProperty`. +pub(super) fn eval_reflection_reflected_property_metadata( + declaring_class: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if let Some(member) = eval_reflection_property_metadata(declaring_class, property_name, context) { + return Ok(Some(member)); + } + if let Some((declaring_class, property)) = + eval_reflection_native_interface_property_requirement( + declaring_class, + property_name, + context, + ) + { + return Ok(Some(eval_reflection_interface_property_metadata( + declaring_class, + &property, + ))); + } + eval_reflection_aot_property_metadata_if_exists(declaring_class, property_name, context, values) +} + +/// Returns eval-declared property names for reflection APIs that do not use AOT lists. +pub(super) fn eval_reflection_eval_property_names( + reflected_name: &str, + context: &ElephcEvalContext, +) -> Vec { + if context.has_trait(reflected_name) { + return context.trait_property_names(reflected_name); + } + if context.has_interface(reflected_name) { + return context.interface_property_names(reflected_name); + } + context.class_property_names(reflected_name) +} + +/// Returns property names that can contribute to `ReflectionClass::getStaticProperties()`. +pub(super) fn eval_reflection_static_property_names( + reflected_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if eval_reflection_class_like_exists(reflected_name, context) { + return Ok(eval_reflection_eval_property_names(reflected_name, context) + .into_iter() + .filter(|name| { + eval_reflection_property_metadata(reflected_name, name, context) + .is_some_and(|property| property.is_static) + }) + .collect()); + } + let names = + eval_reflection_aot_member_names(EVAL_REFLECTION_OWNER_PROPERTY, reflected_name, values)?; + let mut result = Vec::new(); + for name in names { + if eval_reflection_aot_property_metadata_if_exists(reflected_name, &name, context, values)? + .is_some_and(|property| property.is_static) + { + result.push(name); + } + } + Ok(result) +} + +/// Returns eval or generated/AOT property metadata for static-property reflection. +pub(super) fn eval_reflection_static_property_metadata( + reflected_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_reflection_reflected_property_metadata(reflected_name, property_name, context, values) +} + +/// Returns the current eval or generated/AOT static property value. +pub(super) fn eval_reflection_static_property_value( + reflected_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(member) = + eval_reflection_static_property_metadata(reflected_name, property_name, context, values)? + else { + return Ok(None); + }; + if !member.is_static { + return Ok(None); + } + if eval_reflection_class_like_exists(reflected_name, context) { + let declaring_class = member + .declaring_class_name + .as_deref() + .ok_or(EvalStatus::RuntimeFatal)?; + if let Some(value) = context.static_property(declaring_class, property_name) { + return Ok(Some(value)); + } + return member + .default_value + .as_ref() + .map(|default| eval_reflection_member_default_value(&member, default, context, values)) + .transpose(); + } + let declaring_class = member + .declaring_class_name + .as_deref() + .unwrap_or(reflected_name); + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.static_property_get(reflected_name, property_name) + }) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/owner_materialization.rs b/crates/elephc-magician/src/interpreter/reflection/owner_materialization.rs new file mode 100644 index 0000000000..b19efdccd6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/owner_materialization.rs @@ -0,0 +1,1139 @@ +//! Purpose: +//! Materializes common Reflection owner fields and nested member/type objects. +//! +//! Called from: +//! - Reflection owner-specific constructors after resolving metadata. +//! +//! Key details: +//! - Temporary arrays and object handles are transferred through runtime ownership APIs. +//! - Full and shallow class owners avoid recursive metadata expansion. + +use super::*; + +/// Materializes one Reflection owner object and transfers the temporary attribute array. +pub(super) fn eval_reflection_owner_object( + owner_kind: u64, + reflected_name: &str, + attributes: &[EvalAttribute], + interface_names: &[String], + trait_names: &[String], + method_names: &[String], + property_names: &[String], + parent_class_name: Option<&str>, + parameter_metadata: &[EvalReflectionParameterMetadata], + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + settable_type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, + default_value_trait_origin: Option<&str>, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: Option, + backing_value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_owner_object_with_members( + owner_kind, + reflected_name, + attributes, + interface_names, + trait_names, + method_names, + property_names, + parent_class_name, + parameter_metadata, + type_metadata, + settable_type_metadata, + default_value, + default_value_trait_origin, + flags, + modifiers, + method_modifiers, + constant_value, + backing_value, + true, + context, + values, + ) +} + +/// Materializes one Reflection owner object with optional nested class member objects. +pub(super) fn eval_reflection_owner_object_with_members( + owner_kind: u64, + reflected_name: &str, + attributes: &[EvalAttribute], + interface_names: &[String], + trait_names: &[String], + method_names: &[String], + property_names: &[String], + parent_class_name: Option<&str>, + parameter_metadata: &[EvalReflectionParameterMetadata], + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + settable_type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, + default_value_trait_origin: Option<&str>, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: Option, + backing_value: Option, + include_class_members: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = eval_reflection_attribute_array_result( + attributes, + eval_reflection_attribute_target(owner_kind), + context, + values, + )?; + let interface_names_array = eval_reflection_string_array_result(interface_names, values)?; + let trait_names_array = eval_reflection_string_array_result(trait_names, values)?; + let method_names_array = eval_reflection_string_array_result(method_names, values)?; + let property_names_array = eval_reflection_string_array_result(property_names, values)?; + let class_metadata_owner = eval_reflection_owner_uses_class_metadata(owner_kind); + let is_eval_class = class_metadata_owner + && eval_reflection_class_like_exists(reflected_name, context); + let method_objects = if class_metadata_owner && include_class_members { + if is_eval_class { + eval_reflection_member_object_array_result( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + method_names, + None, + context, + values, + )? + } else { + eval_reflection_aot_member_object_array_result( + EVAL_REFLECTION_OWNER_METHOD, + reflected_name, + method_names, + None, + context, + values, + )? + } + } else if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION + ) { + eval_reflection_parameter_object_array_result(parameter_metadata, context, values)? + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + match type_metadata { + Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, + None => values.null()?, + } + } else { + values.array_new(0)? + }; + let property_objects = if class_metadata_owner && include_class_members { + if is_eval_class { + eval_reflection_member_object_array_result( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_names, + None, + context, + values, + )? + } else { + eval_reflection_aot_member_object_array_result( + EVAL_REFLECTION_OWNER_PROPERTY, + reflected_name, + property_names, + None, + context, + values, + )? + } + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + match default_value { + Some(default) => eval_reflection_class_like_default_value( + parent_class_name, + default_value_trait_origin, + default, + context, + values, + )?, + None => values.null()?, + } + } else { + values.array_new(0)? + }; + let parent_class = eval_reflection_related_class_result( + owner_kind, + parent_class_name, + include_class_members, + context, + values, + )?; + let constructor = eval_reflection_constructor_object_result( + owner_kind, + reflected_name, + include_class_members, + context, + values, + )?; + let (constant_value_cell, release_constant_value) = if owner_kind + == EVAL_REFLECTION_OWNER_PROPERTY + { + match settable_type_metadata { + Some(type_metadata) => ( + eval_reflection_type_object_result(type_metadata, values)?, + true, + ), + None => (values.null()?, true), + } + } else { + match constant_value { + Some(value) => (value, false), + None => (values.null()?, true), + } + }; + let (backing_value_cell, release_backing_value) = match backing_value { + Some(value) => (value, false), + None => (values.null()?, true), + }; + let object = values.reflection_owner_new( + owner_kind, + reflected_name, + attrs, + interface_names_array, + trait_names_array, + method_names_array, + property_names_array, + method_objects, + property_objects, + parent_class, + flags, + modifiers, + method_modifiers, + constant_value_cell, + backing_value_cell, + constructor, + )?; + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM + ) { + let identity = values.object_identity(object)?; + context.register_eval_reflection_class(identity, reflected_name); + } else if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + if let Some(declaring_class) = parent_class_name { + let identity = values.object_identity(object)?; + context.register_eval_reflection_method(identity, declaring_class, reflected_name); + } + } else if owner_kind == EVAL_REFLECTION_OWNER_FUNCTION { + let identity = values.object_identity(object)?; + context.register_eval_reflection_function(identity, reflected_name); + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + if let Some(declaring_class) = parent_class_name { + if flags & EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC != 0 { + let identity = values.object_identity(object)?; + context.register_eval_dynamic_reflection_property( + identity, + declaring_class, + reflected_name, + ); + } else { + let identity = values.object_identity(object)?; + context.register_eval_reflection_property( + identity, + declaring_class, + reflected_name, + ); + } + } + } else if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + if let Some(declaring_class) = parent_class_name { + let identity = values.object_identity(object)?; + context.register_eval_reflection_class_constant( + identity, + declaring_class, + reflected_name, + owner_kind, + ); + } + } + values.release(attrs)?; + values.release(interface_names_array)?; + values.release(trait_names_array)?; + values.release(method_names_array)?; + values.release(property_names_array)?; + values.release(method_objects)?; + values.release(property_objects)?; + values.release(parent_class)?; + values.release(constructor)?; + if release_constant_value { + values.release(constant_value_cell)?; + } + if release_backing_value { + values.release(backing_value_cell)?; + } + Ok(object) +} + +/// Builds the `ReflectionClass|false` value stored in parent or declaring-class slots. +pub(super) fn eval_reflection_related_class_result( + owner_kind: u64, + related_class_name: Option<&str>, + include_class_members: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(related_class_name) = related_class_name else { + return values.bool_value(false); + }; + if eval_reflection_owner_uses_class_metadata(owner_kind) && include_class_members { + return eval_reflection_full_class_object_result(related_class_name, context, values); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD + | EVAL_REFLECTION_OWNER_PROPERTY + | EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + return eval_reflection_shallow_class_object_result(related_class_name, context, values); + } + values.bool_value(false) +} + +/// Builds a full `ReflectionClass` object for parent-class metadata. +pub(super) fn eval_reflection_full_class_object_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return eval_reflection_builtin_closure_class_object_result( + EVAL_REFLECTION_OWNER_CLASS, + context, + values, + ); + } + let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { + return values.bool_value(false); + }; + let runtime_class_name = class_name.trim_start_matches('\\'); + let method_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_METHOD, + runtime_class_name, + values, + )?; + let property_names = eval_reflection_aot_member_names( + EVAL_REFLECTION_OWNER_PROPERTY, + runtime_class_name, + values, + )?; + let interface_names = + eval_reflection_aot_class_interface_names(runtime_class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(runtime_class_name, values)?; + let parent_class_name = eval_reflection_aot_parent_class_name(runtime_class_name, values)?; + let attributes = context.native_class_attributes(runtime_class_name); + return eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS, + runtime_class_name, + &attributes, + &interface_names, + &trait_names, + &method_names, + &property_names, + parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + context, + values, + ); + }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_owner_object( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + &metadata.attributes, + &interface_names, + &metadata.trait_names, + &metadata.method_names, + &metadata.property_names, + metadata.parent_class_name.as_deref(), + &[], + None, + None, + None, + None, + flags, + metadata.modifiers, + 0, + None, + None, + context, + values, + ) +} + +/// Builds a shallow `ReflectionClass` object for member declaring-class metadata. +pub(super) fn eval_reflection_shallow_class_object_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(metadata) = eval_reflection_class_like_attributes(class_name, context) else { + let Some((flags, modifiers)) = eval_reflection_aot_class_flags(class_name, values)? else { + return values.bool_value(false); + }; + let interface_names = eval_reflection_aot_class_interface_names(class_name, values)?; + let trait_names = eval_reflection_aot_class_trait_names(class_name, values)?; + let attributes = context.native_class_attributes(class_name); + return eval_reflection_owner_object_with_members( + EVAL_REFLECTION_OWNER_CLASS, + class_name.trim_start_matches('\\'), + &attributes, + &interface_names, + &trait_names, + &[], + &[], + None, + &[], + None, + None, + None, + None, + flags, + modifiers, + 0, + None, + None, + false, + context, + values, + ); + }; + let interface_names = + eval_reflection_eval_metadata_interface_names(&metadata, context, values)?; + let flags = eval_reflection_eval_metadata_flags(&metadata, context, values)?; + eval_reflection_owner_object_with_members( + EVAL_REFLECTION_OWNER_CLASS, + &metadata.resolved_name, + &metadata.attributes, + &interface_names, + &metadata.trait_names, + &[], + &[], + None, + &[], + None, + None, + None, + None, + flags, + metadata.modifiers, + 0, + None, + None, + false, + context, + values, + ) +} + +/// Returns the generated/AOT parent class name for a reflected class, if any. +pub(super) fn eval_reflection_aot_parent_class_name( + class_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let runtime_class_name = class_name.trim_start_matches('\\'); + let class_cell = values.string(runtime_class_name)?; + let parent_cell = match values.parent_class_name(class_cell) { + Ok(parent_cell) => parent_cell, + Err(err) => { + values.release(class_cell)?; + return Err(err); + } + }; + values.release(class_cell)?; + let parent_bytes = match values.string_bytes(parent_cell) { + Ok(parent_bytes) => parent_bytes, + Err(err) => { + values.release(parent_cell)?; + return Err(err); + } + }; + values.release(parent_cell)?; + let parent_name = String::from_utf8(parent_bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + if parent_name.is_empty() { + Ok(None) + } else { + Ok(Some(parent_name)) + } +} + +/// Builds an indexed PHP string array for ReflectionClass metadata names. +pub(super) fn eval_reflection_string_array_result( + names: &[String], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.string_array_new(names.len())?; + for name in names { + result = values.string_array_push(result, name)?; + } + Ok(result) +} + +/// Builds a string-keyed PHP associative array from owned string pairs. +pub(super) fn eval_reflection_string_assoc_result( + pairs: Vec<(String, String)>, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(pairs.len())?; + for (key, value) in pairs { + let key = values.string(&key)?; + let value = values.string(&value)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Builds a name-keyed PHP array of full ReflectionClass objects. +pub(super) fn eval_reflection_class_object_map_result( + names: &[String], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.assoc_new(names.len())?; + for name in names { + let key = values.string(name)?; + let object = eval_reflection_full_class_object_result(name, context, values)?; + result = values.array_set(result, key, object)?; + } + Ok(result) +} + +/// Maps a synthetic reflection owner kind to PHP's `Attribute::TARGET_*` bitmask. +pub(super) fn eval_reflection_attribute_target(owner_kind: u64) -> u64 { + match owner_kind { + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT | EVAL_REFLECTION_OWNER_ENUM => { + EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS + } + EVAL_REFLECTION_OWNER_FUNCTION => EVAL_REFLECTION_ATTRIBUTE_TARGET_FUNCTION, + EVAL_REFLECTION_OWNER_METHOD => EVAL_REFLECTION_ATTRIBUTE_TARGET_METHOD, + EVAL_REFLECTION_OWNER_PROPERTY => EVAL_REFLECTION_ATTRIBUTE_TARGET_PROPERTY, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => EVAL_REFLECTION_ATTRIBUTE_TARGET_CLASS_CONSTANT, + _ => 0, + } +} + +/// Returns whether a synthetic owner stores `ReflectionClass`-style metadata. +pub(super) fn eval_reflection_owner_uses_class_metadata(owner_kind: u64) -> bool { + matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS | EVAL_REFLECTION_OWNER_OBJECT + ) +} + +/// Builds an indexed array of populated ReflectionParameter objects. +pub(super) fn eval_reflection_parameter_object_array_result( + parameters: &[EvalReflectionParameterMetadata], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(parameters.len())?; + for parameter in parameters { + let parameter_object = eval_reflection_parameter_object_result(parameter, context, values)?; + let key = values.int(parameter.position as i64)?; + result = values.array_set(result, key, parameter_object)?; + } + Ok(result) +} + +/// Materializes one ReflectionParameter object through the shared reflection helper. +pub(super) fn eval_reflection_parameter_object_result( + parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = eval_reflection_attribute_array_result( + ¶meter.attributes, + EVAL_REFLECTION_ATTRIBUTE_TARGET_PARAMETER, + context, + values, + )?; + let declaring_function = match parameter.declaring_function.as_ref() { + Some(metadata) => { + eval_reflection_declaring_function_object_result(metadata, context, values)? + } + None => values.null()?, + }; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let method_objects = values.array_new(0)?; + let parent_class = match parameter.declaring_class_name.as_deref() { + Some(declaring_class_name) => { + eval_reflection_shallow_class_object_result(declaring_class_name, context, values)? + } + None => values.null()?, + }; + let type_value = match parameter.type_metadata.as_ref() { + Some(type_metadata) => eval_reflection_type_object_result(type_metadata, values)?, + None => values.null()?, + }; + let class_value = eval_reflection_parameter_class_value(parameter, context, values)?; + let default_value = eval_reflection_parameter_default_value(parameter, context, values)?; + let default_value_constant_name = match parameter.default_value_constant_name.as_deref() { + Some(name) => values.string(name)?, + None => values.null()?, + }; + let constructor = values.null()?; + let flags = eval_reflection_parameter_flags(parameter); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_PARAMETER, + ¶meter.name, + attrs, + declaring_function, + trait_names, + method_names, + property_names, + type_value, + default_value, + parent_class, + flags, + parameter.position as u64, + 0, + default_value_constant_name, + class_value, + constructor, + )?; + values.release(attrs)?; + values.release(declaring_function)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(method_objects)?; + values.release(type_value)?; + values.release(default_value)?; + values.release(parent_class)?; + values.release(default_value_constant_name)?; + values.release(class_value)?; + values.release(constructor)?; + Ok(object) +} + +/// Materializes the legacy ReflectionParameter::getClass() value for known named object types. +pub(super) fn eval_reflection_parameter_class_value( + parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_reflection_parameter_class_name(parameter) { + Some(class_name) => eval_reflection_shallow_class_object_result(class_name, context, values), + None => values.null(), + } +} + +/// Returns the retained object type name used by ReflectionParameter::getClass(). +pub(super) fn eval_reflection_parameter_class_name( + parameter: &EvalReflectionParameterMetadata, +) -> Option<&str> { + match ¶meter.type_metadata.as_ref()?.kind { + EvalReflectionParameterTypeKind::Named(named_type) if !named_type.is_builtin => { + Some(named_type.name.as_str()) + } + _ => None, + } +} + +/// Materializes one ReflectionParameter default using declaring class and magic scopes. +pub(super) fn eval_reflection_parameter_default_value( + parameter: &EvalReflectionParameterMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(default) = parameter.default_value.as_ref() else { + return values.null(); + }; + if let Some(class_name) = parameter.declaring_class_name.as_deref() { + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(class_name.to_string()); + } + let magic_scope = parameter + .declaring_function + .as_ref() + .and_then(|function| function.magic_scope.as_ref()); + if let Some(magic_scope) = magic_scope { + context.push_callable_magic_scope( + &magic_scope.function_name, + &magic_scope.method_name, + magic_scope.class_name.as_deref(), + magic_scope.trait_name.as_deref(), + ); + } + let result = eval_method_parameter_default(default, context, values); + if magic_scope.is_some() { + context.pop_magic_scope(); + } + if parameter.declaring_class_name.is_some() { + context.pop_called_class_scope(); + context.pop_class_scope(); + } + result +} + +/// Evaluates one reflected property default with its declaring class-like magic scope. +pub(super) fn eval_reflection_member_default_value( + member: &EvalReflectionMemberMetadata, + default: &EvalExpr, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_class_like_default_value( + member.declaring_class_name.as_deref(), + member.default_value_trait_origin.as_deref(), + default, + context, + values, + ) +} + +/// Evaluates one class-like default expression with PHP `__CLASS__` and `__TRAIT__`. +pub(super) fn eval_reflection_class_like_default_value( + declaring_class: Option<&str>, + trait_origin: Option<&str>, + default: &EvalExpr, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(declaring_class) = declaring_class else { + return eval_method_parameter_default(default, context, values); + }; + let trait_name = + trait_origin.or_else(|| context.has_trait(declaring_class).then_some(declaring_class)); + context.push_class_like_member_magic_scope(declaring_class, trait_name); + let result = eval_method_parameter_default(default, context, values); + context.pop_magic_scope(); + result +} + +/// Builds a shallow ReflectionMethod object for a parameter's declaring function metadata. +pub(super) fn eval_reflection_declaring_function_object_result( + metadata: &EvalReflectionDeclaringFunctionMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let owner_kind = if metadata.declaring_class_name.is_some() { + EVAL_REFLECTION_OWNER_METHOD + } else { + EVAL_REFLECTION_OWNER_FUNCTION + }; + let method_modifiers = if metadata.declaring_class_name.is_some() { + eval_reflection_method_modifiers_from_flags(metadata.flags) + } else { + 0 + }; + eval_reflection_owner_object( + owner_kind, + &metadata.name, + &metadata.attributes, + &[], + &[], + &[], + &[], + metadata.declaring_class_name.as_deref(), + &[], + None, + None, + None, + None, + metadata.flags, + metadata.required_parameter_count as u64, + method_modifiers, + None, + None, + context, + values, + ) +} + +/// Materializes one parameter ReflectionType object through the shared reflection helper. +pub(super) fn eval_reflection_type_object_result( + type_metadata: &EvalReflectionParameterTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named_type) => { + eval_reflection_named_type_object_result(named_type, values) + } + EvalReflectionParameterTypeKind::Union(union_type) => { + eval_reflection_union_type_object_result(union_type, values) + } + EvalReflectionParameterTypeKind::Intersection(intersection_type) => { + eval_reflection_intersection_type_object_result(intersection_type, values) + } + } +} + +/// Materializes one ReflectionNamedType object through the shared reflection helper. +pub(super) fn eval_reflection_named_type_object_result( + type_metadata: &EvalReflectionNamedTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let method_objects = values.array_new(0)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; + let backing_value = values.null()?; + let flags = eval_reflection_named_type_flags(type_metadata); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_NAMED_TYPE, + &type_metadata.name, + attrs, + interface_names, + trait_names, + method_names, + property_names, + method_objects, + property_objects, + parent_class, + flags, + 0, + 0, + constant_value, + backing_value, + constant_value, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(method_objects)?; + values.release(property_objects)?; + values.release(parent_class)?; + values.release(constant_value)?; + values.release(backing_value)?; + Ok(object) +} + +/// Materializes one ReflectionUnionType object through the shared reflection helper. +pub(super) fn eval_reflection_union_type_object_result( + type_metadata: &EvalReflectionUnionTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; + let backing_value = values.null()?; + let flags = eval_reflection_union_type_flags(type_metadata); + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_UNION_TYPE, + "", + attrs, + interface_names, + trait_names, + method_names, + property_names, + types, + property_objects, + parent_class, + flags, + 0, + 0, + constant_value, + backing_value, + constant_value, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(types)?; + values.release(property_objects)?; + values.release(parent_class)?; + values.release(constant_value)?; + values.release(backing_value)?; + Ok(object) +} + +/// Materializes one ReflectionIntersectionType object through the shared reflection helper. +pub(super) fn eval_reflection_intersection_type_object_result( + type_metadata: &EvalReflectionIntersectionTypeMetadata, + values: &mut impl RuntimeValueOps, +) -> Result { + let attrs = values.array_new(0)?; + let interface_names = values.array_new(0)?; + let trait_names = values.array_new(0)?; + let method_names = values.array_new(0)?; + let property_names = values.array_new(0)?; + let types = eval_reflection_named_type_object_array_result(&type_metadata.types, values)?; + let property_objects = values.array_new(0)?; + let parent_class = values.bool_value(false)?; + let constant_value = values.null()?; + let backing_value = values.null()?; + let object = values.reflection_owner_new( + EVAL_REFLECTION_OWNER_INTERSECTION_TYPE, + "", + attrs, + interface_names, + trait_names, + method_names, + property_names, + types, + property_objects, + parent_class, + 0, + 0, + 0, + constant_value, + backing_value, + constant_value, + )?; + values.release(attrs)?; + values.release(interface_names)?; + values.release(trait_names)?; + values.release(method_names)?; + values.release(property_names)?; + values.release(types)?; + values.release(property_objects)?; + values.release(parent_class)?; + values.release(constant_value)?; + values.release(backing_value)?; + Ok(object) +} + +/// Builds an indexed array of populated ReflectionNamedType objects. +pub(super) fn eval_reflection_named_type_object_array_result( + types: &[EvalReflectionNamedTypeMetadata], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(types.len())?; + for (position, type_metadata) in types.iter().enumerate() { + let type_object = eval_reflection_named_type_object_result(type_metadata, values)?; + let key = values.int(position as i64)?; + result = values.array_set(result, key, type_object)?; + } + Ok(result) +} + +/// Builds the `ReflectionMethod|null` value stored in ReflectionClass::__constructor. +pub(super) fn eval_reflection_constructor_object_result( + owner_kind: u64, + class_name: &str, + include_class_members: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !eval_reflection_owner_uses_class_metadata(owner_kind) || !include_class_members { + return values.null(); + } + let Some(member) = eval_reflection_method_metadata(class_name, "__construct", context) else { + if !eval_reflection_class_like_exists(class_name, context) { + if let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name, + "__construct", + context, + values, + )? { + return eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + "__construct", + &member, + context, + values, + ); + } + } + return values.null(); + }; + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + "__construct", + &member, + context, + values, + ) +} + +/// Materializes one eval-backed ReflectionMethod or ReflectionProperty object. +pub(super) fn eval_reflection_member_object_result( + owner_kind: u64, + reflected_name: &str, + member: &EvalReflectionMemberMetadata, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut flags = eval_reflection_member_flags( + member.visibility, + member.is_static, + member.is_final, + member.is_abstract, + member.is_readonly, + ); + if member.default_value.is_some() { + flags |= EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE; + } + if member.is_promoted { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PROMOTED; + } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 512) != 0 { + flags |= EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL; + } + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY && (member.modifiers & 4096) != 0 { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET | EVAL_REFLECTION_MEMBER_FLAG_FINAL; + } else if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + && (member.modifiers & 2048) != 0 + && !member.is_readonly + { + flags |= EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET; + } + if member.is_dynamic { + flags |= EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC; + } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + flags |= eval_reflection_callable_flags(&member.attributes); + } + let owner_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + member.required_parameter_count as u64 + } else { + member.modifiers + }; + let method_modifiers = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + member.modifiers + } else { + 0 + }; + eval_reflection_owner_object( + owner_kind, + reflected_name, + &member.attributes, + &[], + &[], + &[], + &[], + member.declaring_class_name.as_deref(), + &member.parameters, + member.type_metadata.as_ref(), + member.settable_type_metadata.as_ref(), + member.default_value.as_ref(), + member.default_value_trait_origin.as_deref(), + flags, + owner_modifiers, + method_modifiers, + None, + None, + context, + values, + ) +} + +/// Builds an indexed array of ReflectionMethod or ReflectionProperty objects for a ReflectionClass. +pub(super) fn eval_reflection_member_object_array_result( + owner_kind: u64, + class_name: &str, + names: &[String], + filter: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + let mut index = 0; + for name in names { + let Some(member) = eval_reflection_member_metadata(owner_kind, class_name, name, context) + else { + continue; + }; + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } + let member_object = + eval_reflection_member_object_result(owner_kind, name, &member, context, values)?; + let key = values.int(index)?; + result = values.array_set(result, key, member_object)?; + index += 1; + } + Ok(result) +} + +/// Builds an indexed array of AOT ReflectionMethod or ReflectionProperty objects for a class. +pub(super) fn eval_reflection_aot_member_object_array_result( + owner_kind: u64, + class_name: &str, + names: &[String], + filter: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = values.array_new(names.len())?; + let mut index = 0; + for name in names { + let member = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + eval_reflection_aot_method_metadata_with_signature_if_exists( + class_name, name, context, values, + )? + } else { + eval_reflection_native_interface_property_requirement(class_name, name, context) + .map(|(declaring_class, property)| { + eval_reflection_interface_property_metadata(declaring_class, &property) + }) + .or(eval_reflection_aot_property_metadata_if_exists( + class_name, name, context, values, + )?) + }; + let Some(member) = member else { + continue; + }; + if !eval_reflection_member_matches_filter(&member, filter) { + continue; + } + let reflected_name = if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + name.to_ascii_lowercase() + } else { + name.clone() + }; + let member_object = eval_reflection_member_object_result( + owner_kind, + &reflected_name, + &member, + context, + values, + )?; + let key = values.int(index)?; + result = values.array_set(result, key, member_object)?; + index += 1; + } + Ok(result) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/parameter_construction.rs b/crates/elephc-magician/src/interpreter/reflection/parameter_construction.rs new file mode 100644 index 0000000000..7ae21c4cec --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/parameter_construction.rs @@ -0,0 +1,280 @@ +//! Purpose: +//! Resolves ReflectionParameter constructor selectors and materializes parameters. +//! +//! Called from: +//! - `crate::interpreter::reflection::eval_reflection_owner_new_object()`. +//! +//! Key details: +//! - Function and method targets accept PHP-compatible name or position selectors. + +use super::*; + +/// Builds an eval-backed `ReflectionParameter` object for a function or method parameter. +pub(super) fn eval_reflection_parameter_new( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("function"), String::from("param")], + evaluated_args, + )?; + let selector = eval_reflection_parameter_selector(args[1], values)?; + let Some(parameter) = + eval_reflection_parameter_constructor_metadata(args[0], selector.clone(), context, values)? + else { + return eval_reflection_parameter_constructor_error(args[0], &selector, context, values); + }; + eval_reflection_parameter_object_result(¶meter, context, values).map(Some) +} + +/// Throws the PHP constructor error for eval-backed `ReflectionParameter` misses. +pub(super) fn eval_reflection_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_array_like(target)? { + return eval_reflection_method_parameter_constructor_error(target, selector, context, values); + } + if values.type_tag(target)? == EVAL_TAG_STRING { + return eval_reflection_function_parameter_constructor_error( + target, selector, context, values, + ); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Throws the PHP constructor error for eval-backed function parameter misses. +pub(super) fn eval_reflection_function_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let requested_name = eval_reflection_string_arg(target, values)?; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if context.function(&lookup_name).is_some() || context.native_function(&lookup_name).is_some() { + return eval_reflection_parameter_selector_error(selector, context, values); + } + Ok(None) +} + +/// Throws the PHP constructor error for eval-backed method parameter misses. +pub(super) fn eval_reflection_method_parameter_constructor_error( + target: RuntimeCellHandle, + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(target)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(target, zero)?; + let method = values.array_get(target, one)?; + let method_name = eval_reflection_string_arg(method, values)?; + let class_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_reflection_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + if eval_reflection_class_like_exists(&reflected_name, context) { + let Some(reflected_method_name) = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &reflected_name, + &method_name, + context, + ) else { + return eval_throw_reflection_exception( + &format!("Method {}::{}() does not exist", reflected_name, method_name), + context, + values, + ); + }; + if eval_reflection_method_metadata(&reflected_name, &reflected_method_name, context) + .is_some() + { + return eval_reflection_parameter_selector_error(selector, context, values); + } + return Err(EvalStatus::RuntimeFatal); + } + if eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + &method_name, + context, + values, + )? + .is_some() + { + return eval_reflection_parameter_selector_error(selector, context, values); + } + Ok(None) +} + +/// Throws PHP's selector-specific ReflectionParameter constructor error. +pub(super) fn eval_reflection_parameter_selector_error( + selector: &EvalReflectionParameterSelector, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match selector { + EvalReflectionParameterSelector::Name(_) => eval_throw_reflection_exception( + "The parameter specified by its name could not be found", + context, + values, + ), + EvalReflectionParameterSelector::Position(position) if *position < 0 => { + eval_throw_value_error( + "ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0", + context, + values, + ) + } + EvalReflectionParameterSelector::Position(_) => eval_throw_reflection_exception( + "The parameter specified by its offset could not be found", + context, + values, + ), + } +} + +/// Resolves `ReflectionParameter` constructor target metadata. +pub(super) fn eval_reflection_parameter_constructor_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_array_like(target)? { + return eval_reflection_method_parameter_metadata(target, selector, context, values); + } + if values.type_tag(target)? == EVAL_TAG_STRING { + return eval_reflection_function_parameter_metadata(target, selector, context, values); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Builds selected parameter metadata for an eval or native free function. +pub(super) fn eval_reflection_function_parameter_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let requested_name = eval_reflection_string_arg(target, values)?; + let lookup_name = requested_name.trim_start_matches('\\').to_ascii_lowercase(); + if let Some(function) = context.function(&lookup_name).cloned() { + let parameters = eval_reflection_function_parameters( + function.name(), + function.params(), + function.attributes().to_vec(), + function.parameter_attributes(), + function.parameter_types(), + function.parameter_defaults(), + function.parameter_is_by_ref(), + function.parameter_is_variadic(), + ); + return Ok(eval_reflection_parameter_for_selector(parameters, selector)); + } + if let Some(function) = context.native_function(&lookup_name) { + let reflected_name = requested_name.trim_start_matches('\\'); + let parameters = eval_reflection_native_function_parameters(reflected_name, &function); + return Ok(eval_reflection_parameter_for_selector(parameters, selector)); + } + Ok(None) +} + +/// Builds selected parameter metadata for an eval or generated/AOT method target. +pub(super) fn eval_reflection_method_parameter_metadata( + target: RuntimeCellHandle, + selector: EvalReflectionParameterSelector, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.array_len(target)? != 2 { + return Err(EvalStatus::RuntimeFatal); + } + let zero = values.int(0)?; + let one = values.int(1)?; + let receiver = values.array_get(target, zero)?; + let method = values.array_get(target, one)?; + let method_name = eval_reflection_string_arg(method, values)?; + let class_name = match values.type_tag(receiver)? { + EVAL_TAG_OBJECT => eval_reflection_object_class_name(receiver, context, values)?, + EVAL_TAG_STRING => eval_reflection_string_arg(receiver, values)?, + _ => return Err(EvalStatus::RuntimeFatal), + }; + let reflected_name = context + .resolve_class_like_name(&class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string()); + let member = if eval_reflection_class_like_exists(&reflected_name, context) { + let reflected_method_name = eval_reflection_member_name( + EVAL_REFLECTION_OWNER_METHOD, + &reflected_name, + &method_name, + context, + ); + let Some(reflected_method_name) = reflected_method_name else { + return Ok(None); + }; + let Some(method) = + eval_reflection_method_metadata(&reflected_name, &reflected_method_name, context) + else { + return Ok(None); + }; + method + } else { + let Some(member) = eval_reflection_aot_method_metadata_with_signature_if_exists( + &reflected_name, + &method_name, + context, + values, + )? + else { + return Ok(None); + }; + member + }; + Ok(eval_reflection_parameter_for_selector( + member.parameters, + selector, + )) +} + +/// Converts a `ReflectionParameter` selector runtime value to a supported selector. +pub(super) fn eval_reflection_parameter_selector( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + match values.type_tag(value)? { + EVAL_TAG_STRING => { + eval_reflection_string_arg(value, values).map(EvalReflectionParameterSelector::Name) + } + EVAL_TAG_INT => { + eval_int_value(value, values).map(EvalReflectionParameterSelector::Position) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Selects a parameter by PHP name or zero-based position. +pub(super) fn eval_reflection_parameter_for_selector( + parameters: Vec, + selector: EvalReflectionParameterSelector, +) -> Option { + match selector { + EvalReflectionParameterSelector::Name(name) => parameters + .into_iter() + .find(|parameter| parameter.name == name), + EvalReflectionParameterSelector::Position(position) if position >= 0 => { + parameters.into_iter().nth(position as usize) + } + EvalReflectionParameterSelector::Position(_) => None, + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/parameter_metadata.rs b/crates/elephc-magician/src/interpreter/reflection/parameter_metadata.rs new file mode 100644 index 0000000000..dac7ae2cf7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/parameter_metadata.rs @@ -0,0 +1,348 @@ +//! Purpose: +//! Builds reflected parameter lists, defaults, magic scopes, and type metadata. +//! +//! Called from: +//! - Function, method, property-hook, and ReflectionParameter construction paths. +//! +//! Key details: +//! - Promoted properties, nullable composites, variadics, and defaults remain aligned by index. + +use super::*; + +/// Returns PHP's required parameter count for a reflected method signature. +pub(super) fn eval_reflection_required_parameter_count( + defaults: &[Option], + variadic_flags: &[bool], +) -> usize { + let fixed_count = variadic_flags + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(defaults.len()); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + +/// Builds PHP magic scope metadata for a reflected function parameter default. +pub(super) fn eval_reflection_function_parameter_magic_scope( + function_name: &str, +) -> EvalReflectionParameterMagicScope { + EvalReflectionParameterMagicScope { + function_name: function_name.to_string(), + method_name: function_name.to_string(), + class_name: None, + trait_name: None, + } +} + +/// Builds PHP magic scope metadata for a reflected method parameter default. +pub(super) fn eval_reflection_method_parameter_magic_scope( + class_name: &str, + function_name: &str, + method_name: &str, + trait_name: Option<&str>, +) -> EvalReflectionParameterMagicScope { + EvalReflectionParameterMagicScope { + function_name: function_name.to_string(), + method_name: method_name.to_string(), + class_name: Some(class_name.trim_start_matches('\\').to_string()), + trait_name: trait_name.map(|trait_name| trait_name.trim_start_matches('\\').to_string()), + } +} + +/// Builds PHP magic scope metadata for an eval method's reflected parameter default. +pub(super) fn eval_reflection_eval_method_parameter_magic_scope( + class_name: &str, + method: &EvalClassMethod, + fallback_trait_name: Option<&str>, +) -> EvalReflectionParameterMagicScope { + eval_reflection_method_parameter_magic_scope( + class_name, + method.magic_function_name(), + &method.magic_method_name(class_name), + method + .trait_origin() + .or(fallback_trait_name), + ) +} + +/// Builds parameter reflection metadata from eval parameter names and type flags. +pub(super) fn eval_reflection_parameters_from_names_and_type_flags( + declaring_class_name: Option<&str>, + declaring_function: Option<&EvalReflectionDeclaringFunctionMetadata>, + names: &[String], + has_type_flags: &[bool], + parameter_types: &[Option], + parameter_attributes: &[Vec], + defaults: &[Option], + by_ref_flags: &[bool], + variadic_flags: &[bool], + promoted_parameter_names: &[String], +) -> Vec { + names + .iter() + .enumerate() + .map(|(position, name)| { + let has_type = has_type_flags.get(position).copied().unwrap_or(false); + let default_value = defaults.get(position).and_then(Clone::clone); + let default_value_constant_name = default_value + .as_ref() + .and_then(eval_reflection_default_constant_name); + let type_metadata = parameter_types + .get(position) + .and_then(Option::as_ref) + .and_then(eval_reflection_parameter_type_metadata) + .filter(|_| has_type); + let is_array_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); + EvalReflectionParameterMetadata { + name: name.clone(), + declaring_class_name: declaring_class_name.map(str::to_string), + declaring_function: declaring_function.cloned(), + attributes: parameter_attributes + .get(position) + .cloned() + .unwrap_or_default(), + position, + is_optional: defaults.get(position).is_some_and(Option::is_some) + || variadic_flags.get(position).copied().unwrap_or(false), + is_variadic: variadic_flags.get(position).copied().unwrap_or(false), + is_passed_by_reference: by_ref_flags.get(position).copied().unwrap_or(false), + is_promoted: promoted_parameter_names + .iter() + .any(|promoted_name| promoted_name == name), + has_type, + allows_null: eval_reflection_parameter_allows_null( + has_type, + type_metadata.as_ref(), + default_value.as_ref(), + ), + is_array_type, + is_callable_type, + type_metadata, + default_value, + default_value_constant_name, + } + }) + .collect() +} + +/// Returns whether retained parameter metadata is one named type with the requested name. +pub(super) fn eval_reflection_parameter_has_named_type( + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + expected_name: &str, +) -> bool { + matches!( + type_metadata, + Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(named) + }) if named.name.eq_ignore_ascii_case(expected_name) + ) +} + +/// Returns PHP's ReflectionParameter default-constant name for retained eval defaults. +pub(super) fn eval_reflection_default_constant_name(default: &EvalExpr) -> Option { + match default { + EvalExpr::ConstFetch(name) => Some(name.clone()), + EvalExpr::NamespacedConstFetch { name, .. } => Some(name.clone()), + EvalExpr::ClassConstantFetch { + class_name, + constant, + } => Some(format!("{}::{}", class_name, constant)), + _ => None, + } +} + +/// Builds ReflectionParameter metadata for eval-declared or native free functions. +pub(super) fn eval_reflection_function_parameters( + function_name: &str, + names: &[String], + function_attributes: Vec, + parameter_attributes: &[Vec], + parameter_types: &[Option], + defaults: &[Option], + by_ref_flags: &[bool], + variadic_flags: &[bool], +) -> Vec { + let has_type_flags = parameter_types + .iter() + .map(Option::is_some) + .collect::>(); + let flags = eval_reflection_callable_flags(&function_attributes); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: function_name.to_string(), + declaring_class_name: None, + magic_scope: Some(eval_reflection_function_parameter_magic_scope(function_name)), + attributes: function_attributes, + flags, + required_parameter_count: eval_reflection_required_parameter_count( + defaults, + variadic_flags, + ), + }; + eval_reflection_parameters_from_names_and_type_flags( + None, + Some(&declaring_function), + names, + &has_type_flags, + parameter_types, + parameter_attributes, + defaults, + by_ref_flags, + variadic_flags, + &[], + ) +} + +/// Returns promoted constructor parameter names for one eval class method. +pub(super) fn eval_reflection_promoted_parameter_names( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Vec { + if !method_name.eq_ignore_ascii_case("__construct") { + return Vec::new(); + } + context + .class(class_name) + .map(eval_reflection_promoted_property_names) + .unwrap_or_default() +} + +/// Returns promoted constructor parameter names for one eval trait method. +pub(super) fn eval_reflection_promoted_trait_parameter_names( + trait_decl: &EvalTrait, + method_name: &str, +) -> Vec { + if method_name.eq_ignore_ascii_case("__construct") { + eval_reflection_promoted_property_names_from_slice(trait_decl.properties()) + } else { + Vec::new() + } +} + +/// Returns property names marked as constructor-promoted in one eval class. +pub(super) fn eval_reflection_promoted_property_names(class: &EvalClass) -> Vec { + eval_reflection_promoted_property_names_from_slice(class.properties()) +} + +/// Returns property names marked as constructor-promoted in one property list. +pub(super) fn eval_reflection_promoted_property_names_from_slice( + properties: &[EvalClassProperty], +) -> Vec { + properties + .iter() + .filter(|property| property.is_promoted()) + .map(|property| property.name().to_string()) + .collect() +} + +/// Converts eval parameter type metadata into the supported ReflectionType subset. +pub(super) fn eval_reflection_parameter_type_metadata( + parameter_type: &EvalParameterType, +) -> Option { + let variants = parameter_type.variants(); + if variants.is_empty() { + return None; + } + let allows_null = parameter_type.allows_null(); + let mut types = variants + .iter() + .map(|variant| eval_reflection_named_type_variant_metadata(variant, false)) + .collect::>>()?; + if types.len() == 1 { + let mut named = types.pop()?; + named.allows_null = allows_null; + return Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(named), + }); + } + if parameter_type.is_intersection() { + return Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Intersection( + EvalReflectionIntersectionTypeMetadata { types }, + ), + }); + } + Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Union(EvalReflectionUnionTypeMetadata { + types, + allows_null, + }), + }) +} + +/// Returns PHP's `ReflectionParameter::allowsNull()` value for retained metadata. +pub(super) fn eval_reflection_parameter_allows_null( + has_type: bool, + type_metadata: Option<&EvalReflectionParameterTypeMetadata>, + default_value: Option<&EvalExpr>, +) -> bool { + !has_type + || default_value.is_some_and(|default| matches!(default, EvalExpr::Const(EvalConst::Null))) + || type_metadata.is_some_and(eval_reflection_type_allows_null) +} + +/// Returns whether one retained ReflectionType metadata value accepts null. +pub(super) fn eval_reflection_type_allows_null(type_metadata: &EvalReflectionParameterTypeMetadata) -> bool { + match &type_metadata.kind { + EvalReflectionParameterTypeKind::Named(named_type) => named_type.allows_null, + EvalReflectionParameterTypeKind::Union(union_type) => union_type.allows_null, + EvalReflectionParameterTypeKind::Intersection(_) => false, + } +} + +/// Converts one eval parameter type variant into `ReflectionNamedType` metadata. +pub(super) fn eval_reflection_named_type_variant_metadata( + variant: &EvalParameterTypeVariant, + allows_null: bool, +) -> Option { + match variant { + EvalParameterTypeVariant::Array => { + Some(eval_reflection_builtin_named_type("array", allows_null)) + } + EvalParameterTypeVariant::Bool => { + Some(eval_reflection_builtin_named_type("bool", allows_null)) + } + EvalParameterTypeVariant::Callable => { + Some(eval_reflection_builtin_named_type("callable", allows_null)) + } + EvalParameterTypeVariant::Class(name) => Some(EvalReflectionNamedTypeMetadata { + name: name.clone(), + allows_null, + is_builtin: false, + }), + EvalParameterTypeVariant::Float => { + Some(eval_reflection_builtin_named_type("float", allows_null)) + } + EvalParameterTypeVariant::Int => { + Some(eval_reflection_builtin_named_type("int", allows_null)) + } + EvalParameterTypeVariant::Iterable => { + Some(eval_reflection_builtin_named_type("iterable", allows_null)) + } + EvalParameterTypeVariant::Mixed => Some(eval_reflection_builtin_named_type("mixed", true)), + EvalParameterTypeVariant::Never => Some(eval_reflection_builtin_named_type("never", false)), + EvalParameterTypeVariant::Object => { + Some(eval_reflection_builtin_named_type("object", allows_null)) + } + EvalParameterTypeVariant::String => { + Some(eval_reflection_builtin_named_type("string", allows_null)) + } + EvalParameterTypeVariant::Void => Some(eval_reflection_builtin_named_type("void", false)), + } +} + +/// Builds metadata for one builtin eval `ReflectionNamedType`. +pub(super) fn eval_reflection_builtin_named_type( + name: &str, + allows_null: bool, +) -> EvalReflectionNamedTypeMetadata { + EvalReflectionNamedTypeMetadata { + name: name.to_string(), + allows_null, + is_builtin: true, + } +} diff --git a/crates/elephc-magician/src/interpreter/reflection/property_access.rs b/crates/elephc-magician/src/interpreter/reflection/property_access.rs new file mode 100644 index 0000000000..b7862a05a4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/property_access.rs @@ -0,0 +1,421 @@ +//! Purpose: +//! Reads, writes, and validates reflected instance, static, and dynamic properties. +//! +//! Called from: +//! - ReflectionProperty value, raw-value, and initialized-state APIs. +//! +//! Key details: +//! - Eval and AOT storage paths enforce declaring-class and object compatibility. + +use super::*; + +/// Reads one eval instance property through ReflectionProperty semantics. +pub(super) fn eval_reflection_instance_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (object_class_name, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.has_get_hook() + && !current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_get_method(property.name()), + context, + ) + { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_get_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + return eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + Vec::new(), + context, + values, + ); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_get(object, &storage_property_name) +} + +/// Writes one eval instance property through ReflectionProperty semantics. +pub(super) fn eval_reflection_instance_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let (object_class_name, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + validate_eval_reflection_property_write(declaring_class, &property, context)?; + if property.has_set_hook() { + if !current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_set_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + let hook_result = eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + vec![EvaluatedCallArg { + name: None, + value, + ref_target: None, + }], + context, + values, + )?; + values.release(hook_result)?; + return Ok(()); + } + } else if property.has_get_hook() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_set(object, &storage_property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Reads one generated/AOT instance property through ReflectionProperty semantics. +pub(super) fn eval_reflection_aot_instance_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.property_get(object, property_name) + }) +} + +/// Writes one generated/AOT instance property through ReflectionProperty semantics. +pub(super) fn eval_reflection_aot_instance_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.property_set(object, property_name, value) + }) +} + +/// Checks one generated/AOT instance property initialization marker through ReflectionProperty. +pub(super) fn eval_reflection_aot_instance_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_aot_instance_property_validate_object( + declaring_class, + object, + context, + values, + )?; + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.property_is_initialized(object, property_name) + }) +} + +/// Checks one generated/AOT static property initialization marker through ReflectionProperty. +pub(super) fn eval_reflection_aot_static_property_is_initialized( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_with_declaring_class_scope(declaring_class, context, |_| { + values.static_property_is_initialized(declaring_class, property_name) + }) +} + +/// Verifies a generated/AOT ReflectionProperty instance target is compatible. +pub(super) fn eval_reflection_aot_instance_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.is_null(object)? || values.type_tag(object)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let is_instance = dynamic_object_is_a(object, declaring_class, false, context, values)? + .map_or_else(|| values.object_is_a(object, declaring_class, false), Ok)?; + if is_instance { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether one eval instance property is initialized for ReflectionProperty. +pub(super) fn eval_reflection_instance_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Ok(true); + } + let identity = values.object_identity(object)?; + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + Ok(context.dynamic_property_is_initialized(identity, &storage_property_name)) +} + +/// Reads one eval instance property through ReflectionProperty raw-storage semantics. +pub(super) fn eval_reflection_instance_property_get_raw_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_get(object, &storage_property_name) +} + +/// Writes one eval instance property through ReflectionProperty raw-storage semantics. +pub(super) fn eval_reflection_instance_property_set_raw_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let (_, property) = eval_reflection_instance_property_target( + declaring_class, + property_name, + object, + context, + values, + )?; + if property.is_virtual() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_reflection_property_write(declaring_class, &property, context)?; + let storage_property_name = eval_instance_property_storage_name(declaring_class, &property); + values.property_set(object, &storage_property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Reads a public dynamic property through ReflectionProperty semantics. +pub(super) fn eval_reflection_dynamic_property_get_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + values.property_get(object, property_name) +} + +/// Writes a public dynamic property through ReflectionProperty semantics. +pub(super) fn eval_reflection_dynamic_property_set_value( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + values.property_set(object, property_name, value)?; + let identity = values.object_identity(object)?; + context.mark_dynamic_property_initialized(identity, property_name); + Ok(()) +} + +/// Returns whether a public dynamic property currently exists on the target object. +pub(super) fn eval_reflection_dynamic_property_is_initialized( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_reflection_dynamic_property_validate_object(declaring_class, object, context, values)?; + eval_reflection_object_dynamic_property_exists(object, property_name, values) +} + +/// Validates the object argument used by dynamic ReflectionProperty operations. +pub(super) fn eval_reflection_dynamic_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let object_class_name = eval_reflection_object_class_name(object, context, values)?; + if eval_reflection_class_like_exists(declaring_class, context) { + if context.class_is_a(&object_class_name, declaring_class, false) { + return Ok(()); + } + } else if object_class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case(declaring_class.trim_start_matches('\\')) + { + return Ok(()); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Validates the object argument shared by non-mutating ReflectionProperty instance APIs. +pub(super) fn eval_reflection_property_validate_object( + declaring_class: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let identity = values.object_identity(object)?; + let object_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + .ok_or(EvalStatus::RuntimeFatal)?; + if !context.class_is_a(&object_class_name, declaring_class, false) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Resolves and validates the object/property pair targeted by ReflectionProperty. +pub(super) fn eval_reflection_instance_property_target( + declaring_class: &str, + property_name: &str, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(String, EvalClassProperty), EvalStatus> { + let identity = values.object_identity(object)?; + let object_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + .ok_or(EvalStatus::RuntimeFatal)?; + if !context.class_is_a(&object_class_name, declaring_class, false) { + return Err(EvalStatus::RuntimeFatal); + } + let (_, property) = context + .class_own_property(declaring_class, property_name) + .ok_or(EvalStatus::RuntimeFatal)?; + if property.is_static() || property.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + Ok((object_class_name, property)) +} + +/// Rejects writes to eval properties ReflectionProperty is not allowed to mutate. +pub(super) fn validate_eval_reflection_property_write( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !property.is_readonly() { + return Ok(()); + } + current_eval_property_hook_is( + declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Throws PHP's `ReflectionException` for invalid static-property writes. +pub(super) fn eval_reflection_static_property_missing_for_set( + reflected_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_throw_reflection_exception( + &format!( + "Class {} does not have a property named {}", + reflected_name, property_name + ), + context, + values, + ) +} + +/// Returns ReflectionProperty default metadata for concrete eval properties. +pub(super) fn eval_reflection_property_default_value(property: &EvalClassProperty) -> Option { + if let Some(default) = property.default() { + return Some(default.clone()); + } + if property.is_abstract() || property.property_type().is_some() { + return None; + } + Some(EvalExpr::Const(EvalConst::Null)) +} diff --git a/crates/elephc-magician/src/interpreter/reflection/property_helpers.rs b/crates/elephc-magician/src/interpreter/reflection/property_helpers.rs new file mode 100644 index 0000000000..5c8aa97435 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/reflection/property_helpers.rs @@ -0,0 +1,393 @@ +//! Purpose: +//! Normalizes ReflectionProperty arguments and materializes property hook metadata. +//! +//! Called from: +//! - Property Reflection APIs before runtime reads, writes, or hook inspection. +//! +//! Key details: +//! - Static defaults, raw-value arguments, and synthetic get/set hook methods converge here. + +use super::*; + +/// Binds `getStaticPropertyValue()` arguments while preserving whether a default was supplied. +pub(super) fn eval_reflection_static_property_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let params = [String::from("name"), String::from("default")]; + let mut bound_args = [None, None]; + let mut next_positional = 0; + for arg in evaluated_args { + if let Some(name) = arg.name { + let Some(position) = params.iter().position(|param| param == &name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[position].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[position] = Some(arg.value); + } else { + while next_positional < bound_args.len() && bound_args[next_positional].is_some() { + next_positional += 1; + } + if next_positional >= bound_args.len() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg.value); + next_positional += 1; + } + } + let property_name = bound_args[0].ok_or(EvalStatus::RuntimeFatal)?; + Ok((property_name, bound_args[1])) +} + +/// Binds the optional `ReflectionProperty::getValue()` object argument. +pub(super) fn eval_reflection_property_get_value_arg( + evaluated_args: Vec, +) -> Result, EvalStatus> { + let params = [String::from("object")]; + let mut bound_arg = None; + for arg in evaluated_args { + if let Some(name) = arg.name { + if params.iter().all(|param| param != &name) || bound_arg.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_arg = Some(arg.value); + } else if bound_arg.is_none() { + bound_arg = Some(arg.value); + } else { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(bound_arg) +} + +/// Binds `ReflectionProperty::setValue()` arguments while allowing PHP's static shorthand. +pub(super) fn eval_reflection_property_set_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Option), EvalStatus> { + let params = [String::from("objectOrValue"), String::from("value")]; + let mut bound_args = [None, None]; + let mut next_positional = 0; + for arg in evaluated_args { + if let Some(name) = arg.name { + let Some(position) = params.iter().position(|param| param == &name) else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[position].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[position] = Some(arg.value); + } else { + while next_positional < bound_args.len() && bound_args[next_positional].is_some() { + next_positional += 1; + } + if next_positional >= bound_args.len() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg.value); + next_positional += 1; + } + } + let object_or_value = bound_args[0].ok_or(EvalStatus::RuntimeFatal)?; + Ok((object_or_value, bound_args[1])) +} + +/// Binds the required object argument for `ReflectionProperty::getRawValue()`. +pub(super) fn eval_reflection_property_raw_value_arg( + evaluated_args: Vec, +) -> Result { + let args = bind_evaluated_function_args(&[String::from("object")], evaluated_args)?; + Ok(args[0]) +} + +/// Binds the object and value arguments for `ReflectionProperty::setRawValue()`. +pub(super) fn eval_reflection_property_set_raw_value_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, RuntimeCellHandle), EvalStatus> { + let args = bind_evaluated_function_args( + &[String::from("object"), String::from("value")], + evaluated_args, + )?; + Ok((args[0], args[1])) +} + +/// Returns the eval property metadata eligible for ReflectionProperty hook APIs. +pub(super) fn eval_reflection_property_for_hooks( + declaring_class: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassProperty)> { + if context.has_class(declaring_class) || context.has_enum(declaring_class) { + return context.class_property(declaring_class, property_name); + } + context + .trait_decl(declaring_class) + .and_then(|trait_decl| { + trait_decl + .properties() + .iter() + .find(|property| property.name() == property_name) + .map(|property| (trait_decl.name().to_string(), property.clone())) + }) + .or_else(|| { + context + .interface_property_requirements(declaring_class) + .into_iter() + .find(|property| property.name() == property_name) + .map(|property| { + let property = EvalClassProperty::new(property.name(), None) + .with_type(property.property_type().cloned()) + .with_attributes(property.attributes().to_vec()) + .with_abstract_hook_contract( + property.requires_get(), + property.requires_set(), + ); + (declaring_class.to_string(), property) + }) + }) + .or_else(|| { + eval_reflection_native_interface_property_requirement( + declaring_class, + property_name, + context, + ) + .map(|(owner, property)| { + let property = EvalClassProperty::new(property.name(), None) + .with_type(property.property_type().cloned()) + .with_attributes(property.attributes().to_vec()) + .with_abstract_hook_contract(property.requires_get(), property.requires_set()); + (owner, property) + }) + }) +} + +/// Returns one generated/AOT interface property contract registered for eval reflection. +pub(super) fn eval_reflection_native_interface_property_requirement( + interface_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalInterfaceProperty)> { + context + .native_interface_property_requirements(interface_name) + .into_iter() + .find(|(_, property)| property.name() == property_name) +} + +/// Returns generated/AOT interface property names registered for eval reflection. +pub(super) fn eval_reflection_native_interface_property_names( + interface_name: &str, + context: &ElephcEvalContext, +) -> Vec { + context + .native_interface_property_requirements(interface_name) + .into_iter() + .map(|(_, property)| property.name().to_string()) + .collect() +} + +/// Binds the `PropertyHookType $type` argument used by ReflectionProperty hook APIs. +pub(super) fn eval_reflection_property_hook_arg( + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = bind_evaluated_function_args(&[String::from("type")], evaluated_args)?; + eval_reflection_property_hook_type(args[0], context, values) +} + +/// Converts one synthetic `PropertyHookType` object into an eval reflection hook kind. +pub(super) fn eval_reflection_property_hook_type( + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(value)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + let identity = values.object_identity(value)?; + if !context.dynamic_object_is_class(identity, "PropertyHookType") { + return Err(EvalStatus::RuntimeFatal); + } + let hook_value = values.property_get(value, "value")?; + match eval_reflection_string_arg(hook_value, values)?.as_str() { + "get" => Ok(EvalReflectionPropertyHook::Get), + "set" => Ok(EvalReflectionPropertyHook::Set), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns concrete hook kinds declared on one eval property. +pub(super) fn eval_reflection_property_hook_kinds( + property: &EvalClassProperty, +) -> Vec { + let mut hooks = Vec::new(); + if property.has_get_hook() || property.requires_get_hook() { + hooks.push(EvalReflectionPropertyHook::Get); + } + if property.has_set_hook() || property.requires_set_hook() { + hooks.push(EvalReflectionPropertyHook::Set); + } + hooks +} + +/// Returns whether one eval property exposes the requested concrete hook. +pub(super) fn eval_reflection_property_has_hook( + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> bool { + match hook { + EvalReflectionPropertyHook::Get => property.has_get_hook() || property.requires_get_hook(), + EvalReflectionPropertyHook::Set => property.has_set_hook() || property.requires_set_hook(), + } +} + +/// Builds PHP's string-keyed ReflectionMethod map returned by `getHooks()`. +pub(super) fn eval_reflection_property_hook_method_array( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let hooks = eval_reflection_property_hook_kinds(property); + let mut result = values.assoc_new(hooks.len())?; + for hook in hooks { + let key = values.string(hook.key())?; + let method = eval_reflection_property_hook_method_object( + declaring_class, + property, + hook, + context, + values, + )?; + result = values.array_set(result, key, method)?; + } + Ok(result) +} + +/// Materializes a ReflectionMethod object for one concrete property hook. +pub(super) fn eval_reflection_property_hook_method_object( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let metadata = eval_reflection_property_hook_method_metadata(declaring_class, property, hook); + eval_reflection_member_object_result( + EVAL_REFLECTION_OWNER_METHOD, + &hook.reflected_method_name(property.name()), + &metadata, + context, + values, + ) +} + +/// Builds ReflectionMethod metadata for one eval property hook accessor. +pub(super) fn eval_reflection_property_hook_method_metadata( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> EvalReflectionMemberMetadata { + let parameters = eval_reflection_property_hook_parameters(declaring_class, property, hook); + let required_parameter_count = parameters.len(); + EvalReflectionMemberMetadata { + declaring_class_name: Some(declaring_class.to_string()), + source_file: None, + source_location: None, + attributes: Vec::new(), + visibility: property.visibility(), + is_static: false, + is_final: false, + is_abstract: property.is_abstract(), + is_readonly: false, + is_promoted: false, + is_dynamic: false, + modifiers: eval_reflection_method_modifiers( + property.visibility(), + false, + false, + property.is_abstract(), + ), + type_metadata: None, + settable_type_metadata: None, + return_type_metadata: eval_reflection_property_hook_return_type(property, hook), + default_value: None, + default_value_trait_origin: None, + required_parameter_count, + parameters, + } +} + +/// Builds the synthetic setter parameter metadata exposed by PHP hook reflection. +pub(super) fn eval_reflection_property_hook_parameters( + declaring_class: &str, + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> Vec { + if !matches!(hook, EvalReflectionPropertyHook::Set) { + return Vec::new(); + } + let type_metadata = property + .settable_type() + .and_then(eval_reflection_parameter_type_metadata); + let has_type = type_metadata.is_some(); + let is_array_type = eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "array"); + let is_callable_type = + eval_reflection_parameter_has_named_type(type_metadata.as_ref(), "callable"); + let declaring_function = EvalReflectionDeclaringFunctionMetadata { + name: hook.reflected_method_name(property.name()), + declaring_class_name: Some(declaring_class.to_string()), + magic_scope: None, + attributes: Vec::new(), + flags: eval_reflection_member_flags(property.visibility(), false, false, false, false), + required_parameter_count: 1, + }; + vec![EvalReflectionParameterMetadata { + name: "value".to_string(), + declaring_class_name: Some(declaring_class.to_string()), + declaring_function: Some(declaring_function), + attributes: Vec::new(), + position: 0, + is_optional: false, + is_variadic: false, + is_passed_by_reference: false, + is_promoted: false, + has_type, + allows_null: type_metadata + .as_ref() + .is_some_and(eval_reflection_type_allows_null), + is_array_type, + is_callable_type, + type_metadata, + default_value: None, + default_value_constant_name: None, + }] +} + +/// Returns the ReflectionMethod return type metadata for a property hook. +pub(super) fn eval_reflection_property_hook_return_type( + property: &EvalClassProperty, + hook: EvalReflectionPropertyHook, +) -> Option { + match hook { + EvalReflectionPropertyHook::Get => property + .property_type() + .and_then(eval_reflection_parameter_type_metadata), + EvalReflectionPropertyHook::Set => Some(EvalReflectionParameterTypeMetadata { + kind: EvalReflectionParameterTypeKind::Named(eval_reflection_builtin_named_type( + "void", false, + )), + }), + } +} + +/// Maps PHP-visible property-hook method names back to eval's synthetic method names. +pub(super) fn eval_reflection_property_hook_synthetic_method_name(method_name: &str) -> Option { + let body = method_name.strip_prefix('$')?; + let (property_name, hook_name) = body.rsplit_once("::")?; + match hook_name { + "get" => Some(EvalReflectionPropertyHook::Get.synthetic_method_name(property_name)), + "set" => Some(EvalReflectionPropertyHook::Set.synthetic_method_name(property_name)), + _ => None, + } +} diff --git a/crates/elephc-magician/src/interpreter/statements.rs b/crates/elephc-magician/src/interpreter/statements.rs index 99a126dd16..f6b0e3cc4c 100644 --- a/crates/elephc-magician/src/interpreter/statements.rs +++ b/crates/elephc-magician/src/interpreter/statements.rs @@ -1,5 +1,6 @@ //! Purpose: -//! Executes EvalIR statements, loops, exception handling, static locals, and eval-declared classes. +//! Dispatches EvalIR statements and propagates structured control flow. +//! Statement families and runtime subsystems live in focused child modules. //! //! Called from: //! - `crate::interpreter::execute_program_outcome_with_context()` and dynamic function execution. @@ -8,6 +9,34 @@ //! - Statement execution propagates `EvalControl` instead of flattening returns, throws, breaks, or continues. //! - Scope writes flow through shared scope-cell helpers so global aliases and reference aliases stay coherent. +mod abstract_requirements; +mod array_updates; +mod attributes_magic_validation; +mod callable_objects; +mod class_declarations; +mod class_resolution; +mod closure_binding; +mod dispatch; +mod dynamic_method_execution; +mod enum_declarations; +mod exceptions; +mod instance_property_access; +mod interface_contracts; +mod interface_member_validation; +mod loop_statements; +mod method_dispatch; +mod native_argument_binding; +mod native_constructor_defaults; +mod native_method_execution; +mod native_static_dispatch; +mod property_constant_validation; +mod property_validation; +mod reference_writeback; +mod reflection_instantiation; +mod static_method_dispatch; +mod static_property_access; +mod trait_declarations; + use super::*; use crate::context::{ NativeCallableArrayDefaultElement, NativeCallableArrayDefaultKey, @@ -15,12464 +44,30 @@ use crate::context::{ push_native_frame_called_class_override, }; -/// Executes statements in source order and propagates the first eval `return`. -pub(in crate::interpreter) fn execute_statements( - statements: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - for stmt in statements { - match execute_stmt(stmt, context, scope, values)? { - EvalControl::None => {} - control => return Ok(control), - } - } - Ok(EvalControl::None) -} - -/// Executes one statement and returns `Some` only for eval `return`. -pub(in crate::interpreter) fn execute_stmt( - stmt: &EvalStmt, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match stmt { - EvalStmt::ArrayAppendVar { name, value } => { - eval_array_append_var_stmt(name, value, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::ArraySetVar { name, index, value } => { - eval_array_set_var_stmt(name, index, value, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::Break => Ok(EvalControl::Break), - EvalStmt::Continue => Ok(EvalControl::Continue), - EvalStmt::DoWhile { body, condition } => { - execute_do_while_stmt(body, condition, context, scope, values) - } - EvalStmt::Echo(expr) => { - let value = eval_expr(expr, context, scope, values)?; - let value = eval_string_context_value(value, context, values)?; - values.echo(value)?; - Ok(EvalControl::None) - } - EvalStmt::For { - init, - condition, - update, - body, - } => execute_for_stmt( - init, - condition.as_ref(), - update, - body, - context, - scope, - values, - ), - EvalStmt::ClassDecl(class) => { - execute_class_decl_stmt(class, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::EnumDecl(enum_decl) => { - execute_enum_decl_stmt(enum_decl, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::InterfaceDecl(interface) => { - execute_interface_decl_stmt(interface, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::TraitDecl(trait_decl) => { - execute_trait_decl_stmt(trait_decl, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::Foreach { - array, - key_name, - value_name, - body, - } => execute_foreach_stmt( - array, - key_name.as_deref(), - value_name, - body, - context, - scope, - values, - ), - EvalStmt::FunctionDecl { - name, - source_location, - attributes, - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - return_type, - body, - } => { - let key = name.to_ascii_lowercase(); - let mut function = EvalFunction::new(name.clone(), params.clone(), body.clone()) - .with_attributes(attributes.clone()) - .with_parameter_attributes(parameter_attributes.clone()) - .with_parameter_types(parameter_types.clone()) - .with_parameter_defaults(parameter_defaults.clone()) - .with_parameter_by_ref_flags(parameter_is_by_ref.clone()) - .with_parameter_variadic_flags(parameter_is_variadic.clone()) - .with_return_type(return_type.clone()); - if let Some(source_location) = source_location { - function = function.with_source_location(*source_location); - } - context - .define_function(key, function) - .map_err(|_| EvalStatus::RuntimeFatal)?; - Ok(EvalControl::None) - } - EvalStmt::Global { vars } => { - execute_global_stmt(vars, context, scope)?; - Ok(EvalControl::None) - } - EvalStmt::If { - condition, - then_branch, - else_branch, - } => { - let condition = eval_expr(condition, context, scope, values)?; - if values.truthy(condition)? { - execute_statements(then_branch, context, scope, values) - } else { - execute_statements(else_branch, context, scope, values) - } - } - EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr( - expr, context, scope, values, - )?)), - EvalStmt::Return(None) => Ok(EvalControl::ReturnVoid), - EvalStmt::ReferenceAssign { target, source } => { - for replaced in set_reference_alias(context, scope, target, source, values)? { - values.release(replaced)?; - } - Ok(EvalControl::None) - } - EvalStmt::PropertyReferenceBind { - object, - property, - source, - } => { - let object = eval_expr(object, context, scope, values)?; - eval_property_reference_bind_result(object, property, source, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::DynamicPropertyReferenceBind { - object, - property, - source, - } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_property_reference_bind_result( - object, - &property, - source, - context, - scope, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicPropertySet { - object, - property, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_property_set_result(object, &property, value, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::DynamicPropertyArrayAppend { - object, - property, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_property_array_append_result(object, &property, value, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::DynamicPropertyArraySet { - object, - property, - index, - op, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_property_array_set_result( - object, &property, index, *op, value, context, scope, values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicPropertyCompoundAssign { - object, - property, - op, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - let current = eval_property_get_result(object, &property, context, values)?; - let right = eval_expr(value, context, scope, values)?; - let value = eval_binary_result(*op, current, right, context, values)?; - eval_property_set_result(object, &property, value, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::DynamicPropertyIncDec { - object, - property, - increment, - } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_property_inc_dec_result(object, &property, *increment, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::StaticVar { name, init } => { - execute_static_var_stmt(name, init, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::PropertySet { - object, - property, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_property_set_result(object, property, value, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::PropertyArrayAppend { - object, - property, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - eval_property_array_append_result(object, property, value, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::PropertyArraySet { - object, - property, - index, - op, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - eval_property_array_set_result( - object, property, index, *op, value, context, scope, values, - )?; - Ok(EvalControl::None) - } - EvalStmt::PropertyCompoundAssign { - object, - property, - op, - value, - } => { - let object = eval_expr(object, context, scope, values)?; - let current = eval_property_get_result(object, property, context, values)?; - let right = eval_expr(value, context, scope, values)?; - let value = eval_binary_result(*op, current, right, context, values)?; - eval_property_set_result(object, property, value, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::PropertyIncDec { - object, - property, - increment, - } => { - let object = eval_expr(object, context, scope, values)?; - eval_property_inc_dec_result(object, property, *increment, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::StaticPropertySet { - class_name, - property, - value, - } => { - let value = eval_expr(value, context, scope, values)?; - eval_static_property_set_result(class_name, property, value, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::StaticPropertyReferenceBind { - class_name, - property, - source, - } => { - eval_static_property_reference_bind_result( - class_name, property, source, context, scope, values, - )?; - Ok(EvalControl::None) - } - EvalStmt::StaticPropertyArrayAppend { - class_name, - property, - value, - } => { - eval_static_property_array_append_result( - class_name, property, value, context, scope, values, - )?; - Ok(EvalControl::None) - } - EvalStmt::StaticPropertyArraySet { - class_name, - property, - index, - op, - value, - } => { - eval_static_property_array_set_result( - class_name, property, index, *op, value, context, scope, values, - )?; - Ok(EvalControl::None) - } - EvalStmt::StaticPropertyIncDec { - class_name, - property, - increment, - } => { - eval_static_property_inc_dec_result( - class_name, property, *increment, context, values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertySet { - class_name, - property, - value, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_static_property_set_result(&class_name, property, value, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyReferenceBind { - class_name, - property, - source, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - eval_static_property_reference_bind_result( - &class_name, - property, - source, - context, - scope, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyArrayAppend { - class_name, - property, - value, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - eval_static_property_array_append_result( - &class_name, - property, - value, - context, - scope, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyArraySet { - class_name, - property, - index, - op, - value, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - eval_static_property_array_set_result( - &class_name, - property, - index, - *op, - value, - context, - scope, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyIncDec { - class_name, - property, - increment, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - eval_static_property_inc_dec_result( - &class_name, - property, - *increment, - context, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyNameSet { - class_name, - property, - value, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - eval_static_property_set_result(&class_name, &property, value, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyNameReferenceBind { - class_name, - property, - source, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_static_property_reference_bind_result( - &class_name, - &property, - source, - context, - scope, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyNameArrayAppend { - class_name, - property, - value, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_static_property_array_append_result( - &class_name, - &property, - value, - context, - scope, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyNameArraySet { - class_name, - property, - index, - op, - value, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_static_property_array_set_result( - &class_name, - &property, - index, - *op, - value, - context, - scope, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::DynamicStaticPropertyNameIncDec { - class_name, - property, - increment, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_static_property_inc_dec_result( - &class_name, - &property, - *increment, - context, - values, - )?; - Ok(EvalControl::None) - } - EvalStmt::StoreVar { name, value } => { - let value = eval_expr(value, context, scope, values)?; - for replaced in set_scope_cell( - context, - scope, - name.clone(), - value, - ScopeCellOwnership::Owned, - )? { - eval_release_value(context, values, replaced)?; - } - Ok(EvalControl::None) - } - EvalStmt::Switch { expr, cases } => { - execute_switch_stmt(expr, cases, context, scope, values) - } - EvalStmt::Throw(expr) => { - let thrown = eval_expr(expr, context, scope, values)?; - if values.type_tag(thrown)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - Ok(EvalControl::Throw(thrown)) - } - EvalStmt::Try { - body, - catches, - finally_body, - } => execute_try_stmt(body, catches, finally_body, context, scope, values), - EvalStmt::UnsetArrayElement { array, index } => { - eval_array_unset_element_stmt(array, index, context, scope, values)?; - Ok(EvalControl::None) - } - EvalStmt::UnsetProperty { object, property } => { - let object = eval_expr(object, context, scope, values)?; - eval_property_unset_result(object, property, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::UnsetDynamicProperty { object, property } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_property_unset_result(object, &property, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::UnsetStaticProperty { - class_name, - property, - } => { - eval_static_property_unset_result(class_name, property, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::UnsetDynamicStaticProperty { - class_name, - property, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - eval_static_property_unset_result(&class_name, property, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::UnsetDynamicStaticPropertyName { - class_name, - property, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - eval_static_property_unset_result(&class_name, &property, context, values)?; - Ok(EvalControl::None) - } - EvalStmt::UnsetVar { name } => { - if let Some(replaced) = unset_scope_cell(scope, name.clone()) { - eval_release_value(context, values, replaced)?; - } - Ok(EvalControl::None) - } - EvalStmt::While { condition, body } => { - while { - let condition = eval_expr(condition, context, scope, values)?; - values.truthy(condition)? - } { - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } - Ok(EvalControl::None) - } - EvalStmt::Expr(expr) => { - let result = eval_expr(expr, context, scope, values)?; - eval_release_value(context, values, result)?; - Ok(EvalControl::None) - } - } -} - -/// Applies member increment/decrement to a runtime value using PHP numeric semantics. -fn eval_inc_dec_value( - current: RuntimeCellHandle, - increment: bool, - values: &mut impl RuntimeValueOps, -) -> Result { - let one = values.int(1)?; - if increment { - values.add(current, one) - } else { - values.sub(current, one) - } -} - -/// Reads, updates, and writes one object property after the receiver/name are evaluated. -fn eval_property_inc_dec_result( - object: RuntimeCellHandle, - property: &str, - increment: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let current = eval_property_get_result(object, property, context, values)?; - let value = eval_inc_dec_value(current, increment, values)?; - eval_property_set_result(object, property, value, context, values) -} - -/// Reads, updates, and writes one static property after the receiver/name are resolved. -fn eval_static_property_inc_dec_result( - class_name: &str, - property: &str, - increment: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let current = eval_static_property_get_result(class_name, property, context, values)?; - let value = eval_inc_dec_value(current, increment, values)?; - eval_static_property_set_result(class_name, property, value, context, values) -} - -/// Releases one eval-owned value after running an eval-declared dynamic destructor if needed. -fn eval_release_value( - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, - value: RuntimeCellHandle, -) -> Result<(), EvalStatus> { - if let Some(identity) = values.final_object_identity_for_release(value)? { - eval_dynamic_destructor_for_release(identity, value, context, values)?; - } - values.release(value) -} - -/// Calls a dynamic eval `__destruct()` hook immediately before the runtime frees the object. -fn eval_dynamic_destructor_for_release( - identity: u64, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - eval_dynamic_destructor_for_object_cell(identity, object, context, values).map(|_| ()) -} - -/// Calls a dynamic eval `__destruct()` hook for an already-boxed object cell. -pub(crate) fn eval_dynamic_destructor_for_object_cell( - identity: u64, - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(class_name) = context - .dynamic_object_class(identity) - .map(|class| class.name().to_string()) - else { - return Ok(false); - }; - let Some((declaring_class, method)) = context.class_method(&class_name, "__destruct") else { - return Ok(false); - }; - if !context.begin_dynamic_object_destructor(identity) { - return Ok(true); - } - let result = eval_dynamic_method_with_values( - &declaring_class, - &class_name, - &method, - object, - Vec::new(), - context, - values, - ); - let release_result = match result { - Ok(result) => values.release(result), - Err(status) => Err(status), - }; - context.finish_dynamic_object_destructor(identity); - release_result.map(|_| true) -} - -/// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. -fn eval_array_unset_element_stmt( - array: &EvalExpr, - index: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - match array { - EvalExpr::LoadVar(name) => { - let existing = scope_entry(context, scope, name) - .filter(|entry| entry.flags().is_visible()) - .map(|entry| (entry.cell(), entry.flags().ownership)); - let Some((array, ownership)) = existing else { - return Ok(()); - }; - if let Some(array) = - eval_array_unset_target_result(array, index, context, scope, values)? - { - for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { - values.release(replaced)?; - } - } - return Ok(()); - } - EvalExpr::PropertyGet { object, property } => { - let object = eval_expr(object, context, scope, values)?; - let array = eval_property_get_result(object, property, context, values)?; - if let Some(array) = - eval_array_unset_target_result(array, index, context, scope, values)? - { - eval_property_set_result(object, property, array, context, values)?; - } - return Ok(()); - } - EvalExpr::DynamicPropertyGet { object, property } => { - let object = eval_expr(object, context, scope, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - let array = eval_property_get_result(object, &property, context, values)?; - if let Some(array) = - eval_array_unset_target_result(array, index, context, scope, values)? - { - eval_property_set_result(object, &property, array, context, values)?; - } - return Ok(()); - } - EvalExpr::StaticPropertyGet { - class_name, - property, - } => { - let array = eval_static_property_get_result(class_name, property, context, values)?; - if let Some(array) = - eval_array_unset_target_result(array, index, context, scope, values)? - { - eval_static_property_set_result(class_name, property, array, context, values)?; - } - return Ok(()); - } - EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let array = eval_static_property_get_result(&class_name, property, context, values)?; - if let Some(array) = - eval_array_unset_target_result(array, index, context, scope, values)? - { - eval_static_property_set_result(&class_name, property, array, context, values)?; - } - return Ok(()); - } - EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } => { - let class_name = eval_expr(class_name, context, scope, values)?; - let class_name = eval_dynamic_class_name(class_name, context, values)?; - let property = eval_dynamic_member_name(property, context, scope, values)?; - let array = eval_static_property_get_result(&class_name, &property, context, values)?; - if let Some(array) = - eval_array_unset_target_result(array, index, context, scope, values)? - { - eval_static_property_set_result(&class_name, &property, array, context, values)?; - } - return Ok(()); - } - _ => {} - } - let array = eval_expr(array, context, scope, values)?; - eval_array_access_unset_result(array, index, context, scope, values) -} - -/// Unsets one offset from an already-resolved array-like target and returns a replacement array. -fn eval_array_unset_target_result( - array: RuntimeCellHandle, - index: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.type_tag(array)? == EVAL_TAG_OBJECT { - eval_array_access_unset_result(array, index, context, scope, values)?; - return Ok(None); - } - let tag = values.type_tag(array)?; - if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::UnsupportedConstruct); - } - let index = eval_array_set_index(index, context, scope, values)?; - eval_array_without_key_result(array, index, values).map(Some) -} - -/// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. -fn eval_array_access_unset_result( - array: RuntimeCellHandle, - index: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let index = eval_expr(index, context, scope, values)?; - if values.type_tag(array)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::UnsupportedConstruct); - } - if !eval_array_access_object_matches(array, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let result = eval_method_call_result(array, "offsetUnset", vec![index], context, values)?; - values.release(result)?; - Ok(()) -} - -/// Rebuilds an array without the strict-equal key requested by `unset($array[$key])`. -fn eval_array_without_key_result( - array: RuntimeCellHandle, - index: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let tag = values.type_tag(array)?; - let mut result = if tag == EVAL_TAG_ASSOC { - values.assoc_new(len.saturating_sub(1))? - } else { - values.array_new(len.saturating_sub(1))? - }; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let equal = values.compare(EvalBinOp::StrictEq, key, index)?; - if values.truthy(equal)? { - continue; - } - let value = values.array_get(array, key)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Executes `$var[] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. -fn eval_array_append_var_stmt( - name: &str, - value: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let existing = scope_entry(context, scope, name) - .filter(|entry| entry.flags().is_visible()) - .map(|entry| (entry.cell(), entry.flags().ownership)); - if let Some((object, _)) = existing { - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return eval_non_object_array_append_var_stmt( - name, value, existing, context, scope, values, - ); - } - let offset = values.null()?; - let value = eval_expr(value, context, scope, values)?; - if !eval_array_access_object_matches(object, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let result = - eval_method_call_result(object, "offsetSet", vec![offset, value], context, values)?; - values.release(result)?; - return Ok(()); - } - - eval_non_object_array_append_var_stmt(name, value, existing, context, scope, values) -} - -/// Executes the non-object `$var[] = value` path with the existing array semantics. -fn eval_non_object_array_append_var_stmt( - name: &str, - value: &EvalExpr, - existing: Option<(RuntimeCellHandle, ScopeCellOwnership)>, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let mut ownership = ScopeCellOwnership::Owned; - let array = if let Some((cell, flags_ownership)) = existing { - if values.is_array_like(cell)? { - let tag = values.type_tag(cell)?; - if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::UnsupportedConstruct); - } - ownership = flags_ownership; - cell - } else { - values.array_new(1)? - } - } else { - values.array_new(1)? - }; - let index = eval_array_append_key(array, values)?; - let value = eval_expr(value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { - values.release(replaced)?; - } - Ok(()) -} - -/// Executes `$var[index] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. -fn eval_array_set_var_stmt( - name: &str, - index: &EvalExpr, - value: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let existing = scope_entry(context, scope, name) - .filter(|entry| entry.flags().is_visible()) - .map(|entry| (entry.cell(), entry.flags().ownership)); - if let Some((object, _)) = existing { - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return eval_non_object_array_set_var_stmt( - name, index, value, existing, context, scope, values, - ); - } - let index = eval_expr(index, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - if !eval_array_access_object_matches(object, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let result = - eval_method_call_result(object, "offsetSet", vec![index, value], context, values)?; - values.release(result)?; - return Ok(()); - } - - eval_non_object_array_set_var_stmt(name, index, value, existing, context, scope, values) -} - -/// Executes the non-object `$var[index] = value` path with the existing array semantics. -fn eval_non_object_array_set_var_stmt( - name: &str, - index: &EvalExpr, - value: &EvalExpr, - existing: Option<(RuntimeCellHandle, ScopeCellOwnership)>, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let mut ownership = ScopeCellOwnership::Owned; - let array = if let Some((cell, flags_ownership)) = existing { - if values.is_array_like(cell)? { - ownership = flags_ownership; - cell - } else { - values.array_new(1)? - } - } else { - values.array_new(1)? - }; - let index = eval_array_set_index(index, context, scope, values)?; - let value = eval_expr(value, context, scope, values)?; - let array = eval_array_set_target_for_index(array, index, values)?; - let array = values.array_set(array, index, value)?; - for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { - values.release(replaced)?; - } - Ok(()) -} - -/// Executes `$object->property[] = value`, dispatching ArrayAccess property values when needed. -fn eval_property_array_append_result( - object: RuntimeCellHandle, - property: &str, - value: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let array = eval_property_get_result(object, property, context, values)?; - if values.type_tag(array)? == EVAL_TAG_OBJECT { - if !eval_array_access_object_matches(array, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let offset = values.null()?; - let value = eval_expr(value, context, scope, values)?; - let result = - eval_method_call_result(array, "offsetSet", vec![offset, value], context, values)?; - values.release(result)?; - return Ok(()); - } - let array = if values.is_array_like(array)? { - let tag = values.type_tag(array)?; - if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::UnsupportedConstruct); - } - array - } else { - values.array_new(1)? - }; - let index = eval_array_append_key(array, values)?; - let value = eval_expr(value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - eval_property_set_result(object, property, array, context, values) -} - -/// Executes `$object->property[index] = value` and compound indexed property writes. -fn eval_property_array_set_result( - object: RuntimeCellHandle, - property: &str, - index: &EvalExpr, - op: Option, - value: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let array = eval_property_get_result(object, property, context, values)?; - if values.type_tag(array)? == EVAL_TAG_OBJECT { - if !eval_array_access_object_matches(array, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let index = eval_expr(index, context, scope, values)?; - let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; - let result = - eval_method_call_result(array, "offsetSet", vec![index, value], context, values)?; - values.release(result)?; - return Ok(()); - } - let index = eval_array_set_index(index, context, scope, values)?; - let array = if values.is_array_like(array)? { - array - } else { - values.array_new(1)? - }; - let array = eval_array_set_target_for_index(array, index, values)?; - let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - eval_property_set_result(object, property, array, context, values) -} - -/// Computes the value written by a simple or compound property-array assignment. -fn eval_property_array_set_value( - array: RuntimeCellHandle, - index: RuntimeCellHandle, - op: Option, - value: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(op) = op else { - return eval_expr(value, context, scope, values); - }; - let current = eval_array_get_result(array, index, context, values)?; - let right = eval_expr(value, context, scope, values)?; - eval_binary_result(op, current, right, context, values) -} - -/// Executes `Class::$property[] = value`, including ArrayAccess static-property values. -fn eval_static_property_array_append_result( - class_name: &str, - property: &str, - value: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let array = eval_static_property_get_result(class_name, property, context, values)?; - if values.type_tag(array)? == EVAL_TAG_OBJECT { - if !eval_array_access_object_matches(array, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let offset = values.null()?; - let value = eval_expr(value, context, scope, values)?; - let result = - eval_method_call_result(array, "offsetSet", vec![offset, value], context, values)?; - values.release(result)?; - return Ok(()); - } - let array = if values.is_array_like(array)? { - let tag = values.type_tag(array)?; - if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { - return Err(EvalStatus::UnsupportedConstruct); - } - array - } else { - values.array_new(1)? - }; - let index = eval_array_append_key(array, values)?; - let value = eval_expr(value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - eval_static_property_set_result(class_name, property, array, context, values) -} - -/// Executes `Class::$property[index] = value` and compound indexed static-property writes. -fn eval_static_property_array_set_result( - class_name: &str, - property: &str, - index: &EvalExpr, - op: Option, - value: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let array = eval_static_property_get_result(class_name, property, context, values)?; - if values.type_tag(array)? == EVAL_TAG_OBJECT { - if !eval_array_access_object_matches(array, context, values)? { - return Err(EvalStatus::RuntimeFatal); - } - let index = eval_expr(index, context, scope, values)?; - let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; - let result = - eval_method_call_result(array, "offsetSet", vec![index, value], context, values)?; - values.release(result)?; - return Ok(()); - } - let index = eval_array_set_index(index, context, scope, values)?; - let array = if values.is_array_like(array)? { - array - } else { - values.array_new(1)? - }; - let array = eval_array_set_target_for_index(array, index, values)?; - let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; - let array = values.array_set(array, index, value)?; - eval_static_property_set_result(class_name, property, array, context, values) -} - -/// Evaluates an array-set index and normalizes PHP integer-string keys. -fn eval_array_set_index( - index: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let index = eval_expr(index, context, scope, values)?; - if values.type_tag(index)? != EVAL_TAG_STRING { - return Ok(index); - } - let bytes = values.string_bytes(index)?; - match eval_numeric_string_array_key(&bytes) { - Some(key) => values.int(key), - None => Ok(index), - } -} - -/// Converts indexed arrays to associative arrays before writing a non-numeric string key. -fn eval_array_set_target_for_index( - array: RuntimeCellHandle, - index: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(array)? != EVAL_TAG_ARRAY || values.type_tag(index)? != EVAL_TAG_STRING { - return Ok(array); - } - let len = values.array_len(array)?; - let mut assoc = values.assoc_new(len + 1)?; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - let value = values.array_get(array, key)?; - assoc = values.array_set(assoc, key, value)?; - } - Ok(assoc) -} - -/// Executes an eval `try` body and handles supported `catch` clauses. -pub(in crate::interpreter) fn execute_try_stmt( - body: &[EvalStmt], - catches: &[EvalCatch], - finally_body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let control = match execute_statements(body, context, scope, values) { - Ok(EvalControl::Throw(thrown)) => { - execute_matching_catch(thrown, catches, context, scope, values)? - } - Err(EvalStatus::UncaughtThrowable) => { - let Some(thrown) = context.take_pending_throw() else { - return Err(EvalStatus::UncaughtThrowable); - }; - execute_matching_catch(thrown, catches, context, scope, values)? - } - Ok(control) => control, - Err(status) => return Err(status), - }; - if finally_body.is_empty() { - return Ok(control); - } - match execute_statements(finally_body, context, scope, values) { - Ok(EvalControl::None) => Ok(control), - Ok(finally_control) => { - release_overridden_control(control, values)?; - Ok(finally_control) - } - Err(status) => { - release_overridden_control(control, values)?; - Err(status) - } - } -} - -/// Releases a pending control-flow value when `finally` replaces that action. -pub(in crate::interpreter) fn release_overridden_control( - control: EvalControl, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - match control { - EvalControl::Return(value) | EvalControl::Throw(value) => values.release(value), - EvalControl::None - | EvalControl::ReturnVoid - | EvalControl::Break - | EvalControl::Continue => Ok(()), - } -} - -/// Executes the first supported catch clause for a thrown eval object. -pub(in crate::interpreter) fn execute_matching_catch( - thrown: RuntimeCellHandle, - catches: &[EvalCatch], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut matched = None; - for catch in catches { - if catch_types_match_thrown(thrown, &catch.class_names, context, values)? { - matched = Some(catch); - break; - } - } - let Some(catch) = matched else { - return Ok(EvalControl::Throw(thrown)); - }; - if let Some(var_name) = &catch.var_name { - for replaced in set_scope_cell( - context, - scope, - var_name.clone(), - thrown, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - } else { - values.release(thrown)?; - } - execute_statements(&catch.body, context, scope, values) -} - -/// Returns true when any type in one catch clause accepts the thrown object. -pub(in crate::interpreter) fn catch_types_match_thrown( - thrown: RuntimeCellHandle, - class_names: &[String], - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - for class_name in class_names { - let class_name = class_name.trim_start_matches('\\'); - if class_name.eq_ignore_ascii_case("Throwable") { - return Ok(true); - } - if let Some(matched) = dynamic_object_is_a(thrown, class_name, false, context, values)? { - if matched { - return Ok(true); - } - continue; - } - if values.object_is_a(thrown, class_name, false)? { - return Ok(true); - } - } - Ok(false) -} - -/// Returns whether one name is a PHP native enum interface. -pub(in crate::interpreter) fn eval_builtin_enum_interface_name(name: &str) -> bool { - let name = name.trim_start_matches('\\'); - name.eq_ignore_ascii_case("UnitEnum") || name.eq_ignore_ascii_case("BackedEnum") -} - -/// Returns whether one name is PHP's native backed-enum interface. -fn eval_builtin_backed_enum_interface_name(name: &str) -> bool { - name.trim_start_matches('\\') - .eq_ignore_ascii_case("BackedEnum") -} - -/// Returns whether one name is PHP's native Throwable interface. -fn eval_builtin_throwable_interface_name(name: &str) -> bool { - name.trim_start_matches('\\') - .eq_ignore_ascii_case("Throwable") -} - -/// Returns whether one name is visible as a native/runtime interface to eval. -pub(in crate::interpreter) fn eval_runtime_interface_exists( - name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(eval_builtin_enum_interface_name(name) || values.interface_exists(name)?) -} - -/// Registers an eval-declared class in the dynamic class table. -pub(in crate::interpreter) fn execute_class_decl_stmt( - class: &EvalClass, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let name = class.name().trim_start_matches('\\'); - if context.has_class(name) - || context.has_interface(name) - || context.has_trait(name) - || context.has_enum(name) - || values.class_exists(name)? - || eval_runtime_interface_exists(name, values)? - || values.trait_exists(name)? - || values.enum_exists(name)? - { - return Err(EvalStatus::RuntimeFatal); - } - let class = expand_eval_class_traits(class, context)?.with_readonly_properties(); - let class = &class; - validate_eval_class_modifiers(class, context, values)?; - let native_parent = validate_eval_class_parent(class, context, values)?; - for interface in class.interfaces() { - if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { - return Err(EvalStatus::RuntimeFatal); - } - } - validate_eval_class_does_not_implement_throwable_interfaces(class, context)?; - validate_eval_class_does_not_implement_enum_interfaces(class, context)?; - validate_declared_class_interface_members(class, context)?; - validate_declared_class_builtin_interface_members(class, context)?; - validate_declared_class_aot_interface_members(class, context, values)?; - if !class.is_abstract() { - validate_concrete_class_requirements(class, context)?; - validate_concrete_class_builtin_interface_requirements(class, context)?; - validate_concrete_class_aot_parent_requirements(class, context, values)?; - validate_concrete_class_aot_interface_requirements(class, context, values)?; - } - if context.define_class(class.clone()) { - if let Some(parent) = native_parent.as_deref() { - if !context.define_native_class_parent(class.name(), parent) { - return Err(EvalStatus::RuntimeFatal); - } - } - initialize_eval_declared_constants( - class.name(), - class.constants(), - context, - scope, - values, - )?; - initialize_eval_static_properties(class, context, scope, values) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Validates an eval class parent and returns an AOT parent name when the parent is runtime-backed. -fn validate_eval_class_parent( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(parent) = class.parent() else { - return Ok(None); - }; - let parent = context - .resolve_class_name(parent) - .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); - if let Some(parent_class) = context.class(&parent) { - if parent_class.is_final() - || parent_class.is_readonly_class() != class.is_readonly_class() - || context.class_is_a(&parent, class.name(), false) - { - return Err(EvalStatus::RuntimeFatal); - } - return Ok(None); - } - let Some((parent_is_final, parent_is_readonly)) = - eval_reflection_aot_class_inheritance_modifiers(&parent, values)? - else { - return Err(EvalStatus::RuntimeFatal); - }; - if parent_is_final - || parent_is_readonly != class.is_readonly_class() - || native_class_is_a(&parent, class.name(), context) - { - return Err(EvalStatus::RuntimeFatal); - } - Ok(Some(parent)) -} - -/// Registers one eval anonymous class expression if this execution has not seen it yet. -pub(in crate::interpreter) fn ensure_eval_anonymous_class_decl( - class: &EvalClass, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if !class.is_anonymous() { - return Err(EvalStatus::RuntimeFatal); - } - if let Some(existing) = context.class(class.name()) { - return if existing.is_anonymous() { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - }; - } - execute_class_decl_stmt(class, context, scope, values) -} - -/// Registers an eval-declared enum and materializes its singleton cases. -pub(in crate::interpreter) fn execute_enum_decl_stmt( - enum_decl: &EvalEnum, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let name = enum_decl.name().trim_start_matches('\\'); - if context.has_enum(name) - || context.has_class(name) - || context.has_interface(name) - || context.has_trait(name) - || values.enum_exists(name)? - || values.class_exists(name)? - || eval_runtime_interface_exists(name, values)? - || values.trait_exists(name)? - { - return Err(EvalStatus::RuntimeFatal); - } - validate_eval_enum_direct_method_declarations(enum_decl)?; - let enum_decl = expand_eval_enum_traits(enum_decl, context)?; - let enum_decl = &enum_decl; - validate_eval_enum_decl(enum_decl, context, values)?; - if context.define_enum(enum_decl.clone()) { - initialize_eval_declared_constants( - enum_decl.name(), - enum_decl.constants(), - context, - scope, - values, - )?; - initialize_eval_enum_cases(enum_decl, context, scope, values) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Expands eval trait uses into enum metadata while rejecting imported properties. -fn expand_eval_enum_traits( - enum_decl: &EvalEnum, - context: &ElephcEvalContext, -) -> Result { - if enum_decl.traits().is_empty() { - return Ok(enum_decl.clone()); - } - let enum_class = enum_decl.as_class_metadata(); - validate_eval_trait_adaptations(&enum_class, context)?; - let mut enum_method_names = class_method_name_set(&enum_class); - insert_eval_enum_synthetic_method_names(enum_decl, &mut enum_method_names); - let mut trait_method_names = std::collections::HashSet::new(); - let mut trait_properties = std::collections::HashMap::new(); - let mut trait_constants = std::collections::HashMap::new(); - let mut constants = Vec::new(); - let mut properties = Vec::new(); - let mut methods = Vec::new(); - for trait_name in enum_decl.traits() { - let Some(trait_decl) = context.trait_decl(trait_name) else { - return Err(EvalStatus::RuntimeFatal); - }; - append_eval_trait_constants( - trait_decl, - enum_decl.constants(), - &mut trait_constants, - &mut constants, - )?; - append_eval_trait_properties( - trait_decl, - &[], - &mut trait_properties, - &mut properties, - )?; - append_eval_trait_methods( - trait_decl, - enum_decl.trait_adaptations(), - &enum_method_names, - &mut trait_method_names, - &mut methods, - )?; - } - if !properties.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - constants.extend(enum_decl.constants().iter().cloned()); - methods.extend(enum_decl.methods().iter().cloned()); - let mut expanded = EvalEnum::with_members_traits_adaptations( - enum_decl.name().to_string(), - enum_decl.backing_type(), - enum_decl.interfaces().to_vec(), - enum_decl.cases().to_vec(), - constants, - methods, - enum_decl.traits().to_vec(), - enum_decl.trait_adaptations().to_vec(), - ) - .with_attributes(enum_decl.attributes().to_vec()); - if let Some(source_location) = enum_decl.source_location() { - expanded = expanded.with_source_location(source_location); - } - Ok(expanded) -} - -/// Adds PHP's enum-provided method names to the set that hides trait imports. -fn insert_eval_enum_synthetic_method_names( - enum_decl: &EvalEnum, - method_names: &mut std::collections::HashSet, -) { - method_names.insert(String::from("cases")); - if enum_decl.backing_type().is_some() { - method_names.insert(String::from("from")); - method_names.insert(String::from("tryfrom")); - } -} - -/// Validates enum metadata before it is inserted into the dynamic context. -fn validate_eval_enum_decl( - enum_decl: &EvalEnum, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - validate_eval_enum_attribute_targets(enum_decl)?; - validate_eval_declared_constants(enum_decl.constants())?; - validate_eval_enum_case_declarations(enum_decl)?; - validate_eval_enum_forbidden_magic_methods(enum_decl)?; - let enum_class = enum_decl.as_class_metadata(); - validate_eval_class_modifiers(&enum_class, context, values)?; - validate_eval_enum_interfaces(enum_decl, &enum_class, context, values)?; - validate_declared_class_builtin_interface_members(&enum_class, context)?; - validate_declared_class_aot_interface_members(&enum_class, context, values)?; - validate_concrete_class_builtin_interface_requirements(&enum_class, context)?; - validate_concrete_class_aot_interface_requirements(&enum_class, context, values)?; - validate_concrete_class_requirements(&enum_class, context) -} - -/// Validates PHP's special enum interface rules for one eval enum declaration. -fn validate_eval_enum_interfaces( - enum_decl: &EvalEnum, - enum_class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for interface in enum_decl.interfaces() { - if eval_builtin_enum_interface_name(interface) { - return Err(EvalStatus::RuntimeFatal); - } - if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { - return Err(EvalStatus::RuntimeFatal); - } - } - validate_eval_class_does_not_implement_throwable_interfaces(enum_class, context)?; - if enum_decl.backing_type().is_none() - && pending_class_interface_names(enum_class, context) - .iter() - .any(|interface| eval_builtin_backed_enum_interface_name(interface)) - { - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Validates enum case names and pure/backed declaration shape. -fn validate_eval_enum_case_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { - let mut case_names = std::collections::HashSet::new(); - let constant_names = enum_decl - .constants() - .iter() - .map(|constant| constant.name().to_string()) - .collect::>(); - for case in enum_decl.cases() { - validate_eval_non_method_attribute_targets(case.attributes())?; - if !case_names.insert(case.name().to_string()) { - return Err(EvalStatus::RuntimeFatal); - } - if constant_names.contains(case.name()) { - return Err(EvalStatus::RuntimeFatal); - } - match (enum_decl.backing_type(), case.value()) { - (None, None) | (Some(_), Some(_)) => {} - (None, Some(_)) | (Some(_), None) => return Err(EvalStatus::RuntimeFatal), - } - } - Ok(()) -} - -/// Validates direct enum methods that PHP reserves on enum declarations. -fn validate_eval_enum_direct_method_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { - for method in enum_decl.methods() { - if method.name().eq_ignore_ascii_case("cases") { - return Err(EvalStatus::RuntimeFatal); - } - if enum_decl.backing_type().is_some() - && (method.name().eq_ignore_ascii_case("from") - || method.name().eq_ignore_ascii_case("tryFrom")) - { - return Err(EvalStatus::RuntimeFatal); - } - if is_forbidden_eval_enum_magic_method(method.name()) { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Validates enum methods, including trait imports, that PHP forbids by magic name. -fn validate_eval_enum_forbidden_magic_methods(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { - for method in enum_decl.methods() { - if is_forbidden_eval_enum_magic_method(method.name()) { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Returns whether PHP forbids this magic method name inside enum declarations. -fn is_forbidden_eval_enum_magic_method(name: &str) -> bool { - [ - "__construct", - "__destruct", - "__clone", - "__get", - "__set", - "__isset", - "__unset", - "__sleep", - "__wakeup", - "__serialize", - "__unserialize", - "__toString", - "__debugInfo", - "__set_state", - ] - .iter() - .any(|method| name.eq_ignore_ascii_case(method)) -} - -/// Initializes enum singleton case objects for a newly declared eval enum. -fn initialize_eval_enum_cases( - enum_decl: &EvalEnum, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let mut backing_values = Vec::new(); - for case in enum_decl.cases() { - let backing_value = if let Some(value_expr) = case.value() { - let value = eval_expr(value_expr, context, scope, values)?; - validate_eval_enum_backing_value(enum_decl.backing_type(), value, values)?; - for existing in &backing_values { - let equal = values.compare(EvalBinOp::StrictEq, value, *existing)?; - if values.truthy(equal)? { - return Err(EvalStatus::RuntimeFatal); - } - } - backing_values.push(value); - Some(value) - } else { - None - }; - initialize_eval_enum_case(enum_decl, case, backing_value, context, values)?; - } - Ok(()) -} - -/// Validates that one evaluated enum backing value matches the declared backing type. -fn validate_eval_enum_backing_value( - backing_type: Option, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(backing_type) = backing_type else { - return Err(EvalStatus::RuntimeFatal); - }; - let tag = values.type_tag(value)?; - match backing_type { - EvalEnumBackingType::Int if tag == EVAL_TAG_INT => Ok(()), - EvalEnumBackingType::String if tag == EVAL_TAG_STRING => Ok(()), - EvalEnumBackingType::Int | EvalEnumBackingType::String => Err(EvalStatus::RuntimeFatal), - } -} - -/// Creates and stores one enum case singleton object. -fn initialize_eval_enum_case( - enum_decl: &EvalEnum, - case: &EvalEnumCase, - backing_value: Option, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let object = values.new_object("stdClass")?; - let identity = values.object_identity(object)?; - context.register_dynamic_object(identity, enum_decl.name()); - let name = values.string(case.name())?; - values.property_set(object, "name", name)?; - if let Some(value) = backing_value { - values.property_set(object, "value", value)?; - if let Some(replaced) = context.set_enum_case_value(enum_decl.name(), case.name(), value) { - values.release(replaced)?; - } - } - if let Some(replaced) = context.set_enum_case(enum_decl.name(), case.name(), object) { - values.release(replaced)?; - } - Ok(()) -} - -/// Initializes class-like constant cells for a newly declared eval class-like. -fn initialize_eval_declared_constants( - owner_name: &str, - constants: &[EvalClassConstant], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for constant in constants { - let value = eval_class_like_member_default( - owner_name, - constant.trait_origin(), - constant.value(), - context, - scope, - values, - )?; - if let Some(replaced) = context.set_class_constant_cell(owner_name, constant.name(), value) - { - values.release(replaced)?; - } - } - Ok(()) -} - -/// Evaluates a class-like constant or property initializer with PHP magic scope. -fn eval_class_like_member_default( - owner_name: &str, - trait_origin: Option<&str>, - default: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let trait_name = trait_origin.or_else(|| context.has_trait(owner_name).then_some(owner_name)); - context.push_class_like_member_magic_scope(owner_name, trait_name); - let result = eval_expr(default, context, scope, values); - context.pop_magic_scope(); - result -} - -/// Initializes static property cells for a newly declared eval class. -fn initialize_eval_static_properties( - class: &EvalClass, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for property in class - .properties() - .iter() - .filter(|property| property.is_static()) - { - let value = if let Some(default) = property.default() { - Some(eval_class_like_member_default( - class.name(), - property.trait_origin(), - default, - context, - scope, - values, - )?) - } else if property.property_type().is_none() { - Some(values.null()?) - } else { - None - }; - if let Some(value) = value { - if let Some(replaced) = - context.set_static_property(class.name(), property.name(), value) - { - values.release(replaced)?; - } - } - } - Ok(()) -} - -/// Registers an eval-declared interface in the dynamic interface table. -pub(in crate::interpreter) fn execute_interface_decl_stmt( - interface: &EvalInterface, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let name = interface.name().trim_start_matches('\\'); - if context.has_interface(name) - || context.has_class(name) - || context.has_enum(name) - || eval_runtime_interface_exists(name, values)? - || values.class_exists(name)? - || values.enum_exists(name)? - { - return Err(EvalStatus::RuntimeFatal); - } - for parent in interface.parents() { - if context - .interface_parent_names(parent) - .iter() - .any(|ancestor| ancestor.eq_ignore_ascii_case(name)) - { - return Err(EvalStatus::RuntimeFatal); - } - if !context.has_interface(parent) && !eval_runtime_interface_exists(parent, values)? { - return Err(EvalStatus::RuntimeFatal); - } - } - validate_eval_interface_attribute_targets(interface)?; - validate_eval_interface_override_attributes(interface, context, values)?; - validate_eval_declared_constants(interface.constants())?; - validate_eval_interface_constants(interface.constants())?; - validate_interface_constant_parent_redeclarations(interface, context, values)?; - if context.define_interface(interface.clone()) { - initialize_eval_declared_constants( - interface.name(), - interface.constants(), - context, - scope, - values, - ) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Registers an eval-declared trait in the dynamic trait table. -pub(in crate::interpreter) fn execute_trait_decl_stmt( - trait_decl: &EvalTrait, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let name = trait_decl.name().trim_start_matches('\\'); - if context.has_trait(name) - || context.has_class(name) - || context.has_interface(name) - || context.has_enum(name) - || values.trait_exists(name)? - || values.class_exists(name)? - || eval_runtime_interface_exists(name, values)? - || values.enum_exists(name)? - { - return Err(EvalStatus::RuntimeFatal); - } - let trait_decl = expand_eval_trait_traits(trait_decl, context)?; - validate_eval_trait_attribute_targets(&trait_decl)?; - validate_eval_declared_constants(trait_decl.constants())?; - validate_eval_magic_methods(trait_decl.methods())?; - if context.define_trait(trait_decl.clone()) { - initialize_eval_declared_constants( - trait_decl.name(), - trait_decl.constants(), - context, - scope, - values, - ) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Expands nested eval trait uses into the trait metadata registered by eval. -fn expand_eval_trait_traits( - trait_decl: &EvalTrait, - context: &ElephcEvalContext, -) -> Result { - if trait_decl.traits().is_empty() { - return Ok(trait_decl.clone()); - } - validate_eval_trait_decl_adaptations(trait_decl, context)?; - let trait_method_names = trait_method_name_set(trait_decl); - let mut imported_method_names = std::collections::HashSet::new(); - let mut imported_properties = std::collections::HashMap::new(); - let mut imported_constants = std::collections::HashMap::new(); - let mut constants = Vec::new(); - let mut properties = Vec::new(); - let mut methods = Vec::new(); - for used_trait_name in trait_decl.traits() { - let Some(used_trait_decl) = context.trait_decl(used_trait_name) else { - return Err(EvalStatus::RuntimeFatal); - }; - append_eval_trait_constants( - used_trait_decl, - trait_decl.constants(), - &mut imported_constants, - &mut constants, - )?; - append_eval_trait_properties( - used_trait_decl, - trait_decl.properties(), - &mut imported_properties, - &mut properties, - )?; - append_eval_trait_methods( - used_trait_decl, - trait_decl.trait_adaptations(), - &trait_method_names, - &mut imported_method_names, - &mut methods, - )?; - } - constants.extend(trait_decl.constants().iter().cloned()); - properties.extend(trait_decl.properties().iter().cloned()); - methods.extend(trait_decl.methods().iter().cloned()); - let mut expanded = EvalTrait::with_constants_traits_adaptations( - trait_decl.name().to_string(), - constants, - properties, - methods, - trait_decl.traits().to_vec(), - trait_decl.trait_adaptations().to_vec(), - ) - .with_attributes(trait_decl.attributes().to_vec()); - if let Some(source_location) = trait_decl.source_location() { - expanded = expanded.with_source_location(source_location); - } - Ok(expanded) -} - -/// Validates that trait-level adaptations reference directly used traits and methods. -fn validate_eval_trait_decl_adaptations( - trait_decl: &EvalTrait, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - for adaptation in trait_decl.trait_adaptations() { - match adaptation { - EvalTraitAdaptation::Alias { - trait_name, method, .. - } => validate_eval_trait_decl_adaptation_method( - trait_decl, - context, - trait_name.as_deref(), - method, - )?, - EvalTraitAdaptation::InsteadOf { - trait_name, - method, - instead_of, - } => { - let Some(trait_name) = trait_name.as_deref() else { - return Err(EvalStatus::RuntimeFatal); - }; - validate_eval_trait_decl_adaptation_method( - trait_decl, - context, - Some(trait_name), - method, - )?; - for suppressed in instead_of { - if eval_trait_used_trait_decl(trait_decl, context, suppressed).is_none() { - return Err(EvalStatus::RuntimeFatal); - } - if same_eval_class_name(suppressed, trait_name) { - return Err(EvalStatus::RuntimeFatal); - } - } - } - } - } - Ok(()) -} - -/// Validates one trait-level adaptation method target. -fn validate_eval_trait_decl_adaptation_method( - trait_decl: &EvalTrait, - context: &ElephcEvalContext, - trait_name: Option<&str>, - method: &str, -) -> Result<(), EvalStatus> { - if let Some(trait_name) = trait_name { - let Some(used_trait_decl) = eval_trait_used_trait_decl(trait_decl, context, trait_name) - else { - return Err(EvalStatus::RuntimeFatal); - }; - return trait_has_method(used_trait_decl, method) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal); - } - trait_decl - .traits() - .iter() - .filter_map(|trait_name| context.trait_decl(trait_name)) - .any(|used_trait_decl| trait_has_method(used_trait_decl, method)) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Returns a trait declaration only when the pending trait directly uses that trait. -fn eval_trait_used_trait_decl<'a>( - trait_decl: &EvalTrait, - context: &'a ElephcEvalContext, - trait_name: &str, -) -> Option<&'a EvalTrait> { - trait_decl - .traits() - .iter() - .any(|used_trait| same_eval_class_name(used_trait, trait_name)) - .then(|| context.trait_decl(trait_name)) - .flatten() -} - -/// Expands eval trait uses into the class metadata used by dynamic dispatch. -fn expand_eval_class_traits( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result { - if class.traits().is_empty() { - return Ok(class.clone()); - } - validate_eval_trait_adaptations(class, context)?; - let class_method_names = class_method_name_set(class); - let mut trait_method_names = std::collections::HashSet::new(); - let mut trait_properties = std::collections::HashMap::new(); - let mut trait_constants = std::collections::HashMap::new(); - let mut constants = Vec::new(); - let mut properties = Vec::new(); - let mut methods = Vec::new(); - for trait_name in class.traits() { - let Some(trait_decl) = context.trait_decl(trait_name) else { - return Err(EvalStatus::RuntimeFatal); - }; - append_eval_trait_constants( - trait_decl, - class.constants(), - &mut trait_constants, - &mut constants, - )?; - append_eval_trait_properties( - trait_decl, - class.properties(), - &mut trait_properties, - &mut properties, - )?; - append_eval_trait_methods( - trait_decl, - class.trait_adaptations(), - &class_method_names, - &mut trait_method_names, - &mut methods, - )?; - } - constants.extend(class.constants().iter().cloned()); - properties.extend(class.properties().iter().cloned()); - methods.extend(class.methods().iter().cloned()); - let mut expanded = EvalClass::with_class_modifiers_traits_adaptations_and_constants( - class.name().to_string(), - class.is_abstract(), - class.is_final(), - class.is_readonly_class(), - class.parent().map(str::to_string), - class.interfaces().to_vec(), - class.traits().to_vec(), - class.trait_adaptations().to_vec(), - constants, - properties, - methods, - ) - .with_attributes(class.attributes().to_vec()); - if class.is_anonymous() { - expanded = expanded.with_anonymous(); - } - Ok(expanded) -} - -/// Validates that trait adaptations reference used traits and existing methods. -fn validate_eval_trait_adaptations( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - for adaptation in class.trait_adaptations() { - match adaptation { - EvalTraitAdaptation::Alias { - trait_name, method, .. - } => { - validate_eval_trait_adaptation_method(class, context, trait_name.as_deref(), method)? - } - EvalTraitAdaptation::InsteadOf { - trait_name, - method, - instead_of, - } => { - let Some(trait_name) = trait_name.as_deref() else { - return Err(EvalStatus::RuntimeFatal); - }; - validate_eval_trait_adaptation_method(class, context, Some(trait_name), method)?; - for suppressed in instead_of { - if eval_used_trait_decl(class, context, suppressed).is_none() { - return Err(EvalStatus::RuntimeFatal); - } - if same_eval_class_name(suppressed, trait_name) { - return Err(EvalStatus::RuntimeFatal); - } - } - } - } - } - Ok(()) -} - -/// Validates one adaptation method target, allowing unqualified alias targets. -fn validate_eval_trait_adaptation_method( - class: &EvalClass, - context: &ElephcEvalContext, - trait_name: Option<&str>, - method: &str, -) -> Result<(), EvalStatus> { - if let Some(trait_name) = trait_name { - let Some(trait_decl) = eval_used_trait_decl(class, context, trait_name) else { - return Err(EvalStatus::RuntimeFatal); - }; - return trait_has_method(trait_decl, method) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal); - } - class - .traits() - .iter() - .filter_map(|trait_name| context.trait_decl(trait_name)) - .any(|trait_decl| trait_has_method(trait_decl, method)) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Returns a trait declaration only when the pending class directly uses that trait. -fn eval_used_trait_decl<'a>( - class: &EvalClass, - context: &'a ElephcEvalContext, - trait_name: &str, -) -> Option<&'a EvalTrait> { - class - .traits() - .iter() - .any(|used_trait| same_eval_class_name(used_trait, trait_name)) - .then(|| context.trait_decl(trait_name)) - .flatten() -} - -/// Returns whether a trait declares a method by PHP case-insensitive method name. -fn trait_has_method(trait_decl: &EvalTrait, method: &str) -> bool { - trait_decl - .methods() - .iter() - .any(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) -} - -/// Returns case-insensitive method names declared directly by a pending trait. -fn trait_method_name_set(trait_decl: &EvalTrait) -> std::collections::HashSet { - trait_decl - .methods() - .iter() - .map(|method| method.name().to_ascii_lowercase()) - .collect() -} - -/// Returns case-insensitive method names declared directly by a pending class. -fn class_method_name_set(class: &EvalClass) -> std::collections::HashSet { - class - .methods() - .iter() - .map(|method| method.name().to_ascii_lowercase()) - .collect() -} - -/// Appends trait constants while enforcing PHP-compatible same-name conflicts. -fn append_eval_trait_constants( - trait_decl: &EvalTrait, - class_constants: &[EvalClassConstant], - trait_constants: &mut std::collections::HashMap, - constants: &mut Vec, -) -> Result<(), EvalStatus> { - for constant in trait_decl.constants() { - if let Some(class_constant) = class_constants - .iter() - .find(|class_constant| class_constant.name() == constant.name()) - { - validate_eval_trait_constant_compatibility(class_constant, constant)?; - continue; - } - if let Some(existing) = trait_constants.get(constant.name()) { - validate_eval_trait_constant_compatibility(existing, constant)?; - continue; - } - let constant = constant - .clone() - .with_trait_origin(trait_decl.name().to_string()); - trait_constants.insert(constant.name().to_string(), constant.clone()); - constants.push(constant); - } - Ok(()) -} - -/// Validates that a same-name trait constant definition is compatible with PHP composition. -fn validate_eval_trait_constant_compatibility( - existing: &EvalClassConstant, - incoming: &EvalClassConstant, -) -> Result<(), EvalStatus> { - if existing.visibility() == incoming.visibility() - && existing.is_final() == incoming.is_final() - && existing.value() == incoming.value() - { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Appends trait properties while enforcing PHP-compatible same-name conflicts. -fn append_eval_trait_properties( - trait_decl: &EvalTrait, - class_properties: &[EvalClassProperty], - trait_properties: &mut std::collections::HashMap, - properties: &mut Vec, -) -> Result<(), EvalStatus> { - for property in trait_decl.properties() { - if let Some(class_property) = class_properties - .iter() - .find(|class_property| class_property.name() == property.name()) - { - validate_eval_trait_property_compatibility(class_property, property)?; - continue; - } - if let Some(existing) = trait_properties.get(property.name()) { - let resolved = resolve_eval_trait_property_conflict(existing, property)?; - if &resolved != existing { - trait_properties.insert(property.name().to_string(), resolved.clone()); - if let Some(slot) = properties - .iter_mut() - .find(|candidate| candidate.name() == property.name()) - { - *slot = resolved; - } - } - continue; - } - let property = property - .clone() - .with_trait_origin(trait_decl.name().to_string()); - trait_properties.insert(property.name().to_string(), property.clone()); - properties.push(property); - } - Ok(()) -} - -/// Validates that a same-name trait property definition is compatible with PHP composition. -fn validate_eval_trait_property_compatibility( - existing: &EvalClassProperty, - incoming: &EvalClassProperty, -) -> Result<(), EvalStatus> { - resolve_eval_trait_property_conflict(existing, incoming).map(|_| ()) -} - -/// Resolves compatible same-name properties imported from classes and traits. -fn resolve_eval_trait_property_conflict( - existing: &EvalClassProperty, - incoming: &EvalClassProperty, -) -> Result { - if existing.is_abstract() && !incoming.is_abstract() { - return class_property_satisfies_abstract_contract(incoming, existing) - .then(|| incoming.clone()) - .ok_or(EvalStatus::RuntimeFatal); - } - if incoming.is_abstract() && !existing.is_abstract() { - return class_property_satisfies_abstract_contract(existing, incoming) - .then(|| existing.clone()) - .ok_or(EvalStatus::RuntimeFatal); - } - if existing.is_abstract() && incoming.is_abstract() { - return eval_trait_abstract_properties_are_compatible(existing, incoming) - .then(|| merge_abstract_property_contracts(existing, incoming)) - .ok_or(EvalStatus::RuntimeFatal); - } - if eval_trait_concrete_properties_are_compatible(existing, incoming) { - Ok(existing.clone()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Returns whether two concrete same-name trait properties are identical enough to deduplicate. -fn eval_trait_concrete_properties_are_compatible( - existing: &EvalClassProperty, - incoming: &EvalClassProperty, -) -> bool { - existing.visibility() == incoming.visibility() - && existing.set_visibility() == incoming.set_visibility() - && existing.is_static() == incoming.is_static() - && existing.is_final() == incoming.is_final() - && existing.is_readonly() == incoming.is_readonly() - && existing.is_abstract() == incoming.is_abstract() - && existing.has_get_hook() == incoming.has_get_hook() - && existing.has_set_hook() == incoming.has_set_hook() - && existing.requires_get_hook() == incoming.requires_get_hook() - && existing.requires_set_hook() == incoming.requires_set_hook() - && existing.is_virtual() == incoming.is_virtual() - && existing.property_type() == incoming.property_type() - && existing.set_hook_type() == incoming.set_hook_type() - && existing.default() == incoming.default() -} - -/// Returns whether two abstract trait property contracts can be merged. -fn eval_trait_abstract_properties_are_compatible( - existing: &EvalClassProperty, - incoming: &EvalClassProperty, -) -> bool { - existing.visibility() == incoming.visibility() - && existing.set_visibility() == incoming.set_visibility() - && existing.is_static() == incoming.is_static() - && existing.is_final() == incoming.is_final() - && existing.is_readonly() == incoming.is_readonly() - && existing.property_type() == incoming.property_type() - && existing.set_hook_type() == incoming.set_hook_type() - && existing.default() == incoming.default() -} - -/// Appends trait methods unless the class provides a same-name method. -fn append_eval_trait_methods( - trait_decl: &EvalTrait, - trait_adaptations: &[EvalTraitAdaptation], - class_method_names: &std::collections::HashSet, - trait_method_names: &mut std::collections::HashSet, - methods: &mut Vec, -) -> Result<(), EvalStatus> { - for method in trait_decl.methods() { - if trait_method_suppressed_by_insteadof(trait_decl.name(), method.name(), trait_adaptations) - { - continue; - } - let key = method.name().to_ascii_lowercase(); - if class_method_names.contains(&key) { - continue; - } - let method = method - .clone() - .with_trait_origin(trait_decl.name().to_string()); - let method = apply_trait_visibility_adaptations( - trait_decl.name(), - &method, - trait_adaptations, - ); - if !trait_method_names.insert(key) { - return Err(EvalStatus::RuntimeFatal); - } - methods.push(method); - } - append_eval_trait_method_aliases( - trait_decl, - trait_adaptations, - class_method_names, - trait_method_names, - methods, - ) -} - -/// Appends trait method aliases declared with `as`. -fn append_eval_trait_method_aliases( - trait_decl: &EvalTrait, - trait_adaptations: &[EvalTraitAdaptation], - class_method_names: &std::collections::HashSet, - trait_method_names: &mut std::collections::HashSet, - methods: &mut Vec, -) -> Result<(), EvalStatus> { - for adaptation in trait_adaptations { - let EvalTraitAdaptation::Alias { - trait_name, - method, - alias: Some(alias), - visibility, - } = adaptation - else { - continue; - }; - if !trait_adaptation_target_matches( - trait_name.as_deref(), - method, - trait_decl.name(), - method, - ) { - continue; - } - let Some(source_method) = trait_decl - .methods() - .iter() - .find(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) - else { - if trait_name.is_some() { - return Err(EvalStatus::RuntimeFatal); - } - continue; - }; - let mut alias_method = source_method - .clone() - .with_trait_origin(trait_decl.name().to_string()) - .renamed(alias.clone()); - if let Some(visibility) = visibility { - alias_method = alias_method.with_visibility_override(*visibility); - } - let key = alias_method.name().to_ascii_lowercase(); - if class_method_names.contains(&key) { - continue; - } - if trait_method_names.contains(&key) - && source_method.name().eq_ignore_ascii_case(alias) - && alias_method.visibility() == source_method.visibility() - { - continue; - } - if !trait_method_names.insert(key) { - return Err(EvalStatus::RuntimeFatal); - } - methods.push(alias_method); - } - Ok(()) -} - -/// Returns whether an `insteadof` adaptation suppresses this trait method import. -fn trait_method_suppressed_by_insteadof( - trait_name: &str, - method_name: &str, - trait_adaptations: &[EvalTraitAdaptation], -) -> bool { - trait_adaptations.iter().any(|adaptation| { - let EvalTraitAdaptation::InsteadOf { - trait_name: selected_trait, - method, - instead_of, - } = adaptation - else { - return false; - }; - method.eq_ignore_ascii_case(method_name) - && instead_of - .iter() - .any(|suppressed| same_eval_class_name(suppressed, trait_name)) - && !selected_trait - .as_deref() - .is_some_and(|selected| same_eval_class_name(selected, trait_name)) - }) -} - -/// Applies visibility-only `as` adaptations to an imported trait method. -fn apply_trait_visibility_adaptations( - trait_name: &str, - method: &EvalClassMethod, - trait_adaptations: &[EvalTraitAdaptation], -) -> EvalClassMethod { - let mut method = method.clone(); - for adaptation in trait_adaptations { - let EvalTraitAdaptation::Alias { - trait_name: target_trait, - method: target_method, - alias: None, - visibility: Some(visibility), - } = adaptation - else { - continue; - }; - if trait_adaptation_target_matches( - target_trait.as_deref(), - target_method, - trait_name, - method.name(), - ) { - method = method.with_visibility_override(*visibility); - } - } - method -} - -/// Returns whether an adaptation target selects one trait method. -fn trait_adaptation_target_matches( - target_trait: Option<&str>, - target_method: &str, - trait_name: &str, - method_name: &str, -) -> bool { - target_method.eq_ignore_ascii_case(method_name) - && target_trait.map_or(true, |target_trait| { - same_eval_class_name(target_trait, trait_name) - }) -} - -/// Rejects non-enum classes that implement PHP's native enum interfaces. -fn validate_eval_class_does_not_implement_enum_interfaces( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - if pending_class_interface_names(class, context) - .iter() - .any(|interface| eval_builtin_enum_interface_name(interface)) - { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Rejects eval classes and enums that directly implement PHP's Throwable contract. -fn validate_eval_class_does_not_implement_throwable_interfaces( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - if pending_class_interface_names(class, context) - .iter() - .any(|interface| eval_builtin_throwable_interface_name(interface)) - { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Validates abstract/final modifiers on an eval-declared class and its methods. -fn validate_eval_class_modifiers( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if class.is_abstract() && class.is_final() { - return Err(EvalStatus::RuntimeFatal); - } - validate_eval_class_attribute_targets(class.attributes())?; - if class.is_readonly_class() && eval_class_has_allow_dynamic_properties_attribute(class) { - return Err(EvalStatus::RuntimeFatal); - } - validate_eval_declared_constants(class.constants())?; - for constant in class.constants() { - validate_constant_parent_redeclaration(class, constant, context, values)?; - } - validate_eval_declared_properties(class, context)?; - for property in class.properties() { - validate_property_parent_redeclaration(class, property, context, values)?; - } - for method in class.methods() { - validate_eval_method_attribute_targets(method.attributes())?; - validate_eval_magic_method(method)?; - if method.is_abstract() && method.is_final() { - return Err(EvalStatus::RuntimeFatal); - } - if method.is_abstract() && method.visibility() == EvalVisibility::Private { - return Err(EvalStatus::RuntimeFatal); - } - if method.is_static() && method.name().eq_ignore_ascii_case("__construct") { - return Err(EvalStatus::RuntimeFatal); - } - if method.is_abstract() && !class.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - validate_method_parent_override(class, method, context)?; - validate_method_aot_parent_override(class, method, context, values)?; - validate_eval_override_attribute(class, method, context, values)?; - } - Ok(()) -} - -/// Returns whether a class carries PHP's global `#[AllowDynamicProperties]` attribute. -fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool { - eval_attributes_have_global_builtin_attribute(class.attributes(), "AllowDynamicProperties") -} - -/// Bridge reflection flag for static generated/AOT members. -const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; - -/// Bridge reflection flag for public generated/AOT members. -const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; - -/// Bridge reflection flag for protected generated/AOT members. -const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; - -/// Bridge reflection flag for private generated/AOT members. -const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; - -/// Bridge reflection flag for final generated/AOT members. -const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; - -/// Bridge reflection flag for abstract generated/AOT members. -const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; - -/// Bridge reflection flag for readonly generated/AOT properties. -const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; - -/// Bridge reflection flag for protected-set generated/AOT properties. -const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; - -/// Bridge reflection flag for private-set generated/AOT properties. -const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; - -/// Method requirement discovered from generated/AOT interface metadata. -struct EvalAotInterfaceMethodRequirement { - owner: String, - name: String, - is_static: bool, - signature: Option, -} - -/// Abstract method requirement discovered from generated/AOT parent metadata. -struct EvalAotAbstractMethodRequirement { - owner: String, - is_static: bool, - visibility: EvalVisibility, - signature: Option, -} - -/// Abstract property requirement discovered from generated/AOT parent metadata. -struct EvalAotAbstractPropertyRequirement { - owner: String, - property: EvalClassProperty, -} - -/// Rejects builtin attributes that cannot target an eval-declared class. -fn validate_eval_class_attribute_targets( - attributes: &[EvalAttribute], -) -> Result<(), EvalStatus> { - if eval_attributes_have_global_builtin_attribute(attributes, "Override") { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Rejects builtin attributes that cannot target eval-declared interfaces. -fn validate_eval_interface_attribute_targets( - interface: &EvalInterface, -) -> Result<(), EvalStatus> { - validate_eval_non_class_attribute_targets(interface.attributes())?; - for property in interface.properties() { - validate_eval_non_method_attribute_targets(property.attributes())?; - } - for method in interface.methods() { - validate_eval_method_attribute_targets(method.attributes())?; - } - Ok(()) -} - -/// Validates PHP's global `#[Override]` marker on eval-declared interface methods. -fn validate_eval_interface_override_attributes( - interface: &EvalInterface, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let parent_requirements = eval_interface_parent_method_requirements(interface, context); - for method in interface.methods() { - if !eval_interface_method_has_global_builtin_attribute(method, "Override") { - continue; - } - if parent_requirements - .iter() - .any(|(_, requirement)| eval_interface_method_matches_requirement(method, requirement)) - { - continue; - } - if eval_aot_interface_parent_method_matches(interface, method, values)? { - continue; - } - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Returns method requirements inherited by one eval interface declaration. -fn eval_interface_parent_method_requirements( - interface: &EvalInterface, - context: &ElephcEvalContext, -) -> Vec<(String, EvalInterfaceMethod)> { - let mut requirements = Vec::new(); - for parent in interface.parents() { - if context.has_interface(parent) { - requirements.extend(context.interface_method_requirements_with_owners(parent)); - } - requirements.extend(builtin_interface_method_requirements(parent)); - } - requirements -} - -/// Returns whether a generated/AOT parent interface exposes a matching method. -fn eval_aot_interface_parent_method_matches( - interface: &EvalInterface, - method: &EvalInterfaceMethod, - values: &mut impl RuntimeValueOps, -) -> Result { - for parent in interface.parents() { - if !values.interface_exists(parent)? { - continue; - } - let parent = parent.trim_start_matches('\\'); - if let Some(flags) = values.reflection_method_flags(parent, method.name())? { - let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - return Ok(parent_method_is_static == method.is_static()); - } - } - Ok(false) -} - -/// Returns whether an interface method matches one inherited requirement signature kind. -fn eval_interface_method_matches_requirement( - method: &EvalInterfaceMethod, - requirement: &EvalInterfaceMethod, -) -> bool { - requirement.name().eq_ignore_ascii_case(method.name()) - && requirement.is_static() == method.is_static() -} - -/// Rejects builtin attributes that cannot target eval-declared traits. -fn validate_eval_trait_attribute_targets(trait_decl: &EvalTrait) -> Result<(), EvalStatus> { - validate_eval_non_class_attribute_targets(trait_decl.attributes())?; - for property in trait_decl.properties() { - validate_eval_non_method_attribute_targets(property.attributes())?; - } - for method in trait_decl.methods() { - validate_eval_method_attribute_targets(method.attributes())?; - } - Ok(()) -} - -/// Rejects builtin attributes that cannot target eval-declared enums. -fn validate_eval_enum_attribute_targets(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { - validate_eval_non_class_attribute_targets(enum_decl.attributes()) -} - -/// Rejects class-only or method-only builtin attributes on non-class declarations. -fn validate_eval_non_class_attribute_targets( - attributes: &[EvalAttribute], -) -> Result<(), EvalStatus> { - if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") - || eval_attributes_have_global_builtin_attribute(attributes, "Override") - { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Rejects class-only or method-only builtin attributes on non-method members. -fn validate_eval_non_method_attribute_targets( - attributes: &[EvalAttribute], -) -> Result<(), EvalStatus> { - if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") - || eval_attributes_have_global_builtin_attribute(attributes, "Override") - { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Rejects class-only builtin attributes on method declarations. -fn validate_eval_method_attribute_targets( - attributes: &[EvalAttribute], -) -> Result<(), EvalStatus> { - if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Returns whether the attribute list contains one global builtin attribute. -fn eval_attributes_have_global_builtin_attribute( - attributes: &[EvalAttribute], - builtin: &str, -) -> bool { - attributes - .iter() - .any(|attribute| eval_attribute_is_global_builtin(attribute, builtin)) -} - -/// Returns whether one attribute names a global builtin attribute class. -fn eval_attribute_is_global_builtin(attribute: &EvalAttribute, builtin: &str) -> bool { - attribute - .name() - .trim_start_matches('\\') - .eq_ignore_ascii_case(builtin) -} - -/// Validates PHP's global `#[Override]` marker on one eval-declared method. -fn validate_eval_override_attribute( - class: &EvalClass, - method: &EvalClassMethod, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if !eval_method_has_global_builtin_attribute(method, "Override") { - return Ok(()); - } - if eval_method_overrides_parent(class, method, context) - || eval_method_overrides_aot_parent(class, method, context, values)? - || eval_method_implements_interface(class, method, context, values)? - { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Returns whether a method has a global builtin marker attribute. -fn eval_method_has_global_builtin_attribute(method: &EvalClassMethod, builtin: &str) -> bool { - eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) -} - -/// Returns whether an interface method has a global builtin marker attribute. -fn eval_interface_method_has_global_builtin_attribute( - method: &EvalInterfaceMethod, - builtin: &str, -) -> bool { - eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) -} - -/// Returns whether one method overrides a non-private parent method. -fn eval_method_overrides_parent( - class: &EvalClass, - method: &EvalClassMethod, - context: &ElephcEvalContext, -) -> bool { - class - .parent() - .and_then(|parent| context.class_method(parent, method.name())) - .is_some_and(|(_, parent_method)| { - parent_method.visibility() != EvalVisibility::Private - && parent_method.is_static() == method.is_static() - }) -} - -/// Returns whether one method overrides a visible generated/AOT parent method. -fn eval_method_overrides_aot_parent( - class: &EvalClass, - method: &EvalClassMethod, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(parent) = pending_class_native_parent_name(class, context) else { - return Ok(false); - }; - if !values.class_exists(&parent)? { - return Ok(false); - } - let Some(flags) = values.reflection_method_flags(&parent, method.name())? else { - return Ok(false); - }; - let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - let parent_method_is_private = flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0; - Ok(!parent_method_is_private && parent_method_is_static == method.is_static()) -} - -/// Returns the nearest generated/AOT parent for a class not yet registered in context. -fn pending_class_native_parent_name( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Option { - let mut current = class.parent()?.to_string(); - let mut seen = std::collections::HashSet::new(); - loop { - let resolved = context - .resolve_class_name(¤t) - .unwrap_or_else(|| current.trim_start_matches('\\').to_string()); - if !seen.insert(resolved.to_ascii_lowercase()) { - return None; - } - let Some(parent_class) = context.class(&resolved) else { - return Some(resolved.trim_start_matches('\\').to_string()); - }; - current = parent_class.parent()?.to_string(); - } -} - -/// Returns whether one method implements a direct or inherited interface method. -fn eval_method_implements_interface( - class: &EvalClass, - method: &EvalClassMethod, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if pending_class_interface_names(class, context) - .iter() - .filter(|interface| context.has_interface(interface)) - .any(|interface| { - context - .interface_method_requirements_with_owners(interface) - .into_iter() - .any(|(_, requirement)| { - requirement.name().eq_ignore_ascii_case(method.name()) - && requirement.is_static() == method.is_static() - }) - }) - { - return Ok(true); - } - Ok(pending_class_aot_interface_method_requirements(class, context, values)? - .iter() - .any(|requirement| { - requirement.name.eq_ignore_ascii_case(method.name()) - && requirement.is_static == method.is_static() - })) -} - -/// Validates PHP magic-method contracts for one eval class-like method list. -fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalStatus> { - for method in methods { - validate_eval_magic_method(method)?; - } - Ok(()) -} - -/// Validates staticness, visibility, arity, and declared return type for one eval magic method. -fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus> { - let name = method.name().to_ascii_lowercase(); - if validated_eval_magic_method_rejects_by_ref_params(&name) { - validate_magic_no_by_ref_params(method)?; - } - match name.as_str() { - "__tostring" => { - validate_magic_non_static(method)?; - validate_magic_public(method)?; - validate_magic_arity(method, 0)?; - validate_magic_declared_return_type(method, MagicReturnType::String)?; - } - "__get" | "__isset" | "__unset" => { - validate_magic_non_static(method)?; - validate_magic_arity(method, 1)?; - validate_magic_declared_param_type(method, 0, MagicParamType::String)?; - if method.name().eq_ignore_ascii_case("__isset") { - validate_magic_declared_return_type(method, MagicReturnType::Bool)?; - } else if method.name().eq_ignore_ascii_case("__unset") { - validate_magic_declared_return_type(method, MagicReturnType::Void)?; - } - } - "__set" | "__call" => { - validate_magic_non_static(method)?; - validate_magic_arity(method, 2)?; - validate_magic_declared_param_type(method, 0, MagicParamType::String)?; - if method.name().eq_ignore_ascii_case("__set") { - validate_magic_declared_return_type(method, MagicReturnType::Void)?; - } else { - validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; - } - } - "__callstatic" => { - validate_magic_static(method)?; - validate_magic_arity(method, 2)?; - validate_magic_declared_param_type(method, 0, MagicParamType::String)?; - validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; - } - "__sleep" | "__serialize" => { - validate_magic_non_static(method)?; - validate_magic_arity(method, 0)?; - validate_magic_declared_return_type(method, MagicReturnType::Array)?; - } - "__wakeup" => { - validate_magic_non_static(method)?; - validate_magic_arity(method, 0)?; - validate_magic_declared_return_type(method, MagicReturnType::Void)?; - } - "__unserialize" => { - validate_magic_non_static(method)?; - validate_magic_arity(method, 1)?; - validate_magic_declared_param_type(method, 0, MagicParamType::Array)?; - validate_magic_declared_return_type(method, MagicReturnType::Void)?; - } - "__debuginfo" => { - validate_magic_non_static(method)?; - validate_magic_arity(method, 0)?; - validate_magic_declared_return_type(method, MagicReturnType::NullableArray)?; - } - "__set_state" => { - validate_magic_static(method)?; - validate_magic_arity(method, 1)?; - validate_magic_declared_param_type(method, 0, MagicParamType::Array)?; - } - "__invoke" => { - validate_magic_non_static(method)?; - } - "__clone" | "__destruct" => { - validate_magic_non_static(method)?; - validate_magic_arity(method, 0)?; - if method.name().eq_ignore_ascii_case("__clone") { - validate_magic_declared_return_type(method, MagicReturnType::Void)?; - } else { - validate_magic_no_declared_return_type(method)?; - } - } - "__construct" => { - if method.is_static() { - return Err(EvalStatus::RuntimeFatal); - } - validate_magic_no_declared_return_type(method)?; - } - _ => {} - } - Ok(()) -} - -/// Returns whether PHP rejects by-reference parameters for this magic method. -fn validated_eval_magic_method_rejects_by_ref_params(name: &str) -> bool { - is_validated_eval_magic_method(name) && !matches!(name, "__construct" | "__invoke") -} - -/// Returns whether eval knows PHP declaration-time rules for this magic method. -fn is_validated_eval_magic_method(name: &str) -> bool { - matches!( - name, - "__tostring" - | "__get" - | "__isset" - | "__unset" - | "__set" - | "__call" - | "__callstatic" - | "__sleep" - | "__serialize" - | "__wakeup" - | "__unserialize" - | "__debuginfo" - | "__set_state" - | "__invoke" - | "__clone" - | "__destruct" - | "__construct" - ) -} - -/// Magic method return types that eval can validate from retained declarations. -#[derive(Clone, Copy)] -enum MagicReturnType { - Array, - Bool, - NullableArray, - String, - Void, -} - -/// Magic method parameter types that eval can validate from retained declarations. -#[derive(Clone, Copy)] -enum MagicParamType { - Array, - String, -} - -/// Rejects static declarations for magic methods that must be instance methods. -fn validate_magic_non_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { - if method.is_static() { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Rejects instance declarations for magic methods that must be static methods. -fn validate_magic_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { - if method.is_static() { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Rejects non-public declarations for public-only PHP magic methods. -fn validate_magic_public(method: &EvalClassMethod) -> Result<(), EvalStatus> { - if method.visibility() == EvalVisibility::Public { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Rejects magic methods whose arity differs from PHP's required shape. -fn validate_magic_arity(method: &EvalClassMethod, expected: usize) -> Result<(), EvalStatus> { - let has_variadic = method - .parameter_is_variadic() - .iter() - .any(|is_variadic| *is_variadic); - if method.params().len() == expected && !has_variadic { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Rejects by-reference parameters on PHP magic methods. -fn validate_magic_no_by_ref_params(method: &EvalClassMethod) -> Result<(), EvalStatus> { - if method - .parameter_is_by_ref() - .iter() - .any(|is_by_ref| *is_by_ref) - { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Rejects incompatible explicit parameter types on PHP magic methods. -fn validate_magic_declared_param_type( - method: &EvalClassMethod, - position: usize, - expected: MagicParamType, -) -> Result<(), EvalStatus> { - let Some(Some(parameter_type)) = method.parameter_types().get(position) else { - return Ok(()); - }; - if magic_param_type_matches(parameter_type, expected) { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } -} - -/// Returns whether one retained eval parameter type is exactly the expected magic atom. -fn magic_param_type_matches( - parameter_type: &EvalParameterType, - expected: MagicParamType, -) -> bool { - if parameter_type.allows_null() || parameter_type.is_intersection() { - return false; - } - let [variant] = parameter_type.variants() else { - return false; - }; - matches!( - (expected, variant), - (MagicParamType::Array, EvalParameterTypeVariant::Array) - | (MagicParamType::String, EvalParameterTypeVariant::String) - ) -} - -/// Rejects PHP magic methods that cannot declare any return type. -fn validate_magic_no_declared_return_type(method: &EvalClassMethod) -> Result<(), EvalStatus> { - if method.return_type().is_some() { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } -} - -/// Rejects incompatible explicit return types on PHP magic methods. -fn validate_magic_declared_return_type( - method: &EvalClassMethod, - expected: MagicReturnType, -) -> Result<(), EvalStatus> { - method.return_type().map_or(Ok(()), |return_type| { - if magic_return_type_matches(return_type, expected) { - Ok(()) - } else { - Err(EvalStatus::RuntimeFatal) - } - }) -} - -/// Returns whether one retained eval return type is exactly the expected magic return atom. -fn magic_return_type_matches( - return_type: &EvalParameterType, - expected: MagicReturnType, -) -> bool { - if return_type.is_intersection() { - return false; - } - if return_type.allows_null() && !matches!(expected, MagicReturnType::NullableArray) { - return false; - } - let [variant] = return_type.variants() else { - return false; - }; - matches!( - (expected, variant), - (MagicReturnType::Array, EvalParameterTypeVariant::Array) - | (MagicReturnType::Bool, EvalParameterTypeVariant::Bool) - | (MagicReturnType::NullableArray, EvalParameterTypeVariant::Array) - | (MagicReturnType::String, EvalParameterTypeVariant::String) - | (MagicReturnType::Void, EvalParameterTypeVariant::Void) - ) -} - -/// Validates property declarations that can be checked before class registration. -fn validate_eval_declared_properties( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - let mut names = std::collections::HashSet::new(); - for property in class.properties() { - validate_eval_non_method_attribute_targets(property.attributes())?; - if !names.insert(property.name().to_string()) { - return Err(EvalStatus::RuntimeFatal); - } - if property.is_abstract() - && (!class.is_abstract() - || property.is_static() - || property.is_final() - || property.is_readonly() - || property.default().is_some() - || (!property.requires_get_hook() && !property.requires_set_hook())) - { - return Err(EvalStatus::RuntimeFatal); - } - if property.is_static() && property.is_readonly() { - return Err(EvalStatus::RuntimeFatal); - } - if let Some(set_visibility) = property.set_visibility() { - if property.is_static() || property.property_type().is_none() { - return Err(EvalStatus::RuntimeFatal); - } - if property_visibility_rank(set_visibility) - > property_visibility_rank(property.visibility()) - { - return Err(EvalStatus::RuntimeFatal); - } - } - if property.is_final() && property.visibility() == EvalVisibility::Private { - return Err(EvalStatus::RuntimeFatal); - } - if property.is_readonly() && property.property_type().is_none() { - return Err(EvalStatus::RuntimeFatal); - } - if property.is_readonly() && property.default().is_some() { - return Err(EvalStatus::RuntimeFatal); - } - if (property.has_get_hook() || property.has_set_hook()) - && (property.is_static() || property.is_readonly() || property.default().is_some()) - { - return Err(EvalStatus::RuntimeFatal); - } - validate_eval_property_set_hook_parameter_type(class, property, context)?; - } - Ok(()) -} - -/// Validates that an explicit set-hook parameter type can accept every property value. -fn validate_eval_property_set_hook_parameter_type( - class: &EvalClass, - property: &EvalClassProperty, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - let Some(set_hook_type) = property.set_hook_type() else { - return Ok(()); - }; - let Some(property_type) = property.property_type() else { - return Err(EvalStatus::RuntimeFatal); - }; - let set_hook_types = vec![Some(set_hook_type.clone())]; - let property_types = vec![Some(property_type.clone())]; - method_parameter_type_signature_accepts( - &set_hook_types, - &[], - class.name(), - &property_types, - &[], - class.name(), - 1, - Some(class), - context, - ) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Validates one property declaration against inherited eval property metadata. -fn validate_property_parent_redeclaration( - class: &EvalClass, - property: &EvalClassProperty, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(parent) = class.parent() else { - return Ok(()); - }; - if let Some((parent_declaring_class, parent_property)) = - context.class_property(parent, property.name()) - { - if parent_property.visibility() == EvalVisibility::Private { - return Ok(()); - } - if parent_property.is_final() - || parent_property.set_visibility() == Some(EvalVisibility::Private) - { - return Err(EvalStatus::RuntimeFatal); - } - if parent_property.is_static() != property.is_static() - || (parent_property.is_readonly() && !property.is_readonly()) - || property_visibility_rank(property.visibility()) - < property_visibility_rank(parent_property.visibility()) - || property_visibility_rank(property.write_visibility()) - < property_visibility_rank(parent_property.write_visibility()) - || !property_type_signature_matches( - property.property_type(), - class.name(), - parent_property.property_type(), - &parent_declaring_class, - Some(class), - context, - ) - { - return Err(EvalStatus::RuntimeFatal); - } - return Ok(()); - } - validate_property_aot_parent_redeclaration(parent, class, property, context, values) -} - -/// Validates one property declaration against inherited generated/AOT property metadata. -fn validate_property_aot_parent_redeclaration( - parent: &str, - class: &EvalClass, - property: &EvalClassProperty, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if context.has_class(parent) || !values.class_exists(parent)? { - return Ok(()); - } - let parent = parent.trim_start_matches('\\'); - let Some(flags) = values.reflection_property_flags(parent, property.name())? else { - return Ok(()); - }; - if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - return Ok(()); - } - if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 - || flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 - { - return Err(EvalStatus::RuntimeFatal); - } - let parent_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - let parent_visibility = eval_aot_property_visibility(flags); - let parent_write_visibility = eval_aot_property_write_visibility(flags, parent_visibility); - let parent_is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; - let parent_declaring_class = - eval_aot_property_declaring_class(parent, property.name(), values)?; - let parent_property_type = context - .native_property_type(&parent_declaring_class, property.name()) - .or_else(|| context.native_property_type(parent, property.name())); - if parent_is_static != property.is_static() - || (parent_is_readonly && !property.is_readonly()) - || property_visibility_rank(property.visibility()) - < property_visibility_rank(parent_visibility) - || property_visibility_rank(property.write_visibility()) - < property_visibility_rank(parent_write_visibility) - || !property_type_signature_matches( - property.property_type(), - class.name(), - parent_property_type.as_ref(), - &parent_declaring_class, - Some(class), - context, - ) - { - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Returns the eval visibility represented by generated/AOT property reflection flags. -fn eval_aot_property_visibility(flags: u64) -> EvalVisibility { - if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - } -} - -/// Returns the eval write visibility represented by generated/AOT property flags. -fn eval_aot_property_write_visibility( - flags: u64, - read_visibility: EvalVisibility, -) -> EvalVisibility { - if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { - EvalVisibility::Protected - } else { - read_visibility - } -} - -/// Returns the generated/AOT declaring class for one reflected property. -fn eval_aot_property_declaring_class( - class_name: &str, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - values - .reflection_property_declaring_class(class_name, property_name) - .map(|declaring_class| declaring_class.unwrap_or_else(|| class_name.to_string())) -} - -/// Validates constant declarations that can be checked before registration. -fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { - let mut names = std::collections::HashSet::new(); - for constant in constants { - validate_eval_non_method_attribute_targets(constant.attributes())?; - if !names.insert(constant.name().to_string()) { - return Err(EvalStatus::RuntimeFatal); - } - if constant.is_final() && constant.visibility() == EvalVisibility::Private { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Validates declarations that are specific to PHP interface constants. -fn validate_eval_interface_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { - for constant in constants { - if constant.visibility() != EvalVisibility::Public { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Validates interface constants against inherited parent-interface constants. -fn validate_interface_constant_parent_redeclarations( - interface: &EvalInterface, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for constant in interface.constants() { - for parent in interface.parents() { - if let Some((_, parent_constant)) = context.interface_constant(parent, constant.name()) - { - if parent_constant.is_final() { - return Err(EvalStatus::RuntimeFatal); - } - } - validate_aot_interface_constant_redeclaration(parent, constant, values)?; - } - } - Ok(()) -} - -/// Validates one constant declaration against inherited eval constant metadata. -fn validate_constant_parent_redeclaration( - class: &EvalClass, - constant: &EvalClassConstant, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if let Some(parent) = class.parent() { - if let Some((_, parent_constant)) = context.class_constant(parent, constant.name()) { - if parent_constant.visibility() != EvalVisibility::Private - && (parent_constant.is_final() - || constant_visibility_rank(constant.visibility()) - < constant_visibility_rank(parent_constant.visibility())) - { - return Err(EvalStatus::RuntimeFatal); - } - } - validate_aot_class_constant_redeclaration(parent, constant, values)?; - } - for interface in pending_class_interface_names(class, context) { - if let Some((_, interface_constant)) = - context.interface_constant(&interface, constant.name()) - { - if interface_constant.is_final() - || constant_visibility_rank(constant.visibility()) - < constant_visibility_rank(interface_constant.visibility()) - { - return Err(EvalStatus::RuntimeFatal); - } - } - validate_aot_interface_constant_redeclaration(&interface, constant, values)?; - } - Ok(()) -} - -/// Validates a class constant redeclaration against a generated/AOT parent class constant. -fn validate_aot_class_constant_redeclaration( - parent: &str, - constant: &EvalClassConstant, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if !values.class_exists(parent)? { - return Ok(()); - } - validate_aot_constant_redeclaration(parent, constant, false, values) -} - -/// Validates a class/interface constant redeclaration against a generated/AOT interface constant. -fn validate_aot_interface_constant_redeclaration( - interface: &str, - constant: &EvalClassConstant, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if !values.interface_exists(interface)? { - return Ok(()); - } - validate_aot_constant_redeclaration(interface, constant, true, values) -} - -/// Applies PHP redeclaration rules to one generated/AOT constant metadata row. -fn validate_aot_constant_redeclaration( - class_like: &str, - constant: &EvalClassConstant, - interface_context: bool, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let class_like = class_like.trim_start_matches('\\'); - let Some(flags) = values.reflection_constant_flags(class_like, constant.name())? else { - return Ok(()); - }; - let inherited_visibility = eval_aot_constant_visibility(flags); - if !interface_context && inherited_visibility == EvalVisibility::Private { - return Ok(()); - } - if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 - || constant_visibility_rank(constant.visibility()) - < constant_visibility_rank(inherited_visibility) - { - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Returns the eval visibility represented by generated/AOT constant reflection flags. -fn eval_aot_constant_visibility(flags: u64) -> EvalVisibility { - if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - } -} - -/// Returns a comparable rank where larger means less restrictive constant visibility. -fn constant_visibility_rank(visibility: EvalVisibility) -> u8 { - match visibility { - EvalVisibility::Private => 1, - EvalVisibility::Protected => 2, - EvalVisibility::Public => 3, - } -} - -/// Validates declared or inherited class members that already cover eval interface contracts. -fn validate_declared_class_interface_members( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - for interface in pending_class_interface_names(class, context) { - if !context.has_interface(&interface) { - continue; - } - validate_declared_class_interface_methods(class, &interface, context)?; - validate_declared_class_interface_properties(class, &interface, context)?; - } - Ok(()) -} - -/// Validates declared class methods against PHP builtin runtime interface contracts. -fn validate_declared_class_builtin_interface_members( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - for (requirement_owner, requirement) in - pending_class_builtin_interface_method_requirements(class, context) - { - let Some((declaring_class, method)) = - pending_class_method(class, requirement.name(), context) - else { - continue; - }; - if method.visibility() != EvalVisibility::Public - || method.is_static() != requirement.is_static() - || !class_method_satisfies_builtin_interface_signature( - &method, - &declaring_class, - &requirement, - &requirement_owner, - Some(class), - context, - ) - { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Validates declared class methods against generated/AOT interface contracts. -fn validate_declared_class_aot_interface_members( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for requirement in pending_class_aot_interface_method_requirements(class, context, values)? { - let Some((declaring_class, method)) = pending_class_method(class, &requirement.name, context) - else { - continue; - }; - if !class_method_satisfies_aot_interface_requirement( - &method, - &declaring_class, - &requirement, - Some(class), - context, - false, - ) { - return Err(EvalStatus::RuntimeFatal); - } - } - for (requirement_owner, requirement) in - pending_class_aot_interface_property_requirements(class, context, values)? - { - let Some((declaring_class, property)) = - pending_class_property_with_owner(class, requirement.name(), context) - else { - continue; - }; - if !class_property_can_cover_interface_contract( - &property, - &declaring_class, - &requirement, - &requirement_owner, - Some(class), - context, - ) { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Validates class methods present for an eval interface, even on abstract classes. -fn validate_declared_class_interface_methods( - class: &EvalClass, - interface_name: &str, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - for (requirement_owner, requirement) in - context.interface_method_requirements_with_owners(interface_name) - { - let Some((declaring_class, method)) = - pending_class_method(class, requirement.name(), context) - else { - continue; - }; - if method.visibility() != EvalVisibility::Public - || method.is_static() != requirement.is_static() - || !class_method_satisfies_interface_signature( - &method, - &declaring_class, - &requirement, - &requirement_owner, - Some(class), - context, - ) - { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Validates class properties present for an eval interface, even on abstract classes. -fn validate_declared_class_interface_properties( - class: &EvalClass, - interface_name: &str, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - for (requirement_owner, requirement) in - context.interface_property_requirements_with_owners(interface_name) - { - let Some((declaring_class, property)) = - pending_class_property_with_owner(class, requirement.name(), context) - else { - continue; - }; - if !class_property_can_cover_interface_contract( - &property, - &declaring_class, - &requirement, - &requirement_owner, - Some(class), - context, - ) { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Returns a method from a pending class or one of its already registered parents. -fn pending_class_method( - class: &EvalClass, - method_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, EvalClassMethod)> { - if let Some(method) = class.method(method_name) { - return Some((class.name().to_string(), method.clone())); - } - class - .parent() - .and_then(|parent| context.class_method(parent, method_name)) -} - -/// Validates one method declaration against inherited eval method metadata. -fn validate_method_parent_override( - class: &EvalClass, - method: &EvalClassMethod, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - let Some(parent) = class.parent() else { - return Ok(()); - }; - let Some((parent_declaring_class, parent_method)) = context.class_method(parent, method.name()) - else { - return Ok(()); - }; - if parent_method.visibility() == EvalVisibility::Private { - return Ok(()); - } - if parent_method.is_static() != method.is_static() { - return Err(EvalStatus::RuntimeFatal); - } - if method_visibility_rank(method.visibility()) - < method_visibility_rank(parent_method.visibility()) - { - return Err(EvalStatus::RuntimeFatal); - } - if parent_method.is_final() { - return Err(EvalStatus::RuntimeFatal); - } - if method.is_abstract() && !parent_method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - if !class_method_signature_accepts( - method, - class.name(), - &parent_method, - &parent_declaring_class, - Some(class), - context, - ) { - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Validates one method declaration against inherited generated/AOT method metadata. -fn validate_method_aot_parent_override( - class: &EvalClass, - method: &EvalClassMethod, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(parent) = pending_class_native_parent_name(class, context) else { - return Ok(()); - }; - if !values.class_exists(&parent)? { - return Ok(()); - } - let Some(flags) = values.reflection_method_flags(&parent, method.name())? else { - return Ok(()); - }; - if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - return Ok(()); - } - let parent_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - if parent_is_static != method.is_static() { - return Err(EvalStatus::RuntimeFatal); - } - let parent_visibility = eval_aot_method_visibility(flags); - if method_visibility_rank(method.visibility()) < method_visibility_rank(parent_visibility) { - return Err(EvalStatus::RuntimeFatal); - } - if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 { - return Err(EvalStatus::RuntimeFatal); - } - if method.is_abstract() && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 { - return Err(EvalStatus::RuntimeFatal); - } - let Some(required) = eval_aot_method_signature_requirement( - &parent, - method.name(), - parent_is_static, - context, - values, - )? else { - return Ok(()); - }; - if !class_method_satisfies_interface_signature( - method, - class.name(), - &required, - &eval_aot_method_declaring_class(&parent, method.name(), values)?, - Some(class), - context, - ) { - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Returns the eval visibility represented by generated/AOT reflection flags. -fn eval_aot_method_visibility(flags: u64) -> EvalVisibility { - if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC != 0 { - EvalVisibility::Public - } else { - EvalVisibility::Public - } -} - -/// Returns the generated/AOT declaring class for one reflected method. -fn eval_aot_method_declaring_class( - class_name: &str, - method_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - values - .reflection_method_declaring_class(class_name, method_name) - .map(|declaring_class| declaring_class.unwrap_or_else(|| class_name.to_string())) -} - -/// Returns a generated/AOT parent method signature as an eval method requirement. -fn eval_aot_method_signature_requirement( - class_name: &str, - method_name: &str, - is_static: bool, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let declaring_class = eval_aot_method_declaring_class(class_name, method_name, values)?; - let signature = if is_static { - context.native_static_method_signature(&declaring_class, method_name) - } else { - context.native_method_signature(&declaring_class, method_name) - }; - Ok(signature.map(|signature| { - eval_native_signature_interface_method(method_name, is_static, &signature) - })) -} - -/// Returns whether one eval class method can accept every call accepted by its parent method. -fn class_method_signature_accepts( - method: &EvalClassMethod, - method_owner: &str, - required: &EvalClassMethod, - required_owner: &str, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, -) -> bool { - method_signature_accepts( - method.params().len(), - method.parameter_defaults(), - method.parameter_is_by_ref(), - method.parameter_is_variadic(), - required.params().len(), - required.parameter_defaults(), - required.parameter_is_by_ref(), - required.parameter_is_variadic(), - ) && method_parameter_type_signature_accepts( - method.parameter_types(), - method.parameter_is_variadic(), - method_owner, - required.parameter_types(), - required.parameter_is_variadic(), - required_owner, - required.params().len(), - pending_class, - context, - ) && method_return_type_signature_accepts( - method.return_type(), - method_owner, - required.return_type(), - required_owner, - pending_class, - context, - ) -} - -/// Returns a comparable rank where larger means less restrictive visibility. -fn method_visibility_rank(visibility: EvalVisibility) -> u8 { - match visibility { - EvalVisibility::Private => 1, - EvalVisibility::Protected => 2, - EvalVisibility::Public => 3, - } -} - -/// Validates that a concrete class has satisfied inherited abstract and interface requirements. -fn validate_concrete_class_requirements( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - if !pending_class_abstract_method_requirements(class, context).is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - if !pending_class_abstract_property_requirements(class, context)?.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - for interface in pending_class_interface_names(class, context) { - if context.has_interface(&interface) { - validate_class_implements_eval_interface(class, &interface, context)?; - } - } - Ok(()) -} - -/// Validates concrete class methods required by PHP builtin runtime interfaces. -fn validate_concrete_class_builtin_interface_requirements( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - for (requirement_owner, requirement) in - pending_class_builtin_interface_method_requirements(class, context) - { - if !class_has_builtin_interface_method(class, &requirement_owner, &requirement, context) { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Validates concrete class methods required by generated/AOT abstract parents. -fn validate_concrete_class_aot_parent_requirements( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if !pending_class_aot_parent_abstract_method_requirements(class, context, values)?.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - if !pending_class_aot_parent_abstract_property_requirements(class, context, values)?.is_empty() - { - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Validates concrete class methods required by generated/AOT runtime interfaces. -fn validate_concrete_class_aot_interface_requirements( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for requirement in pending_class_aot_interface_method_requirements(class, context, values)? { - if !class_has_aot_interface_method(class, &requirement, context) { - return Err(EvalStatus::RuntimeFatal); - } - } - for (requirement_owner, requirement) in - pending_class_aot_interface_property_requirements(class, context, values)? - { - if !class_has_interface_property(class, &requirement_owner, &requirement, context) { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Returns inherited abstract methods that the pending class has not concretized. -fn pending_class_abstract_method_requirements( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Vec { - let mut requirements = std::collections::HashMap::new(); - if let Some(parent) = class.parent() { - collect_class_abstract_method_requirements(parent, context, &mut requirements); - } - apply_class_abstract_method_requirements(class, &mut requirements); - requirements.into_values().collect() -} - -/// Returns inherited abstract properties that the pending class has not concretized. -fn pending_class_abstract_property_requirements( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Result, EvalStatus> { - let mut requirements = std::collections::HashMap::new(); - if let Some(parent) = class.parent() { - collect_class_abstract_property_requirements(parent, context, &mut requirements)?; - } - apply_class_abstract_property_requirements(class, &mut requirements)?; - Ok(requirements.into_values().collect()) -} - -/// Returns generated/AOT abstract parent methods the pending class has not concretized. -fn pending_class_aot_parent_abstract_method_requirements( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut requirements = std::collections::HashMap::new(); - if let Some(parent) = class.parent() { - collect_aot_parent_abstract_method_requirements( - parent, - context, - values, - &mut requirements, - )?; - } - apply_class_aot_parent_abstract_method_requirements(class, context, &mut requirements)?; - Ok(requirements.into_values().collect()) -} - -/// Returns generated/AOT abstract parent properties the pending class has not concretized. -fn pending_class_aot_parent_abstract_property_requirements( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut requirements = std::collections::HashMap::new(); - if let Some(parent) = class.parent() { - collect_aot_parent_abstract_property_requirements( - parent, - context, - values, - &mut requirements, - )?; - } - apply_class_aot_parent_abstract_property_requirements(class, context, &mut requirements)?; - Ok(requirements.into_values().collect()) -} - -/// Collects abstract method requirements from one declared eval class ancestry chain. -fn collect_class_abstract_method_requirements( - class_name: &str, - context: &ElephcEvalContext, - requirements: &mut std::collections::HashMap, -) { - let Some(class) = context.class(class_name) else { - return; - }; - if let Some(parent) = class.parent() { - collect_class_abstract_method_requirements(parent, context, requirements); - } - apply_class_abstract_method_requirements(class, requirements); -} - -/// Collects generated/AOT abstract method requirements through eval and AOT parents. -fn collect_aot_parent_abstract_method_requirements( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, - requirements: &mut std::collections::HashMap, -) -> Result<(), EvalStatus> { - let class_name = class_name.trim_start_matches('\\'); - if let Some(class) = context.class(class_name) { - if let Some(parent) = class.parent() { - collect_aot_parent_abstract_method_requirements( - parent, - context, - values, - requirements, - )?; - } - apply_class_aot_parent_abstract_method_requirements(class, context, requirements)?; - return Ok(()); - } - if values.class_exists(class_name)? { - collect_native_aot_abstract_method_requirements( - class_name, - context, - values, - requirements, - )?; - } - Ok(()) -} - -/// Collects abstract methods exposed by one generated/AOT class reflection row. -fn collect_native_aot_abstract_method_requirements( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, - requirements: &mut std::collections::HashMap, -) -> Result<(), EvalStatus> { - for method_name in eval_aot_method_names(class_name, values)? { - let Some(flags) = values.reflection_method_flags(class_name, &method_name)? else { - continue; - }; - let key = method_name.to_ascii_lowercase(); - if flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 { - requirements.remove(&key); - continue; - } - if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - continue; - } - let requirement = - eval_aot_abstract_method_requirement(class_name, &method_name, flags, context, values)?; - requirements.insert(key, requirement); - } - Ok(()) -} - -/// Collects generated/AOT abstract property requirements through eval and AOT parents. -fn collect_aot_parent_abstract_property_requirements( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, - requirements: &mut std::collections::HashMap, -) -> Result<(), EvalStatus> { - let class_name = class_name.trim_start_matches('\\'); - if let Some(class) = context.class(class_name) { - if let Some(parent) = class.parent() { - collect_aot_parent_abstract_property_requirements( - parent, - context, - values, - requirements, - )?; - } - apply_class_aot_parent_abstract_property_requirements(class, context, requirements)?; - return Ok(()); - } - if values.class_exists(class_name)? { - collect_native_aot_abstract_property_requirements( - class_name, - context, - values, - requirements, - )?; - } - Ok(()) -} - -/// Collects abstract properties exposed by one generated/AOT class metadata row. -fn collect_native_aot_abstract_property_requirements( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, - requirements: &mut std::collections::HashMap, -) -> Result<(), EvalStatus> { - for (owner, property) in context.native_abstract_property_requirements(class_name) { - let Some(flags) = values.reflection_property_flags(class_name, property.name())? else { - continue; - }; - if flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 - || flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 - { - continue; - } - let visibility = eval_aot_property_visibility(flags); - let write_visibility = eval_aot_property_write_visibility(flags, visibility); - let set_visibility = (write_visibility != visibility).then_some(write_visibility); - let requirement = EvalClassProperty::with_visibility(property.name(), visibility, None) - .with_type(property.property_type().cloned()) - .with_set_visibility(set_visibility) - .with_abstract_hook_contract(property.requires_get(), property.requires_set()); - requirements.insert( - property.name().to_string(), - EvalAotAbstractPropertyRequirement { - owner, - property: requirement, - }, - ); - } - Ok(()) -} - -/// Collects abstract property requirements from one declared eval class ancestry chain. -fn collect_class_abstract_property_requirements( - class_name: &str, - context: &ElephcEvalContext, - requirements: &mut std::collections::HashMap, -) -> Result<(), EvalStatus> { - let Some(class) = context.class(class_name) else { - return Ok(()); - }; - if let Some(parent) = class.parent() { - collect_class_abstract_property_requirements(parent, context, requirements)?; - } - apply_class_abstract_property_requirements(class, requirements) -} - -/// Applies one class's methods to the open abstract-method requirement set. -fn apply_class_abstract_method_requirements( - class: &EvalClass, - requirements: &mut std::collections::HashMap, -) { - for method in class.methods() { - let key = method.name().to_ascii_lowercase(); - if method.is_abstract() { - requirements.insert(key, method.clone()); - } else { - requirements.remove(&key); - } - } -} - -/// Applies one eval class's methods to the open AOT abstract-method requirement set. -fn apply_class_aot_parent_abstract_method_requirements( - class: &EvalClass, - context: &ElephcEvalContext, - requirements: &mut std::collections::HashMap, -) -> Result<(), EvalStatus> { - for method in class.methods() { - let key = method.name().to_ascii_lowercase(); - let Some(requirement) = requirements.get(&key) else { - continue; - }; - if !class_method_satisfies_aot_abstract_parent_requirement( - method, - class.name(), - requirement, - Some(class), - context, - false, - ) { - return Err(EvalStatus::RuntimeFatal); - } - if !method.is_abstract() { - requirements.remove(&key); - } - } - Ok(()) -} - -/// Applies one eval class's properties to the open AOT abstract-property requirement set. -fn apply_class_aot_parent_abstract_property_requirements( - class: &EvalClass, - context: &ElephcEvalContext, - requirements: &mut std::collections::HashMap, -) -> Result<(), EvalStatus> { - for property in class.properties() { - let key = property.name().to_string(); - let Some(requirement) = requirements.get(&key).map(|requirement| { - EvalAotAbstractPropertyRequirement { - owner: requirement.owner.clone(), - property: requirement.property.clone(), - } - }) else { - continue; - }; - if !class_property_satisfies_aot_abstract_parent_requirement( - property, - class.name(), - &requirement, - Some(class), - context, - ) { - return Err(EvalStatus::RuntimeFatal); - } - if property.is_abstract() { - requirements.insert( - key, - EvalAotAbstractPropertyRequirement { - owner: class.name().to_string(), - property: merge_abstract_property_contracts( - &requirement.property, - property, - ), - }, - ); - } else { - requirements.remove(&key); - } - } - Ok(()) -} - -/// Applies one class's properties to the open abstract-property requirement set. -fn apply_class_abstract_property_requirements( - class: &EvalClass, - requirements: &mut std::collections::HashMap, -) -> Result<(), EvalStatus> { - for property in class.properties() { - let key = property.name().to_string(); - if property.is_abstract() { - if let Some(existing) = requirements.get(&key) { - (property_contract_visibility_allows(existing, property) - && property_contract_write_visibility_allows(existing, property)) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal)?; - requirements.insert(key, merge_abstract_property_contracts(existing, property)); - } else { - requirements.insert(key, property.clone()); - } - } else if let Some(requirement) = requirements.get(&key) { - class_property_satisfies_abstract_contract(property, requirement) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal)?; - requirements.remove(&key); - } - } - Ok(()) -} - -/// Merges inherited and redeclared abstract property hook requirements. -fn merge_abstract_property_contracts( - inherited: &EvalClassProperty, - redeclared: &EvalClassProperty, -) -> EvalClassProperty { - redeclared.clone().with_abstract_hook_contract( - inherited.requires_get_hook() || redeclared.requires_get_hook(), - inherited.requires_set_hook() || redeclared.requires_set_hook(), - ) -} - -/// Returns whether a redeclared property keeps compatible visibility. -fn property_contract_visibility_allows( - inherited: &EvalClassProperty, - redeclared: &EvalClassProperty, -) -> bool { - property_visibility_rank(redeclared.visibility()) - >= property_visibility_rank(inherited.visibility()) -} - -/// Returns whether a redeclared property keeps compatible write visibility. -fn property_contract_write_visibility_allows( - inherited: &EvalClassProperty, - redeclared: &EvalClassProperty, -) -> bool { - !inherited.requires_set_hook() - || property_visibility_rank(redeclared.write_visibility()) - >= property_visibility_rank(inherited.write_visibility()) -} - -/// Returns whether a concrete property satisfies an abstract hook contract. -fn class_property_satisfies_abstract_contract( - property: &EvalClassProperty, - requirement: &EvalClassProperty, -) -> bool { - if property.is_abstract() - || property.is_static() - || property.property_type() != requirement.property_type() - || !property_contract_visibility_allows(requirement, property) - { - return false; - } - if requirement.requires_set_hook() { - return requirement.set_visibility() != Some(EvalVisibility::Private) - && property_contract_write_visibility_allows(requirement, property) - && (property.has_set_hook() || (!property.has_get_hook() && !property.is_readonly())); - } - requirement.requires_get_hook() -} - -/// Returns whether one property satisfies a generated/AOT abstract parent contract. -fn class_property_satisfies_aot_abstract_parent_requirement( - property: &EvalClassProperty, - property_owner: &str, - requirement: &EvalAotAbstractPropertyRequirement, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, -) -> bool { - let required = &requirement.property; - if property.is_static() != required.is_static() - || !property_contract_visibility_allows(required, property) - || !property_type_signature_matches( - property.property_type(), - property_owner, - required.property_type(), - &requirement.owner, - pending_class, - context, - ) - { - return false; - } - if property.is_abstract() { - return (!required.requires_get_hook() || property.requires_get_hook()) - && (!required.requires_set_hook() - || (property.requires_set_hook() - && property_contract_write_visibility_allows(required, property))); - } - if required.requires_get_hook() && !class_property_supports_interface_get(property) { - return false; - } - if required.requires_set_hook() { - return required.set_visibility() != Some(EvalVisibility::Private) - && property_contract_write_visibility_allows(required, property) - && class_property_supports_interface_set(property); - } - required.requires_get_hook() -} - -/// Returns a comparable rank where larger means less restrictive property visibility. -fn property_visibility_rank(visibility: EvalVisibility) -> u8 { - match visibility { - EvalVisibility::Private => 1, - EvalVisibility::Protected => 2, - EvalVisibility::Public => 3, - } -} - -/// Returns interface names inherited or directly declared by a pending eval class. -fn pending_class_interface_names(class: &EvalClass, context: &ElephcEvalContext) -> Vec { - let mut interfaces = Vec::new(); - let mut seen = std::collections::HashSet::new(); - if let Some(parent) = class.parent() { - for interface in context.class_interface_names(parent) { - push_pending_class_interface_name(&interface, &mut interfaces, &mut seen); - } - } - for interface in class.interfaces() { - push_pending_class_interface_tree(interface, context, &mut interfaces, &mut seen); - } - interfaces -} - -/// Adds one interface and its eval-declared parent interfaces to a pending class list. -fn push_pending_class_interface_tree( - interface: &str, - context: &ElephcEvalContext, - interfaces: &mut Vec, - seen: &mut std::collections::HashSet, -) { - push_pending_class_interface_name(interface, interfaces, seen); - for parent in context.interface_parent_names(interface) { - push_pending_class_interface_name(&parent, interfaces, seen); - } -} - -/// Adds one interface name once using PHP class-name case-insensitive matching. -fn push_pending_class_interface_name( - interface: &str, - interfaces: &mut Vec, - seen: &mut std::collections::HashSet, -) { - let interface = interface.trim_start_matches('\\'); - if seen.insert(interface.to_ascii_lowercase()) { - interfaces.push(interface.to_string()); - } -} - -/// Returns PHP builtin interface method requirements inherited by a pending class. -fn pending_class_builtin_interface_method_requirements( - class: &EvalClass, - context: &ElephcEvalContext, -) -> Vec<(String, EvalInterfaceMethod)> { - let mut requirements = Vec::new(); - for interface in pending_class_interface_names(class, context) { - requirements.extend(builtin_interface_method_requirements(&interface)); - } - requirements -} - -/// Returns generated/AOT interface method requirements inherited by a pending class. -fn pending_class_aot_interface_method_requirements( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut requirements = Vec::new(); - for interface in pending_class_interface_names(class, context) { - if context.has_interface(&interface) || !values.interface_exists(&interface)? { - continue; - } - requirements.extend(eval_aot_interface_method_requirements( - &interface, context, values, - )?); - } - Ok(requirements) -} - -/// Returns generated/AOT interface property requirements inherited by a pending class. -fn pending_class_aot_interface_property_requirements( - class: &EvalClass, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut requirements = Vec::new(); - for interface in pending_class_interface_names(class, context) { - if context.has_interface(&interface) || !values.interface_exists(&interface)? { - continue; - } - requirements.extend(context.native_interface_property_requirements(&interface)); - } - Ok(requirements) -} - -/// Returns generated/AOT method requirements for one runtime interface. -fn eval_aot_interface_method_requirements( - interface: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let interface = interface.trim_start_matches('\\'); - let method_names = eval_aot_interface_method_names(interface, values)?; - let mut requirements = Vec::new(); - for method_name in method_names { - if let Some(requirement) = - eval_aot_interface_method_requirement(interface, &method_name, context, values)? - { - requirements.push(requirement); - } - } - Ok(requirements) -} - -/// Returns generated/AOT method names for one runtime interface. -fn eval_aot_interface_method_names( - interface: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - eval_aot_method_names(interface, values) -} - -/// Returns generated/AOT method names for one runtime class-like symbol. -fn eval_aot_method_names( - class_like: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let method_names = values.reflection_method_names(class_like)?; - let names = eval_runtime_string_array_to_vec(method_names, values)?; - values.release(method_names)?; - Ok(names) -} - -/// Builds one generated/AOT abstract parent method requirement from metadata. -fn eval_aot_abstract_method_requirement( - class_name: &str, - method_name: &str, - flags: u64, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - let owner = eval_aot_method_declaring_class(class_name, method_name, values)?; - let signature = eval_aot_method_signature_requirement( - class_name, - method_name, - is_static, - context, - values, - )?; - Ok(EvalAotAbstractMethodRequirement { - owner, - is_static, - visibility: eval_aot_method_visibility(flags), - signature, - }) -} - -/// Builds one generated/AOT interface method requirement from reflection and signature metadata. -fn eval_aot_interface_method_requirement( - interface: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(flags) = values.reflection_method_flags(interface, method_name)? else { - return Ok(None); - }; - let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; - let owner = values - .reflection_method_declaring_class(interface, method_name)? - .unwrap_or_else(|| interface.to_string()); - let signature = if is_static { - context.native_static_method_signature(&owner, method_name) - } else { - context.native_method_signature(&owner, method_name) - }; - Ok(Some(EvalAotInterfaceMethodRequirement { - owner: owner.clone(), - name: method_name.to_string(), - is_static, - signature: signature.map(|signature| { - eval_native_signature_interface_method(method_name, is_static, &signature) - }), - })) -} - -/// Converts generated/AOT callable metadata into an eval interface method requirement. -fn eval_native_signature_interface_method( - method_name: &str, - is_static: bool, - signature: &NativeCallableSignature, -) -> EvalInterfaceMethod { - let param_count = signature.param_count(); - EvalInterfaceMethod::new( - method_name, - (0..param_count) - .map(|index| { - signature - .param_names() - .get(index) - .filter(|name| !name.is_empty()) - .cloned() - .unwrap_or_else(|| format!("arg{index}")) - }) - .collect(), - ) - .with_static(is_static) - .with_parameter_types( - (0..param_count) - .map(|index| signature.param_type(index).cloned()) - .collect(), - ) - .with_parameter_defaults( - (0..param_count) - .map(|index| { - signature - .param_default(index) - .map(|_| EvalExpr::Const(EvalConst::Null)) - }) - .collect(), - ) - .with_parameter_by_ref_flags( - (0..param_count) - .map(|index| signature.param_by_ref(index)) - .collect(), - ) - .with_parameter_variadic_flags( - (0..param_count) - .map(|index| signature.param_variadic(index)) - .collect(), - ) - .with_return_type(signature.return_type().cloned()) -} - -/// Copies a runtime string array into Rust-owned strings for declaration validation. -fn eval_runtime_string_array_to_vec( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let len = values.array_len(array)?; - let mut result = Vec::with_capacity(len); - for position in 0..len { - let key = values.int(position as i64)?; - let value = values.array_get(array, key)?; - result.push(eval_runtime_string_value(value, values)?); - } - Ok(result) -} - -/// Reads one runtime string cell as UTF-8 metadata. -fn eval_runtime_string_value( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Validates that one eval class provides methods required by one eval interface. -fn validate_class_implements_eval_interface( - class: &EvalClass, - interface_name: &str, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - for (requirement_owner, requirement) in - context.interface_method_requirements_with_owners(interface_name) - { - if !class_has_interface_method(class, &requirement_owner, &requirement, context) { - return Err(EvalStatus::RuntimeFatal); - } - } - for (requirement_owner, requirement) in - context.interface_property_requirements_with_owners(interface_name) - { - if !class_has_interface_property(class, &requirement_owner, &requirement, context) { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Returns whether a class or its eval parents satisfy one builtin interface method signature. -fn class_has_builtin_interface_method( - class: &EvalClass, - requirement_owner: &str, - requirement: &EvalInterfaceMethod, - context: &ElephcEvalContext, -) -> bool { - if let Some(method) = class.method(requirement.name()) { - return method.visibility() == EvalVisibility::Public - && method.is_static() == requirement.is_static() - && !method.is_abstract() - && class_method_satisfies_builtin_interface_signature( - method, - class.name(), - requirement, - requirement_owner, - Some(class), - context, - ); - } - class - .parent() - .and_then(|parent| context.class_method(parent, requirement.name())) - .is_some_and(|(declaring_class, method)| { - method.visibility() == EvalVisibility::Public - && method.is_static() == requirement.is_static() - && !method.is_abstract() - && class_method_satisfies_builtin_interface_signature( - &method, - &declaring_class, - requirement, - requirement_owner, - Some(class), - context, - ) - }) -} - -/// Returns whether a class or its eval parents satisfy one generated/AOT interface method. -fn class_has_aot_interface_method( - class: &EvalClass, - requirement: &EvalAotInterfaceMethodRequirement, - context: &ElephcEvalContext, -) -> bool { - if let Some((declaring_class, method)) = pending_class_method(class, &requirement.name, context) - { - return class_method_satisfies_aot_interface_requirement( - &method, - &declaring_class, - requirement, - Some(class), - context, - true, - ); - } - false -} - -/// Returns whether a class or its eval parents satisfy one interface method signature. -fn class_has_interface_method( - class: &EvalClass, - requirement_owner: &str, - requirement: &EvalInterfaceMethod, - context: &ElephcEvalContext, -) -> bool { - if let Some(method) = class.method(requirement.name()) { - return method.visibility() == EvalVisibility::Public - && method.is_static() == requirement.is_static() - && !method.is_abstract() - && class_method_satisfies_interface_signature( - method, - class.name(), - requirement, - requirement_owner, - Some(class), - context, - ); - } - class - .parent() - .and_then(|parent| context.class_method(parent, requirement.name())) - .is_some_and(|(declaring_class, method)| { - method.visibility() == EvalVisibility::Public - && method.is_static() == requirement.is_static() - && !method.is_abstract() - && class_method_satisfies_interface_signature( - &method, - &declaring_class, - requirement, - requirement_owner, - Some(class), - context, - ) - }) -} - -/// Returns whether one method satisfies a generated/AOT interface requirement. -fn class_method_satisfies_aot_interface_requirement( - method: &EvalClassMethod, - method_owner: &str, - requirement: &EvalAotInterfaceMethodRequirement, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, - require_concrete: bool, -) -> bool { - if method.visibility() != EvalVisibility::Public - || method.is_static() != requirement.is_static - || (require_concrete && method.is_abstract()) - { - return false; - } - requirement.signature.as_ref().is_none_or(|signature| { - class_method_satisfies_interface_signature( - method, - method_owner, - signature, - &requirement.owner, - pending_class, - context, - ) - }) -} - -/// Returns whether one method satisfies a generated/AOT abstract parent requirement. -fn class_method_satisfies_aot_abstract_parent_requirement( - method: &EvalClassMethod, - method_owner: &str, - requirement: &EvalAotAbstractMethodRequirement, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, - require_concrete: bool, -) -> bool { - if method.is_static() != requirement.is_static - || method_visibility_rank(method.visibility()) - < method_visibility_rank(requirement.visibility) - || (require_concrete && method.is_abstract()) - { - return false; - } - requirement.signature.as_ref().is_none_or(|signature| { - class_method_satisfies_interface_signature( - method, - method_owner, - signature, - &requirement.owner, - pending_class, - context, - ) - }) -} - -/// Returns whether one class method can accept every call required by an interface method. -fn class_method_satisfies_interface_signature( - method: &EvalClassMethod, - method_owner: &str, - requirement: &EvalInterfaceMethod, - requirement_owner: &str, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, -) -> bool { - class_method_satisfies_interface_signature_with_return_mode( - method, - method_owner, - requirement, - requirement_owner, - pending_class, - context, - false, - ) -} - -/// Returns whether one class method can satisfy a PHP builtin interface method contract. -fn class_method_satisfies_builtin_interface_signature( - method: &EvalClassMethod, - method_owner: &str, - requirement: &EvalInterfaceMethod, - requirement_owner: &str, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, -) -> bool { - class_method_satisfies_interface_signature_with_return_mode( - method, - method_owner, - requirement, - requirement_owner, - pending_class, - context, - true, - ) -} - -/// Returns whether one class method satisfies an interface signature with configurable return checks. -fn class_method_satisfies_interface_signature_with_return_mode( - method: &EvalClassMethod, - method_owner: &str, - requirement: &EvalInterfaceMethod, - requirement_owner: &str, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, - allow_missing_return_type: bool, -) -> bool { - method_signature_accepts( - method.params().len(), - method.parameter_defaults(), - method.parameter_is_by_ref(), - method.parameter_is_variadic(), - requirement.params().len(), - requirement.parameter_defaults(), - requirement.parameter_is_by_ref(), - requirement.parameter_is_variadic(), - ) && method_parameter_type_signature_accepts( - method.parameter_types(), - method.parameter_is_variadic(), - method_owner, - requirement.parameter_types(), - requirement.parameter_is_variadic(), - requirement_owner, - requirement.params().len(), - pending_class, - context, - ) && ((allow_missing_return_type && method.return_type().is_none()) - || method_return_type_signature_accepts( - method.return_type(), - method_owner, - requirement.return_type(), - requirement_owner, - pending_class, - context, - )) -} - -/// Returns whether one class property is compatible with one interface property contract. -fn class_property_can_cover_interface_contract( - property: &EvalClassProperty, - property_owner: &str, - requirement: &EvalInterfaceProperty, - requirement_owner: &str, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, -) -> bool { - if property.visibility() != EvalVisibility::Public || property.is_static() { - return false; - } - if !class_property_type_satisfies_interface_contract( - property.property_type(), - property.settable_type(), - property_owner, - requirement, - requirement_owner, - pending_class, - context, - ) { - return false; - } - if requirement.requires_get() && !class_property_supports_interface_get(property) { - return false; - } - if requirement.requires_set() { - return requirement.set_visibility() != Some(EvalVisibility::Private) - && property_visibility_rank(property.write_visibility()) - >= property_visibility_rank(requirement.write_visibility()) - && class_property_supports_interface_set(property); - } - requirement.requires_get() -} - -/// Returns whether one property type is compatible with interface get/set hook signatures. -fn class_property_type_satisfies_interface_contract( - property_type: Option<&EvalParameterType>, - settable_type: Option<&EvalParameterType>, - property_owner: &str, - requirement: &EvalInterfaceProperty, - requirement_owner: &str, - pending_class: Option<&EvalClass>, - context: &ElephcEvalContext, -) -> bool { - if requirement.requires_get() - && !method_return_type_signature_accepts( - property_type, - property_owner, - requirement.property_type(), - requirement_owner, - pending_class, - context, - ) - { - return false; - } - if requirement.requires_set() { - let property_types = vec![settable_type.cloned()]; - let requirement_types = vec![requirement.property_type().cloned()]; - return method_parameter_type_signature_accepts( - &property_types, - &[], - property_owner, - &requirement_types, - &[], - requirement_owner, - 1, - pending_class, - context, - ); - } - true -} - -/// Returns whether one property can satisfy an interface `get` hook. -fn class_property_supports_interface_get(property: &EvalClassProperty) -> bool { - property.has_get_hook() || property.requires_get_hook() || !property.is_virtual() -} - -/// Returns whether one property can satisfy an interface `set` hook. -fn class_property_supports_interface_set(property: &EvalClassProperty) -> bool { - property.has_set_hook() - || property.requires_set_hook() - || (!property.is_virtual() && !property.is_readonly()) -} - -/// Returns whether an implementing method accepts the full required arity range. -fn method_signature_accepts( - implementation_param_count: usize, - implementation_defaults: &[Option], - implementation_by_refs: &[bool], - implementation_variadics: &[bool], - required_param_count: usize, - required_defaults: &[Option], - required_by_refs: &[bool], - required_variadics: &[bool], -) -> bool { - let implementation_min = method_signature_min_arity( - implementation_param_count, - implementation_defaults, - implementation_variadics, - ); - let required_min = - method_signature_min_arity(required_param_count, required_defaults, required_variadics); - if implementation_min > required_min { - return false; - } - - let implementation_max = - method_signature_max_arity(implementation_param_count, implementation_variadics); - let required_max = method_signature_max_arity(required_param_count, required_variadics); - let arity_accepted = match (implementation_max, required_max) { - (None, _) => true, - (Some(_), None) => false, - (Some(implementation_max), Some(required_max)) => implementation_max >= required_max, - }; - arity_accepted - && method_signature_by_refs_accept( - implementation_by_refs, - implementation_variadics, - required_param_count, - required_by_refs, - required_variadics, - ) -} - -/// Returns whether pass-by-reference requirements are compatible across accepted args. -fn method_signature_by_refs_accept( - implementation_by_refs: &[bool], - implementation_variadics: &[bool], - required_param_count: usize, - required_by_refs: &[bool], - required_variadics: &[bool], -) -> bool { - (0..required_param_count).all(|position| { - method_signature_effective_by_ref( - implementation_by_refs, - implementation_variadics, - position, - ) == method_signature_effective_by_ref(required_by_refs, required_variadics, position) - }) -} - -/// Returns the by-reference mode that one signature applies at an argument position. -fn method_signature_effective_by_ref( - by_refs: &[bool], - variadics: &[bool], - position: usize, -) -> bool { - if let Some(variadic_index) = variadics.iter().position(|is_variadic| *is_variadic) { - if position >= variadic_index { - return by_refs.get(variadic_index).copied().unwrap_or(false); - } - } - by_refs.get(position).copied().unwrap_or(false) -} - -/// Returns the minimum argument count accepted by one eval method signature. -fn method_signature_min_arity( - param_count: usize, - defaults: &[Option], - variadics: &[bool], -) -> usize { - let fixed_count = variadics - .iter() - .position(|is_variadic| *is_variadic) - .unwrap_or(param_count); - (0..fixed_count) - .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) - .map_or(0, |position| position + 1) -} - -/// Returns the maximum argument count accepted by one eval method signature. -fn method_signature_max_arity(param_count: usize, variadics: &[bool]) -> Option { - if variadics.iter().any(|is_variadic| *is_variadic) { - None - } else { - Some(param_count) - } -} - -/// Returns whether a class or its eval parents satisfy one interface property contract. -fn class_has_interface_property( - class: &EvalClass, - requirement_owner: &str, - requirement: &EvalInterfaceProperty, - context: &ElephcEvalContext, -) -> bool { - pending_class_property_with_owner(class, requirement.name(), context).is_some_and( - |(declaring_class, property)| { - !property.is_abstract() - && class_property_can_cover_interface_contract( - &property, - &declaring_class, - requirement, - requirement_owner, - Some(class), - context, - ) - }, - ) -} - -/// Returns a property from a pending class or one of its already registered parents. -fn pending_class_property_with_owner( - class: &EvalClass, - property_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, EvalClassProperty)> { - if let Some(property) = class - .properties() - .iter() - .find(|property| property.name() == property_name) - { - return Some((class.name().to_string(), property.clone())); - } - class - .parent() - .and_then(|parent| context.class_property(parent, property_name)) -} - -/// Reads one object property while enforcing eval-declared member visibility. -pub(in crate::interpreter) fn eval_property_get_result( - object: RuntimeCellHandle, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Ok(identity) = values.object_identity(object) else { - return values.property_get(object, property_name); - }; - let Some(class) = context.dynamic_object_class(identity) else { - let class_name = eval_runtime_object_class_name(object, values)?; - if let Some((declaring_class, visibility, _, is_static)) = - eval_reflection_aot_property_access_metadata(&class_name, property_name, values)? - { - if !is_static && validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_throw_property_access_error( - &declaring_class, - property_name, - visibility, - context, - values, - ); - } - } - return values.property_get(object, property_name); - }; - let object_class_name = class.name().to_string(); - let mut storage_property_name = property_name.to_string(); - let mut declared_property_found = false; - if let Some((declaring_class, property)) = - eval_dynamic_property_for_access(&object_class_name, property_name, context) - { - declared_property_found = true; - if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { - if let Some(result) = - eval_magic_property_get(object, &object_class_name, property_name, context, values)? - { - return Ok(result); - } - return eval_throw_property_access_error( - &declaring_class, - property.name(), - property.visibility(), - context, - values, - ); - } - storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); - if property.has_get_hook() - && !current_eval_property_hook_is( - &declaring_class, - property.name(), - &property_hook_get_method(property.name()), - context, - ) - { - let (hook_class, hook_method) = context - .class_method( - &object_class_name, - &property_hook_get_method(property.name()), - ) - .ok_or(EvalStatus::RuntimeFatal)?; - return eval_dynamic_method_with_values( - &hook_class, - &object_class_name, - &hook_method, - object, - Vec::new(), - context, - values, - ); - } - if property.property_type().is_some() - && !context.dynamic_property_is_initialized(identity, &storage_property_name) - { - return eval_throw_uninitialized_property_error( - &declaring_class, - property.name(), - context, - values, - ); - } - } - if !declared_property_found - && eval_object_public_property_exists(object, property_name, values)? - { - return values.property_get(object, property_name); - } - if !declared_property_found { - if let Some((declaring_class, visibility, _, is_static)) = - eval_dynamic_class_native_property_metadata( - &object_class_name, - property_name, - context, - values, - )? - { - if !is_static { - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - if let Some(result) = eval_magic_property_get( - object, - &object_class_name, - property_name, - context, - values, - )? { - return Ok(result); - } - return eval_throw_property_access_error( - &declaring_class, - property_name, - visibility, - context, - values, - ); - } - return eval_with_native_bridge_scope(&declaring_class, context, || { - values.property_get(object, property_name) - }); - } - } - } - if !declared_property_found { - if let Some(result) = - eval_magic_property_get(object, &object_class_name, property_name, context, values)? - { - return Ok(result); - } - } - if let Some(target) = context - .dynamic_property_alias(identity, &storage_property_name) - .cloned() - { - return eval_reference_target_value(&target, context, values); - } - values.property_get(object, &storage_property_name) -} - -/// Writes one object property while enforcing eval-declared member visibility. -pub(in crate::interpreter) fn eval_property_set_result( - object: RuntimeCellHandle, - property_name: &str, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Ok(identity) = values.object_identity(object) else { - return values.property_set(object, property_name, value); - }; - let Some(class) = context.dynamic_object_class(identity) else { - let class_name = eval_runtime_object_class_name(object, values)?; - if let Some((declaring_class, _, write_visibility, is_static)) = - eval_reflection_aot_property_access_metadata(&class_name, property_name, values)? - { - if !is_static - && validate_eval_member_access(&declaring_class, write_visibility, context).is_err() - { - return eval_throw_property_access_error( - &declaring_class, - property_name, - write_visibility, - context, - values, - ); - } - } - return values.property_set(object, property_name, value); - }; - let object_class_name = class.name().to_string(); - if context.has_enum(&object_class_name) { - return Err(EvalStatus::RuntimeFatal); - } - let class_is_readonly = class.is_readonly_class(); - let mut storage_property_name = property_name.to_string(); - let mut declared_property_found = false; - if let Some((declaring_class, property)) = - eval_dynamic_property_for_access(&object_class_name, property_name, context) - { - declared_property_found = true; - if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { - if eval_magic_property_set( - object, - &object_class_name, - property_name, - value, - context, - values, - )? { - return Ok(()); - } - return eval_throw_property_access_error( - &declaring_class, - property.name(), - property.visibility(), - context, - values, - ); - } - if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { - return eval_throw_property_write_access_error( - &declaring_class, - &property, - context, - values, - ); - } - if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { - return eval_throw_readonly_property_modification_error( - &declaring_class, - property.name(), - context, - values, - ); - } - storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); - if property.has_set_hook() { - if !current_eval_property_hook_is( - &declaring_class, - property.name(), - &property_hook_set_method(property.name()), - context, - ) { - let (hook_class, hook_method) = context - .class_method( - &object_class_name, - &property_hook_set_method(property.name()), - ) - .ok_or(EvalStatus::RuntimeFatal)?; - let hook_result = eval_dynamic_method_with_values( - &hook_class, - &object_class_name, - &hook_method, - object, - vec![EvaluatedCallArg { - name: None, - value, - ref_target: None, - }], - context, - values, - )?; - values.release(hook_result)?; - return Ok(()); - } - } else if property.has_get_hook() { - return eval_throw_property_hook_readonly_error( - &declaring_class, - property.name(), - context, - values, - ); - } - } - if !declared_property_found { - if let Some((declaring_class, _, write_visibility, is_static)) = - eval_dynamic_class_native_property_metadata( - &object_class_name, - property_name, - context, - values, - )? - { - if !is_static { - if validate_eval_member_access(&declaring_class, write_visibility, context) - .is_err() - { - if eval_magic_property_set( - object, - &object_class_name, - property_name, - value, - context, - values, - )? { - return Ok(()); - } - return eval_throw_property_access_error( - &declaring_class, - property_name, - write_visibility, - context, - values, - ); - } - return eval_with_native_bridge_scope(&declaring_class, context, || { - values.property_set(object, property_name, value) - }); - } - } - } - if !declared_property_found - && eval_magic_property_set( - object, - &object_class_name, - property_name, - value, - context, - values, - )? - { - return Ok(()); - } - if !declared_property_found && class_is_readonly { - return eval_throw_dynamic_property_creation_error( - &object_class_name, - property_name, - context, - values, - ); - } - if let Some(target) = context - .dynamic_property_alias(identity, &storage_property_name) - .cloned() - { - eval_reference_target_write( - identity, - &storage_property_name, - target, - value, - context, - values, - )?; - context.mark_dynamic_property_initialized(identity, &storage_property_name); - return values.property_set(object, &storage_property_name, value); - } - values.property_set(object, &storage_property_name, value)?; - context.mark_dynamic_property_initialized(identity, &storage_property_name); - Ok(()) -} - -/// Binds one eval object property to a by-reference source parameter. -fn eval_property_reference_bind_result( - object: RuntimeCellHandle, - property_name: &str, - source_name: &str, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let identity = values.object_identity(object)?; - let class = context - .dynamic_object_class(identity) - .ok_or(EvalStatus::RuntimeFatal)?; - let object_class_name = class.name().to_string(); - if context.has_enum(&object_class_name) { - return Err(EvalStatus::RuntimeFatal); - } - let (declaring_class, property) = - eval_dynamic_property_for_access(&object_class_name, property_name, context) - .ok_or(EvalStatus::RuntimeFatal)?; - validate_eval_property_write_access(&declaring_class, &property, context)?; - if property.is_readonly() { - return Err(EvalStatus::RuntimeFatal); - } - let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); - let target = eval_property_reference_target( - identity, - &storage_property_name, - source_name, - context, - scope, - values, - )?; - let value = eval_reference_target_value(&target, context, values)?; - context.bind_dynamic_property_alias(identity, &storage_property_name, target); - values.property_set(object, &storage_property_name, value)?; - context.mark_dynamic_property_initialized(identity, &storage_property_name); - Ok(()) -} - -/// Resolves a local by-reference source into a persistent property alias target. -fn eval_property_reference_target( - object_identity: u64, - storage_property_name: &str, - source_name: &str, - context: &ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(target) = scope.reference_target(source_name).cloned() { - return Ok(target); - } - if context.current_function().is_some() { - let cell = - visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; - return Ok(EvalReferenceTarget::Cell { cell }); - } - let alias_name = eval_property_reference_alias_name(object_identity, storage_property_name); - for replaced in set_reference_alias(context, scope, &alias_name, source_name, values)? { - values.release(replaced)?; - } - Ok(EvalReferenceTarget::Variable { - scope: scope as *mut ElephcEvalScope, - name: alias_name, - }) -} - -/// Builds the hidden scope variable name that backs one property reference alias. -fn eval_property_reference_alias_name(object_identity: u64, storage_property_name: &str) -> String { - format!("\0elephc_property_ref:{object_identity}:{storage_property_name}") -} - -/// Reads the current value from a persistent reference target. -pub(in crate::interpreter) fn eval_reference_target_value( - target: &EvalReferenceTarget, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match target { - EvalReferenceTarget::Variable { scope, name } => { - let Some(scope) = (unsafe { scope.as_mut() }) else { - return Err(EvalStatus::RuntimeFatal); - }; - visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) - } - EvalReferenceTarget::ArrayElement { - scope, - array_name, - index, - } => { - let Some(scope) = (unsafe { scope.as_mut() }) else { - return Err(EvalStatus::RuntimeFatal); - }; - let array = - visible_scope_cell(context, scope, array_name).map_or_else(|| values.null(), Ok)?; - values.array_get(array, *index) - } - EvalReferenceTarget::NestedArrayElement { - array_target, - index, - } => { - let array = eval_reference_target_value(array_target, context, values)?; - values.array_get(array, *index) - } - EvalReferenceTarget::ObjectProperty { - object, - property, - access_scope, - } => { - let previous_scope = context.replace_execution_scope(access_scope.clone()); - let result = eval_property_get_result(*object, property, context, values); - context.replace_execution_scope(previous_scope); - result - } - EvalReferenceTarget::StaticProperty { - class_name, - property, - access_scope, - } => { - let previous_scope = context.replace_execution_scope(access_scope.clone()); - let result = eval_static_property_get_result(class_name, property, context, values); - context.replace_execution_scope(previous_scope); - result - } - EvalReferenceTarget::Cell { cell } => Ok(*cell), - EvalReferenceTarget::InvokerSlot { slot, source_tag } => { - eval_invoker_slot_ref_target_value(*slot, *source_tag, values) - } - } -} - -/// Writes a new value to a persistent reference target. -fn eval_reference_target_write( - object_identity: u64, - storage_property_name: &str, - target: EvalReferenceTarget, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if matches!(target, EvalReferenceTarget::Cell { .. }) { - context.bind_dynamic_property_alias( - object_identity, - storage_property_name, - EvalReferenceTarget::Cell { cell: value }, - ); - return Ok(()); - } - write_back_method_ref_target(&target, value, context, values) -} - -/// Evaluates PHP `isset($object->property)` without forcing `__get()` first. -pub(in crate::interpreter) fn eval_property_isset_result( - object: RuntimeCellHandle, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Ok(identity) = values.object_identity(object) else { - let value = values.property_get(object, property_name)?; - return Ok(!values.is_null(value)?); - }; - let Some(class) = context.dynamic_object_class(identity) else { - let value = values.property_get(object, property_name)?; - return Ok(!values.is_null(value)?); - }; - let object_class_name = class.name().to_string(); - if let Some((declaring_class, property)) = - eval_dynamic_property_for_access(&object_class_name, property_name, context) - { - if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { - let storage_property_name = - eval_instance_property_storage_name(&declaring_class, &property); - if property.property_type().is_some() - && !context.dynamic_property_is_initialized(identity, &storage_property_name) - { - return Ok(false); - } - let value = eval_property_get_result(object, property_name, context, values)?; - return Ok(!values.is_null(value)?); - } - return eval_magic_property_isset( - object, - &object_class_name, - property_name, - context, - values, - ) - .map(|result| result.unwrap_or(false)); - } - if eval_object_public_property_exists(object, property_name, values)? { - let value = values.property_get(object, property_name)?; - return Ok(!values.is_null(value)?); - } - if let Some((declaring_class, visibility, _, is_static)) = - eval_dynamic_class_native_property_metadata( - &object_class_name, - property_name, - context, - values, - )? - { - if !is_static { - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_magic_property_isset( - object, - &object_class_name, - property_name, - context, - values, - ) - .map(|result| result.unwrap_or(false)); - } - if !eval_with_native_bridge_scope(&declaring_class, context, || { - values.property_is_initialized(object, property_name) - })? { - return Ok(false); - } - let value = eval_with_native_bridge_scope(&declaring_class, context, || { - values.property_get(object, property_name) - })?; - return Ok(!values.is_null(value)?); - } - } - eval_magic_property_isset(object, &object_class_name, property_name, context, values) - .map(|result| result.unwrap_or(false)) -} - -/// Evaluates PHP `unset($object->property)` for eval-declared object receivers. -pub(in crate::interpreter) fn eval_property_unset_result( - object: RuntimeCellHandle, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Ok(identity) = values.object_identity(object) else { - return Ok(()); - }; - let Some(class) = context.dynamic_object_class(identity) else { - return Ok(()); - }; - let object_class_name = class.name().to_string(); - if context.has_enum(&object_class_name) { - return Err(EvalStatus::RuntimeFatal); - } - if let Some((declaring_class, property)) = - eval_dynamic_property_for_access(&object_class_name, property_name, context) - { - if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { - if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { - return eval_throw_property_unset_access_error( - &declaring_class, - &property, - context, - values, - ); - } - if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { - return eval_throw_readonly_property_unset_error( - &declaring_class, - property.name(), - context, - values, - ); - } - let storage_property_name = - eval_instance_property_storage_name(&declaring_class, &property); - context.remove_dynamic_property_alias(identity, &storage_property_name); - context.mark_dynamic_property_uninitialized(identity, &storage_property_name); - let null = values.null()?; - return values.property_set(object, &storage_property_name, null); - } - if eval_magic_property_unset(object, &object_class_name, property_name, context, values)? { - return Ok(()); - } - return Ok(()); - } - if eval_object_public_property_exists(object, property_name, values)? { - let null = values.null()?; - return values.property_set(object, property_name, null); - } - let _ = eval_magic_property_unset(object, &object_class_name, property_name, context, values)?; - Ok(()) -} - -/// Dispatches an undefined or inaccessible eval property read through `__get()`. -fn eval_magic_property_get( - object: RuntimeCellHandle, - object_class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, method)) = context.class_method(object_class_name, "__get") else { - return Ok(None); - }; - if method.is_static() || method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - let property = values.string(property_name)?; - eval_dynamic_method_with_values( - &declaring_class, - object_class_name, - &method, - object, - positional_args(vec![property]), - context, - values, - ) - .map(Some) -} - -/// Dispatches an undefined or inaccessible eval property write through `__set()`. -fn eval_magic_property_set( - object: RuntimeCellHandle, - object_class_name: &str, - property_name: &str, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, method)) = context.class_method(object_class_name, "__set") else { - return Ok(false); - }; - if method.is_static() || method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - let property = values.string(property_name)?; - let result = eval_dynamic_method_with_values( - &declaring_class, - object_class_name, - &method, - object, - positional_args(vec![property, value]), - context, - values, - )?; - values.release(result)?; - Ok(true) -} - -/// Dispatches an undefined or inaccessible eval property probe through `__isset()`. -fn eval_magic_property_isset( - object: RuntimeCellHandle, - object_class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, method)) = context.class_method(object_class_name, "__isset") else { - return Ok(None); - }; - if method.is_static() || method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - let property = values.string(property_name)?; - let result = eval_dynamic_method_with_values( - &declaring_class, - object_class_name, - &method, - object, - positional_args(vec![property]), - context, - values, - )?; - let truthy = values.truthy(result)?; - values.release(result)?; - Ok(Some(truthy)) -} - -/// Dispatches an undefined or inaccessible eval property unset through `__unset()`. -fn eval_magic_property_unset( - object: RuntimeCellHandle, - object_class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some((declaring_class, method)) = context.class_method(object_class_name, "__unset") else { - return Ok(false); - }; - if method.is_static() || method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - let property = values.string(property_name)?; - let result = eval_dynamic_method_with_values( - &declaring_class, - object_class_name, - &method, - object, - positional_args(vec![property]), - context, - values, - )?; - values.release(result)?; - Ok(true) -} - -/// Returns whether the object already has a public dynamic property with this exact name. -fn eval_object_public_property_exists( - object: RuntimeCellHandle, - property_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - let property_count = values.object_property_len(object)?; - for position in 0..property_count { - let key = values.object_property_iter_key(object, position)?; - let key_bytes = values.string_bytes(key); - values.release(key)?; - if key_bytes? == property_name.as_bytes() { - return Ok(true); - } - } - Ok(false) -} - -/// Validates that an object property may be used as a by-reference method argument. -pub(in crate::interpreter) fn validate_property_ref_target( - object: RuntimeCellHandle, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Ok(identity) = values.object_identity(object) else { - return Ok(()); - }; - let Some(class) = context.dynamic_object_class(identity) else { - return Ok(()); - }; - let object_class_name = class.name().to_string(); - if context.has_enum(&object_class_name) { - return Err(EvalStatus::RuntimeFatal); - } - if let Some((declaring_class, property)) = - eval_dynamic_property_for_access(&object_class_name, property_name, context) - { - validate_eval_member_access(&declaring_class, property.visibility(), context)?; - if property.is_readonly() { - return Err(EvalStatus::RuntimeFatal); - } - } - Ok(()) -} - -/// Returns true while executing the named hook accessor for one property. -pub(in crate::interpreter) fn current_eval_property_hook_is( - declaring_class: &str, - property_name: &str, - hook_method: &str, - context: &ElephcEvalContext, -) -> bool { - let Some(current_class) = context.current_class_scope() else { - return false; - }; - if !same_eval_class_name(current_class, declaring_class) { - return false; - } - let Some((_, method)) = context - .current_function() - .and_then(|function| function.rsplit_once("::")) - else { - return false; - }; - method.eq_ignore_ascii_case(hook_method) - || method.eq_ignore_ascii_case(&property_hook_get_method(property_name)) - || method.eq_ignore_ascii_case(&property_hook_set_method(property_name)) -} - -/// Returns the synthetic get-hook method name for one property. -pub(in crate::interpreter) fn property_hook_get_method(property_name: &str) -> String { - format!("__propget_{property_name}") -} - -/// Returns the synthetic set-hook method name for one property. -pub(in crate::interpreter) fn property_hook_set_method(property_name: &str) -> String { - format!("__propset_{property_name}") -} - -/// Rejects writes to readonly eval-declared properties outside their declaring constructor. -fn validate_eval_readonly_property_write( - declaring_class: &str, - property: &EvalClassProperty, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - if !property.is_readonly() { - return Ok(()); - } - current_eval_method_is_declaring_constructor(declaring_class, context) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Returns true while executing `__construct` for the property declaring class. -fn current_eval_method_is_declaring_constructor( - declaring_class: &str, - context: &ElephcEvalContext, -) -> bool { - let Some(current_class) = context.current_class_scope() else { - return false; - }; - if !same_eval_class_name(current_class, declaring_class) { - return false; - } - context - .current_function() - .and_then(|function| function.rsplit_once("::")) - .is_some_and(|(_, method)| method.eq_ignore_ascii_case("__construct")) -} - -/// Resolves the property metadata visible from the current class scope, if any. -fn eval_dynamic_property_for_access( - object_class_name: &str, - property_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, EvalClassProperty)> { - if let Some(current_class) = context.current_class_scope() { - if context.class_is_a(object_class_name, current_class, false) { - if let Some((declaring_class, property)) = - context.class_own_property(current_class, property_name) - { - if property.visibility() == EvalVisibility::Private { - return Some((declaring_class, property)); - } - } - } - } - context.class_property(object_class_name, property_name) -} - -/// Returns the physical storage name for an eval object property slot. -pub(in crate::interpreter) fn eval_instance_property_storage_name( - declaring_class: &str, - property: &EvalClassProperty, -) -> String { - if property.visibility() == EvalVisibility::Private { - format!( - "\0{}\0{}", - declaring_class.trim_start_matches('\\'), - property.name() - ) - } else { - property.name().to_string() - } -} - -/// Validates the visibility that applies to property writes, including asymmetric `set` visibility. -fn validate_eval_property_write_access( - declaring_class: &str, - property: &EvalClassProperty, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - validate_eval_member_access(declaring_class, property.write_visibility(), context) -} - -/// Throws PHP's inaccessible property error for eval-declared properties. -fn eval_throw_property_access_error( - declaring_class: &str, - property_name: &str, - visibility: EvalVisibility, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Cannot access {} property {}::${}", - eval_visibility_label(visibility), - declaring_class.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Throws PHP's write access error for eval-declared properties. -fn eval_throw_property_write_access_error( - declaring_class: &str, - property: &EvalClassProperty, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(set_visibility) = property.set_visibility() { - return eval_throw_error( - &format!( - "Cannot modify {}(set) property {}::${} from {}", - eval_visibility_label(set_visibility), - declaring_class.trim_start_matches('\\'), - property.name(), - eval_native_constructor_scope_label(context) - ), - context, - values, - ); - } - eval_throw_property_access_error( - declaring_class, - property.name(), - property.write_visibility(), - context, - values, - ) -} - -/// Throws PHP's unset access error for asymmetric eval-declared properties. -fn eval_throw_property_unset_access_error( - declaring_class: &str, - property: &EvalClassProperty, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(set_visibility) = property.set_visibility() { - return eval_throw_error( - &format!( - "Cannot unset {}(set) property {}::${} from {}", - eval_visibility_label(set_visibility), - declaring_class.trim_start_matches('\\'), - property.name(), - eval_native_constructor_scope_label(context) - ), - context, - values, - ); - } - eval_throw_property_access_error( - declaring_class, - property.name(), - property.write_visibility(), - context, - values, - ) -} - -/// Throws PHP's read-only property-hook write error. -fn eval_throw_property_hook_readonly_error( - declaring_class: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Property {}::${} is read-only", - declaring_class.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Throws PHP's readonly property assignment error for eval-declared properties. -fn eval_throw_readonly_property_modification_error( - declaring_class: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Cannot modify readonly property {}::${}", - declaring_class.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Throws PHP's readonly property unset error for eval-declared properties. -fn eval_throw_readonly_property_unset_error( - declaring_class: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Cannot unset readonly property {}::${}", - declaring_class.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Throws PHP's dynamic property creation error for readonly eval-declared classes. -fn eval_throw_dynamic_property_creation_error( - class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Cannot create dynamic property {}::${}", - class_name.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Throws PHP's undeclared static property error for static property access. -fn eval_throw_undeclared_static_property_error( - class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Access to undeclared static property {}::${}", - class_name.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Throws PHP's uninitialized typed instance property error. -fn eval_throw_uninitialized_property_error( - declaring_class: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Typed property {}::${} must not be accessed before initialization", - declaring_class.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Throws PHP's uninitialized typed static property error. -fn eval_throw_uninitialized_static_property_error( - declaring_class: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Typed static property {}::${} must not be accessed before initialization", - declaring_class.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Throws PHP's class-not-found error for unresolved static member receivers. -pub(in crate::interpreter) fn eval_throw_class_not_found_error( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!("Class \"{}\" not found", class_name.trim_start_matches('\\')), - context, - values, - ) -} - -/// Throws PHP's inaccessible constant error for eval-declared class constants. -fn eval_throw_constant_access_error( - declaring_class: &str, - constant_name: &str, - visibility: EvalVisibility, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Cannot access {} constant {}::{}", - eval_visibility_label(visibility), - declaring_class.trim_start_matches('\\'), - constant_name - ), - context, - values, - ) -} - -/// Throws PHP's inaccessible method error for eval-declared methods. -fn eval_throw_method_access_error( - declaring_class: &str, - method_name: &str, - visibility: EvalVisibility, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Call to {} method {}::{}() from {}", - eval_visibility_label(visibility), - declaring_class.trim_start_matches('\\'), - method_name, - eval_native_constructor_scope_label(context) - ), - context, - values, - ) -} - -/// Throws PHP's inaccessible clone-expression error for `__clone()` hooks. -fn eval_throw_clone_access_error( - declaring_class: &str, - visibility: EvalVisibility, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Call to {} {}::__clone() from {}", - eval_visibility_label(visibility), - declaring_class.trim_start_matches('\\'), - eval_native_constructor_scope_label(context) - ), - context, - values, - ) -} - -/// Throws PHP's error for calling an instance method through static syntax. -fn eval_throw_non_static_method_call_error( - declaring_class: &str, - method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Non-static method {}::{}() cannot be called statically", - declaring_class.trim_start_matches('\\'), - method_name - ), - context, - values, - ) -} - -/// Throws PHP's error for calling an abstract method directly. -fn eval_throw_abstract_method_call_error( - declaring_class: &str, - method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Cannot call abstract method {}::{}()", - declaring_class.trim_start_matches('\\'), - method_name - ), - context, - values, - ) -} - -/// Throws PHP's undefined method error after static magic fallback misses. -fn eval_throw_undefined_method_call_error( - class_name: &str, - method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Call to undefined method {}::{}()", - class_name.trim_start_matches('\\'), - method_name - ), - context, - values, - ) -} - -/// Throws PHP's error for invoking an object without `__invoke()`. -pub(in crate::interpreter) fn eval_throw_object_not_callable_error( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_throw_error( - &format!( - "Object of type {} is not callable", - class_name.trim_start_matches('\\') - ), - context, - values, - ) -} - -/// Reads one eval-declared static property after resolving the class-like receiver. -pub(in crate::interpreter) fn eval_static_property_get_result( - class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { - if !property.is_static() { - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { - return eval_throw_property_access_error( - &declaring_class, - property.name(), - property.visibility(), - context, - values, - ); - } - if let Some(target) = context - .static_property_alias(&declaring_class, property.name()) - .cloned() - { - return eval_reference_target_value(&target, context, values); - } - if let Some(value) = context.static_property(&declaring_class, property.name()) { - return Ok(value); - } - return eval_throw_uninitialized_static_property_error( - &declaring_class, - property.name(), - context, - values, - ); - } - if eval_static_member_context_owns_class(&class_name, context) { - if let Some(parent) = context.class_native_parent_name(&class_name) { - if let Some((declaring_class, visibility, _, is_static)) = - eval_reflection_aot_static_property_access_metadata( - &parent, - property_name, - context, - values, - )? - { - if !is_static { - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_throw_property_access_error( - &declaring_class, - property_name, - visibility, - context, - values, - ); - } - if let Some(target) = context - .static_property_alias(&declaring_class, property_name) - .cloned() - { - return eval_reference_target_value(&target, context, values); - } - if !eval_with_native_bridge_scope(&declaring_class, context, || { - values.static_property_is_initialized(&declaring_class, property_name) - })? { - return eval_throw_uninitialized_static_property_error( - &declaring_class, - property_name, - context, - values, - ); - } - if let Some(value) = eval_with_native_bridge_scope( - &declaring_class, - context, - || values.static_property_get(&declaring_class, property_name), - )? { - return Ok(value); - } - } - } - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if let Some((declaring_class, visibility, _, is_static)) = - eval_reflection_aot_static_property_access_metadata( - &class_name, - property_name, - context, - values, - )? - { - if is_static { - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_throw_property_access_error( - &declaring_class, - property_name, - visibility, - context, - values, - ); - } - if let Some(target) = context - .static_property_alias(&declaring_class, property_name) - .cloned() - { - return eval_reference_target_value(&target, context, values); - } - if !values.static_property_is_initialized(&declaring_class, property_name)? { - return eval_throw_uninitialized_static_property_error( - &declaring_class, - property_name, - context, - values, - ); - } - } - } - if let Some(value) = values.static_property_get(&class_name, property_name)? { - return Ok(value); - } - if eval_runtime_class_like_exists(&class_name, context, values)? { - eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) - } else { - eval_throw_class_not_found_error(&class_name, context, values) - } -} - -/// Returns whether a static property is PHP-`isset()` without throwing for missing properties. -pub(in crate::interpreter) fn eval_static_property_isset_result( - class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { - if !property.is_static() { - return Ok(false); - } - if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { - return Ok(false); - } - let value = if let Some(target) = context - .static_property_alias(&declaring_class, property.name()) - .cloned() - { - eval_reference_target_value(&target, context, values)? - } else { - let Some(value) = context.static_property(&declaring_class, property.name()) else { - return Ok(false); - }; - value - }; - return Ok(!values.is_null(value)?); - } - if eval_static_member_context_owns_class(&class_name, context) { - if let Some(parent) = context.class_native_parent_name(&class_name) { - if let Some((declaring_class, visibility, _, is_static)) = - eval_reflection_aot_static_property_access_metadata( - &parent, - property_name, - context, - values, - )? - { - if !is_static { - return Ok(false); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return Ok(false); - } - if let Some(target) = context - .static_property_alias(&declaring_class, property_name) - .cloned() - { - let value = eval_reference_target_value(&target, context, values)?; - return Ok(!values.is_null(value)?); - } - if !eval_with_native_bridge_scope(&declaring_class, context, || { - values.static_property_is_initialized(&declaring_class, property_name) - })? { - return Ok(false); - } - if let Some(value) = eval_with_native_bridge_scope( - &declaring_class, - context, - || values.static_property_get(&declaring_class, property_name), - )? { - return Ok(!values.is_null(value)?); - } - } - } - return Ok(false); - } - if let Some((declaring_class, visibility, _, is_static)) = - eval_reflection_aot_static_property_access_metadata( - &class_name, - property_name, - context, - values, - )? - { - if !is_static { - return Ok(false); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return Ok(false); - } - if let Some(target) = context - .static_property_alias(&declaring_class, property_name) - .cloned() - { - let value = eval_reference_target_value(&target, context, values)?; - return Ok(!values.is_null(value)?); - } - if !values.static_property_is_initialized(&declaring_class, property_name)? { - return Ok(false); - } - } else if !eval_runtime_class_like_exists(&class_name, context, values)? { - return eval_throw_class_not_found_error(&class_name, context, values); - } - if let Some(value) = values.static_property_get(&class_name, property_name)? { - return Ok(!values.is_null(value)?); - } - Ok(false) -} - -/// Throws PHP's catchable error for attempts to unset an existing static property target. -pub(in crate::interpreter) fn eval_static_property_unset_result( - class_name: &str, - property_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - if !eval_runtime_class_like_exists(&class_name, context, values)? { - return eval_throw_class_not_found_error(&class_name, context, values); - } - eval_throw_error( - &format!( - "Attempt to unset static property {}::${}", - class_name.trim_start_matches('\\'), - property_name - ), - context, - values, - ) -} - -/// Reads one eval-declared class constant after resolving the class-like receiver. -pub(in crate::interpreter) fn eval_class_constant_fetch_result( - class_name: &str, - constant_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(value) = eval_builtin_reflection_class_constant(class_name, constant_name, values)? - { - return Ok(value); - } - if let Some(value) = - eval_builtin_property_hook_type_case(class_name, constant_name, context, values)? - { - return Ok(value); - } - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - if let Some(case) = context.enum_case(&class_name, constant_name) { - return Ok(case); - } - if let Some((declaring_class, constant)) = context.class_constant(&class_name, constant_name) { - if validate_eval_member_access(&declaring_class, constant.visibility(), context).is_err() { - return eval_throw_constant_access_error( - &declaring_class, - constant.name(), - constant.visibility(), - context, - values, - ); - } - return context - .class_constant_cell(&declaring_class, constant.name()) - .ok_or(EvalStatus::RuntimeFatal); - } - if eval_static_member_context_owns_class(&class_name, context) { - if let Some((declaring_class, visibility)) = - eval_dynamic_class_native_constant_metadata( - &class_name, - constant_name, - context, - values, - )? - { - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_throw_constant_access_error( - &declaring_class, - constant_name, - visibility, - context, - values, - ); - } - if let Some(value) = eval_with_native_bridge_scope( - &declaring_class, - context, - || values.class_constant_get(&declaring_class, constant_name), - )? { - return Ok(value); - } - } - return Err(EvalStatus::RuntimeFatal); - } - if let Some(value) = values.class_constant_get(&class_name, constant_name)? { - return Ok(value); - } - eval_throw_error( - &format!( - "Undefined constant {}::{}", - class_name.trim_start_matches('\\'), - constant_name - ), - context, - values, - ) -} - -/// Resolves eval-visible built-in Reflection class constants. -fn eval_builtin_reflection_class_constant( - class_name: &str, - constant_name: &str, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let class_name = class_name.trim_start_matches('\\'); - let value = if class_name.eq_ignore_ascii_case("ReflectionClass") { - match constant_name { - "IS_IMPLICIT_ABSTRACT" => Some(16), - "IS_FINAL" => Some(32), - "IS_EXPLICIT_ABSTRACT" => Some(64), - "IS_READONLY" => Some(65_536), - _ => None, - } - } else if class_name.eq_ignore_ascii_case("ReflectionMethod") { - match constant_name { - "IS_PUBLIC" => Some(1), - "IS_PROTECTED" => Some(2), - "IS_PRIVATE" => Some(4), - "IS_STATIC" => Some(16), - "IS_FINAL" => Some(32), - "IS_ABSTRACT" => Some(64), - _ => None, - } - } else if class_name.eq_ignore_ascii_case("ReflectionProperty") { - match constant_name { - "IS_STATIC" => Some(16), - "IS_READONLY" => Some(128), - "IS_PUBLIC" => Some(1), - "IS_PROTECTED" => Some(2), - "IS_PRIVATE" => Some(4), - "IS_ABSTRACT" => Some(64), - "IS_PROTECTED_SET" => Some(2048), - "IS_PRIVATE_SET" => Some(4096), - "IS_VIRTUAL" => Some(512), - "IS_FINAL" => Some(32), - _ => None, - } - } else if class_name.eq_ignore_ascii_case("ReflectionClassConstant") - || class_name.eq_ignore_ascii_case("ReflectionEnumUnitCase") - || class_name.eq_ignore_ascii_case("ReflectionEnumBackedCase") - { - match constant_name { - "IS_PUBLIC" => Some(1), - "IS_PROTECTED" => Some(2), - "IS_PRIVATE" => Some(4), - "IS_FINAL" => Some(32), - _ => None, - } - } else { - None - }; - value.map(|value| values.int(value)).transpose() -} - -/// Resolves eval-visible `PropertyHookType` builtin enum cases. -fn eval_builtin_property_hook_type_case( - class_name: &str, - constant_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !class_name - .trim_start_matches('\\') - .eq_ignore_ascii_case("PropertyHookType") - { - return Ok(None); - } - let Some((case_name, case_value)) = eval_property_hook_type_case_parts(constant_name) else { - return Ok(None); - }; - if let Some(case) = context.enum_case("PropertyHookType", case_name) { - return Ok(Some(case)); - } - let object = values.new_object("stdClass")?; - let identity = values.object_identity(object)?; - context.register_dynamic_object(identity, "PropertyHookType"); - let name = values.string(case_name)?; - values.property_set(object, "name", name)?; - let value = values.string(case_value)?; - values.property_set(object, "value", value)?; - if let Some(replaced) = context.set_enum_case_value("PropertyHookType", case_name, value) { - values.release(replaced)?; - } - if let Some(replaced) = context.set_enum_case("PropertyHookType", case_name, object) { - values.release(replaced)?; - } - Ok(Some(object)) -} - -/// Returns the PHP case name and backed value for a builtin property-hook case. -fn eval_property_hook_type_case_parts(constant_name: &str) -> Option<(&'static str, &'static str)> { - match constant_name { - "Get" => Some(("Get", "get")), - "Set" => Some(("Set", "set")), - _ => None, - } -} - -/// Returns the PHP class-name literal for `ClassName::class`-style eval expressions. -pub(in crate::interpreter) fn eval_class_name_fetch_result( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = resolve_eval_class_name_literal(class_name, context)?; - values.string(&class_name) -} - -/// Binds one eval-declared static property to a by-reference source variable. -fn eval_static_property_reference_bind_result( - class_name: &str, - property_name: &str, - source_name: &str, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { - if !property.is_static() { - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { - return eval_throw_property_write_access_error( - &declaring_class, - &property, - context, - values, - ); - } - if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { - return eval_throw_readonly_property_modification_error( - &declaring_class, - property.name(), - context, - values, - ); - } - let target = eval_static_property_reference_target( - &declaring_class, - property.name(), - source_name, - context, - scope, - values, - )?; - let value = eval_reference_target_value(&target, context, values)?; - context.bind_static_property_alias(&declaring_class, property.name(), target); - if let Some(replaced) = - context.set_static_property(&declaring_class, property.name(), value) - { - values.release(replaced)?; - } - return Ok(()); - } - if eval_static_member_context_owns_class(&class_name, context) { - if let Some(parent) = context.class_native_parent_name(&class_name) { - if let Some((declaring_class, _, write_visibility, is_static)) = - eval_reflection_aot_static_property_access_metadata( - &parent, - property_name, - context, - values, - )? - { - if !is_static { - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, write_visibility, context) - .is_err() - { - return eval_throw_property_access_error( - &declaring_class, - property_name, - write_visibility, - context, - values, - ); - } - let target = eval_static_property_reference_target( - &declaring_class, - property_name, - source_name, - context, - scope, - values, - )?; - let value = eval_reference_target_value(&target, context, values)?; - context.bind_static_property_alias(&declaring_class, property_name, target); - if eval_with_native_bridge_scope(&declaring_class, context, || { - values.static_property_set(&declaring_class, property_name, value) - })? { - return Ok(()); - } - } - } - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if let Some((declaring_class, _, write_visibility, is_static)) = - eval_reflection_aot_static_property_access_metadata( - &class_name, - property_name, - context, - values, - )? - { - if !is_static { - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, write_visibility, context).is_err() { - return eval_throw_property_access_error( - &declaring_class, - property_name, - write_visibility, - context, - values, - ); - } - let target = eval_static_property_reference_target( - &declaring_class, - property_name, - source_name, - context, - scope, - values, - )?; - let value = eval_reference_target_value(&target, context, values)?; - context.bind_static_property_alias(&declaring_class, property_name, target); - if values.static_property_set(&class_name, property_name, value)? { - return Ok(()); - } - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if eval_runtime_class_like_exists(&class_name, context, values)? { - eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) - } else { - eval_throw_class_not_found_error(&class_name, context, values) - } -} - -/// Resolves a local by-reference source into a persistent static-property alias target. -fn eval_static_property_reference_target( - class_name: &str, - property_name: &str, - source_name: &str, - context: &ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(target) = scope.reference_target(source_name).cloned() { - return Ok(target); - } - if context.current_function().is_some() { - let cell = - visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; - return Ok(EvalReferenceTarget::Cell { cell }); - } - let alias_name = eval_static_property_reference_alias_name(class_name, property_name); - for replaced in set_reference_alias(context, scope, &alias_name, source_name, values)? { - values.release(replaced)?; - } - Ok(EvalReferenceTarget::Variable { - scope: scope as *mut ElephcEvalScope, - name: alias_name, - }) -} - -/// Builds the hidden scope variable name backing one static-property reference alias. -fn eval_static_property_reference_alias_name(class_name: &str, property_name: &str) -> String { - format!("\0elephc_static_property_ref:{class_name}:{property_name}") -} - -/// Writes one eval static-property assignment through its persistent reference target. -fn eval_static_reference_target_write( - class_name: &str, - property_name: &str, - target: EvalReferenceTarget, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if matches!(target, EvalReferenceTarget::Cell { .. }) { - context.bind_static_property_alias( - class_name, - property_name, - EvalReferenceTarget::Cell { cell: value }, - ); - return Ok(()); - } - write_back_method_ref_target(&target, value, context, values) -} - -/// Writes one eval-declared static property after resolving the class-like receiver. -pub(in crate::interpreter) fn eval_static_property_set_result( - class_name: &str, - property_name: &str, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { - if !property.is_static() { - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { - return eval_throw_property_write_access_error( - &declaring_class, - &property, - context, - values, - ); - } - if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { - return eval_throw_readonly_property_modification_error( - &declaring_class, - property.name(), - context, - values, - ); - } - if let Some(target) = context - .static_property_alias(&declaring_class, property.name()) - .cloned() - { - eval_static_reference_target_write( - &declaring_class, - property.name(), - target, - value, - context, - values, - )?; - } - if let Some(replaced) = - context.set_static_property(&declaring_class, property.name(), value) - { - values.release(replaced)?; - } - return Ok(()); - } - if eval_static_member_context_owns_class(&class_name, context) { - if let Some(parent) = context.class_native_parent_name(&class_name) { - if let Some((declaring_class, _, write_visibility, is_static)) = - eval_reflection_aot_static_property_access_metadata( - &parent, - property_name, - context, - values, - )? - { - if !is_static { - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, write_visibility, context) - .is_err() - { - return eval_throw_property_access_error( - &declaring_class, - property_name, - write_visibility, - context, - values, - ); - } - if let Some(target) = context - .static_property_alias(&declaring_class, property_name) - .cloned() - { - eval_static_reference_target_write( - &declaring_class, - property_name, - target, - value, - context, - values, - )?; - } - if eval_with_native_bridge_scope(&declaring_class, context, || { - values.static_property_set(&declaring_class, property_name, value) - })? { - return Ok(()); - } - } - } - return eval_throw_undeclared_static_property_error( - &class_name, - property_name, - context, - values, - ); - } - if let Some((declaring_class, _, write_visibility, is_static)) = - eval_reflection_aot_static_property_access_metadata( - &class_name, - property_name, - context, - values, - )? - { - if is_static - && validate_eval_member_access(&declaring_class, write_visibility, context).is_err() - { - return eval_throw_property_access_error( - &declaring_class, - property_name, - write_visibility, - context, - values, - ); - } - if is_static { - if let Some(target) = context - .static_property_alias(&declaring_class, property_name) - .cloned() - { - eval_static_reference_target_write( - &declaring_class, - property_name, - target, - value, - context, - values, - )?; - } - } - } - if values.static_property_set(&class_name, property_name, value)? { - return Ok(()); - } - if eval_runtime_class_like_exists(&class_name, context, values)? { - eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) - } else { - eval_throw_class_not_found_error(&class_name, context, values) - } -} - -/// Dispatches a static method call to an eval-declared static method. -pub(in crate::interpreter) fn eval_static_method_call_result( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let receiver = resolve_eval_static_method_receiver(class_name, context)?; - eval_static_method_call_result_resolved( - receiver.dispatch_class, - receiver.called_class, - method_name, - evaluated_args, - None, - context, - values, - ) -} - -/// Dispatches a static-syntax method call from an expression scope that may hold `$this`. -pub(in crate::interpreter) fn eval_static_method_call_result_from_scope( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - scope: &ElephcEvalScope, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let receiver = resolve_eval_static_method_receiver(class_name, context)?; - eval_static_method_call_result_resolved( - receiver.dispatch_class, - receiver.called_class, - method_name, - evaluated_args, - Some(scope), - context, - values, - ) -} - -/// Dispatches a static method call using a first-class callable's captured called class. -pub(in crate::interpreter) fn eval_static_method_call_result_with_called_class( - class_name: &str, - called_class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = resolve_eval_static_member_class_name(class_name, context)?; - let called_class_name = context - .resolve_class_name(called_class_name) - .unwrap_or_else(|| called_class_name.trim_start_matches('\\').to_string()); - eval_static_method_call_result_resolved( - class_name, - called_class_name, - method_name, - evaluated_args, - None, - context, - values, - ) -} - -/// Dispatches a static method call after lookup and late-static names have been resolved. -fn eval_static_method_call_result_resolved( - class_name: String, - called_class_name: String, - method_name: &str, - evaluated_args: Vec, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Some(result) = eval_closure_static_method_result( - &class_name, - method_name, - evaluated_args.clone(), - lexical_scope, - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_builtin_property_hook_type_static_method_result( - &class_name, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_method_create_from_method_name_result( - &class_name, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { - return eval_enum_builtin_static_method_result( - &class_name, - method_name, - evaluated_args, - context, - values, - ); - } - if let Some((declaring_class, method)) = - eval_dynamic_static_method_for_call(&class_name, method_name, context) - { - if method.is_abstract() { - return eval_throw_abstract_method_call_error( - &declaring_class, - method.name(), - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { - if let Some(result) = eval_magic_static_method_call( - &class_name, - &called_class_name, - method_name, - evaluated_args, - context, - values, - )? { - return Ok(result); - } - return eval_throw_method_access_error( - &declaring_class, - method.name(), - method.visibility(), - context, - values, - ); - } - if !method.is_static() { - if let Some(object) = - eval_static_syntax_instance_receiver(&class_name, lexical_scope, context, values)? - { - return eval_dynamic_method_with_values( - &declaring_class, - &called_class_name, - &method, - object, - evaluated_args, - context, - values, - ); - } - return eval_throw_non_static_method_call_error( - &declaring_class, - method.name(), - context, - values, - ); - } - return eval_dynamic_static_method_with_values( - &declaring_class, - &called_class_name, - &method, - evaluated_args, - context, - values, - ); - } - if context.has_class(&class_name) { - if let Some(parent) = context.class_native_parent_name(&class_name) { - if let Some(result) = eval_native_static_syntax_method_result( - &parent, - Some(&called_class_name), - method_name, - evaluated_args.clone(), - lexical_scope, - context, - values, - )? - { - return Ok(result); - } - } - } - if context.has_class(&class_name) - || context.has_interface(&class_name) - || context.has_trait(&class_name) - || context.has_enum(&class_name) - { - if let Some(result) = eval_magic_static_method_call( - &class_name, - &called_class_name, - method_name, - evaluated_args, - context, - values, - )? { - return Ok(result); - } - return eval_throw_undefined_method_call_error( - &class_name, - method_name, - context, - values, - ); - } - if let Some(result) = eval_native_static_syntax_method_result( - &class_name, - None, - method_name, - evaluated_args.clone(), - lexical_scope, - context, - values, - )? { - return Ok(result); - } - eval_native_static_method_with_evaluated_args( - &class_name, - method_name, - evaluated_args, - context, - values, - ) -} - -/// Dispatches static methods for eval's builtin `Closure` class slice. -fn eval_closure_static_method_result( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !class_name - .trim_start_matches('\\') - .eq_ignore_ascii_case("Closure") - { - return Ok(None); - } - if method_name.eq_ignore_ascii_case("fromCallable") { - return eval_closure_from_callable(evaluated_args, lexical_scope, context, values) - .map(Some); - } - if method_name.eq_ignore_ascii_case("bind") { - return eval_closure_bind_static(evaluated_args, context, values).map(Some); - } - Ok(None) -} - -/// Materializes `Closure::fromCallable()` from one normalized eval callback. -fn eval_closure_from_callable( - evaluated_args: Vec, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut args = bind_evaluated_function_args(&[String::from("callback")], evaluated_args)?; - let callback = args.pop().ok_or(EvalStatus::RuntimeFatal)?; - let callable = match lexical_scope { - Some(scope) => eval_callable_from_scope(callback, context, scope, values), - None => eval_callable(callback, context, values), - }; - let callable = match callable { - Ok(callable) => callable, - Err(EvalStatus::UnsupportedConstruct) if values.type_tag(callback)? == EVAL_TAG_OBJECT => { - return eval_closure_from_callable_type_error( - "no array or string given", - context, - values, - ); - } - Err(status) => return Err(status), - }; - eval_validate_closure_from_callable_callback(&callable, context, values)?; - let target = eval_closure_object_target_from_callable(callable); - eval_closure_object_from_target(target, context, values) -} - -/// Converts a normalized callable target into the storage used by eval Closure objects. -fn eval_closure_object_target_from_callable( - callable: EvaluatedCallable, -) -> EvalClosureObjectTarget { - match callable { - EvaluatedCallable::Named { name, .. } => EvalClosureObjectTarget::Named(name), - EvaluatedCallable::BoundClosure { - name, - bound_this, - bound_scope, - } => EvalClosureObjectTarget::BoundNamed { - name, - bound_this, - bound_scope, - }, - EvaluatedCallable::InvokableObject { object } => { - EvalClosureObjectTarget::InvokableObject { object } - } - EvaluatedCallable::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - } => EvalClosureObjectTarget::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - }, - EvaluatedCallable::StaticMethod { - class_name, - method, - called_class, - native_class, - bridge_scope, - } => EvalClosureObjectTarget::StaticMethod { - class_name, - method, - called_class, - native_class, - bridge_scope, - }, - } -} - -/// Allocates a PHP-visible eval Closure object for one retained callable target. -fn eval_closure_object_from_target( - target: EvalClosureObjectTarget, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = values.new_object("stdClass")?; - let identity = values.object_identity(object)?; - context.register_closure_object_target(identity, target); - Ok(object) -} - -/// Materializes `Closure::bind()` from a closure object and a persistent receiver. -fn eval_closure_bind_static( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let (target, bound_this, bound_scope, rebinds_function_scope) = - eval_closure_bind_static_args(evaluated_args, context, values)?; - eval_closure_bind_target( - target, - bound_this, - bound_scope, - rebinds_function_scope, - context, - values, - ) -} - -/// Binds static `Closure::bind()` arguments to their PHP parameter slots. -fn eval_closure_bind_static_args( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result< - ( - EvalClosureObjectTarget, - Option, - Option, - bool, - ), - EvalStatus, -> { - let bound = eval_closure_bind_args( - &["closure", "newThis", "newScope"], - 2, - evaluated_args, - )?; - let closure = required_closure_bind_arg(&bound, 0)?; - let new_this = required_closure_bind_arg(&bound, 1)?; - let target = eval_closure_target_arg(closure.value, context, values)?; - let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; - let (bound_scope, rebinds_function_scope) = - eval_closure_bind_scope_arg(bound.get(2), bound_this, context, values)?; - Ok((target, bound_this, bound_scope, rebinds_function_scope)) -} - -/// Binds `Closure::bindTo()` arguments to their PHP parameter slots. -fn eval_closure_bind_to_args( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(Option, Option, bool), EvalStatus> { - let bound = eval_closure_bind_args(&["newThis", "newScope"], 1, evaluated_args)?; - let new_this = required_closure_bind_arg(&bound, 0)?; - let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; - let (bound_scope, rebinds_function_scope) = - eval_closure_bind_scope_arg(bound.get(1), bound_this, context, values)?; - Ok((bound_this, bound_scope, rebinds_function_scope)) -} - -/// Binds positional and named Closure binding arguments while accepting optional scope. -fn eval_closure_bind_args( - params: &[&str], - required_count: usize, - evaluated_args: Vec, -) -> Result>, EvalStatus> { - let mut bound_args = vec![None; params.len()]; - let mut next_positional = 0; - let mut saw_named = false; - - for arg in evaluated_args { - if let Some(name) = arg.name.as_deref() { - saw_named = true; - let Some(index) = params - .iter() - .position(|param| param.eq_ignore_ascii_case(name)) - else { - return Err(EvalStatus::RuntimeFatal); - }; - if bound_args[index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[index] = Some(arg); - continue; - } - - if saw_named || next_positional >= params.len() || bound_args[next_positional].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - bound_args[next_positional] = Some(arg); - next_positional += 1; - } - - if bound_args - .iter() - .take(required_count) - .any(Option::is_none) - { - return Err(EvalStatus::RuntimeFatal); - } - Ok(bound_args) -} - -/// Returns one required Closure binding argument. -fn required_closure_bind_arg( - bound_args: &[Option], - index: usize, -) -> Result<&EvaluatedCallArg, EvalStatus> { - bound_args - .get(index) - .and_then(Option::as_ref) - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Extracts a stored eval Closure object target from a runtime object. -fn eval_closure_target_arg( - closure: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(closure)?; - context - .closure_object_target(identity) - .cloned() - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Converts the `newThis` binding argument to an optional object receiver. -fn eval_closure_bind_receiver_arg( - new_this: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if values.is_null(new_this)? { - return Ok(None); - } - if values.type_tag(new_this)? != EVAL_TAG_OBJECT { - return Err(EvalStatus::RuntimeFatal); - } - Ok(Some(new_this)) -} - -/// Converts `newScope` into class scope plus whether function scope was rebound. -fn eval_closure_bind_scope_arg( - new_scope: Option<&Option>, - bound_this: Option, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(Option, bool), EvalStatus> { - let Some(new_scope) = new_scope.and_then(Option::as_ref) else { - return Ok((None, false)); - }; - if values.is_null(new_scope.value)? { - return Ok((None, false)); - } - if values.type_tag(new_scope.value)? == EVAL_TAG_OBJECT { - return eval_closure_bound_object_class_name(new_scope.value, context, values) - .map(|scope| (Some(scope), true)); - } - let bytes = values.string_bytes(new_scope.value)?; - let scope = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; - if scope.eq_ignore_ascii_case("static") { - let Some(bound_this) = bound_this else { - return Ok((None, false)); - }; - return eval_closure_bound_object_class_name(bound_this, context, values) - .map(|scope| (Some(scope), false)); - } - Ok(( - Some( - context - .resolve_class_name(&scope) - .unwrap_or_else(|| scope.trim_start_matches('\\').to_string()), - ), - true, - )) -} - -/// Creates a new Closure object with persistent binding metadata when supported. -fn eval_closure_bind_target( - target: EvalClosureObjectTarget, - bound_this: Option, - bound_scope: Option, - rebinds_function_scope: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(bound_this) = bound_this else { - return eval_closure_unbind_target( - target, - bound_scope, - rebinds_function_scope, - context, - values, - ); - }; - match target { - EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { - let Some(closure) = context.closure(&name) else { - if eval_function_probe_exists(context, &name) { - if rebinds_function_scope { - return eval_closure_call_warning_null( - "Cannot rebind scope of closure created from function", - values, - ); - } - return eval_closure_object_from_target( - EvalClosureObjectTarget::BoundNamed { - name, - bound_this: Some(bound_this), - bound_scope, - }, - context, - values, - ); - } - return eval_closure_call_warning_null( - "Cannot rebind scope of closure created from function", - values, - ); - }; - if closure.is_static() { - return eval_closure_call_warning_null( - "Cannot bind an instance to a static closure", - values, - ); - } - eval_closure_object_from_target( - EvalClosureObjectTarget::BoundNamed { - name, - bound_this: Some(bound_this), - bound_scope, - }, - context, - values, - ) - } - EvalClosureObjectTarget::InvokableObject { object } => { - if !eval_closure_bind_bound_class_matches_method( - object, "__invoke", bound_this, context, values, - )? { - return eval_closure_call_warning_null( - "Cannot rebind scope of closure created from method", - values, - ); - } - eval_closure_object_from_target( - EvalClosureObjectTarget::InvokableObject { object: bound_this }, - context, - values, - ) - } - EvalClosureObjectTarget::ObjectMethod { - object, - method, - native_class, - bridge_scope, - .. - } => { - if !eval_closure_bind_bound_class_matches_method( - object, &method, bound_this, context, values, - )? { - return eval_closure_call_warning_null( - "Cannot rebind scope of closure created from method", - values, - ); - } - let called_class = Some(eval_closure_bound_object_class_name( - bound_this, context, values, - )?); - eval_closure_object_from_target( - EvalClosureObjectTarget::ObjectMethod { - object: bound_this, - method, - called_class, - native_class, - bridge_scope, - }, - context, - values, - ) - } - EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( - "Cannot bind an instance to a static closure", - values, - ), - } -} - -/// Creates an unbound Closure object for targets that can drop `$this`. -fn eval_closure_unbind_target( - target: EvalClosureObjectTarget, - bound_scope: Option, - rebinds_function_scope: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match target { - EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { - if rebinds_function_scope - && context.closure(&name).is_none() - && eval_function_probe_exists(context, &name) - { - return eval_closure_call_warning_null( - "Cannot rebind scope of closure created from function", - values, - ); - } - let target = match bound_scope { - Some(bound_scope) => EvalClosureObjectTarget::BoundNamed { - name, - bound_this: None, - bound_scope: Some(bound_scope), - }, - None => EvalClosureObjectTarget::Named(name), - }; - eval_closure_object_from_target(target, context, values) - } - EvalClosureObjectTarget::InvokableObject { .. } - | EvalClosureObjectTarget::ObjectMethod { .. } => { - eval_closure_call_warning_null("Cannot unbind $this of method", values) - } - EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( - "Cannot unbind $this of static method", - values, - ), - } -} - -/// Dispatches one generated/AOT method reached through PHP static-call syntax. -fn eval_native_static_syntax_method_result( - class_name: &str, - called_class_scope: Option<&str>, - method_name: &str, - evaluated_args: Vec, - lexical_scope: Option<&ElephcEvalScope>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? - else { - if eval_native_static_magic_method_available(class_name, context, values)? { - return eval_native_static_method_with_evaluated_args( - class_name, - method_name, - evaluated_args, - context, - values, - ) - .map(Some); - } - return Ok(None); - }; - if is_abstract { - return eval_throw_abstract_method_call_error( - &declaring_class, - method_name, - context, - values, - ); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - if eval_native_static_magic_method_available(class_name, context, values)? { - return eval_native_magic_static_method_call( - class_name, - method_name, - evaluated_args, - context, - values, - ) - .map(Some); - } - return eval_throw_method_access_error( - &declaring_class, - method_name, - visibility, - context, - values, - ); - } - if !is_static { - if let Some(object) = - eval_static_syntax_instance_receiver(class_name, lexical_scope, context, values)? - { - return eval_native_method_with_evaluated_args_bridge_scope( - object, - class_name, - method_name, - evaluated_args, - Some(&declaring_class), - called_class_scope, - context, - values, - ) - .map(Some); - } - return eval_throw_non_static_method_call_error( - &declaring_class, - method_name, - context, - values, - ); - } - eval_native_static_method_with_evaluated_args_bridge_scope( - class_name, - method_name, - evaluated_args, - Some(&declaring_class), - called_class_scope, - context, - values, - ) - .map(Some) -} - -/// Returns `$this` when PHP permits static-call syntax to target an instance method. -pub(in crate::interpreter) fn eval_static_syntax_instance_receiver( - class_name: &str, - lexical_scope: Option<&ElephcEvalScope>, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(scope) = lexical_scope else { - return Ok(None); - }; - let Some(object) = visible_scope_cell(context, scope, "this") else { - return Ok(None); - }; - if values.type_tag(object)? != EVAL_TAG_OBJECT { - return Ok(None); - } - let object_class_name = eval_static_syntax_object_class_name(object, context, values)?; - if eval_static_syntax_object_matches_class(&object_class_name, class_name, context) { - Ok(Some(object)) - } else { - Ok(None) - } -} - -/// Resolves the PHP-visible class name for the current static-syntax `$this` object. -fn eval_static_syntax_object_class_name( - object: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if let Ok(identity) = values.object_identity(object) { - if let Some(class) = context.dynamic_object_class(identity) { - return Ok(class.name().to_string()); - } - } - runtime_object_class_name(object, values) -} - -/// Returns whether `$this` is an instance of the class named by static-call syntax. -fn eval_static_syntax_object_matches_class( - object_class_name: &str, - class_name: &str, - context: &ElephcEvalContext, -) -> bool { - same_eval_class_name(object_class_name, class_name) - || context.class_is_a(object_class_name, class_name, false) - || native_class_is_a(object_class_name, class_name, context) -} - -/// Dispatches static methods for eval's builtin `PropertyHookType` enum slice. -fn eval_builtin_property_hook_type_static_method_result( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !class_name - .trim_start_matches('\\') - .eq_ignore_ascii_case("PropertyHookType") - { - return Ok(None); - } - match eval_enum_static_builtin_name(method_name) { - Some("cases") => { - eval_builtin_property_hook_type_cases(evaluated_args, context, values).map(Some) - } - Some("from") => { - eval_builtin_property_hook_type_from(evaluated_args, false, context, values).map(Some) - } - Some("tryFrom") => { - eval_builtin_property_hook_type_from(evaluated_args, true, context, values).map(Some) - } - _ => Ok(None), - } -} - -/// Builds the indexed case array for eval's builtin `PropertyHookType` enum slice. -fn eval_builtin_property_hook_type_cases( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let case_names = ["Get", "Set"]; - let mut array = values.array_new(case_names.len())?; - for (index, case_name) in case_names.iter().enumerate() { - let key = values.int(index as i64)?; - let case = - eval_builtin_property_hook_type_case("PropertyHookType", case_name, context, values)? - .ok_or(EvalStatus::RuntimeFatal)?; - array = values.array_set(array, key, case)?; - } - Ok(array) -} - -/// Evaluates builtin `PropertyHookType::from()` or `tryFrom()` inside eval. -fn eval_builtin_property_hook_type_from( - evaluated_args: Vec, - nullable_miss: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut args = bind_evaluated_function_args(&[String::from("value")], evaluated_args)?; - let value = args.pop().ok_or(EvalStatus::RuntimeFatal)?; - let bytes = values.string_bytes(value)?; - let value_text = String::from_utf8_lossy(&bytes); - for constant_name in ["Get", "Set"] { - let Some((_, case_value)) = eval_property_hook_type_case_parts(constant_name) else { - continue; - }; - if value_text == case_value { - return eval_builtin_property_hook_type_case( - "PropertyHookType", - constant_name, - context, - values, - )? - .ok_or(EvalStatus::RuntimeFatal); - } - } - if nullable_miss { - values.null() - } else { - let message = eval_enum_invalid_backing_value_message( - "PropertyHookType", - EvalEnumBackingType::String, - value, - values, - )?; - eval_throw_value_error(&message, context, values) - } -} - -/// Returns a recognized enum-provided static method name. -fn eval_enum_static_builtin_name(method_name: &str) -> Option<&'static str> { - if method_name.eq_ignore_ascii_case("cases") { - Some("cases") - } else if method_name.eq_ignore_ascii_case("from") { - Some("from") - } else if method_name.eq_ignore_ascii_case("tryFrom") { - Some("tryFrom") - } else { - None - } -} - -/// Returns a synthetic enum method only when that enum actually provides it. -pub(in crate::interpreter) fn eval_enum_static_builtin_applies( - enum_name: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Option<&'static str> { - let enum_decl = context.enum_decl(enum_name)?; - match eval_enum_static_builtin_name(method_name)? { - "cases" => Some("cases"), - "from" if enum_decl.backing_type().is_some() => Some("from"), - "tryFrom" if enum_decl.backing_type().is_some() => Some("tryFrom"), - _ => None, - } -} - -/// Dispatches enum-provided static methods for eval-declared enums. -pub(in crate::interpreter) fn eval_enum_builtin_static_method_result( - enum_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match eval_enum_static_builtin_name(method_name).ok_or(EvalStatus::RuntimeFatal)? { - "cases" => eval_enum_cases_result(enum_name, evaluated_args, context, values), - "from" => eval_enum_from_result(enum_name, evaluated_args, false, context, values), - "tryFrom" => eval_enum_from_result(enum_name, evaluated_args, true, context, values), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Builds the indexed array returned by `EnumName::cases()`. -fn eval_enum_cases_result( - enum_name: &str, - evaluated_args: Vec, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let enum_decl = context - .enum_decl(enum_name) - .ok_or(EvalStatus::RuntimeFatal)?; - let case_names = enum_decl - .cases() - .iter() - .map(|case| case.name().to_string()) - .collect::>(); - let mut array = values.array_new(case_names.len())?; - for (index, case_name) in case_names.iter().enumerate() { - let key = values.int(index as i64)?; - let case = context - .enum_case(enum_name, case_name) - .ok_or(EvalStatus::RuntimeFatal)?; - array = values.array_set(array, key, case)?; - } - Ok(array) -} - -/// Evaluates `EnumName::from()` or `EnumName::tryFrom()` for eval-backed enums. -fn eval_enum_from_result( - enum_name: &str, - evaluated_args: Vec, - nullable_miss: bool, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let enum_decl = context - .enum_decl(enum_name) - .ok_or(EvalStatus::RuntimeFatal)?; - let backing_type = enum_decl.backing_type().ok_or(EvalStatus::RuntimeFatal)?; - let enum_display_name = enum_decl.name().trim_start_matches('\\').to_string(); - let case_names = enum_decl - .cases() - .iter() - .map(|case| case.name().to_string()) - .collect::>(); - let mut args = bind_evaluated_function_args(&[String::from("value")], evaluated_args)?; - let value = args.pop().ok_or(EvalStatus::RuntimeFatal)?; - for case_name in case_names { - let case_value = context - .enum_case_value(enum_name, &case_name) - .ok_or(EvalStatus::RuntimeFatal)?; - let equal = values.compare(EvalBinOp::StrictEq, value, case_value)?; - if values.truthy(equal)? { - return context - .enum_case(enum_name, &case_name) - .ok_or(EvalStatus::RuntimeFatal); - } - } - if nullable_miss { - values.null() - } else { - let message = eval_enum_invalid_backing_value_message( - &enum_display_name, - backing_type, - value, - values, - )?; - eval_throw_value_error(&message, context, values) - } -} - -/// Builds PHP's backed-enum `ValueError` message for an unmatched enum value. -fn eval_enum_invalid_backing_value_message( - enum_name: &str, - backing_type: EvalEnumBackingType, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let bytes = values.string_bytes(value)?; - let value = String::from_utf8_lossy(&bytes); - let value = match backing_type { - EvalEnumBackingType::Int => value.into_owned(), - EvalEnumBackingType::String => format!("\"{}\"", value), - }; - Ok(format!( - "{} is not a valid backing value for enum {}", - value, enum_name - )) -} - -/// Creates and schedules a `ValueError` through eval's normal Throwable channel. -fn eval_throw_value_error( - message: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let exception = values.new_object("ValueError")?; - let message = values.string(message)?; - let code = values.int(0)?; - values.construct_object(exception, vec![message, code])?; - context.set_pending_throw(exception); - Err(EvalStatus::UncaughtThrowable) -} - -/// Creates and schedules a `ReflectionException` through eval's normal Throwable channel. -fn eval_throw_reflection_exception( - message: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let exception = values.new_object("ReflectionException")?; - let message = values.string(message)?; - let code = values.int(0)?; - values.construct_object(exception, vec![message, code])?; - context.set_pending_throw(exception); - Err(EvalStatus::UncaughtThrowable) -} - -/// Schedules the Throwable category required by one ReflectionClass instantiation error. -fn eval_throw_reflection_instantiation_error( - error: EvalReflectionInstantiationError, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - match error { - EvalReflectionInstantiationError::ThrowableError(message) => { - eval_throw_error(&message, context, values) - } - EvalReflectionInstantiationError::ReflectionException(message) => { - eval_throw_reflection_exception(&message, context, values) - } - } -} - -/// Resolves a static method using private-method scope rules. -pub(in crate::interpreter) fn eval_dynamic_static_method_for_call( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, EvalClassMethod)> { - if let Some(current_class) = context.current_class_scope() { - if eval_classes_are_related(current_class, class_name, context) { - if let Some((declaring_class, method)) = - context.class_own_method(current_class, method_name) - { - if method.visibility() == EvalVisibility::Private { - return Some((declaring_class, method)); - } - } - } - } - context.class_method(class_name, method_name) -} - -/// Resolves `self`, `parent`, and `static` for eval static member access. -pub(in crate::interpreter) fn resolve_eval_static_class_name( - class_name: &str, - context: &ElephcEvalContext, -) -> Result { - match class_name.to_ascii_lowercase().as_str() { - "self" => context - .current_class_scope() - .map(str::to_string) - .ok_or(EvalStatus::RuntimeFatal), - "static" => context - .current_called_class_scope() - .or_else(|| context.current_class_scope()) - .map(str::to_string) - .ok_or(EvalStatus::RuntimeFatal), - "parent" => { - let current = context - .current_class_scope() - .ok_or(EvalStatus::RuntimeFatal)?; - context - .class(current) - .and_then(EvalClass::parent) - .map(|parent| { - context - .resolve_class_name(parent) - .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()) - }) - .or_else(|| context.native_class_parent(current).map(str::to_string)) - .ok_or(EvalStatus::RuntimeFatal) - } - _ => context - .resolve_class_name(class_name) - .or_else(|| { - context - .has_class(class_name) - .then(|| class_name.to_string()) - }) - .ok_or(EvalStatus::RuntimeFatal), - } -} - -/// Resolved static method dispatch metadata preserving PHP late-static forwarding. -pub(in crate::interpreter) struct EvalStaticMethodReceiver { - pub(in crate::interpreter) dispatch_class: String, - pub(in crate::interpreter) called_class: String, -} - -/// Resolves static method receivers into lookup and late-static called-class names. -pub(in crate::interpreter) fn resolve_eval_static_method_receiver( - class_name: &str, - context: &ElephcEvalContext, -) -> Result { - let dispatch_class = resolve_eval_static_member_class_name(class_name, context)?; - let called_class = match class_name.to_ascii_lowercase().as_str() { - "self" | "parent" => context - .current_called_class_scope() - .or_else(|| context.current_class_scope()) - .map(str::to_string) - .ok_or(EvalStatus::RuntimeFatal)?, - "static" => dispatch_class.clone(), - _ => dispatch_class.clone(), - }; - Ok(EvalStaticMethodReceiver { - dispatch_class, - called_class, - }) -} - -/// Resolves static member receivers while allowing non-eval class names to reach AOT lookup. -pub(in crate::interpreter) fn resolve_eval_static_member_class_name( - class_name: &str, - context: &ElephcEvalContext, -) -> Result { - match class_name.to_ascii_lowercase().as_str() { - "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), - _ => Ok(context - .resolve_class_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), - } -} - -/// Returns true when an eval-declared class-like symbol should not fall through to AOT lookup. -fn eval_static_member_context_owns_class( - class_name: &str, - context: &ElephcEvalContext, -) -> bool { - context.has_class(class_name) - || context.has_interface(class_name) - || context.has_trait(class_name) - || context.has_enum(class_name) -} - -/// Returns whether a static member receiver exists in eval metadata or generated metadata. -fn eval_runtime_class_like_exists( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(eval_static_member_context_owns_class(class_name, context) - || values.class_exists(class_name)? - || eval_runtime_interface_exists(class_name, values)? - || values.trait_exists(class_name)? - || values.enum_exists(class_name)?) -} - -/// Resolves class-name literal receivers without requiring named classes to exist. -fn resolve_eval_class_name_literal( - class_name: &str, - context: &ElephcEvalContext, -) -> Result { - match class_name.to_ascii_lowercase().as_str() { - "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), - _ => Ok(context - .resolve_class_like_name(class_name) - .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), - } -} - -/// Creates a backing object for an eval-declared class and runs its constructor. -pub(in crate::interpreter) fn eval_dynamic_class_new_object( - class: &EvalClass, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_dynamic_class_new_object_with_ref_mode( - class, - evaluated_args, - EvalByRefBindingMode::RequireTarget, - context, - caller_scope, - values, - ) -} - -/// Creates an eval-declared object while using the selected constructor by-ref mode. -fn eval_dynamic_class_new_object_with_ref_mode( - class: &EvalClass, - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = eval_dynamic_class_allocate_object(class, context, caller_scope, values)?; - if let Some((constructor_class, constructor)) = - context.class_method(class.name(), "__construct") - { - if validate_eval_member_access(&constructor_class, constructor.visibility(), context) - .is_err() - { - let _ = values.release(object); - return eval_throw_method_access_error( - &constructor_class, - constructor.name(), - constructor.visibility(), - context, - values, - ); - } - let result = eval_dynamic_method_with_values_and_ref_mode( - &constructor_class, - class.name(), - &constructor, - object, - constructor.parameter_is_by_ref(), - evaluated_args, - by_ref_mode, - context, - values, - )?; - eval_release_value(context, values, result)?; - } else if !evaluated_args.is_empty() { - if let Some(parent) = context.class_native_parent_name(class.name()) { - eval_native_constructor_with_evaluated_args_and_ref_mode( - &parent, - object, - evaluated_args, - by_ref_mode, - context, - values, - )?; - } else { - return Err(EvalStatus::RuntimeFatal); - } - } else if let Some(parent) = context.class_native_parent_name(class.name()) { - if eval_aot_method_dispatch_metadata_in_hierarchy( - &parent, - "__construct", - context, - values, - )? - .is_some() - { - eval_native_constructor_with_evaluated_args_and_ref_mode( - &parent, - object, - Vec::new(), - by_ref_mode, - context, - values, - )?; - } - } - Ok(object) -} - -/// Creates a PHP shallow clone and invokes an eval-declared `__clone()` hook when present. -pub(in crate::interpreter) fn eval_object_clone_result( - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let identity = values.object_identity(object)?; - let dynamic_class_name = context - .dynamic_object_class(identity) - .map(|class| class.name().to_string()); - let clone_method = dynamic_class_name - .as_deref() - .and_then(|class_name| context.class_method(class_name, "__clone")); - if let Some((declaring_class, method)) = &clone_method { - if validate_eval_member_access(declaring_class, method.visibility(), context).is_err() { - return eval_throw_clone_access_error( - declaring_class, - method.visibility(), - context, - values, - ); - } - } - let dynamic_native_clone_hook_scope = if clone_method.is_none() { - if let Some(class_name) = dynamic_class_name.as_deref() { - eval_dynamic_native_clone_hook_is_callable(class_name, context, values)? - } else { - None - } - } else { - None - }; - let should_call_aot_clone_hook = if dynamic_class_name.is_none() { - eval_aot_clone_hook_is_callable(object, context, values)? - } else { - false - }; - - let clone = values.object_clone_shallow(object)?; - if let Some(class_name) = dynamic_class_name { - let clone_identity = values.object_identity(clone)?; - context.register_dynamic_object(clone_identity, &class_name); - context.clone_dynamic_property_aliases(identity, clone_identity); - if let Some((declaring_class, method)) = clone_method { - let result = eval_dynamic_method_with_values( - &declaring_class, - &class_name, - &method, - clone, - Vec::new(), - context, - values, - )?; - eval_release_value(context, values, result)?; - } else if let Some(scope) = dynamic_native_clone_hook_scope { - let result = eval_native_method_call_with_scope( - &scope, - None, - clone, - "__clone", - Vec::new(), - context, - values, - )?; - values.release(result)?; - } - } else if should_call_aot_clone_hook { - let result = values.method_call(clone, "__clone", Vec::new())?; - values.release(result)?; - } - Ok(clone) -} - -/// Returns the declaring scope for an inherited generated/AOT `__clone()` hook. -fn eval_dynamic_native_clone_hook_is_callable( - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_dynamic_class_native_method_metadata(class_name, "__clone", context, values)? - else { - return Ok(None); - }; - if is_static || is_abstract { - return Err(EvalStatus::RuntimeFatal); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_throw_clone_access_error(&declaring_class, visibility, context, values); - } - Ok(Some(declaring_class)) -} - -/// Calls one generated/AOT method while presenting an explicit PHP class scope to the bridge. -fn eval_native_method_call_with_scope( - scope: &str, - called_class_scope: Option<&str>, - object: RuntimeCellHandle, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - context.push_class_scope(scope.to_string()); - if let Some(called_class) = called_class_scope { - context.push_called_class_scope(called_class.to_string()); - } - let _called_class_override = called_class_scope - .map(|called_class| push_native_frame_called_class_override(context, scope, called_class)); - let result = values.method_call(object, method_name, evaluated_args); - if called_class_scope.is_some() { - context.pop_called_class_scope(); - } - context.pop_class_scope(); - result -} - -/// Calls one generated/AOT static method while presenting an explicit PHP class scope. -fn eval_native_static_method_call_with_scope( - scope: &str, - called_class_scope: Option<&str>, - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - context.push_class_scope(scope.to_string()); - if let Some(called_class) = called_class_scope { - context.push_called_class_scope(called_class.to_string()); - } - let _called_class_override = called_class_scope - .map(|called_class| push_native_frame_called_class_override(context, scope, called_class)); - let result = values.static_method_call(class_name, method_name, evaluated_args); - if called_class_scope.is_some() { - context.pop_called_class_scope(); - } - context.pop_class_scope(); - result -} - -/// Runs one generated/AOT bridge operation while exposing an explicit PHP class scope. -fn eval_with_native_bridge_scope( - scope: &str, - context: &mut ElephcEvalContext, - call: impl FnOnce() -> Result, -) -> Result { - context.push_class_scope(scope.to_string()); - let result = call(); - context.pop_class_scope(); - result -} - -/// Returns generated/AOT property metadata inherited by an eval-declared class. -fn eval_dynamic_class_native_property_metadata( - called_class_name: &str, - property_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(parent) = context.class_native_parent_name(called_class_name) else { - return Ok(None); - }; - eval_reflection_aot_property_access_metadata(&parent, property_name, values) -} - -/// Returns generated/AOT class-constant metadata inherited by an eval-declared class. -fn eval_dynamic_class_native_constant_metadata( - called_class_name: &str, - constant_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(parent) = context.class_native_parent_name(called_class_name) else { - return Ok(None); - }; - let Some(flags) = values.reflection_constant_flags(&parent, constant_name)? else { - return Ok(None); - }; - let declaring_class = values - .reflection_constant_declaring_class(&parent, constant_name)? - .unwrap_or(parent); - let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { - EvalVisibility::Private - } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { - EvalVisibility::Protected - } else { - EvalVisibility::Public - }; - Ok(Some((declaring_class, visibility))) -} - -/// Returns whether an accessible instance AOT `__clone()` hook should run. -fn eval_aot_clone_hook_is_callable( - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = eval_runtime_object_class_name(object, values)?; - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata(&class_name, "__clone", values)? - else { - return Ok(false); - }; - if is_static || is_abstract { - return Err(EvalStatus::RuntimeFatal); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_throw_clone_access_error(&declaring_class, visibility, context, values); - } - Ok(true) -} - -/// Reads the PHP-visible runtime class name for one AOT object handle. -fn eval_runtime_object_class_name( - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = values.object_class_name(object)?; - let bytes = values.string_bytes(class_name)?; - values.release(class_name)?; - String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Creates a backing object for an eval-declared class without running its constructor. -fn eval_dynamic_class_allocate_object( - class: &EvalClass, - context: &mut ElephcEvalContext, - caller_scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if class.is_abstract() || context.has_enum(class.name()) { - return Err(EvalStatus::RuntimeFatal); - } - let backing_class = context - .class_native_parent_name(class.name()) - .unwrap_or_else(|| String::from("stdClass")); - let object = values.new_object(&backing_class)?; - let identity = values.object_identity(object)?; - context.register_dynamic_object(identity, class.name()); - let mut class_chain = context.class_chain(class.name()); - if class_chain.is_empty() { - class_chain.push(class.clone()); - } - for class in &class_chain { - for property in class - .properties() - .iter() - .filter(|property| !property.is_static() && !property.is_abstract()) - { - let value = if let Some(default) = property.default() { - Some(eval_class_like_member_default( - class.name(), - property.trait_origin(), - default, - context, - caller_scope, - values, - )?) - } else if property.property_type().is_none() { - Some(values.null()?) - } else { - None - }; - let storage_name = eval_instance_property_storage_name(class.name(), property); - if let Some(value) = value { - values.property_set(object, &storage_name, value)?; - context.mark_dynamic_property_initialized(identity, &storage_name); - } - } - } - Ok(object) -} - -/// Dispatches a method call to an eval-declared class method or to the runtime hook. -pub(in crate::interpreter) fn eval_method_call_result( - object: RuntimeCellHandle, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_method_call_result_with_evaluated_args( - object, - method_name, - positional_args(evaluated_args), - context, - values, - ) -} - -/// Dispatches an object method call while preserving named-argument metadata for eval methods. -pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( - object: RuntimeCellHandle, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Ok(identity) = values.object_identity(object) else { - let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; - return values.method_call(object, method_name, evaluated_args); - }; - if let Some(target) = context.closure_object_target(identity).cloned() { - if let Some(result) = - eval_closure_object_method_result(target, method_name, evaluated_args.clone(), context, values)? - { - return Ok(result); - } - } - if let Some(attribute_metadata) = context.eval_reflection_attribute(identity).cloned() { - if method_name.eq_ignore_ascii_case("newInstance") { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - return eval_reflection_attribute_new_instance_result( - attribute_metadata.attribute(), - context, - values, - ); - } - if method_name.eq_ignore_ascii_case("getArguments") { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let Some(args) = attribute_metadata.attribute().args() else { - return Err(EvalStatus::RuntimeFatal); - }; - return eval_class_attribute_args_result(args, values); - } - if method_name.eq_ignore_ascii_case("getTarget") { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - return values.int(attribute_metadata.target() as i64); - } - if method_name.eq_ignore_ascii_case("isRepeated") { - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - return values.bool_value(attribute_metadata.is_repeated()); - } - } - if let Some(result) = eval_reflection_parameter_legacy_type_predicate_result( - object, - method_name, - evaluated_args.clone(), - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_parameter_to_string_result( - object, - method_name, - evaluated_args.clone(), - values, - )? { - return Ok(result); - } - if let Some(result) = - eval_reflection_type_to_string_result(object, method_name, evaluated_args.clone(), values)? - { - return Ok(result); - } - if let Some(result) = eval_reflection_class_to_string_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_implements_interface_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_is_subclass_of_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_is_instance_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_source_location_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_basic_metadata_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_has_method_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_has_property_result( - object, - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_has_constant_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_enum_methods_result( - object, - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_relation_objects_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_trait_aliases_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_constant_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_constants_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_default_properties_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_static_properties_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_static_property_value_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_set_static_property_value_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_function_invoke_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_method_invoke_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_function_method_metadata_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_function_method_to_string_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_method_prototype_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_set_accessible_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_property_hooks_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_property_is_initialized_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_property_lazy_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_property_to_string_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_constant_to_string_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_enum_case_get_enum_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_property_get_value_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_property_raw_value_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_property_set_value_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_reflection_constant_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_reflection_constants_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_members_result( - object, - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(result) = eval_reflection_class_get_member_result( - object, - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(result); - } - if let Some(instance) = eval_reflection_class_new_instance_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(instance); - } - if let Some(instance) = eval_reflection_class_new_instance_without_constructor_result( - identity, - method_name, - evaluated_args.clone(), - context, - values, - )? { - return Ok(instance); - } - let Some(class) = context.dynamic_object_class(identity) else { - let class_name = runtime_object_class_name(object, values)?; - if method_name.eq_ignore_ascii_case("__clone") { - if let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata(&class_name, method_name, values)? - { - if is_static || is_abstract { - return Err(EvalStatus::RuntimeFatal); - } - if validate_eval_member_access(&declaring_class, visibility, context).is_err() { - return eval_throw_method_access_error( - &declaring_class, - method_name, - visibility, - context, - values, - ); - } - } - } - return eval_native_method_with_evaluated_args( - object, - &class_name, - method_name, - evaluated_args, - context, - values, - ); - }; - let called_class_name = class.name().to_string(); - if eval_enum_static_builtin_applies(&called_class_name, method_name, context).is_some() { - return eval_enum_builtin_static_method_result( - &called_class_name, - method_name, - evaluated_args, - context, - values, - ); - } - let mut inaccessible_method = None; - if let Some((class_name, method)) = - eval_dynamic_method_for_call(&called_class_name, method_name, context) - { - if method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - if validate_eval_member_access(&class_name, method.visibility(), context).is_ok() { - if method.is_static() { - return eval_dynamic_static_method_with_values( - &class_name, - &called_class_name, - &method, - evaluated_args, - context, - values, - ); - } - return eval_dynamic_method_with_values( - &class_name, - &called_class_name, - &method, - object, - evaluated_args, - context, - values, - ); - } - inaccessible_method = Some((class_name, method)); - } - if inaccessible_method.is_none() { - if let Some(parent) = context.class_native_parent_name(&called_class_name) { - if let Some((declaring_class, _, _, _)) = - eval_aot_method_dispatch_metadata_in_hierarchy( - &parent, - method_name, - context, - values, - )? - { - return eval_native_method_with_evaluated_args_bridge_scope( - object, - &parent, - method_name, - evaluated_args, - Some(&declaring_class), - Some(&called_class_name), - context, - values, - ); - } - if eval_native_instance_magic_method_available(&parent, context, values)? { - return eval_native_method_with_evaluated_args( - object, - &parent, - method_name, - evaluated_args, - context, - values, - ); - } - } - } - if let Some(result) = eval_magic_instance_method_call( - object, - &called_class_name, - method_name, - evaluated_args, - context, - values, - )? { - return Ok(result); - } - if let Some((declaring_class, method)) = inaccessible_method { - return eval_throw_method_access_error( - &declaring_class, - method.name(), - method.visibility(), - context, - values, - ); - } - eval_throw_undefined_method_call_error(&called_class_name, method_name, context, values) -} - -/// Dispatches PHP-visible methods on eval-backed `Closure` objects. -fn eval_closure_object_method_result( - target: EvalClosureObjectTarget, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if method_name.eq_ignore_ascii_case("__invoke") { - return eval_closure_object_invoke_result(target, evaluated_args, context, values) - .map(Some); - } - if method_name.eq_ignore_ascii_case("bindTo") { - let (bound_this, bound_scope, rebinds_function_scope) = - eval_closure_bind_to_args(evaluated_args, context, values)?; - return eval_closure_bind_target( - target, - bound_this, - bound_scope, - rebinds_function_scope, - context, - values, - ) - .map(Some); - } - if !method_name.eq_ignore_ascii_case("call") { - return Ok(None); - } - let (bound_this, call_args) = eval_closure_call_split_args(evaluated_args)?; - match target { - EvalClosureObjectTarget::Named(name) => { - if context.closure(&name).is_some() { - let callable = EvaluatedCallable::BoundClosure { - name, - bound_this: Some(bound_this), - bound_scope: None, - }; - return eval_evaluated_callable_with_by_value_call_args( - &callable, call_args, context, values, - ) - .map(Some); - } - eval_closure_call_warning_null( - "Cannot rebind scope of closure created from function", - values, - ) - .map(Some) - } - EvalClosureObjectTarget::BoundNamed { - name, bound_scope, .. - } => { - if context.closure(&name).is_some() { - let callable = EvaluatedCallable::BoundClosure { - name, - bound_this: Some(bound_this), - bound_scope, - }; - return eval_evaluated_callable_with_by_value_call_args( - &callable, call_args, context, values, - ) - .map(Some); - } - eval_closure_call_warning_null( - "Cannot rebind scope of closure created from function", - values, - ) - .map(Some) - } - EvalClosureObjectTarget::InvokableObject { object } => { - if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { - return eval_closure_call_warning_null( - "Cannot rebind scope of closure created from method", - values, - ) - .map(Some); - } - let callable = EvaluatedCallable::InvokableObject { object: bound_this }; - eval_evaluated_callable_with_by_value_call_args(&callable, call_args, context, values) - .map(Some) - } - EvalClosureObjectTarget::ObjectMethod { - object, - method, - called_class: _, - native_class, - bridge_scope, - } => { - if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { - return eval_closure_call_warning_null( - "Cannot rebind scope of closure created from method", - values, - ) - .map(Some); - } - let called_class = Some(eval_closure_bound_object_class_name( - bound_this, context, values, - )?); - let callable = EvaluatedCallable::ObjectMethod { - object: bound_this, - method, - called_class, - native_class, - bridge_scope, - }; - eval_evaluated_callable_with_by_value_call_args( - &callable, call_args, context, values, - ) - .map(Some) - } - EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( - "Cannot bind an instance to a static closure", - values, - ) - .map(Some), - } -} - -/// Invokes the callable target retained behind a PHP-visible eval `Closure` object. -fn eval_closure_object_invoke_result( - target: EvalClosureObjectTarget, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let callable = match target { - EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named { - display_name: name.clone(), - name, - }, - EvalClosureObjectTarget::BoundNamed { - name, - bound_this, - bound_scope, - } => EvaluatedCallable::BoundClosure { - name, - bound_this, - bound_scope, - }, - EvalClosureObjectTarget::InvokableObject { object } => { - EvaluatedCallable::InvokableObject { object } - } - EvalClosureObjectTarget::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - } => EvaluatedCallable::ObjectMethod { - object, - method, - called_class, - native_class, - bridge_scope, - }, - EvalClosureObjectTarget::StaticMethod { - class_name, - method, - called_class, - native_class, - bridge_scope, - } => EvaluatedCallable::StaticMethod { - class_name, - method, - called_class, - native_class, - bridge_scope, - }, - }; - eval_evaluated_callable_with_call_array_args(&callable, evaluated_args, context, values) -} - -/// Splits `Closure::call()` arguments into the bound object and forwarded closure args. -fn eval_closure_call_split_args( - evaluated_args: Vec, -) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { - let mut bound_this = None; - let mut consumed_positional_receiver = false; - let mut call_args = Vec::with_capacity(evaluated_args.len().saturating_sub(1)); - - for arg in evaluated_args { - if arg - .name - .as_deref() - .is_some_and(|name| name.eq_ignore_ascii_case("newThis")) - { - if bound_this.replace(arg.value).is_some() { - return Err(EvalStatus::RuntimeFatal); - } - continue; - } - if arg.name.is_none() && !consumed_positional_receiver && bound_this.is_none() { - consumed_positional_receiver = true; - bound_this = Some(arg.value); - continue; - } - call_args.push(arg); - } - - bound_this - .map(|receiver| (receiver, call_args)) - .ok_or(EvalStatus::RuntimeFatal) -} - -/// Returns whether `Closure::call()` may bind a method closure to the new object. -fn eval_closure_call_bound_class_matches( - original_object: RuntimeCellHandle, - bound_this: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let original_class = eval_closure_bound_object_class_name(original_object, context, values)?; - let bound_class = eval_closure_bound_object_class_name(bound_this, context, values)?; - Ok(original_class.eq_ignore_ascii_case(&bound_class)) -} - -/// Returns whether `Closure::bind()` may bind a method closure to the new object. -fn eval_closure_bind_bound_class_matches_method( - original_object: RuntimeCellHandle, - method_name: &str, - bound_this: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Some(declaring_class) = - eval_closure_bind_method_declaring_class(original_object, method_name, context, values)? - else { - return Ok(false); - }; - eval_closure_object_is_instance_of(bound_this, &declaring_class, context, values) -} - -/// Resolves the class that declares the method captured by a method Closure target. -fn eval_closure_bind_method_declaring_class( - original_object: RuntimeCellHandle, - method_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let original_class = eval_closure_bound_object_class_name(original_object, context, values)?; - if let Some((declaring_class, method)) = context.class_method(&original_class, method_name) { - if method.is_static() || method.is_abstract() { - return Ok(None); - } - return Ok(Some(declaring_class)); - } - let native_class = context - .class_native_parent_name(&original_class) - .unwrap_or_else(|| original_class.clone()); - let Some((_, _, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy(&native_class, method_name, context, values)? - else { - return Ok(None); - }; - if is_static || is_abstract { - return Ok(None); - } - let declaring_class = eval_aot_method_declaring_class(&native_class, method_name, values)?; - Ok(Some(declaring_class)) -} - -/// Returns whether an object is an instance of the requested eval or generated class name. -fn eval_closure_object_is_instance_of( - object: RuntimeCellHandle, - class_name: &str, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let object_class = eval_closure_bound_object_class_name(object, context, values)?; - Ok(eval_static_syntax_object_matches_class( - &object_class, - class_name, - context, - )) -} - -/// Emits PHP's `Closure::call()` warning and returns `null`. -fn eval_closure_call_warning_null( - message: &str, - values: &mut impl RuntimeValueOps, -) -> Result { - values.warning(message)?; - values.null() -} - -/// Dispatches an invokable object through `__invoke()` without enforcing hook visibility. -pub(in crate::interpreter) fn eval_invokable_object_call_result( - object: RuntimeCellHandle, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let Ok(identity) = values.object_identity(object) else { - let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; - return values.method_call(object, "__invoke", evaluated_args); - }; - let Some(class) = context.dynamic_object_class(identity) else { - let class_name = runtime_object_class_name(object, values)?; - let Some((_, _, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy( - &class_name, - "__invoke", - context, - values, - )? - else { - return eval_throw_object_not_callable_error(&class_name, context, values); - }; - if is_static || is_abstract { - return Err(EvalStatus::RuntimeFatal); - } - return eval_native_method_with_evaluated_args_unchecked( - object, - &class_name, - "__invoke", - evaluated_args, - context, - values, - ); - }; - let called_class_name = class.name().to_string(); - let Some((declaring_class, method)) = context.class_method(&called_class_name, "__invoke") - else { - if let Some(native_class_name) = - eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? - { - return eval_native_method_with_evaluated_args_unchecked_bridge_scope( - object, - &native_class_name, - "__invoke", - evaluated_args, - Some(&native_class_name), - Some(&called_class_name), - context, - values, - ); - } - return eval_throw_object_not_callable_error(&called_class_name, context, values); - }; - if method.is_static() || method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - eval_dynamic_method_with_values( - &declaring_class, - &called_class_name, - &method, - object, - evaluated_args, - context, - values, - ) -} - -/// Rejects non-invokable eval-declared objects before dynamic-call arguments are evaluated. -pub(in crate::interpreter) fn eval_invokable_object_precheck( - object: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Ok(identity) = values.object_identity(object) else { - return Ok(()); - }; - let Some(class) = context.dynamic_object_class(identity) else { - let class_name = runtime_object_class_name(object, values)?; - let Some((_, _, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata_in_hierarchy( - &class_name, - "__invoke", - context, - values, - )? - else { - return eval_throw_object_not_callable_error(&class_name, context, values); - }; - if is_static || is_abstract { - return Err(EvalStatus::RuntimeFatal); - } - return Ok(()); - }; - let called_class_name = class.name().to_string(); - let Some((_, method)) = context.class_method(&called_class_name, "__invoke") else { - if eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? - .is_some() - { - return Ok(()); - } - return eval_throw_object_not_callable_error(&called_class_name, context, values); - }; - if method.is_static() || method.is_abstract() { - return Err(EvalStatus::RuntimeFatal); - } - Ok(()) -} - -/// Returns the generated/AOT class that can dispatch an inherited `__invoke()` hook. -pub(in crate::interpreter) fn eval_dynamic_class_native_invokable_method_class( - called_class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, _, is_static, is_abstract)) = - eval_dynamic_class_native_method_metadata(called_class_name, "__invoke", context, values)? - else { - return Ok(None); - }; - if is_static || is_abstract { - return Ok(None); - } - Ok(Some(declaring_class)) -} - -/// Returns generated/AOT method metadata inherited by an eval-declared class. -pub(in crate::interpreter) fn eval_dynamic_class_native_method_metadata( - called_class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(parent) = context.class_native_parent_name(called_class_name) else { - return Ok(None); - }; - eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values) -} - -/// Dispatches a missing or inaccessible eval instance method through `__call()`. -pub(in crate::interpreter) fn eval_magic_instance_method_call( - object: RuntimeCellHandle, - called_class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, method)) = context.class_method(called_class_name, "__call") else { - return Ok(None); - }; - if method.is_static() || method.is_abstract() { - return Ok(None); - } - let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; - eval_dynamic_method_with_values( - &declaring_class, - called_class_name, - &method, - object, - magic_args, - context, - values, - ) - .map(Some) -} - -/// Dispatches a missing or inaccessible eval static method through `__callStatic()`. -pub(in crate::interpreter) fn eval_magic_static_method_call( - class_name: &str, - called_class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, method)) = context.class_method(class_name, "__callStatic") else { - return Ok(None); - }; - if !method.is_static() || method.is_abstract() { - return Ok(None); - } - let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; - eval_dynamic_static_method_with_values( - &declaring_class, - called_class_name, - &method, - magic_args, - context, - values, - ) - .map(Some) -} - -/// Builds the two synthetic arguments passed to `__call()` and `__callStatic()`. -fn eval_magic_call_args( - method_name: &str, - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let method = values.string(method_name)?; - let args = eval_magic_call_arg_array(evaluated_args, values)?; - Ok(positional_args(vec![method, args])) -} - -/// Materializes PHP's `$args` array for a magic method fallback. -fn eval_magic_call_arg_array( - evaluated_args: Vec, - values: &mut impl RuntimeValueOps, -) -> Result { - let contains_named = evaluated_args.iter().any(|arg| arg.name.is_some()); - let mut args = if contains_named { - values.assoc_new(evaluated_args.len())? - } else { - values.array_new(evaluated_args.len())? - }; - let mut next_positional = 0_i64; - for arg in evaluated_args { - let key = if let Some(name) = arg.name { - values.string(&name)? - } else { - let key = values.int(next_positional)?; - next_positional = next_positional - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - key - }; - args = values.array_set(args, key, arg.value)?; - } - Ok(args) -} - -/// Returns the runtime-visible class name for a non-eval object receiver. -pub(in crate::interpreter) fn runtime_object_class_name( - object: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let class_name = values.object_class_name(object)?; - let bytes = values.string_bytes(class_name); - values.release(class_name)?; - String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) -} - -/// Instantiates the class named by a materialized eval `ReflectionClass` object. -fn eval_reflection_class_new_instance_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let direct_new_instance = method_name.eq_ignore_ascii_case("newInstance"); - let constructor_args = if direct_new_instance { - eval_reflection_constructor_by_value_args(evaluated_args) - } else if method_name.eq_ignore_ascii_case("newInstanceArgs") { - eval_reflection_class_new_instance_args(evaluated_args, context, values)? - } else { - return Ok(None); - }; - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - if let Some(message) = - eval_reflection_eval_instantiation_error_message(&reflected_name, context) - { - return eval_throw_error(&message, context, values); - } - if let Some(class) = context.class(&reflected_name).cloned() { - if let Some((_, constructor)) = context.class_method(class.name(), "__construct") { - if constructor.visibility() != EvalVisibility::Public { - return eval_throw_reflection_exception( - &format!( - "Access to non-public constructor of class {}", - class.name() - ), - context, - values, - ); - } - } - return eval_reflection_public_constructor_scope(context, values, |context, values| { - let constructor_name = - format!("{}::__construct", class.name().trim_start_matches('\\')); - let by_ref_mode = EvalByRefBindingMode::WarnByValue { - callable_name: &constructor_name, - }; - let mut scope = ElephcEvalScope::new(); - eval_dynamic_class_new_object_with_ref_mode( - &class, - constructor_args, - by_ref_mode, - context, - &mut scope, - values, - ) - .map(Some) - }); - } - let class_name = context - .resolve_class_name(&reflected_name) - .unwrap_or(reflected_name); - if let Some(error) = eval_reflection_aot_class_public_instantiation_error(&class_name, values)? - { - return eval_throw_reflection_instantiation_error(error, context, values); - } - eval_reflection_public_constructor_scope(context, values, |context, values| { - let constructor_name = format!("{}::__construct", class_name.trim_start_matches('\\')); - let by_ref_mode = EvalByRefBindingMode::WarnByValue { - callable_name: &constructor_name, - }; - let instance = values.new_object(&class_name)?; - eval_native_constructor_with_evaluated_args_and_ref_mode( - &class_name, - instance, - constructor_args, - by_ref_mode, - context, - values, - )?; - Ok(Some(instance)) - }) -} - -/// Removes caller writeback targets for ReflectionClass::newInstance() by-value forwarding. -fn eval_reflection_constructor_by_value_args( - evaluated_args: Vec, -) -> Vec { - evaluated_args - .into_iter() - .map(|arg| EvaluatedCallArg { - name: arg.name, - value: arg.value, - ref_target: None, - }) - .collect() -} - -/// Expands the single `ReflectionClass::newInstanceArgs()` array argument. -fn eval_reflection_class_new_instance_args( - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; - eval_array_call_arg_values(args[0], context, values) -} - -/// Runs ReflectionClass construction with only public constructor visibility. -fn eval_reflection_public_constructor_scope( - context: &mut ElephcEvalContext, - values: &mut V, - action: impl FnOnce(&mut ElephcEvalContext, &mut V) -> Result, -) -> Result { - context.push_class_scope(String::new()); - let result = action(context, values); - context.pop_class_scope(); - result -} - -/// Allocates the class named by a materialized eval `ReflectionClass` without running `__construct()`. -fn eval_reflection_class_new_instance_without_constructor_result( - identity: u64, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if !method_name.eq_ignore_ascii_case("newInstanceWithoutConstructor") { - return Ok(None); - } - if !evaluated_args.is_empty() { - return Err(EvalStatus::RuntimeFatal); - } - let Some(reflected_name) = context - .eval_reflection_class_name(identity) - .map(str::to_string) - else { - return Ok(None); - }; - if let Some(message) = - eval_reflection_eval_instantiation_error_message(&reflected_name, context) - { - return eval_throw_error(&message, context, values); - } - if let Some(class) = context.class(&reflected_name).cloned() { - let mut scope = ElephcEvalScope::new(); - return eval_dynamic_class_allocate_object(&class, context, &mut scope, values).map(Some); - } - if context.has_interface(&reflected_name) - || context.has_trait(&reflected_name) - || context.has_enum(&reflected_name) - { - return Err(EvalStatus::RuntimeFatal); - } - let class_name = context - .resolve_class_name(&reflected_name) - .unwrap_or(reflected_name); - if let Some(message) = - eval_reflection_aot_class_without_constructor_error(&class_name, values)? - { - return eval_throw_error(&message, context, values); - } - values.new_object(&class_name).map(Some) -} - -/// Builds PHP's reflection instantiation error for eval non-instantiable class-likes. -fn eval_reflection_eval_instantiation_error_message( - reflected_name: &str, - context: &ElephcEvalContext, -) -> Option { - if let Some(class) = context.class(reflected_name) { - if class.is_abstract() { - return Some(format!("Cannot instantiate abstract class {}", class.name())); - } - if let Some(enum_decl) = context.enum_decl(class.name()) { - return Some(format!("Cannot instantiate enum {}", enum_decl.name())); - } - return None; - } - if let Some(interface) = context.interface(reflected_name) { - return Some(format!("Cannot instantiate interface {}", interface.name())); - } - if let Some(trait_decl) = context.trait_decl(reflected_name) { - return Some(format!("Cannot instantiate trait {}", trait_decl.name())); - } - context - .enum_decl(reflected_name) - .map(|enum_decl| format!("Cannot instantiate enum {}", enum_decl.name())) -} - -/// Instantiates an attribute class for `ReflectionAttribute::newInstance()`. -fn eval_reflection_attribute_new_instance_result( - attribute: &EvalAttribute, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let args = eval_reflection_attribute_evaluated_args(attribute, values)?; - if let Some(class) = context.class(attribute.name()).cloned() { - let mut scope = ElephcEvalScope::new(); - return eval_dynamic_class_new_object(&class, args, context, &mut scope, values); - } - let class_name = context - .resolve_class_name(attribute.name()) - .unwrap_or_else(|| attribute.name().trim_start_matches('\\').to_string()); - if !values.class_exists(&class_name)? { - return values.null(); - } - let object = values.new_object(&class_name)?; - if let Err(err) = eval_native_constructor_with_evaluated_args( - &class_name, - object, - args, - context, - values, - ) { - let _ = values.release(object); - return Err(err); - } - Ok(object) -} - -/// Materializes eval attribute literal arguments as evaluated constructor args. -fn eval_reflection_attribute_evaluated_args( - attribute: &EvalAttribute, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(args) = attribute.args() else { - return Err(EvalStatus::RuntimeFatal); - }; - args.iter() - .map(|arg| { - Ok(EvaluatedCallArg { - name: arg.name().map(str::to_string), - value: eval_reflection_attribute_arg_value(arg.value(), values)?, - ref_target: None, - }) - }) - .collect() -} - -/// Materializes one eval attribute literal as a constructor argument cell. -fn eval_reflection_attribute_arg_value( - arg: &EvalAttributeArg, - values: &mut impl RuntimeValueOps, -) -> Result { - match arg { - EvalAttributeArg::String(value) => values.string(value), - EvalAttributeArg::Int(value) => values.int(*value), - EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), - EvalAttributeArg::Bool(value) => values.bool_value(*value), - EvalAttributeArg::Null => values.null(), - EvalAttributeArg::Array(elements) => { - eval_reflection_attribute_array_arg_value(elements, values) - } - EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { - eval_reflection_attribute_arg_value(value, values) - } - } -} - -/// Materializes one retained attribute array literal for constructor calls. -fn eval_reflection_attribute_array_arg_value( - elements: &[EvalAttributeArg], - values: &mut impl RuntimeValueOps, -) -> Result { - let mut result = if elements - .iter() - .any(|element| element.name().is_some() || element.int_key().is_some()) - { - values.assoc_new(elements.len())? - } else { - values.array_new(elements.len())? - }; - for (index, element) in elements.iter().enumerate() { - let key = match element.name() { - Some(name) => values.string(name)?, - None => values.int(element.int_key().unwrap_or(index as i64))?, - }; - let value = eval_reflection_attribute_arg_value(element.value(), values)?; - result = values.array_set(result, key, value)?; - } - Ok(result) -} - -/// Resolves the method metadata visible from the current class scope. -pub(in crate::interpreter) fn eval_dynamic_method_for_call( - object_class_name: &str, - method_name: &str, - context: &ElephcEvalContext, -) -> Option<(String, EvalClassMethod)> { - if let Some(current_class) = context.current_class_scope() { - if context.class_is_a(object_class_name, current_class, false) { - if let Some((declaring_class, method)) = - context.class_own_method(current_class, method_name) - { - if method.visibility() == EvalVisibility::Private { - return Some((declaring_class, method)); - } - } - } - } - context.class_method(object_class_name, method_name) -} - -/// Returns whether the current eval class scope can access one declared member. -pub(in crate::interpreter) fn validate_eval_member_access( - declaring_class: &str, - visibility: EvalVisibility, - context: &ElephcEvalContext, -) -> Result<(), EvalStatus> { - if visibility == EvalVisibility::Public { - return Ok(()); - } - let Some(current_class) = context.current_class_scope() else { - return Err(EvalStatus::RuntimeFatal); - }; - match visibility { - EvalVisibility::Public => Ok(()), - EvalVisibility::Private => same_eval_class_name(current_class, declaring_class) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal), - EvalVisibility::Protected => { - eval_classes_are_related(current_class, declaring_class, context) - .then_some(()) - .ok_or(EvalStatus::RuntimeFatal) - } - } -} - -/// Returns true when two PHP class names refer to the same eval class. -fn same_eval_class_name(left: &str, right: &str) -> bool { - left.trim_start_matches('\\') - .eq_ignore_ascii_case(right.trim_start_matches('\\')) -} - -/// Returns true when two eval or generated classes are in the same inheritance family. -fn eval_classes_are_related(left: &str, right: &str, context: &ElephcEvalContext) -> bool { - same_eval_class_name(left, right) - || context.class_is_a(left, right, false) - || context.class_is_a(right, left, false) - || native_class_is_a(left, right, context) - || native_class_is_a(right, left, context) -} - -/// Returns true when generated AOT parent metadata proves one class extends another. -fn native_class_is_a(class_name: &str, target: &str, context: &ElephcEvalContext) -> bool { - let mut current = class_name.trim_start_matches('\\').to_string(); - let target = target.trim_start_matches('\\'); - let mut seen = std::collections::HashSet::new(); - loop { - if !seen.insert(current.to_ascii_lowercase()) { - return false; - } - if same_eval_class_name(¤t, target) { - return true; - } - let Some(parent) = context.native_class_parent(¤t) else { - return false; - }; - current = parent.to_string(); - } -} - -/// Returns the calling-scope class when PHP's private-method shadowing rule -/// selects it as the dispatch scope: `$obj->m()` from class scope S calls S's -/// own private instance method `m` — never the receiver's override — when S -/// declares one and the receiver is an instance of S. The returned scope -/// string drives the native bridge's hidden private shadow slot directly. -pub(in crate::interpreter) fn eval_private_scope_shadow_bridge_scope( - receiver_class: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(scope_class) = context.current_class_scope() else { - return Ok(None); - }; - let scope_class = scope_class.to_string(); - if !same_eval_class_name(receiver_class, &scope_class) - && !native_class_is_a(receiver_class, &scope_class, context) - { - return Ok(None); - } - let Some((declaring_class, visibility, is_static, is_abstract)) = - eval_aot_method_dispatch_metadata(&scope_class, method_name, values)? - else { - return Ok(None); - }; - if visibility == EvalVisibility::Private - && !is_static - && !is_abstract - && same_eval_class_name(&declaring_class, &scope_class) - { - Ok(Some(declaring_class)) - } else { - Ok(None) - } -} - -/// Binds method parameters into a fresh method scope and marks by-reference params as aliases. -pub(in crate::interpreter) fn bind_method_scope_args( - method_scope: &mut ElephcEvalScope, - params: &[String], - parameter_is_by_ref: &[bool], - bound_args: &[BoundMethodArg], -) { - for (position, (name, bound_arg)) in params.iter().zip(bound_args.iter()).enumerate() { - if parameter_is_by_ref.get(position).copied().unwrap_or(false) { - method_scope.set_reference( - name.clone(), - name.clone(), - bound_arg.value, - ScopeCellOwnership::Borrowed, - ); - if let Some(target) = bound_arg.ref_target.clone() { - method_scope.set_reference_target(name.clone(), target); - } - } else { - method_scope.set(name.clone(), bound_arg.value, ScopeCellOwnership::Borrowed); - } - } - alias_duplicate_method_ref_args(method_scope, params, bound_args); -} - -/// Creates local aliases when two by-reference method parameters point at the same caller variable. -fn alias_duplicate_method_ref_args( - method_scope: &mut ElephcEvalScope, - params: &[String], - bound_args: &[BoundMethodArg], -) { - for (position, bound_arg) in bound_args.iter().enumerate() { - let Some(target) = bound_arg.ref_target.as_ref() else { - continue; - }; - let Some(param) = params.get(position) else { - continue; - }; - for previous_position in 0..position { - let Some(previous_target) = bound_args[previous_position].ref_target.as_ref() else { - continue; - }; - if !same_method_ref_target(target, previous_target) { - continue; - } - if let Some(previous_param) = params.get(previous_position) { - method_scope.set_reference( - param.clone(), - previous_param.clone(), - bound_args[previous_position].value, - ScopeCellOwnership::Borrowed, - ); - } - break; - } - } -} - -/// Returns true when two evaluated arguments target the same caller-side variable. -fn same_method_ref_target(left: &EvalReferenceTarget, right: &EvalReferenceTarget) -> bool { - match (left, right) { - ( - EvalReferenceTarget::Variable { - scope: left_scope, - name: left_name, - }, - EvalReferenceTarget::Variable { - scope: right_scope, - name: right_name, - }, - ) => left_scope == right_scope && left_name == right_name, - ( - EvalReferenceTarget::ArrayElement { - scope: left_scope, - array_name: left_name, - index: left_index, - }, - EvalReferenceTarget::ArrayElement { - scope: right_scope, - array_name: right_name, - index: right_index, - }, - ) => left_scope == right_scope && left_name == right_name && left_index == right_index, - ( - EvalReferenceTarget::NestedArrayElement { - array_target: left_target, - index: left_index, - }, - EvalReferenceTarget::NestedArrayElement { - array_target: right_target, - index: right_index, - }, - ) => left_index == right_index && same_method_ref_target(left_target, right_target), - ( - EvalReferenceTarget::ObjectProperty { - object: left_object, - property: left_property, - access_scope: left_access_scope, - }, - EvalReferenceTarget::ObjectProperty { - object: right_object, - property: right_property, - access_scope: right_access_scope, - }, - ) => { - left_object == right_object - && left_property == right_property - && left_access_scope == right_access_scope - } - ( - EvalReferenceTarget::Cell { cell: left_cell }, - EvalReferenceTarget::Cell { cell: right_cell }, - ) => left_cell == right_cell, - ( - EvalReferenceTarget::InvokerSlot { - slot: left_slot, - source_tag: left_source_tag, - }, - EvalReferenceTarget::InvokerSlot { - slot: right_slot, - source_tag: right_source_tag, - }, - ) => left_slot == right_slot && left_source_tag == right_source_tag, - ( - EvalReferenceTarget::StaticProperty { - class_name: left_class_name, - property: left_property, - access_scope: left_access_scope, - }, - EvalReferenceTarget::StaticProperty { - class_name: right_class_name, - property: right_property, - access_scope: right_access_scope, - }, - ) => { - left_class_name == right_class_name - && left_property == right_property - && left_access_scope == right_access_scope - } - _ => false, - } -} - -/// Writes completed by-reference method parameter values back to their caller-side variables. -pub(in crate::interpreter) fn write_back_method_ref_args( - params: &[String], - bound_args: &[BoundMethodArg], - method_scope: &ElephcEvalScope, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for (position, bound_arg) in bound_args.iter().enumerate() { - let Some(param) = params.get(position) else { - continue; - }; - if let Some(target) = bound_arg.ref_target.as_ref() { - let Some(entry) = method_scope - .entry(param) - .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) - else { - continue; - }; - write_back_method_ref_target(target, entry.cell(), context, values)?; - } - write_back_method_variadic_ref_args(param, bound_arg, method_scope, context, values)?; - } - Ok(()) -} - -/// Writes element-level changes from a by-reference variadic method parameter back to callers. -fn write_back_method_variadic_ref_args( - param: &str, - bound_arg: &BoundMethodArg, - method_scope: &ElephcEvalScope, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if bound_arg.variadic_ref_targets.is_empty() { - return Ok(()); - } - let Some(entry) = method_scope - .entry(param) - .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) - else { - return Ok(()); - }; - if entry.cell() != bound_arg.value { - return Ok(()); - } - for (key, target) in &bound_arg.variadic_ref_targets { - let value = values.array_get(entry.cell(), *key)?; - write_back_method_ref_target(target, value, context, values)?; - } - Ok(()) -} - -/// Stores one by-reference result in the original caller-side target. -pub(in crate::interpreter) fn write_back_method_ref_target( - target: &EvalReferenceTarget, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - match target { - EvalReferenceTarget::Variable { scope, name } => { - let Some(scope) = (unsafe { scope.as_mut() }) else { - return Err(EvalStatus::RuntimeFatal); - }; - for replaced in set_scope_cell( - context, - scope, - name.clone(), - value, - ScopeCellOwnership::Borrowed, - )? { - values.release(replaced)?; - } - Ok(()) - } - EvalReferenceTarget::ArrayElement { - scope, - array_name, - index, - } => { - let Some(scope) = (unsafe { scope.as_mut() }) else { - return Err(EvalStatus::RuntimeFatal); - }; - write_back_method_array_element_ref_target( - scope, array_name, *index, value, context, values, - ) - } - EvalReferenceTarget::NestedArrayElement { - array_target, - index, - } => write_back_method_nested_array_element_ref_target( - array_target, - *index, - value, - context, - values, - ), - EvalReferenceTarget::ObjectProperty { - object, - property, - access_scope, - } => write_back_method_object_property_ref_target( - *object, - property, - access_scope.clone(), - value, - context, - values, - ), - EvalReferenceTarget::StaticProperty { - class_name, - property, - access_scope, - } => write_back_method_static_property_ref_target( - class_name, - property, - access_scope.clone(), - value, - context, - values, - ), - EvalReferenceTarget::Cell { .. } => Ok(()), - EvalReferenceTarget::InvokerSlot { slot, source_tag } => { - write_back_invoker_slot_ref_target(*slot, *source_tag, value, values) - } - } -} - -/// Reads a value from a native descriptor-invoker by-reference slot. -fn eval_invoker_slot_ref_target_value( - slot: usize, - source_tag: u64, - values: &mut impl RuntimeValueOps, -) -> Result { - match source_tag { - EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { - let word = unsafe { *(slot as *const u64) }; - values.raw_word_value(source_tag, word) - } - EVAL_TAG_STRING => { - let words = unsafe { *(slot as *const [u64; 2]) }; - values.raw_string_value(words[0], words[1]) - } - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { - let word = unsafe { *(slot as *const u64) }; - values.raw_word_value(source_tag, word) - } - EVAL_TAG_MIXED => { - let value = unsafe { *(slot as *const RuntimeCellHandle) }; - values.retain(value) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Writes a value back into a native descriptor-invoker by-reference slot. -fn write_back_invoker_slot_ref_target( - slot: usize, - source_tag: u64, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - match source_tag { - EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { - let word = values.raw_value_word(value)?; - unsafe { - *(slot as *mut u64) = word; - } - Ok(()) - } - EVAL_TAG_STRING => write_back_invoker_string_slot(slot, value, values), - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { - write_back_invoker_heap_slot(slot, source_tag, value, values) - } - EVAL_TAG_MIXED => { - let retained = values.retain(value)?; - let replaced = unsafe { - let slot = slot as *mut RuntimeCellHandle; - let replaced = *slot; - *slot = retained; - replaced - }; - values.release(replaced) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Writes a boxed string value back into a native descriptor-invoker string slot. -fn write_back_invoker_string_slot( - slot: usize, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if values.type_tag(value)? != EVAL_TAG_STRING { - return Err(EvalStatus::RuntimeFatal); - } - let ptr = values.raw_value_word(value)?; - let len = values.raw_value_high_word(value)?; - let retained = values.retain_raw_string_words(ptr, len)?; - let replaced = unsafe { - let slot = slot as *mut [u64; 2]; - let replaced = *slot; - *slot = [retained.0, retained.1]; - replaced - }; - values.release_raw_string_words(replaced[0], replaced[1]) -} - -/// Writes a boxed heap value back into a native descriptor-invoker raw heap slot. -fn write_back_invoker_heap_slot( - slot: usize, - source_tag: u64, - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if values.type_tag(value)? != source_tag { - return Err(EvalStatus::RuntimeFatal); - } - let word = values.raw_value_word(value)?; - let retained = values.retain_raw_heap_word(word)?; - let replaced = unsafe { - let slot = slot as *mut u64; - let replaced = *slot; - *slot = retained; - replaced - }; - values.release_raw_heap_word(replaced) -} - -/// Stores one by-reference method result in a caller-side array element. -fn write_back_method_array_element_ref_target( - scope: &mut ElephcEvalScope, - array_name: &str, - index: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let mut ownership = ScopeCellOwnership::Owned; - let array = if let Some(existing) = - scope_entry(context, scope, array_name).filter(|entry| entry.flags().is_visible()) - { - if values.is_array_like(existing.cell())? { - ownership = existing.flags().ownership; - values.array_clone_shallow(existing.cell())? - } else { - eval_new_array_for_index(index, values)? - } - } else { - eval_new_array_for_index(index, values)? - }; - let array = values.array_set(array, index, value)?; - for replaced in set_scope_cell(context, scope, array_name.to_string(), array, ownership)? { - values.release(replaced)?; - } - Ok(()) -} - -/// Stores one by-reference method result in an element of a nested caller-side array target. -fn write_back_method_nested_array_element_ref_target( - array_target: &EvalReferenceTarget, - index: RuntimeCellHandle, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let current = eval_reference_target_value(array_target, context, values)?; - let array = if values.is_array_like(current)? { - values.array_clone_shallow(current)? - } else { - eval_new_array_for_index(index, values)? - }; - let array = values.array_set(array, index, value)?; - write_back_method_ref_target(array_target, array, context, values) -} - -/// Stores one by-reference method result in a caller-side object property. -fn write_back_method_object_property_ref_target( - object: RuntimeCellHandle, - property: &str, - access_scope: ElephcEvalExecutionScope, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let previous_scope = context.replace_execution_scope(access_scope); - let result = eval_property_set_result(object, property, value, context, values); - context.replace_execution_scope(previous_scope); - result -} - -/// Stores one by-reference method result in a caller-side static property. -fn write_back_method_static_property_ref_target( - class_name: &str, - property: &str, - access_scope: ElephcEvalExecutionScope, - value: RuntimeCellHandle, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let previous_scope = context.replace_execution_scope(access_scope); - let result = eval_static_property_set_result(class_name, property, value, context, values); - context.replace_execution_scope(previous_scope); - result -} - -/// Creates an indexed or associative array according to the first write key. -fn eval_new_array_for_index( - index: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - if values.type_tag(index)? == EVAL_TAG_STRING { - values.assoc_new(1) - } else { - values.array_new(1) - } -} - -/// Executes one eval-declared class method with `$this` bound in method scope. -pub(in crate::interpreter) fn eval_dynamic_method_with_values( - class_name: &str, - called_class_name: &str, - method: &EvalClassMethod, - object: RuntimeCellHandle, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_dynamic_method_with_values_and_ref_flags( - class_name, - called_class_name, - method, - object, - method.parameter_is_by_ref(), - evaluated_args, - context, - values, - ) -} - -/// Executes one eval-declared class method with caller-selected by-ref binding flags. -pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( - class_name: &str, - called_class_name: &str, - method: &EvalClassMethod, - object: RuntimeCellHandle, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_dynamic_method_with_values_and_ref_mode( - class_name, - called_class_name, - method, - object, - parameter_is_by_ref, - evaluated_args, - EvalByRefBindingMode::RequireTarget, - context, - values, - ) -} - -/// Executes one eval-declared class method with caller-selected by-ref mode. -pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_mode( - class_name: &str, - called_class_name: &str, - method: &EvalClassMethod, - object: RuntimeCellHandle, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let qualified_method_name = - format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); - let static_names = static_var_names(method.body()); - context.push_function(qualified_method_name.clone()); - context.push_class_scope(class_name.to_string()); - context.push_called_class_scope(called_class_name.to_string()); - context.push_method_magic_scope(class_name, method); - let evaluated_args = match bind_evaluated_method_args_with_ref_mode( - method.params(), - method.parameter_types(), - method.parameter_defaults(), - parameter_is_by_ref, - method.parameter_is_variadic(), - evaluated_args, - by_ref_mode, - context, - values, - ) { - Ok(args) => args, - Err(status) => { - context.pop_magic_scope(); - context.pop_called_class_scope(); - context.pop_class_scope(); - context.pop_function(); - return Err(status); - } - }; - let mut method_scope = ElephcEvalScope::new(); - method_scope.set("this", object, ScopeCellOwnership::Borrowed); - let scope_parameter_is_by_ref = - method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); - bind_method_scope_args( - &mut method_scope, - method.params(), - &scope_parameter_is_by_ref, - &evaluated_args, - ); - let result = execute_statements(method.body(), context, &mut method_scope, values); - let persist_result = persist_static_locals( - context, - &qualified_method_name, - &static_names, - &method_scope, - values, - ); - let writeback_result = write_back_method_ref_args( - method.params(), - &evaluated_args, - &method_scope, - context, - values, - ); - let return_result = match (persist_result, writeback_result, result) { - (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), - (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( - method.return_type(), - Some(class_name), - Some(called_class_name), - control, - context, - values, - ), - }; - context.pop_magic_scope(); - context.pop_called_class_scope(); - context.pop_class_scope(); - context.pop_function(); - return_result -} - -/// Executes one eval-declared static class method without binding `$this`. -pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( - class_name: &str, - called_class_name: &str, - method: &EvalClassMethod, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_dynamic_static_method_with_values_and_ref_flags( - class_name, - called_class_name, - method, - method.parameter_is_by_ref(), - evaluated_args, - context, - values, - ) -} - -/// Executes one eval-declared static method with caller-selected by-ref binding flags. -pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_flags( - class_name: &str, - called_class_name: &str, - method: &EvalClassMethod, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_dynamic_static_method_with_values_and_ref_mode( - class_name, - called_class_name, - method, - parameter_is_by_ref, - evaluated_args, - EvalByRefBindingMode::RequireTarget, - context, - values, - ) -} - -/// Executes one eval-declared static method with caller-selected by-ref mode. -pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_mode( - class_name: &str, - called_class_name: &str, - method: &EvalClassMethod, - parameter_is_by_ref: &[bool], - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let qualified_method_name = - format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); - let static_names = static_var_names(method.body()); - context.push_function(qualified_method_name.clone()); - context.push_class_scope(class_name.to_string()); - context.push_called_class_scope(called_class_name.to_string()); - context.push_method_magic_scope(class_name, method); - let evaluated_args = match bind_evaluated_method_args_with_ref_mode( - method.params(), - method.parameter_types(), - method.parameter_defaults(), - parameter_is_by_ref, - method.parameter_is_variadic(), - evaluated_args, - by_ref_mode, - context, - values, - ) { - Ok(args) => args, - Err(status) => { - context.pop_magic_scope(); - context.pop_called_class_scope(); - context.pop_class_scope(); - context.pop_function(); - return Err(status); - } - }; - let mut method_scope = ElephcEvalScope::new(); - let scope_parameter_is_by_ref = - method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); - bind_method_scope_args( - &mut method_scope, - method.params(), - &scope_parameter_is_by_ref, - &evaluated_args, - ); - let result = execute_statements(method.body(), context, &mut method_scope, values); - let persist_result = persist_static_locals( - context, - &qualified_method_name, - &static_names, - &method_scope, - values, - ); - let writeback_result = write_back_method_ref_args( - method.params(), - &evaluated_args, - &method_scope, - context, - values, - ); - let return_result = match (persist_result, writeback_result, result) { - (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), - (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( - method.return_type(), - Some(class_name), - Some(called_class_name), - control, - context, - values, - ), - }; - context.pop_magic_scope(); - context.pop_called_class_scope(); - context.pop_class_scope(); - context.pop_function(); - return_result -} - -/// Wraps positional method arguments into the shared dynamic-call binding shape. -pub(in crate::interpreter) fn positional_args( - args: Vec, -) -> Vec { - args.into_iter() - .map(|value| EvaluatedCallArg { - name: None, - value, - ref_target: None, - }) - .collect() -} - -/// Extracts positional runtime values and rejects named args before runtime method dispatch. -pub(in crate::interpreter) fn positional_evaluated_arg_values( - args: Vec, -) -> Result, EvalStatus> { - if args.iter().any(|arg| arg.name.is_some()) { - return Err(EvalStatus::RuntimeFatal); - } - Ok(args.into_iter().map(|arg| arg.value).collect()) -} - -/// Binds native AOT callable args using the selected by-reference degradation mode. -fn bind_native_callable_bound_args_with_mode( - signature: Option, - args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(signature) = signature else { - return positional_evaluated_bound_args(None, args, by_ref_mode, context, values); - }; - if !signature.bridge_supported() { - return Err(EvalStatus::RuntimeFatal); - } - if signature.param_names().len() == signature.param_count() { - bind_native_signature_args(&signature, args, by_ref_mode, context, values) - } else { - positional_evaluated_bound_args(Some(&signature), args, by_ref_mode, context, values) - } -} - -/// Binds positional-only native AOT args and validates registered by-reference slots. -fn positional_evaluated_bound_args( - signature: Option<&NativeCallableSignature>, - args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - if args.iter().any(|arg| arg.name.is_some()) { - return Err(EvalStatus::RuntimeFatal); - } - let mut bound_args = args - .into_iter() - .enumerate() - .map(|(index, arg)| { - let ref_target = match signature { - Some(signature) => native_parameter_ref_target( - signature, - Some(index), - arg.ref_target, - by_ref_mode, - values, - )?, - None => None, - }; - Ok(BoundMethodArg { - value: arg.value, - ref_target, - variadic_ref_targets: Vec::new(), - }) - }) - .collect::, _>>()?; - if let Some(signature) = signature { - apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; - copy_native_call_user_func_by_value_ref_args( - signature, - &mut bound_args, - by_ref_mode, - values, - )?; - } - Ok(bound_args) -} - -/// Returns only runtime cell values from bound native AOT call arguments. -pub(in crate::interpreter) fn native_bound_arg_values( - args: &[BoundMethodArg], -) -> Vec { - args.iter().map(|arg| arg.value).collect() -} - -/// Writes native AOT by-reference argument cells back to their eval caller targets. -pub(in crate::interpreter) fn write_back_native_callable_ref_args( - bound_args: &[BoundMethodArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for bound_arg in bound_args { - if let Some(target) = bound_arg.ref_target.as_ref() { - write_back_method_ref_target(target, bound_arg.value, context, values)?; - } - for (key, target) in &bound_arg.variadic_ref_targets { - let value = values.array_get(bound_arg.value, *key)?; - write_back_method_ref_target(target, value, context, values)?; - } - } - Ok(()) -} - -/// Binds native AOT callable args and fills omitted defaults from metadata. -fn bind_native_signature_args( - signature: &NativeCallableSignature, - args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut bound_args = vec![None; signature.param_count()]; - let variadic_index = native_callable_variadic_index(signature); - let mut next_positional = 0; - let mut next_variadic_index = 0_i64; - - if let Some(index) = variadic_index { - let array = values.array_new(args.len())?; - bound_args[index] = Some(BoundMethodArg { - value: array, - ref_target: None, - variadic_ref_targets: Vec::new(), - }); - } - - for arg in args { - if let Some(name) = arg.name { - bind_native_named_signature_arg( - signature, - variadic_index, - &mut bound_args, - &name, - arg.value, - arg.ref_target, - by_ref_mode, - values, - )?; - } else { - bind_native_positional_signature_arg( - signature, - &mut bound_args, - variadic_index, - &mut next_positional, - &mut next_variadic_index, - arg.value, - arg.ref_target, - by_ref_mode, - values, - )?; - } - } - - for (position, value) in bound_args.iter_mut().enumerate() { - if Some(position) == variadic_index { - continue; - } - if value.is_some() { - continue; - } - if position < signature.required_param_count() { - return Err(EvalStatus::RuntimeFatal); - } - let Some(default) = signature.param_default(position) else { - return Err(EvalStatus::RuntimeFatal); - }; - *value = Some(BoundMethodArg { - value: materialize_native_callable_default(default, context, values)?, - ref_target: None, - variadic_ref_targets: Vec::new(), - }); - } - - let mut bound_args = bound_args - .into_iter() - .collect::>>() - .ok_or(EvalStatus::RuntimeFatal)?; - apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; - copy_native_call_user_func_by_value_ref_args( - signature, - &mut bound_args, - by_ref_mode, - values, - )?; - Ok(bound_args) -} - -/// Applies registered native AOT parameter types after argument binding and default filling. -fn apply_native_callable_bound_arg_types( - signature: &NativeCallableSignature, - bound_args: &mut [BoundMethodArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - for (position, bound_arg) in bound_args.iter_mut().enumerate() { - let Some(param_type) = signature.param_type(position) else { - continue; - }; - if signature.param_variadic(position) { - apply_native_callable_variadic_arg_type(param_type, bound_arg, context, values)?; - } else { - bound_arg.value = - eval_method_parameter_value(param_type, bound_arg.value, context, values)?; - } - } - Ok(()) -} - -/// Applies one registered native variadic parameter type to each collected argument. -fn apply_native_callable_variadic_arg_type( - param_type: &EvalParameterType, - bound_arg: &mut BoundMethodArg, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let len = values.array_len(bound_arg.value)?; - for position in 0..len { - let key = values.array_iter_key(bound_arg.value, position)?; - let value = values.array_get(bound_arg.value, key)?; - let value = eval_method_parameter_value(param_type, value, context, values)?; - bound_arg.value = values.array_set(bound_arg.value, key, value)?; - } - Ok(()) -} - -/// Copies by-value degraded by-ref native method args before the generated bridge mutates them. -fn copy_native_call_user_func_by_value_ref_args( - signature: &NativeCallableSignature, - bound_args: &mut [BoundMethodArg], - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if !matches!(by_ref_mode, EvalByRefBindingMode::WarnByValue { .. }) { - return Ok(()); - } - let variadic_index = native_callable_variadic_index(signature); - for (position, bound_arg) in bound_args.iter_mut().enumerate() { - let param_index = if variadic_index.is_some_and(|index| position >= index) { - variadic_index.ok_or(EvalStatus::RuntimeFatal)? - } else { - position - }; - if !signature.param_by_ref(param_index) || bound_arg.ref_target.is_some() { - continue; - } - bound_arg.value = copy_native_call_user_func_by_value_ref_arg(bound_arg.value, values)?; - } - Ok(()) -} - -/// Allocates a temporary runtime cell for one by-value degraded by-ref native method arg. -fn copy_native_call_user_func_by_value_ref_arg( - value: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let tag = values.type_tag(value)?; - match tag { - EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { - let word = values.raw_value_word(value)?; - values.raw_word_value(tag, word) - } - EVAL_TAG_STRING => { - let bytes = values.string_bytes(value)?; - values.string_bytes_value(&bytes) - } - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => values.array_clone_shallow(value), - EVAL_TAG_OBJECT => { - let word = values.raw_value_word(value)?; - let retained = values.retain_raw_heap_word(word)?; - values.raw_heap_word_value(retained) - } - EVAL_TAG_NULL => values.null(), - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Returns the native callable variadic slot, if metadata registered one. -fn native_callable_variadic_index(signature: &NativeCallableSignature) -> Option { - (0..signature.param_count()).find(|index| signature.param_variadic(*index)) -} - -/// Binds one positional native AOT argument to a fixed slot or variadic array. -fn bind_native_positional_signature_arg( - signature: &NativeCallableSignature, - bound_args: &mut [Option], - variadic_index: Option, - next_positional: &mut usize, - next_variadic_index: &mut i64, - value: RuntimeCellHandle, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if variadic_index.is_some_and(|index| *next_positional >= index) { - let key = values.int(*next_variadic_index)?; - *next_variadic_index = next_variadic_index - .checked_add(1) - .ok_or(EvalStatus::RuntimeFatal)?; - let ref_target = - native_parameter_ref_target(signature, variadic_index, ref_target, by_ref_mode, values)?; - return bind_native_variadic_arg(bound_args, variadic_index, key, value, ref_target, values); - } - let param_index = *next_positional; - if param_index >= bound_args.len() || bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let ref_target = - native_parameter_ref_target(signature, Some(param_index), ref_target, by_ref_mode, values)?; - bound_args[param_index] = Some(BoundMethodArg { - value, - ref_target, - variadic_ref_targets: Vec::new(), - }); - *next_positional += 1; - Ok(()) -} - -/// Binds one named native AOT argument to a fixed non-variadic slot. -fn bind_native_named_signature_arg( - signature: &NativeCallableSignature, - variadic_index: Option, - bound_args: &mut [Option], - name: &str, - value: RuntimeCellHandle, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if let Some(param_index) = native_regular_param_index(signature, variadic_index, name) { - if bound_args[param_index].is_some() { - return Err(EvalStatus::RuntimeFatal); - } - let ref_target = native_parameter_ref_target( - signature, - Some(param_index), - ref_target, - by_ref_mode, - values, - )?; - bound_args[param_index] = Some(BoundMethodArg { - value, - ref_target, - variadic_ref_targets: Vec::new(), - }); - return Ok(()); - } - Err(EvalStatus::RuntimeFatal) -} - -/// Returns the caller writeback target required by a native by-reference parameter. -fn native_parameter_ref_target( - signature: &NativeCallableSignature, - param_index: Option, - ref_target: Option, - by_ref_mode: EvalByRefBindingMode<'_>, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some(param_index) = param_index else { - return Ok(None); - }; - if !signature.param_by_ref(param_index) { - return Ok(None); - } - if let Some(ref_target) = ref_target { - return Ok(Some(ref_target)); - } - match by_ref_mode { - EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), - EvalByRefBindingMode::WarnByValue { callable_name } => { - let param_name = native_callable_param_warning_name(signature, param_index); - values.warning(&format!( - "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", - param_index + 1 - ))?; - Ok(None) - } - } -} - -/// Returns the PHP parameter name used in native method by-reference warnings. -fn native_callable_param_warning_name( - signature: &NativeCallableSignature, - param_index: usize, -) -> String { - signature - .param_names() - .get(param_index) - .filter(|name| !name.is_empty()) - .cloned() - .unwrap_or_else(|| format!("arg{}", param_index + 1)) -} - -/// Returns the matching non-variadic native parameter index for one named arg. -fn native_regular_param_index( - signature: &NativeCallableSignature, - variadic_index: Option, - name: &str, -) -> Option { - signature - .param_names() - .iter() - .enumerate() - .position(|(index, param)| Some(index) != variadic_index && param == name) -} - -/// Appends one value into the native AOT variadic argument array. -fn bind_native_variadic_arg( - bound_args: &mut [Option], - variadic_index: Option, - key: RuntimeCellHandle, - value: RuntimeCellHandle, - ref_target: Option, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; - let bound = bound_args[index].as_mut().ok_or(EvalStatus::RuntimeFatal)?; - let array = values.array_set(bound.value, key, value)?; - bound.value = array; - if let Some(ref_target) = ref_target { - bound.variadic_ref_targets.push((key, ref_target)); - } - Ok(()) -} - -/// Calls one generated/AOT instance method after native signature binding. -pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( - object: RuntimeCellHandle, - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_native_method_with_evaluated_args_bridge_scope( - object, - class_name, - method_name, - evaluated_args, - None, - None, - context, - values, - ) -} - -/// Calls one generated/AOT instance method after validation with an optional bridge scope. -fn eval_native_method_with_evaluated_args_bridge_scope( - object: RuntimeCellHandle, - class_name: &str, - method_name: &str, - evaluated_args: Vec, - bridge_scope: Option<&str>, - called_class_scope: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut resolved_bridge_scope = bridge_scope.map(str::to_string); - if resolved_bridge_scope.is_none() { - if let Some(shadow_scope) = - eval_private_scope_shadow_bridge_scope(class_name, method_name, context, values)? - { - // The calling scope's own private method shadows any override on - // the receiver's class; access is inherently allowed, so skip the - // hierarchy resolution (it would find the override instead). - return eval_native_method_with_evaluated_args_unchecked_bridge_scope( - object, - class_name, - method_name, - evaluated_args, - Some(&shadow_scope), - called_class_scope, - context, - values, - ); - } - } - let metadata = - eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; - if let Some((declaring_class, visibility, _, is_abstract)) = metadata { - if resolved_bridge_scope.is_none() { - resolved_bridge_scope = Some(declaring_class.clone()); - } - if !is_abstract - && validate_eval_member_access(&declaring_class, visibility, context).is_err() - { - if eval_native_instance_magic_method_available(class_name, context, values)? { - return eval_native_magic_instance_method_call( - object, - class_name, - method_name, - evaluated_args, - context, - values, - ); - } - return eval_throw_method_access_error( - &declaring_class, - method_name, - visibility, - context, - values, - ); - } - } else if eval_native_instance_magic_method_available(class_name, context, values)? { - return eval_native_magic_instance_method_call( - object, - class_name, - method_name, - evaluated_args, - context, - values, - ); - } - eval_native_method_with_evaluated_args_unchecked_bridge_scope( - object, - class_name, - method_name, - evaluated_args, - resolved_bridge_scope.as_deref(), - called_class_scope, - context, - values, - ) -} - -/// Calls one generated/AOT instance method without enforcing member visibility. -fn eval_native_method_with_evaluated_args_unchecked( - object: RuntimeCellHandle, - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_native_method_with_evaluated_args_unchecked_bridge_scope( - object, - class_name, - method_name, - evaluated_args, - None, - None, - context, - values, - ) -} - -/// Calls one generated/AOT instance method without visibility checks using an optional bridge scope. -pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_bridge_scope( - object: RuntimeCellHandle, - class_name: &str, - method_name: &str, - evaluated_args: Vec, - bridge_scope: Option<&str>, - called_class_scope: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( - object, - class_name, - method_name, - evaluated_args, - bridge_scope, - called_class_scope, - EvalByRefBindingMode::RequireTarget, - context, - values, - ) -} - -/// Calls one generated/AOT instance method for `call_user_func()` by-value by-ref degradation. -pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - object: RuntimeCellHandle, - class_name: &str, - method_name: &str, - evaluated_args: Vec, - bridge_scope: Option<&str>, - called_class_scope: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let signature_owner = bridge_scope.unwrap_or(class_name); - let callable_name = format!("{}::{}", signature_owner.trim_start_matches('\\'), method_name); - eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( - object, - class_name, - method_name, - evaluated_args, - bridge_scope, - called_class_scope, - EvalByRefBindingMode::WarnByValue { - callable_name: &callable_name, - }, - context, - values, - ) -} - -/// Calls one generated/AOT instance method with a selected by-reference binding mode. -fn eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( - object: RuntimeCellHandle, - class_name: &str, - method_name: &str, - evaluated_args: Vec, - bridge_scope: Option<&str>, - called_class_scope: Option<&str>, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let signature_owner = bridge_scope.unwrap_or(class_name); - let signature = context.native_method_signature(signature_owner, method_name); - let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); - let bound_args = - bind_native_callable_bound_args_with_mode(signature, evaluated_args, by_ref_mode, context, values)?; - let result = if let Some(scope) = bridge_scope { - eval_native_method_call_with_scope( - scope, - called_class_scope, - object, - method_name, - native_bound_arg_values(&bound_args), - context, - values, - ) - } else { - values.method_call(object, method_name, native_bound_arg_values(&bound_args)) - }; - let writeback = write_back_native_callable_ref_args(&bound_args, context, values); - match (result, writeback) { - (Err(status), _) | (_, Err(status)) => Err(status), - (Ok(result), Ok(())) => eval_declared_native_return_value( - return_type.as_ref(), - Some(signature_owner), - called_class_scope.or(Some(class_name)), - result, - context, - values, - ), - } -} - -/// Calls one generated/AOT static method after native signature binding. -pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_native_static_method_with_evaluated_args_bridge_scope( - class_name, - method_name, - evaluated_args, - None, - None, - context, - values, - ) -} - -/// Calls one generated/AOT static method after validation with an optional bridge scope. -fn eval_native_static_method_with_evaluated_args_bridge_scope( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - bridge_scope: Option<&str>, - called_class_scope: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let mut resolved_bridge_scope = bridge_scope.map(str::to_string); - let metadata = - eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; - if let Some((declaring_class, visibility, is_static, is_abstract)) = metadata { - if resolved_bridge_scope.is_none() { - resolved_bridge_scope = Some(declaring_class.clone()); - } - if is_static - && !is_abstract - && validate_eval_member_access(&declaring_class, visibility, context).is_err() - { - if eval_native_static_magic_method_available(class_name, context, values)? { - return eval_native_magic_static_method_call( - class_name, - method_name, - evaluated_args, - context, - values, - ); - } - return eval_throw_method_access_error( - &declaring_class, - method_name, - visibility, - context, - values, - ); - } - } else if eval_native_static_magic_method_available(class_name, context, values)? { - return eval_native_magic_static_method_call( - class_name, - method_name, - evaluated_args, - context, - values, - ); - } - eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( - class_name, - method_name, - evaluated_args, - resolved_bridge_scope.as_deref(), - called_class_scope, - context, - values, - ) -} - -/// Calls one generated/AOT static method without enforcing member visibility. -fn eval_native_static_method_with_evaluated_args_unchecked( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( - class_name, - method_name, - evaluated_args, - None, - None, - context, - values, - ) -} - -/// Calls one generated/AOT static method without visibility checks using an optional bridge scope. -pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - bridge_scope: Option<&str>, - called_class_scope: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( - class_name, - method_name, - evaluated_args, - bridge_scope, - called_class_scope, - EvalByRefBindingMode::RequireTarget, - context, - values, - ) -} - -/// Calls one generated/AOT static method for `call_user_func()` by-value by-ref degradation. -pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - bridge_scope: Option<&str>, - called_class_scope: Option<&str>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let signature_owner = bridge_scope.unwrap_or(class_name); - let callable_name = format!("{}::{}", signature_owner.trim_start_matches('\\'), method_name); - eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( - class_name, - method_name, - evaluated_args, - bridge_scope, - called_class_scope, - EvalByRefBindingMode::WarnByValue { - callable_name: &callable_name, - }, - context, - values, - ) -} - -/// Calls one generated/AOT static method with a selected by-reference binding mode. -fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - bridge_scope: Option<&str>, - called_class_scope: Option<&str>, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let signature_owner = bridge_scope.unwrap_or(class_name); - let signature = context.native_static_method_signature(signature_owner, method_name); - let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); - let bound_args = - bind_native_callable_bound_args_with_mode(signature, evaluated_args, by_ref_mode, context, values)?; - let result = if let Some(scope) = bridge_scope { - eval_native_static_method_call_with_scope( - scope, - called_class_scope, - class_name, - method_name, - native_bound_arg_values(&bound_args), - context, - values, - ) - } else { - values.static_method_call(class_name, method_name, native_bound_arg_values(&bound_args)) - }; - let writeback = write_back_native_callable_ref_args(&bound_args, context, values); - match (result, writeback) { - (Err(status), _) | (_, Err(status)) => Err(status), - (Ok(result), Ok(())) => eval_declared_native_return_value( - return_type.as_ref(), - Some(signature_owner), - called_class_scope.or(Some(class_name)), - result, - context, - values, - ), - } -} - -/// Returns whether a generated/AOT class has an instance `__call()` fallback. -fn eval_native_instance_magic_method_available( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? - .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) -} - -/// Returns whether a generated/AOT class has a static `__callStatic()` fallback. -fn eval_native_static_magic_method_available( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - Ok( - eval_aot_method_dispatch_metadata_in_hierarchy( - class_name, - "__callStatic", - context, - values, - )? - .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), - ) -} - -/// Dispatches a missing or inaccessible generated/AOT instance method through `__call()`. -pub(in crate::interpreter) fn eval_native_magic_instance_method_call( - object: RuntimeCellHandle, - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; - eval_native_method_with_evaluated_args_unchecked( - object, - class_name, - "__call", - magic_args, - context, - values, - ) -} - -/// Dispatches a missing or inaccessible generated/AOT static method through `__callStatic()`. -pub(in crate::interpreter) fn eval_native_magic_static_method_call( - class_name: &str, - method_name: &str, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; - eval_native_static_method_with_evaluated_args_unchecked( - class_name, - "__callStatic", - magic_args, - context, - values, - ) -} - -/// Finds generated/AOT method metadata on a class or its native parent chain. -pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata_in_hierarchy( - class_name: &str, - method_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let mut current = class_name.trim_start_matches('\\').to_string(); - let mut seen = std::collections::HashSet::new(); - loop { - if !seen.insert(current.to_ascii_lowercase()) { - return Ok(None); - } - if let Some(metadata) = eval_aot_method_dispatch_metadata(¤t, method_name, values)? { - return Ok(Some(metadata)); - } - let Some(parent) = context.native_class_parent(¤t) else { - return Ok(None); - }; - current = parent.to_string(); - } -} - -/// Runs one generated/AOT constructor after native signature binding. -pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( - class_name: &str, - object: RuntimeCellHandle, - evaluated_args: Vec, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - eval_native_constructor_with_evaluated_args_and_ref_mode( - class_name, - object, - evaluated_args, - EvalByRefBindingMode::RequireTarget, - context, - values, - ) -} - -/// Runs one generated/AOT constructor with caller-selected by-ref binding behavior. -fn eval_native_constructor_with_evaluated_args_and_ref_mode( - class_name: &str, - object: RuntimeCellHandle, - evaluated_args: Vec, - by_ref_mode: EvalByRefBindingMode<'_>, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - if let Some(message) = eval_native_constructor_access_error(class_name, context, values)? { - return eval_throw_error(&message, context, values); - } - let bridge_scope = - eval_native_constructor_bridge_scope(class_name, context, values)?; - let signature = context.native_constructor_signature(class_name); - let bound_args = bind_native_callable_bound_args_with_mode( - signature, - evaluated_args, - by_ref_mode, - context, - values, - )?; - let result = if let Some(scope) = bridge_scope.as_deref() { - eval_with_native_bridge_scope(scope, context, || { - values.construct_object(object, native_bound_arg_values(&bound_args)) - }) - } else { - values.construct_object(object, native_bound_arg_values(&bound_args)) - }; - let writeback = write_back_native_callable_ref_args(&bound_args, context, values); - match (result, writeback) { - (Err(status), _) | (_, Err(status)) => Err(status), - (Ok(()), Ok(())) => Ok(()), - } -} - -/// Returns the generated/AOT constructor scope that the runtime bridge can recognize. -fn eval_native_constructor_bridge_scope( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, visibility)) = - eval_reflection_aot_non_public_constructor(class_name, values)? - else { - return Ok(None); - }; - if eval_native_constructor_access_allowed(&declaring_class, visibility, context) { - Ok(Some(declaring_class)) - } else { - Ok(None) - } -} - -/// Returns PHP's constructor access error for generated/AOT constructors, if inaccessible. -fn eval_native_constructor_access_error( - class_name: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result, EvalStatus> { - let Some((declaring_class, visibility)) = - eval_reflection_aot_non_public_constructor(class_name, values)? - else { - return Ok(None); - }; - if eval_native_constructor_access_allowed(&declaring_class, visibility, context) { - return Ok(None); - } - Ok(Some(format!( - "Call to {} {}::__construct() from {}", - eval_visibility_label(visibility), - declaring_class.trim_start_matches('\\'), - eval_native_constructor_scope_label(context) - ))) -} - -/// Returns whether the current eval scope may call one generated/AOT constructor. -fn eval_native_constructor_access_allowed( - declaring_class: &str, - visibility: EvalVisibility, - context: &ElephcEvalContext, -) -> bool { - match visibility { - EvalVisibility::Public => true, - EvalVisibility::Private => context - .current_class_scope() - .is_some_and(|current| same_eval_class_name(current, declaring_class)), - EvalVisibility::Protected => context - .current_class_scope() - .is_some_and(|current| eval_classes_are_related(current, declaring_class, context)), - } -} - -/// Returns PHP's scope phrase for constructor access diagnostics. -fn eval_native_constructor_scope_label(context: &ElephcEvalContext) -> String { - context.current_class_scope().map_or_else( - || String::from("global scope"), - |class_name| format!("scope {}", class_name.trim_start_matches('\\')), - ) -} - -/// Returns PHP's lowercase visibility label. -fn eval_visibility_label(visibility: EvalVisibility) -> &'static str { - match visibility { - EvalVisibility::Public => "public", - EvalVisibility::Protected => "protected", - EvalVisibility::Private => "private", - } -} - -/// Allocates a fresh runtime cell for one invocation-safe native AOT default. -pub(in crate::interpreter) fn materialize_native_callable_default( - default: &NativeCallableDefault, - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - match default { - NativeCallableDefault::Null => values.null(), - NativeCallableDefault::Bool(value) => values.bool_value(*value), - NativeCallableDefault::Int(value) => values.int(*value), - NativeCallableDefault::Float(value) => values.float(*value), - NativeCallableDefault::String(value) => values.string(value), - NativeCallableDefault::EmptyArray => values.array_new(0), - NativeCallableDefault::Array(elements) => { - materialize_native_callable_array_default(elements, context, values) - } - NativeCallableDefault::Object { class_name, args } => { - materialize_native_callable_object_default(class_name, args, context, values) - } - } -} - -/// Allocates one array-valued native AOT parameter default with fresh element cells. -fn materialize_native_callable_array_default( - elements: &[NativeCallableArrayDefaultElement], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let has_string_key = elements.iter().any(|element| { - matches!( - element.key, - Some(NativeCallableArrayDefaultKey::String(_)) - ) - }); - let mut array = if has_string_key { - values.assoc_new(elements.len())? - } else { - values.array_new(elements.len())? - }; - let mut next_auto_key = 0; - for element in elements { - let key = match &element.key { - Some(NativeCallableArrayDefaultKey::Int(value)) => { - if *value >= next_auto_key { - next_auto_key = value.saturating_add(1); - } - values.int(*value)? - } - Some(NativeCallableArrayDefaultKey::String(value)) => values.string(value)?, - None => { - let key = values.int(next_auto_key)?; - next_auto_key = next_auto_key.saturating_add(1); - key - } - }; - let value = materialize_native_callable_default(&element.value, context, values)?; - array = values.array_set(array, key, value)?; - } - Ok(array) -} - -/// Allocates and constructs one object-valued native AOT parameter default. -fn materialize_native_callable_object_default( - class_name: &str, - args: &[NativeCallableObjectDefaultArg], - context: &mut ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - let object = values.new_object(class_name)?; - let mut constructor_args = Vec::with_capacity(args.len()); - for arg in args { - constructor_args.push(EvaluatedCallArg { - name: arg.name.clone(), - value: materialize_native_callable_default(&arg.value, context, values)?, - ref_target: None, - }); - } - if let Err(err) = eval_native_constructor_with_evaluated_args( - class_name, - object, - constructor_args, - context, - values, - ) { - let _ = values.release(object); - return Err(err); - } - Ok(object) -} - -/// Executes a PHP `static $name = expr;` declaration in the current eval scope. -pub(in crate::interpreter) fn execute_static_var_stmt( - name: &str, - init: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result<(), EvalStatus> { - let Some(function_name) = context.current_function().map(str::to_string) else { - let value = eval_expr(init, context, scope, values)?; - if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Owned) { - values.release(replaced)?; - } - return Ok(()); - }; - if scope.contains_visible(name) { - return Ok(()); - } - let value = if let Some(value) = context.static_local(&function_name, name) { - value - } else { - let value = eval_expr(init, context, scope, values)?; - let _ = context.set_static_local(function_name.clone(), name.to_string(), value); - value - }; - if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Borrowed) { - values.release(replaced)?; - } - Ok(()) -} - -/// Executes a PHP switch with loose case matching, default fallback, and fallthrough. -pub(in crate::interpreter) fn execute_switch_stmt( - expr: &EvalExpr, - cases: &[EvalSwitchCase], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let subject = eval_expr(expr, context, scope, values)?; - let mut default_index = None; - let mut matched_index = None; - for (index, case) in cases.iter().enumerate() { - let Some(condition) = &case.condition else { - if default_index.is_none() { - default_index = Some(index); - } - continue; - }; - let condition = eval_expr(condition, context, scope, values)?; - let matches = values.compare(EvalBinOp::LooseEq, subject, condition)?; - if values.truthy(matches)? { - matched_index = Some(index); - break; - } - } - let Some(start_index) = matched_index.or(default_index) else { - return Ok(EvalControl::None); - }; - for case in &cases[start_index..] { - match execute_statements(&case.body, context, scope, values)? { - EvalControl::None => {} - EvalControl::Break | EvalControl::Continue => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } - Ok(EvalControl::None) -} - -/// Executes a PHP `do/while` loop, evaluating the condition after every body run. -pub(in crate::interpreter) fn execute_do_while_stmt( - body: &[EvalStmt], - condition: &EvalExpr, - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - loop { - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - let condition = eval_expr(condition, context, scope, values)?; - if !values.truthy(condition)? { - break; - } - } - Ok(EvalControl::None) -} - -/// Executes a PHP `for` loop while preserving update-on-continue semantics. -pub(in crate::interpreter) fn execute_for_stmt( - init: &[EvalStmt], - condition: Option<&EvalExpr>, - update: &[EvalStmt], - body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - match execute_statements(init, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => return Ok(EvalControl::None), - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - loop { - if let Some(condition) = condition { - let condition = eval_expr(condition, context, scope, values)?; - if !values.truthy(condition)? { - break; - } - } - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - match execute_statements(update, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } - Ok(EvalControl::None) -} - -/// Executes a PHP `foreach` loop over eval array and Traversable object values. -pub(in crate::interpreter) fn execute_foreach_stmt( - array: &EvalExpr, - key_name: Option<&str>, - value_name: &str, - body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let array = eval_expr(array, context, scope, values)?; - match values.type_tag(array)? { - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { - execute_foreach_array_stmt(array, key_name, value_name, body, context, scope, values) - } - EVAL_TAG_OBJECT => { - execute_foreach_object_stmt(array, key_name, value_name, body, context, scope, values) - } - _ => Err(EvalStatus::RuntimeFatal), - } -} - -/// Executes `foreach` over a PHP array value using insertion-order runtime hooks. -fn execute_foreach_array_stmt( - array: RuntimeCellHandle, - key_name: Option<&str>, - value_name: &str, - body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - for index in 0..len { - let key = values.array_iter_key(array, index)?; - let value = values.array_get(array, key)?; - if let Some(key_name) = key_name { - for replaced in set_scope_cell( - context, - scope, - key_name.to_string(), - key, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - } else { - values.release(key)?; - } - for replaced in set_scope_cell( - context, - scope, - value_name.to_string(), - value, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => {} - EvalControl::Break => break, - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } - Ok(EvalControl::None) -} - -/// Executes `foreach` over an Iterator or IteratorAggregate object. -fn execute_foreach_object_stmt( - object: RuntimeCellHandle, - key_name: Option<&str>, - value_name: &str, - body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - if eval_foreach_object_is_a(object, "Iterator", context, values)? { - return execute_foreach_iterator_stmt( - object, key_name, value_name, body, context, scope, values, - ); - } - if eval_foreach_object_is_a(object, "IteratorAggregate", context, values)? { - let iterator = eval_method_call_result(object, "getIterator", Vec::new(), context, values)?; - return match values.type_tag(iterator)? { - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => execute_foreach_array_stmt( - iterator, key_name, value_name, body, context, scope, values, - ), - EVAL_TAG_OBJECT if eval_foreach_object_is_a(iterator, "Iterator", context, values)? => { - execute_foreach_iterator_stmt( - iterator, key_name, value_name, body, context, scope, values, - ) - } - _ => Err(EvalStatus::RuntimeFatal), - }; - } - Err(EvalStatus::RuntimeFatal) -} - -/// Drives one Iterator object through PHP's `foreach` method-call sequence. -fn execute_foreach_iterator_stmt( - iterator: RuntimeCellHandle, - key_name: Option<&str>, - value_name: &str, - body: &[EvalStmt], - context: &mut ElephcEvalContext, - scope: &mut ElephcEvalScope, - values: &mut impl RuntimeValueOps, -) -> Result { - let result = eval_method_call_result(iterator, "rewind", Vec::new(), context, values)?; - values.release(result)?; - loop { - let valid = eval_method_call_result(iterator, "valid", Vec::new(), context, values)?; - let is_valid = values.truthy(valid)?; - values.release(valid)?; - if !is_valid { - return Ok(EvalControl::None); - } - - let value = eval_method_call_result(iterator, "current", Vec::new(), context, values)?; - let key = if key_name.is_some() { - Some(eval_method_call_result( - iterator, - "key", - Vec::new(), - context, - values, - )?) - } else { - None - }; - if let Some((key_name, key)) = key_name.zip(key) { - for replaced in set_scope_cell( - context, - scope, - key_name.to_string(), - key, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - } - for replaced in set_scope_cell( - context, - scope, - value_name.to_string(), - value, - ScopeCellOwnership::Owned, - )? { - values.release(replaced)?; - } - - match execute_statements(body, context, scope, values)? { - EvalControl::None | EvalControl::Continue => { - let result = - eval_method_call_result(iterator, "next", Vec::new(), context, values)?; - values.release(result)?; - } - EvalControl::Break => return Ok(EvalControl::None), - EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), - EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), - EvalControl::Return(result) => return Ok(EvalControl::Return(result)), - } - } -} - -/// Returns whether a foreach object satisfies one iterator interface. -fn eval_foreach_object_is_a( - object: RuntimeCellHandle, - target: &str, - context: &ElephcEvalContext, - values: &mut impl RuntimeValueOps, -) -> Result { - dynamic_object_is_a(object, target, false, context, values)? - .map_or_else(|| values.object_is_a(object, target, false), Ok) -} - -/// Returns PHP's next automatic integer key for `$array[]` append writes. -pub(in crate::interpreter) fn eval_array_append_key( - array: RuntimeCellHandle, - values: &mut impl RuntimeValueOps, -) -> Result { - let len = values.array_len(array)?; - let mut next_key = None; - for position in 0..len { - let key = values.array_iter_key(array, position)?; - if values.type_tag(key)? != EVAL_TAG_INT { - continue; - } - let one = values.int(1)?; - let candidate = values.add(key, one)?; - let replace = if let Some(current) = next_key { - let is_greater = values.compare(EvalBinOp::Gt, candidate, current)?; - values.truthy(is_greater)? - } else { - true - }; - if replace { - next_key = Some(candidate); - } - } - next_key.map_or_else(|| values.int(0), Ok) -} +use abstract_requirements::*; +pub(crate) use array_updates::*; +use attributes_magic_validation::*; +pub(in crate::interpreter) use callable_objects::*; +pub(in crate::interpreter) use class_declarations::*; +pub(in crate::interpreter) use class_resolution::*; +use closure_binding::*; +pub(in crate::interpreter) use dispatch::*; +pub(in crate::interpreter) use dynamic_method_execution::*; +pub(in crate::interpreter) use enum_declarations::*; +pub(in crate::interpreter) use exceptions::*; +pub(in crate::interpreter) use instance_property_access::*; +use interface_contracts::*; +use interface_member_validation::*; +pub(in crate::interpreter) use loop_statements::*; +pub(in crate::interpreter) use method_dispatch::*; +pub(in crate::interpreter) use native_argument_binding::*; +pub(in crate::interpreter) use native_constructor_defaults::*; +pub(in crate::interpreter) use native_method_execution::*; +pub(in crate::interpreter) use native_static_dispatch::*; +use property_constant_validation::*; +pub(in crate::interpreter) use property_validation::*; +pub(in crate::interpreter) use reference_writeback::*; +pub(in crate::interpreter) use reflection_instantiation::*; +pub(in crate::interpreter) use static_method_dispatch::*; +pub(in crate::interpreter) use static_property_access::*; +pub(in crate::interpreter) use trait_declarations::*; diff --git a/crates/elephc-magician/src/interpreter/statements/abstract_requirements.rs b/crates/elephc-magician/src/interpreter/statements/abstract_requirements.rs new file mode 100644 index 0000000000..b7be19cff5 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/abstract_requirements.rs @@ -0,0 +1,508 @@ +//! Purpose: +//! Collects, merges, and applies inherited abstract method and property requirements. +//! +//! Called from: +//! - Concrete class validation after direct member compatibility checks. +//! +//! Key details: +//! - Eval and AOT parent contracts retain visibility, staticness, and property hook modes. + +use super::*; + +/// Validates that a concrete class has satisfied inherited abstract and interface requirements. +pub(super) fn validate_concrete_class_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !pending_class_abstract_method_requirements(class, context).is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if !pending_class_abstract_property_requirements(class, context)?.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) { + validate_class_implements_eval_interface(class, &interface, context)?; + } + } + Ok(()) +} + +/// Validates concrete class methods required by PHP builtin runtime interfaces. +pub(super) fn validate_concrete_class_builtin_interface_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + pending_class_builtin_interface_method_requirements(class, context) + { + if !class_has_builtin_interface_method(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates concrete class methods required by generated/AOT abstract parents. +pub(super) fn validate_concrete_class_aot_parent_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !pending_class_aot_parent_abstract_method_requirements(class, context, values)?.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + if !pending_class_aot_parent_abstract_property_requirements(class, context, values)?.is_empty() + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Validates concrete class methods required by generated/AOT runtime interfaces. +pub(super) fn validate_concrete_class_aot_interface_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for requirement in pending_class_aot_interface_method_requirements(class, context, values)? { + if !class_has_aot_interface_method(class, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + for (requirement_owner, requirement) in + pending_class_aot_interface_property_requirements(class, context, values)? + { + if !class_has_interface_property(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns inherited abstract methods that the pending class has not concretized. +pub(super) fn pending_class_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Vec { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_class_abstract_method_requirements(parent, context, &mut requirements); + } + apply_class_abstract_method_requirements(class, &mut requirements); + requirements.into_values().collect() +} + +/// Returns inherited abstract properties that the pending class has not concretized. +pub(super) fn pending_class_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_class_abstract_property_requirements(parent, context, &mut requirements)?; + } + apply_class_abstract_property_requirements(class, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + +/// Returns generated/AOT abstract parent methods the pending class has not concretized. +pub(super) fn pending_class_aot_parent_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_method_requirements( + parent, + context, + values, + &mut requirements, + )?; + } + apply_class_aot_parent_abstract_method_requirements(class, context, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + +/// Returns generated/AOT abstract parent properties the pending class has not concretized. +pub(super) fn pending_class_aot_parent_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = std::collections::HashMap::new(); + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_property_requirements( + parent, + context, + values, + &mut requirements, + )?; + } + apply_class_aot_parent_abstract_property_requirements(class, context, &mut requirements)?; + Ok(requirements.into_values().collect()) +} + +/// Collects abstract method requirements from one declared eval class ancestry chain. +pub(super) fn collect_class_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) { + let Some(class) = context.class(class_name) else { + return; + }; + if let Some(parent) = class.parent() { + collect_class_abstract_method_requirements(parent, context, requirements); + } + apply_class_abstract_method_requirements(class, requirements); +} + +/// Collects generated/AOT abstract method requirements through eval and AOT parents. +pub(super) fn collect_aot_parent_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + if let Some(class) = context.class(class_name) { + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_method_requirements( + parent, + context, + values, + requirements, + )?; + } + apply_class_aot_parent_abstract_method_requirements(class, context, requirements)?; + return Ok(()); + } + if values.class_exists(class_name)? { + collect_native_aot_abstract_method_requirements( + class_name, + context, + values, + requirements, + )?; + } + Ok(()) +} + +/// Collects abstract methods exposed by one generated/AOT class reflection row. +pub(super) fn collect_native_aot_abstract_method_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for method_name in eval_aot_method_names(class_name, values)? { + let Some(flags) = values.reflection_method_flags(class_name, &method_name)? else { + continue; + }; + let key = method_name.to_ascii_lowercase(); + if flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 { + requirements.remove(&key); + continue; + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + continue; + } + let requirement = + eval_aot_abstract_method_requirement(class_name, &method_name, flags, context, values)?; + requirements.insert(key, requirement); + } + Ok(()) +} + +/// Collects generated/AOT abstract property requirements through eval and AOT parents. +pub(super) fn collect_aot_parent_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + if let Some(class) = context.class(class_name) { + if let Some(parent) = class.parent() { + collect_aot_parent_abstract_property_requirements( + parent, + context, + values, + requirements, + )?; + } + apply_class_aot_parent_abstract_property_requirements(class, context, requirements)?; + return Ok(()); + } + if values.class_exists(class_name)? { + collect_native_aot_abstract_property_requirements( + class_name, + context, + values, + requirements, + )?; + } + Ok(()) +} + +/// Collects abstract properties exposed by one generated/AOT class metadata row. +pub(super) fn collect_native_aot_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for (owner, property) in context.native_abstract_property_requirements(class_name) { + let Some(flags) = values.reflection_property_flags(class_name, property.name())? else { + continue; + }; + if flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 + || flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 + { + continue; + } + let visibility = eval_aot_property_visibility(flags); + let write_visibility = eval_aot_property_write_visibility(flags, visibility); + let set_visibility = (write_visibility != visibility).then_some(write_visibility); + let requirement = EvalClassProperty::with_visibility(property.name(), visibility, None) + .with_type(property.property_type().cloned()) + .with_set_visibility(set_visibility) + .with_abstract_hook_contract(property.requires_get(), property.requires_set()); + requirements.insert( + property.name().to_string(), + EvalAotAbstractPropertyRequirement { + owner, + property: requirement, + }, + ); + } + Ok(()) +} + +/// Collects abstract property requirements from one declared eval class ancestry chain. +pub(super) fn collect_class_abstract_property_requirements( + class_name: &str, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + let Some(class) = context.class(class_name) else { + return Ok(()); + }; + if let Some(parent) = class.parent() { + collect_class_abstract_property_requirements(parent, context, requirements)?; + } + apply_class_abstract_property_requirements(class, requirements) +} + +/// Applies one class's methods to the open abstract-method requirement set. +pub(super) fn apply_class_abstract_method_requirements( + class: &EvalClass, + requirements: &mut std::collections::HashMap, +) { + for method in class.methods() { + let key = method.name().to_ascii_lowercase(); + if method.is_abstract() { + requirements.insert(key, method.clone()); + } else { + requirements.remove(&key); + } + } +} + +/// Applies one eval class's methods to the open AOT abstract-method requirement set. +pub(super) fn apply_class_aot_parent_abstract_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for method in class.methods() { + let key = method.name().to_ascii_lowercase(); + let Some(requirement) = requirements.get(&key) else { + continue; + }; + if !class_method_satisfies_aot_abstract_parent_requirement( + method, + class.name(), + requirement, + Some(class), + context, + false, + ) { + return Err(EvalStatus::RuntimeFatal); + } + if !method.is_abstract() { + requirements.remove(&key); + } + } + Ok(()) +} + +/// Applies one eval class's properties to the open AOT abstract-property requirement set. +pub(super) fn apply_class_aot_parent_abstract_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for property in class.properties() { + let key = property.name().to_string(); + let Some(requirement) = requirements.get(&key).map(|requirement| { + EvalAotAbstractPropertyRequirement { + owner: requirement.owner.clone(), + property: requirement.property.clone(), + } + }) else { + continue; + }; + if !class_property_satisfies_aot_abstract_parent_requirement( + property, + class.name(), + &requirement, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_abstract() { + requirements.insert( + key, + EvalAotAbstractPropertyRequirement { + owner: class.name().to_string(), + property: merge_abstract_property_contracts( + &requirement.property, + property, + ), + }, + ); + } else { + requirements.remove(&key); + } + } + Ok(()) +} + +/// Applies one class's properties to the open abstract-property requirement set. +pub(super) fn apply_class_abstract_property_requirements( + class: &EvalClass, + requirements: &mut std::collections::HashMap, +) -> Result<(), EvalStatus> { + for property in class.properties() { + let key = property.name().to_string(); + if property.is_abstract() { + if let Some(existing) = requirements.get(&key) { + (property_contract_visibility_allows(existing, property) + && property_contract_write_visibility_allows(existing, property)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal)?; + requirements.insert(key, merge_abstract_property_contracts(existing, property)); + } else { + requirements.insert(key, property.clone()); + } + } else if let Some(requirement) = requirements.get(&key) { + class_property_satisfies_abstract_contract(property, requirement) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal)?; + requirements.remove(&key); + } + } + Ok(()) +} + +/// Merges inherited and redeclared abstract property hook requirements. +pub(super) fn merge_abstract_property_contracts( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> EvalClassProperty { + redeclared.clone().with_abstract_hook_contract( + inherited.requires_get_hook() || redeclared.requires_get_hook(), + inherited.requires_set_hook() || redeclared.requires_set_hook(), + ) +} + +/// Returns whether a redeclared property keeps compatible visibility. +pub(super) fn property_contract_visibility_allows( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> bool { + property_visibility_rank(redeclared.visibility()) + >= property_visibility_rank(inherited.visibility()) +} + +/// Returns whether a redeclared property keeps compatible write visibility. +pub(super) fn property_contract_write_visibility_allows( + inherited: &EvalClassProperty, + redeclared: &EvalClassProperty, +) -> bool { + !inherited.requires_set_hook() + || property_visibility_rank(redeclared.write_visibility()) + >= property_visibility_rank(inherited.write_visibility()) +} + +/// Returns whether a concrete property satisfies an abstract hook contract. +pub(super) fn class_property_satisfies_abstract_contract( + property: &EvalClassProperty, + requirement: &EvalClassProperty, +) -> bool { + if property.is_abstract() + || property.is_static() + || property.property_type() != requirement.property_type() + || !property_contract_visibility_allows(requirement, property) + { + return false; + } + if requirement.requires_set_hook() { + return requirement.set_visibility() != Some(EvalVisibility::Private) + && property_contract_write_visibility_allows(requirement, property) + && (property.has_set_hook() || (!property.has_get_hook() && !property.is_readonly())); + } + requirement.requires_get_hook() +} + +/// Returns whether one property satisfies a generated/AOT abstract parent contract. +pub(super) fn class_property_satisfies_aot_abstract_parent_requirement( + property: &EvalClassProperty, + property_owner: &str, + requirement: &EvalAotAbstractPropertyRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + let required = &requirement.property; + if property.is_static() != required.is_static() + || !property_contract_visibility_allows(required, property) + || !property_type_signature_matches( + property.property_type(), + property_owner, + required.property_type(), + &requirement.owner, + pending_class, + context, + ) + { + return false; + } + if property.is_abstract() { + return (!required.requires_get_hook() || property.requires_get_hook()) + && (!required.requires_set_hook() + || (property.requires_set_hook() + && property_contract_write_visibility_allows(required, property))); + } + if required.requires_get_hook() && !class_property_supports_interface_get(property) { + return false; + } + if required.requires_set_hook() { + return required.set_visibility() != Some(EvalVisibility::Private) + && property_contract_write_visibility_allows(required, property) + && class_property_supports_interface_set(property); + } + required.requires_get_hook() +} + +/// Returns a comparable rank where larger means less restrictive property visibility. +pub(super) fn property_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/array_updates.rs b/crates/elephc-magician/src/interpreter/statements/array_updates.rs new file mode 100644 index 0000000000..836f096f28 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/array_updates.rs @@ -0,0 +1,590 @@ +//! Purpose: +//! Executes increment, unset, append, and indexed array mutation statements. +//! +//! Called from: +//! - `crate::interpreter::statements::execute_stmt()`. +//! +//! Key details: +//! - Object ArrayAccess and plain runtime arrays preserve reference and release semantics. + +use super::*; + +/// Applies member increment/decrement to a runtime value using PHP numeric semantics. +pub(super) fn eval_inc_dec_value( + current: RuntimeCellHandle, + increment: bool, + values: &mut impl RuntimeValueOps, +) -> Result { + let one = values.int(1)?; + if increment { + values.add(current, one) + } else { + values.sub(current, one) + } +} + +/// Reads, updates, and writes one object property after the receiver/name are evaluated. +pub(super) fn eval_property_inc_dec_result( + object: RuntimeCellHandle, + property: &str, + increment: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_property_get_result(object, property, context, values)?; + let value = eval_inc_dec_value(current, increment, values)?; + eval_property_set_result(object, property, value, context, values) +} + +/// Reads, updates, and writes one static property after the receiver/name are resolved. +pub(super) fn eval_static_property_inc_dec_result( + class_name: &str, + property: &str, + increment: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_static_property_get_result(class_name, property, context, values)?; + let value = eval_inc_dec_value(current, increment, values)?; + eval_static_property_set_result(class_name, property, value, context, values) +} + +/// Releases one eval-owned value after running an eval-declared dynamic destructor if needed. +pub(super) fn eval_release_value( + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, + value: RuntimeCellHandle, +) -> Result<(), EvalStatus> { + if let Some(identity) = values.final_object_identity_for_release(value)? { + eval_dynamic_destructor_for_release(identity, value, context, values)?; + } + values.release(value) +} + +/// Calls a dynamic eval `__destruct()` hook immediately before the runtime frees the object. +pub(super) fn eval_dynamic_destructor_for_release( + identity: u64, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_dynamic_destructor_for_object_cell(identity, object, context, values).map(|_| ()) +} + +/// Calls a dynamic eval `__destruct()` hook for an already-boxed object cell. +pub(crate) fn eval_dynamic_destructor_for_object_cell( + identity: u64, + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(class_name) = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()) + else { + return Ok(false); + }; + let Some((declaring_class, method)) = context.class_method(&class_name, "__destruct") else { + return Ok(false); + }; + if !context.begin_dynamic_object_destructor(identity) { + return Ok(true); + } + let result = eval_dynamic_method_with_values( + &declaring_class, + &class_name, + &method, + object, + Vec::new(), + context, + values, + ); + let release_result = match result { + Ok(result) => values.release(result), + Err(status) => Err(status), + }; + context.finish_dynamic_object_destructor(identity); + release_result.map(|_| true) +} + +/// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. +pub(super) fn eval_array_unset_element_stmt( + array: &EvalExpr, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match array { + EvalExpr::LoadVar(name) => { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + let Some((array, ownership)) = existing else { + return Ok(()); + }; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + for replaced in set_scope_cell(context, scope, name.clone(), array, ownership)? { + values.release(replaced)?; + } + } + return Ok(()); + } + EvalExpr::PropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let array = eval_property_get_result(object, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_property_set_result(object, property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::DynamicPropertyGet { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let array = eval_property_get_result(object, &property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_property_set_result(object, &property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(class_name, property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let array = eval_static_property_get_result(&class_name, property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(&class_name, property, array, context, values)?; + } + return Ok(()); + } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let array = eval_static_property_get_result(&class_name, &property, context, values)?; + if let Some(array) = + eval_array_unset_target_result(array, index, context, scope, values)? + { + eval_static_property_set_result(&class_name, &property, array, context, values)?; + } + return Ok(()); + } + _ => {} + } + let array = eval_expr(array, context, scope, values)?; + eval_array_access_unset_result(array, index, context, scope, values) +} + +/// Unsets one offset from an already-resolved array-like target and returns a replacement array. +pub(super) fn eval_array_unset_target_result( + array: RuntimeCellHandle, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.type_tag(array)? == EVAL_TAG_OBJECT { + eval_array_access_unset_result(array, index, context, scope, values)?; + return Ok(None); + } + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + let index = eval_array_set_index(index, context, scope, values)?; + eval_array_without_key_result(array, index, values).map(Some) +} + +/// Executes `unset($object[$key])` through `ArrayAccess::offsetUnset()`. +pub(super) fn eval_array_access_unset_result( + array: RuntimeCellHandle, + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(array)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::UnsupportedConstruct); + } + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = eval_method_call_result(array, "offsetUnset", vec![index], context, values)?; + values.release(result)?; + Ok(()) +} + +/// Rebuilds an array without the strict-equal key requested by `unset($array[$key])`. +pub(super) fn eval_array_without_key_result( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let tag = values.type_tag(array)?; + let mut result = if tag == EVAL_TAG_ASSOC { + values.assoc_new(len.saturating_sub(1))? + } else { + values.array_new(len.saturating_sub(1))? + }; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let equal = values.compare(EvalBinOp::StrictEq, key, index)?; + if values.truthy(equal)? { + continue; + } + let value = values.array_get(array, key)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Executes `$var[] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. +pub(super) fn eval_array_append_var_stmt( + name: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + if let Some((object, _)) = existing { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return eval_non_object_array_append_var_stmt( + name, value, existing, context, scope, values, + ); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = + eval_method_call_result(object, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + + eval_non_object_array_append_var_stmt(name, value, existing, context, scope, values) +} + +/// Executes the non-object `$var[] = value` path with the existing array semantics. +pub(super) fn eval_non_object_array_append_var_stmt( + name: &str, + value: &EvalExpr, + existing: Option<(RuntimeCellHandle, ScopeCellOwnership)>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some((cell, flags_ownership)) = existing { + if values.is_array_like(cell)? { + let tag = values.type_tag(cell)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + ownership = flags_ownership; + cell + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { + values.release(replaced)?; + } + Ok(()) +} + +/// Executes `$var[index] = value` and dispatches object writes through `ArrayAccess::offsetSet()`. +pub(super) fn eval_array_set_var_stmt( + name: &str, + index: &EvalExpr, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let existing = scope_entry(context, scope, name) + .filter(|entry| entry.flags().is_visible()) + .map(|entry| (entry.cell(), entry.flags().ownership)); + if let Some((object, _)) = existing { + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return eval_non_object_array_set_var_stmt( + name, index, value, existing, context, scope, values, + ); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + if !eval_array_access_object_matches(object, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let result = + eval_method_call_result(object, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + + eval_non_object_array_set_var_stmt(name, index, value, existing, context, scope, values) +} + +/// Executes the non-object `$var[index] = value` path with the existing array semantics. +pub(super) fn eval_non_object_array_set_var_stmt( + name: &str, + index: &EvalExpr, + value: &EvalExpr, + existing: Option<(RuntimeCellHandle, ScopeCellOwnership)>, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some((cell, flags_ownership)) = existing { + if values.is_array_like(cell)? { + ownership = flags_ownership; + cell + } else { + values.array_new(1)? + } + } else { + values.array_new(1)? + }; + let index = eval_array_set_index(index, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = eval_array_set_target_for_index(array, index, values)?; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, name.to_string(), array, ownership)? { + values.release(replaced)?; + } + Ok(()) +} + +/// Executes `$object->property[] = value`, dispatching ArrayAccess property values when needed. +pub(super) fn eval_property_array_append_result( + object: RuntimeCellHandle, + property: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_property_get_result(object, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let array = if values.is_array_like(array)? { + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + array + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_property_set_result(object, property, array, context, values) +} + +/// Executes `$object->property[index] = value` and compound indexed property writes. +pub(super) fn eval_property_array_set_result( + object: RuntimeCellHandle, + property: &str, + index: &EvalExpr, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_property_get_result(object, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let index = eval_array_set_index(index, context, scope, values)?; + let array = if values.is_array_like(array)? { + array + } else { + values.array_new(1)? + }; + let array = eval_array_set_target_for_index(array, index, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_property_set_result(object, property, array, context, values) +} + +/// Computes the value written by a simple or compound property-array assignment. +pub(super) fn eval_property_array_set_value( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(op) = op else { + return eval_expr(value, context, scope, values); + }; + let current = eval_array_get_result(array, index, context, values)?; + let right = eval_expr(value, context, scope, values)?; + eval_binary_result(op, current, right, context, values) +} + +/// Executes `Class::$property[] = value`, including ArrayAccess static-property values. +pub(super) fn eval_static_property_array_append_result( + class_name: &str, + property: &str, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let offset = values.null()?; + let value = eval_expr(value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![offset, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let array = if values.is_array_like(array)? { + let tag = values.type_tag(array)?; + if !matches!(tag, EVAL_TAG_ARRAY | EVAL_TAG_ASSOC) { + return Err(EvalStatus::UnsupportedConstruct); + } + array + } else { + values.array_new(1)? + }; + let index = eval_array_append_key(array, values)?; + let value = eval_expr(value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_static_property_set_result(class_name, property, array, context, values) +} + +/// Executes `Class::$property[index] = value` and compound indexed static-property writes. +pub(super) fn eval_static_property_array_set_result( + class_name: &str, + property: &str, + index: &EvalExpr, + op: Option, + value: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let array = eval_static_property_get_result(class_name, property, context, values)?; + if values.type_tag(array)? == EVAL_TAG_OBJECT { + if !eval_array_access_object_matches(array, context, values)? { + return Err(EvalStatus::RuntimeFatal); + } + let index = eval_expr(index, context, scope, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let result = + eval_method_call_result(array, "offsetSet", vec![index, value], context, values)?; + values.release(result)?; + return Ok(()); + } + let index = eval_array_set_index(index, context, scope, values)?; + let array = if values.is_array_like(array)? { + array + } else { + values.array_new(1)? + }; + let array = eval_array_set_target_for_index(array, index, values)?; + let value = eval_property_array_set_value(array, index, op, value, context, scope, values)?; + let array = values.array_set(array, index, value)?; + eval_static_property_set_result(class_name, property, array, context, values) +} + +/// Evaluates an array-set index and normalizes PHP integer-string keys. +pub(super) fn eval_array_set_index( + index: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let index = eval_expr(index, context, scope, values)?; + if values.type_tag(index)? != EVAL_TAG_STRING { + return Ok(index); + } + let bytes = values.string_bytes(index)?; + match eval_numeric_string_array_key(&bytes) { + Some(key) => values.int(key), + None => Ok(index), + } +} + +/// Converts indexed arrays to associative arrays before writing a non-numeric string key. +pub(super) fn eval_array_set_target_for_index( + array: RuntimeCellHandle, + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(array)? != EVAL_TAG_ARRAY || values.type_tag(index)? != EVAL_TAG_STRING { + return Ok(array); + } + let len = values.array_len(array)?; + let mut assoc = values.assoc_new(len + 1)?; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + let value = values.array_get(array, key)?; + assoc = values.array_set(assoc, key, value)?; + } + Ok(assoc) +} diff --git a/crates/elephc-magician/src/interpreter/statements/attributes_magic_validation.rs b/crates/elephc-magician/src/interpreter/statements/attributes_magic_validation.rs new file mode 100644 index 0000000000..ce6f02f4f1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/attributes_magic_validation.rs @@ -0,0 +1,620 @@ +//! Purpose: +//! Validates class-like attributes, modifiers, override markers, and magic methods. +//! +//! Called from: +//! - Eval class, interface, trait, and enum declaration validation. +//! +//! Key details: +//! - AOT member flags and magic signature contracts are shared with later validators. + +use super::*; + +/// Bridge reflection flag for static generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_STATIC: u64 = 1; + +/// Bridge reflection flag for public generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PUBLIC: u64 = 2; + +/// Bridge reflection flag for protected generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED: u64 = 4; + +/// Bridge reflection flag for private generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE: u64 = 8; + +/// Bridge reflection flag for final generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_FINAL: u64 = 16; + +/// Bridge reflection flag for abstract generated/AOT members. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT: u64 = 32; + +/// Bridge reflection flag for readonly generated/AOT properties. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_READONLY: u64 = 64; + +/// Bridge reflection flag for protected-set generated/AOT properties. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET: u64 = 2048; + +/// Bridge reflection flag for private-set generated/AOT properties. +pub(super) const EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET: u64 = 4096; + +/// Method requirement discovered from generated/AOT interface metadata. +pub(super) struct EvalAotInterfaceMethodRequirement { + pub(super) owner: String, + pub(super) name: String, + pub(super) is_static: bool, + pub(super) signature: Option, +} + +/// Abstract method requirement discovered from generated/AOT parent metadata. +pub(super) struct EvalAotAbstractMethodRequirement { + pub(super) owner: String, + pub(super) is_static: bool, + pub(super) visibility: EvalVisibility, + pub(super) signature: Option, +} + +/// Abstract property requirement discovered from generated/AOT parent metadata. +pub(super) struct EvalAotAbstractPropertyRequirement { + pub(super) owner: String, + pub(super) property: EvalClassProperty, +} + +/// Rejects builtin attributes that cannot target an eval-declared class. +pub(super) fn validate_eval_class_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "Override") { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects builtin attributes that cannot target eval-declared interfaces. +pub(super) fn validate_eval_interface_attribute_targets( + interface: &EvalInterface, +) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(interface.attributes())?; + for property in interface.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; + } + for method in interface.methods() { + validate_eval_method_attribute_targets(method.attributes())?; + } + Ok(()) +} + +/// Validates PHP's global `#[Override]` marker on eval-declared interface methods. +pub(super) fn validate_eval_interface_override_attributes( + interface: &EvalInterface, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let parent_requirements = eval_interface_parent_method_requirements(interface, context); + for method in interface.methods() { + if !eval_interface_method_has_global_builtin_attribute(method, "Override") { + continue; + } + if parent_requirements + .iter() + .any(|(_, requirement)| eval_interface_method_matches_requirement(method, requirement)) + { + continue; + } + if eval_aot_interface_parent_method_matches(interface, method, values)? { + continue; + } + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns method requirements inherited by one eval interface declaration. +pub(super) fn eval_interface_parent_method_requirements( + interface: &EvalInterface, + context: &ElephcEvalContext, +) -> Vec<(String, EvalInterfaceMethod)> { + let mut requirements = Vec::new(); + for parent in interface.parents() { + if context.has_interface(parent) { + requirements.extend(context.interface_method_requirements_with_owners(parent)); + } + requirements.extend(builtin_interface_method_requirements(parent)); + } + requirements +} + +/// Returns whether a generated/AOT parent interface exposes a matching method. +pub(super) fn eval_aot_interface_parent_method_matches( + interface: &EvalInterface, + method: &EvalInterfaceMethod, + values: &mut impl RuntimeValueOps, +) -> Result { + for parent in interface.parents() { + if !values.interface_exists(parent)? { + continue; + } + let parent = parent.trim_start_matches('\\'); + if let Some(flags) = values.reflection_method_flags(parent, method.name())? { + let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + return Ok(parent_method_is_static == method.is_static()); + } + } + Ok(false) +} + +/// Returns whether an interface method matches one inherited requirement signature kind. +pub(super) fn eval_interface_method_matches_requirement( + method: &EvalInterfaceMethod, + requirement: &EvalInterfaceMethod, +) -> bool { + requirement.name().eq_ignore_ascii_case(method.name()) + && requirement.is_static() == method.is_static() +} + +/// Rejects builtin attributes that cannot target eval-declared traits. +pub(super) fn validate_eval_trait_attribute_targets(trait_decl: &EvalTrait) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(trait_decl.attributes())?; + for property in trait_decl.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; + } + for method in trait_decl.methods() { + validate_eval_method_attribute_targets(method.attributes())?; + } + Ok(()) +} + +/// Rejects builtin attributes that cannot target eval-declared enums. +pub(super) fn validate_eval_enum_attribute_targets(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + validate_eval_non_class_attribute_targets(enum_decl.attributes()) +} + +/// Rejects class-only or method-only builtin attributes on non-class declarations. +pub(super) fn validate_eval_non_class_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") + || eval_attributes_have_global_builtin_attribute(attributes, "Override") + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects class-only or method-only builtin attributes on non-method members. +pub(super) fn validate_eval_non_method_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") + || eval_attributes_have_global_builtin_attribute(attributes, "Override") + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects class-only builtin attributes on method declarations. +pub(super) fn validate_eval_method_attribute_targets( + attributes: &[EvalAttribute], +) -> Result<(), EvalStatus> { + if eval_attributes_have_global_builtin_attribute(attributes, "AllowDynamicProperties") { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Returns whether the attribute list contains one global builtin attribute. +pub(super) fn eval_attributes_have_global_builtin_attribute( + attributes: &[EvalAttribute], + builtin: &str, +) -> bool { + attributes + .iter() + .any(|attribute| eval_attribute_is_global_builtin(attribute, builtin)) +} + +/// Returns whether one attribute names a global builtin attribute class. +pub(super) fn eval_attribute_is_global_builtin(attribute: &EvalAttribute, builtin: &str) -> bool { + attribute + .name() + .trim_start_matches('\\') + .eq_ignore_ascii_case(builtin) +} + +/// Validates PHP's global `#[Override]` marker on one eval-declared method. +pub(super) fn validate_eval_override_attribute( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !eval_method_has_global_builtin_attribute(method, "Override") { + return Ok(()); + } + if eval_method_overrides_parent(class, method, context) + || eval_method_overrides_aot_parent(class, method, context, values)? + || eval_method_implements_interface(class, method, context, values)? + { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether a method has a global builtin marker attribute. +pub(super) fn eval_method_has_global_builtin_attribute(method: &EvalClassMethod, builtin: &str) -> bool { + eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) +} + +/// Returns whether an interface method has a global builtin marker attribute. +pub(super) fn eval_interface_method_has_global_builtin_attribute( + method: &EvalInterfaceMethod, + builtin: &str, +) -> bool { + eval_attributes_have_global_builtin_attribute(method.attributes(), builtin) +} + +/// Returns whether one method overrides a non-private parent method. +pub(super) fn eval_method_overrides_parent( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, +) -> bool { + class + .parent() + .and_then(|parent| context.class_method(parent, method.name())) + .is_some_and(|(_, parent_method)| { + parent_method.visibility() != EvalVisibility::Private + && parent_method.is_static() == method.is_static() + }) +} + +/// Returns whether one method overrides a visible generated/AOT parent method. +pub(super) fn eval_method_overrides_aot_parent( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(parent) = pending_class_native_parent_name(class, context) else { + return Ok(false); + }; + if !values.class_exists(&parent)? { + return Ok(false); + } + let Some(flags) = values.reflection_method_flags(&parent, method.name())? else { + return Ok(false); + }; + let parent_method_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let parent_method_is_private = flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0; + Ok(!parent_method_is_private && parent_method_is_static == method.is_static()) +} + +/// Returns the nearest generated/AOT parent for a class not yet registered in context. +pub(super) fn pending_class_native_parent_name( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Option { + let mut current = class.parent()?.to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + let resolved = context + .resolve_class_name(¤t) + .unwrap_or_else(|| current.trim_start_matches('\\').to_string()); + if !seen.insert(resolved.to_ascii_lowercase()) { + return None; + } + let Some(parent_class) = context.class(&resolved) else { + return Some(resolved.trim_start_matches('\\').to_string()); + }; + current = parent_class.parent()?.to_string(); + } +} + +/// Returns whether one method implements a direct or inherited interface method. +pub(super) fn eval_method_implements_interface( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if pending_class_interface_names(class, context) + .iter() + .filter(|interface| context.has_interface(interface)) + .any(|interface| { + context + .interface_method_requirements_with_owners(interface) + .into_iter() + .any(|(_, requirement)| { + requirement.name().eq_ignore_ascii_case(method.name()) + && requirement.is_static() == method.is_static() + }) + }) + { + return Ok(true); + } + Ok(pending_class_aot_interface_method_requirements(class, context, values)? + .iter() + .any(|requirement| { + requirement.name.eq_ignore_ascii_case(method.name()) + && requirement.is_static == method.is_static() + })) +} + +/// Validates PHP magic-method contracts for one eval class-like method list. +pub(super) fn validate_eval_magic_methods(methods: &[EvalClassMethod]) -> Result<(), EvalStatus> { + for method in methods { + validate_eval_magic_method(method)?; + } + Ok(()) +} + +/// Validates staticness, visibility, arity, and declared return type for one eval magic method. +pub(super) fn validate_eval_magic_method(method: &EvalClassMethod) -> Result<(), EvalStatus> { + let name = method.name().to_ascii_lowercase(); + if validated_eval_magic_method_rejects_by_ref_params(&name) { + validate_magic_no_by_ref_params(method)?; + } + match name.as_str() { + "__tostring" => { + validate_magic_non_static(method)?; + validate_magic_public(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::String)?; + } + "__get" | "__isset" | "__unset" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; + if method.name().eq_ignore_ascii_case("__isset") { + validate_magic_declared_return_type(method, MagicReturnType::Bool)?; + } else if method.name().eq_ignore_ascii_case("__unset") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } + } + "__set" | "__call" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 2)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; + if method.name().eq_ignore_ascii_case("__set") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } else { + validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; + } + } + "__callstatic" => { + validate_magic_static(method)?; + validate_magic_arity(method, 2)?; + validate_magic_declared_param_type(method, 0, MagicParamType::String)?; + validate_magic_declared_param_type(method, 1, MagicParamType::Array)?; + } + "__sleep" | "__serialize" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::Array)?; + } + "__wakeup" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } + "__unserialize" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::Array)?; + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } + "__debuginfo" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + validate_magic_declared_return_type(method, MagicReturnType::NullableArray)?; + } + "__set_state" => { + validate_magic_static(method)?; + validate_magic_arity(method, 1)?; + validate_magic_declared_param_type(method, 0, MagicParamType::Array)?; + } + "__invoke" => { + validate_magic_non_static(method)?; + } + "__clone" | "__destruct" => { + validate_magic_non_static(method)?; + validate_magic_arity(method, 0)?; + if method.name().eq_ignore_ascii_case("__clone") { + validate_magic_declared_return_type(method, MagicReturnType::Void)?; + } else { + validate_magic_no_declared_return_type(method)?; + } + } + "__construct" => { + if method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + validate_magic_no_declared_return_type(method)?; + } + _ => {} + } + Ok(()) +} + +/// Returns whether PHP rejects by-reference parameters for this magic method. +pub(super) fn validated_eval_magic_method_rejects_by_ref_params(name: &str) -> bool { + is_validated_eval_magic_method(name) && !matches!(name, "__construct" | "__invoke") +} + +/// Returns whether eval knows PHP declaration-time rules for this magic method. +pub(super) fn is_validated_eval_magic_method(name: &str) -> bool { + matches!( + name, + "__tostring" + | "__get" + | "__isset" + | "__unset" + | "__set" + | "__call" + | "__callstatic" + | "__sleep" + | "__serialize" + | "__wakeup" + | "__unserialize" + | "__debuginfo" + | "__set_state" + | "__invoke" + | "__clone" + | "__destruct" + | "__construct" + ) +} + +/// Magic method return types that eval can validate from retained declarations. +#[derive(Clone, Copy)] +pub(super) enum MagicReturnType { + Array, + Bool, + NullableArray, + String, + Void, +} + +/// Magic method parameter types that eval can validate from retained declarations. +#[derive(Clone, Copy)] +pub(super) enum MagicParamType { + Array, + String, +} + +/// Rejects static declarations for magic methods that must be instance methods. +pub(super) fn validate_magic_non_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.is_static() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects instance declarations for magic methods that must be static methods. +pub(super) fn validate_magic_static(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.is_static() { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Rejects non-public declarations for public-only PHP magic methods. +pub(super) fn validate_magic_public(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.visibility() == EvalVisibility::Public { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Rejects magic methods whose arity differs from PHP's required shape. +pub(super) fn validate_magic_arity(method: &EvalClassMethod, expected: usize) -> Result<(), EvalStatus> { + let has_variadic = method + .parameter_is_variadic() + .iter() + .any(|is_variadic| *is_variadic); + if method.params().len() == expected && !has_variadic { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Rejects by-reference parameters on PHP magic methods. +pub(super) fn validate_magic_no_by_ref_params(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method + .parameter_is_by_ref() + .iter() + .any(|is_by_ref| *is_by_ref) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects incompatible explicit parameter types on PHP magic methods. +pub(super) fn validate_magic_declared_param_type( + method: &EvalClassMethod, + position: usize, + expected: MagicParamType, +) -> Result<(), EvalStatus> { + let Some(Some(parameter_type)) = method.parameter_types().get(position) else { + return Ok(()); + }; + if magic_param_type_matches(parameter_type, expected) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether one retained eval parameter type is exactly the expected magic atom. +pub(super) fn magic_param_type_matches( + parameter_type: &EvalParameterType, + expected: MagicParamType, +) -> bool { + if parameter_type.allows_null() || parameter_type.is_intersection() { + return false; + } + let [variant] = parameter_type.variants() else { + return false; + }; + matches!( + (expected, variant), + (MagicParamType::Array, EvalParameterTypeVariant::Array) + | (MagicParamType::String, EvalParameterTypeVariant::String) + ) +} + +/// Rejects PHP magic methods that cannot declare any return type. +pub(super) fn validate_magic_no_declared_return_type(method: &EvalClassMethod) -> Result<(), EvalStatus> { + if method.return_type().is_some() { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects incompatible explicit return types on PHP magic methods. +pub(super) fn validate_magic_declared_return_type( + method: &EvalClassMethod, + expected: MagicReturnType, +) -> Result<(), EvalStatus> { + method.return_type().map_or(Ok(()), |return_type| { + if magic_return_type_matches(return_type, expected) { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } + }) +} + +/// Returns whether one retained eval return type is exactly the expected magic return atom. +pub(super) fn magic_return_type_matches( + return_type: &EvalParameterType, + expected: MagicReturnType, +) -> bool { + if return_type.is_intersection() { + return false; + } + if return_type.allows_null() && !matches!(expected, MagicReturnType::NullableArray) { + return false; + } + let [variant] = return_type.variants() else { + return false; + }; + matches!( + (expected, variant), + (MagicReturnType::Array, EvalParameterTypeVariant::Array) + | (MagicReturnType::Bool, EvalParameterTypeVariant::Bool) + | (MagicReturnType::NullableArray, EvalParameterTypeVariant::Array) + | (MagicReturnType::String, EvalParameterTypeVariant::String) + | (MagicReturnType::Void, EvalParameterTypeVariant::Void) + ) +} diff --git a/crates/elephc-magician/src/interpreter/statements/callable_objects.rs b/crates/elephc-magician/src/interpreter/statements/callable_objects.rs new file mode 100644 index 0000000000..3d80bbe81e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/callable_objects.rs @@ -0,0 +1,242 @@ +//! Purpose: +//! Dispatches invokable objects and magic instance/static calls. +//! +//! Called from: +//! - Callable and method dispatch after normal method lookup. +//! +//! Key details: +//! - `__invoke`, `__call`, and `__callStatic` receive normalized argument arrays. + +use super::*; + +/// Dispatches an invokable object through `__invoke()` without enforcing hook visibility. +pub(in crate::interpreter) fn eval_invokable_object_call_result( + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; + return values.method_call(object, "__invoke", evaluated_args); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__invoke", + context, + values, + )? + else { + return eval_throw_object_not_callable_error(&class_name, context, values); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + return eval_native_method_with_evaluated_args_unchecked( + object, + &class_name, + "__invoke", + evaluated_args, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + let Some((declaring_class, method)) = context.class_method(&called_class_name, "__invoke") + else { + if let Some(native_class_name) = + eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? + { + return eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + &native_class_name, + "__invoke", + evaluated_args, + Some(&native_class_name), + Some(&called_class_name), + context, + values, + ); + } + return eval_throw_object_not_callable_error(&called_class_name, context, values); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ) +} + +/// Rejects non-invokable eval-declared objects before dynamic-call arguments are evaluated. +pub(in crate::interpreter) fn eval_invokable_object_precheck( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &class_name, + "__invoke", + context, + values, + )? + else { + return eval_throw_object_not_callable_error(&class_name, context, values); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(()); + }; + let called_class_name = class.name().to_string(); + let Some((_, method)) = context.class_method(&called_class_name, "__invoke") else { + if eval_dynamic_class_native_invokable_method_class(&called_class_name, context, values)? + .is_some() + { + return Ok(()); + } + return eval_throw_object_not_callable_error(&called_class_name, context, values); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the generated/AOT class that can dispatch an inherited `__invoke()` hook. +pub(in crate::interpreter) fn eval_dynamic_class_native_invokable_method_class( + called_class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, _, is_static, is_abstract)) = + eval_dynamic_class_native_method_metadata(called_class_name, "__invoke", context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + Ok(Some(declaring_class)) +} + +/// Returns generated/AOT method metadata inherited by an eval-declared class. +pub(in crate::interpreter) fn eval_dynamic_class_native_method_metadata( + called_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + eval_aot_method_dispatch_metadata_in_hierarchy(&parent, method_name, context, values) +} + +/// Dispatches a missing or inaccessible eval instance method through `__call()`. +pub(in crate::interpreter) fn eval_magic_instance_method_call( + object: RuntimeCellHandle, + called_class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(called_class_name, "__call") else { + return Ok(None); + }; + if method.is_static() || method.is_abstract() { + return Ok(None); + } + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_dynamic_method_with_values( + &declaring_class, + called_class_name, + &method, + object, + magic_args, + context, + values, + ) + .map(Some) +} + +/// Dispatches a missing or inaccessible eval static method through `__callStatic()`. +pub(in crate::interpreter) fn eval_magic_static_method_call( + class_name: &str, + called_class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(class_name, "__callStatic") else { + return Ok(None); + }; + if !method.is_static() || method.is_abstract() { + return Ok(None); + } + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_dynamic_static_method_with_values( + &declaring_class, + called_class_name, + &method, + magic_args, + context, + values, + ) + .map(Some) +} + +/// Builds the two synthetic arguments passed to `__call()` and `__callStatic()`. +pub(super) fn eval_magic_call_args( + method_name: &str, + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method = values.string(method_name)?; + let args = eval_magic_call_arg_array(evaluated_args, values)?; + Ok(positional_args(vec![method, args])) +} + +/// Materializes PHP's `$args` array for a magic method fallback. +pub(super) fn eval_magic_call_arg_array( + evaluated_args: Vec, + values: &mut impl RuntimeValueOps, +) -> Result { + let contains_named = evaluated_args.iter().any(|arg| arg.name.is_some()); + let mut args = if contains_named { + values.assoc_new(evaluated_args.len())? + } else { + values.array_new(evaluated_args.len())? + }; + let mut next_positional = 0_i64; + for arg in evaluated_args { + let key = if let Some(name) = arg.name { + values.string(&name)? + } else { + let key = values.int(next_positional)?; + next_positional = next_positional + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + key + }; + args = values.array_set(args, key, arg.value)?; + } + Ok(args) +} diff --git a/crates/elephc-magician/src/interpreter/statements/class_declarations.rs b/crates/elephc-magician/src/interpreter/statements/class_declarations.rs new file mode 100644 index 0000000000..f4905b5229 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/class_declarations.rs @@ -0,0 +1,123 @@ +//! Purpose: +//! Registers eval class declarations and validates their direct parent. +//! +//! Called from: +//! - Statement dispatch for class and anonymous-class declarations. +//! +//! Key details: +//! - Registration occurs only after trait expansion and declaration validation succeed. + +use super::*; + +/// Registers an eval-declared class in the dynamic class table. +pub(in crate::interpreter) fn execute_class_decl_stmt( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = class.name().trim_start_matches('\\'); + if context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || context.has_enum(name) + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.trait_exists(name)? + || values.enum_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + let class = expand_eval_class_traits(class, context)?.with_readonly_properties(); + let class = &class; + validate_eval_class_modifiers(class, context, values)?; + let native_parent = validate_eval_class_parent(class, context, values)?; + for interface in class.interfaces() { + if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_eval_class_does_not_implement_throwable_interfaces(class, context)?; + validate_eval_class_does_not_implement_enum_interfaces(class, context)?; + validate_declared_class_interface_members(class, context)?; + validate_declared_class_builtin_interface_members(class, context)?; + validate_declared_class_aot_interface_members(class, context, values)?; + if !class.is_abstract() { + validate_concrete_class_requirements(class, context)?; + validate_concrete_class_builtin_interface_requirements(class, context)?; + validate_concrete_class_aot_parent_requirements(class, context, values)?; + validate_concrete_class_aot_interface_requirements(class, context, values)?; + } + if context.define_class(class.clone()) { + if let Some(parent) = native_parent.as_deref() { + if !context.define_native_class_parent(class.name(), parent) { + return Err(EvalStatus::RuntimeFatal); + } + } + initialize_eval_declared_constants( + class.name(), + class.constants(), + context, + scope, + values, + )?; + initialize_eval_static_properties(class, context, scope, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Validates an eval class parent and returns an AOT parent name when the parent is runtime-backed. +pub(super) fn validate_eval_class_parent( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(None); + }; + let parent = context + .resolve_class_name(parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()); + if let Some(parent_class) = context.class(&parent) { + if parent_class.is_final() + || parent_class.is_readonly_class() != class.is_readonly_class() + || context.class_is_a(&parent, class.name(), false) + { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(None); + } + let Some((parent_is_final, parent_is_readonly)) = + eval_reflection_aot_class_inheritance_modifiers(&parent, values)? + else { + return Err(EvalStatus::RuntimeFatal); + }; + if parent_is_final + || parent_is_readonly != class.is_readonly_class() + || native_class_is_a(&parent, class.name(), context) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(parent)) +} + +/// Registers one eval anonymous class expression if this execution has not seen it yet. +pub(in crate::interpreter) fn ensure_eval_anonymous_class_decl( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !class.is_anonymous() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(existing) = context.class(class.name()) { + return if existing.is_anonymous() { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + }; + } + execute_class_decl_stmt(class, context, scope, values) +} diff --git a/crates/elephc-magician/src/interpreter/statements/class_resolution.rs b/crates/elephc-magician/src/interpreter/statements/class_resolution.rs new file mode 100644 index 0000000000..b16c5f66de --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/class_resolution.rs @@ -0,0 +1,512 @@ +//! Purpose: +//! Resolves class-like names and allocates or clones dynamic objects. +//! +//! Called from: +//! - New-object, clone, and static-member dispatch paths. +//! +//! Key details: +//! - self/parent/static scope, native parents, constructor modes, and clone hooks are resolved here. + +use super::*; + +/// Resolves a static method using private-method scope rules. +pub(in crate::interpreter) fn eval_dynamic_static_method_for_call( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(current_class) = context.current_class_scope() { + if eval_classes_are_related(current_class, class_name, context) { + if let Some((declaring_class, method)) = + context.class_own_method(current_class, method_name) + { + if method.visibility() == EvalVisibility::Private { + return Some((declaring_class, method)); + } + } + } + } + context.class_method(class_name, method_name) +} + +/// Resolves `self`, `parent`, and `static` for eval static member access. +pub(in crate::interpreter) fn resolve_eval_static_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" => context + .current_class_scope() + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "static" => context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal), + "parent" => { + let current = context + .current_class_scope() + .ok_or(EvalStatus::RuntimeFatal)?; + context + .class(current) + .and_then(EvalClass::parent) + .map(|parent| { + context + .resolve_class_name(parent) + .unwrap_or_else(|| parent.trim_start_matches('\\').to_string()) + }) + .or_else(|| context.native_class_parent(current).map(str::to_string)) + .ok_or(EvalStatus::RuntimeFatal) + } + _ => context + .resolve_class_name(class_name) + .or_else(|| { + context + .has_class(class_name) + .then(|| class_name.to_string()) + }) + .ok_or(EvalStatus::RuntimeFatal), + } +} + +/// Resolved static method dispatch metadata preserving PHP late-static forwarding. +pub(in crate::interpreter) struct EvalStaticMethodReceiver { + pub(in crate::interpreter) dispatch_class: String, + pub(in crate::interpreter) called_class: String, +} + +/// Resolves static method receivers into lookup and late-static called-class names. +pub(in crate::interpreter) fn resolve_eval_static_method_receiver( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + let dispatch_class = resolve_eval_static_member_class_name(class_name, context)?; + let called_class = match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" => context + .current_called_class_scope() + .or_else(|| context.current_class_scope()) + .map(str::to_string) + .ok_or(EvalStatus::RuntimeFatal)?, + "static" => dispatch_class.clone(), + _ => dispatch_class.clone(), + }; + Ok(EvalStaticMethodReceiver { + dispatch_class, + called_class, + }) +} + +/// Resolves static member receivers while allowing non-eval class names to reach AOT lookup. +pub(in crate::interpreter) fn resolve_eval_static_member_class_name( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Returns true when an eval-declared class-like symbol should not fall through to AOT lookup. +pub(super) fn eval_static_member_context_owns_class( + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + context.has_class(class_name) + || context.has_interface(class_name) + || context.has_trait(class_name) + || context.has_enum(class_name) +} + +/// Returns whether a static member receiver exists in eval metadata or generated metadata. +pub(super) fn eval_runtime_class_like_exists( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_static_member_context_owns_class(class_name, context) + || values.class_exists(class_name)? + || eval_runtime_interface_exists(class_name, values)? + || values.trait_exists(class_name)? + || values.enum_exists(class_name)?) +} + +/// Resolves class-name literal receivers without requiring named classes to exist. +pub(super) fn resolve_eval_class_name_literal( + class_name: &str, + context: &ElephcEvalContext, +) -> Result { + match class_name.to_ascii_lowercase().as_str() { + "self" | "parent" | "static" => resolve_eval_static_class_name(class_name, context), + _ => Ok(context + .resolve_class_like_name(class_name) + .unwrap_or_else(|| class_name.trim_start_matches('\\').to_string())), + } +} + +/// Creates a backing object for an eval-declared class and runs its constructor. +pub(in crate::interpreter) fn eval_dynamic_class_new_object( + class: &EvalClass, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_class_new_object_with_ref_mode( + class, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + caller_scope, + values, + ) +} + +/// Creates an eval-declared object while using the selected constructor by-ref mode. +pub(super) fn eval_dynamic_class_new_object_with_ref_mode( + class: &EvalClass, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = eval_dynamic_class_allocate_object(class, context, caller_scope, values)?; + if let Some((constructor_class, constructor)) = + context.class_method(class.name(), "__construct") + { + if validate_eval_member_access(&constructor_class, constructor.visibility(), context) + .is_err() + { + let _ = values.release(object); + return eval_throw_method_access_error( + &constructor_class, + constructor.name(), + constructor.visibility(), + context, + values, + ); + } + let result = eval_dynamic_method_with_values_and_ref_mode( + &constructor_class, + class.name(), + &constructor, + object, + constructor.parameter_is_by_ref(), + evaluated_args, + by_ref_mode, + context, + values, + )?; + eval_release_value(context, values, result)?; + } else if !evaluated_args.is_empty() { + if let Some(parent) = context.class_native_parent_name(class.name()) { + eval_native_constructor_with_evaluated_args_and_ref_mode( + &parent, + object, + evaluated_args, + by_ref_mode, + context, + values, + )?; + } else { + return Err(EvalStatus::RuntimeFatal); + } + } else if let Some(parent) = context.class_native_parent_name(class.name()) { + if eval_aot_method_dispatch_metadata_in_hierarchy( + &parent, + "__construct", + context, + values, + )? + .is_some() + { + eval_native_constructor_with_evaluated_args_and_ref_mode( + &parent, + object, + Vec::new(), + by_ref_mode, + context, + values, + )?; + } + } + Ok(object) +} + +/// Creates a PHP shallow clone and invokes an eval-declared `__clone()` hook when present. +pub(in crate::interpreter) fn eval_object_clone_result( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(object)?; + let dynamic_class_name = context + .dynamic_object_class(identity) + .map(|class| class.name().to_string()); + let clone_method = dynamic_class_name + .as_deref() + .and_then(|class_name| context.class_method(class_name, "__clone")); + if let Some((declaring_class, method)) = &clone_method { + if validate_eval_member_access(declaring_class, method.visibility(), context).is_err() { + return eval_throw_clone_access_error( + declaring_class, + method.visibility(), + context, + values, + ); + } + } + let dynamic_native_clone_hook_scope = if clone_method.is_none() { + if let Some(class_name) = dynamic_class_name.as_deref() { + eval_dynamic_native_clone_hook_is_callable(class_name, context, values)? + } else { + None + } + } else { + None + }; + let should_call_aot_clone_hook = if dynamic_class_name.is_none() { + eval_aot_clone_hook_is_callable(object, context, values)? + } else { + false + }; + + let clone = values.object_clone_shallow(object)?; + if let Some(class_name) = dynamic_class_name { + let clone_identity = values.object_identity(clone)?; + context.register_dynamic_object(clone_identity, &class_name); + context.clone_dynamic_property_aliases(identity, clone_identity); + if let Some((declaring_class, method)) = clone_method { + let result = eval_dynamic_method_with_values( + &declaring_class, + &class_name, + &method, + clone, + Vec::new(), + context, + values, + )?; + eval_release_value(context, values, result)?; + } else if let Some(scope) = dynamic_native_clone_hook_scope { + let result = eval_native_method_call_with_scope( + &scope, + None, + clone, + "__clone", + Vec::new(), + context, + values, + )?; + values.release(result)?; + } + } else if should_call_aot_clone_hook { + let result = values.method_call(clone, "__clone", Vec::new())?; + values.release(result)?; + } + Ok(clone) +} + +/// Returns the declaring scope for an inherited generated/AOT `__clone()` hook. +pub(super) fn eval_dynamic_native_clone_hook_is_callable( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_dynamic_class_native_method_metadata(class_name, "__clone", context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_clone_access_error(&declaring_class, visibility, context, values); + } + Ok(Some(declaring_class)) +} + +/// Calls one generated/AOT method while presenting an explicit PHP class scope to the bridge. +pub(super) fn eval_native_method_call_with_scope( + scope: &str, + called_class_scope: Option<&str>, + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.push_class_scope(scope.to_string()); + if let Some(called_class) = called_class_scope { + context.push_called_class_scope(called_class.to_string()); + } + let _called_class_override = called_class_scope + .map(|called_class| push_native_frame_called_class_override(context, scope, called_class)); + let result = values.method_call(object, method_name, evaluated_args); + if called_class_scope.is_some() { + context.pop_called_class_scope(); + } + context.pop_class_scope(); + result +} + +/// Calls one generated/AOT static method while presenting an explicit PHP class scope. +pub(super) fn eval_native_static_method_call_with_scope( + scope: &str, + called_class_scope: Option<&str>, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + context.push_class_scope(scope.to_string()); + if let Some(called_class) = called_class_scope { + context.push_called_class_scope(called_class.to_string()); + } + let _called_class_override = called_class_scope + .map(|called_class| push_native_frame_called_class_override(context, scope, called_class)); + let result = values.static_method_call(class_name, method_name, evaluated_args); + if called_class_scope.is_some() { + context.pop_called_class_scope(); + } + context.pop_class_scope(); + result +} + +/// Runs one generated/AOT bridge operation while exposing an explicit PHP class scope. +pub(super) fn eval_with_native_bridge_scope( + scope: &str, + context: &mut ElephcEvalContext, + call: impl FnOnce() -> Result, +) -> Result { + context.push_class_scope(scope.to_string()); + let result = call(); + context.pop_class_scope(); + result +} + +/// Returns generated/AOT property metadata inherited by an eval-declared class. +pub(super) fn eval_dynamic_class_native_property_metadata( + called_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + eval_reflection_aot_property_access_metadata(&parent, property_name, values) +} + +/// Returns generated/AOT class-constant metadata inherited by an eval-declared class. +pub(super) fn eval_dynamic_class_native_constant_metadata( + called_class_name: &str, + constant_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(parent) = context.class_native_parent_name(called_class_name) else { + return Ok(None); + }; + let Some(flags) = values.reflection_constant_flags(&parent, constant_name)? else { + return Ok(None); + }; + let declaring_class = values + .reflection_constant_declaring_class(&parent, constant_name)? + .unwrap_or(parent); + let visibility = if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + }; + Ok(Some((declaring_class, visibility))) +} + +/// Returns whether an accessible instance AOT `__clone()` hook should run. +pub(super) fn eval_aot_clone_hook_is_callable( + object: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = eval_runtime_object_class_name(object, values)?; + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&class_name, "__clone", values)? + else { + return Ok(false); + }; + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_clone_access_error(&declaring_class, visibility, context, values); + } + Ok(true) +} + +/// Reads the PHP-visible runtime class name for one AOT object handle. +pub(super) fn eval_runtime_object_class_name( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name)?; + values.release(class_name)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Creates a backing object for an eval-declared class without running its constructor. +pub(super) fn eval_dynamic_class_allocate_object( + class: &EvalClass, + context: &mut ElephcEvalContext, + caller_scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if class.is_abstract() || context.has_enum(class.name()) { + return Err(EvalStatus::RuntimeFatal); + } + let backing_class = context + .class_native_parent_name(class.name()) + .unwrap_or_else(|| String::from("stdClass")); + let object = values.new_object(&backing_class)?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, class.name()); + let mut class_chain = context.class_chain(class.name()); + if class_chain.is_empty() { + class_chain.push(class.clone()); + } + for class in &class_chain { + for property in class + .properties() + .iter() + .filter(|property| !property.is_static() && !property.is_abstract()) + { + let value = if let Some(default) = property.default() { + Some(eval_class_like_member_default( + class.name(), + property.trait_origin(), + default, + context, + caller_scope, + values, + )?) + } else if property.property_type().is_none() { + Some(values.null()?) + } else { + None + }; + let storage_name = eval_instance_property_storage_name(class.name(), property); + if let Some(value) = value { + values.property_set(object, &storage_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_name); + } + } + } + Ok(object) +} diff --git a/crates/elephc-magician/src/interpreter/statements/closure_binding.rs b/crates/elephc-magician/src/interpreter/statements/closure_binding.rs new file mode 100644 index 0000000000..4d3df3b96d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/closure_binding.rs @@ -0,0 +1,451 @@ +//! Purpose: +//! Implements builtin Closure static methods and binding transformations. +//! +//! Called from: +//! - Static method dispatch for the eval Closure class surface. +//! +//! Key details: +//! - Receiver, scope, called class, and unbound targets are validated before cloning closures. + +use super::*; + +/// Dispatches static methods for eval's builtin `Closure` class slice. +pub(super) fn eval_closure_static_method_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("Closure") + { + return Ok(None); + } + if method_name.eq_ignore_ascii_case("fromCallable") { + return eval_closure_from_callable(evaluated_args, lexical_scope, context, values) + .map(Some); + } + if method_name.eq_ignore_ascii_case("bind") { + return eval_closure_bind_static(evaluated_args, context, values).map(Some); + } + Ok(None) +} + +/// Materializes `Closure::fromCallable()` from one normalized eval callback. +pub(super) fn eval_closure_from_callable( + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut args = bind_evaluated_function_args(&[String::from("callback")], evaluated_args)?; + let callback = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + let callable = match lexical_scope { + Some(scope) => eval_callable_from_scope(callback, context, scope, values), + None => eval_callable(callback, context, values), + }; + let callable = match callable { + Ok(callable) => callable, + Err(EvalStatus::UnsupportedConstruct) if values.type_tag(callback)? == EVAL_TAG_OBJECT => { + return eval_closure_from_callable_type_error( + "no array or string given", + context, + values, + ); + } + Err(status) => return Err(status), + }; + eval_validate_closure_from_callable_callback(&callable, context, values)?; + let target = eval_closure_object_target_from_callable(callable); + eval_closure_object_from_target(target, context, values) +} + +/// Converts a normalized callable target into the storage used by eval Closure objects. +pub(super) fn eval_closure_object_target_from_callable( + callable: EvaluatedCallable, +) -> EvalClosureObjectTarget { + match callable { + EvaluatedCallable::Named { name, .. } => EvalClosureObjectTarget::Named(name), + EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + } => EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + }, + EvaluatedCallable::InvokableObject { object } => { + EvalClosureObjectTarget::InvokableObject { object } + } + EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + }, + EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + }, + } +} + +/// Allocates a PHP-visible eval Closure object for one retained callable target. +pub(super) fn eval_closure_object_from_target( + target: EvalClosureObjectTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_closure_object_target(identity, target); + Ok(object) +} + +/// Materializes `Closure::bind()` from a closure object and a persistent receiver. +pub(super) fn eval_closure_bind_static( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let (target, bound_this, bound_scope, rebinds_function_scope) = + eval_closure_bind_static_args(evaluated_args, context, values)?; + eval_closure_bind_target( + target, + bound_this, + bound_scope, + rebinds_function_scope, + context, + values, + ) +} + +/// Binds static `Closure::bind()` arguments to their PHP parameter slots. +pub(super) fn eval_closure_bind_static_args( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result< + ( + EvalClosureObjectTarget, + Option, + Option, + bool, + ), + EvalStatus, +> { + let bound = eval_closure_bind_args( + &["closure", "newThis", "newScope"], + 2, + evaluated_args, + )?; + let closure = required_closure_bind_arg(&bound, 0)?; + let new_this = required_closure_bind_arg(&bound, 1)?; + let target = eval_closure_target_arg(closure.value, context, values)?; + let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; + let (bound_scope, rebinds_function_scope) = + eval_closure_bind_scope_arg(bound.get(2), bound_this, context, values)?; + Ok((target, bound_this, bound_scope, rebinds_function_scope)) +} + +/// Binds `Closure::bindTo()` arguments to their PHP parameter slots. +pub(super) fn eval_closure_bind_to_args( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, Option, bool), EvalStatus> { + let bound = eval_closure_bind_args(&["newThis", "newScope"], 1, evaluated_args)?; + let new_this = required_closure_bind_arg(&bound, 0)?; + let bound_this = eval_closure_bind_receiver_arg(new_this.value, values)?; + let (bound_scope, rebinds_function_scope) = + eval_closure_bind_scope_arg(bound.get(1), bound_this, context, values)?; + Ok((bound_this, bound_scope, rebinds_function_scope)) +} + +/// Binds positional and named Closure binding arguments while accepting optional scope. +pub(super) fn eval_closure_bind_args( + params: &[&str], + required_count: usize, + evaluated_args: Vec, +) -> Result>, EvalStatus> { + let mut bound_args = vec![None; params.len()]; + let mut next_positional = 0; + let mut saw_named = false; + + for arg in evaluated_args { + if let Some(name) = arg.name.as_deref() { + saw_named = true; + let Some(index) = params + .iter() + .position(|param| param.eq_ignore_ascii_case(name)) + else { + return Err(EvalStatus::RuntimeFatal); + }; + if bound_args[index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[index] = Some(arg); + continue; + } + + if saw_named || next_positional >= params.len() || bound_args[next_positional].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + bound_args[next_positional] = Some(arg); + next_positional += 1; + } + + if bound_args + .iter() + .take(required_count) + .any(Option::is_none) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(bound_args) +} + +/// Returns one required Closure binding argument. +pub(super) fn required_closure_bind_arg( + bound_args: &[Option], + index: usize, +) -> Result<&EvaluatedCallArg, EvalStatus> { + bound_args + .get(index) + .and_then(Option::as_ref) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Extracts a stored eval Closure object target from a runtime object. +pub(super) fn eval_closure_target_arg( + closure: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let identity = values.object_identity(closure)?; + context + .closure_object_target(identity) + .cloned() + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Converts the `newThis` binding argument to an optional object receiver. +pub(super) fn eval_closure_bind_receiver_arg( + new_this: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if values.is_null(new_this)? { + return Ok(None); + } + if values.type_tag(new_this)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + Ok(Some(new_this)) +} + +/// Converts `newScope` into class scope plus whether function scope was rebound. +pub(super) fn eval_closure_bind_scope_arg( + new_scope: Option<&Option>, + bound_this: Option, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(Option, bool), EvalStatus> { + let Some(new_scope) = new_scope.and_then(Option::as_ref) else { + return Ok((None, false)); + }; + if values.is_null(new_scope.value)? { + return Ok((None, false)); + } + if values.type_tag(new_scope.value)? == EVAL_TAG_OBJECT { + return eval_closure_bound_object_class_name(new_scope.value, context, values) + .map(|scope| (Some(scope), true)); + } + let bytes = values.string_bytes(new_scope.value)?; + let scope = String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal)?; + if scope.eq_ignore_ascii_case("static") { + let Some(bound_this) = bound_this else { + return Ok((None, false)); + }; + return eval_closure_bound_object_class_name(bound_this, context, values) + .map(|scope| (Some(scope), false)); + } + Ok(( + Some( + context + .resolve_class_name(&scope) + .unwrap_or_else(|| scope.trim_start_matches('\\').to_string()), + ), + true, + )) +} + +/// Creates a new Closure object with persistent binding metadata when supported. +pub(super) fn eval_closure_bind_target( + target: EvalClosureObjectTarget, + bound_this: Option, + bound_scope: Option, + rebinds_function_scope: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(bound_this) = bound_this else { + return eval_closure_unbind_target( + target, + bound_scope, + rebinds_function_scope, + context, + values, + ); + }; + match target { + EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { + let Some(closure) = context.closure(&name) else { + if eval_function_probe_exists(context, &name) { + if rebinds_function_scope { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + } + return eval_closure_object_from_target( + EvalClosureObjectTarget::BoundNamed { + name, + bound_this: Some(bound_this), + bound_scope, + }, + context, + values, + ); + } + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + }; + if closure.is_static() { + return eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ); + } + eval_closure_object_from_target( + EvalClosureObjectTarget::BoundNamed { + name, + bound_this: Some(bound_this), + bound_scope, + }, + context, + values, + ) + } + EvalClosureObjectTarget::InvokableObject { object } => { + if !eval_closure_bind_bound_class_matches_method( + object, "__invoke", bound_this, context, values, + )? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ); + } + eval_closure_object_from_target( + EvalClosureObjectTarget::InvokableObject { object: bound_this }, + context, + values, + ) + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + native_class, + bridge_scope, + .. + } => { + if !eval_closure_bind_bound_class_matches_method( + object, &method, bound_this, context, values, + )? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ); + } + let called_class = Some(eval_closure_bound_object_class_name( + bound_this, context, values, + )?); + eval_closure_object_from_target( + EvalClosureObjectTarget::ObjectMethod { + object: bound_this, + method, + called_class, + native_class, + bridge_scope, + }, + context, + values, + ) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ), + } +} + +/// Creates an unbound Closure object for targets that can drop `$this`. +pub(super) fn eval_closure_unbind_target( + target: EvalClosureObjectTarget, + bound_scope: Option, + rebinds_function_scope: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match target { + EvalClosureObjectTarget::Named(name) | EvalClosureObjectTarget::BoundNamed { name, .. } => { + if rebinds_function_scope + && context.closure(&name).is_none() + && eval_function_probe_exists(context, &name) + { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ); + } + let target = match bound_scope { + Some(bound_scope) => EvalClosureObjectTarget::BoundNamed { + name, + bound_this: None, + bound_scope: Some(bound_scope), + }, + None => EvalClosureObjectTarget::Named(name), + }; + eval_closure_object_from_target(target, context, values) + } + EvalClosureObjectTarget::InvokableObject { .. } + | EvalClosureObjectTarget::ObjectMethod { .. } => { + eval_closure_call_warning_null("Cannot unbind $this of method", values) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot unbind $this of static method", + values, + ), + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/dispatch.rs b/crates/elephc-magician/src/interpreter/statements/dispatch.rs new file mode 100644 index 0000000000..572e7e6a3c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/dispatch.rs @@ -0,0 +1,617 @@ +//! Purpose: +//! Dispatches each EvalIR statement variant to its focused execution helper and +//! propagates structured loop, throw, and return control flow. +//! +//! Called from: +//! - `crate::interpreter::execute_program_outcome_with_context()`. +//! - Dynamic eval function and method execution. +//! +//! Key details: +//! - The exhaustive match remains centralized so every new `EvalStmt` variant +//! must explicitly define its runtime control-flow behavior. + +use super::*; + +/// Executes statements in source order and propagates the first eval `return`. +pub(in crate::interpreter) fn execute_statements( + statements: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + for stmt in statements { + match execute_stmt(stmt, context, scope, values)? { + EvalControl::None => {} + control => return Ok(control), + } + } + Ok(EvalControl::None) +} + +/// Executes one statement and returns `Some` only for eval `return`. +pub(in crate::interpreter) fn execute_stmt( + stmt: &EvalStmt, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match stmt { + EvalStmt::ArrayAppendVar { name, value } => { + eval_array_append_var_stmt(name, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::ArraySetVar { name, index, value } => { + eval_array_set_var_stmt(name, index, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::Break => Ok(EvalControl::Break), + EvalStmt::Continue => Ok(EvalControl::Continue), + EvalStmt::DoWhile { body, condition } => { + execute_do_while_stmt(body, condition, context, scope, values) + } + EvalStmt::Echo(expr) => { + let value = eval_expr(expr, context, scope, values)?; + let value = eval_string_context_value(value, context, values)?; + values.echo(value)?; + Ok(EvalControl::None) + } + EvalStmt::For { + init, + condition, + update, + body, + } => execute_for_stmt( + init, + condition.as_ref(), + update, + body, + context, + scope, + values, + ), + EvalStmt::ClassDecl(class) => { + execute_class_decl_stmt(class, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::EnumDecl(enum_decl) => { + execute_enum_decl_stmt(enum_decl, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::InterfaceDecl(interface) => { + execute_interface_decl_stmt(interface, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::TraitDecl(trait_decl) => { + execute_trait_decl_stmt(trait_decl, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::Foreach { + array, + key_name, + value_name, + body, + } => execute_foreach_stmt( + array, + key_name.as_deref(), + value_name, + body, + context, + scope, + values, + ), + EvalStmt::FunctionDecl { + name, + source_location, + attributes, + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type, + body, + } => { + let key = name.to_ascii_lowercase(); + let mut function = EvalFunction::new(name.clone(), params.clone(), body.clone()) + .with_attributes(attributes.clone()) + .with_parameter_attributes(parameter_attributes.clone()) + .with_parameter_types(parameter_types.clone()) + .with_parameter_defaults(parameter_defaults.clone()) + .with_parameter_by_ref_flags(parameter_is_by_ref.clone()) + .with_parameter_variadic_flags(parameter_is_variadic.clone()) + .with_return_type(return_type.clone()); + if let Some(source_location) = source_location { + function = function.with_source_location(*source_location); + } + context + .define_function(key, function) + .map_err(|_| EvalStatus::RuntimeFatal)?; + Ok(EvalControl::None) + } + EvalStmt::Global { vars } => { + execute_global_stmt(vars, context, scope)?; + Ok(EvalControl::None) + } + EvalStmt::If { + condition, + then_branch, + else_branch, + } => { + let condition = eval_expr(condition, context, scope, values)?; + if values.truthy(condition)? { + execute_statements(then_branch, context, scope, values) + } else { + execute_statements(else_branch, context, scope, values) + } + } + EvalStmt::Return(Some(expr)) => Ok(EvalControl::Return(eval_expr( + expr, context, scope, values, + )?)), + EvalStmt::Return(None) => Ok(EvalControl::ReturnVoid), + EvalStmt::ReferenceAssign { target, source } => { + for replaced in set_reference_alias(context, scope, target, source, values)? { + values.release(replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::PropertyReferenceBind { + object, + property, + source, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_reference_bind_result(object, property, source, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyReferenceBind { + object, + property, + source, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_reference_bind_result( + object, + &property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertySet { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_property_set_result(object, &property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyArrayAppend { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_array_append_result(object, &property, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyArraySet { + object, + property, + index, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_array_set_result( + object, &property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyCompoundAssign { + object, + property, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let current = eval_property_get_result(object, &property, context, values)?; + let right = eval_expr(value, context, scope, values)?; + let value = eval_binary_result(*op, current, right, context, values)?; + eval_property_set_result(object, &property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicPropertyIncDec { + object, + property, + increment, + } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_inc_dec_result(object, &property, *increment, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::StaticVar { name, init } => { + execute_static_var_stmt(name, init, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertySet { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_property_set_result(object, property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertyArrayAppend { + object, + property, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_array_append_result(object, property, value, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertyArraySet { + object, + property, + index, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_array_set_result( + object, property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::PropertyCompoundAssign { + object, + property, + op, + value, + } => { + let object = eval_expr(object, context, scope, values)?; + let current = eval_property_get_result(object, property, context, values)?; + let right = eval_expr(value, context, scope, values)?; + let value = eval_binary_result(*op, current, right, context, values)?; + eval_property_set_result(object, property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::PropertyIncDec { + object, + property, + increment, + } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_inc_dec_result(object, property, *increment, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertySet { + class_name, + property, + value, + } => { + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(class_name, property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyReferenceBind { + class_name, + property, + source, + } => { + eval_static_property_reference_bind_result( + class_name, property, source, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyArrayAppend { + class_name, + property, + value, + } => { + eval_static_property_array_append_result( + class_name, property, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyArraySet { + class_name, + property, + index, + op, + value, + } => { + eval_static_property_array_set_result( + class_name, property, index, *op, value, context, scope, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StaticPropertyIncDec { + class_name, + property, + increment, + } => { + eval_static_property_inc_dec_result( + class_name, property, *increment, context, values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(&class_name, property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyReferenceBind { + class_name, + property, + source, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_reference_bind_result( + &class_name, + property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_array_append_result( + &class_name, + property, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyArraySet { + class_name, + property, + index, + op, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_array_set_result( + &class_name, + property, + index, + *op, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyIncDec { + class_name, + property, + increment, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_inc_dec_result( + &class_name, + property, + *increment, + context, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + let value = eval_expr(value, context, scope, values)?; + eval_static_property_set_result(&class_name, &property, value, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name, + property, + source, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_reference_bind_result( + &class_name, + &property, + source, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name, + property, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_array_append_result( + &class_name, + &property, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameArraySet { + class_name, + property, + index, + op, + value, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_array_set_result( + &class_name, + &property, + index, + *op, + value, + context, + scope, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name, + property, + increment, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_inc_dec_result( + &class_name, + &property, + *increment, + context, + values, + )?; + Ok(EvalControl::None) + } + EvalStmt::StoreVar { name, value } => { + let value = eval_expr(value, context, scope, values)?; + for replaced in set_scope_cell( + context, + scope, + name.clone(), + value, + ScopeCellOwnership::Owned, + )? { + eval_release_value(context, values, replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::Switch { expr, cases } => { + execute_switch_stmt(expr, cases, context, scope, values) + } + EvalStmt::Throw(expr) => { + let thrown = eval_expr(expr, context, scope, values)?; + if values.type_tag(thrown)? != EVAL_TAG_OBJECT { + return Err(EvalStatus::RuntimeFatal); + } + Ok(EvalControl::Throw(thrown)) + } + EvalStmt::Try { + body, + catches, + finally_body, + } => execute_try_stmt(body, catches, finally_body, context, scope, values), + EvalStmt::UnsetArrayElement { array, index } => { + eval_array_unset_element_stmt(array, index, context, scope, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetProperty { object, property } => { + let object = eval_expr(object, context, scope, values)?; + eval_property_unset_result(object, property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetDynamicProperty { object, property } => { + let object = eval_expr(object, context, scope, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_property_unset_result(object, &property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetStaticProperty { + class_name, + property, + } => { + eval_static_property_unset_result(class_name, property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetDynamicStaticProperty { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + eval_static_property_unset_result(&class_name, property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetDynamicStaticPropertyName { + class_name, + property, + } => { + let class_name = eval_expr(class_name, context, scope, values)?; + let class_name = eval_dynamic_class_name(class_name, context, values)?; + let property = eval_dynamic_member_name(property, context, scope, values)?; + eval_static_property_unset_result(&class_name, &property, context, values)?; + Ok(EvalControl::None) + } + EvalStmt::UnsetVar { name } => { + if let Some(replaced) = unset_scope_cell(scope, name.clone()) { + eval_release_value(context, values, replaced)?; + } + Ok(EvalControl::None) + } + EvalStmt::While { condition, body } => { + while { + let condition = eval_expr(condition, context, scope, values)?; + values.truthy(condition)? + } { + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) + } + EvalStmt::Expr(expr) => { + let result = eval_expr(expr, context, scope, values)?; + eval_release_value(context, values, result)?; + Ok(EvalControl::None) + } + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/dynamic_method_execution.rs b/crates/elephc-magician/src/interpreter/statements/dynamic_method_execution.rs new file mode 100644 index 0000000000..1d3ec07aef --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/dynamic_method_execution.rs @@ -0,0 +1,283 @@ +//! Purpose: +//! Executes eval-declared instance and static methods from prepared argument values. +//! +//! Called from: +//! - Method dispatch and callable invocation after target resolution. +//! +//! Key details: +//! - Called-class scope, reference modes, positional extraction, and writeback stay paired. + +use super::*; + +/// Executes one eval-declared class method with `$this` bound in method scope. +pub(in crate::interpreter) fn eval_dynamic_method_with_values( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_method_with_values_and_ref_flags( + class_name, + called_class_name, + method, + object, + method.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Executes one eval-declared class method with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_flags( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_method_with_values_and_ref_mode( + class_name, + called_class_name, + method, + object, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Executes one eval-declared class method with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_method_with_values_and_ref_mode( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + object: RuntimeCellHandle, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(called_class_name.to_string()); + context.push_method_magic_scope(class_name, method); + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( + method.params(), + method.parameter_types(), + method.parameter_defaults(), + parameter_is_by_ref, + method.parameter_is_variadic(), + evaluated_args, + by_ref_mode, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return Err(status); + } + }; + let mut method_scope = ElephcEvalScope::new(); + method_scope.set("this", object, ScopeCellOwnership::Borrowed); + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); + bind_method_scope_args( + &mut method_scope, + method.params(), + &scope_parameter_is_by_ref, + &evaluated_args, + ); + let result = execute_statements(method.body(), context, &mut method_scope, values); + let persist_result = persist_static_locals( + context, + &qualified_method_name, + &static_names, + &method_scope, + values, + ); + let writeback_result = write_back_method_ref_args( + method.params(), + &evaluated_args, + &method_scope, + context, + values, + ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + method.return_type(), + Some(class_name), + Some(called_class_name), + control, + context, + values, + ), + }; + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return_result +} + +/// Executes one eval-declared static class method without binding `$this`. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_static_method_with_values_and_ref_flags( + class_name, + called_class_name, + method, + method.parameter_is_by_ref(), + evaluated_args, + context, + values, + ) +} + +/// Executes one eval-declared static method with caller-selected by-ref binding flags. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_flags( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_dynamic_static_method_with_values_and_ref_mode( + class_name, + called_class_name, + method, + parameter_is_by_ref, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Executes one eval-declared static method with caller-selected by-ref mode. +pub(in crate::interpreter) fn eval_dynamic_static_method_with_values_and_ref_mode( + class_name: &str, + called_class_name: &str, + method: &EvalClassMethod, + parameter_is_by_ref: &[bool], + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let qualified_method_name = + format!("{}::{}", class_name.trim_start_matches('\\'), method.name()); + let static_names = static_var_names(method.body()); + context.push_function(qualified_method_name.clone()); + context.push_class_scope(class_name.to_string()); + context.push_called_class_scope(called_class_name.to_string()); + context.push_method_magic_scope(class_name, method); + let evaluated_args = match bind_evaluated_method_args_with_ref_mode( + method.params(), + method.parameter_types(), + method.parameter_defaults(), + parameter_is_by_ref, + method.parameter_is_variadic(), + evaluated_args, + by_ref_mode, + context, + values, + ) { + Ok(args) => args, + Err(status) => { + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return Err(status); + } + }; + let mut method_scope = ElephcEvalScope::new(); + let scope_parameter_is_by_ref = + method_scope_parameter_ref_flags(parameter_is_by_ref, &evaluated_args, by_ref_mode); + bind_method_scope_args( + &mut method_scope, + method.params(), + &scope_parameter_is_by_ref, + &evaluated_args, + ); + let result = execute_statements(method.body(), context, &mut method_scope, values); + let persist_result = persist_static_locals( + context, + &qualified_method_name, + &static_names, + &method_scope, + values, + ); + let writeback_result = write_back_method_ref_args( + method.params(), + &evaluated_args, + &method_scope, + context, + values, + ); + let return_result = match (persist_result, writeback_result, result) { + (Err(status), _, _) | (_, Err(status), _) | (_, _, Err(status)) => Err(status), + (Ok(()), Ok(()), Ok(control)) => eval_declared_return_control_value( + method.return_type(), + Some(class_name), + Some(called_class_name), + control, + context, + values, + ), + }; + context.pop_magic_scope(); + context.pop_called_class_scope(); + context.pop_class_scope(); + context.pop_function(); + return_result +} + +/// Wraps positional method arguments into the shared dynamic-call binding shape. +pub(in crate::interpreter) fn positional_args( + args: Vec, +) -> Vec { + args.into_iter() + .map(|value| EvaluatedCallArg { + name: None, + value, + ref_target: None, + }) + .collect() +} + +/// Extracts positional runtime values and rejects named args before runtime method dispatch. +pub(in crate::interpreter) fn positional_evaluated_arg_values( + args: Vec, +) -> Result, EvalStatus> { + if args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(args.into_iter().map(|arg| arg.value).collect()) +} diff --git a/crates/elephc-magician/src/interpreter/statements/enum_declarations.rs b/crates/elephc-magician/src/interpreter/statements/enum_declarations.rs new file mode 100644 index 0000000000..ef167c5a73 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/enum_declarations.rs @@ -0,0 +1,392 @@ +//! Purpose: +//! Validates, registers, and initializes eval-declared enums. +//! +//! Called from: +//! - Statement dispatch for enum declarations. +//! +//! Key details: +//! - Trait expansion, synthetic methods, backing values, cases, and static defaults stay ordered. + +use super::*; + +/// Registers an eval-declared enum and materializes its singleton cases. +pub(in crate::interpreter) fn execute_enum_decl_stmt( + enum_decl: &EvalEnum, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = enum_decl.name().trim_start_matches('\\'); + if context.has_enum(name) + || context.has_class(name) + || context.has_interface(name) + || context.has_trait(name) + || values.enum_exists(name)? + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.trait_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_enum_direct_method_declarations(enum_decl)?; + let enum_decl = expand_eval_enum_traits(enum_decl, context)?; + let enum_decl = &enum_decl; + validate_eval_enum_decl(enum_decl, context, values)?; + if context.define_enum(enum_decl.clone()) { + initialize_eval_declared_constants( + enum_decl.name(), + enum_decl.constants(), + context, + scope, + values, + )?; + initialize_eval_enum_cases(enum_decl, context, scope, values) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Expands eval trait uses into enum metadata while rejecting imported properties. +pub(super) fn expand_eval_enum_traits( + enum_decl: &EvalEnum, + context: &ElephcEvalContext, +) -> Result { + if enum_decl.traits().is_empty() { + return Ok(enum_decl.clone()); + } + let enum_class = enum_decl.as_class_metadata(); + validate_eval_trait_adaptations(&enum_class, context)?; + let mut enum_method_names = class_method_name_set(&enum_class); + insert_eval_enum_synthetic_method_names(enum_decl, &mut enum_method_names); + let mut trait_method_names = std::collections::HashSet::new(); + let mut trait_properties = std::collections::HashMap::new(); + let mut trait_constants = std::collections::HashMap::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for trait_name in enum_decl.traits() { + let Some(trait_decl) = context.trait_decl(trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_constants( + trait_decl, + enum_decl.constants(), + &mut trait_constants, + &mut constants, + )?; + append_eval_trait_properties( + trait_decl, + &[], + &mut trait_properties, + &mut properties, + )?; + append_eval_trait_methods( + trait_decl, + enum_decl.trait_adaptations(), + &enum_method_names, + &mut trait_method_names, + &mut methods, + )?; + } + if !properties.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + constants.extend(enum_decl.constants().iter().cloned()); + methods.extend(enum_decl.methods().iter().cloned()); + let mut expanded = EvalEnum::with_members_traits_adaptations( + enum_decl.name().to_string(), + enum_decl.backing_type(), + enum_decl.interfaces().to_vec(), + enum_decl.cases().to_vec(), + constants, + methods, + enum_decl.traits().to_vec(), + enum_decl.trait_adaptations().to_vec(), + ) + .with_attributes(enum_decl.attributes().to_vec()); + if let Some(source_location) = enum_decl.source_location() { + expanded = expanded.with_source_location(source_location); + } + Ok(expanded) +} + +/// Adds PHP's enum-provided method names to the set that hides trait imports. +pub(super) fn insert_eval_enum_synthetic_method_names( + enum_decl: &EvalEnum, + method_names: &mut std::collections::HashSet, +) { + method_names.insert(String::from("cases")); + if enum_decl.backing_type().is_some() { + method_names.insert(String::from("from")); + method_names.insert(String::from("tryfrom")); + } +} + +/// Validates enum metadata before it is inserted into the dynamic context. +pub(super) fn validate_eval_enum_decl( + enum_decl: &EvalEnum, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + validate_eval_enum_attribute_targets(enum_decl)?; + validate_eval_declared_constants(enum_decl.constants())?; + validate_eval_enum_case_declarations(enum_decl)?; + validate_eval_enum_forbidden_magic_methods(enum_decl)?; + let enum_class = enum_decl.as_class_metadata(); + validate_eval_class_modifiers(&enum_class, context, values)?; + validate_eval_enum_interfaces(enum_decl, &enum_class, context, values)?; + validate_declared_class_builtin_interface_members(&enum_class, context)?; + validate_declared_class_aot_interface_members(&enum_class, context, values)?; + validate_concrete_class_builtin_interface_requirements(&enum_class, context)?; + validate_concrete_class_aot_interface_requirements(&enum_class, context, values)?; + validate_concrete_class_requirements(&enum_class, context) +} + +/// Validates PHP's special enum interface rules for one eval enum declaration. +pub(super) fn validate_eval_enum_interfaces( + enum_decl: &EvalEnum, + enum_class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for interface in enum_decl.interfaces() { + if eval_builtin_enum_interface_name(interface) { + return Err(EvalStatus::RuntimeFatal); + } + if !context.has_interface(interface) && !eval_runtime_interface_exists(interface, values)? { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_eval_class_does_not_implement_throwable_interfaces(enum_class, context)?; + if enum_decl.backing_type().is_none() + && pending_class_interface_names(enum_class, context) + .iter() + .any(|interface| eval_builtin_backed_enum_interface_name(interface)) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Validates enum case names and pure/backed declaration shape. +pub(super) fn validate_eval_enum_case_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + let mut case_names = std::collections::HashSet::new(); + let constant_names = enum_decl + .constants() + .iter() + .map(|constant| constant.name().to_string()) + .collect::>(); + for case in enum_decl.cases() { + validate_eval_non_method_attribute_targets(case.attributes())?; + if !case_names.insert(case.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + if constant_names.contains(case.name()) { + return Err(EvalStatus::RuntimeFatal); + } + match (enum_decl.backing_type(), case.value()) { + (None, None) | (Some(_), Some(_)) => {} + (None, Some(_)) | (Some(_), None) => return Err(EvalStatus::RuntimeFatal), + } + } + Ok(()) +} + +/// Validates direct enum methods that PHP reserves on enum declarations. +pub(super) fn validate_eval_enum_direct_method_declarations(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + for method in enum_decl.methods() { + if method.name().eq_ignore_ascii_case("cases") { + return Err(EvalStatus::RuntimeFatal); + } + if enum_decl.backing_type().is_some() + && (method.name().eq_ignore_ascii_case("from") + || method.name().eq_ignore_ascii_case("tryFrom")) + { + return Err(EvalStatus::RuntimeFatal); + } + if is_forbidden_eval_enum_magic_method(method.name()) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates enum methods, including trait imports, that PHP forbids by magic name. +pub(super) fn validate_eval_enum_forbidden_magic_methods(enum_decl: &EvalEnum) -> Result<(), EvalStatus> { + for method in enum_decl.methods() { + if is_forbidden_eval_enum_magic_method(method.name()) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns whether PHP forbids this magic method name inside enum declarations. +pub(super) fn is_forbidden_eval_enum_magic_method(name: &str) -> bool { + [ + "__construct", + "__destruct", + "__clone", + "__get", + "__set", + "__isset", + "__unset", + "__sleep", + "__wakeup", + "__serialize", + "__unserialize", + "__toString", + "__debugInfo", + "__set_state", + ] + .iter() + .any(|method| name.eq_ignore_ascii_case(method)) +} + +/// Initializes enum singleton case objects for a newly declared eval enum. +pub(super) fn initialize_eval_enum_cases( + enum_decl: &EvalEnum, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut backing_values = Vec::new(); + for case in enum_decl.cases() { + let backing_value = if let Some(value_expr) = case.value() { + let value = eval_expr(value_expr, context, scope, values)?; + validate_eval_enum_backing_value(enum_decl.backing_type(), value, values)?; + for existing in &backing_values { + let equal = values.compare(EvalBinOp::StrictEq, value, *existing)?; + if values.truthy(equal)? { + return Err(EvalStatus::RuntimeFatal); + } + } + backing_values.push(value); + Some(value) + } else { + None + }; + initialize_eval_enum_case(enum_decl, case, backing_value, context, values)?; + } + Ok(()) +} + +/// Validates that one evaluated enum backing value matches the declared backing type. +pub(super) fn validate_eval_enum_backing_value( + backing_type: Option, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(backing_type) = backing_type else { + return Err(EvalStatus::RuntimeFatal); + }; + let tag = values.type_tag(value)?; + match backing_type { + EvalEnumBackingType::Int if tag == EVAL_TAG_INT => Ok(()), + EvalEnumBackingType::String if tag == EVAL_TAG_STRING => Ok(()), + EvalEnumBackingType::Int | EvalEnumBackingType::String => Err(EvalStatus::RuntimeFatal), + } +} + +/// Creates and stores one enum case singleton object. +pub(super) fn initialize_eval_enum_case( + enum_decl: &EvalEnum, + case: &EvalEnumCase, + backing_value: Option, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, enum_decl.name()); + let name = values.string(case.name())?; + values.property_set(object, "name", name)?; + if let Some(value) = backing_value { + values.property_set(object, "value", value)?; + if let Some(replaced) = context.set_enum_case_value(enum_decl.name(), case.name(), value) { + values.release(replaced)?; + } + } + if let Some(replaced) = context.set_enum_case(enum_decl.name(), case.name(), object) { + values.release(replaced)?; + } + Ok(()) +} + +/// Initializes class-like constant cells for a newly declared eval class-like. +pub(super) fn initialize_eval_declared_constants( + owner_name: &str, + constants: &[EvalClassConstant], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for constant in constants { + let value = eval_class_like_member_default( + owner_name, + constant.trait_origin(), + constant.value(), + context, + scope, + values, + )?; + if let Some(replaced) = context.set_class_constant_cell(owner_name, constant.name(), value) + { + values.release(replaced)?; + } + } + Ok(()) +} + +/// Evaluates a class-like constant or property initializer with PHP magic scope. +pub(super) fn eval_class_like_member_default( + owner_name: &str, + trait_origin: Option<&str>, + default: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let trait_name = trait_origin.or_else(|| context.has_trait(owner_name).then_some(owner_name)); + context.push_class_like_member_magic_scope(owner_name, trait_name); + let result = eval_expr(default, context, scope, values); + context.pop_magic_scope(); + result +} + +/// Initializes static property cells for a newly declared eval class. +pub(super) fn initialize_eval_static_properties( + class: &EvalClass, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for property in class + .properties() + .iter() + .filter(|property| property.is_static()) + { + let value = if let Some(default) = property.default() { + Some(eval_class_like_member_default( + class.name(), + property.trait_origin(), + default, + context, + scope, + values, + )?) + } else if property.property_type().is_none() { + Some(values.null()?) + } else { + None + }; + if let Some(value) = value { + if let Some(replaced) = + context.set_static_property(class.name(), property.name(), value) + { + values.release(replaced)?; + } + } + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/statements/exceptions.rs b/crates/elephc-magician/src/interpreter/statements/exceptions.rs new file mode 100644 index 0000000000..0a225b5e82 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/exceptions.rs @@ -0,0 +1,147 @@ +//! Purpose: +//! Executes try, catch, and finally control-flow semantics. +//! +//! Called from: +//! - Statement dispatch for EvalIR exception handlers. +//! +//! Key details: +//! - Finally control overrides and throwable interface matching remain explicit. + +use super::*; + +/// Executes an eval `try` body and handles supported `catch` clauses. +pub(in crate::interpreter) fn execute_try_stmt( + body: &[EvalStmt], + catches: &[EvalCatch], + finally_body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let control = match execute_statements(body, context, scope, values) { + Ok(EvalControl::Throw(thrown)) => { + execute_matching_catch(thrown, catches, context, scope, values)? + } + Err(EvalStatus::UncaughtThrowable) => { + let Some(thrown) = context.take_pending_throw() else { + return Err(EvalStatus::UncaughtThrowable); + }; + execute_matching_catch(thrown, catches, context, scope, values)? + } + Ok(control) => control, + Err(status) => return Err(status), + }; + if finally_body.is_empty() { + return Ok(control); + } + match execute_statements(finally_body, context, scope, values) { + Ok(EvalControl::None) => Ok(control), + Ok(finally_control) => { + release_overridden_control(control, values)?; + Ok(finally_control) + } + Err(status) => { + release_overridden_control(control, values)?; + Err(status) + } + } +} + +/// Releases a pending control-flow value when `finally` replaces that action. +pub(in crate::interpreter) fn release_overridden_control( + control: EvalControl, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match control { + EvalControl::Return(value) | EvalControl::Throw(value) => values.release(value), + EvalControl::None + | EvalControl::ReturnVoid + | EvalControl::Break + | EvalControl::Continue => Ok(()), + } +} + +/// Executes the first supported catch clause for a thrown eval object. +pub(in crate::interpreter) fn execute_matching_catch( + thrown: RuntimeCellHandle, + catches: &[EvalCatch], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut matched = None; + for catch in catches { + if catch_types_match_thrown(thrown, &catch.class_names, context, values)? { + matched = Some(catch); + break; + } + } + let Some(catch) = matched else { + return Ok(EvalControl::Throw(thrown)); + }; + if let Some(var_name) = &catch.var_name { + for replaced in set_scope_cell( + context, + scope, + var_name.clone(), + thrown, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } else { + values.release(thrown)?; + } + execute_statements(&catch.body, context, scope, values) +} + +/// Returns true when any type in one catch clause accepts the thrown object. +pub(in crate::interpreter) fn catch_types_match_thrown( + thrown: RuntimeCellHandle, + class_names: &[String], + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + for class_name in class_names { + let class_name = class_name.trim_start_matches('\\'); + if class_name.eq_ignore_ascii_case("Throwable") { + return Ok(true); + } + if let Some(matched) = dynamic_object_is_a(thrown, class_name, false, context, values)? { + if matched { + return Ok(true); + } + continue; + } + if values.object_is_a(thrown, class_name, false)? { + return Ok(true); + } + } + Ok(false) +} + +/// Returns whether one name is a PHP native enum interface. +pub(in crate::interpreter) fn eval_builtin_enum_interface_name(name: &str) -> bool { + let name = name.trim_start_matches('\\'); + name.eq_ignore_ascii_case("UnitEnum") || name.eq_ignore_ascii_case("BackedEnum") +} + +/// Returns whether one name is PHP's native backed-enum interface. +pub(super) fn eval_builtin_backed_enum_interface_name(name: &str) -> bool { + name.trim_start_matches('\\') + .eq_ignore_ascii_case("BackedEnum") +} + +/// Returns whether one name is PHP's native Throwable interface. +pub(super) fn eval_builtin_throwable_interface_name(name: &str) -> bool { + name.trim_start_matches('\\') + .eq_ignore_ascii_case("Throwable") +} + +/// Returns whether one name is visible as a native/runtime interface to eval. +pub(in crate::interpreter) fn eval_runtime_interface_exists( + name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_builtin_enum_interface_name(name) || values.interface_exists(name)?) +} diff --git a/crates/elephc-magician/src/interpreter/statements/instance_property_access.rs b/crates/elephc-magician/src/interpreter/statements/instance_property_access.rs new file mode 100644 index 0000000000..d8c375f045 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/instance_property_access.rs @@ -0,0 +1,809 @@ +//! Purpose: +//! Reads, writes, binds, tests, and unsets eval instance properties. +//! +//! Called from: +//! - Expression and statement dispatch for object property operations. +//! +//! Key details: +//! - Declared, dynamic, magic, hooked, and reference-backed properties share visibility checks. + +use super::*; + +/// Reads one object property while enforcing eval-declared member visibility. +pub(in crate::interpreter) fn eval_property_get_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + return values.property_get(object, property_name); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_runtime_object_class_name(object, values)?; + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_property_access_metadata(&class_name, property_name, values)? + { + if !is_static && validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + } + return values.property_get(object, property_name); + }; + let object_class_name = class.name().to_string(); + let mut storage_property_name = property_name.to_string(); + let mut declared_property_found = false; + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + declared_property_found = true; + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + if let Some(result) = + eval_magic_property_get(object, &object_class_name, property_name, context, values)? + { + return Ok(result); + } + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); + } + storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); + if property.has_get_hook() + && !current_eval_property_hook_is( + &declaring_class, + property.name(), + &property_hook_get_method(property.name()), + context, + ) + { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_get_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + return eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + Vec::new(), + context, + values, + ); + } + if property.property_type().is_some() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + return eval_throw_uninitialized_property_error( + &declaring_class, + property.name(), + context, + values, + ); + } + } + if !declared_property_found + && eval_object_public_property_exists(object, property_name, values)? + { + return values.property_get(object, property_name); + } + if !declared_property_found { + if let Some((declaring_class, visibility, _, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if let Some(result) = eval_magic_property_get( + object, + &object_class_name, + property_name, + context, + values, + )? { + return Ok(result); + } + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + return eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_get(object, property_name) + }); + } + } + } + if !declared_property_found { + if let Some(result) = + eval_magic_property_get(object, &object_class_name, property_name, context, values)? + { + return Ok(result); + } + } + if let Some(target) = context + .dynamic_property_alias(identity, &storage_property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + values.property_get(object, &storage_property_name) +} + +/// Writes one object property while enforcing eval-declared member visibility. +pub(in crate::interpreter) fn eval_property_set_result( + object: RuntimeCellHandle, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return values.property_set(object, property_name, value); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = eval_runtime_object_class_name(object, values)?; + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_property_access_metadata(&class_name, property_name, values)? + { + if !is_static + && validate_eval_member_access(&declaring_class, write_visibility, context).is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + } + return values.property_set(object, property_name, value); + }; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + let class_is_readonly = class.is_readonly_class(); + let mut storage_property_name = property_name.to_string(); + let mut declared_property_found = false; + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + declared_property_found = true; + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + if eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? { + return Ok(()); + } + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); + } + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_write_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } + storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); + if property.has_set_hook() { + if !current_eval_property_hook_is( + &declaring_class, + property.name(), + &property_hook_set_method(property.name()), + context, + ) { + let (hook_class, hook_method) = context + .class_method( + &object_class_name, + &property_hook_set_method(property.name()), + ) + .ok_or(EvalStatus::RuntimeFatal)?; + let hook_result = eval_dynamic_method_with_values( + &hook_class, + &object_class_name, + &hook_method, + object, + vec![EvaluatedCallArg { + name: None, + value, + ref_target: None, + }], + context, + values, + )?; + values.release(hook_result)?; + return Ok(()); + } + } else if property.has_get_hook() { + return eval_throw_property_hook_readonly_error( + &declaring_class, + property.name(), + context, + values, + ); + } + } + if !declared_property_found { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + if eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? { + return Ok(()); + } + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + return eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_set(object, property_name, value) + }); + } + } + } + if !declared_property_found + && eval_magic_property_set( + object, + &object_class_name, + property_name, + value, + context, + values, + )? + { + return Ok(()); + } + if !declared_property_found && class_is_readonly { + return eval_throw_dynamic_property_creation_error( + &object_class_name, + property_name, + context, + values, + ); + } + if let Some(target) = context + .dynamic_property_alias(identity, &storage_property_name) + .cloned() + { + eval_reference_target_write( + identity, + &storage_property_name, + target, + value, + context, + values, + )?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + return values.property_set(object, &storage_property_name, value); + } + values.property_set(object, &storage_property_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Binds one eval object property to a by-reference source parameter. +pub(super) fn eval_property_reference_bind_result( + object: RuntimeCellHandle, + property_name: &str, + source_name: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let identity = values.object_identity(object)?; + let class = context + .dynamic_object_class(identity) + .ok_or(EvalStatus::RuntimeFatal)?; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + let (declaring_class, property) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + .ok_or(EvalStatus::RuntimeFatal)?; + validate_eval_property_write_access(&declaring_class, &property, context)?; + if property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + let storage_property_name = eval_instance_property_storage_name(&declaring_class, &property); + let target = eval_property_reference_target( + identity, + &storage_property_name, + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_dynamic_property_alias(identity, &storage_property_name, target); + values.property_set(object, &storage_property_name, value)?; + context.mark_dynamic_property_initialized(identity, &storage_property_name); + Ok(()) +} + +/// Resolves a local by-reference source into a persistent property alias target. +pub(super) fn eval_property_reference_target( + object_identity: u64, + storage_property_name: &str, + source_name: &str, + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(target) = scope.reference_target(source_name).cloned() { + return Ok(target); + } + if context.current_function().is_some() { + let cell = + visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; + return Ok(EvalReferenceTarget::Cell { cell }); + } + let alias_name = eval_property_reference_alias_name(object_identity, storage_property_name); + for replaced in set_reference_alias(context, scope, &alias_name, source_name, values)? { + values.release(replaced)?; + } + Ok(EvalReferenceTarget::Variable { + scope: scope as *mut ElephcEvalScope, + name: alias_name, + }) +} + +/// Builds the hidden scope variable name that backs one property reference alias. +pub(super) fn eval_property_reference_alias_name(object_identity: u64, storage_property_name: &str) -> String { + format!("\0elephc_property_ref:{object_identity}:{storage_property_name}") +} + +/// Reads the current value from a persistent reference target. +pub(in crate::interpreter) fn eval_reference_target_value( + target: &EvalReferenceTarget, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match target { + EvalReferenceTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + visible_scope_cell(context, scope, name).map_or_else(|| values.null(), Ok) + } + EvalReferenceTarget::ArrayElement { + scope, + array_name, + index, + } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + let array = + visible_scope_cell(context, scope, array_name).map_or_else(|| values.null(), Ok)?; + values.array_get(array, *index) + } + EvalReferenceTarget::NestedArrayElement { + array_target, + index, + } => { + let array = eval_reference_target_value(array_target, context, values)?; + values.array_get(array, *index) + } + EvalReferenceTarget::ObjectProperty { + object, + property, + access_scope, + } => { + let previous_scope = context.replace_execution_scope(access_scope.clone()); + let result = eval_property_get_result(*object, property, context, values); + context.replace_execution_scope(previous_scope); + result + } + EvalReferenceTarget::StaticProperty { + class_name, + property, + access_scope, + } => { + let previous_scope = context.replace_execution_scope(access_scope.clone()); + let result = eval_static_property_get_result(class_name, property, context, values); + context.replace_execution_scope(previous_scope); + result + } + EvalReferenceTarget::Cell { cell } => Ok(*cell), + EvalReferenceTarget::InvokerSlot { slot, source_tag } => { + eval_invoker_slot_ref_target_value(*slot, *source_tag, values) + } + } +} + +/// Writes a new value to a persistent reference target. +pub(super) fn eval_reference_target_write( + object_identity: u64, + storage_property_name: &str, + target: EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if matches!(target, EvalReferenceTarget::Cell { .. }) { + context.bind_dynamic_property_alias( + object_identity, + storage_property_name, + EvalReferenceTarget::Cell { cell: value }, + ); + return Ok(()); + } + write_back_method_ref_target(&target, value, context, values) +} + +/// Evaluates PHP `isset($object->property)` without forcing `__get()` first. +pub(in crate::interpreter) fn eval_property_isset_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + }; + let Some(class) = context.dynamic_object_class(identity) else { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + }; + let object_class_name = class.name().to_string(); + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { + let storage_property_name = + eval_instance_property_storage_name(&declaring_class, &property); + if property.property_type().is_some() + && !context.dynamic_property_is_initialized(identity, &storage_property_name) + { + return Ok(false); + } + let value = eval_property_get_result(object, property_name, context, values)?; + return Ok(!values.is_null(value)?); + } + return eval_magic_property_isset( + object, + &object_class_name, + property_name, + context, + values, + ) + .map(|result| result.unwrap_or(false)); + } + if eval_object_public_property_exists(object, property_name, values)? { + let value = values.property_get(object, property_name)?; + return Ok(!values.is_null(value)?); + } + if let Some((declaring_class, visibility, _, is_static)) = + eval_dynamic_class_native_property_metadata( + &object_class_name, + property_name, + context, + values, + )? + { + if !is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_magic_property_isset( + object, + &object_class_name, + property_name, + context, + values, + ) + .map(|result| result.unwrap_or(false)); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_is_initialized(object, property_name) + })? { + return Ok(false); + } + let value = eval_with_native_bridge_scope(&declaring_class, context, || { + values.property_get(object, property_name) + })?; + return Ok(!values.is_null(value)?); + } + } + eval_magic_property_isset(object, &object_class_name, property_name, context, values) + .map(|result| result.unwrap_or(false)) +} + +/// Evaluates PHP `unset($object->property)` for eval-declared object receivers. +pub(in crate::interpreter) fn eval_property_unset_result( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(()); + }; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_ok() { + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_unset_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_unset_error( + &declaring_class, + property.name(), + context, + values, + ); + } + let storage_property_name = + eval_instance_property_storage_name(&declaring_class, &property); + context.remove_dynamic_property_alias(identity, &storage_property_name); + context.mark_dynamic_property_uninitialized(identity, &storage_property_name); + let null = values.null()?; + return values.property_set(object, &storage_property_name, null); + } + if eval_magic_property_unset(object, &object_class_name, property_name, context, values)? { + return Ok(()); + } + return Ok(()); + } + if eval_object_public_property_exists(object, property_name, values)? { + let null = values.null()?; + return values.property_set(object, property_name, null); + } + let _ = eval_magic_property_unset(object, &object_class_name, property_name, context, values)?; + Ok(()) +} + +/// Dispatches an undefined or inaccessible eval property read through `__get()`. +pub(super) fn eval_magic_property_get( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__get") else { + return Ok(None); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let property = values.string(property_name)?; + eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + ) + .map(Some) +} + +/// Dispatches an undefined or inaccessible eval property write through `__set()`. +pub(super) fn eval_magic_property_set( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__set") else { + return Ok(false); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property, value]), + context, + values, + )?; + values.release(result)?; + Ok(true) +} + +/// Dispatches an undefined or inaccessible eval property probe through `__isset()`. +pub(super) fn eval_magic_property_isset( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__isset") else { + return Ok(None); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + )?; + let truthy = values.truthy(result)?; + values.release(result)?; + Ok(Some(truthy)) +} + +/// Dispatches an undefined or inaccessible eval property unset through `__unset()`. +pub(super) fn eval_magic_property_unset( + object: RuntimeCellHandle, + object_class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some((declaring_class, method)) = context.class_method(object_class_name, "__unset") else { + return Ok(false); + }; + if method.is_static() || method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + let property = values.string(property_name)?; + let result = eval_dynamic_method_with_values( + &declaring_class, + object_class_name, + &method, + object, + positional_args(vec![property]), + context, + values, + )?; + values.release(result)?; + Ok(true) +} + +/// Returns whether the object already has a public dynamic property with this exact name. +pub(super) fn eval_object_public_property_exists( + object: RuntimeCellHandle, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + let property_count = values.object_property_len(object)?; + for position in 0..property_count { + let key = values.object_property_iter_key(object, position)?; + let key_bytes = values.string_bytes(key); + values.release(key)?; + if key_bytes? == property_name.as_bytes() { + return Ok(true); + } + } + Ok(false) +} + +/// Validates that an object property may be used as a by-reference method argument. +pub(in crate::interpreter) fn validate_property_ref_target( + object: RuntimeCellHandle, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Ok(identity) = values.object_identity(object) else { + return Ok(()); + }; + let Some(class) = context.dynamic_object_class(identity) else { + return Ok(()); + }; + let object_class_name = class.name().to_string(); + if context.has_enum(&object_class_name) { + return Err(EvalStatus::RuntimeFatal); + } + if let Some((declaring_class, property)) = + eval_dynamic_property_for_access(&object_class_name, property_name, context) + { + validate_eval_member_access(&declaring_class, property.visibility(), context)?; + if property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns true while executing the named hook accessor for one property. +pub(in crate::interpreter) fn current_eval_property_hook_is( + declaring_class: &str, + property_name: &str, + hook_method: &str, + context: &ElephcEvalContext, +) -> bool { + let Some(current_class) = context.current_class_scope() else { + return false; + }; + if !same_eval_class_name(current_class, declaring_class) { + return false; + } + let Some((_, method)) = context + .current_function() + .and_then(|function| function.rsplit_once("::")) + else { + return false; + }; + method.eq_ignore_ascii_case(hook_method) + || method.eq_ignore_ascii_case(&property_hook_get_method(property_name)) + || method.eq_ignore_ascii_case(&property_hook_set_method(property_name)) +} diff --git a/crates/elephc-magician/src/interpreter/statements/interface_contracts.rs b/crates/elephc-magician/src/interpreter/statements/interface_contracts.rs new file mode 100644 index 0000000000..3d75073d1e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/interface_contracts.rs @@ -0,0 +1,736 @@ +//! Purpose: +//! Resolves interface hierarchies and checks method/property signature compatibility. +//! +//! Called from: +//! - Class declaration and abstract-requirement validation. +//! +//! Key details: +//! - Arity, by-reference flags, variadics, return types, and hook capabilities remain PHP-compatible. + +use super::*; + +/// Returns interface names inherited or directly declared by a pending eval class. +pub(super) fn pending_class_interface_names(class: &EvalClass, context: &ElephcEvalContext) -> Vec { + let mut interfaces = Vec::new(); + let mut seen = std::collections::HashSet::new(); + if let Some(parent) = class.parent() { + for interface in context.class_interface_names(parent) { + push_pending_class_interface_name(&interface, &mut interfaces, &mut seen); + } + } + for interface in class.interfaces() { + push_pending_class_interface_tree(interface, context, &mut interfaces, &mut seen); + } + interfaces +} + +/// Adds one interface and its eval-declared parent interfaces to a pending class list. +pub(super) fn push_pending_class_interface_tree( + interface: &str, + context: &ElephcEvalContext, + interfaces: &mut Vec, + seen: &mut std::collections::HashSet, +) { + push_pending_class_interface_name(interface, interfaces, seen); + for parent in context.interface_parent_names(interface) { + push_pending_class_interface_name(&parent, interfaces, seen); + } +} + +/// Adds one interface name once using PHP class-name case-insensitive matching. +pub(super) fn push_pending_class_interface_name( + interface: &str, + interfaces: &mut Vec, + seen: &mut std::collections::HashSet, +) { + let interface = interface.trim_start_matches('\\'); + if seen.insert(interface.to_ascii_lowercase()) { + interfaces.push(interface.to_string()); + } +} + +/// Returns PHP builtin interface method requirements inherited by a pending class. +pub(super) fn pending_class_builtin_interface_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Vec<(String, EvalInterfaceMethod)> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + requirements.extend(builtin_interface_method_requirements(&interface)); + } + requirements +} + +/// Returns generated/AOT interface method requirements inherited by a pending class. +pub(super) fn pending_class_aot_interface_method_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) || !values.interface_exists(&interface)? { + continue; + } + requirements.extend(eval_aot_interface_method_requirements( + &interface, context, values, + )?); + } + Ok(requirements) +} + +/// Returns generated/AOT interface property requirements inherited by a pending class. +pub(super) fn pending_class_aot_interface_property_requirements( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut requirements = Vec::new(); + for interface in pending_class_interface_names(class, context) { + if context.has_interface(&interface) || !values.interface_exists(&interface)? { + continue; + } + requirements.extend(context.native_interface_property_requirements(&interface)); + } + Ok(requirements) +} + +/// Returns generated/AOT method requirements for one runtime interface. +pub(super) fn eval_aot_interface_method_requirements( + interface: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let interface = interface.trim_start_matches('\\'); + let method_names = eval_aot_interface_method_names(interface, values)?; + let mut requirements = Vec::new(); + for method_name in method_names { + if let Some(requirement) = + eval_aot_interface_method_requirement(interface, &method_name, context, values)? + { + requirements.push(requirement); + } + } + Ok(requirements) +} + +/// Returns generated/AOT method names for one runtime interface. +pub(super) fn eval_aot_interface_method_names( + interface: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + eval_aot_method_names(interface, values) +} + +/// Returns generated/AOT method names for one runtime class-like symbol. +pub(super) fn eval_aot_method_names( + class_like: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let method_names = values.reflection_method_names(class_like)?; + let names = eval_runtime_string_array_to_vec(method_names, values)?; + values.release(method_names)?; + Ok(names) +} + +/// Builds one generated/AOT abstract parent method requirement from metadata. +pub(super) fn eval_aot_abstract_method_requirement( + class_name: &str, + method_name: &str, + flags: u64, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let owner = eval_aot_method_declaring_class(class_name, method_name, values)?; + let signature = eval_aot_method_signature_requirement( + class_name, + method_name, + is_static, + context, + values, + )?; + Ok(EvalAotAbstractMethodRequirement { + owner, + is_static, + visibility: eval_aot_method_visibility(flags), + signature, + }) +} + +/// Builds one generated/AOT interface method requirement from reflection and signature metadata. +pub(super) fn eval_aot_interface_method_requirement( + interface: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(flags) = values.reflection_method_flags(interface, method_name)? else { + return Ok(None); + }; + let is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let owner = values + .reflection_method_declaring_class(interface, method_name)? + .unwrap_or_else(|| interface.to_string()); + let signature = if is_static { + context.native_static_method_signature(&owner, method_name) + } else { + context.native_method_signature(&owner, method_name) + }; + Ok(Some(EvalAotInterfaceMethodRequirement { + owner: owner.clone(), + name: method_name.to_string(), + is_static, + signature: signature.map(|signature| { + eval_native_signature_interface_method(method_name, is_static, &signature) + }), + })) +} + +/// Converts generated/AOT callable metadata into an eval interface method requirement. +pub(super) fn eval_native_signature_interface_method( + method_name: &str, + is_static: bool, + signature: &NativeCallableSignature, +) -> EvalInterfaceMethod { + let param_count = signature.param_count(); + EvalInterfaceMethod::new( + method_name, + (0..param_count) + .map(|index| { + signature + .param_names() + .get(index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{index}")) + }) + .collect(), + ) + .with_static(is_static) + .with_parameter_types( + (0..param_count) + .map(|index| signature.param_type(index).cloned()) + .collect(), + ) + .with_parameter_defaults( + (0..param_count) + .map(|index| { + signature + .param_default(index) + .map(|_| EvalExpr::Const(EvalConst::Null)) + }) + .collect(), + ) + .with_parameter_by_ref_flags( + (0..param_count) + .map(|index| signature.param_by_ref(index)) + .collect(), + ) + .with_parameter_variadic_flags( + (0..param_count) + .map(|index| signature.param_variadic(index)) + .collect(), + ) + .with_return_type(signature.return_type().cloned()) +} + +/// Copies a runtime string array into Rust-owned strings for declaration validation. +pub(super) fn eval_runtime_string_array_to_vec( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let len = values.array_len(array)?; + let mut result = Vec::with_capacity(len); + for position in 0..len { + let key = values.int(position as i64)?; + let value = values.array_get(array, key)?; + result.push(eval_runtime_string_value(value, values)?); + } + Ok(result) +} + +/// Reads one runtime string cell as UTF-8 metadata. +pub(super) fn eval_runtime_string_value( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + String::from_utf8(bytes).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Validates that one eval class provides methods required by one eval interface. +pub(super) fn validate_class_implements_eval_interface( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + context.interface_method_requirements_with_owners(interface_name) + { + if !class_has_interface_method(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + for (requirement_owner, requirement) in + context.interface_property_requirements_with_owners(interface_name) + { + if !class_has_interface_property(class, &requirement_owner, &requirement, context) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns whether a class or its eval parents satisfy one builtin interface method signature. +pub(super) fn class_has_builtin_interface_method( + class: &EvalClass, + requirement_owner: &str, + requirement: &EvalInterfaceMethod, + context: &ElephcEvalContext, +) -> bool { + if let Some(method) = class.method(requirement.name()) { + return method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_builtin_interface_signature( + method, + class.name(), + requirement, + requirement_owner, + Some(class), + context, + ); + } + class + .parent() + .and_then(|parent| context.class_method(parent, requirement.name())) + .is_some_and(|(declaring_class, method)| { + method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_builtin_interface_signature( + &method, + &declaring_class, + requirement, + requirement_owner, + Some(class), + context, + ) + }) +} + +/// Returns whether a class or its eval parents satisfy one generated/AOT interface method. +pub(super) fn class_has_aot_interface_method( + class: &EvalClass, + requirement: &EvalAotInterfaceMethodRequirement, + context: &ElephcEvalContext, +) -> bool { + if let Some((declaring_class, method)) = pending_class_method(class, &requirement.name, context) + { + return class_method_satisfies_aot_interface_requirement( + &method, + &declaring_class, + requirement, + Some(class), + context, + true, + ); + } + false +} + +/// Returns whether a class or its eval parents satisfy one interface method signature. +pub(super) fn class_has_interface_method( + class: &EvalClass, + requirement_owner: &str, + requirement: &EvalInterfaceMethod, + context: &ElephcEvalContext, +) -> bool { + if let Some(method) = class.method(requirement.name()) { + return method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_interface_signature( + method, + class.name(), + requirement, + requirement_owner, + Some(class), + context, + ); + } + class + .parent() + .and_then(|parent| context.class_method(parent, requirement.name())) + .is_some_and(|(declaring_class, method)| { + method.visibility() == EvalVisibility::Public + && method.is_static() == requirement.is_static() + && !method.is_abstract() + && class_method_satisfies_interface_signature( + &method, + &declaring_class, + requirement, + requirement_owner, + Some(class), + context, + ) + }) +} + +/// Returns whether one method satisfies a generated/AOT interface requirement. +pub(super) fn class_method_satisfies_aot_interface_requirement( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalAotInterfaceMethodRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + require_concrete: bool, +) -> bool { + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static + || (require_concrete && method.is_abstract()) + { + return false; + } + requirement.signature.as_ref().is_none_or(|signature| { + class_method_satisfies_interface_signature( + method, + method_owner, + signature, + &requirement.owner, + pending_class, + context, + ) + }) +} + +/// Returns whether one method satisfies a generated/AOT abstract parent requirement. +pub(super) fn class_method_satisfies_aot_abstract_parent_requirement( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalAotAbstractMethodRequirement, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + require_concrete: bool, +) -> bool { + if method.is_static() != requirement.is_static + || method_visibility_rank(method.visibility()) + < method_visibility_rank(requirement.visibility) + || (require_concrete && method.is_abstract()) + { + return false; + } + requirement.signature.as_ref().is_none_or(|signature| { + class_method_satisfies_interface_signature( + method, + method_owner, + signature, + &requirement.owner, + pending_class, + context, + ) + }) +} + +/// Returns whether one class method can accept every call required by an interface method. +pub(super) fn class_method_satisfies_interface_signature( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + class_method_satisfies_interface_signature_with_return_mode( + method, + method_owner, + requirement, + requirement_owner, + pending_class, + context, + false, + ) +} + +/// Returns whether one class method can satisfy a PHP builtin interface method contract. +pub(super) fn class_method_satisfies_builtin_interface_signature( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + class_method_satisfies_interface_signature_with_return_mode( + method, + method_owner, + requirement, + requirement_owner, + pending_class, + context, + true, + ) +} + +/// Returns whether one class method satisfies an interface signature with configurable return checks. +pub(super) fn class_method_satisfies_interface_signature_with_return_mode( + method: &EvalClassMethod, + method_owner: &str, + requirement: &EvalInterfaceMethod, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, + allow_missing_return_type: bool, +) -> bool { + method_signature_accepts( + method.params().len(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + requirement.params().len(), + requirement.parameter_defaults(), + requirement.parameter_is_by_ref(), + requirement.parameter_is_variadic(), + ) && method_parameter_type_signature_accepts( + method.parameter_types(), + method.parameter_is_variadic(), + method_owner, + requirement.parameter_types(), + requirement.parameter_is_variadic(), + requirement_owner, + requirement.params().len(), + pending_class, + context, + ) && ((allow_missing_return_type && method.return_type().is_none()) + || method_return_type_signature_accepts( + method.return_type(), + method_owner, + requirement.return_type(), + requirement_owner, + pending_class, + context, + )) +} + +/// Returns whether one class property is compatible with one interface property contract. +pub(super) fn class_property_can_cover_interface_contract( + property: &EvalClassProperty, + property_owner: &str, + requirement: &EvalInterfaceProperty, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if property.visibility() != EvalVisibility::Public || property.is_static() { + return false; + } + if !class_property_type_satisfies_interface_contract( + property.property_type(), + property.settable_type(), + property_owner, + requirement, + requirement_owner, + pending_class, + context, + ) { + return false; + } + if requirement.requires_get() && !class_property_supports_interface_get(property) { + return false; + } + if requirement.requires_set() { + return requirement.set_visibility() != Some(EvalVisibility::Private) + && property_visibility_rank(property.write_visibility()) + >= property_visibility_rank(requirement.write_visibility()) + && class_property_supports_interface_set(property); + } + requirement.requires_get() +} + +/// Returns whether one property type is compatible with interface get/set hook signatures. +pub(super) fn class_property_type_satisfies_interface_contract( + property_type: Option<&EvalParameterType>, + settable_type: Option<&EvalParameterType>, + property_owner: &str, + requirement: &EvalInterfaceProperty, + requirement_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + if requirement.requires_get() + && !method_return_type_signature_accepts( + property_type, + property_owner, + requirement.property_type(), + requirement_owner, + pending_class, + context, + ) + { + return false; + } + if requirement.requires_set() { + let property_types = vec![settable_type.cloned()]; + let requirement_types = vec![requirement.property_type().cloned()]; + return method_parameter_type_signature_accepts( + &property_types, + &[], + property_owner, + &requirement_types, + &[], + requirement_owner, + 1, + pending_class, + context, + ); + } + true +} + +/// Returns whether one property can satisfy an interface `get` hook. +pub(super) fn class_property_supports_interface_get(property: &EvalClassProperty) -> bool { + property.has_get_hook() || property.requires_get_hook() || !property.is_virtual() +} + +/// Returns whether one property can satisfy an interface `set` hook. +pub(super) fn class_property_supports_interface_set(property: &EvalClassProperty) -> bool { + property.has_set_hook() + || property.requires_set_hook() + || (!property.is_virtual() && !property.is_readonly()) +} + +/// Returns whether an implementing method accepts the full required arity range. +pub(super) fn method_signature_accepts( + implementation_param_count: usize, + implementation_defaults: &[Option], + implementation_by_refs: &[bool], + implementation_variadics: &[bool], + required_param_count: usize, + required_defaults: &[Option], + required_by_refs: &[bool], + required_variadics: &[bool], +) -> bool { + let implementation_min = method_signature_min_arity( + implementation_param_count, + implementation_defaults, + implementation_variadics, + ); + let required_min = + method_signature_min_arity(required_param_count, required_defaults, required_variadics); + if implementation_min > required_min { + return false; + } + + let implementation_max = + method_signature_max_arity(implementation_param_count, implementation_variadics); + let required_max = method_signature_max_arity(required_param_count, required_variadics); + let arity_accepted = match (implementation_max, required_max) { + (None, _) => true, + (Some(_), None) => false, + (Some(implementation_max), Some(required_max)) => implementation_max >= required_max, + }; + arity_accepted + && method_signature_by_refs_accept( + implementation_by_refs, + implementation_variadics, + required_param_count, + required_by_refs, + required_variadics, + ) +} + +/// Returns whether pass-by-reference requirements are compatible across accepted args. +pub(super) fn method_signature_by_refs_accept( + implementation_by_refs: &[bool], + implementation_variadics: &[bool], + required_param_count: usize, + required_by_refs: &[bool], + required_variadics: &[bool], +) -> bool { + (0..required_param_count).all(|position| { + method_signature_effective_by_ref( + implementation_by_refs, + implementation_variadics, + position, + ) == method_signature_effective_by_ref(required_by_refs, required_variadics, position) + }) +} + +/// Returns the by-reference mode that one signature applies at an argument position. +pub(super) fn method_signature_effective_by_ref( + by_refs: &[bool], + variadics: &[bool], + position: usize, +) -> bool { + if let Some(variadic_index) = variadics.iter().position(|is_variadic| *is_variadic) { + if position >= variadic_index { + return by_refs.get(variadic_index).copied().unwrap_or(false); + } + } + by_refs.get(position).copied().unwrap_or(false) +} + +/// Returns the minimum argument count accepted by one eval method signature. +pub(super) fn method_signature_min_arity( + param_count: usize, + defaults: &[Option], + variadics: &[bool], +) -> usize { + let fixed_count = variadics + .iter() + .position(|is_variadic| *is_variadic) + .unwrap_or(param_count); + (0..fixed_count) + .rfind(|position| !defaults.get(*position).is_some_and(Option::is_some)) + .map_or(0, |position| position + 1) +} + +/// Returns the maximum argument count accepted by one eval method signature. +pub(super) fn method_signature_max_arity(param_count: usize, variadics: &[bool]) -> Option { + if variadics.iter().any(|is_variadic| *is_variadic) { + None + } else { + Some(param_count) + } +} + +/// Returns whether a class or its eval parents satisfy one interface property contract. +pub(super) fn class_has_interface_property( + class: &EvalClass, + requirement_owner: &str, + requirement: &EvalInterfaceProperty, + context: &ElephcEvalContext, +) -> bool { + pending_class_property_with_owner(class, requirement.name(), context).is_some_and( + |(declaring_class, property)| { + !property.is_abstract() + && class_property_can_cover_interface_contract( + &property, + &declaring_class, + requirement, + requirement_owner, + Some(class), + context, + ) + }, + ) +} + +/// Returns a property from a pending class or one of its already registered parents. +pub(super) fn pending_class_property_with_owner( + class: &EvalClass, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassProperty)> { + if let Some(property) = class + .properties() + .iter() + .find(|property| property.name() == property_name) + { + return Some((class.name().to_string(), property.clone())); + } + class + .parent() + .and_then(|parent| context.class_property(parent, property_name)) +} diff --git a/crates/elephc-magician/src/interpreter/statements/interface_member_validation.rs b/crates/elephc-magician/src/interpreter/statements/interface_member_validation.rs new file mode 100644 index 0000000000..4bc7f48c7f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/interface_member_validation.rs @@ -0,0 +1,360 @@ +//! Purpose: +//! Validates declared methods and properties against interface member contracts. +//! +//! Called from: +//! - Class declaration validation before abstract-requirement collection. +//! +//! Key details: +//! - Eval, builtin, and AOT interface signatures use the same compatibility checks. + +use super::*; + +/// Validates declared or inherited class members that already cover eval interface contracts. +pub(super) fn validate_declared_class_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for interface in pending_class_interface_names(class, context) { + if !context.has_interface(&interface) { + continue; + } + validate_declared_class_interface_methods(class, &interface, context)?; + validate_declared_class_interface_properties(class, &interface, context)?; + } + Ok(()) +} + +/// Validates declared class methods against PHP builtin runtime interface contracts. +pub(super) fn validate_declared_class_builtin_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + pending_class_builtin_interface_method_requirements(class, context) + { + let Some((declaring_class, method)) = + pending_class_method(class, requirement.name(), context) + else { + continue; + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static() + || !class_method_satisfies_builtin_interface_signature( + &method, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates declared class methods against generated/AOT interface contracts. +pub(super) fn validate_declared_class_aot_interface_members( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for requirement in pending_class_aot_interface_method_requirements(class, context, values)? { + let Some((declaring_class, method)) = pending_class_method(class, &requirement.name, context) + else { + continue; + }; + if !class_method_satisfies_aot_interface_requirement( + &method, + &declaring_class, + &requirement, + Some(class), + context, + false, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } + for (requirement_owner, requirement) in + pending_class_aot_interface_property_requirements(class, context, values)? + { + let Some((declaring_class, property)) = + pending_class_property_with_owner(class, requirement.name(), context) + else { + continue; + }; + if !class_property_can_cover_interface_contract( + &property, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates class methods present for an eval interface, even on abstract classes. +pub(super) fn validate_declared_class_interface_methods( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + context.interface_method_requirements_with_owners(interface_name) + { + let Some((declaring_class, method)) = + pending_class_method(class, requirement.name(), context) + else { + continue; + }; + if method.visibility() != EvalVisibility::Public + || method.is_static() != requirement.is_static() + || !class_method_satisfies_interface_signature( + &method, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates class properties present for an eval interface, even on abstract classes. +pub(super) fn validate_declared_class_interface_properties( + class: &EvalClass, + interface_name: &str, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for (requirement_owner, requirement) in + context.interface_property_requirements_with_owners(interface_name) + { + let Some((declaring_class, property)) = + pending_class_property_with_owner(class, requirement.name(), context) + else { + continue; + }; + if !class_property_can_cover_interface_contract( + &property, + &declaring_class, + &requirement, + &requirement_owner, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Returns a method from a pending class or one of its already registered parents. +pub(super) fn pending_class_method( + class: &EvalClass, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(method) = class.method(method_name) { + return Some((class.name().to_string(), method.clone())); + } + class + .parent() + .and_then(|parent| context.class_method(parent, method_name)) +} + +/// Validates one method declaration against inherited eval method metadata. +pub(super) fn validate_method_parent_override( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(()); + }; + let Some((parent_declaring_class, parent_method)) = context.class_method(parent, method.name()) + else { + return Ok(()); + }; + if parent_method.visibility() == EvalVisibility::Private { + return Ok(()); + } + if parent_method.is_static() != method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + if method_visibility_rank(method.visibility()) + < method_visibility_rank(parent_method.visibility()) + { + return Err(EvalStatus::RuntimeFatal); + } + if parent_method.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && !parent_method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if !class_method_signature_accepts( + method, + class.name(), + &parent_method, + &parent_declaring_class, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Validates one method declaration against inherited generated/AOT method metadata. +pub(super) fn validate_method_aot_parent_override( + class: &EvalClass, + method: &EvalClassMethod, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = pending_class_native_parent_name(class, context) else { + return Ok(()); + }; + if !values.class_exists(&parent)? { + return Ok(()); + } + let Some(flags) = values.reflection_method_flags(&parent, method.name())? else { + return Ok(()); + }; + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + return Ok(()); + } + let parent_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + if parent_is_static != method.is_static() { + return Err(EvalStatus::RuntimeFatal); + } + let parent_visibility = eval_aot_method_visibility(flags); + if method_visibility_rank(method.visibility()) < method_visibility_rank(parent_visibility) { + return Err(EvalStatus::RuntimeFatal); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT == 0 { + return Err(EvalStatus::RuntimeFatal); + } + let Some(required) = eval_aot_method_signature_requirement( + &parent, + method.name(), + parent_is_static, + context, + values, + )? else { + return Ok(()); + }; + if !class_method_satisfies_interface_signature( + method, + class.name(), + &required, + &eval_aot_method_declaring_class(&parent, method.name(), values)?, + Some(class), + context, + ) { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the eval visibility represented by generated/AOT reflection flags. +pub(super) fn eval_aot_method_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC != 0 { + EvalVisibility::Public + } else { + EvalVisibility::Public + } +} + +/// Returns the generated/AOT declaring class for one reflected method. +pub(super) fn eval_aot_method_declaring_class( + class_name: &str, + method_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values + .reflection_method_declaring_class(class_name, method_name) + .map(|declaring_class| declaring_class.unwrap_or_else(|| class_name.to_string())) +} + +/// Returns a generated/AOT parent method signature as an eval method requirement. +pub(super) fn eval_aot_method_signature_requirement( + class_name: &str, + method_name: &str, + is_static: bool, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let declaring_class = eval_aot_method_declaring_class(class_name, method_name, values)?; + let signature = if is_static { + context.native_static_method_signature(&declaring_class, method_name) + } else { + context.native_method_signature(&declaring_class, method_name) + }; + Ok(signature.map(|signature| { + eval_native_signature_interface_method(method_name, is_static, &signature) + })) +} + +/// Returns whether one eval class method can accept every call accepted by its parent method. +pub(super) fn class_method_signature_accepts( + method: &EvalClassMethod, + method_owner: &str, + required: &EvalClassMethod, + required_owner: &str, + pending_class: Option<&EvalClass>, + context: &ElephcEvalContext, +) -> bool { + method_signature_accepts( + method.params().len(), + method.parameter_defaults(), + method.parameter_is_by_ref(), + method.parameter_is_variadic(), + required.params().len(), + required.parameter_defaults(), + required.parameter_is_by_ref(), + required.parameter_is_variadic(), + ) && method_parameter_type_signature_accepts( + method.parameter_types(), + method.parameter_is_variadic(), + method_owner, + required.parameter_types(), + required.parameter_is_variadic(), + required_owner, + required.params().len(), + pending_class, + context, + ) && method_return_type_signature_accepts( + method.return_type(), + method_owner, + required.return_type(), + required_owner, + pending_class, + context, + ) +} + +/// Returns a comparable rank where larger means less restrictive visibility. +pub(super) fn method_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/loop_statements.rs b/crates/elephc-magician/src/interpreter/statements/loop_statements.rs new file mode 100644 index 0000000000..5b6030fd5d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/loop_statements.rs @@ -0,0 +1,353 @@ +//! Purpose: +//! Executes static-local declarations, switch, and loop statement families. +//! +//! Called from: +//! - `crate::interpreter::statements::execute_stmt()`. +//! +//! Key details: +//! - Break/continue control, foreach array/object/iterator traversal, and key materialization are preserved. + +use super::*; + +/// Executes a PHP `static $name = expr;` declaration in the current eval scope. +pub(in crate::interpreter) fn execute_static_var_stmt( + name: &str, + init: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(function_name) = context.current_function().map(str::to_string) else { + let value = eval_expr(init, context, scope, values)?; + if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Owned) { + values.release(replaced)?; + } + return Ok(()); + }; + if scope.contains_visible(name) { + return Ok(()); + } + let value = if let Some(value) = context.static_local(&function_name, name) { + value + } else { + let value = eval_expr(init, context, scope, values)?; + let _ = context.set_static_local(function_name.clone(), name.to_string(), value); + value + }; + if let Some(replaced) = scope.set(name.to_string(), value, ScopeCellOwnership::Borrowed) { + values.release(replaced)?; + } + Ok(()) +} + +/// Executes a PHP switch with loose case matching, default fallback, and fallthrough. +pub(in crate::interpreter) fn execute_switch_stmt( + expr: &EvalExpr, + cases: &[EvalSwitchCase], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let subject = eval_expr(expr, context, scope, values)?; + let mut default_index = None; + let mut matched_index = None; + for (index, case) in cases.iter().enumerate() { + let Some(condition) = &case.condition else { + if default_index.is_none() { + default_index = Some(index); + } + continue; + }; + let condition = eval_expr(condition, context, scope, values)?; + let matches = values.compare(EvalBinOp::LooseEq, subject, condition)?; + if values.truthy(matches)? { + matched_index = Some(index); + break; + } + } + let Some(start_index) = matched_index.or(default_index) else { + return Ok(EvalControl::None); + }; + for case in &cases[start_index..] { + match execute_statements(&case.body, context, scope, values)? { + EvalControl::None => {} + EvalControl::Break | EvalControl::Continue => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `do/while` loop, evaluating the condition after every body run. +pub(in crate::interpreter) fn execute_do_while_stmt( + body: &[EvalStmt], + condition: &EvalExpr, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + loop { + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + let condition = eval_expr(condition, context, scope, values)?; + if !values.truthy(condition)? { + break; + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `for` loop while preserving update-on-continue semantics. +pub(in crate::interpreter) fn execute_for_stmt( + init: &[EvalStmt], + condition: Option<&EvalExpr>, + update: &[EvalStmt], + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + match execute_statements(init, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => return Ok(EvalControl::None), + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + loop { + if let Some(condition) = condition { + let condition = eval_expr(condition, context, scope, values)?; + if !values.truthy(condition)? { + break; + } + } + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + match execute_statements(update, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Executes a PHP `foreach` loop over eval array and Traversable object values. +pub(in crate::interpreter) fn execute_foreach_stmt( + array: &EvalExpr, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let array = eval_expr(array, context, scope, values)?; + match values.type_tag(array)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => { + execute_foreach_array_stmt(array, key_name, value_name, body, context, scope, values) + } + EVAL_TAG_OBJECT => { + execute_foreach_object_stmt(array, key_name, value_name, body, context, scope, values) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Executes `foreach` over a PHP array value using insertion-order runtime hooks. +pub(super) fn execute_foreach_array_stmt( + array: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + for index in 0..len { + let key = values.array_iter_key(array, index)?; + let value = values.array_get(array, key)?; + if let Some(key_name) = key_name { + for replaced in set_scope_cell( + context, + scope, + key_name.to_string(), + key, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } else { + values.release(key)?; + } + for replaced in set_scope_cell( + context, + scope, + value_name.to_string(), + value, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => {} + EvalControl::Break => break, + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } + Ok(EvalControl::None) +} + +/// Executes `foreach` over an Iterator or IteratorAggregate object. +pub(super) fn execute_foreach_object_stmt( + object: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if eval_foreach_object_is_a(object, "Iterator", context, values)? { + return execute_foreach_iterator_stmt( + object, key_name, value_name, body, context, scope, values, + ); + } + if eval_foreach_object_is_a(object, "IteratorAggregate", context, values)? { + let iterator = eval_method_call_result(object, "getIterator", Vec::new(), context, values)?; + return match values.type_tag(iterator)? { + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => execute_foreach_array_stmt( + iterator, key_name, value_name, body, context, scope, values, + ), + EVAL_TAG_OBJECT if eval_foreach_object_is_a(iterator, "Iterator", context, values)? => { + execute_foreach_iterator_stmt( + iterator, key_name, value_name, body, context, scope, values, + ) + } + _ => Err(EvalStatus::RuntimeFatal), + }; + } + Err(EvalStatus::RuntimeFatal) +} + +/// Drives one Iterator object through PHP's `foreach` method-call sequence. +pub(super) fn execute_foreach_iterator_stmt( + iterator: RuntimeCellHandle, + key_name: Option<&str>, + value_name: &str, + body: &[EvalStmt], + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + let result = eval_method_call_result(iterator, "rewind", Vec::new(), context, values)?; + values.release(result)?; + loop { + let valid = eval_method_call_result(iterator, "valid", Vec::new(), context, values)?; + let is_valid = values.truthy(valid)?; + values.release(valid)?; + if !is_valid { + return Ok(EvalControl::None); + } + + let value = eval_method_call_result(iterator, "current", Vec::new(), context, values)?; + let key = if key_name.is_some() { + Some(eval_method_call_result( + iterator, + "key", + Vec::new(), + context, + values, + )?) + } else { + None + }; + if let Some((key_name, key)) = key_name.zip(key) { + for replaced in set_scope_cell( + context, + scope, + key_name.to_string(), + key, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + } + for replaced in set_scope_cell( + context, + scope, + value_name.to_string(), + value, + ScopeCellOwnership::Owned, + )? { + values.release(replaced)?; + } + + match execute_statements(body, context, scope, values)? { + EvalControl::None | EvalControl::Continue => { + let result = + eval_method_call_result(iterator, "next", Vec::new(), context, values)?; + values.release(result)?; + } + EvalControl::Break => return Ok(EvalControl::None), + EvalControl::Throw(result) => return Ok(EvalControl::Throw(result)), + EvalControl::ReturnVoid => return Ok(EvalControl::ReturnVoid), + EvalControl::Return(result) => return Ok(EvalControl::Return(result)), + } + } +} + +/// Returns whether a foreach object satisfies one iterator interface. +pub(super) fn eval_foreach_object_is_a( + object: RuntimeCellHandle, + target: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + dynamic_object_is_a(object, target, false, context, values)? + .map_or_else(|| values.object_is_a(object, target, false), Ok) +} + +/// Returns PHP's next automatic integer key for `$array[]` append writes. +pub(in crate::interpreter) fn eval_array_append_key( + array: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let len = values.array_len(array)?; + let mut next_key = None; + for position in 0..len { + let key = values.array_iter_key(array, position)?; + if values.type_tag(key)? != EVAL_TAG_INT { + continue; + } + let one = values.int(1)?; + let candidate = values.add(key, one)?; + let replace = if let Some(current) = next_key { + let is_greater = values.compare(EvalBinOp::Gt, candidate, current)?; + values.truthy(is_greater)? + } else { + true + }; + if replace { + next_key = Some(candidate); + } + } + next_key.map_or_else(|| values.int(0), Ok) +} diff --git a/crates/elephc-magician/src/interpreter/statements/method_dispatch.rs b/crates/elephc-magician/src/interpreter/statements/method_dispatch.rs new file mode 100644 index 0000000000..25421c06ce --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/method_dispatch.rs @@ -0,0 +1,863 @@ +//! Purpose: +//! Dispatches eval and native instance methods with dynamic visibility and scope. +//! +//! Called from: +//! - Method-call expression evaluation and reflected invocation. +//! +//! Key details: +//! - Private shadows, native bridges, closures, magic methods, and reference args remain ordered. + +use super::*; + +/// Dispatches a method call to an eval-declared class method or to the runtime hook. +pub(in crate::interpreter) fn eval_method_call_result( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_method_call_result_with_evaluated_args( + object, + method_name, + positional_args(evaluated_args), + context, + values, + ) +} + +/// Dispatches an object method call while preserving named-argument metadata for eval methods. +pub(in crate::interpreter) fn eval_method_call_result_with_evaluated_args( + object: RuntimeCellHandle, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Ok(identity) = values.object_identity(object) else { + let evaluated_args = positional_evaluated_arg_values(evaluated_args)?; + return values.method_call(object, method_name, evaluated_args); + }; + if let Some(target) = context.closure_object_target(identity).cloned() { + if let Some(result) = + eval_closure_object_method_result(target, method_name, evaluated_args.clone(), context, values)? + { + return Ok(result); + } + } + if let Some(attribute_metadata) = context.eval_reflection_attribute(identity).cloned() { + if method_name.eq_ignore_ascii_case("newInstance") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return eval_reflection_attribute_new_instance_result( + attribute_metadata.attribute(), + context, + values, + ); + } + if method_name.eq_ignore_ascii_case("getArguments") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(args) = attribute_metadata.attribute().args() else { + return Err(EvalStatus::RuntimeFatal); + }; + return eval_class_attribute_args_result(args, values); + } + if method_name.eq_ignore_ascii_case("getTarget") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return values.int(attribute_metadata.target() as i64); + } + if method_name.eq_ignore_ascii_case("isRepeated") { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + return values.bool_value(attribute_metadata.is_repeated()); + } + } + if let Some(result) = eval_reflection_parameter_legacy_type_predicate_result( + object, + method_name, + evaluated_args.clone(), + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_parameter_to_string_result( + object, + method_name, + evaluated_args.clone(), + values, + )? { + return Ok(result); + } + if let Some(result) = + eval_reflection_type_to_string_result(object, method_name, evaluated_args.clone(), values)? + { + return Ok(result); + } + if let Some(result) = eval_reflection_class_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_implements_interface_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_is_subclass_of_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_is_instance_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_source_location_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_basic_metadata_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_has_method_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_has_property_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_has_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_enum_methods_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_relation_objects_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_trait_aliases_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_constants_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_default_properties_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_static_properties_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_static_property_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_set_static_property_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_function_invoke_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_method_invoke_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_function_method_metadata_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_function_method_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_method_prototype_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_set_accessible_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_hooks_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_is_initialized_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_lazy_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_constant_to_string_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_enum_case_get_enum_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_get_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_raw_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_property_set_value_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_reflection_constant_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_reflection_constants_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_members_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_class_get_member_result( + object, + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(instance) = eval_reflection_class_new_instance_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(instance); + } + if let Some(instance) = eval_reflection_class_new_instance_without_constructor_result( + identity, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(instance); + } + let Some(class) = context.dynamic_object_class(identity) else { + let class_name = runtime_object_class_name(object, values)?; + if method_name.eq_ignore_ascii_case("__clone") { + if let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&class_name, method_name, values)? + { + if is_static || is_abstract { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + } + } + return eval_native_method_with_evaluated_args( + object, + &class_name, + method_name, + evaluated_args, + context, + values, + ); + }; + let called_class_name = class.name().to_string(); + if eval_enum_static_builtin_applies(&called_class_name, method_name, context).is_some() { + return eval_enum_builtin_static_method_result( + &called_class_name, + method_name, + evaluated_args, + context, + values, + ); + } + let mut inaccessible_method = None; + if let Some((class_name, method)) = + eval_dynamic_method_for_call(&called_class_name, method_name, context) + { + if method.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + if validate_eval_member_access(&class_name, method.visibility(), context).is_ok() { + if method.is_static() { + return eval_dynamic_static_method_with_values( + &class_name, + &called_class_name, + &method, + evaluated_args, + context, + values, + ); + } + return eval_dynamic_method_with_values( + &class_name, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ); + } + inaccessible_method = Some((class_name, method)); + } + if inaccessible_method.is_none() { + if let Some(parent) = context.class_native_parent_name(&called_class_name) { + if let Some((declaring_class, _, _, _)) = + eval_aot_method_dispatch_metadata_in_hierarchy( + &parent, + method_name, + context, + values, + )? + { + return eval_native_method_with_evaluated_args_bridge_scope( + object, + &parent, + method_name, + evaluated_args, + Some(&declaring_class), + Some(&called_class_name), + context, + values, + ); + } + if eval_native_instance_magic_method_available(&parent, context, values)? { + return eval_native_method_with_evaluated_args( + object, + &parent, + method_name, + evaluated_args, + context, + values, + ); + } + } + } + if let Some(result) = eval_magic_instance_method_call( + object, + &called_class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } + if let Some((declaring_class, method)) = inaccessible_method { + return eval_throw_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + eval_throw_undefined_method_call_error(&called_class_name, method_name, context, values) +} + +/// Dispatches PHP-visible methods on eval-backed `Closure` objects. +pub(super) fn eval_closure_object_method_result( + target: EvalClosureObjectTarget, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if method_name.eq_ignore_ascii_case("__invoke") { + return eval_closure_object_invoke_result(target, evaluated_args, context, values) + .map(Some); + } + if method_name.eq_ignore_ascii_case("bindTo") { + let (bound_this, bound_scope, rebinds_function_scope) = + eval_closure_bind_to_args(evaluated_args, context, values)?; + return eval_closure_bind_target( + target, + bound_this, + bound_scope, + rebinds_function_scope, + context, + values, + ) + .map(Some); + } + if !method_name.eq_ignore_ascii_case("call") { + return Ok(None); + } + let (bound_this, call_args) = eval_closure_call_split_args(evaluated_args)?; + match target { + EvalClosureObjectTarget::Named(name) => { + if context.closure(&name).is_some() { + let callable = EvaluatedCallable::BoundClosure { + name, + bound_this: Some(bound_this), + bound_scope: None, + }; + return eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, + ) + .map(Some); + } + eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ) + .map(Some) + } + EvalClosureObjectTarget::BoundNamed { + name, bound_scope, .. + } => { + if context.closure(&name).is_some() { + let callable = EvaluatedCallable::BoundClosure { + name, + bound_this: Some(bound_this), + bound_scope, + }; + return eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, + ) + .map(Some); + } + eval_closure_call_warning_null( + "Cannot rebind scope of closure created from function", + values, + ) + .map(Some) + } + EvalClosureObjectTarget::InvokableObject { object } => { + if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ) + .map(Some); + } + let callable = EvaluatedCallable::InvokableObject { object: bound_this }; + eval_evaluated_callable_with_by_value_call_args(&callable, call_args, context, values) + .map(Some) + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class: _, + native_class, + bridge_scope, + } => { + if !eval_closure_call_bound_class_matches(object, bound_this, context, values)? { + return eval_closure_call_warning_null( + "Cannot rebind scope of closure created from method", + values, + ) + .map(Some); + } + let called_class = Some(eval_closure_bound_object_class_name( + bound_this, context, values, + )?); + let callable = EvaluatedCallable::ObjectMethod { + object: bound_this, + method, + called_class, + native_class, + bridge_scope, + }; + eval_evaluated_callable_with_by_value_call_args( + &callable, call_args, context, values, + ) + .map(Some) + } + EvalClosureObjectTarget::StaticMethod { .. } => eval_closure_call_warning_null( + "Cannot bind an instance to a static closure", + values, + ) + .map(Some), + } +} + +/// Invokes the callable target retained behind a PHP-visible eval `Closure` object. +pub(super) fn eval_closure_object_invoke_result( + target: EvalClosureObjectTarget, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let callable = match target { + EvalClosureObjectTarget::Named(name) => EvaluatedCallable::Named { + display_name: name.clone(), + name, + }, + EvalClosureObjectTarget::BoundNamed { + name, + bound_this, + bound_scope, + } => EvaluatedCallable::BoundClosure { + name, + bound_this, + bound_scope, + }, + EvalClosureObjectTarget::InvokableObject { object } => { + EvaluatedCallable::InvokableObject { object } + } + EvalClosureObjectTarget::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::ObjectMethod { + object, + method, + called_class, + native_class, + bridge_scope, + }, + EvalClosureObjectTarget::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + } => EvaluatedCallable::StaticMethod { + class_name, + method, + called_class, + native_class, + bridge_scope, + }, + }; + eval_evaluated_callable_with_call_array_args(&callable, evaluated_args, context, values) +} + +/// Splits `Closure::call()` arguments into the bound object and forwarded closure args. +pub(super) fn eval_closure_call_split_args( + evaluated_args: Vec, +) -> Result<(RuntimeCellHandle, Vec), EvalStatus> { + let mut bound_this = None; + let mut consumed_positional_receiver = false; + let mut call_args = Vec::with_capacity(evaluated_args.len().saturating_sub(1)); + + for arg in evaluated_args { + if arg + .name + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case("newThis")) + { + if bound_this.replace(arg.value).is_some() { + return Err(EvalStatus::RuntimeFatal); + } + continue; + } + if arg.name.is_none() && !consumed_positional_receiver && bound_this.is_none() { + consumed_positional_receiver = true; + bound_this = Some(arg.value); + continue; + } + call_args.push(arg); + } + + bound_this + .map(|receiver| (receiver, call_args)) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns whether `Closure::call()` may bind a method closure to the new object. +pub(super) fn eval_closure_call_bound_class_matches( + original_object: RuntimeCellHandle, + bound_this: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let original_class = eval_closure_bound_object_class_name(original_object, context, values)?; + let bound_class = eval_closure_bound_object_class_name(bound_this, context, values)?; + Ok(original_class.eq_ignore_ascii_case(&bound_class)) +} + +/// Returns whether `Closure::bind()` may bind a method closure to the new object. +pub(super) fn eval_closure_bind_bound_class_matches_method( + original_object: RuntimeCellHandle, + method_name: &str, + bound_this: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let Some(declaring_class) = + eval_closure_bind_method_declaring_class(original_object, method_name, context, values)? + else { + return Ok(false); + }; + eval_closure_object_is_instance_of(bound_this, &declaring_class, context, values) +} + +/// Resolves the class that declares the method captured by a method Closure target. +pub(super) fn eval_closure_bind_method_declaring_class( + original_object: RuntimeCellHandle, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let original_class = eval_closure_bound_object_class_name(original_object, context, values)?; + if let Some((declaring_class, method)) = context.class_method(&original_class, method_name) { + if method.is_static() || method.is_abstract() { + return Ok(None); + } + return Ok(Some(declaring_class)); + } + let native_class = context + .class_native_parent_name(&original_class) + .unwrap_or_else(|| original_class.clone()); + let Some((_, _, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(&native_class, method_name, context, values)? + else { + return Ok(None); + }; + if is_static || is_abstract { + return Ok(None); + } + let declaring_class = eval_aot_method_declaring_class(&native_class, method_name, values)?; + Ok(Some(declaring_class)) +} + +/// Returns whether an object is an instance of the requested eval or generated class name. +pub(super) fn eval_closure_object_is_instance_of( + object: RuntimeCellHandle, + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object_class = eval_closure_bound_object_class_name(object, context, values)?; + Ok(eval_static_syntax_object_matches_class( + &object_class, + class_name, + context, + )) +} + +/// Emits PHP's `Closure::call()` warning and returns `null`. +pub(super) fn eval_closure_call_warning_null( + message: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values.warning(message)?; + values.null() +} diff --git a/crates/elephc-magician/src/interpreter/statements/native_argument_binding.rs b/crates/elephc-magician/src/interpreter/statements/native_argument_binding.rs new file mode 100644 index 0000000000..82d9eaecca --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/native_argument_binding.rs @@ -0,0 +1,420 @@ +//! Purpose: +//! Binds native callable arguments and prepares by-reference writeback metadata. +//! +//! Called from: +//! - Native instance, static, constructor, and call_user_func dispatch. +//! +//! Key details: +//! - Named, variadic, typed, and degraded by-value reference modes share one binder. + +use super::*; + +/// Binds native AOT callable args using the selected by-reference degradation mode. +pub(super) fn bind_native_callable_bound_args_with_mode( + signature: Option, + args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(signature) = signature else { + return positional_evaluated_bound_args(None, args, by_ref_mode, context, values); + }; + if !signature.bridge_supported() { + return Err(EvalStatus::RuntimeFatal); + } + if signature.param_names().len() == signature.param_count() { + bind_native_signature_args(&signature, args, by_ref_mode, context, values) + } else { + positional_evaluated_bound_args(Some(&signature), args, by_ref_mode, context, values) + } +} + +/// Binds positional-only native AOT args and validates registered by-reference slots. +pub(super) fn positional_evaluated_bound_args( + signature: Option<&NativeCallableSignature>, + args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if args.iter().any(|arg| arg.name.is_some()) { + return Err(EvalStatus::RuntimeFatal); + } + let mut bound_args = args + .into_iter() + .enumerate() + .map(|(index, arg)| { + let ref_target = match signature { + Some(signature) => native_parameter_ref_target( + signature, + Some(index), + arg.ref_target, + by_ref_mode, + values, + )?, + None => None, + }; + Ok(BoundMethodArg { + value: arg.value, + ref_target, + variadic_ref_targets: Vec::new(), + }) + }) + .collect::, _>>()?; + if let Some(signature) = signature { + apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; + copy_native_call_user_func_by_value_ref_args( + signature, + &mut bound_args, + by_ref_mode, + values, + )?; + } + Ok(bound_args) +} + +/// Returns only runtime cell values from bound native AOT call arguments. +pub(in crate::interpreter) fn native_bound_arg_values( + args: &[BoundMethodArg], +) -> Vec { + args.iter().map(|arg| arg.value).collect() +} + +/// Writes native AOT by-reference argument cells back to their eval caller targets. +pub(in crate::interpreter) fn write_back_native_callable_ref_args( + bound_args: &[BoundMethodArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for bound_arg in bound_args { + if let Some(target) = bound_arg.ref_target.as_ref() { + write_back_method_ref_target(target, bound_arg.value, context, values)?; + } + for (key, target) in &bound_arg.variadic_ref_targets { + let value = values.array_get(bound_arg.value, *key)?; + write_back_method_ref_target(target, value, context, values)?; + } + } + Ok(()) +} + +/// Binds native AOT callable args and fills omitted defaults from metadata. +pub(super) fn bind_native_signature_args( + signature: &NativeCallableSignature, + args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut bound_args = vec![None; signature.param_count()]; + let variadic_index = native_callable_variadic_index(signature); + let mut next_positional = 0; + let mut next_variadic_index = 0_i64; + + if let Some(index) = variadic_index { + let array = values.array_new(args.len())?; + bound_args[index] = Some(BoundMethodArg { + value: array, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + for arg in args { + if let Some(name) = arg.name { + bind_native_named_signature_arg( + signature, + variadic_index, + &mut bound_args, + &name, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } else { + bind_native_positional_signature_arg( + signature, + &mut bound_args, + variadic_index, + &mut next_positional, + &mut next_variadic_index, + arg.value, + arg.ref_target, + by_ref_mode, + values, + )?; + } + } + + for (position, value) in bound_args.iter_mut().enumerate() { + if Some(position) == variadic_index { + continue; + } + if value.is_some() { + continue; + } + if position < signature.required_param_count() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(default) = signature.param_default(position) else { + return Err(EvalStatus::RuntimeFatal); + }; + *value = Some(BoundMethodArg { + value: materialize_native_callable_default(default, context, values)?, + ref_target: None, + variadic_ref_targets: Vec::new(), + }); + } + + let mut bound_args = bound_args + .into_iter() + .collect::>>() + .ok_or(EvalStatus::RuntimeFatal)?; + apply_native_callable_bound_arg_types(signature, &mut bound_args, context, values)?; + copy_native_call_user_func_by_value_ref_args( + signature, + &mut bound_args, + by_ref_mode, + values, + )?; + Ok(bound_args) +} + +/// Applies registered native AOT parameter types after argument binding and default filling. +pub(super) fn apply_native_callable_bound_arg_types( + signature: &NativeCallableSignature, + bound_args: &mut [BoundMethodArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, bound_arg) in bound_args.iter_mut().enumerate() { + let Some(param_type) = signature.param_type(position) else { + continue; + }; + if signature.param_variadic(position) { + apply_native_callable_variadic_arg_type(param_type, bound_arg, context, values)?; + } else { + bound_arg.value = + eval_method_parameter_value(param_type, bound_arg.value, context, values)?; + } + } + Ok(()) +} + +/// Applies one registered native variadic parameter type to each collected argument. +pub(super) fn apply_native_callable_variadic_arg_type( + param_type: &EvalParameterType, + bound_arg: &mut BoundMethodArg, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let len = values.array_len(bound_arg.value)?; + for position in 0..len { + let key = values.array_iter_key(bound_arg.value, position)?; + let value = values.array_get(bound_arg.value, key)?; + let value = eval_method_parameter_value(param_type, value, context, values)?; + bound_arg.value = values.array_set(bound_arg.value, key, value)?; + } + Ok(()) +} + +/// Copies by-value degraded by-ref native method args before the generated bridge mutates them. +pub(super) fn copy_native_call_user_func_by_value_ref_args( + signature: &NativeCallableSignature, + bound_args: &mut [BoundMethodArg], + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !matches!(by_ref_mode, EvalByRefBindingMode::WarnByValue { .. }) { + return Ok(()); + } + let variadic_index = native_callable_variadic_index(signature); + for (position, bound_arg) in bound_args.iter_mut().enumerate() { + let param_index = if variadic_index.is_some_and(|index| position >= index) { + variadic_index.ok_or(EvalStatus::RuntimeFatal)? + } else { + position + }; + if !signature.param_by_ref(param_index) || bound_arg.ref_target.is_some() { + continue; + } + bound_arg.value = copy_native_call_user_func_by_value_ref_arg(bound_arg.value, values)?; + } + Ok(()) +} + +/// Allocates a temporary runtime cell for one by-value degraded by-ref native method arg. +pub(super) fn copy_native_call_user_func_by_value_ref_arg( + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let tag = values.type_tag(value)?; + match tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = values.raw_value_word(value)?; + values.raw_word_value(tag, word) + } + EVAL_TAG_STRING => { + let bytes = values.string_bytes(value)?; + values.string_bytes_value(&bytes) + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC => values.array_clone_shallow(value), + EVAL_TAG_OBJECT => { + let word = values.raw_value_word(value)?; + let retained = values.retain_raw_heap_word(word)?; + values.raw_heap_word_value(retained) + } + EVAL_TAG_NULL => values.null(), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Returns the native callable variadic slot, if metadata registered one. +pub(super) fn native_callable_variadic_index(signature: &NativeCallableSignature) -> Option { + (0..signature.param_count()).find(|index| signature.param_variadic(*index)) +} + +/// Binds one positional native AOT argument to a fixed slot or variadic array. +pub(super) fn bind_native_positional_signature_arg( + signature: &NativeCallableSignature, + bound_args: &mut [Option], + variadic_index: Option, + next_positional: &mut usize, + next_variadic_index: &mut i64, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if variadic_index.is_some_and(|index| *next_positional >= index) { + let key = values.int(*next_variadic_index)?; + *next_variadic_index = next_variadic_index + .checked_add(1) + .ok_or(EvalStatus::RuntimeFatal)?; + let ref_target = + native_parameter_ref_target(signature, variadic_index, ref_target, by_ref_mode, values)?; + return bind_native_variadic_arg(bound_args, variadic_index, key, value, ref_target, values); + } + let param_index = *next_positional; + if param_index >= bound_args.len() || bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = + native_parameter_ref_target(signature, Some(param_index), ref_target, by_ref_mode, values)?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + *next_positional += 1; + Ok(()) +} + +/// Binds one named native AOT argument to a fixed non-variadic slot. +pub(super) fn bind_native_named_signature_arg( + signature: &NativeCallableSignature, + variadic_index: Option, + bound_args: &mut [Option], + name: &str, + value: RuntimeCellHandle, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(param_index) = native_regular_param_index(signature, variadic_index, name) { + if bound_args[param_index].is_some() { + return Err(EvalStatus::RuntimeFatal); + } + let ref_target = native_parameter_ref_target( + signature, + Some(param_index), + ref_target, + by_ref_mode, + values, + )?; + bound_args[param_index] = Some(BoundMethodArg { + value, + ref_target, + variadic_ref_targets: Vec::new(), + }); + return Ok(()); + } + Err(EvalStatus::RuntimeFatal) +} + +/// Returns the caller writeback target required by a native by-reference parameter. +pub(super) fn native_parameter_ref_target( + signature: &NativeCallableSignature, + param_index: Option, + ref_target: Option, + by_ref_mode: EvalByRefBindingMode<'_>, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(param_index) = param_index else { + return Ok(None); + }; + if !signature.param_by_ref(param_index) { + return Ok(None); + } + if let Some(ref_target) = ref_target { + return Ok(Some(ref_target)); + } + match by_ref_mode { + EvalByRefBindingMode::RequireTarget => Err(EvalStatus::RuntimeFatal), + EvalByRefBindingMode::WarnByValue { callable_name } => { + let param_name = native_callable_param_warning_name(signature, param_index); + values.warning(&format!( + "{callable_name}(): Argument #{} (${param_name}) must be passed by reference, value given", + param_index + 1 + ))?; + Ok(None) + } + } +} + +/// Returns the PHP parameter name used in native method by-reference warnings. +pub(super) fn native_callable_param_warning_name( + signature: &NativeCallableSignature, + param_index: usize, +) -> String { + signature + .param_names() + .get(param_index) + .filter(|name| !name.is_empty()) + .cloned() + .unwrap_or_else(|| format!("arg{}", param_index + 1)) +} + +/// Returns the matching non-variadic native parameter index for one named arg. +pub(super) fn native_regular_param_index( + signature: &NativeCallableSignature, + variadic_index: Option, + name: &str, +) -> Option { + signature + .param_names() + .iter() + .enumerate() + .position(|(index, param)| Some(index) != variadic_index && param == name) +} + +/// Appends one value into the native AOT variadic argument array. +pub(super) fn bind_native_variadic_arg( + bound_args: &mut [Option], + variadic_index: Option, + key: RuntimeCellHandle, + value: RuntimeCellHandle, + ref_target: Option, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let index = variadic_index.ok_or(EvalStatus::RuntimeFatal)?; + let bound = bound_args[index].as_mut().ok_or(EvalStatus::RuntimeFatal)?; + let array = values.array_set(bound.value, key, value)?; + bound.value = array; + if let Some(ref_target) = ref_target { + bound.variadic_ref_targets.push((key, ref_target)); + } + Ok(()) +} diff --git a/crates/elephc-magician/src/interpreter/statements/native_constructor_defaults.rs b/crates/elephc-magician/src/interpreter/statements/native_constructor_defaults.rs new file mode 100644 index 0000000000..fb6a5153b6 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/native_constructor_defaults.rs @@ -0,0 +1,251 @@ +//! Purpose: +//! Resolves AOT method metadata, invokes native constructors, and materializes defaults. +//! +//! Called from: +//! - Native class construction and signature binding. +//! +//! Key details: +//! - Parent metadata lookup, constructor visibility, and compound default ownership stay centralized. + +use super::*; + +/// Finds generated/AOT method metadata on a class or its native parent chain. +pub(in crate::interpreter) fn eval_aot_method_dispatch_metadata_in_hierarchy( + class_name: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let mut current = class_name.trim_start_matches('\\').to_string(); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return Ok(None); + } + if let Some(metadata) = eval_aot_method_dispatch_metadata(¤t, method_name, values)? { + return Ok(Some(metadata)); + } + let Some(parent) = context.native_class_parent(¤t) else { + return Ok(None); + }; + current = parent.to_string(); + } +} + +/// Runs one generated/AOT constructor after native signature binding. +pub(in crate::interpreter) fn eval_native_constructor_with_evaluated_args( + class_name: &str, + object: RuntimeCellHandle, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + eval_native_constructor_with_evaluated_args_and_ref_mode( + class_name, + object, + evaluated_args, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Runs one generated/AOT constructor with caller-selected by-ref binding behavior. +pub(super) fn eval_native_constructor_with_evaluated_args_and_ref_mode( + class_name: &str, + object: RuntimeCellHandle, + evaluated_args: Vec, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(message) = eval_native_constructor_access_error(class_name, context, values)? { + return eval_throw_error(&message, context, values); + } + let bridge_scope = + eval_native_constructor_bridge_scope(class_name, context, values)?; + let signature = context.native_constructor_signature(class_name); + let bound_args = bind_native_callable_bound_args_with_mode( + signature, + evaluated_args, + by_ref_mode, + context, + values, + )?; + let result = if let Some(scope) = bridge_scope.as_deref() { + eval_with_native_bridge_scope(scope, context, || { + values.construct_object(object, native_bound_arg_values(&bound_args)) + }) + } else { + values.construct_object(object, native_bound_arg_values(&bound_args)) + }; + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(()), Ok(())) => Ok(()), + } +} + +/// Returns the generated/AOT constructor scope that the runtime bridge can recognize. +pub(super) fn eval_native_constructor_bridge_scope( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility)) = + eval_reflection_aot_non_public_constructor(class_name, values)? + else { + return Ok(None); + }; + if eval_native_constructor_access_allowed(&declaring_class, visibility, context) { + Ok(Some(declaring_class)) + } else { + Ok(None) + } +} + +/// Returns PHP's constructor access error for generated/AOT constructors, if inaccessible. +pub(super) fn eval_native_constructor_access_error( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility)) = + eval_reflection_aot_non_public_constructor(class_name, values)? + else { + return Ok(None); + }; + if eval_native_constructor_access_allowed(&declaring_class, visibility, context) { + return Ok(None); + } + Ok(Some(format!( + "Call to {} {}::__construct() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + eval_native_constructor_scope_label(context) + ))) +} + +/// Returns whether the current eval scope may call one generated/AOT constructor. +pub(super) fn eval_native_constructor_access_allowed( + declaring_class: &str, + visibility: EvalVisibility, + context: &ElephcEvalContext, +) -> bool { + match visibility { + EvalVisibility::Public => true, + EvalVisibility::Private => context + .current_class_scope() + .is_some_and(|current| same_eval_class_name(current, declaring_class)), + EvalVisibility::Protected => context + .current_class_scope() + .is_some_and(|current| eval_classes_are_related(current, declaring_class, context)), + } +} + +/// Returns PHP's scope phrase for constructor access diagnostics. +pub(super) fn eval_native_constructor_scope_label(context: &ElephcEvalContext) -> String { + context.current_class_scope().map_or_else( + || String::from("global scope"), + |class_name| format!("scope {}", class_name.trim_start_matches('\\')), + ) +} + +/// Returns PHP's lowercase visibility label. +pub(super) fn eval_visibility_label(visibility: EvalVisibility) -> &'static str { + match visibility { + EvalVisibility::Public => "public", + EvalVisibility::Protected => "protected", + EvalVisibility::Private => "private", + } +} + +/// Allocates a fresh runtime cell for one invocation-safe native AOT default. +pub(in crate::interpreter) fn materialize_native_callable_default( + default: &NativeCallableDefault, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match default { + NativeCallableDefault::Null => values.null(), + NativeCallableDefault::Bool(value) => values.bool_value(*value), + NativeCallableDefault::Int(value) => values.int(*value), + NativeCallableDefault::Float(value) => values.float(*value), + NativeCallableDefault::String(value) => values.string(value), + NativeCallableDefault::EmptyArray => values.array_new(0), + NativeCallableDefault::Array(elements) => { + materialize_native_callable_array_default(elements, context, values) + } + NativeCallableDefault::Object { class_name, args } => { + materialize_native_callable_object_default(class_name, args, context, values) + } + } +} + +/// Allocates one array-valued native AOT parameter default with fresh element cells. +pub(super) fn materialize_native_callable_array_default( + elements: &[NativeCallableArrayDefaultElement], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let has_string_key = elements.iter().any(|element| { + matches!( + element.key, + Some(NativeCallableArrayDefaultKey::String(_)) + ) + }); + let mut array = if has_string_key { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; + let mut next_auto_key = 0; + for element in elements { + let key = match &element.key { + Some(NativeCallableArrayDefaultKey::Int(value)) => { + if *value >= next_auto_key { + next_auto_key = value.saturating_add(1); + } + values.int(*value)? + } + Some(NativeCallableArrayDefaultKey::String(value)) => values.string(value)?, + None => { + let key = values.int(next_auto_key)?; + next_auto_key = next_auto_key.saturating_add(1); + key + } + }; + let value = materialize_native_callable_default(&element.value, context, values)?; + array = values.array_set(array, key, value)?; + } + Ok(array) +} + +/// Allocates and constructs one object-valued native AOT parameter default. +pub(super) fn materialize_native_callable_object_default( + class_name: &str, + args: &[NativeCallableObjectDefaultArg], + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let object = values.new_object(class_name)?; + let mut constructor_args = Vec::with_capacity(args.len()); + for arg in args { + constructor_args.push(EvaluatedCallArg { + name: arg.name.clone(), + value: materialize_native_callable_default(&arg.value, context, values)?, + ref_target: None, + }); + } + if let Err(err) = eval_native_constructor_with_evaluated_args( + class_name, + object, + constructor_args, + context, + values, + ) { + let _ = values.release(object); + return Err(err); + } + Ok(object) +} diff --git a/crates/elephc-magician/src/interpreter/statements/native_method_execution.rs b/crates/elephc-magician/src/interpreter/statements/native_method_execution.rs new file mode 100644 index 0000000000..7cad8b8dfc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/native_method_execution.rs @@ -0,0 +1,480 @@ +//! Purpose: +//! Executes generated/AOT instance and static methods through native bridge scopes. +//! +//! Called from: +//! - Method, static-method, Reflection, and call_user_func dispatch. +//! +//! Key details: +//! - Checked and unchecked entry points share binding, bridge scope, and reference-mode handling. + +use super::*; + +/// Calls one generated/AOT instance method after native signature binding. +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + None, + None, + context, + values, + ) +} + +/// Calls one generated/AOT instance method after validation with an optional bridge scope. +pub(super) fn eval_native_method_with_evaluated_args_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut resolved_bridge_scope = bridge_scope.map(str::to_string); + if resolved_bridge_scope.is_none() { + if let Some(shadow_scope) = + eval_private_scope_shadow_bridge_scope(class_name, method_name, context, values)? + { + // The calling scope's own private method shadows any override on + // the receiver's class; access is inherently allowed, so skip the + // hierarchy resolution (it would find the override instead). + return eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&shadow_scope), + called_class_scope, + context, + values, + ); + } + } + let metadata = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; + if let Some((declaring_class, visibility, _, is_abstract)) = metadata { + if resolved_bridge_scope.is_none() { + resolved_bridge_scope = Some(declaring_class.clone()); + } + if !is_abstract + && validate_eval_member_access(&declaring_class, visibility, context).is_err() + { + if eval_native_instance_magic_method_available(class_name, context, values)? { + return eval_native_magic_instance_method_call( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + } else if eval_native_instance_magic_method_available(class_name, context, values)? { + return eval_native_magic_instance_method_call( + object, + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + resolved_bridge_scope.as_deref(), + called_class_scope, + context, + values, + ) +} + +/// Calls one generated/AOT instance method without enforcing member visibility. +pub(super) fn eval_native_method_with_evaluated_args_unchecked( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + None, + None, + context, + values, + ) +} + +/// Calls one generated/AOT instance method without visibility checks using an optional bridge scope. +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_unchecked_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object, + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Calls one generated/AOT instance method for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn eval_native_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let callable_name = format!("{}::{}", signature_owner.trim_start_matches('\\'), method_name); + eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object, + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) +} + +/// Calls one generated/AOT instance method with a selected by-reference binding mode. +pub(super) fn eval_native_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let signature = context.native_method_signature(signature_owner, method_name); + let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); + let bound_args = + bind_native_callable_bound_args_with_mode(signature, evaluated_args, by_ref_mode, context, values)?; + let result = if let Some(scope) = bridge_scope { + eval_native_method_call_with_scope( + scope, + called_class_scope, + object, + method_name, + native_bound_arg_values(&bound_args), + context, + values, + ) + } else { + values.method_call(object, method_name, native_bound_arg_values(&bound_args)) + }; + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => eval_declared_native_return_value( + return_type.as_ref(), + Some(signature_owner), + called_class_scope.or(Some(class_name)), + result, + context, + values, + ), + } +} + +/// Calls one generated/AOT static method after native signature binding. +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_bridge_scope( + class_name, + method_name, + evaluated_args, + None, + None, + context, + values, + ) +} + +/// Calls one generated/AOT static method after validation with an optional bridge scope. +pub(super) fn eval_native_static_method_with_evaluated_args_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut resolved_bridge_scope = bridge_scope.map(str::to_string); + let metadata = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)?; + if let Some((declaring_class, visibility, is_static, is_abstract)) = metadata { + if resolved_bridge_scope.is_none() { + resolved_bridge_scope = Some(declaring_class.clone()); + } + if is_static + && !is_abstract + && validate_eval_member_access(&declaring_class, visibility, context).is_err() + { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + } else if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, + method_name, + evaluated_args, + context, + values, + ); + } + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + class_name, + method_name, + evaluated_args, + resolved_bridge_scope.as_deref(), + called_class_scope, + context, + values, + ) +} + +/// Calls one generated/AOT static method without enforcing member visibility. +pub(super) fn eval_native_static_method_with_evaluated_args_unchecked( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + class_name, + method_name, + evaluated_args, + None, + None, + context, + values, + ) +} + +/// Calls one generated/AOT static method without visibility checks using an optional bridge scope. +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::RequireTarget, + context, + values, + ) +} + +/// Calls one generated/AOT static method for `call_user_func()` by-value by-ref degradation. +pub(in crate::interpreter) fn eval_native_static_method_with_evaluated_args_for_call_user_func_unchecked_bridge_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let callable_name = format!("{}::{}", signature_owner.trim_start_matches('\\'), method_name); + eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name, + method_name, + evaluated_args, + bridge_scope, + called_class_scope, + EvalByRefBindingMode::WarnByValue { + callable_name: &callable_name, + }, + context, + values, + ) +} + +/// Calls one generated/AOT static method with a selected by-reference binding mode. +pub(super) fn eval_native_static_method_with_evaluated_args_unchecked_bridge_scope_with_ref_mode( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + bridge_scope: Option<&str>, + called_class_scope: Option<&str>, + by_ref_mode: EvalByRefBindingMode<'_>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let signature_owner = bridge_scope.unwrap_or(class_name); + let signature = context.native_static_method_signature(signature_owner, method_name); + let return_type = signature.as_ref().and_then(|signature| signature.return_type().cloned()); + let bound_args = + bind_native_callable_bound_args_with_mode(signature, evaluated_args, by_ref_mode, context, values)?; + let result = if let Some(scope) = bridge_scope { + eval_native_static_method_call_with_scope( + scope, + called_class_scope, + class_name, + method_name, + native_bound_arg_values(&bound_args), + context, + values, + ) + } else { + values.static_method_call(class_name, method_name, native_bound_arg_values(&bound_args)) + }; + let writeback = write_back_native_callable_ref_args(&bound_args, context, values); + match (result, writeback) { + (Err(status), _) | (_, Err(status)) => Err(status), + (Ok(result), Ok(())) => eval_declared_native_return_value( + return_type.as_ref(), + Some(signature_owner), + called_class_scope.or(Some(class_name)), + result, + context, + values, + ), + } +} + +/// Returns whether a generated/AOT class has an instance `__call()` fallback. +pub(super) fn eval_native_instance_magic_method_available( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok(eval_aot_method_dispatch_metadata_in_hierarchy(class_name, "__call", context, values)? + .is_some_and(|(_, _, is_static, is_abstract)| !is_static && !is_abstract)) +} + +/// Returns whether a generated/AOT class has a static `__callStatic()` fallback. +pub(super) fn eval_native_static_magic_method_available( + class_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + Ok( + eval_aot_method_dispatch_metadata_in_hierarchy( + class_name, + "__callStatic", + context, + values, + )? + .is_some_and(|(_, _, is_static, is_abstract)| is_static && !is_abstract), + ) +} + +/// Dispatches a missing or inaccessible generated/AOT instance method through `__call()`. +pub(in crate::interpreter) fn eval_native_magic_instance_method_call( + object: RuntimeCellHandle, + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_native_method_with_evaluated_args_unchecked( + object, + class_name, + "__call", + magic_args, + context, + values, + ) +} + +/// Dispatches a missing or inaccessible generated/AOT static method through `__callStatic()`. +pub(in crate::interpreter) fn eval_native_magic_static_method_call( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let magic_args = eval_magic_call_args(method_name, evaluated_args, values)?; + eval_native_static_method_with_evaluated_args_unchecked( + class_name, + "__callStatic", + magic_args, + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/statements/native_static_dispatch.rs b/crates/elephc-magician/src/interpreter/statements/native_static_dispatch.rs new file mode 100644 index 0000000000..a0c8b2f455 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/native_static_dispatch.rs @@ -0,0 +1,413 @@ +//! Purpose: +//! Dispatches AOT static syntax and builtin enum/property-hook static methods. +//! +//! Called from: +//! - Static method dispatch after eval-declared targets are exhausted. +//! +//! Key details: +//! - Native receiver metadata, enum backing values, and PHP-visible errors share this boundary. + +use super::*; + +/// Dispatches one generated/AOT method reached through PHP static-call syntax. +pub(super) fn eval_native_static_syntax_method_result( + class_name: &str, + called_class_scope: Option<&str>, + method_name: &str, + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata_in_hierarchy(class_name, method_name, context, values)? + else { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_static_method_with_evaluated_args( + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + return Ok(None); + }; + if is_abstract { + return eval_throw_abstract_method_call_error( + &declaring_class, + method_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + if eval_native_static_magic_method_available(class_name, context, values)? { + return eval_native_magic_static_method_call( + class_name, + method_name, + evaluated_args, + context, + values, + ) + .map(Some); + } + return eval_throw_method_access_error( + &declaring_class, + method_name, + visibility, + context, + values, + ); + } + if !is_static { + if let Some(object) = + eval_static_syntax_instance_receiver(class_name, lexical_scope, context, values)? + { + return eval_native_method_with_evaluated_args_bridge_scope( + object, + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + called_class_scope, + context, + values, + ) + .map(Some); + } + return eval_throw_non_static_method_call_error( + &declaring_class, + method_name, + context, + values, + ); + } + eval_native_static_method_with_evaluated_args_bridge_scope( + class_name, + method_name, + evaluated_args, + Some(&declaring_class), + called_class_scope, + context, + values, + ) + .map(Some) +} + +/// Returns `$this` when PHP permits static-call syntax to target an instance method. +pub(in crate::interpreter) fn eval_static_syntax_instance_receiver( + class_name: &str, + lexical_scope: Option<&ElephcEvalScope>, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(scope) = lexical_scope else { + return Ok(None); + }; + let Some(object) = visible_scope_cell(context, scope, "this") else { + return Ok(None); + }; + if values.type_tag(object)? != EVAL_TAG_OBJECT { + return Ok(None); + } + let object_class_name = eval_static_syntax_object_class_name(object, context, values)?; + if eval_static_syntax_object_matches_class(&object_class_name, class_name, context) { + Ok(Some(object)) + } else { + Ok(None) + } +} + +/// Resolves the PHP-visible class name for the current static-syntax `$this` object. +pub(super) fn eval_static_syntax_object_class_name( + object: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Ok(identity) = values.object_identity(object) { + if let Some(class) = context.dynamic_object_class(identity) { + return Ok(class.name().to_string()); + } + } + runtime_object_class_name(object, values) +} + +/// Returns whether `$this` is an instance of the class named by static-call syntax. +pub(super) fn eval_static_syntax_object_matches_class( + object_class_name: &str, + class_name: &str, + context: &ElephcEvalContext, +) -> bool { + same_eval_class_name(object_class_name, class_name) + || context.class_is_a(object_class_name, class_name, false) + || native_class_is_a(object_class_name, class_name, context) +} + +/// Dispatches static methods for eval's builtin `PropertyHookType` enum slice. +pub(super) fn eval_builtin_property_hook_type_static_method_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("PropertyHookType") + { + return Ok(None); + } + match eval_enum_static_builtin_name(method_name) { + Some("cases") => { + eval_builtin_property_hook_type_cases(evaluated_args, context, values).map(Some) + } + Some("from") => { + eval_builtin_property_hook_type_from(evaluated_args, false, context, values).map(Some) + } + Some("tryFrom") => { + eval_builtin_property_hook_type_from(evaluated_args, true, context, values).map(Some) + } + _ => Ok(None), + } +} + +/// Builds the indexed case array for eval's builtin `PropertyHookType` enum slice. +pub(super) fn eval_builtin_property_hook_type_cases( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let case_names = ["Get", "Set"]; + let mut array = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let key = values.int(index as i64)?; + let case = + eval_builtin_property_hook_type_case("PropertyHookType", case_name, context, values)? + .ok_or(EvalStatus::RuntimeFatal)?; + array = values.array_set(array, key, case)?; + } + Ok(array) +} + +/// Evaluates builtin `PropertyHookType::from()` or `tryFrom()` inside eval. +pub(super) fn eval_builtin_property_hook_type_from( + evaluated_args: Vec, + nullable_miss: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let mut args = bind_evaluated_function_args(&[String::from("value")], evaluated_args)?; + let value = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + let bytes = values.string_bytes(value)?; + let value_text = String::from_utf8_lossy(&bytes); + for constant_name in ["Get", "Set"] { + let Some((_, case_value)) = eval_property_hook_type_case_parts(constant_name) else { + continue; + }; + if value_text == case_value { + return eval_builtin_property_hook_type_case( + "PropertyHookType", + constant_name, + context, + values, + )? + .ok_or(EvalStatus::RuntimeFatal); + } + } + if nullable_miss { + values.null() + } else { + let message = eval_enum_invalid_backing_value_message( + "PropertyHookType", + EvalEnumBackingType::String, + value, + values, + )?; + eval_throw_value_error(&message, context, values) + } +} + +/// Returns a recognized enum-provided static method name. +pub(super) fn eval_enum_static_builtin_name(method_name: &str) -> Option<&'static str> { + if method_name.eq_ignore_ascii_case("cases") { + Some("cases") + } else if method_name.eq_ignore_ascii_case("from") { + Some("from") + } else if method_name.eq_ignore_ascii_case("tryFrom") { + Some("tryFrom") + } else { + None + } +} + +/// Returns a synthetic enum method only when that enum actually provides it. +pub(in crate::interpreter) fn eval_enum_static_builtin_applies( + enum_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<&'static str> { + let enum_decl = context.enum_decl(enum_name)?; + match eval_enum_static_builtin_name(method_name)? { + "cases" => Some("cases"), + "from" if enum_decl.backing_type().is_some() => Some("from"), + "tryFrom" if enum_decl.backing_type().is_some() => Some("tryFrom"), + _ => None, + } +} + +/// Dispatches enum-provided static methods for eval-declared enums. +pub(in crate::interpreter) fn eval_enum_builtin_static_method_result( + enum_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + match eval_enum_static_builtin_name(method_name).ok_or(EvalStatus::RuntimeFatal)? { + "cases" => eval_enum_cases_result(enum_name, evaluated_args, context, values), + "from" => eval_enum_from_result(enum_name, evaluated_args, false, context, values), + "tryFrom" => eval_enum_from_result(enum_name, evaluated_args, true, context, values), + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Builds the indexed array returned by `EnumName::cases()`. +pub(super) fn eval_enum_cases_result( + enum_name: &str, + evaluated_args: Vec, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let enum_decl = context + .enum_decl(enum_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + let mut array = values.array_new(case_names.len())?; + for (index, case_name) in case_names.iter().enumerate() { + let key = values.int(index as i64)?; + let case = context + .enum_case(enum_name, case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + array = values.array_set(array, key, case)?; + } + Ok(array) +} + +/// Evaluates `EnumName::from()` or `EnumName::tryFrom()` for eval-backed enums. +pub(super) fn eval_enum_from_result( + enum_name: &str, + evaluated_args: Vec, + nullable_miss: bool, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let enum_decl = context + .enum_decl(enum_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let backing_type = enum_decl.backing_type().ok_or(EvalStatus::RuntimeFatal)?; + let enum_display_name = enum_decl.name().trim_start_matches('\\').to_string(); + let case_names = enum_decl + .cases() + .iter() + .map(|case| case.name().to_string()) + .collect::>(); + let mut args = bind_evaluated_function_args(&[String::from("value")], evaluated_args)?; + let value = args.pop().ok_or(EvalStatus::RuntimeFatal)?; + for case_name in case_names { + let case_value = context + .enum_case_value(enum_name, &case_name) + .ok_or(EvalStatus::RuntimeFatal)?; + let equal = values.compare(EvalBinOp::StrictEq, value, case_value)?; + if values.truthy(equal)? { + return context + .enum_case(enum_name, &case_name) + .ok_or(EvalStatus::RuntimeFatal); + } + } + if nullable_miss { + values.null() + } else { + let message = eval_enum_invalid_backing_value_message( + &enum_display_name, + backing_type, + value, + values, + )?; + eval_throw_value_error(&message, context, values) + } +} + +/// Builds PHP's backed-enum `ValueError` message for an unmatched enum value. +pub(super) fn eval_enum_invalid_backing_value_message( + enum_name: &str, + backing_type: EvalEnumBackingType, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let bytes = values.string_bytes(value)?; + let value = String::from_utf8_lossy(&bytes); + let value = match backing_type { + EvalEnumBackingType::Int => value.into_owned(), + EvalEnumBackingType::String => format!("\"{}\"", value), + }; + Ok(format!( + "{} is not a valid backing value for enum {}", + value, enum_name + )) +} + +/// Creates and schedules a `ValueError` through eval's normal Throwable channel. +pub(super) fn eval_throw_value_error( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let exception = values.new_object("ValueError")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Creates and schedules a `ReflectionException` through eval's normal Throwable channel. +pub(super) fn eval_throw_reflection_exception( + message: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let exception = values.new_object("ReflectionException")?; + let message = values.string(message)?; + let code = values.int(0)?; + values.construct_object(exception, vec![message, code])?; + context.set_pending_throw(exception); + Err(EvalStatus::UncaughtThrowable) +} + +/// Schedules the Throwable category required by one ReflectionClass instantiation error. +pub(super) fn eval_throw_reflection_instantiation_error( + error: EvalReflectionInstantiationError, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + match error { + EvalReflectionInstantiationError::ThrowableError(message) => { + eval_throw_error(&message, context, values) + } + EvalReflectionInstantiationError::ReflectionException(message) => { + eval_throw_reflection_exception(&message, context, values) + } + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/property_constant_validation.rs b/crates/elephc-magician/src/interpreter/statements/property_constant_validation.rs new file mode 100644 index 0000000000..6fc296e6f4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/property_constant_validation.rs @@ -0,0 +1,371 @@ +//! Purpose: +//! Validates property and class-constant declarations against inherited contracts. +//! +//! Called from: +//! - Eval class and interface declaration validation. +//! +//! Key details: +//! - Readonly, asymmetric visibility, hook parameter types, and AOT redeclarations are checked here. + +use super::*; + +/// Validates property declarations that can be checked before class registration. +pub(super) fn validate_eval_declared_properties( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let mut names = std::collections::HashSet::new(); + for property in class.properties() { + validate_eval_non_method_attribute_targets(property.attributes())?; + if !names.insert(property.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_abstract() + && (!class.is_abstract() + || property.is_static() + || property.is_final() + || property.is_readonly() + || property.default().is_some() + || (!property.requires_get_hook() && !property.requires_set_hook())) + { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_static() && property.is_readonly() { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(set_visibility) = property.set_visibility() { + if property.is_static() || property.property_type().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if property_visibility_rank(set_visibility) + > property_visibility_rank(property.visibility()) + { + return Err(EvalStatus::RuntimeFatal); + } + } + if property.is_final() && property.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_readonly() && property.property_type().is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if property.is_readonly() && property.default().is_some() { + return Err(EvalStatus::RuntimeFatal); + } + if (property.has_get_hook() || property.has_set_hook()) + && (property.is_static() || property.is_readonly() || property.default().is_some()) + { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_property_set_hook_parameter_type(class, property, context)?; + } + Ok(()) +} + +/// Validates that an explicit set-hook parameter type can accept every property value. +pub(super) fn validate_eval_property_set_hook_parameter_type( + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + let Some(set_hook_type) = property.set_hook_type() else { + return Ok(()); + }; + let Some(property_type) = property.property_type() else { + return Err(EvalStatus::RuntimeFatal); + }; + let set_hook_types = vec![Some(set_hook_type.clone())]; + let property_types = vec![Some(property_type.clone())]; + method_parameter_type_signature_accepts( + &set_hook_types, + &[], + class.name(), + &property_types, + &[], + class.name(), + 1, + Some(class), + context, + ) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Validates one property declaration against inherited eval property metadata. +pub(super) fn validate_property_parent_redeclaration( + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let Some(parent) = class.parent() else { + return Ok(()); + }; + if let Some((parent_declaring_class, parent_property)) = + context.class_property(parent, property.name()) + { + if parent_property.visibility() == EvalVisibility::Private { + return Ok(()); + } + if parent_property.is_final() + || parent_property.set_visibility() == Some(EvalVisibility::Private) + { + return Err(EvalStatus::RuntimeFatal); + } + if parent_property.is_static() != property.is_static() + || (parent_property.is_readonly() && !property.is_readonly()) + || property_visibility_rank(property.visibility()) + < property_visibility_rank(parent_property.visibility()) + || property_visibility_rank(property.write_visibility()) + < property_visibility_rank(parent_property.write_visibility()) + || !property_type_signature_matches( + property.property_type(), + class.name(), + parent_property.property_type(), + &parent_declaring_class, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + return Ok(()); + } + validate_property_aot_parent_redeclaration(parent, class, property, context, values) +} + +/// Validates one property declaration against inherited generated/AOT property metadata. +pub(super) fn validate_property_aot_parent_redeclaration( + parent: &str, + class: &EvalClass, + property: &EvalClassProperty, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if context.has_class(parent) || !values.class_exists(parent)? { + return Ok(()); + } + let parent = parent.trim_start_matches('\\'); + let Some(flags) = values.reflection_property_flags(parent, property.name())? else { + return Ok(()); + }; + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + return Ok(()); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 + || flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 + { + return Err(EvalStatus::RuntimeFatal); + } + let parent_is_static = flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC != 0; + let parent_visibility = eval_aot_property_visibility(flags); + let parent_write_visibility = eval_aot_property_write_visibility(flags, parent_visibility); + let parent_is_readonly = flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY != 0; + let parent_declaring_class = + eval_aot_property_declaring_class(parent, property.name(), values)?; + let parent_property_type = context + .native_property_type(&parent_declaring_class, property.name()) + .or_else(|| context.native_property_type(parent, property.name())); + if parent_is_static != property.is_static() + || (parent_is_readonly && !property.is_readonly()) + || property_visibility_rank(property.visibility()) + < property_visibility_rank(parent_visibility) + || property_visibility_rank(property.write_visibility()) + < property_visibility_rank(parent_write_visibility) + || !property_type_signature_matches( + property.property_type(), + class.name(), + parent_property_type.as_ref(), + &parent_declaring_class, + Some(class), + context, + ) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the eval visibility represented by generated/AOT property reflection flags. +pub(super) fn eval_aot_property_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + +/// Returns the eval write visibility represented by generated/AOT property flags. +pub(super) fn eval_aot_property_write_visibility( + flags: u64, + read_visibility: EvalVisibility, +) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE_SET != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED_SET != 0 { + EvalVisibility::Protected + } else { + read_visibility + } +} + +/// Returns the generated/AOT declaring class for one reflected property. +pub(super) fn eval_aot_property_declaring_class( + class_name: &str, + property_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result { + values + .reflection_property_declaring_class(class_name, property_name) + .map(|declaring_class| declaring_class.unwrap_or_else(|| class_name.to_string())) +} + +/// Validates constant declarations that can be checked before registration. +pub(super) fn validate_eval_declared_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { + let mut names = std::collections::HashSet::new(); + for constant in constants { + validate_eval_non_method_attribute_targets(constant.attributes())?; + if !names.insert(constant.name().to_string()) { + return Err(EvalStatus::RuntimeFatal); + } + if constant.is_final() && constant.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates declarations that are specific to PHP interface constants. +pub(super) fn validate_eval_interface_constants(constants: &[EvalClassConstant]) -> Result<(), EvalStatus> { + for constant in constants { + if constant.visibility() != EvalVisibility::Public { + return Err(EvalStatus::RuntimeFatal); + } + } + Ok(()) +} + +/// Validates interface constants against inherited parent-interface constants. +pub(super) fn validate_interface_constant_parent_redeclarations( + interface: &EvalInterface, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for constant in interface.constants() { + for parent in interface.parents() { + if let Some((_, parent_constant)) = context.interface_constant(parent, constant.name()) + { + if parent_constant.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_aot_interface_constant_redeclaration(parent, constant, values)?; + } + } + Ok(()) +} + +/// Validates one constant declaration against inherited eval constant metadata. +pub(super) fn validate_constant_parent_redeclaration( + class: &EvalClass, + constant: &EvalClassConstant, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if let Some(parent) = class.parent() { + if let Some((_, parent_constant)) = context.class_constant(parent, constant.name()) { + if parent_constant.visibility() != EvalVisibility::Private + && (parent_constant.is_final() + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(parent_constant.visibility())) + { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_aot_class_constant_redeclaration(parent, constant, values)?; + } + for interface in pending_class_interface_names(class, context) { + if let Some((_, interface_constant)) = + context.interface_constant(&interface, constant.name()) + { + if interface_constant.is_final() + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(interface_constant.visibility()) + { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_aot_interface_constant_redeclaration(&interface, constant, values)?; + } + Ok(()) +} + +/// Validates a class constant redeclaration against a generated/AOT parent class constant. +pub(super) fn validate_aot_class_constant_redeclaration( + parent: &str, + constant: &EvalClassConstant, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.class_exists(parent)? { + return Ok(()); + } + validate_aot_constant_redeclaration(parent, constant, false, values) +} + +/// Validates a class/interface constant redeclaration against a generated/AOT interface constant. +pub(super) fn validate_aot_interface_constant_redeclaration( + interface: &str, + constant: &EvalClassConstant, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if !values.interface_exists(interface)? { + return Ok(()); + } + validate_aot_constant_redeclaration(interface, constant, true, values) +} + +/// Applies PHP redeclaration rules to one generated/AOT constant metadata row. +pub(super) fn validate_aot_constant_redeclaration( + class_like: &str, + constant: &EvalClassConstant, + interface_context: bool, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_like = class_like.trim_start_matches('\\'); + let Some(flags) = values.reflection_constant_flags(class_like, constant.name())? else { + return Ok(()); + }; + let inherited_visibility = eval_aot_constant_visibility(flags); + if !interface_context && inherited_visibility == EvalVisibility::Private { + return Ok(()); + } + if flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL != 0 + || constant_visibility_rank(constant.visibility()) + < constant_visibility_rank(inherited_visibility) + { + return Err(EvalStatus::RuntimeFatal); + } + Ok(()) +} + +/// Returns the eval visibility represented by generated/AOT constant reflection flags. +pub(super) fn eval_aot_constant_visibility(flags: u64) -> EvalVisibility { + if flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE != 0 { + EvalVisibility::Private + } else if flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED != 0 { + EvalVisibility::Protected + } else { + EvalVisibility::Public + } +} + +/// Returns a comparable rank where larger means less restrictive constant visibility. +pub(super) fn constant_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/property_validation.rs b/crates/elephc-magician/src/interpreter/statements/property_validation.rs new file mode 100644 index 0000000000..14cef80d84 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/property_validation.rs @@ -0,0 +1,443 @@ +//! Purpose: +//! Validates property-hook context, readonly writes, visibility, and property failures. +//! +//! Called from: +//! - Instance and static property access paths. +//! +//! Key details: +//! - Synthetic hook names and PHP-visible access errors stay centralized. + +use super::*; + +/// Returns the synthetic get-hook method name for one property. +pub(in crate::interpreter) fn property_hook_get_method(property_name: &str) -> String { + format!("__propget_{property_name}") +} + +/// Returns the synthetic set-hook method name for one property. +pub(in crate::interpreter) fn property_hook_set_method(property_name: &str) -> String { + format!("__propset_{property_name}") +} + +/// Rejects writes to readonly eval-declared properties outside their declaring constructor. +pub(super) fn validate_eval_readonly_property_write( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if !property.is_readonly() { + return Ok(()); + } + current_eval_method_is_declaring_constructor(declaring_class, context) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns true while executing `__construct` for the property declaring class. +pub(super) fn current_eval_method_is_declaring_constructor( + declaring_class: &str, + context: &ElephcEvalContext, +) -> bool { + let Some(current_class) = context.current_class_scope() else { + return false; + }; + if !same_eval_class_name(current_class, declaring_class) { + return false; + } + context + .current_function() + .and_then(|function| function.rsplit_once("::")) + .is_some_and(|(_, method)| method.eq_ignore_ascii_case("__construct")) +} + +/// Resolves the property metadata visible from the current class scope, if any. +pub(super) fn eval_dynamic_property_for_access( + object_class_name: &str, + property_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassProperty)> { + if let Some(current_class) = context.current_class_scope() { + if context.class_is_a(object_class_name, current_class, false) { + if let Some((declaring_class, property)) = + context.class_own_property(current_class, property_name) + { + if property.visibility() == EvalVisibility::Private { + return Some((declaring_class, property)); + } + } + } + } + context.class_property(object_class_name, property_name) +} + +/// Returns the physical storage name for an eval object property slot. +pub(in crate::interpreter) fn eval_instance_property_storage_name( + declaring_class: &str, + property: &EvalClassProperty, +) -> String { + if property.visibility() == EvalVisibility::Private { + format!( + "\0{}\0{}", + declaring_class.trim_start_matches('\\'), + property.name() + ) + } else { + property.name().to_string() + } +} + +/// Validates the visibility that applies to property writes, including asymmetric `set` visibility. +pub(super) fn validate_eval_property_write_access( + declaring_class: &str, + property: &EvalClassProperty, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + validate_eval_member_access(declaring_class, property.write_visibility(), context) +} + +/// Throws PHP's inaccessible property error for eval-declared properties. +pub(super) fn eval_throw_property_access_error( + declaring_class: &str, + property_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot access {} property {}::${}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's write access error for eval-declared properties. +pub(super) fn eval_throw_property_write_access_error( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(set_visibility) = property.set_visibility() { + return eval_throw_error( + &format!( + "Cannot modify {}(set) property {}::${} from {}", + eval_visibility_label(set_visibility), + declaring_class.trim_start_matches('\\'), + property.name(), + eval_native_constructor_scope_label(context) + ), + context, + values, + ); + } + eval_throw_property_access_error( + declaring_class, + property.name(), + property.write_visibility(), + context, + values, + ) +} + +/// Throws PHP's unset access error for asymmetric eval-declared properties. +pub(super) fn eval_throw_property_unset_access_error( + declaring_class: &str, + property: &EvalClassProperty, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(set_visibility) = property.set_visibility() { + return eval_throw_error( + &format!( + "Cannot unset {}(set) property {}::${} from {}", + eval_visibility_label(set_visibility), + declaring_class.trim_start_matches('\\'), + property.name(), + eval_native_constructor_scope_label(context) + ), + context, + values, + ); + } + eval_throw_property_access_error( + declaring_class, + property.name(), + property.write_visibility(), + context, + values, + ) +} + +/// Throws PHP's read-only property-hook write error. +pub(super) fn eval_throw_property_hook_readonly_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Property {}::${} is read-only", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's readonly property assignment error for eval-declared properties. +pub(super) fn eval_throw_readonly_property_modification_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot modify readonly property {}::${}", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's readonly property unset error for eval-declared properties. +pub(super) fn eval_throw_readonly_property_unset_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot unset readonly property {}::${}", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's dynamic property creation error for readonly eval-declared classes. +pub(super) fn eval_throw_dynamic_property_creation_error( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot create dynamic property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's undeclared static property error for static property access. +pub(super) fn eval_throw_undeclared_static_property_error( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Access to undeclared static property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's uninitialized typed instance property error. +pub(super) fn eval_throw_uninitialized_property_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Typed property {}::${} must not be accessed before initialization", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's uninitialized typed static property error. +pub(super) fn eval_throw_uninitialized_static_property_error( + declaring_class: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Typed static property {}::${} must not be accessed before initialization", + declaring_class.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Throws PHP's class-not-found error for unresolved static member receivers. +pub(in crate::interpreter) fn eval_throw_class_not_found_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!("Class \"{}\" not found", class_name.trim_start_matches('\\')), + context, + values, + ) +} + +/// Throws PHP's inaccessible constant error for eval-declared class constants. +pub(super) fn eval_throw_constant_access_error( + declaring_class: &str, + constant_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot access {} constant {}::{}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + constant_name + ), + context, + values, + ) +} + +/// Throws PHP's inaccessible method error for eval-declared methods. +pub(super) fn eval_throw_method_access_error( + declaring_class: &str, + method_name: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} method {}::{}() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + method_name, + eval_native_constructor_scope_label(context) + ), + context, + values, + ) +} + +/// Throws PHP's inaccessible clone-expression error for `__clone()` hooks. +pub(super) fn eval_throw_clone_access_error( + declaring_class: &str, + visibility: EvalVisibility, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to {} {}::__clone() from {}", + eval_visibility_label(visibility), + declaring_class.trim_start_matches('\\'), + eval_native_constructor_scope_label(context) + ), + context, + values, + ) +} + +/// Throws PHP's error for calling an instance method through static syntax. +pub(super) fn eval_throw_non_static_method_call_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Non-static method {}::{}() cannot be called statically", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's error for calling an abstract method directly. +pub(super) fn eval_throw_abstract_method_call_error( + declaring_class: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Cannot call abstract method {}::{}()", + declaring_class.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's undefined method error after static magic fallback misses. +pub(super) fn eval_throw_undefined_method_call_error( + class_name: &str, + method_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Call to undefined method {}::{}()", + class_name.trim_start_matches('\\'), + method_name + ), + context, + values, + ) +} + +/// Throws PHP's error for invoking an object without `__invoke()`. +pub(in crate::interpreter) fn eval_throw_object_not_callable_error( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + eval_throw_error( + &format!( + "Object of type {} is not callable", + class_name.trim_start_matches('\\') + ), + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/statements/reference_writeback.rs b/crates/elephc-magician/src/interpreter/statements/reference_writeback.rs new file mode 100644 index 0000000000..6f5a12cd42 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/reference_writeback.rs @@ -0,0 +1,509 @@ +//! Purpose: +//! Binds and writes back method by-reference targets across runtime storage shapes. +//! +//! Called from: +//! - Eval and native method invocation after argument binding. +//! +//! Key details: +//! - Invoker slots, arrays, nested arrays, object properties, and static properties preserve aliases. + +use super::*; + +/// Returns the calling-scope class when PHP's private-method shadowing rule +/// selects it as the dispatch scope: `$obj->m()` from class scope S calls S's +/// own private instance method `m` — never the receiver's override — when S +/// declares one and the receiver is an instance of S. The returned scope +/// string drives the native bridge's hidden private shadow slot directly. +pub(in crate::interpreter) fn eval_private_scope_shadow_bridge_scope( + receiver_class: &str, + method_name: &str, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(scope_class) = context.current_class_scope() else { + return Ok(None); + }; + let scope_class = scope_class.to_string(); + if !same_eval_class_name(receiver_class, &scope_class) + && !native_class_is_a(receiver_class, &scope_class, context) + { + return Ok(None); + } + let Some((declaring_class, visibility, is_static, is_abstract)) = + eval_aot_method_dispatch_metadata(&scope_class, method_name, values)? + else { + return Ok(None); + }; + if visibility == EvalVisibility::Private + && !is_static + && !is_abstract + && same_eval_class_name(&declaring_class, &scope_class) + { + Ok(Some(declaring_class)) + } else { + Ok(None) + } +} + +/// Binds method parameters into a fresh method scope and marks by-reference params as aliases. +pub(in crate::interpreter) fn bind_method_scope_args( + method_scope: &mut ElephcEvalScope, + params: &[String], + parameter_is_by_ref: &[bool], + bound_args: &[BoundMethodArg], +) { + for (position, (name, bound_arg)) in params.iter().zip(bound_args.iter()).enumerate() { + if parameter_is_by_ref.get(position).copied().unwrap_or(false) { + method_scope.set_reference( + name.clone(), + name.clone(), + bound_arg.value, + ScopeCellOwnership::Borrowed, + ); + if let Some(target) = bound_arg.ref_target.clone() { + method_scope.set_reference_target(name.clone(), target); + } + } else { + method_scope.set(name.clone(), bound_arg.value, ScopeCellOwnership::Borrowed); + } + } + alias_duplicate_method_ref_args(method_scope, params, bound_args); +} + +/// Creates local aliases when two by-reference method parameters point at the same caller variable. +pub(super) fn alias_duplicate_method_ref_args( + method_scope: &mut ElephcEvalScope, + params: &[String], + bound_args: &[BoundMethodArg], +) { + for (position, bound_arg) in bound_args.iter().enumerate() { + let Some(target) = bound_arg.ref_target.as_ref() else { + continue; + }; + let Some(param) = params.get(position) else { + continue; + }; + for previous_position in 0..position { + let Some(previous_target) = bound_args[previous_position].ref_target.as_ref() else { + continue; + }; + if !same_method_ref_target(target, previous_target) { + continue; + } + if let Some(previous_param) = params.get(previous_position) { + method_scope.set_reference( + param.clone(), + previous_param.clone(), + bound_args[previous_position].value, + ScopeCellOwnership::Borrowed, + ); + } + break; + } + } +} + +/// Returns true when two evaluated arguments target the same caller-side variable. +pub(super) fn same_method_ref_target(left: &EvalReferenceTarget, right: &EvalReferenceTarget) -> bool { + match (left, right) { + ( + EvalReferenceTarget::Variable { + scope: left_scope, + name: left_name, + }, + EvalReferenceTarget::Variable { + scope: right_scope, + name: right_name, + }, + ) => left_scope == right_scope && left_name == right_name, + ( + EvalReferenceTarget::ArrayElement { + scope: left_scope, + array_name: left_name, + index: left_index, + }, + EvalReferenceTarget::ArrayElement { + scope: right_scope, + array_name: right_name, + index: right_index, + }, + ) => left_scope == right_scope && left_name == right_name && left_index == right_index, + ( + EvalReferenceTarget::NestedArrayElement { + array_target: left_target, + index: left_index, + }, + EvalReferenceTarget::NestedArrayElement { + array_target: right_target, + index: right_index, + }, + ) => left_index == right_index && same_method_ref_target(left_target, right_target), + ( + EvalReferenceTarget::ObjectProperty { + object: left_object, + property: left_property, + access_scope: left_access_scope, + }, + EvalReferenceTarget::ObjectProperty { + object: right_object, + property: right_property, + access_scope: right_access_scope, + }, + ) => { + left_object == right_object + && left_property == right_property + && left_access_scope == right_access_scope + } + ( + EvalReferenceTarget::Cell { cell: left_cell }, + EvalReferenceTarget::Cell { cell: right_cell }, + ) => left_cell == right_cell, + ( + EvalReferenceTarget::InvokerSlot { + slot: left_slot, + source_tag: left_source_tag, + }, + EvalReferenceTarget::InvokerSlot { + slot: right_slot, + source_tag: right_source_tag, + }, + ) => left_slot == right_slot && left_source_tag == right_source_tag, + ( + EvalReferenceTarget::StaticProperty { + class_name: left_class_name, + property: left_property, + access_scope: left_access_scope, + }, + EvalReferenceTarget::StaticProperty { + class_name: right_class_name, + property: right_property, + access_scope: right_access_scope, + }, + ) => { + left_class_name == right_class_name + && left_property == right_property + && left_access_scope == right_access_scope + } + _ => false, + } +} + +/// Writes completed by-reference method parameter values back to their caller-side variables. +pub(in crate::interpreter) fn write_back_method_ref_args( + params: &[String], + bound_args: &[BoundMethodArg], + method_scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + for (position, bound_arg) in bound_args.iter().enumerate() { + let Some(param) = params.get(position) else { + continue; + }; + if let Some(target) = bound_arg.ref_target.as_ref() { + let Some(entry) = method_scope + .entry(param) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + continue; + }; + write_back_method_ref_target(target, entry.cell(), context, values)?; + } + write_back_method_variadic_ref_args(param, bound_arg, method_scope, context, values)?; + } + Ok(()) +} + +/// Writes element-level changes from a by-reference variadic method parameter back to callers. +pub(super) fn write_back_method_variadic_ref_args( + param: &str, + bound_arg: &BoundMethodArg, + method_scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if bound_arg.variadic_ref_targets.is_empty() { + return Ok(()); + } + let Some(entry) = method_scope + .entry(param) + .filter(|entry| entry.flags().is_visible() && entry.flags().by_ref) + else { + return Ok(()); + }; + if entry.cell() != bound_arg.value { + return Ok(()); + } + for (key, target) in &bound_arg.variadic_ref_targets { + let value = values.array_get(entry.cell(), *key)?; + write_back_method_ref_target(target, value, context, values)?; + } + Ok(()) +} + +/// Stores one by-reference result in the original caller-side target. +pub(in crate::interpreter) fn write_back_method_ref_target( + target: &EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match target { + EvalReferenceTarget::Variable { scope, name } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + for replaced in set_scope_cell( + context, + scope, + name.clone(), + value, + ScopeCellOwnership::Borrowed, + )? { + values.release(replaced)?; + } + Ok(()) + } + EvalReferenceTarget::ArrayElement { + scope, + array_name, + index, + } => { + let Some(scope) = (unsafe { scope.as_mut() }) else { + return Err(EvalStatus::RuntimeFatal); + }; + write_back_method_array_element_ref_target( + scope, array_name, *index, value, context, values, + ) + } + EvalReferenceTarget::NestedArrayElement { + array_target, + index, + } => write_back_method_nested_array_element_ref_target( + array_target, + *index, + value, + context, + values, + ), + EvalReferenceTarget::ObjectProperty { + object, + property, + access_scope, + } => write_back_method_object_property_ref_target( + *object, + property, + access_scope.clone(), + value, + context, + values, + ), + EvalReferenceTarget::StaticProperty { + class_name, + property, + access_scope, + } => write_back_method_static_property_ref_target( + class_name, + property, + access_scope.clone(), + value, + context, + values, + ), + EvalReferenceTarget::Cell { .. } => Ok(()), + EvalReferenceTarget::InvokerSlot { slot, source_tag } => { + write_back_invoker_slot_ref_target(*slot, *source_tag, value, values) + } + } +} + +/// Reads a value from a native descriptor-invoker by-reference slot. +pub(super) fn eval_invoker_slot_ref_target_value( + slot: usize, + source_tag: u64, + values: &mut impl RuntimeValueOps, +) -> Result { + match source_tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } + EVAL_TAG_STRING => { + let words = unsafe { *(slot as *const [u64; 2]) }; + values.raw_string_value(words[0], words[1]) + } + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + let word = unsafe { *(slot as *const u64) }; + values.raw_word_value(source_tag, word) + } + EVAL_TAG_MIXED => { + let value = unsafe { *(slot as *const RuntimeCellHandle) }; + values.retain(value) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Writes a value back into a native descriptor-invoker by-reference slot. +pub(super) fn write_back_invoker_slot_ref_target( + slot: usize, + source_tag: u64, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + match source_tag { + EVAL_TAG_INT | EVAL_TAG_FLOAT | EVAL_TAG_BOOL | EVAL_TAG_RESOURCE => { + let word = values.raw_value_word(value)?; + unsafe { + *(slot as *mut u64) = word; + } + Ok(()) + } + EVAL_TAG_STRING => write_back_invoker_string_slot(slot, value, values), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + write_back_invoker_heap_slot(slot, source_tag, value, values) + } + EVAL_TAG_MIXED => { + let retained = values.retain(value)?; + let replaced = unsafe { + let slot = slot as *mut RuntimeCellHandle; + let replaced = *slot; + *slot = retained; + replaced + }; + values.release(replaced) + } + _ => Err(EvalStatus::RuntimeFatal), + } +} + +/// Writes a boxed string value back into a native descriptor-invoker string slot. +pub(super) fn write_back_invoker_string_slot( + slot: usize, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != EVAL_TAG_STRING { + return Err(EvalStatus::RuntimeFatal); + } + let ptr = values.raw_value_word(value)?; + let len = values.raw_value_high_word(value)?; + let retained = values.retain_raw_string_words(ptr, len)?; + let replaced = unsafe { + let slot = slot as *mut [u64; 2]; + let replaced = *slot; + *slot = [retained.0, retained.1]; + replaced + }; + values.release_raw_string_words(replaced[0], replaced[1]) +} + +/// Writes a boxed heap value back into a native descriptor-invoker raw heap slot. +pub(super) fn write_back_invoker_heap_slot( + slot: usize, + source_tag: u64, + value: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if values.type_tag(value)? != source_tag { + return Err(EvalStatus::RuntimeFatal); + } + let word = values.raw_value_word(value)?; + let retained = values.retain_raw_heap_word(word)?; + let replaced = unsafe { + let slot = slot as *mut u64; + let replaced = *slot; + *slot = retained; + replaced + }; + values.release_raw_heap_word(replaced) +} + +/// Stores one by-reference method result in a caller-side array element. +pub(super) fn write_back_method_array_element_ref_target( + scope: &mut ElephcEvalScope, + array_name: &str, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let mut ownership = ScopeCellOwnership::Owned; + let array = if let Some(existing) = + scope_entry(context, scope, array_name).filter(|entry| entry.flags().is_visible()) + { + if values.is_array_like(existing.cell())? { + ownership = existing.flags().ownership; + values.array_clone_shallow(existing.cell())? + } else { + eval_new_array_for_index(index, values)? + } + } else { + eval_new_array_for_index(index, values)? + }; + let array = values.array_set(array, index, value)?; + for replaced in set_scope_cell(context, scope, array_name.to_string(), array, ownership)? { + values.release(replaced)?; + } + Ok(()) +} + +/// Stores one by-reference method result in an element of a nested caller-side array target. +pub(super) fn write_back_method_nested_array_element_ref_target( + array_target: &EvalReferenceTarget, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let current = eval_reference_target_value(array_target, context, values)?; + let array = if values.is_array_like(current)? { + values.array_clone_shallow(current)? + } else { + eval_new_array_for_index(index, values)? + }; + let array = values.array_set(array, index, value)?; + write_back_method_ref_target(array_target, array, context, values) +} + +/// Stores one by-reference method result in a caller-side object property. +pub(super) fn write_back_method_object_property_ref_target( + object: RuntimeCellHandle, + property: &str, + access_scope: ElephcEvalExecutionScope, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let previous_scope = context.replace_execution_scope(access_scope); + let result = eval_property_set_result(object, property, value, context, values); + context.replace_execution_scope(previous_scope); + result +} + +/// Stores one by-reference method result in a caller-side static property. +pub(super) fn write_back_method_static_property_ref_target( + class_name: &str, + property: &str, + access_scope: ElephcEvalExecutionScope, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let previous_scope = context.replace_execution_scope(access_scope); + let result = eval_static_property_set_result(class_name, property, value, context, values); + context.replace_execution_scope(previous_scope); + result +} + +/// Creates an indexed or associative array according to the first write key. +pub(super) fn eval_new_array_for_index( + index: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + if values.type_tag(index)? == EVAL_TAG_STRING { + values.assoc_new(1) + } else { + values.array_new(1) + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/reflection_instantiation.rs b/crates/elephc-magician/src/interpreter/statements/reflection_instantiation.rs new file mode 100644 index 0000000000..ec277218e4 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/reflection_instantiation.rs @@ -0,0 +1,384 @@ +//! Purpose: +//! Implements Reflection-driven class and attribute instantiation plus access checks. +//! +//! Called from: +//! - Method dispatch for ReflectionClass and ReflectionAttribute owners. +//! +//! Key details: +//! - Constructor visibility, without-constructor allocation, and attribute argument materialization align with PHP. + +use super::*; + +/// Returns the runtime-visible class name for a non-eval object receiver. +pub(in crate::interpreter) fn runtime_object_class_name( + object: RuntimeCellHandle, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = values.object_class_name(object)?; + let bytes = values.string_bytes(class_name); + values.release(class_name)?; + String::from_utf8(bytes?).map_err(|_| EvalStatus::RuntimeFatal) +} + +/// Instantiates the class named by a materialized eval `ReflectionClass` object. +pub(super) fn eval_reflection_class_new_instance_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let direct_new_instance = method_name.eq_ignore_ascii_case("newInstance"); + let constructor_args = if direct_new_instance { + eval_reflection_constructor_by_value_args(evaluated_args) + } else if method_name.eq_ignore_ascii_case("newInstanceArgs") { + eval_reflection_class_new_instance_args(evaluated_args, context, values)? + } else { + return Ok(None); + }; + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(message) = + eval_reflection_eval_instantiation_error_message(&reflected_name, context) + { + return eval_throw_error(&message, context, values); + } + if let Some(class) = context.class(&reflected_name).cloned() { + if let Some((_, constructor)) = context.class_method(class.name(), "__construct") { + if constructor.visibility() != EvalVisibility::Public { + return eval_throw_reflection_exception( + &format!( + "Access to non-public constructor of class {}", + class.name() + ), + context, + values, + ); + } + } + return eval_reflection_public_constructor_scope(context, values, |context, values| { + let constructor_name = + format!("{}::__construct", class.name().trim_start_matches('\\')); + let by_ref_mode = EvalByRefBindingMode::WarnByValue { + callable_name: &constructor_name, + }; + let mut scope = ElephcEvalScope::new(); + eval_dynamic_class_new_object_with_ref_mode( + &class, + constructor_args, + by_ref_mode, + context, + &mut scope, + values, + ) + .map(Some) + }); + } + let class_name = context + .resolve_class_name(&reflected_name) + .unwrap_or(reflected_name); + if let Some(error) = eval_reflection_aot_class_public_instantiation_error(&class_name, values)? + { + return eval_throw_reflection_instantiation_error(error, context, values); + } + eval_reflection_public_constructor_scope(context, values, |context, values| { + let constructor_name = format!("{}::__construct", class_name.trim_start_matches('\\')); + let by_ref_mode = EvalByRefBindingMode::WarnByValue { + callable_name: &constructor_name, + }; + let instance = values.new_object(&class_name)?; + eval_native_constructor_with_evaluated_args_and_ref_mode( + &class_name, + instance, + constructor_args, + by_ref_mode, + context, + values, + )?; + Ok(Some(instance)) + }) +} + +/// Removes caller writeback targets for ReflectionClass::newInstance() by-value forwarding. +pub(super) fn eval_reflection_constructor_by_value_args( + evaluated_args: Vec, +) -> Vec { + evaluated_args + .into_iter() + .map(|arg| EvaluatedCallArg { + name: arg.name, + value: arg.value, + ref_target: None, + }) + .collect() +} + +/// Expands the single `ReflectionClass::newInstanceArgs()` array argument. +pub(super) fn eval_reflection_class_new_instance_args( + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let args = bind_evaluated_function_args(&[String::from("args")], evaluated_args)?; + eval_array_call_arg_values(args[0], context, values) +} + +/// Runs ReflectionClass construction with only public constructor visibility. +pub(super) fn eval_reflection_public_constructor_scope( + context: &mut ElephcEvalContext, + values: &mut V, + action: impl FnOnce(&mut ElephcEvalContext, &mut V) -> Result, +) -> Result { + context.push_class_scope(String::new()); + let result = action(context, values); + context.pop_class_scope(); + result +} + +/// Allocates the class named by a materialized eval `ReflectionClass` without running `__construct()`. +pub(super) fn eval_reflection_class_new_instance_without_constructor_result( + identity: u64, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !method_name.eq_ignore_ascii_case("newInstanceWithoutConstructor") { + return Ok(None); + } + if !evaluated_args.is_empty() { + return Err(EvalStatus::RuntimeFatal); + } + let Some(reflected_name) = context + .eval_reflection_class_name(identity) + .map(str::to_string) + else { + return Ok(None); + }; + if let Some(message) = + eval_reflection_eval_instantiation_error_message(&reflected_name, context) + { + return eval_throw_error(&message, context, values); + } + if let Some(class) = context.class(&reflected_name).cloned() { + let mut scope = ElephcEvalScope::new(); + return eval_dynamic_class_allocate_object(&class, context, &mut scope, values).map(Some); + } + if context.has_interface(&reflected_name) + || context.has_trait(&reflected_name) + || context.has_enum(&reflected_name) + { + return Err(EvalStatus::RuntimeFatal); + } + let class_name = context + .resolve_class_name(&reflected_name) + .unwrap_or(reflected_name); + if let Some(message) = + eval_reflection_aot_class_without_constructor_error(&class_name, values)? + { + return eval_throw_error(&message, context, values); + } + values.new_object(&class_name).map(Some) +} + +/// Builds PHP's reflection instantiation error for eval non-instantiable class-likes. +pub(super) fn eval_reflection_eval_instantiation_error_message( + reflected_name: &str, + context: &ElephcEvalContext, +) -> Option { + if let Some(class) = context.class(reflected_name) { + if class.is_abstract() { + return Some(format!("Cannot instantiate abstract class {}", class.name())); + } + if let Some(enum_decl) = context.enum_decl(class.name()) { + return Some(format!("Cannot instantiate enum {}", enum_decl.name())); + } + return None; + } + if let Some(interface) = context.interface(reflected_name) { + return Some(format!("Cannot instantiate interface {}", interface.name())); + } + if let Some(trait_decl) = context.trait_decl(reflected_name) { + return Some(format!("Cannot instantiate trait {}", trait_decl.name())); + } + context + .enum_decl(reflected_name) + .map(|enum_decl| format!("Cannot instantiate enum {}", enum_decl.name())) +} + +/// Instantiates an attribute class for `ReflectionAttribute::newInstance()`. +pub(super) fn eval_reflection_attribute_new_instance_result( + attribute: &EvalAttribute, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let args = eval_reflection_attribute_evaluated_args(attribute, values)?; + if let Some(class) = context.class(attribute.name()).cloned() { + let mut scope = ElephcEvalScope::new(); + return eval_dynamic_class_new_object(&class, args, context, &mut scope, values); + } + let class_name = context + .resolve_class_name(attribute.name()) + .unwrap_or_else(|| attribute.name().trim_start_matches('\\').to_string()); + if !values.class_exists(&class_name)? { + return values.null(); + } + let object = values.new_object(&class_name)?; + if let Err(err) = eval_native_constructor_with_evaluated_args( + &class_name, + object, + args, + context, + values, + ) { + let _ = values.release(object); + return Err(err); + } + Ok(object) +} + +/// Materializes eval attribute literal arguments as evaluated constructor args. +pub(super) fn eval_reflection_attribute_evaluated_args( + attribute: &EvalAttribute, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let Some(args) = attribute.args() else { + return Err(EvalStatus::RuntimeFatal); + }; + args.iter() + .map(|arg| { + Ok(EvaluatedCallArg { + name: arg.name().map(str::to_string), + value: eval_reflection_attribute_arg_value(arg.value(), values)?, + ref_target: None, + }) + }) + .collect() +} + +/// Materializes one eval attribute literal as a constructor argument cell. +pub(super) fn eval_reflection_attribute_arg_value( + arg: &EvalAttributeArg, + values: &mut impl RuntimeValueOps, +) -> Result { + match arg { + EvalAttributeArg::String(value) => values.string(value), + EvalAttributeArg::Int(value) => values.int(*value), + EvalAttributeArg::Float(bits) => values.float(f64::from_bits(*bits)), + EvalAttributeArg::Bool(value) => values.bool_value(*value), + EvalAttributeArg::Null => values.null(), + EvalAttributeArg::Array(elements) => { + eval_reflection_attribute_array_arg_value(elements, values) + } + EvalAttributeArg::Named { value, .. } | EvalAttributeArg::IntKeyed { value, .. } => { + eval_reflection_attribute_arg_value(value, values) + } + } +} + +/// Materializes one retained attribute array literal for constructor calls. +pub(super) fn eval_reflection_attribute_array_arg_value( + elements: &[EvalAttributeArg], + values: &mut impl RuntimeValueOps, +) -> Result { + let mut result = if elements + .iter() + .any(|element| element.name().is_some() || element.int_key().is_some()) + { + values.assoc_new(elements.len())? + } else { + values.array_new(elements.len())? + }; + for (index, element) in elements.iter().enumerate() { + let key = match element.name() { + Some(name) => values.string(name)?, + None => values.int(element.int_key().unwrap_or(index as i64))?, + }; + let value = eval_reflection_attribute_arg_value(element.value(), values)?; + result = values.array_set(result, key, value)?; + } + Ok(result) +} + +/// Resolves the method metadata visible from the current class scope. +pub(in crate::interpreter) fn eval_dynamic_method_for_call( + object_class_name: &str, + method_name: &str, + context: &ElephcEvalContext, +) -> Option<(String, EvalClassMethod)> { + if let Some(current_class) = context.current_class_scope() { + if context.class_is_a(object_class_name, current_class, false) { + if let Some((declaring_class, method)) = + context.class_own_method(current_class, method_name) + { + if method.visibility() == EvalVisibility::Private { + return Some((declaring_class, method)); + } + } + } + } + context.class_method(object_class_name, method_name) +} + +/// Returns whether the current eval class scope can access one declared member. +pub(in crate::interpreter) fn validate_eval_member_access( + declaring_class: &str, + visibility: EvalVisibility, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if visibility == EvalVisibility::Public { + return Ok(()); + } + let Some(current_class) = context.current_class_scope() else { + return Err(EvalStatus::RuntimeFatal); + }; + match visibility { + EvalVisibility::Public => Ok(()), + EvalVisibility::Private => same_eval_class_name(current_class, declaring_class) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal), + EvalVisibility::Protected => { + eval_classes_are_related(current_class, declaring_class, context) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) + } + } +} + +/// Returns true when two PHP class names refer to the same eval class. +pub(super) fn same_eval_class_name(left: &str, right: &str) -> bool { + left.trim_start_matches('\\') + .eq_ignore_ascii_case(right.trim_start_matches('\\')) +} + +/// Returns true when two eval or generated classes are in the same inheritance family. +pub(super) fn eval_classes_are_related(left: &str, right: &str, context: &ElephcEvalContext) -> bool { + same_eval_class_name(left, right) + || context.class_is_a(left, right, false) + || context.class_is_a(right, left, false) + || native_class_is_a(left, right, context) + || native_class_is_a(right, left, context) +} + +/// Returns true when generated AOT parent metadata proves one class extends another. +pub(super) fn native_class_is_a(class_name: &str, target: &str, context: &ElephcEvalContext) -> bool { + let mut current = class_name.trim_start_matches('\\').to_string(); + let target = target.trim_start_matches('\\'); + let mut seen = std::collections::HashSet::new(); + loop { + if !seen.insert(current.to_ascii_lowercase()) { + return false; + } + if same_eval_class_name(¤t, target) { + return true; + } + let Some(parent) = context.native_class_parent(¤t) else { + return false; + }; + current = parent.to_string(); + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/static_method_dispatch.rs b/crates/elephc-magician/src/interpreter/statements/static_method_dispatch.rs new file mode 100644 index 0000000000..31cf91fa48 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/static_method_dispatch.rs @@ -0,0 +1,240 @@ +//! Purpose: +//! Resolves and dispatches eval-declared static method calls. +//! +//! Called from: +//! - Static-call expression evaluation. +//! +//! Key details: +//! - Called-class scope and private-method resolution are preserved across entry points. + +use super::*; + +/// Dispatches a static method call to an eval-declared static method. +pub(in crate::interpreter) fn eval_static_method_call_result( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + eval_static_method_call_result_resolved( + receiver.dispatch_class, + receiver.called_class, + method_name, + evaluated_args, + None, + context, + values, + ) +} + +/// Dispatches a static-syntax method call from an expression scope that may hold `$this`. +pub(in crate::interpreter) fn eval_static_method_call_result_from_scope( + class_name: &str, + method_name: &str, + evaluated_args: Vec, + scope: &ElephcEvalScope, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let receiver = resolve_eval_static_method_receiver(class_name, context)?; + eval_static_method_call_result_resolved( + receiver.dispatch_class, + receiver.called_class, + method_name, + evaluated_args, + Some(scope), + context, + values, + ) +} + +/// Dispatches a static method call using a first-class callable's captured called class. +pub(in crate::interpreter) fn eval_static_method_call_result_with_called_class( + class_name: &str, + called_class_name: &str, + method_name: &str, + evaluated_args: Vec, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + let called_class_name = context + .resolve_class_name(called_class_name) + .unwrap_or_else(|| called_class_name.trim_start_matches('\\').to_string()); + eval_static_method_call_result_resolved( + class_name, + called_class_name, + method_name, + evaluated_args, + None, + context, + values, + ) +} + +/// Dispatches a static method call after lookup and late-static names have been resolved. +pub(super) fn eval_static_method_call_result_resolved( + class_name: String, + called_class_name: String, + method_name: &str, + evaluated_args: Vec, + lexical_scope: Option<&ElephcEvalScope>, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(result) = eval_closure_static_method_result( + &class_name, + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_builtin_property_hook_type_static_method_result( + &class_name, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if let Some(result) = eval_reflection_method_create_from_method_name_result( + &class_name, + method_name, + evaluated_args.clone(), + context, + values, + )? { + return Ok(result); + } + if eval_enum_static_builtin_applies(&class_name, method_name, context).is_some() { + return eval_enum_builtin_static_method_result( + &class_name, + method_name, + evaluated_args, + context, + values, + ); + } + if let Some((declaring_class, method)) = + eval_dynamic_static_method_for_call(&class_name, method_name, context) + { + if method.is_abstract() { + return eval_throw_abstract_method_call_error( + &declaring_class, + method.name(), + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, method.visibility(), context).is_err() { + if let Some(result) = eval_magic_static_method_call( + &class_name, + &called_class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } + return eval_throw_method_access_error( + &declaring_class, + method.name(), + method.visibility(), + context, + values, + ); + } + if !method.is_static() { + if let Some(object) = + eval_static_syntax_instance_receiver(&class_name, lexical_scope, context, values)? + { + return eval_dynamic_method_with_values( + &declaring_class, + &called_class_name, + &method, + object, + evaluated_args, + context, + values, + ); + } + return eval_throw_non_static_method_call_error( + &declaring_class, + method.name(), + context, + values, + ); + } + return eval_dynamic_static_method_with_values( + &declaring_class, + &called_class_name, + &method, + evaluated_args, + context, + values, + ); + } + if context.has_class(&class_name) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some(result) = eval_native_static_syntax_method_result( + &parent, + Some(&called_class_name), + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? + { + return Ok(result); + } + } + } + if context.has_class(&class_name) + || context.has_interface(&class_name) + || context.has_trait(&class_name) + || context.has_enum(&class_name) + { + if let Some(result) = eval_magic_static_method_call( + &class_name, + &called_class_name, + method_name, + evaluated_args, + context, + values, + )? { + return Ok(result); + } + return eval_throw_undefined_method_call_error( + &class_name, + method_name, + context, + values, + ); + } + if let Some(result) = eval_native_static_syntax_method_result( + &class_name, + None, + method_name, + evaluated_args.clone(), + lexical_scope, + context, + values, + )? { + return Ok(result); + } + eval_native_static_method_with_evaluated_args( + &class_name, + method_name, + evaluated_args, + context, + values, + ) +} diff --git a/crates/elephc-magician/src/interpreter/statements/static_property_access.rs b/crates/elephc-magician/src/interpreter/statements/static_property_access.rs new file mode 100644 index 0000000000..6c90827259 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/static_property_access.rs @@ -0,0 +1,827 @@ +//! Purpose: +//! Executes static-property, class-constant, and static reference operations. +//! +//! Called from: +//! - Expression and statement dispatch for class-member access. +//! +//! Key details: +//! - Receiver resolution, hooks, enum/reflection constants, and reference aliases share one path. + +use super::*; + +/// Reads one eval-declared static property after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_static_property_get_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property.name(), + property.visibility(), + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + if let Some(value) = context.static_property(&declaring_class, property.name()) { + return Ok(value); + } + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property.name(), + context, + values, + ); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_is_initialized(&declaring_class, property_name) + })? { + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property_name, + context, + values, + ); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.static_property_get(&declaring_class, property_name), + )? { + return Ok(value); + } + } + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if is_static { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + visibility, + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + return eval_reference_target_value(&target, context, values); + } + if !values.static_property_is_initialized(&declaring_class, property_name)? { + return eval_throw_uninitialized_static_property_error( + &declaring_class, + property_name, + context, + values, + ); + } + } + } + if let Some(value) = values.static_property_get(&class_name, property_name)? { + return Ok(value); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) + } else { + eval_throw_class_not_found_error(&class_name, context, values) + } +} + +/// Returns whether a static property is PHP-`isset()` without throwing for missing properties. +pub(in crate::interpreter) fn eval_static_property_isset_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, property.visibility(), context).is_err() { + return Ok(false); + } + let value = if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + eval_reference_target_value(&target, context, values)? + } else { + let Some(value) = context.static_property(&declaring_class, property.name()) else { + return Ok(false); + }; + value + }; + return Ok(!values.is_null(value)?); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return Ok(false); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + let value = eval_reference_target_value(&target, context, values)?; + return Ok(!values.is_null(value)?); + } + if !eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_is_initialized(&declaring_class, property_name) + })? { + return Ok(false); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.static_property_get(&declaring_class, property_name), + )? { + return Ok(!values.is_null(value)?); + } + } + } + return Ok(false); + } + if let Some((declaring_class, visibility, _, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if !is_static { + return Ok(false); + } + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return Ok(false); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + let value = eval_reference_target_value(&target, context, values)?; + return Ok(!values.is_null(value)?); + } + if !values.static_property_is_initialized(&declaring_class, property_name)? { + return Ok(false); + } + } else if !eval_runtime_class_like_exists(&class_name, context, values)? { + return eval_throw_class_not_found_error(&class_name, context, values); + } + if let Some(value) = values.static_property_get(&class_name, property_name)? { + return Ok(!values.is_null(value)?); + } + Ok(false) +} + +/// Throws PHP's catchable error for attempts to unset an existing static property target. +pub(in crate::interpreter) fn eval_static_property_unset_result( + class_name: &str, + property_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if !eval_runtime_class_like_exists(&class_name, context, values)? { + return eval_throw_class_not_found_error(&class_name, context, values); + } + eval_throw_error( + &format!( + "Attempt to unset static property {}::${}", + class_name.trim_start_matches('\\'), + property_name + ), + context, + values, + ) +} + +/// Reads one eval-declared class constant after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_class_constant_fetch_result( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(value) = eval_builtin_reflection_class_constant(class_name, constant_name, values)? + { + return Ok(value); + } + if let Some(value) = + eval_builtin_property_hook_type_case(class_name, constant_name, context, values)? + { + return Ok(value); + } + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some(case) = context.enum_case(&class_name, constant_name) { + return Ok(case); + } + if let Some((declaring_class, constant)) = context.class_constant(&class_name, constant_name) { + if validate_eval_member_access(&declaring_class, constant.visibility(), context).is_err() { + return eval_throw_constant_access_error( + &declaring_class, + constant.name(), + constant.visibility(), + context, + values, + ); + } + return context + .class_constant_cell(&declaring_class, constant.name()) + .ok_or(EvalStatus::RuntimeFatal); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some((declaring_class, visibility)) = + eval_dynamic_class_native_constant_metadata( + &class_name, + constant_name, + context, + values, + )? + { + if validate_eval_member_access(&declaring_class, visibility, context).is_err() { + return eval_throw_constant_access_error( + &declaring_class, + constant_name, + visibility, + context, + values, + ); + } + if let Some(value) = eval_with_native_bridge_scope( + &declaring_class, + context, + || values.class_constant_get(&declaring_class, constant_name), + )? { + return Ok(value); + } + } + return Err(EvalStatus::RuntimeFatal); + } + if let Some(value) = values.class_constant_get(&class_name, constant_name)? { + return Ok(value); + } + eval_throw_error( + &format!( + "Undefined constant {}::{}", + class_name.trim_start_matches('\\'), + constant_name + ), + context, + values, + ) +} + +/// Resolves eval-visible built-in Reflection class constants. +pub(super) fn eval_builtin_reflection_class_constant( + class_name: &str, + constant_name: &str, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + let class_name = class_name.trim_start_matches('\\'); + let value = if class_name.eq_ignore_ascii_case("ReflectionClass") { + match constant_name { + "IS_IMPLICIT_ABSTRACT" => Some(16), + "IS_FINAL" => Some(32), + "IS_EXPLICIT_ABSTRACT" => Some(64), + "IS_READONLY" => Some(65_536), + _ => None, + } + } else if class_name.eq_ignore_ascii_case("ReflectionMethod") { + match constant_name { + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_STATIC" => Some(16), + "IS_FINAL" => Some(32), + "IS_ABSTRACT" => Some(64), + _ => None, + } + } else if class_name.eq_ignore_ascii_case("ReflectionProperty") { + match constant_name { + "IS_STATIC" => Some(16), + "IS_READONLY" => Some(128), + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_ABSTRACT" => Some(64), + "IS_PROTECTED_SET" => Some(2048), + "IS_PRIVATE_SET" => Some(4096), + "IS_VIRTUAL" => Some(512), + "IS_FINAL" => Some(32), + _ => None, + } + } else if class_name.eq_ignore_ascii_case("ReflectionClassConstant") + || class_name.eq_ignore_ascii_case("ReflectionEnumUnitCase") + || class_name.eq_ignore_ascii_case("ReflectionEnumBackedCase") + { + match constant_name { + "IS_PUBLIC" => Some(1), + "IS_PROTECTED" => Some(2), + "IS_PRIVATE" => Some(4), + "IS_FINAL" => Some(32), + _ => None, + } + } else { + None + }; + value.map(|value| values.int(value)).transpose() +} + +/// Resolves eval-visible `PropertyHookType` builtin enum cases. +pub(super) fn eval_builtin_property_hook_type_case( + class_name: &str, + constant_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result, EvalStatus> { + if !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("PropertyHookType") + { + return Ok(None); + } + let Some((case_name, case_value)) = eval_property_hook_type_case_parts(constant_name) else { + return Ok(None); + }; + if let Some(case) = context.enum_case("PropertyHookType", case_name) { + return Ok(Some(case)); + } + let object = values.new_object("stdClass")?; + let identity = values.object_identity(object)?; + context.register_dynamic_object(identity, "PropertyHookType"); + let name = values.string(case_name)?; + values.property_set(object, "name", name)?; + let value = values.string(case_value)?; + values.property_set(object, "value", value)?; + if let Some(replaced) = context.set_enum_case_value("PropertyHookType", case_name, value) { + values.release(replaced)?; + } + if let Some(replaced) = context.set_enum_case("PropertyHookType", case_name, object) { + values.release(replaced)?; + } + Ok(Some(object)) +} + +/// Returns the PHP case name and backed value for a builtin property-hook case. +pub(super) fn eval_property_hook_type_case_parts(constant_name: &str) -> Option<(&'static str, &'static str)> { + match constant_name { + "Get" => Some(("Get", "get")), + "Set" => Some(("Set", "set")), + _ => None, + } +} + +/// Returns the PHP class-name literal for `ClassName::class`-style eval expressions. +pub(in crate::interpreter) fn eval_class_name_fetch_result( + class_name: &str, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result { + let class_name = resolve_eval_class_name_literal(class_name, context)?; + values.string(&class_name) +} + +/// Binds one eval-declared static property to a by-reference source variable. +pub(super) fn eval_static_property_reference_bind_result( + class_name: &str, + property_name: &str, + source_name: &str, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_write_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property.name(), + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property.name(), target); + if let Some(replaced) = + context.set_static_property(&declaring_class, property.name(), value) + { + values.release(replaced)?; + } + return Ok(()); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property_name, + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property_name, target); + if eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_set(&declaring_class, property_name, value) + })? { + return Ok(()); + } + } + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context).is_err() { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + let target = eval_static_property_reference_target( + &declaring_class, + property_name, + source_name, + context, + scope, + values, + )?; + let value = eval_reference_target_value(&target, context, values)?; + context.bind_static_property_alias(&declaring_class, property_name, target); + if values.static_property_set(&class_name, property_name, value)? { + return Ok(()); + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) + } else { + eval_throw_class_not_found_error(&class_name, context, values) + } +} + +/// Resolves a local by-reference source into a persistent static-property alias target. +pub(super) fn eval_static_property_reference_target( + class_name: &str, + property_name: &str, + source_name: &str, + context: &ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result { + if let Some(target) = scope.reference_target(source_name).cloned() { + return Ok(target); + } + if context.current_function().is_some() { + let cell = + visible_scope_cell(context, scope, source_name).map_or_else(|| values.null(), Ok)?; + return Ok(EvalReferenceTarget::Cell { cell }); + } + let alias_name = eval_static_property_reference_alias_name(class_name, property_name); + for replaced in set_reference_alias(context, scope, &alias_name, source_name, values)? { + values.release(replaced)?; + } + Ok(EvalReferenceTarget::Variable { + scope: scope as *mut ElephcEvalScope, + name: alias_name, + }) +} + +/// Builds the hidden scope variable name backing one static-property reference alias. +pub(super) fn eval_static_property_reference_alias_name(class_name: &str, property_name: &str) -> String { + format!("\0elephc_static_property_ref:{class_name}:{property_name}") +} + +/// Writes one eval static-property assignment through its persistent reference target. +pub(super) fn eval_static_reference_target_write( + class_name: &str, + property_name: &str, + target: EvalReferenceTarget, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if matches!(target, EvalReferenceTarget::Cell { .. }) { + context.bind_static_property_alias( + class_name, + property_name, + EvalReferenceTarget::Cell { cell: value }, + ); + return Ok(()); + } + write_back_method_ref_target(&target, value, context, values) +} + +/// Writes one eval-declared static property after resolving the class-like receiver. +pub(in crate::interpreter) fn eval_static_property_set_result( + class_name: &str, + property_name: &str, + value: RuntimeCellHandle, + context: &mut ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let class_name = resolve_eval_static_member_class_name(class_name, context)?; + if let Some((declaring_class, property)) = context.class_property(&class_name, property_name) { + if !property.is_static() { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_property_write_access(&declaring_class, &property, context).is_err() { + return eval_throw_property_write_access_error( + &declaring_class, + &property, + context, + values, + ); + } + if validate_eval_readonly_property_write(&declaring_class, &property, context).is_err() { + return eval_throw_readonly_property_modification_error( + &declaring_class, + property.name(), + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property.name()) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property.name(), + target, + value, + context, + values, + )?; + } + if let Some(replaced) = + context.set_static_property(&declaring_class, property.name(), value) + { + values.release(replaced)?; + } + return Ok(()); + } + if eval_static_member_context_owns_class(&class_name, context) { + if let Some(parent) = context.class_native_parent_name(&class_name) { + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &parent, + property_name, + context, + values, + )? + { + if !is_static { + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if validate_eval_member_access(&declaring_class, write_visibility, context) + .is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property_name, + target, + value, + context, + values, + )?; + } + if eval_with_native_bridge_scope(&declaring_class, context, || { + values.static_property_set(&declaring_class, property_name, value) + })? { + return Ok(()); + } + } + } + return eval_throw_undeclared_static_property_error( + &class_name, + property_name, + context, + values, + ); + } + if let Some((declaring_class, _, write_visibility, is_static)) = + eval_reflection_aot_static_property_access_metadata( + &class_name, + property_name, + context, + values, + )? + { + if is_static + && validate_eval_member_access(&declaring_class, write_visibility, context).is_err() + { + return eval_throw_property_access_error( + &declaring_class, + property_name, + write_visibility, + context, + values, + ); + } + if is_static { + if let Some(target) = context + .static_property_alias(&declaring_class, property_name) + .cloned() + { + eval_static_reference_target_write( + &declaring_class, + property_name, + target, + value, + context, + values, + )?; + } + } + } + if values.static_property_set(&class_name, property_name, value)? { + return Ok(()); + } + if eval_runtime_class_like_exists(&class_name, context, values)? { + eval_throw_undeclared_static_property_error(&class_name, property_name, context, values) + } else { + eval_throw_class_not_found_error(&class_name, context, values) + } +} diff --git a/crates/elephc-magician/src/interpreter/statements/trait_declarations.rs b/crates/elephc-magician/src/interpreter/statements/trait_declarations.rs new file mode 100644 index 0000000000..473fd78305 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/statements/trait_declarations.rs @@ -0,0 +1,791 @@ +//! Purpose: +//! Registers interfaces and traits and composes trait members into classes. +//! +//! Called from: +//! - Class-like declaration execution before validation and registration. +//! +//! Key details: +//! - Trait adaptations, conflicts, aliases, visibility, constants, and properties are resolved here. + +use super::*; + +/// Registers an eval-declared interface in the dynamic interface table. +pub(in crate::interpreter) fn execute_interface_decl_stmt( + interface: &EvalInterface, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = interface.name().trim_start_matches('\\'); + if context.has_interface(name) + || context.has_class(name) + || context.has_enum(name) + || eval_runtime_interface_exists(name, values)? + || values.class_exists(name)? + || values.enum_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + for parent in interface.parents() { + if context + .interface_parent_names(parent) + .iter() + .any(|ancestor| ancestor.eq_ignore_ascii_case(name)) + { + return Err(EvalStatus::RuntimeFatal); + } + if !context.has_interface(parent) && !eval_runtime_interface_exists(parent, values)? { + return Err(EvalStatus::RuntimeFatal); + } + } + validate_eval_interface_attribute_targets(interface)?; + validate_eval_interface_override_attributes(interface, context, values)?; + validate_eval_declared_constants(interface.constants())?; + validate_eval_interface_constants(interface.constants())?; + validate_interface_constant_parent_redeclarations(interface, context, values)?; + if context.define_interface(interface.clone()) { + initialize_eval_declared_constants( + interface.name(), + interface.constants(), + context, + scope, + values, + ) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Registers an eval-declared trait in the dynamic trait table. +pub(in crate::interpreter) fn execute_trait_decl_stmt( + trait_decl: &EvalTrait, + context: &mut ElephcEvalContext, + scope: &mut ElephcEvalScope, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + let name = trait_decl.name().trim_start_matches('\\'); + if context.has_trait(name) + || context.has_class(name) + || context.has_interface(name) + || context.has_enum(name) + || values.trait_exists(name)? + || values.class_exists(name)? + || eval_runtime_interface_exists(name, values)? + || values.enum_exists(name)? + { + return Err(EvalStatus::RuntimeFatal); + } + let trait_decl = expand_eval_trait_traits(trait_decl, context)?; + validate_eval_trait_attribute_targets(&trait_decl)?; + validate_eval_declared_constants(trait_decl.constants())?; + validate_eval_magic_methods(trait_decl.methods())?; + if context.define_trait(trait_decl.clone()) { + initialize_eval_declared_constants( + trait_decl.name(), + trait_decl.constants(), + context, + scope, + values, + ) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Expands nested eval trait uses into the trait metadata registered by eval. +pub(super) fn expand_eval_trait_traits( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, +) -> Result { + if trait_decl.traits().is_empty() { + return Ok(trait_decl.clone()); + } + validate_eval_trait_decl_adaptations(trait_decl, context)?; + let trait_method_names = trait_method_name_set(trait_decl); + let mut imported_method_names = std::collections::HashSet::new(); + let mut imported_properties = std::collections::HashMap::new(); + let mut imported_constants = std::collections::HashMap::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for used_trait_name in trait_decl.traits() { + let Some(used_trait_decl) = context.trait_decl(used_trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_constants( + used_trait_decl, + trait_decl.constants(), + &mut imported_constants, + &mut constants, + )?; + append_eval_trait_properties( + used_trait_decl, + trait_decl.properties(), + &mut imported_properties, + &mut properties, + )?; + append_eval_trait_methods( + used_trait_decl, + trait_decl.trait_adaptations(), + &trait_method_names, + &mut imported_method_names, + &mut methods, + )?; + } + constants.extend(trait_decl.constants().iter().cloned()); + properties.extend(trait_decl.properties().iter().cloned()); + methods.extend(trait_decl.methods().iter().cloned()); + let mut expanded = EvalTrait::with_constants_traits_adaptations( + trait_decl.name().to_string(), + constants, + properties, + methods, + trait_decl.traits().to_vec(), + trait_decl.trait_adaptations().to_vec(), + ) + .with_attributes(trait_decl.attributes().to_vec()); + if let Some(source_location) = trait_decl.source_location() { + expanded = expanded.with_source_location(source_location); + } + Ok(expanded) +} + +/// Validates that trait-level adaptations reference directly used traits and methods. +pub(super) fn validate_eval_trait_decl_adaptations( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for adaptation in trait_decl.trait_adaptations() { + match adaptation { + EvalTraitAdaptation::Alias { + trait_name, method, .. + } => validate_eval_trait_decl_adaptation_method( + trait_decl, + context, + trait_name.as_deref(), + method, + )?, + EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + } => { + let Some(trait_name) = trait_name.as_deref() else { + return Err(EvalStatus::RuntimeFatal); + }; + validate_eval_trait_decl_adaptation_method( + trait_decl, + context, + Some(trait_name), + method, + )?; + for suppressed in instead_of { + if eval_trait_used_trait_decl(trait_decl, context, suppressed).is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if same_eval_class_name(suppressed, trait_name) { + return Err(EvalStatus::RuntimeFatal); + } + } + } + } + } + Ok(()) +} + +/// Validates one trait-level adaptation method target. +pub(super) fn validate_eval_trait_decl_adaptation_method( + trait_decl: &EvalTrait, + context: &ElephcEvalContext, + trait_name: Option<&str>, + method: &str, +) -> Result<(), EvalStatus> { + if let Some(trait_name) = trait_name { + let Some(used_trait_decl) = eval_trait_used_trait_decl(trait_decl, context, trait_name) + else { + return Err(EvalStatus::RuntimeFatal); + }; + return trait_has_method(used_trait_decl, method) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal); + } + trait_decl + .traits() + .iter() + .filter_map(|trait_name| context.trait_decl(trait_name)) + .any(|used_trait_decl| trait_has_method(used_trait_decl, method)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns a trait declaration only when the pending trait directly uses that trait. +pub(super) fn eval_trait_used_trait_decl<'a>( + trait_decl: &EvalTrait, + context: &'a ElephcEvalContext, + trait_name: &str, +) -> Option<&'a EvalTrait> { + trait_decl + .traits() + .iter() + .any(|used_trait| same_eval_class_name(used_trait, trait_name)) + .then(|| context.trait_decl(trait_name)) + .flatten() +} + +/// Expands eval trait uses into the class metadata used by dynamic dispatch. +pub(super) fn expand_eval_class_traits( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result { + if class.traits().is_empty() { + return Ok(class.clone()); + } + validate_eval_trait_adaptations(class, context)?; + let class_method_names = class_method_name_set(class); + let mut trait_method_names = std::collections::HashSet::new(); + let mut trait_properties = std::collections::HashMap::new(); + let mut trait_constants = std::collections::HashMap::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + for trait_name in class.traits() { + let Some(trait_decl) = context.trait_decl(trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + append_eval_trait_constants( + trait_decl, + class.constants(), + &mut trait_constants, + &mut constants, + )?; + append_eval_trait_properties( + trait_decl, + class.properties(), + &mut trait_properties, + &mut properties, + )?; + append_eval_trait_methods( + trait_decl, + class.trait_adaptations(), + &class_method_names, + &mut trait_method_names, + &mut methods, + )?; + } + constants.extend(class.constants().iter().cloned()); + properties.extend(class.properties().iter().cloned()); + methods.extend(class.methods().iter().cloned()); + let mut expanded = EvalClass::with_class_modifiers_traits_adaptations_and_constants( + class.name().to_string(), + class.is_abstract(), + class.is_final(), + class.is_readonly_class(), + class.parent().map(str::to_string), + class.interfaces().to_vec(), + class.traits().to_vec(), + class.trait_adaptations().to_vec(), + constants, + properties, + methods, + ) + .with_attributes(class.attributes().to_vec()); + if class.is_anonymous() { + expanded = expanded.with_anonymous(); + } + Ok(expanded) +} + +/// Validates that trait adaptations reference used traits and existing methods. +pub(super) fn validate_eval_trait_adaptations( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + for adaptation in class.trait_adaptations() { + match adaptation { + EvalTraitAdaptation::Alias { + trait_name, method, .. + } => { + validate_eval_trait_adaptation_method(class, context, trait_name.as_deref(), method)? + } + EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + } => { + let Some(trait_name) = trait_name.as_deref() else { + return Err(EvalStatus::RuntimeFatal); + }; + validate_eval_trait_adaptation_method(class, context, Some(trait_name), method)?; + for suppressed in instead_of { + if eval_used_trait_decl(class, context, suppressed).is_none() { + return Err(EvalStatus::RuntimeFatal); + } + if same_eval_class_name(suppressed, trait_name) { + return Err(EvalStatus::RuntimeFatal); + } + } + } + } + } + Ok(()) +} + +/// Validates one adaptation method target, allowing unqualified alias targets. +pub(super) fn validate_eval_trait_adaptation_method( + class: &EvalClass, + context: &ElephcEvalContext, + trait_name: Option<&str>, + method: &str, +) -> Result<(), EvalStatus> { + if let Some(trait_name) = trait_name { + let Some(trait_decl) = eval_used_trait_decl(class, context, trait_name) else { + return Err(EvalStatus::RuntimeFatal); + }; + return trait_has_method(trait_decl, method) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal); + } + class + .traits() + .iter() + .filter_map(|trait_name| context.trait_decl(trait_name)) + .any(|trait_decl| trait_has_method(trait_decl, method)) + .then_some(()) + .ok_or(EvalStatus::RuntimeFatal) +} + +/// Returns a trait declaration only when the pending class directly uses that trait. +pub(super) fn eval_used_trait_decl<'a>( + class: &EvalClass, + context: &'a ElephcEvalContext, + trait_name: &str, +) -> Option<&'a EvalTrait> { + class + .traits() + .iter() + .any(|used_trait| same_eval_class_name(used_trait, trait_name)) + .then(|| context.trait_decl(trait_name)) + .flatten() +} + +/// Returns whether a trait declares a method by PHP case-insensitive method name. +pub(super) fn trait_has_method(trait_decl: &EvalTrait, method: &str) -> bool { + trait_decl + .methods() + .iter() + .any(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) +} + +/// Returns case-insensitive method names declared directly by a pending trait. +pub(super) fn trait_method_name_set(trait_decl: &EvalTrait) -> std::collections::HashSet { + trait_decl + .methods() + .iter() + .map(|method| method.name().to_ascii_lowercase()) + .collect() +} + +/// Returns case-insensitive method names declared directly by a pending class. +pub(super) fn class_method_name_set(class: &EvalClass) -> std::collections::HashSet { + class + .methods() + .iter() + .map(|method| method.name().to_ascii_lowercase()) + .collect() +} + +/// Appends trait constants while enforcing PHP-compatible same-name conflicts. +pub(super) fn append_eval_trait_constants( + trait_decl: &EvalTrait, + class_constants: &[EvalClassConstant], + trait_constants: &mut std::collections::HashMap, + constants: &mut Vec, +) -> Result<(), EvalStatus> { + for constant in trait_decl.constants() { + if let Some(class_constant) = class_constants + .iter() + .find(|class_constant| class_constant.name() == constant.name()) + { + validate_eval_trait_constant_compatibility(class_constant, constant)?; + continue; + } + if let Some(existing) = trait_constants.get(constant.name()) { + validate_eval_trait_constant_compatibility(existing, constant)?; + continue; + } + let constant = constant + .clone() + .with_trait_origin(trait_decl.name().to_string()); + trait_constants.insert(constant.name().to_string(), constant.clone()); + constants.push(constant); + } + Ok(()) +} + +/// Validates that a same-name trait constant definition is compatible with PHP composition. +pub(super) fn validate_eval_trait_constant_compatibility( + existing: &EvalClassConstant, + incoming: &EvalClassConstant, +) -> Result<(), EvalStatus> { + if existing.visibility() == incoming.visibility() + && existing.is_final() == incoming.is_final() + && existing.value() == incoming.value() + { + Ok(()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Appends trait properties while enforcing PHP-compatible same-name conflicts. +pub(super) fn append_eval_trait_properties( + trait_decl: &EvalTrait, + class_properties: &[EvalClassProperty], + trait_properties: &mut std::collections::HashMap, + properties: &mut Vec, +) -> Result<(), EvalStatus> { + for property in trait_decl.properties() { + if let Some(class_property) = class_properties + .iter() + .find(|class_property| class_property.name() == property.name()) + { + validate_eval_trait_property_compatibility(class_property, property)?; + continue; + } + if let Some(existing) = trait_properties.get(property.name()) { + let resolved = resolve_eval_trait_property_conflict(existing, property)?; + if &resolved != existing { + trait_properties.insert(property.name().to_string(), resolved.clone()); + if let Some(slot) = properties + .iter_mut() + .find(|candidate| candidate.name() == property.name()) + { + *slot = resolved; + } + } + continue; + } + let property = property + .clone() + .with_trait_origin(trait_decl.name().to_string()); + trait_properties.insert(property.name().to_string(), property.clone()); + properties.push(property); + } + Ok(()) +} + +/// Validates that a same-name trait property definition is compatible with PHP composition. +pub(super) fn validate_eval_trait_property_compatibility( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> Result<(), EvalStatus> { + resolve_eval_trait_property_conflict(existing, incoming).map(|_| ()) +} + +/// Resolves compatible same-name properties imported from classes and traits. +pub(super) fn resolve_eval_trait_property_conflict( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> Result { + if existing.is_abstract() && !incoming.is_abstract() { + return class_property_satisfies_abstract_contract(incoming, existing) + .then(|| incoming.clone()) + .ok_or(EvalStatus::RuntimeFatal); + } + if incoming.is_abstract() && !existing.is_abstract() { + return class_property_satisfies_abstract_contract(existing, incoming) + .then(|| existing.clone()) + .ok_or(EvalStatus::RuntimeFatal); + } + if existing.is_abstract() && incoming.is_abstract() { + return eval_trait_abstract_properties_are_compatible(existing, incoming) + .then(|| merge_abstract_property_contracts(existing, incoming)) + .ok_or(EvalStatus::RuntimeFatal); + } + if eval_trait_concrete_properties_are_compatible(existing, incoming) { + Ok(existing.clone()) + } else { + Err(EvalStatus::RuntimeFatal) + } +} + +/// Returns whether two concrete same-name trait properties are identical enough to deduplicate. +pub(super) fn eval_trait_concrete_properties_are_compatible( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> bool { + existing.visibility() == incoming.visibility() + && existing.set_visibility() == incoming.set_visibility() + && existing.is_static() == incoming.is_static() + && existing.is_final() == incoming.is_final() + && existing.is_readonly() == incoming.is_readonly() + && existing.is_abstract() == incoming.is_abstract() + && existing.has_get_hook() == incoming.has_get_hook() + && existing.has_set_hook() == incoming.has_set_hook() + && existing.requires_get_hook() == incoming.requires_get_hook() + && existing.requires_set_hook() == incoming.requires_set_hook() + && existing.is_virtual() == incoming.is_virtual() + && existing.property_type() == incoming.property_type() + && existing.set_hook_type() == incoming.set_hook_type() + && existing.default() == incoming.default() +} + +/// Returns whether two abstract trait property contracts can be merged. +pub(super) fn eval_trait_abstract_properties_are_compatible( + existing: &EvalClassProperty, + incoming: &EvalClassProperty, +) -> bool { + existing.visibility() == incoming.visibility() + && existing.set_visibility() == incoming.set_visibility() + && existing.is_static() == incoming.is_static() + && existing.is_final() == incoming.is_final() + && existing.is_readonly() == incoming.is_readonly() + && existing.property_type() == incoming.property_type() + && existing.set_hook_type() == incoming.set_hook_type() + && existing.default() == incoming.default() +} + +/// Appends trait methods unless the class provides a same-name method. +pub(super) fn append_eval_trait_methods( + trait_decl: &EvalTrait, + trait_adaptations: &[EvalTraitAdaptation], + class_method_names: &std::collections::HashSet, + trait_method_names: &mut std::collections::HashSet, + methods: &mut Vec, +) -> Result<(), EvalStatus> { + for method in trait_decl.methods() { + if trait_method_suppressed_by_insteadof(trait_decl.name(), method.name(), trait_adaptations) + { + continue; + } + let key = method.name().to_ascii_lowercase(); + if class_method_names.contains(&key) { + continue; + } + let method = method + .clone() + .with_trait_origin(trait_decl.name().to_string()); + let method = apply_trait_visibility_adaptations( + trait_decl.name(), + &method, + trait_adaptations, + ); + if !trait_method_names.insert(key) { + return Err(EvalStatus::RuntimeFatal); + } + methods.push(method); + } + append_eval_trait_method_aliases( + trait_decl, + trait_adaptations, + class_method_names, + trait_method_names, + methods, + ) +} + +/// Appends trait method aliases declared with `as`. +pub(super) fn append_eval_trait_method_aliases( + trait_decl: &EvalTrait, + trait_adaptations: &[EvalTraitAdaptation], + class_method_names: &std::collections::HashSet, + trait_method_names: &mut std::collections::HashSet, + methods: &mut Vec, +) -> Result<(), EvalStatus> { + for adaptation in trait_adaptations { + let EvalTraitAdaptation::Alias { + trait_name, + method, + alias: Some(alias), + visibility, + } = adaptation + else { + continue; + }; + if !trait_adaptation_target_matches( + trait_name.as_deref(), + method, + trait_decl.name(), + method, + ) { + continue; + } + let Some(source_method) = trait_decl + .methods() + .iter() + .find(|trait_method| trait_method.name().eq_ignore_ascii_case(method)) + else { + if trait_name.is_some() { + return Err(EvalStatus::RuntimeFatal); + } + continue; + }; + let mut alias_method = source_method + .clone() + .with_trait_origin(trait_decl.name().to_string()) + .renamed(alias.clone()); + if let Some(visibility) = visibility { + alias_method = alias_method.with_visibility_override(*visibility); + } + let key = alias_method.name().to_ascii_lowercase(); + if class_method_names.contains(&key) { + continue; + } + if trait_method_names.contains(&key) + && source_method.name().eq_ignore_ascii_case(alias) + && alias_method.visibility() == source_method.visibility() + { + continue; + } + if !trait_method_names.insert(key) { + return Err(EvalStatus::RuntimeFatal); + } + methods.push(alias_method); + } + Ok(()) +} + +/// Returns whether an `insteadof` adaptation suppresses this trait method import. +pub(super) fn trait_method_suppressed_by_insteadof( + trait_name: &str, + method_name: &str, + trait_adaptations: &[EvalTraitAdaptation], +) -> bool { + trait_adaptations.iter().any(|adaptation| { + let EvalTraitAdaptation::InsteadOf { + trait_name: selected_trait, + method, + instead_of, + } = adaptation + else { + return false; + }; + method.eq_ignore_ascii_case(method_name) + && instead_of + .iter() + .any(|suppressed| same_eval_class_name(suppressed, trait_name)) + && !selected_trait + .as_deref() + .is_some_and(|selected| same_eval_class_name(selected, trait_name)) + }) +} + +/// Applies visibility-only `as` adaptations to an imported trait method. +pub(super) fn apply_trait_visibility_adaptations( + trait_name: &str, + method: &EvalClassMethod, + trait_adaptations: &[EvalTraitAdaptation], +) -> EvalClassMethod { + let mut method = method.clone(); + for adaptation in trait_adaptations { + let EvalTraitAdaptation::Alias { + trait_name: target_trait, + method: target_method, + alias: None, + visibility: Some(visibility), + } = adaptation + else { + continue; + }; + if trait_adaptation_target_matches( + target_trait.as_deref(), + target_method, + trait_name, + method.name(), + ) { + method = method.with_visibility_override(*visibility); + } + } + method +} + +/// Returns whether an adaptation target selects one trait method. +pub(super) fn trait_adaptation_target_matches( + target_trait: Option<&str>, + target_method: &str, + trait_name: &str, + method_name: &str, +) -> bool { + target_method.eq_ignore_ascii_case(method_name) + && target_trait.map_or(true, |target_trait| { + same_eval_class_name(target_trait, trait_name) + }) +} + +/// Rejects non-enum classes that implement PHP's native enum interfaces. +pub(super) fn validate_eval_class_does_not_implement_enum_interfaces( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if pending_class_interface_names(class, context) + .iter() + .any(|interface| eval_builtin_enum_interface_name(interface)) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Rejects eval classes and enums that directly implement PHP's Throwable contract. +pub(super) fn validate_eval_class_does_not_implement_throwable_interfaces( + class: &EvalClass, + context: &ElephcEvalContext, +) -> Result<(), EvalStatus> { + if pending_class_interface_names(class, context) + .iter() + .any(|interface| eval_builtin_throwable_interface_name(interface)) + { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } +} + +/// Validates abstract/final modifiers on an eval-declared class and its methods. +pub(super) fn validate_eval_class_modifiers( + class: &EvalClass, + context: &ElephcEvalContext, + values: &mut impl RuntimeValueOps, +) -> Result<(), EvalStatus> { + if class.is_abstract() && class.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_class_attribute_targets(class.attributes())?; + if class.is_readonly_class() && eval_class_has_allow_dynamic_properties_attribute(class) { + return Err(EvalStatus::RuntimeFatal); + } + validate_eval_declared_constants(class.constants())?; + for constant in class.constants() { + validate_constant_parent_redeclaration(class, constant, context, values)?; + } + validate_eval_declared_properties(class, context)?; + for property in class.properties() { + validate_property_parent_redeclaration(class, property, context, values)?; + } + for method in class.methods() { + validate_eval_method_attribute_targets(method.attributes())?; + validate_eval_magic_method(method)?; + if method.is_abstract() && method.is_final() { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && method.visibility() == EvalVisibility::Private { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_static() && method.name().eq_ignore_ascii_case("__construct") { + return Err(EvalStatus::RuntimeFatal); + } + if method.is_abstract() && !class.is_abstract() { + return Err(EvalStatus::RuntimeFatal); + } + validate_method_parent_override(class, method, context)?; + validate_method_aot_parent_override(class, method, context, values)?; + validate_eval_override_attribute(class, method, context, values)?; + } + Ok(()) +} + +/// Returns whether a class carries PHP's global `#[AllowDynamicProperties]` attribute. +pub(super) fn eval_class_has_allow_dynamic_properties_attribute(class: &EvalClass) -> bool { + eval_attributes_have_global_builtin_attribute(class.attributes(), "AllowDynamicProperties") +} From 6ba1f305aa55b475dac1efba5b9da838393b391c Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 13 Jul 2026 13:27:08 +0200 Subject: [PATCH 1204/1208] refactor(magician): split runtime operation implementations --- .../interpreter/tests/support/object_ops.rs | 1467 +---------------- .../tests/support/object_ops/class_runtime.rs | 324 ++++ .../support/object_ops/method_dispatch.rs | 448 +++++ .../tests/support/object_ops/properties.rs | 105 ++ .../object_ops/reflection_construction.rs | 300 ++++ .../support/object_ops/reflection_helpers.rs | 252 +++ .../tests/support/object_ops/relations.rs | 70 + .../support/object_ops/static_dispatch.rs | 54 + .../interpreter/tests/support/runtime_ops.rs | 683 +------- .../support/runtime_ops/collection_calls.rs | 165 ++ .../support/runtime_ops/construction_raw.rs | 165 ++ .../support/runtime_ops/lifecycle_scalars.rs | 89 + .../support/runtime_ops/numeric_string.rs | 160 ++ .../tests/support/runtime_ops/reflection.rs | 176 ++ .../elephc-magician/src/runtime_hooks/ops.rs | 1158 +------------ .../src/runtime_hooks/ops/collection_calls.rs | 323 ++++ .../src/runtime_hooks/ops/construction_raw.rs | 214 +++ .../runtime_hooks/ops/lifecycle_scalars.rs | 114 ++ .../src/runtime_hooks/ops/native_results.rs | 53 + .../src/runtime_hooks/ops/numeric_string.rs | 205 +++ .../src/runtime_hooks/ops/reflection.rs | 330 ++++ 21 files changed, 3592 insertions(+), 3263 deletions(-) create mode 100644 crates/elephc-magician/src/interpreter/tests/support/object_ops/class_runtime.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/object_ops/method_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/object_ops/properties.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_construction.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_helpers.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/object_ops/relations.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/object_ops/static_dispatch.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/runtime_ops/collection_calls.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/runtime_ops/construction_raw.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/runtime_ops/lifecycle_scalars.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/runtime_ops/numeric_string.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/support/runtime_ops/reflection.rs create mode 100644 crates/elephc-magician/src/runtime_hooks/ops/collection_calls.rs create mode 100644 crates/elephc-magician/src/runtime_hooks/ops/construction_raw.rs create mode 100644 crates/elephc-magician/src/runtime_hooks/ops/lifecycle_scalars.rs create mode 100644 crates/elephc-magician/src/runtime_hooks/ops/native_results.rs create mode 100644 crates/elephc-magician/src/runtime_hooks/ops/numeric_string.rs create mode 100644 crates/elephc-magician/src/runtime_hooks/ops/reflection.rs diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs index ec67b963af..71e3ab5ec7 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops.rs @@ -9,6 +9,16 @@ use super::*; +mod class_runtime; +mod method_dispatch; +mod properties; +mod reflection_construction; +mod reflection_helpers; +mod relations; +mod static_dispatch; + +use relations::*; + const EVAL_REFLECTION_CLASS_FLAG_FINAL: u64 = 1; const EVAL_REFLECTION_CLASS_FLAG_ABSTRACT: u64 = 2; const EVAL_REFLECTION_CLASS_FLAG_READONLY: u64 = 32; @@ -35,1460 +45,3 @@ const EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL: u64 = 64; const EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT: u64 = 128; const EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE: u64 = 256; const EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE: u64 = 512; - -impl FakeOps { - /// Reads one fake object property by name. - pub(super) fn runtime_property_get( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => properties - .iter() - .find_map(|(name, value)| (name == property).then_some(*value)) - .map_or_else(|| self.null(), Ok), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - /// Returns whether one fake object property exists by name. - pub(super) fn runtime_property_is_initialized( - &self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => { - Ok(properties.iter().any(|(name, _)| name == property)) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - /// Writes one fake object property by name. - pub(super) fn runtime_property_set( - &mut self, - object: RuntimeCellHandle, - property: &str, - value: RuntimeCellHandle, - ) -> Result<(), EvalStatus> { - let id = object.as_ptr() as usize; - let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if let Some((_, existing_value)) = properties.iter_mut().find(|(name, _)| name == property) - { - *existing_value = value; - } else { - properties.push((property.to_string(), value)); - } - Ok(()) - } - /// Creates one shallow fake object clone, preserving stored property handles. - pub(super) fn runtime_object_clone_shallow( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - let id = object.as_ptr() as usize; - let properties = match self.get(object) { - FakeValue::Object(properties) => properties.clone(), - _ => return Err(EvalStatus::UnsupportedConstruct), - }; - let clone = self.alloc(FakeValue::Object(properties)); - if let Some(class_name) = self.object_classes.get(&id).cloned() { - self.object_classes - .insert(clone.as_ptr() as usize, class_name); - } - Ok(clone) - } - /// Returns the number of fake object properties in insertion order. - pub(super) fn runtime_object_property_len( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => Ok(properties.len()), - FakeValue::Iterator { .. } => Ok(0), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - /// Returns one fake object property key by insertion-order position. - pub(super) fn runtime_object_property_iter_key( - &mut self, - object: RuntimeCellHandle, - position: usize, - ) -> Result { - match self.get(object) { - FakeValue::Object(properties) => { - let Some((name, _)) = properties.get(position) else { - return self.null(); - }; - self.string(name) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - /// Calls one fake object method by name. - pub(super) fn runtime_method_call( - &mut self, - object: RuntimeCellHandle, - method: &str, - args: Vec, - ) -> Result { - let method = method.to_ascii_lowercase(); - match (self.get(object), method.as_str()) { - (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { - let id = object.as_ptr() as usize; - let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - *position = 0; - self.null() - } - (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { - self.bool_value(position < len) - } - (FakeValue::Iterator { .. }, "next") if args.is_empty() => { - let id = object.as_ptr() as usize; - let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - *position += 1; - self.null() - } - (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), - (FakeValue::Object(properties), "__tostring") if args.is_empty() => { - let class_name = self.object_classes.get(&(object.as_ptr() as usize)).cloned(); - self.reflection_type_to_string(class_name.as_deref(), &properties) - } - (FakeValue::Object(properties), "getname") if args.is_empty() => { - Self::object_property(&properties, "__name").map_or_else(|| self.string(""), Ok) - } - (FakeValue::Object(_), "getdoccomment" | "getextensionname") if args.is_empty() => { - self.bool_value(false) - } - (FakeValue::Object(_), "getextension") if args.is_empty() => self.null(), - (FakeValue::Object(properties), "getshortname") if args.is_empty() => { - Self::object_property(&properties, "__short_name") - .map_or_else(|| self.string(""), Ok) - } - (FakeValue::Object(properties), "getnamespacename") if args.is_empty() => { - Self::object_property(&properties, "__namespace_name") - .map_or_else(|| self.string(""), Ok) - } - (FakeValue::Object(properties), "innamespace") if args.is_empty() => { - Self::object_property(&properties, "__in_namespace") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isfinal") if args.is_empty() => { - Self::object_property(&properties, "__is_final") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isabstract") if args.is_empty() => { - Self::object_property(&properties, "__is_abstract") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isinterface") if args.is_empty() => { - Self::object_property(&properties, "__is_interface") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "istrait") if args.is_empty() => { - Self::object_property(&properties, "__is_trait") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isenum") if args.is_empty() => { - Self::object_property(&properties, "__is_enum") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isreadonly") if args.is_empty() => { - Self::object_property(&properties, "__is_readonly") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "ispromoted") if args.is_empty() => { - Self::object_property(&properties, "__is_promoted") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isvirtual") if args.is_empty() => { - Self::object_property(&properties, "__is_virtual") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isanonymous") if args.is_empty() => { - Self::object_property(&properties, "__is_anonymous") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isinstantiable") if args.is_empty() => { - Self::object_property(&properties, "__is_instantiable") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "iscloneable") if args.is_empty() => { - Self::object_property(&properties, "__is_cloneable") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isiterable" | "isiterateable") if args.is_empty() => { - Self::object_property(&properties, "__is_iterable") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isinternal") if args.is_empty() => { - Self::object_property(&properties, "__is_internal") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isuserdefined") if args.is_empty() => { - Self::object_property(&properties, "__is_user_defined") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isdeprecated") if args.is_empty() => { - Self::object_property(&properties, "__is_deprecated") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "getparentclass") if args.is_empty() => { - Self::object_property(&properties, "__parent_class") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "getconstructor") if args.is_empty() => { - Self::object_property(&properties, "__constructor").map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "getdeclaringclass") if args.is_empty() => { - Self::object_property(&properties, "__declaring_class") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "getdeclaringfunction") if args.is_empty() => { - Self::object_property(&properties, "__declaring_function") - .map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { - Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) - } - (FakeValue::Object(properties), "isprotectedset") if args.is_empty() => { - self.reflection_modifier_mask(&properties, 2048) - } - (FakeValue::Object(properties), "isprivateset") if args.is_empty() => { - self.reflection_modifier_mask(&properties, 4096) - } - (FakeValue::Object(properties), "getvalue") if args.is_empty() => { - Self::object_property(&properties, "__value").map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "getbackingvalue") if args.is_empty() => { - Self::object_property(&properties, "__backing_value") - .map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "isenumcase") if args.is_empty() => { - Self::object_property(&properties, "__is_enum_case") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isstatic") if args.is_empty() => { - Self::object_property(&properties, "__is_static") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "ispublic") if args.is_empty() => { - Self::object_property(&properties, "__is_public") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isprotected") if args.is_empty() => { - Self::object_property(&properties, "__is_protected") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isprivate") if args.is_empty() => { - Self::object_property(&properties, "__is_private") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "hasmethod") if args.len() == 1 => { - self.object_string_array_contains(&properties, "__method_names", args[0], true) - } - (FakeValue::Object(properties), "hasproperty") if args.len() == 1 => { - self.object_string_array_contains(&properties, "__property_names", args[0], false) - } - (FakeValue::Object(properties), "hasconstant") if args.len() == 1 => { - self.object_string_array_contains(&properties, "__constant_names", args[0], false) - } - (FakeValue::Object(properties), "getconstant") if args.len() == 1 => { - let Some(constants) = Self::object_property(&properties, "__constants") else { - return self.bool_value(false); - }; - let exists = self.runtime_array_key_exists(args[0], constants)?; - if matches!(self.get(exists), FakeValue::Bool(true)) { - self.runtime_array_get(constants, args[0]) - } else { - self.bool_value(false) - } - } - (FakeValue::Object(properties), "implementsinterface") if args.len() == 1 => { - let direct = self.object_string_array_contains( - &properties, - "__interface_names", - args[0], - true, - )?; - if matches!(self.get(direct), FakeValue::Bool(true)) { - return Ok(direct); - } - let Some(is_interface) = Self::object_property(&properties, "__is_interface") - else { - return Ok(direct); - }; - if !matches!(self.get(is_interface), FakeValue::Bool(true)) { - return Ok(direct); - } - let Some(reflected_name) = Self::object_property(&properties, "__name") else { - return Ok(direct); - }; - let FakeValue::String(reflected_name) = self.get(reflected_name) else { - return Ok(direct); - }; - let FakeValue::String(interface_name) = self.get(args[0]) else { - return Ok(direct); - }; - self.bool_value(reflected_name.eq_ignore_ascii_case(&interface_name)) - } - (FakeValue::Object(properties), "getinterfacenames") if args.is_empty() => { - Self::object_property(&properties, "__interface_names") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "getinterfaces") if args.is_empty() => { - self.object_relation_reflection_classes(&properties, "__interface_names") - } - (FakeValue::Object(properties), "gettraitnames") if args.is_empty() => { - Self::object_property(&properties, "__trait_names") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "gettraits") if args.is_empty() => { - self.object_relation_reflection_classes(&properties, "__trait_names") - } - (FakeValue::Object(properties), "getmethods") if args.is_empty() => { - Self::object_property(&properties, "__methods") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "getmethod") if args.len() == 1 => { - self.object_named_member(&properties, "__methods", args[0], true) - } - (FakeValue::Object(properties), "getproperties") if args.is_empty() => { - Self::object_property(&properties, "__properties") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "getproperty") if args.len() == 1 => { - self.object_named_member(&properties, "__properties", args[0], false) - } - (FakeValue::Object(properties), "getconstants") if args.is_empty() => { - Self::object_property(&properties, "__constants") - .map_or_else(|| self.runtime_assoc_new(0), Ok) - } - (FakeValue::Object(properties), "getreflectionconstants") if args.is_empty() => { - Self::object_property(&properties, "__reflection_constants") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "getreflectionconstant") if args.len() == 1 => self - .object_named_member_or_false( - &properties, - "__reflection_constants", - args[0], - false, - ), - (FakeValue::Object(properties), "getarguments") if args.is_empty() => { - Self::object_property(&properties, "__args") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "getparameters") if args.is_empty() => { - Self::object_property(&properties, "__parameters") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "getnumberofparameters") if args.is_empty() => { - match Self::object_property(&properties, "__parameters") { - Some(parameters) => { - let len = self.array_len(parameters)?; - self.int(len as i64) - } - None => self.int(0), - } - } - (FakeValue::Object(properties), "getnumberofrequiredparameters") if args.is_empty() => { - Self::object_property(&properties, "__required_parameter_count") - .map_or_else(|| self.int(0), Ok) - } - (FakeValue::Object(properties), "getposition") if args.is_empty() => { - Self::object_property(&properties, "__position").map_or_else(|| self.int(0), Ok) - } - (FakeValue::Object(properties), "gettype") if args.is_empty() => { - Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "getsettabletype") if args.is_empty() => { - Self::object_property(&properties, "__settable_type") - .map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "getclass") if args.is_empty() => { - Self::object_property(&properties, "__class").map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "isdynamic") if args.is_empty() => { - Self::object_property(&properties, "__is_dynamic") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "gettypes") if args.is_empty() => { - Self::object_property(&properties, "__types") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "getdefaultvalue") if args.is_empty() => { - Self::object_property(&properties, "__default_value") - .map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "getdefaultvalueconstantname") if args.is_empty() => { - Self::object_property(&properties, "__default_value_constant_name") - .map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "isconstructor") if args.is_empty() => { - let Some(name) = Self::object_property(&properties, "__name") else { - return self.bool_value(false); - }; - let FakeValue::String(name) = self.get(name) else { - return self.bool_value(false); - }; - self.bool_value(name.eq_ignore_ascii_case("__construct")) - } - (FakeValue::Object(properties), "isdestructor") if args.is_empty() => { - let Some(name) = Self::object_property(&properties, "__name") else { - return self.bool_value(false); - }; - let FakeValue::String(name) = self.get(name) else { - return self.bool_value(false); - }; - self.bool_value(name.eq_ignore_ascii_case("__destruct")) - } - (FakeValue::Object(properties), "isoptional") if args.is_empty() => { - Self::object_property(&properties, "__is_optional") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isvariadic") if args.is_empty() => { - Self::object_property(&properties, "__is_variadic") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "ispassedbyreference") if args.is_empty() => { - Self::object_property(&properties, "__is_passed_by_reference") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "canbepassedbyvalue") if args.is_empty() => { - let Some(by_ref) = Self::object_property(&properties, "__is_passed_by_reference") - else { - return self.bool_value(true); - }; - self.bool_value(!matches!(self.get(by_ref), FakeValue::Bool(true))) - } - (FakeValue::Object(properties), "hastype") if args.is_empty() => { - if let Some(has_type) = Self::object_property(&properties, "__has_type") { - return Ok(has_type); - } - match Self::object_property(&properties, "__type") { - Some(value) => self.bool_value(!matches!(self.get(value), FakeValue::Null)), - None => self.bool_value(false), - } - } - (FakeValue::Object(properties), "isdefaultvalueavailable" | "hasdefaultvalue") - if args.is_empty() => - { - Self::object_property(&properties, "__has_default_value") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isdefaultvalueconstant") if args.is_empty() => { - Self::object_property(&properties, "__is_default_value_constant") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isdefault") if args.is_empty() => { - match Self::object_property(&properties, "__is_dynamic") { - Some(is_dynamic) => { - self.bool_value(!matches!(self.get(is_dynamic), FakeValue::Bool(true))) - } - None => self.bool_value(true), - } - } - (FakeValue::Object(properties), "allowsnull") if args.is_empty() => { - Self::object_property(&properties, "__allows_null") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isarray") if args.is_empty() => { - Self::object_property(&properties, "__is_array_type") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "iscallable") if args.is_empty() => { - Self::object_property(&properties, "__is_callable_type") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(properties), "isbuiltin") if args.is_empty() => { - Self::object_property(&properties, "__is_builtin") - .map_or_else(|| self.bool_value(false), Ok) - } - (FakeValue::Object(_), "newinstance") if args.is_empty() => self.null(), - (FakeValue::Object(properties), "getattributes") if args.is_empty() => { - Self::object_property(&properties, "__attrs") - .map_or_else(|| self.runtime_array_new(0), Ok) - } - (FakeValue::Object(properties), "getmessage") if args.is_empty() => { - Self::object_property(&properties, "message").map_or_else(|| self.string(""), Ok) - } - (FakeValue::Object(properties), "getcode") if args.is_empty() => { - Self::object_property(&properties, "code").map_or_else(|| self.int(0), Ok) - } - (FakeValue::Object(properties), "read_x") => { - if !args.is_empty() { - return Err(EvalStatus::UnsupportedConstruct); - } - Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) - } - (FakeValue::Object(properties), "add_x") => { - let [arg] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; - let FakeValue::Int(x) = self.get(x) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(arg) = self.get(*arg) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(x + arg) - } - (FakeValue::Object(properties), "add2_x") => { - let [left, right] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; - let FakeValue::Int(x) = self.get(x) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(left) = self.get(*left) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(right) = self.get(*right) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(x + left + right) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - /// Calls one fake public static AOT method by class and method name. - pub(super) fn runtime_static_method_call( - &mut self, - class_name: &str, - method: &str, - args: Vec, - ) -> Result { - let method = method.to_ascii_lowercase(); - if !class_name.eq_ignore_ascii_case("KnownClass") { - return Err(EvalStatus::UnsupportedConstruct); - } - match method.as_str() { - "join" => { - let [left, right] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::String(left) = self.get(*left) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::String(right) = self.get(*right) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.string(&format!("{}{}", left, right)) - } - "sum" => { - let [left, right] = args.as_slice() else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(left) = self.get(*left) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::Int(right) = self.get(*right) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - self.int(left + right) - } - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - /// Materializes one fake populated `ReflectionAttribute` object. - pub(super) fn runtime_reflection_attribute_new( - &mut self, - name: &str, - args: RuntimeCellHandle, - target: u64, - repeated: bool, - ) -> Result { - let name = self.string(name)?; - let factory = self.int(0)?; - let target = self.int(target as i64)?; - let repeated = self.bool_value(repeated)?; - let object = self.alloc(FakeValue::Object(vec![ - ("__name".to_string(), name), - ("__args".to_string(), args), - ("__factory".to_string(), factory), - ("__target".to_string(), target), - ("__is_repeated".to_string(), repeated), - ])); - self.object_classes - .insert(object.as_ptr() as usize, "ReflectionAttribute".to_string()); - Ok(object) - } - /// Materializes one fake populated Reflection owner object. - pub(super) fn runtime_reflection_owner_new( - &mut self, - owner_kind: u64, - reflected_name: &str, - attrs: RuntimeCellHandle, - interface_names: RuntimeCellHandle, - trait_names: RuntimeCellHandle, - method_names: RuntimeCellHandle, - property_names: RuntimeCellHandle, - method_objects: RuntimeCellHandle, - property_objects: RuntimeCellHandle, - parent_class: RuntimeCellHandle, - flags: u64, - modifiers: u64, - method_modifiers: u64, - constant_value: RuntimeCellHandle, - backing_value: RuntimeCellHandle, - constructor: RuntimeCellHandle, - ) -> Result { - let class_name = match owner_kind { - EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", - EVAL_REFLECTION_OWNER_OBJECT => "ReflectionObject", - EVAL_REFLECTION_OWNER_ENUM => "ReflectionEnum", - EVAL_REFLECTION_OWNER_FUNCTION => "ReflectionFunction", - EVAL_REFLECTION_OWNER_METHOD => "ReflectionMethod", - EVAL_REFLECTION_OWNER_PROPERTY => "ReflectionProperty", - EVAL_REFLECTION_OWNER_CLASS_CONSTANT => "ReflectionClassConstant", - EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE => "ReflectionEnumUnitCase", - EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => "ReflectionEnumBackedCase", - EVAL_REFLECTION_OWNER_PARAMETER => "ReflectionParameter", - EVAL_REFLECTION_OWNER_NAMED_TYPE => "ReflectionNamedType", - EVAL_REFLECTION_OWNER_UNION_TYPE => "ReflectionUnionType", - EVAL_REFLECTION_OWNER_INTERSECTION_TYPE => "ReflectionIntersectionType", - _ => return Err(EvalStatus::RuntimeFatal), - }; - let name = self.string(reflected_name)?; - let is_final = self.bool_value((flags & 1) != 0)?; - let is_abstract = self.bool_value((flags & 2) != 0)?; - let is_interface = self.bool_value((flags & 4) != 0)?; - let is_trait = self.bool_value((flags & 8) != 0)?; - let is_enum = self.bool_value((flags & 16) != 0)?; - let is_readonly = self.bool_value((flags & 32) != 0)?; - let is_instantiable = self.bool_value((flags & 64) != 0)?; - let is_cloneable = self.bool_value((flags & 128) != 0)?; - let is_internal = self.bool_value((flags & 256) != 0)?; - let is_user_defined = self.bool_value((flags & 512) != 0)?; - let is_iterable = self.bool_value((flags & 1024) != 0)?; - let is_anonymous = self.bool_value((flags & 2048) != 0)?; - let modifiers_cell = self.int(modifiers as i64)?; - let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; - if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_CLASS - | EVAL_REFLECTION_OWNER_OBJECT - | EVAL_REFLECTION_OWNER_ENUM - ) { - let (namespace_name, short_name) = reflection_name_parts(reflected_name); - let has_namespace = !namespace_name.is_empty(); - let namespace_name = self.string(namespace_name)?; - let short_name = self.string(short_name)?; - let in_namespace = self.bool_value(has_namespace)?; - let constant_names = self.runtime_array_new(0)?; - let constants = self.runtime_assoc_new(0)?; - let reflection_constants = self.runtime_array_new(0)?; - properties.push(("__is_final".to_string(), is_final)); - properties.push(("__is_abstract".to_string(), is_abstract)); - properties.push(("__is_interface".to_string(), is_interface)); - properties.push(("__is_trait".to_string(), is_trait)); - properties.push(("__is_enum".to_string(), is_enum)); - properties.push(("__is_readonly".to_string(), is_readonly)); - properties.push(("__is_anonymous".to_string(), is_anonymous)); - properties.push(("__is_instantiable".to_string(), is_instantiable)); - properties.push(("__is_cloneable".to_string(), is_cloneable)); - properties.push(("__is_iterable".to_string(), is_iterable)); - properties.push(("__is_internal".to_string(), is_internal)); - properties.push(("__is_user_defined".to_string(), is_user_defined)); - properties.push(("__modifiers".to_string(), modifiers_cell)); - properties.push(("__short_name".to_string(), short_name)); - properties.push(("__namespace_name".to_string(), namespace_name)); - properties.push(("__in_namespace".to_string(), in_namespace)); - properties.push(("__interface_names".to_string(), interface_names)); - properties.push(("__trait_names".to_string(), trait_names)); - properties.push(("__method_names".to_string(), method_names)); - properties.push(("__property_names".to_string(), property_names)); - properties.push(("__constant_names".to_string(), constant_names)); - properties.push(("__constants".to_string(), constants)); - properties.push(("__reflection_constants".to_string(), reflection_constants)); - properties.push(("__methods".to_string(), method_objects)); - properties.push(("__constructor".to_string(), constructor)); - properties.push(("__parent_class".to_string(), parent_class)); - properties.push(("__properties".to_string(), property_objects)); - } - if owner_kind == EVAL_REFLECTION_OWNER_METHOD - || owner_kind == EVAL_REFLECTION_OWNER_PROPERTY - { - let is_static = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC) != 0)?; - let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; - let is_protected = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; - let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; - properties.push(("__is_static".to_string(), is_static)); - properties.push(("__is_public".to_string(), is_public)); - properties.push(("__is_protected".to_string(), is_protected)); - properties.push(("__is_private".to_string(), is_private)); - if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { - let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; - let is_abstract = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; - let is_readonly = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY) != 0)?; - let has_default_value = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE) != 0)?; - let is_promoted = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED) != 0)?; - let is_virtual = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL) != 0)?; - let is_dynamic = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC) != 0)?; - properties.push(("__is_final".to_string(), is_final)); - properties.push(("__is_abstract".to_string(), is_abstract)); - properties.push(("__is_readonly".to_string(), is_readonly)); - properties.push(("__modifiers".to_string(), modifiers_cell)); - properties.push(("__type".to_string(), method_objects)); - properties.push(("__settable_type".to_string(), constant_value)); - properties.push(("__has_default_value".to_string(), has_default_value)); - properties.push(("__is_promoted".to_string(), is_promoted)); - properties.push(("__is_virtual".to_string(), is_virtual)); - properties.push(("__is_dynamic".to_string(), is_dynamic)); - properties.push(("__default_value".to_string(), property_objects)); - } - } - if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_METHOD - | EVAL_REFLECTION_OWNER_PROPERTY - | EVAL_REFLECTION_OWNER_CLASS_CONSTANT - | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE - | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE - | EVAL_REFLECTION_OWNER_PARAMETER - ) { - properties.push(("__declaring_class".to_string(), parent_class)); - } - if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION - ) { - let is_deprecated = - self.bool_value((flags & EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED) != 0)?; - properties.push(("__parameters".to_string(), method_objects)); - properties.push(("__required_parameter_count".to_string(), modifiers_cell)); - properties.push(("__is_deprecated".to_string(), is_deprecated)); - } - if owner_kind == EVAL_REFLECTION_OWNER_METHOD { - let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; - let is_abstract = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; - let method_modifiers = self.int(method_modifiers as i64)?; - properties.push(("__is_final".to_string(), is_final)); - properties.push(("__is_abstract".to_string(), is_abstract)); - properties.push(("__modifiers".to_string(), method_modifiers)); - } - if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_CLASS_CONSTANT - | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE - | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE - ) { - let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; - let is_protected = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; - let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; - let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; - let is_enum_case = - self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE) != 0)?; - properties.push(("__is_public".to_string(), is_public)); - properties.push(("__is_protected".to_string(), is_protected)); - properties.push(("__is_private".to_string(), is_private)); - properties.push(("__is_final".to_string(), is_final)); - properties.push(("__is_enum_case".to_string(), is_enum_case)); - properties.push(("__modifiers".to_string(), modifiers_cell)); - } - if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { - properties.push(("__value".to_string(), constant_value)); - } - if matches!( - owner_kind, - EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE - ) { - properties.push(("__value".to_string(), constant_value)); - } - if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { - properties.push(("__backing_value".to_string(), backing_value)); - } - if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { - let position = self.int(modifiers as i64)?; - let is_optional = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL) != 0)?; - let is_variadic = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC) != 0)?; - let is_passed_by_reference = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_BY_REF) != 0)?; - let has_type = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE) != 0)?; - let has_default_value = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE) != 0)?; - let is_promoted = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED) != 0)?; - let allows_null = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL) != 0)?; - let is_default_value_constant = self - .bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT) != 0)?; - let is_array_type = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE) != 0)?; - let is_callable_type = - self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE) != 0)?; - let class_value = self.reflection_parameter_class_value(method_objects)?; - properties.push(("__position".to_string(), position)); - properties.push(("__is_optional".to_string(), is_optional)); - properties.push(("__is_variadic".to_string(), is_variadic)); - properties.push(( - "__is_passed_by_reference".to_string(), - is_passed_by_reference, - )); - properties.push(("__is_promoted".to_string(), is_promoted)); - properties.push(("__has_type".to_string(), has_type)); - properties.push(("__allows_null".to_string(), allows_null)); - properties.push(("__is_array_type".to_string(), is_array_type)); - properties.push(("__is_callable_type".to_string(), is_callable_type)); - properties.push(("__type".to_string(), method_objects)); - properties.push(("__class".to_string(), class_value)); - properties.push(("__has_default_value".to_string(), has_default_value)); - properties.push(( - "__is_default_value_constant".to_string(), - is_default_value_constant, - )); - properties.push(("__default_value_constant_name".to_string(), constant_value)); - properties.push(("__default_value".to_string(), property_objects)); - properties.push(("__declaring_function".to_string(), interface_names)); - } - if owner_kind == EVAL_REFLECTION_OWNER_NAMED_TYPE { - let allows_null = self.bool_value((flags & 1) != 0)?; - let is_builtin = self.bool_value((flags & 2) != 0)?; - properties.push(("__allows_null".to_string(), allows_null)); - properties.push(("__is_builtin".to_string(), is_builtin)); - } - if owner_kind == EVAL_REFLECTION_OWNER_UNION_TYPE { - let allows_null = self.bool_value((flags & 1) != 0)?; - properties.push(("__types".to_string(), method_objects)); - properties.push(("__allows_null".to_string(), allows_null)); - } - if owner_kind == EVAL_REFLECTION_OWNER_INTERSECTION_TYPE { - let allows_null = self.bool_value(false)?; - properties.push(("__types".to_string(), method_objects)); - properties.push(("__allows_null".to_string(), allows_null)); - } - let object = self.alloc(FakeValue::Object(properties)); - self.object_classes - .insert(object.as_ptr() as usize, class_name.to_string()); - Ok(object) - } - - /// Builds the fake `ReflectionParameter::getClass()` value from a named non-builtin type. - fn reflection_parameter_class_value( - &mut self, - type_value: RuntimeCellHandle, - ) -> Result { - if !self - .object_classes - .get(&(type_value.as_ptr() as usize)) - .is_some_and(|class_name| class_name == "ReflectionNamedType") - { - return self.null(); - } - let FakeValue::Object(type_properties) = self.get(type_value) else { - return self.null(); - }; - let is_builtin = Self::object_property(&type_properties, "__is_builtin") - .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); - if is_builtin { - return self.null(); - } - let Some(name) = Self::object_property(&type_properties, "__name") else { - return self.null(); - }; - let FakeValue::String(name) = self.get(name) else { - return self.null(); - }; - self.fake_reflection_class_object(&name) - } - - /// Checks whether a private fake object array property contains one string. - fn object_string_array_contains( - &mut self, - properties: &[(String, RuntimeCellHandle)], - property: &str, - needle: RuntimeCellHandle, - case_insensitive: bool, - ) -> Result { - let FakeValue::String(mut needle) = self.get(needle) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if case_insensitive { - needle = needle.to_ascii_lowercase(); - } - let Some(array) = Self::object_property(properties, property) else { - return self.bool_value(false); - }; - let contains = match self.get(array) { - FakeValue::Array(elements) => elements.iter().any(|element| match self.get(*element) { - FakeValue::String(value) if case_insensitive => { - value.to_ascii_lowercase() == needle - } - FakeValue::String(value) => value == needle, - _ => false, - }), - _ => false, - }; - self.bool_value(contains) - } - - /// Returns whether a fake Reflection owner stores one modifier bit. - fn reflection_modifier_mask( - &mut self, - properties: &[(String, RuntimeCellHandle)], - mask: i64, - ) -> Result { - let modifiers = Self::object_property(properties, "__modifiers") - .map(|handle| self.fake_int(&self.get(handle))) - .unwrap_or(0); - self.bool_value((modifiers & mask) != 0) - } - - /// Builds a name-keyed fake ReflectionClass map from a private string-array property. - fn object_relation_reflection_classes( - &mut self, - properties: &[(String, RuntimeCellHandle)], - property: &str, - ) -> Result { - let result = self.runtime_assoc_new(0)?; - let Some(array) = Self::object_property(properties, property) else { - return Ok(result); - }; - let FakeValue::Array(elements) = self.get(array) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - for element in elements { - let FakeValue::String(name) = self.get(element) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let key = self.runtime_string(&name)?; - let object = self.fake_reflection_class_object(&name)?; - self.runtime_array_set(result, key, object)?; - } - Ok(result) - } - - /// Builds a minimal fake ReflectionClass object with a working `getName()` slot. - fn fake_reflection_class_object( - &mut self, - class_name: &str, - ) -> Result { - let name = self.string(class_name)?; - let object = self.alloc(FakeValue::Object(vec![("__name".to_string(), name)])); - self.object_classes - .insert(object.as_ptr() as usize, "ReflectionClass".to_string()); - Ok(object) - } - - /// Formats fake ReflectionType objects through their synthetic `__toString()` method. - fn reflection_type_to_string( - &mut self, - class_name: Option<&str>, - properties: &[(String, RuntimeCellHandle)], - ) -> Result { - match class_name { - Some("ReflectionNamedType") => self.reflection_named_type_to_string(properties), - Some("ReflectionUnionType") => self.reflection_composite_type_to_string( - properties, - "|", - true, - ), - Some("ReflectionIntersectionType") => self.reflection_composite_type_to_string( - properties, - "&", - false, - ), - _ => Err(EvalStatus::UnsupportedConstruct), - } - } - - /// Formats one fake ReflectionNamedType object using retained name/nullability slots. - fn reflection_named_type_to_string( - &mut self, - properties: &[(String, RuntimeCellHandle)], - ) -> Result { - let Some(name) = Self::object_property(properties, "__name") else { - return self.string(""); - }; - let FakeValue::String(name) = self.get(name) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let allows_null = Self::object_property(properties, "__allows_null") - .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); - if allows_null && name != "mixed" { - self.string(&format!("?{name}")) - } else { - self.string(&name) - } - } - - /// Formats one fake ReflectionUnionType or ReflectionIntersectionType object. - fn reflection_composite_type_to_string( - &mut self, - properties: &[(String, RuntimeCellHandle)], - separator: &str, - append_null: bool, - ) -> Result { - let mut names = Vec::new(); - if let Some(types) = Self::object_property(properties, "__types") { - let FakeValue::Array(elements) = self.get(types) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - for element in elements { - let FakeValue::Object(type_properties) = self.get(element) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let Some(name) = Self::object_property(&type_properties, "__name") else { - return Err(EvalStatus::UnsupportedConstruct); - }; - let FakeValue::String(name) = self.get(name) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - names.push(name); - } - } - let allows_null = Self::object_property(properties, "__allows_null") - .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); - if append_null && allows_null { - names.push("null".to_string()); - } - self.string(&names.join(separator)) - } - - /// Finds one fake ReflectionMethod/ReflectionProperty object by its private name slot. - fn object_named_member( - &mut self, - properties: &[(String, RuntimeCellHandle)], - property: &str, - needle: RuntimeCellHandle, - case_insensitive: bool, - ) -> Result { - let FakeValue::String(mut needle) = self.get(needle) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if case_insensitive { - needle = needle.to_ascii_lowercase(); - } - let Some(array) = Self::object_property(properties, property) else { - return Err(EvalStatus::RuntimeFatal); - }; - let FakeValue::Array(elements) = self.get(array) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - for element in elements { - let FakeValue::Object(member_properties) = self.get(element) else { - continue; - }; - let Some(name) = Self::object_property(&member_properties, "__name") else { - continue; - }; - let FakeValue::String(mut name) = self.get(name) else { - continue; - }; - if case_insensitive { - name = name.to_ascii_lowercase(); - } - if name == needle { - return Ok(element); - } - } - Err(EvalStatus::RuntimeFatal) - } - - /// Finds one fake reflection member by name, returning PHP `false` when absent. - fn object_named_member_or_false( - &mut self, - properties: &[(String, RuntimeCellHandle)], - property: &str, - needle: RuntimeCellHandle, - case_insensitive: bool, - ) -> Result { - match self.object_named_member(properties, property, needle, case_insensitive) { - Ok(member) => Ok(member), - Err(EvalStatus::RuntimeFatal) => self.bool_value(false), - Err(status) => Err(status), - } - } - - /// Creates one fake object for eval `new` unit tests. - pub(super) fn runtime_new_object( - &mut self, - class_name: &str, - ) -> Result { - let object = self.alloc(FakeValue::Object(Vec::new())); - self.object_classes - .insert(object.as_ptr() as usize, class_name.to_string()); - Ok(object) - } - /// Applies fake constructor side effects for eval `new` unit tests. - pub(super) fn runtime_construct_object( - &mut self, - object: RuntimeCellHandle, - args: Vec, - ) -> Result<(), EvalStatus> { - let id = object.as_ptr() as usize; - let class_name = self.object_classes.get(&id).cloned(); - let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { - return Err(EvalStatus::UnsupportedConstruct); - }; - if class_name - .as_deref() - .is_some_and(fake_runtime_exception_like_class) - { - if let Some(message) = args.first().copied() { - if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "message") - { - *value = message; - } else { - properties.push(("message".to_string(), message)); - } - } - if let Some(code) = args.get(1).copied() { - if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "code") { - *value = code; - } else { - properties.push(("code".to_string(), code)); - } - } - return Ok(()); - } - if class_name - .as_deref() - .is_some_and(|name| name.eq_ignore_ascii_case("KnownFailingConstructor")) - { - return Err(EvalStatus::RuntimeFatal); - } - if let Some(first) = args.first().copied() { - if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { - *value = first; - } else { - properties.push(("x".to_string(), first)); - } - } - Ok(()) - } - /// Reports one fake AOT class for eval `class_exists` unit tests. - pub(super) fn runtime_class_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownClass") - || name.eq_ignore_ascii_case("KnownFailingConstructor")) - } - /// Reports fake generated AOT ReflectionClass flags for eval metadata unit tests. - pub(super) fn runtime_reflection_class_flags( - &mut self, - class_name: &str, - ) -> Result, EvalStatus> { - match class_name.to_ascii_lowercase().as_str() { - "knownabstract" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_ABSTRACT)), - "knownfinal" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_FINAL)), - "knownreadonly" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_READONLY)), - _ => Ok(None), - } - } - /// Reports fake generated AOT ReflectionMethod flags for eval metadata unit tests. - pub(super) fn runtime_reflection_method_flags( - &mut self, - class_name: &str, - method_name: &str, - ) -> Result, EvalStatus> { - if !class_name.eq_ignore_ascii_case("KnownClass") { - return Ok(None); - } - match method_name.to_ascii_lowercase().as_str() { - "answer" | "add_x" | "add2_x" | "read_x" | "run" => { - Ok(Some(EVAL_REFLECTION_MEMBER_FLAG_PUBLIC)) - } - "helper" => Ok(Some( - EVAL_REFLECTION_MEMBER_FLAG_STATIC | EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, - )), - "locked" => Ok(Some( - EVAL_REFLECTION_MEMBER_FLAG_PUBLIC | EVAL_REFLECTION_MEMBER_FLAG_FINAL, - )), - _ => Ok(None), - } - } - /// Reports fake generated AOT ReflectionMethod declaring classes for metadata unit tests. - pub(super) fn runtime_reflection_method_declaring_class( - &mut self, - class_name: &str, - method_name: &str, - ) -> Result, EvalStatus> { - if !class_name.eq_ignore_ascii_case("KnownClass") { - return Ok(None); - } - match method_name.to_ascii_lowercase().as_str() { - "answer" | "add_x" | "add2_x" | "read_x" | "run" | "helper" | "locked" => { - Ok(Some("KnownClass".to_string())) - } - _ => Ok(None), - } - } - /// Reports fake generated AOT ReflectionMethod names for eval metadata unit tests. - pub(super) fn runtime_reflection_method_names( - &mut self, - class_name: &str, - ) -> Result { - let mut array = self.runtime_string_array_new(3)?; - if class_name.eq_ignore_ascii_case("KnownClass") { - array = self.runtime_string_array_push(array, "run")?; - array = self.runtime_string_array_push(array, "helper")?; - array = self.runtime_string_array_push(array, "locked")?; - } - Ok(array) - } - /// Reports fake generated AOT ReflectionProperty flags for eval metadata unit tests. - pub(super) fn runtime_reflection_property_flags( - &mut self, - class_name: &str, - property_name: &str, - ) -> Result, EvalStatus> { - if class_name.eq_ignore_ascii_case("KnownClass") && property_name == "promoted" { - return Ok(Some( - EVAL_REFLECTION_MEMBER_FLAG_PUBLIC | EVAL_REFLECTION_MEMBER_FLAG_PROMOTED, - )); - } - Ok(None) - } - /// Reports fake generated AOT ReflectionProperty declaring classes for metadata unit tests. - pub(super) fn runtime_reflection_property_declaring_class( - &mut self, - class_name: &str, - property_name: &str, - ) -> Result, EvalStatus> { - if class_name.eq_ignore_ascii_case("KnownClass") && property_name == "promoted" { - Ok(Some("KnownClass".to_string())) - } else { - Ok(None) - } - } - /// Reports fake generated AOT ReflectionProperty names for eval metadata unit tests. - pub(super) fn runtime_reflection_property_names( - &mut self, - class_name: &str, - ) -> Result { - let mut array = self.runtime_string_array_new(1)?; - if class_name.eq_ignore_ascii_case("KnownClass") { - array = self.runtime_string_array_push(array, "promoted")?; - } - Ok(array) - } - /// Reports fake generated/AOT ReflectionClass interface names for metadata unit tests. - pub(super) fn runtime_reflection_class_interface_names( - &mut self, - class_name: &str, - ) -> Result { - let mut array = self.runtime_string_array_new(1)?; - if class_name.eq_ignore_ascii_case("KnownClass") { - array = self.runtime_string_array_push(array, "KnownInterface")?; - } else if class_name.eq_ignore_ascii_case("KnownInterface") { - array = self.runtime_string_array_push(array, "Traversable")?; - } - Ok(array) - } - /// Reports fake generated/AOT ReflectionClass trait names for metadata unit tests. - pub(super) fn runtime_reflection_class_trait_names( - &mut self, - class_name: &str, - ) -> Result { - let mut array = self.runtime_string_array_new(1)?; - if class_name.eq_ignore_ascii_case("KnownClass") { - array = self.runtime_string_array_push(array, "KnownTrait")?; - } else if class_name.eq_ignore_ascii_case("KnownTrait") { - array = self.runtime_string_array_push(array, "KnownInnerTrait")?; - } - Ok(array) - } - /// Reports fake generated/AOT ReflectionClass trait alias names for metadata unit tests. - pub(super) fn runtime_reflection_class_trait_alias_names( - &mut self, - class_name: &str, - ) -> Result { - let mut array = self.runtime_string_array_new(1)?; - if class_name.eq_ignore_ascii_case("KnownClass") { - array = self.runtime_string_array_push(array, "knownAlias")?; - } - Ok(array) - } - /// Reports fake generated/AOT ReflectionClass trait alias sources for metadata unit tests. - pub(super) fn runtime_reflection_class_trait_alias_sources( - &mut self, - class_name: &str, - ) -> Result { - let mut array = self.runtime_string_array_new(1)?; - if class_name.eq_ignore_ascii_case("KnownClass") { - array = self.runtime_string_array_push(array, "KnownTrait::source")?; - } - Ok(array) - } - /// Reports one fake AOT interface for eval `interface_exists` unit tests. - pub(super) fn runtime_interface_exists(&mut self, name: &str) -> Result { - Ok([ - "KnownInterface", - "ArrayAccess", - "Countable", - "Iterator", - "IteratorAggregate", - "JsonSerializable", - "OuterIterator", - "RecursiveIterator", - "SeekableIterator", - "SplObserver", - "SplSubject", - "Stringable", - "Throwable", - "Traversable", - ] - .iter() - .any(|known| name.eq_ignore_ascii_case(known))) - } - /// Reports one fake AOT trait for eval `trait_exists` unit tests. - pub(super) fn runtime_trait_exists(&mut self, name: &str) -> Result { - Ok(["KnownTrait", "KnownInnerTrait"] - .iter() - .any(|known| name.eq_ignore_ascii_case(known))) - } - /// Reports one fake AOT enum for eval `enum_exists` unit tests. - pub(super) fn runtime_enum_exists(&mut self, name: &str) -> Result { - Ok(name.eq_ignore_ascii_case("KnownEnum")) - } - /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. - pub(super) fn runtime_object_is_a( - &mut self, - object_or_class: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - ) -> Result { - let object_id = object_or_class.as_ptr() as usize; - match self.get(object_or_class) { - FakeValue::Object(_) if self.object_classes.contains_key(&object_id) => Ok(self - .object_classes - .get(&object_id) - .is_some_and(|class_name| { - fake_runtime_object_is_a(class_name, target_class, exclude_self) - })), - FakeValue::Object(_) - if target_class.eq_ignore_ascii_case("Exception") - || target_class.eq_ignore_ascii_case("Throwable") => - { - Ok(!exclude_self) - } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { - Ok(!exclude_self) - } - FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), - FakeValue::String(name) => { - Ok(fake_runtime_object_is_a(&name, target_class, exclude_self)) - } - _ => Ok(false), - } - } - /// Returns a fake PHP class name for object-tagged test values. - pub(super) fn runtime_object_class_name( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - let object_id = object.as_ptr() as usize; - if let Some(class_name) = self.object_classes.get(&object_id).cloned() { - return self.string(&class_name); - } - match self.get(object) { - FakeValue::Object(_) => self.string("stdClass"), - FakeValue::Iterator { .. } => self.string("Iterator"), - _ => Err(EvalStatus::RuntimeFatal), - } - } - /// Returns fake parent-class names for eval introspection unit tests. - pub(super) fn runtime_parent_class_name( - &mut self, - object_or_class: RuntimeCellHandle, - ) -> Result { - match self.get(object_or_class) { - FakeValue::Object(_) => self.string("ParentClass"), - FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { - self.string("ParentClass") - } - _ => self.string(""), - } - } - /// Returns the fake object handle as a stable object identity. - pub(super) fn runtime_object_identity( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - match self.get(object) { - FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), - _ => Err(EvalStatus::RuntimeFatal), - } - } -} - -/// Returns whether a fake runtime class stores PHP Throwable constructor state. -fn fake_runtime_exception_like_class(class_name: &str) -> bool { - [ - "Exception", - "JsonException", - "ReflectionException", - "Error", - "ValueError", - "TypeError", - ] - .iter() - .any(|known| class_name.eq_ignore_ascii_case(known)) -} - -/// Splits one PHP class-like name into namespace and short-name parts. -fn reflection_name_parts(reflected_name: &str) -> (&str, &str) { - match reflected_name.rfind('\\') { - Some(separator) => ( - &reflected_name[..separator], - &reflected_name[separator + 1..], - ), - None => ("", reflected_name), - } -} - -/// Checks the small fake Throwable inheritance graph used by eval interpreter tests. -fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: bool) -> bool { - if class_name.eq_ignore_ascii_case(target_class) { - return !exclude_self; - } - if class_name.eq_ignore_ascii_case("KnownClass") - && target_class.eq_ignore_ascii_case("ParentClass") - { - return true; - } - if class_name.eq_ignore_ascii_case("KnownClass") - && target_class.eq_ignore_ascii_case("KnownInterface") - { - return true; - } - if class_name.eq_ignore_ascii_case("ReflectionObject") - && target_class.eq_ignore_ascii_case("ReflectionClass") - { - return true; - } - if target_class.eq_ignore_ascii_case("Throwable") { - return fake_runtime_exception_like_class(class_name); - } - if target_class.eq_ignore_ascii_case("Exception") { - return ["Exception", "JsonException", "ReflectionException"] - .iter() - .any(|known| class_name.eq_ignore_ascii_case(known)); - } - if target_class.eq_ignore_ascii_case("Error") { - return ["Error", "ValueError", "TypeError"] - .iter() - .any(|known| class_name.eq_ignore_ascii_case(known)); - } - false -} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/class_runtime.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/class_runtime.rs new file mode 100644 index 0000000000..7f9ee6f645 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/class_runtime.rs @@ -0,0 +1,324 @@ +//! Purpose: +//! Fake runtime operations for object construction, class-like existence, +//! reflection metadata, inheritance checks, class names, and object identity. +//! +//! Called from: +//! - `FakeOps`'s object/class `RuntimeValueOps` methods. +//! +//! Key details: +//! - The intentionally small fake class graph is shared with relation helpers. + +use super::*; + +impl FakeOps { + + /// Creates one fake object for eval `new` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_new_object( + &mut self, + class_name: &str, + ) -> Result { + let object = self.alloc(FakeValue::Object(Vec::new())); + self.object_classes + .insert(object.as_ptr() as usize, class_name.to_string()); + Ok(object) + } + /// Applies fake constructor side effects for eval `new` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let class_name = self.object_classes.get(&id).cloned(); + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if class_name + .as_deref() + .is_some_and(fake_runtime_exception_like_class) + { + if let Some(message) = args.first().copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "message") + { + *value = message; + } else { + properties.push(("message".to_string(), message)); + } + } + if let Some(code) = args.get(1).copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "code") { + *value = code; + } else { + properties.push(("code".to_string(), code)); + } + } + return Ok(()); + } + if class_name + .as_deref() + .is_some_and(|name| name.eq_ignore_ascii_case("KnownFailingConstructor")) + { + return Err(EvalStatus::RuntimeFatal); + } + if let Some(first) = args.first().copied() { + if let Some((_, value)) = properties.iter_mut().find(|(name, _)| name == "x") { + *value = first; + } else { + properties.push(("x".to_string(), first)); + } + } + Ok(()) + } + /// Reports one fake AOT class for eval `class_exists` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_class_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownClass") + || name.eq_ignore_ascii_case("KnownFailingConstructor")) + } + /// Reports fake generated AOT ReflectionClass flags for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_flags( + &mut self, + class_name: &str, + ) -> Result, EvalStatus> { + match class_name.to_ascii_lowercase().as_str() { + "knownabstract" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_ABSTRACT)), + "knownfinal" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_FINAL)), + "knownreadonly" => Ok(Some(EVAL_REFLECTION_CLASS_FLAG_READONLY)), + _ => Ok(None), + } + } + /// Reports fake generated AOT ReflectionMethod flags for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Ok(None); + } + match method_name.to_ascii_lowercase().as_str() { + "answer" | "add_x" | "add2_x" | "read_x" | "run" => { + Ok(Some(EVAL_REFLECTION_MEMBER_FLAG_PUBLIC)) + } + "helper" => Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_STATIC | EVAL_REFLECTION_MEMBER_FLAG_PROTECTED, + )), + "locked" => Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_PUBLIC | EVAL_REFLECTION_MEMBER_FLAG_FINAL, + )), + _ => Ok(None), + } + } + /// Reports fake generated AOT ReflectionMethod declaring classes for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Ok(None); + } + match method_name.to_ascii_lowercase().as_str() { + "answer" | "add_x" | "add2_x" | "read_x" | "run" | "helper" | "locked" => { + Ok(Some("KnownClass".to_string())) + } + _ => Ok(None), + } + } + /// Reports fake generated AOT ReflectionMethod names for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(3)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "run")?; + array = self.runtime_string_array_push(array, "helper")?; + array = self.runtime_string_array_push(array, "locked")?; + } + Ok(array) + } + /// Reports fake generated AOT ReflectionProperty flags for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + if class_name.eq_ignore_ascii_case("KnownClass") && property_name == "promoted" { + return Ok(Some( + EVAL_REFLECTION_MEMBER_FLAG_PUBLIC | EVAL_REFLECTION_MEMBER_FLAG_PROMOTED, + )); + } + Ok(None) + } + /// Reports fake generated AOT ReflectionProperty declaring classes for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + if class_name.eq_ignore_ascii_case("KnownClass") && property_name == "promoted" { + Ok(Some("KnownClass".to_string())) + } else { + Ok(None) + } + } + /// Reports fake generated AOT ReflectionProperty names for eval metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "promoted")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass interface names for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownInterface")?; + } else if class_name.eq_ignore_ascii_case("KnownInterface") { + array = self.runtime_string_array_push(array, "Traversable")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass trait names for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownTrait")?; + } else if class_name.eq_ignore_ascii_case("KnownTrait") { + array = self.runtime_string_array_push(array, "KnownInnerTrait")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass trait alias names for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "knownAlias")?; + } + Ok(array) + } + /// Reports fake generated/AOT ReflectionClass trait alias sources for metadata unit tests. + pub(in crate::interpreter::tests::support) fn runtime_reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + let mut array = self.runtime_string_array_new(1)?; + if class_name.eq_ignore_ascii_case("KnownClass") { + array = self.runtime_string_array_push(array, "KnownTrait::source")?; + } + Ok(array) + } + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_interface_exists(&mut self, name: &str) -> Result { + Ok([ + "KnownInterface", + "ArrayAccess", + "Countable", + "Iterator", + "IteratorAggregate", + "JsonSerializable", + "OuterIterator", + "RecursiveIterator", + "SeekableIterator", + "SplObserver", + "SplSubject", + "Stringable", + "Throwable", + "Traversable", + ] + .iter() + .any(|known| name.eq_ignore_ascii_case(known))) + } + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_trait_exists(&mut self, name: &str) -> Result { + Ok(["KnownTrait", "KnownInnerTrait"] + .iter() + .any(|known| name.eq_ignore_ascii_case(known))) + } + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_enum_exists(&mut self, name: &str) -> Result { + Ok(name.eq_ignore_ascii_case("KnownEnum")) + } + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + pub(in crate::interpreter::tests::support) fn runtime_object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + let object_id = object_or_class.as_ptr() as usize; + match self.get(object_or_class) { + FakeValue::Object(_) if self.object_classes.contains_key(&object_id) => Ok(self + .object_classes + .get(&object_id) + .is_some_and(|class_name| { + fake_runtime_object_is_a(class_name, target_class, exclude_self) + })), + FakeValue::Object(_) + if target_class.eq_ignore_ascii_case("Exception") + || target_class.eq_ignore_ascii_case("Throwable") => + { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("KnownClass") => { + Ok(!exclude_self) + } + FakeValue::Object(_) if target_class.eq_ignore_ascii_case("ParentClass") => Ok(true), + FakeValue::String(name) => { + Ok(fake_runtime_object_is_a(&name, target_class, exclude_self)) + } + _ => Ok(false), + } + } + /// Returns a fake PHP class name for object-tagged test values. + pub(in crate::interpreter::tests::support) fn runtime_object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + let object_id = object.as_ptr() as usize; + if let Some(class_name) = self.object_classes.get(&object_id).cloned() { + return self.string(&class_name); + } + match self.get(object) { + FakeValue::Object(_) => self.string("stdClass"), + FakeValue::Iterator { .. } => self.string("Iterator"), + _ => Err(EvalStatus::RuntimeFatal), + } + } + /// Returns fake parent-class names for eval introspection unit tests. + pub(in crate::interpreter::tests::support) fn runtime_parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + match self.get(object_or_class) { + FakeValue::Object(_) => self.string("ParentClass"), + FakeValue::String(name) if name.eq_ignore_ascii_case("ChildClass") => { + self.string("ParentClass") + } + _ => self.string(""), + } + } + /// Returns the fake object handle as a stable object identity. + pub(in crate::interpreter::tests::support) fn runtime_object_identity( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(_) | FakeValue::Iterator { .. } => Ok(object.as_ptr() as u64), + _ => Err(EvalStatus::RuntimeFatal), + } + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/method_dispatch.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/method_dispatch.rs new file mode 100644 index 0000000000..b6b6066de2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/method_dispatch.rs @@ -0,0 +1,448 @@ +//! Purpose: +//! Implements fake runtime instance-method dispatch used by interpreter tests. +//! +//! Called from: +//! - `FakeOps`'s `RuntimeValueOps::method_call()` implementation. +//! +//! Key details: +//! - The match models only registered test doubles and their observable effects. + +use super::*; + +impl FakeOps { + + /// Calls one fake object method by name. + pub(in crate::interpreter::tests::support) fn runtime_method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + let method = method.to_ascii_lowercase(); + match (self.get(object), method.as_str()) { + (FakeValue::Iterator { .. }, "rewind") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position = 0; + self.null() + } + (FakeValue::Iterator { len, position }, "valid") if args.is_empty() => { + self.bool_value(position < len) + } + (FakeValue::Iterator { .. }, "next") if args.is_empty() => { + let id = object.as_ptr() as usize; + let Some(FakeValue::Iterator { position, .. }) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + *position += 1; + self.null() + } + (FakeValue::Object(_), "answer") if args.is_empty() => self.int(42), + (FakeValue::Object(properties), "__tostring") if args.is_empty() => { + let class_name = self.object_classes.get(&(object.as_ptr() as usize)).cloned(); + self.reflection_type_to_string(class_name.as_deref(), &properties) + } + (FakeValue::Object(properties), "getname") if args.is_empty() => { + Self::object_property(&properties, "__name").map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(_), "getdoccomment" | "getextensionname") if args.is_empty() => { + self.bool_value(false) + } + (FakeValue::Object(_), "getextension") if args.is_empty() => self.null(), + (FakeValue::Object(properties), "getshortname") if args.is_empty() => { + Self::object_property(&properties, "__short_name") + .map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "getnamespacename") if args.is_empty() => { + Self::object_property(&properties, "__namespace_name") + .map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "innamespace") if args.is_empty() => { + Self::object_property(&properties, "__in_namespace") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isfinal") if args.is_empty() => { + Self::object_property(&properties, "__is_final") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isabstract") if args.is_empty() => { + Self::object_property(&properties, "__is_abstract") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isinterface") if args.is_empty() => { + Self::object_property(&properties, "__is_interface") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "istrait") if args.is_empty() => { + Self::object_property(&properties, "__is_trait") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isenum") if args.is_empty() => { + Self::object_property(&properties, "__is_enum") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isreadonly") if args.is_empty() => { + Self::object_property(&properties, "__is_readonly") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "ispromoted") if args.is_empty() => { + Self::object_property(&properties, "__is_promoted") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isvirtual") if args.is_empty() => { + Self::object_property(&properties, "__is_virtual") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isanonymous") if args.is_empty() => { + Self::object_property(&properties, "__is_anonymous") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isinstantiable") if args.is_empty() => { + Self::object_property(&properties, "__is_instantiable") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "iscloneable") if args.is_empty() => { + Self::object_property(&properties, "__is_cloneable") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isiterable" | "isiterateable") if args.is_empty() => { + Self::object_property(&properties, "__is_iterable") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isinternal") if args.is_empty() => { + Self::object_property(&properties, "__is_internal") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isuserdefined") if args.is_empty() => { + Self::object_property(&properties, "__is_user_defined") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isdeprecated") if args.is_empty() => { + Self::object_property(&properties, "__is_deprecated") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "getparentclass") if args.is_empty() => { + Self::object_property(&properties, "__parent_class") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "getconstructor") if args.is_empty() => { + Self::object_property(&properties, "__constructor").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getdeclaringclass") if args.is_empty() => { + Self::object_property(&properties, "__declaring_class") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "getdeclaringfunction") if args.is_empty() => { + Self::object_property(&properties, "__declaring_function") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getmodifiers") if args.is_empty() => { + Self::object_property(&properties, "__modifiers").map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "isprotectedset") if args.is_empty() => { + self.reflection_modifier_mask(&properties, 2048) + } + (FakeValue::Object(properties), "isprivateset") if args.is_empty() => { + self.reflection_modifier_mask(&properties, 4096) + } + (FakeValue::Object(properties), "getvalue") if args.is_empty() => { + Self::object_property(&properties, "__value").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getbackingvalue") if args.is_empty() => { + Self::object_property(&properties, "__backing_value") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "isenumcase") if args.is_empty() => { + Self::object_property(&properties, "__is_enum_case") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isstatic") if args.is_empty() => { + Self::object_property(&properties, "__is_static") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "ispublic") if args.is_empty() => { + Self::object_property(&properties, "__is_public") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isprotected") if args.is_empty() => { + Self::object_property(&properties, "__is_protected") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isprivate") if args.is_empty() => { + Self::object_property(&properties, "__is_private") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "hasmethod") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__method_names", args[0], true) + } + (FakeValue::Object(properties), "hasproperty") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__property_names", args[0], false) + } + (FakeValue::Object(properties), "hasconstant") if args.len() == 1 => { + self.object_string_array_contains(&properties, "__constant_names", args[0], false) + } + (FakeValue::Object(properties), "getconstant") if args.len() == 1 => { + let Some(constants) = Self::object_property(&properties, "__constants") else { + return self.bool_value(false); + }; + let exists = self.runtime_array_key_exists(args[0], constants)?; + if matches!(self.get(exists), FakeValue::Bool(true)) { + self.runtime_array_get(constants, args[0]) + } else { + self.bool_value(false) + } + } + (FakeValue::Object(properties), "implementsinterface") if args.len() == 1 => { + let direct = self.object_string_array_contains( + &properties, + "__interface_names", + args[0], + true, + )?; + if matches!(self.get(direct), FakeValue::Bool(true)) { + return Ok(direct); + } + let Some(is_interface) = Self::object_property(&properties, "__is_interface") + else { + return Ok(direct); + }; + if !matches!(self.get(is_interface), FakeValue::Bool(true)) { + return Ok(direct); + } + let Some(reflected_name) = Self::object_property(&properties, "__name") else { + return Ok(direct); + }; + let FakeValue::String(reflected_name) = self.get(reflected_name) else { + return Ok(direct); + }; + let FakeValue::String(interface_name) = self.get(args[0]) else { + return Ok(direct); + }; + self.bool_value(reflected_name.eq_ignore_ascii_case(&interface_name)) + } + (FakeValue::Object(properties), "getinterfacenames") if args.is_empty() => { + Self::object_property(&properties, "__interface_names") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getinterfaces") if args.is_empty() => { + self.object_relation_reflection_classes(&properties, "__interface_names") + } + (FakeValue::Object(properties), "gettraitnames") if args.is_empty() => { + Self::object_property(&properties, "__trait_names") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "gettraits") if args.is_empty() => { + self.object_relation_reflection_classes(&properties, "__trait_names") + } + (FakeValue::Object(properties), "getmethods") if args.is_empty() => { + Self::object_property(&properties, "__methods") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getmethod") if args.len() == 1 => { + self.object_named_member(&properties, "__methods", args[0], true) + } + (FakeValue::Object(properties), "getproperties") if args.is_empty() => { + Self::object_property(&properties, "__properties") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getproperty") if args.len() == 1 => { + self.object_named_member(&properties, "__properties", args[0], false) + } + (FakeValue::Object(properties), "getconstants") if args.is_empty() => { + Self::object_property(&properties, "__constants") + .map_or_else(|| self.runtime_assoc_new(0), Ok) + } + (FakeValue::Object(properties), "getreflectionconstants") if args.is_empty() => { + Self::object_property(&properties, "__reflection_constants") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getreflectionconstant") if args.len() == 1 => self + .object_named_member_or_false( + &properties, + "__reflection_constants", + args[0], + false, + ), + (FakeValue::Object(properties), "getarguments") if args.is_empty() => { + Self::object_property(&properties, "__args") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getparameters") if args.is_empty() => { + Self::object_property(&properties, "__parameters") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getnumberofparameters") if args.is_empty() => { + match Self::object_property(&properties, "__parameters") { + Some(parameters) => { + let len = self.array_len(parameters)?; + self.int(len as i64) + } + None => self.int(0), + } + } + (FakeValue::Object(properties), "getnumberofrequiredparameters") if args.is_empty() => { + Self::object_property(&properties, "__required_parameter_count") + .map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "getposition") if args.is_empty() => { + Self::object_property(&properties, "__position").map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "gettype") if args.is_empty() => { + Self::object_property(&properties, "__type").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getsettabletype") if args.is_empty() => { + Self::object_property(&properties, "__settable_type") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getclass") if args.is_empty() => { + Self::object_property(&properties, "__class").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "isdynamic") if args.is_empty() => { + Self::object_property(&properties, "__is_dynamic") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "gettypes") if args.is_empty() => { + Self::object_property(&properties, "__types") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getdefaultvalue") if args.is_empty() => { + Self::object_property(&properties, "__default_value") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "getdefaultvalueconstantname") if args.is_empty() => { + Self::object_property(&properties, "__default_value_constant_name") + .map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "isconstructor") if args.is_empty() => { + let Some(name) = Self::object_property(&properties, "__name") else { + return self.bool_value(false); + }; + let FakeValue::String(name) = self.get(name) else { + return self.bool_value(false); + }; + self.bool_value(name.eq_ignore_ascii_case("__construct")) + } + (FakeValue::Object(properties), "isdestructor") if args.is_empty() => { + let Some(name) = Self::object_property(&properties, "__name") else { + return self.bool_value(false); + }; + let FakeValue::String(name) = self.get(name) else { + return self.bool_value(false); + }; + self.bool_value(name.eq_ignore_ascii_case("__destruct")) + } + (FakeValue::Object(properties), "isoptional") if args.is_empty() => { + Self::object_property(&properties, "__is_optional") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isvariadic") if args.is_empty() => { + Self::object_property(&properties, "__is_variadic") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "ispassedbyreference") if args.is_empty() => { + Self::object_property(&properties, "__is_passed_by_reference") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "canbepassedbyvalue") if args.is_empty() => { + let Some(by_ref) = Self::object_property(&properties, "__is_passed_by_reference") + else { + return self.bool_value(true); + }; + self.bool_value(!matches!(self.get(by_ref), FakeValue::Bool(true))) + } + (FakeValue::Object(properties), "hastype") if args.is_empty() => { + if let Some(has_type) = Self::object_property(&properties, "__has_type") { + return Ok(has_type); + } + match Self::object_property(&properties, "__type") { + Some(value) => self.bool_value(!matches!(self.get(value), FakeValue::Null)), + None => self.bool_value(false), + } + } + (FakeValue::Object(properties), "isdefaultvalueavailable" | "hasdefaultvalue") + if args.is_empty() => + { + Self::object_property(&properties, "__has_default_value") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isdefaultvalueconstant") if args.is_empty() => { + Self::object_property(&properties, "__is_default_value_constant") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isdefault") if args.is_empty() => { + match Self::object_property(&properties, "__is_dynamic") { + Some(is_dynamic) => { + self.bool_value(!matches!(self.get(is_dynamic), FakeValue::Bool(true))) + } + None => self.bool_value(true), + } + } + (FakeValue::Object(properties), "allowsnull") if args.is_empty() => { + Self::object_property(&properties, "__allows_null") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isarray") if args.is_empty() => { + Self::object_property(&properties, "__is_array_type") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "iscallable") if args.is_empty() => { + Self::object_property(&properties, "__is_callable_type") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(properties), "isbuiltin") if args.is_empty() => { + Self::object_property(&properties, "__is_builtin") + .map_or_else(|| self.bool_value(false), Ok) + } + (FakeValue::Object(_), "newinstance") if args.is_empty() => self.null(), + (FakeValue::Object(properties), "getattributes") if args.is_empty() => { + Self::object_property(&properties, "__attrs") + .map_or_else(|| self.runtime_array_new(0), Ok) + } + (FakeValue::Object(properties), "getmessage") if args.is_empty() => { + Self::object_property(&properties, "message").map_or_else(|| self.string(""), Ok) + } + (FakeValue::Object(properties), "getcode") if args.is_empty() => { + Self::object_property(&properties, "code").map_or_else(|| self.int(0), Ok) + } + (FakeValue::Object(properties), "read_x") => { + if !args.is_empty() { + return Err(EvalStatus::UnsupportedConstruct); + } + Self::object_property(&properties, "x").map_or_else(|| self.null(), Ok) + } + (FakeValue::Object(properties), "add_x") => { + let [arg] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(arg) = self.get(*arg) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + arg) + } + (FakeValue::Object(properties), "add2_x") => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let x = Self::object_property(&properties, "x").ok_or(EvalStatus::RuntimeFatal)?; + let FakeValue::Int(x) = self.get(x) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(x + left + right) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/properties.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/properties.rs new file mode 100644 index 0000000000..af12031ef2 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/properties.rs @@ -0,0 +1,105 @@ +//! Purpose: +//! Fake runtime operations for object property reads, writes, initialization, +//! shallow cloning, and property iteration. +//! +//! Called from: +//! - `FakeOps`'s `RuntimeValueOps` implementation in interpreter tests. +//! +//! Key details: +//! - Operations mutate only the in-memory fake value store. + +use super::*; + +impl FakeOps { + /// Reads one fake object property by name. + pub(in crate::interpreter::tests::support) fn runtime_property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => properties + .iter() + .find_map(|(name, value)| (name == property).then_some(*value)) + .map_or_else(|| self.null(), Ok), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns whether one fake object property exists by name. + pub(in crate::interpreter::tests::support) fn runtime_property_is_initialized( + &self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + Ok(properties.iter().any(|(name, _)| name == property)) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Writes one fake object property by name. + pub(in crate::interpreter::tests::support) fn runtime_property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let id = object.as_ptr() as usize; + let Some(FakeValue::Object(properties)) = self.values.get_mut(&id) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if let Some((_, existing_value)) = properties.iter_mut().find(|(name, _)| name == property) + { + *existing_value = value; + } else { + properties.push((property.to_string(), value)); + } + Ok(()) + } + /// Creates one shallow fake object clone, preserving stored property handles. + pub(in crate::interpreter::tests::support) fn runtime_object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + let id = object.as_ptr() as usize; + let properties = match self.get(object) { + FakeValue::Object(properties) => properties.clone(), + _ => return Err(EvalStatus::UnsupportedConstruct), + }; + let clone = self.alloc(FakeValue::Object(properties)); + if let Some(class_name) = self.object_classes.get(&id).cloned() { + self.object_classes + .insert(clone.as_ptr() as usize, class_name); + } + Ok(clone) + } + /// Returns the number of fake object properties in insertion order. + pub(in crate::interpreter::tests::support) fn runtime_object_property_len( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => Ok(properties.len()), + FakeValue::Iterator { .. } => Ok(0), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + /// Returns one fake object property key by insertion-order position. + pub(in crate::interpreter::tests::support) fn runtime_object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + match self.get(object) { + FakeValue::Object(properties) => { + let Some((name, _)) = properties.get(position) else { + return self.null(); + }; + self.string(name) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_construction.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_construction.rs new file mode 100644 index 0000000000..6ad4de08b3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_construction.rs @@ -0,0 +1,300 @@ +//! Purpose: +//! Builds fake ReflectionAttribute and reflection owner/member objects for +//! interpreter tests. +//! +//! Called from: +//! - Reflection-related `RuntimeValueOps` hooks on `FakeOps`. +//! +//! Key details: +//! - Object layouts mirror the metadata consumed by eval Reflection builtins. + +use super::*; + +impl FakeOps { + + /// Materializes one fake populated `ReflectionAttribute` object. + pub(in crate::interpreter::tests::support) fn runtime_reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + target: u64, + repeated: bool, + ) -> Result { + let name = self.string(name)?; + let factory = self.int(0)?; + let target = self.int(target as i64)?; + let repeated = self.bool_value(repeated)?; + let object = self.alloc(FakeValue::Object(vec![ + ("__name".to_string(), name), + ("__args".to_string(), args), + ("__factory".to_string(), factory), + ("__target".to_string(), target), + ("__is_repeated".to_string(), repeated), + ])); + self.object_classes + .insert(object.as_ptr() as usize, "ReflectionAttribute".to_string()); + Ok(object) + } + /// Materializes one fake populated Reflection owner object. + pub(in crate::interpreter::tests::support) fn runtime_reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, + ) -> Result { + let class_name = match owner_kind { + EVAL_REFLECTION_OWNER_CLASS => "ReflectionClass", + EVAL_REFLECTION_OWNER_OBJECT => "ReflectionObject", + EVAL_REFLECTION_OWNER_ENUM => "ReflectionEnum", + EVAL_REFLECTION_OWNER_FUNCTION => "ReflectionFunction", + EVAL_REFLECTION_OWNER_METHOD => "ReflectionMethod", + EVAL_REFLECTION_OWNER_PROPERTY => "ReflectionProperty", + EVAL_REFLECTION_OWNER_CLASS_CONSTANT => "ReflectionClassConstant", + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE => "ReflectionEnumUnitCase", + EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE => "ReflectionEnumBackedCase", + EVAL_REFLECTION_OWNER_PARAMETER => "ReflectionParameter", + EVAL_REFLECTION_OWNER_NAMED_TYPE => "ReflectionNamedType", + EVAL_REFLECTION_OWNER_UNION_TYPE => "ReflectionUnionType", + EVAL_REFLECTION_OWNER_INTERSECTION_TYPE => "ReflectionIntersectionType", + _ => return Err(EvalStatus::RuntimeFatal), + }; + let name = self.string(reflected_name)?; + let is_final = self.bool_value((flags & 1) != 0)?; + let is_abstract = self.bool_value((flags & 2) != 0)?; + let is_interface = self.bool_value((flags & 4) != 0)?; + let is_trait = self.bool_value((flags & 8) != 0)?; + let is_enum = self.bool_value((flags & 16) != 0)?; + let is_readonly = self.bool_value((flags & 32) != 0)?; + let is_instantiable = self.bool_value((flags & 64) != 0)?; + let is_cloneable = self.bool_value((flags & 128) != 0)?; + let is_internal = self.bool_value((flags & 256) != 0)?; + let is_user_defined = self.bool_value((flags & 512) != 0)?; + let is_iterable = self.bool_value((flags & 1024) != 0)?; + let is_anonymous = self.bool_value((flags & 2048) != 0)?; + let modifiers_cell = self.int(modifiers as i64)?; + let mut properties = vec![("__name".to_string(), name), ("__attrs".to_string(), attrs)]; + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS + | EVAL_REFLECTION_OWNER_OBJECT + | EVAL_REFLECTION_OWNER_ENUM + ) { + let (namespace_name, short_name) = reflection_name_parts(reflected_name); + let has_namespace = !namespace_name.is_empty(); + let namespace_name = self.string(namespace_name)?; + let short_name = self.string(short_name)?; + let in_namespace = self.bool_value(has_namespace)?; + let constant_names = self.runtime_array_new(0)?; + let constants = self.runtime_assoc_new(0)?; + let reflection_constants = self.runtime_array_new(0)?; + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); + properties.push(("__is_interface".to_string(), is_interface)); + properties.push(("__is_trait".to_string(), is_trait)); + properties.push(("__is_enum".to_string(), is_enum)); + properties.push(("__is_readonly".to_string(), is_readonly)); + properties.push(("__is_anonymous".to_string(), is_anonymous)); + properties.push(("__is_instantiable".to_string(), is_instantiable)); + properties.push(("__is_cloneable".to_string(), is_cloneable)); + properties.push(("__is_iterable".to_string(), is_iterable)); + properties.push(("__is_internal".to_string(), is_internal)); + properties.push(("__is_user_defined".to_string(), is_user_defined)); + properties.push(("__modifiers".to_string(), modifiers_cell)); + properties.push(("__short_name".to_string(), short_name)); + properties.push(("__namespace_name".to_string(), namespace_name)); + properties.push(("__in_namespace".to_string(), in_namespace)); + properties.push(("__interface_names".to_string(), interface_names)); + properties.push(("__trait_names".to_string(), trait_names)); + properties.push(("__method_names".to_string(), method_names)); + properties.push(("__property_names".to_string(), property_names)); + properties.push(("__constant_names".to_string(), constant_names)); + properties.push(("__constants".to_string(), constants)); + properties.push(("__reflection_constants".to_string(), reflection_constants)); + properties.push(("__methods".to_string(), method_objects)); + properties.push(("__constructor".to_string(), constructor)); + properties.push(("__parent_class".to_string(), parent_class)); + properties.push(("__properties".to_string(), property_objects)); + } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD + || owner_kind == EVAL_REFLECTION_OWNER_PROPERTY + { + let is_static = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_STATIC) != 0)?; + let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; + let is_protected = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; + let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; + properties.push(("__is_static".to_string(), is_static)); + properties.push(("__is_public".to_string(), is_public)); + properties.push(("__is_protected".to_string(), is_protected)); + properties.push(("__is_private".to_string(), is_private)); + if owner_kind == EVAL_REFLECTION_OWNER_PROPERTY { + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_abstract = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; + let is_readonly = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_READONLY) != 0)?; + let has_default_value = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_HAS_DEFAULT_VALUE) != 0)?; + let is_promoted = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROMOTED) != 0)?; + let is_virtual = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_VIRTUAL) != 0)?; + let is_dynamic = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_DYNAMIC) != 0)?; + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); + properties.push(("__is_readonly".to_string(), is_readonly)); + properties.push(("__modifiers".to_string(), modifiers_cell)); + properties.push(("__type".to_string(), method_objects)); + properties.push(("__settable_type".to_string(), constant_value)); + properties.push(("__has_default_value".to_string(), has_default_value)); + properties.push(("__is_promoted".to_string(), is_promoted)); + properties.push(("__is_virtual".to_string(), is_virtual)); + properties.push(("__is_dynamic".to_string(), is_dynamic)); + properties.push(("__default_value".to_string(), property_objects)); + } + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD + | EVAL_REFLECTION_OWNER_PROPERTY + | EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + | EVAL_REFLECTION_OWNER_PARAMETER + ) { + properties.push(("__declaring_class".to_string(), parent_class)); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_METHOD | EVAL_REFLECTION_OWNER_FUNCTION + ) { + let is_deprecated = + self.bool_value((flags & EVAL_REFLECTION_CALLABLE_FLAG_DEPRECATED) != 0)?; + properties.push(("__parameters".to_string(), method_objects)); + properties.push(("__required_parameter_count".to_string(), modifiers_cell)); + properties.push(("__is_deprecated".to_string(), is_deprecated)); + } + if owner_kind == EVAL_REFLECTION_OWNER_METHOD { + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_abstract = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ABSTRACT) != 0)?; + let method_modifiers = self.int(method_modifiers as i64)?; + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_abstract".to_string(), is_abstract)); + properties.push(("__modifiers".to_string(), method_modifiers)); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_CLASS_CONSTANT + | EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE + | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + let is_public = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PUBLIC) != 0)?; + let is_protected = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PROTECTED) != 0)?; + let is_private = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_PRIVATE) != 0)?; + let is_final = self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_FINAL) != 0)?; + let is_enum_case = + self.bool_value((flags & EVAL_REFLECTION_MEMBER_FLAG_ENUM_CASE) != 0)?; + properties.push(("__is_public".to_string(), is_public)); + properties.push(("__is_protected".to_string(), is_protected)); + properties.push(("__is_private".to_string(), is_private)); + properties.push(("__is_final".to_string(), is_final)); + properties.push(("__is_enum_case".to_string(), is_enum_case)); + properties.push(("__modifiers".to_string(), modifiers_cell)); + } + if owner_kind == EVAL_REFLECTION_OWNER_CLASS_CONSTANT { + properties.push(("__value".to_string(), constant_value)); + } + if matches!( + owner_kind, + EVAL_REFLECTION_OWNER_ENUM_UNIT_CASE | EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE + ) { + properties.push(("__value".to_string(), constant_value)); + } + if owner_kind == EVAL_REFLECTION_OWNER_ENUM_BACKED_CASE { + properties.push(("__backing_value".to_string(), backing_value)); + } + if owner_kind == EVAL_REFLECTION_OWNER_PARAMETER { + let position = self.int(modifiers as i64)?; + let is_optional = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_OPTIONAL) != 0)?; + let is_variadic = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_VARIADIC) != 0)?; + let is_passed_by_reference = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_BY_REF) != 0)?; + let has_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_TYPE) != 0)?; + let has_default_value = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_HAS_DEFAULT_VALUE) != 0)?; + let is_promoted = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_PROMOTED) != 0)?; + let allows_null = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ALLOWS_NULL) != 0)?; + let is_default_value_constant = self + .bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_DEFAULT_VALUE_CONSTANT) != 0)?; + let is_array_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_ARRAY_TYPE) != 0)?; + let is_callable_type = + self.bool_value((flags & EVAL_REFLECTION_PARAMETER_FLAG_CALLABLE_TYPE) != 0)?; + let class_value = self.reflection_parameter_class_value(method_objects)?; + properties.push(("__position".to_string(), position)); + properties.push(("__is_optional".to_string(), is_optional)); + properties.push(("__is_variadic".to_string(), is_variadic)); + properties.push(( + "__is_passed_by_reference".to_string(), + is_passed_by_reference, + )); + properties.push(("__is_promoted".to_string(), is_promoted)); + properties.push(("__has_type".to_string(), has_type)); + properties.push(("__allows_null".to_string(), allows_null)); + properties.push(("__is_array_type".to_string(), is_array_type)); + properties.push(("__is_callable_type".to_string(), is_callable_type)); + properties.push(("__type".to_string(), method_objects)); + properties.push(("__class".to_string(), class_value)); + properties.push(("__has_default_value".to_string(), has_default_value)); + properties.push(( + "__is_default_value_constant".to_string(), + is_default_value_constant, + )); + properties.push(("__default_value_constant_name".to_string(), constant_value)); + properties.push(("__default_value".to_string(), property_objects)); + properties.push(("__declaring_function".to_string(), interface_names)); + } + if owner_kind == EVAL_REFLECTION_OWNER_NAMED_TYPE { + let allows_null = self.bool_value((flags & 1) != 0)?; + let is_builtin = self.bool_value((flags & 2) != 0)?; + properties.push(("__allows_null".to_string(), allows_null)); + properties.push(("__is_builtin".to_string(), is_builtin)); + } + if owner_kind == EVAL_REFLECTION_OWNER_UNION_TYPE { + let allows_null = self.bool_value((flags & 1) != 0)?; + properties.push(("__types".to_string(), method_objects)); + properties.push(("__allows_null".to_string(), allows_null)); + } + if owner_kind == EVAL_REFLECTION_OWNER_INTERSECTION_TYPE { + let allows_null = self.bool_value(false)?; + properties.push(("__types".to_string(), method_objects)); + properties.push(("__allows_null".to_string(), allows_null)); + } + let object = self.alloc(FakeValue::Object(properties)); + self.object_classes + .insert(object.as_ptr() as usize, class_name.to_string()); + Ok(object) + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_helpers.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_helpers.rs new file mode 100644 index 0000000000..2f91fe1b57 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/reflection_helpers.rs @@ -0,0 +1,252 @@ +//! Purpose: +//! Shared fake reflection value, modifier, type-formatting, and named-member +//! helpers. +//! +//! Called from: +//! - Fake reflection owner construction and metadata lookup modules. +//! +//! Key details: +//! - Missing members retain the false-versus-fatal behavior expected by PHP APIs. + +use super::*; + +impl FakeOps { + + /// Builds the fake `ReflectionParameter::getClass()` value from a named non-builtin type. + pub(super) fn reflection_parameter_class_value( + &mut self, + type_value: RuntimeCellHandle, + ) -> Result { + if !self + .object_classes + .get(&(type_value.as_ptr() as usize)) + .is_some_and(|class_name| class_name == "ReflectionNamedType") + { + return self.null(); + } + let FakeValue::Object(type_properties) = self.get(type_value) else { + return self.null(); + }; + let is_builtin = Self::object_property(&type_properties, "__is_builtin") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if is_builtin { + return self.null(); + } + let Some(name) = Self::object_property(&type_properties, "__name") else { + return self.null(); + }; + let FakeValue::String(name) = self.get(name) else { + return self.null(); + }; + self.fake_reflection_class_object(&name) + } + + /// Checks whether a private fake object array property contains one string. + pub(super) fn object_string_array_contains( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + let FakeValue::String(mut needle) = self.get(needle) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if case_insensitive { + needle = needle.to_ascii_lowercase(); + } + let Some(array) = Self::object_property(properties, property) else { + return self.bool_value(false); + }; + let contains = match self.get(array) { + FakeValue::Array(elements) => elements.iter().any(|element| match self.get(*element) { + FakeValue::String(value) if case_insensitive => { + value.to_ascii_lowercase() == needle + } + FakeValue::String(value) => value == needle, + _ => false, + }), + _ => false, + }; + self.bool_value(contains) + } + + /// Returns whether a fake Reflection owner stores one modifier bit. + pub(super) fn reflection_modifier_mask( + &mut self, + properties: &[(String, RuntimeCellHandle)], + mask: i64, + ) -> Result { + let modifiers = Self::object_property(properties, "__modifiers") + .map(|handle| self.fake_int(&self.get(handle))) + .unwrap_or(0); + self.bool_value((modifiers & mask) != 0) + } + + /// Builds a name-keyed fake ReflectionClass map from a private string-array property. + pub(super) fn object_relation_reflection_classes( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + ) -> Result { + let result = self.runtime_assoc_new(0)?; + let Some(array) = Self::object_property(properties, property) else { + return Ok(result); + }; + let FakeValue::Array(elements) = self.get(array) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::String(name) = self.get(element) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let key = self.runtime_string(&name)?; + let object = self.fake_reflection_class_object(&name)?; + self.runtime_array_set(result, key, object)?; + } + Ok(result) + } + + /// Builds a minimal fake ReflectionClass object with a working `getName()` slot. + pub(super) fn fake_reflection_class_object( + &mut self, + class_name: &str, + ) -> Result { + let name = self.string(class_name)?; + let object = self.alloc(FakeValue::Object(vec![("__name".to_string(), name)])); + self.object_classes + .insert(object.as_ptr() as usize, "ReflectionClass".to_string()); + Ok(object) + } + + /// Formats fake ReflectionType objects through their synthetic `__toString()` method. + pub(super) fn reflection_type_to_string( + &mut self, + class_name: Option<&str>, + properties: &[(String, RuntimeCellHandle)], + ) -> Result { + match class_name { + Some("ReflectionNamedType") => self.reflection_named_type_to_string(properties), + Some("ReflectionUnionType") => self.reflection_composite_type_to_string( + properties, + "|", + true, + ), + Some("ReflectionIntersectionType") => self.reflection_composite_type_to_string( + properties, + "&", + false, + ), + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + + /// Formats one fake ReflectionNamedType object using retained name/nullability slots. + pub(super) fn reflection_named_type_to_string( + &mut self, + properties: &[(String, RuntimeCellHandle)], + ) -> Result { + let Some(name) = Self::object_property(properties, "__name") else { + return self.string(""); + }; + let FakeValue::String(name) = self.get(name) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let allows_null = Self::object_property(properties, "__allows_null") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if allows_null && name != "mixed" { + self.string(&format!("?{name}")) + } else { + self.string(&name) + } + } + + /// Formats one fake ReflectionUnionType or ReflectionIntersectionType object. + pub(super) fn reflection_composite_type_to_string( + &mut self, + properties: &[(String, RuntimeCellHandle)], + separator: &str, + append_null: bool, + ) -> Result { + let mut names = Vec::new(); + if let Some(types) = Self::object_property(properties, "__types") { + let FakeValue::Array(elements) = self.get(types) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::Object(type_properties) = self.get(element) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let Some(name) = Self::object_property(&type_properties, "__name") else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(name) = self.get(name) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + names.push(name); + } + } + let allows_null = Self::object_property(properties, "__allows_null") + .is_some_and(|value| matches!(self.get(value), FakeValue::Bool(true))); + if append_null && allows_null { + names.push("null".to_string()); + } + self.string(&names.join(separator)) + } + + /// Finds one fake ReflectionMethod/ReflectionProperty object by its private name slot. + pub(super) fn object_named_member( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + let FakeValue::String(mut needle) = self.get(needle) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + if case_insensitive { + needle = needle.to_ascii_lowercase(); + } + let Some(array) = Self::object_property(properties, property) else { + return Err(EvalStatus::RuntimeFatal); + }; + let FakeValue::Array(elements) = self.get(array) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + for element in elements { + let FakeValue::Object(member_properties) = self.get(element) else { + continue; + }; + let Some(name) = Self::object_property(&member_properties, "__name") else { + continue; + }; + let FakeValue::String(mut name) = self.get(name) else { + continue; + }; + if case_insensitive { + name = name.to_ascii_lowercase(); + } + if name == needle { + return Ok(element); + } + } + Err(EvalStatus::RuntimeFatal) + } + + /// Finds one fake reflection member by name, returning PHP `false` when absent. + pub(super) fn object_named_member_or_false( + &mut self, + properties: &[(String, RuntimeCellHandle)], + property: &str, + needle: RuntimeCellHandle, + case_insensitive: bool, + ) -> Result { + match self.object_named_member(properties, property, needle, case_insensitive) { + Ok(member) => Ok(member), + Err(EvalStatus::RuntimeFatal) => self.bool_value(false), + Err(status) => Err(status), + } + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/relations.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/relations.rs new file mode 100644 index 0000000000..68f69d476c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/relations.rs @@ -0,0 +1,70 @@ +//! Purpose: +//! Models the small fake class-name, namespace, Throwable, and inheritance graph +//! used by interpreter tests. +//! +//! Called from: +//! - Fake object construction, reflection, and `is_a` runtime operations. +//! +//! Key details: +//! - Matching is case-insensitive to mirror PHP class-like names. + +/// Returns whether a fake runtime class stores PHP Throwable constructor state. +pub(super) fn fake_runtime_exception_like_class(class_name: &str) -> bool { + [ + "Exception", + "JsonException", + "ReflectionException", + "Error", + "ValueError", + "TypeError", + ] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)) +} + +/// Splits one PHP class-like name into namespace and short-name parts. +pub(super) fn reflection_name_parts(reflected_name: &str) -> (&str, &str) { + match reflected_name.rfind('\\') { + Some(separator) => ( + &reflected_name[..separator], + &reflected_name[separator + 1..], + ), + None => ("", reflected_name), + } +} + +/// Checks the small fake Throwable inheritance graph used by eval interpreter tests. +pub(super) fn fake_runtime_object_is_a(class_name: &str, target_class: &str, exclude_self: bool) -> bool { + if class_name.eq_ignore_ascii_case(target_class) { + return !exclude_self; + } + if class_name.eq_ignore_ascii_case("KnownClass") + && target_class.eq_ignore_ascii_case("ParentClass") + { + return true; + } + if class_name.eq_ignore_ascii_case("KnownClass") + && target_class.eq_ignore_ascii_case("KnownInterface") + { + return true; + } + if class_name.eq_ignore_ascii_case("ReflectionObject") + && target_class.eq_ignore_ascii_case("ReflectionClass") + { + return true; + } + if target_class.eq_ignore_ascii_case("Throwable") { + return fake_runtime_exception_like_class(class_name); + } + if target_class.eq_ignore_ascii_case("Exception") { + return ["Exception", "JsonException", "ReflectionException"] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)); + } + if target_class.eq_ignore_ascii_case("Error") { + return ["Error", "ValueError", "TypeError"] + .iter() + .any(|known| class_name.eq_ignore_ascii_case(known)); + } + false +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/object_ops/static_dispatch.rs b/crates/elephc-magician/src/interpreter/tests/support/object_ops/static_dispatch.rs new file mode 100644 index 0000000000..af0383dc2a --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/object_ops/static_dispatch.rs @@ -0,0 +1,54 @@ +//! Purpose: +//! Implements fake runtime static-method dispatch for AOT bridge tests. +//! +//! Called from: +//! - `FakeOps`'s `RuntimeValueOps::static_method_call()` implementation. +//! +//! Key details: +//! - Unsupported class/method pairs fail with the existing eval status. + +use super::*; + +impl FakeOps { + + /// Calls one fake public static AOT method by class and method name. + pub(in crate::interpreter::tests::support) fn runtime_static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + let method = method.to_ascii_lowercase(); + if !class_name.eq_ignore_ascii_case("KnownClass") { + return Err(EvalStatus::UnsupportedConstruct); + } + match method.as_str() { + "join" => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::String(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.string(&format!("{}{}", left, right)) + } + "sum" => { + let [left, right] = args.as_slice() else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(left) = self.get(*left) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + let FakeValue::Int(right) = self.get(*right) else { + return Err(EvalStatus::UnsupportedConstruct); + }; + self.int(left + right) + } + _ => Err(EvalStatus::UnsupportedConstruct), + } + } + +} diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs index 54a5bcad2c..90a27e4aca 100644 --- a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops.rs @@ -12,671 +12,22 @@ use super::*; +mod collection_calls; +mod construction_raw; +mod lifecycle_scalars; +mod numeric_string; +mod reflection; + +use collection_calls::impl_fake_collection_call_ops; +use construction_raw::impl_fake_construction_raw_ops; +use lifecycle_scalars::impl_fake_lifecycle_scalar_ops; +use numeric_string::impl_fake_numeric_string_ops; +use reflection::impl_fake_reflection_ops; + impl RuntimeValueOps for FakeOps { - /// Creates a fake indexed array cell. - fn array_new(&mut self, capacity: usize) -> Result { - self.runtime_array_new(capacity) - } - /// Creates a fake direct-string indexed array cell. - fn string_array_new(&mut self, capacity: usize) -> Result { - self.runtime_string_array_new(capacity) - } - /// Appends one string to a fake direct-string indexed array cell. - fn string_array_push( - &mut self, - array: RuntimeCellHandle, - value: &str, - ) -> Result { - self.runtime_string_array_push(array, value) - } - /// Creates a fake associative array cell. - fn assoc_new(&mut self, _capacity: usize) -> Result { - self.runtime_assoc_new(_capacity) - } - /// Reads one fake indexed array element. - fn array_get( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - ) -> Result { - self.runtime_array_get(array, index) - } - /// Checks whether a fake array has the requested key without reading its value. - fn array_key_exists( - &mut self, - key: RuntimeCellHandle, - array: RuntimeCellHandle, - ) -> Result { - self.runtime_array_key_exists(key, array) - } - /// Returns one fake foreach key by insertion-order position. - fn array_iter_key( - &mut self, - array: RuntimeCellHandle, - position: usize, - ) -> Result { - self.runtime_array_iter_key(array, position) - } - /// Writes one fake indexed or associative array element. - fn array_set( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - value: RuntimeCellHandle, - ) -> Result { - self.runtime_array_set(array, index, value) - } - /// Reads one fake object property by name. - fn property_get( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - self.runtime_property_get(object, property) - } - /// Checks whether one fake object property exists by name. - fn property_is_initialized( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - self.runtime_property_is_initialized(object, property) - } - /// Writes one fake object property by name. - fn property_set( - &mut self, - object: RuntimeCellHandle, - property: &str, - value: RuntimeCellHandle, - ) -> Result<(), EvalStatus> { - self.runtime_property_set(object, property, value) - } - /// Reports no fake AOT static property match. - fn static_property_get( - &mut self, - _class_name: &str, - _property: &str, - ) -> Result, EvalStatus> { - Ok(None) - } - /// Reports no fake AOT static property initialization match. - fn static_property_is_initialized( - &mut self, - _class_name: &str, - _property: &str, - ) -> Result { - Ok(false) - } - /// Reports a failed fake AOT static property write. - fn static_property_set( - &mut self, - _class_name: &str, - _property: &str, - _value: RuntimeCellHandle, - ) -> Result { - Ok(false) - } - /// Reports no fake AOT class constant match. - fn class_constant_get( - &mut self, - _class_name: &str, - _constant: &str, - ) -> Result, EvalStatus> { - Ok(None) - } - /// Creates one shallow fake object clone. - fn object_clone_shallow( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - self.runtime_object_clone_shallow(object) - } - /// Returns the number of fake object properties in insertion order. - fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { - self.runtime_object_property_len(object) - } - /// Returns one fake object property key by insertion-order position. - fn object_property_iter_key( - &mut self, - object: RuntimeCellHandle, - position: usize, - ) -> Result { - self.runtime_object_property_iter_key(object, position) - } - /// Calls one fake object method by name. - fn method_call( - &mut self, - object: RuntimeCellHandle, - method: &str, - args: Vec, - ) -> Result { - self.runtime_method_call(object, method, args) - } - /// Calls one fake static runtime method by class and method name. - fn static_method_call( - &mut self, - class_name: &str, - method: &str, - args: Vec, - ) -> Result { - self.runtime_static_method_call(class_name, method, args) - } - /// Materializes one fake `ReflectionAttribute` object for eval metadata tests. - fn reflection_attribute_new( - &mut self, - name: &str, - args: RuntimeCellHandle, - target: u64, - repeated: bool, - ) -> Result { - self.runtime_reflection_attribute_new(name, args, target, repeated) - } - /// Materializes one fake Reflection owner object for eval metadata tests. - fn reflection_owner_new( - &mut self, - owner_kind: u64, - reflected_name: &str, - attrs: RuntimeCellHandle, - interface_names: RuntimeCellHandle, - trait_names: RuntimeCellHandle, - method_names: RuntimeCellHandle, - property_names: RuntimeCellHandle, - method_objects: RuntimeCellHandle, - property_objects: RuntimeCellHandle, - parent_class: RuntimeCellHandle, - flags: u64, - modifiers: u64, - method_modifiers: u64, - constant_value: RuntimeCellHandle, - backing_value: RuntimeCellHandle, - constructor: RuntimeCellHandle, - ) -> Result { - self.runtime_reflection_owner_new( - owner_kind, - reflected_name, - attrs, - interface_names, - trait_names, - method_names, - property_names, - method_objects, - property_objects, - parent_class, - flags, - modifiers, - method_modifiers, - constant_value, - backing_value, - constructor, - ) - } - /// Reports fake generated AOT ReflectionMethod flags for metadata bridge tests. - fn reflection_method_flags( - &mut self, - class_name: &str, - method_name: &str, - ) -> Result, EvalStatus> { - self.runtime_reflection_method_flags(class_name, method_name) - } - /// Reports fake generated AOT ReflectionMethod declaring classes for metadata bridge tests. - fn reflection_method_declaring_class( - &mut self, - class_name: &str, - method_name: &str, - ) -> Result, EvalStatus> { - self.runtime_reflection_method_declaring_class(class_name, method_name) - } - /// Reports fake generated AOT ReflectionMethod names for metadata bridge tests. - fn reflection_method_names( - &mut self, - class_name: &str, - ) -> Result { - self.runtime_reflection_method_names(class_name) - } - /// Reports fake generated AOT ReflectionClass flags for metadata bridge tests. - fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { - self.runtime_reflection_class_flags(class_name) - } - /// Reports fake generated AOT ReflectionProperty flags for metadata bridge tests. - fn reflection_property_flags( - &mut self, - class_name: &str, - property_name: &str, - ) -> Result, EvalStatus> { - self.runtime_reflection_property_flags(class_name, property_name) - } - /// Reports fake generated AOT ReflectionProperty declaring classes for metadata bridge tests. - fn reflection_property_declaring_class( - &mut self, - class_name: &str, - property_name: &str, - ) -> Result, EvalStatus> { - self.runtime_reflection_property_declaring_class(class_name, property_name) - } - /// Reports fake generated AOT ReflectionProperty names for metadata bridge tests. - fn reflection_property_names( - &mut self, - class_name: &str, - ) -> Result { - self.runtime_reflection_property_names(class_name) - } - /// Reports no fake generated AOT ReflectionClassConstant value. - fn reflection_constant_value( - &mut self, - _class_name: &str, - _constant_name: &str, - ) -> Result, EvalStatus> { - Ok(None) - } - /// Reports no fake generated AOT ReflectionClassConstant flags. - fn reflection_constant_flags( - &mut self, - _class_name: &str, - _constant_name: &str, - ) -> Result, EvalStatus> { - Ok(None) - } - /// Reports no fake generated AOT ReflectionClassConstant declaring class. - fn reflection_constant_declaring_class( - &mut self, - _class_name: &str, - _constant_name: &str, - ) -> Result, EvalStatus> { - Ok(None) - } - /// Reports an empty fake generated AOT ReflectionClassConstant name list. - fn reflection_constant_names( - &mut self, - _class_name: &str, - ) -> Result { - self.runtime_string_array_new(0) - } - /// Reports fake generated/AOT ReflectionClass interface names for metadata bridge tests. - fn reflection_class_interface_names( - &mut self, - class_name: &str, - ) -> Result { - self.runtime_reflection_class_interface_names(class_name) - } - /// Reports fake generated/AOT ReflectionClass trait names for metadata bridge tests. - fn reflection_class_trait_names( - &mut self, - class_name: &str, - ) -> Result { - self.runtime_reflection_class_trait_names(class_name) - } - /// Reports fake generated/AOT ReflectionClass trait alias names for metadata bridge tests. - fn reflection_class_trait_alias_names( - &mut self, - class_name: &str, - ) -> Result { - self.runtime_reflection_class_trait_alias_names(class_name) - } - /// Reports fake generated/AOT ReflectionClass trait alias sources for metadata bridge tests. - fn reflection_class_trait_alias_sources( - &mut self, - class_name: &str, - ) -> Result { - self.runtime_reflection_class_trait_alias_sources(class_name) - } - /// Creates one fake object for eval `new` unit tests. - fn new_object(&mut self, _class_name: &str) -> Result { - self.runtime_new_object(_class_name) - } - /// Applies fake constructor side effects for eval `new` unit tests. - fn construct_object( - &mut self, - object: RuntimeCellHandle, - args: Vec, - ) -> Result<(), EvalStatus> { - self.runtime_construct_object(object, args) - } - /// Reports one fake AOT class for eval `class_exists` unit tests. - fn class_exists(&mut self, name: &str) -> Result { - self.runtime_class_exists(name) - } - /// Reports one fake AOT interface for eval `interface_exists` unit tests. - fn interface_exists(&mut self, name: &str) -> Result { - self.runtime_interface_exists(name) - } - /// Reports one fake AOT trait for eval `trait_exists` unit tests. - fn trait_exists(&mut self, name: &str) -> Result { - self.runtime_trait_exists(name) - } - /// Reports one fake AOT enum for eval `enum_exists` unit tests. - fn enum_exists(&mut self, name: &str) -> Result { - self.runtime_enum_exists(name) - } - /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. - fn object_is_a( - &mut self, - object_or_class: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - ) -> Result { - self.runtime_object_is_a(object_or_class, target_class, exclude_self) - } - /// Returns a fake PHP class name for object-tagged test values. - fn object_class_name( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - self.runtime_object_class_name(object) - } - /// Returns fake parent-class names for eval introspection unit tests. - fn parent_class_name( - &mut self, - object_or_class: RuntimeCellHandle, - ) -> Result { - self.runtime_parent_class_name(object_or_class) - } - /// Returns the visible element count for fake array values. - fn array_len(&mut self, array: RuntimeCellHandle) -> Result { - self.runtime_array_len(array) - } - /// Returns whether a fake runtime cell is an indexed or associative array. - fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_is_array_like(value) - } - /// Returns whether a fake runtime cell is null. - fn is_null(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_is_null(value) - } - /// Returns the fake runtime tag corresponding to a test value. - fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_type_tag(value) - } - /// Creates a fake invoker-only by-reference marker. - fn invoker_ref_cell( - &mut self, - slot: *mut RuntimeCellHandle, - ) -> Result { - Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) - } - /// Creates a fake invoker-only raw by-reference marker. - fn invoker_raw_ref_cell( - &mut self, - slot: *mut std::ffi::c_void, - _source_tag: u64, - ) -> Result { - Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) - } - /// Extracts one fake low payload word for raw by-reference staging. - fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { - Ok(match self.get(value) { - FakeValue::Bool(value) => u64::from(value), - FakeValue::Float(value) => value.to_bits(), - FakeValue::Int(value) => value as u64, - FakeValue::String(_) | FakeValue::Bytes(_) => value.as_ptr() as u64, - FakeValue::Array(_) - | FakeValue::Assoc(_) - | FakeValue::Object(_) - | FakeValue::Iterator { .. } => value.as_ptr() as u64, - _ => 0, - }) - } - /// Extracts one fake high payload word for raw by-reference staging. - fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result { - Ok(match self.get(value) { - FakeValue::String(value) => value.len() as u64, - FakeValue::Bytes(value) => value.len() as u64, - _ => 0, - }) - } - /// Retains a fake raw string payload for native by-reference staging. - fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus> { - self.runtime_retain(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell))?; - Ok((ptr, len)) - } - /// Converts a fake raw string payload back to its stable fake handle. - fn raw_string_value(&mut self, ptr: u64, _len: u64) -> Result { - Ok(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell)) - } - /// Records release of a fake raw string payload owned by a staged slot. - fn release_raw_string_words(&mut self, ptr: u64, _len: u64) -> Result<(), EvalStatus> { - self.runtime_release(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell)) - } - /// Retains a fake raw heap word for native by-reference staging. - fn retain_raw_heap_word(&mut self, word: u64) -> Result { - self.runtime_retain(RuntimeCellHandle::from_raw(word as *mut RuntimeCell))?; - Ok(word) - } - /// Boxes one fake one-word raw payload with the provided runtime tag. - fn raw_word_value( - &mut self, - source_tag: u64, - word: u64, - ) -> Result { - match source_tag { - EVAL_TAG_INT => self.runtime_int(word as i64), - EVAL_TAG_FLOAT => self.runtime_float(f64::from_bits(word)), - EVAL_TAG_BOOL => self.runtime_bool_value(word != 0), - EVAL_TAG_RESOURCE => self.runtime_resource(word as i64), - EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { - Ok(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) - } - _ => Err(EvalStatus::RuntimeFatal), - } - } - /// Converts a fake raw heap word back to its stable fake handle. - fn raw_heap_word_value(&mut self, word: u64) -> Result { - Ok(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) - } - /// Records release of a fake raw heap word owned by a staged slot. - fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus> { - self.runtime_release(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) - } - /// Returns the fake object handle as a stable object identity. - fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { - self.runtime_object_identity(object) - } - /// Returns fake object identity for releases that target object cells. - fn final_object_identity_for_release( - &mut self, - value: RuntimeCellHandle, - ) -> Result, EvalStatus> { - if self.runtime_type_tag(value)? == EVAL_TAG_OBJECT { - self.runtime_object_identity(value).map(Some) - } else { - Ok(None) - } - } - /// Records fake releases without freeing handles needed for assertions. - fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - self.runtime_release(value) - } - /// Returns the same fake handle because fake cells do not refcount. - fn retain(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_retain(value) - } - /// Records fake PHP warnings without writing to stderr. - fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { - self.runtime_warning(message) - } - /// Creates a fake null cell. - fn null(&mut self) -> Result { - self.runtime_null() - } - /// Creates a fake bool cell. - fn bool_value(&mut self, value: bool) -> Result { - self.runtime_bool_value(value) - } - /// Creates a fake int cell. - fn int(&mut self, value: i64) -> Result { - self.runtime_int(value) - } - /// Creates a fake resource cell. - fn resource(&mut self, value: i64) -> Result { - self.runtime_resource(value) - } - /// Creates a fake float cell. - fn float(&mut self, value: f64) -> Result { - self.runtime_float(value) - } - /// Creates a fake string cell. - fn string(&mut self, value: &str) -> Result { - self.runtime_string(value) - } - /// Creates a fake string cell from raw PHP bytes. - fn string_bytes_value(&mut self, value: &[u8]) -> Result { - self.runtime_string_bytes_value(value) - } - /// Casts a fake runtime cell to a fake integer cell. - fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_cast_int(value) - } - /// Casts a fake runtime cell to a fake float cell. - fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_cast_float(value) - } - /// Casts a fake runtime cell to a fake string cell. - fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_cast_string(value) - } - /// Casts a fake runtime cell to a fake boolean cell. - fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_cast_bool(value) - } - /// Computes fake PHP absolute value while preserving float payloads. - fn abs(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_abs(value) - } - /// Computes fake PHP ceiling through numeric conversion as a float result. - fn ceil(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_ceil(value) - } - /// Computes fake PHP floor through numeric conversion as a float result. - fn floor(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_floor(value) - } - /// Computes fake PHP square root through numeric conversion as a float result. - fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_sqrt(value) - } - /// Reverses a fake string byte-wise for interpreter tests. - fn strrev(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_strrev(value) - } - /// Divides fake numeric cells with PHP `fdiv()` zero handling. - fn fdiv( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_fdiv(left, right) - } - /// Computes fake floating-point modulo for interpreter tests. - fn fmod( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_fmod(left, right) - } - /// Adds fake numeric cells for interpreter tests. - fn add( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_add(left, right) - } - /// Subtracts fake numeric cells for interpreter tests. - fn sub( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_sub(left, right) - } - /// Multiplies fake numeric cells for interpreter tests. - fn mul( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_mul(left, right) - } - /// Divides fake numeric cells for interpreter tests. - fn div( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_div(left, right) - } - /// Computes fake integer modulo for interpreter tests. - fn modulo( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_modulo(left, right) - } - /// Raises fake numeric cells for interpreter tests. - fn pow( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_pow(left, right) - } - /// Rounds fake numeric cells with PHP's optional decimal precision. - fn round( - &mut self, - value: RuntimeCellHandle, - precision: Option, - ) -> Result { - self.runtime_round(value, precision) - } - /// Applies fake integer bitwise and shift operations for interpreter tests. - fn bitwise( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_bitwise(op, left, right) - } - /// Applies fake integer bitwise NOT for interpreter tests. - fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_bit_not(value) - } - /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. - fn concat( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_concat(left, right) - } - /// Compares fake scalar cells and returns a fake PHP boolean. - fn compare( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_compare(op, left, right) - } - /// Compares fake numeric cells and returns a PHP spaceship integer. - fn spaceship( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - self.runtime_spaceship(left, right) - } - /// Appends fake echo output for interpreter tests. - fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - self.runtime_echo(value) - } - /// Casts one fake runtime cell to bytes for nested eval parsing. - fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { - self.runtime_string_bytes(value) - } - /// Returns PHP-like truthiness for fake runtime cells. - fn truthy(&mut self, value: RuntimeCellHandle) -> Result { - self.runtime_truthy(value) - } + impl_fake_collection_call_ops!(); + impl_fake_reflection_ops!(); + impl_fake_construction_raw_ops!(); + impl_fake_lifecycle_scalar_ops!(); + impl_fake_numeric_string_ops!(); } diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/collection_calls.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/collection_calls.rs new file mode 100644 index 0000000000..c77d751a0c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/collection_calls.rs @@ -0,0 +1,165 @@ +//! Purpose: +//! Defines fake array, property, object-call, and static-call trait methods for +//! interpreter tests. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - Methods delegate to focused fake runtime helpers without altering handles. + +macro_rules! impl_fake_collection_call_ops { + () => { + /// Creates a fake indexed array cell. + fn array_new(&mut self, capacity: usize) -> Result { + self.runtime_array_new(capacity) + } + /// Creates a fake direct-string indexed array cell. + fn string_array_new(&mut self, capacity: usize) -> Result { + self.runtime_string_array_new(capacity) + } + /// Appends one string to a fake direct-string indexed array cell. + fn string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result { + self.runtime_string_array_push(array, value) + } + /// Creates a fake associative array cell. + fn assoc_new(&mut self, _capacity: usize) -> Result { + self.runtime_assoc_new(_capacity) + } + /// Reads one fake indexed array element. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + self.runtime_array_get(array, index) + } + /// Checks whether a fake array has the requested key without reading its value. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + self.runtime_array_key_exists(key, array) + } + /// Returns one fake foreach key by insertion-order position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + self.runtime_array_iter_key(array, position) + } + /// Writes one fake indexed or associative array element. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + self.runtime_array_set(array, index, value) + } + /// Reads one fake object property by name. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + self.runtime_property_get(object, property) + } + /// Checks whether one fake object property exists by name. + fn property_is_initialized( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + self.runtime_property_is_initialized(object, property) + } + /// Writes one fake object property by name. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + self.runtime_property_set(object, property, value) + } + /// Reports no fake AOT static property match. + fn static_property_get( + &mut self, + _class_name: &str, + _property: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports no fake AOT static property initialization match. + fn static_property_is_initialized( + &mut self, + _class_name: &str, + _property: &str, + ) -> Result { + Ok(false) + } + /// Reports a failed fake AOT static property write. + fn static_property_set( + &mut self, + _class_name: &str, + _property: &str, + _value: RuntimeCellHandle, + ) -> Result { + Ok(false) + } + /// Reports no fake AOT class constant match. + fn class_constant_get( + &mut self, + _class_name: &str, + _constant: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Creates one shallow fake object clone. + fn object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + self.runtime_object_clone_shallow(object) + } + /// Returns the number of fake object properties in insertion order. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { + self.runtime_object_property_len(object) + } + /// Returns one fake object property key by insertion-order position. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + self.runtime_object_property_iter_key(object, position) + } + /// Calls one fake object method by name. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + self.runtime_method_call(object, method, args) + } + /// Calls one fake static runtime method by class and method name. + fn static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + self.runtime_static_method_call(class_name, method, args) + } + + }; +} + +pub(super) use impl_fake_collection_call_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/construction_raw.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/construction_raw.rs new file mode 100644 index 0000000000..c0ed994d6f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/construction_raw.rs @@ -0,0 +1,165 @@ +//! Purpose: +//! Defines fake construction, class queries, raw words, and raw string/heap +//! ownership trait methods. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - Opaque fake handles preserve the staging and release behavior under test. + +macro_rules! impl_fake_construction_raw_ops { + () => { + + /// Creates one fake object for eval `new` unit tests. + fn new_object(&mut self, _class_name: &str) -> Result { + self.runtime_new_object(_class_name) + } + /// Applies fake constructor side effects for eval `new` unit tests. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + self.runtime_construct_object(object, args) + } + /// Reports one fake AOT class for eval `class_exists` unit tests. + fn class_exists(&mut self, name: &str) -> Result { + self.runtime_class_exists(name) + } + /// Reports one fake AOT interface for eval `interface_exists` unit tests. + fn interface_exists(&mut self, name: &str) -> Result { + self.runtime_interface_exists(name) + } + /// Reports one fake AOT trait for eval `trait_exists` unit tests. + fn trait_exists(&mut self, name: &str) -> Result { + self.runtime_trait_exists(name) + } + /// Reports one fake AOT enum for eval `enum_exists` unit tests. + fn enum_exists(&mut self, name: &str) -> Result { + self.runtime_enum_exists(name) + } + /// Reports fake class relations for eval `is_a` and `is_subclass_of` unit tests. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + self.runtime_object_is_a(object_or_class, target_class, exclude_self) + } + /// Returns a fake PHP class name for object-tagged test values. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + self.runtime_object_class_name(object) + } + /// Returns fake parent-class names for eval introspection unit tests. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + self.runtime_parent_class_name(object_or_class) + } + /// Returns the visible element count for fake array values. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + self.runtime_array_len(array) + } + /// Returns whether a fake runtime cell is an indexed or associative array. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_is_array_like(value) + } + /// Returns whether a fake runtime cell is null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_is_null(value) + } + /// Returns the fake runtime tag corresponding to a test value. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_type_tag(value) + } + /// Creates a fake invoker-only by-reference marker. + fn invoker_ref_cell( + &mut self, + slot: *mut RuntimeCellHandle, + ) -> Result { + Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) + } + /// Creates a fake invoker-only raw by-reference marker. + fn invoker_raw_ref_cell( + &mut self, + slot: *mut std::ffi::c_void, + _source_tag: u64, + ) -> Result { + Ok(self.alloc(FakeValue::InvokerRefCell(slot as usize))) + } + /// Extracts one fake low payload word for raw by-reference staging. + fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::Bool(value) => u64::from(value), + FakeValue::Float(value) => value.to_bits(), + FakeValue::Int(value) => value as u64, + FakeValue::String(_) | FakeValue::Bytes(_) => value.as_ptr() as u64, + FakeValue::Array(_) + | FakeValue::Assoc(_) + | FakeValue::Object(_) + | FakeValue::Iterator { .. } => value.as_ptr() as u64, + _ => 0, + }) + } + /// Extracts one fake high payload word for raw by-reference staging. + fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(match self.get(value) { + FakeValue::String(value) => value.len() as u64, + FakeValue::Bytes(value) => value.len() as u64, + _ => 0, + }) + } + /// Retains a fake raw string payload for native by-reference staging. + fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus> { + self.runtime_retain(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell))?; + Ok((ptr, len)) + } + /// Converts a fake raw string payload back to its stable fake handle. + fn raw_string_value(&mut self, ptr: u64, _len: u64) -> Result { + Ok(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell)) + } + /// Records release of a fake raw string payload owned by a staged slot. + fn release_raw_string_words(&mut self, ptr: u64, _len: u64) -> Result<(), EvalStatus> { + self.runtime_release(RuntimeCellHandle::from_raw(ptr as *mut RuntimeCell)) + } + /// Retains a fake raw heap word for native by-reference staging. + fn retain_raw_heap_word(&mut self, word: u64) -> Result { + self.runtime_retain(RuntimeCellHandle::from_raw(word as *mut RuntimeCell))?; + Ok(word) + } + /// Boxes one fake one-word raw payload with the provided runtime tag. + fn raw_word_value( + &mut self, + source_tag: u64, + word: u64, + ) -> Result { + match source_tag { + EVAL_TAG_INT => self.runtime_int(word as i64), + EVAL_TAG_FLOAT => self.runtime_float(f64::from_bits(word)), + EVAL_TAG_BOOL => self.runtime_bool_value(word != 0), + EVAL_TAG_RESOURCE => self.runtime_resource(word as i64), + EVAL_TAG_ARRAY | EVAL_TAG_ASSOC | EVAL_TAG_OBJECT | EVAL_TAG_CALLABLE => { + Ok(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } + _ => Err(EvalStatus::RuntimeFatal), + } + } + /// Converts a fake raw heap word back to its stable fake handle. + fn raw_heap_word_value(&mut self, word: u64) -> Result { + Ok(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } + /// Records release of a fake raw heap word owned by a staged slot. + fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus> { + self.runtime_release(RuntimeCellHandle::from_raw(word as *mut RuntimeCell)) + } + + }; +} + +pub(super) use impl_fake_construction_raw_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/lifecycle_scalars.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/lifecycle_scalars.rs new file mode 100644 index 0000000000..b1068336fc --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/lifecycle_scalars.rs @@ -0,0 +1,89 @@ +//! Purpose: +//! Defines fake identity, retain/release, warning, scalar construction, and cast +//! trait methods. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - Operations retain the fake runtime's stable-cell ownership model. + +macro_rules! impl_fake_lifecycle_scalar_ops { + () => { + + /// Returns the fake object handle as a stable object identity. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + self.runtime_object_identity(object) + } + /// Returns fake object identity for releases that target object cells. + fn final_object_identity_for_release( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus> { + if self.runtime_type_tag(value)? == EVAL_TAG_OBJECT { + self.runtime_object_identity(value).map(Some) + } else { + Ok(None) + } + } + /// Records fake releases without freeing handles needed for assertions. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.runtime_release(value) + } + /// Returns the same fake handle because fake cells do not refcount. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_retain(value) + } + /// Records fake PHP warnings without writing to stderr. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + self.runtime_warning(message) + } + /// Creates a fake null cell. + fn null(&mut self) -> Result { + self.runtime_null() + } + /// Creates a fake bool cell. + fn bool_value(&mut self, value: bool) -> Result { + self.runtime_bool_value(value) + } + /// Creates a fake int cell. + fn int(&mut self, value: i64) -> Result { + self.runtime_int(value) + } + /// Creates a fake resource cell. + fn resource(&mut self, value: i64) -> Result { + self.runtime_resource(value) + } + /// Creates a fake float cell. + fn float(&mut self, value: f64) -> Result { + self.runtime_float(value) + } + /// Creates a fake string cell. + fn string(&mut self, value: &str) -> Result { + self.runtime_string(value) + } + /// Creates a fake string cell from raw PHP bytes. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + self.runtime_string_bytes_value(value) + } + /// Casts a fake runtime cell to a fake integer cell. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_int(value) + } + /// Casts a fake runtime cell to a fake float cell. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_float(value) + } + /// Casts a fake runtime cell to a fake string cell. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_string(value) + } + /// Casts a fake runtime cell to a fake boolean cell. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_cast_bool(value) + } + + }; +} + +pub(super) use impl_fake_lifecycle_scalar_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/numeric_string.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/numeric_string.rs new file mode 100644 index 0000000000..8e3f80382c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/numeric_string.rs @@ -0,0 +1,160 @@ +//! Purpose: +//! Defines fake numeric, bitwise, comparison, string, echo, byte, and truthiness +//! trait methods. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - PHP-like fake behavior remains in the existing `runtime_*` helpers. + +macro_rules! impl_fake_numeric_string_ops { + () => { + + /// Computes fake PHP absolute value while preserving float payloads. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_abs(value) + } + /// Computes fake PHP ceiling through numeric conversion as a float result. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_ceil(value) + } + /// Computes fake PHP floor through numeric conversion as a float result. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_floor(value) + } + /// Computes fake PHP square root through numeric conversion as a float result. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_sqrt(value) + } + /// Reverses a fake string byte-wise for interpreter tests. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_strrev(value) + } + /// Divides fake numeric cells with PHP `fdiv()` zero handling. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_fdiv(left, right) + } + /// Computes fake floating-point modulo for interpreter tests. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_fmod(left, right) + } + /// Adds fake numeric cells for interpreter tests. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_add(left, right) + } + /// Subtracts fake numeric cells for interpreter tests. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_sub(left, right) + } + /// Multiplies fake numeric cells for interpreter tests. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_mul(left, right) + } + /// Divides fake numeric cells for interpreter tests. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_div(left, right) + } + /// Computes fake integer modulo for interpreter tests. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_modulo(left, right) + } + /// Raises fake numeric cells for interpreter tests. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_pow(left, right) + } + /// Rounds fake numeric cells with PHP's optional decimal precision. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + self.runtime_round(value, precision) + } + /// Applies fake integer bitwise and shift operations for interpreter tests. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_bitwise(op, left, right) + } + /// Applies fake integer bitwise NOT for interpreter tests. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_bit_not(value) + } + /// Concatenates fake cells with byte-preserving string conversion for interpreter tests. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_concat(left, right) + } + /// Compares fake scalar cells and returns a fake PHP boolean. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_compare(op, left, right) + } + /// Compares fake numeric cells and returns a PHP spaceship integer. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + self.runtime_spaceship(left, right) + } + /// Appends fake echo output for interpreter tests. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + self.runtime_echo(value) + } + /// Casts one fake runtime cell to bytes for nested eval parsing. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + self.runtime_string_bytes(value) + } + /// Returns PHP-like truthiness for fake runtime cells. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + self.runtime_truthy(value) + } + + }; +} + +pub(super) use impl_fake_numeric_string_ops; diff --git a/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/reflection.rs b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/reflection.rs new file mode 100644 index 0000000000..a1fb93b55c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/support/runtime_ops/reflection.rs @@ -0,0 +1,176 @@ +//! Purpose: +//! Defines fake Reflection object and metadata-query trait methods for +//! interpreter tests. +//! +//! Called from: +//! - The single `RuntimeValueOps for FakeOps` implementation in `super`. +//! +//! Key details: +//! - All calls delegate to the fake reflection object model. + +macro_rules! impl_fake_reflection_ops { + () => { + + /// Materializes one fake `ReflectionAttribute` object for eval metadata tests. + fn reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + target: u64, + repeated: bool, + ) -> Result { + self.runtime_reflection_attribute_new(name, args, target, repeated) + } + /// Materializes one fake Reflection owner object for eval metadata tests. + fn reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, + ) -> Result { + self.runtime_reflection_owner_new( + owner_kind, + reflected_name, + attrs, + interface_names, + trait_names, + method_names, + property_names, + method_objects, + property_objects, + parent_class, + flags, + modifiers, + method_modifiers, + constant_value, + backing_value, + constructor, + ) + } + /// Reports fake generated AOT ReflectionMethod flags for metadata bridge tests. + fn reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_method_flags(class_name, method_name) + } + /// Reports fake generated AOT ReflectionMethod declaring classes for metadata bridge tests. + fn reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_method_declaring_class(class_name, method_name) + } + /// Reports fake generated AOT ReflectionMethod names for metadata bridge tests. + fn reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_method_names(class_name) + } + /// Reports fake generated AOT ReflectionClass flags for metadata bridge tests. + fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { + self.runtime_reflection_class_flags(class_name) + } + /// Reports fake generated AOT ReflectionProperty flags for metadata bridge tests. + fn reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_property_flags(class_name, property_name) + } + /// Reports fake generated AOT ReflectionProperty declaring classes for metadata bridge tests. + fn reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + self.runtime_reflection_property_declaring_class(class_name, property_name) + } + /// Reports fake generated AOT ReflectionProperty names for metadata bridge tests. + fn reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_property_names(class_name) + } + /// Reports no fake generated AOT ReflectionClassConstant value. + fn reflection_constant_value( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports no fake generated AOT ReflectionClassConstant flags. + fn reflection_constant_flags( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports no fake generated AOT ReflectionClassConstant declaring class. + fn reflection_constant_declaring_class( + &mut self, + _class_name: &str, + _constant_name: &str, + ) -> Result, EvalStatus> { + Ok(None) + } + /// Reports an empty fake generated AOT ReflectionClassConstant name list. + fn reflection_constant_names( + &mut self, + _class_name: &str, + ) -> Result { + self.runtime_string_array_new(0) + } + /// Reports fake generated/AOT ReflectionClass interface names for metadata bridge tests. + fn reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_interface_names(class_name) + } + /// Reports fake generated/AOT ReflectionClass trait names for metadata bridge tests. + fn reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_names(class_name) + } + /// Reports fake generated/AOT ReflectionClass trait alias names for metadata bridge tests. + fn reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_alias_names(class_name) + } + /// Reports fake generated/AOT ReflectionClass trait alias sources for metadata bridge tests. + fn reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + self.runtime_reflection_class_trait_alias_sources(class_name) + } + + }; +} + +pub(super) use impl_fake_reflection_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops.rs b/crates/elephc-magician/src/runtime_hooks/ops.rs index 1c98d07c2d..76d0b678ab 100644 --- a/crates/elephc-magician/src/runtime_hooks/ops.rs +++ b/crates/elephc-magician/src/runtime_hooks/ops.rs @@ -17,1146 +17,24 @@ use crate::eval_ir::EvalBinOp; use crate::interpreter::RuntimeValueOps; use crate::value::{RuntimeCell, RuntimeCellHandle}; -#[cfg(not(test))] -impl RuntimeValueOps for ElephcRuntimeOps { - /// Creates a boxed Mixed indexed array through the generated runtime wrapper. - fn array_new(&mut self, capacity: usize) -> Result { - Self::handle(unsafe { __elephc_eval_value_array_new(capacity as u64) }) - } - - /// Creates a boxed Mixed indexed array whose payload uses direct string slots. - fn string_array_new(&mut self, capacity: usize) -> Result { - Self::handle(unsafe { __elephc_eval_value_string_array_new(capacity as u64) }) - } - - /// Appends one string to a boxed direct-string indexed array. - fn string_array_push( - &mut self, - array: RuntimeCellHandle, - value: &str, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_value_string_array_push( - array.as_ptr(), - value.as_ptr(), - value.len() as u64, - ) - }) - } - - /// Creates a boxed Mixed associative array through the generated runtime wrapper. - fn assoc_new(&mut self, capacity: usize) -> Result { - Self::handle(unsafe { __elephc_eval_value_assoc_new(capacity as u64) }) - } - - /// Reads one element from a boxed Mixed array through the generated runtime wrapper. - fn array_get( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_array_get(array.as_ptr(), index.as_ptr()) }) - } - - /// Checks whether a boxed Mixed array contains a normalized PHP key. - fn array_key_exists( - &mut self, - key: RuntimeCellHandle, - array: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_array_key_exists(key.as_ptr(), array.as_ptr()) }) - } - - /// Returns one foreach-visible key from a boxed Mixed array by iteration position. - fn array_iter_key( - &mut self, - array: RuntimeCellHandle, - position: usize, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_array_iter_key(array.as_ptr(), position as u64) }) - } - - /// Writes one element to a boxed Mixed array through the generated runtime wrapper. - fn array_set( - &mut self, - array: RuntimeCellHandle, - index: RuntimeCellHandle, - value: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_value_array_set(array.as_ptr(), index.as_ptr(), value.as_ptr()) - }) - } - - /// Reads a boxed Mixed object property through the generated user helper. - fn property_get( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - Self::handle(unsafe { - __elephc_eval_value_property_get( - object.as_ptr(), - property.as_ptr(), - property.len() as u64, - scope_ptr, - scope_len, - ) - }) - } - - /// Checks an AOT instance property initialization marker through the generated helper. - fn property_is_initialized( - &mut self, - object: RuntimeCellHandle, - property: &str, - ) -> Result { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let initialized = unsafe { - __elephc_eval_value_property_is_initialized( - object.as_ptr(), - property.as_ptr(), - property.len() as u64, - scope_ptr, - scope_len, - ) - }; - Ok(initialized != 0) - } - - /// Writes a boxed Mixed object property through the generated user helper. - fn property_set( - &mut self, - object: RuntimeCellHandle, - property: &str, - value: RuntimeCellHandle, - ) -> Result<(), EvalStatus> { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let ok = unsafe { - __elephc_eval_value_property_set( - object.as_ptr(), - property.as_ptr(), - property.len() as u64, - value.as_ptr(), - scope_ptr, - scope_len, - ) - }; - if ok == 0 { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(()) - } - } - - /// Reads an AOT static property through the generated user helper. - fn static_property_get( - &mut self, - class_name: &str, - property: &str, - ) -> Result, EvalStatus> { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let ptr = unsafe { - __elephc_eval_value_static_property_get( - class_name.as_ptr(), - class_name.len() as u64, - property.as_ptr(), - property.len() as u64, - scope_ptr, - scope_len, - ) - }; - if ptr.is_null() { - Ok(None) - } else { - Ok(Some(RuntimeCellHandle::from_raw(ptr))) - } - } - - /// Checks an AOT static property initialization marker through the generated helper. - fn static_property_is_initialized( - &mut self, - class_name: &str, - property: &str, - ) -> Result { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let initialized = unsafe { - __elephc_eval_value_static_property_is_initialized( - class_name.as_ptr(), - class_name.len() as u64, - property.as_ptr(), - property.len() as u64, - scope_ptr, - scope_len, - ) - }; - Ok(initialized != 0) - } - - /// Writes an AOT static property through the generated user helper. - fn static_property_set( - &mut self, - class_name: &str, - property: &str, - value: RuntimeCellHandle, - ) -> Result { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let ok = unsafe { - __elephc_eval_value_static_property_set( - class_name.as_ptr(), - class_name.len() as u64, - property.as_ptr(), - property.len() as u64, - value.as_ptr(), - scope_ptr, - scope_len, - ) - }; - Ok(ok != 0) - } - - /// Reads an AOT class-like constant through the generated user helper. - fn class_constant_get( - &mut self, - class_name: &str, - constant: &str, - ) -> Result, EvalStatus> { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let ptr = unsafe { - __elephc_eval_value_class_constant_get( - class_name.as_ptr(), - class_name.len() as u64, - constant.as_ptr(), - constant.len() as u64, - scope_ptr, - scope_len, - ) - }; - if ptr.is_null() { - Ok(None) - } else { - Ok(Some(RuntimeCellHandle::from_raw(ptr))) - } - } - - /// Creates a shallow clone of a boxed Mixed stdClass/eval object through the generated wrapper. - fn object_clone_shallow( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_object_clone_shallow(object.as_ptr()) }) - } - - /// Returns the JSON-visible public property count for a boxed Mixed object. - fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { - let len = unsafe { __elephc_eval_value_object_property_len(object.as_ptr()) }; - usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) - } - - /// Returns one JSON-visible public property key for a boxed Mixed object. - fn object_property_iter_key( - &mut self, - object: RuntimeCellHandle, - position: usize, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_value_object_property_iter_key(object.as_ptr(), position as u64) - }) - } - - /// Calls a boxed Mixed object method through the generated user helper. - fn method_call( - &mut self, - object: RuntimeCellHandle, - method: &str, - args: Vec, - ) -> Result { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let arg_array = Self::arg_array(args)?; - let result = unsafe { - __elephc_eval_value_method_call( - object.as_ptr(), - method.as_ptr(), - method.len() as u64, - arg_array.as_ptr(), - scope_ptr, - scope_len, - self.context.cast(), - ) - }; - unsafe { - __elephc_eval_value_release(arg_array.as_ptr()); - } - self.handle_native_call_result(result) - } - - /// Calls an AOT static method through the generated user helper. - fn static_method_call( - &mut self, - class_name: &str, - method: &str, - args: Vec, - ) -> Result { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let arg_array = Self::arg_array(args)?; - let result = unsafe { - __elephc_eval_value_static_method_call( - class_name.as_ptr(), - class_name.len() as u64, - method.as_ptr(), - method.len() as u64, - arg_array.as_ptr(), - scope_ptr, - scope_len, - self.context.cast(), - ) - }; - unsafe { - __elephc_eval_value_release(arg_array.as_ptr()); - } - self.handle_native_call_result(result) - } - - /// Converts a native free-function result into eval status, preserving pending throwables. - fn native_call_result( - &mut self, - result: RuntimeCellHandle, - ) -> Result { - self.handle_native_call_result(result.as_ptr()) - } - - /// Materializes a populated synthetic `ReflectionAttribute` object for eval metadata. - fn reflection_attribute_new( - &mut self, - name: &str, - args: RuntimeCellHandle, - target: u64, - repeated: bool, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_attribute_new( - name.as_ptr(), - name.len() as u64, - args.as_ptr(), - target, - if repeated { 1 } else { 0 }, - ) - }) - } - - /// Materializes a populated synthetic Reflection owner object for eval metadata. - fn reflection_owner_new( - &mut self, - owner_kind: u64, - reflected_name: &str, - attrs: RuntimeCellHandle, - interface_names: RuntimeCellHandle, - trait_names: RuntimeCellHandle, - method_names: RuntimeCellHandle, - property_names: RuntimeCellHandle, - method_objects: RuntimeCellHandle, - property_objects: RuntimeCellHandle, - parent_class: RuntimeCellHandle, - flags: u64, - modifiers: u64, - method_modifiers: u64, - constant_value: RuntimeCellHandle, - backing_value: RuntimeCellHandle, - constructor: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_owner_new( - owner_kind, - reflected_name.as_ptr(), - reflected_name.len() as u64, - attrs.as_ptr(), - interface_names.as_ptr(), - trait_names.as_ptr(), - method_names.as_ptr(), - property_names.as_ptr(), - method_objects.as_ptr(), - property_objects.as_ptr(), - parent_class.as_ptr(), - flags, - modifiers, - method_modifiers, - constant_value.as_ptr(), - backing_value.as_ptr(), - constructor.as_ptr(), - ) - }) - } - - /// Returns generated AOT ReflectionMethod flags, or `None` when no row matches. - fn reflection_method_flags( - &mut self, - class_name: &str, - method_name: &str, - ) -> Result, EvalStatus> { - let flags = unsafe { - __elephc_eval_reflection_method_flags( - class_name.as_ptr(), - class_name.len() as u64, - method_name.as_ptr(), - method_name.len() as u64, - ) - }; - Ok((flags != 0).then_some(flags)) - } - - /// Returns generated AOT ReflectionMethod declaring class metadata. - fn reflection_method_declaring_class( - &mut self, - class_name: &str, - method_name: &str, - ) -> Result, EvalStatus> { - let ptr = unsafe { - __elephc_eval_reflection_method_declaring_class( - class_name.as_ptr(), - class_name.len() as u64, - method_name.as_ptr(), - method_name.len() as u64, - ) - }; - if ptr.is_null() { - return Ok(None); - } - let handle = RuntimeCellHandle::from_raw(ptr); - let bytes = self.string_bytes(handle)?; - self.release(handle)?; - String::from_utf8(bytes) - .map(Some) - .map_err(|_| EvalStatus::RuntimeFatal) - } - - /// Returns generated AOT ReflectionMethod names visible for one class. - fn reflection_method_names( - &mut self, - class_name: &str, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_method_names(class_name.as_ptr(), class_name.len() as u64) - }) - } - - /// Returns generated AOT source-file metadata for reflection source-location calls. - fn reflection_source_file(&mut self) -> Result, EvalStatus> { - let ptr = unsafe { __elephc_eval_reflection_source_file() }; - if ptr.is_null() { - return Ok(None); - } - let handle = RuntimeCellHandle::from_raw(ptr); - let bytes = self.string_bytes(handle)?; - self.release(handle)?; - String::from_utf8(bytes) - .map(Some) - .map_err(|_| EvalStatus::RuntimeFatal) - } - - /// Returns generated AOT ReflectionClass modifier flags, or `None` when no row matches. - fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { - let flags = unsafe { - __elephc_eval_reflection_class_flags(class_name.as_ptr(), class_name.len() as u64) - }; - Ok((flags != 0).then_some(flags)) - } - - /// Returns generated AOT ReflectionProperty flags, or `None` when no row matches. - fn reflection_property_flags( - &mut self, - class_name: &str, - property_name: &str, - ) -> Result, EvalStatus> { - let flags = unsafe { - __elephc_eval_reflection_property_flags( - class_name.as_ptr(), - class_name.len() as u64, - property_name.as_ptr(), - property_name.len() as u64, - ) - }; - Ok((flags != 0).then_some(flags)) - } - - /// Returns generated AOT ReflectionProperty declaring class metadata. - fn reflection_property_declaring_class( - &mut self, - class_name: &str, - property_name: &str, - ) -> Result, EvalStatus> { - let ptr = unsafe { - __elephc_eval_reflection_property_declaring_class( - class_name.as_ptr(), - class_name.len() as u64, - property_name.as_ptr(), - property_name.len() as u64, - ) - }; - if ptr.is_null() { - return Ok(None); - } - let handle = RuntimeCellHandle::from_raw(ptr); - let bytes = self.string_bytes(handle)?; - self.release(handle)?; - String::from_utf8(bytes) - .map(Some) - .map_err(|_| EvalStatus::RuntimeFatal) - } - - /// Returns generated AOT ReflectionProperty names visible for one class. - fn reflection_property_names( - &mut self, - class_name: &str, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_property_names(class_name.as_ptr(), class_name.len() as u64) - }) - } - - /// Returns generated AOT ReflectionClassConstant values without visibility checks. - fn reflection_constant_value( - &mut self, - class_name: &str, - constant_name: &str, - ) -> Result, EvalStatus> { - let ptr = unsafe { - __elephc_eval_reflection_constant_value( - class_name.as_ptr(), - class_name.len() as u64, - constant_name.as_ptr(), - constant_name.len() as u64, - ) - }; - if ptr.is_null() { - Ok(None) - } else { - Ok(Some(RuntimeCellHandle::from_raw(ptr))) - } - } - - /// Returns generated AOT ReflectionClassConstant flags for one constant. - fn reflection_constant_flags( - &mut self, - class_name: &str, - constant_name: &str, - ) -> Result, EvalStatus> { - let flags = unsafe { - __elephc_eval_reflection_constant_flags( - class_name.as_ptr(), - class_name.len() as u64, - constant_name.as_ptr(), - constant_name.len() as u64, - ) - }; - Ok((flags != 0).then_some(flags)) - } - - /// Returns generated AOT ReflectionClassConstant declaring class metadata. - fn reflection_constant_declaring_class( - &mut self, - class_name: &str, - constant_name: &str, - ) -> Result, EvalStatus> { - let ptr = unsafe { - __elephc_eval_reflection_constant_declaring_class( - class_name.as_ptr(), - class_name.len() as u64, - constant_name.as_ptr(), - constant_name.len() as u64, - ) - }; - if ptr.is_null() { - return Ok(None); - } - let handle = RuntimeCellHandle::from_raw(ptr); - let bytes = self.string_bytes(handle)?; - self.release(handle)?; - String::from_utf8(bytes) - .map(Some) - .map_err(|_| EvalStatus::RuntimeFatal) - } - - /// Returns generated AOT ReflectionClassConstant names visible for one class. - fn reflection_constant_names( - &mut self, - class_name: &str, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_constant_names(class_name.as_ptr(), class_name.len() as u64) - }) - } - - /// Returns generated AOT interface names visible for one reflected class-like symbol. - fn reflection_class_interface_names( - &mut self, - class_name: &str, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_class_interface_names( - class_name.as_ptr(), - class_name.len() as u64, - ) - }) - } - - /// Returns generated AOT trait names visible for one reflected class-like symbol. - fn reflection_class_trait_names( - &mut self, - class_name: &str, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_class_trait_names( - class_name.as_ptr(), - class_name.len() as u64, - ) - }) - } - - /// Returns generated AOT trait alias names visible for one reflected class-like symbol. - fn reflection_class_trait_alias_names( - &mut self, - class_name: &str, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_class_trait_alias_names( - class_name.as_ptr(), - class_name.len() as u64, - ) - }) - } - - /// Returns generated AOT trait alias sources visible for one reflected class-like symbol. - fn reflection_class_trait_alias_sources( - &mut self, - class_name: &str, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_reflection_class_trait_alias_sources( - class_name.as_ptr(), - class_name.len() as u64, - ) - }) - } - - /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. - fn new_object(&mut self, class_name: &str) -> Result { - let object = Self::handle(unsafe { - __elephc_eval_value_new_object(class_name.as_ptr(), class_name.len() as u64) - })?; - match self.is_null(object) { - Ok(false) => Ok(object), - Ok(true) => { - self.release(object)?; - Err(EvalStatus::RuntimeFatal) - } - Err(err) => { - let _ = self.release(object); - Err(err) - } - } - } - - /// Calls an AOT constructor through the generated user bridge when one exists. - fn construct_object( - &mut self, - object: RuntimeCellHandle, - args: Vec, - ) -> Result<(), EvalStatus> { - let (scope_ptr, scope_len) = self.current_class_scope_abi(); - let arg_array = Self::arg_array(args)?; - let ok = unsafe { - __elephc_eval_value_construct_object( - object.as_ptr(), - arg_array.as_ptr(), - scope_ptr, - scope_len, - self.context.cast(), - ) - }; - unsafe { - __elephc_eval_value_release(arg_array.as_ptr()); - } - if ok == 0 { - self.take_pending_native_throwable() - .map_or(Err(EvalStatus::RuntimeFatal), |thrown| { - self.schedule_pending_throw(thrown)?; - Err(EvalStatus::UncaughtThrowable) - }) - } else { - Ok(()) - } - } - - /// Returns whether the generated AOT class-name table contains the requested class. - fn class_exists(&mut self, name: &str) -> Result { - Ok(unsafe { __elephc_eval_class_exists(name.as_ptr(), name.len() as u64) != 0 }) - } - - /// Returns whether the generated AOT interface-name table contains the requested interface. - fn interface_exists(&mut self, name: &str) -> Result { - Ok(unsafe { __elephc_eval_interface_exists(name.as_ptr(), name.len() as u64) != 0 }) - } - - /// Returns whether the generated AOT trait-name table contains the requested trait. - fn trait_exists(&mut self, name: &str) -> Result { - Ok(unsafe { __elephc_eval_trait_exists(name.as_ptr(), name.len() as u64) != 0 }) - } - - /// Returns whether the generated AOT enum-name table contains the requested enum. - fn enum_exists(&mut self, name: &str) -> Result { - Ok(unsafe { __elephc_eval_enum_exists(name.as_ptr(), name.len() as u64) != 0 }) - } - - /// Tests a boxed Mixed object against generated class/interface metadata. - fn object_is_a( - &mut self, - object_or_class: RuntimeCellHandle, - target_class: &str, - exclude_self: bool, - ) -> Result { - Ok(unsafe { - __elephc_eval_value_is_a( - object_or_class.as_ptr(), - target_class.as_ptr(), - target_class.len() as u64, - u64::from(exclude_self), - ) != 0 - }) - } - - /// Returns a boxed Mixed string naming a boxed Mixed object's runtime class. - fn object_class_name( - &mut self, - object: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_object_class_name(object.as_ptr()) }) - } - - /// Returns a boxed Mixed string naming a boxed Mixed object's or class string's parent class. - fn parent_class_name( - &mut self, - object_or_class: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_parent_class_name(object_or_class.as_ptr()) }) - } - - /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. - fn array_len(&mut self, array: RuntimeCellHandle) -> Result { - let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; - usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) - } - - /// Returns whether a boxed Mixed cell has an array-like runtime tag. - fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { - Ok(unsafe { __elephc_eval_value_is_array_like(value.as_ptr()) != 0 }) - } - - /// Returns whether a boxed Mixed cell unwraps to PHP null. - fn is_null(&mut self, value: RuntimeCellHandle) -> Result { - Ok(unsafe { __elephc_eval_value_is_null(value.as_ptr()) != 0 }) - } - - /// Returns the unboxed Mixed runtime tag for PHP type-predicate builtins. - fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { - Ok(unsafe { __elephc_eval_value_type_tag(value.as_ptr()) }) - } - - /// Creates an invoker-only by-reference marker for a staged Mixed slot. - fn invoker_ref_cell( - &mut self, - slot: *mut RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_invoker_ref_cell(slot) }) - } - - /// Creates an invoker-only by-reference marker for a staged raw one-word slot. - fn invoker_raw_ref_cell( - &mut self, - slot: *mut std::ffi::c_void, - source_tag: u64, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_invoker_raw_ref_cell(slot, source_tag) }) - } - - /// Extracts the low raw payload word from a boxed Mixed cell. - fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { - Ok(unsafe { __elephc_eval_value_raw_word(value.as_ptr()) }) - } - - /// Extracts the high raw payload word from a boxed Mixed cell. - fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result { - Ok(unsafe { __elephc_eval_value_raw_high_word(value.as_ptr()) }) - } - - /// Duplicates one raw string payload for owned native by-reference staging. - fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus> { - let mut out_len = 0; - let out_ptr = unsafe { __elephc_eval_value_retain_raw_string(ptr, len, &mut out_len) }; - Ok((out_ptr, out_len)) - } - - /// Boxes one raw string payload as a Mixed string cell. - fn raw_string_value(&mut self, ptr: u64, len: u64) -> Result { - Self::handle(unsafe { __elephc_eval_value_from_raw_string(ptr, len) }) - } - - /// Releases one raw string payload owned by native by-reference staging. - fn release_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(), EvalStatus> { - unsafe { - __elephc_eval_value_release_raw_string(ptr, len); - } - Ok(()) - } - - /// Retains one raw heap payload word for owned native by-reference staging. - fn retain_raw_heap_word(&mut self, word: u64) -> Result { - Ok(unsafe { __elephc_eval_value_retain_raw_heap_word(word) }) - } - - /// Boxes one one-word raw payload as a Mixed cell with the provided runtime tag. - fn raw_word_value( - &mut self, - source_tag: u64, - word: u64, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_from_raw_word(source_tag, word) }) - } - - /// Boxes one raw heap payload word as a Mixed cell using its runtime heap kind. - fn raw_heap_word_value(&mut self, word: u64) -> Result { - Self::handle(unsafe { __elephc_eval_value_from_raw_heap_word(word) }) - } - - /// Releases one raw heap payload word owned by native by-reference staging. - fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus> { - unsafe { - __elephc_eval_value_release_raw_heap_word(word); - } - Ok(()) - } - - /// Returns the unboxed object payload pointer for SPL object identity builtins. - fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { - let identity = unsafe { __elephc_eval_value_object_identity(object.as_ptr()) }; - if identity == 0 { - Err(EvalStatus::RuntimeFatal) - } else { - Ok(identity) - } - } - - /// Returns the object payload that the next release would destroy, when known. - fn final_object_identity_for_release( - &mut self, - value: RuntimeCellHandle, - ) -> Result, EvalStatus> { - let identity = unsafe { __elephc_eval_value_final_object_identity(value.as_ptr()) }; - Ok((identity != 0).then_some(identity)) - } - - /// Releases one boxed Mixed cell through the generated runtime wrapper. - fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - unsafe { - __elephc_eval_value_release(value.as_ptr()); - } - Ok(()) - } - - /// Retains one boxed Mixed cell through the generated runtime wrapper. - fn retain(&mut self, value: RuntimeCellHandle) -> Result { - Ok(RuntimeCellHandle::from_raw(unsafe { - __elephc_eval_value_retain(value.as_ptr()) - })) - } - - /// Emits one PHP warning through the generated runtime diagnostic helper. - fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { - unsafe { - __elephc_eval_warning(message.as_ptr(), message.len() as u64); - } - Ok(()) - } - - /// Creates a boxed null Mixed cell through the generated runtime wrapper. - fn null(&mut self) -> Result { - Self::handle(unsafe { __elephc_eval_value_null() }) - } - - /// Creates a boxed bool Mixed cell through the generated runtime wrapper. - fn bool_value(&mut self, value: bool) -> Result { - Self::handle(unsafe { __elephc_eval_value_bool(u64::from(value)) }) - } - - /// Creates a boxed int Mixed cell through the generated runtime wrapper. - fn int(&mut self, value: i64) -> Result { - Self::handle(unsafe { __elephc_eval_value_int(value) }) - } - - /// Creates a boxed resource Mixed cell through the generated runtime wrapper. - fn resource(&mut self, value: i64) -> Result { - Self::handle(unsafe { __elephc_eval_value_resource(value) }) - } - - /// Creates a boxed float Mixed cell through the generated runtime wrapper. - fn float(&mut self, value: f64) -> Result { - Self::handle(unsafe { __elephc_eval_value_float(value) }) - } - - /// Creates a boxed string Mixed cell through the generated runtime wrapper. - fn string(&mut self, value: &str) -> Result { - Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) - } - - /// Creates a boxed string Mixed cell from raw PHP bytes through the generated runtime wrapper. - fn string_bytes_value(&mut self, value: &[u8]) -> Result { - Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) - } - - /// Casts a boxed Mixed cell to a boxed integer Mixed cell through the generated runtime wrapper. - fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_cast_int(value.as_ptr()) }) - } - - /// Casts a boxed Mixed cell to a boxed float Mixed cell through the generated runtime wrapper. - fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_cast_float(value.as_ptr()) }) - } - - /// Casts a boxed Mixed cell to a boxed string Mixed cell through the generated runtime wrapper. - fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_cast_string(value.as_ptr()) }) - } - - /// Casts a boxed Mixed cell to a boxed boolean Mixed cell through the generated runtime wrapper. - fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_cast_bool(value.as_ptr()) }) - } - - /// Computes PHP `abs()` for a boxed Mixed cell through the generated runtime wrapper. - fn abs(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_abs(value.as_ptr()) }) - } - - /// Computes PHP `ceil()` for a boxed Mixed cell through the generated runtime wrapper. - fn ceil(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_ceil(value.as_ptr()) }) - } - - /// Computes PHP `floor()` for a boxed Mixed cell through the generated runtime wrapper. - fn floor(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_floor(value.as_ptr()) }) - } - - /// Computes PHP `sqrt()` for a boxed Mixed cell through the generated runtime wrapper. - fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_sqrt(value.as_ptr()) }) - } - - /// Computes PHP `strrev()` for a boxed Mixed cell through the generated runtime wrapper. - fn strrev(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_strrev(value.as_ptr()) }) - } - - /// Computes PHP `fdiv()` for boxed Mixed cells through the generated runtime wrapper. - fn fdiv( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_fdiv(left.as_ptr(), right.as_ptr()) }) - } - - /// Computes PHP `fmod()` for boxed Mixed cells through the generated runtime wrapper. - fn fmod( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_fmod(left.as_ptr(), right.as_ptr()) }) - } - - /// Adds two boxed Mixed cells using elephc runtime numeric semantics. - fn add( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_add(left.as_ptr(), right.as_ptr()) }) - } - - /// Subtracts two boxed Mixed cells using elephc runtime numeric semantics. - fn sub( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_sub(left.as_ptr(), right.as_ptr()) }) - } - - /// Multiplies two boxed Mixed cells using elephc runtime numeric semantics. - fn mul( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_mul(left.as_ptr(), right.as_ptr()) }) - } - - /// Divides two boxed Mixed cells using elephc runtime numeric semantics. - fn div( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_div(left.as_ptr(), right.as_ptr()) }) - } - - /// Computes modulo for two boxed Mixed cells using elephc runtime integer semantics. - fn modulo( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_mod(left.as_ptr(), right.as_ptr()) }) - } - - /// Raises two boxed Mixed cells using elephc runtime numeric exponentiation semantics. - fn pow( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_pow(left.as_ptr(), right.as_ptr()) }) - } - - /// Rounds a boxed Mixed cell through the generated runtime wrapper. - fn round( - &mut self, - value: RuntimeCellHandle, - precision: Option, - ) -> Result { - let (precision, has_precision) = if let Some(precision) = precision { - (precision.as_ptr(), 1) - } else { - (core::ptr::null_mut(), 0) - }; - Self::handle(unsafe { __elephc_eval_value_round(value.as_ptr(), precision, has_precision) }) - } - - /// Applies an integer bitwise or shift operation through the generated runtime wrapper. - fn bitwise( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_value_bitwise(left.as_ptr(), right.as_ptr(), bitwise_op_tag(op)) - }) - } - - /// Applies integer bitwise NOT through the generated runtime wrapper. - fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { - Self::handle(unsafe { __elephc_eval_value_bit_not(value.as_ptr()) }) - } - - /// Concatenates two boxed Mixed cells using elephc runtime string semantics. - fn concat( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_concat(left.as_ptr(), right.as_ptr()) }) - } - - /// Compares two boxed Mixed cells through the generated runtime wrapper. - fn compare( - &mut self, - op: EvalBinOp, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { - __elephc_eval_value_compare(left.as_ptr(), right.as_ptr(), compare_op_tag(op)) - }) - } - - /// Computes a PHP numeric spaceship result through the generated runtime wrapper. - fn spaceship( - &mut self, - left: RuntimeCellHandle, - right: RuntimeCellHandle, - ) -> Result { - Self::handle(unsafe { __elephc_eval_value_spaceship(left.as_ptr(), right.as_ptr()) }) - } - - /// Emits one boxed Mixed cell to stdout through the generated runtime wrapper. - fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { - unsafe { - __elephc_eval_value_echo(value.as_ptr()); - } - Ok(()) - } - - /// Casts one boxed Mixed cell to a PHP string and copies the bytes into Rust memory. - fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { - let mut ptr = std::ptr::null(); - let mut len = 0; - let ok = unsafe { __elephc_eval_value_string_bytes(value.as_ptr(), &mut ptr, &mut len) }; - if ok == 0 || (len > 0 && ptr.is_null()) { - return Err(EvalStatus::RuntimeFatal); - } - let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; - let bytes = if len == 0 { - &[] - } else { - unsafe { std::slice::from_raw_parts(ptr, len) } - }; - Ok(bytes.to_vec()) - } - - /// Converts one boxed Mixed cell to PHP truthiness through the generated runtime wrapper. - fn truthy(&mut self, value: RuntimeCellHandle) -> Result { - Ok(unsafe { __elephc_eval_value_truthy(value.as_ptr()) != 0 }) - } -} +mod collection_calls; +mod construction_raw; +mod lifecycle_scalars; +mod native_results; +mod numeric_string; +mod reflection; + +use collection_calls::impl_collection_call_ops; +use construction_raw::impl_construction_raw_ops; +use lifecycle_scalars::impl_lifecycle_scalar_ops; +use numeric_string::impl_numeric_string_ops; +use reflection::impl_reflection_ops; #[cfg(not(test))] -impl ElephcRuntimeOps { - /// Converts a generated native method-call result into an eval result status. - fn handle_native_call_result( - &self, - result: *mut RuntimeCell, - ) -> Result { - if !result.is_null() { - return Ok(RuntimeCellHandle::from_raw(result)); - } - self.take_pending_native_throwable() - .map_or(Err(EvalStatus::RuntimeFatal), |thrown| { - self.schedule_pending_throw(thrown)?; - Err(EvalStatus::UncaughtThrowable) - }) - } - - /// Takes a native Throwable that escaped through the generated constructor bridge. - fn take_pending_native_throwable(&self) -> Option { - let thrown = unsafe { __elephc_eval_value_take_pending_throwable() }; - if thrown.is_null() { - None - } else { - Self::object_from_raw(thrown).ok() - } - } - - /// Schedules a native Throwable so eval's ordinary catch machinery can handle it. - fn schedule_pending_throw(&self, thrown: RuntimeCellHandle) -> Result<(), EvalStatus> { - let Some(context) = - (unsafe { (self.context as *mut crate::abi::ElephcEvalContext).as_mut() }) - else { - return Err(EvalStatus::RuntimeFatal); - }; - context.set_pending_throw(thrown); - Ok(()) - } +impl RuntimeValueOps for ElephcRuntimeOps { + impl_collection_call_ops!(); + impl_reflection_ops!(); + impl_construction_raw_ops!(); + impl_lifecycle_scalar_ops!(); + impl_numeric_string_ops!(); } diff --git a/crates/elephc-magician/src/runtime_hooks/ops/collection_calls.rs b/crates/elephc-magician/src/runtime_hooks/ops/collection_calls.rs new file mode 100644 index 0000000000..e5e4cf4ea9 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/collection_calls.rs @@ -0,0 +1,323 @@ +//! Purpose: +//! Defines the array, property, object-call, static-call, and native-result +//! methods of the generated-runtime `RuntimeValueOps` adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Temporary argument arrays are released after bridge calls. + +macro_rules! impl_collection_call_ops { + () => { + /// Creates a boxed Mixed indexed array through the generated runtime wrapper. + fn array_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_new(capacity as u64) }) + } + + /// Creates a boxed Mixed indexed array whose payload uses direct string slots. + fn string_array_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_string_array_new(capacity as u64) }) + } + + /// Appends one string to a boxed direct-string indexed array. + fn string_array_push( + &mut self, + array: RuntimeCellHandle, + value: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_string_array_push( + array.as_ptr(), + value.as_ptr(), + value.len() as u64, + ) + }) + } + + /// Creates a boxed Mixed associative array through the generated runtime wrapper. + fn assoc_new(&mut self, capacity: usize) -> Result { + Self::handle(unsafe { __elephc_eval_value_assoc_new(capacity as u64) }) + } + + /// Reads one element from a boxed Mixed array through the generated runtime wrapper. + fn array_get( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_get(array.as_ptr(), index.as_ptr()) }) + } + + /// Checks whether a boxed Mixed array contains a normalized PHP key. + fn array_key_exists( + &mut self, + key: RuntimeCellHandle, + array: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_key_exists(key.as_ptr(), array.as_ptr()) }) + } + + /// Returns one foreach-visible key from a boxed Mixed array by iteration position. + fn array_iter_key( + &mut self, + array: RuntimeCellHandle, + position: usize, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_array_iter_key(array.as_ptr(), position as u64) }) + } + + /// Writes one element to a boxed Mixed array through the generated runtime wrapper. + fn array_set( + &mut self, + array: RuntimeCellHandle, + index: RuntimeCellHandle, + value: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_array_set(array.as_ptr(), index.as_ptr(), value.as_ptr()) + }) + } + + /// Reads a boxed Mixed object property through the generated user helper. + fn property_get( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + Self::handle(unsafe { + __elephc_eval_value_property_get( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }) + } + + /// Checks an AOT instance property initialization marker through the generated helper. + fn property_is_initialized( + &mut self, + object: RuntimeCellHandle, + property: &str, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let initialized = unsafe { + __elephc_eval_value_property_is_initialized( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }; + Ok(initialized != 0) + } + + /// Writes a boxed Mixed object property through the generated user helper. + fn property_set( + &mut self, + object: RuntimeCellHandle, + property: &str, + value: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ok = unsafe { + __elephc_eval_value_property_set( + object.as_ptr(), + property.as_ptr(), + property.len() as u64, + value.as_ptr(), + scope_ptr, + scope_len, + ) + }; + if ok == 0 { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(()) + } + } + + /// Reads an AOT static property through the generated user helper. + fn static_property_get( + &mut self, + class_name: &str, + property: &str, + ) -> Result, EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ptr = unsafe { + __elephc_eval_value_static_property_get( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + + /// Checks an AOT static property initialization marker through the generated helper. + fn static_property_is_initialized( + &mut self, + class_name: &str, + property: &str, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let initialized = unsafe { + __elephc_eval_value_static_property_is_initialized( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + scope_ptr, + scope_len, + ) + }; + Ok(initialized != 0) + } + + /// Writes an AOT static property through the generated user helper. + fn static_property_set( + &mut self, + class_name: &str, + property: &str, + value: RuntimeCellHandle, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ok = unsafe { + __elephc_eval_value_static_property_set( + class_name.as_ptr(), + class_name.len() as u64, + property.as_ptr(), + property.len() as u64, + value.as_ptr(), + scope_ptr, + scope_len, + ) + }; + Ok(ok != 0) + } + + /// Reads an AOT class-like constant through the generated user helper. + fn class_constant_get( + &mut self, + class_name: &str, + constant: &str, + ) -> Result, EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let ptr = unsafe { + __elephc_eval_value_class_constant_get( + class_name.as_ptr(), + class_name.len() as u64, + constant.as_ptr(), + constant.len() as u64, + scope_ptr, + scope_len, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + + /// Creates a shallow clone of a boxed Mixed stdClass/eval object through the generated wrapper. + fn object_clone_shallow( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_object_clone_shallow(object.as_ptr()) }) + } + + /// Returns the JSON-visible public property count for a boxed Mixed object. + fn object_property_len(&mut self, object: RuntimeCellHandle) -> Result { + let len = unsafe { __elephc_eval_value_object_property_len(object.as_ptr()) }; + usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns one JSON-visible public property key for a boxed Mixed object. + fn object_property_iter_key( + &mut self, + object: RuntimeCellHandle, + position: usize, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_object_property_iter_key(object.as_ptr(), position as u64) + }) + } + + /// Calls a boxed Mixed object method through the generated user helper. + fn method_call( + &mut self, + object: RuntimeCellHandle, + method: &str, + args: Vec, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let arg_array = Self::arg_array(args)?; + let result = unsafe { + __elephc_eval_value_method_call( + object.as_ptr(), + method.as_ptr(), + method.len() as u64, + arg_array.as_ptr(), + scope_ptr, + scope_len, + self.context.cast(), + ) + }; + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + self.handle_native_call_result(result) + } + + /// Calls an AOT static method through the generated user helper. + fn static_method_call( + &mut self, + class_name: &str, + method: &str, + args: Vec, + ) -> Result { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let arg_array = Self::arg_array(args)?; + let result = unsafe { + __elephc_eval_value_static_method_call( + class_name.as_ptr(), + class_name.len() as u64, + method.as_ptr(), + method.len() as u64, + arg_array.as_ptr(), + scope_ptr, + scope_len, + self.context.cast(), + ) + }; + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + self.handle_native_call_result(result) + } + + /// Converts a native free-function result into eval status, preserving pending throwables. + fn native_call_result( + &mut self, + result: RuntimeCellHandle, + ) -> Result { + self.handle_native_call_result(result.as_ptr()) + } + + }; +} + +pub(super) use impl_collection_call_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops/construction_raw.rs b/crates/elephc-magician/src/runtime_hooks/ops/construction_raw.rs new file mode 100644 index 0000000000..14ef19e2aa --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/construction_raw.rs @@ -0,0 +1,214 @@ +//! Purpose: +//! Defines object construction, class-like queries, raw value conversion, and +//! raw heap/string ownership methods for the runtime adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Raw retain/release pairs preserve the generated runtime ownership contract. + +macro_rules! impl_construction_raw_ops { + () => { + + /// Creates a boxed Mixed object through the generated dynamic class-name wrapper. + fn new_object(&mut self, class_name: &str) -> Result { + let object = Self::handle(unsafe { + __elephc_eval_value_new_object(class_name.as_ptr(), class_name.len() as u64) + })?; + match self.is_null(object) { + Ok(false) => Ok(object), + Ok(true) => { + self.release(object)?; + Err(EvalStatus::RuntimeFatal) + } + Err(err) => { + let _ = self.release(object); + Err(err) + } + } + } + + /// Calls an AOT constructor through the generated user bridge when one exists. + fn construct_object( + &mut self, + object: RuntimeCellHandle, + args: Vec, + ) -> Result<(), EvalStatus> { + let (scope_ptr, scope_len) = self.current_class_scope_abi(); + let arg_array = Self::arg_array(args)?; + let ok = unsafe { + __elephc_eval_value_construct_object( + object.as_ptr(), + arg_array.as_ptr(), + scope_ptr, + scope_len, + self.context.cast(), + ) + }; + unsafe { + __elephc_eval_value_release(arg_array.as_ptr()); + } + if ok == 0 { + self.take_pending_native_throwable() + .map_or(Err(EvalStatus::RuntimeFatal), |thrown| { + self.schedule_pending_throw(thrown)?; + Err(EvalStatus::UncaughtThrowable) + }) + } else { + Ok(()) + } + } + + /// Returns whether the generated AOT class-name table contains the requested class. + fn class_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_class_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Returns whether the generated AOT interface-name table contains the requested interface. + fn interface_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_interface_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Returns whether the generated AOT trait-name table contains the requested trait. + fn trait_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_trait_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Returns whether the generated AOT enum-name table contains the requested enum. + fn enum_exists(&mut self, name: &str) -> Result { + Ok(unsafe { __elephc_eval_enum_exists(name.as_ptr(), name.len() as u64) != 0 }) + } + + /// Tests a boxed Mixed object against generated class/interface metadata. + fn object_is_a( + &mut self, + object_or_class: RuntimeCellHandle, + target_class: &str, + exclude_self: bool, + ) -> Result { + Ok(unsafe { + __elephc_eval_value_is_a( + object_or_class.as_ptr(), + target_class.as_ptr(), + target_class.len() as u64, + u64::from(exclude_self), + ) != 0 + }) + } + + /// Returns a boxed Mixed string naming a boxed Mixed object's runtime class. + fn object_class_name( + &mut self, + object: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_object_class_name(object.as_ptr()) }) + } + + /// Returns a boxed Mixed string naming a boxed Mixed object's or class string's parent class. + fn parent_class_name( + &mut self, + object_or_class: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_parent_class_name(object_or_class.as_ptr()) }) + } + + /// Returns the visible element count for a boxed Mixed array through the generated runtime wrapper. + fn array_len(&mut self, array: RuntimeCellHandle) -> Result { + let len = unsafe { __elephc_eval_value_array_len(array.as_ptr()) }; + usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns whether a boxed Mixed cell has an array-like runtime tag. + fn is_array_like(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_is_array_like(value.as_ptr()) != 0 }) + } + + /// Returns whether a boxed Mixed cell unwraps to PHP null. + fn is_null(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_is_null(value.as_ptr()) != 0 }) + } + + /// Returns the unboxed Mixed runtime tag for PHP type-predicate builtins. + fn type_tag(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_type_tag(value.as_ptr()) }) + } + + /// Creates an invoker-only by-reference marker for a staged Mixed slot. + fn invoker_ref_cell( + &mut self, + slot: *mut RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_invoker_ref_cell(slot) }) + } + + /// Creates an invoker-only by-reference marker for a staged raw one-word slot. + fn invoker_raw_ref_cell( + &mut self, + slot: *mut std::ffi::c_void, + source_tag: u64, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_invoker_raw_ref_cell(slot, source_tag) }) + } + + /// Extracts the low raw payload word from a boxed Mixed cell. + fn raw_value_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_raw_word(value.as_ptr()) }) + } + + /// Extracts the high raw payload word from a boxed Mixed cell. + fn raw_value_high_word(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_raw_high_word(value.as_ptr()) }) + } + + /// Duplicates one raw string payload for owned native by-reference staging. + fn retain_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(u64, u64), EvalStatus> { + let mut out_len = 0; + let out_ptr = unsafe { __elephc_eval_value_retain_raw_string(ptr, len, &mut out_len) }; + Ok((out_ptr, out_len)) + } + + /// Boxes one raw string payload as a Mixed string cell. + fn raw_string_value(&mut self, ptr: u64, len: u64) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_string(ptr, len) }) + } + + /// Releases one raw string payload owned by native by-reference staging. + fn release_raw_string_words(&mut self, ptr: u64, len: u64) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release_raw_string(ptr, len); + } + Ok(()) + } + + /// Retains one raw heap payload word for owned native by-reference staging. + fn retain_raw_heap_word(&mut self, word: u64) -> Result { + Ok(unsafe { __elephc_eval_value_retain_raw_heap_word(word) }) + } + + /// Boxes one one-word raw payload as a Mixed cell with the provided runtime tag. + fn raw_word_value( + &mut self, + source_tag: u64, + word: u64, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_word(source_tag, word) }) + } + + /// Boxes one raw heap payload word as a Mixed cell using its runtime heap kind. + fn raw_heap_word_value(&mut self, word: u64) -> Result { + Self::handle(unsafe { __elephc_eval_value_from_raw_heap_word(word) }) + } + + /// Releases one raw heap payload word owned by native by-reference staging. + fn release_raw_heap_word(&mut self, word: u64) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release_raw_heap_word(word); + } + Ok(()) + } + + }; +} + +pub(super) use impl_construction_raw_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops/lifecycle_scalars.rs b/crates/elephc-magician/src/runtime_hooks/ops/lifecycle_scalars.rs new file mode 100644 index 0000000000..8c29450b92 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/lifecycle_scalars.rs @@ -0,0 +1,114 @@ +//! Purpose: +//! Defines object identity, retain/release, warning, scalar construction, and +//! scalar-cast methods for the generated runtime adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Every runtime pointer is validated before it becomes a handle. + +macro_rules! impl_lifecycle_scalar_ops { + () => { + + /// Returns the unboxed object payload pointer for SPL object identity builtins. + fn object_identity(&mut self, object: RuntimeCellHandle) -> Result { + let identity = unsafe { __elephc_eval_value_object_identity(object.as_ptr()) }; + if identity == 0 { + Err(EvalStatus::RuntimeFatal) + } else { + Ok(identity) + } + } + + /// Returns the object payload that the next release would destroy, when known. + fn final_object_identity_for_release( + &mut self, + value: RuntimeCellHandle, + ) -> Result, EvalStatus> { + let identity = unsafe { __elephc_eval_value_final_object_identity(value.as_ptr()) }; + Ok((identity != 0).then_some(identity)) + } + + /// Releases one boxed Mixed cell through the generated runtime wrapper. + fn release(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_release(value.as_ptr()); + } + Ok(()) + } + + /// Retains one boxed Mixed cell through the generated runtime wrapper. + fn retain(&mut self, value: RuntimeCellHandle) -> Result { + Ok(RuntimeCellHandle::from_raw(unsafe { + __elephc_eval_value_retain(value.as_ptr()) + })) + } + + /// Emits one PHP warning through the generated runtime diagnostic helper. + fn warning(&mut self, message: &str) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_warning(message.as_ptr(), message.len() as u64); + } + Ok(()) + } + + /// Creates a boxed null Mixed cell through the generated runtime wrapper. + fn null(&mut self) -> Result { + Self::handle(unsafe { __elephc_eval_value_null() }) + } + + /// Creates a boxed bool Mixed cell through the generated runtime wrapper. + fn bool_value(&mut self, value: bool) -> Result { + Self::handle(unsafe { __elephc_eval_value_bool(u64::from(value)) }) + } + + /// Creates a boxed int Mixed cell through the generated runtime wrapper. + fn int(&mut self, value: i64) -> Result { + Self::handle(unsafe { __elephc_eval_value_int(value) }) + } + + /// Creates a boxed resource Mixed cell through the generated runtime wrapper. + fn resource(&mut self, value: i64) -> Result { + Self::handle(unsafe { __elephc_eval_value_resource(value) }) + } + + /// Creates a boxed float Mixed cell through the generated runtime wrapper. + fn float(&mut self, value: f64) -> Result { + Self::handle(unsafe { __elephc_eval_value_float(value) }) + } + + /// Creates a boxed string Mixed cell through the generated runtime wrapper. + fn string(&mut self, value: &str) -> Result { + Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) + } + + /// Creates a boxed string Mixed cell from raw PHP bytes through the generated runtime wrapper. + fn string_bytes_value(&mut self, value: &[u8]) -> Result { + Self::handle(unsafe { __elephc_eval_value_string(value.as_ptr(), value.len() as u64) }) + } + + /// Casts a boxed Mixed cell to a boxed integer Mixed cell through the generated runtime wrapper. + fn cast_int(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_int(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed float Mixed cell through the generated runtime wrapper. + fn cast_float(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_float(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed string Mixed cell through the generated runtime wrapper. + fn cast_string(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_string(value.as_ptr()) }) + } + + /// Casts a boxed Mixed cell to a boxed boolean Mixed cell through the generated runtime wrapper. + fn cast_bool(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_cast_bool(value.as_ptr()) }) + } + + }; +} + +pub(super) use impl_lifecycle_scalar_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops/native_results.rs b/crates/elephc-magician/src/runtime_hooks/ops/native_results.rs new file mode 100644 index 0000000000..4424a05325 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/native_results.rs @@ -0,0 +1,53 @@ +//! Purpose: +//! Converts null native bridge results into pending Throwable state and schedules +//! escaped native exceptions for eval catch handling. +//! +//! Called from: +//! - Runtime method and constructor bridge operations. +//! +//! Key details: +//! - Null without a pending Throwable remains a runtime fatal error. + +use super::*; + +#[cfg(not(test))] +impl ElephcRuntimeOps { + /// Converts a generated native method-call result into an eval result status. + pub(super) fn handle_native_call_result( + &self, + result: *mut RuntimeCell, + ) -> Result { + if !result.is_null() { + return Ok(RuntimeCellHandle::from_raw(result)); + } + self.take_pending_native_throwable() + .map_or(Err(EvalStatus::RuntimeFatal), |thrown| { + self.schedule_pending_throw(thrown)?; + Err(EvalStatus::UncaughtThrowable) + }) + } + + /// Takes a native Throwable that escaped through the generated constructor bridge. + pub(super) fn take_pending_native_throwable(&self) -> Option { + let thrown = unsafe { __elephc_eval_value_take_pending_throwable() }; + if thrown.is_null() { + None + } else { + Self::object_from_raw(thrown).ok() + } + } + + /// Schedules a native Throwable so eval's ordinary catch machinery can handle it. + pub(super) fn schedule_pending_throw( + &self, + thrown: RuntimeCellHandle, + ) -> Result<(), EvalStatus> { + let Some(context) = + (unsafe { (self.context as *mut crate::abi::ElephcEvalContext).as_mut() }) + else { + return Err(EvalStatus::RuntimeFatal); + }; + context.set_pending_throw(thrown); + Ok(()) + } +} diff --git a/crates/elephc-magician/src/runtime_hooks/ops/numeric_string.rs b/crates/elephc-magician/src/runtime_hooks/ops/numeric_string.rs new file mode 100644 index 0000000000..226d66d03b --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/numeric_string.rs @@ -0,0 +1,205 @@ +//! Purpose: +//! Defines numeric, bitwise, comparison, concatenation, output, byte, and +//! truthiness operations for the generated runtime adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Binary and comparison op tags continue to use the shared target mappings. + +macro_rules! impl_numeric_string_ops { + () => { + + /// Computes PHP `abs()` for a boxed Mixed cell through the generated runtime wrapper. + fn abs(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_abs(value.as_ptr()) }) + } + + /// Computes PHP `ceil()` for a boxed Mixed cell through the generated runtime wrapper. + fn ceil(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_ceil(value.as_ptr()) }) + } + + /// Computes PHP `floor()` for a boxed Mixed cell through the generated runtime wrapper. + fn floor(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_floor(value.as_ptr()) }) + } + + /// Computes PHP `sqrt()` for a boxed Mixed cell through the generated runtime wrapper. + fn sqrt(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_sqrt(value.as_ptr()) }) + } + + /// Computes PHP `strrev()` for a boxed Mixed cell through the generated runtime wrapper. + fn strrev(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_strrev(value.as_ptr()) }) + } + + /// Computes PHP `fdiv()` for boxed Mixed cells through the generated runtime wrapper. + fn fdiv( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_fdiv(left.as_ptr(), right.as_ptr()) }) + } + + /// Computes PHP `fmod()` for boxed Mixed cells through the generated runtime wrapper. + fn fmod( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_fmod(left.as_ptr(), right.as_ptr()) }) + } + + /// Adds two boxed Mixed cells using elephc runtime numeric semantics. + fn add( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_add(left.as_ptr(), right.as_ptr()) }) + } + + /// Subtracts two boxed Mixed cells using elephc runtime numeric semantics. + fn sub( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_sub(left.as_ptr(), right.as_ptr()) }) + } + + /// Multiplies two boxed Mixed cells using elephc runtime numeric semantics. + fn mul( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_mul(left.as_ptr(), right.as_ptr()) }) + } + + /// Divides two boxed Mixed cells using elephc runtime numeric semantics. + fn div( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_div(left.as_ptr(), right.as_ptr()) }) + } + + /// Computes modulo for two boxed Mixed cells using elephc runtime integer semantics. + fn modulo( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_mod(left.as_ptr(), right.as_ptr()) }) + } + + /// Raises two boxed Mixed cells using elephc runtime numeric exponentiation semantics. + fn pow( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_pow(left.as_ptr(), right.as_ptr()) }) + } + + /// Rounds a boxed Mixed cell through the generated runtime wrapper. + fn round( + &mut self, + value: RuntimeCellHandle, + precision: Option, + ) -> Result { + let (precision, has_precision) = if let Some(precision) = precision { + (precision.as_ptr(), 1) + } else { + (core::ptr::null_mut(), 0) + }; + Self::handle(unsafe { __elephc_eval_value_round(value.as_ptr(), precision, has_precision) }) + } + + /// Applies an integer bitwise or shift operation through the generated runtime wrapper. + fn bitwise( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_bitwise(left.as_ptr(), right.as_ptr(), bitwise_op_tag(op)) + }) + } + + /// Applies integer bitwise NOT through the generated runtime wrapper. + fn bit_not(&mut self, value: RuntimeCellHandle) -> Result { + Self::handle(unsafe { __elephc_eval_value_bit_not(value.as_ptr()) }) + } + + /// Concatenates two boxed Mixed cells using elephc runtime string semantics. + fn concat( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_concat(left.as_ptr(), right.as_ptr()) }) + } + + /// Compares two boxed Mixed cells through the generated runtime wrapper. + fn compare( + &mut self, + op: EvalBinOp, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_value_compare(left.as_ptr(), right.as_ptr(), compare_op_tag(op)) + }) + } + + /// Computes a PHP numeric spaceship result through the generated runtime wrapper. + fn spaceship( + &mut self, + left: RuntimeCellHandle, + right: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { __elephc_eval_value_spaceship(left.as_ptr(), right.as_ptr()) }) + } + + /// Emits one boxed Mixed cell to stdout through the generated runtime wrapper. + fn echo(&mut self, value: RuntimeCellHandle) -> Result<(), EvalStatus> { + unsafe { + __elephc_eval_value_echo(value.as_ptr()); + } + Ok(()) + } + + /// Casts one boxed Mixed cell to a PHP string and copies the bytes into Rust memory. + fn string_bytes(&mut self, value: RuntimeCellHandle) -> Result, EvalStatus> { + let mut ptr = std::ptr::null(); + let mut len = 0; + let ok = unsafe { __elephc_eval_value_string_bytes(value.as_ptr(), &mut ptr, &mut len) }; + if ok == 0 || (len > 0 && ptr.is_null()) { + return Err(EvalStatus::RuntimeFatal); + } + let len = usize::try_from(len).map_err(|_| EvalStatus::RuntimeFatal)?; + let bytes = if len == 0 { + &[] + } else { + unsafe { std::slice::from_raw_parts(ptr, len) } + }; + Ok(bytes.to_vec()) + } + + /// Converts one boxed Mixed cell to PHP truthiness through the generated runtime wrapper. + fn truthy(&mut self, value: RuntimeCellHandle) -> Result { + Ok(unsafe { __elephc_eval_value_truthy(value.as_ptr()) != 0 }) + } + + }; +} + +pub(super) use impl_numeric_string_ops; diff --git a/crates/elephc-magician/src/runtime_hooks/ops/reflection.rs b/crates/elephc-magician/src/runtime_hooks/ops/reflection.rs new file mode 100644 index 0000000000..eea14a9858 --- /dev/null +++ b/crates/elephc-magician/src/runtime_hooks/ops/reflection.rs @@ -0,0 +1,330 @@ +//! Purpose: +//! Defines reflection object materialization and metadata-query methods for the +//! generated-runtime `RuntimeValueOps` adapter. +//! +//! Called from: +//! - The single `RuntimeValueOps for ElephcRuntimeOps` implementation in `super`. +//! +//! Key details: +//! - Nullable wrapper results are mapped to `Option` without manufacturing handles. + +macro_rules! impl_reflection_ops { + () => { + + /// Materializes a populated synthetic `ReflectionAttribute` object for eval metadata. + fn reflection_attribute_new( + &mut self, + name: &str, + args: RuntimeCellHandle, + target: u64, + repeated: bool, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_attribute_new( + name.as_ptr(), + name.len() as u64, + args.as_ptr(), + target, + if repeated { 1 } else { 0 }, + ) + }) + } + + /// Materializes a populated synthetic Reflection owner object for eval metadata. + fn reflection_owner_new( + &mut self, + owner_kind: u64, + reflected_name: &str, + attrs: RuntimeCellHandle, + interface_names: RuntimeCellHandle, + trait_names: RuntimeCellHandle, + method_names: RuntimeCellHandle, + property_names: RuntimeCellHandle, + method_objects: RuntimeCellHandle, + property_objects: RuntimeCellHandle, + parent_class: RuntimeCellHandle, + flags: u64, + modifiers: u64, + method_modifiers: u64, + constant_value: RuntimeCellHandle, + backing_value: RuntimeCellHandle, + constructor: RuntimeCellHandle, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_owner_new( + owner_kind, + reflected_name.as_ptr(), + reflected_name.len() as u64, + attrs.as_ptr(), + interface_names.as_ptr(), + trait_names.as_ptr(), + method_names.as_ptr(), + property_names.as_ptr(), + method_objects.as_ptr(), + property_objects.as_ptr(), + parent_class.as_ptr(), + flags, + modifiers, + method_modifiers, + constant_value.as_ptr(), + backing_value.as_ptr(), + constructor.as_ptr(), + ) + }) + } + + /// Returns generated AOT ReflectionMethod flags, or `None` when no row matches. + fn reflection_method_flags( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_method_flags( + class_name.as_ptr(), + class_name.len() as u64, + method_name.as_ptr(), + method_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionMethod declaring class metadata. + fn reflection_method_declaring_class( + &mut self, + class_name: &str, + method_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_method_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + method_name.as_ptr(), + method_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionMethod names visible for one class. + fn reflection_method_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_method_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + + /// Returns generated AOT source-file metadata for reflection source-location calls. + fn reflection_source_file(&mut self) -> Result, EvalStatus> { + let ptr = unsafe { __elephc_eval_reflection_source_file() }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionClass modifier flags, or `None` when no row matches. + fn reflection_class_flags(&mut self, class_name: &str) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_class_flags(class_name.as_ptr(), class_name.len() as u64) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionProperty flags, or `None` when no row matches. + fn reflection_property_flags( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_property_flags( + class_name.as_ptr(), + class_name.len() as u64, + property_name.as_ptr(), + property_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionProperty declaring class metadata. + fn reflection_property_declaring_class( + &mut self, + class_name: &str, + property_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_property_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + property_name.as_ptr(), + property_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionProperty names visible for one class. + fn reflection_property_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_property_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + + /// Returns generated AOT ReflectionClassConstant values without visibility checks. + fn reflection_constant_value( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_constant_value( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(RuntimeCellHandle::from_raw(ptr))) + } + } + + /// Returns generated AOT ReflectionClassConstant flags for one constant. + fn reflection_constant_flags( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let flags = unsafe { + __elephc_eval_reflection_constant_flags( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + Ok((flags != 0).then_some(flags)) + } + + /// Returns generated AOT ReflectionClassConstant declaring class metadata. + fn reflection_constant_declaring_class( + &mut self, + class_name: &str, + constant_name: &str, + ) -> Result, EvalStatus> { + let ptr = unsafe { + __elephc_eval_reflection_constant_declaring_class( + class_name.as_ptr(), + class_name.len() as u64, + constant_name.as_ptr(), + constant_name.len() as u64, + ) + }; + if ptr.is_null() { + return Ok(None); + } + let handle = RuntimeCellHandle::from_raw(ptr); + let bytes = self.string_bytes(handle)?; + self.release(handle)?; + String::from_utf8(bytes) + .map(Some) + .map_err(|_| EvalStatus::RuntimeFatal) + } + + /// Returns generated AOT ReflectionClassConstant names visible for one class. + fn reflection_constant_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_constant_names(class_name.as_ptr(), class_name.len() as u64) + }) + } + + /// Returns generated AOT interface names visible for one reflected class-like symbol. + fn reflection_class_interface_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_interface_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + /// Returns generated AOT trait names visible for one reflected class-like symbol. + fn reflection_class_trait_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + /// Returns generated AOT trait alias names visible for one reflected class-like symbol. + fn reflection_class_trait_alias_names( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_alias_names( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + /// Returns generated AOT trait alias sources visible for one reflected class-like symbol. + fn reflection_class_trait_alias_sources( + &mut self, + class_name: &str, + ) -> Result { + Self::handle(unsafe { + __elephc_eval_reflection_class_trait_alias_sources( + class_name.as_ptr(), + class_name.len() as u64, + ) + }) + } + + }; +} + +pub(super) use impl_reflection_ops; From 9027443c45ffd877d93479c1131c2a4a052326ff Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 13 Jul 2026 13:27:19 +0200 Subject: [PATCH 1205/1208] refactor(magician): split interpreter test suites --- .../tests/builtins_class_metadata.rs | 4063 ----------------- .../builtins_class_metadata/attributes.rs | 463 ++ .../builtins_class_metadata/callable_types.rs | 403 ++ .../class_capabilities.rs | 247 + .../builtins_class_metadata/class_identity.rs | 418 ++ .../class_operations.rs | 440 ++ .../constants_enums_objects.rs | 432 ++ .../member_constructors.rs | 418 ++ .../member_metadata.rs | 453 ++ .../tests/builtins_class_metadata/mod.rs | 21 + .../parameter_defaults.rs | 154 + .../property_values.rs | 474 ++ .../builtins_class_metadata/relations.rs | 270 ++ .../src/interpreter/tests/classes.rs | 2629 ----------- .../tests/classes/asymmetric_properties.rs | 381 ++ .../interpreter/tests/classes/attributes.rs | 205 + .../src/interpreter/tests/classes/basics.rs | 430 ++ .../tests/classes/hook_contracts.rs | 396 ++ .../interpreter/tests/classes/lifecycle.rs | 179 + .../tests/classes/magic_methods.rs | 434 ++ .../src/interpreter/tests/classes/mod.rs | 19 + .../tests/classes/promoted_references.rs | 171 + .../tests/classes/property_hooks.rs | 209 + .../src/interpreter/tests/classes/readonly.rs | 325 ++ .../src/interpreter/tests/dynamic_calls.rs | 1065 ----- .../tests/dynamic_calls/call_user_func.rs | 306 ++ .../dynamic_calls/call_user_func_array.rs | 227 + .../dynamic_calls/first_class_objects.rs | 340 ++ .../interpreter/tests/dynamic_calls/mod.rs | 14 + .../tests/dynamic_calls/runtime_callables.rs | 229 + .../src/interpreter/tests/expressions.rs | 1584 ------- .../tests/expressions/classes_traits.rs | 389 ++ .../tests/expressions/interface_contracts.rs | 375 ++ .../tests/expressions/method_contracts.rs | 322 ++ .../src/interpreter/tests/expressions/mod.rs | 15 + .../tests/expressions/scalars_objects.rs | 341 ++ .../tests/expressions/visibility.rs | 207 + .../src/interpreter/tests/method_arguments.rs | 789 ---- .../method_arguments/binding_defaults.rs | 152 + .../tests/method_arguments/by_reference.rs | 337 ++ .../interpreter/tests/method_arguments/mod.rs | 14 + .../method_arguments/runtime_fallback.rs | 166 + .../tests/method_arguments/types_errors.rs | 171 + 43 files changed, 10547 insertions(+), 10130 deletions(-) delete mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/attributes.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/callable_types.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_capabilities.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_identity.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_operations.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/constants_enums_objects.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_constructors.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_metadata.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/parameter_defaults.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/property_values.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/relations.rs delete mode 100644 crates/elephc-magician/src/interpreter/tests/classes.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/asymmetric_properties.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/attributes.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/basics.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/hook_contracts.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/lifecycle.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/magic_methods.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/promoted_references.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/property_hooks.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/classes/readonly.rs delete mode 100644 crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func_array.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/dynamic_calls/first_class_objects.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/dynamic_calls/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/dynamic_calls/runtime_callables.rs delete mode 100644 crates/elephc-magician/src/interpreter/tests/expressions.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/expressions/classes_traits.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/expressions/interface_contracts.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/expressions/method_contracts.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/expressions/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/expressions/scalars_objects.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/expressions/visibility.rs delete mode 100644 crates/elephc-magician/src/interpreter/tests/method_arguments.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/method_arguments/binding_defaults.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/method_arguments/by_reference.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/method_arguments/mod.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/method_arguments/runtime_fallback.rs create mode 100644 crates/elephc-magician/src/interpreter/tests/method_arguments/types_errors.rs diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs deleted file mode 100644 index b969288960..0000000000 --- a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata.rs +++ /dev/null @@ -1,4063 +0,0 @@ -//! Purpose: -//! Interpreter tests for eval class metadata and relation builtins. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - Eval class declarations expose parent/interface metadata plus class-level -//! attribute names and supported literal positional args. -//! - Tests verify direct calls, dynamic calls, named arguments, and builtin probes. - -use super::super::*; -use super::support::*; - -/// Verifies class-relation helpers handle eval class and trait targets. -#[test] -fn execute_program_dispatches_class_relation_builtins() { - let program = parse_fragment( - br#"class EvalMeta {} -trait EvalMetaInnerTrait {} -trait EvalMetaOuterTrait { - use EvalMetaInnerTrait; -} -$object = new EvalMeta(); -$implements = class_implements("EvalMeta"); -echo is_array($implements) && count($implements) === 0 ? "impl" : "bad"; echo ":"; -$parents = class_parents($object); -echo is_array($parents) && count($parents) === 0 ? "parents" : "bad"; echo ":"; -$uses = class_uses("EvalMeta"); -echo is_array($uses) && count($uses) === 0 ? "uses" : "bad"; echo ":"; -echo class_implements("MissingMeta") === false ? "missing" : "bad"; echo ":"; -$call = call_user_func("class_implements", "EvalMeta"); -echo is_array($call) && count($call) === 0 ? "call" : "bad"; echo ":"; -$named = call_user_func_array("class_parents", ["object_or_class" => "EvalMeta"]); -echo is_array($named) && count($named) === 0 ? "named" : "bad"; echo ":"; -$trait_uses = class_uses("EvalMetaOuterTrait"); -echo $trait_uses["EvalMetaInnerTrait"]; echo ":"; -class_alias("EvalMetaOuterTrait", "EvalMetaOuterTraitAlias"); -$alias_uses = class_uses("EvalMetaOuterTraitAlias"); -echo $alias_uses["EvalMetaInnerTrait"]; echo ":"; -echo function_exists("class_implements"); echo function_exists("class_parents"); -echo function_exists("class_uses"); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "impl:parents:uses:missing:call:named:EvalMetaInnerTrait:EvalMetaInnerTrait:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} -/// Verifies eval-declared parent and interface metadata is exposed to relation builtins. -#[test] -fn execute_program_reports_eval_class_relation_metadata() { - let program = parse_fragment( - br#"class EvalMetaBase {} -class EvalMetaChild extends EvalMetaBase implements KnownInterface {} -$object = new EvalMetaChild(); -$implements = class_implements($object); -echo count($implements); echo ":"; -echo $implements["KnownInterface"]; echo ":"; -echo $implements["Traversable"]; echo ":"; -$parents = class_parents("EvalMetaChild"); -echo count($parents); echo ":"; -echo $parents["EvalMetaBase"]; echo ":"; -$call = call_user_func("class_implements", "EvalMetaChild"); -echo $call["KnownInterface"]; echo ":"; -echo $call["Traversable"]; echo ":"; -$named = call_user_func_array("class_parents", ["object_or_class" => $object]); -echo $named["EvalMetaBase"]; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2:KnownInterface:Traversable:1:EvalMetaBase:KnownInterface:Traversable:EvalMetaBase" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies generated/AOT parent and interface metadata is exposed to relation builtins. -#[test] -fn execute_program_reports_aot_class_relation_metadata() { - let program = parse_fragment( - br#"$implements = class_implements("KnownClass"); -echo count($implements); echo ":"; -echo $implements["KnownInterface"]; echo ":"; -$parents = class_parents("KnownClass"); -echo count($parents); echo ":"; -echo $parents["ParentClass"]; echo ":"; -$call = call_user_func("class_implements", "KnownClass"); -echo $call["KnownInterface"]; echo ":"; -$interfaceParents = class_implements("KnownInterface"); -echo $interfaceParents["Traversable"]; echo ":"; -$uses = class_uses("KnownClass"); -echo count($uses); echo ":"; echo $uses["KnownTrait"]; echo ":"; -$traitUses = class_uses("KnownTrait"); -echo $traitUses["KnownInnerTrait"]; echo ":"; -$named = call_user_func_array("class_parents", ["object_or_class" => "KnownClass"]); -echo $named["ParentClass"]; echo ":"; -class_alias("KnownClass", "KnownAlias"); -$aliasImplements = class_implements("KnownAlias"); -echo $aliasImplements["KnownInterface"]; echo ":"; -$aliasParents = class_parents("KnownAlias"); -echo $aliasParents["ParentClass"]; echo ":"; -$aliasUses = class_uses("KnownAlias"); -echo $aliasUses["KnownTrait"]; -return true;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - assert!(context.define_native_class_parent("KnownClass", "ParentClass")); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = - execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!( - values.output, - "1:KnownInterface:1:ParentClass:KnownInterface:Traversable:1:KnownTrait:KnownInnerTrait:ParentClass:KnownInterface:ParentClass:KnownTrait" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies PHP OOP introspection builtins follow eval visibility and scope rules. -#[test] -fn execute_program_dispatches_oop_introspection_builtins() { - let program = parse_fragment( - br#"class EvalOopIntrospectBase { - private $baseSecret = "bp"; - protected $baseProtected = "bq"; - public $basePublic = "br"; - private function basePrivate() {} - protected function baseProtectedMethod() {} - public function basePublicMethod() {} - public function parentView() { - $vars = get_object_vars($this); - ksort($vars); - echo implode(",", array_keys($vars)); - } -} -class EvalOopIntrospectChild extends EvalOopIntrospectBase { - private $childSecret = "cp"; - protected $childProtected = "cq"; - public $childPublic = "cr"; - private function childPrivate() {} - protected function childProtectedMethod() {} - public function childPublicMethod() {} - public function childView() { - $methods = get_class_methods($this); - sort($methods); - echo implode(",", $methods); echo "|"; - $vars = get_object_vars($this); - ksort($vars); - echo implode(",", array_keys($vars)); - } -} -$object = new EvalOopIntrospectChild(); -$object->dynamic = "dyn"; -echo method_exists("EvalOopIntrospectChild", "basePrivate") ? "bad" : "noParentPrivateMethod"; echo ":"; -echo method_exists($object, "basePrivate") ? "objectParentPrivateMethod" : "bad"; echo ":"; -echo method_exists("EvalOopIntrospectChild", "baseProtectedMethod") ? "classProtectedMethod" : "bad"; echo ":"; -echo property_exists("EvalOopIntrospectChild", "baseSecret") ? "bad" : "noParentPrivateProperty"; echo ":"; -echo property_exists($object, "baseSecret") ? "bad" : "noObjectParentPrivateProperty"; echo ":"; -echo property_exists($object, "dynamic") ? "dynamicProperty" : "bad"; echo ":"; -$methods = get_class_methods("EvalOopIntrospectChild"); -sort($methods); -echo implode(",", $methods); echo ":"; -$vars = get_object_vars($object); -ksort($vars); -echo implode(",", array_keys($vars)); echo ":"; -$object->childView(); echo ":"; -$object->parentView(); echo ":"; -echo call_user_func("method_exists", $object, "childPrivate") ? "callMethod" : "bad"; echo ":"; -echo call_user_func_array("property_exists", ["property" => "dynamic", "object_or_class" => $object]) ? "namedProperty" : "bad"; echo ":"; -echo function_exists("method_exists"); echo function_exists("property_exists"); -echo function_exists("get_class_methods"); echo function_exists("get_object_vars"); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basePublicMethod,childPublicMethod,childView,parentView:basePublic,childPublic,dynamic:baseProtectedMethod,basePublicMethod,childPrivate,childProtectedMethod,childPublicMethod,childView,parentView|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies `get_class_vars()` materializes visible defaults for eval class-like metadata. -#[test] -fn execute_program_dispatches_get_class_vars_builtin() { - let program = parse_fragment( - br#"trait EvalClassVarsTrait { - public $traitPublic = "tp"; - protected $traitProtected = "tq"; -} -enum EvalClassVarsBacked: int { case Ready = 1; } -class EvalClassVarsBase { - public $basePublic = "bp"; - protected $baseProtected = "bq"; - private $basePrivate = "bs"; - public static $baseStatic = "static"; - public int $typed; -} -class EvalClassVarsChild extends EvalClassVarsBase { - use EvalClassVarsTrait; - public $childPublic = "cp"; - protected $childProtected = "cq"; - private $childPrivate = "cs"; - public function childView() { - $vars = get_class_vars(self::class); - ksort($vars); - foreach ($vars as $name => $value) { - echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; - } - } - public function baseView() { - $vars = get_class_vars(EvalClassVarsBase::class); - ksort($vars); - foreach ($vars as $name => $value) { - echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; - } - } -} -$outside = get_class_vars("EvalClassVarsChild"); -ksort($outside); -foreach ($outside as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } -echo ":"; -(new EvalClassVarsChild())->childView(); -echo ":"; -(new EvalClassVarsChild())->baseView(); -echo ":"; -$trait = call_user_func("get_class_vars", "EvalClassVarsTrait"); -ksort($trait); -foreach ($trait as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } -echo ":"; -$enum = call_user_func_array("get_class_vars", ["class" => "EvalClassVarsBacked"]); -ksort($enum); -foreach ($enum as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } -echo ":"; -echo function_exists("get_class_vars") ? "F" : "f"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "basePublic=bp|baseStatic=static|childPublic=cp|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|childPrivate=cs|childProtected=cq|childPublic=cp|traitProtected=tq|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|typed=null|:traitPublic=tp|:name=null|value=null|:F" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies class attribute helpers expose eval class-level metadata. -#[test] -fn execute_program_dispatches_class_attribute_metadata_builtins() { - let program = parse_fragment( - br#"class EvalAttrDep {} -#[Route("/home", -1, 1.5, true, null, EvalAttrDep::class, ["nested", 2])] -#[Tag("first"), Tag("second")] -class EvalAttrMeta {} -$names = class_attribute_names("EvalAttrMeta"); -echo count($names); echo ":"; echo $names[0]; echo ":"; echo $names[1]; echo ":"; echo $names[2]; echo ":"; -$args = class_attribute_args("EvalAttrMeta", "route"); -echo count($args); echo ":"; echo $args[0]; echo ":"; echo $args[1]; echo ":"; -echo $args[2]; echo ":"; echo $args[3] ? "T" : "F"; echo ":"; echo is_null($args[4]) ? "N" : "bad"; echo ":"; -echo $args[5]; echo ":"; -echo count($args[6]); echo ":"; echo $args[6][0]; echo ":"; echo $args[6][1]; echo ":"; -$tag = class_attribute_args("evalattrmeta", "Tag"); -echo $tag[0]; echo ":"; -$missing = class_attribute_args("EvalAttrMeta", "Missing"); -echo count($missing); echo ":"; -$attrs = class_get_attributes("EvalAttrMeta"); -echo count($attrs); echo ":"; echo $attrs[0]->getName(); echo ":"; -$attr_args = $attrs[0]->getArguments(); -echo count($attr_args); echo ":"; echo $attr_args[0]; echo ":"; echo $attr_args[1]; echo ":"; -echo $attr_args[2]; echo ":"; echo $attr_args[3] ? "T" : "F"; echo ":"; echo is_null($attr_args[4]) ? "N" : "bad"; echo ":"; -echo $attr_args[5]; echo ":"; -echo count($attr_args[6]); echo ":"; echo $attr_args[6][0]; echo ":"; echo $attr_args[6][1]; echo ":"; -$tag_attr_args = $attrs[1]->getArguments(); -echo $attrs[1]->getName(); echo ":"; echo $tag_attr_args[0]; echo ":"; -echo is_null($attrs[0]->newInstance()) ? "N" : "bad"; echo ":"; -$call_names = call_user_func("class_attribute_names", "EvalAttrMeta"); -echo $call_names[0]; echo ":"; -$call_args = call_user_func_array( - "class_attribute_args", - ["class_name" => "EvalAttrMeta", "attribute_name" => "Route"] -); -echo $call_args[0]; echo ":"; -echo function_exists("class_attribute_names"); echo function_exists("class_get_attributes"); -echo function_exists("class_attribute_args"); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "3:Route:Tag:Tag:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:first:0:3:Route:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:Tag:first:N:Route:/home:111" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval class attribute metadata preserves named literal arguments. -#[test] -fn execute_program_dispatches_named_class_attribute_args() { - let program = parse_fragment( - br#"#[Route(path: "/eval", secure: true, code: 9)] -class EvalNamedAttrMeta {} -$args = class_attribute_args("EvalNamedAttrMeta", "Route"); -echo count($args); echo ":"; -echo $args["path"]; echo ":"; -echo $args["secure"] ? "T" : "F"; echo ":"; -echo $args["code"]; echo ":"; -$attrs = class_get_attributes("EvalNamedAttrMeta"); -$attr_args = $attrs[0]->getArguments(); -echo $attr_args["path"]; echo ":"; -echo $attr_args["secure"] ? "T" : "F"; echo ":"; -echo $attr_args["code"]; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:/eval:T:9:/eval:T:9"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionAttribute::newInstance instantiates eval-declared attribute classes. -#[test] -fn execute_program_instantiates_eval_declared_reflection_attribute() { - let program = parse_fragment( - br#"class EvalRoute { - public $path; - public $code; - public $enabled; - public function __construct($path, $code, $enabled) { - $this->path = $path; - $this->code = $code; - $this->enabled = $enabled; - } - public function summary() { - return $this->path . ":" . $this->code . ":" . ($this->enabled ? "T" : "F"); - } -} -#[EvalRoute(enabled: true, code: -7, path: "/home")] -class EvalRouteTarget {} -$attrs = class_get_attributes("EvalRouteTarget"); -$instance = $attrs[0]->newInstance(); -echo get_class($instance); echo ":"; echo $instance->summary(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "EvalRoute:/home:-7:T"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass/Method/Property expose eval-declared attribute metadata. -#[test] -fn execute_program_reflects_eval_member_attributes() { - let program = parse_fragment( - br#"class EvalMarker { - public $name; - public function __construct($name) { - $this->name = $name; - } - public function label() { - return $this->name; - } -} -#[EvalMarker("class")] -class EvalReflectTarget { - #[EvalMarker("method")] - public function handle() {} - #[EvalMarker("property")] - public $id; -} -$class_attrs = (new ReflectionClass("EvalReflectTarget"))->getAttributes(); -echo count($class_attrs); echo ":"; echo (new ReflectionClass("EvalReflectTarget"))->getName(); echo ":"; -echo $class_attrs[0]->getName(); echo ":"; echo $class_attrs[0]->newInstance()->label(); echo ":"; -$method_attrs = (new ReflectionMethod("EvalReflectTarget", "handle"))->getAttributes(); -echo count($method_attrs); echo ":"; echo (new ReflectionMethod("EvalReflectTarget", "handle"))->getName(); echo ":"; -echo $method_attrs[0]->getName(); echo ":"; -echo $method_attrs[0]->getArguments()[0]; echo ":"; echo $method_attrs[0]->newInstance()->label(); echo ":"; -$property_attrs = (new ReflectionProperty("EvalReflectTarget", "id"))->getAttributes(); -echo count($property_attrs); echo ":"; echo (new ReflectionProperty("EvalReflectTarget", "id"))->getName(); echo ":"; -echo $property_attrs[0]->getName(); echo ":"; -echo $property_attrs[0]->getArguments()[0]; echo ":"; echo $property_attrs[0]->newInstance()->label(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:EvalReflectTarget:EvalMarker:class:1:handle:EvalMarker:method:method:1:id:EvalMarker:property:property" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionAttribute reports target bitmasks and repeated-owner metadata. -#[test] -fn execute_program_reflection_attribute_reports_target_and_repetition() { - let program = parse_fragment( - br#"class EvalTargetMarker { - public function __construct($name = null) {} -} -#[EvalTargetMarker("class-a"), EvalTargetMarker("class-b")] -class EvalReflectAttributeTarget { - #[EvalTargetMarker("method")] - public function run(#[EvalTargetMarker("param")] $id) {} - #[EvalTargetMarker("property")] - public $id; - #[EvalTargetMarker("const")] - public const ANSWER = 42; -} -enum EvalReflectAttributeEnum { - #[EvalTargetMarker("case")] - case Ready; -} -$class_attrs = (new ReflectionClass("EvalReflectAttributeTarget"))->getAttributes(); -echo $class_attrs[0]->getTarget(); echo "/"; -echo $class_attrs[0]->isRepeated() ? "R" : "r"; echo ":"; -echo $class_attrs[1]->getTarget(); echo "/"; -echo $class_attrs[1]->isRepeated() ? "R" : "r"; echo ":"; -$method_attr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getAttributes()[0]; -echo $method_attr->getTarget(); echo "/"; -echo $method_attr->isRepeated() ? "R" : "r"; echo ":"; -$property_attr = (new ReflectionProperty("EvalReflectAttributeTarget", "id"))->getAttributes()[0]; -echo $property_attr->getTarget(); echo "/"; -echo $property_attr->isRepeated() ? "R" : "r"; echo ":"; -$param_attr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getParameters()[0]->getAttributes()[0]; -echo $param_attr->getTarget(); echo "/"; -echo $param_attr->isRepeated() ? "R" : "r"; echo ":"; -$const_attr = (new ReflectionClassConstant("EvalReflectAttributeTarget", "ANSWER"))->getAttributes()[0]; -echo $const_attr->getTarget(); echo "/"; -echo $const_attr->isRepeated() ? "R" : "r"; echo ":"; -$case_attr = (new ReflectionEnumUnitCase("EvalReflectAttributeEnum", "Ready"))->getAttributes()[0]; -echo $case_attr->getTarget(); echo "/"; -echo $case_attr->isRepeated() ? "R" : "r"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1/R:1/R:4/r:8/r:32/r:16/r:16/r"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies reflection owner origin metadata APIs report eval user-defined defaults. -#[test] -fn execute_program_reflection_owners_report_origin_metadata_defaults() { - let program = parse_fragment( - br#"class EvalReflectOriginTarget { - public $id; - public const ANSWER = 42; - public function run() {} -} -enum EvalReflectOriginCase: string { - case Ready = "ready"; -} -$class = new ReflectionClass("EvalReflectOriginTarget"); -$method = new ReflectionMethod("EvalReflectOriginTarget", "run"); -$property = new ReflectionProperty("EvalReflectOriginTarget", "id"); -$constant = new ReflectionClassConstant("EvalReflectOriginTarget", "ANSWER"); -$unit = new ReflectionEnumUnitCase("EvalReflectOriginCase", "Ready"); -$backed = new ReflectionEnumBackedCase("EvalReflectOriginCase", "Ready"); -echo ($class->getDocComment() === false) ? "C" : "c"; echo ":"; -echo ($method->getDocComment() === false) ? "M" : "m"; echo ":"; -echo ($property->getDocComment() === false) ? "P" : "p"; echo ":"; -echo ($constant->getDocComment() === false) ? "K" : "k"; echo ":"; -echo ($unit->getDocComment() === false) ? "U" : "u"; echo ":"; -echo ($backed->getDocComment() === false) ? "B" : "b"; echo ":"; -echo ($class->getExtensionName() === false) ? "E" : "e"; echo ":"; -echo ($method->getExtensionName() === false) ? "N" : "n"; echo ":"; -echo ($class->getExtension() === null) ? "X" : "x"; echo ":"; -echo ($method->getExtension() === null) ? "Y" : "y"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "C:M:P:K:U:B:E:N:X:Y"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass and ReflectionMethod report eval source-location metadata. -#[test] -fn execute_program_reflection_class_and_method_report_source_location() { - let program = parse_fragment( - br#"class EvalReflectSource { - public function run() { - return 1; - } -} -interface EvalReflectSourceIface { - public function iface(); -} -$class = new ReflectionClass("EvalReflectSource"); -$method = new ReflectionMethod("EvalReflectSource", "run"); -$iface = new ReflectionClass("EvalReflectSourceIface"); -$ifaceMethod = new ReflectionMethod("EvalReflectSourceIface", "iface"); -echo $class->getFileName(); echo ":"; -echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; -echo $method->getStartLine(); echo ":"; echo $method->getEndLine(); echo ":"; -echo $iface->getStartLine(); echo ":"; echo $iface->getEndLine(); echo ":"; -echo $ifaceMethod->getStartLine(); echo ":"; echo $ifaceMethod->getEndLine(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - context.set_call_site("/tmp/eval-class-source.php", "/tmp", 23); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!( - values.output, - "/tmp/eval-class-source.php(23) : eval()'d code:1:5:2:4:6:8:7:7" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod exposes PHP-compatible name and origin predicate metadata. -#[test] -fn execute_program_reflection_method_reports_name_and_origin_predicates() { - let program = parse_fragment( - br#"namespace EvalReflectMethodNs; -class Target { - public function run(...$items) {} - public static function stat() {} -} -$ref = new \ReflectionMethod(Target::class, "run"); -echo $ref->getShortName(); echo ":"; -echo $ref->getNamespaceName(); echo ":"; -echo $ref->inNamespace() ? "Y" : "N"; echo ":"; -echo $ref->isInternal() ? "I" : "i"; -echo $ref->isUserDefined() ? "U" : "u"; echo ":"; -echo $ref->isClosure() ? "C" : "c"; echo ":"; -echo $ref->isDeprecated() ? "D" : "d"; echo ":"; -echo $ref->isStatic() ? "S" : "s"; echo ":"; -echo $ref->returnsReference() ? "R" : "r"; echo ":"; -echo $ref->hasReturnType() ? "T" : "t"; echo ":"; -echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; -echo $ref->isGenerator() ? "G" : "g"; echo ":"; -echo $ref->isVariadic() ? "V" : "v"; echo ":"; -echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; -echo $ref->getTentativeReturnType() === null ? "Q" : "q"; echo ":"; -echo count($ref->getClosureUsedVariables()); echo ":"; -echo $ref->getClosureThis() === null ? "T" : "t"; echo ":"; -echo $ref->getClosureScopeClass() === null ? "S" : "s"; echo ":"; -echo $ref->getClosureCalledClass() === null ? "L" : "l"; echo ":"; -$static = new \ReflectionMethod(Target::class, "stat"); -echo $static->isStatic() ? "S" : "s"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "run::N:iU:c:d:s:r:t:N:g:V:h:Q:0:T:S:L:S"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod derives `isDeprecated()` from eval-retained attributes. -#[test] -fn execute_program_reflection_method_reports_deprecated_attribute() { - let program = parse_fragment( - br#"class EvalReflectDeprecatedMethodTarget { - #[\Deprecated] - public function old() {} - public function fresh() {} -} -$deprecated = new ReflectionMethod(EvalReflectDeprecatedMethodTarget::class, "old"); -$plain = new ReflectionMethod(EvalReflectDeprecatedMethodTarget::class, "fresh"); -echo $deprecated->isDeprecated() ? "D" : "d"; echo ":"; -echo $plain->isDeprecated() ? "D" : "d"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "D:d"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod exposes eval static locals using the declaring class key. -#[test] -fn execute_program_reflection_method_reports_static_variables() { - let program = parse_fragment( - br#"class EvalReflectMethodStaticBase { - public function tick() { - static $count = 3; - static $label = "method"; - $count = $count + 1; - return $count; - } -} -class EvalReflectMethodStaticChild extends EvalReflectMethodStaticBase {} -$object = new EvalReflectMethodStaticChild(); -$ref = new ReflectionMethod("EvalReflectMethodStaticChild", "tick"); -$before = $ref->getStaticVariables(); -echo $before["count"]; echo ":"; echo $before["label"]; echo ":"; -echo $ref->invoke($object); echo ":"; -$after = $ref->getStaticVariables(); -echo $after["count"]; echo ":"; echo $after["label"]; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:method:4:4:method"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod exposes eval parent and interface prototypes. -#[test] -fn execute_program_reflection_method_reports_eval_prototypes() { - let program = parse_fragment( - br#"interface EvalProtoParentIface { - public function parented(); -} -interface EvalProtoChildIface extends EvalProtoParentIface {} -interface EvalProtoIface { - public function iface(); -} -class EvalProtoBase { - public function run() {} - public function inherited() {} -} -class EvalProtoChild extends EvalProtoBase implements EvalProtoIface, EvalProtoChildIface { - public function run() {} - public function iface() {} - public function parented() {} - public function own() {} -} -$override = new ReflectionMethod("EvalProtoChild", "run"); -$overrideProto = $override->getPrototype(); -echo $override->hasPrototype() ? "Y" : "N"; echo ":"; -echo $overrideProto->getDeclaringClass()->getName(); echo "::"; -echo $overrideProto->getName(); echo ":"; -$iface = new ReflectionMethod("EvalProtoChild", "iface"); -$ifaceProto = $iface->getPrototype(); -echo $iface->hasPrototype() ? "Y" : "N"; echo ":"; -echo $ifaceProto->getDeclaringClass()->getName(); echo "::"; -echo $ifaceProto->getName(); echo ":"; -$parentIface = new ReflectionMethod("EvalProtoChild", "parented"); -$parentIfaceProto = $parentIface->getPrototype(); -echo $parentIfaceProto->getDeclaringClass()->getName(); echo "::"; -echo $parentIfaceProto->getName(); echo ":"; -$own = new ReflectionMethod("EvalProtoChild", "own"); -echo $own->hasPrototype() ? "Y" : "N"; echo ":"; -try { - $own->getPrototype(); -} catch (ReflectionException $e) { - echo "E"; -} -echo ":"; -$inherited = new ReflectionMethod("EvalProtoChild", "inherited"); -echo $inherited->hasPrototype() ? "Y" : "N"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Y:EvalProtoBase::run:Y:EvalProtoIface::iface:EvalProtoParentIface::parented:N:E:N" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass exposes eval class namespace-derived name parts. -#[test] -fn execute_program_reflects_eval_class_name_parts() { - let program = parse_fragment( - br#"namespace Eval\Ns; -class Thing {} -$ref = new \ReflectionClass("Eval\\Ns\\Thing"); -echo $ref->getName(); echo ":"; -echo $ref->getShortName(); echo ":"; -echo $ref->getNamespaceName(); echo ":"; -echo $ref->inNamespace() ? "Y" : "N"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Eval\\Ns\\Thing:Thing:Eval\\Ns:Y"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass exposes eval interface and trait relation names. -#[test] -fn execute_program_reflects_eval_class_relation_names() { - let program = parse_fragment( - br#"interface EvalRelationIface {} -trait EvalRelationTrait { - public function primary() {} -} -trait EvalRelationOtherTrait { - public function other() {} -} -class EvalRelationTarget implements EvalRelationIface { - use EvalRelationTrait, EvalRelationOtherTrait { - EvalRelationTrait::primary as relationAlias; - EvalRelationOtherTrait::other as private hiddenOther; - EvalRelationOtherTrait::other as protected; - } -} -class EvalRelationInherited extends EvalRelationTarget {} -interface EvalRelationParent {} -interface EvalRelationChild extends EvalRelationParent {} -$ref = new ReflectionClass("EvalRelationTarget"); -$interfaces = $ref->getInterfaceNames(); -$traits = $ref->getTraitNames(); -echo count($interfaces); echo ":"; echo $interfaces[0]; echo ":"; -echo count($traits); echo ":"; echo $traits[0]; echo ":"; echo $traits[1]; echo ":"; -$parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); -echo count($parentInterfaces); echo ":"; echo $parentInterfaces[0]; -$interfaceObjects = $ref->getInterfaces(); -echo ":"; echo count($interfaceObjects); echo ":"; echo $interfaceObjects["EvalRelationIface"]->getName(); -$traitObjects = $ref->getTraits(); -echo ":"; echo count($traitObjects); echo ":"; echo $traitObjects["EvalRelationTrait"]->getName(); echo ":"; echo $traitObjects["EvalRelationOtherTrait"]->getName(); -$parentInterfaceObjects = (new ReflectionClass("EvalRelationChild"))->getInterfaces(); -echo ":"; echo count($parentInterfaceObjects); echo ":"; echo $parentInterfaceObjects["EvalRelationParent"]->getName(); -$aliases = $ref->getTraitAliases(); -echo ":"; echo count($aliases); echo ":"; echo $aliases["relationAlias"]; echo ":"; echo $aliases["hiddenOther"]; -$inheritedAliases = (new ReflectionClass("EvalRelationInherited"))->getTraitAliases(); -echo ":"; echo count($inheritedAliases); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:2:EvalRelationTrait::primary:EvalRelationOtherTrait::other:0" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass relation-name helpers read fake generated/AOT metadata. -#[test] -fn execute_program_reflects_aot_class_relation_names() { - let program = parse_fragment( - br#"$class_names = (new ReflectionClass("KnownClass"))->getInterfaceNames(); -echo count($class_names); echo ":"; echo $class_names[0]; echo ":"; -$interface_names = (new ReflectionClass("KnownInterface"))->getInterfaceNames(); -echo count($interface_names); echo ":"; echo $interface_names[0]; echo ":"; -$class_objects = (new ReflectionClass("KnownClass"))->getInterfaces(); -echo count($class_objects); echo ":"; echo $class_objects["KnownInterface"]->getName(); echo ":"; -$interface_objects = (new ReflectionClass("KnownInterface"))->getInterfaces(); -echo count($interface_objects); echo ":"; echo $interface_objects["Traversable"]->getName(); echo ":"; -$trait_names = (new ReflectionClass("KnownClass"))->getTraitNames(); -echo count($trait_names); echo ":"; echo $trait_names[0]; echo ":"; -$trait_objects = (new ReflectionClass("KnownClass"))->getTraits(); -echo count($trait_objects); echo ":"; echo $trait_objects["KnownTrait"]->getName(); echo ":"; -$nested_trait_names = (new ReflectionClass("KnownTrait"))->getTraitNames(); -echo count($nested_trait_names); echo ":"; echo $nested_trait_names[0]; echo ":"; -$aliases = (new ReflectionClass("KnownClass"))->getTraitAliases(); -echo count($aliases); echo ":"; echo $aliases["knownAlias"]; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:KnownInterface:1:Traversable:1:KnownInterface:1:Traversable:1:KnownTrait:1:KnownTrait:1:KnownInnerTrait:1:KnownTrait::source" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::implementsInterface reports eval class, enum, and -/// interface metadata using case-insensitive interface names. -#[test] -fn execute_program_reflects_eval_class_implements_interface_predicate() { - let program = parse_fragment( - br#"interface EvalImplBase {} -interface EvalImplChild extends EvalImplBase {} -class EvalImplTarget implements EvalImplChild {} -enum EvalImplEnum implements EvalImplBase { case Ready; } -trait EvalImplTrait {} -echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("EvalImplChild") ? "C" : "c"; -echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("evalimplbase") ? "B" : "b"; -echo (new ReflectionClass("EvalImplEnum"))->implementsInterface("EvalImplBase") ? "E" : "e"; -echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplChild") ? "I" : "i"; -echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplBase") ? "P" : "p"; -echo (new ReflectionClass("EvalImplTrait"))->implementsInterface("EvalImplBase") ? "T" : "t"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "CBEIPt"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::implementsInterface checks fake generated/AOT relations. -#[test] -fn execute_program_reflects_aot_class_implements_interface_predicate() { - let program = parse_fragment( - br#"$ref = new ReflectionClass("KnownClass"); -echo $ref->implementsInterface("KnownInterface") ? "Y" : "N"; echo ":"; -echo $ref->implementsInterface("Iterator") ? "bad" : "N"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:N"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::implementsInterface rejects non-interface names with catchable errors. -#[test] -fn execute_program_reflection_class_implements_interface_rejects_non_interfaces() { - let program = parse_fragment( - br#"interface EvalImplRejectIface {} -class EvalImplRejectTarget {} -class EvalImplRejectClass {} -trait EvalImplRejectTrait {} -enum EvalImplRejectEnum { case Ready; } -$ref = new ReflectionClass("EvalImplRejectTarget"); -echo $ref->implementsInterface("EvalImplRejectIface") ? "T" : "F"; -try { - $ref->implementsInterface("EvalImplRejectClass"); - echo ":bad"; -} catch (ReflectionException $e) { - echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); -} -try { - $ref->implementsInterface("EvalImplRejectTrait"); - echo ":bad"; -} catch (ReflectionException $e) { - echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); -} -try { - $ref->implementsInterface("EvalImplRejectEnum"); - echo ":bad"; -} catch (ReflectionException $e) { - echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); -} -try { - $ref->implementsInterface("EvalImplRejectMissing"); - echo ":bad"; -} catch (ReflectionException $e) { - echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "F:ReflectionException:EvalImplRejectClass is not an interface:ReflectionException:EvalImplRejectTrait is not an interface:ReflectionException:EvalImplRejectEnum is not an interface:ReflectionException:Interface \"EvalImplRejectMissing\" does not exist" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::isSubclassOf reports eval parent/interface metadata. -#[test] -fn execute_program_reflection_class_is_subclass_of_predicate() { - let program = parse_fragment( - br#"interface EvalSubclassIface {} -interface EvalSubclassChildIface extends EvalSubclassIface {} -class EvalSubclassBase {} -class EvalSubclassParent extends EvalSubclassBase {} -class EvalSubclassChild extends EvalSubclassParent implements EvalSubclassChildIface {} -trait EvalSubclassTrait {} -enum EvalSubclassEnum implements EvalSubclassIface { case Ready; } -$ref = new ReflectionClass("EvalSubclassChild"); -echo $ref->isSubclassOf("EvalSubclassParent") ? "P" : "p"; -echo $ref->isSubclassOf("evalsubclassbase") ? "B" : "b"; -echo $ref->isSubclassOf("EvalSubclassIface") ? "I" : "i"; -echo $ref->isSubclassOf("EvalSubclassChild") ? "S" : "s"; -echo (new ReflectionClass("EvalSubclassChildIface"))->isSubclassOf("EvalSubclassIface") ? "J" : "j"; -echo (new ReflectionClass("EvalSubclassIface"))->isSubclassOf("EvalSubclassIface") ? "X" : "x"; -echo $ref->isSubclassOf("EvalSubclassTrait") ? "T" : "t"; -echo $ref->isSubclassOf("EvalSubclassEnum") ? "Q" : "q"; -echo (new ReflectionClass("EvalSubclassEnum"))->isSubclassOf("EvalSubclassIface") ? "E" : "e"; -try { - $ref->isSubclassOf("EvalSubclassMissing"); - echo ":bad"; -} catch (ReflectionException $e) { - echo ":missing"; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "PBIsJxtqE:missing"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::isInstance reports eval object class/interface metadata. -#[test] -fn execute_program_reflection_class_is_instance_predicate() { - let program = parse_fragment( - br#"interface EvalInstanceIface {} -class EvalInstanceBase {} -class EvalInstanceChild extends EvalInstanceBase implements EvalInstanceIface {} -trait EvalInstanceTrait {} -enum EvalInstanceEnum implements EvalInstanceIface { case Ready; } -$base = new ReflectionClass("EvalInstanceBase"); -$child = new ReflectionClass("EvalInstanceChild"); -$iface = new ReflectionClass("EvalInstanceIface"); -$trait = new ReflectionClass("EvalInstanceTrait"); -$enum = new ReflectionClass("EvalInstanceEnum"); -$childObj = new EvalInstanceChild(); -$objectRef = new ReflectionClass($childObj); -echo $objectRef->getName(); echo ":"; -echo $objectRef->getParentClass()->getName(); echo ":"; -echo $objectRef->isInstance($childObj) ? "O" : "o"; echo ":"; -echo $base->isInstance($childObj) ? "B" : "b"; -echo $child->isInstance(new EvalInstanceBase()) ? "C" : "c"; -echo $iface->isInstance($childObj) ? "I" : "i"; -echo $trait->isInstance($childObj) ? "T" : "t"; -echo $enum->isInstance(EvalInstanceEnum::Ready) ? "E" : "e"; -echo $iface->isInstance(EvalInstanceEnum::Ready) ? "N" : "n"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "EvalInstanceChild:EvalInstanceBase:O:BcItEN"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass exposes eval class-like final and abstract flags. -#[test] -fn execute_program_reflects_eval_class_modifier_flags() { - let program = parse_fragment( - br#"abstract class EvalAbstractReflect {} -final class EvalFinalReflect {} -interface EvalIfaceReflect {} -trait EvalTraitReflect {} -enum EvalEnumReflect { case Ready; } -echo (new ReflectionClass("EvalAbstractReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; -echo (new ReflectionClass("EvalAbstractReflect"))->isInterface() ? "I" : "i"; -echo (new ReflectionClass("EvalAbstractReflect"))->isTrait() ? "T" : "t"; -echo (new ReflectionClass("EvalAbstractReflect"))->isEnum() ? "E" : "e"; echo ":"; -echo (new ReflectionClass("EvalFinalReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; -echo (new ReflectionClass("EvalFinalReflect"))->isInterface() ? "I" : "i"; -echo (new ReflectionClass("EvalFinalReflect"))->isTrait() ? "T" : "t"; -echo (new ReflectionClass("EvalFinalReflect"))->isEnum() ? "E" : "e"; echo ":"; -echo (new ReflectionClass("EvalEnumReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; -echo (new ReflectionClass("EvalEnumReflect"))->isInterface() ? "I" : "i"; -echo (new ReflectionClass("EvalEnumReflect"))->isTrait() ? "T" : "t"; -echo (new ReflectionClass("EvalEnumReflect"))->isEnum() ? "E" : "e"; echo ":"; -echo (new ReflectionClass("EvalIfaceReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; -echo (new ReflectionClass("EvalIfaceReflect"))->isInterface() ? "I" : "i"; -echo (new ReflectionClass("EvalIfaceReflect"))->isTrait() ? "T" : "t"; -echo (new ReflectionClass("EvalIfaceReflect"))->isEnum() ? "E" : "e"; echo ":"; -echo (new ReflectionClass("EvalTraitReflect"))->isAbstract() ? "A" : "a"; -echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f"; -echo (new ReflectionClass("EvalTraitReflect"))->isInterface() ? "I" : "i"; -echo (new ReflectionClass("EvalTraitReflect"))->isTrait() ? "T" : "t"; -echo (new ReflectionClass("EvalTraitReflect"))->isEnum() ? "E" : "e"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Afite:aFite:aFitE:afIte:afiTe"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass exposes PHP modifier bitmasks for eval class-like metadata. -#[test] -fn execute_program_reflects_eval_class_modifier_bitmask() { - let program = parse_fragment( - br#"abstract class EvalModifierAbstract {} -final class EvalModifierFinal {} -readonly class EvalModifierReadonly {} -final readonly class EvalModifierFinalReadonly {} -enum EvalModifierEnum { case Ready; } -interface EvalModifierIface {} -trait EvalModifierTrait {} -echo (new ReflectionClass("EvalModifierAbstract"))->getModifiers(); echo ":"; -echo (new ReflectionClass("EvalModifierFinal"))->getModifiers(); echo ":"; -echo (new ReflectionClass("EvalModifierReadonly"))->getModifiers(); echo ":"; -echo (new ReflectionClass("EvalModifierFinalReadonly"))->getModifiers(); echo ":"; -echo (new ReflectionClass("EvalModifierEnum"))->getModifiers(); echo ":"; -echo (new ReflectionClass("EvalModifierIface"))->getModifiers(); echo ":"; -echo (new ReflectionClass("EvalModifierTrait"))->getModifiers(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "64:32:65536:65568:32:0:0"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval can read built-in Reflection `IS_*` class constants. -#[test] -fn execute_program_reads_builtin_reflection_modifier_constants() { - let program = parse_fragment( - br#"echo ReflectionClass::IS_FINAL; echo ":"; -echo ReflectionClass::IS_EXPLICIT_ABSTRACT; echo ":"; -echo ReflectionClass::IS_READONLY; echo ":"; -echo ReflectionMethod::IS_STATIC; echo ":"; -echo ReflectionMethod::IS_PRIVATE; echo ":"; -echo ReflectionMethod::IS_ABSTRACT; echo ":"; -echo ReflectionProperty::IS_STATIC; echo ":"; -echo ReflectionProperty::IS_READONLY; echo ":"; -echo ReflectionProperty::IS_PUBLIC; echo ":"; -echo ReflectionProperty::IS_PROTECTED; echo ":"; -echo ReflectionProperty::IS_PRIVATE; echo ":"; -echo ReflectionProperty::IS_ABSTRACT; echo ":"; -echo ReflectionProperty::IS_PROTECTED_SET; echo ":"; -echo ReflectionProperty::IS_PRIVATE_SET; echo ":"; -echo ReflectionProperty::IS_VIRTUAL; echo ":"; -echo ReflectionProperty::IS_FINAL; echo ":"; -echo ReflectionClassConstant::IS_PUBLIC; echo ":"; -echo ReflectionClassConstant::IS_PROTECTED; echo ":"; -echo ReflectionClassConstant::IS_PRIVATE; echo ":"; -echo ReflectionClassConstant::IS_FINAL; echo ":"; -echo ReflectionEnumUnitCase::IS_PUBLIC; echo ":"; -echo ReflectionEnumUnitCase::IS_PROTECTED; echo ":"; -echo ReflectionEnumUnitCase::IS_PRIVATE; echo ":"; -echo ReflectionEnumUnitCase::IS_FINAL; echo ":"; -echo ReflectionEnumBackedCase::IS_PUBLIC; echo ":"; -echo ReflectionEnumBackedCase::IS_PROTECTED; echo ":"; -echo ReflectionEnumBackedCase::IS_PRIVATE; echo ":"; -echo ReflectionEnumBackedCase::IS_FINAL; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "32:64:65536:16:4:64:16:128:1:2:4:64:2048:4096:512:32:1:2:4:32:1:2:4:32:1:2:4:32" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass exposes eval readonly class metadata. -#[test] -fn execute_program_reflects_eval_class_readonly_predicate() { - let program = parse_fragment( - br#"class EvalReadonlyPlain {} -readonly class EvalReadonlyReflect {} -final readonly class EvalReadonlyFinalReflect {} -enum EvalReadonlyEnumReflect { case Ready; } -interface EvalReadonlyIface {} -trait EvalReadonlyTrait {} -echo (new ReflectionClass("EvalReadonlyPlain"))->isReadOnly() ? "R" : "r"; -echo (new ReflectionClass("EvalReadonlyReflect"))->isReadOnly() ? "R" : "r"; -echo (new ReflectionClass("EvalReadonlyFinalReflect"))->isReadOnly() ? "R" : "r"; -echo (new ReflectionClass("EvalReadonlyEnumReflect"))->isReadOnly() ? "R" : "r"; -echo (new ReflectionClass("EvalReadonlyIface"))->isReadOnly() ? "R" : "r"; -echo (new ReflectionClass("EvalReadonlyTrait"))->isReadOnly() ? "R" : "r"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "rRRrrr"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass exposes eval class instantiability metadata. -#[test] -fn execute_program_reflects_eval_class_instantiable_predicate() { - let program = parse_fragment( - br#"abstract class EvalInstAbstract {} -class EvalInstPublic {} -final class EvalInstFinal {} -class EvalInstPrivate { private function __construct() {} } -class EvalInstProtected { protected function __construct() {} } -interface EvalInstIface {} -trait EvalInstTrait {} -enum EvalInstEnum { case Ready; } -echo (new ReflectionClass("EvalInstAbstract"))->isInstantiable() ? "A" : "a"; -echo (new ReflectionClass("EvalInstPublic"))->isInstantiable() ? "B" : "b"; -echo (new ReflectionClass("EvalInstFinal"))->isInstantiable() ? "C" : "c"; -echo (new ReflectionClass("EvalInstPrivate"))->isInstantiable() ? "P" : "p"; -echo (new ReflectionClass("EvalInstProtected"))->isInstantiable() ? "R" : "r"; -echo (new ReflectionClass("EvalInstIface"))->isInstantiable() ? "I" : "i"; -echo (new ReflectionClass("EvalInstTrait"))->isInstantiable() ? "T" : "t"; -echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "aBCprite"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::isAnonymous reports false for eval-declared named class-like symbols. -#[test] -fn execute_program_reflection_class_reports_named_classes_not_anonymous() { - let program = parse_fragment( - br#"class EvalNamedAnonymousReflect {} -interface EvalNamedAnonymousIface {} -trait EvalNamedAnonymousTrait {} -enum EvalNamedAnonymousEnum { case Ready; } -echo (new ReflectionClass("EvalNamedAnonymousReflect"))->isAnonymous() ? "C" : "c"; -echo (new ReflectionClass("EvalNamedAnonymousIface"))->isAnonymous() ? "I" : "i"; -echo (new ReflectionClass("EvalNamedAnonymousTrait"))->isAnonymous() ? "T" : "t"; -echo (new ReflectionClass("EvalNamedAnonymousEnum"))->isAnonymous() ? "E" : "e"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "cite"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::isCloneable reports eval class clone metadata. -#[test] -fn execute_program_reflects_eval_class_cloneable_predicate() { - let program = parse_fragment( - br#"abstract class EvalCloneAbstract {} -class EvalClonePlain {} -final class EvalCloneFinal {} -class EvalClonePrivate { private function __clone() {} } -class EvalCloneProtected { protected function __clone() {} } -class EvalClonePublic { public function __clone() {} } -interface EvalCloneIface {} -trait EvalCloneTrait {} -enum EvalCloneEnum { case Ready; } -echo (new ReflectionClass("EvalCloneAbstract"))->isCloneable() ? "A" : "a"; -echo (new ReflectionClass("EvalClonePlain"))->isCloneable() ? "P" : "p"; -echo (new ReflectionClass("EvalCloneFinal"))->isCloneable() ? "F" : "f"; -echo (new ReflectionClass("EvalClonePrivate"))->isCloneable() ? "V" : "v"; -echo (new ReflectionClass("EvalCloneProtected"))->isCloneable() ? "R" : "r"; -echo (new ReflectionClass("EvalClonePublic"))->isCloneable() ? "U" : "u"; -echo (new ReflectionClass("EvalCloneIface"))->isCloneable() ? "I" : "i"; -echo (new ReflectionClass("EvalCloneTrait"))->isCloneable() ? "T" : "t"; -echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "aPFvrUite"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::isIterable reports eval Traversable-compatible class metadata. -#[test] -fn execute_program_reflects_eval_class_iterable_predicate() { - let program = parse_fragment( - br#"class EvalIterablePlain {} -abstract class EvalIterableAbstract implements Iterator {} -interface EvalIterableIface extends Iterator {} -trait EvalIterableTrait {} -enum EvalIterableEnum { case Ready; } -class EvalIterableIterator implements Iterator { - public function current() { return null; } - public function key() { return null; } - public function next() {} - public function valid() { return false; } - public function rewind() {} -} -class EvalIterableAggregate implements IteratorAggregate { - public function getIterator() { return $this; } -} -echo (new ReflectionClass("EvalIterablePlain"))->isIterable() ? "P" : "p"; -$iter = new ReflectionClass("EvalIterableIterator"); -echo $iter->isIterable() ? "I" : "i"; -echo $iter->isIterateable() ? "A" : "a"; -echo (new ReflectionClass("EvalIterableAggregate"))->isIterable() ? "G" : "g"; -echo (new ReflectionClass("EvalIterableAbstract"))->isIterable() ? "B" : "b"; -echo (new ReflectionClass("EvalIterableIface"))->isIterable() ? "F" : "f"; -echo (new ReflectionClass("EvalIterableEnum"))->isIterable() ? "E" : "e"; -echo (new ReflectionClass("EvalIterableTrait"))->isIterable() ? "H" : "h"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "pIAGbfeh"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass origin predicates report eval class-like symbols as user-defined. -#[test] -fn execute_program_reflects_eval_class_origin_predicates() { - let program = parse_fragment( - br#"class EvalOriginClass {} -interface EvalOriginIface {} -trait EvalOriginTrait {} -enum EvalOriginEnum { case Ready; } -function eval_reflect_origin($name) { - $r = new ReflectionClass($name); - echo $r->isInternal() ? "I" : "i"; - echo $r->isUserDefined() ? "U" : "u"; - echo ":"; -} -eval_reflect_origin("EvalOriginClass"); -eval_reflect_origin("EvalOriginIface"); -eval_reflect_origin("EvalOriginTrait"); -eval_reflect_origin("EvalOriginEnum"); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "iU:iU:iU:iU:"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::getConstructor exposes eval constructor metadata. -#[test] -fn execute_program_reflection_class_get_constructor() { - let program = parse_fragment( - br#"class EvalCtorBase { - public function __construct($required, $optional = 2) {} -} -class EvalCtorChild extends EvalCtorBase {} -class EvalCtorPlain {} -interface EvalCtorInterface { - public function __construct($required); -} -trait EvalCtorTrait { - public function __construct($required, $optional = null, ...$rest) {} -} -$base = (new ReflectionClass("EvalCtorBase"))->getConstructor(); -echo $base->getName(); echo "/"; -echo $base->getNumberOfParameters(); echo "/"; -echo $base->getNumberOfRequiredParameters(); echo ":"; -$child = (new ReflectionClass("EvalCtorChild"))->getConstructor(); -echo $child->getName(); echo "/"; -echo $child->getNumberOfParameters(); echo "/"; -echo $child->getNumberOfRequiredParameters(); echo ":"; -$plain = (new ReflectionClass("EvalCtorPlain"))->getConstructor(); -echo $plain === null ? "null" : "bad"; echo ":"; -$interface = (new ReflectionClass("EvalCtorInterface"))->getConstructor(); -echo $interface->getName(); echo "/"; -echo $interface->getNumberOfParameters(); echo "/"; -echo $interface->getNumberOfRequiredParameters(); echo ":"; -$trait = (new ReflectionClass("EvalCtorTrait"))->getConstructor(); -echo $trait->getName(); echo "/"; -echo $trait->getNumberOfParameters(); echo "/"; -echo $trait->getNumberOfRequiredParameters(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/3/1" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass reports eval class-like method, property, and constant membership. -#[test] -fn execute_program_reflects_eval_class_member_existence() { - let program = parse_fragment( - br#"class EvalMemberParent { - const PARENT_CONST = 1; - private function hiddenParent() {} - protected static function parentStatic() {} - private $hiddenProp; - protected static $parentStaticProp; -} -interface EvalMemberClassIface { - const CLASS_LIMIT = 10; -} -class EvalMemberChild extends EvalMemberParent implements EvalMemberClassIface { - const CHILD_CONST = 2; - public function ChildMethod() {} - public $childProp; -} -interface EvalMemberIfaceParent { - const PARENT_LIMIT = 10; - public function parentRequirement(); -} -interface EvalMemberIface extends EvalMemberIfaceParent { - const CHILD_LIMIT = 20; - public function childRequirement(); - public string $hook { get; } -} -trait EvalMemberTrait { - const TRAIT_CONST = 30; - private function traitHidden() {} - public $traitProp; -} -enum EvalMemberPureEnum { - case Ready; - const LEVEL = 40; - public function label() { return "ok"; } -} -enum EvalMemberBackedEnum: string { - case Ready = "ready"; -} -$child = new ReflectionClass("EvalMemberChild"); -echo $child->hasMethod("childmethod") ? "M" : "m"; -echo $child->hasMethod("HIDDENPARENT") ? "P" : "p"; -echo $child->hasMethod("parentStatic") ? "S" : "s"; -echo $child->hasMethod("missing") ? "X" : "x"; -echo ":"; -echo $child->hasProperty("childProp") ? "C" : "c"; -echo $child->hasProperty("hiddenProp") ? "H" : "h"; -echo $child->hasProperty("parentStaticProp") ? "T" : "t"; -echo $child->hasProperty("childprop") ? "W" : "w"; -echo $child->hasConstant("CHILD_CONST") ? "D" : "d"; -echo $child->hasConstant("PARENT_CONST") ? "P" : "p"; -echo $child->hasConstant("CLASS_LIMIT") ? "A" : "a"; -echo $child->hasConstant("child_const") ? "Z" : "z"; -echo ":"; -$iface = new ReflectionClass("EvalMemberIface"); -echo $iface->hasMethod("parentrequirement") ? "I" : "i"; -echo $iface->hasMethod("childRequirement") ? "J" : "j"; -echo $iface->hasProperty("hook") ? "K" : "k"; -echo $iface->hasConstant("PARENT_LIMIT") ? "L" : "l"; -echo $iface->hasConstant("CHILD_LIMIT") ? "C" : "c"; -echo ":"; -$trait = new ReflectionClass("EvalMemberTrait"); -echo $trait->hasMethod("traithidden") ? "R" : "r"; -echo $trait->hasProperty("traitProp") ? "U" : "u"; -echo $trait->hasConstant("TRAIT_CONST") ? "K" : "k"; -echo ":"; -$pure = new ReflectionClass("EvalMemberPureEnum"); -echo $pure->hasMethod("cases") ? "E" : "e"; -echo $pure->hasMethod("label") ? "L" : "l"; -echo $pure->hasProperty("name") ? "N" : "n"; -echo $pure->hasProperty("value") ? "V" : "v"; -echo $pure->hasConstant("Ready") ? "G" : "g"; -echo $pure->hasConstant("LEVEL") ? "F" : "f"; -echo $pure->hasConstant("ready") ? "R" : "r"; -echo ":"; -$backed = new ReflectionClass("EvalMemberBackedEnum"); -echo $backed->hasMethod("tryfrom") ? "B" : "b"; -echo $backed->hasProperty("value") ? "Y" : "y"; -echo $backed->hasConstant("Ready") ? "Q" : "q"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BYQ"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass returns eval class-like constant values and enum cases. -#[test] -fn execute_program_reflects_eval_class_constant_values() { - let program = parse_fragment( - br#"class EvalReflectConstBase { - public const BASE = 1; -} -interface EvalReflectConstIface { - public const LIMIT = 2; -} -trait EvalReflectConstTrait { - public const TRAIT_VALUE = 3; -} -class EvalReflectConstChild extends EvalReflectConstBase implements EvalReflectConstIface { - private const SECRET = 9; - public const OWN = "own"; - public const SUM = 5; -} -enum EvalReflectConstEnum { - case Ready; - public const LEVEL = 40; -} -$ref = new ReflectionClass("EvalReflectConstChild"); -$all = $ref->getConstants(); -$public = $ref->getConstants(ReflectionClassConstant::IS_PUBLIC); -$private = $ref->getConstants(filter: ReflectionClassConstant::IS_PRIVATE); -$none = $ref->getConstants(0); -$null = $ref->getConstants(null); -echo $ref->getConstant("OWN"); echo ":"; -echo $ref->getConstant("BASE"); echo ":"; -echo $ref->getConstant("LIMIT"); echo ":"; -echo $ref->getConstant("SECRET"); echo ":"; -echo $ref->getConstant("SUM"); echo ":"; -echo $ref->getConstant("own") ? "bad" : "missing"; -echo ":"; echo count($all); echo ":"; echo $all["OWN"]; echo ":"; echo $all["BASE"]; echo ":"; echo $all["LIMIT"]; -echo ":"; echo count($public); echo ":"; echo $public["OWN"]; echo ":"; echo $public["BASE"]; -echo ":"; echo count($private); echo ":"; echo $private["SECRET"]; -echo ":"; echo count($none); echo ":"; echo count($null); -$trait = new ReflectionClass("EvalReflectConstTrait"); -$traitAll = $trait->getConstants(); -echo ":"; echo $trait->getConstant("TRAIT_VALUE"); echo ":"; echo count($traitAll); echo ":"; echo $traitAll["TRAIT_VALUE"]; -$enum = new ReflectionClass("EvalReflectConstEnum"); -$case = $enum->getConstant("Ready"); -$enumAll = $enum->getConstants(); -echo ":"; echo $case->name; -echo ":"; echo $enum->getConstant("LEVEL"); echo ":"; echo $enumAll["LEVEL"]; echo ":"; echo count($enumAll); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "own:1:2:9:5:missing:5:own:1:2:4:own:1:1:9:0:5:3:1:3:Ready:40:40:2" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass returns eval class-constant reflector objects. -#[test] -fn execute_program_reflects_eval_class_constant_reflector_objects() { - let program = parse_fragment( - br#"class EvalReflectConstMarker { - public $label; - public function __construct($label) { - $this->label = $label; - } - public function label() { - return $this->label; - } -} -class EvalReflectConstObjectTarget { - #[EvalReflectConstMarker("const")] - final public const ANSWER = 42; -} -enum EvalReflectConstObjectEnum { - #[EvalReflectConstMarker("case")] - case Ready; - final public const LEVEL = 7; -} -$ref = new ReflectionClass("EvalReflectConstObjectTarget"); -$single = $ref->getReflectionConstant("ANSWER"); -$all = $ref->getReflectionConstants(); -$public = $ref->getReflectionConstants(ReflectionClassConstant::IS_PUBLIC); -$final = $ref->getReflectionConstants(filter: ReflectionClassConstant::IS_FINAL); -echo $single->getName(); echo ":"; -echo count($all); echo ":"; echo $all[0]->getName(); echo ":"; -echo $single->getAttributes()[0]->newInstance()->label(); echo ":"; -echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; -echo ":"; echo count($public); echo ":"; echo $public[0]->getName(); -echo ":"; echo count($final); echo ":"; echo $final[0]->getName(); -$enum = new ReflectionClass("EvalReflectConstObjectEnum"); -$enumAll = $enum->getReflectionConstants(); -$enumFinal = $enum->getReflectionConstants(ReflectionClassConstant::IS_FINAL); -$case = $enum->getReflectionConstant("Ready"); -$level = $enum->getReflectionConstant("LEVEL"); -echo ":"; echo count($enumAll); echo ":"; echo $enumAll[0]->getName(); echo ":"; echo $enumAll[1]->getName(); -echo ":"; echo $case->getAttributes()[0]->newInstance()->label(); echo ":"; -echo count($level->getAttributes()); echo ":"; echo count($enumFinal); echo ":"; echo $enumFinal[0]->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "ANSWER:1:ANSWER:const:missing:1:ANSWER:1:ANSWER:2:Ready:LEVEL:case:0:1:LEVEL" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod and ReflectionProperty expose eval member predicate metadata. -#[test] -fn execute_program_reflects_eval_member_predicates() { - let program = parse_fragment( - br#"abstract class EvalReflectMemberBase { - protected static function baseStatic() {} - abstract protected function mustImplement(); - final public function locked() {} -} -readonly class EvalReflectReadonlyClass { - public int $classReadonly; -} -abstract class EvalReflectAbstractProperty { - abstract public int $mustRead { get; } -} -class EvalReflectMemberChild extends EvalReflectMemberBase { - public function mustImplement() {} - private static $token; - final public static $staticSeal; - protected $visible; - public readonly int $locked; - final public int $sealed; -} -$baseStatic = new ReflectionMethod("EvalReflectMemberChild", "baseStatic"); -echo $baseStatic->isStatic() ? "S" : "s"; -echo $baseStatic->isProtected() ? "P" : "p"; -echo $baseStatic->isPublic() ? "U" : "u"; -echo $baseStatic->isPrivate() ? "R" : "r"; -echo $baseStatic->isFinal() ? "F" : "f"; -echo $baseStatic->isAbstract() ? "A" : "a"; -echo ":"; -$abstractMethod = new ReflectionMethod("EvalReflectMemberBase", "mustImplement"); -echo $abstractMethod->isAbstract() ? "A" : "a"; -echo $abstractMethod->isProtected() ? "P" : "p"; -echo $abstractMethod->isStatic() ? "S" : "s"; -echo ":"; -$finalMethod = new ReflectionMethod("EvalReflectMemberChild", "locked"); -echo $finalMethod->isFinal() ? "F" : "f"; -echo $finalMethod->isPublic() ? "U" : "u"; -echo $finalMethod->isStatic() ? "S" : "s"; -echo ":"; -$staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); -echo $staticProp->isStatic() ? "S" : "s"; -echo $staticProp->isPrivate() ? "R" : "r"; -echo $staticProp->isProtected() ? "P" : "p"; -echo $staticProp->isFinal() ? "F" : "f"; -echo $staticProp->isAbstract() ? "A" : "a"; -echo $staticProp->isReadOnly() ? "R" : "r"; -echo $staticProp->isProtectedSet() ? "T" : "t"; -echo $staticProp->isPrivateSet() ? "D" : "d"; -echo $staticProp->getModifiers(); -echo ":"; -$visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); -echo $visibleProp->isStatic() ? "S" : "s"; -echo $visibleProp->isProtected() ? "P" : "p"; -echo $visibleProp->isPublic() ? "U" : "u"; -echo $visibleProp->isFinal() ? "F" : "f"; -echo $visibleProp->isAbstract() ? "A" : "a"; -echo $visibleProp->isReadOnly() ? "R" : "r"; -echo $visibleProp->isProtectedSet() ? "T" : "t"; -echo $visibleProp->isPrivateSet() ? "D" : "d"; -echo $visibleProp->getModifiers(); -echo ":"; -$readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); -echo $readonlyProp->isReadOnly() ? "R" : "r"; -echo $readonlyProp->isPublic() ? "U" : "u"; -echo $readonlyProp->isProtectedSet() ? "T" : "t"; -echo $readonlyProp->isPrivateSet() ? "D" : "d"; -echo $readonlyProp->getModifiers(); -echo ":"; -$sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); -echo $sealedProp->isFinal() ? "F" : "f"; -echo $sealedProp->isPublic() ? "U" : "u"; -echo $sealedProp->getModifiers(); -echo ":"; -$staticFinalProp = new ReflectionProperty("EvalReflectMemberChild", "staticSeal"); -echo $staticFinalProp->isFinal() ? "F" : "f"; -echo $staticFinalProp->isStatic() ? "S" : "s"; -echo $staticFinalProp->getModifiers(); -echo ":"; -$abstractProp = new ReflectionProperty("EvalReflectAbstractProperty", "mustRead"); -echo $abstractProp->isAbstract() ? "A" : "a"; -echo $abstractProp->isFinal() ? "F" : "f"; -echo $abstractProp->getModifiers(); -echo ":"; -$classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); -echo $classReadonlyProp->isReadOnly() ? "C" : "c"; -echo $classReadonlyProp->isProtectedSet() ? "T" : "t"; -echo $classReadonlyProp->isPrivateSet() ? "D" : "d"; -echo $classReadonlyProp->getModifiers(); -echo ":"; -echo $visibleProp->isDynamic() ? "D" : "d"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177:d" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty reports eval-declared asymmetric set visibility. -#[test] -fn execute_program_reflects_eval_asymmetric_property_set_visibility() { - let program = parse_fragment( - br#"class EvalReflectAsymmetricProperty { - public private(set) int $privateSet = 1; - public protected(set) int $protectedSet = 2; - protected private(set) int $protectedPrivateSet = 3; -} -$private = new ReflectionProperty("EvalReflectAsymmetricProperty", "privateSet"); -echo $private->isPrivateSet() ? "P" : "p"; -echo $private->isProtectedSet() ? "T" : "t"; -echo $private->getModifiers(); echo ":"; -$protected = new ReflectionProperty("EvalReflectAsymmetricProperty", "protectedSet"); -echo $protected->isPrivateSet() ? "P" : "p"; -echo $protected->isProtectedSet() ? "T" : "t"; -echo $protected->getModifiers(); echo ":"; -$protectedPrivate = new ReflectionProperty("EvalReflectAsymmetricProperty", "protectedPrivateSet"); -echo $protectedPrivate->isPrivateSet() ? "P" : "p"; -echo $protectedPrivate->isProtectedSet() ? "T" : "t"; -echo $protectedPrivate->getModifiers(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Pt4129:pT2049:Pt4130"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty reports asymmetric set visibility on eval interface contracts. -#[test] -fn execute_program_reflects_eval_interface_asymmetric_property_set_visibility() { - let program = parse_fragment( - br#"interface EvalReflectAsymmetricIfaceProperty { - public protected(set) string $name { get; set; } - private(set) int $id { get; set; } -} -$protected = new ReflectionProperty("EvalReflectAsymmetricIfaceProperty", "name"); -echo $protected->isProtectedSet() ? "T" : "t"; -echo $protected->isPrivateSet() ? "P" : "p"; -echo $protected->isFinal() ? "F" : "f"; -echo $protected->getModifiers(); echo ":"; -$private = new ReflectionProperty("EvalReflectAsymmetricIfaceProperty", "id"); -echo $private->isProtectedSet() ? "T" : "t"; -echo $private->isPrivateSet() ? "P" : "p"; -echo $private->isFinal() ? "F" : "f"; -echo $private->getModifiers(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Tpf2625:tPF4705"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty reports eval constructor-promotion metadata. -#[test] -fn execute_program_reflection_property_reports_eval_promoted_metadata() { - let program = parse_fragment( - br#"class EvalReflectPromotedTarget { - public function __construct(public int $id, private string $name = "Ada") {} - public string $plain = "x"; -} -$id = new ReflectionProperty("EvalReflectPromotedTarget", "id"); -$name = new ReflectionProperty("EvalReflectPromotedTarget", "name"); -$plain = new ReflectionProperty("EvalReflectPromotedTarget", "plain"); -echo $id->isPromoted() ? "I" : "i"; -echo $name->isPromoted() ? "N" : "n"; -echo $plain->isPromoted() ? "P" : "p"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "INp"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod constructor/destructor predicates for eval methods. -#[test] -fn execute_program_reflection_method_reports_constructor_and_destructor() { - let program = parse_fragment( - br#"class EvalReflectLifecycle { - public function __construct() {} - public function __destruct() {} - public function run() {} -} -$ctor = new ReflectionMethod("EvalReflectLifecycle", "__CONSTRUCT"); -echo $ctor->isConstructor() ? "C" : "c"; -echo $ctor->isDestructor() ? "D" : "d"; -echo ":"; -$dtor = new ReflectionMethod("EvalReflectLifecycle", "__destruct"); -echo $dtor->isConstructor() ? "C" : "c"; -echo $dtor->isDestructor() ? "D" : "d"; -echo ":"; -$run = new ReflectionMethod("EvalReflectLifecycle", "run"); -echo $run->isConstructor() ? "C" : "c"; -echo $run->isDestructor() ? "D" : "d"; -echo ":"; -$listed = (new ReflectionClass("EvalReflectLifecycle"))->getConstructor(); -echo $listed->isConstructor() ? "C" : "c"; -echo $listed->isDestructor() ? "D" : "d"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Cd:cD:cd:Cd"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod preserves declared method case after case-insensitive lookup. -#[test] -fn execute_program_reflection_method_preserves_declared_name_case() { - let program = parse_fragment( - br#"class EvalReflectMethodCaseBase { - public function MiXeDCase() { return "base"; } -} -class EvalReflectMethodCaseChild extends EvalReflectMethodCaseBase { - public function childCase() { return "child"; } -} -$object = new EvalReflectMethodCaseChild(); -$direct = new ReflectionMethod("EvalReflectMethodCaseChild", "mixedcase"); -echo $direct->getName(); echo ":"; -echo $direct->getShortName(); echo ":"; -echo $direct->invoke($object); echo ":"; -$listed = (new ReflectionClass("EvalReflectMethodCaseChild"))->getMethod("CHILDCASE"); -echo $listed->getName(); echo ":"; -echo $listed->invoke($object); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "MiXeDCase:MiXeDCase:base:childCase:child"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod accepts object targets and reflects the runtime class. -#[test] -fn execute_program_reflection_method_accepts_object_targets() { - let program = parse_fragment( - br#"class EvalReflectMethodObjectBase { - public function MiXeDCase() { return "base"; } -} -class EvalReflectMethodObjectChild extends EvalReflectMethodObjectBase { - public function childCase() { return "child"; } -} -$object = new EvalReflectMethodObjectChild(); -$inherited = new ReflectionMethod($object, "mixedcase"); -echo $inherited->getName(); echo ":"; -echo $inherited->getDeclaringClass()->getName(); echo ":"; -echo $inherited->invoke($object); echo ":"; -$own = new ReflectionMethod($object, "CHILDCASE"); -echo $own->getName(); echo ":"; -echo $own->getDeclaringClass()->getName(); echo ":"; -echo $own->invoke($object); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "MiXeDCase:EvalReflectMethodObjectBase:base:childCase:EvalReflectMethodObjectChild:child" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod::createFromMethodName resolves eval method strings. -#[test] -fn execute_program_reflection_method_create_from_method_name() { - let program = parse_fragment( - br#"class EvalReflectCreateMethodTarget { - public function MiXeDCase() { return "ok"; } -} -$ref = ReflectionMethod::createFromMethodName("EvalReflectCreateMethodTarget::mixedcase"); -echo $ref->getDeclaringClass()->getName(); echo ":"; -echo $ref->getName(); echo ":"; -echo $ref->invoke(new EvalReflectCreateMethodTarget()); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "EvalReflectCreateMethodTarget:MiXeDCase:ok"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod accepts PHP's deprecated one-string method target. -#[test] -fn execute_program_reflection_method_accepts_single_method_string() { - let program = parse_fragment( - br#"class EvalReflectCtorMethodTarget { - public function MiXeDCase() { return "ok"; } -} -$ref = new ReflectionMethod(objectOrMethod: "EvalReflectCtorMethodTarget::mixedcase"); -echo $ref->getDeclaringClass()->getName(); echo ":"; -echo $ref->getName(); echo ":"; -echo $ref->invoke(new EvalReflectCtorMethodTarget()); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "EvalReflectCtorMethodTarget:MiXeDCase:ok"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod construction throws catchable PHP reflection errors. -#[test] -fn execute_program_reflection_method_constructor_throws_reflection_exceptions() { - let program = parse_fragment( - br#"class EvalReflectMissingMethodTarget {} -try { - new ReflectionMethod("EvalReflectMissingMethodTarget", "missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionMethod("EvalReflectMissingMethodTarget::missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - ReflectionMethod::createFromMethodName("EvalReflectMissingMethodTarget::missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionMethod("EvalReflectMissingClass", "run"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - ReflectionMethod::createFromMethodName("not-a-method"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - assert!( - result.is_ok(), - "execute eval ir failed after output {:?}", - values.output - ); - - assert_eq!( - values.output, - "ReflectionException:Method EvalReflectMissingMethodTarget::missing() does not exist|Method EvalReflectMissingMethodTarget::missing() does not exist|Method EvalReflectMissingMethodTarget::missing() does not exist|Class \"EvalReflectMissingClass\" does not exist|ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name" - ); - assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty construction throws catchable PHP reflection errors. -#[test] -fn execute_program_reflection_property_constructor_throws_reflection_exceptions() { - let program = parse_fragment( - br#"class EvalReflectMissingPropertyTarget {} -$object = new EvalReflectMissingPropertyTarget(); -$object->dynamic = 1; -try { - new ReflectionProperty("EvalReflectMissingPropertyTarget", "missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionProperty("EvalReflectMissingPropertyClass", "value"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionProperty($object, "missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -$property = new ReflectionProperty($object, "dynamic"); -echo $property->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - assert!( - result.is_ok(), - "execute eval ir failed after output {:?}", - values.output - ); - - assert_eq!( - values.output, - "ReflectionException:Property EvalReflectMissingPropertyTarget::$missing does not exist|Class \"EvalReflectMissingPropertyClass\" does not exist|Property EvalReflectMissingPropertyTarget::$missing does not exist|dynamic" - ); - assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClassConstant construction throws catchable PHP reflection errors. -#[test] -fn execute_program_reflection_class_constant_constructor_throws_reflection_exceptions() { - let program = parse_fragment( - br#"class EvalReflectMissingConstantTarget { - public const OK = 1; -} -try { - new ReflectionClassConstant("EvalReflectMissingConstantTarget", "missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionClassConstant("EvalReflectMissingConstantClass", "VALUE"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -echo (new ReflectionClassConstant("EvalReflectMissingConstantTarget", "OK"))->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - assert!( - result.is_ok(), - "execute eval ir failed after output {:?}", - values.output - ); - - assert_eq!( - values.output, - "ReflectionException:Constant EvalReflectMissingConstantTarget::missing does not exist|Class \"EvalReflectMissingConstantClass\" does not exist|OK" - ); - assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); -} - -/// Verifies ReflectionEnumUnitCase/BackedCase construction throws PHP reflection errors. -#[test] -fn execute_program_reflection_enum_case_constructor_throws_reflection_exceptions() { - let program = parse_fragment( - br#"enum EvalReflectMissingCaseUnit { - case Ready; - public const TOKEN = 1; -} -enum EvalReflectMissingCaseBacked: string { - case Ready = "ready"; - public const TOKEN = 1; -} -class EvalReflectMissingCaseClass { - public const TOKEN = 1; -} -try { - new ReflectionEnumUnitCase("EvalReflectMissingCaseUnit", "Missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionEnumUnitCase("EvalReflectMissingCaseClass", "TOKEN"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionEnumUnitCase("EvalReflectMissingCaseUnit", "TOKEN"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionEnumBackedCase("EvalReflectMissingCaseUnit", "Ready"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionEnumBackedCase("EvalReflectMissingCaseBacked", "Missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionEnumBackedCase("EvalReflectMissingCaseClass", "Missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -echo (new ReflectionEnumUnitCase("EvalReflectMissingCaseBacked", "Ready"))->getName(); echo ":"; -echo (new ReflectionEnumBackedCase("EvalReflectMissingCaseBacked", "Ready"))->getBackingValue(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - assert!( - result.is_ok(), - "execute eval ir failed after output {:?}", - values.output - ); - - assert_eq!( - values.output, - "ReflectionException:Constant EvalReflectMissingCaseUnit::Missing does not exist|Constant EvalReflectMissingCaseClass::TOKEN is not a case|Constant EvalReflectMissingCaseUnit::TOKEN is not a case|Enum case EvalReflectMissingCaseUnit::Ready is not a backed case|Constant EvalReflectMissingCaseBacked::Missing does not exist|Constant EvalReflectMissingCaseClass::Missing does not exist|Ready:ready" - ); - assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); -} - -/// Verifies eval member and enum-case reflectors expose their declaring class. -#[test] -fn execute_program_reflects_eval_declaring_class_metadata() { - let program = parse_fragment( - br#"class EvalDeclaringBase { - public $baseProp = 1; - public function inherited() { return "base"; } - public const BASE_CONST = 10; -} -class EvalDeclaringChild extends EvalDeclaringBase { - public $childProp = 2; - public function own() { return "child"; } - public const CHILD_CONST = 20; -} -enum EvalDeclaringEnum: string { - case Ready = "ready"; - public const LEVEL = 3; -} -echo (new ReflectionMethod("EvalDeclaringChild", "inherited"))->getDeclaringClass()->getName(); echo ":"; -echo (new ReflectionClass("EvalDeclaringChild"))->getMethod("own")->getDeclaringClass()->getName(); echo ":"; -echo (new ReflectionProperty("EvalDeclaringChild", "baseProp"))->getDeclaringClass()->getName(); echo ":"; -echo (new ReflectionClass("EvalDeclaringChild"))->getProperty("childProp")->getDeclaringClass()->getName(); echo ":"; -echo (new ReflectionClass("EvalDeclaringChild"))->getReflectionConstant("BASE_CONST")->getDeclaringClass()->getName(); echo ":"; -echo (new ReflectionClassConstant("EvalDeclaringChild", "BASE_CONST"))->getDeclaringClass()->getName(); echo ":"; -echo (new ReflectionClass("EvalDeclaringEnum"))->getReflectionConstant("Ready")->getDeclaringClass()->getName(); echo ":"; -echo (new ReflectionEnumBackedCase("EvalDeclaringEnum", "Ready"))->getDeclaringClass()->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringBase:EvalDeclaringEnum:EvalDeclaringEnum" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass stringifies retained eval class metadata. -#[test] -fn execute_program_reflection_class_to_string() { - let program = parse_fragment( - br#"class EvalReflectClassStringTarget { - public const ANSWER = 42; - public int $id = 7; - public function read(string $name = "Ada"): ?string { return $name; } -} -$ref = new ReflectionClass("EvalReflectClassStringTarget"); -echo $ref; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Class [ class EvalReflectClassStringTarget ] {\n - Constants [1] {\n Constant [ public int ANSWER ] { 42 }\n }\n - Properties [1] {\n Property [ public int $id = 7 ]\n }\n - Methods [1] {\n Method [ public method read ]\n }\n}\n" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod exposes eval method parameter objects with names and positions. -#[test] -fn execute_program_reflects_eval_method_parameters() { - let program = parse_fragment( -br##"interface EvalReflectLeft {} -interface EvalReflectRight {} -class EvalReflectParamTarget { - public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, ?array $items = null, ?callable $callback = null, \App\Name|null $second = null, &...$rest) {} -} -$method = new ReflectionMethod("EvalReflectParamTarget", "run"); -echo $method->getNumberOfParameters(); echo "/"; -echo $method->getNumberOfRequiredParameters(); echo ":"; -$params = $method->getParameters(); -foreach ($params as $param) { - echo $param->getName(); echo "#"; echo $param->getPosition(); - echo $param->isOptional() ? "O" : "r"; - echo $param->isVariadic() ? "V" : "v"; - echo $param->isPassedByReference() ? "R" : "b"; - echo $param->canBePassedByValue() ? "Y" : "N"; - echo $param->hasType() ? "T" : "t"; - echo $param->allowsNull() ? "N" : "n"; - echo $param->isArray() ? "A" : "a"; - echo $param->isCallable() ? "C" : "c"; - $type = $param->getType(); - if ($param->getName() == "union") { - echo ":union"; - echo $type->allowsNull() ? "?" : "!"; - foreach ($type->getTypes() as $memberType) { - echo ":"; echo $memberType->getName(); - echo $memberType->isBuiltin() ? "B" : "C"; - } - } elseif ($param->getName() == "both") { - echo ":intersection"; - echo $type->allowsNull() ? "?" : "!"; - foreach ($type->getTypes() as $memberType) { - echo ":"; echo $memberType->getName(); - echo $memberType->isBuiltin() ? "B" : "C"; - } - } elseif ($type) { - echo ":"; echo $type->getName(); - echo $type->allowsNull() ? "?" : "!"; - echo $type->isBuiltin() ? "B" : "C"; - } else { - echo ":null"; - } - $attrs = $param->getAttributes(); - echo ":A"; echo count($attrs); - if (count($attrs) > 0) { - echo ":"; echo $attrs[0]->getName(); - echo ":"; echo $attrs[0]->getArguments()[0]; - } - echo $param->isDefaultValueAvailable() ? ":D" : ":d"; - if ($param->isDefaultValueAvailable()) { - echo "="; - echo $param->getDefaultValue() === null ? "null" : $param->getDefaultValue(); - } - echo "|"; -} -return true;"##, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "7/3:first#0rvRNTnac:int!B:A1:EvalParamTag:first:d|union#1rvbYTnac:union!:intB:stringB:A0:d|both#2rvbYTnac:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|items#3OvbYTNAc:array?B:A0:D=null|callback#4OvbYTNaC:callable?B:A0:D=null|second#5OvbYTNac:App\\Name?C:A0:D=null|rest#6OVRNtNac:null:A0:d|" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionType objects stringify retained eval parameter metadata. -#[test] -fn execute_program_reflection_type_to_string() { - let program = parse_fragment( -br##"class EvalReflectTypeStringDep {} -interface EvalReflectTypeStringLeft {} -interface EvalReflectTypeStringRight {} -class EvalReflectTypeStringTarget { - public function run(?EvalReflectTypeStringDep $dep, int|string|null $union, EvalReflectTypeStringLeft&EvalReflectTypeStringRight $both, mixed $mixed, ?array $items) {} -} -$params = (new ReflectionMethod("EvalReflectTypeStringTarget", "run"))->getParameters(); -foreach ($params as $param) { - $type = $param->getType(); - echo $param->getName(); echo ":"; - echo $type->__toString(); echo "|"; -} -$unionType = $params[1]->getType(); -echo "cast:" . (string)$unionType . "|"; -echo "concat:" . $unionType . "|"; -echo "echo:"; -echo $unionType; -return true;"##, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|cast:int|string|null|concat:int|string|null|echo:int|string|null" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionParameter formats retained eval parameter metadata through `__toString()`. -#[test] -fn execute_program_reflection_parameter_to_string() { - let program = parse_fragment( - br#"class EvalReflectParameterStringTarget { - const LABEL = "L"; - public function run(string $name, int $count = 3, $label = self::LABEL, &...$items) {} -} -$params = (new ReflectionMethod("EvalReflectParameterStringTarget", "run"))->getParameters(); -foreach ($params as $param) { - echo $param->__toString(); - echo "|"; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Parameter #0 [ string $name ]|Parameter #1 [ int $count = 3 ]|Parameter #2 [ $label = self::LABEL ]|Parameter #3 [ &...$items ]|" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod exposes eval-declared return type metadata. -#[test] -fn execute_program_reflection_method_reports_return_type_metadata() { - let program = parse_fragment( - br#"interface EvalReflectReturnIface { - public function read(): string; -} -class EvalReflectReturnTarget implements EvalReflectReturnIface { - public function read(): string { return "ok"; } - public function selfReturn(): static { return $this; } - public function done(): void {} -} -$iface = new ReflectionMethod("EvalReflectReturnIface", "read"); -$ifaceType = $iface->getReturnType(); -echo $iface->hasReturnType() ? "I" : "i"; echo ":"; -echo $ifaceType->getName(); echo ":"; -echo $ifaceType->isBuiltin() ? "B" : "b"; echo ":"; -$self = (new ReflectionMethod("EvalReflectReturnTarget", "selfReturn"))->getReturnType(); -echo $self->getName(); echo ":"; -echo $self->isBuiltin() ? "B" : "b"; echo ":"; -$void = (new ReflectionMethod("EvalReflectReturnTarget", "done"))->getReturnType(); -echo $void->getName(); echo ":"; -echo $void->allowsNull() ? "N" : "n"; echo ":"; -echo $void->isBuiltin() ? "B" : "b"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "I:string:B:static:b:void:n:B"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod formats retained eval method metadata through `__toString()`. -#[test] -fn execute_program_reflection_method_to_string() { - let program = parse_fragment( - br#"class EvalReflectMethodStringTarget { - final public static function run(?int $id, string $label = "ok"): ?string { - return $label; - } -} -$ref = new ReflectionMethod("EvalReflectMethodStringTarget", "run"); -echo str_replace("\n", "|", $ref->__toString()); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Method [ final static public method run ] {| - Parameters [2] {| Parameter #0 [ ?int $id ]| Parameter #1 [ string $label = 'ok' ]| }| - Return [ ?string ]|}|" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionParameter reports eval constructor-promotion metadata. -#[test] -fn execute_program_reflection_parameter_reports_eval_promoted_metadata() { - let program = parse_fragment( - br#"class EvalReflectPromotedParamTarget { - public function __construct(public int $id, string $name = "Ada") {} - public function run(int $id) {} -} -$ctorParams = (new ReflectionMethod("EvalReflectPromotedParamTarget", "__construct"))->getParameters(); -$runParams = (new ReflectionMethod("EvalReflectPromotedParamTarget", "run"))->getParameters(); -echo $ctorParams[0]->isPromoted() ? "I" : "i"; -echo $ctorParams[1]->isPromoted() ? "N" : "n"; -echo $runParams[0]->isPromoted() ? "R" : "r"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Inr"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty exposes eval property get/set type metadata. -#[test] -fn execute_program_reflection_property_get_type_metadata() { - let program = parse_fragment( - br##"class EvalReflectPropertyTypeDep {} -class EvalReflectPropertyTypeTarget { - public int $id; - public ?string $name; - public EvalReflectPropertyTypeDep $dep; - public $plain; - public int|string $union; -} -$properties = (new ReflectionClass("EvalReflectPropertyTypeTarget"))->getProperties(); -foreach ($properties as $property) { - echo $property->getName(); echo ":"; - echo $property->hasType() ? "T:" : "t:"; - $type = $property->getType(); - if ($property->getName() == "union") { - echo "union"; - echo $type->allowsNull() ? "?" : "!"; - foreach ($type->getTypes() as $memberType) { - echo ":"; echo $memberType->getName(); - echo $memberType->isBuiltin() ? "B" : "C"; - } - } elseif ($type) { - echo $type->getName(); - echo $type->allowsNull() ? "?" : "!"; - echo $type->isBuiltin() ? "B" : "C"; - } else { - echo "null"; - } - echo "|"; -} -$direct = new ReflectionProperty("EvalReflectPropertyTypeTarget", "dep"); -$directType = $direct->getType(); -echo "direct:"; echo $direct->hasType() ? "T:" : "t:"; -echo $directType->getName(); -$directSettableType = $direct->getSettableType(); -echo ":set:"; echo $directSettableType->getName(); -$plain = new ReflectionProperty("EvalReflectPropertyTypeTarget", "plain"); -echo ":plainSet:"; echo $plain->getSettableType() === null ? "N" : "n"; -$directUnion = new ReflectionProperty("EvalReflectPropertyTypeTarget", "union"); -echo ":unionSet:"; echo count($directUnion->getSettableType()->getTypes()); -return true;"##, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep:set:EvalReflectPropertyTypeDep:plainSet:N:unionSet:2" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty retains explicit set-hook parameter metadata as the settable type. -#[test] -fn execute_program_reflection_property_get_settable_type_uses_set_hook_parameter() { - let program = parse_fragment( - br##"class EvalReflectSettableTypeTarget { - public string $value { - get => $this->value; - set(int|string $raw) => (string) $raw; - } -} -$property = new ReflectionProperty("EvalReflectSettableTypeTarget", "value"); -$type = $property->getType(); -$settable = $property->getSettableType(); -echo $type->getName(); echo ":"; -echo count($settable->getTypes()); -foreach ($settable->getTypes() as $memberType) { - echo ":"; echo $memberType->getName(); - echo $memberType->isBuiltin() ? "B" : "C"; -} -$setHook = $property->getHook(PropertyHookType::Set); -$paramType = $setHook->getParameters()[0]->getType(); -echo ":"; echo count($paramType->getTypes()); -$box = new EvalReflectSettableTypeTarget(); -$box->value = 7; -echo ":"; echo $box->value; -return true;"##, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "string:2:intB:stringB:2:7"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty exposes eval property default metadata. -#[test] -fn execute_program_reflection_property_get_default_value_metadata() { - let program = parse_fragment( - br#"class EvalReflectPropertyDefaultTarget { - public $implicit; - public int $typed; - public ?string $nullableTyped; - public $explicitNull = null; - public int $count = 7; - public static string $label = "ok"; -} - -foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label"] as $name) { - $property = new ReflectionProperty("EvalReflectPropertyDefaultTarget", $name); - echo $property->getName(); echo ":"; - echo $property->isDefault() ? "Y:" : "N:"; - echo $property->hasDefaultValue() ? "D:" : "d:"; - $value = $property->getDefaultValue(); - echo $value === null ? "null" : $value; - echo "|"; -} -$listed = (new ReflectionClass("EvalReflectPropertyDefaultTarget"))->getProperty("implicit"); -echo "listed:"; -echo $listed->isDefault() ? "Y:" : "N:"; -echo $listed->hasDefaultValue() ? "D:" : "d:"; -echo $listed->getDefaultValue() === null ? "null" : "bad"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|listed:Y:D:null" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty formats retained property metadata through `__toString()`. -#[test] -fn execute_program_reflection_property_to_string() { - let program = parse_fragment( - br#"class EvalReflectPropertyStringTarget { - public int $id = 7; - protected static string $label = "ok"; - private $implicit; - public $virtual { - get => 1; - } -} -foreach (["id", "label", "implicit", "virtual"] as $name) { - echo (new ReflectionProperty("EvalReflectPropertyStringTarget", $name))->__toString(); - echo "|"; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public $virtual ]|" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionParameter reports eval constant-default metadata. -#[test] -fn execute_program_reflection_parameter_reports_default_constant_metadata() { - let program = parse_fragment( - br##"define("EVAL_REFLECT_PARAM_DEFAULT_GLOBAL", "G"); -class EvalReflectParamDefaultBase { - const BASE = "B"; -} -class EvalReflectParamDefaultTarget extends EvalReflectParamDefaultBase { - const LABEL = "L"; - public function run($required, $global = EVAL_REFLECT_PARAM_DEFAULT_GLOBAL, $self = self::LABEL, $parent = parent::BASE, $literal = 7) {} -} -$params = (new ReflectionMethod("EvalReflectParamDefaultTarget", "run"))->getParameters(); -foreach ($params as $param) { - echo $param->getName(); echo ":"; - echo $param->isDefaultValueAvailable() ? "D:" : "d:"; - if ($param->isDefaultValueAvailable()) { - if ($param->isDefaultValueConstant()) { - echo "C:"; - echo $param->getDefaultValueConstantName(); - echo ":"; - } else { - echo "c:null:"; - } - echo $param->getDefaultValue(); - } - echo "|"; -} -return true;"##, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "required:d:|global:D:C:EVAL_REFLECT_PARAM_DEFAULT_GLOBAL:G|self:D:C:self::LABEL:L|parent:D:C:parent::BASE:B|literal:D:c:null:7|" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionParameter default magic constants use declaring callable scopes. -#[test] -fn execute_program_reflection_parameter_resolves_default_magic_constants() { - let program = parse_fragment( - br##"namespace EvalReflectParamMagicNs; -function eval_reflect_param_magic($fn = __FUNCTION__, $m = __METHOD__, $c = __CLASS__, $t = __TRAIT__, $n = __NAMESPACE__) {} -interface EvalReflectParamMagicIface { - public function read($c = __CLASS__, $m = __METHOD__, $fn = __FUNCTION__, $t = __TRAIT__, $n = __NAMESPACE__); -} -trait EvalReflectParamMagicTrait { - public function source($c = __CLASS__, $t = __TRAIT__, $m = __METHOD__, $fn = __FUNCTION__, $n = __NAMESPACE__) {} -} -class EvalReflectParamMagicBox { - use EvalReflectParamMagicTrait { source as aliasSource; } - public function own($c = __CLASS__, $t = __TRAIT__, $m = __METHOD__, $fn = __FUNCTION__, $n = __NAMESPACE__) {} -} -function eval_param_magic_dump($ref) { - foreach ($ref->getParameters() as $param) { - echo "[" . $param->getDefaultValue() . "]"; - } - echo ":"; -} -eval_param_magic_dump(new \ReflectionFunction(__NAMESPACE__ . "\\eval_reflect_param_magic")); -eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "own")); -eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "aliasSource")); -eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicIface::class, "read")); -return true;"##, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - concat!( - "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", - "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", - "[][][EvalReflectParamMagicNs]:", - "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", - "[]", - "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox::own]", - "[own]", - "[EvalReflectParamMagicNs]:", - "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", - "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait]", - "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait::source]", - "[source]", - "[EvalReflectParamMagicNs]:", - "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface]", - "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface::read]", - "[read]", - "[]", - "[EvalReflectParamMagicNs]:" - ) - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass exposes eval property default metadata as an associative map. -#[test] -fn execute_program_reflection_class_get_default_properties_metadata() { - let program = parse_fragment( - br#"class EvalReflectDefaultBase { - public int $base = 1; - protected string $prot = "p"; - private int $shadow = 3; - public $implicit; - public int $typed; - public static string $baseStatic = "bs"; -} -class EvalReflectDefaultChild extends EvalReflectDefaultBase { - public int $child = 5; - private int $shadow = 9; - public static int $childStatic = 7; - public ?int $nullable = null; -} -$defaults = (new ReflectionClass("EvalReflectDefaultChild"))->getDefaultProperties(); -echo $defaults["childStatic"]; echo ":"; -echo $defaults["baseStatic"]; echo ":"; -echo $defaults["child"]; echo ":"; -echo $defaults["shadow"]; echo ":"; -echo $defaults["base"]; echo ":"; -echo $defaults["prot"]; echo ":"; -echo array_key_exists("implicit", $defaults) && $defaults["implicit"] === null ? "I:" : "i:"; -echo array_key_exists("nullable", $defaults) && $defaults["nullable"] === null ? "N:" : "n:"; -echo array_key_exists("typed", $defaults) ? "T" : "t"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "7:bs:5:9:1:p:I:N:t"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty can read and write eval instance and static property values. -#[test] -fn execute_program_reflection_property_gets_and_sets_eval_values() { - let program = parse_fragment( - br#"class EvalReflectValueBase { - private $secret = "base"; - public static $count = 1; -} -class EvalReflectValueChild extends EvalReflectValueBase { - protected $name = "Ada"; -} -class EvalReflectValueHook { - public $raw = 2; - public $doubled { - get => $this->raw * 2; - set { $this->raw = $value + 1; } - } - public $backed { - get { return $this->backed * 2; } - set { $this->backed = $value; } - } - public $virtual { - get => $this->raw + 100; - } - public function __construct() { - $this->backed = 2; - } -} -$child = new EvalReflectValueChild(); -$secret = new ReflectionProperty("EvalReflectValueBase", "secret"); -echo $secret->getValue($child); echo ":"; -$secret->setValue($child, "changed"); -echo $secret->getValue(object: $child); echo ":"; -$name = new ReflectionProperty("EvalReflectValueChild", "name"); -echo $name->getValue($child); echo ":"; -$name->setValue(objectOrValue: $child, value: "Grace"); -echo $name->getValue($child); echo ":"; -$count = new ReflectionProperty("EvalReflectValueBase", "count"); -echo $count->getValue(); echo ":"; -$count->setValue(5); -echo EvalReflectValueChild::$count; echo ":"; -$count->setValue(null, 6); -echo $count->getValue($child); echo ":"; -$hook = new EvalReflectValueHook(); -$doubled = new ReflectionProperty("EvalReflectValueHook", "doubled"); -echo $doubled->getValue($hook); echo ":"; -$doubled->setValue($hook, 4); -echo $hook->raw; echo ":"; -echo $doubled->getValue($hook); echo ":"; -$backed = new ReflectionProperty("EvalReflectValueHook", "backed"); -echo $backed->getRawValue($hook); echo ":"; -echo $backed->getValue($hook); echo ":"; -$backed->setValue($hook, 4); -echo $backed->getRawValue(object: $hook); echo ":"; -echo $backed->getValue($hook); echo ":"; -$backed->setRawValue(object: $hook, value: 7); -echo $backed->getRawValue($hook); echo ":"; -echo $backed->getValue($hook); echo ":"; -echo $backed->isLazy($hook) ? "L" : "l"; echo ":"; -$backed->skipLazyInitialization(object: $hook); -$backed->setRawValueWithoutLazyInitialization(object: $hook, value: 8); -echo $backed->getRawValue($hook); echo ":"; -echo $backed->getValue($hook); echo ":"; -echo $backed->getModifiers(); echo ":"; -echo $backed->isVirtual() ? "V" : "b"; echo ":"; -echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->isVirtual() ? "V" : "b"; echo ":"; -echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:l:8:16:1:b:V:513" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty raw APIs reject virtual eval property hooks. -#[test] -fn execute_program_reflection_property_rejects_virtual_raw_value() { - let program = parse_fragment( - br#"class EvalReflectVirtualRawHook { - public $raw = 2; - public $virtual { - get => $this->raw * 2; - } -} -$object = new EvalReflectVirtualRawHook(); -$property = new ReflectionProperty("EvalReflectVirtualRawHook", "virtual"); -$property->getRawValue($object); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("virtual raw property read should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies ReflectionProperty reports eval instance and static initialization state. -#[test] -fn execute_program_reflection_property_reports_initialized_state() { - let program = parse_fragment( - br#"class EvalReflectInitializedTarget { - public int $typed; - public ?int $nullable; - public $plain; - public static int $staticTyped; - public static $staticPlain; - public $virtual { - get => 42; - } -} -$object = new EvalReflectInitializedTarget(); -$typed = new ReflectionProperty("EvalReflectInitializedTarget", "typed"); -$nullable = new ReflectionProperty("EvalReflectInitializedTarget", "nullable"); -$plain = new ReflectionProperty("EvalReflectInitializedTarget", "plain"); -$staticTyped = new ReflectionProperty("EvalReflectInitializedTarget", "staticTyped"); -$staticPlain = new ReflectionProperty("EvalReflectInitializedTarget", "staticPlain"); -$virtual = new ReflectionProperty("EvalReflectInitializedTarget", "virtual"); -echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; -echo $plain->isInitialized(object: $object) ? "P" : "p"; echo ":"; -echo $staticTyped->isInitialized() ? "S" : "s"; echo ":"; -echo $staticPlain->isInitialized() ? "N" : "n"; echo ":"; -EvalReflectInitializedTarget::$staticTyped = 3; -echo $staticTyped->isInitialized() ? "S" : "s"; echo ":"; -$object->typed = 5; -echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; -unset($object->typed); -echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; -$typed->setRawValue(object: $object, value: 9); -echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; -echo $nullable->isInitialized($object) ? "Y" : "y"; echo ":"; -$nullable->setValue($object, null); -echo $nullable->isInitialized($object) ? "Y" : "y"; echo ":"; -echo $virtual->isInitialized($object) ? "V" : "v"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "t:P:s:N:S:T:t:T:y:Y:V"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty materializes and operates on public dynamic properties. -#[test] -fn execute_program_reflection_property_supports_dynamic_properties() { - let program = parse_fragment( - br#"class EvalReflectDynamicBase {} -class EvalReflectDynamicChild extends EvalReflectDynamicBase {} -$object = new EvalReflectDynamicBase(); -$object->dynamic = "first"; -$child = new EvalReflectDynamicChild(); -$child->dynamic = "child"; -$empty = new EvalReflectDynamicChild(); -$property = new ReflectionProperty($object, "dynamic"); -echo $property->getName(); echo ":"; -echo $property->isDynamic() ? "D" : "d"; echo ":"; -echo $property->isDefault() ? "Y" : "N"; echo ":"; -echo $property->getModifiers(); echo ":"; -echo is_null($property->getType()) ? "T" : "t"; echo ":"; -echo is_null($property->getSettableType()) ? "S" : "s"; echo ":"; -echo $property->hasDefaultValue() ? "H" : "h"; echo ":"; -echo is_null($property->getDefaultValue()) ? "V" : "v"; echo ":"; -echo $property->isLazy($object) ? "L" : "l"; echo ":"; -echo $property->isInitialized($object) ? "I" : "i"; echo ":"; -echo $property->getValue($object); echo ":"; -echo $property->getValue($child); echo ":"; -echo $property->isInitialized($empty) ? "E" : "e"; echo ":"; -echo is_null($property->getValue($empty)) ? "null" : "bad"; echo ":"; -$property->setValue($empty, "filled"); -echo $property->getValue($empty); echo ":"; -$property->setRawValue($object, "raw"); -echo $property->getRawValue($object); echo ":"; -echo str_replace("\n", "\\n", $property->__toString()); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "dynamic:D:N:1:T:S:h:V:l:I:first:child:e:null:filled:raw:Property [ public $dynamic ]\\n" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionProperty exposes eval property hook metadata and methods. -#[test] -fn execute_program_reflection_property_gets_eval_hook_metadata() { - let program = parse_fragment( - br#"class EvalReflectHookedProperty { - public int $raw = 2; - public int $doubled { - get { return $this->raw * 2; } - set { $this->raw = $value; } - } - public int $readonlyHook { - get => $this->raw + 1; - } - public int $plain = 5; -} -abstract class EvalReflectAbstractHookProperty { - abstract public int $contract { get; set; } -} -interface EvalReflectInterfaceHookProperty { - public int $iface { get; } -} -$hooked = new ReflectionProperty("EvalReflectHookedProperty", "doubled"); -$plain = new ReflectionProperty("EvalReflectHookedProperty", "plain"); -$readonly = new ReflectionProperty("EvalReflectHookedProperty", "readonlyHook"); -$abstract = new ReflectionProperty("EvalReflectAbstractHookProperty", "contract"); -$iface = new ReflectionProperty("EvalReflectInterfaceHookProperty", "iface"); -$getCase = PropertyHookType::Get; -$setCase = PropertyHookType::Set; -echo $getCase->name; echo ":"; echo $getCase->value; echo ":"; -$caseList = PropertyHookType::cases(); -echo count($caseList); echo ":"; echo $caseList[0]->name; echo ":"; echo $caseList[1]->value; echo ":"; -echo PropertyHookType::from("set")->name; echo ":"; -echo PropertyHookType::tryFrom("missing") === null ? "T" : "t"; echo ":"; -echo $hooked->hasHooks() ? "H" : "h"; echo ":"; -echo $hooked->hasHook($getCase) ? "G" : "g"; echo ":"; -echo $hooked->hasHook(type: $setCase) ? "S" : "s"; echo ":"; -$hooks = $hooked->getHooks(); -echo count($hooks); echo ":"; echo $hooks["get"]->getName(); echo ":"; echo $hooks["set"]->getName(); echo ":"; -$get = $hooked->getHook($getCase); -$set = $hooked->getHook(type: $setCase); -echo $get->getDeclaringClass()->getName(); echo ":"; echo $get->getNumberOfParameters(); echo ":"; -echo $set->getNumberOfParameters(); echo ":"; echo $set->getParameters()[0]->getName(); echo ":"; -$box = new EvalReflectHookedProperty(); -echo $get->invoke($box); echo ":"; -$set->invoke($box, 7); -echo $box->raw; echo ":"; -echo $readonly->hasHook($getCase) ? "R" : "r"; echo ":"; -echo $readonly->hasHook($setCase) ? "w" : "W"; echo ":"; -echo $readonly->getHook($setCase) === null ? "N" : "n"; echo ":"; -echo $plain->hasHooks() ? "bad" : "plain"; echo ":"; -echo count($plain->getHooks()); echo ":"; -$abstractHooks = $abstract->getHooks(); -echo count($abstractHooks); echo ":"; -echo $abstract->hasHook($getCase) ? "AG" : "ag"; echo ":"; -echo $abstract->hasHook($setCase) ? "AS" : "as"; echo ":"; -echo $abstractHooks["get"]->getName(); echo ":"; echo $abstractHooks["get"]->isAbstract() ? "A" : "a"; echo ":"; -echo $abstractHooks["set"]->getName(); echo ":"; echo $abstractHooks["set"]->isAbstract() ? "A" : "a"; echo ":"; -$ifaceHook = $iface->getHook($getCase); -echo count($iface->getHooks()); echo ":"; -echo $iface->hasHook($getCase) ? "IG" : "ig"; echo ":"; -echo $iface->hasHook($setCase) ? "bad" : "is"; echo ":"; -echo $ifaceHook->isAbstract() ? "IA" : "ia"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Get:get:2:Get:set:Set:T:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass exposes and mutates eval static property values. -#[test] -fn execute_program_reflection_class_static_property_values() { - let program = parse_fragment( - br#"class EvalReflectStaticBase { - public static $base = "b"; - protected static $prot = "p"; - private static $shadow = "base-hidden"; - public $instance = "i"; -} -class EvalReflectStaticChild extends EvalReflectStaticBase { - public static $child = "c"; - private static $shadow = "child-hidden"; - public static int $count = 1; -} -EvalReflectStaticChild::$child = "mut"; -$ref = new ReflectionClass("EvalReflectStaticChild"); -$statics = $ref->getStaticProperties(); -echo count($statics); echo ":"; -echo $statics["child"]; echo ":"; -echo $statics["base"]; echo ":"; -echo $statics["prot"]; echo ":"; -echo $statics["shadow"]; echo ":"; -echo $ref->getStaticPropertyValue("count"); echo ":"; -$ref->setStaticPropertyValue("shadow", "changed"); -echo $ref->getStaticPropertyValue("shadow"); echo ":"; -$ref->setStaticPropertyValue(name: "count", value: 5); -echo EvalReflectStaticChild::$count; echo ":"; -echo $ref->getStaticPropertyValue("instance", "fallback"); echo ":"; -echo $ref->getStaticPropertyValue("missing", "fallback"); echo ":"; -try { - $ref->getStaticPropertyValue("missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo "E"; -} -echo ":"; -try { - $ref->setStaticPropertyValue("instance", "bad"); - echo "bad"; -} catch (ReflectionException $e) { - echo "S"; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "5:mut:b:p:child-hidden:1:changed:5:fallback:fallback:E:S" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval ReflectionParameter exposes declaring class metadata. -#[test] -fn execute_program_reflects_eval_parameter_declaring_class() { - let program = parse_fragment( - br#"class EvalDeclaringParamBase { - public function inherited($base) {} -} -class EvalDeclaringParamChild extends EvalDeclaringParamBase { - public function own($child) {} -} -$inherited = (new ReflectionMethod("EvalDeclaringParamChild", "inherited"))->getParameters()[0]; -echo $inherited->getDeclaringClass()->getName(); echo ":"; -echo $inherited->getDeclaringFunction()->getName(); echo ":"; -echo $inherited->getDeclaringFunction()->getDeclaringClass()->getName(); echo ":"; -$listed = (new ReflectionMethod("EvalDeclaringParamChild", "own"))->getParameters()[0]; -echo $listed->getDeclaringClass()->getName(); echo ":"; -echo $listed->getDeclaringFunction()->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "EvalDeclaringParamBase:inherited:EvalDeclaringParamBase:EvalDeclaringParamChild:own" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies direct ReflectionParameter construction accepts runtime object method targets. -#[test] -fn execute_program_reflection_parameter_accepts_object_expression_target() { - let program = parse_fragment( - br#"class EvalDirectParamObjectTarget { - public function run(int $id, ?string $name = null) {} -} -$param = new ReflectionParameter([new EvalDirectParamObjectTarget(), "run"], "name"); -echo $param->getName(); echo ":"; -echo $param->getPosition(); echo ":"; -echo $param->getDeclaringClass()->getName(); echo ":"; -echo $param->getDeclaringFunction()->getName(); echo ":"; -echo $param->isOptional() ? "O" : "R"; echo ":"; -echo $param->getType()->getName(); echo ":"; -echo $param->allowsNull() ? "N" : "n"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "name:1:EvalDirectParamObjectTarget:run:O:string:N" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionParameter construction throws catchable PHP reflection errors. -#[test] -fn execute_program_reflection_parameter_constructor_throws_reflection_exceptions() { - let program = parse_fragment( - br#"function eval_reflect_param_error_function($known) {} -class EvalReflectParamErrorTarget { - public function run($known) {} -} -try { - new ReflectionParameter("eval_reflect_param_error_function", "missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], "missing"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], 3); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionParameter(["EvalReflectParamErrorTarget", "missing"], "known"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], -1); - echo "bad"; -} catch (ValueError $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -echo "|"; -echo (new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], "known"))->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - assert!( - result.is_ok(), - "execute eval ir failed after output {:?}", - values.output - ); - - assert_eq!( - values.output, - "ReflectionException:The parameter specified by its name could not be found|The parameter specified by its name could not be found|The parameter specified by its offset could not be found|Method EvalReflectParamErrorTarget::missing() does not exist|ValueError:ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0|known" - ); - assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::getMethods preserves eval method parameter metadata. -#[test] -fn execute_program_reflection_class_lists_eval_method_parameters() { - let program = parse_fragment( - br#"class EvalReflectListedParamTarget { - public function first($left) {} - public function second($right, $tail) {} -} -$methods = (new ReflectionClass("EvalReflectListedParamTarget"))->getMethods(); -foreach ($methods as $method) { - $params = $method->getParameters(); - echo $method->getName(); echo ":"; - echo $method->getNumberOfParameters(); echo "/"; - echo $method->getNumberOfRequiredParameters(); - if (count($params) > 0) { - echo ":"; echo $params[0]->getName(); echo ":"; echo $params[0]->getPosition(); - } - echo "|"; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "first:1/1:left:0|second:2/2:right:0|"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass getMethods/getProperties return eval member objects. -#[test] -fn execute_program_reflection_class_lists_eval_member_objects() { - let program = parse_fragment( - br#"#[Attribute] -class EvalListMarker {} -class EvalReflectListTarget { - #[EvalListMarker] - public function first() {} - private static function helper() {} - #[EvalListMarker] - protected $visible; - private static $token; -} -$ref = new ReflectionClass("EvalReflectListTarget"); -$methods = $ref->getMethods(); -$properties = $ref->getProperties(); -$staticMethods = $ref->getMethods(ReflectionMethod::IS_STATIC); -$privateMethods = $ref->getMethods(filter: ReflectionMethod::IS_PRIVATE); -$noMethods = $ref->getMethods(0); -$nullMethods = $ref->getMethods(null); -$staticProperties = $ref->getProperties(ReflectionProperty::IS_STATIC); -$protectedProperties = $ref->getProperties(filter: ReflectionProperty::IS_PROTECTED); -$noProperties = $ref->getProperties(0); -echo count($methods); echo ":"; echo count($properties); echo ":"; -echo ReflectionMethod::IS_STATIC; echo ":"; echo ReflectionMethod::IS_PRIVATE; echo ":"; -$direct = new ReflectionMethod("EvalReflectListTarget", "helper"); -echo "D"; echo $direct->getModifiers(); echo ":"; -foreach ($methods as $method) { - if ($method->getName() === "first") { - echo "F"; echo count($method->getAttributes()); - echo "M"; echo $method->getModifiers(); - } - if ($method->getName() === "helper") { - echo $method->isStatic() ? "S" : "s"; - echo $method->isPrivate() ? "R" : "r"; - echo "M"; echo $method->getModifiers(); - } -} -echo ":"; -foreach ($properties as $property) { - if ($property->getName() === "visible") { - echo "V"; echo count($property->getAttributes()); - echo $property->isProtected() ? "P" : "p"; - echo "M"; echo $property->getModifiers(); - } - if ($property->getName() === "token") { - echo $property->isStatic() ? "T" : "t"; - echo $property->isPrivate() ? "R" : "r"; - echo "M"; echo $property->getModifiers(); - } -} -echo ":"; -echo count($staticMethods); echo $staticMethods[0]->getName(); echo ":"; -echo count($privateMethods); echo $privateMethods[0]->getName(); echo ":"; -echo count($noMethods); echo ":"; echo count($nullMethods); echo ":"; -echo count($staticProperties); echo $staticProperties[0]->getName(); echo ":"; -echo count($protectedProperties); echo $protectedProperties[0]->getName(); echo ":"; -echo count($noProperties); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20:1helper:1helper:0:2:1token:1visible:0" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass getMethod/getProperty return eval member objects. -#[test] -fn execute_program_reflection_class_gets_eval_member_objects() { - let program = parse_fragment( - br#"class EvalReflectLookupTarget { - public function first() {} - private static function helper() {} - protected $visible; - private static $token; -} -$ref = new ReflectionClass("EvalReflectLookupTarget"); -$method = $ref->getMethod("FIRST"); -echo $method->getName(); echo ":"; -echo $method->isPublic() ? "U" : "u"; echo ":"; -$helper = $ref->getMethod("helper"); -echo $helper->isPrivate() ? "P" : "p"; -echo $helper->isStatic() ? "S" : "s"; echo ":"; -$property = $ref->getProperty("visible"); -echo $property->getName(); echo ":"; -echo $property->isProtected() ? "R" : "r"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "first:U:PS:visible:R"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::getParentClass returns eval parent metadata or false. -#[test] -fn execute_program_reflection_class_get_parent_class() { - let program = parse_fragment( - br#"class EvalReflectParentBase {} -class EvalReflectParentChild extends EvalReflectParentBase {} -$parent = (new ReflectionClass("EvalReflectParentChild"))->getParentClass(); -echo $parent->getName(); -echo ":"; -$root = (new ReflectionClass("EvalReflectParentBase"))->getParentClass(); -if ($root === false) { - echo "false"; -} else { - echo "bad"; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "EvalReflectParentBase:false"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::newInstance constructs eval-declared classes. -#[test] -fn execute_program_reflection_class_new_instance_constructs_eval_class() { - let program = parse_fragment( - br#"class EvalReflectNewTarget { - public $label; - public function __construct($left, $right) { - $this->label = $left . $right; - } - public function label() { - return $this->label; - } -} -$ref = new ReflectionClass("EvalReflectNewTarget"); -$first = $ref->newInstance("I", "J"); -echo $first->label(); echo ":"; -$second = $ref->newInstance(...["K", "L"]); -echo $second->label(); echo ":"; -$third = $ref->newInstanceArgs(["right" => "N", "left" => "M"]); -echo $third->label(); echo ":"; -$fourth = $ref->newInstanceArgs(["O", "P"]); -echo $fourth->label(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "IJ:KL:MN:OP"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::newInstance throws for non-public eval constructors. -#[test] -fn execute_program_reflection_class_new_instance_rejects_non_public_eval_constructors() { - let program = parse_fragment( - br#"class EvalReflectNewPrivateCtor { - private function __construct() {} -} -class EvalReflectNewProtectedCtor { - protected function __construct() {} -} -try { - (new ReflectionClass("EvalReflectNewPrivateCtor"))->newInstance(); - echo "bad"; -} catch (ReflectionException $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -echo "|"; -try { - (new ReflectionClass("EvalReflectNewProtectedCtor"))->newInstance(); - echo "bad"; -} catch (ReflectionException $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "ReflectionException:Access to non-public constructor of class EvalReflectNewPrivateCtor|ReflectionException:Access to non-public constructor of class EvalReflectNewProtectedCtor" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass instantiation throws Error for eval non-instantiable class-likes. -#[test] -fn execute_program_reflection_class_new_instance_rejects_eval_non_instantiable_class_likes() { - let program = parse_fragment( - br#"abstract class EvalReflectNewAbstract {} -interface EvalReflectNewIface {} -trait EvalReflectNewTrait {} -enum EvalReflectNewEnum { case Ready; } -function eval_reflect_new_error($class, $without) { - try { - $ref = new ReflectionClass($class); - if ($without) { - $ref->newInstanceWithoutConstructor(); - } else { - $ref->newInstance(); - } - echo "bad"; - } catch (Error $e) { - echo get_class($e) . ":" . $e->getMessage(); - } -} -eval_reflect_new_error("EvalReflectNewAbstract", false); echo "|"; -eval_reflect_new_error("EvalReflectNewAbstract", true); echo "|"; -eval_reflect_new_error("EvalReflectNewIface", false); echo "|"; -eval_reflect_new_error("EvalReflectNewIface", true); echo "|"; -eval_reflect_new_error("EvalReflectNewTrait", false); echo "|"; -eval_reflect_new_error("EvalReflectNewTrait", true); echo "|"; -eval_reflect_new_error("EvalReflectNewEnum", false); echo "|"; -eval_reflect_new_error("EvalReflectNewEnum", true); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate enum EvalReflectNewEnum|Error:Cannot instantiate enum EvalReflectNewEnum" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod::invoke dispatches eval-declared methods. -#[test] -fn execute_program_reflection_method_invoke_calls_eval_method() { - let program = parse_fragment( - br#"class EvalReflectInvokeBase { - private function hidden($label = "H") { - return "hidden:" . $label; - } - public function who() { - return static::class; - } - public static function make($left, $right = "S") { - return static::class . ":" . $left . $right; - } -} -class EvalReflectInvokeChild extends EvalReflectInvokeBase { - public function join($a, $b = "B") { - return $a . $b; - } - public function mutate(&$value) { - $value = $value . "!"; - return $value; - } -} -$object = new EvalReflectInvokeChild(); -$hidden = new ReflectionMethod("EvalReflectInvokeBase", "hidden"); -echo $hidden->invoke($object, "X"); echo ":"; -$who = (new ReflectionClass("EvalReflectInvokeChild"))->getMethod("who"); -echo $who->invoke($object); echo ":"; -$static = new ReflectionMethod("EvalReflectInvokeBase", "make"); -echo $static->invoke(null, right: "Y", left: "X"); echo ":"; -echo $static->invoke($object, "A"); echo ":"; -$join = null; -foreach ((new ReflectionClass("EvalReflectInvokeChild"))->getMethods() as $method) { - if ($method->getName() === "join") { - $join = $method; - } -} -$value = "Q"; -$mutate = new ReflectionMethod("EvalReflectInvokeChild", "mutate"); -echo $join->invokeArgs($object, ["b" => "2", "a" => "1"]); echo ":"; -echo $mutate->invoke($object, $value); echo ":"; echo $value; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "hidden:X:EvalReflectInvokeChild:EvalReflectInvokeBase:XY:EvalReflectInvokeBase:AS:12:Q!:Q" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod::invoke throws for incompatible eval receivers. -#[test] -fn execute_program_reflection_method_invoke_rejects_wrong_object() { - let program = parse_fragment( - br#"class EvalReflectInvokeOwner { - public function run() { - return "owner"; - } -} -class EvalReflectInvokeOther {} -try { - (new ReflectionMethod("EvalReflectInvokeOwner", "run"))->invoke(new EvalReflectInvokeOther()); - echo "bad"; -} catch (ReflectionException $e) { - echo "caught"; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "caught"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionMethod/Property::setAccessible are PHP-compatible no-ops. -#[test] -fn execute_program_reflection_set_accessible_is_noop() { - let program = parse_fragment( - br#"class EvalReflectAccessTarget { - private $secret = "s"; - private function hidden() { - return $this->secret; - } -} -$object = new EvalReflectAccessTarget(); -$method = new ReflectionMethod("EvalReflectAccessTarget", "hidden"); -echo is_null($method->setAccessible(false)) ? "M" : "m"; echo ":"; -echo $method->invoke($object); echo ":"; -$property = new ReflectionProperty("EvalReflectAccessTarget", "secret"); -echo is_null($property->setAccessible(accessible: true)) ? "P" : "p"; echo ":"; -echo $property->getValue($object); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "M:s:P:s"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClass::newInstanceWithoutConstructor skips eval constructors. -#[test] -fn execute_program_reflection_class_new_instance_without_constructor_allocates_eval_class() { - let program = parse_fragment( - br#"class EvalReflectNoCtorTarget { - public $label = "default"; - private $secret = "hidden"; - public function __construct() { - $this->label = "ctor"; - } - public function label() { - return $this->label; - } - public function secret() { - return $this->secret; - } -} -$ref = new ReflectionClass("EvalReflectNoCtorTarget"); -$without = $ref->newInstanceWithoutConstructor(); -echo $without->label(); echo ":"; -echo $without->secret(); echo ":"; -$with = $ref->newInstance(); -echo $with->label(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "default:hidden:ctor"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. -#[test] -fn execute_program_reflects_eval_constant_and_enum_case_attributes() { - let program = parse_fragment( - br#"class EvalConstMarker { - public $name; - public function __construct($name) { - $this->name = $name; - } - public function label() { - return $this->name; - } -} -class EvalConstReflectTarget { - #[EvalConstMarker("const")] - final public const ANSWER = 42; -} -enum EvalCaseReflectTarget: string { - #[EvalConstMarker("case")] - case Ready = "ready"; -} -$const_attrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); -echo count($const_attrs); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName(); echo ":"; -echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isFinal() ? "F" : "f"; echo ":"; -echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isEnumCase() ? "enum" : "plain"; echo ":"; -echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getValue(); echo ":"; -echo ((new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "E" : "e"; echo ":"; -echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->isEnumCase() ? "enum" : "plain"; echo ":"; -echo $const_attrs[0]->getName(); echo ":"; echo $const_attrs[0]->getArguments()[0]; echo ":"; -echo $const_attrs[0]->newInstance()->label(); echo ":"; -$case_attrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); -echo count($case_attrs); echo ":"; echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; -echo $case_attrs[0]->getName(); echo ":"; echo $case_attrs[0]->getArguments()[0]; echo ":"; -$unit_attrs = (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); -echo (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; -echo ((new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "unit" : "bad"; echo ":"; -echo $unit_attrs[0]->newInstance()->label(); echo ":"; -$backed_attrs = (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); -echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; -echo ((new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "backed" : "bad"; echo ":"; -echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getBackingValue(); echo ":"; -echo $backed_attrs[0]->newInstance()->label(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "1:ANSWER:F:plain:42:E:enum:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:unit:case:Ready:backed:ready:case" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClassConstant and enum case metadata expose PHP's untyped defaults. -#[test] -fn execute_program_reflects_eval_constant_type_metadata_defaults() { - let program = parse_fragment( - br#"class EvalConstTypeTarget { - public const ANSWER = 42; -} -enum EvalConstTypeEnum: string { - case Ready = "ready"; -} -$constant = new ReflectionClassConstant("EvalConstTypeTarget", "ANSWER"); -echo $constant->isDeprecated() ? "D" : "d"; echo ":"; -echo $constant->hasType() ? "T" : "t"; echo ":"; -echo $constant->getType() === null ? "N" : "n"; echo ":"; -$case = new ReflectionClassConstant("EvalConstTypeEnum", "Ready"); -echo $case->isDeprecated() ? "D" : "d"; echo ":"; -echo $case->hasType() ? "T" : "t"; echo ":"; -echo $case->getType() === null ? "N" : "n"; echo ":"; -$unit = new ReflectionEnumUnitCase("EvalConstTypeEnum", "Ready"); -echo $unit->isDeprecated() ? "D" : "d"; echo ":"; -echo $unit->hasType() ? "T" : "t"; echo ":"; -echo $unit->getType() === null ? "N" : "n"; echo ":"; -$backed = new ReflectionEnumBackedCase("EvalConstTypeEnum", "Ready"); -echo $backed->isDeprecated() ? "D" : "d"; echo ":"; -echo $backed->hasType() ? "T" : "t"; echo ":"; -echo $backed->getType() === null ? "N" : "n"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "d:t:N:d:t:N:d:t:N:d:t:N"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionClassConstant and enum case objects stringify retained metadata. -#[test] -fn execute_program_reflects_eval_constant_to_string() { - let program = parse_fragment( - br#"class EvalConstStringTarget { - public const ANSWER = 42; - final protected const LIMIT = 7; - private const FLAG = true; - public const LABEL = "ok"; - public const NOTHING = null; -} -enum EvalConstStringEnum: string { - case Ready = "ready"; -} -foreach (["ANSWER", "LIMIT", "FLAG", "LABEL", "NOTHING"] as $name) { - echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringTarget", $name))->__toString()); - echo "|"; -} -echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringEnum", "Ready"))->__toString()); -echo "|"; -echo str_replace("\n", "\\n", (new ReflectionEnumUnitCase("EvalConstStringEnum", "Ready"))->__toString()); -echo "|"; -echo str_replace("\n", "\\n", (new ReflectionEnumBackedCase("EvalConstStringEnum", "Ready"))->__toString()); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Constant [ public int ANSWER ] { 42 }\\n|Constant [ final protected int LIMIT ] { 7 }\\n|Constant [ private bool FLAG ] { 1 }\\n|Constant [ public string LABEL ] { ok }\\n|Constant [ public null NOTHING ] { }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies enum-case reflection owners expose inherited constant metadata predicates. -#[test] -fn execute_program_reflects_enum_case_visibility_and_modifiers() { - let program = parse_fragment( - br#"enum EvalEnumCaseVisibility: string { - case Ready = "ready"; -} -$unit = new ReflectionEnumUnitCase("EvalEnumCaseVisibility", "Ready"); -$backed = new ReflectionEnumBackedCase("EvalEnumCaseVisibility", "Ready"); -foreach ([$unit, $backed] as $case) { - echo $case->isEnumCase() ? "E" : "e"; - echo $case->isPrivate() ? "R" : "r"; - echo $case->isProtected() ? "P" : "p"; - echo $case->isPublic() ? "U" : "u"; - echo $case->isFinal() ? "F" : "f"; - echo $case->getModifiers(); echo ":"; -} -echo ReflectionEnumUnitCase::IS_PUBLIC; echo ":"; -echo ReflectionEnumUnitCase::IS_PROTECTED; echo ":"; -echo ReflectionEnumUnitCase::IS_PRIVATE; echo ":"; -echo ReflectionEnumUnitCase::IS_FINAL; echo ":"; -echo ReflectionEnumBackedCase::IS_PUBLIC; echo ":"; -echo ReflectionEnumBackedCase::IS_PROTECTED; echo ":"; -echo ReflectionEnumBackedCase::IS_PRIVATE; echo ":"; -echo ReflectionEnumBackedCase::IS_FINAL; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "ErpUf1:ErpUf1:1:2:4:32:1:2:4:32"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionEnum exposes eval-declared enum cases and backing metadata. -#[test] -fn execute_program_reflects_eval_enum_owner_metadata() { - let program = parse_fragment( - br#"enum EvalReflectPure { - case Ready; - case Done; -} -enum EvalReflectBacked: string { - case Ready = "ready"; - case Done = "done"; -} -$pure = new ReflectionEnum("EvalReflectPure"); -echo $pure->getName(); echo ":"; -echo $pure->isEnum() ? "E" : "e"; echo ":"; -echo $pure->isBacked() ? "B" : "b"; echo ":"; -echo $pure->getBackingType() === null ? "N" : "n"; echo ":"; -echo $pure->hasCase("Ready") ? "R" : "r"; -echo $pure->hasCase("Missing") ? "M" : "m"; echo ":"; -$case = $pure->getCase("Done"); -echo $case->getName(); echo ":"; -echo $case->getEnum()->getName(); echo ":"; -$cases = $pure->getCases(); -echo count($cases); echo ":"; -echo $cases[0]->getName(); echo ":"; -echo $cases[1]->getEnum()->getName(); echo ":"; -$backed = new ReflectionEnum("EvalReflectBacked"); -$type = $backed->getBackingType(); -echo $backed->isBacked() ? "B" : "b"; echo ":"; -echo $type->getName(); echo ":"; -echo $type->isBuiltin() ? "I" : "i"; echo ":"; -$backed_case = $backed->getCase("Ready"); -echo $backed_case->getName(); echo ":"; -echo $backed_case->getBackingValue(); echo ":"; -echo $backed_case->getEnum()->isBacked() ? "E" : "e"; echo ":"; -$backed_cases = $backed->getCases(); -echo count($backed_cases); echo ":"; -echo $backed_cases[1]->getBackingValue(); echo ":"; -echo $backed_cases[0]->getEnum()->getBackingType()->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "EvalReflectPure:E:b:N:Rm:Done:EvalReflectPure:2:Ready:EvalReflectPure:B:string:I:Ready:ready:E:2:done:string" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionEnum construction throws catchable PHP reflection errors. -#[test] -fn execute_program_reflection_enum_constructor_throws_reflection_exceptions() { - let program = parse_fragment( - br#"class EvalReflectNotEnumClass {} -interface EvalReflectNotEnumIface {} -trait EvalReflectNotEnumTrait {} -enum EvalReflectActualEnum { - case Ready; -} -try { - new ReflectionEnum("EvalReflectNotEnumClass"); - echo "bad"; -} catch (ReflectionException $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionEnum("EvalReflectNotEnumIface"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionEnum("EvalReflectNotEnumTrait"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionEnum("EvalReflectMissingEnum"); - echo "bad"; -} catch (ReflectionException $e) { - echo $e->getMessage(); -} -echo "|"; -echo (new ReflectionEnum("EvalReflectActualEnum"))->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - assert!( - result.is_ok(), - "execute eval ir failed after output {:?}", - values.output - ); - - assert_eq!( - values.output, - "ReflectionException:Class \"EvalReflectNotEnumClass\" is not an enum|Class \"EvalReflectNotEnumIface\" is not an enum|Class \"EvalReflectNotEnumTrait\" is not an enum|Class \"EvalReflectMissingEnum\" does not exist|EvalReflectActualEnum" - ); - assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); -} - -/// Verifies ReflectionObject reflects the runtime class of an eval object instance. -#[test] -fn execute_program_reflection_object_reflects_eval_instances() { - let program = parse_fragment( - br#"class EvalReflectObjectBase { - public function inherited(): string { - return "base"; - } -} -class EvalReflectObjectChild extends EvalReflectObjectBase { - public int $count = 3; -} -$ref = new ReflectionObject(new EvalReflectObjectChild()); -echo get_class($ref); echo ":"; -echo ($ref instanceof ReflectionObject) ? "O" : "o"; -echo ($ref instanceof ReflectionClass) ? "C" : "c"; echo ":"; -echo $ref->getName(); echo ":"; -echo $ref->getParentClass()->getName(); echo ":"; -echo $ref->hasMethod("inherited") ? "M" : "m"; -echo $ref->hasProperty("count") ? "P" : "p"; echo ":"; -$object = $ref->newInstanceWithoutConstructor(); -echo get_class($object); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "ReflectionObject:OC:EvalReflectObjectChild:EvalReflectObjectBase:MP:EvalReflectObjectChild" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionObject lists dynamic public properties from the reflected instance. -#[test] -fn execute_program_reflection_object_lists_dynamic_properties() { - let program = parse_fragment( - br#"class EvalReflectObjectDynamicTarget { - public $declared = "declared"; -} -$object = new EvalReflectObjectDynamicTarget(); -$object->dynamic = "value"; -$ref = new ReflectionObject($object); -$properties = $ref->getProperties(); -foreach ($properties as $property) { - echo $property->getName(); echo ":"; - echo $property->isDynamic() ? "D" : "d"; echo "|"; -} -echo ":"; -$dynamic = $ref->getProperty("dynamic"); -echo $dynamic->isDynamic() ? "D" : "d"; echo ":"; -echo $dynamic->getValue($object); echo ":"; -echo count($ref->getProperties(ReflectionProperty::IS_PUBLIC)); echo ":"; -echo count($ref->getProperties(ReflectionProperty::IS_STATIC)); echo ":"; -echo $ref->hasProperty("dynamic") ? "H" : "h"; -echo $ref->hasProperty("declared") ? "D" : "d"; -echo $ref->hasProperty("missing") ? "M" : "m"; -echo (new ReflectionClass($object))->hasProperty("dynamic") ? "C" : "c"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "declared:d|dynamic:D|:D:value:2:0:HDmc"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies ReflectionObject constructor type errors are catchable. -#[test] -fn execute_program_reflection_object_constructor_throws_type_errors() { - let program = parse_fragment( - br#"try { - new ReflectionObject("EvalReflectObjectChild"); - echo "bad"; -} catch (TypeError $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -echo "|"; -try { - new ReflectionObject([]); - echo "bad"; -} catch (TypeError $e) { - echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - assert!( - result.is_ok(), - "execute eval ir failed after output {:?}", - values.output - ); - - assert_eq!( - values.output, - "TypeError:ReflectionObject::__construct(): Argument #1 ($object) must be of type object, string given|ReflectionObject::__construct(): Argument #1 ($object) must be of type object, array given" - ); - assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); -} - -/// Verifies unsupported attribute argument metadata remains name-visible but not materializable. -#[test] -fn execute_program_rejects_unsupported_class_attribute_args_metadata() { - for source in [ - br#"#[Tag($dynamic)] -class EvalUnsupportedAttr {} -$names = class_attribute_names("EvalUnsupportedAttr"); -echo count($names); echo ":"; echo $names[0]; echo ":"; -class_attribute_args("EvalUnsupportedAttr", "Tag");"# as &[u8], - br#"#[Tag(["fixed" => "ok", $dynamic => "bad"])] -class EvalUnsupportedAttr {} -$names = class_attribute_names("EvalUnsupportedAttr"); -echo count($names); echo ":"; echo $names[0]; echo ":"; -class_attribute_args("EvalUnsupportedAttr", "Tag");"#, - ] { - let program = parse_fragment(source).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("unsupported attribute metadata should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); - assert_eq!(values.output, "1:Tag:"); - } -} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/attributes.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/attributes.rs new file mode 100644 index 0000000000..732a93706d --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/attributes.rs @@ -0,0 +1,463 @@ +//! Purpose: +//! Interpreter tests for Reflection attributes, origins, sources, and prototypes. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Coverage includes eval-declared attributes and callable metadata provenance. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies class attribute helpers expose eval class-level metadata. +#[test] +fn execute_program_dispatches_class_attribute_metadata_builtins() { + let program = parse_fragment( + br#"class EvalAttrDep {} +#[Route("/home", -1, 1.5, true, null, EvalAttrDep::class, ["nested", 2])] +#[Tag("first"), Tag("second")] +class EvalAttrMeta {} +$names = class_attribute_names("EvalAttrMeta"); +echo count($names); echo ":"; echo $names[0]; echo ":"; echo $names[1]; echo ":"; echo $names[2]; echo ":"; +$args = class_attribute_args("EvalAttrMeta", "route"); +echo count($args); echo ":"; echo $args[0]; echo ":"; echo $args[1]; echo ":"; +echo $args[2]; echo ":"; echo $args[3] ? "T" : "F"; echo ":"; echo is_null($args[4]) ? "N" : "bad"; echo ":"; +echo $args[5]; echo ":"; +echo count($args[6]); echo ":"; echo $args[6][0]; echo ":"; echo $args[6][1]; echo ":"; +$tag = class_attribute_args("evalattrmeta", "Tag"); +echo $tag[0]; echo ":"; +$missing = class_attribute_args("EvalAttrMeta", "Missing"); +echo count($missing); echo ":"; +$attrs = class_get_attributes("EvalAttrMeta"); +echo count($attrs); echo ":"; echo $attrs[0]->getName(); echo ":"; +$attr_args = $attrs[0]->getArguments(); +echo count($attr_args); echo ":"; echo $attr_args[0]; echo ":"; echo $attr_args[1]; echo ":"; +echo $attr_args[2]; echo ":"; echo $attr_args[3] ? "T" : "F"; echo ":"; echo is_null($attr_args[4]) ? "N" : "bad"; echo ":"; +echo $attr_args[5]; echo ":"; +echo count($attr_args[6]); echo ":"; echo $attr_args[6][0]; echo ":"; echo $attr_args[6][1]; echo ":"; +$tag_attr_args = $attrs[1]->getArguments(); +echo $attrs[1]->getName(); echo ":"; echo $tag_attr_args[0]; echo ":"; +echo is_null($attrs[0]->newInstance()) ? "N" : "bad"; echo ":"; +$call_names = call_user_func("class_attribute_names", "EvalAttrMeta"); +echo $call_names[0]; echo ":"; +$call_args = call_user_func_array( + "class_attribute_args", + ["class_name" => "EvalAttrMeta", "attribute_name" => "Route"] +); +echo $call_args[0]; echo ":"; +echo function_exists("class_attribute_names"); echo function_exists("class_get_attributes"); +echo function_exists("class_attribute_args"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "3:Route:Tag:Tag:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:first:0:3:Route:7:/home:-1:1.5:T:N:EvalAttrDep:2:nested:2:Tag:first:N:Route:/home:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval class attribute metadata preserves named literal arguments. +#[test] +fn execute_program_dispatches_named_class_attribute_args() { + let program = parse_fragment( + br#"#[Route(path: "/eval", secure: true, code: 9)] +class EvalNamedAttrMeta {} +$args = class_attribute_args("EvalNamedAttrMeta", "Route"); +echo count($args); echo ":"; +echo $args["path"]; echo ":"; +echo $args["secure"] ? "T" : "F"; echo ":"; +echo $args["code"]; echo ":"; +$attrs = class_get_attributes("EvalNamedAttrMeta"); +$attr_args = $attrs[0]->getArguments(); +echo $attr_args["path"]; echo ":"; +echo $attr_args["secure"] ? "T" : "F"; echo ":"; +echo $attr_args["code"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:/eval:T:9:/eval:T:9"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionAttribute::newInstance instantiates eval-declared attribute classes. +#[test] +fn execute_program_instantiates_eval_declared_reflection_attribute() { + let program = parse_fragment( + br#"class EvalRoute { + public $path; + public $code; + public $enabled; + public function __construct($path, $code, $enabled) { + $this->path = $path; + $this->code = $code; + $this->enabled = $enabled; + } + public function summary() { + return $this->path . ":" . $this->code . ":" . ($this->enabled ? "T" : "F"); + } +} +#[EvalRoute(enabled: true, code: -7, path: "/home")] +class EvalRouteTarget {} +$attrs = class_get_attributes("EvalRouteTarget"); +$instance = $attrs[0]->newInstance(); +echo get_class($instance); echo ":"; echo $instance->summary(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalRoute:/home:-7:T"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass/Method/Property expose eval-declared attribute metadata. +#[test] +fn execute_program_reflects_eval_member_attributes() { + let program = parse_fragment( + br#"class EvalMarker { + public $name; + public function __construct($name) { + $this->name = $name; + } + public function label() { + return $this->name; + } +} +#[EvalMarker("class")] +class EvalReflectTarget { + #[EvalMarker("method")] + public function handle() {} + #[EvalMarker("property")] + public $id; +} +$class_attrs = (new ReflectionClass("EvalReflectTarget"))->getAttributes(); +echo count($class_attrs); echo ":"; echo (new ReflectionClass("EvalReflectTarget"))->getName(); echo ":"; +echo $class_attrs[0]->getName(); echo ":"; echo $class_attrs[0]->newInstance()->label(); echo ":"; +$method_attrs = (new ReflectionMethod("EvalReflectTarget", "handle"))->getAttributes(); +echo count($method_attrs); echo ":"; echo (new ReflectionMethod("EvalReflectTarget", "handle"))->getName(); echo ":"; +echo $method_attrs[0]->getName(); echo ":"; +echo $method_attrs[0]->getArguments()[0]; echo ":"; echo $method_attrs[0]->newInstance()->label(); echo ":"; +$property_attrs = (new ReflectionProperty("EvalReflectTarget", "id"))->getAttributes(); +echo count($property_attrs); echo ":"; echo (new ReflectionProperty("EvalReflectTarget", "id"))->getName(); echo ":"; +echo $property_attrs[0]->getName(); echo ":"; +echo $property_attrs[0]->getArguments()[0]; echo ":"; echo $property_attrs[0]->newInstance()->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:EvalReflectTarget:EvalMarker:class:1:handle:EvalMarker:method:method:1:id:EvalMarker:property:property" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionAttribute reports target bitmasks and repeated-owner metadata. +#[test] +fn execute_program_reflection_attribute_reports_target_and_repetition() { + let program = parse_fragment( + br#"class EvalTargetMarker { + public function __construct($name = null) {} +} +#[EvalTargetMarker("class-a"), EvalTargetMarker("class-b")] +class EvalReflectAttributeTarget { + #[EvalTargetMarker("method")] + public function run(#[EvalTargetMarker("param")] $id) {} + #[EvalTargetMarker("property")] + public $id; + #[EvalTargetMarker("const")] + public const ANSWER = 42; +} +enum EvalReflectAttributeEnum { + #[EvalTargetMarker("case")] + case Ready; +} +$class_attrs = (new ReflectionClass("EvalReflectAttributeTarget"))->getAttributes(); +echo $class_attrs[0]->getTarget(); echo "/"; +echo $class_attrs[0]->isRepeated() ? "R" : "r"; echo ":"; +echo $class_attrs[1]->getTarget(); echo "/"; +echo $class_attrs[1]->isRepeated() ? "R" : "r"; echo ":"; +$method_attr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getAttributes()[0]; +echo $method_attr->getTarget(); echo "/"; +echo $method_attr->isRepeated() ? "R" : "r"; echo ":"; +$property_attr = (new ReflectionProperty("EvalReflectAttributeTarget", "id"))->getAttributes()[0]; +echo $property_attr->getTarget(); echo "/"; +echo $property_attr->isRepeated() ? "R" : "r"; echo ":"; +$param_attr = (new ReflectionMethod("EvalReflectAttributeTarget", "run"))->getParameters()[0]->getAttributes()[0]; +echo $param_attr->getTarget(); echo "/"; +echo $param_attr->isRepeated() ? "R" : "r"; echo ":"; +$const_attr = (new ReflectionClassConstant("EvalReflectAttributeTarget", "ANSWER"))->getAttributes()[0]; +echo $const_attr->getTarget(); echo "/"; +echo $const_attr->isRepeated() ? "R" : "r"; echo ":"; +$case_attr = (new ReflectionEnumUnitCase("EvalReflectAttributeEnum", "Ready"))->getAttributes()[0]; +echo $case_attr->getTarget(); echo "/"; +echo $case_attr->isRepeated() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1/R:1/R:4/r:8/r:32/r:16/r:16/r"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies reflection owner origin metadata APIs report eval user-defined defaults. +#[test] +fn execute_program_reflection_owners_report_origin_metadata_defaults() { + let program = parse_fragment( + br#"class EvalReflectOriginTarget { + public $id; + public const ANSWER = 42; + public function run() {} +} +enum EvalReflectOriginCase: string { + case Ready = "ready"; +} +$class = new ReflectionClass("EvalReflectOriginTarget"); +$method = new ReflectionMethod("EvalReflectOriginTarget", "run"); +$property = new ReflectionProperty("EvalReflectOriginTarget", "id"); +$constant = new ReflectionClassConstant("EvalReflectOriginTarget", "ANSWER"); +$unit = new ReflectionEnumUnitCase("EvalReflectOriginCase", "Ready"); +$backed = new ReflectionEnumBackedCase("EvalReflectOriginCase", "Ready"); +echo ($class->getDocComment() === false) ? "C" : "c"; echo ":"; +echo ($method->getDocComment() === false) ? "M" : "m"; echo ":"; +echo ($property->getDocComment() === false) ? "P" : "p"; echo ":"; +echo ($constant->getDocComment() === false) ? "K" : "k"; echo ":"; +echo ($unit->getDocComment() === false) ? "U" : "u"; echo ":"; +echo ($backed->getDocComment() === false) ? "B" : "b"; echo ":"; +echo ($class->getExtensionName() === false) ? "E" : "e"; echo ":"; +echo ($method->getExtensionName() === false) ? "N" : "n"; echo ":"; +echo ($class->getExtension() === null) ? "X" : "x"; echo ":"; +echo ($method->getExtension() === null) ? "Y" : "y"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "C:M:P:K:U:B:E:N:X:Y"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass and ReflectionMethod report eval source-location metadata. +#[test] +fn execute_program_reflection_class_and_method_report_source_location() { + let program = parse_fragment( + br#"class EvalReflectSource { + public function run() { + return 1; + } +} +interface EvalReflectSourceIface { + public function iface(); +} +$class = new ReflectionClass("EvalReflectSource"); +$method = new ReflectionMethod("EvalReflectSource", "run"); +$iface = new ReflectionClass("EvalReflectSourceIface"); +$ifaceMethod = new ReflectionMethod("EvalReflectSourceIface", "iface"); +echo $class->getFileName(); echo ":"; +echo $class->getStartLine(); echo ":"; echo $class->getEndLine(); echo ":"; +echo $method->getStartLine(); echo ":"; echo $method->getEndLine(); echo ":"; +echo $iface->getStartLine(); echo ":"; echo $iface->getEndLine(); echo ":"; +echo $ifaceMethod->getStartLine(); echo ":"; echo $ifaceMethod->getEndLine(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + context.set_call_site("/tmp/eval-class-source.php", "/tmp", 23); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.output, + "/tmp/eval-class-source.php(23) : eval()'d code:1:5:2:4:6:8:7:7" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod exposes PHP-compatible name and origin predicate metadata. +#[test] +fn execute_program_reflection_method_reports_name_and_origin_predicates() { + let program = parse_fragment( + br#"namespace EvalReflectMethodNs; +class Target { + public function run(...$items) {} + public static function stat() {} +} +$ref = new \ReflectionMethod(Target::class, "run"); +echo $ref->getShortName(); echo ":"; +echo $ref->getNamespaceName(); echo ":"; +echo $ref->inNamespace() ? "Y" : "N"; echo ":"; +echo $ref->isInternal() ? "I" : "i"; +echo $ref->isUserDefined() ? "U" : "u"; echo ":"; +echo $ref->isClosure() ? "C" : "c"; echo ":"; +echo $ref->isDeprecated() ? "D" : "d"; echo ":"; +echo $ref->isStatic() ? "S" : "s"; echo ":"; +echo $ref->returnsReference() ? "R" : "r"; echo ":"; +echo $ref->hasReturnType() ? "T" : "t"; echo ":"; +echo $ref->getReturnType() === null ? "N" : "n"; echo ":"; +echo $ref->isGenerator() ? "G" : "g"; echo ":"; +echo $ref->isVariadic() ? "V" : "v"; echo ":"; +echo $ref->hasTentativeReturnType() ? "H" : "h"; echo ":"; +echo $ref->getTentativeReturnType() === null ? "Q" : "q"; echo ":"; +echo count($ref->getClosureUsedVariables()); echo ":"; +echo $ref->getClosureThis() === null ? "T" : "t"; echo ":"; +echo $ref->getClosureScopeClass() === null ? "S" : "s"; echo ":"; +echo $ref->getClosureCalledClass() === null ? "L" : "l"; echo ":"; +$static = new \ReflectionMethod(Target::class, "stat"); +echo $static->isStatic() ? "S" : "s"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "run::N:iU:c:d:s:r:t:N:g:V:h:Q:0:T:S:L:S"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod derives `isDeprecated()` from eval-retained attributes. +#[test] +fn execute_program_reflection_method_reports_deprecated_attribute() { + let program = parse_fragment( + br#"class EvalReflectDeprecatedMethodTarget { + #[\Deprecated] + public function old() {} + public function fresh() {} +} +$deprecated = new ReflectionMethod(EvalReflectDeprecatedMethodTarget::class, "old"); +$plain = new ReflectionMethod(EvalReflectDeprecatedMethodTarget::class, "fresh"); +echo $deprecated->isDeprecated() ? "D" : "d"; echo ":"; +echo $plain->isDeprecated() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "D:d"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod exposes eval static locals using the declaring class key. +#[test] +fn execute_program_reflection_method_reports_static_variables() { + let program = parse_fragment( + br#"class EvalReflectMethodStaticBase { + public function tick() { + static $count = 3; + static $label = "method"; + $count = $count + 1; + return $count; + } +} +class EvalReflectMethodStaticChild extends EvalReflectMethodStaticBase {} +$object = new EvalReflectMethodStaticChild(); +$ref = new ReflectionMethod("EvalReflectMethodStaticChild", "tick"); +$before = $ref->getStaticVariables(); +echo $before["count"]; echo ":"; echo $before["label"]; echo ":"; +echo $ref->invoke($object); echo ":"; +$after = $ref->getStaticVariables(); +echo $after["count"]; echo ":"; echo $after["label"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:method:4:4:method"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod exposes eval parent and interface prototypes. +#[test] +fn execute_program_reflection_method_reports_eval_prototypes() { + let program = parse_fragment( + br#"interface EvalProtoParentIface { + public function parented(); +} +interface EvalProtoChildIface extends EvalProtoParentIface {} +interface EvalProtoIface { + public function iface(); +} +class EvalProtoBase { + public function run() {} + public function inherited() {} +} +class EvalProtoChild extends EvalProtoBase implements EvalProtoIface, EvalProtoChildIface { + public function run() {} + public function iface() {} + public function parented() {} + public function own() {} +} +$override = new ReflectionMethod("EvalProtoChild", "run"); +$overrideProto = $override->getPrototype(); +echo $override->hasPrototype() ? "Y" : "N"; echo ":"; +echo $overrideProto->getDeclaringClass()->getName(); echo "::"; +echo $overrideProto->getName(); echo ":"; +$iface = new ReflectionMethod("EvalProtoChild", "iface"); +$ifaceProto = $iface->getPrototype(); +echo $iface->hasPrototype() ? "Y" : "N"; echo ":"; +echo $ifaceProto->getDeclaringClass()->getName(); echo "::"; +echo $ifaceProto->getName(); echo ":"; +$parentIface = new ReflectionMethod("EvalProtoChild", "parented"); +$parentIfaceProto = $parentIface->getPrototype(); +echo $parentIfaceProto->getDeclaringClass()->getName(); echo "::"; +echo $parentIfaceProto->getName(); echo ":"; +$own = new ReflectionMethod("EvalProtoChild", "own"); +echo $own->hasPrototype() ? "Y" : "N"; echo ":"; +try { + $own->getPrototype(); +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +$inherited = new ReflectionMethod("EvalProtoChild", "inherited"); +echo $inherited->hasPrototype() ? "Y" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Y:EvalProtoBase::run:Y:EvalProtoIface::iface:EvalProtoParentIface::parented:N:E:N" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/callable_types.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/callable_types.rs new file mode 100644 index 0000000000..b8d05ec0f1 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/callable_types.rs @@ -0,0 +1,403 @@ +//! Purpose: +//! Interpreter tests for reflected callable parameters, types, and string formatting. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Method, parameter, property, and composite type metadata stay PHP-compatible. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionMethod exposes eval method parameter objects with names and positions. +#[test] +fn execute_program_reflects_eval_method_parameters() { + let program = parse_fragment( +br##"interface EvalReflectLeft {} +interface EvalReflectRight {} +class EvalReflectParamTarget { + public function run(#[EvalParamTag("first")] int &$first, int|string $union, #[EvalParamTag("both")] EvalReflectLeft&EvalReflectRight $both, ?array $items = null, ?callable $callback = null, \App\Name|null $second = null, &...$rest) {} +} +$method = new ReflectionMethod("EvalReflectParamTarget", "run"); +echo $method->getNumberOfParameters(); echo "/"; +echo $method->getNumberOfRequiredParameters(); echo ":"; +$params = $method->getParameters(); +foreach ($params as $param) { + echo $param->getName(); echo "#"; echo $param->getPosition(); + echo $param->isOptional() ? "O" : "r"; + echo $param->isVariadic() ? "V" : "v"; + echo $param->isPassedByReference() ? "R" : "b"; + echo $param->canBePassedByValue() ? "Y" : "N"; + echo $param->hasType() ? "T" : "t"; + echo $param->allowsNull() ? "N" : "n"; + echo $param->isArray() ? "A" : "a"; + echo $param->isCallable() ? "C" : "c"; + $type = $param->getType(); + if ($param->getName() == "union") { + echo ":union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($param->getName() == "both") { + echo ":intersection"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { + echo ":"; echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo ":null"; + } + $attrs = $param->getAttributes(); + echo ":A"; echo count($attrs); + if (count($attrs) > 0) { + echo ":"; echo $attrs[0]->getName(); + echo ":"; echo $attrs[0]->getArguments()[0]; + } + echo $param->isDefaultValueAvailable() ? ":D" : ":d"; + if ($param->isDefaultValueAvailable()) { + echo "="; + echo $param->getDefaultValue() === null ? "null" : $param->getDefaultValue(); + } + echo "|"; +} +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "7/3:first#0rvRNTnac:int!B:A1:EvalParamTag:first:d|union#1rvbYTnac:union!:intB:stringB:A0:d|both#2rvbYTnac:intersection!:EvalReflectLeftC:EvalReflectRightC:A1:EvalParamTag:both:d|items#3OvbYTNAc:array?B:A0:D=null|callback#4OvbYTNaC:callable?B:A0:D=null|second#5OvbYTNac:App\\Name?C:A0:D=null|rest#6OVRNtNac:null:A0:d|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionType objects stringify retained eval parameter metadata. +#[test] +fn execute_program_reflection_type_to_string() { + let program = parse_fragment( +br##"class EvalReflectTypeStringDep {} +interface EvalReflectTypeStringLeft {} +interface EvalReflectTypeStringRight {} +class EvalReflectTypeStringTarget { + public function run(?EvalReflectTypeStringDep $dep, int|string|null $union, EvalReflectTypeStringLeft&EvalReflectTypeStringRight $both, mixed $mixed, ?array $items) {} +} +$params = (new ReflectionMethod("EvalReflectTypeStringTarget", "run"))->getParameters(); +foreach ($params as $param) { + $type = $param->getType(); + echo $param->getName(); echo ":"; + echo $type->__toString(); echo "|"; +} +$unionType = $params[1]->getType(); +echo "cast:" . (string)$unionType . "|"; +echo "concat:" . $unionType . "|"; +echo "echo:"; +echo $unionType; +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "dep:?EvalReflectTypeStringDep|union:int|string|null|both:EvalReflectTypeStringLeft&EvalReflectTypeStringRight|mixed:mixed|items:?array|cast:int|string|null|concat:int|string|null|echo:int|string|null" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionParameter formats retained eval parameter metadata through `__toString()`. +#[test] +fn execute_program_reflection_parameter_to_string() { + let program = parse_fragment( + br#"class EvalReflectParameterStringTarget { + const LABEL = "L"; + public function run(string $name, int $count = 3, $label = self::LABEL, &...$items) {} +} +$params = (new ReflectionMethod("EvalReflectParameterStringTarget", "run"))->getParameters(); +foreach ($params as $param) { + echo $param->__toString(); + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Parameter #0 [ string $name ]|Parameter #1 [ int $count = 3 ]|Parameter #2 [ $label = self::LABEL ]|Parameter #3 [ &...$items ]|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod exposes eval-declared return type metadata. +#[test] +fn execute_program_reflection_method_reports_return_type_metadata() { + let program = parse_fragment( + br#"interface EvalReflectReturnIface { + public function read(): string; +} +class EvalReflectReturnTarget implements EvalReflectReturnIface { + public function read(): string { return "ok"; } + public function selfReturn(): static { return $this; } + public function done(): void {} +} +$iface = new ReflectionMethod("EvalReflectReturnIface", "read"); +$ifaceType = $iface->getReturnType(); +echo $iface->hasReturnType() ? "I" : "i"; echo ":"; +echo $ifaceType->getName(); echo ":"; +echo $ifaceType->isBuiltin() ? "B" : "b"; echo ":"; +$self = (new ReflectionMethod("EvalReflectReturnTarget", "selfReturn"))->getReturnType(); +echo $self->getName(); echo ":"; +echo $self->isBuiltin() ? "B" : "b"; echo ":"; +$void = (new ReflectionMethod("EvalReflectReturnTarget", "done"))->getReturnType(); +echo $void->getName(); echo ":"; +echo $void->allowsNull() ? "N" : "n"; echo ":"; +echo $void->isBuiltin() ? "B" : "b"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "I:string:B:static:b:void:n:B"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod formats retained eval method metadata through `__toString()`. +#[test] +fn execute_program_reflection_method_to_string() { + let program = parse_fragment( + br#"class EvalReflectMethodStringTarget { + final public static function run(?int $id, string $label = "ok"): ?string { + return $label; + } +} +$ref = new ReflectionMethod("EvalReflectMethodStringTarget", "run"); +echo str_replace("\n", "|", $ref->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Method [ final static public method run ] {| - Parameters [2] {| Parameter #0 [ ?int $id ]| Parameter #1 [ string $label = 'ok' ]| }| - Return [ ?string ]|}|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionParameter reports eval constructor-promotion metadata. +#[test] +fn execute_program_reflection_parameter_reports_eval_promoted_metadata() { + let program = parse_fragment( + br#"class EvalReflectPromotedParamTarget { + public function __construct(public int $id, string $name = "Ada") {} + public function run(int $id) {} +} +$ctorParams = (new ReflectionMethod("EvalReflectPromotedParamTarget", "__construct"))->getParameters(); +$runParams = (new ReflectionMethod("EvalReflectPromotedParamTarget", "run"))->getParameters(); +echo $ctorParams[0]->isPromoted() ? "I" : "i"; +echo $ctorParams[1]->isPromoted() ? "N" : "n"; +echo $runParams[0]->isPromoted() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Inr"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty exposes eval property get/set type metadata. +#[test] +fn execute_program_reflection_property_get_type_metadata() { + let program = parse_fragment( + br##"class EvalReflectPropertyTypeDep {} +class EvalReflectPropertyTypeTarget { + public int $id; + public ?string $name; + public EvalReflectPropertyTypeDep $dep; + public $plain; + public int|string $union; +} +$properties = (new ReflectionClass("EvalReflectPropertyTypeTarget"))->getProperties(); +foreach ($properties as $property) { + echo $property->getName(); echo ":"; + echo $property->hasType() ? "T:" : "t:"; + $type = $property->getType(); + if ($property->getName() == "union") { + echo "union"; + echo $type->allowsNull() ? "?" : "!"; + foreach ($type->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; + } + } elseif ($type) { + echo $type->getName(); + echo $type->allowsNull() ? "?" : "!"; + echo $type->isBuiltin() ? "B" : "C"; + } else { + echo "null"; + } + echo "|"; +} +$direct = new ReflectionProperty("EvalReflectPropertyTypeTarget", "dep"); +$directType = $direct->getType(); +echo "direct:"; echo $direct->hasType() ? "T:" : "t:"; +echo $directType->getName(); +$directSettableType = $direct->getSettableType(); +echo ":set:"; echo $directSettableType->getName(); +$plain = new ReflectionProperty("EvalReflectPropertyTypeTarget", "plain"); +echo ":plainSet:"; echo $plain->getSettableType() === null ? "N" : "n"; +$directUnion = new ReflectionProperty("EvalReflectPropertyTypeTarget", "union"); +echo ":unionSet:"; echo count($directUnion->getSettableType()->getTypes()); +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "id:T:int!B|name:T:string?B|dep:T:EvalReflectPropertyTypeDep!C|plain:t:null|union:T:union!:intB:stringB|direct:T:EvalReflectPropertyTypeDep:set:EvalReflectPropertyTypeDep:plainSet:N:unionSet:2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty retains explicit set-hook parameter metadata as the settable type. +#[test] +fn execute_program_reflection_property_get_settable_type_uses_set_hook_parameter() { + let program = parse_fragment( + br##"class EvalReflectSettableTypeTarget { + public string $value { + get => $this->value; + set(int|string $raw) => (string) $raw; + } +} +$property = new ReflectionProperty("EvalReflectSettableTypeTarget", "value"); +$type = $property->getType(); +$settable = $property->getSettableType(); +echo $type->getName(); echo ":"; +echo count($settable->getTypes()); +foreach ($settable->getTypes() as $memberType) { + echo ":"; echo $memberType->getName(); + echo $memberType->isBuiltin() ? "B" : "C"; +} +$setHook = $property->getHook(PropertyHookType::Set); +$paramType = $setHook->getParameters()[0]->getType(); +echo ":"; echo count($paramType->getTypes()); +$box = new EvalReflectSettableTypeTarget(); +$box->value = 7; +echo ":"; echo $box->value; +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "string:2:intB:stringB:2:7"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty exposes eval property default metadata. +#[test] +fn execute_program_reflection_property_get_default_value_metadata() { + let program = parse_fragment( + br#"class EvalReflectPropertyDefaultTarget { + public $implicit; + public int $typed; + public ?string $nullableTyped; + public $explicitNull = null; + public int $count = 7; + public static string $label = "ok"; +} + +foreach (["implicit", "typed", "nullableTyped", "explicitNull", "count", "label"] as $name) { + $property = new ReflectionProperty("EvalReflectPropertyDefaultTarget", $name); + echo $property->getName(); echo ":"; + echo $property->isDefault() ? "Y:" : "N:"; + echo $property->hasDefaultValue() ? "D:" : "d:"; + $value = $property->getDefaultValue(); + echo $value === null ? "null" : $value; + echo "|"; +} +$listed = (new ReflectionClass("EvalReflectPropertyDefaultTarget"))->getProperty("implicit"); +echo "listed:"; +echo $listed->isDefault() ? "Y:" : "N:"; +echo $listed->hasDefaultValue() ? "D:" : "d:"; +echo $listed->getDefaultValue() === null ? "null" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "implicit:Y:D:null|typed:Y:d:null|nullableTyped:Y:d:null|explicitNull:Y:D:null|count:Y:D:7|label:Y:D:ok|listed:Y:D:null" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty formats retained property metadata through `__toString()`. +#[test] +fn execute_program_reflection_property_to_string() { + let program = parse_fragment( + br#"class EvalReflectPropertyStringTarget { + public int $id = 7; + protected static string $label = "ok"; + private $implicit; + public $virtual { + get => 1; + } +} +foreach (["id", "label", "implicit", "virtual"] as $name) { + echo (new ReflectionProperty("EvalReflectPropertyStringTarget", $name))->__toString(); + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Property [ public int $id = 7 ]|Property [ protected static string $label = 'ok' ]|Property [ private $implicit = NULL ]|Property [ public $virtual ]|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_capabilities.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_capabilities.rs new file mode 100644 index 0000000000..319d923930 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_capabilities.rs @@ -0,0 +1,247 @@ +//! Purpose: +//! Interpreter tests for ReflectionClass capabilities and construction metadata. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Readonly, instantiable, cloneable, iterable, and origin predicates are covered. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClass exposes eval readonly class metadata. +#[test] +fn execute_program_reflects_eval_class_readonly_predicate() { + let program = parse_fragment( + br#"class EvalReadonlyPlain {} +readonly class EvalReadonlyReflect {} +final readonly class EvalReadonlyFinalReflect {} +enum EvalReadonlyEnumReflect { case Ready; } +interface EvalReadonlyIface {} +trait EvalReadonlyTrait {} +echo (new ReflectionClass("EvalReadonlyPlain"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyFinalReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyEnumReflect"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyIface"))->isReadOnly() ? "R" : "r"; +echo (new ReflectionClass("EvalReadonlyTrait"))->isReadOnly() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "rRRrrr"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes eval class instantiability metadata. +#[test] +fn execute_program_reflects_eval_class_instantiable_predicate() { + let program = parse_fragment( + br#"abstract class EvalInstAbstract {} +class EvalInstPublic {} +final class EvalInstFinal {} +class EvalInstPrivate { private function __construct() {} } +class EvalInstProtected { protected function __construct() {} } +interface EvalInstIface {} +trait EvalInstTrait {} +enum EvalInstEnum { case Ready; } +echo (new ReflectionClass("EvalInstAbstract"))->isInstantiable() ? "A" : "a"; +echo (new ReflectionClass("EvalInstPublic"))->isInstantiable() ? "B" : "b"; +echo (new ReflectionClass("EvalInstFinal"))->isInstantiable() ? "C" : "c"; +echo (new ReflectionClass("EvalInstPrivate"))->isInstantiable() ? "P" : "p"; +echo (new ReflectionClass("EvalInstProtected"))->isInstantiable() ? "R" : "r"; +echo (new ReflectionClass("EvalInstIface"))->isInstantiable() ? "I" : "i"; +echo (new ReflectionClass("EvalInstTrait"))->isInstantiable() ? "T" : "t"; +echo (new ReflectionClass("EvalInstEnum"))->isInstantiable() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "aBCprite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isAnonymous reports false for eval-declared named class-like symbols. +#[test] +fn execute_program_reflection_class_reports_named_classes_not_anonymous() { + let program = parse_fragment( + br#"class EvalNamedAnonymousReflect {} +interface EvalNamedAnonymousIface {} +trait EvalNamedAnonymousTrait {} +enum EvalNamedAnonymousEnum { case Ready; } +echo (new ReflectionClass("EvalNamedAnonymousReflect"))->isAnonymous() ? "C" : "c"; +echo (new ReflectionClass("EvalNamedAnonymousIface"))->isAnonymous() ? "I" : "i"; +echo (new ReflectionClass("EvalNamedAnonymousTrait"))->isAnonymous() ? "T" : "t"; +echo (new ReflectionClass("EvalNamedAnonymousEnum"))->isAnonymous() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "cite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isCloneable reports eval class clone metadata. +#[test] +fn execute_program_reflects_eval_class_cloneable_predicate() { + let program = parse_fragment( + br#"abstract class EvalCloneAbstract {} +class EvalClonePlain {} +final class EvalCloneFinal {} +class EvalClonePrivate { private function __clone() {} } +class EvalCloneProtected { protected function __clone() {} } +class EvalClonePublic { public function __clone() {} } +interface EvalCloneIface {} +trait EvalCloneTrait {} +enum EvalCloneEnum { case Ready; } +echo (new ReflectionClass("EvalCloneAbstract"))->isCloneable() ? "A" : "a"; +echo (new ReflectionClass("EvalClonePlain"))->isCloneable() ? "P" : "p"; +echo (new ReflectionClass("EvalCloneFinal"))->isCloneable() ? "F" : "f"; +echo (new ReflectionClass("EvalClonePrivate"))->isCloneable() ? "V" : "v"; +echo (new ReflectionClass("EvalCloneProtected"))->isCloneable() ? "R" : "r"; +echo (new ReflectionClass("EvalClonePublic"))->isCloneable() ? "U" : "u"; +echo (new ReflectionClass("EvalCloneIface"))->isCloneable() ? "I" : "i"; +echo (new ReflectionClass("EvalCloneTrait"))->isCloneable() ? "T" : "t"; +echo (new ReflectionClass("EvalCloneEnum"))->isCloneable() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "aPFvrUite"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isIterable reports eval Traversable-compatible class metadata. +#[test] +fn execute_program_reflects_eval_class_iterable_predicate() { + let program = parse_fragment( + br#"class EvalIterablePlain {} +abstract class EvalIterableAbstract implements Iterator {} +interface EvalIterableIface extends Iterator {} +trait EvalIterableTrait {} +enum EvalIterableEnum { case Ready; } +class EvalIterableIterator implements Iterator { + public function current() { return null; } + public function key() { return null; } + public function next() {} + public function valid() { return false; } + public function rewind() {} +} +class EvalIterableAggregate implements IteratorAggregate { + public function getIterator() { return $this; } +} +echo (new ReflectionClass("EvalIterablePlain"))->isIterable() ? "P" : "p"; +$iter = new ReflectionClass("EvalIterableIterator"); +echo $iter->isIterable() ? "I" : "i"; +echo $iter->isIterateable() ? "A" : "a"; +echo (new ReflectionClass("EvalIterableAggregate"))->isIterable() ? "G" : "g"; +echo (new ReflectionClass("EvalIterableAbstract"))->isIterable() ? "B" : "b"; +echo (new ReflectionClass("EvalIterableIface"))->isIterable() ? "F" : "f"; +echo (new ReflectionClass("EvalIterableEnum"))->isIterable() ? "E" : "e"; +echo (new ReflectionClass("EvalIterableTrait"))->isIterable() ? "H" : "h"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "pIAGbfeh"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass origin predicates report eval class-like symbols as user-defined. +#[test] +fn execute_program_reflects_eval_class_origin_predicates() { + let program = parse_fragment( + br#"class EvalOriginClass {} +interface EvalOriginIface {} +trait EvalOriginTrait {} +enum EvalOriginEnum { case Ready; } +function eval_reflect_origin($name) { + $r = new ReflectionClass($name); + echo $r->isInternal() ? "I" : "i"; + echo $r->isUserDefined() ? "U" : "u"; + echo ":"; +} +eval_reflect_origin("EvalOriginClass"); +eval_reflect_origin("EvalOriginIface"); +eval_reflect_origin("EvalOriginTrait"); +eval_reflect_origin("EvalOriginEnum"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "iU:iU:iU:iU:"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::getConstructor exposes eval constructor metadata. +#[test] +fn execute_program_reflection_class_get_constructor() { + let program = parse_fragment( + br#"class EvalCtorBase { + public function __construct($required, $optional = 2) {} +} +class EvalCtorChild extends EvalCtorBase {} +class EvalCtorPlain {} +interface EvalCtorInterface { + public function __construct($required); +} +trait EvalCtorTrait { + public function __construct($required, $optional = null, ...$rest) {} +} +$base = (new ReflectionClass("EvalCtorBase"))->getConstructor(); +echo $base->getName(); echo "/"; +echo $base->getNumberOfParameters(); echo "/"; +echo $base->getNumberOfRequiredParameters(); echo ":"; +$child = (new ReflectionClass("EvalCtorChild"))->getConstructor(); +echo $child->getName(); echo "/"; +echo $child->getNumberOfParameters(); echo "/"; +echo $child->getNumberOfRequiredParameters(); echo ":"; +$plain = (new ReflectionClass("EvalCtorPlain"))->getConstructor(); +echo $plain === null ? "null" : "bad"; echo ":"; +$interface = (new ReflectionClass("EvalCtorInterface"))->getConstructor(); +echo $interface->getName(); echo "/"; +echo $interface->getNumberOfParameters(); echo "/"; +echo $interface->getNumberOfRequiredParameters(); echo ":"; +$trait = (new ReflectionClass("EvalCtorTrait"))->getConstructor(); +echo $trait->getName(); echo "/"; +echo $trait->getNumberOfParameters(); echo "/"; +echo $trait->getNumberOfRequiredParameters(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "__construct/2/1:__construct/2/1:null:__construct/1/1:__construct/3/1" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_identity.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_identity.rs new file mode 100644 index 0000000000..9906789553 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_identity.rs @@ -0,0 +1,418 @@ +//! Purpose: +//! Interpreter tests for ReflectionClass identity, relations, and modifier flags. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Eval and AOT class-like targets are checked through the same APIs. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClass exposes eval class namespace-derived name parts. +#[test] +fn execute_program_reflects_eval_class_name_parts() { + let program = parse_fragment( + br#"namespace Eval\Ns; +class Thing {} +$ref = new \ReflectionClass("Eval\\Ns\\Thing"); +echo $ref->getName(); echo ":"; +echo $ref->getShortName(); echo ":"; +echo $ref->getNamespaceName(); echo ":"; +echo $ref->inNamespace() ? "Y" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Eval\\Ns\\Thing:Thing:Eval\\Ns:Y"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes eval interface and trait relation names. +#[test] +fn execute_program_reflects_eval_class_relation_names() { + let program = parse_fragment( + br#"interface EvalRelationIface {} +trait EvalRelationTrait { + public function primary() {} +} +trait EvalRelationOtherTrait { + public function other() {} +} +class EvalRelationTarget implements EvalRelationIface { + use EvalRelationTrait, EvalRelationOtherTrait { + EvalRelationTrait::primary as relationAlias; + EvalRelationOtherTrait::other as private hiddenOther; + EvalRelationOtherTrait::other as protected; + } +} +class EvalRelationInherited extends EvalRelationTarget {} +interface EvalRelationParent {} +interface EvalRelationChild extends EvalRelationParent {} +$ref = new ReflectionClass("EvalRelationTarget"); +$interfaces = $ref->getInterfaceNames(); +$traits = $ref->getTraitNames(); +echo count($interfaces); echo ":"; echo $interfaces[0]; echo ":"; +echo count($traits); echo ":"; echo $traits[0]; echo ":"; echo $traits[1]; echo ":"; +$parentInterfaces = (new ReflectionClass("EvalRelationChild"))->getInterfaceNames(); +echo count($parentInterfaces); echo ":"; echo $parentInterfaces[0]; +$interfaceObjects = $ref->getInterfaces(); +echo ":"; echo count($interfaceObjects); echo ":"; echo $interfaceObjects["EvalRelationIface"]->getName(); +$traitObjects = $ref->getTraits(); +echo ":"; echo count($traitObjects); echo ":"; echo $traitObjects["EvalRelationTrait"]->getName(); echo ":"; echo $traitObjects["EvalRelationOtherTrait"]->getName(); +$parentInterfaceObjects = (new ReflectionClass("EvalRelationChild"))->getInterfaces(); +echo ":"; echo count($parentInterfaceObjects); echo ":"; echo $parentInterfaceObjects["EvalRelationParent"]->getName(); +$aliases = $ref->getTraitAliases(); +echo ":"; echo count($aliases); echo ":"; echo $aliases["relationAlias"]; echo ":"; echo $aliases["hiddenOther"]; +$inheritedAliases = (new ReflectionClass("EvalRelationInherited"))->getTraitAliases(); +echo ":"; echo count($inheritedAliases); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:1:EvalRelationIface:2:EvalRelationTrait:EvalRelationOtherTrait:1:EvalRelationParent:2:EvalRelationTrait::primary:EvalRelationOtherTrait::other:0" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass relation-name helpers read fake generated/AOT metadata. +#[test] +fn execute_program_reflects_aot_class_relation_names() { + let program = parse_fragment( + br#"$class_names = (new ReflectionClass("KnownClass"))->getInterfaceNames(); +echo count($class_names); echo ":"; echo $class_names[0]; echo ":"; +$interface_names = (new ReflectionClass("KnownInterface"))->getInterfaceNames(); +echo count($interface_names); echo ":"; echo $interface_names[0]; echo ":"; +$class_objects = (new ReflectionClass("KnownClass"))->getInterfaces(); +echo count($class_objects); echo ":"; echo $class_objects["KnownInterface"]->getName(); echo ":"; +$interface_objects = (new ReflectionClass("KnownInterface"))->getInterfaces(); +echo count($interface_objects); echo ":"; echo $interface_objects["Traversable"]->getName(); echo ":"; +$trait_names = (new ReflectionClass("KnownClass"))->getTraitNames(); +echo count($trait_names); echo ":"; echo $trait_names[0]; echo ":"; +$trait_objects = (new ReflectionClass("KnownClass"))->getTraits(); +echo count($trait_objects); echo ":"; echo $trait_objects["KnownTrait"]->getName(); echo ":"; +$nested_trait_names = (new ReflectionClass("KnownTrait"))->getTraitNames(); +echo count($nested_trait_names); echo ":"; echo $nested_trait_names[0]; echo ":"; +$aliases = (new ReflectionClass("KnownClass"))->getTraitAliases(); +echo count($aliases); echo ":"; echo $aliases["knownAlias"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:KnownInterface:1:Traversable:1:KnownInterface:1:Traversable:1:KnownTrait:1:KnownTrait:1:KnownInnerTrait:1:KnownTrait::source" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::implementsInterface reports eval class, enum, and +/// interface metadata using case-insensitive interface names. +#[test] +fn execute_program_reflects_eval_class_implements_interface_predicate() { + let program = parse_fragment( + br#"interface EvalImplBase {} +interface EvalImplChild extends EvalImplBase {} +class EvalImplTarget implements EvalImplChild {} +enum EvalImplEnum implements EvalImplBase { case Ready; } +trait EvalImplTrait {} +echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("EvalImplChild") ? "C" : "c"; +echo (new ReflectionClass("EvalImplTarget"))->implementsInterface("evalimplbase") ? "B" : "b"; +echo (new ReflectionClass("EvalImplEnum"))->implementsInterface("EvalImplBase") ? "E" : "e"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplChild") ? "I" : "i"; +echo (new ReflectionClass("EvalImplChild"))->implementsInterface("EvalImplBase") ? "P" : "p"; +echo (new ReflectionClass("EvalImplTrait"))->implementsInterface("EvalImplBase") ? "T" : "t"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "CBEIPt"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::implementsInterface checks fake generated/AOT relations. +#[test] +fn execute_program_reflects_aot_class_implements_interface_predicate() { + let program = parse_fragment( + br#"$ref = new ReflectionClass("KnownClass"); +echo $ref->implementsInterface("KnownInterface") ? "Y" : "N"; echo ":"; +echo $ref->implementsInterface("Iterator") ? "bad" : "N"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:N"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::implementsInterface rejects non-interface names with catchable errors. +#[test] +fn execute_program_reflection_class_implements_interface_rejects_non_interfaces() { + let program = parse_fragment( + br#"interface EvalImplRejectIface {} +class EvalImplRejectTarget {} +class EvalImplRejectClass {} +trait EvalImplRejectTrait {} +enum EvalImplRejectEnum { case Ready; } +$ref = new ReflectionClass("EvalImplRejectTarget"); +echo $ref->implementsInterface("EvalImplRejectIface") ? "T" : "F"; +try { + $ref->implementsInterface("EvalImplRejectClass"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectTrait"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectEnum"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +try { + $ref->implementsInterface("EvalImplRejectMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "F:ReflectionException:EvalImplRejectClass is not an interface:ReflectionException:EvalImplRejectTrait is not an interface:ReflectionException:EvalImplRejectEnum is not an interface:ReflectionException:Interface \"EvalImplRejectMissing\" does not exist" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isSubclassOf reports eval parent/interface metadata. +#[test] +fn execute_program_reflection_class_is_subclass_of_predicate() { + let program = parse_fragment( + br#"interface EvalSubclassIface {} +interface EvalSubclassChildIface extends EvalSubclassIface {} +class EvalSubclassBase {} +class EvalSubclassParent extends EvalSubclassBase {} +class EvalSubclassChild extends EvalSubclassParent implements EvalSubclassChildIface {} +trait EvalSubclassTrait {} +enum EvalSubclassEnum implements EvalSubclassIface { case Ready; } +$ref = new ReflectionClass("EvalSubclassChild"); +echo $ref->isSubclassOf("EvalSubclassParent") ? "P" : "p"; +echo $ref->isSubclassOf("evalsubclassbase") ? "B" : "b"; +echo $ref->isSubclassOf("EvalSubclassIface") ? "I" : "i"; +echo $ref->isSubclassOf("EvalSubclassChild") ? "S" : "s"; +echo (new ReflectionClass("EvalSubclassChildIface"))->isSubclassOf("EvalSubclassIface") ? "J" : "j"; +echo (new ReflectionClass("EvalSubclassIface"))->isSubclassOf("EvalSubclassIface") ? "X" : "x"; +echo $ref->isSubclassOf("EvalSubclassTrait") ? "T" : "t"; +echo $ref->isSubclassOf("EvalSubclassEnum") ? "Q" : "q"; +echo (new ReflectionClass("EvalSubclassEnum"))->isSubclassOf("EvalSubclassIface") ? "E" : "e"; +try { + $ref->isSubclassOf("EvalSubclassMissing"); + echo ":bad"; +} catch (ReflectionException $e) { + echo ":missing"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "PBIsJxtqE:missing"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::isInstance reports eval object class/interface metadata. +#[test] +fn execute_program_reflection_class_is_instance_predicate() { + let program = parse_fragment( + br#"interface EvalInstanceIface {} +class EvalInstanceBase {} +class EvalInstanceChild extends EvalInstanceBase implements EvalInstanceIface {} +trait EvalInstanceTrait {} +enum EvalInstanceEnum implements EvalInstanceIface { case Ready; } +$base = new ReflectionClass("EvalInstanceBase"); +$child = new ReflectionClass("EvalInstanceChild"); +$iface = new ReflectionClass("EvalInstanceIface"); +$trait = new ReflectionClass("EvalInstanceTrait"); +$enum = new ReflectionClass("EvalInstanceEnum"); +$childObj = new EvalInstanceChild(); +$objectRef = new ReflectionClass($childObj); +echo $objectRef->getName(); echo ":"; +echo $objectRef->getParentClass()->getName(); echo ":"; +echo $objectRef->isInstance($childObj) ? "O" : "o"; echo ":"; +echo $base->isInstance($childObj) ? "B" : "b"; +echo $child->isInstance(new EvalInstanceBase()) ? "C" : "c"; +echo $iface->isInstance($childObj) ? "I" : "i"; +echo $trait->isInstance($childObj) ? "T" : "t"; +echo $enum->isInstance(EvalInstanceEnum::Ready) ? "E" : "e"; +echo $iface->isInstance(EvalInstanceEnum::Ready) ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalInstanceChild:EvalInstanceBase:O:BcItEN"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes eval class-like final and abstract flags. +#[test] +fn execute_program_reflects_eval_class_modifier_flags() { + let program = parse_fragment( + br#"abstract class EvalAbstractReflect {} +final class EvalFinalReflect {} +interface EvalIfaceReflect {} +trait EvalTraitReflect {} +enum EvalEnumReflect { case Ready; } +echo (new ReflectionClass("EvalAbstractReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalAbstractReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalAbstractReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalAbstractReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalAbstractReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalFinalReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalFinalReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalFinalReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalFinalReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalFinalReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalEnumReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalEnumReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalEnumReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalEnumReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalEnumReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalIfaceReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalIfaceReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalIfaceReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalIfaceReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalIfaceReflect"))->isEnum() ? "E" : "e"; echo ":"; +echo (new ReflectionClass("EvalTraitReflect"))->isAbstract() ? "A" : "a"; +echo (new ReflectionClass("EvalTraitReflect"))->isFinal() ? "F" : "f"; +echo (new ReflectionClass("EvalTraitReflect"))->isInterface() ? "I" : "i"; +echo (new ReflectionClass("EvalTraitReflect"))->isTrait() ? "T" : "t"; +echo (new ReflectionClass("EvalTraitReflect"))->isEnum() ? "E" : "e"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Afite:aFite:aFitE:afIte:afiTe"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes PHP modifier bitmasks for eval class-like metadata. +#[test] +fn execute_program_reflects_eval_class_modifier_bitmask() { + let program = parse_fragment( + br#"abstract class EvalModifierAbstract {} +final class EvalModifierFinal {} +readonly class EvalModifierReadonly {} +final readonly class EvalModifierFinalReadonly {} +enum EvalModifierEnum { case Ready; } +interface EvalModifierIface {} +trait EvalModifierTrait {} +echo (new ReflectionClass("EvalModifierAbstract"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierFinal"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierReadonly"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierFinalReadonly"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierEnum"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierIface"))->getModifiers(); echo ":"; +echo (new ReflectionClass("EvalModifierTrait"))->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "64:32:65536:65568:32:0:0"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval can read built-in Reflection `IS_*` class constants. +#[test] +fn execute_program_reads_builtin_reflection_modifier_constants() { + let program = parse_fragment( + br#"echo ReflectionClass::IS_FINAL; echo ":"; +echo ReflectionClass::IS_EXPLICIT_ABSTRACT; echo ":"; +echo ReflectionClass::IS_READONLY; echo ":"; +echo ReflectionMethod::IS_STATIC; echo ":"; +echo ReflectionMethod::IS_PRIVATE; echo ":"; +echo ReflectionMethod::IS_ABSTRACT; echo ":"; +echo ReflectionProperty::IS_STATIC; echo ":"; +echo ReflectionProperty::IS_READONLY; echo ":"; +echo ReflectionProperty::IS_PUBLIC; echo ":"; +echo ReflectionProperty::IS_PROTECTED; echo ":"; +echo ReflectionProperty::IS_PRIVATE; echo ":"; +echo ReflectionProperty::IS_ABSTRACT; echo ":"; +echo ReflectionProperty::IS_PROTECTED_SET; echo ":"; +echo ReflectionProperty::IS_PRIVATE_SET; echo ":"; +echo ReflectionProperty::IS_VIRTUAL; echo ":"; +echo ReflectionProperty::IS_FINAL; echo ":"; +echo ReflectionClassConstant::IS_PUBLIC; echo ":"; +echo ReflectionClassConstant::IS_PROTECTED; echo ":"; +echo ReflectionClassConstant::IS_PRIVATE; echo ":"; +echo ReflectionClassConstant::IS_FINAL; echo ":"; +echo ReflectionEnumUnitCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumUnitCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumUnitCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumUnitCase::IS_FINAL; echo ":"; +echo ReflectionEnumBackedCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumBackedCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumBackedCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumBackedCase::IS_FINAL; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "32:64:65536:16:4:64:16:128:1:2:4:64:2048:4096:512:32:1:2:4:32:1:2:4:32:1:2:4:32" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_operations.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_operations.rs new file mode 100644 index 0000000000..a8999e6295 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/class_operations.rs @@ -0,0 +1,440 @@ +//! Purpose: +//! Interpreter tests for ReflectionClass member lists, instantiation, and invocation. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Object allocation and reflected method calls retain PHP visibility checks. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClass::getMethods preserves eval method parameter metadata. +#[test] +fn execute_program_reflection_class_lists_eval_method_parameters() { + let program = parse_fragment( + br#"class EvalReflectListedParamTarget { + public function first($left) {} + public function second($right, $tail) {} +} +$methods = (new ReflectionClass("EvalReflectListedParamTarget"))->getMethods(); +foreach ($methods as $method) { + $params = $method->getParameters(); + echo $method->getName(); echo ":"; + echo $method->getNumberOfParameters(); echo "/"; + echo $method->getNumberOfRequiredParameters(); + if (count($params) > 0) { + echo ":"; echo $params[0]->getName(); echo ":"; echo $params[0]->getPosition(); + } + echo "|"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "first:1/1:left:0|second:2/2:right:0|"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass getMethods/getProperties return eval member objects. +#[test] +fn execute_program_reflection_class_lists_eval_member_objects() { + let program = parse_fragment( + br#"#[Attribute] +class EvalListMarker {} +class EvalReflectListTarget { + #[EvalListMarker] + public function first() {} + private static function helper() {} + #[EvalListMarker] + protected $visible; + private static $token; +} +$ref = new ReflectionClass("EvalReflectListTarget"); +$methods = $ref->getMethods(); +$properties = $ref->getProperties(); +$staticMethods = $ref->getMethods(ReflectionMethod::IS_STATIC); +$privateMethods = $ref->getMethods(filter: ReflectionMethod::IS_PRIVATE); +$noMethods = $ref->getMethods(0); +$nullMethods = $ref->getMethods(null); +$staticProperties = $ref->getProperties(ReflectionProperty::IS_STATIC); +$protectedProperties = $ref->getProperties(filter: ReflectionProperty::IS_PROTECTED); +$noProperties = $ref->getProperties(0); +echo count($methods); echo ":"; echo count($properties); echo ":"; +echo ReflectionMethod::IS_STATIC; echo ":"; echo ReflectionMethod::IS_PRIVATE; echo ":"; +$direct = new ReflectionMethod("EvalReflectListTarget", "helper"); +echo "D"; echo $direct->getModifiers(); echo ":"; +foreach ($methods as $method) { + if ($method->getName() === "first") { + echo "F"; echo count($method->getAttributes()); + echo "M"; echo $method->getModifiers(); + } + if ($method->getName() === "helper") { + echo $method->isStatic() ? "S" : "s"; + echo $method->isPrivate() ? "R" : "r"; + echo "M"; echo $method->getModifiers(); + } +} +echo ":"; +foreach ($properties as $property) { + if ($property->getName() === "visible") { + echo "V"; echo count($property->getAttributes()); + echo $property->isProtected() ? "P" : "p"; + echo "M"; echo $property->getModifiers(); + } + if ($property->getName() === "token") { + echo $property->isStatic() ? "T" : "t"; + echo $property->isPrivate() ? "R" : "r"; + echo "M"; echo $property->getModifiers(); + } +} +echo ":"; +echo count($staticMethods); echo $staticMethods[0]->getName(); echo ":"; +echo count($privateMethods); echo $privateMethods[0]->getName(); echo ":"; +echo count($noMethods); echo ":"; echo count($nullMethods); echo ":"; +echo count($staticProperties); echo $staticProperties[0]->getName(); echo ":"; +echo count($protectedProperties); echo $protectedProperties[0]->getName(); echo ":"; +echo count($noProperties); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2:2:16:4:D20:F1M1SRM20:V1PM2TRM20:1helper:1helper:0:2:1token:1visible:0" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass getMethod/getProperty return eval member objects. +#[test] +fn execute_program_reflection_class_gets_eval_member_objects() { + let program = parse_fragment( + br#"class EvalReflectLookupTarget { + public function first() {} + private static function helper() {} + protected $visible; + private static $token; +} +$ref = new ReflectionClass("EvalReflectLookupTarget"); +$method = $ref->getMethod("FIRST"); +echo $method->getName(); echo ":"; +echo $method->isPublic() ? "U" : "u"; echo ":"; +$helper = $ref->getMethod("helper"); +echo $helper->isPrivate() ? "P" : "p"; +echo $helper->isStatic() ? "S" : "s"; echo ":"; +$property = $ref->getProperty("visible"); +echo $property->getName(); echo ":"; +echo $property->isProtected() ? "R" : "r"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "first:U:PS:visible:R"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::getParentClass returns eval parent metadata or false. +#[test] +fn execute_program_reflection_class_get_parent_class() { + let program = parse_fragment( + br#"class EvalReflectParentBase {} +class EvalReflectParentChild extends EvalReflectParentBase {} +$parent = (new ReflectionClass("EvalReflectParentChild"))->getParentClass(); +echo $parent->getName(); +echo ":"; +$root = (new ReflectionClass("EvalReflectParentBase"))->getParentClass(); +if ($root === false) { + echo "false"; +} else { + echo "bad"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectParentBase:false"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::newInstance constructs eval-declared classes. +#[test] +fn execute_program_reflection_class_new_instance_constructs_eval_class() { + let program = parse_fragment( + br#"class EvalReflectNewTarget { + public $label; + public function __construct($left, $right) { + $this->label = $left . $right; + } + public function label() { + return $this->label; + } +} +$ref = new ReflectionClass("EvalReflectNewTarget"); +$first = $ref->newInstance("I", "J"); +echo $first->label(); echo ":"; +$second = $ref->newInstance(...["K", "L"]); +echo $second->label(); echo ":"; +$third = $ref->newInstanceArgs(["right" => "N", "left" => "M"]); +echo $third->label(); echo ":"; +$fourth = $ref->newInstanceArgs(["O", "P"]); +echo $fourth->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "IJ:KL:MN:OP"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::newInstance throws for non-public eval constructors. +#[test] +fn execute_program_reflection_class_new_instance_rejects_non_public_eval_constructors() { + let program = parse_fragment( + br#"class EvalReflectNewPrivateCtor { + private function __construct() {} +} +class EvalReflectNewProtectedCtor { + protected function __construct() {} +} +try { + (new ReflectionClass("EvalReflectNewPrivateCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + (new ReflectionClass("EvalReflectNewProtectedCtor"))->newInstance(); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ReflectionException:Access to non-public constructor of class EvalReflectNewPrivateCtor|ReflectionException:Access to non-public constructor of class EvalReflectNewProtectedCtor" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass instantiation throws Error for eval non-instantiable class-likes. +#[test] +fn execute_program_reflection_class_new_instance_rejects_eval_non_instantiable_class_likes() { + let program = parse_fragment( + br#"abstract class EvalReflectNewAbstract {} +interface EvalReflectNewIface {} +trait EvalReflectNewTrait {} +enum EvalReflectNewEnum { case Ready; } +function eval_reflect_new_error($class, $without) { + try { + $ref = new ReflectionClass($class); + if ($without) { + $ref->newInstanceWithoutConstructor(); + } else { + $ref->newInstance(); + } + echo "bad"; + } catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); + } +} +eval_reflect_new_error("EvalReflectNewAbstract", false); echo "|"; +eval_reflect_new_error("EvalReflectNewAbstract", true); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", false); echo "|"; +eval_reflect_new_error("EvalReflectNewIface", true); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", false); echo "|"; +eval_reflect_new_error("EvalReflectNewTrait", true); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", false); echo "|"; +eval_reflect_new_error("EvalReflectNewEnum", true); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate abstract class EvalReflectNewAbstract|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate interface EvalReflectNewIface|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate trait EvalReflectNewTrait|Error:Cannot instantiate enum EvalReflectNewEnum|Error:Cannot instantiate enum EvalReflectNewEnum" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod::invoke dispatches eval-declared methods. +#[test] +fn execute_program_reflection_method_invoke_calls_eval_method() { + let program = parse_fragment( + br#"class EvalReflectInvokeBase { + private function hidden($label = "H") { + return "hidden:" . $label; + } + public function who() { + return static::class; + } + public static function make($left, $right = "S") { + return static::class . ":" . $left . $right; + } +} +class EvalReflectInvokeChild extends EvalReflectInvokeBase { + public function join($a, $b = "B") { + return $a . $b; + } + public function mutate(&$value) { + $value = $value . "!"; + return $value; + } +} +$object = new EvalReflectInvokeChild(); +$hidden = new ReflectionMethod("EvalReflectInvokeBase", "hidden"); +echo $hidden->invoke($object, "X"); echo ":"; +$who = (new ReflectionClass("EvalReflectInvokeChild"))->getMethod("who"); +echo $who->invoke($object); echo ":"; +$static = new ReflectionMethod("EvalReflectInvokeBase", "make"); +echo $static->invoke(null, right: "Y", left: "X"); echo ":"; +echo $static->invoke($object, "A"); echo ":"; +$join = null; +foreach ((new ReflectionClass("EvalReflectInvokeChild"))->getMethods() as $method) { + if ($method->getName() === "join") { + $join = $method; + } +} +$value = "Q"; +$mutate = new ReflectionMethod("EvalReflectInvokeChild", "mutate"); +echo $join->invokeArgs($object, ["b" => "2", "a" => "1"]); echo ":"; +echo $mutate->invoke($object, $value); echo ":"; echo $value; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "hidden:X:EvalReflectInvokeChild:EvalReflectInvokeBase:XY:EvalReflectInvokeBase:AS:12:Q!:Q" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod::invoke throws for incompatible eval receivers. +#[test] +fn execute_program_reflection_method_invoke_rejects_wrong_object() { + let program = parse_fragment( + br#"class EvalReflectInvokeOwner { + public function run() { + return "owner"; + } +} +class EvalReflectInvokeOther {} +try { + (new ReflectionMethod("EvalReflectInvokeOwner", "run"))->invoke(new EvalReflectInvokeOther()); + echo "bad"; +} catch (ReflectionException $e) { + echo "caught"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "caught"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod/Property::setAccessible are PHP-compatible no-ops. +#[test] +fn execute_program_reflection_set_accessible_is_noop() { + let program = parse_fragment( + br#"class EvalReflectAccessTarget { + private $secret = "s"; + private function hidden() { + return $this->secret; + } +} +$object = new EvalReflectAccessTarget(); +$method = new ReflectionMethod("EvalReflectAccessTarget", "hidden"); +echo is_null($method->setAccessible(false)) ? "M" : "m"; echo ":"; +echo $method->invoke($object); echo ":"; +$property = new ReflectionProperty("EvalReflectAccessTarget", "secret"); +echo is_null($property->setAccessible(accessible: true)) ? "P" : "p"; echo ":"; +echo $property->getValue($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "M:s:P:s"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass::newInstanceWithoutConstructor skips eval constructors. +#[test] +fn execute_program_reflection_class_new_instance_without_constructor_allocates_eval_class() { + let program = parse_fragment( + br#"class EvalReflectNoCtorTarget { + public $label = "default"; + private $secret = "hidden"; + public function __construct() { + $this->label = "ctor"; + } + public function label() { + return $this->label; + } + public function secret() { + return $this->secret; + } +} +$ref = new ReflectionClass("EvalReflectNoCtorTarget"); +$without = $ref->newInstanceWithoutConstructor(); +echo $without->label(); echo ":"; +echo $without->secret(); echo ":"; +$with = $ref->newInstance(); +echo $with->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "default:hidden:ctor"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/constants_enums_objects.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/constants_enums_objects.rs new file mode 100644 index 0000000000..7628955d0f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/constants_enums_objects.rs @@ -0,0 +1,432 @@ +//! Purpose: +//! Interpreter tests for class constants, enum cases, and ReflectionObject. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Enum ownership, constant types, dynamic properties, and constructor errors are covered. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClassConstant/EnumCase expose eval-declared attribute metadata. +#[test] +fn execute_program_reflects_eval_constant_and_enum_case_attributes() { + let program = parse_fragment( + br#"class EvalConstMarker { + public $name; + public function __construct($name) { + $this->name = $name; + } + public function label() { + return $this->name; + } +} +class EvalConstReflectTarget { + #[EvalConstMarker("const")] + final public const ANSWER = 42; +} +enum EvalCaseReflectTarget: string { + #[EvalConstMarker("case")] + case Ready = "ready"; +} +$const_attrs = (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getAttributes(); +echo count($const_attrs); echo ":"; echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getName(); echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isFinal() ? "F" : "f"; echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->isEnumCase() ? "enum" : "plain"; echo ":"; +echo (new ReflectionClassConstant("EvalConstReflectTarget", "ANSWER"))->getValue(); echo ":"; +echo ((new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "E" : "e"; echo ":"; +echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->isEnumCase() ? "enum" : "plain"; echo ":"; +echo $const_attrs[0]->getName(); echo ":"; echo $const_attrs[0]->getArguments()[0]; echo ":"; +echo $const_attrs[0]->newInstance()->label(); echo ":"; +$case_attrs = (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo count($case_attrs); echo ":"; echo (new ReflectionClassConstant("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo $case_attrs[0]->getName(); echo ":"; echo $case_attrs[0]->getArguments()[0]; echo ":"; +$unit_attrs = (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo ((new ReflectionEnumUnitCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "unit" : "bad"; echo ":"; +echo $unit_attrs[0]->newInstance()->label(); echo ":"; +$backed_attrs = (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getAttributes(); +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getName(); echo ":"; +echo ((new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getValue() === EvalCaseReflectTarget::Ready) ? "backed" : "bad"; echo ":"; +echo (new ReflectionEnumBackedCase("EvalCaseReflectTarget", "Ready"))->getBackingValue(); echo ":"; +echo $backed_attrs[0]->newInstance()->label(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "1:ANSWER:F:plain:42:E:enum:EvalConstMarker:const:const:1:Ready:EvalConstMarker:case:Ready:unit:case:Ready:backed:ready:case" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClassConstant and enum case metadata expose PHP's untyped defaults. +#[test] +fn execute_program_reflects_eval_constant_type_metadata_defaults() { + let program = parse_fragment( + br#"class EvalConstTypeTarget { + public const ANSWER = 42; +} +enum EvalConstTypeEnum: string { + case Ready = "ready"; +} +$constant = new ReflectionClassConstant("EvalConstTypeTarget", "ANSWER"); +echo $constant->isDeprecated() ? "D" : "d"; echo ":"; +echo $constant->hasType() ? "T" : "t"; echo ":"; +echo $constant->getType() === null ? "N" : "n"; echo ":"; +$case = new ReflectionClassConstant("EvalConstTypeEnum", "Ready"); +echo $case->isDeprecated() ? "D" : "d"; echo ":"; +echo $case->hasType() ? "T" : "t"; echo ":"; +echo $case->getType() === null ? "N" : "n"; echo ":"; +$unit = new ReflectionEnumUnitCase("EvalConstTypeEnum", "Ready"); +echo $unit->isDeprecated() ? "D" : "d"; echo ":"; +echo $unit->hasType() ? "T" : "t"; echo ":"; +echo $unit->getType() === null ? "N" : "n"; echo ":"; +$backed = new ReflectionEnumBackedCase("EvalConstTypeEnum", "Ready"); +echo $backed->isDeprecated() ? "D" : "d"; echo ":"; +echo $backed->hasType() ? "T" : "t"; echo ":"; +echo $backed->getType() === null ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "d:t:N:d:t:N:d:t:N:d:t:N"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClassConstant and enum case objects stringify retained metadata. +#[test] +fn execute_program_reflects_eval_constant_to_string() { + let program = parse_fragment( + br#"class EvalConstStringTarget { + public const ANSWER = 42; + final protected const LIMIT = 7; + private const FLAG = true; + public const LABEL = "ok"; + public const NOTHING = null; +} +enum EvalConstStringEnum: string { + case Ready = "ready"; +} +foreach (["ANSWER", "LIMIT", "FLAG", "LABEL", "NOTHING"] as $name) { + echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringTarget", $name))->__toString()); + echo "|"; +} +echo str_replace("\n", "\\n", (new ReflectionClassConstant("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumUnitCase("EvalConstStringEnum", "Ready"))->__toString()); +echo "|"; +echo str_replace("\n", "\\n", (new ReflectionEnumBackedCase("EvalConstStringEnum", "Ready"))->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Constant [ public int ANSWER ] { 42 }\\n|Constant [ final protected int LIMIT ] { 7 }\\n|Constant [ private bool FLAG ] { 1 }\\n|Constant [ public string LABEL ] { ok }\\n|Constant [ public null NOTHING ] { }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n|Constant [ public EvalConstStringEnum Ready ] { Object }\\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies enum-case reflection owners expose inherited constant metadata predicates. +#[test] +fn execute_program_reflects_enum_case_visibility_and_modifiers() { + let program = parse_fragment( + br#"enum EvalEnumCaseVisibility: string { + case Ready = "ready"; +} +$unit = new ReflectionEnumUnitCase("EvalEnumCaseVisibility", "Ready"); +$backed = new ReflectionEnumBackedCase("EvalEnumCaseVisibility", "Ready"); +foreach ([$unit, $backed] as $case) { + echo $case->isEnumCase() ? "E" : "e"; + echo $case->isPrivate() ? "R" : "r"; + echo $case->isProtected() ? "P" : "p"; + echo $case->isPublic() ? "U" : "u"; + echo $case->isFinal() ? "F" : "f"; + echo $case->getModifiers(); echo ":"; +} +echo ReflectionEnumUnitCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumUnitCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumUnitCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumUnitCase::IS_FINAL; echo ":"; +echo ReflectionEnumBackedCase::IS_PUBLIC; echo ":"; +echo ReflectionEnumBackedCase::IS_PROTECTED; echo ":"; +echo ReflectionEnumBackedCase::IS_PRIVATE; echo ":"; +echo ReflectionEnumBackedCase::IS_FINAL; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "ErpUf1:ErpUf1:1:2:4:32:1:2:4:32"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionEnum exposes eval-declared enum cases and backing metadata. +#[test] +fn execute_program_reflects_eval_enum_owner_metadata() { + let program = parse_fragment( + br#"enum EvalReflectPure { + case Ready; + case Done; +} +enum EvalReflectBacked: string { + case Ready = "ready"; + case Done = "done"; +} +$pure = new ReflectionEnum("EvalReflectPure"); +echo $pure->getName(); echo ":"; +echo $pure->isEnum() ? "E" : "e"; echo ":"; +echo $pure->isBacked() ? "B" : "b"; echo ":"; +echo $pure->getBackingType() === null ? "N" : "n"; echo ":"; +echo $pure->hasCase("Ready") ? "R" : "r"; +echo $pure->hasCase("Missing") ? "M" : "m"; echo ":"; +$case = $pure->getCase("Done"); +echo $case->getName(); echo ":"; +echo $case->getEnum()->getName(); echo ":"; +$cases = $pure->getCases(); +echo count($cases); echo ":"; +echo $cases[0]->getName(); echo ":"; +echo $cases[1]->getEnum()->getName(); echo ":"; +$backed = new ReflectionEnum("EvalReflectBacked"); +$type = $backed->getBackingType(); +echo $backed->isBacked() ? "B" : "b"; echo ":"; +echo $type->getName(); echo ":"; +echo $type->isBuiltin() ? "I" : "i"; echo ":"; +$backed_case = $backed->getCase("Ready"); +echo $backed_case->getName(); echo ":"; +echo $backed_case->getBackingValue(); echo ":"; +echo $backed_case->getEnum()->isBacked() ? "E" : "e"; echo ":"; +$backed_cases = $backed->getCases(); +echo count($backed_cases); echo ":"; +echo $backed_cases[1]->getBackingValue(); echo ":"; +echo $backed_cases[0]->getEnum()->getBackingType()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalReflectPure:E:b:N:Rm:Done:EvalReflectPure:2:Ready:EvalReflectPure:B:string:I:Ready:ready:E:2:done:string" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionEnum construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_enum_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectNotEnumClass {} +interface EvalReflectNotEnumIface {} +trait EvalReflectNotEnumTrait {} +enum EvalReflectActualEnum { + case Ready; +} +try { + new ReflectionEnum("EvalReflectNotEnumClass"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectNotEnumIface"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectNotEnumTrait"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnum("EvalReflectMissingEnum"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnum("EvalReflectActualEnum"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Class \"EvalReflectNotEnumClass\" is not an enum|Class \"EvalReflectNotEnumIface\" is not an enum|Class \"EvalReflectNotEnumTrait\" is not an enum|Class \"EvalReflectMissingEnum\" does not exist|EvalReflectActualEnum" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionObject reflects the runtime class of an eval object instance. +#[test] +fn execute_program_reflection_object_reflects_eval_instances() { + let program = parse_fragment( + br#"class EvalReflectObjectBase { + public function inherited(): string { + return "base"; + } +} +class EvalReflectObjectChild extends EvalReflectObjectBase { + public int $count = 3; +} +$ref = new ReflectionObject(new EvalReflectObjectChild()); +echo get_class($ref); echo ":"; +echo ($ref instanceof ReflectionObject) ? "O" : "o"; +echo ($ref instanceof ReflectionClass) ? "C" : "c"; echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->getParentClass()->getName(); echo ":"; +echo $ref->hasMethod("inherited") ? "M" : "m"; +echo $ref->hasProperty("count") ? "P" : "p"; echo ":"; +$object = $ref->newInstanceWithoutConstructor(); +echo get_class($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ReflectionObject:OC:EvalReflectObjectChild:EvalReflectObjectBase:MP:EvalReflectObjectChild" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionObject lists dynamic public properties from the reflected instance. +#[test] +fn execute_program_reflection_object_lists_dynamic_properties() { + let program = parse_fragment( + br#"class EvalReflectObjectDynamicTarget { + public $declared = "declared"; +} +$object = new EvalReflectObjectDynamicTarget(); +$object->dynamic = "value"; +$ref = new ReflectionObject($object); +$properties = $ref->getProperties(); +foreach ($properties as $property) { + echo $property->getName(); echo ":"; + echo $property->isDynamic() ? "D" : "d"; echo "|"; +} +echo ":"; +$dynamic = $ref->getProperty("dynamic"); +echo $dynamic->isDynamic() ? "D" : "d"; echo ":"; +echo $dynamic->getValue($object); echo ":"; +echo count($ref->getProperties(ReflectionProperty::IS_PUBLIC)); echo ":"; +echo count($ref->getProperties(ReflectionProperty::IS_STATIC)); echo ":"; +echo $ref->hasProperty("dynamic") ? "H" : "h"; +echo $ref->hasProperty("declared") ? "D" : "d"; +echo $ref->hasProperty("missing") ? "M" : "m"; +echo (new ReflectionClass($object))->hasProperty("dynamic") ? "C" : "c"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "declared:d|dynamic:D|:D:value:2:0:HDmc"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionObject constructor type errors are catchable. +#[test] +fn execute_program_reflection_object_constructor_throws_type_errors() { + let program = parse_fragment( + br#"try { + new ReflectionObject("EvalReflectObjectChild"); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionObject([]); + echo "bad"; +} catch (TypeError $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "TypeError:ReflectionObject::__construct(): Argument #1 ($object) must be of type object, string given|ReflectionObject::__construct(): Argument #1 ($object) must be of type object, array given" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies unsupported attribute argument metadata remains name-visible but not materializable. +#[test] +fn execute_program_rejects_unsupported_class_attribute_args_metadata() { + for source in [ + br#"#[Tag($dynamic)] +class EvalUnsupportedAttr {} +$names = class_attribute_names("EvalUnsupportedAttr"); +echo count($names); echo ":"; echo $names[0]; echo ":"; +class_attribute_args("EvalUnsupportedAttr", "Tag");"# as &[u8], + br#"#[Tag(["fixed" => "ok", $dynamic => "bad"])] +class EvalUnsupportedAttr {} +$names = class_attribute_names("EvalUnsupportedAttr"); +echo count($names); echo ":"; echo $names[0]; echo ":"; +class_attribute_args("EvalUnsupportedAttr", "Tag");"#, + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unsupported attribute metadata should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.output, "1:Tag:"); + } +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_constructors.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_constructors.rs new file mode 100644 index 0000000000..a7b32fdf4c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_constructors.rs @@ -0,0 +1,418 @@ +//! Purpose: +//! Interpreter tests for Reflection member constructors and constructor failures. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Constructor target normalization and PHP-visible exceptions are asserted together. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionMethod preserves declared method case after case-insensitive lookup. +#[test] +fn execute_program_reflection_method_preserves_declared_name_case() { + let program = parse_fragment( + br#"class EvalReflectMethodCaseBase { + public function MiXeDCase() { return "base"; } +} +class EvalReflectMethodCaseChild extends EvalReflectMethodCaseBase { + public function childCase() { return "child"; } +} +$object = new EvalReflectMethodCaseChild(); +$direct = new ReflectionMethod("EvalReflectMethodCaseChild", "mixedcase"); +echo $direct->getName(); echo ":"; +echo $direct->getShortName(); echo ":"; +echo $direct->invoke($object); echo ":"; +$listed = (new ReflectionClass("EvalReflectMethodCaseChild"))->getMethod("CHILDCASE"); +echo $listed->getName(); echo ":"; +echo $listed->invoke($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "MiXeDCase:MiXeDCase:base:childCase:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod accepts object targets and reflects the runtime class. +#[test] +fn execute_program_reflection_method_accepts_object_targets() { + let program = parse_fragment( + br#"class EvalReflectMethodObjectBase { + public function MiXeDCase() { return "base"; } +} +class EvalReflectMethodObjectChild extends EvalReflectMethodObjectBase { + public function childCase() { return "child"; } +} +$object = new EvalReflectMethodObjectChild(); +$inherited = new ReflectionMethod($object, "mixedcase"); +echo $inherited->getName(); echo ":"; +echo $inherited->getDeclaringClass()->getName(); echo ":"; +echo $inherited->invoke($object); echo ":"; +$own = new ReflectionMethod($object, "CHILDCASE"); +echo $own->getName(); echo ":"; +echo $own->getDeclaringClass()->getName(); echo ":"; +echo $own->invoke($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "MiXeDCase:EvalReflectMethodObjectBase:base:childCase:EvalReflectMethodObjectChild:child" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod::createFromMethodName resolves eval method strings. +#[test] +fn execute_program_reflection_method_create_from_method_name() { + let program = parse_fragment( + br#"class EvalReflectCreateMethodTarget { + public function MiXeDCase() { return "ok"; } +} +$ref = ReflectionMethod::createFromMethodName("EvalReflectCreateMethodTarget::mixedcase"); +echo $ref->getDeclaringClass()->getName(); echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->invoke(new EvalReflectCreateMethodTarget()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectCreateMethodTarget:MiXeDCase:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod accepts PHP's deprecated one-string method target. +#[test] +fn execute_program_reflection_method_accepts_single_method_string() { + let program = parse_fragment( + br#"class EvalReflectCtorMethodTarget { + public function MiXeDCase() { return "ok"; } +} +$ref = new ReflectionMethod(objectOrMethod: "EvalReflectCtorMethodTarget::mixedcase"); +echo $ref->getDeclaringClass()->getName(); echo ":"; +echo $ref->getName(); echo ":"; +echo $ref->invoke(new EvalReflectCtorMethodTarget()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReflectCtorMethodTarget:MiXeDCase:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_method_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingMethodTarget {} +try { + new ReflectionMethod("EvalReflectMissingMethodTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalReflectMissingMethodTarget::missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + ReflectionMethod::createFromMethodName("EvalReflectMissingMethodTarget::missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionMethod("EvalReflectMissingClass", "run"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + ReflectionMethod::createFromMethodName("not-a-method"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Method EvalReflectMissingMethodTarget::missing() does not exist|Method EvalReflectMissingMethodTarget::missing() does not exist|Method EvalReflectMissingMethodTarget::missing() does not exist|Class \"EvalReflectMissingClass\" does not exist|ReflectionMethod::createFromMethodName(): Argument #1 ($method) must be a valid method name" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_property_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingPropertyTarget {} +$object = new EvalReflectMissingPropertyTarget(); +$object->dynamic = 1; +try { + new ReflectionProperty("EvalReflectMissingPropertyTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty("EvalReflectMissingPropertyClass", "value"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionProperty($object, "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +$property = new ReflectionProperty($object, "dynamic"); +echo $property->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Property EvalReflectMissingPropertyTarget::$missing does not exist|Class \"EvalReflectMissingPropertyClass\" does not exist|Property EvalReflectMissingPropertyTarget::$missing does not exist|dynamic" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClassConstant construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_class_constant_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"class EvalReflectMissingConstantTarget { + public const OK = 1; +} +try { + new ReflectionClassConstant("EvalReflectMissingConstantTarget", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionClassConstant("EvalReflectMissingConstantClass", "VALUE"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionClassConstant("EvalReflectMissingConstantTarget", "OK"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Constant EvalReflectMissingConstantTarget::missing does not exist|Class \"EvalReflectMissingConstantClass\" does not exist|OK" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies ReflectionEnumUnitCase/BackedCase construction throws PHP reflection errors. +#[test] +fn execute_program_reflection_enum_case_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"enum EvalReflectMissingCaseUnit { + case Ready; + public const TOKEN = 1; +} +enum EvalReflectMissingCaseBacked: string { + case Ready = "ready"; + public const TOKEN = 1; +} +class EvalReflectMissingCaseClass { + public const TOKEN = 1; +} +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseUnit", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseClass", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumUnitCase("EvalReflectMissingCaseUnit", "TOKEN"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseUnit", "Ready"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseBacked", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionEnumBackedCase("EvalReflectMissingCaseClass", "Missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionEnumUnitCase("EvalReflectMissingCaseBacked", "Ready"))->getName(); echo ":"; +echo (new ReflectionEnumBackedCase("EvalReflectMissingCaseBacked", "Ready"))->getBackingValue(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:Constant EvalReflectMissingCaseUnit::Missing does not exist|Constant EvalReflectMissingCaseClass::TOKEN is not a case|Constant EvalReflectMissingCaseUnit::TOKEN is not a case|Enum case EvalReflectMissingCaseUnit::Ready is not a backed case|Constant EvalReflectMissingCaseBacked::Missing does not exist|Constant EvalReflectMissingCaseClass::Missing does not exist|Ready:ready" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} + +/// Verifies eval member and enum-case reflectors expose their declaring class. +#[test] +fn execute_program_reflects_eval_declaring_class_metadata() { + let program = parse_fragment( + br#"class EvalDeclaringBase { + public $baseProp = 1; + public function inherited() { return "base"; } + public const BASE_CONST = 10; +} +class EvalDeclaringChild extends EvalDeclaringBase { + public $childProp = 2; + public function own() { return "child"; } + public const CHILD_CONST = 20; +} +enum EvalDeclaringEnum: string { + case Ready = "ready"; + public const LEVEL = 3; +} +echo (new ReflectionMethod("EvalDeclaringChild", "inherited"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getMethod("own")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionProperty("EvalDeclaringChild", "baseProp"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getProperty("childProp")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringChild"))->getReflectionConstant("BASE_CONST")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClassConstant("EvalDeclaringChild", "BASE_CONST"))->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionClass("EvalDeclaringEnum"))->getReflectionConstant("Ready")->getDeclaringClass()->getName(); echo ":"; +echo (new ReflectionEnumBackedCase("EvalDeclaringEnum", "Ready"))->getDeclaringClass()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringChild:EvalDeclaringBase:EvalDeclaringBase:EvalDeclaringEnum:EvalDeclaringEnum" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass stringifies retained eval class metadata. +#[test] +fn execute_program_reflection_class_to_string() { + let program = parse_fragment( + br#"class EvalReflectClassStringTarget { + public const ANSWER = 42; + public int $id = 7; + public function read(string $name = "Ada"): ?string { return $name; } +} +$ref = new ReflectionClass("EvalReflectClassStringTarget"); +echo $ref; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Class [ class EvalReflectClassStringTarget ] {\n - Constants [1] {\n Constant [ public int ANSWER ] { 42 }\n }\n - Properties [1] {\n Property [ public int $id = 7 ]\n }\n - Methods [1] {\n Method [ public method read ]\n }\n}\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_metadata.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_metadata.rs new file mode 100644 index 0000000000..ac1bf4bb41 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/member_metadata.rs @@ -0,0 +1,453 @@ +//! Purpose: +//! Interpreter tests for reflected member discovery, flags, and declaring metadata. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Methods, properties, class constants, and enum cases share this metadata layer. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionClass reports eval class-like method, property, and constant membership. +#[test] +fn execute_program_reflects_eval_class_member_existence() { + let program = parse_fragment( + br#"class EvalMemberParent { + const PARENT_CONST = 1; + private function hiddenParent() {} + protected static function parentStatic() {} + private $hiddenProp; + protected static $parentStaticProp; +} +interface EvalMemberClassIface { + const CLASS_LIMIT = 10; +} +class EvalMemberChild extends EvalMemberParent implements EvalMemberClassIface { + const CHILD_CONST = 2; + public function ChildMethod() {} + public $childProp; +} +interface EvalMemberIfaceParent { + const PARENT_LIMIT = 10; + public function parentRequirement(); +} +interface EvalMemberIface extends EvalMemberIfaceParent { + const CHILD_LIMIT = 20; + public function childRequirement(); + public string $hook { get; } +} +trait EvalMemberTrait { + const TRAIT_CONST = 30; + private function traitHidden() {} + public $traitProp; +} +enum EvalMemberPureEnum { + case Ready; + const LEVEL = 40; + public function label() { return "ok"; } +} +enum EvalMemberBackedEnum: string { + case Ready = "ready"; +} +$child = new ReflectionClass("EvalMemberChild"); +echo $child->hasMethod("childmethod") ? "M" : "m"; +echo $child->hasMethod("HIDDENPARENT") ? "P" : "p"; +echo $child->hasMethod("parentStatic") ? "S" : "s"; +echo $child->hasMethod("missing") ? "X" : "x"; +echo ":"; +echo $child->hasProperty("childProp") ? "C" : "c"; +echo $child->hasProperty("hiddenProp") ? "H" : "h"; +echo $child->hasProperty("parentStaticProp") ? "T" : "t"; +echo $child->hasProperty("childprop") ? "W" : "w"; +echo $child->hasConstant("CHILD_CONST") ? "D" : "d"; +echo $child->hasConstant("PARENT_CONST") ? "P" : "p"; +echo $child->hasConstant("CLASS_LIMIT") ? "A" : "a"; +echo $child->hasConstant("child_const") ? "Z" : "z"; +echo ":"; +$iface = new ReflectionClass("EvalMemberIface"); +echo $iface->hasMethod("parentrequirement") ? "I" : "i"; +echo $iface->hasMethod("childRequirement") ? "J" : "j"; +echo $iface->hasProperty("hook") ? "K" : "k"; +echo $iface->hasConstant("PARENT_LIMIT") ? "L" : "l"; +echo $iface->hasConstant("CHILD_LIMIT") ? "C" : "c"; +echo ":"; +$trait = new ReflectionClass("EvalMemberTrait"); +echo $trait->hasMethod("traithidden") ? "R" : "r"; +echo $trait->hasProperty("traitProp") ? "U" : "u"; +echo $trait->hasConstant("TRAIT_CONST") ? "K" : "k"; +echo ":"; +$pure = new ReflectionClass("EvalMemberPureEnum"); +echo $pure->hasMethod("cases") ? "E" : "e"; +echo $pure->hasMethod("label") ? "L" : "l"; +echo $pure->hasProperty("name") ? "N" : "n"; +echo $pure->hasProperty("value") ? "V" : "v"; +echo $pure->hasConstant("Ready") ? "G" : "g"; +echo $pure->hasConstant("LEVEL") ? "F" : "f"; +echo $pure->hasConstant("ready") ? "R" : "r"; +echo ":"; +$backed = new ReflectionClass("EvalMemberBackedEnum"); +echo $backed->hasMethod("tryfrom") ? "B" : "b"; +echo $backed->hasProperty("value") ? "Y" : "y"; +echo $backed->hasConstant("Ready") ? "Q" : "q"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "MPSx:ChTwDPAz:IJKLC:RUK:ELNvGFr:BYQ"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass returns eval class-like constant values and enum cases. +#[test] +fn execute_program_reflects_eval_class_constant_values() { + let program = parse_fragment( + br#"class EvalReflectConstBase { + public const BASE = 1; +} +interface EvalReflectConstIface { + public const LIMIT = 2; +} +trait EvalReflectConstTrait { + public const TRAIT_VALUE = 3; +} +class EvalReflectConstChild extends EvalReflectConstBase implements EvalReflectConstIface { + private const SECRET = 9; + public const OWN = "own"; + public const SUM = 5; +} +enum EvalReflectConstEnum { + case Ready; + public const LEVEL = 40; +} +$ref = new ReflectionClass("EvalReflectConstChild"); +$all = $ref->getConstants(); +$public = $ref->getConstants(ReflectionClassConstant::IS_PUBLIC); +$private = $ref->getConstants(filter: ReflectionClassConstant::IS_PRIVATE); +$none = $ref->getConstants(0); +$null = $ref->getConstants(null); +echo $ref->getConstant("OWN"); echo ":"; +echo $ref->getConstant("BASE"); echo ":"; +echo $ref->getConstant("LIMIT"); echo ":"; +echo $ref->getConstant("SECRET"); echo ":"; +echo $ref->getConstant("SUM"); echo ":"; +echo $ref->getConstant("own") ? "bad" : "missing"; +echo ":"; echo count($all); echo ":"; echo $all["OWN"]; echo ":"; echo $all["BASE"]; echo ":"; echo $all["LIMIT"]; +echo ":"; echo count($public); echo ":"; echo $public["OWN"]; echo ":"; echo $public["BASE"]; +echo ":"; echo count($private); echo ":"; echo $private["SECRET"]; +echo ":"; echo count($none); echo ":"; echo count($null); +$trait = new ReflectionClass("EvalReflectConstTrait"); +$traitAll = $trait->getConstants(); +echo ":"; echo $trait->getConstant("TRAIT_VALUE"); echo ":"; echo count($traitAll); echo ":"; echo $traitAll["TRAIT_VALUE"]; +$enum = new ReflectionClass("EvalReflectConstEnum"); +$case = $enum->getConstant("Ready"); +$enumAll = $enum->getConstants(); +echo ":"; echo $case->name; +echo ":"; echo $enum->getConstant("LEVEL"); echo ":"; echo $enumAll["LEVEL"]; echo ":"; echo count($enumAll); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "own:1:2:9:5:missing:5:own:1:2:4:own:1:1:9:0:5:3:1:3:Ready:40:40:2" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass returns eval class-constant reflector objects. +#[test] +fn execute_program_reflects_eval_class_constant_reflector_objects() { + let program = parse_fragment( + br#"class EvalReflectConstMarker { + public $label; + public function __construct($label) { + $this->label = $label; + } + public function label() { + return $this->label; + } +} +class EvalReflectConstObjectTarget { + #[EvalReflectConstMarker("const")] + final public const ANSWER = 42; +} +enum EvalReflectConstObjectEnum { + #[EvalReflectConstMarker("case")] + case Ready; + final public const LEVEL = 7; +} +$ref = new ReflectionClass("EvalReflectConstObjectTarget"); +$single = $ref->getReflectionConstant("ANSWER"); +$all = $ref->getReflectionConstants(); +$public = $ref->getReflectionConstants(ReflectionClassConstant::IS_PUBLIC); +$final = $ref->getReflectionConstants(filter: ReflectionClassConstant::IS_FINAL); +echo $single->getName(); echo ":"; +echo count($all); echo ":"; echo $all[0]->getName(); echo ":"; +echo $single->getAttributes()[0]->newInstance()->label(); echo ":"; +echo $ref->getReflectionConstant("answer") ? "bad" : "missing"; +echo ":"; echo count($public); echo ":"; echo $public[0]->getName(); +echo ":"; echo count($final); echo ":"; echo $final[0]->getName(); +$enum = new ReflectionClass("EvalReflectConstObjectEnum"); +$enumAll = $enum->getReflectionConstants(); +$enumFinal = $enum->getReflectionConstants(ReflectionClassConstant::IS_FINAL); +$case = $enum->getReflectionConstant("Ready"); +$level = $enum->getReflectionConstant("LEVEL"); +echo ":"; echo count($enumAll); echo ":"; echo $enumAll[0]->getName(); echo ":"; echo $enumAll[1]->getName(); +echo ":"; echo $case->getAttributes()[0]->newInstance()->label(); echo ":"; +echo count($level->getAttributes()); echo ":"; echo count($enumFinal); echo ":"; echo $enumFinal[0]->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "ANSWER:1:ANSWER:const:missing:1:ANSWER:1:ANSWER:2:Ready:LEVEL:case:0:1:LEVEL" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod and ReflectionProperty expose eval member predicate metadata. +#[test] +fn execute_program_reflects_eval_member_predicates() { + let program = parse_fragment( + br#"abstract class EvalReflectMemberBase { + protected static function baseStatic() {} + abstract protected function mustImplement(); + final public function locked() {} +} +readonly class EvalReflectReadonlyClass { + public int $classReadonly; +} +abstract class EvalReflectAbstractProperty { + abstract public int $mustRead { get; } +} +class EvalReflectMemberChild extends EvalReflectMemberBase { + public function mustImplement() {} + private static $token; + final public static $staticSeal; + protected $visible; + public readonly int $locked; + final public int $sealed; +} +$baseStatic = new ReflectionMethod("EvalReflectMemberChild", "baseStatic"); +echo $baseStatic->isStatic() ? "S" : "s"; +echo $baseStatic->isProtected() ? "P" : "p"; +echo $baseStatic->isPublic() ? "U" : "u"; +echo $baseStatic->isPrivate() ? "R" : "r"; +echo $baseStatic->isFinal() ? "F" : "f"; +echo $baseStatic->isAbstract() ? "A" : "a"; +echo ":"; +$abstractMethod = new ReflectionMethod("EvalReflectMemberBase", "mustImplement"); +echo $abstractMethod->isAbstract() ? "A" : "a"; +echo $abstractMethod->isProtected() ? "P" : "p"; +echo $abstractMethod->isStatic() ? "S" : "s"; +echo ":"; +$finalMethod = new ReflectionMethod("EvalReflectMemberChild", "locked"); +echo $finalMethod->isFinal() ? "F" : "f"; +echo $finalMethod->isPublic() ? "U" : "u"; +echo $finalMethod->isStatic() ? "S" : "s"; +echo ":"; +$staticProp = new ReflectionProperty("EvalReflectMemberChild", "token"); +echo $staticProp->isStatic() ? "S" : "s"; +echo $staticProp->isPrivate() ? "R" : "r"; +echo $staticProp->isProtected() ? "P" : "p"; +echo $staticProp->isFinal() ? "F" : "f"; +echo $staticProp->isAbstract() ? "A" : "a"; +echo $staticProp->isReadOnly() ? "R" : "r"; +echo $staticProp->isProtectedSet() ? "T" : "t"; +echo $staticProp->isPrivateSet() ? "D" : "d"; +echo $staticProp->getModifiers(); +echo ":"; +$visibleProp = new ReflectionProperty("EvalReflectMemberChild", "visible"); +echo $visibleProp->isStatic() ? "S" : "s"; +echo $visibleProp->isProtected() ? "P" : "p"; +echo $visibleProp->isPublic() ? "U" : "u"; +echo $visibleProp->isFinal() ? "F" : "f"; +echo $visibleProp->isAbstract() ? "A" : "a"; +echo $visibleProp->isReadOnly() ? "R" : "r"; +echo $visibleProp->isProtectedSet() ? "T" : "t"; +echo $visibleProp->isPrivateSet() ? "D" : "d"; +echo $visibleProp->getModifiers(); +echo ":"; +$readonlyProp = new ReflectionProperty("EvalReflectMemberChild", "locked"); +echo $readonlyProp->isReadOnly() ? "R" : "r"; +echo $readonlyProp->isPublic() ? "U" : "u"; +echo $readonlyProp->isProtectedSet() ? "T" : "t"; +echo $readonlyProp->isPrivateSet() ? "D" : "d"; +echo $readonlyProp->getModifiers(); +echo ":"; +$sealedProp = new ReflectionProperty("EvalReflectMemberChild", "sealed"); +echo $sealedProp->isFinal() ? "F" : "f"; +echo $sealedProp->isPublic() ? "U" : "u"; +echo $sealedProp->getModifiers(); +echo ":"; +$staticFinalProp = new ReflectionProperty("EvalReflectMemberChild", "staticSeal"); +echo $staticFinalProp->isFinal() ? "F" : "f"; +echo $staticFinalProp->isStatic() ? "S" : "s"; +echo $staticFinalProp->getModifiers(); +echo ":"; +$abstractProp = new ReflectionProperty("EvalReflectAbstractProperty", "mustRead"); +echo $abstractProp->isAbstract() ? "A" : "a"; +echo $abstractProp->isFinal() ? "F" : "f"; +echo $abstractProp->getModifiers(); +echo ":"; +$classReadonlyProp = new ReflectionProperty("EvalReflectReadonlyClass", "classReadonly"); +echo $classReadonlyProp->isReadOnly() ? "C" : "c"; +echo $classReadonlyProp->isProtectedSet() ? "T" : "t"; +echo $classReadonlyProp->isPrivateSet() ? "D" : "d"; +echo $classReadonlyProp->getModifiers(); +echo ":"; +echo $visibleProp->isDynamic() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "SPurfa:APs:FUs:SRpfartd20:sPufartd2:RUTd2177:FU33:FS49:Af577:CTd2177:d" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty reports eval-declared asymmetric set visibility. +#[test] +fn execute_program_reflects_eval_asymmetric_property_set_visibility() { + let program = parse_fragment( + br#"class EvalReflectAsymmetricProperty { + public private(set) int $privateSet = 1; + public protected(set) int $protectedSet = 2; + protected private(set) int $protectedPrivateSet = 3; +} +$private = new ReflectionProperty("EvalReflectAsymmetricProperty", "privateSet"); +echo $private->isPrivateSet() ? "P" : "p"; +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->getModifiers(); echo ":"; +$protected = new ReflectionProperty("EvalReflectAsymmetricProperty", "protectedSet"); +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->getModifiers(); echo ":"; +$protectedPrivate = new ReflectionProperty("EvalReflectAsymmetricProperty", "protectedPrivateSet"); +echo $protectedPrivate->isPrivateSet() ? "P" : "p"; +echo $protectedPrivate->isProtectedSet() ? "T" : "t"; +echo $protectedPrivate->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Pt4129:pT2049:Pt4130"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty reports asymmetric set visibility on eval interface contracts. +#[test] +fn execute_program_reflects_eval_interface_asymmetric_property_set_visibility() { + let program = parse_fragment( + br#"interface EvalReflectAsymmetricIfaceProperty { + public protected(set) string $name { get; set; } + private(set) int $id { get; set; } +} +$protected = new ReflectionProperty("EvalReflectAsymmetricIfaceProperty", "name"); +echo $protected->isProtectedSet() ? "T" : "t"; +echo $protected->isPrivateSet() ? "P" : "p"; +echo $protected->isFinal() ? "F" : "f"; +echo $protected->getModifiers(); echo ":"; +$private = new ReflectionProperty("EvalReflectAsymmetricIfaceProperty", "id"); +echo $private->isProtectedSet() ? "T" : "t"; +echo $private->isPrivateSet() ? "P" : "p"; +echo $private->isFinal() ? "F" : "f"; +echo $private->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Tpf2625:tPF4705"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty reports eval constructor-promotion metadata. +#[test] +fn execute_program_reflection_property_reports_eval_promoted_metadata() { + let program = parse_fragment( + br#"class EvalReflectPromotedTarget { + public function __construct(public int $id, private string $name = "Ada") {} + public string $plain = "x"; +} +$id = new ReflectionProperty("EvalReflectPromotedTarget", "id"); +$name = new ReflectionProperty("EvalReflectPromotedTarget", "name"); +$plain = new ReflectionProperty("EvalReflectPromotedTarget", "plain"); +echo $id->isPromoted() ? "I" : "i"; +echo $name->isPromoted() ? "N" : "n"; +echo $plain->isPromoted() ? "P" : "p"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "INp"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionMethod constructor/destructor predicates for eval methods. +#[test] +fn execute_program_reflection_method_reports_constructor_and_destructor() { + let program = parse_fragment( + br#"class EvalReflectLifecycle { + public function __construct() {} + public function __destruct() {} + public function run() {} +} +$ctor = new ReflectionMethod("EvalReflectLifecycle", "__CONSTRUCT"); +echo $ctor->isConstructor() ? "C" : "c"; +echo $ctor->isDestructor() ? "D" : "d"; +echo ":"; +$dtor = new ReflectionMethod("EvalReflectLifecycle", "__destruct"); +echo $dtor->isConstructor() ? "C" : "c"; +echo $dtor->isDestructor() ? "D" : "d"; +echo ":"; +$run = new ReflectionMethod("EvalReflectLifecycle", "run"); +echo $run->isConstructor() ? "C" : "c"; +echo $run->isDestructor() ? "D" : "d"; +echo ":"; +$listed = (new ReflectionClass("EvalReflectLifecycle"))->getConstructor(); +echo $listed->isConstructor() ? "C" : "c"; +echo $listed->isDestructor() ? "D" : "d"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Cd:cD:cd:Cd"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/mod.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/mod.rs new file mode 100644 index 0000000000..b7cf7ab37f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/mod.rs @@ -0,0 +1,21 @@ +//! Purpose: +//! Organizes interpreter coverage for eval-backed class metadata and Reflection. +//! Each child module owns one coherent PHP-visible metadata surface. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - The split mirrors the production Reflection boundaries without sharing fixtures. + +mod attributes; +mod callable_types; +mod class_capabilities; +mod class_identity; +mod class_operations; +mod constants_enums_objects; +mod member_constructors; +mod member_metadata; +mod parameter_defaults; +mod property_values; +mod relations; diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/parameter_defaults.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/parameter_defaults.rs new file mode 100644 index 0000000000..c11a611a93 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/parameter_defaults.rs @@ -0,0 +1,154 @@ +//! Purpose: +//! Interpreter tests for reflected parameter and property default values. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Constant and magic-constant defaults are resolved in their declaring scope. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionParameter reports eval constant-default metadata. +#[test] +fn execute_program_reflection_parameter_reports_default_constant_metadata() { + let program = parse_fragment( + br##"define("EVAL_REFLECT_PARAM_DEFAULT_GLOBAL", "G"); +class EvalReflectParamDefaultBase { + const BASE = "B"; +} +class EvalReflectParamDefaultTarget extends EvalReflectParamDefaultBase { + const LABEL = "L"; + public function run($required, $global = EVAL_REFLECT_PARAM_DEFAULT_GLOBAL, $self = self::LABEL, $parent = parent::BASE, $literal = 7) {} +} +$params = (new ReflectionMethod("EvalReflectParamDefaultTarget", "run"))->getParameters(); +foreach ($params as $param) { + echo $param->getName(); echo ":"; + echo $param->isDefaultValueAvailable() ? "D:" : "d:"; + if ($param->isDefaultValueAvailable()) { + if ($param->isDefaultValueConstant()) { + echo "C:"; + echo $param->getDefaultValueConstantName(); + echo ":"; + } else { + echo "c:null:"; + } + echo $param->getDefaultValue(); + } + echo "|"; +} +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "required:d:|global:D:C:EVAL_REFLECT_PARAM_DEFAULT_GLOBAL:G|self:D:C:self::LABEL:L|parent:D:C:parent::BASE:B|literal:D:c:null:7|" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionParameter default magic constants use declaring callable scopes. +#[test] +fn execute_program_reflection_parameter_resolves_default_magic_constants() { + let program = parse_fragment( + br##"namespace EvalReflectParamMagicNs; +function eval_reflect_param_magic($fn = __FUNCTION__, $m = __METHOD__, $c = __CLASS__, $t = __TRAIT__, $n = __NAMESPACE__) {} +interface EvalReflectParamMagicIface { + public function read($c = __CLASS__, $m = __METHOD__, $fn = __FUNCTION__, $t = __TRAIT__, $n = __NAMESPACE__); +} +trait EvalReflectParamMagicTrait { + public function source($c = __CLASS__, $t = __TRAIT__, $m = __METHOD__, $fn = __FUNCTION__, $n = __NAMESPACE__) {} +} +class EvalReflectParamMagicBox { + use EvalReflectParamMagicTrait { source as aliasSource; } + public function own($c = __CLASS__, $t = __TRAIT__, $m = __METHOD__, $fn = __FUNCTION__, $n = __NAMESPACE__) {} +} +function eval_param_magic_dump($ref) { + foreach ($ref->getParameters() as $param) { + echo "[" . $param->getDefaultValue() . "]"; + } + echo ":"; +} +eval_param_magic_dump(new \ReflectionFunction(__NAMESPACE__ . "\\eval_reflect_param_magic")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "own")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicBox::class, "aliasSource")); +eval_param_magic_dump(new \ReflectionMethod(EvalReflectParamMagicIface::class, "read")); +return true;"##, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + concat!( + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[EvalReflectParamMagicNs\\eval_reflect_param_magic]", + "[][][EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox::own]", + "[own]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicBox]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicTrait::source]", + "[source]", + "[EvalReflectParamMagicNs]:", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface]", + "[EvalReflectParamMagicNs\\EvalReflectParamMagicIface::read]", + "[read]", + "[]", + "[EvalReflectParamMagicNs]:" + ) + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes eval property default metadata as an associative map. +#[test] +fn execute_program_reflection_class_get_default_properties_metadata() { + let program = parse_fragment( + br#"class EvalReflectDefaultBase { + public int $base = 1; + protected string $prot = "p"; + private int $shadow = 3; + public $implicit; + public int $typed; + public static string $baseStatic = "bs"; +} +class EvalReflectDefaultChild extends EvalReflectDefaultBase { + public int $child = 5; + private int $shadow = 9; + public static int $childStatic = 7; + public ?int $nullable = null; +} +$defaults = (new ReflectionClass("EvalReflectDefaultChild"))->getDefaultProperties(); +echo $defaults["childStatic"]; echo ":"; +echo $defaults["baseStatic"]; echo ":"; +echo $defaults["child"]; echo ":"; +echo $defaults["shadow"]; echo ":"; +echo $defaults["base"]; echo ":"; +echo $defaults["prot"]; echo ":"; +echo array_key_exists("implicit", $defaults) && $defaults["implicit"] === null ? "I:" : "i:"; +echo array_key_exists("nullable", $defaults) && $defaults["nullable"] === null ? "N:" : "n:"; +echo array_key_exists("typed", $defaults) ? "T" : "t"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:bs:5:9:1:p:I:N:t"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/property_values.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/property_values.rs new file mode 100644 index 0000000000..bf5ed2dfae --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/property_values.rs @@ -0,0 +1,474 @@ +//! Purpose: +//! Interpreter tests for ReflectionProperty value access and parameter construction. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Static, instance, dynamic, hooked, raw, and initialized-state paths are covered. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies ReflectionProperty can read and write eval instance and static property values. +#[test] +fn execute_program_reflection_property_gets_and_sets_eval_values() { + let program = parse_fragment( + br#"class EvalReflectValueBase { + private $secret = "base"; + public static $count = 1; +} +class EvalReflectValueChild extends EvalReflectValueBase { + protected $name = "Ada"; +} +class EvalReflectValueHook { + public $raw = 2; + public $doubled { + get => $this->raw * 2; + set { $this->raw = $value + 1; } + } + public $backed { + get { return $this->backed * 2; } + set { $this->backed = $value; } + } + public $virtual { + get => $this->raw + 100; + } + public function __construct() { + $this->backed = 2; + } +} +$child = new EvalReflectValueChild(); +$secret = new ReflectionProperty("EvalReflectValueBase", "secret"); +echo $secret->getValue($child); echo ":"; +$secret->setValue($child, "changed"); +echo $secret->getValue(object: $child); echo ":"; +$name = new ReflectionProperty("EvalReflectValueChild", "name"); +echo $name->getValue($child); echo ":"; +$name->setValue(objectOrValue: $child, value: "Grace"); +echo $name->getValue($child); echo ":"; +$count = new ReflectionProperty("EvalReflectValueBase", "count"); +echo $count->getValue(); echo ":"; +$count->setValue(5); +echo EvalReflectValueChild::$count; echo ":"; +$count->setValue(null, 6); +echo $count->getValue($child); echo ":"; +$hook = new EvalReflectValueHook(); +$doubled = new ReflectionProperty("EvalReflectValueHook", "doubled"); +echo $doubled->getValue($hook); echo ":"; +$doubled->setValue($hook, 4); +echo $hook->raw; echo ":"; +echo $doubled->getValue($hook); echo ":"; +$backed = new ReflectionProperty("EvalReflectValueHook", "backed"); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +$backed->setValue($hook, 4); +echo $backed->getRawValue(object: $hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +$backed->setRawValue(object: $hook, value: 7); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +echo $backed->isLazy($hook) ? "L" : "l"; echo ":"; +$backed->skipLazyInitialization(object: $hook); +$backed->setRawValueWithoutLazyInitialization(object: $hook, value: 8); +echo $backed->getRawValue($hook); echo ":"; +echo $backed->getValue($hook); echo ":"; +echo $backed->getModifiers(); echo ":"; +echo $backed->isVirtual() ? "V" : "b"; echo ":"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->isVirtual() ? "V" : "b"; echo ":"; +echo (new ReflectionProperty("EvalReflectValueHook", "virtual"))->getModifiers(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "base:changed:Ada:Grace:1:5:6:4:5:10:2:4:4:8:7:14:l:8:16:1:b:V:513" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty raw APIs reject virtual eval property hooks. +#[test] +fn execute_program_reflection_property_rejects_virtual_raw_value() { + let program = parse_fragment( + br#"class EvalReflectVirtualRawHook { + public $raw = 2; + public $virtual { + get => $this->raw * 2; + } +} +$object = new EvalReflectVirtualRawHook(); +$property = new ReflectionProperty("EvalReflectVirtualRawHook", "virtual"); +$property->getRawValue($object); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("virtual raw property read should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies ReflectionProperty reports eval instance and static initialization state. +#[test] +fn execute_program_reflection_property_reports_initialized_state() { + let program = parse_fragment( + br#"class EvalReflectInitializedTarget { + public int $typed; + public ?int $nullable; + public $plain; + public static int $staticTyped; + public static $staticPlain; + public $virtual { + get => 42; + } +} +$object = new EvalReflectInitializedTarget(); +$typed = new ReflectionProperty("EvalReflectInitializedTarget", "typed"); +$nullable = new ReflectionProperty("EvalReflectInitializedTarget", "nullable"); +$plain = new ReflectionProperty("EvalReflectInitializedTarget", "plain"); +$staticTyped = new ReflectionProperty("EvalReflectInitializedTarget", "staticTyped"); +$staticPlain = new ReflectionProperty("EvalReflectInitializedTarget", "staticPlain"); +$virtual = new ReflectionProperty("EvalReflectInitializedTarget", "virtual"); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +echo $plain->isInitialized(object: $object) ? "P" : "p"; echo ":"; +echo $staticTyped->isInitialized() ? "S" : "s"; echo ":"; +echo $staticPlain->isInitialized() ? "N" : "n"; echo ":"; +EvalReflectInitializedTarget::$staticTyped = 3; +echo $staticTyped->isInitialized() ? "S" : "s"; echo ":"; +$object->typed = 5; +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +unset($object->typed); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +$typed->setRawValue(object: $object, value: 9); +echo $typed->isInitialized($object) ? "T" : "t"; echo ":"; +echo $nullable->isInitialized($object) ? "Y" : "y"; echo ":"; +$nullable->setValue($object, null); +echo $nullable->isInitialized($object) ? "Y" : "y"; echo ":"; +echo $virtual->isInitialized($object) ? "V" : "v"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "t:P:s:N:S:T:t:T:y:Y:V"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty materializes and operates on public dynamic properties. +#[test] +fn execute_program_reflection_property_supports_dynamic_properties() { + let program = parse_fragment( + br#"class EvalReflectDynamicBase {} +class EvalReflectDynamicChild extends EvalReflectDynamicBase {} +$object = new EvalReflectDynamicBase(); +$object->dynamic = "first"; +$child = new EvalReflectDynamicChild(); +$child->dynamic = "child"; +$empty = new EvalReflectDynamicChild(); +$property = new ReflectionProperty($object, "dynamic"); +echo $property->getName(); echo ":"; +echo $property->isDynamic() ? "D" : "d"; echo ":"; +echo $property->isDefault() ? "Y" : "N"; echo ":"; +echo $property->getModifiers(); echo ":"; +echo is_null($property->getType()) ? "T" : "t"; echo ":"; +echo is_null($property->getSettableType()) ? "S" : "s"; echo ":"; +echo $property->hasDefaultValue() ? "H" : "h"; echo ":"; +echo is_null($property->getDefaultValue()) ? "V" : "v"; echo ":"; +echo $property->isLazy($object) ? "L" : "l"; echo ":"; +echo $property->isInitialized($object) ? "I" : "i"; echo ":"; +echo $property->getValue($object); echo ":"; +echo $property->getValue($child); echo ":"; +echo $property->isInitialized($empty) ? "E" : "e"; echo ":"; +echo is_null($property->getValue($empty)) ? "null" : "bad"; echo ":"; +$property->setValue($empty, "filled"); +echo $property->getValue($empty); echo ":"; +$property->setRawValue($object, "raw"); +echo $property->getRawValue($object); echo ":"; +echo str_replace("\n", "\\n", $property->__toString()); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "dynamic:D:N:1:T:S:h:V:l:I:first:child:e:null:filled:raw:Property [ public $dynamic ]\\n" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionProperty exposes eval property hook metadata and methods. +#[test] +fn execute_program_reflection_property_gets_eval_hook_metadata() { + let program = parse_fragment( + br#"class EvalReflectHookedProperty { + public int $raw = 2; + public int $doubled { + get { return $this->raw * 2; } + set { $this->raw = $value; } + } + public int $readonlyHook { + get => $this->raw + 1; + } + public int $plain = 5; +} +abstract class EvalReflectAbstractHookProperty { + abstract public int $contract { get; set; } +} +interface EvalReflectInterfaceHookProperty { + public int $iface { get; } +} +$hooked = new ReflectionProperty("EvalReflectHookedProperty", "doubled"); +$plain = new ReflectionProperty("EvalReflectHookedProperty", "plain"); +$readonly = new ReflectionProperty("EvalReflectHookedProperty", "readonlyHook"); +$abstract = new ReflectionProperty("EvalReflectAbstractHookProperty", "contract"); +$iface = new ReflectionProperty("EvalReflectInterfaceHookProperty", "iface"); +$getCase = PropertyHookType::Get; +$setCase = PropertyHookType::Set; +echo $getCase->name; echo ":"; echo $getCase->value; echo ":"; +$caseList = PropertyHookType::cases(); +echo count($caseList); echo ":"; echo $caseList[0]->name; echo ":"; echo $caseList[1]->value; echo ":"; +echo PropertyHookType::from("set")->name; echo ":"; +echo PropertyHookType::tryFrom("missing") === null ? "T" : "t"; echo ":"; +echo $hooked->hasHooks() ? "H" : "h"; echo ":"; +echo $hooked->hasHook($getCase) ? "G" : "g"; echo ":"; +echo $hooked->hasHook(type: $setCase) ? "S" : "s"; echo ":"; +$hooks = $hooked->getHooks(); +echo count($hooks); echo ":"; echo $hooks["get"]->getName(); echo ":"; echo $hooks["set"]->getName(); echo ":"; +$get = $hooked->getHook($getCase); +$set = $hooked->getHook(type: $setCase); +echo $get->getDeclaringClass()->getName(); echo ":"; echo $get->getNumberOfParameters(); echo ":"; +echo $set->getNumberOfParameters(); echo ":"; echo $set->getParameters()[0]->getName(); echo ":"; +$box = new EvalReflectHookedProperty(); +echo $get->invoke($box); echo ":"; +$set->invoke($box, 7); +echo $box->raw; echo ":"; +echo $readonly->hasHook($getCase) ? "R" : "r"; echo ":"; +echo $readonly->hasHook($setCase) ? "w" : "W"; echo ":"; +echo $readonly->getHook($setCase) === null ? "N" : "n"; echo ":"; +echo $plain->hasHooks() ? "bad" : "plain"; echo ":"; +echo count($plain->getHooks()); echo ":"; +$abstractHooks = $abstract->getHooks(); +echo count($abstractHooks); echo ":"; +echo $abstract->hasHook($getCase) ? "AG" : "ag"; echo ":"; +echo $abstract->hasHook($setCase) ? "AS" : "as"; echo ":"; +echo $abstractHooks["get"]->getName(); echo ":"; echo $abstractHooks["get"]->isAbstract() ? "A" : "a"; echo ":"; +echo $abstractHooks["set"]->getName(); echo ":"; echo $abstractHooks["set"]->isAbstract() ? "A" : "a"; echo ":"; +$ifaceHook = $iface->getHook($getCase); +echo count($iface->getHooks()); echo ":"; +echo $iface->hasHook($getCase) ? "IG" : "ig"; echo ":"; +echo $iface->hasHook($setCase) ? "bad" : "is"; echo ":"; +echo $ifaceHook->isAbstract() ? "IA" : "ia"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Get:get:2:Get:set:Set:T:H:G:S:2:$doubled::get:$doubled::set:EvalReflectHookedProperty:0:1:value:4:7:R:W:N:plain:0:2:AG:AS:$contract::get:A:$contract::set:A:1:IG:is:IA" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionClass exposes and mutates eval static property values. +#[test] +fn execute_program_reflection_class_static_property_values() { + let program = parse_fragment( + br#"class EvalReflectStaticBase { + public static $base = "b"; + protected static $prot = "p"; + private static $shadow = "base-hidden"; + public $instance = "i"; +} +class EvalReflectStaticChild extends EvalReflectStaticBase { + public static $child = "c"; + private static $shadow = "child-hidden"; + public static int $count = 1; +} +EvalReflectStaticChild::$child = "mut"; +$ref = new ReflectionClass("EvalReflectStaticChild"); +$statics = $ref->getStaticProperties(); +echo count($statics); echo ":"; +echo $statics["child"]; echo ":"; +echo $statics["base"]; echo ":"; +echo $statics["prot"]; echo ":"; +echo $statics["shadow"]; echo ":"; +echo $ref->getStaticPropertyValue("count"); echo ":"; +$ref->setStaticPropertyValue("shadow", "changed"); +echo $ref->getStaticPropertyValue("shadow"); echo ":"; +$ref->setStaticPropertyValue(name: "count", value: 5); +echo EvalReflectStaticChild::$count; echo ":"; +echo $ref->getStaticPropertyValue("instance", "fallback"); echo ":"; +echo $ref->getStaticPropertyValue("missing", "fallback"); echo ":"; +try { + $ref->getStaticPropertyValue("missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo "E"; +} +echo ":"; +try { + $ref->setStaticPropertyValue("instance", "bad"); + echo "bad"; +} catch (ReflectionException $e) { + echo "S"; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "5:mut:b:p:child-hidden:1:changed:5:fallback:fallback:E:S" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval ReflectionParameter exposes declaring class metadata. +#[test] +fn execute_program_reflects_eval_parameter_declaring_class() { + let program = parse_fragment( + br#"class EvalDeclaringParamBase { + public function inherited($base) {} +} +class EvalDeclaringParamChild extends EvalDeclaringParamBase { + public function own($child) {} +} +$inherited = (new ReflectionMethod("EvalDeclaringParamChild", "inherited"))->getParameters()[0]; +echo $inherited->getDeclaringClass()->getName(); echo ":"; +echo $inherited->getDeclaringFunction()->getName(); echo ":"; +echo $inherited->getDeclaringFunction()->getDeclaringClass()->getName(); echo ":"; +$listed = (new ReflectionMethod("EvalDeclaringParamChild", "own"))->getParameters()[0]; +echo $listed->getDeclaringClass()->getName(); echo ":"; +echo $listed->getDeclaringFunction()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalDeclaringParamBase:inherited:EvalDeclaringParamBase:EvalDeclaringParamChild:own" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies direct ReflectionParameter construction accepts runtime object method targets. +#[test] +fn execute_program_reflection_parameter_accepts_object_expression_target() { + let program = parse_fragment( + br#"class EvalDirectParamObjectTarget { + public function run(int $id, ?string $name = null) {} +} +$param = new ReflectionParameter([new EvalDirectParamObjectTarget(), "run"], "name"); +echo $param->getName(); echo ":"; +echo $param->getPosition(); echo ":"; +echo $param->getDeclaringClass()->getName(); echo ":"; +echo $param->getDeclaringFunction()->getName(); echo ":"; +echo $param->isOptional() ? "O" : "R"; echo ":"; +echo $param->getType()->getName(); echo ":"; +echo $param->allowsNull() ? "N" : "n"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "name:1:EvalDirectParamObjectTarget:run:O:string:N" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies ReflectionParameter construction throws catchable PHP reflection errors. +#[test] +fn execute_program_reflection_parameter_constructor_throws_reflection_exceptions() { + let program = parse_fragment( + br#"function eval_reflect_param_error_function($known) {} +class EvalReflectParamErrorTarget { + public function run($known) {} +} +try { + new ReflectionParameter("eval_reflect_param_error_function", "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], "missing"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], 3); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "missing"], "known"); + echo "bad"; +} catch (ReflectionException $e) { + echo $e->getMessage(); +} +echo "|"; +try { + new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], -1); + echo "bad"; +} catch (ValueError $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +echo "|"; +echo (new ReflectionParameter(["EvalReflectParamErrorTarget", "run"], "known"))->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + assert!( + result.is_ok(), + "execute eval ir failed after output {:?}", + values.output + ); + + assert_eq!( + values.output, + "ReflectionException:The parameter specified by its name could not be found|The parameter specified by its name could not be found|The parameter specified by its offset could not be found|Method EvalReflectParamErrorTarget::missing() does not exist|ValueError:ReflectionParameter::__construct(): Argument #2 ($param) must be greater than or equal to 0|known" + ); + assert_eq!(values.get(result.expect("execute eval ir")), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/relations.rs b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/relations.rs new file mode 100644 index 0000000000..f0d284640c --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/builtins_class_metadata/relations.rs @@ -0,0 +1,270 @@ +//! Purpose: +//! Interpreter tests for class relations, visible members, and class variables. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Eval and AOT class targets share relation-builtin behavior. +//! - Visibility-sensitive OOP probes run through the fake interpreter runtime. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies class-relation helpers handle eval class and trait targets. +#[test] +fn execute_program_dispatches_class_relation_builtins() { + let program = parse_fragment( + br#"class EvalMeta {} +trait EvalMetaInnerTrait {} +trait EvalMetaOuterTrait { + use EvalMetaInnerTrait; +} +$object = new EvalMeta(); +$implements = class_implements("EvalMeta"); +echo is_array($implements) && count($implements) === 0 ? "impl" : "bad"; echo ":"; +$parents = class_parents($object); +echo is_array($parents) && count($parents) === 0 ? "parents" : "bad"; echo ":"; +$uses = class_uses("EvalMeta"); +echo is_array($uses) && count($uses) === 0 ? "uses" : "bad"; echo ":"; +echo class_implements("MissingMeta") === false ? "missing" : "bad"; echo ":"; +$call = call_user_func("class_implements", "EvalMeta"); +echo is_array($call) && count($call) === 0 ? "call" : "bad"; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => "EvalMeta"]); +echo is_array($named) && count($named) === 0 ? "named" : "bad"; echo ":"; +$trait_uses = class_uses("EvalMetaOuterTrait"); +echo $trait_uses["EvalMetaInnerTrait"]; echo ":"; +class_alias("EvalMetaOuterTrait", "EvalMetaOuterTraitAlias"); +$alias_uses = class_uses("EvalMetaOuterTraitAlias"); +echo $alias_uses["EvalMetaInnerTrait"]; echo ":"; +echo function_exists("class_implements"); echo function_exists("class_parents"); +echo function_exists("class_uses"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "impl:parents:uses:missing:call:named:EvalMetaInnerTrait:EvalMetaInnerTrait:111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval-declared parent and interface metadata is exposed to relation builtins. +#[test] +fn execute_program_reports_eval_class_relation_metadata() { + let program = parse_fragment( + br#"class EvalMetaBase {} +class EvalMetaChild extends EvalMetaBase implements KnownInterface {} +$object = new EvalMetaChild(); +$implements = class_implements($object); +echo count($implements); echo ":"; +echo $implements["KnownInterface"]; echo ":"; +echo $implements["Traversable"]; echo ":"; +$parents = class_parents("EvalMetaChild"); +echo count($parents); echo ":"; +echo $parents["EvalMetaBase"]; echo ":"; +$call = call_user_func("class_implements", "EvalMetaChild"); +echo $call["KnownInterface"]; echo ":"; +echo $call["Traversable"]; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => $object]); +echo $named["EvalMetaBase"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "2:KnownInterface:Traversable:1:EvalMetaBase:KnownInterface:Traversable:EvalMetaBase" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies generated/AOT parent and interface metadata is exposed to relation builtins. +#[test] +fn execute_program_reports_aot_class_relation_metadata() { + let program = parse_fragment( + br#"$implements = class_implements("KnownClass"); +echo count($implements); echo ":"; +echo $implements["KnownInterface"]; echo ":"; +$parents = class_parents("KnownClass"); +echo count($parents); echo ":"; +echo $parents["ParentClass"]; echo ":"; +$call = call_user_func("class_implements", "KnownClass"); +echo $call["KnownInterface"]; echo ":"; +$interfaceParents = class_implements("KnownInterface"); +echo $interfaceParents["Traversable"]; echo ":"; +$uses = class_uses("KnownClass"); +echo count($uses); echo ":"; echo $uses["KnownTrait"]; echo ":"; +$traitUses = class_uses("KnownTrait"); +echo $traitUses["KnownInnerTrait"]; echo ":"; +$named = call_user_func_array("class_parents", ["object_or_class" => "KnownClass"]); +echo $named["ParentClass"]; echo ":"; +class_alias("KnownClass", "KnownAlias"); +$aliasImplements = class_implements("KnownAlias"); +echo $aliasImplements["KnownInterface"]; echo ":"; +$aliasParents = class_parents("KnownAlias"); +echo $aliasParents["ParentClass"]; echo ":"; +$aliasUses = class_uses("KnownAlias"); +echo $aliasUses["KnownTrait"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + assert!(context.define_native_class_parent("KnownClass", "ParentClass")); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = + execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!( + values.output, + "1:KnownInterface:1:ParentClass:KnownInterface:Traversable:1:KnownTrait:KnownInnerTrait:ParentClass:KnownInterface:ParentClass:KnownTrait" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies PHP OOP introspection builtins follow eval visibility and scope rules. +#[test] +fn execute_program_dispatches_oop_introspection_builtins() { + let program = parse_fragment( + br#"class EvalOopIntrospectBase { + private $baseSecret = "bp"; + protected $baseProtected = "bq"; + public $basePublic = "br"; + private function basePrivate() {} + protected function baseProtectedMethod() {} + public function basePublicMethod() {} + public function parentView() { + $vars = get_object_vars($this); + ksort($vars); + echo implode(",", array_keys($vars)); + } +} +class EvalOopIntrospectChild extends EvalOopIntrospectBase { + private $childSecret = "cp"; + protected $childProtected = "cq"; + public $childPublic = "cr"; + private function childPrivate() {} + protected function childProtectedMethod() {} + public function childPublicMethod() {} + public function childView() { + $methods = get_class_methods($this); + sort($methods); + echo implode(",", $methods); echo "|"; + $vars = get_object_vars($this); + ksort($vars); + echo implode(",", array_keys($vars)); + } +} +$object = new EvalOopIntrospectChild(); +$object->dynamic = "dyn"; +echo method_exists("EvalOopIntrospectChild", "basePrivate") ? "bad" : "noParentPrivateMethod"; echo ":"; +echo method_exists($object, "basePrivate") ? "objectParentPrivateMethod" : "bad"; echo ":"; +echo method_exists("EvalOopIntrospectChild", "baseProtectedMethod") ? "classProtectedMethod" : "bad"; echo ":"; +echo property_exists("EvalOopIntrospectChild", "baseSecret") ? "bad" : "noParentPrivateProperty"; echo ":"; +echo property_exists($object, "baseSecret") ? "bad" : "noObjectParentPrivateProperty"; echo ":"; +echo property_exists($object, "dynamic") ? "dynamicProperty" : "bad"; echo ":"; +$methods = get_class_methods("EvalOopIntrospectChild"); +sort($methods); +echo implode(",", $methods); echo ":"; +$vars = get_object_vars($object); +ksort($vars); +echo implode(",", array_keys($vars)); echo ":"; +$object->childView(); echo ":"; +$object->parentView(); echo ":"; +echo call_user_func("method_exists", $object, "childPrivate") ? "callMethod" : "bad"; echo ":"; +echo call_user_func_array("property_exists", ["property" => "dynamic", "object_or_class" => $object]) ? "namedProperty" : "bad"; echo ":"; +echo function_exists("method_exists"); echo function_exists("property_exists"); +echo function_exists("get_class_methods"); echo function_exists("get_object_vars"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "noParentPrivateMethod:objectParentPrivateMethod:classProtectedMethod:noParentPrivateProperty:noObjectParentPrivateProperty:dynamicProperty:basePublicMethod,childPublicMethod,childView,parentView:basePublic,childPublic,dynamic:baseProtectedMethod,basePublicMethod,childPrivate,childProtectedMethod,childPublicMethod,childView,parentView|baseProtected,basePublic,childProtected,childPublic,childSecret,dynamic:baseProtected,basePublic,baseSecret,childProtected,childPublic,dynamic:callMethod:namedProperty:1111" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies `get_class_vars()` materializes visible defaults for eval class-like metadata. +#[test] +fn execute_program_dispatches_get_class_vars_builtin() { + let program = parse_fragment( + br#"trait EvalClassVarsTrait { + public $traitPublic = "tp"; + protected $traitProtected = "tq"; +} +enum EvalClassVarsBacked: int { case Ready = 1; } +class EvalClassVarsBase { + public $basePublic = "bp"; + protected $baseProtected = "bq"; + private $basePrivate = "bs"; + public static $baseStatic = "static"; + public int $typed; +} +class EvalClassVarsChild extends EvalClassVarsBase { + use EvalClassVarsTrait; + public $childPublic = "cp"; + protected $childProtected = "cq"; + private $childPrivate = "cs"; + public function childView() { + $vars = get_class_vars(self::class); + ksort($vars); + foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } + public function baseView() { + $vars = get_class_vars(EvalClassVarsBase::class); + ksort($vars); + foreach ($vars as $name => $value) { + echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; + } + } +} +$outside = get_class_vars("EvalClassVarsChild"); +ksort($outside); +foreach ($outside as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +(new EvalClassVarsChild())->childView(); +echo ":"; +(new EvalClassVarsChild())->baseView(); +echo ":"; +$trait = call_user_func("get_class_vars", "EvalClassVarsTrait"); +ksort($trait); +foreach ($trait as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +$enum = call_user_func_array("get_class_vars", ["class" => "EvalClassVarsBacked"]); +ksort($enum); +foreach ($enum as $name => $value) { echo $name . "=" . (is_null($value) ? "null" : $value) . "|"; } +echo ":"; +echo function_exists("get_class_vars") ? "F" : "f"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "basePublic=bp|baseStatic=static|childPublic=cp|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|childPrivate=cs|childProtected=cq|childPublic=cp|traitProtected=tq|traitPublic=tp|typed=null|:baseProtected=bq|basePublic=bp|baseStatic=static|typed=null|:traitPublic=tp|:name=null|value=null|:F" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes.rs b/crates/elephc-magician/src/interpreter/tests/classes.rs deleted file mode 100644 index 3d39977960..0000000000 --- a/crates/elephc-magician/src/interpreter/tests/classes.rs +++ /dev/null @@ -1,2629 +0,0 @@ -//! Purpose: -//! Interpreter tests for eval-declared class runtime behavior. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - These cases cover class property semantics that need eval runtime state. - -use super::super::*; -use super::support::*; - -/// Verifies promoted constructor properties initialize before the constructor body runs. -#[test] -fn execute_program_initializes_constructor_promoted_properties() { - let program = parse_fragment( - br#"class EvalPromotedUser { - public function __construct(public int $id, private string $name = "Ada") { - $this->id = $this->id + 1; - } - public function label() { return $this->id . ":" . $this->name; } -} -$user = new EvalPromotedUser(6); -echo $user->id; echo ":"; -return $user->label();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "7:"); - assert_eq!(values.get(result), FakeValue::String("7:Ada".to_string())); -} - -/// Verifies `new self/static/parent` resolve inside eval-declared methods. -#[test] -fn execute_program_constructs_relative_class_names_from_eval_methods() { - let program = parse_fragment( - br#"class EvalRelativeFactoryBase { - public string $label; - public function __construct($label = "base") { $this->label = $label; } - public function selfFactory() { return new self("self"); } - public function staticFactory() { return new static("static"); } -} -class EvalRelativeFactoryChild extends EvalRelativeFactoryBase { - public function parentFactory() { return new parent("parent"); } -} -$child = new EvalRelativeFactoryChild("root"); -$self = $child->selfFactory(); -$static = $child->staticFactory(); -$parent = $child->parentFactory(); -echo get_class($self); echo ":"; echo $self->label; echo ":"; -echo get_class($static); echo ":"; echo $static->label; echo ":"; -echo get_class($parent); echo ":"; echo $parent->label; -return $self instanceof EvalRelativeFactoryBase - && !($self instanceof EvalRelativeFactoryChild) - && $static instanceof EvalRelativeFactoryChild - && $parent instanceof EvalRelativeFactoryBase - && !($parent instanceof EvalRelativeFactoryChild);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "EvalRelativeFactoryBase:self:EvalRelativeFactoryChild:static:EvalRelativeFactoryBase:parent" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies `new self/static/parent` stay relative inside eval namespaces. -#[test] -fn execute_program_constructs_namespaced_relative_class_names_from_eval_methods() { - let program = parse_fragment( - br#"namespace EvalRelativeNs; -class Base { - public string $label; - public function __construct($label = "base") { $this->label = $label; } - public function selfFactory() { return new self("self"); } - public function staticFactory() { return new static("static"); } -} -class Child extends Base { - public function parentFactory() { return new parent("parent"); } -} -$child = new Child("root"); -$self = $child->selfFactory(); -$static = $child->staticFactory(); -$parent = $child->parentFactory(); -echo get_class($self); echo ":"; echo $self->label; echo ":"; -echo get_class($static); echo ":"; echo $static->label; echo ":"; -echo get_class($parent); echo ":"; echo $parent->label; -return $self instanceof Base - && !($self instanceof Child) - && $static instanceof Child - && $parent instanceof Base - && !($parent instanceof Child);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "EvalRelativeNs\\Base:self:EvalRelativeNs\\Child:static:EvalRelativeNs\\Base:parent" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies PHP legacy `var` properties behave as public eval properties. -#[test] -fn execute_program_supports_legacy_var_properties() { - let program = parse_fragment( - br#"trait EvalLegacyVarTrait { - var ?string $label = "trait"; -} -class EvalLegacyVarProperty { - use EvalLegacyVarTrait; - var $plain = "p"; - var ?int $count = null; -} -$object = new EvalLegacyVarProperty(); -$plain = new ReflectionProperty("EvalLegacyVarProperty", "plain"); -$count = new ReflectionProperty("EvalLegacyVarProperty", "count"); -$label = new ReflectionProperty("EvalLegacyVarProperty", "label"); -$defaults = (new ReflectionClass("EvalLegacyVarProperty"))->getDefaultProperties(); -echo $object->plain; echo ":"; -echo $plain->isPublic() ? "P" : "p"; echo ":"; -echo $plain->hasType() ? "T" : "t"; echo ":"; -echo $count->isPublic() ? "C" : "c"; echo ":"; -echo $count->hasType() ? $count->getType()->getName() : "none"; echo ":"; -echo $count->getType()->allowsNull() ? "N" : "n"; echo ":"; -echo is_null($defaults["count"]) ? "null" : "bad"; echo ":"; -echo $object->label; echo ":"; -echo $label->isPublic() ? "L" : "l"; echo ":"; -echo $label->getType()->getName(); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "p:P:t:C:int:N:null:trait:L:string"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies comma-separated eval properties initialize instance, static, and trait storage. -#[test] -fn execute_program_reads_comma_separated_eval_properties() { - let program = parse_fragment( - br#"class EvalMultiPropertyBox { - public int $a = 1, $b = 2; - public static int $s = 3, $t = 4; - public function sum() { return $this->a + $this->b + self::$s + self::$t; } -} -trait EvalMultiPropertyTrait { - public int $x = 5, $y = 6; -} -class EvalMultiPropertyTraitBox { - use EvalMultiPropertyTrait; - public function sum() { return $this->x + $this->y; } -} -$box = new EvalMultiPropertyBox(); -$traitBox = new EvalMultiPropertyTraitBox(); -echo $box->a; echo $box->b; echo ":"; -echo EvalMultiPropertyBox::$s; echo EvalMultiPropertyBox::$t; echo ":"; -echo $traitBox->x; echo $traitBox->y; echo ":"; -return $box->sum() + $traitBox->sum();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "12:34:56:"); - assert_eq!(values.get(result), FakeValue::Int(21)); -} - -/// Verifies eval static method calls preserve PHP late-static forwarding rules. -#[test] -fn execute_program_forwards_eval_static_method_called_class() { - let program = parse_fragment( - br#"class EvalForwardA { - public static function who() { return static::tag(); } - public static function relayNamed() { return EvalForwardA::who(); } - public static function relaySelf() { return self::who(); } - public static function tag() { return "A"; } -} -class EvalForwardB extends EvalForwardA { - public static function relayParent() { return parent::who(); } - public static function relayStatic() { return static::who(); } - public static function tag() { return "B"; } -} -echo EvalForwardB::relayNamed(); echo ":"; -echo EvalForwardB::relaySelf(); echo ":"; -echo EvalForwardB::relayParent(); echo ":"; -return EvalForwardB::relayStatic();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A:B:B:"); - assert_eq!(values.get(result), FakeValue::String("B".to_string())); -} - -/// Verifies `get_called_class()` follows eval late-static method scopes. -#[test] -fn execute_program_dispatches_get_called_class_builtin() { - let program = parse_fragment( - br#"class EvalCalledClassBase { - public function instanceWho() { return get_called_class(); } - public function instanceCall() { return call_user_func("get_called_class"); } - public static function staticWho() { return get_called_class(); } - public static function staticCallArray() { return call_user_func_array("get_called_class", []); } - public static function makeCallable() { return get_called_class(...); } -} -class EvalCalledClassChild extends EvalCalledClassBase {} -$child = new EvalCalledClassChild(); -echo $child->instanceWho(); echo ":"; -echo $child->instanceCall(); echo ":"; -echo EvalCalledClassChild::staticWho(); echo ":"; -echo EvalCalledClassChild::staticCallArray(); echo ":"; -echo EvalCalledClassBase::staticWho(); echo ":"; -try { - get_called_class(); -} catch (Error $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); echo ":"; -} -$fn = EvalCalledClassChild::makeCallable(); -try { - $fn(); -} catch (Error $e) { - echo "callable:"; -} -echo function_exists("get_called_class"); echo is_callable("get_called_class"); -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassBase:Error:get_called_class() must be called from within a class:callable:11" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval classes can extend runtime/AOT classes through the dynamic backing object. -#[test] -fn execute_program_extends_runtime_class_from_eval_declaration() { - let program = parse_fragment( - br#"class EvalRuntimeParentChild extends KnownClass { - public function own() { return $this->read_x() + 1; } -} -$box = new EvalRuntimeParentChild(9); -echo get_class($box); echo ":"; -echo get_parent_class($box); echo ":"; -echo is_a($box, "EvalRuntimeParentChild") ? "D" : "d"; echo ":"; -echo is_a($box, "KnownClass") ? "K" : "k"; echo ":"; -echo is_a($box, "KnownInterface") ? "I" : "i"; echo ":"; -echo is_subclass_of($box, "KnownClass") ? "S" : "s"; echo ":"; -echo is_subclass_of("EvalRuntimeParentChild", "KnownClass") ? "N" : "n"; echo ":"; -echo $box->read_x(); echo ":"; -return $box->own();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "EvalRuntimeParentChild:KnownClass:D:K:I:S:N:9:" - ); - assert_eq!(values.get(result), FakeValue::Int(10)); -} - -/// Verifies eval classes cannot directly implement PHP's special Throwable contract. -#[test] -fn execute_program_rejects_eval_class_implementing_throwable_contracts() { - for (source, label) in [ - ( - br#"class EvalInvalidThrowableClass implements Throwable {}"# as &[u8], - "direct Throwable implementation should fail", - ), - ( - br#"interface EvalThrowableMarker extends Throwable {} -class EvalInvalidThrowableMarkerClass implements EvalThrowableMarker {}"#, - "Throwable-derived interface implementation should fail", - ), - ] { - let program = parse_fragment(source).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values).expect_err(label); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } -} - -/// Verifies eval classes must satisfy methods required by PHP builtin interfaces. -#[test] -fn execute_program_rejects_invalid_builtin_interface_implementations() { - for (source, label) in [ - ( - br#"class EvalMissingCountable implements Countable {}"# as &[u8], - "missing Countable::count should fail", - ), - ( - br#"class EvalBadCountableReturn implements Countable { - public function count(): string { return "1"; } -}"#, - "incompatible Countable::count return type should fail", - ), - ( - br#"class EvalMissingStringable implements Stringable {}"#, - "missing Stringable::__toString should fail", - ), - ( - br#"class EvalBadIterator implements Iterator { - public function current(): mixed { return null; } - public function key(): mixed { return null; } - public function next(): void {} - public function valid(): bool { return false; } -}"#, - "missing Iterator::rewind should fail", - ), - ( - br#"class EvalBadJsonSerializable implements JsonSerializable { - public static function jsonSerialize(): mixed { return []; } -}"#, - "static JsonSerializable::jsonSerialize should fail", - ), - ] { - let program = parse_fragment(source).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values).expect_err(label); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } -} - -/// Verifies abstract eval classes can defer PHP builtin interface methods. -#[test] -fn execute_program_allows_abstract_builtin_interface_implementations() { - let program = parse_fragment( - br#"abstract class EvalAbstractCountable implements Countable {} -return class_exists("EvalAbstractCountable");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval-declared `ArrayAccess` objects dispatch reads, writes, append, probes, and unset. -#[test] -fn execute_program_dispatches_eval_array_access_objects() { - let program = parse_fragment( - br#"class EvalArrayAccessBox implements ArrayAccess { - public function offsetExists(mixed $offset): bool { - echo "exists:" . $offset . ":"; - if ($offset === "missing") { - return false; - } - return true; - } - public function offsetGet(mixed $offset): mixed { - echo "get:" . $offset . ":"; - if ($offset === "empty") { - return ""; - } - return "v" . $offset; - } - public function offsetSet(mixed $offset, mixed $value): void { - if ($offset === null) { - echo "set:null:" . $value . ":"; - } else { - echo "set:" . $offset . ":" . $value . ":"; - } - } - public function offsetUnset(mixed $offset): void { - echo "unset:" . $offset . ":"; - } -} -$box = new EvalArrayAccessBox(); -$box["x"] = "1"; -$box[] = "tail"; -unset($box["drop"]); -if (isset($box["x"])) { echo "I:"; } else { echo "i:"; } -if (isset($box["missing"])) { echo "M:"; } else { echo "m:"; } -if (empty($box["empty"])) { echo "E:"; } else { echo "e:"; } -if (empty($box["missing"])) { echo "N:"; } else { echo "n:"; } -return $box["y"];"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "set:x:1:set:null:tail:unset:drop:exists:x:I:exists:missing:m:exists:empty:get:empty:E:exists:missing:N:get:y:" - ); - assert_eq!(values.get(result), FakeValue::String("vy".to_string())); -} - -/// Verifies by-reference promoted properties stay aliased to caller variables. -#[test] -fn execute_program_aliases_by_reference_promoted_variable_properties() { - let program = parse_fragment( - br#"class EvalPromotedRefBox { - public function __construct(public &$value) {} -} -$value = 1; -$box = new EvalPromotedRefBox($value); -$box->value = 5; -echo $value; echo ":"; -$value = 7; -echo $box->value; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:7"); - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies by-reference promoted properties can alias caller array elements. -#[test] -fn execute_program_aliases_by_reference_promoted_array_element_properties() { - let program = parse_fragment( - br#"class EvalPromotedArrayRefBox { - public function __construct(public &$value) {} -} -$items = [1]; -$box = new EvalPromotedArrayRefBox($items[0]); -$box->value = 5; -echo $items[0]; echo ":"; -$items[0] = 7; -echo $box->value; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:7"); - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies by-reference promoted properties can alias caller object properties. -#[test] -fn execute_program_aliases_by_reference_promoted_object_property_properties() { - let program = parse_fragment( - br#"class EvalPromotedObjectRefHolder { - public $value = 1; -} -class EvalPromotedObjectRefBox { - public function __construct(public &$value) {} -} -$holder = new EvalPromotedObjectRefHolder(); -$box = new EvalPromotedObjectRefBox($holder->value); -$box->value = 5; -echo $holder->value; echo ":"; -$holder->value = 7; -echo $box->value; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:7"); - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies by-reference promoted properties can alias static and nested property targets. -#[test] -fn execute_program_aliases_by_reference_promoted_static_and_nested_properties() { - let program = parse_fragment( - br#"class EvalPromotedStaticRefHolder { - public static $value = 1; - public $items = [1]; - public static $staticItems = [1]; -} -class EvalPromotedStaticRefBox { - public function __construct(public &$value) {} -} -$box = new EvalPromotedStaticRefBox(EvalPromotedStaticRefHolder::$value); -$box->value = 5; -echo EvalPromotedStaticRefHolder::$value; echo ":"; -EvalPromotedStaticRefHolder::$value = 7; -echo $box->value; echo ":"; -$holder = new EvalPromotedStaticRefHolder(); -$itemBox = new EvalPromotedStaticRefBox($holder->items[0]); -$itemBox->value = 11; -echo $holder->items[0]; echo ":"; -$holder->items[0] = 13; -echo $itemBox->value; echo ":"; -$staticItemBox = new EvalPromotedStaticRefBox(EvalPromotedStaticRefHolder::$staticItems[0]); -$staticItemBox->value = 17; -echo EvalPromotedStaticRefHolder::$staticItems[0]; echo ":"; -EvalPromotedStaticRefHolder::$staticItems[0] = 19; -return $staticItemBox->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5:7:11:13:17:"); - assert_eq!(values.get(result), FakeValue::Int(19)); -} - -/// Verifies by-reference promoted defaults use internal property alias storage. -#[test] -fn execute_program_aliases_by_reference_promoted_default_properties() { - let program = parse_fragment( - br#"class EvalPromotedDefaultRefBox { - public function __construct(public &$value = null) {} -} -$box = new EvalPromotedDefaultRefBox(); -$box->value = 5; -echo $box->value; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "5"); - assert_eq!(values.get(result), FakeValue::Int(5)); -} - -/// Verifies readonly by-reference promotion fails when the constructor creates the alias. -#[test] -fn execute_program_rejects_readonly_by_reference_promoted_properties() { - let program = parse_fragment( - br#"class EvalPromotedReadonlyRefBox { - public function __construct(public readonly int &$value) {} -} -$value = 1; -new EvalPromotedReadonlyRefBox($value);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("readonly by-reference promoted property should fail at construction"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies promoted readonly properties throw Error outside their constructor. -#[test] -fn execute_program_promoted_readonly_property_write_after_constructor_throws_error() { - let program = parse_fragment( - br#"class EvalPromotedReadonlyBox { - public function __construct(public readonly int $id) {} - public function replace($id) { $this->id = $id; } -} -$box = new EvalPromotedReadonlyBox(7); -echo $box->id; -try { - $box->replace(8); - echo "bad"; -} catch (Error $e) { - echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "7:Error:Cannot modify readonly property EvalPromotedReadonlyBox::$id" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies readonly eval properties can be initialized inside their constructor. -#[test] -fn execute_program_initializes_readonly_property_in_constructor() { - let program = parse_fragment( - br#"class EvalReadonlyBox { - public readonly int $id; - public function __construct($id) { $this->id = $id; } - public function id() { return $this->id; } -} -$box = new EvalReadonlyBox(7); -echo $box->id(); echo ":"; -return $box->id();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "7:"); - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies direct reads of uninitialized typed eval properties throw catchable PHP errors. -#[test] -fn execute_program_rejects_uninitialized_typed_property_reads() { - let program = parse_fragment( - br#"class EvalTypedReadBox { - public int $typed; - public ?int $nullable; - public ?int $defaultNull = null; - public $plain; -} -$box = new EvalTypedReadBox(); -try { - echo $box->typed; -} catch (Error $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -echo "|"; -try { - echo $box->nullable; -} catch (Error $e) { - echo $e->getMessage(); -} -echo "|"; -echo is_null($box->defaultNull) ? "default-null" : "bad"; -echo "|"; -echo is_null($box->plain) ? "plain-null" : "bad"; -echo "|"; -$box->typed = 0; -echo $box->typed; -echo "|"; -unset($box->typed); -try { - echo $box->typed; -} catch (Error $e) { - echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Typed property EvalTypedReadBox::$typed must not be accessed before initialization|\ -Typed property EvalTypedReadBox::$nullable must not be accessed before initialization|\ -default-null|plain-null|0|\ -Typed property EvalTypedReadBox::$typed must not be accessed before initialization" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies readonly eval properties throw Error on writes outside the declaring constructor. -#[test] -fn execute_program_readonly_property_write_after_constructor_throws_error() { - let program = parse_fragment( - br#"class EvalReadonlyBox { - public readonly int $id; - public function __construct($id) { $this->id = $id; } - public function replace($id) { $this->id = $id; } -} -$box = new EvalReadonlyBox(7); -try { - $box->replace(8); - echo "bad"; -} catch (Error $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Cannot modify readonly property EvalReadonlyBox::$id" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies readonly eval properties must declare a type like PHP requires. -#[test] -fn execute_program_rejects_untyped_readonly_properties() { - let explicit = parse_fragment( - br#"class EvalReadonlyUntypedBox { - public readonly $value; -}"#, - ) - .expect("parse explicit readonly property"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&explicit, &mut scope, &mut values) - .expect_err("explicit readonly property without type should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); - - let readonly_class = parse_fragment( - br#"readonly class EvalReadonlyClassUntypedBox { - public $value; -}"#, - ) - .expect("parse readonly class property"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&readonly_class, &mut scope, &mut values) - .expect_err("readonly class property without type should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies readonly classes make instance properties readonly implicitly. -#[test] -fn execute_program_initializes_readonly_class_property_in_constructor() { - let program = parse_fragment( - br#"readonly class EvalReadonlyClassBox { - public int $id; - public function __construct($id) { $this->id = $id; } - public function id() { return $this->id; } -} -$box = new EvalReadonlyClassBox(11); -echo $box->id(); echo ":"; -return $box->id();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "11:"); - assert_eq!(values.get(result), FakeValue::Int(11)); -} - -/// Verifies readonly class instance properties throw Error on writes after construction. -#[test] -fn execute_program_readonly_class_property_write_after_constructor_throws_error() { - let program = parse_fragment( - br#"readonly class EvalReadonlyClassFailBox { - public int $id; - public function __construct($id) { $this->id = $id; } - public function replace($id) { $this->id = $id; } -} -$box = new EvalReadonlyClassFailBox(11); -try { - $box->replace(12); - echo "bad"; -} catch (Error $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Cannot modify readonly property EvalReadonlyClassFailBox::$id" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies readonly classes throw Error on dynamic property creation without a magic setter. -#[test] -fn execute_program_readonly_class_dynamic_property_creation_throws_error() { - let program = parse_fragment( - br#"readonly class EvalReadonlyDynamicFailBox { - public int $id; - public function __construct($id) { $this->id = $id; } -} -$box = new EvalReadonlyDynamicFailBox(11); -try { - $box->dynamic = 12; - echo "bad"; -} catch (Error $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Cannot create dynamic property EvalReadonlyDynamicFailBox::$dynamic" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies readonly classes may still handle missing property writes through `__set()`. -#[test] -fn execute_program_allows_readonly_class_magic_set_for_missing_properties() { - let program = parse_fragment( - br#"readonly class EvalReadonlyMagicSetBox { - public function __set($name, $value) { - echo $name; echo ":"; echo $value; - } -} -$box = new EvalReadonlyMagicSetBox(); -$box->dynamic = 12; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "dynamic:12"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies readonly classes reject PHP's global dynamic-property marker attribute. -#[test] -fn execute_program_rejects_allow_dynamic_properties_on_readonly_class() { - let program = - parse_fragment(br#"#[\AllowDynamicProperties] readonly class EvalReadonlyAllowDynamic {}"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("AllowDynamicProperties cannot apply to readonly classes"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies namespaced non-builtin attributes do not trigger the readonly-class marker rule. -#[test] -fn execute_program_allows_namespaced_allow_dynamic_properties_on_readonly_class() { - let program = parse_fragment( - br#"namespace EvalReadonlyAttrNs; -#[AllowDynamicProperties] readonly class Box {} -echo class_attribute_names("EvalReadonlyAttrNs\Box")[0]; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "EvalReadonlyAttrNs\\AllowDynamicProperties"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval validates PHP's global `#[Override]` method marker. -#[test] -fn execute_program_validates_override_attribute_targets() { - let valid = parse_fragment( - br#"interface EvalOverrideContract { - public function label(): string; -} -class EvalOverrideBase { - public function name(): string { return "base"; } -} -class EvalOverrideChild extends EvalOverrideBase implements EvalOverrideContract { - #[\Override] - public function name(): string { return "child"; } - #[Override] - public function label(): string { return "contract"; } -} -$box = new EvalOverrideChild(); -echo $box->name() . ":" . $box->label();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - execute_program(&valid, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "child:contract"); - - let invalid = parse_fragment( - br#"class EvalOverrideMissing { - #[\Override] - public function missing(): string { return "bad"; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&invalid, &mut scope, &mut values) - .expect_err("override marker without target should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies interface `#[Override]` methods require an inherited interface method. -#[test] -fn execute_program_validates_interface_override_attribute_targets() { - let valid = parse_fragment( - br#"interface EvalIfaceOverrideParent { - public function label(): string; -} -interface EvalIfaceOverrideChild extends EvalIfaceOverrideParent { - #[\Override] - public function label(): string; -} -class EvalIfaceOverrideImpl implements EvalIfaceOverrideChild { - public function label(): string { return "child"; } -} -$box = new EvalIfaceOverrideImpl(); -echo $box->label();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - execute_program(&valid, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "child"); - - let builtin_parent = parse_fragment( - br#"interface EvalIfaceOverrideStringable extends Stringable { - #[\Override] - public function __toString(): string; -} -class EvalIfaceOverrideStringableImpl implements EvalIfaceOverrideStringable { - public function __toString(): string { return "stringable"; } -} -$box = new EvalIfaceOverrideStringableImpl(); -echo $box;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - execute_program(&builtin_parent, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "stringable"); - - let invalid = parse_fragment( - br#"interface EvalIfaceOverrideMissing { - #[\Override] - public function missing(): string; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&invalid, &mut scope, &mut values) - .expect_err("interface override marker without parent method should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval rejects global builtin attributes on unsupported OOP targets. -#[test] -fn execute_program_rejects_invalid_builtin_attribute_targets() { - let cases: &[(&[u8], &str)] = &[ - ( - br#"#[\AllowDynamicProperties] interface EvalInvalidAttrInterface {}"#, - "AllowDynamicProperties interface", - ), - ( - br#"#[\AllowDynamicProperties] trait EvalInvalidAttrTrait {}"#, - "AllowDynamicProperties trait", - ), - ( - br#"#[\AllowDynamicProperties] enum EvalInvalidAttrEnum { case Ready; }"#, - "AllowDynamicProperties enum", - ), - ( - br#"#[\Override] class EvalInvalidAttrClass {}"#, - "Override class", - ), - ( - br#"class EvalInvalidAttrProperty { #[\Override] public int $value; }"#, - "Override property", - ), - ( - br#"class EvalInvalidAttrConstant { #[\AllowDynamicProperties] public const VALUE = 1; }"#, - "AllowDynamicProperties constant", - ), - ( - br#"class EvalInvalidAttrMethod { #[\AllowDynamicProperties] public function run() {} }"#, - "AllowDynamicProperties method", - ), - ( - br#"enum EvalInvalidAttrCase { #[\AllowDynamicProperties] case Ready; }"#, - "AllowDynamicProperties enum case", - ), - ]; - - for &(source, label) in cases { - let program = parse_fragment(source).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values).expect_err(label); - - assert_eq!(err, EvalStatus::RuntimeFatal); - } -} - -/// Verifies readonly classes leave static properties mutable like ordinary classes. -#[test] -fn execute_program_allows_readonly_class_static_property() { - let program = parse_fragment( - br#"readonly class EvalReadonlyStaticBox { - public static int $count = 1; -} -EvalReadonlyStaticBox::$count = EvalReadonlyStaticBox::$count + 1; -echo EvalReadonlyStaticBox::$count;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2"); - assert_eq!(values.get(result), FakeValue::Null); -} - -/// Verifies readonly classes may extend readonly parents and use inherited constructors. -#[test] -fn execute_program_allows_readonly_class_extending_readonly_parent() { - let program = parse_fragment( - br#"readonly class EvalReadonlyParentBase { - public int $id; - public function __construct($id) { $this->id = $id; } - public function id() { return $this->id; } -} -readonly class EvalReadonlyParentChild extends EvalReadonlyParentBase {} -$box = new EvalReadonlyParentChild(13); -echo $box->id(); echo ":"; -return $box->id();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "13:"); - assert_eq!(values.get(result), FakeValue::Int(13)); -} - -/// Verifies eval-declared asymmetric properties allow owner and subclass writes as PHP does. -#[test] -fn execute_program_allows_asymmetric_property_writes_from_allowed_scopes() { - let program = parse_fragment( - br#"class EvalAsymWriteBase { - public private(set) int $privateValue = 1; - public protected(set) string $protectedName = "base"; - public function ownerWrite($value, $name) { - $this->privateValue = $value; - $this->protectedName = $name; - } -} -class EvalAsymWriteChild extends EvalAsymWriteBase { - public function childWrite($name) { - $this->protectedName = $name; - } -} -$box = new EvalAsymWriteChild(); -echo $box->privateValue; echo ":"; echo $box->protectedName; echo ":"; -$box->ownerWrite(7, "owner"); -echo $box->privateValue; echo ":"; echo $box->protectedName; echo ":"; -$box->childWrite("child"); -echo $box->protectedName; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:base:7:owner:child"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval-declared `private(set)` throws Error without dispatching `__set`. -#[test] -fn execute_program_private_set_property_write_outside_declaring_class_throws_error() { - let program = parse_fragment( - br#"class EvalAsymPrivateSetBox { - public private(set) int $value = 1; - public function __set($name, $value) { - echo "bad"; - } -} -$box = new EvalAsymPrivateSetBox(); -try { - $box->value = 2; - echo "bad"; -} catch (Error $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Cannot modify private(set) property EvalAsymPrivateSetBox::$value from global scope" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval-declared `protected(set)` throws Error for global writes. -#[test] -fn execute_program_protected_set_property_write_outside_hierarchy_throws_error() { - let program = parse_fragment( - br#"class EvalAsymProtectedSetBox { - public protected(set) int $value = 1; -} -$box = new EvalAsymProtectedSetBox(); -try { - $box->value = 2; - echo "bad"; -} catch (Error $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Cannot modify protected(set) property EvalAsymProtectedSetBox::$value from global scope" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies asymmetric write restrictions cannot satisfy a public interface set contract. -#[test] -fn execute_program_rejects_private_set_property_for_interface_set_contract() { - let program = parse_fragment( - br#"interface EvalAsymSetContract { - public int $value { get; set; } -} -class EvalAsymSetContractBox implements EvalAsymSetContract { - public private(set) int $value = 1; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("private(set) property should fail public interface set contract"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies asymmetric write restrictions cannot satisfy a public abstract set contract. -#[test] -fn execute_program_rejects_private_set_property_for_abstract_set_contract() { - let program = parse_fragment( - br#"abstract class EvalAsymAbstractSetBase { - abstract public int $value { get; set; } -} -class EvalAsymAbstractSetBox extends EvalAsymAbstractSetBase { - public private(set) int $value = 1; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("private(set) property should fail public abstract set contract"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval interface protected(set) property contracts accept compatible implementations. -#[test] -fn execute_program_allows_interface_protected_set_property_contract() { - let program = parse_fragment( - br#"interface EvalAsymProtectedSetContract { - public protected(set) string $name { get; set; } -} -class EvalAsymProtectedSetBase implements EvalAsymProtectedSetContract { - public protected(set) string $name = "base"; -} -class EvalAsymProtectedSetChild extends EvalAsymProtectedSetBase { - public function rename($name) { $this->name = $name; } -} -$box = new EvalAsymProtectedSetChild(); -echo $box->name; echo ":"; -$box->rename("child"); -echo $box->name; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "base:child"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies private(set) interface contracts are final and cannot be implemented by a class. -#[test] -fn execute_program_rejects_private_set_interface_property_contract_implementation() { - let program = parse_fragment( - br#"interface EvalAsymPrivateSetInterfaceContract { - public private(set) int $value { get; set; } -} -class EvalAsymPrivateSetInterfaceBox implements EvalAsymPrivateSetInterfaceContract { - public private(set) int $value = 1; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("private(set) interface contract should be final"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies private(set) abstract properties behave as final contracts. -#[test] -fn execute_program_rejects_private_set_abstract_property_redeclaration() { - let program = parse_fragment( - br#"abstract class EvalAsymPrivateSetAbstractBase { - abstract public private(set) int $value { get; set; } -} -class EvalAsymPrivateSetAbstractBox extends EvalAsymPrivateSetAbstractBase { - public private(set) int $value = 1; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("private(set) abstract property should be final"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval property redeclarations may widen visibility while preserving invariant types. -#[test] -fn execute_program_accepts_compatible_property_redeclarations() { - let program = parse_fragment( - br#"class EvalPropertyRedeclareBase { - protected int|string $value; -} -class EvalPropertyRedeclareChild extends EvalPropertyRedeclareBase { - public string|int $value; -} -class EvalPropertyRelativeBase { - public self $selfValue; - public EvalPropertyRelativeBase $parentValue; -} -class EvalPropertyRelativeChild extends EvalPropertyRelativeBase { - public self $selfValue; - public parent $parentValue; -} -class EvalPropertyReadonlyAddBase { - public int $count = 0; -} -class EvalPropertyReadonlyAddChild extends EvalPropertyReadonlyAddBase { - public readonly int $count; - public function __construct() { $this->count = 7; } -} -class EvalPropertyReadonlyWidenBase { - protected int $count = 0; - public function count() { return $this->count; } -} -class EvalPropertyReadonlyWidenChild extends EvalPropertyReadonlyWidenBase { - public readonly int $count; - public function __construct() { $this->count = 9; } -} -$box = new EvalPropertyRedeclareChild(); -$box->value = "ok"; -$readonly = new EvalPropertyReadonlyAddChild(); -$widened = new EvalPropertyReadonlyWidenChild(); -return $box->value . ":" . $readonly->count . ":" . $widened->count . ":" . $widened->count();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("ok:7:9:9".to_string())); -} - -/// Verifies eval rejects inherited property redeclarations that violate PHP invariance. -#[test] -fn execute_program_rejects_incompatible_property_redeclarations() { - let incompatible_type = parse_fragment( - br#"class EvalPropertyTypeBase { - public int $value; -} -class EvalPropertyStringChild extends EvalPropertyTypeBase { - public string $value; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&incompatible_type, &mut scope, &mut values) - .expect_err("incompatible inherited property type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let reduced_visibility = parse_fragment( - br#"class EvalPropertyPublicBase { - public int $value; -} -class EvalPropertyProtectedChild extends EvalPropertyPublicBase { - protected int $value; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&reduced_visibility, &mut scope, &mut values) - .expect_err("reduced inherited property visibility should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let typed_from_untyped = parse_fragment( - br#"class EvalPropertyUntypedBase { - public $value; -} -class EvalPropertyTypedChild extends EvalPropertyUntypedBase { - public int $value; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&typed_from_untyped, &mut scope, &mut values) - .expect_err("typed inherited property redeclaration should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let static_mismatch = parse_fragment( - br#"class EvalPropertyStaticBase { - public static int $value; -} -class EvalPropertyInstanceChild extends EvalPropertyStaticBase { - public int $value; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&static_mismatch, &mut scope, &mut values) - .expect_err("static inherited property redeclaration should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let readonly_mismatch = parse_fragment( - br#"class EvalPropertyReadonlyBase { - public readonly int $value; -} -class EvalPropertyMutableChild extends EvalPropertyReadonlyBase { - public int $value; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&readonly_mismatch, &mut scope, &mut values) - .expect_err("readonly inherited property redeclaration should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let reduced_write_visibility = parse_fragment( - br#"class EvalPropertyProtectedSetBase { - public protected(set) int $value; -} -class EvalPropertyPrivateSetChild extends EvalPropertyProtectedSetBase { - public private(set) int $value; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&reduced_write_visibility, &mut scope, &mut values) - .expect_err("reduced inherited property write visibility should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies readonly class inheritance requires matching readonly status. -#[test] -fn execute_program_rejects_readonly_class_extending_non_readonly_parent() { - let program = parse_fragment( - br#"class EvalReadonlyParentMismatchBase {} -readonly class EvalReadonlyParentMismatchChild extends EvalReadonlyParentMismatchBase {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("readonly class cannot extend non-readonly parent"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies anonymous eval classes instantiate, reuse their synthetic class, and reflect as anonymous. -#[test] -fn execute_program_instantiates_anonymous_class_expressions() { - let program = parse_fragment( - br#"interface EvalAnonRuntimeLabel { - function label(); -} -class EvalAnonRuntimeBase { - protected string $prefix; - public function __construct($prefix) { $this->prefix = $prefix; } -} -function eval_anon_make($prefix) { - return new class($prefix) extends EvalAnonRuntimeBase implements EvalAnonRuntimeLabel { - public function label() { return $this->prefix . ":anon"; } - }; -} -$first = eval_anon_make("A"); -$second = eval_anon_make("B"); -echo $first->label(); echo ":"; -echo $second->label(); echo ":"; -echo get_class($first) === get_class($second) ? "same" : "different"; echo ":"; -$ref = new ReflectionClass(get_class($first)); -echo $ref->isAnonymous() ? "anonymous" : "named"; echo ":"; -echo $ref->implementsInterface("EvalAnonRuntimeLabel") ? "iface" : "bad"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A:anon:B:anon:same:anonymous:iface"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies readonly anonymous eval classes initialize and reject property writes. -#[test] -fn execute_program_instantiates_readonly_anonymous_class_expressions() { - let program = parse_fragment( - br#"$box = new readonly class("frozen") { - public function __construct(public string $label) {} -}; -echo $box->label; echo ":"; -try { - $box->label = "bad"; - echo "bad"; -} catch (Error $e) { - echo get_class($e); -} -return $box->label;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "frozen:Error"); - assert_eq!(values.get(result), FakeValue::String("frozen".to_string())); -} - -/// Verifies eval object cloning copies properties before running `__clone()`. -#[test] -fn execute_program_clones_eval_object_and_runs_clone_hook() { - let program = parse_fragment( - br#"class EvalCloneRuntimeBox { - public string $name; - public function __construct($name) { $this->name = $name; } - public function __clone() { $this->name = $this->name . ":clone"; } -} -$first = new EvalCloneRuntimeBox("A"); -$second = clone $first; -echo $first->name; echo ":"; -echo $second->name; -$second->name = "B"; -return $first->name . ":" . $second->name;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A:A:clone"); - assert_eq!(values.get(result), FakeValue::String("A:B".to_string())); -} - -/// Verifies private `__clone()` can be invoked from inside the declaring eval class. -#[test] -fn execute_program_allows_private_clone_hook_inside_declaring_class() { - let program = parse_fragment( - br#"class EvalCloneRuntimePrivateBox { - public string $name = "A"; - private function __clone() { $this->name = $this->name . ":copy"; } - public function copy() { return clone $this; } -} -$first = new EvalCloneRuntimePrivateBox(); -$second = $first->copy(); -echo $first->name; echo ":"; -echo $second->name; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A:A:copy"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval-declared `__destruct()` runs for explicit unset and discarded temporaries. -#[test] -fn execute_program_runs_eval_destructor_on_final_release() { - let program = parse_fragment( - br#"class EvalDestructRuntimeBox { - public string $name; - public function __construct($name) { $this->name = $name; } - public function __destruct() { echo "drop:" . $this->name . ":"; } -} -$box = new EvalDestructRuntimeBox("A"); -unset($box); -new EvalDestructRuntimeBox("B"); -echo "after"; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "drop:A:drop:B:after"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies private `__clone()` throws Error through a global clone expression. -#[test] -fn execute_program_private_clone_hook_outside_declaring_class_throws_error() { - let program = parse_fragment( - br#"class EvalCloneRuntimePrivateFail { - private function __clone() {} -} -$box = new EvalCloneRuntimePrivateFail(); -try { - clone $box; - echo "bad"; -} catch (Error $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Call to private EvalCloneRuntimePrivateFail::__clone() from global scope" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies a get-only property hook computes a virtual eval property. -#[test] -fn execute_program_reads_eval_property_get_hook() { - let program = parse_fragment( - br#"class EvalHookPerson { - public string $first = "Ada"; - public string $last = "Lovelace"; - public string $full { - get => $this->first . " " . $this->last; - } -} -$person = new EvalHookPerson(); -echo $person->full; -return $person->full;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada Lovelace"); - assert_eq!( - values.get(result), - FakeValue::String("Ada Lovelace".to_string()) - ); -} - -/// Verifies by-reference get hook syntax routes through the concrete eval get accessor. -#[test] -fn execute_program_reads_eval_by_ref_get_property_hook() { - let program = parse_fragment( - br#"class EvalByRefGetHookPerson { - public string $first = "Ada"; - public string $last = "Lovelace"; - public string $full { - &get => $this->first . " " . $this->last; - } -} -$person = new EvalByRefGetHookPerson(); -echo $person->full; -return $person->full;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada Lovelace"); - assert_eq!( - values.get(result), - FakeValue::String("Ada Lovelace".to_string()) - ); -} - -/// Verifies get/set property hooks can use the raw backing slot from inside accessors. -#[test] -fn execute_program_routes_eval_property_get_and_set_hooks() { - let program = parse_fragment( - br#"class EvalHookName { - public string $value { - get => $this->value; - set { $this->value = $value . "!"; } - } -} -$name = new EvalHookName(); -$name->value = "Ada"; -echo $name->value; -return $name->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada!"); - assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); -} - -/// Verifies short set hooks assign their expression result into the raw backing slot. -#[test] -fn execute_program_routes_eval_short_set_property_hooks() { - let program = parse_fragment( - br#"class EvalShortSetHookName { - public string $value { - get => $this->value; - set => trim($value); - } -} -class EvalShortSetHookLabel { - public string $text { - get => $this->text; - set(string $raw) => strtoupper($raw); - } -} -$name = new EvalShortSetHookName(); -$name->value = " Ada "; -echo "[" . $name->value . "]:"; -$label = new EvalShortSetHookLabel(); -$label->text = "hi"; -echo $label->text; -return $label->text;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "[Ada]:HI"); - assert_eq!(values.get(result), FakeValue::String("HI".to_string())); -} - -/// Verifies explicit set-hook parameter types are contravariant with the property type. -#[test] -fn execute_program_validates_eval_property_set_hook_parameter_types() { - let valid_program = parse_fragment( - br#"class EvalWideSetHookParam { - public string $value { - get => $this->value; - set(mixed $raw) => $raw; - } -} -$box = new EvalWideSetHookParam(); -$box->value = "Ada"; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&valid_program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); - - for source in [ - br#"class EvalNarrowSetHookParam { - public mixed $value { - set(string $raw) => $raw; - } -}"# - .as_slice(), - br#"class EvalNullableSetHookParam { - public ?string $value { - set(string $raw) => $raw; - } -}"# - .as_slice(), - ] { - let program = parse_fragment(source).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("incompatible set-hook parameter type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - } -} - -/// Verifies nullsafe reads and mixed-case names still route through eval property hooks. -#[test] -fn execute_program_routes_eval_nullsafe_and_mixed_case_property_hooks() { - let program = parse_fragment( - br#"class EvalNullsafeHookPerson { - public string $first = "Ada"; - public string $last = "Lovelace"; - public string $full { - get => $this->first . " " . $this->last; - } -} -class EvalMixedCaseHookBox { - private int $store = 0; - public int $Total { - get { return $this->store; } - } - public function set(int $value) { $this->store = $value; } -} -function eval_hook_describe($person) { - return $person?->full ?? "(none)"; -} -$person = new EvalNullsafeHookPerson(); -$box = new EvalMixedCaseHookBox(); -$box->set(5); -echo eval_hook_describe($person) . "|" . eval_hook_describe(null) . "|" . $box->Total; -return $box->Total;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada Lovelace|(none)|5"); - assert_eq!(values.get(result), FakeValue::Int(5)); -} - -/// Verifies undefined eval property reads and writes dispatch through `__get` and `__set`. -#[test] -fn execute_program_dispatches_eval_magic_get_and_set() { - let program = parse_fragment( - br#"class EvalMagicPropertyBox { - public string $events = ""; - public function __get($name) { - $this->events = $this->events . "get:" . $name . ";"; - return "value:" . $name; - } - public function __set($name, $value) { - $this->events = $this->events . "set:" . $name . "=" . $value . ";"; - } -} -$box = new EvalMagicPropertyBox(); -echo $box->missing; echo ":"; -$box->other = "B"; -$box->events = $box->events . "public;"; -return $box->events;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "value:missing:"); - assert_eq!( - values.get(result), - FakeValue::String("get:missing;set:other=B;public;".to_string()) - ); -} - -/// Verifies eval invokes non-public magic methods that PHP accepts with warnings. -#[test] -fn execute_program_dispatches_non_public_eval_magic_methods() { - let program = parse_fragment( - br#"class EvalNonPublicMagicBox { - public string $events = ""; - protected function __get(string $name) { - $this->events = $this->events . "get:" . $name . ";"; - return "value:" . $name; - } - protected function __set(string $name, $value): void { - $this->events = $this->events . "set:" . $name . "=" . $value . ";"; - } - private function __isset(string $name): bool { - $this->events = $this->events . "isset:" . $name . ";"; - return true; - } - private function __unset(string $name): void { - $this->events = $this->events . "unset:" . $name . ";"; - } - private function __call(string $name, array $args) { - return $name . ":" . $args[0] . ":" . $args["name"]; - } - private static function __callStatic(string $name, array $args) { - return $name . ":" . $args[0] . ":" . $args["name"]; - } - private function __invoke(string $left = "I", string $right = "J") { - return "invoke:" . $left . $right; - } -} -$box = new EvalNonPublicMagicBox(); -echo is_callable($box) ? "callable:" : "bad:"; -echo $box->missing; echo ":"; -$box->other = "B"; -echo isset($box->probe) ? "isset:" : "bad:"; -unset($box->gone); -echo $box->run("A", name: "B"); echo ":"; -echo EvalNonPublicMagicBox::staticRun("C", name: "D"); echo ":"; -echo $box(right: "F", left: "E"); -return $box->events;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "callable:value:missing:isset:run:A:B:staticRun:C:D:invoke:EF" - ); - assert_eq!( - values.get(result), - FakeValue::String("get:missing;set:other=B;isset:probe;unset:gone;".to_string()) - ); -} - -/// Verifies inaccessible eval properties dispatch through magic property methods. -#[test] -fn execute_program_dispatches_inaccessible_eval_properties_to_magic_methods() { - let program = parse_fragment( - br#"class EvalMagicPrivatePropertyBox { - private string $secret = "raw"; - public string $events = ""; - public function readOwn() { return $this->secret; } - public function __get($name) { - $this->events = $this->events . "get:" . $name . ";"; - return "read:" . $name; - } - public function __set($name, $value) { - $this->events = $this->events . "set:" . $name . "=" . $value . ";"; - } -} -$box = new EvalMagicPrivatePropertyBox(); -echo $box->readOwn(); echo ":"; -echo $box->secret; echo ":"; -$box->secret = "new"; -return $box->events;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "raw:read:secret:"); - assert_eq!( - values.get(result), - FakeValue::String("get:secret;set:secret=new;".to_string()) - ); -} - -/// Verifies dynamic properties created without `__set` are read directly even when `__get` exists. -#[test] -fn execute_program_reads_existing_dynamic_property_before_magic_get() { - let program = parse_fragment( - br#"class EvalMagicExistingDynamicBox { - public function __get($name) { - return "magic:" . $name; - } -} -$box = new EvalMagicExistingDynamicBox(); -$box->known = "plain"; -echo $box->known; echo ":"; -return $box->missing;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "plain:"); - assert_eq!( - values.get(result), - FakeValue::String("magic:missing".to_string()) - ); -} - -/// Verifies eval property probes and unsets dispatch through `__isset` and `__unset`. -#[test] -fn execute_program_dispatches_eval_magic_isset_empty_and_unset() { - let program = parse_fragment( - br#"class EvalMagicPropertyProbeBox { - public string $events = ""; - public string $present = "ready"; - public $nullish = null; - private string $secret = "raw"; - public function __isset($name) { - $this->events = $this->events . "isset:" . $name . ";"; - return $name !== "no"; - } - public function __get($name) { - $this->events = $this->events . "get:" . $name . ";"; - return $name === "empty" ? "" : "value:" . $name; - } - public function __unset($name) { - $this->events = $this->events . "unset:" . $name . ";"; - } -} -$box = new EvalMagicPropertyProbeBox(); -echo isset($box->present) ? "P" : "p"; echo ":"; -echo isset($box->nullish) ? "N" : "n"; echo ":"; -echo isset($box->secret) ? "S" : "s"; echo ":"; -echo isset($box->no) ? "bad" : "no"; echo ":"; -echo empty($box->secret) ? "bad" : "filled"; echo ":"; -echo empty($box->empty) ? "empty" : "bad"; echo ":"; -unset($box->present); -unset($box->secret); -unset($box->missing); -echo isset($box->present) ? "bad" : "unset"; echo ":"; -return $box->events;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "P:n:S:no:filled:empty:unset:"); - assert_eq!( - values.get(result), - FakeValue::String( - "isset:secret;isset:no;isset:secret;get:secret;isset:empty;get:empty;unset:secret;unset:missing;" - .to_string() - ) - ); -} - -/// Verifies eval objects stringify through public `__toString()` in PHP string contexts. -#[test] -fn execute_program_dispatches_eval_magic_tostring_for_string_contexts() { - let program = parse_fragment( - br#"class EvalStringableBox { - public string $name = "Ada"; - public function __toString() { - return "box:" . $this->name; - } - public function accepts(string $value) { - return "typed:" . $value; - } -} -$box = new EvalStringableBox(); -echo $box; echo ":"; -print $box; echo ":"; -echo "pre" . $box; echo ":"; -echo strval($box); echo ":"; -echo call_user_func("strval", $box); echo ":"; -echo call_user_func_array("strval", [$box]); echo ":"; -echo $box instanceof Stringable ? "S" : "s"; echo ":"; -return $box->accepts($box);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "box:Ada:box:Ada:prebox:Ada:box:Ada:box:Ada:box:Ada:S:" - ); - assert_eq!( - values.get(result), - FakeValue::String("typed:box:Ada".to_string()) - ); -} - -/// Verifies eval objects without `__toString()` fail in PHP string contexts. -#[test] -fn execute_program_rejects_eval_object_string_context_without_tostring() { - let program = parse_fragment( - br#"class EvalPlainStringContext {} -$box = new EvalPlainStringContext(); -try { - echo $box; -} catch (Error $e) { - echo get_class($e) . ":" . $e->getMessage(); -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Object of class EvalPlainStringContext could not be converted to string" - ); -} - -/// Verifies eval rejects magic methods whose staticness, arity, or fatal contracts are invalid. -#[test] -fn execute_program_rejects_invalid_eval_magic_method_contracts() { - let cases: Vec<(&[u8], &str)> = vec![ - ( - br#"class EvalBadToString { private function __toString() { return "x"; } }"#.as_slice(), - "private __toString", - ), - ( - br#"class EvalBadToStringReturn { public function __toString(): int { return 1; } }"#.as_slice(), - "bad __toString return type", - ), - ( - br#"class EvalBadGetByRef { public function __get(&$name) { return "x"; } }"#.as_slice(), - "by-ref __get", - ), - ( - br#"class EvalBadGetParamType { public function __get(int $name) { return "x"; } }"#.as_slice(), - "bad __get parameter type", - ), - ( - br#"class EvalBadIssetReturn { public function __isset($name): string { return "yes"; } }"#.as_slice(), - "bad __isset return type", - ), - ( - br#"class EvalBadUnsetReturn { public function __unset($name): int { return 1; } }"#.as_slice(), - "bad __unset return type", - ), - ( - br#"class EvalBadSetReturn { public function __set($name, $value): int { return 1; } }"#.as_slice(), - "bad __set return type", - ), - ( - br#"class EvalBadSetParamType { public function __set(int $name, $value): void {} }"#.as_slice(), - "bad __set parameter type", - ), - ( - br#"class EvalBadCall { public function __call($name, ...$args) { return "x"; } }"#.as_slice(), - "variadic __call", - ), - ( - br#"class EvalBadCallArgsType { public function __call(string $name, string $args) {} }"#.as_slice(), - "bad __call args type", - ), - ( - br#"class EvalBadCallNameType { public function __call(int $name, array $args) {} }"#.as_slice(), - "bad __call name type", - ), - ( - br#"class EvalBadCallStatic { public function __callStatic($name, $args) { return "x"; } }"#.as_slice(), - "instance __callStatic", - ), - ( - br#"class EvalBadCallStaticArgsType { public static function __callStatic(string $name, string $args) {} }"#.as_slice(), - "bad __callStatic args type", - ), - ( - br#"class EvalBadSleepReturn { public function __sleep(): string { return "x"; } }"#.as_slice(), - "bad __sleep return type", - ), - ( - br#"class EvalBadSerializeStatic { public static function __serialize(): array { return []; } }"#.as_slice(), - "static __serialize", - ), - ( - br#"class EvalBadWakeupArity { public function __wakeup($value): void {} }"#.as_slice(), - "bad __wakeup arity", - ), - ( - br#"class EvalBadUnserializeArity { public function __unserialize(): void {} }"#.as_slice(), - "bad __unserialize arity", - ), - ( - br#"class EvalBadUnserializeReturn { public function __unserialize(array $data): int { return 1; } }"#.as_slice(), - "bad __unserialize return type", - ), - ( - br#"class EvalBadUnserializeParam { public function __unserialize(string $data): void {} }"#.as_slice(), - "bad __unserialize parameter type", - ), - ( - br#"class EvalBadDebugInfoReturn { public function __debugInfo(): string { return "x"; } }"#.as_slice(), - "bad __debugInfo return type", - ), - ( - br#"class EvalBadDebugInfoStatic { public static function __debugInfo(): array { return []; } }"#.as_slice(), - "static __debugInfo", - ), - ( - br#"class EvalBadSetStateInstance { public function __set_state($data) {} }"#.as_slice(), - "instance __set_state", - ), - ( - br#"class EvalBadSetStateArity { public static function __set_state($data, $extra) {} }"#.as_slice(), - "bad __set_state arity", - ), - ( - br#"class EvalBadSetStateParam { public static function __set_state(string $data) {} }"#.as_slice(), - "bad __set_state parameter type", - ), - ( - br#"class EvalBadClone { public static function __clone() {} }"#.as_slice(), - "static __clone", - ), - ( - br#"class EvalBadCloneReturn { public function __clone(): int {} }"#.as_slice(), - "bad __clone return type", - ), - ( - br#"class EvalBadDestruct { public static function __destruct() {} }"#.as_slice(), - "static __destruct", - ), - ( - br#"class EvalBadConstructReturn { public function __construct(): void {} }"#.as_slice(), - "bad __construct return type", - ), - ( - br#"class EvalBadDestructReturn { public function __destruct(): void {} }"#.as_slice(), - "bad __destruct return type", - ), - ( - br#"trait EvalBadMagicTrait { public static function __isset($name) { return true; } }"#.as_slice(), - "trait static __isset", - ), - ]; - - for (source, label) in cases { - let program = parse_fragment(source).expect(label); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - execute_program(&program, &mut scope, &mut values).expect_err(label); - } -} - -/// Verifies eval accepts PHP-compatible debug and set-state magic method contracts. -#[test] -fn execute_program_accepts_debug_and_set_state_magic_contracts() { - let program = parse_fragment( - br#"class EvalGoodDebugInfoMagic { - public function __debugInfo(): ?array { return null; } -} -class EvalGoodSetStateMagic { - public static function __set_state($data) {} -} -return class_exists("EvalGoodDebugInfoMagic") && class_exists("EvalGoodSetStateMagic");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies get-only property hooks throw Error on writes outside a set accessor. -#[test] -fn execute_program_write_to_get_only_eval_property_hook_throws_error() { - let program = parse_fragment( - br#"class EvalHookReadOnly { - public int $answer { - get => 42; - } -} -$box = new EvalHookReadOnly(); -try { - $box->answer = 7; - echo "bad"; -} catch (Error $e) { - echo get_class($e); echo ":"; echo $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Property EvalHookReadOnly::$answer is read-only" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval subclasses inherit parent property hooks. -#[test] -fn execute_program_inherits_eval_property_hooks() { - let program = parse_fragment( - br#"class EvalHookBase { - public string $value { - get => $this->value; - set { $this->value = $value . "!"; } - } -} -class EvalHookChild extends EvalHookBase { - public function shout() { return $this->value . "?"; } -} -$box = new EvalHookChild(); -$box->value = "Ada"; -echo $box->value; echo ":"; -return $box->shout();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada!:"); - assert_eq!(values.get(result), FakeValue::String("Ada!?".to_string())); -} - -/// Verifies eval interface property hook contracts are enforced through inheritance. -#[test] -fn execute_program_accepts_interface_property_hook_contracts() { - let program = parse_fragment( - br#"interface EvalHookContract { - public string $value { get; set; } -} -interface EvalNamedHookContract extends EvalHookContract { - public string $name { get; } -} -class EvalHookContractBox implements EvalNamedHookContract { - public string $name = "box"; - public string $value { - get => $this->value; - set { $this->value = $value . "!"; } - } -} -$box = new EvalHookContractBox(); -$box->value = "Ada"; -echo $box->name; echo ":"; -echo $box->value; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "box:Ada!"); - assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); -} - -/// Verifies a normal public mutable property satisfies an eval interface get/set contract. -#[test] -fn execute_program_accepts_plain_property_for_interface_hook_contracts() { - let program = parse_fragment( - br#"interface EvalPlainHookContract { - public string $value { get; set; } -} -class EvalPlainHookContractBox implements EvalPlainHookContract { - public string $value = "Ada"; -} -$box = new EvalPlainHookContractBox(); -echo $box->value; echo ":"; -$box->value = "Grace"; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada:"); - assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); -} - -/// Verifies interface property hook types are checked on abstract and concrete classes. -#[test] -fn execute_program_validates_interface_property_hook_types() { - let valid_program = parse_fragment( - br#"interface EvalIfaceGetWide { - public int|string $value { get; } -} -interface EvalIfaceSetNarrow { - public int $slot { set; } -} -abstract class EvalIfacePropertyDeferred implements EvalIfaceGetWide {} -abstract class EvalIfacePropertyGood implements EvalIfaceGetWide, EvalIfaceSetNarrow { - abstract public int $value { get; } - abstract public int|string $slot { set; } -} -class EvalIfacePropertyConcrete implements EvalIfaceGetWide { - public int $value = 4; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let result = execute_program(&valid_program, &mut scope, &mut values).expect("execute eval ir"); - assert_eq!(values.get(result), FakeValue::Bool(true)); - - let bad_abstract_get = parse_fragment( - br#"interface EvalIfaceGetInt { - public int $value { get; } -} -abstract class EvalIfaceGetWideBad implements EvalIfaceGetInt { - abstract public int|string $value { get; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_abstract_get, &mut scope, &mut values) - .expect_err("wider abstract get property type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let bad_abstract_set = parse_fragment( - br#"interface EvalIfaceSetWide { - public int|string $value { set; } -} -abstract class EvalIfaceSetNarrowBad implements EvalIfaceSetWide { - abstract public int $value { set; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_abstract_set, &mut scope, &mut values) - .expect_err("narrower abstract set property type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let bad_concrete_get = parse_fragment( - br#"interface EvalIfaceConcreteGetInt { - public int $value { get; } -} -class EvalIfaceConcreteGetWideBad implements EvalIfaceConcreteGetInt { - public int|string $value = 4; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_concrete_get, &mut scope, &mut values) - .expect_err("wider concrete get property type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let bad_inherited_property = parse_fragment( - br#"interface EvalIfaceInheritedGet { - public int $value { get; } -} -abstract class EvalIfaceInheritedPropertyBase { - public string $value = "bad"; -} -abstract class EvalIfaceInheritedPropertyChild extends EvalIfaceInheritedPropertyBase implements EvalIfaceInheritedGet {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_inherited_property, &mut scope, &mut values) - .expect_err("inherited incompatible interface property should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies a get-only hook cannot satisfy a writable eval interface contract. -#[test] -fn execute_program_rejects_get_only_hook_for_interface_set_contract() { - let program = parse_fragment( - br#"interface EvalHookSetContract { - public int $answer { get; set; } -} -class EvalHookGetOnlyContractBox implements EvalHookSetContract { - public int $answer { - get => 42; - } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("get-only hook should fail writable interface contract"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies readonly properties cannot satisfy writable eval interface contracts. -#[test] -fn execute_program_rejects_readonly_property_for_interface_set_contract() { - let program = parse_fragment( - br#"interface EvalReadonlyHookContract { - public int $id { get; set; } -} -class EvalReadonlyHookContractBox implements EvalReadonlyHookContract { - public readonly int $id; - public function __construct($id) { $this->id = $id; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("readonly property should fail writable interface contract"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies concrete eval subclasses satisfy abstract property hook contracts. -#[test] -fn execute_program_accepts_abstract_property_hook_contracts() { - let program = parse_fragment( - br#"abstract class EvalAbstractHookBase { - abstract public string $value { get; set; } -} -class EvalAbstractHookBox extends EvalAbstractHookBase { - public string $value { - get => $this->value; - set { $this->value = $value . "!"; } - } -} -$box = new EvalAbstractHookBox(); -$box->value = "Ada"; -echo $box->value; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada!"); - assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); -} - -/// Verifies normal mutable properties satisfy abstract get/set hook contracts. -#[test] -fn execute_program_accepts_plain_property_for_abstract_hook_contracts() { - let program = parse_fragment( - br#"abstract class EvalPlainAbstractHookBase { - abstract public string $value { get; set; } -} -class EvalPlainAbstractHookBox extends EvalPlainAbstractHookBase { - public string $value = "Ada"; -} -$box = new EvalPlainAbstractHookBox(); -echo $box->value; echo ":"; -$box->value = "Grace"; -return $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada:"); - assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); -} - -/// Verifies concrete eval subclasses must declare inherited abstract properties. -#[test] -fn execute_program_rejects_missing_abstract_property_hook_contract() { - let program = parse_fragment( - br#"abstract class EvalMissingAbstractHookBase { - abstract public string $value { get; } -} -class EvalMissingAbstractHookBox extends EvalMissingAbstractHookBase {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("missing abstract property should fail concrete subclass"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies abstract final eval properties are rejected while parsing. -#[test] -fn parse_fragment_rejects_final_abstract_property_hook_contract() { - let err = parse_fragment( - br#"abstract class EvalFinalAbstractHookBase { - abstract final public string $value { get; } -}"#, - ) - .expect_err("final abstract property should fail"); - - assert_eq!(err, EvalParseError::UnsupportedConstruct); -} - -/// Verifies readonly properties cannot satisfy abstract writable hook contracts. -#[test] -fn execute_program_rejects_readonly_property_for_abstract_set_contract() { - let program = parse_fragment( - br#"abstract class EvalReadonlyAbstractHookBase { - abstract public int $id { get; set; } -} -class EvalReadonlyAbstractHookBox extends EvalReadonlyAbstractHookBase { - public readonly int $id; - public function __construct($id) { $this->id = $id; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("readonly property should fail abstract writable contract"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies abstract trait property hook contracts are enforced after trait expansion. -#[test] -fn execute_program_enforces_trait_abstract_property_hook_contracts() { - let program = parse_fragment( - br#"trait EvalTraitNeedsName { - abstract protected string $name { get; } - public function label() { return $this->name; } -} -class EvalTraitNameBox { - use EvalTraitNeedsName; - protected string $name = "Ada"; -} -$box = new EvalTraitNameBox(); -echo $box->label(); -return $box->label();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Ada"); - assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); -} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/asymmetric_properties.rs b/crates/elephc-magician/src/interpreter/tests/classes/asymmetric_properties.rs new file mode 100644 index 0000000000..3092ac8f23 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/asymmetric_properties.rs @@ -0,0 +1,381 @@ +//! Purpose: +//! Interpreter tests for asymmetric property visibility and interface, +//! abstract-class, inheritance, and readonly compatibility rules. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Read and write visibility are exercised from owner, hierarchy, and external +//! scopes. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared asymmetric properties allow owner and subclass writes as PHP does. +#[test] +fn execute_program_allows_asymmetric_property_writes_from_allowed_scopes() { + let program = parse_fragment( + br#"class EvalAsymWriteBase { + public private(set) int $privateValue = 1; + public protected(set) string $protectedName = "base"; + public function ownerWrite($value, $name) { + $this->privateValue = $value; + $this->protectedName = $name; + } +} +class EvalAsymWriteChild extends EvalAsymWriteBase { + public function childWrite($name) { + $this->protectedName = $name; + } +} +$box = new EvalAsymWriteChild(); +echo $box->privateValue; echo ":"; echo $box->protectedName; echo ":"; +$box->ownerWrite(7, "owner"); +echo $box->privateValue; echo ":"; echo $box->protectedName; echo ":"; +$box->childWrite("child"); +echo $box->protectedName; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:base:7:owner:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `private(set)` throws Error without dispatching `__set`. +#[test] +fn execute_program_private_set_property_write_outside_declaring_class_throws_error() { + let program = parse_fragment( + br#"class EvalAsymPrivateSetBox { + public private(set) int $value = 1; + public function __set($name, $value) { + echo "bad"; + } +} +$box = new EvalAsymPrivateSetBox(); +try { + $box->value = 2; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot modify private(set) property EvalAsymPrivateSetBox::$value from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `protected(set)` throws Error for global writes. +#[test] +fn execute_program_protected_set_property_write_outside_hierarchy_throws_error() { + let program = parse_fragment( + br#"class EvalAsymProtectedSetBox { + public protected(set) int $value = 1; +} +$box = new EvalAsymProtectedSetBox(); +try { + $box->value = 2; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot modify protected(set) property EvalAsymProtectedSetBox::$value from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies asymmetric write restrictions cannot satisfy a public interface set contract. +#[test] +fn execute_program_rejects_private_set_property_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalAsymSetContract { + public int $value { get; set; } +} +class EvalAsymSetContractBox implements EvalAsymSetContract { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) property should fail public interface set contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies asymmetric write restrictions cannot satisfy a public abstract set contract. +#[test] +fn execute_program_rejects_private_set_property_for_abstract_set_contract() { + let program = parse_fragment( + br#"abstract class EvalAsymAbstractSetBase { + abstract public int $value { get; set; } +} +class EvalAsymAbstractSetBox extends EvalAsymAbstractSetBase { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) property should fail public abstract set contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval interface protected(set) property contracts accept compatible implementations. +#[test] +fn execute_program_allows_interface_protected_set_property_contract() { + let program = parse_fragment( + br#"interface EvalAsymProtectedSetContract { + public protected(set) string $name { get; set; } +} +class EvalAsymProtectedSetBase implements EvalAsymProtectedSetContract { + public protected(set) string $name = "base"; +} +class EvalAsymProtectedSetChild extends EvalAsymProtectedSetBase { + public function rename($name) { $this->name = $name; } +} +$box = new EvalAsymProtectedSetChild(); +echo $box->name; echo ":"; +$box->rename("child"); +echo $box->name; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "base:child"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies private(set) interface contracts are final and cannot be implemented by a class. +#[test] +fn execute_program_rejects_private_set_interface_property_contract_implementation() { + let program = parse_fragment( + br#"interface EvalAsymPrivateSetInterfaceContract { + public private(set) int $value { get; set; } +} +class EvalAsymPrivateSetInterfaceBox implements EvalAsymPrivateSetInterfaceContract { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) interface contract should be final"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies private(set) abstract properties behave as final contracts. +#[test] +fn execute_program_rejects_private_set_abstract_property_redeclaration() { + let program = parse_fragment( + br#"abstract class EvalAsymPrivateSetAbstractBase { + abstract public private(set) int $value { get; set; } +} +class EvalAsymPrivateSetAbstractBox extends EvalAsymPrivateSetAbstractBase { + public private(set) int $value = 1; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("private(set) abstract property should be final"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval property redeclarations may widen visibility while preserving invariant types. +#[test] +fn execute_program_accepts_compatible_property_redeclarations() { + let program = parse_fragment( + br#"class EvalPropertyRedeclareBase { + protected int|string $value; +} +class EvalPropertyRedeclareChild extends EvalPropertyRedeclareBase { + public string|int $value; +} +class EvalPropertyRelativeBase { + public self $selfValue; + public EvalPropertyRelativeBase $parentValue; +} +class EvalPropertyRelativeChild extends EvalPropertyRelativeBase { + public self $selfValue; + public parent $parentValue; +} +class EvalPropertyReadonlyAddBase { + public int $count = 0; +} +class EvalPropertyReadonlyAddChild extends EvalPropertyReadonlyAddBase { + public readonly int $count; + public function __construct() { $this->count = 7; } +} +class EvalPropertyReadonlyWidenBase { + protected int $count = 0; + public function count() { return $this->count; } +} +class EvalPropertyReadonlyWidenChild extends EvalPropertyReadonlyWidenBase { + public readonly int $count; + public function __construct() { $this->count = 9; } +} +$box = new EvalPropertyRedeclareChild(); +$box->value = "ok"; +$readonly = new EvalPropertyReadonlyAddChild(); +$widened = new EvalPropertyReadonlyWidenChild(); +return $box->value . ":" . $readonly->count . ":" . $widened->count . ":" . $widened->count();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok:7:9:9".to_string())); +} + +/// Verifies eval rejects inherited property redeclarations that violate PHP invariance. +#[test] +fn execute_program_rejects_incompatible_property_redeclarations() { + let incompatible_type = parse_fragment( + br#"class EvalPropertyTypeBase { + public int $value; +} +class EvalPropertyStringChild extends EvalPropertyTypeBase { + public string $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible inherited property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_visibility = parse_fragment( + br#"class EvalPropertyPublicBase { + public int $value; +} +class EvalPropertyProtectedChild extends EvalPropertyPublicBase { + protected int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_visibility, &mut scope, &mut values) + .expect_err("reduced inherited property visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let typed_from_untyped = parse_fragment( + br#"class EvalPropertyUntypedBase { + public $value; +} +class EvalPropertyTypedChild extends EvalPropertyUntypedBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&typed_from_untyped, &mut scope, &mut values) + .expect_err("typed inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let static_mismatch = parse_fragment( + br#"class EvalPropertyStaticBase { + public static int $value; +} +class EvalPropertyInstanceChild extends EvalPropertyStaticBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&static_mismatch, &mut scope, &mut values) + .expect_err("static inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let readonly_mismatch = parse_fragment( + br#"class EvalPropertyReadonlyBase { + public readonly int $value; +} +class EvalPropertyMutableChild extends EvalPropertyReadonlyBase { + public int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&readonly_mismatch, &mut scope, &mut values) + .expect_err("readonly inherited property redeclaration should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let reduced_write_visibility = parse_fragment( + br#"class EvalPropertyProtectedSetBase { + public protected(set) int $value; +} +class EvalPropertyPrivateSetChild extends EvalPropertyProtectedSetBase { + public private(set) int $value; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&reduced_write_visibility, &mut scope, &mut values) + .expect_err("reduced inherited property write visibility should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly class inheritance requires matching readonly status. +#[test] +fn execute_program_rejects_readonly_class_extending_non_readonly_parent() { + let program = parse_fragment( + br#"class EvalReadonlyParentMismatchBase {} +readonly class EvalReadonlyParentMismatchChild extends EvalReadonlyParentMismatchBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly class cannot extend non-readonly parent"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/attributes.rs b/crates/elephc-magician/src/interpreter/tests/classes/attributes.rs new file mode 100644 index 0000000000..4b99223b84 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/attributes.rs @@ -0,0 +1,205 @@ +//! Purpose: +//! Interpreter tests for class-related builtin attributes and readonly +//! inheritance constraints. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Attribute targets and `Override` declarations are validated at eval time. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval validates PHP's global `#[Override]` method marker. +#[test] +fn execute_program_validates_override_attribute_targets() { + let valid = parse_fragment( + br#"interface EvalOverrideContract { + public function label(): string; +} +class EvalOverrideBase { + public function name(): string { return "base"; } +} +class EvalOverrideChild extends EvalOverrideBase implements EvalOverrideContract { + #[\Override] + public function name(): string { return "child"; } + #[Override] + public function label(): string { return "contract"; } +} +$box = new EvalOverrideChild(); +echo $box->name() . ":" . $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&valid, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child:contract"); + + let invalid = parse_fragment( + br#"class EvalOverrideMissing { + #[\Override] + public function missing(): string { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&invalid, &mut scope, &mut values) + .expect_err("override marker without target should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies interface `#[Override]` methods require an inherited interface method. +#[test] +fn execute_program_validates_interface_override_attribute_targets() { + let valid = parse_fragment( + br#"interface EvalIfaceOverrideParent { + public function label(): string; +} +interface EvalIfaceOverrideChild extends EvalIfaceOverrideParent { + #[\Override] + public function label(): string; +} +class EvalIfaceOverrideImpl implements EvalIfaceOverrideChild { + public function label(): string { return "child"; } +} +$box = new EvalIfaceOverrideImpl(); +echo $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&valid, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child"); + + let builtin_parent = parse_fragment( + br#"interface EvalIfaceOverrideStringable extends Stringable { + #[\Override] + public function __toString(): string; +} +class EvalIfaceOverrideStringableImpl implements EvalIfaceOverrideStringable { + public function __toString(): string { return "stringable"; } +} +$box = new EvalIfaceOverrideStringableImpl(); +echo $box;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&builtin_parent, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "stringable"); + + let invalid = parse_fragment( + br#"interface EvalIfaceOverrideMissing { + #[\Override] + public function missing(): string; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&invalid, &mut scope, &mut values) + .expect_err("interface override marker without parent method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval rejects global builtin attributes on unsupported OOP targets. +#[test] +fn execute_program_rejects_invalid_builtin_attribute_targets() { + let cases: &[(&[u8], &str)] = &[ + ( + br#"#[\AllowDynamicProperties] interface EvalInvalidAttrInterface {}"#, + "AllowDynamicProperties interface", + ), + ( + br#"#[\AllowDynamicProperties] trait EvalInvalidAttrTrait {}"#, + "AllowDynamicProperties trait", + ), + ( + br#"#[\AllowDynamicProperties] enum EvalInvalidAttrEnum { case Ready; }"#, + "AllowDynamicProperties enum", + ), + ( + br#"#[\Override] class EvalInvalidAttrClass {}"#, + "Override class", + ), + ( + br#"class EvalInvalidAttrProperty { #[\Override] public int $value; }"#, + "Override property", + ), + ( + br#"class EvalInvalidAttrConstant { #[\AllowDynamicProperties] public const VALUE = 1; }"#, + "AllowDynamicProperties constant", + ), + ( + br#"class EvalInvalidAttrMethod { #[\AllowDynamicProperties] public function run() {} }"#, + "AllowDynamicProperties method", + ), + ( + br#"enum EvalInvalidAttrCase { #[\AllowDynamicProperties] case Ready; }"#, + "AllowDynamicProperties enum case", + ), + ]; + + for &(source, label) in cases { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies readonly classes leave static properties mutable like ordinary classes. +#[test] +fn execute_program_allows_readonly_class_static_property() { + let program = parse_fragment( + br#"readonly class EvalReadonlyStaticBox { + public static int $count = 1; +} +EvalReadonlyStaticBox::$count = EvalReadonlyStaticBox::$count + 1; +echo EvalReadonlyStaticBox::$count;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2"); + assert_eq!(values.get(result), FakeValue::Null); +} + +/// Verifies readonly classes may extend readonly parents and use inherited constructors. +#[test] +fn execute_program_allows_readonly_class_extending_readonly_parent() { + let program = parse_fragment( + br#"readonly class EvalReadonlyParentBase { + public int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +readonly class EvalReadonlyParentChild extends EvalReadonlyParentBase {} +$box = new EvalReadonlyParentChild(13); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "13:"); + assert_eq!(values.get(result), FakeValue::Int(13)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/basics.rs b/crates/elephc-magician/src/interpreter/tests/classes/basics.rs new file mode 100644 index 0000000000..b11ae0ec9b --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/basics.rs @@ -0,0 +1,430 @@ +//! Purpose: +//! Basic interpreter tests for eval-declared construction, inheritance, +//! properties, static dispatch, builtin contracts, and `ArrayAccess`. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover class property semantics that need eval runtime state. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies promoted constructor properties initialize before the constructor body runs. +#[test] +fn execute_program_initializes_constructor_promoted_properties() { + let program = parse_fragment( + br#"class EvalPromotedUser { + public function __construct(public int $id, private string $name = "Ada") { + $this->id = $this->id + 1; + } + public function label() { return $this->id . ":" . $this->name; } +} +$user = new EvalPromotedUser(6); +echo $user->id; echo ":"; +return $user->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:"); + assert_eq!(values.get(result), FakeValue::String("7:Ada".to_string())); +} + +/// Verifies `new self/static/parent` resolve inside eval-declared methods. +#[test] +fn execute_program_constructs_relative_class_names_from_eval_methods() { + let program = parse_fragment( + br#"class EvalRelativeFactoryBase { + public string $label; + public function __construct($label = "base") { $this->label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class EvalRelativeFactoryChild extends EvalRelativeFactoryBase { + public function parentFactory() { return new parent("parent"); } +} +$child = new EvalRelativeFactoryChild("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label; +return $self instanceof EvalRelativeFactoryBase + && !($self instanceof EvalRelativeFactoryChild) + && $static instanceof EvalRelativeFactoryChild + && $parent instanceof EvalRelativeFactoryBase + && !($parent instanceof EvalRelativeFactoryChild);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRelativeFactoryBase:self:EvalRelativeFactoryChild:static:EvalRelativeFactoryBase:parent" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies `new self/static/parent` stay relative inside eval namespaces. +#[test] +fn execute_program_constructs_namespaced_relative_class_names_from_eval_methods() { + let program = parse_fragment( + br#"namespace EvalRelativeNs; +class Base { + public string $label; + public function __construct($label = "base") { $this->label = $label; } + public function selfFactory() { return new self("self"); } + public function staticFactory() { return new static("static"); } +} +class Child extends Base { + public function parentFactory() { return new parent("parent"); } +} +$child = new Child("root"); +$self = $child->selfFactory(); +$static = $child->staticFactory(); +$parent = $child->parentFactory(); +echo get_class($self); echo ":"; echo $self->label; echo ":"; +echo get_class($static); echo ":"; echo $static->label; echo ":"; +echo get_class($parent); echo ":"; echo $parent->label; +return $self instanceof Base + && !($self instanceof Child) + && $static instanceof Child + && $parent instanceof Base + && !($parent instanceof Child);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRelativeNs\\Base:self:EvalRelativeNs\\Child:static:EvalRelativeNs\\Base:parent" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies PHP legacy `var` properties behave as public eval properties. +#[test] +fn execute_program_supports_legacy_var_properties() { + let program = parse_fragment( + br#"trait EvalLegacyVarTrait { + var ?string $label = "trait"; +} +class EvalLegacyVarProperty { + use EvalLegacyVarTrait; + var $plain = "p"; + var ?int $count = null; +} +$object = new EvalLegacyVarProperty(); +$plain = new ReflectionProperty("EvalLegacyVarProperty", "plain"); +$count = new ReflectionProperty("EvalLegacyVarProperty", "count"); +$label = new ReflectionProperty("EvalLegacyVarProperty", "label"); +$defaults = (new ReflectionClass("EvalLegacyVarProperty"))->getDefaultProperties(); +echo $object->plain; echo ":"; +echo $plain->isPublic() ? "P" : "p"; echo ":"; +echo $plain->hasType() ? "T" : "t"; echo ":"; +echo $count->isPublic() ? "C" : "c"; echo ":"; +echo $count->hasType() ? $count->getType()->getName() : "none"; echo ":"; +echo $count->getType()->allowsNull() ? "N" : "n"; echo ":"; +echo is_null($defaults["count"]) ? "null" : "bad"; echo ":"; +echo $object->label; echo ":"; +echo $label->isPublic() ? "L" : "l"; echo ":"; +echo $label->getType()->getName(); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "p:P:t:C:int:N:null:trait:L:string"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies comma-separated eval properties initialize instance, static, and trait storage. +#[test] +fn execute_program_reads_comma_separated_eval_properties() { + let program = parse_fragment( + br#"class EvalMultiPropertyBox { + public int $a = 1, $b = 2; + public static int $s = 3, $t = 4; + public function sum() { return $this->a + $this->b + self::$s + self::$t; } +} +trait EvalMultiPropertyTrait { + public int $x = 5, $y = 6; +} +class EvalMultiPropertyTraitBox { + use EvalMultiPropertyTrait; + public function sum() { return $this->x + $this->y; } +} +$box = new EvalMultiPropertyBox(); +$traitBox = new EvalMultiPropertyTraitBox(); +echo $box->a; echo $box->b; echo ":"; +echo EvalMultiPropertyBox::$s; echo EvalMultiPropertyBox::$t; echo ":"; +echo $traitBox->x; echo $traitBox->y; echo ":"; +return $box->sum() + $traitBox->sum();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12:34:56:"); + assert_eq!(values.get(result), FakeValue::Int(21)); +} + +/// Verifies eval static method calls preserve PHP late-static forwarding rules. +#[test] +fn execute_program_forwards_eval_static_method_called_class() { + let program = parse_fragment( + br#"class EvalForwardA { + public static function who() { return static::tag(); } + public static function relayNamed() { return EvalForwardA::who(); } + public static function relaySelf() { return self::who(); } + public static function tag() { return "A"; } +} +class EvalForwardB extends EvalForwardA { + public static function relayParent() { return parent::who(); } + public static function relayStatic() { return static::who(); } + public static function tag() { return "B"; } +} +echo EvalForwardB::relayNamed(); echo ":"; +echo EvalForwardB::relaySelf(); echo ":"; +echo EvalForwardB::relayParent(); echo ":"; +return EvalForwardB::relayStatic();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:B:B:"); + assert_eq!(values.get(result), FakeValue::String("B".to_string())); +} + +/// Verifies `get_called_class()` follows eval late-static method scopes. +#[test] +fn execute_program_dispatches_get_called_class_builtin() { + let program = parse_fragment( + br#"class EvalCalledClassBase { + public function instanceWho() { return get_called_class(); } + public function instanceCall() { return call_user_func("get_called_class"); } + public static function staticWho() { return get_called_class(); } + public static function staticCallArray() { return call_user_func_array("get_called_class", []); } + public static function makeCallable() { return get_called_class(...); } +} +class EvalCalledClassChild extends EvalCalledClassBase {} +$child = new EvalCalledClassChild(); +echo $child->instanceWho(); echo ":"; +echo $child->instanceCall(); echo ":"; +echo EvalCalledClassChild::staticWho(); echo ":"; +echo EvalCalledClassChild::staticCallArray(); echo ":"; +echo EvalCalledClassBase::staticWho(); echo ":"; +try { + get_called_class(); +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); echo ":"; +} +$fn = EvalCalledClassChild::makeCallable(); +try { + $fn(); +} catch (Error $e) { + echo "callable:"; +} +echo function_exists("get_called_class"); echo is_callable("get_called_class"); +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassChild:EvalCalledClassBase:Error:get_called_class() must be called from within a class:callable:11" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval classes can extend runtime/AOT classes through the dynamic backing object. +#[test] +fn execute_program_extends_runtime_class_from_eval_declaration() { + let program = parse_fragment( + br#"class EvalRuntimeParentChild extends KnownClass { + public function own() { return $this->read_x() + 1; } +} +$box = new EvalRuntimeParentChild(9); +echo get_class($box); echo ":"; +echo get_parent_class($box); echo ":"; +echo is_a($box, "EvalRuntimeParentChild") ? "D" : "d"; echo ":"; +echo is_a($box, "KnownClass") ? "K" : "k"; echo ":"; +echo is_a($box, "KnownInterface") ? "I" : "i"; echo ":"; +echo is_subclass_of($box, "KnownClass") ? "S" : "s"; echo ":"; +echo is_subclass_of("EvalRuntimeParentChild", "KnownClass") ? "N" : "n"; echo ":"; +echo $box->read_x(); echo ":"; +return $box->own();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "EvalRuntimeParentChild:KnownClass:D:K:I:S:N:9:" + ); + assert_eq!(values.get(result), FakeValue::Int(10)); +} + +/// Verifies eval classes cannot directly implement PHP's special Throwable contract. +#[test] +fn execute_program_rejects_eval_class_implementing_throwable_contracts() { + for (source, label) in [ + ( + br#"class EvalInvalidThrowableClass implements Throwable {}"# as &[u8], + "direct Throwable implementation should fail", + ), + ( + br#"interface EvalThrowableMarker extends Throwable {} +class EvalInvalidThrowableMarkerClass implements EvalThrowableMarker {}"#, + "Throwable-derived interface implementation should fail", + ), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies eval classes must satisfy methods required by PHP builtin interfaces. +#[test] +fn execute_program_rejects_invalid_builtin_interface_implementations() { + for (source, label) in [ + ( + br#"class EvalMissingCountable implements Countable {}"# as &[u8], + "missing Countable::count should fail", + ), + ( + br#"class EvalBadCountableReturn implements Countable { + public function count(): string { return "1"; } +}"#, + "incompatible Countable::count return type should fail", + ), + ( + br#"class EvalMissingStringable implements Stringable {}"#, + "missing Stringable::__toString should fail", + ), + ( + br#"class EvalBadIterator implements Iterator { + public function current(): mixed { return null; } + public function key(): mixed { return null; } + public function next(): void {} + public function valid(): bool { return false; } +}"#, + "missing Iterator::rewind should fail", + ), + ( + br#"class EvalBadJsonSerializable implements JsonSerializable { + public static function jsonSerialize(): mixed { return []; } +}"#, + "static JsonSerializable::jsonSerialize should fail", + ), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values).expect_err(label); + + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies abstract eval classes can defer PHP builtin interface methods. +#[test] +fn execute_program_allows_abstract_builtin_interface_implementations() { + let program = parse_fragment( + br#"abstract class EvalAbstractCountable implements Countable {} +return class_exists("EvalAbstractCountable");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `ArrayAccess` objects dispatch reads, writes, append, probes, and unset. +#[test] +fn execute_program_dispatches_eval_array_access_objects() { + let program = parse_fragment( + br#"class EvalArrayAccessBox implements ArrayAccess { + public function offsetExists(mixed $offset): bool { + echo "exists:" . $offset . ":"; + if ($offset === "missing") { + return false; + } + return true; + } + public function offsetGet(mixed $offset): mixed { + echo "get:" . $offset . ":"; + if ($offset === "empty") { + return ""; + } + return "v" . $offset; + } + public function offsetSet(mixed $offset, mixed $value): void { + if ($offset === null) { + echo "set:null:" . $value . ":"; + } else { + echo "set:" . $offset . ":" . $value . ":"; + } + } + public function offsetUnset(mixed $offset): void { + echo "unset:" . $offset . ":"; + } +} +$box = new EvalArrayAccessBox(); +$box["x"] = "1"; +$box[] = "tail"; +unset($box["drop"]); +if (isset($box["x"])) { echo "I:"; } else { echo "i:"; } +if (isset($box["missing"])) { echo "M:"; } else { echo "m:"; } +if (empty($box["empty"])) { echo "E:"; } else { echo "e:"; } +if (empty($box["missing"])) { echo "N:"; } else { echo "n:"; } +return $box["y"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "set:x:1:set:null:tail:unset:drop:exists:x:I:exists:missing:m:exists:empty:get:empty:E:exists:missing:N:get:y:" + ); + assert_eq!(values.get(result), FakeValue::String("vy".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/hook_contracts.rs b/crates/elephc-magician/src/interpreter/tests/classes/hook_contracts.rs new file mode 100644 index 0000000000..eb99913fbe --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/hook_contracts.rs @@ -0,0 +1,396 @@ +//! Purpose: +//! Interpreter tests for inherited, interface, abstract, and trait property-hook +//! contracts. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Tests distinguish get-only, set-required, readonly, type, and final/abstract +//! compatibility failures. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies get-only property hooks throw Error on writes outside a set accessor. +#[test] +fn execute_program_write_to_get_only_eval_property_hook_throws_error() { + let program = parse_fragment( + br#"class EvalHookReadOnly { + public int $answer { + get => 42; + } +} +$box = new EvalHookReadOnly(); +try { + $box->answer = 7; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Property EvalHookReadOnly::$answer is read-only" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval subclasses inherit parent property hooks. +#[test] +fn execute_program_inherits_eval_property_hooks() { + let program = parse_fragment( + br#"class EvalHookBase { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +class EvalHookChild extends EvalHookBase { + public function shout() { return $this->value . "?"; } +} +$box = new EvalHookChild(); +$box->value = "Ada"; +echo $box->value; echo ":"; +return $box->shout();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!:"); + assert_eq!(values.get(result), FakeValue::String("Ada!?".to_string())); +} + +/// Verifies eval interface property hook contracts are enforced through inheritance. +#[test] +fn execute_program_accepts_interface_property_hook_contracts() { + let program = parse_fragment( + br#"interface EvalHookContract { + public string $value { get; set; } +} +interface EvalNamedHookContract extends EvalHookContract { + public string $name { get; } +} +class EvalHookContractBox implements EvalNamedHookContract { + public string $name = "box"; + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$box = new EvalHookContractBox(); +$box->value = "Ada"; +echo $box->name; echo ":"; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "box:Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies a normal public mutable property satisfies an eval interface get/set contract. +#[test] +fn execute_program_accepts_plain_property_for_interface_hook_contracts() { + let program = parse_fragment( + br#"interface EvalPlainHookContract { + public string $value { get; set; } +} +class EvalPlainHookContractBox implements EvalPlainHookContract { + public string $value = "Ada"; +} +$box = new EvalPlainHookContractBox(); +echo $box->value; echo ":"; +$box->value = "Grace"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada:"); + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} + +/// Verifies interface property hook types are checked on abstract and concrete classes. +#[test] +fn execute_program_validates_interface_property_hook_types() { + let valid_program = parse_fragment( + br#"interface EvalIfaceGetWide { + public int|string $value { get; } +} +interface EvalIfaceSetNarrow { + public int $slot { set; } +} +abstract class EvalIfacePropertyDeferred implements EvalIfaceGetWide {} +abstract class EvalIfacePropertyGood implements EvalIfaceGetWide, EvalIfaceSetNarrow { + abstract public int $value { get; } + abstract public int|string $slot { set; } +} +class EvalIfacePropertyConcrete implements EvalIfaceGetWide { + public int $value = 4; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let result = execute_program(&valid_program, &mut scope, &mut values).expect("execute eval ir"); + assert_eq!(values.get(result), FakeValue::Bool(true)); + + let bad_abstract_get = parse_fragment( + br#"interface EvalIfaceGetInt { + public int $value { get; } +} +abstract class EvalIfaceGetWideBad implements EvalIfaceGetInt { + abstract public int|string $value { get; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_get, &mut scope, &mut values) + .expect_err("wider abstract get property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_abstract_set = parse_fragment( + br#"interface EvalIfaceSetWide { + public int|string $value { set; } +} +abstract class EvalIfaceSetNarrowBad implements EvalIfaceSetWide { + abstract public int $value { set; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_set, &mut scope, &mut values) + .expect_err("narrower abstract set property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_concrete_get = parse_fragment( + br#"interface EvalIfaceConcreteGetInt { + public int $value { get; } +} +class EvalIfaceConcreteGetWideBad implements EvalIfaceConcreteGetInt { + public int|string $value = 4; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_concrete_get, &mut scope, &mut values) + .expect_err("wider concrete get property type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_inherited_property = parse_fragment( + br#"interface EvalIfaceInheritedGet { + public int $value { get; } +} +abstract class EvalIfaceInheritedPropertyBase { + public string $value = "bad"; +} +abstract class EvalIfaceInheritedPropertyChild extends EvalIfaceInheritedPropertyBase implements EvalIfaceInheritedGet {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_inherited_property, &mut scope, &mut values) + .expect_err("inherited incompatible interface property should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies a get-only hook cannot satisfy a writable eval interface contract. +#[test] +fn execute_program_rejects_get_only_hook_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalHookSetContract { + public int $answer { get; set; } +} +class EvalHookGetOnlyContractBox implements EvalHookSetContract { + public int $answer { + get => 42; + } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("get-only hook should fail writable interface contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly properties cannot satisfy writable eval interface contracts. +#[test] +fn execute_program_rejects_readonly_property_for_interface_set_contract() { + let program = parse_fragment( + br#"interface EvalReadonlyHookContract { + public int $id { get; set; } +} +class EvalReadonlyHookContractBox implements EvalReadonlyHookContract { + public readonly int $id; + public function __construct($id) { $this->id = $id; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly property should fail writable interface contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies concrete eval subclasses satisfy abstract property hook contracts. +#[test] +fn execute_program_accepts_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"abstract class EvalAbstractHookBase { + abstract public string $value { get; set; } +} +class EvalAbstractHookBox extends EvalAbstractHookBase { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$box = new EvalAbstractHookBox(); +$box->value = "Ada"; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies normal mutable properties satisfy abstract get/set hook contracts. +#[test] +fn execute_program_accepts_plain_property_for_abstract_hook_contracts() { + let program = parse_fragment( + br#"abstract class EvalPlainAbstractHookBase { + abstract public string $value { get; set; } +} +class EvalPlainAbstractHookBox extends EvalPlainAbstractHookBase { + public string $value = "Ada"; +} +$box = new EvalPlainAbstractHookBox(); +echo $box->value; echo ":"; +$box->value = "Grace"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada:"); + assert_eq!(values.get(result), FakeValue::String("Grace".to_string())); +} + +/// Verifies concrete eval subclasses must declare inherited abstract properties. +#[test] +fn execute_program_rejects_missing_abstract_property_hook_contract() { + let program = parse_fragment( + br#"abstract class EvalMissingAbstractHookBase { + abstract public string $value { get; } +} +class EvalMissingAbstractHookBox extends EvalMissingAbstractHookBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing abstract property should fail concrete subclass"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract final eval properties are rejected while parsing. +#[test] +fn parse_fragment_rejects_final_abstract_property_hook_contract() { + let err = parse_fragment( + br#"abstract class EvalFinalAbstractHookBase { + abstract final public string $value { get; } +}"#, + ) + .expect_err("final abstract property should fail"); + + assert_eq!(err, EvalParseError::UnsupportedConstruct); +} + +/// Verifies readonly properties cannot satisfy abstract writable hook contracts. +#[test] +fn execute_program_rejects_readonly_property_for_abstract_set_contract() { + let program = parse_fragment( + br#"abstract class EvalReadonlyAbstractHookBase { + abstract public int $id { get; set; } +} +class EvalReadonlyAbstractHookBox extends EvalReadonlyAbstractHookBase { + public readonly int $id; + public function __construct($id) { $this->id = $id; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly property should fail abstract writable contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract trait property hook contracts are enforced after trait expansion. +#[test] +fn execute_program_enforces_trait_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"trait EvalTraitNeedsName { + abstract protected string $name { get; } + public function label() { return $this->name; } +} +class EvalTraitNameBox { + use EvalTraitNeedsName; + protected string $name = "Ada"; +} +$box = new EvalTraitNameBox(); +echo $box->label(); +return $box->label();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada"); + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/lifecycle.rs b/crates/elephc-magician/src/interpreter/tests/classes/lifecycle.rs new file mode 100644 index 0000000000..8d0abbc4db --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/lifecycle.rs @@ -0,0 +1,179 @@ +//! Purpose: +//! Interpreter tests for anonymous classes, cloning, clone visibility, and +//! destructor execution. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Lifecycle hooks are checked at their PHP-visible invocation boundaries. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies anonymous eval classes instantiate, reuse their synthetic class, and reflect as anonymous. +#[test] +fn execute_program_instantiates_anonymous_class_expressions() { + let program = parse_fragment( + br#"interface EvalAnonRuntimeLabel { + function label(); +} +class EvalAnonRuntimeBase { + protected string $prefix; + public function __construct($prefix) { $this->prefix = $prefix; } +} +function eval_anon_make($prefix) { + return new class($prefix) extends EvalAnonRuntimeBase implements EvalAnonRuntimeLabel { + public function label() { return $this->prefix . ":anon"; } + }; +} +$first = eval_anon_make("A"); +$second = eval_anon_make("B"); +echo $first->label(); echo ":"; +echo $second->label(); echo ":"; +echo get_class($first) === get_class($second) ? "same" : "different"; echo ":"; +$ref = new ReflectionClass(get_class($first)); +echo $ref->isAnonymous() ? "anonymous" : "named"; echo ":"; +echo $ref->implementsInterface("EvalAnonRuntimeLabel") ? "iface" : "bad"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:anon:B:anon:same:anonymous:iface"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly anonymous eval classes initialize and reject property writes. +#[test] +fn execute_program_instantiates_readonly_anonymous_class_expressions() { + let program = parse_fragment( + br#"$box = new readonly class("frozen") { + public function __construct(public string $label) {} +}; +echo $box->label; echo ":"; +try { + $box->label = "bad"; + echo "bad"; +} catch (Error $e) { + echo get_class($e); +} +return $box->label;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "frozen:Error"); + assert_eq!(values.get(result), FakeValue::String("frozen".to_string())); +} + +/// Verifies eval object cloning copies properties before running `__clone()`. +#[test] +fn execute_program_clones_eval_object_and_runs_clone_hook() { + let program = parse_fragment( + br#"class EvalCloneRuntimeBox { + public string $name; + public function __construct($name) { $this->name = $name; } + public function __clone() { $this->name = $this->name . ":clone"; } +} +$first = new EvalCloneRuntimeBox("A"); +$second = clone $first; +echo $first->name; echo ":"; +echo $second->name; +$second->name = "B"; +return $first->name . ":" . $second->name;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:A:clone"); + assert_eq!(values.get(result), FakeValue::String("A:B".to_string())); +} + +/// Verifies private `__clone()` can be invoked from inside the declaring eval class. +#[test] +fn execute_program_allows_private_clone_hook_inside_declaring_class() { + let program = parse_fragment( + br#"class EvalCloneRuntimePrivateBox { + public string $name = "A"; + private function __clone() { $this->name = $this->name . ":copy"; } + public function copy() { return clone $this; } +} +$first = new EvalCloneRuntimePrivateBox(); +$second = $first->copy(); +echo $first->name; echo ":"; +echo $second->name; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A:A:copy"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval-declared `__destruct()` runs for explicit unset and discarded temporaries. +#[test] +fn execute_program_runs_eval_destructor_on_final_release() { + let program = parse_fragment( + br#"class EvalDestructRuntimeBox { + public string $name; + public function __construct($name) { $this->name = $name; } + public function __destruct() { echo "drop:" . $this->name . ":"; } +} +$box = new EvalDestructRuntimeBox("A"); +unset($box); +new EvalDestructRuntimeBox("B"); +echo "after"; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "drop:A:drop:B:after"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies private `__clone()` throws Error through a global clone expression. +#[test] +fn execute_program_private_clone_hook_outside_declaring_class_throws_error() { + let program = parse_fragment( + br#"class EvalCloneRuntimePrivateFail { + private function __clone() {} +} +$box = new EvalCloneRuntimePrivateFail(); +try { + clone $box; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Call to private EvalCloneRuntimePrivateFail::__clone() from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/magic_methods.rs b/crates/elephc-magician/src/interpreter/tests/classes/magic_methods.rs new file mode 100644 index 0000000000..4e28c289af --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/magic_methods.rs @@ -0,0 +1,434 @@ +//! Purpose: +//! Interpreter tests for eval magic property, string-conversion, debug, and +//! state-restoration methods. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Dispatch, visibility, existing dynamic properties, and PHP magic-method +//! contracts are covered separately. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies undefined eval property reads and writes dispatch through `__get` and `__set`. +#[test] +fn execute_program_dispatches_eval_magic_get_and_set() { + let program = parse_fragment( + br#"class EvalMagicPropertyBox { + public string $events = ""; + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return "value:" . $name; + } + public function __set($name, $value) { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } +} +$box = new EvalMagicPropertyBox(); +echo $box->missing; echo ":"; +$box->other = "B"; +$box->events = $box->events . "public;"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "value:missing:"); + assert_eq!( + values.get(result), + FakeValue::String("get:missing;set:other=B;public;".to_string()) + ); +} + +/// Verifies eval invokes non-public magic methods that PHP accepts with warnings. +#[test] +fn execute_program_dispatches_non_public_eval_magic_methods() { + let program = parse_fragment( + br#"class EvalNonPublicMagicBox { + public string $events = ""; + protected function __get(string $name) { + $this->events = $this->events . "get:" . $name . ";"; + return "value:" . $name; + } + protected function __set(string $name, $value): void { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } + private function __isset(string $name): bool { + $this->events = $this->events . "isset:" . $name . ";"; + return true; + } + private function __unset(string $name): void { + $this->events = $this->events . "unset:" . $name . ";"; + } + private function __call(string $name, array $args) { + return $name . ":" . $args[0] . ":" . $args["name"]; + } + private static function __callStatic(string $name, array $args) { + return $name . ":" . $args[0] . ":" . $args["name"]; + } + private function __invoke(string $left = "I", string $right = "J") { + return "invoke:" . $left . $right; + } +} +$box = new EvalNonPublicMagicBox(); +echo is_callable($box) ? "callable:" : "bad:"; +echo $box->missing; echo ":"; +$box->other = "B"; +echo isset($box->probe) ? "isset:" : "bad:"; +unset($box->gone); +echo $box->run("A", name: "B"); echo ":"; +echo EvalNonPublicMagicBox::staticRun("C", name: "D"); echo ":"; +echo $box(right: "F", left: "E"); +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "callable:value:missing:isset:run:A:B:staticRun:C:D:invoke:EF" + ); + assert_eq!( + values.get(result), + FakeValue::String("get:missing;set:other=B;isset:probe;unset:gone;".to_string()) + ); +} + +/// Verifies inaccessible eval properties dispatch through magic property methods. +#[test] +fn execute_program_dispatches_inaccessible_eval_properties_to_magic_methods() { + let program = parse_fragment( + br#"class EvalMagicPrivatePropertyBox { + private string $secret = "raw"; + public string $events = ""; + public function readOwn() { return $this->secret; } + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return "read:" . $name; + } + public function __set($name, $value) { + $this->events = $this->events . "set:" . $name . "=" . $value . ";"; + } +} +$box = new EvalMagicPrivatePropertyBox(); +echo $box->readOwn(); echo ":"; +echo $box->secret; echo ":"; +$box->secret = "new"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "raw:read:secret:"); + assert_eq!( + values.get(result), + FakeValue::String("get:secret;set:secret=new;".to_string()) + ); +} + +/// Verifies dynamic properties created without `__set` are read directly even when `__get` exists. +#[test] +fn execute_program_reads_existing_dynamic_property_before_magic_get() { + let program = parse_fragment( + br#"class EvalMagicExistingDynamicBox { + public function __get($name) { + return "magic:" . $name; + } +} +$box = new EvalMagicExistingDynamicBox(); +$box->known = "plain"; +echo $box->known; echo ":"; +return $box->missing;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "plain:"); + assert_eq!( + values.get(result), + FakeValue::String("magic:missing".to_string()) + ); +} + +/// Verifies eval property probes and unsets dispatch through `__isset` and `__unset`. +#[test] +fn execute_program_dispatches_eval_magic_isset_empty_and_unset() { + let program = parse_fragment( + br#"class EvalMagicPropertyProbeBox { + public string $events = ""; + public string $present = "ready"; + public $nullish = null; + private string $secret = "raw"; + public function __isset($name) { + $this->events = $this->events . "isset:" . $name . ";"; + return $name !== "no"; + } + public function __get($name) { + $this->events = $this->events . "get:" . $name . ";"; + return $name === "empty" ? "" : "value:" . $name; + } + public function __unset($name) { + $this->events = $this->events . "unset:" . $name . ";"; + } +} +$box = new EvalMagicPropertyProbeBox(); +echo isset($box->present) ? "P" : "p"; echo ":"; +echo isset($box->nullish) ? "N" : "n"; echo ":"; +echo isset($box->secret) ? "S" : "s"; echo ":"; +echo isset($box->no) ? "bad" : "no"; echo ":"; +echo empty($box->secret) ? "bad" : "filled"; echo ":"; +echo empty($box->empty) ? "empty" : "bad"; echo ":"; +unset($box->present); +unset($box->secret); +unset($box->missing); +echo isset($box->present) ? "bad" : "unset"; echo ":"; +return $box->events;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "P:n:S:no:filled:empty:unset:"); + assert_eq!( + values.get(result), + FakeValue::String( + "isset:secret;isset:no;isset:secret;get:secret;isset:empty;get:empty;unset:secret;unset:missing;" + .to_string() + ) + ); +} + +/// Verifies eval objects stringify through public `__toString()` in PHP string contexts. +#[test] +fn execute_program_dispatches_eval_magic_tostring_for_string_contexts() { + let program = parse_fragment( + br#"class EvalStringableBox { + public string $name = "Ada"; + public function __toString() { + return "box:" . $this->name; + } + public function accepts(string $value) { + return "typed:" . $value; + } +} +$box = new EvalStringableBox(); +echo $box; echo ":"; +print $box; echo ":"; +echo "pre" . $box; echo ":"; +echo strval($box); echo ":"; +echo call_user_func("strval", $box); echo ":"; +echo call_user_func_array("strval", [$box]); echo ":"; +echo $box instanceof Stringable ? "S" : "s"; echo ":"; +return $box->accepts($box);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "box:Ada:box:Ada:prebox:Ada:box:Ada:box:Ada:box:Ada:S:" + ); + assert_eq!( + values.get(result), + FakeValue::String("typed:box:Ada".to_string()) + ); +} + +/// Verifies eval objects without `__toString()` fail in PHP string contexts. +#[test] +fn execute_program_rejects_eval_object_string_context_without_tostring() { + let program = parse_fragment( + br#"class EvalPlainStringContext {} +$box = new EvalPlainStringContext(); +try { + echo $box; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Object of class EvalPlainStringContext could not be converted to string" + ); +} + +/// Verifies eval rejects magic methods whose staticness, arity, or fatal contracts are invalid. +#[test] +fn execute_program_rejects_invalid_eval_magic_method_contracts() { + let cases: Vec<(&[u8], &str)> = vec![ + ( + br#"class EvalBadToString { private function __toString() { return "x"; } }"#.as_slice(), + "private __toString", + ), + ( + br#"class EvalBadToStringReturn { public function __toString(): int { return 1; } }"#.as_slice(), + "bad __toString return type", + ), + ( + br#"class EvalBadGetByRef { public function __get(&$name) { return "x"; } }"#.as_slice(), + "by-ref __get", + ), + ( + br#"class EvalBadGetParamType { public function __get(int $name) { return "x"; } }"#.as_slice(), + "bad __get parameter type", + ), + ( + br#"class EvalBadIssetReturn { public function __isset($name): string { return "yes"; } }"#.as_slice(), + "bad __isset return type", + ), + ( + br#"class EvalBadUnsetReturn { public function __unset($name): int { return 1; } }"#.as_slice(), + "bad __unset return type", + ), + ( + br#"class EvalBadSetReturn { public function __set($name, $value): int { return 1; } }"#.as_slice(), + "bad __set return type", + ), + ( + br#"class EvalBadSetParamType { public function __set(int $name, $value): void {} }"#.as_slice(), + "bad __set parameter type", + ), + ( + br#"class EvalBadCall { public function __call($name, ...$args) { return "x"; } }"#.as_slice(), + "variadic __call", + ), + ( + br#"class EvalBadCallArgsType { public function __call(string $name, string $args) {} }"#.as_slice(), + "bad __call args type", + ), + ( + br#"class EvalBadCallNameType { public function __call(int $name, array $args) {} }"#.as_slice(), + "bad __call name type", + ), + ( + br#"class EvalBadCallStatic { public function __callStatic($name, $args) { return "x"; } }"#.as_slice(), + "instance __callStatic", + ), + ( + br#"class EvalBadCallStaticArgsType { public static function __callStatic(string $name, string $args) {} }"#.as_slice(), + "bad __callStatic args type", + ), + ( + br#"class EvalBadSleepReturn { public function __sleep(): string { return "x"; } }"#.as_slice(), + "bad __sleep return type", + ), + ( + br#"class EvalBadSerializeStatic { public static function __serialize(): array { return []; } }"#.as_slice(), + "static __serialize", + ), + ( + br#"class EvalBadWakeupArity { public function __wakeup($value): void {} }"#.as_slice(), + "bad __wakeup arity", + ), + ( + br#"class EvalBadUnserializeArity { public function __unserialize(): void {} }"#.as_slice(), + "bad __unserialize arity", + ), + ( + br#"class EvalBadUnserializeReturn { public function __unserialize(array $data): int { return 1; } }"#.as_slice(), + "bad __unserialize return type", + ), + ( + br#"class EvalBadUnserializeParam { public function __unserialize(string $data): void {} }"#.as_slice(), + "bad __unserialize parameter type", + ), + ( + br#"class EvalBadDebugInfoReturn { public function __debugInfo(): string { return "x"; } }"#.as_slice(), + "bad __debugInfo return type", + ), + ( + br#"class EvalBadDebugInfoStatic { public static function __debugInfo(): array { return []; } }"#.as_slice(), + "static __debugInfo", + ), + ( + br#"class EvalBadSetStateInstance { public function __set_state($data) {} }"#.as_slice(), + "instance __set_state", + ), + ( + br#"class EvalBadSetStateArity { public static function __set_state($data, $extra) {} }"#.as_slice(), + "bad __set_state arity", + ), + ( + br#"class EvalBadSetStateParam { public static function __set_state(string $data) {} }"#.as_slice(), + "bad __set_state parameter type", + ), + ( + br#"class EvalBadClone { public static function __clone() {} }"#.as_slice(), + "static __clone", + ), + ( + br#"class EvalBadCloneReturn { public function __clone(): int {} }"#.as_slice(), + "bad __clone return type", + ), + ( + br#"class EvalBadDestruct { public static function __destruct() {} }"#.as_slice(), + "static __destruct", + ), + ( + br#"class EvalBadConstructReturn { public function __construct(): void {} }"#.as_slice(), + "bad __construct return type", + ), + ( + br#"class EvalBadDestructReturn { public function __destruct(): void {} }"#.as_slice(), + "bad __destruct return type", + ), + ( + br#"trait EvalBadMagicTrait { public static function __isset($name) { return true; } }"#.as_slice(), + "trait static __isset", + ), + ]; + + for (source, label) in cases { + let program = parse_fragment(source).expect(label); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect_err(label); + } +} + +/// Verifies eval accepts PHP-compatible debug and set-state magic method contracts. +#[test] +fn execute_program_accepts_debug_and_set_state_magic_contracts() { + let program = parse_fragment( + br#"class EvalGoodDebugInfoMagic { + public function __debugInfo(): ?array { return null; } +} +class EvalGoodSetStateMagic { + public static function __set_state($data) {} +} +return class_exists("EvalGoodDebugInfoMagic") && class_exists("EvalGoodSetStateMagic");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/mod.rs b/crates/elephc-magician/src/interpreter/tests/classes/mod.rs new file mode 100644 index 0000000000..8b166a68d3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/mod.rs @@ -0,0 +1,19 @@ +//! Purpose: +//! Organizes interpreter tests for eval-declared class runtime behavior by +//! property, lifecycle, hook, and contract responsibility. +//! +//! Called from: +//! - `crate::interpreter::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve the original test names for stable exact filtering. + +mod asymmetric_properties; +mod attributes; +mod basics; +mod hook_contracts; +mod lifecycle; +mod magic_methods; +mod promoted_references; +mod property_hooks; +mod readonly; diff --git a/crates/elephc-magician/src/interpreter/tests/classes/promoted_references.rs b/crates/elephc-magician/src/interpreter/tests/classes/promoted_references.rs new file mode 100644 index 0000000000..fef311f02f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/promoted_references.rs @@ -0,0 +1,171 @@ +//! Purpose: +//! Interpreter tests for by-reference constructor-promoted property aliases. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Cases cover variables, array elements, object/static storage, nested paths, +//! defaults, and the readonly incompatibility. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies by-reference promoted properties stay aliased to caller variables. +#[test] +fn execute_program_aliases_by_reference_promoted_variable_properties() { + let program = parse_fragment( + br#"class EvalPromotedRefBox { + public function __construct(public &$value) {} +} +$value = 1; +$box = new EvalPromotedRefBox($value); +$box->value = 5; +echo $value; echo ":"; +$value = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted properties can alias caller array elements. +#[test] +fn execute_program_aliases_by_reference_promoted_array_element_properties() { + let program = parse_fragment( + br#"class EvalPromotedArrayRefBox { + public function __construct(public &$value) {} +} +$items = [1]; +$box = new EvalPromotedArrayRefBox($items[0]); +$box->value = 5; +echo $items[0]; echo ":"; +$items[0] = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted properties can alias caller object properties. +#[test] +fn execute_program_aliases_by_reference_promoted_object_property_properties() { + let program = parse_fragment( + br#"class EvalPromotedObjectRefHolder { + public $value = 1; +} +class EvalPromotedObjectRefBox { + public function __construct(public &$value) {} +} +$holder = new EvalPromotedObjectRefHolder(); +$box = new EvalPromotedObjectRefBox($holder->value); +$box->value = 5; +echo $holder->value; echo ":"; +$holder->value = 7; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies by-reference promoted properties can alias static and nested property targets. +#[test] +fn execute_program_aliases_by_reference_promoted_static_and_nested_properties() { + let program = parse_fragment( + br#"class EvalPromotedStaticRefHolder { + public static $value = 1; + public $items = [1]; + public static $staticItems = [1]; +} +class EvalPromotedStaticRefBox { + public function __construct(public &$value) {} +} +$box = new EvalPromotedStaticRefBox(EvalPromotedStaticRefHolder::$value); +$box->value = 5; +echo EvalPromotedStaticRefHolder::$value; echo ":"; +EvalPromotedStaticRefHolder::$value = 7; +echo $box->value; echo ":"; +$holder = new EvalPromotedStaticRefHolder(); +$itemBox = new EvalPromotedStaticRefBox($holder->items[0]); +$itemBox->value = 11; +echo $holder->items[0]; echo ":"; +$holder->items[0] = 13; +echo $itemBox->value; echo ":"; +$staticItemBox = new EvalPromotedStaticRefBox(EvalPromotedStaticRefHolder::$staticItems[0]); +$staticItemBox->value = 17; +echo EvalPromotedStaticRefHolder::$staticItems[0]; echo ":"; +EvalPromotedStaticRefHolder::$staticItems[0] = 19; +return $staticItemBox->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5:7:11:13:17:"); + assert_eq!(values.get(result), FakeValue::Int(19)); +} + +/// Verifies by-reference promoted defaults use internal property alias storage. +#[test] +fn execute_program_aliases_by_reference_promoted_default_properties() { + let program = parse_fragment( + br#"class EvalPromotedDefaultRefBox { + public function __construct(public &$value = null) {} +} +$box = new EvalPromotedDefaultRefBox(); +$box->value = 5; +echo $box->value; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "5"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} + +/// Verifies readonly by-reference promotion fails when the constructor creates the alias. +#[test] +fn execute_program_rejects_readonly_by_reference_promoted_properties() { + let program = parse_fragment( + br#"class EvalPromotedReadonlyRefBox { + public function __construct(public readonly int &$value) {} +} +$value = 1; +new EvalPromotedReadonlyRefBox($value);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("readonly by-reference promoted property should fail at construction"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/property_hooks.rs b/crates/elephc-magician/src/interpreter/tests/classes/property_hooks.rs new file mode 100644 index 0000000000..12bd1e9b89 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/property_hooks.rs @@ -0,0 +1,209 @@ +//! Purpose: +//! Interpreter tests for eval property get/set hooks and their type rules. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Tests cover by-reference getters, short setters, mixed-case access, and +//! parameter validation. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies a get-only property hook computes a virtual eval property. +#[test] +fn execute_program_reads_eval_property_get_hook() { + let program = parse_fragment( + br#"class EvalHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + get => $this->first . " " . $this->last; + } +} +$person = new EvalHookPerson(); +echo $person->full; +return $person->full;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace"); + assert_eq!( + values.get(result), + FakeValue::String("Ada Lovelace".to_string()) + ); +} + +/// Verifies by-reference get hook syntax routes through the concrete eval get accessor. +#[test] +fn execute_program_reads_eval_by_ref_get_property_hook() { + let program = parse_fragment( + br#"class EvalByRefGetHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + &get => $this->first . " " . $this->last; + } +} +$person = new EvalByRefGetHookPerson(); +echo $person->full; +return $person->full;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace"); + assert_eq!( + values.get(result), + FakeValue::String("Ada Lovelace".to_string()) + ); +} + +/// Verifies get/set property hooks can use the raw backing slot from inside accessors. +#[test] +fn execute_program_routes_eval_property_get_and_set_hooks() { + let program = parse_fragment( + br#"class EvalHookName { + public string $value { + get => $this->value; + set { $this->value = $value . "!"; } + } +} +$name = new EvalHookName(); +$name->value = "Ada"; +echo $name->value; +return $name->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada!"); + assert_eq!(values.get(result), FakeValue::String("Ada!".to_string())); +} + +/// Verifies short set hooks assign their expression result into the raw backing slot. +#[test] +fn execute_program_routes_eval_short_set_property_hooks() { + let program = parse_fragment( + br#"class EvalShortSetHookName { + public string $value { + get => $this->value; + set => trim($value); + } +} +class EvalShortSetHookLabel { + public string $text { + get => $this->text; + set(string $raw) => strtoupper($raw); + } +} +$name = new EvalShortSetHookName(); +$name->value = " Ada "; +echo "[" . $name->value . "]:"; +$label = new EvalShortSetHookLabel(); +$label->text = "hi"; +echo $label->text; +return $label->text;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "[Ada]:HI"); + assert_eq!(values.get(result), FakeValue::String("HI".to_string())); +} + +/// Verifies explicit set-hook parameter types are contravariant with the property type. +#[test] +fn execute_program_validates_eval_property_set_hook_parameter_types() { + let valid_program = parse_fragment( + br#"class EvalWideSetHookParam { + public string $value { + get => $this->value; + set(mixed $raw) => $raw; + } +} +$box = new EvalWideSetHookParam(); +$box->value = "Ada"; +return $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&valid_program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("Ada".to_string())); + + for source in [ + br#"class EvalNarrowSetHookParam { + public mixed $value { + set(string $raw) => $raw; + } +}"# + .as_slice(), + br#"class EvalNullableSetHookParam { + public ?string $value { + set(string $raw) => $raw; + } +}"# + .as_slice(), + ] { + let program = parse_fragment(source).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("incompatible set-hook parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + } +} + +/// Verifies nullsafe reads and mixed-case names still route through eval property hooks. +#[test] +fn execute_program_routes_eval_nullsafe_and_mixed_case_property_hooks() { + let program = parse_fragment( + br#"class EvalNullsafeHookPerson { + public string $first = "Ada"; + public string $last = "Lovelace"; + public string $full { + get => $this->first . " " . $this->last; + } +} +class EvalMixedCaseHookBox { + private int $store = 0; + public int $Total { + get { return $this->store; } + } + public function set(int $value) { $this->store = $value; } +} +function eval_hook_describe($person) { + return $person?->full ?? "(none)"; +} +$person = new EvalNullsafeHookPerson(); +$box = new EvalMixedCaseHookBox(); +$box->set(5); +echo eval_hook_describe($person) . "|" . eval_hook_describe(null) . "|" . $box->Total; +return $box->Total;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Ada Lovelace|(none)|5"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/classes/readonly.rs b/crates/elephc-magician/src/interpreter/tests/classes/readonly.rs new file mode 100644 index 0000000000..471e8dd895 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/classes/readonly.rs @@ -0,0 +1,325 @@ +//! Purpose: +//! Interpreter tests for readonly properties, readonly classes, and dynamic- +//! property restrictions. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Constructor initialization and all post-initialization write paths are +//! checked independently. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies promoted readonly properties throw Error outside their constructor. +#[test] +fn execute_program_promoted_readonly_property_write_after_constructor_throws_error() { + let program = parse_fragment( + br#"class EvalPromotedReadonlyBox { + public function __construct(public readonly int $id) {} + public function replace($id) { $this->id = $id; } +} +$box = new EvalPromotedReadonlyBox(7); +echo $box->id; +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo ":"; echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "7:Error:Cannot modify readonly property EvalPromotedReadonlyBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly eval properties can be initialized inside their constructor. +#[test] +fn execute_program_initializes_readonly_property_in_constructor() { + let program = parse_fragment( + br#"class EvalReadonlyBox { + public readonly int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +$box = new EvalReadonlyBox(7); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies direct reads of uninitialized typed eval properties throw catchable PHP errors. +#[test] +fn execute_program_rejects_uninitialized_typed_property_reads() { + let program = parse_fragment( + br#"class EvalTypedReadBox { + public int $typed; + public ?int $nullable; + public ?int $defaultNull = null; + public $plain; +} +$box = new EvalTypedReadBox(); +try { + echo $box->typed; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + echo $box->nullable; +} catch (Error $e) { + echo $e->getMessage(); +} +echo "|"; +echo is_null($box->defaultNull) ? "default-null" : "bad"; +echo "|"; +echo is_null($box->plain) ? "plain-null" : "bad"; +echo "|"; +$box->typed = 0; +echo $box->typed; +echo "|"; +unset($box->typed); +try { + echo $box->typed; +} catch (Error $e) { + echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Typed property EvalTypedReadBox::$typed must not be accessed before initialization|\ +Typed property EvalTypedReadBox::$nullable must not be accessed before initialization|\ +default-null|plain-null|0|\ +Typed property EvalTypedReadBox::$typed must not be accessed before initialization" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly eval properties throw Error on writes outside the declaring constructor. +#[test] +fn execute_program_readonly_property_write_after_constructor_throws_error() { + let program = parse_fragment( + br#"class EvalReadonlyBox { + public readonly int $id; + public function __construct($id) { $this->id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyBox(7); +try { + $box->replace(8); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot modify readonly property EvalReadonlyBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly eval properties must declare a type like PHP requires. +#[test] +fn execute_program_rejects_untyped_readonly_properties() { + let explicit = parse_fragment( + br#"class EvalReadonlyUntypedBox { + public readonly $value; +}"#, + ) + .expect("parse explicit readonly property"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&explicit, &mut scope, &mut values) + .expect_err("explicit readonly property without type should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); + + let readonly_class = parse_fragment( + br#"readonly class EvalReadonlyClassUntypedBox { + public $value; +}"#, + ) + .expect("parse readonly class property"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&readonly_class, &mut scope, &mut values) + .expect_err("readonly class property without type should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies readonly classes make instance properties readonly implicitly. +#[test] +fn execute_program_initializes_readonly_class_property_in_constructor() { + let program = parse_fragment( + br#"readonly class EvalReadonlyClassBox { + public int $id; + public function __construct($id) { $this->id = $id; } + public function id() { return $this->id; } +} +$box = new EvalReadonlyClassBox(11); +echo $box->id(); echo ":"; +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "11:"); + assert_eq!(values.get(result), FakeValue::Int(11)); +} + +/// Verifies readonly class instance properties throw Error on writes after construction. +#[test] +fn execute_program_readonly_class_property_write_after_constructor_throws_error() { + let program = parse_fragment( + br#"readonly class EvalReadonlyClassFailBox { + public int $id; + public function __construct($id) { $this->id = $id; } + public function replace($id) { $this->id = $id; } +} +$box = new EvalReadonlyClassFailBox(11); +try { + $box->replace(12); + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot modify readonly property EvalReadonlyClassFailBox::$id" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly classes throw Error on dynamic property creation without a magic setter. +#[test] +fn execute_program_readonly_class_dynamic_property_creation_throws_error() { + let program = parse_fragment( + br#"readonly class EvalReadonlyDynamicFailBox { + public int $id; + public function __construct($id) { $this->id = $id; } +} +$box = new EvalReadonlyDynamicFailBox(11); +try { + $box->dynamic = 12; + echo "bad"; +} catch (Error $e) { + echo get_class($e); echo ":"; echo $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot create dynamic property EvalReadonlyDynamicFailBox::$dynamic" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly classes may still handle missing property writes through `__set()`. +#[test] +fn execute_program_allows_readonly_class_magic_set_for_missing_properties() { + let program = parse_fragment( + br#"readonly class EvalReadonlyMagicSetBox { + public function __set($name, $value) { + echo $name; echo ":"; echo $value; + } +} +$box = new EvalReadonlyMagicSetBox(); +$box->dynamic = 12; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "dynamic:12"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies readonly classes reject PHP's global dynamic-property marker attribute. +#[test] +fn execute_program_rejects_allow_dynamic_properties_on_readonly_class() { + let program = + parse_fragment(br#"#[\AllowDynamicProperties] readonly class EvalReadonlyAllowDynamic {}"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("AllowDynamicProperties cannot apply to readonly classes"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies namespaced non-builtin attributes do not trigger the readonly-class marker rule. +#[test] +fn execute_program_allows_namespaced_allow_dynamic_properties_on_readonly_class() { + let program = parse_fragment( + br#"namespace EvalReadonlyAttrNs; +#[AllowDynamicProperties] readonly class Box {} +echo class_attribute_names("EvalReadonlyAttrNs\Box")[0]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalReadonlyAttrNs\\AllowDynamicProperties"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs deleted file mode 100644 index 8aca8ba05a..0000000000 --- a/crates/elephc-magician/src/interpreter/tests/dynamic_calls.rs +++ /dev/null @@ -1,1065 +0,0 @@ -//! Purpose: -//! Interpreter tests for variable calls, call_user_func, call_user_func_array, and duplicate dynamic declarations. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - These cases verify callable dispatch across eval functions, builtins, native functions, and object methods. - -use super::super::*; -use super::support::*; - -/// Verifies `call_user_func` inside eval can dispatch an eval-declared function. -#[test] -fn execute_program_call_user_func_dispatches_declared_function() { - let program = parse_fragment( - br#"function dyn($x) { return $x + 1; } -return call_user_func("dyn", 4);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(5)); -} -/// Verifies `call_user_func` inside eval can dispatch a supported builtin. -#[test] -fn execute_program_call_user_func_dispatches_builtin() { - let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); -} -/// Verifies `call_user_func` releases literal callback temporaries after dispatch. -#[test] -fn execute_program_call_user_func_releases_literal_callback_after_dispatch() { - let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); - assert!( - values - .releases - .iter() - .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), - "literal callback string should be released after dispatch" - ); -} - -/// Verifies `call_user_func` releases literal callback temporaries after dispatch fatal. -#[test] -fn execute_program_call_user_func_releases_literal_callback_after_dispatch_fatal() { - let program = - parse_fragment(br#"return call_user_func("strlen");"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - assert!( - values - .releases - .iter() - .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), - "literal callback string should be released after fatal dispatch" - ); -} - -/// Verifies `call_user_func` releases literal callback when later arg evaluation fails. -#[test] -fn execute_program_call_user_func_releases_literal_callback_after_arg_eval_fatal() { - let program = parse_fragment(br#"return call_user_func("strlen", MISSING_ARG);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - assert!( - values - .releases - .iter() - .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), - "literal callback string should be released when argument evaluation fails" - ); -} - -/// Verifies `call_user_func` inside eval can dispatch a registered native function. -#[test] -fn execute_program_call_user_func_dispatches_registered_native_function() { - let program = - parse_fragment(br#"return call_user_func("native_answer");"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} -/// Verifies string variable calls inside eval can dispatch a supported builtin. -#[test] -fn execute_program_variable_call_dispatches_builtin() { - let program = parse_fragment( - br#"$fn = "strlen"; -return $fn("abcd");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); -} -/// Verifies callable array entries can be invoked through postfix dynamic calls. -#[test] -fn execute_program_postfix_variable_call_dispatches_builtin() { - let program = parse_fragment( - br#"$callbacks = ["strlen"]; -return $callbacks[0]("abc");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(3)); -} -/// Verifies variable calls bind eval-declared function arguments by name. -#[test] -fn execute_program_variable_call_binds_declared_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } -$fn = "dyn"; -return $fn(y: 2, x: 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} -/// Verifies variable calls can dispatch registered native functions with named args. -#[test] -fn execute_program_variable_call_binds_registered_native_named_args() { - let program = parse_fragment( - br#"$fn = "native_answer"; -return $fn(right: 2, left: 1);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} -/// Verifies direct callable-array variable calls dispatch object methods. -#[test] -fn execute_program_callable_array_variable_dispatches_object_method() { - let program = parse_fragment( - br#"$box = new Box(41); -$cb = [$box, "add_x"]; -return $cb(1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); - assert_released_array_callable_index_temps(&values); -} -/// Verifies `call_user_func` dispatches callable arrays with object receivers. -#[test] -fn execute_program_call_user_func_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(42); -$cb = [$box, "read_x"]; -return call_user_func($cb);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); - assert_released_array_callable_index_temps(&values); -} - -/// Verifies callable-array index temporaries are released when validation rejects the callback. -#[test] -fn execute_program_call_user_func_releases_array_callable_index_temps_after_validation_fatal() { - let program = parse_fragment( - br#"class EvalMissingArrayCallableMethod {} -$missing = new EvalMissingArrayCallableMethod(); -try { - call_user_func([$missing, "MiSsInG"]); - return false; -} catch (TypeError $e) { - return true; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Bool(true)); - assert_released_array_callable_index_temps(&values); -} -/// Verifies `call_user_func_array` dispatches callable arrays with positional args. -#[test] -fn execute_program_call_user_func_array_dispatches_object_method_array() { - let program = parse_fragment( - br#"$box = new Box(39); -return call_user_func_array([$box, "add2_x"], [1, 2]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); -} - -/// Verifies static method callable arrays dispatch eval-declared static methods. -#[test] -fn execute_program_static_callable_array_dispatches_eval_method() { - let program = parse_fragment( - br#"class EvalStaticCallableBox { - public static function join($left, $right) { - return $left . $right; - } -} -$cb = ["EvalStaticCallableBox", "join"]; -echo $cb(right: "B", left: "A"); echo ":"; -echo call_user_func($cb, "C", "D"); echo ":"; -echo call_user_func_array($cb, ["right" => "F", "left" => "E"]); echo ":"; -$named = "EvalStaticCallableBox::join"; -return $named(right: "H", left: "G");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "AB:CD:EF:"); - assert_eq!(values.get(result), FakeValue::String("GH".to_string())); -} - -/// Verifies fake runtime releases include both temporary callable-array index cells. -fn assert_released_array_callable_index_temps(values: &FakeOps) { - assert!( - values - .releases - .iter() - .any(|release| values.get(*release) == FakeValue::Int(0)), - "array callable index 0 temporary should be released" - ); - assert!( - values - .releases - .iter() - .any(|release| values.get(*release) == FakeValue::Int(1)), - "array callable index 1 temporary should be released" - ); -} - -/// Verifies first-class callable syntax dispatches through eval's callback paths. -#[test] -fn execute_program_first_class_callables_dispatch_functions_and_methods() { - let program = parse_fragment( - br#"function eval_fc_double($value) { - return $value * 2; -} -class EvalFirstClassCallableBase { - public function __construct($offset = 1) { - $this->offset = $offset; - } - public function add($value) { - return $value + $this->offset; - } - public function keep($value) { - return $value > 2; - } - public function sum($carry, $value) { - return $carry + $value + $this->offset; - } - public function show($value, $key) { - echo $key . $value; - } - public static function join($left, $right) { - return $left . $right; - } - public static function compareDesc($left, $right) { - return $right - $left; - } - public static function label($value) { - return "base:" . $value; - } - public static function relay($value) { - $fn = static::label(...); - return $fn($value); - } -} -class EvalFirstClassCallableChild extends EvalFirstClassCallableBase { - public static function label($value) { - return "child:" . $value; - } -} -$function = eval_fc_double(...); -echo $function(4); echo ":"; -echo (strlen(...))("abcd"); echo ":"; -$box = new EvalFirstClassCallableBase(3); -$method = $box->add(...); -echo $method(4); echo ":"; -echo call_user_func($box->add(...), 5); echo ":"; -$static = EvalFirstClassCallableBase::join(...); -echo $static(right: "B", left: "A"); echo ":"; -$mapped = array_map($box->add(...), [1, 2]); -echo $mapped[0] . $mapped[1] . ":"; -$filtered = array_filter([1, 2, 3, 4], $box->keep(...)); -echo count($filtered) . ":"; -echo array_reduce([1, 2], $box->sum(...), 0) . ":"; -$walked = ["a" => 1]; -array_walk($walked, $box->show(...)); -echo ":"; -$sorted = [3, 1, 2]; -usort($sorted, EvalFirstClassCallableBase::compareDesc(...)); -echo $sorted[0] . $sorted[1] . $sorted[2] . ":"; -return EvalFirstClassCallableChild::relay("ok");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "8:4:7:8:AB:45:2:9:a1:321:"); - assert_eq!( - values.get(result), - FakeValue::String("child:ok".to_string()) - ); -} - -/// Verifies first-class static callables preserve late-static forwarding metadata. -#[test] -fn execute_program_first_class_static_callables_preserve_called_class() { - let program = parse_fragment( - br#"class EvalFirstClassStaticForwardBase { - public static function who() { - return static::tag(); - } - public static function tag() { - return "base"; - } - public static function relaySelf() { - $fn = self::who(...); - return $fn(); - } -} -class EvalFirstClassStaticForwardChild extends EvalFirstClassStaticForwardBase { - public static function relayParent() { - $fn = parent::who(...); - return $fn(); - } - public static function tag() { - return "child"; - } -} -echo EvalFirstClassStaticForwardChild::relayParent(); echo ":"; -return EvalFirstClassStaticForwardChild::relaySelf();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "child:"); - assert_eq!(values.get(result), FakeValue::String("child".to_string())); -} - -/// Verifies invokable eval objects dispatch through variable and callback call paths. -#[test] -fn execute_program_invokes_eval_object_callables() { - let program = parse_fragment( - br#"function eval_plain_call_side_effect() { - echo "bad"; - return "x"; -} -class EvalInvokableBox { - public function __construct($label = "box") { - $this->label = $label; - } - public function __invoke($left = "A", $right = "B") { - return $this->label . ":" . $left . $right; - } -} -class EvalPlainCallableProbe {} -$box = new EvalInvokableBox("box"); -$plain = new EvalPlainCallableProbe(); -echo is_callable($box) ? "Y:" : "N:"; -echo is_callable($plain) ? "bad:" : "plain:"; -echo $box(right: "D", left: "C"); echo ":"; -try { - $plain(eval_plain_call_side_effect()); - echo "bad"; -} catch (Error $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -echo ":"; -echo (new EvalInvokableBox("new"))("E", "F"); echo ":"; -echo call_user_func($box, "G", "H"); echo ":"; -$first = $box(...); -echo $first("K", "L"); echo ":"; -return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:box:KL:" - ); - assert_eq!(values.get(result), FakeValue::String("box:IJ".to_string())); -} - -/// Verifies call_user_func rejects non-invokable eval objects with PHP's TypeError. -#[test] -fn execute_program_call_user_func_rejects_non_invokable_eval_object() { - let program = parse_fragment( - br#"class EvalPlainCallbackError {} -$plain = new EvalPlainCallbackError(); -try { - call_user_func($plain); - echo "bad"; -} catch (TypeError $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -echo "|"; -try { - call_user_func_array($plain, []); - echo "bad"; -} catch (TypeError $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given|\ -TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, no array or string given" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies call_user_func rejects invalid object-method callable arrays with PHP's TypeError. -#[test] -fn execute_program_call_user_func_rejects_invalid_object_method_arrays() { - let program = parse_fragment( - br#"class EvalMissingCallbackArray {} -class EvalPrivateCallbackArray { - private function hidden() { - return "bad"; - } -} -class EvalPrivateInvokeCallbackArray { - private function __invoke() { - return "bad"; - } -} -class EvalInstanceCallbackArray { - public function inst() { - return "bad"; - } -} -$missing = new EvalMissingCallbackArray(); -try { - call_user_func([$missing, "MiSsInG"]); - echo "bad"; -} catch (TypeError $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -echo "|"; -try { - call_user_func_array([$missing, "missing"], []); - echo "bad"; -} catch (TypeError $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -echo "|"; -try { - call_user_func([new EvalPrivateCallbackArray(), "hidden"]); - echo "bad"; -} catch (TypeError $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -echo "|"; -try { - call_user_func([new EvalPrivateInvokeCallbackArray(), "__invoke"]); - echo "bad"; -} catch (TypeError $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -echo "|"; -try { - call_user_func(["EvalInstanceCallbackArray", "inst"]); - echo "bad"; -} catch (TypeError $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"MiSsInG\"|\ -TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"missing\"|\ -TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateCallbackArray::hidden()|\ -TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateInvokeCallbackArray::__invoke()|\ -TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, non-static method EvalInstanceCallbackArray::inst() cannot be called statically" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies call_user_func callable arrays still dispatch through magic method fallbacks. -#[test] -fn execute_program_call_user_func_arrays_dispatch_magic_method_fallbacks() { - let program = parse_fragment( - br#"class EvalMagicCallbackArray { - public function __call($method, $args) { - return $method . ":" . $args[0]; - } - public static function __callStatic($method, $args) { - return $method . ":" . $args[0]; - } -} -$box = new EvalMagicCallbackArray(); -echo is_callable([$box, "missing"]) ? "Y:" : "N:"; -echo call_user_func([$box, "missing"], "A") . ":"; -echo call_user_func_array([$box, "missing"], ["B"]) . ":"; -echo is_callable(["EvalMagicCallbackArray", "static_missing"]) ? "S:" : "s:"; -return call_user_func(["EvalMagicCallbackArray", "static_missing"], "C");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:missing:A:missing:B:S:"); - assert_eq!( - values.get(result), - FakeValue::String("static_missing:C".to_string()) - ); -} - -/// Verifies object-method callable arrays preserve eval named-argument binding. -#[test] -fn execute_program_object_method_callable_array_binds_eval_named_args() { - let program = parse_fragment( - br#"class EvalObjectCallableArrayBox { - public function join($left, $right) { - return $left . $right; - } -} -$box = new EvalObjectCallableArrayBox(); -$cb = [$box, "join"]; -echo is_callable($cb) ? "Y:" : "N:"; -return call_user_func_array($cb, ["right" => "B", "left" => "A"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "Y:"); - assert_eq!(values.get(result), FakeValue::String("AB".to_string())); -} - -/// Verifies static calls fall back to runtime AOT hooks when no eval class matches. -#[test] -fn execute_program_static_call_dispatches_runtime_method_hook() { - let program = parse_fragment( - br#"echo KnownClass::join("A", "B"); echo ":"; -$cb = ["KnownClass", "join"]; -echo call_user_func($cb, "C", "D"); echo ":"; -$named = "KnownClass::join"; -echo $named("E", "F"); echo ":"; -return call_user_func_array(["KnownClass", "sum"], [2, 5]);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut join_signature = NativeCallableSignature::new(2); - assert!(join_signature.set_param_name(0, "left")); - assert!(join_signature.set_param_name(1, "right")); - assert!(context.define_native_static_method_signature( - "KnownClass", - "join", - join_signature - )); - let mut sum_signature = NativeCallableSignature::new(2); - assert!(sum_signature.set_param_name(0, "left")); - assert!(sum_signature.set_param_name(1, "right")); - assert!(context.define_native_static_method_signature( - "KnownClass", - "sum", - sum_signature - )); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(values.output, "AB:CD:EF:"); - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies runtime AOT static method fallback binds registered named arguments. -#[test] -fn execute_program_static_runtime_method_hook_binds_named_args() { - let program = parse_fragment( - br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(context.define_native_static_method_signature("KnownClass", "join", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("registered named AOT call should bind"); - - assert_eq!(values.get(result), FakeValue::String("AB".to_string())); -} - -/// Verifies runtime AOT static method fallback fills registered scalar defaults. -#[test] -fn execute_program_static_runtime_method_hook_binds_default_args() { - let program = parse_fragment( - br#"echo KnownClass::join("A"); echo ":"; -return call_user_func_array(["KnownClass", "join"], ["left" => "C"]);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(signature.set_param_default(1, NativeCallableDefault::String("B".to_string()))); - assert!(context.define_native_static_method_signature("KnownClass", "join", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("registered AOT defaults should bind"); - - assert_eq!(values.output, "AB:"); - assert_eq!(values.get(result), FakeValue::String("CB".to_string())); -} - -/// Verifies runtime AOT static method fallback honors by-reference parameter metadata. -#[test] -fn execute_program_static_runtime_method_hook_rejects_by_ref_temporary_arg() { - let program = parse_fragment(br#"return KnownClass::sum(1, 2);"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect_err("literal cannot satisfy a static by-reference method parameter"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies callable-array AOT method dispatch preserves by-reference writeback. -#[test] -fn execute_program_callable_array_runtime_method_writes_back_by_ref_type_coercion() { - let program = parse_fragment( - br#"$box = new KnownClass(10); -$cb = [$box, "add2_x"]; -$value = "3"; -echo $cb($value, 2); -return $value;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(signature.set_param_type( - 0, - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("callable-array runtime method should preserve by-ref target"); - - assert_eq!(values.output, "15"); - assert_eq!(values.get(result), FakeValue::Int(3)); -} - -/// Verifies `call_user_func_array()` preserves by-reference array elements for AOT methods. -#[test] -fn execute_program_call_user_func_array_runtime_method_writes_back_by_ref_type_coercion() { - let program = parse_fragment( - br#"$box = new KnownClass(10); -$cb = [$box, "add2_x"]; -$value = "3"; -echo call_user_func_array($cb, [&$value, 2]); -echo ":"; -return $value;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(signature.set_param_type( - 0, - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("call_user_func_array should preserve by-ref array element target"); - - assert_eq!(values.output, "15:"); - assert_eq!(values.get(result), FakeValue::Int(3)); -} - -/// Verifies string and first-class AOT static callables preserve by-reference writeback. -#[test] -fn execute_program_static_runtime_callables_write_back_by_ref_type_coercion() { - let program = parse_fragment( - br#"$string = "KnownClass::sum"; -$left = "3"; -echo $string($left, 2); echo ":"; -echo $left + 1; echo ":"; -$first = KnownClass::sum(...); -$next = "4"; -echo $first($next, 5); echo ":"; -return $next;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(signature.set_param_type( - 0, - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("runtime static callables should preserve by-ref target"); - - assert_eq!(values.output, "5:4:9:"); - assert_eq!(values.get(result), FakeValue::Int(4)); -} - -/// Verifies runtime AOT static method fallback rejects named arguments without metadata. -#[test] -fn execute_program_static_runtime_method_hook_rejects_unregistered_named_args() { - let program = parse_fragment( - br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let error = - execute_program(&program, &mut scope, &mut values).expect_err("named AOT call should fail"); - - assert_eq!(error, EvalStatus::UncaughtThrowable); -} - -/// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. -#[test] -fn execute_program_call_user_func_array_dispatches_declared_function() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } -return call_user_func_array("dyn", [4, 5]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(9)); -} -/// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. -#[test] -fn execute_program_call_user_func_array_binds_declared_named_args() { - let program = parse_fragment( - br#"function dyn($x, $y) { return ($x * 10) + $y; } -return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} -/// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. -#[test] -fn execute_context_function_call_array_binds_declared_named_args() { - let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - let arg_array = values.assoc_new(2).expect("allocate argument array"); - let key_y = values.string("y").expect("allocate y key"); - let value_y = values.int(2).expect("allocate y value"); - let _ = values - .array_set(arg_array, key_y, value_y) - .expect("store y argument"); - let key_x = values.string("x").expect("allocate x key"); - let value_x = values.int(1).expect("allocate x value"); - let _ = values - .array_set(arg_array, key_x, value_x) - .expect("store x argument"); - - let result = execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) - .expect("execute context function call array"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} -/// Verifies `call_user_func_array` rejects positional values after named keys. -#[test] -fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { - let program = parse_fragment( - br#"function dyn($x, $y) { return $x + $y; } -return call_user_func_array("dyn", ["y" => 2, 1]);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); -} -/// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. -#[test] -fn execute_program_call_user_func_array_dispatches_builtin() { - let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); -} -/// Verifies `call_user_func_array` releases literal callback and argument-array temporaries. -#[test] -fn execute_program_call_user_func_array_releases_literal_callback_and_arg_array() { - let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(4)); - assert!( - values - .releases - .iter() - .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), - "literal callback string should be released after dispatch" - ); - assert!( - values - .releases - .iter() - .any(|release| matches!(values.get(*release), FakeValue::Array(_))), - "literal call argument array should be released after dispatch" - ); -} -/// Verifies `call_user_func_array` releases literal temporaries after a fatal dispatch. -#[test] -fn execute_program_call_user_func_array_releases_literal_temporaries_after_fatal() { - let program = - parse_fragment(br#"return call_user_func_array("strlen", ["unknown" => "abcd"]);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - assert!( - values - .releases - .iter() - .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), - "literal callback string should be released after fatal dispatch" - ); - assert!( - values - .releases - .iter() - .any(|release| matches!(values.get(*release), FakeValue::Assoc(_))), - "literal call argument hash should be released after fatal dispatch" - ); -} -/// Verifies `call_user_func_array` releases literal callback when arg-array evaluation fails. -#[test] -fn execute_program_call_user_func_array_releases_literal_callback_after_arg_array_eval_fatal() { - let program = parse_fragment(br#"return call_user_func_array("strlen", MISSING_ARG_ARRAY);"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values); - - assert_eq!(result, Err(EvalStatus::RuntimeFatal)); - assert!( - values - .releases - .iter() - .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), - "literal callback string should be released when argument-array evaluation fails" - ); -} -/// Verifies `call_user_func_array` inside eval can dispatch a registered native function. -#[test] -fn execute_program_call_user_func_array_dispatches_registered_native_function() { - let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} -/// Verifies `call_user_func_array` named keys can bind registered native parameters. -#[test] -fn execute_program_call_user_func_array_binds_registered_native_named_args() { - let program = parse_fragment( - br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let expected = values.int(42).expect("allocate fake result"); - let mut native = - NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); - assert!(native.set_param_name(0, "left")); - assert!(native.set_param_name(1, "right")); - assert!(context - .define_native_function("native_answer", native) - .is_ok()); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("execute eval ir"); - - assert_eq!(result, expected); -} -/// Verifies duplicate eval-declared function names fail in a shared context. -#[test] -fn execute_program_rejects_duplicate_declared_function() { - let define = parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect("execute first declaration"); - let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) - .expect_err("duplicate function declaration should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func.rs new file mode 100644 index 0000000000..25baeeeac7 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func.rs @@ -0,0 +1,306 @@ +//! Purpose: +//! Interpreter tests for variable calls, `call_user_func`, callable arrays, and +//! temporary release on validation failures. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify callable dispatch across eval functions, builtins, native functions, and object methods. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies `call_user_func` inside eval can dispatch an eval-declared function. +#[test] +fn execute_program_call_user_func_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x) { return $x + 1; } +return call_user_func("dyn", 4);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies `call_user_func` inside eval can dispatch a supported builtin. +#[test] +fn execute_program_call_user_func_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); +} +/// Verifies `call_user_func` releases literal callback temporaries after dispatch. +#[test] +fn execute_program_call_user_func_releases_literal_callback_after_dispatch() { + let program = parse_fragment(br#"return call_user_func("strlen", "abcd");"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after dispatch" + ); +} + +/// Verifies `call_user_func` releases literal callback temporaries after dispatch fatal. +#[test] +fn execute_program_call_user_func_releases_literal_callback_after_dispatch_fatal() { + let program = + parse_fragment(br#"return call_user_func("strlen");"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after fatal dispatch" + ); +} + +/// Verifies `call_user_func` releases literal callback when later arg evaluation fails. +#[test] +fn execute_program_call_user_func_releases_literal_callback_after_arg_eval_fatal() { + let program = parse_fragment(br#"return call_user_func("strlen", MISSING_ARG);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released when argument evaluation fails" + ); +} + +/// Verifies `call_user_func` inside eval can dispatch a registered native function. +#[test] +fn execute_program_call_user_func_dispatches_registered_native_function() { + let program = + parse_fragment(br#"return call_user_func("native_answer");"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 0); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies string variable calls inside eval can dispatch a supported builtin. +#[test] +fn execute_program_variable_call_dispatches_builtin() { + let program = parse_fragment( + br#"$fn = "strlen"; +return $fn("abcd");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); +} +/// Verifies callable array entries can be invoked through postfix dynamic calls. +#[test] +fn execute_program_postfix_variable_call_dispatches_builtin() { + let program = parse_fragment( + br#"$callbacks = ["strlen"]; +return $callbacks[0]("abc");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(3)); +} +/// Verifies variable calls bind eval-declared function arguments by name. +#[test] +fn execute_program_variable_call_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } +$fn = "dyn"; +return $fn(y: 2, x: 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies variable calls can dispatch registered native functions with named args. +#[test] +fn execute_program_variable_call_binds_registered_native_named_args() { + let program = parse_fragment( + br#"$fn = "native_answer"; +return $fn(right: 2, left: 1);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies direct callable-array variable calls dispatch object methods. +#[test] +fn execute_program_callable_array_variable_dispatches_object_method() { + let program = parse_fragment( + br#"$box = new Box(41); +$cb = [$box, "add_x"]; +return $cb(1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + assert_released_array_callable_index_temps(&values); +} +/// Verifies `call_user_func` dispatches callable arrays with object receivers. +#[test] +fn execute_program_call_user_func_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(42); +$cb = [$box, "read_x"]; +return call_user_func($cb);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); + assert_released_array_callable_index_temps(&values); +} + +/// Verifies callable-array index temporaries are released when validation rejects the callback. +#[test] +fn execute_program_call_user_func_releases_array_callable_index_temps_after_validation_fatal() { + let program = parse_fragment( + br#"class EvalMissingArrayCallableMethod {} +$missing = new EvalMissingArrayCallableMethod(); +try { + call_user_func([$missing, "MiSsInG"]); + return false; +} catch (TypeError $e) { + return true; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); + assert_released_array_callable_index_temps(&values); +} +/// Verifies `call_user_func_array` dispatches callable arrays with positional args. +#[test] +fn execute_program_call_user_func_array_dispatches_object_method_array() { + let program = parse_fragment( + br#"$box = new Box(39); +return call_user_func_array([$box, "add2_x"], [1, 2]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); +} + +/// Verifies static method callable arrays dispatch eval-declared static methods. +#[test] +fn execute_program_static_callable_array_dispatches_eval_method() { + let program = parse_fragment( + br#"class EvalStaticCallableBox { + public static function join($left, $right) { + return $left . $right; + } +} +$cb = ["EvalStaticCallableBox", "join"]; +echo $cb(right: "B", left: "A"); echo ":"; +echo call_user_func($cb, "C", "D"); echo ":"; +echo call_user_func_array($cb, ["right" => "F", "left" => "E"]); echo ":"; +$named = "EvalStaticCallableBox::join"; +return $named(right: "H", left: "G");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:CD:EF:"); + assert_eq!(values.get(result), FakeValue::String("GH".to_string())); +} + +/// Verifies fake runtime releases include both temporary callable-array index cells. +fn assert_released_array_callable_index_temps(values: &FakeOps) { + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::Int(0)), + "array callable index 0 temporary should be released" + ); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::Int(1)), + "array callable index 1 temporary should be released" + ); +} diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func_array.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func_array.rs new file mode 100644 index 0000000000..5d0f249df8 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/call_user_func_array.rs @@ -0,0 +1,227 @@ +//! Purpose: +//! Interpreter tests for `call_user_func_array` dispatch, named argument +//! normalization, temporary cleanup, and duplicate declarations. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Eval, builtin, and registered native function targets share this surface. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies `call_user_func_array` inside eval can dispatch an eval-declared function. +#[test] +fn execute_program_call_user_func_array_dispatches_declared_function() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", [4, 5]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(9)); +} +/// Verifies `call_user_func_array` string keys bind eval-declared parameters by name. +#[test] +fn execute_program_call_user_func_array_binds_declared_named_args() { + let program = parse_fragment( + br#"function dyn($x, $y) { return ($x * 10) + $y; } +return call_user_func_array("dyn", ["y" => 2, "x" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies context-level `call_user_func_array` dispatch binds eval-declared named args. +#[test] +fn execute_context_function_call_array_binds_declared_named_args() { + let program = parse_fragment(br#"function dyn($x, $y) { return ($x * 10) + $y; }"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let _ = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + let arg_array = values.assoc_new(2).expect("allocate argument array"); + let key_y = values.string("y").expect("allocate y key"); + let value_y = values.int(2).expect("allocate y value"); + let _ = values + .array_set(arg_array, key_y, value_y) + .expect("store y argument"); + let key_x = values.string("x").expect("allocate x key"); + let value_x = values.int(1).expect("allocate x value"); + let _ = values + .array_set(arg_array, key_x, value_x) + .expect("store x argument"); + + let result = execute_context_function_call_array(&mut context, "dyn", arg_array, &mut values) + .expect("execute context function call array"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies `call_user_func_array` rejects positional values after named keys. +#[test] +fn execute_program_call_user_func_array_rejects_positional_after_named_arg() { + let program = parse_fragment( + br#"function dyn($x, $y) { return $x + $y; } +return call_user_func_array("dyn", ["y" => 2, 1]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); +} +/// Verifies `call_user_func_array` inside eval can dispatch a supported builtin. +#[test] +fn execute_program_call_user_func_array_dispatches_builtin() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); +} +/// Verifies `call_user_func_array` releases literal callback and argument-array temporaries. +#[test] +fn execute_program_call_user_func_array_releases_literal_callback_and_arg_array() { + let program = parse_fragment(br#"return call_user_func_array("strlen", ["abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(4)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after dispatch" + ); + assert!( + values + .releases + .iter() + .any(|release| matches!(values.get(*release), FakeValue::Array(_))), + "literal call argument array should be released after dispatch" + ); +} +/// Verifies `call_user_func_array` releases literal temporaries after a fatal dispatch. +#[test] +fn execute_program_call_user_func_array_releases_literal_temporaries_after_fatal() { + let program = + parse_fragment(br#"return call_user_func_array("strlen", ["unknown" => "abcd"]);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released after fatal dispatch" + ); + assert!( + values + .releases + .iter() + .any(|release| matches!(values.get(*release), FakeValue::Assoc(_))), + "literal call argument hash should be released after fatal dispatch" + ); +} +/// Verifies `call_user_func_array` releases literal callback when arg-array evaluation fails. +#[test] +fn execute_program_call_user_func_array_releases_literal_callback_after_arg_array_eval_fatal() { + let program = parse_fragment(br#"return call_user_func_array("strlen", MISSING_ARG_ARRAY);"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values); + + assert_eq!(result, Err(EvalStatus::RuntimeFatal)); + assert!( + values + .releases + .iter() + .any(|release| values.get(*release) == FakeValue::String("strlen".to_string())), + "literal callback string should be released when argument-array evaluation fails" + ); +} +/// Verifies `call_user_func_array` inside eval can dispatch a registered native function. +#[test] +fn execute_program_call_user_func_array_dispatches_registered_native_function() { + let program = parse_fragment(br#"return call_user_func_array("native_answer", [4, 5]);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let native = NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies `call_user_func_array` named keys can bind registered native parameters. +#[test] +fn execute_program_call_user_func_array_binds_registered_native_named_args() { + let program = parse_fragment( + br#"return call_user_func_array("native_answer", ["right" => 2, "left" => 1]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let expected = values.int(42).expect("allocate fake result"); + let mut native = + NativeFunction::new(expected.as_ptr().cast(), fake_native_return_descriptor, 2); + assert!(native.set_param_name(0, "left")); + assert!(native.set_param_name(1, "right")); + assert!(context + .define_native_function("native_answer", native) + .is_ok()); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(result, expected); +} +/// Verifies duplicate eval-declared function names fail in a shared context. +#[test] +fn execute_program_rejects_duplicate_declared_function() { + let define = parse_fragment(br#"function dyn() { return 1; }"#).expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect("execute first declaration"); + let err = execute_program_with_context(&mut context, &define, &mut scope, &mut values) + .expect_err("duplicate function declaration should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/first_class_objects.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/first_class_objects.rs new file mode 100644 index 0000000000..65fc26a687 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/first_class_objects.rs @@ -0,0 +1,340 @@ +//! Purpose: +//! Interpreter tests for first-class function/method callables, invokable eval +//! objects, magic fallback, and named object-method arguments. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Static called-class state and invalid object callables are covered. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies first-class callable syntax dispatches through eval's callback paths. +#[test] +fn execute_program_first_class_callables_dispatch_functions_and_methods() { + let program = parse_fragment( + br#"function eval_fc_double($value) { + return $value * 2; +} +class EvalFirstClassCallableBase { + public function __construct($offset = 1) { + $this->offset = $offset; + } + public function add($value) { + return $value + $this->offset; + } + public function keep($value) { + return $value > 2; + } + public function sum($carry, $value) { + return $carry + $value + $this->offset; + } + public function show($value, $key) { + echo $key . $value; + } + public static function join($left, $right) { + return $left . $right; + } + public static function compareDesc($left, $right) { + return $right - $left; + } + public static function label($value) { + return "base:" . $value; + } + public static function relay($value) { + $fn = static::label(...); + return $fn($value); + } +} +class EvalFirstClassCallableChild extends EvalFirstClassCallableBase { + public static function label($value) { + return "child:" . $value; + } +} +$function = eval_fc_double(...); +echo $function(4); echo ":"; +echo (strlen(...))("abcd"); echo ":"; +$box = new EvalFirstClassCallableBase(3); +$method = $box->add(...); +echo $method(4); echo ":"; +echo call_user_func($box->add(...), 5); echo ":"; +$static = EvalFirstClassCallableBase::join(...); +echo $static(right: "B", left: "A"); echo ":"; +$mapped = array_map($box->add(...), [1, 2]); +echo $mapped[0] . $mapped[1] . ":"; +$filtered = array_filter([1, 2, 3, 4], $box->keep(...)); +echo count($filtered) . ":"; +echo array_reduce([1, 2], $box->sum(...), 0) . ":"; +$walked = ["a" => 1]; +array_walk($walked, $box->show(...)); +echo ":"; +$sorted = [3, 1, 2]; +usort($sorted, EvalFirstClassCallableBase::compareDesc(...)); +echo $sorted[0] . $sorted[1] . $sorted[2] . ":"; +return EvalFirstClassCallableChild::relay("ok");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:4:7:8:AB:45:2:9:a1:321:"); + assert_eq!( + values.get(result), + FakeValue::String("child:ok".to_string()) + ); +} + +/// Verifies first-class static callables preserve late-static forwarding metadata. +#[test] +fn execute_program_first_class_static_callables_preserve_called_class() { + let program = parse_fragment( + br#"class EvalFirstClassStaticForwardBase { + public static function who() { + return static::tag(); + } + public static function tag() { + return "base"; + } + public static function relaySelf() { + $fn = self::who(...); + return $fn(); + } +} +class EvalFirstClassStaticForwardChild extends EvalFirstClassStaticForwardBase { + public static function relayParent() { + $fn = parent::who(...); + return $fn(); + } + public static function tag() { + return "child"; + } +} +echo EvalFirstClassStaticForwardChild::relayParent(); echo ":"; +return EvalFirstClassStaticForwardChild::relaySelf();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "child:"); + assert_eq!(values.get(result), FakeValue::String("child".to_string())); +} + +/// Verifies invokable eval objects dispatch through variable and callback call paths. +#[test] +fn execute_program_invokes_eval_object_callables() { + let program = parse_fragment( + br#"function eval_plain_call_side_effect() { + echo "bad"; + return "x"; +} +class EvalInvokableBox { + public function __construct($label = "box") { + $this->label = $label; + } + public function __invoke($left = "A", $right = "B") { + return $this->label . ":" . $left . $right; + } +} +class EvalPlainCallableProbe {} +$box = new EvalInvokableBox("box"); +$plain = new EvalPlainCallableProbe(); +echo is_callable($box) ? "Y:" : "N:"; +echo is_callable($plain) ? "bad:" : "plain:"; +echo $box(right: "D", left: "C"); echo ":"; +try { + $plain(eval_plain_call_side_effect()); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo ":"; +echo (new EvalInvokableBox("new"))("E", "F"); echo ":"; +echo call_user_func($box, "G", "H"); echo ":"; +$first = $box(...); +echo $first("K", "L"); echo ":"; +return call_user_func_array($box, ["right" => "J", "left" => "I"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Y:plain:box:CD:Error:Object of type EvalPlainCallableProbe is not callable:new:EF:box:GH:box:KL:" + ); + assert_eq!(values.get(result), FakeValue::String("box:IJ".to_string())); +} + +/// Verifies call_user_func rejects non-invokable eval objects with PHP's TypeError. +#[test] +fn execute_program_call_user_func_rejects_non_invokable_eval_object() { + let program = parse_fragment( + br#"class EvalPlainCallbackError {} +$plain = new EvalPlainCallbackError(); +try { + call_user_func($plain); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func_array($plain, []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, no array or string given|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, no array or string given" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies call_user_func rejects invalid object-method callable arrays with PHP's TypeError. +#[test] +fn execute_program_call_user_func_rejects_invalid_object_method_arrays() { + let program = parse_fragment( + br#"class EvalMissingCallbackArray {} +class EvalPrivateCallbackArray { + private function hidden() { + return "bad"; + } +} +class EvalPrivateInvokeCallbackArray { + private function __invoke() { + return "bad"; + } +} +class EvalInstanceCallbackArray { + public function inst() { + return "bad"; + } +} +$missing = new EvalMissingCallbackArray(); +try { + call_user_func([$missing, "MiSsInG"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func_array([$missing, "missing"], []); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func([new EvalPrivateCallbackArray(), "hidden"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func([new EvalPrivateInvokeCallbackArray(), "__invoke"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +echo "|"; +try { + call_user_func(["EvalInstanceCallbackArray", "inst"]); + echo "bad"; +} catch (TypeError $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"MiSsInG\"|\ +TypeError:call_user_func_array(): Argument #1 ($callback) must be a valid callback, class EvalMissingCallbackArray does not have a method \"missing\"|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateCallbackArray::hidden()|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, cannot access private method EvalPrivateInvokeCallbackArray::__invoke()|\ +TypeError:call_user_func(): Argument #1 ($callback) must be a valid callback, non-static method EvalInstanceCallbackArray::inst() cannot be called statically" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies call_user_func callable arrays still dispatch through magic method fallbacks. +#[test] +fn execute_program_call_user_func_arrays_dispatch_magic_method_fallbacks() { + let program = parse_fragment( + br#"class EvalMagicCallbackArray { + public function __call($method, $args) { + return $method . ":" . $args[0]; + } + public static function __callStatic($method, $args) { + return $method . ":" . $args[0]; + } +} +$box = new EvalMagicCallbackArray(); +echo is_callable([$box, "missing"]) ? "Y:" : "N:"; +echo call_user_func([$box, "missing"], "A") . ":"; +echo call_user_func_array([$box, "missing"], ["B"]) . ":"; +echo is_callable(["EvalMagicCallbackArray", "static_missing"]) ? "S:" : "s:"; +return call_user_func(["EvalMagicCallbackArray", "static_missing"], "C");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:missing:A:missing:B:S:"); + assert_eq!( + values.get(result), + FakeValue::String("static_missing:C".to_string()) + ); +} + +/// Verifies object-method callable arrays preserve eval named-argument binding. +#[test] +fn execute_program_object_method_callable_array_binds_eval_named_args() { + let program = parse_fragment( + br#"class EvalObjectCallableArrayBox { + public function join($left, $right) { + return $left . $right; + } +} +$box = new EvalObjectCallableArrayBox(); +$cb = [$box, "join"]; +echo is_callable($cb) ? "Y:" : "N:"; +return call_user_func_array($cb, ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "Y:"); + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/mod.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/mod.rs new file mode 100644 index 0000000000..e45aee5113 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/mod.rs @@ -0,0 +1,14 @@ +//! Purpose: +//! Organizes interpreter tests for dynamic callable dispatch by callable form +//! and runtime bridge surface. +//! +//! Called from: +//! - `crate::interpreter::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test and helper function name. + +mod call_user_func; +mod call_user_func_array; +mod first_class_objects; +mod runtime_callables; diff --git a/crates/elephc-magician/src/interpreter/tests/dynamic_calls/runtime_callables.rs b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/runtime_callables.rs new file mode 100644 index 0000000000..4f267853ac --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/dynamic_calls/runtime_callables.rs @@ -0,0 +1,229 @@ +//! Purpose: +//! Interpreter tests for runtime AOT static/method callable hooks, named/default +//! arguments, by-reference constraints, and writeback. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Runtime bridge failures and coercion writeback are asserted separately. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies static calls fall back to runtime AOT hooks when no eval class matches. +#[test] +fn execute_program_static_call_dispatches_runtime_method_hook() { + let program = parse_fragment( + br#"echo KnownClass::join("A", "B"); echo ":"; +$cb = ["KnownClass", "join"]; +echo call_user_func($cb, "C", "D"); echo ":"; +$named = "KnownClass::join"; +echo $named("E", "F"); echo ":"; +return call_user_func_array(["KnownClass", "sum"], [2, 5]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut join_signature = NativeCallableSignature::new(2); + assert!(join_signature.set_param_name(0, "left")); + assert!(join_signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature( + "KnownClass", + "join", + join_signature + )); + let mut sum_signature = NativeCallableSignature::new(2); + assert!(sum_signature.set_param_name(0, "left")); + assert!(sum_signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature( + "KnownClass", + "sum", + sum_signature + )); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("execute eval ir"); + + assert_eq!(values.output, "AB:CD:EF:"); + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies runtime AOT static method fallback binds registered named arguments. +#[test] +fn execute_program_static_runtime_method_hook_binds_named_args() { + let program = parse_fragment( + br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(context.define_native_static_method_signature("KnownClass", "join", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered named AOT call should bind"); + + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} + +/// Verifies runtime AOT static method fallback fills registered scalar defaults. +#[test] +fn execute_program_static_runtime_method_hook_binds_default_args() { + let program = parse_fragment( + br#"echo KnownClass::join("A"); echo ":"; +return call_user_func_array(["KnownClass", "join"], ["left" => "C"]);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_default(1, NativeCallableDefault::String("B".to_string()))); + assert!(context.define_native_static_method_signature("KnownClass", "join", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered AOT defaults should bind"); + + assert_eq!(values.output, "AB:"); + assert_eq!(values.get(result), FakeValue::String("CB".to_string())); +} + +/// Verifies runtime AOT static method fallback honors by-reference parameter metadata. +#[test] +fn execute_program_static_runtime_method_hook_rejects_by_ref_temporary_arg() { + let program = parse_fragment(br#"return KnownClass::sum(1, 2);"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a static by-reference method parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies callable-array AOT method dispatch preserves by-reference writeback. +#[test] +fn execute_program_callable_array_runtime_method_writes_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$cb = [$box, "add2_x"]; +$value = "3"; +echo $cb($value, 2); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("callable-array runtime method should preserve by-ref target"); + + assert_eq!(values.output, "15"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies `call_user_func_array()` preserves by-reference array elements for AOT methods. +#[test] +fn execute_program_call_user_func_array_runtime_method_writes_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$cb = [$box, "add2_x"]; +$value = "3"; +echo call_user_func_array($cb, [&$value, 2]); +echo ":"; +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("call_user_func_array should preserve by-ref array element target"); + + assert_eq!(values.output, "15:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies string and first-class AOT static callables preserve by-reference writeback. +#[test] +fn execute_program_static_runtime_callables_write_back_by_ref_type_coercion() { + let program = parse_fragment( + br#"$string = "KnownClass::sum"; +$left = "3"; +echo $string($left, 2); echo ":"; +echo $left + 1; echo ":"; +$first = KnownClass::sum(...); +$next = "4"; +echo $first($next, 5); echo ":"; +return $next;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("runtime static callables should preserve by-ref target"); + + assert_eq!(values.output, "5:4:9:"); + assert_eq!(values.get(result), FakeValue::Int(4)); +} + +/// Verifies runtime AOT static method fallback rejects named arguments without metadata. +#[test] +fn execute_program_static_runtime_method_hook_rejects_unregistered_named_args() { + let program = parse_fragment( + br#"return call_user_func_array(["KnownClass", "join"], ["right" => "B", "left" => "A"]);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let error = + execute_program(&program, &mut scope, &mut values).expect_err("named AOT call should fail"); + + assert_eq!(error, EvalStatus::UncaughtThrowable); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions.rs b/crates/elephc-magician/src/interpreter/tests/expressions.rs deleted file mode 100644 index d1aaf8aa6a..0000000000 --- a/crates/elephc-magician/src/interpreter/tests/expressions.rs +++ /dev/null @@ -1,1584 +0,0 @@ -//! Purpose: -//! Interpreter tests for scalar expressions, echo/print, objects, and construction. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - These cases assert EvalIR expression execution against fake runtime values. - -use super::super::*; -use super::support::*; - -/// Verifies simple variable compound assignments read, compute, and write the scope value. -#[test] -fn execute_program_evaluates_compound_assignments() { - let program = - parse_fragment(br#"$x = 2; $x += 3; $x *= 4; $x -= 5; $s = "v"; $s .= $x; echo $s;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.output, "v15"); - assert_eq!(values.get(x), FakeValue::Int(15)); -} -/// Verifies division and modulo evaluate through fake runtime numeric hooks. -#[test] -fn execute_program_evaluates_division_and_modulo() { - let program = parse_fragment(br#"$x = 20; $x /= 2; $x %= 6; echo $x; return 9 / 2;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.output, "4"); - assert_eq!(values.get(x), FakeValue::Int(4)); - assert_eq!(values.get(result), FakeValue::Float(4.5)); -} -/// Verifies exponentiation evaluates through fake runtime numeric hooks. -#[test] -fn execute_program_evaluates_exponentiation() { - let program = parse_fragment( - br#"$x = 2; $x **= 3; echo $x; echo ":"; echo -2 ** 2; return 2 ** 3 ** 2;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let x = scope.visible_cell("x").expect("scope should contain x"); - - assert_eq!(values.output, "8:-4"); - assert_eq!(values.get(x), FakeValue::Float(8.0)); - assert_eq!(values.get(result), FakeValue::Float(512.0)); -} -/// Verifies bitwise and shift operators evaluate through fake runtime hooks. -#[test] -fn execute_program_evaluates_bitwise_and_shift_ops() { - let program = parse_fragment( - br#"$x = 6; $x &= 3; echo $x; echo ":"; -$x = 4; $x |= 1; echo $x; echo ":"; -$x = 7; $x ^= 3; echo $x; echo ":"; -$x = 1; $x <<= 5; echo $x; echo ":"; -$x = 64; $x >>= 3; echo $x; echo ":"; -echo ~0; echo ":"; echo -16 >> 2; -return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "2:5:4:32:8:-1:-4"); - assert_eq!(values.get(result), FakeValue::Int(21)); -} -/// Verifies simple variable increment and decrement statements update the scope value. -#[test] -fn execute_program_evaluates_inc_dec_statements() { - let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let i = scope.visible_cell("i").expect("scope should contain i"); - - assert_eq!(values.output, "1"); - assert_eq!(values.get(i), FakeValue::Int(1)); -} -/// Verifies echo and unset operate through runtime hooks and scope metadata. -#[test] -fn execute_program_echoes_and_unsets_scope_value() { - let program = - parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let name = values.string(" Ada").expect("create fake string"); - scope.set("name", name, ScopeCellOwnership::Owned); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "hi Ada"); - assert_eq!(values.get(result), FakeValue::Null); - assert!(scope.entry("name").expect("unset marker").flags().unset); -} -/// Verifies comma-separated echo expressions are executed in source order. -#[test] -fn execute_program_echoes_comma_list() { - let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let b = values.string("b").expect("create fake string"); - scope.set("b", b, ScopeCellOwnership::Owned); - - let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "abc"); -} -/// Verifies print writes output and returns integer 1. -#[test] -fn execute_program_print_returns_one() { - let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "p"); - assert_eq!(values.get(result), FakeValue::Int(1)); -} -/// Verifies eval property reads and writes dispatch through runtime hooks. -#[test] -fn execute_program_reads_and_writes_object_property() { - let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(1).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); - assert_eq!( - values - .property_get(object, "x") - .map(|value| values.get(value)) - .expect("property should be readable"), - FakeValue::Int(2) - ); -} -/// Verifies eval method calls dispatch through the runtime method hook. -#[test] -fn execute_program_calls_object_method() { - let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(42)); -} -/// Verifies eval method calls forward evaluated arguments to the runtime hook. -#[test] -fn execute_program_calls_object_method_with_argument() { - let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(12)); -} -/// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. -#[test] -fn execute_program_calls_object_method_with_two_arguments() { - let program = parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(18)); -} -/// Verifies eval method calls forward numerically unpacked arguments. -#[test] -fn execute_program_calls_object_method_with_spread_arguments() { - let program = - parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let x = values.int(7).expect("create fake int"); - let properties = vec![("x".to_string(), x)]; - let object = values.alloc(FakeValue::Object(properties)); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(18)); -} -/// Verifies eval object construction dispatches through runtime hooks. -#[test] -fn execute_program_constructs_named_object() { - let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Object(Vec::new())); -} -/// Verifies eval object construction passes constructor arguments through runtime hooks. -#[test] -fn execute_program_constructs_named_object_with_args() { - let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - let FakeValue::Object(properties) = values.get(result) else { - panic!("expected fake object"); - }; - let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); - - assert_eq!(values.get(x), FakeValue::Int(1)); -} - -/// Verifies eval object construction binds registered AOT constructor named arguments. -#[test] -fn execute_program_constructs_named_object_with_registered_named_args() { - let program = parse_fragment(br#"$box = new KnownClass(value: 9); return $box->read_x();"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(1); - assert!(signature.set_param_name(0, "value")); - assert!(context.define_native_constructor_signature("KnownClass", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("registered constructor named args should bind"); - - assert_eq!(values.get(result), FakeValue::Int(9)); -} - -/// Verifies runtime/AOT constructor fallback honors by-reference parameter metadata. -#[test] -fn execute_program_rejects_runtime_constructor_by_ref_temporary_arg() { - let program = parse_fragment(br#"$box = new KnownClass(9); return $box->read_x();"#) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(1); - assert!(signature.set_param_name(0, "value")); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_constructor_signature("KnownClass", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect_err("literal cannot satisfy a constructor by-reference parameter"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies runtime/AOT constructor fallback writes coerced by-reference args back. -#[test] -fn execute_program_writes_back_runtime_constructor_by_ref_type_coercion() { - let program = parse_fragment( - br#"$value = "9"; -$box = new KnownClass($value); -echo $box->read_x(); -return $value;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(1); - assert!(signature.set_param_name(0, "value")); - assert!(signature.set_param_type( - 0, - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_constructor_signature("KnownClass", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("registered constructor by-ref coercion should bind"); - - assert_eq!(values.output, "9"); - assert_eq!(values.get(result), FakeValue::Int(9)); -} - -/// Verifies AOT constructor by-reference writeback still runs when construction fatals. -#[test] -fn execute_program_writes_back_runtime_constructor_by_ref_before_fatal() { - let program = parse_fragment( - br#"$value = "9"; -new KnownFailingConstructor($value);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(1); - assert!(signature.set_param_name(0, "value")); - assert!(signature.set_param_type( - 0, - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_constructor_signature("KnownFailingConstructor", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect_err("failing constructor should abort after argument binding"); - let value = scope - .entry("value") - .expect("caller variable should remain visible") - .cell(); - - assert_eq!(err, EvalStatus::RuntimeFatal); - assert_eq!(values.get(value), FakeValue::Int(9)); -} - -/// Verifies eval-declared classes create objects with properties and methods. -#[test] -fn execute_program_constructs_eval_declared_class_with_method() { - let program = parse_fragment( - br#"class DynBox { - public int $x = 1; - public function __construct($x) { $this->x = $x; } - public function bump($n) { $this->x = $this->x + $n; return $this->x; } -} -$box = new DynBox(4); -echo get_class($box); -echo ":"; -echo $box->bump(3); -echo ":"; -echo is_a($box, "DynBox") ? "Y" : "N"; -$call = [$box, "bump"]; -echo call_user_func($call, 1); -echo ":"; -echo call_user_func_array($call, [2]); -echo ":"; -return $box->x;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "DynBox:7:Y8:10:"); - assert_eq!(values.get(result), FakeValue::Int(10)); -} -/// Verifies eval-declared classes inherit properties, methods, and constructors. -#[test] -fn execute_program_constructs_eval_declared_class_with_inheritance() { - let program = parse_fragment( - br#"class EvalBaseBox { - public int $base = 1; - public function __construct($base) { $this->base = $base; } - public function sum($n) { return $this->base + $this->tail + $n; } -} -class EvalChildBox extends EvalBaseBox implements KnownInterface { - public int $tail = 4; - public function read($n) { return $this->sum($n); } -} -$box = new EvalChildBox(3); -echo $box->read(5); echo ":"; -echo get_parent_class($box); echo ":"; -echo is_a($box, "EvalBaseBox") ? "isa" : "bad"; echo ":"; -echo is_a($box, "KnownInterface") ? "iface" : "bad"; echo ":"; -echo is_subclass_of($box, "EvalChildBox") ? "bad" : "self"; echo ":"; -echo is_subclass_of($box, "EvalBaseBox") ? "sub" : "bad"; echo ":"; -$parents = class_parents($box); -echo count($parents); echo ":"; -echo $parents["EvalBaseBox"]; -return $box->base;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "12:EvalBaseBox:isa:iface:self:sub:1:EvalBaseBox" - ); - assert_eq!(values.get(result), FakeValue::Int(3)); -} - -/// Verifies eval `instanceof` uses eval class, interface, and dynamic-target metadata. -#[test] -fn execute_program_evaluates_eval_instanceof_targets() { - let program = parse_fragment( - br#"interface EvalInstanceIface {} -class EvalInstanceBase {} -class EvalInstanceChild extends EvalInstanceBase implements EvalInstanceIface {} -class EvalInstanceOther {} -$box = new EvalInstanceChild(); -$class = "EvalInstanceChild"; -$target = ["EvalInstanceIface"]; -$prefix = "EvalInstance"; -$suffix = "Base"; -$targetObject = new EvalInstanceChild(); -echo $box instanceof EvalInstanceChild ? "C" : "c"; -echo $box instanceof EvalInstanceBase ? "B" : "b"; -echo $box instanceof EvalInstanceIface ? "I" : "i"; -echo $box instanceof $class ? "D" : "d"; -echo $box instanceof $target[0] ? "A" : "a"; -echo $box instanceof ($prefix . $suffix) ? "P" : "p"; -echo $box instanceof $targetObject ? "O" : "o"; -echo 7 instanceof MissingEvalClass ? "bad" : "S"; -return $box instanceof EvalInstanceOther;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "CBIDAPOS"); - assert_eq!(values.get(result), FakeValue::Bool(false)); -} - -/// Verifies dynamic `instanceof` rejects targets that are not strings or objects. -#[test] -fn execute_program_rejects_invalid_dynamic_instanceof_target() { - let program = parse_fragment( - br#"class EvalInvalidInstanceTarget {} -$box = new EvalInvalidInstanceTarget(); -$target = 42; -return $box instanceof $target;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("invalid instanceof target should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval-declared classes can implement eval-declared interfaces. -#[test] -fn execute_program_constructs_eval_declared_class_with_dynamic_interface() { - let program = parse_fragment( - br#"interface EvalReader { - function read($n); -} -interface EvalNamedReader extends EvalReader { - function label(); -} -class EvalReaderBox implements EvalNamedReader { - public function read($n) { return $n + 1; } - public function label() { return "box"; } -} -$box = new EvalReaderBox(); -echo $box->read(4); echo ":"; -echo $box->label(); echo ":"; -echo is_a($box, "EvalNamedReader") ? "isa" : "bad"; echo ":"; -echo is_subclass_of($box, "EvalReader") ? "sub" : "bad"; echo ":"; -echo is_subclass_of("EvalReaderBox", "EvalReader") ? "str" : "bad"; echo ":"; -$implements = class_implements($box); -echo count($implements); echo ":"; -echo $implements["EvalNamedReader"]; echo ":"; -echo $implements["EvalReader"]; -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "5:box:isa:sub:str:2:EvalNamedReader:EvalReader" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} -/// Verifies concrete eval classes can implement abstract class and interface contracts. -#[test] -fn execute_program_constructs_concrete_child_from_abstract_eval_class() { - let program = parse_fragment( - br#"interface EvalAbstractReadable { - function read($n); -} -abstract class EvalAbstractBase implements EvalAbstractReadable { - abstract public function read($n); - public function wrap($n) { return $this->read($n) + 1; } -} -class EvalConcreteBox extends EvalAbstractBase { - public function read($n) { return $n + 3; } -} -$box = new EvalConcreteBox(); -echo $box->wrap(4); echo ":"; -echo is_a($box, "EvalAbstractReadable") ? "iface" : "bad"; echo ":"; -echo is_subclass_of($box, "EvalAbstractBase") ? "abstract" : "bad"; -return $box->read(2);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "8:iface:abstract"); - assert_eq!(values.get(result), FakeValue::Int(5)); -} -/// Verifies eval rejects instantiation of abstract eval-declared classes. -#[test] -fn execute_program_rejects_abstract_eval_class_instantiation() { - let program = parse_fragment( - br#"abstract class EvalAbstractOnly { - public function read() { return 1; } -} -new EvalAbstractOnly();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("abstract class instantiation should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} -/// Verifies concrete eval classes must implement inherited abstract methods. -#[test] -fn execute_program_rejects_concrete_eval_class_with_abstract_methods() { - let program = parse_fragment( - br#"abstract class EvalNeedsRead { - abstract public function read(); -} -class EvalMissingReadChild extends EvalNeedsRead {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("concrete class missing abstract method should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} -/// Verifies eval rejects extending a final eval-declared class. -#[test] -fn execute_program_rejects_extending_final_eval_class() { - let program = parse_fragment( - br#"final class EvalFinalBase {} -class EvalFinalChild extends EvalFinalBase {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("extending final class should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} -/// Verifies eval rejects overriding a final eval-declared method. -#[test] -fn execute_program_rejects_overriding_final_eval_method() { - let program = parse_fragment( - br#"class EvalFinalMethodBase { - final public function read() { return 1; } -} -class EvalFinalMethodChild extends EvalFinalMethodBase { - public function read() { return 2; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("overriding final method should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval rejects overriding a final eval-declared property. -#[test] -fn execute_program_rejects_overriding_final_eval_property() { - let program = parse_fragment( - br#"class EvalFinalPropertyBase { - final public $value = 1; -} -class EvalFinalPropertyChild extends EvalFinalPropertyBase { - public $value = 2; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("overriding final property should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval-declared traits contribute methods, properties, and metadata. -#[test] -fn execute_program_constructs_class_using_eval_declared_trait() { - let program = parse_fragment( - br#"trait EvalReusableTrait { - public int $seed = 2; - public function add($n) { return $this->seed + $n; } -} -class EvalTraitBox { - use EvalReusableTrait; - public function read($n) { return $this->add($n) + 1; } -} -$box = new EvalTraitBox(); -echo $box->read(4); echo ":"; -echo trait_exists("EvalReusableTrait") ? "trait" : "bad"; echo ":"; -$traits = get_declared_traits(); -echo count($traits); echo ":"; echo $traits[0]; echo ":"; -$uses = class_uses($box); -echo count($uses); echo ":"; echo $uses["EvalReusableTrait"]; -return $box->seed;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "7:trait:1:EvalReusableTrait:1:EvalReusableTrait" - ); - assert_eq!(values.get(result), FakeValue::Int(2)); -} -/// Verifies eval trait abstract methods can be implemented by the using class. -#[test] -fn execute_program_constructs_class_satisfying_eval_trait_abstract_method() { - let program = parse_fragment( - br#"trait EvalTraitNeedsRead { - abstract public function read($n); - public function wrap($n) { return $this->read($n) + 1; } -} -class EvalTraitReader { - use EvalTraitNeedsRead; - public function read($n) { return $n + 4; } -} -$reader = new EvalTraitReader(); -return $reader->wrap(3);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(8)); -} -/// Verifies eval rejects a concrete class that leaves a trait abstract method open. -#[test] -fn execute_program_rejects_missing_eval_trait_abstract_method() { - let program = parse_fragment( - br#"trait EvalTraitAbstractMethod { - abstract public function read(); -} -class EvalTraitMissingRead { - use EvalTraitAbstractMethod; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("class missing trait abstract method should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} -/// Verifies eval rejects classes using traits that are not eval-declared. -#[test] -fn execute_program_rejects_missing_eval_trait_use() { - let program = parse_fragment( - br#"class EvalTraitMissingUse { - use MissingEvalTraitUse; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("missing eval trait use should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} -/// Verifies eval methods can access private properties and methods declared in their class. -#[test] -fn execute_program_allows_private_eval_members_inside_declaring_class() { - let program = parse_fragment( - br#"class EvalPrivateBox { - private int $secret = 4; - private function bump($n) { return $this->secret + $n; } - public function read($n) { return $this->bump($n); } -} -$box = new EvalPrivateBox(); -return $box->read(3);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); -} -/// Verifies protected eval members are accessible across a class hierarchy. -#[test] -fn execute_program_allows_protected_eval_members_from_related_classes() { - let program = parse_fragment( - br#"class EvalProtectedBase { - protected int $base = 5; - protected function add($n) { return $this->base + $n; } -} -class EvalProtectedChild extends EvalProtectedBase { - public function read($n) { return $this->add($n); } -} -$box = new EvalProtectedChild(); -return $box->read(2);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies eval child properties shadow private parent properties with a separate storage slot. -#[test] -fn execute_program_shadows_private_eval_parent_property_with_separate_slot() { - let program = parse_fragment( - br#"class EvalPrivateShadowBase { - private $value = 1; - - public function parentValue() { - return $this->value; - } -} -class EvalPrivateShadowChild extends EvalPrivateShadowBase { - public $value = "child"; - - public function childValue() { - return $this->value; - } -} -$box = new EvalPrivateShadowChild(); -echo $box->parentValue(); echo ":"; -echo $box->childValue(); echo ":"; -echo $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:child:child"); -} - -/// Verifies eval later redeclarations update the visible slot while preserving a private grandparent slot. -#[test] -fn execute_program_keeps_eval_private_grandparent_slot_after_later_redeclaration() { - let program = parse_fragment( - br#"class EvalPrivateGrandBase { - private $value = 1; - - public function grandValue() { - return $this->value; - } -} -class EvalPrivateGrandParent extends EvalPrivateGrandBase { - public $value = 2; - - public function parentValue() { - return $this->value; - } -} -class EvalPrivateGrandChild extends EvalPrivateGrandParent { - public $value = 3; -} -$box = new EvalPrivateGrandChild(); -echo $box->grandValue(); echo ":"; -echo $box->parentValue(); echo ":"; -echo $box->value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "1:3:3"); -} - -/// Verifies eval throws Error for private property access from global scope. -#[test] -fn execute_program_private_eval_member_access_from_global_scope_throws_error() { - let program = parse_fragment( - br#"class EvalPrivateGlobalBox { - private int $secret = 4; - private function read() { return $this->secret; } -} -$box = new EvalPrivateGlobalBox(); -try { - echo $box->secret; - echo "bad"; -} catch (Error $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Cannot access private property EvalPrivateGlobalBox::$secret" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} -/// Verifies eval throws Error for calls to private methods from global scope. -#[test] -fn execute_program_private_eval_method_call_from_global_scope_throws_error() { - let program = parse_fragment( - br#"class EvalPrivateMethodBox { - private function read() { return 4; } -} -$box = new EvalPrivateMethodBox(); -try { - echo $box->read(); - echo "bad"; -} catch (Error $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Call to private method EvalPrivateMethodBox::read() from global scope" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies missing eval-declared instance methods throw PHP-compatible Error values. -#[test] -fn execute_program_missing_eval_method_call_throws_error() { - let program = parse_fragment( - br#"class EvalMissingMethodBox {} -$box = new EvalMissingMethodBox(); -try { - echo $box->missing(); - echo "bad"; -} catch (Error $e) { - echo get_class($e) . ":" . $e->getMessage(); -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "Error:Call to undefined method EvalMissingMethodBox::missing()" - ); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval rejects overriding a public method with lower visibility. -#[test] -fn execute_program_rejects_method_override_with_reduced_visibility() { - let program = parse_fragment( - br#"class EvalVisibleBase { - public function read() { return 1; } -} -class EvalVisibleChild extends EvalVisibleBase { - protected function read() { return 2; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("reduced method visibility should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval rejects parent method overrides that require more arguments. -#[test] -fn execute_program_rejects_method_override_with_narrower_arity() { - let program = parse_fragment( - br#"class EvalArityBase { - public function read($value = "base") { return $value; } -} -class EvalArityChild extends EvalArityBase { - public function read($value) { return $value; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("narrower method override arity should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval accepts PHP-contravariant method parameter type overrides. -#[test] -fn execute_program_accepts_contravariant_method_parameter_type_overrides() { - let program = parse_fragment( - br#"class EvalParamBase { - public function anyInt(int $value) { return $value; } - public function maybeInt(int $value) { return $value; } - public function untypedInt(int $value) { return $value; } -} -class EvalParamChild extends EvalParamBase { - public function anyInt(mixed $value) { return $value . ":mixed"; } - public function maybeInt(?int $value) { return $value; } - public function untypedInt($value) { return $value; } -} -$child = new EvalParamChild(); -echo $child->anyInt(7); echo ":"; -echo $child->untypedInt("ok"); -return $child->maybeInt(null) === null;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "7:mixed:ok"); - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval rejects method parameter overrides that narrow PHP's accepted type set. -#[test] -fn execute_program_rejects_incompatible_method_parameter_type_overrides() { - let incompatible_type = parse_fragment( - br#"class EvalParamTypeBase { - public function read(int $value) { return $value; } -} -class EvalParamStringChild extends EvalParamTypeBase { - public function read(string $value) { return $value; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&incompatible_type, &mut scope, &mut values) - .expect_err("incompatible parameter override type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let narrower_nullable = parse_fragment( - br#"class EvalParamNullableBase { - public function maybe(?int $value) { return $value; } -} -class EvalParamNonNullChild extends EvalParamNullableBase { - public function maybe(int $value) { return $value; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&narrower_nullable, &mut scope, &mut values) - .expect_err("narrower nullable parameter override type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let untyped_to_typed = parse_fragment( - br#"class EvalParamUntypedBase { - public function read($value) { return $value; } -} -class EvalParamTypedChild extends EvalParamUntypedBase { - public function read(int $value) { return $value; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&untyped_to_typed, &mut scope, &mut values) - .expect_err("typed parameter override of untyped parent should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval accepts covariant method return type overrides. -#[test] -fn execute_program_accepts_covariant_method_return_type_overrides() { - let program = parse_fragment( - br#"class EvalReturnBase { - public function id(): ?int { return 1; } - public function make(): EvalReturnBase { return $this; } - public function selfType(): self { return $this; } -} -class EvalReturnChild extends EvalReturnBase { - public function id(): int { return 2; } - public function make(): EvalReturnChild { return $this; } - public function selfType(): static { return $this; } -} -class EvalReturnParentRoot {} -class EvalReturnParentBase extends EvalReturnParentRoot { - public function parentKeyword(): EvalReturnParentRoot { return new EvalReturnParentRoot(); } -} -class EvalReturnParentChild extends EvalReturnParentBase { - public function parentKeyword(): parent { return new EvalReturnParentBase(); } -} -class EvalReturnMixedBase { - public function maybe(): mixed { return null; } -} -class EvalReturnMixedChild extends EvalReturnMixedBase { - public function maybe(): ?int { return null; } -} -$child = new EvalReturnChild(); -return $child->id();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(2)); -} - -/// Verifies eval rejects method overrides that widen declared return types. -#[test] -fn execute_program_rejects_incompatible_method_return_type_overrides() { - let wider_nullable = parse_fragment( - br#"class EvalReturnNarrowBase { - public function id(): int { return 1; } -} -class EvalReturnWiderNullable extends EvalReturnNarrowBase { - public function id(): ?int { return 2; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&wider_nullable, &mut scope, &mut values) - .expect_err("wider nullable return type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let missing_return = parse_fragment( - br#"class EvalReturnRequiredBase { - public function label(): string { return "base"; } -} -class EvalReturnMissingChild extends EvalReturnRequiredBase { - public function label() { return "child"; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&missing_return, &mut scope, &mut values) - .expect_err("missing return type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let static_to_self = parse_fragment( - br#"class EvalReturnStaticBase { - public function make(): static { return $this; } -} -class EvalReturnSelfChild extends EvalReturnStaticBase { - public function make(): self { return $this; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&static_to_self, &mut scope, &mut values) - .expect_err("static return type should not widen to self"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let nullable_to_mixed = parse_fragment( - br#"class EvalReturnNullableBase { - public function maybe(): ?int { return null; } -} -class EvalReturnMixedChildBad extends EvalReturnNullableBase { - public function maybe(): mixed { return null; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&nullable_to_mixed, &mut scope, &mut values) - .expect_err("mixed return type should widen nullable int"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval enforces declared method return values at runtime. -#[test] -fn execute_program_enforces_eval_method_return_type_values() { - let program = parse_fragment( - br#"class EvalReturnRuntimeBase { - public function id(): int { return "12"; } - public function makeSelf(): self { return new EvalReturnRuntimeBase(); } - public function done(): void { return; } -} -class EvalReturnRuntimeChild extends EvalReturnRuntimeBase {} -$child = new EvalReturnRuntimeChild(); -echo $child->id(); echo ":"; -echo get_class($child->makeSelf()); echo ":"; -$child->done(); -return 3;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "12:EvalReturnRuntimeBase:"); - assert_eq!(values.get(result), FakeValue::Int(3)); -} - -/// Verifies eval rejects method return values that do not satisfy declarations. -#[test] -fn execute_program_rejects_invalid_eval_method_return_type_values() { - let bad_scalar = parse_fragment( - br#"class EvalReturnBadScalar { - public function id(): int { return "nope"; } -} -$box = new EvalReturnBadScalar(); -return $box->id();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_scalar, &mut scope, &mut values) - .expect_err("non-numeric string should fail int return type"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let bad_void = parse_fragment( - br#"class EvalReturnBadVoid { - public function done(): void { return null; } -} -$box = new EvalReturnBadVoid(); -return $box->done();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_void, &mut scope, &mut values) - .expect_err("explicit value should fail void return type"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let bad_static = parse_fragment( - br#"class EvalReturnStaticRuntimeBase { - public function make(): static { return new EvalReturnStaticRuntimeBase(); } -} -class EvalReturnStaticRuntimeChild extends EvalReturnStaticRuntimeBase {} -$child = new EvalReturnStaticRuntimeChild(); -return $child->make();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_static, &mut scope, &mut values) - .expect_err("base instance should fail inherited static return type"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let implicit_return = parse_fragment( - br#"class EvalReturnImplicitBad { - public function id(): ?int {} -} -$box = new EvalReturnImplicitBad(); -return $box->id();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&implicit_return, &mut scope, &mut values) - .expect_err("implicit return should fail non-void return type"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval rejects classes missing methods required by eval interfaces. -#[test] -fn execute_program_rejects_missing_dynamic_interface_method() { - let program = parse_fragment( - br#"interface EvalNeedsRead { - function read($n); -} -class EvalMissingRead implements EvalNeedsRead {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("missing interface method should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval accepts covariant return types for interface method contracts. -#[test] -fn execute_program_accepts_covariant_interface_method_return_type() { - let program = parse_fragment( - br#"interface EvalReturnReadable { - function read(): int|string; -} -class EvalReturnReader implements EvalReturnReadable { - public function read(): int { - return 7; - } -} -interface EvalReturnRootSelf { - function linked(): self; -} -interface EvalReturnChildSelf extends EvalReturnRootSelf {} -class EvalReturnSelfImpl implements EvalReturnChildSelf { - public function linked(): EvalReturnRootSelf { - return $this; - } -} -$reader = new EvalReturnReader(); -return $reader->read();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies eval rejects missing or wider return types for interface method contracts. -#[test] -fn execute_program_rejects_incompatible_interface_method_return_type() { - let missing_return = parse_fragment( - br#"interface EvalNeedsReturn { - function read(): string; -} -class EvalMissingReturnImpl implements EvalNeedsReturn { - public function read() { return "bad"; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&missing_return, &mut scope, &mut values) - .expect_err("missing interface return type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let wider_return = parse_fragment( - br#"interface EvalNeedsStringReturn { - function read(): string; -} -class EvalWiderReturnImpl implements EvalNeedsStringReturn { - public function read(): int|string { return "bad"; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&wider_return, &mut scope, &mut values) - .expect_err("wider interface return type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies abstract eval classes must keep declared interface method signatures compatible. -#[test] -fn execute_program_rejects_incompatible_abstract_interface_method_declarations() { - let bad_abstract_param = parse_fragment( - br#"interface EvalAbstractIfaceParam { - function read(int $value); -} -abstract class EvalAbstractIfaceParamBase implements EvalAbstractIfaceParam { - abstract public function read(string $value); -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_abstract_param, &mut scope, &mut values) - .expect_err("abstract interface method parameter type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let bad_abstract_return = parse_fragment( - br#"interface EvalAbstractIfaceReturn { - function read(): int; -} -abstract class EvalAbstractIfaceReturnBase implements EvalAbstractIfaceReturn { - abstract public function read(): string; -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_abstract_return, &mut scope, &mut values) - .expect_err("abstract interface method return type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let bad_inherited_method = parse_fragment( - br#"interface EvalInheritedIfaceMethod { - function read(int $value); -} -abstract class EvalInheritedIfaceMethodBase { - public function read(string $value) {} -} -abstract class EvalInheritedIfaceMethodChild extends EvalInheritedIfaceMethodBase implements EvalInheritedIfaceMethod {}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_inherited_method, &mut scope, &mut values) - .expect_err("inherited incompatible interface method should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies abstract eval classes may defer missing compatible interface methods. -#[test] -fn execute_program_accepts_deferred_abstract_interface_method_declarations() { - let program = parse_fragment( - br#"interface EvalAbstractIfaceDeferred { - function read(int $value): int; -} -abstract class EvalAbstractIfaceDeferredBase implements EvalAbstractIfaceDeferred {} -abstract class EvalAbstractIfaceDeferredTyped implements EvalAbstractIfaceDeferred { - abstract public function read(mixed $value): int; -} -return true;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Bool(true)); -} - -/// Verifies eval accepts PHP-contravariant parameter types for interface contracts. -#[test] -fn execute_program_accepts_contravariant_interface_method_parameter_types() { - let program = parse_fragment( - br#"interface EvalParamContract { - function read(int $value); -} -class EvalParamContractReader implements EvalParamContract { - public function read(mixed $value) { - return $value . ":ok"; - } -} -$reader = new EvalParamContractReader(); -return $reader->read(8);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("8:ok".to_string())); -} - -/// Verifies eval rejects interface implementations with incompatible parameter types. -#[test] -fn execute_program_rejects_incompatible_interface_method_parameter_types() { - let incompatible_type = parse_fragment( - br#"interface EvalParamStringContract { - function read(int $value); -} -class EvalParamStringReader implements EvalParamStringContract { - public function read(string $value) { return $value; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&incompatible_type, &mut scope, &mut values) - .expect_err("incompatible interface parameter type should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let untyped_to_typed = parse_fragment( - br#"interface EvalParamUntypedContract { - function read($value); -} -class EvalParamTypedReader implements EvalParamUntypedContract { - public function read(int $value) { return $value; } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&untyped_to_typed, &mut scope, &mut values) - .expect_err("typed parameter implementation of untyped contract should fail"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval static interface method contracts are satisfied by public static methods. -#[test] -fn execute_program_accepts_static_dynamic_interface_method() { - let program = parse_fragment( - br#"interface EvalNeedsStaticRead { - public static function read($n); -} -class EvalStaticReader implements EvalNeedsStaticRead { - public static function read($n) { - return $n . "!"; - } -} -return EvalStaticReader::read("ok");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("ok!".to_string())); -} - -/// Verifies eval rejects instance methods for static interface method contracts. -#[test] -fn execute_program_rejects_instance_method_for_static_dynamic_interface_method() { - let program = parse_fragment( - br#"interface EvalNeedsStaticRead { - public static function read(); -} -class EvalInstanceReader implements EvalNeedsStaticRead { - public function read() {} -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("instance method should not satisfy static interface method"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval interface method contracts require matching by-reference parameters. -#[test] -fn execute_program_validates_interface_method_by_ref_parameters() { - let program = parse_fragment( - br#"interface EvalRefReadable { - function read(&$value); -} -class EvalRefReader implements EvalRefReadable { - public function read(&$value) { - $value = "ok"; - } -} -$value = "bad"; -$reader = new EvalRefReader(); -$reader->read($value); -return $value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("ok".to_string())); - - let bad_value_impl = parse_fragment( - br#"interface EvalNeedsByRef { - function read(&$value); -} -class EvalByValueReader implements EvalNeedsByRef { - public function read($value) {} -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_value_impl, &mut scope, &mut values) - .expect_err("by-value implementation must not satisfy by-reference contract"); - assert_eq!(err, EvalStatus::RuntimeFatal); - - let bad_ref_impl = parse_fragment( - br#"interface EvalNeedsByValue { - function read($value); -} -class EvalByRefReader implements EvalNeedsByValue { - public function read(&$value) {} -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&bad_ref_impl, &mut scope, &mut values) - .expect_err("by-reference implementation must not satisfy by-value contract"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies variadic eval methods can satisfy fixed-arity interface contracts. -#[test] -fn execute_program_accepts_variadic_method_for_fixed_interface_contract() { - let program = parse_fragment( - br#"interface EvalFixedReadable { - function read($left, $right); -} -class EvalVariadicReadable implements EvalFixedReadable { - public function read($left, ...$tail) { - return $left . $tail[0]; - } -} -$box = new EvalVariadicReadable(); -return $box->read("A", "B");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::String("AB".to_string())); -} - -/// Verifies non-variadic eval methods cannot satisfy variadic interface contracts. -#[test] -fn execute_program_rejects_non_variadic_method_for_variadic_interface_contract() { - let program = parse_fragment( - br#"interface EvalVariadicReadable { - function read($left, ...$tail); -} -class EvalFixedReadable implements EvalVariadicReadable { - public function read($left, $tail = null) { - return $left; - } -}"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("non-variadic implementation should not satisfy variadic contract"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/classes_traits.rs b/crates/elephc-magician/src/interpreter/tests/expressions/classes_traits.rs new file mode 100644 index 0000000000..b27a973a99 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/classes_traits.rs @@ -0,0 +1,389 @@ +//! Purpose: +//! Interpreter tests for eval class construction, inheritance, abstract/final +//! rules, dynamic interfaces, and traits. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Class declaration validation and construction behavior are exercised together. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared classes create objects with properties and methods. +#[test] +fn execute_program_constructs_eval_declared_class_with_method() { + let program = parse_fragment( + br#"class DynBox { + public int $x = 1; + public function __construct($x) { $this->x = $x; } + public function bump($n) { $this->x = $this->x + $n; return $this->x; } +} +$box = new DynBox(4); +echo get_class($box); +echo ":"; +echo $box->bump(3); +echo ":"; +echo is_a($box, "DynBox") ? "Y" : "N"; +$call = [$box, "bump"]; +echo call_user_func($call, 1); +echo ":"; +echo call_user_func_array($call, [2]); +echo ":"; +return $box->x;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "DynBox:7:Y8:10:"); + assert_eq!(values.get(result), FakeValue::Int(10)); +} +/// Verifies eval-declared classes inherit properties, methods, and constructors. +#[test] +fn execute_program_constructs_eval_declared_class_with_inheritance() { + let program = parse_fragment( + br#"class EvalBaseBox { + public int $base = 1; + public function __construct($base) { $this->base = $base; } + public function sum($n) { return $this->base + $this->tail + $n; } +} +class EvalChildBox extends EvalBaseBox implements KnownInterface { + public int $tail = 4; + public function read($n) { return $this->sum($n); } +} +$box = new EvalChildBox(3); +echo $box->read(5); echo ":"; +echo get_parent_class($box); echo ":"; +echo is_a($box, "EvalBaseBox") ? "isa" : "bad"; echo ":"; +echo is_a($box, "KnownInterface") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalChildBox") ? "bad" : "self"; echo ":"; +echo is_subclass_of($box, "EvalBaseBox") ? "sub" : "bad"; echo ":"; +$parents = class_parents($box); +echo count($parents); echo ":"; +echo $parents["EvalBaseBox"]; +return $box->base;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "12:EvalBaseBox:isa:iface:self:sub:1:EvalBaseBox" + ); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies eval `instanceof` uses eval class, interface, and dynamic-target metadata. +#[test] +fn execute_program_evaluates_eval_instanceof_targets() { + let program = parse_fragment( + br#"interface EvalInstanceIface {} +class EvalInstanceBase {} +class EvalInstanceChild extends EvalInstanceBase implements EvalInstanceIface {} +class EvalInstanceOther {} +$box = new EvalInstanceChild(); +$class = "EvalInstanceChild"; +$target = ["EvalInstanceIface"]; +$prefix = "EvalInstance"; +$suffix = "Base"; +$targetObject = new EvalInstanceChild(); +echo $box instanceof EvalInstanceChild ? "C" : "c"; +echo $box instanceof EvalInstanceBase ? "B" : "b"; +echo $box instanceof EvalInstanceIface ? "I" : "i"; +echo $box instanceof $class ? "D" : "d"; +echo $box instanceof $target[0] ? "A" : "a"; +echo $box instanceof ($prefix . $suffix) ? "P" : "p"; +echo $box instanceof $targetObject ? "O" : "o"; +echo 7 instanceof MissingEvalClass ? "bad" : "S"; +return $box instanceof EvalInstanceOther;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "CBIDAPOS"); + assert_eq!(values.get(result), FakeValue::Bool(false)); +} + +/// Verifies dynamic `instanceof` rejects targets that are not strings or objects. +#[test] +fn execute_program_rejects_invalid_dynamic_instanceof_target() { + let program = parse_fragment( + br#"class EvalInvalidInstanceTarget {} +$box = new EvalInvalidInstanceTarget(); +$target = 42; +return $box instanceof $target;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("invalid instanceof target should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared classes can implement eval-declared interfaces. +#[test] +fn execute_program_constructs_eval_declared_class_with_dynamic_interface() { + let program = parse_fragment( + br#"interface EvalReader { + function read($n); +} +interface EvalNamedReader extends EvalReader { + function label(); +} +class EvalReaderBox implements EvalNamedReader { + public function read($n) { return $n + 1; } + public function label() { return "box"; } +} +$box = new EvalReaderBox(); +echo $box->read(4); echo ":"; +echo $box->label(); echo ":"; +echo is_a($box, "EvalNamedReader") ? "isa" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalReader") ? "sub" : "bad"; echo ":"; +echo is_subclass_of("EvalReaderBox", "EvalReader") ? "str" : "bad"; echo ":"; +$implements = class_implements($box); +echo count($implements); echo ":"; +echo $implements["EvalNamedReader"]; echo ":"; +echo $implements["EvalReader"]; +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "5:box:isa:sub:str:2:EvalNamedReader:EvalReader" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies concrete eval classes can implement abstract class and interface contracts. +#[test] +fn execute_program_constructs_concrete_child_from_abstract_eval_class() { + let program = parse_fragment( + br#"interface EvalAbstractReadable { + function read($n); +} +abstract class EvalAbstractBase implements EvalAbstractReadable { + abstract public function read($n); + public function wrap($n) { return $this->read($n) + 1; } +} +class EvalConcreteBox extends EvalAbstractBase { + public function read($n) { return $n + 3; } +} +$box = new EvalConcreteBox(); +echo $box->wrap(4); echo ":"; +echo is_a($box, "EvalAbstractReadable") ? "iface" : "bad"; echo ":"; +echo is_subclass_of($box, "EvalAbstractBase") ? "abstract" : "bad"; +return $box->read(2);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:iface:abstract"); + assert_eq!(values.get(result), FakeValue::Int(5)); +} +/// Verifies eval rejects instantiation of abstract eval-declared classes. +#[test] +fn execute_program_rejects_abstract_eval_class_instantiation() { + let program = parse_fragment( + br#"abstract class EvalAbstractOnly { + public function read() { return 1; } +} +new EvalAbstractOnly();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("abstract class instantiation should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies concrete eval classes must implement inherited abstract methods. +#[test] +fn execute_program_rejects_concrete_eval_class_with_abstract_methods() { + let program = parse_fragment( + br#"abstract class EvalNeedsRead { + abstract public function read(); +} +class EvalMissingReadChild extends EvalNeedsRead {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("concrete class missing abstract method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects extending a final eval-declared class. +#[test] +fn execute_program_rejects_extending_final_eval_class() { + let program = parse_fragment( + br#"final class EvalFinalBase {} +class EvalFinalChild extends EvalFinalBase {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("extending final class should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects overriding a final eval-declared method. +#[test] +fn execute_program_rejects_overriding_final_eval_method() { + let program = parse_fragment( + br#"class EvalFinalMethodBase { + final public function read() { return 1; } +} +class EvalFinalMethodChild extends EvalFinalMethodBase { + public function read() { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval rejects overriding a final eval-declared property. +#[test] +fn execute_program_rejects_overriding_final_eval_property() { + let program = parse_fragment( + br#"class EvalFinalPropertyBase { + final public $value = 1; +} +class EvalFinalPropertyChild extends EvalFinalPropertyBase { + public $value = 2; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("overriding final property should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared traits contribute methods, properties, and metadata. +#[test] +fn execute_program_constructs_class_using_eval_declared_trait() { + let program = parse_fragment( + br#"trait EvalReusableTrait { + public int $seed = 2; + public function add($n) { return $this->seed + $n; } +} +class EvalTraitBox { + use EvalReusableTrait; + public function read($n) { return $this->add($n) + 1; } +} +$box = new EvalTraitBox(); +echo $box->read(4); echo ":"; +echo trait_exists("EvalReusableTrait") ? "trait" : "bad"; echo ":"; +$traits = get_declared_traits(); +echo count($traits); echo ":"; echo $traits[0]; echo ":"; +$uses = class_uses($box); +echo count($uses); echo ":"; echo $uses["EvalReusableTrait"]; +return $box->seed;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "7:trait:1:EvalReusableTrait:1:EvalReusableTrait" + ); + assert_eq!(values.get(result), FakeValue::Int(2)); +} +/// Verifies eval trait abstract methods can be implemented by the using class. +#[test] +fn execute_program_constructs_class_satisfying_eval_trait_abstract_method() { + let program = parse_fragment( + br#"trait EvalTraitNeedsRead { + abstract public function read($n); + public function wrap($n) { return $this->read($n) + 1; } +} +class EvalTraitReader { + use EvalTraitNeedsRead; + public function read($n) { return $n + 4; } +} +$reader = new EvalTraitReader(); +return $reader->wrap(3);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(8)); +} +/// Verifies eval rejects a concrete class that leaves a trait abstract method open. +#[test] +fn execute_program_rejects_missing_eval_trait_abstract_method() { + let program = parse_fragment( + br#"trait EvalTraitAbstractMethod { + abstract public function read(); +} +class EvalTraitMissingRead { + use EvalTraitAbstractMethod; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("class missing trait abstract method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} +/// Verifies eval rejects classes using traits that are not eval-declared. +#[test] +fn execute_program_rejects_missing_eval_trait_use() { + let program = parse_fragment( + br#"class EvalTraitMissingUse { + use MissingEvalTraitUse; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing eval trait use should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/interface_contracts.rs b/crates/elephc-magician/src/interpreter/tests/expressions/interface_contracts.rs new file mode 100644 index 0000000000..c846fa562e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/interface_contracts.rs @@ -0,0 +1,375 @@ +//! Purpose: +//! Interpreter tests for eval interface method presence, variance, staticness, +//! by-reference parameters, and variadics. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Concrete and deferred abstract implementations are checked independently. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval rejects classes missing methods required by eval interfaces. +#[test] +fn execute_program_rejects_missing_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsRead { + function read($n); +} +class EvalMissingRead implements EvalNeedsRead {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("missing interface method should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval accepts covariant return types for interface method contracts. +#[test] +fn execute_program_accepts_covariant_interface_method_return_type() { + let program = parse_fragment( + br#"interface EvalReturnReadable { + function read(): int|string; +} +class EvalReturnReader implements EvalReturnReadable { + public function read(): int { + return 7; + } +} +interface EvalReturnRootSelf { + function linked(): self; +} +interface EvalReturnChildSelf extends EvalReturnRootSelf {} +class EvalReturnSelfImpl implements EvalReturnChildSelf { + public function linked(): EvalReturnRootSelf { + return $this; + } +} +$reader = new EvalReturnReader(); +return $reader->read();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies eval rejects missing or wider return types for interface method contracts. +#[test] +fn execute_program_rejects_incompatible_interface_method_return_type() { + let missing_return = parse_fragment( + br#"interface EvalNeedsReturn { + function read(): string; +} +class EvalMissingReturnImpl implements EvalNeedsReturn { + public function read() { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&missing_return, &mut scope, &mut values) + .expect_err("missing interface return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let wider_return = parse_fragment( + br#"interface EvalNeedsStringReturn { + function read(): string; +} +class EvalWiderReturnImpl implements EvalNeedsStringReturn { + public function read(): int|string { return "bad"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&wider_return, &mut scope, &mut values) + .expect_err("wider interface return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract eval classes must keep declared interface method signatures compatible. +#[test] +fn execute_program_rejects_incompatible_abstract_interface_method_declarations() { + let bad_abstract_param = parse_fragment( + br#"interface EvalAbstractIfaceParam { + function read(int $value); +} +abstract class EvalAbstractIfaceParamBase implements EvalAbstractIfaceParam { + abstract public function read(string $value); +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_param, &mut scope, &mut values) + .expect_err("abstract interface method parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_abstract_return = parse_fragment( + br#"interface EvalAbstractIfaceReturn { + function read(): int; +} +abstract class EvalAbstractIfaceReturnBase implements EvalAbstractIfaceReturn { + abstract public function read(): string; +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_abstract_return, &mut scope, &mut values) + .expect_err("abstract interface method return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_inherited_method = parse_fragment( + br#"interface EvalInheritedIfaceMethod { + function read(int $value); +} +abstract class EvalInheritedIfaceMethodBase { + public function read(string $value) {} +} +abstract class EvalInheritedIfaceMethodChild extends EvalInheritedIfaceMethodBase implements EvalInheritedIfaceMethod {}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_inherited_method, &mut scope, &mut values) + .expect_err("inherited incompatible interface method should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies abstract eval classes may defer missing compatible interface methods. +#[test] +fn execute_program_accepts_deferred_abstract_interface_method_declarations() { + let program = parse_fragment( + br#"interface EvalAbstractIfaceDeferred { + function read(int $value): int; +} +abstract class EvalAbstractIfaceDeferredBase implements EvalAbstractIfaceDeferred {} +abstract class EvalAbstractIfaceDeferredTyped implements EvalAbstractIfaceDeferred { + abstract public function read(mixed $value): int; +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval accepts PHP-contravariant parameter types for interface contracts. +#[test] +fn execute_program_accepts_contravariant_interface_method_parameter_types() { + let program = parse_fragment( + br#"interface EvalParamContract { + function read(int $value); +} +class EvalParamContractReader implements EvalParamContract { + public function read(mixed $value) { + return $value . ":ok"; + } +} +$reader = new EvalParamContractReader(); +return $reader->read(8);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("8:ok".to_string())); +} + +/// Verifies eval rejects interface implementations with incompatible parameter types. +#[test] +fn execute_program_rejects_incompatible_interface_method_parameter_types() { + let incompatible_type = parse_fragment( + br#"interface EvalParamStringContract { + function read(int $value); +} +class EvalParamStringReader implements EvalParamStringContract { + public function read(string $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible interface parameter type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let untyped_to_typed = parse_fragment( + br#"interface EvalParamUntypedContract { + function read($value); +} +class EvalParamTypedReader implements EvalParamUntypedContract { + public function read(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&untyped_to_typed, &mut scope, &mut values) + .expect_err("typed parameter implementation of untyped contract should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval static interface method contracts are satisfied by public static methods. +#[test] +fn execute_program_accepts_static_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsStaticRead { + public static function read($n); +} +class EvalStaticReader implements EvalNeedsStaticRead { + public static function read($n) { + return $n . "!"; + } +} +return EvalStaticReader::read("ok");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok!".to_string())); +} + +/// Verifies eval rejects instance methods for static interface method contracts. +#[test] +fn execute_program_rejects_instance_method_for_static_dynamic_interface_method() { + let program = parse_fragment( + br#"interface EvalNeedsStaticRead { + public static function read(); +} +class EvalInstanceReader implements EvalNeedsStaticRead { + public function read() {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("instance method should not satisfy static interface method"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval interface method contracts require matching by-reference parameters. +#[test] +fn execute_program_validates_interface_method_by_ref_parameters() { + let program = parse_fragment( + br#"interface EvalRefReadable { + function read(&$value); +} +class EvalRefReader implements EvalRefReadable { + public function read(&$value) { + $value = "ok"; + } +} +$value = "bad"; +$reader = new EvalRefReader(); +$reader->read($value); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("ok".to_string())); + + let bad_value_impl = parse_fragment( + br#"interface EvalNeedsByRef { + function read(&$value); +} +class EvalByValueReader implements EvalNeedsByRef { + public function read($value) {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_value_impl, &mut scope, &mut values) + .expect_err("by-value implementation must not satisfy by-reference contract"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_ref_impl = parse_fragment( + br#"interface EvalNeedsByValue { + function read($value); +} +class EvalByRefReader implements EvalNeedsByValue { + public function read(&$value) {} +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_ref_impl, &mut scope, &mut values) + .expect_err("by-reference implementation must not satisfy by-value contract"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies variadic eval methods can satisfy fixed-arity interface contracts. +#[test] +fn execute_program_accepts_variadic_method_for_fixed_interface_contract() { + let program = parse_fragment( + br#"interface EvalFixedReadable { + function read($left, $right); +} +class EvalVariadicReadable implements EvalFixedReadable { + public function read($left, ...$tail) { + return $left . $tail[0]; + } +} +$box = new EvalVariadicReadable(); +return $box->read("A", "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::String("AB".to_string())); +} + +/// Verifies non-variadic eval methods cannot satisfy variadic interface contracts. +#[test] +fn execute_program_rejects_non_variadic_method_for_variadic_interface_contract() { + let program = parse_fragment( + br#"interface EvalVariadicReadable { + function read($left, ...$tail); +} +class EvalFixedReadable implements EvalVariadicReadable { + public function read($left, $tail = null) { + return $left; + } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("non-variadic implementation should not satisfy variadic contract"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/method_contracts.rs b/crates/elephc-magician/src/interpreter/tests/expressions/method_contracts.rs new file mode 100644 index 0000000000..69df12b452 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/method_contracts.rs @@ -0,0 +1,322 @@ +//! Purpose: +//! Interpreter tests for method override visibility, arity, variance, and return +//! type enforcement. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Both declaration compatibility and runtime return values are validated. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval rejects overriding a public method with lower visibility. +#[test] +fn execute_program_rejects_method_override_with_reduced_visibility() { + let program = parse_fragment( + br#"class EvalVisibleBase { + public function read() { return 1; } +} +class EvalVisibleChild extends EvalVisibleBase { + protected function read() { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("reduced method visibility should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval rejects parent method overrides that require more arguments. +#[test] +fn execute_program_rejects_method_override_with_narrower_arity() { + let program = parse_fragment( + br#"class EvalArityBase { + public function read($value = "base") { return $value; } +} +class EvalArityChild extends EvalArityBase { + public function read($value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("narrower method override arity should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval accepts PHP-contravariant method parameter type overrides. +#[test] +fn execute_program_accepts_contravariant_method_parameter_type_overrides() { + let program = parse_fragment( + br#"class EvalParamBase { + public function anyInt(int $value) { return $value; } + public function maybeInt(int $value) { return $value; } + public function untypedInt(int $value) { return $value; } +} +class EvalParamChild extends EvalParamBase { + public function anyInt(mixed $value) { return $value . ":mixed"; } + public function maybeInt(?int $value) { return $value; } + public function untypedInt($value) { return $value; } +} +$child = new EvalParamChild(); +echo $child->anyInt(7); echo ":"; +echo $child->untypedInt("ok"); +return $child->maybeInt(null) === null;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "7:mixed:ok"); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies eval rejects method parameter overrides that narrow PHP's accepted type set. +#[test] +fn execute_program_rejects_incompatible_method_parameter_type_overrides() { + let incompatible_type = parse_fragment( + br#"class EvalParamTypeBase { + public function read(int $value) { return $value; } +} +class EvalParamStringChild extends EvalParamTypeBase { + public function read(string $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&incompatible_type, &mut scope, &mut values) + .expect_err("incompatible parameter override type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let narrower_nullable = parse_fragment( + br#"class EvalParamNullableBase { + public function maybe(?int $value) { return $value; } +} +class EvalParamNonNullChild extends EvalParamNullableBase { + public function maybe(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&narrower_nullable, &mut scope, &mut values) + .expect_err("narrower nullable parameter override type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let untyped_to_typed = parse_fragment( + br#"class EvalParamUntypedBase { + public function read($value) { return $value; } +} +class EvalParamTypedChild extends EvalParamUntypedBase { + public function read(int $value) { return $value; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&untyped_to_typed, &mut scope, &mut values) + .expect_err("typed parameter override of untyped parent should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval accepts covariant method return type overrides. +#[test] +fn execute_program_accepts_covariant_method_return_type_overrides() { + let program = parse_fragment( + br#"class EvalReturnBase { + public function id(): ?int { return 1; } + public function make(): EvalReturnBase { return $this; } + public function selfType(): self { return $this; } +} +class EvalReturnChild extends EvalReturnBase { + public function id(): int { return 2; } + public function make(): EvalReturnChild { return $this; } + public function selfType(): static { return $this; } +} +class EvalReturnParentRoot {} +class EvalReturnParentBase extends EvalReturnParentRoot { + public function parentKeyword(): EvalReturnParentRoot { return new EvalReturnParentRoot(); } +} +class EvalReturnParentChild extends EvalReturnParentBase { + public function parentKeyword(): parent { return new EvalReturnParentBase(); } +} +class EvalReturnMixedBase { + public function maybe(): mixed { return null; } +} +class EvalReturnMixedChild extends EvalReturnMixedBase { + public function maybe(): ?int { return null; } +} +$child = new EvalReturnChild(); +return $child->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); +} + +/// Verifies eval rejects method overrides that widen declared return types. +#[test] +fn execute_program_rejects_incompatible_method_return_type_overrides() { + let wider_nullable = parse_fragment( + br#"class EvalReturnNarrowBase { + public function id(): int { return 1; } +} +class EvalReturnWiderNullable extends EvalReturnNarrowBase { + public function id(): ?int { return 2; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&wider_nullable, &mut scope, &mut values) + .expect_err("wider nullable return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let missing_return = parse_fragment( + br#"class EvalReturnRequiredBase { + public function label(): string { return "base"; } +} +class EvalReturnMissingChild extends EvalReturnRequiredBase { + public function label() { return "child"; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&missing_return, &mut scope, &mut values) + .expect_err("missing return type should fail"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let static_to_self = parse_fragment( + br#"class EvalReturnStaticBase { + public function make(): static { return $this; } +} +class EvalReturnSelfChild extends EvalReturnStaticBase { + public function make(): self { return $this; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&static_to_self, &mut scope, &mut values) + .expect_err("static return type should not widen to self"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let nullable_to_mixed = parse_fragment( + br#"class EvalReturnNullableBase { + public function maybe(): ?int { return null; } +} +class EvalReturnMixedChildBad extends EvalReturnNullableBase { + public function maybe(): mixed { return null; } +}"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&nullable_to_mixed, &mut scope, &mut values) + .expect_err("mixed return type should widen nullable int"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval enforces declared method return values at runtime. +#[test] +fn execute_program_enforces_eval_method_return_type_values() { + let program = parse_fragment( + br#"class EvalReturnRuntimeBase { + public function id(): int { return "12"; } + public function makeSelf(): self { return new EvalReturnRuntimeBase(); } + public function done(): void { return; } +} +class EvalReturnRuntimeChild extends EvalReturnRuntimeBase {} +$child = new EvalReturnRuntimeChild(); +echo $child->id(); echo ":"; +echo get_class($child->makeSelf()); echo ":"; +$child->done(); +return 3;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "12:EvalReturnRuntimeBase:"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies eval rejects method return values that do not satisfy declarations. +#[test] +fn execute_program_rejects_invalid_eval_method_return_type_values() { + let bad_scalar = parse_fragment( + br#"class EvalReturnBadScalar { + public function id(): int { return "nope"; } +} +$box = new EvalReturnBadScalar(); +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_scalar, &mut scope, &mut values) + .expect_err("non-numeric string should fail int return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_void = parse_fragment( + br#"class EvalReturnBadVoid { + public function done(): void { return null; } +} +$box = new EvalReturnBadVoid(); +return $box->done();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_void, &mut scope, &mut values) + .expect_err("explicit value should fail void return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let bad_static = parse_fragment( + br#"class EvalReturnStaticRuntimeBase { + public function make(): static { return new EvalReturnStaticRuntimeBase(); } +} +class EvalReturnStaticRuntimeChild extends EvalReturnStaticRuntimeBase {} +$child = new EvalReturnStaticRuntimeChild(); +return $child->make();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&bad_static, &mut scope, &mut values) + .expect_err("base instance should fail inherited static return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); + + let implicit_return = parse_fragment( + br#"class EvalReturnImplicitBad { + public function id(): ?int {} +} +$box = new EvalReturnImplicitBad(); +return $box->id();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&implicit_return, &mut scope, &mut values) + .expect_err("implicit return should fail non-void return type"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/mod.rs b/crates/elephc-magician/src/interpreter/tests/expressions/mod.rs new file mode 100644 index 0000000000..6217ca6c2f --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/mod.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Organizes interpreter expression tests by scalar/object execution, class +//! behavior, visibility, and callable contracts. +//! +//! Called from: +//! - `crate::interpreter::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod classes_traits; +mod interface_contracts; +mod method_contracts; +mod scalars_objects; +mod visibility; diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/scalars_objects.rs b/crates/elephc-magician/src/interpreter/tests/expressions/scalars_objects.rs new file mode 100644 index 0000000000..6326553828 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/scalars_objects.rs @@ -0,0 +1,341 @@ +//! Purpose: +//! Interpreter tests for scalar expressions, echo/print, object calls, and +//! runtime constructor argument handling. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases assert EvalIR expression execution against fake runtime values. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies simple variable compound assignments read, compute, and write the scope value. +#[test] +fn execute_program_evaluates_compound_assignments() { + let program = + parse_fragment(br#"$x = 2; $x += 3; $x *= 4; $x -= 5; $s = "v"; $s .= $x; echo $s;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "v15"); + assert_eq!(values.get(x), FakeValue::Int(15)); +} +/// Verifies division and modulo evaluate through fake runtime numeric hooks. +#[test] +fn execute_program_evaluates_division_and_modulo() { + let program = parse_fragment(br#"$x = 20; $x /= 2; $x %= 6; echo $x; return 9 / 2;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "4"); + assert_eq!(values.get(x), FakeValue::Int(4)); + assert_eq!(values.get(result), FakeValue::Float(4.5)); +} +/// Verifies exponentiation evaluates through fake runtime numeric hooks. +#[test] +fn execute_program_evaluates_exponentiation() { + let program = parse_fragment( + br#"$x = 2; $x **= 3; echo $x; echo ":"; echo -2 ** 2; return 2 ** 3 ** 2;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let x = scope.visible_cell("x").expect("scope should contain x"); + + assert_eq!(values.output, "8:-4"); + assert_eq!(values.get(x), FakeValue::Float(8.0)); + assert_eq!(values.get(result), FakeValue::Float(512.0)); +} +/// Verifies bitwise and shift operators evaluate through fake runtime hooks. +#[test] +fn execute_program_evaluates_bitwise_and_shift_ops() { + let program = parse_fragment( + br#"$x = 6; $x &= 3; echo $x; echo ":"; +$x = 4; $x |= 1; echo $x; echo ":"; +$x = 7; $x ^= 3; echo $x; echo ":"; +$x = 1; $x <<= 5; echo $x; echo ":"; +$x = 64; $x >>= 3; echo $x; echo ":"; +echo ~0; echo ":"; echo -16 >> 2; +return (1 << 4) | ((16 >> 2) ^ (3 & 1));"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "2:5:4:32:8:-1:-4"); + assert_eq!(values.get(result), FakeValue::Int(21)); +} +/// Verifies simple variable increment and decrement statements update the scope value. +#[test] +fn execute_program_evaluates_inc_dec_statements() { + let program = parse_fragment(br#"$i = 1; $i++; ++$i; $i--; --$i; echo $i;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let i = scope.visible_cell("i").expect("scope should contain i"); + + assert_eq!(values.output, "1"); + assert_eq!(values.get(i), FakeValue::Int(1)); +} +/// Verifies echo and unset operate through runtime hooks and scope metadata. +#[test] +fn execute_program_echoes_and_unsets_scope_value() { + let program = + parse_fragment(br#"echo "hi" . $name; unset($name);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let name = values.string(" Ada").expect("create fake string"); + scope.set("name", name, ScopeCellOwnership::Owned); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "hi Ada"); + assert_eq!(values.get(result), FakeValue::Null); + assert!(scope.entry("name").expect("unset marker").flags().unset); +} +/// Verifies comma-separated echo expressions are executed in source order. +#[test] +fn execute_program_echoes_comma_list() { + let program = parse_fragment(br#"echo "a", $b, "c";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let b = values.string("b").expect("create fake string"); + scope.set("b", b, ScopeCellOwnership::Owned); + + let _ = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "abc"); +} +/// Verifies print writes output and returns integer 1. +#[test] +fn execute_program_print_returns_one() { + let program = parse_fragment(br#"return print "p";"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "p"); + assert_eq!(values.get(result), FakeValue::Int(1)); +} +/// Verifies eval property reads and writes dispatch through runtime hooks. +#[test] +fn execute_program_reads_and_writes_object_property() { + let program = parse_fragment(br#"$this->x = $this->x + 1; return $this->x;"#) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(1).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(2)); + assert_eq!( + values + .property_get(object, "x") + .map(|value| values.get(value)) + .expect("property should be readable"), + FakeValue::Int(2) + ); +} +/// Verifies eval method calls dispatch through the runtime method hook. +#[test] +fn execute_program_calls_object_method() { + let program = parse_fragment(br#"return $this->answer();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(42)); +} +/// Verifies eval method calls forward evaluated arguments to the runtime hook. +#[test] +fn execute_program_calls_object_method_with_argument() { + let program = parse_fragment(br#"return $this->add_x(5);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(12)); +} +/// Verifies eval method calls forward multiple evaluated arguments to the runtime hook. +#[test] +fn execute_program_calls_object_method_with_two_arguments() { + let program = parse_fragment(br#"return $this->add2_x(5, 6);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); +} +/// Verifies eval method calls forward numerically unpacked arguments. +#[test] +fn execute_program_calls_object_method_with_spread_arguments() { + let program = + parse_fragment(br#"return $this->add2_x(...[5, 6]);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let x = values.int(7).expect("create fake int"); + let properties = vec![("x".to_string(), x)]; + let object = values.alloc(FakeValue::Object(properties)); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(18)); +} +/// Verifies eval object construction dispatches through runtime hooks. +#[test] +fn execute_program_constructs_named_object() { + let program = parse_fragment(br#"return new Box();"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Object(Vec::new())); +} +/// Verifies eval object construction passes constructor arguments through runtime hooks. +#[test] +fn execute_program_constructs_named_object_with_args() { + let program = parse_fragment(br#"return new Box(1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + let FakeValue::Object(properties) = values.get(result) else { + panic!("expected fake object"); + }; + let x = FakeOps::object_property(&properties, "x").expect("constructor should set x"); + + assert_eq!(values.get(x), FakeValue::Int(1)); +} + +/// Verifies eval object construction binds registered AOT constructor named arguments. +#[test] +fn execute_program_constructs_named_object_with_registered_named_args() { + let program = parse_fragment(br#"$box = new KnownClass(value: 9); return $box->read_x();"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered constructor named args should bind"); + + assert_eq!(values.get(result), FakeValue::Int(9)); +} + +/// Verifies runtime/AOT constructor fallback honors by-reference parameter metadata. +#[test] +fn execute_program_rejects_runtime_constructor_by_ref_temporary_arg() { + let program = parse_fragment(br#"$box = new KnownClass(9); return $box->read_x();"#) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a constructor by-reference parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies runtime/AOT constructor fallback writes coerced by-reference args back. +#[test] +fn execute_program_writes_back_runtime_constructor_by_ref_type_coercion() { + let program = parse_fragment( + br#"$value = "9"; +$box = new KnownClass($value); +echo $box->read_x(); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownClass", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered constructor by-ref coercion should bind"); + + assert_eq!(values.output, "9"); + assert_eq!(values.get(result), FakeValue::Int(9)); +} + +/// Verifies AOT constructor by-reference writeback still runs when construction fatals. +#[test] +fn execute_program_writes_back_runtime_constructor_by_ref_before_fatal() { + let program = parse_fragment( + br#"$value = "9"; +new KnownFailingConstructor($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "value")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_constructor_signature("KnownFailingConstructor", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("failing constructor should abort after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::RuntimeFatal); + assert_eq!(values.get(value), FakeValue::Int(9)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/expressions/visibility.rs b/crates/elephc-magician/src/interpreter/tests/expressions/visibility.rs new file mode 100644 index 0000000000..f7722a00f9 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/expressions/visibility.rs @@ -0,0 +1,207 @@ +//! Purpose: +//! Interpreter tests for private/protected member access, shadowing, and missing +//! method failures. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Declaring-class scope and global-scope failures use separate cases. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval methods can access private properties and methods declared in their class. +#[test] +fn execute_program_allows_private_eval_members_inside_declaring_class() { + let program = parse_fragment( + br#"class EvalPrivateBox { + private int $secret = 4; + private function bump($n) { return $this->secret + $n; } + public function read($n) { return $this->bump($n); } +} +$box = new EvalPrivateBox(); +return $box->read(3);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} +/// Verifies protected eval members are accessible across a class hierarchy. +#[test] +fn execute_program_allows_protected_eval_members_from_related_classes() { + let program = parse_fragment( + br#"class EvalProtectedBase { + protected int $base = 5; + protected function add($n) { return $this->base + $n; } +} +class EvalProtectedChild extends EvalProtectedBase { + public function read($n) { return $this->add($n); } +} +$box = new EvalProtectedChild(); +return $box->read(2);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies eval child properties shadow private parent properties with a separate storage slot. +#[test] +fn execute_program_shadows_private_eval_parent_property_with_separate_slot() { + let program = parse_fragment( + br#"class EvalPrivateShadowBase { + private $value = 1; + + public function parentValue() { + return $this->value; + } +} +class EvalPrivateShadowChild extends EvalPrivateShadowBase { + public $value = "child"; + + public function childValue() { + return $this->value; + } +} +$box = new EvalPrivateShadowChild(); +echo $box->parentValue(); echo ":"; +echo $box->childValue(); echo ":"; +echo $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:child:child"); +} + +/// Verifies eval later redeclarations update the visible slot while preserving a private grandparent slot. +#[test] +fn execute_program_keeps_eval_private_grandparent_slot_after_later_redeclaration() { + let program = parse_fragment( + br#"class EvalPrivateGrandBase { + private $value = 1; + + public function grandValue() { + return $this->value; + } +} +class EvalPrivateGrandParent extends EvalPrivateGrandBase { + public $value = 2; + + public function parentValue() { + return $this->value; + } +} +class EvalPrivateGrandChild extends EvalPrivateGrandParent { + public $value = 3; +} +$box = new EvalPrivateGrandChild(); +echo $box->grandValue(); echo ":"; +echo $box->parentValue(); echo ":"; +echo $box->value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "1:3:3"); +} + +/// Verifies eval throws Error for private property access from global scope. +#[test] +fn execute_program_private_eval_member_access_from_global_scope_throws_error() { + let program = parse_fragment( + br#"class EvalPrivateGlobalBox { + private int $secret = 4; + private function read() { return $this->secret; } +} +$box = new EvalPrivateGlobalBox(); +try { + echo $box->secret; + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Cannot access private property EvalPrivateGlobalBox::$secret" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} +/// Verifies eval throws Error for calls to private methods from global scope. +#[test] +fn execute_program_private_eval_method_call_from_global_scope_throws_error() { + let program = parse_fragment( + br#"class EvalPrivateMethodBox { + private function read() { return 4; } +} +$box = new EvalPrivateMethodBox(); +try { + echo $box->read(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Call to private method EvalPrivateMethodBox::read() from global scope" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} + +/// Verifies missing eval-declared instance methods throw PHP-compatible Error values. +#[test] +fn execute_program_missing_eval_method_call_throws_error() { + let program = parse_fragment( + br#"class EvalMissingMethodBox {} +$box = new EvalMissingMethodBox(); +try { + echo $box->missing(); + echo "bad"; +} catch (Error $e) { + echo get_class($e) . ":" . $e->getMessage(); +} +return true;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "Error:Call to undefined method EvalMissingMethodBox::missing()" + ); + assert_eq!(values.get(result), FakeValue::Bool(true)); +} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments.rs deleted file mode 100644 index 0b2b504f0c..0000000000 --- a/crates/elephc-magician/src/interpreter/tests/method_arguments.rs +++ /dev/null @@ -1,789 +0,0 @@ -//! Purpose: -//! Interpreter tests for eval-declared method and constructor argument binding. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - These cases cover named arguments and named unpacking on instance methods, -//! static methods, and constructors declared inside eval fragments. - -use super::super::*; -use super::support::*; - -/// Verifies eval-declared instance, static, and constructor methods bind named args. -#[test] -fn execute_program_binds_eval_method_named_args() { - let program = parse_fragment( - br#"class EvalNamedMethodBox { - public function __construct($left, $right) { - $this->label = $left . $right; - } - public function read($left, $right) { - return $this->label . ":" . $left . ":" . $right; - } - public static function join($left, $right) { - return $left . "-" . $right; - } -} -$box = new EvalNamedMethodBox(right: "B", left: "A"); -echo $box->read(right: "D", left: "C"); echo ":"; -$args = ["right" => "F", "left" => "E"]; -echo $box->read(...$args); echo ":"; -return EvalNamedMethodBox::join(right: "H", left: "G");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "AB:C:D:AB:E:F:"); - assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); -} - -/// Verifies eval-declared methods use default values for omitted arguments. -#[test] -fn execute_program_binds_eval_method_default_args() { - let program = parse_fragment( - br#"class EvalDefaultMethodBox { - public function __construct($left = "A", $right = "B") { - $this->label = $left . $right; - } - public function read($left, $right = "D") { - return $this->label . ":" . $left . ":" . $right; - } - public static function join($left = "G", $right = "H") { - return $left . "-" . $right; - } -} -$box = new EvalDefaultMethodBox(); -echo $box->read("C"); echo ":"; -echo $box->read(right: "F", left: "E"); echo ":"; -return EvalDefaultMethodBox::join();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "AB:C:D:AB:E:F:"); - assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); -} - -/// Verifies eval-declared methods materialize constant-expression parameter defaults. -#[test] -fn execute_program_binds_eval_method_constant_default_args() { - let program = parse_fragment( - br#"define("EVAL_METHOD_DEFAULT_GLOBAL", "G"); -class EvalDefaultConstBase { - const LABEL = "base"; -} -interface EvalDefaultConstIface { - const WORD = "iface"; -} -class EvalDefaultConstDep { - public function __construct($label = "dep") { - $this->label = $label; - } - public function read() { - return $this->label; - } -} -class EvalDefaultConstBox extends EvalDefaultConstBase { - const LABEL = "box"; - public function __construct($label = self::LABEL) { - $this->label = $label; - } - public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "fallback"], $method = __METHOD__, $dep = new EvalDefaultConstDep(label: "dep"), $clone = new self("inner")) { - return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass . ":" . $items[self::LABEL] . ":" . $items["fallback"] . ":" . $method . ":" . $dep->read() . ":" . $clone->label; - } - public static function join($label = self::LABEL, $parent = parent::LABEL) { - return $label . "-" . $parent; - } -} -$box = new EvalDefaultConstBox(); -echo $box->read(); echo ":"; -return EvalDefaultConstBox::join();"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!( - values.output, - "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:3:fallback:EvalDefaultConstBox::read:dep:inner:" - ); - assert_eq!(values.get(result), FakeValue::String("box-base".to_string())); -} - -/// Verifies eval-declared methods bind positional and named values into variadic arrays. -#[test] -fn execute_program_binds_eval_method_variadic_args() { - let program = parse_fragment( - br#"class EvalVariadicMethodBox { - public function __construct(...$parts) { - $this->label = $parts[0] . $parts["right"]; - } - public function read($head, ...$tail) { - echo count($tail); echo ":"; - return $this->label . ":" . $head . ":" . $tail[0] . ":" . $tail["named"] . ":" . $tail["tail"]; - } - public static function join(...$items) { - return $items[0] . $items[1]; - } -} -$box = new EvalVariadicMethodBox("A", right: "B"); -echo $box->read("C", "D", named: "E", tail: "F"); echo ":"; -return EvalVariadicMethodBox::join("G", "H");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "3:AB:C:D:E:F:"); - assert_eq!(values.get(result), FakeValue::String("GH".to_string())); -} - -/// Verifies eval-declared instance, static, and constructor methods write back by-reference args. -#[test] -fn execute_program_writes_back_eval_method_by_ref_args() { - let program = parse_fragment( - br#"class EvalByRefMethodBox { - public function __construct(&$value) { - $value = $value . "-ctor"; - } - public function change(&$value) { - $value = $value . "-method"; - } - public static function changeStatic(&$value) { - $value = $value . "-static"; - } -} -$ctor = "A"; -$box = new EvalByRefMethodBox($ctor); -$box->change($ctor); -EvalByRefMethodBox::changeStatic($ctor); -$named = "B"; -$box->change(value: $named); -echo $ctor; echo ":"; -return $named;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A-ctor-method-static:"); - assert_eq!(values.get(result), FakeValue::String("B-method".to_string())); -} - -/// Verifies eval-declared by-reference method parameters reject temporary values. -#[test] -fn execute_program_rejects_eval_method_by_ref_temporary_arg() { - let program = parse_fragment( - br#"class EvalByRefMethodBox { - public function change(&$value) { - $value = "changed"; - } -} -$box = new EvalByRefMethodBox(); -$box->change("literal");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("literal cannot satisfy a by-reference parameter"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies typed eval-declared by-reference method params write back entry coercions. -#[test] -fn execute_program_writes_back_eval_method_by_ref_type_coercion() { - let program = parse_fragment( - br#"class EvalByRefTypedMethodBox { - public function coerce(int &$value) {} -} -$value = "3"; -$box = new EvalByRefTypedMethodBox(); -$box->coerce($value); -return $value;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(3)); -} - -/// Verifies eval-declared by-reference variadics write back mutated captured elements. -#[test] -fn execute_program_writes_back_eval_method_by_ref_variadic_elements() { - let program = parse_fragment( - br#"class EvalByRefVariadicMethodBox { - public function change(&...$items) { - $items[0] = $items[0] . "-first"; - $items["named"] = $items["named"] . "-named"; - } - public function rebind(&...$items) { - $items = []; - } -} -$box = new EvalByRefVariadicMethodBox(); -$first = "A"; -$named = "B"; -$box->change($first, named: $named); -$box->rebind($first); -echo $first; echo ":"; -return $named;"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "A-first:"); - assert_eq!(values.get(result), FakeValue::String("B-named".to_string())); -} - -/// Verifies eval-declared by-reference method params write back array-element lvalues. -#[test] -fn execute_program_writes_back_eval_method_by_ref_array_elements() { - let program = parse_fragment( - br#"class EvalByRefArrayElementMethodBox { - public function set(&$value, $next) { - $value = $next; - } - public function variadic(&...$items) { - $items[0] = "variadic"; - } -} -$box = new EvalByRefArrayElementMethodBox(); -$items = ["k" => "old"]; -$box->set($items["k"], "changed"); -$box->set($missing["new"], "created"); -$box->variadic($items["k"]); -echo $items["k"]; echo ":"; -return $missing["new"];"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "variadic:"); - assert_eq!(values.get(result), FakeValue::String("created".to_string())); -} - -/// Verifies eval-declared by-reference method params write back object-property lvalues. -#[test] -fn execute_program_writes_back_eval_method_by_ref_object_properties() { - let program = parse_fragment( - br#"class EvalByRefPropertyChanger { - public function set(&$value, $next) { - $value = $next; - } - public function variadic(&...$items) { - $items[0] = "variadic"; - } -} -class EvalByRefPublicPropertyBox { - public string $value = "old"; -} -class EvalByRefPrivatePropertyBox { - private string $value = "private"; - public function update($changer) { - $name = "value"; - $changer->set($this->{$name}, "secret"); - return $this->value; - } -} -$changer = new EvalByRefPropertyChanger(); -$public = new EvalByRefPublicPropertyBox(); -$changer->set($public->value, "changed"); -$changer->variadic($public->value); -$name = "value"; -$changer->set($public->{$name}, "dynamic"); -echo $public->value; echo ":"; -$private = new EvalByRefPrivatePropertyBox(); -return $private->update($changer);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "dynamic:"); - assert_eq!(values.get(result), FakeValue::String("secret".to_string())); -} - -/// Verifies eval-declared by-reference method params write back static-property lvalues. -#[test] -fn execute_program_writes_back_eval_method_by_ref_static_properties() { - let program = parse_fragment( - br#"class EvalByRefStaticPropertyChanger { - public function set(&$value, $next) { - $value = $next; - } - public function pair(&$left, &$right) { - $left = "left"; - $right = "right"; - return $left; - } -} -class EvalByRefStaticPropertyBox { - public static string $value = "old"; - public static string $other = "second"; - public static string $third = "third"; - private static string $secret = "private"; - public static function updatePrivate($changer) { - $changer->set(self::$secret, "secret"); - return self::$secret; - } -} -$changer = new EvalByRefStaticPropertyChanger(); -$changer->set(EvalByRefStaticPropertyBox::$value, "changed"); -echo $changer->pair(EvalByRefStaticPropertyBox::$value, EvalByRefStaticPropertyBox::$value); -echo ":"; -echo EvalByRefStaticPropertyBox::$value; echo ":"; -$class = "EvalByRefStaticPropertyBox"; -$changer->set($class::$other, "dynamic"); -$name = "third"; -$changer->set($class::${$name}, "name"); -echo EvalByRefStaticPropertyBox::$other; echo ":"; -echo EvalByRefStaticPropertyBox::$third; echo ":"; -return EvalByRefStaticPropertyBox::updatePrivate($changer);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "right:right:dynamic:name:"); - assert_eq!(values.get(result), FakeValue::String("secret".to_string())); -} - -/// Verifies by-reference method params write back array elements stored in properties. -#[test] -fn execute_program_writes_back_eval_method_by_ref_property_array_elements() { - let program = parse_fragment( - br#"class EvalByRefPropertyArrayElementChanger { - public function set(&$value, $next) { - $value = $next; - } - public function pair(&$left, &$right) { - $left = "left"; - $right = "right"; - return $left; - } -} -class EvalByRefPropertyArrayElementBox { - public $items = ["first" => "old", "same" => "same"]; - public $other = null; - public static $staticItems = ["first" => "static-old", "same" => "static-same"]; -} -$changer = new EvalByRefPropertyArrayElementChanger(); -$box = new EvalByRefPropertyArrayElementBox(); -$changer->set($box->items["first"], "changed"); -$name = "items"; -$changer->set($box->{$name}["dynamic"], "dynamic"); -$changer->set($box->other["created"], "created"); -echo $box->items["first"]; echo ":"; -echo $box->items["dynamic"]; echo ":"; -echo $box->other["created"]; echo ":"; -echo $changer->pair($box->items["same"], $box->items["same"]); echo ":"; -echo $box->items["same"]; echo ":"; -$changer->set(EvalByRefPropertyArrayElementBox::$staticItems["first"], "static"); -$class = "EvalByRefPropertyArrayElementBox"; -$staticName = "staticItems"; -$changer->set($class::${$staticName}["dynamic"], "static-dynamic"); -echo EvalByRefPropertyArrayElementBox::$staticItems["first"]; echo ":"; -echo EvalByRefPropertyArrayElementBox::$staticItems["dynamic"]; echo ":"; -return $changer->pair( - EvalByRefPropertyArrayElementBox::$staticItems["same"], - EvalByRefPropertyArrayElementBox::$staticItems["same"] -) . ":" . EvalByRefPropertyArrayElementBox::$staticItems["same"];"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "changed:dynamic:created:right:right:static:static-dynamic:"); - assert_eq!(values.get(result), FakeValue::String("right:right".to_string())); -} - -/// Verifies eval-declared by-reference method params keep property access restrictions. -#[test] -fn execute_program_rejects_invalid_eval_method_by_ref_object_property_targets() { - let private_program = parse_fragment( - br#"class EvalByRefPrivatePropertyFailChanger { - public function set(&$value) { - $value = "bad"; - } -} -class EvalByRefPrivatePropertyFailBox { - private string $value = "private"; -} -$changer = new EvalByRefPrivatePropertyFailChanger(); -$box = new EvalByRefPrivatePropertyFailBox(); -$changer->set($box->value);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&private_program, &mut scope, &mut values) - .expect_err("private property by-ref target should fail from global scope"); - assert_eq!(err, EvalStatus::UncaughtThrowable); - - let readonly_program = parse_fragment( - br#"class EvalByRefReadonlyPropertyFailChanger { - public function set(&$value) { - $value = "bad"; - } -} -class EvalByRefReadonlyPropertyFailBox { - public readonly string $value; - public function __construct($changer) { - $this->value = "old"; - $changer->set($this->value); - } -} -new EvalByRefReadonlyPropertyFailBox(new EvalByRefReadonlyPropertyFailChanger());"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let err = execute_program(&readonly_program, &mut scope, &mut values) - .expect_err("readonly property by-ref target should fail as an indirect modification"); - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval-declared variadic methods reject duplicate named variadic keys. -#[test] -fn execute_program_rejects_duplicate_eval_method_variadic_named_arg() { - let program = parse_fragment( - br#"class EvalDuplicateVariadicBox { - public function read(...$tail) { - return count($tail); - } -} -$box = new EvalDuplicateVariadicBox(); -return $box->read(name: "A", name: "B");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("duplicate named variadic argument should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies defaults before required eval method parameters do not make earlier slots optional. -#[test] -fn execute_program_rejects_eval_method_default_before_required_omission() { - let program = parse_fragment( - br#"class EvalRequiredAfterDefaultBox { - public function read($left = "A", $right) { - return $left . $right; - } -} -$box = new EvalRequiredAfterDefaultBox(); -return $box->read(right: "B");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("default before required parameter should remain required"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval-declared method scalar type hints coerce weak scalar arguments. -#[test] -fn execute_program_enforces_eval_method_scalar_type_hints() { - let program = parse_fragment( - br#"class EvalTypedScalarBox { - public function read(int $id, string $label, bool $flag) { - echo $id + 1; echo ":"; - echo $label; echo ":"; - return $flag ? "T" : "F"; - } -} -$box = new EvalTypedScalarBox(); -return $box->read("7", 8, 1);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "8:8:"); - assert_eq!(values.get(result), FakeValue::String("T".to_string())); -} - -/// Verifies eval-declared method scalar type hints reject non-coercible values. -#[test] -fn execute_program_rejects_eval_method_scalar_type_mismatch() { - let program = parse_fragment( - br#"class EvalTypedScalarFailBox { - public function read(int $id) { - return $id; - } -} -$box = new EvalTypedScalarFailBox(); -return $box->read("not numeric");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("non-numeric string should fail int parameter type"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies eval-declared method class/interface type hints accept matching eval objects. -#[test] -fn execute_program_enforces_eval_method_object_type_hints() { - let program = parse_fragment( - br#"interface EvalTypedReadable {} -class EvalTypedDep implements EvalTypedReadable {} -class EvalTypedObjectBox { - public function read(EvalTypedReadable $dep, ?EvalTypedDep $nullable) { - echo get_class($dep); echo ":"; - return $nullable === null ? "N" : "bad"; - } -} -$dep = new EvalTypedDep(); -$box = new EvalTypedObjectBox(); -return $box->read($dep, null);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.output, "EvalTypedDep:"); - assert_eq!(values.get(result), FakeValue::String("N".to_string())); -} - -/// Verifies eval-declared variadic method type hints apply to each captured argument. -#[test] -fn execute_program_enforces_eval_method_variadic_type_hints() { - let program = parse_fragment( - br#"class EvalTypedVariadicBox { - public function sum(int ...$items) { - return $items[0] + $items[1]; - } -} -$box = new EvalTypedVariadicBox(); -return $box->sum("3", 4);"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); - - assert_eq!(values.get(result), FakeValue::Int(7)); -} - -/// Verifies eval-declared methods reject unknown named arguments. -#[test] -fn execute_program_rejects_unknown_eval_method_named_arg() { - let program = parse_fragment( - br#"class EvalUnknownNamedMethodBox { - public function read($left) { - return $left; - } -} -$box = new EvalUnknownNamedMethodBox(); -return $box->read(missing: "bad");"#, - ) - .expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("unknown named method argument should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies runtime/AOT method fallback binds registered native method named arguments. -#[test] -fn execute_program_binds_registered_runtime_method_named_args() { - let program = parse_fragment( - br#"$box = new KnownClass(10); -return $box->add2_x(right: 2, left: 3);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("registered runtime method named args should bind"); - - assert_eq!(values.get(result), FakeValue::Int(15)); -} - -/// Verifies runtime/AOT method fallback honors registered by-reference parameter metadata. -#[test] -fn execute_program_rejects_runtime_method_by_ref_temporary_arg() { - let program = parse_fragment( - br#"$box = new KnownClass(10); -return $box->add2_x(1, 2);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect_err("literal cannot satisfy a runtime by-reference method parameter"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} - -/// Verifies runtime/AOT method fallback writes coerced by-reference args back. -#[test] -fn execute_program_writes_back_runtime_method_by_ref_type_coercion() { - let program = parse_fragment( - br#"$box = new KnownClass(10); -$value = "3"; -echo $box->add2_x($value, 2); -return $value;"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(2); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_name(1, "right")); - assert!(signature.set_param_type( - 0, - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect("registered runtime method by-ref coercion should bind"); - - assert_eq!(values.output, "15"); - assert_eq!(values.get(result), FakeValue::Int(3)); -} - -/// Verifies AOT instance method by-reference writeback still runs when the method fatals. -#[test] -fn execute_program_writes_back_runtime_method_by_ref_before_fatal() { - let program = parse_fragment( - br#"$box = new KnownClass(10); -$value = "3"; -$box->add2_x($value);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(1); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_type( - 0, - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect_err("runtime method should fail after argument binding"); - let value = scope - .entry("value") - .expect("caller variable should remain visible") - .cell(); - - assert_eq!(err, EvalStatus::UnsupportedConstruct); - assert_eq!(values.get(value), FakeValue::Int(3)); -} - -/// Verifies AOT static method by-reference writeback still runs when the method fatals. -#[test] -fn execute_program_writes_back_runtime_static_method_by_ref_before_fatal() { - let program = parse_fragment( - br#"$value = "3"; -KnownClass::sum($value);"#, - ) - .expect("parse eval fragment"); - let mut context = ElephcEvalContext::new(); - let mut signature = NativeCallableSignature::new(1); - assert!(signature.set_param_name(0, "left")); - assert!(signature.set_param_type( - 0, - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )); - assert!(signature.set_param_by_ref(0, true)); - assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - - let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) - .expect_err("runtime static method should fail after argument binding"); - let value = scope - .entry("value") - .expect("caller variable should remain visible") - .cell(); - - assert_eq!(err, EvalStatus::UnsupportedConstruct); - assert_eq!(values.get(value), FakeValue::Int(3)); -} - -/// Verifies runtime/AOT method fallback rejects named arguments without metadata. -#[test] -fn execute_program_rejects_unregistered_named_args_for_runtime_method_fallback() { - let program = - parse_fragment(br#"return $this->answer(value: 1);"#).expect("parse eval fragment"); - let mut scope = ElephcEvalScope::new(); - let mut values = FakeOps::default(); - let object = values.alloc(FakeValue::Object(Vec::new())); - scope.set("this", object, ScopeCellOwnership::Borrowed); - - let err = execute_program(&program, &mut scope, &mut values) - .expect_err("unregistered runtime method fallback named args should fail"); - - assert_eq!(err, EvalStatus::RuntimeFatal); -} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/binding_defaults.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/binding_defaults.rs new file mode 100644 index 0000000000..f8ad7b0374 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/binding_defaults.rs @@ -0,0 +1,152 @@ +//! Purpose: +//! Interpreter tests for named, default, constant-default, and variadic argument +//! binding on eval methods and constructors. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover named arguments and named unpacking on instance methods, +//! static methods, and constructors declared inside eval fragments. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared instance, static, and constructor methods bind named args. +#[test] +fn execute_program_binds_eval_method_named_args() { + let program = parse_fragment( + br#"class EvalNamedMethodBox { + public function __construct($left, $right) { + $this->label = $left . $right; + } + public function read($left, $right) { + return $this->label . ":" . $left . ":" . $right; + } + public static function join($left, $right) { + return $left . "-" . $right; + } +} +$box = new EvalNamedMethodBox(right: "B", left: "A"); +echo $box->read(right: "D", left: "C"); echo ":"; +$args = ["right" => "F", "left" => "E"]; +echo $box->read(...$args); echo ":"; +return EvalNamedMethodBox::join(right: "H", left: "G");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:C:D:AB:E:F:"); + assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); +} + +/// Verifies eval-declared methods use default values for omitted arguments. +#[test] +fn execute_program_binds_eval_method_default_args() { + let program = parse_fragment( + br#"class EvalDefaultMethodBox { + public function __construct($left = "A", $right = "B") { + $this->label = $left . $right; + } + public function read($left, $right = "D") { + return $this->label . ":" . $left . ":" . $right; + } + public static function join($left = "G", $right = "H") { + return $left . "-" . $right; + } +} +$box = new EvalDefaultMethodBox(); +echo $box->read("C"); echo ":"; +echo $box->read(right: "F", left: "E"); echo ":"; +return EvalDefaultMethodBox::join();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "AB:C:D:AB:E:F:"); + assert_eq!(values.get(result), FakeValue::String("G-H".to_string())); +} + +/// Verifies eval-declared methods materialize constant-expression parameter defaults. +#[test] +fn execute_program_binds_eval_method_constant_default_args() { + let program = parse_fragment( + br#"define("EVAL_METHOD_DEFAULT_GLOBAL", "G"); +class EvalDefaultConstBase { + const LABEL = "base"; +} +interface EvalDefaultConstIface { + const WORD = "iface"; +} +class EvalDefaultConstDep { + public function __construct($label = "dep") { + $this->label = $label; + } + public function read() { + return $this->label; + } +} +class EvalDefaultConstBox extends EvalDefaultConstBase { + const LABEL = "box"; + public function __construct($label = self::LABEL) { + $this->label = $label; + } + public function read($global = EVAL_METHOD_DEFAULT_GLOBAL, $parent = parent::LABEL, $iface = EvalDefaultConstIface::WORD, $class = self::class, $parentClass = parent::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "fallback"], $method = __METHOD__, $dep = new EvalDefaultConstDep(label: "dep"), $clone = new self("inner")) { + return $this->label . ":" . $global . ":" . $parent . ":" . $iface . ":" . $class . ":" . $parentClass . ":" . $items[self::LABEL] . ":" . $items["fallback"] . ":" . $method . ":" . $dep->read() . ":" . $clone->label; + } + public static function join($label = self::LABEL, $parent = parent::LABEL) { + return $label . "-" . $parent; + } +} +$box = new EvalDefaultConstBox(); +echo $box->read(); echo ":"; +return EvalDefaultConstBox::join();"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!( + values.output, + "box:G:base:iface:EvalDefaultConstBox:EvalDefaultConstBase:3:fallback:EvalDefaultConstBox::read:dep:inner:" + ); + assert_eq!(values.get(result), FakeValue::String("box-base".to_string())); +} + +/// Verifies eval-declared methods bind positional and named values into variadic arrays. +#[test] +fn execute_program_binds_eval_method_variadic_args() { + let program = parse_fragment( + br#"class EvalVariadicMethodBox { + public function __construct(...$parts) { + $this->label = $parts[0] . $parts["right"]; + } + public function read($head, ...$tail) { + echo count($tail); echo ":"; + return $this->label . ":" . $head . ":" . $tail[0] . ":" . $tail["named"] . ":" . $tail["tail"]; + } + public static function join(...$items) { + return $items[0] . $items[1]; + } +} +$box = new EvalVariadicMethodBox("A", right: "B"); +echo $box->read("C", "D", named: "E", tail: "F"); echo ":"; +return EvalVariadicMethodBox::join("G", "H");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "3:AB:C:D:E:F:"); + assert_eq!(values.get(result), FakeValue::String("GH".to_string())); +} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/by_reference.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/by_reference.rs new file mode 100644 index 0000000000..bc80635cad --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/by_reference.rs @@ -0,0 +1,337 @@ +//! Purpose: +//! Interpreter tests for eval method by-reference binding and writeback across +//! variables, arrays, object/static properties, and nested elements. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Invalid temporary and readonly targets retain their fatal behavior. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared instance, static, and constructor methods write back by-reference args. +#[test] +fn execute_program_writes_back_eval_method_by_ref_args() { + let program = parse_fragment( + br#"class EvalByRefMethodBox { + public function __construct(&$value) { + $value = $value . "-ctor"; + } + public function change(&$value) { + $value = $value . "-method"; + } + public static function changeStatic(&$value) { + $value = $value . "-static"; + } +} +$ctor = "A"; +$box = new EvalByRefMethodBox($ctor); +$box->change($ctor); +EvalByRefMethodBox::changeStatic($ctor); +$named = "B"; +$box->change(value: $named); +echo $ctor; echo ":"; +return $named;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A-ctor-method-static:"); + assert_eq!(values.get(result), FakeValue::String("B-method".to_string())); +} + +/// Verifies eval-declared by-reference method parameters reject temporary values. +#[test] +fn execute_program_rejects_eval_method_by_ref_temporary_arg() { + let program = parse_fragment( + br#"class EvalByRefMethodBox { + public function change(&$value) { + $value = "changed"; + } +} +$box = new EvalByRefMethodBox(); +$box->change("literal");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a by-reference parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies typed eval-declared by-reference method params write back entry coercions. +#[test] +fn execute_program_writes_back_eval_method_by_ref_type_coercion() { + let program = parse_fragment( + br#"class EvalByRefTypedMethodBox { + public function coerce(int &$value) {} +} +$value = "3"; +$box = new EvalByRefTypedMethodBox(); +$box->coerce($value); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies eval-declared by-reference variadics write back mutated captured elements. +#[test] +fn execute_program_writes_back_eval_method_by_ref_variadic_elements() { + let program = parse_fragment( + br#"class EvalByRefVariadicMethodBox { + public function change(&...$items) { + $items[0] = $items[0] . "-first"; + $items["named"] = $items["named"] . "-named"; + } + public function rebind(&...$items) { + $items = []; + } +} +$box = new EvalByRefVariadicMethodBox(); +$first = "A"; +$named = "B"; +$box->change($first, named: $named); +$box->rebind($first); +echo $first; echo ":"; +return $named;"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "A-first:"); + assert_eq!(values.get(result), FakeValue::String("B-named".to_string())); +} + +/// Verifies eval-declared by-reference method params write back array-element lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_array_elements() { + let program = parse_fragment( + br#"class EvalByRefArrayElementMethodBox { + public function set(&$value, $next) { + $value = $next; + } + public function variadic(&...$items) { + $items[0] = "variadic"; + } +} +$box = new EvalByRefArrayElementMethodBox(); +$items = ["k" => "old"]; +$box->set($items["k"], "changed"); +$box->set($missing["new"], "created"); +$box->variadic($items["k"]); +echo $items["k"]; echo ":"; +return $missing["new"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "variadic:"); + assert_eq!(values.get(result), FakeValue::String("created".to_string())); +} + +/// Verifies eval-declared by-reference method params write back object-property lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_object_properties() { + let program = parse_fragment( + br#"class EvalByRefPropertyChanger { + public function set(&$value, $next) { + $value = $next; + } + public function variadic(&...$items) { + $items[0] = "variadic"; + } +} +class EvalByRefPublicPropertyBox { + public string $value = "old"; +} +class EvalByRefPrivatePropertyBox { + private string $value = "private"; + public function update($changer) { + $name = "value"; + $changer->set($this->{$name}, "secret"); + return $this->value; + } +} +$changer = new EvalByRefPropertyChanger(); +$public = new EvalByRefPublicPropertyBox(); +$changer->set($public->value, "changed"); +$changer->variadic($public->value); +$name = "value"; +$changer->set($public->{$name}, "dynamic"); +echo $public->value; echo ":"; +$private = new EvalByRefPrivatePropertyBox(); +return $private->update($changer);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "dynamic:"); + assert_eq!(values.get(result), FakeValue::String("secret".to_string())); +} + +/// Verifies eval-declared by-reference method params write back static-property lvalues. +#[test] +fn execute_program_writes_back_eval_method_by_ref_static_properties() { + let program = parse_fragment( + br#"class EvalByRefStaticPropertyChanger { + public function set(&$value, $next) { + $value = $next; + } + public function pair(&$left, &$right) { + $left = "left"; + $right = "right"; + return $left; + } +} +class EvalByRefStaticPropertyBox { + public static string $value = "old"; + public static string $other = "second"; + public static string $third = "third"; + private static string $secret = "private"; + public static function updatePrivate($changer) { + $changer->set(self::$secret, "secret"); + return self::$secret; + } +} +$changer = new EvalByRefStaticPropertyChanger(); +$changer->set(EvalByRefStaticPropertyBox::$value, "changed"); +echo $changer->pair(EvalByRefStaticPropertyBox::$value, EvalByRefStaticPropertyBox::$value); +echo ":"; +echo EvalByRefStaticPropertyBox::$value; echo ":"; +$class = "EvalByRefStaticPropertyBox"; +$changer->set($class::$other, "dynamic"); +$name = "third"; +$changer->set($class::${$name}, "name"); +echo EvalByRefStaticPropertyBox::$other; echo ":"; +echo EvalByRefStaticPropertyBox::$third; echo ":"; +return EvalByRefStaticPropertyBox::updatePrivate($changer);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "right:right:dynamic:name:"); + assert_eq!(values.get(result), FakeValue::String("secret".to_string())); +} + +/// Verifies by-reference method params write back array elements stored in properties. +#[test] +fn execute_program_writes_back_eval_method_by_ref_property_array_elements() { + let program = parse_fragment( + br#"class EvalByRefPropertyArrayElementChanger { + public function set(&$value, $next) { + $value = $next; + } + public function pair(&$left, &$right) { + $left = "left"; + $right = "right"; + return $left; + } +} +class EvalByRefPropertyArrayElementBox { + public $items = ["first" => "old", "same" => "same"]; + public $other = null; + public static $staticItems = ["first" => "static-old", "same" => "static-same"]; +} +$changer = new EvalByRefPropertyArrayElementChanger(); +$box = new EvalByRefPropertyArrayElementBox(); +$changer->set($box->items["first"], "changed"); +$name = "items"; +$changer->set($box->{$name}["dynamic"], "dynamic"); +$changer->set($box->other["created"], "created"); +echo $box->items["first"]; echo ":"; +echo $box->items["dynamic"]; echo ":"; +echo $box->other["created"]; echo ":"; +echo $changer->pair($box->items["same"], $box->items["same"]); echo ":"; +echo $box->items["same"]; echo ":"; +$changer->set(EvalByRefPropertyArrayElementBox::$staticItems["first"], "static"); +$class = "EvalByRefPropertyArrayElementBox"; +$staticName = "staticItems"; +$changer->set($class::${$staticName}["dynamic"], "static-dynamic"); +echo EvalByRefPropertyArrayElementBox::$staticItems["first"]; echo ":"; +echo EvalByRefPropertyArrayElementBox::$staticItems["dynamic"]; echo ":"; +return $changer->pair( + EvalByRefPropertyArrayElementBox::$staticItems["same"], + EvalByRefPropertyArrayElementBox::$staticItems["same"] +) . ":" . EvalByRefPropertyArrayElementBox::$staticItems["same"];"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "changed:dynamic:created:right:right:static:static-dynamic:"); + assert_eq!(values.get(result), FakeValue::String("right:right".to_string())); +} + +/// Verifies eval-declared by-reference method params keep property access restrictions. +#[test] +fn execute_program_rejects_invalid_eval_method_by_ref_object_property_targets() { + let private_program = parse_fragment( + br#"class EvalByRefPrivatePropertyFailChanger { + public function set(&$value) { + $value = "bad"; + } +} +class EvalByRefPrivatePropertyFailBox { + private string $value = "private"; +} +$changer = new EvalByRefPrivatePropertyFailChanger(); +$box = new EvalByRefPrivatePropertyFailBox(); +$changer->set($box->value);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&private_program, &mut scope, &mut values) + .expect_err("private property by-ref target should fail from global scope"); + assert_eq!(err, EvalStatus::UncaughtThrowable); + + let readonly_program = parse_fragment( + br#"class EvalByRefReadonlyPropertyFailChanger { + public function set(&$value) { + $value = "bad"; + } +} +class EvalByRefReadonlyPropertyFailBox { + public readonly string $value; + public function __construct($changer) { + $this->value = "old"; + $changer->set($this->value); + } +} +new EvalByRefReadonlyPropertyFailBox(new EvalByRefReadonlyPropertyFailChanger());"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let err = execute_program(&readonly_program, &mut scope, &mut values) + .expect_err("readonly property by-ref target should fail as an indirect modification"); + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/mod.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/mod.rs new file mode 100644 index 0000000000..5c763f818e --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/mod.rs @@ -0,0 +1,14 @@ +//! Purpose: +//! Organizes interpreter tests for eval and runtime method argument binding by +//! defaults, references, types, and fallback surface. +//! +//! Called from: +//! - `crate::interpreter::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod binding_defaults; +mod by_reference; +mod runtime_fallback; +mod types_errors; diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/runtime_fallback.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/runtime_fallback.rs new file mode 100644 index 0000000000..fa4f36b7dd --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/runtime_fallback.rs @@ -0,0 +1,166 @@ +//! Purpose: +//! Interpreter tests for runtime/AOT method argument fallback, named binding, +//! by-reference validation, coercion writeback, and fatal paths. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Instance and static runtime hooks preserve writeback before errors. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies runtime/AOT method fallback binds registered native method named arguments. +#[test] +fn execute_program_binds_registered_runtime_method_named_args() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +return $box->add2_x(right: 2, left: 3);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered runtime method named args should bind"); + + assert_eq!(values.get(result), FakeValue::Int(15)); +} + +/// Verifies runtime/AOT method fallback honors registered by-reference parameter metadata. +#[test] +fn execute_program_rejects_runtime_method_by_ref_temporary_arg() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +return $box->add2_x(1, 2);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("literal cannot satisfy a runtime by-reference method parameter"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies runtime/AOT method fallback writes coerced by-reference args back. +#[test] +fn execute_program_writes_back_runtime_method_by_ref_type_coercion() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$value = "3"; +echo $box->add2_x($value, 2); +return $value;"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(2); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_name(1, "right")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect("registered runtime method by-ref coercion should bind"); + + assert_eq!(values.output, "15"); + assert_eq!(values.get(result), FakeValue::Int(3)); +} + +/// Verifies AOT instance method by-reference writeback still runs when the method fatals. +#[test] +fn execute_program_writes_back_runtime_method_by_ref_before_fatal() { + let program = parse_fragment( + br#"$box = new KnownClass(10); +$value = "3"; +$box->add2_x($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_method_signature("KnownClass", "add2_x", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("runtime method should fail after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(value), FakeValue::Int(3)); +} + +/// Verifies AOT static method by-reference writeback still runs when the method fatals. +#[test] +fn execute_program_writes_back_runtime_static_method_by_ref_before_fatal() { + let program = parse_fragment( + br#"$value = "3"; +KnownClass::sum($value);"#, + ) + .expect("parse eval fragment"); + let mut context = ElephcEvalContext::new(); + let mut signature = NativeCallableSignature::new(1); + assert!(signature.set_param_name(0, "left")); + assert!(signature.set_param_type( + 0, + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )); + assert!(signature.set_param_by_ref(0, true)); + assert!(context.define_native_static_method_signature("KnownClass", "sum", signature)); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program_with_context(&mut context, &program, &mut scope, &mut values) + .expect_err("runtime static method should fail after argument binding"); + let value = scope + .entry("value") + .expect("caller variable should remain visible") + .cell(); + + assert_eq!(err, EvalStatus::UnsupportedConstruct); + assert_eq!(values.get(value), FakeValue::Int(3)); +} + +/// Verifies runtime/AOT method fallback rejects named arguments without metadata. +#[test] +fn execute_program_rejects_unregistered_named_args_for_runtime_method_fallback() { + let program = + parse_fragment(br#"return $this->answer(value: 1);"#).expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + let object = values.alloc(FakeValue::Object(Vec::new())); + scope.set("this", object, ScopeCellOwnership::Borrowed); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unregistered runtime method fallback named args should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} diff --git a/crates/elephc-magician/src/interpreter/tests/method_arguments/types_errors.rs b/crates/elephc-magician/src/interpreter/tests/method_arguments/types_errors.rs new file mode 100644 index 0000000000..b41c406eb3 --- /dev/null +++ b/crates/elephc-magician/src/interpreter/tests/method_arguments/types_errors.rs @@ -0,0 +1,171 @@ +//! Purpose: +//! Interpreter tests for duplicate/unknown arguments, omission rules, and scalar, +//! object, or variadic method type hints. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Binding diagnostics and runtime coercion checks use separate cases. + +use super::super::super::*; +use super::super::support::*; + +/// Verifies eval-declared variadic methods reject duplicate named variadic keys. +#[test] +fn execute_program_rejects_duplicate_eval_method_variadic_named_arg() { + let program = parse_fragment( + br#"class EvalDuplicateVariadicBox { + public function read(...$tail) { + return count($tail); + } +} +$box = new EvalDuplicateVariadicBox(); +return $box->read(name: "A", name: "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("duplicate named variadic argument should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies defaults before required eval method parameters do not make earlier slots optional. +#[test] +fn execute_program_rejects_eval_method_default_before_required_omission() { + let program = parse_fragment( + br#"class EvalRequiredAfterDefaultBox { + public function read($left = "A", $right) { + return $left . $right; + } +} +$box = new EvalRequiredAfterDefaultBox(); +return $box->read(right: "B");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("default before required parameter should remain required"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared method scalar type hints coerce weak scalar arguments. +#[test] +fn execute_program_enforces_eval_method_scalar_type_hints() { + let program = parse_fragment( + br#"class EvalTypedScalarBox { + public function read(int $id, string $label, bool $flag) { + echo $id + 1; echo ":"; + echo $label; echo ":"; + return $flag ? "T" : "F"; + } +} +$box = new EvalTypedScalarBox(); +return $box->read("7", 8, 1);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "8:8:"); + assert_eq!(values.get(result), FakeValue::String("T".to_string())); +} + +/// Verifies eval-declared method scalar type hints reject non-coercible values. +#[test] +fn execute_program_rejects_eval_method_scalar_type_mismatch() { + let program = parse_fragment( + br#"class EvalTypedScalarFailBox { + public function read(int $id) { + return $id; + } +} +$box = new EvalTypedScalarFailBox(); +return $box->read("not numeric");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("non-numeric string should fail int parameter type"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} + +/// Verifies eval-declared method class/interface type hints accept matching eval objects. +#[test] +fn execute_program_enforces_eval_method_object_type_hints() { + let program = parse_fragment( + br#"interface EvalTypedReadable {} +class EvalTypedDep implements EvalTypedReadable {} +class EvalTypedObjectBox { + public function read(EvalTypedReadable $dep, ?EvalTypedDep $nullable) { + echo get_class($dep); echo ":"; + return $nullable === null ? "N" : "bad"; + } +} +$dep = new EvalTypedDep(); +$box = new EvalTypedObjectBox(); +return $box->read($dep, null);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.output, "EvalTypedDep:"); + assert_eq!(values.get(result), FakeValue::String("N".to_string())); +} + +/// Verifies eval-declared variadic method type hints apply to each captured argument. +#[test] +fn execute_program_enforces_eval_method_variadic_type_hints() { + let program = parse_fragment( + br#"class EvalTypedVariadicBox { + public function sum(int ...$items) { + return $items[0] + $items[1]; + } +} +$box = new EvalTypedVariadicBox(); +return $box->sum("3", 4);"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let result = execute_program(&program, &mut scope, &mut values).expect("execute eval ir"); + + assert_eq!(values.get(result), FakeValue::Int(7)); +} + +/// Verifies eval-declared methods reject unknown named arguments. +#[test] +fn execute_program_rejects_unknown_eval_method_named_arg() { + let program = parse_fragment( + br#"class EvalUnknownNamedMethodBox { + public function read($left) { + return $left; + } +} +$box = new EvalUnknownNamedMethodBox(); +return $box->read(missing: "bad");"#, + ) + .expect("parse eval fragment"); + let mut scope = ElephcEvalScope::new(); + let mut values = FakeOps::default(); + + let err = execute_program(&program, &mut scope, &mut values) + .expect_err("unknown named method argument should fail"); + + assert_eq!(err, EvalStatus::RuntimeFatal); +} From c90e047b19591e0d65aa2af865075db6939a87f3 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 13 Jul 2026 13:27:28 +0200 Subject: [PATCH 1206/1208] refactor(magician): split parser modules and tests --- .../elephc-magician/src/parser/expressions.rs | 1436 +----- .../parser/expressions/callables_arrays.rs | 258 ++ .../src/parser/expressions/postfix.rs | 277 ++ .../src/parser/expressions/precedence.rs | 372 ++ .../src/parser/expressions/primary.rs | 220 + .../src/parser/expressions/static_names.rs | 377 ++ .../elephc-magician/src/parser/statements.rs | 4002 +---------------- .../src/parser/statements/assignments.rs | 600 +++ .../parser/statements/class_declarations.rs | 627 +++ .../src/parser/statements/class_members.rs | 344 ++ .../src/parser/statements/control_flow.rs | 242 + .../parser/statements/default_validation.rs | 109 + .../src/parser/statements/enums.rs | 164 + .../parser/statements/functions_namespaces.rs | 246 + .../parser/statements/globals_exceptions.rs | 109 + .../src/parser/statements/interfaces.rs | 293 ++ .../src/parser/statements/loops.rs | 106 + .../src/parser/statements/parameters_types.rs | 333 ++ .../parser/statements/property_builders.rs | 279 ++ .../statements/property_hook_analysis.rs | 537 +++ .../src/parser/statements/traits.rs | 153 + .../src/parser/tests/arrays_objects.rs | 672 --- .../tests/arrays_objects/arrays_access.rs | 243 + .../arrays_objects/construction_calls.rs | 189 + .../src/parser/tests/arrays_objects/mod.rs | 13 + .../arrays_objects/property_mutations.rs | 264 ++ .../src/parser/tests/classes_errors.rs | 1613 ------- .../parser/tests/classes_errors/attributes.rs | 282 ++ .../tests/classes_errors/declarations.rs | 311 ++ .../classes_errors/interfaces_properties.rs | 326 ++ .../classes_errors/methods_traits_errors.rs | 319 ++ .../src/parser/tests/classes_errors/mod.rs | 15 + .../tests/classes_errors/visibility_hooks.rs | 419 ++ .../src/parser/tests/static_members.rs | 672 --- .../tests/static_members/calls_receivers.rs | 301 ++ .../tests/static_members/dynamic_names.rs | 115 + .../src/parser/tests/static_members/mod.rs | 13 + .../static_members/property_mutations.rs | 279 ++ 38 files changed, 8761 insertions(+), 8369 deletions(-) create mode 100644 crates/elephc-magician/src/parser/expressions/callables_arrays.rs create mode 100644 crates/elephc-magician/src/parser/expressions/postfix.rs create mode 100644 crates/elephc-magician/src/parser/expressions/precedence.rs create mode 100644 crates/elephc-magician/src/parser/expressions/primary.rs create mode 100644 crates/elephc-magician/src/parser/expressions/static_names.rs create mode 100644 crates/elephc-magician/src/parser/statements/assignments.rs create mode 100644 crates/elephc-magician/src/parser/statements/class_declarations.rs create mode 100644 crates/elephc-magician/src/parser/statements/class_members.rs create mode 100644 crates/elephc-magician/src/parser/statements/control_flow.rs create mode 100644 crates/elephc-magician/src/parser/statements/default_validation.rs create mode 100644 crates/elephc-magician/src/parser/statements/enums.rs create mode 100644 crates/elephc-magician/src/parser/statements/functions_namespaces.rs create mode 100644 crates/elephc-magician/src/parser/statements/globals_exceptions.rs create mode 100644 crates/elephc-magician/src/parser/statements/interfaces.rs create mode 100644 crates/elephc-magician/src/parser/statements/loops.rs create mode 100644 crates/elephc-magician/src/parser/statements/parameters_types.rs create mode 100644 crates/elephc-magician/src/parser/statements/property_builders.rs create mode 100644 crates/elephc-magician/src/parser/statements/property_hook_analysis.rs create mode 100644 crates/elephc-magician/src/parser/statements/traits.rs delete mode 100644 crates/elephc-magician/src/parser/tests/arrays_objects.rs create mode 100644 crates/elephc-magician/src/parser/tests/arrays_objects/arrays_access.rs create mode 100644 crates/elephc-magician/src/parser/tests/arrays_objects/construction_calls.rs create mode 100644 crates/elephc-magician/src/parser/tests/arrays_objects/mod.rs create mode 100644 crates/elephc-magician/src/parser/tests/arrays_objects/property_mutations.rs delete mode 100644 crates/elephc-magician/src/parser/tests/classes_errors.rs create mode 100644 crates/elephc-magician/src/parser/tests/classes_errors/attributes.rs create mode 100644 crates/elephc-magician/src/parser/tests/classes_errors/declarations.rs create mode 100644 crates/elephc-magician/src/parser/tests/classes_errors/interfaces_properties.rs create mode 100644 crates/elephc-magician/src/parser/tests/classes_errors/methods_traits_errors.rs create mode 100644 crates/elephc-magician/src/parser/tests/classes_errors/mod.rs create mode 100644 crates/elephc-magician/src/parser/tests/classes_errors/visibility_hooks.rs delete mode 100644 crates/elephc-magician/src/parser/tests/static_members.rs create mode 100644 crates/elephc-magician/src/parser/tests/static_members/calls_receivers.rs create mode 100644 crates/elephc-magician/src/parser/tests/static_members/dynamic_names.rs create mode 100644 crates/elephc-magician/src/parser/tests/static_members/mod.rs create mode 100644 crates/elephc-magician/src/parser/tests/static_members/property_mutations.rs diff --git a/crates/elephc-magician/src/parser/expressions.rs b/crates/elephc-magician/src/parser/expressions.rs index fa6336d53b..c784206323 100644 --- a/crates/elephc-magician/src/parser/expressions.rs +++ b/crates/elephc-magician/src/parser/expressions.rs @@ -19,1434 +19,8 @@ use crate::eval_ir::{ }; use crate::lexer::TokenKind; -impl Parser { - /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. - pub(super) fn parse_expr(&mut self) -> Result { - self.parse_keyword_or() - } - - /// Parses PHP keyword `or`, whose precedence is lower than `xor`, `and`, and ternary. - pub(super) fn parse_keyword_or(&mut self) -> Result { - let mut expr = self.parse_keyword_xor()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "or")) { - self.advance(); - let right = self.parse_keyword_xor()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP keyword `xor`, whose operands are evaluated before boolean XOR. - pub(super) fn parse_keyword_xor(&mut self) -> Result { - let mut expr = self.parse_keyword_and()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "xor")) { - self.advance(); - let right = self.parse_keyword_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalXor, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP keyword `and`, whose precedence is lower than ternary and `&&`. - pub(super) fn parse_keyword_and(&mut self) -> Result { - let mut expr = self.parse_ternary()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "and")) { - self.advance(); - let right = self.parse_ternary()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. - pub(super) fn parse_ternary(&mut self) -> Result { - let condition = self.parse_null_coalesce()?; - if !self.consume(TokenKind::Question) { - return Ok(condition); - } - let then_branch = if self.consume(TokenKind::Colon) { - None - } else { - let expr = self.parse_expr()?; - self.expect(TokenKind::Colon)?; - Some(Box::new(expr)) - }; - let else_branch = self.parse_expr()?; - Ok(EvalExpr::Ternary { - condition: Box::new(condition), - then_branch, - else_branch: Box::new(else_branch), - }) - } - - /// Parses right-associative null coalescing below logical OR and above ternary. - pub(super) fn parse_null_coalesce(&mut self) -> Result { - let value = self.parse_logical_or()?; - if !self.consume(TokenKind::QuestionQuestion) { - return Ok(value); - } - let default = self.parse_null_coalesce()?; - Ok(EvalExpr::NullCoalesce { - value: Box::new(value), - default: Box::new(default), - }) - } - - /// Parses left-associative logical OR with lower precedence than logical AND. - pub(super) fn parse_logical_or(&mut self) -> Result { - let mut expr = self.parse_logical_and()?; - while self.consume(TokenKind::OrOr) { - let right = self.parse_logical_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative logical AND with lower precedence than equality. - pub(super) fn parse_logical_and(&mut self) -> Result { - let mut expr = self.parse_bit_or()?; - while self.consume(TokenKind::AndAnd) { - let right = self.parse_bit_or()?; - expr = EvalExpr::Binary { - op: EvalBinOp::LogicalAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise OR with lower precedence than bitwise XOR. - pub(super) fn parse_bit_or(&mut self) -> Result { - let mut expr = self.parse_bit_xor()?; - while self.consume(TokenKind::Pipe) { - let right = self.parse_bit_xor()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitOr, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise XOR with lower precedence than bitwise AND. - pub(super) fn parse_bit_xor(&mut self) -> Result { - let mut expr = self.parse_bit_and()?; - while self.consume(TokenKind::Caret) { - let right = self.parse_bit_and()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitXor, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative bitwise AND with lower precedence than equality. - pub(super) fn parse_bit_and(&mut self) -> Result { - let mut expr = self.parse_equality()?; - while self.consume(TokenKind::Ampersand) { - let right = self.parse_equality()?; - expr = EvalExpr::Binary { - op: EvalBinOp::BitAnd, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative equality and inequality comparisons. - pub(super) fn parse_equality(&mut self) -> Result { - let mut expr = self.parse_ordering()?; - loop { - let op = if self.consume(TokenKind::EqualEqual) { - EvalBinOp::LooseEq - } else if self.consume(TokenKind::NotEqual) { - EvalBinOp::LooseNotEq - } else if self.consume(TokenKind::EqualEqualEqual) { - EvalBinOp::StrictEq - } else if self.consume(TokenKind::NotEqualEqual) { - EvalBinOp::StrictNotEq - } else { - break; - }; - let right = self.parse_ordering()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative ordered comparisons. - pub(super) fn parse_ordering(&mut self) -> Result { - let mut expr = self.parse_shift()?; - loop { - let op = if self.consume(TokenKind::Less) { - EvalBinOp::Lt - } else if self.consume(TokenKind::LessEqual) { - EvalBinOp::LtEq - } else if self.consume(TokenKind::Greater) { - EvalBinOp::Gt - } else if self.consume(TokenKind::GreaterEqual) { - EvalBinOp::GtEq - } else if self.consume(TokenKind::Spaceship) { - EvalBinOp::Spaceship - } else { - break; - }; - let right = self.parse_shift()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative integer shift operators. - pub(super) fn parse_shift(&mut self) -> Result { - let mut expr = self.parse_concat()?; - loop { - let op = if self.consume(TokenKind::LessLess) { - EvalBinOp::ShiftLeft - } else if self.consume(TokenKind::GreaterGreater) { - EvalBinOp::ShiftRight - } else { - break; - }; - let right = self.parse_concat()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative string concatenation. - pub(super) fn parse_concat(&mut self) -> Result { - let mut expr = self.parse_add()?; - while self.consume(TokenKind::Dot) { - let right = self.parse_add()?; - expr = EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative numeric addition and subtraction. - pub(super) fn parse_add(&mut self) -> Result { - let mut expr = self.parse_mul()?; - loop { - let op = if self.consume(TokenKind::Plus) { - EvalBinOp::Add - } else if self.consume(TokenKind::Minus) { - EvalBinOp::Sub - } else { - break; - }; - let right = self.parse_mul()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses left-associative numeric multiplication, division, and modulo. - pub(super) fn parse_mul(&mut self) -> Result { - let mut expr = self.parse_unary()?; - loop { - let op = if self.consume(TokenKind::Star) { - EvalBinOp::Mul - } else if self.consume(TokenKind::Slash) { - EvalBinOp::Div - } else if self.consume(TokenKind::Percent) { - EvalBinOp::Mod - } else { - break; - }; - let right = self.parse_unary()?; - expr = EvalExpr::Binary { - op, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses right-associative unary prefix expressions. - pub(super) fn parse_unary(&mut self) -> Result { - if let Some(target) = self.peek_scalar_cast_type() { - self.advance(); - self.advance(); - self.advance(); - let expr = self.parse_concat()?; - return Ok(EvalExpr::Cast { - target, - expr: Box::new(expr), - }); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "clone")) { - self.advance(); - let expr = self.parse_unary()?; - return Ok(EvalExpr::Clone(Box::new(expr))); - } - if self.consume(TokenKind::Plus) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::Plus, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Minus) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Bang) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::LogicalNot, - expr: Box::new(expr), - }); - } - if self.consume(TokenKind::Tilde) { - let expr = self.parse_unary()?; - return Ok(EvalExpr::Unary { - op: EvalUnaryOp::BitNot, - expr: Box::new(expr), - }); - } - self.parse_instanceof() - } - - /// Returns the scalar cast target represented by the current `(type)` token window. - fn peek_scalar_cast_type(&self) -> Option { - if !matches!(self.current(), TokenKind::LParen) { - return None; - } - let Some(TokenKind::Ident(name)) = self.tokens.get(self.pos + 1) else { - return None; - }; - if !matches!(self.tokens.get(self.pos + 2), Some(TokenKind::RParen)) { - return None; - } - if ident_eq(name, "int") || ident_eq(name, "integer") { - Some(EvalCastType::Int) - } else if ident_eq(name, "float") || ident_eq(name, "double") || ident_eq(name, "real") { - Some(EvalCastType::Float) - } else if ident_eq(name, "string") { - Some(EvalCastType::String) - } else if ident_eq(name, "bool") || ident_eq(name, "boolean") { - Some(EvalCastType::Bool) - } else { - None - } - } - - /// Parses left-associative `instanceof` with PHP's high operator precedence. - pub(super) fn parse_instanceof(&mut self) -> Result { - let mut expr = self.parse_power()?; - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "instanceof")) { - self.advance(); - let target = self.parse_instanceof_target()?; - expr = EvalExpr::InstanceOf { - value: Box::new(expr), - target, - }; - } - Ok(expr) - } - - /// Parses a static or dynamic target after PHP's `instanceof` operator. - pub(super) fn parse_instanceof_target( - &mut self, - ) -> Result { - if self.consume(TokenKind::LParen) { - let expr = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - return Ok(EvalInstanceOfTarget::Expr(Box::new(expr))); - } - if matches!(self.current(), TokenKind::DollarIdent(_)) { - let target = self.parse_instanceof_variable_target()?; - return Ok(EvalInstanceOfTarget::Expr(Box::new(target))); - } - let name = self.parse_class_reference_name(true)?; - let class_name = self.resolve_static_class_name(name); - if self.consume(TokenKind::DoubleColon) { - let TokenKind::DollarIdent(property) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let property = property.clone(); - self.advance(); - return Ok(EvalInstanceOfTarget::Expr(Box::new( - EvalExpr::StaticPropertyGet { - class_name, - property, - }, - ))); - } - Ok(EvalInstanceOfTarget::ClassName(class_name)) - } - - /// Parses PHP's unparenthesized dynamic `instanceof` variable/property/array target. - pub(super) fn parse_instanceof_variable_target(&mut self) -> Result { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let mut expr = EvalExpr::LoadVar(name.clone()); - self.advance(); - loop { - if matches!(self.current(), TokenKind::LBracket) - && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::RBracket)) - { - break; - } - if self.consume(TokenKind::LBracket) { - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - expr = EvalExpr::ArrayGet { - array: Box::new(expr), - index: Box::new(index), - }; - continue; - } - if self.consume(TokenKind::Arrow) { - let TokenKind::Ident(member) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let member = member.clone(); - self.advance(); - if matches!(self.current(), TokenKind::LParen) { - return Err(EvalParseError::UnexpectedToken); - } - expr = EvalExpr::PropertyGet { - object: Box::new(expr), - property: member, - }; - continue; - } - break; - } - Ok(expr) - } - - /// Parses right-associative exponentiation with higher precedence than unary prefix operators. - pub(super) fn parse_power(&mut self) -> Result { - let mut expr = self.parse_postfix()?; - if self.consume(TokenKind::StarStar) { - let right = self.parse_unary()?; - expr = EvalExpr::Binary { - op: EvalBinOp::Pow, - left: Box::new(expr), - right: Box::new(right), - }; - } - Ok(expr) - } - - /// Parses postfix array reads, property reads, method calls, and dynamic calls. - pub(super) fn parse_postfix(&mut self) -> Result { - let mut expr = self.parse_primary()?; - loop { - if matches!(self.current(), TokenKind::LParen) { - if self.consume_first_class_callable_marker() { - expr = Self::invokable_callable_expr(expr); - continue; - } - let args = self.parse_call_args()?; - expr = EvalExpr::DynamicCall { - callee: Box::new(expr), - args, - }; - continue; - } - if matches!(self.current(), TokenKind::LBracket) - && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::RBracket)) - { - break; - } - if self.consume(TokenKind::LBracket) { - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - expr = EvalExpr::ArrayGet { - array: Box::new(expr), - index: Box::new(index), - }; - continue; - } - if self.consume(TokenKind::DoubleColon) { - expr = self.parse_dynamic_static_member_expr(expr)?; - continue; - } - let nullsafe = if self.consume(TokenKind::Arrow) { - false - } else if self.consume(TokenKind::QuestionArrow) { - true - } else { - break; - }; - expr = self.parse_object_member_postfix(expr, nullsafe)?; - continue; - } - Ok(expr) - } - - /// Parses the member name after `->` or `?->` and builds the corresponding postfix expression. - fn parse_object_member_postfix( - &mut self, - object: EvalExpr, - nullsafe: bool, - ) -> Result { - match self.current() { - TokenKind::Ident(member) => { - let member = member.clone(); - self.advance(); - self.parse_named_object_member_postfix(object, member, nullsafe) - } - TokenKind::DollarIdent(name) => { - let member = EvalExpr::LoadVar(name.clone()); - self.advance(); - self.parse_dynamic_object_member_postfix(object, member, nullsafe) - } - TokenKind::LBrace => { - self.advance(); - let member = self.parse_expr()?; - self.expect(TokenKind::RBrace)?; - self.parse_dynamic_object_member_postfix(object, member, nullsafe) - } - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Builds a static-name property read or method call after parsing the member name. - fn parse_named_object_member_postfix( - &mut self, - object: EvalExpr, - member: String, - nullsafe: bool, - ) -> Result { - if matches!(self.current(), TokenKind::LParen) { - if self.consume_first_class_callable_marker() { - if nullsafe { - return Err(EvalParseError::UnsupportedConstruct); - } - return Ok(Self::method_callable_expr( - object, - EvalExpr::Const(EvalConst::String(member)), - )); - } - let args = self.parse_call_args()?; - return Ok(if nullsafe { - EvalExpr::NullsafeMethodCall { - object: Box::new(object), - method: member, - args, - } - } else { - EvalExpr::MethodCall { - object: Box::new(object), - method: member, - args, - } - }); - } - Ok(if nullsafe { - EvalExpr::NullsafePropertyGet { - object: Box::new(object), - property: member, - } - } else { - EvalExpr::PropertyGet { - object: Box::new(object), - property: member, - } - }) - } - - /// Builds a runtime-name property read or method call after parsing the member expression. - fn parse_dynamic_object_member_postfix( - &mut self, - object: EvalExpr, - member: EvalExpr, - nullsafe: bool, - ) -> Result { - if matches!(self.current(), TokenKind::LParen) { - if self.consume_first_class_callable_marker() { - if nullsafe { - return Err(EvalParseError::UnsupportedConstruct); - } - return Ok(Self::method_callable_expr(object, member)); - } - let args = self.parse_call_args()?; - return Ok(if nullsafe { - EvalExpr::NullsafeDynamicMethodCall { - object: Box::new(object), - method: Box::new(member), - args, - } - } else { - EvalExpr::DynamicMethodCall { - object: Box::new(object), - method: Box::new(member), - args, - } - }); - } - Ok(if nullsafe { - EvalExpr::NullsafeDynamicPropertyGet { - object: Box::new(object), - property: Box::new(member), - } - } else { - EvalExpr::DynamicPropertyGet { - object: Box::new(object), - property: Box::new(member), - } - }) - } - - /// Parses primary expressions supported by the initial eval subset. - pub(super) fn parse_primary(&mut self) -> Result { - match self.current() { - TokenKind::Int(value) => { - let value = *value; - self.advance(); - Ok(EvalExpr::Const(EvalConst::Int(value))) - } - TokenKind::Float(value) => { - let value = *value; - self.advance(); - Ok(EvalExpr::Const(EvalConst::Float(value))) - } - TokenKind::String(value) => { - let value = value.clone(); - self.advance(); - Ok(EvalExpr::Const(EvalConst::String(value))) - } - TokenKind::DollarIdent(name) => { - let name = name.clone(); - self.advance(); - let expr = EvalExpr::LoadVar(name); - if self.consume(TokenKind::DoubleColon) { - self.parse_dynamic_static_member_expr(expr) - } else { - Ok(expr) - } - } - TokenKind::Magic(EvalMagicConst::Namespace) => { - let namespace = self.namespace.clone(); - self.advance(); - Ok(EvalExpr::Const(EvalConst::String(namespace))) - } - TokenKind::Magic(magic) => { - let magic = magic.clone(); - self.advance(); - Ok(EvalExpr::Magic(magic)) - } - TokenKind::Ident(name) if ident_eq(name, "null") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Null)) - } - TokenKind::Ident(name) if ident_eq(name, "true") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Bool(true))) - } - TokenKind::Ident(name) if ident_eq(name, "false") => { - self.advance(); - Ok(EvalExpr::Const(EvalConst::Bool(false))) - } - TokenKind::Ident(name) if ident_eq(name, "print") => { - self.advance(); - let expr = self.parse_expr()?; - Ok(EvalExpr::Print(Box::new(expr))) - } - TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_closure_expr(false), - TokenKind::Ident(name) - if ident_eq(name, "static") - && matches!(self.peek(), TokenKind::Ident(next) if ident_eq(next, "function")) => - { - self.parse_closure_expr(true) - } - TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { - self.parse_legacy_array_literal() - } - TokenKind::Ident(name) if is_include_construct_name(name) => self.parse_include_expr(), - TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), - TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), - TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { - Err(EvalParseError::UnsupportedConstruct) - } - TokenKind::Backslash => self.parse_qualified_name_expr(), - TokenKind::Ident(_) - if matches!(self.peek(), TokenKind::Backslash | TokenKind::DoubleColon) => - { - self.parse_qualified_name_expr() - } - TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { - self.parse_call_expr(name.clone()) - } - TokenKind::Ident(name) => { - let name = name.clone(); - self.advance(); - Ok(self.const_fetch_expr(name)) - } - TokenKind::LBracket => self.parse_array_literal(), - TokenKind::LParen => { - self.advance(); - let expr = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - Ok(expr) - } - TokenKind::Eof => Err(EvalParseError::UnexpectedEof), - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Parses PHP include/require expression constructs and their path expression. - pub(super) fn parse_include_expr(&mut self) -> Result { - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let required = ident_eq(name, "require") || ident_eq(name, "require_once"); - let once = ident_eq(name, "include_once") || ident_eq(name, "require_once"); - self.advance(); - let path = if self.consume(TokenKind::LParen) { - let path = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - path - } else { - self.parse_expr()? - }; - Ok(EvalExpr::Include { - path: Box::new(path), - required, - once, - }) - } - - /// Parses `match (expr) { pattern, other => value, default => fallback }`. - pub(super) fn parse_match_expr(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - let subject = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect(TokenKind::LBrace)?; - - let mut arms = Vec::new(); - let mut default = None; - while !self.consume(TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { - self.advance(); - self.expect(TokenKind::FatArrow)?; - default = Some(Box::new(self.parse_expr()?)); - } else { - arms.push(self.parse_match_arm()?); - } - if self.consume(TokenKind::Comma) { - continue; - } - self.expect(TokenKind::RBrace)?; - break; - } - - Ok(EvalExpr::Match { - subject: Box::new(subject), - arms, - default, - }) - } - - /// Parses one non-default `match` arm and its comma-separated pattern list. - pub(super) fn parse_match_arm(&mut self) -> Result { - let mut patterns = Vec::new(); - loop { - patterns.push(self.parse_expr()?); - if !self.consume(TokenKind::Comma) { - break; - } - if matches!(self.current(), TokenKind::FatArrow) { - return Err(EvalParseError::UnexpectedToken); - } - if matches!(self.current(), TokenKind::Eof | TokenKind::RBrace) { - return Err(EvalParseError::UnexpectedToken); - } - } - self.expect(TokenKind::FatArrow)?; - let value = self.parse_expr()?; - Ok(EvalMatchArm { patterns, value }) - } - - /// Parses a function-like call expression and its source-order arguments. - pub(super) fn parse_call_expr(&mut self, name: String) -> Result { - self.advance(); - if self.consume_first_class_callable_marker() { - return Ok(self.function_callable_expr(name)); - } - let args = self.parse_call_args()?; - Ok(self.call_expr(name, args)) - } - - /// Parses an explicitly qualified call or constant-fetch expression. - pub(super) fn parse_qualified_name_expr(&mut self) -> Result { - let name = self.parse_qualified_name()?; - if self.consume(TokenKind::DoubleColon) { - let class_name = self.resolve_static_class_name(name); - return self.parse_static_member_expr(class_name); - } - let name = self.resolve_qualified_name(name); - if matches!(self.current(), TokenKind::LParen) { - if self.consume_first_class_callable_marker() { - return Ok(Self::function_callable_value(name.to_ascii_lowercase(), None)); - } - let args = self.parse_call_args()?; - return Ok(EvalExpr::Call { - name: name.to_ascii_lowercase(), - args, - }); - } - Ok(EvalExpr::ConstFetch(name)) - } - - /// Parses `Class::$property` and `Class::method(...)` expressions. - pub(super) fn parse_static_member_expr( - &mut self, - class_name: String, - ) -> Result { - match self.current() { - TokenKind::DollarIdent(property) => { - let property = property.clone(); - self.advance(); - if matches!(self.current(), TokenKind::LParen) { - if self.consume_first_class_callable_marker() { - return Ok(EvalExpr::StaticMethodCallable { - class_name, - method: Box::new(EvalExpr::LoadVar(property)), - }); - } - let args = self.parse_call_args()?; - return Ok(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), - method: Box::new(EvalExpr::LoadVar(property)), - args, - }); - } - Ok(EvalExpr::StaticPropertyGet { - class_name, - property, - }) - } - TokenKind::DollarLBrace => { - self.advance(); - let property = self.parse_expr()?; - self.expect(TokenKind::RBrace)?; - Ok(EvalExpr::DynamicStaticPropertyNameGet { - class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), - property: Box::new(property), - }) - } - TokenKind::Ident(name) - if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => - { - self.advance(); - Ok(EvalExpr::ClassNameFetch { class_name }) - } - TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { - let method = method.clone(); - self.advance(); - if self.consume_first_class_callable_marker() { - return Ok(EvalExpr::StaticMethodCallable { - class_name, - method: Box::new(EvalExpr::Const(EvalConst::String(method))), - }); - } - let args = self.parse_call_args()?; - Ok(EvalExpr::StaticMethodCall { - class_name, - method, - args, - }) - } - TokenKind::LBrace => { - self.advance(); - let member = self.parse_expr()?; - self.expect(TokenKind::RBrace)?; - if matches!(self.current(), TokenKind::LParen) { - if self.consume_first_class_callable_marker() { - return Ok(EvalExpr::StaticMethodCallable { - class_name, - method: Box::new(member), - }); - } - let args = self.parse_call_args()?; - return Ok(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), - method: Box::new(member), - args, - }); - } - Ok(EvalExpr::DynamicClassConstantNameFetch { - class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), - constant: Box::new(member), - }) - } - TokenKind::Ident(constant) => { - let constant = constant.clone(); - self.advance(); - Ok(EvalExpr::ClassConstantFetch { - class_name, - constant, - }) - } - _ => Err(EvalParseError::UnsupportedConstruct), - } - } - - /// Parses `$class::member` expressions whose static receiver is runtime-valued. - fn parse_dynamic_static_member_expr( - &mut self, - class_name: EvalExpr, - ) -> Result { - match self.current() { - TokenKind::DollarIdent(member) => { - let member = member.clone(); - self.advance(); - if matches!(self.current(), TokenKind::LParen) { - if self.consume_first_class_callable_marker() { - return Ok(Self::dynamic_static_method_callable_expr( - class_name, - EvalExpr::LoadVar(member), - )); - } - let args = self.parse_call_args()?; - return Ok(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(class_name), - method: Box::new(EvalExpr::LoadVar(member)), - args, - }); - } - Ok(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(class_name), - property: member, - }) - } - TokenKind::DollarLBrace => { - self.advance(); - let property = self.parse_expr()?; - self.expect(TokenKind::RBrace)?; - Ok(EvalExpr::DynamicStaticPropertyNameGet { - class_name: Box::new(class_name), - property: Box::new(property), - }) - } - TokenKind::Ident(name) - if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => - { - self.advance(); - Ok(EvalExpr::DynamicClassNameFetch { - class_name: Box::new(class_name), - }) - } - TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { - let method = method.clone(); - self.advance(); - if self.consume_first_class_callable_marker() { - return Ok(Self::dynamic_static_method_callable_expr( - class_name, - EvalExpr::Const(EvalConst::String(method)), - )); - } - let args = self.parse_call_args()?; - Ok(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(class_name), - method: Box::new(EvalExpr::Const(EvalConst::String(method))), - args, - }) - } - TokenKind::LBrace => { - self.advance(); - let member = self.parse_expr()?; - self.expect(TokenKind::RBrace)?; - if matches!(self.current(), TokenKind::LParen) { - if self.consume_first_class_callable_marker() { - return Ok(Self::dynamic_static_method_callable_expr(class_name, member)); - } - let args = self.parse_call_args()?; - return Ok(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(class_name), - method: Box::new(member), - args, - }); - } - Ok(EvalExpr::DynamicClassConstantNameFetch { - class_name: Box::new(class_name), - constant: Box::new(member), - }) - } - TokenKind::Ident(constant) => { - let constant = constant.clone(); - self.advance(); - Ok(EvalExpr::DynamicClassConstantFetch { - class_name: Box::new(class_name), - constant, - }) - } - _ => Err(EvalParseError::UnsupportedConstruct), - } - } - - /// Parses `new ClassName(...)` and anonymous `new class {}` expressions in eval fragments. - pub(super) fn parse_new_object_expr(&mut self) -> Result { - self.advance(); - let is_readonly_anonymous = matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "readonly")) - && matches!(self.peek(), TokenKind::Ident(name) if ident_eq(name, "class")); - if is_readonly_anonymous { - self.advance(); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { - return self.parse_anonymous_class_expr(is_readonly_anonymous); - } - if let TokenKind::DollarIdent(name) = self.current() { - let class_name = EvalExpr::LoadVar(name.clone()); - self.advance(); - let args = self.parse_optional_constructor_args()?; - return Ok(EvalExpr::DynamicNewObject { - class_name: Box::new(class_name), - args, - }); - } - if self.consume(TokenKind::LParen) { - let class_name = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - let args = self.parse_optional_constructor_args()?; - return Ok(EvalExpr::DynamicNewObject { - class_name: Box::new(class_name), - args, - }); - } - let class_name = self.parse_class_reference_name(true)?; - let class_name = self.resolve_static_class_name(class_name); - let args = self.parse_optional_constructor_args()?; - Ok(EvalExpr::NewObject { class_name, args }) - } - - /// Parses an optional constructor argument list after `new` class targets. - fn parse_optional_constructor_args(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::LParen) { - self.parse_call_args() - } else { - Ok(Vec::new()) - } - } - - /// Parses a simple or explicitly qualified PHP name. - pub(super) fn parse_qualified_name(&mut self) -> Result { - let absolute = self.consume(TokenKind::Backslash); - let TokenKind::Ident(first) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let mut name = first.clone(); - self.advance(); - while self.consume(TokenKind::Backslash) { - let TokenKind::Ident(part) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - name.push('\\'); - name.push_str(part); - self.advance(); - } - Ok(ParsedQualifiedName { name, absolute }) - } - - /// Parses a class-like reference name while rejecting PHP-reserved unqualified names. - pub(super) fn parse_class_reference_name( - &mut self, - allow_relative_keywords: bool, - ) -> Result { - let name = self.parse_qualified_name()?; - if self.class_reference_name_is_reserved(&name, allow_relative_keywords) { - return Err(EvalParseError::UnsupportedConstruct); - } - Ok(name) - } - - /// Returns whether a parsed class-like reference uses a PHP-reserved bare name. - pub(super) fn class_reference_name_is_reserved( - &self, - name: &ParsedQualifiedName, - allow_relative_keywords: bool, - ) -> bool { - if name.absolute || name.name.contains('\\') { - return false; - } - if allow_relative_keywords - && ["self", "parent", "static"] - .iter() - .any(|keyword| ident_eq(&name.name, keyword)) - { - return false; - } - is_reserved_class_like_name(&name.name) - } - - /// Resolves a class name used before `::`, preserving PHP relative class keywords. - pub(super) fn resolve_static_class_name(&self, name: ParsedQualifiedName) -> String { - if !name.absolute - && ["self", "parent", "static"] - .iter() - .any(|keyword| ident_eq(&name.name, keyword)) - { - return name.name.to_ascii_lowercase(); - } - self.resolve_class_name(name) - } - - /// Builds a call expression, adding namespace fallback for unqualified names. - pub(super) fn call_expr(&self, name: String, args: Vec) -> EvalExpr { - if let Some(imported) = self.imports.resolve_function(&name) { - return EvalExpr::Call { - name: imported.to_ascii_lowercase(), - args, - }; - } - let fallback_name = name.to_ascii_lowercase(); - if self.namespace.is_empty() { - EvalExpr::Call { - name: fallback_name, - args, - } - } else { - EvalExpr::NamespacedCall { - name: self - .qualify_name_in_current_namespace(&name) - .to_ascii_lowercase(), - fallback_name, - args, - } - } - } - - /// Builds a constant fetch expression, adding namespace fallback for unqualified names. - pub(super) fn const_fetch_expr(&self, name: String) -> EvalExpr { - if let Some(imported) = self.imports.resolve_constant(&name) { - return EvalExpr::ConstFetch(imported.to_string()); - } - if self.namespace.is_empty() { - EvalExpr::ConstFetch(name) - } else { - EvalExpr::NamespacedConstFetch { - name: self.qualify_name_in_current_namespace(&name), - fallback_name: name, - } - } - } - - /// Prefixes a name with the parser's current namespace when one is active. - pub(super) fn qualify_name_in_current_namespace(&self, name: &str) -> String { - if self.namespace.is_empty() { - name.to_string() - } else { - format!("{}\\{}", self.namespace, name) - } - } - - /// Resolves a class name through active imports before namespace qualification. - pub(super) fn resolve_class_name(&self, name: ParsedQualifiedName) -> String { - if name.absolute { - return name.name; - } - if let Some(imported) = self.imports.resolve_class(&name.name) { - return imported; - } - self.resolve_qualified_name(name) - } - - /// Resolves a parsed PHP name according to the current namespace. - pub(super) fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { - if name.absolute || self.namespace.is_empty() { - name.name - } else { - self.qualify_name_in_current_namespace(&name.name) - } - } - - /// Parses a parenthesized source-order argument list. - pub(super) fn parse_call_args(&mut self) -> Result, EvalParseError> { - self.expect(TokenKind::LParen)?; - let mut args = Vec::new(); - if self.consume(TokenKind::RParen) { - return Ok(args); - } - loop { - args.push(self.parse_call_arg()?); - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(TokenKind::RParen) { - return Ok(args); - } - } - self.expect(TokenKind::RParen)?; - Ok(args) - } - - /// Parses one positional or named argument within a call argument list. - pub(super) fn parse_call_arg(&mut self) -> Result { - if self.consume(TokenKind::Ellipsis) { - return self.parse_expr().map(EvalCallArg::spread); - } - if matches!(self.peek(), TokenKind::Colon) { - if let TokenKind::Ident(name) = self.current() { - let name = name.clone(); - self.advance(); - self.expect(TokenKind::Colon)?; - let value = self.parse_expr()?; - return Ok(EvalCallArg::named(name, value)); - } - } - self.parse_expr().map(EvalCallArg::positional) - } - - /// Consumes PHP's `(...)` first-class callable marker when it is the whole argument list. - fn consume_first_class_callable_marker(&mut self) -> bool { - if matches!(self.current(), TokenKind::LParen) - && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::Ellipsis)) - && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::RParen)) - { - self.advance(); - self.advance(); - self.advance(); - true - } else { - false - } - } - - /// Builds an eval function-callable expression with namespace fallback metadata. - fn function_callable_expr(&self, name: String) -> EvalExpr { - if let Some(imported) = self.imports.resolve_function(&name) { - return Self::function_callable_value(imported.to_ascii_lowercase(), None); - } - let fallback_name = name.to_ascii_lowercase(); - if self.namespace.is_empty() { - Self::function_callable_value(fallback_name, None) - } else { - Self::function_callable_value( - self.qualify_name_in_current_namespace(&name) - .to_ascii_lowercase(), - Some(fallback_name), - ) - } - } - - /// Builds the EvalIR node that resolves a first-class function callable at runtime. - fn function_callable_value(name: String, fallback_name: Option) -> EvalExpr { - EvalExpr::FunctionCallable { - name, - fallback_name, - } - } - - /// Builds the EvalIR node used for object method first-class callables. - fn method_callable_expr(object: EvalExpr, method: EvalExpr) -> EvalExpr { - EvalExpr::MethodCallable { - object: Box::new(object), - method: Box::new(method), - } - } - - /// Builds the EvalIR node used for invokable-object first-class callables. - fn invokable_callable_expr(object: EvalExpr) -> EvalExpr { - EvalExpr::InvokableCallable { - object: Box::new(object), - } - } - - /// Builds the EvalIR node used for runtime-class static first-class callables. - fn dynamic_static_method_callable_expr(class_name: EvalExpr, method: EvalExpr) -> EvalExpr { - EvalExpr::DynamicStaticMethodCallable { - class_name: Box::new(class_name), - method: Box::new(method), - } - } - - /// Parses an anonymous function expression into a runtime eval closure payload. - fn parse_closure_expr(&mut self, is_static: bool) -> Result { - let source_start_line = self.current_line(); - if is_static { - self.advance(); - } - self.advance(); - self.expect(TokenKind::LParen)?; - let ParsedMethodParams { - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - promoted_properties, - promoted_assignments, - } = self.parse_method_params("", false)?; - if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { - return Err(EvalParseError::UnsupportedConstruct); - } - let captures = self.parse_optional_closure_use_captures(¶ms)?; - let return_type = self.parse_optional_return_type(EvalTypePosition::FunctionReturn)?; - let (body, source_end_line) = self.parse_block_with_end_line()?; - let function = EvalFunction::new(next_closure_function_name(), params, body) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) - .with_parameter_attributes(parameter_attributes) - .with_parameter_types(parameter_types) - .with_parameter_defaults(parameter_defaults) - .with_parameter_by_ref_flags(parameter_is_by_ref) - .with_parameter_variadic_flags(parameter_is_variadic) - .with_return_type(return_type); - Ok(EvalExpr::Closure { - function, - captures, - is_static, - }) - } - - /// Parses an optional closure `use (...)` capture list. - fn parse_optional_closure_use_captures( - &mut self, - params: &[String], - ) -> Result, EvalParseError> { - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) { - return Ok(Vec::new()); - } - self.advance(); - self.expect(TokenKind::LParen)?; - if self.consume(TokenKind::RParen) { - return Ok(Vec::new()); - } - let mut captures = Vec::new(); - loop { - let by_ref = self.consume(TokenKind::Ampersand); - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - if params.iter().any(|param| param == name) - || captures - .iter() - .any(|capture: &EvalClosureCapture| capture.name() == name) - { - return Err(EvalParseError::UnsupportedConstruct); - } - captures.push(EvalClosureCapture::new(name.clone(), by_ref)); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - if matches!(self.current(), TokenKind::RParen) { - return Err(EvalParseError::ExpectedVariable); - } - } - self.expect(TokenKind::RParen)?; - Ok(captures) - } - - /// Parses an array literal with source-order optional key/value element expressions. - pub(super) fn parse_array_literal(&mut self) -> Result { - self.expect(TokenKind::LBracket)?; - self.parse_array_elements_until(TokenKind::RBracket) - } - - /// Parses PHP's legacy `array(...)` literal into the same EvalIR node as `[...]`. - pub(super) fn parse_legacy_array_literal(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - self.parse_array_elements_until(TokenKind::RParen) - } - - /// Returns whether the current token starts PHP's legacy `array(...)` literal syntax. - pub(super) fn current_starts_legacy_array_literal(&self) -> bool { - matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "array")) - && matches!(self.peek(), TokenKind::LParen) - } - - /// Parses comma-separated array elements until the supplied closing delimiter. - pub(super) fn parse_array_elements_until( - &mut self, - close: TokenKind, - ) -> Result { - let mut elements = Vec::new(); - if self.consume(close.clone()) { - return Ok(EvalExpr::Array(elements)); - } - loop { - if self.consume(TokenKind::Ampersand) { - let value = self.parse_expr()?; - elements.push(EvalArrayElement::Reference(value)); - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(close.clone()) { - return Ok(EvalExpr::Array(elements)); - } - continue; - } - let first = self.parse_expr()?; - if self.consume(TokenKind::FatArrow) { - if self.consume(TokenKind::Ampersand) { - let value = self.parse_expr()?; - elements.push(EvalArrayElement::KeyReference { - key: first, - value, - }); - } else { - let value = self.parse_expr()?; - elements.push(EvalArrayElement::KeyValue { key: first, value }); - } - } else { - elements.push(EvalArrayElement::Value(first)); - } - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(close.clone()) { - return Ok(EvalExpr::Array(elements)); - } - } - self.expect(close)?; - Ok(EvalExpr::Array(elements)) - } -} +mod callables_arrays; +mod postfix; +mod precedence; +mod primary; +mod static_names; diff --git a/crates/elephc-magician/src/parser/expressions/callables_arrays.rs b/crates/elephc-magician/src/parser/expressions/callables_arrays.rs new file mode 100644 index 0000000000..00caae4421 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/callables_arrays.rs @@ -0,0 +1,258 @@ +//! Purpose: +//! Parses call arguments, first-class callable markers, closures/captures, and +//! modern or legacy array literals. +//! +//! Called from: +//! - Primary, postfix, static-member, and object-construction expression parsing. +//! +//! Key details: +//! - Source-order arguments, spread/named syntax, and closure captures remain intact. + +use super::*; + +impl Parser { + + /// Parses a parenthesized source-order argument list. + pub(in crate::parser) fn parse_call_args(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LParen)?; + let mut args = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(args); + } + loop { + args.push(self.parse_call_arg()?); + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RParen) { + return Ok(args); + } + } + self.expect(TokenKind::RParen)?; + Ok(args) + } + + /// Parses one positional or named argument within a call argument list. + pub(in crate::parser) fn parse_call_arg(&mut self) -> Result { + if self.consume(TokenKind::Ellipsis) { + return self.parse_expr().map(EvalCallArg::spread); + } + if matches!(self.peek(), TokenKind::Colon) { + if let TokenKind::Ident(name) = self.current() { + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Colon)?; + let value = self.parse_expr()?; + return Ok(EvalCallArg::named(name, value)); + } + } + self.parse_expr().map(EvalCallArg::positional) + } + + /// Consumes PHP's `(...)` first-class callable marker when it is the whole argument list. + pub(super) fn consume_first_class_callable_marker(&mut self) -> bool { + if matches!(self.current(), TokenKind::LParen) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::Ellipsis)) + && matches!(self.tokens.get(self.pos + 2), Some(TokenKind::RParen)) + { + self.advance(); + self.advance(); + self.advance(); + true + } else { + false + } + } + + /// Builds an eval function-callable expression with namespace fallback metadata. + pub(super) fn function_callable_expr(&self, name: String) -> EvalExpr { + if let Some(imported) = self.imports.resolve_function(&name) { + return Self::function_callable_value(imported.to_ascii_lowercase(), None); + } + let fallback_name = name.to_ascii_lowercase(); + if self.namespace.is_empty() { + Self::function_callable_value(fallback_name, None) + } else { + Self::function_callable_value( + self.qualify_name_in_current_namespace(&name) + .to_ascii_lowercase(), + Some(fallback_name), + ) + } + } + + /// Builds the EvalIR node that resolves a first-class function callable at runtime. + pub(super) fn function_callable_value(name: String, fallback_name: Option) -> EvalExpr { + EvalExpr::FunctionCallable { + name, + fallback_name, + } + } + + /// Builds the EvalIR node used for object method first-class callables. + pub(super) fn method_callable_expr(object: EvalExpr, method: EvalExpr) -> EvalExpr { + EvalExpr::MethodCallable { + object: Box::new(object), + method: Box::new(method), + } + } + + /// Builds the EvalIR node used for invokable-object first-class callables. + pub(super) fn invokable_callable_expr(object: EvalExpr) -> EvalExpr { + EvalExpr::InvokableCallable { + object: Box::new(object), + } + } + + /// Builds the EvalIR node used for runtime-class static first-class callables. + pub(super) fn dynamic_static_method_callable_expr(class_name: EvalExpr, method: EvalExpr) -> EvalExpr { + EvalExpr::DynamicStaticMethodCallable { + class_name: Box::new(class_name), + method: Box::new(method), + } + } + + /// Parses an anonymous function expression into a runtime eval closure payload. + pub(super) fn parse_closure_expr(&mut self, is_static: bool) -> Result { + let source_start_line = self.current_line(); + if is_static { + self.advance(); + } + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params("", false)?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + let captures = self.parse_optional_closure_use_captures(¶ms)?; + let return_type = self.parse_optional_return_type(EvalTypePosition::FunctionReturn)?; + let (body, source_end_line) = self.parse_block_with_end_line()?; + let function = EvalFunction::new(next_closure_function_name(), params, body) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_parameter_attributes(parameter_attributes) + .with_parameter_types(parameter_types) + .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type); + Ok(EvalExpr::Closure { + function, + captures, + is_static, + }) + } + + /// Parses an optional closure `use (...)` capture list. + pub(super) fn parse_optional_closure_use_captures( + &mut self, + params: &[String], + ) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) { + return Ok(Vec::new()); + } + self.advance(); + self.expect(TokenKind::LParen)?; + if self.consume(TokenKind::RParen) { + return Ok(Vec::new()); + } + let mut captures = Vec::new(); + loop { + let by_ref = self.consume(TokenKind::Ampersand); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + if params.iter().any(|param| param == name) + || captures + .iter() + .any(|capture: &EvalClosureCapture| capture.name() == name) + { + return Err(EvalParseError::UnsupportedConstruct); + } + captures.push(EvalClosureCapture::new(name.clone(), by_ref)); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok(captures) + } + + /// Parses an array literal with source-order optional key/value element expressions. + pub(in crate::parser) fn parse_array_literal(&mut self) -> Result { + self.expect(TokenKind::LBracket)?; + self.parse_array_elements_until(TokenKind::RBracket) + } + + /// Parses PHP's legacy `array(...)` literal into the same EvalIR node as `[...]`. + pub(in crate::parser) fn parse_legacy_array_literal(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + self.parse_array_elements_until(TokenKind::RParen) + } + + /// Returns whether the current token starts PHP's legacy `array(...)` literal syntax. + pub(in crate::parser) fn current_starts_legacy_array_literal(&self) -> bool { + matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "array")) + && matches!(self.peek(), TokenKind::LParen) + } + + /// Parses comma-separated array elements until the supplied closing delimiter. + pub(in crate::parser) fn parse_array_elements_until( + &mut self, + close: TokenKind, + ) -> Result { + let mut elements = Vec::new(); + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + loop { + if self.consume(TokenKind::Ampersand) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::Reference(value)); + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + continue; + } + let first = self.parse_expr()?; + if self.consume(TokenKind::FatArrow) { + if self.consume(TokenKind::Ampersand) { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyReference { + key: first, + value, + }); + } else { + let value = self.parse_expr()?; + elements.push(EvalArrayElement::KeyValue { key: first, value }); + } + } else { + elements.push(EvalArrayElement::Value(first)); + } + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(close.clone()) { + return Ok(EvalExpr::Array(elements)); + } + } + self.expect(close)?; + Ok(EvalExpr::Array(elements)) + } +} diff --git a/crates/elephc-magician/src/parser/expressions/postfix.rs b/crates/elephc-magician/src/parser/expressions/postfix.rs new file mode 100644 index 0000000000..baf7064f07 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/postfix.rs @@ -0,0 +1,277 @@ +//! Purpose: +//! Parses `instanceof`, exponentiation, and postfix property, method, index, and +//! dynamic-member expression forms. +//! +//! Called from: +//! - The unary/precedence parser and primary-expression parser. +//! +//! Key details: +//! - Postfix operations preserve PHP chaining and dynamic member evaluation order. + +use super::*; + +impl Parser { + + /// Parses left-associative `instanceof` with PHP's high operator precedence. + pub(in crate::parser) fn parse_instanceof(&mut self) -> Result { + let mut expr = self.parse_power()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "instanceof")) { + self.advance(); + let target = self.parse_instanceof_target()?; + expr = EvalExpr::InstanceOf { + value: Box::new(expr), + target, + }; + } + Ok(expr) + } + + /// Parses a static or dynamic target after PHP's `instanceof` operator. + pub(in crate::parser) fn parse_instanceof_target( + &mut self, + ) -> Result { + if self.consume(TokenKind::LParen) { + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + return Ok(EvalInstanceOfTarget::Expr(Box::new(expr))); + } + if matches!(self.current(), TokenKind::DollarIdent(_)) { + let target = self.parse_instanceof_variable_target()?; + return Ok(EvalInstanceOfTarget::Expr(Box::new(target))); + } + let name = self.parse_class_reference_name(true)?; + let class_name = self.resolve_static_class_name(name); + if self.consume(TokenKind::DoubleColon) { + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let property = property.clone(); + self.advance(); + return Ok(EvalInstanceOfTarget::Expr(Box::new( + EvalExpr::StaticPropertyGet { + class_name, + property, + }, + ))); + } + Ok(EvalInstanceOfTarget::ClassName(class_name)) + } + + /// Parses PHP's unparenthesized dynamic `instanceof` variable/property/array target. + pub(in crate::parser) fn parse_instanceof_variable_target(&mut self) -> Result { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut expr = EvalExpr::LoadVar(name.clone()); + self.advance(); + loop { + if matches!(self.current(), TokenKind::LBracket) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::RBracket)) + { + break; + } + if self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + continue; + } + if self.consume(TokenKind::Arrow) { + let TokenKind::Ident(member) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + return Err(EvalParseError::UnexpectedToken); + } + expr = EvalExpr::PropertyGet { + object: Box::new(expr), + property: member, + }; + continue; + } + break; + } + Ok(expr) + } + + /// Parses right-associative exponentiation with higher precedence than unary prefix operators. + pub(in crate::parser) fn parse_power(&mut self) -> Result { + let mut expr = self.parse_postfix()?; + if self.consume(TokenKind::StarStar) { + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Pow, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses postfix array reads, property reads, method calls, and dynamic calls. + pub(in crate::parser) fn parse_postfix(&mut self) -> Result { + let mut expr = self.parse_primary()?; + loop { + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + expr = Self::invokable_callable_expr(expr); + continue; + } + let args = self.parse_call_args()?; + expr = EvalExpr::DynamicCall { + callee: Box::new(expr), + args, + }; + continue; + } + if matches!(self.current(), TokenKind::LBracket) + && matches!(self.tokens.get(self.pos + 1), Some(TokenKind::RBracket)) + { + break; + } + if self.consume(TokenKind::LBracket) { + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + expr = EvalExpr::ArrayGet { + array: Box::new(expr), + index: Box::new(index), + }; + continue; + } + if self.consume(TokenKind::DoubleColon) { + expr = self.parse_dynamic_static_member_expr(expr)?; + continue; + } + let nullsafe = if self.consume(TokenKind::Arrow) { + false + } else if self.consume(TokenKind::QuestionArrow) { + true + } else { + break; + }; + expr = self.parse_object_member_postfix(expr, nullsafe)?; + continue; + } + Ok(expr) + } + + /// Parses the member name after `->` or `?->` and builds the corresponding postfix expression. + pub(super) fn parse_object_member_postfix( + &mut self, + object: EvalExpr, + nullsafe: bool, + ) -> Result { + match self.current() { + TokenKind::Ident(member) => { + let member = member.clone(); + self.advance(); + self.parse_named_object_member_postfix(object, member, nullsafe) + } + TokenKind::DollarIdent(name) => { + let member = EvalExpr::LoadVar(name.clone()); + self.advance(); + self.parse_dynamic_object_member_postfix(object, member, nullsafe) + } + TokenKind::LBrace => { + self.advance(); + let member = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + self.parse_dynamic_object_member_postfix(object, member, nullsafe) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Builds a static-name property read or method call after parsing the member name. + pub(super) fn parse_named_object_member_postfix( + &mut self, + object: EvalExpr, + member: String, + nullsafe: bool, + ) -> Result { + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + if nullsafe { + return Err(EvalParseError::UnsupportedConstruct); + } + return Ok(Self::method_callable_expr( + object, + EvalExpr::Const(EvalConst::String(member)), + )); + } + let args = self.parse_call_args()?; + return Ok(if nullsafe { + EvalExpr::NullsafeMethodCall { + object: Box::new(object), + method: member, + args, + } + } else { + EvalExpr::MethodCall { + object: Box::new(object), + method: member, + args, + } + }); + } + Ok(if nullsafe { + EvalExpr::NullsafePropertyGet { + object: Box::new(object), + property: member, + } + } else { + EvalExpr::PropertyGet { + object: Box::new(object), + property: member, + } + }) + } + + /// Builds a runtime-name property read or method call after parsing the member expression. + pub(super) fn parse_dynamic_object_member_postfix( + &mut self, + object: EvalExpr, + member: EvalExpr, + nullsafe: bool, + ) -> Result { + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + if nullsafe { + return Err(EvalParseError::UnsupportedConstruct); + } + return Ok(Self::method_callable_expr(object, member)); + } + let args = self.parse_call_args()?; + return Ok(if nullsafe { + EvalExpr::NullsafeDynamicMethodCall { + object: Box::new(object), + method: Box::new(member), + args, + } + } else { + EvalExpr::DynamicMethodCall { + object: Box::new(object), + method: Box::new(member), + args, + } + }); + } + Ok(if nullsafe { + EvalExpr::NullsafeDynamicPropertyGet { + object: Box::new(object), + property: Box::new(member), + } + } else { + EvalExpr::DynamicPropertyGet { + object: Box::new(object), + property: Box::new(member), + } + }) + } + +} diff --git a/crates/elephc-magician/src/parser/expressions/precedence.rs b/crates/elephc-magician/src/parser/expressions/precedence.rs new file mode 100644 index 0000000000..ae46fdf79c --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/precedence.rs @@ -0,0 +1,372 @@ +//! Purpose: +//! Parses PHP expression precedence from keyword logical operators through +//! unary operators and scalar casts. +//! +//! Called from: +//! - `Parser::parse_expr()` and the next tighter parser precedence layer. +//! +//! Key details: +//! - Ternary, coalesce, exponentiation handoff, and PHP keyword precedence are +//! kept explicit. + +use super::*; + +impl Parser { + /// Parses an expression using PHP-like logical, comparison, concatenation, and arithmetic precedence. + pub(in crate::parser) fn parse_expr(&mut self) -> Result { + self.parse_keyword_or() + } + + /// Parses PHP keyword `or`, whose precedence is lower than `xor`, `and`, and ternary. + pub(in crate::parser) fn parse_keyword_or(&mut self) -> Result { + let mut expr = self.parse_keyword_xor()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "or")) { + self.advance(); + let right = self.parse_keyword_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `xor`, whose operands are evaluated before boolean XOR. + pub(in crate::parser) fn parse_keyword_xor(&mut self) -> Result { + let mut expr = self.parse_keyword_and()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "xor")) { + self.advance(); + let right = self.parse_keyword_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP keyword `and`, whose precedence is lower than ternary and `&&`. + pub(in crate::parser) fn parse_keyword_and(&mut self) -> Result { + let mut expr = self.parse_ternary()?; + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "and")) { + self.advance(); + let right = self.parse_ternary()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses PHP ternary expressions, including the short `expr ?: fallback` form. + pub(in crate::parser) fn parse_ternary(&mut self) -> Result { + let condition = self.parse_null_coalesce()?; + if !self.consume(TokenKind::Question) { + return Ok(condition); + } + let then_branch = if self.consume(TokenKind::Colon) { + None + } else { + let expr = self.parse_expr()?; + self.expect(TokenKind::Colon)?; + Some(Box::new(expr)) + }; + let else_branch = self.parse_expr()?; + Ok(EvalExpr::Ternary { + condition: Box::new(condition), + then_branch, + else_branch: Box::new(else_branch), + }) + } + + /// Parses right-associative null coalescing below logical OR and above ternary. + pub(in crate::parser) fn parse_null_coalesce(&mut self) -> Result { + let value = self.parse_logical_or()?; + if !self.consume(TokenKind::QuestionQuestion) { + return Ok(value); + } + let default = self.parse_null_coalesce()?; + Ok(EvalExpr::NullCoalesce { + value: Box::new(value), + default: Box::new(default), + }) + } + + /// Parses left-associative logical OR with lower precedence than logical AND. + pub(in crate::parser) fn parse_logical_or(&mut self) -> Result { + let mut expr = self.parse_logical_and()?; + while self.consume(TokenKind::OrOr) { + let right = self.parse_logical_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative logical AND with lower precedence than equality. + pub(in crate::parser) fn parse_logical_and(&mut self) -> Result { + let mut expr = self.parse_bit_or()?; + while self.consume(TokenKind::AndAnd) { + let right = self.parse_bit_or()?; + expr = EvalExpr::Binary { + op: EvalBinOp::LogicalAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise OR with lower precedence than bitwise XOR. + pub(in crate::parser) fn parse_bit_or(&mut self) -> Result { + let mut expr = self.parse_bit_xor()?; + while self.consume(TokenKind::Pipe) { + let right = self.parse_bit_xor()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitOr, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise XOR with lower precedence than bitwise AND. + pub(in crate::parser) fn parse_bit_xor(&mut self) -> Result { + let mut expr = self.parse_bit_and()?; + while self.consume(TokenKind::Caret) { + let right = self.parse_bit_and()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitXor, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative bitwise AND with lower precedence than equality. + pub(in crate::parser) fn parse_bit_and(&mut self) -> Result { + let mut expr = self.parse_equality()?; + while self.consume(TokenKind::Ampersand) { + let right = self.parse_equality()?; + expr = EvalExpr::Binary { + op: EvalBinOp::BitAnd, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative equality and inequality comparisons. + pub(in crate::parser) fn parse_equality(&mut self) -> Result { + let mut expr = self.parse_ordering()?; + loop { + let op = if self.consume(TokenKind::EqualEqual) { + EvalBinOp::LooseEq + } else if self.consume(TokenKind::NotEqual) { + EvalBinOp::LooseNotEq + } else if self.consume(TokenKind::EqualEqualEqual) { + EvalBinOp::StrictEq + } else if self.consume(TokenKind::NotEqualEqual) { + EvalBinOp::StrictNotEq + } else { + break; + }; + let right = self.parse_ordering()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative ordered comparisons. + pub(in crate::parser) fn parse_ordering(&mut self) -> Result { + let mut expr = self.parse_shift()?; + loop { + let op = if self.consume(TokenKind::Less) { + EvalBinOp::Lt + } else if self.consume(TokenKind::LessEqual) { + EvalBinOp::LtEq + } else if self.consume(TokenKind::Greater) { + EvalBinOp::Gt + } else if self.consume(TokenKind::GreaterEqual) { + EvalBinOp::GtEq + } else if self.consume(TokenKind::Spaceship) { + EvalBinOp::Spaceship + } else { + break; + }; + let right = self.parse_shift()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative integer shift operators. + pub(in crate::parser) fn parse_shift(&mut self) -> Result { + let mut expr = self.parse_concat()?; + loop { + let op = if self.consume(TokenKind::LessLess) { + EvalBinOp::ShiftLeft + } else if self.consume(TokenKind::GreaterGreater) { + EvalBinOp::ShiftRight + } else { + break; + }; + let right = self.parse_concat()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative string concatenation. + pub(in crate::parser) fn parse_concat(&mut self) -> Result { + let mut expr = self.parse_add()?; + while self.consume(TokenKind::Dot) { + let right = self.parse_add()?; + expr = EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric addition and subtraction. + pub(in crate::parser) fn parse_add(&mut self) -> Result { + let mut expr = self.parse_mul()?; + loop { + let op = if self.consume(TokenKind::Plus) { + EvalBinOp::Add + } else if self.consume(TokenKind::Minus) { + EvalBinOp::Sub + } else { + break; + }; + let right = self.parse_mul()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses left-associative numeric multiplication, division, and modulo. + pub(in crate::parser) fn parse_mul(&mut self) -> Result { + let mut expr = self.parse_unary()?; + loop { + let op = if self.consume(TokenKind::Star) { + EvalBinOp::Mul + } else if self.consume(TokenKind::Slash) { + EvalBinOp::Div + } else if self.consume(TokenKind::Percent) { + EvalBinOp::Mod + } else { + break; + }; + let right = self.parse_unary()?; + expr = EvalExpr::Binary { + op, + left: Box::new(expr), + right: Box::new(right), + }; + } + Ok(expr) + } + + /// Parses right-associative unary prefix expressions. + pub(in crate::parser) fn parse_unary(&mut self) -> Result { + if let Some(target) = self.peek_scalar_cast_type() { + self.advance(); + self.advance(); + self.advance(); + let expr = self.parse_concat()?; + return Ok(EvalExpr::Cast { + target, + expr: Box::new(expr), + }); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "clone")) { + self.advance(); + let expr = self.parse_unary()?; + return Ok(EvalExpr::Clone(Box::new(expr))); + } + if self.consume(TokenKind::Plus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Plus, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Minus) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Bang) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::LogicalNot, + expr: Box::new(expr), + }); + } + if self.consume(TokenKind::Tilde) { + let expr = self.parse_unary()?; + return Ok(EvalExpr::Unary { + op: EvalUnaryOp::BitNot, + expr: Box::new(expr), + }); + } + self.parse_instanceof() + } + + /// Returns the scalar cast target represented by the current `(type)` token window. + pub(super) fn peek_scalar_cast_type(&self) -> Option { + if !matches!(self.current(), TokenKind::LParen) { + return None; + } + let Some(TokenKind::Ident(name)) = self.tokens.get(self.pos + 1) else { + return None; + }; + if !matches!(self.tokens.get(self.pos + 2), Some(TokenKind::RParen)) { + return None; + } + if ident_eq(name, "int") || ident_eq(name, "integer") { + Some(EvalCastType::Int) + } else if ident_eq(name, "float") || ident_eq(name, "double") || ident_eq(name, "real") { + Some(EvalCastType::Float) + } else if ident_eq(name, "string") { + Some(EvalCastType::String) + } else if ident_eq(name, "bool") || ident_eq(name, "boolean") { + Some(EvalCastType::Bool) + } else { + None + } + } + +} diff --git a/crates/elephc-magician/src/parser/expressions/primary.rs b/crates/elephc-magician/src/parser/expressions/primary.rs new file mode 100644 index 0000000000..3ee26b7924 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/primary.rs @@ -0,0 +1,220 @@ +//! Purpose: +//! Parses primary literals, variables, closures, includes, match expressions, +//! calls, and qualified-name constants. +//! +//! Called from: +//! - `Parser::parse_postfix()`. +//! +//! Key details: +//! - Token dispatch remains centralized for exhaustive primary syntax handling. + +use super::*; + +impl Parser { + + /// Parses primary expressions supported by the initial eval subset. + pub(in crate::parser) fn parse_primary(&mut self) -> Result { + match self.current() { + TokenKind::Int(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Int(value))) + } + TokenKind::Float(value) => { + let value = *value; + self.advance(); + Ok(EvalExpr::Const(EvalConst::Float(value))) + } + TokenKind::String(value) => { + let value = value.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(value))) + } + TokenKind::DollarIdent(name) => { + let name = name.clone(); + self.advance(); + let expr = EvalExpr::LoadVar(name); + if self.consume(TokenKind::DoubleColon) { + self.parse_dynamic_static_member_expr(expr) + } else { + Ok(expr) + } + } + TokenKind::Magic(EvalMagicConst::Namespace) => { + let namespace = self.namespace.clone(); + self.advance(); + Ok(EvalExpr::Const(EvalConst::String(namespace))) + } + TokenKind::Magic(magic) => { + let magic = magic.clone(); + self.advance(); + Ok(EvalExpr::Magic(magic)) + } + TokenKind::Ident(name) if ident_eq(name, "null") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Null)) + } + TokenKind::Ident(name) if ident_eq(name, "true") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(true))) + } + TokenKind::Ident(name) if ident_eq(name, "false") => { + self.advance(); + Ok(EvalExpr::Const(EvalConst::Bool(false))) + } + TokenKind::Ident(name) if ident_eq(name, "print") => { + self.advance(); + let expr = self.parse_expr()?; + Ok(EvalExpr::Print(Box::new(expr))) + } + TokenKind::Ident(name) if ident_eq(name, "function") => self.parse_closure_expr(false), + TokenKind::Ident(name) + if ident_eq(name, "static") + && matches!(self.peek(), TokenKind::Ident(next) if ident_eq(next, "function")) => + { + self.parse_closure_expr(true) + } + TokenKind::Ident(_) if self.current_starts_legacy_array_literal() => { + self.parse_legacy_array_literal() + } + TokenKind::Ident(name) if is_include_construct_name(name) => self.parse_include_expr(), + TokenKind::Ident(name) if ident_eq(name, "match") => self.parse_match_expr(), + TokenKind::Ident(name) if ident_eq(name, "new") => self.parse_new_object_expr(), + TokenKind::Ident(name) if is_unsupported_expression_keyword(name) => { + Err(EvalParseError::UnsupportedConstruct) + } + TokenKind::Backslash => self.parse_qualified_name_expr(), + TokenKind::Ident(_) + if matches!(self.peek(), TokenKind::Backslash | TokenKind::DoubleColon) => + { + self.parse_qualified_name_expr() + } + TokenKind::Ident(name) if matches!(self.peek(), TokenKind::LParen) => { + self.parse_call_expr(name.clone()) + } + TokenKind::Ident(name) => { + let name = name.clone(); + self.advance(); + Ok(self.const_fetch_expr(name)) + } + TokenKind::LBracket => self.parse_array_literal(), + TokenKind::LParen => { + self.advance(); + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + Ok(expr) + } + TokenKind::Eof => Err(EvalParseError::UnexpectedEof), + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Parses PHP include/require expression constructs and their path expression. + pub(in crate::parser) fn parse_include_expr(&mut self) -> Result { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let required = ident_eq(name, "require") || ident_eq(name, "require_once"); + let once = ident_eq(name, "include_once") || ident_eq(name, "require_once"); + self.advance(); + let path = if self.consume(TokenKind::LParen) { + let path = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + path + } else { + self.parse_expr()? + }; + Ok(EvalExpr::Include { + path: Box::new(path), + required, + once, + }) + } + + /// Parses `match (expr) { pattern, other => value, default => fallback }`. + pub(in crate::parser) fn parse_match_expr(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let subject = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + + let mut arms = Vec::new(); + let mut default = None; + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + self.expect(TokenKind::FatArrow)?; + default = Some(Box::new(self.parse_expr()?)); + } else { + arms.push(self.parse_match_arm()?); + } + if self.consume(TokenKind::Comma) { + continue; + } + self.expect(TokenKind::RBrace)?; + break; + } + + Ok(EvalExpr::Match { + subject: Box::new(subject), + arms, + default, + }) + } + + /// Parses one non-default `match` arm and its comma-separated pattern list. + pub(in crate::parser) fn parse_match_arm(&mut self) -> Result { + let mut patterns = Vec::new(); + loop { + patterns.push(self.parse_expr()?); + if !self.consume(TokenKind::Comma) { + break; + } + if matches!(self.current(), TokenKind::FatArrow) { + return Err(EvalParseError::UnexpectedToken); + } + if matches!(self.current(), TokenKind::Eof | TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + } + self.expect(TokenKind::FatArrow)?; + let value = self.parse_expr()?; + Ok(EvalMatchArm { patterns, value }) + } + + /// Parses a function-like call expression and its source-order arguments. + pub(in crate::parser) fn parse_call_expr(&mut self, name: String) -> Result { + self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(self.function_callable_expr(name)); + } + let args = self.parse_call_args()?; + Ok(self.call_expr(name, args)) + } + + /// Parses an explicitly qualified call or constant-fetch expression. + pub(in crate::parser) fn parse_qualified_name_expr(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if self.consume(TokenKind::DoubleColon) { + let class_name = self.resolve_static_class_name(name); + return self.parse_static_member_expr(class_name); + } + let name = self.resolve_qualified_name(name); + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::function_callable_value(name.to_ascii_lowercase(), None)); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::Call { + name: name.to_ascii_lowercase(), + args, + }); + } + Ok(EvalExpr::ConstFetch(name)) + } + +} diff --git a/crates/elephc-magician/src/parser/expressions/static_names.rs b/crates/elephc-magician/src/parser/expressions/static_names.rs new file mode 100644 index 0000000000..a19c3c33c5 --- /dev/null +++ b/crates/elephc-magician/src/parser/expressions/static_names.rs @@ -0,0 +1,377 @@ +//! Purpose: +//! Parses static member and object-construction expressions and resolves PHP +//! class, function, and constant names. +//! +//! Called from: +//! - Primary and postfix parsing for qualified names, `new`, and `Class::member`. +//! +//! Key details: +//! - Namespace/import fallback and reserved class-reference rules stay shared. + +use super::*; + +impl Parser { + + /// Parses `Class::$property` and `Class::method(...)` expressions. + pub(in crate::parser) fn parse_static_member_expr( + &mut self, + class_name: String, + ) -> Result { + match self.current() { + TokenKind::DollarIdent(property) => { + let property = property.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(EvalExpr::LoadVar(property)), + }); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + method: Box::new(EvalExpr::LoadVar(property)), + args, + }); + } + Ok(EvalExpr::StaticPropertyGet { + class_name, + property, + }) + } + TokenKind::DollarLBrace => { + self.advance(); + let property = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + Ok(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + property: Box::new(property), + }) + } + TokenKind::Ident(name) + if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => + { + self.advance(); + Ok(EvalExpr::ClassNameFetch { class_name }) + } + TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { + let method = method.clone(); + self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(EvalExpr::Const(EvalConst::String(method))), + }); + } + let args = self.parse_call_args()?; + Ok(EvalExpr::StaticMethodCall { + class_name, + method, + args, + }) + } + TokenKind::LBrace => { + self.advance(); + let member = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(EvalExpr::StaticMethodCallable { + class_name, + method: Box::new(member), + }); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + method: Box::new(member), + args, + }); + } + Ok(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(EvalExpr::ClassNameFetch { class_name }), + constant: Box::new(member), + }) + } + TokenKind::Ident(constant) => { + let constant = constant.clone(); + self.advance(); + Ok(EvalExpr::ClassConstantFetch { + class_name, + constant, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses `$class::member` expressions whose static receiver is runtime-valued. + pub(super) fn parse_dynamic_static_member_expr( + &mut self, + class_name: EvalExpr, + ) -> Result { + match self.current() { + TokenKind::DollarIdent(member) => { + let member = member.clone(); + self.advance(); + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::dynamic_static_method_callable_expr( + class_name, + EvalExpr::LoadVar(member), + )); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(EvalExpr::LoadVar(member)), + args, + }); + } + Ok(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name), + property: member, + }) + } + TokenKind::DollarLBrace => { + self.advance(); + let property = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + Ok(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(class_name), + property: Box::new(property), + }) + } + TokenKind::Ident(name) + if ident_eq(name, "class") && !matches!(self.peek(), TokenKind::LParen) => + { + self.advance(); + Ok(EvalExpr::DynamicClassNameFetch { + class_name: Box::new(class_name), + }) + } + TokenKind::Ident(method) if matches!(self.peek(), TokenKind::LParen) => { + let method = method.clone(); + self.advance(); + if self.consume_first_class_callable_marker() { + return Ok(Self::dynamic_static_method_callable_expr( + class_name, + EvalExpr::Const(EvalConst::String(method)), + )); + } + let args = self.parse_call_args()?; + Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(EvalExpr::Const(EvalConst::String(method))), + args, + }) + } + TokenKind::LBrace => { + self.advance(); + let member = self.parse_expr()?; + self.expect(TokenKind::RBrace)?; + if matches!(self.current(), TokenKind::LParen) { + if self.consume_first_class_callable_marker() { + return Ok(Self::dynamic_static_method_callable_expr(class_name, member)); + } + let args = self.parse_call_args()?; + return Ok(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(class_name), + method: Box::new(member), + args, + }); + } + Ok(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(class_name), + constant: Box::new(member), + }) + } + TokenKind::Ident(constant) => { + let constant = constant.clone(); + self.advance(); + Ok(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(class_name), + constant, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses `new ClassName(...)` and anonymous `new class {}` expressions in eval fragments. + pub(in crate::parser) fn parse_new_object_expr(&mut self) -> Result { + self.advance(); + let is_readonly_anonymous = matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "readonly")) + && matches!(self.peek(), TokenKind::Ident(name) if ident_eq(name, "class")); + if is_readonly_anonymous { + self.advance(); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return self.parse_anonymous_class_expr(is_readonly_anonymous); + } + if let TokenKind::DollarIdent(name) = self.current() { + let class_name = EvalExpr::LoadVar(name.clone()); + self.advance(); + let args = self.parse_optional_constructor_args()?; + return Ok(EvalExpr::DynamicNewObject { + class_name: Box::new(class_name), + args, + }); + } + if self.consume(TokenKind::LParen) { + let class_name = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let args = self.parse_optional_constructor_args()?; + return Ok(EvalExpr::DynamicNewObject { + class_name: Box::new(class_name), + args, + }); + } + let class_name = self.parse_class_reference_name(true)?; + let class_name = self.resolve_static_class_name(class_name); + let args = self.parse_optional_constructor_args()?; + Ok(EvalExpr::NewObject { class_name, args }) + } + + /// Parses an optional constructor argument list after `new` class targets. + pub(super) fn parse_optional_constructor_args(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::LParen) { + self.parse_call_args() + } else { + Ok(Vec::new()) + } + } + + /// Parses a simple or explicitly qualified PHP name. + pub(in crate::parser) fn parse_qualified_name(&mut self) -> Result { + let absolute = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok(ParsedQualifiedName { name, absolute }) + } + + /// Parses a class-like reference name while rejecting PHP-reserved unqualified names. + pub(in crate::parser) fn parse_class_reference_name( + &mut self, + allow_relative_keywords: bool, + ) -> Result { + let name = self.parse_qualified_name()?; + if self.class_reference_name_is_reserved(&name, allow_relative_keywords) { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(name) + } + + /// Returns whether a parsed class-like reference uses a PHP-reserved bare name. + pub(in crate::parser) fn class_reference_name_is_reserved( + &self, + name: &ParsedQualifiedName, + allow_relative_keywords: bool, + ) -> bool { + if name.absolute || name.name.contains('\\') { + return false; + } + if allow_relative_keywords + && ["self", "parent", "static"] + .iter() + .any(|keyword| ident_eq(&name.name, keyword)) + { + return false; + } + is_reserved_class_like_name(&name.name) + } + + /// Resolves a class name used before `::`, preserving PHP relative class keywords. + pub(in crate::parser) fn resolve_static_class_name(&self, name: ParsedQualifiedName) -> String { + if !name.absolute + && ["self", "parent", "static"] + .iter() + .any(|keyword| ident_eq(&name.name, keyword)) + { + return name.name.to_ascii_lowercase(); + } + self.resolve_class_name(name) + } + + /// Builds a call expression, adding namespace fallback for unqualified names. + pub(in crate::parser) fn call_expr(&self, name: String, args: Vec) -> EvalExpr { + if let Some(imported) = self.imports.resolve_function(&name) { + return EvalExpr::Call { + name: imported.to_ascii_lowercase(), + args, + }; + } + let fallback_name = name.to_ascii_lowercase(); + if self.namespace.is_empty() { + EvalExpr::Call { + name: fallback_name, + args, + } + } else { + EvalExpr::NamespacedCall { + name: self + .qualify_name_in_current_namespace(&name) + .to_ascii_lowercase(), + fallback_name, + args, + } + } + } + + /// Builds a constant fetch expression, adding namespace fallback for unqualified names. + pub(in crate::parser) fn const_fetch_expr(&self, name: String) -> EvalExpr { + if let Some(imported) = self.imports.resolve_constant(&name) { + return EvalExpr::ConstFetch(imported.to_string()); + } + if self.namespace.is_empty() { + EvalExpr::ConstFetch(name) + } else { + EvalExpr::NamespacedConstFetch { + name: self.qualify_name_in_current_namespace(&name), + fallback_name: name, + } + } + } + + /// Prefixes a name with the parser's current namespace when one is active. + pub(in crate::parser) fn qualify_name_in_current_namespace(&self, name: &str) -> String { + if self.namespace.is_empty() { + name.to_string() + } else { + format!("{}\\{}", self.namespace, name) + } + } + + /// Resolves a class name through active imports before namespace qualification. + pub(in crate::parser) fn resolve_class_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute { + return name.name; + } + if let Some(imported) = self.imports.resolve_class(&name.name) { + return imported; + } + self.resolve_qualified_name(name) + } + + /// Resolves a parsed PHP name according to the current namespace. + pub(in crate::parser) fn resolve_qualified_name(&self, name: ParsedQualifiedName) -> String { + if name.absolute || self.namespace.is_empty() { + name.name + } else { + self.qualify_name_in_current_namespace(&name.name) + } + } + +} diff --git a/crates/elephc-magician/src/parser/statements.rs b/crates/elephc-magician/src/parser/statements.rs index cf1bd85210..753d4bd75d 100644 --- a/crates/elephc-magician/src/parser/statements.rs +++ b/crates/elephc-magician/src/parser/statements.rs @@ -1,5 +1,6 @@ //! Purpose: -//! Parses PHP eval statements, declarations, control structures, and statement blocks into EvalIR. +//! Dispatches PHP eval statements and owns shared parser statement metadata. +//! Syntax families and analysis helpers live in focused child modules. //! //! Called from: //! - `crate::parser::state::Parser::parse_program()`. @@ -8,6 +9,21 @@ //! - Statement parsing expands multi-variable constructs such as `unset($a, $b)` into multiple EvalIR statements. //! - Namespace/use parsing lives here because declarations are statement-level syntax in PHP. +mod assignments; +mod class_declarations; +mod class_members; +mod control_flow; +mod default_validation; +mod enums; +mod functions_namespaces; +mod globals_exceptions; +mod interfaces; +mod loops; +mod parameters_types; +mod property_builders; +mod property_hook_analysis; +mod traits; + use super::cursor::*; use super::state::*; use crate::errors::EvalParseError; @@ -21,6 +37,10 @@ use crate::eval_ir::{ }; use crate::lexer::TokenKind; +use default_validation::*; +use property_builders::*; +use property_hook_analysis::*; + /// Parsed method parameters plus constructor-promotion side products. pub(super) struct ParsedMethodParams { pub(super) params: Vec, @@ -387,3984 +407,4 @@ impl Parser { cursor += 1; } } - - /// Parses `do { ... } while (expr);`. - pub(super) fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let body = self.parse_statement_body()?; - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::DoWhile { body, condition }]) - } - - /// Parses `$name[index] = expr;` and `$name[] = expr;` eval writes. - pub(super) fn parse_array_set_stmt( - &mut self, - name: String, - ) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LBracket)?; - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - self.expect_semicolon()?; - return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) - } - - /// Parses `for (init; condition; update) { ... }`. - pub(super) fn parse_for_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let init = self.parse_for_init_clause()?; - self.expect_semicolon()?; - let condition = if matches!(self.current(), TokenKind::Semicolon) { - None - } else { - Some(self.parse_expr()?) - }; - self.expect_semicolon()?; - let update = self.parse_for_update_clause()?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::For { - init, - condition, - update, - body, - }]) - } - - /// Parses `foreach (expr as $value) { ... }` or `foreach (expr as $key => $value) { ... }`. - pub(super) fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let array = self.parse_expr()?; - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "as")) { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - let TokenKind::DollarIdent(value_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let value_name = value_name.clone(); - self.advance(); - let (key_name, value_name) = if matches!(self.current(), TokenKind::FatArrow) { - self.advance(); - let TokenKind::DollarIdent(next_value_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let key_name = value_name; - let value_name = next_value_name.clone(); - self.advance(); - (Some(key_name), value_name) - } else { - (None, value_name) - }; - self.expect(TokenKind::RParen)?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::Foreach { - array, - key_name, - value_name, - body, - }]) - } - - /// Parses `[abstract|final|readonly] class Name [extends Parent] [implements Iface, ...] { ... }`. - pub(super) fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { - self.parse_class_decl_stmt_with_attributes(Vec::new()) - } - - /// Parses a class declaration and attaches already parsed class attributes. - pub(super) fn parse_class_decl_stmt_with_attributes( - &mut self, - attributes: Vec, - ) -> Result, EvalParseError> { - let (is_abstract, is_final, is_readonly_class) = self.parse_class_decl_modifiers()?; - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { - return Err(EvalParseError::UnsupportedConstruct); - } - let source_start_line = self.current_line(); - self.advance(); - let name = self.parse_class_like_decl_name()?; - let parent = self.parse_class_parent_clause()?; - let interfaces = self.parse_class_interface_clause()?; - let body = self.parse_class_body_members(is_readonly_class)?; - let source_location = EvalSourceLocation::new(source_start_line, body.source_end_line); - self.consume_semicolon(); - Ok(vec![EvalStmt::ClassDecl( - EvalClass::with_class_modifiers_traits_adaptations_and_constants( - name, - is_abstract, - is_final, - is_readonly_class, - parent, - interfaces, - body.traits, - body.trait_adaptations, - body.constants, - body.properties, - body.methods, - ) - .with_source_location(source_location) - .with_attributes(attributes), - )]) - } - - /// Parses `class [(args)] [extends Parent] [implements Iface, ...] { ... }` after `new`. - pub(super) fn parse_anonymous_class_expr( - &mut self, - is_readonly_class: bool, - ) -> Result { - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { - return Err(EvalParseError::UnsupportedConstruct); - } - let source_start_line = self.current_line(); - self.advance(); - let args = if matches!(self.current(), TokenKind::LParen) { - self.parse_call_args()? - } else { - Vec::new() - }; - let parent = self.parse_class_parent_clause()?; - let interfaces = self.parse_class_interface_clause()?; - let body = self.parse_class_body_members(is_readonly_class)?; - let source_location = EvalSourceLocation::new(source_start_line, body.source_end_line); - let name = next_anonymous_class_name(); - let class = EvalClass::with_class_modifiers_traits_adaptations_and_constants( - name, - false, - false, - is_readonly_class, - parent, - interfaces, - body.traits, - body.trait_adaptations, - body.constants, - body.properties, - body.methods, - ) - .with_source_location(source_location) - .with_anonymous(); - Ok(EvalExpr::NewAnonymousClass { class, args }) - } - - /// Parses and namespace-qualifies a declared class, interface, trait, or enum name. - fn parse_class_like_decl_name(&mut self) -> Result { - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - if is_reserved_class_like_name(name) { - return Err(EvalParseError::UnsupportedConstruct); - } - let qualified_name = self.qualify_name_in_current_namespace(name); - self.advance(); - Ok(qualified_name) - } - - /// Parses members inside a class body after relation clauses. - fn parse_class_body_members( - &mut self, - is_readonly_class: bool, - ) -> Result { - self.expect(TokenKind::LBrace)?; - let mut constants = Vec::new(); - let mut properties = Vec::new(); - let mut methods = Vec::new(); - let mut traits = Vec::new(); - let mut trait_adaptations = Vec::new(); - loop { - if matches!(self.current(), TokenKind::RBrace) { - let source_end_line = self.current_line(); - self.advance(); - return Ok(ParsedClassBody { - source_end_line, - constants, - properties, - methods, - traits, - trait_adaptations, - }); - } - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - self.parse_class_member( - is_readonly_class, - &mut constants, - &mut properties, - &mut methods, - &mut traits, - &mut trait_adaptations, - )?; - } - } - - /// Parses class-level `abstract`, `final`, and `readonly` modifiers before `class`. - pub(super) fn parse_class_decl_modifiers( - &mut self, - ) -> Result<(bool, bool, bool), EvalParseError> { - let mut is_abstract = false; - let mut is_final = false; - let mut is_readonly_class = false; - loop { - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "abstract") => { - if is_abstract { - return Err(EvalParseError::UnsupportedConstruct); - } - is_abstract = true; - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "final") => { - if is_final { - return Err(EvalParseError::UnsupportedConstruct); - } - is_final = true; - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "readonly") => { - if is_readonly_class { - return Err(EvalParseError::UnsupportedConstruct); - } - is_readonly_class = true; - self.advance(); - } - _ => return Ok((is_abstract, is_final, is_readonly_class)), - } - } - } - - /// Parses an optional `extends Parent` class declaration clause. - pub(super) fn parse_class_parent_clause(&mut self) -> Result, EvalParseError> { - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { - return Ok(None); - } - self.advance(); - let parent = self.parse_class_reference_name(false)?; - Ok(Some(self.resolve_class_name(parent))) - } - - /// Parses an optional `implements Iface, ...` class declaration clause. - pub(super) fn parse_class_interface_clause(&mut self) -> Result, EvalParseError> { - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "implements")) { - return Ok(Vec::new()); - } - self.advance(); - let mut interfaces = Vec::new(); - loop { - let interface = self.parse_class_reference_name(false)?; - interfaces.push(self.resolve_class_name(interface)); - if !self.consume(TokenKind::Comma) { - break; - } - } - Ok(interfaces) - } - - /// Parses one public property or method from an eval class body. - pub(super) fn parse_class_member( - &mut self, - is_readonly_class: bool, - constants: &mut Vec, - properties: &mut Vec, - methods: &mut Vec, - traits: &mut Vec, - trait_adaptations: &mut Vec, - ) -> Result<(), EvalParseError> { - let attributes = self.parse_optional_member_attributes()?; - let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = - self.parse_class_member_modifiers()?; - - if is_abstract && is_final { - return Err(EvalParseError::UnsupportedConstruct); - } - - if visibility.is_none() - && !is_static - && !is_abstract - && !is_final - && !is_readonly - && set_visibility.is_none() - && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) - { - if !attributes.is_empty() { - return Err(EvalParseError::UnsupportedConstruct); - } - self.parse_class_trait_use(traits, trait_adaptations)?; - return Ok(()); - } - - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { - self.parse_legacy_var_property_member( - attributes, - visibility.is_some() - || is_static - || is_abstract - || is_final - || is_readonly - || set_visibility.is_some(), - is_readonly_class, - properties, - methods, - )?; - return Ok(()); - } - - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_abstract || is_readonly || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - constants.extend( - self.parse_class_const_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_final, - &attributes, - )?, - ); - return Ok(()); - } - - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if is_readonly || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - let (method, promoted_properties) = self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - is_abstract, - is_final, - )?; - properties.extend(promoted_properties); - methods.push(method.with_attributes(attributes)); - return Ok(()); - } - - let visibility = visibility.unwrap_or(EvalVisibility::Public); - let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( - visibility, - set_visibility, - is_static, - is_final, - is_readonly, - is_readonly_class, - is_abstract, - )?; - properties.extend( - parsed_properties - .into_iter() - .map(|property| property.with_attributes(attributes.clone())), - ); - methods.append(&mut hook_methods); - Ok(()) - } - - /// Parses PHP's legacy `var` public-property marker after the keyword is recognized. - fn parse_legacy_var_property_member( - &mut self, - attributes: Vec, - has_other_modifiers: bool, - is_readonly_class: bool, - properties: &mut Vec, - methods: &mut Vec, - ) -> Result<(), EvalParseError> { - if has_other_modifiers { - return Err(EvalParseError::UnsupportedConstruct); - } - self.advance(); - let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( - EvalVisibility::Public, - None, - false, - false, - false, - is_readonly_class, - false, - )?; - properties.extend( - parsed_properties - .into_iter() - .map(|property| property.with_attributes(attributes.clone())), - ); - methods.append(&mut hook_methods); - Ok(()) - } - - /// Parses optional attributes that decorate one class-like member. - pub(super) fn parse_optional_member_attributes( - &mut self, - ) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::AttributeStart) { - self.parse_attribute_groups() - } else { - Ok(Vec::new()) - } - } - - /// Parses one eval class constant declaration, including comma-separated constants. - pub(super) fn parse_class_const_decl( - &mut self, - visibility: EvalVisibility, - is_final: bool, - attributes: &[EvalAttribute], - ) -> Result, EvalParseError> { - self.advance(); - let mut constants = Vec::new(); - loop { - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - if ident_eq(name, "class") { - return Err(EvalParseError::UnsupportedConstruct); - } - let name = name.clone(); - self.advance(); - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - constants.push( - EvalClassConstant::with_visibility_and_final(name, visibility, is_final, value) - .with_attributes(attributes.to_vec()), - ); - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect_semicolon()?; - Ok(constants) - } - - /// Parses `use TraitName, OtherTrait;` or an adaptation block inside an eval class body. - pub(super) fn parse_class_trait_use( - &mut self, - traits: &mut Vec, - trait_adaptations: &mut Vec, - ) -> Result<(), EvalParseError> { - self.advance(); - loop { - let trait_name = self.parse_class_reference_name(false)?; - traits.push(self.resolve_class_name(trait_name)); - if !self.consume(TokenKind::Comma) { - break; - } - } - if self.consume(TokenKind::LBrace) { - while !self.consume(TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - trait_adaptations.push(self.parse_trait_adaptation()?); - self.expect_semicolon()?; - } - self.consume_semicolon(); - Ok(()) - } else { - self.expect_semicolon() - } - } - - /// Parses one `as` or `insteadof` trait adaptation clause. - pub(super) fn parse_trait_adaptation(&mut self) -> Result { - let (trait_name, method) = self.parse_trait_adaptation_target()?; - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "as") => { - self.advance(); - let visibility = self.parse_optional_trait_adaptation_visibility()?; - let alias = if let TokenKind::Ident(alias) = self.current() { - let alias = alias.clone(); - self.advance(); - Some(alias) - } else { - None - }; - if visibility.is_none() && alias.is_none() { - return Err(EvalParseError::UnsupportedConstruct); - } - Ok(EvalTraitAdaptation::Alias { - trait_name, - method, - alias, - visibility, - }) - } - TokenKind::Ident(name) if ident_eq(name, "insteadof") => { - self.advance(); - let mut instead_of = Vec::new(); - loop { - let trait_name = self.parse_class_reference_name(false)?; - instead_of.push(self.resolve_class_name(trait_name)); - if !self.consume(TokenKind::Comma) { - break; - } - } - if instead_of.is_empty() { - return Err(EvalParseError::UnsupportedConstruct); - } - Ok(EvalTraitAdaptation::InsteadOf { - trait_name, - method, - instead_of, - }) - } - _ => Err(EvalParseError::UnsupportedConstruct), - } - } - - /// Parses the target before `as` or `insteadof`. - pub(super) fn parse_trait_adaptation_target( - &mut self, - ) -> Result<(Option, String), EvalParseError> { - let first = self.parse_qualified_name()?; - if self.consume(TokenKind::DoubleColon) { - if self.class_reference_name_is_reserved(&first, false) { - return Err(EvalParseError::UnsupportedConstruct); - } - let TokenKind::Ident(method) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let method = method.clone(); - self.advance(); - Ok((Some(self.resolve_class_name(first)), method)) - } else { - let method = first - .name - .rsplit('\\') - .next() - .filter(|segment| !segment.is_empty()) - .ok_or(EvalParseError::UnexpectedToken)? - .to_string(); - Ok((None, method)) - } - } - - /// Parses an optional visibility modifier inside a trait `as` adaptation. - pub(super) fn parse_optional_trait_adaptation_visibility( - &mut self, - ) -> Result, EvalParseError> { - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "public") => { - self.advance(); - Ok(Some(EvalVisibility::Public)) - } - TokenKind::Ident(name) if ident_eq(name, "protected") => { - self.advance(); - Ok(Some(EvalVisibility::Protected)) - } - TokenKind::Ident(name) if ident_eq(name, "private") => { - self.advance(); - Ok(Some(EvalVisibility::Private)) - } - _ => Ok(None), - } - } - - /// Parses method modifiers supported by eval class declarations. - pub(super) fn parse_class_member_modifiers( - &mut self, - ) -> Result< - ( - Option, - Option, - bool, - bool, - bool, - bool, - ), - EvalParseError, - > { - let mut visibility = None; - let mut set_visibility = None; - let mut is_static = false; - let mut is_abstract = false; - let mut is_final = false; - let mut is_readonly = false; - loop { - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "public") => { - self.advance(); - if self.consume_set_marker()? { - if set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - set_visibility = Some(EvalVisibility::Public); - } else if visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } else { - visibility = Some(EvalVisibility::Public); - } - } - TokenKind::Ident(name) if ident_eq(name, "protected") => { - self.advance(); - if self.consume_set_marker()? { - if set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - set_visibility = Some(EvalVisibility::Protected); - } else if visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } else { - visibility = Some(EvalVisibility::Protected); - } - } - TokenKind::Ident(name) if ident_eq(name, "private") => { - self.advance(); - if self.consume_set_marker()? { - if set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - set_visibility = Some(EvalVisibility::Private); - } else if visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } else { - visibility = Some(EvalVisibility::Private); - } - } - TokenKind::Ident(name) if ident_eq(name, "static") => { - if is_static { - return Err(EvalParseError::UnsupportedConstruct); - } - is_static = true; - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "abstract") => { - if is_abstract { - return Err(EvalParseError::UnsupportedConstruct); - } - is_abstract = true; - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "final") => { - if is_final { - return Err(EvalParseError::UnsupportedConstruct); - } - is_final = true; - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "readonly") => { - if is_readonly { - return Err(EvalParseError::UnsupportedConstruct); - } - is_readonly = true; - self.advance(); - } - TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { - return Err(EvalParseError::UnsupportedConstruct); - } - _ => { - return Ok(( - visibility, - set_visibility, - is_static, - is_abstract, - is_final, - is_readonly, - )) - } - } - } - } - - /// Consumes a PHP asymmetric visibility `(set)` marker after a visibility keyword. - fn consume_set_marker(&mut self) -> Result { - if !self.consume(TokenKind::LParen) { - return Ok(false); - } - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "set") => self.advance(), - _ => return Err(EvalParseError::UnsupportedConstruct), - } - self.expect(TokenKind::RParen)?; - Ok(true) - } - - /// Returns a comparable visibility rank where larger means less restrictive. - fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { - match visibility { - EvalVisibility::Private => 1, - EvalVisibility::Protected => 2, - EvalVisibility::Public => 3, - } - } - - /// Parses one class/trait/enum method and returns constructor-promoted properties. - pub(super) fn parse_class_method_decl( - &mut self, - visibility: EvalVisibility, - is_static: bool, - is_abstract: bool, - is_final: bool, - ) -> Result<(EvalClassMethod, Vec), EvalParseError> { - let source_start_line = self.current_line(); - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - self.expect(TokenKind::LParen)?; - let ParsedMethodParams { - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - promoted_properties, - promoted_assignments, - } = self.parse_method_params(&name, true)?; - if !promoted_properties.is_empty() && (is_abstract || is_static) { - return Err(EvalParseError::UnsupportedConstruct); - } - let return_type = self.parse_optional_return_type(EvalTypePosition::MethodReturn)?; - let (body, source_end_line) = if is_abstract { - let source_end_line = self.current_line(); - self.expect_semicolon()?; - (Vec::new(), source_end_line) - } else { - let (body, source_end_line) = self.parse_block_with_end_line()?; - let body = if promoted_assignments.is_empty() { - body - } else { - promoted_assignments.into_iter().chain(body).collect() - }; - (body, source_end_line) - }; - Ok(( - EvalClassMethod::with_visibility_and_modifiers( - name, - visibility, - is_static, - is_abstract, - is_final, - params, - body, - ) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) - .with_parameter_types(parameter_types) - .with_parameter_attributes(parameter_attributes) - .with_parameter_defaults(parameter_defaults) - .with_parameter_by_ref_flags(parameter_is_by_ref) - .with_parameter_variadic_flags(parameter_is_variadic) - .with_return_type(return_type), - promoted_properties, - )) - } - - /// Parses one property declaration, including comma-separated simple properties. - pub(super) fn parse_class_property_decl( - &mut self, - visibility: EvalVisibility, - set_visibility: Option, - is_static: bool, - is_final: bool, - is_readonly: bool, - is_readonly_class: bool, - is_abstract: bool, - ) -> Result<(Vec, Vec), EvalParseError> { - if is_static && is_readonly { - return Err(EvalParseError::UnsupportedConstruct); - } - if set_visibility.is_some() && is_static { - return Err(EvalParseError::UnsupportedConstruct); - } - if set_visibility.is_some_and(|set_visibility| { - Self::eval_visibility_rank(set_visibility) > Self::eval_visibility_rank(visibility) - }) { - return Err(EvalParseError::UnsupportedConstruct); - } - let effective_readonly = is_readonly || (is_readonly_class && !is_static); - let property_type = self.parse_optional_property_type()?; - if set_visibility.is_some() && property_type.is_none() { - return Err(EvalParseError::UnsupportedConstruct); - } - let mut properties = Vec::new(); - let mut hook_methods = Vec::new(); - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - let default = if self.consume(TokenKind::Equal) { - if is_abstract || effective_readonly { - return Err(EvalParseError::UnsupportedConstruct); - } - Some(self.parse_expr()?) - } else { - None - }; - if is_abstract { - if is_static || effective_readonly { - return Err(EvalParseError::UnsupportedConstruct); - } - let (requires_get_hook, requires_set_hook) = - self.parse_property_hook_contracts()?; - let property = EvalClassProperty::with_visibility_static_final_and_readonly( - name, - visibility, - is_static, - is_final, - effective_readonly, - None, - ) - .with_type(property_type) - .with_set_visibility(set_visibility) - .with_abstract_hook_contract(requires_get_hook, requires_set_hook); - return Ok((vec![property], Vec::new())); - } - let default_is_some = default.is_some(); - if self.consume(TokenKind::Comma) { - properties.push( - EvalClassProperty::with_visibility_static_final_and_readonly( - name, - visibility, - is_static, - is_final, - effective_readonly, - default, - ) - .with_type(property_type.clone()) - .with_set_visibility(set_visibility), - ); - continue; - } - if !properties.is_empty() { - self.expect_semicolon()?; - properties.push( - EvalClassProperty::with_visibility_static_final_and_readonly( - name, - visibility, - is_static, - is_final, - effective_readonly, - default, - ) - .with_type(property_type.clone()) - .with_set_visibility(set_visibility), - ); - break; - } - let (has_get_hook, has_set_hook, set_hook_type, parsed_hook_methods) = self - .parse_property_hook_tail( - &name, - property_type.as_ref(), - is_static, - effective_readonly, - default_is_some, - )?; - if set_hook_type.is_some() && property_type.is_none() { - return Err(EvalParseError::UnsupportedConstruct); - } - let is_virtual = (has_get_hook || has_set_hook) - && !property_hook_methods_use_backing_slot(&parsed_hook_methods, &name); - properties.push( - EvalClassProperty::with_visibility_static_final_and_readonly( - name, - visibility, - is_static, - is_final, - effective_readonly, - default, - ) - .with_type(property_type.clone()) - .with_set_hook_type(set_hook_type) - .with_set_visibility(set_visibility) - .with_hooks(has_get_hook, has_set_hook) - .with_virtual(is_virtual), - ); - hook_methods.extend(parsed_hook_methods); - break; - } - Ok((properties, hook_methods)) - } - - /// Parses `;` or a concrete eval property hook block after one property declaration. - pub(super) fn parse_property_hook_tail( - &mut self, - property_name: &str, - property_type: Option<&EvalParameterType>, - is_static: bool, - is_readonly: bool, - has_default: bool, - ) -> Result<(bool, bool, Option, Vec), EvalParseError> { - if self.consume(TokenKind::Semicolon) { - return Ok((false, false, None, Vec::new())); - } - if !matches!(self.current(), TokenKind::LBrace) { - return Err(EvalParseError::UnexpectedToken); - } - if is_static || is_readonly || has_default { - return Err(EvalParseError::UnsupportedConstruct); - } - self.advance(); - let mut has_get_hook = false; - let mut has_set_hook = false; - let mut set_hook_type = None; - let mut methods = Vec::new(); - while !self.consume(TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - let (is_get, hook_set_type, method) = - self.parse_property_hook_decl(property_name, property_type)?; - if is_get { - if has_get_hook { - return Err(EvalParseError::UnsupportedConstruct); - } - has_get_hook = true; - } else { - if has_set_hook { - return Err(EvalParseError::UnsupportedConstruct); - } - has_set_hook = true; - set_hook_type = hook_set_type; - } - methods.push(method); - } - if !has_get_hook && !has_set_hook { - return Err(EvalParseError::UnsupportedConstruct); - } - Ok((has_get_hook, has_set_hook, set_hook_type, methods)) - } - - /// Parses one concrete `get` or `set` property hook declaration. - pub(super) fn parse_property_hook_decl( - &mut self, - property_name: &str, - property_type: Option<&EvalParameterType>, - ) -> Result<(bool, Option, EvalClassMethod), EvalParseError> { - let returns_by_ref = self.consume(TokenKind::Ampersand); - let TokenKind::Ident(hook_name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let is_get = ident_eq(hook_name, "get"); - let is_set = ident_eq(hook_name, "set"); - if !is_get && !is_set { - return Err(EvalParseError::UnsupportedConstruct); - } - if returns_by_ref && !is_get { - return Err(EvalParseError::UnsupportedConstruct); - } - let source_start_line = self.current_line(); - self.advance(); - let (params, set_hook_type) = if is_set { - let (param, set_hook_type, has_explicit_param) = - self.parse_property_set_hook_param()?; - if has_explicit_param && set_hook_type.is_none() && property_type.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - (vec![param], set_hook_type) - } else { - (Vec::new(), None) - }; - let (body, source_end_line) = match self.current() { - TokenKind::Semicolon => return Err(EvalParseError::UnsupportedConstruct), - TokenKind::FatArrow => { - self.advance(); - let expr = self.parse_expr()?; - let source_end_line = self.current_line(); - self.expect_semicolon()?; - let body = if is_get { - vec![EvalStmt::Return(Some(expr))] - } else { - vec![EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: property_name.to_string(), - value: expr, - }] - }; - (body, source_end_line) - } - TokenKind::LBrace => self.parse_block_with_end_line()?, - _ => return Err(EvalParseError::UnexpectedToken), - }; - let method_name = if is_get { - property_hook_get_method(property_name) - } else { - property_hook_set_method(property_name) - }; - let mut method = EvalClassMethod::with_visibility_and_modifiers( - method_name, - EvalVisibility::Public, - false, - false, - false, - params, - body, - ) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)); - if is_set { - method = method.with_parameter_types(vec![ - set_hook_type.clone().or_else(|| property_type.cloned()), - ]); - } - Ok((is_get, set_hook_type, method)) - } - - /// Parses an optional set-hook parameter list and returns the value variable metadata. - pub(super) fn parse_property_set_hook_param( - &mut self, - ) -> Result<(String, Option, bool), EvalParseError> { - if !self.consume(TokenKind::LParen) { - return Ok(("value".to_string(), None, false)); - } - let set_hook_type = self.parse_optional_property_type()?; - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let name = name.clone(); - self.advance(); - self.expect(TokenKind::RParen)?; - Ok((name, set_hook_type, true)) - } - - /// Parses `trait Name { ... }` declarations into dynamic trait metadata. - pub(super) fn parse_trait_decl_stmt(&mut self) -> Result, EvalParseError> { - self.parse_trait_decl_stmt_with_attributes(Vec::new()) - } - - /// Parses a trait declaration and attaches already parsed class-like attributes. - pub(super) fn parse_trait_decl_stmt_with_attributes( - &mut self, - attributes: Vec, - ) -> Result, EvalParseError> { - let source_start_line = self.current_line(); - self.advance(); - let name = self.parse_class_like_decl_name()?; - self.expect(TokenKind::LBrace)?; - let mut traits = Vec::new(); - let mut trait_adaptations = Vec::new(); - let mut constants = Vec::new(); - let mut properties = Vec::new(); - let mut methods = Vec::new(); - let source_end_line = loop { - if matches!(self.current(), TokenKind::RBrace) { - let source_end_line = self.current_line(); - self.advance(); - break source_end_line; - } - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - self.parse_trait_member( - &mut constants, - &mut properties, - &mut methods, - &mut traits, - &mut trait_adaptations, - )?; - }; - self.consume_semicolon(); - Ok(vec![EvalStmt::TraitDecl( - EvalTrait::with_constants_traits_adaptations( - name, - constants, - properties, - methods, - traits, - trait_adaptations, - ) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) - .with_attributes(attributes), - )]) - } - - /// Parses one property or method from an eval trait body. - pub(super) fn parse_trait_member( - &mut self, - constants: &mut Vec, - properties: &mut Vec, - methods: &mut Vec, - traits: &mut Vec, - trait_adaptations: &mut Vec, - ) -> Result<(), EvalParseError> { - let attributes = self.parse_optional_member_attributes()?; - let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = - self.parse_class_member_modifiers()?; - if is_abstract && is_final { - return Err(EvalParseError::UnsupportedConstruct); - } - if visibility.is_none() - && !is_static - && !is_abstract - && !is_final - && !is_readonly - && set_visibility.is_none() - && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) - { - if !attributes.is_empty() { - return Err(EvalParseError::UnsupportedConstruct); - } - self.parse_class_trait_use(traits, trait_adaptations)?; - return Ok(()); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || is_abstract || is_readonly || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - constants.extend( - self.parse_class_const_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_final, - &attributes, - )?, - ); - return Ok(()); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if is_readonly || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - let (method, promoted_properties) = self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - is_abstract, - is_final, - )?; - properties.extend(promoted_properties); - methods.push(method.with_attributes(attributes)); - return Ok(()); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { - self.parse_legacy_var_property_member( - attributes, - visibility.is_some() - || is_static - || is_abstract - || is_final - || is_readonly - || set_visibility.is_some(), - false, - properties, - methods, - )?; - return Ok(()); - } - let visibility = visibility.unwrap_or(EvalVisibility::Public); - let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( - visibility, - set_visibility, - is_static, - is_final, - is_readonly, - false, - is_abstract, - )?; - properties.extend( - parsed_properties - .into_iter() - .map(|property| property.with_attributes(attributes.clone())), - ); - methods.append(&mut hook_methods); - Ok(()) - } - - /// Parses `enum Name [: int|string] [implements Iface, ...] { ... }`. - pub(super) fn parse_enum_decl_stmt(&mut self) -> Result, EvalParseError> { - self.parse_enum_decl_stmt_with_attributes(Vec::new()) - } - - /// Parses an enum declaration and attaches already parsed class-like attributes. - pub(super) fn parse_enum_decl_stmt_with_attributes( - &mut self, - attributes: Vec, - ) -> Result, EvalParseError> { - let source_start_line = self.current_line(); - self.advance(); - let name = self.parse_class_like_decl_name()?; - let backing_type = self.parse_enum_backing_type()?; - let interfaces = self.parse_class_interface_clause()?; - self.expect(TokenKind::LBrace)?; - let mut traits = Vec::new(); - let mut trait_adaptations = Vec::new(); - let mut cases = Vec::new(); - let mut constants = Vec::new(); - let mut methods = Vec::new(); - let source_end_line = loop { - if matches!(self.current(), TokenKind::RBrace) { - let source_end_line = self.current_line(); - self.advance(); - break source_end_line; - } - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - self.parse_enum_member( - &mut cases, - &mut constants, - &mut methods, - &mut traits, - &mut trait_adaptations, - )?; - }; - self.consume_semicolon(); - Ok(vec![EvalStmt::EnumDecl( - EvalEnum::with_members_traits_adaptations( - name, - backing_type, - interfaces, - cases, - constants, - methods, - traits, - trait_adaptations, - ) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) - .with_attributes(attributes), - )]) - } - - /// Parses an optional backed-enum scalar type after the enum name. - pub(super) fn parse_enum_backing_type( - &mut self, - ) -> Result, EvalParseError> { - if !self.consume(TokenKind::Colon) { - return Ok(None); - } - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let backing_type = if ident_eq(name, "int") { - EvalEnumBackingType::Int - } else if ident_eq(name, "string") { - EvalEnumBackingType::String - } else { - return Err(EvalParseError::UnsupportedConstruct); - }; - self.advance(); - Ok(Some(backing_type)) - } - - /// Parses one enum case, constant, or method declaration. - pub(super) fn parse_enum_member( - &mut self, - cases: &mut Vec, - constants: &mut Vec, - methods: &mut Vec, - traits: &mut Vec, - trait_adaptations: &mut Vec, - ) -> Result<(), EvalParseError> { - let attributes = self.parse_optional_member_attributes()?; - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) { - cases.push(self.parse_enum_case_decl()?.with_attributes(attributes)); - return Ok(()); - } - let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = - self.parse_class_member_modifiers()?; - if is_abstract || is_readonly || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - if visibility.is_none() - && !is_static - && !is_final - && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) - { - if !attributes.is_empty() { - return Err(EvalParseError::UnsupportedConstruct); - } - self.parse_class_trait_use(traits, trait_adaptations)?; - return Ok(()); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static { - return Err(EvalParseError::UnsupportedConstruct); - } - constants.extend( - self.parse_class_const_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_final, - &attributes, - )?, - ); - return Ok(()); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - let (method, promoted_properties) = self.parse_class_method_decl( - visibility.unwrap_or(EvalVisibility::Public), - is_static, - false, - is_final, - )?; - if !promoted_properties.is_empty() { - return Err(EvalParseError::UnsupportedConstruct); - } - methods.push(method.with_attributes(attributes)); - return Ok(()); - } - Err(EvalParseError::UnsupportedConstruct) - } - - /// Parses `case Name;` or `case Name = expr;` inside an eval enum body. - pub(super) fn parse_enum_case_decl(&mut self) -> Result { - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - let value = if self.consume(TokenKind::Equal) { - Some(self.parse_expr()?) - } else { - None - }; - self.expect_semicolon()?; - Ok(EvalEnumCase::new(name, value)) - } - - /// Parses `interface Name [extends Parent, ...] { function name(...); }`. - pub(super) fn parse_interface_decl_stmt(&mut self) -> Result, EvalParseError> { - self.parse_interface_decl_stmt_with_attributes(Vec::new()) - } - - /// Parses an interface declaration and attaches already parsed class-like attributes. - pub(super) fn parse_interface_decl_stmt_with_attributes( - &mut self, - attributes: Vec, - ) -> Result, EvalParseError> { - let source_start_line = self.current_line(); - self.advance(); - let name = self.parse_class_like_decl_name()?; - let parents = self.parse_interface_parent_clause()?; - self.expect(TokenKind::LBrace)?; - let mut constants = Vec::new(); - let mut properties = Vec::new(); - let mut methods = Vec::new(); - let source_end_line = loop { - if matches!(self.current(), TokenKind::RBrace) { - let source_end_line = self.current_line(); - self.advance(); - break source_end_line; - } - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - self.parse_interface_member(&mut constants, &mut properties, &mut methods)?; - }; - self.consume_semicolon(); - Ok(vec![EvalStmt::InterfaceDecl( - EvalInterface::with_constants_and_properties( - name, parents, constants, properties, methods, - ) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) - .with_attributes(attributes), - )]) - } - - /// Parses an optional `extends Parent, ...` interface declaration clause. - pub(super) fn parse_interface_parent_clause(&mut self) -> Result, EvalParseError> { - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { - return Ok(Vec::new()); - } - self.advance(); - let mut parents = Vec::new(); - loop { - let parent = self.parse_class_reference_name(false)?; - parents.push(self.resolve_class_name(parent)); - if !self.consume(TokenKind::Comma) { - break; - } - } - Ok(parents) - } - - /// Parses one eval interface constant, property contract, or method signature. - pub(super) fn parse_interface_member( - &mut self, - constants: &mut Vec, - properties: &mut Vec, - methods: &mut Vec, - ) -> Result<(), EvalParseError> { - let attributes = self.parse_optional_member_attributes()?; - let mut is_static = false; - let mut is_final = false; - let mut saw_public = false; - let mut set_visibility = None; - loop { - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "public") => { - self.advance(); - if self.consume_set_marker()? { - if set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - set_visibility = Some(EvalVisibility::Public); - } else if saw_public { - return Err(EvalParseError::UnsupportedConstruct); - } else { - saw_public = true; - } - } - TokenKind::Ident(name) if ident_eq(name, "protected") => { - self.advance(); - if !self.consume_set_marker()? || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - set_visibility = Some(EvalVisibility::Protected); - } - TokenKind::Ident(name) if ident_eq(name, "private") => { - self.advance(); - if !self.consume_set_marker()? || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - set_visibility = Some(EvalVisibility::Private); - } - TokenKind::Ident(name) if ident_eq(name, "static") => { - if is_static { - return Err(EvalParseError::UnsupportedConstruct); - } - is_static = true; - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "final") => { - if is_final { - return Err(EvalParseError::UnsupportedConstruct); - } - is_final = true; - self.advance(); - } - TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { - return Err(EvalParseError::UnsupportedConstruct); - } - _ => break, - } - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if is_static || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - constants.extend(self.parse_class_const_decl( - EvalVisibility::Public, - is_final, - &attributes, - )?); - return Ok(()); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if is_final || set_visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - methods.push( - self.parse_interface_method_decl_after_function_keyword(is_static)? - .with_attributes(attributes), - ); - return Ok(()); - } - if is_static || is_final { - return Err(EvalParseError::UnsupportedConstruct); - } - properties.push( - self.parse_interface_property_decl(set_visibility)? - .with_attributes(attributes), - ); - Ok(()) - } - - /// Parses one eval interface method signature after `function` has been selected. - pub(super) fn parse_interface_method_decl_after_function_keyword( - &mut self, - is_static: bool, - ) -> Result { - let source_start_line = self.current_line(); - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = name.clone(); - self.advance(); - self.expect(TokenKind::LParen)?; - let ParsedMethodParams { - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - promoted_properties, - promoted_assignments, - } = self.parse_method_params(&name, true)?; - if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { - return Err(EvalParseError::UnsupportedConstruct); - } - let return_type = self.parse_optional_return_type(EvalTypePosition::MethodReturn)?; - let source_end_line = self.current_line(); - self.expect_semicolon()?; - Ok(EvalInterfaceMethod::new(name, params) - .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) - .with_static(is_static) - .with_parameter_types(parameter_types) - .with_parameter_attributes(parameter_attributes) - .with_parameter_defaults(parameter_defaults) - .with_parameter_by_ref_flags(parameter_is_by_ref) - .with_parameter_variadic_flags(parameter_is_variadic) - .with_return_type(return_type)) - } - - /// Parses one interface property hook contract. - pub(super) fn parse_interface_property_decl( - &mut self, - set_visibility: Option, - ) -> Result { - let property_type = self.parse_optional_property_type()?; - if set_visibility.is_some() && property_type.is_none() { - return Err(EvalParseError::UnsupportedConstruct); - } - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let name = name.clone(); - self.advance(); - if matches!(self.current(), TokenKind::Equal) { - return Err(EvalParseError::UnsupportedConstruct); - } - let (requires_get, requires_set) = self.parse_interface_property_hook_contracts()?; - if set_visibility.is_some() && !requires_set { - return Err(EvalParseError::UnsupportedConstruct); - } - Ok(EvalInterfaceProperty::new(name, requires_get, requires_set) - .with_type(property_type) - .with_set_visibility(set_visibility)) - } - - /// Parses `{ get; set; }` hook contracts for an abstract or interface property. - pub(super) fn parse_property_hook_contracts(&mut self) -> Result<(bool, bool), EvalParseError> { - self.expect(TokenKind::LBrace)?; - let mut requires_get = false; - let mut requires_set = false; - while !self.consume(TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - let returns_by_ref = self.consume(TokenKind::Ampersand); - let TokenKind::Ident(hook_name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let is_get = ident_eq(hook_name, "get"); - let is_set = ident_eq(hook_name, "set"); - if !is_get && !is_set { - return Err(EvalParseError::UnsupportedConstruct); - } - if returns_by_ref && !is_get { - return Err(EvalParseError::UnsupportedConstruct); - } - self.advance(); - if matches!( - self.current(), - TokenKind::LParen | TokenKind::FatArrow | TokenKind::LBrace - ) { - return Err(EvalParseError::UnsupportedConstruct); - } - self.expect_semicolon()?; - if is_get { - if requires_get { - return Err(EvalParseError::UnsupportedConstruct); - } - requires_get = true; - } else { - if requires_set { - return Err(EvalParseError::UnsupportedConstruct); - } - requires_set = true; - } - } - if !requires_get && !requires_set { - return Err(EvalParseError::UnsupportedConstruct); - } - Ok((requires_get, requires_set)) - } - - /// Parses `{ get; set; }` hook contracts for an interface property. - pub(super) fn parse_interface_property_hook_contracts( - &mut self, - ) -> Result<(bool, bool), EvalParseError> { - self.parse_property_hook_contracts() - } - - /// Parses retained property type metadata before the `$property` token. - pub(super) fn parse_optional_property_type( - &mut self, - ) -> Result, EvalParseError> { - if matches!( - self.current(), - TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis - ) { - return Ok(None); - } - self.parse_type_decl(EvalTypePosition::Property) - } - - /// Parses `function name($param, ...) { ... }` declarations. - pub(super) fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { - self.parse_function_decl_stmt_with_attributes(Vec::new()) - } - - /// Parses `function name($param, ...) { ... }` declarations with attributes. - pub(super) fn parse_function_decl_stmt_with_attributes( - &mut self, - attributes: Vec, - ) -> Result, EvalParseError> { - let source_start_line = self.current_line(); - self.advance(); - let TokenKind::Ident(name) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let name = self.qualify_name_in_current_namespace(name); - self.advance(); - self.expect(TokenKind::LParen)?; - let ParsedMethodParams { - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - promoted_properties, - promoted_assignments, - } = self.parse_method_params("", false)?; - if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { - return Err(EvalParseError::UnsupportedConstruct); - } - let return_type = self.parse_optional_return_type(EvalTypePosition::FunctionReturn)?; - let (body, source_end_line) = self.parse_block_with_end_line()?; - Ok(vec![EvalStmt::FunctionDecl { - name, - source_location: Some(EvalSourceLocation::new(source_start_line, source_end_line)), - attributes, - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - return_type, - body, - }]) - } - - /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. - pub(super) fn parse_namespace_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let namespace = if self.consume(TokenKind::LBrace) { - return self.parse_namespace_block(String::new()); - } else { - self.parse_namespace_name()? - }; - if self.consume_semicolon() { - self.namespace = namespace; - self.imports = NamespaceImports::default(); - return Ok(Vec::new()); - } - self.expect(TokenKind::LBrace)?; - self.parse_namespace_block(namespace) - } - - /// Parses statements inside an already opened namespace block. - pub(super) fn parse_namespace_block( - &mut self, - namespace: String, - ) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.namespace, namespace); - let previous_imports = std::mem::take(&mut self.imports); - let previous_allow_use_imports = std::mem::replace(&mut self.allow_use_imports, true); - let result = self.parse_block_contents(); - self.namespace = previous; - self.imports = previous_imports; - self.allow_use_imports = previous_allow_use_imports; - result - } - - /// Parses a namespace declaration name without a leading global separator. - pub(super) fn parse_namespace_name(&mut self) -> Result { - let name = self.parse_qualified_name()?; - if name.absolute { - return Err(EvalParseError::UnexpectedToken); - } - Ok(name.name) - } - - /// Parses PHP `use`, `use function`, and `use const` import declarations. - pub(super) fn parse_use_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let kind = self.parse_use_import_kind(); - - loop { - self.parse_use_import(kind)?; - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect_semicolon()?; - Ok(Vec::new()) - } - - /// Parses an optional top-level `function` or `const` use-import kind. - pub(super) fn parse_use_import_kind(&mut self) -> UseImportKind { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - self.advance(); - UseImportKind::Function - } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - self.advance(); - UseImportKind::Const - } else { - UseImportKind::Class - } - } - - /// Parses and registers one comma-separated import entry. - pub(super) fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { - let (name, grouped) = self.parse_use_name_or_group_start()?; - if grouped { - return self.parse_grouped_use_imports(kind, name); - } - self.parse_use_alias_and_register(kind, name) - } - - /// Parses a use-import name, stopping after a trailing namespace separator before `{`. - pub(super) fn parse_use_name_or_group_start( - &mut self, - ) -> Result<(String, bool), EvalParseError> { - let _ = self.consume(TokenKind::Backslash); - let TokenKind::Ident(first) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let mut name = first.clone(); - self.advance(); - while self.consume(TokenKind::Backslash) { - if self.consume(TokenKind::LBrace) { - return Ok((name, true)); - } - let TokenKind::Ident(part) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - name.push('\\'); - name.push_str(part); - self.advance(); - } - Ok((name, false)) - } - - /// Parses all members inside a grouped namespace import declaration. - pub(super) fn parse_grouped_use_imports( - &mut self, - default_kind: UseImportKind, - prefix: String, - ) -> Result<(), EvalParseError> { - if matches!(self.current(), TokenKind::RBrace) { - return Err(EvalParseError::UnexpectedToken); - } - loop { - let kind = self.parse_grouped_use_entry_kind(default_kind)?; - let member = self.parse_grouped_use_member_name()?; - let name = join_grouped_use_name(&prefix, &member); - self.parse_use_alias_and_register(kind, name)?; - if !self.consume(TokenKind::Comma) { - break; - } - if self.consume(TokenKind::RBrace) { - return Ok(()); - } - } - self.expect(TokenKind::RBrace) - } - - /// Parses an optional per-entry grouped import kind, matching PHP's mixed group rules. - pub(super) fn parse_grouped_use_entry_kind( - &mut self, - default_kind: UseImportKind, - ) -> Result { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { - if default_kind != UseImportKind::Class { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - return Ok(UseImportKind::Function); - } - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { - if default_kind != UseImportKind::Class { - return Err(EvalParseError::UnexpectedToken); - } - self.advance(); - return Ok(UseImportKind::Const); - } - Ok(default_kind) - } - - /// Parses one non-absolute member name inside a grouped use declaration. - pub(super) fn parse_grouped_use_member_name(&mut self) -> Result { - let name = self.parse_qualified_name()?; - if name.absolute { - return Err(EvalParseError::UnexpectedToken); - } - Ok(name.name) - } - - /// Parses an optional alias and stores one namespace import. - pub(super) fn parse_use_alias_and_register( - &mut self, - kind: UseImportKind, - name: String, - ) -> Result<(), EvalParseError> { - let alias = if matches!( - self.current(), - TokenKind::Ident(keyword) if ident_eq(keyword, "as") - ) { - self.advance(); - let TokenKind::Ident(alias) = self.current() else { - return Err(EvalParseError::UnexpectedToken); - }; - let alias = alias.clone(); - self.advance(); - alias - } else { - last_name_segment(&name).to_string() - }; - - match kind { - UseImportKind::Class => self.imports.insert_class(alias, name), - UseImportKind::Function => self.imports.insert_function(alias, name), - UseImportKind::Const => self.imports.insert_constant(alias, name), - } - Ok(()) - } - - /// Parses `global $name, $other;` declarations in eval fragments. - pub(super) fn parse_global_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let mut vars = Vec::new(); - loop { - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - vars.push(name.clone()); - self.advance(); - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect_semicolon()?; - Ok(vec![EvalStmt::Global { vars }]) - } - - /// Parses `static $name = expr;` declarations in eval fragments. - pub(super) fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let name = name.clone(); - self.advance(); - self.expect(TokenKind::Equal)?; - let init = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::StaticVar { name, init }]) - } - - /// Parses `throw expr;` statements in eval fragments. - pub(super) fn parse_throw_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let expr = self.parse_expr()?; - self.expect_semicolon()?; - Ok(vec![EvalStmt::Throw(expr)]) - } - - /// Parses `try { ... } catch (Type|Other $name) { ... } finally { ... }` statements. - pub(super) fn parse_try_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - let body = self.parse_block()?; - let mut catches = Vec::new(); - while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { - catches.push(self.parse_catch_clause()?); - } - let finally_body = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) - { - self.advance(); - self.parse_block()? - } else { - Vec::new() - }; - if catches.is_empty() && finally_body.is_empty() { - return Err(EvalParseError::UnexpectedToken); - } - Ok(vec![EvalStmt::Try { - body, - catches, - finally_body, - }]) - } - - /// Parses one `catch (ClassName|Other [$name]) { ... }` clause. - pub(super) fn parse_catch_clause(&mut self) -> Result { - self.advance(); - self.expect(TokenKind::LParen)?; - let class_names = self.parse_catch_types()?; - let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { - let var_name = var_name.clone(); - self.advance(); - Some(var_name) - } else { - None - }; - self.expect(TokenKind::RParen)?; - let body = self.parse_block()?; - Ok(EvalCatch { - class_names, - var_name, - body, - }) - } - - /// Parses one or more unioned catch types in source order. - pub(super) fn parse_catch_types(&mut self) -> Result, EvalParseError> { - let class_name = self.parse_class_reference_name(false)?; - let mut class_names = vec![self.resolve_class_name(class_name)]; - while self.consume(TokenKind::Pipe) { - let class_name = self.parse_class_reference_name(false)?; - class_names.push(self.resolve_class_name(class_name)); - } - Ok(class_names) - } - - /// Parses a method parameter list and records metadata plus promotion side effects. - pub(super) fn parse_method_params( - &mut self, - method_name: &str, - allow_class_scope_types: bool, - ) -> Result { - let mut params = Vec::new(); - let mut parameter_attributes = Vec::new(); - let mut parameter_types = Vec::new(); - let mut parameter_defaults = Vec::new(); - let mut parameter_is_by_ref = Vec::new(); - let mut parameter_is_variadic = Vec::new(); - let mut promoted_properties = Vec::new(); - let mut promoted_assignments = Vec::new(); - if self.consume(TokenKind::RParen) { - return Ok(ParsedMethodParams { - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - promoted_properties, - promoted_assignments, - }); - } - loop { - let attributes = self.parse_attribute_groups()?; - let promotion = self.parse_promoted_parameter_modifiers()?; - if promotion.is_some() && !method_name.eq_ignore_ascii_case("__construct") { - return Err(EvalParseError::UnsupportedConstruct); - } - let param_type = if promotion.is_some() { - self.parse_optional_promoted_property_type()? - } else { - let position = if allow_class_scope_types { - EvalTypePosition::MethodParameter - } else { - EvalTypePosition::FunctionParameter - }; - self.parse_optional_parameter_type(position)? - }; - let is_by_ref = self.consume(TokenKind::Ampersand); - let is_variadic = self.consume(TokenKind::Ellipsis); - let TokenKind::DollarIdent(name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - if promotion.is_some() && is_variadic { - return Err(EvalParseError::UnsupportedConstruct); - } - if let Some((visibility, is_readonly)) = promotion { - promoted_properties.push( - EvalClassProperty::with_visibility_static_final_and_readonly( - name.clone(), - visibility, - false, - false, - is_readonly, - None, - ) - .with_type(param_type.clone()) - .with_promoted() - .with_attributes(attributes.clone()), - ); - promoted_assignments.push(promoted_property_assignment(name, is_by_ref)); - } - params.push(name.clone()); - parameter_attributes.push(attributes); - parameter_types.push(param_type); - parameter_is_by_ref.push(is_by_ref); - parameter_is_variadic.push(is_variadic); - self.advance(); - let default = if self.consume(TokenKind::Equal) { - if is_variadic { - return Err(EvalParseError::UnsupportedConstruct); - } - let default = self.parse_expr()?; - if !method_parameter_default_is_supported(&default) { - return Err(EvalParseError::UnsupportedConstruct); - } - Some(default) - } else { - None - }; - parameter_defaults.push(default); - if !self.consume(TokenKind::Comma) { - break; - } - if is_variadic { - return Err(EvalParseError::UnsupportedConstruct); - } - if matches!(self.current(), TokenKind::RParen) { - return Err(EvalParseError::ExpectedVariable); - } - } - self.expect(TokenKind::RParen)?; - Ok(ParsedMethodParams { - params, - parameter_attributes, - parameter_types, - parameter_defaults, - parameter_is_by_ref, - parameter_is_variadic, - promoted_properties, - promoted_assignments, - }) - } - - /// Parses visibility and readonly modifiers on a promoted constructor parameter. - fn parse_promoted_parameter_modifiers( - &mut self, - ) -> Result, EvalParseError> { - let mut visibility = None; - let mut is_readonly = false; - let mut saw_modifier = false; - loop { - match self.current() { - TokenKind::Ident(name) if ident_eq(name, "public") => { - if visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - saw_modifier = true; - visibility = Some(EvalVisibility::Public); - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "protected") => { - if visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - saw_modifier = true; - visibility = Some(EvalVisibility::Protected); - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "private") => { - if visibility.is_some() { - return Err(EvalParseError::UnsupportedConstruct); - } - saw_modifier = true; - visibility = Some(EvalVisibility::Private); - self.advance(); - } - TokenKind::Ident(name) if ident_eq(name, "readonly") => { - if is_readonly { - return Err(EvalParseError::UnsupportedConstruct); - } - saw_modifier = true; - is_readonly = true; - self.advance(); - } - _ => break, - } - } - if saw_modifier { - Ok(Some(( - visibility.unwrap_or(EvalVisibility::Public), - is_readonly, - ))) - } else { - Ok(None) - } - } - - /// Parses a constructor-promoted parameter type using PHP property-type restrictions. - fn parse_optional_promoted_property_type( - &mut self, - ) -> Result, EvalParseError> { - if matches!( - self.current(), - TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis - ) { - return Ok(None); - } - self.parse_type_decl(EvalTypePosition::Property) - } - - /// Consumes a supported method parameter type and returns retained metadata. - fn parse_optional_parameter_type( - &mut self, - position: EvalTypePosition, - ) -> Result, EvalParseError> { - if matches!( - self.current(), - TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis - ) { - return Ok(None); - } - self.parse_type_decl(position) - } - - /// Consumes a supported function or method return type after `:`. - pub(super) fn parse_optional_return_type( - &mut self, - position: EvalTypePosition, - ) -> Result, EvalParseError> { - if !self.consume(TokenKind::Colon) { - return Ok(None); - } - self.parse_type_decl(position) - } - - /// Parses one PHP type declaration and returns retained eval metadata. - fn parse_type_decl( - &mut self, - position: EvalTypePosition, - ) -> Result, EvalParseError> { - let nullable_shorthand = self.consume(TokenKind::Question); - if nullable_shorthand && matches!(self.current(), TokenKind::DollarIdent(_)) { - return Err(EvalParseError::UnexpectedToken); - } - let first = self.parse_type_name(position)?; - let mut variants = Vec::new(); - let mut allows_null = nullable_shorthand || matches!(first, None); - if let Some(first) = first { - variants.push(first); - } - if nullable_shorthand && matches!(self.current(), TokenKind::Pipe) { - return Err(EvalParseError::UnsupportedConstruct); - } - if nullable_shorthand - && matches!(self.current(), TokenKind::Ampersand) - && !self.next_token_starts_parameter_storage() - { - return Err(EvalParseError::UnsupportedConstruct); - } - if matches!(self.current(), TokenKind::Ampersand) - && !self.next_token_starts_parameter_storage() - { - while self.consume(TokenKind::Ampersand) { - let Some(variant) = self.parse_type_name(position)? else { - return Err(EvalParseError::UnsupportedConstruct); - }; - variants.push(variant); - } - if type_variants_contain_standalone_return_only_atoms(&variants) { - return Err(EvalParseError::UnsupportedConstruct); - } - return Ok(Some(EvalParameterType::intersection(variants))); - } - while self.consume(TokenKind::Pipe) { - match self.parse_type_name(position)? { - Some(variant) => variants.push(variant), - None => allows_null = true, - } - } - if type_variants_contain_standalone_return_only_atoms(&variants) - && (variants.len() != 1 || allows_null) - { - return Err(EvalParseError::UnsupportedConstruct); - } - Ok(Some(EvalParameterType::new(variants, allows_null))) - } - - /// Returns whether `&` belongs to by-reference parameter storage. - fn next_token_starts_parameter_storage(&self) -> bool { - matches!(self.peek(), TokenKind::DollarIdent(_) | TokenKind::Ellipsis) - } - - /// Consumes one simple qualified method type name. - fn parse_type_name( - &mut self, - position: EvalTypePosition, - ) -> Result, EvalParseError> { - match self.current() { - TokenKind::Ident(_) | TokenKind::Backslash => { - let name = self.parse_qualified_name()?; - self.type_variant_from_name(name, position) - } - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Converts one parsed PHP type name to retained eval metadata. - fn type_variant_from_name( - &self, - name: ParsedQualifiedName, - position: EvalTypePosition, - ) -> Result, EvalParseError> { - if !name.absolute { - let lower = name.name.to_ascii_lowercase(); - let builtin = match lower.as_str() { - "array" => Some(EvalParameterTypeVariant::Array), - "bool" => Some(EvalParameterTypeVariant::Bool), - "callable" if matches!(position, EvalTypePosition::Property) => { - return Err(EvalParseError::UnsupportedConstruct); - } - "callable" => Some(EvalParameterTypeVariant::Callable), - "float" => Some(EvalParameterTypeVariant::Float), - "int" => Some(EvalParameterTypeVariant::Int), - "iterable" => Some(EvalParameterTypeVariant::Iterable), - "mixed" => Some(EvalParameterTypeVariant::Mixed), - "never" if type_position_allows_return_only_atoms(position) => { - Some(EvalParameterTypeVariant::Never) - } - "null" => return Ok(None), - "object" => Some(EvalParameterTypeVariant::Object), - "string" => Some(EvalParameterTypeVariant::String), - "void" if type_position_allows_return_only_atoms(position) => { - Some(EvalParameterTypeVariant::Void) - } - "void" | "never" => return Err(EvalParseError::UnsupportedConstruct), - "static" if matches!(position, EvalTypePosition::MethodReturn) => { - Some(EvalParameterTypeVariant::Class(lower.to_string())) - } - "static" => return Err(EvalParseError::UnsupportedConstruct), - "self" | "parent" if !type_position_allows_class_scope_atoms(position) => { - return Err(EvalParseError::UnsupportedConstruct); - } - "self" | "parent" => { - Some(EvalParameterTypeVariant::Class(lower.to_string())) - } - _ => None, - }; - if let Some(builtin) = builtin { - return Ok(Some(builtin)); - } - } - Ok(Some(EvalParameterTypeVariant::Class( - self.resolve_class_name(name), - ))) - } - - /// Parses the optional first clause of a `for` loop. - pub(super) fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::Semicolon) { - return Ok(Vec::new()); - } - self.parse_for_clause_stmt() - } - - /// Parses the optional update clause of a `for` loop. - pub(super) fn parse_for_update_clause(&mut self) -> Result, EvalParseError> { - if self.consume(TokenKind::RParen) { - return Ok(Vec::new()); - } - let statements = self.parse_for_clause_stmt()?; - self.expect(TokenKind::RParen)?; - Ok(statements) - } - - /// Parses one statement-like `for` clause without consuming a delimiter. - pub(super) fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { - match self.current() { - TokenKind::PlusPlus | TokenKind::MinusMinus - if self.current_starts_prefixed_static_property_inc_dec() => - { - self.parse_static_property_inc_dec_stmt(true, false) - } - TokenKind::PlusPlus | TokenKind::MinusMinus - if self.current_starts_prefixed_dynamic_static_property_inc_dec() => - { - self.parse_dynamic_static_property_inc_dec_stmt(true, false) - } - TokenKind::PlusPlus | TokenKind::MinusMinus - if self.current_starts_prefixed_property_inc_dec() => - { - self.parse_prefixed_property_inc_dec_stmt(false) - } - TokenKind::PlusPlus | TokenKind::MinusMinus => { - self.parse_prefix_inc_dec_stmt(false) - } - TokenKind::Ident(_) | TokenKind::Backslash - if self.current_starts_static_property_postfix_inc_dec() => - { - self.parse_static_property_inc_dec_stmt(false, false) - } - TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { - self.parse_array_set_clause(name.clone()) - } - TokenKind::DollarIdent(_) - if self.current_starts_dynamic_static_property_postfix_inc_dec() => - { - self.parse_dynamic_static_property_inc_dec_stmt(false, false) - } - TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { - self.parse_property_stmt(false) - } - TokenKind::DollarIdent(name) - if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => - { - self.parse_postfix_inc_dec_stmt(name.clone(), false) - } - TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { - let name = name.clone(); - self.parse_var_store_stmt(name, false) - } - _ => { - let expr = self.parse_expr()?; - self.parse_property_like_stmt_tail(expr, false) - } - } - } - - /// Parses `$name[index] = expr` and `$name[] = expr` in a `for` clause. - pub(super) fn parse_array_set_clause( - &mut self, - name: String, - ) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LBracket)?; - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) - } - - /// Parses `$name = expr` and simple variable compound assignments. - pub(super) fn parse_var_store_stmt( - &mut self, - name: String, - require_semicolon: bool, - ) -> Result, EvalParseError> { - self.advance(); - let Some(op) = assignment_op(self.current()) else { - return Err(EvalParseError::UnexpectedToken); - }; - self.advance(); - if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { - self.advance(); - let TokenKind::DollarIdent(source) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let source = source.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::ReferenceAssign { - target: name, - source, - }]); - } - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - let value = assignment_value(&name, op, value); - Ok(vec![EvalStmt::StoreVar { name, value }]) - } - - /// Parses `Class::$property = expr` and simple static-property compound assignments. - pub(super) fn parse_static_property_set_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let class_name = self.parse_qualified_name()?; - let class_name = self.resolve_static_class_name(class_name); - self.expect(TokenKind::DoubleColon)?; - let TokenKind::DollarIdent(property) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let property = property.clone(); - self.advance(); - if self.consume(TokenKind::LBracket) { - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::StaticPropertyArrayAppend { - class_name, - property, - value, - }]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - let Some(op) = assignment_op(self.current()) else { - return Err(EvalParseError::UnexpectedToken); - }; - self.advance(); - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::StaticPropertyArraySet { - class_name, - property, - index, - op, - value, - }]); - } - let Some(op) = assignment_op(self.current()) else { - return Err(EvalParseError::UnexpectedToken); - }; - self.advance(); - if op.is_none() && self.consume(TokenKind::Ampersand) { - let TokenKind::DollarIdent(source) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let source = source.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::StaticPropertyReferenceBind { - class_name, - property, - source, - }]); - } - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - let value = match op { - Some(op) => EvalExpr::Binary { - op, - left: Box::new(EvalExpr::StaticPropertyGet { - class_name: class_name.clone(), - property: property.clone(), - }), - right: Box::new(value), - }, - None => value, - }; - Ok(vec![EvalStmt::StaticPropertySet { - class_name, - property, - value, - }]) - } - - /// Parses `$class::$property = expr` and compound assignments with a dynamic receiver. - pub(super) fn parse_dynamic_static_property_set_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let TokenKind::DollarIdent(class_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let class_name = EvalExpr::LoadVar(class_name.clone()); - self.advance(); - self.expect(TokenKind::DoubleColon)?; - let TokenKind::DollarIdent(property) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let property = property.clone(); - self.advance(); - if self.consume(TokenKind::LBracket) { - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::DynamicStaticPropertyArrayAppend { - class_name, - property, - value, - }]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - let Some(op) = assignment_op(self.current()) else { - return Err(EvalParseError::UnexpectedToken); - }; - self.advance(); - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::DynamicStaticPropertyArraySet { - class_name, - property, - index, - op, - value, - }]); - } - let Some(op) = assignment_op(self.current()) else { - return Err(EvalParseError::UnexpectedToken); - }; - self.advance(); - if op.is_none() && self.consume(TokenKind::Ampersand) { - let TokenKind::DollarIdent(source) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let source = source.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::DynamicStaticPropertyReferenceBind { - class_name, - property, - source, - }]); - } - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - let value = match op { - Some(op) => EvalExpr::Binary { - op, - left: Box::new(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(class_name.clone()), - property: property.clone(), - }), - right: Box::new(value), - }, - None => value, - }; - Ok(vec![EvalStmt::DynamicStaticPropertySet { - class_name, - property, - value, - }]) - } - - /// Parses static property increment/decrement as read-modify-write. - pub(super) fn parse_static_property_inc_dec_stmt( - &mut self, - prefixed: bool, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let prefix_increment = if prefixed { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - Some(increment) - } else { - None - }; - let class_name = self.parse_qualified_name()?; - let class_name = self.resolve_static_class_name(class_name); - self.expect(TokenKind::DoubleColon)?; - let TokenKind::DollarIdent(property) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let property = property.clone(); - self.advance(); - let increment = if let Some(increment) = prefix_increment { - increment - } else { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - increment - }; - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![EvalStmt::StaticPropertyIncDec { - class_name, - property, - increment, - }]) - } - - /// Parses dynamic static property increment/decrement as read-modify-write. - pub(super) fn parse_dynamic_static_property_inc_dec_stmt( - &mut self, - prefixed: bool, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let prefix_increment = if prefixed { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - Some(increment) - } else { - None - }; - let TokenKind::DollarIdent(class_name) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let class_name = EvalExpr::LoadVar(class_name.clone()); - self.advance(); - self.expect(TokenKind::DoubleColon)?; - let TokenKind::DollarIdent(property) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let property = property.clone(); - self.advance(); - let increment = if let Some(increment) = prefix_increment { - increment - } else { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - increment - }; - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![EvalStmt::DynamicStaticPropertyIncDec { - class_name, - property, - increment, - }]) - } - - /// Parses prefix `++$name` / `--$name` and supported property-like prefix mutations. - pub(super) fn parse_prefix_inc_dec_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - if let TokenKind::DollarIdent(name) = self.current() { - if !matches!( - self.peek(), - TokenKind::DoubleColon - | TokenKind::Arrow - | TokenKind::QuestionArrow - | TokenKind::LBracket - ) { - let name = name.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![inc_dec_store(name, increment)]); - } - } - let target = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]) - } - - /// Parses postfix `$name++` and `$name--` as simple statement effects. - pub(super) fn parse_postfix_inc_dec_stmt( - &mut self, - name: String, - require_semicolon: bool, - ) -> Result, EvalParseError> { - self.advance(); - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - Ok(vec![inc_dec_store(name, increment)]) - } - - /// Parses prefix property increment/decrement as read-modify-write. - pub(super) fn parse_prefixed_property_inc_dec_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - let target = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]) - } - - /// Parses `$object->property` as either an expression statement or property write. - pub(super) fn parse_property_stmt( - &mut self, - require_semicolon: bool, - ) -> Result, EvalParseError> { - let target = self.parse_expr()?; - self.parse_property_like_stmt_tail(target, require_semicolon) - } - - /// Parses assignment, array-write, or inc/dec tails after a parsed property-like target. - fn parse_property_like_stmt_tail( - &mut self, - target: EvalExpr, - require_semicolon: bool, - ) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) { - let increment = matches!(self.current(), TokenKind::PlusPlus); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - return property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]); - } - if self.consume(TokenKind::LBracket) { - if self.consume(TokenKind::RBracket) { - self.expect(TokenKind::Equal)?; - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - return property_array_append_stmt(target, value).map(|stmt| vec![stmt]); - } - let index = self.parse_expr()?; - self.expect(TokenKind::RBracket)?; - let Some(op) = assignment_op(self.current()) else { - return Err(EvalParseError::UnexpectedToken); - }; - self.advance(); - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - return property_array_set_stmt(target, index, op, value).map(|stmt| vec![stmt]); - } - let Some(op) = assignment_op(self.current()) else { - if require_semicolon { - self.expect_semicolon()?; - } - return Ok(vec![EvalStmt::Expr(target)]); - }; - self.advance(); - if op.is_none() && self.consume(TokenKind::Ampersand) { - let TokenKind::DollarIdent(source) = self.current() else { - return Err(EvalParseError::ExpectedVariable); - }; - let source = source.clone(); - self.advance(); - if require_semicolon { - self.expect_semicolon()?; - } - return property_reference_bind_stmt(target, source).map(|stmt| vec![stmt]); - } - let value = self.parse_expr()?; - if require_semicolon { - self.expect_semicolon()?; - } - match (target, op) { - (EvalExpr::ArrayGet { array, index }, op) => { - property_array_set_stmt(*array, *index, op, value).map(|stmt| vec![stmt]) - } - (EvalExpr::PropertyGet { object, property }, None) => Ok(vec![EvalStmt::PropertySet { - object: *object, - property, - value, - }]), - (EvalExpr::PropertyGet { object, property }, Some(op)) => { - Ok(vec![EvalStmt::PropertyCompoundAssign { - object: *object, - property, - op, - value, - }]) - } - (EvalExpr::DynamicPropertyGet { object, property }, None) => { - Ok(vec![EvalStmt::DynamicPropertySet { - object: *object, - property: *property, - value, - }]) - } - (EvalExpr::DynamicPropertyGet { object, property }, Some(op)) => { - Ok(vec![EvalStmt::DynamicPropertyCompoundAssign { - object: *object, - property: *property, - op, - value, - }]) - } - ( - EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - }, - op, - ) => { - let class_name = *class_name; - let value = match op { - Some(op) => EvalExpr::Binary { - op, - left: Box::new(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(class_name.clone()), - property: property.clone(), - }), - right: Box::new(value), - }, - None => value, - }; - Ok(vec![EvalStmt::DynamicStaticPropertySet { - class_name, - property, - value, - }]) - } - ( - EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - }, - op, - ) => { - let class_name = *class_name; - let property = *property; - let value = match op { - Some(op) => EvalExpr::Binary { - op, - left: Box::new(EvalExpr::DynamicStaticPropertyNameGet { - class_name: Box::new(class_name.clone()), - property: Box::new(property.clone()), - }), - right: Box::new(value), - }, - None => value, - }; - Ok(vec![EvalStmt::DynamicStaticPropertyNameSet { - class_name, - property, - value, - }]) - } - _ => Err(EvalParseError::UnexpectedToken), - } - } - - /// Parses a complete `if` statement after consuming the `if` keyword. - pub(super) fn parse_if_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - Ok(vec![self.parse_if_after_keyword()?]) - } - - /// Parses the condition, then block, and optional else branch for an `if` chain. - pub(super) fn parse_if_after_keyword(&mut self) -> Result { - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - let then_branch = self.parse_statement_body()?; - let else_branch = self.parse_optional_else_branch()?; - Ok(EvalStmt::If { - condition, - then_branch, - else_branch, - }) - } - - /// Parses `elseif`, `else if`, or `else` branches after an `if` body. - pub(super) fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { - self.advance(); - return Ok(vec![self.parse_if_after_keyword()?]); - } - if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "else")) { - return Ok(Vec::new()); - } - self.advance(); - if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "if")) { - self.advance(); - Ok(vec![self.parse_if_after_keyword()?]) - } else { - self.parse_statement_body() - } - } - - /// Parses `switch (expr) { case expr: ... default: ... }`. - pub(super) fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let expr = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - self.expect(TokenKind::LBrace)?; - let mut cases = Vec::new(); - while !matches!(self.current(), TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - cases.push(self.parse_switch_case()?); - } - self.expect(TokenKind::RBrace)?; - Ok(vec![EvalStmt::Switch { expr, cases }]) - } - - /// Parses one `case` or `default` arm inside a switch body. - pub(super) fn parse_switch_case(&mut self) -> Result { - let condition = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) - { - self.advance(); - let condition = self.parse_expr()?; - Some(condition) - } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { - self.advance(); - None - } else { - return Err(EvalParseError::UnexpectedToken); - }; - self.expect(TokenKind::Colon)?; - let body = self.parse_switch_case_body()?; - Ok(EvalSwitchCase { condition, body }) - } - - /// Parses case body statements until the next case boundary or switch close. - pub(super) fn parse_switch_case_body(&mut self) -> Result, EvalParseError> { - let mut body = Vec::new(); - while !is_switch_case_boundary(self.current()) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - body.extend(self.parse_stmt()?); - } - Ok(body) - } - - /// Parses `unset($name[, ...]);` with variable, array-access, and property operands. - pub(super) fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let mut statements = Vec::new(); - loop { - let target = self.parse_expr()?; - let stmt = match target { - EvalExpr::ArrayGet { array, index } => EvalStmt::UnsetArrayElement { - array: *array, - index: *index, - }, - EvalExpr::LoadVar(name) => EvalStmt::UnsetVar { name }, - EvalExpr::PropertyGet { object, property } => EvalStmt::UnsetProperty { - object: *object, - property, - }, - EvalExpr::DynamicPropertyGet { object, property } => { - EvalStmt::UnsetDynamicProperty { - object: *object, - property: *property, - } - } - EvalExpr::StaticPropertyGet { - class_name, - property, - } => EvalStmt::UnsetStaticProperty { - class_name, - property, - }, - EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } => EvalStmt::UnsetDynamicStaticProperty { - class_name: *class_name, - property, - }, - EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } => EvalStmt::UnsetDynamicStaticPropertyName { - class_name: *class_name, - property: *property, - }, - _ => return Err(EvalParseError::ExpectedVariable), - }; - statements.push(stmt); - if !self.consume(TokenKind::Comma) { - break; - } - } - self.expect(TokenKind::RParen)?; - self.expect_semicolon()?; - Ok(statements) - } - - /// Parses `while (expr) { ... }`. - pub(super) fn parse_while_stmt(&mut self) -> Result, EvalParseError> { - self.advance(); - self.expect(TokenKind::LParen)?; - let condition = self.parse_expr()?; - self.expect(TokenKind::RParen)?; - let body = self.parse_statement_body()?; - Ok(vec![EvalStmt::While { condition, body }]) - } - - /// Parses either a brace-delimited block or one braceless statement body. - pub(super) fn parse_statement_body(&mut self) -> Result, EvalParseError> { - if matches!(self.current(), TokenKind::LBrace) { - self.parse_block() - } else { - self.parse_nested_stmt() - } - } - - /// Parses a brace-delimited statement block. - pub(super) fn parse_block(&mut self) -> Result, EvalParseError> { - self.expect(TokenKind::LBrace)?; - self.parse_nested_block_contents() - } - - /// Parses a brace-delimited statement block and returns the closing-brace line. - pub(super) fn parse_block_with_end_line( - &mut self, - ) -> Result<(Vec, i64), EvalParseError> { - self.expect(TokenKind::LBrace)?; - self.parse_nested_block_contents_with_end_line() - } - - /// Parses one nested statement where import declarations are not legal. - pub(super) fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.allow_use_imports, false); - let result = self.parse_stmt(); - self.allow_use_imports = previous; - result - } - - /// Parses a nested block while preserving active imports for name resolution. - pub(super) fn parse_nested_block_contents(&mut self) -> Result, EvalParseError> { - let previous = std::mem::replace(&mut self.allow_use_imports, false); - let result = self.parse_block_contents(); - self.allow_use_imports = previous; - result - } - - /// Parses a nested block and returns the closing-brace line. - pub(super) fn parse_nested_block_contents_with_end_line( - &mut self, - ) -> Result<(Vec, i64), EvalParseError> { - let previous = std::mem::replace(&mut self.allow_use_imports, false); - let result = self.parse_block_contents_with_end_line(); - self.allow_use_imports = previous; - result - } - - /// Parses statements until the closing brace for the current block. - pub(super) fn parse_block_contents(&mut self) -> Result, EvalParseError> { - let mut statements = Vec::new(); - while !matches!(self.current(), TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - statements.extend(self.parse_stmt()?); - } - self.expect(TokenKind::RBrace)?; - Ok(statements) - } - - /// Parses statements until the closing brace and returns that brace's line. - pub(super) fn parse_block_contents_with_end_line( - &mut self, - ) -> Result<(Vec, i64), EvalParseError> { - let mut statements = Vec::new(); - while !matches!(self.current(), TokenKind::RBrace) { - if matches!(self.current(), TokenKind::Eof) { - return Err(EvalParseError::UnexpectedEof); - } - statements.extend(self.parse_stmt()?); - } - let source_end_line = self.current_line(); - self.expect(TokenKind::RBrace)?; - Ok((statements, source_end_line)) - } -} - -/// Returns whether an eval method parameter default can be materialized safely. -fn method_parameter_default_is_supported(default: &EvalExpr) -> bool { - eval_constant_expression_default_is_supported(default) -} - -/// Returns whether an EvalIR expression is safe to retain as a method default. -fn eval_constant_expression_default_is_supported(expr: &EvalExpr) -> bool { - match expr { - EvalExpr::Array(elements) => elements.iter().all(eval_array_element_default_is_supported), - EvalExpr::Const(_) => true, - EvalExpr::Magic(_) => true, - EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, - EvalExpr::ClassConstantFetch { class_name, .. } - | EvalExpr::ClassNameFetch { class_name } => { - eval_default_class_receiver_is_supported(class_name) - } - EvalExpr::NewObject { class_name, args } => { - eval_default_class_receiver_is_supported(class_name) - && args.iter().all(eval_call_arg_default_is_supported) - } - EvalExpr::NewAnonymousClass { .. } => false, - EvalExpr::NullCoalesce { value, default } => { - eval_constant_expression_default_is_supported(value) - && eval_constant_expression_default_is_supported(default) - } - EvalExpr::Ternary { - condition, - then_branch, - else_branch, - } => { - eval_constant_expression_default_is_supported(condition) - && then_branch - .as_deref() - .is_none_or(eval_constant_expression_default_is_supported) - && eval_constant_expression_default_is_supported(else_branch) - } - EvalExpr::Cast { expr, .. } => eval_constant_expression_default_is_supported(expr), - EvalExpr::Unary { expr, .. } => eval_constant_expression_default_is_supported(expr), - EvalExpr::Binary { left, right, .. } => { - eval_constant_expression_default_is_supported(left) - && eval_constant_expression_default_is_supported(right) - } - _ => false, - } -} - -/// Returns whether one object-construction argument is safe inside a method default. -fn eval_call_arg_default_is_supported(arg: &EvalCallArg) -> bool { - !arg.is_spread() && eval_constant_expression_default_is_supported(arg.value()) -} - -/// Returns whether one array default element contains only supported constant expressions. -fn eval_array_element_default_is_supported(element: &EvalArrayElement) -> bool { - match element { - EvalArrayElement::Value(value) => eval_constant_expression_default_is_supported(value), - EvalArrayElement::Reference(_) => false, - EvalArrayElement::KeyValue { key, value } => { - eval_constant_expression_default_is_supported(key) - && eval_constant_expression_default_is_supported(value) - } - EvalArrayElement::KeyReference { .. } => false, - } -} - -/// Returns whether a type list contains return-only standalone atoms. -fn type_variants_contain_standalone_return_only_atoms( - variants: &[EvalParameterTypeVariant], -) -> bool { - variants.iter().any(|variant| { - matches!( - variant, - EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void - ) - }) -} - -/// Returns whether the type position accepts standalone return-only atoms. -fn type_position_allows_return_only_atoms(position: EvalTypePosition) -> bool { - matches!( - position, - EvalTypePosition::FunctionReturn | EvalTypePosition::MethodReturn - ) -} - -/// Returns whether `self` and `parent` are legal in this type position. -fn type_position_allows_class_scope_atoms(position: EvalTypePosition) -> bool { - !matches!( - position, - EvalTypePosition::FunctionParameter | EvalTypePosition::FunctionReturn - ) -} - -/// Returns whether a class-like receiver is legal in a compile-time method default. -fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { - !class_name - .trim_start_matches('\\') - .eq_ignore_ascii_case("static") -} - -/// Builds a property by-reference binding statement from a parsed property target. -fn property_reference_bind_stmt( - target: EvalExpr, - source: String, -) -> Result { - match target { - EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyReferenceBind { - object: *object, - property, - source, - }), - EvalExpr::DynamicPropertyGet { object, property } => { - Ok(EvalStmt::DynamicPropertyReferenceBind { - object: *object, - property: *property, - source, - }) - } - EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } => Ok(EvalStmt::DynamicStaticPropertyReferenceBind { - class_name: *class_name, - property, - source, - }), - EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } => Ok(EvalStmt::DynamicStaticPropertyNameReferenceBind { - class_name: *class_name, - property: *property, - source, - }), - _ => Err(EvalParseError::UnexpectedToken), - } -} - -/// Builds an object-property increment/decrement statement from a parsed property target. -fn property_inc_dec_stmt(target: EvalExpr, increment: bool) -> Result { - match target { - EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyIncDec { - object: *object, - property, - increment, - }), - EvalExpr::DynamicPropertyGet { object, property } => Ok(EvalStmt::DynamicPropertyIncDec { - object: *object, - property: *property, - increment, - }), - EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } => Ok(EvalStmt::DynamicStaticPropertyIncDec { - class_name: *class_name, - property, - increment, - }), - EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } => Ok(EvalStmt::DynamicStaticPropertyNameIncDec { - class_name: *class_name, - property: *property, - increment, - }), - _ => Err(EvalParseError::UnexpectedToken), - } -} - -/// Builds an object-property array append statement from a parsed property target. -fn property_array_append_stmt(target: EvalExpr, value: EvalExpr) -> Result { - match target { - EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyArrayAppend { - object: *object, - property, - value, - }), - EvalExpr::DynamicPropertyGet { object, property } => { - Ok(EvalStmt::DynamicPropertyArrayAppend { - object: *object, - property: *property, - value, - }) - } - EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } => Ok(EvalStmt::DynamicStaticPropertyArrayAppend { - class_name: *class_name, - property, - value, - }), - EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } => Ok(EvalStmt::DynamicStaticPropertyNameArrayAppend { - class_name: *class_name, - property: *property, - value, - }), - _ => Err(EvalParseError::UnexpectedToken), - } -} - -/// Builds an object-property array write statement from a parsed property target. -fn property_array_set_stmt( - target: EvalExpr, - index: EvalExpr, - op: Option, - value: EvalExpr, -) -> Result { - match target { - EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyArraySet { - object: *object, - property, - index, - op, - value, - }), - EvalExpr::DynamicPropertyGet { object, property } => Ok(EvalStmt::DynamicPropertyArraySet { - object: *object, - property: *property, - index, - op, - value, - }), - EvalExpr::DynamicStaticPropertyGet { - class_name, - property, - } => Ok(EvalStmt::DynamicStaticPropertyArraySet { - class_name: *class_name, - property, - index, - op, - value, - }), - EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } => Ok(EvalStmt::DynamicStaticPropertyNameArraySet { - class_name: *class_name, - property: *property, - index, - op, - value, - }), - _ => Err(EvalParseError::UnexpectedToken), - } -} - -/// Converts a parsed attribute argument expression into retained literal metadata. -fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { - match expr { - EvalExpr::Const(EvalConst::String(value)) => Some(EvalAttributeArg::String(value.clone())), - EvalExpr::Const(EvalConst::Int(value)) => Some(EvalAttributeArg::Int(*value)), - EvalExpr::Const(EvalConst::Float(value)) => Some(EvalAttributeArg::Float(value.to_bits())), - EvalExpr::Const(EvalConst::Bool(value)) => Some(EvalAttributeArg::Bool(*value)), - EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Null), - EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr, - } => match expr.as_ref() { - EvalExpr::Const(EvalConst::Int(value)) => { - Some(EvalAttributeArg::Int(value.wrapping_neg())) - } - EvalExpr::Const(EvalConst::Float(value)) => { - Some(EvalAttributeArg::Float((-*value).to_bits())) - } - _ => None, - }, - EvalExpr::ClassNameFetch { class_name } => { - eval_attribute_class_name_arg(class_name).map(EvalAttributeArg::String) - } - EvalExpr::Array(elements) => eval_attribute_array_arg_from_elements(elements), - _ => None, - } -} - -/// Converts an eval array literal into retained attribute metadata. -fn eval_attribute_array_arg_from_elements( - elements: &[EvalArrayElement], -) -> Option { - elements - .iter() - .map(|element| match element { - EvalArrayElement::Value(value) => eval_attribute_arg_from_expr(value), - EvalArrayElement::Reference(_) => None, - EvalArrayElement::KeyValue { key, value } => { - let value = eval_attribute_arg_from_expr(value)?; - eval_attribute_array_keyed_arg(key, value) - } - EvalArrayElement::KeyReference { .. } => None, - }) - .collect::>>() - .map(EvalAttributeArg::Array) -} - -/// Wraps an attribute array value with the PHP-normalized literal key metadata. -fn eval_attribute_array_keyed_arg( - key: &EvalExpr, - value: EvalAttributeArg, -) -> Option { - match key { - EvalExpr::Const(EvalConst::String(name)) => Some(EvalAttributeArg::Named { - name: name.clone(), - value: Box::new(value), - }), - EvalExpr::Const(EvalConst::Int(key)) => Some(EvalAttributeArg::IntKeyed { - key: *key, - value: Box::new(value), - }), - EvalExpr::Const(EvalConst::Bool(key)) => Some(EvalAttributeArg::IntKeyed { - key: i64::from(*key), - value: Box::new(value), - }), - EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Named { - name: String::new(), - value: Box::new(value), - }), - EvalExpr::Const(EvalConst::Float(key)) => Some(EvalAttributeArg::IntKeyed { - key: *key as i64, - value: Box::new(value), - }), - EvalExpr::Unary { - op: EvalUnaryOp::Negate, - expr, - } => eval_attribute_array_negated_keyed_arg(expr, value), - EvalExpr::ClassNameFetch { class_name } => { - eval_attribute_class_name_arg(class_name).map(|name| EvalAttributeArg::Named { - name, - value: Box::new(value), - }) - } - _ => None, - } -} - -/// Wraps an attribute array value with a normalized negative numeric literal key. -fn eval_attribute_array_negated_keyed_arg( - key: &EvalExpr, - value: EvalAttributeArg, -) -> Option { - match key { - EvalExpr::Const(EvalConst::Int(key)) => Some(EvalAttributeArg::IntKeyed { - key: key.wrapping_neg(), - value: Box::new(value), - }), - EvalExpr::Const(EvalConst::Float(key)) => Some(EvalAttributeArg::IntKeyed { - key: (-*key) as i64, - value: Box::new(value), - }), - _ => None, - } -} - -/// Returns a compile-time class-name string for named `ClassName::class` attribute args. -fn eval_attribute_class_name_arg(class_name: &str) -> Option { - let class_name = class_name.trim_start_matches('\\'); - if ["self", "parent", "static"] - .iter() - .any(|special| class_name.eq_ignore_ascii_case(special)) - { - return None; - } - Some(class_name.to_string()) -} - -/// Returns whether any parsed property hook accessor uses its own backing slot. -fn property_hook_methods_use_backing_slot( - hook_methods: &[EvalClassMethod], - property_name: &str, -) -> bool { - hook_methods.iter().any(|method| { - method - .body() - .iter() - .any(|stmt| eval_stmt_uses_this_property(stmt, property_name)) - }) -} - -/// Returns whether one statement touches `$this->{$property_name}` directly. -fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { - match stmt { - EvalStmt::ArrayAppendVar { value, .. } => { - eval_expr_uses_this_property(value, property_name) - } - EvalStmt::ArraySetVar { index, value, .. } => { - eval_expr_uses_this_property(index, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::Break - | EvalStmt::Continue - | EvalStmt::ClassDecl(_) - | EvalStmt::EnumDecl(_) - | EvalStmt::FunctionDecl { .. } - | EvalStmt::Global { .. } - | EvalStmt::InterfaceDecl(_) - | EvalStmt::ReferenceAssign { .. } - | EvalStmt::TraitDecl(_) - | EvalStmt::UnsetVar { .. } => false, - EvalStmt::UnsetArrayElement { array, index } => { - eval_expr_uses_this_property(array, property_name) - || eval_expr_uses_this_property(index, property_name) - } - EvalStmt::DoWhile { body, condition } | EvalStmt::While { condition, body } => { - eval_expr_uses_this_property(condition, property_name) - || eval_stmt_list_uses_this_property(body, property_name) - } - EvalStmt::Echo(expr) - | EvalStmt::Expr(expr) - | EvalStmt::StaticVar { init: expr, .. } - | EvalStmt::Throw(expr) => eval_expr_uses_this_property(expr, property_name), - EvalStmt::For { - init, - condition, - update, - body, - } => { - eval_stmt_list_uses_this_property(init, property_name) - || condition - .as_ref() - .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) - || eval_stmt_list_uses_this_property(update, property_name) - || eval_stmt_list_uses_this_property(body, property_name) - } - EvalStmt::Foreach { array, body, .. } => { - eval_expr_uses_this_property(array, property_name) - || eval_stmt_list_uses_this_property(body, property_name) - } - EvalStmt::If { - condition, - then_branch, - else_branch, - } => { - eval_expr_uses_this_property(condition, property_name) - || eval_stmt_list_uses_this_property(then_branch, property_name) - || eval_stmt_list_uses_this_property(else_branch, property_name) - } - EvalStmt::Return(expr) => expr - .as_ref() - .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)), - EvalStmt::PropertyReferenceBind { - object, property, .. - } - | EvalStmt::UnsetProperty { object, property } => { - eval_is_this_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - } - EvalStmt::DynamicPropertyReferenceBind { - object, property, .. - } => { - eval_is_this_dynamic_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(property, property_name) - } - EvalStmt::UnsetDynamicProperty { object, property } => { - eval_is_this_dynamic_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(property, property_name) - } - EvalStmt::UnsetDynamicStaticProperty { class_name, .. } => { - eval_expr_uses_this_property(class_name, property_name) - } - EvalStmt::UnsetDynamicStaticPropertyName { - class_name, - property, - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(property, property_name) - } - EvalStmt::StaticPropertyIncDec { .. } - | EvalStmt::StaticPropertyReferenceBind { .. } - | EvalStmt::UnsetStaticProperty { .. } => false, - EvalStmt::DynamicPropertySet { - object, - property, - value, - } - | EvalStmt::DynamicPropertyArrayAppend { - object, - property, - value, - } - | EvalStmt::DynamicPropertyCompoundAssign { - object, - property, - value, - .. - } => { - eval_is_this_dynamic_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(property, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::DynamicPropertyArraySet { - object, - property, - index, - value, - .. - } => { - eval_is_this_dynamic_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(property, property_name) - || eval_expr_uses_this_property(index, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::DynamicPropertyIncDec { - object, property, .. - } => { - eval_is_this_dynamic_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(property, property_name) - } - EvalStmt::PropertySet { - object, - property, - value, - } - | EvalStmt::PropertyArrayAppend { - object, - property, - value, - } - | EvalStmt::PropertyCompoundAssign { - object, - property, - value, - .. - } => { - eval_is_this_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::PropertyArraySet { - object, - property, - index, - value, - .. - } => { - eval_is_this_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(index, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::PropertyIncDec { - object, property, .. - } => { - eval_is_this_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - } - EvalStmt::DynamicStaticPropertySet { - class_name, value, .. - } - | EvalStmt::DynamicStaticPropertyArrayAppend { - class_name, value, .. - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::DynamicStaticPropertyArraySet { - class_name, - index, - value, - .. - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(index, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::DynamicStaticPropertyIncDec { class_name, .. } => { - eval_expr_uses_this_property(class_name, property_name) - } - EvalStmt::DynamicStaticPropertyReferenceBind { class_name, .. } => { - eval_expr_uses_this_property(class_name, property_name) - } - EvalStmt::DynamicStaticPropertyNameSet { - class_name, - property, - value, - } - | EvalStmt::DynamicStaticPropertyNameArrayAppend { - class_name, - property, - value, - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(property, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::DynamicStaticPropertyNameArraySet { - class_name, - property, - index, - value, - .. - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(property, property_name) - || eval_expr_uses_this_property(index, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::DynamicStaticPropertyNameIncDec { - class_name, - property, - .. - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(property, property_name) - } - EvalStmt::DynamicStaticPropertyNameReferenceBind { - class_name, - property, - .. - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(property, property_name) - } - EvalStmt::StaticPropertySet { value, .. } - | EvalStmt::StaticPropertyArrayAppend { value, .. } - | EvalStmt::StoreVar { value, .. } => { - eval_expr_uses_this_property(value, property_name) - } - EvalStmt::StaticPropertyArraySet { index, value, .. } => { - eval_expr_uses_this_property(index, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalStmt::Switch { expr, cases } => { - eval_expr_uses_this_property(expr, property_name) - || cases.iter().any(|case| { - case.condition - .as_ref() - .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) - || eval_stmt_list_uses_this_property(&case.body, property_name) - }) - } - EvalStmt::Try { - body, - catches, - finally_body, - } => { - eval_stmt_list_uses_this_property(body, property_name) - || catches - .iter() - .any(|catch| eval_stmt_list_uses_this_property(&catch.body, property_name)) - || eval_stmt_list_uses_this_property(finally_body, property_name) - } - } -} - -/// Returns whether any statement in a list touches `$this->{$property_name}` directly. -fn eval_stmt_list_uses_this_property(stmts: &[EvalStmt], property_name: &str) -> bool { - stmts - .iter() - .any(|stmt| eval_stmt_uses_this_property(stmt, property_name)) -} - -/// Returns whether one expression touches `$this->{$property_name}` directly. -fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { - match expr { - EvalExpr::Array(elements) => elements.iter().any(|element| match element { - EvalArrayElement::Value(value) => eval_expr_uses_this_property(value, property_name), - EvalArrayElement::Reference(value) => { - eval_expr_uses_this_property(value, property_name) - } - EvalArrayElement::KeyValue { key, value } => { - eval_expr_uses_this_property(key, property_name) - || eval_expr_uses_this_property(value, property_name) - } - EvalArrayElement::KeyReference { key, value } => { - eval_expr_uses_this_property(key, property_name) - || eval_expr_uses_this_property(value, property_name) - } - }), - EvalExpr::ArrayGet { array, index } => { - eval_expr_uses_this_property(array, property_name) - || eval_expr_uses_this_property(index, property_name) - } - EvalExpr::Call { args, .. } - | EvalExpr::NamespacedCall { args, .. } - | EvalExpr::NewObject { args, .. } - | EvalExpr::StaticMethodCall { args, .. } => args - .iter() - .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), - EvalExpr::DynamicCall { callee, args } => { - eval_expr_uses_this_property(callee, property_name) - || args - .iter() - .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) - } - EvalExpr::DynamicNewObject { class_name, args } => { - eval_expr_uses_this_property(class_name, property_name) - || args - .iter() - .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) - } - EvalExpr::DynamicStaticMethodCall { - class_name, - method, - args, - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(method, property_name) - || args - .iter() - .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) - } - EvalExpr::DynamicStaticPropertyGet { class_name, .. } - | EvalExpr::DynamicClassConstantFetch { class_name, .. } - | EvalExpr::DynamicClassNameFetch { class_name } => { - eval_expr_uses_this_property(class_name, property_name) - } - EvalExpr::DynamicStaticPropertyNameGet { - class_name, - property, - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(property, property_name) - } - EvalExpr::DynamicClassConstantNameFetch { - class_name, - constant, - } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(constant, property_name) - } - EvalExpr::DynamicMethodCall { - object, - method, - args, - } - | EvalExpr::NullsafeDynamicMethodCall { - object, - method, - args, - } => { - eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(method, property_name) - || args - .iter() - .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) - } - EvalExpr::Const(_) - | EvalExpr::ConstFetch(_) - | EvalExpr::Closure { .. } - | EvalExpr::FunctionCallable { .. } - | EvalExpr::ClassConstantFetch { .. } - | EvalExpr::ClassNameFetch { .. } - | EvalExpr::LoadVar(_) - | EvalExpr::Magic(_) - | EvalExpr::NamespacedConstFetch { .. } - | EvalExpr::StaticPropertyGet { .. } => false, - EvalExpr::MethodCallable { object, method } => { - eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(method, property_name) - } - EvalExpr::StaticMethodCallable { method, .. } => { - eval_expr_uses_this_property(method, property_name) - } - EvalExpr::InvokableCallable { object } => { - eval_expr_uses_this_property(object, property_name) - } - EvalExpr::DynamicStaticMethodCallable { class_name, method } => { - eval_expr_uses_this_property(class_name, property_name) - || eval_expr_uses_this_property(method, property_name) - } - EvalExpr::Include { path, .. } - | EvalExpr::Cast { expr: path, .. } - | EvalExpr::Clone(path) - | EvalExpr::Print(path) - | EvalExpr::Unary { expr: path, .. } => eval_expr_uses_this_property(path, property_name), - EvalExpr::InstanceOf { value, target } => { - eval_expr_uses_this_property(value, property_name) - || matches!( - target, - EvalInstanceOfTarget::Expr(target) - if eval_expr_uses_this_property(target, property_name) - ) - } - EvalExpr::Match { - subject, - arms, - default, - } => { - eval_expr_uses_this_property(subject, property_name) - || arms.iter().any(|arm| { - arm.patterns - .iter() - .any(|pattern| eval_expr_uses_this_property(pattern, property_name)) - || eval_expr_uses_this_property(&arm.value, property_name) - }) - || default - .as_ref() - .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) - } - EvalExpr::MethodCall { object, args, .. } => { - eval_expr_uses_this_property(object, property_name) - || args - .iter() - .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) - } - EvalExpr::NullsafeMethodCall { object, args, .. } => { - eval_expr_uses_this_property(object, property_name) - || args - .iter() - .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) - } - EvalExpr::NewAnonymousClass { args, .. } => args - .iter() - .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), - EvalExpr::NullCoalesce { value, default } => { - eval_expr_uses_this_property(value, property_name) - || eval_expr_uses_this_property(default, property_name) - } - EvalExpr::PropertyGet { object, property } => { - eval_is_this_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - } - EvalExpr::NullsafePropertyGet { object, property } => { - eval_is_this_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - } - EvalExpr::DynamicPropertyGet { object, property } - | EvalExpr::NullsafeDynamicPropertyGet { object, property } => { - eval_is_this_dynamic_property(object, property, property_name) - || eval_expr_uses_this_property(object, property_name) - || eval_expr_uses_this_property(property, property_name) - } - EvalExpr::Ternary { - condition, - then_branch, - else_branch, - } => { - eval_expr_uses_this_property(condition, property_name) - || then_branch - .as_ref() - .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) - || eval_expr_uses_this_property(else_branch, property_name) - } - EvalExpr::Binary { left, right, .. } => { - eval_expr_uses_this_property(left, property_name) - || eval_expr_uses_this_property(right, property_name) - } - } -} - -/// Returns whether one object/property pair is exactly `$this->{$property_name}`. -fn eval_is_this_property(object: &EvalExpr, property: &str, property_name: &str) -> bool { - matches!(object, EvalExpr::LoadVar(name) if name == "this") && property == property_name -} - -/// Returns whether one dynamic object/property pair is exactly `$this->{"property"}`. -fn eval_is_this_dynamic_property( - object: &EvalExpr, - property: &EvalExpr, - property_name: &str, -) -> bool { - matches!( - (object, property), - ( - EvalExpr::LoadVar(object_name), - EvalExpr::Const(EvalConst::String(property)) - ) if object_name == "this" && property == property_name - ) -} - -/// Returns the synthetic get-hook method name for one property. -fn property_hook_get_method(property_name: &str) -> String { - format!("__propget_{property_name}") -} - -/// Returns the synthetic set-hook method name for one property. -fn property_hook_set_method(property_name: &str) -> String { - format!("__propset_{property_name}") -} - -/// Builds the implicit constructor assignment or alias for a promoted parameter. -fn promoted_property_assignment(name: &str, is_by_ref: bool) -> EvalStmt { - if is_by_ref { - EvalStmt::PropertyReferenceBind { - object: EvalExpr::LoadVar("this".to_string()), - property: name.to_string(), - source: name.to_string(), - } - } else { - EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: name.to_string(), - value: EvalExpr::LoadVar(name.to_string()), - } - } } diff --git a/crates/elephc-magician/src/parser/statements/assignments.rs b/crates/elephc-magician/src/parser/statements/assignments.rs new file mode 100644 index 0000000000..d42e0e0227 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/assignments.rs @@ -0,0 +1,600 @@ +//! Purpose: +//! Parses for clauses, variable stores, property/static assignments, and increment/decrement forms. +//! +//! Called from: +//! - Statement dispatch and for-loop clause parsing. +//! +//! Key details: +//! - Compound assignment and property targets lower directly into explicit EvalIR statement variants. + +use super::*; + +impl Parser { + /// Parses the optional first clause of a `for` loop. + pub(in crate::parser) fn parse_for_init_clause(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Semicolon) { + return Ok(Vec::new()); + } + self.parse_for_clause_stmt() + } + + /// Parses the optional update clause of a `for` loop. + pub(in crate::parser) fn parse_for_update_clause(&mut self) -> Result, EvalParseError> { + if self.consume(TokenKind::RParen) { + return Ok(Vec::new()); + } + let statements = self.parse_for_clause_stmt()?; + self.expect(TokenKind::RParen)?; + Ok(statements) + } + + /// Parses one statement-like `for` clause without consuming a delimiter. + pub(in crate::parser) fn parse_for_clause_stmt(&mut self) -> Result, EvalParseError> { + match self.current() { + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_static_property_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(true, false) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_dynamic_static_property_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(true, false) + } + TokenKind::PlusPlus | TokenKind::MinusMinus + if self.current_starts_prefixed_property_inc_dec() => + { + self.parse_prefixed_property_inc_dec_stmt(false) + } + TokenKind::PlusPlus | TokenKind::MinusMinus => { + self.parse_prefix_inc_dec_stmt(false) + } + TokenKind::Ident(_) | TokenKind::Backslash + if self.current_starts_static_property_postfix_inc_dec() => + { + self.parse_static_property_inc_dec_stmt(false, false) + } + TokenKind::DollarIdent(name) if matches!(self.peek(), TokenKind::LBracket) => { + self.parse_array_set_clause(name.clone()) + } + TokenKind::DollarIdent(_) + if self.current_starts_dynamic_static_property_postfix_inc_dec() => + { + self.parse_dynamic_static_property_inc_dec_stmt(false, false) + } + TokenKind::DollarIdent(_) if matches!(self.peek(), TokenKind::Arrow) => { + self.parse_property_stmt(false) + } + TokenKind::DollarIdent(name) + if matches!(self.peek(), TokenKind::PlusPlus | TokenKind::MinusMinus) => + { + self.parse_postfix_inc_dec_stmt(name.clone(), false) + } + TokenKind::DollarIdent(name) if assignment_op(self.peek()).is_some() => { + let name = name.clone(); + self.parse_var_store_stmt(name, false) + } + _ => { + let expr = self.parse_expr()?; + self.parse_property_like_stmt_tail(expr, false) + } + } + } + + /// Parses `$name[index] = expr` and `$name[] = expr` in a `for` clause. + pub(in crate::parser) fn parse_array_set_clause( + &mut self, + name: String, + ) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `$name = expr` and simple variable compound assignments. + pub(in crate::parser) fn parse_var_store_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + if op.is_none() && matches!(self.current(), TokenKind::Ampersand) { + self.advance(); + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::ReferenceAssign { + target: name, + source, + }]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = assignment_value(&name, op, value); + Ok(vec![EvalStmt::StoreVar { name, value }]) + } + + /// Parses `Class::$property = expr` and simple static-property compound assignments. + pub(in crate::parser) fn parse_static_property_set_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_static_class_name(class_name); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyArrayAppend { + class_name, + property, + value, + }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyArraySet { + class_name, + property, + index, + op, + value, + }]); + } + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::StaticPropertyReferenceBind { + class_name, + property, + source, + }]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::StaticPropertyGet { + class_name: class_name.clone(), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::StaticPropertySet { + class_name, + property, + value, + }]) + } + + /// Parses `$class::$property = expr` and compound assignments with a dynamic receiver. + pub(in crate::parser) fn parse_dynamic_static_property_set_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let TokenKind::DollarIdent(class_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let class_name = EvalExpr::LoadVar(class_name.clone()); + self.advance(); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, + property, + value, + }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyArraySet { + class_name, + property, + index, + op, + value, + }]); + } + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::DynamicStaticPropertyReferenceBind { + class_name, + property, + source, + }]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name.clone()), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + }]) + } + + /// Parses static property increment/decrement as read-modify-write. + pub(in crate::parser) fn parse_static_property_inc_dec_stmt( + &mut self, + prefixed: bool, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let prefix_increment = if prefixed { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + Some(increment) + } else { + None + }; + let class_name = self.parse_qualified_name()?; + let class_name = self.resolve_static_class_name(class_name); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + let increment = if let Some(increment) = prefix_increment { + increment + } else { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + increment + }; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::StaticPropertyIncDec { + class_name, + property, + increment, + }]) + } + + /// Parses dynamic static property increment/decrement as read-modify-write. + pub(in crate::parser) fn parse_dynamic_static_property_inc_dec_stmt( + &mut self, + prefixed: bool, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let prefix_increment = if prefixed { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + Some(increment) + } else { + None + }; + let TokenKind::DollarIdent(class_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let class_name = EvalExpr::LoadVar(class_name.clone()); + self.advance(); + self.expect(TokenKind::DoubleColon)?; + let TokenKind::DollarIdent(property) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let property = property.clone(); + self.advance(); + let increment = if let Some(increment) = prefix_increment { + increment + } else { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + increment + }; + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![EvalStmt::DynamicStaticPropertyIncDec { + class_name, + property, + increment, + }]) + } + + /// Parses prefix `++$name` / `--$name` and supported property-like prefix mutations. + pub(in crate::parser) fn parse_prefix_inc_dec_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if let TokenKind::DollarIdent(name) = self.current() { + if !matches!( + self.peek(), + TokenKind::DoubleColon + | TokenKind::Arrow + | TokenKind::QuestionArrow + | TokenKind::LBracket + ) { + let name = name.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![inc_dec_store(name, increment)]); + } + } + let target = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]) + } + + /// Parses postfix `$name++` and `$name--` as simple statement effects. + pub(in crate::parser) fn parse_postfix_inc_dec_stmt( + &mut self, + name: String, + require_semicolon: bool, + ) -> Result, EvalParseError> { + self.advance(); + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + Ok(vec![inc_dec_store(name, increment)]) + } + + /// Parses prefix property increment/decrement as read-modify-write. + pub(in crate::parser) fn parse_prefixed_property_inc_dec_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + let target = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]) + } + + /// Parses `$object->property` as either an expression statement or property write. + pub(in crate::parser) fn parse_property_stmt( + &mut self, + require_semicolon: bool, + ) -> Result, EvalParseError> { + let target = self.parse_expr()?; + self.parse_property_like_stmt_tail(target, require_semicolon) + } + + /// Parses assignment, array-write, or inc/dec tails after a parsed property-like target. + pub(super) fn parse_property_like_stmt_tail( + &mut self, + target: EvalExpr, + require_semicolon: bool, + ) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::PlusPlus | TokenKind::MinusMinus) { + let increment = matches!(self.current(), TokenKind::PlusPlus); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return property_inc_dec_stmt(target, increment).map(|stmt| vec![stmt]); + } + if self.consume(TokenKind::LBracket) { + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return property_array_append_stmt(target, value).map(|stmt| vec![stmt]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + let Some(op) = assignment_op(self.current()) else { + return Err(EvalParseError::UnexpectedToken); + }; + self.advance(); + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + return property_array_set_stmt(target, index, op, value).map(|stmt| vec![stmt]); + } + let Some(op) = assignment_op(self.current()) else { + if require_semicolon { + self.expect_semicolon()?; + } + return Ok(vec![EvalStmt::Expr(target)]); + }; + self.advance(); + if op.is_none() && self.consume(TokenKind::Ampersand) { + let TokenKind::DollarIdent(source) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let source = source.clone(); + self.advance(); + if require_semicolon { + self.expect_semicolon()?; + } + return property_reference_bind_stmt(target, source).map(|stmt| vec![stmt]); + } + let value = self.parse_expr()?; + if require_semicolon { + self.expect_semicolon()?; + } + match (target, op) { + (EvalExpr::ArrayGet { array, index }, op) => { + property_array_set_stmt(*array, *index, op, value).map(|stmt| vec![stmt]) + } + (EvalExpr::PropertyGet { object, property }, None) => Ok(vec![EvalStmt::PropertySet { + object: *object, + property, + value, + }]), + (EvalExpr::PropertyGet { object, property }, Some(op)) => { + Ok(vec![EvalStmt::PropertyCompoundAssign { + object: *object, + property, + op, + value, + }]) + } + (EvalExpr::DynamicPropertyGet { object, property }, None) => { + Ok(vec![EvalStmt::DynamicPropertySet { + object: *object, + property: *property, + value, + }]) + } + (EvalExpr::DynamicPropertyGet { object, property }, Some(op)) => { + Ok(vec![EvalStmt::DynamicPropertyCompoundAssign { + object: *object, + property: *property, + op, + value, + }]) + } + ( + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + }, + op, + ) => { + let class_name = *class_name; + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(class_name.clone()), + property: property.clone(), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertySet { + class_name, + property, + value, + }]) + } + ( + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + }, + op, + ) => { + let class_name = *class_name; + let property = *property; + let value = match op { + Some(op) => EvalExpr::Binary { + op, + left: Box::new(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(class_name.clone()), + property: Box::new(property.clone()), + }), + right: Box::new(value), + }, + None => value, + }; + Ok(vec![EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + }]) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } +} diff --git a/crates/elephc-magician/src/parser/statements/class_declarations.rs b/crates/elephc-magician/src/parser/statements/class_declarations.rs new file mode 100644 index 0000000000..e6361e2f82 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/class_declarations.rs @@ -0,0 +1,627 @@ +//! Purpose: +//! Parses class declarations, anonymous classes, modifiers, traits, constants, and member headers. +//! +//! Called from: +//! - Statement and expression parsing for class-like syntax. +//! +//! Key details: +//! - Class body collection preserves promoted-property and trait-adaptation side products. + +use super::*; + +impl Parser { + /// Parses `[abstract|final|readonly] class Name [extends Parent] [implements Iface, ...] { ... }`. + pub(in crate::parser) fn parse_class_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_class_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses a class declaration and attaches already parsed class attributes. + pub(in crate::parser) fn parse_class_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let (is_abstract, is_final, is_readonly_class) = self.parse_class_decl_modifiers()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return Err(EvalParseError::UnsupportedConstruct); + } + let source_start_line = self.current_line(); + self.advance(); + let name = self.parse_class_like_decl_name()?; + let parent = self.parse_class_parent_clause()?; + let interfaces = self.parse_class_interface_clause()?; + let body = self.parse_class_body_members(is_readonly_class)?; + let source_location = EvalSourceLocation::new(source_start_line, body.source_end_line); + self.consume_semicolon(); + Ok(vec![EvalStmt::ClassDecl( + EvalClass::with_class_modifiers_traits_adaptations_and_constants( + name, + is_abstract, + is_final, + is_readonly_class, + parent, + interfaces, + body.traits, + body.trait_adaptations, + body.constants, + body.properties, + body.methods, + ) + .with_source_location(source_location) + .with_attributes(attributes), + )]) + } + + /// Parses `class [(args)] [extends Parent] [implements Iface, ...] { ... }` after `new`. + pub(in crate::parser) fn parse_anonymous_class_expr( + &mut self, + is_readonly_class: bool, + ) -> Result { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "class")) { + return Err(EvalParseError::UnsupportedConstruct); + } + let source_start_line = self.current_line(); + self.advance(); + let args = if matches!(self.current(), TokenKind::LParen) { + self.parse_call_args()? + } else { + Vec::new() + }; + let parent = self.parse_class_parent_clause()?; + let interfaces = self.parse_class_interface_clause()?; + let body = self.parse_class_body_members(is_readonly_class)?; + let source_location = EvalSourceLocation::new(source_start_line, body.source_end_line); + let name = next_anonymous_class_name(); + let class = EvalClass::with_class_modifiers_traits_adaptations_and_constants( + name, + false, + false, + is_readonly_class, + parent, + interfaces, + body.traits, + body.trait_adaptations, + body.constants, + body.properties, + body.methods, + ) + .with_source_location(source_location) + .with_anonymous(); + Ok(EvalExpr::NewAnonymousClass { class, args }) + } + + /// Parses and namespace-qualifies a declared class, interface, trait, or enum name. + pub(super) fn parse_class_like_decl_name(&mut self) -> Result { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + if is_reserved_class_like_name(name) { + return Err(EvalParseError::UnsupportedConstruct); + } + let qualified_name = self.qualify_name_in_current_namespace(name); + self.advance(); + Ok(qualified_name) + } + + /// Parses members inside a class body after relation clauses. + pub(super) fn parse_class_body_members( + &mut self, + is_readonly_class: bool, + ) -> Result { + self.expect(TokenKind::LBrace)?; + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); + loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + return Ok(ParsedClassBody { + source_end_line, + constants, + properties, + methods, + traits, + trait_adaptations, + }); + } + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_class_member( + is_readonly_class, + &mut constants, + &mut properties, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; + } + } + + /// Parses class-level `abstract`, `final`, and `readonly` modifiers before `class`. + pub(in crate::parser) fn parse_class_decl_modifiers( + &mut self, + ) -> Result<(bool, bool, bool), EvalParseError> { + let mut is_abstract = false; + let mut is_final = false; + let mut is_readonly_class = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "abstract") => { + if is_abstract { + return Err(EvalParseError::UnsupportedConstruct); + } + is_abstract = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly_class { + return Err(EvalParseError::UnsupportedConstruct); + } + is_readonly_class = true; + self.advance(); + } + _ => return Ok((is_abstract, is_final, is_readonly_class)), + } + } + } + + /// Parses an optional `extends Parent` class declaration clause. + pub(in crate::parser) fn parse_class_parent_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { + return Ok(None); + } + self.advance(); + let parent = self.parse_class_reference_name(false)?; + Ok(Some(self.resolve_class_name(parent))) + } + + /// Parses an optional `implements Iface, ...` class declaration clause. + pub(in crate::parser) fn parse_class_interface_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "implements")) { + return Ok(Vec::new()); + } + self.advance(); + let mut interfaces = Vec::new(); + loop { + let interface = self.parse_class_reference_name(false)?; + interfaces.push(self.resolve_class_name(interface)); + if !self.consume(TokenKind::Comma) { + break; + } + } + Ok(interfaces) + } + + /// Parses one public property or method from an eval class body. + pub(in crate::parser) fn parse_class_member( + &mut self, + is_readonly_class: bool, + constants: &mut Vec, + properties: &mut Vec, + methods: &mut Vec, + traits: &mut Vec, + trait_adaptations: &mut Vec, + ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; + + if is_abstract && is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + + if visibility.is_none() + && !is_static + && !is_abstract + && !is_final + && !is_readonly + && set_visibility.is_none() + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + self.parse_class_trait_use(traits, trait_adaptations)?; + return Ok(()); + } + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { + self.parse_legacy_var_property_member( + attributes, + visibility.is_some() + || is_static + || is_abstract + || is_final + || is_readonly + || set_visibility.is_some(), + is_readonly_class, + properties, + methods, + )?; + return Ok(()); + } + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || is_abstract || is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + constants.extend( + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + &attributes, + )?, + ); + return Ok(()); + } + + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + is_abstract, + is_final, + )?; + properties.extend(promoted_properties); + methods.push(method.with_attributes(attributes)); + return Ok(()); + } + + let visibility = visibility.unwrap_or(EvalVisibility::Public); + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( + visibility, + set_visibility, + is_static, + is_final, + is_readonly, + is_readonly_class, + is_abstract, + )?; + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); + methods.append(&mut hook_methods); + Ok(()) + } + + /// Parses PHP's legacy `var` public-property marker after the keyword is recognized. + pub(super) fn parse_legacy_var_property_member( + &mut self, + attributes: Vec, + has_other_modifiers: bool, + is_readonly_class: bool, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + if has_other_modifiers { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( + EvalVisibility::Public, + None, + false, + false, + false, + is_readonly_class, + false, + )?; + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); + methods.append(&mut hook_methods); + Ok(()) + } + + /// Parses optional attributes that decorate one class-like member. + pub(in crate::parser) fn parse_optional_member_attributes( + &mut self, + ) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::AttributeStart) { + self.parse_attribute_groups() + } else { + Ok(Vec::new()) + } + } + + /// Parses one eval class constant declaration, including comma-separated constants. + pub(in crate::parser) fn parse_class_const_decl( + &mut self, + visibility: EvalVisibility, + is_final: bool, + attributes: &[EvalAttribute], + ) -> Result, EvalParseError> { + self.advance(); + let mut constants = Vec::new(); + loop { + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + if ident_eq(name, "class") { + return Err(EvalParseError::UnsupportedConstruct); + } + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + constants.push( + EvalClassConstant::with_visibility_and_final(name, visibility, is_final, value) + .with_attributes(attributes.to_vec()), + ); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(constants) + } + + /// Parses `use TraitName, OtherTrait;` or an adaptation block inside an eval class body. + pub(in crate::parser) fn parse_class_trait_use( + &mut self, + traits: &mut Vec, + trait_adaptations: &mut Vec, + ) -> Result<(), EvalParseError> { + self.advance(); + loop { + let trait_name = self.parse_class_reference_name(false)?; + traits.push(self.resolve_class_name(trait_name)); + if !self.consume(TokenKind::Comma) { + break; + } + } + if self.consume(TokenKind::LBrace) { + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + trait_adaptations.push(self.parse_trait_adaptation()?); + self.expect_semicolon()?; + } + self.consume_semicolon(); + Ok(()) + } else { + self.expect_semicolon() + } + } + + /// Parses one `as` or `insteadof` trait adaptation clause. + pub(in crate::parser) fn parse_trait_adaptation(&mut self) -> Result { + let (trait_name, method) = self.parse_trait_adaptation_target()?; + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "as") => { + self.advance(); + let visibility = self.parse_optional_trait_adaptation_visibility()?; + let alias = if let TokenKind::Ident(alias) = self.current() { + let alias = alias.clone(); + self.advance(); + Some(alias) + } else { + None + }; + if visibility.is_none() && alias.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalTraitAdaptation::Alias { + trait_name, + method, + alias, + visibility, + }) + } + TokenKind::Ident(name) if ident_eq(name, "insteadof") => { + self.advance(); + let mut instead_of = Vec::new(); + loop { + let trait_name = self.parse_class_reference_name(false)?; + instead_of.push(self.resolve_class_name(trait_name)); + if !self.consume(TokenKind::Comma) { + break; + } + } + if instead_of.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalTraitAdaptation::InsteadOf { + trait_name, + method, + instead_of, + }) + } + _ => Err(EvalParseError::UnsupportedConstruct), + } + } + + /// Parses the target before `as` or `insteadof`. + pub(in crate::parser) fn parse_trait_adaptation_target( + &mut self, + ) -> Result<(Option, String), EvalParseError> { + let first = self.parse_qualified_name()?; + if self.consume(TokenKind::DoubleColon) { + if self.class_reference_name_is_reserved(&first, false) { + return Err(EvalParseError::UnsupportedConstruct); + } + let TokenKind::Ident(method) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let method = method.clone(); + self.advance(); + Ok((Some(self.resolve_class_name(first)), method)) + } else { + let method = first + .name + .rsplit('\\') + .next() + .filter(|segment| !segment.is_empty()) + .ok_or(EvalParseError::UnexpectedToken)? + .to_string(); + Ok((None, method)) + } + } + + /// Parses an optional visibility modifier inside a trait `as` adaptation. + pub(in crate::parser) fn parse_optional_trait_adaptation_visibility( + &mut self, + ) -> Result, EvalParseError> { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + self.advance(); + Ok(Some(EvalVisibility::Public)) + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + self.advance(); + Ok(Some(EvalVisibility::Protected)) + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + self.advance(); + Ok(Some(EvalVisibility::Private)) + } + _ => Ok(None), + } + } + + /// Parses method modifiers supported by eval class declarations. + pub(in crate::parser) fn parse_class_member_modifiers( + &mut self, + ) -> Result< + ( + Option, + Option, + bool, + bool, + bool, + bool, + ), + EvalParseError, + > { + let mut visibility = None; + let mut set_visibility = None; + let mut is_static = false; + let mut is_abstract = false; + let mut is_final = false; + let mut is_readonly = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Public); + } else if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Public); + } + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Protected); + } else if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Protected); + } + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Private); + } else if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } else { + visibility = Some(EvalVisibility::Private); + } + } + TokenKind::Ident(name) if ident_eq(name, "static") => { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + is_static = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "abstract") => { + if is_abstract { + return Err(EvalParseError::UnsupportedConstruct); + } + is_abstract = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + is_readonly = true; + self.advance(); + } + TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { + return Err(EvalParseError::UnsupportedConstruct); + } + _ => { + return Ok(( + visibility, + set_visibility, + is_static, + is_abstract, + is_final, + is_readonly, + )) + } + } + } + } + + /// Consumes a PHP asymmetric visibility `(set)` marker after a visibility keyword. + pub(super) fn consume_set_marker(&mut self) -> Result { + if !self.consume(TokenKind::LParen) { + return Ok(false); + } + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "set") => self.advance(), + _ => return Err(EvalParseError::UnsupportedConstruct), + } + self.expect(TokenKind::RParen)?; + Ok(true) + } + + /// Returns a comparable visibility rank where larger means less restrictive. + pub(super) fn eval_visibility_rank(visibility: EvalVisibility) -> u8 { + match visibility { + EvalVisibility::Private => 1, + EvalVisibility::Protected => 2, + EvalVisibility::Public => 3, + } + } +} diff --git a/crates/elephc-magician/src/parser/statements/class_members.rs b/crates/elephc-magician/src/parser/statements/class_members.rs new file mode 100644 index 0000000000..e0055b540c --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/class_members.rs @@ -0,0 +1,344 @@ +//! Purpose: +//! Parses class methods, properties, property hooks, and promoted property metadata. +//! +//! Called from: +//! - Class, trait, enum, and interface member parsing. +//! +//! Key details: +//! - Hook bodies, asymmetric visibility, types, defaults, and promotion remain aligned. + +use super::*; + +impl Parser { + /// Parses one class/trait/enum method and returns constructor-promoted properties. + pub(in crate::parser) fn parse_class_method_decl( + &mut self, + visibility: EvalVisibility, + is_static: bool, + is_abstract: bool, + is_final: bool, + ) -> Result<(EvalClassMethod, Vec), EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params(&name, true)?; + if !promoted_properties.is_empty() && (is_abstract || is_static) { + return Err(EvalParseError::UnsupportedConstruct); + } + let return_type = self.parse_optional_return_type(EvalTypePosition::MethodReturn)?; + let (body, source_end_line) = if is_abstract { + let source_end_line = self.current_line(); + self.expect_semicolon()?; + (Vec::new(), source_end_line) + } else { + let (body, source_end_line) = self.parse_block_with_end_line()?; + let body = if promoted_assignments.is_empty() { + body + } else { + promoted_assignments.into_iter().chain(body).collect() + }; + (body, source_end_line) + }; + Ok(( + EvalClassMethod::with_visibility_and_modifiers( + name, + visibility, + is_static, + is_abstract, + is_final, + params, + body, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_parameter_types(parameter_types) + .with_parameter_attributes(parameter_attributes) + .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type), + promoted_properties, + )) + } + + /// Parses one property declaration, including comma-separated simple properties. + pub(in crate::parser) fn parse_class_property_decl( + &mut self, + visibility: EvalVisibility, + set_visibility: Option, + is_static: bool, + is_final: bool, + is_readonly: bool, + is_readonly_class: bool, + is_abstract: bool, + ) -> Result<(Vec, Vec), EvalParseError> { + if is_static && is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + if set_visibility.is_some() && is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + if set_visibility.is_some_and(|set_visibility| { + Self::eval_visibility_rank(set_visibility) > Self::eval_visibility_rank(visibility) + }) { + return Err(EvalParseError::UnsupportedConstruct); + } + let effective_readonly = is_readonly || (is_readonly_class && !is_static); + let property_type = self.parse_optional_property_type()?; + if set_visibility.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + let mut properties = Vec::new(); + let mut hook_methods = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let default = if self.consume(TokenKind::Equal) { + if is_abstract || effective_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + Some(self.parse_expr()?) + } else { + None + }; + if is_abstract { + if is_static || effective_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + let (requires_get_hook, requires_set_hook) = + self.parse_property_hook_contracts()?; + let property = EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + None, + ) + .with_type(property_type) + .with_set_visibility(set_visibility) + .with_abstract_hook_contract(requires_get_hook, requires_set_hook); + return Ok((vec![property], Vec::new())); + } + let default_is_some = default.is_some(); + if self.consume(TokenKind::Comma) { + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_visibility(set_visibility), + ); + continue; + } + if !properties.is_empty() { + self.expect_semicolon()?; + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_visibility(set_visibility), + ); + break; + } + let (has_get_hook, has_set_hook, set_hook_type, parsed_hook_methods) = self + .parse_property_hook_tail( + &name, + property_type.as_ref(), + is_static, + effective_readonly, + default_is_some, + )?; + if set_hook_type.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + let is_virtual = (has_get_hook || has_set_hook) + && !property_hook_methods_use_backing_slot(&parsed_hook_methods, &name); + properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name, + visibility, + is_static, + is_final, + effective_readonly, + default, + ) + .with_type(property_type.clone()) + .with_set_hook_type(set_hook_type) + .with_set_visibility(set_visibility) + .with_hooks(has_get_hook, has_set_hook) + .with_virtual(is_virtual), + ); + hook_methods.extend(parsed_hook_methods); + break; + } + Ok((properties, hook_methods)) + } + + /// Parses `;` or a concrete eval property hook block after one property declaration. + pub(in crate::parser) fn parse_property_hook_tail( + &mut self, + property_name: &str, + property_type: Option<&EvalParameterType>, + is_static: bool, + is_readonly: bool, + has_default: bool, + ) -> Result<(bool, bool, Option, Vec), EvalParseError> { + if self.consume(TokenKind::Semicolon) { + return Ok((false, false, None, Vec::new())); + } + if !matches!(self.current(), TokenKind::LBrace) { + return Err(EvalParseError::UnexpectedToken); + } + if is_static || is_readonly || has_default { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + let mut has_get_hook = false; + let mut has_set_hook = false; + let mut set_hook_type = None; + let mut methods = Vec::new(); + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + let (is_get, hook_set_type, method) = + self.parse_property_hook_decl(property_name, property_type)?; + if is_get { + if has_get_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + has_get_hook = true; + } else { + if has_set_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + has_set_hook = true; + set_hook_type = hook_set_type; + } + methods.push(method); + } + if !has_get_hook && !has_set_hook { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok((has_get_hook, has_set_hook, set_hook_type, methods)) + } + + /// Parses one concrete `get` or `set` property hook declaration. + pub(in crate::parser) fn parse_property_hook_decl( + &mut self, + property_name: &str, + property_type: Option<&EvalParameterType>, + ) -> Result<(bool, Option, EvalClassMethod), EvalParseError> { + let returns_by_ref = self.consume(TokenKind::Ampersand); + let TokenKind::Ident(hook_name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let is_get = ident_eq(hook_name, "get"); + let is_set = ident_eq(hook_name, "set"); + if !is_get && !is_set { + return Err(EvalParseError::UnsupportedConstruct); + } + if returns_by_ref && !is_get { + return Err(EvalParseError::UnsupportedConstruct); + } + let source_start_line = self.current_line(); + self.advance(); + let (params, set_hook_type) = if is_set { + let (param, set_hook_type, has_explicit_param) = + self.parse_property_set_hook_param()?; + if has_explicit_param && set_hook_type.is_none() && property_type.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + (vec![param], set_hook_type) + } else { + (Vec::new(), None) + }; + let (body, source_end_line) = match self.current() { + TokenKind::Semicolon => return Err(EvalParseError::UnsupportedConstruct), + TokenKind::FatArrow => { + self.advance(); + let expr = self.parse_expr()?; + let source_end_line = self.current_line(); + self.expect_semicolon()?; + let body = if is_get { + vec![EvalStmt::Return(Some(expr))] + } else { + vec![EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: property_name.to_string(), + value: expr, + }] + }; + (body, source_end_line) + } + TokenKind::LBrace => self.parse_block_with_end_line()?, + _ => return Err(EvalParseError::UnexpectedToken), + }; + let method_name = if is_get { + property_hook_get_method(property_name) + } else { + property_hook_set_method(property_name) + }; + let mut method = EvalClassMethod::with_visibility_and_modifiers( + method_name, + EvalVisibility::Public, + false, + false, + false, + params, + body, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)); + if is_set { + method = method.with_parameter_types(vec![ + set_hook_type.clone().or_else(|| property_type.cloned()), + ]); + } + Ok((is_get, set_hook_type, method)) + } + + /// Parses an optional set-hook parameter list and returns the value variable metadata. + pub(in crate::parser) fn parse_property_set_hook_param( + &mut self, + ) -> Result<(String, Option, bool), EvalParseError> { + if !self.consume(TokenKind::LParen) { + return Ok(("value".to_string(), None, false)); + } + let set_hook_type = self.parse_optional_property_type()?; + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::RParen)?; + Ok((name, set_hook_type, true)) + } +} diff --git a/crates/elephc-magician/src/parser/statements/control_flow.rs b/crates/elephc-magician/src/parser/statements/control_flow.rs new file mode 100644 index 0000000000..d04cf28312 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/control_flow.rs @@ -0,0 +1,242 @@ +//! Purpose: +//! Parses if, switch, unset, while, and nested statement blocks. +//! +//! Called from: +//! - Statement dispatch and every construct that owns a nested body. +//! +//! Key details: +//! - Alternative PHP syntax and source-end lines are preserved in block parsing. + +use super::*; + +impl Parser { + /// Parses a complete `if` statement after consuming the `if` keyword. + pub(in crate::parser) fn parse_if_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } + + /// Parses the condition, then block, and optional else branch for an `if` chain. + pub(in crate::parser) fn parse_if_after_keyword(&mut self) -> Result { + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let then_branch = self.parse_statement_body()?; + let else_branch = self.parse_optional_else_branch()?; + Ok(EvalStmt::If { + condition, + then_branch, + else_branch, + }) + } + + /// Parses `elseif`, `else if`, or `else` branches after an `if` body. + pub(in crate::parser) fn parse_optional_else_branch(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "elseif")) { + self.advance(); + return Ok(vec![self.parse_if_after_keyword()?]); + } + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "else")) { + return Ok(Vec::new()); + } + self.advance(); + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "if")) { + self.advance(); + Ok(vec![self.parse_if_after_keyword()?]) + } else { + self.parse_statement_body() + } + } + + /// Parses `switch (expr) { case expr: ... default: ... }`. + pub(in crate::parser) fn parse_switch_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let expr = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect(TokenKind::LBrace)?; + let mut cases = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + cases.push(self.parse_switch_case()?); + } + self.expect(TokenKind::RBrace)?; + Ok(vec![EvalStmt::Switch { expr, cases }]) + } + + /// Parses one `case` or `default` arm inside a switch body. + pub(in crate::parser) fn parse_switch_case(&mut self) -> Result { + let condition = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) + { + self.advance(); + let condition = self.parse_expr()?; + Some(condition) + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "default")) { + self.advance(); + None + } else { + return Err(EvalParseError::UnexpectedToken); + }; + self.expect(TokenKind::Colon)?; + let body = self.parse_switch_case_body()?; + Ok(EvalSwitchCase { condition, body }) + } + + /// Parses case body statements until the next case boundary or switch close. + pub(in crate::parser) fn parse_switch_case_body(&mut self) -> Result, EvalParseError> { + let mut body = Vec::new(); + while !is_switch_case_boundary(self.current()) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + body.extend(self.parse_stmt()?); + } + Ok(body) + } + + /// Parses `unset($name[, ...]);` with variable, array-access, and property operands. + pub(in crate::parser) fn parse_unset_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let mut statements = Vec::new(); + loop { + let target = self.parse_expr()?; + let stmt = match target { + EvalExpr::ArrayGet { array, index } => EvalStmt::UnsetArrayElement { + array: *array, + index: *index, + }, + EvalExpr::LoadVar(name) => EvalStmt::UnsetVar { name }, + EvalExpr::PropertyGet { object, property } => EvalStmt::UnsetProperty { + object: *object, + property, + }, + EvalExpr::DynamicPropertyGet { object, property } => { + EvalStmt::UnsetDynamicProperty { + object: *object, + property: *property, + } + } + EvalExpr::StaticPropertyGet { + class_name, + property, + } => EvalStmt::UnsetStaticProperty { + class_name, + property, + }, + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => EvalStmt::UnsetDynamicStaticProperty { + class_name: *class_name, + property, + }, + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => EvalStmt::UnsetDynamicStaticPropertyName { + class_name: *class_name, + property: *property, + }, + _ => return Err(EvalParseError::ExpectedVariable), + }; + statements.push(stmt); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(statements) + } + + /// Parses `while (expr) { ... }`. + pub(in crate::parser) fn parse_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::While { condition, body }]) + } + + /// Parses either a brace-delimited block or one braceless statement body. + pub(in crate::parser) fn parse_statement_body(&mut self) -> Result, EvalParseError> { + if matches!(self.current(), TokenKind::LBrace) { + self.parse_block() + } else { + self.parse_nested_stmt() + } + } + + /// Parses a brace-delimited statement block. + pub(in crate::parser) fn parse_block(&mut self) -> Result, EvalParseError> { + self.expect(TokenKind::LBrace)?; + self.parse_nested_block_contents() + } + + /// Parses a brace-delimited statement block and returns the closing-brace line. + pub(in crate::parser) fn parse_block_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + self.expect(TokenKind::LBrace)?; + self.parse_nested_block_contents_with_end_line() + } + + /// Parses one nested statement where import declarations are not legal. + pub(in crate::parser) fn parse_nested_stmt(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_stmt(); + self.allow_use_imports = previous; + result + } + + /// Parses a nested block while preserving active imports for name resolution. + pub(in crate::parser) fn parse_nested_block_contents(&mut self) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_block_contents(); + self.allow_use_imports = previous; + result + } + + /// Parses a nested block and returns the closing-brace line. + pub(in crate::parser) fn parse_nested_block_contents_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + let previous = std::mem::replace(&mut self.allow_use_imports, false); + let result = self.parse_block_contents_with_end_line(); + self.allow_use_imports = previous; + result + } + + /// Parses statements until the closing brace for the current block. + pub(in crate::parser) fn parse_block_contents(&mut self) -> Result, EvalParseError> { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + statements.extend(self.parse_stmt()?); + } + self.expect(TokenKind::RBrace)?; + Ok(statements) + } + + /// Parses statements until the closing brace and returns that brace's line. + pub(in crate::parser) fn parse_block_contents_with_end_line( + &mut self, + ) -> Result<(Vec, i64), EvalParseError> { + let mut statements = Vec::new(); + while !matches!(self.current(), TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + statements.extend(self.parse_stmt()?); + } + let source_end_line = self.current_line(); + self.expect(TokenKind::RBrace)?; + Ok((statements, source_end_line)) + } +} diff --git a/crates/elephc-magician/src/parser/statements/default_validation.rs b/crates/elephc-magician/src/parser/statements/default_validation.rs new file mode 100644 index 0000000000..ab9b7d7101 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/default_validation.rs @@ -0,0 +1,109 @@ +//! Purpose: +//! Validates whether parameter and property defaults can be materialized safely. +//! +//! Called from: +//! - Parameter, method, and property declaration parsing. +//! +//! Key details: +//! - Constant expressions, arrays, calls, and class receivers are accepted conservatively. + +use super::*; + +/// Returns whether an eval method parameter default can be materialized safely. +pub(super) fn method_parameter_default_is_supported(default: &EvalExpr) -> bool { + eval_constant_expression_default_is_supported(default) +} + +/// Returns whether an EvalIR expression is safe to retain as a method default. +pub(super) fn eval_constant_expression_default_is_supported(expr: &EvalExpr) -> bool { + match expr { + EvalExpr::Array(elements) => elements.iter().all(eval_array_element_default_is_supported), + EvalExpr::Const(_) => true, + EvalExpr::Magic(_) => true, + EvalExpr::ConstFetch(_) | EvalExpr::NamespacedConstFetch { .. } => true, + EvalExpr::ClassConstantFetch { class_name, .. } + | EvalExpr::ClassNameFetch { class_name } => { + eval_default_class_receiver_is_supported(class_name) + } + EvalExpr::NewObject { class_name, args } => { + eval_default_class_receiver_is_supported(class_name) + && args.iter().all(eval_call_arg_default_is_supported) + } + EvalExpr::NewAnonymousClass { .. } => false, + EvalExpr::NullCoalesce { value, default } => { + eval_constant_expression_default_is_supported(value) + && eval_constant_expression_default_is_supported(default) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_constant_expression_default_is_supported(condition) + && then_branch + .as_deref() + .is_none_or(eval_constant_expression_default_is_supported) + && eval_constant_expression_default_is_supported(else_branch) + } + EvalExpr::Cast { expr, .. } => eval_constant_expression_default_is_supported(expr), + EvalExpr::Unary { expr, .. } => eval_constant_expression_default_is_supported(expr), + EvalExpr::Binary { left, right, .. } => { + eval_constant_expression_default_is_supported(left) + && eval_constant_expression_default_is_supported(right) + } + _ => false, + } +} + +/// Returns whether one object-construction argument is safe inside a method default. +pub(super) fn eval_call_arg_default_is_supported(arg: &EvalCallArg) -> bool { + !arg.is_spread() && eval_constant_expression_default_is_supported(arg.value()) +} + +/// Returns whether one array default element contains only supported constant expressions. +pub(super) fn eval_array_element_default_is_supported(element: &EvalArrayElement) -> bool { + match element { + EvalArrayElement::Value(value) => eval_constant_expression_default_is_supported(value), + EvalArrayElement::Reference(_) => false, + EvalArrayElement::KeyValue { key, value } => { + eval_constant_expression_default_is_supported(key) + && eval_constant_expression_default_is_supported(value) + } + EvalArrayElement::KeyReference { .. } => false, + } +} + +/// Returns whether a type list contains return-only standalone atoms. +pub(super) fn type_variants_contain_standalone_return_only_atoms( + variants: &[EvalParameterTypeVariant], +) -> bool { + variants.iter().any(|variant| { + matches!( + variant, + EvalParameterTypeVariant::Never | EvalParameterTypeVariant::Void + ) + }) +} + +/// Returns whether the type position accepts standalone return-only atoms. +pub(super) fn type_position_allows_return_only_atoms(position: EvalTypePosition) -> bool { + matches!( + position, + EvalTypePosition::FunctionReturn | EvalTypePosition::MethodReturn + ) +} + +/// Returns whether `self` and `parent` are legal in this type position. +pub(super) fn type_position_allows_class_scope_atoms(position: EvalTypePosition) -> bool { + !matches!( + position, + EvalTypePosition::FunctionParameter | EvalTypePosition::FunctionReturn + ) +} + +/// Returns whether a class-like receiver is legal in a compile-time method default. +pub(super) fn eval_default_class_receiver_is_supported(class_name: &str) -> bool { + !class_name + .trim_start_matches('\\') + .eq_ignore_ascii_case("static") +} diff --git a/crates/elephc-magician/src/parser/statements/enums.rs b/crates/elephc-magician/src/parser/statements/enums.rs new file mode 100644 index 0000000000..7b520ccedf --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/enums.rs @@ -0,0 +1,164 @@ +//! Purpose: +//! Parses enum declarations, backing types, cases, traits, and methods. +//! +//! Called from: +//! - Statement dispatch for attributed and plain enum declarations. +//! +//! Key details: +//! - Backed and unit cases retain source metadata and literal backing expressions. + +use super::*; + +impl Parser { + /// Parses `enum Name [: int|string] [implements Iface, ...] { ... }`. + pub(in crate::parser) fn parse_enum_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_enum_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses an enum declaration and attaches already parsed class-like attributes. + pub(in crate::parser) fn parse_enum_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let name = self.parse_class_like_decl_name()?; + let backing_type = self.parse_enum_backing_type()?; + let interfaces = self.parse_class_interface_clause()?; + self.expect(TokenKind::LBrace)?; + let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); + let mut cases = Vec::new(); + let mut constants = Vec::new(); + let mut methods = Vec::new(); + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_enum_member( + &mut cases, + &mut constants, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; + }; + self.consume_semicolon(); + Ok(vec![EvalStmt::EnumDecl( + EvalEnum::with_members_traits_adaptations( + name, + backing_type, + interfaces, + cases, + constants, + methods, + traits, + trait_adaptations, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_attributes(attributes), + )]) + } + + /// Parses an optional backed-enum scalar type after the enum name. + pub(in crate::parser) fn parse_enum_backing_type( + &mut self, + ) -> Result, EvalParseError> { + if !self.consume(TokenKind::Colon) { + return Ok(None); + } + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let backing_type = if ident_eq(name, "int") { + EvalEnumBackingType::Int + } else if ident_eq(name, "string") { + EvalEnumBackingType::String + } else { + return Err(EvalParseError::UnsupportedConstruct); + }; + self.advance(); + Ok(Some(backing_type)) + } + + /// Parses one enum case, constant, or method declaration. + pub(in crate::parser) fn parse_enum_member( + &mut self, + cases: &mut Vec, + constants: &mut Vec, + methods: &mut Vec, + traits: &mut Vec, + trait_adaptations: &mut Vec, + ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "case")) { + cases.push(self.parse_enum_case_decl()?.with_attributes(attributes)); + return Ok(()); + } + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; + if is_abstract || is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + if visibility.is_none() + && !is_static + && !is_final + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + self.parse_class_trait_use(traits, trait_adaptations)?; + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + constants.extend( + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + &attributes, + )?, + ); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + false, + is_final, + )?; + if !promoted_properties.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + methods.push(method.with_attributes(attributes)); + return Ok(()); + } + Err(EvalParseError::UnsupportedConstruct) + } + + /// Parses `case Name;` or `case Name = expr;` inside an eval enum body. + pub(in crate::parser) fn parse_enum_case_decl(&mut self) -> Result { + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + let value = if self.consume(TokenKind::Equal) { + Some(self.parse_expr()?) + } else { + None + }; + self.expect_semicolon()?; + Ok(EvalEnumCase::new(name, value)) + } +} diff --git a/crates/elephc-magician/src/parser/statements/functions_namespaces.rs b/crates/elephc-magician/src/parser/statements/functions_namespaces.rs new file mode 100644 index 0000000000..e209d45a0e --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/functions_namespaces.rs @@ -0,0 +1,246 @@ +//! Purpose: +//! Parses function declarations, namespaces, and use imports. +//! +//! Called from: +//! - Top-level statement dispatch. +//! +//! Key details: +//! - Namespace and grouped-use resolution update parser import state before later statements. + +use super::*; + +impl Parser { + /// Parses `function name($param, ...) { ... }` declarations. + pub(in crate::parser) fn parse_function_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_function_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses `function name($param, ...) { ... }` declarations with attributes. + pub(in crate::parser) fn parse_function_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = self.qualify_name_in_current_namespace(name); + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params("", false)?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + let return_type = self.parse_optional_return_type(EvalTypePosition::FunctionReturn)?; + let (body, source_end_line) = self.parse_block_with_end_line()?; + Ok(vec![EvalStmt::FunctionDecl { + name, + source_location: Some(EvalSourceLocation::new(source_start_line, source_end_line)), + attributes, + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + return_type, + body, + }]) + } + + /// Parses `namespace Name;` or `namespace Name { ... }` eval namespace blocks. + pub(in crate::parser) fn parse_namespace_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let namespace = if self.consume(TokenKind::LBrace) { + return self.parse_namespace_block(String::new()); + } else { + self.parse_namespace_name()? + }; + if self.consume_semicolon() { + self.namespace = namespace; + self.imports = NamespaceImports::default(); + return Ok(Vec::new()); + } + self.expect(TokenKind::LBrace)?; + self.parse_namespace_block(namespace) + } + + /// Parses statements inside an already opened namespace block. + pub(in crate::parser) fn parse_namespace_block( + &mut self, + namespace: String, + ) -> Result, EvalParseError> { + let previous = std::mem::replace(&mut self.namespace, namespace); + let previous_imports = std::mem::take(&mut self.imports); + let previous_allow_use_imports = std::mem::replace(&mut self.allow_use_imports, true); + let result = self.parse_block_contents(); + self.namespace = previous; + self.imports = previous_imports; + self.allow_use_imports = previous_allow_use_imports; + result + } + + /// Parses a namespace declaration name without a leading global separator. + pub(in crate::parser) fn parse_namespace_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses PHP `use`, `use function`, and `use const` import declarations. + pub(in crate::parser) fn parse_use_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let kind = self.parse_use_import_kind(); + + loop { + self.parse_use_import(kind)?; + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(Vec::new()) + } + + /// Parses an optional top-level `function` or `const` use-import kind. + pub(in crate::parser) fn parse_use_import_kind(&mut self) -> UseImportKind { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + self.advance(); + UseImportKind::Function + } else if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + self.advance(); + UseImportKind::Const + } else { + UseImportKind::Class + } + } + + /// Parses and registers one comma-separated import entry. + pub(in crate::parser) fn parse_use_import(&mut self, kind: UseImportKind) -> Result<(), EvalParseError> { + let (name, grouped) = self.parse_use_name_or_group_start()?; + if grouped { + return self.parse_grouped_use_imports(kind, name); + } + self.parse_use_alias_and_register(kind, name) + } + + /// Parses a use-import name, stopping after a trailing namespace separator before `{`. + pub(in crate::parser) fn parse_use_name_or_group_start( + &mut self, + ) -> Result<(String, bool), EvalParseError> { + let _ = self.consume(TokenKind::Backslash); + let TokenKind::Ident(first) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let mut name = first.clone(); + self.advance(); + while self.consume(TokenKind::Backslash) { + if self.consume(TokenKind::LBrace) { + return Ok((name, true)); + } + let TokenKind::Ident(part) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + name.push('\\'); + name.push_str(part); + self.advance(); + } + Ok((name, false)) + } + + /// Parses all members inside a grouped namespace import declaration. + pub(in crate::parser) fn parse_grouped_use_imports( + &mut self, + default_kind: UseImportKind, + prefix: String, + ) -> Result<(), EvalParseError> { + if matches!(self.current(), TokenKind::RBrace) { + return Err(EvalParseError::UnexpectedToken); + } + loop { + let kind = self.parse_grouped_use_entry_kind(default_kind)?; + let member = self.parse_grouped_use_member_name()?; + let name = join_grouped_use_name(&prefix, &member); + self.parse_use_alias_and_register(kind, name)?; + if !self.consume(TokenKind::Comma) { + break; + } + if self.consume(TokenKind::RBrace) { + return Ok(()); + } + } + self.expect(TokenKind::RBrace) + } + + /// Parses an optional per-entry grouped import kind, matching PHP's mixed group rules. + pub(in crate::parser) fn parse_grouped_use_entry_kind( + &mut self, + default_kind: UseImportKind, + ) -> Result { + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Function); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if default_kind != UseImportKind::Class { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + return Ok(UseImportKind::Const); + } + Ok(default_kind) + } + + /// Parses one non-absolute member name inside a grouped use declaration. + pub(in crate::parser) fn parse_grouped_use_member_name(&mut self) -> Result { + let name = self.parse_qualified_name()?; + if name.absolute { + return Err(EvalParseError::UnexpectedToken); + } + Ok(name.name) + } + + /// Parses an optional alias and stores one namespace import. + pub(in crate::parser) fn parse_use_alias_and_register( + &mut self, + kind: UseImportKind, + name: String, + ) -> Result<(), EvalParseError> { + let alias = if matches!( + self.current(), + TokenKind::Ident(keyword) if ident_eq(keyword, "as") + ) { + self.advance(); + let TokenKind::Ident(alias) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let alias = alias.clone(); + self.advance(); + alias + } else { + last_name_segment(&name).to_string() + }; + + match kind { + UseImportKind::Class => self.imports.insert_class(alias, name), + UseImportKind::Function => self.imports.insert_function(alias, name), + UseImportKind::Const => self.imports.insert_constant(alias, name), + } + Ok(()) + } +} diff --git a/crates/elephc-magician/src/parser/statements/globals_exceptions.rs b/crates/elephc-magician/src/parser/statements/globals_exceptions.rs new file mode 100644 index 0000000000..66bef6fea1 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/globals_exceptions.rs @@ -0,0 +1,109 @@ +//! Purpose: +//! Parses global/static declarations and throw/try/catch statements. +//! +//! Called from: +//! - Top-level and nested statement dispatch. +//! +//! Key details: +//! - Catch union types are canonicalized when parsed into EvalIR. + +use super::*; + +impl Parser { + /// Parses `global $name, $other;` declarations in eval fragments. + pub(in crate::parser) fn parse_global_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let mut vars = Vec::new(); + loop { + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + vars.push(name.clone()); + self.advance(); + if !self.consume(TokenKind::Comma) { + break; + } + } + self.expect_semicolon()?; + Ok(vec![EvalStmt::Global { vars }]) + } + + /// Parses `static $name = expr;` declarations in eval fragments. + pub(in crate::parser) fn parse_static_var_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::Equal)?; + let init = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::StaticVar { name, init }]) + } + + /// Parses `throw expr;` statements in eval fragments. + pub(in crate::parser) fn parse_throw_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let expr = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::Throw(expr)]) + } + + /// Parses `try { ... } catch (Type|Other $name) { ... } finally { ... }` statements. + pub(in crate::parser) fn parse_try_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_block()?; + let mut catches = Vec::new(); + while matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "catch")) { + catches.push(self.parse_catch_clause()?); + } + let finally_body = if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "finally")) + { + self.advance(); + self.parse_block()? + } else { + Vec::new() + }; + if catches.is_empty() && finally_body.is_empty() { + return Err(EvalParseError::UnexpectedToken); + } + Ok(vec![EvalStmt::Try { + body, + catches, + finally_body, + }]) + } + + /// Parses one `catch (ClassName|Other [$name]) { ... }` clause. + pub(in crate::parser) fn parse_catch_clause(&mut self) -> Result { + self.advance(); + self.expect(TokenKind::LParen)?; + let class_names = self.parse_catch_types()?; + let var_name = if let TokenKind::DollarIdent(var_name) = self.current() { + let var_name = var_name.clone(); + self.advance(); + Some(var_name) + } else { + None + }; + self.expect(TokenKind::RParen)?; + let body = self.parse_block()?; + Ok(EvalCatch { + class_names, + var_name, + body, + }) + } + + /// Parses one or more unioned catch types in source order. + pub(in crate::parser) fn parse_catch_types(&mut self) -> Result, EvalParseError> { + let class_name = self.parse_class_reference_name(false)?; + let mut class_names = vec![self.resolve_class_name(class_name)]; + while self.consume(TokenKind::Pipe) { + let class_name = self.parse_class_reference_name(false)?; + class_names.push(self.resolve_class_name(class_name)); + } + Ok(class_names) + } +} diff --git a/crates/elephc-magician/src/parser/statements/interfaces.rs b/crates/elephc-magician/src/parser/statements/interfaces.rs new file mode 100644 index 0000000000..b799661781 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/interfaces.rs @@ -0,0 +1,293 @@ +//! Purpose: +//! Parses interfaces, parents, abstract methods, properties, and hook contracts. +//! +//! Called from: +//! - Statement dispatch for attributed and plain interface declarations. +//! +//! Key details: +//! - Interface property hook requirements and types remain declarative EvalIR metadata. + +use super::*; + +impl Parser { + /// Parses `interface Name [extends Parent, ...] { function name(...); }`. + pub(in crate::parser) fn parse_interface_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_interface_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses an interface declaration and attaches already parsed class-like attributes. + pub(in crate::parser) fn parse_interface_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let name = self.parse_class_like_decl_name()?; + let parents = self.parse_interface_parent_clause()?; + self.expect(TokenKind::LBrace)?; + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_interface_member(&mut constants, &mut properties, &mut methods)?; + }; + self.consume_semicolon(); + Ok(vec![EvalStmt::InterfaceDecl( + EvalInterface::with_constants_and_properties( + name, parents, constants, properties, methods, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_attributes(attributes), + )]) + } + + /// Parses an optional `extends Parent, ...` interface declaration clause. + pub(in crate::parser) fn parse_interface_parent_clause(&mut self) -> Result, EvalParseError> { + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "extends")) { + return Ok(Vec::new()); + } + self.advance(); + let mut parents = Vec::new(); + loop { + let parent = self.parse_class_reference_name(false)?; + parents.push(self.resolve_class_name(parent)); + if !self.consume(TokenKind::Comma) { + break; + } + } + Ok(parents) + } + + /// Parses one eval interface constant, property contract, or method signature. + pub(in crate::parser) fn parse_interface_member( + &mut self, + constants: &mut Vec, + properties: &mut Vec, + methods: &mut Vec, + ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; + let mut is_static = false; + let mut is_final = false; + let mut saw_public = false; + let mut set_visibility = None; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + self.advance(); + if self.consume_set_marker()? { + if set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Public); + } else if saw_public { + return Err(EvalParseError::UnsupportedConstruct); + } else { + saw_public = true; + } + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + self.advance(); + if !self.consume_set_marker()? || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Protected); + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + self.advance(); + if !self.consume_set_marker()? || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + set_visibility = Some(EvalVisibility::Private); + } + TokenKind::Ident(name) if ident_eq(name, "static") => { + if is_static { + return Err(EvalParseError::UnsupportedConstruct); + } + is_static = true; + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "final") => { + if is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + is_final = true; + self.advance(); + } + TokenKind::Ident(name) if is_unsupported_class_member_modifier(name) => { + return Err(EvalParseError::UnsupportedConstruct); + } + _ => break, + } + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + constants.extend(self.parse_class_const_decl( + EvalVisibility::Public, + is_final, + &attributes, + )?); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_final || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + methods.push( + self.parse_interface_method_decl_after_function_keyword(is_static)? + .with_attributes(attributes), + ); + return Ok(()); + } + if is_static || is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + properties.push( + self.parse_interface_property_decl(set_visibility)? + .with_attributes(attributes), + ); + Ok(()) + } + + /// Parses one eval interface method signature after `function` has been selected. + pub(in crate::parser) fn parse_interface_method_decl_after_function_keyword( + &mut self, + is_static: bool, + ) -> Result { + let source_start_line = self.current_line(); + self.advance(); + let TokenKind::Ident(name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let name = name.clone(); + self.advance(); + self.expect(TokenKind::LParen)?; + let ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + } = self.parse_method_params(&name, true)?; + if !promoted_properties.is_empty() || !promoted_assignments.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + let return_type = self.parse_optional_return_type(EvalTypePosition::MethodReturn)?; + let source_end_line = self.current_line(); + self.expect_semicolon()?; + Ok(EvalInterfaceMethod::new(name, params) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_static(is_static) + .with_parameter_types(parameter_types) + .with_parameter_attributes(parameter_attributes) + .with_parameter_defaults(parameter_defaults) + .with_parameter_by_ref_flags(parameter_is_by_ref) + .with_parameter_variadic_flags(parameter_is_variadic) + .with_return_type(return_type)) + } + + /// Parses one interface property hook contract. + pub(in crate::parser) fn parse_interface_property_decl( + &mut self, + set_visibility: Option, + ) -> Result { + let property_type = self.parse_optional_property_type()?; + if set_visibility.is_some() && property_type.is_none() { + return Err(EvalParseError::UnsupportedConstruct); + } + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let name = name.clone(); + self.advance(); + if matches!(self.current(), TokenKind::Equal) { + return Err(EvalParseError::UnsupportedConstruct); + } + let (requires_get, requires_set) = self.parse_interface_property_hook_contracts()?; + if set_visibility.is_some() && !requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(EvalInterfaceProperty::new(name, requires_get, requires_set) + .with_type(property_type) + .with_set_visibility(set_visibility)) + } + + /// Parses `{ get; set; }` hook contracts for an abstract or interface property. + pub(in crate::parser) fn parse_property_hook_contracts(&mut self) -> Result<(bool, bool), EvalParseError> { + self.expect(TokenKind::LBrace)?; + let mut requires_get = false; + let mut requires_set = false; + while !self.consume(TokenKind::RBrace) { + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + let returns_by_ref = self.consume(TokenKind::Ampersand); + let TokenKind::Ident(hook_name) = self.current() else { + return Err(EvalParseError::UnexpectedToken); + }; + let is_get = ident_eq(hook_name, "get"); + let is_set = ident_eq(hook_name, "set"); + if !is_get && !is_set { + return Err(EvalParseError::UnsupportedConstruct); + } + if returns_by_ref && !is_get { + return Err(EvalParseError::UnsupportedConstruct); + } + self.advance(); + if matches!( + self.current(), + TokenKind::LParen | TokenKind::FatArrow | TokenKind::LBrace + ) { + return Err(EvalParseError::UnsupportedConstruct); + } + self.expect_semicolon()?; + if is_get { + if requires_get { + return Err(EvalParseError::UnsupportedConstruct); + } + requires_get = true; + } else { + if requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + requires_set = true; + } + } + if !requires_get && !requires_set { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok((requires_get, requires_set)) + } + + /// Parses `{ get; set; }` hook contracts for an interface property. + pub(in crate::parser) fn parse_interface_property_hook_contracts( + &mut self, + ) -> Result<(bool, bool), EvalParseError> { + self.parse_property_hook_contracts() + } + + /// Parses retained property type metadata before the `$property` token. + pub(in crate::parser) fn parse_optional_property_type( + &mut self, + ) -> Result, EvalParseError> { + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis + ) { + return Ok(None); + } + self.parse_type_decl(EvalTypePosition::Property) + } +} diff --git a/crates/elephc-magician/src/parser/statements/loops.rs b/crates/elephc-magician/src/parser/statements/loops.rs new file mode 100644 index 0000000000..429c9856d4 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/loops.rs @@ -0,0 +1,106 @@ +//! Purpose: +//! Parses do-while, for, foreach, and array-set loop-adjacent statements. +//! +//! Called from: +//! - `Parser::parse_stmt()` and for-clause parsing. +//! +//! Key details: +//! - Foreach key/value targets and statement bodies retain EvalIR source order. + +use super::*; + +impl Parser { + /// Parses `do { ... } while (expr);`. + pub(in crate::parser) fn parse_do_while_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + let body = self.parse_statement_body()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "while")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + self.expect(TokenKind::LParen)?; + let condition = self.parse_expr()?; + self.expect(TokenKind::RParen)?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::DoWhile { body, condition }]) + } + + /// Parses `$name[index] = expr;` and `$name[] = expr;` eval writes. + pub(in crate::parser) fn parse_array_set_stmt( + &mut self, + name: String, + ) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LBracket)?; + if self.consume(TokenKind::RBracket) { + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + return Ok(vec![EvalStmt::ArrayAppendVar { name, value }]); + } + let index = self.parse_expr()?; + self.expect(TokenKind::RBracket)?; + self.expect(TokenKind::Equal)?; + let value = self.parse_expr()?; + self.expect_semicolon()?; + Ok(vec![EvalStmt::ArraySetVar { name, index, value }]) + } + + /// Parses `for (init; condition; update) { ... }`. + pub(in crate::parser) fn parse_for_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let init = self.parse_for_init_clause()?; + self.expect_semicolon()?; + let condition = if matches!(self.current(), TokenKind::Semicolon) { + None + } else { + Some(self.parse_expr()?) + }; + self.expect_semicolon()?; + let update = self.parse_for_update_clause()?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::For { + init, + condition, + update, + body, + }]) + } + + /// Parses `foreach (expr as $value) { ... }` or `foreach (expr as $key => $value) { ... }`. + pub(in crate::parser) fn parse_foreach_stmt(&mut self) -> Result, EvalParseError> { + self.advance(); + self.expect(TokenKind::LParen)?; + let array = self.parse_expr()?; + if !matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "as")) { + return Err(EvalParseError::UnexpectedToken); + } + self.advance(); + let TokenKind::DollarIdent(value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let value_name = value_name.clone(); + self.advance(); + let (key_name, value_name) = if matches!(self.current(), TokenKind::FatArrow) { + self.advance(); + let TokenKind::DollarIdent(next_value_name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + let key_name = value_name; + let value_name = next_value_name.clone(); + self.advance(); + (Some(key_name), value_name) + } else { + (None, value_name) + }; + self.expect(TokenKind::RParen)?; + let body = self.parse_statement_body()?; + Ok(vec![EvalStmt::Foreach { + array, + key_name, + value_name, + body, + }]) + } +} diff --git a/crates/elephc-magician/src/parser/statements/parameters_types.rs b/crates/elephc-magician/src/parser/statements/parameters_types.rs new file mode 100644 index 0000000000..e89c1b23a3 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/parameters_types.rs @@ -0,0 +1,333 @@ +//! Purpose: +//! Parses callable parameters, promoted modifiers, and PHP type declarations. +//! +//! Called from: +//! - Function, method, closure, property, and hook parsing. +//! +//! Key details: +//! - Union/intersection atoms, nullability, defaults, variadics, and promotion stay indexed together. + +use super::*; + +impl Parser { + /// Parses a method parameter list and records metadata plus promotion side effects. + pub(in crate::parser) fn parse_method_params( + &mut self, + method_name: &str, + allow_class_scope_types: bool, + ) -> Result { + let mut params = Vec::new(); + let mut parameter_attributes = Vec::new(); + let mut parameter_types = Vec::new(); + let mut parameter_defaults = Vec::new(); + let mut parameter_is_by_ref = Vec::new(); + let mut parameter_is_variadic = Vec::new(); + let mut promoted_properties = Vec::new(); + let mut promoted_assignments = Vec::new(); + if self.consume(TokenKind::RParen) { + return Ok(ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + }); + } + loop { + let attributes = self.parse_attribute_groups()?; + let promotion = self.parse_promoted_parameter_modifiers()?; + if promotion.is_some() && !method_name.eq_ignore_ascii_case("__construct") { + return Err(EvalParseError::UnsupportedConstruct); + } + let param_type = if promotion.is_some() { + self.parse_optional_promoted_property_type()? + } else { + let position = if allow_class_scope_types { + EvalTypePosition::MethodParameter + } else { + EvalTypePosition::FunctionParameter + }; + self.parse_optional_parameter_type(position)? + }; + let is_by_ref = self.consume(TokenKind::Ampersand); + let is_variadic = self.consume(TokenKind::Ellipsis); + let TokenKind::DollarIdent(name) = self.current() else { + return Err(EvalParseError::ExpectedVariable); + }; + if promotion.is_some() && is_variadic { + return Err(EvalParseError::UnsupportedConstruct); + } + if let Some((visibility, is_readonly)) = promotion { + promoted_properties.push( + EvalClassProperty::with_visibility_static_final_and_readonly( + name.clone(), + visibility, + false, + false, + is_readonly, + None, + ) + .with_type(param_type.clone()) + .with_promoted() + .with_attributes(attributes.clone()), + ); + promoted_assignments.push(promoted_property_assignment(name, is_by_ref)); + } + params.push(name.clone()); + parameter_attributes.push(attributes); + parameter_types.push(param_type); + parameter_is_by_ref.push(is_by_ref); + parameter_is_variadic.push(is_variadic); + self.advance(); + let default = if self.consume(TokenKind::Equal) { + if is_variadic { + return Err(EvalParseError::UnsupportedConstruct); + } + let default = self.parse_expr()?; + if !method_parameter_default_is_supported(&default) { + return Err(EvalParseError::UnsupportedConstruct); + } + Some(default) + } else { + None + }; + parameter_defaults.push(default); + if !self.consume(TokenKind::Comma) { + break; + } + if is_variadic { + return Err(EvalParseError::UnsupportedConstruct); + } + if matches!(self.current(), TokenKind::RParen) { + return Err(EvalParseError::ExpectedVariable); + } + } + self.expect(TokenKind::RParen)?; + Ok(ParsedMethodParams { + params, + parameter_attributes, + parameter_types, + parameter_defaults, + parameter_is_by_ref, + parameter_is_variadic, + promoted_properties, + promoted_assignments, + }) + } + + /// Parses visibility and readonly modifiers on a promoted constructor parameter. + pub(super) fn parse_promoted_parameter_modifiers( + &mut self, + ) -> Result, EvalParseError> { + let mut visibility = None; + let mut is_readonly = false; + let mut saw_modifier = false; + loop { + match self.current() { + TokenKind::Ident(name) if ident_eq(name, "public") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Public); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "protected") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Protected); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "private") => { + if visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + visibility = Some(EvalVisibility::Private); + self.advance(); + } + TokenKind::Ident(name) if ident_eq(name, "readonly") => { + if is_readonly { + return Err(EvalParseError::UnsupportedConstruct); + } + saw_modifier = true; + is_readonly = true; + self.advance(); + } + _ => break, + } + } + if saw_modifier { + Ok(Some(( + visibility.unwrap_or(EvalVisibility::Public), + is_readonly, + ))) + } else { + Ok(None) + } + } + + /// Parses a constructor-promoted parameter type using PHP property-type restrictions. + pub(super) fn parse_optional_promoted_property_type( + &mut self, + ) -> Result, EvalParseError> { + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis + ) { + return Ok(None); + } + self.parse_type_decl(EvalTypePosition::Property) + } + + /// Consumes a supported method parameter type and returns retained metadata. + pub(super) fn parse_optional_parameter_type( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + if matches!( + self.current(), + TokenKind::DollarIdent(_) | TokenKind::Ampersand | TokenKind::Ellipsis + ) { + return Ok(None); + } + self.parse_type_decl(position) + } + + /// Consumes a supported function or method return type after `:`. + pub(in crate::parser) fn parse_optional_return_type( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + if !self.consume(TokenKind::Colon) { + return Ok(None); + } + self.parse_type_decl(position) + } + + /// Parses one PHP type declaration and returns retained eval metadata. + pub(super) fn parse_type_decl( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + let nullable_shorthand = self.consume(TokenKind::Question); + if nullable_shorthand && matches!(self.current(), TokenKind::DollarIdent(_)) { + return Err(EvalParseError::UnexpectedToken); + } + let first = self.parse_type_name(position)?; + let mut variants = Vec::new(); + let mut allows_null = nullable_shorthand || matches!(first, None); + if let Some(first) = first { + variants.push(first); + } + if nullable_shorthand && matches!(self.current(), TokenKind::Pipe) { + return Err(EvalParseError::UnsupportedConstruct); + } + if nullable_shorthand + && matches!(self.current(), TokenKind::Ampersand) + && !self.next_token_starts_parameter_storage() + { + return Err(EvalParseError::UnsupportedConstruct); + } + if matches!(self.current(), TokenKind::Ampersand) + && !self.next_token_starts_parameter_storage() + { + while self.consume(TokenKind::Ampersand) { + let Some(variant) = self.parse_type_name(position)? else { + return Err(EvalParseError::UnsupportedConstruct); + }; + variants.push(variant); + } + if type_variants_contain_standalone_return_only_atoms(&variants) { + return Err(EvalParseError::UnsupportedConstruct); + } + return Ok(Some(EvalParameterType::intersection(variants))); + } + while self.consume(TokenKind::Pipe) { + match self.parse_type_name(position)? { + Some(variant) => variants.push(variant), + None => allows_null = true, + } + } + if type_variants_contain_standalone_return_only_atoms(&variants) + && (variants.len() != 1 || allows_null) + { + return Err(EvalParseError::UnsupportedConstruct); + } + Ok(Some(EvalParameterType::new(variants, allows_null))) + } + + /// Returns whether `&` belongs to by-reference parameter storage. + pub(super) fn next_token_starts_parameter_storage(&self) -> bool { + matches!(self.peek(), TokenKind::DollarIdent(_) | TokenKind::Ellipsis) + } + + /// Consumes one simple qualified method type name. + pub(super) fn parse_type_name( + &mut self, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + match self.current() { + TokenKind::Ident(_) | TokenKind::Backslash => { + let name = self.parse_qualified_name()?; + self.type_variant_from_name(name, position) + } + _ => Err(EvalParseError::UnexpectedToken), + } + } + + /// Converts one parsed PHP type name to retained eval metadata. + pub(super) fn type_variant_from_name( + &self, + name: ParsedQualifiedName, + position: EvalTypePosition, + ) -> Result, EvalParseError> { + if !name.absolute { + let lower = name.name.to_ascii_lowercase(); + let builtin = match lower.as_str() { + "array" => Some(EvalParameterTypeVariant::Array), + "bool" => Some(EvalParameterTypeVariant::Bool), + "callable" if matches!(position, EvalTypePosition::Property) => { + return Err(EvalParseError::UnsupportedConstruct); + } + "callable" => Some(EvalParameterTypeVariant::Callable), + "float" => Some(EvalParameterTypeVariant::Float), + "int" => Some(EvalParameterTypeVariant::Int), + "iterable" => Some(EvalParameterTypeVariant::Iterable), + "mixed" => Some(EvalParameterTypeVariant::Mixed), + "never" if type_position_allows_return_only_atoms(position) => { + Some(EvalParameterTypeVariant::Never) + } + "null" => return Ok(None), + "object" => Some(EvalParameterTypeVariant::Object), + "string" => Some(EvalParameterTypeVariant::String), + "void" if type_position_allows_return_only_atoms(position) => { + Some(EvalParameterTypeVariant::Void) + } + "void" | "never" => return Err(EvalParseError::UnsupportedConstruct), + "static" if matches!(position, EvalTypePosition::MethodReturn) => { + Some(EvalParameterTypeVariant::Class(lower.to_string())) + } + "static" => return Err(EvalParseError::UnsupportedConstruct), + "self" | "parent" if !type_position_allows_class_scope_atoms(position) => { + return Err(EvalParseError::UnsupportedConstruct); + } + "self" | "parent" => { + Some(EvalParameterTypeVariant::Class(lower.to_string())) + } + _ => None, + }; + if let Some(builtin) = builtin { + return Ok(Some(builtin)); + } + } + Ok(Some(EvalParameterTypeVariant::Class( + self.resolve_class_name(name), + ))) + } +} diff --git a/crates/elephc-magician/src/parser/statements/property_builders.rs b/crates/elephc-magician/src/parser/statements/property_builders.rs new file mode 100644 index 0000000000..839b3a9d8d --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/property_builders.rs @@ -0,0 +1,279 @@ +//! Purpose: +//! Builds EvalIR statements and attribute values from parsed property-like syntax. +//! +//! Called from: +//! - Assignment, member, attribute, and property-hook parsing. +//! +//! Key details: +//! - Reference, inc/dec, array mutation, literals, and class-name attribute args are normalized here. + +use super::*; + +/// Builds a property by-reference binding statement from a parsed property target. +pub(super) fn property_reference_bind_stmt( + target: EvalExpr, + source: String, +) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyReferenceBind { + object: *object, + property, + source, + }), + EvalExpr::DynamicPropertyGet { object, property } => { + Ok(EvalStmt::DynamicPropertyReferenceBind { + object: *object, + property: *property, + source, + }) + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyReferenceBind { + class_name: *class_name, + property, + source, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: *class_name, + property: *property, + source, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Builds an object-property increment/decrement statement from a parsed property target. +pub(super) fn property_inc_dec_stmt(target: EvalExpr, increment: bool) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyIncDec { + object: *object, + property, + increment, + }), + EvalExpr::DynamicPropertyGet { object, property } => Ok(EvalStmt::DynamicPropertyIncDec { + object: *object, + property: *property, + increment, + }), + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyIncDec { + class_name: *class_name, + property, + increment, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameIncDec { + class_name: *class_name, + property: *property, + increment, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Builds an object-property array append statement from a parsed property target. +pub(super) fn property_array_append_stmt(target: EvalExpr, value: EvalExpr) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyArrayAppend { + object: *object, + property, + value, + }), + EvalExpr::DynamicPropertyGet { object, property } => { + Ok(EvalStmt::DynamicPropertyArrayAppend { + object: *object, + property: *property, + value, + }) + } + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: *class_name, + property, + value, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name: *class_name, + property: *property, + value, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Builds an object-property array write statement from a parsed property target. +pub(super) fn property_array_set_stmt( + target: EvalExpr, + index: EvalExpr, + op: Option, + value: EvalExpr, +) -> Result { + match target { + EvalExpr::PropertyGet { object, property } => Ok(EvalStmt::PropertyArraySet { + object: *object, + property, + index, + op, + value, + }), + EvalExpr::DynamicPropertyGet { object, property } => Ok(EvalStmt::DynamicPropertyArraySet { + object: *object, + property: *property, + index, + op, + value, + }), + EvalExpr::DynamicStaticPropertyGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyArraySet { + class_name: *class_name, + property, + index, + op, + value, + }), + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => Ok(EvalStmt::DynamicStaticPropertyNameArraySet { + class_name: *class_name, + property: *property, + index, + op, + value, + }), + _ => Err(EvalParseError::UnexpectedToken), + } +} + +/// Converts a parsed attribute argument expression into retained literal metadata. +pub(super) fn eval_attribute_arg_from_expr(expr: &EvalExpr) -> Option { + match expr { + EvalExpr::Const(EvalConst::String(value)) => Some(EvalAttributeArg::String(value.clone())), + EvalExpr::Const(EvalConst::Int(value)) => Some(EvalAttributeArg::Int(*value)), + EvalExpr::Const(EvalConst::Float(value)) => Some(EvalAttributeArg::Float(value.to_bits())), + EvalExpr::Const(EvalConst::Bool(value)) => Some(EvalAttributeArg::Bool(*value)), + EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Null), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => match expr.as_ref() { + EvalExpr::Const(EvalConst::Int(value)) => { + Some(EvalAttributeArg::Int(value.wrapping_neg())) + } + EvalExpr::Const(EvalConst::Float(value)) => { + Some(EvalAttributeArg::Float((-*value).to_bits())) + } + _ => None, + }, + EvalExpr::ClassNameFetch { class_name } => { + eval_attribute_class_name_arg(class_name).map(EvalAttributeArg::String) + } + EvalExpr::Array(elements) => eval_attribute_array_arg_from_elements(elements), + _ => None, + } +} + +/// Converts an eval array literal into retained attribute metadata. +pub(super) fn eval_attribute_array_arg_from_elements( + elements: &[EvalArrayElement], +) -> Option { + elements + .iter() + .map(|element| match element { + EvalArrayElement::Value(value) => eval_attribute_arg_from_expr(value), + EvalArrayElement::Reference(_) => None, + EvalArrayElement::KeyValue { key, value } => { + let value = eval_attribute_arg_from_expr(value)?; + eval_attribute_array_keyed_arg(key, value) + } + EvalArrayElement::KeyReference { .. } => None, + }) + .collect::>>() + .map(EvalAttributeArg::Array) +} + +/// Wraps an attribute array value with the PHP-normalized literal key metadata. +pub(super) fn eval_attribute_array_keyed_arg( + key: &EvalExpr, + value: EvalAttributeArg, +) -> Option { + match key { + EvalExpr::Const(EvalConst::String(name)) => Some(EvalAttributeArg::Named { + name: name.clone(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Int(key)) => Some(EvalAttributeArg::IntKeyed { + key: *key, + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Bool(key)) => Some(EvalAttributeArg::IntKeyed { + key: i64::from(*key), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Null) => Some(EvalAttributeArg::Named { + name: String::new(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Float(key)) => Some(EvalAttributeArg::IntKeyed { + key: *key as i64, + value: Box::new(value), + }), + EvalExpr::Unary { + op: EvalUnaryOp::Negate, + expr, + } => eval_attribute_array_negated_keyed_arg(expr, value), + EvalExpr::ClassNameFetch { class_name } => { + eval_attribute_class_name_arg(class_name).map(|name| EvalAttributeArg::Named { + name, + value: Box::new(value), + }) + } + _ => None, + } +} + +/// Wraps an attribute array value with a normalized negative numeric literal key. +pub(super) fn eval_attribute_array_negated_keyed_arg( + key: &EvalExpr, + value: EvalAttributeArg, +) -> Option { + match key { + EvalExpr::Const(EvalConst::Int(key)) => Some(EvalAttributeArg::IntKeyed { + key: key.wrapping_neg(), + value: Box::new(value), + }), + EvalExpr::Const(EvalConst::Float(key)) => Some(EvalAttributeArg::IntKeyed { + key: (-*key) as i64, + value: Box::new(value), + }), + _ => None, + } +} + +/// Returns a compile-time class-name string for named `ClassName::class` attribute args. +pub(super) fn eval_attribute_class_name_arg(class_name: &str) -> Option { + let class_name = class_name.trim_start_matches('\\'); + if ["self", "parent", "static"] + .iter() + .any(|special| class_name.eq_ignore_ascii_case(special)) + { + return None; + } + Some(class_name.to_string()) +} diff --git a/crates/elephc-magician/src/parser/statements/property_hook_analysis.rs b/crates/elephc-magician/src/parser/statements/property_hook_analysis.rs new file mode 100644 index 0000000000..b960cf8a85 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/property_hook_analysis.rs @@ -0,0 +1,537 @@ +//! Purpose: +//! Detects property backing-slot usage inside parsed hook statements and expressions. +//! +//! Called from: +//! - Property-hook declaration parsing after hook bodies are available. +//! +//! Key details: +//! - The recursive walk covers every EvalIR statement and expression shape without executing code. + +use super::*; + +/// Returns whether any parsed property hook accessor uses its own backing slot. +pub(super) fn property_hook_methods_use_backing_slot( + hook_methods: &[EvalClassMethod], + property_name: &str, +) -> bool { + hook_methods.iter().any(|method| { + method + .body() + .iter() + .any(|stmt| eval_stmt_uses_this_property(stmt, property_name)) + }) +} + +/// Returns whether one statement touches `$this->{$property_name}` directly. +pub(super) fn eval_stmt_uses_this_property(stmt: &EvalStmt, property_name: &str) -> bool { + match stmt { + EvalStmt::ArrayAppendVar { value, .. } => { + eval_expr_uses_this_property(value, property_name) + } + EvalStmt::ArraySetVar { index, value, .. } => { + eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::Break + | EvalStmt::Continue + | EvalStmt::ClassDecl(_) + | EvalStmt::EnumDecl(_) + | EvalStmt::FunctionDecl { .. } + | EvalStmt::Global { .. } + | EvalStmt::InterfaceDecl(_) + | EvalStmt::ReferenceAssign { .. } + | EvalStmt::TraitDecl(_) + | EvalStmt::UnsetVar { .. } => false, + EvalStmt::UnsetArrayElement { array, index } => { + eval_expr_uses_this_property(array, property_name) + || eval_expr_uses_this_property(index, property_name) + } + EvalStmt::DoWhile { body, condition } | EvalStmt::While { condition, body } => { + eval_expr_uses_this_property(condition, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::Echo(expr) + | EvalStmt::Expr(expr) + | EvalStmt::StaticVar { init: expr, .. } + | EvalStmt::Throw(expr) => eval_expr_uses_this_property(expr, property_name), + EvalStmt::For { + init, + condition, + update, + body, + } => { + eval_stmt_list_uses_this_property(init, property_name) + || condition + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_stmt_list_uses_this_property(update, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::Foreach { array, body, .. } => { + eval_expr_uses_this_property(array, property_name) + || eval_stmt_list_uses_this_property(body, property_name) + } + EvalStmt::If { + condition, + then_branch, + else_branch, + } => { + eval_expr_uses_this_property(condition, property_name) + || eval_stmt_list_uses_this_property(then_branch, property_name) + || eval_stmt_list_uses_this_property(else_branch, property_name) + } + EvalStmt::Return(expr) => expr + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)), + EvalStmt::PropertyReferenceBind { + object, property, .. + } + | EvalStmt::UnsetProperty { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalStmt::DynamicPropertyReferenceBind { + object, property, .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::UnsetDynamicProperty { object, property } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::UnsetDynamicStaticProperty { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalStmt::UnsetDynamicStaticPropertyName { + class_name, + property, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::StaticPropertyIncDec { .. } + | EvalStmt::StaticPropertyReferenceBind { .. } + | EvalStmt::UnsetStaticProperty { .. } => false, + EvalStmt::DynamicPropertySet { + object, + property, + value, + } + | EvalStmt::DynamicPropertyArrayAppend { + object, + property, + value, + } + | EvalStmt::DynamicPropertyCompoundAssign { + object, + property, + value, + .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicPropertyArraySet { + object, + property, + index, + value, + .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicPropertyIncDec { + object, property, .. + } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::PropertySet { + object, + property, + value, + } + | EvalStmt::PropertyArrayAppend { + object, + property, + value, + } + | EvalStmt::PropertyCompoundAssign { + object, + property, + value, + .. + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::PropertyArraySet { + object, + property, + index, + value, + .. + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::PropertyIncDec { + object, property, .. + } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalStmt::DynamicStaticPropertySet { + class_name, value, .. + } + | EvalStmt::DynamicStaticPropertyArrayAppend { + class_name, value, .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyArraySet { + class_name, + index, + value, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyIncDec { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalStmt::DynamicStaticPropertyReferenceBind { class_name, .. } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalStmt::DynamicStaticPropertyNameSet { + class_name, + property, + value, + } + | EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name, + property, + value, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyNameArraySet { + class_name, + property, + index, + value, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + || eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name, + property, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name, + property, + .. + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalStmt::StaticPropertySet { value, .. } + | EvalStmt::StaticPropertyArrayAppend { value, .. } + | EvalStmt::StoreVar { value, .. } => { + eval_expr_uses_this_property(value, property_name) + } + EvalStmt::StaticPropertyArraySet { index, value, .. } => { + eval_expr_uses_this_property(index, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalStmt::Switch { expr, cases } => { + eval_expr_uses_this_property(expr, property_name) + || cases.iter().any(|case| { + case.condition + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_stmt_list_uses_this_property(&case.body, property_name) + }) + } + EvalStmt::Try { + body, + catches, + finally_body, + } => { + eval_stmt_list_uses_this_property(body, property_name) + || catches + .iter() + .any(|catch| eval_stmt_list_uses_this_property(&catch.body, property_name)) + || eval_stmt_list_uses_this_property(finally_body, property_name) + } + } +} + +/// Returns whether any statement in a list touches `$this->{$property_name}` directly. +pub(super) fn eval_stmt_list_uses_this_property(stmts: &[EvalStmt], property_name: &str) -> bool { + stmts + .iter() + .any(|stmt| eval_stmt_uses_this_property(stmt, property_name)) +} + +/// Returns whether one expression touches `$this->{$property_name}` directly. +pub(super) fn eval_expr_uses_this_property(expr: &EvalExpr, property_name: &str) -> bool { + match expr { + EvalExpr::Array(elements) => elements.iter().any(|element| match element { + EvalArrayElement::Value(value) => eval_expr_uses_this_property(value, property_name), + EvalArrayElement::Reference(value) => { + eval_expr_uses_this_property(value, property_name) + } + EvalArrayElement::KeyValue { key, value } => { + eval_expr_uses_this_property(key, property_name) + || eval_expr_uses_this_property(value, property_name) + } + EvalArrayElement::KeyReference { key, value } => { + eval_expr_uses_this_property(key, property_name) + || eval_expr_uses_this_property(value, property_name) + } + }), + EvalExpr::ArrayGet { array, index } => { + eval_expr_uses_this_property(array, property_name) + || eval_expr_uses_this_property(index, property_name) + } + EvalExpr::Call { args, .. } + | EvalExpr::NamespacedCall { args, .. } + | EvalExpr::NewObject { args, .. } + | EvalExpr::StaticMethodCall { args, .. } => args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), + EvalExpr::DynamicCall { callee, args } => { + eval_expr_uses_this_property(callee, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::DynamicNewObject { class_name, args } => { + eval_expr_uses_this_property(class_name, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::DynamicStaticMethodCall { + class_name, + method, + args, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(method, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::DynamicStaticPropertyGet { class_name, .. } + | EvalExpr::DynamicClassConstantFetch { class_name, .. } + | EvalExpr::DynamicClassNameFetch { class_name } => { + eval_expr_uses_this_property(class_name, property_name) + } + EvalExpr::DynamicStaticPropertyNameGet { + class_name, + property, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalExpr::DynamicClassConstantNameFetch { + class_name, + constant, + } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(constant, property_name) + } + EvalExpr::DynamicMethodCall { + object, + method, + args, + } + | EvalExpr::NullsafeDynamicMethodCall { + object, + method, + args, + } => { + eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(method, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::Const(_) + | EvalExpr::ConstFetch(_) + | EvalExpr::Closure { .. } + | EvalExpr::FunctionCallable { .. } + | EvalExpr::ClassConstantFetch { .. } + | EvalExpr::ClassNameFetch { .. } + | EvalExpr::LoadVar(_) + | EvalExpr::Magic(_) + | EvalExpr::NamespacedConstFetch { .. } + | EvalExpr::StaticPropertyGet { .. } => false, + EvalExpr::MethodCallable { object, method } => { + eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(method, property_name) + } + EvalExpr::StaticMethodCallable { method, .. } => { + eval_expr_uses_this_property(method, property_name) + } + EvalExpr::InvokableCallable { object } => { + eval_expr_uses_this_property(object, property_name) + } + EvalExpr::DynamicStaticMethodCallable { class_name, method } => { + eval_expr_uses_this_property(class_name, property_name) + || eval_expr_uses_this_property(method, property_name) + } + EvalExpr::Include { path, .. } + | EvalExpr::Cast { expr: path, .. } + | EvalExpr::Clone(path) + | EvalExpr::Print(path) + | EvalExpr::Unary { expr: path, .. } => eval_expr_uses_this_property(path, property_name), + EvalExpr::InstanceOf { value, target } => { + eval_expr_uses_this_property(value, property_name) + || matches!( + target, + EvalInstanceOfTarget::Expr(target) + if eval_expr_uses_this_property(target, property_name) + ) + } + EvalExpr::Match { + subject, + arms, + default, + } => { + eval_expr_uses_this_property(subject, property_name) + || arms.iter().any(|arm| { + arm.patterns + .iter() + .any(|pattern| eval_expr_uses_this_property(pattern, property_name)) + || eval_expr_uses_this_property(&arm.value, property_name) + }) + || default + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + } + EvalExpr::MethodCall { object, args, .. } => { + eval_expr_uses_this_property(object, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::NullsafeMethodCall { object, args, .. } => { + eval_expr_uses_this_property(object, property_name) + || args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)) + } + EvalExpr::NewAnonymousClass { args, .. } => args + .iter() + .any(|arg| eval_expr_uses_this_property(arg.value(), property_name)), + EvalExpr::NullCoalesce { value, default } => { + eval_expr_uses_this_property(value, property_name) + || eval_expr_uses_this_property(default, property_name) + } + EvalExpr::PropertyGet { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalExpr::NullsafePropertyGet { object, property } => { + eval_is_this_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + } + EvalExpr::DynamicPropertyGet { object, property } + | EvalExpr::NullsafeDynamicPropertyGet { object, property } => { + eval_is_this_dynamic_property(object, property, property_name) + || eval_expr_uses_this_property(object, property_name) + || eval_expr_uses_this_property(property, property_name) + } + EvalExpr::Ternary { + condition, + then_branch, + else_branch, + } => { + eval_expr_uses_this_property(condition, property_name) + || then_branch + .as_ref() + .is_some_and(|expr| eval_expr_uses_this_property(expr, property_name)) + || eval_expr_uses_this_property(else_branch, property_name) + } + EvalExpr::Binary { left, right, .. } => { + eval_expr_uses_this_property(left, property_name) + || eval_expr_uses_this_property(right, property_name) + } + } +} + +/// Returns whether one object/property pair is exactly `$this->{$property_name}`. +pub(super) fn eval_is_this_property(object: &EvalExpr, property: &str, property_name: &str) -> bool { + matches!(object, EvalExpr::LoadVar(name) if name == "this") && property == property_name +} + +/// Returns whether one dynamic object/property pair is exactly `$this->{"property"}`. +pub(super) fn eval_is_this_dynamic_property( + object: &EvalExpr, + property: &EvalExpr, + property_name: &str, +) -> bool { + matches!( + (object, property), + ( + EvalExpr::LoadVar(object_name), + EvalExpr::Const(EvalConst::String(property)) + ) if object_name == "this" && property == property_name + ) +} + +/// Returns the synthetic get-hook method name for one property. +pub(super) fn property_hook_get_method(property_name: &str) -> String { + format!("__propget_{property_name}") +} + +/// Returns the synthetic set-hook method name for one property. +pub(super) fn property_hook_set_method(property_name: &str) -> String { + format!("__propset_{property_name}") +} + +/// Builds the implicit constructor assignment or alias for a promoted parameter. +pub(super) fn promoted_property_assignment(name: &str, is_by_ref: bool) -> EvalStmt { + if is_by_ref { + EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: name.to_string(), + source: name.to_string(), + } + } else { + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: name.to_string(), + value: EvalExpr::LoadVar(name.to_string()), + } + } +} diff --git a/crates/elephc-magician/src/parser/statements/traits.rs b/crates/elephc-magician/src/parser/statements/traits.rs new file mode 100644 index 0000000000..241c09f309 --- /dev/null +++ b/crates/elephc-magician/src/parser/statements/traits.rs @@ -0,0 +1,153 @@ +//! Purpose: +//! Parses trait declarations and trait-specific members. +//! +//! Called from: +//! - Statement dispatch for attributed and plain trait declarations. +//! +//! Key details: +//! - Trait properties and hooks reuse class-member parsing while preserving metadata. + +use super::*; + +impl Parser { + /// Parses `trait Name { ... }` declarations into dynamic trait metadata. + pub(in crate::parser) fn parse_trait_decl_stmt(&mut self) -> Result, EvalParseError> { + self.parse_trait_decl_stmt_with_attributes(Vec::new()) + } + + /// Parses a trait declaration and attaches already parsed class-like attributes. + pub(in crate::parser) fn parse_trait_decl_stmt_with_attributes( + &mut self, + attributes: Vec, + ) -> Result, EvalParseError> { + let source_start_line = self.current_line(); + self.advance(); + let name = self.parse_class_like_decl_name()?; + self.expect(TokenKind::LBrace)?; + let mut traits = Vec::new(); + let mut trait_adaptations = Vec::new(); + let mut constants = Vec::new(); + let mut properties = Vec::new(); + let mut methods = Vec::new(); + let source_end_line = loop { + if matches!(self.current(), TokenKind::RBrace) { + let source_end_line = self.current_line(); + self.advance(); + break source_end_line; + } + if matches!(self.current(), TokenKind::Eof) { + return Err(EvalParseError::UnexpectedEof); + } + self.parse_trait_member( + &mut constants, + &mut properties, + &mut methods, + &mut traits, + &mut trait_adaptations, + )?; + }; + self.consume_semicolon(); + Ok(vec![EvalStmt::TraitDecl( + EvalTrait::with_constants_traits_adaptations( + name, + constants, + properties, + methods, + traits, + trait_adaptations, + ) + .with_source_location(EvalSourceLocation::new(source_start_line, source_end_line)) + .with_attributes(attributes), + )]) + } + + /// Parses one property or method from an eval trait body. + pub(in crate::parser) fn parse_trait_member( + &mut self, + constants: &mut Vec, + properties: &mut Vec, + methods: &mut Vec, + traits: &mut Vec, + trait_adaptations: &mut Vec, + ) -> Result<(), EvalParseError> { + let attributes = self.parse_optional_member_attributes()?; + let (visibility, set_visibility, is_static, is_abstract, is_final, is_readonly) = + self.parse_class_member_modifiers()?; + if is_abstract && is_final { + return Err(EvalParseError::UnsupportedConstruct); + } + if visibility.is_none() + && !is_static + && !is_abstract + && !is_final + && !is_readonly + && set_visibility.is_none() + && matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "use")) + { + if !attributes.is_empty() { + return Err(EvalParseError::UnsupportedConstruct); + } + self.parse_class_trait_use(traits, trait_adaptations)?; + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "const")) { + if is_static || is_abstract || is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + constants.extend( + self.parse_class_const_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_final, + &attributes, + )?, + ); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "function")) { + if is_readonly || set_visibility.is_some() { + return Err(EvalParseError::UnsupportedConstruct); + } + let (method, promoted_properties) = self.parse_class_method_decl( + visibility.unwrap_or(EvalVisibility::Public), + is_static, + is_abstract, + is_final, + )?; + properties.extend(promoted_properties); + methods.push(method.with_attributes(attributes)); + return Ok(()); + } + if matches!(self.current(), TokenKind::Ident(name) if ident_eq(name, "var")) { + self.parse_legacy_var_property_member( + attributes, + visibility.is_some() + || is_static + || is_abstract + || is_final + || is_readonly + || set_visibility.is_some(), + false, + properties, + methods, + )?; + return Ok(()); + } + let visibility = visibility.unwrap_or(EvalVisibility::Public); + let (parsed_properties, mut hook_methods) = self.parse_class_property_decl( + visibility, + set_visibility, + is_static, + is_final, + is_readonly, + false, + is_abstract, + )?; + properties.extend( + parsed_properties + .into_iter() + .map(|property| property.with_attributes(attributes.clone())), + ); + methods.append(&mut hook_methods); + Ok(()) + } +} diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects.rs b/crates/elephc-magician/src/parser/tests/arrays_objects.rs deleted file mode 100644 index fecf94929e..0000000000 --- a/crates/elephc-magician/src/parser/tests/arrays_objects.rs +++ /dev/null @@ -1,672 +0,0 @@ -//! Purpose: -//! Parser tests for arrays, array writes, object properties, methods, and object construction. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - These cases cover postfix and aggregate expression syntax. - -use super::support::*; - -/// Verifies indexed array literals and reads parse as runtime array expressions. -#[test] -fn parse_fragment_accepts_indexed_array_read_source() { - let program = parse_fragment(br#"return [1, 2][0];"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(2))), - ])), - index: Box::new(EvalExpr::Const(EvalConst::Int(0))), - }))] - ); -} -/// Verifies legacy `array(...)` literals parse through the same EvalIR array node. -#[test] -fn parse_fragment_accepts_legacy_array_literal_source() { - let program = - parse_fragment(br#"return array(1, "name" => "Ada",)[1];"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::ArrayGet { - array: Box::new(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), - EvalArrayElement::KeyValue { - key: EvalExpr::Const(EvalConst::String("name".to_string())), - value: EvalExpr::Const(EvalConst::String("Ada".to_string())), - }, - ])), - index: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }))] - ); -} -/// Verifies associative array literals preserve explicit key/value expressions. -#[test] -fn parse_fragment_accepts_assoc_array_literal_source() { - let program = parse_fragment(br#"return ["name" => "Ada"];"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::KeyValue { - key: EvalExpr::Const(EvalConst::String("name".to_string())), - value: EvalExpr::Const(EvalConst::String("Ada".to_string())), - } - ])))] - ); -} - -/// Verifies array literals preserve by-reference element syntax. -#[test] -fn parse_fragment_accepts_array_reference_elements_source() { - let program = parse_fragment(br#"return [&$value, "named" => &$other];"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::Reference(EvalExpr::LoadVar("value".to_string())), - EvalArrayElement::KeyReference { - key: EvalExpr::Const(EvalConst::String("named".to_string())), - value: EvalExpr::LoadVar("other".to_string()), - }, - ])))] - ); -} - -/// Verifies indexed array writes parse as variable-target array set statements. -#[test] -fn parse_fragment_accepts_indexed_array_write_source() { - let program = parse_fragment(br#"$items[1] = "x";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ArraySetVar { - name: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(1)), - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }] - ); -} -/// Verifies indexed array append syntax parses as a variable-target append statement. -#[test] -fn parse_fragment_accepts_indexed_array_append_source() { - let program = parse_fragment(br#"$items[] = "x";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ArrayAppendVar { - name: "items".to_string(), - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }] - ); -} -/// Verifies array append syntax is accepted inside `for` update clauses. -#[test] -fn parse_fragment_accepts_array_append_in_for_update_source() { - let program = parse_fragment(br#"for ($i = 0; $i < 2; $items[] = $i) { $i += 1; }"#) - .expect("fragment should parse"); - let [EvalStmt::For { update, .. }] = program.statements() else { - panic!("expected for statement"); - }; - assert_eq!( - update, - &vec![EvalStmt::ArrayAppendVar { - name: "items".to_string(), - value: EvalExpr::LoadVar("i".to_string()), - }] - ); -} -/// Verifies object property reads parse as postfix EvalIR expressions. -#[test] -fn parse_fragment_accepts_property_read_source() { - let program = parse_fragment(br#"return $this->x;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }))] - ); -} -/// Verifies property names preserve source case while keywords remain case-insensitive. -#[test] -fn parse_fragment_preserves_property_case_source() { - let program = parse_fragment(br#"RETURN $this->camelName;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "camelName".to_string(), - }))] - ); -} -/// Verifies object method calls parse as postfix EvalIR call expressions. -#[test] -fn parse_fragment_accepts_method_call_source() { - let program = parse_fragment(br#"return $this->Answer();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "Answer".to_string(), - args: Vec::new(), - }))] - ); -} -/// Verifies first-class object method syntax keeps callable-capture semantics in EvalIR. -#[test] -fn parse_fragment_accepts_first_class_method_callable_source() { - let program = parse_fragment(br#"return $this->Answer(...);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCallable { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: Box::new(EvalExpr::Const(EvalConst::String("Answer".to_string()))), - }))] - ); -} -/// Verifies braced dynamic object property reads parse as runtime-name EvalIR expressions. -#[test] -fn parse_fragment_accepts_dynamic_property_read_source() { - let program = parse_fragment(br#"return $this->{$name};"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicPropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: Box::new(EvalExpr::LoadVar("name".to_string())), - }))] - ); -} -/// Verifies variable-name dynamic object method calls parse as runtime-name EvalIR calls. -#[test] -fn parse_fragment_accepts_dynamic_method_call_source() { - let program = parse_fragment(br#"return $this->$method();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicMethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: Box::new(EvalExpr::LoadVar("method".to_string())), - args: Vec::new(), - }))] - ); -} -/// Verifies nullsafe object property reads parse as dedicated postfix EvalIR expressions. -#[test] -fn parse_fragment_accepts_nullsafe_property_read_source() { - let program = parse_fragment(br#"return $this?->x;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NullsafePropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }))] - ); -} -/// Verifies nullsafe object method calls parse as dedicated postfix EvalIR call expressions. -#[test] -fn parse_fragment_accepts_nullsafe_method_call_source() { - let program = parse_fragment(br#"return $this?->Answer();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NullsafeMethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "Answer".to_string(), - args: Vec::new(), - }))] - ); -} -/// Verifies nullsafe braced dynamic property reads parse as runtime-name EvalIR expressions. -#[test] -fn parse_fragment_accepts_nullsafe_dynamic_property_read_source() { - let program = parse_fragment(br#"return $this?->{$name};"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NullsafeDynamicPropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: Box::new(EvalExpr::LoadVar("name".to_string())), - }))] - ); -} -/// Verifies nullsafe dynamic method calls parse as runtime-name EvalIR call expressions. -#[test] -fn parse_fragment_accepts_nullsafe_dynamic_method_call_source() { - let program = parse_fragment(br#"return $this?->$method();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NullsafeDynamicMethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: Box::new(EvalExpr::LoadVar("method".to_string())), - args: Vec::new(), - }))] - ); -} -/// Verifies object construction parses as a named EvalIR expression. -#[test] -fn parse_fragment_accepts_new_object_source() { - let program = parse_fragment(br#"return new Box();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "Box".to_string(), - args: Vec::new(), - }))] - ); -} -/// Verifies object construction accepts a runtime class-name variable. -#[test] -fn parse_fragment_accepts_dynamic_new_object_source() { - let program = - parse_fragment(br#"return new $className("Ada");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { - class_name: Box::new(EvalExpr::LoadVar("className".to_string())), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "Ada".to_string() - )))], - }))] - ); -} -/// Verifies object construction accepts a parenthesized runtime class-name expression. -#[test] -fn parse_fragment_accepts_expression_new_object_source() { - let program = - parse_fragment(br#"return new ($factory->className)("Ada");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { - class_name: Box::new(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("factory".to_string())), - property: "className".to_string(), - }), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "Ada".to_string() - )))], - }))] - ); -} -/// Verifies PHP constructor parentheses are optional for named and runtime class targets. -#[test] -fn parse_fragment_accepts_new_object_without_constructor_parentheses_source() { - let named = parse_fragment(br#"return new Box;"#).expect("fragment should parse"); - assert_eq!( - named.statements(), - &[EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "Box".to_string(), - args: Vec::new(), - }))] - ); - - let dynamic = parse_fragment(br#"return new $className;"#).expect("fragment should parse"); - assert_eq!( - dynamic.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { - class_name: Box::new(EvalExpr::LoadVar("className".to_string())), - args: Vec::new(), - }))] - ); -} -/// Verifies object construction accepts explicitly qualified class names. -#[test] -fn parse_fragment_accepts_qualified_new_object_source() { - let program = parse_fragment(br#"return new \EvalNs\Box();"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::NewObject { - class_name: "EvalNs\\Box".to_string(), - args: Vec::new(), - }))] - ); -} - -/// Verifies clone expressions parse as unary object expressions. -#[test] -fn parse_fragment_accepts_clone_expression_source() { - let program = parse_fragment(br#"return clone $box;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Clone(Box::new( - EvalExpr::LoadVar("box".to_string()) - ))))] - ); -} - -/// Verifies anonymous class expressions parse as executable eval class metadata. -#[test] -fn parse_fragment_accepts_anonymous_class_source() { - let program = parse_fragment( - br#"return new readonly class("Ada") extends BaseBox implements Labelled { - public string $name; - public function label() { return $this->name; } -};"#, - ) - .expect("fragment should parse"); - let [EvalStmt::Return(Some(EvalExpr::NewAnonymousClass { class, args }))] = - program.statements() - else { - panic!("expected anonymous class return"); - }; - - assert!(class.name().starts_with("class@anonymous#eval")); - assert!(class.is_anonymous()); - assert!(class.is_readonly_class()); - assert_eq!(class.parent(), Some("BaseBox")); - assert_eq!(class.interfaces(), &["Labelled".to_string()]); - assert_eq!(class.properties().len(), 1); - assert_eq!(class.methods().len(), 1); - assert_eq!( - args, - &[EvalCallArg::positional(EvalExpr::Const(EvalConst::String( - "Ada".to_string(), - )))] - ); -} - -/// Verifies object method calls preserve source-order argument expressions. -#[test] -fn parse_fragment_accepts_method_call_args_source() { - let program = parse_fragment(br#"return $this->add($x + 1);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "add".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("x".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - })], - }))] - ); -} -/// Verifies object method calls parse multiple argument expressions in source order. -#[test] -fn parse_fragment_accepts_method_call_multiple_args_source() { - let program = - parse_fragment(br#"return $this->label($x, "ok");"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "label".to_string(), - args: vec![ - EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), - EvalCallArg::positional(EvalExpr::Const(EvalConst::String("ok".to_string()))), - ], - }))] - ); -} - -/// Verifies object method calls preserve named arguments in source order. -#[test] -fn parse_fragment_accepts_named_method_call_args_source() { - let program = parse_fragment(br#"return $this->label(right: "ok", left: $x);"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::MethodCall { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - method: "label".to_string(), - args: vec![ - EvalCallArg::named( - "right", - EvalExpr::Const(EvalConst::String("ok".to_string())), - ), - EvalCallArg::named("left", EvalExpr::LoadVar("x".to_string())), - ], - }))] - ); -} - -/// Verifies object property writes parse as dedicated EvalIR statements. -#[test] -fn parse_fragment_accepts_property_write_source() { - let program = parse_fragment(br#"$this->x = $this->x + 1;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: "x".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))), - }, - }] - ); -} - -/// Verifies object property reference bindings parse as dedicated EvalIR statements. -#[test] -fn parse_fragment_accepts_property_reference_bind_source() { - let program = - parse_fragment(br#"$this->x =& $source; $this->{$name} =& $other;"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::PropertyReferenceBind { - object: EvalExpr::LoadVar("this".to_string()), - property: "x".to_string(), - source: "source".to_string(), - }, - EvalStmt::DynamicPropertyReferenceBind { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - source: "other".to_string(), - }, - ] - ); -} - -/// Verifies object property array writes and appends parse as dedicated EvalIR statements. -#[test] -fn parse_fragment_accepts_property_array_write_source() { - let program = parse_fragment(br#"$this->items[0] = "x"; $this->items[] = "y";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::PropertyArraySet { - object: EvalExpr::LoadVar("this".to_string()), - property: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(0)), - op: None, - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }, - EvalStmt::PropertyArrayAppend { - object: EvalExpr::LoadVar("this".to_string()), - property: "items".to_string(), - value: EvalExpr::Const(EvalConst::String("y".to_string())), - }, - ] - ); -} - -/// Verifies property array compound assignment retains the indexed property target. -#[test] -fn parse_fragment_accepts_property_array_compound_assignment_source() { - let program = parse_fragment(br#"$this->items[0] += 2;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::PropertyArraySet { - object: EvalExpr::LoadVar("this".to_string()), - property: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(0)), - op: Some(EvalBinOp::Add), - value: EvalExpr::Const(EvalConst::Int(2)), - }] - ); -} - -/// Verifies object property increment/decrement parses as dedicated member updates. -#[test] -fn parse_fragment_accepts_property_inc_dec_source() { - let program = parse_fragment(br#"$this->x++; --$this->x;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::PropertyIncDec { - object: EvalExpr::LoadVar("this".to_string()), - property: "x".to_string(), - increment: true, - }, - EvalStmt::PropertyIncDec { - object: EvalExpr::LoadVar("this".to_string()), - property: "x".to_string(), - increment: false, - }, - ] - ); -} - -/// Verifies dynamic object property writes parse as runtime-name EvalIR statements. -#[test] -fn parse_fragment_accepts_dynamic_property_write_source() { - let program = parse_fragment(br#"$this->{$name} = 7;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::DynamicPropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - value: EvalExpr::Const(EvalConst::Int(7)), - }] - ); -} - -/// Verifies dynamic property array writes keep the runtime property expression. -#[test] -fn parse_fragment_accepts_dynamic_property_array_write_source() { - let program = parse_fragment(br#"$this->{$name}[0] = "x"; $this->{$name}[] = "y";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::DynamicPropertyArraySet { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - index: EvalExpr::Const(EvalConst::Int(0)), - op: None, - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }, - EvalStmt::DynamicPropertyArrayAppend { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - value: EvalExpr::Const(EvalConst::String("y".to_string())), - }, - ] - ); -} - -/// Verifies object property compound assignment parses as a dedicated member update. -#[test] -fn parse_fragment_accepts_property_compound_assignment_source() { - let program = parse_fragment(br#"$this->x += 2; $this->label .= "ok";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::PropertyCompoundAssign { - object: EvalExpr::LoadVar("this".to_string()), - property: "x".to_string(), - op: EvalBinOp::Add, - value: EvalExpr::Const(EvalConst::Int(2)), - }, - EvalStmt::PropertyCompoundAssign { - object: EvalExpr::LoadVar("this".to_string()), - property: "label".to_string(), - op: EvalBinOp::Concat, - value: EvalExpr::Const(EvalConst::String("ok".to_string())), - }, - ] - ); -} - -/// Verifies dynamic object property compound assignment keeps the runtime property expression. -#[test] -fn parse_fragment_accepts_dynamic_property_compound_assignment_source() { - let program = - parse_fragment(br#"$this->{$name} += 2; $this->{$label} .= "ok";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::DynamicPropertyCompoundAssign { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - op: EvalBinOp::Add, - value: EvalExpr::Const(EvalConst::Int(2)), - }, - EvalStmt::DynamicPropertyCompoundAssign { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("label".to_string()), - op: EvalBinOp::Concat, - value: EvalExpr::Const(EvalConst::String("ok".to_string())), - }, - ] - ); -} - -/// Verifies dynamic object property increment/decrement keeps the runtime property expression. -#[test] -fn parse_fragment_accepts_dynamic_property_inc_dec_source() { - let program = - parse_fragment(br#"$this->{$name}++; --$this->{$name};"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::DynamicPropertyIncDec { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - increment: true, - }, - EvalStmt::DynamicPropertyIncDec { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - increment: false, - }, - ] - ); -} - -/// Verifies dynamic object property unsets parse as runtime-name EvalIR statements. -#[test] -fn parse_fragment_accepts_dynamic_property_unset_source() { - let program = parse_fragment(br#"unset($this->{$name});"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::UnsetDynamicProperty { - object: EvalExpr::LoadVar("this".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - }] - ); -} - -/// Verifies unsetting object-property array elements keeps the property expression target. -#[test] -fn parse_fragment_accepts_property_array_unset_source() { - let program = parse_fragment(br#"unset($this->items[0], $this->{$name}[1]);"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::UnsetArrayElement { - array: EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "items".to_string(), - }, - index: EvalExpr::Const(EvalConst::Int(0)), - }, - EvalStmt::UnsetArrayElement { - array: EvalExpr::DynamicPropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: Box::new(EvalExpr::LoadVar("name".to_string())), - }, - index: EvalExpr::Const(EvalConst::Int(1)), - }, - ] - ); -} diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects/arrays_access.rs b/crates/elephc-magician/src/parser/tests/arrays_objects/arrays_access.rs new file mode 100644 index 0000000000..6936dbddeb --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/arrays_objects/arrays_access.rs @@ -0,0 +1,243 @@ +//! Purpose: +//! Parser tests for array literals/access and object property, method, dynamic, +//! and nullsafe access expressions. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover postfix and aggregate expression syntax. + +use super::super::support::*; + +/// Verifies indexed array literals and reads parse as runtime array expressions. +#[test] +fn parse_fragment_accepts_indexed_array_read_source() { + let program = parse_fragment(br#"return [1, 2][0];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(2))), + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(0))), + }))] + ); +} +/// Verifies legacy `array(...)` literals parse through the same EvalIR array node. +#[test] +fn parse_fragment_accepts_legacy_array_literal_source() { + let program = + parse_fragment(br#"return array(1, "name" => "Ada",)[1];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::ArrayGet { + array: Box::new(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::Const(EvalConst::Int(1))), + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + }, + ])), + index: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }))] + ); +} +/// Verifies associative array literals preserve explicit key/value expressions. +#[test] +fn parse_fragment_accepts_assoc_array_literal_source() { + let program = parse_fragment(br#"return ["name" => "Ada"];"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::KeyValue { + key: EvalExpr::Const(EvalConst::String("name".to_string())), + value: EvalExpr::Const(EvalConst::String("Ada".to_string())), + } + ])))] + ); +} + +/// Verifies array literals preserve by-reference element syntax. +#[test] +fn parse_fragment_accepts_array_reference_elements_source() { + let program = parse_fragment(br#"return [&$value, "named" => &$other];"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Reference(EvalExpr::LoadVar("value".to_string())), + EvalArrayElement::KeyReference { + key: EvalExpr::Const(EvalConst::String("named".to_string())), + value: EvalExpr::LoadVar("other".to_string()), + }, + ])))] + ); +} + +/// Verifies indexed array writes parse as variable-target array set statements. +#[test] +fn parse_fragment_accepts_indexed_array_write_source() { + let program = parse_fragment(br#"$items[1] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArraySetVar { + name: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(1)), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} +/// Verifies indexed array append syntax parses as a variable-target append statement. +#[test] +fn parse_fragment_accepts_indexed_array_append_source() { + let program = parse_fragment(br#"$items[] = "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} +/// Verifies array append syntax is accepted inside `for` update clauses. +#[test] +fn parse_fragment_accepts_array_append_in_for_update_source() { + let program = parse_fragment(br#"for ($i = 0; $i < 2; $items[] = $i) { $i += 1; }"#) + .expect("fragment should parse"); + let [EvalStmt::For { update, .. }] = program.statements() else { + panic!("expected for statement"); + }; + assert_eq!( + update, + &vec![EvalStmt::ArrayAppendVar { + name: "items".to_string(), + value: EvalExpr::LoadVar("i".to_string()), + }] + ); +} +/// Verifies object property reads parse as postfix EvalIR expressions. +#[test] +fn parse_fragment_accepts_property_read_source() { + let program = parse_fragment(br#"return $this->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + ); +} +/// Verifies property names preserve source case while keywords remain case-insensitive. +#[test] +fn parse_fragment_preserves_property_case_source() { + let program = parse_fragment(br#"RETURN $this->camelName;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "camelName".to_string(), + }))] + ); +} +/// Verifies object method calls parse as postfix EvalIR call expressions. +#[test] +fn parse_fragment_accepts_method_call_source() { + let program = parse_fragment(br#"return $this->Answer();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "Answer".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies first-class object method syntax keeps callable-capture semantics in EvalIR. +#[test] +fn parse_fragment_accepts_first_class_method_callable_source() { + let program = parse_fragment(br#"return $this->Answer(...);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCallable { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Answer".to_string()))), + }))] + ); +} +/// Verifies braced dynamic object property reads parse as runtime-name EvalIR expressions. +#[test] +fn parse_fragment_accepts_dynamic_property_read_source() { + let program = parse_fragment(br#"return $this->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }))] + ); +} +/// Verifies variable-name dynamic object method calls parse as runtime-name EvalIR calls. +#[test] +fn parse_fragment_accepts_dynamic_method_call_source() { + let program = parse_fragment(br#"return $this->$method();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: Vec::new(), + }))] + ); +} +/// Verifies nullsafe object property reads parse as dedicated postfix EvalIR expressions. +#[test] +fn parse_fragment_accepts_nullsafe_property_read_source() { + let program = parse_fragment(br#"return $this?->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafePropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + ); +} +/// Verifies nullsafe object method calls parse as dedicated postfix EvalIR call expressions. +#[test] +fn parse_fragment_accepts_nullsafe_method_call_source() { + let program = parse_fragment(br#"return $this?->Answer();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "Answer".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies nullsafe braced dynamic property reads parse as runtime-name EvalIR expressions. +#[test] +fn parse_fragment_accepts_nullsafe_dynamic_property_read_source() { + let program = parse_fragment(br#"return $this?->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeDynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }))] + ); +} +/// Verifies nullsafe dynamic method calls parse as runtime-name EvalIR call expressions. +#[test] +fn parse_fragment_accepts_nullsafe_dynamic_method_call_source() { + let program = parse_fragment(br#"return $this?->$method();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NullsafeDynamicMethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: Vec::new(), + }))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects/construction_calls.rs b/crates/elephc-magician/src/parser/tests/arrays_objects/construction_calls.rs new file mode 100644 index 0000000000..0abc59751a --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/arrays_objects/construction_calls.rs @@ -0,0 +1,189 @@ +//! Purpose: +//! Parser tests for named/dynamic/anonymous object construction, clone, and +//! method call argument forms. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Parenthesis-free construction and named call arguments are retained. + +use super::super::support::*; + +/// Verifies object construction parses as a named EvalIR expression. +#[test] +fn parse_fragment_accepts_new_object_source() { + let program = parse_fragment(br#"return new Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Box".to_string(), + args: Vec::new(), + }))] + ); +} +/// Verifies object construction accepts a runtime class-name variable. +#[test] +fn parse_fragment_accepts_dynamic_new_object_source() { + let program = + parse_fragment(br#"return new $className("Ada");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::LoadVar("className".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string() + )))], + }))] + ); +} +/// Verifies object construction accepts a parenthesized runtime class-name expression. +#[test] +fn parse_fragment_accepts_expression_new_object_source() { + let program = + parse_fragment(br#"return new ($factory->className)("Ada");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("factory".to_string())), + property: "className".to_string(), + }), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string() + )))], + }))] + ); +} +/// Verifies PHP constructor parentheses are optional for named and runtime class targets. +#[test] +fn parse_fragment_accepts_new_object_without_constructor_parentheses_source() { + let named = parse_fragment(br#"return new Box;"#).expect("fragment should parse"); + assert_eq!( + named.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "Box".to_string(), + args: Vec::new(), + }))] + ); + + let dynamic = parse_fragment(br#"return new $className;"#).expect("fragment should parse"); + assert_eq!( + dynamic.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicNewObject { + class_name: Box::new(EvalExpr::LoadVar("className".to_string())), + args: Vec::new(), + }))] + ); +} +/// Verifies object construction accepts explicitly qualified class names. +#[test] +fn parse_fragment_accepts_qualified_new_object_source() { + let program = parse_fragment(br#"return new \EvalNs\Box();"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::NewObject { + class_name: "EvalNs\\Box".to_string(), + args: Vec::new(), + }))] + ); +} + +/// Verifies clone expressions parse as unary object expressions. +#[test] +fn parse_fragment_accepts_clone_expression_source() { + let program = parse_fragment(br#"return clone $box;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Clone(Box::new( + EvalExpr::LoadVar("box".to_string()) + ))))] + ); +} + +/// Verifies anonymous class expressions parse as executable eval class metadata. +#[test] +fn parse_fragment_accepts_anonymous_class_source() { + let program = parse_fragment( + br#"return new readonly class("Ada") extends BaseBox implements Labelled { + public string $name; + public function label() { return $this->name; } +};"#, + ) + .expect("fragment should parse"); + let [EvalStmt::Return(Some(EvalExpr::NewAnonymousClass { class, args }))] = + program.statements() + else { + panic!("expected anonymous class return"); + }; + + assert!(class.name().starts_with("class@anonymous#eval")); + assert!(class.is_anonymous()); + assert!(class.is_readonly_class()); + assert_eq!(class.parent(), Some("BaseBox")); + assert_eq!(class.interfaces(), &["Labelled".to_string()]); + assert_eq!(class.properties().len(), 1); + assert_eq!(class.methods().len(), 1); + assert_eq!( + args, + &[EvalCallArg::positional(EvalExpr::Const(EvalConst::String( + "Ada".to_string(), + )))] + ); +} + +/// Verifies object method calls preserve source-order argument expressions. +#[test] +fn parse_fragment_accepts_method_call_args_source() { + let program = parse_fragment(br#"return $this->add($x + 1);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "add".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("x".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + })], + }))] + ); +} +/// Verifies object method calls parse multiple argument expressions in source order. +#[test] +fn parse_fragment_accepts_method_call_multiple_args_source() { + let program = + parse_fragment(br#"return $this->label($x, "ok");"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "label".to_string(), + args: vec![ + EvalCallArg::positional(EvalExpr::LoadVar("x".to_string())), + EvalCallArg::positional(EvalExpr::Const(EvalConst::String("ok".to_string()))), + ], + }))] + ); +} + +/// Verifies object method calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_method_call_args_source() { + let program = parse_fragment(br#"return $this->label(right: "ok", left: $x);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::MethodCall { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + method: "label".to_string(), + args: vec![ + EvalCallArg::named( + "right", + EvalExpr::Const(EvalConst::String("ok".to_string())), + ), + EvalCallArg::named("left", EvalExpr::LoadVar("x".to_string())), + ], + }))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects/mod.rs b/crates/elephc-magician/src/parser/tests/arrays_objects/mod.rs new file mode 100644 index 0000000000..3058447988 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/arrays_objects/mod.rs @@ -0,0 +1,13 @@ +//! Purpose: +//! Organizes parser tests for arrays, object access/construction, calls, and +//! property mutations. +//! +//! Called from: +//! - `crate::parser::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod arrays_access; +mod construction_calls; +mod property_mutations; diff --git a/crates/elephc-magician/src/parser/tests/arrays_objects/property_mutations.rs b/crates/elephc-magician/src/parser/tests/arrays_objects/property_mutations.rs new file mode 100644 index 0000000000..f05f2e27e5 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/arrays_objects/property_mutations.rs @@ -0,0 +1,264 @@ +//! Purpose: +//! Parser tests for object property assignment, references, array writes, +//! compound operations, increment/decrement, and unset. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Named and dynamic property mutations retain distinct EvalIR statements. + +use super::super::support::*; + +/// Verifies object property writes parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_write_source() { + let program = parse_fragment(br#"$this->x = $this->x + 1;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))), + }, + }] + ); +} + +/// Verifies object property reference bindings parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_reference_bind_source() { + let program = + parse_fragment(br#"$this->x =& $source; $this->{$name} =& $other;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + source: "source".to_string(), + }, + EvalStmt::DynamicPropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + source: "other".to_string(), + }, + ] + ); +} + +/// Verifies object property array writes and appends parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_property_array_write_source() { + let program = parse_fragment(br#"$this->items[0] = "x"; $this->items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::PropertyArrayAppend { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + +/// Verifies property array compound assignment retains the indexed property target. +#[test] +fn parse_fragment_accepts_property_array_compound_assignment_source() { + let program = parse_fragment(br#"$this->items[0] += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::PropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: Some(EvalBinOp::Add), + value: EvalExpr::Const(EvalConst::Int(2)), + }] + ); +} + +/// Verifies object property increment/decrement parses as dedicated member updates. +#[test] +fn parse_fragment_accepts_property_inc_dec_source() { + let program = parse_fragment(br#"$this->x++; --$this->x;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + increment: true, + }, + EvalStmt::PropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + increment: false, + }, + ] + ); +} + +/// Verifies dynamic object property writes parse as runtime-name EvalIR statements. +#[test] +fn parse_fragment_accepts_dynamic_property_write_source() { + let program = parse_fragment(br#"$this->{$name} = 7;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicPropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + value: EvalExpr::Const(EvalConst::Int(7)), + }] + ); +} + +/// Verifies dynamic property array writes keep the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_array_write_source() { + let program = parse_fragment(br#"$this->{$name}[0] = "x"; $this->{$name}[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyArraySet { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::DynamicPropertyArrayAppend { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + +/// Verifies object property compound assignment parses as a dedicated member update. +#[test] +fn parse_fragment_accepts_property_compound_assignment_source() { + let program = parse_fragment(br#"$this->x += 2; $this->label .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::PropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: "x".to_string(), + op: EvalBinOp::Add, + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::PropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: "label".to_string(), + op: EvalBinOp::Concat, + value: EvalExpr::Const(EvalConst::String("ok".to_string())), + }, + ] + ); +} + +/// Verifies dynamic object property compound assignment keeps the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_compound_assignment_source() { + let program = + parse_fragment(br#"$this->{$name} += 2; $this->{$label} .= "ok";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + op: EvalBinOp::Add, + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicPropertyCompoundAssign { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("label".to_string()), + op: EvalBinOp::Concat, + value: EvalExpr::Const(EvalConst::String("ok".to_string())), + }, + ] + ); +} + +/// Verifies dynamic object property increment/decrement keeps the runtime property expression. +#[test] +fn parse_fragment_accepts_dynamic_property_inc_dec_source() { + let program = + parse_fragment(br#"$this->{$name}++; --$this->{$name};"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicPropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + increment: true, + }, + EvalStmt::DynamicPropertyIncDec { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + increment: false, + }, + ] + ); +} + +/// Verifies dynamic object property unsets parse as runtime-name EvalIR statements. +#[test] +fn parse_fragment_accepts_dynamic_property_unset_source() { + let program = parse_fragment(br#"unset($this->{$name});"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::UnsetDynamicProperty { + object: EvalExpr::LoadVar("this".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + }] + ); +} + +/// Verifies unsetting object-property array elements keeps the property expression target. +#[test] +fn parse_fragment_accepts_property_array_unset_source() { + let program = parse_fragment(br#"unset($this->items[0], $this->{$name}[1]);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetArrayElement { + array: EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(0)), + }, + EvalStmt::UnsetArrayElement { + array: EvalExpr::DynamicPropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: Box::new(EvalExpr::LoadVar("name".to_string())), + }, + index: EvalExpr::Const(EvalConst::Int(1)), + }, + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors.rs deleted file mode 100644 index 43f3ddf43b..0000000000 --- a/crates/elephc-magician/src/parser/tests/classes_errors.rs +++ /dev/null @@ -1,1613 +0,0 @@ -//! Purpose: -//! Parser tests for class declarations and parser diagnostics. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - These cases cover dynamic class metadata and malformed fragment errors. - -use super::support::*; - -/// Verifies empty class declarations lower to dynamic class-registration statements. -#[test] -fn parse_fragment_accepts_empty_class_declaration_source() { - let program = parse_fragment(b"class DynEvalClass {};").expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalClass", - Vec::new(), - Vec::new() - ))] - ); -} -/// Verifies class relation clauses lower into dynamic class metadata. -#[test] -fn parse_fragment_accepts_class_extends_and_implements_source() { - let program = parse_fragment( - br#"class DynEvalChild extends DynEvalBase implements DynEvalIface, \Root\Iface {}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::with_relations( - "DynEvalChild", - Some("DynEvalBase".to_string()), - vec!["DynEvalIface".to_string(), "Root\\Iface".to_string()], - Vec::new(), - Vec::new(), - ))] - ); -} - -/// Verifies function, interface, and class method return types are retained. -#[test] -fn parse_fragment_accepts_return_type_metadata() { - let program = parse_fragment( - br#"function DynEvalReturn(): ?int { return 1; } -interface DynEvalReturnIface { - public function read(): string; -} -class DynEvalReturnClass { - public function selfReturn(): static { return $this; } - public function done(): void {} -}"#, - ) - .expect("fragment should parse"); - let statements = program.statements(); - let EvalStmt::FunctionDecl { - return_type: Some(function_return_type), - .. - } = &statements[0] - else { - panic!("expected function declaration with return type"); - }; - assert_eq!( - function_return_type.variants(), - &[EvalParameterTypeVariant::Int] - ); - assert!(function_return_type.allows_null()); - - let EvalStmt::InterfaceDecl(interface) = &statements[1] else { - panic!("expected interface declaration"); - }; - let interface_return_type = interface.methods()[0] - .return_type() - .expect("interface method return type"); - assert_eq!( - interface_return_type.variants(), - &[EvalParameterTypeVariant::String] - ); - - let EvalStmt::ClassDecl(class) = &statements[2] else { - panic!("expected class declaration"); - }; - let self_return_type = class.methods()[0] - .return_type() - .expect("class method return type"); - assert_eq!( - self_return_type.variants(), - &[EvalParameterTypeVariant::Class("static".to_string())] - ); - let void_return_type = class.methods()[1] - .return_type() - .expect("void method return type"); - assert_eq!( - void_return_type.variants(), - &[EvalParameterTypeVariant::Void] - ); -} - -/// Verifies type atoms are rejected in positions where PHP forbids them. -#[test] -fn parse_fragment_rejects_invalid_type_atom_forms() { - for source in [ - b"function DynEvalBadVoid(): ?void {}" as &[u8], - b"function DynEvalBadVoidUnion(): void|null {}", - b"function DynEvalBadNeverUnion(): never|int {}", - b"function DynEvalBadNeverIntersection(): never&Countable {}", - b"function DynEvalBadVoidParam(void $value) {}", - b"function DynEvalBadSelfParam(self $value) {}", - b"function DynEvalBadParentParam(parent $value) {}", - b"function DynEvalBadStaticParam(static $value) {}", - b"function DynEvalBadSelfReturn(): self {}", - b"function DynEvalBadParentReturn(): parent {}", - b"function DynEvalBadStaticReturn(): static {}", - b"class DynEvalBadStaticMethodParam { public function read(static $value) {} }", - b"class DynEvalBadCallableProperty { public callable $value; }", - b"class DynEvalBadStaticProperty { public static static $value; }", - b"interface DynEvalBadCallableInterfaceProperty { public callable $value { get; } }", - b"class DynEvalBadCallablePromoted { public function __construct(public callable $value) {} }", - b"class DynEvalBadStaticPromoted { public function __construct(public static $value) {} }", - ] { - assert_eq!( - parse_fragment(source), - Err(EvalParseError::UnsupportedConstruct) - ); - } -} - -/// Verifies eval rejects PHP's reserved `class` class-constant name. -#[test] -fn parse_fragment_rejects_reserved_class_constant_name() { - for source in [ - b"class DynEvalBadConstName { const class = 1; }" as &[u8], - b"interface DynEvalBadIfaceConstName { const class = 1; }", - b"trait DynEvalBadTraitConstName { const class = 1; }", - b"enum DynEvalBadEnumConstName { const class = 1; }", - ] { - assert_eq!( - parse_fragment(source), - Err(EvalParseError::UnsupportedConstruct) - ); - } -} - -/// Verifies eval rejects PHP-reserved class-like declaration names. -#[test] -fn parse_fragment_rejects_reserved_class_like_declaration_names() { - for source in [ - b"class match {}" as &[u8], - b"class string {}", - b"class CLASS {}", - b"interface interface {}", - b"trait readonly {}", - b"enum bool { case Ready; }", - ] { - assert_eq!( - parse_fragment(source), - Err(EvalParseError::UnsupportedConstruct) - ); - } -} - -/// Verifies eval accepts PHP semi-reserved class-like declaration names. -#[test] -fn parse_fragment_accepts_semi_reserved_class_like_declaration_names() { - let program = parse_fragment( - br#"class enum {} -interface from {} -trait resource {} -enum integer { case Ready; }"#, - ) - .expect("fragment should parse"); - let statements = program.statements(); - let EvalStmt::ClassDecl(class) = &statements[0] else { - panic!("expected class declaration"); - }; - assert_eq!(class.name(), "enum"); - - let EvalStmt::InterfaceDecl(interface) = &statements[1] else { - panic!("expected interface declaration"); - }; - assert_eq!(interface.name(), "from"); - - let EvalStmt::TraitDecl(trait_decl) = &statements[2] else { - panic!("expected trait declaration"); - }; - assert_eq!(trait_decl.name(), "resource"); - - let EvalStmt::EnumDecl(enum_decl) = &statements[3] else { - panic!("expected enum declaration"); - }; - assert_eq!(enum_decl.name(), "integer"); -} - -/// Verifies eval rejects PHP-reserved bare class-like reference names. -#[test] -fn parse_fragment_rejects_reserved_unqualified_class_like_reference_names() { - for source in [ - b"class DynEvalBadExtends extends match {}" as &[u8], - b"class DynEvalBadImplements implements match {}", - b"interface DynEvalBadIfaceExtends extends match {}", - b"class DynEvalBadTraitUse { use match; }", - b"class DynEvalBadTraitAdapt { use SomeTrait { match::run insteadof SomeTrait; } }", - b"$box = new match();", - b"$ok = $box instanceof match;", - b"try {} catch (match $e) {}", - ] { - assert_eq!( - parse_fragment(source), - Err(EvalParseError::UnsupportedConstruct) - ); - } -} - -/// Verifies eval accepts semi-reserved or qualified class-like reference names PHP parses. -#[test] -fn parse_fragment_accepts_semi_reserved_and_qualified_class_like_reference_names() { - parse_fragment( - br#"class enum {} -class DynEvalExtendsSemiReserved extends enum {} -class DynEvalExtendsQualifiedReserved extends \match {} -class DynEvalNewSelf { - public function make() { return new self(); } -} -$ok = $box instanceof \match;"#, - ) - .expect("fragment should parse"); -} - -/// Verifies comma-separated class-like constants lower to individual eval constants. -#[test] -fn parse_fragment_accepts_comma_separated_class_like_constants() { - let program = parse_fragment( - br#"class DynEvalMultiConstClass { - public const A = 1, B = 2; -} -interface DynEvalMultiConstIface { - final public const C = 3, D = 4; -} -trait DynEvalMultiConstTrait { - protected const E = 5, F = 6; -} -enum DynEvalMultiConstEnum { - public const G = 7, H = 8; - case Ready; -}"#, - ) - .expect("fragment should parse"); - let statements = program.statements(); - let EvalStmt::ClassDecl(class) = &statements[0] else { - panic!("expected class declaration"); - }; - assert_eq!( - class.constants(), - &[ - EvalClassConstant::new("A", EvalExpr::Const(EvalConst::Int(1))), - EvalClassConstant::new("B", EvalExpr::Const(EvalConst::Int(2))), - ], - ); - let EvalStmt::InterfaceDecl(interface) = &statements[1] else { - panic!("expected interface declaration"); - }; - assert_eq!( - interface.constants(), - &[ - EvalClassConstant::with_visibility_and_final( - "C", - EvalVisibility::Public, - true, - EvalExpr::Const(EvalConst::Int(3)), - ), - EvalClassConstant::with_visibility_and_final( - "D", - EvalVisibility::Public, - true, - EvalExpr::Const(EvalConst::Int(4)), - ), - ], - ); - let EvalStmt::TraitDecl(trait_decl) = &statements[2] else { - panic!("expected trait declaration"); - }; - assert_eq!( - trait_decl.constants(), - &[ - EvalClassConstant::with_visibility( - "E", - EvalVisibility::Protected, - EvalExpr::Const(EvalConst::Int(5)), - ), - EvalClassConstant::with_visibility( - "F", - EvalVisibility::Protected, - EvalExpr::Const(EvalConst::Int(6)), - ), - ], - ); - let EvalStmt::EnumDecl(enum_decl) = &statements[3] else { - panic!("expected enum declaration"); - }; - assert_eq!( - enum_decl.constants(), - &[ - EvalClassConstant::new("G", EvalExpr::Const(EvalConst::Int(7))), - EvalClassConstant::new("H", EvalExpr::Const(EvalConst::Int(8))), - ], - ); -} - -/// Verifies class attributes lower to eval class metadata with supported literal args. -#[test] -fn parse_fragment_accepts_class_attribute_metadata() { - let program = parse_fragment( - br#"#[Route("/home", -1, 1.5, -2.5, true, null, EvalAttrDep::class, ["nested", "key" => 2])] -#[Tag(name: "named")] -class DynEvalAttributed {}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl( - EvalClass::new("DynEvalAttributed", Vec::new(), Vec::new()).with_attributes(vec![ - EvalAttribute::new( - "Route", - Some(vec![ - EvalAttributeArg::String("/home".to_string()), - EvalAttributeArg::Int(-1), - EvalAttributeArg::Float(1.5f64.to_bits()), - EvalAttributeArg::Float((-2.5f64).to_bits()), - EvalAttributeArg::Bool(true), - EvalAttributeArg::Null, - EvalAttributeArg::String("EvalAttrDep".to_string()), - EvalAttributeArg::Array(vec![ - EvalAttributeArg::String("nested".to_string()), - EvalAttributeArg::Named { - name: "key".to_string(), - value: Box::new(EvalAttributeArg::Int(2)), - }, - ]), - ]), - ), - EvalAttribute::new( - "Tag", - Some(vec![EvalAttributeArg::Named { - name: "name".to_string(), - value: Box::new(EvalAttributeArg::String("named".to_string())), - }]), - ), - ]) - )] - ); -} - -/// Verifies class-like declaration attributes attach to interfaces, traits, and enums. -#[test] -fn parse_fragment_accepts_class_like_attribute_metadata() { - let program = parse_fragment( - br#"#[IfaceMark] interface DynEvalAttrIface {} -#[TraitMark] trait DynEvalAttrTrait {} -#[EnumMark] enum DynEvalAttrEnum { case Ready; }"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::InterfaceDecl( - EvalInterface::new("DynEvalAttrIface", Vec::new(), Vec::new()) - .with_attributes(vec![EvalAttribute::new("IfaceMark", Some(Vec::new()))]) - ), - EvalStmt::TraitDecl( - EvalTrait::new("DynEvalAttrTrait", Vec::new(), Vec::new()) - .with_attributes(vec![EvalAttribute::new("TraitMark", Some(Vec::new()))]) - ), - EvalStmt::EnumDecl( - EvalEnum::new( - "DynEvalAttrEnum", - None, - vec![EvalEnumCase::new("Ready", None)] - ) - .with_attributes(vec![EvalAttribute::new("EnumMark", Some(Vec::new()))]) - ), - ] - ); -} - -/// Verifies attributes on class constants, properties, and methods are retained. -#[test] -fn parse_fragment_accepts_class_member_attribute_metadata() { - let program = parse_fragment( - br#"class DynEvalMemberAttrs { - #[ConstMark] - public const SEED = 1; - #[PropMark("p")] - public int $value; - #[MethodMark(true)] - public function read() { return 1; } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl( - EvalClass::with_modifiers_traits_and_constants( - "DynEvalMemberAttrs", - false, - false, - None, - Vec::new(), - Vec::new(), - vec![ - EvalClassConstant::new("SEED", EvalExpr::Const(EvalConst::Int(1))) - .with_attributes(vec![EvalAttribute::new("ConstMark", Some(Vec::new()))]) - ], - vec![EvalClassProperty::new("value", None) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - .with_attributes(vec![EvalAttribute::new( - "PropMark", - Some(vec![EvalAttributeArg::String("p".to_string())]) - )])], - vec![EvalClassMethod::new( - "read", - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))] - ) - .with_attributes(vec![EvalAttribute::new( - "MethodMark", - Some(vec![EvalAttributeArg::Bool(true)]) - )])], - ) - )] - ); -} - -/// Verifies PHP's legacy `var` property marker parses as public property syntax. -#[test] -fn parse_fragment_accepts_legacy_var_properties() { - let program = parse_fragment( - br#"class DynEvalVarProps { - var $plain = "p"; - var ?int $count = null; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalVarProps", - vec![ - EvalClassProperty::new( - "plain", - Some(EvalExpr::Const(EvalConst::String("p".to_string()))) - ), - EvalClassProperty::new("count", Some(EvalExpr::Const(EvalConst::Null))) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - true - ))) - ], - Vec::new(), - ))] - ); -} - -/// Verifies legacy `var` property syntax also works inside eval traits. -#[test] -fn parse_fragment_accepts_legacy_var_trait_properties() { - let program = parse_fragment( - br#"trait DynEvalVarTrait { - var ?int $count = null; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::TraitDecl(EvalTrait::new( - "DynEvalVarTrait", - vec![ - EvalClassProperty::new("count", Some(EvalExpr::Const(EvalConst::Null))) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - true - ))) - ], - Vec::new(), - ))] - ); -} - -/// Verifies legacy `var` cannot be combined with other property modifiers. -#[test] -fn parse_fragment_rejects_legacy_var_modifier_combinations() { - for source in [ - b"class DynEvalBadPublicVar { public var $value; }" as &[u8], - b"class DynEvalBadStaticVar { static var $value; }", - b"class DynEvalBadReadonlyVar { readonly var $value; }", - ] { - assert_eq!( - parse_fragment(source), - Err(EvalParseError::UnsupportedConstruct) - ); - } -} - -/// Verifies interface, trait, and enum member attributes are retained. -#[test] -fn parse_fragment_accepts_class_like_member_attribute_metadata() { - let program = parse_fragment( - br#"interface DynEvalMemberIface { - #[IfaceProp] - public string $value { get; } - #[IfaceMethod] - function read(); -} -trait DynEvalMemberTrait { - #[TraitProp] - public int $seed; - #[TraitMethod] - public function add() { return 2; } -} -enum DynEvalMemberEnum { - #[CaseMark] - case Ready; - #[EnumMethod] - public function label() { return "ready"; } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::InterfaceDecl(EvalInterface::with_constants_and_properties( - "DynEvalMemberIface", - Vec::new(), - Vec::new(), - vec![EvalInterfaceProperty::new("value", true, false) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::String], - false - ))) - .with_attributes(vec![EvalAttribute::new("IfaceProp", Some(Vec::new()))])], - vec![EvalInterfaceMethod::new("read", Vec::new()) - .with_attributes(vec![EvalAttribute::new("IfaceMethod", Some(Vec::new()))])], - )), - EvalStmt::TraitDecl(EvalTrait::new( - "DynEvalMemberTrait", - vec![EvalClassProperty::new("seed", None) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - .with_attributes(vec![EvalAttribute::new("TraitProp", Some(Vec::new()))])], - vec![EvalClassMethod::new( - "add", - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(2))))] - ) - .with_attributes(vec![EvalAttribute::new("TraitMethod", Some(Vec::new()))])], - )), - EvalStmt::EnumDecl(EvalEnum::with_members( - "DynEvalMemberEnum", - None, - Vec::new(), - vec![EvalEnumCase::new("Ready", None) - .with_attributes(vec![EvalAttribute::new("CaseMark", Some(Vec::new()))])], - Vec::new(), - vec![EvalClassMethod::new( - "label", - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( - "ready".to_string() - ))))] - ) - .with_attributes(vec![EvalAttribute::new("EnumMethod", Some(Vec::new()))])], - )), - ] - ); -} - -/// Verifies eval interface declarations lower to dynamic interface metadata. -#[test] -fn parse_fragment_accepts_interface_declaration_source() { - let program = parse_fragment( - br#"interface DynEvalIface extends ParentIface, \Root\Iface { - public function read($value); - function label(); -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::InterfaceDecl(EvalInterface::new( - "DynEvalIface", - vec!["ParentIface".to_string(), "Root\\Iface".to_string()], - vec![ - EvalInterfaceMethod::new("read", vec!["value".to_string()]), - EvalInterfaceMethod::new("label", Vec::new()), - ], - ))] - ); -} - -/// Verifies interface property hook contracts lower to eval interface metadata. -#[test] -fn parse_fragment_accepts_interface_property_hook_contracts() { - let program = parse_fragment( - br#"interface DynEvalHookIface { - public string $value { get; set; } - public int $id { &get; } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::InterfaceDecl( - EvalInterface::with_constants_and_properties( - "DynEvalHookIface", - Vec::new(), - Vec::new(), - vec![ - EvalInterfaceProperty::new("value", true, true).with_type(Some( - EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) - )), - EvalInterfaceProperty::new("id", true, false).with_type(Some( - EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) - )), - ], - Vec::new(), - ) - )] - ); -} - -/// Verifies interface property contracts retain asymmetric set visibility. -#[test] -fn parse_fragment_accepts_interface_asymmetric_property_contracts() { - let program = parse_fragment( - br#"interface DynEvalAsymmetricIface { - public protected(set) string $name { get; set; } - private(set) int $id { get; set; } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::InterfaceDecl( - EvalInterface::with_constants_and_properties( - "DynEvalAsymmetricIface", - Vec::new(), - Vec::new(), - vec![ - EvalInterfaceProperty::new("name", true, true) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::String], - false - ))) - .with_set_visibility(Some(EvalVisibility::Protected)), - EvalInterfaceProperty::new("id", true, true) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - .with_set_visibility(Some(EvalVisibility::Private)), - ], - Vec::new(), - ) - )] - ); -} - -/// Verifies public property and method class members lower into dynamic class metadata. -#[test] -fn parse_fragment_accepts_public_class_members() { - let program = parse_fragment( - b"class DynEvalSupported { public int $x = 1; public function read() { return $this->x; } }", - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalSupported", - vec![ - EvalClassProperty::new("x", Some(EvalExpr::Const(EvalConst::Int(1)))).with_type( - Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - )) - ) - ], - vec![EvalClassMethod::new( - "read", - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "x".to_string(), - }))] - )] - ))] - ); -} - -/// Verifies constructor-promoted properties lower to property metadata and assignments. -#[test] -fn parse_fragment_accepts_constructor_promoted_properties() { - let program = parse_fragment( - br#"class DynEvalPromoted { - public function __construct(public int $id, private readonly ?string $name = null) {} -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalPromoted", - vec![ - EvalClassProperty::with_visibility_static_final_and_readonly( - "id", - EvalVisibility::Public, - false, - false, - false, - None, - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - .with_promoted(), - EvalClassProperty::with_visibility_static_final_and_readonly( - "name", - EvalVisibility::Private, - false, - false, - true, - None, - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::String], - true - ))) - .with_promoted(), - ], - vec![EvalClassMethod::new( - "__construct", - vec!["id".to_string(), "name".to_string()], - vec![ - EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: "id".to_string(), - value: EvalExpr::LoadVar("id".to_string()), - }, - EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: "name".to_string(), - value: EvalExpr::LoadVar("name".to_string()), - }, - ], - ) - .with_parameter_types(vec![ - Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - )), - Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::String], - true - )), - ]) - .with_parameter_defaults(vec![None, Some(EvalExpr::Const(EvalConst::Null))])] - ))] - ); -} - -/// Verifies comma-separated simple properties lower to individual eval properties. -#[test] -fn parse_fragment_accepts_comma_separated_class_properties() { - let program = parse_fragment( - br#"class DynEvalMultiProperties { - public private(set) int $id = 1, $nextId; - public static string $first = "a", $second = "b"; - var $legacy, $legacyDefault = 3; -} -trait DynEvalMultiPropertyTrait { - protected int $left = 4, $right = 5; -}"#, - ) - .expect("fragment should parse"); - let EvalStmt::ClassDecl(class) = &program.statements()[0] else { - panic!("expected class declaration"); - }; - let properties = class.properties(); - assert_eq!(properties.len(), 6); - assert_eq!(properties[0].name(), "id"); - assert_eq!(properties[0].set_visibility(), Some(EvalVisibility::Private)); - assert_eq!( - properties[0].property_type(), - Some(&EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false, - )), - ); - assert_eq!(properties[0].default(), Some(&EvalExpr::Const(EvalConst::Int(1)))); - assert_eq!(properties[1].name(), "nextId"); - assert_eq!(properties[1].set_visibility(), Some(EvalVisibility::Private)); - assert_eq!(properties[1].default(), None); - assert_eq!(properties[2].name(), "first"); - assert!(properties[2].is_static()); - assert_eq!( - properties[2].property_type(), - Some(&EvalParameterType::new( - vec![EvalParameterTypeVariant::String], - false, - )), - ); - assert_eq!(properties[3].name(), "second"); - assert!(properties[3].is_static()); - assert_eq!(properties[4].name(), "legacy"); - assert_eq!(properties[4].property_type(), None); - assert_eq!(properties[5].name(), "legacyDefault"); - assert_eq!(properties[5].default(), Some(&EvalExpr::Const(EvalConst::Int(3)))); - - let EvalStmt::TraitDecl(trait_decl) = &program.statements()[1] else { - panic!("expected trait declaration"); - }; - assert_eq!(trait_decl.properties().len(), 2); - assert_eq!(trait_decl.properties()[0].name(), "left"); - assert_eq!(trait_decl.properties()[0].visibility(), EvalVisibility::Protected); - assert_eq!(trait_decl.properties()[1].name(), "right"); - assert_eq!(trait_decl.properties()[1].visibility(), EvalVisibility::Protected); -} - -/// Verifies by-reference promoted constructor parameters lower to property aliases. -#[test] -fn parse_fragment_accepts_by_reference_constructor_promoted_properties() { - let program = parse_fragment( - br#"class DynEvalPromotedRef { - public function __construct(public int &$id) {} -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalPromotedRef", - vec![ - EvalClassProperty::with_visibility_static_final_and_readonly( - "id", - EvalVisibility::Public, - false, - false, - false, - None, - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - .with_promoted() - ], - vec![EvalClassMethod::new( - "__construct", - vec!["id".to_string()], - vec![EvalStmt::PropertyReferenceBind { - object: EvalExpr::LoadVar("this".to_string()), - property: "id".to_string(), - source: "id".to_string(), - }], - ) - .with_parameter_types(vec![Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))]) - .with_parameter_by_ref_flags(vec![true])] - ))] - ); -} - -/// Verifies eval rejects promoted parameter forms that the eval runtime cannot model yet. -#[test] -fn parse_fragment_rejects_unsupported_constructor_promotion_forms() { - parse_fragment(b"class DynEvalPromotedMethod { public function run(public int $id) {} }") - .expect_err("promotion is only valid on constructors"); - parse_fragment( - b"class DynEvalPromotedVariadic { public function __construct(public ...$ids) {} }", - ) - .expect_err("promoted variadic parameters need variadic property semantics"); - parse_fragment( - b"interface DynEvalPromotedIface { public function __construct(public int $id); }", - ) - .expect_err("interface signatures cannot promote properties"); - parse_fragment(b"enum DynEvalPromotedEnum { public function __construct(public int $id) {} }") - .expect_err("enum methods cannot promote properties"); -} - -/// Verifies private and protected class members lower with explicit visibility metadata. -#[test] -fn parse_fragment_accepts_private_and_protected_class_members() { - let program = parse_fragment( - b"class DynEvalVisibility { private int $secret = 3; protected function reveal() { return $this->secret; } }", - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalVisibility", - vec![EvalClassProperty::with_visibility( - "secret", - EvalVisibility::Private, - Some(EvalExpr::Const(EvalConst::Int(3))) - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - )))], - vec![EvalClassMethod::with_visibility_and_modifiers( - "reveal", - EvalVisibility::Protected, - false, - false, - false, - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "secret".to_string(), - }))] - )] - ))] - ); -} - -/// Verifies readonly property modifiers lower into dynamic class metadata. -#[test] -fn parse_fragment_accepts_readonly_class_property() { - let program = parse_fragment(b"class DynEvalReadonly { public readonly int $id; }") - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalReadonly", - vec![EvalClassProperty::with_visibility_static_and_readonly( - "id", - EvalVisibility::Public, - false, - true, - None - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - )))], - Vec::new() - ))] - ); -} - -/// Verifies asymmetric property visibility lowers into eval class metadata. -#[test] -fn parse_fragment_accepts_asymmetric_property_visibility() { - let program = parse_fragment( - b"class DynEvalAsymmetric { public private(set) int $id = 1; protected(set) string $name = \"x\"; }", - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalAsymmetric", - vec![ - EvalClassProperty::with_visibility_static_final_and_readonly( - "id", - EvalVisibility::Public, - false, - false, - false, - Some(EvalExpr::Const(EvalConst::Int(1))) - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - .with_set_visibility(Some(EvalVisibility::Private)), - EvalClassProperty::with_visibility_static_final_and_readonly( - "name", - EvalVisibility::Public, - false, - false, - false, - Some(EvalExpr::Const(EvalConst::String("x".to_string()))) - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::String], - false - ))) - .with_set_visibility(Some(EvalVisibility::Protected)), - ], - Vec::new() - ))] - ); -} - -/// Verifies eval rejects asymmetric property visibility forms that PHP rejects. -#[test] -fn parse_fragment_rejects_invalid_asymmetric_property_visibility() { - parse_fragment(b"class DynEvalAsymUntyped { public private(set) $id = 1; }") - .expect_err("asymmetric properties must be typed"); - parse_fragment(b"class DynEvalAsymStatic { public private(set) static int $id = 1; }") - .expect_err("asymmetric properties cannot be static"); - parse_fragment(b"class DynEvalAsymWeak { private public(set) int $id = 1; }") - .expect_err("set visibility cannot be weaker than read visibility"); - parse_fragment(b"class DynEvalAsymMethod { public private(set) function run() {} }") - .expect_err("asymmetric visibility is property-only"); -} - -/// Verifies readonly class modifiers lower into class and property metadata. -#[test] -fn parse_fragment_accepts_readonly_class_modifier() { - let program = parse_fragment( - b"final readonly class DynEvalReadonlyClass { public int $id; public static int $count = 0; }", - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::with_class_modifiers( - "DynEvalReadonlyClass", - false, - true, - true, - None, - Vec::new(), - vec![ - EvalClassProperty::with_visibility_static_and_readonly( - "id", - EvalVisibility::Public, - false, - true, - None - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))), - EvalClassProperty::with_visibility_static_and_readonly( - "count", - EvalVisibility::Public, - true, - false, - Some(EvalExpr::Const(EvalConst::Int(0))) - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - ], - Vec::new() - ))] - ); -} - -/// Verifies concrete property hooks lower to property metadata plus accessor methods. -#[test] -fn parse_fragment_accepts_concrete_class_property_hooks() { - let program = parse_fragment( - br#"class DynEvalHooked { - public int $value { - &get => 7; - set => $value + 1; - } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "DynEvalHooked", - vec![EvalClassProperty::with_visibility_static_and_readonly( - "value", - EvalVisibility::Public, - false, - false, - None - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - .with_hooks(true, true) - .with_virtual(false)], - vec![ - EvalClassMethod::new( - "__propget_value", - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(7))))] - ) - .with_source_location(EvalSourceLocation::new(3, 3)), - EvalClassMethod::new( - "__propset_value", - vec!["value".to_string()], - vec![EvalStmt::PropertySet { - object: EvalExpr::LoadVar("this".to_string()), - property: "value".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::LoadVar("value".to_string())), - right: Box::new(EvalExpr::Const(EvalConst::Int(1))) - } - }] - ) - .with_parameter_types(vec![Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))]) - .with_source_location(EvalSourceLocation::new(4, 4)) - ] - ) - .with_source_location(EvalSourceLocation::new(1, 6)))] - ); -} - -/// Verifies typed set-hook parameters are retained separately from the property type. -#[test] -fn parse_fragment_retains_property_set_hook_parameter_type() { - let program = parse_fragment( - br#"class DynEvalTypedSetHooked { - public string $value { - set(int|string $raw) => $raw; - } -}"#, - ) - .expect("fragment should parse"); - let EvalStmt::ClassDecl(class) = &program.statements()[0] else { - panic!("expected class declaration"); - }; - let property = &class.properties()[0]; - assert_eq!( - property.property_type(), - Some(&EvalParameterType::new( - vec![EvalParameterTypeVariant::String], - false - )) - ); - assert_eq!( - property.set_hook_type(), - Some(&EvalParameterType::new( - vec![ - EvalParameterTypeVariant::Int, - EvalParameterTypeVariant::String - ], - false - )) - ); - assert_eq!( - class.methods()[0].parameter_types(), - &[Some(EvalParameterType::new( - vec![ - EvalParameterTypeVariant::Int, - EvalParameterTypeVariant::String - ], - false - ))] - ); -} - -/// Verifies eval rejects explicit untyped set-hook parameters for typed properties. -#[test] -fn parse_fragment_rejects_untyped_explicit_property_set_hook_parameter_for_typed_property() { - assert_eq!( - parse_fragment( - br#"class DynEvalUntypedSetParamHooked { - public string $value { - set($raw) => $raw; - } -}"# - ), - Err(EvalParseError::UnsupportedConstruct) - ); - - parse_fragment( - br#"class DynEvalUntypedSetParamUntypedProperty { - public $value { - set($raw) => $raw; - } -}"#, - ) - .expect("untyped properties may use an untyped explicit set-hook parameter"); -} - -/// Verifies abstract property hook contracts lower without concrete accessors. -#[test] -fn parse_fragment_accepts_abstract_class_property_hook_contracts() { - let program = parse_fragment( - br#"abstract class DynEvalAbstractHooked { - abstract public int $value { get; set; } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::with_modifiers( - "DynEvalAbstractHooked", - true, - false, - None, - Vec::new(), - vec![EvalClassProperty::with_visibility_static_and_readonly( - "value", - EvalVisibility::Public, - false, - false, - None - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - .with_abstract_hook_contract(true, true)], - Vec::new(), - ))] - ); -} - -/// Verifies trait abstract property hook contracts lower without concrete accessors. -#[test] -fn parse_fragment_accepts_trait_abstract_property_hook_contracts() { - let program = parse_fragment( - br#"trait DynEvalAbstractHookTrait { - abstract protected string $name { get; } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::TraitDecl(EvalTrait::new( - "DynEvalAbstractHookTrait", - vec![EvalClassProperty::with_visibility_static_and_readonly( - "name", - EvalVisibility::Protected, - false, - false, - None - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::String], - false - ))) - .with_abstract_hook_contract(true, false)], - Vec::new(), - ))] - ); -} - -/// Verifies eval rejects readonly property forms that PHP does not allow. -#[test] -fn parse_fragment_rejects_invalid_readonly_class_properties() { - parse_fragment(b"class DynEvalReadonlyDefault { public readonly int $id = 1; }") - .expect_err("readonly properties cannot have defaults in eval"); - parse_fragment(b"class DynEvalReadonlyStatic { public static readonly int $id; }") - .expect_err("static properties cannot be readonly in eval"); - parse_fragment(b"readonly class DynEvalReadonlyClassDefault { public int $id = 1; }") - .expect_err("readonly class instance properties cannot have defaults in eval"); -} - -/// Verifies eval rejects property hook forms that need broader class contracts. -#[test] -fn parse_fragment_rejects_invalid_property_hooks() { - parse_fragment(b"class DynEvalHookDefault { public int $id = 1 { get => $this->id; } }") - .expect_err("hooked properties cannot have defaults in eval"); - parse_fragment(b"class DynEvalHookStatic { public static int $id { get => 1; } }") - .expect_err("static properties cannot have hooks in eval"); - parse_fragment(b"class DynEvalHookByRefSet { public int $id { &set => 1; } }") - .expect_err("set property hooks cannot return by reference"); - parse_fragment( - b"abstract class DynEvalHookAbstractDefault { abstract public int $id = 1 { get; } }", - ) - .expect_err("abstract properties cannot have defaults in eval"); - parse_fragment( - b"abstract class DynEvalHookAbstractStatic { abstract public static int $id { get; } }", - ) - .expect_err("static properties cannot have abstract hooks in eval"); -} - -/// Verifies eval rejects concrete hook bodies in interface property contracts. -#[test] -fn parse_fragment_rejects_invalid_interface_property_hooks() { - parse_fragment(b"interface DynEvalIfaceHookBody { public int $id { get => 1; } }") - .expect_err("interface property hooks cannot have concrete bodies"); - parse_fragment(b"interface DynEvalIfaceHookDuplicate { public int $id { get; get; } }") - .expect_err("interface property hooks cannot repeat contracts"); - parse_fragment(b"interface DynEvalIfaceHookEmpty { public int $id { } }") - .expect_err("interface property hooks require at least one contract"); - parse_fragment(b"interface DynEvalIfaceHookByRefSet { public int $id { &set; } }") - .expect_err("set interface property hooks cannot return by reference"); - parse_fragment(b"interface DynEvalIfaceHookDefault { public int $id = 1 { get; } }") - .expect_err("interface property hook contracts cannot have defaults"); - parse_fragment( - b"interface DynEvalIfaceAsymReadOnly { public protected(set) int $id { get; } }", - ) - .expect_err("readonly virtual property cannot have asymmetric visibility"); - parse_fragment( - b"interface DynEvalIfaceAsymUntyped { public protected(set) $id { get; set; } }", - ) - .expect_err("asymmetric interface property must be typed"); -} - -/// Verifies abstract and final class modifiers lower into dynamic class metadata. -#[test] -fn parse_fragment_accepts_abstract_and_final_class_members() { - let program = parse_fragment( - br#"abstract class DynEvalAbstract { - abstract public function read($value); - final public $value = 42; - final public function label() { return "base"; } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::with_modifiers( - "DynEvalAbstract", - true, - false, - None, - Vec::new(), - vec![ - EvalClassProperty::with_visibility_static_final_and_readonly( - "value", - EvalVisibility::Public, - false, - true, - false, - Some(EvalExpr::Const(EvalConst::Int(42))), - ) - ], - vec![ - EvalClassMethod::with_modifiers( - "read", - true, - false, - vec!["value".to_string()], - Vec::new() - ), - EvalClassMethod::with_modifiers( - "label", - false, - true, - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( - "base".to_string() - ))))] - ), - ], - ))] - ); -} - -/// Verifies eval method parameters retain type, default, by-reference, and variadic metadata. -#[test] -fn parse_fragment_accepts_typed_method_parameter_metadata() { - let program = parse_fragment( - br#"class DynEvalTypedParams { - public function run(#[Both("pair")] Left&Right $both, int &$id = 7, ?\App\Name &$name = null, string|null $label = "x", mixed &...$tail) {} -}"#, - ) - .expect("fragment should parse"); - let [EvalStmt::ClassDecl(class)] = program.statements() else { - panic!("expected one class declaration"); - }; - let [method] = class.methods() else { - panic!("expected one class method"); - }; - assert_eq!( - method.params(), - &[ - "both".to_string(), - "id".to_string(), - "name".to_string(), - "label".to_string(), - "tail".to_string() - ] - ); - assert_eq!( - method.parameter_has_types(), - &[true, true, true, true, true] - ); - assert!(method.parameter_types().iter().all(Option::is_some)); - assert_eq!(method.parameter_attributes()[0][0].name(), "Both"); - assert!(method.parameter_attributes()[1].is_empty()); - let both_type = method.parameter_types()[0].as_ref().expect("both type"); - assert_eq!( - both_type.variants(), - &[ - EvalParameterTypeVariant::Class("Left".to_string()), - EvalParameterTypeVariant::Class("Right".to_string()) - ] - ); - assert!(!both_type.allows_null()); - assert!(both_type.is_intersection()); - let id_type = method.parameter_types()[1].as_ref().expect("id type"); - assert_eq!(id_type.variants(), &[EvalParameterTypeVariant::Int]); - assert!(!id_type.allows_null()); - assert!(!id_type.is_intersection()); - let name_type = method.parameter_types()[2].as_ref().expect("name type"); - assert_eq!( - name_type.variants(), - &[EvalParameterTypeVariant::Class("App\\Name".to_string())] - ); - assert!(name_type.allows_null()); - assert!(!name_type.is_intersection()); - let label_type = method.parameter_types()[3].as_ref().expect("label type"); - assert_eq!(label_type.variants(), &[EvalParameterTypeVariant::String]); - assert!(label_type.allows_null()); - assert!(!label_type.is_intersection()); - assert!(matches!( - method.parameter_defaults(), - [ - None, - Some(EvalExpr::Const(EvalConst::Int(7))), - Some(EvalExpr::Const(EvalConst::Null)), - Some(EvalExpr::Const(EvalConst::String(label))), - None - ] if label == "x" - )); - assert_eq!( - method.parameter_is_by_ref(), - &[false, true, true, false, true] - ); - assert_eq!( - method.parameter_is_variadic(), - &[false, false, false, false, true] - ); -} - -/// Verifies eval method parameter defaults retain supported constant-expression metadata. -#[test] -fn parse_fragment_accepts_method_parameter_constant_defaults() { - let program = parse_fragment( - br#"class DynEvalDefaultConstants { - const LABEL = "box"; - public function read($global = DYN_EVAL_DEFAULT_GLOBAL, $label = self::LABEL, $parent = parent::LABEL, $class = self::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "x"], $method = __METHOD__, $dep = new DynEvalDefaultDep(label: "dep")) {} -}"#, - ) - .expect("fragment should parse"); - let [EvalStmt::ClassDecl(class)] = program.statements() else { - panic!("expected one class declaration"); - }; - let [method] = class.methods() else { - panic!("expected one class method"); - }; - - assert!(matches!( - method.parameter_defaults(), - [ - Some(EvalExpr::ConstFetch(global)), - Some(EvalExpr::ClassConstantFetch { class_name: self_name, constant: self_constant }), - Some(EvalExpr::ClassConstantFetch { class_name: parent_name, constant: parent_constant }), - Some(EvalExpr::ClassNameFetch { class_name }), - Some(EvalExpr::Array(_)), - Some(EvalExpr::Magic(EvalMagicConst::Method)), - Some(EvalExpr::NewObject { class_name: dep_name, args }) - ] if global == "DYN_EVAL_DEFAULT_GLOBAL" - && self_name == "self" - && self_constant == "LABEL" - && parent_name == "parent" - && parent_constant == "LABEL" - && class_name == "self" - && dep_name == "DynEvalDefaultDep" - && args.len() == 1 - && args[0].name() == Some("label") - )); -} - -/// Verifies eval rejects late-bound `static::` defaults like PHP compile-time constants do. -#[test] -fn parse_fragment_rejects_late_bound_static_method_parameter_defaults() { - parse_fragment( - b"class DynEvalStaticDefault { public function read($label = static::LABEL) {} }", - ) - .expect_err("static class constant defaults are not PHP compile-time constants"); - parse_fragment( - b"class DynEvalStaticClassDefault { public function read($class = static::class) {} }", - ) - .expect_err("static class-name defaults are not PHP compile-time constants"); - parse_fragment( - b"class DynEvalStaticNewDefault { public function read($dep = new static()) {} }", - ) - .expect_err("static object defaults are not PHP compile-time constants"); - parse_fragment( - b"class DynEvalSpreadNewDefault { public function read($dep = new DynEvalDep(...[\"x\"])) {} }", - ) - .expect_err("argument unpacking is not supported in PHP constant expressions"); -} - -/// Verifies eval rejects invalid variadic method parameter forms. -#[test] -fn parse_fragment_rejects_invalid_variadic_method_parameters() { - parse_fragment(b"class DynEvalVariadicDefault { public function run(...$tail = null) {} }") - .expect_err("variadic method parameters cannot have defaults"); - parse_fragment(b"class DynEvalVariadicNotLast { public function run(...$tail, $next) {} }") - .expect_err("variadic method parameters must be last"); - parse_fragment(b"class DynEvalVariadicRefOrder { public function run(...&$tail) {} }") - .expect_err("by-reference marker must precede variadic marker"); -} - -/// Verifies trait declarations and class trait uses lower into dynamic metadata. -#[test] -fn parse_fragment_accepts_trait_declaration_and_class_use() { - let program = parse_fragment( - br#"trait DynEvalTrait { - public int $seed = 2; - public function read($value) { return $this->seed + $value; } -} -class DynEvalUsesTrait { - use DynEvalTrait, \Root\SharedTrait; -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::TraitDecl(EvalTrait::new( - "DynEvalTrait", - vec![ - EvalClassProperty::new("seed", Some(EvalExpr::Const(EvalConst::Int(2)))) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false - ))) - ], - vec![EvalClassMethod::new( - "read", - vec!["value".to_string()], - vec![EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::PropertyGet { - object: Box::new(EvalExpr::LoadVar("this".to_string())), - property: "seed".to_string(), - }), - right: Box::new(EvalExpr::LoadVar("value".to_string())), - }))] - )], - )), - EvalStmt::ClassDecl(EvalClass::with_modifiers_and_traits( - "DynEvalUsesTrait", - false, - false, - None, - Vec::new(), - vec!["DynEvalTrait".to_string(), "Root\\SharedTrait".to_string()], - Vec::new(), - Vec::new(), - )), - ] - ); -} - -/// Verifies trait declarations can compose other traits with adaptations. -#[test] -fn parse_fragment_accepts_trait_use_inside_trait() { - let program = parse_fragment( - br#"trait DynEvalInnerTrait { - public function read() { return "inner"; } -} -trait DynEvalOuterTrait { - use DynEvalInnerTrait { - read as private hiddenRead; - } - public function expose() { return $this->hiddenRead(); } -}"#, - ) - .expect("fragment should parse"); - - let [_, EvalStmt::TraitDecl(trait_decl)] = program.statements() else { - panic!("second statement should be a trait declaration"); - }; - assert_eq!(trait_decl.traits(), &["DynEvalInnerTrait".to_string()]); - assert_eq!( - trait_decl.trait_adaptations(), - &[EvalTraitAdaptation::Alias { - trait_name: None, - method: "read".to_string(), - alias: Some("hiddenRead".to_string()), - visibility: Some(EvalVisibility::Private), - }] - ); -} -/// Verifies malformed object construction reports an unexpected token. -#[test] -fn parse_fragment_rejects_new_without_class_name() { - assert_eq!( - parse_fragment(b"return new ();"), - Err(EvalParseError::UnexpectedToken) - ); -} -/// Verifies unsupported expression keywords report the unsupported construct status. -#[test] -fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { - for source in [b"return yield 1;" as &[u8]] { - assert_eq!( - parse_fragment(source), - Err(EvalParseError::UnsupportedConstruct) - ); - } -} -/// Verifies malformed statements report parse errors instead of partial IR. -#[test] -fn parse_fragment_rejects_missing_semicolon() { - assert_eq!( - parse_fragment(b"$x = 1"), - Err(EvalParseError::ExpectedSemicolon) - ); -} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/attributes.rs b/crates/elephc-magician/src/parser/tests/classes_errors/attributes.rs new file mode 100644 index 0000000000..26b29cba02 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/attributes.rs @@ -0,0 +1,282 @@ +//! Purpose: +//! Parser tests for class/member attributes and legacy `var` properties. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Attribute literal metadata and legacy modifier compatibility are covered. + +use super::super::support::*; + +/// Verifies class attributes lower to eval class metadata with supported literal args. +#[test] +fn parse_fragment_accepts_class_attribute_metadata() { + let program = parse_fragment( + br#"#[Route("/home", -1, 1.5, -2.5, true, null, EvalAttrDep::class, ["nested", "key" => 2])] +#[Tag(name: "named")] +class DynEvalAttributed {}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::new("DynEvalAttributed", Vec::new(), Vec::new()).with_attributes(vec![ + EvalAttribute::new( + "Route", + Some(vec![ + EvalAttributeArg::String("/home".to_string()), + EvalAttributeArg::Int(-1), + EvalAttributeArg::Float(1.5f64.to_bits()), + EvalAttributeArg::Float((-2.5f64).to_bits()), + EvalAttributeArg::Bool(true), + EvalAttributeArg::Null, + EvalAttributeArg::String("EvalAttrDep".to_string()), + EvalAttributeArg::Array(vec![ + EvalAttributeArg::String("nested".to_string()), + EvalAttributeArg::Named { + name: "key".to_string(), + value: Box::new(EvalAttributeArg::Int(2)), + }, + ]), + ]), + ), + EvalAttribute::new( + "Tag", + Some(vec![EvalAttributeArg::Named { + name: "name".to_string(), + value: Box::new(EvalAttributeArg::String("named".to_string())), + }]), + ), + ]) + )] + ); +} + +/// Verifies class-like declaration attributes attach to interfaces, traits, and enums. +#[test] +fn parse_fragment_accepts_class_like_attribute_metadata() { + let program = parse_fragment( + br#"#[IfaceMark] interface DynEvalAttrIface {} +#[TraitMark] trait DynEvalAttrTrait {} +#[EnumMark] enum DynEvalAttrEnum { case Ready; }"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::InterfaceDecl( + EvalInterface::new("DynEvalAttrIface", Vec::new(), Vec::new()) + .with_attributes(vec![EvalAttribute::new("IfaceMark", Some(Vec::new()))]) + ), + EvalStmt::TraitDecl( + EvalTrait::new("DynEvalAttrTrait", Vec::new(), Vec::new()) + .with_attributes(vec![EvalAttribute::new("TraitMark", Some(Vec::new()))]) + ), + EvalStmt::EnumDecl( + EvalEnum::new( + "DynEvalAttrEnum", + None, + vec![EvalEnumCase::new("Ready", None)] + ) + .with_attributes(vec![EvalAttribute::new("EnumMark", Some(Vec::new()))]) + ), + ] + ); +} + +/// Verifies attributes on class constants, properties, and methods are retained. +#[test] +fn parse_fragment_accepts_class_member_attribute_metadata() { + let program = parse_fragment( + br#"class DynEvalMemberAttrs { + #[ConstMark] + public const SEED = 1; + #[PropMark("p")] + public int $value; + #[MethodMark(true)] + public function read() { return 1; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl( + EvalClass::with_modifiers_traits_and_constants( + "DynEvalMemberAttrs", + false, + false, + None, + Vec::new(), + Vec::new(), + vec![ + EvalClassConstant::new("SEED", EvalExpr::Const(EvalConst::Int(1))) + .with_attributes(vec![EvalAttribute::new("ConstMark", Some(Vec::new()))]) + ], + vec![EvalClassProperty::new("value", None) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_attributes(vec![EvalAttribute::new( + "PropMark", + Some(vec![EvalAttributeArg::String("p".to_string())]) + )])], + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(1))))] + ) + .with_attributes(vec![EvalAttribute::new( + "MethodMark", + Some(vec![EvalAttributeArg::Bool(true)]) + )])], + ) + )] + ); +} + +/// Verifies PHP's legacy `var` property marker parses as public property syntax. +#[test] +fn parse_fragment_accepts_legacy_var_properties() { + let program = parse_fragment( + br#"class DynEvalVarProps { + var $plain = "p"; + var ?int $count = null; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalVarProps", + vec![ + EvalClassProperty::new( + "plain", + Some(EvalExpr::Const(EvalConst::String("p".to_string()))) + ), + EvalClassProperty::new("count", Some(EvalExpr::Const(EvalConst::Null))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + true + ))) + ], + Vec::new(), + ))] + ); +} + +/// Verifies legacy `var` property syntax also works inside eval traits. +#[test] +fn parse_fragment_accepts_legacy_var_trait_properties() { + let program = parse_fragment( + br#"trait DynEvalVarTrait { + var ?int $count = null; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalVarTrait", + vec![ + EvalClassProperty::new("count", Some(EvalExpr::Const(EvalConst::Null))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + true + ))) + ], + Vec::new(), + ))] + ); +} + +/// Verifies legacy `var` cannot be combined with other property modifiers. +#[test] +fn parse_fragment_rejects_legacy_var_modifier_combinations() { + for source in [ + b"class DynEvalBadPublicVar { public var $value; }" as &[u8], + b"class DynEvalBadStaticVar { static var $value; }", + b"class DynEvalBadReadonlyVar { readonly var $value; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies interface, trait, and enum member attributes are retained. +#[test] +fn parse_fragment_accepts_class_like_member_attribute_metadata() { + let program = parse_fragment( + br#"interface DynEvalMemberIface { + #[IfaceProp] + public string $value { get; } + #[IfaceMethod] + function read(); +} +trait DynEvalMemberTrait { + #[TraitProp] + public int $seed; + #[TraitMethod] + public function add() { return 2; } +} +enum DynEvalMemberEnum { + #[CaseMark] + case Ready; + #[EnumMethod] + public function label() { return "ready"; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::InterfaceDecl(EvalInterface::with_constants_and_properties( + "DynEvalMemberIface", + Vec::new(), + Vec::new(), + vec![EvalInterfaceProperty::new("value", true, false) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_attributes(vec![EvalAttribute::new("IfaceProp", Some(Vec::new()))])], + vec![EvalInterfaceMethod::new("read", Vec::new()) + .with_attributes(vec![EvalAttribute::new("IfaceMethod", Some(Vec::new()))])], + )), + EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalMemberTrait", + vec![EvalClassProperty::new("seed", None) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_attributes(vec![EvalAttribute::new("TraitProp", Some(Vec::new()))])], + vec![EvalClassMethod::new( + "add", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(2))))] + ) + .with_attributes(vec![EvalAttribute::new("TraitMethod", Some(Vec::new()))])], + )), + EvalStmt::EnumDecl(EvalEnum::with_members( + "DynEvalMemberEnum", + None, + Vec::new(), + vec![EvalEnumCase::new("Ready", None) + .with_attributes(vec![EvalAttribute::new("CaseMark", Some(Vec::new()))])], + Vec::new(), + vec![EvalClassMethod::new( + "label", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( + "ready".to_string() + ))))] + ) + .with_attributes(vec![EvalAttribute::new("EnumMethod", Some(Vec::new()))])], + )), + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/declarations.rs b/crates/elephc-magician/src/parser/tests/classes_errors/declarations.rs new file mode 100644 index 0000000000..2337970b10 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/declarations.rs @@ -0,0 +1,311 @@ +//! Purpose: +//! Parser tests for basic class declarations, types, reserved names, references, +//! and class-like constants. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases cover dynamic class metadata and malformed fragment errors. + +use super::super::support::*; + +/// Verifies empty class declarations lower to dynamic class-registration statements. +#[test] +fn parse_fragment_accepts_empty_class_declaration_source() { + let program = parse_fragment(b"class DynEvalClass {};").expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalClass", + Vec::new(), + Vec::new() + ))] + ); +} +/// Verifies class relation clauses lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_class_extends_and_implements_source() { + let program = parse_fragment( + br#"class DynEvalChild extends DynEvalBase implements DynEvalIface, \Root\Iface {}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_relations( + "DynEvalChild", + Some("DynEvalBase".to_string()), + vec!["DynEvalIface".to_string(), "Root\\Iface".to_string()], + Vec::new(), + Vec::new(), + ))] + ); +} + +/// Verifies function, interface, and class method return types are retained. +#[test] +fn parse_fragment_accepts_return_type_metadata() { + let program = parse_fragment( + br#"function DynEvalReturn(): ?int { return 1; } +interface DynEvalReturnIface { + public function read(): string; +} +class DynEvalReturnClass { + public function selfReturn(): static { return $this; } + public function done(): void {} +}"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::FunctionDecl { + return_type: Some(function_return_type), + .. + } = &statements[0] + else { + panic!("expected function declaration with return type"); + }; + assert_eq!( + function_return_type.variants(), + &[EvalParameterTypeVariant::Int] + ); + assert!(function_return_type.allows_null()); + + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + let interface_return_type = interface.methods()[0] + .return_type() + .expect("interface method return type"); + assert_eq!( + interface_return_type.variants(), + &[EvalParameterTypeVariant::String] + ); + + let EvalStmt::ClassDecl(class) = &statements[2] else { + panic!("expected class declaration"); + }; + let self_return_type = class.methods()[0] + .return_type() + .expect("class method return type"); + assert_eq!( + self_return_type.variants(), + &[EvalParameterTypeVariant::Class("static".to_string())] + ); + let void_return_type = class.methods()[1] + .return_type() + .expect("void method return type"); + assert_eq!( + void_return_type.variants(), + &[EvalParameterTypeVariant::Void] + ); +} + +/// Verifies type atoms are rejected in positions where PHP forbids them. +#[test] +fn parse_fragment_rejects_invalid_type_atom_forms() { + for source in [ + b"function DynEvalBadVoid(): ?void {}" as &[u8], + b"function DynEvalBadVoidUnion(): void|null {}", + b"function DynEvalBadNeverUnion(): never|int {}", + b"function DynEvalBadNeverIntersection(): never&Countable {}", + b"function DynEvalBadVoidParam(void $value) {}", + b"function DynEvalBadSelfParam(self $value) {}", + b"function DynEvalBadParentParam(parent $value) {}", + b"function DynEvalBadStaticParam(static $value) {}", + b"function DynEvalBadSelfReturn(): self {}", + b"function DynEvalBadParentReturn(): parent {}", + b"function DynEvalBadStaticReturn(): static {}", + b"class DynEvalBadStaticMethodParam { public function read(static $value) {} }", + b"class DynEvalBadCallableProperty { public callable $value; }", + b"class DynEvalBadStaticProperty { public static static $value; }", + b"interface DynEvalBadCallableInterfaceProperty { public callable $value { get; } }", + b"class DynEvalBadCallablePromoted { public function __construct(public callable $value) {} }", + b"class DynEvalBadStaticPromoted { public function __construct(public static $value) {} }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval rejects PHP's reserved `class` class-constant name. +#[test] +fn parse_fragment_rejects_reserved_class_constant_name() { + for source in [ + b"class DynEvalBadConstName { const class = 1; }" as &[u8], + b"interface DynEvalBadIfaceConstName { const class = 1; }", + b"trait DynEvalBadTraitConstName { const class = 1; }", + b"enum DynEvalBadEnumConstName { const class = 1; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval rejects PHP-reserved class-like declaration names. +#[test] +fn parse_fragment_rejects_reserved_class_like_declaration_names() { + for source in [ + b"class match {}" as &[u8], + b"class string {}", + b"class CLASS {}", + b"interface interface {}", + b"trait readonly {}", + b"enum bool { case Ready; }", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval accepts PHP semi-reserved class-like declaration names. +#[test] +fn parse_fragment_accepts_semi_reserved_class_like_declaration_names() { + let program = parse_fragment( + br#"class enum {} +interface from {} +trait resource {} +enum integer { case Ready; }"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::ClassDecl(class) = &statements[0] else { + panic!("expected class declaration"); + }; + assert_eq!(class.name(), "enum"); + + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + assert_eq!(interface.name(), "from"); + + let EvalStmt::TraitDecl(trait_decl) = &statements[2] else { + panic!("expected trait declaration"); + }; + assert_eq!(trait_decl.name(), "resource"); + + let EvalStmt::EnumDecl(enum_decl) = &statements[3] else { + panic!("expected enum declaration"); + }; + assert_eq!(enum_decl.name(), "integer"); +} + +/// Verifies eval rejects PHP-reserved bare class-like reference names. +#[test] +fn parse_fragment_rejects_reserved_unqualified_class_like_reference_names() { + for source in [ + b"class DynEvalBadExtends extends match {}" as &[u8], + b"class DynEvalBadImplements implements match {}", + b"interface DynEvalBadIfaceExtends extends match {}", + b"class DynEvalBadTraitUse { use match; }", + b"class DynEvalBadTraitAdapt { use SomeTrait { match::run insteadof SomeTrait; } }", + b"$box = new match();", + b"$ok = $box instanceof match;", + b"try {} catch (match $e) {}", + ] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} + +/// Verifies eval accepts semi-reserved or qualified class-like reference names PHP parses. +#[test] +fn parse_fragment_accepts_semi_reserved_and_qualified_class_like_reference_names() { + parse_fragment( + br#"class enum {} +class DynEvalExtendsSemiReserved extends enum {} +class DynEvalExtendsQualifiedReserved extends \match {} +class DynEvalNewSelf { + public function make() { return new self(); } +} +$ok = $box instanceof \match;"#, + ) + .expect("fragment should parse"); +} + +/// Verifies comma-separated class-like constants lower to individual eval constants. +#[test] +fn parse_fragment_accepts_comma_separated_class_like_constants() { + let program = parse_fragment( + br#"class DynEvalMultiConstClass { + public const A = 1, B = 2; +} +interface DynEvalMultiConstIface { + final public const C = 3, D = 4; +} +trait DynEvalMultiConstTrait { + protected const E = 5, F = 6; +} +enum DynEvalMultiConstEnum { + public const G = 7, H = 8; + case Ready; +}"#, + ) + .expect("fragment should parse"); + let statements = program.statements(); + let EvalStmt::ClassDecl(class) = &statements[0] else { + panic!("expected class declaration"); + }; + assert_eq!( + class.constants(), + &[ + EvalClassConstant::new("A", EvalExpr::Const(EvalConst::Int(1))), + EvalClassConstant::new("B", EvalExpr::Const(EvalConst::Int(2))), + ], + ); + let EvalStmt::InterfaceDecl(interface) = &statements[1] else { + panic!("expected interface declaration"); + }; + assert_eq!( + interface.constants(), + &[ + EvalClassConstant::with_visibility_and_final( + "C", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(3)), + ), + EvalClassConstant::with_visibility_and_final( + "D", + EvalVisibility::Public, + true, + EvalExpr::Const(EvalConst::Int(4)), + ), + ], + ); + let EvalStmt::TraitDecl(trait_decl) = &statements[2] else { + panic!("expected trait declaration"); + }; + assert_eq!( + trait_decl.constants(), + &[ + EvalClassConstant::with_visibility( + "E", + EvalVisibility::Protected, + EvalExpr::Const(EvalConst::Int(5)), + ), + EvalClassConstant::with_visibility( + "F", + EvalVisibility::Protected, + EvalExpr::Const(EvalConst::Int(6)), + ), + ], + ); + let EvalStmt::EnumDecl(enum_decl) = &statements[3] else { + panic!("expected enum declaration"); + }; + assert_eq!( + enum_decl.constants(), + &[ + EvalClassConstant::new("G", EvalExpr::Const(EvalConst::Int(7))), + EvalClassConstant::new("H", EvalExpr::Const(EvalConst::Int(8))), + ], + ); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/interfaces_properties.rs b/crates/elephc-magician/src/parser/tests/classes_errors/interfaces_properties.rs new file mode 100644 index 0000000000..edaff35d90 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/interfaces_properties.rs @@ -0,0 +1,326 @@ +//! Purpose: +//! Parser tests for interfaces, property contracts, constructor promotion, and +//! public class members. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Interface and property forms retain their EvalIR metadata and diagnostics. + +use super::super::support::*; + +/// Verifies eval interface declarations lower to dynamic interface metadata. +#[test] +fn parse_fragment_accepts_interface_declaration_source() { + let program = parse_fragment( + br#"interface DynEvalIface extends ParentIface, \Root\Iface { + public function read($value); + function label(); +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl(EvalInterface::new( + "DynEvalIface", + vec!["ParentIface".to_string(), "Root\\Iface".to_string()], + vec![ + EvalInterfaceMethod::new("read", vec!["value".to_string()]), + EvalInterfaceMethod::new("label", Vec::new()), + ], + ))] + ); +} + +/// Verifies interface property hook contracts lower to eval interface metadata. +#[test] +fn parse_fragment_accepts_interface_property_hook_contracts() { + let program = parse_fragment( + br#"interface DynEvalHookIface { + public string $value { get; set; } + public int $id { &get; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl( + EvalInterface::with_constants_and_properties( + "DynEvalHookIface", + Vec::new(), + Vec::new(), + vec![ + EvalInterfaceProperty::new("value", true, true).with_type(Some( + EvalParameterType::new(vec![EvalParameterTypeVariant::String], false) + )), + EvalInterfaceProperty::new("id", true, false).with_type(Some( + EvalParameterType::new(vec![EvalParameterTypeVariant::Int], false) + )), + ], + Vec::new(), + ) + )] + ); +} + +/// Verifies interface property contracts retain asymmetric set visibility. +#[test] +fn parse_fragment_accepts_interface_asymmetric_property_contracts() { + let program = parse_fragment( + br#"interface DynEvalAsymmetricIface { + public protected(set) string $name { get; set; } + private(set) int $id { get; set; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::InterfaceDecl( + EvalInterface::with_constants_and_properties( + "DynEvalAsymmetricIface", + Vec::new(), + Vec::new(), + vec![ + EvalInterfaceProperty::new("name", true, true) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_set_visibility(Some(EvalVisibility::Protected)), + EvalInterfaceProperty::new("id", true, true) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_set_visibility(Some(EvalVisibility::Private)), + ], + Vec::new(), + ) + )] + ); +} + +/// Verifies public property and method class members lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_public_class_members() { + let program = parse_fragment( + b"class DynEvalSupported { public int $x = 1; public function read() { return $this->x; } }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalSupported", + vec![ + EvalClassProperty::new("x", Some(EvalExpr::Const(EvalConst::Int(1)))).with_type( + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )) + ) + ], + vec![EvalClassMethod::new( + "read", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "x".to_string(), + }))] + )] + ))] + ); +} + +/// Verifies constructor-promoted properties lower to property metadata and assignments. +#[test] +fn parse_fragment_accepts_constructor_promoted_properties() { + let program = parse_fragment( + br#"class DynEvalPromoted { + public function __construct(public int $id, private readonly ?string $name = null) {} +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalPromoted", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_promoted(), + EvalClassProperty::with_visibility_static_final_and_readonly( + "name", + EvalVisibility::Private, + false, + false, + true, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + true + ))) + .with_promoted(), + ], + vec![EvalClassMethod::new( + "__construct", + vec!["id".to_string(), "name".to_string()], + vec![ + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "id".to_string(), + value: EvalExpr::LoadVar("id".to_string()), + }, + EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "name".to_string(), + value: EvalExpr::LoadVar("name".to_string()), + }, + ], + ) + .with_parameter_types(vec![ + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )), + Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + true + )), + ]) + .with_parameter_defaults(vec![None, Some(EvalExpr::Const(EvalConst::Null))])] + ))] + ); +} + +/// Verifies comma-separated simple properties lower to individual eval properties. +#[test] +fn parse_fragment_accepts_comma_separated_class_properties() { + let program = parse_fragment( + br#"class DynEvalMultiProperties { + public private(set) int $id = 1, $nextId; + public static string $first = "a", $second = "b"; + var $legacy, $legacyDefault = 3; +} +trait DynEvalMultiPropertyTrait { + protected int $left = 4, $right = 5; +}"#, + ) + .expect("fragment should parse"); + let EvalStmt::ClassDecl(class) = &program.statements()[0] else { + panic!("expected class declaration"); + }; + let properties = class.properties(); + assert_eq!(properties.len(), 6); + assert_eq!(properties[0].name(), "id"); + assert_eq!(properties[0].set_visibility(), Some(EvalVisibility::Private)); + assert_eq!( + properties[0].property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false, + )), + ); + assert_eq!(properties[0].default(), Some(&EvalExpr::Const(EvalConst::Int(1)))); + assert_eq!(properties[1].name(), "nextId"); + assert_eq!(properties[1].set_visibility(), Some(EvalVisibility::Private)); + assert_eq!(properties[1].default(), None); + assert_eq!(properties[2].name(), "first"); + assert!(properties[2].is_static()); + assert_eq!( + properties[2].property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false, + )), + ); + assert_eq!(properties[3].name(), "second"); + assert!(properties[3].is_static()); + assert_eq!(properties[4].name(), "legacy"); + assert_eq!(properties[4].property_type(), None); + assert_eq!(properties[5].name(), "legacyDefault"); + assert_eq!(properties[5].default(), Some(&EvalExpr::Const(EvalConst::Int(3)))); + + let EvalStmt::TraitDecl(trait_decl) = &program.statements()[1] else { + panic!("expected trait declaration"); + }; + assert_eq!(trait_decl.properties().len(), 2); + assert_eq!(trait_decl.properties()[0].name(), "left"); + assert_eq!(trait_decl.properties()[0].visibility(), EvalVisibility::Protected); + assert_eq!(trait_decl.properties()[1].name(), "right"); + assert_eq!(trait_decl.properties()[1].visibility(), EvalVisibility::Protected); +} + +/// Verifies by-reference promoted constructor parameters lower to property aliases. +#[test] +fn parse_fragment_accepts_by_reference_constructor_promoted_properties() { + let program = parse_fragment( + br#"class DynEvalPromotedRef { + public function __construct(public int &$id) {} +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalPromotedRef", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + None, + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_promoted() + ], + vec![EvalClassMethod::new( + "__construct", + vec!["id".to_string()], + vec![EvalStmt::PropertyReferenceBind { + object: EvalExpr::LoadVar("this".to_string()), + property: "id".to_string(), + source: "id".to_string(), + }], + ) + .with_parameter_types(vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))]) + .with_parameter_by_ref_flags(vec![true])] + ))] + ); +} + +/// Verifies eval rejects promoted parameter forms that the eval runtime cannot model yet. +#[test] +fn parse_fragment_rejects_unsupported_constructor_promotion_forms() { + parse_fragment(b"class DynEvalPromotedMethod { public function run(public int $id) {} }") + .expect_err("promotion is only valid on constructors"); + parse_fragment( + b"class DynEvalPromotedVariadic { public function __construct(public ...$ids) {} }", + ) + .expect_err("promoted variadic parameters need variadic property semantics"); + parse_fragment( + b"interface DynEvalPromotedIface { public function __construct(public int $id); }", + ) + .expect_err("interface signatures cannot promote properties"); + parse_fragment(b"enum DynEvalPromotedEnum { public function __construct(public int $id) {} }") + .expect_err("enum methods cannot promote properties"); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/methods_traits_errors.rs b/crates/elephc-magician/src/parser/tests/classes_errors/methods_traits_errors.rs new file mode 100644 index 0000000000..683888cae0 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/methods_traits_errors.rs @@ -0,0 +1,319 @@ +//! Purpose: +//! Parser tests for abstract/final methods, typed/default parameters, traits, +//! object construction, and terminal diagnostics. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Parameter metadata and invalid variadic or late-bound defaults are retained. + +use super::super::support::*; + +/// Verifies abstract and final class modifiers lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_abstract_and_final_class_members() { + let program = parse_fragment( + br#"abstract class DynEvalAbstract { + abstract public function read($value); + final public $value = 42; + final public function label() { return "base"; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_modifiers( + "DynEvalAbstract", + true, + false, + None, + Vec::new(), + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "value", + EvalVisibility::Public, + false, + true, + false, + Some(EvalExpr::Const(EvalConst::Int(42))), + ) + ], + vec![ + EvalClassMethod::with_modifiers( + "read", + true, + false, + vec!["value".to_string()], + Vec::new() + ), + EvalClassMethod::with_modifiers( + "label", + false, + true, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::String( + "base".to_string() + ))))] + ), + ], + ))] + ); +} + +/// Verifies eval method parameters retain type, default, by-reference, and variadic metadata. +#[test] +fn parse_fragment_accepts_typed_method_parameter_metadata() { + let program = parse_fragment( + br#"class DynEvalTypedParams { + public function run(#[Both("pair")] Left&Right $both, int &$id = 7, ?\App\Name &$name = null, string|null $label = "x", mixed &...$tail) {} +}"#, + ) + .expect("fragment should parse"); + let [EvalStmt::ClassDecl(class)] = program.statements() else { + panic!("expected one class declaration"); + }; + let [method] = class.methods() else { + panic!("expected one class method"); + }; + assert_eq!( + method.params(), + &[ + "both".to_string(), + "id".to_string(), + "name".to_string(), + "label".to_string(), + "tail".to_string() + ] + ); + assert_eq!( + method.parameter_has_types(), + &[true, true, true, true, true] + ); + assert!(method.parameter_types().iter().all(Option::is_some)); + assert_eq!(method.parameter_attributes()[0][0].name(), "Both"); + assert!(method.parameter_attributes()[1].is_empty()); + let both_type = method.parameter_types()[0].as_ref().expect("both type"); + assert_eq!( + both_type.variants(), + &[ + EvalParameterTypeVariant::Class("Left".to_string()), + EvalParameterTypeVariant::Class("Right".to_string()) + ] + ); + assert!(!both_type.allows_null()); + assert!(both_type.is_intersection()); + let id_type = method.parameter_types()[1].as_ref().expect("id type"); + assert_eq!(id_type.variants(), &[EvalParameterTypeVariant::Int]); + assert!(!id_type.allows_null()); + assert!(!id_type.is_intersection()); + let name_type = method.parameter_types()[2].as_ref().expect("name type"); + assert_eq!( + name_type.variants(), + &[EvalParameterTypeVariant::Class("App\\Name".to_string())] + ); + assert!(name_type.allows_null()); + assert!(!name_type.is_intersection()); + let label_type = method.parameter_types()[3].as_ref().expect("label type"); + assert_eq!(label_type.variants(), &[EvalParameterTypeVariant::String]); + assert!(label_type.allows_null()); + assert!(!label_type.is_intersection()); + assert!(matches!( + method.parameter_defaults(), + [ + None, + Some(EvalExpr::Const(EvalConst::Int(7))), + Some(EvalExpr::Const(EvalConst::Null)), + Some(EvalExpr::Const(EvalConst::String(label))), + None + ] if label == "x" + )); + assert_eq!( + method.parameter_is_by_ref(), + &[false, true, true, false, true] + ); + assert_eq!( + method.parameter_is_variadic(), + &[false, false, false, false, true] + ); +} + +/// Verifies eval method parameter defaults retain supported constant-expression metadata. +#[test] +fn parse_fragment_accepts_method_parameter_constant_defaults() { + let program = parse_fragment( + br#"class DynEvalDefaultConstants { + const LABEL = "box"; + public function read($global = DYN_EVAL_DEFAULT_GLOBAL, $label = self::LABEL, $parent = parent::LABEL, $class = self::class, $items = [self::LABEL => 1 + 2, "fallback" => null ?? "x"], $method = __METHOD__, $dep = new DynEvalDefaultDep(label: "dep")) {} +}"#, + ) + .expect("fragment should parse"); + let [EvalStmt::ClassDecl(class)] = program.statements() else { + panic!("expected one class declaration"); + }; + let [method] = class.methods() else { + panic!("expected one class method"); + }; + + assert!(matches!( + method.parameter_defaults(), + [ + Some(EvalExpr::ConstFetch(global)), + Some(EvalExpr::ClassConstantFetch { class_name: self_name, constant: self_constant }), + Some(EvalExpr::ClassConstantFetch { class_name: parent_name, constant: parent_constant }), + Some(EvalExpr::ClassNameFetch { class_name }), + Some(EvalExpr::Array(_)), + Some(EvalExpr::Magic(EvalMagicConst::Method)), + Some(EvalExpr::NewObject { class_name: dep_name, args }) + ] if global == "DYN_EVAL_DEFAULT_GLOBAL" + && self_name == "self" + && self_constant == "LABEL" + && parent_name == "parent" + && parent_constant == "LABEL" + && class_name == "self" + && dep_name == "DynEvalDefaultDep" + && args.len() == 1 + && args[0].name() == Some("label") + )); +} + +/// Verifies eval rejects late-bound `static::` defaults like PHP compile-time constants do. +#[test] +fn parse_fragment_rejects_late_bound_static_method_parameter_defaults() { + parse_fragment( + b"class DynEvalStaticDefault { public function read($label = static::LABEL) {} }", + ) + .expect_err("static class constant defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalStaticClassDefault { public function read($class = static::class) {} }", + ) + .expect_err("static class-name defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalStaticNewDefault { public function read($dep = new static()) {} }", + ) + .expect_err("static object defaults are not PHP compile-time constants"); + parse_fragment( + b"class DynEvalSpreadNewDefault { public function read($dep = new DynEvalDep(...[\"x\"])) {} }", + ) + .expect_err("argument unpacking is not supported in PHP constant expressions"); +} + +/// Verifies eval rejects invalid variadic method parameter forms. +#[test] +fn parse_fragment_rejects_invalid_variadic_method_parameters() { + parse_fragment(b"class DynEvalVariadicDefault { public function run(...$tail = null) {} }") + .expect_err("variadic method parameters cannot have defaults"); + parse_fragment(b"class DynEvalVariadicNotLast { public function run(...$tail, $next) {} }") + .expect_err("variadic method parameters must be last"); + parse_fragment(b"class DynEvalVariadicRefOrder { public function run(...&$tail) {} }") + .expect_err("by-reference marker must precede variadic marker"); +} + +/// Verifies trait declarations and class trait uses lower into dynamic metadata. +#[test] +fn parse_fragment_accepts_trait_declaration_and_class_use() { + let program = parse_fragment( + br#"trait DynEvalTrait { + public int $seed = 2; + public function read($value) { return $this->seed + $value; } +} +class DynEvalUsesTrait { + use DynEvalTrait, \Root\SharedTrait; +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalTrait", + vec![ + EvalClassProperty::new("seed", Some(EvalExpr::Const(EvalConst::Int(2)))) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + ], + vec![EvalClassMethod::new( + "read", + vec!["value".to_string()], + vec![EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "seed".to_string(), + }), + right: Box::new(EvalExpr::LoadVar("value".to_string())), + }))] + )], + )), + EvalStmt::ClassDecl(EvalClass::with_modifiers_and_traits( + "DynEvalUsesTrait", + false, + false, + None, + Vec::new(), + vec!["DynEvalTrait".to_string(), "Root\\SharedTrait".to_string()], + Vec::new(), + Vec::new(), + )), + ] + ); +} + +/// Verifies trait declarations can compose other traits with adaptations. +#[test] +fn parse_fragment_accepts_trait_use_inside_trait() { + let program = parse_fragment( + br#"trait DynEvalInnerTrait { + public function read() { return "inner"; } +} +trait DynEvalOuterTrait { + use DynEvalInnerTrait { + read as private hiddenRead; + } + public function expose() { return $this->hiddenRead(); } +}"#, + ) + .expect("fragment should parse"); + + let [_, EvalStmt::TraitDecl(trait_decl)] = program.statements() else { + panic!("second statement should be a trait declaration"); + }; + assert_eq!(trait_decl.traits(), &["DynEvalInnerTrait".to_string()]); + assert_eq!( + trait_decl.trait_adaptations(), + &[EvalTraitAdaptation::Alias { + trait_name: None, + method: "read".to_string(), + alias: Some("hiddenRead".to_string()), + visibility: Some(EvalVisibility::Private), + }] + ); +} +/// Verifies malformed object construction reports an unexpected token. +#[test] +fn parse_fragment_rejects_new_without_class_name() { + assert_eq!( + parse_fragment(b"return new ();"), + Err(EvalParseError::UnexpectedToken) + ); +} +/// Verifies unsupported expression keywords report the unsupported construct status. +#[test] +fn parse_fragment_rejects_expression_keywords_as_unsupported_constructs() { + for source in [b"return yield 1;" as &[u8]] { + assert_eq!( + parse_fragment(source), + Err(EvalParseError::UnsupportedConstruct) + ); + } +} +/// Verifies malformed statements report parse errors instead of partial IR. +#[test] +fn parse_fragment_rejects_missing_semicolon() { + assert_eq!( + parse_fragment(b"$x = 1"), + Err(EvalParseError::ExpectedSemicolon) + ); +} diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/mod.rs b/crates/elephc-magician/src/parser/tests/classes_errors/mod.rs new file mode 100644 index 0000000000..6887625df6 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/mod.rs @@ -0,0 +1,15 @@ +//! Purpose: +//! Organizes parser tests for class-like declarations, members, attributes, +//! hooks, traits, and diagnostics by syntax responsibility. +//! +//! Called from: +//! - `crate::parser::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod attributes; +mod declarations; +mod interfaces_properties; +mod methods_traits_errors; +mod visibility_hooks; diff --git a/crates/elephc-magician/src/parser/tests/classes_errors/visibility_hooks.rs b/crates/elephc-magician/src/parser/tests/classes_errors/visibility_hooks.rs new file mode 100644 index 0000000000..b7ad5c6313 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/classes_errors/visibility_hooks.rs @@ -0,0 +1,419 @@ +//! Purpose: +//! Parser tests for member visibility, readonly/asymmetric properties, property +//! hooks, and related invalid forms. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Concrete, abstract, interface, and trait hook contracts are distinguished. + +use super::super::support::*; + +/// Verifies private and protected class members lower with explicit visibility metadata. +#[test] +fn parse_fragment_accepts_private_and_protected_class_members() { + let program = parse_fragment( + b"class DynEvalVisibility { private int $secret = 3; protected function reveal() { return $this->secret; } }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalVisibility", + vec![EvalClassProperty::with_visibility( + "secret", + EvalVisibility::Private, + Some(EvalExpr::Const(EvalConst::Int(3))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )))], + vec![EvalClassMethod::with_visibility_and_modifiers( + "reveal", + EvalVisibility::Protected, + false, + false, + false, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::PropertyGet { + object: Box::new(EvalExpr::LoadVar("this".to_string())), + property: "secret".to_string(), + }))] + )] + ))] + ); +} + +/// Verifies readonly property modifiers lower into dynamic class metadata. +#[test] +fn parse_fragment_accepts_readonly_class_property() { + let program = parse_fragment(b"class DynEvalReadonly { public readonly int $id; }") + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalReadonly", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "id", + EvalVisibility::Public, + false, + true, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + )))], + Vec::new() + ))] + ); +} + +/// Verifies asymmetric property visibility lowers into eval class metadata. +#[test] +fn parse_fragment_accepts_asymmetric_property_visibility() { + let program = parse_fragment( + b"class DynEvalAsymmetric { public private(set) int $id = 1; protected(set) string $name = \"x\"; }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalAsymmetric", + vec![ + EvalClassProperty::with_visibility_static_final_and_readonly( + "id", + EvalVisibility::Public, + false, + false, + false, + Some(EvalExpr::Const(EvalConst::Int(1))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_set_visibility(Some(EvalVisibility::Private)), + EvalClassProperty::with_visibility_static_final_and_readonly( + "name", + EvalVisibility::Public, + false, + false, + false, + Some(EvalExpr::Const(EvalConst::String("x".to_string()))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_set_visibility(Some(EvalVisibility::Protected)), + ], + Vec::new() + ))] + ); +} + +/// Verifies eval rejects asymmetric property visibility forms that PHP rejects. +#[test] +fn parse_fragment_rejects_invalid_asymmetric_property_visibility() { + parse_fragment(b"class DynEvalAsymUntyped { public private(set) $id = 1; }") + .expect_err("asymmetric properties must be typed"); + parse_fragment(b"class DynEvalAsymStatic { public private(set) static int $id = 1; }") + .expect_err("asymmetric properties cannot be static"); + parse_fragment(b"class DynEvalAsymWeak { private public(set) int $id = 1; }") + .expect_err("set visibility cannot be weaker than read visibility"); + parse_fragment(b"class DynEvalAsymMethod { public private(set) function run() {} }") + .expect_err("asymmetric visibility is property-only"); +} + +/// Verifies readonly class modifiers lower into class and property metadata. +#[test] +fn parse_fragment_accepts_readonly_class_modifier() { + let program = parse_fragment( + b"final readonly class DynEvalReadonlyClass { public int $id; public static int $count = 0; }", + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_class_modifiers( + "DynEvalReadonlyClass", + false, + true, + true, + None, + Vec::new(), + vec![ + EvalClassProperty::with_visibility_static_and_readonly( + "id", + EvalVisibility::Public, + false, + true, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))), + EvalClassProperty::with_visibility_static_and_readonly( + "count", + EvalVisibility::Public, + true, + false, + Some(EvalExpr::Const(EvalConst::Int(0))) + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + ], + Vec::new() + ))] + ); +} + +/// Verifies concrete property hooks lower to property metadata plus accessor methods. +#[test] +fn parse_fragment_accepts_concrete_class_property_hooks() { + let program = parse_fragment( + br#"class DynEvalHooked { + public int $value { + &get => 7; + set => $value + 1; + } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "DynEvalHooked", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "value", + EvalVisibility::Public, + false, + false, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_hooks(true, true) + .with_virtual(false)], + vec![ + EvalClassMethod::new( + "__propget_value", + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::Const(EvalConst::Int(7))))] + ) + .with_source_location(EvalSourceLocation::new(3, 3)), + EvalClassMethod::new( + "__propset_value", + vec!["value".to_string()], + vec![EvalStmt::PropertySet { + object: EvalExpr::LoadVar("this".to_string()), + property: "value".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::LoadVar("value".to_string())), + right: Box::new(EvalExpr::Const(EvalConst::Int(1))) + } + }] + ) + .with_parameter_types(vec![Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))]) + .with_source_location(EvalSourceLocation::new(4, 4)) + ] + ) + .with_source_location(EvalSourceLocation::new(1, 6)))] + ); +} + +/// Verifies typed set-hook parameters are retained separately from the property type. +#[test] +fn parse_fragment_retains_property_set_hook_parameter_type() { + let program = parse_fragment( + br#"class DynEvalTypedSetHooked { + public string $value { + set(int|string $raw) => $raw; + } +}"#, + ) + .expect("fragment should parse"); + let EvalStmt::ClassDecl(class) = &program.statements()[0] else { + panic!("expected class declaration"); + }; + let property = &class.properties()[0]; + assert_eq!( + property.property_type(), + Some(&EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + )) + ); + assert_eq!( + property.set_hook_type(), + Some(&EvalParameterType::new( + vec![ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ], + false + )) + ); + assert_eq!( + class.methods()[0].parameter_types(), + &[Some(EvalParameterType::new( + vec![ + EvalParameterTypeVariant::Int, + EvalParameterTypeVariant::String + ], + false + ))] + ); +} + +/// Verifies eval rejects explicit untyped set-hook parameters for typed properties. +#[test] +fn parse_fragment_rejects_untyped_explicit_property_set_hook_parameter_for_typed_property() { + assert_eq!( + parse_fragment( + br#"class DynEvalUntypedSetParamHooked { + public string $value { + set($raw) => $raw; + } +}"# + ), + Err(EvalParseError::UnsupportedConstruct) + ); + + parse_fragment( + br#"class DynEvalUntypedSetParamUntypedProperty { + public $value { + set($raw) => $raw; + } +}"#, + ) + .expect("untyped properties may use an untyped explicit set-hook parameter"); +} + +/// Verifies abstract property hook contracts lower without concrete accessors. +#[test] +fn parse_fragment_accepts_abstract_class_property_hook_contracts() { + let program = parse_fragment( + br#"abstract class DynEvalAbstractHooked { + abstract public int $value { get; set; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::with_modifiers( + "DynEvalAbstractHooked", + true, + false, + None, + Vec::new(), + vec![EvalClassProperty::with_visibility_static_and_readonly( + "value", + EvalVisibility::Public, + false, + false, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false + ))) + .with_abstract_hook_contract(true, true)], + Vec::new(), + ))] + ); +} + +/// Verifies trait abstract property hook contracts lower without concrete accessors. +#[test] +fn parse_fragment_accepts_trait_abstract_property_hook_contracts() { + let program = parse_fragment( + br#"trait DynEvalAbstractHookTrait { + abstract protected string $name { get; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::TraitDecl(EvalTrait::new( + "DynEvalAbstractHookTrait", + vec![EvalClassProperty::with_visibility_static_and_readonly( + "name", + EvalVisibility::Protected, + false, + false, + None + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::String], + false + ))) + .with_abstract_hook_contract(true, false)], + Vec::new(), + ))] + ); +} + +/// Verifies eval rejects readonly property forms that PHP does not allow. +#[test] +fn parse_fragment_rejects_invalid_readonly_class_properties() { + parse_fragment(b"class DynEvalReadonlyDefault { public readonly int $id = 1; }") + .expect_err("readonly properties cannot have defaults in eval"); + parse_fragment(b"class DynEvalReadonlyStatic { public static readonly int $id; }") + .expect_err("static properties cannot be readonly in eval"); + parse_fragment(b"readonly class DynEvalReadonlyClassDefault { public int $id = 1; }") + .expect_err("readonly class instance properties cannot have defaults in eval"); +} + +/// Verifies eval rejects property hook forms that need broader class contracts. +#[test] +fn parse_fragment_rejects_invalid_property_hooks() { + parse_fragment(b"class DynEvalHookDefault { public int $id = 1 { get => $this->id; } }") + .expect_err("hooked properties cannot have defaults in eval"); + parse_fragment(b"class DynEvalHookStatic { public static int $id { get => 1; } }") + .expect_err("static properties cannot have hooks in eval"); + parse_fragment(b"class DynEvalHookByRefSet { public int $id { &set => 1; } }") + .expect_err("set property hooks cannot return by reference"); + parse_fragment( + b"abstract class DynEvalHookAbstractDefault { abstract public int $id = 1 { get; } }", + ) + .expect_err("abstract properties cannot have defaults in eval"); + parse_fragment( + b"abstract class DynEvalHookAbstractStatic { abstract public static int $id { get; } }", + ) + .expect_err("static properties cannot have abstract hooks in eval"); +} + +/// Verifies eval rejects concrete hook bodies in interface property contracts. +#[test] +fn parse_fragment_rejects_invalid_interface_property_hooks() { + parse_fragment(b"interface DynEvalIfaceHookBody { public int $id { get => 1; } }") + .expect_err("interface property hooks cannot have concrete bodies"); + parse_fragment(b"interface DynEvalIfaceHookDuplicate { public int $id { get; get; } }") + .expect_err("interface property hooks cannot repeat contracts"); + parse_fragment(b"interface DynEvalIfaceHookEmpty { public int $id { } }") + .expect_err("interface property hooks require at least one contract"); + parse_fragment(b"interface DynEvalIfaceHookByRefSet { public int $id { &set; } }") + .expect_err("set interface property hooks cannot return by reference"); + parse_fragment(b"interface DynEvalIfaceHookDefault { public int $id = 1 { get; } }") + .expect_err("interface property hook contracts cannot have defaults"); + parse_fragment( + b"interface DynEvalIfaceAsymReadOnly { public protected(set) int $id { get; } }", + ) + .expect_err("readonly virtual property cannot have asymmetric visibility"); + parse_fragment( + b"interface DynEvalIfaceAsymUntyped { public protected(set) $id { get; set; } }", + ) + .expect_err("asymmetric interface property must be typed"); +} diff --git a/crates/elephc-magician/src/parser/tests/static_members.rs b/crates/elephc-magician/src/parser/tests/static_members.rs deleted file mode 100644 index d6004d6b11..0000000000 --- a/crates/elephc-magician/src/parser/tests/static_members.rs +++ /dev/null @@ -1,672 +0,0 @@ -//! Purpose: -//! Parser tests for eval static property and static method syntax. -//! -//! Called from: -//! - `cargo test -p elephc-magician` through Rust's test harness. -//! -//! Key details: -//! - These cases verify `::` receivers lower to EvalIR static-member nodes. - -use super::support::*; - -/// Verifies static class members lower with explicit static metadata. -#[test] -fn parse_fragment_accepts_static_class_members() { - let program = parse_fragment( - br#"class EvalStaticBox { - public static int $count = 1; - public static function read() { return self::$count; } -}"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::ClassDecl(EvalClass::new( - "EvalStaticBox", - vec![EvalClassProperty::with_visibility_and_static( - "count", - EvalVisibility::Public, - true, - Some(EvalExpr::Const(EvalConst::Int(1))), - ) - .with_type(Some(EvalParameterType::new( - vec![EvalParameterTypeVariant::Int], - false, - )))], - vec![EvalClassMethod::with_visibility_and_modifiers( - "read", - EvalVisibility::Public, - true, - false, - false, - Vec::new(), - vec![EvalStmt::Return(Some(EvalExpr::StaticPropertyGet { - class_name: "self".to_string(), - property: "count".to_string(), - }))], - )], - ))] - ); -} - -/// Verifies static method calls lower to EvalIR call expressions. -#[test] -fn parse_fragment_accepts_static_method_call_expression() { - let program = - parse_fragment(br#"return EvalStaticBox::Read(2);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { - class_name: "EvalStaticBox".to_string(), - method: "Read".to_string(), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], - }))] - ); -} - -/// Verifies first-class static method syntax retains static callable metadata. -#[test] -fn parse_fragment_accepts_first_class_static_method_callable_source() { - let program = - parse_fragment(br#"return EvalStaticBox::Read(...);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::StaticMethodCallable { - class_name: "EvalStaticBox".to_string(), - method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), - }))] - ); -} - -/// Verifies static method calls preserve named arguments in source order. -#[test] -fn parse_fragment_accepts_named_static_method_call_expression() { - let program = - parse_fragment(br#"return EvalStaticBox::Read(step: 2);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { - class_name: "EvalStaticBox".to_string(), - method: "Read".to_string(), - args: vec![EvalCallArg::named( - "step", - EvalExpr::Const(EvalConst::Int(2)), - )], - }))] - ); -} - -/// Verifies runtime-valued static receivers lower to dynamic static method calls. -#[test] -fn parse_fragment_accepts_dynamic_static_method_receiver() { - let program = - parse_fragment(br#"return $class::Read(step: 2);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), - args: vec![EvalCallArg::named( - "step", - EvalExpr::Const(EvalConst::Int(2)), - )], - }))] - ); -} - -/// Verifies runtime-valued static receivers support variable method names. -#[test] -fn parse_fragment_accepts_dynamic_static_method_name() { - let program = - parse_fragment(br#"return $class::$method(2);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - method: Box::new(EvalExpr::LoadVar("method".to_string())), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], - }))] - ); -} - -/// Verifies named static receivers support variable method names. -#[test] -fn parse_fragment_accepts_named_receiver_dynamic_static_method_name() { - let program = - parse_fragment(br#"return EvalStaticBox::$method(2);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(EvalExpr::ClassNameFetch { - class_name: "EvalStaticBox".to_string(), - }), - method: Box::new(EvalExpr::LoadVar("method".to_string())), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], - }))] - ); -} - -/// Verifies braced dynamic static method names preserve their method expression. -#[test] -fn parse_fragment_accepts_braced_dynamic_static_method_name() { - let program = - parse_fragment(br#"return EvalStaticBox::{$method}(2) . $class::{"Read"}(3);"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(EvalExpr::ClassNameFetch { - class_name: "EvalStaticBox".to_string(), - }), - method: Box::new(EvalExpr::LoadVar("method".to_string())), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], - }), - right: Box::new(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(3)))], - }), - }))] - ); -} - -/// Verifies braced dynamic class constant names preserve their constant-name expression. -#[test] -fn parse_fragment_accepts_braced_dynamic_class_constant_name() { - let program = - parse_fragment(br#"return EvalStaticBox::{$constant} . $class::{"VALUE"};"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::DynamicClassConstantNameFetch { - class_name: Box::new(EvalExpr::ClassNameFetch { - class_name: "EvalStaticBox".to_string(), - }), - constant: Box::new(EvalExpr::LoadVar("constant".to_string())), - }), - right: Box::new(EvalExpr::DynamicClassConstantNameFetch { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - constant: Box::new(EvalExpr::Const(EvalConst::String("VALUE".to_string()))), - }), - }))] - ); -} - -/// Verifies parenthesized static receivers parse through the dynamic receiver path. -#[test] -fn parse_fragment_accepts_expression_static_receiver_members() { - let program = parse_fragment( - br#"return [($class)::read(2), (factory())::VALUE, (factory())::{"VALUE"}, (factory())::$count];"#, - ) - .expect("fragment should parse"); - let factory_call = || EvalExpr::Call { - name: "factory".to_string(), - args: Vec::new(), - }; - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::DynamicStaticMethodCall { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - method: Box::new(EvalExpr::Const(EvalConst::String("read".to_string()))), - args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], - }), - EvalArrayElement::Value(EvalExpr::DynamicClassConstantFetch { - class_name: Box::new(factory_call()), - constant: "VALUE".to_string(), - }), - EvalArrayElement::Value(EvalExpr::DynamicClassConstantNameFetch { - class_name: Box::new(factory_call()), - constant: Box::new(EvalExpr::Const(EvalConst::String("VALUE".to_string()))), - }), - EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(factory_call()), - property: "count".to_string(), - }), - ])))] - ); -} - -/// Verifies expression static receivers support property write statement lowering. -#[test] -fn parse_fragment_accepts_expression_static_receiver_property_writes() { - let program = parse_fragment( - br#"(factory())::$count = 2; -(factory())::$count += 3; -(factory())::$items[] = 4; -(factory())::$items[0] = 5; -(factory())::$count++; -++ (factory())::$count; --- (factory())::$count;"#, - ) - .expect("fragment should parse"); - let factory_call = || EvalExpr::Call { - name: "factory".to_string(), - args: Vec::new(), - }; - assert_eq!( - program.statements(), - &[ - EvalStmt::DynamicStaticPropertySet { - class_name: factory_call(), - property: "count".to_string(), - value: EvalExpr::Const(EvalConst::Int(2)), - }, - EvalStmt::DynamicStaticPropertySet { - class_name: factory_call(), - property: "count".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(factory_call()), - property: "count".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(3))), - }, - }, - EvalStmt::DynamicStaticPropertyArrayAppend { - class_name: factory_call(), - property: "items".to_string(), - value: EvalExpr::Const(EvalConst::Int(4)), - }, - EvalStmt::DynamicStaticPropertyArraySet { - class_name: factory_call(), - property: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(0)), - op: None, - value: EvalExpr::Const(EvalConst::Int(5)), - }, - EvalStmt::DynamicStaticPropertyIncDec { - class_name: factory_call(), - property: "count".to_string(), - increment: true, - }, - EvalStmt::DynamicStaticPropertyIncDec { - class_name: factory_call(), - property: "count".to_string(), - increment: true, - }, - EvalStmt::DynamicStaticPropertyIncDec { - class_name: factory_call(), - property: "count".to_string(), - increment: false, - }, - ] - ); -} - -/// Verifies `${...}` static property names parse as runtime-name expressions. -#[test] -fn parse_fragment_accepts_dynamic_static_property_name_expressions() { - let program = parse_fragment( - br#"return [EvalStaticBox::${$property}, $class::${name_expr()}, (factory())::${"items"}];"#, - ) - .expect("fragment should parse"); - let name_expr_call = || EvalExpr::Call { - name: "name_expr".to_string(), - args: Vec::new(), - }; - let factory_call = || EvalExpr::Call { - name: "factory".to_string(), - args: Vec::new(), - }; - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Array(vec![ - EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { - class_name: Box::new(EvalExpr::ClassNameFetch { - class_name: "EvalStaticBox".to_string(), - }), - property: Box::new(EvalExpr::LoadVar("property".to_string())), - }), - EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - property: Box::new(name_expr_call()), - }), - EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { - class_name: Box::new(factory_call()), - property: Box::new(EvalExpr::Const(EvalConst::String("items".to_string()))), - }), - ])))] - ); -} - -/// Verifies `${...}` static property names lower write-like statements. -#[test] -fn parse_fragment_accepts_dynamic_static_property_name_writes() { - let program = parse_fragment( - br#"EvalStaticBox::${$property} = 2; -++$class::${name_expr()}; -(factory())::${"items"}[] = 4;"#, - ) - .expect("fragment should parse"); - let name_expr_call = || EvalExpr::Call { - name: "name_expr".to_string(), - args: Vec::new(), - }; - let factory_call = || EvalExpr::Call { - name: "factory".to_string(), - args: Vec::new(), - }; - assert_eq!( - program.statements(), - &[ - EvalStmt::DynamicStaticPropertyNameSet { - class_name: EvalExpr::ClassNameFetch { - class_name: "EvalStaticBox".to_string(), - }, - property: EvalExpr::LoadVar("property".to_string()), - value: EvalExpr::Const(EvalConst::Int(2)), - }, - EvalStmt::DynamicStaticPropertyNameIncDec { - class_name: EvalExpr::LoadVar("class".to_string()), - property: name_expr_call(), - increment: true, - }, - EvalStmt::DynamicStaticPropertyNameArrayAppend { - class_name: factory_call(), - property: EvalExpr::Const(EvalConst::String("items".to_string())), - value: EvalExpr::Const(EvalConst::Int(4)), - }, - ] - ); -} - -/// Verifies runtime-valued static receivers support properties, constants, and `::class`. -#[test] -fn parse_fragment_accepts_dynamic_static_metadata_receiver() { - let program = parse_fragment(br#"return $class::$count . $class::VALUE . $object::class;"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::Return(Some(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::Binary { - op: EvalBinOp::Concat, - left: Box::new(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - property: "count".to_string(), - }), - right: Box::new(EvalExpr::DynamicClassConstantFetch { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - constant: "VALUE".to_string(), - }), - }), - right: Box::new(EvalExpr::DynamicClassNameFetch { - class_name: Box::new(EvalExpr::LoadVar("object".to_string())), - }), - }))] - ); -} - -/// Verifies runtime-valued static receivers support property writes. -#[test] -fn parse_fragment_accepts_dynamic_static_property_assignment() { - let program = parse_fragment(br#"$class::$count = 2;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::DynamicStaticPropertySet { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "count".to_string(), - value: EvalExpr::Const(EvalConst::Int(2)), - }] - ); -} - -/// Verifies dynamic static property compound assignments lower to read-modify-write EvalIR. -#[test] -fn parse_fragment_accepts_dynamic_static_property_compound_assignment() { - let program = parse_fragment(br#"$class::$count += 2;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::DynamicStaticPropertySet { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "count".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - property: "count".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }, - }] - ); -} - -/// Verifies static property unsets parse as explicit static-unset statements. -#[test] -fn parse_fragment_accepts_static_property_unset() { - let program = - parse_fragment(br#"unset(EvalStaticBox::$count);"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::UnsetStaticProperty { - class_name: "EvalStaticBox".to_string(), - property: "count".to_string(), - }] - ); -} - -/// Verifies runtime-valued static property unsets preserve the class receiver expression. -#[test] -fn parse_fragment_accepts_dynamic_static_property_unset() { - let program = parse_fragment(br#"unset($class::$count, $class::${$name});"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::UnsetDynamicStaticProperty { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "count".to_string(), - }, - EvalStmt::UnsetDynamicStaticPropertyName { - class_name: EvalExpr::LoadVar("class".to_string()), - property: EvalExpr::LoadVar("name".to_string()), - }, - ] - ); -} - -/// Verifies static-property array element unset preserves the static member expression. -#[test] -fn parse_fragment_accepts_static_property_array_unset() { - let program = parse_fragment(br#"unset(EvalStaticBox::$items[0], $class::$items[1]);"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::UnsetArrayElement { - array: EvalExpr::StaticPropertyGet { - class_name: "EvalStaticBox".to_string(), - property: "items".to_string(), - }, - index: EvalExpr::Const(EvalConst::Int(0)), - }, - EvalStmt::UnsetArrayElement { - array: EvalExpr::DynamicStaticPropertyGet { - class_name: Box::new(EvalExpr::LoadVar("class".to_string())), - property: "items".to_string(), - }, - index: EvalExpr::Const(EvalConst::Int(1)), - }, - ] - ); -} - -/// Verifies static property increment/decrement parse as dedicated member updates. -#[test] -fn parse_fragment_accepts_static_property_inc_dec() { - let program = parse_fragment(br#"EvalStaticBox::$count++; --EvalStaticBox::$count;"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::StaticPropertyIncDec { - class_name: "EvalStaticBox".to_string(), - property: "count".to_string(), - increment: true, - }, - EvalStmt::StaticPropertyIncDec { - class_name: "EvalStaticBox".to_string(), - property: "count".to_string(), - increment: false, - }, - ] - ); -} - -/// Verifies dynamic static property increment/decrement preserves the receiver expression. -#[test] -fn parse_fragment_accepts_dynamic_static_property_inc_dec() { - let program = - parse_fragment(br#"$class::$count++; --$class::$count;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::DynamicStaticPropertyIncDec { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "count".to_string(), - increment: true, - }, - EvalStmt::DynamicStaticPropertyIncDec { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "count".to_string(), - increment: false, - }, - ] - ); -} - -/// Verifies static property compound assignments lower to one read-modify-write statement. -#[test] -fn parse_fragment_accepts_static_property_compound_assignment() { - let program = parse_fragment(br#"EvalStaticBox::$count += 2;"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::StaticPropertySet { - class_name: "EvalStaticBox".to_string(), - property: "count".to_string(), - value: EvalExpr::Binary { - op: EvalBinOp::Add, - left: Box::new(EvalExpr::StaticPropertyGet { - class_name: "EvalStaticBox".to_string(), - property: "count".to_string(), - }), - right: Box::new(EvalExpr::Const(EvalConst::Int(2))), - }, - }] - ); -} - -/// Verifies static property reference bindings parse for named and dynamic targets. -#[test] -fn parse_fragment_accepts_static_property_reference_bind_source() { - let program = parse_fragment( - br#"EvalStaticBox::$count =& $source; -$class::$count =& $dynamic; -EvalStaticBox::${$name} =& $other; -(factory())::${$name} =& $third;"#, - ) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::StaticPropertyReferenceBind { - class_name: "EvalStaticBox".to_string(), - property: "count".to_string(), - source: "source".to_string(), - }, - EvalStmt::DynamicStaticPropertyReferenceBind { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "count".to_string(), - source: "dynamic".to_string(), - }, - EvalStmt::DynamicStaticPropertyNameReferenceBind { - class_name: EvalExpr::ClassNameFetch { - class_name: "EvalStaticBox".to_string(), - }, - property: EvalExpr::LoadVar("name".to_string()), - source: "other".to_string(), - }, - EvalStmt::DynamicStaticPropertyNameReferenceBind { - class_name: EvalExpr::Call { - name: "factory".to_string(), - args: Vec::new(), - }, - property: EvalExpr::LoadVar("name".to_string()), - source: "third".to_string(), - }, - ] - ); -} - -/// Verifies indexed static-property writes parse as dedicated EvalIR statements. -#[test] -fn parse_fragment_accepts_static_property_array_write_source() { - let program = - parse_fragment(br#"EvalStaticBox::$items[0] = "x"; EvalStaticBox::$items[] = "y";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::StaticPropertyArraySet { - class_name: "EvalStaticBox".to_string(), - property: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(0)), - op: None, - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }, - EvalStmt::StaticPropertyArrayAppend { - class_name: "EvalStaticBox".to_string(), - property: "items".to_string(), - value: EvalExpr::Const(EvalConst::String("y".to_string())), - }, - ] - ); -} - -/// Verifies indexed static-property compound assignment retains the static target. -#[test] -fn parse_fragment_accepts_static_property_array_compound_assignment_source() { - let program = - parse_fragment(br#"EvalStaticBox::$items[0] .= "x";"#).expect("fragment should parse"); - assert_eq!( - program.statements(), - &[EvalStmt::StaticPropertyArraySet { - class_name: "EvalStaticBox".to_string(), - property: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(0)), - op: Some(EvalBinOp::Concat), - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }] - ); -} - -/// Verifies runtime-valued static receivers support indexed static-property writes. -#[test] -fn parse_fragment_accepts_dynamic_static_property_array_write_source() { - let program = parse_fragment(br#"$class::$items[0] = "x"; $class::$items[] = "y";"#) - .expect("fragment should parse"); - assert_eq!( - program.statements(), - &[ - EvalStmt::DynamicStaticPropertyArraySet { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "items".to_string(), - index: EvalExpr::Const(EvalConst::Int(0)), - op: None, - value: EvalExpr::Const(EvalConst::String("x".to_string())), - }, - EvalStmt::DynamicStaticPropertyArrayAppend { - class_name: EvalExpr::LoadVar("class".to_string()), - property: "items".to_string(), - value: EvalExpr::Const(EvalConst::String("y".to_string())), - }, - ] - ); -} diff --git a/crates/elephc-magician/src/parser/tests/static_members/calls_receivers.rs b/crates/elephc-magician/src/parser/tests/static_members/calls_receivers.rs new file mode 100644 index 0000000000..21eb840ec6 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/static_members/calls_receivers.rs @@ -0,0 +1,301 @@ +//! Purpose: +//! Parser tests for static declarations, calls, first-class callables, dynamic +//! receivers, and expression receivers. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - These cases verify `::` receivers lower to EvalIR static-member nodes. + +use super::super::support::*; + +/// Verifies static class members lower with explicit static metadata. +#[test] +fn parse_fragment_accepts_static_class_members() { + let program = parse_fragment( + br#"class EvalStaticBox { + public static int $count = 1; + public static function read() { return self::$count; } +}"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::ClassDecl(EvalClass::new( + "EvalStaticBox", + vec![EvalClassProperty::with_visibility_and_static( + "count", + EvalVisibility::Public, + true, + Some(EvalExpr::Const(EvalConst::Int(1))), + ) + .with_type(Some(EvalParameterType::new( + vec![EvalParameterTypeVariant::Int], + false, + )))], + vec![EvalClassMethod::with_visibility_and_modifiers( + "read", + EvalVisibility::Public, + true, + false, + false, + Vec::new(), + vec![EvalStmt::Return(Some(EvalExpr::StaticPropertyGet { + class_name: "self".to_string(), + property: "count".to_string(), + }))], + )], + ))] + ); +} + +/// Verifies static method calls lower to EvalIR call expressions. +#[test] +fn parse_fragment_accepts_static_method_call_expression() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { + class_name: "EvalStaticBox".to_string(), + method: "Read".to_string(), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + +/// Verifies first-class static method syntax retains static callable metadata. +#[test] +fn parse_fragment_accepts_first_class_static_method_callable_source() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(...);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCallable { + class_name: "EvalStaticBox".to_string(), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + }))] + ); +} + +/// Verifies static method calls preserve named arguments in source order. +#[test] +fn parse_fragment_accepts_named_static_method_call_expression() { + let program = + parse_fragment(br#"return EvalStaticBox::Read(step: 2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::StaticMethodCall { + class_name: "EvalStaticBox".to_string(), + method: "Read".to_string(), + args: vec![EvalCallArg::named( + "step", + EvalExpr::Const(EvalConst::Int(2)), + )], + }))] + ); +} + +/// Verifies runtime-valued static receivers lower to dynamic static method calls. +#[test] +fn parse_fragment_accepts_dynamic_static_method_receiver() { + let program = + parse_fragment(br#"return $class::Read(step: 2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + args: vec![EvalCallArg::named( + "step", + EvalExpr::Const(EvalConst::Int(2)), + )], + }))] + ); +} + +/// Verifies runtime-valued static receivers support variable method names. +#[test] +fn parse_fragment_accepts_dynamic_static_method_name() { + let program = + parse_fragment(br#"return $class::$method(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + +/// Verifies named static receivers support variable method names. +#[test] +fn parse_fragment_accepts_named_receiver_dynamic_static_method_name() { + let program = + parse_fragment(br#"return EvalStaticBox::$method(2);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }))] + ); +} + +/// Verifies braced dynamic static method names preserve their method expression. +#[test] +fn parse_fragment_accepts_braced_dynamic_static_method_name() { + let program = + parse_fragment(br#"return EvalStaticBox::{$method}(2) . $class::{"Read"}(3);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + method: Box::new(EvalExpr::LoadVar("method".to_string())), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }), + right: Box::new(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("Read".to_string()))), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(3)))], + }), + }))] + ); +} + +/// Verifies braced dynamic class constant names preserve their constant-name expression. +#[test] +fn parse_fragment_accepts_braced_dynamic_class_constant_name() { + let program = + parse_fragment(br#"return EvalStaticBox::{$constant} . $class::{"VALUE"};"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + constant: Box::new(EvalExpr::LoadVar("constant".to_string())), + }), + right: Box::new(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + constant: Box::new(EvalExpr::Const(EvalConst::String("VALUE".to_string()))), + }), + }))] + ); +} + +/// Verifies parenthesized static receivers parse through the dynamic receiver path. +#[test] +fn parse_fragment_accepts_expression_static_receiver_members() { + let program = parse_fragment( + br#"return [($class)::read(2), (factory())::VALUE, (factory())::{"VALUE"}, (factory())::$count];"#, + ) + .expect("fragment should parse"); + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::DynamicStaticMethodCall { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + method: Box::new(EvalExpr::Const(EvalConst::String("read".to_string()))), + args: vec![EvalCallArg::positional(EvalExpr::Const(EvalConst::Int(2)))], + }), + EvalArrayElement::Value(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(factory_call()), + constant: "VALUE".to_string(), + }), + EvalArrayElement::Value(EvalExpr::DynamicClassConstantNameFetch { + class_name: Box::new(factory_call()), + constant: Box::new(EvalExpr::Const(EvalConst::String("VALUE".to_string()))), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(factory_call()), + property: "count".to_string(), + }), + ])))] + ); +} + +/// Verifies expression static receivers support property write statement lowering. +#[test] +fn parse_fragment_accepts_expression_static_receiver_property_writes() { + let program = parse_fragment( + br#"(factory())::$count = 2; +(factory())::$count += 3; +(factory())::$items[] = 4; +(factory())::$items[0] = 5; +(factory())::$count++; +++ (factory())::$count; +-- (factory())::$count;"#, + ) + .expect("fragment should parse"); + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertySet { + class_name: factory_call(), + property: "count".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicStaticPropertySet { + class_name: factory_call(), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(factory_call()), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(3))), + }, + }, + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: factory_call(), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::Int(4)), + }, + EvalStmt::DynamicStaticPropertyArraySet { + class_name: factory_call(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::Int(5)), + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: factory_call(), + property: "count".to_string(), + increment: false, + }, + ] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/static_members/dynamic_names.rs b/crates/elephc-magician/src/parser/tests/static_members/dynamic_names.rs new file mode 100644 index 0000000000..e0618d5a23 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/static_members/dynamic_names.rs @@ -0,0 +1,115 @@ +//! Purpose: +//! Parser tests for dynamic static-property names and runtime-valued metadata +//! receivers. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Braced runtime names lower to explicit EvalIR name expressions. + +use super::super::support::*; + +/// Verifies `${...}` static property names parse as runtime-name expressions. +#[test] +fn parse_fragment_accepts_dynamic_static_property_name_expressions() { + let program = parse_fragment( + br#"return [EvalStaticBox::${$property}, $class::${name_expr()}, (factory())::${"items"}];"#, + ) + .expect("fragment should parse"); + let name_expr_call = || EvalExpr::Call { + name: "name_expr".to_string(), + args: Vec::new(), + }; + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Array(vec![ + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }), + property: Box::new(EvalExpr::LoadVar("property".to_string())), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: Box::new(name_expr_call()), + }), + EvalArrayElement::Value(EvalExpr::DynamicStaticPropertyNameGet { + class_name: Box::new(factory_call()), + property: Box::new(EvalExpr::Const(EvalConst::String("items".to_string()))), + }), + ])))] + ); +} + +/// Verifies `${...}` static property names lower write-like statements. +#[test] +fn parse_fragment_accepts_dynamic_static_property_name_writes() { + let program = parse_fragment( + br#"EvalStaticBox::${$property} = 2; +++$class::${name_expr()}; +(factory())::${"items"}[] = 4;"#, + ) + .expect("fragment should parse"); + let name_expr_call = || EvalExpr::Call { + name: "name_expr".to_string(), + args: Vec::new(), + }; + let factory_call = || EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }; + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertyNameSet { + class_name: EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }, + property: EvalExpr::LoadVar("property".to_string()), + value: EvalExpr::Const(EvalConst::Int(2)), + }, + EvalStmt::DynamicStaticPropertyNameIncDec { + class_name: EvalExpr::LoadVar("class".to_string()), + property: name_expr_call(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyNameArrayAppend { + class_name: factory_call(), + property: EvalExpr::Const(EvalConst::String("items".to_string())), + value: EvalExpr::Const(EvalConst::Int(4)), + }, + ] + ); +} + +/// Verifies runtime-valued static receivers support properties, constants, and `::class`. +#[test] +fn parse_fragment_accepts_dynamic_static_metadata_receiver() { + let program = parse_fragment(br#"return $class::$count . $class::VALUE . $object::class;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::Return(Some(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::Binary { + op: EvalBinOp::Concat, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::DynamicClassConstantFetch { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + constant: "VALUE".to_string(), + }), + }), + right: Box::new(EvalExpr::DynamicClassNameFetch { + class_name: Box::new(EvalExpr::LoadVar("object".to_string())), + }), + }))] + ); +} diff --git a/crates/elephc-magician/src/parser/tests/static_members/mod.rs b/crates/elephc-magician/src/parser/tests/static_members/mod.rs new file mode 100644 index 0000000000..c49122625c --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/static_members/mod.rs @@ -0,0 +1,13 @@ +//! Purpose: +//! Organizes parser tests for static calls, receivers, dynamic member names, and +//! static-property mutations. +//! +//! Called from: +//! - `crate::parser::tests` through Rust's test harness. +//! +//! Key details: +//! - Child modules preserve every original test function name. + +mod calls_receivers; +mod dynamic_names; +mod property_mutations; diff --git a/crates/elephc-magician/src/parser/tests/static_members/property_mutations.rs b/crates/elephc-magician/src/parser/tests/static_members/property_mutations.rs new file mode 100644 index 0000000000..48d04cae13 --- /dev/null +++ b/crates/elephc-magician/src/parser/tests/static_members/property_mutations.rs @@ -0,0 +1,279 @@ +//! Purpose: +//! Parser tests for static-property assignment, compound operations, unset, +//! increment/decrement, reference binding, and array writes. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Named and dynamic class/property receivers retain dedicated EvalIR forms. + +use super::super::support::*; + +/// Verifies runtime-valued static receivers support property writes. +#[test] +fn parse_fragment_accepts_dynamic_static_property_assignment() { + let program = parse_fragment(br#"$class::$count = 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicStaticPropertySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + value: EvalExpr::Const(EvalConst::Int(2)), + }] + ); +} + +/// Verifies dynamic static property compound assignments lower to read-modify-write EvalIR. +#[test] +fn parse_fragment_accepts_dynamic_static_property_compound_assignment() { + let program = parse_fragment(br#"$class::$count += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::DynamicStaticPropertySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }] + ); +} + +/// Verifies static property unsets parse as explicit static-unset statements. +#[test] +fn parse_fragment_accepts_static_property_unset() { + let program = + parse_fragment(br#"unset(EvalStaticBox::$count);"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::UnsetStaticProperty { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + }] + ); +} + +/// Verifies runtime-valued static property unsets preserve the class receiver expression. +#[test] +fn parse_fragment_accepts_dynamic_static_property_unset() { + let program = parse_fragment(br#"unset($class::$count, $class::${$name});"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetDynamicStaticProperty { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + }, + EvalStmt::UnsetDynamicStaticPropertyName { + class_name: EvalExpr::LoadVar("class".to_string()), + property: EvalExpr::LoadVar("name".to_string()), + }, + ] + ); +} + +/// Verifies static-property array element unset preserves the static member expression. +#[test] +fn parse_fragment_accepts_static_property_array_unset() { + let program = parse_fragment(br#"unset(EvalStaticBox::$items[0], $class::$items[1]);"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::UnsetArrayElement { + array: EvalExpr::StaticPropertyGet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(0)), + }, + EvalStmt::UnsetArrayElement { + array: EvalExpr::DynamicStaticPropertyGet { + class_name: Box::new(EvalExpr::LoadVar("class".to_string())), + property: "items".to_string(), + }, + index: EvalExpr::Const(EvalConst::Int(1)), + }, + ] + ); +} + +/// Verifies static property increment/decrement parse as dedicated member updates. +#[test] +fn parse_fragment_accepts_static_property_inc_dec() { + let program = parse_fragment(br#"EvalStaticBox::$count++; --EvalStaticBox::$count;"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertyIncDec { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + increment: true, + }, + EvalStmt::StaticPropertyIncDec { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + increment: false, + }, + ] + ); +} + +/// Verifies dynamic static property increment/decrement preserves the receiver expression. +#[test] +fn parse_fragment_accepts_dynamic_static_property_inc_dec() { + let program = + parse_fragment(br#"$class::$count++; --$class::$count;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertyIncDec { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + increment: true, + }, + EvalStmt::DynamicStaticPropertyIncDec { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + increment: false, + }, + ] + ); +} + +/// Verifies static property compound assignments lower to one read-modify-write statement. +#[test] +fn parse_fragment_accepts_static_property_compound_assignment() { + let program = parse_fragment(br#"EvalStaticBox::$count += 2;"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StaticPropertySet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + value: EvalExpr::Binary { + op: EvalBinOp::Add, + left: Box::new(EvalExpr::StaticPropertyGet { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + }), + right: Box::new(EvalExpr::Const(EvalConst::Int(2))), + }, + }] + ); +} + +/// Verifies static property reference bindings parse for named and dynamic targets. +#[test] +fn parse_fragment_accepts_static_property_reference_bind_source() { + let program = parse_fragment( + br#"EvalStaticBox::$count =& $source; +$class::$count =& $dynamic; +EvalStaticBox::${$name} =& $other; +(factory())::${$name} =& $third;"#, + ) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertyReferenceBind { + class_name: "EvalStaticBox".to_string(), + property: "count".to_string(), + source: "source".to_string(), + }, + EvalStmt::DynamicStaticPropertyReferenceBind { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "count".to_string(), + source: "dynamic".to_string(), + }, + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr::ClassNameFetch { + class_name: "EvalStaticBox".to_string(), + }, + property: EvalExpr::LoadVar("name".to_string()), + source: "other".to_string(), + }, + EvalStmt::DynamicStaticPropertyNameReferenceBind { + class_name: EvalExpr::Call { + name: "factory".to_string(), + args: Vec::new(), + }, + property: EvalExpr::LoadVar("name".to_string()), + source: "third".to_string(), + }, + ] + ); +} + +/// Verifies indexed static-property writes parse as dedicated EvalIR statements. +#[test] +fn parse_fragment_accepts_static_property_array_write_source() { + let program = + parse_fragment(br#"EvalStaticBox::$items[0] = "x"; EvalStaticBox::$items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::StaticPropertyArraySet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::StaticPropertyArrayAppend { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} + +/// Verifies indexed static-property compound assignment retains the static target. +#[test] +fn parse_fragment_accepts_static_property_array_compound_assignment_source() { + let program = + parse_fragment(br#"EvalStaticBox::$items[0] .= "x";"#).expect("fragment should parse"); + assert_eq!( + program.statements(), + &[EvalStmt::StaticPropertyArraySet { + class_name: "EvalStaticBox".to_string(), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: Some(EvalBinOp::Concat), + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }] + ); +} + +/// Verifies runtime-valued static receivers support indexed static-property writes. +#[test] +fn parse_fragment_accepts_dynamic_static_property_array_write_source() { + let program = parse_fragment(br#"$class::$items[0] = "x"; $class::$items[] = "y";"#) + .expect("fragment should parse"); + assert_eq!( + program.statements(), + &[ + EvalStmt::DynamicStaticPropertyArraySet { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "items".to_string(), + index: EvalExpr::Const(EvalConst::Int(0)), + op: None, + value: EvalExpr::Const(EvalConst::String("x".to_string())), + }, + EvalStmt::DynamicStaticPropertyArrayAppend { + class_name: EvalExpr::LoadVar("class".to_string()), + property: "items".to_string(), + value: EvalExpr::Const(EvalConst::String("y".to_string())), + }, + ] + ); +} From 8b5cea329906eeed9563ee8ed02821d71b3bea5e Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 13 Jul 2026 13:27:40 +0200 Subject: [PATCH 1207/1208] refactor(magician): split scope and stream resources --- crates/elephc-magician/src/scope.rs | 172 +-- crates/elephc-magician/src/scope/tests.rs | 179 +++ .../elephc-magician/src/stream_resources.rs | 1220 +---------------- .../stream_resources/file_process_opening.rs | 98 ++ .../src/stream_resources/operations.rs | 397 ++++++ .../stream_resources/resource_registration.rs | 206 +++ .../src/stream_resources/sockets.rs | 116 ++ .../src/stream_resources/storage.rs | 119 ++ .../src/stream_resources/types.rs | 360 +++++ 9 files changed, 1485 insertions(+), 1382 deletions(-) create mode 100644 crates/elephc-magician/src/scope/tests.rs create mode 100644 crates/elephc-magician/src/stream_resources/file_process_opening.rs create mode 100644 crates/elephc-magician/src/stream_resources/operations.rs create mode 100644 crates/elephc-magician/src/stream_resources/resource_registration.rs create mode 100644 crates/elephc-magician/src/stream_resources/sockets.rs create mode 100644 crates/elephc-magician/src/stream_resources/storage.rs create mode 100644 crates/elephc-magician/src/stream_resources/types.rs diff --git a/crates/elephc-magician/src/scope.rs b/crates/elephc-magician/src/scope.rs index 15f23177b7..2f363349e9 100644 --- a/crates/elephc-magician/src/scope.rs +++ b/crates/elephc-magician/src/scope.rs @@ -436,174 +436,4 @@ impl Default for ElephcEvalScope { } #[cfg(test)] -mod tests { - use super::*; - - /// Verifies setting a variable records a visible dirty runtime-cell handle. - #[test] - fn set_records_visible_dirty_cell() { - let mut scope = ElephcEvalScope::new(); - let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); - let old = scope.set("x", cell, ScopeCellOwnership::Borrowed); - - let entry = scope.entry("x").expect("entry must exist"); - assert_eq!(old, None); - assert_eq!(entry.cell(), cell); - assert!(entry.flags().is_visible()); - assert!(entry.flags().dirty); - assert_eq!(entry.generation(), 1); - assert_eq!(scope.visible_cell("x"), Some(cell)); - } - - /// Verifies unsetting a variable creates a dirty marker that is not visible. - #[test] - fn unset_records_missing_dirty_marker() { - let mut scope = ElephcEvalScope::new(); - let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); - scope.set("x", cell, ScopeCellOwnership::Borrowed); - scope.mark_all_clean(); - let old = scope.unset("x"); - - let entry = scope.entry("x").expect("unset marker must exist"); - assert_eq!(old, None); - assert!(entry.flags().unset); - assert!(!entry.flags().is_visible()); - assert!(entry.flags().dirty); - assert_eq!(scope.visible_cell("x"), None); - assert_eq!(scope.dirty_names(), vec!["x"]); - } - - /// Verifies replacing an owned entry returns the old cell for release. - #[test] - fn set_returns_replaced_owned_cell() { - let mut scope = ElephcEvalScope::new(); - let old_cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); - let new_cell = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); - - scope.set("x", old_cell, ScopeCellOwnership::Owned); - let replaced = scope.set("x", new_cell, ScopeCellOwnership::Owned); - - assert_eq!(replaced, Some(old_cell)); - assert_eq!(scope.visible_cell("x"), Some(new_cell)); - } - - /// Verifies replacing an owned entry with the same cell does not release it. - #[test] - fn set_does_not_return_same_owned_cell() { - let mut scope = ElephcEvalScope::new(); - let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); - - scope.set("x", cell, ScopeCellOwnership::Owned); - let replaced = scope.set("x", cell, ScopeCellOwnership::Owned); - - assert_eq!(replaced, None); - } - - /// Verifies reference binding points two variable names at one runtime cell. - #[test] - fn set_reference_binds_names_to_source_cell() { - let mut scope = ElephcEvalScope::new(); - let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); - - scope.set("source", cell, ScopeCellOwnership::Owned); - let replaced = scope.set_reference( - "alias", - "source", - RuntimeCellHandle::from_raw(std::ptr::null_mut()), - ScopeCellOwnership::Owned, - ); - - let source = scope.entry("source").expect("source entry should exist"); - let alias = scope.entry("alias").expect("alias entry should exist"); - assert!(replaced.is_empty()); - assert_eq!(source.cell(), cell); - assert_eq!(alias.cell(), cell); - assert!(source.flags().by_ref); - assert!(alias.flags().by_ref); - assert_eq!(source.flags().ownership, ScopeCellOwnership::Owned); - assert_eq!(alias.flags().ownership, ScopeCellOwnership::Borrowed); - } - - /// Verifies writing through one reference alias updates every alias in the group. - #[test] - fn set_respecting_references_updates_alias_group() { - let mut scope = ElephcEvalScope::new(); - let old_cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); - let new_cell = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); - scope.set("source", old_cell, ScopeCellOwnership::Owned); - scope.set_reference( - "alias", - "source", - RuntimeCellHandle::from_raw(std::ptr::null_mut()), - ScopeCellOwnership::Owned, - ); - - let replaced = - scope.set_respecting_references("alias", new_cell, ScopeCellOwnership::Owned); - - assert_eq!(replaced, vec![old_cell]); - assert_eq!(scope.visible_cell("source"), Some(new_cell)); - assert_eq!(scope.visible_cell("alias"), Some(new_cell)); - assert_eq!( - scope.entry("alias").expect("alias").flags().ownership, - ScopeCellOwnership::Owned - ); - assert_eq!( - scope.entry("source").expect("source").flags().ownership, - ScopeCellOwnership::Borrowed - ); - } - - /// Verifies unsetting an owned alias transfers ownership to a remaining reference. - #[test] - fn unset_respecting_references_transfers_owned_cell() { - let mut scope = ElephcEvalScope::new(); - let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); - scope.set("source", cell, ScopeCellOwnership::Owned); - scope.set_reference( - "alias", - "source", - RuntimeCellHandle::from_raw(std::ptr::null_mut()), - ScopeCellOwnership::Owned, - ); - scope.set_respecting_references("alias", cell, ScopeCellOwnership::Owned); - - let replaced = scope.unset_respecting_references("alias"); - - assert_eq!(replaced, None); - assert!(scope.entry("alias").expect("alias").flags().unset); - assert_eq!( - scope.entry("source").expect("source").flags().ownership, - ScopeCellOwnership::Owned - ); - } - - /// Verifies local aliases can point at differently named globals. - #[test] - fn global_alias_to_records_target_name() { - let mut scope = ElephcEvalScope::new(); - - scope.mark_global_alias_to("alias", "source"); - - assert!(scope.is_global_alias("alias")); - assert_eq!(scope.global_alias_target("alias"), Some("source")); - assert_eq!(scope.global_alias_target("source"), None); - } - - /// Verifies draining a scope returns only visible owned cells. - #[test] - fn drain_owned_cells_returns_visible_owned_entries() { - let mut scope = ElephcEvalScope::new(); - let owned = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); - let borrowed = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); - scope.set("owned", owned, ScopeCellOwnership::Owned); - scope.set("borrowed", borrowed, ScopeCellOwnership::Borrowed); - scope.unset("borrowed"); - - let drained = scope.drain_owned_cells(); - - assert_eq!(drained, vec![owned]); - assert!(scope.entry("owned").is_none()); - assert!(scope.entry("borrowed").is_none()); - } -} +mod tests; diff --git a/crates/elephc-magician/src/scope/tests.rs b/crates/elephc-magician/src/scope/tests.rs new file mode 100644 index 0000000000..6a742b6051 --- /dev/null +++ b/crates/elephc-magician/src/scope/tests.rs @@ -0,0 +1,179 @@ +//! Purpose: +//! Unit tests for scope visibility, ownership, references, aliases, generations, +//! dirty markers, unset behavior, and draining. +//! +//! Called from: +//! - `cargo test -p elephc-magician` through Rust's test harness. +//! +//! Key details: +//! - Fake cell pointers are compared as opaque handles and never dereferenced. + +use super::*; + +/// Verifies setting a variable records a visible dirty runtime-cell handle. +#[test] +fn set_records_visible_dirty_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let old = scope.set("x", cell, ScopeCellOwnership::Borrowed); + + let entry = scope.entry("x").expect("entry must exist"); + assert_eq!(old, None); + assert_eq!(entry.cell(), cell); + assert!(entry.flags().is_visible()); + assert!(entry.flags().dirty); + assert_eq!(entry.generation(), 1); + assert_eq!(scope.visible_cell("x"), Some(cell)); +} + +/// Verifies unsetting a variable creates a dirty marker that is not visible. +#[test] +fn unset_records_missing_dirty_marker() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + scope.set("x", cell, ScopeCellOwnership::Borrowed); + scope.mark_all_clean(); + let old = scope.unset("x"); + + let entry = scope.entry("x").expect("unset marker must exist"); + assert_eq!(old, None); + assert!(entry.flags().unset); + assert!(!entry.flags().is_visible()); + assert!(entry.flags().dirty); + assert_eq!(scope.visible_cell("x"), None); + assert_eq!(scope.dirty_names(), vec!["x"]); +} + +/// Verifies replacing an owned entry returns the old cell for release. +#[test] +fn set_returns_replaced_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let old_cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let new_cell = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + + scope.set("x", old_cell, ScopeCellOwnership::Owned); + let replaced = scope.set("x", new_cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, Some(old_cell)); + assert_eq!(scope.visible_cell("x"), Some(new_cell)); +} + +/// Verifies replacing an owned entry with the same cell does not release it. +#[test] +fn set_does_not_return_same_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + + scope.set("x", cell, ScopeCellOwnership::Owned); + let replaced = scope.set("x", cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, None); +} + +/// Verifies reference binding points two variable names at one runtime cell. +#[test] +fn set_reference_binds_names_to_source_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + + scope.set("source", cell, ScopeCellOwnership::Owned); + let replaced = scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + + let source = scope.entry("source").expect("source entry should exist"); + let alias = scope.entry("alias").expect("alias entry should exist"); + assert!(replaced.is_empty()); + assert_eq!(source.cell(), cell); + assert_eq!(alias.cell(), cell); + assert!(source.flags().by_ref); + assert!(alias.flags().by_ref); + assert_eq!(source.flags().ownership, ScopeCellOwnership::Owned); + assert_eq!(alias.flags().ownership, ScopeCellOwnership::Borrowed); +} + +/// Verifies writing through one reference alias updates every alias in the group. +#[test] +fn set_respecting_references_updates_alias_group() { + let mut scope = ElephcEvalScope::new(); + let old_cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let new_cell = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + scope.set("source", old_cell, ScopeCellOwnership::Owned); + scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + + let replaced = + scope.set_respecting_references("alias", new_cell, ScopeCellOwnership::Owned); + + assert_eq!(replaced, vec![old_cell]); + assert_eq!(scope.visible_cell("source"), Some(new_cell)); + assert_eq!(scope.visible_cell("alias"), Some(new_cell)); + assert_eq!( + scope.entry("alias").expect("alias").flags().ownership, + ScopeCellOwnership::Owned + ); + assert_eq!( + scope.entry("source").expect("source").flags().ownership, + ScopeCellOwnership::Borrowed + ); +} + +/// Verifies unsetting an owned alias transfers ownership to a remaining reference. +#[test] +fn unset_respecting_references_transfers_owned_cell() { + let mut scope = ElephcEvalScope::new(); + let cell = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + scope.set("source", cell, ScopeCellOwnership::Owned); + scope.set_reference( + "alias", + "source", + RuntimeCellHandle::from_raw(std::ptr::null_mut()), + ScopeCellOwnership::Owned, + ); + scope.set_respecting_references("alias", cell, ScopeCellOwnership::Owned); + + let replaced = scope.unset_respecting_references("alias"); + + assert_eq!(replaced, None); + assert!(scope.entry("alias").expect("alias").flags().unset); + assert_eq!( + scope.entry("source").expect("source").flags().ownership, + ScopeCellOwnership::Owned + ); +} + +/// Verifies local aliases can point at differently named globals. +#[test] +fn global_alias_to_records_target_name() { + let mut scope = ElephcEvalScope::new(); + + scope.mark_global_alias_to("alias", "source"); + + assert!(scope.is_global_alias("alias")); + assert_eq!(scope.global_alias_target("alias"), Some("source")); + assert_eq!(scope.global_alias_target("source"), None); +} + +/// Verifies draining a scope returns only visible owned cells. +#[test] +fn drain_owned_cells_returns_visible_owned_entries() { + let mut scope = ElephcEvalScope::new(); + let owned = RuntimeCellHandle::from_raw(1usize as *mut crate::value::RuntimeCell); + let borrowed = RuntimeCellHandle::from_raw(2usize as *mut crate::value::RuntimeCell); + scope.set("owned", owned, ScopeCellOwnership::Owned); + scope.set("borrowed", borrowed, ScopeCellOwnership::Borrowed); + scope.unset("borrowed"); + + let drained = scope.drain_owned_cells(); + + assert_eq!(drained, vec![owned]); + assert!(scope.entry("owned").is_none()); + assert!(scope.entry("borrowed").is_none()); +} diff --git a/crates/elephc-magician/src/stream_resources.rs b/crates/elephc-magician/src/stream_resources.rs index 4c2d2c9e98..6525dfaddd 100644 --- a/crates/elephc-magician/src/stream_resources.rs +++ b/crates/elephc-magician/src/stream_resources.rs @@ -25,6 +25,15 @@ use std::process::{Child, Command, Stdio}; use crate::stream_wrappers; use crate::value::RuntimeCellHandle; +mod file_process_opening; +mod operations; +mod resource_registration; +mod sockets; +mod storage; +mod types; + +use types::*; + /// Eval-owned table of local file streams keyed by runtime resource payload. #[derive(Default)] pub(crate) struct EvalStreamResources { @@ -45,1214 +54,3 @@ pub(crate) struct EvalStreamResources { user_wrapper_directories: HashMap, user_wrapper_streams: HashMap, } - -impl EvalStreamResources { - /// Opens a local path using PHP's common `fopen()` mode strings. - pub(crate) fn open_path(&mut self, path: &str, mode: &str) -> Option { - let mode = EvalOpenMode::parse(mode)?; - if stream_wrappers::is_php_memory_stream(path) { - return self.open_ephemeral_stream(path, &mode, &[], None, false); - } - if stream_wrappers::is_data_stream(path) { - let bytes = stream_wrappers::decode_data_uri(path)?; - return self.open_ephemeral_stream(path, &mode, &bytes, None, false); - } - if stream_wrappers::is_phar_stream(path) { - return self.open_phar_stream(path, &mode); - } - if stream_wrappers::is_http_stream(path) && mode.read && !mode.write { - let bytes = stream_wrappers::read_http_url(path)?; - return self.open_ephemeral_stream(path, &mode, &bytes, None, false); - } - let path = stream_wrappers::local_filesystem_path(path)?; - let file = mode.open(&path).ok()?; - Some(self.insert(EvalFileStream::new(file, path, mode.label))) - } - - /// Opens an anonymous temporary file and returns its resource id. - pub(crate) fn open_tmpfile(&mut self) -> Option { - let path = eval_tmpfile_path(); - let file = OpenOptions::new() - .read(true) - .write(true) - .create_new(true) - .open(&path) - .ok()?; - let _ = std::fs::remove_file(&path); - Some(self.insert(EvalFileStream::new( - file, - path.to_string_lossy().into_owned(), - "w+".to_string(), - ))) - } - - /// Opens a shell process pipe and returns its stream resource id. - pub(crate) fn open_process_pipe(&mut self, command: &str, mode: &str) -> Option { - let read_mode = match mode.chars().next()? { - 'r' => true, - 'w' => false, - _ => return None, - }; - let mut child = Command::new("/bin/sh") - .arg("-c") - .arg(command) - .stdin(if read_mode { - Stdio::null() - } else { - Stdio::piped() - }) - .stdout(if read_mode { - Stdio::piped() - } else { - Stdio::null() - }) - .spawn() - .ok()?; - let file = if read_mode { - let stdout = child.stdout.take()?; - unsafe { - // The ChildStdout pipe is converted into the File that backs - // this eval stream; no second owner keeps the fd alive. - File::from_raw_fd(stdout.into_raw_fd()) - } - } else { - let stdin = child.stdin.take()?; - unsafe { - // The ChildStdin pipe is converted into the File that backs - // this eval stream; dropping it before wait sends EOF. - File::from_raw_fd(stdin.into_raw_fd()) - } - }; - let id = self.insert(EvalFileStream::new( - file, - command.to_string(), - if read_mode { "r" } else { "w" }.to_string(), - )); - self.process_children.insert(id, child); - Some(id) - } - - /// Opens a TCP listener resource for `stream_socket_server()`. - pub(crate) fn open_tcp_listener(&mut self, address: &str) -> Option { - let listener = TcpListener::bind(eval_tcp_address(address)).ok()?; - let local = listener.local_addr().ok()?.to_string(); - let id = self.next_id; - self.next_id += 1; - self.socket_names.insert( - id, - EvalSocketNames { - local, - peer: None, - }, - ); - self.socket_listeners.insert(id, listener); - Some(id) - } - - /// Opens a connected TCP stream resource. - pub(crate) fn open_tcp_stream(&mut self, address: &str) -> Option { - self.open_tcp_stream_result(address).ok() - } - - /// Opens a connected TCP stream resource and preserves the host I/O error on failure. - pub(crate) fn open_tcp_stream_result(&mut self, address: &str) -> io::Result { - let stream = TcpStream::connect(eval_tcp_address(address))?; - self.insert_tcp_stream(stream).ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, "failed to track eval TCP stream") - }) - } - - /// Opens a connected TCP stream from separate host and port arguments. - pub(crate) fn open_tcp_stream_host_port(&mut self, host: &str, port: i64) -> Option { - self.open_tcp_stream_host_port_result(host, port).ok() - } - - /// Opens a connected TCP stream from host and port while preserving I/O errors. - pub(crate) fn open_tcp_stream_host_port_result( - &mut self, - host: &str, - port: i64, - ) -> io::Result { - let host = host - .strip_prefix("tcp://") - .or_else(|| host.strip_prefix("ssl://")) - .or_else(|| host.strip_prefix("tls://")) - .unwrap_or(host); - self.open_tcp_stream_result(&format!("{host}:{port}")) - } - - /// Accepts one TCP connection from a listener resource. - pub(crate) fn accept_tcp(&mut self, id: i64) -> Option { - let listener = self.socket_listeners.get(&id)?; - let (stream, _) = listener.accept().ok()?; - self.insert_tcp_stream(stream) - } - - /// Opens a pair of connected local stream resources. - pub(crate) fn open_socket_pair(&mut self) -> Option<(i64, i64)> { - #[cfg(unix)] - { - let (left, right) = UnixStream::pair().ok()?; - let left = unsafe { - // The UnixStream endpoint is moved into the File-backed eval stream. - File::from_raw_fd(left.into_raw_fd()) - }; - let right = unsafe { - // The UnixStream endpoint is moved into the File-backed eval stream. - File::from_raw_fd(right.into_raw_fd()) - }; - let left_id = self.insert(EvalFileStream::new( - left, - "socketpair".to_string(), - "r+".to_string(), - )); - let right_id = self.insert(EvalFileStream::new( - right, - "socketpair".to_string(), - "r+".to_string(), - )); - self.socket_names.insert( - left_id, - EvalSocketNames { - local: "socketpair".to_string(), - peer: Some("socketpair".to_string()), - }, - ); - self.socket_names.insert( - right_id, - EvalSocketNames { - local: "socketpair".to_string(), - peer: Some("socketpair".to_string()), - }, - ); - Some((left_id, right_id)) - } - #[cfg(not(unix))] - { - None - } - } - - /// Opens a local directory and returns its resource id. - pub(crate) fn open_directory(&mut self, path: &str) -> Option { - let directory = EvalDirectoryStream::open(path)?; - Some(self.insert_directory(directory)) - } - - /// Opens an incremental hash context and returns its resource id. - pub(crate) fn open_hash_context(&mut self, algo: &[u8]) -> Option { - let handle = unsafe { - // elephc-crypto reads the algorithm name during this call and returns - // an owned opaque context handle on success. - elephc_crypto::elephc_crypto_init(algo.as_ptr(), algo.len()) - }; - if handle.is_null() { - return None; - } - Some(self.insert_hash_context(EvalHashContext { handle })) - } - - /// Opens a stream context resource with optional persisted options. - pub(crate) fn open_stream_context(&mut self, options: Option) -> i64 { - self.insert_stream_context(EvalStreamContext { options }) - } - - /// Registers a user stream wrapper protocol and class in eval-local state. - pub(crate) fn register_stream_wrapper( - &mut self, - protocol: &str, - class_name: &str, - builtins: &[&str], - ) -> bool { - let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { - return false; - }; - if self - .user_stream_wrappers - .iter() - .any(|current| current.eq_ignore_ascii_case(&protocol)) - { - return false; - } - if eval_builtin_stream_wrapper_exists(builtins, &protocol) - && !self.disabled_builtin_stream_wrappers.contains(&protocol) - { - return false; - } - self.user_stream_wrapper_classes - .insert(protocol.clone(), class_name.to_string()); - self.user_stream_wrappers.push(protocol); - true - } - - /// Unregisters a user or built-in stream wrapper protocol. - pub(crate) fn unregister_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { - let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { - return false; - }; - if let Some(index) = self - .user_stream_wrappers - .iter() - .position(|current| current.eq_ignore_ascii_case(&protocol)) - { - let protocol = self.user_stream_wrappers.remove(index); - self.user_stream_wrapper_classes.remove(&protocol); - return true; - } - if eval_builtin_stream_wrapper_exists(builtins, &protocol) { - return self.disabled_builtin_stream_wrappers.insert(protocol); - } - false - } - - /// Restores a built-in stream wrapper protocol or accepts no-op user restores. - pub(crate) fn restore_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { - let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { - return false; - }; - if eval_builtin_stream_wrapper_exists(builtins, &protocol) { - self.disabled_builtin_stream_wrappers.remove(&protocol); - } - true - } - - /// Returns the currently visible stream wrapper protocol list. - pub(crate) fn stream_wrappers(&self, builtins: &[&str]) -> Vec { - let mut wrappers = Vec::with_capacity(builtins.len() + self.user_stream_wrappers.len()); - for builtin in builtins { - if !self.disabled_builtin_stream_wrappers.contains(*builtin) { - wrappers.push((*builtin).to_string()); - } - } - wrappers.extend(self.user_stream_wrappers.iter().cloned()); - wrappers - } - - /// Returns the registered userspace wrapper class for a URL scheme. - pub(crate) fn user_stream_wrapper_class_for_path(&self, path: &str) -> Option { - let scheme = stream_wrappers::stream_scheme(path)?; - self.user_stream_wrapper_classes.get(&scheme).cloned() - } - - /// Opens a userspace wrapper stream around an eval-created wrapper object. - pub(crate) fn open_user_wrapper_stream( - &mut self, - object: RuntimeCellHandle, - class_name: &str, - uri: &str, - mode: &str, - ) -> i64 { - let id = self.next_id; - self.next_id += 1; - self.user_wrapper_streams.insert( - id, - EvalUserWrapperStream { - object, - class_name: class_name.to_string(), - uri: uri.to_string(), - mode: mode.to_string(), - eof: false, - }, - ); - id - } - - /// Opens a userspace wrapper directory around an eval-created wrapper object. - pub(crate) fn open_user_wrapper_directory( - &mut self, - object: RuntimeCellHandle, - class_name: &str, - ) -> i64 { - let id = self.next_id; - self.next_id += 1; - self.user_wrapper_directories.insert( - id, - EvalUserWrapperDirectory { - object, - class_name: class_name.to_string(), - }, - ); - id - } - - /// Returns a copied userspace-wrapper stream descriptor for dispatch. - pub(crate) fn user_wrapper_stream_info( - &self, - id: i64, - ) -> Option { - self.user_wrapper_streams - .get(&id) - .map(EvalUserWrapperStream::info) - } - - /// Returns a copied userspace-wrapper directory descriptor for dispatch. - pub(crate) fn user_wrapper_directory_info( - &self, - id: i64, - ) -> Option { - self.user_wrapper_directories - .get(&id) - .map(EvalUserWrapperDirectory::info) - } - - /// Removes a userspace-wrapper directory and returns its descriptor. - pub(crate) fn close_user_wrapper_directory( - &mut self, - id: i64, - ) -> Option { - self.user_wrapper_directories - .remove(&id) - .map(|directory| directory.info()) - } - - /// Updates the cached EOF state for a userspace-wrapper stream. - pub(crate) fn set_user_wrapper_eof(&mut self, id: i64, eof: bool) -> bool { - let Some(stream) = self.user_wrapper_streams.get_mut(&id) else { - return false; - }; - stream.eof = eof; - true - } - - /// Returns the default stream context resource id, creating it if needed. - pub(crate) fn default_stream_context(&mut self) -> i64 { - if let Some(id) = self.default_stream_context { - return id; - } - let id = self.open_stream_context(None); - self.default_stream_context = Some(id); - id - } - - /// Removes a stream resource from the table, closing its file handle. - pub(crate) fn close(&mut self, id: i64) -> bool { - let mut closed = false; - let mut ok = true; - if let Some(stream) = self.streams.remove(&id) { - closed = true; - ok = stream.finalize_on_close(); - } - closed = closed - || self.user_wrapper_streams.remove(&id).is_some() - || self.filter_resources.remove(&id) - || self.socket_listeners.remove(&id).is_some(); - self.socket_names.remove(&id); - if let Some(mut child) = self.process_children.remove(&id) { - let _ = child.wait(); - } - closed && ok - } - - /// Returns whether a file-like stream resource exists. - pub(crate) fn has_stream(&self, id: i64) -> bool { - self.streams.contains_key(&id) || self.user_wrapper_streams.contains_key(&id) - } - - /// Returns a local or remote socket name for a socket resource. - pub(crate) fn socket_name(&self, id: i64, remote: bool) -> Option { - let names = self.socket_names.get(&id)?; - if remote { - names.peer.clone() - } else { - Some(names.local.clone()) - } - } - - /// Applies a TCP/Unix stream shutdown operation. - pub(crate) fn socket_shutdown(&self, id: i64, mode: i64) -> Option { - let stream = self.streams.get(&id)?; - let shutdown = match mode { - 0 => Shutdown::Read, - 1 => Shutdown::Write, - 2 => Shutdown::Both, - _ => return Some(false), - }; - let result = unsafe { - // libc shutdown only observes the borrowed descriptor and mode. - libc::shutdown(stream.file.as_raw_fd(), eval_shutdown_how(shutdown)) - }; - Some(result == 0) - } - - /// Allocates an eval-local stream filter resource handle. - pub(crate) fn open_filter_resource(&mut self) -> i64 { - let id = self.next_id; - self.next_id += 1; - self.filter_resources.insert(id); - id - } - - /// Removes an eval-local stream filter resource handle. - pub(crate) fn close_filter_resource(&mut self, id: i64) -> bool { - self.filter_resources.remove(&id) - } - - /// Closes a process pipe stream and returns the child exit status. - pub(crate) fn pclose(&mut self, id: i64) -> Option { - let mut child = self.process_children.remove(&id)?; - self.streams.remove(&id)?; - let status = child.wait().ok()?; - Some(status.code().unwrap_or(0) as i64) - } - - /// Removes a directory resource from the table. - pub(crate) fn close_directory(&mut self, id: i64) -> bool { - self.directories.remove(&id).is_some() - } - - /// Reads up to `length` bytes from a stream resource. - pub(crate) fn read(&mut self, id: i64, length: usize) -> Option> { - let stream = self.streams.get_mut(&id)?; - let mut buffer = vec![0_u8; length]; - let read = stream.file.read(&mut buffer).ok()?; - buffer.truncate(read); - stream.eof = read == 0 || read < length; - Some(buffer) - } - - /// Reads the next entry name from a directory resource. - pub(crate) fn read_directory(&mut self, id: i64) -> Option { - self.directories.get_mut(&id)?.read() - } - - /// Feeds bytes into an incremental hash context. - pub(crate) fn update_hash_context(&mut self, id: i64, data: &[u8]) -> bool { - let Some(context) = self.hash_contexts.get_mut(&id) else { - return false; - }; - unsafe { - // The table owns the opaque handle and this mutable borrow gives the - // crypto call exclusive access for the duration of the update. - elephc_crypto::elephc_crypto_update(context.handle, data.as_ptr(), data.len()); - } - true - } - - /// Returns the persisted options for a stream context resource. - pub(crate) fn stream_context_options(&self, id: i64) -> Option { - self.stream_contexts.get(&id).and_then(|context| context.options) - } - - /// Replaces persisted options for a stream context resource. - pub(crate) fn set_stream_context_options( - &mut self, - id: i64, - options: Option, - ) -> bool { - let Some(context) = self.stream_contexts.get_mut(&id) else { - return false; - }; - context.options = options; - true - } - - /// Reads one stream line up to a limit, newline, or custom delimiter. - pub(crate) fn read_line( - &mut self, - id: i64, - length: usize, - ending: Option<&[u8]>, - include_ending: bool, - stop_at_newline: bool, - ) -> Option> { - let stream = self.streams.get_mut(&id)?; - let mut output = Vec::new(); - let mut byte = [0_u8; 1]; - while output.len() < length { - let read = stream.file.read(&mut byte).ok()?; - if read == 0 { - stream.eof = true; - break; - } - output.push(byte[0]); - if let Some(ending) = ending { - if !ending.is_empty() && output.ends_with(ending) { - if !include_ending { - output.truncate(output.len().saturating_sub(ending.len())); - } - break; - } - } else if stop_at_newline && byte[0] == b'\n' { - break; - } - } - Some(output) - } - - /// Writes all provided bytes to a stream resource and returns the written byte count. - pub(crate) fn write(&mut self, id: i64, data: &[u8]) -> Option { - let stream = self.streams.get_mut(&id)?; - let written = stream.file.write(data).ok()?; - stream.eof = false; - Some(written) - } - - /// Flushes buffered stream data to the host file handle. - pub(crate) fn flush(&mut self, id: i64) -> bool { - self.streams - .get_mut(&id) - .is_some_and(|stream| stream.file.flush().is_ok()) - } - - /// Returns whether a stream's file descriptor is attached to a terminal. - pub(crate) fn isatty(&self, id: i64) -> Option { - let stream = self.streams.get(&id)?; - let result = unsafe { - // libc only reads the descriptor value during the terminal probe. - libc::isatty(stream.file.as_raw_fd()) - }; - Some(result == 1) - } - - /// Toggles blocking mode on a stream's file descriptor. - pub(crate) fn set_blocking(&self, id: i64, enable: bool) -> Option { - let stream = self.streams.get(&id)?; - let fd = stream.file.as_raw_fd(); - let flags = unsafe { - // fcntl reads the current descriptor flags without taking ownership. - libc::fcntl(fd, libc::F_GETFL) - }; - if flags < 0 { - return Some(false); - } - let flags = if enable { - flags & !libc::O_NONBLOCK - } else { - flags | libc::O_NONBLOCK - }; - let result = unsafe { - // fcntl updates the descriptor flags in place. - libc::fcntl(fd, libc::F_SETFL, flags) - }; - Some(result == 0) - } - - /// Reports timeout-setting support for local file streams. - pub(crate) fn set_timeout(&self, id: i64, _seconds: i64, _microseconds: i64) -> Option { - self.streams.get(&id).map(|_| false) - } - - /// Stores a per-stream chunk size and returns the previous size. - pub(crate) fn set_chunk_size(&mut self, id: i64, size: i64) -> Option { - if !self.streams.contains_key(&id) || size <= 0 { - return None; - } - Some(self.chunk_sizes.insert(id, size).unwrap_or(8192)) - } - - /// Accepts read/write buffer settings for local file streams. - pub(crate) fn set_buffer(&self, id: i64, _size: i64) -> Option { - self.streams.get(&id).map(|_| 0) - } - - /// Applies an advisory lock operation to a stream's backing file descriptor. - pub(crate) fn flock(&self, id: i64, operation: i64) -> Option<(bool, bool)> { - let stream = self.streams.get(&id)?; - let operation = eval_flock_operation(operation)?; - let result = unsafe { - // libc only observes the borrowed raw fd during this call. - libc::flock(stream.file.as_raw_fd(), operation) - }; - if result == 0 { - Some((true, false)) - } else { - Some((false, eval_flock_would_block())) - } - } - - /// Synchronizes stream data and metadata to storage. - pub(crate) fn sync_all(&mut self, id: i64) -> bool { - self.streams - .get_mut(&id) - .is_some_and(|stream| stream.file.sync_all().is_ok()) - } - - /// Synchronizes stream data to storage where the host platform supports it. - pub(crate) fn sync_data(&mut self, id: i64) -> bool { - self.streams - .get_mut(&id) - .is_some_and(|stream| stream.file.sync_data().is_ok()) - } - - /// Returns whether the stream has reached EOF after the last read attempt. - pub(crate) fn eof(&self, id: i64) -> Option { - self.streams - .get(&id) - .map(|stream| stream.eof) - .or_else(|| self.user_wrapper_streams.get(&id).map(|stream| stream.eof)) - } - - /// Returns the current stream cursor offset. - pub(crate) fn tell(&mut self, id: i64) -> Option { - self.streams.get_mut(&id)?.file.stream_position().ok() - } - - /// Moves the stream cursor according to PHP `fseek()` whence values. - pub(crate) fn seek(&mut self, id: i64, offset: i64, whence: i64) -> bool { - let Some(stream) = self.streams.get_mut(&id) else { - return false; - }; - let position = match whence { - 0 => SeekFrom::Start(u64::try_from(offset).unwrap_or(u64::MAX)), - 1 => SeekFrom::Current(offset), - 2 => SeekFrom::End(offset), - _ => return false, - }; - stream.eof = false; - stream.file.seek(position).is_ok() - } - - /// Rewinds a stream to the beginning. - pub(crate) fn rewind(&mut self, id: i64) -> bool { - self.seek(id, 0, 0) - } - - /// Rewinds a directory resource to its first entry. - pub(crate) fn rewind_directory(&mut self, id: i64) -> bool { - self.directories - .get_mut(&id) - .is_some_and(EvalDirectoryStream::rewind) - } - - /// Finalizes and removes an incremental hash context, returning raw digest bytes. - pub(crate) fn finalize_hash_context(&mut self, id: i64) -> Option> { - let context = self.hash_contexts.remove(&id)?; - let mut output = [0_u8; 64]; - let len = unsafe { - // elephc-crypto consumes and frees the owned context handle here. - elephc_crypto::elephc_crypto_final(context.handle, output.as_mut_ptr()) - }; - eval_hash_digest_bytes(len, &output) - } - - /// Clones an incremental hash context into a new resource id. - pub(crate) fn copy_hash_context(&mut self, id: i64) -> Option { - let context = self.hash_contexts.get(&id)?; - let handle = unsafe { - // elephc-crypto returns a deep clone with independent ownership. - elephc_crypto::elephc_crypto_clone(context.handle) - }; - if handle.is_null() { - return None; - } - Some(self.insert_hash_context(EvalHashContext { handle })) - } - - /// Truncates a stream to the requested byte length. - pub(crate) fn truncate(&mut self, id: i64, size: u64) -> bool { - self.streams - .get_mut(&id) - .is_some_and(|stream| stream.file.set_len(size).is_ok()) - } - - /// Returns host metadata for a stream's backing file handle. - pub(crate) fn metadata(&self, id: i64) -> Option { - self.streams - .get(&id) - .and_then(|stream| stream.file.metadata().ok()) - } - - /// Reads a full or bounded byte sequence from the stream, with optional offset seek. - pub(crate) fn get_contents( - &mut self, - id: i64, - length: Option, - offset: Option, - ) -> Option> { - if let Some(offset) = offset { - if !self.seek(id, offset, 0) { - return None; - } - } - match length { - Some(length) => self.read(id, length), - None => { - let stream = self.streams.get_mut(&id)?; - let mut bytes = Vec::new(); - stream.file.read_to_end(&mut bytes).ok()?; - stream.eof = true; - Some(bytes) - } - } - } - - /// Copies bytes between two streams and returns the copied byte count. - pub(crate) fn copy_to_stream( - &mut self, - from: i64, - to: i64, - length: Option, - offset: Option, - ) -> Option { - let bytes = self.get_contents(from, length, offset)?; - self.write(to, &bytes) - } - - /// Returns metadata fields used by PHP `stream_get_meta_data()`. - pub(crate) fn meta_data(&self, id: i64) -> Option { - if let Some(stream) = self.streams.get(&id) { - return Some(EvalStreamMetaData { - eof: stream.eof, - mode: stream.mode.clone(), - uri: stream.uri.clone(), - }); - } - let stream = self.user_wrapper_streams.get(&id)?; - Some(EvalStreamMetaData { - eof: stream.eof, - mode: stream.mode.clone(), - uri: stream.uri.clone(), - }) - } - - /// Inserts a file stream and returns the assigned zero-based resource payload. - fn insert(&mut self, stream: EvalFileStream) -> i64 { - let id = self.next_id; - self.next_id += 1; - self.streams.insert(id, stream); - id - } - - /// Opens one unlinked temporary file as the backing storage for wrapper streams. - fn open_ephemeral_stream( - &mut self, - uri: &str, - mode: &EvalOpenMode, - initial: &[u8], - flush_target: Option, - append: bool, - ) -> Option { - let path = eval_tmpfile_path(); - let mut file = OpenOptions::new() - .read(true) - .write(true) - .create_new(true) - .open(&path) - .ok()?; - let _ = std::fs::remove_file(&path); - file.write_all(initial).ok()?; - if append { - file.seek(SeekFrom::End(0)).ok()?; - } else { - file.seek(SeekFrom::Start(0)).ok()?; - } - Some(self.insert(EvalFileStream::new_with_flush_target( - file, - uri.to_string(), - mode.label.clone(), - flush_target, - ))) - } - - /// Opens a `phar://` entry for reading or buffered write-back on close. - fn open_phar_stream(&mut self, path: &str, mode: &EvalOpenMode) -> Option { - let url = path.as_bytes(); - if mode.write { - let initial = if mode.truncate { - Vec::new() - } else { - match elephc_phar::extract_url_bytes(url) { - Some(bytes) => bytes, - None if mode.create => Vec::new(), - None => return None, - } - }; - return self.open_ephemeral_stream( - path, - mode, - &initial, - Some(EvalStreamFlushTarget::PharUrl(url.to_vec())), - mode.append, - ); - } - let bytes = elephc_phar::extract_url_bytes(url)?; - self.open_ephemeral_stream(path, mode, &bytes, None, false) - } - - /// Inserts a TCP stream as a File-backed eval stream and records endpoint names. - fn insert_tcp_stream(&mut self, stream: TcpStream) -> Option { - let local = stream.local_addr().ok()?.to_string(); - let peer = stream.peer_addr().ok().map(|addr| addr.to_string()); - let file = unsafe { - // The TcpStream is moved into the File-backed eval stream. - File::from_raw_fd(stream.into_raw_fd()) - }; - let id = self.insert(EvalFileStream::new(file, local.clone(), "r+".to_string())); - self.socket_names - .insert(id, EvalSocketNames { local, peer }); - Some(id) - } - - /// Inserts a directory stream and returns the assigned zero-based resource payload. - fn insert_directory(&mut self, directory: EvalDirectoryStream) -> i64 { - let id = self.next_id; - self.next_id += 1; - self.directories.insert(id, directory); - id - } - - /// Inserts a hash context and returns the assigned zero-based resource payload. - fn insert_hash_context(&mut self, context: EvalHashContext) -> i64 { - let id = self.next_id; - self.next_id += 1; - self.hash_contexts.insert(id, context); - id - } - - /// Inserts a stream context and returns the assigned zero-based resource payload. - fn insert_stream_context(&mut self, context: EvalStreamContext) -> i64 { - let id = self.next_id; - self.next_id += 1; - self.stream_contexts.insert(id, context); - id - } -} - -impl Drop for EvalStreamResources { - /// Frees any incremental hash contexts that were never finalized. - fn drop(&mut self) { - for context in self.hash_contexts.drain().map(|(_, context)| context) { - unsafe { - // The resource table owns these handles; draining prevents reuse - // after the crypto free call. - elephc_crypto::elephc_crypto_free(context.handle); - } - } - } -} - -/// PHP-visible metadata for one eval stream resource. -pub(crate) struct EvalStreamMetaData { - pub(crate) eof: bool, - pub(crate) mode: String, - pub(crate) uri: String, -} - -/// Local and peer names tracked for socket-backed eval streams. -struct EvalSocketNames { - local: String, - peer: Option, -} - -/// Normalizes supported TCP-style stream socket addresses. -fn eval_tcp_address(address: &str) -> &str { - address - .strip_prefix("tcp://") - .or_else(|| address.strip_prefix("ssl://")) - .or_else(|| address.strip_prefix("tls://")) - .unwrap_or(address) -} - -/// Converts Rust's socket shutdown enum into libc constants. -fn eval_shutdown_how(shutdown: Shutdown) -> libc::c_int { - match shutdown { - Shutdown::Read => libc::SHUT_RD, - Shutdown::Write => libc::SHUT_WR, - Shutdown::Both => libc::SHUT_RDWR, - } -} - -/// Converts PHP `LOCK_*` bit flags into host `flock()` flags. -fn eval_flock_operation(operation: i64) -> Option { - let non_blocking = operation & 4 != 0; - let base = match operation & !4 { - 1 => libc::LOCK_SH, - 2 => libc::LOCK_EX, - 3 => libc::LOCK_UN, - _ => return None, - }; - Some(base | if non_blocking { libc::LOCK_NB } else { 0 }) -} - -/// Returns whether the last host `flock()` failure was a non-blocking lock miss. -fn eval_flock_would_block() -> bool { - let errno = std::io::Error::last_os_error().raw_os_error(); - errno.is_some_and(|code| code == libc::EWOULDBLOCK || code == libc::EAGAIN) -} - -/// Converts an elephc-crypto digest length into owned raw bytes. -fn eval_hash_digest_bytes(len: isize, output: &[u8; 64]) -> Option> { - let len = usize::try_from(len).ok()?; - if len > output.len() { - return None; - } - Some(output[..len].to_vec()) -} - -/// Normalizes a PHP stream wrapper protocol name for eval registry storage. -fn eval_normalize_stream_wrapper_protocol(protocol: &str) -> Option { - let protocol = protocol.trim().trim_end_matches("://"); - if protocol.is_empty() { - return None; - } - Some(protocol.to_ascii_lowercase()) -} - -/// Returns whether the protocol is one of elephc's built-in stream wrappers. -fn eval_builtin_stream_wrapper_exists(builtins: &[&str], protocol: &str) -> bool { - builtins - .iter() - .any(|builtin| builtin.eq_ignore_ascii_case(protocol)) -} - -/// File stream stored behind one eval resource id. -struct EvalFileStream { - file: File, - uri: String, - mode: String, - eof: bool, - flush_target: Option, -} - -impl EvalFileStream { - /// Creates a tracked stream around a host file handle. - fn new(file: File, uri: String, mode: String) -> Self { - Self::new_with_flush_target(file, uri, mode, None) - } - - /// Creates a tracked stream that may write back to a wrapper target on close. - fn new_with_flush_target( - file: File, - uri: String, - mode: String, - flush_target: Option, - ) -> Self { - Self { - file, - uri, - mode, - eof: false, - flush_target, - } - } - - /// Flushes any buffered wrapper target before the stream resource disappears. - fn finalize_on_close(mut self) -> bool { - let Some(flush_target) = self.flush_target.take() else { - return true; - }; - let mut bytes = Vec::new(); - if self.file.flush().is_err() || self.file.seek(SeekFrom::Start(0)).is_err() { - return false; - } - if self.file.read_to_end(&mut bytes).is_err() { - return false; - } - flush_target.write_back(&bytes) - } -} - -/// Userspace wrapper stream stored behind one eval resource id. -struct EvalUserWrapperStream { - object: RuntimeCellHandle, - class_name: String, - uri: String, - mode: String, - eof: bool, -} - -impl EvalUserWrapperStream { - /// Copies the dispatch-relevant wrapper fields out of the resource table. - fn info(&self) -> EvalUserWrapperStreamInfo { - EvalUserWrapperStreamInfo { - object: self.object, - class_name: self.class_name.clone(), - eof: self.eof, - } - } -} - -/// Copied userspace-wrapper stream fields used while dispatching PHP methods. -pub(crate) struct EvalUserWrapperStreamInfo { - pub(crate) object: RuntimeCellHandle, - pub(crate) class_name: String, - pub(crate) eof: bool, -} - -/// Userspace-wrapper directory stored behind one eval resource id. -struct EvalUserWrapperDirectory { - object: RuntimeCellHandle, - class_name: String, -} - -impl EvalUserWrapperDirectory { - /// Copies the dispatch fields needed while invoking wrapper directory methods. - fn info(&self) -> EvalUserWrapperDirectoryInfo { - EvalUserWrapperDirectoryInfo { - object: self.object, - class_name: self.class_name.clone(), - } - } -} - -/// Copied userspace-wrapper directory fields used while dispatching PHP methods. -pub(crate) struct EvalUserWrapperDirectoryInfo { - pub(crate) object: RuntimeCellHandle, - pub(crate) class_name: String, -} - -/// Wrapper targets that need a write-back step when their stream closes. -enum EvalStreamFlushTarget { - PharUrl(Vec), -} - -impl EvalStreamFlushTarget { - /// Writes buffered stream bytes back to the target URL. - fn write_back(&self, bytes: &[u8]) -> bool { - match self { - Self::PharUrl(url) => elephc_phar::put_url_bytes(url, bytes).is_some(), - } - } -} - -/// Directory stream stored behind one eval resource id. -struct EvalDirectoryStream { - entries: Vec, - index: usize, -} - -impl EvalDirectoryStream { - /// Opens a local directory and snapshots its entry names. - fn open(path: &str) -> Option { - let entries = std::fs::read_dir(path).ok()?; - let mut names = vec![".".to_string(), "..".to_string()]; - for entry in entries { - let entry = entry.ok()?; - names.push(entry.file_name().to_string_lossy().into_owned()); - } - Some(Self { - entries: names, - index: 0, - }) - } - - /// Returns the next directory entry name. - fn read(&mut self) -> Option { - let name = self.entries.get(self.index)?.clone(); - self.index += 1; - Some(name) - } - - /// Moves the directory cursor back to its first entry. - fn rewind(&mut self) -> bool { - self.index = 0; - true - } -} - -/// Opaque elephc-crypto incremental hash context resource. -struct EvalHashContext { - handle: *mut c_void, -} - -/// Stream context metadata tracked by eval. -struct EvalStreamContext { - options: Option, -} - -/// Parsed PHP fopen mode used to configure `OpenOptions`. -struct EvalOpenMode { - read: bool, - write: bool, - append: bool, - truncate: bool, - create: bool, - create_new: bool, - label: String, -} - -impl EvalOpenMode { - /// Parses PHP's common fopen mode grammar, ignoring binary/text markers. - fn parse(mode: &str) -> Option { - let mut chars = mode.chars(); - let first = chars.next()?; - let plus = mode.contains('+'); - if !mode - .chars() - .all(|ch| matches!(ch, 'r' | 'w' | 'a' | 'x' | 'c' | '+' | 'b' | 't' | 'e')) - { - return None; - } - let mut mode = match first { - 'r' => Self { - read: true, - write: plus, - append: false, - truncate: false, - create: false, - create_new: false, - label: if plus { "r+" } else { "r" }.to_string(), - }, - 'w' => Self { - read: plus, - write: true, - append: false, - truncate: true, - create: true, - create_new: false, - label: if plus { "w+" } else { "w" }.to_string(), - }, - 'a' => Self { - read: plus, - write: true, - append: true, - truncate: false, - create: true, - create_new: false, - label: if plus { "a+" } else { "a" }.to_string(), - }, - 'x' => Self { - read: plus, - write: true, - append: false, - truncate: false, - create: false, - create_new: true, - label: if plus { "x+" } else { "x" }.to_string(), - }, - 'c' => Self { - read: plus, - write: true, - append: false, - truncate: false, - create: true, - create_new: false, - label: if plus { "c+" } else { "c" }.to_string(), - }, - _ => return None, - }; - mode.write = mode.write || plus; - Some(mode) - } - - /// Opens a path with the parsed stream mode. - fn open(&self, path: &str) -> std::io::Result { - OpenOptions::new() - .read(self.read) - .write(self.write) - .append(self.append) - .truncate(self.truncate) - .create(self.create) - .create_new(self.create_new) - .open(path) - } -} - -/// Builds a unique temporary path for eval `tmpfile()`. -fn eval_tmpfile_path() -> PathBuf { - let mut path = std::env::temp_dir(); - path.push(format!( - "elephc-magician-tmpfile-{}-{}", - std::process::id(), - eval_tmpfile_nonce() - )); - path -} - -/// Returns a monotonic-ish nonce for temporary file names. -fn eval_tmpfile_nonce() -> u128 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_nanos()) - .unwrap_or(0) -} diff --git a/crates/elephc-magician/src/stream_resources/file_process_opening.rs b/crates/elephc-magician/src/stream_resources/file_process_opening.rs new file mode 100644 index 0000000000..7c9d8fa5b1 --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/file_process_opening.rs @@ -0,0 +1,98 @@ +//! Purpose: +//! Opens local, temporary, process-pipe, memory, and Phar-backed eval streams. +//! +//! Called from: +//! - Filesystem and process stream builtins through `EvalStreamResources`. +//! +//! Key details: +//! - PHP mode parsing and write-back targets are delegated to shared storage types. + +use super::*; + +impl EvalStreamResources { + /// Opens a local path using PHP's common `fopen()` mode strings. + pub(crate) fn open_path(&mut self, path: &str, mode: &str) -> Option { + let mode = EvalOpenMode::parse(mode)?; + if stream_wrappers::is_php_memory_stream(path) { + return self.open_ephemeral_stream(path, &mode, &[], None, false); + } + if stream_wrappers::is_data_stream(path) { + let bytes = stream_wrappers::decode_data_uri(path)?; + return self.open_ephemeral_stream(path, &mode, &bytes, None, false); + } + if stream_wrappers::is_phar_stream(path) { + return self.open_phar_stream(path, &mode); + } + if stream_wrappers::is_http_stream(path) && mode.read && !mode.write { + let bytes = stream_wrappers::read_http_url(path)?; + return self.open_ephemeral_stream(path, &mode, &bytes, None, false); + } + let path = stream_wrappers::local_filesystem_path(path)?; + let file = mode.open(&path).ok()?; + Some(self.insert(EvalFileStream::new(file, path, mode.label))) + } + + /// Opens an anonymous temporary file and returns its resource id. + pub(crate) fn open_tmpfile(&mut self) -> Option { + let path = eval_tmpfile_path(); + let file = OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + .ok()?; + let _ = std::fs::remove_file(&path); + Some(self.insert(EvalFileStream::new( + file, + path.to_string_lossy().into_owned(), + "w+".to_string(), + ))) + } + + /// Opens a shell process pipe and returns its stream resource id. + pub(crate) fn open_process_pipe(&mut self, command: &str, mode: &str) -> Option { + let read_mode = match mode.chars().next()? { + 'r' => true, + 'w' => false, + _ => return None, + }; + let mut child = Command::new("/bin/sh") + .arg("-c") + .arg(command) + .stdin(if read_mode { + Stdio::null() + } else { + Stdio::piped() + }) + .stdout(if read_mode { + Stdio::piped() + } else { + Stdio::null() + }) + .spawn() + .ok()?; + let file = if read_mode { + let stdout = child.stdout.take()?; + unsafe { + // The ChildStdout pipe is converted into the File that backs + // this eval stream; no second owner keeps the fd alive. + File::from_raw_fd(stdout.into_raw_fd()) + } + } else { + let stdin = child.stdin.take()?; + unsafe { + // The ChildStdin pipe is converted into the File that backs + // this eval stream; dropping it before wait sends EOF. + File::from_raw_fd(stdin.into_raw_fd()) + } + }; + let id = self.insert(EvalFileStream::new( + file, + command.to_string(), + if read_mode { "r" } else { "w" }.to_string(), + )); + self.process_children.insert(id, child); + Some(id) + } + +} diff --git a/crates/elephc-magician/src/stream_resources/operations.rs b/crates/elephc-magician/src/stream_resources/operations.rs new file mode 100644 index 0000000000..e5700c7d41 --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/operations.rs @@ -0,0 +1,397 @@ +//! Purpose: +//! Performs close, read/write, positioning, metadata, synchronization, hash, +//! socket, directory, context, filter, and copy operations on resource ids. +//! +//! Called from: +//! - Stream builtins after a resource id has been resolved. +//! +//! Key details: +//! - Each operation checks the concrete resource table and preserves existing +//! false/none behavior for incompatible ids. + +use super::*; + +impl EvalStreamResources { + + /// Removes a stream resource from the table, closing its file handle. + pub(crate) fn close(&mut self, id: i64) -> bool { + let mut closed = false; + let mut ok = true; + if let Some(stream) = self.streams.remove(&id) { + closed = true; + ok = stream.finalize_on_close(); + } + closed = closed + || self.user_wrapper_streams.remove(&id).is_some() + || self.filter_resources.remove(&id) + || self.socket_listeners.remove(&id).is_some(); + self.socket_names.remove(&id); + if let Some(mut child) = self.process_children.remove(&id) { + let _ = child.wait(); + } + closed && ok + } + + /// Returns whether a file-like stream resource exists. + pub(crate) fn has_stream(&self, id: i64) -> bool { + self.streams.contains_key(&id) || self.user_wrapper_streams.contains_key(&id) + } + + /// Returns a local or remote socket name for a socket resource. + pub(crate) fn socket_name(&self, id: i64, remote: bool) -> Option { + let names = self.socket_names.get(&id)?; + if remote { + names.peer.clone() + } else { + Some(names.local.clone()) + } + } + + /// Applies a TCP/Unix stream shutdown operation. + pub(crate) fn socket_shutdown(&self, id: i64, mode: i64) -> Option { + let stream = self.streams.get(&id)?; + let shutdown = match mode { + 0 => Shutdown::Read, + 1 => Shutdown::Write, + 2 => Shutdown::Both, + _ => return Some(false), + }; + let result = unsafe { + // libc shutdown only observes the borrowed descriptor and mode. + libc::shutdown(stream.file.as_raw_fd(), eval_shutdown_how(shutdown)) + }; + Some(result == 0) + } + + /// Allocates an eval-local stream filter resource handle. + pub(crate) fn open_filter_resource(&mut self) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.filter_resources.insert(id); + id + } + + /// Removes an eval-local stream filter resource handle. + pub(crate) fn close_filter_resource(&mut self, id: i64) -> bool { + self.filter_resources.remove(&id) + } + + /// Closes a process pipe stream and returns the child exit status. + pub(crate) fn pclose(&mut self, id: i64) -> Option { + let mut child = self.process_children.remove(&id)?; + self.streams.remove(&id)?; + let status = child.wait().ok()?; + Some(status.code().unwrap_or(0) as i64) + } + + /// Removes a directory resource from the table. + pub(crate) fn close_directory(&mut self, id: i64) -> bool { + self.directories.remove(&id).is_some() + } + + /// Reads up to `length` bytes from a stream resource. + pub(crate) fn read(&mut self, id: i64, length: usize) -> Option> { + let stream = self.streams.get_mut(&id)?; + let mut buffer = vec![0_u8; length]; + let read = stream.file.read(&mut buffer).ok()?; + buffer.truncate(read); + stream.eof = read == 0 || read < length; + Some(buffer) + } + + /// Reads the next entry name from a directory resource. + pub(crate) fn read_directory(&mut self, id: i64) -> Option { + self.directories.get_mut(&id)?.read() + } + + /// Feeds bytes into an incremental hash context. + pub(crate) fn update_hash_context(&mut self, id: i64, data: &[u8]) -> bool { + let Some(context) = self.hash_contexts.get_mut(&id) else { + return false; + }; + unsafe { + // The table owns the opaque handle and this mutable borrow gives the + // crypto call exclusive access for the duration of the update. + elephc_crypto::elephc_crypto_update(context.handle, data.as_ptr(), data.len()); + } + true + } + + /// Returns the persisted options for a stream context resource. + pub(crate) fn stream_context_options(&self, id: i64) -> Option { + self.stream_contexts.get(&id).and_then(|context| context.options) + } + + /// Replaces persisted options for a stream context resource. + pub(crate) fn set_stream_context_options( + &mut self, + id: i64, + options: Option, + ) -> bool { + let Some(context) = self.stream_contexts.get_mut(&id) else { + return false; + }; + context.options = options; + true + } + + /// Reads one stream line up to a limit, newline, or custom delimiter. + pub(crate) fn read_line( + &mut self, + id: i64, + length: usize, + ending: Option<&[u8]>, + include_ending: bool, + stop_at_newline: bool, + ) -> Option> { + let stream = self.streams.get_mut(&id)?; + let mut output = Vec::new(); + let mut byte = [0_u8; 1]; + while output.len() < length { + let read = stream.file.read(&mut byte).ok()?; + if read == 0 { + stream.eof = true; + break; + } + output.push(byte[0]); + if let Some(ending) = ending { + if !ending.is_empty() && output.ends_with(ending) { + if !include_ending { + output.truncate(output.len().saturating_sub(ending.len())); + } + break; + } + } else if stop_at_newline && byte[0] == b'\n' { + break; + } + } + Some(output) + } + + /// Writes all provided bytes to a stream resource and returns the written byte count. + pub(crate) fn write(&mut self, id: i64, data: &[u8]) -> Option { + let stream = self.streams.get_mut(&id)?; + let written = stream.file.write(data).ok()?; + stream.eof = false; + Some(written) + } + + /// Flushes buffered stream data to the host file handle. + pub(crate) fn flush(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.flush().is_ok()) + } + + /// Returns whether a stream's file descriptor is attached to a terminal. + pub(crate) fn isatty(&self, id: i64) -> Option { + let stream = self.streams.get(&id)?; + let result = unsafe { + // libc only reads the descriptor value during the terminal probe. + libc::isatty(stream.file.as_raw_fd()) + }; + Some(result == 1) + } + + /// Toggles blocking mode on a stream's file descriptor. + pub(crate) fn set_blocking(&self, id: i64, enable: bool) -> Option { + let stream = self.streams.get(&id)?; + let fd = stream.file.as_raw_fd(); + let flags = unsafe { + // fcntl reads the current descriptor flags without taking ownership. + libc::fcntl(fd, libc::F_GETFL) + }; + if flags < 0 { + return Some(false); + } + let flags = if enable { + flags & !libc::O_NONBLOCK + } else { + flags | libc::O_NONBLOCK + }; + let result = unsafe { + // fcntl updates the descriptor flags in place. + libc::fcntl(fd, libc::F_SETFL, flags) + }; + Some(result == 0) + } + + /// Reports timeout-setting support for local file streams. + pub(crate) fn set_timeout(&self, id: i64, _seconds: i64, _microseconds: i64) -> Option { + self.streams.get(&id).map(|_| false) + } + + /// Stores a per-stream chunk size and returns the previous size. + pub(crate) fn set_chunk_size(&mut self, id: i64, size: i64) -> Option { + if !self.streams.contains_key(&id) || size <= 0 { + return None; + } + Some(self.chunk_sizes.insert(id, size).unwrap_or(8192)) + } + + /// Accepts read/write buffer settings for local file streams. + pub(crate) fn set_buffer(&self, id: i64, _size: i64) -> Option { + self.streams.get(&id).map(|_| 0) + } + + /// Applies an advisory lock operation to a stream's backing file descriptor. + pub(crate) fn flock(&self, id: i64, operation: i64) -> Option<(bool, bool)> { + let stream = self.streams.get(&id)?; + let operation = eval_flock_operation(operation)?; + let result = unsafe { + // libc only observes the borrowed raw fd during this call. + libc::flock(stream.file.as_raw_fd(), operation) + }; + if result == 0 { + Some((true, false)) + } else { + Some((false, eval_flock_would_block())) + } + } + + /// Synchronizes stream data and metadata to storage. + pub(crate) fn sync_all(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.sync_all().is_ok()) + } + + /// Synchronizes stream data to storage where the host platform supports it. + pub(crate) fn sync_data(&mut self, id: i64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.sync_data().is_ok()) + } + + /// Returns whether the stream has reached EOF after the last read attempt. + pub(crate) fn eof(&self, id: i64) -> Option { + self.streams + .get(&id) + .map(|stream| stream.eof) + .or_else(|| self.user_wrapper_streams.get(&id).map(|stream| stream.eof)) + } + + /// Returns the current stream cursor offset. + pub(crate) fn tell(&mut self, id: i64) -> Option { + self.streams.get_mut(&id)?.file.stream_position().ok() + } + + /// Moves the stream cursor according to PHP `fseek()` whence values. + pub(crate) fn seek(&mut self, id: i64, offset: i64, whence: i64) -> bool { + let Some(stream) = self.streams.get_mut(&id) else { + return false; + }; + let position = match whence { + 0 => SeekFrom::Start(u64::try_from(offset).unwrap_or(u64::MAX)), + 1 => SeekFrom::Current(offset), + 2 => SeekFrom::End(offset), + _ => return false, + }; + stream.eof = false; + stream.file.seek(position).is_ok() + } + + /// Rewinds a stream to the beginning. + pub(crate) fn rewind(&mut self, id: i64) -> bool { + self.seek(id, 0, 0) + } + + /// Rewinds a directory resource to its first entry. + pub(crate) fn rewind_directory(&mut self, id: i64) -> bool { + self.directories + .get_mut(&id) + .is_some_and(EvalDirectoryStream::rewind) + } + + /// Finalizes and removes an incremental hash context, returning raw digest bytes. + pub(crate) fn finalize_hash_context(&mut self, id: i64) -> Option> { + let context = self.hash_contexts.remove(&id)?; + let mut output = [0_u8; 64]; + let len = unsafe { + // elephc-crypto consumes and frees the owned context handle here. + elephc_crypto::elephc_crypto_final(context.handle, output.as_mut_ptr()) + }; + eval_hash_digest_bytes(len, &output) + } + + /// Clones an incremental hash context into a new resource id. + pub(crate) fn copy_hash_context(&mut self, id: i64) -> Option { + let context = self.hash_contexts.get(&id)?; + let handle = unsafe { + // elephc-crypto returns a deep clone with independent ownership. + elephc_crypto::elephc_crypto_clone(context.handle) + }; + if handle.is_null() { + return None; + } + Some(self.insert_hash_context(EvalHashContext { handle })) + } + + /// Truncates a stream to the requested byte length. + pub(crate) fn truncate(&mut self, id: i64, size: u64) -> bool { + self.streams + .get_mut(&id) + .is_some_and(|stream| stream.file.set_len(size).is_ok()) + } + + /// Returns host metadata for a stream's backing file handle. + pub(crate) fn metadata(&self, id: i64) -> Option { + self.streams + .get(&id) + .and_then(|stream| stream.file.metadata().ok()) + } + + /// Reads a full or bounded byte sequence from the stream, with optional offset seek. + pub(crate) fn get_contents( + &mut self, + id: i64, + length: Option, + offset: Option, + ) -> Option> { + if let Some(offset) = offset { + if !self.seek(id, offset, 0) { + return None; + } + } + match length { + Some(length) => self.read(id, length), + None => { + let stream = self.streams.get_mut(&id)?; + let mut bytes = Vec::new(); + stream.file.read_to_end(&mut bytes).ok()?; + stream.eof = true; + Some(bytes) + } + } + } + + /// Copies bytes between two streams and returns the copied byte count. + pub(crate) fn copy_to_stream( + &mut self, + from: i64, + to: i64, + length: Option, + offset: Option, + ) -> Option { + let bytes = self.get_contents(from, length, offset)?; + self.write(to, &bytes) + } + + /// Returns metadata fields used by PHP `stream_get_meta_data()`. + pub(crate) fn meta_data(&self, id: i64) -> Option { + if let Some(stream) = self.streams.get(&id) { + return Some(EvalStreamMetaData { + eof: stream.eof, + mode: stream.mode.clone(), + uri: stream.uri.clone(), + }); + } + let stream = self.user_wrapper_streams.get(&id)?; + Some(EvalStreamMetaData { + eof: stream.eof, + mode: stream.mode.clone(), + uri: stream.uri.clone(), + }) + } + +} diff --git a/crates/elephc-magician/src/stream_resources/resource_registration.rs b/crates/elephc-magician/src/stream_resources/resource_registration.rs new file mode 100644 index 0000000000..a9e7cadf6d --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/resource_registration.rs @@ -0,0 +1,206 @@ +//! Purpose: +//! Registers directories, incremental hashes, stream contexts, filters, and +//! user-defined stream wrappers in the eval resource table. +//! +//! Called from: +//! - Directory, hash, context, and userspace-wrapper builtins. +//! +//! Key details: +//! - Wrapper protocol names are normalized before registry mutation. + +use super::*; + +impl EvalStreamResources { + + /// Opens a local directory and returns its resource id. + pub(crate) fn open_directory(&mut self, path: &str) -> Option { + let directory = EvalDirectoryStream::open(path)?; + Some(self.insert_directory(directory)) + } + + /// Opens an incremental hash context and returns its resource id. + pub(crate) fn open_hash_context(&mut self, algo: &[u8]) -> Option { + let handle = unsafe { + // elephc-crypto reads the algorithm name during this call and returns + // an owned opaque context handle on success. + elephc_crypto::elephc_crypto_init(algo.as_ptr(), algo.len()) + }; + if handle.is_null() { + return None; + } + Some(self.insert_hash_context(EvalHashContext { handle })) + } + + /// Opens a stream context resource with optional persisted options. + pub(crate) fn open_stream_context(&mut self, options: Option) -> i64 { + self.insert_stream_context(EvalStreamContext { options }) + } + + /// Registers a user stream wrapper protocol and class in eval-local state. + pub(crate) fn register_stream_wrapper( + &mut self, + protocol: &str, + class_name: &str, + builtins: &[&str], + ) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if self + .user_stream_wrappers + .iter() + .any(|current| current.eq_ignore_ascii_case(&protocol)) + { + return false; + } + if eval_builtin_stream_wrapper_exists(builtins, &protocol) + && !self.disabled_builtin_stream_wrappers.contains(&protocol) + { + return false; + } + self.user_stream_wrapper_classes + .insert(protocol.clone(), class_name.to_string()); + self.user_stream_wrappers.push(protocol); + true + } + + /// Unregisters a user or built-in stream wrapper protocol. + pub(crate) fn unregister_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if let Some(index) = self + .user_stream_wrappers + .iter() + .position(|current| current.eq_ignore_ascii_case(&protocol)) + { + let protocol = self.user_stream_wrappers.remove(index); + self.user_stream_wrapper_classes.remove(&protocol); + return true; + } + if eval_builtin_stream_wrapper_exists(builtins, &protocol) { + return self.disabled_builtin_stream_wrappers.insert(protocol); + } + false + } + + /// Restores a built-in stream wrapper protocol or accepts no-op user restores. + pub(crate) fn restore_stream_wrapper(&mut self, protocol: &str, builtins: &[&str]) -> bool { + let Some(protocol) = eval_normalize_stream_wrapper_protocol(protocol) else { + return false; + }; + if eval_builtin_stream_wrapper_exists(builtins, &protocol) { + self.disabled_builtin_stream_wrappers.remove(&protocol); + } + true + } + + /// Returns the currently visible stream wrapper protocol list. + pub(crate) fn stream_wrappers(&self, builtins: &[&str]) -> Vec { + let mut wrappers = Vec::with_capacity(builtins.len() + self.user_stream_wrappers.len()); + for builtin in builtins { + if !self.disabled_builtin_stream_wrappers.contains(*builtin) { + wrappers.push((*builtin).to_string()); + } + } + wrappers.extend(self.user_stream_wrappers.iter().cloned()); + wrappers + } + + /// Returns the registered userspace wrapper class for a URL scheme. + pub(crate) fn user_stream_wrapper_class_for_path(&self, path: &str) -> Option { + let scheme = stream_wrappers::stream_scheme(path)?; + self.user_stream_wrapper_classes.get(&scheme).cloned() + } + + /// Opens a userspace wrapper stream around an eval-created wrapper object. + pub(crate) fn open_user_wrapper_stream( + &mut self, + object: RuntimeCellHandle, + class_name: &str, + uri: &str, + mode: &str, + ) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.user_wrapper_streams.insert( + id, + EvalUserWrapperStream { + object, + class_name: class_name.to_string(), + uri: uri.to_string(), + mode: mode.to_string(), + eof: false, + }, + ); + id + } + + /// Opens a userspace wrapper directory around an eval-created wrapper object. + pub(crate) fn open_user_wrapper_directory( + &mut self, + object: RuntimeCellHandle, + class_name: &str, + ) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.user_wrapper_directories.insert( + id, + EvalUserWrapperDirectory { + object, + class_name: class_name.to_string(), + }, + ); + id + } + + /// Returns a copied userspace-wrapper stream descriptor for dispatch. + pub(crate) fn user_wrapper_stream_info( + &self, + id: i64, + ) -> Option { + self.user_wrapper_streams + .get(&id) + .map(EvalUserWrapperStream::info) + } + + /// Returns a copied userspace-wrapper directory descriptor for dispatch. + pub(crate) fn user_wrapper_directory_info( + &self, + id: i64, + ) -> Option { + self.user_wrapper_directories + .get(&id) + .map(EvalUserWrapperDirectory::info) + } + + /// Removes a userspace-wrapper directory and returns its descriptor. + pub(crate) fn close_user_wrapper_directory( + &mut self, + id: i64, + ) -> Option { + self.user_wrapper_directories + .remove(&id) + .map(|directory| directory.info()) + } + + /// Updates the cached EOF state for a userspace-wrapper stream. + pub(crate) fn set_user_wrapper_eof(&mut self, id: i64, eof: bool) -> bool { + let Some(stream) = self.user_wrapper_streams.get_mut(&id) else { + return false; + }; + stream.eof = eof; + true + } + + /// Returns the default stream context resource id, creating it if needed. + pub(crate) fn default_stream_context(&mut self) -> i64 { + if let Some(id) = self.default_stream_context { + return id; + } + let id = self.open_stream_context(None); + self.default_stream_context = Some(id); + id + } + +} diff --git a/crates/elephc-magician/src/stream_resources/sockets.rs b/crates/elephc-magician/src/stream_resources/sockets.rs new file mode 100644 index 0000000000..f98928c356 --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/sockets.rs @@ -0,0 +1,116 @@ +//! Purpose: +//! Opens TCP listeners/streams, accepted connections, and Unix socket pairs for +//! eval stream builtins. +//! +//! Called from: +//! - Socket-related filesystem and network builtins through `EvalStreamResources`. +//! +//! Key details: +//! - Socket names are captured when handles enter the shared resource table. + +use super::*; + +impl EvalStreamResources { + + /// Opens a TCP listener resource for `stream_socket_server()`. + pub(crate) fn open_tcp_listener(&mut self, address: &str) -> Option { + let listener = TcpListener::bind(eval_tcp_address(address)).ok()?; + let local = listener.local_addr().ok()?.to_string(); + let id = self.next_id; + self.next_id += 1; + self.socket_names.insert( + id, + EvalSocketNames { + local, + peer: None, + }, + ); + self.socket_listeners.insert(id, listener); + Some(id) + } + + /// Opens a connected TCP stream resource. + pub(crate) fn open_tcp_stream(&mut self, address: &str) -> Option { + self.open_tcp_stream_result(address).ok() + } + + /// Opens a connected TCP stream resource and preserves the host I/O error on failure. + pub(crate) fn open_tcp_stream_result(&mut self, address: &str) -> io::Result { + let stream = TcpStream::connect(eval_tcp_address(address))?; + self.insert_tcp_stream(stream).ok_or_else(|| { + io::Error::new(io::ErrorKind::Other, "failed to track eval TCP stream") + }) + } + + /// Opens a connected TCP stream from separate host and port arguments. + pub(crate) fn open_tcp_stream_host_port(&mut self, host: &str, port: i64) -> Option { + self.open_tcp_stream_host_port_result(host, port).ok() + } + + /// Opens a connected TCP stream from host and port while preserving I/O errors. + pub(crate) fn open_tcp_stream_host_port_result( + &mut self, + host: &str, + port: i64, + ) -> io::Result { + let host = host + .strip_prefix("tcp://") + .or_else(|| host.strip_prefix("ssl://")) + .or_else(|| host.strip_prefix("tls://")) + .unwrap_or(host); + self.open_tcp_stream_result(&format!("{host}:{port}")) + } + + /// Accepts one TCP connection from a listener resource. + pub(crate) fn accept_tcp(&mut self, id: i64) -> Option { + let listener = self.socket_listeners.get(&id)?; + let (stream, _) = listener.accept().ok()?; + self.insert_tcp_stream(stream) + } + + /// Opens a pair of connected local stream resources. + pub(crate) fn open_socket_pair(&mut self) -> Option<(i64, i64)> { + #[cfg(unix)] + { + let (left, right) = UnixStream::pair().ok()?; + let left = unsafe { + // The UnixStream endpoint is moved into the File-backed eval stream. + File::from_raw_fd(left.into_raw_fd()) + }; + let right = unsafe { + // The UnixStream endpoint is moved into the File-backed eval stream. + File::from_raw_fd(right.into_raw_fd()) + }; + let left_id = self.insert(EvalFileStream::new( + left, + "socketpair".to_string(), + "r+".to_string(), + )); + let right_id = self.insert(EvalFileStream::new( + right, + "socketpair".to_string(), + "r+".to_string(), + )); + self.socket_names.insert( + left_id, + EvalSocketNames { + local: "socketpair".to_string(), + peer: Some("socketpair".to_string()), + }, + ); + self.socket_names.insert( + right_id, + EvalSocketNames { + local: "socketpair".to_string(), + peer: Some("socketpair".to_string()), + }, + ); + Some((left_id, right_id)) + } + #[cfg(not(unix))] + { + None + } + } + +} diff --git a/crates/elephc-magician/src/stream_resources/storage.rs b/crates/elephc-magician/src/stream_resources/storage.rs new file mode 100644 index 0000000000..9d8794020d --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/storage.rs @@ -0,0 +1,119 @@ +//! Purpose: +//! Allocates resource ids and inserts file, ephemeral, Phar, TCP, directory, +//! hash, and context storage entries. +//! +//! Called from: +//! - Resource-opening and registration modules in this tree. +//! +//! Key details: +//! - `next_id` remains the single monotonically increasing id source. + +use super::*; + +impl EvalStreamResources { + /// Inserts a file stream and returns the assigned zero-based resource payload. + pub(super) fn insert(&mut self, stream: EvalFileStream) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.streams.insert(id, stream); + id + } + + /// Opens one unlinked temporary file as the backing storage for wrapper streams. + pub(super) fn open_ephemeral_stream( + &mut self, + uri: &str, + mode: &EvalOpenMode, + initial: &[u8], + flush_target: Option, + append: bool, + ) -> Option { + let path = eval_tmpfile_path(); + let mut file = OpenOptions::new() + .read(true) + .write(true) + .create_new(true) + .open(&path) + .ok()?; + let _ = std::fs::remove_file(&path); + file.write_all(initial).ok()?; + if append { + file.seek(SeekFrom::End(0)).ok()?; + } else { + file.seek(SeekFrom::Start(0)).ok()?; + } + Some(self.insert(EvalFileStream::new_with_flush_target( + file, + uri.to_string(), + mode.label.clone(), + flush_target, + ))) + } + + /// Opens a `phar://` entry for reading or buffered write-back on close. + pub(super) fn open_phar_stream( + &mut self, + path: &str, + mode: &EvalOpenMode, + ) -> Option { + let url = path.as_bytes(); + if mode.write { + let initial = if mode.truncate { + Vec::new() + } else { + match elephc_phar::extract_url_bytes(url) { + Some(bytes) => bytes, + None if mode.create => Vec::new(), + None => return None, + } + }; + return self.open_ephemeral_stream( + path, + mode, + &initial, + Some(EvalStreamFlushTarget::PharUrl(url.to_vec())), + mode.append, + ); + } + let bytes = elephc_phar::extract_url_bytes(url)?; + self.open_ephemeral_stream(path, mode, &bytes, None, false) + } + + /// Inserts a TCP stream as a File-backed eval stream and records endpoint names. + pub(super) fn insert_tcp_stream(&mut self, stream: TcpStream) -> Option { + let local = stream.local_addr().ok()?.to_string(); + let peer = stream.peer_addr().ok().map(|addr| addr.to_string()); + let file = unsafe { + // The TcpStream is moved into the File-backed eval stream. + File::from_raw_fd(stream.into_raw_fd()) + }; + let id = self.insert(EvalFileStream::new(file, local.clone(), "r+".to_string())); + self.socket_names + .insert(id, EvalSocketNames { local, peer }); + Some(id) + } + + /// Inserts a directory stream and returns the assigned zero-based resource payload. + pub(super) fn insert_directory(&mut self, directory: EvalDirectoryStream) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.directories.insert(id, directory); + id + } + + /// Inserts a hash context and returns the assigned zero-based resource payload. + pub(super) fn insert_hash_context(&mut self, context: EvalHashContext) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.hash_contexts.insert(id, context); + id + } + + /// Inserts a stream context and returns the assigned zero-based resource payload. + pub(super) fn insert_stream_context(&mut self, context: EvalStreamContext) -> i64 { + let id = self.next_id; + self.next_id += 1; + self.stream_contexts.insert(id, context); + id + } +} diff --git a/crates/elephc-magician/src/stream_resources/types.rs b/crates/elephc-magician/src/stream_resources/types.rs new file mode 100644 index 0000000000..3b2d943117 --- /dev/null +++ b/crates/elephc-magician/src/stream_resources/types.rs @@ -0,0 +1,360 @@ +//! Purpose: +//! Defines concrete file, socket, wrapper, directory, hash, context, and fopen +//! mode storage used by `EvalStreamResources`. +//! +//! Called from: +//! - Resource opening, registration, operations, storage, and cleanup modules. +//! +//! Key details: +//! - File streams may carry a close-time write-back target for virtual wrappers. + +use super::*; + +impl Drop for EvalStreamResources { + /// Frees any incremental hash contexts that were never finalized. + fn drop(&mut self) { + for context in self.hash_contexts.drain().map(|(_, context)| context) { + unsafe { + // The resource table owns these handles; draining prevents reuse + // after the crypto free call. + elephc_crypto::elephc_crypto_free(context.handle); + } + } + } +} + +/// PHP-visible metadata for one eval stream resource. +pub(crate) struct EvalStreamMetaData { + pub(crate) eof: bool, + pub(crate) mode: String, + pub(crate) uri: String, +} + +/// Local and peer names tracked for socket-backed eval streams. +pub(super) struct EvalSocketNames { + pub(super) local: String, + pub(super) peer: Option, +} + +/// Normalizes supported TCP-style stream socket addresses. +pub(super) fn eval_tcp_address(address: &str) -> &str { + address + .strip_prefix("tcp://") + .or_else(|| address.strip_prefix("ssl://")) + .or_else(|| address.strip_prefix("tls://")) + .unwrap_or(address) +} + +/// Converts Rust's socket shutdown enum into libc constants. +pub(super) fn eval_shutdown_how(shutdown: Shutdown) -> libc::c_int { + match shutdown { + Shutdown::Read => libc::SHUT_RD, + Shutdown::Write => libc::SHUT_WR, + Shutdown::Both => libc::SHUT_RDWR, + } +} + +/// Converts PHP `LOCK_*` bit flags into host `flock()` flags. +pub(super) fn eval_flock_operation(operation: i64) -> Option { + let non_blocking = operation & 4 != 0; + let base = match operation & !4 { + 1 => libc::LOCK_SH, + 2 => libc::LOCK_EX, + 3 => libc::LOCK_UN, + _ => return None, + }; + Some(base | if non_blocking { libc::LOCK_NB } else { 0 }) +} + +/// Returns whether the last host `flock()` failure was a non-blocking lock miss. +pub(super) fn eval_flock_would_block() -> bool { + let errno = std::io::Error::last_os_error().raw_os_error(); + errno.is_some_and(|code| code == libc::EWOULDBLOCK || code == libc::EAGAIN) +} + +/// Converts an elephc-crypto digest length into owned raw bytes. +pub(super) fn eval_hash_digest_bytes(len: isize, output: &[u8; 64]) -> Option> { + let len = usize::try_from(len).ok()?; + if len > output.len() { + return None; + } + Some(output[..len].to_vec()) +} + +/// Normalizes a PHP stream wrapper protocol name for eval registry storage. +pub(super) fn eval_normalize_stream_wrapper_protocol(protocol: &str) -> Option { + let protocol = protocol.trim().trim_end_matches("://"); + if protocol.is_empty() { + return None; + } + Some(protocol.to_ascii_lowercase()) +} + +/// Returns whether the protocol is one of elephc's built-in stream wrappers. +pub(super) fn eval_builtin_stream_wrapper_exists(builtins: &[&str], protocol: &str) -> bool { + builtins + .iter() + .any(|builtin| builtin.eq_ignore_ascii_case(protocol)) +} + +/// File stream stored behind one eval resource id. +pub(super) struct EvalFileStream { + pub(super) file: File, + pub(super) uri: String, + pub(super) mode: String, + pub(super) eof: bool, + pub(super) flush_target: Option, +} + +impl EvalFileStream { + /// Creates a tracked stream around a host file handle. + pub(super) fn new(file: File, uri: String, mode: String) -> Self { + Self::new_with_flush_target(file, uri, mode, None) + } + + /// Creates a tracked stream that may write back to a wrapper target on close. + pub(super) fn new_with_flush_target( + file: File, + uri: String, + mode: String, + flush_target: Option, + ) -> Self { + Self { + file, + uri, + mode, + eof: false, + flush_target, + } + } + + /// Flushes any buffered wrapper target before the stream resource disappears. + pub(super) fn finalize_on_close(mut self) -> bool { + let Some(flush_target) = self.flush_target.take() else { + return true; + }; + let mut bytes = Vec::new(); + if self.file.flush().is_err() || self.file.seek(SeekFrom::Start(0)).is_err() { + return false; + } + if self.file.read_to_end(&mut bytes).is_err() { + return false; + } + flush_target.write_back(&bytes) + } +} + +/// Userspace wrapper stream stored behind one eval resource id. +pub(super) struct EvalUserWrapperStream { + pub(super) object: RuntimeCellHandle, + pub(super) class_name: String, + pub(super) uri: String, + pub(super) mode: String, + pub(super) eof: bool, +} + +impl EvalUserWrapperStream { + /// Copies the dispatch-relevant wrapper fields out of the resource table. + pub(super) fn info(&self) -> EvalUserWrapperStreamInfo { + EvalUserWrapperStreamInfo { + object: self.object, + class_name: self.class_name.clone(), + eof: self.eof, + } + } +} + +/// Copied userspace-wrapper stream fields used while dispatching PHP methods. +pub(crate) struct EvalUserWrapperStreamInfo { + pub(crate) object: RuntimeCellHandle, + pub(crate) class_name: String, + pub(crate) eof: bool, +} + +/// Userspace-wrapper directory stored behind one eval resource id. +pub(super) struct EvalUserWrapperDirectory { + pub(super) object: RuntimeCellHandle, + pub(super) class_name: String, +} + +impl EvalUserWrapperDirectory { + /// Copies the dispatch fields needed while invoking wrapper directory methods. + pub(super) fn info(&self) -> EvalUserWrapperDirectoryInfo { + EvalUserWrapperDirectoryInfo { + object: self.object, + class_name: self.class_name.clone(), + } + } +} + +/// Copied userspace-wrapper directory fields used while dispatching PHP methods. +pub(crate) struct EvalUserWrapperDirectoryInfo { + pub(crate) object: RuntimeCellHandle, + pub(crate) class_name: String, +} + +/// Wrapper targets that need a write-back step when their stream closes. +pub(super) enum EvalStreamFlushTarget { + PharUrl(Vec), +} + +impl EvalStreamFlushTarget { + /// Writes buffered stream bytes back to the target URL. + pub(super) fn write_back(&self, bytes: &[u8]) -> bool { + match self { + Self::PharUrl(url) => elephc_phar::put_url_bytes(url, bytes).is_some(), + } + } +} + +/// Directory stream stored behind one eval resource id. +pub(super) struct EvalDirectoryStream { + pub(super) entries: Vec, + pub(super) index: usize, +} + +impl EvalDirectoryStream { + /// Opens a local directory and snapshots its entry names. + pub(super) fn open(path: &str) -> Option { + let entries = std::fs::read_dir(path).ok()?; + let mut names = vec![".".to_string(), "..".to_string()]; + for entry in entries { + let entry = entry.ok()?; + names.push(entry.file_name().to_string_lossy().into_owned()); + } + Some(Self { + entries: names, + index: 0, + }) + } + + /// Returns the next directory entry name. + pub(super) fn read(&mut self) -> Option { + let name = self.entries.get(self.index)?.clone(); + self.index += 1; + Some(name) + } + + /// Moves the directory cursor back to its first entry. + pub(super) fn rewind(&mut self) -> bool { + self.index = 0; + true + } +} + +/// Opaque elephc-crypto incremental hash context resource. +pub(super) struct EvalHashContext { + pub(super) handle: *mut c_void, +} + +/// Stream context metadata tracked by eval. +pub(super) struct EvalStreamContext { + pub(super) options: Option, +} + +/// Parsed PHP fopen mode used to configure `OpenOptions`. +pub(super) struct EvalOpenMode { + pub(super) read: bool, + pub(super) write: bool, + pub(super) append: bool, + pub(super) truncate: bool, + pub(super) create: bool, + pub(super) create_new: bool, + pub(super) label: String, +} + +impl EvalOpenMode { + /// Parses PHP's common fopen mode grammar, ignoring binary/text markers. + pub(super) fn parse(mode: &str) -> Option { + let mut chars = mode.chars(); + let first = chars.next()?; + let plus = mode.contains('+'); + if !mode + .chars() + .all(|ch| matches!(ch, 'r' | 'w' | 'a' | 'x' | 'c' | '+' | 'b' | 't' | 'e')) + { + return None; + } + let mut mode = match first { + 'r' => Self { + read: true, + write: plus, + append: false, + truncate: false, + create: false, + create_new: false, + label: if plus { "r+" } else { "r" }.to_string(), + }, + 'w' => Self { + read: plus, + write: true, + append: false, + truncate: true, + create: true, + create_new: false, + label: if plus { "w+" } else { "w" }.to_string(), + }, + 'a' => Self { + read: plus, + write: true, + append: true, + truncate: false, + create: true, + create_new: false, + label: if plus { "a+" } else { "a" }.to_string(), + }, + 'x' => Self { + read: plus, + write: true, + append: false, + truncate: false, + create: false, + create_new: true, + label: if plus { "x+" } else { "x" }.to_string(), + }, + 'c' => Self { + read: plus, + write: true, + append: false, + truncate: false, + create: true, + create_new: false, + label: if plus { "c+" } else { "c" }.to_string(), + }, + _ => return None, + }; + mode.write = mode.write || plus; + Some(mode) + } + + /// Opens a path with the parsed stream mode. + pub(super) fn open(&self, path: &str) -> std::io::Result { + OpenOptions::new() + .read(self.read) + .write(self.write) + .append(self.append) + .truncate(self.truncate) + .create(self.create) + .create_new(self.create_new) + .open(path) + } +} + +/// Builds a unique temporary path for eval `tmpfile()`. +pub(super) fn eval_tmpfile_path() -> PathBuf { + let mut path = std::env::temp_dir(); + path.push(format!( + "elephc-magician-tmpfile-{}-{}", + std::process::id(), + eval_tmpfile_nonce() + )); + path +} + +/// Returns a monotonic-ish nonce for temporary file names. +pub(super) fn eval_tmpfile_nonce() -> u128 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_nanos()) + .unwrap_or(0) +} From d26e03abaa87c601cf64398916c3b5ced38a4401 Mon Sep 17 00:00:00 2001 From: Vincenzo Petrucci Date: Mon, 13 Jul 2026 15:38:44 +0200 Subject: [PATCH 1208/1208] docs: document experimental eval support [skip ci] --- CHANGELOG.md | 4 + README.md | 18 +- ROADMAP.md | 13 -- docs/README.md | 5 +- docs/compiling/cli-reference.md | 2 +- .../linking-and-conditional-compilation.md | 16 +- docs/compiling/overview.md | 8 +- docs/getting-started/your-first-program.md | 2 +- docs/internals/architecture.md | 15 +- docs/internals/arm64-assembly.md | 2 +- docs/internals/arm64-instructions.md | 2 +- docs/internals/eval-runtime.md | 197 ++++++++++++++++++ docs/internals/how-elephc-works.md | 9 +- docs/internals/memory-model.md | 2 +- docs/internals/the-codegen.md | 32 ++- docs/internals/the-ir.md | 40 +++- docs/internals/the-optimizer.md | 15 ++ docs/internals/the-runtime.md | 143 ++----------- docs/internals/the-type-checker.md | 19 ++ docs/internals/what-is-a-compiler.md | 6 +- docs/php/arrays.md | 5 +- docs/php/builtins/class/get_called_class.md | 7 - docs/php/builtins/class/get_class_methods.md | 7 - docs/php/builtins/class/get_class_vars.md | 7 - docs/php/builtins/class/get_object_vars.md | 7 - docs/php/builtins/misc/buffer_new.md | 7 - docs/php/builtins/process/die.md | 7 - docs/php/builtins/process/exit.md | 7 - docs/php/eval.md | 107 ++++++++-- docs/php/types.md | 6 +- scripts/docs/elephc_builtins/render.py | 7 +- 31 files changed, 481 insertions(+), 243 deletions(-) create mode 100644 docs/internals/eval-runtime.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 304ebd92e7..70e2cafa2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ All notable changes to elephc, a PHP-to-native compiler written in Rust. Releases are listed newest first. +## [Unreleased] +- Added experimental PHP `eval()` support across macOS ARM64, Linux ARM64, and Linux x86_64. Eligible literal fragments are parsed at compile time and lowered to native EIR, including direct or scope-backed caller-local synchronization; dynamic strings and unsupported literal shapes fall back to the optional statically linked `elephc-magician` EvalIR interpreter. Within the supported eval subset, the fallback preserves caller/global scope updates, dynamic functions/classes/constants, callables, reflection, builtins, exceptions, ownership/COW behavior, and PHP-visible diagnostics without requiring PHP or the Zend Engine. Bridge linking is automatic when required and can be forced with `--with-eval`; generated builtin documentation now reports AOT and eval availability separately. + ## [0.26.1] - Expanded flow-sensitive type narrowing for PHP's common guard patterns: `int|false` and other false-sentinel unions now preserve the literal `false` subtype and narrow to their success type after a divergent `=== false` guard, without incorrectly removing a full `bool` member; `=== null` and `is_null()` guards narrow nullable values; and stable object properties can be narrowed through `instanceof`, ternaries, and throw guards. Property facts are invalidated after writes or receiver rebindings and are not retained across property hooks or `__get`, whose repeated reads may differ. - Object-subtype declaration defaults are now validated after class and interface schemas are complete: parameters, methods, and constructor-promoted properties may use an implementing class or subclass instance as the default for an interface/base-class type, while unrelated object defaults are still rejected with a type error. @@ -466,6 +469,7 @@ Releases are listed newest first. ## [0.1.0] - 2026-03-22 - Initial compiler: echo, variables, integers, arithmetic and string concatenation, comparison operators, control flow (`if`/`while`/`for`/`break`/`continue`), functions, logical/assignment/increment operators. +[Unreleased]: https://github.com/illegalstudio/elephc/compare/v0.26.1...HEAD [0.26.1]: https://github.com/illegalstudio/elephc/compare/v0.26.0...v0.26.1 [0.26.0]: https://github.com/illegalstudio/elephc/compare/v0.25.2...v0.26.0 [0.25.2]: https://github.com/illegalstudio/elephc/compare/v0.25.1...v0.25.2 diff --git a/README.md b/README.md index b61ad6be82..48ed84b9d0 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@

- 3 native targets · no Zend Engine · zero runtime dependencies · single standalone binary + 3 native targets · no Zend Engine · no external PHP runtime · single standalone binary

- A PHP-to-native compiler that takes a subset of PHP and compiles it directly to native assembly, producing standalone binaries for macOS ARM64, Linux ARM64, and Linux x86_64. No opcode fallback, just real machine code. + A PHP-to-native compiler that takes a subset of PHP and compiles it directly to native assembly, producing standalone binaries for macOS ARM64, Linux ARM64, and Linux x86_64. Ordinary source is AOT-compiled with no opcode fallback; experimental eval() can embed an optional interpreter bridge when runtime parsing is required.

@@ -88,6 +88,8 @@ I made the project as modular as possible. Every function has its own codegen fi You can write PHP using the constructs documented in the [docs](docs/). Classes with single inheritance, interfaces, `instanceof`, nullsafe access (`?->`), abstract classes, final classes, methods and typed/static properties, PHP-style static property redeclarations, constructor property promotion, traits, constructors, instance/static methods, case-insensitive PHP symbol lookup for functions/classes/methods, `self::` / `parent::` / `static::` with late static binding, `readonly` properties and classes, enums, PHP 8 attributes on declarations, named arguments, first-class callables, typed function and method parameters and returns, `try` / `catch` / `finally` / `throw`, visibility modifiers, union and nullable types, copy-on-write arrays, associative arrays with PHP insertion order and integer/numeric-string key normalization, array union with `+`, closures, generator functions and generator closures with `yield` / `yield from`, namespaces, includes, compile-time Composer/SPL autoloading, class/introspection helpers, `PDO` database access (`PDO` / `PDOStatement` / `PDOException`) with SQLite, PostgreSQL, and MySQL/MariaDB drivers, image creation and manipulation (GD raster I/O, drawing, transforms/filters, Exif/IPTC metadata, and the `Imagick`/`Gmagick`/Cairo object APIs) on a pure-Rust codec/raster bridge, and PHP 8.1-style `Fiber` coroutines on macOS ARM64, Linux ARM64, and Linux x86_64. +Experimental [`eval()` support](docs/php/eval.md) AOT-lowers eligible literal fragments and falls back to the optional, statically linked Magician interpreter for dynamic fragments. Runnable examples live in [`examples/eval/`](examples/eval/) and [`examples/eval-globals/`](examples/eval-globals/). + For performance-oriented code, elephc exposes compiler extensions beyond standard PHP — see the Why section above. Then compile and run: @@ -109,13 +111,13 @@ elephc is designed to be read. The code generation and runtime layers are heavil There are several ways to make PHP easier to distribute or faster to run: bundling a PHP runtime into one executable, encrypting bytecode, running through the Zend VM with JIT, or compiling selected hot paths while falling back to opcodes for dynamic code. -elephc takes a narrower but cleaner route: it is a from-scratch compiler for a static subset of PHP. It parses PHP source, type-checks it, lowers it to target-specific assembly, assembles and links it into a native executable, and ships only the small runtime routines needed by the generated program. If elephc compiles a construct, that construct is native code rather than interpreted PHP. +elephc takes a narrower but cleaner route: it is a from-scratch compiler for a static subset of PHP. It parses PHP source, type-checks it, lowers it to target-specific assembly, assembles and links it into a native executable, and ships only the small runtime routines needed by the generated program. Ordinary supported constructs are native code. Eligible literal `eval()` fragments can also be lowered ahead of time; fragments that require runtime parsing use the optional Magician interpreter bridge. That tradeoff is intentional: - **Less long-tail compatibility** than a VM-backed PHP implementation. - **More mechanical transparency**: readable assembly output, source maps, line-by-line commented codegen, and a documented memory model. -- **No hidden runtime dependency**: the generated binary does not need PHP, the Zend Engine, a loader extension, or an embedded interpreter. +- **No hidden external PHP runtime dependency**: the generated binary does not need PHP, the Zend Engine, or a loader extension. A program that needs dynamic `eval()` embeds its optional interpreter bridge directly in the standalone binary. - **Native-oriented extensions**: `extern`, `ptr`, `buffer`, and `packed class` let PHP-shaped code cross into systems, FFI, game, and performance-sensitive workloads. That does not mean elephc has to live outside the existing PHP ecosystem. The current CLI path produces standalone executables, but the roadmap also includes shared/static library output and an experimental PHP extension bridge. That opens a practical middle path: keep a framework such as WordPress, Laravel, or Symfony running on PHP, then compile static, performance-sensitive modules into native libraries or PHP extensions. @@ -195,8 +197,10 @@ elephc --no-ir-opt hot.php # Link extra native libraries or frameworks for FFI elephc app.php -l sqlite3 -L /opt/homebrew/lib --framework Cocoa -# Force-enable a bridge crate (pdo, tls, crypto, phar, tz, image) regardless of auto-detection +# Force-enable a bridge crate (pdo, tls, crypto, phar, tz, image, eval, web) regardless of auto-detection elephc app.php --with-pdo --with-crypto +# --with-eval force-links Magician; normal eval use is detected automatically +elephc app.php --with-eval # Explicit target selection # Supported targets today: macos-aarch64, linux-aarch64, linux-x86_64 @@ -461,6 +465,7 @@ src/ ├── magic_constants/ # File/scope/trait magic-constant walkers ├── autoload/ # Composer/SPL AOT autoload indexing and file insertion ├── resolver/ # Include/require resolution, declaration discovery, once guards +├── eval_aot.rs # Compile-time planning for literal eval AOT vs bridge fallback ├── runtime_cache.rs # Preassembled runtime object cache ├── source_map.rs # Assembly/source-map sidecar emission ├── termination.rs # Structured terminal-effect analysis @@ -567,6 +572,9 @@ src/ │ └── generators/ # Generator frame layout and __rt_gen_* helpers │ └── errors/ # Error formatting with line:col + +crates/ +└── elephc-magician/ # Optional EvalIR interpreter staticlib for dynamic eval ``` diff --git a/ROADMAP.md b/ROADMAP.md index 2ec01f7018..8e499cb682 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1047,19 +1047,6 @@ future use cases. | Fiber parity v2 | Medium | MVP delivered in v0.20.x for ARM64 and Linux x86_64. Remaining parity work: arithmetic auto-unboxing on `mixed` payloads received from `suspend()`, true variadic `start(...$args)` beyond seven args, dynamic callback targets, by-reference callback start parameters, configurable stack sizing, and PHP-exact `FiberError` hierarchy. See `docs/php/fibers.md`. | | Conditional include class-like variants | High | Keep class/interface/trait/enum duplicate detection strict for now. Supporting branch-selected class-like declarations would require runtime class metadata/layout dispatch, while modern PHP can avoid the ambiguity with namespaces. | ---- - -## Will not implement - -Features that are fundamentally incompatible with a static ahead-of-time compiler. - -| Feature | Reason | -|---|---| -| `compact()` | Resolves variable names from strings at runtime. In elephc, variables are fixed stack slots allocated at compile time — there is no variable name table at runtime. | -| `extract()` | Creates new variables from array keys at runtime. A static compiler must know all variables before execution — it cannot allocate stack slots on the fly. | -| `$$var` (variable variables) | Requires a runtime symbol table to resolve variable names dynamically. Incompatible with static stack-based variable allocation. | -| `eval()` | Requires a full interpreter/compiler at runtime. Fundamentally impossible in an AOT compiler. | - ## Future 1.0 perspective 1.0 is not an active planning gate for the current roadmap. Revisit it only diff --git a/docs/README.md b/docs/README.md index 360d40e9b7..59a1b46cb5 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ sidebar: order: 0 --- -elephc compiles PHP to native binaries for the supported targets — currently macOS ARM64, Linux ARM64, and Linux x86_64. No interpreter, no VM, no runtime dependencies. This documentation covers everything from PHP syntax support to compiler-specific extensions and internal architecture. +elephc compiles PHP to standalone native binaries for the supported targets — currently macOS ARM64, Linux ARM64, and Linux x86_64 — without PHP, the Zend Engine, or an external VM. Ordinary source is AOT-compiled; experimental `eval()` may embed an optional interpreter bridge when a fragment requires runtime parsing. This documentation covers everything from PHP syntax support to compiler-specific extensions and internal architecture. ## Getting Started @@ -40,7 +40,7 @@ Standard PHP features supported by elephc. Implemented PHP syntax is intended to - [Operators](php/operators.md) — arithmetic, comparison, `instanceof`, logical, bitwise, string, assignment, ternary, null coalescing, error control - [Control Structures](php/control-structures.md) — if/else, while, for, foreach, switch, match, multi-level break/continue, try/catch/finally - [Functions](php/functions.md) — declarations, closures, arrow functions, named arguments, variadic, spread, pass-by-reference, first-class callables, static variables -- [Eval](php/eval.md) — runtime PHP fragment evaluation, scope synchronization, dynamic declarations, supported builtins, and limitations +- [Eval](php/eval.md) — experimental literal-AOT and runtime PHP fragment evaluation, scope synchronization, dynamic declarations, safety, supported builtins, and limitations - [Strings](php/strings.md) — escape sequences, interpolation, heredoc/nowdoc, 70+ built-in string functions - [Regex](php/regex.md) — PCRE2-backed `preg_*` functions, SPL regex iterators, and native PCRE2 build requirements - [Arrays](php/arrays.md) — indexed, associative, copy-on-write, 50+ built-in array functions @@ -84,6 +84,7 @@ How elephc works under the hood — from lexing to code generation and runtime s - [The Code Generator](internals/the-codegen.md) — checked AST to EIR, then target assembly - [The EIR Design](internals/the-ir.md) — PHP-shaped intermediate representation used by codegen and `--emit-ir` - [The Runtime](internals/the-runtime.md) — hand-written assembly routines +- [Eval Runtime Architecture](internals/eval-runtime.md) — literal AOT planning, scope synchronization, Magician fallback, and bridge ABI - [Memory Model](internals/memory-model.md) — stack frames, heap, reference counting - [Architecture](internals/architecture.md) — module map, calling conventions - [ARM64 Assembly](internals/arm64-assembly.md) — introduction to ARM64 diff --git a/docs/compiling/cli-reference.md b/docs/compiling/cli-reference.md index 83da7854ba..5a0a454d42 100644 --- a/docs/compiling/cli-reference.md +++ b/docs/compiling/cli-reference.md @@ -92,7 +92,7 @@ See [Optimization and codegen controls](optimization.md). | `--link LIB` / `-l LIB` / `-lLIB` | library name | — | Link an extra native library (repeatable). | | `--link-path DIR` / `-L DIR` / `-LDIR` | directory | — | Add a library search path (repeatable). | | `--framework NAME` | framework name | — | Link a macOS framework (repeatable). | -| `--with-CRATE` | `pdo`, `tls`, `crypto`, `phar`, `tz`, `image`, `web` | — | Force-enable a bridge crate regardless of feature auto-detection (repeatable). Force-links the staticlib (whole-archived, so it is not dead-stripped) and, for crates with a PHP-surface prelude (`pdo`, `tz`, `image`), force-injects that prelude so the API is available. `--with-web` is an alias for `--web`. An unknown crate name is an error. | +| `--with-CRATE` | `pdo`, `tls`, `crypto`, `phar`, `tz`, `image`, `eval`, `web` | — | Force-enable a bridge crate regardless of feature auto-detection (repeatable). Force-links the staticlib (whole-archived, so it is not dead-stripped) and, for crates with a PHP-surface prelude (`pdo`, `tz`, `image`), force-injects that prelude so the API is available. `--with-eval` force-links Magician but is not required for normal `eval()` use; eligible literal eval can remain bridge-free. `--with-web` is an alias for `--web`. An unknown crate name is an error. | See [Linking, heap, and conditional compilation](linking-and-conditional-compilation.md). diff --git a/docs/compiling/linking-and-conditional-compilation.md b/docs/compiling/linking-and-conditional-compilation.md index 8656dc82c0..90ba15a0db 100644 --- a/docs/compiling/linking-and-conditional-compilation.md +++ b/docs/compiling/linking-and-conditional-compilation.md @@ -51,12 +51,15 @@ Some optional features are implemented as Rust *bridge crates* (`staticlib` archives) that elephc links into the program: `pdo` (database access), `tls` (`https://`/`ftps://` streams), `crypto` (the `hash()`/`md5()`/`sha1()` family), `phar` (Phar archives), `tz` (timezone introspection), `image` (GD/Imagick image -processing), and `web` (the `--web` server). +processing), `eval` (the Magician interpreter fallback for dynamic `eval()`), +and `web` (the `--web` server). By default a bridge is linked **only when the program uses it** — using a hash function pulls in `crypto`, opening an `https://` stream pulls in `tls`, -referencing `PDO` pulls in `pdo`, and so on. Programs that do not use a feature -never link its crate, so binaries stay small. +referencing `PDO` pulls in `pdo`, and so on. An `eval()` call pulls in Magician +only when it needs runtime parsing: eligible literal fragments can be parsed at +compile time and lowered to native EIR without the interpreter bridge. Programs +that do not need a feature never link its crate, so binaries stay small. `--with-CRATE` force-enables a bridge regardless of that auto-detection. It force-links the staticlib (whole-archived, so it is retained even if no symbol @@ -68,8 +71,15 @@ that detection cannot see. The flag is repeatable: ```bash elephc app.php --with-pdo elephc app.php --with-crypto --with-tls +elephc app.php --with-eval ``` +`--with-eval` force-links `elephc_magician`; it does not enable new syntax or +change which fragments are eligible for AOT lowering. Normal eval usage is +detected automatically. See [Eval](../php/eval.md) for language semantics and +[Eval Runtime Architecture](../internals/eval-runtime.md) for the AOT/fallback +decision and scope ABI. + `--with-web` is an alias for [`--web`](../beyond-php/web.md) (the full server mode, which owns the program entry point). An unknown crate name is rejected with the list of valid crates. Forcing a crate increases binary size, since the whole diff --git a/docs/compiling/overview.md b/docs/compiling/overview.md index fe6433014c..22bbfda304 100644 --- a/docs/compiling/overview.md +++ b/docs/compiling/overview.md @@ -6,9 +6,11 @@ sidebar: --- elephc is an ahead-of-time compiler: it reads a `.php` file and produces a -standalone native binary with no interpreter, no virtual machine, and no runtime -dependencies. The generated program is plain machine code linked against a small -hand-written runtime that is baked into the executable. +standalone native binary with no PHP, Zend Engine, or external virtual-machine +dependency. Ordinary source is machine code linked against a small hand-written +runtime baked into the executable. Experimental dynamic `eval()` is the explicit +exception: fragments that cannot be AOT-lowered embed the optional Magician +interpreter bridge in that same standalone binary. ## Basic invocation diff --git a/docs/getting-started/your-first-program.md b/docs/getting-started/your-first-program.md index 8ca8c70e07..258d786093 100644 --- a/docs/getting-started/your-first-program.md +++ b/docs/getting-started/your-first-program.md @@ -30,7 +30,7 @@ This produces a native binary called `hello` in the same directory. Run it: Hello, World! ``` -That's it — a standalone native binary, no PHP interpreter needed. +That's it — a standalone native binary, with no external PHP interpreter needed. ## A slightly bigger example diff --git a/docs/internals/architecture.md b/docs/internals/architecture.md index b60a5c2018..036e0f010a 100644 --- a/docs/internals/architecture.md +++ b/docs/internals/architecture.md @@ -2,7 +2,7 @@ title: "Architecture" description: "Module map, calling conventions, and pipeline diagram." sidebar: - order: 10 + order: 11 --- ## Compilation pipeline @@ -155,6 +155,13 @@ PHP source (.php) └─────────┘ ``` +Experimental `eval()` follows the same front-end pipeline. A literal fragment +is classified inside EIR lowering by `src/eval_aot.rs`: eligible fragments +become native EIR, scope-backed fragments use only core runtime helpers, and +dynamic fragments enable the optional `elephc-magician` interpreter staticlib +at final linking. This is a lowering/linking decision, not a separate timed +compiler phase. See [Eval Runtime Architecture](eval-runtime.md). + ## Target Model The compiler now distinguishes the operating-system side of a target from the instruction set: @@ -184,6 +191,7 @@ src/ ├── conditional/ Build-time `ifdef` pass ├── autoload/ Composer/SPL AOT autoload indexing, rule interpretation, and file insertion ├── resolver/ Include/require resolution, declaration discovery, once guards +├── eval_aot.rs Target-independent literal eval planning and fallback classification ├── optimize.rs Public optimizer entry points and effect context ├── optimize/ Constant folding, constant propagation, control-flow pruning, normalization, dead-code elimination ├── ir/ EIR types, builder, validator, printer, effects, and tests @@ -312,6 +320,8 @@ src/ │ ├── mod.rs Runtime module boundary; re-exports the emission entry points │ ├── data/ Fixed, user-program, and instanceof runtime data tables (4 files) │ ├── diagnostics.rs Suppressible runtime-warning channel used by `@` +│ ├── eval_bridge.rs C-ABI value, callable, class, and runtime hooks used by Magician +│ ├── eval_scope.rs Core materialized-scope helpers usable without the interpreter │ ├── emitters.rs `emit_runtime()` orchestration — emits every runtime category in a fixed order │ ├── strings/ itoa, concat, resource display, ftoa, sprintf, md5, sha1, str_persist, ... (71 files) │ ├── arrays/ heap_alloc, heap_free, array_free_deep, array_grow, hash_grow, hash_*, mixed boxing/freeing, mixed instanceof, sort, usort, refcount, gc/decref dispatch, ... (148 files) @@ -332,6 +342,9 @@ src/ └── errors/ ├── mod.rs CompileError, error trait └── report.rs Error formatting + +crates/ +└── elephc-magician/ Optional EvalIR parser/interpreter staticlib for dynamic eval ``` ## ARM64 calling conventions diff --git a/docs/internals/arm64-assembly.md b/docs/internals/arm64-assembly.md index 53eae56385..03f24c1bf2 100644 --- a/docs/internals/arm64-assembly.md +++ b/docs/internals/arm64-assembly.md @@ -2,7 +2,7 @@ title: "ARM64 Assembly" description: "Introduction to ARM64 assembly for compiler output." sidebar: - order: 11 + order: 12 --- ## What is assembly language? diff --git a/docs/internals/arm64-instructions.md b/docs/internals/arm64-instructions.md index 29ac5a0fd6..394790e3cb 100644 --- a/docs/internals/arm64-instructions.md +++ b/docs/internals/arm64-instructions.md @@ -2,7 +2,7 @@ title: "ARM64 Instructions" description: "ARM64 instruction reference used by elephc." sidebar: - order: 12 + order: 13 --- This is a reference for the ARM64 instructions elephc uses most often, organized by category. Each entry shows the instruction, what it does, and where elephc uses it. diff --git a/docs/internals/eval-runtime.md b/docs/internals/eval-runtime.md new file mode 100644 index 0000000000..1434dba62b --- /dev/null +++ b/docs/internals/eval-runtime.md @@ -0,0 +1,197 @@ +--- +title: "Eval Runtime Architecture" +description: "How literal eval is planned for AOT lowering, when Magician is linked, and how native and interpreted scopes share values." +sidebar: + order: 9 +--- + +**Sources:** `src/eval_aot.rs`, `src/ir_lower/expr/mod.rs`, +`src/ir_lower/program.rs`, `src/codegen/lower_inst/builtins/eval.rs`, +`src/codegen_support/runtime/eval_bridge.rs`, +`src/codegen_support/runtime/eval_scope.rs`, and +`crates/elephc-magician/`. + +Eval is an experimental hybrid feature. Ordinary elephc source and eligible +literal eval fragments are compiled ahead of time. Only fragments that require +runtime parsing or dynamic symbol behavior use the optional Magician +interpreter. The resulting executable remains standalone: Magician is a static +bridge library, not an external PHP or Zend dependency. + +For PHP-visible behavior and the supported fragment subset, see +[Eval](../php/eval.md). This page documents the compiler/runtime boundary. + +## Pipeline position + +`eval` is not a dedicated lexer token or AST node. The parser produces an +ordinary `ExprKind::FunctionCall`; later semantic passes recognize the +case-insensitive PHP language-construct name. + +```text +eval($code) + -> checker: exactly one argument, result Mixed, conservative barrier + -> EIR lowering: BuiltinCall or EvalLiteralCall + -> literal planner, when the source is statically known + -> direct/native EIR + -> EIR plus core eval-scope helpers + -> Magician interpreter fallback + -> target-aware assembly and optional bridge linking +``` + +The checker and AST optimizer deliberately stay conservative even for a source +literal. The more precise AOT decision happens during EIR lowering, after the +front end has already preserved PHP's dynamic semantics and diagnostics. + +## Execution-path decision + +| Path | Typical input | Runtime requirements | +|---|---|---| +| Direct AOT | A literal fragment whose operations and caller-local reads/writes can be lowered statically | No eval context, scope, or Magician library | +| Scope-backed AOT | A literal fragment that is statically lowerable but needs materialized known scope values | Core `eval_scope` runtime feature; no interpreter library | +| Interpreter fallback | A dynamic string or a literal requiring dynamic declarations, includes, references, dynamic calls, or another unsupported AOT shape | `eval_bridge`, synchronized scopes, PCRE2, and `elephc_magician` | + +`src/eval_aot.rs` parses literal fragments at compile time, applies call-site +magic-constant metadata, records known scope reads and writes, and produces an +`EvalAotPlan`. A plan can contain a fully static EIR body, a scope-read EIR +body, or a conservative fallback reason. + +Current fallback classes include parse failures, `include`/`require`, runtime +declarations, global/static scope, references and by-reference operations, +dynamic calls or class/member resolution, unsupported object/array/iterable +shapes, `try`/`throw`, unsupported control flow, and unsupported static calls. +Eligibility is intentionally conservative: a fragment falls back rather than +being partially compiled with different observable behavior. + +`src/ir_lower/program.rs` repeats the final bridge-requirement check against the +completed EIR module. This accounts for actual local-slot types and supported +static function/method targets before setting `RuntimeFeatures::eval_bridge`. +Consequently, the presence of `EvalLiteralCall` alone does not imply that the +binary links Magician. + +## EIR representation + +Literal calls use `EvalLiteralCall`, carrying the fragment in the module data +pool. Dynamic calls remain ordinary builtin calls until the eval lowering path +materializes the runtime code string. + +The eval-specific EIR operations are: + +| Operation | Responsibility | +|---|---| +| `EvalLiteralCall` | Preserve a literal fragment for AOT planning or interpreter fallback. | +| `EvalScopeGet`, `EvalScopeSet` | Read or update a named boxed cell in a materialized eval scope. | +| `EvalFunctionCall`, `EvalFunctionCallArray` | Dispatch a function created or registered in the persistent eval context. | +| `EvalFunctionExists`, `EvalClassExists` | Probe dynamic symbols that may have been created by an earlier eval barrier. | +| `EvalConstantExists`, `EvalConstantFetch` | Probe or fetch constants retained in the eval context. | +| `EvalObjectNew` | Construct a class that may have been declared at runtime. | +| `EvalStaticMethodCall` | Dispatch a static method whose target may come from eval metadata. | + +The conservative default effects for eval calls include arbitrary observable +call effects. Symbol probes read global state; scope access reads or writes heap +state and can fail; constant fetches also carry ownership/refcount effects. +Later lowering may refine a literal call after the AOT plan proves a narrower +path. + +Three addressable local kinds hold eval state when required: + +| `LocalKind` | Lifetime and role | +|---|---| +| `EvalContext` | Persistent Magician context for declarations, constants, callable metadata, and interpreter state. Its presence requires the full bridge. | +| `EvalScope` | Materialized activation/closure scope shared with the executing fragment. | +| `EvalGlobalScope` | Materialized program-global scope used by `global` aliases and CLI argument globals. | + +Frame sizing and cleanup see these slots before assembly emission. A direct AOT +fragment does not declare them merely because the source contains `eval()`. + +## Checker and optimizer barrier + +The type checker enforces exactly one argument, infers that argument for its +side effects, and gives the call the static type `Mixed`. After the call it: + +- marks the active statement stream as having crossed eval; +- widens known local types to `Mixed`; +- drops closure, callable-signature, capture, and callable-target facts; +- permits later reads of variables and dynamic symbols that eval may have + created. + +AST constant propagation reports `Invalidation::All` for eval. This prevents a +pre-call constant or alias fact from being reused after code that can create, +overwrite, or unset caller-visible state. EIR planning may later omit the +physical runtime barrier for a proven literal path without weakening those +front-end safety rules. + +## Dynamic bridge lifecycle + +When interpreter fallback is required, generated code performs these steps: + +1. Coerce the code argument to a PHP string. +2. Lazily allocate the `EvalContext`, activation `EvalScope`, and, when needed, + `EvalGlobalScope`. +3. Register bridge-compatible AOT functions, methods, constructors, class + metadata, parameter names, defaults, and visibility information. +4. Flush visible locals, by-reference cells, closure captures, and eligible + globals into boxed scope cells. +5. Set call-site file, directory, namespace, class, trait, function, and method + metadata used by magic constants. +6. Call `__elephc_eval_execute` through the target-aware ABI. +7. Reload dirty, created, or unset scope entries and propagate return/fatal/ + throwable state through the normal generated runtime paths. + +Top-level scope setup also seeds `$argc` and `$argv`. Function fragments can +bind those values or compiler-known program globals with PHP `global` aliases. +By-value closure captures synchronize only their captured copy; by-reference +captures share a ref cell whose storage is widened before it crosses the +barrier when necessary. + +## Shared value ABI and ownership + +Magician does not introduce a second PHP value layout. The generated runtime +exports C-ABI hooks that box, unbox, retain, release, compare, cast, iterate, +and mutate the same `Mixed` cells used by native code. Array writes still pass +through the normal copy-on-write helpers, and object/class operations reuse +generated metadata when the bridge shape is supported. + +Scope setters retain the value stored in the context; getters return values +with the ownership expected by their EIR result. Normal returns, runtime +fatals, thrown values, early fragment returns, and function cleanup must all +balance those cells. Persistent declarations and metadata live in the eval +context until its owning generated function or process scope is destroyed. + +## Parsing and cache + +Dynamic source is parsed into Magician's immutable EvalIR. The process-wide +parse cache stores both successful parse results and parse errors by exact +source bytes: + +- FIFO capacity: 256 distinct fragments; +- maximum cacheable fragment size: 64 KiB; +- cached data: immutable `Arc` or `EvalParseError` only; +- excluded data: scopes, cells, declarations, context, and call-site magic + constant values. + +The same cache is used by the public eval FFI entry and nested eval/include +execution. Large one-off fragments bypass it instead of occupying global cache +capacity. + +## Linking and targets + +`RuntimeFeatures::eval_scope` emits only the core scope helpers. +`RuntimeFeatures::eval_bridge` additionally links PCRE2 and +`libelephc_magician.a`. The bridge is registered in `src/linker.rs` as +`--with-eval` with the optional `ELEPHC_MAGICIAN_LIB_DIR` archive-directory +override. Normal compilation derives the feature automatically; `--with-eval` +force-loads the archive and increases binary size but does not alter AOT +eligibility. + +All eval lowering and bridge ABI paths are target-aware and covered by +dedicated integration shards on macOS ARM64, Linux ARM64, and Linux x86_64. +Shared lowering must use the existing ABI helpers rather than assume a specific +register set or object format. + +## Documentation ownership + +The exhaustive language subset, reflection behavior, builtins, and known gaps +belong in [Eval](../php/eval.md). Per-builtin AOT/eval availability is generated +from the `builtin!` and `eval_builtin!` registries in the +[Builtin Reference](../php/builtins.md). [The Runtime](the-runtime.md) documents +the shared `__rt_*` assembly families and links here for the optional eval +boundary. diff --git a/docs/internals/how-elephc-works.md b/docs/internals/how-elephc-works.md index efe80d9024..6e566acb82 100644 --- a/docs/internals/how-elephc-works.md +++ b/docs/internals/how-elephc-works.md @@ -355,7 +355,8 @@ In normal compile mode, the toolchain flow is: 2. Write the program assembly to `file.s` 3. Optionally write `file.map` 4. Assemble `file.s` into `file.o` -5. Link `file.o` together with the cached runtime object into the final executable +5. Link `file.o` together with the cached runtime object, any required optional + bridge archives, and system libraries into the final executable If `--timings` is enabled, elephc prints the duration of each major phase to stderr so you can see where time is being spent. @@ -371,7 +372,7 @@ ld -arch arm64 -e _main -o file file.o -lSystem -syslibroot /path/to/sdk On Linux, elephc invokes the native assembler/linker for the requested target. - **`as`** (assembler) converts the user assembly text mnemonics into binary machine code, producing an object file (`.o`) -- **`ld`** (linker) resolves label addresses, links the user object together with the cached runtime object and any requested system libraries, and produces the final native executable (Mach-O on macOS, ELF on Linux) +- **`ld`** (linker) resolves label addresses, links the user object together with the cached runtime object, required bridge staticlibs, and requested system libraries, and produces the final native executable (Mach-O on macOS, ELF on Linux) The `.o` file is deleted after linking. The result is a standalone executable. @@ -384,7 +385,7 @@ With `--emit cdylib` the same flow produces a shared library instead: codegen em big ``` -The binary runs directly on the CPU. There is no PHP interpreter or VM at runtime. The kernel loads the executable for the target platform into memory, jumps to the entry point, and the CPU executes the instructions we generated. The binary still contains elephc's emitted helper routines and links the platform's system libraries for OS/libc services. +The binary runs directly on the CPU and has no PHP, Zend Engine, or external VM dependency. The kernel loads the executable for the target platform into memory, jumps to the entry point, and the CPU executes the instructions we generated. The binary still contains elephc's emitted helper routines and links the platform's system libraries for OS/libc services. If experimental dynamic `eval()` is required, the optional Magician interpreter is statically embedded and handles only those fragments; eligible literal eval stays in native EIR. ## The complete flow @@ -441,7 +442,7 @@ The binary runs directly on the CPU. There is no PHP interpreter or VM at runtim file.o (machine code bytes for user program) │ ▼ ld (linker) - file (user object + cached runtime object) + file (user object + cached runtime object + optional bridge staticlibs) │ ▼ CPU "big\n" diff --git a/docs/internals/memory-model.md b/docs/internals/memory-model.md index 782d87dac6..f3fa075f31 100644 --- a/docs/internals/memory-model.md +++ b/docs/internals/memory-model.md @@ -2,7 +2,7 @@ title: "Memory Model" description: "Stack frames, heap allocation, and memory management." sidebar: - order: 9 + order: 10 --- elephc manages memory without calling `malloc`/`free` for PHP values directly. Storage lives on the **stack** (automatic, per-function), in fixed BSS regions, or in a compiler-managed **heap buffer** with a free-list allocator, reference counting, and a targeted cycle collector for array/hash/object graphs. The final binary still links the target platform's system C library for OS and libc services. diff --git a/docs/internals/the-codegen.md b/docs/internals/the-codegen.md index 4af8188e3a..e9886e4a2d 100644 --- a/docs/internals/the-codegen.md +++ b/docs/internals/the-codegen.md @@ -6,8 +6,9 @@ sidebar: --- **Source:** assembly emitter `src/codegen/`; EIR lowering `src/ir_lower/`; -IR model and validation `src/ir/`; shared runtime/ABI support -`src/codegen_support/`. +literal eval planning `src/eval_aot.rs`; IR model and validation `src/ir/`; +shared runtime/ABI support `src/codegen_support/`; optional bridge crates under +`crates/`. Codegen is a single EIR pipeline. The checked and optimized AST is always lowered into EIR, IR passes run over that module, and `src/codegen/` emits the @@ -46,6 +47,7 @@ and link the resulting user object against the cached runtime object. | `src/codegen/mod.rs` | Public codegen facade, EIR backend entry points, runtime metadata finalization | | `src/codegen/block_emit.rs` | Function/block traversal, prologues, top-level entry and deferred EIR wrappers | | `src/codegen/lower_inst.rs`, `src/codegen/lower_inst/` | Instruction lowering and builtin-specific EIR emission | +| `src/codegen/lower_inst/builtins/eval.rs` | Literal-eval AOT bodies, scope synchronization, bridge calls, and dynamic post-barrier dispatch | | `src/codegen/lower_term.rs` | Terminator lowering for returns, branches, switches, and unreachable paths | | `src/codegen/context.rs` | EIR function emission state and value materialization helpers | | `src/codegen/frame.rs` | Stack-frame sizing, local slots, register allocation integration | @@ -56,6 +58,7 @@ and link the resulting user object against the cached runtime object. | `src/codegen_support/value_boxing.rs` | Shared scalar/string/array/object/iterable boxing into runtime `Mixed` cells | | `src/codegen_support/wrappers/` | Shared callback and fiber wrapper emitters used by deferred EIR wrapper emission | | `src/codegen_support/runtime/` | Shared `__rt_*` routines and runtime data emission | +| `src/codegen_support/runtime/eval_bridge.rs`, `eval_scope.rs` | C-ABI value hooks and core materialized-scope helpers for eval | | `src/codegen_support/platform/` | Target descriptions and assembler/linker naming conventions | The active backend must remain target-aware. New lowering paths should use the @@ -63,17 +66,40 @@ ABI helpers instead of hardcoding AArch64 or x86_64 register and stack details. ## Runtime Split -Codegen produces two linked artifacts: +Codegen always produces two compiler-owned artifacts: 1. **User assembly** from `src/codegen/`, containing lowered PHP functions, methods, top-level entry code, user metadata, and literal data. 2. **Runtime object** from `src/codegen_support/runtime/`, cached by compiler version, target, heap size, runtime features, and PIC mode. +The linker may also add optional bridge staticlibs as a third class of input. +For example, dynamic eval links `libelephc_magician.a`; fully AOT literal eval +does not. Bridge archives are discovered and linked by `src/linker.rs` rather +than emitted by the assembly backend. + Runtime feature selection is derived from the EIR module plus CLI-owned modes such as `--web`. This keeps ordinary binaries from carrying unused helper families while preserving deterministic linking. +## Eval Lowering Boundary + +Literal `eval()` calls reach EIR as `EvalLiteralCall`. The shared planner in +`src/eval_aot.rs` classifies the fragment before target assembly is chosen: + +1. Direct-local and fully static plans become native instructions or internal + EIR functions and require no eval runtime state. +2. Scope-backed AOT plans use `EvalScopeGet`/`EvalScopeSet` and enable only the + core `eval_scope` runtime feature. +3. Dynamic or unsupported plans materialize `EvalContext`, `EvalScope`, and + optional `EvalGlobalScope` slots, call the Magician C ABI, and enable + `eval_bridge`. + +The lowerer preserves PHP source evaluation order before ABI materialization, +boxes values into the shared `Mixed` cell representation, and uses the normal +target-aware call helpers on macOS ARM64, Linux ARM64, and Linux x86_64. See +[Eval Runtime Architecture](eval-runtime.md) for the complete boundary. + ## Emit Modes `--emit executable` is the default and emits a process entry point. `--emit diff --git a/docs/internals/the-ir.md b/docs/internals/the-ir.md index b770cf0ed7..2066a7cc31 100644 --- a/docs/internals/the-ir.md +++ b/docs/internals/the-ir.md @@ -2,7 +2,7 @@ title: "The EIR Design" description: "Specification for elephc's default intermediate representation between AST optimization and assembly emission." sidebar: - order: 13 + order: 14 --- **Status:** EIR is the canonical compiler IR and backend contract for v1.0. @@ -249,17 +249,24 @@ pub enum LocalKind { StaticLocal, RefCell, HiddenTemp, + OwnedTemp, TryHandler, ClosureCapture, NamedArgTemp, IteratorState, GeneratorState, + EvalContext, + EvalScope, + EvalGlobalScope, } ``` `LocalSlot` exists even in SSA form because PHP locals, globals, statics, references, try handlers, and hidden temporaries have addressable storage or -observable lifetime behavior. +observable lifetime behavior. `OwnedTemp` carries cleanup-owning temporaries; +the three eval slot kinds hold the optional interpreter context, activation +scope, and program-global scope. Fully native literal eval paths do not declare +those slots. ## Value Ownership @@ -563,6 +570,26 @@ observable order: 4. Materialize ABI parameters in callee signature order in the backend. 5. Avoid temp preevaluation for ref-like and mutating parameters. +### Eval and Dynamic Symbols + +| Op | Operands | Result | Effects | +|---|---|---|---| +| `EvalLiteralCall` | literal data id plus lowered call operands | `Heap(Mixed)` | conservative call effects until literal planning refines the path | +| `EvalScopeGet` | scope handle plus global-name immediate | boxed value | `reads_heap`, `may_fatal` | +| `EvalScopeSet` | scope handle and value plus global-name immediate | `Void` | `reads_heap`, `writes_heap`, `refcount_op`, `may_fatal` | +| `EvalFunctionCall` | normalized positional values plus dynamic-function metadata | `Heap(Mixed)` | conservative dynamic-call effects | +| `EvalFunctionCallArray` | argument-array value plus function-name data id | `Heap(Mixed)` | conservative dynamic-call effects | +| `EvalFunctionExists`, `EvalClassExists`, `EvalConstantExists` | symbol-name data id | `I64` bool | `reads_global` | +| `EvalConstantFetch` | constant-name data id | `Heap(Mixed)` | global/heap reads, heap/refcount writes, `may_fatal` | +| `EvalObjectNew` | constructor arguments plus dynamic-class metadata | object or `Mixed` | conservative construction/call effects | +| `EvalStaticMethodCall` | normalized arguments plus target data id | typed or `Mixed` | conservative dynamic-call effects | + +`EvalLiteralCall` does not by itself require the interpreter. The target- +independent planner in `src/eval_aot.rs` can lower the fragment to an internal +EIR function, synchronize supported locals directly, use only the core +eval-scope helpers, or retain interpreter fallback. See +[Eval Runtime Architecture](eval-runtime.md). + ### Externs, Pointers, Buffers, and Packed Data | Op | Operands | Result | Effects | @@ -1230,7 +1257,7 @@ instructions, but they must be represented in metadata or consumed by lowering. | `PostIncrement` | Load local, save old value, increment, store, return old value. | | `PreDecrement` | Load local, decrement, store, return new value. | | `PostDecrement` | Load local, save old value, decrement, store, return old value. | -| `FunctionCall` | If extern: `ExternCall`; if builtin: `BuiltinCall`; otherwise `Call`/variant dispatch after shared argument planning. | +| `FunctionCall` | If extern: `ExternCall`; if builtin: `BuiltinCall`; otherwise `Call`/variant dispatch after shared argument planning. Literal `eval()` becomes `EvalLiteralCall` so later lowering can choose native, scope-only, or interpreter execution. | | `ArrayLiteral` | Allocate indexed array and insert elements in source order, including spread handling. | | `ArrayLiteralAssoc` | Allocate hash table and insert key/value pairs in source order; empty hash keeps mixed key/value metadata. | | `Match` | Lower subject once; build strict-comparison arm CFG; default absence may `Fatal` via match-unhandled runtime. | @@ -1356,10 +1383,13 @@ Runtime effect categories: | I/O | streams, filesystem, stat/path helpers | | pointers | C string conversion and pointer string helpers | | fibers | stack allocation, switch, start/resume/suspend/throw/getters | +| eval scope | materialized boxed scope access without requiring the interpreter | +| eval bridge | Magician context, dynamic parsing/execution, symbol registration, and native callback hooks | `RuntimeFeatures` remains the mechanism for optional runtime categories such as -regex. EIR lowering must set required runtime features when it emits operations -that need optional helpers. +regex, `eval_scope`, and `eval_bridge`. EIR lowering must set required runtime +features when it emits operations that need optional helpers. A literal eval +that is fully lowered to native EIR leaves both eval runtime features disabled. ## Target-Aware Backend Boundary diff --git a/docs/internals/the-optimizer.md b/docs/internals/the-optimizer.md index 843d99658c..de84c51c37 100644 --- a/docs/internals/the-optimizer.md +++ b/docs/internals/the-optimizer.md @@ -342,6 +342,21 @@ It now recognizes a useful subset of call expressions precisely, but it still do That conservatism is why the pass is safe to run by default: if an expression could have runtime behavior and elephc cannot prove otherwise with its local summaries, the optimizer prefers to keep it. +### Eval as a dynamic barrier + +`eval()` is the strongest AST-level invalidation case. Its argument is evaluated +normally, but the call itself is observable, may warn/throw/fatal, and can read, +write, create, or unset caller-visible variables and dynamic symbols. Constant +propagation therefore returns `Invalidation::All`; effect analysis must never +classify eval as pure, even when its return value is unused. + +This remains true for a literal source string during AST optimization. A later +target-independent planner can lower an eligible literal to EIR and omit the +physical interpreter/scope barrier, but the AST optimizer does not speculate +across that boundary. The separation keeps type and propagation facts safe +while still allowing the backend to produce bridge-free native code. See +[Eval Runtime Architecture](eval-runtime.md). + ## Pipe operator optimizations PHP 8.5's pipe operator `|>` is implemented as a dedicated `ExprKind::Pipe` diff --git a/docs/internals/the-runtime.md b/docs/internals/the-runtime.md index e7ce2ad22d..d453d1e5f3 100644 --- a/docs/internals/the-runtime.md +++ b/docs/internals/the-runtime.md @@ -11,130 +11,25 @@ The runtime is a collection of **hand-written assembly routines** that handle op These routines end up in every compiled binary. In the CLI flow they are usually pre-assembled into the cached runtime object rather than textually appended to each user `.s` file, but they are still part of the final executable rather than an external shared dependency. -## Optional eval bridge - -Programs that use PHP's `eval()` link one extra static bridge library, `elephc_magician`, in addition to the normal runtime object. Programs without `eval()` do not link that library and keep the ordinary fully native runtime path. - -The generated EIR backend treats `eval()` as a strong dynamic barrier. At the call site it coerces the code argument to a string, lazily creates a persistent eval context plus materialized activation and compiler-known global scopes, registers eligible AOT global functions through descriptor-compatible native callbacks plus parameter-name metadata, flushes visible native locals into the activation scope, flushes eligible program-global storage into the global scope, calls `__elephc_eval_execute`, then reloads locals and synchronized globals that may have been read, written, created, or unset by the evaluated fragment. Top-level eval barriers also materialize CLI `$argc` / `$argv`, and eval global-scope sync seeds those process argument globals for `global $argc` / `global $argv` inside function fragments. The same context stores eval-declared functions and registered native callbacks and can be queried after the barrier by native call sites, string-literal `call_user_func()` callbacks, string-literal `call_user_func_array()` callbacks whose second argument is an array container, and `function_exists()` / `is_callable()` probes. Eval-declared and registered native function calls inside eval can bind positional, named, or spread arguments; post-barrier direct native calls and `call_user_func()` callbacks currently accept simple positional arguments, while post-barrier `call_user_func_array()` callbacks can bind indexed and string-keyed argument containers to eval-declared functions. The scopes store boxed `Mixed` cells so the eval interpreter uses the same runtime value representation as native code. - -Closure bodies that can execute `eval()` participate in the same dynamic barrier rules. By-value captures flush and reload only the closure-local captured copy. By-reference captures store a ref-cell pointer shared with the source variable; when such a capture may cross an eval barrier, EIR lowering widens the shared cell to boxed `Mixed` before closure creation so the caller and closure agree on the representation after eval writes through the cell. - -The bridge crate parses the fragment at runtime into a small EvalIR and interprets that EvalIR through C-ABI value hooks emitted by `src/codegen/runtime/eval_bridge.rs`. Those hooks delegate to existing helpers such as `__rt_mixed_from_value`, `__rt_mixed_numeric_*`, `__rt_mixed_cast_bool`, `__rt_mixed_cast_string`, `__rt_str_loose_eq`, `__rt_hash_normalize_key`, `__rt_mixed_array_get`, and `__rt_mixed_array_set`; eval does not introduce a second PHP value ABI. The initial fragment subset covers PHP comments, scalar expressions, by-name locals, output including comma-separated `echo`, return, `throw`, `try`/`catch (Throwable $e)` and class-specific and union `catch` clauses, unset, simple variable compound assignments, simple variable increment/decrement statements, nested `eval()`, supported builtin dispatch for `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `sleep()`, `usleep()`, `getcwd()`, `sys_get_temp_dir()`, `getenv()`, `putenv()`, `gethostbyname()`, `long2ip()`, `ip2long()`, `inet_pton()`, `inet_ntop()`, `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()`, `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, `umask()`, `basename()`, `dirname()`, `pathinfo()`, `fnmatch()`, `realpath()`, `realpath_cache_get()`, `realpath_cache_size()`, `strlen()`, `ord()`, `strtolower()`, `strtoupper()`, `ucfirst()`, `lcfirst()`, `str_contains()`, `str_starts_with()`, `str_ends_with()`, `strpos()`, `strrpos()`, `strcmp()`, `strcasecmp()`, `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_equals()`, `hash_algos()`, `trim()`, `ltrim()`, `rtrim()`, `chop()`, `addslashes()`, `stripslashes()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, `base64_decode()`, `sprintf()`, `printf()`, `vsprintf()`, `vprintf()`, `sscanf()`, `print_r()`, `var_dump()`, `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, `preg_split()`, `array_sum()`, `array_product()`, `array_chunk()`, `array_column()`, `array_combine()`, `array_fill()`, `array_fill_keys()`, `array_flip()`, `array_keys()`, `array_values()`, `array_diff()`, `array_intersect()`, `array_diff_key()`, `array_intersect_key()`, `range()`, `array_merge()`, `array_pad()`, `array_reverse()`, `array_slice()`, `array_splice()`, `array_unique()`, `array_key_exists()`, `array_rand()`, `in_array()`, `array_search()`, `arsort()`, `asort()`, `krsort()`, `ksort()`, `natcasesort()`, `natsort()`, `rsort()`, `shuffle()`, `sort()`, `uasort()`, `uksort()`, `usort()`, `count()`, `define()`, `defined()`, `abs()`, `sqrt()`, `floor()`, `ceil()`, `round()`, `pow()`, `min()`, `max()`, `pi()`, `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `iterator_count()`, `iterator_to_array()`, `rand()`, `mt_rand()`, `random_int()`, scalar casts (`intval()`, `floatval()`, `strval()`, `boolval()`), mutating scalar casts (`settype()`), type inspection (`gettype()`, `get_class()`, `get_parent_class()`, `get_resource_type()`, `get_resource_id()`), type predicates (`is_int()` / `is_integer()` / `is_long()`, `is_float()` / `is_double()` / `is_real()`, `is_nan()`, `is_finite()`, `is_infinite()`, `is_string()`, `is_bool()`, `is_null()`, `is_array()`, `is_object()`, `is_iterable()`, `is_numeric()`, `is_resource()`), `function_exists()`, `is_callable()`, `class_exists()`, `interface_exists()`, `is_a()`, `is_subclass_of()`, and `spl_classes()`, eval-declared functions callable from eval fragments with positional, named, or spread arguments that share the same generated context, static locals inside eval-declared functions that persist across calls through the eval context, top-level CLI `$argc` / `$argv` visibility, `global $name` aliases for compiler-known program-global storage, `global $argc` / `global $argv` aliases inside function eval fragments, AOT global user functions callable from eval fragments with positional, named, or spread arguments through descriptor invokers, eval-visible AOT class-name probes and object relation probes through `is_a()` / `is_subclass_of()`, eval resource introspection through `get_resource_type()` / `get_resource_id()`, eval-declared class-name registration for `class_exists()`, post-barrier native `class_exists("Name")` probes for eval-declared classes, duplicate eval class-name rejection, eval-declared classes with inheritance, visibility, readonly/final/abstract modifiers, traits, interfaces, static members, constants, attributes, enums, and `__construct()`, AOT `new ClassName(...)` construction through generated class metadata, and eval-declared `new ClassName(...)` construction through dynamic context metadata, Throwable propagation through the normal runtime unwinder and eval-internal `catch` handling with `Throwable` and class-specific and union clauses, eval-declared functions callable from native code or string-literal `call_user_func()` with simple positional arguments after the barrier, eval-declared functions callable through post-barrier string-literal `call_user_func_array()` with indexed or string-keyed argument containers after the barrier, post-barrier native `function_exists()` / `is_callable()` probes for eval-declared functions, eval magic constants including call-site `__FILE__` / `__DIR__`, fragment `__LINE__`, empty eval-scope `__CLASS__` / `__TRAIT__`, namespace-aware `__NAMESPACE__`, and eval-declared-function `__FUNCTION__` / `__METHOD__`, scalar numeric arithmetic including exponentiation, division, modulo, and spaceship, integer bitwise and shift operations, scalar equality/comparisons, logical operators including PHP keyword `and` / `or` / `xor`, null coalescing, ternary expressions, basic control flow (`if`/`elseif`/`else`/`while`/`for`/`foreach`), indexed arrays including `$array[] = value` append writes, associative array literals, and PHP-normalized scalar-key reads, writes, and insertion-order foreach iteration on associative arrays. Missing class names during eval `new` fail as runtime eval fatals. Broader PHP constructs require future expansion of the EvalIR and dynamic symbol tables. - -Eval `finally` bodies run inside the EvalIR interpreter before a fragment return or thrown `Throwable` escapes, and a control action produced by `finally` replaces the pending action from the protected body or catch. - -Eval type-introspection builtin coverage includes `get_called_class()`. It reads the current late-static-bound eval method scope for direct calls, `call_user_func()`, and `call_user_func_array()`, and throws PHP's class-scope `Error` when invoked outside a class scope. - -Eval `catch (Throwable)` may omit the variable binding; in that case the interpreter consumes the throwable and releases the unbound cell before executing the catch body. - -The eval callable dispatcher currently normalizes string callbacks and two-element object-method callable arrays such as `[$this, "method"]`. Object-method arrays are supported for direct `$cb(...)`, `call_user_func()`, `call_user_func_array()`, and `iterator_apply()` paths with positional arguments; static-method callable arrays and closure/descriptor callback values are still handled only by the native descriptor runtime outside eval fragments. - -Eval `match` expressions live entirely inside the interpreter: the subject is evaluated once, patterns are compared through the existing strict-comparison value hook, result arms are evaluated lazily, and a miss without `default` returns the eval runtime-fatal status. - -Eval namespace declarations are resolved while the bridge parser builds EvalIR. The parser qualifies function declarations, class declarations, object construction names, and qualified references against the active namespace; for unqualified function calls and constant fetches it emits a namespaced lookup plus global fallback so PHP builtin functions and predefined constants remain visible from namespaced eval fragments. Simple and grouped `use`, `use function`, and `use const` declarations are parser-time import tables for the active namespace declaration region: class imports rewrite `new` targets, function imports rewrite unqualified calls, and constant imports rewrite unqualified constant fetches before interpretation. `__NAMESPACE__` is lowered to the active namespace string for the parsed block. - -Eval URL codec dispatch includes `urlencode()`, `urldecode()`, `rawurlencode()`, and `rawurldecode()` through direct eval calls, named arguments, `call_user_func()`, `call_user_func_array()`, and `function_exists()`. - -Eval ASCII character-class dispatch includes `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, and `ctype_space()` on non-empty byte strings through the same builtin-call binding path. - -Eval `print_r()` formats scalars, recursive arrays, and bridge-visible object -properties inside the interpreter. With the second argument truthy it returns -the captured string; otherwise it emits that string through the eval output -hook and returns `true`. - -Eval `var_dump()` formats one or more materialized arguments inside the -interpreter, then emits one byte string through the eval output hook. Array -dumping reads foreach-visible keys and values through the same eval hooks used -by `foreach`, `array_keys()`, and `array_values()`. Object dumping uses eval -class metadata for eval-declared property visibility labels, public runtime -property hooks for generated/AOT objects, and eval property-alias metadata for -reference markers. - -Eval SPL class introspection dispatch includes `spl_classes()` through direct -calls, callable dispatch, and `function_exists()` probes. The interpreter builds -the same static class-name snapshot as the native SPL builtin. Eval SPL object -identity dispatch includes `spl_object_id()` and `spl_object_hash()` by deriving -the integer or decimal-string identity from the unboxed object payload after the -normal runtime tag check. - -Eval class-like metadata probe dispatch includes `trait_exists()` and -`enum_exists()` through small generated `(name_ptr, name_len)` tables in the -user runtime data section. The eval bridge scans those tables with the same -case-insensitive string comparison convention used by `class_exists()`. - -Eval `ucwords()` performs ASCII word-start capitalization and honors the optional `separators` argument. - -Eval `strstr()` uses the shared byte-search helper to return the matching suffix, optional prefix, or PHP `false`. - -Eval `str_split()` builds indexed chunk arrays through the eval value hooks so later eval code can read the chunks normally. - -Eval `str_pad()` computes left/right pad budgets in the interpreter and emits a boxed string through the eval value hooks. - -Eval `wordwrap()` performs the same word-aware byte scan as the static helper, preserving existing newlines and honoring `cut_long_words`. - -Eval `number_format()` formats finite numeric cells inside the interpreter, including grouping and custom separators. - -Eval printf-family dispatch formats `sprintf()` and `vsprintf()` inside the interpreter, while `printf()` and `vprintf()` echo the formatted bytes through the eval output hook and return the byte count. The formatter consumes scalar values through the same eval cast hooks and array values in PHP iteration order for the `v*` forms. - -Eval `json_encode()` currently runs a recursive encoder in the eval interpreter for scalar, indexed-array, associative-array, and `stdClass` dynamic-property cells, supporting zero flags plus the `JSON_HEX_*` family, `JSON_UNESCAPED_SLASHES`, `JSON_UNESCAPED_UNICODE`, `JSON_FORCE_OBJECT`, `JSON_NUMERIC_CHECK`, `JSON_PARTIAL_OUTPUT_ON_ERROR`, `JSON_PRETTY_PRINT`, `JSON_PRESERVE_ZERO_FRACTION`, `JSON_INVALID_UTF8_IGNORE`, `JSON_INVALID_UTF8_SUBSTITUTE`, and `JSON_THROW_ON_ERROR`. It uses eval type tags plus array/object iteration hooks, escapes multibyte UTF-8 by default, preserves raw UTF-8 when `JSON_UNESCAPED_UNICODE` is set, records `JSON_ERROR_INF_OR_NAN` for non-finite floats, records `JSON_ERROR_UTF8` for malformed strings unless the ignore/substitute flags sanitize them, schedules eval `JsonException` objects through the normal Throwable channel when the throw flag is active, and intentionally rejects other non-zero flags until the generated runtime JSON flag/error-state surface is bridged. Eval `json_decode()` and `json_validate()` share a byte-level recursive parser for JSON syntax, UTF-8 string bytes, unicode escape pairs, and PHP-style container depth checks; decode then materializes parsed values through `RuntimeValueOps`, preserving overflowing integer tokens as strings when `JSON_BIGINT_AS_STRING` is set, applying the malformed-UTF-8 ignore/substitute modes when those decode flags are present, creating `stdClass` objects for JSON objects unless `$associative` is true, and raising `JsonException` through the eval Throwable channel for parse failures when `JSON_THROW_ON_ERROR` is active, while validate can run the parser in a malformed-UTF-8-ignore mode for `JSON_INVALID_UTF8_IGNORE`. The eval context stores eval-local JSON error code/message state, so `json_last_error()` and `json_last_error_msg()` report INF/NaN, syntax, depth, control-character, UTF-8, and UTF-16 failures from eval JSON calls, include the same location-aware line/column suffix shape as the native decoder, and reset to no-error after successful eval JSON calls. - -Eval `array_map()` iterates source arrays through eval key/value hooks and invokes string callbacks through the shared eval callable dispatcher. The one-array form writes mapped results with original keys and uses PHP's null-callback identity behavior; the multi-array form reindexes output rows, pads missing values with `null`, and materializes zipped row arrays for `array_map(null, ...)`. - -Eval `array_reduce()` reads the provided initial carry or creates a `null` carry when the argument is omitted, walks the source array in PHP iteration order, and invokes the shared eval callable dispatcher with carry and current item cells for each entry. The resulting carry cell is returned directly to the eval caller. - -Eval `array_walk()` iterates keys through the eval value hooks, invokes the shared eval callable dispatcher with value and key cells, discards callback results, and returns a boxed `true` cell. - -Eval `array_pop()`, `array_shift()`, `array_push()`, `array_unshift()`, and `array_splice()` have a direct-call path that requires the first argument to be an eval variable, reconstructs the replacement array through `RuntimeValueOps`, and writes the new cell back through the same scope/reference/global helper used by eval array writes. The pop/shift path returns the removed value or `null`; the push/unshift path returns the rebuilt element count after evaluating inserted values in source order; the splice path returns the removed array, supports optional `length` and `replacement`, expands replacement arrays in iteration order, reindexes integer keys, and preserves string keys. The dynamic callable path receives already-evaluated values, so it computes the popped/shifted value, resulting push/unshift count, or removed splice array without replacing the caller's original array and emits the PHP by-reference warning through the eval warning hook. - -Eval `sort()`, `rsort()`, `asort()`, `arsort()`, `ksort()`, `krsort()`, `natsort()`, and `natcasesort()` reuse the same direct-variable write-back path. The interpreter snapshots array values through `RuntimeValueOps`, builds homogeneous numeric, byte-string, or natural-sort keys, reconstructs either an indexed replacement array or a key-preserving associative replacement, and returns `true`; dynamic callable dispatch validates the array and reports PHP's by-reference warning without mutating the original caller value. - -Eval `shuffle()` also uses that direct-variable write-back path, but collects source values, applies an eval-local Fisher-Yates pass, and reconstructs an indexed replacement array so string keys are discarded and integer keys are reindexed. - -Eval `usort()`, `uksort()`, and `uasort()` use explicit comparator loops instead of host-language sort callbacks because eval comparators can emit output or call back into eval/AOT functions. `usort()` rebuilds an indexed array, while `uksort()` and `uasort()` preserve source keys. Dynamic callable dispatch sorts a by-value copy for comparator side effects, releases the copy, reports the PHP by-reference warning, and leaves the caller's original array unchanged. - -Eval `array_filter()` implements PHP's omitted/null callback form by iterating source keys through the eval value hooks, testing each value with PHP truthiness, and inserting key/value pairs into an associative result so numeric and string keys are preserved. String callbacks route through the same eval callable dispatcher as `call_user_func()`, and `ARRAY_FILTER_USE_VALUE`, `ARRAY_FILTER_USE_BOTH`, and `ARRAY_FILTER_USE_KEY` select value-only, value/key, or key-only callback argument lists. - -Eval `iterator_count()` and `iterator_to_array()` currently use the `RuntimeValueOps` array iteration hooks for array inputs. `iterator_to_array()` copies keys and values in iteration order by default, and delegates to the indexed `array_values()` projection when `preserve_keys` is false. Eval `iterator_apply()` drives Traversable objects through the eval method-call hooks (`rewind()`, `valid()`, and `next()`) and invokes string callbacks or object-method callable arrays through the shared eval callable dispatcher. The callback receives only the optional third-argument array, matching PHP's `iterator_apply()` behavior; current value/key are not passed implicitly. Array inputs remain runtime fatals for `iterator_apply()`. - -Eval `preg_match()`, `preg_match_all()`, `preg_replace()`, `preg_replace_callback()`, and `preg_split()` compile stripped PHP-delimited patterns through Rust's `regex` crate inside the eval bridge. The implementation is deliberately separate from the native PCRE2 runtime helpers: eval supports common modifiers, direct `preg_match()` captures with optional `PREG_OFFSET_CAPTURE` pairs and `PREG_UNMATCHED_AS_NULL` values, direct `preg_match_all()` default `PREG_PATTERN_ORDER` captures, direct `preg_match_all()` `PREG_SET_ORDER` captures, offset-capture pairs and unmatched-as-null values for both `preg_match_all()` result orders, `preg_split()` no-empty, delimiter-capture, and offset-capture result forms, and replacement/split array construction through `RuntimeValueOps`, while unsupported PCRE syntax surfaces as eval runtime fatals instead of falling back to a partial native ABI. - -Eval `count()` uses the same array-length hook for normal mode and recursively walks nested array-like cells for `COUNT_RECURSIVE`, with predefined `COUNT_NORMAL` and `COUNT_RECURSIVE` constants materialized by the eval constant table. - -Eval `crc32()` uses the same table-free reflected CRC-32 polynomial as the generated runtime helper. Eval one-shot digest calls `hash()`, `hash_file()`, `hash_hmac()`, `md5()`, and `sha1()` call through the same pure-Rust algorithm table as the generated crypto bridge, including raw `$binary` output. `hash_file()` reads local host files directly and boxes PHP false when the file cannot be read. Eval `hash_algos()` builds the same indexed supported-algorithm list as the generated runtime helper. - -Eval system and path calls include `time()`, `microtime()`, `date()`, `mktime()`, `strtotime()`, `phpversion()`, `getcwd()`, and `sys_get_temp_dir()`. `phpversion()` mirrors the root compiler package version, and `sys_get_temp_dir()` mirrors the static helper's `/tmp` literal. - -Eval `microtime()` returns a boxed float Unix timestamp with microsecond precision, matching the static lowering convention. Eval `date()` formats libc local time with elephc's documented token subset, `mktime()` delegates local timestamp normalization to libc, and `strtotime()` parses `now`, fixed-width ISO dates, fixed-width ISO datetimes with optional seconds, and `T` datetime separators. Unsupported eval `strtotime()` inputs return `-1`. - -Eval blocking time calls include `sleep()` and `usleep()`. `sleep()` returns zero after an uninterrupted sleep; `usleep()` returns null. - -Eval environment calls include `getenv()` and `putenv()`. Missing variables read as the empty string to match elephc's native runtime convention. - -Eval local filesystem dispatch includes `file()`, `file_get_contents()`, `file_put_contents()`, `readfile()`, `file_exists()`, `is_file()`, `is_dir()`, `is_readable()`, `is_writable()` / `is_writeable()`, `filesize()`, `filemtime()`, `fileatime()`, `filectime()`, `fileperms()`, `fileowner()`, `filegroup()`, `fileinode()`, `filetype()`, `stat()`, `lstat()`, `is_executable()`, `is_link()`, `unlink()`, `copy()`, `rename()`, `mkdir()`, `rmdir()`, `chdir()`, `chmod()`, `touch()`, `symlink()`, `link()`, `readlink()`, `linkinfo()`, `clearstatcache()`, `scandir()`, `glob()`, `tempnam()`, and `umask()` through the interpreter. This path uses host filesystem calls directly; stream wrappers, PHAR URLs, network URLs, ownership/group modification, and `fstat()` array results remain in the native runtime path rather than the eval subset. - -Eval include/require constructs are interpreted in the eval bridge, not dispatched as callable builtins. The bridge resolves relative paths with PHP's cwd-first behavior and an eval call-site directory fallback, tracks canonical successful loads in the eval context for `include_once` and `require_once`, emits raw non-PHP bytes through the eval output hook, executes `` blocks as EvalIR against the same materialized scope, and temporarily overrides file magic metadata so included code sees the included file path and directory. - -Eval `gethostbyname()` resolves to the first IPv4 address from the host resolver and returns the original input when no IPv4 result is available. - -Eval IPv4 conversion dispatch includes `long2ip()`, `ip2long()`, `inet_pton()`, and `inet_ntop()` for direct calls, named arguments, callable dispatch, and `function_exists()` probes. The interpreter currently handles IPv4 dotted-quad strings and four-byte packed addresses. - -Eval path component dispatch includes `basename()`, `dirname()`, `pathinfo()`, and `fnmatch()` in the interpreter, including suffix trimming, repeated parent traversal, `PATHINFO_*` / `FNM_*` constants, pathinfo associative-array construction, and shell-glob matching without a filesystem lookup. - -Eval `realpath()` uses the host filesystem to canonicalize existing paths and boxes PHP false for unresolved paths. - -Eval realpath-cache calls mirror elephc's native no-cache path: `realpath_cache_get()` returns an empty array and `realpath_cache_size()` returns zero. - -Dynamic eval constants store case-sensitive names and retained boxed Mixed values for `define()` / `defined()` probes and bare constant-fetch expressions across eval fragments. Native `defined("Name")` after an eval barrier lowers to an eval-context constant-exists probe for names that were not known AOT constants, and native unknown constant fetches after the barrier lower to an eval-context fetch that returns a retained boxed Mixed cell. Duplicate eval `define()` attempts keep the first value, return `false`, and route the suppressible duplicate-constant warning through the eval bridge to `__rt_diag_warning`. - -Eval predefined constants bypass the dynamic context and are materialized directly by the interpreter. The current eval-visible set is `PHP_EOL`, `PHP_OS`, `DIRECTORY_SEPARATOR`, `PHP_INT_MAX`, `PATHINFO_*`, `FNM_*`, `ARRAY_FILTER_USE_*`, `COUNT_*`, and the supported `PREG_*` constants; `defined()` recognizes them and duplicate `define()` attempts follow the same warning path as dynamic constants. - -Argument unpacking (`...`), expression callable calls such as `$fn(...)`, and `call_user_func_array()` argument arrays are supported for eval-declared functions, registered AOT function calls, and supported PHP-visible builtin calls inside eval when the argument source is an array. Indexed elements bind positionally, string-keyed elements bind by parameter name, and a positional element after a named argument is rejected. Direct unpacking after a named argument is rejected before the unpacked expression is evaluated. Eval method calls reuse this path for numeric unpacking, but reject named method arguments because the method hook receives positional cells only. - -Eval associative array literals and `$array[] = value` append writes use PHP's next automatic integer key rule. Hash appends scan existing integer keys through the eval value hooks before delegating to the shared mixed-array setter; mixed literal construction advances the same automatic key when explicit keys normalize to integers, while `null` keys remain the empty-string key. - -Eval `array(...)` syntax is parsed as the same EvalIR array literal node as `[...]`, so runtime key normalization, append-key tracking, and array hook behavior are shared between both syntaxes. - -Eval builtin dispatch also covers string and byte helpers including `chr()`, `strrev()`, `str_repeat()`, `substr()`, `substr_replace()`, `str_pad()`, `strstr()`, `str_split()`, `wordwrap()`, `nl2br()`, `explode()`, `implode()`, `str_replace()`, `str_ireplace()`, `ucwords()`, `htmlspecialchars()`, `htmlentities()`, `html_entity_decode()`, `urlencode()`, `urldecode()`, `rawurlencode()`, `rawurldecode()`, `ctype_alpha()`, `ctype_digit()`, `ctype_alnum()`, `ctype_space()`, `addslashes()`, `stripslashes()`, `crc32()`, `hash()`, `hash_hmac()`, `md5()`, `sha1()`, `hash_algos()`, `bin2hex()`, `hex2bin()`, `base64_encode()`, and `base64_decode()`, numeric formatting via `number_format()` and printf-family formatting via `sprintf()`, `printf()`, `vsprintf()`, and `vprintf()`, scalar math calls `sin()`, `cos()`, `tan()`, `asin()`, `acos()`, `atan()`, `atan2()`, `sinh()`, `cosh()`, `tanh()`, `log()`, `log2()`, `log10()`, `exp()`, `deg2rad()`, `rad2deg()`, `hypot()`, `intdiv()`, `rand()`, `mt_rand()`, and `random_int()`, floating-point predicate calls `is_nan()`, `is_finite()`, and `is_infinite()`, plus `fdiv()` / `fmod()` through target-aware bridge wrappers that return boxed double cells. +## Eval execution paths + +`eval()` is an optional hybrid boundary rather than part of the ordinary +shared runtime. Eligible literal fragments are parsed at compile time and +lowered directly to EIR. Some literal fragments use only the core eval-scope +helpers, while dynamic or unsupported fragments link the +`elephc_magician` static interpreter bridge. + +The bridge shares elephc's boxed `Mixed` cells, copy-on-write containers, +generated class/callable metadata, diagnostics, exceptions, and target-aware +ABI helpers. It is embedded only when the final EIR module requires the +`eval_bridge` runtime feature; the presence of a fully native literal +`eval()` call does not force it into the binary. + +The compile-time planner, scope/context lifecycle, eval-specific EIR +instructions, parse cache, ownership rules, and bridge linking contract are +documented in [Eval Runtime Architecture](eval-runtime.md). PHP-visible +syntax, supported statements and builtins, reflection behavior, safety, and +limitations live in [Eval](../php/eval.md). ## Why a runtime? diff --git a/docs/internals/the-type-checker.md b/docs/internals/the-type-checker.md index 284c229bfc..b26eb2ba4b 100644 --- a/docs/internals/the-type-checker.md +++ b/docs/internals/the-type-checker.md @@ -168,6 +168,25 @@ Built-in functions have hardcoded type signatures (see below). User-defined func Codegen receives enough signature information to evaluate named/spread arguments in PHP source order while still materializing values in ABI parameter order. +### Eval barrier + +`eval()` is recognized as a PHP language construct rather than a registry-backed +builtin. The checker requires exactly one argument, infers that argument for its +normal side effects, and assigns the call the static result type `Mixed`. + +After an eval call, the caller-visible environment becomes dynamic: the +fragment may overwrite or unset existing locals, introduce new locals, declare +symbols, or mutate referenced storage. The checker therefore marks an eval +barrier, widens known local types to `Mixed`, discards callable/capture facts, +and permits later reads of variables and class-like symbols that may have been +created at runtime. + +This rule is intentionally conservative for literal strings too. The later EIR +planner may prove that a literal fragment can be AOT-lowered without runtime +scope or interpreter state, but that backend decision must not hide frontend +diagnostics or make pre-call type facts unsound. See +[Eval Runtime Architecture](eval-runtime.md). + ## Built-in function signatures **Files:** `src/types/checker/builtins/`, plus `src/types/checker/mod.rs` and `src/types/checker/inference/` for special expression forms such as `ExprKind::PtrCast`, `ExprKind::InstanceOf`, and `ExprKind::Print` diff --git a/docs/internals/what-is-a-compiler.md b/docs/internals/what-is-a-compiler.md index efa8866bee..3a7f3af65a 100644 --- a/docs/internals/what-is-a-compiler.md +++ b/docs/internals/what-is-a-compiler.md @@ -26,13 +26,13 @@ The key difference: an interpreter is always present at runtime, translating as ## What elephc does -elephc is a compiler. It takes PHP source code and produces a native binary for the supported targets. No PHP interpreter involved. The output is a standalone executable, just like a C program compiled with `gcc`. +elephc is a compiler. It takes PHP source code and produces a native binary for the supported targets. No external PHP interpreter is required. The output is a standalone executable, just like a C program compiled with `gcc`. ``` hello.php → elephc → hello (native executable) → runs directly on CPU ``` -The resulting binary has no dependency on PHP and no interpreter or VM. It includes elephc's emitted helper routines and links the platform's native system libraries for OS and libc services. +The resulting binary has no dependency on PHP, the Zend Engine, or an external VM. It includes elephc's emitted helper routines and links the platform's native system libraries for OS and libc services. Experimental `eval()` is a hybrid boundary: eligible literals are still AOT-compiled, while dynamic fragments embed the optional Magician interpreter bridge inside the executable. ## The phases of compilation @@ -74,6 +74,6 @@ elephc handles the first four phases. The last two (assembler and linker) are de PHP is normally interpreted, and that's fine for web servers. So why compile it? -Compiling PHP turns it into fast, standalone native binaries with no interpreter, VM, or runtime dependency — you ship a single executable. PHP is also a great language to compile: its syntax is simple enough to be tractable but rich enough to be interesting (strings, arrays, functions, control flow, type coercion), and millions of developers already know it. +Compiling PHP turns it into fast, standalone native binaries without an external PHP interpreter or VM — you ship a single executable. PHP is also a great language to compile: its syntax is simple enough to be tractable but rich enough to be interesting (strings, arrays, functions, control flow, type coercion), and millions of developers already know it. Programs that use experimental dynamic `eval()` may carry the optional embedded Magician bridge; the rest of the program remains native code. The fact that the output is *real assembly* means you can see exactly what the CPU does for every PHP construct. Many of the internals documents use AArch64 examples because that was the original backend and remains the most explanatory path, but the same compiler pipeline now targets more than one platform/architecture pair. `echo 1 + 2` isn't magic — it's a few data moves, an add, a call to a conversion routine, and a system call. You can trace every step. diff --git a/docs/php/arrays.md b/docs/php/arrays.md index 30b536715e..35f246ae75 100644 --- a/docs/php/arrays.md +++ b/docs/php/arrays.md @@ -284,4 +284,7 @@ PHP does not allow keyed and unkeyed entries in the same destructuring pattern, `usort()` and `uasort()` sort arrays of **objects** as well as scalars. The comparator receives each element as its object handle, so an unannotated comparator's parameters are typed from the array element automatically — `usort($items, fn($a, $b) => $a->weight <=> $b->weight)` works without writing `($a, $b)` type hints, and `usort($dates, fn($a, $b) => $a <=> $b)` over `DateTime`/`DateTimeImmutable` compares by instant. Explicit hints (`function (Item $a, Item $b)`) are equally accepted. Sorting an array of **strings** with a user comparator is not yet supported and reports a clear unsupported-feature error. -**Not supported by design:** `compact()`, `extract()` require runtime variable-name tables and are listed in the roadmap's "Will not implement" section. +**Not supported yet:** `compact()` and `extract()` need dynamic access to the +current variable scope. Magician's materialized named scope makes that behavior +feasible, but these functions are not wired into the compiler or interpreter +today. Use an associative array explicitly in portable elephc code. diff --git a/docs/php/builtins/class/get_called_class.md b/docs/php/builtins/class/get_called_class.md index 5d4988d857..15b96f8633 100644 --- a/docs/php/builtins/class/get_called_class.md +++ b/docs/php/builtins/class/get_called_class.md @@ -23,10 +23,3 @@ get_called_class() is available inside eval'd code via the magician interpreter; - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_called_class.rs)). _No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - - - diff --git a/docs/php/builtins/class/get_class_methods.md b/docs/php/builtins/class/get_class_methods.md index c87045dfb7..77d23d0780 100644 --- a/docs/php/builtins/class/get_class_methods.md +++ b/docs/php/builtins/class/get_class_methods.md @@ -24,10 +24,3 @@ get_class_methods() is available inside eval'd code via the magician interpreter - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_methods.rs)). _No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - - - diff --git a/docs/php/builtins/class/get_class_vars.md b/docs/php/builtins/class/get_class_vars.md index bd2e53dcfe..2fe580caf0 100644 --- a/docs/php/builtins/class/get_class_vars.md +++ b/docs/php/builtins/class/get_class_vars.md @@ -24,10 +24,3 @@ get_class_vars() is available inside eval'd code via the magician interpreter; c - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_class_vars.rs)). _No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - - - diff --git a/docs/php/builtins/class/get_object_vars.md b/docs/php/builtins/class/get_object_vars.md index 40cfdd3a0e..2b4a9ecfc1 100644 --- a/docs/php/builtins/class/get_object_vars.md +++ b/docs/php/builtins/class/get_object_vars.md @@ -24,10 +24,3 @@ get_object_vars() is available inside eval'd code via the magician interpreter; - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/symbols/get_object_vars.rs)). _No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - - - diff --git a/docs/php/builtins/misc/buffer_new.md b/docs/php/builtins/misc/buffer_new.md index 34580c141e..ca48729c20 100644 --- a/docs/php/builtins/misc/buffer_new.md +++ b/docs/php/builtins/misc/buffer_new.md @@ -24,10 +24,3 @@ function buffer_new(int $length): mixed - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/raw_memory/buffer_new.rs)). _No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - - - diff --git a/docs/php/builtins/process/die.md b/docs/php/builtins/process/die.md index 3b8f3fd6db..0dfb3e0148 100644 --- a/docs/php/builtins/process/die.md +++ b/docs/php/builtins/process/die.md @@ -24,10 +24,3 @@ function die(int $status): void - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/die.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/die.rs)). _No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - - - diff --git a/docs/php/builtins/process/exit.md b/docs/php/builtins/process/exit.md index 5d84057249..d29169c8cf 100644 --- a/docs/php/builtins/process/exit.md +++ b/docs/php/builtins/process/exit.md @@ -24,10 +24,3 @@ function exit(int $status): void - **`eval()` (magician interpreter)**: supported — declarative interpreter builtin ([`crates/elephc-magician/src/interpreter/builtins/core/exit.rs`](https://github.com/illegalstudio/elephc/blob/main/crates/elephc-magician/src/interpreter/builtins/core/exit.rs)). _No examples yet — check `examples/` and `showcases/` for usage patterns._ - - - - - - - diff --git a/docs/php/eval.md b/docs/php/eval.md index be14eb855d..9df120a23f 100644 --- a/docs/php/eval.md +++ b/docs/php/eval.md @@ -1,20 +1,82 @@ --- title: "Eval" -description: "Runtime PHP fragment evaluation, dynamic scope synchronization, supported EvalIR subset, and current limitations." +description: "Experimental PHP eval support, including literal AOT lowering, dynamic interpreter fallback, scope synchronization, safety, and limitations." sidebar: order: 5 --- -`eval($code): mixed` parses and executes a PHP fragment at runtime in the -caller-visible local scope. It is a PHP language construct, not a normal -callable: `function_exists("eval")` and `is_callable("eval")` return `false`, +`eval($code): mixed` evaluates a PHP fragment at the call site in the +caller-visible local scope. Dynamic source is parsed at runtime; eligible +string literals may be parsed and lowered ahead of time. It is a PHP language +construct, not a normal callable: `function_exists("eval")` and +`is_callable("eval")` return `false`, and first-class callable syntax for `eval` is rejected. -Programs that call `eval()` link the optional `elephc_magician` bridge. Programs -that do not use `eval()` keep the ordinary fully native runtime path and do not -link the bridge. +> **Experimental:** Eval support is still evolving. The supported fragment +> surface and the boundary between AOT lowering and interpreter fallback may +> change between releases. + +> **Security:** `eval()` is not a sandbox. Evaluated code can access the caller's +> state and the host-visible filesystem, environment, process, and network +> facilities exposed by supported builtins. Never evaluate untrusted input. The evaluated string must be a PHP fragment without an opening ` EIR -> native code | No eval context and no Magician bridge | +| Eligible string literal that needs only known scope reads/writes | AOT-lowered with direct locals or core eval-scope helpers | Eval scope only; no interpreter bridge | +| Dynamic string, runtime declaration, include, reference, dynamic dispatch, or unsupported literal construct | Parsed into EvalIR and interpreted at runtime | Persistent eval context, synchronized scopes, and `elephc_magician` | + +The compiler makes this decision per literal call. A program may therefore use +`eval()` without linking Magician when every call is fully handled by the AOT +paths. A program that needs interpreter fallback links the optional static +bridge into the standalone binary; programs without that requirement do not. +`--with-eval` force-links the bridge for testing or indirect use, but it is not +required to enable the language construct. + +The AOT and fallback paths are covered on macOS ARM64, Linux ARM64, and Linux +x86_64. CI runs dedicated eval integration shards for every supported target. + +## Performance + +A fully native literal fragment has no runtime parsing or interpreter-dispatch +cost and does not increase the binary with Magician. Scope-backed AOT adds only +the materialization required for the statically known reads and writes. + +Interpreter fallback parses dynamic source into EvalIR. Exact source strings +up to 64 KiB share a process-wide FIFO cache of 256 immutable parse results; +larger fragments bypass the cache. The cache avoids repeated tokenization and +parsing, but execution remains interpreted and scope/context state is never +cached. Force-linking with `--with-eval` also increases binary size even if no +call ultimately requires fallback. + +## Quick start + +```php + EIR -> native codegen pipeline used for ordinary elephc source. -Unsupported constructs and missing class names during eval object construction -fail at runtime with an eval fatal diagnostic. +Dynamic fragments and literal fragments outside the current AOT eligibility +rules execute through the `elephc_magician` interpreter bridge. Eligible +literal fragments instead use the normal AST -> EIR -> native codegen pipeline, +either with direct caller locals or with core eval-scope helpers. Unsupported +constructs that reach the interpreter, and missing class names during eval +object construction, fail at runtime with an eval fatal diagnostic. The fragment subset is broad but not the full elephc language surface. Eval retains `ReflectionFunction` closure metadata for common @@ -974,7 +1040,8 @@ by-reference free-function bridge shapes beyond the current Mixed/scalar/string/one-word heap slice remain metadata-only rather than invocable through eval. -Because `eval()` is a dynamic barrier, the compiler must be conservative after -an eval call. Values that cross the barrier may be widened to boxed `Mixed` -storage internally, and optimizer/type facts from before the call cannot be -blindly reused afterward. +The type checker and AST optimizer conservatively treat every `eval()` call as +a dynamic barrier: local facts are widened, constant propagation is invalidated, +and pre-call facts cannot be blindly reused afterward. Later EIR planning may +prove that a literal fragment can run natively and omit the runtime barrier; +values that actually cross a dynamic barrier use boxed `Mixed` storage. diff --git a/docs/php/types.md b/docs/php/types.md index a9d186486d..f80021fe1f 100644 --- a/docs/php/types.md +++ b/docs/php/types.md @@ -240,7 +240,11 @@ Narrowing applies to function and method parameters. A parameter whose call site - Plain array numeric casts (`(int)$array`, `(float)$array`) follow elephc's existing array cast semantics (return the element count rather than PHP's `0`/`1`). Direct `iterable` numeric casts use PHP's empty/non-empty `0`/`1` semantics. - `__destruct` runs when an object's refcount reaches zero (scope exit, reassignment, `unset`, program end), matching PHP's timing, but **object resurrection is not supported**: re-storing `$this` so the object would outlive the destructor does not keep it alive — the object is still freed once `__destruct` returns. - Under the compatibility `--null-repr=sentinel` opt-out, the integer `9223372036854775806` (`PHP_INT_MAX - 1`) collides with elephc's internal null marker in unboxed scalar slots and is misread as `null` by `echo`, `var_dump()`, `is_null()`, `??`, and related null checks. The default tagged null representation does not have this collision: the full 64-bit integer range round-trips. -- Variable variables (`$$name`, `${$expr}`) are not supported. elephc allocates each local to a fixed compile-time stack slot and keeps no per-frame variable-name table, so a variable whose name is computed at runtime cannot be resolved. Use an array keyed by the dynamic name instead. +- Variable variables (`$$name`, `${$expr}`) are not supported yet. Native AOT + locals use fixed compile-time stack slots; supporting a runtime-computed name + will require routing the access through Magician's materialized named scope + and synchronizing the affected native locals. Use an array keyed by the + dynamic name in portable elephc code for now. - Reference aliases to array elements (`$b =& $a[0]`) are limited to **indexed arrays with integer indices**; associative arrays (`$b =& $a['key']`) are rejected at compile time. Referencing an out-of-range index binds the alias to a null cell instead of creating the element (PHP autovivifies it as `null`), and the alias points into the array's storage, so it is only valid while the array is alive and not reallocated by growth. - `print_r($value, true)` captures into a fixed 64 KiB buffer: rendered output longer than 65536 bytes is truncated at the cap (PHP returns the full string). Echo mode (`print_r($value)`) is unaffected. - `serialize()`/`unserialize()` cover scalars, arrays, and objects (including the `__serialize`/`__unserialize`/`__sleep`/`__wakeup` magic methods and `r:`/`R:` object back-references) byte-for-byte compatibly with PHP. Remaining gaps: a cyclic reference inside an object's own properties resolves to `null` on `unserialize()` (serialization handles cycles), the deprecated `Serializable` interface (`C:` wire form) is unsupported, writing a property of an unserialized object held in a `Mixed` does not persist (a separate `Mixed` property-write limitation), and `unserialize()` does not emit PHP's `E_WARNING` / `E_NOTICE` on malformed input — it just returns `false`. diff --git a/scripts/docs/elephc_builtins/render.py b/scripts/docs/elephc_builtins/render.py index f59352c110..3fc5004d99 100644 --- a/scripts/docs/elephc_builtins/render.py +++ b/scripts/docs/elephc_builtins/render.py @@ -302,7 +302,7 @@ def render_user(b: dict, order: int, repo_root: Path) -> str: _ = repo_root # reserved for future cross-repo links area_lower = b['area'].lower() article = "an" if area_lower[0] in "aeiou" else "a" - return USER_TEMPLATE.format( + rendered = USER_TEMPLATE.format( name=b["name"], short_description=_short_description(b).replace('"', '\\"'), area=b["area"], @@ -319,6 +319,11 @@ def render_user(b: dict, order: int, repo_root: Path) -> str: see_also_section=_see_also_section(b), internals_link=_internals_link(b), ) + # Empty optional sections can stack several blank lines. Preserve the + # established output otherwise, but collapse pathological trailing runs. + if rendered.endswith("\n\n\n"): + return rendered.rstrip() + "\n" + return rendered def render_internals(b: dict, order: int, repo_root: Path) -> str: